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