task-summary-extractor 8.3.0 → 9.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 (44) hide show
  1. package/.env.example +38 -0
  2. package/ARCHITECTURE.md +99 -3
  3. package/EXPLORATION.md +148 -89
  4. package/QUICK_START.md +5 -2
  5. package/README.md +51 -7
  6. package/bin/taskex.js +11 -4
  7. package/package.json +38 -5
  8. package/src/config.js +52 -3
  9. package/src/modes/focused-reanalysis.js +2 -1
  10. package/src/modes/progress-updater.js +1 -1
  11. package/src/phases/_shared.js +43 -0
  12. package/src/phases/compile.js +101 -0
  13. package/src/phases/deep-dive.js +118 -0
  14. package/src/phases/discover.js +178 -0
  15. package/src/phases/init.js +192 -0
  16. package/src/phases/output.js +238 -0
  17. package/src/phases/process-media.js +633 -0
  18. package/src/phases/services.js +104 -0
  19. package/src/phases/summary.js +86 -0
  20. package/src/pipeline.js +431 -1463
  21. package/src/renderers/docx.js +531 -0
  22. package/src/renderers/html.js +672 -0
  23. package/src/renderers/markdown.js +15 -183
  24. package/src/renderers/pdf.js +90 -0
  25. package/src/renderers/shared.js +211 -0
  26. package/src/schemas/analysis-compiled.schema.json +381 -0
  27. package/src/schemas/analysis-segment.schema.json +380 -0
  28. package/src/services/doc-parser.js +346 -0
  29. package/src/services/gemini.js +101 -44
  30. package/src/services/video.js +123 -8
  31. package/src/utils/adaptive-budget.js +6 -4
  32. package/src/utils/checkpoint.js +2 -1
  33. package/src/utils/cli.js +131 -110
  34. package/src/utils/colors.js +83 -0
  35. package/src/utils/confidence-filter.js +138 -0
  36. package/src/utils/diff-engine.js +2 -1
  37. package/src/utils/global-config.js +6 -5
  38. package/src/utils/health-dashboard.js +11 -9
  39. package/src/utils/json-parser.js +4 -2
  40. package/src/utils/learning-loop.js +3 -2
  41. package/src/utils/progress-bar.js +286 -0
  42. package/src/utils/quality-gate.js +4 -2
  43. package/src/utils/retry.js +3 -1
  44. package/src/utils/schema-validator.js +314 -0
@@ -14,6 +14,7 @@ const fs = require('fs');
14
14
  const path = require('path');
15
15
  const { SPEED, SEG_TIME, PRESET } = require('../config');
16
16
  const { fmtDuration } = require('../utils/format');
17
+ const { c } = require('../utils/colors');
17
18
 
18
19
  // ======================== BINARY DETECTION ========================
19
20
 
@@ -181,7 +182,7 @@ function compressAndSegment(inputFile, outputDir) {
181
182
 
182
183
  const result = spawnSync(getFFmpeg(), args, { stdio: 'inherit' });
183
184
  if (result.status !== 0) {
184
- console.warn(` ffmpeg exited with code ${result.status} (output may still be usable)`);
185
+ console.warn(` ${c.warn(`ffmpeg exited with code ${result.status} (output may still be usable)`)}`);
185
186
  }
186
187
  } else {
187
188
  console.log(` Compressing (single output, ${effectiveDuration ? fmtDuration(effectiveDuration) : '?'} effective)...`);
@@ -196,7 +197,7 @@ function compressAndSegment(inputFile, outputDir) {
196
197
 
197
198
  const result = spawnSync(getFFmpeg(), args, { stdio: 'inherit' });
198
199
  if (result.status !== 0) {
199
- console.warn(` ffmpeg exited with code ${result.status}`);
200
+ console.warn(` ${c.warn(`ffmpeg exited with code ${result.status}`)}`);
200
201
  }
201
202
  }
202
203
 
@@ -214,7 +215,7 @@ function compressAndSegment(inputFile, outputDir) {
214
215
  valid.push(seg);
215
216
  } else {
216
217
  corrupt.push(seg);
217
- console.warn(` Corrupt segment detected: ${path.basename(seg)} (missing moov atom)`);
218
+ console.warn(` ${c.warn(`Corrupt segment detected: ${path.basename(seg)} (missing moov atom)`)}`);
218
219
  }
219
220
  }
220
221
 
@@ -239,7 +240,7 @@ function compressAndSegment(inputFile, outputDir) {
239
240
  const dest = path.join(outputDir, 'segment_00.mp4');
240
241
  fs.renameSync(fallbackPath, dest);
241
242
  segments = [dest];
242
- console.log(` Re-encoded successfully as single segment`);
243
+ console.log(` ${c.success('Re-encoded successfully as single segment')}`);
243
244
  } else {
244
245
  // Re-segment the fallback
245
246
  const reSegDir = path.join(outputDir, '_reseg');
@@ -264,10 +265,10 @@ function compressAndSegment(inputFile, outputDir) {
264
265
  .filter(f => f.startsWith('segment_') && f.endsWith('.mp4'))
265
266
  .sort()
266
267
  .map(f => path.join(outputDir, f));
267
- console.log(` Re-segmented from fallback: ${segments.length} segment(s)`);
268
+ console.log(` ${c.success(`Re-segmented from fallback: ${segments.length} segment(s)`)}`);
268
269
  }
269
270
  } else {
270
- console.error(` Fallback re-encode also failed`);
271
+ console.error(` ${c.error('Fallback re-encode also failed')}`);
271
272
  try { fs.unlinkSync(fallbackPath); } catch {}
272
273
  }
273
274
  } else if (corrupt.length > 0 && !needsSegmentation) {
@@ -285,9 +286,122 @@ function compressAndSegment(inputFile, outputDir) {
285
286
  const retryResult = spawnSync(getFFmpeg(), retryArgs, { stdio: 'inherit' });
286
287
  if (retryResult.status === 0 && verifySegment(retryPath)) {
287
288
  segments = [retryPath];
288
- console.log(` Retry succeeded`);
289
+ console.log(` ${c.success('Retry succeeded')}`);
289
290
  } else {
290
- console.error(` Retry also produced invalid output`);
291
+ console.error(` ${c.error('Retry also produced invalid output')}`);
292
+ }
293
+ }
294
+
295
+ return segments;
296
+ }
297
+
298
+ /**
299
+ * Compress and segment an audio-only file using ffmpeg.
300
+ * No video stream — just audio compression + segmentation in MP4/M4A container
301
+ * (for Gemini File API compatibility).
302
+ *
303
+ * Returns sorted array of segment file paths.
304
+ */
305
+ function compressAndSegmentAudio(inputFile, outputDir) {
306
+ fs.mkdirSync(outputDir, { recursive: true });
307
+
308
+ const duration = probeFormat(inputFile, 'duration');
309
+ const durationSec = duration ? parseFloat(duration) : null;
310
+ const effectiveDuration = durationSec ? durationSec / SPEED : null;
311
+ const channels = parseInt(probe(inputFile, 'a:0', 'channels') || '1', 10);
312
+ const sampleRate = probe(inputFile, 'a:0', 'sample_rate') || '16000';
313
+ const audioBr = channels >= 2 ? '128k' : '64k';
314
+
315
+ console.log(` Duration : ${duration ? fmtDuration(parseFloat(duration)) : 'unknown'}${effectiveDuration ? ` (${fmtDuration(effectiveDuration)} at ${SPEED}x)` : ''}`);
316
+ console.log(` Audio-only mode | ${SPEED}x speed | ${audioBr} bitrate`);
317
+
318
+ const encodingArgs = [
319
+ '-af', `atempo=${SPEED}`,
320
+ '-c:a', 'aac', '-b:a', audioBr, '-ar', sampleRate, '-ac', String(channels),
321
+ '-vn', // no video
322
+ '-movflags', '+faststart',
323
+ ];
324
+
325
+ const needsSegmentation = effectiveDuration === null || effectiveDuration > SEG_TIME;
326
+
327
+ if (needsSegmentation) {
328
+ console.log(` Compressing (segmented, ${SEG_TIME}s chunks)...`);
329
+ const args = [
330
+ '-y', '-i', inputFile,
331
+ ...encodingArgs,
332
+ '-f', 'segment', '-segment_time', String(SEG_TIME), '-reset_timestamps', '1',
333
+ path.join(outputDir, 'segment_%02d.m4a'),
334
+ ];
335
+ const result = spawnSync(getFFmpeg(), args, { stdio: 'inherit' });
336
+ if (result.status !== 0) {
337
+ console.warn(` ${c.warn(`ffmpeg exited with code ${result.status} (output may still be usable)`)}`);
338
+ }
339
+ } else {
340
+ console.log(` Compressing (single output, ${effectiveDuration ? fmtDuration(effectiveDuration) : '?'} effective)...`);
341
+ const outPath = path.join(outputDir, 'segment_00.m4a');
342
+ const args = ['-y', '-i', inputFile, ...encodingArgs, outPath];
343
+ const result = spawnSync(getFFmpeg(), args, { stdio: 'inherit' });
344
+ if (result.status !== 0) {
345
+ console.warn(` ${c.warn(`ffmpeg exited with code ${result.status}`)}`);
346
+ }
347
+ }
348
+
349
+ // Collect segments (both .mp4 and .m4a)
350
+ let segments = fs.readdirSync(outputDir)
351
+ .filter(f => f.startsWith('segment_') && (f.endsWith('.m4a') || f.endsWith('.mp4')))
352
+ .sort()
353
+ .map(f => path.join(outputDir, f));
354
+
355
+ // Validate segments
356
+ const valid = [];
357
+ const corrupt = [];
358
+ for (const seg of segments) {
359
+ if (verifySegment(seg)) {
360
+ valid.push(seg);
361
+ } else {
362
+ corrupt.push(seg);
363
+ console.warn(` ${c.warn(`Corrupt audio segment: ${path.basename(seg)}`)}`);
364
+ }
365
+ }
366
+
367
+ if (corrupt.length > 0) {
368
+ console.log(` Retrying ${corrupt.length} corrupt segment(s)...`);
369
+ const fallbackPath = path.join(outputDir, '_fallback_full.m4a');
370
+ const fbArgs = ['-y', '-i', inputFile, ...encodingArgs, fallbackPath];
371
+ const fbResult = spawnSync(getFFmpeg(), fbArgs, { stdio: 'inherit' });
372
+ if (fbResult.status === 0 && verifySegment(fallbackPath)) {
373
+ for (const seg of corrupt) { try { fs.unlinkSync(seg); } catch {} }
374
+ if (segments.length === 1) {
375
+ const dest = path.join(outputDir, 'segment_00.m4a');
376
+ fs.renameSync(fallbackPath, dest);
377
+ segments = [dest];
378
+ console.log(` ${c.success('Re-encoded as single segment')}`);
379
+ } else {
380
+ // Re-segment
381
+ const reSegDir = path.join(outputDir, '_reseg');
382
+ fs.mkdirSync(reSegDir, { recursive: true });
383
+ const rsArgs = [
384
+ '-y', '-i', fallbackPath,
385
+ '-c', 'copy', '-vn',
386
+ '-f', 'segment', '-segment_time', String(SEG_TIME), '-reset_timestamps', '1',
387
+ path.join(reSegDir, 'segment_%02d.m4a'),
388
+ ];
389
+ spawnSync(getFFmpeg(), rsArgs, { stdio: 'inherit' });
390
+ const reSegs = fs.readdirSync(reSegDir).filter(f => f.endsWith('.m4a')).sort();
391
+ for (const f of reSegs) {
392
+ fs.renameSync(path.join(reSegDir, f), path.join(outputDir, f));
393
+ }
394
+ try { fs.rmSync(reSegDir, { recursive: true }); } catch {}
395
+ try { fs.unlinkSync(fallbackPath); } catch {}
396
+ segments = fs.readdirSync(outputDir)
397
+ .filter(f => f.startsWith('segment_') && (f.endsWith('.m4a') || f.endsWith('.mp4')))
398
+ .sort()
399
+ .map(f => path.join(outputDir, f));
400
+ console.log(` ${c.success(`Re-segmented from fallback: ${segments.length} segment(s)`)}`);
401
+ }
402
+ } else {
403
+ console.error(` ${c.error('Fallback audio re-encode failed')}`);
404
+ try { fs.unlinkSync(fallbackPath); } catch {}
291
405
  }
292
406
  }
293
407
 
@@ -299,6 +413,7 @@ module.exports = {
299
413
  probe,
300
414
  probeFormat,
301
415
  compressAndSegment,
416
+ compressAndSegmentAudio,
302
417
  verifySegment,
303
418
  getFFmpeg,
304
419
  getFFprobe,
@@ -14,6 +14,8 @@
14
14
 
15
15
  'use strict';
16
16
 
17
+ const config = require('../config');
18
+
17
19
  // ======================== BUDGET RANGES ========================
18
20
 
19
21
  const BUDGET = {
@@ -21,12 +23,12 @@ const BUDGET = {
21
23
  MIN: 8192,
22
24
  /** Base thinking budget for a simple segment */
23
25
  BASE: 16384,
24
- /** Maximum thinking budget per segment (avoid eating output token pool) */
25
- MAX: 32768,
26
+ /** Maximum thinking budget per segment dynamically read from model config */
27
+ get MAX() { return config.getMaxThinkingBudget(); },
26
28
  /** Base compilation thinking budget */
27
29
  COMPILATION_BASE: 10240,
28
- /** Max compilation thinking budget */
29
- COMPILATION_MAX: 24576,
30
+ /** Max compilation thinking budget — dynamically read from model config */
31
+ get COMPILATION_MAX() { return config.getMaxThinkingBudget(); },
30
32
  };
31
33
 
32
34
  // ======================== COMPLEXITY ANALYSIS ========================
@@ -11,6 +11,7 @@
11
11
 
12
12
  const fs = require('fs');
13
13
  const path = require('path');
14
+ const { c } = require('./colors');
14
15
 
15
16
  const STATE_FILE = '.pipeline-state.json';
16
17
 
@@ -68,7 +69,7 @@ class Progress {
68
69
  try {
69
70
  fs.writeFileSync(this.filePath, JSON.stringify(this.state, null, 2), 'utf8');
70
71
  } catch (err) {
71
- console.warn(` Could not save progress: ${err.message}`);
72
+ console.warn(` ${c.warn(`Could not save progress: ${err.message}`)}`);
72
73
  }
73
74
  }
74
75
 
package/src/utils/cli.js CHANGED
@@ -17,6 +17,7 @@
17
17
 
18
18
  const fs = require('fs');
19
19
  const path = require('path');
20
+ const { c } = require('./colors');
20
21
 
21
22
  /**
22
23
  * Parse command-line arguments into flags and positional args.
@@ -36,6 +37,7 @@ function parseArgs(argv) {
36
37
  'resume', 'reanalyze', 'dry-run',
37
38
  'dynamic', 'deep-dive', 'update-progress',
38
39
  'no-focused-pass', 'no-learning', 'no-diff',
40
+ 'no-html',
39
41
  ]);
40
42
 
41
43
  for (let i = 0; i < argv.length; i++) {
@@ -89,7 +91,12 @@ const SKIP_FOLDER_NAMES = new Set([
89
91
  */
90
92
  function discoverFolders(projectRoot) {
91
93
  const VIDEO_EXTS = new Set(['.mp4', '.mkv', '.avi', '.mov', '.webm']);
92
- const DOC_EXTS = new Set(['.vtt', '.txt', '.pdf', '.docx', '.doc', '.srt', '.csv', '.md']);
94
+ const AUDIO_EXTS = new Set(['.mp3', '.wav', '.m4a', '.ogg', '.flac', '.aac', '.wma']);
95
+ const DOC_EXTS = new Set([
96
+ '.vtt', '.txt', '.pdf', '.docx', '.doc', '.srt', '.csv', '.md',
97
+ '.xlsx', '.xls', '.pptx', '.ppt', '.odt', '.odp', '.ods', '.rtf', '.epub',
98
+ '.html', '.htm',
99
+ ]);
93
100
  const folders = [];
94
101
 
95
102
  let entries;
@@ -105,6 +112,7 @@ function discoverFolders(projectRoot) {
105
112
 
106
113
  const absPath = path.join(projectRoot, entry.name);
107
114
  let hasVideo = false;
115
+ let hasAudio = false;
108
116
  let docCount = 0;
109
117
  let hasRuns = false;
110
118
 
@@ -116,6 +124,7 @@ function discoverFolders(projectRoot) {
116
124
  if (item.isFile()) {
117
125
  const ext = path.extname(item.name).toLowerCase();
118
126
  if (VIDEO_EXTS.has(ext)) hasVideo = true;
127
+ if (AUDIO_EXTS.has(ext)) hasAudio = true;
119
128
  if (DOC_EXTS.has(ext)) docCount++;
120
129
  } else if (item.isDirectory() && depth === 0) {
121
130
  if (item.name === 'runs') hasRuns = true;
@@ -128,15 +137,17 @@ function discoverFolders(projectRoot) {
128
137
  scan(absPath);
129
138
 
130
139
  // Only include folders with at least some content
131
- if (hasVideo || docCount > 0) {
140
+ if (hasVideo || hasAudio || docCount > 0) {
132
141
  const parts = [];
133
142
  if (hasVideo) parts.push('video');
143
+ if (hasAudio) parts.push('audio');
134
144
  if (docCount > 0) parts.push(`${docCount} doc(s)`);
135
145
  if (hasRuns) parts.push('has runs');
136
146
  folders.push({
137
147
  name: entry.name,
138
148
  absPath,
139
149
  hasVideo,
150
+ hasAudio,
140
151
  docCount,
141
152
  hasRuns,
142
153
  description: parts.join(', '),
@@ -159,18 +170,23 @@ async function selectFolder(projectRoot) {
159
170
  const folders = discoverFolders(projectRoot);
160
171
 
161
172
  if (folders.length === 0) {
162
- console.log('\n No call/project folders found in the current directory.');
163
- console.log(' Create a folder with your recording or documents, then run again.\n');
173
+ console.log('');
174
+ console.log(c.warn('No call/project folders found in the current directory.'));
175
+ console.log(c.muted(' Create a folder with your recording or documents, then run again.'));
176
+ console.log('');
164
177
  return null;
165
178
  }
166
179
 
167
180
  console.log('');
168
- console.log(' Available folders:');
169
- console.log(' ─────────────────');
181
+ console.log(c.heading(' 📂 Available Folders'));
182
+ console.log(c.dim(' ' + '─'.repeat(50)));
170
183
  folders.forEach((f, i) => {
171
- const icon = f.hasVideo ? '🎥' : '📄';
172
- const mode = f.hasVideo ? '' : ' (docs only → use --dynamic)';
173
- console.log(` [${i + 1}] ${icon} ${f.name} — ${f.description}${mode}`);
184
+ const icon = f.hasVideo ? '🎥' : f.hasAudio ? '🎵' : '📄';
185
+ const num = c.cyan(`[${i + 1}]`);
186
+ const name = c.bold(f.name);
187
+ const desc = c.dim(f.description);
188
+ const mode = (!f.hasVideo && !f.hasAudio) ? c.yellow(' (docs only)') : '';
189
+ console.log(` ${num} ${icon} ${name} ${desc}${mode}`);
174
190
  });
175
191
  console.log('');
176
192
 
@@ -237,24 +253,24 @@ async function selectModel(GEMINI_MODELS, currentModel) {
237
253
  }
238
254
 
239
255
  console.log('');
240
- console.log(' ┌──────────────────────────────────────────────────────────────────────────────┐');
241
- console.log(' │ 🤖 Gemini Model Selection │');
242
- console.log(' └──────────────────────────────────────────────────────────────────────────────┘');
256
+ console.log(c.heading(' ┌──────────────────────────────────────────────────────────────────────────────┐'));
257
+ console.log(c.heading(' │ 🤖 Gemini Model Selection │'));
258
+ console.log(c.heading(' └──────────────────────────────────────────────────────────────────────────────┘'));
243
259
 
244
260
  for (const [, tier] of Object.entries(tiers)) {
245
261
  if (tier.models.length === 0) continue;
246
262
  console.log('');
247
- console.log(` ${tier.icon} ${tier.label}`);
248
- console.log(' ' + '─'.repeat(76));
263
+ console.log(` ${tier.icon} ${c.bold(tier.label)}`);
264
+ console.log(c.dim(' ' + '─'.repeat(76)));
249
265
 
250
266
  for (const m of tier.models) {
251
267
  const isDefault = m.id === currentModel;
252
- const marker = isDefault ? ' ← default' : '';
253
- const thinkTag = m.thinking ? ' [thinking]' : '';
268
+ const marker = isDefault ? c.green(' ← default') : '';
269
+ const thinkTag = m.thinking ? c.magenta(' [thinking]') : '';
254
270
 
255
271
  // Line 1: number, name, description
256
- console.log(` [${m.idx}] ${m.name}${thinkTag}${marker}`);
257
- console.log(` ${m.description}`);
272
+ console.log(` ${c.cyan(`[${m.idx}]`)} ${c.bold(m.name)}${thinkTag}${marker}`);
273
+ console.log(` ${c.dim(m.description)}`);
258
274
 
259
275
  // Line 2: specs
260
276
  const ctxStr = fmtContext(m.contextWindow);
@@ -262,8 +278,8 @@ async function selectModel(GEMINI_MODELS, currentModel) {
262
278
  const inPrice = `$${m.pricing.inputPerM.toFixed(m.pricing.inputPerM < 0.1 ? 4 : 2)}/1M in`;
263
279
  const outPrice = `$${m.pricing.outputPerM.toFixed(m.pricing.outputPerM < 1 ? 2 : 2)}/1M out`;
264
280
  const thinkPrice = m.thinking ? ` · $${m.pricing.thinkingPerM.toFixed(2)}/1M think` : '';
265
- console.log(` Context: ${ctxStr} tokens · Max output: ${outStr} · ${m.costEstimate}`);
266
- console.log(` Pricing: ${inPrice} · ${outPrice}${thinkPrice}`);
281
+ console.log(` ${c.dim('Context:')} ${ctxStr} · ${c.dim('Max output:')} ${outStr} · ${c.highlight(m.costEstimate)}`);
282
+ console.log(` ${c.dim('Pricing:')} ${inPrice} · ${outPrice}${thinkPrice}`);
267
283
  }
268
284
  }
269
285
 
@@ -277,7 +293,7 @@ async function selectModel(GEMINI_MODELS, currentModel) {
277
293
 
278
294
  // Enter = keep default
279
295
  if (!trimmed) {
280
- console.log(`Using ${GEMINI_MODELS[currentModel].name}`);
296
+ console.log(c.success(`Using ${GEMINI_MODELS[currentModel].name}`));
281
297
  resolve(currentModel);
282
298
  return;
283
299
  }
@@ -286,14 +302,14 @@ async function selectModel(GEMINI_MODELS, currentModel) {
286
302
  const num = parseInt(trimmed, 10);
287
303
  if (!isNaN(num) && indexMap[num]) {
288
304
  const chosen = indexMap[num];
289
- console.log(`Selected ${GEMINI_MODELS[chosen].name}`);
305
+ console.log(c.success(`Selected ${GEMINI_MODELS[chosen].name}`));
290
306
  resolve(chosen);
291
307
  return;
292
308
  }
293
309
 
294
310
  // Direct model ID input
295
311
  if (GEMINI_MODELS[trimmed]) {
296
- console.log(`Selected ${GEMINI_MODELS[trimmed].name}`);
312
+ console.log(c.success(`Selected ${GEMINI_MODELS[trimmed].name}`));
297
313
  resolve(trimmed);
298
314
  return;
299
315
  }
@@ -305,12 +321,12 @@ async function selectModel(GEMINI_MODELS, currentModel) {
305
321
  GEMINI_MODELS[id].name.toLowerCase().includes(lower)
306
322
  );
307
323
  if (match) {
308
- console.log(`Matched ${GEMINI_MODELS[match].name}`);
324
+ console.log(c.success(`Matched ${GEMINI_MODELS[match].name}`));
309
325
  resolve(match);
310
326
  return;
311
327
  }
312
328
 
313
- console.log(`Unknown selection "${trimmed}" — using default (${currentModel})`);
329
+ console.log(c.warn(`Unknown selection "${trimmed}" — using default (${currentModel})`));
314
330
  resolve(currentModel);
315
331
  });
316
332
  });
@@ -321,92 +337,97 @@ async function selectModel(GEMINI_MODELS, currentModel) {
321
337
  * Callers should catch this and exit cleanly (no process.exit in library code).
322
338
  */
323
339
  function showHelp() {
340
+ const h = (s) => c.heading(s);
341
+ const f = (flag, desc) => ` ${c.green(flag.padEnd(38))}${desc}`;
342
+ const f2 = (desc) => ` ${''.padEnd(38)}${c.dim(desc)}`;
343
+
344
+ const pkg = (() => {
345
+ try { return JSON.parse(fs.readFileSync(path.join(__dirname, '..', '..', 'package.json'), 'utf8')); }
346
+ catch { return { version: '?.?.?' }; }
347
+ })();
348
+
324
349
  console.log(`
325
- Usage: taskex [options] [folder]
326
- taskex config [--show | --clear]
327
- node process_and_upload.js [options] [folder]
328
-
329
- AI-powered meeting analysis & document generation pipeline.
330
- If no folder is specified, shows an interactive folder selector.
331
- If you cd into a folder, just run: taskex
332
-
333
- Subcommands:
334
- config Interactive global config setup (~/.taskexrc)
335
- config --show Show saved config (masked secrets)
336
- config --clear Remove global config file
337
-
338
- Arguments:
339
- [folder] Path to the call/project folder (optional — interactive if omitted)
340
-
341
- Modes:
342
- (default) Video analysis compress, analyze, extract, compile
343
- --dynamic Document-only mode no video required, generates docs from context + request
344
- --update-progress Track item completion via git since last analysis
345
- --deep-dive (after video analysis) Generate explanatory docs per topic discussed
346
-
347
- Core Options:
348
- --name <name> Your name (skips interactive prompt)
349
- --model <id> Gemini model to use (skips interactive selector)
350
- Models: gemini-3.1-pro-preview, gemini-3-flash-preview,
351
- gemini-2.5-pro, gemini-2.5-flash (default), gemini-2.5-flash-lite
352
- --skip-upload Skip all Firebase Storage uploads
353
- --force-upload Re-upload files even if they already exist in Storage
354
- --no-storage-url Disable Storage URL optimization (force Gemini File API)
355
- --skip-compression Skip video compression (use existing segments)
356
- --skip-gemini Skip Gemini AI analysis
357
- --resume Resume from last checkpoint (skip completed steps)
358
- --reanalyze Force re-analysis of all segments
359
- --dry-run Show what would be done without executing
360
-
361
- Dynamic Mode:
362
- --dynamic Enable document-only mode (no video required)
363
- --request <text> What to generate — e.g. "Plan migration from X to Y"
364
- (prompted interactively if omitted)
365
-
366
- Progress Tracking:
367
- --repo <path> Path to the project git repo (for change detection)
368
-
369
- Configuration:
370
- --gemini-key <key> Gemini API key (overrides .env / ~/.taskexrc)
371
- --firebase-key <key> Firebase API key (overrides .env / ~/.taskexrc)
372
- --firebase-project <id> Firebase project ID (overrides .env / ~/.taskexrc)
373
- --firebase-bucket <bucket> Firebase storage bucket (overrides .env / ~/.taskexrc)
374
- --firebase-domain <domain> Firebase auth domain (overrides .env / ~/.taskexrc)
375
-
376
- Config resolution (highest wins):
377
- CLI flags → env vars → CWD .env → ~/.taskexrc → package .env
378
-
379
- Tuning:
380
- --parallel <n> Max parallel uploads (default: 3)
381
- --parallel-analysis <n> Concurrent segment analysis batches (default: 2)
382
- --thinking-budget <n> Thinking token budget per segment (default: 24576)
383
- --compilation-thinking-budget <n> Thinking tokens for final compilation (default: 10240)
384
- --log-level <level> Log level: debug, info, warn, error (default: info)
385
- --output <dir> Custom output directory for results
386
- --no-focused-pass Disable focused re-analysis for weak segments
387
- --no-learning Disable learning loop (historical budget adjustments)
388
- --no-diff Disable diff comparison against previous runs
389
-
390
- Info:
391
- --help, -h Show this help message
392
- --version, -v Show version
393
-
394
- Examples:
395
- taskex Interactive (cd into folder first)
396
- taskex "call 1" Analyze a call (with video)
397
- taskex --name "Jane" --skip-upload "call 1" Skip Firebase, set name
398
- taskex --gemini-key "AIza..." --skip-upload "call 1" Pass API key inline (no .env)
399
- taskex --model gemini-2.5-pro "call 1" Use Gemini 2.5 Pro model
400
- taskex --resume "call 1" Resume interrupted run
401
- taskex --deep-dive "call 1" Video analysis + deep dive docs
402
- taskex --dynamic "my-project" Doc-only mode (prompted for request)
403
- taskex --dynamic --request "Plan API migration" "specs" Dynamic with request
404
- taskex --update-progress --repo "C:\\my-project" "call 1" Progress tracking via git
405
-
406
- First-time setup:
407
- taskex config Save API keys globally (~/.taskexrc)
408
- taskex config --show View saved config
409
- taskex config --clear Remove saved config
350
+ ${c.bold(c.cyan('taskex'))} ${c.dim(`v${pkg.version}`)} — AI-powered meeting analysis & document generation
351
+
352
+ ${h('USAGE')}
353
+ ${c.bold('taskex')} ${c.dim('[options]')} ${c.cyan('[folder]')}
354
+ ${c.bold('taskex setup')} ${c.dim('[--check | --silent]')}
355
+ ${c.bold('taskex config')} ${c.dim('[--show | --clear]')}
356
+
357
+ ${h('SUBCOMMANDS')}
358
+ ${f('setup', 'Full interactive setup (prerequisites, deps, .env)')}
359
+ ${f('setup --check', 'Validation only — verify environment')}
360
+ ${f('config', 'Interactive global config (~/.taskexrc)')}
361
+ ${f('config --show', 'Show saved config (masked secrets)')}
362
+ ${f('config --clear', 'Remove global config')}
363
+
364
+ ${h('MODES')}
365
+ ${f('(default)', 'Video/audio analysis — compress, analyze, compile')}
366
+ ${f('--dynamic', 'Document generation — no media required')}
367
+ ${f('--update-progress', 'Track item completion via git changes')}
368
+ ${f('--deep-dive', 'Generate explanatory docs per topic')}
369
+
370
+ ${h('CORE OPTIONS')}
371
+ ${f('--name <name>', 'Your name (skip interactive prompt)')}
372
+ ${f('--model <id>', 'Gemini model (skip interactive selector)')}
373
+ ${f('--format <type>', 'Output formats: md, html, json, pdf, docx, all (default: all)')}
374
+ ${f('--min-confidence <level>', 'Filter: high, medium, low (default: all)')}
375
+ ${f('--output <dir>', 'Custom output directory for results')}
376
+ ${f('--skip-upload', 'Skip Firebase Storage uploads')}
377
+ ${f('--skip-compression', 'Use existing segments (no re-compress)')}
378
+ ${f('--skip-gemini', 'Skip AI analysis')}
379
+ ${f('--resume', 'Resume from last checkpoint')}
380
+ ${f('--reanalyze', 'Force re-analysis of all segments')}
381
+ ${f('--dry-run', 'Preview without executing')}
382
+
383
+ ${h('TUNING')}
384
+ ${f('--parallel <n>', 'Max parallel uploads (default: 3)')}
385
+ ${f('--parallel-analysis <n>', 'Concurrent analysis batches (default: 2)')}
386
+ ${f('--thinking-budget <n>', 'Thinking tokens per segment (default: 24576)')}
387
+ ${f('--compilation-thinking-budget <n>', 'Thinking tokens for compilation (default: 10240)')}
388
+ ${f('--no-focused-pass', 'Disable focused re-analysis')}
389
+ ${f('--no-learning', 'Disable learning loop')}
390
+ ${f('--no-diff', 'Disable diff comparison')}
391
+ ${f('--no-html', 'Skip HTML output (Markdown only)')}
392
+ ${f('--log-level <level>', 'debug, info, warn, error (default: info)')}
393
+
394
+ ${h('DYNAMIC MODE')}
395
+ ${f('--dynamic', 'Enable document generation mode')}
396
+ ${f('--request <text>', 'What to generate (prompted if omitted)')}
397
+
398
+ ${h('PROGRESS TRACKING')}
399
+ ${f('--update-progress', 'Detect changes via git since last analysis')}
400
+ ${f('--repo <path>', 'Path to project git repo')}
401
+
402
+ ${h('UPLOAD & STORAGE')}
403
+ ${f('--skip-upload', 'Skip all Firebase uploads')}
404
+ ${f('--force-upload', 'Re-upload even if files exist')}
405
+ ${f('--no-storage-url', 'Force Gemini File API (no storage URLs)')}
406
+
407
+ ${h('CONFIGURATION')}
408
+ ${f('--gemini-key <key>', 'Gemini API key (overrides .env)')}
409
+ ${f('--firebase-key <key>', 'Firebase API key')}
410
+ ${f('--firebase-project <id>', 'Firebase project ID')}
411
+ ${f('--firebase-bucket <bucket>', 'Firebase storage bucket')}
412
+ ${f('--firebase-domain <domain>', 'Firebase auth domain')}
413
+ ${f2('Resolution: CLI flags env → .env → ~/.taskexrc')}
414
+
415
+ ${h('INFO')}
416
+ ${f('--help, -h', 'Show this help message')}
417
+ ${f('--version, -v', 'Show version')}
418
+
419
+ ${h('EXAMPLES')}
420
+ ${c.dim('$')} taskex ${c.dim('# Interactive mode')}
421
+ ${c.dim('$')} taskex "call 1" ${c.dim('# Analyze a call')}
422
+ ${c.dim('$')} taskex --name "Jane" --skip-upload "call 1"
423
+ ${c.dim('$')} taskex --model gemini-2.5-pro --deep-dive "call 1"
424
+ ${c.dim('$')} taskex --dynamic --request "Plan API migration" "specs"
425
+ ${c.dim('$')} taskex --min-confidence medium "call 1" ${c.dim('# Filter low-confidence')}
426
+ ${c.dim('$')} taskex --format md "call 1" ${c.dim('# Markdown only')}
427
+ ${c.dim('$')} taskex --format pdf "call 1" ${c.dim('# PDF report')}
428
+ ${c.dim('$')} taskex --format docx "call 1" ${c.dim('# Word document')}
429
+ ${c.dim('$')} taskex --resume "call 1" ${c.dim('# Resume interrupted run')}
430
+ ${c.dim('$')} taskex --update-progress --repo ./my-project "call 1"
410
431
  `);
411
432
  // Signal early exit — pipeline checks for help flag before calling this
412
433
  throw Object.assign(new Error('HELP_SHOWN'), { code: 'HELP_SHOWN' });