aura-code 0.6.3 → 0.10.1

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 (265) hide show
  1. package/README.md +47 -164
  2. package/dist/agent/affect.d.ts +13 -0
  3. package/dist/agent/affect.js +34 -0
  4. package/dist/agent/affect.js.map +1 -0
  5. package/dist/agent/compactor.d.ts +27 -4
  6. package/dist/agent/compactor.js +185 -54
  7. package/dist/agent/compactor.js.map +1 -1
  8. package/dist/agent/confess.d.ts +35 -0
  9. package/dist/agent/confess.js +296 -0
  10. package/dist/agent/confess.js.map +1 -0
  11. package/dist/agent/context.d.ts +0 -4
  12. package/dist/agent/context.js +0 -38
  13. package/dist/agent/context.js.map +1 -1
  14. package/dist/agent/domain-expertise.d.ts +22 -0
  15. package/dist/agent/domain-expertise.js +110 -0
  16. package/dist/agent/domain-expertise.js.map +1 -0
  17. package/dist/agent/executive-queue.d.ts +26 -0
  18. package/dist/agent/executive-queue.js +50 -0
  19. package/dist/agent/executive-queue.js.map +1 -0
  20. package/dist/agent/generational-flush.d.ts +28 -0
  21. package/dist/agent/generational-flush.js +48 -0
  22. package/dist/agent/generational-flush.js.map +1 -0
  23. package/dist/agent/loop-profile.d.ts +50 -0
  24. package/dist/agent/loop-profile.js +88 -0
  25. package/dist/agent/loop-profile.js.map +1 -0
  26. package/dist/agent/loop.d.ts +14 -5
  27. package/dist/agent/loop.js +167 -39
  28. package/dist/agent/loop.js.map +1 -1
  29. package/dist/agent/memory-consolidate.d.ts +14 -0
  30. package/dist/agent/memory-consolidate.js +141 -0
  31. package/dist/agent/memory-consolidate.js.map +1 -0
  32. package/dist/agent/mixture.d.ts +33 -0
  33. package/dist/agent/mixture.js +138 -0
  34. package/dist/agent/mixture.js.map +1 -0
  35. package/dist/agent/spawner.js +2 -2
  36. package/dist/agent/spawner.js.map +1 -1
  37. package/dist/agent/system-prompt.d.ts +1 -1
  38. package/dist/agent/system-prompt.js +50 -22
  39. package/dist/agent/system-prompt.js.map +1 -1
  40. package/dist/agent/unified-memory.d.ts +15 -0
  41. package/dist/agent/unified-memory.js +164 -0
  42. package/dist/agent/unified-memory.js.map +1 -0
  43. package/dist/checkpoints/engine.d.ts +44 -0
  44. package/dist/checkpoints/engine.js +324 -0
  45. package/dist/checkpoints/engine.js.map +1 -0
  46. package/dist/cli/command-palette.d.ts +26 -0
  47. package/dist/cli/command-palette.js +133 -0
  48. package/dist/cli/command-palette.js.map +1 -0
  49. package/dist/cli/context-health.d.ts +51 -0
  50. package/dist/cli/context-health.js +187 -0
  51. package/dist/cli/context-health.js.map +1 -0
  52. package/dist/cli/diamond.d.ts +67 -0
  53. package/dist/cli/diamond.js +222 -12
  54. package/dist/cli/diamond.js.map +1 -1
  55. package/dist/cli/dic.d.ts +2 -0
  56. package/dist/cli/dic.js +106 -0
  57. package/dist/cli/dic.js.map +1 -0
  58. package/dist/cli/diff-view.d.ts +18 -0
  59. package/dist/cli/diff-view.js +114 -0
  60. package/dist/cli/diff-view.js.map +1 -0
  61. package/dist/cli/display.d.ts +18 -4
  62. package/dist/cli/display.js +74 -151
  63. package/dist/cli/display.js.map +1 -1
  64. package/dist/cli/help-data.d.ts +7 -0
  65. package/dist/cli/help-data.js +80 -0
  66. package/dist/cli/help-data.js.map +1 -0
  67. package/dist/cli/index.js +1002 -852
  68. package/dist/cli/index.js.map +1 -1
  69. package/dist/cli/markdown.d.ts +5 -0
  70. package/dist/cli/markdown.js +128 -0
  71. package/dist/cli/markdown.js.map +1 -0
  72. package/dist/cli/model-select.d.ts +39 -0
  73. package/dist/cli/model-select.js +65 -0
  74. package/dist/cli/model-select.js.map +1 -0
  75. package/dist/cli/tui.d.ts +34 -0
  76. package/dist/cli/tui.js +1176 -0
  77. package/dist/cli/tui.js.map +1 -0
  78. package/dist/config/defaults.d.ts +6 -17
  79. package/dist/config/defaults.js +25 -46
  80. package/dist/config/defaults.js.map +1 -1
  81. package/dist/config/project-config.d.ts +2 -6
  82. package/dist/config/project-config.js +2 -2
  83. package/dist/config/project-config.js.map +1 -1
  84. package/dist/doctor/checks.d.ts +18 -0
  85. package/dist/doctor/checks.js +524 -0
  86. package/dist/doctor/checks.js.map +1 -0
  87. package/dist/doctor/index.d.ts +4 -0
  88. package/dist/doctor/index.js +229 -0
  89. package/dist/doctor/index.js.map +1 -0
  90. package/dist/doctor/repair.d.ts +19 -0
  91. package/dist/doctor/repair.js +126 -0
  92. package/dist/doctor/repair.js.map +1 -0
  93. package/dist/doctor/types.d.ts +38 -0
  94. package/dist/doctor/types.js +10 -0
  95. package/dist/doctor/types.js.map +1 -0
  96. package/dist/dream/dream.d.ts +79 -59
  97. package/dist/dream/dream.js +324 -144
  98. package/dist/dream/dream.js.map +1 -1
  99. package/dist/dream/episode.d.ts +17 -0
  100. package/dist/dream/episode.js +116 -0
  101. package/dist/dream/episode.js.map +1 -0
  102. package/dist/kanban/engine.d.ts +26 -0
  103. package/dist/kanban/engine.js +208 -0
  104. package/dist/kanban/engine.js.map +1 -0
  105. package/dist/kanban/types.d.ts +19 -0
  106. package/dist/kanban/types.js +9 -1
  107. package/dist/kanban/types.js.map +1 -1
  108. package/dist/machina/render-html.js +4 -2
  109. package/dist/machina/render-html.js.map +1 -1
  110. package/dist/machina/render-terminal.js +11 -4
  111. package/dist/machina/render-terminal.js.map +1 -1
  112. package/dist/machina/repair.d.ts +23 -0
  113. package/dist/machina/repair.js +98 -0
  114. package/dist/machina/repair.js.map +1 -0
  115. package/dist/machina/spec.js +12 -12
  116. package/dist/machina/spec.js.map +1 -1
  117. package/dist/machina/verify.d.ts +13 -1
  118. package/dist/machina/verify.js +12 -2
  119. package/dist/machina/verify.js.map +1 -1
  120. package/dist/mining/extract.d.ts +34 -0
  121. package/dist/mining/extract.js +229 -0
  122. package/dist/mining/extract.js.map +1 -0
  123. package/dist/mining/refine.d.ts +28 -0
  124. package/dist/mining/refine.js +256 -0
  125. package/dist/mining/refine.js.map +1 -0
  126. package/dist/perception/extractor.js +5 -3
  127. package/dist/perception/extractor.js.map +1 -1
  128. package/dist/perception/graph-store.d.ts +0 -11
  129. package/dist/perception/graph-store.js +0 -33
  130. package/dist/perception/graph-store.js.map +1 -1
  131. package/dist/perception/index.d.ts +1 -1
  132. package/dist/perception/index.js +1 -2
  133. package/dist/perception/index.js.map +1 -1
  134. package/dist/plugins/commands.d.ts +38 -0
  135. package/dist/plugins/commands.js +134 -0
  136. package/dist/plugins/commands.js.map +1 -0
  137. package/dist/plugins/frontmatter.d.ts +14 -0
  138. package/dist/plugins/frontmatter.js +78 -0
  139. package/dist/plugins/frontmatter.js.map +1 -0
  140. package/dist/plugins/hooks.d.ts +12 -0
  141. package/dist/plugins/hooks.js +148 -0
  142. package/dist/plugins/hooks.js.map +1 -0
  143. package/dist/plugins/loader.d.ts +7 -0
  144. package/dist/plugins/loader.js +255 -0
  145. package/dist/plugins/loader.js.map +1 -0
  146. package/dist/plugins/market.d.ts +23 -0
  147. package/dist/plugins/market.js +281 -0
  148. package/dist/plugins/market.js.map +1 -0
  149. package/dist/plugins/types.d.ts +90 -0
  150. package/dist/plugins/types.js +20 -0
  151. package/dist/plugins/types.js.map +1 -0
  152. package/dist/providers/anthropic-oauth-draft.d.ts +119 -0
  153. package/dist/providers/anthropic-oauth-draft.js +414 -0
  154. package/dist/providers/anthropic-oauth-draft.js.map +1 -0
  155. package/dist/providers/anthropic.d.ts +4 -1
  156. package/dist/providers/anthropic.js +35 -7
  157. package/dist/providers/anthropic.js.map +1 -1
  158. package/dist/providers/factory.d.ts +43 -19
  159. package/dist/providers/factory.js +183 -129
  160. package/dist/providers/factory.js.map +1 -1
  161. package/dist/providers/google.js +9 -22
  162. package/dist/providers/google.js.map +1 -1
  163. package/dist/providers/live-models.d.ts +23 -0
  164. package/dist/providers/live-models.js +145 -0
  165. package/dist/providers/live-models.js.map +1 -0
  166. package/dist/providers/openai-compatible.d.ts +1 -12
  167. package/dist/providers/openai-compatible.js +71 -108
  168. package/dist/providers/openai-compatible.js.map +1 -1
  169. package/dist/providers/resilient-factory.js +1 -1
  170. package/dist/providers/resilient-factory.js.map +1 -1
  171. package/dist/repl/queue.d.ts +22 -0
  172. package/dist/repl/queue.js +165 -0
  173. package/dist/repl/queue.js.map +1 -0
  174. package/dist/repl/side-channel.d.ts +23 -0
  175. package/dist/repl/side-channel.js +63 -0
  176. package/dist/repl/side-channel.js.map +1 -0
  177. package/dist/research/council.d.ts +2 -0
  178. package/dist/research/council.js +29 -4
  179. package/dist/research/council.js.map +1 -1
  180. package/dist/ruby/alternator.d.ts +11 -30
  181. package/dist/ruby/alternator.js +59 -38
  182. package/dist/ruby/alternator.js.map +1 -1
  183. package/dist/ruby/episode-capture.d.ts +0 -10
  184. package/dist/ruby/episode-capture.js +0 -33
  185. package/dist/ruby/episode-capture.js.map +1 -1
  186. package/dist/ruby/types.js +1 -1
  187. package/dist/ruby/types.js.map +1 -1
  188. package/dist/safety/path-jail.d.ts +17 -0
  189. package/dist/safety/path-jail.js +100 -0
  190. package/dist/safety/path-jail.js.map +1 -0
  191. package/dist/safety/permissions.d.ts +12 -21
  192. package/dist/safety/permissions.js +81 -216
  193. package/dist/safety/permissions.js.map +1 -1
  194. package/dist/safety/ssrf.d.ts +29 -0
  195. package/dist/safety/ssrf.js +185 -0
  196. package/dist/safety/ssrf.js.map +1 -0
  197. package/dist/server/index.js +64 -10
  198. package/dist/server/index.js.map +1 -1
  199. package/dist/setup/first-run.d.ts +0 -24
  200. package/dist/setup/first-run.js +10 -344
  201. package/dist/setup/first-run.js.map +1 -1
  202. package/dist/setup/key-store.d.ts +12 -0
  203. package/dist/setup/key-store.js +108 -0
  204. package/dist/setup/key-store.js.map +1 -0
  205. package/dist/setup/provider-registry.js +31 -18
  206. package/dist/setup/provider-registry.js.map +1 -1
  207. package/dist/setup/provider-test.d.ts +7 -0
  208. package/dist/setup/provider-test.js +73 -10
  209. package/dist/setup/provider-test.js.map +1 -1
  210. package/dist/setup/provider-wizard.d.ts +4 -1
  211. package/dist/setup/provider-wizard.js +92 -26
  212. package/dist/setup/provider-wizard.js.map +1 -1
  213. package/dist/setup/xiaomi.d.ts +1 -1
  214. package/dist/setup/xiaomi.js +3 -3
  215. package/dist/setup/xiaomi.js.map +1 -1
  216. package/dist/tools/browser.js +14 -10
  217. package/dist/tools/browser.js.map +1 -1
  218. package/dist/tools/clipboard.js +7 -1
  219. package/dist/tools/clipboard.js.map +1 -1
  220. package/dist/tools/dictate.d.ts +60 -0
  221. package/dist/tools/dictate.js +983 -0
  222. package/dist/tools/dictate.js.map +1 -0
  223. package/dist/tools/dictate_patched.js +1057 -0
  224. package/dist/tools/edit-file.js +10 -2
  225. package/dist/tools/edit-file.js.map +1 -1
  226. package/dist/tools/http-request.js +14 -4
  227. package/dist/tools/http-request.js.map +1 -1
  228. package/dist/tools/image-read.js +34 -2
  229. package/dist/tools/image-read.js.map +1 -1
  230. package/dist/tools/index.d.ts +16 -1
  231. package/dist/tools/index.js +59 -8
  232. package/dist/tools/index.js.map +1 -1
  233. package/dist/tools/mcp.js +12 -0
  234. package/dist/tools/mcp.js.map +1 -1
  235. package/dist/tools/read-file.js +10 -14
  236. package/dist/tools/read-file.js.map +1 -1
  237. package/dist/tools/telegram-audio-policy.d.ts +30 -0
  238. package/dist/tools/telegram-audio-policy.js +49 -0
  239. package/dist/tools/telegram-audio-policy.js.map +1 -0
  240. package/dist/tools/telegram-bot.js +1136 -537
  241. package/dist/tools/telegram-bot.js.map +1 -1
  242. package/dist/tools/telegram-voice.d.ts +18 -5
  243. package/dist/tools/telegram-voice.js +54 -12
  244. package/dist/tools/telegram-voice.js.map +1 -1
  245. package/dist/tools/telegram.d.ts +3 -3
  246. package/dist/tools/telegram.js +5 -84
  247. package/dist/tools/telegram.js.map +1 -1
  248. package/dist/tools/tools.d.ts +2 -14
  249. package/dist/tools/tools.js +79 -144
  250. package/dist/tools/tools.js.map +1 -1
  251. package/dist/tools/web-fetch.js +39 -7
  252. package/dist/tools/web-fetch.js.map +1 -1
  253. package/dist/tools/web-search.d.ts +0 -20
  254. package/dist/tools/web-search.js +40 -137
  255. package/dist/tools/web-search.js.map +1 -1
  256. package/dist/util/errors.js +1 -15
  257. package/dist/util/errors.js.map +1 -1
  258. package/dist/util/json-repair.d.ts +11 -0
  259. package/dist/util/json-repair.js +31 -0
  260. package/dist/util/json-repair.js.map +1 -0
  261. package/dist/util/rate-limiter.js +2 -8
  262. package/dist/util/rate-limiter.js.map +1 -1
  263. package/dist/viz/index.js +793 -792
  264. package/dist/viz/index.js.map +1 -1
  265. package/package.json +12 -32
@@ -0,0 +1,983 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.injectText = injectText;
40
+ exports.listDevices = listDevices;
41
+ exports.getDefaultDevice = getDefaultDevice;
42
+ exports.dictate = dictate;
43
+ exports.toggleDictation = toggleDictation;
44
+ exports.speakText = speakText;
45
+ exports.synthesizeSpeech = synthesizeSpeech;
46
+ exports.stripWavPrefix = stripWavPrefix;
47
+ exports.transcribeFile = transcribeFile;
48
+ exports.listVoices = listVoices;
49
+ exports.dictationLoop = dictationLoop;
50
+ const child_process_1 = require("child_process");
51
+ const fs = __importStar(require("fs"));
52
+ const path = __importStar(require("path"));
53
+ const os = __importStar(require("os"));
54
+ const chalk_1 = __importDefault(require("chalk"));
55
+ const openai_1 = __importDefault(require("openai"));
56
+ const https = __importStar(require("https"));
57
+ const SAMPLE_RATE = 16000;
58
+ const MIMO_BASE = 'https://api.xiaomimimo.com/v1';
59
+ const MIMO_VOICES = {
60
+ mimo_default: { gender: 'auto', lang: 'auto' },
61
+ "\u51b0\u7cd6": { gender: 'female', lang: 'zh' },
62
+ "\u8309\u8389": { gender: 'female', lang: 'zh' },
63
+ "\u82cf\u6253": { gender: 'male', lang: 'zh' },
64
+ "\u767d\u6866": { gender: 'male', lang: 'zh' },
65
+ Mia: { gender: 'female', lang: 'en' },
66
+ Chloe: { gender: 'female', lang: 'en' },
67
+ Milo: { gender: 'male', lang: 'en' },
68
+ Dean: { gender: 'male', lang: 'en' },
69
+ };
70
+ function listProviders() {
71
+ const providers = [];
72
+ // Local Parakeet (NVIDIA) — highest priority when explicitly configured
73
+ if (process.env.PARAKEET_BASE_URL) {
74
+ const key = process.env.PARAKEET_API_KEY || 'sk-local';
75
+ providers.push({ key, baseURL: process.env.PARAKEET_BASE_URL, name: 'Parakeet' });
76
+ }
77
+ // GLM-ASR (Z.ai) — next-gen real-time ASR, OpenAI-compatible /audio/transcriptions.
78
+ // OPT-IN via DIC_USE_GLM_ASR=1: it currently needs a Z.ai audio balance
79
+ // package (err 1113) and the plain OpenAI SDK can hang against api.z.ai, so
80
+ // it's off the default path until the account is funded. When enabled it's
81
+ // tried first and falls through to MiMo on any failure.
82
+ if (process.env.ZHIPU_API_KEY && process.env.DIC_USE_GLM_ASR === '1') {
83
+ providers.push({
84
+ key: process.env.ZHIPU_API_KEY,
85
+ baseURL: process.env.ZHIPU_BASE_URL || 'https://api.z.ai/api/paas/v4',
86
+ name: 'GLM-ASR',
87
+ });
88
+ }
89
+ if (process.env.XIAOMI_API_KEY) {
90
+ const key = process.env.XIAOMI_API_KEY;
91
+ const baseURL = key.startsWith('tp-')
92
+ ? (process.env.XIAOMI_BASE_URL || 'https://token-plan-sgp.xiaomimimo.com/v1')
93
+ : MIMO_BASE;
94
+ providers.push({ key, baseURL, name: 'Xiaomi MiMo' });
95
+ }
96
+ if (process.env.OPENAI_API_KEY) {
97
+ providers.push({ key: process.env.OPENAI_API_KEY, baseURL: 'https://api.openai.com/v1', name: 'OpenAI' });
98
+ }
99
+ if (process.env.GROQ_API_KEY) {
100
+ providers.push({ key: process.env.GROQ_API_KEY, baseURL: 'https://api.groq.com/openai/v1', name: 'Groq' });
101
+ }
102
+ return providers;
103
+ }
104
+ function buildClient(api) {
105
+ const opts = {
106
+ apiKey: api.key,
107
+ baseURL: api.baseURL,
108
+ // Bounded so a hung provider eventually falls through, but generous enough
109
+ // for TTS (audio generation is slower than a chat/transcription call).
110
+ timeout: 60_000,
111
+ maxRetries: 0,
112
+ // api.z.ai silently drops requests that arrive without a User-Agent
113
+ // (see the zai-edge memory) — always send one.
114
+ defaultHeaders: { 'User-Agent': 'aura-dic/1.0' },
115
+ // OpenAI SDK v4 uses agentkeepalive by default, which hangs on some
116
+ // endpoints (e.g. Xiaomi MiMo Token Plan). A plain https.Agent with
117
+ // keepAlive: false avoids the issue.
118
+ httpAgent: new https.Agent({ keepAlive: false }),
119
+ };
120
+ return new openai_1.default(opts);
121
+ }
122
+ function cleanup(tmpDir) {
123
+ try {
124
+ fs.rmSync(tmpDir, { recursive: true, force: true });
125
+ }
126
+ catch { }
127
+ }
128
+ // ─── Clipboard injection ──────────────────────────────────────────────────
129
+ /** Best-effort clipboard copy (wl-copy on Wayland, xclip/xsel on X11). */
130
+ async function copyToClipboard(text) {
131
+ const candidates = process.env.WAYLAND_DISPLAY
132
+ ? [['wl-copy'], ['xclip', '-selection', 'clipboard'], ['xsel', '--clipboard', '--input']]
133
+ : [['xclip', '-selection', 'clipboard'], ['xsel', '--clipboard', '--input'], ['wl-copy']];
134
+ for (const [cmd, ...args] of candidates) {
135
+ try {
136
+ (0, child_process_1.execSync)('which ' + cmd, { stdio: 'pipe' });
137
+ // stdout/stderr must be 'ignore': wl-copy (and xclip) fork a daemon to
138
+ // serve the clipboard, which inherits the pipes — 'close' then never
139
+ // fires. Wait on 'exit' (the parent's) instead.
140
+ const proc = (0, child_process_1.spawn)(cmd, args, { stdio: ['pipe', 'ignore', 'ignore'] });
141
+ proc.stdin.write(text);
142
+ proc.stdin.end();
143
+ await new Promise((resolve, reject) => {
144
+ const t = setTimeout(() => reject(new Error(cmd + ' timed out')), 5000);
145
+ proc.on('exit', () => { clearTimeout(t); resolve(); });
146
+ proc.on('error', (e) => { clearTimeout(t); reject(e); });
147
+ });
148
+ return;
149
+ }
150
+ catch { /* try next */ }
151
+ }
152
+ throw new Error('No clipboard tool found (install wl-clipboard, xclip, or xsel)');
153
+ }
154
+ /** Press the Enter key in the focused window (Wayland: ydotool; X11: xdotool). */
155
+ function pressEnter(isWayland) {
156
+ if (isWayland) {
157
+ try {
158
+ (0, child_process_1.execSync)('which ydotool', { stdio: 'pipe' });
159
+ // ydotool key: <keycode>:<1=down|0=up>. 28 = KEY_ENTER (Linux evdev).
160
+ (0, child_process_1.execSync)('ydotool key 28:1 28:0', { stdio: ['ignore', 'ignore', 'ignore'], timeout: 5000 });
161
+ return;
162
+ }
163
+ catch { /* fall through */ }
164
+ }
165
+ try {
166
+ (0, child_process_1.execSync)('which xdotool', { stdio: 'pipe' });
167
+ (0, child_process_1.execSync)('xdotool key --clearmodifiers Return', { stdio: ['ignore', 'ignore', 'ignore'], timeout: 5000 });
168
+ }
169
+ catch { /* Enter is best-effort — the text is already typed */ }
170
+ }
171
+ /**
172
+ * Copy text to clipboard AND type it into the focused window.
173
+ * Types the characters directly (wtype on Wayland, xdotool type on X11)
174
+ * instead of simulating Ctrl+V — terminals treat Ctrl+V as a literal
175
+ * character, not paste, so a paste keystroke never lands in a CLI prompt.
176
+ * With { submit: true }, presses Enter afterward to send the prompt.
177
+ */
178
+ async function injectText(text, opts = {}) {
179
+ try {
180
+ await copyToClipboard(text);
181
+ }
182
+ catch { /* typing is the main path */ }
183
+ // Newlines would submit a prompt / run a shell line mid-text — flatten them.
184
+ const typed = text.replace(/\s*\n\s*/g, ' ').trim();
185
+ if (!typed)
186
+ return;
187
+ const isWayland = !!process.env.WAYLAND_DISPLAY;
188
+ if (isWayland) {
189
+ let injected = false;
190
+ try {
191
+ (0, child_process_1.execSync)('which wtype', { stdio: 'pipe' });
192
+ await new Promise(r => setTimeout(r, 150));
193
+ // "wtype -" reads the text from stdin — no shell-quoting pitfalls.
194
+ (0, child_process_1.execSync)('wtype -', { input: typed, stdio: ['pipe', 'pipe', 'pipe'], timeout: 20000 });
195
+ injected = true;
196
+ }
197
+ catch { /* KWin rejects the virtual-keyboard protocol — try ydotool */ }
198
+ // ydotool types via kernel uinput — works on any Wayland compositor,
199
+ // but needs the ydotoold daemon and /dev/uinput access.
200
+ if (!injected) {
201
+ try {
202
+ (0, child_process_1.execSync)('which ydotool', { stdio: 'pipe' });
203
+ await new Promise(r => setTimeout(r, 150));
204
+ (0, child_process_1.execSync)('ydotool type --file -', { input: typed, stdio: ['pipe', 'pipe', 'pipe'], timeout: 20000 });
205
+ injected = true;
206
+ }
207
+ catch { /* fall through to error below */ }
208
+ }
209
+ if (!injected) {
210
+ // Don't fall back to xdotool here: on Wayland it "types" only into
211
+ // XWayland windows, so it reports success while a native terminal
212
+ // (Konsole, kitty…) receives nothing — a silent failure.
213
+ throw new Error('no working Wayland injector — this compositor rejects wtype; install ydotool + ydotoold');
214
+ }
215
+ if (opts.submit)
216
+ pressEnter(true);
217
+ return;
218
+ }
219
+ try {
220
+ (0, child_process_1.execSync)('which xdotool', { stdio: 'pipe' });
221
+ await new Promise(r => setTimeout(r, 150));
222
+ (0, child_process_1.execSync)('xdotool type --clearmodifiers --file -', { input: typed, stdio: ['pipe', 'pipe', 'pipe'], timeout: 20000 });
223
+ if (opts.submit)
224
+ pressEnter(false);
225
+ }
226
+ catch {
227
+ throw new Error('Cannot inject text: install xdotool (X11)');
228
+ }
229
+ }
230
+ // ─── Audio level helper ────────────────────────────────────────────────────
231
+ function rmsLevel(pcmBuffer) {
232
+ if (pcmBuffer.length < 2)
233
+ return 0;
234
+ let sum = 0;
235
+ const samples = pcmBuffer.length / 2;
236
+ for (let i = 0; i < pcmBuffer.length; i += 2) {
237
+ const val = pcmBuffer.readInt16LE(i);
238
+ sum += val * val;
239
+ }
240
+ return Math.sqrt(sum / samples);
241
+ }
242
+ function pickPlayer() {
243
+ for (const p of ['aplay', 'paplay', 'ffplay']) {
244
+ try {
245
+ (0, child_process_1.execSync)('which ' + p, { stdio: 'pipe' });
246
+ return p;
247
+ }
248
+ catch { }
249
+ }
250
+ return 'aplay';
251
+ }
252
+ function playWav(wavBuffer) {
253
+ return new Promise((resolve, reject) => {
254
+ const player = pickPlayer();
255
+ const proc = (0, child_process_1.spawn)(player, [], { stdio: ['pipe', 'inherit', 'inherit'] });
256
+ proc.on('error', reject);
257
+ proc.on('close', (code) => code === 0 ? resolve() : reject(new Error('Player exited with code ' + code)));
258
+ proc.stdin.write(wavBuffer);
259
+ proc.stdin.end();
260
+ });
261
+ }
262
+ // ─── Audio device listing ─────────────────────────────────────────────────
263
+ function listDevices() {
264
+ console.log(chalk_1.default.hex('#cc785c').bold('\n Available Audio Input Devices\n'));
265
+ try {
266
+ const output = (0, child_process_1.execSync)('pactl list sources short', { stdio: 'pipe', encoding: 'utf8', timeout: 5000 });
267
+ const lines = output.trim().split('\n');
268
+ if (lines.length === 0 || (lines.length === 1 && !lines[0].trim())) {
269
+ console.log(chalk_1.default.hex('#ede0cc')(' No audio input devices found.\n'));
270
+ return;
271
+ }
272
+ for (const line of lines) {
273
+ const parts = line.trim().split(/\s+/);
274
+ if (parts.length < 2)
275
+ continue;
276
+ const id = parts[1]; // source name
277
+ const state = parts[7] || '';
278
+ const icon = state === 'RUNNING' ? '🎤 ' : ' ';
279
+ console.log(` ${icon}${chalk_1.default.hex('#ede0cc')(id)}`);
280
+ }
281
+ console.log(chalk_1.default.hex('#8a7768')('\n Use: dic --device <name>\n'));
282
+ }
283
+ catch {
284
+ console.log(chalk_1.default.hex('#cc9e5c')(' Could not list devices (pactl not available).\n'));
285
+ }
286
+ }
287
+ function getDefaultDevice() {
288
+ try {
289
+ const output = (0, child_process_1.execSync)('pactl get-default-source 2>/dev/null || pw-record --list-targets 2>/dev/null | head -1', { stdio: 'pipe', encoding: 'utf8', timeout: 5000 });
290
+ return output.trim();
291
+ }
292
+ catch {
293
+ return '';
294
+ }
295
+ }
296
+ function pickRecorder(deviceId) {
297
+ // Prefer PipeWire's pw-record (handles BT, USB headsets correctly)
298
+ try {
299
+ (0, child_process_1.execSync)('which pw-record', { stdio: 'pipe' });
300
+ const args = ['--rate=' + String(SAMPLE_RATE), '--format=s16', '--channels=1'];
301
+ if (deviceId) {
302
+ args.push('--target=' + deviceId);
303
+ }
304
+ return { cmd: 'pw-record', args };
305
+ }
306
+ catch { }
307
+ // Fallback 1: PulseAudio's parec + ffmpeg conversion
308
+ try {
309
+ (0, child_process_1.execSync)('which parec', { stdio: 'pipe' });
310
+ const args = ['--rate=' + String(SAMPLE_RATE), '--format=s16le', '--channels=1', '--raw'];
311
+ if (deviceId) {
312
+ args.push('--device=' + deviceId);
313
+ }
314
+ return { cmd: 'parec', args };
315
+ }
316
+ catch { }
317
+ // Fallback 2: ALSA arecord
318
+ try {
319
+ (0, child_process_1.execSync)('which arecord', { stdio: 'pipe' });
320
+ const args = ['-r', String(SAMPLE_RATE), '-f', 'S16_LE', '-c', '1', '-t', 'wav'];
321
+ if (deviceId) {
322
+ args.push('-D', deviceId);
323
+ }
324
+ return { cmd: 'arecord', args };
325
+ }
326
+ catch { }
327
+ return null;
328
+ }
329
+ /**
330
+ * Try to transcribe with one provider. Returns the transcribed text.
331
+ * Throws on failure; caller should catch and try next provider.
332
+ */
333
+ async function transcribeWith(api, wavPath, printName) {
334
+ const client = buildClient(api);
335
+ if (printName) {
336
+ const sizeKb = (fs.statSync(wavPath).size / 1024).toFixed(1);
337
+ console.log(chalk_1.default.hex('#8a7768')('\n Audio: ' + sizeKb + ' KB (' + api.name + ')'));
338
+ console.log(chalk_1.default.hex('#cc785c')(' Transcribing\u2026\n'));
339
+ }
340
+ if (api.name === 'Xiaomi MiMo') {
341
+ // MiMo ASR takes inline base64 — cap it so a runaway recording doesn't
342
+ // blow the request (25 MB wav ≈ 13 min at 16 kHz mono, ~33 MB base64).
343
+ const audioBytes = fs.statSync(wavPath).size;
344
+ const MAX_AUDIO_BYTES = 25_000_000;
345
+ if (audioBytes > MAX_AUDIO_BYTES) {
346
+ throw new Error(`Recording too large for inline ASR (${(audioBytes / 1e6).toFixed(1)} MB > ${MAX_AUDIO_BYTES / 1e6} MB). ` +
347
+ 'Record shorter segments or use "dic loop" for continuous dictation.');
348
+ }
349
+ const audioBase64 = fs.readFileSync(wavPath).toString('base64');
350
+ const response = await client.chat.completions.create({
351
+ model: 'mimo-v2.5-asr',
352
+ messages: [{
353
+ role: 'user',
354
+ content: [{
355
+ type: 'input_audio',
356
+ input_audio: { data: 'data:audio/wav;base64,' + audioBase64 },
357
+ }],
358
+ }],
359
+ asr_options: { language: 'auto' },
360
+ });
361
+ return (response.choices[0]?.message?.content ?? '').trim();
362
+ }
363
+ const model = api.name === 'Groq' ? 'whisper-large-v3-turbo'
364
+ : api.name === 'Parakeet' ? 'parakeet-tdt-0.6b'
365
+ : api.name === 'GLM-ASR' ? 'glm-asr-2512'
366
+ : 'whisper-1';
367
+ // GLM-ASR caps at 30s / 25 MB per Z.ai docs; guard so we fail fast (and
368
+ // fall through to MiMo) rather than sending an over-long clip.
369
+ if (api.name === 'GLM-ASR') {
370
+ const bytes = fs.statSync(wavPath).size;
371
+ if (bytes > 25_000_000)
372
+ throw new Error('GLM-ASR: audio > 25 MB');
373
+ }
374
+ const transcription = await client.audio.transcriptions.create({
375
+ file: fs.createReadStream(wavPath),
376
+ model,
377
+ response_format: 'text',
378
+ });
379
+ const text = typeof transcription === 'string'
380
+ ? transcription
381
+ : transcription.text ?? String(transcription);
382
+ return text.trim();
383
+ }
384
+ /**
385
+ * Should we fall through to the next STT provider on this error?
386
+ *
387
+ * A fallback chain should be forgiving: auth failures (401), balance/resource
388
+ * errors (Z.ai 1113), rate limits (429), and network/connection blips all mean
389
+ * "this provider couldn't serve it — try the next one". The ONLY thing that
390
+ * should abort the whole chain is a fatal input problem (audio too large),
391
+ * which we mark explicitly so it isn't retried pointlessly against every
392
+ * provider.
393
+ */
394
+ function shouldFallThrough(err) {
395
+ const msg = String(err?.message ?? err ?? '');
396
+ if (/audio > 25 MB|too large/i.test(msg))
397
+ return false; // fatal input — don't retry
398
+ if (err?.status === 401 || err?.status === 403 || err?.status === 429)
399
+ return true;
400
+ return /401|403|429|Invalid API Key|1113|Insufficient balance|no resource package|Connection error| ETIMEDOUT|ECONNRESET|ENOTFOUND|network|timeout|fetch failed/i.test(msg);
401
+ }
402
+ async function dictate(opts = {}) {
403
+ const deviceId = opts.deviceId;
404
+ const inject = opts.inject ?? false;
405
+ const providers = listProviders();
406
+ if (providers.length === 0) {
407
+ console.error(chalk_1.default.hex('#b15439')('\n No API key found. Set one of:\n' +
408
+ ' PARAKEET_BASE_URL (local NVIDIA Parakeet, no key needed)\n' +
409
+ ' XIAOMI_API_KEY (recommended, MiMo ASR free tier)\n' +
410
+ ' OPENAI_API_KEY (Whisper)\n' +
411
+ ' GROQ_API_KEY (Whisper via Groq, very fast)\n'));
412
+ process.exit(1);
413
+ }
414
+ const recorderInfo = pickRecorder(deviceId);
415
+ if (!recorderInfo) {
416
+ console.error(chalk_1.default.hex('#b15439')('\n No audio recorder found. Install pw-record, parec, or arecord.\n'));
417
+ process.exit(1);
418
+ }
419
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dic-'));
420
+ const wavPath = path.join(tmpDir, 'recording.wav');
421
+ // Spawn the appropriate recorder
422
+ let recorder;
423
+ let parecFfmpeg = null;
424
+ if (recorderInfo.cmd === 'parec') {
425
+ // parec outputs raw PCM (args already have --raw) → pipe through ffmpeg to create WAV
426
+ const ffmpeg = (0, child_process_1.spawn)('ffmpeg', [
427
+ '-f', 's16le', '-ar', String(SAMPLE_RATE), '-ac', '1',
428
+ '-i', 'pipe:0', '-y', wavPath,
429
+ ], { stdio: ['pipe', 'inherit', 'inherit'] });
430
+ recorder = (0, child_process_1.spawn)(recorderInfo.cmd, recorderInfo.args, {
431
+ stdio: ['ignore', 'pipe', 'inherit'],
432
+ });
433
+ recorder.stdout.pipe(ffmpeg.stdin);
434
+ parecFfmpeg = ffmpeg;
435
+ }
436
+ else {
437
+ // pw-record and arecord write WAV directly
438
+ recorder = (0, child_process_1.spawn)(recorderInfo.cmd, [...recorderInfo.args, wavPath], {
439
+ stdio: ['ignore', 'inherit', 'inherit'],
440
+ });
441
+ }
442
+ // Show device info
443
+ const devName = deviceId || getDefaultDevice();
444
+ console.log(chalk_1.default.hex('#5a9e6e')('\n \uD83C\uDFA4 Recording \u2014 speak into your microphone.'));
445
+ if (devName)
446
+ console.log(chalk_1.default.hex('#8a7768')(' Device: ' + devName));
447
+ console.log(chalk_1.default.hex('#8a7768')(' Press Ctrl+C to stop and transcribe.\n'));
448
+ await new Promise((resolve) => {
449
+ const onSigint = () => {
450
+ process.removeListener('SIGINT', onSigint);
451
+ recorder.kill('SIGTERM');
452
+ if (parecFfmpeg)
453
+ parecFfmpeg.kill('SIGTERM');
454
+ resolve();
455
+ };
456
+ process.on('SIGINT', onSigint);
457
+ });
458
+ await new Promise(r => setTimeout(r, 300));
459
+ if (!fs.existsSync(wavPath)) {
460
+ console.error(chalk_1.default.hex('#b15439')('\n No audio captured.\n'));
461
+ cleanup(tmpDir);
462
+ process.exit(1);
463
+ }
464
+ const stat = fs.statSync(wavPath);
465
+ if (stat.size < 512) {
466
+ console.log(chalk_1.default.hex('#cc9e5c')('\n Recording too short \u2014 nothing to transcribe.\n'));
467
+ cleanup(tmpDir);
468
+ return;
469
+ }
470
+ // Normalize audio level — boosts quiet speech for better ASR accuracy
471
+ const normPath = path.join(tmpDir, 'normalized.wav');
472
+ try {
473
+ (0, child_process_1.execSync)(`ffmpeg -y -i "${wavPath}" -af "dynaudnorm=p=0.9:r=0.5" -ar ${SAMPLE_RATE} -ac 1 "${normPath}" 2>/dev/null`, { stdio: 'pipe', timeout: 10000 });
474
+ fs.renameSync(normPath, wavPath);
475
+ }
476
+ catch {
477
+ try {
478
+ fs.unlinkSync(normPath);
479
+ }
480
+ catch { }
481
+ }
482
+ // Try each provider in order, falling through on auth errors
483
+ let text = '';
484
+ for (let i = 0; i < providers.length; i++) {
485
+ const api = providers[i];
486
+ try {
487
+ text = await transcribeWith(api, wavPath, i === 0);
488
+ break; // success
489
+ }
490
+ catch (err) {
491
+ if (shouldFallThrough(err) && i < providers.length - 1) {
492
+ console.log(chalk_1.default.hex('#cc9e5c')(' ' + api.name + ': unavailable (key/balance), trying next provider...\n'));
493
+ continue;
494
+ }
495
+ // Last provider failed or non-recoverable error — bail out
496
+ console.error(chalk_1.default.hex('#b15439')('\n Transcription failed (' + api.name + '):'), String(err), '\n');
497
+ cleanup(tmpDir);
498
+ process.exit(1);
499
+ }
500
+ }
501
+ const cleaned = text.trim();
502
+ if (cleaned) {
503
+ console.log(chalk_1.default.hex('#5a9e6e').bold(' \u2500\u2500 Transcription \u2500\u2500'));
504
+ console.log(chalk_1.default.hex('#ede0cc')(' ' + cleaned));
505
+ console.log(chalk_1.default.hex('#5a9e6e').bold(' \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n'));
506
+ let onClipboard = false;
507
+ try {
508
+ const clipboardModule = await Promise.resolve().then(() => __importStar(require('./clipboard.js')));
509
+ await clipboardModule.clipboardTool({ action: 'copy', text: cleaned });
510
+ onClipboard = true;
511
+ }
512
+ catch { /* injection below still works without clipboard */ }
513
+ if (inject) {
514
+ try {
515
+ await injectText(cleaned);
516
+ console.log(chalk_1.default.hex('#8a7768')(' ⌨️ Typed into focused window' + (onClipboard ? ' (also on clipboard).\n' : '.\n')));
517
+ }
518
+ catch (err) {
519
+ console.log(chalk_1.default.hex('#cc9e5c')(' ⌨️ Injection failed (' + String(err) + ')' +
520
+ (onClipboard ? ' — text is on the clipboard, paste manually.\n' : '.\n')));
521
+ }
522
+ }
523
+ else if (onClipboard) {
524
+ console.log(chalk_1.default.hex('#8a7768')(' 📋 Copied to clipboard.\n'));
525
+ }
526
+ else {
527
+ console.log(chalk_1.default.hex('#cc9e5c')(' Could not copy to clipboard — text shown above only.\n'));
528
+ }
529
+ }
530
+ else {
531
+ // Save failed recording for debugging
532
+ const saveDir = path.join(os.homedir(), '.aura', 'recordings');
533
+ const saveName = 'dic_' + new Date().toISOString().replace(/[:.]/g, '-') + '.wav';
534
+ const savePath = path.join(saveDir, saveName);
535
+ try {
536
+ fs.mkdirSync(saveDir, { recursive: true });
537
+ if (fs.existsSync(wavPath))
538
+ fs.copyFileSync(wavPath, savePath);
539
+ }
540
+ catch { }
541
+ cleanup(tmpDir);
542
+ if (fs.existsSync(savePath)) {
543
+ console.log(chalk_1.default.hex('#cc9e5c')('\n No speech detected.'));
544
+ console.log(chalk_1.default.hex('#8a7768')(' Recording saved for debugging: ' + savePath + '\n'));
545
+ }
546
+ else {
547
+ console.log(chalk_1.default.hex('#cc9e5c')('\n No speech detected.\n'));
548
+ }
549
+ return;
550
+ }
551
+ cleanup(tmpDir);
552
+ }
553
+ // ─── Toggle dictation (one hotkey: start → stop → transcribe → inject) ──────
554
+ //
555
+ // Designed for a global shortcut (e.g. KDE Super+Space). Press once to start
556
+ // recording; press again to stop, transcribe, and type the result — with Enter
557
+ // — into the focused window. State lives in a lock dir so the second, separate
558
+ // process can find and stop the first.
559
+ function toggleStateDir() {
560
+ const base = process.env.XDG_RUNTIME_DIR || os.tmpdir();
561
+ return path.join(base, 'dic-toggle');
562
+ }
563
+ /** Normalize a recorded wav in place and transcribe it with provider fallback. */
564
+ async function normalizeAndTranscribe(wavPath, providers) {
565
+ const normPath = wavPath.replace(/\.wav$/, '.norm.wav');
566
+ try {
567
+ (0, child_process_1.execSync)(`ffmpeg -y -i "${wavPath}" -af "dynaudnorm=p=0.9:r=0.5" -ar ${SAMPLE_RATE} -ac 1 "${normPath}" 2>/dev/null`, { stdio: 'pipe', timeout: 10000 });
568
+ fs.renameSync(normPath, wavPath);
569
+ }
570
+ catch {
571
+ try {
572
+ fs.unlinkSync(normPath);
573
+ }
574
+ catch { }
575
+ }
576
+ for (let i = 0; i < providers.length; i++) {
577
+ try {
578
+ return (await transcribeWith(providers[i], wavPath, false)).trim();
579
+ }
580
+ catch (err) {
581
+ if (shouldFallThrough(err) && i < providers.length - 1)
582
+ continue;
583
+ throw err;
584
+ }
585
+ }
586
+ return '';
587
+ }
588
+ function notify(title, body, replaceTag = 'dic-toggle') {
589
+ try {
590
+ (0, child_process_1.execSync)(`notify-send -a dic -h string:x-canonical-private-synchronous:${replaceTag} ` +
591
+ `${JSON.stringify(title)} ${JSON.stringify(body)}`, { stdio: 'ignore', timeout: 3000 });
592
+ }
593
+ catch { /* notifications are optional */ }
594
+ }
595
+ /**
596
+ * Toggle: first call starts recording (detached recorder + state files),
597
+ * second call stops it and transcribes + injects. Returns when the whole
598
+ * cycle (or the start half) is done.
599
+ */
600
+ async function toggleDictation(opts = {}) {
601
+ const stateDir = toggleStateDir();
602
+ const pidFile = path.join(stateDir, 'recorder.pid');
603
+ const wavFile = path.join(stateDir, 'recording.wav');
604
+ const rawFile = path.join(stateDir, 'recording.raw');
605
+ // ── STOP branch: a recording is already in progress ──────────────────────
606
+ if (fs.existsSync(pidFile)) {
607
+ let pid = 0;
608
+ try {
609
+ pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10);
610
+ }
611
+ catch { }
612
+ const isParec = fs.existsSync(path.join(stateDir, 'parec'));
613
+ // Stop the recorder; a stale pid (process already gone) is fine.
614
+ if (pid > 0) {
615
+ try {
616
+ process.kill(pid, 'SIGTERM');
617
+ }
618
+ catch { }
619
+ }
620
+ try {
621
+ fs.unlinkSync(pidFile);
622
+ }
623
+ catch { }
624
+ await new Promise(r => setTimeout(r, 400)); // let the file flush/close
625
+ // parec path recorded raw PCM → wrap to wav.
626
+ const srcRaw = isParec ? rawFile : wavFile;
627
+ if (isParec && fs.existsSync(rawFile)) {
628
+ try {
629
+ (0, child_process_1.execSync)(`ffmpeg -y -f s16le -ar ${SAMPLE_RATE} -ac 1 -i "${rawFile}" "${wavFile}" 2>/dev/null`, { stdio: 'pipe', timeout: 10000 });
630
+ }
631
+ catch { }
632
+ }
633
+ try {
634
+ fs.unlinkSync(path.join(stateDir, 'parec'));
635
+ }
636
+ catch { }
637
+ if (!fs.existsSync(wavFile) || fs.statSync(wavFile).size < 512) {
638
+ notify('🎤 dic', 'Nothing recorded.');
639
+ console.log(chalk_1.default.hex('#cc9e5c')(' Nothing recorded.\n'));
640
+ try {
641
+ fs.unlinkSync(rawFile);
642
+ }
643
+ catch { }
644
+ return;
645
+ }
646
+ notify('🎤 dic', 'Transcribing…');
647
+ const providers = listProviders();
648
+ if (providers.length === 0) {
649
+ notify('🎤 dic', 'No STT API key set.');
650
+ console.error(chalk_1.default.hex('#b15439')(' No STT API key set.\n'));
651
+ return;
652
+ }
653
+ let cleaned = '';
654
+ try {
655
+ cleaned = await normalizeAndTranscribe(wavFile, providers);
656
+ }
657
+ catch (err) {
658
+ notify('🎤 dic', 'Transcription failed.');
659
+ console.error(chalk_1.default.hex('#b15439')(' Transcription failed: ' + String(err) + '\n'));
660
+ return;
661
+ }
662
+ finally {
663
+ try {
664
+ fs.unlinkSync(wavFile);
665
+ }
666
+ catch { }
667
+ try {
668
+ fs.unlinkSync(rawFile);
669
+ }
670
+ catch { }
671
+ }
672
+ if (!cleaned) {
673
+ notify('🎤 dic', 'No speech detected.');
674
+ console.log(chalk_1.default.hex('#cc9e5c')(' No speech detected.\n'));
675
+ return;
676
+ }
677
+ console.log(chalk_1.default.hex('#ede0cc')(' ' + cleaned));
678
+ try {
679
+ await injectText(cleaned, { submit: opts.submit ?? true });
680
+ notify('🎤 dic', '✓ ' + cleaned.slice(0, 80));
681
+ }
682
+ catch (err) {
683
+ // injectText already copied to clipboard on the way in.
684
+ notify('🎤 dic', 'Typed failed — on clipboard.');
685
+ console.log(chalk_1.default.hex('#cc9e5c')(' Injection failed (' + String(err) + ') — text is on the clipboard.\n'));
686
+ }
687
+ return;
688
+ }
689
+ // ── START branch: no recording yet → begin one ───────────────────────────
690
+ fs.mkdirSync(stateDir, { recursive: true });
691
+ const recorderInfo = pickRecorder(opts.deviceId);
692
+ if (!recorderInfo) {
693
+ notify('🎤 dic', 'No recorder (install pw-record/arecord).');
694
+ console.error(chalk_1.default.hex('#b15439')(' No audio recorder found.\n'));
695
+ return;
696
+ }
697
+ let child;
698
+ if (recorderInfo.cmd === 'parec') {
699
+ // Mark this as the raw-PCM path so STOP knows to wrap it.
700
+ fs.writeFileSync(path.join(stateDir, 'parec'), '');
701
+ child = (0, child_process_1.spawn)(recorderInfo.cmd, [...recorderInfo.args], {
702
+ stdio: ['ignore', fs.openSync(rawFile, 'w'), 'ignore'],
703
+ detached: true,
704
+ });
705
+ }
706
+ else {
707
+ child = (0, child_process_1.spawn)(recorderInfo.cmd, [...recorderInfo.args, wavFile], {
708
+ stdio: ['ignore', 'ignore', 'ignore'],
709
+ detached: true,
710
+ });
711
+ }
712
+ child.unref();
713
+ fs.writeFileSync(pidFile, String(child.pid));
714
+ notify('🎤 dic', 'Recording… (press again to send)');
715
+ console.log(chalk_1.default.hex('#5a9e6e')(' 🎤 Recording — press the hotkey again to stop & send.\n'));
716
+ }
717
+ // ─── TTS: text-to-speech via MiMo TTS ─────────────────────────────────────
718
+ async function speakText(text, voice) {
719
+ const apiKey = process.env.XIAOMI_API_KEY;
720
+ if (!apiKey) {
721
+ console.error(chalk_1.default.hex('#b15439')('\n XIAOMI_API_KEY required for MiMo TTS.\n'));
722
+ process.exit(1);
723
+ }
724
+ const baseURL = apiKey.startsWith('tp-')
725
+ ? (process.env.XIAOMI_BASE_URL || 'https://token-plan-sgp.xiaomimimo.com/v1')
726
+ : MIMO_BASE;
727
+ const client = buildClient({ key: apiKey, baseURL });
728
+ const selectedVoice = voice || 'mimo_default';
729
+ console.log(chalk_1.default.hex('#cc785c')(' Generating speech (voice: ' + selectedVoice + ')...\n'));
730
+ try {
731
+ const response = await client.chat.completions.create({
732
+ model: 'mimo-v2.5-tts',
733
+ messages: [
734
+ {
735
+ role: 'user',
736
+ content: 'Read the following text clearly and naturally with appropriate expression.',
737
+ },
738
+ {
739
+ role: 'assistant',
740
+ content: text,
741
+ },
742
+ ],
743
+ audio: { format: 'wav', voice: selectedVoice },
744
+ });
745
+ const msg = response.choices[0]?.message;
746
+ const audioData = msg?.audio?.data;
747
+ if (!audioData) {
748
+ throw new Error('No audio data in response. Message keys: ' + Object.keys(msg || {}).join(', '));
749
+ }
750
+ // Decode base64 and play
751
+ const wavBuffer = Buffer.from(audioData, 'base64');
752
+ console.log(chalk_1.default.hex('#8a7768')(' Playing... (' + (wavBuffer.length / 1024).toFixed(0) + ' KB)\n'));
753
+ await playWav(wavBuffer);
754
+ }
755
+ catch (err) {
756
+ console.error(chalk_1.default.hex('#b15439')('\n TTS failed:'), String(err), '\n');
757
+ process.exit(1);
758
+ }
759
+ }
760
+ /**
761
+ * Synthesize speech and return the WAV bytes (no playback, no process.exit).
762
+ * For programmatic callers like the Telegram bot that need the audio buffer
763
+ * to send as a voice message. Throws on failure rather than exiting.
764
+ */
765
+ async function synthesizeSpeech(text, voice) {
766
+ const apiKey = process.env.XIAOMI_API_KEY;
767
+ if (!apiKey)
768
+ throw new Error('XIAOMI_API_KEY required for MiMo TTS');
769
+ const baseURL = apiKey.startsWith('tp-')
770
+ ? (process.env.XIAOMI_BASE_URL || 'https://token-plan-sgp.xiaomimimo.com/v1')
771
+ : MIMO_BASE;
772
+ const client = buildClient({ key: apiKey, baseURL });
773
+ const response = await client.chat.completions.create({
774
+ model: 'mimo-v2.5-tts',
775
+ messages: [
776
+ { role: 'user', content: 'Read the following text clearly and naturally with appropriate expression.' },
777
+ { role: 'assistant', content: text },
778
+ ],
779
+ audio: { format: 'wav', voice: voice || 'mimo_default' },
780
+ });
781
+ const audioData = response.choices[0]?.message?.audio?.data;
782
+ if (!audioData)
783
+ throw new Error('No audio data in TTS response');
784
+ return stripWavPrefix(Buffer.from(audioData, 'base64'));
785
+ }
786
+ /**
787
+ * Strip a stray CRLF (0x0D 0x0A) prefix that the MiMo TTS API occasionally
788
+ * prepends before the WAV RIFF header, which corrupts the file. Only strips
789
+ * the exact \r\n signature to stay safe if the API ever fixes this upstream.
790
+ */
791
+ function stripWavPrefix(buf) {
792
+ if (buf.length >= 2 && buf[0] === 0x0D && buf[1] === 0x0A) {
793
+ return buf.subarray(2);
794
+ }
795
+ return buf;
796
+ }
797
+ /**
798
+ * Transcribe an existing audio file to text using the provider fallback chain.
799
+ * For programmatic callers (e.g. the Telegram bot receiving a voice note).
800
+ * Accepts any format ffmpeg can read; normalizes to 16 kHz mono wav first.
801
+ */
802
+ async function transcribeFile(audioPath) {
803
+ const providers = listProviders();
804
+ if (providers.length === 0)
805
+ throw new Error('No STT provider configured (set XIAOMI_API_KEY, GROQ_API_KEY, …)');
806
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dic-tx-'));
807
+ const wavPath = path.join(tmpDir, 'in.wav');
808
+ try {
809
+ (0, child_process_1.execSync)(`ffmpeg -y -i "${audioPath}" -ar ${SAMPLE_RATE} -ac 1 "${wavPath}" 2>/dev/null`, { stdio: 'pipe', timeout: 15000 });
810
+ return await normalizeAndTranscribe(wavPath, providers);
811
+ }
812
+ finally {
813
+ cleanup(tmpDir);
814
+ }
815
+ }
816
+ // ─── List available TTS voices ─────────────────────────────────────────────
817
+ function listVoices() {
818
+ console.log(chalk_1.default.hex('#cc785c').bold('\n Xiaomi MiMo TTS Voices\n'));
819
+ console.log(chalk_1.default.hex('#4e3d30')(' ' + 'ID'.padEnd(16) + 'Gender'.padEnd(10) + 'Language'));
820
+ console.log(chalk_1.default.hex('#4e3d30')(' ' + '\u2500'.repeat(36)));
821
+ for (const [id, meta] of Object.entries(MIMO_VOICES)) {
822
+ const name = id === 'mimo_default' ? 'Auto (cluster default)' : id;
823
+ console.log(' ' + chalk_1.default.hex('#cc785c')(id.padEnd(16)) + ' ' +
824
+ chalk_1.default.hex('#8a7768')(meta.gender.padEnd(10)) + ' ' +
825
+ chalk_1.default.hex('#8a7768')(meta.lang === 'zh' ? 'Chinese' : meta.lang === 'en' ? 'English' : 'Auto'));
826
+ }
827
+ console.log(chalk_1.default.hex('#4e3d30')('\n Usage: dic speak <text> --voice <id>'));
828
+ console.log(chalk_1.default.hex('#4e3d30')(' Default: dic speak <text>\n'));
829
+ }
830
+ /**
831
+ * Continuous dictation: record → transcribe → inject → repeat.
832
+ * Stops on Ctrl+C. Uses arecord/parec with raw PCM for real-time silence detection.
833
+ */
834
+ async function dictationLoop(opts = {}) {
835
+ const providers = listProviders();
836
+ if (providers.length === 0) {
837
+ console.error(chalk_1.default.hex('#b15439')('\n No API key found. Set GROQ_API_KEY, OPENAI_API_KEY, or XIAOMI_API_KEY.\n'));
838
+ process.exit(1);
839
+ }
840
+ const deviceId = opts.deviceId;
841
+ const silenceMs = opts.silenceMs ?? 1500;
842
+ const maxDurationMs = opts.maxDurationMs ?? 60000;
843
+ const devName = deviceId || getDefaultDevice();
844
+ console.log(chalk_1.default.hex('#5a9e6e').bold('\n 🎙️ Dictation Loop — Continuous voice-to-text'));
845
+ if (devName)
846
+ console.log(chalk_1.default.hex('#8a7768')(' Device: ' + devName));
847
+ console.log(chalk_1.default.hex('#8a7768')(' Silence threshold: ' + silenceMs + 'ms'));
848
+ console.log(chalk_1.default.hex('#8a7768')(' Transcriptions will be injected into focused window.'));
849
+ console.log(chalk_1.default.hex('#cc9e5c')(' Press Ctrl+C to stop.\n'));
850
+ let running = true;
851
+ let roundNum = 0;
852
+ const onSigint = () => {
853
+ running = false;
854
+ console.log(chalk_1.default.hex('#cc9e5c')('\n Stopping dictation loop...\n'));
855
+ };
856
+ process.on('SIGINT', onSigint);
857
+ while (running) {
858
+ roundNum++;
859
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dic-loop-'));
860
+ const rawPath = path.join(tmpDir, 'recording.raw');
861
+ const wavPath = path.join(tmpDir, 'recording.wav');
862
+ try {
863
+ // ── Record with silence detection ──────────────────────────────
864
+ console.log(chalk_1.default.hex('#5a9e6e')(' ── Round ' + roundNum + ' ── Listening...\n'));
865
+ const recArgs = ['-r', String(SAMPLE_RATE), '-f', 'S16_LE', '-c', '1', '-t', 'raw',
866
+ ...(deviceId ? ['-D', deviceId] : []), rawPath];
867
+ const recorder = (0, child_process_1.spawn)('arecord', recArgs, { stdio: ['ignore', 'pipe', 'pipe'] });
868
+ let stopped = false;
869
+ const silenceThreshold = 400;
870
+ let lastSoundTime = Date.now();
871
+ const startTime = Date.now();
872
+ // Monitor the raw file for audio levels
873
+ const monitor = setInterval(() => {
874
+ if (!running || stopped)
875
+ return;
876
+ try {
877
+ if (!fs.existsSync(rawPath))
878
+ return;
879
+ const stat = fs.statSync(rawPath);
880
+ if (stat.size < SAMPLE_RATE * 2)
881
+ return; // need at least 1 second
882
+ // Read last 100ms of audio for level check
883
+ const chunkSize = SAMPLE_RATE * 2 * 0.1; // 100ms of s16le
884
+ const readStart = Math.max(0, stat.size - chunkSize);
885
+ const fd = fs.openSync(rawPath, 'r');
886
+ const buf = Buffer.alloc(Math.min(chunkSize, stat.size));
887
+ fs.readSync(fd, buf, 0, buf.length, readStart);
888
+ fs.closeSync(fd);
889
+ const level = rmsLevel(buf);
890
+ if (level > silenceThreshold) {
891
+ lastSoundTime = Date.now();
892
+ }
893
+ const elapsed = Date.now() - startTime;
894
+ const silenceElapsed = Date.now() - lastSoundTime;
895
+ if (silenceElapsed >= silenceMs && elapsed > 1000) {
896
+ // Silence detected — stop recording
897
+ stopped = true;
898
+ recorder.kill('SIGTERM');
899
+ }
900
+ else if (elapsed >= maxDurationMs) {
901
+ stopped = true;
902
+ recorder.kill('SIGTERM');
903
+ }
904
+ }
905
+ catch { }
906
+ }, 200);
907
+ await new Promise((resolve) => {
908
+ recorder.on('close', () => resolve());
909
+ if (!running) {
910
+ recorder.kill('SIGTERM');
911
+ }
912
+ });
913
+ clearInterval(monitor);
914
+ if (!running)
915
+ break;
916
+ // ── Convert raw to WAV ─────────────────────────────────────────
917
+ if (!fs.existsSync(rawPath) || fs.statSync(rawPath).size < SAMPLE_RATE) {
918
+ console.log(chalk_1.default.hex('#cc9e5c')(' Too short, skipping...\n'));
919
+ cleanup(tmpDir);
920
+ continue;
921
+ }
922
+ try {
923
+ (0, child_process_1.execSync)(`ffmpeg -y -f s16le -ar ${SAMPLE_RATE} -ac 1 -i "${rawPath}" -af "dynaudnorm=p=0.9:r=0.5" "${wavPath}"`, { stdio: 'pipe', timeout: 10000 });
924
+ }
925
+ catch {
926
+ // Try without normalization
927
+ try {
928
+ (0, child_process_1.execSync)(`ffmpeg -y -f s16le -ar ${SAMPLE_RATE} -ac 1 -i "${rawPath}" "${wavPath}"`, { stdio: 'pipe', timeout: 10000 });
929
+ }
930
+ catch {
931
+ console.log(chalk_1.default.hex('#cc9e5c')(' Conversion failed, skipping...\n'));
932
+ cleanup(tmpDir);
933
+ continue;
934
+ }
935
+ }
936
+ // ── Transcribe ─────────────────────────────────────────────────
937
+ let text = '';
938
+ for (let i = 0; i < providers.length && running; i++) {
939
+ try {
940
+ text = await transcribeWith(providers[i], wavPath, false);
941
+ break;
942
+ }
943
+ catch (err) {
944
+ if (shouldFallThrough(err) && i < providers.length - 1)
945
+ continue;
946
+ console.error(chalk_1.default.hex('#b15439')(' Transcribe error: ' + String(err)));
947
+ break;
948
+ }
949
+ }
950
+ const cleaned = text.trim();
951
+ if (cleaned) {
952
+ console.log(chalk_1.default.hex('#ede0cc')(' ' + cleaned));
953
+ try {
954
+ await injectText(cleaned);
955
+ console.log(chalk_1.default.hex('#5a9e6e')(' ✓ Injected\n'));
956
+ }
957
+ catch (err) {
958
+ // Fallback to clipboard
959
+ try {
960
+ const clipboardModule = await Promise.resolve().then(() => __importStar(require('./clipboard.js')));
961
+ await clipboardModule.clipboardTool({ action: 'copy', text: cleaned });
962
+ console.log(chalk_1.default.hex('#cc9e5c')(' 📋 Copied to clipboard (inject failed)\n'));
963
+ }
964
+ catch { }
965
+ }
966
+ }
967
+ else {
968
+ console.log(chalk_1.default.hex('#cc9e5c')(' (no speech detected)\n'));
969
+ }
970
+ }
971
+ catch (err) {
972
+ if (!running)
973
+ break;
974
+ console.error(chalk_1.default.hex('#b15439')(' Error: ' + String(err)));
975
+ }
976
+ cleanup(tmpDir);
977
+ if (running)
978
+ await new Promise(r => setTimeout(r, 300)); // brief pause between rounds
979
+ }
980
+ process.removeListener('SIGINT', onSigint);
981
+ console.log(chalk_1.default.hex('#5a9e6e').bold(' Dictation loop ended.\n'));
982
+ }
983
+ //# sourceMappingURL=dictate.js.map