v0.0.1 - Added static MUSL build pipeline, Dockerfile.alpine-musl, versioned main.h integration, and release tooling updates
This commit is contained in:
+16
@@ -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
|
||||
*~
|
||||
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
Executable
+206
@@ -0,0 +1,206 @@
|
||||
#!/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)"
|
||||
BUILD_DIR="$SCRIPT_DIR/build"
|
||||
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 "Build directory: $BUILD_DIR"
|
||||
echo "Debug build: $DEBUG_BUILD"
|
||||
echo ""
|
||||
|
||||
# Create build directory
|
||||
mkdir -p "$BUILD_DIR"
|
||||
|
||||
# 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 "=========================================="
|
||||
|
||||
# Build the builder stage to extract the binary
|
||||
$DOCKER_CMD build \
|
||||
--platform "$PLATFORM" \
|
||||
--build-arg DEBUG_BUILD=$DEBUG_BUILD \
|
||||
--target builder \
|
||||
-f "$DOCKERFILE" \
|
||||
-t didactyl-static-builder-stage:latest \
|
||||
. > /dev/null 2>&1
|
||||
|
||||
# Create a temporary container to copy the binary
|
||||
CONTAINER_ID=$($DOCKER_CMD create didactyl-static-builder-stage:latest)
|
||||
|
||||
# Copy binary from container
|
||||
$DOCKER_CMD cp "$CONTAINER_ID:/build/didactyl_static" "$BUILD_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: $BUILD_DIR/$OUTPUT_NAME"
|
||||
echo ""
|
||||
|
||||
# Make binary executable
|
||||
chmod +x "$BUILD_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 "$BUILD_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 "$BUILD_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: $BUILD_DIR/$OUTPUT_NAME"
|
||||
echo "Size: $(du -h "$BUILD_DIR/$OUTPUT_NAME" | cut -f1)"
|
||||
echo "Static: $TRULY_STATIC"
|
||||
echo "Debug: $DEBUG_BUILD"
|
||||
echo "Platform: $PLATFORM"
|
||||
echo "=========================================="
|
||||
Executable
+503
@@ -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
|
||||
+83
-10
@@ -8,9 +8,11 @@
|
||||
|
||||
#include "llm.h"
|
||||
#include "nostr_handler.h"
|
||||
#include "tools.h"
|
||||
|
||||
static didactyl_config_t* g_cfg = NULL;
|
||||
static char* g_system_context = NULL;
|
||||
static tools_context_t g_tools_ctx;
|
||||
|
||||
int agent_init(didactyl_config_t* config, const char* system_context) {
|
||||
if (!config || !system_context) {
|
||||
@@ -23,6 +25,13 @@ 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;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -36,22 +45,86 @@ 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 (!g_cfg->tools.enabled) {
|
||||
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;
|
||||
}
|
||||
|
||||
fprintf(stdout, "[didactyl] llm response: %.240s%s\n",
|
||||
response,
|
||||
strlen(response) > 240 ? "..." : "");
|
||||
(void)nostr_handler_send_dm(sender_pubkey_hex, response);
|
||||
free(response);
|
||||
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;
|
||||
}
|
||||
|
||||
llm_response_t resp;
|
||||
if (llm_chat_with_tools(g_system_context, message, tools_json, &resp) != 0) {
|
||||
free(tools_json);
|
||||
(void)nostr_handler_send_dm(sender_pubkey_hex, "LLM request failed.");
|
||||
return;
|
||||
}
|
||||
free(tools_json);
|
||||
|
||||
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 ? "..." : "");
|
||||
(void)nostr_handler_send_dm(sender_pubkey_hex, answer);
|
||||
llm_response_free(&resp);
|
||||
return;
|
||||
}
|
||||
|
||||
int max_turns = g_cfg->tools.max_turns > 0 ? g_cfg->tools.max_turns : 8;
|
||||
llm_response_t current = resp;
|
||||
|
||||
for (int turn = 0; turn < max_turns; turn++) {
|
||||
if (current.tool_call_count <= 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
llm_tool_call_t* tc = ¤t.tool_calls[0];
|
||||
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\"}");
|
||||
}
|
||||
|
||||
llm_response_free(¤t);
|
||||
if (llm_chat_with_tools(g_system_context, tool_result, NULL, ¤t) != 0) {
|
||||
free(tool_result);
|
||||
(void)nostr_handler_send_dm(sender_pubkey_hex, "LLM failed after tool execution.");
|
||||
return;
|
||||
}
|
||||
free(tool_result);
|
||||
|
||||
if (current.content && current.tool_call_count == 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const char* final_answer = current.content ? current.content : "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);
|
||||
llm_response_free(¤t);
|
||||
}
|
||||
|
||||
void agent_cleanup(void) {
|
||||
tools_cleanup(&g_tools_ctx);
|
||||
free(g_system_context);
|
||||
g_system_context = NULL;
|
||||
g_cfg = NULL;
|
||||
|
||||
@@ -117,6 +117,50 @@ 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 int parse_relays(cJSON* root, didactyl_config_t* config) {
|
||||
cJSON* relays = cJSON_GetObjectItemCaseSensitive(root, "relays");
|
||||
if (!relays || !cJSON_IsArray(relays)) {
|
||||
@@ -171,6 +215,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 +304,10 @@ 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 (decode_private_key(config->keys.nsec, config->keys.private_key) != 0) {
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
@@ -37,6 +37,19 @@ 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 {
|
||||
agent_profile_t profile;
|
||||
agent_keys_t keys;
|
||||
@@ -44,6 +57,7 @@ typedef struct {
|
||||
char** relays;
|
||||
int relay_count;
|
||||
llm_config_t llm;
|
||||
tools_config_t tools;
|
||||
} didactyl_config_t;
|
||||
|
||||
int config_load(const char* path, didactyl_config_t* config);
|
||||
|
||||
+45
@@ -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
@@ -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
|
||||
@@ -41,6 +41,47 @@ static size_t write_cb(void* contents, size_t size, size_t nmemb, void* userp) {
|
||||
return total;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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 || status < 200 || status >= 300 || !rb.data) {
|
||||
free(rb.data);
|
||||
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 +117,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 +220,88 @@ 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) {
|
||||
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(const char* system_prompt,
|
||||
const char* user_message,
|
||||
const char* tools_json,
|
||||
llm_response_t* out_response) {
|
||||
if (!g_initialized || !out_response) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON* root = cJSON_CreateObject();
|
||||
cJSON* messages = cJSON_CreateArray();
|
||||
if (!root || !messages) {
|
||||
cJSON_Delete(root);
|
||||
cJSON_Delete(messages);
|
||||
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* system_msg = cJSON_CreateObject();
|
||||
cJSON* user_msg = cJSON_CreateObject();
|
||||
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);
|
||||
cJSON_AddItemToObject(root, "messages", messages);
|
||||
|
||||
if (tools_json) {
|
||||
cJSON* tools = cJSON_Parse(tools_json);
|
||||
if (tools && cJSON_IsArray(tools)) {
|
||||
cJSON_AddItemToObject(root, "tools", tools);
|
||||
} 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);
|
||||
free(raw);
|
||||
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;
|
||||
|
||||
@@ -3,8 +3,25 @@
|
||||
|
||||
#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);
|
||||
void llm_response_free(llm_response_t* response);
|
||||
void llm_cleanup(void);
|
||||
|
||||
#endif
|
||||
+21
-4
@@ -6,11 +6,13 @@
|
||||
#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;
|
||||
|
||||
@@ -23,17 +25,25 @@ int main(int argc, char** argv) {
|
||||
const char* config_path = "./config.json";
|
||||
const char* context_path = "./SYSTEM.md";
|
||||
|
||||
int debug_level = DEBUG_LEVEL_INFO;
|
||||
|
||||
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>] [--context <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;
|
||||
@@ -81,11 +91,15 @@ int main(int argc, char** argv) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
DEBUG_INFO("[didactyl] startup phase: publish profile begin");
|
||||
if (nostr_handler_publish_profile() != 0) {
|
||||
fprintf(stderr, "Warning: failed to publish profile\n");
|
||||
DEBUG_WARN("[didactyl] publish profile deferred/failed (will continue startup)");
|
||||
}
|
||||
DEBUG_INFO("[didactyl] startup phase: publish profile end");
|
||||
|
||||
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();
|
||||
@@ -96,10 +110,13 @@ int main(int argc, char** argv) {
|
||||
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,7 +124,7 @@ 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();
|
||||
|
||||
+23
@@ -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 1
|
||||
#define DIDACTYL_VERSION "v0.0.1"
|
||||
|
||||
// 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 */
|
||||
+210
-25
@@ -9,6 +9,7 @@
|
||||
|
||||
#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 +17,8 @@ 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 const char* relay_status_str(nostr_pool_relay_status_t status) {
|
||||
switch (status) {
|
||||
@@ -37,21 +40,55 @@ static void log_relay_statuses(const char* reason) {
|
||||
return;
|
||||
}
|
||||
|
||||
fprintf(stdout, "[didactyl] relay status snapshot (%s)\n", reason ? reason : "periodic");
|
||||
DEBUG_INFO("[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);
|
||||
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));
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
fprintf(stdout, "\n");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,9 +97,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]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,9 +210,9 @@ static void on_event(cJSON* event, const char* relay_url, void* user_data) {
|
||||
return;
|
||||
}
|
||||
|
||||
fprintf(stdout, "[didactyl] received DM from %.16s... via %s\n",
|
||||
pubkey->valuestring,
|
||||
relay_url ? relay_url : "unknown relay");
|
||||
DEBUG_INFO("[didactyl] received DM from %.16s... via %s",
|
||||
pubkey->valuestring,
|
||||
relay_url ? relay_url : "unknown relay");
|
||||
g_dm_callback(pubkey->valuestring, decrypted, g_dm_user_data);
|
||||
free(decrypted);
|
||||
}
|
||||
@@ -195,7 +232,7 @@ int nostr_handler_init(didactyl_config_t* config) {
|
||||
g_poll_counter = 0;
|
||||
g_start_time = time(NULL);
|
||||
|
||||
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,9 +249,19 @@ 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");
|
||||
return 0;
|
||||
}
|
||||
@@ -224,6 +271,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 +291,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 +299,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 +379,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;
|
||||
}
|
||||
|
||||
@@ -351,17 +420,125 @@ int nostr_handler_send_dm(const char* recipient_pubkey_hex, const char* message)
|
||||
|
||||
log_publish_targets("publish DM");
|
||||
|
||||
int sent = nostr_relay_pool_publish_async(
|
||||
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);
|
||||
}
|
||||
|
||||
free(connected_relays);
|
||||
cJSON_Delete(event);
|
||||
DEBUG_INFO("[didactyl] sent DM to %.16s... via %d connected relay(s)", 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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
log_publish_targets("publish kind event");
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
free(connected_relays);
|
||||
cJSON_Delete(event);
|
||||
DEBUG_INFO("[didactyl] published kind %d event via %d connected relay(s)", kind, sent);
|
||||
return sent > 0 ? 0 : -1;
|
||||
}
|
||||
|
||||
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,
|
||||
event,
|
||||
NULL,
|
||||
NULL);
|
||||
filter,
|
||||
&event_count,
|
||||
timeout_ms);
|
||||
|
||||
cJSON_Delete(event);
|
||||
fprintf(stdout, "[didactyl] sent DM to %.16s... via %d relay(s)\n", recipient_pubkey_hex, sent);
|
||||
return sent > 0 ? 0 : -1;
|
||||
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;
|
||||
}
|
||||
|
||||
int nostr_handler_poll(int timeout_ms) {
|
||||
@@ -372,8 +549,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,6 +565,10 @@ 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;
|
||||
|
||||
g_pool = NULL;
|
||||
g_cfg = NULL;
|
||||
g_dm_callback = NULL;
|
||||
|
||||
@@ -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,6 +10,8 @@ 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);
|
||||
void nostr_handler_cleanup(void);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
+266
@@ -0,0 +1,266 @@
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
|
||||
#include "tools.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.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;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
return json_error("unknown tool");
|
||||
}
|
||||
+15
@@ -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
|
||||
Reference in New Issue
Block a user