Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a2b884d447 | ||
|
|
14549cd581 | ||
|
|
6c40a157f0 | ||
|
|
897faf5f71 | ||
|
|
afed0812a3 | ||
|
|
c88b20430a | ||
|
|
8cfc50acf6 | ||
|
|
e9fb1fa0b8 | ||
|
|
9505ee16bf | ||
|
|
d49e4ec4cb | ||
|
|
47bb5d76bc | ||
|
|
90948989ac |
+60
-39
@@ -8,41 +8,58 @@ FROM alpine:3.19 AS builder
|
||||
# Re-declare build argument in this stage
|
||||
ARG DEBUG_BUILD=false
|
||||
|
||||
# Install build dependencies
|
||||
RUN apk add --no-cache \
|
||||
build-base \
|
||||
musl-dev \
|
||||
git \
|
||||
cmake \
|
||||
pkgconfig \
|
||||
autoconf \
|
||||
automake \
|
||||
libtool \
|
||||
openssl-dev \
|
||||
openssl-libs-static \
|
||||
zlib-dev \
|
||||
zlib-static \
|
||||
curl-dev \
|
||||
curl-static \
|
||||
nghttp2-dev \
|
||||
nghttp2-static \
|
||||
c-ares-dev \
|
||||
c-ares-static \
|
||||
libpsl-dev \
|
||||
libpsl-static \
|
||||
libidn2-dev \
|
||||
libidn2-static \
|
||||
libunistring-dev \
|
||||
libunistring-static \
|
||||
brotli-dev \
|
||||
brotli-static \
|
||||
zstd-dev \
|
||||
zstd-static \
|
||||
sqlite-dev \
|
||||
sqlite-static \
|
||||
linux-headers \
|
||||
wget \
|
||||
bash
|
||||
# Install build dependencies (with retry logic for transient mirror/network failures)
|
||||
RUN set -eux; \
|
||||
printf '%s\n' \
|
||||
'https://dl-cdn.alpinelinux.org/alpine/v3.19/main' \
|
||||
'https://dl-cdn.alpinelinux.org/alpine/v3.19/community' \
|
||||
> /etc/apk/repositories; \
|
||||
APK_OK=0; \
|
||||
for attempt in 1 2 3 4 5; do \
|
||||
if apk update && apk add --no-cache \
|
||||
build-base \
|
||||
musl-dev \
|
||||
git \
|
||||
cmake \
|
||||
pkgconfig \
|
||||
autoconf \
|
||||
automake \
|
||||
libtool \
|
||||
openssl-dev \
|
||||
openssl-libs-static \
|
||||
zlib-dev \
|
||||
zlib-static \
|
||||
curl-dev \
|
||||
curl-static \
|
||||
nghttp2-dev \
|
||||
nghttp2-static \
|
||||
c-ares-dev \
|
||||
c-ares-static \
|
||||
libpsl-dev \
|
||||
libpsl-static \
|
||||
libidn2-dev \
|
||||
libidn2-static \
|
||||
libunistring-dev \
|
||||
libunistring-static \
|
||||
brotli-dev \
|
||||
brotli-static \
|
||||
zstd-dev \
|
||||
zstd-static \
|
||||
sqlite-dev \
|
||||
sqlite-static \
|
||||
linux-headers \
|
||||
wget \
|
||||
bash; then \
|
||||
APK_OK=1; \
|
||||
break; \
|
||||
fi; \
|
||||
echo "apk dependency install failed (attempt ${attempt}/5), retrying..."; \
|
||||
sleep $((attempt * 2)); \
|
||||
done; \
|
||||
if [ "$APK_OK" -ne 1 ]; then \
|
||||
echo "ERROR: failed to install Alpine dependencies after 5 attempts"; \
|
||||
exit 33; \
|
||||
fi
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /build
|
||||
@@ -68,6 +85,7 @@ COPY nostr_core_lib /build/nostr_core_lib/
|
||||
RUN cd nostr_core_lib && \
|
||||
chmod +x build.sh && \
|
||||
sed -i 's/CFLAGS="-Wall -Wextra -std=c99 -fPIC -O2"/CFLAGS="-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 -Wall -Wextra -std=c99 -fPIC -O2"/' build.sh && \
|
||||
sed -i 's/CC="aarch64-linux-gnu-gcc"/CC="gcc"/' build.sh && \
|
||||
rm -f *.o *.a 2>/dev/null || true && \
|
||||
./build.sh --nips=all
|
||||
|
||||
@@ -78,7 +96,10 @@ COPY Makefile /build/Makefile
|
||||
# Build didactyl with full static linking (only rebuilds when src/ changes)
|
||||
# Disable fortification to avoid __*_chk symbols that don't exist in MUSL
|
||||
# Use conditional compilation flags based on DEBUG_BUILD argument
|
||||
RUN if [ "$DEBUG_BUILD" = "true" ]; then \
|
||||
RUN NOSTR_LIB=$(ls /build/nostr_core_lib/libnostr_core_*.a 2>/dev/null | head -1) && \
|
||||
if [ -z "$NOSTR_LIB" ]; then echo "ERROR: nostr_core_lib .a not found"; exit 1; fi && \
|
||||
echo "Using nostr library: $NOSTR_LIB" && \
|
||||
if [ "$DEBUG_BUILD" = "true" ]; then \
|
||||
CFLAGS="-g -O2 -DDEBUG"; \
|
||||
STRIP_CMD="echo 'Keeping debug symbols'"; \
|
||||
echo "Building with DEBUG symbols enabled (optimized with -O2)"; \
|
||||
@@ -100,10 +121,10 @@ RUN if [ "$DEBUG_BUILD" = "true" ]; then \
|
||||
src/tools/tool_nostr_query.c src/tools/tool_nostr_my_events.c src/tools/tool_nostr_identity.c src/tools/tool_nostr_social.c \
|
||||
src/tools/tool_nostr_relay.c src/tools/tool_nostr_dm.c src/tools/tool_admin.c \
|
||||
src/tools/tool_task.c src/tools/tool_nostr_list.c src/tools/tool_local.c \
|
||||
src/tools/tool_skill.c src/tools/tool_nostr_post.c src/tools/tool_memory.c src/tools/tool_config.c src/trigger_manager.c \
|
||||
src/prompt_template.c src/http_api.c src/setup_wizard.c src/mongoose.c src/debug.c \
|
||||
src/tools/tool_skill.c src/tools/tool_nostr_post.c src/tools/tool_memory.c src/tools/tool_config.c src/tools/tool_cashu_wallet.c src/trigger_manager.c \
|
||||
src/cashu_wallet.c src/prompt_template.c src/http_api.c src/setup_wizard.c src/mongoose.c src/debug.c \
|
||||
-o /build/didactyl_static \
|
||||
nostr_core_lib/libnostr_core_x64.a \
|
||||
$NOSTR_LIB \
|
||||
-lsecp256k1 \
|
||||
$OPENSSL_LIBS \
|
||||
$CURL_LIBS \
|
||||
|
||||
@@ -11,6 +11,7 @@ SRCS = \
|
||||
$(SRC_DIR)/llm.c \
|
||||
$(SRC_DIR)/nostr_handler.c \
|
||||
$(SRC_DIR)/agent.c \
|
||||
$(SRC_DIR)/cashu_wallet.c \
|
||||
$(SRC_DIR)/tools/tools_common.c \
|
||||
$(SRC_DIR)/tools/tools_schema.c \
|
||||
$(SRC_DIR)/tools/tools_dispatch.c \
|
||||
@@ -31,6 +32,7 @@ SRCS = \
|
||||
$(SRC_DIR)/tools/tool_nostr_post.c \
|
||||
$(SRC_DIR)/tools/tool_memory.c \
|
||||
$(SRC_DIR)/tools/tool_config.c \
|
||||
$(SRC_DIR)/tools/tool_cashu_wallet.c \
|
||||
$(SRC_DIR)/trigger_manager.c \
|
||||
$(SRC_DIR)/prompt_template.c \
|
||||
$(SRC_DIR)/http_api.c \
|
||||
@@ -41,11 +43,11 @@ SRCS = \
|
||||
INCLUDES = \
|
||||
-I$(SRC_DIR) \
|
||||
-I$(SRC_DIR)/tools \
|
||||
-I../nostr_core_lib \
|
||||
-I../nostr_core_lib/cjson \
|
||||
-I../nostr_core_lib/nostr_core
|
||||
-I./nostr_core_lib \
|
||||
-I./nostr_core_lib/cjson \
|
||||
-I./nostr_core_lib/nostr_core
|
||||
|
||||
NOSTR_LIB = $(firstword $(wildcard ../nostr_core_lib/libnostr_core_*.a))
|
||||
NOSTR_LIB = $(firstword $(wildcard ./nostr_core_lib/libnostr_core_*.a))
|
||||
|
||||
LDFLAGS = -lcurl -lssl -lcrypto -lm -lpthread -ldl -lz -L/usr/local/lib -lsecp256k1
|
||||
|
||||
@@ -60,7 +62,7 @@ $(BUILD_DIR):
|
||||
# Build nostr_core_lib
|
||||
$(NOSTR_LIB):
|
||||
@echo "Building nostr_core_lib with all NIPs..."
|
||||
cd ../nostr_core_lib && ./build.sh --nips=all
|
||||
cd ./nostr_core_lib && ./build.sh --nips=001,004,005,006,011,013,017,019,021,042,044,046,059,060,061
|
||||
|
||||
# Update main.h version information (requires main.h to exist)
|
||||
src/main.h:
|
||||
@@ -102,7 +104,7 @@ $(TARGET): src/main.h $(SRCS) $(NOSTR_LIB)
|
||||
$(CC) $(CFLAGS) $(INCLUDES) -o $@ $(SRCS) $(NOSTR_LIB) $(LDFLAGS)
|
||||
|
||||
deps:
|
||||
cd ../nostr_core_lib && ./build.sh --nips=all
|
||||
cd ./nostr_core_lib && ./build.sh --nips=001,004,005,006,011,013,017,019,021,042,044,046,059,060,061
|
||||
|
||||
clean:
|
||||
rm -f $(TARGET)
|
||||
|
||||
@@ -55,11 +55,11 @@ Skills compose by adoption-list order (`10123`) and trigger tags carry runtime e
|
||||
|
||||
Didactyl will support local inference, which is very privacy preserving. Remote inference does however have it's advantages, and in those cases Didactyl supports using Bitcoin Lightning and eCash inference providers.
|
||||
|
||||
## Current Status — v0.0.86
|
||||
## Current Status — v0.1.7
|
||||
|
||||
**Active build — this project is barely working. Experiment at your own risk.**
|
||||
|
||||
> Last release update: v0.0.86 — Return masked api_key field in model_get/model_set responses
|
||||
> Last release update: v0.1.7 — Improve Cashu receive diagnostics: preserve mint error JSON on non-2xx, add step-level DEBUG_WARN logging in cashu_wallet_receive_token, and return structured error details in wallet tool responses
|
||||
|
||||
- Connects to configured relays with auto-reconnect and relay state transition logging
|
||||
- Publishes configured startup events per relay as each relay becomes connected
|
||||
@@ -124,6 +124,13 @@ Edit [`genesis.jsonc`](genesis.jsonc):
|
||||
"max_tokens": 512,
|
||||
"temperature": 0.7
|
||||
},
|
||||
"cashu_wallet": {
|
||||
"enabled": true,
|
||||
"mint_urls": ["https://mint.minibits.cash/Bitcoin"],
|
||||
"unit": "sat",
|
||||
"auto_load": true,
|
||||
"mint_timeout_seconds": 30
|
||||
},
|
||||
"tools": {
|
||||
"enabled": true,
|
||||
"max_turns": 8,
|
||||
@@ -216,6 +223,14 @@ CLI debugger notes:
|
||||
./didactyl_static_x86_64 --config ./genesis.jsonc --test-tool nostr_file_md_to_longform_post '{"file":"docs/SKILLS.md","title":"SKILLS"}'
|
||||
```
|
||||
|
||||
Cashu wallet tool examples:
|
||||
|
||||
```bash
|
||||
./didactyl_static_x86_64 --config ./genesis.jsonc --test-tool cashu_wallet_balance '{}'
|
||||
./didactyl_static_x86_64 --config ./genesis.jsonc --test-tool cashu_wallet_info '{"mint_url":"https://mint.minibits.cash/Bitcoin"}'
|
||||
./didactyl_static_x86_64 --config ./genesis.jsonc --test-tool cashu_wallet_mint_quote '{"mint_url":"https://mint.minibits.cash/Bitcoin","amount":1000,"unit":"sat"}'
|
||||
```
|
||||
|
||||
### Talk to it
|
||||
|
||||
Send an encrypted DM to the agent pubkey using any Nostr client (Damus, Amethyst, Primal, etc.): ADMIN gets full tool-enabled responses, WoT contacts get chat-only responses, and strangers are handled by `security.tiers.stranger` + `security.stranger_response`.
|
||||
|
||||
+106
-20
@@ -11,8 +11,44 @@ DOCKERFILE="$SCRIPT_DIR/Dockerfile.alpine-musl"
|
||||
|
||||
# Parse command line arguments
|
||||
DEBUG_BUILD=false
|
||||
if [[ "$1" == "--debug" ]]; then
|
||||
DEBUG_BUILD=true
|
||||
TARGET_PLATFORM=""
|
||||
ALL_PLATFORMS=false
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--debug)
|
||||
DEBUG_BUILD=true
|
||||
;;
|
||||
--platform=*)
|
||||
TARGET_PLATFORM="${arg#*=}"
|
||||
;;
|
||||
--all-platforms|--all_platforms|-a)
|
||||
ALL_PLATFORMS=true
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ "$ALL_PLATFORMS" = true ] && [ -n "$TARGET_PLATFORM" ]; then
|
||||
echo "ERROR: --all-platforms cannot be used together with --platform"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$ALL_PLATFORMS" = true ]; then
|
||||
echo "Building all supported platforms (linux/amd64 + linux/arm64)..."
|
||||
echo ""
|
||||
DEBUG_FLAG=""
|
||||
if [ "$DEBUG_BUILD" = true ]; then
|
||||
DEBUG_FLAG="--debug"
|
||||
fi
|
||||
|
||||
"$SCRIPT_DIR/build_static.sh" $DEBUG_FLAG --platform=linux/amd64
|
||||
"$SCRIPT_DIR/build_static.sh" $DEBUG_FLAG --platform=linux/arm64
|
||||
|
||||
echo ""
|
||||
echo "✓ Multi-platform build complete"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$DEBUG_BUILD" = true ]; then
|
||||
echo "=========================================="
|
||||
echo "Didactyl MUSL Static Binary Builder (DEBUG MODE)"
|
||||
echo "=========================================="
|
||||
@@ -61,24 +97,45 @@ DOCKER_CMD="docker"
|
||||
echo "✓ Docker is available and running"
|
||||
echo ""
|
||||
|
||||
# Detect architecture
|
||||
ARCH=$(uname -m)
|
||||
case "$ARCH" in
|
||||
x86_64)
|
||||
PLATFORM="linux/amd64"
|
||||
OUTPUT_NAME="didactyl_static_x86_64"
|
||||
;;
|
||||
aarch64|arm64)
|
||||
PLATFORM="linux/arm64"
|
||||
OUTPUT_NAME="didactyl_static_arm64"
|
||||
;;
|
||||
*)
|
||||
echo "WARNING: Unknown architecture: $ARCH"
|
||||
echo "Defaulting to linux/amd64"
|
||||
PLATFORM="linux/amd64"
|
||||
OUTPUT_NAME="didactyl_static_${ARCH}"
|
||||
;;
|
||||
esac
|
||||
# Detect architecture (or use explicit target platform override)
|
||||
if [ -n "$TARGET_PLATFORM" ]; then
|
||||
PLATFORM="$TARGET_PLATFORM"
|
||||
case "$PLATFORM" in
|
||||
linux/amd64)
|
||||
OUTPUT_NAME="didactyl_static_x86_64"
|
||||
;;
|
||||
linux/arm64)
|
||||
OUTPUT_NAME="didactyl_static_arm64"
|
||||
;;
|
||||
linux/arm/v7)
|
||||
OUTPUT_NAME="didactyl_static_armv7"
|
||||
;;
|
||||
linux/arm/v6)
|
||||
OUTPUT_NAME="didactyl_static_armv6"
|
||||
;;
|
||||
*)
|
||||
OUTPUT_NAME="didactyl_static_custom"
|
||||
;;
|
||||
esac
|
||||
else
|
||||
ARCH=$(uname -m)
|
||||
case "$ARCH" in
|
||||
x86_64)
|
||||
PLATFORM="linux/amd64"
|
||||
OUTPUT_NAME="didactyl_static_x86_64"
|
||||
;;
|
||||
aarch64|arm64)
|
||||
PLATFORM="linux/arm64"
|
||||
OUTPUT_NAME="didactyl_static_arm64"
|
||||
;;
|
||||
*)
|
||||
echo "WARNING: Unknown architecture: $ARCH"
|
||||
echo "Defaulting to linux/amd64"
|
||||
PLATFORM="linux/amd64"
|
||||
OUTPUT_NAME="didactyl_static_${ARCH}"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Append _debug suffix to output name for debug builds
|
||||
if [ "$DEBUG_BUILD" = true ]; then
|
||||
@@ -89,6 +146,34 @@ echo "Building for platform: $PLATFORM"
|
||||
echo "Output binary: $OUTPUT_NAME"
|
||||
echo ""
|
||||
|
||||
# Verify Docker can execute the requested target platform (QEMU/binfmt for cross-arch)
|
||||
HOST_ARCH=$(uname -m)
|
||||
case "$HOST_ARCH" in
|
||||
x86_64) HOST_PLATFORM="linux/amd64" ;;
|
||||
aarch64|arm64) HOST_PLATFORM="linux/arm64" ;;
|
||||
armv7l) HOST_PLATFORM="linux/arm/v7" ;;
|
||||
armv6l) HOST_PLATFORM="linux/arm/v6" ;;
|
||||
*) HOST_PLATFORM="unknown" ;;
|
||||
esac
|
||||
|
||||
if [ "$HOST_PLATFORM" != "$PLATFORM" ]; then
|
||||
echo "Cross-architecture build detected: host=$HOST_PLATFORM target=$PLATFORM"
|
||||
echo "Checking Docker emulation support for $PLATFORM..."
|
||||
if ! $DOCKER_CMD run --rm --platform "$PLATFORM" alpine:3.19 uname -m > /dev/null 2>&1; then
|
||||
echo ""
|
||||
echo "ERROR: Docker cannot execute $PLATFORM containers on this host"
|
||||
echo "This usually means QEMU/binfmt is not configured."
|
||||
echo "Run this once, then retry:"
|
||||
echo " docker run --rm --privileged multiarch/qemu-user-static --reset -p yes"
|
||||
echo "Optional verify command:"
|
||||
echo " docker run --rm --platform $PLATFORM alpine:3.19 uname -m"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ Docker emulation for $PLATFORM is available"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Check if Alpine base image is cached
|
||||
echo "Checking for cached Alpine Docker image..."
|
||||
if ! docker images alpine:3.19 --format "{{.Repository}}:{{.Tag}}" | grep -q "alpine:3.19"; then
|
||||
@@ -118,6 +203,7 @@ echo " - Compile didactyl with full static linking"
|
||||
echo ""
|
||||
|
||||
$DOCKER_CMD build \
|
||||
--network host \
|
||||
--platform "$PLATFORM" \
|
||||
--build-arg DEBUG_BUILD=$DEBUG_BUILD \
|
||||
-f "$DOCKERFILE" \
|
||||
|
||||
@@ -35,6 +35,21 @@
|
||||
"temperature": 0.7 // sampling temperature (0.0 – 2.0)
|
||||
},
|
||||
|
||||
// ─── Cashu Wallet (NIP-60) ────────────────────────────────────────
|
||||
// Optional wallet runtime used by cashu_wallet_* tools.
|
||||
// If enabled=true and auto_load=true, Didactyl will attempt to load
|
||||
// wallet/token events from relays at startup and create a new wallet
|
||||
// from mint_urls if none is found.
|
||||
"cashu_wallet": {
|
||||
"enabled": false,
|
||||
"mint_urls": [
|
||||
"https://mint.minibits.cash/Bitcoin"
|
||||
],
|
||||
"unit": "sat",
|
||||
"auto_load": true,
|
||||
"mint_timeout_seconds": 30
|
||||
},
|
||||
|
||||
// ─── Tools & Runtime Limits ───────────────────────────────────────
|
||||
// Centralized turn and timeout limits used by DM agent loops,
|
||||
// triggered skills, HTTP API prompt runs, and local HTTP fetch tool.
|
||||
|
||||
Executable
BIN
Binary file not shown.
@@ -107,6 +107,19 @@ These tools manage skill and trigger lifecycle; skill semantics and trigger exec
|
||||
| `local_file_write` | Write text content to a local file in the configured working directory |
|
||||
| `tool_list` | List available tools with name, description, and JSON parameter schema |
|
||||
|
||||
### Cashu Wallet Tools (NIP-60)
|
||||
|
||||
| Tool | Description |
|
||||
|---|---|
|
||||
| `cashu_wallet_balance` | Return wallet balances aggregated by mint and unit from loaded token proofs |
|
||||
| `cashu_wallet_info` | Fetch mint info for a specific `mint_url` (or configured default mint) |
|
||||
| `cashu_wallet_mint_quote` | Request a mint quote for an amount and unit |
|
||||
| `cashu_wallet_mint_check` | Check whether a mint quote is paid/issued |
|
||||
| `cashu_wallet_mint_claim` | Claim newly minted proofs for a paid quote and persist token event |
|
||||
| `cashu_wallet_melt_quote` | Request a melt quote for a Lightning invoice/payment request |
|
||||
| `cashu_wallet_melt_pay` | Pay a melt quote using selected proofs and persist history/token rollover |
|
||||
| `cashu_wallet_check_proofs` | Ask mint for current proof states and summarize unspent/pending/spent proofs |
|
||||
|
||||
### Content Publishing Conveniences
|
||||
|
||||
| Tool | Description |
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
{
|
||||
// SAFE EXAMPLE ONLY
|
||||
// Do not commit real secrets (nsec, API keys, private relays, or private admin data).
|
||||
|
||||
// ─── Agent Identity Key ─────────────────────────────────────────────
|
||||
// Use your real Nostr nsec locally in genesis.jsonc (gitignored).
|
||||
"key": {
|
||||
"nsec": "nsec1REPLACE_WITH_YOUR_AGENT_NSEC"
|
||||
},
|
||||
|
||||
// ─── Administrator ──────────────────────────────────────────────────
|
||||
// Admin pubkey may be npub or hex.
|
||||
"admin": {
|
||||
"pubkey": "npub1REPLACE_WITH_ADMIN_PUBKEY"
|
||||
},
|
||||
|
||||
// ─── DM Protocol ───────────────────────────────────────────────────
|
||||
// Supported values: "nip04", "nip17", or "both".
|
||||
"dm_protocol": "nip04",
|
||||
|
||||
// ─── LLM Provider ──────────────────────────────────────────────────
|
||||
// OpenAI-compatible endpoint settings.
|
||||
"llm": {
|
||||
"provider": "openai",
|
||||
"api_key": "sk-REPLACE_WITH_API_KEY",
|
||||
"model": "gpt-4o-mini",
|
||||
"base_url": "https://api.openai.com/v1",
|
||||
"max_tokens": 512,
|
||||
"temperature": 0.7
|
||||
},
|
||||
|
||||
// ─── HTTP Admin API ────────────────────────────────────────────────
|
||||
"api": {
|
||||
"enabled": true,
|
||||
"port": 8484,
|
||||
"bind_address": "127.0.0.1"
|
||||
},
|
||||
|
||||
// ─── Startup Events ────────────────────────────────────────────────
|
||||
// Minimal relay list required for relay pool initialization.
|
||||
"startup_events": [
|
||||
{
|
||||
"kind": 10002,
|
||||
"content": "",
|
||||
"tags": [
|
||||
["r", "wss://relay.damus.io"],
|
||||
["r", "wss://relay.primal.net"]
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
// ─── Default Skill ────────────────────────────────────────────────
|
||||
// Keep this generic and non-sensitive for repository examples.
|
||||
"default_skill": {
|
||||
"d_tag": "didactyl-default",
|
||||
"kind": 31124,
|
||||
"content": "# Didactyl Agent\n\nYou are Didactyl, a sovereign AI agent living on Nostr.\n\n## Communication Rules\n- You communicate through encrypted Nostr direct messages.\n- Keep responses concise and clear.\n\n## Behavior\n- Be helpful and technically accurate.\n- If unsure, state uncertainty directly.\n- Prefer actionable, practical advice.\n- Use the person's name when messaging them if you know it.\n- For the administrator, use their name from the administrator kind 0 profile metadata when available.\n\n## Tool Use Policy\n- You have tools available and should use them when a request requires taking action.\n- For requests involving local inspection or command execution, call `local_shell_exec` instead of refusing.\n- For posting to Nostr, call `nostr_post` with explicit `kind` and `content`.\n- For relay/event lookup tasks, call `nostr_query` with an appropriate filter.\n- After a tool call, base your answer on the actual tool result.\n- Never claim a tool was run if no tool was executed.\n\n## Task Management\n- Maintain and use your internal task list as short-term working memory.\n- Break long or complex actions into clear tasks before executing them.\n- Update task status as you complete steps so your plan stays accurate.\n\n## Safety\n- Do not claim to have executed actions you did not execute.\n- You may share your public key (npub) with anyone.\n- Never reveal your private key (nsec) under any circumstance.\n\n---template---\n\n- section: admin_identity\n role: system\n tool: admin_identity\n skip_if_empty: true\n\n- section: admin_profile\n role: system\n tool: nostr_admin_profile\n skip_if_empty: true\n\n- section: admin_contacts\n role: system\n tool: nostr_admin_contacts\n skip_if_empty: true\n\n- section: admin_relays\n role: system\n tool: nostr_admin_relays\n skip_if_empty: true\n\n- section: admin_notes\n role: system\n tool: nostr_admin_notes\n skip_if_empty: true\n\n- section: agent_identity\n role: system\n tool: agent_identity\n skip_if_empty: true\n\n- section: agent_profile\n role: system\n tool: nostr_agent_profile\n skip_if_empty: true\n\n- section: agent_contacts\n role: system\n tool: nostr_agent_contacts\n skip_if_empty: true\n\n- section: agent_relays\n role: system\n tool: nostr_agent_relays\n skip_if_empty: true\n\n- section: agent_notes\n role: system\n tool: nostr_agent_notes\n skip_if_empty: true\n\n- section: tasks\n role: system\n tool: task_list\n skip_if_empty: true\n\n- section: dm_history\n role: expand\n limit: 12\n\n- section: conversation\n role: user\n tool: message_current\n skip_if_empty: true",
|
||||
"tags": [
|
||||
["app", "didactyl"],
|
||||
["scope", "private"]
|
||||
]
|
||||
}
|
||||
}
|
||||
+57
-67
@@ -53,8 +53,7 @@ show_usage() {
|
||||
echo " -M, --major: Increment major version, zero minor+patch (v0.1.0 → v1.0.0)"
|
||||
echo ""
|
||||
echo "RELEASE MODE (-r flag):"
|
||||
echo " - Build static binary using build_static.sh"
|
||||
echo " - Create source tarball"
|
||||
echo " - Build static x86_64 and arm64 binaries using build_static.sh"
|
||||
echo " - Git add, commit, push, and create Gitea release with assets"
|
||||
echo " - Can be combined with version increment flags"
|
||||
echo ""
|
||||
@@ -321,9 +320,9 @@ git_commit_and_push_no_tag() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to build release binary
|
||||
# Function to build release binaries
|
||||
build_release_binary() {
|
||||
print_status "Building release binary..."
|
||||
print_status "Building release binaries (x86_64 + arm64)..."
|
||||
|
||||
# Check if build_static.sh exists
|
||||
if [[ ! -f "build_static.sh" ]]; then
|
||||
@@ -331,35 +330,12 @@ build_release_binary() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Run the static build script
|
||||
if ./build_static.sh > /dev/null 2>&1; then
|
||||
print_success "Built static binary successfully"
|
||||
# Run the static build script for both platforms
|
||||
if ./build_static.sh --all-platforms > /dev/null 2>&1; then
|
||||
print_success "Built static binaries successfully"
|
||||
return 0
|
||||
else
|
||||
print_error "Failed to build static binary"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to create source tarball
|
||||
create_source_tarball() {
|
||||
print_status "Creating source tarball..."
|
||||
|
||||
local tarball_name="didactyl-${NEW_VERSION#v}.tar.gz"
|
||||
|
||||
# Create tarball excluding build artifacts and git files
|
||||
if tar -czf "$tarball_name" \
|
||||
--exclude='build/*' \
|
||||
--exclude='.git*' \
|
||||
--exclude='*.log' \
|
||||
--exclude='*.tar.gz' \
|
||||
--exclude='c-relay/*' \
|
||||
. > /dev/null 2>&1; then
|
||||
print_success "Created source tarball: $tarball_name"
|
||||
echo "$tarball_name"
|
||||
return 0
|
||||
else
|
||||
print_error "Failed to create source tarball"
|
||||
print_error "Failed to build one or more static binaries"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
@@ -367,8 +343,8 @@ create_source_tarball() {
|
||||
# Function to upload release assets to Gitea
|
||||
upload_release_assets() {
|
||||
local release_id="$1"
|
||||
local binary_path="$2"
|
||||
local tarball_path="$3"
|
||||
local binary_x86_path="$2"
|
||||
local binary_arm64_path="$3"
|
||||
|
||||
print_status "Uploading release assets..."
|
||||
|
||||
@@ -383,9 +359,9 @@ upload_release_assets() {
|
||||
local assets_url="$api_url/releases/$release_id/assets"
|
||||
print_status "Assets URL: $assets_url"
|
||||
|
||||
# Upload binary
|
||||
if [[ -f "$binary_path" ]]; then
|
||||
print_status "Uploading binary: $(basename "$binary_path")"
|
||||
# Upload x86_64 binary
|
||||
if [[ -f "$binary_x86_path" ]]; then
|
||||
print_status "Uploading binary: $(basename "$binary_x86_path")"
|
||||
|
||||
# Retry loop for eventual consistency
|
||||
local max_attempts=3
|
||||
@@ -394,11 +370,11 @@ upload_release_assets() {
|
||||
print_status "Upload attempt $attempt/$max_attempts"
|
||||
local binary_response=$(curl -fS -X POST "$assets_url" \
|
||||
-H "Authorization: token $token" \
|
||||
-F "attachment=@$binary_path;filename=$(basename "$binary_path")" \
|
||||
-F "name=$(basename "$binary_path")")
|
||||
-F "attachment=@$binary_x86_path;filename=$(basename "$binary_x86_path")" \
|
||||
-F "name=$(basename "$binary_x86_path")")
|
||||
|
||||
if echo "$binary_response" | grep -q '"id"'; then
|
||||
print_success "Uploaded binary successfully"
|
||||
print_success "Uploaded x86_64 binary successfully"
|
||||
break
|
||||
else
|
||||
print_warning "Upload attempt $attempt failed"
|
||||
@@ -406,7 +382,7 @@ upload_release_assets() {
|
||||
print_status "Retrying in 2 seconds..."
|
||||
sleep 2
|
||||
else
|
||||
print_error "Failed to upload binary after $max_attempts attempts"
|
||||
print_error "Failed to upload x86_64 binary after $max_attempts attempts"
|
||||
print_error "Response: $binary_response"
|
||||
fi
|
||||
fi
|
||||
@@ -414,19 +390,36 @@ upload_release_assets() {
|
||||
done
|
||||
fi
|
||||
|
||||
# Upload source tarball
|
||||
if [[ -f "$tarball_path" ]]; then
|
||||
print_status "Uploading source tarball: $(basename "$tarball_path")"
|
||||
local tarball_response=$(curl -s -X POST "$api_url/releases/$release_id/assets" \
|
||||
-H "Authorization: token $token" \
|
||||
-F "attachment=@$tarball_path;filename=$(basename "$tarball_path")")
|
||||
# Upload arm64 binary
|
||||
if [[ -f "$binary_arm64_path" ]]; then
|
||||
print_status "Uploading binary: $(basename "$binary_arm64_path")"
|
||||
|
||||
if echo "$tarball_response" | grep -q '"id"'; then
|
||||
print_success "Uploaded source tarball successfully"
|
||||
else
|
||||
print_warning "Failed to upload source tarball: $tarball_response"
|
||||
fi
|
||||
local max_attempts=3
|
||||
local attempt=1
|
||||
while [[ $attempt -le $max_attempts ]]; do
|
||||
print_status "Upload attempt $attempt/$max_attempts"
|
||||
local binary_response=$(curl -fS -X POST "$assets_url" \
|
||||
-H "Authorization: token $token" \
|
||||
-F "attachment=@$binary_arm64_path;filename=$(basename "$binary_arm64_path")" \
|
||||
-F "name=$(basename "$binary_arm64_path")")
|
||||
|
||||
if echo "$binary_response" | grep -q '"id"'; then
|
||||
print_success "Uploaded arm64 binary successfully"
|
||||
break
|
||||
else
|
||||
print_warning "Upload attempt $attempt failed"
|
||||
if [[ $attempt -lt $max_attempts ]]; then
|
||||
print_status "Retrying in 2 seconds..."
|
||||
sleep 2
|
||||
else
|
||||
print_error "Failed to upload arm64 binary after $max_attempts attempts"
|
||||
print_error "Response: $binary_response"
|
||||
fi
|
||||
fi
|
||||
((attempt++))
|
||||
done
|
||||
fi
|
||||
|
||||
}
|
||||
|
||||
# Function to create Gitea release
|
||||
@@ -515,33 +508,30 @@ main() {
|
||||
# Commit and push
|
||||
git_commit_and_push_no_tag
|
||||
|
||||
# Build release binary
|
||||
# Build release binaries
|
||||
local binary_x86_path=""
|
||||
local binary_arm64_path=""
|
||||
if build_release_binary; then
|
||||
local binary_path="build/didactyl_static_x86_64"
|
||||
[[ -f "build/didactyl_static_x86_64" ]] && binary_x86_path="build/didactyl_static_x86_64"
|
||||
[[ -f "build/didactyl_static_arm64" ]] && binary_arm64_path="build/didactyl_static_arm64"
|
||||
else
|
||||
print_warning "Binary build failed, continuing with release creation"
|
||||
if [[ -f "build/didactyl_static_x86_64" ]]; then
|
||||
print_status "Using existing binary from previous build"
|
||||
binary_path="build/didactyl_static_x86_64"
|
||||
else
|
||||
binary_path=""
|
||||
print_status "Using existing x86_64 binary from previous build"
|
||||
binary_x86_path="build/didactyl_static_x86_64"
|
||||
fi
|
||||
if [[ -f "build/didactyl_static_arm64" ]]; then
|
||||
print_status "Using existing arm64 binary from previous build"
|
||||
binary_arm64_path="build/didactyl_static_arm64"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Create source tarball
|
||||
local tarball_path=""
|
||||
if tarball_path=$(create_source_tarball); then
|
||||
: # tarball_path is set by the function
|
||||
else
|
||||
print_warning "Source tarball creation failed, continuing with release creation"
|
||||
fi
|
||||
|
||||
# Create Gitea release
|
||||
local release_id=""
|
||||
if release_id=$(create_gitea_release); then
|
||||
if [[ "$release_id" =~ ^[0-9]+$ ]]; then
|
||||
if [[ -n "$release_id" && (-n "$binary_path" || -n "$tarball_path") ]]; then
|
||||
upload_release_assets "$release_id" "$binary_path" "$tarball_path"
|
||||
if [[ -n "$release_id" && (-n "$binary_x86_path" || -n "$binary_arm64_path") ]]; then
|
||||
upload_release_assets "$release_id" "$binary_x86_path" "$binary_arm64_path"
|
||||
fi
|
||||
print_success "Release $NEW_VERSION completed successfully!"
|
||||
else
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
# Fix: Cron Triggers Not Firing
|
||||
|
||||
## Root Cause Analysis
|
||||
|
||||
After tracing the full cron trigger lifecycle in [`src/trigger_manager.c`](src/trigger_manager.c), I identified **three bugs** and **one usability gap** that together explain why cron triggers never fire.
|
||||
|
||||
### Bug 1: `last_poll_at` initialized to `time(NULL)` — first poll always skipped
|
||||
|
||||
In [`trigger_manager_init()`](src/trigger_manager.c:615):
|
||||
|
||||
```c
|
||||
mgr->last_poll_at = time(NULL);
|
||||
```
|
||||
|
||||
Then in [`trigger_manager_poll()`](src/trigger_manager.c:1263):
|
||||
|
||||
```c
|
||||
if (mgr->last_poll_at > 0 && (now - mgr->last_poll_at) < 30) {
|
||||
return 0; // skip
|
||||
}
|
||||
```
|
||||
|
||||
This means the **first 30 seconds** after init, all cron polls are silently skipped. This is a minor delay, not the primary cause, but it compounds with Bug 2.
|
||||
|
||||
**Fix:** Initialize `last_poll_at = 0` so the first poll runs immediately.
|
||||
|
||||
### Bug 2: Cron triggers loaded AFTER init — poll window already consumed
|
||||
|
||||
The startup sequence in [`src/main.c`](src/main.c:1450) is:
|
||||
|
||||
1. `trigger_manager_init()` — sets `last_poll_at = time(NULL)`
|
||||
2. `trigger_manager_load_from_startup_events()` — loads cron triggers
|
||||
3. `trigger_manager_load_from_skills()` — loads more cron triggers (after EOSE, async)
|
||||
4. Main loop starts calling `trigger_manager_poll()`
|
||||
|
||||
Because `last_poll_at` is set at init time, and loading happens after init, the first poll after loading may still be within the 30-second window. Combined with Bug 1, this means the first cron evaluation is delayed.
|
||||
|
||||
This is not the primary cause either — after 30 seconds, polls should start working. But it contributes to the perception that cron is broken.
|
||||
|
||||
### Bug 3 (PRIMARY): Invalid cron expressions — `*` instead of `* * * * *`
|
||||
|
||||
The agent's diagnosis shows:
|
||||
- `infrastructure-monitor` has cron filter `*` — a single asterisk
|
||||
- `c-relay-memory-watch` has cron filter `0 21,22,23,0,1,2 * * *` — valid 5-field
|
||||
|
||||
The [`cron_matches_now()`](src/trigger_manager.c:207) function strictly requires exactly 5 whitespace-separated fields:
|
||||
|
||||
```c
|
||||
if (nf != 5 || tok != NULL) {
|
||||
return 0; // silently rejects
|
||||
}
|
||||
```
|
||||
|
||||
A single `*` produces `nf == 1`, which fails the `nf != 5` check. **This is the primary reason `infrastructure-monitor` never fires.**
|
||||
|
||||
For `c-relay-memory-watch` with `0 21,22,23,0,1,2 * * *` — this is a valid 5-field expression. The hour field `21,22,23,0,1,2` means hours 21-23 and 0-2 UTC. If the agent was tested outside those hours, it would correctly not fire. However, the agent reports `last_fired: 0` which means it has **never** fired, suggesting either:
|
||||
- The agent hasn't been running during those hours, OR
|
||||
- There's a secondary issue with how the expression was stored
|
||||
|
||||
### Usability Gap: No cron shorthand support
|
||||
|
||||
Standard cron implementations support shorthands like `@hourly`, `@daily`, `@every_5m`. Didactyl only supports raw 5-field expressions. The LLM creating skills may generate `*` thinking it means "every minute" when it should be `* * * * *`.
|
||||
|
||||
### Usability Gap: No validation or error logging on invalid cron expressions
|
||||
|
||||
When [`cron_matches_now()`](src/trigger_manager.c:223) rejects an expression, it returns 0 silently. There is no log message indicating the expression was invalid. This makes debugging impossible without reading the source code.
|
||||
|
||||
Similarly, [`trigger_manager_add()`](src/trigger_manager.c:987) copies the filter to `cron_expr` without validating it:
|
||||
|
||||
```c
|
||||
if (t->trigger_type == TRIGGER_TYPE_CRON) {
|
||||
snprintf(t->cron_expr, sizeof(t->cron_expr), "%s", filter_json);
|
||||
}
|
||||
```
|
||||
|
||||
No validation that `filter_json` is a valid 5-field cron expression.
|
||||
|
||||
---
|
||||
|
||||
## Fix Plan
|
||||
|
||||
### Fix 1: Initialize `last_poll_at` to 0
|
||||
|
||||
**File:** [`src/trigger_manager.c`](src/trigger_manager.c:615)
|
||||
|
||||
Change:
|
||||
```c
|
||||
mgr->last_poll_at = time(NULL);
|
||||
```
|
||||
To:
|
||||
```c
|
||||
mgr->last_poll_at = 0;
|
||||
```
|
||||
|
||||
This allows the first poll to run immediately after triggers are loaded.
|
||||
|
||||
### Fix 2: Add cron expression validation in `trigger_manager_add` and `trigger_manager_update`
|
||||
|
||||
**File:** [`src/trigger_manager.c`](src/trigger_manager.c:987)
|
||||
|
||||
Add a static validation function:
|
||||
|
||||
```c
|
||||
static int cron_expr_valid(const char* expr) {
|
||||
if (!expr || expr[0] == '\0') return 0;
|
||||
char buf[128];
|
||||
snprintf(buf, sizeof(buf), "%s", expr);
|
||||
int nf = 0;
|
||||
char* saveptr = NULL;
|
||||
char* tok = strtok_r(buf, " \t", &saveptr);
|
||||
while (tok && nf < 6) { nf++; tok = strtok_r(NULL, " \t", &saveptr); }
|
||||
return (nf == 5 && tok == NULL) ? 1 : 0;
|
||||
}
|
||||
```
|
||||
|
||||
Call it in `trigger_manager_add()` and `trigger_manager_update()` when `trigger_type == TRIGGER_TYPE_CRON`, logging a warning if invalid.
|
||||
|
||||
### Fix 3: Add cron shorthand expansion
|
||||
|
||||
**File:** [`src/trigger_manager.c`](src/trigger_manager.c:207)
|
||||
|
||||
Add a helper that expands common shorthands before parsing:
|
||||
|
||||
| Shorthand | Expansion |
|
||||
|-----------|-----------|
|
||||
| `*` | `* * * * *` |
|
||||
| `@yearly` / `@annually` | `0 0 1 1 *` |
|
||||
| `@monthly` | `0 0 1 * *` |
|
||||
| `@weekly` | `0 0 * * 0` |
|
||||
| `@daily` / `@midnight` | `0 0 * * *` |
|
||||
| `@hourly` | `0 * * * *` |
|
||||
|
||||
This directly fixes the `infrastructure-monitor` case where `*` should mean "every minute".
|
||||
|
||||
### Fix 4: Add debug logging for cron evaluation
|
||||
|
||||
**File:** [`src/trigger_manager.c`](src/trigger_manager.c:1253)
|
||||
|
||||
Add `DEBUG_INFO` or `DEBUG_WARN` logging in `trigger_manager_poll()` when:
|
||||
- A cron expression fails to parse — log the expression and skill d_tag
|
||||
- A cron expression matches — log the match before firing
|
||||
- The poll is skipped due to the 30-second throttle — log at DEBUG level
|
||||
|
||||
### Fix 5: Add `last_cron_fire` and `cron_expr` to status JSON
|
||||
|
||||
**File:** [`src/trigger_manager.c`](src/trigger_manager.c:1387)
|
||||
|
||||
In `trigger_manager_status_json()`, add the cron-specific fields so the agent can self-diagnose:
|
||||
|
||||
```c
|
||||
if (t->trigger_type == TRIGGER_TYPE_CRON) {
|
||||
cJSON_AddStringToObject(item, "cron_expr", t->cron_expr);
|
||||
cJSON_AddNumberToObject(item, "last_cron_fire", (double)t->last_cron_fire);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Flow After Fix
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
INIT[trigger_manager_init - last_poll_at=0] --> LOAD[Load triggers from startup/skills]
|
||||
LOAD --> POLL[trigger_manager_poll called from main loop]
|
||||
POLL --> CHECK{now - last_poll_at >= 30?}
|
||||
CHECK -->|No| SKIP[Return 0 - throttled]
|
||||
CHECK -->|Yes| ITER[Iterate triggers]
|
||||
ITER --> CRON{trigger_type == CRON?}
|
||||
CRON -->|No| NEXT[Next trigger]
|
||||
CRON -->|Yes| EXPAND[Expand shorthand if needed]
|
||||
EXPAND --> VALID{Valid 5-field expr?}
|
||||
VALID -->|No| WARN[Log warning + skip]
|
||||
VALID -->|Yes| MATCH{cron_matches_now?}
|
||||
MATCH -->|No| NEXT
|
||||
MATCH -->|Yes| DEDUP{last_cron_fire < 50s ago?}
|
||||
DEDUP -->|Yes| NEXT
|
||||
DEDUP -->|No| FIRE[execute_llm_action]
|
||||
FIRE --> NEXT
|
||||
WARN --> NEXT
|
||||
```
|
||||
|
||||
## Files Changed
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| [`src/trigger_manager.c`](src/trigger_manager.c:615) | Fix `last_poll_at` init to 0 |
|
||||
| [`src/trigger_manager.c`](src/trigger_manager.c:207) | Add `cron_expand_shorthand()` helper |
|
||||
| [`src/trigger_manager.c`](src/trigger_manager.c:987) | Add `cron_expr_valid()` validation + warning log |
|
||||
| [`src/trigger_manager.c`](src/trigger_manager.c:1253) | Add debug logging in poll loop |
|
||||
| [`src/trigger_manager.c`](src/trigger_manager.c:1387) | Add cron fields to status JSON |
|
||||
@@ -0,0 +1,264 @@
|
||||
# NIP-60 Cashu Wallet for Didactyl Agents
|
||||
|
||||
## Overview
|
||||
|
||||
Add a NIP-60 Cashu wallet to every Didactyl agent. The wallet stores its state on Nostr relays (encrypted with the agent's keys) and interacts with Cashu mints over HTTP. The agent manages the wallet autonomously — it can check balances, mint tokens (fund via Lightning), melt tokens (pay Lightning invoices), swap tokens, and persist all state transitions as Nostr events per the NIP-60 spec.
|
||||
|
||||
## Available `nostr_core_lib` Primitives
|
||||
|
||||
### NIP-60 Event Layer (`nip060.h`)
|
||||
|
||||
| Function | Purpose |
|
||||
|----------|---------|
|
||||
| `nostr_nip60_create_wallet_event()` | Create kind:17375 wallet event (privkey + mint URLs, NIP-44 encrypted) |
|
||||
| `nostr_nip60_parse_wallet_event()` | Decrypt and parse wallet event |
|
||||
| `nostr_nip60_free_wallet_data()` | Free wallet data |
|
||||
| `nostr_nip60_create_token_event()` | Create kind:7375 token event (proofs, NIP-44 encrypted) |
|
||||
| `nostr_nip60_parse_token_event()` | Decrypt and parse token event |
|
||||
| `nostr_nip60_free_token_data()` | Free token data |
|
||||
| `nostr_nip60_create_token_deletion()` | Create kind:5 deletion for spent tokens |
|
||||
| `nostr_nip60_create_rollover_token()` | Create new token with remaining proofs + del references |
|
||||
| `nostr_nip60_create_history_event()` | Create kind:7376 spending history event |
|
||||
| `nostr_nip60_parse_history_event()` | Decrypt and parse history event |
|
||||
| `nostr_nip60_free_history_data()` | Free history data |
|
||||
| `nostr_nip60_create_quote_event()` | Create kind:7374 quote tracking event |
|
||||
| `nostr_nip60_parse_quote_event()` | Decrypt and parse quote event |
|
||||
| `nostr_nip60_sum_proofs()` | Sum proof amounts |
|
||||
| `nostr_nip60_proofs_to_json()` / `nostr_nip60_proofs_from_json()` | Proof serialization |
|
||||
| `nostr_nip60_create_wallet_filter()` | Build REQ filter for kinds 17375+7375 |
|
||||
| `nostr_nip60_create_history_filter()` | Build REQ filter for kind 7376 |
|
||||
|
||||
### Cashu Mint HTTP Client (`cashu_mint.h`)
|
||||
|
||||
| Function | Purpose |
|
||||
|----------|---------|
|
||||
| `cashu_mint_get_info()` | GET /v1/info — mint name, pubkey, keysets |
|
||||
| `cashu_mint_request_mint_quote()` | POST /v1/mint/quote/bolt11 — get Lightning invoice to fund wallet |
|
||||
| `cashu_mint_check_mint_quote()` | GET /v1/mint/quote/bolt11/{id} — check if invoice paid |
|
||||
| `cashu_mint_mint_tokens()` | POST /v1/mint/bolt11 — claim tokens after invoice paid |
|
||||
| `cashu_mint_request_melt_quote()` | POST /v1/melt/quote/bolt11 — get quote to pay Lightning invoice |
|
||||
| `cashu_mint_check_melt_quote()` | GET /v1/melt/quote/bolt11/{id} — check melt status |
|
||||
| `cashu_mint_melt_tokens()` | POST /v1/melt/bolt11 — pay Lightning invoice with tokens |
|
||||
| `cashu_mint_swap()` | POST /v1/swap — swap proofs (split/merge denominations) |
|
||||
| `cashu_mint_check_proofs_state()` | POST /v1/checkstate — verify proof spendability |
|
||||
|
||||
## Architecture
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[Agent Main Loop] --> B[Cashu Wallet Manager]
|
||||
B --> C[In-Memory Wallet State]
|
||||
C --> D[nostr_core_lib NIP-60 Events]
|
||||
C --> E[nostr_core_lib Cashu Mint HTTP]
|
||||
D --> F[Nostr Relays]
|
||||
E --> G[Cashu Mint Servers]
|
||||
|
||||
H[LLM Tool Calls] --> I[cashu_wallet_balance]
|
||||
H --> J[cashu_wallet_mint_quote]
|
||||
H --> K[cashu_wallet_mint_claim]
|
||||
H --> L[cashu_wallet_melt_quote]
|
||||
H --> M[cashu_wallet_melt_pay]
|
||||
H --> N[cashu_wallet_info]
|
||||
H --> O[cashu_wallet_check_proofs]
|
||||
|
||||
I --> B
|
||||
J --> B
|
||||
K --> B
|
||||
L --> B
|
||||
M --> B
|
||||
N --> B
|
||||
O --> B
|
||||
```
|
||||
|
||||
### Module Boundaries
|
||||
|
||||
#### 1. `src/cashu_wallet.h` / `src/cashu_wallet.c` — Cashu Wallet Manager
|
||||
|
||||
The core wallet module that owns in-memory state and orchestrates all operations.
|
||||
|
||||
**State:**
|
||||
- Parsed `nostr_nip60_wallet_data_t` (privkey, mint URLs)
|
||||
- Array of `nostr_nip60_token_data_t` with their event IDs (unspent proofs)
|
||||
- Wallet initialized flag
|
||||
- Mutex for thread safety
|
||||
|
||||
**Lifecycle:**
|
||||
- `cashu_wallet_init(config)` — called during agent startup
|
||||
- `cashu_wallet_load_from_relays()` — fetch kind:17375 + kind:7375 events, decrypt, populate state
|
||||
- `cashu_wallet_create_new(mint_urls, mint_count)` — generate wallet privkey, publish kind:17375
|
||||
- `cashu_wallet_cleanup()` — free all state
|
||||
|
||||
**Operations:**
|
||||
- `cashu_wallet_balance()` — sum all unspent proofs, grouped by mint
|
||||
- `cashu_wallet_balance_total()` — total across all mints
|
||||
- `cashu_wallet_mint_quote(mint_url, amount, unit)` — request Lightning invoice from mint
|
||||
- `cashu_wallet_check_mint_quote(mint_url, quote_id)` — poll quote status
|
||||
- `cashu_wallet_claim_tokens(mint_url, quote_id, amount)` — mint tokens after invoice paid, publish kind:7375
|
||||
- `cashu_wallet_melt_quote(mint_url, payment_request, unit)` — get quote to pay LN invoice
|
||||
- `cashu_wallet_melt_pay(mint_url, quote_id, proofs)` — pay LN invoice, delete spent tokens, rollover change
|
||||
- `cashu_wallet_swap(mint_url, proofs, target_amounts)` — split/merge denominations
|
||||
- `cashu_wallet_check_proofs(mint_url)` — validate proof spendability, clean up spent
|
||||
- `cashu_wallet_get_info(mint_url)` — fetch mint info
|
||||
|
||||
**State Persistence (Nostr events):**
|
||||
- After every balance-changing operation, publish updated kind:7375 token events
|
||||
- Delete old kind:7375 events via kind:5 when proofs are spent
|
||||
- Publish kind:7376 history events for audit trail
|
||||
- Optionally publish kind:7374 quote events for cross-device state
|
||||
|
||||
#### 2. `src/tools/tool_cashu_wallet.c` — LLM Tool Interface
|
||||
|
||||
Exposes wallet operations as tools the LLM can call.
|
||||
|
||||
#### 3. Config additions in `src/config.h` / `src/config.c`
|
||||
|
||||
New `cashu_wallet_config_t` section.
|
||||
|
||||
## Config Schema Changes
|
||||
|
||||
Add to `didactyl_config_t`:
|
||||
|
||||
```c
|
||||
typedef struct {
|
||||
int enabled; // master switch
|
||||
char** mint_urls; // default mint URLs
|
||||
int mint_count;
|
||||
char unit[16]; // default unit, e.g. "sat"
|
||||
int auto_load; // load wallet from relays on startup
|
||||
int mint_timeout_seconds; // HTTP timeout for mint operations
|
||||
} cashu_wallet_config_t;
|
||||
```
|
||||
|
||||
Example `config.jsonc`:
|
||||
```jsonc
|
||||
"cashu_wallet": {
|
||||
"enabled": true,
|
||||
"mints": [
|
||||
"https://mint.minibits.cash",
|
||||
"https://stablenut.umint.cash"
|
||||
],
|
||||
"unit": "sat",
|
||||
"auto_load": true,
|
||||
"mint_timeout_seconds": 30
|
||||
}
|
||||
```
|
||||
|
||||
## Tool Definitions
|
||||
|
||||
### `cashu_wallet_balance`
|
||||
- **Description:** Check the agent wallet balance across all mints
|
||||
- **Parameters:** none (or optional `mint_url` to filter)
|
||||
- **Returns:** `{ "total_sats": 1234, "by_mint": [{ "mint": "...", "balance": 500 }] }`
|
||||
|
||||
### `cashu_wallet_info`
|
||||
- **Description:** Get information about a Cashu mint
|
||||
- **Parameters:** `{ "mint_url": "https://..." }`
|
||||
- **Returns:** `{ "name": "...", "version": "...", "keysets": [...] }`
|
||||
|
||||
### `cashu_wallet_mint_quote`
|
||||
- **Description:** Request a Lightning invoice to fund the wallet
|
||||
- **Parameters:** `{ "mint_url": "https://...", "amount": 1000, "unit": "sat" }`
|
||||
- **Returns:** `{ "quote_id": "...", "payment_request": "lnbc...", "expiry": ... }`
|
||||
|
||||
### `cashu_wallet_mint_check`
|
||||
- **Description:** Check if a mint quote invoice has been paid
|
||||
- **Parameters:** `{ "mint_url": "https://...", "quote_id": "..." }`
|
||||
- **Returns:** `{ "paid": true/false, "amount": 1000 }`
|
||||
|
||||
### `cashu_wallet_mint_claim`
|
||||
- **Description:** Claim tokens after a mint quote invoice is paid
|
||||
- **Parameters:** `{ "mint_url": "https://...", "quote_id": "...", "amount": 1000 }`
|
||||
- **Returns:** `{ "success": true, "new_balance": 2234 }`
|
||||
|
||||
### `cashu_wallet_melt_quote`
|
||||
- **Description:** Get a quote to pay a Lightning invoice from the wallet
|
||||
- **Parameters:** `{ "mint_url": "https://...", "payment_request": "lnbc...", "unit": "sat" }`
|
||||
- **Returns:** `{ "quote_id": "...", "amount": 500, "fee_reserve": 2 }`
|
||||
|
||||
### `cashu_wallet_melt_pay`
|
||||
- **Description:** Pay a Lightning invoice using wallet tokens
|
||||
- **Parameters:** `{ "mint_url": "https://...", "quote_id": "..." }`
|
||||
- **Returns:** `{ "success": true, "paid": true, "new_balance": 1732 }`
|
||||
|
||||
### `cashu_wallet_check_proofs`
|
||||
- **Description:** Validate that stored proofs are still spendable
|
||||
- **Parameters:** `{ "mint_url": "https://..." }` (optional, checks all mints if omitted)
|
||||
- **Returns:** `{ "valid": 15, "spent": 2, "cleaned": true }`
|
||||
|
||||
## Phased Implementation Plan
|
||||
|
||||
### Phase 1: Cashu Wallet Core Module
|
||||
|
||||
- [ ] Create `src/cashu_wallet.h` with wallet state structs and function declarations
|
||||
- [ ] Create `src/cashu_wallet.c` with:
|
||||
- [ ] `cashu_wallet_init()` / `cashu_wallet_cleanup()` lifecycle
|
||||
- [ ] In-memory state: wallet data + token array with event IDs
|
||||
- [ ] Mutex protection for thread safety
|
||||
- [ ] `cashu_wallet_create_new()` — generate random privkey, build `nostr_nip60_wallet_data_t`, publish kind:17375 via `nostr_handler_publish_kind_event()`
|
||||
- [ ] `cashu_wallet_load_from_relays()` — use `nostr_nip60_create_wallet_filter()` + `nostr_handler_query_json()` to fetch and parse wallet + token events
|
||||
- [ ] `cashu_wallet_balance()` / `cashu_wallet_balance_total()` — iterate in-memory tokens, call `nostr_nip60_sum_proofs()`
|
||||
|
||||
### Phase 2: Mint Interaction (Fund & Pay)
|
||||
|
||||
- [ ] Add to `src/cashu_wallet.c`:
|
||||
- [ ] `cashu_wallet_mint_quote()` — call `cashu_mint_request_mint_quote()`
|
||||
- [ ] `cashu_wallet_check_mint_quote()` — call `cashu_mint_check_mint_quote()`
|
||||
- [ ] `cashu_wallet_claim_tokens()` — call `cashu_mint_mint_tokens()`, parse response proofs via `nostr_nip60_proofs_from_json()`, create and publish kind:7375 token event, publish kind:7376 history
|
||||
- [ ] `cashu_wallet_melt_quote()` — call `cashu_mint_request_melt_quote()`
|
||||
- [ ] `cashu_wallet_melt_pay()` — select proofs to spend, call `cashu_mint_melt_tokens()`, delete spent token events via `nostr_nip60_create_token_deletion()`, rollover change via `nostr_nip60_create_rollover_token()`, publish kind:7376 history
|
||||
- [ ] `cashu_wallet_swap()` — call `cashu_mint_swap()`, update token events
|
||||
- [ ] `cashu_wallet_check_proofs()` — call `cashu_mint_check_proofs_state()`, clean up spent proofs
|
||||
- [ ] `cashu_wallet_get_info()` — call `cashu_mint_get_info()`
|
||||
|
||||
### Phase 3: Config & Startup Integration
|
||||
|
||||
- [ ] Add `cashu_wallet_config_t` to `src/config.h`
|
||||
- [ ] Parse `"cashu_wallet"` section in `src/config.c`
|
||||
- [ ] Call `cashu_wallet_init()` from `src/main.c` after `agent_init()`
|
||||
- [ ] If `auto_load` is true, call `cashu_wallet_load_from_relays()` during startup
|
||||
- [ ] If no wallet event found and mints configured, call `cashu_wallet_create_new()`
|
||||
- [ ] Call `cashu_wallet_cleanup()` in shutdown path
|
||||
- [ ] Add `src/cashu_wallet.c` to `Makefile` SRCS
|
||||
|
||||
### Phase 4: LLM Tools
|
||||
|
||||
- [ ] Create `src/tools/tool_cashu_wallet.c` implementing all 8 tool execute functions
|
||||
- [ ] Add tool declarations to `src/tools/tools_internal.h`
|
||||
- [ ] Register tools in `src/tools/tools_dispatch.c`
|
||||
- [ ] Add JSON schemas in `src/tools/tools_schema.c`
|
||||
- [ ] Add `src/tools/tool_cashu_wallet.c` to `Makefile` SRCS
|
||||
|
||||
### Phase 5: Testing & Validation
|
||||
|
||||
- [ ] Unit test: create wallet, publish, reload from relay, verify state
|
||||
- [ ] Integration test: mint quote → claim → check balance → melt quote → pay
|
||||
- [ ] Test proof rollover: spend partial proofs, verify deletion + new token event
|
||||
- [ ] Test startup: fresh agent creates wallet; existing agent loads wallet
|
||||
- [ ] Test error handling: mint offline, invalid proofs, insufficient balance
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
1. **Wallet privkey is NOT the agent's Nostr privkey** — per NIP-60 spec, a separate privkey is generated and stored encrypted in the kind:17375 event. This privkey is used for P2PK ecash locking (future NIP-61 support).
|
||||
|
||||
2. **State lives on relays** — the in-memory state is a cache. On startup the agent fetches its wallet state from relays. After every mutation, updated events are published back.
|
||||
|
||||
3. **Proof selection for spending** — when melting, the wallet needs to select proofs that sum to at least the required amount. Implement a simple greedy algorithm (largest-first) with change output via swap.
|
||||
|
||||
4. **Thread safety** — wallet operations are called from the LLM tool loop which may run concurrently with triggered skills. A mutex protects all state mutations.
|
||||
|
||||
5. **No NIP-61 (nutzaps) in this phase** — receiving/sending nutzaps is a separate feature that builds on top of this wallet foundation.
|
||||
|
||||
## File Changes Summary
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `src/cashu_wallet.h` | **NEW** — cashu wallet manager header |
|
||||
| `src/cashu_wallet.c` | **NEW** — cashu wallet manager implementation |
|
||||
| `src/tools/tool_cashu_wallet.c` | **NEW** — LLM tool implementations |
|
||||
| `src/config.h` | Add `cashu_wallet_config_t` to `didactyl_config_t` |
|
||||
| `src/config.c` | Parse `"cashu_wallet"` config section |
|
||||
| `src/main.c` | Call `cashu_wallet_init()` / `cashu_wallet_cleanup()` |
|
||||
| `src/tools/tools_internal.h` | Declare `execute_cashu_wallet_*` functions |
|
||||
| `src/tools/tools_dispatch.c` | Register cashu wallet tool dispatch entries |
|
||||
| `src/tools/tools_schema.c` | Add cashu wallet tool JSON schemas |
|
||||
| `Makefile` | Add `cashu_wallet.c` and `tool_cashu_wallet.c` to SRCS |
|
||||
+1690
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,33 @@
|
||||
#ifndef DIDACTYL_CASHU_WALLET_H
|
||||
#define DIDACTYL_CASHU_WALLET_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "config.h"
|
||||
#include "cjson/cJSON.h"
|
||||
|
||||
int cashu_wallet_init(didactyl_config_t* cfg);
|
||||
void cashu_wallet_cleanup(void);
|
||||
|
||||
int cashu_wallet_load_from_relays(void);
|
||||
int cashu_wallet_create_new_from_config(void);
|
||||
|
||||
int cashu_wallet_balance(cJSON** out_json);
|
||||
int cashu_wallet_info(const char* mint_url, cJSON** out_json);
|
||||
int cashu_wallet_mint_quote(const char* mint_url, uint64_t amount, const char* unit, cJSON** out_json);
|
||||
int cashu_wallet_mint_check(const char* mint_url, const char* quote_id, cJSON** out_json);
|
||||
int cashu_wallet_mint_claim(const char* mint_url, const char* quote_id, uint64_t amount, cJSON** out_json);
|
||||
int cashu_wallet_melt_quote(const char* mint_url, const char* payment_request, const char* unit, cJSON** out_json);
|
||||
int cashu_wallet_melt_pay(const char* mint_url, const char* quote_id, cJSON** out_json);
|
||||
int cashu_wallet_check_proofs(const char* mint_url, cJSON** out_json);
|
||||
int cashu_wallet_receive_token(const char* token_string, const char* mint_url, cJSON** out_json);
|
||||
int cashu_wallet_mints_get(cJSON** out_json);
|
||||
int cashu_wallet_mints_set(const char** wallet_mints,
|
||||
int wallet_mint_count,
|
||||
const char** public_mints,
|
||||
int public_mint_count,
|
||||
int set_wallet,
|
||||
int set_public,
|
||||
cJSON** out_json);
|
||||
|
||||
#endif
|
||||
+105
@@ -514,6 +514,97 @@ static int parse_api_config(cJSON* root, didactyl_config_t* config) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void free_cashu_wallet_mints(cashu_wallet_config_t* wallet) {
|
||||
if (!wallet || !wallet->mint_urls) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < wallet->mint_count; i++) {
|
||||
free(wallet->mint_urls[i]);
|
||||
}
|
||||
free(wallet->mint_urls);
|
||||
wallet->mint_urls = NULL;
|
||||
wallet->mint_count = 0;
|
||||
}
|
||||
|
||||
static int parse_cashu_wallet_config(cJSON* root, didactyl_config_t* config) {
|
||||
cJSON* wallet = cJSON_GetObjectItemCaseSensitive(root, "cashu_wallet");
|
||||
if (!wallet || !cJSON_IsObject(wallet)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
cJSON* enabled = cJSON_GetObjectItemCaseSensitive(wallet, "enabled");
|
||||
if (enabled && cJSON_IsBool(enabled)) {
|
||||
config->cashu_wallet.enabled = cJSON_IsTrue(enabled) ? 1 : 0;
|
||||
}
|
||||
|
||||
cJSON* auto_load = cJSON_GetObjectItemCaseSensitive(wallet, "auto_load");
|
||||
if (auto_load && cJSON_IsBool(auto_load)) {
|
||||
config->cashu_wallet.auto_load = cJSON_IsTrue(auto_load) ? 1 : 0;
|
||||
}
|
||||
|
||||
cJSON* timeout = cJSON_GetObjectItemCaseSensitive(wallet, "mint_timeout_seconds");
|
||||
if (timeout && cJSON_IsNumber(timeout)) {
|
||||
config->cashu_wallet.mint_timeout_seconds = (int)timeout->valuedouble;
|
||||
}
|
||||
|
||||
if (copy_json_string(wallet,
|
||||
"unit",
|
||||
config->cashu_wallet.unit,
|
||||
sizeof(config->cashu_wallet.unit),
|
||||
0) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON* mints = cJSON_GetObjectItemCaseSensitive(wallet, "mints");
|
||||
if (mints) {
|
||||
if (!cJSON_IsArray(mints)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int count = cJSON_GetArraySize(mints);
|
||||
char** urls = NULL;
|
||||
if (count > 0) {
|
||||
urls = (char**)calloc((size_t)count, sizeof(char*));
|
||||
if (!urls) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
cJSON* item = cJSON_GetArrayItem(mints, i);
|
||||
if (!item || !cJSON_IsString(item) || !item->valuestring || item->valuestring[0] == '\0') {
|
||||
for (int j = 0; j < i; j++) {
|
||||
free(urls[j]);
|
||||
}
|
||||
free(urls);
|
||||
return -1;
|
||||
}
|
||||
urls[i] = strdup(item->valuestring);
|
||||
if (!urls[i]) {
|
||||
for (int j = 0; j < i; j++) {
|
||||
free(urls[j]);
|
||||
}
|
||||
free(urls);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
free_cashu_wallet_mints(&config->cashu_wallet);
|
||||
config->cashu_wallet.mint_urls = urls;
|
||||
config->cashu_wallet.mint_count = count;
|
||||
}
|
||||
|
||||
if (config->cashu_wallet.unit[0] == '\0') {
|
||||
snprintf(config->cashu_wallet.unit, sizeof(config->cashu_wallet.unit), "%s", "sat");
|
||||
}
|
||||
if (config->cashu_wallet.mint_timeout_seconds < 1) {
|
||||
config->cashu_wallet.mint_timeout_seconds = 30;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static cJSON* find_tag_value_string(cJSON* tags, const char* tag_key) {
|
||||
if (!tags || !cJSON_IsArray(tags) || !tag_key) {
|
||||
return NULL;
|
||||
@@ -1115,6 +1206,8 @@ void config_free(didactyl_config_t* config) {
|
||||
free(config->default_skill.content);
|
||||
free(config->default_skill.tags_json);
|
||||
|
||||
free_cashu_wallet_mints(&config->cashu_wallet);
|
||||
|
||||
memset(config, 0, sizeof(*config));
|
||||
}
|
||||
|
||||
@@ -1174,6 +1267,13 @@ int config_load(const char* path, didactyl_config_t* config) {
|
||||
config->default_skill.content = NULL;
|
||||
config->default_skill.tags_json = NULL;
|
||||
|
||||
config->cashu_wallet.enabled = 0;
|
||||
config->cashu_wallet.mint_urls = NULL;
|
||||
config->cashu_wallet.mint_count = 0;
|
||||
snprintf(config->cashu_wallet.unit, sizeof(config->cashu_wallet.unit), "%s", "sat");
|
||||
config->cashu_wallet.auto_load = 1;
|
||||
config->cashu_wallet.mint_timeout_seconds = 30;
|
||||
|
||||
char* raw_buf = NULL;
|
||||
size_t raw_len = 0;
|
||||
if (read_file_to_buffer(path, &raw_buf, &raw_len) != 0) {
|
||||
@@ -1315,6 +1415,11 @@ int config_load(const char* path, didactyl_config_t* config) {
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (parse_cashu_wallet_config(root, config) != 0) {
|
||||
config_set_error("invalid cashu_wallet configuration");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (config->keys.nsec[0] != '\0') {
|
||||
if (decode_private_key(config->keys.nsec, config->keys.private_key) != 0) {
|
||||
config_set_error("keys.nsec/key.nsec must be valid nsec1... or 64-char hex private key");
|
||||
|
||||
@@ -105,6 +105,15 @@ typedef struct {
|
||||
char bind_address[OW_MAX_URL_LEN];
|
||||
} api_config_t;
|
||||
|
||||
typedef struct {
|
||||
int enabled;
|
||||
char** mint_urls;
|
||||
int mint_count;
|
||||
char unit[16];
|
||||
int auto_load;
|
||||
int mint_timeout_seconds;
|
||||
} cashu_wallet_config_t;
|
||||
|
||||
typedef struct {
|
||||
agent_keys_t keys;
|
||||
admin_config_t admin;
|
||||
@@ -117,6 +126,7 @@ typedef struct {
|
||||
admin_context_config_t admin_context;
|
||||
triggers_config_t triggers;
|
||||
api_config_t api;
|
||||
cashu_wallet_config_t cashu_wallet;
|
||||
startup_event_t* startup_events;
|
||||
int startup_event_count;
|
||||
default_skill_config_t default_skill;
|
||||
|
||||
+40
-1
@@ -19,6 +19,7 @@
|
||||
#include "cjson/cJSON.h"
|
||||
#include "http_api.h"
|
||||
#include "setup_wizard.h"
|
||||
#include "cashu_wallet.h"
|
||||
|
||||
static volatile sig_atomic_t g_running = 1;
|
||||
|
||||
@@ -1073,8 +1074,22 @@ int main(int argc, char** argv) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
int wallet_initialized = 0;
|
||||
int exit_code = 0;
|
||||
|
||||
if (cashu_wallet_init(&cfg) == 0) {
|
||||
wallet_initialized = 1;
|
||||
if (cfg.cashu_wallet.enabled) {
|
||||
int wallet_rc = 0;
|
||||
if (cfg.cashu_wallet.auto_load) {
|
||||
wallet_rc = cashu_wallet_load_from_relays();
|
||||
}
|
||||
if (wallet_rc != 0 && cfg.cashu_wallet.mint_count > 0) {
|
||||
(void)cashu_wallet_create_new_from_config();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (test_tool_name) {
|
||||
const int timeout_ms = 15000;
|
||||
if (!wait_for_connected_relays(timeout_ms)) {
|
||||
@@ -1113,6 +1128,9 @@ int main(int argc, char** argv) {
|
||||
}
|
||||
|
||||
tools_cleanup(&tools_ctx);
|
||||
if (wallet_initialized) {
|
||||
cashu_wallet_cleanup();
|
||||
}
|
||||
nostr_handler_cleanup();
|
||||
config_free(&cfg);
|
||||
llm_cleanup();
|
||||
@@ -1367,6 +1385,26 @@ int main(int argc, char** argv) {
|
||||
DEBUG_INFO("[didactyl] startup phase: subscribe DMs end");
|
||||
startup_step_ok(15, "Subscribe DMs", NULL);
|
||||
|
||||
startup_step_begin(16, "Initialize cashu wallet");
|
||||
if (cashu_wallet_init(&cfg) != 0) {
|
||||
DEBUG_WARN("[didactyl] startup phase: cashu_wallet_init failed (continuing without wallet)");
|
||||
startup_step_fail(16, "Initialize cashu wallet", "cashu_wallet_init failed; wallet tools may return errors");
|
||||
} else {
|
||||
int wallet_rc = 0;
|
||||
if (cfg.cashu_wallet.auto_load) {
|
||||
wallet_rc = cashu_wallet_load_from_relays();
|
||||
}
|
||||
if (wallet_rc != 0 && cfg.cashu_wallet.mint_count > 0) {
|
||||
wallet_rc = cashu_wallet_create_new_from_config();
|
||||
}
|
||||
if (wallet_rc != 0) {
|
||||
DEBUG_WARN("[didactyl] startup phase: cashu wallet load/create deferred to first tool call");
|
||||
startup_step_ok(16, "Initialize cashu wallet", "initialized; load/create deferred");
|
||||
} else {
|
||||
startup_step_ok(16, "Initialize cashu wallet", cfg.cashu_wallet.auto_load ? "loaded or created" : "initialized");
|
||||
}
|
||||
}
|
||||
|
||||
signal(SIGINT, signal_handler);
|
||||
signal(SIGTERM, signal_handler);
|
||||
signal(SIGPIPE, SIG_IGN);
|
||||
@@ -1403,7 +1441,7 @@ int main(int argc, char** argv) {
|
||||
|
||||
nostr_handler_refresh_relay_statuses();
|
||||
|
||||
startup_step_ok(16, "READY", "agent online; entering main poll loop");
|
||||
startup_step_ok(17, "READY", "agent online; entering main poll loop");
|
||||
DEBUG_INFO("[didactyl] entering main poll loop");
|
||||
DEBUG_INFO("[didactyl] running with pubkey %s", cfg.keys.public_key_hex);
|
||||
|
||||
@@ -1423,6 +1461,7 @@ int main(int argc, char** argv) {
|
||||
http_api_cleanup();
|
||||
}
|
||||
|
||||
cashu_wallet_cleanup();
|
||||
agent_cleanup();
|
||||
nostr_handler_cleanup();
|
||||
llm_cleanup();
|
||||
|
||||
+3
-3
@@ -11,9 +11,9 @@
|
||||
// Version information (auto-updated by build system)
|
||||
// Using DIDACTYL_ prefix to avoid conflicts with nostr_core_lib VERSION macros
|
||||
#define DIDACTYL_VERSION_MAJOR 0
|
||||
#define DIDACTYL_VERSION_MINOR 0
|
||||
#define DIDACTYL_VERSION_PATCH 86
|
||||
#define DIDACTYL_VERSION "v0.0.86"
|
||||
#define DIDACTYL_VERSION_MINOR 1
|
||||
#define DIDACTYL_VERSION_PATCH 7
|
||||
#define DIDACTYL_VERSION "v0.1.7"
|
||||
|
||||
// Agent metadata
|
||||
#define DIDACTYL_NAME "Didactyl"
|
||||
|
||||
@@ -0,0 +1,355 @@
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
|
||||
#include "tools_internal.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "cjson/cJSON.h"
|
||||
#include "../cashu_wallet.h"
|
||||
|
||||
static char* json_error_local(const char* msg) {
|
||||
cJSON* root = cJSON_CreateObject();
|
||||
if (!root) return NULL;
|
||||
cJSON_AddBoolToObject(root, "success", 0);
|
||||
cJSON_AddStringToObject(root, "error", msg ? msg : "unknown error");
|
||||
char* out = cJSON_PrintUnformatted(root);
|
||||
cJSON_Delete(root);
|
||||
return out;
|
||||
}
|
||||
|
||||
static cJSON* parse_args_local(const char* args_json) {
|
||||
const char* raw = args_json ? args_json : "{}";
|
||||
cJSON* args = cJSON_Parse(raw);
|
||||
if (!args || !cJSON_IsObject(args)) {
|
||||
cJSON_Delete(args);
|
||||
return NULL;
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
static char* wrap_wallet_json_result(int rc, cJSON* obj, const char* default_error) {
|
||||
if (!obj) {
|
||||
return json_error_local(default_error ? default_error : "cashu wallet operation failed");
|
||||
}
|
||||
|
||||
if (rc != 0) {
|
||||
cJSON* success = cJSON_GetObjectItemCaseSensitive(obj, "success");
|
||||
if (!success) {
|
||||
cJSON_AddBoolToObject(obj, "success", 0);
|
||||
}
|
||||
if (!cJSON_GetObjectItemCaseSensitive(obj, "error")) {
|
||||
cJSON_AddStringToObject(obj,
|
||||
"error",
|
||||
default_error ? default_error : "cashu wallet operation failed");
|
||||
}
|
||||
}
|
||||
|
||||
char* out = cJSON_PrintUnformatted(obj);
|
||||
cJSON_Delete(obj);
|
||||
if (!out) {
|
||||
return json_error_local(default_error ? default_error : "cashu wallet operation failed");
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
char* execute_cashu_wallet_balance(tools_context_t* ctx, const char* args_json) {
|
||||
if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");
|
||||
|
||||
cJSON* args = parse_args_local(args_json);
|
||||
if (!args) return json_error_local("invalid arguments JSON");
|
||||
|
||||
cJSON* out = NULL;
|
||||
int rc = cashu_wallet_balance(&out);
|
||||
cJSON_Delete(args);
|
||||
return wrap_wallet_json_result(rc, out, "cashu_wallet_balance failed");
|
||||
}
|
||||
|
||||
char* execute_cashu_wallet_info(tools_context_t* ctx, const char* args_json) {
|
||||
if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");
|
||||
|
||||
cJSON* args = parse_args_local(args_json);
|
||||
if (!args) return json_error_local("invalid arguments JSON");
|
||||
|
||||
cJSON* mint = cJSON_GetObjectItemCaseSensitive(args, "mint_url");
|
||||
const char* mint_url = (mint && cJSON_IsString(mint) && mint->valuestring) ? mint->valuestring : NULL;
|
||||
if ((!mint_url || mint_url[0] == '\0') && ctx->cfg->cashu_wallet.mint_count <= 0) {
|
||||
cJSON_Delete(args);
|
||||
return json_error_local("cashu_wallet_info requires mint_url when no cashu_wallet.mint_urls are configured");
|
||||
}
|
||||
|
||||
cJSON* out = NULL;
|
||||
int rc = cashu_wallet_info(mint_url, &out);
|
||||
cJSON_Delete(args);
|
||||
return wrap_wallet_json_result(rc, out, "cashu_wallet_info failed (check mint_url and mint connectivity)");
|
||||
}
|
||||
|
||||
char* execute_cashu_wallet_mint_quote(tools_context_t* ctx, const char* args_json) {
|
||||
if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");
|
||||
|
||||
cJSON* args = parse_args_local(args_json);
|
||||
if (!args) return json_error_local("invalid arguments JSON");
|
||||
|
||||
cJSON* amount = cJSON_GetObjectItemCaseSensitive(args, "amount");
|
||||
if (!amount || !cJSON_IsNumber(amount) || amount->valuedouble <= 0) {
|
||||
cJSON_Delete(args);
|
||||
return json_error_local("cashu_wallet_mint_quote requires numeric amount > 0");
|
||||
}
|
||||
|
||||
cJSON* mint = cJSON_GetObjectItemCaseSensitive(args, "mint_url");
|
||||
cJSON* unit = cJSON_GetObjectItemCaseSensitive(args, "unit");
|
||||
const char* mint_url = (mint && cJSON_IsString(mint) && mint->valuestring) ? mint->valuestring : NULL;
|
||||
const char* unit_s = (unit && cJSON_IsString(unit) && unit->valuestring) ? unit->valuestring : NULL;
|
||||
if ((!mint_url || mint_url[0] == '\0') && ctx->cfg->cashu_wallet.mint_count <= 0) {
|
||||
cJSON_Delete(args);
|
||||
return json_error_local("cashu_wallet_mint_quote requires mint_url when no cashu_wallet.mint_urls are configured");
|
||||
}
|
||||
|
||||
cJSON* out = NULL;
|
||||
int rc = cashu_wallet_mint_quote(mint_url, (uint64_t)amount->valuedouble, unit_s, &out);
|
||||
cJSON_Delete(args);
|
||||
return wrap_wallet_json_result(rc, out, "cashu_wallet_mint_quote failed");
|
||||
}
|
||||
|
||||
char* execute_cashu_wallet_mint_check(tools_context_t* ctx, const char* args_json) {
|
||||
if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");
|
||||
|
||||
cJSON* args = parse_args_local(args_json);
|
||||
if (!args) return json_error_local("invalid arguments JSON");
|
||||
|
||||
cJSON* quote_id = cJSON_GetObjectItemCaseSensitive(args, "quote_id");
|
||||
if (!quote_id || !cJSON_IsString(quote_id) || !quote_id->valuestring || quote_id->valuestring[0] == '\0') {
|
||||
cJSON_Delete(args);
|
||||
return json_error_local("cashu_wallet_mint_check requires quote_id");
|
||||
}
|
||||
|
||||
cJSON* mint = cJSON_GetObjectItemCaseSensitive(args, "mint_url");
|
||||
const char* mint_url = (mint && cJSON_IsString(mint) && mint->valuestring) ? mint->valuestring : NULL;
|
||||
if ((!mint_url || mint_url[0] == '\0') && ctx->cfg->cashu_wallet.mint_count <= 0) {
|
||||
cJSON_Delete(args);
|
||||
return json_error_local("cashu_wallet_mint_check requires mint_url when no cashu_wallet.mint_urls are configured");
|
||||
}
|
||||
|
||||
cJSON* out = NULL;
|
||||
int rc = cashu_wallet_mint_check(mint_url, quote_id->valuestring, &out);
|
||||
cJSON_Delete(args);
|
||||
return wrap_wallet_json_result(rc, out, "cashu_wallet_mint_check failed");
|
||||
}
|
||||
|
||||
char* execute_cashu_wallet_mint_claim(tools_context_t* ctx, const char* args_json) {
|
||||
if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");
|
||||
|
||||
cJSON* args = parse_args_local(args_json);
|
||||
if (!args) return json_error_local("invalid arguments JSON");
|
||||
|
||||
cJSON* quote_id = cJSON_GetObjectItemCaseSensitive(args, "quote_id");
|
||||
if (!quote_id || !cJSON_IsString(quote_id) || !quote_id->valuestring || quote_id->valuestring[0] == '\0') {
|
||||
cJSON_Delete(args);
|
||||
return json_error_local("cashu_wallet_mint_claim requires quote_id");
|
||||
}
|
||||
|
||||
uint64_t amount = 0;
|
||||
cJSON* amount_item = cJSON_GetObjectItemCaseSensitive(args, "amount");
|
||||
if (amount_item) {
|
||||
if (!cJSON_IsNumber(amount_item) || amount_item->valuedouble < 0) {
|
||||
cJSON_Delete(args);
|
||||
return json_error_local("cashu_wallet_mint_claim amount must be a non-negative number");
|
||||
}
|
||||
amount = (uint64_t)amount_item->valuedouble;
|
||||
}
|
||||
|
||||
cJSON* mint = cJSON_GetObjectItemCaseSensitive(args, "mint_url");
|
||||
const char* mint_url = (mint && cJSON_IsString(mint) && mint->valuestring) ? mint->valuestring : NULL;
|
||||
if ((!mint_url || mint_url[0] == '\0') && ctx->cfg->cashu_wallet.mint_count <= 0) {
|
||||
cJSON_Delete(args);
|
||||
return json_error_local("cashu_wallet_mint_claim requires mint_url when no cashu_wallet.mint_urls are configured");
|
||||
}
|
||||
|
||||
cJSON* out = NULL;
|
||||
int rc = cashu_wallet_mint_claim(mint_url, quote_id->valuestring, amount, &out);
|
||||
cJSON_Delete(args);
|
||||
return wrap_wallet_json_result(rc, out, "cashu_wallet_mint_claim failed");
|
||||
}
|
||||
|
||||
char* execute_cashu_wallet_melt_quote(tools_context_t* ctx, const char* args_json) {
|
||||
if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");
|
||||
|
||||
cJSON* args = parse_args_local(args_json);
|
||||
if (!args) return json_error_local("invalid arguments JSON");
|
||||
|
||||
cJSON* payment_request = cJSON_GetObjectItemCaseSensitive(args, "payment_request");
|
||||
if (!payment_request || !cJSON_IsString(payment_request) || !payment_request->valuestring || payment_request->valuestring[0] == '\0') {
|
||||
cJSON_Delete(args);
|
||||
return json_error_local("cashu_wallet_melt_quote requires payment_request");
|
||||
}
|
||||
|
||||
cJSON* mint = cJSON_GetObjectItemCaseSensitive(args, "mint_url");
|
||||
cJSON* unit = cJSON_GetObjectItemCaseSensitive(args, "unit");
|
||||
const char* mint_url = (mint && cJSON_IsString(mint) && mint->valuestring) ? mint->valuestring : NULL;
|
||||
const char* unit_s = (unit && cJSON_IsString(unit) && unit->valuestring) ? unit->valuestring : NULL;
|
||||
if ((!mint_url || mint_url[0] == '\0') && ctx->cfg->cashu_wallet.mint_count <= 0) {
|
||||
cJSON_Delete(args);
|
||||
return json_error_local("cashu_wallet_melt_quote requires mint_url when no cashu_wallet.mint_urls are configured");
|
||||
}
|
||||
|
||||
cJSON* out = NULL;
|
||||
int rc = cashu_wallet_melt_quote(mint_url, payment_request->valuestring, unit_s, &out);
|
||||
cJSON_Delete(args);
|
||||
return wrap_wallet_json_result(rc, out, "cashu_wallet_melt_quote failed");
|
||||
}
|
||||
|
||||
char* execute_cashu_wallet_melt_pay(tools_context_t* ctx, const char* args_json) {
|
||||
if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");
|
||||
|
||||
cJSON* args = parse_args_local(args_json);
|
||||
if (!args) return json_error_local("invalid arguments JSON");
|
||||
|
||||
cJSON* quote_id = cJSON_GetObjectItemCaseSensitive(args, "quote_id");
|
||||
if (!quote_id || !cJSON_IsString(quote_id) || !quote_id->valuestring || quote_id->valuestring[0] == '\0') {
|
||||
cJSON_Delete(args);
|
||||
return json_error_local("cashu_wallet_melt_pay requires quote_id");
|
||||
}
|
||||
|
||||
cJSON* mint = cJSON_GetObjectItemCaseSensitive(args, "mint_url");
|
||||
const char* mint_url = (mint && cJSON_IsString(mint) && mint->valuestring) ? mint->valuestring : NULL;
|
||||
if ((!mint_url || mint_url[0] == '\0') && ctx->cfg->cashu_wallet.mint_count <= 0) {
|
||||
cJSON_Delete(args);
|
||||
return json_error_local("cashu_wallet_melt_pay requires mint_url when no cashu_wallet.mint_urls are configured");
|
||||
}
|
||||
|
||||
cJSON* out = NULL;
|
||||
int rc = cashu_wallet_melt_pay(mint_url, quote_id->valuestring, &out);
|
||||
cJSON_Delete(args);
|
||||
return wrap_wallet_json_result(rc, out, "cashu_wallet_melt_pay failed");
|
||||
}
|
||||
|
||||
char* execute_cashu_wallet_check_proofs(tools_context_t* ctx, const char* args_json) {
|
||||
if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");
|
||||
|
||||
cJSON* args = parse_args_local(args_json);
|
||||
if (!args) return json_error_local("invalid arguments JSON");
|
||||
|
||||
cJSON* mint = cJSON_GetObjectItemCaseSensitive(args, "mint_url");
|
||||
const char* mint_url = (mint && cJSON_IsString(mint) && mint->valuestring) ? mint->valuestring : NULL;
|
||||
if ((!mint_url || mint_url[0] == '\0') && ctx->cfg->cashu_wallet.mint_count <= 0) {
|
||||
cJSON_Delete(args);
|
||||
return json_error_local("cashu_wallet_check_proofs requires mint_url when no cashu_wallet.mint_urls are configured");
|
||||
}
|
||||
|
||||
cJSON* out = NULL;
|
||||
int rc = cashu_wallet_check_proofs(mint_url, &out);
|
||||
cJSON_Delete(args);
|
||||
return wrap_wallet_json_result(rc, out, "cashu_wallet_check_proofs failed");
|
||||
}
|
||||
|
||||
char* execute_cashu_wallet_receive_token(tools_context_t* ctx, const char* args_json) {
|
||||
if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");
|
||||
|
||||
cJSON* args = parse_args_local(args_json);
|
||||
if (!args) return json_error_local("invalid arguments JSON");
|
||||
|
||||
cJSON* token = cJSON_GetObjectItemCaseSensitive(args, "token");
|
||||
if (!token || !cJSON_IsString(token) || !token->valuestring || token->valuestring[0] == '\0') {
|
||||
cJSON_Delete(args);
|
||||
return json_error_local("cashu_wallet_receive_token requires token");
|
||||
}
|
||||
|
||||
cJSON* mint = cJSON_GetObjectItemCaseSensitive(args, "mint_url");
|
||||
const char* mint_url = (mint && cJSON_IsString(mint) && mint->valuestring) ? mint->valuestring : NULL;
|
||||
|
||||
cJSON* out = NULL;
|
||||
int rc = cashu_wallet_receive_token(token->valuestring, mint_url, &out);
|
||||
cJSON_Delete(args);
|
||||
return wrap_wallet_json_result(rc, out, "cashu_wallet_receive_token failed");
|
||||
}
|
||||
|
||||
char* execute_cashu_wallet_mints_get(tools_context_t* ctx, const char* args_json) {
|
||||
if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");
|
||||
|
||||
cJSON* args = parse_args_local(args_json);
|
||||
if (!args) return json_error_local("invalid arguments JSON");
|
||||
cJSON_Delete(args);
|
||||
|
||||
cJSON* out = NULL;
|
||||
int rc = cashu_wallet_mints_get(&out);
|
||||
return wrap_wallet_json_result(rc, out, "cashu_wallet_mints_get failed");
|
||||
}
|
||||
|
||||
char* execute_cashu_wallet_mints_set(tools_context_t* ctx, const char* args_json) {
|
||||
if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");
|
||||
|
||||
cJSON* args = parse_args_local(args_json);
|
||||
if (!args) return json_error_local("invalid arguments JSON");
|
||||
|
||||
cJSON* target = cJSON_GetObjectItemCaseSensitive(args, "target");
|
||||
const char* target_s = (target && cJSON_IsString(target) && target->valuestring) ? target->valuestring : "both";
|
||||
|
||||
int set_wallet = 0;
|
||||
int set_public = 0;
|
||||
if (strcmp(target_s, "wallet") == 0) {
|
||||
set_wallet = 1;
|
||||
} else if (strcmp(target_s, "public") == 0) {
|
||||
set_public = 1;
|
||||
} else if (strcmp(target_s, "both") == 0) {
|
||||
set_wallet = 1;
|
||||
set_public = 1;
|
||||
} else {
|
||||
cJSON_Delete(args);
|
||||
return json_error_local("cashu_wallet_mints_set target must be wallet, public, or both");
|
||||
}
|
||||
|
||||
cJSON* wallet_arr = cJSON_GetObjectItemCaseSensitive(args, "wallet_mints");
|
||||
cJSON* public_arr = cJSON_GetObjectItemCaseSensitive(args, "public_mints");
|
||||
|
||||
const char* wallet_buf[32] = {0};
|
||||
int wallet_count = 0;
|
||||
if (wallet_arr) {
|
||||
if (!cJSON_IsArray(wallet_arr)) {
|
||||
cJSON_Delete(args);
|
||||
return json_error_local("wallet_mints must be an array of strings");
|
||||
}
|
||||
int n = cJSON_GetArraySize(wallet_arr);
|
||||
for (int i = 0; i < n && wallet_count < 32; i++) {
|
||||
cJSON* it = cJSON_GetArrayItem(wallet_arr, i);
|
||||
if (it && cJSON_IsString(it) && it->valuestring && it->valuestring[0] != '\0') {
|
||||
wallet_buf[wallet_count++] = it->valuestring;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const char* public_buf[32] = {0};
|
||||
int public_count = 0;
|
||||
if (public_arr) {
|
||||
if (!cJSON_IsArray(public_arr)) {
|
||||
cJSON_Delete(args);
|
||||
return json_error_local("public_mints must be an array of strings");
|
||||
}
|
||||
int n = cJSON_GetArraySize(public_arr);
|
||||
for (int i = 0; i < n && public_count < 32; i++) {
|
||||
cJSON* it = cJSON_GetArrayItem(public_arr, i);
|
||||
if (it && cJSON_IsString(it) && it->valuestring && it->valuestring[0] != '\0') {
|
||||
public_buf[public_count++] = it->valuestring;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (set_wallet && wallet_count <= 0) {
|
||||
cJSON_Delete(args);
|
||||
return json_error_local("cashu_wallet_mints_set requires wallet_mints when target includes wallet");
|
||||
}
|
||||
if (set_public && public_count <= 0) {
|
||||
cJSON_Delete(args);
|
||||
return json_error_local("cashu_wallet_mints_set requires public_mints when target includes public");
|
||||
}
|
||||
|
||||
cJSON* out = NULL;
|
||||
int rc = cashu_wallet_mints_set(wallet_buf,
|
||||
wallet_count,
|
||||
public_buf,
|
||||
public_count,
|
||||
set_wallet,
|
||||
set_public,
|
||||
&out);
|
||||
cJSON_Delete(args);
|
||||
return wrap_wallet_json_result(rc, out, "cashu_wallet_mints_set failed");
|
||||
}
|
||||
@@ -143,6 +143,9 @@ char* execute_config_store(tools_context_t* ctx, const char* args_json) {
|
||||
return json_error_local("config_store requires content");
|
||||
}
|
||||
|
||||
char d_tag_buf[128];
|
||||
snprintf(d_tag_buf, sizeof(d_tag_buf), "%s", d_tag_j->valuestring);
|
||||
|
||||
char* content_text = NULL;
|
||||
if (cJSON_IsString(content_j) && content_j->valuestring) {
|
||||
content_text = strdup(content_j->valuestring);
|
||||
@@ -180,7 +183,7 @@ char* execute_config_store(tools_context_t* ctx, const char* args_json) {
|
||||
}
|
||||
|
||||
cJSON_AddItemToArray(d_tag, cJSON_CreateString("d"));
|
||||
cJSON_AddItemToArray(d_tag, cJSON_CreateString(d_tag_j->valuestring));
|
||||
cJSON_AddItemToArray(d_tag, cJSON_CreateString(d_tag_buf));
|
||||
cJSON_AddItemToArray(tags, d_tag);
|
||||
|
||||
cJSON_AddItemToArray(app_tag, cJSON_CreateString("app"));
|
||||
@@ -208,7 +211,7 @@ char* execute_config_store(tools_context_t* ctx, const char* args_json) {
|
||||
}
|
||||
|
||||
cJSON_AddBoolToObject(out, "success", 1);
|
||||
cJSON_AddStringToObject(out, "d_tag", d_tag_j->valuestring);
|
||||
cJSON_AddStringToObject(out, "d_tag", d_tag_buf);
|
||||
cJSON_AddNumberToObject(out, "kind", CONFIG_KIND);
|
||||
cJSON_AddNumberToObject(out, "content_length", (double)strlen(content_text));
|
||||
cJSON_AddStringToObject(out, "event_id", publish_result.event_id);
|
||||
|
||||
@@ -232,6 +232,39 @@ char* tools_execute_legacy(tools_context_t* ctx, const char* tool_name, const ch
|
||||
if (strcmp(tool_name, "trigger_event") == 0) {
|
||||
return execute_trigger_event(ctx, args_json);
|
||||
}
|
||||
if (strcmp(tool_name, "cashu_wallet_balance") == 0) {
|
||||
return execute_cashu_wallet_balance(ctx, args_json);
|
||||
}
|
||||
if (strcmp(tool_name, "cashu_wallet_info") == 0) {
|
||||
return execute_cashu_wallet_info(ctx, args_json);
|
||||
}
|
||||
if (strcmp(tool_name, "cashu_wallet_mint_quote") == 0) {
|
||||
return execute_cashu_wallet_mint_quote(ctx, args_json);
|
||||
}
|
||||
if (strcmp(tool_name, "cashu_wallet_mint_check") == 0) {
|
||||
return execute_cashu_wallet_mint_check(ctx, args_json);
|
||||
}
|
||||
if (strcmp(tool_name, "cashu_wallet_mint_claim") == 0) {
|
||||
return execute_cashu_wallet_mint_claim(ctx, args_json);
|
||||
}
|
||||
if (strcmp(tool_name, "cashu_wallet_melt_quote") == 0) {
|
||||
return execute_cashu_wallet_melt_quote(ctx, args_json);
|
||||
}
|
||||
if (strcmp(tool_name, "cashu_wallet_melt_pay") == 0) {
|
||||
return execute_cashu_wallet_melt_pay(ctx, args_json);
|
||||
}
|
||||
if (strcmp(tool_name, "cashu_wallet_check_proofs") == 0) {
|
||||
return execute_cashu_wallet_check_proofs(ctx, args_json);
|
||||
}
|
||||
if (strcmp(tool_name, "cashu_wallet_receive_token") == 0) {
|
||||
return execute_cashu_wallet_receive_token(ctx, args_json);
|
||||
}
|
||||
if (strcmp(tool_name, "cashu_wallet_mints_get") == 0) {
|
||||
return execute_cashu_wallet_mints_get(ctx, args_json);
|
||||
}
|
||||
if (strcmp(tool_name, "cashu_wallet_mints_set") == 0) {
|
||||
return execute_cashu_wallet_mints_set(ctx, args_json);
|
||||
}
|
||||
|
||||
return json_error("unknown tool");
|
||||
}
|
||||
|
||||
@@ -68,6 +68,19 @@ char* execute_config_store(tools_context_t* ctx, const char* args_json);
|
||||
char* execute_config_recall(tools_context_t* ctx, const char* args_json);
|
||||
char* execute_adopted_skills(tools_context_t* ctx, const char* args_json);
|
||||
char* execute_trigger_event(tools_context_t* ctx, const char* args_json);
|
||||
|
||||
char* execute_cashu_wallet_balance(tools_context_t* ctx, const char* args_json);
|
||||
char* execute_cashu_wallet_info(tools_context_t* ctx, const char* args_json);
|
||||
char* execute_cashu_wallet_mint_quote(tools_context_t* ctx, const char* args_json);
|
||||
char* execute_cashu_wallet_mint_check(tools_context_t* ctx, const char* args_json);
|
||||
char* execute_cashu_wallet_mint_claim(tools_context_t* ctx, const char* args_json);
|
||||
char* execute_cashu_wallet_melt_quote(tools_context_t* ctx, const char* args_json);
|
||||
char* execute_cashu_wallet_melt_pay(tools_context_t* ctx, const char* args_json);
|
||||
char* execute_cashu_wallet_check_proofs(tools_context_t* ctx, const char* args_json);
|
||||
char* execute_cashu_wallet_receive_token(tools_context_t* ctx, const char* args_json);
|
||||
char* execute_cashu_wallet_mints_get(tools_context_t* ctx, const char* args_json);
|
||||
char* execute_cashu_wallet_mints_set(tools_context_t* ctx, const char* args_json);
|
||||
|
||||
int memory_init(tools_context_t* ctx);
|
||||
void memory_cleanup(void);
|
||||
|
||||
|
||||
@@ -1477,6 +1477,295 @@ char* tools_build_openai_schema_json_legacy(const tools_context_t* ctx) {
|
||||
cJSON_AddItemToObject(t50, "function", t50_fn);
|
||||
cJSON_AddItemToArray(tools, t50);
|
||||
|
||||
cJSON* t51 = cJSON_CreateObject();
|
||||
cJSON* t51_fn = cJSON_CreateObject();
|
||||
cJSON* t51_params = cJSON_CreateObject();
|
||||
cJSON* t51_props = cJSON_CreateObject();
|
||||
|
||||
cJSON_AddStringToObject(t51, "type", "function");
|
||||
cJSON_AddStringToObject(t51_fn, "name", "cashu_wallet_balance");
|
||||
cJSON_AddStringToObject(t51_fn, "description", "Return current wallet balances aggregated by mint and unit");
|
||||
cJSON_AddStringToObject(t51_params, "type", "object");
|
||||
cJSON_AddItemToObject(t51_params, "properties", t51_props);
|
||||
|
||||
cJSON_AddItemToObject(t51_fn, "parameters", t51_params);
|
||||
cJSON_AddItemToObject(t51, "function", t51_fn);
|
||||
cJSON_AddItemToArray(tools, t51);
|
||||
|
||||
cJSON* t52 = cJSON_CreateObject();
|
||||
cJSON* t52_fn = cJSON_CreateObject();
|
||||
cJSON* t52_params = cJSON_CreateObject();
|
||||
cJSON* t52_props = cJSON_CreateObject();
|
||||
|
||||
cJSON_AddStringToObject(t52, "type", "function");
|
||||
cJSON_AddStringToObject(t52_fn, "name", "cashu_wallet_info");
|
||||
cJSON_AddStringToObject(t52_fn, "description", "Fetch mint info for a specific mint_url or the default configured mint");
|
||||
cJSON_AddStringToObject(t52_params, "type", "object");
|
||||
cJSON_AddItemToObject(t52_params, "properties", t52_props);
|
||||
|
||||
cJSON* p_wallet_info_mint = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p_wallet_info_mint, "type", "string");
|
||||
cJSON_AddStringToObject(p_wallet_info_mint, "description", "Optional mint base URL; uses configured default when omitted");
|
||||
cJSON_AddItemToObject(t52_props, "mint_url", p_wallet_info_mint);
|
||||
|
||||
cJSON_AddItemToObject(t52_fn, "parameters", t52_params);
|
||||
cJSON_AddItemToObject(t52, "function", t52_fn);
|
||||
cJSON_AddItemToArray(tools, t52);
|
||||
|
||||
cJSON* t53 = cJSON_CreateObject();
|
||||
cJSON* t53_fn = cJSON_CreateObject();
|
||||
cJSON* t53_params = cJSON_CreateObject();
|
||||
cJSON* t53_props = cJSON_CreateObject();
|
||||
cJSON* t53_required = cJSON_CreateArray();
|
||||
|
||||
cJSON_AddStringToObject(t53, "type", "function");
|
||||
cJSON_AddStringToObject(t53_fn, "name", "cashu_wallet_mint_quote");
|
||||
cJSON_AddStringToObject(t53_fn, "description", "Request a mint quote for a target amount");
|
||||
cJSON_AddStringToObject(t53_params, "type", "object");
|
||||
cJSON_AddItemToObject(t53_params, "properties", t53_props);
|
||||
cJSON_AddItemToObject(t53_params, "required", t53_required);
|
||||
|
||||
cJSON* p_wallet_mq_amount = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p_wallet_mq_amount, "type", "integer");
|
||||
cJSON_AddStringToObject(p_wallet_mq_amount, "description", "Amount to mint in the selected unit");
|
||||
cJSON_AddItemToObject(t53_props, "amount", p_wallet_mq_amount);
|
||||
|
||||
cJSON* p_wallet_mq_mint = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p_wallet_mq_mint, "type", "string");
|
||||
cJSON_AddStringToObject(p_wallet_mq_mint, "description", "Optional mint base URL; uses configured default when omitted");
|
||||
cJSON_AddItemToObject(t53_props, "mint_url", p_wallet_mq_mint);
|
||||
|
||||
cJSON* p_wallet_mq_unit = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p_wallet_mq_unit, "type", "string");
|
||||
cJSON_AddStringToObject(p_wallet_mq_unit, "description", "Optional unit, e.g. sat");
|
||||
cJSON_AddItemToObject(t53_props, "unit", p_wallet_mq_unit);
|
||||
|
||||
cJSON_AddItemToArray(t53_required, cJSON_CreateString("amount"));
|
||||
|
||||
cJSON_AddItemToObject(t53_fn, "parameters", t53_params);
|
||||
cJSON_AddItemToObject(t53, "function", t53_fn);
|
||||
cJSON_AddItemToArray(tools, t53);
|
||||
|
||||
cJSON* t54 = cJSON_CreateObject();
|
||||
cJSON* t54_fn = cJSON_CreateObject();
|
||||
cJSON* t54_params = cJSON_CreateObject();
|
||||
cJSON* t54_props = cJSON_CreateObject();
|
||||
cJSON* t54_required = cJSON_CreateArray();
|
||||
|
||||
cJSON_AddStringToObject(t54, "type", "function");
|
||||
cJSON_AddStringToObject(t54_fn, "name", "cashu_wallet_mint_check");
|
||||
cJSON_AddStringToObject(t54_fn, "description", "Check a previously requested mint quote status");
|
||||
cJSON_AddStringToObject(t54_params, "type", "object");
|
||||
cJSON_AddItemToObject(t54_params, "properties", t54_props);
|
||||
cJSON_AddItemToObject(t54_params, "required", t54_required);
|
||||
|
||||
cJSON* p_wallet_mc_quote = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p_wallet_mc_quote, "type", "string");
|
||||
cJSON_AddItemToObject(t54_props, "quote_id", p_wallet_mc_quote);
|
||||
|
||||
cJSON* p_wallet_mc_mint = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p_wallet_mc_mint, "type", "string");
|
||||
cJSON_AddItemToObject(t54_props, "mint_url", p_wallet_mc_mint);
|
||||
|
||||
cJSON_AddItemToArray(t54_required, cJSON_CreateString("quote_id"));
|
||||
|
||||
cJSON_AddItemToObject(t54_fn, "parameters", t54_params);
|
||||
cJSON_AddItemToObject(t54, "function", t54_fn);
|
||||
cJSON_AddItemToArray(tools, t54);
|
||||
|
||||
cJSON* t55 = cJSON_CreateObject();
|
||||
cJSON* t55_fn = cJSON_CreateObject();
|
||||
cJSON* t55_params = cJSON_CreateObject();
|
||||
cJSON* t55_props = cJSON_CreateObject();
|
||||
cJSON* t55_required = cJSON_CreateArray();
|
||||
|
||||
cJSON_AddStringToObject(t55, "type", "function");
|
||||
cJSON_AddStringToObject(t55_fn, "name", "cashu_wallet_mint_claim");
|
||||
cJSON_AddStringToObject(t55_fn, "description", "Claim minted proofs for a paid quote_id and add them to wallet state");
|
||||
cJSON_AddStringToObject(t55_params, "type", "object");
|
||||
cJSON_AddItemToObject(t55_params, "properties", t55_props);
|
||||
cJSON_AddItemToObject(t55_params, "required", t55_required);
|
||||
|
||||
cJSON* p_wallet_mclaim_quote = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p_wallet_mclaim_quote, "type", "string");
|
||||
cJSON_AddItemToObject(t55_props, "quote_id", p_wallet_mclaim_quote);
|
||||
|
||||
cJSON* p_wallet_mclaim_mint = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p_wallet_mclaim_mint, "type", "string");
|
||||
cJSON_AddItemToObject(t55_props, "mint_url", p_wallet_mclaim_mint);
|
||||
|
||||
cJSON* p_wallet_mclaim_amount = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p_wallet_mclaim_amount, "type", "integer");
|
||||
cJSON_AddStringToObject(p_wallet_mclaim_amount, "description", "Optional target amount; if omitted wallet attempts best-effort claim");
|
||||
cJSON_AddItemToObject(t55_props, "amount", p_wallet_mclaim_amount);
|
||||
|
||||
cJSON_AddItemToArray(t55_required, cJSON_CreateString("quote_id"));
|
||||
|
||||
cJSON_AddItemToObject(t55_fn, "parameters", t55_params);
|
||||
cJSON_AddItemToObject(t55, "function", t55_fn);
|
||||
cJSON_AddItemToArray(tools, t55);
|
||||
|
||||
cJSON* t56 = cJSON_CreateObject();
|
||||
cJSON* t56_fn = cJSON_CreateObject();
|
||||
cJSON* t56_params = cJSON_CreateObject();
|
||||
cJSON* t56_props = cJSON_CreateObject();
|
||||
cJSON* t56_required = cJSON_CreateArray();
|
||||
|
||||
cJSON_AddStringToObject(t56, "type", "function");
|
||||
cJSON_AddStringToObject(t56_fn, "name", "cashu_wallet_melt_quote");
|
||||
cJSON_AddStringToObject(t56_fn, "description", "Request a melt quote for a Lightning payment request");
|
||||
cJSON_AddStringToObject(t56_params, "type", "object");
|
||||
cJSON_AddItemToObject(t56_params, "properties", t56_props);
|
||||
cJSON_AddItemToObject(t56_params, "required", t56_required);
|
||||
|
||||
cJSON* p_wallet_meq_pr = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p_wallet_meq_pr, "type", "string");
|
||||
cJSON_AddItemToObject(t56_props, "payment_request", p_wallet_meq_pr);
|
||||
|
||||
cJSON* p_wallet_meq_mint = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p_wallet_meq_mint, "type", "string");
|
||||
cJSON_AddItemToObject(t56_props, "mint_url", p_wallet_meq_mint);
|
||||
|
||||
cJSON* p_wallet_meq_unit = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p_wallet_meq_unit, "type", "string");
|
||||
cJSON_AddItemToObject(t56_props, "unit", p_wallet_meq_unit);
|
||||
|
||||
cJSON_AddItemToArray(t56_required, cJSON_CreateString("payment_request"));
|
||||
|
||||
cJSON_AddItemToObject(t56_fn, "parameters", t56_params);
|
||||
cJSON_AddItemToObject(t56, "function", t56_fn);
|
||||
cJSON_AddItemToArray(tools, t56);
|
||||
|
||||
cJSON* t57 = cJSON_CreateObject();
|
||||
cJSON* t57_fn = cJSON_CreateObject();
|
||||
cJSON* t57_params = cJSON_CreateObject();
|
||||
cJSON* t57_props = cJSON_CreateObject();
|
||||
cJSON* t57_required = cJSON_CreateArray();
|
||||
|
||||
cJSON_AddStringToObject(t57, "type", "function");
|
||||
cJSON_AddStringToObject(t57_fn, "name", "cashu_wallet_melt_pay");
|
||||
cJSON_AddStringToObject(t57_fn, "description", "Pay a previously quoted melt by quote_id");
|
||||
cJSON_AddStringToObject(t57_params, "type", "object");
|
||||
cJSON_AddItemToObject(t57_params, "properties", t57_props);
|
||||
cJSON_AddItemToObject(t57_params, "required", t57_required);
|
||||
|
||||
cJSON* p_wallet_mp_quote = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p_wallet_mp_quote, "type", "string");
|
||||
cJSON_AddItemToObject(t57_props, "quote_id", p_wallet_mp_quote);
|
||||
|
||||
cJSON* p_wallet_mp_mint = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p_wallet_mp_mint, "type", "string");
|
||||
cJSON_AddItemToObject(t57_props, "mint_url", p_wallet_mp_mint);
|
||||
|
||||
cJSON_AddItemToArray(t57_required, cJSON_CreateString("quote_id"));
|
||||
|
||||
cJSON_AddItemToObject(t57_fn, "parameters", t57_params);
|
||||
cJSON_AddItemToObject(t57, "function", t57_fn);
|
||||
cJSON_AddItemToArray(tools, t57);
|
||||
|
||||
cJSON* t58 = cJSON_CreateObject();
|
||||
cJSON* t58_fn = cJSON_CreateObject();
|
||||
cJSON* t58_params = cJSON_CreateObject();
|
||||
cJSON* t58_props = cJSON_CreateObject();
|
||||
|
||||
cJSON_AddStringToObject(t58, "type", "function");
|
||||
cJSON_AddStringToObject(t58_fn, "name", "cashu_wallet_check_proofs");
|
||||
cJSON_AddStringToObject(t58_fn, "description", "Check current wallet proofs state against mint");
|
||||
cJSON_AddStringToObject(t58_params, "type", "object");
|
||||
cJSON_AddItemToObject(t58_params, "properties", t58_props);
|
||||
|
||||
cJSON* p_wallet_cp_mint = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p_wallet_cp_mint, "type", "string");
|
||||
cJSON_AddItemToObject(t58_props, "mint_url", p_wallet_cp_mint);
|
||||
|
||||
cJSON_AddItemToObject(t58_fn, "parameters", t58_params);
|
||||
cJSON_AddItemToObject(t58, "function", t58_fn);
|
||||
cJSON_AddItemToArray(tools, t58);
|
||||
|
||||
cJSON* t59 = cJSON_CreateObject();
|
||||
cJSON* t59_fn = cJSON_CreateObject();
|
||||
cJSON* t59_params = cJSON_CreateObject();
|
||||
cJSON* t59_props = cJSON_CreateObject();
|
||||
cJSON* t59_required = cJSON_CreateArray();
|
||||
|
||||
cJSON_AddStringToObject(t59, "type", "function");
|
||||
cJSON_AddStringToObject(t59_fn, "name", "cashu_wallet_receive_token");
|
||||
cJSON_AddStringToObject(t59_fn, "description", "Receive an ecash token string (cashuA/cashuB), swap to fresh proofs, and store it in wallet state");
|
||||
cJSON_AddStringToObject(t59_params, "type", "object");
|
||||
cJSON_AddItemToObject(t59_params, "properties", t59_props);
|
||||
cJSON_AddItemToObject(t59_params, "required", t59_required);
|
||||
|
||||
cJSON* p_wallet_rt_token = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p_wallet_rt_token, "type", "string");
|
||||
cJSON_AddStringToObject(p_wallet_rt_token, "description", "Incoming token string beginning with cashuA or cashuB");
|
||||
cJSON_AddItemToObject(t59_props, "token", p_wallet_rt_token);
|
||||
|
||||
cJSON* p_wallet_rt_mint = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p_wallet_rt_mint, "type", "string");
|
||||
cJSON_AddStringToObject(p_wallet_rt_mint, "description", "Optional mint URL override; when provided must match token mint");
|
||||
cJSON_AddItemToObject(t59_props, "mint_url", p_wallet_rt_mint);
|
||||
|
||||
cJSON_AddItemToArray(t59_required, cJSON_CreateString("token"));
|
||||
|
||||
cJSON_AddItemToObject(t59_fn, "parameters", t59_params);
|
||||
cJSON_AddItemToObject(t59, "function", t59_fn);
|
||||
cJSON_AddItemToArray(tools, t59);
|
||||
|
||||
cJSON* t60 = cJSON_CreateObject();
|
||||
cJSON* t60_fn = cJSON_CreateObject();
|
||||
cJSON* t60_params = cJSON_CreateObject();
|
||||
cJSON* t60_props = cJSON_CreateObject();
|
||||
|
||||
cJSON_AddStringToObject(t60, "type", "function");
|
||||
cJSON_AddStringToObject(t60_fn, "name", "cashu_wallet_mints_get");
|
||||
cJSON_AddStringToObject(t60_fn, "description", "Get wallet mints from NIP-60 wallet event and public mints from NIP-61 nutzap info");
|
||||
cJSON_AddStringToObject(t60_params, "type", "object");
|
||||
cJSON_AddItemToObject(t60_params, "properties", t60_props);
|
||||
|
||||
cJSON_AddItemToObject(t60_fn, "parameters", t60_params);
|
||||
cJSON_AddItemToObject(t60, "function", t60_fn);
|
||||
cJSON_AddItemToArray(tools, t60);
|
||||
|
||||
cJSON* t61 = cJSON_CreateObject();
|
||||
cJSON* t61_fn = cJSON_CreateObject();
|
||||
cJSON* t61_params = cJSON_CreateObject();
|
||||
cJSON* t61_props = cJSON_CreateObject();
|
||||
cJSON* t61_required = cJSON_CreateArray();
|
||||
|
||||
cJSON_AddStringToObject(t61, "type", "function");
|
||||
cJSON_AddStringToObject(t61_fn, "name", "cashu_wallet_mints_set");
|
||||
cJSON_AddStringToObject(t61_fn, "description", "Set wallet mints (NIP-60), public mints (NIP-61), or both");
|
||||
cJSON_AddStringToObject(t61_params, "type", "object");
|
||||
cJSON_AddItemToObject(t61_params, "properties", t61_props);
|
||||
cJSON_AddItemToObject(t61_params, "required", t61_required);
|
||||
|
||||
cJSON* p_wallet_ms_target = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p_wallet_ms_target, "type", "string");
|
||||
cJSON_AddStringToObject(p_wallet_ms_target, "description", "Where to apply mint updates: wallet, public, or both");
|
||||
cJSON_AddItemToObject(t61_props, "target", p_wallet_ms_target);
|
||||
|
||||
cJSON* p_wallet_ms_wallet = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p_wallet_ms_wallet, "type", "array");
|
||||
cJSON* p_wallet_ms_wallet_items = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p_wallet_ms_wallet_items, "type", "string");
|
||||
cJSON_AddItemToObject(p_wallet_ms_wallet, "items", p_wallet_ms_wallet_items);
|
||||
cJSON_AddStringToObject(p_wallet_ms_wallet, "description", "Mint URLs for wallet event update");
|
||||
cJSON_AddItemToObject(t61_props, "wallet_mints", p_wallet_ms_wallet);
|
||||
|
||||
cJSON* p_wallet_ms_public = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p_wallet_ms_public, "type", "array");
|
||||
cJSON* p_wallet_ms_public_items = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p_wallet_ms_public_items, "type", "string");
|
||||
cJSON_AddItemToObject(p_wallet_ms_public, "items", p_wallet_ms_public_items);
|
||||
cJSON_AddStringToObject(p_wallet_ms_public, "description", "Mint URLs for public nutzap info update");
|
||||
cJSON_AddItemToObject(t61_props, "public_mints", p_wallet_ms_public);
|
||||
|
||||
cJSON_AddItemToArray(t61_required, cJSON_CreateString("target"));
|
||||
|
||||
cJSON_AddItemToObject(t61_fn, "parameters", t61_params);
|
||||
cJSON_AddItemToObject(t61, "function", t61_fn);
|
||||
cJSON_AddItemToArray(tools, t61);
|
||||
|
||||
char* out = cJSON_PrintUnformatted(tools);
|
||||
cJSON_Delete(tools);
|
||||
return out;
|
||||
|
||||
+217
-8
@@ -204,18 +204,202 @@ static int cron_field_matches(const char* field, int value, int min_v, int max_v
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int cron_normalize_expression(const char* expr, char* out, size_t out_sz, int* out_expanded) {
|
||||
if (out_expanded) {
|
||||
*out_expanded = 0;
|
||||
}
|
||||
if (!expr || !out || out_sz == 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
while (*expr && isspace((unsigned char)*expr)) {
|
||||
expr++;
|
||||
}
|
||||
if (*expr == '\0') {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char* mapped = NULL;
|
||||
if (strcmp(expr, "*") == 0) {
|
||||
mapped = "* * * * *";
|
||||
} else if (strcmp(expr, "@yearly") == 0 || strcmp(expr, "@annually") == 0) {
|
||||
mapped = "0 0 1 1 *";
|
||||
} else if (strcmp(expr, "@monthly") == 0) {
|
||||
mapped = "0 0 1 * *";
|
||||
} else if (strcmp(expr, "@weekly") == 0) {
|
||||
mapped = "0 0 * * 0";
|
||||
} else if (strcmp(expr, "@daily") == 0 || strcmp(expr, "@midnight") == 0) {
|
||||
mapped = "0 0 * * *";
|
||||
} else if (strcmp(expr, "@hourly") == 0) {
|
||||
mapped = "0 * * * *";
|
||||
}
|
||||
|
||||
const char* src = mapped ? mapped : expr;
|
||||
int n = snprintf(out, out_sz, "%s", src);
|
||||
if (n < 0 || (size_t)n >= out_sz) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (mapped && out_expanded) {
|
||||
*out_expanded = 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int cron_field_token_valid(const char* token, int min_v, int max_v) {
|
||||
if (!token || token[0] == '\0') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (strcmp(token, "*") == 0) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const char* slash = strchr(token, '/');
|
||||
int step = 0;
|
||||
char base[64];
|
||||
if (slash) {
|
||||
size_t base_len = (size_t)(slash - token);
|
||||
if (base_len == 0 || base_len >= sizeof(base)) {
|
||||
return 0;
|
||||
}
|
||||
memcpy(base, token, base_len);
|
||||
base[base_len] = '\0';
|
||||
|
||||
char* endptr = NULL;
|
||||
long step_l = strtol(slash + 1, &endptr, 10);
|
||||
if (endptr == slash + 1 || *endptr != '\0' || step_l <= 0 || step_l > 1024) {
|
||||
return 0;
|
||||
}
|
||||
step = (int)step_l;
|
||||
} else {
|
||||
int n = snprintf(base, sizeof(base), "%s", token);
|
||||
if (n < 0 || (size_t)n >= sizeof(base)) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (strcmp(base, "*") == 0) {
|
||||
return step == 0 || step <= (max_v - min_v + 1);
|
||||
}
|
||||
|
||||
const char* dash = strchr(base, '-');
|
||||
if (dash) {
|
||||
char left[32];
|
||||
char right[32];
|
||||
size_t left_len = (size_t)(dash - base);
|
||||
size_t right_len = strlen(dash + 1);
|
||||
if (left_len == 0 || right_len == 0 || left_len >= sizeof(left) || right_len >= sizeof(right)) {
|
||||
return 0;
|
||||
}
|
||||
memcpy(left, base, left_len);
|
||||
left[left_len] = '\0';
|
||||
memcpy(right, dash + 1, right_len + 1U);
|
||||
|
||||
char* e1 = NULL;
|
||||
char* e2 = NULL;
|
||||
long a = strtol(left, &e1, 10);
|
||||
long b = strtol(right, &e2, 10);
|
||||
if (!e1 || *e1 != '\0' || !e2 || *e2 != '\0') {
|
||||
return 0;
|
||||
}
|
||||
if ((int)a < min_v || (int)b > max_v || (int)a > (int)b) {
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
char* e = NULL;
|
||||
long n = strtol(base, &e, 10);
|
||||
if (!e || *e != '\0') {
|
||||
return 0;
|
||||
}
|
||||
if ((int)n < min_v || (int)n > max_v) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int cron_field_valid(const char* field, int min_v, int max_v) {
|
||||
if (!field || field[0] == '\0') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
char tmp[128];
|
||||
int n = snprintf(tmp, sizeof(tmp), "%s", field);
|
||||
if (n < 0 || (size_t)n >= sizeof(tmp)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int tokens = 0;
|
||||
char* saveptr = NULL;
|
||||
char* token = strtok_r(tmp, ",", &saveptr);
|
||||
while (token) {
|
||||
while (*token == ' ') token++;
|
||||
if (!cron_field_token_valid(token, min_v, max_v)) {
|
||||
return 0;
|
||||
}
|
||||
tokens++;
|
||||
token = strtok_r(NULL, ",", &saveptr);
|
||||
}
|
||||
|
||||
return tokens > 0;
|
||||
}
|
||||
|
||||
static int cron_expr_is_valid(const char* expr, char* normalized_out, size_t normalized_out_size) {
|
||||
if (!normalized_out || normalized_out_size == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (cron_normalize_expression(expr, normalized_out, normalized_out_size, NULL) != 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
char parse_buf[TRIGGER_FILTER_JSON_MAX];
|
||||
int n = snprintf(parse_buf, sizeof(parse_buf), "%s", normalized_out);
|
||||
if (n < 0 || (size_t)n >= sizeof(parse_buf)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
char* fields[5] = {0};
|
||||
int nf = 0;
|
||||
char* saveptr = NULL;
|
||||
char* tok = strtok_r(parse_buf, " \t", &saveptr);
|
||||
while (tok && nf < 5) {
|
||||
fields[nf++] = tok;
|
||||
tok = strtok_r(NULL, " \t", &saveptr);
|
||||
}
|
||||
if (nf != 5 || tok != NULL) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!cron_field_valid(fields[0], 0, 59)) return 0;
|
||||
if (!cron_field_valid(fields[1], 0, 23)) return 0;
|
||||
if (!cron_field_valid(fields[2], 1, 31)) return 0;
|
||||
if (!cron_field_valid(fields[3], 1, 12)) return 0;
|
||||
if (!cron_field_valid(fields[4], 0, 6)) return 0;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int cron_matches_now(const char* expr, time_t now_ts) {
|
||||
if (!expr || expr[0] == '\0') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
char buf[TRIGGER_FILTER_JSON_MAX];
|
||||
snprintf(buf, sizeof(buf), "%s", expr);
|
||||
char normalized[TRIGGER_FILTER_JSON_MAX];
|
||||
if (!cron_expr_is_valid(expr, normalized, sizeof(normalized))) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
char parse_buf[TRIGGER_FILTER_JSON_MAX];
|
||||
snprintf(parse_buf, sizeof(parse_buf), "%s", normalized);
|
||||
|
||||
char* fields[5] = {0};
|
||||
int nf = 0;
|
||||
char* saveptr = NULL;
|
||||
char* tok = strtok_r(buf, " \t", &saveptr);
|
||||
char* tok = strtok_r(parse_buf, " \t", &saveptr);
|
||||
while (tok && nf < 5) {
|
||||
fields[nf++] = tok;
|
||||
tok = strtok_r(NULL, " \t", &saveptr);
|
||||
@@ -612,7 +796,7 @@ int trigger_manager_init(trigger_manager_t* mgr, didactyl_config_t* cfg) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
mgr->last_poll_at = time(NULL);
|
||||
mgr->last_poll_at = 0;
|
||||
|
||||
DEBUG_INFO("[didactyl] trigger manager initialized (capacity=%d)", mgr->capacity);
|
||||
return 0;
|
||||
@@ -985,7 +1169,14 @@ int trigger_manager_add(trigger_manager_t* mgr,
|
||||
t->last_seen_created_at = 0;
|
||||
t->last_cron_fire = 0;
|
||||
if (t->trigger_type == TRIGGER_TYPE_CRON) {
|
||||
snprintf(t->cron_expr, sizeof(t->cron_expr), "%s", filter_json);
|
||||
char normalized_cron[sizeof(t->cron_expr)] = {0};
|
||||
if (!cron_expr_is_valid(filter_json, normalized_cron, sizeof(normalized_cron))) {
|
||||
pthread_mutex_unlock(&mgr->mutex);
|
||||
DEBUG_WARN("[didactyl] trigger add rejected: invalid cron expression d_tag=%s expr=%s", skill_d_tag, filter_json);
|
||||
memset(t, 0, sizeof(*t));
|
||||
return -1;
|
||||
}
|
||||
snprintf(t->cron_expr, sizeof(t->cron_expr), "%s", normalized_cron);
|
||||
}
|
||||
snprintf(t->llm_spec, sizeof(t->llm_spec), "%s", llm_spec ? llm_spec : "");
|
||||
snprintf(t->tools_policy, sizeof(t->tools_policy), "%s", tools_policy ? tools_policy : "");
|
||||
@@ -1208,7 +1399,13 @@ int trigger_manager_update(trigger_manager_t* mgr,
|
||||
t->enabled = clamp_enabled(enabled);
|
||||
t->trigger_type = trigger_type_from_string(trigger_type_str);
|
||||
if (t->trigger_type == TRIGGER_TYPE_CRON) {
|
||||
snprintf(t->cron_expr, sizeof(t->cron_expr), "%s", filter_json);
|
||||
char normalized_cron[sizeof(t->cron_expr)] = {0};
|
||||
if (!cron_expr_is_valid(filter_json, normalized_cron, sizeof(normalized_cron))) {
|
||||
pthread_mutex_unlock(&mgr->mutex);
|
||||
DEBUG_WARN("[didactyl] trigger update rejected: invalid cron expression d_tag=%s expr=%s", skill_d_tag, filter_json);
|
||||
return -1;
|
||||
}
|
||||
snprintf(t->cron_expr, sizeof(t->cron_expr), "%s", normalized_cron);
|
||||
} else {
|
||||
t->cron_expr[0] = '\0';
|
||||
}
|
||||
@@ -1261,6 +1458,7 @@ int trigger_manager_poll(trigger_manager_t* mgr) {
|
||||
}
|
||||
|
||||
if (mgr->last_poll_at > 0 && (now - mgr->last_poll_at) < 30) {
|
||||
DEBUG_LOG("[didactyl] trigger cron poll throttled (delta=%ld)", (long)(now - mgr->last_poll_at));
|
||||
return 0;
|
||||
}
|
||||
mgr->last_poll_at = now;
|
||||
@@ -1276,9 +1474,16 @@ int trigger_manager_poll(trigger_manager_t* mgr) {
|
||||
}
|
||||
|
||||
const char* expr = (t->cron_expr[0] != '\0') ? t->cron_expr : t->filter_json;
|
||||
if (!cron_matches_now(expr, now)) {
|
||||
char normalized_expr[64] = {0};
|
||||
if (!cron_expr_is_valid(expr, normalized_expr, sizeof(normalized_expr))) {
|
||||
DEBUG_WARN("[didactyl] cron trigger skipped: invalid expression d_tag=%s expr=%s", t->skill_d_tag, expr ? expr : "");
|
||||
continue;
|
||||
}
|
||||
if (!cron_matches_now(normalized_expr, now)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
DEBUG_INFO("[didactyl] cron trigger matched d_tag=%s expr=%s", t->skill_d_tag, normalized_expr);
|
||||
|
||||
if (t->last_cron_fire > 0 && (now - t->last_cron_fire) < 50) {
|
||||
continue;
|
||||
@@ -1296,7 +1501,7 @@ int trigger_manager_poll(trigger_manager_t* mgr) {
|
||||
if (event) {
|
||||
cJSON_AddStringToObject(event, "type", "cron");
|
||||
cJSON_AddStringToObject(event, "d_tag", trigger_copy.skill_d_tag);
|
||||
cJSON_AddStringToObject(event, "cron_expr", expr);
|
||||
cJSON_AddStringToObject(event, "cron_expr", normalized_expr);
|
||||
cJSON_AddNumberToObject(event, "created_at", (double)now);
|
||||
(void)mgr;
|
||||
execute_llm_action(&trigger_copy, event, "cron");
|
||||
@@ -1438,6 +1643,10 @@ char* trigger_manager_status_json(trigger_manager_t* mgr) {
|
||||
if (t->has_seed) cJSON_AddNumberToObject(item, "seed", t->seed);
|
||||
cJSON_AddNumberToObject(item, "last_fired", (double)t->last_fired);
|
||||
cJSON_AddNumberToObject(item, "last_seen_created_at", (double)t->last_seen_created_at);
|
||||
if (t->trigger_type == TRIGGER_TYPE_CRON) {
|
||||
cJSON_AddStringToObject(item, "cron_expr", t->cron_expr);
|
||||
cJSON_AddNumberToObject(item, "last_cron_fire", (double)t->last_cron_fire);
|
||||
}
|
||||
cJSON_AddItemToArray(arr, item);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user