@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,409 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* connect-session — Chatroom-style persistent session.
|
|
3
|
+
*
|
|
4
|
+
* `beehaven connect <app>` joins you into a room where all the app's
|
|
5
|
+
* buildings are present. Events stream in like chat messages, commands
|
|
6
|
+
* are addressed as `building.action` or just `action`, and other
|
|
7
|
+
* participants (Claude, browser, other terminals) are visible.
|
|
8
|
+
*
|
|
9
|
+
* Architecture:
|
|
10
|
+
* CLI readline → persistent IpcClient → daemon → buildings (local/cloud)
|
|
11
|
+
* buildings → daemon broadcasts → IpcClient → rendered as chat feed
|
|
12
|
+
*/
|
|
13
|
+
import { connectClient } from '../ipc-client.js';
|
|
14
|
+
import { color } from '../spinner.js';
|
|
15
|
+
// ── Event rendering ──────────────────────────────────────────────────────
|
|
16
|
+
/** Format a broadcast event as a chat-style line */
|
|
17
|
+
function renderEvent(msg, rl) {
|
|
18
|
+
// Determine source label
|
|
19
|
+
const source = msg.building ?? msg.source ?? msg.type?.split(':')[0] ?? '?';
|
|
20
|
+
// Skip noisy internal events
|
|
21
|
+
const type = msg.type ?? '';
|
|
22
|
+
if (type === 'pong' || type === 'status')
|
|
23
|
+
return;
|
|
24
|
+
// Clear the current prompt line, print the event, redraw prompt
|
|
25
|
+
const prefix = color.dim(` [${source}]`);
|
|
26
|
+
switch (type) {
|
|
27
|
+
case 'pipeline:start':
|
|
28
|
+
write(rl, `${prefix} ${color.cyan('pipeline')} ${msg.title ?? ''} (${msg.stepCount} steps)`);
|
|
29
|
+
break;
|
|
30
|
+
case 'pipeline:step':
|
|
31
|
+
if (msg.phase === 'start') {
|
|
32
|
+
write(rl, `${prefix} ${color.dim('step')} ${msg.label}...`);
|
|
33
|
+
}
|
|
34
|
+
else if (msg.phase === 'ok') {
|
|
35
|
+
write(rl, `${prefix} ${color.green('ok')} ${msg.label}`);
|
|
36
|
+
}
|
|
37
|
+
else if (msg.phase === 'error') {
|
|
38
|
+
write(rl, `${prefix} ${color.red('error')} ${msg.label}: ${msg.error}`);
|
|
39
|
+
}
|
|
40
|
+
break;
|
|
41
|
+
case 'pipeline:end':
|
|
42
|
+
write(rl, `${prefix} pipeline ${msg.ok ? color.green('done') : color.red('failed')}`);
|
|
43
|
+
break;
|
|
44
|
+
case 'building:connected':
|
|
45
|
+
write(rl, `${prefix} ${color.green('connected')} ${msg.buildingType}/${msg.buildingId}`);
|
|
46
|
+
break;
|
|
47
|
+
case 'building:disconnected':
|
|
48
|
+
write(rl, `${prefix} ${color.dim('disconnected')}`);
|
|
49
|
+
break;
|
|
50
|
+
case 'session:join':
|
|
51
|
+
write(rl, ` ${color.dim('-->')} ${color.bold(msg.label ?? msg.clientId)} joined`);
|
|
52
|
+
break;
|
|
53
|
+
case 'session:leave':
|
|
54
|
+
write(rl, ` ${color.dim('<--')} ${color.bold(msg.label ?? msg.clientId)} left`);
|
|
55
|
+
break;
|
|
56
|
+
case 'agent:text':
|
|
57
|
+
case 'agent:chunk':
|
|
58
|
+
write(rl, ` ${color.cyan('[claude]')} ${msg.text ?? msg.chunk ?? ''}`);
|
|
59
|
+
break;
|
|
60
|
+
case 'agent:tool-call':
|
|
61
|
+
write(rl, ` ${color.cyan('[claude]')} ${color.dim('calling')} ${msg.toolName}(${JSON.stringify(msg.args ?? {}).slice(0, 80)})`);
|
|
62
|
+
break;
|
|
63
|
+
case 'stream:chunk':
|
|
64
|
+
// AI streaming — print without newline prefix for continuation
|
|
65
|
+
process.stdout.write(msg.chunk ?? msg.text ?? '');
|
|
66
|
+
break;
|
|
67
|
+
case 'stream:complete':
|
|
68
|
+
write(rl, ''); // newline after stream
|
|
69
|
+
break;
|
|
70
|
+
case 'cloud-app-rpc:forward':
|
|
71
|
+
write(rl, `${prefix} ${color.dim('->')} cloud ${msg.action}`);
|
|
72
|
+
break;
|
|
73
|
+
case 'cloud-app-rpc:result':
|
|
74
|
+
write(rl, `${prefix} ${color.dim('<-')} cloud ${msg.action} ${msg.ok ? color.green('ok') : color.red('failed')}`);
|
|
75
|
+
break;
|
|
76
|
+
default: {
|
|
77
|
+
// Generic event — show type and a compact summary
|
|
78
|
+
const summary = msg.text ?? msg.message ?? msg.error ?? '';
|
|
79
|
+
if (type.includes(':')) {
|
|
80
|
+
write(rl, `${prefix} ${type.split(':').pop()} ${summary}`);
|
|
81
|
+
}
|
|
82
|
+
else if (summary) {
|
|
83
|
+
write(rl, `${prefix} ${summary}`);
|
|
84
|
+
}
|
|
85
|
+
// Truly internal/noisy events with no summary are silently dropped
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/** Write a line without clobbering the readline prompt */
|
|
91
|
+
function write(rl, text) {
|
|
92
|
+
// Move to column 0, clear line, print, then redraw prompt
|
|
93
|
+
process.stdout.write(`\r\x1b[K${text}\n`);
|
|
94
|
+
if (rl?.prompt)
|
|
95
|
+
rl.prompt(true);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Parse a chat-room command line. Supports:
|
|
99
|
+
* ableton.play → building=ableton, action=play
|
|
100
|
+
* ableton play → building=ableton, action=play
|
|
101
|
+
* play → auto-resolve building from actionMap
|
|
102
|
+
* play {"speed": 1.5} → with JSON input
|
|
103
|
+
*/
|
|
104
|
+
function parseCommand(line, ctx) {
|
|
105
|
+
const trimmed = line.trim();
|
|
106
|
+
if (!trimmed)
|
|
107
|
+
return null;
|
|
108
|
+
// Split: first token is [building.]action, rest is optional JSON
|
|
109
|
+
let firstToken;
|
|
110
|
+
let rest;
|
|
111
|
+
const spaceIdx = trimmed.indexOf(' ');
|
|
112
|
+
if (spaceIdx === -1) {
|
|
113
|
+
firstToken = trimmed;
|
|
114
|
+
rest = '';
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
firstToken = trimmed.slice(0, spaceIdx);
|
|
118
|
+
rest = trimmed.slice(spaceIdx + 1).trim();
|
|
119
|
+
}
|
|
120
|
+
// Parse JSON input if present
|
|
121
|
+
let input = {};
|
|
122
|
+
if (rest) {
|
|
123
|
+
try {
|
|
124
|
+
input = JSON.parse(rest);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// Treat as a string arg for convenience: `ask "how are you"` → { text: rest }
|
|
128
|
+
input = { text: rest };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// Resolve building.action or just action
|
|
132
|
+
let building;
|
|
133
|
+
let action;
|
|
134
|
+
if (firstToken.includes('.')) {
|
|
135
|
+
const dotIdx = firstToken.indexOf('.');
|
|
136
|
+
building = firstToken.slice(0, dotIdx);
|
|
137
|
+
action = firstToken.slice(dotIdx + 1);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
action = firstToken;
|
|
141
|
+
// Try to auto-resolve from action map
|
|
142
|
+
const resolved = ctx.actionMap.get(action);
|
|
143
|
+
if (resolved) {
|
|
144
|
+
building = resolved;
|
|
145
|
+
}
|
|
146
|
+
else if (resolved === null) {
|
|
147
|
+
// Ambiguous — action exists on multiple buildings
|
|
148
|
+
const owners = ctx.buildings.filter((b) => b.actions.includes(action)).map((b) => b.name);
|
|
149
|
+
console.error(` ${color.yellow('?')} "${action}" exists on multiple buildings: ${owners.join(', ')}`);
|
|
150
|
+
console.error(` Use: ${owners[0]}.${action}`);
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
else if (ctx.buildings.length === 1) {
|
|
154
|
+
// Only one building — always route there
|
|
155
|
+
building = ctx.buildings[0].name;
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
console.error(` ${color.yellow('?')} Unknown action "${action}". Try "help" to see available actions.`);
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return { building, action, input };
|
|
163
|
+
}
|
|
164
|
+
// ── Session lifecycle ────────────────────────────────────────────────────
|
|
165
|
+
/**
|
|
166
|
+
* Fetch manifests for all buildings in an app and build the session context.
|
|
167
|
+
*/
|
|
168
|
+
async function buildSessionContext(client, appName, buildingNames) {
|
|
169
|
+
const buildings = [];
|
|
170
|
+
const actionMap = new Map();
|
|
171
|
+
for (const name of buildingNames) {
|
|
172
|
+
try {
|
|
173
|
+
const manifest = await client.appCall('manifest', {}, {
|
|
174
|
+
building: name,
|
|
175
|
+
instanceId: 'live',
|
|
176
|
+
timeout: 5_000,
|
|
177
|
+
});
|
|
178
|
+
const categories = manifest?.categories ?? {};
|
|
179
|
+
const actions = Object.values(categories)
|
|
180
|
+
.flat()
|
|
181
|
+
.map((a) => a.name);
|
|
182
|
+
buildings.push({
|
|
183
|
+
name,
|
|
184
|
+
actions,
|
|
185
|
+
description: manifest?.description,
|
|
186
|
+
});
|
|
187
|
+
// Build action → building map (null = ambiguous)
|
|
188
|
+
for (const a of actions) {
|
|
189
|
+
if (actionMap.has(a)) {
|
|
190
|
+
actionMap.set(a, null); // ambiguous
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
actionMap.set(a, name);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
// Building not responding — still add it with no actions
|
|
199
|
+
buildings.push({ name, actions: [] });
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return { appName, buildings, client, actionMap };
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Enter a persistent chatroom session connected to an app's buildings.
|
|
206
|
+
* This is the heart of `beehaven connect <app>`.
|
|
207
|
+
*/
|
|
208
|
+
export async function connectSession(opts) {
|
|
209
|
+
const { appName, buildingNames, json, ensureLocalApps = true } = opts;
|
|
210
|
+
// 1. Persistent connection to daemon
|
|
211
|
+
const client = await connectClient({ timeout: 10_000 });
|
|
212
|
+
// 2. Ensure local apps are loaded + running on the daemon
|
|
213
|
+
if (ensureLocalApps) {
|
|
214
|
+
try {
|
|
215
|
+
const appStatus = await client.call('daemon:app-ensure', {});
|
|
216
|
+
if (!appStatus?.running) {
|
|
217
|
+
console.error(` ${color.yellow('!')} Local app server did not start — local buildings may not respond`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
// Daemon may not support this RPC (older version) — continue anyway
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// 3. Subscribe to all building events + global events
|
|
225
|
+
const patterns = [
|
|
226
|
+
...buildingNames.map((b) => `${b}:*`),
|
|
227
|
+
'pipeline:*',
|
|
228
|
+
'agent:*',
|
|
229
|
+
'stream:*',
|
|
230
|
+
'session:*',
|
|
231
|
+
'building:*',
|
|
232
|
+
'cloud-app-rpc:*',
|
|
233
|
+
];
|
|
234
|
+
client.subscribe(patterns);
|
|
235
|
+
// 4. Build session context (fetch manifests, build action map)
|
|
236
|
+
const ctx = await buildSessionContext(client, appName, buildingNames);
|
|
237
|
+
// 5. Announce ourselves to the daemon
|
|
238
|
+
client.send({ type: 'session:announce', label: 'terminal', app: appName });
|
|
239
|
+
// ── Piped / JSON mode (for Claude and scripts) ─────────────────────
|
|
240
|
+
if (json || !process.stdin.isTTY) {
|
|
241
|
+
return pipedSession(client, ctx);
|
|
242
|
+
}
|
|
243
|
+
// ── Interactive mode ───────────────────────────────────────────────
|
|
244
|
+
return interactiveSession(client, ctx);
|
|
245
|
+
}
|
|
246
|
+
// ── Interactive TTY session ──────────────────────────────────────────────
|
|
247
|
+
async function interactiveSession(client, ctx) {
|
|
248
|
+
const { createInterface } = await import('readline');
|
|
249
|
+
// Print welcome
|
|
250
|
+
console.log('');
|
|
251
|
+
console.log(` ${color.bold(ctx.appName)} ${color.dim('—')} ${ctx.buildings.length} building${ctx.buildings.length === 1 ? '' : 's'} online`);
|
|
252
|
+
for (const b of ctx.buildings) {
|
|
253
|
+
const actionCount = b.actions.length;
|
|
254
|
+
const desc = b.description ? ` ${color.dim(b.description)}` : '';
|
|
255
|
+
console.log(` ${color.cyan(b.name.padEnd(16))} ${actionCount} actions${desc}`);
|
|
256
|
+
}
|
|
257
|
+
console.log('');
|
|
258
|
+
console.log(` ${color.dim('Type building.action or just action. Tab to complete. "help" for actions. Ctrl+C to leave.')}`);
|
|
259
|
+
console.log('');
|
|
260
|
+
// Collect all action names for tab completion (building.action and bare action)
|
|
261
|
+
const completions = [];
|
|
262
|
+
for (const b of ctx.buildings) {
|
|
263
|
+
for (const a of b.actions) {
|
|
264
|
+
completions.push(`${b.name}.${a}`);
|
|
265
|
+
if (ctx.actionMap.get(a) !== null) {
|
|
266
|
+
completions.push(a); // unambiguous shorthand
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
completions.push('help', 'status', 'exit', 'quit');
|
|
271
|
+
const rl = createInterface({
|
|
272
|
+
input: process.stdin,
|
|
273
|
+
output: process.stdout,
|
|
274
|
+
prompt: `${ctx.appName}> `,
|
|
275
|
+
completer: (line) => {
|
|
276
|
+
const hits = completions.filter((c) => c.startsWith(line));
|
|
277
|
+
return [hits.length ? hits : completions, line];
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
// Wire up broadcast events → chat feed
|
|
281
|
+
client.on('broadcast', (msg) => {
|
|
282
|
+
renderEvent(msg, rl);
|
|
283
|
+
});
|
|
284
|
+
rl.prompt();
|
|
285
|
+
rl.on('line', async (line) => {
|
|
286
|
+
const trimmed = line.trim();
|
|
287
|
+
if (!trimmed) {
|
|
288
|
+
rl.prompt();
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
// Built-in commands
|
|
292
|
+
if (trimmed === 'exit' || trimmed === 'quit') {
|
|
293
|
+
client.disconnect();
|
|
294
|
+
process.exit(0);
|
|
295
|
+
}
|
|
296
|
+
if (trimmed === 'help') {
|
|
297
|
+
for (const b of ctx.buildings) {
|
|
298
|
+
console.log(` ${color.bold(b.name)}:`);
|
|
299
|
+
const cols = b.actions.length > 12 ? formatColumns(b.actions, 3) : b.actions.map((a) => ` ${a}`);
|
|
300
|
+
for (const line of cols)
|
|
301
|
+
console.log(line);
|
|
302
|
+
console.log('');
|
|
303
|
+
}
|
|
304
|
+
rl.prompt();
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
if (trimmed === 'status') {
|
|
308
|
+
for (const b of ctx.buildings) {
|
|
309
|
+
try {
|
|
310
|
+
const ctxResult = await client.appCall('context', {}, {
|
|
311
|
+
building: b.name,
|
|
312
|
+
instanceId: 'live',
|
|
313
|
+
timeout: 5_000,
|
|
314
|
+
});
|
|
315
|
+
console.log(` ${color.cyan(b.name)}: ${JSON.stringify(ctxResult, null, 2)}`);
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
console.log(` ${color.cyan(b.name)}: ${color.dim('no context')}`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
rl.prompt();
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
// Parse and execute command
|
|
325
|
+
const cmd = parseCommand(trimmed, ctx);
|
|
326
|
+
if (!cmd) {
|
|
327
|
+
rl.prompt();
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
try {
|
|
331
|
+
const result = await client.appCall(cmd.action, cmd.input, {
|
|
332
|
+
building: cmd.building,
|
|
333
|
+
instanceId: 'live',
|
|
334
|
+
});
|
|
335
|
+
if (result && typeof result === 'object') {
|
|
336
|
+
console.log(JSON.stringify(result, null, 2));
|
|
337
|
+
}
|
|
338
|
+
else if (result !== undefined && result !== null) {
|
|
339
|
+
console.log(result);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
catch (err) {
|
|
343
|
+
console.error(` ${color.red('x')} ${err.message}`);
|
|
344
|
+
}
|
|
345
|
+
rl.prompt();
|
|
346
|
+
});
|
|
347
|
+
rl.on('close', () => {
|
|
348
|
+
client.disconnect();
|
|
349
|
+
process.exit(0);
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
// ── Piped / JSON session (for Claude and scripts) ────────────────────────
|
|
353
|
+
async function pipedSession(client, ctx) {
|
|
354
|
+
const { createInterface } = await import('readline');
|
|
355
|
+
// In piped mode, also print broadcast events as JSON lines
|
|
356
|
+
client.on('broadcast', (msg) => {
|
|
357
|
+
const type = msg?.type ?? '';
|
|
358
|
+
if (type === 'pong' || type === 'status')
|
|
359
|
+
return;
|
|
360
|
+
console.log(JSON.stringify({ event: type, ...msg }));
|
|
361
|
+
});
|
|
362
|
+
// Print session info first
|
|
363
|
+
console.log(JSON.stringify({
|
|
364
|
+
session: ctx.appName,
|
|
365
|
+
buildings: ctx.buildings.map((b) => ({
|
|
366
|
+
name: b.name,
|
|
367
|
+
actions: b.actions,
|
|
368
|
+
description: b.description,
|
|
369
|
+
})),
|
|
370
|
+
}));
|
|
371
|
+
const rl = createInterface({ input: process.stdin, terminal: false });
|
|
372
|
+
const lines = [];
|
|
373
|
+
await new Promise((resolve) => {
|
|
374
|
+
rl.on('line', (line) => {
|
|
375
|
+
const trimmed = line.trim();
|
|
376
|
+
if (trimmed)
|
|
377
|
+
lines.push(trimmed);
|
|
378
|
+
});
|
|
379
|
+
rl.on('close', resolve);
|
|
380
|
+
});
|
|
381
|
+
for (const line of lines) {
|
|
382
|
+
const cmd = parseCommand(line, ctx);
|
|
383
|
+
if (!cmd) {
|
|
384
|
+
console.log(JSON.stringify({ error: `Could not parse: ${line}` }));
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
try {
|
|
388
|
+
const result = await client.appCall(cmd.action, cmd.input, {
|
|
389
|
+
building: cmd.building,
|
|
390
|
+
instanceId: 'live',
|
|
391
|
+
});
|
|
392
|
+
console.log(JSON.stringify({ action: cmd.action, building: cmd.building, result }));
|
|
393
|
+
}
|
|
394
|
+
catch (err) {
|
|
395
|
+
console.log(JSON.stringify({ action: cmd.action, building: cmd.building, error: err.message }));
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
client.disconnect();
|
|
399
|
+
}
|
|
400
|
+
// ── Helpers ──────────────────────────────────────────────────────────────
|
|
401
|
+
function formatColumns(items, cols) {
|
|
402
|
+
const maxLen = Math.max(...items.map((s) => s.length));
|
|
403
|
+
const lines = [];
|
|
404
|
+
for (let i = 0; i < items.length; i += cols) {
|
|
405
|
+
const row = items.slice(i, i + cols).map((s) => s.padEnd(maxLen + 2));
|
|
406
|
+
lines.push(' ' + row.join(''));
|
|
407
|
+
}
|
|
408
|
+
return lines;
|
|
409
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `beehaven conversations` (#109) — list / resume / export Clearly
|
|
3
|
+
* FAB conversations from the CLI.
|
|
4
|
+
*
|
|
5
|
+
* Wraps three workspace RPCs:
|
|
6
|
+
* - `conversation-list` — recent sessions with librarian titles
|
|
7
|
+
* - `conversation-export` — render a session as markdown
|
|
8
|
+
* - (resume is client-side: pick the top row + print the sessionId)
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
*
|
|
12
|
+
* beehaven conversations # list 20 most recent
|
|
13
|
+
* beehaven conversations --limit 50
|
|
14
|
+
* beehaven conversations --composition <id> # filter to one comp
|
|
15
|
+
* beehaven conversations resume # print most recent sessionId
|
|
16
|
+
* beehaven conversations export <sessionId> # write conversation .md to cwd
|
|
17
|
+
* beehaven conversations export <sessionId> --out path.md
|
|
18
|
+
* beehaven conversations export <sessionId> --stdout
|
|
19
|
+
*
|
|
20
|
+
* Output: pretty table by default, `--json` for machine-readable.
|
|
21
|
+
*/
|
|
22
|
+
import { rpcOnce } from '../ipc-client.js';
|
|
23
|
+
import { writeFileSync } from 'node:fs';
|
|
24
|
+
import { resolve as resolvePath } from 'node:path';
|
|
25
|
+
export async function cmdConversations(sub, positionals, values) {
|
|
26
|
+
switch (sub) {
|
|
27
|
+
case 'list':
|
|
28
|
+
case undefined:
|
|
29
|
+
case '':
|
|
30
|
+
return cmdConversationsList(values);
|
|
31
|
+
case 'resume':
|
|
32
|
+
return cmdConversationsResume(values);
|
|
33
|
+
case 'export':
|
|
34
|
+
return cmdConversationsExport(positionals, values);
|
|
35
|
+
case 'help':
|
|
36
|
+
return printHelp();
|
|
37
|
+
default:
|
|
38
|
+
// Treat unknown sub as a sessionId for export — common shorthand.
|
|
39
|
+
if (sub.startsWith('appr-') || sub.length > 8) {
|
|
40
|
+
return cmdConversationsExport([sub, ...positionals], values);
|
|
41
|
+
}
|
|
42
|
+
printHelp();
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function printHelp() {
|
|
47
|
+
console.log(`Usage:
|
|
48
|
+
beehaven conversations # list 20 most recent
|
|
49
|
+
beehaven conversations --limit 50
|
|
50
|
+
beehaven conversations --composition <id> # filter to a canvas
|
|
51
|
+
beehaven conversations resume # print most recent sessionId
|
|
52
|
+
beehaven conversations export <sessionId> # write .md to cwd
|
|
53
|
+
beehaven conversations export <sessionId> --out path.md
|
|
54
|
+
beehaven conversations export <sessionId> --stdout
|
|
55
|
+
|
|
56
|
+
Flags:
|
|
57
|
+
--limit <N> max rows to fetch (default 20, max 100)
|
|
58
|
+
--composition <id> filter to one composition
|
|
59
|
+
--json output raw JSON
|
|
60
|
+
--out <path> export destination (default: derived from title)
|
|
61
|
+
--stdout write export to stdout instead of a file`);
|
|
62
|
+
}
|
|
63
|
+
/** List rows in a tight tabular layout. Title is the librarian's
|
|
64
|
+
* curated label — the most useful field to scan. Time-since-last
|
|
65
|
+
* is what tells you "yesterday's bug-fix thread" vs "this morning". */
|
|
66
|
+
async function cmdConversationsList(values) {
|
|
67
|
+
const limit = Math.max(1, Math.min(Number(values.limit) || 20, 100));
|
|
68
|
+
const compositionId = values.composition;
|
|
69
|
+
const input = { limit };
|
|
70
|
+
if (compositionId)
|
|
71
|
+
input.compositionId = compositionId;
|
|
72
|
+
const res = await rpcOnce('conversation-list', input);
|
|
73
|
+
if (!res?.ok) {
|
|
74
|
+
console.error(` conversation-list failed: ${res?.error ?? 'unknown error'}`);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
const rows = res.conversations ?? [];
|
|
78
|
+
if (values.json) {
|
|
79
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (rows.length === 0) {
|
|
83
|
+
console.log(' No conversations yet.');
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
// Render — fixed-width columns so resume / scroll feels readable.
|
|
87
|
+
console.log(` ${rows.length} conversation${rows.length === 1 ? '' : 's'}:\n`);
|
|
88
|
+
for (const r of rows) {
|
|
89
|
+
const when = formatRelative(r.lastTs);
|
|
90
|
+
const title = (r.title || '(untitled)').slice(0, 60);
|
|
91
|
+
const turns = `${r.turnCount} turn${r.turnCount === 1 ? '' : 's'}`;
|
|
92
|
+
console.log(` • ${title}`);
|
|
93
|
+
console.log(` ${r.sessionId} · ${when} · ${turns} · ${r.lastModel || '—'}`);
|
|
94
|
+
if (r.summary)
|
|
95
|
+
console.log(` ${r.summary.slice(0, 80)}`);
|
|
96
|
+
console.log('');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/** Print the most-recent sessionId. Designed for shell pipelines —
|
|
100
|
+
* `beehaven conversations export "$(beehaven conversations resume)"`. */
|
|
101
|
+
async function cmdConversationsResume(values) {
|
|
102
|
+
const res = await rpcOnce('conversation-list', { limit: 1 });
|
|
103
|
+
if (!res?.ok) {
|
|
104
|
+
console.error(` conversation-list failed: ${res?.error ?? 'unknown error'}`);
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
const top = res.conversations?.[0];
|
|
108
|
+
if (!top) {
|
|
109
|
+
console.error(' No conversations to resume.');
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
if (values.json) {
|
|
113
|
+
console.log(JSON.stringify(top, null, 2));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
// Print JUST the sessionId so it composes well with $(...) substitution.
|
|
117
|
+
console.log(top.sessionId);
|
|
118
|
+
}
|
|
119
|
+
async function cmdConversationsExport(positionals, values) {
|
|
120
|
+
const sessionId = positionals[0];
|
|
121
|
+
if (!sessionId) {
|
|
122
|
+
console.error(' Usage: beehaven conversations export <sessionId> [--out path.md] [--stdout]');
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
const res = await rpcOnce('conversation-export', { sessionId, format: 'markdown' });
|
|
126
|
+
if (!res?.ok) {
|
|
127
|
+
console.error(` conversation-export failed: ${res?.error ?? 'unknown error'}`);
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
if (values.json) {
|
|
131
|
+
console.log(JSON.stringify(res, null, 2));
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (values.stdout) {
|
|
135
|
+
process.stdout.write(res.markdown);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const outPath = values.out
|
|
139
|
+
? resolvePath(String(values.out))
|
|
140
|
+
: resolvePath(String(res.filename || `conversation-${sessionId}.md`));
|
|
141
|
+
writeFileSync(outPath, res.markdown, 'utf8');
|
|
142
|
+
console.log(` Exported ${res.turnCount} turn${res.turnCount === 1 ? '' : 's'} → ${outPath}`);
|
|
143
|
+
}
|
|
144
|
+
function formatRelative(ts) {
|
|
145
|
+
if (!ts)
|
|
146
|
+
return '';
|
|
147
|
+
const delta = Date.now() - ts;
|
|
148
|
+
const m = Math.floor(delta / 60_000);
|
|
149
|
+
if (m < 1)
|
|
150
|
+
return 'just now';
|
|
151
|
+
if (m < 60)
|
|
152
|
+
return `${m}m ago`;
|
|
153
|
+
const h = Math.floor(m / 60);
|
|
154
|
+
if (h < 24)
|
|
155
|
+
return `${h}h ago`;
|
|
156
|
+
const d = Math.floor(h / 24);
|
|
157
|
+
if (d < 7)
|
|
158
|
+
return `${d}d ago`;
|
|
159
|
+
return new Date(ts).toLocaleDateString();
|
|
160
|
+
}
|