Compare commits

...
1 Commits
5 changed files with 352 additions and 93 deletions
+2 -2
View File
@@ -53,11 +53,11 @@ Skills support context modes (`inject`, `full`, `override`) and per-skill LLM fa
Didactyl will support local inference, which is very privacy preserving. Remote inference does however have it's advantages, and in those cases Didactyl supports using Bitcoin Lightning and eCash inference providers.
## Current Status — v0.0.61
## Current Status — v0.0.62
**Active build — this project is barely working. Experiment at your own risk.**
> Last release update: v0.0.61 — feat: add webhook cron and chain trigger types
> Last release update: v0.0.62 — fix: webhook trigger lookup uses trigger manager and add startup PoC triggers/tests
- Connects to configured relays with auto-reconnect and relay state transition logging
- Publishes configured startup events per relay as each relay becomes connected
+240
View File
@@ -1,3 +1,243 @@
```text
Context Log - not seen by model
timestamp=2026-03-10 05:44:14
phase=direct_tool_exec
sender=8ff74724ed641b3c28e5a86d7c5cbc49c37638ace8c6c38935860e7a5eedde0e
model=claude-opus-4.6
context_bytes=7997
approx_tokens=1999
```
slash=/help
result_json=AVAILABLE TOOLS
- admin_identity — Build admin identity context block from cached runtime metadata
- agent_identity — Build agent identity context block with pubkey and npub
- agent_version — Return current Didactyl version and metadata from build macros
- local_file_read — Read a local file as text from the configured working directory
- local_file_write — Write text content to a local file in the configured working directory
- local_http_fetch — Fetch HTTP(S) resources with optional method, headers, timeout, and body
- local_shell_exec — Execute a shell command and return stdout/stderr
- model_get — Get current active LLM runtime configuration (excluding API key)
- model_list — List available model IDs using provider OpenAI-compatible /models endpoint
- model_set — Update active LLM configuration and persist it to config.jsonc
- my_npub — Alias for nostr_npub: return this agent's pubkey encoded as npub bech32
- my_pubkey — Alias for nostr_pubkey: return this agent's pubkey in hex format
- nostr_admin_contacts — Build admin contacts context block from cached kind 3 contact list
- nostr_admin_notes — Build admin notes context block from cached kind 1 notes
- nostr_admin_profile — Build admin profile context block from cached kind 0 metadata
- nostr_admin_relays — Build admin relay context block from cached kind 10002 data
- nostr_decode — Decode a Nostr bech32/nostr: URI into components
- nostr_decrypt — Decrypt NIP-44 ciphertext from a sender
- nostr_delete — Request deletion of one or more previously published events (NIP-09 kind 5)
- nostr_dm_send — Send a NIP-04 encrypted DM
- nostr_dm_send_nip17 — Send a private DM using NIP-17 gift wrap protocol
- nostr_encode — Encode a Nostr entity into nostr: URI (npub, note, nprofile, nevent, naddr)
- nostr_encrypt — Encrypt plaintext using NIP-44 for a recipient
- nostr_file_md_to_longform_post — Read a markdown file and publish it as kind 30023 longform post; defaults d-tag to lowercase filename
- nostr_list_manage — Add/remove tag tuples in replaceable list events (NIP-51 style)
- nostr_nip05_lookup — Look up or verify a NIP-05 identifier (user@domain)
- nostr_npub — Return this agent's pubkey encoded as npub bech32
- nostr_post — Publish a Nostr event to connected relays
- nostr_post_readme — Publish README.md as kind 30023 with deterministic d tag readme.md
- nostr_profile_get — Look up a Nostr profile (kind 0 metadata) by pubkey
- nostr_pubkey — Return this agent's pubkey in hex format
- nostr_query — Query events from relays using a Nostr filter
- nostr_react — React to a Nostr event with like/dislike/emoji (NIP-25 kind 7)
- nostr_relay_info — Fetch NIP-11 relay information document
- nostr_relay_status — Get connection status and statistics for all relays
- skill_adopt — Adopt a skill by adding its address to kind 10123 adoption list
- skill_create — Create or update a skill definition as kind 31123/31124 and optionally auto-adopt it
- skill_edit — Edit an existing self skill by d tag and republish it as kind 31123/31124
- skill_list — List this agent's published skills, optionally filtered by scope
- skill_remove — Remove a skill address from kind 10123 adoption list
- skill_search — Search public skills by query/author and optionally rank by adoption popularity
- task_list — Build current task list context block from tasks.json
- task_manage — Manage agent short-term task memory stored in tasks.json (list/add/update/remove/clear/replace)
- tool_list — List available tools with name, description, and JSON parameter schema
- trigger_list — List active triggered skills and their runtime status
AVAILABLE SKILLS
- long_form_note
- post_readme_to_nostr
- admin_ops
- webhook_source_poc — PoC webhook trigger skill
- cron_poc — PoC cron trigger skill
- chain_from_webhook_poc — PoC chain trigger skill
- cheerleader — Cheer on admin whenever they post a kind 1 note
result_markdown=AVAILABLE TOOLS
- admin_identity — Build admin identity context block from cached runtime metadata
- agent_identity — Build agent identity context block with pubkey and npub
- agent_version — Return current Didactyl version and metadata from build macros
- local_file_read — Read a local file as text from the configured working directory
- local_file_write — Write text content to a local file in the configured working directory
- local_http_fetch — Fetch HTTP(S) resources with optional method, headers, timeout, and body
- local_shell_exec — Execute a shell command and return stdout/stderr
- model_get — Get current active LLM runtime configuration (excluding API key)
- model_list — List available model IDs using provider OpenAI-compatible /models endpoint
- model_set — Update active LLM configuration and persist it to config.jsonc
- my_npub — Alias for nostr_npub: return this agent's pubkey encoded as npub bech32
- my_pubkey — Alias for nostr_pubkey: return this agent's pubkey in hex format
- nostr_admin_contacts — Build admin contacts context block from cached kind 3 contact list
- nostr_admin_notes — Build admin notes context block from cached kind 1 notes
- nostr_admin_profile — Build admin profile context block from cached kind 0 metadata
- nostr_admin_relays — Build admin relay context block from cached kind 10002 data
- nostr_decode — Decode a Nostr bech32/nostr: URI into components
- nostr_decrypt — Decrypt NIP-44 ciphertext from a sender
- nostr_delete — Request deletion of one or more previously published events (NIP-09 kind 5)
- nostr_dm_send — Send a NIP-04 encrypted DM
- nostr_dm_send_nip17 — Send a private DM using NIP-17 gift wrap protocol
- nostr_encode — Encode a Nostr entity into nostr: URI (npub, note, nprofile, nevent, naddr)
- nostr_encrypt — Encrypt plaintext using NIP-44 for a recipient
- nostr_file_md_to_longform_post — Read a markdown file and publish it as kind 30023 longform post; defaults d-tag to lowercase filename
- nostr_list_manage — Add/remove tag tuples in replaceable list events (NIP-51 style)
- nostr_nip05_lookup — Look up or verify a NIP-05 identifier (user@domain)
- nostr_npub — Return this agent's pubkey encoded as npub bech32
- nostr_post — Publish a Nostr event to connected relays
- nostr_post_readme — Publish README.md as kind 30023 with deterministic d tag readme.md
- nostr_profile_get — Look up a Nostr profile (kind 0 metadata) by pubkey
- nostr_pubkey — Return this agent's pubkey in hex format
- nostr_query — Query events from relays using a Nostr filter
- nostr_react — React to a Nostr event with like/dislike/emoji (NIP-25 kind 7)
- nostr_relay_info — Fetch NIP-11 relay information document
- nostr_relay_status — Get connection status and statistics for all relays
- skill_adopt — Adopt a skill by adding its address to kind 10123 adoption list
- skill_create — Create or update a skill definition as kind 31123/31124 and optionally auto-adopt it
- skill_edit — Edit an existing self skill by d tag and republish it as kind 31123/31124
- skill_list — List this agent's published skills, optionally filtered by scope
- skill_remove — Remove a skill address from kind 10123 adoption list
- skill_search — Search public skills by query/author and optionally rank by adoption popularity
- task_list — Build current task list context block from tasks.json
- task_manage — Manage agent short-term task memory stored in tasks.json (list/add/update/remove/clear/replace)
- tool_list — List available tools with name, description, and JSON parameter schema
- trigger_list — List active triggered skills and their runtime status
AVAILABLE SKILLS
- long_form_note
- post_readme_to_nostr
- admin_ops
- webhook_source_poc — PoC webhook trigger skill
- cron_poc — PoC cron trigger skill
- chain_from_webhook_poc — PoC chain trigger skill
- cheerleader — Cheer on admin whenever they post a kind 1 note
---
```text
Context Log - not seen by model
timestamp=2026-03-10 05:34:22
phase=llm_chat_with_tools_messages
sender=8ff74724ed641b3c28e5a86d7c5cbc49c37638ace8c6c38935860e7a5eedde0e
model=claude-opus-4.6
context_bytes=4008
approx_tokens=1002
```
Sections: 10
# system_prompt | role=system
## Didactyl Agent
You are Didactyl, a sovereign AI agent living on Nostr.
### Communication Rules
- You communicate through encrypted Nostr direct messages.
- Keep responses concise and clear.
### Behavior
- Be helpful and technically accurate.
- If unsure, state uncertainty directly.
- Prefer actionable, practical advice.
- Use the person's name when messaging them if you know it.
- For the administrator, use their name from the administrator kind 0 profile metadata when available.
### 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 `local_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.
### Task Management
- Maintain and use your internal task list as short-term working memory.
- Break long or complex actions into clear tasks before executing them.
- Update task status as you complete steps so your plan stays accurate.
### Safety
- Do not claim to have executed actions you did not execute.
- You may share your public key (npub) with anyone.
- Never reveal your private key (nsec) under any circumstance.
# admin_identity | role=system
### Administrator Identity (source: config.admin.pubkey)
This is your administrator! Admin pubkey (hex): 8ff74724ed641b3c28e5a86d7c5cbc49c37638ace8c6c38935860e7a5eedde0e
This message has been cryptographically verified as coming from your administrator.
# admin_profile | role=system
### Administrator Kind 0 Profile (source: nostr kind 0)
Administrator kind 0 profile content (JSON): {"name":"William S Burroughs","display_name":"WSB","about":"I like to write, and I like crank. They are good. Sometimes I'm not so sure.","banner":"https://www.grunge.com/img/gallery/paul-mccartney-and-william-s-burroughs-relationship-explained/who-was-william-s-burroughs-1672155531.jpg","website":"https://addicted.com","picture":"https://hips.hearstapps.com/hmg-prod/images/william-s-burroughs.jpg?resize=1200:*","lud16":"","nip05":""}
# admin_contacts | role=system
### Administrator Kind 3 Contacts (source: nostr kind 3)
Administrator contacts (JSON): ["fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52","460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c","82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2","4c800257a588a82849d049817c2bdaad984b25a45ad9f6dad66e47d3b47e3b2f","8ff74724ed641b3c28e5a86d7c5cbc49c37638ace8c6c38935860e7a5eedde0e","52a3e82f7b3743852fbe804cfcbf4db3448115887895247c001f2b50e790acb8"]
# admin_relays | role=system
### Administrator Kind 10002 Relays (source: nostr kind 10002)
Administrator relay list (JSON): ["wss://relay.laantungir.net/","wss://relay.damus.io/","wss://nos.lol/","wss://relay.nostr.band/","wss://purplepag.es/","ws://127.0.0.1:7777/","wss://nostr.mom/","wss://relay.0xchat.com"]
# admin_notes | role=system
### Administrator Recent Kind 1 Notes
Administrator recent public notes:
- Forever we live. We never die.
- I love my agent.
- What time is it world?
- The sky is so blue.
- Today is a great day.
- hello
- What a great day to be alive.
- Good day everyone.
- test
- Now is a good time to go on a diet.
# tasks | role=system
### Current Task List
#### Current Tasks
Your active task list - short-term working memory for tracking plan steps.
- [ ] No active tasks yet.
# dm_history | role=chat
05:33 Agent Didactyl has started up and is online at 2026-03-10 05:33:58 (version v0.0.61, connected relays: 5/5).
05:34 WSB Good morning.
# conversation | role=user
Good morning.
---
```text
Context Log - not seen by model
timestamp=2026-03-09 20:25:36
+6 -89
View File
@@ -105,81 +105,6 @@ static int uri_extract_after_prefix(const struct mg_str* uri,
return 0;
}
static cJSON* find_skill_event_by_d_tag(const char* d_tag) {
if (!d_tag || d_tag[0] == '\0') {
return NULL;
}
char* events_json = nostr_handler_get_self_skill_events_json();
if (!events_json) {
return NULL;
}
cJSON* events = cJSON_Parse(events_json);
free(events_json);
if (!events || !cJSON_IsArray(events)) {
cJSON_Delete(events);
return NULL;
}
cJSON* found = NULL;
int n = cJSON_GetArraySize(events);
for (int i = 0; i < n; i++) {
cJSON* ev = cJSON_GetArrayItem(events, i);
cJSON* tags = ev ? cJSON_GetObjectItemCaseSensitive(ev, "tags") : NULL;
if (!tags || !cJSON_IsArray(tags)) {
continue;
}
int tn = cJSON_GetArraySize(tags);
for (int ti = 0; ti < tn; ti++) {
cJSON* tag = cJSON_GetArrayItem(tags, ti);
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, "d") == 0 && strcmp(val->valuestring, d_tag) == 0) {
found = cJSON_Duplicate(ev, 1);
break;
}
}
if (found) {
break;
}
}
cJSON_Delete(events);
return found;
}
static const char* find_tag_string_local(cJSON* tags, const char* key) {
if (!tags || !cJSON_IsArray(tags) || !key || key[0] == '\0') {
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* k = cJSON_GetArrayItem(tag, 0);
cJSON* v = cJSON_GetArrayItem(tag, 1);
if (!k || !v || !cJSON_IsString(k) || !cJSON_IsString(v) || !k->valuestring || !v->valuestring) {
continue;
}
if (strcmp(k->valuestring, key) == 0) {
return v->valuestring;
}
}
return NULL;
}
static void handle_trigger_webhook(struct mg_connection* c,
const struct mg_http_message* hm,
@@ -189,23 +114,18 @@ static void handle_trigger_webhook(struct mg_connection* c,
return;
}
cJSON* skill_ev = find_skill_event_by_d_tag(d_tag);
if (!skill_ev) {
reply_error(c, 404, "skill not found by d_tag");
active_trigger_t trigger;
if (trigger_manager_find(g_api_ctx.trigger_manager, d_tag, &trigger) != 0) {
reply_error(c, 404, "active trigger not found for d_tag");
return;
}
cJSON* tags = cJSON_GetObjectItemCaseSensitive(skill_ev, "tags");
const char* trigger_type = find_tag_string_local(tags, "trigger");
if (!trigger_type || strcmp(trigger_type, "webhook") != 0) {
cJSON_Delete(skill_ev);
reply_error(c, 400, "skill trigger type is not webhook");
if (trigger.trigger_type != TRIGGER_TYPE_WEBHOOK) {
reply_error(c, 400, "trigger type is not webhook");
return;
}
const char* enabled = find_tag_string_local(tags, "enabled");
if (enabled && (strcmp(enabled, "false") == 0 || strcmp(enabled, "0") == 0)) {
cJSON_Delete(skill_ev);
if (!trigger.enabled) {
reply_error(c, 400, "webhook trigger is disabled");
return;
}
@@ -213,7 +133,6 @@ static void handle_trigger_webhook(struct mg_connection* c,
cJSON* payload = parse_body_json(hm);
if (!payload || !cJSON_IsObject(payload)) {
cJSON_Delete(payload);
cJSON_Delete(skill_ev);
reply_error(c, 400, "invalid JSON body");
return;
}
@@ -221,7 +140,6 @@ static void handle_trigger_webhook(struct mg_connection* c,
cJSON* event = cJSON_CreateObject();
if (!event) {
cJSON_Delete(payload);
cJSON_Delete(skill_ev);
reply_error(c, 500, "oom");
return;
}
@@ -234,7 +152,6 @@ static void handle_trigger_webhook(struct mg_connection* c,
int fired = trigger_manager_fire(g_api_ctx.trigger_manager, d_tag, event, "webhook");
cJSON_Delete(event);
cJSON_Delete(skill_ev);
if (fired < 0) {
reply_error(c, 404, "active trigger not found for d_tag");
+2 -2
View File
@@ -12,8 +12,8 @@
// 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 61
#define DIDACTYL_VERSION "v0.0.61"
#define DIDACTYL_VERSION_PATCH 62
#define DIDACTYL_VERSION "v0.0.62"
// Agent metadata
#define DIDACTYL_NAME "Didactyl"
Executable
+102
View File
@@ -0,0 +1,102 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"
PASS_COUNT=0
FAIL_COUNT=0
pass() {
PASS_COUNT=$((PASS_COUNT + 1))
printf "[PASS] %s\n" "$1"
}
fail() {
FAIL_COUNT=$((FAIL_COUNT + 1))
printf "[FAIL] %s\n" "$1"
}
assert_grep() {
local pattern="$1"
local file="$2"
local desc="$3"
if grep -E -q "$pattern" "$file"; then
pass "$desc"
else
fail "$desc (pattern not found in $file)"
fi
}
assert_cmd() {
local desc="$1"
shift
if "$@" >/dev/null 2>&1; then
pass "$desc"
else
fail "$desc"
fi
}
printf "== Didactyl Trigger Feature Test ==\n"
printf "Root: %s\n" "$ROOT_DIR"
printf "\n-- Build check --\n"
if make -j >/tmp/didactyl-test-build.log 2>&1; then
pass "Project builds successfully"
else
fail "Project build failed (see /tmp/didactyl-test-build.log)"
fi
assert_cmd "Built binary exists" test -x ./didactyl
printf "\n-- Source wiring checks --\n"
assert_grep "TRIGGER_TYPE_WEBHOOK" src/trigger_manager.h "Webhook trigger type defined"
assert_grep "TRIGGER_TYPE_CRON" src/trigger_manager.h "Cron trigger type defined"
assert_grep "TRIGGER_TYPE_CHAIN" src/trigger_manager.h "Chain trigger type defined"
assert_grep "trigger_manager_fire\(" src/trigger_manager.c "Generic trigger fire API implemented"
assert_grep "trigger_manager_fire_chains\(" src/trigger_manager.c "Chain fire API implemented"
assert_grep "cron_matches_now\(" src/trigger_manager.c "Cron matcher implemented"
assert_grep "trigger_type_to_string" src/trigger_manager.c "Trigger type serialization implemented"
assert_grep "\"/api/trigger/\*\"" src/http_api.c "Webhook API route registered"
assert_grep "handle_trigger_webhook\(" src/http_api.c "Webhook handler implemented"
assert_grep "nostr-subscription, webhook, cron, chain" src/tools.c "skill_create validation accepts 4 trigger types"
assert_grep "skill_edit trigger must be one of: nostr-subscription, webhook, cron, chain" src/tools.c "skill_edit validation accepts 4 trigger types"
assert_grep "trigger_manager_fire_chains\(" src/agent.c "Post-execution chain hook wired in agent"
printf "\n-- Docs checks --\n"
assert_grep "\`webhook\`" docs/SKILLS.md "SKILLS docs include webhook trigger"
assert_grep "\`cron\`" docs/SKILLS.md "SKILLS docs include cron trigger"
assert_grep "\`chain\`" docs/SKILLS.md "SKILLS docs include chain trigger"
assert_grep "POST /api/trigger/:d_tag" docs/API.md "API docs include webhook endpoint"
printf "\n-- Optional local API probe --\n"
if command -v curl >/dev/null 2>&1; then
HTTP_CODE=""
HTTP_CODE="$(curl -k -s --connect-timeout 2 --max-time 5 -o /tmp/didactyl-trigger-probe.out -w "%{http_code}" https://127.0.0.1:8484/api/trigger/nonexistent -X POST -H 'Content-Type: application/json' -d '{}' || true)"
if [[ "$HTTP_CODE" == "000" ]]; then
HTTP_CODE="$(curl -s --connect-timeout 2 --max-time 5 -o /tmp/didactyl-trigger-probe.out -w "%{http_code}" http://127.0.0.1:8484/api/trigger/nonexistent -X POST -H 'Content-Type: application/json' -d '{}' || true)"
fi
if [[ "$HTTP_CODE" == "200" || "$HTTP_CODE" == "400" || "$HTTP_CODE" == "404" || "$HTTP_CODE" == "503" ]]; then
pass "Webhook endpoint responds on local API (http code: $HTTP_CODE)"
else
fail "Webhook endpoint probe did not get expected response (http code: ${HTTP_CODE:-none})"
fi
else
fail "curl not available for API probe"
fi
printf "\n== Result ==\n"
printf "Pass: %d\n" "$PASS_COUNT"
printf "Fail: %d\n" "$FAIL_COUNT"
if [[ "$FAIL_COUNT" -gt 0 ]]; then
exit 1
fi
exit 0