Compare commits

...
6 Commits
23 changed files with 3249 additions and 182 deletions
+16
View File
@@ -1,4 +1,20 @@
# Sibling workspace directories (not part of this project)
/c_utils_lib/
/nostr_core_lib/
/openclaw/
/c-relay/
/nips/
# Build artifacts
/build/
/didactyl
*.o
*.a
*.tar.gz
debug.log
a.out
# Editor/IDE
*.swp
*.swo
*~
+9
View File
@@ -0,0 +1,9 @@
---
description: "Build Didactyl for end-of-change validation"
---
After making code changes, run [`build_static.sh`](build_static.sh) instead of `make` for final compile validation.
Example:
./build_static.sh
+7
View File
@@ -0,0 +1,7 @@
---
description: "Increments and pushes the repo"
---
Run increment_and_push.sh followed in the command line with a good description of the changes that were made.
For example: ./increment_and_push.sh "Fixed that nasty bug"
+116
View File
@@ -0,0 +1,116 @@
# Alpine-based MUSL static binary builder for Didactyl
# Produces truly portable binaries with zero runtime dependencies
ARG DEBUG_BUILD=false
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
# Set working directory
WORKDIR /build
# Build libsecp256k1 static (cached layer - only rebuilds if Alpine version changes)
RUN cd /tmp && \
git clone https://github.com/bitcoin-core/secp256k1.git && \
cd secp256k1 && \
./autogen.sh && \
./configure --enable-static --disable-shared --prefix=/usr \
CFLAGS="-fPIC" && \
make -j$(nproc) && \
make install && \
rm -rf /tmp/secp256k1
# Copy nostr_core_lib source files (cached unless nostr_core_lib changes)
# NOTE: nostr_core_lib currently compiles request_validator, which includes <sqlite3.h>,
# so sqlite-dev/sqlite-static are required in this image even if didactyl does not use sqlite directly.
COPY nostr_core_lib /build/nostr_core_lib/
# Build nostr_core_lib with all NIPs
# Disable fortification in build.sh to prevent __*_chk symbol issues
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 && \
rm -f *.o *.a 2>/dev/null || true && \
./build.sh --nips=all
# Copy didactyl source files LAST (only this layer rebuilds on source changes)
COPY src/ /build/src/
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 \
CFLAGS="-g -O2 -DDEBUG"; \
STRIP_CMD="echo 'Keeping debug symbols'"; \
echo "Building with DEBUG symbols enabled (optimized with -O2)"; \
else \
CFLAGS="-O2"; \
STRIP_CMD="strip /build/didactyl_static"; \
echo "Building optimized production binary (symbols stripped)"; \
fi && \
CURL_LIBS="$(pkg-config --static --libs libcurl)" && \
OPENSSL_LIBS="$(pkg-config --static --libs openssl)" && \
gcc -static $CFLAGS -Wall -Wextra -std=c99 \
-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 \
-I. -Isrc -Inostr_core_lib -Inostr_core_lib/nostr_core \
-Inostr_core_lib/cjson -Inostr_core_lib/nostr_websocket \
src/main.c src/config.c src/context.c src/llm.c \
src/nostr_handler.c src/agent.c src/tools.c src/debug.c \
-o /build/didactyl_static \
nostr_core_lib/libnostr_core_x64.a \
-lsecp256k1 \
$OPENSSL_LIBS \
$CURL_LIBS \
-lpthread -lm -ldl && \
eval "$STRIP_CMD"
# Verify it's truly static
RUN echo "=== Binary Information ===" && \
file /build/didactyl_static && \
ls -lh /build/didactyl_static && \
echo "=== Checking for dynamic dependencies ===" && \
(ldd /build/didactyl_static 2>&1 || echo "Binary is static") && \
echo "=== Build complete ==="
# Output stage - just the binary
FROM scratch AS output
COPY --from=builder /build/didactyl_static /didactyl_static
+52 -4
View File
@@ -11,7 +11,8 @@ SRCS = \
$(SRC_DIR)/llm.c \
$(SRC_DIR)/nostr_handler.c \
$(SRC_DIR)/agent.c \
$(SRC_DIR)/secp_compat.c
$(SRC_DIR)/tools.c \
$(SRC_DIR)/debug.c
INCLUDES = \
-I$(SRC_DIR) \
@@ -21,11 +22,57 @@ INCLUDES = \
NOSTR_LIB = $(firstword $(wildcard ../nostr_core_lib/libnostr_core_*.a))
LDFLAGS = -lcurl -lssl -lcrypto -lm -lpthread -ldl -lz -lsecp256k1
LDFLAGS = -lcurl -lssl -lcrypto -lm -lpthread -ldl -lz -L/usr/local/lib -lsecp256k1
# Build directory
BUILD_DIR = build
all: deps $(TARGET)
$(TARGET): $(SRCS)
$(BUILD_DIR):
mkdir -p $(BUILD_DIR)
# Build nostr_core_lib
$(NOSTR_LIB):
@echo "Building nostr_core_lib with all NIPs..."
cd ../nostr_core_lib && ./build.sh --nips=all
# Update main.h version information (requires main.h to exist)
src/main.h:
@if [ ! -f src/main.h ]; then \
echo "ERROR: src/main.h not found!"; \
echo "Please ensure src/main.h exists with version metadata."; \
exit 1; \
fi; \
if [ -d .git ]; then \
echo "Updating main.h version information from git tags..."; \
RAW_VERSION=$$(git describe --tags --always 2>/dev/null || echo "unknown"); \
if echo "$$RAW_VERSION" | grep -q "^v[0-9]"; then \
CLEAN_VERSION=$$(echo "$$RAW_VERSION" | sed 's/^v//' | cut -d- -f1); \
VERSION="v$$CLEAN_VERSION"; \
MAJOR=$$(echo "$$CLEAN_VERSION" | cut -d. -f1); \
MINOR=$$(echo "$$CLEAN_VERSION" | cut -d. -f2); \
PATCH=$$(echo "$$CLEAN_VERSION" | cut -d. -f3); \
else \
VERSION="v0.0.0"; \
MAJOR=0; MINOR=0; PATCH=0; \
fi; \
echo "Updating version information in existing main.h..."; \
sed -i "s/#define DIDACTYL_VERSION \".*\"/#define DIDACTYL_VERSION \"$$VERSION\"/g" src/main.h; \
sed -i "s/#define DIDACTYL_VERSION_MAJOR [0-9]*/#define DIDACTYL_VERSION_MAJOR $$MAJOR/g" src/main.h; \
sed -i "s/#define DIDACTYL_VERSION_MINOR [0-9]*/#define DIDACTYL_VERSION_MINOR $$MINOR/g" src/main.h; \
sed -i "s/#define DIDACTYL_VERSION_PATCH [0-9]*/#define DIDACTYL_VERSION_PATCH $$PATCH/g" src/main.h; \
echo "Updated main.h version to: $$VERSION"; \
else \
echo "Git not available, preserving existing main.h version information"; \
fi
# Force update version
force-version:
@echo "Force updating main.h version information..."
@$(MAKE) src/main.h
$(TARGET): src/main.h $(SRCS) $(NOSTR_LIB)
@if [ -z "$(NOSTR_LIB)" ]; then echo "nostr_core_lib static library not found; run 'make deps'"; exit 1; fi
$(CC) $(CFLAGS) $(INCLUDES) -o $@ $(SRCS) $(NOSTR_LIB) $(LDFLAGS)
@@ -34,5 +81,6 @@ deps:
clean:
rm -f $(TARGET)
rm -rf $(BUILD_DIR)
.PHONY: all deps clean
.PHONY: all deps clean force-version
+165 -55
View File
@@ -1,6 +1,6 @@
# Didactyl
A sovereign AI agent daemon written in C99 that uses Nostr as its primary operating layer.
An unstoppable agentic system.
Didactyl boots on any internet-connected machine, connects to Nostr relays, listens for encrypted commands from its administrator, reasons with an LLM, and takes actions — posting events, querying relays, running shell commands — all orchestrated through Nostr.
@@ -10,48 +10,53 @@ Didactyl boots on any internet-connected machine, connects to Nostr relays, list
Because all identity, communication, and memory live on Nostr, the agent is **portable** (start it anywhere) and **sovereign** (no single entity can erase its memory).
**Skills are the new apps.** Agents learn capabilities through skills — public Nostr events that any agent can discover, adopt, and share. There is no app store, no gatekeeper, no approval process. If someone publishes a useful skill, your agent can find it through your web of trust and start using it. Popularity is measured by adoption, not by a rating algorithm. The best skills spread because agents actually use them.
## Current Status
**MVP — Working chat agent with relay connectivity and LLM integration.**
**Active build — relay-aware autonomous agent with tool-use and Nostr-native startup memory.**
- Connects to configured Nostr relays with auto-reconnect
- Publishes agent profile (kind 0 metadata)
- Connects to configured relays with auto-reconnect and relay state transition logging
- Publishes configured startup events per relay as each relay becomes connected
- Uses kind `31120` startup content as live Soul at boot
- Listens for NIP-04 encrypted DMs from authorized admin
- Forwards messages to an OpenAI-compatible LLM API
- Sends LLM responses back as encrypted DMs
- Runtime logging: relay status, connection health, message flow
**Next: Agentic tool-use system** — see [plans/didactyl_agentic.md](plans/didactyl_agentic.md).
- Builds LLM context from system prompt + startup events + last 12 DM turns
- Supports tool-calling loop with configurable max turns and local safety limits
- Appends every outbound LLM context payload to [`context.log`](context.log)
## Quick Start
### Prerequisites
- GCC with C99 support
- libcurl, libssl, libcrypto, libsecp256k1
- Docker (for static binary build)
- An OpenAI-compatible LLM API key (OpenAI, PPQ, Ollama, etc.)
- A Nostr keypair (nsec)
### Build
```bash
make deps # builds nostr_core_lib
make # builds didactyl
./build_static.sh # builds a fully static MUSL binary via Docker
```
### Configure
Edit `config.json`:
Edit [`config.json`](config.json):
```json
{
"agent": {
"name": "Didactyl Agent",
"display_name": "Didactyl",
"about": "A sovereign AI agent on Nostr"
"about": "A sovereign AI agent on Nostr",
"picture": "https://...",
"banner": "https://...",
"nip05": ""
},
"keys": {
"nsec": "nsec1..."
"nsec": "nsec1...",
"npub": "npub1...",
"npubHex": "<optional helper>",
"nsecHex": "<optional helper>"
},
"admin": {
"pubkey": "npub1... or hex pubkey"
@@ -61,17 +66,44 @@ Edit `config.json`:
"wss://nos.lol"
],
"llm": {
"provider": "openai",
"provider": "openai|ppq|...",
"api_key": "sk-...",
"model": "gpt-4o-mini",
"base_url": "https://api.openai.com/v1",
"max_tokens": 512,
"temperature": 0.7
}
},
"tools": {
"enabled": true,
"max_turns": 8,
"shell": {
"enabled": true,
"timeout_seconds": 30,
"max_output_bytes": 65536,
"working_directory": "."
}
},
"startup_events": [
{
"kind": 31120,
"content": "You are Didactyl...",
"tags": [["d", "soul"], ["app", "didactyl"], ["scope", "private"]]
},
{
"kind": 31123,
"content_fields": {"name": "long_form_note", "description": "..."},
"tags": [["d", "long_form_note"], ["app", "didactyl"], ["scope", "public"], ["slug", "long_form_note"]]
},
{
"kind": 10123,
"content": "",
"tags": [["a", "31123:<author-pubkey>:long_form_note"], ["app", "didactyl"], ["scope", "public"]]
}
]
}
```
Edit `SYSTEM.md` to define the agent's personality and instructions.
`startup_events[].content_fields` is accepted for human-readable authoring and encoded to JSON string content at runtime.
### Run
@@ -82,7 +114,7 @@ Edit `SYSTEM.md` to define the agent's personality and instructions.
Options:
```
./didactyl --config <path> # custom config file (default: ./config.json)
./didactyl --context <path> # custom context file (default: ./SYSTEM.md)
./didactyl --debug <0-5> # log verbosity (0 none, 3 info, 5 trace)
```
### Talk to it
@@ -92,45 +124,116 @@ Send an encrypted DM to the agent's pubkey from the admin account using any Nost
## Architecture
```
┌─────────────────────────────────────────────┐
│ Didactyl │
│ │
│ ┌─────────┐ ┌─────────┐ ┌────────────┐ │
│ │ config │ │ context │ │ agent │ │
│ │ loader │ │ loader │ │ loop │ │
│ └────┬────┘ └────┬────┘ └─────┬──────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────────────────────────
│ │ nostr_handler │ │
│ │ relay pool · subscribe · publish │ │
│ └──────────────────┬──────────────────┘ │
│ │ │
│ ┌──────────────────┴──────────────────┐ │
│ │ LLM client │ │
│ │ OpenAI-compatible chat API │ │
│ └─────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────
Didactyl │
│ ┌─────────┐ ┌─────────┐ ┌────────────┐ │
│ │ config │ │ skills │ │ agent │ │
│ │ loader │ │ loader │ │ loop │ │
│ └────┬────┘ └────┬────┘ └─────┬──────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────────────────────────────┐
│ │ nostr_handler │
│ │ relay pool · subscribe · publish │
│ └──────────────────┬──────────────────┘
│ │
│ ┌──────────────────┴──────────────────┐
│ │ LLM client │
│ │ OpenAI-compatible chat API │
│ └─────────────────────────────────────┘
└─────────────────────────────────────────────
│ │
▼ ▼
Nostr Relays LLM API
```
## Didactyl Kinds (Nostr)
Didactyl uses a two-layer skill model: authors publish public skill definitions, and adopters publish which skills they use.
- `31120`**Soul** (private instruction baseline)
- `d=soul`
- `31123`**Public Skill Definition** (markdown skill body in `content` or structured JSON in `content_fields`)
- `d=<skill_slug>` (example: `d=long_form_note`)
- `31124`**Private Skill Definition** (private/internal procedures)
- `d=<skill_slug>` (example: `d=admin_ops`)
- `10123`**Public Skill Adoption List**
- tags contain one or more `a` references to selected `31123` skills
## Skill Sharing & Discovery
Skills are shared across Nostr without any centralized registry or approval process.
### How it works
1. **Publish**: An author publishes a skill as a kind `31123` event. The `content` field contains the skill body (markdown or structured JSON). The `d` tag is the skill's slug (e.g. `long_form_note`).
2. **Adopt**: An agent that wants to use a skill adds an `a`-tag reference to its kind `10123` adoption list. This is a public, replaceable event — anyone can see which skills an agent uses.
3. **Discover**: A new user queries `{"kinds": [10123], "authors": [<my-follows>]}` to see which skills their web of trust has adopted. The most-referenced `31123` addresses are the most popular skills — no rating system needed.
4. **Improve**: Anyone can publish their own `31123` with the same slug but a different pubkey. If their version is better, people adopt it instead. Competition happens through adoption, not through a store ranking.
### Why this works
- **No gatekeeper**: Skills are just Nostr events. Anyone can publish one.
- **WoT as curation**: You see what people you trust actually use, not what an algorithm promotes.
- **Visible adoption**: The `10123` list is public. Popularity is a countable fact, not a manipulable score.
- **Censorship resistant**: Skills live on relays. No single entity can remove a skill from the network.
## Startup
Didactyl startup behavior is configured in [`config.json`](config.json) under `startup_events`.
Also used at startup:
- `0` — profile metadata
- `10002` — relay list
- `1` — optional startup note/status
- `3` — contacts/follows (optional placeholder)
On boot, Didactyl attempts startup publishes to each relay as that relay transitions to connected state.
## Runtime Context Model
For each admin DM request, Didactyl builds message context in this order:
1. Soul message from kind `31120` (or fallback default)
2. Startup events memory block (`kinds/content/tags` snapshot)
3. Last 12 decrypted DM turns between admin and agent
4. Current user message
Every serialized LLM context payload is appended to [`context.log`](context.log).
## Tooling Interface
Current tool schema exposed to the LLM in [`tools_build_openai_schema_json()`](src/tools.c:72):
- `nostr_post`
- `nostr_query`
- `shell_exec`
- `file_read`
- `file_write`
Execution entrypoint: [`tools_execute()`](src/tools.c:434).
## Project Structure
```
.
├── config.json # Agent configuration
├── SYSTEM.md # Agent personality/instructions for LLM
├── config.json # Agent/runtime config including startup_events + tools
├── context.log # Appended outbound LLM context payloads
├── Makefile # Build system
├── build_static.sh # Preferred final build validation
├── src/
│ ├── main.c # Entry point, signal handling, daemon loop
│ ├── config.c / .h # JSON config parsing, key decoding
│ ├── context.c / .h # SYSTEM.md file loader
│ ├── agent.c / .h # Core agent logic: receive → LLM → respond
│ ├── main.c # Entry point, args (--config/--debug), lifecycle
│ ├── config.c / .h # JSON config parsing, key decode, startup events
│ ├── agent.c / .h # Context assembly, tool loop, DM response flow
│ ├── tools.c / .h # LLM tool schema and tool execution
│ ├── llm.c / .h # LLM HTTP API client (OpenAI-compatible)
│ ├── nostr_handler.c / .h # Relay pool, subscriptions, publish, DMs
│ └── secp_compat.c # secp256k1 API compatibility shim
│ ├── nostr_handler.c / .h # Relay pool, subscriptions, publish, startup reconcile
│ └── debug.c / .h # Runtime log levels/macros
├── plans/ # Architecture and planning documents
│ ├── didactyl_mvp.md
│ └── didactyl_agentic.md
@@ -139,26 +242,33 @@ Send an encrypted DM to the agent's pubkey from the admin account using any Nost
## Dependencies
All dependencies are statically linked into the binary at build time. No system libraries are required at runtime.
| Dependency | Purpose | Source |
|---|---|---|
| nostr_core_lib | Nostr protocol: keys, events, NIPs, relay pool | Workspace (sibling directory) |
| cJSON | JSON parsing | Bundled in nostr_core_lib |
| libcurl | HTTPS for LLM API calls | System package |
| libssl / libcrypto | TLS for WebSocket relay connections | System package |
| libsecp256k1 | Schnorr signatures, ECDH | System package |
| libcurl | HTTPS for LLM API calls | Statically linked (Alpine/MUSL) |
| libssl / libcrypto | TLS for WebSocket relay connections | Statically linked (Alpine/MUSL) |
| libsecp256k1 | Schnorr signatures, ECDH | Statically linked (Alpine/MUSL) |
## Roadmap
- [x] MVP chat agent — DM in, LLM response out
- [x] Relay pool with auto-reconnect and status logging
- [x] Runtime diagnostics — relay health, message flow, LLM calls
- [ ] **Agentic tool-use** — LLM can call tools (nostr_post, nostr_query, shell_exec)
- [x] Per-relay startup publish on relay-connected transitions
- [x] Runtime diagnostics — relay health, message flow, event kind publish logs
- [x] Tool-calling loop (nostr_post, nostr_query, shell_exec, file_read, file_write)
- [x] Context assembly with startup events + recent DM history
- [x] Context payload logging to [`context.log`](context.log)
- [x] Skill kind definitions (`31120` Soul, `31123` Public Skill, `31124` Private Skill)
- [x] Skill adoption list (`10123`) for WoT-driven discovery
- [ ] Runtime skill loading from adopted `31123` events on relays
- [ ] Skill discovery CLI/tool (query WoT adoption lists)
- [ ] Upgrade to NIP-17 gift-wrapped DMs
- [ ] NIP-44 encrypted private skills (`31124`)
- [ ] Nostr-native data storage (kind 30078 app-specific events)
- [ ] Blossom blob storage integration
- [ ] Conversation memory on Nostr
- [ ] Config and SYSTEM.md stored as Nostr events
- [ ] Multi-turn conversation context window
- [ ] Agent-to-agent communication
## License
+8
View File
@@ -11,6 +11,14 @@ You are Didactyl, a sovereign AI agent living on Nostr.
- If unsure, state uncertainty directly.
- Prefer actionable, practical advice.
## Tool Use Policy
- You have tools available and should use them when a request requires taking action.
- For requests involving local inspection or command execution, call `shell_exec` instead of refusing.
- For posting to Nostr, call `nostr_post` with explicit `kind` and `content`.
- For relay/event lookup tasks, call `nostr_query` with an appropriate filter.
- After a tool call, base your answer on the actual tool result.
- Never claim a tool was run if no tool was executed.
## Safety
- Do not claim to have executed actions you did not execute.
- Do not reveal secrets from configuration or keys.
+201
View File
@@ -0,0 +1,201 @@
#!/bin/bash
# Build fully static MUSL binaries for Didactyl using Alpine Docker
# Produces truly portable binaries with zero runtime dependencies
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
OUTPUT_DIR="$SCRIPT_DIR"
DOCKERFILE="$SCRIPT_DIR/Dockerfile.alpine-musl"
# Parse command line arguments
DEBUG_BUILD=false
if [[ "$1" == "--debug" ]]; then
DEBUG_BUILD=true
echo "=========================================="
echo "Didactyl MUSL Static Binary Builder (DEBUG MODE)"
echo "=========================================="
else
echo "=========================================="
echo "Didactyl MUSL Static Binary Builder (PRODUCTION MODE)"
echo "=========================================="
fi
echo "Project directory: $SCRIPT_DIR"
echo "Output directory: $OUTPUT_DIR"
echo "Debug build: $DEBUG_BUILD"
echo ""
# Remove legacy build directory if present
if [ -d "$SCRIPT_DIR/build" ]; then
rm -rf "$SCRIPT_DIR/build"
echo "Removed legacy build directory: $SCRIPT_DIR/build"
fi
# Check if Docker is available
if ! command -v docker &> /dev/null; then
echo "ERROR: Docker is not installed or not in PATH"
echo ""
echo "Docker is required to build MUSL static binaries."
echo "Please install Docker:"
echo " - Ubuntu/Debian: sudo apt install docker.io"
echo " - Or visit: https://docs.docker.com/engine/install/"
echo ""
exit 1
fi
# Check if Docker daemon is running
if ! docker info &> /dev/null; then
echo "ERROR: Docker daemon is not running or user not in docker group"
echo ""
echo "Please start Docker and ensure you're in the docker group:"
echo " - sudo systemctl start docker"
echo " - sudo usermod -aG docker $USER && newgrp docker"
echo " - Or start Docker Desktop"
echo ""
exit 1
fi
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
# Append _debug suffix to output name for debug builds
if [ "$DEBUG_BUILD" = true ]; then
OUTPUT_NAME="${OUTPUT_NAME}_debug"
fi
echo "Building for platform: $PLATFORM"
echo "Output binary: $OUTPUT_NAME"
echo ""
# 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
echo "⚠ Alpine 3.19 image not found in cache"
echo "Attempting to pull Alpine 3.19 image..."
if ! docker pull alpine:3.19; then
echo ""
echo "ERROR: Failed to pull Alpine 3.19 image"
echo "This is required for the static build."
echo ""
exit 1
fi
echo "✓ Alpine 3.19 image pulled successfully"
else
echo "✓ Alpine 3.19 image found in cache"
fi
echo ""
# Build the Docker image
echo "=========================================="
echo "Step 1: Building Alpine Docker image"
echo "=========================================="
echo "This will:"
echo " - Use Alpine Linux (native MUSL)"
echo " - Build all dependencies statically"
echo " - Compile didactyl with full static linking"
echo ""
$DOCKER_CMD build \
--platform "$PLATFORM" \
--build-arg DEBUG_BUILD=$DEBUG_BUILD \
-f "$DOCKERFILE" \
-t didactyl-musl-builder:latest \
--progress=plain \
. || {
echo ""
echo "ERROR: Docker build failed"
echo "Check the output above for details"
exit 1
}
echo ""
echo "✓ Docker image built successfully"
echo ""
# Extract the binary from the container
echo "=========================================="
echo "Step 2: Extracting static binary"
echo "=========================================="
# Reuse the already-built final image and extract binary directly
# (avoids a second docker build pass)
CONTAINER_ID=$($DOCKER_CMD create didactyl-musl-builder:latest /didactyl_static)
# Copy binary from final image
$DOCKER_CMD cp "$CONTAINER_ID:/didactyl_static" "$OUTPUT_DIR/$OUTPUT_NAME" || {
echo "ERROR: Failed to extract binary from container"
$DOCKER_CMD rm "$CONTAINER_ID" 2>/dev/null
exit 1
}
# Clean up container
$DOCKER_CMD rm "$CONTAINER_ID" > /dev/null
echo "✓ Binary extracted to: $OUTPUT_DIR/$OUTPUT_NAME"
echo ""
# Make binary executable
chmod +x "$OUTPUT_DIR/$OUTPUT_NAME"
# Verify the binary
echo "=========================================="
echo "Step 3: Verifying static binary"
echo "=========================================="
echo ""
echo "Checking for dynamic dependencies:"
if LDD_OUTPUT=$(timeout 5 ldd "$OUTPUT_DIR/$OUTPUT_NAME" 2>&1); then
if echo "$LDD_OUTPUT" | grep -q "not a dynamic executable"; then
echo "✓ Binary is fully static (no dynamic dependencies)"
TRULY_STATIC=true
elif echo "$LDD_OUTPUT" | grep -q "statically linked"; then
echo "✓ Binary is statically linked"
TRULY_STATIC=true
else
echo "⚠ WARNING: Binary may have dynamic dependencies:"
echo "$LDD_OUTPUT"
TRULY_STATIC=false
fi
else
# ldd failed or timed out - check with file command instead
if file "$OUTPUT_DIR/$OUTPUT_NAME" | grep -q "statically linked"; then
echo "✓ Binary is statically linked (verified with file command)"
TRULY_STATIC=true
else
echo "⚠ Could not verify static linking (ldd check failed)"
TRULY_STATIC=false
fi
fi
echo ""
echo "=========================================="
echo "Build Summary"
echo "=========================================="
echo "Binary: $OUTPUT_DIR/$OUTPUT_NAME"
echo "Size: $(du -h "$OUTPUT_DIR/$OUTPUT_NAME" | cut -f1)"
echo "Static: $TRULY_STATIC"
echo "Debug: $DEBUG_BUILD"
echo "Platform: $PLATFORM"
echo "=========================================="
+503
View File
@@ -0,0 +1,503 @@
#!/bin/bash
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
print_status() { echo -e "${BLUE}[INFO]${NC} $1" >&2; }
print_success() { echo -e "${GREEN}[SUCCESS]${NC} $1" >&2; }
print_warning() { echo -e "${YELLOW}[WARNING]${NC} $1" >&2; }
print_error() { echo -e "${RED}[ERROR]${NC} $1" >&2; }
# Global variables
COMMIT_MESSAGE=""
RELEASE_MODE=false
VERSION_INCREMENT_TYPE="patch" # "patch", "minor", or "major"
show_usage() {
echo "Didactyl Increment and Push Script"
echo ""
echo "USAGE:"
echo " $0 [OPTIONS] \"commit message\""
echo ""
echo "COMMANDS:"
echo " $0 \"commit message\" Default: increment patch, commit & push"
echo " $0 -p \"commit message\" Increment patch version"
echo " $0 -m \"commit message\" Increment minor version"
echo " $0 -M \"commit message\" Increment major version"
echo " $0 -r \"commit message\" Create release with assets (no version increment)"
echo " $0 -r -m \"commit message\" Create release with minor version increment"
echo " $0 -h Show this help message"
echo ""
echo "OPTIONS:"
echo " -p, --patch Increment patch version (default)"
echo " -m, --minor Increment minor version"
echo " -M, --major Increment major version"
echo " -r, --release Create release with assets"
echo " -h, --help Show this help message"
echo ""
echo "EXAMPLES:"
echo " $0 \"Fixed event signing bug\""
echo " $0 -m \"Added tool-use system\""
echo " $0 -M \"Breaking API changes\""
echo " $0 -r \"Release current version\""
echo " $0 -r -m \"Release with minor increment\""
echo ""
echo "VERSION INCREMENT MODES:"
echo " -p, --patch (default): Increment patch version (v0.1.0 → v0.1.1)"
echo " -m, --minor: Increment minor version, zero patch (v0.1.0 → v0.2.0)"
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 " - Git add, commit, push, and create Gitea release with assets"
echo " - Can be combined with version increment flags"
echo ""
echo "REQUIREMENTS FOR RELEASE MODE:"
echo " - Gitea token in ~/.gitea_token for release uploads"
echo " - Docker installed for static binary builds"
}
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
-r|--release)
RELEASE_MODE=true
shift
;;
-p|--patch)
VERSION_INCREMENT_TYPE="patch"
shift
;;
-m|--minor)
VERSION_INCREMENT_TYPE="minor"
shift
;;
-M|--major)
VERSION_INCREMENT_TYPE="major"
shift
;;
-h|--help)
show_usage
exit 0
;;
*)
# First non-flag argument is the commit message
if [[ -z "$COMMIT_MESSAGE" ]]; then
COMMIT_MESSAGE="$1"
fi
shift
;;
esac
done
# Validate inputs
if [[ -z "$COMMIT_MESSAGE" ]]; then
print_error "Commit message is required"
echo ""
show_usage
exit 1
fi
# Check if we're in a git repository
check_git_repo() {
if ! git rev-parse --git-dir > /dev/null 2>&1; then
print_error "Not in a git repository"
exit 1
fi
}
# Function to get current version and increment appropriately
increment_version() {
local increment_type="$1" # "patch", "minor", or "major"
print_status "Getting current version..."
# Get the highest version tag (not chronologically latest)
LATEST_TAG=$(git tag -l 'v*.*.*' | sort -V | tail -n 1 || echo "")
if [[ -z "$LATEST_TAG" ]]; then
LATEST_TAG="v0.0.0"
print_warning "No version tags found, starting from $LATEST_TAG"
fi
# Extract version components (remove 'v' prefix)
VERSION=${LATEST_TAG#v}
# Parse major.minor.patch using regex
if [[ $VERSION =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
MAJOR=${BASH_REMATCH[1]}
MINOR=${BASH_REMATCH[2]}
PATCH=${BASH_REMATCH[3]}
else
print_error "Invalid version format in tag: $LATEST_TAG"
print_error "Expected format: v0.1.0"
exit 1
fi
# Increment version based on type
if [[ "$increment_type" == "major" ]]; then
NEW_MAJOR=$((MAJOR + 1))
NEW_MINOR=0
NEW_PATCH=0
NEW_VERSION="v${NEW_MAJOR}.${NEW_MINOR}.${NEW_PATCH}"
print_status "Major version increment: incrementing major version"
elif [[ "$increment_type" == "minor" ]]; then
NEW_MAJOR=$MAJOR
NEW_MINOR=$((MINOR + 1))
NEW_PATCH=0
NEW_VERSION="v${NEW_MAJOR}.${NEW_MINOR}.${NEW_PATCH}"
print_status "Minor version increment: incrementing minor version"
else
NEW_MAJOR=$MAJOR
NEW_MINOR=$MINOR
NEW_PATCH=$((PATCH + 1))
NEW_VERSION="v${NEW_MAJOR}.${NEW_MINOR}.${NEW_PATCH}"
print_status "Patch version increment: incrementing patch version"
fi
print_status "Current version: $LATEST_TAG"
print_status "New version: $NEW_VERSION"
# Update version in src/main.h
update_version_in_header "$NEW_VERSION" "$NEW_MAJOR" "$NEW_MINOR" "$NEW_PATCH"
# Export for use in other functions
export NEW_VERSION
}
# Function to update version macros in src/main.h
update_version_in_header() {
local new_version="$1"
local major="$2"
local minor="$3"
local patch="$4"
print_status "Updating version in src/main.h..."
# Check if src/main.h exists
if [[ ! -f "src/main.h" ]]; then
print_error "src/main.h not found"
exit 1
fi
# Update DIDACTYL_VERSION macro
sed -i "s/#define DIDACTYL_VERSION \".*\"/#define DIDACTYL_VERSION \"$new_version\"/" src/main.h
# Update DIDACTYL_VERSION_MAJOR macro
sed -i "s/#define DIDACTYL_VERSION_MAJOR [0-9]\+/#define DIDACTYL_VERSION_MAJOR $major/" src/main.h
# Update DIDACTYL_VERSION_MINOR macro
sed -i "s/#define DIDACTYL_VERSION_MINOR .*/#define DIDACTYL_VERSION_MINOR $minor/" src/main.h
# Update DIDACTYL_VERSION_PATCH macro
sed -i "s/#define DIDACTYL_VERSION_PATCH [0-9]\+/#define DIDACTYL_VERSION_PATCH $patch/" src/main.h
print_success "Updated version in src/main.h to $new_version"
}
# Function to commit and push changes without creating a tag (tag already created)
git_commit_and_push_no_tag() {
print_status "Preparing git commit..."
# Stage all changes
if git add . > /dev/null 2>&1; then
print_success "Staged all changes"
else
print_error "Failed to stage changes"
exit 1
fi
# Check if there are changes to commit
if git diff --staged --quiet; then
print_warning "No changes to commit"
else
# Commit changes
if git commit -m "$NEW_VERSION - $COMMIT_MESSAGE" > /dev/null 2>&1; then
print_success "Committed changes"
else
print_error "Failed to commit changes"
exit 1
fi
fi
# Push changes
print_status "Pushing to remote repository..."
if git push > /dev/null 2>&1; then
print_success "Pushed changes"
else
print_error "Failed to push changes"
exit 1
fi
# Push only the new tag to avoid conflicts with existing tags
if git push origin "$NEW_VERSION" > /dev/null 2>&1; then
print_success "Pushed tag: $NEW_VERSION"
else
print_warning "Tag push failed, trying force push..."
if git push --force origin "$NEW_VERSION" > /dev/null 2>&1; then
print_success "Force-pushed updated tag: $NEW_VERSION"
else
print_error "Failed to push tag: $NEW_VERSION"
exit 1
fi
fi
}
# Function to build release binary
build_release_binary() {
print_status "Building release binary..."
# Check if build_static.sh exists
if [[ ! -f "build_static.sh" ]]; then
print_error "build_static.sh not found"
return 1
fi
# Run the static build script
if ./build_static.sh > /dev/null 2>&1; then
print_success "Built static binary 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"
return 1
fi
}
# Function to upload release assets to Gitea
upload_release_assets() {
local release_id="$1"
local binary_path="$2"
local tarball_path="$3"
print_status "Uploading release assets..."
# Check for Gitea token
if [[ ! -f "$HOME/.gitea_token" ]]; then
print_warning "No ~/.gitea_token found. Skipping asset uploads."
return 0
fi
local token=$(cat "$HOME/.gitea_token" | tr -d '\n\r')
local api_url="https://git.laantungir.net/api/v1/repos/laantungir/didactyl"
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")"
# Retry loop for eventual consistency
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_path;filename=$(basename "$binary_path")" \
-F "name=$(basename "$binary_path")")
if echo "$binary_response" | grep -q '"id"'; then
print_success "Uploaded 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 binary after $max_attempts attempts"
print_error "Response: $binary_response"
fi
fi
((attempt++))
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")")
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
fi
}
# Function to create Gitea release
create_gitea_release() {
print_status "Creating Gitea release..."
# Check for Gitea token
if [[ ! -f "$HOME/.gitea_token" ]]; then
print_warning "No ~/.gitea_token found. Skipping release creation."
print_warning "Create ~/.gitea_token with your Gitea access token to enable releases."
return 0
fi
local token=$(cat "$HOME/.gitea_token" | tr -d '\n\r')
local api_url="https://git.laantungir.net/api/v1/repos/laantungir/didactyl"
# Create release
print_status "Creating release $NEW_VERSION..."
local response=$(curl -s -X POST "$api_url/releases" \
-H "Authorization: token $token" \
-H "Content-Type: application/json" \
-d "{\"tag_name\": \"$NEW_VERSION\", \"name\": \"$NEW_VERSION\", \"body\": \"$COMMIT_MESSAGE\"}")
if echo "$response" | grep -q '"id"'; then
print_success "Created release $NEW_VERSION"
local release_id=$(echo "$response" | grep -o '"id":[0-9]*' | head -1 | cut -d':' -f2)
echo $release_id
elif echo "$response" | grep -q "already exists"; then
print_warning "Release $NEW_VERSION already exists"
local check_response=$(curl -s -H "Authorization: token $token" "$api_url/releases/tags/$NEW_VERSION")
if echo "$check_response" | grep -q '"id"'; then
local release_id=$(echo "$check_response" | grep -o '"id":[0-9]*' | head -1 | cut -d':' -f2)
print_status "Using existing release ID: $release_id"
echo $release_id
else
print_error "Could not find existing release ID"
return 1
fi
else
print_error "Failed to create release $NEW_VERSION"
print_error "Response: $response"
local check_response=$(curl -s -H "Authorization: token $token" "$api_url/releases/tags/$NEW_VERSION")
if echo "$check_response" | grep -q '"id"'; then
print_warning "Release exists but creation response was unexpected"
local release_id=$(echo "$check_response" | grep -o '"id":[0-9]*' | head -1 | cut -d':' -f2)
echo $release_id
else
print_error "Release does not exist and creation failed"
return 1
fi
fi
}
# Main execution
main() {
print_status "Didactyl Increment and Push Script"
# Check prerequisites
check_git_repo
if [[ "$RELEASE_MODE" == true ]]; then
print_status "=== RELEASE MODE ==="
# Only increment version if explicitly requested
if [[ "$VERSION_INCREMENT_TYPE" != "patch" ]]; then
increment_version "$VERSION_INCREMENT_TYPE"
else
LATEST_TAG=$(git tag -l 'v*.*.*' | sort -V | tail -n 1 || echo "v0.0.0")
NEW_VERSION="$LATEST_TAG"
export NEW_VERSION
fi
# Create new git tag BEFORE compilation so version picks it up
if git tag "$NEW_VERSION" > /dev/null 2>&1; then
print_success "Created tag: $NEW_VERSION"
else
print_warning "Tag $NEW_VERSION already exists, removing and recreating..."
git tag -d "$NEW_VERSION" > /dev/null 2>&1
git tag "$NEW_VERSION" > /dev/null 2>&1
fi
# Commit and push
git_commit_and_push_no_tag
# Build release binary
if build_release_binary; then
local binary_path="build/didactyl_static_x86_64"
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=""
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"
fi
print_success "Release $NEW_VERSION completed successfully!"
else
print_error "Invalid release_id: $release_id"
exit 1
fi
else
print_error "Release creation failed"
fi
else
print_status "=== DEFAULT MODE ==="
# Increment version based on type (default to patch)
increment_version "$VERSION_INCREMENT_TYPE"
# Create new git tag BEFORE compilation so version picks it up
if git tag "$NEW_VERSION" > /dev/null 2>&1; then
print_success "Created tag: $NEW_VERSION"
else
print_warning "Tag $NEW_VERSION already exists, removing and recreating..."
git tag -d "$NEW_VERSION" > /dev/null 2>&1
git tag "$NEW_VERSION" > /dev/null 2>&1
fi
# Commit and push
git_commit_and_push_no_tag
print_success "Increment and push completed successfully!"
print_status "Version $NEW_VERSION pushed to repository"
fi
}
# Execute main function
main
+538 -11
View File
@@ -5,12 +5,409 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <time.h>
#include <pthread.h>
#include "llm.h"
#include "nostr_handler.h"
#include "tools.h"
#include "cjson/cJSON.h"
#include "../../nostr_core_lib/nostr_core/nostr_core.h"
static didactyl_config_t* g_cfg = NULL;
static char* g_system_context = NULL;
static tools_context_t g_tools_ctx;
#define AGENT_DEBOUNCE_WINDOW_SECONDS 5
#define AGENT_DEBOUNCE_CACHE_SIZE 256
#define AGENT_HISTORY_TURNS 12
#define AGENT_HISTORY_QUERY_LIMIT 200
typedef struct {
uint64_t fingerprint;
time_t seen_at;
} agent_seen_msg_t;
static agent_seen_msg_t g_seen_msgs[AGENT_DEBOUNCE_CACHE_SIZE];
static int g_seen_msgs_count = 0;
static int g_seen_msgs_next = 0;
static pthread_mutex_t g_seen_msgs_mutex = PTHREAD_MUTEX_INITIALIZER;
static uint64_t fnv1a64(const char* s) {
uint64_t h = 1469598103934665603ULL;
if (!s) return h;
while (*s) {
h ^= (unsigned char)(*s++);
h *= 1099511628211ULL;
}
return h;
}
static uint64_t message_fingerprint(const char* sender_pubkey_hex, const char* message) {
uint64_t a = fnv1a64(sender_pubkey_hex);
uint64_t b = fnv1a64(message);
return a ^ (b + 0x9e3779b97f4a7c15ULL + (a << 6) + (a >> 2));
}
static int agent_message_is_debounced(const char* sender_pubkey_hex, const char* message) {
time_t now = time(NULL);
uint64_t fp = message_fingerprint(sender_pubkey_hex, message);
int duplicate = 0;
pthread_mutex_lock(&g_seen_msgs_mutex);
for (int i = 0; i < g_seen_msgs_count; i++) {
if (g_seen_msgs[i].fingerprint == fp &&
g_seen_msgs[i].seen_at > 0 &&
(now - g_seen_msgs[i].seen_at) <= AGENT_DEBOUNCE_WINDOW_SECONDS) {
duplicate = 1;
break;
}
}
if (!duplicate) {
int slot = 0;
if (g_seen_msgs_count < AGENT_DEBOUNCE_CACHE_SIZE) {
slot = g_seen_msgs_count;
g_seen_msgs_count++;
} else {
slot = g_seen_msgs_next;
g_seen_msgs_next = (g_seen_msgs_next + 1) % AGENT_DEBOUNCE_CACHE_SIZE;
}
g_seen_msgs[slot].fingerprint = fp;
g_seen_msgs[slot].seen_at = now;
}
pthread_mutex_unlock(&g_seen_msgs_mutex);
return duplicate;
}
static int append_simple_message(cJSON* messages, const char* role, const char* content) {
if (!messages || !role) return -1;
cJSON* msg = cJSON_CreateObject();
if (!msg) return -1;
cJSON_AddStringToObject(msg, "role", role);
cJSON_AddStringToObject(msg, "content", content ? content : "");
cJSON_AddItemToArray(messages, msg);
return 0;
}
static int append_assistant_tool_calls_message(cJSON* messages, const llm_response_t* resp) {
if (!messages || !resp || resp->tool_call_count <= 0) return -1;
cJSON* assistant = cJSON_CreateObject();
cJSON* tool_calls = cJSON_CreateArray();
if (!assistant || !tool_calls) {
cJSON_Delete(assistant);
cJSON_Delete(tool_calls);
return -1;
}
cJSON_AddStringToObject(assistant, "role", "assistant");
if (resp->content) {
cJSON_AddStringToObject(assistant, "content", resp->content);
} else {
cJSON_AddNullToObject(assistant, "content");
}
for (int i = 0; i < resp->tool_call_count; i++) {
const llm_tool_call_t* tc = &resp->tool_calls[i];
cJSON* tc_obj = cJSON_CreateObject();
cJSON* fn_obj = cJSON_CreateObject();
if (!tc_obj || !fn_obj) {
cJSON_Delete(tc_obj);
cJSON_Delete(fn_obj);
cJSON_Delete(assistant);
cJSON_Delete(tool_calls);
return -1;
}
cJSON_AddStringToObject(tc_obj, "id", tc->id ? tc->id : "");
cJSON_AddStringToObject(tc_obj, "type", "function");
cJSON_AddStringToObject(fn_obj, "name", tc->name ? tc->name : "");
cJSON_AddStringToObject(fn_obj, "arguments", tc->arguments_json ? tc->arguments_json : "{}");
cJSON_AddItemToObject(tc_obj, "function", fn_obj);
cJSON_AddItemToArray(tool_calls, tc_obj);
}
cJSON_AddItemToObject(assistant, "tool_calls", tool_calls);
cJSON_AddItemToArray(messages, assistant);
return 0;
}
static int append_tool_result_message(cJSON* messages, const char* tool_call_id, const char* tool_result_json) {
if (!messages || !tool_call_id) return -1;
cJSON* msg = cJSON_CreateObject();
if (!msg) return -1;
cJSON_AddStringToObject(msg, "role", "tool");
cJSON_AddStringToObject(msg, "tool_call_id", tool_call_id);
cJSON_AddStringToObject(msg, "content", tool_result_json ? tool_result_json : "{\"success\":false,\"error\":\"empty tool result\"}");
cJSON_AddItemToArray(messages, msg);
return 0;
}
static void append_context_log(const char* sender_pubkey_hex, const char* phase, const char* context_payload) {
FILE* fp = fopen("context.log", "a");
if (!fp) {
return;
}
time_t now = time(NULL);
struct tm tm_info;
localtime_r(&now, &tm_info);
char timestamp[32] = {0};
strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", &tm_info);
fprintf(fp,
"[%s] phase=%s sender=%s\n%s\n\n---\n\n",
timestamp,
phase ? phase : "unknown",
sender_pubkey_hex ? sender_pubkey_hex : "unknown",
context_payload ? context_payload : "");
fclose(fp);
}
typedef struct {
time_t created_at;
int role_is_user;
char* content;
} agent_history_item_t;
static int extract_first_p_tag_local(cJSON* tags, char out_pubkey_hex[65]) {
if (!tags || !cJSON_IsArray(tags) || !out_pubkey_hex) {
return -1;
}
int n = cJSON_GetArraySize(tags);
for (int i = 0; i < n; i++) {
cJSON* tag = cJSON_GetArrayItem(tags, i);
if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) {
continue;
}
cJSON* key = cJSON_GetArrayItem(tag, 0);
cJSON* val = cJSON_GetArrayItem(tag, 1);
if (!key || !val || !cJSON_IsString(key) || !cJSON_IsString(val) || !val->valuestring) {
continue;
}
if (strcmp(key->valuestring, "p") == 0 && strlen(val->valuestring) == 64U) {
memcpy(out_pubkey_hex, val->valuestring, 64U);
out_pubkey_hex[64] = '\0';
return 0;
}
}
return -1;
}
static int history_item_cmp_created_at(const void* a, const void* b) {
const agent_history_item_t* ia = (const agent_history_item_t*)a;
const agent_history_item_t* ib = (const agent_history_item_t*)b;
if (ia->created_at < ib->created_at) return -1;
if (ia->created_at > ib->created_at) return 1;
return 0;
}
static void free_history_items(agent_history_item_t* items, int count) {
if (!items) return;
for (int i = 0; i < count; i++) {
free(items[i].content);
}
free(items);
}
static int append_startup_events_context(cJSON* messages) {
if (!messages || !g_cfg || g_cfg->startup_event_count <= 0 || !g_cfg->startup_events) {
return 0;
}
cJSON* arr = cJSON_CreateArray();
if (!arr) {
return -1;
}
for (int i = 0; i < g_cfg->startup_event_count; i++) {
startup_event_t* se = &g_cfg->startup_events[i];
cJSON* item = cJSON_CreateObject();
if (!item) {
cJSON_Delete(arr);
return -1;
}
cJSON_AddNumberToObject(item, "kind", se->kind);
cJSON_AddStringToObject(item, "content", se->content ? se->content : "");
cJSON* tags = NULL;
if (se->tags_json) {
tags = cJSON_Parse(se->tags_json);
}
if (!tags || !cJSON_IsArray(tags)) {
cJSON_Delete(tags);
tags = cJSON_CreateArray();
}
cJSON_AddItemToObject(item, "tags", tags);
cJSON_AddItemToArray(arr, item);
}
char* events_json = cJSON_PrintUnformatted(arr);
cJSON_Delete(arr);
if (!events_json) {
return -1;
}
const char* prefix = "Startup events memory (kinds/content/tags): ";
size_t out_len = strlen(prefix) + strlen(events_json) + 1U;
char* payload = (char*)malloc(out_len);
if (!payload) {
free(events_json);
return -1;
}
snprintf(payload, out_len, "%s%s", prefix, events_json);
free(events_json);
int rc = append_simple_message(messages, "system", payload);
free(payload);
return rc;
}
static int append_recent_admin_dm_history(cJSON* messages, const char* current_message) {
if (!messages || !g_cfg) {
return -1;
}
unsigned char admin_pubkey[32];
if (nostr_hex_to_bytes(g_cfg->admin.pubkey, admin_pubkey, 32) != 0) {
return -1;
}
cJSON* filter = cJSON_CreateObject();
cJSON* kinds = cJSON_CreateArray();
cJSON* authors = cJSON_CreateArray();
if (!filter || !kinds || !authors) {
cJSON_Delete(filter);
cJSON_Delete(kinds);
cJSON_Delete(authors);
return -1;
}
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(4));
cJSON_AddItemToObject(filter, "kinds", kinds);
cJSON_AddItemToArray(authors, cJSON_CreateString(g_cfg->admin.pubkey));
cJSON_AddItemToArray(authors, cJSON_CreateString(g_cfg->keys.public_key_hex));
cJSON_AddItemToObject(filter, "authors", authors);
cJSON_AddNumberToObject(filter, "limit", AGENT_HISTORY_QUERY_LIMIT);
char* events_json = nostr_handler_query_json(filter, 8000);
cJSON_Delete(filter);
if (!events_json) {
return 0;
}
cJSON* events = cJSON_Parse(events_json);
free(events_json);
if (!events || !cJSON_IsArray(events)) {
cJSON_Delete(events);
return 0;
}
agent_history_item_t* items = NULL;
int item_count = 0;
int n = cJSON_GetArraySize(events);
for (int i = 0; i < n; i++) {
cJSON* ev = cJSON_GetArrayItem(events, i);
cJSON* kind = ev ? cJSON_GetObjectItemCaseSensitive(ev, "kind") : NULL;
cJSON* pubkey = ev ? cJSON_GetObjectItemCaseSensitive(ev, "pubkey") : NULL;
cJSON* content = ev ? cJSON_GetObjectItemCaseSensitive(ev, "content") : NULL;
cJSON* tags = ev ? cJSON_GetObjectItemCaseSensitive(ev, "tags") : NULL;
cJSON* created_at = ev ? cJSON_GetObjectItemCaseSensitive(ev, "created_at") : NULL;
if (!kind || !pubkey || !content || !tags || !created_at ||
!cJSON_IsNumber(kind) || !cJSON_IsString(pubkey) ||
!cJSON_IsString(content) || !cJSON_IsArray(tags) || !cJSON_IsNumber(created_at) ||
(int)kind->valuedouble != 4 || !pubkey->valuestring || !content->valuestring) {
continue;
}
char p_tag_pubkey[65] = {0};
if (extract_first_p_tag_local(tags, p_tag_pubkey) != 0) {
continue;
}
int role_is_user = 0;
if (strcmp(pubkey->valuestring, g_cfg->admin.pubkey) == 0 &&
strcmp(p_tag_pubkey, g_cfg->keys.public_key_hex) == 0) {
role_is_user = 1;
} else if (strcmp(pubkey->valuestring, g_cfg->keys.public_key_hex) == 0 &&
strcmp(p_tag_pubkey, g_cfg->admin.pubkey) == 0) {
role_is_user = 0;
} else {
continue;
}
char* plaintext = (char*)malloc(NOSTR_NIP04_MAX_PLAINTEXT_SIZE);
if (!plaintext) {
continue;
}
plaintext[0] = '\0';
if (nostr_nip04_decrypt(g_cfg->keys.private_key,
admin_pubkey,
content->valuestring,
plaintext,
NOSTR_NIP04_MAX_PLAINTEXT_SIZE) != NOSTR_SUCCESS) {
free(plaintext);
continue;
}
agent_history_item_t* grown = (agent_history_item_t*)realloc(items, (size_t)(item_count + 1) * sizeof(agent_history_item_t));
if (!grown) {
free(plaintext);
free_history_items(items, item_count);
cJSON_Delete(events);
return -1;
}
items = grown;
items[item_count].created_at = (time_t)created_at->valuedouble;
items[item_count].role_is_user = role_is_user;
items[item_count].content = plaintext;
item_count++;
}
cJSON_Delete(events);
if (item_count > 1) {
qsort(items, (size_t)item_count, sizeof(agent_history_item_t), history_item_cmp_created_at);
}
int start = item_count > AGENT_HISTORY_TURNS ? item_count - AGENT_HISTORY_TURNS : 0;
for (int i = start; i < item_count; i++) {
if (i == item_count - 1 &&
items[i].role_is_user &&
current_message &&
strcmp(items[i].content, current_message) == 0) {
continue;
}
if (append_simple_message(messages,
items[i].role_is_user ? "user" : "assistant",
items[i].content ? items[i].content : "") != 0) {
free_history_items(items, item_count);
return -1;
}
}
free_history_items(items, item_count);
return 0;
}
int agent_init(didactyl_config_t* config, const char* system_context) {
if (!config || !system_context) {
@@ -23,6 +420,17 @@ int agent_init(didactyl_config_t* config, const char* system_context) {
return -1;
}
if (tools_init(&g_tools_ctx, g_cfg) != 0) {
free(g_system_context);
g_system_context = NULL;
g_cfg = NULL;
return -1;
}
memset(g_seen_msgs, 0, sizeof(g_seen_msgs));
g_seen_msgs_count = 0;
g_seen_msgs_next = 0;
return 0;
}
@@ -34,25 +442,144 @@ void agent_on_message(const char* sender_pubkey_hex, const char* message, void*
}
fprintf(stdout, "[didactyl] incoming message from %.16s...\n", sender_pubkey_hex);
fprintf(stdout, "[didactyl] calling llm for sender %.16s...\n", sender_pubkey_hex);
char* response = llm_chat(g_system_context, message);
if (!response) {
const char* fallback = "I could not get a response from the LLM right now.";
fprintf(stdout, "[didactyl] llm response unavailable, sending fallback\n");
(void)nostr_handler_send_dm(sender_pubkey_hex, fallback);
if (agent_message_is_debounced(sender_pubkey_hex, message)) {
fprintf(stdout, "[didactyl] debounced duplicate inbound message from %.16s...\n", sender_pubkey_hex);
return;
}
fprintf(stdout, "[didactyl] llm response: %.240s%s\n",
response,
strlen(response) > 240 ? "..." : "");
(void)nostr_handler_send_dm(sender_pubkey_hex, response);
free(response);
fprintf(stdout, "[didactyl] calling llm for sender %.16s...\n", sender_pubkey_hex);
if (!g_cfg->tools.enabled) {
size_t context_len = strlen("system:\n\nuser:\n") + strlen(g_system_context) + strlen(message) + 1U;
char* plain_context = (char*)malloc(context_len);
if (plain_context) {
snprintf(plain_context, context_len, "system:\n%s\n\nuser:\n%s", g_system_context, message);
append_context_log(sender_pubkey_hex, "llm_chat", plain_context);
free(plain_context);
}
char* response = llm_chat(g_system_context, message);
if (!response) {
const char* fallback = "I could not get a response from the LLM right now.";
fprintf(stdout, "[didactyl] llm response unavailable, sending fallback\n");
(void)nostr_handler_send_dm(sender_pubkey_hex, fallback);
return;
}
fprintf(stdout, "[didactyl] llm response: %.240s%s\n",
response,
strlen(response) > 240 ? "..." : "");
(void)nostr_handler_send_dm(sender_pubkey_hex, response);
free(response);
return;
}
char* tools_json = tools_build_openai_schema_json(&g_tools_ctx);
if (!tools_json) {
(void)nostr_handler_send_dm(sender_pubkey_hex, "Tool schema generation failed.");
return;
}
cJSON* messages = cJSON_CreateArray();
if (!messages) {
free(tools_json);
(void)nostr_handler_send_dm(sender_pubkey_hex, "Failed to initialize conversation state.");
return;
}
if (append_simple_message(messages, "system", g_system_context) != 0 ||
append_startup_events_context(messages) != 0 ||
append_recent_admin_dm_history(messages, message) != 0 ||
append_simple_message(messages, "user", message) != 0) {
cJSON_Delete(messages);
free(tools_json);
(void)nostr_handler_send_dm(sender_pubkey_hex, "Failed to initialize conversation messages.");
return;
}
int max_turns = g_cfg->tools.max_turns > 0 ? g_cfg->tools.max_turns : 8;
char* final_answer_owned = NULL;
for (int turn = 0; turn < max_turns; turn++) {
char* messages_json = cJSON_PrintUnformatted(messages);
if (!messages_json) {
break;
}
append_context_log(sender_pubkey_hex, "llm_chat_with_tools_messages", messages_json);
llm_response_t resp;
int rc = llm_chat_with_tools_messages(messages_json, tools_json, "auto", &resp);
free(messages_json);
if (rc != 0) {
(void)nostr_handler_send_dm(sender_pubkey_hex, "LLM request failed.");
cJSON_Delete(messages);
free(tools_json);
return;
}
if (resp.tool_call_count <= 0) {
const char* answer = resp.content ? resp.content : "No response content.";
fprintf(stdout, "[didactyl] llm response (no tool call): %.240s%s\n",
answer,
strlen(answer) > 240 ? "..." : "");
final_answer_owned = strdup(answer);
llm_response_free(&resp);
break;
}
if (append_assistant_tool_calls_message(messages, &resp) != 0) {
llm_response_free(&resp);
break;
}
for (int i = 0; i < resp.tool_call_count; i++) {
llm_tool_call_t* tc = &resp.tool_calls[i];
fprintf(stdout, "[didactyl] executing tool call: %s\n", tc->name ? tc->name : "<null>");
char* tool_result = tools_execute(&g_tools_ctx, tc->name, tc->arguments_json);
if (!tool_result) {
tool_result = strdup("{\"success\":false,\"error\":\"tool execution failed\"}");
}
if (append_tool_result_message(messages,
tc->id ? tc->id : "",
tool_result ? tool_result : "{\"success\":false,\"error\":\"tool execution failed\"}") != 0) {
free(tool_result);
llm_response_free(&resp);
cJSON_Delete(messages);
free(tools_json);
(void)nostr_handler_send_dm(sender_pubkey_hex, "Failed to append tool result.");
return;
}
free(tool_result);
}
llm_response_free(&resp);
}
if (!final_answer_owned) {
final_answer_owned = strdup("I hit my tool-use limit for this request.");
}
const char* final_answer = final_answer_owned ? final_answer_owned : "I hit my tool-use limit for this request.";
fprintf(stdout, "[didactyl] final response: %.240s%s\n",
final_answer,
strlen(final_answer) > 240 ? "..." : "");
(void)nostr_handler_send_dm(sender_pubkey_hex, final_answer);
free(final_answer_owned);
cJSON_Delete(messages);
free(tools_json);
}
void agent_cleanup(void) {
tools_cleanup(&g_tools_ctx);
free(g_system_context);
g_system_context = NULL;
g_cfg = NULL;
memset(g_seen_msgs, 0, sizeof(g_seen_msgs));
g_seen_msgs_count = 0;
g_seen_msgs_next = 0;
}
+238
View File
@@ -117,6 +117,222 @@ static int decode_pubkey_hex_or_npub(const char* in, char out_hex[65]) {
return -1;
}
static int parse_tools_config(cJSON* root, didactyl_config_t* config) {
cJSON* tools = cJSON_GetObjectItemCaseSensitive(root, "tools");
if (!tools || !cJSON_IsObject(tools)) {
return 0;
}
cJSON* enabled = cJSON_GetObjectItemCaseSensitive(tools, "enabled");
cJSON* max_turns = cJSON_GetObjectItemCaseSensitive(tools, "max_turns");
if (enabled && cJSON_IsBool(enabled)) {
config->tools.enabled = cJSON_IsTrue(enabled) ? 1 : 0;
}
if (max_turns && cJSON_IsNumber(max_turns)) {
config->tools.max_turns = (int)max_turns->valuedouble;
}
cJSON* shell = cJSON_GetObjectItemCaseSensitive(tools, "shell");
if (!shell || !cJSON_IsObject(shell)) {
return 0;
}
cJSON* shell_enabled = cJSON_GetObjectItemCaseSensitive(shell, "enabled");
cJSON* timeout_seconds = cJSON_GetObjectItemCaseSensitive(shell, "timeout_seconds");
cJSON* max_output_bytes = cJSON_GetObjectItemCaseSensitive(shell, "max_output_bytes");
if (shell_enabled && cJSON_IsBool(shell_enabled)) {
config->tools.shell.enabled = cJSON_IsTrue(shell_enabled) ? 1 : 0;
}
if (timeout_seconds && cJSON_IsNumber(timeout_seconds)) {
config->tools.shell.timeout_seconds = (int)timeout_seconds->valuedouble;
}
if (max_output_bytes && cJSON_IsNumber(max_output_bytes)) {
config->tools.shell.max_output_bytes = (int)max_output_bytes->valuedouble;
}
if (copy_json_string(shell,
"working_directory",
config->tools.shell.working_directory,
sizeof(config->tools.shell.working_directory),
0) != 0) {
return -1;
}
return 0;
}
static cJSON* find_tag_value_string(cJSON* tags, const char* tag_key) {
if (!tags || !cJSON_IsArray(tags) || !tag_key) {
return NULL;
}
int n = cJSON_GetArraySize(tags);
for (int i = 0; i < n; i++) {
cJSON* tag = cJSON_GetArrayItem(tags, i);
if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) {
continue;
}
cJSON* key = cJSON_GetArrayItem(tag, 0);
cJSON* val = cJSON_GetArrayItem(tag, 1);
if (!key || !val || !cJSON_IsString(key) || !cJSON_IsString(val) || !key->valuestring || !val->valuestring) {
continue;
}
if (strcmp(key->valuestring, tag_key) == 0) {
return val;
}
}
return NULL;
}
static int set_tag_value_string(cJSON* tags, const char* tag_key, const char* tag_value) {
if (!tags || !cJSON_IsArray(tags) || !tag_key || !tag_value || tag_value[0] == '\0') {
return -1;
}
int n = cJSON_GetArraySize(tags);
for (int i = 0; i < n; i++) {
cJSON* tag = cJSON_GetArrayItem(tags, i);
if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) {
continue;
}
cJSON* key = cJSON_GetArrayItem(tag, 0);
cJSON* val = cJSON_GetArrayItem(tag, 1);
if (!key || !val || !cJSON_IsString(key) || !key->valuestring) {
continue;
}
if (strcmp(key->valuestring, tag_key) == 0) {
if (cJSON_IsString(val)) {
if (!cJSON_SetValuestring(val, tag_value)) {
return -1;
}
return 0;
}
cJSON* new_val = cJSON_CreateString(tag_value);
if (!new_val) {
return -1;
}
cJSON_ReplaceItemInArray(tag, 1, new_val);
return 0;
}
}
cJSON* new_tag = cJSON_CreateArray();
if (!new_tag) {
return -1;
}
cJSON_AddItemToArray(new_tag, cJSON_CreateString(tag_key));
cJSON_AddItemToArray(new_tag, cJSON_CreateString(tag_value));
cJSON_AddItemToArray(tags, new_tag);
return 0;
}
static int normalize_skill_d_tag(int event_kind, cJSON* item, cJSON* tags) {
if (!item || !tags || !cJSON_IsArray(tags)) {
return 0;
}
if (event_kind != 31123 && event_kind != 31124) {
return 0;
}
cJSON* d_val = find_tag_value_string(tags, "d");
if (!d_val || !cJSON_IsString(d_val) || !d_val->valuestring) {
return 0;
}
int needs_normalize =
(strcmp(d_val->valuestring, "skill") == 0 || strcmp(d_val->valuestring, "private_skill") == 0);
if (!needs_normalize) {
return 0;
}
const char* slug = NULL;
cJSON* slug_val = find_tag_value_string(tags, "slug");
if (slug_val && cJSON_IsString(slug_val) && slug_val->valuestring && slug_val->valuestring[0] != '\0') {
slug = slug_val->valuestring;
}
if (!slug) {
cJSON* content_fields = cJSON_GetObjectItemCaseSensitive(item, "content_fields");
if (content_fields && cJSON_IsObject(content_fields)) {
cJSON* name = cJSON_GetObjectItemCaseSensitive(content_fields, "name");
if (name && cJSON_IsString(name) && name->valuestring && name->valuestring[0] != '\0') {
slug = name->valuestring;
}
}
}
if (!slug) {
return 0;
}
return set_tag_value_string(tags, "d", slug);
}
static int parse_startup_events(cJSON* root, didactyl_config_t* config) {
cJSON* arr = cJSON_GetObjectItemCaseSensitive(root, "startup_events");
if (!arr || !cJSON_IsArray(arr)) {
return 0;
}
int count = cJSON_GetArraySize(arr);
if (count <= 0) return 0;
config->startup_events = (startup_event_t*)calloc((size_t)count, sizeof(startup_event_t));
if (!config->startup_events) return -1;
config->startup_event_count = count;
for (int i = 0; i < count; i++) {
cJSON* item = cJSON_GetArrayItem(arr, i);
if (!item || !cJSON_IsObject(item)) return -1;
cJSON* kind = cJSON_GetObjectItemCaseSensitive(item, "kind");
cJSON* content = cJSON_GetObjectItemCaseSensitive(item, "content");
cJSON* content_fields = cJSON_GetObjectItemCaseSensitive(item, "content_fields");
cJSON* tags = cJSON_GetObjectItemCaseSensitive(item, "tags");
if (!kind || !cJSON_IsNumber(kind)) {
return -1;
}
config->startup_events[i].kind = (int)kind->valuedouble;
if (content_fields) {
if (!cJSON_IsObject(content_fields)) {
return -1;
}
config->startup_events[i].content = cJSON_PrintUnformatted(content_fields);
if (!config->startup_events[i].content) {
return -1;
}
} else {
if (!content || !cJSON_IsString(content) || !content->valuestring) {
return -1;
}
config->startup_events[i].content = strdup(content->valuestring);
if (!config->startup_events[i].content) {
return -1;
}
}
if (tags) {
if (!cJSON_IsArray(tags)) return -1;
if (normalize_skill_d_tag(config->startup_events[i].kind, item, tags) != 0) {
return -1;
}
config->startup_events[i].tags_json = cJSON_PrintUnformatted(tags);
if (!config->startup_events[i].tags_json) return -1;
}
}
return 0;
}
static int parse_relays(cJSON* root, didactyl_config_t* config) {
cJSON* relays = cJSON_GetObjectItemCaseSensitive(root, "relays");
if (!relays || !cJSON_IsArray(relays)) {
@@ -162,6 +378,14 @@ void config_free(didactyl_config_t* config) {
free(config->relays);
}
if (config->startup_events) {
for (int i = 0; i < config->startup_event_count; i++) {
free(config->startup_events[i].content);
free(config->startup_events[i].tags_json);
}
free(config->startup_events);
}
memset(config, 0, sizeof(*config));
}
@@ -171,6 +395,12 @@ int config_load(const char* path, didactyl_config_t* config) {
}
memset(config, 0, sizeof(*config));
config->tools.enabled = 1;
config->tools.max_turns = 8;
config->tools.shell.enabled = 1;
config->tools.shell.timeout_seconds = 30;
config->tools.shell.max_output_bytes = 65536;
strcpy(config->tools.shell.working_directory, ".");
char* json_buf = NULL;
size_t json_len = 0;
@@ -254,6 +484,14 @@ int config_load(const char* path, didactyl_config_t* config) {
config->llm.max_tokens = (max_tokens && cJSON_IsNumber(max_tokens)) ? (int)max_tokens->valuedouble : 512;
config->llm.temperature = (temperature && cJSON_IsNumber(temperature)) ? temperature->valuedouble : 0.7;
if (parse_tools_config(root, config) != 0) {
goto cleanup;
}
if (parse_startup_events(root, config) != 0) {
goto cleanup;
}
if (decode_private_key(config->keys.nsec, config->keys.private_key) != 0) {
goto cleanup;
}
+22
View File
@@ -37,6 +37,25 @@ typedef struct {
double temperature;
} llm_config_t;
typedef struct {
int enabled;
int timeout_seconds;
int max_output_bytes;
char working_directory[OW_MAX_URL_LEN];
} shell_tools_config_t;
typedef struct {
int enabled;
int max_turns;
shell_tools_config_t shell;
} tools_config_t;
typedef struct {
int kind;
char* content;
char* tags_json; // JSON array string for tags, optional
} startup_event_t;
typedef struct {
agent_profile_t profile;
agent_keys_t keys;
@@ -44,6 +63,9 @@ typedef struct {
char** relays;
int relay_count;
llm_config_t llm;
tools_config_t tools;
startup_event_t* startup_events;
int startup_event_count;
} didactyl_config_t;
int config_load(const char* path, didactyl_config_t* config);
+45
View File
@@ -0,0 +1,45 @@
#include "debug.h"
#include <stdarg.h>
#include <string.h>
debug_level_t g_debug_level = DEBUG_LEVEL_NONE;
void debug_init(int level) {
if (level < 0) level = 0;
if (level > 5) level = 5;
g_debug_level = (debug_level_t)level;
}
void debug_log(debug_level_t level, const char* file, int line, const char* format, ...) {
time_t now = time(NULL);
struct tm* tm_info = localtime(&now);
char timestamp[32];
strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", tm_info);
const char* level_str = "UNKNOWN";
switch (level) {
case DEBUG_LEVEL_ERROR: level_str = "ERROR"; break;
case DEBUG_LEVEL_WARN: level_str = "WARN "; break;
case DEBUG_LEVEL_INFO: level_str = "INFO "; break;
case DEBUG_LEVEL_DEBUG: level_str = "DEBUG"; break;
case DEBUG_LEVEL_TRACE: level_str = "TRACE"; break;
default: break;
}
printf("[%s] [%s] ", timestamp, level_str);
if (file && g_debug_level >= DEBUG_LEVEL_TRACE) {
const char* filename = strrchr(file, '/');
filename = filename ? filename + 1 : file;
printf("[%s:%d] ", filename, line);
}
va_list args;
va_start(args, format);
vprintf(format, args);
va_end(args);
printf("\n");
fflush(stdout);
}
+36
View File
@@ -0,0 +1,36 @@
#ifndef DIDACTYL_DEBUG_H
#define DIDACTYL_DEBUG_H
#include <stdio.h>
#include <time.h>
typedef enum {
DEBUG_LEVEL_NONE = 0,
DEBUG_LEVEL_ERROR = 1,
DEBUG_LEVEL_WARN = 2,
DEBUG_LEVEL_INFO = 3,
DEBUG_LEVEL_DEBUG = 4,
DEBUG_LEVEL_TRACE = 5
} debug_level_t;
extern debug_level_t g_debug_level;
void debug_init(int level);
void debug_log(debug_level_t level, const char* file, int line, const char* format, ...);
#define DEBUG_ERROR(...) \
do { if (g_debug_level >= DEBUG_LEVEL_ERROR) debug_log(DEBUG_LEVEL_ERROR, __FILE__, __LINE__, __VA_ARGS__); } while(0)
#define DEBUG_WARN(...) \
do { if (g_debug_level >= DEBUG_LEVEL_WARN) debug_log(DEBUG_LEVEL_WARN, __FILE__, __LINE__, __VA_ARGS__); } while(0)
#define DEBUG_INFO(...) \
do { if (g_debug_level >= DEBUG_LEVEL_INFO) debug_log(DEBUG_LEVEL_INFO, __FILE__, __LINE__, __VA_ARGS__); } while(0)
#define DEBUG_LOG(...) \
do { if (g_debug_level >= DEBUG_LEVEL_DEBUG) debug_log(DEBUG_LEVEL_DEBUG, __FILE__, __LINE__, __VA_ARGS__); } while(0)
#define DEBUG_TRACE(...) \
do { if (g_debug_level >= DEBUG_LEVEL_TRACE) debug_log(DEBUG_LEVEL_TRACE, __FILE__, __LINE__, __VA_ARGS__); } while(0)
#endif
+275 -53
View File
@@ -6,6 +6,7 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "cjson/cJSON.h"
@@ -41,6 +42,96 @@ static size_t write_cb(void* contents, size_t size, size_t nmemb, void* userp) {
return total;
}
static const char* detect_ca_bundle_path(void) {
const char* env = getenv("SSL_CERT_FILE");
if (env && env[0] != '\0' && access(env, R_OK) == 0) {
return env;
}
static const char* candidates[] = {
"/etc/ssl/certs/ca-certificates.crt", // Debian/Ubuntu
"/etc/ssl/cert.pem", // Alpine
"/etc/pki/tls/certs/ca-bundle.crt", // RHEL/CentOS/Fedora
"/etc/ssl/ca-bundle.pem" // openSUSE
};
for (size_t i = 0; i < sizeof(candidates) / sizeof(candidates[0]); i++) {
if (access(candidates[i], R_OK) == 0) {
return candidates[i];
}
}
return NULL;
}
static char* perform_chat_request(const char* body) {
CURL* curl = curl_easy_init();
if (!curl || !body) {
if (curl) curl_easy_cleanup(curl);
return NULL;
}
char url[OW_MAX_URL_LEN + 64];
snprintf(url, sizeof(url), "%s/chat/completions", g_cfg.base_url);
response_buffer_t rb = {0};
struct curl_slist* headers = NULL;
headers = curl_slist_append(headers, "Content-Type: application/json");
char auth_header[OW_MAX_KEY_LEN + 32];
snprintf(auth_header, sizeof(auth_header), "Authorization: Bearer %s", g_cfg.api_key);
headers = curl_slist_append(headers, auth_header);
curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_POST, 1L);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 60L);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &rb);
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
const char* ca_bundle = detect_ca_bundle_path();
if (ca_bundle) {
curl_easy_setopt(curl, CURLOPT_CAINFO, ca_bundle);
}
CURLcode res = curl_easy_perform(curl);
long status = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status);
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
if (res != CURLE_OK) {
fprintf(stderr, "[didactyl] llm http request failed: curl=%s\n", curl_easy_strerror(res));
if (rb.data && rb.len > 0) {
fprintf(stderr, "[didactyl] llm partial response: %.600s%s\n",
rb.data,
rb.len > 600 ? "..." : "");
}
free(rb.data);
return NULL;
}
if (status < 200 || status >= 300) {
fprintf(stderr, "[didactyl] llm http request failed: status=%ld\n", status);
if (rb.data && rb.len > 0) {
fprintf(stderr, "[didactyl] llm error response: %.1200s%s\n",
rb.data,
rb.len > 1200 ? "..." : "");
}
free(rb.data);
return NULL;
}
if (!rb.data) {
fprintf(stderr, "[didactyl] llm http request failed: empty response body\n");
return NULL;
}
return rb.data;
}
static char* build_request_json(const char* system_prompt, const char* user_message) {
cJSON* root = cJSON_CreateObject();
cJSON* messages = cJSON_CreateArray();
@@ -76,29 +167,90 @@ static char* build_request_json(const char* system_prompt, const char* user_mess
return body;
}
static char* parse_response_content(const char* json) {
cJSON* root = cJSON_Parse(json);
if (!root) {
return NULL;
static int parse_tool_calls(cJSON* msg, llm_response_t* out) {
cJSON* tc = cJSON_GetObjectItemCaseSensitive(msg, "tool_calls");
if (!tc || !cJSON_IsArray(tc)) {
out->tool_calls = NULL;
out->tool_call_count = 0;
return 0;
}
int n = cJSON_GetArraySize(tc);
if (n <= 0) {
out->tool_calls = NULL;
out->tool_call_count = 0;
return 0;
}
llm_tool_call_t* calls = (llm_tool_call_t*)calloc((size_t)n, sizeof(llm_tool_call_t));
if (!calls) return -1;
int actual = 0;
for (int i = 0; i < n; i++) {
cJSON* item = cJSON_GetArrayItem(tc, i);
cJSON* id = item ? cJSON_GetObjectItemCaseSensitive(item, "id") : NULL;
cJSON* fn = item ? cJSON_GetObjectItemCaseSensitive(item, "function") : NULL;
cJSON* name = fn ? cJSON_GetObjectItemCaseSensitive(fn, "name") : NULL;
cJSON* args = fn ? cJSON_GetObjectItemCaseSensitive(fn, "arguments") : NULL;
if (!id || !cJSON_IsString(id) || !id->valuestring ||
!name || !cJSON_IsString(name) || !name->valuestring) {
continue;
}
calls[actual].id = strdup(id->valuestring);
calls[actual].name = strdup(name->valuestring);
calls[actual].arguments_json = strdup((args && cJSON_IsString(args) && args->valuestring) ? args->valuestring : "{}");
if (!calls[actual].id || !calls[actual].name || !calls[actual].arguments_json) {
free(calls[actual].id);
free(calls[actual].name);
free(calls[actual].arguments_json);
continue;
}
actual++;
}
if (actual == 0) {
free(calls);
out->tool_calls = NULL;
out->tool_call_count = 0;
return 0;
}
out->tool_calls = calls;
out->tool_call_count = actual;
return 0;
}
static int parse_llm_response(const char* json, llm_response_t* out) {
memset(out, 0, sizeof(*out));
cJSON* root = cJSON_Parse(json);
if (!root) return -1;
cJSON* choices = cJSON_GetObjectItemCaseSensitive(root, "choices");
if (!choices || !cJSON_IsArray(choices) || cJSON_GetArraySize(choices) == 0) {
cJSON* first = (choices && cJSON_IsArray(choices) && cJSON_GetArraySize(choices) > 0)
? cJSON_GetArrayItem(choices, 0)
: NULL;
cJSON* msg = first ? cJSON_GetObjectItemCaseSensitive(first, "message") : NULL;
if (!msg || !cJSON_IsObject(msg)) {
cJSON_Delete(root);
return NULL;
return -1;
}
cJSON* first = cJSON_GetArrayItem(choices, 0);
cJSON* msg = cJSON_GetObjectItemCaseSensitive(first, "message");
cJSON* content = msg ? cJSON_GetObjectItemCaseSensitive(msg, "content") : NULL;
if (!content || !cJSON_IsString(content) || !content->valuestring) {
cJSON_Delete(root);
return NULL;
cJSON* content = cJSON_GetObjectItemCaseSensitive(msg, "content");
if (content && cJSON_IsString(content) && content->valuestring) {
out->content = strdup(content->valuestring);
}
if (parse_tool_calls(msg, out) != 0) {
cJSON_Delete(root);
llm_response_free(out);
return -1;
}
char* out = strdup(content->valuestring);
cJSON_Delete(root);
return out;
return 0;
}
int llm_init(const llm_config_t* config) {
@@ -118,54 +270,124 @@ char* llm_chat(const char* system_prompt, const char* user_message) {
}
char* body = build_request_json(system_prompt, user_message);
if (!body) {
return NULL;
}
if (!body) return NULL;
CURL* curl = curl_easy_init();
if (!curl) {
free(body);
return NULL;
}
char url[OW_MAX_URL_LEN + 64];
snprintf(url, sizeof(url), "%s/chat/completions", g_cfg.base_url);
response_buffer_t rb = {0};
struct curl_slist* headers = NULL;
headers = curl_slist_append(headers, "Content-Type: application/json");
char auth_header[OW_MAX_KEY_LEN + 32];
snprintf(auth_header, sizeof(auth_header), "Authorization: Bearer %s", g_cfg.api_key);
headers = curl_slist_append(headers, auth_header);
curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_POST, 1L);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 60L);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &rb);
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
CURLcode res = curl_easy_perform(curl);
long status = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status);
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
char* raw = perform_chat_request(body);
free(body);
if (!raw) return NULL;
if (res != CURLE_OK || status < 200 || status >= 300 || !rb.data) {
free(rb.data);
llm_response_t parsed;
if (parse_llm_response(raw, &parsed) != 0) {
fprintf(stderr, "[didactyl] failed to parse llm response (non-tool path): %.1200s%s\n",
raw,
strlen(raw) > 1200 ? "..." : "");
free(raw);
return NULL;
}
free(raw);
char* answer = parse_response_content(rb.data);
free(rb.data);
char* answer = parsed.content ? strdup(parsed.content) : NULL;
llm_response_free(&parsed);
return answer;
}
int llm_chat_with_tools_messages(const char* messages_json,
const char* tools_json,
const char* tool_choice,
llm_response_t* out_response) {
if (!g_initialized || !out_response || !messages_json) {
return -1;
}
cJSON* root = cJSON_CreateObject();
if (!root) {
return -1;
}
cJSON_AddStringToObject(root, "model", g_cfg.model);
cJSON_AddNumberToObject(root, "max_tokens", g_cfg.max_tokens);
cJSON_AddNumberToObject(root, "temperature", g_cfg.temperature);
cJSON* messages = cJSON_Parse(messages_json);
if (!messages || !cJSON_IsArray(messages)) {
cJSON_Delete(messages);
cJSON_Delete(root);
return -1;
}
cJSON_AddItemToObject(root, "messages", messages);
if (tools_json) {
cJSON* tools = cJSON_Parse(tools_json);
if (tools && cJSON_IsArray(tools)) {
cJSON_AddItemToObject(root, "tools", tools);
cJSON_AddStringToObject(root, "tool_choice", tool_choice ? tool_choice : "auto");
} else {
cJSON_Delete(tools);
}
}
char* body = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
if (!body) return -1;
char* raw = perform_chat_request(body);
free(body);
if (!raw) return -1;
int rc = parse_llm_response(raw, out_response);
if (rc != 0) {
fprintf(stderr, "[didactyl] failed to parse llm response (tools path): %.1200s%s\n",
raw,
strlen(raw) > 1200 ? "..." : "");
}
free(raw);
return rc;
}
int llm_chat_with_tools(const char* system_prompt,
const char* user_message,
const char* tools_json,
llm_response_t* out_response) {
cJSON* messages = cJSON_CreateArray();
cJSON* system_msg = cJSON_CreateObject();
cJSON* user_msg = cJSON_CreateObject();
if (!messages || !system_msg || !user_msg) {
cJSON_Delete(messages);
cJSON_Delete(system_msg);
cJSON_Delete(user_msg);
return -1;
}
cJSON_AddStringToObject(system_msg, "role", "system");
cJSON_AddStringToObject(system_msg, "content", system_prompt ? system_prompt : "");
cJSON_AddStringToObject(user_msg, "role", "user");
cJSON_AddStringToObject(user_msg, "content", user_message ? user_message : "");
cJSON_AddItemToArray(messages, system_msg);
cJSON_AddItemToArray(messages, user_msg);
char* messages_json = cJSON_PrintUnformatted(messages);
cJSON_Delete(messages);
if (!messages_json) {
return -1;
}
int rc = llm_chat_with_tools_messages(messages_json, tools_json, "auto", out_response);
free(messages_json);
return rc;
}
void llm_response_free(llm_response_t* response) {
if (!response) return;
free(response->content);
for (int i = 0; i < response->tool_call_count; i++) {
free(response->tool_calls[i].id);
free(response->tool_calls[i].name);
free(response->tool_calls[i].arguments_json);
}
free(response->tool_calls);
memset(response, 0, sizeof(*response));
}
void llm_cleanup(void) {
if (!g_initialized) {
return;
+21
View File
@@ -3,8 +3,29 @@
#include "config.h"
typedef struct {
char* id;
char* name;
char* arguments_json;
} llm_tool_call_t;
typedef struct {
char* content;
llm_tool_call_t* tool_calls;
int tool_call_count;
} llm_response_t;
int llm_init(const llm_config_t* config);
char* llm_chat(const char* system_prompt, const char* user_message);
int llm_chat_with_tools(const char* system_prompt,
const char* user_message,
const char* tools_json,
llm_response_t* out_response);
int llm_chat_with_tools_messages(const char* messages_json,
const char* tools_json,
const char* tool_choice,
llm_response_t* out_response);
void llm_response_free(llm_response_t* response);
void llm_cleanup(void);
#endif
+26 -24
View File
@@ -6,11 +6,12 @@
#include <string.h>
#include "../../nostr_core_lib/nostr_core/nostr_core.h"
#include "main.h"
#include "agent.h"
#include "config.h"
#include "context.h"
#include "llm.h"
#include "nostr_handler.h"
#include "debug.h"
static volatile sig_atomic_t g_running = 1;
@@ -21,19 +22,23 @@ static void signal_handler(int signum) {
int main(int argc, char** argv) {
const char* config_path = "./config.json";
const char* context_path = "./SYSTEM.md";
int debug_level = DEBUG_LEVEL_TRACE;
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "--config") == 0 && i + 1 < argc) {
config_path = argv[++i];
} else if (strcmp(argv[i], "--context") == 0 && i + 1 < argc) {
context_path = argv[++i];
} else if (strcmp(argv[i], "--debug") == 0 && i + 1 < argc) {
debug_level = atoi(argv[++i]);
} else {
fprintf(stderr, "Usage: %s [--config <path>] [--context <path>]\n", argv[0]);
fprintf(stderr, "Usage: %s [--config <path>] [--debug <0-5>]\n", argv[0]);
return 1;
}
}
debug_init(debug_level);
DEBUG_INFO("%s %s starting", DIDACTYL_NAME, DIDACTYL_VERSION);
if (nostr_init() != NOSTR_SUCCESS) {
fprintf(stderr, "Failed to initialize nostr core\n");
return 1;
@@ -46,17 +51,8 @@ int main(int argc, char** argv) {
return 1;
}
char* system_context = context_load(context_path);
if (!system_context) {
fprintf(stderr, "Failed to load context file: %s\n", context_path);
config_free(&cfg);
nostr_cleanup();
return 1;
}
if (llm_init(&cfg.llm) != 0) {
fprintf(stderr, "Failed to initialize llm client\n");
context_free(system_context);
config_free(&cfg);
nostr_cleanup();
return 1;
@@ -65,41 +61,48 @@ int main(int argc, char** argv) {
if (nostr_handler_init(&cfg) != 0) {
fprintf(stderr, "Failed to initialize nostr handler\n");
llm_cleanup();
context_free(system_context);
config_free(&cfg);
nostr_cleanup();
return 1;
}
if (nostr_handler_reconcile_startup_events() != 0) {
DEBUG_WARN("[didactyl] startup phase: reconcile startup events failed (continuing)");
}
const char* system_context = nostr_handler_get_system_context();
if (!system_context || system_context[0] == '\0') {
system_context = "You are Didactyl, a sovereign AI agent living on Nostr.";
}
if (agent_init(&cfg, system_context) != 0) {
fprintf(stderr, "Failed to initialize agent\n");
nostr_handler_cleanup();
llm_cleanup();
context_free(system_context);
config_free(&cfg);
nostr_cleanup();
return 1;
}
if (nostr_handler_publish_profile() != 0) {
fprintf(stderr, "Warning: failed to publish profile\n");
}
DEBUG_INFO("[didactyl] startup phase: subscribe DMs begin");
if (nostr_handler_subscribe_dms(agent_on_message, NULL) != 0) {
DEBUG_ERROR("[didactyl] startup phase: subscribe DMs failed");
fprintf(stderr, "Failed to subscribe to DMs\n");
agent_cleanup();
nostr_handler_cleanup();
llm_cleanup();
context_free(system_context);
config_free(&cfg);
nostr_cleanup();
return 1;
}
DEBUG_INFO("[didactyl] startup phase: subscribe DMs end");
signal(SIGINT, signal_handler);
signal(SIGTERM, signal_handler);
fprintf(stdout, "[didactyl] running with pubkey %s\n", cfg.keys.public_key_hex);
DEBUG_INFO("[didactyl] entering main poll loop");
DEBUG_INFO("[didactyl] running with pubkey %s", cfg.keys.public_key_hex);
while (g_running) {
(void)nostr_handler_poll(100);
@@ -107,12 +110,11 @@ int main(int argc, char** argv) {
nanosleep(&ts, NULL);
}
fprintf(stdout, "[didactyl] shutting down\n");
DEBUG_INFO("[didactyl] shutting down");
agent_cleanup();
nostr_handler_cleanup();
llm_cleanup();
context_free(system_context);
config_free(&cfg);
nostr_cleanup();
+23
View File
@@ -0,0 +1,23 @@
/*
* Didactyl Main Header - Version and Metadata Information
*
* This header contains version information and agent metadata.
* Version macros are auto-updated by the build system.
*/
#ifndef DIDACTYL_MAIN_H
#define DIDACTYL_MAIN_H
// 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 5
#define DIDACTYL_VERSION "v0.0.5"
// Agent metadata
#define DIDACTYL_NAME "Didactyl"
#define DIDACTYL_DESCRIPTION "A sovereign AI agent daemon on Nostr"
#define DIDACTYL_SOFTWARE "https://git.laantungir.net/laantungir/didactyl.git"
#endif /* DIDACTYL_MAIN_H */
+474 -22
View File
@@ -6,9 +6,11 @@
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <pthread.h>
#include "../../nostr_core_lib/cjson/cJSON.h"
#include "../../nostr_core_lib/nostr_core/nostr_core.h"
#include "debug.h"
static didactyl_config_t* g_cfg = NULL;
static nostr_relay_pool_t* g_pool = NULL;
@@ -16,6 +18,52 @@ static dm_callback_t g_dm_callback = NULL;
static void* g_dm_user_data = NULL;
static int g_poll_counter = 0;
static time_t g_start_time = 0;
static time_t g_last_status_log_time = 0;
static nostr_pool_relay_status_t* g_last_relay_statuses = NULL;
static char* g_system_context = NULL;
static unsigned char* g_startup_published = NULL;
static int g_startup_publish_tracking_enabled = 0;
#define DM_DEDUP_CACHE_SIZE 256
static char g_seen_dm_ids[DM_DEDUP_CACHE_SIZE][65];
static int g_seen_dm_count = 0;
static int g_seen_dm_next = 0;
static pthread_mutex_t g_dm_dedup_mutex = PTHREAD_MUTEX_INITIALIZER;
static int dm_id_seen_or_remember(const char* event_id_hex) {
if (!event_id_hex || strlen(event_id_hex) != 64U) {
return 0;
}
int seen = 0;
pthread_mutex_lock(&g_dm_dedup_mutex);
for (int i = 0; i < g_seen_dm_count; i++) {
if (strncmp(g_seen_dm_ids[i], event_id_hex, 64U) == 0) {
seen = 1;
break;
}
}
if (!seen) {
int slot = 0;
if (g_seen_dm_count < DM_DEDUP_CACHE_SIZE) {
slot = g_seen_dm_count;
g_seen_dm_count++;
} else {
slot = g_seen_dm_next;
g_seen_dm_next = (g_seen_dm_next + 1) % DM_DEDUP_CACHE_SIZE;
}
memcpy(g_seen_dm_ids[slot], event_id_hex, 64U);
g_seen_dm_ids[slot][64] = '\0';
}
pthread_mutex_unlock(&g_dm_dedup_mutex);
return seen;
}
static const char* relay_status_str(nostr_pool_relay_status_t status) {
switch (status) {
@@ -32,26 +80,89 @@ static const char* relay_status_str(nostr_pool_relay_status_t status) {
}
}
static void publish_pending_startup_events_for_relay_index(int relay_index, const char* reason);
static int publish_kind_event_to_relays(int kind,
const char* content,
cJSON* tags,
const char** relay_urls,
int relay_count,
const char* reason_label);
static void log_relay_statuses(const char* reason) {
if (!g_pool || !g_cfg) {
return;
}
fprintf(stdout, "[didactyl] relay status snapshot (%s)\n", reason ? reason : "periodic");
int info_level = (reason && strcmp(reason, "after init") == 0) ? 1 : 0;
if (info_level) {
DEBUG_INFO("[didactyl] relay status snapshot (%s)", reason ? reason : "periodic");
} else {
DEBUG_TRACE("[didactyl] relay status snapshot (%s)", reason ? reason : "periodic");
}
for (int i = 0; i < g_cfg->relay_count; i++) {
const char* relay = g_cfg->relays[i];
nostr_pool_relay_status_t status = nostr_relay_pool_get_relay_status(g_pool, relay);
const char* last_err = nostr_relay_pool_get_relay_last_connection_error(g_pool, relay);
double ping_ms = nostr_relay_pool_get_relay_ping_latency(g_pool, relay);
fprintf(stdout, "[didactyl] - %s => %s", relay, relay_status_str(status));
if (ping_ms > 0.0) {
fprintf(stdout, " (ping %.1f ms)", ping_ms);
if (info_level) {
if (ping_ms > 0.0) {
DEBUG_INFO("[didactyl] - %s => %s (ping %.1f ms)",
relay,
relay_status_str(status),
ping_ms);
} else {
DEBUG_INFO("[didactyl] - %s => %s", relay, relay_status_str(status));
}
} else {
if (ping_ms > 0.0) {
DEBUG_TRACE("[didactyl] - %s => %s (ping %.1f ms)",
relay,
relay_status_str(status),
ping_ms);
} else {
DEBUG_TRACE("[didactyl] - %s => %s", relay, relay_status_str(status));
}
}
if (last_err && last_err[0] != '\0') {
fprintf(stdout, " [last_error: %s]", last_err);
DEBUG_WARN("[didactyl] - %s last_connection_error: %s", relay, last_err);
}
const nostr_relay_stats_t* stats = nostr_relay_pool_get_relay_stats(g_pool, relay);
if (stats) {
DEBUG_LOG("[didactyl] - %s stats attempts=%d failures=%d recv=%d pub_ok=%d pub_fail=%d",
relay,
stats->connection_attempts,
stats->connection_failures,
stats->events_received,
stats->events_published_ok,
stats->events_published_failed);
}
}
}
static void log_relay_state_changes(void) {
if (!g_pool || !g_cfg || !g_last_relay_statuses) {
return;
}
for (int i = 0; i < g_cfg->relay_count; i++) {
const char* relay = g_cfg->relays[i];
nostr_pool_relay_status_t now = nostr_relay_pool_get_relay_status(g_pool, relay);
nostr_pool_relay_status_t prev = g_last_relay_statuses[i];
if (now != prev) {
DEBUG_INFO("[didactyl] relay state changed: %s %s -> %s",
relay,
relay_status_str(prev),
relay_status_str(now));
g_last_relay_statuses[i] = now;
if (now == NOSTR_POOL_RELAY_CONNECTED) {
publish_pending_startup_events_for_relay_index(i, "relay_connected");
}
}
fprintf(stdout, "\n");
}
}
@@ -60,9 +171,9 @@ static void log_publish_targets(const char* action) {
return;
}
fprintf(stdout, "[didactyl] %s target relays (%d):\n", action ? action : "publish", g_cfg->relay_count);
DEBUG_INFO("[didactyl] %s target relays (%d):", action ? action : "publish", g_cfg->relay_count);
for (int i = 0; i < g_cfg->relay_count; i++) {
fprintf(stdout, "[didactyl] -> %s\n", g_cfg->relays[i]);
DEBUG_INFO("[didactyl] -> %s", g_cfg->relays[i]);
}
}
@@ -118,6 +229,28 @@ static int extract_first_p_tag(cJSON* tags, char out_pubkey_hex[65]) {
return -1;
}
static void trace_event_json(const char* prefix, cJSON* event) {
if (g_debug_level < DEBUG_LEVEL_TRACE || !event) {
return;
}
char* event_json = cJSON_PrintUnformatted(event);
if (!event_json) {
DEBUG_TRACE("[didactyl] %s <failed to serialize event>", prefix ? prefix : "event");
return;
}
DEBUG_TRACE("[didactyl] %s %s", prefix ? prefix : "event", event_json);
free(event_json);
}
static void trace_plaintext_dm(const char* prefix, const char* plaintext) {
if (g_debug_level < DEBUG_LEVEL_TRACE) {
return;
}
DEBUG_TRACE("[didactyl] %s %s", prefix ? prefix : "dm plaintext", plaintext ? plaintext : "");
}
static void on_event(cJSON* event, const char* relay_url, void* user_data) {
(void)user_data;
@@ -125,6 +258,7 @@ static void on_event(cJSON* event, const char* relay_url, void* user_data) {
return;
}
cJSON* id = cJSON_GetObjectItemCaseSensitive(event, "id");
cJSON* kind = cJSON_GetObjectItemCaseSensitive(event, "kind");
cJSON* pubkey = cJSON_GetObjectItemCaseSensitive(event, "pubkey");
cJSON* content = cJSON_GetObjectItemCaseSensitive(event, "content");
@@ -135,6 +269,10 @@ static void on_event(cJSON* event, const char* relay_url, void* user_data) {
return;
}
const char* event_id_hex = (id && cJSON_IsString(id) && id->valuestring && strlen(id->valuestring) == 64U)
? id->valuestring
: NULL;
if ((int)kind->valuedouble != 4) {
return;
}
@@ -167,15 +305,30 @@ static void on_event(cJSON* event, const char* relay_url, void* user_data) {
}
decrypted[0] = '\0';
trace_event_json("received encrypted DM event:", event);
if (nostr_nip04_decrypt(g_cfg->keys.private_key, sender_pubkey, content->valuestring, decrypted, NOSTR_NIP04_MAX_PLAINTEXT_SIZE) != NOSTR_SUCCESS) {
fprintf(stdout, "[didactyl] failed to decrypt incoming DM from %.16s...\n", pubkey->valuestring);
free(decrypted);
return;
}
fprintf(stdout, "[didactyl] received DM from %.16s... via %s\n",
pubkey->valuestring,
relay_url ? relay_url : "unknown relay");
trace_plaintext_dm("received decrypted DM content:", decrypted);
if (event_id_hex && dm_id_seen_or_remember(event_id_hex)) {
DEBUG_LOG("[didactyl] skipped duplicate DM event %.16s... from %.16s... via %s",
event_id_hex,
pubkey->valuestring,
relay_url ? relay_url : "unknown relay");
free(decrypted);
return;
}
DEBUG_INFO("[didactyl] received kind %d event %.16s... from %.16s... via %s",
(int)kind->valuedouble,
event_id_hex ? event_id_hex : "<no-id>",
pubkey->valuestring,
relay_url ? relay_url : "unknown relay");
g_dm_callback(pubkey->valuestring, decrypted, g_dm_user_data);
free(decrypted);
}
@@ -194,8 +347,11 @@ int nostr_handler_init(didactyl_config_t* config) {
g_cfg = config;
g_poll_counter = 0;
g_start_time = time(NULL);
memset(g_seen_dm_ids, 0, sizeof(g_seen_dm_ids));
g_seen_dm_count = 0;
g_seen_dm_next = 0;
fprintf(stdout, "[didactyl] initializing relay pool with %d relays\n", g_cfg->relay_count);
DEBUG_INFO("[didactyl] initializing relay pool with %d relays", g_cfg->relay_count);
nostr_pool_reconnect_config_t reconnect = *nostr_pool_reconnect_config_default();
reconnect.enable_auto_reconnect = 1;
@@ -212,10 +368,25 @@ int nostr_handler_init(didactyl_config_t* config) {
fprintf(stderr, "[didactyl] failed to add relay: %s\n", g_cfg->relays[i]);
return -1;
}
fprintf(stdout, "[didactyl] added relay: %s\n", g_cfg->relays[i]);
DEBUG_INFO("[didactyl] added relay: %s", g_cfg->relays[i]);
}
free(g_last_relay_statuses);
g_last_relay_statuses = (nostr_pool_relay_status_t*)calloc((size_t)g_cfg->relay_count, sizeof(nostr_pool_relay_status_t));
if (!g_last_relay_statuses) {
return -1;
}
for (int i = 0; i < g_cfg->relay_count; i++) {
g_last_relay_statuses[i] = nostr_relay_pool_get_relay_status(g_pool, g_cfg->relays[i]);
}
g_last_status_log_time = time(NULL);
log_relay_statuses("after init");
free(g_startup_published);
g_startup_published = NULL;
g_startup_publish_tracking_enabled = 0;
return 0;
}
@@ -224,6 +395,7 @@ int nostr_handler_publish_profile(void) {
return -1;
}
DEBUG_INFO("[didactyl] publish_profile: build metadata payload");
cJSON* profile = cJSON_CreateObject();
if (!profile) {
return -1;
@@ -243,6 +415,7 @@ int nostr_handler_publish_profile(void) {
return -1;
}
DEBUG_INFO("[didactyl] publish_profile: sign kind-0 event");
cJSON* event = nostr_create_and_sign_event(0, content, NULL, g_cfg->keys.private_key, time(NULL));
free(content);
@@ -250,18 +423,38 @@ int nostr_handler_publish_profile(void) {
return -1;
}
log_publish_targets("publish profile");
const char** connected_relays = (const char**)calloc((size_t)g_cfg->relay_count, sizeof(char*));
if (!connected_relays) {
cJSON_Delete(event);
return -1;
}
int connected_count = 0;
for (int i = 0; i < g_cfg->relay_count; i++) {
if (nostr_relay_pool_get_relay_status(g_pool, g_cfg->relays[i]) == NOSTR_POOL_RELAY_CONNECTED) {
connected_relays[connected_count++] = g_cfg->relays[i];
}
}
if (connected_count == 0) {
DEBUG_WARN("[didactyl] publish_profile: no connected relays yet, deferring publish");
free(connected_relays);
cJSON_Delete(event);
return 0;
}
DEBUG_INFO("[didactyl] publish_profile: publishing to %d connected relay(s)", connected_count);
int sent = nostr_relay_pool_publish_async(
g_pool,
(const char**)g_cfg->relays,
g_cfg->relay_count,
connected_relays,
connected_count,
event,
NULL,
NULL);
free(connected_relays);
cJSON_Delete(event);
fprintf(stdout, "[didactyl] publish profile result: sent_to=%d relays\n", sent);
DEBUG_INFO("[didactyl] publish profile result: sent_to=%d connected relay(s)", sent);
return sent > 0 ? 0 : -1;
}
@@ -310,7 +503,7 @@ int nostr_handler_subscribe_dms(dm_callback_t callback, void* user_data) {
return -1;
}
fprintf(stdout, "[didactyl] DM subscription active for pubkey %.16s...\n", g_cfg->keys.public_key_hex);
DEBUG_INFO("[didactyl] DM subscription active for pubkey %.16s...", g_cfg->keys.public_key_hex);
return 0;
}
@@ -324,6 +517,8 @@ int nostr_handler_send_dm(const char* recipient_pubkey_hex, const char* message)
return -1;
}
trace_plaintext_dm("sending plaintext DM content:", message);
char* encrypted = (char*)malloc(NOSTR_NIP04_MAX_ENCRYPTED_SIZE);
if (!encrypted) {
fprintf(stderr, "[didactyl] failed to allocate DM encrypt buffer\n");
@@ -349,21 +544,261 @@ int nostr_handler_send_dm(const char* recipient_pubkey_hex, const char* message)
return -1;
}
trace_event_json("sending encrypted DM event:", event);
log_publish_targets("publish DM");
const char** connected_relays = (const char**)calloc((size_t)g_cfg->relay_count, sizeof(char*));
if (!connected_relays) {
cJSON_Delete(event);
return -1;
}
int connected_count = 0;
for (int i = 0; i < g_cfg->relay_count; i++) {
if (nostr_relay_pool_get_relay_status(g_pool, g_cfg->relays[i]) == NOSTR_POOL_RELAY_CONNECTED) {
connected_relays[connected_count++] = g_cfg->relays[i];
}
}
int sent = 0;
if (connected_count > 0) {
sent = nostr_relay_pool_publish_async(
g_pool,
connected_relays,
connected_count,
event,
NULL,
NULL);
for (int i = 0; i < connected_count; i++) {
DEBUG_INFO("[didactyl] kind 4 event published to %s (async)", connected_relays[i]);
}
} else {
DEBUG_WARN("[didactyl] kind 4 event not queued: no connected relays");
}
cJSON* event_id = cJSON_GetObjectItemCaseSensitive(event, "id");
const char* out_event_id_hex = (event_id && cJSON_IsString(event_id) && event_id->valuestring) ? event_id->valuestring : "<no-id>";
DEBUG_INFO("[didactyl] sent DM %.16s... to %.16s... via %d connected relay(s)",
out_event_id_hex,
recipient_pubkey_hex,
sent);
free(connected_relays);
cJSON_Delete(event);
return sent > 0 ? 0 : -1;
}
static int publish_kind_event_to_relays(int kind,
const char* content,
cJSON* tags,
const char** relay_urls,
int relay_count,
const char* reason_label) {
if (!g_cfg || !g_pool || !content || !relay_urls || relay_count <= 0) {
return -1;
}
cJSON* tags_copy = NULL;
if (tags) {
tags_copy = cJSON_Duplicate(tags, 1);
if (!tags_copy) {
return -1;
}
}
cJSON* event = nostr_create_and_sign_event(kind, content, tags_copy, g_cfg->keys.private_key, time(NULL));
if (tags_copy) {
cJSON_Delete(tags_copy);
}
if (!event) {
return -1;
}
int sent = nostr_relay_pool_publish_async(
g_pool,
(const char**)g_cfg->relays,
g_cfg->relay_count,
relay_urls,
relay_count,
event,
NULL,
NULL);
for (int i = 0; i < relay_count; i++) {
DEBUG_INFO("[didactyl] kind %d event published to %s (async%s%s)",
kind,
relay_urls[i],
reason_label ? ", reason=" : "",
reason_label ? reason_label : "");
}
cJSON_Delete(event);
fprintf(stdout, "[didactyl] sent DM to %.16s... via %d relay(s)\n", recipient_pubkey_hex, sent);
return sent > 0 ? 0 : -1;
}
int nostr_handler_publish_kind_event(int kind, const char* content, cJSON* tags) {
if (!g_cfg || !g_pool || !content) {
return -1;
}
log_publish_targets("publish kind event");
const char** connected_relays = (const char**)calloc((size_t)g_cfg->relay_count, sizeof(char*));
if (!connected_relays) {
return -1;
}
int connected_count = 0;
for (int i = 0; i < g_cfg->relay_count; i++) {
if (nostr_relay_pool_get_relay_status(g_pool, g_cfg->relays[i]) == NOSTR_POOL_RELAY_CONNECTED) {
connected_relays[connected_count++] = g_cfg->relays[i];
}
}
if (connected_count <= 0) {
DEBUG_WARN("[didactyl] kind %d event not queued: no connected relays", kind);
free(connected_relays);
return -1;
}
int rc = publish_kind_event_to_relays(kind, content, tags, connected_relays, connected_count, "manual_publish");
free(connected_relays);
DEBUG_INFO("[didactyl] published kind %d event via %d connected relay(s)", kind, connected_count);
return rc;
}
char* nostr_handler_query_json(cJSON* filter, int timeout_ms) {
if (!g_cfg || !g_pool || !filter) {
return NULL;
}
int event_count = 0;
cJSON** events = nostr_relay_pool_query_sync(
g_pool,
(const char**)g_cfg->relays,
g_cfg->relay_count,
filter,
&event_count,
timeout_ms);
cJSON* arr = cJSON_CreateArray();
if (!arr) {
return NULL;
}
if (events && event_count > 0) {
for (int i = 0; i < event_count; i++) {
if (!events[i]) {
continue;
}
cJSON* dup = cJSON_Duplicate(events[i], 1);
if (dup) {
cJSON_AddItemToArray(arr, dup);
}
cJSON_Delete(events[i]);
}
free(events);
}
char* out = cJSON_PrintUnformatted(arr);
cJSON_Delete(arr);
return out;
}
static void publish_pending_startup_events_for_relay_index(int relay_index, const char* reason) {
if (!g_cfg || !g_pool || !g_startup_publish_tracking_enabled || !g_startup_published) {
return;
}
if (relay_index < 0 || relay_index >= g_cfg->relay_count) {
return;
}
const char* relay_url = g_cfg->relays[relay_index];
if (nostr_relay_pool_get_relay_status(g_pool, relay_url) != NOSTR_POOL_RELAY_CONNECTED) {
return;
}
for (int i = 0; i < g_cfg->startup_event_count; i++) {
startup_event_t* se = &g_cfg->startup_events[i];
if (!se->content) {
continue;
}
size_t slot = (size_t)i * (size_t)g_cfg->relay_count + (size_t)relay_index;
if (g_startup_published[slot]) {
continue;
}
cJSON* tags = NULL;
if (se->tags_json) {
tags = cJSON_Parse(se->tags_json);
if (!tags || !cJSON_IsArray(tags)) {
cJSON_Delete(tags);
tags = NULL;
}
}
const char* one_relay[1] = { relay_url };
if (publish_kind_event_to_relays(se->kind, se->content, tags, one_relay, 1, reason) == 0) {
g_startup_published[slot] = 1;
} else {
DEBUG_WARN("[didactyl] startup event publish failed for kind=%d relay=%s", se->kind, relay_url);
}
if (se->kind == 31120 && !g_system_context) {
g_system_context = strdup(se->content);
}
cJSON_Delete(tags);
}
}
int nostr_handler_reconcile_startup_events(void) {
if (!g_cfg || !g_pool) {
return -1;
}
free(g_system_context);
g_system_context = NULL;
free(g_startup_published);
g_startup_published = NULL;
g_startup_publish_tracking_enabled = 0;
if (g_cfg->startup_event_count > 0 && g_cfg->relay_count > 0) {
size_t total = (size_t)g_cfg->startup_event_count * (size_t)g_cfg->relay_count;
g_startup_published = (unsigned char*)calloc(total, 1U);
if (!g_startup_published) {
return -1;
}
g_startup_publish_tracking_enabled = 1;
}
for (int i = 0; i < g_cfg->startup_event_count; i++) {
startup_event_t* se = &g_cfg->startup_events[i];
if (se->kind == 31120 && se->content && !g_system_context) {
g_system_context = strdup(se->content);
break;
}
}
if (!g_system_context) {
g_system_context = strdup("You are Didactyl, a sovereign AI agent living on Nostr.");
}
for (int relay_index = 0; relay_index < g_cfg->relay_count; relay_index++) {
publish_pending_startup_events_for_relay_index(relay_index, "startup_reconcile");
}
return 0;
}
const char* nostr_handler_get_system_context(void) {
return g_system_context;
}
int nostr_handler_poll(int timeout_ms) {
if (!g_pool) {
return -1;
@@ -372,8 +807,12 @@ int nostr_handler_poll(int timeout_ms) {
int rc = nostr_relay_pool_poll(g_pool, timeout_ms);
g_poll_counter++;
if ((g_poll_counter % 600) == 0) {
log_relay_state_changes();
time_t now = time(NULL);
if (g_last_status_log_time == 0 || difftime(now, g_last_status_log_time) >= 10.0) {
log_relay_statuses("periodic");
g_last_status_log_time = now;
}
return rc;
@@ -384,8 +823,21 @@ void nostr_handler_cleanup(void) {
nostr_relay_pool_destroy(g_pool);
}
free(g_last_relay_statuses);
g_last_relay_statuses = NULL;
g_last_status_log_time = 0;
free(g_startup_published);
g_startup_published = NULL;
g_startup_publish_tracking_enabled = 0;
g_pool = NULL;
g_cfg = NULL;
g_dm_callback = NULL;
g_dm_user_data = NULL;
free(g_system_context);
g_system_context = NULL;
memset(g_seen_dm_ids, 0, sizeof(g_seen_dm_ids));
g_seen_dm_count = 0;
g_seen_dm_next = 0;
}
+5
View File
@@ -2,6 +2,7 @@
#define OPEN_WING_NOSTR_HANDLER_H
#include "config.h"
#include "cjson/cJSON.h"
typedef void (*dm_callback_t)(const char* sender_pubkey_hex, const char* message, void* user_data);
@@ -9,7 +10,11 @@ int nostr_handler_init(didactyl_config_t* config);
int nostr_handler_publish_profile(void);
int nostr_handler_subscribe_dms(dm_callback_t callback, void* user_data);
int nostr_handler_send_dm(const char* recipient_pubkey_hex, const char* message);
int nostr_handler_publish_kind_event(int kind, const char* content, cJSON* tags);
char* nostr_handler_query_json(cJSON* filter, int timeout_ms);
int nostr_handler_poll(int timeout_ms);
int nostr_handler_reconcile_startup_events(void);
const char* nostr_handler_get_system_context(void);
void nostr_handler_cleanup(void);
#endif
-13
View File
@@ -1,13 +0,0 @@
#include <secp256k1.h>
#include <secp256k1_extrakeys.h>
#include <secp256k1_schnorrsig.h>
int secp256k1_schnorrsig_sign32(
const secp256k1_context* ctx,
unsigned char* sig64,
const unsigned char* msg32,
const secp256k1_keypair* keypair,
const unsigned char* aux_rand32
) {
return secp256k1_schnorrsig_sign(ctx, sig64, msg32, keypair, aux_rand32);
}
+454
View File
@@ -0,0 +1,454 @@
#define _POSIX_C_SOURCE 200809L
#include "tools.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include "cjson/cJSON.h"
#include "nostr_handler.h"
static char* json_error(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 char* json_success_with_message(const char* msg) {
cJSON* root = cJSON_CreateObject();
if (!root) return NULL;
cJSON_AddBoolToObject(root, "success", 1);
cJSON_AddStringToObject(root, "message", msg ? msg : "ok");
char* out = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
return out;
}
static int is_safe_relative_path(const char* path) {
if (!path || path[0] == '\0') return 0;
if (path[0] == '/') return 0;
if (strstr(path, "..") != NULL) return 0;
if (strchr(path, '\\') != NULL) return 0;
return 1;
}
static int build_tool_path(tools_context_t* ctx, const char* rel_path, char* out, size_t out_size) {
if (!ctx || !ctx->cfg || !rel_path || !out || out_size == 0) return -1;
if (!is_safe_relative_path(rel_path)) return -1;
const char* cwd = ctx->cfg->tools.shell.working_directory[0] != '\0'
? ctx->cfg->tools.shell.working_directory
: ".";
int n = 0;
if (strcmp(cwd, ".") == 0) {
n = snprintf(out, out_size, "%s", rel_path);
} else {
n = snprintf(out, out_size, "%s/%s", cwd, rel_path);
}
if (n < 0 || (size_t)n >= out_size) return -1;
return 0;
}
int tools_init(tools_context_t* ctx, didactyl_config_t* cfg) {
if (!ctx || !cfg) return -1;
memset(ctx, 0, sizeof(*ctx));
ctx->cfg = cfg;
return 0;
}
void tools_cleanup(tools_context_t* ctx) {
if (!ctx) return;
memset(ctx, 0, sizeof(*ctx));
}
char* tools_build_openai_schema_json(const tools_context_t* ctx) {
(void)ctx;
cJSON* tools = cJSON_CreateArray();
if (!tools) return NULL;
cJSON* t1 = cJSON_CreateObject();
cJSON* t1_fn = cJSON_CreateObject();
cJSON* t1_params = cJSON_CreateObject();
cJSON* t1_props = cJSON_CreateObject();
cJSON* t1_required = cJSON_CreateArray();
cJSON_AddStringToObject(t1, "type", "function");
cJSON_AddStringToObject(t1_fn, "name", "nostr_post");
cJSON_AddStringToObject(t1_fn, "description", "Publish a Nostr event to connected relays");
cJSON_AddStringToObject(t1_params, "type", "object");
cJSON_AddItemToObject(t1_params, "properties", t1_props);
cJSON_AddItemToObject(t1_params, "required", t1_required);
cJSON* p_kind = cJSON_CreateObject();
cJSON_AddStringToObject(p_kind, "type", "integer");
cJSON_AddItemToObject(t1_props, "kind", p_kind);
cJSON* p_content = cJSON_CreateObject();
cJSON_AddStringToObject(p_content, "type", "string");
cJSON_AddItemToObject(t1_props, "content", p_content);
cJSON_AddItemToArray(t1_required, cJSON_CreateString("kind"));
cJSON_AddItemToArray(t1_required, cJSON_CreateString("content"));
cJSON_AddItemToObject(t1_fn, "parameters", t1_params);
cJSON_AddItemToObject(t1, "function", t1_fn);
cJSON_AddItemToArray(tools, t1);
cJSON* t2 = cJSON_CreateObject();
cJSON* t2_fn = cJSON_CreateObject();
cJSON* t2_params = cJSON_CreateObject();
cJSON* t2_props = cJSON_CreateObject();
cJSON* t2_required = cJSON_CreateArray();
cJSON_AddStringToObject(t2, "type", "function");
cJSON_AddStringToObject(t2_fn, "name", "nostr_query");
cJSON_AddStringToObject(t2_fn, "description", "Query events from relays using a Nostr filter");
cJSON_AddStringToObject(t2_params, "type", "object");
cJSON_AddItemToObject(t2_params, "properties", t2_props);
cJSON_AddItemToObject(t2_params, "required", t2_required);
cJSON* p_filter = cJSON_CreateObject();
cJSON_AddStringToObject(p_filter, "type", "object");
cJSON_AddItemToObject(t2_props, "filter", p_filter);
cJSON* p_timeout = cJSON_CreateObject();
cJSON_AddStringToObject(p_timeout, "type", "integer");
cJSON_AddItemToObject(t2_props, "timeout_ms", p_timeout);
cJSON_AddItemToArray(t2_required, cJSON_CreateString("filter"));
cJSON_AddItemToObject(t2_fn, "parameters", t2_params);
cJSON_AddItemToObject(t2, "function", t2_fn);
cJSON_AddItemToArray(tools, t2);
cJSON* t3 = cJSON_CreateObject();
cJSON* t3_fn = cJSON_CreateObject();
cJSON* t3_params = cJSON_CreateObject();
cJSON* t3_props = cJSON_CreateObject();
cJSON* t3_required = cJSON_CreateArray();
cJSON_AddStringToObject(t3, "type", "function");
cJSON_AddStringToObject(t3_fn, "name", "shell_exec");
cJSON_AddStringToObject(t3_fn, "description", "Execute a shell command and return stdout/stderr");
cJSON_AddStringToObject(t3_params, "type", "object");
cJSON_AddItemToObject(t3_params, "properties", t3_props);
cJSON_AddItemToObject(t3_params, "required", t3_required);
cJSON* p_cmd = cJSON_CreateObject();
cJSON_AddStringToObject(p_cmd, "type", "string");
cJSON_AddItemToObject(t3_props, "command", p_cmd);
cJSON_AddItemToArray(t3_required, cJSON_CreateString("command"));
cJSON_AddItemToObject(t3_fn, "parameters", t3_params);
cJSON_AddItemToObject(t3, "function", t3_fn);
cJSON_AddItemToArray(tools, t3);
cJSON* t4 = cJSON_CreateObject();
cJSON* t4_fn = cJSON_CreateObject();
cJSON* t4_params = cJSON_CreateObject();
cJSON* t4_props = cJSON_CreateObject();
cJSON* t4_required = cJSON_CreateArray();
cJSON_AddStringToObject(t4, "type", "function");
cJSON_AddStringToObject(t4_fn, "name", "file_read");
cJSON_AddStringToObject(t4_fn, "description", "Read a local file as text from the configured working directory");
cJSON_AddStringToObject(t4_params, "type", "object");
cJSON_AddItemToObject(t4_params, "properties", t4_props);
cJSON_AddItemToObject(t4_params, "required", t4_required);
cJSON* p_fr_path = cJSON_CreateObject();
cJSON_AddStringToObject(p_fr_path, "type", "string");
cJSON_AddItemToObject(t4_props, "path", p_fr_path);
cJSON* p_fr_max = cJSON_CreateObject();
cJSON_AddStringToObject(p_fr_max, "type", "integer");
cJSON_AddItemToObject(t4_props, "max_bytes", p_fr_max);
cJSON_AddItemToArray(t4_required, cJSON_CreateString("path"));
cJSON_AddItemToObject(t4_fn, "parameters", t4_params);
cJSON_AddItemToObject(t4, "function", t4_fn);
cJSON_AddItemToArray(tools, t4);
cJSON* t5 = cJSON_CreateObject();
cJSON* t5_fn = cJSON_CreateObject();
cJSON* t5_params = cJSON_CreateObject();
cJSON* t5_props = cJSON_CreateObject();
cJSON* t5_required = cJSON_CreateArray();
cJSON_AddStringToObject(t5, "type", "function");
cJSON_AddStringToObject(t5_fn, "name", "file_write");
cJSON_AddStringToObject(t5_fn, "description", "Write text content to a local file in the configured working directory");
cJSON_AddStringToObject(t5_params, "type", "object");
cJSON_AddItemToObject(t5_params, "properties", t5_props);
cJSON_AddItemToObject(t5_params, "required", t5_required);
cJSON* p_fw_path = cJSON_CreateObject();
cJSON_AddStringToObject(p_fw_path, "type", "string");
cJSON_AddItemToObject(t5_props, "path", p_fw_path);
cJSON* p_fw_content = cJSON_CreateObject();
cJSON_AddStringToObject(p_fw_content, "type", "string");
cJSON_AddItemToObject(t5_props, "content", p_fw_content);
cJSON* p_fw_append = cJSON_CreateObject();
cJSON_AddStringToObject(p_fw_append, "type", "boolean");
cJSON_AddItemToObject(t5_props, "append", p_fw_append);
cJSON_AddItemToArray(t5_required, cJSON_CreateString("path"));
cJSON_AddItemToArray(t5_required, cJSON_CreateString("content"));
cJSON_AddItemToObject(t5_fn, "parameters", t5_params);
cJSON_AddItemToObject(t5, "function", t5_fn);
cJSON_AddItemToArray(tools, t5);
char* out = cJSON_PrintUnformatted(tools);
cJSON_Delete(tools);
return out;
}
static char* execute_nostr_post(const char* args_json) {
cJSON* args = cJSON_Parse(args_json ? args_json : "{}");
if (!args) return json_error("invalid arguments JSON");
cJSON* kind = cJSON_GetObjectItemCaseSensitive(args, "kind");
cJSON* content = cJSON_GetObjectItemCaseSensitive(args, "content");
if (!kind || !cJSON_IsNumber(kind) || !content || !cJSON_IsString(content) || !content->valuestring) {
cJSON_Delete(args);
return json_error("nostr_post requires integer kind and string content");
}
int rc = nostr_handler_publish_kind_event((int)kind->valuedouble, content->valuestring, NULL);
cJSON_Delete(args);
if (rc != 0) return json_error("nostr_post failed");
return json_success_with_message("nostr_post published");
}
static char* execute_nostr_query(const char* args_json) {
cJSON* args = cJSON_Parse(args_json ? args_json : "{}");
if (!args) return json_error("invalid arguments JSON");
cJSON* filter = cJSON_GetObjectItemCaseSensitive(args, "filter");
cJSON* timeout = cJSON_GetObjectItemCaseSensitive(args, "timeout_ms");
if (!filter || !cJSON_IsObject(filter)) {
cJSON_Delete(args);
return json_error("nostr_query requires object filter");
}
cJSON* filter_dup = cJSON_Duplicate(filter, 1);
if (!filter_dup) {
cJSON_Delete(args);
return json_error("failed to duplicate filter");
}
int timeout_ms = (timeout && cJSON_IsNumber(timeout)) ? (int)timeout->valuedouble : 8000;
char* events_json = nostr_handler_query_json(filter_dup, timeout_ms);
cJSON_Delete(filter_dup);
cJSON_Delete(args);
if (!events_json) return json_error("nostr_query failed");
cJSON* out = cJSON_CreateObject();
if (!out) {
free(events_json);
return NULL;
}
cJSON* events = cJSON_Parse(events_json);
free(events_json);
if (!events) {
cJSON_Delete(out);
return json_error("nostr_query returned invalid JSON");
}
cJSON_AddBoolToObject(out, "success", 1);
cJSON_AddItemToObject(out, "events", events);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
return json;
}
static char* execute_shell_exec(tools_context_t* ctx, const char* args_json) {
if (!ctx || !ctx->cfg) return json_error("tool context unavailable");
if (!ctx->cfg->tools.shell.enabled) return json_error("shell tool disabled");
cJSON* args = cJSON_Parse(args_json ? args_json : "{}");
if (!args) return json_error("invalid arguments JSON");
cJSON* command = cJSON_GetObjectItemCaseSensitive(args, "command");
if (!command || !cJSON_IsString(command) || !command->valuestring) {
cJSON_Delete(args);
return json_error("shell_exec requires string command");
}
const char* cwd = ctx->cfg->tools.shell.working_directory[0] != '\0'
? ctx->cfg->tools.shell.working_directory
: ".";
int timeout_s = ctx->cfg->tools.shell.timeout_seconds > 0 ? ctx->cfg->tools.shell.timeout_seconds : 30;
char cmd[4096];
snprintf(cmd,
sizeof(cmd),
"cd %s && timeout %ds sh -lc %s 2>&1",
cwd,
timeout_s,
command->valuestring);
FILE* fp = popen(cmd, "r");
cJSON_Delete(args);
if (!fp) return json_error("failed to execute command");
int max_bytes = ctx->cfg->tools.shell.max_output_bytes > 0 ? ctx->cfg->tools.shell.max_output_bytes : 65536;
char* output = (char*)calloc((size_t)max_bytes + 1U, 1U);
if (!output) {
pclose(fp);
return json_error("allocation failure");
}
size_t used = 0;
while (!feof(fp) && used < (size_t)max_bytes) {
size_t n = fread(output + used, 1, (size_t)max_bytes - used, fp);
used += n;
if (n == 0) break;
}
int status = pclose(fp);
cJSON* out = cJSON_CreateObject();
if (!out) {
free(output);
return NULL;
}
cJSON_AddBoolToObject(out, "success", status == 0 ? 1 : 0);
cJSON_AddNumberToObject(out, "exit_status", status);
cJSON_AddStringToObject(out, "output", output);
free(output);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
return json;
}
static char* execute_file_read(tools_context_t* ctx, const char* args_json) {
if (!ctx || !ctx->cfg) return json_error("tool context unavailable");
cJSON* args = cJSON_Parse(args_json ? args_json : "{}");
if (!args) return json_error("invalid arguments JSON");
cJSON* path = cJSON_GetObjectItemCaseSensitive(args, "path");
cJSON* maxb = cJSON_GetObjectItemCaseSensitive(args, "max_bytes");
if (!path || !cJSON_IsString(path) || !path->valuestring) {
cJSON_Delete(args);
return json_error("file_read requires string path");
}
int hard_max = ctx->cfg->tools.shell.max_output_bytes > 0 ? ctx->cfg->tools.shell.max_output_bytes : 65536;
int max_bytes = (maxb && cJSON_IsNumber(maxb)) ? (int)maxb->valuedouble : hard_max;
if (max_bytes <= 0 || max_bytes > hard_max) max_bytes = hard_max;
char file_path[PATH_MAX];
if (build_tool_path(ctx, path->valuestring, file_path, sizeof(file_path)) != 0) {
cJSON_Delete(args);
return json_error("file_read path is not allowed");
}
FILE* fp = fopen(file_path, "rb");
cJSON_Delete(args);
if (!fp) return json_error("file_read failed to open file");
char* buf = (char*)calloc((size_t)max_bytes + 1U, 1U);
if (!buf) {
fclose(fp);
return json_error("allocation failure");
}
size_t n = fread(buf, 1, (size_t)max_bytes, fp);
int truncated = !feof(fp) ? 1 : 0;
fclose(fp);
buf[n] = '\0';
cJSON* out = cJSON_CreateObject();
if (!out) {
free(buf);
return NULL;
}
cJSON_AddBoolToObject(out, "success", 1);
cJSON_AddStringToObject(out, "path", file_path);
cJSON_AddNumberToObject(out, "bytes_read", (double)n);
cJSON_AddBoolToObject(out, "truncated", truncated);
cJSON_AddStringToObject(out, "content", buf);
free(buf);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
return json;
}
static char* execute_file_write(tools_context_t* ctx, const char* args_json) {
if (!ctx || !ctx->cfg) return json_error("tool context unavailable");
cJSON* args = cJSON_Parse(args_json ? args_json : "{}");
if (!args) return json_error("invalid arguments JSON");
cJSON* path = cJSON_GetObjectItemCaseSensitive(args, "path");
cJSON* content = cJSON_GetObjectItemCaseSensitive(args, "content");
cJSON* append = cJSON_GetObjectItemCaseSensitive(args, "append");
if (!path || !cJSON_IsString(path) || !path->valuestring ||
!content || !cJSON_IsString(content) || !content->valuestring) {
cJSON_Delete(args);
return json_error("file_write requires string path and content");
}
char file_path[PATH_MAX];
if (build_tool_path(ctx, path->valuestring, file_path, sizeof(file_path)) != 0) {
cJSON_Delete(args);
return json_error("file_write path is not allowed");
}
const char* content_str = content->valuestring;
size_t len = strlen(content_str);
int do_append = (append && cJSON_IsBool(append) && cJSON_IsTrue(append)) ? 1 : 0;
FILE* fp = fopen(file_path, do_append ? "ab" : "wb");
cJSON_Delete(args);
if (!fp) return json_error("file_write failed to open file");
size_t n = fwrite(content_str, 1, len, fp);
fclose(fp);
if (n != len) return json_error("file_write failed to write all bytes");
cJSON* out = cJSON_CreateObject();
if (!out) return NULL;
cJSON_AddBoolToObject(out, "success", 1);
cJSON_AddStringToObject(out, "path", file_path);
cJSON_AddNumberToObject(out, "bytes_written", (double)n);
cJSON_AddBoolToObject(out, "append", do_append);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
return json;
}
char* tools_execute(tools_context_t* ctx, const char* tool_name, const char* args_json) {
if (!tool_name) return json_error("missing tool name");
if (strcmp(tool_name, "nostr_post") == 0) {
return execute_nostr_post(args_json);
}
if (strcmp(tool_name, "nostr_query") == 0) {
return execute_nostr_query(args_json);
}
if (strcmp(tool_name, "shell_exec") == 0) {
return execute_shell_exec(ctx, args_json);
}
if (strcmp(tool_name, "file_read") == 0) {
return execute_file_read(ctx, args_json);
}
if (strcmp(tool_name, "file_write") == 0) {
return execute_file_write(ctx, args_json);
}
return json_error("unknown tool");
}
+15
View File
@@ -0,0 +1,15 @@
#ifndef DIDACTYL_TOOLS_H
#define DIDACTYL_TOOLS_H
#include "config.h"
typedef struct {
didactyl_config_t* cfg;
} tools_context_t;
int tools_init(tools_context_t* ctx, didactyl_config_t* cfg);
void tools_cleanup(tools_context_t* ctx);
char* tools_build_openai_schema_json(const tools_context_t* ctx);
char* tools_execute(tools_context_t* ctx, const char* tool_name, const char* args_json);
#endif