v0.2.62 - Skill picker cycles X(enabled)→E(edit)→ (disabled); edit mode prompts for new content

This commit is contained in:
Didactyl User
2026-08-07 10:06:00 -04:00
parent 6c24ba8d96
commit 0348992532
4 changed files with 764 additions and 12 deletions
+2 -2
View File
@@ -54,11 +54,11 @@ Skills compose by adoption-list order (`10123`) and trigger tags carry runtime e
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.2.61
## Current Status — v0.2.62
**Active build — this project is barely working. Experiment at your own risk.**
> Last release update: v0.2.61 — Skill template uses agent name + admin npub; add skill picker step with toggle menu and custom skill creation
> Last release update: v0.2.62 — Skill picker cycles X(enabled)→E(edit)→ (disabled); edit mode prompts for new content
- Connects to configured relays with auto-reconnect and relay state transition logging
- Publishes configured startup events per relay as each relay becomes connected
+713
View File
@@ -0,0 +1,713 @@
# Browser Didactyl — A Pure-Browser Agent Runtime
## Vision
Port Didactyl's agent loop to JavaScript as a standalone web page. The browser
becomes the runtime substrate — no shell, no C binary. You open the page, sign
in **as the agent** using the agent's Nostr private key (via
`nostr-login-lite`), and your agent boots: it reads its soul, skills, adoption
list, and memory from relays, listens for triggers, reasons with an LLM, and
acts through a browser-safe tool set.
This is **not** `ai.html` (a chat app for conversations) and **not**
`sovereign_browser` (a full OS browser in C/WebKitGTK). It is Didactyl's agent
semantics — triggers, two-layer context, skill adoption, tool-call loop —
running natively in a browser tab.
---
## Why This Works
Didactyl is Nostr-first. Identity, memory, skills, soul, and communication all
live as Nostr events. The only things tying it to a shell are:
1. **The C binary process** — a browser tab replaces this
2. **Shell-specific tools** — `local_shell_exec`, `local_file_read`,
`local_file_write`, `local_http_fetch` — these are dropped or replaced with
browser equivalents
The agent loop itself is portable:
```
Boot → load config from Nostr → subscribe to triggers →
on trigger: build context → call LLM → execute tools → loop →
deliver response
```
A browser can do every step:
| Capability | Browser Implementation |
|---|---|
| Nostr relay connection | WebSocket (NDK worker or nostr-tools) |
| Event signing | NIP-07 `window.nostr` or n_signer |
| NIP-04/44 encrypt/decrypt | `window.nostr.nip04` / `window.nostr.nip44` |
| LLM calls | `fetch()` to OpenAI-compatible endpoint |
| Skill/soul/memory storage | Kind 31123/31124/30078/10123 events |
| Blossom blob storage | HTTP `fetch()` to Blossom servers |
| Tool execution | JS tool dispatch (browser-safe subset) |
---
## Architecture
```mermaid
flowchart TB
subgraph BrowserTab
UI[Chat UI Page]
CORE[Agent Core JS Module]
TOOLS[Tool Dispatch JS Module]
CTX[Context Builder JS Module]
end
subgraph NostrLayer
RELAYS[Nostr Relays via WebSocket]
EVENTS[Kind 31123 Skills<br/>Kind 10123 Adoption<br/>Kind 30078 Memory/Config<br/>Kind 4/13 DMs]
end
subgraph External
LLM[OpenAI-compatible LLM API]
SIGNER[NIP-07 Signer Extension<br/>or n_signer]
BLOSSOM[Blossom Servers]
end
UI --> CORE
CORE --> CTX
CORE --> TOOLS
CORE -->|WebSocket| RELAYS
RELAYS --> EVENTS
CORE -->|fetch| LLM
CORE -->|window.nostr| SIGNER
TOOLS -->|fetch| BLOSSOM
TOOLS -->|publish events| RELAYS
```
### Boot Sequence
```mermaid
sequenceDiagram
participant User
participant Page as Browser Didactyl
participant NLL as nostr-login-lite
participant Signer as window.nostr
participant Relays as Nostr Relays
User->>Page: Open page
Page->>NLL: NOSTR_LOGIN_LITE.init methods: local, extension, connect, nsigner
NLL->>NLL: Restore persisted auth or show login modal
User->>NLL: Enter agent nsec or connect signer
NLL->>NLL: Install window.nostr facade
Page->>Signer: window.nostr.getPublicKey()
Signer-->>Page: agent pubkey hex
Page->>Relays: Connect to configured relays
Page->>Relays: Subscribe: kind 10002 own pubkey
Page->>Relays: Subscribe: kind 10123 own pubkey
Page->>Relays: Subscribe: kinds 31123,31124 own pubkey
Page->>Relays: Subscribe: kind 30078 own pubkey d=memory
Page->>Relays: Subscribe: kind 30078 own pubkey d=llm_config
Page->>Relays: Subscribe: kind 4 own pubkey p-tag
Relays-->>Page: EOSE + cached events
Page->>Page: Build adoption list + skill cache
Page->>Page: Load LLM config from 30078
Page->>Page: Register trigger subscriptions
Page-->>User: Agent ready — listening for DMs and triggers
```
---
## Browser-Safe Tool Set
The C agent has ~60 tools. The browser agent starts with a focused subset and
can grow. Tools are grouped by what the browser can actually do.
### Phase 1 — Core Nostr Tools
These are the agent's hands in a browser. All use `window.nostr` for signing and
the WebSocket relay pool for publishing.
| Tool | C Equivalent | Browser Implementation |
|---|---|---|
| `nostr_post` | [`nostr_post`](../src/tools/tool_nostr_post.c:1) | Build event → `window.nostr.signEvent` → publish via relay pool |
| `nostr_query` | [`nostr_query`](../src/tools/tool_nostr_query.c:1) | Send filter over WebSocket, collect until EOSE |
| `nostr_dm_send` | [`nostr_dm_send`](../src/tools/tool_nostr_dm.c:1) | NIP-04 encrypt via `window.nostr.nip04.encrypt` → publish kind 4 |
| `nostr_dm_send_nip17` | [`nostr_dm_send_nip17`](../src/tools/tool_nostr_dm.c:1) | NIP-44 encrypt → gift-wrap kind 13/14 (if signer supports nip44) |
| `nostr_delete` | [`nostr_delete`](../src/tools/tool_nostr_post.c:1) | Sign kind 5 → publish |
| `nostr_react` | [`nostr_react`](../src/tools/tool_nostr_post.c:1) | Sign kind 7 → publish |
| `nostr_profile_get` | [`nostr_profile_get`](../src/tools/tool_nostr_identity.c:1) | Query kind 0 → parse metadata |
| `nostr_encrypt` | [`nostr_encrypt`](../src/tools/tool_signer_crypto.c:1) | `window.nostr.nip44.encrypt` |
| `nostr_decrypt` | [`nostr_decrypt`](../src/tools/tool_signer_crypto.c:1) | `window.nostr.nip44.decrypt` |
### Phase 1 — Identity & Context Tools
| Tool | Browser Implementation |
|---|---|
| `nostr_pubkey` / `my_pubkey` | Return cached agent pubkey hex |
| `nostr_npub` / `my_npub` | bech32 encode via nostr-tools |
| `agent_identity` | Build identity context block |
| `nostr_relay_status` | Query relay pool connection states |
| `tool_list` | Return browser tool schemas |
### Phase 1 — Memory & Task Tools
| Tool | C Equivalent | Browser Implementation |
|---|---|---|
| `memory_save` | [`memory_save`](../src/tools/tool_memory.c:1) | NIP-44 encrypt → publish kind 30078 d=memory |
| `memory_recall` | [`memory_recall`](../src/tools/tool_memory.c:1) | Fetch kind 30078 d=memory → decrypt |
| `task_manage` | [`task_manage`](../src/tools/tool_task.c:1) | Publish/fetch kind 30078 d=tasks |
| `task_list` | [`task_list`](../src/tools/tool_task.c:1) | Fetch kind 30078 d=tasks → format |
### Phase 1 — Skill Tools
| Tool | Browser Implementation |
|---|---|
| `skill_create` | Build kind 31123/31124 → sign → publish |
| `skill_edit` | Fetch existing → modify → republish |
| `skill_list` | Query adopted skills from cache |
| `skill_adopt` | Update kind 10123 → sign → publish |
| `skill_remove` | Update kind 10123 → sign → publish |
| `skill_search` | Query kind 31123 by tag/author |
### Phase 1 — Config & Model Tools
| Tool | Browser Implementation |
|---|---|
| `model_get` | Return active LLM config from 30078 or localStorage |
| `model_set` | Publish kind 30078 d=llm_config |
| `config_store` | Encrypt + publish kind 30078 |
| `config_recall` | Fetch + decrypt kind 30078 |
| `agent_version` | Return hardcoded browser version string |
### Phase 2 — Blossom Tools
| Tool | Browser Implementation |
|---|---|
| `blossom_upload` | `fetch()` PUT to Blossom server with signed auth |
| `blossom_download` | `fetch()` GET from Blossom server |
| `blossom_head` | `fetch()` HEAD |
| `blossom_list` | `fetch()` GET list endpoint |
| `blossom_delete` | `fetch()` DELETE with signed auth |
### Phase 2 — Browser-Native Tools
These have no C equivalent — they are browser-only capabilities:
| Tool | Description |
|---|---|
| `browser_fetch` | `fetch()` any URL — replaces `local_http_fetch` |
| `browser_tab_open` | Open a URL in a new tab |
| `browser_clipboard` | Read/write clipboard |
| `browser_local_storage` | Read/write localStorage key-value pairs |
### Explicitly Excluded
These tools cannot run in a browser and are intentionally absent:
| Tool | Reason |
|---|---|
| `local_shell_exec` | No shell access |
| `local_file_read` | No filesystem (except via File System Access API, Phase 3) |
| `local_file_write` | Same |
| `cashu_wallet_*` | Could be ported later (Cashu is HTTP-based) — Phase 3 |
---
## Context Assembly
The browser agent replicates Didactyl's two-layer context model from
[`docs/CONTEXT.md`](../docs/CONTEXT.md:1) and [`docs/SKILLS.md`](../docs/SKILLS.md:1).
### Two-Layer Model
```mermaid
flowchart TD
TRIGGER[Trigger fires: DM, cron, subscription]
TRIGGER --> WALK[Walk adoption list 10123]
WALK --> MATCH{Skill trigger matches?}
MATCH -- yes --> L1[Add to Layer 1]
MATCH -- no --> SKIP[Skip]
L1 --> RESOLVE[Resolve template variables]
RESOLVE --> VARTYPE{Variable type?}
VARTYPE -- tool name --> TOOLEXEC[Execute tool, insert result as markdown]
VARTYPE -- skill d-tag --> SKILLLOOKUP[Insert adopted skill content - Layer 2]
VARTYPE -- unknown --> EMPTY[Resolve to empty]
TOOLEXEC --> FORMAT
SKILLLOOKUP --> FORMAT
EMPTY --> FORMAT
FORMAT[Format document: add title, bump headings, join with ---]
FORMAT --> ROLES[Split into system/user roles]
ROLES --> LLM[Send to LLM with tool schemas]
```
### Template Variable Resolution
The C agent resolves `{{variable}}` placeholders in skill templates. The browser
agent does the same:
- `{{skill_d_tag}}` → look up adopted skill, insert content (layer 2)
- `{{tool_name}}` → execute tool, convert JSON result to markdown, insert
- `{{message}}` → the incoming trigger payload (DM text, etc.)
- Unknown → empty string
### Heading Bump
Skill content headings are bumped down one level (`#` → `##`, `##` → `###`)
during assembly, matching [`context_bump_headings()`](../src/context_format.c:1)
in the C agent.
### Role Split
The assembled markdown is split into system/user roles via the same logic as
[`context_roles_split()`](../src/context_roles.c:1). The runtime owns role
construction — skill authors write plain markdown.
---
## Trigger System
The browser agent supports a subset of Didactyl's trigger types:
| Trigger Type | Browser Support | Implementation |
|---|---|---|
| `dm` | Yes | Subscribe to kind 4 events with own pubkey as p-tag |
| `nostr-subscription` | Yes | Register WebSocket subscription with skill's filter |
| `cron` | Yes | `setInterval` / `setTimeout` with cron expression parser |
| `chain` | Yes | Internal event — fire when another skill completes |
| `webhook` | No | No HTTP server in browser (could use a relay-based webhook relay) |
### DM Trigger Flow
```mermaid
sequenceDiagram
participant Admin
participant Relays as Nostr Relays
participant Page as Browser Didactyl
participant LLM as LLM API
Admin->>Relays: Publish kind 4 DM to agent
Relays->>Page: WebSocket event notification
Page->>Page: NIP-04 decrypt
Page->>Page: Dedup check event ID cache
Page->>Page: Classify sender tier admin/wot/stranger
Page->>Page: Build context from triggered skills
Page->>LLM: Chat completion with tools
LLM-->>Page: Response or tool_calls
loop tool call loop
Page->>Page: Execute tool
Page->>LLM: Feed tool result back
LLM-->>Page: Next response
end
Page->>Relays: Publish kind 4 DM reply
Relays->>Admin: Deliver reply
```
### Cron Trigger
Cron expressions are parsed in JS and scheduled via `setTimeout`. The browser
tab must stay open for cron to fire (a Service Worker could keep it alive in a
future enhancement).
### Sender Tier Classification
The C agent classifies senders as ADMIN / WoT / STRANGER. The browser agent
replicates this:
- **ADMIN** — pubkey matches configured admin pubkey
- **WoT** — pubkey is in admin's kind 3 contact list
- **STRANGER** — everyone else (configurable: ignore or canned reply)
---
## LLM Integration
### Provider Config
LLM config is read from kind 30078 `d:llm_config` under the agent's own pubkey,
falling back to the shared `d:user-settings` `global_llm` namespace (matching
the cross-project settings sync from
[`sovereign_browser/plans/cross-project-agent-sync.md`](../sovereign_browser/plans/cross-project-agent-sync.md:1)).
The config shape:
```json
{
"provider": "ppq",
"api_key": "sk-...",
"model": "claude-haiku-4.5",
"base_url": "https://api.ppq.ai",
"max_tokens": 4096,
"temperature": 0.7
}
```
### LLM Call
Uses `fetch()` to the OpenAI-compatible `/v1/chat/completions` endpoint. The
existing [`ai-chat.mjs`](../client/www/js/ai-chat.mjs:1) and
[`ai-ui.mjs`](../client/www/js/ai-ui.mjs:72) modules provide reusable
`sendAiChatJson()` and config normalization functions.
### Tool-Call Loop
```javascript
async function agentLoop(messages, toolsSchema, maxTurns) {
for (let turn = 0; turn < maxTurns; turn++) {
const response = await callLLM({ messages, tools: toolsSchema });
if (response.tool_calls.length === 0) {
return response.content; // final answer
}
messages.push({ role: 'assistant', tool_calls: response.tool_calls });
for (const tc of response.tool_calls) {
const result = await toolsExecute(tc.name, tc.arguments);
messages.push({ role: 'tool', tool_call_id: tc.id, content: result });
}
}
return null; // max turns exhausted
}
```
### LLM Fallback Chain
Skill `llm` tags use the CSS font-stack style fallback chain
(`anthropic/claude-sonnet, openai/gpt-4o-mini, cheap`). The browser agent
resolves this against its configured providers, matching the C agent's
[`docs/SKILLS.md`](../docs/SKILLS.md:269) behavior.
---
## Nostr Connectivity
### Relay Pool
Two options:
1. **NDK Worker** — reuse [`ndk-worker.js`](../client/www/ndk-worker.js:1) from
the client project. Provides a Web Worker with relay pool, subscription
management, event dedup, and NIP-07 signer bridge. This is the fastest path
— it is already battle-tested in `ai.html` and other client pages.
2. **nostr-tools** — lighter weight, direct in-page. Simpler but less
infrastructure.
**Recommendation: NDK Worker.** It already handles relay connection management,
reconnection, event dedup, and signer integration. The browser Didactyl page
would import [`init-ndk.mjs`](../client/www/js/init-ndk.mjs:1) for the auth +
worker bootstrap, then use the worker's `subscribe` and `publish` message types.
### Subscriptions on Boot
| Subscription | Filter | Purpose |
|---|---|---|
| Relay list | kind 10002, own pubkey | Discover agent's configured relays |
| Adoption list | kind 10123, own pubkey | Load adopted skills |
| Self skills | kinds 31123, 31124, own pubkey | Load skill definitions |
| Memory | kind 30078, own pubkey, d=memory | Agent long-term memory |
| Tasks | kind 30078, own pubkey, d=tasks | Agent task list |
| LLM config | kind 30078, own pubkey, d=llm_config | Provider/model config |
| DMs | kind 4, own pubkey as p-tag | Admin command channel |
| Admin context | kinds 0, 3, 10002, 1, admin pubkey | WoT + admin awareness |
### Event Dedup
The C agent uses an event-ID cache + FNV-1a fingerprint debounce window. The
browser agent uses a `Set<string>` of seen event IDs with a periodic cleanup
(evict entries older than N minutes). NDK also has built-in dedup.
---
## Identity & Signing
### nostr-login-lite — Primary Auth Path
The page uses [`nostr-login-lite`](../nostr_login_lite/README.md:1) for
authentication and signing. This is the same login library used across the
client project. It provides a NIP-07 compliant `window.nostr` facade that works
with multiple signing methods:
| Method | How It Works | Use Case |
|---|---|---|
| **local** | User enters agent nsec; key is AES-GCM encrypted with a session password and stored in localStorage | Primary — sign in as the agent with its private key |
| **extension** | Delegates to a real NIP-07 browser extension (Alby, nos2x, Amber) | If the agent key is in an extension |
| **connect** | NIP-46 remote signer (bunker) | Agent key held by a remote signer service |
| **nsigner** | USB hardware signer via WebUSB/WebSerial | Agent key on n_signer hardware device |
| **seedphrase** | BIP-39 seed phrase → derive nsec | Agent key from a 12-word seed |
| **readonly** | npub only, no signing | Inspection mode — read relay data, no actions |
The key insight: **you sign in as the agent, with the agent's private key.**
nostr-login-lite installs `window.nostr` with the full NIP-07 surface:
- `window.nostr.getPublicKey()` — returns agent pubkey hex
- `window.nostr.signEvent(event)` — signs any Nostr event with the agent key
- `window.nostr.nip04.encrypt(pubkey, plaintext)` — NIP-04 encrypt
- `window.nostr.nip04.decrypt(pubkey, ciphertext)` — NIP-04 decrypt
- `window.nostr.nip44.encrypt(pubkey, plaintext)` — NIP-44 encrypt (if supported)
- `window.nostr.nip44.decrypt(pubkey, ciphertext)` — NIP-44 decrypt (if supported)
All tool implementations that need to sign events, encrypt DMs, or decrypt
memory route through `window.nostr` — exactly like the C agent routes through
its signer. The agent's nsec never touches the agent code directly; it stays
inside nostr-login-lite's encrypted store or the extension/hardware signer.
### Initialization
```javascript
await window.NOSTR_LOGIN_LITE.init({
persistence: true, // Remember login across page reloads
isolateSession: false, // Share across tabs (agent is one identity)
relays: ['wss://relay.damus.io', 'wss://nos.lol'],
methods: {
extension: true, // Browser extension if available
local: true, // Manual nsec entry — primary for agent
readonly: true, // Inspection mode
seedphrase: true, // BIP-39 seed
connect: true, // NIP-46 remote signer
nsigner: true, // USB hardware signer
},
floatingTab: {
enabled: true,
appearance: { text: 'Agent Login' }
}
});
```
After init, nostr-login-lite either restores a persisted session or shows the
login modal. Once authenticated, `window.nostr` is installed and the boot
sequence continues.
### Admin Identity
The admin pubkey is stored in the agent's kind 30078 `d:llm_config` or a
dedicated config event. On boot, the page fetches this to determine the admin
identity for DM classification.
---
## File Layout
The page lives in the client project's `www/` directory alongside the existing
pages, reusing the shared CSS, NDK worker, and JS modules.
```
~/lt/client/www/
├── didactyl-agent.html # NEW — the browser Didactyl page
├── css/
│ └── client.css # existing shared styles
├── js/
│ ├── init-ndk.mjs # existing — NDK worker bootstrap + auth
│ ├── ai-ui.mjs # existing — LLM config + chat completions URL
│ ├── ai-chat.mjs # existing — sendAiChatJson()
│ ├── didactyl-agent.mjs # NEW — agent core: boot, trigger dispatch, loop
│ ├── didactyl-context.mjs # NEW — two-layer context assembly
│ ├── didactyl-tools.mjs # NEW — browser-safe tool dispatch + schemas
│ ├── didactyl-skills.mjs # NEW — skill cache, adoption list, trigger matching
│ ├── didactyl-memory.mjs # NEW — memory/task persistence via kind 30078
│ └── didactyl-triggers.mjs # NEW — DM, cron, subscription trigger management
├── ndk-worker.js # existing — relay pool worker
├── nostr-login-lite/ # existing — login library (vendored or built)
└── template.html # existing — page template (auth, sidenav, theme)
```
### Module Responsibilities
| Module | Responsibility |
|---|---|
| [`didactyl-agent.mjs`](../client/www/js/didactyl-agent.mjs:1) | Boot sequence, agent loop, message dispatch, UI binding |
| [`didactyl-context.mjs`](../client/www/js/didactyl-context.mjs:1) | Two-layer context assembly, heading bump, role split, template variable resolution |
| [`didactyl-tools.mjs`](../client/www/js/didactyl-tools.mjs:1) | Tool schema definitions, tool dispatch, browser-safe tool implementations |
| [`didactyl-skills.mjs`](../client/www/js/didactyl-skills.mjs:1) | Skill cache, adoption list management, trigger matching |
| [`didactyl-memory.mjs`](../client/www/js/didactyl-memory.mjs:1) | Memory save/recall, task management via kind 30078 |
| [`didactyl-triggers.mjs`](../client/www/js/didactyl-triggers.mjs:1) | DM listener, cron scheduler, subscription trigger registration |
---
## UI Design
### Layout
```
+------------------------------------------------------------------+
| DIDACTYL AGENT [dark/light] |
+------------------------------------------------------------------+
| Status: Connected to 3 relays · Listening for DMs and triggers |
+------------------------------------------------------------------+
| | |
| +------------------------------+ | +---------------------------+|
| | CONVERSATION / LOG | | | AGENT STATE ||
| | | | | ||
| | [admin] Fix the typo in my | | | Pubkey: npub1... ||
| | latest post | | | Admin: npub1... ||
| | | | | Relays: 3/3 connected ||
| | [agent] I found your post | | | Skills: 12 adopted ||
| | note1abc... | | | Triggers: 3 active ||
| | The typo is "recieve" | | | ||
| | → fixing now... | | | --- Trigger Log --- ||
| | | | | [cron] readme-monitor ||
| | [tool] nostr_query → 1 event | | | [dm] admin message ||
| | [tool] nostr_post → published | | | [sub] new follower ||
| | | | | ||
| | [admin] Thanks! | | | --- Skills --- ||
| | | | | [x] personality ||
| | [agent] Done! | | | [x] chat ||
| | | | | [x] readme-monitor ||
| | | | | [ ] spellcheck ||
| +------------------------------+ | +---------------------------+|
| | |
| +------------------------------+ | +---------------------------+|
| | [Message input...] [Send] | | | [Settings] ||
| +------------------------------+ | +---------------------------+|
+------------------------------------------------------------------+
```
### Key UI Elements
- **Conversation log** — DM history with the admin, showing tool calls inline
- **Agent state panel** — pubkey, admin, relay status, skill count, trigger status
- **Trigger log** — recent trigger fires with type and skill name
- **Skills list** — adopted skills with toggle (adopts/removes from 10123)
- **Message input** — send a DM to the agent directly (for testing)
- **Settings** — LLM provider config, relay list, admin pubkey
### Reusable Components
The page builds on [`template.html`](../client/www/template.html:1) from the
client project, inheriting:
- Auth flow (NIP-07 login via `init-ndk.mjs`)
- Sidenav navigation
- Theme toggle (dark/light)
- Relay status indicator
- Hamburger morphing menu
---
## Implementation Phases
### Phase 1 — Minimum Viable Agent
Goal: A browser page that boots from a Nostr identity, loads its config and
skills, listens for admin DMs, reasons with an LLM, and replies via DM.
- [ ] Create `didactyl-agent.html` page shell using `template.html` as base
- [ ] Integrate `nostr-login-lite` for auth: init with local/extension/connect/nsigner/seedphrase methods, sign in as agent with its nsec
- [ ] Implement `didactyl-agent.mjs` boot sequence: nostr-login-lite auth → `window.nostr.getPublicKey()` → relay connect → subscribe to config/skills/adoption/DMs
- [ ] Implement `didactyl-skills.mjs`: skill cache, adoption list parsing, trigger matching for `dm` trigger type
- [ ] Implement `didactyl-context.mjs`: two-layer context assembly, heading bump, role split, `{{message}}` and `{{skill_d_tag}}` variable resolution
- [ ] Implement `didactyl-tools.mjs`: core Nostr tools (`nostr_post`, `nostr_query`, `nostr_dm_send`, `nostr_profile_get`, `nostr_pubkey`, `nostr_npub`, `tool_list`), OpenAI-format tool schemas
- [ ] Implement `didactyl-memory.mjs`: `memory_save`, `memory_recall`, `task_manage`, `task_list` via kind 30078
- [ ] Implement agent loop: build context → call LLM → execute tools → loop → reply as DM
- [ ] Implement sender tier classification (admin / WoT / stranger)
- [ ] Wire up conversation log UI + agent state panel
- [ ] LLM config from kind 30078 `d:llm_config` with localStorage fallback
### Phase 2 — Full Tool Set + Triggers
Goal: Complete the browser-safe tool set and add cron + subscription triggers.
- [ ] Add remaining Nostr tools: `nostr_delete`, `nostr_react`, `nostr_dm_send_nip17`, `nostr_encrypt`, `nostr_decrypt`, `nostr_nip05_lookup`, `nostr_encode`, `nostr_decode`
- [ ] Add skill tools: `skill_create`, `skill_edit`, `skill_list`, `skill_adopt`, `skill_remove`, `skill_search`
- [ ] Add config/model tools: `model_get`, `model_set`, `config_store`, `config_recall`, `agent_version`
- [ ] Add Blossom tools: `blossom_upload`, `blossom_download`, `blossom_head`, `blossom_list`, `blossom_delete`
- [ ] Add browser-native tools: `browser_fetch`, `browser_tab_open`, `browser_clipboard`, `browser_local_storage`
- [ ] Implement `didactyl-triggers.mjs`: cron trigger scheduler, nostr-subscription trigger registration, chain trigger
- [ ] Add trigger log UI
- [ ] Add skills management UI (adopt/remove, create/edit)
### Phase 3 — Advanced
Goal: Push the boundaries of what a browser agent can do.
- [ ] Cashu wallet tools (NIP-60) — all HTTP-based, feasible in browser
- [ ] File System Access API — `local_file_read` / `local_file_write` for user-selected directories
- [ ] Service Worker — keep cron triggers alive when tab is backgrounded
- [ ] NIP-17 gift-wrap messaging (requires nip44 signer support)
- [ ] Cross-project settings sync — read/write shared `d:user-settings` kind 30078
- [ ] Broadcast-debounce admin client — fan-out DMs to multiple agents (browser + C), accept first reply (from [`DECENTRALIZED_DIDACTYL.md`](DECENTRALIZED_DIDACTYL.md:1))
- [ ] Mobile responsive layout
- [ ] Offline skill caching via IndexedDB
---
## Compatibility with C Didactyl
### Shared State
The browser agent and C agent share **all state via Nostr events** — no direct
communication is needed. Both read and write the same event kinds:
| State | Event | Shared |
|---|---|---|
| Skills | kind 31123/31124 | Yes — both publish and consume |
| Adoption list | kind 10123 | Yes |
| Memory | kind 30078 d=memory | Yes (encrypted, same key needed) |
| Tasks | kind 30078 d=tasks | Yes |
| LLM config | kind 30078 d=llm_config | Yes |
| DMs | kind 4 | Yes |
| Profile | kind 0 | Yes |
### Same Keys, Different Runtimes
If the browser agent and C agent use the **same nsec**, they share a Nostr
identity. This has the same problems described in
[`DECENTRALIZED_DIDACTYL.md`](DECENTRALIZED_DIDACTYL.md:1): both subscribe to
the same DMs, both process, both reply. The broadcast-debounce model applies.
**Recommended: different keys.** The browser agent is a distinct agent with its
own identity. The admin talks to whichever agent is appropriate. Skills and
soul can be shared via adoption lists.
### Skill Portability
Skills are portable by design. A skill published by the C agent works in the
browser agent and vice versa, as long as:
1. The skill's `tools` tag only references browser-available tools
2. The skill doesn't depend on shell-specific capabilities
The `llm` fallback chain ensures model portability across runtimes.
---
## Security Considerations
| Concern | Mitigation |
|---|---|
| nsec in browser memory | nostr-login-lite encrypts the nsec with AES-GCM and a session password before storing in localStorage. For maximum security, use nsigner (hardware) or connect (NIP-46 remote) methods so the key never enters the browser. |
| LLM API key exposure | Key is in kind 30078 encrypted event. Browser decrypts to memory. Never logged. |
| Tool execution safety | Browser sandbox limits damage. No shell, no filesystem (Phase 1/2). |
| DM sender spoofing | Verify event signatures before processing (NDK does this). Classify sender tier. |
| Relay trust | Relays are untrusted. Event signatures verified. Encrypted DMs. |
| CORS for LLM calls | OpenAI-compatible APIs typically support CORS. If not, use a proxy. |
| Blossom auth | Signed auth events via `window.nostr.signEvent`. |
---
## Reusable Infrastructure
| Asset | Location | What It Provides |
|---|---|---|
| NDK Worker | [`ndk-worker.js`](../client/www/ndk-worker.js:1) | Relay pool, subscriptions, event dedup, publish |
| NDK Init | [`init-ndk.mjs`](../client/www/js/init-ndk.mjs:1) | NIP-07 auth, worker bootstrap, mute list |
| nostr-login-lite | [`nostr-login-lite`](../nostr_login_lite/README.md:1) | NIP-07 facade: local nsec, extension, NIP-46, nsigner, seedphrase, readonly |
| LLM Config | [`ai-ui.mjs`](../client/www/js/ai-ui.mjs:1) | Config normalization, chat completions URL, provider merge |
| LLM Call | [`ai-chat.mjs`](../client/www/js/ai-chat.mjs:1) | `sendAiChatJson()` — fetch-based LLM call |
| Page Template | [`template.html`](../client/www/template.html:1) | Auth, sidenav, theme toggle, relay status |
| Shared CSS | [`client.css`](../client/www/css/client.css:1) | Theme variables, layout, components |
| Skills Demo | [`skills-demo.mjs`](../client/www/js/skills-demo.mjs:1) | Skill discovery, template parsing, execution |
---
## Open Questions
1. **Should the browser agent use the same relay list as the C agent (from kind
10002), or its own?** Same is simpler and more sovereign. Different allows
geographic diversity.
2. **NIP-17 gift-wrap support depends on the signer having nip44.** Not all
NIP-07 extensions support this. Fallback to NIP-04 or skip NIP-17 tools if
unavailable.
3. **Cron triggers die when the tab closes.** Is this acceptable, or do we need
a Service Worker? For a true "always-on" agent, the C binary is still the
right choice. The browser agent is for interactive/attended use.
4. **Should we support the broadcast-debounce admin client pattern from
[`DECENTRALIZED_DIDACTYL.md`](DECENTRALIZED_DIDACTYL.md:1) so the admin can
fan-out to both browser and C agents?** This is a Phase 3 enhancement.
+2 -2
View File
@@ -12,8 +12,8 @@
// Using DIDACTYL_ prefix to avoid conflicts with nostr_core_lib VERSION macros
#define DIDACTYL_VERSION_MAJOR 0
#define DIDACTYL_VERSION_MINOR 2
#define DIDACTYL_VERSION_PATCH 61
#define DIDACTYL_VERSION "v0.2.61"
#define DIDACTYL_VERSION_PATCH 62
#define DIDACTYL_VERSION "v0.2.62"
// Agent metadata
#define DIDACTYL_NAME "Didactyl"
+47 -8
View File
@@ -648,7 +648,6 @@ static const skill_descriptor_t DIDACTYL_DEFAULT_SKILLS[] = {
static int prompt_skill_configuration(didactyl_config_t* cfg, const char* agent_name) {
if (!cfg || !agent_name) return -1;
int skill_enabled[SKILL_PICKER_MAX];
char* skill_d_tags[SKILL_PICKER_MAX];
char* skill_contents[SKILL_PICKER_MAX];
char* skill_tags[SKILL_PICKER_MAX];
@@ -656,7 +655,6 @@ static int prompt_skill_configuration(didactyl_config_t* cfg, const char* agent_
int skill_count = 0;
for (int i = 0; i < DIDACTYL_DEFAULT_SKILL_COUNT && skill_count < SKILL_PICKER_MAX; i++) {
skill_enabled[skill_count] = DIDACTYL_DEFAULT_SKILLS[i].enabled_by_default;
skill_d_tags[skill_count] = strdup(DIDACTYL_DEFAULT_SKILLS[i].d_tag);
if (strcmp(DIDACTYL_DEFAULT_SKILLS[i].d_tag, "dm_history") == 0) {
skill_contents[skill_count] = strdup(DIDACTYL_DEFAULT_DM_HISTORY_SKILL_TEMPLATE);
@@ -668,13 +666,20 @@ static int prompt_skill_configuration(didactyl_config_t* cfg, const char* agent_
skill_count++;
}
/* Skill states: 0 = disabled, 1 = enabled, 2 = edit mode.
* Cycling: 0→1 (enabled), 1→2 (edit), 2→0 (disabled). */
int skill_state[SKILL_PICKER_MAX];
for (int i = 0; i < skill_count; i++) {
skill_state[i] = DIDACTYL_DEFAULT_SKILLS[i].enabled_by_default ? 1 : 0;
}
char input[WIZARD_LINE_MAX];
for (;;) {
render_wizard_page_header("Step 7 of 8", "New Agent Setup -- Skills");
fprintf(stderr, " Skills (enter a number to toggle, [X] = enabled):\n");
fprintf(stderr, " Skills (enter a number to cycle: X=enabled, E=edit, space=disabled):\n");
for (int i = 0; i < skill_count; i++) {
fprintf(stderr, " %d. [%c] %s\n", i + 1,
skill_enabled[i] ? 'X' : ' ',
char state_char = skill_state[i] == 1 ? 'X' : (skill_state[i] == 2 ? 'E' : ' ');
fprintf(stderr, " %d. [%c] %s\n", i + 1, state_char,
skill_d_tags[i] ? skill_d_tags[i] : "");
}
fprintf(stderr, "\n");
@@ -699,7 +704,41 @@ static int prompt_skill_configuration(didactyl_config_t* cfg, const char* agent_
if (is_number) {
int idx = atoi(trimmed) - 1;
if (idx >= 0 && idx < skill_count) {
skill_enabled[idx] = !skill_enabled[idx];
/* Cycle: 0→1 (enabled), 1→2 (edit), 2→0 (disabled) */
skill_state[idx]++;
if (skill_state[idx] > 2) skill_state[idx] = 0;
/* If we entered edit mode, prompt to edit the content. */
if (skill_state[idx] == 2) {
fprintf(stderr, "\n Editing skill '%s':\n", skill_d_tags[idx] ? skill_d_tags[idx] : "");
fprintf(stderr, " Current content:\n%s\n\n", skill_contents[idx] ? skill_contents[idx] : "");
fprintf(stderr, " Enter new content (markdown, end with '.' on its own line):\n");
char new_content[4096] = {0};
size_t content_len = 0;
for (;;) {
char line[WIZARD_LINE_MAX];
if (read_line_prompt(" ", line, sizeof(line)) != 0) {
for (int i = 0; i < skill_count; i++) { free(skill_d_tags[i]); free(skill_contents[i]); free(skill_tags[i]); }
return -1;
}
if (strcmp(line, ".") == 0) break;
size_t line_len = strlen(line);
if (content_len + line_len + 2 > sizeof(new_content)) {
fprintf(stderr, "%sContent too long (max %zu bytes).%s\n", ANSI_RED, sizeof(new_content) - 1, ANSI_RESET);
break;
}
if (content_len > 0) new_content[content_len++] = '\n';
memcpy(new_content + content_len, line, line_len);
content_len += line_len;
}
if (content_len > 0) {
new_content[content_len] = '\0';
free(skill_contents[idx]);
skill_contents[idx] = strdup(new_content);
fprintf(stderr, "%sSkill '%s' updated.%s\n", ANSI_YELLOW, skill_d_tags[idx] ? skill_d_tags[idx] : "", ANSI_RESET);
}
/* After edit, set back to enabled. */
skill_state[idx] = 1;
}
} else {
fprintf(stderr, "%sInvalid skill number (1-%d).%s\n", ANSI_RED, skill_count, ANSI_RESET);
}
@@ -722,7 +761,7 @@ static int prompt_skill_configuration(didactyl_config_t* cfg, const char* agent_
cfg->startup_event_count = 0;
for (int i = 0; i < skill_count; i++) {
if (skill_enabled[i]) {
if (skill_state[i] == 1) {
if (upsert_startup_event(cfg, skill_kinds[i],
skill_contents[i],
skill_tags[i]) != 0) {
@@ -794,7 +833,7 @@ static int prompt_skill_configuration(didactyl_config_t* cfg, const char* agent_
d_tag);
skill_tags[skill_count] = strdup(tags_json);
skill_kinds[skill_count] = DIDACTYL_DEFAULT_SKILL_KIND;
skill_enabled[skill_count] = 1;
skill_state[skill_count] = 1;
skill_count++;
fprintf(stderr, "%sCustom skill '%s' added.%s\n", ANSI_YELLOW, d_tag, ANSI_RESET);
continue;