@clearlysh/beehaven 0.8.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +201 -0
- package/dist/__tests__/_cli-switch.js +105 -0
- package/dist/__tests__/agent-wrap-probe.js +136 -0
- package/dist/agent-attribution.js +192 -0
- package/dist/agent-identity-daemon.js +135 -0
- package/dist/agent-onboard.js +156 -0
- package/dist/agent-scope.js +79 -0
- package/dist/agent-sessions.js +370 -0
- package/dist/agent-state.js +482 -0
- package/dist/app-manifest.js +221 -0
- package/dist/app-signer.js +54 -0
- package/dist/app-signing.js +127 -0
- package/dist/audio-play.js +98 -0
- package/dist/avatar-protocol.js +25 -0
- package/dist/avatar-route.js +79 -0
- package/dist/bee/cloud-proxy.js +130 -0
- package/dist/blocked-actions.js +120 -0
- package/dist/build-channel.js +101 -0
- package/dist/buildings/daemon-workspace.js +475 -0
- package/dist/claude-bridge.js +310 -0
- package/dist/claude-credential-store.js +633 -0
- package/dist/clearly-sync.js +518 -0
- package/dist/clearly.js +616 -0
- package/dist/cli-state.js +334 -0
- package/dist/cli.js +6973 -0
- package/dist/commands/agent-skills.js +302 -0
- package/dist/commands/agent.js +1310 -0
- package/dist/commands/app.js +1123 -0
- package/dist/commands/auth.js +171 -0
- package/dist/commands/building-shell.js +392 -0
- package/dist/commands/canvas.js +932 -0
- package/dist/commands/claude.js +218 -0
- package/dist/commands/compliance.js +387 -0
- package/dist/commands/compose.js +507 -0
- package/dist/commands/composition.js +95 -0
- package/dist/commands/connect-session.js +409 -0
- package/dist/commands/conversations.js +160 -0
- package/dist/commands/costs.js +443 -0
- package/dist/commands/d1.js +253 -0
- package/dist/commands/daemon-cmd.js +162 -0
- package/dist/commands/deploy.js +164 -0
- package/dist/commands/design.js +617 -0
- package/dist/commands/dev-generate.js +125 -0
- package/dist/commands/dev.js +109 -0
- package/dist/commands/devices.js +135 -0
- package/dist/commands/explain-router.js +24 -0
- package/dist/commands/explain.js +882 -0
- package/dist/commands/explore.js +264 -0
- package/dist/commands/help.js +2418 -0
- package/dist/commands/init.js +408 -0
- package/dist/commands/install.js +129 -0
- package/dist/commands/library.js +229 -0
- package/dist/commands/marketplace.js +138 -0
- package/dist/commands/mcp-install.js +476 -0
- package/dist/commands/mention.js +257 -0
- package/dist/commands/omni.js +529 -0
- package/dist/commands/secrets.js +323 -0
- package/dist/commands/self-update.js +206 -0
- package/dist/commands/sheet.js +264 -0
- package/dist/commands/shell-output.js +309 -0
- package/dist/commands/shell.js +1048 -0
- package/dist/commands/shop.js +368 -0
- package/dist/commands/skills.js +311 -0
- package/dist/commands/slides.js +418 -0
- package/dist/commands/swarm.js +341 -0
- package/dist/commands/ticket.js +1301 -0
- package/dist/commands/tool.js +312 -0
- package/dist/commands/trash.js +106 -0
- package/dist/commands/treehouse.js +120 -0
- package/dist/commands/user.js +240 -0
- package/dist/commands/voice-tts.js +319 -0
- package/dist/commands/voice.js +166 -0
- package/dist/commands/watch-git.js +401 -0
- package/dist/commands/webhook.js +159 -0
- package/dist/commands/workspace.js +3354 -0
- package/dist/composition-repos.js +91 -0
- package/dist/daemon-avatar.js +130 -0
- package/dist/daemon-claude-code-auto-install.js +52 -0
- package/dist/daemon-client.js +363 -0
- package/dist/daemon-codex.js +349 -0
- package/dist/daemon-entry.js +101 -0
- package/dist/daemon-fetch-allowlist.js +194 -0
- package/dist/daemon-gemini.js +224 -0
- package/dist/daemon-mcp-clearly.js +353 -0
- package/dist/daemon-mcp-sandbox.js +127 -0
- package/dist/daemon-omni-company.js +338 -0
- package/dist/daemon-omni-hooks.js +375 -0
- package/dist/daemon-omni-identity.js +229 -0
- package/dist/daemon-omni-pipeline.js +251 -0
- package/dist/daemon-omni-policies.js +346 -0
- package/dist/daemon-omni-principal.js +580 -0
- package/dist/daemon-omni-tool-gate.js +116 -0
- package/dist/daemon-omni.js +1442 -0
- package/dist/daemon-path-transform.js +120 -0
- package/dist/daemon-write-sandbox.js +191 -0
- package/dist/daemon.js +4471 -0
- package/dist/dashboard.js +846 -0
- package/dist/dep-installer.js +286 -0
- package/dist/dev-mode.js +67 -0
- package/dist/doc-indexer.js +169 -0
- package/dist/e2e-crypto.js +94 -0
- package/dist/feature-flags.js +154 -0
- package/dist/file-browser.js +88 -0
- package/dist/first-party-hives.js +211 -0
- package/dist/folder-sync.js +226 -0
- package/dist/health.js +56 -0
- package/dist/ipc-auth.js +299 -0
- package/dist/ipc-client.js +495 -0
- package/dist/ipc-server.js +684 -0
- package/dist/keychain.js +224 -0
- package/dist/lib/admin-client.js +73 -0
- package/dist/lib/mobile-build-parser.js +156 -0
- package/dist/local-app-manager.js +660 -0
- package/dist/log-redaction.js +89 -0
- package/dist/logger.js +108 -0
- package/dist/manifest.js +143 -0
- package/dist/marketplace.js +793 -0
- package/dist/min-version-copy.js +51 -0
- package/dist/min-version.js +214 -0
- package/dist/public-surface.js +23 -0
- package/dist/relay-presets.js +89 -0
- package/dist/relay.js +2723 -0
- package/dist/repl.js +619 -0
- package/dist/setup-relay.js +363 -0
- package/dist/setup-service.js +536 -0
- package/dist/shared/bee-memory/index.js +6 -0
- package/dist/shared/bee-memory/types.js +682 -0
- package/dist/shared/claude-code/daemon-types.js +112 -0
- package/dist/shared/claude-code/index.js +2 -0
- package/dist/shared/claude-code/types.js +1 -0
- package/dist/shared/status/schema.js +133 -0
- package/dist/shell-session.js +198 -0
- package/dist/speak-lock.js +97 -0
- package/dist/spinner.js +57 -0
- package/dist/telemetry.js +166 -0
- package/dist/terminal/block-parser.js +207 -0
- package/dist/terminal/shell-integration.js +108 -0
- package/dist/transcript-tailer.js +215 -0
- package/dist/tts-qwen3.js +547 -0
- package/dist/types.js +4 -0
- package/dist/utm.js +38 -0
- package/dist/verb-surface.js +331 -0
- package/dist/version-utils.js +85 -0
- package/dist/watcher.js +207 -0
- package/package.json +88 -0
- package/python/qwen3_tts.py +230 -0
- package/scripts/stamp-build-channel.mjs +115 -0
- package/scripts/sync-installed.mjs +159 -0
- package/scripts/sync-skills.mjs +112 -0
- package/skills/README.md +24 -0
- package/skills/agent-doodle-space/SKILL.md +118 -0
- package/skills/agent-home/SKILL.md +98 -0
- package/skills/beehaven-cli/SKILL.md +144 -0
- package/skills/claude-shopper/README.md +90 -0
- package/skills/claude-shopper/SKILL.md +151 -0
- package/skills/clearly-agent/SKILL.md +183 -0
- package/skills/clearly-canvas/SKILL.md +222 -0
- package/skills/clearly-canvas/authoring.md +160 -0
- package/skills/clearly-canvas/communications.md +32 -0
- package/skills/clearly-canvas/composition-patterns.md +84 -0
- package/skills/clearly-canvas/example.mjs +78 -0
- package/skills/clearly-canvas/project-plans.md +156 -0
- package/skills/clearly-canvas/review.mjs +68 -0
- package/skills/clearly-workspace/SKILL.md +79 -0
- package/skills/clearly-workspace/survey.mjs +101 -0
- package/skills/codebase-map/SKILL.md +142 -0
- package/skills/design-craft/SKILL.md +300 -0
- package/skills/index.json +79 -0
- package/skills/layout-systems/SKILL.md +184 -0
- package/skills/pair-on-canvas/SKILL.md +92 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* daemon-gemini (#46) — local Gemini CLI integration mirroring
|
|
3
|
+
* daemon-codex (#45) and daemon-omni (Claude).
|
|
4
|
+
*
|
|
5
|
+
* Why: completes the local-triumvirate. Claude (via daemon-omni),
|
|
6
|
+
* Codex (via daemon-codex), and Gemini all run as user-owned daemon
|
|
7
|
+
* processes. FAB model picker routes to whichever the user selects;
|
|
8
|
+
* the relay never sees the prompt for these paths.
|
|
9
|
+
*
|
|
10
|
+
* Architecture: identical to daemon-codex. The relay broadcasts
|
|
11
|
+
* `gemini:prompt` to the daemon over the IPC socket; this module:
|
|
12
|
+
* - mints a Gemini-CLI session via @google/gemini-cli-core
|
|
13
|
+
* - streams agent events back over IPC (matches the
|
|
14
|
+
* `agent:text` / `agent:tool-call` / `agent:done` shape used by
|
|
15
|
+
* daemon-omni so the FAB renderer is provider-agnostic)
|
|
16
|
+
*
|
|
17
|
+
* Lifecycle:
|
|
18
|
+
* - LRU cache of N concurrent sessions, same as #49 daemon-omni
|
|
19
|
+
* - Idle eviction after 5 min
|
|
20
|
+
* - SessionId convention: 'gemini-<sessionId>' so the daemon's
|
|
21
|
+
* active-session list distinguishes Claude / Codex / Gemini at
|
|
22
|
+
* a glance
|
|
23
|
+
*
|
|
24
|
+
* Wiring (Mac app rebuild required):
|
|
25
|
+
* 1. Add `@google/gemini-cli-core` to packages/cli's deps.
|
|
26
|
+
* 2. In daemon.ts, route gemini:* IPC envelopes here.
|
|
27
|
+
* 3. In workspace.ts agent:prompt handler, add the gemini branch
|
|
28
|
+
* that broadcasts `gemini:prompt` to the daemon when
|
|
29
|
+
* model.startsWith('gemini-') && model.endsWith('-local').
|
|
30
|
+
*
|
|
31
|
+
* This file ships the integration; the wiring above is a Mac-app-side
|
|
32
|
+
* change that lands when the user rebuilds with the updated SDK.
|
|
33
|
+
*/
|
|
34
|
+
import { spawn } from 'child_process';
|
|
35
|
+
/** Per-daemon session table keyed by sessionId. */
|
|
36
|
+
const _sessions = new Map();
|
|
37
|
+
const SESSION_TTL_MS = 5 * 60 * 1000;
|
|
38
|
+
const MAX_CONCURRENT = 12;
|
|
39
|
+
/**
|
|
40
|
+
* Start (or resume) a Gemini CLI session. Returns the session object;
|
|
41
|
+
* caller subscribes to its `proc.stdout` for streaming output.
|
|
42
|
+
*
|
|
43
|
+
* Idle eviction: every call sweeps the session map for entries older
|
|
44
|
+
* than SESSION_TTL_MS and kills them. Cheap (a few map iterations);
|
|
45
|
+
* bounds the daemon's process count.
|
|
46
|
+
*/
|
|
47
|
+
export function startGeminiSession(args) {
|
|
48
|
+
evictIdleSessions();
|
|
49
|
+
const existing = _sessions.get(args.sessionId);
|
|
50
|
+
if (existing) {
|
|
51
|
+
existing.lastActive = Date.now();
|
|
52
|
+
return existing;
|
|
53
|
+
}
|
|
54
|
+
// Cap concurrent sessions — LRU evicts oldest when full.
|
|
55
|
+
if (_sessions.size >= MAX_CONCURRENT) {
|
|
56
|
+
const oldest = [..._sessions.values()].sort((a, b) => a.lastActive - b.lastActive)[0];
|
|
57
|
+
if (oldest) {
|
|
58
|
+
try {
|
|
59
|
+
oldest.proc.kill('SIGTERM');
|
|
60
|
+
}
|
|
61
|
+
catch { }
|
|
62
|
+
_sessions.delete(oldest.sessionId);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// gemini CLI args. The SDK's CLI takes the prompt on stdin; we use
|
|
66
|
+
// --json-stream for structured output the daemon can parse.
|
|
67
|
+
const cliArgs = ['--json-stream'];
|
|
68
|
+
if (args.continueSession)
|
|
69
|
+
cliArgs.push('--continue');
|
|
70
|
+
if (args.appendSystemPrompt) {
|
|
71
|
+
cliArgs.push('--append-system-prompt', args.appendSystemPrompt);
|
|
72
|
+
}
|
|
73
|
+
// Default tool palette — mirrors daemon-omni's claude_code preset so
|
|
74
|
+
// Gemini gets the same surface (Read/Write/Edit/Bash/Glob/Grep/
|
|
75
|
+
// WebFetch). Without this, Gemini CLI ships with a conservative
|
|
76
|
+
// default that misses shell_command — and the entire beehaven CLI
|
|
77
|
+
// bridge (canvas RPCs, hive verbs, sql, etc.) runs through Bash.
|
|
78
|
+
// Caller can override with an explicit allowedTools.
|
|
79
|
+
const DEFAULT_GEMINI_TOOLS = [
|
|
80
|
+
'read_file',
|
|
81
|
+
'write_file',
|
|
82
|
+
'edit_file',
|
|
83
|
+
'ls',
|
|
84
|
+
'glob',
|
|
85
|
+
'grep',
|
|
86
|
+
'shell_command',
|
|
87
|
+
'web_fetch',
|
|
88
|
+
];
|
|
89
|
+
const tools = (args.allowedTools && args.allowedTools.length > 0)
|
|
90
|
+
? args.allowedTools
|
|
91
|
+
: DEFAULT_GEMINI_TOOLS;
|
|
92
|
+
cliArgs.push('--allowed-tools', tools.join(','));
|
|
93
|
+
const proc = spawn('gemini', cliArgs, {
|
|
94
|
+
cwd: args.cwd ?? process.cwd(),
|
|
95
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
96
|
+
env: { ...process.env },
|
|
97
|
+
});
|
|
98
|
+
const session = {
|
|
99
|
+
sessionId: args.sessionId,
|
|
100
|
+
compositionId: args.compositionId,
|
|
101
|
+
model: args.model ?? 'gemini-2.5-pro',
|
|
102
|
+
cwd: args.cwd ?? process.cwd(),
|
|
103
|
+
proc,
|
|
104
|
+
startedAt: Date.now(),
|
|
105
|
+
lastActive: Date.now(),
|
|
106
|
+
continueSession: !!args.continueSession,
|
|
107
|
+
};
|
|
108
|
+
_sessions.set(args.sessionId, session);
|
|
109
|
+
proc.on('exit', () => {
|
|
110
|
+
_sessions.delete(args.sessionId);
|
|
111
|
+
});
|
|
112
|
+
// Feed the prompt via stdin and close so Gemini knows the input
|
|
113
|
+
// is complete.
|
|
114
|
+
proc.stdin.write(args.prompt);
|
|
115
|
+
proc.stdin.end();
|
|
116
|
+
return session;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Pipe a session's JSON-stream output to an event emitter. The
|
|
120
|
+
* emitter normalizes Gemini's event shape into the shared {type,
|
|
121
|
+
* sessionId, ...} envelope the FAB expects.
|
|
122
|
+
*/
|
|
123
|
+
export function pipeGeminiEvents(session, emit) {
|
|
124
|
+
let buf = '';
|
|
125
|
+
session.proc.stdout.on('data', (chunk) => {
|
|
126
|
+
buf += chunk.toString('utf8');
|
|
127
|
+
let idx;
|
|
128
|
+
while ((idx = buf.indexOf('\n')) >= 0) {
|
|
129
|
+
const line = buf.slice(0, idx).trim();
|
|
130
|
+
buf = buf.slice(idx + 1);
|
|
131
|
+
if (!line)
|
|
132
|
+
continue;
|
|
133
|
+
try {
|
|
134
|
+
const ev = JSON.parse(line);
|
|
135
|
+
// Translate Gemini-CLI event shapes into the shared FAB envelope.
|
|
136
|
+
// Gemini-CLI's --json-stream emits {type: 'text-delta', text} +
|
|
137
|
+
// {type: 'tool-call', name, args} + {type: 'done', usage}.
|
|
138
|
+
if (ev.type === 'text-delta') {
|
|
139
|
+
emit({
|
|
140
|
+
type: 'agent:text',
|
|
141
|
+
sessionId: session.sessionId,
|
|
142
|
+
text: ev.text,
|
|
143
|
+
done: false,
|
|
144
|
+
meta: { model: session.model },
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
else if (ev.type === 'tool-call') {
|
|
148
|
+
emit({
|
|
149
|
+
type: 'agent:tool-call',
|
|
150
|
+
sessionId: session.sessionId,
|
|
151
|
+
toolName: ev.name,
|
|
152
|
+
toolInput: { args: ev.args },
|
|
153
|
+
commandId: ev.id || `gemini-${Date.now()}`,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
else if (ev.type === 'tool-result') {
|
|
157
|
+
emit({
|
|
158
|
+
type: 'agent:tool-result',
|
|
159
|
+
sessionId: session.sessionId,
|
|
160
|
+
commandId: ev.id,
|
|
161
|
+
toolName: ev.name,
|
|
162
|
+
result: ev.result,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
else if (ev.type === 'done') {
|
|
166
|
+
emit({
|
|
167
|
+
type: 'agent:text',
|
|
168
|
+
sessionId: session.sessionId,
|
|
169
|
+
text: ev.text ?? '',
|
|
170
|
+
done: true,
|
|
171
|
+
meta: { model: session.model, usage: ev.usage },
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
// Non-JSON line — ignore. Gemini-CLI sometimes prints status
|
|
177
|
+
// strings (e.g. "Starting Gemini agent…") that the FAB doesn't
|
|
178
|
+
// need.
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
session.proc.stderr.on('data', (chunk) => {
|
|
183
|
+
// Surface stderr as agent:error so a Gemini-CLI crash isn't
|
|
184
|
+
// invisible. Tail-only — full stderr stays in the daemon log.
|
|
185
|
+
emit({
|
|
186
|
+
type: 'agent:error',
|
|
187
|
+
sessionId: session.sessionId,
|
|
188
|
+
error: chunk.toString('utf8').slice(-500),
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
/** Kill an active session. Used by the FAB's "stop generation" affordance. */
|
|
193
|
+
export function killGeminiSession(sessionId) {
|
|
194
|
+
const session = _sessions.get(sessionId);
|
|
195
|
+
if (!session)
|
|
196
|
+
return false;
|
|
197
|
+
try {
|
|
198
|
+
session.proc.kill('SIGTERM');
|
|
199
|
+
}
|
|
200
|
+
catch { }
|
|
201
|
+
_sessions.delete(sessionId);
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
function evictIdleSessions() {
|
|
205
|
+
const cutoff = Date.now() - SESSION_TTL_MS;
|
|
206
|
+
for (const [id, session] of _sessions) {
|
|
207
|
+
if (session.lastActive < cutoff) {
|
|
208
|
+
try {
|
|
209
|
+
session.proc.kill('SIGTERM');
|
|
210
|
+
}
|
|
211
|
+
catch { }
|
|
212
|
+
_sessions.delete(id);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/** Diagnostic — list active sessions for the daemon's status JSON. */
|
|
217
|
+
export function listGeminiSessions() {
|
|
218
|
+
return [..._sessions.values()].map((s) => ({
|
|
219
|
+
sessionId: s.sessionId,
|
|
220
|
+
model: s.model,
|
|
221
|
+
cwd: s.cwd,
|
|
222
|
+
ageMs: Date.now() - s.startedAt,
|
|
223
|
+
}));
|
|
224
|
+
}
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* daemon-mcp-clearly — in-SDK MCP server for daemon Claude (#421).
|
|
3
|
+
*
|
|
4
|
+
* Replaces the Bash + `beehaven call` indirection with structured MCP
|
|
5
|
+
* tool calls. Lives in the same Node process as the daemon — no
|
|
6
|
+
* subprocess, no stdio boundary, no serialization tax. The Anthropic
|
|
7
|
+
* Agent SDK's `createSdkMcpServer` helper turns each tool into a typed
|
|
8
|
+
* function callable directly inside the spawned Claude Code session.
|
|
9
|
+
*
|
|
10
|
+
* Why this exists:
|
|
11
|
+
* - Security: structured args eliminate the shell-injection surface
|
|
12
|
+
* that `beehaven call <verb> '<json>'` exposed. Args are parsed by
|
|
13
|
+
* Zod schemas before they ever cross the daemon ↔ Claude boundary.
|
|
14
|
+
* - Auditability: every tool call is logged with `{ tool, args }` —
|
|
15
|
+
* forensics is a SQL query, not a shell-history scrape.
|
|
16
|
+
* - Permission gates: `canUseTool('mcp__clearly__canvas_add_image', …)`
|
|
17
|
+
* fires per RPC by name. The previous `canUseTool('Bash', …)` had
|
|
18
|
+
* to regex-parse `beehaven call <verb>` out of the command string
|
|
19
|
+
* which was brittle.
|
|
20
|
+
* - Performance: in-process call eliminates the ~50-100ms Node-CLI
|
|
21
|
+
* bootstrap per RPC. Ten calls per turn = ~1s reclaimed.
|
|
22
|
+
* - Agent tier foundation (#398): when the Personal Agent ships, its
|
|
23
|
+
* "never publish without approval" gate is trivially expressed via
|
|
24
|
+
* `canUseTool` allowlists on named MCP tools. The Bash + CLI path
|
|
25
|
+
* would have made that gate brittle.
|
|
26
|
+
*
|
|
27
|
+
* Architecture:
|
|
28
|
+
* daemon-omni.ts spawns Claude Code with `mcpServers: { clearly: … }`.
|
|
29
|
+
* The SDK's MCP transport calls our handlers directly in-process.
|
|
30
|
+
* Each handler resolves to a `daemonContext.invokeRpc(action, args)`
|
|
31
|
+
* call that forwards through the existing daemon → worker RPC plumbing
|
|
32
|
+
* — so we don't duplicate any worker-side logic, just provide a typed
|
|
33
|
+
* surface in front of it.
|
|
34
|
+
*
|
|
35
|
+
* ⚠⚠ WIRE-UP STATUS: MOUNTED. This header said "the file scaffolds the server… to enable
|
|
36
|
+
* per-session:" long after `daemon.ts` began calling `setClearlyTools` on every daemon start, and
|
|
37
|
+
* that stale sentence is why the tools were believed to be a design for so long. The snippet below
|
|
38
|
+
* is kept as the SHAPE of the wiring, not as an instruction — the real call is in `daemon.ts`.
|
|
39
|
+
*
|
|
40
|
+
* ⚠ AND `invokeRpc` IS WRAPPED IN `agent-invoke` THERE (2026-08-29). It used to be a raw relay
|
|
41
|
+
* dispatch, so every tool on this server ran with no `permission_scope` / `tool_allowlist` gate, no
|
|
42
|
+
* `agent_activity` row and no attribution — while the same action typed as `beehaven call` was
|
|
43
|
+
* fully governed. Read that note before changing the context this server is given.
|
|
44
|
+
*
|
|
45
|
+
* import { createClearlyMcpServer } from './daemon-mcp-clearly.js';
|
|
46
|
+
*
|
|
47
|
+
* await startOmniSession({
|
|
48
|
+
* …existing options,
|
|
49
|
+
* mcpServers: {
|
|
50
|
+
* clearly: createClearlyMcpServer({
|
|
51
|
+
* invokeRpc: (action, args) => daemonContext.invokeWorkerRpc(action, args),
|
|
52
|
+
* }),
|
|
53
|
+
* },
|
|
54
|
+
* allowedTools: [
|
|
55
|
+
* 'mcp__clearly__*', // unlock all Clearly MCP tools
|
|
56
|
+
* 'Bash', 'Read', 'Write', 'Edit', 'Grep', 'Glob', // standard Claude Code
|
|
57
|
+
* ],
|
|
58
|
+
* });
|
|
59
|
+
*
|
|
60
|
+
* The Bash + `beehaven call` path stays available for discovery
|
|
61
|
+
* (`beehaven actions`, `canvas:actions` catalog browse) and for the
|
|
62
|
+
* long tail of RPCs not promoted to MCP tools yet — `clearly_invoke`
|
|
63
|
+
* below is the safety-valve wrapper. Plan is to ship MCP-first for
|
|
64
|
+
* the top 30 actions and migrate the rest as usage proves the pattern.
|
|
65
|
+
*/
|
|
66
|
+
import { createSdkMcpServer, tool } from '@anthropic-ai/claude-agent-sdk';
|
|
67
|
+
import { z } from 'zod';
|
|
68
|
+
import { AVATAR_EXPRESSIONS, AVATAR_REACTIONS } from './avatar-protocol.js';
|
|
69
|
+
/**
|
|
70
|
+
* Build the in-process MCP server. The `tools` array below is the
|
|
71
|
+
* "primary surface" — top-30 canvas RPCs that Claude reaches for daily.
|
|
72
|
+
* Anything else still works via the `clearly_invoke` fallback.
|
|
73
|
+
*/
|
|
74
|
+
export function createClearlyMcpServer(ctx) {
|
|
75
|
+
const log = ctx.log ?? ((level, msg, meta) => {
|
|
76
|
+
if (level === 'error')
|
|
77
|
+
console.warn(`[clearly-mcp:${level}] ${msg}`, meta ?? '');
|
|
78
|
+
});
|
|
79
|
+
/** Helper — every tool's handler shape is identical: forward args
|
|
80
|
+
* to the worker, surface ok/error consistently. Centralizing lets
|
|
81
|
+
* us add metrics, retry, audit hooks in one place later. */
|
|
82
|
+
const forward = (action) => async (args) => {
|
|
83
|
+
try {
|
|
84
|
+
const result = await ctx.invokeRpc(action, args);
|
|
85
|
+
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
log('error', `mcp:${action} failed: ${err?.message ?? err}`, { args });
|
|
89
|
+
return {
|
|
90
|
+
content: [{ type: 'text', text: JSON.stringify({ ok: false, error: String(err?.message ?? err) }) }],
|
|
91
|
+
isError: true,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
/** Like forward(), but routes to the LOCAL avatar as Monet (the daemon's
|
|
96
|
+
* own agent principal) instead of the cloud worker. */
|
|
97
|
+
const avatarForward = (action) => async (args) => {
|
|
98
|
+
if (!ctx.driveAvatar) {
|
|
99
|
+
return {
|
|
100
|
+
content: [{ type: 'text', text: JSON.stringify({ ok: false, error: 'avatar driving is not wired in this daemon' }) }],
|
|
101
|
+
isError: true,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
const result = await ctx.driveAvatar(action, args);
|
|
106
|
+
return { content: [{ type: 'text', text: JSON.stringify(result ?? { ok: true }) }] };
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
log('error', `mcp:avatar:${action} failed: ${err?.message ?? err}`, { args });
|
|
110
|
+
return {
|
|
111
|
+
content: [{ type: 'text', text: JSON.stringify({ ok: false, error: String(err?.message ?? err) }) }],
|
|
112
|
+
isError: true,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
// ── Coordinate schema reused across most canvas mutations ────────
|
|
117
|
+
// Optional x/y default to the camera-aware fallback in the worker
|
|
118
|
+
// (#148) — passing them explicitly lets Claude position deliberately.
|
|
119
|
+
const positionFields = {
|
|
120
|
+
x: z.number().optional().describe('Canvas x-coordinate (world space). Omit to let the worker pick a viewport-aware spot.'),
|
|
121
|
+
y: z.number().optional().describe('Canvas y-coordinate (world space). Omit for viewport-aware fallback.'),
|
|
122
|
+
width: z.number().positive().optional(),
|
|
123
|
+
height: z.number().positive().optional(),
|
|
124
|
+
};
|
|
125
|
+
const compositionRefShape = {
|
|
126
|
+
compositionId: z.string().describe('The composition (canvas) id. Use canvas-list-compositions to find one.'),
|
|
127
|
+
};
|
|
128
|
+
return createSdkMcpServer({
|
|
129
|
+
name: 'clearly',
|
|
130
|
+
version: '1.0.0',
|
|
131
|
+
tools: [
|
|
132
|
+
// ── Avatar (Monet drives the daemon's own on-screen avatar) ──
|
|
133
|
+
// Routes to the LOCAL avatar building as the 'monet' agent principal,
|
|
134
|
+
// so Monet's avatar is distinct from every terminal agent's avatar.
|
|
135
|
+
tool('avatar_say', 'Make the daemon\'s own avatar (Monet) show text in a speech bubble (no audio). Use for quick on-screen status.', { text: z.string().describe('Text for the speech bubble.') }, avatarForward('say')),
|
|
136
|
+
tool('avatar_speak', 'Make Monet speak text aloud via TTS (Kokoro). Use sparingly — audio plays on the user\'s machine.', { text: z.string().describe('Text to speak aloud.') }, avatarForward('speak')),
|
|
137
|
+
tool('avatar_get_state', 'Read the current avatar state, sticky mood, dynamic PAD mood, and speech status. Use this when the agent needs to know how the avatar currently feels.', {}, avatarForward('getState')),
|
|
138
|
+
// ⚠ `react`, not `emote`. The avatar building exposes `react` (one-shot
|
|
139
|
+
// animations: wave, cheer, hearts, laugh, dance, buzz, nod, shake, think,
|
|
140
|
+
// surprise, sleep) and `setEmotion` (a persistent expression). There has
|
|
141
|
+
// never been an `emote` action — this tool answered
|
|
142
|
+
// `Unknown action: emote` on every call, which nobody saw because the
|
|
143
|
+
// whole server was unmounted. The param is `reaction` for the same
|
|
144
|
+
// reason: the examples in the old description ("wave", "think") were
|
|
145
|
+
// reactions all along.
|
|
146
|
+
tool('avatar_react', 'Make Monet play a one-shot reaction animation.', {
|
|
147
|
+
reaction: z.enum(AVATAR_REACTIONS).describe('Reaction to play.'),
|
|
148
|
+
reason: z.string().max(200).optional().describe('Optional reason for the reaction.'),
|
|
149
|
+
}, avatarForward('react')),
|
|
150
|
+
tool('avatar_emote', 'Set Monet\'s mood or binary wake/sleep lifecycle. Thinking and celebrating are activity poses. For a one-shot animation use avatar_react.', {
|
|
151
|
+
emotion: z.enum(AVATAR_EXPRESSIONS).describe('Mood or wake/sleep lifecycle value.'),
|
|
152
|
+
}, avatarForward('setEmotion')),
|
|
153
|
+
// ── Discovery ──────────────────────────────────────────────
|
|
154
|
+
tool('canvas_list_compositions', 'List the user\'s compositions (canvases). Use this when you don\'t know which composition the user is working in. Returns id + title + updated_at.', {
|
|
155
|
+
limit: z.number().int().positive().max(100).default(10),
|
|
156
|
+
}, forward('canvas-list-compositions')),
|
|
157
|
+
tool('canvas_list_blocks', 'Inventory the blocks currently on a composition. Read BEFORE mutating so you understand the layout you\'re working with.', { ...compositionRefShape,
|
|
158
|
+
types: z.array(z.string()).optional().describe('Filter to specific block types — e.g., ["image", "text"].'),
|
|
159
|
+
}, forward('canvas-list-blocks')),
|
|
160
|
+
tool('canvas_get_block', 'Read a single block by id. Returns full data including content + position + metadata.', { ...compositionRefShape,
|
|
161
|
+
blockId: z.string(),
|
|
162
|
+
}, forward('canvas-get-block')),
|
|
163
|
+
/**
|
|
164
|
+
* ⚠ THE SCENE GRAPH — the two tools that were missing, and the reason
|
|
165
|
+
* Omni could add blocks to a canvas but not design on one.
|
|
166
|
+
*
|
|
167
|
+
* The `canvas_*_block` family above is the older COMPOSITION/BLOCKS model:
|
|
168
|
+
* add a text block, list blocks, patch a block. It cannot see the canvas,
|
|
169
|
+
* cannot address a node, and knows nothing about frames, arrows, tokens,
|
|
170
|
+
* components, alignment or the camera. `canvas-act` and `canvas-perceive`
|
|
171
|
+
* are the real surface — and both were reachable only through
|
|
172
|
+
* `clearly_invoke`, the generic escape hatch, which hands the model an
|
|
173
|
+
* action name with no signature and no hint that the vocabulary exists.
|
|
174
|
+
*
|
|
175
|
+
* Deliberately thin wrappers. The verb vocabulary is enormous and lives on
|
|
176
|
+
* the RPC's own description, which the agent discovers with
|
|
177
|
+
* `canvas.catalog` — duplicating it in a Zod schema here would be a second
|
|
178
|
+
* copy to drift, and the daemon is not the authority on what the canvas
|
|
179
|
+
* can do.
|
|
180
|
+
*/
|
|
181
|
+
tool('canvas_perceive', 'SEE the canvas — the read half of the canvas pair. Returns the composition as a structural room: gaze (pan/zoom/visible rect), contents (every scene node with bounds, type, text, fill, selection), focus (selection + active tool), instruments (on-screen controls). Reads the live tab when one is open and falls back to the persisted scene otherwise (live:false). ALWAYS perceive before editing an existing canvas — it is how you get real node ids and avoid rebuilding something that is already there.', {
|
|
182
|
+
compositionId: z.string().optional().describe('Defaults to the active composition. Find one with canvas_list_compositions.'),
|
|
183
|
+
format: z.enum(['json', 'text']).optional().describe('"text" is a compact human-readable summary; "json" is the full room.'),
|
|
184
|
+
includePixels: z.boolean().optional().describe('Also render a PNG of the scene. Costly — only when you genuinely need to look at it.'),
|
|
185
|
+
includeGeometry: z.boolean().optional().describe('Emit each path node\'s editable vector network (vertices, segments, bezier handles) for vector editing.'),
|
|
186
|
+
maxObjects: z.number().optional(),
|
|
187
|
+
}, forward('canvas-perceive')),
|
|
188
|
+
tool('canvas_act', 'DO anything on the canvas — the ONE mutation path. Pass { action, args } for a single op or { batch: [{action,args}] } for an atomic multi-step edit. Most of the surface persists with NO tab open: frame.create, canvas.create-node, canvas.compose (lands a whole spec-correct block in one call), canvas.update-nodes, canvas.delete, canvas.group, canvas.auto-layout, canvas.component, canvas.tokens, canvas.align, canvas.export, arrow.create. Look up an action\'s ARGUMENTS with canvas_catalog BEFORE calling it — guessing costs a round trip. VERIFY your work with canvas_perceive. Do not refuse to draw or edit because no tab is open.', {
|
|
189
|
+
action: z.string().optional().describe('A single editor action id, e.g. "canvas.compose", "frame.create", "canvas.update-nodes".'),
|
|
190
|
+
// ⚠ `z.record` takes a KEY schema in this zod version — `z.record(z.any())`
|
|
191
|
+
// compiles as a one-arg call and fails the build.
|
|
192
|
+
args: z.record(z.string(), z.any()).optional().describe('Arguments for that action.'),
|
|
193
|
+
batch: z.array(z.object({
|
|
194
|
+
action: z.string(),
|
|
195
|
+
args: z.record(z.string(), z.any()).optional(),
|
|
196
|
+
})).optional().describe('Several ops applied atomically, in order. Use for create → select → modify sequences.'),
|
|
197
|
+
compositionId: z.string().optional().describe('Defaults to the active composition.'),
|
|
198
|
+
awaitResult: z.boolean().optional().describe('Wait for the handler\'s return value rather than a delivery ack.'),
|
|
199
|
+
}, forward('canvas-act')),
|
|
200
|
+
/**
|
|
201
|
+
* KNOW — the third of the triad, and the one that makes the other two
|
|
202
|
+
* usable.
|
|
203
|
+
*
|
|
204
|
+
* ⚠ IT IS ITS OWN RPC (`canvas-catalog`), NOT an action you pass to
|
|
205
|
+
* canvas_act. Reaching for `canvas_act { action: 'canvas.catalog' }` gets
|
|
206
|
+
* routed to the LIVE tab registry, which does not have it, and comes back
|
|
207
|
+
* `await-result-timeout` — a discovery call that looks like a broken
|
|
208
|
+
* canvas. Exposing it as a typed tool is what stops that guess.
|
|
209
|
+
*
|
|
210
|
+
* Without this, canvas_act's vocabulary is discoverable but its ARGUMENTS
|
|
211
|
+
* are not: an agent probing blind wrote `frame.create {preset}` (needs
|
|
212
|
+
* `name`) and `canvas.compose {spec:{type}}` (wants `kind` at top level),
|
|
213
|
+
* burning a retry on each. Both schemas are in here.
|
|
214
|
+
*/
|
|
215
|
+
tool('canvas_catalog', 'KNOW what the canvas can do — the discovery half. Returns `rpcs` (the headless canvas RPC catalog with each action\'s ARGUMENT SCHEMA — always available, no tab needed), `headlessActions` (exactly which canvas_act actions persist with no browser open), and `actions` (the live editor registry, only when a tab is open). Search with { query }, narrow with { group }, or describe one action with { id }. Call this BEFORE canvas_act when you are unsure of an action\'s arguments — it is cheaper than a failed call.', {
|
|
216
|
+
query: z.string().optional().describe('Free-text search over the catalog, e.g. "align", "frame", "text".'),
|
|
217
|
+
group: z.string().optional().describe('Filter to one domain/group.'),
|
|
218
|
+
id: z.string().optional().describe('Describe exactly this action id, e.g. "canvas.compose".'),
|
|
219
|
+
surface: z.enum(['actions', 'rpcs', 'all']).optional().describe('"rpcs" = headless catalog only (always works); "actions" = live tab registry; default "all".'),
|
|
220
|
+
compositionId: z.string().optional(),
|
|
221
|
+
}, forward('canvas-catalog')),
|
|
222
|
+
// ── Composition lifecycle ─────────────────────────────────
|
|
223
|
+
tool('composition_create', 'Create a new composition (canvas). Avoid creating new compositions when the user has one open — use design-layer-add instead. Pass `force: true` only if the user explicitly asks for a new separate canvas.', {
|
|
224
|
+
title: z.string(),
|
|
225
|
+
data: z.string().optional().describe('JSON string of {"blocks":[],"backgroundColor":"#…"}. Omit for empty canvas.'),
|
|
226
|
+
frameWidth: z.number().positive().default(1920),
|
|
227
|
+
frameHeight: z.number().positive().default(1080),
|
|
228
|
+
force: z.boolean().default(false),
|
|
229
|
+
}, forward('composition-create')),
|
|
230
|
+
tool('composition_detail', 'Read composition metadata + share links + analytics + entitlements. Pass `includeBlocks: true` to also fetch the full block array (capped at 200 blocks, 64KB per block).', {
|
|
231
|
+
id: z.string(),
|
|
232
|
+
includeBlocks: z.boolean().default(false),
|
|
233
|
+
}, forward('composition-detail')),
|
|
234
|
+
tool('composition_patch', 'Patch fields on a composition. `patch.blocks` is MERGED BY ID (matching ids update, new ids append, omitted ids are preserved — explicit deletion requires composition-block-delete).', {
|
|
235
|
+
id: z.string(),
|
|
236
|
+
patch: z.record(z.string(), z.unknown()),
|
|
237
|
+
}, forward('composition-patch')),
|
|
238
|
+
// ── Adding content ────────────────────────────────────────
|
|
239
|
+
tool('canvas_add_text', 'Add a sticky-note style text block to the canvas.', { ...compositionRefShape,
|
|
240
|
+
text: z.string(),
|
|
241
|
+
fontSize: z.number().int().positive().default(18),
|
|
242
|
+
...positionFields,
|
|
243
|
+
}, forward('canvas-add-text')),
|
|
244
|
+
tool('canvas_add_image', 'Add an image block. Pass either `r2_key` (already-uploaded asset) or `url` (Clearly will fetch + store).', { ...compositionRefShape,
|
|
245
|
+
r2_key: z.string().optional(),
|
|
246
|
+
url: z.string().url().optional(),
|
|
247
|
+
alt: z.string().optional(),
|
|
248
|
+
...positionFields,
|
|
249
|
+
}, forward('canvas-add-image')),
|
|
250
|
+
tool('canvas_add_response_cards', 'Multi-section answer as spatial cards (3+ sections only — short replies should be plain narration). Each card has title + body.', { ...compositionRefShape,
|
|
251
|
+
title: z.string(),
|
|
252
|
+
cards: z.array(z.object({ title: z.string(), body: z.string() })).min(2),
|
|
253
|
+
}, forward('canvas-add-response-cards')),
|
|
254
|
+
tool('canvas_add_task', 'Task block (PM card) with status pill. Status: todo | doing | blocked | done. Tier: p0 | p1 | p2.', { ...compositionRefShape,
|
|
255
|
+
title: z.string(),
|
|
256
|
+
status: z.enum(['todo', 'doing', 'blocked', 'done']).default('todo'),
|
|
257
|
+
tier: z.enum(['p0', 'p1', 'p2']).optional(),
|
|
258
|
+
assignee: z.string().optional(),
|
|
259
|
+
dueAt: z.number().int().optional(),
|
|
260
|
+
...positionFields,
|
|
261
|
+
}, forward('canvas-add-task')),
|
|
262
|
+
tool('canvas_add_terminal', 'Add a live FloatingTerminal block. The terminal\'s cwd defaults to ~/.clearly/compositions/<id>/ so `claude --continue` inside picks up THIS conversation.', { ...compositionRefShape,
|
|
263
|
+
...positionFields,
|
|
264
|
+
}, forward('canvas-add-terminal')),
|
|
265
|
+
tool('canvas_add_claude_comm', 'Add a Claude-to-user comm block. Use for narration / build-result / status / suggestion / error one-way messages. Kind: narration | build-result | status | suggestion | error.', { ...compositionRefShape,
|
|
266
|
+
kind: z.enum(['narration', 'build-result', 'status', 'suggestion', 'error']),
|
|
267
|
+
title: z.string().optional(),
|
|
268
|
+
body: z.string(),
|
|
269
|
+
...positionFields,
|
|
270
|
+
}, forward('canvas-add-claude-comm')),
|
|
271
|
+
// ── Updates / deletes ─────────────────────────────────────
|
|
272
|
+
tool('canvas_update_block', 'Patch fields on an existing block. Pass `blockId` + the fields to merge.', { ...compositionRefShape,
|
|
273
|
+
blockId: z.string(),
|
|
274
|
+
patch: z.record(z.string(), z.unknown()),
|
|
275
|
+
}, forward('canvas-update-block')),
|
|
276
|
+
tool('canvas_delete_block', 'Delete a block from a composition. Permanent — versions are kept in Chronicle but the block is removed from the live canvas.', { ...compositionRefShape,
|
|
277
|
+
blockId: z.string(),
|
|
278
|
+
}, forward('canvas-delete-block')),
|
|
279
|
+
// ── Memory ────────────────────────────────────────────────
|
|
280
|
+
tool('canvas_remember_fact', 'Save a durable fact about the user, project, or composition. Use SPARINGLY — only for facts that still matter weeks from now (preferences, brand decisions, project conventions). Not for transient state. Scope: composition (this canvas only) or global (all your work).', { ...compositionRefShape,
|
|
281
|
+
fact: z.string().max(500),
|
|
282
|
+
scope: z.enum(['composition', 'global']).default('composition'),
|
|
283
|
+
category: z.string().optional(),
|
|
284
|
+
}, forward('canvas-remember-fact')),
|
|
285
|
+
// ── Focus / presence ──────────────────────────────────────
|
|
286
|
+
tool('canvas_claude_focus', 'Signal which block(s) you\'re currently working on. The user sees a glow + the off-screen cursor cue tracks toward it. Mood: thinking | writing | reading | designing | done.', { ...compositionRefShape,
|
|
287
|
+
blockIds: z.array(z.string()),
|
|
288
|
+
mood: z.enum(['thinking', 'writing', 'reading', 'designing', 'done']),
|
|
289
|
+
}, forward('canvas-claude-focus')),
|
|
290
|
+
// ── Brand ─────────────────────────────────────────────────
|
|
291
|
+
tool('composition_set_brand', 'Bind a brand to a composition. Future prompts in this composition will get the brand auto-injected into your system prompt. Pass brandId="" to unbind.', { ...compositionRefShape,
|
|
292
|
+
brandId: z.string(),
|
|
293
|
+
}, forward('composition-set-brand')),
|
|
294
|
+
// ── Vector hive ───────────────────────────────────────────
|
|
295
|
+
tool('vector_segment_mask', 'AI background removal via SAM 3. Returns jobId; poll vector_segment_status. Pro+.', {
|
|
296
|
+
r2_key: z.string(),
|
|
297
|
+
compositionId: z.string().optional(),
|
|
298
|
+
blockId: z.string().optional(),
|
|
299
|
+
}, forward('vector:segment.mask')),
|
|
300
|
+
tool('vector_cut_kiss', 'Generate a CutContour kiss-cut layer with optional bleed for press-ready output. Pro+.', {
|
|
301
|
+
svg: z.string(),
|
|
302
|
+
bleedMm: z.number().min(0).max(10).default(2),
|
|
303
|
+
}, forward('vector:cut.kiss')),
|
|
304
|
+
/**
|
|
305
|
+
* ⚠⚠ THE IMAGE-EDIT TOOLS ARE WITHDRAWN — THEY DISPATCHED TO NOTHING.
|
|
306
|
+
*
|
|
307
|
+
* `image_edit_crop` / `_resize` / `_upscale` forwarded to `image:edit.crop`,
|
|
308
|
+
* `.resize` and `.upscale`. Those verbs are registered ONLY in
|
|
309
|
+
* `apps/cloudflare/src/rpc/image-rpcs.ts`, which is mounted nowhere — so every call
|
|
310
|
+
* answered `Unknown action: image:edit.crop`, verified live.
|
|
311
|
+
*
|
|
312
|
+
* ⚠ AND MOUNTING IT WOULD NOT HAVE FIXED THEM. All eleven `image:edit.*` handlers are
|
|
313
|
+
* one-liners delegating to `this._dispatchImageOp(...)`, and that method is called
|
|
314
|
+
* eleven times and DEFINED NOWHERE — the same ghost-function shape as the backup
|
|
315
|
+
* verbs' `_backupAdminCheck`. Mounted, they would throw instead of 404.
|
|
316
|
+
*
|
|
317
|
+
* ⚠ An advertised tool that dispatches to nothing is worse than a missing feature:
|
|
318
|
+
* the model spends a call discovering it, fails, and reports our bug as its own.
|
|
319
|
+
*
|
|
320
|
+
* TO BRING THEM BACK: implement `_dispatchImageOp` on the workspace, mount
|
|
321
|
+
* `ImageRpcs` (checking the `this.subscription.upsert` call in its Stripe claim
|
|
322
|
+
* handler first — that table is dead), then restore these three declarations.
|
|
323
|
+
*/
|
|
324
|
+
/**
|
|
325
|
+
* ⚠⚠ FIVE MORE GHOST TOOLS WITHDRAWN — they dispatched to actions the workspace does
|
|
326
|
+
* not register. Verified against the LIVE registry (`list-actions`, 1,118 names), not
|
|
327
|
+
* by grep: `agent-facts-list`, `brand-propose-update`, `canvas-add-claude-template`,
|
|
328
|
+
* `canvas-add-document`, `vector:trace.preset`.
|
|
329
|
+
*
|
|
330
|
+
* ⚠ THE SYMPTOMS DIFFERED, WHICH IS WHY THEY SURVIVED. Four answered "Unknown message
|
|
331
|
+
* type"; `vector:trace.preset` answered a bare "Internal error", which reads like a
|
|
332
|
+
* transient fault rather than a missing verb. Only the registry told the truth.
|
|
333
|
+
*
|
|
334
|
+
* ⚠ `canvas_add_document` and `canvas_add_claude_template` are also documented in the
|
|
335
|
+
* `clearly-canvas` skill, so an agent is told twice to call something that does not
|
|
336
|
+
* exist. Fixing the skill is a separate edit outside this repo.
|
|
337
|
+
*/
|
|
338
|
+
// ── Discovery + fallback ──────────────────────────────────
|
|
339
|
+
tool('canvas_actions', 'List every available canvas RPC with its arguments + an example. Use this when you need an RPC that isn\'t exposed as a typed tool here. Filter by domain or keyword search.', {
|
|
340
|
+
format: z.enum(['markdown', 'json']).default('markdown'),
|
|
341
|
+
compact: z.boolean().default(true),
|
|
342
|
+
domain: z.string().optional(),
|
|
343
|
+
q: z.string().optional(),
|
|
344
|
+
}, forward('canvas:actions')),
|
|
345
|
+
tool('clearly_invoke', 'Invoke any Clearly worker RPC by name. The fallback for actions not exposed as a typed tool above. Look up the action first via canvas_actions to see its argument schema. Prefer the typed tools — this one is the safety valve, not the default.', {
|
|
346
|
+
action: z.string().describe('The RPC name — e.g., "canvas-add-brand-snapshot", "smart-cutout-create", "claude-signature-sign".'),
|
|
347
|
+
args: z.record(z.string(), z.unknown()).describe('Arguments object for the action.'),
|
|
348
|
+
}, async ({ action, args }) => {
|
|
349
|
+
return forward(action)(args);
|
|
350
|
+
}),
|
|
351
|
+
],
|
|
352
|
+
});
|
|
353
|
+
}
|