Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
421e1abb15 | ||
|
|
c93972019c | ||
|
|
f7dd1f98c6 | ||
|
|
93bfd0741d | ||
|
|
ffed72c9fd | ||
|
|
1b9ecb782c | ||
|
|
aec697ad7b | ||
|
|
6359681cb3 | ||
|
|
1f7bbc9597 | ||
|
|
90adb88a4a |
@@ -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.50
|
||||
## Current Status — v0.0.60
|
||||
|
||||
**Active build — this project is barely working. Experiment at your own risk.**
|
||||
|
||||
> Last release update: v0.0.50 — Rename tools to category-consistent names, add local_* prefixes, merge context tool naming, and keep message_current template-only
|
||||
> Last release update: v0.0.60 — Remove 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
|
||||
|
||||
@@ -213,6 +213,23 @@
|
||||
]
|
||||
},
|
||||
|
||||
// Kind 31123: Public triggered skill — cheerleader
|
||||
// Watches for admin kind-1 notes and sends an encouraging DM.
|
||||
{
|
||||
"kind": 31123,
|
||||
"content": "You are my personal cheerleader. When the admin posts a kind 1 note, read their note and send them a short DM that cheers them on, praises their effort, tells them they are good looking, and encourages them to keep going. Be warm, playful, and positive. Mention something specific from their note so it feels personal. Keep it to 2-3 sentences.",
|
||||
"tags": [
|
||||
["d", "cheerleader"],
|
||||
["app", "didactyl"],
|
||||
["scope", "public"],
|
||||
["description", "Cheer on admin whenever they post a kind 1 note"],
|
||||
["trigger", "nostr-subscription"],
|
||||
["filter", "{\"kinds\":[1],\"authors\":[\"REPLACE_WITH_ADMIN_HEX_PUBKEY\"]}"],
|
||||
["action", "llm"],
|
||||
["enabled", "true"]
|
||||
]
|
||||
},
|
||||
|
||||
// Kind 31124: Private skill — admin_ops
|
||||
// Private operational procedures (admin-only).
|
||||
{
|
||||
@@ -237,6 +254,7 @@
|
||||
"tags": [
|
||||
["a", "31123:55993e3db0ed7bf07395fd44c2d695c224d195553a1aff7320a18e41679d9c7c:long_form_note"],
|
||||
["a", "31123:55993e3db0ed7bf07395fd44c2d695c224d195553a1aff7320a18e41679d9c7c:post_readme_to_nostr"],
|
||||
["a", "31123:55993e3db0ed7bf07395fd44c2d695c224d195553a1aff7320a18e41679d9c7c:cheerleader"],
|
||||
["app", "didactyl"],
|
||||
["scope", "public"]
|
||||
]
|
||||
|
||||
+7336
-156
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -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 |
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
# Didactyl — Nostr Subscriptions
|
||||
|
||||
## Overview
|
||||
|
||||
Didactyl maintains persistent websocket subscriptions to Nostr relays for the lifetime of the process. Subscriptions are opened during startup and are **never closed** — the relay pool keeps them alive, automatically reconnecting and resubscribing when relays drop.
|
||||
|
||||
All subscriptions are created via `nostr_relay_pool_subscribe()` from `nostr_core_lib` and are sent to every relay in the configured relay list.
|
||||
|
||||
## Startup Sequence
|
||||
|
||||
The subscriptions are opened in a specific order during `main()` startup. The diagram below shows the full sequence:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[nostr_handler_init] --> B[Connect to all relays]
|
||||
B --> C[nostr_handler_reconcile_startup_events]
|
||||
C --> D[Publish startup events to relays]
|
||||
D --> E[trigger_manager_init]
|
||||
E --> F[trigger_manager_load_from_startup_events]
|
||||
F --> G["Subscribe: Admin Context"]
|
||||
G --> H["Subscribe: Self Skills"]
|
||||
H --> I[Send startup DM to admin]
|
||||
I --> J["Subscribe: DMs"]
|
||||
J --> K[Enter main poll loop]
|
||||
K --> L["Poll: nostr_handler_poll + trigger_manager_poll + http_api_poll"]
|
||||
|
||||
style F fill:#2a7,stroke:#333,color:#fff
|
||||
style G fill:#27a,stroke:#333,color:#fff
|
||||
style H fill:#27a,stroke:#333,color:#fff
|
||||
style J fill:#27a,stroke:#333,color:#fff
|
||||
```
|
||||
|
||||
## Subscription Categories
|
||||
|
||||
### 1. Admin Context Subscription
|
||||
|
||||
**Function:** `nostr_handler_subscribe_admin_context()` in `src/nostr_handler.c`
|
||||
**When:** During startup, before self-skill subscription
|
||||
**Condition:** Only if `admin_context.enabled` is true in config
|
||||
|
||||
This creates up to two persistent subscriptions for the admin's pubkey:
|
||||
|
||||
#### Profile Subscription
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Kinds** | 0 (profile), 3 (contacts), 10002 (relay list) — each configurable |
|
||||
| **Authors** | Admin pubkey |
|
||||
| **Limit** | 32 |
|
||||
| **Callback** | `on_admin_context_event` |
|
||||
| **Dedup** | Enabled |
|
||||
| **Close on EOSE** | No |
|
||||
|
||||
Tracks the admin's profile metadata, contact list (WoT), and relay preferences. Used to build agent context about who the admin is.
|
||||
|
||||
#### Kind 1 Notes Subscription
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Kinds** | 1 |
|
||||
| **Authors** | Admin pubkey |
|
||||
| **Limit** | Configurable via `kind_1_limit`, default 10, max 256 |
|
||||
| **Callback** | `on_admin_context_event` |
|
||||
| **Dedup** | Enabled |
|
||||
| **Close on EOSE** | No |
|
||||
| **Condition** | Only if `admin_context.track_kind_1` is true |
|
||||
|
||||
Tracks the admin's recent public notes. Used for agent context and as the event source for triggered skills that watch admin posts.
|
||||
|
||||
### 2. Self-Skill Subscription
|
||||
|
||||
**Function:** `nostr_handler_subscribe_self_skills()` in `src/nostr_handler.c`
|
||||
**When:** During startup, after admin context subscription
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Kinds** | 31123 (public skill), 31124 (private skill), 10123 (adoption list) |
|
||||
| **Authors** | Agent's own pubkey |
|
||||
| **Limit** | 300 |
|
||||
| **Callback** | `on_self_skill_event` |
|
||||
| **EOSE Callback** | `on_self_skill_eose` |
|
||||
| **Dedup** | Disabled (handles dedup internally via cache upsert) |
|
||||
| **Close on EOSE** | No |
|
||||
|
||||
This is the core skill awareness subscription. It serves three purposes:
|
||||
|
||||
1. **Cache population** — Every arriving event is stored in the in-memory self-skill cache via `self_skill_cache_upsert_event_locked()`, making skills available for LLM tool calls and context building.
|
||||
|
||||
2. **Live trigger registration** — When a kind 31123 or 31124 event arrives with `trigger=nostr-subscription` and a valid `filter` tag, `register_trigger_from_self_skill_event()` immediately calls `trigger_manager_add()` to create a persistent trigger subscription. This means skills published from any client are automatically activated without restart.
|
||||
|
||||
3. **Deferred bulk load** — After EOSE, the `on_self_skill_eose` callback fires `trigger_manager_load_from_skills()` as a one-time bulk scan of the adoption list. This catches any skills that were already cached before the per-event path was wired up.
|
||||
|
||||
### 3. DM Subscriptions
|
||||
|
||||
**Function:** `nostr_handler_subscribe_dms()` in `src/nostr_handler.c`
|
||||
**When:** During startup, after self-skill subscription and startup DM
|
||||
**Required:** Yes — startup fails if DM subscription cannot be created
|
||||
|
||||
Creates one or two subscriptions depending on the configured DM protocol:
|
||||
|
||||
#### NIP-04 DM Subscription
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Kinds** | 4 |
|
||||
| **#p** | Agent's own pubkey |
|
||||
| **Since** | Process start time |
|
||||
| **Limit** | 100 |
|
||||
| **Callback** | `on_event` (routes to `agent_on_message`) |
|
||||
| **Dedup** | Disabled (handled by `dm_id_seen_or_remember`) |
|
||||
| **Close on EOSE** | No |
|
||||
| **Condition** | `dm_protocol` is `nip04` or `both` |
|
||||
|
||||
#### NIP-17 DM Subscription
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Kinds** | 1059 (gift wrap) |
|
||||
| **#p** | Agent's own pubkey |
|
||||
| **Since** | Process start time |
|
||||
| **Limit** | 400 |
|
||||
| **Callback** | `on_event` (unwraps gift wrap, routes to `agent_on_message`) |
|
||||
| **Dedup** | Disabled (handled by `dm_id_seen_or_remember`) |
|
||||
| **Close on EOSE** | No |
|
||||
| **Condition** | `dm_protocol` is `nip17` or `both` |
|
||||
|
||||
### 4. Trigger Subscriptions
|
||||
|
||||
**Function:** `register_trigger_subscription_locked()` in `src/trigger_manager.c`
|
||||
**When:** Dynamically, whenever a trigger is registered via `trigger_manager_add()`
|
||||
**Created by:** `nostr_handler_subscribe_with_filter()` wrapper
|
||||
|
||||
Each active trigger gets its own persistent subscription based on the skill's `filter` tag:
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Filter** | Parsed from the skill's `filter` tag JSON |
|
||||
| **Since** | From filter, or defaults to `now - 30s` |
|
||||
| **Limit** | From filter, or defaults to 200 |
|
||||
| **Callback** | `on_trigger_subscription_event` |
|
||||
| **Dedup** | Enabled |
|
||||
| **Close on EOSE** | No |
|
||||
|
||||
When an event matches the filter, `maybe_fire_trigger_locked()` checks cooldown and dedup, then executes the trigger action (LLM or template).
|
||||
|
||||
Trigger subscriptions are created at three points:
|
||||
- **Startup config scan** — `trigger_manager_load_from_startup_events()` parses `startup_events[]` from config for skills with trigger tags
|
||||
- **Live self-skill event** — `register_trigger_from_self_skill_event()` in the self-skill subscription callback
|
||||
- **EOSE bulk load** — `trigger_manager_load_from_skills()` after self-skill EOSE
|
||||
- **Runtime tool call** — `skill_create` tool with trigger parameters
|
||||
|
||||
## Subscription Parameters
|
||||
|
||||
All subscriptions share these common pool parameters:
|
||||
|
||||
| Parameter | Value | Meaning |
|
||||
|-----------|-------|---------|
|
||||
| `close_on_eose` | 0 | Subscription stays open after initial EOSE |
|
||||
| `result_mode` | `NOSTR_POOL_EOSE_FULL_SET` | EOSE fires after all relays respond or timeout |
|
||||
| `relay_timeout_seconds` | 30 | Per-relay timeout for initial response |
|
||||
| `eose_timeout_seconds` | 120 | Overall EOSE timeout across all relays |
|
||||
|
||||
## Subscription Lifecycle
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
INIT["Process Start"] --> CONNECT["Connect Relays"]
|
||||
CONNECT --> SUB["Open Subscriptions"]
|
||||
SUB --> LIVE["Live Event Stream"]
|
||||
LIVE --> |"Relay disconnects"| RECON["Auto-Reconnect"]
|
||||
RECON --> |"Relay reconnects"| RESUB["Auto-Resubscribe"]
|
||||
RESUB --> LIVE
|
||||
LIVE --> |"SIGINT/SIGTERM"| SHUT["Shutdown"]
|
||||
SHUT --> CLOSE["Close All + Cleanup"]
|
||||
```
|
||||
|
||||
Subscriptions are never manually closed during normal operation. The relay pool handles reconnection and resubscription transparently. On shutdown, `trigger_manager_cleanup()` closes trigger subscriptions and `nostr_handler_cleanup()` destroys the pool.
|
||||
|
||||
## Summary Table
|
||||
|
||||
| Subscription | Kinds | Target | Persistent | Created At |
|
||||
|-------------|-------|--------|-----------|------------|
|
||||
| Admin Profile | 0, 3, 10002 | Admin pubkey | Yes | Startup |
|
||||
| Admin Notes | 1 | Admin pubkey | Yes | Startup |
|
||||
| Self Skills | 31123, 31124, 10123 | Own pubkey | Yes | Startup |
|
||||
| DMs NIP-04 | 4 | Own pubkey (#p) | Yes | Startup |
|
||||
| DMs NIP-17 | 1059 | Own pubkey (#p) | Yes | Startup |
|
||||
| Trigger N | Per skill filter | Varies | Yes | Dynamic |
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Skills](SKILLS.md) — Skill event format and trigger tags
|
||||
- [Tools](TOOLS.md) — `skill_create` tool with trigger parameters
|
||||
- [API](API.md) — `trigger_list` and `trigger_status` endpoints
|
||||
+3
-3
@@ -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 |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
# Cheerleader Triggered Skill — Implementation Plan
|
||||
|
||||
## Goal
|
||||
|
||||
Create the first triggered skill: whenever the admin posts a kind 1 note on Nostr, the agent reads the note content and sends a DM back to the admin cheering them on, praising them, and telling them how good looking they are.
|
||||
|
||||
---
|
||||
|
||||
## Current State
|
||||
|
||||
The entire triggered-skill infrastructure is **already built and functional**:
|
||||
|
||||
| Component | File | Status |
|
||||
|---|---|---|
|
||||
| Trigger manager (poll, cooldown, add/remove) | `src/trigger_manager.c` | ✅ Complete |
|
||||
| LLM-mediated trigger execution | `src/agent.c` `agent_on_trigger()` | ✅ Complete |
|
||||
| `skill_create` tool with trigger support | `src/tools.c` `execute_skill_create()` | ✅ Complete |
|
||||
| Trigger polling in main loop | `src/main.c` | ✅ Complete |
|
||||
| Config parsing for triggers section | `src/config.c` | ✅ Complete |
|
||||
| `trigger_list` tool | `src/tools.c` | ✅ Complete |
|
||||
|
||||
### Gap Found
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### 1. Fix `skill_create` Tool Schema
|
||||
|
||||
**File:** `src/tools.c` lines ~1607-1641
|
||||
|
||||
Add four new properties to the `skill_create` tool definition so the LLM can see and use them:
|
||||
|
||||
```c
|
||||
// After auto_adopt property (line ~1634):
|
||||
|
||||
cJSON* p_skill_create_trigger = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p_skill_create_trigger, "type", "string");
|
||||
cJSON_AddStringToObject(p_skill_create_trigger, "description",
|
||||
"Trigger type. Use nostr-subscription to activate on matching Nostr events");
|
||||
cJSON_AddItemToObject(t22_props, "trigger", p_skill_create_trigger);
|
||||
|
||||
cJSON* p_skill_create_filter = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p_skill_create_filter, "type", "string");
|
||||
cJSON_AddStringToObject(p_skill_create_filter, "description",
|
||||
"Nostr subscription filter JSON for the trigger, e.g. {\"kinds\":[1],\"authors\":[\"<hex>\"]}");
|
||||
cJSON_AddItemToObject(t22_props, "filter", p_skill_create_filter);
|
||||
|
||||
cJSON* p_skill_create_action = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p_skill_create_action, "type", "string");
|
||||
cJSON_AddStringToObject(p_skill_create_action, "description",
|
||||
"Action type: llm (default, full LLM reasoning) or template (fast interpolation)");
|
||||
cJSON_AddItemToObject(t22_props, "action", p_skill_create_action);
|
||||
|
||||
cJSON* p_skill_create_enabled = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p_skill_create_enabled, "type", "boolean");
|
||||
cJSON_AddStringToObject(p_skill_create_enabled, "description",
|
||||
"Whether the trigger is active. Default: true");
|
||||
cJSON_AddItemToObject(t22_props, "enabled", p_skill_create_enabled);
|
||||
```
|
||||
|
||||
This is ~20 lines of code. No changes needed to the execution function — it already handles all four parameters.
|
||||
|
||||
### 2. Add Cheerleader Skill as Startup Event
|
||||
|
||||
**File:** `config.jsonc.example`
|
||||
|
||||
Add a new kind 31123 skill event with trigger tags. Insert before the kind 10123 adoption list event:
|
||||
|
||||
```jsonc
|
||||
// Kind 31123: Public triggered skill — cheerleader
|
||||
// Watches for admin kind 1 notes and sends encouraging DMs.
|
||||
{
|
||||
"kind": 31123,
|
||||
"content": "You are the admin's biggest fan and personal cheerleader. When the admin posts a note on Nostr, read the note content carefully and send them a DM that:\n\n1. References what they actually wrote about\n2. Cheers them on enthusiastically\n3. Praises their intelligence and insight\n4. Tells them how good looking they are\n5. Encourages them to keep posting\n\nBe genuine, warm, and over-the-top positive. Use their name from the admin profile if available. Keep it to 2-3 sentences max.",
|
||||
"tags": [
|
||||
["d", "cheerleader"],
|
||||
["app", "didactyl"],
|
||||
["scope", "public"],
|
||||
["description", "Cheer on the admin whenever they post a kind 1 note"],
|
||||
["trigger", "nostr-subscription"],
|
||||
["filter", "{\"kinds\":[1],\"authors\":[\"ADMIN_PUBKEY_HEX\"]}"],
|
||||
["action", "llm"],
|
||||
["enabled", "true"]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
> **Note:** The `ADMIN_PUBKEY_HEX` placeholder in the filter must be replaced with the actual admin pubkey from the config. Since this is a static config example, we use a placeholder. At runtime, the user replaces it with their admin pubkey.
|
||||
|
||||
### 3. Add to Adoption List
|
||||
|
||||
**File:** `config.jsonc.example`
|
||||
|
||||
Add the cheerleader skill address to the kind 10123 adoption list tags:
|
||||
|
||||
```jsonc
|
||||
["a", "31123:55993e3db0ed7bf07395fd44c2d695c224d195553a1aff7320a18e41679d9c7c:cheerleader"]
|
||||
```
|
||||
|
||||
### 4. Verify Trigger Filter
|
||||
|
||||
The Nostr filter for this trigger:
|
||||
|
||||
```json
|
||||
{"kinds": [1], "authors": ["<admin_pubkey_hex>"]}
|
||||
```
|
||||
|
||||
This matches:
|
||||
- **Kind 1** — text notes only (not DMs, not reactions, not reposts)
|
||||
- **Authors** — only the admin's pubkey (not anyone else's notes)
|
||||
|
||||
The trigger manager polls every 10 seconds (`trigger_manager_poll` checks `now - last_poll_at < 10`), applies the `since` parameter to only fetch events newer than the last seen, and enforces the configured cooldown (default 60 seconds) between firings.
|
||||
|
||||
---
|
||||
|
||||
## Data Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Admin as Admin Client
|
||||
participant Relay as Nostr Relay
|
||||
participant TM as Trigger Manager
|
||||
participant Agent as agent_on_trigger
|
||||
participant LLM as LLM API
|
||||
|
||||
Admin->>Relay: Publish kind 1 note
|
||||
Note over TM: Poll every 10s
|
||||
TM->>Relay: Query filter: kinds=1, authors=admin
|
||||
Relay-->>TM: New event found
|
||||
TM->>TM: Check cooldown, check last_seen_created_at
|
||||
TM->>Agent: agent_on_trigger with skill content + event
|
||||
Agent->>Agent: Build system prompt with soul + skill instructions
|
||||
Agent->>LLM: llm_chat with system + triggering event JSON
|
||||
LLM-->>Agent: Cheerful response
|
||||
Agent->>Relay: DM to admin via nostr_handler_send_dm_auto
|
||||
Relay-->>Admin: Encrypted DM with encouragement
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File Changes Summary
|
||||
|
||||
| File | Change | Lines |
|
||||
|---|---|---|
|
||||
| `src/tools.c` | Add trigger/filter/action/enabled to skill_create schema | ~20 lines added |
|
||||
| `config.jsonc.example` | Add cheerleader skill startup event | ~20 lines added |
|
||||
| `config.jsonc.example` | Add cheerleader to adoption list | 1 line added |
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
1. Build with `make`
|
||||
2. Start the agent
|
||||
3. Verify trigger loads on startup via `trigger_list` tool or HTTP API `/status`
|
||||
4. Post a kind 1 note from the admin account
|
||||
5. Wait up to ~10 seconds for the poll cycle
|
||||
6. Receive a cheerful DM from the agent
|
||||
7. Verify cooldown works — posting again within 60s should not trigger a second DM
|
||||
@@ -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) │
|
||||
|
||||
@@ -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:
|
||||
|
||||
+30
-30
@@ -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()` (t22–t26)
|
||||
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 1–64 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
|
||||
|
||||
@@ -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],
|
||||
|
||||
+14
-14
@@ -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` |
|
||||
|
||||
@@ -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);
|
||||
|
||||
+162
-201
@@ -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);
|
||||
@@ -805,6 +805,8 @@ static char* format_context_payload_for_log(const char* context_payload) {
|
||||
}
|
||||
out[0] = '\0';
|
||||
size_t used = 0;
|
||||
char* conversation_role = NULL;
|
||||
char* conversation_content = NULL;
|
||||
|
||||
int n = cJSON_GetArraySize(root);
|
||||
(void)appendf(&out, &cap, &used, "Sections: %d\n\n", n);
|
||||
@@ -830,6 +832,14 @@ static char* format_context_payload_for_log(const char* context_payload) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (section && strcmp(section, "conversation") == 0) {
|
||||
free(conversation_role);
|
||||
free(conversation_content);
|
||||
conversation_role = strdup(role_s);
|
||||
conversation_content = strdup(content_s);
|
||||
continue;
|
||||
}
|
||||
|
||||
char* display_content = demote_markdown_headings_for_log(content_s);
|
||||
const char* safe_display_content = display_content ? display_content : content_s;
|
||||
|
||||
@@ -901,8 +911,19 @@ static char* format_context_payload_for_log(const char* context_payload) {
|
||||
}
|
||||
}
|
||||
|
||||
if (conversation_role && conversation_content) {
|
||||
(void)appendf(&out,
|
||||
&cap,
|
||||
&used,
|
||||
"# conversation | role=%s\n\n%s\n\n",
|
||||
conversation_role,
|
||||
conversation_content);
|
||||
}
|
||||
|
||||
free(chat);
|
||||
free(latest_admin_line);
|
||||
free(conversation_role);
|
||||
free(conversation_content);
|
||||
|
||||
cJSON_Delete(root);
|
||||
return out;
|
||||
@@ -1074,55 +1095,6 @@ void agent_append_context_log(const char* sender_pubkey_hex, const char* phase,
|
||||
append_context_log(sender_pubkey_hex, phase, context_payload);
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
time_t created_at;
|
||||
int role_is_user;
|
||||
char* content;
|
||||
} agent_history_item_t;
|
||||
|
||||
static int extract_first_p_tag_local(cJSON* tags, char out_pubkey_hex[65]) {
|
||||
if (!tags || !cJSON_IsArray(tags) || !out_pubkey_hex) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int n = cJSON_GetArraySize(tags);
|
||||
for (int i = 0; i < n; i++) {
|
||||
cJSON* tag = cJSON_GetArrayItem(tags, i);
|
||||
if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cJSON* key = cJSON_GetArrayItem(tag, 0);
|
||||
cJSON* val = cJSON_GetArrayItem(tag, 1);
|
||||
if (!key || !val || !cJSON_IsString(key) || !cJSON_IsString(val) || !val->valuestring) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcmp(key->valuestring, "p") == 0 && strlen(val->valuestring) == 64U) {
|
||||
memcpy(out_pubkey_hex, val->valuestring, 64U);
|
||||
out_pubkey_hex[64] = '\0';
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int history_item_cmp_created_at(const void* a, const void* b) {
|
||||
const agent_history_item_t* ia = (const agent_history_item_t*)a;
|
||||
const agent_history_item_t* ib = (const agent_history_item_t*)b;
|
||||
if (ia->created_at < ib->created_at) return -1;
|
||||
if (ia->created_at > ib->created_at) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void free_history_items(agent_history_item_t* items, int count) {
|
||||
if (!items) return;
|
||||
for (int i = 0; i < count; i++) {
|
||||
free(items[i].content);
|
||||
}
|
||||
free(items);
|
||||
}
|
||||
|
||||
static int append_startup_events_context(cJSON* messages) {
|
||||
if (!messages || !g_cfg || g_cfg->startup_event_count <= 0 || !g_cfg->startup_events) {
|
||||
@@ -1389,142 +1361,65 @@ static int append_recent_admin_dm_history(cJSON* messages, const char* current_m
|
||||
return -1;
|
||||
}
|
||||
|
||||
unsigned char admin_pubkey[32];
|
||||
if (nostr_hex_to_bytes(g_cfg->admin.pubkey, admin_pubkey, 32) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON* filter = cJSON_CreateObject();
|
||||
cJSON* kinds = cJSON_CreateArray();
|
||||
if (!filter || !kinds) {
|
||||
cJSON_Delete(filter);
|
||||
cJSON_Delete(kinds);
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(4));
|
||||
cJSON_AddItemToObject(filter, "kinds", kinds);
|
||||
cJSON_AddNumberToObject(filter, "limit", AGENT_HISTORY_QUERY_LIMIT * 4);
|
||||
|
||||
char* events_json = nostr_handler_query_json(filter, 5000);
|
||||
cJSON_Delete(filter);
|
||||
if (!events_json) {
|
||||
char* history_json = nostr_handler_get_dm_history_json(g_cfg->admin.pubkey, AGENT_HISTORY_TURNS);
|
||||
if (!history_json) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
cJSON* events = cJSON_Parse(events_json);
|
||||
free(events_json);
|
||||
if (!events || !cJSON_IsArray(events)) {
|
||||
cJSON_Delete(events);
|
||||
cJSON* items = cJSON_Parse(history_json);
|
||||
free(history_json);
|
||||
if (!items || !cJSON_IsArray(items)) {
|
||||
cJSON_Delete(items);
|
||||
return 0;
|
||||
}
|
||||
|
||||
agent_history_item_t* items = NULL;
|
||||
int item_count = 0;
|
||||
|
||||
int n = cJSON_GetArraySize(events);
|
||||
for (int i = 0; i < n; i++) {
|
||||
cJSON* ev = cJSON_GetArrayItem(events, i);
|
||||
cJSON* kind = ev ? cJSON_GetObjectItemCaseSensitive(ev, "kind") : NULL;
|
||||
cJSON* pubkey = ev ? cJSON_GetObjectItemCaseSensitive(ev, "pubkey") : NULL;
|
||||
cJSON* content = ev ? cJSON_GetObjectItemCaseSensitive(ev, "content") : NULL;
|
||||
cJSON* tags = ev ? cJSON_GetObjectItemCaseSensitive(ev, "tags") : NULL;
|
||||
cJSON* created_at = ev ? cJSON_GetObjectItemCaseSensitive(ev, "created_at") : NULL;
|
||||
|
||||
if (!kind || !pubkey || !content || !tags || !created_at ||
|
||||
!cJSON_IsNumber(kind) || !cJSON_IsString(pubkey) ||
|
||||
!cJSON_IsString(content) || !cJSON_IsArray(tags) || !cJSON_IsNumber(created_at) ||
|
||||
(int)kind->valuedouble != 4 || !pubkey->valuestring || !content->valuestring) {
|
||||
continue;
|
||||
}
|
||||
|
||||
char p_tag_pubkey[65] = {0};
|
||||
if (extract_first_p_tag_local(tags, p_tag_pubkey) != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int role_is_user = 0;
|
||||
if (strcmp(pubkey->valuestring, g_cfg->admin.pubkey) == 0 &&
|
||||
strcmp(p_tag_pubkey, g_cfg->keys.public_key_hex) == 0) {
|
||||
role_is_user = 1;
|
||||
} else if (strcmp(pubkey->valuestring, g_cfg->keys.public_key_hex) == 0 &&
|
||||
strcmp(p_tag_pubkey, g_cfg->admin.pubkey) == 0) {
|
||||
role_is_user = 0;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
char* plaintext = (char*)malloc(NOSTR_NIP04_MAX_PLAINTEXT_SIZE);
|
||||
if (!plaintext) {
|
||||
continue;
|
||||
}
|
||||
plaintext[0] = '\0';
|
||||
|
||||
if (nostr_nip04_decrypt(g_cfg->keys.private_key,
|
||||
admin_pubkey,
|
||||
content->valuestring,
|
||||
plaintext,
|
||||
NOSTR_NIP04_MAX_PLAINTEXT_SIZE) != NOSTR_SUCCESS) {
|
||||
free(plaintext);
|
||||
continue;
|
||||
}
|
||||
|
||||
agent_history_item_t* grown = (agent_history_item_t*)realloc(items, (size_t)(item_count + 1) * sizeof(agent_history_item_t));
|
||||
if (!grown) {
|
||||
free(plaintext);
|
||||
free_history_items(items, item_count);
|
||||
cJSON_Delete(events);
|
||||
return -1;
|
||||
}
|
||||
|
||||
items = grown;
|
||||
items[item_count].created_at = (time_t)created_at->valuedouble;
|
||||
items[item_count].role_is_user = role_is_user;
|
||||
items[item_count].content = plaintext;
|
||||
item_count++;
|
||||
}
|
||||
|
||||
cJSON_Delete(events);
|
||||
|
||||
if (item_count > 1) {
|
||||
qsort(items, (size_t)item_count, sizeof(agent_history_item_t), history_item_cmp_created_at);
|
||||
}
|
||||
|
||||
int start = item_count > AGENT_HISTORY_TURNS ? item_count - AGENT_HISTORY_TURNS : 0;
|
||||
const char* last_appended_role = NULL;
|
||||
const char* last_appended_content = NULL;
|
||||
for (int i = start; i < item_count; i++) {
|
||||
if (i == item_count - 1 &&
|
||||
items[i].role_is_user &&
|
||||
current_message &&
|
||||
strcmp(items[i].content, current_message) == 0) {
|
||||
|
||||
int n = cJSON_GetArraySize(items);
|
||||
for (int i = 0; i < n; i++) {
|
||||
cJSON* item = cJSON_GetArrayItem(items, i);
|
||||
cJSON* role = item ? cJSON_GetObjectItemCaseSensitive(item, "role") : NULL;
|
||||
cJSON* content = item ? cJSON_GetObjectItemCaseSensitive(item, "content") : NULL;
|
||||
cJSON* created_at = item ? cJSON_GetObjectItemCaseSensitive(item, "created_at") : NULL;
|
||||
|
||||
if (!role || !content || !cJSON_IsString(role) || !cJSON_IsString(content) ||
|
||||
!role->valuestring || !content->valuestring) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const char* role = items[i].role_is_user ? "user" : "assistant";
|
||||
const char* content = items[i].content ? items[i].content : "";
|
||||
const char* role_s = role->valuestring;
|
||||
const char* content_s = content->valuestring;
|
||||
int role_is_user = (strcmp(role_s, "user") == 0);
|
||||
if (!role_is_user && strcmp(role_s, "assistant") != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (i == n - 1 && role_is_user && current_message && strcmp(content_s, current_message) == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (last_appended_role && last_appended_content &&
|
||||
strcmp(last_appended_role, role) == 0 &&
|
||||
strcmp(last_appended_content, content) == 0) {
|
||||
strcmp(last_appended_role, role_s) == 0 &&
|
||||
strcmp(last_appended_content, content_s) == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (append_simple_message(messages, role, content) != 0) {
|
||||
free_history_items(items, item_count);
|
||||
if (append_simple_message(messages, role_s, content_s) != 0) {
|
||||
cJSON_Delete(items);
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON* appended = cJSON_GetArrayItem(messages, cJSON_GetArraySize(messages) - 1);
|
||||
if (appended && cJSON_IsObject(appended)) {
|
||||
cJSON_AddNumberToObject(appended, "_ts", (double)items[i].created_at);
|
||||
if (appended && cJSON_IsObject(appended) && created_at && cJSON_IsNumber(created_at)) {
|
||||
cJSON_AddNumberToObject(appended, "_ts", created_at->valuedouble);
|
||||
}
|
||||
|
||||
last_appended_role = role;
|
||||
last_appended_content = content;
|
||||
last_appended_role = role_s;
|
||||
last_appended_content = content_s;
|
||||
}
|
||||
|
||||
free_history_items(items, item_count);
|
||||
cJSON_Delete(items);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1554,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;
|
||||
}
|
||||
|
||||
@@ -1583,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;
|
||||
@@ -1599,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;
|
||||
}
|
||||
@@ -1648,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;
|
||||
}
|
||||
|
||||
@@ -1671,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);
|
||||
@@ -1700,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);
|
||||
|
||||
@@ -1737,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++;
|
||||
@@ -1757,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;
|
||||
@@ -1777,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++;
|
||||
@@ -1908,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;
|
||||
}
|
||||
@@ -2015,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;
|
||||
}
|
||||
|
||||
@@ -2036,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);
|
||||
@@ -2052,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);
|
||||
|
||||
@@ -2067,20 +1962,86 @@ void agent_on_trigger(const char* skill_slug,
|
||||
snprintf(user_prompt, user_len, "Triggering event JSON:\n%s", event_json);
|
||||
free(event_json);
|
||||
|
||||
append_context_log(g_cfg->admin.pubkey, "llm_trigger", user_prompt);
|
||||
|
||||
char* response = llm_chat(system_prompt, user_prompt);
|
||||
free(system_prompt);
|
||||
free(user_prompt);
|
||||
|
||||
if (!response) {
|
||||
const char* fallback = "Triggered skill execution failed: LLM unavailable.";
|
||||
(void)nostr_handler_send_dm_auto(g_cfg->admin.pubkey, fallback);
|
||||
char* tools_json = tools_build_openai_schema_json(&g_tools_ctx);
|
||||
if (!tools_json) {
|
||||
free(system_prompt);
|
||||
free(user_prompt);
|
||||
(void)nostr_handler_send_dm_auto(g_cfg->admin.pubkey, "Triggered skill execution failed: tools unavailable.");
|
||||
return;
|
||||
}
|
||||
|
||||
(void)nostr_handler_send_dm_auto(g_cfg->admin.pubkey, response);
|
||||
free(response);
|
||||
cJSON* messages = cJSON_CreateArray();
|
||||
if (!messages || !cJSON_IsArray(messages) ||
|
||||
append_simple_message(messages, "system", system_prompt) != 0 ||
|
||||
append_simple_message(messages, "user", user_prompt) != 0) {
|
||||
cJSON_Delete(messages);
|
||||
free(tools_json);
|
||||
free(system_prompt);
|
||||
free(user_prompt);
|
||||
(void)nostr_handler_send_dm_auto(g_cfg->admin.pubkey, "Triggered skill execution failed: prompt initialization failed.");
|
||||
return;
|
||||
}
|
||||
|
||||
append_context_log(g_cfg->admin.pubkey, "llm_trigger", user_prompt);
|
||||
free(system_prompt);
|
||||
free(user_prompt);
|
||||
|
||||
int max_turns = g_cfg->tools.max_turns > 0 ? g_cfg->tools.max_turns : 6;
|
||||
|
||||
for (int turn = 0; turn < max_turns; turn++) {
|
||||
char* messages_json = cJSON_PrintUnformatted(messages);
|
||||
if (!messages_json) {
|
||||
break;
|
||||
}
|
||||
|
||||
append_context_log(g_cfg->admin.pubkey, "llm_trigger_with_tools_messages", messages_json);
|
||||
|
||||
llm_response_t resp;
|
||||
int rc = llm_chat_with_tools_messages(messages_json, tools_json, "auto", &resp);
|
||||
free(messages_json);
|
||||
if (rc != 0) {
|
||||
(void)nostr_handler_send_dm_auto(g_cfg->admin.pubkey, "Triggered skill execution failed: LLM request failed.");
|
||||
cJSON_Delete(messages);
|
||||
free(tools_json);
|
||||
return;
|
||||
}
|
||||
|
||||
if (resp.tool_call_count <= 0) {
|
||||
llm_response_free(&resp);
|
||||
break;
|
||||
}
|
||||
|
||||
if (append_assistant_tool_calls_message(messages, &resp) != 0) {
|
||||
llm_response_free(&resp);
|
||||
break;
|
||||
}
|
||||
|
||||
for (int i = 0; i < resp.tool_call_count; i++) {
|
||||
llm_tool_call_t* tc = &resp.tool_calls[i];
|
||||
char* tool_result = tools_execute(&g_tools_ctx, tc->name, tc->arguments_json);
|
||||
if (!tool_result) {
|
||||
tool_result = strdup("{\"success\":false,\"error\":\"tool execution failed\"}");
|
||||
}
|
||||
|
||||
if (append_tool_result_message(messages,
|
||||
tc->id ? tc->id : "",
|
||||
tool_result ? tool_result : "{\"success\":false,\"error\":\"tool execution failed\"}") != 0) {
|
||||
free(tool_result);
|
||||
llm_response_free(&resp);
|
||||
cJSON_Delete(messages);
|
||||
free(tools_json);
|
||||
(void)nostr_handler_send_dm_auto(g_cfg->admin.pubkey, "Triggered skill execution failed: tool result append failed.");
|
||||
return;
|
||||
}
|
||||
|
||||
free(tool_result);
|
||||
}
|
||||
|
||||
llm_response_free(&resp);
|
||||
}
|
||||
|
||||
cJSON_Delete(messages);
|
||||
free(tools_json);
|
||||
}
|
||||
|
||||
int agent_build_admin_messages_json(const char* current_user_message,
|
||||
|
||||
+1
-1
@@ -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);
|
||||
|
||||
+5
-9
@@ -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) {
|
||||
|
||||
+4
-4
@@ -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);
|
||||
|
||||
+44
-2
@@ -20,6 +20,37 @@
|
||||
|
||||
static volatile sig_atomic_t g_running = 1;
|
||||
|
||||
typedef struct {
|
||||
trigger_manager_t* trigger_manager;
|
||||
int loaded_once;
|
||||
} deferred_trigger_load_ctx_t;
|
||||
|
||||
static void on_self_skill_eose_load_triggers(int event_count, void* user_data) {
|
||||
deferred_trigger_load_ctx_t* ctx = (deferred_trigger_load_ctx_t*)user_data;
|
||||
if (!ctx || !ctx->trigger_manager) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ctx->loaded_once) {
|
||||
DEBUG_TRACE("[didactyl] deferred trigger load skipped (already loaded once) event_count=%d", event_count);
|
||||
return;
|
||||
}
|
||||
|
||||
DEBUG_INFO("[didactyl] deferred trigger load begin after self-skill EOSE (event_count=%d)", event_count);
|
||||
if (trigger_manager_load_from_skills(ctx->trigger_manager) != 0) {
|
||||
DEBUG_WARN("[didactyl] deferred trigger load failed after self-skill EOSE");
|
||||
return;
|
||||
}
|
||||
|
||||
ctx->loaded_once = 1;
|
||||
|
||||
char* status = trigger_manager_status_json(ctx->trigger_manager);
|
||||
if (status) {
|
||||
DEBUG_INFO("[didactyl] deferred trigger load status: %s", status);
|
||||
free(status);
|
||||
}
|
||||
}
|
||||
|
||||
static void signal_handler(int signum) {
|
||||
(void)signum;
|
||||
g_running = 0;
|
||||
@@ -231,10 +262,21 @@ int main(int argc, char** argv) {
|
||||
return 1;
|
||||
}
|
||||
agent_set_trigger_manager(&trigger_manager);
|
||||
nostr_handler_set_trigger_manager(&trigger_manager);
|
||||
|
||||
if (trigger_manager_load_from_skills(&trigger_manager) != 0) {
|
||||
DEBUG_WARN("[didactyl] startup phase: trigger manager load from skills failed (continuing)");
|
||||
deferred_trigger_load_ctx_t deferred_trigger_ctx = {
|
||||
.trigger_manager = &trigger_manager,
|
||||
.loaded_once = 0
|
||||
};
|
||||
nostr_handler_set_self_skill_eose_callback(on_self_skill_eose_load_triggers, &deferred_trigger_ctx);
|
||||
|
||||
DEBUG_INFO("[didactyl] startup phase: deferred trigger load will run after self-skill EOSE");
|
||||
|
||||
DEBUG_INFO("[didactyl] startup phase: startup-config trigger load begin");
|
||||
if (trigger_manager_load_from_startup_events(&trigger_manager) != 0) {
|
||||
DEBUG_WARN("[didactyl] startup phase: startup-config trigger load failed (continuing)");
|
||||
}
|
||||
DEBUG_INFO("[didactyl] startup phase: startup-config trigger load end");
|
||||
|
||||
DEBUG_INFO("[didactyl] startup phase: subscribe admin context begin");
|
||||
if (nostr_handler_subscribe_admin_context() != 0) {
|
||||
|
||||
+2
-2
@@ -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 50
|
||||
#define DIDACTYL_VERSION "v0.0.50"
|
||||
#define DIDACTYL_VERSION_PATCH 60
|
||||
#define DIDACTYL_VERSION "v0.0.60"
|
||||
|
||||
// Agent metadata
|
||||
#define DIDACTYL_NAME "Didactyl"
|
||||
|
||||
+280
-1
@@ -11,6 +11,7 @@
|
||||
#include "../../nostr_core_lib/cjson/cJSON.h"
|
||||
#include "../../nostr_core_lib/nostr_core/nostr_core.h"
|
||||
#include "debug.h"
|
||||
#include "trigger_manager.h"
|
||||
|
||||
#define NIP17_MAX_RELAYS 32
|
||||
#define NIP17_MAX_GIFT_WRAPS 8
|
||||
@@ -48,6 +49,11 @@ static pthread_mutex_t g_self_skill_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
#define DM_DEDUP_CACHE_SIZE 256
|
||||
|
||||
static nostr_self_skill_eose_cb_t g_self_skill_eose_cb = NULL;
|
||||
static void* g_self_skill_eose_user_data = NULL;
|
||||
|
||||
static struct trigger_manager* g_trigger_manager = NULL;
|
||||
|
||||
static char g_seen_dm_ids[DM_DEDUP_CACHE_SIZE][65];
|
||||
static int g_seen_dm_count = 0;
|
||||
static int g_seen_dm_next = 0;
|
||||
@@ -55,6 +61,7 @@ static int g_seen_dm_next = 0;
|
||||
static pthread_mutex_t g_dm_dedup_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
#define SENDER_PROTOCOL_CACHE_SIZE 128
|
||||
#define DM_HISTORY_RING_SIZE 256
|
||||
|
||||
typedef struct {
|
||||
char pubkey_hex[65];
|
||||
@@ -62,9 +69,21 @@ typedef struct {
|
||||
time_t seen_at;
|
||||
} sender_protocol_entry_t;
|
||||
|
||||
typedef struct {
|
||||
time_t created_at;
|
||||
int incoming;
|
||||
char peer_pubkey_hex[65];
|
||||
char* content;
|
||||
} dm_history_entry_t;
|
||||
|
||||
static sender_protocol_entry_t g_sender_protocol_cache[SENDER_PROTOCOL_CACHE_SIZE];
|
||||
static pthread_mutex_t g_sender_protocol_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
static dm_history_entry_t g_dm_history_ring[DM_HISTORY_RING_SIZE];
|
||||
static int g_dm_history_count = 0;
|
||||
static int g_dm_history_next = 0;
|
||||
static pthread_mutex_t g_dm_history_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
static int dm_id_seen_or_remember(const char* event_id_hex) {
|
||||
if (!event_id_hex || strlen(event_id_hex) != 64U) {
|
||||
return 0;
|
||||
@@ -180,6 +199,49 @@ static dm_protocol_t sender_protocol_lookup(const char* sender_pubkey_hex) {
|
||||
return out;
|
||||
}
|
||||
|
||||
static void dm_history_clear_locked(void) {
|
||||
for (int i = 0; i < DM_HISTORY_RING_SIZE; i++) {
|
||||
free(g_dm_history_ring[i].content);
|
||||
g_dm_history_ring[i].content = NULL;
|
||||
g_dm_history_ring[i].created_at = 0;
|
||||
g_dm_history_ring[i].incoming = 0;
|
||||
g_dm_history_ring[i].peer_pubkey_hex[0] = '\0';
|
||||
}
|
||||
g_dm_history_count = 0;
|
||||
g_dm_history_next = 0;
|
||||
}
|
||||
|
||||
static void dm_history_remember(const char* peer_pubkey_hex, const char* content, int incoming, time_t created_at) {
|
||||
if (!peer_pubkey_hex || strlen(peer_pubkey_hex) != 64U || !content) {
|
||||
return;
|
||||
}
|
||||
|
||||
char* dup = strdup(content);
|
||||
if (!dup) {
|
||||
return;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&g_dm_history_mutex);
|
||||
|
||||
int slot = 0;
|
||||
if (g_dm_history_count < DM_HISTORY_RING_SIZE) {
|
||||
slot = g_dm_history_count;
|
||||
g_dm_history_count++;
|
||||
} else {
|
||||
slot = g_dm_history_next;
|
||||
g_dm_history_next = (g_dm_history_next + 1) % DM_HISTORY_RING_SIZE;
|
||||
}
|
||||
|
||||
free(g_dm_history_ring[slot].content);
|
||||
g_dm_history_ring[slot].content = dup;
|
||||
g_dm_history_ring[slot].created_at = (created_at > 0) ? created_at : time(NULL);
|
||||
g_dm_history_ring[slot].incoming = incoming ? 1 : 0;
|
||||
memcpy(g_dm_history_ring[slot].peer_pubkey_hex, peer_pubkey_hex, 64U);
|
||||
g_dm_history_ring[slot].peer_pubkey_hex[64] = '\0';
|
||||
|
||||
pthread_mutex_unlock(&g_dm_history_mutex);
|
||||
}
|
||||
|
||||
static const char* relay_status_str(nostr_pool_relay_status_t status) {
|
||||
switch (status) {
|
||||
case NOSTR_POOL_RELAY_DISCONNECTED:
|
||||
@@ -210,7 +272,10 @@ static void upsert_kind1_note(time_t created_at, const char* content);
|
||||
static int startup_self_kind1_exists(void);
|
||||
static void load_startup_display_name(void);
|
||||
static void build_startup_kind1_content(char* out, size_t out_size, const char* fallback);
|
||||
static void register_trigger_from_self_skill_event(cJSON* event);
|
||||
static void on_self_skill_event(cJSON* event, const char* relay_url, void* user_data);
|
||||
static void dm_history_clear_locked(void);
|
||||
static void dm_history_remember(const char* peer_pubkey_hex, const char* content, int incoming, time_t created_at);
|
||||
|
||||
static void log_relay_state_changes(void) {
|
||||
if (!g_pool || !g_cfg || !g_last_relay_statuses) {
|
||||
@@ -384,6 +449,72 @@ static const char* find_tag_value_local(cJSON* tags, const char* key) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int parse_enabled_flag_local(const char* enabled_s) {
|
||||
if (!enabled_s || enabled_s[0] == '\0') {
|
||||
return 1;
|
||||
}
|
||||
return (strcmp(enabled_s, "false") == 0 || strcmp(enabled_s, "0") == 0) ? 0 : 1;
|
||||
}
|
||||
|
||||
static void register_trigger_from_self_skill_event(cJSON* event) {
|
||||
if (!event || !g_cfg || !g_trigger_manager) {
|
||||
return;
|
||||
}
|
||||
|
||||
cJSON* kind = cJSON_GetObjectItemCaseSensitive(event, "kind");
|
||||
cJSON* pubkey = cJSON_GetObjectItemCaseSensitive(event, "pubkey");
|
||||
cJSON* content = cJSON_GetObjectItemCaseSensitive(event, "content");
|
||||
cJSON* tags = cJSON_GetObjectItemCaseSensitive(event, "tags");
|
||||
|
||||
if (!kind || !cJSON_IsNumber(kind) ||
|
||||
!pubkey || !cJSON_IsString(pubkey) || !pubkey->valuestring ||
|
||||
!content || !cJSON_IsString(content) || !content->valuestring ||
|
||||
!tags || !cJSON_IsArray(tags)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int kind_val = (int)kind->valuedouble;
|
||||
if (kind_val != 31123 && kind_val != 31124) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (strcmp(pubkey->valuestring, g_cfg->keys.public_key_hex) != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
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 (!d_tag || d_tag[0] == '\0' ||
|
||||
!trigger || strcmp(trigger, "nostr-subscription") != 0 ||
|
||||
!filter || filter[0] == '\0') {
|
||||
return;
|
||||
}
|
||||
|
||||
trigger_action_type_t action_type = (action && strcmp(action, "template") == 0)
|
||||
? TRIGGER_ACTION_TEMPLATE
|
||||
: TRIGGER_ACTION_LLM;
|
||||
int is_enabled = parse_enabled_flag_local(enabled);
|
||||
|
||||
int rc = trigger_manager_add((trigger_manager_t*)g_trigger_manager,
|
||||
d_tag,
|
||||
content->valuestring,
|
||||
filter,
|
||||
action_type,
|
||||
is_enabled);
|
||||
if (rc == 0) {
|
||||
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 d_tag=%s", d_tag);
|
||||
}
|
||||
}
|
||||
|
||||
static void self_skill_cache_upsert_event_locked(cJSON* event) {
|
||||
if (!event || !cJSON_IsObject(event)) {
|
||||
return;
|
||||
@@ -869,6 +1000,11 @@ static void on_event(cJSON* event, const char* relay_url, void* user_data) {
|
||||
(int)tier,
|
||||
received_protocol == DM_PROTOCOL_NIP17 ? "nip17" : "nip04");
|
||||
|
||||
dm_history_remember(sender_pubkey_hex,
|
||||
decrypted,
|
||||
1,
|
||||
time(NULL));
|
||||
|
||||
g_dm_callback(sender_pubkey_hex, decrypted, tier, g_dm_user_data);
|
||||
free(decrypted);
|
||||
}
|
||||
@@ -879,6 +1015,16 @@ static void on_eose(cJSON** events, int event_count, void* user_data) {
|
||||
DEBUG_TRACE("[didactyl] DEBUG on_eose called: event_count=%d", event_count);
|
||||
}
|
||||
|
||||
static void on_self_skill_eose(cJSON** events, int event_count, void* user_data) {
|
||||
(void)events;
|
||||
(void)user_data;
|
||||
DEBUG_INFO("[didactyl] self-skill EOSE received: cached skill events=%d", event_count);
|
||||
|
||||
if (g_self_skill_eose_cb) {
|
||||
g_self_skill_eose_cb(event_count, g_self_skill_eose_user_data);
|
||||
}
|
||||
}
|
||||
|
||||
static void free_admin_context_locked(void) {
|
||||
free(g_admin_kind0_json);
|
||||
g_admin_kind0_json = NULL;
|
||||
@@ -1089,6 +1235,8 @@ static void on_self_skill_event(cJSON* event, const char* relay_url, void* user_
|
||||
return;
|
||||
}
|
||||
|
||||
register_trigger_from_self_skill_event(event);
|
||||
|
||||
pthread_mutex_lock(&g_self_skill_mutex);
|
||||
self_skill_cache_upsert_event_locked(event);
|
||||
pthread_mutex_unlock(&g_self_skill_mutex);
|
||||
@@ -1106,6 +1254,10 @@ int nostr_handler_init(didactyl_config_t* config) {
|
||||
g_seen_dm_count = 0;
|
||||
g_seen_dm_next = 0;
|
||||
|
||||
pthread_mutex_lock(&g_dm_history_mutex);
|
||||
dm_history_clear_locked();
|
||||
pthread_mutex_unlock(&g_dm_history_mutex);
|
||||
|
||||
DEBUG_INFO("[didactyl] initializing relay pool with %d relays", g_cfg->relay_count);
|
||||
|
||||
nostr_pool_reconnect_config_t reconnect = *nostr_pool_reconnect_config_default();
|
||||
@@ -1253,6 +1405,10 @@ int nostr_handler_subscribe_admin_context(void) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
void nostr_handler_set_trigger_manager(struct trigger_manager* trigger_manager) {
|
||||
g_trigger_manager = trigger_manager;
|
||||
}
|
||||
|
||||
int nostr_handler_subscribe_self_skills(void) {
|
||||
if (!g_cfg || !g_pool) {
|
||||
return -1;
|
||||
@@ -1282,7 +1438,7 @@ int nostr_handler_subscribe_self_skills(void) {
|
||||
g_cfg->relay_count,
|
||||
filter,
|
||||
on_self_skill_event,
|
||||
on_eose,
|
||||
on_self_skill_eose,
|
||||
NULL,
|
||||
0,
|
||||
0,
|
||||
@@ -1426,6 +1582,11 @@ int nostr_handler_subscribe_dms(dm_callback_t callback, void* user_data) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
void nostr_handler_set_self_skill_eose_callback(nostr_self_skill_eose_cb_t callback, void* user_data) {
|
||||
g_self_skill_eose_cb = callback;
|
||||
g_self_skill_eose_user_data = user_data;
|
||||
}
|
||||
|
||||
int nostr_handler_send_dm(const char* recipient_pubkey_hex, const char* message) {
|
||||
if (!g_cfg || !g_pool || !recipient_pubkey_hex || !message) {
|
||||
return -1;
|
||||
@@ -1505,6 +1666,13 @@ int nostr_handler_send_dm(const char* recipient_pubkey_hex, const char* message)
|
||||
recipient_pubkey_hex,
|
||||
sent);
|
||||
|
||||
if (sent > 0) {
|
||||
dm_history_remember(recipient_pubkey_hex,
|
||||
message,
|
||||
0,
|
||||
time(NULL));
|
||||
}
|
||||
|
||||
free(connected_relays);
|
||||
cJSON_Delete(event);
|
||||
return sent > 0 ? 0 : -1;
|
||||
@@ -1681,6 +1849,42 @@ char* nostr_handler_query_json(cJSON* filter, int timeout_ms) {
|
||||
return out;
|
||||
}
|
||||
|
||||
nostr_pool_subscription_t* nostr_handler_subscribe_with_filter(
|
||||
cJSON* filter,
|
||||
void (*on_event)(cJSON* event, const char* relay_url, void* user_data),
|
||||
void (*on_eose)(cJSON** events, int event_count, void* user_data),
|
||||
void* user_data,
|
||||
int close_on_eose,
|
||||
int enable_deduplication,
|
||||
nostr_pool_eose_result_mode_t result_mode,
|
||||
int relay_timeout_seconds,
|
||||
int eose_timeout_seconds) {
|
||||
if (!g_cfg || !g_pool || !filter || !on_event) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return nostr_relay_pool_subscribe(
|
||||
g_pool,
|
||||
(const char**)g_cfg->relays,
|
||||
g_cfg->relay_count,
|
||||
filter,
|
||||
on_event,
|
||||
on_eose,
|
||||
user_data,
|
||||
close_on_eose,
|
||||
enable_deduplication,
|
||||
result_mode,
|
||||
relay_timeout_seconds,
|
||||
eose_timeout_seconds);
|
||||
}
|
||||
|
||||
int nostr_handler_close_subscription(nostr_pool_subscription_t* subscription) {
|
||||
if (!subscription) {
|
||||
return 0;
|
||||
}
|
||||
return nostr_pool_subscription_close(subscription);
|
||||
}
|
||||
|
||||
static void publish_pending_startup_events_for_relay_index(int relay_index, const char* reason) {
|
||||
if (!g_cfg || !g_pool || !g_startup_publish_tracking_enabled || !g_startup_published) {
|
||||
return;
|
||||
@@ -2178,9 +2382,80 @@ int nostr_handler_send_dm_nip17(const char* recipient_pubkey_hex, const char* me
|
||||
return -1;
|
||||
}
|
||||
|
||||
dm_history_remember(recipient_pubkey_hex,
|
||||
message,
|
||||
0,
|
||||
time(NULL));
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
char* nostr_handler_get_dm_history_json(const char* peer_pubkey_hex, int limit) {
|
||||
if (!peer_pubkey_hex || strlen(peer_pubkey_hex) != 64U) {
|
||||
return strdup("[]");
|
||||
}
|
||||
|
||||
int max_take = limit > 0 ? limit : 64;
|
||||
if (max_take > DM_HISTORY_RING_SIZE) {
|
||||
max_take = DM_HISTORY_RING_SIZE;
|
||||
}
|
||||
|
||||
cJSON* arr = cJSON_CreateArray();
|
||||
if (!arr) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&g_dm_history_mutex);
|
||||
|
||||
int total = g_dm_history_count;
|
||||
int matches = 0;
|
||||
for (int i = 0; i < total; i++) {
|
||||
int idx = (g_dm_history_count < DM_HISTORY_RING_SIZE)
|
||||
? i
|
||||
: (g_dm_history_next + i) % DM_HISTORY_RING_SIZE;
|
||||
dm_history_entry_t* entry = &g_dm_history_ring[idx];
|
||||
if (entry->peer_pubkey_hex[0] == '\0' || !entry->content) {
|
||||
continue;
|
||||
}
|
||||
if (strcmp(entry->peer_pubkey_hex, peer_pubkey_hex) == 0) {
|
||||
matches++;
|
||||
}
|
||||
}
|
||||
|
||||
int skip = matches > max_take ? (matches - max_take) : 0;
|
||||
int seen = 0;
|
||||
for (int i = 0; i < total; i++) {
|
||||
int idx = (g_dm_history_count < DM_HISTORY_RING_SIZE)
|
||||
? i
|
||||
: (g_dm_history_next + i) % DM_HISTORY_RING_SIZE;
|
||||
dm_history_entry_t* entry = &g_dm_history_ring[idx];
|
||||
if (entry->peer_pubkey_hex[0] == '\0' || !entry->content) {
|
||||
continue;
|
||||
}
|
||||
if (strcmp(entry->peer_pubkey_hex, peer_pubkey_hex) != 0) {
|
||||
continue;
|
||||
}
|
||||
if (seen++ < skip) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cJSON* item = cJSON_CreateObject();
|
||||
if (!item) {
|
||||
continue;
|
||||
}
|
||||
cJSON_AddStringToObject(item, "role", entry->incoming ? "user" : "assistant");
|
||||
cJSON_AddStringToObject(item, "content", entry->content);
|
||||
cJSON_AddNumberToObject(item, "created_at", (double)entry->created_at);
|
||||
cJSON_AddItemToArray(arr, item);
|
||||
}
|
||||
|
||||
pthread_mutex_unlock(&g_dm_history_mutex);
|
||||
|
||||
char* out = cJSON_PrintUnformatted(arr);
|
||||
cJSON_Delete(arr);
|
||||
return out ? out : strdup("[]");
|
||||
}
|
||||
|
||||
char* nostr_handler_get_admin_kind0_context(void) {
|
||||
if (!g_cfg || !g_cfg->admin_context.enabled || !g_cfg->admin_context.track_kind_0) {
|
||||
return NULL;
|
||||
@@ -2350,6 +2625,10 @@ void nostr_handler_cleanup(void) {
|
||||
memset(g_sender_protocol_cache, 0, sizeof(g_sender_protocol_cache));
|
||||
pthread_mutex_unlock(&g_sender_protocol_mutex);
|
||||
|
||||
pthread_mutex_lock(&g_dm_history_mutex);
|
||||
dm_history_clear_locked();
|
||||
pthread_mutex_unlock(&g_dm_history_mutex);
|
||||
|
||||
pthread_mutex_lock(&g_admin_ctx_mutex);
|
||||
free_admin_context_locked();
|
||||
pthread_mutex_unlock(&g_admin_ctx_mutex);
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
|
||||
#include "config.h"
|
||||
#include "cjson/cJSON.h"
|
||||
#include "../nostr_core_lib/nostr_core/nostr_core.h"
|
||||
|
||||
struct trigger_manager;
|
||||
|
||||
typedef enum {
|
||||
DIDACTYL_SENDER_ADMIN = 1,
|
||||
@@ -27,16 +30,31 @@ typedef struct {
|
||||
char** relays;
|
||||
} nostr_publish_result_t;
|
||||
|
||||
typedef void (*nostr_self_skill_eose_cb_t)(int event_count, void* user_data);
|
||||
|
||||
int nostr_handler_init(didactyl_config_t* config);
|
||||
void nostr_handler_set_trigger_manager(struct trigger_manager* trigger_manager);
|
||||
int nostr_handler_subscribe_admin_context(void);
|
||||
int nostr_handler_subscribe_self_skills(void);
|
||||
char* nostr_handler_get_self_events_by_kind_json(int kind);
|
||||
void nostr_handler_set_self_skill_eose_callback(nostr_self_skill_eose_cb_t callback, void* user_data);
|
||||
int nostr_handler_subscribe_dms(dm_callback_t callback, void* user_data);
|
||||
int nostr_handler_send_dm(const char* recipient_pubkey_hex, const char* message);
|
||||
int nostr_handler_send_dm_auto(const char* recipient_pubkey_hex, const char* message);
|
||||
int nostr_handler_publish_kind_event(int kind, const char* content, cJSON* tags, nostr_publish_result_t* out_result);
|
||||
void nostr_handler_publish_result_free(nostr_publish_result_t* result);
|
||||
char* nostr_handler_query_json(cJSON* filter, int timeout_ms);
|
||||
nostr_pool_subscription_t* nostr_handler_subscribe_with_filter(
|
||||
cJSON* filter,
|
||||
void (*on_event)(cJSON* event, const char* relay_url, void* user_data),
|
||||
void (*on_eose)(cJSON** events, int event_count, void* user_data),
|
||||
void* user_data,
|
||||
int close_on_eose,
|
||||
int enable_deduplication,
|
||||
nostr_pool_eose_result_mode_t result_mode,
|
||||
int relay_timeout_seconds,
|
||||
int eose_timeout_seconds);
|
||||
int nostr_handler_close_subscription(nostr_pool_subscription_t* subscription);
|
||||
int nostr_handler_poll(int timeout_ms);
|
||||
int nostr_handler_reconcile_startup_events(void);
|
||||
void nostr_handler_refresh_relay_statuses(void);
|
||||
@@ -52,6 +70,7 @@ int nostr_handler_is_wot_contact(const char* pubkey_hex);
|
||||
char* nostr_handler_relay_status_json(void);
|
||||
char* nostr_handler_relay_info_json(const char* relay_url);
|
||||
int nostr_handler_send_dm_nip17(const char* recipient_pubkey_hex, const char* message, const char* subject);
|
||||
char* nostr_handler_get_dm_history_json(const char* peer_pubkey_hex, int limit);
|
||||
void nostr_handler_cleanup(void);
|
||||
|
||||
#endif
|
||||
+409
-46
@@ -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);
|
||||
@@ -1633,7 +1656,23 @@ char* tools_build_openai_schema_json(const tools_context_t* ctx) {
|
||||
cJSON_AddStringToObject(p_skill_create_auto, "type", "boolean");
|
||||
cJSON_AddItemToObject(t22_props, "auto_adopt", p_skill_create_auto);
|
||||
|
||||
cJSON_AddItemToArray(t22_required, cJSON_CreateString("slug"));
|
||||
cJSON* p_skill_create_trigger = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p_skill_create_trigger, "type", "string");
|
||||
cJSON_AddItemToObject(t22_props, "trigger", p_skill_create_trigger);
|
||||
|
||||
cJSON* p_skill_create_filter = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p_skill_create_filter, "type", "string");
|
||||
cJSON_AddItemToObject(t22_props, "filter", p_skill_create_filter);
|
||||
|
||||
cJSON* p_skill_create_action = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p_skill_create_action, "type", "string");
|
||||
cJSON_AddItemToObject(t22_props, "action", p_skill_create_action);
|
||||
|
||||
cJSON* p_skill_create_enabled = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p_skill_create_enabled, "type", "boolean");
|
||||
cJSON_AddItemToObject(t22_props, "enabled", p_skill_create_enabled);
|
||||
|
||||
cJSON_AddItemToArray(t22_required, cJSON_CreateString("d"));
|
||||
cJSON_AddItemToArray(t22_required, cJSON_CreateString("content"));
|
||||
|
||||
cJSON_AddItemToObject(t22_fn, "parameters", t22_params);
|
||||
@@ -1675,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);
|
||||
@@ -1705,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();
|
||||
@@ -3419,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");
|
||||
@@ -3429,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);
|
||||
@@ -3483,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);
|
||||
@@ -3552,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) {
|
||||
@@ -3586,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,
|
||||
@@ -3604,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);
|
||||
@@ -3710,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;
|
||||
@@ -3726,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;
|
||||
@@ -3803,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;
|
||||
@@ -3815,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;
|
||||
@@ -3827,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;
|
||||
@@ -3862,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();
|
||||
@@ -3893,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");
|
||||
@@ -4090,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) {
|
||||
@@ -5633,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);
|
||||
}
|
||||
|
||||
+230
-91
@@ -12,6 +12,11 @@
|
||||
#include "debug.h"
|
||||
#include "nostr_handler.h"
|
||||
|
||||
typedef struct {
|
||||
trigger_manager_t* mgr;
|
||||
char skill_d_tag[TRIGGER_SKILL_D_TAG_MAX];
|
||||
} trigger_subscription_ctx_t;
|
||||
|
||||
static int clamp_enabled(int enabled) {
|
||||
return enabled ? 1 : 0;
|
||||
}
|
||||
@@ -47,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;
|
||||
}
|
||||
}
|
||||
@@ -80,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;
|
||||
}
|
||||
|
||||
@@ -103,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;
|
||||
@@ -168,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);
|
||||
}
|
||||
@@ -180,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) {
|
||||
@@ -220,6 +225,107 @@ static int maybe_fire_trigger_locked(trigger_manager_t* mgr, int index, cJSON* e
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void on_trigger_subscription_eose(cJSON** events, int event_count, void* user_data) {
|
||||
(void)events;
|
||||
(void)event_count;
|
||||
(void)user_data;
|
||||
}
|
||||
|
||||
static void close_trigger_subscription_locked(active_trigger_t* t) {
|
||||
if (!t) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (t->subscription) {
|
||||
(void)nostr_handler_close_subscription(t->subscription);
|
||||
t->subscription = NULL;
|
||||
}
|
||||
|
||||
if (t->subscription_ctx) {
|
||||
free(t->subscription_ctx);
|
||||
t->subscription_ctx = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
static void on_trigger_subscription_event(cJSON* event, const char* relay_url, void* user_data) {
|
||||
trigger_subscription_ctx_t* ctx = (trigger_subscription_ctx_t*)user_data;
|
||||
if (!ctx || !ctx->mgr || !event) {
|
||||
return;
|
||||
}
|
||||
|
||||
trigger_manager_t* mgr = ctx->mgr;
|
||||
pthread_mutex_lock(&mgr->mutex);
|
||||
|
||||
int idx = find_trigger_index_locked(mgr, ctx->skill_d_tag);
|
||||
if (idx >= 0) {
|
||||
(void)maybe_fire_trigger_locked(mgr, idx, event, relay_url);
|
||||
}
|
||||
|
||||
pthread_mutex_unlock(&mgr->mutex);
|
||||
}
|
||||
|
||||
static int register_trigger_subscription_locked(trigger_manager_t* mgr, active_trigger_t* t) {
|
||||
if (!mgr || !t) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
close_trigger_subscription_locked(t);
|
||||
|
||||
if (!t->enabled || t->filter_json[0] == '\0') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
cJSON* filter = cJSON_Parse(t->filter_json);
|
||||
if (!filter || !cJSON_IsObject(filter)) {
|
||||
cJSON_Delete(filter);
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON* since = cJSON_GetObjectItemCaseSensitive(filter, "since");
|
||||
if (!since || !cJSON_IsNumber(since)) {
|
||||
time_t now = time(NULL);
|
||||
time_t default_since = now > 30 ? now - 30 : 0;
|
||||
cJSON_AddNumberToObject(filter, "since", (double)default_since);
|
||||
}
|
||||
|
||||
cJSON* limit = cJSON_GetObjectItemCaseSensitive(filter, "limit");
|
||||
if (!limit || !cJSON_IsNumber(limit)) {
|
||||
cJSON_AddNumberToObject(filter, "limit", 200);
|
||||
}
|
||||
|
||||
trigger_subscription_ctx_t* ctx = (trigger_subscription_ctx_t*)calloc(1, sizeof(*ctx));
|
||||
if (!ctx) {
|
||||
cJSON_Delete(filter);
|
||||
return -1;
|
||||
}
|
||||
|
||||
ctx->mgr = mgr;
|
||||
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,
|
||||
on_trigger_subscription_event,
|
||||
on_trigger_subscription_eose,
|
||||
ctx,
|
||||
0,
|
||||
0,
|
||||
NOSTR_POOL_EOSE_FULL_SET,
|
||||
30,
|
||||
120);
|
||||
|
||||
cJSON_Delete(filter);
|
||||
|
||||
if (!sub) {
|
||||
free(ctx);
|
||||
return -1;
|
||||
}
|
||||
|
||||
t->subscription = sub;
|
||||
t->subscription_ctx = ctx;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int trigger_manager_init(trigger_manager_t* mgr, didactyl_config_t* cfg) {
|
||||
if (!mgr || !cfg) {
|
||||
return -1;
|
||||
@@ -286,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;
|
||||
}
|
||||
|
||||
@@ -309,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);
|
||||
@@ -338,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);
|
||||
|
||||
@@ -376,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++;
|
||||
}
|
||||
}
|
||||
@@ -389,13 +495,84 @@ int trigger_manager_load_from_skills(trigger_manager_t* mgr) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int trigger_manager_load_from_startup_events(trigger_manager_t* mgr) {
|
||||
if (!mgr || !mgr->cfg) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int loaded = 0;
|
||||
int considered = 0;
|
||||
|
||||
for (int i = 0; i < mgr->cfg->startup_event_count; i++) {
|
||||
startup_event_t* ev = &mgr->cfg->startup_events[i];
|
||||
if (!ev) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ev->kind != 31123 && ev->kind != 31124) {
|
||||
continue;
|
||||
}
|
||||
|
||||
considered++;
|
||||
|
||||
if (!ev->content || ev->content[0] == '\0' || !ev->tags_json || ev->tags_json[0] == '\0') {
|
||||
continue;
|
||||
}
|
||||
|
||||
cJSON* tags = cJSON_Parse(ev->tags_json);
|
||||
if (!tags || !cJSON_IsArray(tags)) {
|
||||
cJSON_Delete(tags);
|
||||
DEBUG_WARN("[didactyl] startup trigger skip: invalid tags_json for startup event index=%d", i);
|
||||
continue;
|
||||
}
|
||||
|
||||
cJSON* d = find_tag_value_string(tags, "d");
|
||||
cJSON* trigger = find_tag_value_string(tags, "trigger");
|
||||
cJSON* filter = find_tag_value_string(tags, "filter");
|
||||
cJSON* action = find_tag_value_string(tags, "action");
|
||||
cJSON* enabled = find_tag_value_string(tags, "enabled");
|
||||
|
||||
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 (!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;
|
||||
}
|
||||
|
||||
if (!trigger_s || strcmp(trigger_s, "nostr-subscription") != 0 || !filter_s || filter_s[0] == '\0') {
|
||||
cJSON_Delete(tags);
|
||||
continue;
|
||||
}
|
||||
|
||||
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, d_tag, ev->content, filter_s, at, is_enabled) == 0) {
|
||||
loaded++;
|
||||
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 d_tag=%s", d_tag);
|
||||
}
|
||||
|
||||
cJSON_Delete(tags);
|
||||
}
|
||||
|
||||
DEBUG_INFO("[didactyl] trigger manager loaded %d trigger(s) from startup config (considered=%d)", loaded, considered);
|
||||
return 0;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -405,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;
|
||||
@@ -426,35 +603,46 @@ 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;
|
||||
t->enabled = clamp_enabled(enabled);
|
||||
t->last_fired = 0;
|
||||
t->last_seen_created_at = 0;
|
||||
t->subscription = NULL;
|
||||
t->subscription_ctx = NULL;
|
||||
|
||||
if (register_trigger_subscription_locked(mgr, t) != 0) {
|
||||
pthread_mutex_unlock(&mgr->mutex);
|
||||
DEBUG_WARN("[didactyl] trigger add rejected: failed to create subscription d_tag=%s", skill_d_tag);
|
||||
memset(t, 0, sizeof(*t));
|
||||
return -1;
|
||||
}
|
||||
|
||||
mgr->count++;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
close_trigger_subscription_locked(&mgr->triggers[idx]);
|
||||
|
||||
if (idx < mgr->count - 1) {
|
||||
memmove(&mgr->triggers[idx],
|
||||
&mgr->triggers[idx + 1],
|
||||
@@ -466,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;
|
||||
}
|
||||
|
||||
@@ -486,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];
|
||||
@@ -498,9 +686,15 @@ int trigger_manager_update(trigger_manager_t* mgr,
|
||||
t->action_type = action_type;
|
||||
t->enabled = clamp_enabled(enabled);
|
||||
|
||||
if (register_trigger_subscription_locked(mgr, t) != 0) {
|
||||
pthread_mutex_unlock(&mgr->mutex);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -522,66 +716,7 @@ int trigger_manager_active_count(trigger_manager_t* mgr) {
|
||||
}
|
||||
|
||||
int trigger_manager_poll(trigger_manager_t* mgr) {
|
||||
if (!mgr || !mgr->cfg || !mgr->cfg->triggers.enabled) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
time_t now = time(NULL);
|
||||
if (mgr->last_poll_at > 0 && (now - mgr->last_poll_at) < 10) {
|
||||
return 0;
|
||||
}
|
||||
mgr->last_poll_at = now;
|
||||
|
||||
pthread_mutex_lock(&mgr->mutex);
|
||||
int count = mgr->count;
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
active_trigger_t snapshot = mgr->triggers[i];
|
||||
if (!snapshot.enabled || snapshot.filter_json[0] == '\0') {
|
||||
continue;
|
||||
}
|
||||
|
||||
cJSON* filter = cJSON_Parse(snapshot.filter_json);
|
||||
if (!filter || !cJSON_IsObject(filter)) {
|
||||
cJSON_Delete(filter);
|
||||
continue;
|
||||
}
|
||||
|
||||
time_t since = snapshot.last_seen_created_at > 0 ? snapshot.last_seen_created_at + 1 : now - 30;
|
||||
if (since < 0) since = 0;
|
||||
cJSON_AddNumberToObject(filter, "since", (double)since);
|
||||
cJSON_AddNumberToObject(filter, "limit", 8);
|
||||
|
||||
char* events_json = nostr_handler_query_json(filter, 1200);
|
||||
cJSON_Delete(filter);
|
||||
if (!events_json) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cJSON* events = cJSON_Parse(events_json);
|
||||
free(events_json);
|
||||
if (!events || !cJSON_IsArray(events)) {
|
||||
cJSON_Delete(events);
|
||||
continue;
|
||||
}
|
||||
|
||||
int n = cJSON_GetArraySize(events);
|
||||
for (int e = 0; e < n; e++) {
|
||||
cJSON* ev = cJSON_GetArrayItem(events, e);
|
||||
if (!ev || !cJSON_IsObject(ev)) continue;
|
||||
|
||||
int idx = find_trigger_index_locked(mgr, snapshot.skill_slug);
|
||||
if (idx < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
(void)maybe_fire_trigger_locked(mgr, idx, ev, NULL);
|
||||
}
|
||||
|
||||
cJSON_Delete(events);
|
||||
}
|
||||
|
||||
pthread_mutex_unlock(&mgr->mutex);
|
||||
(void)mgr;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -623,10 +758,11 @@ 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);
|
||||
cJSON_AddBoolToObject(item, "subscribed", t->subscription ? 1 : 0);
|
||||
cJSON_AddNumberToObject(item, "last_fired", (double)t->last_fired);
|
||||
cJSON_AddNumberToObject(item, "last_seen_created_at", (double)t->last_seen_created_at);
|
||||
cJSON_AddItemToArray(arr, item);
|
||||
@@ -648,6 +784,9 @@ void trigger_manager_cleanup(trigger_manager_t* mgr) {
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&mgr->mutex);
|
||||
for (int i = 0; i < mgr->count; i++) {
|
||||
close_trigger_subscription_locked(&mgr->triggers[i]);
|
||||
}
|
||||
free(mgr->triggers);
|
||||
mgr->triggers = NULL;
|
||||
mgr->count = 0;
|
||||
|
||||
@@ -5,9 +5,10 @@
|
||||
#include <time.h>
|
||||
|
||||
#include "config.h"
|
||||
#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
|
||||
|
||||
@@ -17,13 +18,15 @@ 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;
|
||||
int enabled;
|
||||
time_t last_fired;
|
||||
time_t last_seen_created_at;
|
||||
nostr_pool_subscription_t* subscription;
|
||||
void* subscription_ctx;
|
||||
} active_trigger_t;
|
||||
|
||||
typedef struct trigger_manager {
|
||||
@@ -37,15 +40,16 @@ typedef struct trigger_manager {
|
||||
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user