task-summary-extractor 8.3.0 → 9.0.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 (46) 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/prompt.json +2 -2
  9. package/src/config.js +52 -3
  10. package/src/logger.js +7 -4
  11. package/src/modes/focused-reanalysis.js +2 -1
  12. package/src/modes/progress-updater.js +1 -1
  13. package/src/phases/_shared.js +43 -0
  14. package/src/phases/compile.js +101 -0
  15. package/src/phases/deep-dive.js +118 -0
  16. package/src/phases/discover.js +178 -0
  17. package/src/phases/init.js +199 -0
  18. package/src/phases/output.js +238 -0
  19. package/src/phases/process-media.js +633 -0
  20. package/src/phases/services.js +104 -0
  21. package/src/phases/summary.js +86 -0
  22. package/src/pipeline.js +432 -1464
  23. package/src/renderers/docx.js +531 -0
  24. package/src/renderers/html.js +672 -0
  25. package/src/renderers/markdown.js +15 -183
  26. package/src/renderers/pdf.js +90 -0
  27. package/src/renderers/shared.js +215 -0
  28. package/src/schemas/analysis-compiled.schema.json +381 -0
  29. package/src/schemas/analysis-segment.schema.json +380 -0
  30. package/src/services/doc-parser.js +346 -0
  31. package/src/services/gemini.js +118 -45
  32. package/src/services/video.js +123 -8
  33. package/src/utils/adaptive-budget.js +6 -4
  34. package/src/utils/checkpoint.js +2 -1
  35. package/src/utils/cli.js +132 -111
  36. package/src/utils/colors.js +83 -0
  37. package/src/utils/confidence-filter.js +139 -0
  38. package/src/utils/diff-engine.js +2 -1
  39. package/src/utils/global-config.js +6 -5
  40. package/src/utils/health-dashboard.js +11 -9
  41. package/src/utils/json-parser.js +4 -2
  42. package/src/utils/learning-loop.js +3 -2
  43. package/src/utils/progress-bar.js +286 -0
  44. package/src/utils/quality-gate.js +10 -8
  45. package/src/utils/retry.js +3 -1
  46. package/src/utils/schema-validator.js +314 -0
@@ -0,0 +1,118 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+
5
+ // --- Modes ---
6
+ const { discoverTopics, generateAllDocuments, writeDeepDiveOutput } = require('../modes/deep-dive');
7
+
8
+ // --- Utils ---
9
+ const { c } = require('../utils/colors');
10
+
11
+ // --- Shared state ---
12
+ const { getLog, phaseTimer, PROJECT_ROOT } = require('./_shared');
13
+
14
+ // ======================== PHASE: DEEP DIVE ========================
15
+
16
+ /**
17
+ * Generate explanatory documents for topics discussed in the meeting.
18
+ * Two-phase: discover topics → generate documents in parallel.
19
+ */
20
+ async function phaseDeepDive(ctx, compiledAnalysis, runDir) {
21
+ const log = getLog();
22
+ const timer = phaseTimer('deep_dive');
23
+ const { ai, callName, userName, costTracker, opts, contextDocs } = ctx;
24
+
25
+ console.log('');
26
+ console.log(c.cyan('══════════════════════════════════════════════'));
27
+ console.log(c.heading(' DEEP DIVE — Generating Explanatory Documents'));
28
+ console.log(c.cyan('══════════════════════════════════════════════'));
29
+ console.log('');
30
+
31
+ const thinkingBudget = opts.thinkingBudget ||
32
+ require('../config').DEEP_DIVE_THINKING_BUDGET;
33
+
34
+ // Gather context snippets from inline text docs (for richer AI context)
35
+ const contextSnippets = [];
36
+ for (const doc of (contextDocs || [])) {
37
+ if (doc.type === 'inlineText' && doc.content) {
38
+ const snippet = doc.content.length > 3000
39
+ ? doc.content.slice(0, 3000) + '\n... (truncated)'
40
+ : doc.content;
41
+ contextSnippets.push(`[${doc.fileName}]\n${snippet}`);
42
+ }
43
+ }
44
+
45
+ // Phase 1: Discover topics
46
+ console.log(` ${c.dim('Phase 1:')} Discovering topics...`);
47
+ let topicResult;
48
+ try {
49
+ topicResult = await discoverTopics(ai, compiledAnalysis, {
50
+ callName, userName, thinkingBudget, contextSnippets,
51
+ });
52
+ } catch (err) {
53
+ console.error(` ${c.error(`Topic discovery failed: ${err.message}`)}`);
54
+ log.error(`Deep dive topic discovery failed: ${err.message}`);
55
+ timer.end();
56
+ return;
57
+ }
58
+
59
+ const topics = topicResult.topics;
60
+ if (!topics || topics.length === 0) {
61
+ console.log(` ${c.info('No topics identified for deep dive')}`);
62
+ log.step('Deep dive: no topics discovered');
63
+ timer.end();
64
+ return;
65
+ }
66
+
67
+ console.log(` ${c.success(`Found ${c.highlight(topics.length)} topic(s):`)}`);
68
+ topics.forEach(t => console.log(` ${c.cyan(t.id)} ${c.dim(`[${t.category}]`)} ${t.title}`));
69
+ console.log('');
70
+
71
+ if (topicResult.tokenUsage) {
72
+ costTracker.addSegment('deep-dive-discovery', topicResult.tokenUsage, topicResult.durationMs, false);
73
+ }
74
+ log.step(`Deep dive: ${topics.length} topics discovered in ${(topicResult.durationMs / 1000).toFixed(1)}s`);
75
+
76
+ // Phase 2: Generate documents
77
+ console.log(` ${c.dim('Phase 2:')} Generating ${c.highlight(topics.length)} document(s)...`);
78
+ const documents = await generateAllDocuments(ai, topics, compiledAnalysis, {
79
+ callName,
80
+ userName,
81
+ thinkingBudget,
82
+ contextSnippets,
83
+ concurrency: Math.min(opts.parallelAnalysis || 2, 3), // match pipeline parallelism
84
+ onProgress: (done, total, topic) => {
85
+ console.log(` ${c.dim(`[${done}/${total}]`)} ${c.success(topic.title)}`);
86
+ },
87
+ });
88
+
89
+ // Track cost
90
+ for (const doc of documents) {
91
+ if (doc.tokenUsage && doc.tokenUsage.totalTokens > 0) {
92
+ costTracker.addSegment(`deep-dive-${doc.topic.id}`, doc.tokenUsage, doc.durationMs, false);
93
+ }
94
+ }
95
+
96
+ // Phase 3: Write output
97
+ const deepDiveDir = path.join(runDir, 'deep-dive');
98
+ const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
99
+ const { indexPath, stats } = writeDeepDiveOutput(deepDiveDir, documents, {
100
+ callName,
101
+ timestamp: ts,
102
+ });
103
+
104
+ console.log('');
105
+ console.log(` ${c.success(`Deep dive complete: ${c.highlight(stats.successful + '/' + stats.total)} documents generated`)}`);
106
+ console.log(` Output: ${c.cyan(path.relative(PROJECT_ROOT, deepDiveDir) + '/')}`);
107
+ console.log(` Index: ${c.cyan(path.relative(PROJECT_ROOT, indexPath))}`);
108
+ if (stats.failed > 0) {
109
+ console.log(` ${c.warn(`${stats.failed} document(s) failed`)}`);
110
+ }
111
+ console.log(` Tokens: ${c.yellow(stats.totalTokens.toLocaleString())} | Time: ${c.yellow((stats.totalDurationMs / 1000).toFixed(1) + 's')}`);
112
+ console.log('');
113
+
114
+ log.step(`Deep dive complete: ${stats.successful} docs, ${stats.totalTokens} tokens, ${(stats.totalDurationMs / 1000).toFixed(1)}s`);
115
+ timer.end();
116
+ }
117
+
118
+ module.exports = phaseDeepDive;
@@ -0,0 +1,178 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ // --- Config ---
7
+ const config = require('../config');
8
+ const { VIDEO_EXTS, AUDIO_EXTS, DOC_EXTS, SPEED, SEG_TIME } = config;
9
+
10
+ // --- Utils ---
11
+ const { c } = require('../utils/colors');
12
+ const { findDocsRecursive } = require('../utils/fs');
13
+ const { promptUserText } = require('../utils/cli');
14
+
15
+ // --- Shared state ---
16
+ const { getLog, phaseTimer } = require('./_shared');
17
+
18
+ // ======================== PHASE: DISCOVER ========================
19
+
20
+ /**
21
+ * Discover videos and documents, resolve user name, show banner.
22
+ * Returns augmented ctx with videoFiles, allDocFiles, userName.
23
+ */
24
+ async function phaseDiscover(ctx) {
25
+ const log = getLog();
26
+ const timer = phaseTimer('discover');
27
+ const { opts, targetDir, progress } = ctx;
28
+
29
+ // --- Find video files ---
30
+ let videoFiles = fs.readdirSync(targetDir)
31
+ .filter(f => {
32
+ const stat = fs.statSync(path.join(targetDir, f));
33
+ return stat.isFile() && VIDEO_EXTS.includes(path.extname(f).toLowerCase());
34
+ })
35
+ .map(f => path.join(targetDir, f));
36
+
37
+ // --- Find audio files (if no video) ---
38
+ let audioFiles = [];
39
+ if (videoFiles.length === 0) {
40
+ audioFiles = fs.readdirSync(targetDir)
41
+ .filter(f => {
42
+ const stat = fs.statSync(path.join(targetDir, f));
43
+ return stat.isFile() && AUDIO_EXTS.includes(path.extname(f).toLowerCase());
44
+ })
45
+ .map(f => path.join(targetDir, f));
46
+ }
47
+
48
+ // --- Find ALL document files recursively ---
49
+ const allDocFiles = findDocsRecursive(targetDir, DOC_EXTS);
50
+
51
+ // --- Determine input mode ---
52
+ let inputMode;
53
+ if (videoFiles.length > 0) {
54
+ inputMode = 'video';
55
+ } else if (audioFiles.length > 0) {
56
+ inputMode = 'audio';
57
+ } else if (allDocFiles.length > 0) {
58
+ inputMode = 'document';
59
+ } else {
60
+ throw new Error(
61
+ 'No processable files found (video, audio, or documents).\n' +
62
+ ' Supported: .mp4 .mkv .avi .mov .webm (video) | .mp3 .wav .m4a .ogg .flac .aac .wma (audio) | .vtt .txt .pdf .docx .md (docs)'
63
+ );
64
+ }
65
+
66
+ // Combine video + audio into mediaFiles for processing
67
+ const mediaFiles = inputMode === 'video' ? videoFiles : audioFiles;
68
+
69
+ const modeBanner = inputMode === 'video' ? ' Video Compress → Upload → AI Process' :
70
+ inputMode === 'audio' ? ' Audio Compress → Upload → AI Process' :
71
+ ' Document Analysis → AI Process';
72
+
73
+ console.log('');
74
+ console.log(c.cyan('=============================================='));
75
+ console.log(c.heading(modeBanner));
76
+ console.log(c.cyan('=============================================='));
77
+
78
+ // Show active flags
79
+ const activeFlags = [];
80
+ if (opts.skipUpload) activeFlags.push('skip-upload');
81
+ if (opts.forceUpload) activeFlags.push('force-upload');
82
+ if (opts.noStorageUrl) activeFlags.push('no-storage-url');
83
+ if (opts.skipCompression) activeFlags.push('skip-compression');
84
+ if (opts.skipGemini) activeFlags.push('skip-gemini');
85
+ if (opts.resume) activeFlags.push('resume');
86
+ if (opts.reanalyze) activeFlags.push('reanalyze');
87
+ if (opts.dryRun) activeFlags.push('dry-run');
88
+ if (activeFlags.length > 0) {
89
+ console.log(` Flags: ${c.yellow(activeFlags.join(', '))}`);
90
+ }
91
+ console.log('');
92
+
93
+ // --- Resume check ---
94
+ if (opts.resume && progress.hasResumableState()) {
95
+ progress.printResumeSummary();
96
+ console.log('');
97
+ }
98
+
99
+ // --- Ask for user's name (or use --name flag) ---
100
+ let userName = opts.userName;
101
+ if (!userName) {
102
+ if (opts.resume && progress.state.userName) {
103
+ userName = progress.state.userName;
104
+ console.log(` Using saved name: ${c.cyan(userName)}`);
105
+ } else {
106
+ userName = await promptUserText(' Your name (for task assignment detection): ');
107
+ }
108
+ }
109
+ if (!userName) {
110
+ throw new Error('Name is required for personalized analysis. Use --name "Your Name" or enter it when prompted.');
111
+ }
112
+ log.step(`User identified as: ${userName}`);
113
+
114
+ console.log('');
115
+ console.log(` User : ${c.cyan(userName)}`);
116
+ console.log(` Source : ${c.dim(targetDir)}`);
117
+ console.log(` Input : ${c.yellow(inputMode)}`);
118
+ if (inputMode === 'video') console.log(` Videos : ${c.highlight(videoFiles.length)}`);
119
+ if (inputMode === 'audio') console.log(` Audio : ${c.highlight(audioFiles.length)}`);
120
+ console.log(` Docs : ${c.highlight(allDocFiles.length)}`);
121
+ if (inputMode !== 'document') {
122
+ console.log(` Speed : ${c.yellow(SPEED + 'x')}`);
123
+ console.log(` Segments: ${c.dim('< 5 min each')} (${c.yellow(SEG_TIME + 's')})`);
124
+ }
125
+ console.log(` Model : ${c.cyan(config.GEMINI_MODEL)}`);
126
+ if (inputMode !== 'document') {
127
+ console.log(` Parallel: ${c.yellow(opts.parallel)} concurrent uploads`);
128
+ }
129
+ console.log(` Thinking: ${c.yellow(opts.thinkingBudget)} tokens ${c.dim('(analysis)')} / ${c.yellow(opts.compilationThinkingBudget)} tokens ${c.dim('(compilation)')}`);
130
+ console.log('');
131
+
132
+ // Save progress init
133
+ progress.init(path.basename(targetDir), userName);
134
+
135
+ if (inputMode === 'document') {
136
+ console.log(` ${c.info('No video or audio files found \u2014 running in document-only mode.')}`);
137
+ console.log(` ${c.dim('Tip: Use --dynamic for custom document generation.')}\n`);
138
+ } else {
139
+ const mediaLabel = inputMode === 'video' ? 'video' : 'audio';
140
+ console.log(` Found ${c.highlight(mediaFiles.length)} ${mediaLabel} file(s):`);
141
+ mediaFiles.forEach((f, i) => console.log(` ${c.dim(`[${i + 1}]`)} ${c.cyan(path.basename(f))}`));
142
+
143
+ // If multiple media files found, let user select which to process
144
+ if (mediaFiles.length > 1) {
145
+ console.log('');
146
+ const selectionInput = await promptUserText(` Which files to process? (comma-separated numbers, or "all", default: all): `);
147
+ const trimmed = (selectionInput || '').trim().toLowerCase();
148
+ if (trimmed && trimmed !== 'all') {
149
+ const indices = trimmed.split(',').map(s => parseInt(s.trim(), 10) - 1).filter(n => !isNaN(n) && n >= 0 && n < mediaFiles.length);
150
+ if (indices.length > 0) {
151
+ const selected = indices.map(i => mediaFiles[i]);
152
+ if (inputMode === 'video') videoFiles = selected;
153
+ else audioFiles = selected;
154
+ console.log(` \u2192 Processing ${c.highlight(selected.length)} selected file(s):`);
155
+ selected.forEach(f => console.log(` ${c.dim('-')} ${c.cyan(path.basename(f))}`));
156
+ } else {
157
+ console.log(` \u2192 ${c.dim('Invalid selection, processing all files')}`);
158
+ }
159
+ } else {
160
+ console.log(` \u2192 Processing all ${c.highlight(mediaLabel)} files`);
161
+ }
162
+ }
163
+ const finalMedia = inputMode === 'video' ? videoFiles : audioFiles;
164
+ log.step(`Found ${finalMedia.length} ${mediaLabel}(s): ${finalMedia.map(f => path.basename(f)).join(', ')}`);
165
+ console.log('');
166
+ }
167
+
168
+ if (allDocFiles.length > 0) {
169
+ console.log(` Found ${c.highlight(allDocFiles.length)} document(s) for context ${c.dim('(recursive)')}:`);
170
+ allDocFiles.forEach(f => console.log(` ${c.dim('-')} ${c.cyan(f.relPath)}`));
171
+ console.log('');
172
+ }
173
+
174
+ timer.end();
175
+ return { ...ctx, videoFiles, audioFiles, allDocFiles, userName, inputMode };
176
+ }
177
+
178
+ module.exports = phaseDiscover;
@@ -0,0 +1,199 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ // --- Config ---
7
+ const config = require('../config');
8
+ const {
9
+ LOG_LEVEL, MAX_PARALLEL_UPLOADS, THINKING_BUDGET, COMPILATION_THINKING_BUDGET,
10
+ validateConfig, GEMINI_MODELS, setActiveModel, getActiveModelPricing,
11
+ } = config;
12
+
13
+ // --- Utils ---
14
+ const { c } = require('../utils/colors');
15
+ const { parseArgs, showHelp, selectFolder, selectModel } = require('../utils/cli');
16
+ const { promptForKey } = require('../utils/global-config');
17
+ const Logger = require('../logger');
18
+ const Progress = require('../utils/checkpoint');
19
+ const CostTracker = require('../utils/cost-tracker');
20
+ const { createProgressBar } = require('../utils/progress-bar');
21
+ const { loadHistory, analyzeHistory, printLearningInsights } = require('../utils/learning-loop');
22
+
23
+ // --- Shared state ---
24
+ const { PKG_ROOT, PROJECT_ROOT, setLog, isShuttingDown, setShuttingDown } = require('./_shared');
25
+
26
+ /** Parse an integer flag, falling back to `defaultVal` only when the input is absent or NaN. */
27
+ function safeInt(raw, defaultVal) {
28
+ if (raw === undefined || raw === null || raw === true) return defaultVal;
29
+ const n = parseInt(raw, 10);
30
+ return Number.isNaN(n) ? defaultVal : n;
31
+ }
32
+
33
+ // ======================== PHASE: INIT ========================
34
+
35
+ /**
36
+ * Parse CLI args, validate config, initialize logger, set up shutdown handlers.
37
+ * Returns the pipeline context object shared by all phases.
38
+ */
39
+ async function phaseInit() {
40
+ const { flags, positional } = parseArgs(process.argv.slice(2));
41
+
42
+ if (flags.help || flags.h) showHelp();
43
+ if (flags.version || flags.v) {
44
+ const pkg = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, 'package.json'), 'utf8'));
45
+ process.stdout.write(`v${pkg.version}\n`);
46
+ throw Object.assign(new Error('VERSION_SHOWN'), { code: 'VERSION_SHOWN' });
47
+ }
48
+
49
+ const opts = {
50
+ skipUpload: !!flags['skip-upload'],
51
+ forceUpload: !!flags['force-upload'],
52
+ noStorageUrl: !!flags['no-storage-url'],
53
+ skipCompression: !!flags['skip-compression'],
54
+ skipGemini: !!flags['skip-gemini'],
55
+ resume: !!flags.resume,
56
+ reanalyze: !!flags.reanalyze,
57
+ dryRun: !!flags['dry-run'],
58
+ userName: flags.name || null,
59
+ parallel: safeInt(flags.parallel, MAX_PARALLEL_UPLOADS),
60
+ logLevel: flags['log-level'] || LOG_LEVEL,
61
+ outputDir: flags.output || null,
62
+ thinkingBudget: safeInt(flags['thinking-budget'], THINKING_BUDGET),
63
+ compilationThinkingBudget: safeInt(flags['compilation-thinking-budget'], COMPILATION_THINKING_BUDGET),
64
+ parallelAnalysis: safeInt(flags['parallel-analysis'], 2), // concurrent segment analysis
65
+ disableFocusedPass: !!flags['no-focused-pass'],
66
+ disableLearning: !!flags['no-learning'],
67
+ disableDiff: !!flags['no-diff'],
68
+ noHtml: !!flags['no-html'],
69
+ deepDive: !!flags['deep-dive'],
70
+ dynamic: !!flags.dynamic,
71
+ request: typeof flags.request === 'string' ? flags.request : null,
72
+ updateProgress: !!flags['update-progress'],
73
+ repoPath: flags.repo || null,
74
+ model: typeof flags.model === 'string' ? flags.model : null,
75
+ minConfidence: typeof flags['min-confidence'] === 'string' ? flags['min-confidence'].toLowerCase() : null,
76
+ format: typeof flags.format === 'string' ? flags.format.toLowerCase() : 'all',
77
+ };
78
+
79
+ // --- Validate min-confidence level ---
80
+ if (opts.minConfidence) {
81
+ const { validateConfidenceLevel } = require('../utils/confidence-filter');
82
+ const check = validateConfidenceLevel(opts.minConfidence);
83
+ if (!check.valid) {
84
+ throw new Error(check.error);
85
+ }
86
+ opts.minConfidence = check.normalised.toLowerCase();
87
+ }
88
+
89
+ // --- Validate --format flag ---
90
+ const VALID_FORMATS = new Set(['md', 'html', 'json', 'pdf', 'docx', 'all']);
91
+ if (!VALID_FORMATS.has(opts.format)) {
92
+ throw new Error(`Invalid --format "${opts.format}". Must be: md, html, json, pdf, docx, or all`);
93
+ }
94
+
95
+ // --- Resolve folder: positional arg or interactive selection ---
96
+ let folderArg = positional[0];
97
+ if (!folderArg) {
98
+ folderArg = await selectFolder(PROJECT_ROOT);
99
+ if (!folderArg) {
100
+ showHelp();
101
+ }
102
+ }
103
+
104
+ const targetDir = path.resolve(folderArg);
105
+ if (!fs.existsSync(targetDir) || !fs.statSync(targetDir).isDirectory()) {
106
+ throw new Error(`"${targetDir}" is not a valid folder. Check the path and try again.`);
107
+ }
108
+
109
+ // --- Validate configuration (with first-run recovery) ---
110
+ let configCheck = validateConfig({
111
+ skipFirebase: opts.skipUpload,
112
+ skipGemini: opts.skipGemini,
113
+ });
114
+
115
+ // First-run experience: if GEMINI_API_KEY is missing, prompt interactively
116
+ if (!configCheck.valid && !opts.skipGemini && !config.GEMINI_API_KEY) {
117
+ const key = await promptForKey('GEMINI_API_KEY');
118
+ if (key) {
119
+ // Re-validate after user provided the key
120
+ configCheck = validateConfig({
121
+ skipFirebase: opts.skipUpload,
122
+ skipGemini: opts.skipGemini,
123
+ });
124
+ }
125
+ }
126
+
127
+ if (!configCheck.valid) {
128
+ console.error(`\n ${c.error('Configuration errors:')}`);
129
+ configCheck.errors.forEach(e => console.error(` ${c.error(e)}`));
130
+ console.error(`\n ${c.dim('Fix these via:')}`);
131
+ console.error(` ${c.dim('•')} ${c.cyan('taskex config')} ${c.dim('(save globally for all projects)')}`);
132
+ console.error(` ${c.dim('•')} ${c.cyan('.env file')} ${c.dim('(project-specific config)')}`);
133
+ console.error(` ${c.dim('•')} ${c.cyan('--gemini-key <key>')} ${c.dim('(one-time inline)')}\n`);
134
+ throw new Error('Invalid configuration. See errors above.');
135
+ }
136
+
137
+ // --- Initialize logger ---
138
+ const logsDir = path.join(PROJECT_ROOT, 'logs');
139
+ const log = new Logger(logsDir, path.basename(targetDir), { level: opts.logLevel });
140
+ setLog(log);
141
+ log.patchConsole();
142
+ log.step(`START processing "${path.basename(targetDir)}"`);
143
+
144
+ // --- Learning Loop: load historical insights ---
145
+ let learningInsights = { hasData: false, budgetAdjustment: 0, compilationBudgetAdjustment: 0 };
146
+ if (!opts.disableLearning) {
147
+ const history = loadHistory(PROJECT_ROOT);
148
+ learningInsights = analyzeHistory(history);
149
+ if (learningInsights.hasData) {
150
+ printLearningInsights(learningInsights);
151
+ // Apply budget adjustments from learning (clamped to model max)
152
+ if (learningInsights.budgetAdjustment !== 0) {
153
+ const modelMax = config.getMaxThinkingBudget();
154
+ opts.thinkingBudget = Math.min(modelMax, Math.max(8192, opts.thinkingBudget + learningInsights.budgetAdjustment));
155
+ log.step(`Learning: adjusted thinking budget → ${opts.thinkingBudget}`);
156
+ }
157
+ if (learningInsights.compilationBudgetAdjustment !== 0) {
158
+ const modelMax = config.getMaxThinkingBudget();
159
+ opts.compilationThinkingBudget = Math.min(modelMax, Math.max(8192, opts.compilationThinkingBudget + learningInsights.compilationBudgetAdjustment));
160
+ log.step(`Learning: adjusted compilation budget → ${opts.compilationThinkingBudget}`);
161
+ }
162
+ }
163
+ }
164
+
165
+ // --- Graceful shutdown handler ---
166
+ const shutdown = (signal) => {
167
+ if (isShuttingDown()) return;
168
+ setShuttingDown(true);
169
+ console.warn(`\n ${c.warn(`Received ${signal} \u2014 shutting down gracefully...`)}`);
170
+ log.step(`SHUTDOWN requested (${signal})`);
171
+ log.close();
172
+ };
173
+ process.on('SIGINT', () => shutdown('SIGINT'));
174
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
175
+
176
+ // --- Model selection ---
177
+ if (opts.model) {
178
+ // CLI flag: --model <id> — validate and activate
179
+ setActiveModel(opts.model);
180
+ log.step(`Model set via flag: ${config.GEMINI_MODEL}`);
181
+ } else {
182
+ // Interactive model selection
183
+ const chosenModel = await selectModel(GEMINI_MODELS, config.GEMINI_MODEL);
184
+ setActiveModel(chosenModel);
185
+ log.step(`Model selected: ${config.GEMINI_MODEL}`);
186
+ }
187
+
188
+ // --- Initialize progress tracking ---
189
+ const progress = new Progress(targetDir);
190
+ const costTracker = new CostTracker(getActiveModelPricing());
191
+ const progressBar = createProgressBar({
192
+ costTracker,
193
+ callName: path.basename(targetDir),
194
+ });
195
+
196
+ return { opts, targetDir, progress, costTracker, progressBar };
197
+ }
198
+
199
+ module.exports = phaseInit;