@laitszkin/apollo-toolkit 4.1.4 → 5.0.0

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 (205) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/bin/apollo-toolkit.ts +4 -0
  3. package/dist/bin/apollo-toolkit.js +4 -0
  4. package/package.json +4 -2
  5. package/packages/cli/dist/help-text-builder.d.ts +23 -0
  6. package/packages/cli/dist/help-text-builder.js +166 -0
  7. package/packages/cli/dist/index.d.ts +6 -17
  8. package/packages/cli/dist/index.js +52 -246
  9. package/packages/cli/dist/installer.d.ts +1 -0
  10. package/packages/cli/dist/installer.js +20 -7
  11. package/packages/cli/dist/parsers/install-parser.d.ts +15 -0
  12. package/packages/cli/dist/parsers/install-parser.js +87 -0
  13. package/packages/cli/dist/parsers/parser-utils.d.ts +9 -0
  14. package/packages/cli/dist/parsers/parser-utils.js +16 -0
  15. package/packages/cli/dist/parsers/tool-parser.d.ts +16 -0
  16. package/packages/cli/dist/parsers/tool-parser.js +58 -0
  17. package/packages/cli/dist/parsers/types.d.ts +50 -0
  18. package/packages/cli/dist/parsers/types.js +1 -0
  19. package/packages/cli/dist/parsers/uninstall-parser.d.ts +15 -0
  20. package/packages/cli/dist/parsers/uninstall-parser.js +67 -0
  21. package/packages/cli/dist/tool-registration.d.ts +2 -0
  22. package/packages/cli/dist/tool-registration.js +2 -0
  23. package/packages/cli/dist/tsconfig.tsbuildinfo +1 -1
  24. package/packages/cli/dist/types.d.ts +3 -1
  25. package/packages/cli/dist/updater.js +11 -5
  26. package/packages/cli/help-text-builder.ts +180 -0
  27. package/packages/cli/index.ts +59 -251
  28. package/packages/cli/installer.ts +19 -7
  29. package/packages/cli/package.json +6 -3
  30. package/packages/cli/parsers/install-parser.ts +94 -0
  31. package/packages/cli/parsers/parser-utils.ts +17 -0
  32. package/packages/cli/parsers/tool-parser.ts +65 -0
  33. package/packages/cli/parsers/types.ts +56 -0
  34. package/packages/cli/parsers/uninstall-parser.ts +75 -0
  35. package/packages/cli/tool-registration.ts +3 -0
  36. package/packages/cli/types.ts +6 -1
  37. package/packages/cli/updater.ts +11 -5
  38. package/packages/tool-registry/dist/registry.js +3 -4
  39. package/packages/tool-registry/dist/tsconfig.tsbuildinfo +1 -1
  40. package/packages/tool-registry/dist/types.d.ts +2 -9
  41. package/packages/tool-registry/package.json +3 -3
  42. package/packages/tool-registry/registry.ts +3 -4
  43. package/packages/tool-registry/tsconfig.json +6 -2
  44. package/packages/tool-registry/types.ts +3 -9
  45. package/packages/tool-utils/app-error.ts +97 -0
  46. package/packages/tool-utils/dist/app-error.d.ts +49 -0
  47. package/packages/tool-utils/dist/app-error.js +80 -0
  48. package/packages/tool-utils/dist/index.d.ts +5 -0
  49. package/packages/tool-utils/dist/index.js +3 -0
  50. package/packages/tool-utils/dist/platform-adapter.d.ts +48 -0
  51. package/packages/tool-utils/dist/platform-adapter.js +73 -0
  52. package/packages/tool-utils/dist/schema.d.ts +68 -0
  53. package/packages/tool-utils/dist/schema.js +67 -0
  54. package/packages/tool-utils/dist/tsconfig.tsbuildinfo +1 -1
  55. package/packages/tool-utils/index.ts +12 -0
  56. package/packages/tool-utils/package.json +3 -3
  57. package/packages/tool-utils/platform-adapter.ts +112 -0
  58. package/packages/tool-utils/schema.ts +122 -0
  59. package/packages/tools/architecture/dist/index.d.ts +13 -0
  60. package/packages/tools/architecture/dist/index.js +55 -57
  61. package/packages/tools/architecture/dist/index.test.js +17 -4
  62. package/packages/tools/architecture/dist/tsconfig.tsbuildinfo +1 -1
  63. package/packages/tools/architecture/index.test.ts +27 -14
  64. package/packages/tools/architecture/index.ts +85 -88
  65. package/packages/tools/architecture/package.json +3 -3
  66. package/packages/tools/codegraph/dist/index.js +12 -22
  67. package/packages/tools/codegraph/dist/tsconfig.tsbuildinfo +1 -1
  68. package/packages/tools/codegraph/index.ts +13 -22
  69. package/packages/tools/codegraph/package.json +3 -3
  70. package/packages/tools/create-review-report/dist/index.d.ts +1 -2
  71. package/packages/tools/create-review-report/dist/index.js +46 -77
  72. package/packages/tools/create-review-report/dist/tsconfig.tsbuildinfo +1 -1
  73. package/packages/tools/create-review-report/index.ts +52 -81
  74. package/packages/tools/create-review-report/package.json +3 -3
  75. package/packages/tools/create-specs/dist/index.d.ts +1 -2
  76. package/packages/tools/create-specs/dist/index.js +70 -123
  77. package/packages/tools/create-specs/dist/tsconfig.tsbuildinfo +1 -1
  78. package/packages/tools/create-specs/index.ts +82 -128
  79. package/packages/tools/create-specs/package.json +3 -3
  80. package/packages/tools/docs-to-voice/dist/index.d.ts +1 -2
  81. package/packages/tools/docs-to-voice/dist/index.js +116 -219
  82. package/packages/tools/docs-to-voice/dist/tsconfig.tsbuildinfo +1 -1
  83. package/packages/tools/docs-to-voice/index.ts +265 -385
  84. package/packages/tools/docs-to-voice/package.json +3 -3
  85. package/packages/tools/enforce-video-aspect-ratio/dist/index.d.ts +1 -2
  86. package/packages/tools/enforce-video-aspect-ratio/dist/index.js +77 -154
  87. package/packages/tools/enforce-video-aspect-ratio/dist/tsconfig.tsbuildinfo +1 -1
  88. package/packages/tools/enforce-video-aspect-ratio/index.ts +87 -172
  89. package/packages/tools/enforce-video-aspect-ratio/package.json +3 -3
  90. package/packages/tools/eval/dist/index.js +7 -0
  91. package/packages/tools/eval/dist/tsconfig.tsbuildinfo +1 -1
  92. package/packages/tools/eval/index.ts +8 -0
  93. package/packages/tools/eval/package.json +3 -3
  94. package/packages/tools/extract-conversations/dist/index.d.ts +1 -2
  95. package/packages/tools/extract-conversations/dist/index.js +31 -29
  96. package/packages/tools/extract-conversations/dist/tsconfig.tsbuildinfo +1 -1
  97. package/packages/tools/extract-conversations/index.ts +37 -30
  98. package/packages/tools/extract-conversations/package.json +3 -3
  99. package/packages/tools/extract-pdf-text/dist/index.d.ts +1 -2
  100. package/packages/tools/extract-pdf-text/dist/index.js +44 -65
  101. package/packages/tools/extract-pdf-text/dist/tsconfig.tsbuildinfo +1 -1
  102. package/packages/tools/extract-pdf-text/index.ts +55 -74
  103. package/packages/tools/extract-pdf-text/package.json +3 -3
  104. package/packages/tools/filter-logs/dist/index.js +60 -84
  105. package/packages/tools/filter-logs/dist/tsconfig.tsbuildinfo +1 -1
  106. package/packages/tools/filter-logs/index.ts +67 -97
  107. package/packages/tools/filter-logs/package.json +3 -3
  108. package/packages/tools/find-github-issues/dist/index.d.ts +10 -0
  109. package/packages/tools/find-github-issues/dist/index.js +34 -5
  110. package/packages/tools/find-github-issues/dist/tsconfig.tsbuildinfo +1 -1
  111. package/packages/tools/find-github-issues/index.ts +37 -5
  112. package/packages/tools/find-github-issues/package.json +3 -3
  113. package/packages/tools/generate-storyboard-images/dist/index.d.ts +1 -2
  114. package/packages/tools/generate-storyboard-images/dist/index.js +98 -173
  115. package/packages/tools/generate-storyboard-images/dist/tsconfig.tsbuildinfo +1 -1
  116. package/packages/tools/generate-storyboard-images/index.ts +100 -188
  117. package/packages/tools/generate-storyboard-images/package.json +3 -3
  118. package/packages/tools/open-github-issue/dist/index.d.ts +13 -0
  119. package/packages/tools/open-github-issue/dist/index.js +67 -68
  120. package/packages/tools/open-github-issue/dist/tsconfig.tsbuildinfo +1 -1
  121. package/packages/tools/open-github-issue/index.ts +71 -72
  122. package/packages/tools/open-github-issue/package.json +3 -3
  123. package/packages/tools/read-github-issue/dist/index.d.ts +16 -1
  124. package/packages/tools/read-github-issue/dist/index.js +32 -40
  125. package/packages/tools/read-github-issue/dist/tsconfig.tsbuildinfo +1 -1
  126. package/packages/tools/read-github-issue/index.ts +32 -45
  127. package/packages/tools/read-github-issue/package.json +3 -3
  128. package/packages/tools/render-error-book/dist/index.d.ts +1 -2
  129. package/packages/tools/render-error-book/dist/index.js +74 -95
  130. package/packages/tools/render-error-book/dist/tsconfig.tsbuildinfo +1 -1
  131. package/packages/tools/render-error-book/index.ts +88 -103
  132. package/packages/tools/render-error-book/package.json +3 -3
  133. package/packages/tools/render-katex/dist/index.d.ts +1 -2
  134. package/packages/tools/render-katex/dist/index.js +70 -157
  135. package/packages/tools/render-katex/dist/tsconfig.tsbuildinfo +1 -1
  136. package/packages/tools/render-katex/index.ts +138 -222
  137. package/packages/tools/render-katex/package.json +3 -3
  138. package/packages/tools/review-threads/dist/index.d.ts +12 -0
  139. package/packages/tools/review-threads/dist/index.js +83 -86
  140. package/packages/tools/review-threads/dist/tsconfig.tsbuildinfo +1 -1
  141. package/packages/tools/review-threads/index.ts +90 -84
  142. package/packages/tools/review-threads/package.json +3 -3
  143. package/packages/tools/search-logs/dist/index.js +100 -136
  144. package/packages/tools/search-logs/dist/tsconfig.tsbuildinfo +1 -1
  145. package/packages/tools/search-logs/index.ts +113 -145
  146. package/packages/tools/search-logs/package.json +3 -3
  147. package/packages/tools/sync-memory-index/dist/index.js +34 -28
  148. package/packages/tools/sync-memory-index/dist/tsconfig.tsbuildinfo +1 -1
  149. package/packages/tools/sync-memory-index/index.ts +37 -28
  150. package/packages/tools/sync-memory-index/package.json +3 -3
  151. package/packages/tools/validate-openai-agent-config/dist/index.js +13 -7
  152. package/packages/tools/validate-openai-agent-config/dist/tsconfig.tsbuildinfo +1 -1
  153. package/packages/tools/validate-openai-agent-config/index.ts +13 -7
  154. package/packages/tools/validate-openai-agent-config/package.json +3 -3
  155. package/packages/tools/validate-skill-frontmatter/dist/index.js +12 -6
  156. package/packages/tools/validate-skill-frontmatter/dist/tsconfig.tsbuildinfo +1 -1
  157. package/packages/tools/validate-skill-frontmatter/index.ts +12 -6
  158. package/packages/tools/validate-skill-frontmatter/package.json +3 -3
  159. package/packages/tui/dist/index.d.ts +2 -1
  160. package/packages/tui/dist/index.js +1 -0
  161. package/packages/tui/dist/stdio-adapter.d.ts +36 -0
  162. package/packages/tui/dist/stdio-adapter.js +69 -0
  163. package/packages/tui/dist/terminal.js +3 -1
  164. package/packages/tui/dist/tsconfig.tsbuildinfo +1 -1
  165. package/packages/tui/dist/types.d.ts +17 -0
  166. package/packages/tui/index.ts +2 -1
  167. package/packages/tui/package.json +6 -5
  168. package/packages/tui/stdio-adapter.ts +85 -0
  169. package/packages/tui/terminal.ts +3 -1
  170. package/packages/tui/tsconfig.json +5 -2
  171. package/packages/tui/types.ts +19 -0
  172. package/resources/project-architecture/assets/architecture.css +2 -1
  173. package/resources/project-architecture/atlas/atlas.history.log +1 -0
  174. package/resources/project-architecture/atlas/atlas.history.undo.json +13 -2
  175. package/resources/project-architecture/atlas/atlas.history.undo.stack.json +610 -0
  176. package/resources/project-architecture/atlas/atlas.index.yaml +81 -5
  177. package/resources/project-architecture/atlas/features/cli-dispatch.yaml +43 -0
  178. package/resources/project-architecture/atlas/features/terminal-ui.yaml +29 -0
  179. package/resources/project-architecture/atlas/features/tool-registry.yaml +22 -0
  180. package/resources/project-architecture/atlas/features/tool-utils.yaml +22 -0
  181. package/resources/project-architecture/features/cli-dispatch/arg-parser.html +40 -0
  182. package/resources/project-architecture/features/cli-dispatch/help-builder.html +40 -0
  183. package/resources/project-architecture/features/cli-dispatch/index.html +64 -0
  184. package/resources/project-architecture/features/cli-dispatch/installer-core.html +40 -0
  185. package/resources/project-architecture/features/cli-dispatch/tool-discovery.html +40 -0
  186. package/resources/project-architecture/features/cli-dispatch/update-checker.html +40 -0
  187. package/resources/project-architecture/features/terminal-ui/banner-display.html +40 -0
  188. package/resources/project-architecture/features/terminal-ui/index.html +50 -0
  189. package/resources/project-architecture/features/terminal-ui/interactive-prompts.html +40 -0
  190. package/resources/project-architecture/features/terminal-ui/terminal-detection.html +40 -0
  191. package/resources/project-architecture/features/tool-registry/formatter.html +40 -0
  192. package/resources/project-architecture/features/tool-registry/index.html +43 -0
  193. package/resources/project-architecture/features/tool-registry/registry-core.html +40 -0
  194. package/resources/project-architecture/features/tool-utils/index.html +43 -0
  195. package/resources/project-architecture/features/tool-utils/log-utils.html +40 -0
  196. package/resources/project-architecture/features/tool-utils/skill-discovery.html +40 -0
  197. package/resources/project-architecture/index.html +365 -121
  198. package/scripts/rewrite-imports.mjs +2 -2
  199. package/scripts/test.sh +144 -8
  200. package/skills/design/SKILL.md +57 -64
  201. package/skills/design/assets/templates/DESIGN.md +12 -0
  202. package/skills/design/references/code-smells.md +94 -0
  203. package/skills/design/references/module-boundary-adjustment.md +126 -0
  204. package/skills/design/references/module-internal-restructuring.md +132 -0
  205. package/skills/design/references/module-internal-simplification.md +164 -0
@@ -3,112 +3,19 @@ import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import https from 'node:https';
5
5
  import http from 'node:http';
6
+ import { SystemError, UserInputError, createToolRunner } from '../../../tool-utils/dist/index.js';
6
7
  const DEFAULT_API_ENDPOINT = 'https://dashscope-intl.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation';
7
8
  const DEFAULT_API_MODEL = 'qwen3-tts';
8
9
  const DEFAULT_API_VOICE = 'Cherry';
9
- function parseArgs(args) {
10
- const parsed = {
11
- inputText: null,
12
- inputFile: null,
13
- projectDir: '.',
14
- projectName: null,
15
- outputName: null,
16
- mode: 'say',
17
- voice: null,
18
- rate: null,
19
- speechRate: null,
20
- apiEndpoint: DEFAULT_API_ENDPOINT,
21
- apiModel: DEFAULT_API_MODEL,
22
- apiVoice: DEFAULT_API_VOICE,
23
- apiKey: null,
24
- maxChars: null,
25
- noAutoProsody: false,
26
- force: false,
27
- help: false,
28
- };
29
- for (let i = 0; i < args.length; i++) {
30
- const arg = args[i];
31
- if (arg === '--help' || arg === '-h') {
32
- parsed.help = true;
33
- continue;
34
- }
35
- if (arg.startsWith('--')) {
36
- const eqIndex = arg.indexOf('=');
37
- let key;
38
- let value;
39
- if (eqIndex !== -1) {
40
- key = arg.slice(2, eqIndex);
41
- value = arg.slice(eqIndex + 1);
42
- }
43
- else {
44
- key = arg.slice(2);
45
- value = args[++i] || '';
46
- }
47
- switch (key) {
48
- case 'text':
49
- parsed.inputText = value;
50
- break;
51
- case 'input':
52
- case 'input-file':
53
- parsed.inputFile = value;
54
- break;
55
- case 'project-dir':
56
- parsed.projectDir = value;
57
- break;
58
- case 'project-name':
59
- parsed.projectName = value;
60
- break;
61
- case 'output-name':
62
- parsed.outputName = value;
63
- break;
64
- case 'engine':
65
- case 'mode':
66
- parsed.mode = value.toLowerCase();
67
- break;
68
- case 'voice':
69
- parsed.voice = value;
70
- break;
71
- case 'rate':
72
- parsed.rate = value;
73
- break;
74
- case 'speech-rate':
75
- parsed.speechRate = value;
76
- break;
77
- case 'api-endpoint':
78
- parsed.apiEndpoint = value;
79
- break;
80
- case 'api-model':
81
- parsed.apiModel = value;
82
- break;
83
- case 'api-voice':
84
- parsed.apiVoice = value;
85
- break;
86
- case 'api-key':
87
- parsed.apiKey = value;
88
- break;
89
- case 'max-chars':
90
- parsed.maxChars = value;
91
- break;
92
- case 'no-auto-prosody':
93
- parsed.noAutoProsody = true;
94
- break;
95
- case 'force':
96
- parsed.force = true;
97
- break;
98
- }
99
- }
100
- }
101
- return parsed;
102
- }
103
- function readInputText(opts) {
104
- if (opts.inputFile) {
105
- const inputPath = path.resolve(opts.inputFile);
10
+ function readInputText(inputFile, inputText) {
11
+ if (inputFile) {
12
+ const inputPath = path.resolve(inputFile);
106
13
  if (!fs.existsSync(inputPath)) {
107
- throw new Error(`Input file not found: ${inputPath}`);
14
+ throw new UserInputError(`Input file not found: ${inputPath}`);
108
15
  }
109
16
  return fs.readFileSync(inputPath, 'utf-8');
110
17
  }
111
- return opts.inputText || '';
18
+ return inputText || '';
112
19
  }
113
20
  function splitSentences(rawText) {
114
21
  const endings = new Set(['。', '!', '?', '!', '?', ';', ';']);
@@ -299,7 +206,7 @@ function applySpeechRateToAudio(outputPath, speechRate) {
299
206
  catch (err) {
300
207
  if (fs.existsSync(tmpPath))
301
208
  fs.unlinkSync(tmpPath);
302
- throw new Error(`ffmpeg failed while applying --speech-rate: ${err instanceof Error ? err.message : 'unknown error'}`);
209
+ throw new SystemError(`ffmpeg failed while applying --speech-rate: ${err instanceof Error ? err.message : 'unknown error'}`, undefined, { cause: err });
303
210
  }
304
211
  }
305
212
  function splitTextForTts(text, maxChars) {
@@ -344,7 +251,7 @@ function splitTextForTts(text, maxChars) {
344
251
  }
345
252
  function concatAudioFiles(partPaths, outputPath) {
346
253
  if (partPaths.length === 0) {
347
- throw new Error('No chunk audio generated for concatenation.');
254
+ throw new SystemError('No chunk audio generated for concatenation.');
348
255
  }
349
256
  if (partPaths.length === 1) {
350
257
  fs.copyFileSync(partPaths[0], outputPath);
@@ -359,7 +266,7 @@ function concatAudioFiles(partPaths, outputPath) {
359
266
  execSync(`ffmpeg -hide_banner -loglevel error -y -f concat -safe 0 -i "${listFile}" -c:a copy "${outputPath}"`, { stdio: 'ignore', timeout: 120000 });
360
267
  }
361
268
  catch (err) {
362
- throw new Error(`ffmpeg concat failed: ${err instanceof Error ? err.message : 'unknown error'}`);
269
+ throw new SystemError(`ffmpeg concat failed: ${err instanceof Error ? err.message : 'unknown error'}`, undefined, { cause: err });
363
270
  }
364
271
  finally {
365
272
  try {
@@ -415,104 +322,104 @@ function requestAlibabaCloudTTS(endpoint, apiKey, model, voice, text) {
415
322
  const audioData = audio.data || '';
416
323
  const audioFormat = audio.format || audio.mime_type || '';
417
324
  if (!audioUrl && !audioData) {
418
- reject(new Error('API response does not contain output.audio.url or output.audio.data'));
325
+ reject(new SystemError('API response does not contain output.audio.url or output.audio.data'));
419
326
  return;
420
327
  }
421
328
  resolve({ audioUrl, audioData, audioFormat });
422
329
  }
423
330
  catch {
424
- reject(new Error('API response is not valid JSON.'));
331
+ reject(new SystemError('API response is not valid JSON.'));
425
332
  }
426
333
  });
427
334
  res.on('error', reject);
428
335
  });
429
336
  req.on('error', reject);
430
- req.on('timeout', () => { req.destroy(); reject(new Error('API request timed out')); });
337
+ req.on('timeout', () => { req.destroy(); reject(new SystemError('API request timed out')); });
431
338
  req.write(payload);
432
339
  req.end();
433
340
  });
434
341
  }
435
- export async function docsToVoiceHandler(args, context) {
436
- const stdout = context.stdout || process.stdout;
437
- const stderr = context.stderr || process.stderr;
438
- try {
439
- const opts = parseArgs(args);
440
- if (opts.help) {
441
- stdout.write(`Usage: apltk docs-to-voice [options]
442
-
443
- Convert text into audio and sentence timelines.
444
-
445
- Options:
446
- --input, --input-file <path> Path to input text file
447
- --text <string> Raw text input
448
- --project-dir <path> Root project directory (default: .)
449
- --project-name <name> Folder name under DIR/audio/
450
- --output-name <name> Output filename
451
- --engine, --mode <mode> TTS mode: say (default) | api
452
- --voice <name> macOS say voice
453
- --rate <wpm> macOS say rate
454
- --speech-rate <factor> Speech rate multiplier (e.g. 1.2)
455
- --api-endpoint <url> Alibaba Cloud TTS endpoint
456
- --api-model <name> Alibaba Cloud model (default: qwen3-tts)
457
- --api-voice <name> Alibaba Cloud voice (default: Cherry)
458
- --api-key <key> Alibaba Cloud API key
459
- --max-chars <n> Max chars per TTS chunk (0 disables)
460
- --no-auto-prosody Disable punctuation pause enhancement
461
- --force Overwrite existing files
462
- `);
463
- return 0;
464
- }
465
- if (opts.mode !== 'say' && opts.mode !== 'api') {
466
- stderr.write('Error: --mode must be one of: say, api\n');
467
- return 1;
342
+ const schema = {
343
+ options: {
344
+ 'text': { type: 'string' },
345
+ 'input': { type: 'string' },
346
+ 'input-file': { type: 'string' },
347
+ 'project-dir': { type: 'string', default: '.' },
348
+ 'project-name': { type: 'string' },
349
+ 'output-name': { type: 'string' },
350
+ 'engine': { type: 'string' },
351
+ 'mode': { type: 'string', default: 'say' },
352
+ 'voice': { type: 'string' },
353
+ 'rate': { type: 'string' },
354
+ 'speech-rate': { type: 'string' },
355
+ 'api-endpoint': { type: 'string', default: DEFAULT_API_ENDPOINT },
356
+ 'api-model': { type: 'string', default: DEFAULT_API_MODEL },
357
+ 'api-voice': { type: 'string', default: DEFAULT_API_VOICE },
358
+ 'api-key': { type: 'string' },
359
+ 'max-chars': { type: 'string' },
360
+ 'no-auto-prosody': { type: 'boolean', default: false },
361
+ 'force': { type: 'boolean', default: false },
362
+ },
363
+ allowPositionals: true,
364
+ usage: 'apltk docs-to-voice [options]',
365
+ description: 'Convert text into audio and sentence timelines.',
366
+ handler: async (values, _positionals, context) => {
367
+ const stdout = context.stdout ?? process.stdout;
368
+ const stderr = context.stderr ?? process.stderr;
369
+ // Resolve args (previously handled by parseCliArgs)
370
+ const inputText = values['text'] ?? null;
371
+ const inputFile = values['input'] || values['input-file'] || null;
372
+ const projectDir = values['project-dir'] || '.';
373
+ const projectName = values['project-name'] ?? null;
374
+ const outputName = values['output-name'] ?? null;
375
+ const mode = (values['mode'] || values['engine'] || 'say').toLowerCase();
376
+ const voice = values['voice'] ?? null;
377
+ const rate = values['rate'] ?? null;
378
+ const speechRate = values['speech-rate'] ?? null;
379
+ const apiEndpoint = values['api-endpoint'] || DEFAULT_API_ENDPOINT;
380
+ const apiModel = values['api-model'] || DEFAULT_API_MODEL;
381
+ const apiVoice = values['api-voice'] || DEFAULT_API_VOICE;
382
+ const apiKey = values['api-key'] ?? null;
383
+ const maxChars = values['max-chars'] ?? null;
384
+ const noAutoProsody = !!values['no-auto-prosody'];
385
+ const force = !!values['force'];
386
+ if (mode !== 'say' && mode !== 'api') {
387
+ throw new UserInputError('--mode must be one of: say, api');
468
388
  }
469
- const sourceText = readInputText(opts);
389
+ const sourceText = readInputText(inputFile, inputText);
470
390
  if (!sourceText.trim()) {
471
- stderr.write('Error: No text content found for conversion.\n');
472
- return 1;
391
+ throw new UserInputError('No text content found for conversion.');
473
392
  }
474
393
  // Resolve output directory
475
- const projectDir = path.resolve(opts.projectDir);
476
- const projectName = opts.projectName || path.basename(projectDir);
477
- if (!projectName) {
478
- stderr.write('Error: Unable to determine project name.\n');
479
- return 1;
394
+ const resolvedProjectDir = path.resolve(projectDir);
395
+ const resolvedProjectName = projectName || path.basename(resolvedProjectDir);
396
+ if (!resolvedProjectName) {
397
+ throw new UserInputError('Unable to determine project name.');
480
398
  }
481
- const outputDir = path.join(projectDir, 'audio', projectName);
399
+ const outputDir = path.join(resolvedProjectDir, 'audio', resolvedProjectName);
482
400
  fs.mkdirSync(outputDir, { recursive: true });
483
401
  const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
484
- const outputName = opts.outputName || `voice-${timestamp}`;
485
- const hasExtension = outputName.includes('.');
486
- if (opts.mode === 'say') {
487
- // macOS say mode
488
- const textChunks = splitTextForTts(sourceText, opts.maxChars ? parseInt(opts.maxChars, 10) || null : null);
489
- if (textChunks.length === 0) {
490
- stderr.write('Error: No text content found for conversion.\n');
491
- return 1;
492
- }
493
- // Check if `say` is available
494
- try {
495
- execSync('which say', { stdio: 'ignore' });
496
- }
497
- catch {
498
- stderr.write("Error: macOS 'say' command not found.\n");
499
- return 1;
500
- }
501
- const finalOutputName = hasExtension ? outputName : `${outputName}.aiff`;
402
+ const outName = outputName || `voice-${timestamp}`;
403
+ const hasExtension = outName.includes('.');
404
+ if (mode === 'say') {
405
+ const finalOutputName = hasExtension ? outName : `${outName}.aiff`;
502
406
  const outputPath = path.join(outputDir, finalOutputName);
503
- if (fs.existsSync(outputPath) && !opts.force) {
504
- stderr.write(`Error: Output already exists: ${outputPath}. Use --force to overwrite.\n`);
505
- return 1;
407
+ if (fs.existsSync(outputPath) && !force) {
408
+ throw new UserInputError(`Output already exists: ${outputPath}. Use --force to overwrite.`);
506
409
  }
507
410
  // Build prosody-enhanced text
508
- const chunks = opts.noAutoProsody ? textChunks : textChunks.map(buildAutoProsodyText);
411
+ const textChunks = splitTextForTts(sourceText, maxChars ? parseInt(maxChars, 10) || null : null);
412
+ if (textChunks.length === 0) {
413
+ throw new UserInputError('No text content found for conversion.');
414
+ }
415
+ const chunks = noAutoProsody ? textChunks : textChunks.map(buildAutoProsodyText);
509
416
  if (chunks.length === 1) {
510
417
  // Single say command
511
418
  const sayArgs = ['-o', outputPath];
512
- if (opts.voice)
513
- sayArgs.push('-v', opts.voice);
514
- if (opts.rate)
515
- sayArgs.push('-r', opts.rate);
419
+ if (voice)
420
+ sayArgs.push('-v', voice);
421
+ if (rate)
422
+ sayArgs.push('-r', rate);
516
423
  const tmpFile = path.join(fs.mkdtempSync('docs-to-voice-'), 'input.txt');
517
424
  fs.mkdirSync(path.dirname(tmpFile), { recursive: true });
518
425
  fs.writeFileSync(tmpFile, chunks[0], 'utf-8');
@@ -525,7 +432,7 @@ Options:
525
432
  }
526
433
  catch (err) {
527
434
  const msg = err instanceof Error ? err.message : 'unknown error';
528
- throw new Error(`say mode failed: ${msg}`);
435
+ throw new SystemError(`say mode failed: ${msg}`);
529
436
  }
530
437
  finally {
531
438
  try {
@@ -543,15 +450,15 @@ Options:
543
450
  try {
544
451
  for (let i = 0; i < chunks.length; i++) {
545
452
  const partPath = path.join(tempDir, `part-${String(i + 1).padStart(4, '0')}${partExt}`);
546
- const sayArgs = ['-o', partPath];
547
- if (opts.voice)
548
- sayArgs.push('-v', opts.voice);
549
- if (opts.rate)
550
- sayArgs.push('-r', opts.rate);
551
- const tmpFile = path.join(tempDir, `chunk-${i}.txt`);
552
- fs.writeFileSync(tmpFile, chunks[i], 'utf-8');
553
- sayArgs.push('-f', tmpFile);
554
- execSync(`say ${sayArgs.map((a) => (a.includes(' ') ? `"${a}"` : a)).join(' ')}`, { stdio: 'ignore', timeout: 300000 });
453
+ const sArgs = ['-o', partPath];
454
+ if (voice)
455
+ sArgs.push('-v', voice);
456
+ if (rate)
457
+ sArgs.push('-r', rate);
458
+ const tFile = path.join(tempDir, `chunk-${i}.txt`);
459
+ fs.writeFileSync(tFile, chunks[i], 'utf-8');
460
+ sArgs.push('-f', tFile);
461
+ execSync(`say ${sArgs.map((a) => (a.includes(' ') ? `"${a}"` : a)).join(' ')}`, { stdio: 'ignore', timeout: 300000 });
555
462
  partPaths.push(partPath);
556
463
  }
557
464
  concatAudioFiles(partPaths, outputPath);
@@ -564,10 +471,10 @@ Options:
564
471
  }
565
472
  }
566
473
  // Apply speech rate if requested
567
- if (opts.speechRate) {
568
- const rate = parseFloat(opts.speechRate);
569
- if (rate > 0)
570
- applySpeechRateToAudio(outputPath, rate);
474
+ if (speechRate) {
475
+ const rateVal = parseFloat(speechRate);
476
+ if (rateVal > 0)
477
+ applySpeechRateToAudio(outputPath, rateVal);
571
478
  }
572
479
  // Write timeline files
573
480
  writeTimelineFiles(sourceText, outputPath, null);
@@ -575,23 +482,20 @@ Options:
575
482
  }
576
483
  else {
577
484
  // API mode
578
- const apiKey = opts.apiKey;
579
485
  if (!apiKey) {
580
- stderr.write('Error: --api-key is required for api mode.\n');
581
- return 1;
486
+ throw new UserInputError('--api-key is required for api mode.');
582
487
  }
583
488
  const sentences = splitSentences(sourceText);
584
489
  if (sentences.length === 0) {
585
- stderr.write('Error: No text content found for conversion.\n');
586
- return 1;
490
+ throw new UserInputError('No text content found for conversion.');
587
491
  }
588
- const maxChars = opts.maxChars ? parseInt(opts.maxChars, 10) || null : null;
492
+ const maxCharsNum = maxChars ? parseInt(maxChars, 10) || null : null;
589
493
  const requestItems = [];
590
494
  for (let si = 0; si < sentences.length; si++) {
591
495
  const sentence = sentences[si];
592
- if (maxChars && sentence.length > maxChars) {
593
- for (let i = 0; i < sentence.length; i += maxChars) {
594
- requestItems.push({ sentenceIndex: si, text: sentence.slice(i, i + maxChars) });
496
+ if (maxCharsNum && sentence.length > maxCharsNum) {
497
+ for (let i = 0; i < sentence.length; i += maxCharsNum) {
498
+ requestItems.push({ sentenceIndex: si, text: sentence.slice(i, i + maxCharsNum) });
595
499
  }
596
500
  }
597
501
  else {
@@ -599,8 +503,7 @@ Options:
599
503
  }
600
504
  }
601
505
  if (requestItems.length === 0) {
602
- stderr.write('Error: No text content found for conversion.\n');
603
- return 1;
506
+ throw new UserInputError('No text content found for conversion.');
604
507
  }
605
508
  const tempDir = fs.mkdtempSync('docs-to-voice-api-');
606
509
  const partPaths = [];
@@ -610,7 +513,7 @@ Options:
610
513
  try {
611
514
  for (let i = 0; i < requestItems.length; i++) {
612
515
  const item = requestItems[i];
613
- const apiResult = await requestAlibabaCloudTTS(opts.apiEndpoint, apiKey, opts.apiModel, opts.apiVoice, item.text);
516
+ const apiResult = await requestAlibabaCloudTTS(apiEndpoint, apiKey, apiModel, apiVoice, item.text);
614
517
  const currentExt = apiResult.audioFormat || 'wav';
615
518
  if (!partExt)
616
519
  partExt = currentExt;
@@ -622,10 +525,10 @@ Options:
622
525
  fs.writeFileSync(partPath, Buffer.from(apiResult.audioData, 'base64'));
623
526
  }
624
527
  else {
625
- throw new Error('No audio data in API response.');
528
+ throw new SystemError('No audio data in API response.');
626
529
  }
627
530
  if (!fs.existsSync(partPath) || fs.statSync(partPath).size === 0) {
628
- throw new Error(`Failed to generate audio chunk ${i + 1}.`);
531
+ throw new SystemError(`Failed to generate audio chunk ${i + 1}.`);
629
532
  }
630
533
  partPaths.push(partPath);
631
534
  const partDuration = readDurationSeconds(partPath);
@@ -637,12 +540,11 @@ Options:
637
540
  }
638
541
  }
639
542
  const finalOutputName = hasExtension
640
- ? outputName
641
- : `${outputName}.${partExt || 'wav'}`;
543
+ ? outName
544
+ : `${outName}.${partExt || 'wav'}`;
642
545
  const outputPath = path.join(outputDir, finalOutputName);
643
- if (fs.existsSync(outputPath) && !opts.force) {
644
- stderr.write(`Error: Output already exists: ${outputPath}. Use --force to overwrite.\n`);
645
- return 1;
546
+ if (fs.existsSync(outputPath) && !force) {
547
+ throw new UserInputError(`Output already exists: ${outputPath}. Use --force to overwrite.`);
646
548
  }
647
549
  concatAudioFiles(partPaths, outputPath);
648
550
  // Build timeline durations
@@ -670,12 +572,12 @@ Options:
670
572
  }
671
573
  }
672
574
  // Apply speech rate if requested
673
- if (opts.speechRate) {
674
- const rate = parseFloat(opts.speechRate);
675
- if (rate > 0) {
676
- applySpeechRateToAudio(outputPath, rate);
575
+ if (speechRate) {
576
+ const rateVal = parseFloat(speechRate);
577
+ if (rateVal > 0) {
578
+ applySpeechRateToAudio(outputPath, rateVal);
677
579
  if (timelineDurations) {
678
- timelineDurations = timelineDurations.map((d) => d / rate);
580
+ timelineDurations = timelineDurations.map((d) => d / rateVal);
679
581
  }
680
582
  }
681
583
  }
@@ -690,16 +592,11 @@ Options:
690
592
  }
691
593
  }
692
594
  return 0;
693
- }
694
- catch (err) {
695
- const msg = err instanceof Error ? err.message : 'Unknown error';
696
- stderr.write(`Error: ${msg}\n`);
697
- return 1;
698
- }
699
- }
595
+ },
596
+ };
700
597
  export const tool = {
701
598
  name: 'docs-to-voice',
702
599
  category: 'media',
703
600
  description: 'Convert text into audio and sentence timelines.',
704
- handler: docsToVoiceHandler,
601
+ handler: createToolRunner(schema),
705
602
  };