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,171 @@
1
+ /**
2
+ * beehaven login/signup/logout — Beehaven account management.
3
+ *
4
+ * Credentials stored in ~/.beehaven/credentials.json
5
+ * All subsequent CLI commands use the stored API key.
6
+ */
7
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, unlinkSync } from 'fs';
8
+ import { join } from 'path';
9
+ import { homedir } from 'os';
10
+ import { resolveRelaySelection } from '../relay-presets.js';
11
+ const BEEHAVEN_API = process.env.BEEHAVEN_API_URL ?? 'https://api.beehaven.dev';
12
+ const CRED_DIR = join(homedir(), '.beehaven');
13
+ const CRED_FILE = join(CRED_DIR, 'credentials.json');
14
+ export function getCredentials() {
15
+ if (!existsSync(CRED_FILE))
16
+ return null;
17
+ try {
18
+ return JSON.parse(readFileSync(CRED_FILE, 'utf8'));
19
+ }
20
+ catch {
21
+ return null;
22
+ }
23
+ }
24
+ export function getApiKey() {
25
+ return getCredentials()?.apiKey ?? null;
26
+ }
27
+ export function readDaemonIdentity() {
28
+ const cfgPath = join(CRED_DIR, 'config.json');
29
+ if (!existsSync(cfgPath))
30
+ return null;
31
+ try {
32
+ const c = JSON.parse(readFileSync(cfgPath, 'utf8'));
33
+ // A TOKEN is what "logged in" means — not the presence of display fields.
34
+ //
35
+ // This gated on `!c.userId && !c.email`, and the Mac app (installedBy:
36
+ // 'electron' — i.e. how every consumer signs in) writes a token, a
37
+ // refreshToken and tokenStorage but NEITHER identity field. So this returned
38
+ // null, whoami fell through to the legacy self-hosted credentials.json,
39
+ // found nothing, and reported "Not logged in. Run `beehaven login`" to a
40
+ // user holding a perfectly good credential. `beehaven status` read the same
41
+ // file and said "Account: Connected" — two commands, one config, opposite
42
+ // answers.
43
+ //
44
+ // It is worst for AGENTS: told they are logged out, the prescribed fix is
45
+ // `beehaven login`, which opens a BROWSER. An agent cannot complete that and
46
+ // cannot know the advice was wrong. The identity fields are for DISPLAY;
47
+ // authority lives in the token.
48
+ if (!c.userId && !c.email && !c.token)
49
+ return null;
50
+ // No email on disk? Read it off the token for display. The id token is a JWT
51
+ // whose payload carries email/user_id, and this is the only place that
52
+ // matters — we are labelling a line of output, never authorizing. Unverified
53
+ // on purpose (verification is the server's job) and fully guarded: a
54
+ // malformed token must degrade to "(unknown)", never throw whoami.
55
+ let email = c.email ?? null;
56
+ let userId = c.userId ?? null;
57
+ if ((!email || !userId) && typeof c.token === 'string') {
58
+ try {
59
+ const seg = c.token.split('.')[1];
60
+ if (seg) {
61
+ const json = JSON.parse(Buffer.from(seg.replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString('utf8'));
62
+ email ??= json.email ?? null;
63
+ userId ??= json.user_id ?? json.sub ?? null;
64
+ }
65
+ }
66
+ catch { /* not a JWT / not decodable → keep what we had */ }
67
+ }
68
+ // Surface the CANONICAL relay/env (derived from `endpoint`), not the raw
69
+ // envName/relayUrl aliases — those can lag the real dial target after a
70
+ // fresh login / Mac-app install that writes only `endpoint`.
71
+ const relay = resolveRelaySelection(c);
72
+ return {
73
+ email,
74
+ userId,
75
+ displayName: c.displayName ?? null,
76
+ envName: relay.envName,
77
+ relayUrl: relay.url,
78
+ hasIdToken: !!c.token,
79
+ hasRefreshToken: !!c.refreshToken,
80
+ tokenStorage: c.tokenStorage ?? null,
81
+ };
82
+ }
83
+ catch {
84
+ return null;
85
+ }
86
+ }
87
+ function saveCredentials(creds) {
88
+ if (!existsSync(CRED_DIR))
89
+ mkdirSync(CRED_DIR, { recursive: true });
90
+ writeFileSync(CRED_FILE, JSON.stringify(creds, null, 2));
91
+ }
92
+ export async function signup(email, password) {
93
+ console.log('\n Creating Beehaven account...\n');
94
+ const res = await fetch(`${BEEHAVEN_API}/auth/signup`, {
95
+ method: 'POST',
96
+ headers: { 'Content-Type': 'application/json' },
97
+ body: JSON.stringify({ email, password }),
98
+ });
99
+ const data = (await res.json());
100
+ if (!res.ok) {
101
+ console.error(` Error: ${data.error ?? 'Signup failed'}`);
102
+ process.exit(1);
103
+ }
104
+ saveCredentials({
105
+ apiKey: data.apiKey,
106
+ userId: data.userId,
107
+ email,
108
+ plan: data.plan,
109
+ });
110
+ console.log(` Account created!`);
111
+ console.log(` Email: ${email}`);
112
+ console.log(` Plan: ${data.plan}`);
113
+ console.log(` API key saved to ~/.beehaven/credentials.json\n`);
114
+ }
115
+ export async function login(email, password) {
116
+ console.log('\n Logging in...\n');
117
+ const res = await fetch(`${BEEHAVEN_API}/auth/login`, {
118
+ method: 'POST',
119
+ headers: { 'Content-Type': 'application/json' },
120
+ body: JSON.stringify({ email, password }),
121
+ });
122
+ const data = (await res.json());
123
+ if (!res.ok) {
124
+ console.error(` Error: ${data.error ?? 'Login failed'}`);
125
+ process.exit(1);
126
+ }
127
+ saveCredentials({
128
+ apiKey: data.apiKey,
129
+ userId: data.userId,
130
+ email,
131
+ plan: data.plan,
132
+ });
133
+ console.log(` Logged in as ${email} (${data.plan} plan)\n`);
134
+ }
135
+ export function logout() {
136
+ if (existsSync(CRED_FILE)) {
137
+ unlinkSync(CRED_FILE);
138
+ console.log('\n Logged out. Credentials removed.\n');
139
+ }
140
+ else {
141
+ console.log('\n Not logged in.\n');
142
+ }
143
+ }
144
+ export function whoami() {
145
+ // Prefer the Firebase loopback identity (config.json) — that's what
146
+ // `beehaven login` writes today. Fall back to the legacy email/password
147
+ // credentials.json for self-hosted deploys still using that flow.
148
+ const daemon = readDaemonIdentity();
149
+ if (daemon) {
150
+ console.log('');
151
+ console.log(` Email: ${daemon.email ?? '(unknown)'}`);
152
+ console.log(` User ID: ${daemon.userId ?? '(unknown)'}`);
153
+ if (daemon.displayName)
154
+ console.log(` Name: ${daemon.displayName}`);
155
+ if (daemon.envName)
156
+ console.log(` Env: ${daemon.envName}`);
157
+ if (daemon.relayUrl)
158
+ console.log(` Relay: ${daemon.relayUrl}`);
159
+ console.log(` Tokens: ${daemon.hasIdToken ? 'idToken ✓' : '✗'} ${daemon.hasRefreshToken ? 'refreshToken ✓' : '✗'} (${daemon.tokenStorage ?? 'file'})`);
160
+ console.log('');
161
+ return;
162
+ }
163
+ const creds = getCredentials();
164
+ if (!creds) {
165
+ console.log('\n Not logged in. Run `beehaven login` or `beehaven signup`.\n');
166
+ return;
167
+ }
168
+ console.log(`\n Email: ${creds.email}`);
169
+ console.log(` Plan: ${creds.plan}`);
170
+ console.log(` API key: ${creds.apiKey.slice(0, 8)}****\n`);
171
+ }
@@ -0,0 +1,392 @@
1
+ /**
2
+ * beehaven <building> — Building shell.
3
+ *
4
+ * Three modes:
5
+ * 1. `beehaven ableton` → print context overview (orientation)
6
+ * 2. `beehaven ableton getSession` → one-shot RPC, return JSON
7
+ * 3. `beehaven ableton --shell` → interactive REPL (readline + tab complete)
8
+ *
9
+ * Piped stdin is also supported — Claude or scripts can pipe multiple
10
+ * commands and the shell executes them sequentially over a single
11
+ * persistent IPC connection:
12
+ *
13
+ * printf 'getSession\ncreateTrack {"type":"midi"}\n' | beehaven ableton --shell
14
+ */
15
+ import { readFileSync, existsSync, readdirSync } from 'fs';
16
+ import { join } from 'path';
17
+ import { homedir } from 'os';
18
+ import { appRpcOnce, connectClient } from '../ipc-client.js';
19
+ const APPS_DIR = join(homedir(), '.beehaven', 'apps');
20
+ // ── Helpers ──────────────────────────────────────────────────────────────
21
+ /** Collect all registered building names from installed app manifests. */
22
+ export function getInstalledBuildings() {
23
+ if (!existsSync(APPS_DIR))
24
+ return [];
25
+ const buildings = [];
26
+ for (const entry of readdirSync(APPS_DIR)) {
27
+ if (entry.startsWith('.'))
28
+ continue;
29
+ const manifestPath = join(APPS_DIR, entry, 'app.json');
30
+ if (!existsSync(manifestPath))
31
+ continue;
32
+ try {
33
+ const m = JSON.parse(readFileSync(manifestPath, 'utf8'));
34
+ if (m?.buildings) {
35
+ for (const name of Object.keys(m.buildings)) {
36
+ if (!buildings.includes(name))
37
+ buildings.push(name);
38
+ }
39
+ }
40
+ }
41
+ catch {
42
+ /* skip malformed */
43
+ }
44
+ }
45
+ return buildings;
46
+ }
47
+ /** Find recipes that depend on or target a specific building. */
48
+ function getRecipesForBuilding(building) {
49
+ if (!existsSync(APPS_DIR))
50
+ return [];
51
+ const recipes = [];
52
+ for (const entry of readdirSync(APPS_DIR)) {
53
+ if (entry.startsWith('.'))
54
+ continue;
55
+ const manifestPath = join(APPS_DIR, entry, 'app.json');
56
+ if (!existsSync(manifestPath))
57
+ continue;
58
+ let m;
59
+ try {
60
+ m = JSON.parse(readFileSync(manifestPath, 'utf8'));
61
+ }
62
+ catch {
63
+ continue;
64
+ }
65
+ if (!m?.recipes)
66
+ continue;
67
+ // Check if this app's recipes reference the building
68
+ for (const [rName, rPath] of Object.entries(m.recipes)) {
69
+ let title = rName;
70
+ let targets = false;
71
+ const rFile = typeof rPath === 'string' ? join(APPS_DIR, entry, rPath) : null;
72
+ if (rFile && existsSync(rFile)) {
73
+ try {
74
+ const recipe = JSON.parse(readFileSync(rFile, 'utf8'));
75
+ title = recipe.title ?? rName;
76
+ // Check if any step targets this building
77
+ if (Array.isArray(recipe.steps)) {
78
+ targets = recipe.steps.some((s) => s.building === building || (!s.building && m.buildings?.[building]));
79
+ }
80
+ }
81
+ catch { }
82
+ }
83
+ // Include if the app owns the building or recipe targets it
84
+ if (m.buildings?.[building] || targets) {
85
+ recipes.push({ app: m.name, recipe: rName, title });
86
+ }
87
+ }
88
+ }
89
+ return recipes;
90
+ }
91
+ /** Format a compact track list. */
92
+ function formatTrackList(names) {
93
+ const lines = [];
94
+ const cols = 3;
95
+ for (let i = 0; i < names.length; i += cols) {
96
+ const row = names.slice(i, i + cols).map((name, j) => {
97
+ const idx = String(i + j).padStart(2);
98
+ const truncated = name.length > 22 ? name.slice(0, 20) + '..' : name;
99
+ return ` ${idx} ${truncated}`;
100
+ });
101
+ lines.push(row.map((s) => s.padEnd(30)).join(''));
102
+ }
103
+ return lines.join('\n');
104
+ }
105
+ /** Format category → action names compactly. */
106
+ function formatActionSummary(categories) {
107
+ const lines = [];
108
+ for (const [cat, actions] of Object.entries(categories)) {
109
+ const names = actions.map((a) => a.name);
110
+ // Show first few, then "..."
111
+ const shown = names.length > 6 ? [...names.slice(0, 5), `+${names.length - 5} more`] : names;
112
+ lines.push(` ${cat.padEnd(14)} ${shown.join(', ')}`);
113
+ }
114
+ return lines.join('\n');
115
+ }
116
+ // ── Mode 1: Context Overview ─────────────────────────────────────────────
117
+ export async function buildingContext(building, instanceId) {
118
+ let manifest;
119
+ let context;
120
+ try {
121
+ [manifest, context] = await Promise.all([
122
+ appRpcOnce('manifest', {}, { building, instanceId, timeout: 8_000 }),
123
+ appRpcOnce('context', {}, { building, instanceId, timeout: 8_000 }).catch(() => null),
124
+ ]);
125
+ }
126
+ catch (err) {
127
+ console.error(` ✗ Cannot connect to ${building}/${instanceId}: ${err.message}`);
128
+ console.error(' Is the daemon running? Run `beehaven start --headless` (or open the Clearly Dev Mac app).');
129
+ process.exit(1);
130
+ }
131
+ const categories = manifest.categories ?? {};
132
+ const actionCount = manifest.actionCount ?? manifest.totalActions ?? 0;
133
+ const desc = manifest.description ?? '';
134
+ // Session state (building-specific — Ableton has session, others may not)
135
+ const session = context?.session;
136
+ const recentCommands = context?.recentCommands ?? [];
137
+ console.log('');
138
+ console.log(` ${building} — ${desc}`);
139
+ console.log(' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
140
+ // Live state
141
+ if (session) {
142
+ const parts = [
143
+ `${session.tempo} BPM`,
144
+ `${session.signatureNumerator}/${session.signatureDenominator}`,
145
+ `${session.numTracks} tracks`,
146
+ `${session.numScenes} scenes`,
147
+ session.isPlaying ? 'playing' : 'stopped',
148
+ session.loop ? `loop ${session.loopStart}..${session.loopStart + session.loopLength}` : '',
149
+ ].filter(Boolean);
150
+ console.log(` ${parts.join(', ')}`);
151
+ console.log('');
152
+ if (session.trackNames?.length) {
153
+ console.log(' Tracks:');
154
+ console.log(formatTrackList(session.trackNames));
155
+ console.log('');
156
+ }
157
+ }
158
+ // Actions by category
159
+ console.log(` Actions (${actionCount}):`);
160
+ console.log(formatActionSummary(categories));
161
+ console.log('');
162
+ // Recipes
163
+ const recipes = getRecipesForBuilding(building);
164
+ if (recipes.length > 0) {
165
+ console.log(' Recipes:');
166
+ for (const r of recipes) {
167
+ console.log(` beehaven app run ${r.app}/${r.recipe}`.padEnd(48) + r.title);
168
+ }
169
+ console.log('');
170
+ }
171
+ // Recent activity
172
+ if (recentCommands.length > 0) {
173
+ const recent = recentCommands.slice(0, 5);
174
+ console.log(' Recent:');
175
+ for (const cmd of recent) {
176
+ const time = cmd.created_at?.split(' ')[1] ?? '';
177
+ console.log(` ${time} ${cmd.action}`);
178
+ }
179
+ console.log('');
180
+ }
181
+ // Usage
182
+ console.log(' Usage:');
183
+ console.log(` beehaven ${building} <action> [json] Run an action`);
184
+ console.log(` beehaven ${building} --shell Interactive REPL`);
185
+ console.log(` beehaven ${building} --actions Full action reference`);
186
+ console.log('');
187
+ // Examples (building-specific)
188
+ if (building === 'ableton') {
189
+ console.log(' Examples:');
190
+ console.log(` beehaven ableton getSession`);
191
+ console.log(` beehaven ableton createTrack '{"type":"midi","name":"Strings"}'`);
192
+ console.log(` beehaven ableton composeToClip '{"trackIndex":0,"clipIndex":0,"generator":"generateMelody","generatorParams":{"root":"C","scale":"minor","length":16}}'`);
193
+ console.log(` beehaven ableton play`);
194
+ console.log('');
195
+ }
196
+ }
197
+ // ── Mode 2: One-Shot RPC ─────────────────────────────────────────────────
198
+ export async function buildingOneShot(building, action, rawInput, instanceId, timeout) {
199
+ let input = {};
200
+ if (rawInput) {
201
+ try {
202
+ input = JSON.parse(rawInput);
203
+ }
204
+ catch {
205
+ console.error(` ✗ Invalid JSON: ${rawInput}`);
206
+ process.exit(1);
207
+ }
208
+ }
209
+ try {
210
+ const result = await appRpcOnce(action, input, { building, instanceId, timeout });
211
+ if (result && typeof result === 'object') {
212
+ console.log(JSON.stringify(result, null, 2));
213
+ }
214
+ else {
215
+ console.log(result);
216
+ }
217
+ }
218
+ catch (err) {
219
+ console.error(JSON.stringify({ error: err.message }, null, 2));
220
+ process.exit(1);
221
+ }
222
+ }
223
+ // ── Mode 3: Interactive Shell / Piped Shell ──────────────────────────────
224
+ export async function buildingShell(building, instanceId) {
225
+ const client = await connectClient({ timeout: 10_000 });
226
+ // Fetch manifest for tab completion
227
+ let actionNames = [];
228
+ let categories = {};
229
+ try {
230
+ const manifest = await client.appCall('manifest', {}, { building, instanceId });
231
+ categories = manifest.categories ?? {};
232
+ actionNames = Object.values(categories)
233
+ .flat()
234
+ .map((a) => a.name);
235
+ }
236
+ catch {
237
+ /* proceed without completion */
238
+ }
239
+ const isTTY = process.stdin.isTTY;
240
+ if (isTTY) {
241
+ // Interactive mode — readline with tab completion and prompt
242
+ const { createInterface } = await import('readline');
243
+ // Print compact welcome
244
+ try {
245
+ const ctx = await client.appCall('context', {}, { building, instanceId, timeout: 5_000 });
246
+ const session = ctx?.session;
247
+ if (session) {
248
+ const parts = [
249
+ `${session.tempo} BPM`,
250
+ `${session.numTracks} tracks`,
251
+ session.isPlaying ? 'playing' : 'stopped',
252
+ ].filter(Boolean);
253
+ console.log(` ${building} — ${parts.join(', ')}`);
254
+ }
255
+ else {
256
+ console.log(` ${building} — connected`);
257
+ }
258
+ }
259
+ catch {
260
+ console.log(` ${building} — connected`);
261
+ }
262
+ console.log(` ${actionNames.length} actions available. Tab to complete. "help" for categories. Ctrl+C to exit.`);
263
+ console.log('');
264
+ const rl = createInterface({
265
+ input: process.stdin,
266
+ output: process.stdout,
267
+ prompt: `${building}> `,
268
+ completer: (line) => {
269
+ const hits = actionNames.filter((a) => a.startsWith(line));
270
+ return [hits.length ? hits : actionNames, line];
271
+ },
272
+ });
273
+ rl.prompt();
274
+ rl.on('line', async (line) => {
275
+ const trimmed = line.trim();
276
+ if (!trimmed) {
277
+ rl.prompt();
278
+ return;
279
+ }
280
+ if (trimmed === 'exit' || trimmed === 'quit') {
281
+ client.disconnect();
282
+ process.exit(0);
283
+ }
284
+ if (trimmed === 'help') {
285
+ for (const [cat, actions] of Object.entries(categories)) {
286
+ const names = actions.map((a) => a.name);
287
+ console.log(` ${cat}: ${names.join(', ')}`);
288
+ }
289
+ console.log('');
290
+ rl.prompt();
291
+ return;
292
+ }
293
+ // Parse: action [json]
294
+ const spaceIdx = trimmed.indexOf(' ');
295
+ const action = spaceIdx === -1 ? trimmed : trimmed.slice(0, spaceIdx);
296
+ const jsonStr = spaceIdx === -1 ? undefined : trimmed.slice(spaceIdx + 1).trim();
297
+ let input = {};
298
+ if (jsonStr) {
299
+ try {
300
+ input = JSON.parse(jsonStr);
301
+ }
302
+ catch {
303
+ console.error(` ✗ Invalid JSON: ${jsonStr}`);
304
+ rl.prompt();
305
+ return;
306
+ }
307
+ }
308
+ try {
309
+ const result = await client.appCall(action, input, { building, instanceId });
310
+ if (result && typeof result === 'object') {
311
+ console.log(JSON.stringify(result, null, 2));
312
+ }
313
+ else {
314
+ console.log(result);
315
+ }
316
+ }
317
+ catch (err) {
318
+ console.error(` ✗ ${err.message}`);
319
+ }
320
+ rl.prompt();
321
+ });
322
+ rl.on('close', () => {
323
+ client.disconnect();
324
+ process.exit(0);
325
+ });
326
+ }
327
+ else {
328
+ // Piped mode — read lines from stdin, execute sequentially, print JSON, exit on EOF.
329
+ // Claude and scripts use this path.
330
+ const { createInterface } = await import('readline');
331
+ const rl = createInterface({ input: process.stdin, terminal: false });
332
+ // Collect all lines first, then execute sequentially.
333
+ // This avoids race conditions where lines fire before the connection is ready.
334
+ const lines = [];
335
+ await new Promise((resolve) => {
336
+ rl.on('line', (line) => {
337
+ const trimmed = line.trim();
338
+ if (trimmed)
339
+ lines.push(trimmed);
340
+ });
341
+ rl.on('close', resolve);
342
+ });
343
+ for (const trimmed of lines) {
344
+ const spaceIdx = trimmed.indexOf(' ');
345
+ const action = spaceIdx === -1 ? trimmed : trimmed.slice(0, spaceIdx);
346
+ const jsonStr = spaceIdx === -1 ? undefined : trimmed.slice(spaceIdx + 1).trim();
347
+ let input = {};
348
+ if (jsonStr) {
349
+ try {
350
+ input = JSON.parse(jsonStr);
351
+ }
352
+ catch {
353
+ console.log(JSON.stringify({ error: `Invalid JSON: ${jsonStr}` }));
354
+ continue;
355
+ }
356
+ }
357
+ try {
358
+ const result = await client.appCall(action, input, { building, instanceId });
359
+ console.log(JSON.stringify(result));
360
+ }
361
+ catch (err) {
362
+ console.log(JSON.stringify({ error: err.message }));
363
+ }
364
+ }
365
+ client.disconnect();
366
+ }
367
+ }
368
+ // ── Entry Point (called from cli.ts) ─────────────────────────────────────
369
+ export async function cmdBuildingShell(building, args, opts) {
370
+ const instanceId = opts.instance ?? 'live';
371
+ const timeout = opts.timeout ? parseInt(opts.timeout, 10) : 30_000;
372
+ // --actions → delegate to existing app actions command
373
+ if (opts.actions) {
374
+ const { cmdAppActions } = await import('./app.js');
375
+ await cmdAppActions([building], opts);
376
+ return;
377
+ }
378
+ // --shell → interactive/piped REPL
379
+ if (opts.shell) {
380
+ await buildingShell(building, instanceId);
381
+ return;
382
+ }
383
+ // No action arg → context overview
384
+ if (args.length === 0) {
385
+ await buildingContext(building, instanceId);
386
+ return;
387
+ }
388
+ // action [json] → one-shot RPC
389
+ const action = args[0];
390
+ const rawInput = args.slice(1).join(' ') || undefined;
391
+ await buildingOneShot(building, action, rawInput, instanceId, timeout);
392
+ }