v0.0.60 - Remove forced final trigger DM, add skill_edit tool, and complete d_tag terminology migration

This commit is contained in:
Your Name
2026-03-09 20:19:14 -04:00
parent c93972019c
commit 421e1abb15
21 changed files with 6767 additions and 219 deletions

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.59
## Current Status — v0.0.60
**Active build — this project is barely working. Experiment at your own risk.**
> Last release update: v0.0.59Use tool-capable multi-turn execution for triggered skills
> Last release update: v0.0.60Remove forced final trigger DM, add skill_edit tool, and complete d_tag terminology migration
- Connects to configured relays with auto-reconnect and relay state transition logging
- Publishes configured startup events per relay as each relay becomes connected

File diff suppressed because it is too large Load Diff

View File

@@ -477,8 +477,8 @@ The following endpoints are planned but not yet implemented:
| GET | `/api/events/soul` | Fetch agent soul event |
| PUT | `/api/events/soul` | Update soul content |
| GET | `/api/events/skills` | List published skills |
| GET | `/api/events/skills/:slug` | Fetch skill by slug |
| PUT | `/api/events/skills/:slug` | Update skill |
| GET | `/api/events/skills/:d_tag` | Fetch skill by d_tag |
| PUT | `/api/events/skills/:d_tag` | Update skill |
| GET | `/api/events/adoption` | Fetch adoption list |
| GET | `/api/events/profile` | Fetch agent profile |
| PUT | `/api/events/profile` | Update agent profile |

View File

@@ -75,9 +75,9 @@ All endpoints return JSON. All mutations use POST/PUT/DELETE. All reads use GET.
| GET | `/api/events/soul` | Fetch the agent soul event (kind 31120, d=soul) |
| PUT | `/api/events/soul` | Update soul content, republish to relays |
| GET | `/api/events/skills` | List all published skills (kind 31123/31124 by own pubkey) |
| GET | `/api/events/skills/:slug` | Fetch a single skill by slug |
| PUT | `/api/events/skills/:slug` | Update skill content/tags, republish |
| DELETE | `/api/events/skills/:slug` | Remove skill from adoption list |
| GET | `/api/events/skills/:d_tag` | Fetch a single skill by d_tag |
| PUT | `/api/events/skills/:d_tag` | Update skill content/tags, republish |
| DELETE | `/api/events/skills/:d_tag` | Remove skill from adoption list |
| GET | `/api/events/adoption` | Fetch kind 10123 adoption list |
| GET | `/api/events/startup` | List startup events from config |
| GET | `/api/events/profile` | Fetch agent kind 0 profile |

View File

@@ -53,7 +53,7 @@ These are not yet implemented but are on the roadmap:
| GET | `/api/events/soul` | Agent soul/system prompt event |
| PUT | `/api/events/soul` | Update soul content |
| GET | `/api/events/skills` | List skills |
| GET/PUT | `/api/events/skills/:slug` | Read/update individual skills |
| GET/PUT | `/api/events/skills/:d_tag` | Read/update individual skills |
| GET | `/api/events/profile` | Agent Nostr profile |
| GET | `/api/tools` | List all tool schemas |
| POST | `/api/tools/:name/execute` | Execute a tool directly |

View File

@@ -21,7 +21,7 @@ The entire triggered-skill infrastructure is **already built and functional**:
### Gap Found
The `skill_create` tool **schema** (what the LLM sees) only exposes 5 parameters: `slug`, `content`, `scope`, `description`, `auto_adopt`. The execution function already handles `trigger`, `filter`, `action`, and `enabled` — but these are **not declared in the tool schema**, so the LLM will never pass them.
The `skill_create` tool **schema** (what the LLM sees) only exposes 5 parameters: `d_tag`, `content`, `scope`, `description`, `auto_adopt`. The execution function already handles `trigger`, `filter`, `action`, and `enabled` — but these are **not declared in the tool schema**, so the LLM will never pass them.
---

View File

@@ -109,7 +109,7 @@ The agent selects the profile matching the active model, falling back to `defaul
│ - skill_remove (remove) │
│ │
│ Structure per skill: │
│ - slug (string) │
│ - d_tag (string) │
│ - description (string) │
│ - content (string, full) │
│ - scope (public/private) │

View File

@@ -34,7 +34,7 @@ NIP: NIP-23
Event Kind: 30023
Format: The content field must be markdown text...
Required Tags:
- d: Addressable identifier slug...
- d: Addressable identifier d_tag...
- title: Human-readable article title
- published_at: Unix timestamp as string...
Procedure:

View File

@@ -12,12 +12,12 @@ Skills are Nostr events. The tools are thin orchestration wrappers over the exis
| Kind | Purpose | Replaceable? | Key tag |
|---|---|---|---|
| `31123` | Public skill definition | Yes (d-tag) | `d=<slug>` |
| `31124` | Private skill definition | Yes (d-tag) | `d=<slug>` |
| `31123` | Public skill definition | Yes (d-tag) | `d=<d_tag>` |
| `31124` | Private skill definition | Yes (d-tag) | `d=<d_tag>` |
| `10123` | Public skill adoption list | Yes (replaceable) | `a` refs to 31123 events |
All skill events carry these standard tags:
- `["d", "<slug>"]` — unique identifier within the author's pubkey
- `["d", "<d_tag>"]` — unique identifier within the author's pubkey
- `["app", "didactyl"]` — app namespace
- `["scope", "public"]` or `["scope", "private"]`
@@ -38,37 +38,37 @@ All skill events carry these standard tags:
"parameters": {
"type": "object",
"properties": {
"slug": { "type": "string", "description": "Unique skill identifier (lowercase, hyphens allowed)" },
"d_tag": { "type": "string", "description": "Unique skill identifier (lowercase, hyphens allowed)" },
"content": { "type": "string", "description": "Skill body — markdown instructions or structured JSON" },
"scope": { "type": "string", "description": "public (kind 31123) or private (kind 31124). Default: public" },
"description": { "type": "string", "description": "Short one-line description for the skill" },
"auto_adopt": { "type": "boolean", "description": "Automatically add to adoption list (kind 10123). Default: true" }
},
"required": ["slug", "content"]
"required": ["d_tag", "content"]
}
}
```
**Execution logic (`execute_skill_create`):**
1. Validate `slug` — must be non-empty, lowercase alphanumeric + hyphens, no spaces
1. Validate `d_tag` — must be non-empty, lowercase alphanumeric + hyphens, no spaces
2. Determine kind: `31123` if scope is `"public"` or absent; `31124` if `"private"`
3. Build tags array:
- `["d", slug]`
- `["d", d_tag]`
- `["app", "didactyl"]`
- `["scope", scope]`
- `["description", description]` if provided
4. Call `nostr_handler_publish_kind_event(kind, content, tags, &result)`
5. If `auto_adopt` is true (default), update the kind `10123` adoption list:
- Query existing `10123` event for own pubkey (same pattern as `execute_nostr_list_manage`)
- Add `["a", "31123:<own_pubkey>:<slug>"]` tag if not already present
- Add `["a", "31123:<own_pubkey>:<d_tag>"]` tag if not already present
- Republish the updated `10123` event
6. Return JSON with `success`, `event_id`, `naddr_uri`, `slug`, `adopted`
6. Return JSON with `success`, `event_id`, `naddr_uri`, `d_tag`, `adopted`
**Key design decisions:**
- Auto-adopt defaults to `true` — creating a skill you don't adopt is unusual
- Private skills (31124) are NOT added to the public adoption list (10123)
- Republishing with the same slug replaces the previous version (replaceable event)
- Republishing with the same d_tag replaces the previous version (replaceable event)
---
@@ -99,7 +99,7 @@ All skill events carry these standard tags:
- Private only: `{"kinds": [31124], "authors": [own_pubkey]}`
2. Call `nostr_handler_query_json(filter, 8000)`
3. Parse results, extract for each event:
- `slug` (from d-tag)
- `d_tag` (from d-tag)
- `kind`
- `scope` (from scope tag)
- `description` (from description tag, if present)
@@ -123,21 +123,21 @@ All skill events carry these standard tags:
"type": "object",
"properties": {
"pubkey": { "type": "string", "description": "Hex pubkey of the skill author" },
"slug": { "type": "string", "description": "Skill slug (d-tag value)" },
"d_tag": { "type": "string", "description": "Skill d_tag (d-tag value)" },
"kind": { "type": "integer", "description": "Skill kind (31123 or 31124). Default: 31123" }
},
"required": ["pubkey", "slug"]
"required": ["pubkey", "d_tag"]
}
}
```
**Execution logic (`execute_skill_adopt`):**
1. Validate pubkey (64-char hex) and slug (non-empty)
1. Validate pubkey (64-char hex) and d_tag (non-empty)
2. Default kind to 31123 if not provided
3. Build the `a`-tag value: `"<kind>:<pubkey>:<slug>"`
3. Build the `a`-tag value: `"<kind>:<pubkey>:<d_tag>"`
4. Query existing kind `10123` event for own pubkey
5. Check if `["a", "<kind>:<pubkey>:<slug>"]` already exists — if so, return success with `already_adopted: true`
5. Check if `["a", "<kind>:<pubkey>:<d_tag>"]` already exists — if so, return success with `already_adopted: true`
6. Add the tag, republish `10123`
7. Return JSON with `success`, `adopted_address`, `event_id`
@@ -157,10 +157,10 @@ All skill events carry these standard tags:
"type": "object",
"properties": {
"pubkey": { "type": "string", "description": "Hex pubkey of the skill author. Defaults to own pubkey." },
"slug": { "type": "string", "description": "Skill slug to remove" },
"d_tag": { "type": "string", "description": "Skill d_tag to remove" },
"kind": { "type": "integer", "description": "Skill kind (31123 or 31124). Default: 31123" }
},
"required": ["slug"]
"required": ["d_tag"]
}
}
```
@@ -169,7 +169,7 @@ All skill events carry these standard tags:
1. Default pubkey to own pubkey if not provided
2. Default kind to 31123
3. Build the `a`-tag value: `"<kind>:<pubkey>:<slug>"`
3. Build the `a`-tag value: `"<kind>:<pubkey>:<d_tag>"`
4. Query existing kind `10123` event for own pubkey
5. Find and remove matching `["a", ...]` tag
6. Republish `10123`
@@ -211,7 +211,7 @@ All skill events carry these standard tags:
- Return skill summaries
3. If `query` is provided (keyword search):
- Query `{"kinds": [31123]}` with limit
- Filter results client-side by matching `query` against slug, description tag, or content
- Filter results client-side by matching `query` against d_tag, description tag, or content
- Return matching skill summaries
4. Default (no params): return own adopted skills from `10123`
@@ -236,7 +236,7 @@ This is essentially the same pattern already used in `execute_nostr_list_manage(
### Shared helper: skill summary extraction
```c
// Extract slug, kind, scope, description, created_at from a skill event JSON
// Extract d_tag, kind, scope, description, created_at from a skill event JSON
static cJSON* extract_skill_summary(cJSON* event);
```
@@ -269,7 +269,7 @@ flowchart TD
1. **Schema registration** — Add 5 new tool definitions in `tools_build_openai_schema_json()` (t22t26)
2. **Execution functions** — Add 5 new `execute_skill_*()` static functions
3. **Dispatch** — Add 5 new `strcmp` branches in `tools_execute()`
4. **Shared helpers** — Add `fetch_adoption_list_tags()`, `publish_adoption_list()`, `extract_skill_summary()`, and `validate_skill_slug()`
4. **Shared helpers** — Add `fetch_adoption_list_tags()`, `publish_adoption_list()`, `extract_skill_summary()`, and `validate_skill_d_tag()`
### `README.md`
@@ -284,19 +284,19 @@ flowchart TD
## Slug Validation Rules
A valid skill slug must:
A valid skill d_tag must:
- Be 164 characters
- Contain only lowercase letters, digits, and hyphens
- Not start or end with a hyphen
- Not contain consecutive hyphens
```c
static int validate_skill_slug(const char* slug) {
if (!slug || slug[0] == '\0' || strlen(slug) > 64) return 0;
if (slug[0] == '-') return 0;
static int validate_skill_d_tag(const char* d_tag) {
if (!d_tag || d_tag[0] == '\0' || strlen(d_tag) > 64) return 0;
if (d_tag[0] == '-') return 0;
int prev_dash = 0;
for (size_t i = 0; slug[i]; i++) {
char c = slug[i];
for (size_t i = 0; d_tag[i]; i++) {
char c = d_tag[i];
if (c == '-') {
if (prev_dash) return 0;
prev_dash = 1;
@@ -306,7 +306,7 @@ static int validate_skill_slug(const char* slug) {
return 0;
}
}
if (slug[strlen(slug) - 1] == '-') return 0;
if (d_tag[strlen(d_tag) - 1] == '-') return 0;
return 1;
}
```
@@ -315,7 +315,7 @@ static int validate_skill_slug(const char* slug) {
## Implementation Order
1. **Shared helpers**`validate_skill_slug`, `fetch_adoption_list_tags`, `publish_adoption_list`, `extract_skill_summary`
1. **Shared helpers**`validate_skill_d_tag`, `fetch_adoption_list_tags`, `publish_adoption_list`, `extract_skill_summary`
2. **`skill_create`** — most important tool, enables the agent to author skills
3. **`skill_list`** — lets the agent see what it has published
4. **`skill_adopt`** — adopt skills from other authors

View File

@@ -19,7 +19,7 @@ A skill is a Nostr event (kind `31123` for public, `31124` for private) that def
- **Parameters** like temperature and max_tokens
- A **description** for human display
Skills are replaceable events keyed by their `d` tag (the slug). Publishing a new event with the same `d` tag replaces the previous version.
Skills are replaceable events keyed by their `d` tag (the d_tag). Publishing a new event with the same `d` tag replaces the previous version.
### Skill Event Structure
@@ -206,7 +206,7 @@ To find skills by a specific author:
1. Receive the event from the relay
2. Parse `event.content` as JSON to get the skill definition object
3. Extract `description`, `template`, `temperature`, `max_tokens`
4. Extract the `d` tag value from `event.tags` as the skill slug
4. Extract the `d` tag value from `event.tags` as the skill d_tag
5. Extract the `description` tag from `event.tags` as a display label
6. The skill is ready to display and execute
@@ -261,7 +261,7 @@ To find skills by a specific author:
- **Skills list**: Radio buttons or selectable cards, one skill selected at a time
- Each skill card shows:
- Skill slug (the `d` tag)
- Skill d_tag (the `d` tag)
- Description (from the `description` tag or content field)
- Author pubkey (truncated, e.g., `52a3e8...0acb8`)
- **Run Selected Skill** button: executes the selected skill against the current text
@@ -398,7 +398,7 @@ sequenceDiagram
**Building the event:**
```javascript
function buildSkillEvent(slug, description, template, temperature, maxTokens) {
function buildSkillEvent(d_tag, description, template, temperature, maxTokens) {
const content = JSON.stringify({
description: description,
context_mode: 'full',
@@ -413,7 +413,7 @@ function buildSkillEvent(slug, description, template, temperature, maxTokens) {
kind: 31123,
content: content,
tags: [
['d', slug],
['d', d_tag],
['app', 'didactyl'],
['scope', 'public'],
['description', description],

View File

@@ -62,13 +62,13 @@ Today, skills are passive — they're injected into the LLM context via [`append
#### A. `/run` slash command (admin direct invocation)
```
/run deploy-website staging # run own adopted skill by slug
/run deploy-website staging # run own adopted skill by d_tag
/run 31123:<pubkey>:deploy-website staging # run anyone's skill by address
/run deploy-website {"target": "production"} # JSON args
```
Parsing:
1. First token after `/run` is the skill identifier (slug or `kind:pubkey:slug` address)
1. First token after `/run` is the skill identifier (d_tag or `kind:pubkey:d_tag` address)
2. Everything after is args (try JSON first, fall back to `{"input": "plain text"}`)
#### B. `skill_run` tool (LLM-mediated invocation)
@@ -78,13 +78,13 @@ The LLM can invoke skills on behalf of the admin during conversation:
```json
{
"name": "skill_run",
"description": "Fetch and execute a skill one-shot without adopting it. Works with own adopted skills by slug or any public skill by address.",
"description": "Fetch and execute a skill one-shot without adopting it. Works with own adopted skills by d_tag or any public skill by address.",
"parameters": {
"type": "object",
"properties": {
"slug": { "type": "string", "description": "Skill slug for own adopted skills" },
"address": { "type": "string", "description": "Full skill address: kind:pubkey:slug" },
"pubkey": { "type": "string", "description": "Author pubkey, used with slug to form address" },
"d_tag": { "type": "string", "description": "Skill d_tag for own adopted skills" },
"address": { "type": "string", "description": "Full skill address: kind:pubkey:d_tag" },
"pubkey": { "type": "string", "description": "Author pubkey, used with d_tag to form address" },
"args": { "type": "string", "description": "Arguments or context to pass to the skill" },
"sandbox": { "type": "boolean", "description": "Override sandbox setting. Default: true for external, false for own skills" }
}
@@ -104,8 +104,8 @@ Both `/run` and `skill_run` use the same underlying executor:
```mermaid
graph TD
A[/run or skill_run called] --> B{Skill identifier type?}
B -->|slug only| C[Look up in adopted skills cache]
B -->|address or pubkey+slug| D[Fetch skill event from Nostr]
B -->|d_tag only| C[Look up in adopted skills cache]
B -->|address or pubkey+d_tag| D[Fetch skill event from Nostr]
C --> E{Found?}
D --> E
E -->|No| F[Return error: skill not found]
@@ -133,7 +133,7 @@ Skill execution context:
- The user's arguments provide the context for this execution.
- Keep output concise and actionable.
Skill slug: deploy-website
Skill d_tag: deploy-website
Skill address: 31123:<pubkey>:deploy-website
Skill source: [own | external:<author_display_name>]
@@ -175,7 +175,7 @@ graph TD
A[Friend creates skill] -->|kind 31123| B[Published on Nostr relays]
B --> C{How do you find it?}
C -->|skill_search popular:true| D[Discovery via WoT adoption lists]
C -->|Friend tells you the slug| E[Direct reference]
C -->|Friend tells you the d_tag| E[Direct reference]
C -->|skill_search pubkey:friend| F[Browse friends skills]
D --> G[skill_run - try it once, sandboxed]
E --> G
@@ -185,7 +185,7 @@ graph TD
H -->|No| J[Done - nothing persisted]
I --> K[Shows in adopted skills context]
K --> L[Agent uses it automatically]
L -->|Or invoke directly| M[/run skill-slug args]
L -->|Or invoke directly| M[/run skill-d_tag args]
```
## 3. Skill-Tool Maturity Levels
@@ -255,7 +255,7 @@ steps:
args:
kind: 30023
content: "{{file_content}}"
tags: [["d", "{{slug}}"]]
tags: [["d", "{{d_tag}}"]]
# Conditional - simple
- if: "{{listing.success}}"
@@ -266,7 +266,7 @@ steps:
- return: "Failed: {{listing.error}}"
# Return final result
- return: "Published to {{slug}}"
- return: "Published to {{d_tag}}"
```
### Variable Substitution
@@ -333,7 +333,7 @@ typedef enum {
| Scenario | Default sandbox | Override |
|----------|----------------|---------|
| Own adopted skill via `/run slug` | No sandbox | N/A |
| Own adopted skill via `/run d_tag` | No sandbox | N/A |
| External skill via `/run address` | Sandbox ON | `/run --unsafe address` |
| `skill_run` tool, own skill | No sandbox | `sandbox: true` |
| `skill_run` tool, external skill | Sandbox ON | `sandbox: false` |

View File

@@ -21,7 +21,7 @@ Create `src/trigger_manager.c` and `src/trigger_manager.h` — the core componen
#define TRIGGER_COOLDOWN_SECONDS 60
typedef struct {
char skill_slug[65];
char skill_d_tag[65];
char skill_content[4096]; // the action template or LLM prompt
int action_type; // 0 = llm, 1 = template
char filter_json[2048]; // the Nostr subscription filter
@@ -43,11 +43,11 @@ typedef struct {
```c
int trigger_manager_init(trigger_manager_t* mgr, didactyl_config_t* cfg);
int trigger_manager_load_from_skills(trigger_manager_t* mgr);
int trigger_manager_add(trigger_manager_t* mgr, const char* skill_slug,
int trigger_manager_add(trigger_manager_t* mgr, const char* skill_d_tag,
const char* content, const char* filter_json,
int action_type, int enabled);
int trigger_manager_remove(trigger_manager_t* mgr, const char* skill_slug);
int trigger_manager_update(trigger_manager_t* mgr, const char* skill_slug,
int trigger_manager_remove(trigger_manager_t* mgr, const char* skill_d_tag);
int trigger_manager_update(trigger_manager_t* mgr, const char* skill_d_tag,
const char* content, const char* filter_json,
int action_type, int enabled);
int trigger_manager_active_count(trigger_manager_t* mgr);
@@ -115,7 +115,7 @@ When `skill_remove` is called for a triggered skill:
The agent currently only handles DM-initiated conversations via `agent_on_message()`. Add a new entry point:
```c
void agent_on_trigger(const char* skill_slug,
void agent_on_trigger(const char* skill_d_tag,
const char* skill_content,
cJSON* triggering_event,
const char* relay_url);

View File

@@ -48,7 +48,7 @@ typedef struct {
typedef struct {
int kind;
char author_pubkey_hex[65];
char slug[65];
char d_tag[65];
char* content;
} agent_adopted_skill_t;
@@ -473,14 +473,14 @@ static char* build_slash_help_all_json(void) {
int sn = cJSON_GetArraySize(skills);
for (int i = 0; i < sn; i++) {
cJSON* row = cJSON_GetArrayItem(skills, i);
cJSON* slug = row ? cJSON_GetObjectItemCaseSensitive(row, "slug") : NULL;
cJSON* d_tag = row ? cJSON_GetObjectItemCaseSensitive(row, "d_tag") : NULL;
cJSON* desc = row ? cJSON_GetObjectItemCaseSensitive(row, "description") : NULL;
const char* slug_s = (slug && cJSON_IsString(slug) && slug->valuestring) ? slug->valuestring : NULL;
const char* d_tag_s = (d_tag && cJSON_IsString(d_tag) && d_tag->valuestring) ? d_tag->valuestring : NULL;
const char* desc_s = (desc && cJSON_IsString(desc) && desc->valuestring) ? desc->valuestring : "";
if (!slug_s || slug_s[0] == '\0') {
if (!d_tag_s || d_tag_s[0] == '\0') {
continue;
}
if (append_textf_local(&out, &cap, &used, "- %s%s%s\n", slug_s, desc_s[0] ? "" : "", desc_s) != 0) {
if (append_textf_local(&out, &cap, &used, "- %s%s%s\n", d_tag_s, desc_s[0] ? "" : "", desc_s) != 0) {
cJSON_Delete(skill_root);
free(skill_list_json);
free(out);
@@ -1449,8 +1449,8 @@ static cJSON* find_tag_value_string_local(cJSON* tags, const char* key) {
return NULL;
}
static int parse_skill_address_tag_local(const char* addr, int* out_kind, char out_pubkey_hex[65], char out_slug[65]) {
if (!addr || !out_kind || !out_pubkey_hex || !out_slug) {
static int parse_skill_address_tag_local(const char* addr, int* out_kind, char out_pubkey_hex[65], char out_d_tag[65]) {
if (!addr || !out_kind || !out_pubkey_hex || !out_d_tag) {
return -1;
}
@@ -1478,11 +1478,11 @@ static int parse_skill_address_tag_local(const char* addr, int* out_kind, char o
memcpy(out_pubkey_hex, p1 + 1, 64U);
out_pubkey_hex[64] = '\0';
size_t slug_len = strlen(p2 + 1);
if (slug_len == 0 || slug_len >= 65U) {
size_t d_tag_len = strlen(p2 + 1);
if (d_tag_len == 0 || d_tag_len >= 65U) {
return -1;
}
memcpy(out_slug, p2 + 1, slug_len + 1U);
memcpy(out_d_tag, p2 + 1, d_tag_len + 1U);
*out_kind = kind;
return 0;
@@ -1494,7 +1494,7 @@ static void clear_adopted_skills_cache_locked(void) {
g_adopted_skills[i].content = NULL;
g_adopted_skills[i].kind = 0;
g_adopted_skills[i].author_pubkey_hex[0] = '\0';
g_adopted_skills[i].slug[0] = '\0';
g_adopted_skills[i].d_tag[0] = '\0';
}
g_adopted_skills_count = 0;
}
@@ -1543,8 +1543,8 @@ static int refresh_adopted_skills_cache_if_needed(void) {
int skill_kind = 0;
char skill_author[65] = {0};
char skill_slug[65] = {0};
if (parse_skill_address_tag_local(val->valuestring, &skill_kind, skill_author, skill_slug) != 0) {
char skill_d_tag[65] = {0};
if (parse_skill_address_tag_local(val->valuestring, &skill_kind, skill_author, skill_d_tag) != 0) {
continue;
}
@@ -1566,7 +1566,7 @@ static int refresh_adopted_skills_cache_if_needed(void) {
if (!ev_pubkey || !cJSON_IsString(ev_pubkey) || !ev_pubkey->valuestring ||
strcmp(ev_pubkey->valuestring, skill_author) != 0 ||
!ev_d || !cJSON_IsString(ev_d) || !ev_d->valuestring ||
strcmp(ev_d->valuestring, skill_slug) != 0) {
strcmp(ev_d->valuestring, skill_d_tag) != 0) {
continue;
}
cJSON* dup = cJSON_Duplicate(ev, 1);
@@ -1595,7 +1595,7 @@ static int refresh_adopted_skills_cache_if_needed(void) {
cJSON_AddItemToObject(skill_filter, "kinds", sk_kinds);
cJSON_AddItemToArray(sk_authors, cJSON_CreateString(skill_author));
cJSON_AddItemToObject(skill_filter, "authors", sk_authors);
cJSON_AddItemToArray(d_values, cJSON_CreateString(skill_slug));
cJSON_AddItemToArray(d_values, cJSON_CreateString(skill_d_tag));
cJSON_AddItemToObject(skill_filter, "#d", d_values);
cJSON_AddNumberToObject(skill_filter, "limit", 1);
@@ -1632,7 +1632,7 @@ static int refresh_adopted_skills_cache_if_needed(void) {
tmp[tmp_count].kind = skill_kind;
snprintf(tmp[tmp_count].author_pubkey_hex, sizeof(tmp[tmp_count].author_pubkey_hex), "%s", skill_author);
snprintf(tmp[tmp_count].slug, sizeof(tmp[tmp_count].slug), "%s", skill_slug);
snprintf(tmp[tmp_count].d_tag, sizeof(tmp[tmp_count].d_tag), "%s", skill_d_tag);
tmp[tmp_count].content = strdup(content->valuestring);
if (tmp[tmp_count].content) {
tmp_count++;
@@ -1652,17 +1652,17 @@ static int refresh_adopted_skills_cache_if_needed(void) {
continue;
}
char slug[65] = {0};
char d_tag_val[65] = {0};
if (se->tags_json) {
cJSON* tags = cJSON_Parse(se->tags_json);
cJSON* d_tag = tags ? find_tag_value_string_local(tags, "d") : NULL;
if (d_tag && cJSON_IsString(d_tag) && d_tag->valuestring && d_tag->valuestring[0] != '\0') {
snprintf(slug, sizeof(slug), "%s", d_tag->valuestring);
snprintf(d_tag_val, sizeof(d_tag_val), "%s", d_tag->valuestring);
}
cJSON_Delete(tags);
}
if (slug[0] == '\0') {
snprintf(slug, sizeof(slug), "startup-skill-%d", i + 1);
if (d_tag_val[0] == '\0') {
snprintf(d_tag_val, sizeof(d_tag_val), "startup-skill-%d", i + 1);
}
tmp[tmp_count].kind = se->kind;
@@ -1672,7 +1672,7 @@ static int refresh_adopted_skills_cache_if_needed(void) {
g_cfg->keys.public_key_hex[0] != '\0'
? g_cfg->keys.public_key_hex
: "unknown");
snprintf(tmp[tmp_count].slug, sizeof(tmp[tmp_count].slug), "%s", slug);
snprintf(tmp[tmp_count].d_tag, sizeof(tmp[tmp_count].d_tag), "%s", d_tag_val);
tmp[tmp_count].content = strdup(se->content);
if (tmp[tmp_count].content) {
tmp_count++;
@@ -1803,11 +1803,11 @@ static int append_adopted_skills_context(cJSON* messages) {
int head_n = snprintf(payload + used,
capacity - used,
"\n\n---\n\n### Skill\nSkill slug: %s\nSkill address: %d:%s:%s\n\nInstructions:\n",
g_adopted_skills[i].slug,
"\n\n---\n\n### Skill\nSkill d_tag: %s\nSkill address: %d:%s:%s\n\nInstructions:\n",
g_adopted_skills[i].d_tag,
g_adopted_skills[i].kind,
g_adopted_skills[i].author_pubkey_hex,
g_adopted_skills[i].slug);
g_adopted_skills[i].d_tag);
if (head_n < 0 || (size_t)head_n >= (capacity - used)) {
break;
}
@@ -1910,11 +1910,11 @@ void agent_set_trigger_manager(struct trigger_manager* trigger_manager) {
g_tools_ctx.trigger_manager = trigger_manager;
}
void agent_on_trigger(const char* skill_slug,
void agent_on_trigger(const char* skill_d_tag,
const char* skill_content,
cJSON* triggering_event,
const char* relay_url) {
if (!g_cfg || !g_system_context || !skill_slug || !skill_content || !triggering_event) {
if (!g_cfg || !g_system_context || !skill_d_tag || !skill_content || !triggering_event) {
return;
}
@@ -1931,9 +1931,9 @@ void agent_on_trigger(const char* skill_slug,
"- Execute the skill instructions below against the triggering event.\n"
"- Keep output concise and actionable for the administrator.\n"
"- If no action is needed, explicitly say why.\n\n"
"Skill slug: ";
"Skill d_tag: ";
size_t system_len = strlen(g_system_context) + 2 + strlen(trigger_prefix) + strlen(skill_slug) +
size_t system_len = strlen(g_system_context) + 2 + strlen(trigger_prefix) + strlen(skill_d_tag) +
strlen("\nRelay: ") + strlen(relay) + strlen("\n\nSkill instructions:\n") +
strlen(skill_content) + 1U;
char* system_prompt = (char*)malloc(system_len);
@@ -1947,7 +1947,7 @@ void agent_on_trigger(const char* skill_slug,
"%s\n\n%s%s\nRelay: %s\n\nSkill instructions:\n%s",
g_system_context,
trigger_prefix,
skill_slug,
skill_d_tag,
relay,
skill_content);
@@ -1987,7 +1987,6 @@ void agent_on_trigger(const char* skill_slug,
free(user_prompt);
int max_turns = g_cfg->tools.max_turns > 0 ? g_cfg->tools.max_turns : 6;
char* final_answer_owned = NULL;
for (int turn = 0; turn < max_turns; turn++) {
char* messages_json = cJSON_PrintUnformatted(messages);
@@ -2008,8 +2007,6 @@ void agent_on_trigger(const char* skill_slug,
}
if (resp.tool_call_count <= 0) {
const char* answer = resp.content ? resp.content : "No response content.";
final_answer_owned = strdup(answer);
llm_response_free(&resp);
break;
}
@@ -2043,14 +2040,6 @@ void agent_on_trigger(const char* skill_slug,
llm_response_free(&resp);
}
if (!final_answer_owned) {
final_answer_owned = strdup("I hit my tool-use limit for this trigger.");
}
const char* final_answer = final_answer_owned ? final_answer_owned : "I hit my tool-use limit for this trigger.";
(void)nostr_handler_send_dm_auto(g_cfg->admin.pubkey, final_answer);
free(final_answer_owned);
cJSON_Delete(messages);
free(tools_json);
}

View File

@@ -10,7 +10,7 @@ struct trigger_manager;
int agent_init(didactyl_config_t* config, const char* system_context);
void agent_set_trigger_manager(struct trigger_manager* trigger_manager);
void agent_on_trigger(const char* skill_slug,
void agent_on_trigger(const char* skill_d_tag,
const char* skill_content,
cJSON* triggering_event,
const char* relay_url);

View File

@@ -548,27 +548,23 @@ static int normalize_skill_d_tag(int event_kind, cJSON* item, cJSON* tags) {
return 0;
}
const char* slug = NULL;
cJSON* slug_val = find_tag_value_string(tags, "slug");
if (slug_val && cJSON_IsString(slug_val) && slug_val->valuestring && slug_val->valuestring[0] != '\0') {
slug = slug_val->valuestring;
}
const char* d_tag = NULL;
if (!slug) {
if (!d_tag) {
cJSON* content_fields = cJSON_GetObjectItemCaseSensitive(item, "content_fields");
if (content_fields && cJSON_IsObject(content_fields)) {
cJSON* name = cJSON_GetObjectItemCaseSensitive(content_fields, "name");
if (name && cJSON_IsString(name) && name->valuestring && name->valuestring[0] != '\0') {
slug = name->valuestring;
d_tag = name->valuestring;
}
}
}
if (!slug) {
if (!d_tag) {
return 0;
}
return set_tag_value_string(tags, "d", slug);
return set_tag_value_string(tags, "d", d_tag);
}
static int parse_startup_events(cJSON* root, didactyl_config_t* config) {

View File

@@ -524,14 +524,14 @@ static char* build_slash_help_all_json_local(void) {
int sn = cJSON_GetArraySize(skills);
for (int i = 0; i < sn; i++) {
cJSON* row = cJSON_GetArrayItem(skills, i);
cJSON* slug = row ? cJSON_GetObjectItemCaseSensitive(row, "slug") : NULL;
cJSON* d_tag = row ? cJSON_GetObjectItemCaseSensitive(row, "d_tag") : NULL;
cJSON* desc = row ? cJSON_GetObjectItemCaseSensitive(row, "description") : NULL;
const char* slug_s = (slug && cJSON_IsString(slug) && slug->valuestring) ? slug->valuestring : NULL;
const char* d_tag_s = (d_tag && cJSON_IsString(d_tag) && d_tag->valuestring) ? d_tag->valuestring : NULL;
const char* desc_s = (desc && cJSON_IsString(desc) && desc->valuestring) ? desc->valuestring : "";
if (!slug_s || slug_s[0] == '\0') {
if (!d_tag_s || d_tag_s[0] == '\0') {
continue;
}
if (append_textf_http(&out, &cap, &used, "- %s%s%s\n", slug_s, desc_s[0] ? "" : "", desc_s) != 0) {
if (append_textf_http(&out, &cap, &used, "- %s%s%s\n", d_tag_s, desc_s[0] ? "" : "", desc_s) != 0) {
cJSON_Delete(skill_root);
free(skill_list_json);
free(out);

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 59
#define DIDACTYL_VERSION "v0.0.59"
#define DIDACTYL_VERSION_PATCH 60
#define DIDACTYL_VERSION "v0.0.60"
// Agent metadata
#define DIDACTYL_NAME "Didactyl"

View File

@@ -482,13 +482,13 @@ static void register_trigger_from_self_skill_event(cJSON* event) {
return;
}
const char* slug = find_tag_value_local(tags, "d");
const char* d_tag = find_tag_value_local(tags, "d");
const char* trigger = find_tag_value_local(tags, "trigger");
const char* filter = find_tag_value_local(tags, "filter");
const char* action = find_tag_value_local(tags, "action");
const char* enabled = find_tag_value_local(tags, "enabled");
if (!slug || slug[0] == '\0' ||
if (!d_tag || d_tag[0] == '\0' ||
!trigger || strcmp(trigger, "nostr-subscription") != 0 ||
!filter || filter[0] == '\0') {
return;
@@ -500,18 +500,18 @@ static void register_trigger_from_self_skill_event(cJSON* event) {
int is_enabled = parse_enabled_flag_local(enabled);
int rc = trigger_manager_add((trigger_manager_t*)g_trigger_manager,
slug,
d_tag,
content->valuestring,
filter,
action_type,
is_enabled);
if (rc == 0) {
DEBUG_INFO("[didactyl] live self-skill trigger registered slug=%s action=%s enabled=%d",
slug,
DEBUG_INFO("[didactyl] live self-skill trigger registered d_tag=%s action=%s enabled=%d",
d_tag,
action_type == TRIGGER_ACTION_TEMPLATE ? "template" : "llm",
is_enabled);
} else {
DEBUG_WARN("[didactyl] live self-skill trigger register failed slug=%s", slug);
DEBUG_WARN("[didactyl] live self-skill trigger register failed d_tag=%s", d_tag);
}
}

View File

@@ -21,6 +21,8 @@
#include "llm.h"
#include "../../nostr_core_lib/nostr_core/nostr_core.h"
static int validate_skill_d_tag(const char* d_tag);
static char* json_error(const char* msg) {
cJSON* root = cJSON_CreateObject();
if (!root) return NULL;
@@ -251,6 +253,27 @@ static int add_string_tag(cJSON* tags, const char* key, const char* value) {
return 0;
}
static int remove_tag_key_all(cJSON* tags, const char* key) {
if (!tags || !cJSON_IsArray(tags) || !key || key[0] == '\0') return 0;
int removed = 0;
for (int i = cJSON_GetArraySize(tags) - 1; i >= 0; i--) {
cJSON* tag = cJSON_GetArrayItem(tags, i);
if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 1) continue;
cJSON* k = cJSON_GetArrayItem(tag, 0);
if (k && cJSON_IsString(k) && k->valuestring && strcmp(k->valuestring, key) == 0) {
cJSON_DeleteItemFromArray(tags, i);
removed++;
}
}
return removed;
}
static int upsert_string_tag(cJSON* tags, const char* key, const char* value) {
if (!tags || !cJSON_IsArray(tags) || !key || !value || value[0] == '\0') return -1;
remove_tag_key_all(tags, key);
return add_string_tag(tags, key, value);
}
static char* trim_copy(const char* start, size_t len) {
while (len > 0 && isspace((unsigned char)start[0])) {
start++;
@@ -431,7 +454,7 @@ static char* first_markdown_image_url(const char* content) {
return NULL;
}
static char* slugify_string(const char* in, const char* fallback) {
static char* d_tagify_string(const char* in, const char* fallback) {
if (!in || in[0] == '\0') {
return fallback ? strdup(fallback) : NULL;
}
@@ -493,17 +516,17 @@ static void ensure_nip23_metadata_tags(int kind, const char* content, cJSON** ta
}
if (!has_tag_key(tags, "d")) {
char* slug = NULL;
char* d_tag = NULL;
if (title) {
slug = slugify_string(title, "long-form-note");
d_tag = d_tagify_string(title, "long-form-note");
} else {
char* derived_title = first_markdown_h1(content);
slug = slugify_string(derived_title, "long-form-note");
d_tag = d_tagify_string(derived_title, "long-form-note");
free(derived_title);
}
if (slug) {
(void)add_string_tag(tags, "d", slug);
free(slug);
if (d_tag) {
(void)add_string_tag(tags, "d", d_tag);
free(d_tag);
}
}
@@ -847,15 +870,15 @@ static cJSON* find_tag_value_string(cJSON* tags, const char* key) {
return NULL;
}
static int validate_skill_slug(const char* slug) {
if (!slug) return 0;
size_t len = strlen(slug);
static int validate_skill_d_tag(const char* d_tag) {
if (!d_tag) return 0;
size_t len = strlen(d_tag);
if (len == 0 || len > 64U) return 0;
if (slug[0] == '-' || slug[len - 1] == '-') return 0;
if (d_tag[0] == '-' || d_tag[len - 1] == '-') return 0;
int prev_dash = 0;
for (size_t i = 0; i < len; i++) {
unsigned char c = (unsigned char)slug[i];
unsigned char c = (unsigned char)d_tag[i];
if (c == '-') {
if (prev_dash) return 0;
prev_dash = 1;
@@ -986,7 +1009,7 @@ static cJSON* extract_skill_summary(cJSON* event) {
cJSON* description = find_tag_value_string(tags, "description");
if (d && cJSON_IsString(d) && d->valuestring) {
cJSON_AddStringToObject(summary, "slug", d->valuestring);
cJSON_AddStringToObject(summary, "d_tag", d->valuestring);
}
if (scope && cJSON_IsString(scope) && scope->valuestring) {
cJSON_AddStringToObject(summary, "scope", scope->valuestring);
@@ -1043,7 +1066,7 @@ char* tools_build_openai_schema_json(const tools_context_t* ctx) {
cJSON_AddItemToObject(t1_props, "content", p_content);
cJSON* p_tags = cJSON_CreateObject();
cJSON_AddStringToObject(p_tags, "type", "array");
cJSON_AddStringToObject(p_tags, "description", "Optional Nostr tags array, e.g. [[\"d\",\"slug\"],[\"t\",\"nostr\"]]");
cJSON_AddStringToObject(p_tags, "description", "Optional Nostr tags array, e.g. [[\"d\",\"my-skill\"],[\"t\",\"nostr\"]]");
cJSON* p_tags_items = cJSON_CreateObject();
cJSON_AddStringToObject(p_tags_items, "type", "array");
cJSON* p_tag_item = cJSON_CreateObject();
@@ -1617,9 +1640,9 @@ char* tools_build_openai_schema_json(const tools_context_t* ctx) {
cJSON_AddItemToObject(t22_params, "properties", t22_props);
cJSON_AddItemToObject(t22_params, "required", t22_required);
cJSON* p_skill_create_slug = cJSON_CreateObject();
cJSON_AddStringToObject(p_skill_create_slug, "type", "string");
cJSON_AddItemToObject(t22_props, "slug", p_skill_create_slug);
cJSON* p_skill_create_d = cJSON_CreateObject();
cJSON_AddStringToObject(p_skill_create_d, "type", "string");
cJSON_AddItemToObject(t22_props, "d", p_skill_create_d);
cJSON* p_skill_create_content = cJSON_CreateObject();
cJSON_AddStringToObject(p_skill_create_content, "type", "string");
cJSON_AddItemToObject(t22_props, "content", p_skill_create_content);
@@ -1649,7 +1672,7 @@ char* tools_build_openai_schema_json(const tools_context_t* ctx) {
cJSON_AddStringToObject(p_skill_create_enabled, "type", "boolean");
cJSON_AddItemToObject(t22_props, "enabled", p_skill_create_enabled);
cJSON_AddItemToArray(t22_required, cJSON_CreateString("slug"));
cJSON_AddItemToArray(t22_required, cJSON_CreateString("d"));
cJSON_AddItemToArray(t22_required, cJSON_CreateString("content"));
cJSON_AddItemToObject(t22_fn, "parameters", t22_params);
@@ -1691,15 +1714,15 @@ char* tools_build_openai_schema_json(const tools_context_t* ctx) {
cJSON* p_skill_adopt_pubkey = cJSON_CreateObject();
cJSON_AddStringToObject(p_skill_adopt_pubkey, "type", "string");
cJSON_AddItemToObject(t24_props, "pubkey", p_skill_adopt_pubkey);
cJSON* p_skill_adopt_slug = cJSON_CreateObject();
cJSON_AddStringToObject(p_skill_adopt_slug, "type", "string");
cJSON_AddItemToObject(t24_props, "slug", p_skill_adopt_slug);
cJSON* p_skill_adopt_d = cJSON_CreateObject();
cJSON_AddStringToObject(p_skill_adopt_d, "type", "string");
cJSON_AddItemToObject(t24_props, "d", p_skill_adopt_d);
cJSON* p_skill_adopt_kind = cJSON_CreateObject();
cJSON_AddStringToObject(p_skill_adopt_kind, "type", "integer");
cJSON_AddItemToObject(t24_props, "kind", p_skill_adopt_kind);
cJSON_AddItemToArray(t24_required, cJSON_CreateString("pubkey"));
cJSON_AddItemToArray(t24_required, cJSON_CreateString("slug"));
cJSON_AddItemToArray(t24_required, cJSON_CreateString("d"));
cJSON_AddItemToObject(t24_fn, "parameters", t24_params);
cJSON_AddItemToObject(t24, "function", t24_fn);
@@ -1721,19 +1744,70 @@ char* tools_build_openai_schema_json(const tools_context_t* ctx) {
cJSON* p_skill_remove_pubkey = cJSON_CreateObject();
cJSON_AddStringToObject(p_skill_remove_pubkey, "type", "string");
cJSON_AddItemToObject(t25_props, "pubkey", p_skill_remove_pubkey);
cJSON* p_skill_remove_slug = cJSON_CreateObject();
cJSON_AddStringToObject(p_skill_remove_slug, "type", "string");
cJSON_AddItemToObject(t25_props, "slug", p_skill_remove_slug);
cJSON* p_skill_remove_d = cJSON_CreateObject();
cJSON_AddStringToObject(p_skill_remove_d, "type", "string");
cJSON_AddItemToObject(t25_props, "d", p_skill_remove_d);
cJSON* p_skill_remove_kind = cJSON_CreateObject();
cJSON_AddStringToObject(p_skill_remove_kind, "type", "integer");
cJSON_AddItemToObject(t25_props, "kind", p_skill_remove_kind);
cJSON_AddItemToArray(t25_required, cJSON_CreateString("slug"));
cJSON_AddItemToArray(t25_required, cJSON_CreateString("d"));
cJSON_AddItemToObject(t25_fn, "parameters", t25_params);
cJSON_AddItemToObject(t25, "function", t25_fn);
cJSON_AddItemToArray(tools, t25);
cJSON* t25b = cJSON_CreateObject();
cJSON* t25b_fn = cJSON_CreateObject();
cJSON* t25b_params = cJSON_CreateObject();
cJSON* t25b_props = cJSON_CreateObject();
cJSON* t25b_required = cJSON_CreateArray();
cJSON_AddStringToObject(t25b, "type", "function");
cJSON_AddStringToObject(t25b_fn, "name", "skill_edit");
cJSON_AddStringToObject(t25b_fn, "description", "Edit an existing self skill by d tag and republish it as kind 31123/31124");
cJSON_AddStringToObject(t25b_params, "type", "object");
cJSON_AddItemToObject(t25b_params, "properties", t25b_props);
cJSON_AddItemToObject(t25b_params, "required", t25b_required);
cJSON* p_skill_edit_d = cJSON_CreateObject();
cJSON_AddStringToObject(p_skill_edit_d, "type", "string");
cJSON_AddItemToObject(t25b_props, "d", p_skill_edit_d);
cJSON* p_skill_edit_content = cJSON_CreateObject();
cJSON_AddStringToObject(p_skill_edit_content, "type", "string");
cJSON_AddItemToObject(t25b_props, "content", p_skill_edit_content);
cJSON* p_skill_edit_scope = cJSON_CreateObject();
cJSON_AddStringToObject(p_skill_edit_scope, "type", "string");
cJSON_AddItemToObject(t25b_props, "scope", p_skill_edit_scope);
cJSON* p_skill_edit_desc = cJSON_CreateObject();
cJSON_AddStringToObject(p_skill_edit_desc, "type", "string");
cJSON_AddItemToObject(t25b_props, "description", p_skill_edit_desc);
cJSON* p_skill_edit_trigger = cJSON_CreateObject();
cJSON_AddStringToObject(p_skill_edit_trigger, "type", "string");
cJSON_AddItemToObject(t25b_props, "trigger", p_skill_edit_trigger);
cJSON* p_skill_edit_filter = cJSON_CreateObject();
cJSON_AddStringToObject(p_skill_edit_filter, "type", "string");
cJSON_AddItemToObject(t25b_props, "filter", p_skill_edit_filter);
cJSON* p_skill_edit_action = cJSON_CreateObject();
cJSON_AddStringToObject(p_skill_edit_action, "type", "string");
cJSON_AddItemToObject(t25b_props, "action", p_skill_edit_action);
cJSON* p_skill_edit_enabled = cJSON_CreateObject();
cJSON_AddStringToObject(p_skill_edit_enabled, "type", "boolean");
cJSON_AddItemToObject(t25b_props, "enabled", p_skill_edit_enabled);
cJSON_AddItemToArray(t25b_required, cJSON_CreateString("d"));
cJSON_AddItemToObject(t25b_fn, "parameters", t25b_params);
cJSON_AddItemToObject(t25b, "function", t25b_fn);
cJSON_AddItemToArray(tools, t25b);
cJSON* t26 = cJSON_CreateObject();
cJSON* t26_fn = cJSON_CreateObject();
cJSON* t26_params = cJSON_CreateObject();
@@ -3435,7 +3509,7 @@ static char* execute_skill_create(tools_context_t* ctx, const char* args_json) {
cJSON* args = parse_tool_args_json(args_json);
if (!args) return json_error("invalid arguments JSON");
cJSON* slug = cJSON_GetObjectItemCaseSensitive(args, "slug");
cJSON* d_tag = cJSON_GetObjectItemCaseSensitive(args, "d");
cJSON* content = cJSON_GetObjectItemCaseSensitive(args, "content");
cJSON* scope = cJSON_GetObjectItemCaseSensitive(args, "scope");
cJSON* description = cJSON_GetObjectItemCaseSensitive(args, "description");
@@ -3445,14 +3519,14 @@ static char* execute_skill_create(tools_context_t* ctx, const char* args_json) {
cJSON* action = cJSON_GetObjectItemCaseSensitive(args, "action");
cJSON* enabled = cJSON_GetObjectItemCaseSensitive(args, "enabled");
if (!slug || !cJSON_IsString(slug) || !slug->valuestring ||
if (!d_tag || !cJSON_IsString(d_tag) || !d_tag->valuestring ||
!content || !cJSON_IsString(content) || !content->valuestring) {
cJSON_Delete(args);
return json_error("skill_create requires slug and content strings");
return json_error("skill_create requires d and content strings");
}
if (!validate_skill_slug(slug->valuestring)) {
if (!validate_skill_d_tag(d_tag->valuestring)) {
cJSON_Delete(args);
return json_error("skill_create slug must be lowercase letters/digits/hyphens (1-64 chars)");
return json_error("skill_create d must be lowercase letters/digits/hyphens (1-64 chars)");
}
if (scope && (!cJSON_IsString(scope) || !scope->valuestring)) {
cJSON_Delete(args);
@@ -3499,7 +3573,7 @@ static char* execute_skill_create(tools_context_t* ctx, const char* args_json) {
return json_error("failed to create skill tags");
}
if (add_string_tag(tags, "d", slug->valuestring) != 0 ||
if (add_string_tag(tags, "d", d_tag->valuestring) != 0 ||
add_string_tag(tags, "app", "didactyl") != 0 ||
add_string_tag(tags, "scope", scope_str) != 0) {
cJSON_Delete(tags);
@@ -3568,7 +3642,7 @@ static char* execute_skill_create(tools_context_t* ctx, const char* args_json) {
char* adoption_content = NULL;
if (fetch_adoption_list_tags(ctx, &adoption_tags, &adoption_content) == 0) {
char addr[256];
snprintf(addr, sizeof(addr), "31123:%s:%s", ctx->cfg->keys.public_key_hex, slug->valuestring);
snprintf(addr, sizeof(addr), "31123:%s:%s", ctx->cfg->keys.public_key_hex, d_tag->valuestring);
cJSON* tuple = cJSON_CreateArray();
if (tuple) {
@@ -3602,7 +3676,7 @@ static char* execute_skill_create(tools_context_t* ctx, const char* args_json) {
? TRIGGER_ACTION_TEMPLATE
: TRIGGER_ACTION_LLM;
if (trigger_manager_add(ctx->trigger_manager,
slug->valuestring,
d_tag->valuestring,
content->valuestring,
filter_str,
at,
@@ -3620,7 +3694,7 @@ static char* execute_skill_create(tools_context_t* ctx, const char* args_json) {
}
cJSON_AddBoolToObject(out, "success", skill_publish.success ? 1 : 0);
cJSON_AddStringToObject(out, "slug", slug->valuestring);
cJSON_AddStringToObject(out, "d_tag", d_tag->valuestring);
cJSON_AddNumberToObject(out, "kind", kind);
cJSON_AddStringToObject(out, "scope", scope_str);
cJSON_AddStringToObject(out, "skill_event_id", skill_publish.event_id);
@@ -3726,13 +3800,13 @@ static char* execute_skill_adopt(tools_context_t* ctx, const char* args_json) {
if (!args) return json_error("invalid arguments JSON");
cJSON* pubkey = cJSON_GetObjectItemCaseSensitive(args, "pubkey");
cJSON* slug = cJSON_GetObjectItemCaseSensitive(args, "slug");
cJSON* d_tag = cJSON_GetObjectItemCaseSensitive(args, "d");
cJSON* kind = cJSON_GetObjectItemCaseSensitive(args, "kind");
if (!pubkey || !cJSON_IsString(pubkey) || !pubkey->valuestring || !is_hex_string_len(pubkey->valuestring, 64U) ||
!slug || !cJSON_IsString(slug) || !slug->valuestring || !validate_skill_slug(slug->valuestring)) {
!d_tag || !cJSON_IsString(d_tag) || !d_tag->valuestring || !validate_skill_d_tag(d_tag->valuestring)) {
cJSON_Delete(args);
return json_error("skill_adopt requires pubkey (hex) and valid slug");
return json_error("skill_adopt requires pubkey (hex) and valid d tag");
}
int kind_val = (kind && cJSON_IsNumber(kind)) ? (int)kind->valuedouble : 31123;
@@ -3742,7 +3816,7 @@ static char* execute_skill_adopt(tools_context_t* ctx, const char* args_json) {
}
char addr[256];
snprintf(addr, sizeof(addr), "%d:%s:%s", kind_val, pubkey->valuestring, slug->valuestring);
snprintf(addr, sizeof(addr), "%d:%s:%s", kind_val, pubkey->valuestring, d_tag->valuestring);
cJSON* tags = NULL;
char* content = NULL;
@@ -3819,7 +3893,7 @@ static char* execute_skill_remove(tools_context_t* ctx, const char* args_json) {
if (!args) return json_error("invalid arguments JSON");
cJSON* pubkey = cJSON_GetObjectItemCaseSensitive(args, "pubkey");
cJSON* slug = cJSON_GetObjectItemCaseSensitive(args, "slug");
cJSON* d_tag = cJSON_GetObjectItemCaseSensitive(args, "d");
cJSON* kind = cJSON_GetObjectItemCaseSensitive(args, "kind");
const char* pubkey_str = ctx->cfg->keys.public_key_hex;
@@ -3831,9 +3905,9 @@ static char* execute_skill_remove(tools_context_t* ctx, const char* args_json) {
pubkey_str = pubkey->valuestring;
}
if (!slug || !cJSON_IsString(slug) || !slug->valuestring || !validate_skill_slug(slug->valuestring)) {
if (!d_tag || !cJSON_IsString(d_tag) || !d_tag->valuestring || !validate_skill_d_tag(d_tag->valuestring)) {
cJSON_Delete(args);
return json_error("skill_remove requires valid slug");
return json_error("skill_remove requires valid d tag");
}
int kind_val = (kind && cJSON_IsNumber(kind)) ? (int)kind->valuedouble : 31123;
@@ -3843,7 +3917,7 @@ static char* execute_skill_remove(tools_context_t* ctx, const char* args_json) {
}
char addr[256];
snprintf(addr, sizeof(addr), "%d:%s:%s", kind_val, pubkey_str, slug->valuestring);
snprintf(addr, sizeof(addr), "%d:%s:%s", kind_val, pubkey_str, d_tag->valuestring);
cJSON* tags = NULL;
char* content = NULL;
@@ -3878,7 +3952,7 @@ static char* execute_skill_remove(tools_context_t* ctx, const char* args_json) {
}
if (ctx->trigger_manager) {
(void)trigger_manager_remove(ctx->trigger_manager, slug->valuestring);
(void)trigger_manager_remove(ctx->trigger_manager, d_tag->valuestring);
}
cJSON* out = cJSON_CreateObject();
@@ -3909,6 +3983,276 @@ static char* execute_skill_remove(tools_context_t* ctx, const char* args_json) {
return json;
}
static char* execute_skill_edit(tools_context_t* ctx, const char* args_json) {
if (!ctx || !ctx->cfg) return json_error("tool context unavailable");
cJSON* args = parse_tool_args_json(args_json);
if (!args) return json_error("invalid arguments JSON");
cJSON* d = cJSON_GetObjectItemCaseSensitive(args, "d");
cJSON* content = cJSON_GetObjectItemCaseSensitive(args, "content");
cJSON* scope = cJSON_GetObjectItemCaseSensitive(args, "scope");
cJSON* description = cJSON_GetObjectItemCaseSensitive(args, "description");
cJSON* trigger = cJSON_GetObjectItemCaseSensitive(args, "trigger");
cJSON* filter = cJSON_GetObjectItemCaseSensitive(args, "filter");
cJSON* action = cJSON_GetObjectItemCaseSensitive(args, "action");
cJSON* enabled = cJSON_GetObjectItemCaseSensitive(args, "enabled");
if (!d || !cJSON_IsString(d) || !d->valuestring || !validate_skill_d_tag(d->valuestring)) {
cJSON_Delete(args);
return json_error("skill_edit requires valid d tag");
}
if (content && (!cJSON_IsString(content) || !content->valuestring)) {
cJSON_Delete(args);
return json_error("skill_edit content must be string when provided");
}
if (scope && (!cJSON_IsString(scope) || !scope->valuestring)) {
cJSON_Delete(args);
return json_error("skill_edit scope must be string when provided");
}
if (description && (!cJSON_IsString(description) || !description->valuestring)) {
cJSON_Delete(args);
return json_error("skill_edit description must be string when provided");
}
if (trigger && (!cJSON_IsString(trigger) || !trigger->valuestring)) {
cJSON_Delete(args);
return json_error("skill_edit trigger must be string when provided");
}
if (filter && (!cJSON_IsString(filter) || !filter->valuestring)) {
cJSON_Delete(args);
return json_error("skill_edit filter must be string when provided");
}
if (action && (!cJSON_IsString(action) || !action->valuestring)) {
cJSON_Delete(args);
return json_error("skill_edit action must be string when provided");
}
if (enabled && !cJSON_IsBool(enabled)) {
cJSON_Delete(args);
return json_error("skill_edit enabled must be boolean when provided");
}
char* events_json = nostr_handler_get_self_skill_events_json();
if (!events_json) {
cJSON_Delete(args);
return json_error("skill_edit cache unavailable");
}
cJSON* events = cJSON_Parse(events_json);
free(events_json);
if (!events || !cJSON_IsArray(events)) {
cJSON_Delete(args);
cJSON_Delete(events);
return json_error("skill_edit cache returned invalid JSON");
}
cJSON* target = NULL;
long long best_created_at = -1;
int event_count = cJSON_GetArraySize(events);
for (int i = 0; i < event_count; i++) {
cJSON* ev = cJSON_GetArrayItem(events, i);
if (!ev || !cJSON_IsObject(ev)) continue;
cJSON* kind = cJSON_GetObjectItemCaseSensitive(ev, "kind");
cJSON* tags = cJSON_GetObjectItemCaseSensitive(ev, "tags");
if (!kind || !cJSON_IsNumber(kind) || !tags || !cJSON_IsArray(tags)) continue;
int kind_val = (int)kind->valuedouble;
if (kind_val != 31123 && kind_val != 31124) continue;
cJSON* d_val = find_tag_value_string(tags, "d");
if (!d_val || !cJSON_IsString(d_val) || !d_val->valuestring) continue;
if (strcmp(d_val->valuestring, d->valuestring) != 0) continue;
cJSON* created_at = cJSON_GetObjectItemCaseSensitive(ev, "created_at");
long long created = (created_at && cJSON_IsNumber(created_at)) ? (long long)created_at->valuedouble : 0;
if (!target || created > best_created_at) {
target = ev;
best_created_at = created;
}
}
if (!target) {
cJSON_Delete(args);
cJSON_Delete(events);
return json_error("skill_edit could not find self skill with that d");
}
cJSON* target_kind = cJSON_GetObjectItemCaseSensitive(target, "kind");
cJSON* target_content = cJSON_GetObjectItemCaseSensitive(target, "content");
cJSON* target_tags = cJSON_GetObjectItemCaseSensitive(target, "tags");
if (!target_kind || !cJSON_IsNumber(target_kind) || !target_content || !cJSON_IsString(target_content) ||
!target_content->valuestring || !target_tags || !cJSON_IsArray(target_tags)) {
cJSON_Delete(args);
cJSON_Delete(events);
return json_error("skill_edit found malformed skill event");
}
int out_kind = (int)target_kind->valuedouble;
if (scope && scope->valuestring) {
if (strcmp(scope->valuestring, "public") == 0) {
out_kind = 31123;
} else if (strcmp(scope->valuestring, "private") == 0) {
out_kind = 31124;
} else {
cJSON_Delete(args);
cJSON_Delete(events);
return json_error("skill_edit scope must be public or private");
}
}
const char* out_content = (content && content->valuestring) ? content->valuestring : target_content->valuestring;
cJSON* tags_out = cJSON_Duplicate(target_tags, 1);
if (!tags_out || !cJSON_IsArray(tags_out)) {
cJSON_Delete(args);
cJSON_Delete(events);
cJSON_Delete(tags_out);
return json_error("skill_edit failed to duplicate existing tags");
}
if (upsert_string_tag(tags_out, "d", d->valuestring) != 0) {
cJSON_Delete(args);
cJSON_Delete(events);
cJSON_Delete(tags_out);
return json_error("skill_edit failed to set d tag");
}
if (upsert_string_tag(tags_out, "app", "didactyl") != 0) {
cJSON_Delete(args);
cJSON_Delete(events);
cJSON_Delete(tags_out);
return json_error("skill_edit failed to set app tag");
}
if (upsert_string_tag(tags_out, "scope", out_kind == 31123 ? "public" : "private") != 0) {
cJSON_Delete(args);
cJSON_Delete(events);
cJSON_Delete(tags_out);
return json_error("skill_edit failed to set scope tag");
}
if (description) {
if (description->valuestring[0] == '\0') {
remove_tag_key_all(tags_out, "description");
} else if (upsert_string_tag(tags_out, "description", description->valuestring) != 0) {
cJSON_Delete(args);
cJSON_Delete(events);
cJSON_Delete(tags_out);
return json_error("skill_edit failed to set description tag");
}
}
cJSON* existing_trigger = find_tag_value_string(tags_out, "trigger");
cJSON* existing_filter = find_tag_value_string(tags_out, "filter");
cJSON* existing_action = find_tag_value_string(tags_out, "action");
cJSON* existing_enabled = find_tag_value_string(tags_out, "enabled");
const char* merged_trigger = (trigger && trigger->valuestring) ? trigger->valuestring
: (existing_trigger && cJSON_IsString(existing_trigger) ? existing_trigger->valuestring : NULL);
const char* merged_filter = (filter && filter->valuestring) ? filter->valuestring
: (existing_filter && cJSON_IsString(existing_filter) ? existing_filter->valuestring : NULL);
const char* merged_action = (action && action->valuestring) ? action->valuestring
: (existing_action && cJSON_IsString(existing_action) && existing_action->valuestring[0] != '\0'
? existing_action->valuestring
: "llm");
int merged_enabled = (enabled && cJSON_IsBool(enabled)) ? (cJSON_IsTrue(enabled) ? 1 : 0)
: (existing_enabled && cJSON_IsString(existing_enabled) && existing_enabled->valuestring &&
strcmp(existing_enabled->valuestring, "false") == 0)
? 0
: 1;
int trigger_patch_requested = (trigger || filter || action || enabled) ? 1 : 0;
if (trigger_patch_requested || (merged_trigger || merged_filter)) {
if ((merged_trigger && !merged_filter) || (!merged_trigger && merged_filter)) {
cJSON_Delete(args);
cJSON_Delete(events);
cJSON_Delete(tags_out);
return json_error("skill_edit trigger and filter must both be set together");
}
remove_tag_key_all(tags_out, "trigger");
remove_tag_key_all(tags_out, "filter");
remove_tag_key_all(tags_out, "action");
remove_tag_key_all(tags_out, "enabled");
if (merged_trigger && merged_filter) {
if (strcmp(merged_trigger, "nostr-subscription") != 0) {
cJSON_Delete(args);
cJSON_Delete(events);
cJSON_Delete(tags_out);
return json_error("skill_edit trigger must be nostr-subscription");
}
if (add_string_tag(tags_out, "trigger", merged_trigger) != 0 ||
add_string_tag(tags_out, "filter", merged_filter) != 0 ||
add_string_tag(tags_out, "action", merged_action) != 0 ||
add_string_tag(tags_out, "enabled", merged_enabled ? "true" : "false") != 0) {
cJSON_Delete(args);
cJSON_Delete(events);
cJSON_Delete(tags_out);
return json_error("skill_edit failed to set trigger tags");
}
}
}
nostr_publish_result_t publish_result;
memset(&publish_result, 0, sizeof(publish_result));
int rc = nostr_handler_publish_kind_event(out_kind, out_content, tags_out, &publish_result);
cJSON_Delete(tags_out);
cJSON_Delete(events);
if (rc != 0) {
cJSON_Delete(args);
nostr_handler_publish_result_free(&publish_result);
return json_error("skill_edit failed to publish updated skill");
}
int trigger_registered = 0;
if (ctx->trigger_manager) {
if (merged_trigger && merged_filter && merged_enabled) {
trigger_action_type_t at = (strcmp(merged_action, "template") == 0)
? TRIGGER_ACTION_TEMPLATE
: TRIGGER_ACTION_LLM;
if (trigger_manager_update(ctx->trigger_manager,
d->valuestring,
out_content,
merged_filter,
at,
merged_enabled) == 0 ||
trigger_manager_add(ctx->trigger_manager,
d->valuestring,
out_content,
merged_filter,
at,
merged_enabled) == 0) {
trigger_registered = 1;
}
} else {
(void)trigger_manager_remove(ctx->trigger_manager, d->valuestring);
}
}
cJSON* out = cJSON_CreateObject();
if (!out) {
cJSON_Delete(args);
nostr_handler_publish_result_free(&publish_result);
return NULL;
}
cJSON_AddBoolToObject(out, "success", publish_result.success ? 1 : 0);
cJSON_AddStringToObject(out, "d_tag", d->valuestring);
cJSON_AddNumberToObject(out, "kind", out_kind);
cJSON_AddStringToObject(out, "scope", out_kind == 31123 ? "public" : "private");
cJSON_AddStringToObject(out, "skill_event_id", publish_result.event_id);
cJSON_AddBoolToObject(out, "trigger_registered", trigger_registered ? 1 : 0);
if (publish_result.naddr_uri[0] != '\0') {
cJSON_AddStringToObject(out, "naddr_uri", publish_result.naddr_uri);
}
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
cJSON_Delete(args);
nostr_handler_publish_result_free(&publish_result);
return json;
}
static char* execute_skill_search(const char* args_json) {
cJSON* args = parse_tool_args_json(args_json);
if (!args) return json_error("invalid arguments JSON");
@@ -4106,11 +4450,11 @@ static char* execute_skill_search(const char* args_json) {
if (!summary) continue;
if (query_str && query_str[0] != '\0') {
cJSON* slug = cJSON_GetObjectItemCaseSensitive(summary, "slug");
cJSON* d_tag = cJSON_GetObjectItemCaseSensitive(summary, "d_tag");
cJSON* desc = cJSON_GetObjectItemCaseSensitive(summary, "description");
cJSON* preview = cJSON_GetObjectItemCaseSensitive(summary, "content_preview");
int match = 0;
if (slug && cJSON_IsString(slug) && slug->valuestring && ci_contains(slug->valuestring, query_str)) match = 1;
if (d_tag && cJSON_IsString(d_tag) && d_tag->valuestring && ci_contains(d_tag->valuestring, query_str)) match = 1;
if (!match && desc && cJSON_IsString(desc) && desc->valuestring && ci_contains(desc->valuestring, query_str)) match = 1;
if (!match && preview && cJSON_IsString(preview) && preview->valuestring && ci_contains(preview->valuestring, query_str)) match = 1;
if (!match) {
@@ -5649,6 +5993,9 @@ char* tools_execute(tools_context_t* ctx, const char* tool_name, const char* arg
if (strcmp(tool_name, "skill_search") == 0) {
return execute_skill_search(args_json);
}
if (strcmp(tool_name, "skill_edit") == 0) {
return execute_skill_edit(ctx, args_json);
}
if (strcmp(tool_name, "trigger_list") == 0) {
return execute_trigger_list(ctx, args_json);
}

View File

@@ -14,7 +14,7 @@
typedef struct {
trigger_manager_t* mgr;
char skill_slug[TRIGGER_SKILL_SLUG_MAX];
char skill_d_tag[TRIGGER_SKILL_D_TAG_MAX];
} trigger_subscription_ctx_t;
static int clamp_enabled(int enabled) {
@@ -52,13 +52,13 @@ static int ensure_capacity(trigger_manager_t* mgr, int needed) {
return 0;
}
static int find_trigger_index_locked(trigger_manager_t* mgr, const char* skill_slug) {
if (!mgr || !skill_slug || skill_slug[0] == '\0') {
static int find_trigger_index_locked(trigger_manager_t* mgr, const char* skill_d_tag) {
if (!mgr || !skill_d_tag || skill_d_tag[0] == '\0') {
return -1;
}
for (int i = 0; i < mgr->count; i++) {
if (strncmp(mgr->triggers[i].skill_slug, skill_slug, TRIGGER_SKILL_SLUG_MAX) == 0) {
if (strncmp(mgr->triggers[i].skill_d_tag, skill_d_tag, TRIGGER_SKILL_D_TAG_MAX) == 0) {
return i;
}
}
@@ -85,8 +85,8 @@ static cJSON* find_tag_value_string(cJSON* tags, const char* key) {
return NULL;
}
static int parse_address_tag(const char* addr, int* out_kind, char out_pubkey[65], char out_slug[65]) {
if (!addr || !out_kind || !out_pubkey || !out_slug) {
static int parse_address_tag(const char* addr, int* out_kind, char out_pubkey[65], char out_d_tag[65]) {
if (!addr || !out_kind || !out_pubkey || !out_d_tag) {
return -1;
}
@@ -108,9 +108,9 @@ static int parse_address_tag(const char* addr, int* out_kind, char out_pubkey[65
memcpy(out_pubkey, p1 + 1, 64U);
out_pubkey[64] = '\0';
size_t slug_len = strlen(p2 + 1);
if (slug_len == 0 || slug_len >= 65U) return -1;
memcpy(out_slug, p2 + 1, slug_len + 1U);
size_t d_tag_len = strlen(p2 + 1);
if (d_tag_len == 0 || d_tag_len >= 65U) return -1;
memcpy(out_d_tag, p2 + 1, d_tag_len + 1U);
*out_kind = kind;
return 0;
@@ -173,7 +173,7 @@ static void execute_template_action(trigger_manager_t* mgr,
} else if (strncmp(rendered, "LOG:", 4) == 0) {
const char* body = rendered + 4;
while (*body == ' ') body++;
DEBUG_INFO("[didactyl] trigger template log (%s): %s", t->skill_slug, body);
DEBUG_INFO("[didactyl] trigger template log (%s): %s", t->skill_d_tag, body);
} else {
(void)nostr_handler_send_dm_auto(mgr->cfg->admin.pubkey, rendered);
}
@@ -185,7 +185,7 @@ static void execute_llm_action(const active_trigger_t* t, cJSON* event, const ch
if (!t || !event) {
return;
}
agent_on_trigger(t->skill_slug, t->skill_content, event, relay_url);
agent_on_trigger(t->skill_d_tag, t->skill_content, event, relay_url);
}
static int maybe_fire_trigger_locked(trigger_manager_t* mgr, int index, cJSON* event, const char* relay_url) {
@@ -256,7 +256,7 @@ static void on_trigger_subscription_event(cJSON* event, const char* relay_url, v
trigger_manager_t* mgr = ctx->mgr;
pthread_mutex_lock(&mgr->mutex);
int idx = find_trigger_index_locked(mgr, ctx->skill_slug);
int idx = find_trigger_index_locked(mgr, ctx->skill_d_tag);
if (idx >= 0) {
(void)maybe_fire_trigger_locked(mgr, idx, event, relay_url);
}
@@ -300,7 +300,7 @@ static int register_trigger_subscription_locked(trigger_manager_t* mgr, active_t
}
ctx->mgr = mgr;
snprintf(ctx->skill_slug, sizeof(ctx->skill_slug), "%s", t->skill_slug);
snprintf(ctx->skill_d_tag, sizeof(ctx->skill_d_tag), "%s", t->skill_d_tag);
nostr_pool_subscription_t* sub = nostr_handler_subscribe_with_filter(
filter,
@@ -392,8 +392,8 @@ int trigger_manager_load_from_skills(trigger_manager_t* mgr) {
int kind = 0;
char pubkey[65] = {0};
char slug[65] = {0};
if (parse_address_tag(val->valuestring, &kind, pubkey, slug) != 0) {
char d_tag[65] = {0};
if (parse_address_tag(val->valuestring, &kind, pubkey, d_tag) != 0) {
continue;
}
@@ -415,7 +415,7 @@ int trigger_manager_load_from_skills(trigger_manager_t* mgr) {
if (!ev_pubkey || !cJSON_IsString(ev_pubkey) || !ev_pubkey->valuestring ||
strcmp(ev_pubkey->valuestring, pubkey) != 0 ||
!ev_d || !cJSON_IsString(ev_d) || !ev_d->valuestring ||
strcmp(ev_d->valuestring, slug) != 0) {
strcmp(ev_d->valuestring, d_tag) != 0) {
continue;
}
cJSON* dup = cJSON_Duplicate(ev, 1);
@@ -444,7 +444,7 @@ int trigger_manager_load_from_skills(trigger_manager_t* mgr) {
cJSON_AddItemToObject(skill_filter, "kinds", sk_kinds);
cJSON_AddItemToArray(sk_authors, cJSON_CreateString(pubkey));
cJSON_AddItemToObject(skill_filter, "authors", sk_authors);
cJSON_AddItemToArray(d_values, cJSON_CreateString(slug));
cJSON_AddItemToArray(d_values, cJSON_CreateString(d_tag));
cJSON_AddItemToObject(skill_filter, "#d", d_values);
cJSON_AddNumberToObject(skill_filter, "limit", 1);
@@ -482,7 +482,7 @@ int trigger_manager_load_from_skills(trigger_manager_t* mgr) {
if (trigger_s && strcmp(trigger_s, "nostr-subscription") == 0 && filter_s && filter_s[0] != '\0') {
trigger_action_type_t at = (strcmp(action_s, "template") == 0) ? TRIGGER_ACTION_TEMPLATE : TRIGGER_ACTION_LLM;
int is_enabled = (strcmp(enabled_s, "false") == 0 || strcmp(enabled_s, "0") == 0) ? 0 : 1;
if (trigger_manager_add(mgr, slug, content->valuestring, filter_s, at, is_enabled) == 0) {
if (trigger_manager_add(mgr, d_tag, content->valuestring, filter_s, at, is_enabled) == 0) {
loaded++;
}
}
@@ -532,13 +532,13 @@ int trigger_manager_load_from_startup_events(trigger_manager_t* mgr) {
cJSON* action = find_tag_value_string(tags, "action");
cJSON* enabled = find_tag_value_string(tags, "enabled");
const char* slug = (d && cJSON_IsString(d) && d->valuestring) ? d->valuestring : NULL;
const char* d_tag = (d && cJSON_IsString(d) && d->valuestring) ? d->valuestring : NULL;
const char* trigger_s = (trigger && cJSON_IsString(trigger) && trigger->valuestring) ? trigger->valuestring : NULL;
const char* filter_s = (filter && cJSON_IsString(filter) && filter->valuestring) ? filter->valuestring : NULL;
const char* action_s = (action && cJSON_IsString(action) && action->valuestring) ? action->valuestring : "llm";
const char* enabled_s = (enabled && cJSON_IsString(enabled) && enabled->valuestring) ? enabled->valuestring : "true";
if (!slug || slug[0] == '\0') {
if (!d_tag || d_tag[0] == '\0') {
cJSON_Delete(tags);
DEBUG_WARN("[didactyl] startup trigger skip: missing d tag for startup event index=%d", i);
continue;
@@ -552,11 +552,11 @@ int trigger_manager_load_from_startup_events(trigger_manager_t* mgr) {
trigger_action_type_t at = (strcmp(action_s, "template") == 0) ? TRIGGER_ACTION_TEMPLATE : TRIGGER_ACTION_LLM;
int is_enabled = (strcmp(enabled_s, "false") == 0 || strcmp(enabled_s, "0") == 0) ? 0 : 1;
if (trigger_manager_add(mgr, slug, ev->content, filter_s, at, is_enabled) == 0) {
if (trigger_manager_add(mgr, d_tag, ev->content, filter_s, at, is_enabled) == 0) {
loaded++;
DEBUG_INFO("[didactyl] startup trigger registered slug=%s action=%s enabled=%d", slug, at == TRIGGER_ACTION_TEMPLATE ? "template" : "llm", is_enabled);
DEBUG_INFO("[didactyl] startup trigger registered d_tag=%s action=%s enabled=%d", d_tag, at == TRIGGER_ACTION_TEMPLATE ? "template" : "llm", is_enabled);
} else {
DEBUG_WARN("[didactyl] startup trigger failed slug=%s", slug);
DEBUG_WARN("[didactyl] startup trigger failed d_tag=%s", d_tag);
}
cJSON_Delete(tags);
@@ -567,12 +567,12 @@ int trigger_manager_load_from_startup_events(trigger_manager_t* mgr) {
}
int trigger_manager_add(trigger_manager_t* mgr,
const char* skill_slug,
const char* skill_d_tag,
const char* content,
const char* filter_json,
trigger_action_type_t action_type,
int enabled) {
if (!mgr || !skill_slug || skill_slug[0] == '\0' || !content || !filter_json) {
if (!mgr || !skill_d_tag || skill_d_tag[0] == '\0' || !content || !filter_json) {
return -1;
}
@@ -582,10 +582,10 @@ int trigger_manager_add(trigger_manager_t* mgr,
pthread_mutex_lock(&mgr->mutex);
int existing = find_trigger_index_locked(mgr, skill_slug);
int existing = find_trigger_index_locked(mgr, skill_d_tag);
if (existing >= 0) {
pthread_mutex_unlock(&mgr->mutex);
return trigger_manager_update(mgr, skill_slug, content, filter_json, action_type, enabled);
return trigger_manager_update(mgr, skill_d_tag, content, filter_json, action_type, enabled);
}
int max_active = mgr->cfg ? mgr->cfg->triggers.max_active : TRIGGER_DEFAULT_MAX_ACTIVE;
@@ -603,7 +603,7 @@ int trigger_manager_add(trigger_manager_t* mgr,
active_trigger_t* t = &mgr->triggers[mgr->count];
memset(t, 0, sizeof(*t));
snprintf(t->skill_slug, sizeof(t->skill_slug), "%s", skill_slug);
snprintf(t->skill_d_tag, sizeof(t->skill_d_tag), "%s", skill_d_tag);
snprintf(t->skill_content, sizeof(t->skill_content), "%s", content);
snprintf(t->filter_json, sizeof(t->filter_json), "%s", filter_json);
t->action_type = action_type;
@@ -615,7 +615,7 @@ int trigger_manager_add(trigger_manager_t* mgr,
if (register_trigger_subscription_locked(mgr, t) != 0) {
pthread_mutex_unlock(&mgr->mutex);
DEBUG_WARN("[didactyl] trigger add rejected: failed to create subscription slug=%s", skill_slug);
DEBUG_WARN("[didactyl] trigger add rejected: failed to create subscription d_tag=%s", skill_d_tag);
memset(t, 0, sizeof(*t));
return -1;
}
@@ -624,18 +624,18 @@ int trigger_manager_add(trigger_manager_t* mgr,
pthread_mutex_unlock(&mgr->mutex);
DEBUG_INFO("[didactyl] trigger added slug=%s action=%d enabled=%d", skill_slug, (int)action_type, clamp_enabled(enabled));
DEBUG_INFO("[didactyl] trigger added d_tag=%s action=%d enabled=%d", skill_d_tag, (int)action_type, clamp_enabled(enabled));
return 0;
}
int trigger_manager_remove(trigger_manager_t* mgr, const char* skill_slug) {
if (!mgr || !skill_slug || skill_slug[0] == '\0') {
int trigger_manager_remove(trigger_manager_t* mgr, const char* skill_d_tag) {
if (!mgr || !skill_d_tag || skill_d_tag[0] == '\0') {
return -1;
}
pthread_mutex_lock(&mgr->mutex);
int idx = find_trigger_index_locked(mgr, skill_slug);
int idx = find_trigger_index_locked(mgr, skill_d_tag);
if (idx < 0) {
pthread_mutex_unlock(&mgr->mutex);
return 0;
@@ -654,17 +654,17 @@ int trigger_manager_remove(trigger_manager_t* mgr, const char* skill_slug) {
pthread_mutex_unlock(&mgr->mutex);
DEBUG_INFO("[didactyl] trigger removed slug=%s", skill_slug);
DEBUG_INFO("[didactyl] trigger removed d_tag=%s", skill_d_tag);
return 0;
}
int trigger_manager_update(trigger_manager_t* mgr,
const char* skill_slug,
const char* skill_d_tag,
const char* content,
const char* filter_json,
trigger_action_type_t action_type,
int enabled) {
if (!mgr || !skill_slug || skill_slug[0] == '\0' || !content || !filter_json) {
if (!mgr || !skill_d_tag || skill_d_tag[0] == '\0' || !content || !filter_json) {
return -1;
}
@@ -674,10 +674,10 @@ int trigger_manager_update(trigger_manager_t* mgr,
pthread_mutex_lock(&mgr->mutex);
int idx = find_trigger_index_locked(mgr, skill_slug);
int idx = find_trigger_index_locked(mgr, skill_d_tag);
if (idx < 0) {
pthread_mutex_unlock(&mgr->mutex);
return trigger_manager_add(mgr, skill_slug, content, filter_json, action_type, enabled);
return trigger_manager_add(mgr, skill_d_tag, content, filter_json, action_type, enabled);
}
active_trigger_t* t = &mgr->triggers[idx];
@@ -688,13 +688,13 @@ int trigger_manager_update(trigger_manager_t* mgr,
if (register_trigger_subscription_locked(mgr, t) != 0) {
pthread_mutex_unlock(&mgr->mutex);
DEBUG_WARN("[didactyl] trigger update failed to (re)subscribe slug=%s", skill_slug);
DEBUG_WARN("[didactyl] trigger update failed to (re)subscribe d_tag=%s", skill_d_tag);
return -1;
}
pthread_mutex_unlock(&mgr->mutex);
DEBUG_INFO("[didactyl] trigger updated slug=%s action=%d enabled=%d", skill_slug, (int)action_type, clamp_enabled(enabled));
DEBUG_INFO("[didactyl] trigger updated d_tag=%s action=%d enabled=%d", skill_d_tag, (int)action_type, clamp_enabled(enabled));
return 0;
}
@@ -758,7 +758,7 @@ char* trigger_manager_status_json(trigger_manager_t* mgr) {
continue;
}
cJSON_AddStringToObject(item, "skill_slug", t->skill_slug);
cJSON_AddStringToObject(item, "skill_d_tag", t->skill_d_tag);
cJSON_AddStringToObject(item, "filter_json", t->filter_json);
cJSON_AddStringToObject(item, "action", t->action_type == TRIGGER_ACTION_TEMPLATE ? "template" : "llm");
cJSON_AddBoolToObject(item, "enabled", t->enabled ? 1 : 0);

View File

@@ -8,7 +8,7 @@
#include "../nostr_core_lib/nostr_core/nostr_core.h"
#define TRIGGER_DEFAULT_MAX_ACTIVE 16
#define TRIGGER_SKILL_SLUG_MAX 65
#define TRIGGER_SKILL_D_TAG_MAX 65
#define TRIGGER_SKILL_CONTENT_MAX 4096
#define TRIGGER_FILTER_JSON_MAX 2048
@@ -18,7 +18,7 @@ typedef enum {
} trigger_action_type_t;
typedef struct {
char skill_slug[TRIGGER_SKILL_SLUG_MAX];
char skill_d_tag[TRIGGER_SKILL_D_TAG_MAX];
char skill_content[TRIGGER_SKILL_CONTENT_MAX];
char filter_json[TRIGGER_FILTER_JSON_MAX];
trigger_action_type_t action_type;
@@ -42,14 +42,14 @@ int trigger_manager_init(trigger_manager_t* mgr, didactyl_config_t* cfg);
int trigger_manager_load_from_skills(trigger_manager_t* mgr);
int trigger_manager_load_from_startup_events(trigger_manager_t* mgr);
int trigger_manager_add(trigger_manager_t* mgr,
const char* skill_slug,
const char* skill_d_tag,
const char* content,
const char* filter_json,
trigger_action_type_t action_type,
int enabled);
int trigger_manager_remove(trigger_manager_t* mgr, const char* skill_slug);
int trigger_manager_remove(trigger_manager_t* mgr, const char* skill_d_tag);
int trigger_manager_update(trigger_manager_t* mgr,
const char* skill_slug,
const char* skill_d_tag,
const char* content,
const char* filter_json,
trigger_action_type_t action_type,