@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.
Files changed (171) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +201 -0
  3. package/dist/__tests__/_cli-switch.js +105 -0
  4. package/dist/__tests__/agent-wrap-probe.js +136 -0
  5. package/dist/agent-attribution.js +192 -0
  6. package/dist/agent-identity-daemon.js +135 -0
  7. package/dist/agent-onboard.js +156 -0
  8. package/dist/agent-scope.js +79 -0
  9. package/dist/agent-sessions.js +370 -0
  10. package/dist/agent-state.js +482 -0
  11. package/dist/app-manifest.js +221 -0
  12. package/dist/app-signer.js +54 -0
  13. package/dist/app-signing.js +127 -0
  14. package/dist/audio-play.js +98 -0
  15. package/dist/avatar-protocol.js +25 -0
  16. package/dist/avatar-route.js +79 -0
  17. package/dist/bee/cloud-proxy.js +130 -0
  18. package/dist/blocked-actions.js +120 -0
  19. package/dist/build-channel.js +101 -0
  20. package/dist/buildings/daemon-workspace.js +475 -0
  21. package/dist/claude-bridge.js +310 -0
  22. package/dist/claude-credential-store.js +633 -0
  23. package/dist/clearly-sync.js +518 -0
  24. package/dist/clearly.js +616 -0
  25. package/dist/cli-state.js +334 -0
  26. package/dist/cli.js +6973 -0
  27. package/dist/commands/agent-skills.js +302 -0
  28. package/dist/commands/agent.js +1310 -0
  29. package/dist/commands/app.js +1123 -0
  30. package/dist/commands/auth.js +171 -0
  31. package/dist/commands/building-shell.js +392 -0
  32. package/dist/commands/canvas.js +932 -0
  33. package/dist/commands/claude.js +218 -0
  34. package/dist/commands/compliance.js +387 -0
  35. package/dist/commands/compose.js +507 -0
  36. package/dist/commands/composition.js +95 -0
  37. package/dist/commands/connect-session.js +409 -0
  38. package/dist/commands/conversations.js +160 -0
  39. package/dist/commands/costs.js +443 -0
  40. package/dist/commands/d1.js +253 -0
  41. package/dist/commands/daemon-cmd.js +162 -0
  42. package/dist/commands/deploy.js +164 -0
  43. package/dist/commands/design.js +617 -0
  44. package/dist/commands/dev-generate.js +125 -0
  45. package/dist/commands/dev.js +109 -0
  46. package/dist/commands/devices.js +135 -0
  47. package/dist/commands/explain-router.js +24 -0
  48. package/dist/commands/explain.js +882 -0
  49. package/dist/commands/explore.js +264 -0
  50. package/dist/commands/help.js +2418 -0
  51. package/dist/commands/init.js +408 -0
  52. package/dist/commands/install.js +129 -0
  53. package/dist/commands/library.js +229 -0
  54. package/dist/commands/marketplace.js +138 -0
  55. package/dist/commands/mcp-install.js +476 -0
  56. package/dist/commands/mention.js +257 -0
  57. package/dist/commands/omni.js +529 -0
  58. package/dist/commands/secrets.js +323 -0
  59. package/dist/commands/self-update.js +206 -0
  60. package/dist/commands/sheet.js +264 -0
  61. package/dist/commands/shell-output.js +309 -0
  62. package/dist/commands/shell.js +1048 -0
  63. package/dist/commands/shop.js +368 -0
  64. package/dist/commands/skills.js +311 -0
  65. package/dist/commands/slides.js +418 -0
  66. package/dist/commands/swarm.js +341 -0
  67. package/dist/commands/ticket.js +1301 -0
  68. package/dist/commands/tool.js +312 -0
  69. package/dist/commands/trash.js +106 -0
  70. package/dist/commands/treehouse.js +120 -0
  71. package/dist/commands/user.js +240 -0
  72. package/dist/commands/voice-tts.js +319 -0
  73. package/dist/commands/voice.js +166 -0
  74. package/dist/commands/watch-git.js +401 -0
  75. package/dist/commands/webhook.js +159 -0
  76. package/dist/commands/workspace.js +3354 -0
  77. package/dist/composition-repos.js +91 -0
  78. package/dist/daemon-avatar.js +130 -0
  79. package/dist/daemon-claude-code-auto-install.js +52 -0
  80. package/dist/daemon-client.js +363 -0
  81. package/dist/daemon-codex.js +349 -0
  82. package/dist/daemon-entry.js +101 -0
  83. package/dist/daemon-fetch-allowlist.js +194 -0
  84. package/dist/daemon-gemini.js +224 -0
  85. package/dist/daemon-mcp-clearly.js +353 -0
  86. package/dist/daemon-mcp-sandbox.js +127 -0
  87. package/dist/daemon-omni-company.js +338 -0
  88. package/dist/daemon-omni-hooks.js +375 -0
  89. package/dist/daemon-omni-identity.js +229 -0
  90. package/dist/daemon-omni-pipeline.js +251 -0
  91. package/dist/daemon-omni-policies.js +346 -0
  92. package/dist/daemon-omni-principal.js +580 -0
  93. package/dist/daemon-omni-tool-gate.js +116 -0
  94. package/dist/daemon-omni.js +1442 -0
  95. package/dist/daemon-path-transform.js +120 -0
  96. package/dist/daemon-write-sandbox.js +191 -0
  97. package/dist/daemon.js +4471 -0
  98. package/dist/dashboard.js +846 -0
  99. package/dist/dep-installer.js +286 -0
  100. package/dist/dev-mode.js +67 -0
  101. package/dist/doc-indexer.js +169 -0
  102. package/dist/e2e-crypto.js +94 -0
  103. package/dist/feature-flags.js +154 -0
  104. package/dist/file-browser.js +88 -0
  105. package/dist/first-party-hives.js +211 -0
  106. package/dist/folder-sync.js +226 -0
  107. package/dist/health.js +56 -0
  108. package/dist/ipc-auth.js +299 -0
  109. package/dist/ipc-client.js +495 -0
  110. package/dist/ipc-server.js +684 -0
  111. package/dist/keychain.js +224 -0
  112. package/dist/lib/admin-client.js +73 -0
  113. package/dist/lib/mobile-build-parser.js +156 -0
  114. package/dist/local-app-manager.js +660 -0
  115. package/dist/log-redaction.js +89 -0
  116. package/dist/logger.js +108 -0
  117. package/dist/manifest.js +143 -0
  118. package/dist/marketplace.js +793 -0
  119. package/dist/min-version-copy.js +51 -0
  120. package/dist/min-version.js +214 -0
  121. package/dist/public-surface.js +23 -0
  122. package/dist/relay-presets.js +89 -0
  123. package/dist/relay.js +2723 -0
  124. package/dist/repl.js +619 -0
  125. package/dist/setup-relay.js +363 -0
  126. package/dist/setup-service.js +536 -0
  127. package/dist/shared/bee-memory/index.js +6 -0
  128. package/dist/shared/bee-memory/types.js +682 -0
  129. package/dist/shared/claude-code/daemon-types.js +112 -0
  130. package/dist/shared/claude-code/index.js +2 -0
  131. package/dist/shared/claude-code/types.js +1 -0
  132. package/dist/shared/status/schema.js +133 -0
  133. package/dist/shell-session.js +198 -0
  134. package/dist/speak-lock.js +97 -0
  135. package/dist/spinner.js +57 -0
  136. package/dist/telemetry.js +166 -0
  137. package/dist/terminal/block-parser.js +207 -0
  138. package/dist/terminal/shell-integration.js +108 -0
  139. package/dist/transcript-tailer.js +215 -0
  140. package/dist/tts-qwen3.js +547 -0
  141. package/dist/types.js +4 -0
  142. package/dist/utm.js +38 -0
  143. package/dist/verb-surface.js +331 -0
  144. package/dist/version-utils.js +85 -0
  145. package/dist/watcher.js +207 -0
  146. package/package.json +88 -0
  147. package/python/qwen3_tts.py +230 -0
  148. package/scripts/stamp-build-channel.mjs +115 -0
  149. package/scripts/sync-installed.mjs +159 -0
  150. package/scripts/sync-skills.mjs +112 -0
  151. package/skills/README.md +24 -0
  152. package/skills/agent-doodle-space/SKILL.md +118 -0
  153. package/skills/agent-home/SKILL.md +98 -0
  154. package/skills/beehaven-cli/SKILL.md +144 -0
  155. package/skills/claude-shopper/README.md +90 -0
  156. package/skills/claude-shopper/SKILL.md +151 -0
  157. package/skills/clearly-agent/SKILL.md +183 -0
  158. package/skills/clearly-canvas/SKILL.md +222 -0
  159. package/skills/clearly-canvas/authoring.md +160 -0
  160. package/skills/clearly-canvas/communications.md +32 -0
  161. package/skills/clearly-canvas/composition-patterns.md +84 -0
  162. package/skills/clearly-canvas/example.mjs +78 -0
  163. package/skills/clearly-canvas/project-plans.md +156 -0
  164. package/skills/clearly-canvas/review.mjs +68 -0
  165. package/skills/clearly-workspace/SKILL.md +79 -0
  166. package/skills/clearly-workspace/survey.mjs +101 -0
  167. package/skills/codebase-map/SKILL.md +142 -0
  168. package/skills/design-craft/SKILL.md +300 -0
  169. package/skills/index.json +79 -0
  170. package/skills/layout-systems/SKILL.md +184 -0
  171. package/skills/pair-on-canvas/SKILL.md +92 -0
@@ -0,0 +1,312 @@
1
+ /**
2
+ * beehaven tool — Local tool registry for Claude (and other agents).
3
+ *
4
+ * Tools live under ~/.beehaven/tools/<name>/ as either:
5
+ * - run.sh / run.mjs / run.ts / run.js (executable script)
6
+ * - manifest.json (optional metadata + input schema)
7
+ *
8
+ * Each tool is a self-contained script that takes JSON on stdin (or a single
9
+ * positional arg) and prints to stdout. Tools are discoverable, runnable, and
10
+ * persistent across Claude sessions — the user's workspace DO already gives Claude
11
+ * persistent memory; this gives Claude persistent CAPABILITIES.
12
+ *
13
+ * Subcommands:
14
+ * beehaven tool list # list all installed tools
15
+ * beehaven tool show <name> # show one tool's manifest + script path
16
+ * beehaven tool run <name> [<json>] # execute a tool, json passed on stdin
17
+ * beehaven tool create <name> # scaffold a new tool dir
18
+ * beehaven tool remove <name> # delete a tool
19
+ *
20
+ * Example: install an elevenlabs transcription tool
21
+ *
22
+ * beehaven tool create elevenlabs-transcribe
23
+ * # writes ~/.beehaven/tools/elevenlabs-transcribe/run.mjs and manifest.json
24
+ * # Claude edits run.mjs to call the ElevenLabs API
25
+ *
26
+ * beehaven tool run elevenlabs-transcribe '{"audioFile":"/tmp/note.webm"}'
27
+ * # Output: {"transcript":"...","duration":12.3,"language":"en"}
28
+ *
29
+ * The tool runs in the local shell with the user's environment. The daemon
30
+ * is NOT involved for local tool execution — this is intentional. Daemon
31
+ * RPCs are for talking to cloud DOs; local tools are for talking to APIs,
32
+ * the filesystem, the user's environment.
33
+ *
34
+ * Tools can call `beehaven rpc <action>` internally to compose with cloud
35
+ * data, including `beehaven rpc admin:store-rpc '{"storeId":"...","action":"...","input":{...}}'`
36
+ * to talk to a specific building DO.
37
+ */
38
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, rmSync, statSync, chmodSync, } from 'fs';
39
+ import { join } from 'path';
40
+ import { homedir } from 'os';
41
+ import { spawn } from 'child_process';
42
+ const TOOLS_DIR = join(homedir(), '.beehaven', 'tools');
43
+ function ensureToolsDir() {
44
+ if (!existsSync(TOOLS_DIR))
45
+ mkdirSync(TOOLS_DIR, { recursive: true });
46
+ }
47
+ function listToolDirs() {
48
+ ensureToolsDir();
49
+ try {
50
+ return readdirSync(TOOLS_DIR).filter((name) => {
51
+ const p = join(TOOLS_DIR, name);
52
+ try {
53
+ return statSync(p).isDirectory();
54
+ }
55
+ catch {
56
+ return false;
57
+ }
58
+ });
59
+ }
60
+ catch {
61
+ return [];
62
+ }
63
+ }
64
+ function findEntry(dir) {
65
+ const candidates = [
66
+ { file: 'run.mjs', runtime: 'node' },
67
+ { file: 'run.js', runtime: 'node' },
68
+ { file: 'run.ts', runtime: 'node' },
69
+ { file: 'run.sh', runtime: 'bash' },
70
+ { file: 'run.py', runtime: 'python' },
71
+ ];
72
+ for (const c of candidates) {
73
+ const p = join(dir, c.file);
74
+ if (existsSync(p))
75
+ return { entry: p, runtime: c.runtime };
76
+ }
77
+ return null;
78
+ }
79
+ function loadTool(name) {
80
+ const dir = join(TOOLS_DIR, name);
81
+ if (!existsSync(dir))
82
+ return null;
83
+ let manifest = { name };
84
+ const manifestPath = join(dir, 'manifest.json');
85
+ if (existsSync(manifestPath)) {
86
+ try {
87
+ manifest = { name, ...JSON.parse(readFileSync(manifestPath, 'utf8')) };
88
+ }
89
+ catch (e) {
90
+ console.warn(`[tool] ${name}/manifest.json parse error: ${e.message}`);
91
+ }
92
+ }
93
+ let entry;
94
+ let runtime;
95
+ if (manifest.entry) {
96
+ entry = join(dir, manifest.entry);
97
+ runtime =
98
+ manifest.runtime && manifest.runtime !== 'auto'
99
+ ? manifest.runtime
100
+ : entry.endsWith('.sh')
101
+ ? 'bash'
102
+ : entry.endsWith('.py')
103
+ ? 'python'
104
+ : 'node';
105
+ }
106
+ else {
107
+ const found = findEntry(dir);
108
+ if (!found) {
109
+ console.warn(`[tool] ${name}: no run.{mjs,js,ts,sh,py} found in ${dir}`);
110
+ return null;
111
+ }
112
+ entry = found.entry;
113
+ runtime = found.runtime;
114
+ }
115
+ return { name, dir, entry, runtime, manifest };
116
+ }
117
+ function runTool(tool, input) {
118
+ return new Promise((resolve) => {
119
+ const interpreter = tool.runtime === 'node' ? 'node' : tool.runtime === 'python' ? 'python3' : 'bash';
120
+ const args = tool.runtime === 'node' && tool.entry.endsWith('.ts')
121
+ ? ['--experimental-strip-types', tool.entry]
122
+ : [tool.entry];
123
+ const env = { ...process.env };
124
+ const timeout = tool.manifest.timeout ?? 60_000;
125
+ const child = spawn(interpreter, args, {
126
+ cwd: tool.dir,
127
+ env,
128
+ stdio: ['pipe', 'pipe', 'pipe'],
129
+ });
130
+ let stdout = '';
131
+ let stderr = '';
132
+ const timer = setTimeout(() => {
133
+ child.kill('SIGKILL');
134
+ }, timeout);
135
+ child.stdout.on('data', (chunk) => {
136
+ stdout += chunk.toString();
137
+ });
138
+ child.stderr.on('data', (chunk) => {
139
+ stderr += chunk.toString();
140
+ });
141
+ child.on('close', (code) => {
142
+ clearTimeout(timer);
143
+ resolve({ stdout, stderr, exitCode: code ?? 1 });
144
+ });
145
+ // Pass input via stdin
146
+ if (input !== undefined && input !== null) {
147
+ child.stdin.write(typeof input === 'string' ? input : JSON.stringify(input));
148
+ }
149
+ child.stdin.end();
150
+ });
151
+ }
152
+ // ── Subcommand handlers ─────────────────────────────────────────────────
153
+ export async function listTools() {
154
+ ensureToolsDir();
155
+ const dirs = listToolDirs();
156
+ if (dirs.length === 0) {
157
+ console.log(`No tools installed. Create one with:`);
158
+ console.log(` beehaven tool create <name>`);
159
+ return;
160
+ }
161
+ console.log(`Tools (${dirs.length}):`);
162
+ for (const name of dirs) {
163
+ const tool = loadTool(name);
164
+ if (!tool) {
165
+ console.log(` ${name} (broken — no entry script)`);
166
+ continue;
167
+ }
168
+ const desc = tool.manifest.description ? ` — ${tool.manifest.description}` : '';
169
+ console.log(` ${name} [${tool.runtime}]${desc}`);
170
+ }
171
+ console.log(`\nTools dir: ${TOOLS_DIR}`);
172
+ }
173
+ export async function showTool(name) {
174
+ if (!name) {
175
+ console.error('Usage: beehaven tool show <name>');
176
+ process.exit(1);
177
+ }
178
+ const tool = loadTool(name);
179
+ if (!tool) {
180
+ console.error(`Tool not found: ${name}`);
181
+ process.exit(1);
182
+ }
183
+ console.log(`Name: ${tool.name}`);
184
+ console.log(`Dir: ${tool.dir}`);
185
+ console.log(`Entry: ${tool.entry}`);
186
+ console.log(`Runtime: ${tool.runtime}`);
187
+ if (tool.manifest.description)
188
+ console.log(`Description: ${tool.manifest.description}`);
189
+ if (tool.manifest.timeout)
190
+ console.log(`Timeout: ${tool.manifest.timeout}ms`);
191
+ if (tool.manifest.input) {
192
+ console.log(`Input schema:`);
193
+ for (const [key, param] of Object.entries(tool.manifest.input)) {
194
+ const req = param.required === false ? ' (optional)' : '';
195
+ const desc = param.description ? ` — ${param.description}` : '';
196
+ console.log(` ${key}: ${param.type}${req}${desc}`);
197
+ }
198
+ }
199
+ }
200
+ export async function runToolCmd(name, inputJson) {
201
+ if (!name) {
202
+ console.error('Usage: beehaven tool run <name> [<json-input>]');
203
+ process.exit(1);
204
+ }
205
+ const tool = loadTool(name);
206
+ if (!tool) {
207
+ console.error(`Tool not found: ${name}`);
208
+ process.exit(1);
209
+ }
210
+ let input = undefined;
211
+ if (inputJson) {
212
+ try {
213
+ input = JSON.parse(inputJson);
214
+ }
215
+ catch (e) {
216
+ console.error(`Invalid JSON input: ${e.message}`);
217
+ process.exit(1);
218
+ }
219
+ }
220
+ const result = await runTool(tool, input);
221
+ if (result.stdout)
222
+ process.stdout.write(result.stdout);
223
+ if (result.stderr)
224
+ process.stderr.write(result.stderr);
225
+ process.exit(result.exitCode);
226
+ }
227
+ export async function createTool(name) {
228
+ if (!name) {
229
+ console.error('Usage: beehaven tool create <name>');
230
+ process.exit(1);
231
+ }
232
+ if (!/^[a-z0-9-]+$/.test(name)) {
233
+ console.error(`Tool name must be lowercase letters/digits/hyphens only: ${name}`);
234
+ process.exit(1);
235
+ }
236
+ const dir = join(TOOLS_DIR, name);
237
+ if (existsSync(dir)) {
238
+ console.error(`Tool already exists: ${name}`);
239
+ process.exit(1);
240
+ }
241
+ mkdirSync(dir, { recursive: true });
242
+ // Write a starter run.mjs — a working echo with timestamp so the
243
+ // scaffolded tool is immediately runnable. Replace the `transform`
244
+ // function with real logic.
245
+ const runScript = `#!/usr/bin/env node
246
+ /**
247
+ * ${name} — local tool for Claude / agents.
248
+ *
249
+ * Reads JSON from stdin, writes JSON to stdout.
250
+ * Compose with: beehaven rpc <action> '<json>' for cloud DO calls.
251
+ */
252
+ let input = '';
253
+ process.stdin.on('data', (chunk) => { input += chunk; });
254
+ process.stdin.on('end', () => {
255
+ let payload = {};
256
+ try { payload = input.trim() ? JSON.parse(input) : {}; } catch (e) {
257
+ process.stderr.write('Invalid JSON input\\n');
258
+ process.exit(1);
259
+ }
260
+
261
+ const result = transform(payload);
262
+ process.stdout.write(JSON.stringify(result, null, 2) + '\\n');
263
+ });
264
+
265
+ function transform(payload) {
266
+ // Your tool logic here. Return any JSON-serializable value.
267
+ // Examples:
268
+ // - Call a local binary: execSync('ffmpeg -i ...')
269
+ // - Read a local file: readFileSync('/tmp/x.json', 'utf-8')
270
+ // - Call an API: await fetch('https://api.example.com/...')
271
+ return {
272
+ tool: '${name}',
273
+ echoed: payload,
274
+ receivedAt: new Date().toISOString(),
275
+ };
276
+ }
277
+ `;
278
+ const runPath = join(dir, 'run.mjs');
279
+ writeFileSync(runPath, runScript);
280
+ chmodSync(runPath, 0o755);
281
+ // Write a starter manifest.json — input map describes payload fields
282
+ // the tool accepts (for agents + docs). Replace with your real fields.
283
+ const manifest = {
284
+ name,
285
+ description: `Local tool: ${name}`,
286
+ runtime: 'node',
287
+ entry: 'run.mjs',
288
+ timeout: 60_000,
289
+ input: {
290
+ message: { type: 'string', description: 'Example input field' },
291
+ },
292
+ };
293
+ writeFileSync(join(dir, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n');
294
+ console.log(`Created: ${dir}`);
295
+ console.log(` ├── run.mjs ← edit this`);
296
+ console.log(` └── manifest.json ← edit this`);
297
+ console.log('');
298
+ console.log(`Run with: beehaven tool run ${name} '{}'`);
299
+ }
300
+ export async function removeTool(name) {
301
+ if (!name) {
302
+ console.error('Usage: beehaven tool remove <name>');
303
+ process.exit(1);
304
+ }
305
+ const dir = join(TOOLS_DIR, name);
306
+ if (!existsSync(dir)) {
307
+ console.error(`Tool not found: ${name}`);
308
+ process.exit(1);
309
+ }
310
+ rmSync(dir, { recursive: true, force: true });
311
+ console.log(`Removed: ${name}`);
312
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * `beehaven trash` / `beehaven restore` — the way BACK from `rm`.
3
+ *
4
+ * ⚠⚠ `rm` ARCHIVES, IT DOES NOT DESTROY (CBH2-34). Its help page says so now. What it did not have
5
+ * was a route back: `beehaven help | grep -iE 'restore|trash|undelete|unarchive'` returned nothing,
6
+ * so the everyday surface could put a document away and never get it out again. Undoing an `rm`
7
+ * meant knowing that `document-archive` takes `{ archived: false }` — i.e. reading the worker.
8
+ *
9
+ * ⚠ BOTH CAPABILITIES ALREADY EXISTED SERVER-SIDE. `document-list { archived: true }` lists them
10
+ * (145 on this workspace) and `document-archive { archived: false }` puts one back. This is a
11
+ * missing DOOR, not a missing feature — so it is two thin verbs over the existing RPCs, never a
12
+ * second implementation of archiving.
13
+ *
14
+ * ⚠ TWO VERBS, NOT ONE. `trash` answers "what did I delete" and `restore` answers "put that back".
15
+ * Folding restore into `trash --restore <ref>` reads as a flag on a listing, and the thing a person
16
+ * types when they have just deleted the wrong file is the verb, not the flag.
17
+ */
18
+ import { agentRpc as rpcOnce } from './workspace.js';
19
+ const c = {
20
+ cyan: (s) => `\x1b[36m${s}\x1b[0m`,
21
+ dim: (s) => `\x1b[2m${s}\x1b[0m`,
22
+ bold: (s) => `\x1b[1m${s}\x1b[0m`,
23
+ };
24
+ /** Matches the rest of the CLI: a page is a page, and the ceiling is stated rather than silent. */
25
+ const LIST_DEFAULT = 50;
26
+ const LIST_MAX = 500;
27
+ function getTimeout(values) {
28
+ const t = values?.timeout ? parseInt(String(values.timeout), 10) : undefined;
29
+ return Number.isFinite(t) ? t : undefined;
30
+ }
31
+ function fail(msg, hint) {
32
+ console.error(` ${msg}`);
33
+ if (hint)
34
+ console.error(c.dim(` ${hint}`));
35
+ process.exitCode = 1;
36
+ }
37
+ export async function cmdTrash(rest, values) {
38
+ const t = getTimeout(values);
39
+ if (rest[0] === 'help') {
40
+ console.log(`
41
+ ${c.cyan('beehaven trash')} — documents ${c.bold('rm')} put away (it archives, it does not destroy)
42
+
43
+ trash List archived documents (--limit <n>, max ${LIST_MAX})
44
+ ${c.dim(`restore <key|title> Put one back — see \`beehaven restore\``)}
45
+ `);
46
+ return;
47
+ }
48
+ const asked = values?.limit ? parseInt(String(values.limit), 10) : LIST_DEFAULT;
49
+ const limit = Math.max(1, Math.min(Number.isFinite(asked) ? asked : LIST_DEFAULT, LIST_MAX));
50
+ // ⚠ SAY SO WHEN THE ASK WAS CLAMPED. A silently reduced limit is the defect this sprint spent
51
+ // most of its time on — the caller believes they saw everything.
52
+ if (Number.isFinite(asked) && asked > LIST_MAX) {
53
+ console.error(c.dim(` --limit ${asked} exceeds the ceiling; using ${LIST_MAX}.`));
54
+ }
55
+ const res = await rpcOnce('document-list', { limit, archived: true }, t, values);
56
+ if (!res || res.ok === false)
57
+ return fail(res?.error || 'could not list the trash', res?.hint);
58
+ const docs = Array.isArray(res.documents) ? res.documents
59
+ : Array.isArray(res.notes) ? res.notes
60
+ : Array.isArray(res.items) ? res.items : [];
61
+ if (!docs.length) {
62
+ console.log(c.dim('\n Nothing archived.\n'));
63
+ return;
64
+ }
65
+ console.log('');
66
+ for (const d of docs) {
67
+ const key = String(d.key ?? '');
68
+ const title = String(d.title ?? '(untitled)');
69
+ console.log(` ${c.cyan(key.padEnd(10))} ${title}`);
70
+ }
71
+ // ⚠ A FULL PAGE IS NOT THE SAME AS THE WHOLE LIST, and it must not read like one.
72
+ console.log(c.dim(`\n ${docs.length} archived`
73
+ + (docs.length >= limit ? ` — that is the whole page, there may be more (--limit, max ${LIST_MAX})` : '')));
74
+ console.log(c.dim(` Put one back: beehaven restore <key>\n`));
75
+ }
76
+ export async function cmdRestore(rest, values) {
77
+ const t = getTimeout(values);
78
+ const ref = (rest[0] ?? '').trim();
79
+ if (!ref || ref === 'help') {
80
+ console.log(`
81
+ ${c.cyan('beehaven restore')} — put an archived document back
82
+
83
+ restore <key|title|id> Un-archive it and return it to \`ls\`
84
+
85
+ ${c.dim('See what is there first: beehaven trash')}
86
+ `);
87
+ if (!ref)
88
+ process.exitCode = 1;
89
+ return;
90
+ }
91
+ // ⚠ `archived: false` IS THE UN-ARCHIVE. `document-archive` defaults the field to TRUE when it is
92
+ // omitted, so sending the verb with no flag would archive the thing the caller asked to restore.
93
+ const res = await rpcOnce('document-archive', { id: ref, archived: false }, t, values);
94
+ if (!res || res.ok === false) {
95
+ return fail(res?.error || 'could not restore that document', res?.code === 'not-found' ? 'Check the key with `beehaven trash`.' : res?.hint);
96
+ }
97
+ const title = String(res.title ?? ref);
98
+ // ⚠ REPORT THE NO-OP AS A NO-OP. The handler answers `unchanged: true` when the row was already
99
+ // live, and printing "restored" there would claim work that did not happen — the exact shape
100
+ // this sprint kept finding. Same reason `document-update` reports `unchanged`.
101
+ if (res.unchanged) {
102
+ console.log(c.dim(`\n ${title} was not archived — nothing to do.\n`));
103
+ return;
104
+ }
105
+ console.log(`\n Restored ${c.bold(title)}${res.key ? c.dim(` (${res.key})`) : ''}\n`);
106
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * beehaven treehouse — drive a BYOK room (claude/codex/gemini/pi) from the CLI.
3
+ *
4
+ * WHY THIS EXISTS. The four rooms the Clearly desktop app hosts ran behind
5
+ * `ipcMain.handle('treehouse:send')`, which means the only caller that could ever
6
+ * reach them was the chat window itself. No CLI, no script, no test, no agent
7
+ * could ask Gemini for anything — the sole way to send a room a message was for a
8
+ * human to type it. That is a strange shape for four agents that exist to be
9
+ * automated, and it is why this verb exists.
10
+ *
11
+ * DEVELOPER MODE ONLY, and deliberately so: a turn here spends the USER'S OWN
12
+ * Claude/Codex/Gemini subscription and runs with their ~/Clearly filesystem. The
13
+ * desktop app only mounts the route when developer mode is on (main.ts →
14
+ * isDeveloperModeEnabled), so on a consumer build this 404s — the endpoint isn't
15
+ * there to reject you.
16
+ *
17
+ * TRANSPORT. Loopback HTTP to the Electron main process (127.0.0.1:3456), the same
18
+ * bridge `browser-rpc.ts` already exposes for tab control, authorised with the
19
+ * token at ~/.beehaven/browser-token. NOT the daemon: the daemon is a different
20
+ * process and has no treehouse — it was never the thing holding these sessions.
21
+ */
22
+ import { readFileSync } from 'fs';
23
+ import { homedir } from 'os';
24
+ import { join } from 'path';
25
+ import { color as c, spinner } from '../spinner.js';
26
+ const DEFAULT_PORT = 3456;
27
+ const TOKEN_PATH = join(homedir(), '.beehaven', 'browser-token');
28
+ /** A room turn can build a whole page — minutes, not seconds. Don't cut it off. */
29
+ const DEFAULT_TIMEOUT_MS = 15 * 60_000;
30
+ const ROOMS = ['claude', 'codex', 'gemini', 'pi'];
31
+ function readToken() {
32
+ try {
33
+ return readFileSync(TOKEN_PATH, 'utf8').trim() || null;
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ }
39
+ /**
40
+ * `beehaven treehouse <room> "<message>"` — send one turn, print the reply.
41
+ *
42
+ * Synchronous: the desktop holds the connection until the agent's turn finishes,
43
+ * then returns the whole transcript at once. There is no streaming here on
44
+ * purpose — one JSON reply is what a script can actually consume, and the tool
45
+ * lines come back in `events` so `--verbose` can show the work.
46
+ */
47
+ export async function treehouse(args, opts = {}) {
48
+ const room = (args[0] || '').trim().toLowerCase();
49
+ const message = args.slice(1).join(' ').trim();
50
+ if (!room || !message) {
51
+ console.error(`Usage: beehaven treehouse <${ROOMS.join('|')}> "<message>"`);
52
+ console.error(' beehaven treehouse gemini "design a landing page for a hair salon"');
53
+ process.exit(1);
54
+ }
55
+ if (!ROOMS.includes(room)) {
56
+ // Name the valid set rather than just refusing — the same reason
57
+ // canvas.create-node's error lists its types.
58
+ console.error(`Unknown room '${room}'. Rooms: ${ROOMS.join(', ')}`);
59
+ process.exit(1);
60
+ }
61
+ const token = readToken();
62
+ if (!token) {
63
+ console.error(c.red('✗') + ` no browser token at ${TOKEN_PATH}`);
64
+ console.error(' The Clearly desktop app writes it on launch — open the app, then retry.');
65
+ process.exit(1);
66
+ }
67
+ const port = opts.port ?? DEFAULT_PORT;
68
+ const s = spinner(`${room} is thinking…`).start();
69
+ const ctl = new AbortController();
70
+ const timer = setTimeout(() => ctl.abort(), opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
71
+ let body;
72
+ try {
73
+ const r = await fetch(`http://127.0.0.1:${port}/treehouse`, {
74
+ method: 'POST',
75
+ headers: { 'Content-Type': 'application/json', 'X-Clearly-Browser-Token': token },
76
+ body: JSON.stringify({ agent: room, message, model: opts.model, sessionId: opts.session }),
77
+ signal: ctl.signal,
78
+ });
79
+ body = await r.json().catch(() => ({ error: `HTTP ${r.status}` }));
80
+ if (r.status === 404) {
81
+ s.fail('the treehouse bridge is developer-mode only');
82
+ console.error(' Turn it on: HIVE_DEV_MODE=1 in the app\'s environment, or the tray\'s Settings entry, then relaunch.');
83
+ process.exit(1);
84
+ }
85
+ }
86
+ catch (err) {
87
+ clearTimeout(timer);
88
+ if (err?.name === 'AbortError') {
89
+ s.fail(`timed out after ${(opts.timeoutMs ?? DEFAULT_TIMEOUT_MS) / 1000}s`);
90
+ process.exit(1);
91
+ }
92
+ s.fail(`cannot reach the desktop app on 127.0.0.1:${port} — is it running?`);
93
+ process.exit(1);
94
+ }
95
+ clearTimeout(timer);
96
+ const events = body.events ?? [];
97
+ const tools = events.filter((e) => e.kind === 'tool_call' || e.kind === 'tool_result');
98
+ s.succeed(`${room} · ${tools.length} tool call(s)`);
99
+ if (opts.verbose) {
100
+ for (const e of events) {
101
+ if (e.kind === 'tool_call')
102
+ console.log(c.dim(` → ${e.tool}${e.detail ? ' ' + e.detail : ''}`));
103
+ else if (e.kind === 'tool_result')
104
+ console.log(c.dim(` ${e.ok ? '✓' : '✗'} ${e.tool}${e.preview ? ' ' + e.preview : ''}`));
105
+ else if (e.kind === 'error')
106
+ console.log(c.red(` ✗ ${e.message}`));
107
+ }
108
+ if (events.length)
109
+ console.log('');
110
+ }
111
+ // An error is worth surfacing even when some text came back: a room that hit an
112
+ // auth wall halfway still returns prose, and printing only the prose would read
113
+ // as success.
114
+ if (body.error)
115
+ console.error(c.red('✗ ') + body.error);
116
+ if (body.finalText)
117
+ console.log(body.finalText);
118
+ if (!body.ok)
119
+ process.exit(1);
120
+ }