@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,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
|
+
}
|