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.
- package/.env.example +38 -0
- package/ARCHITECTURE.md +99 -3
- package/EXPLORATION.md +148 -89
- package/QUICK_START.md +5 -2
- package/README.md +51 -7
- package/bin/taskex.js +11 -4
- package/package.json +38 -5
- package/src/config.js +52 -3
- package/src/modes/focused-reanalysis.js +2 -1
- package/src/modes/progress-updater.js +1 -1
- package/src/phases/_shared.js +43 -0
- package/src/phases/compile.js +101 -0
- package/src/phases/deep-dive.js +118 -0
- package/src/phases/discover.js +178 -0
- package/src/phases/init.js +192 -0
- package/src/phases/output.js +238 -0
- package/src/phases/process-media.js +633 -0
- package/src/phases/services.js +104 -0
- package/src/phases/summary.js +86 -0
- package/src/pipeline.js +431 -1463
- package/src/renderers/docx.js +531 -0
- package/src/renderers/html.js +672 -0
- package/src/renderers/markdown.js +15 -183
- package/src/renderers/pdf.js +90 -0
- package/src/renderers/shared.js +211 -0
- package/src/schemas/analysis-compiled.schema.json +381 -0
- package/src/schemas/analysis-segment.schema.json +380 -0
- package/src/services/doc-parser.js +346 -0
- package/src/services/gemini.js +101 -44
- package/src/services/video.js +123 -8
- package/src/utils/adaptive-budget.js +6 -4
- package/src/utils/checkpoint.js +2 -1
- package/src/utils/cli.js +131 -110
- package/src/utils/colors.js +83 -0
- package/src/utils/confidence-filter.js +138 -0
- package/src/utils/diff-engine.js +2 -1
- package/src/utils/global-config.js +6 -5
- package/src/utils/health-dashboard.js +11 -9
- package/src/utils/json-parser.js +4 -2
- package/src/utils/learning-loop.js +3 -2
- package/src/utils/progress-bar.js +286 -0
- package/src/utils/quality-gate.js +4 -2
- package/src/utils/retry.js +3 -1
- package/src/utils/schema-validator.js +314 -0
package/src/services/video.js
CHANGED
|
@@ -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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
268
|
+
console.log(` ${c.success(`Re-segmented from fallback: ${segments.length} segment(s)`)}`);
|
|
268
269
|
}
|
|
269
270
|
} else {
|
|
270
|
-
console.error(`
|
|
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(`
|
|
289
|
+
console.log(` ${c.success('Retry succeeded')}`);
|
|
289
290
|
} else {
|
|
290
|
-
console.error(`
|
|
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
|
|
25
|
-
MAX
|
|
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
|
|
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 ========================
|
package/src/utils/checkpoint.js
CHANGED
|
@@ -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(`
|
|
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
|
|
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('
|
|
163
|
-
console.log('
|
|
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
|
|
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
|
|
173
|
-
|
|
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}
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
324
|
+
console.log(c.success(`Matched ${GEMINI_MODELS[match].name}`));
|
|
309
325
|
resolve(match);
|
|
310
326
|
return;
|
|
311
327
|
}
|
|
312
328
|
|
|
313
|
-
console.log(`
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
taskex
|
|
396
|
-
taskex "call 1"
|
|
397
|
-
taskex --name "Jane" --skip-upload "call 1"
|
|
398
|
-
taskex --gemini-
|
|
399
|
-
taskex --
|
|
400
|
-
taskex --
|
|
401
|
-
taskex --
|
|
402
|
-
taskex --
|
|
403
|
-
taskex --
|
|
404
|
-
taskex --
|
|
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' });
|