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,240 @@
1
+ /**
2
+ * beehaven user — per-user admin sugar (auto-discovers folders)
3
+ *
4
+ * Wraps the worker's /admin/do/<type>/<id>/<action> surface so dev doesn't
5
+ * type `beehaven admin sql User/<id> "..."` for each folder. The DO type
6
+ * comes from --workspace, or `userWorkspace` in beehaven.json (project root
7
+ * or worker subdir), or errors with a helpful message.
8
+ *
9
+ * Subcommands:
10
+ * inspect Storage breakdown + per-table row counts
11
+ * dump [folder] Dump one folder (or all) for this user as JSON
12
+ * sql "<query>" Sugar over admin:sql for this user's DO
13
+ * export [--out] Dump every folder to one JSON file
14
+ * wipe --yes Call the project's configured wipe action (or DELETE per-folder)
15
+ */
16
+ import { existsSync, readFileSync, writeFileSync } from 'fs';
17
+ import { dirname, join, resolve } from 'path';
18
+ import { color, spinner, table } from '../spinner.js';
19
+ import { getAdminClient, doActionPath } from '../lib/admin-client.js';
20
+ function findProjectConfig(start = process.cwd()) {
21
+ let dir = resolve(start);
22
+ while (true) {
23
+ const p = join(dir, 'beehaven.json');
24
+ if (existsSync(p)) {
25
+ try {
26
+ return { ...JSON.parse(readFileSync(p, 'utf-8')), _path: p };
27
+ }
28
+ catch {
29
+ return {};
30
+ }
31
+ }
32
+ const parent = dirname(dir);
33
+ if (parent === dir)
34
+ return {};
35
+ dir = parent;
36
+ }
37
+ }
38
+ function resolveWorkspaceType(opts) {
39
+ if (opts.workspace)
40
+ return opts.workspace;
41
+ const cfg = findProjectConfig();
42
+ const fromConfig = cfg.user?.workspace ?? cfg.userWorkspace;
43
+ if (fromConfig)
44
+ return fromConfig;
45
+ return {
46
+ error: 'Unknown user workspace type. Pass --workspace <ClassName> (e.g. --workspace User),\n' +
47
+ ' or add to beehaven.json: { "userWorkspace": "User" }',
48
+ };
49
+ }
50
+ export async function user(opts) {
51
+ if (!opts.userId) {
52
+ console.error('Usage: beehaven user <userId> <inspect|dump|sql|export|wipe> [args]');
53
+ process.exit(1);
54
+ }
55
+ const wsType = resolveWorkspaceType(opts);
56
+ if (typeof wsType !== 'string') {
57
+ console.error(color.red(wsType.error));
58
+ process.exit(1);
59
+ }
60
+ const client = getAdminClient({ url: opts.url, secret: opts.secret });
61
+ if ('error' in client) {
62
+ console.error(color.red(client.error));
63
+ process.exit(1);
64
+ }
65
+ const sub = opts.subcommand ?? 'inspect';
66
+ switch (sub) {
67
+ case 'inspect': {
68
+ const s = spinner(`Inspecting ${wsType}/${opts.userId}…`).start();
69
+ const { ok, status, body } = await client.call(doActionPath(wsType, opts.userId, 'admin:storage'), { method: 'POST', body: {} });
70
+ if (!ok) {
71
+ s.fail(`HTTP ${status}: ${body?.error ?? 'unknown'}`);
72
+ process.exit(1);
73
+ }
74
+ const totalMb = body?.sqlite?.mb ?? '?';
75
+ s.succeed(`${wsType}/${opts.userId} — ${totalMb} MB`);
76
+ const sizes = body?.tableSizes ?? {};
77
+ const rows = Object.entries(sizes).map(([t, info]) => [
78
+ color.cyan(t),
79
+ String(info.rowCount ?? '—'),
80
+ info.bytes ? `${(info.bytes / 1024).toFixed(1)} KB` : '—',
81
+ ]);
82
+ console.log('');
83
+ console.log(table(['Table', 'Rows', 'Size'], rows));
84
+ console.log('');
85
+ console.log(color.dim(` beehaven user ${opts.userId} dump <table>`));
86
+ console.log(color.dim(` beehaven user ${opts.userId} export`));
87
+ return;
88
+ }
89
+ case 'sql': {
90
+ const query = opts.positionals[0];
91
+ if (!query) {
92
+ console.error('Usage: beehaven user <userId> sql "<query>"');
93
+ process.exit(1);
94
+ }
95
+ const s = spinner('Running SQL…').start();
96
+ const { ok, status, body } = await client.call(doActionPath(wsType, opts.userId, 'admin:sql'), { method: 'POST', body: { query } });
97
+ if (!ok) {
98
+ s.fail(`HTTP ${status}: ${body?.error ?? 'unknown'}`);
99
+ process.exit(1);
100
+ }
101
+ const results = body?.rows ?? [];
102
+ s.succeed(`${results.length} rows`);
103
+ if (results.length === 0) {
104
+ console.log(color.dim('(no rows)'));
105
+ return;
106
+ }
107
+ if (opts.json) {
108
+ console.log(JSON.stringify(results, null, 2));
109
+ return;
110
+ }
111
+ const cols = Object.keys(results[0]);
112
+ console.log(table(cols, results.map((r) => cols.map((c) => String(r[c] ?? '')))));
113
+ return;
114
+ }
115
+ case 'dump': {
116
+ const folder = opts.positionals[0];
117
+ if (!folder) {
118
+ console.error('Usage: beehaven user <userId> dump <folder> [--out file]');
119
+ process.exit(1);
120
+ }
121
+ const s = spinner(`Dumping ${folder}…`).start();
122
+ const { ok, status, body } = await client.call(doActionPath(wsType, opts.userId, 'admin:sql'), { method: 'POST', body: { query: `SELECT * FROM ${folder}` } });
123
+ if (!ok) {
124
+ s.fail(`HTTP ${status}: ${body?.error ?? 'unknown'}`);
125
+ process.exit(1);
126
+ }
127
+ const rows = body?.rows ?? [];
128
+ s.succeed(`${rows.length} rows from ${folder}`);
129
+ const payload = JSON.stringify(rows, null, 2);
130
+ if (opts.out) {
131
+ writeFileSync(opts.out, payload);
132
+ console.log(color.green(`✓ Wrote ${opts.out}`));
133
+ }
134
+ else {
135
+ console.log(payload);
136
+ }
137
+ return;
138
+ }
139
+ case 'export': {
140
+ const cfg = findProjectConfig();
141
+ const exportAction = cfg.user?.exportAction;
142
+ if (exportAction) {
143
+ const s = spinner(`Calling ${exportAction}…`).start();
144
+ const { ok, status, body } = await client.call(doActionPath(wsType, opts.userId, exportAction), { method: 'POST', body: {} });
145
+ if (!ok) {
146
+ s.fail(`HTTP ${status}: ${body?.error ?? 'unknown'}`);
147
+ process.exit(1);
148
+ }
149
+ s.succeed(`Exported ${opts.userId}`);
150
+ const payload = JSON.stringify(body, null, 2);
151
+ if (opts.out) {
152
+ writeFileSync(opts.out, payload);
153
+ console.log(color.green(`✓ Wrote ${opts.out}`));
154
+ }
155
+ else
156
+ console.log(payload);
157
+ return;
158
+ }
159
+ // Fallback: enumerate folders, dump each
160
+ const sList = spinner('Enumerating tables…').start();
161
+ const tablesRes = await client.call(doActionPath(wsType, opts.userId, 'admin:sql'), {
162
+ method: 'POST',
163
+ body: {
164
+ query: "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '_cf_%'",
165
+ },
166
+ });
167
+ if (!tablesRes.ok) {
168
+ sList.fail(`HTTP ${tablesRes.status}`);
169
+ process.exit(1);
170
+ }
171
+ const tableNames = (tablesRes.body?.rows ?? []).map((r) => r.name);
172
+ sList.succeed(`${tableNames.length} tables`);
173
+ const out = {};
174
+ for (const t of tableNames) {
175
+ const r = await client.call(doActionPath(wsType, opts.userId, 'admin:sql'), {
176
+ method: 'POST',
177
+ body: { query: `SELECT * FROM ${t}` },
178
+ });
179
+ out[t] = r.body?.rows ?? [];
180
+ console.log(color.dim(` ${t}: ${out[t].length} rows`));
181
+ }
182
+ const payload = JSON.stringify(out, null, 2);
183
+ if (opts.out) {
184
+ writeFileSync(opts.out, payload);
185
+ console.log(color.green(`✓ Wrote ${opts.out}`));
186
+ }
187
+ else
188
+ console.log(payload);
189
+ return;
190
+ }
191
+ case 'wipe': {
192
+ if (!opts.yes) {
193
+ console.error(color.yellow(`This will DELETE all data on ${wsType}/${opts.userId}.`));
194
+ console.error(color.dim('Re-run with --yes to confirm.'));
195
+ process.exit(1);
196
+ }
197
+ const cfg = findProjectConfig();
198
+ const wipeAction = cfg.user?.wipeAction;
199
+ if (wipeAction) {
200
+ const s = spinner(`Calling ${wipeAction}…`).start();
201
+ const { ok, status, body } = await client.call(doActionPath(wsType, opts.userId, wipeAction), { method: 'POST', body: {} });
202
+ if (!ok) {
203
+ s.fail(`HTTP ${status}: ${body?.error ?? 'unknown'}`);
204
+ process.exit(1);
205
+ }
206
+ s.succeed(`Wiped ${opts.userId}`);
207
+ return;
208
+ }
209
+ // Fallback: DELETE FROM each user table
210
+ const tablesRes = await client.call(doActionPath(wsType, opts.userId, 'admin:sql'), {
211
+ method: 'POST',
212
+ body: {
213
+ query: "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '_cf_%'",
214
+ },
215
+ });
216
+ if (!tablesRes.ok) {
217
+ console.error(color.red('Could not enumerate tables.'));
218
+ process.exit(1);
219
+ }
220
+ const tableNames = (tablesRes.body?.rows ?? []).map((r) => r.name);
221
+ for (const t of tableNames) {
222
+ const s = spinner(`DELETE FROM ${t}…`).start();
223
+ const { ok, status } = await client.call(doActionPath(wsType, opts.userId, 'admin:sql'), {
224
+ method: 'POST',
225
+ body: { query: `DELETE FROM ${t}` },
226
+ });
227
+ if (!ok)
228
+ s.fail(`${t}: HTTP ${status}`);
229
+ else
230
+ s.succeed(t);
231
+ }
232
+ console.log(color.dim(' (configure beehaven.json "user.wipeAction" to call a single project-defined wipe RPC instead)'));
233
+ return;
234
+ }
235
+ default:
236
+ console.error(`Unknown user subcommand: ${sub}`);
237
+ console.error('Use: inspect | dump | sql | export | wipe');
238
+ process.exit(1);
239
+ }
240
+ }
@@ -0,0 +1,319 @@
1
+ /**
2
+ * voice-tts.ts — `beehaven voice <subcommand>`: choose how `beehaven speak`
3
+ * makes sound, and manage locally cloned voices.
4
+ *
5
+ * Bare `beehaven voice` is still the push-to-talk REPL (`voice.ts`); this file
6
+ * owns the noun-y subcommands underneath it:
7
+ *
8
+ * beehaven voice status what's active + what's installed
9
+ * beehaven voice provider [<name>] read / set the TTS provider flag
10
+ * beehaven voice setup install the local Qwen3-TTS runtime
11
+ * beehaven voice clone <name> <file> clone a voice from a recording
12
+ * beehaven voice list cloned voices
13
+ * beehaven voice use <name> which cloned voice speak uses
14
+ * beehaven voice remove <name> forget a cloned voice
15
+ * beehaven voice test ["text"] synthesize + play a sample
16
+ *
17
+ * The provider + voice are PERSISTED (in `~/.beehaven/config.json`) rather than
18
+ * env-only because `speak` is one process per utterance — an env var exported
19
+ * in one shell tells the next invocation nothing.
20
+ */
21
+ import { color, spinner } from '../spinner.js';
22
+ import { TTS_PROVIDERS, isTtsProvider, patchCliConfig, readCliConfig, resolveTtsProvider, resolveTtsVoice, } from '../feature-flags.js';
23
+ import * as qwen3 from '../tts-qwen3.js';
24
+ /** Subcommands this module claims. Anything else falls through to the REPL. */
25
+ export const VOICE_SUBCOMMANDS = [
26
+ 'status',
27
+ 'provider',
28
+ 'setup',
29
+ 'clone',
30
+ 'list',
31
+ 'use',
32
+ 'remove',
33
+ 'test',
34
+ ];
35
+ export function isVoiceSubcommand(s) {
36
+ return !!s && VOICE_SUBCOMMANDS.includes(s);
37
+ }
38
+ /**
39
+ * ⚠ `Record<TtsProvider, …>` IS LOAD-BEARING, NOT DECORATION. It is the only thing
40
+ * that made removing `avatar` and `system` from `TTS_PROVIDERS` a COMPILE error
41
+ * rather than a menu that offers engines nothing can run. Keep the exact type.
42
+ */
43
+ const PROVIDER_BLURB = {
44
+ auto: 'the default — resolves to qwen3-mlx',
45
+ 'qwen3-mlx': 'local Qwen3-TTS under Apple MLX — on-device, cloned voice',
46
+ none: 'silent — synthesize nothing',
47
+ };
48
+ function usage() {
49
+ console.log('');
50
+ console.log(` ${color.bold('beehaven voice')} — TTS provider + cloned voices`);
51
+ console.log('');
52
+ for (const [cmd, desc] of [
53
+ ['status', 'active provider, voice, and runtime state'],
54
+ ['provider [<name>]', `show or set the provider (${TTS_PROVIDERS.join(' | ')})`],
55
+ ['setup', 'install the local Qwen3-TTS (MLX) runtime'],
56
+ ['clone <name> <audio>', 'clone a voice from a recording (3-30s is plenty)'],
57
+ ['list', 'list cloned voices'],
58
+ ['use <name>', 'set the cloned voice speak uses'],
59
+ ['remove <name>', 'forget a cloned voice'],
60
+ ['test ["text"]', 'synthesize + play a sample with current settings'],
61
+ ]) {
62
+ console.log(` ${cmd.padEnd(24)} ${color.dim(desc)}`);
63
+ }
64
+ console.log('');
65
+ console.log(` ${color.dim('Bare `beehaven voice` starts the push-to-talk REPL.')}`);
66
+ console.log('');
67
+ }
68
+ export async function cmdVoiceTts(sub, args, values) {
69
+ switch (sub) {
70
+ case 'status':
71
+ return status();
72
+ case 'provider':
73
+ return provider(args[0]);
74
+ case 'setup':
75
+ return setup();
76
+ case 'clone':
77
+ return clone(args[0], args[1], values);
78
+ case 'list':
79
+ return list();
80
+ case 'use':
81
+ return use(args[0]);
82
+ case 'remove':
83
+ return remove(args[0]);
84
+ case 'test':
85
+ return test(args.join(' '), values);
86
+ default:
87
+ usage();
88
+ }
89
+ }
90
+ // ── status ──────────────────────────────────────────────────────────────────
91
+ async function status() {
92
+ const active = resolveTtsProvider();
93
+ const voice = resolveTtsVoice();
94
+ const fromEnv = !!(process.env.BEEHAVEN_TTS_PROVIDER ?? '').trim();
95
+ console.log('');
96
+ console.log(` ${color.bold('TTS')}`);
97
+ console.log(` Provider: ${color.cyan(active)} ${color.dim(PROVIDER_BLURB[active])}` +
98
+ (fromEnv ? color.dim(' [from $BEEHAVEN_TTS_PROVIDER]') : ''));
99
+ console.log(` Voice: ${voice ? color.cyan(voice) : color.dim('(none — base voice)')}`);
100
+ console.log('');
101
+ console.log(` ${color.bold('qwen3-mlx runtime')}`);
102
+ if (!qwen3.isSupported()) {
103
+ console.log(` ${color.yellow('unavailable')} — ${qwen3.unsupportedReason()}`);
104
+ }
105
+ else if (!qwen3.isInstalled()) {
106
+ console.log(` ${color.yellow('not installed')} — run ${color.cyan('beehaven voice setup')}`);
107
+ }
108
+ else {
109
+ try {
110
+ const p = await qwen3.probe();
111
+ console.log(` ${color.green('ready')} mlx-audio ${p.mlxAudio} · mlx ${p.mlx} · py ${p.python}`);
112
+ console.log(` Device: ${p.device}`);
113
+ console.log(` Model: ${color.dim(qwen3.QWEN3_DEFAULT_MODEL)}`);
114
+ }
115
+ catch (e) {
116
+ console.log(` ${color.red('broken')} — ${e?.message ?? e}`);
117
+ console.log(` ${color.dim('Reinstall with `beehaven voice setup`.')}`);
118
+ }
119
+ }
120
+ const voices = qwen3.listVoices();
121
+ console.log('');
122
+ console.log(` ${color.bold('Cloned voices')} ${voices.length ? voices.map((v) => v.name).join(', ') : color.dim('(none)')}`);
123
+ console.log('');
124
+ }
125
+ // ── provider ────────────────────────────────────────────────────────────────
126
+ function provider(next) {
127
+ if (!next) {
128
+ const active = resolveTtsProvider();
129
+ console.log('');
130
+ for (const p of TTS_PROVIDERS) {
131
+ const mark = p === active ? color.green('●') : color.dim('○');
132
+ console.log(` ${mark} ${p.padEnd(11)} ${color.dim(PROVIDER_BLURB[p])}`);
133
+ }
134
+ console.log('');
135
+ console.log(` ${color.dim('Set with: beehaven voice provider <name>')}`);
136
+ console.log('');
137
+ return;
138
+ }
139
+ if (!isTtsProvider(next)) {
140
+ console.error(` ${color.red('✗')} unknown provider "${next}" — expected: ${TTS_PROVIDERS.join(' | ')}`);
141
+ process.exit(1);
142
+ }
143
+ // `auto` is the default, so store nothing rather than pinning the literal —
144
+ // that way a future change to the default ladder reaches users who never
145
+ // opted out of it.
146
+ patchCliConfig({ ttsProvider: next === 'auto' ? undefined : next });
147
+ console.log(` ${color.green('✓')} TTS provider → ${color.cyan(next)} ${color.dim(PROVIDER_BLURB[next])}`);
148
+ if (next === 'qwen3-mlx') {
149
+ if (!qwen3.isSupported()) {
150
+ console.log(` ${color.yellow('!')} ${qwen3.unsupportedReason()} — speak will fall back.`);
151
+ }
152
+ else if (!qwen3.isInstalled()) {
153
+ console.log(` ${color.dim('↳')} not installed yet: ${color.cyan('beehaven voice setup')}`);
154
+ }
155
+ else if (!resolveTtsVoice()) {
156
+ const have = qwen3.listVoices();
157
+ console.log(have.length
158
+ ? ` ${color.dim('↳')} no voice selected: ${color.cyan(`beehaven voice use ${have[0].name}`)}`
159
+ : ` ${color.dim('↳')} clone a voice: ${color.cyan('beehaven voice clone <name> <audio>')}`);
160
+ }
161
+ }
162
+ }
163
+ // ── setup ───────────────────────────────────────────────────────────────────
164
+ async function setup() {
165
+ if (!qwen3.isSupported()) {
166
+ console.error(` ${color.red('✗')} ${qwen3.unsupportedReason()}`);
167
+ process.exit(1);
168
+ }
169
+ if (qwen3.isInstalled()) {
170
+ console.log(` ${color.green('✓')} already installed at ${color.dim(qwen3.QWEN3_ROOT)}`);
171
+ }
172
+ else {
173
+ const sp = spinner('installing Qwen3-TTS runtime (mlx-audio + mlx)…').start();
174
+ try {
175
+ await qwen3.install((line) => sp.update(`installing… ${line.slice(0, 60)}`));
176
+ sp.succeed(`installed ${qwen3.MLX_AUDIO_SPEC} at ${qwen3.QWEN3_ROOT}`);
177
+ }
178
+ catch (e) {
179
+ sp.fail(`install failed: ${e?.message ?? e}`);
180
+ process.exit(1);
181
+ }
182
+ }
183
+ const sp2 = spinner('verifying MLX…').start();
184
+ try {
185
+ const p = await qwen3.probe();
186
+ sp2.succeed(`mlx-audio ${p.mlxAudio} · mlx ${p.mlx} · ${p.device}`);
187
+ }
188
+ catch (e) {
189
+ sp2.fail(`verification failed: ${e?.message ?? e}`);
190
+ process.exit(1);
191
+ }
192
+ console.log('');
193
+ console.log(` ${color.dim('Next:')} beehaven voice clone <name> <audio-file>`);
194
+ console.log(` ${color.dim('Then:')} beehaven voice provider qwen3-mlx`);
195
+ console.log('');
196
+ }
197
+ // ── clone ───────────────────────────────────────────────────────────────────
198
+ async function clone(name, audio, values) {
199
+ if (!name || !audio) {
200
+ console.error(' Usage: beehaven voice clone <name> <audio-file> [--ref-text "…"] [--force]');
201
+ process.exit(1);
202
+ }
203
+ if (!qwen3.isSupported()) {
204
+ console.error(` ${color.red('✗')} ${qwen3.unsupportedReason()}`);
205
+ process.exit(1);
206
+ }
207
+ if (!qwen3.isInstalled()) {
208
+ console.error(` ${color.red('✗')} qwen3-mlx not installed — run \`beehaven voice setup\` first`);
209
+ process.exit(1);
210
+ }
211
+ const refText = values['ref-text'];
212
+ const sp = spinner(refText ? `cloning "${name}"…` : `cloning "${name}" — transcribing reference with whisper…`).start();
213
+ try {
214
+ const v = await qwen3.cloneVoice(name, audio, {
215
+ refText,
216
+ force: !!values.force,
217
+ onLine: (l) => sp.update(`cloning "${name}"… ${l.slice(0, 50)}`),
218
+ });
219
+ sp.succeed(`cloned voice ${color.cyan(name)}`);
220
+ console.log(` ${color.dim('reference:')} ${v.refAudio}`);
221
+ console.log(` ${color.dim('transcript:')} ${JSON.stringify(v.refText.slice(0, 120))}${v.refText.length > 120 ? '…' : ''}`);
222
+ if (v.transcribedBy)
223
+ console.log(` ${color.dim('transcribed by:')} ${v.transcribedBy}`);
224
+ console.log('');
225
+ console.log(` ${color.dim('Use it:')} beehaven voice use ${name}`);
226
+ console.log(` ${color.dim('Then:')} beehaven voice provider qwen3-mlx`);
227
+ console.log('');
228
+ }
229
+ catch (e) {
230
+ sp.fail(`clone failed: ${e?.message ?? e}`);
231
+ process.exit(1);
232
+ }
233
+ }
234
+ // ── list / use / remove ─────────────────────────────────────────────────────
235
+ function list() {
236
+ const voices = qwen3.listVoices();
237
+ const active = resolveTtsVoice();
238
+ if (!voices.length) {
239
+ console.log('');
240
+ console.log(` ${color.dim('No cloned voices.')}`);
241
+ console.log(` ${color.dim('Clone one:')} beehaven voice clone <name> <audio-file>`);
242
+ console.log('');
243
+ return;
244
+ }
245
+ console.log('');
246
+ for (const v of voices) {
247
+ const mark = v.name === active ? color.green('●') : color.dim('○');
248
+ const when = new Date(v.createdAt).toISOString().slice(0, 10);
249
+ console.log(` ${mark} ${color.bold(v.name.padEnd(16))} ${color.dim(when)} ${color.dim(v.source ?? v.refAudio)}`);
250
+ console.log(` ${color.dim(JSON.stringify(v.refText.slice(0, 90)) + (v.refText.length > 90 ? '…' : ''))}`);
251
+ }
252
+ console.log('');
253
+ }
254
+ function use(name) {
255
+ if (!name) {
256
+ console.error(' Usage: beehaven voice use <name> (or `none` to clear)');
257
+ process.exit(1);
258
+ }
259
+ if (name === 'none' || name === '-') {
260
+ patchCliConfig({ ttsVoice: undefined });
261
+ console.log(` ${color.green('✓')} cloned voice cleared — Qwen3 will use its base voice`);
262
+ return;
263
+ }
264
+ if (!qwen3.getVoice(name)) {
265
+ const have = qwen3.listVoices().map((v) => v.name);
266
+ console.error(` ${color.red('✗')} no cloned voice "${name}"` +
267
+ (have.length ? ` — have: ${have.join(', ')}` : ' — clone one first'));
268
+ process.exit(1);
269
+ }
270
+ patchCliConfig({ ttsVoice: name });
271
+ console.log(` ${color.green('✓')} speak voice → ${color.cyan(name)}`);
272
+ if (resolveTtsProvider() !== 'qwen3-mlx') {
273
+ console.log(` ${color.dim('↳')} only ${color.cyan('qwen3-mlx')} speaks cloned voices: ` +
274
+ color.cyan('beehaven voice provider qwen3-mlx'));
275
+ }
276
+ }
277
+ function remove(name) {
278
+ if (!name) {
279
+ console.error(' Usage: beehaven voice remove <name>');
280
+ process.exit(1);
281
+ }
282
+ if (!qwen3.removeVoice(name)) {
283
+ console.error(` ${color.red('✗')} no cloned voice "${name}"`);
284
+ process.exit(1);
285
+ }
286
+ if (readCliConfig().ttsVoice === name)
287
+ patchCliConfig({ ttsVoice: undefined });
288
+ console.log(` ${color.green('✓')} removed cloned voice ${name}`);
289
+ }
290
+ // ── test ────────────────────────────────────────────────────────────────────
291
+ async function test(text, values) {
292
+ const sample = text.trim() ||
293
+ 'This is the beehaven command line, speaking through Qwen3 text to speech, running locally on this machine.';
294
+ const voice = values['tts-voice'] ?? resolveTtsVoice();
295
+ if (!qwen3.isInstalled()) {
296
+ console.error(` ${color.red('✗')} qwen3-mlx not installed — run \`beehaven voice setup\``);
297
+ process.exit(1);
298
+ }
299
+ const sp = spinner(`synthesizing${voice ? ` as "${voice}"` : ''}…`).start();
300
+ try {
301
+ const res = await qwen3.synthesize(sample, {
302
+ voice,
303
+ onLine: (l) => sp.update(`synthesizing… ${l.slice(0, 50)}`),
304
+ });
305
+ sp.succeed(`${res.durationSec ?? '?'}s of audio in ${res.elapsedSec}s` +
306
+ (res.realTimeFactor ? ` (${res.realTimeFactor}x real-time)` : '') +
307
+ (res.voice ? ` · voice ${color.cyan(res.voice)}` : ' · base voice'));
308
+ const { playAudioFile } = await import('../audio-play.js');
309
+ const played = await playAudioFile(res.path);
310
+ if (!played.ok) {
311
+ console.error(` ${color.yellow('!')} could not play it: ${played.error}`);
312
+ console.log(` ${color.dim('audio is at')} ${res.path}`);
313
+ }
314
+ }
315
+ catch (e) {
316
+ sp.fail(`synthesis failed: ${e?.message ?? e}`);
317
+ process.exit(1);
318
+ }
319
+ }