task-summary-extractor 9.5.0 → 9.6.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/README.md +2 -0
- package/package.json +1 -1
- package/src/utils/cli.js +102 -298
- package/src/utils/interactive.js +356 -0
package/README.md
CHANGED
|
@@ -596,6 +596,8 @@ task-summary-extractor/
|
|
|
596
596
|
|
|
597
597
|
| Version | Highlights |
|
|
598
598
|
|---------|-----------|
|
|
599
|
+
| **v9.6.0** | **Interactive CLI UX** — arrow-key navigation for all selectors (folder, model, run mode, formats, confidence, doc exclusion), zero-dependency prompt engine (`interactive.js`), `selectOne()` with ↑↓+Enter, `selectMany()` with Space toggle + A all/none, non-TTY fallback to number input |
|
|
600
|
+
| **v9.5.0** | **Video processing flags** — `--no-compress`, `--speed`, `--segment-time` CLI flags, hardcoded 1200s for raw mode, deprecated `--skip-compression` |
|
|
599
601
|
| **v9.4.0** | **Context window safety** — pre-flight token checks, auto-truncation for oversized docs/VTTs, RESOURCE_EXHAUSTED recovery with automatic doc shedding, chunked compilation for large segment sets, P0/P1 hard cap (2× budget) prevents context overflow, improved deep-summary prompt quality |
|
|
600
602
|
| **v9.3.1** | **Audit & polish** — VIDEO_SPEED 1.5→1.6, `--exclude-docs` flag for non-interactive deep-summary exclusion, friendlier Gemini error messages, dead code removal, DRY RUN_PRESETS |
|
|
601
603
|
| **v9.3.0** | **Deep summary** — `--deep-summary` pre-summarizes context documents (60-80% token savings), interactive doc picker, `--exclude-docs` for CLI automation, batch processing |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "task-summary-extractor",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.6.0",
|
|
4
4
|
"description": "AI-powered meeting analysis & document generation CLI — video + document processing, deep dive docs, dynamic mode, interactive CLI with model selection, confidence scoring, learning loop, git progress tracking",
|
|
5
5
|
"main": "process_and_upload.js",
|
|
6
6
|
"bin": {
|
package/src/utils/cli.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
const fs = require('fs');
|
|
19
19
|
const path = require('path');
|
|
20
20
|
const { c } = require('./colors');
|
|
21
|
+
const { selectOne, selectMany } = require('./interactive');
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* Parse command-line arguments into flags and positional args.
|
|
@@ -166,7 +167,6 @@ function discoverFolders(projectRoot) {
|
|
|
166
167
|
* @returns {Promise<string|null>} - Folder name or null
|
|
167
168
|
*/
|
|
168
169
|
async function selectFolder(projectRoot) {
|
|
169
|
-
const readline = require('readline');
|
|
170
170
|
const folders = discoverFolders(projectRoot);
|
|
171
171
|
|
|
172
172
|
if (folders.length === 0) {
|
|
@@ -177,37 +177,24 @@ async function selectFolder(projectRoot) {
|
|
|
177
177
|
return null;
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
-
|
|
181
|
-
console.log(c.heading(' 📂 Available Folders'));
|
|
182
|
-
console.log(c.dim(' ' + '─'.repeat(50)));
|
|
183
|
-
folders.forEach((f, i) => {
|
|
180
|
+
const items = folders.map(f => {
|
|
184
181
|
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
182
|
const mode = (!f.hasVideo && !f.hasAudio) ? c.yellow(' (docs only)') : '';
|
|
189
|
-
|
|
183
|
+
return {
|
|
184
|
+
label: `${icon} ${c.bold(f.name)}${mode}`,
|
|
185
|
+
hint: f.description,
|
|
186
|
+
value: f.name,
|
|
187
|
+
};
|
|
190
188
|
});
|
|
191
|
-
console.log('');
|
|
192
|
-
|
|
193
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
194
|
-
return new Promise(resolve => {
|
|
195
|
-
rl.question(' Select folder (number, or type a path): ', answer => {
|
|
196
|
-
rl.close();
|
|
197
|
-
const trimmed = (answer || '').trim();
|
|
198
|
-
if (!trimmed) { resolve(null); return; }
|
|
199
|
-
|
|
200
|
-
// Number selection
|
|
201
|
-
const num = parseInt(trimmed, 10);
|
|
202
|
-
if (!isNaN(num) && num >= 1 && num <= folders.length) {
|
|
203
|
-
resolve(folders[num - 1].name);
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
189
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
190
|
+
const result = await selectOne({
|
|
191
|
+
title: c.bold('📂 Select Folder'),
|
|
192
|
+
items,
|
|
193
|
+
default: 0,
|
|
194
|
+
footer: '↑↓ navigate · Enter select',
|
|
210
195
|
});
|
|
196
|
+
|
|
197
|
+
return result.value;
|
|
211
198
|
}
|
|
212
199
|
|
|
213
200
|
// ======================== INTERACTIVE MODEL SELECTOR ========================
|
|
@@ -232,24 +219,26 @@ function fmtContext(tokens) {
|
|
|
232
219
|
* @returns {Promise<string>} Selected model ID
|
|
233
220
|
*/
|
|
234
221
|
async function selectModel(GEMINI_MODELS, currentModel) {
|
|
235
|
-
const readline = require('readline');
|
|
236
222
|
const modelIds = Object.keys(GEMINI_MODELS);
|
|
237
223
|
|
|
238
|
-
//
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
balanced: { label: 'Balanced (recommended)', icon: '⚡', models: [] },
|
|
242
|
-
economy: { label: 'Economy (lowest cost)', icon: '💰', models: [] },
|
|
243
|
-
};
|
|
224
|
+
// Build items with tier grouping info in hints
|
|
225
|
+
const items = [];
|
|
226
|
+
let defaultIdx = 0;
|
|
244
227
|
|
|
245
|
-
let idx = 0;
|
|
246
|
-
const indexMap = {}; // index → modelId
|
|
247
228
|
for (const id of modelIds) {
|
|
248
229
|
const m = GEMINI_MODELS[id];
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
230
|
+
const thinkTag = m.thinking ? c.magenta(' [thinking]') : '';
|
|
231
|
+
const ctxStr = fmtContext(m.contextWindow);
|
|
232
|
+
const tierIcon = m.tier === 'premium' ? '🏆' : m.tier === 'balanced' ? '⚡' : '💰';
|
|
233
|
+
const costLabel = m.costEstimate || '';
|
|
234
|
+
|
|
235
|
+
if (id === currentModel) defaultIdx = items.length;
|
|
236
|
+
|
|
237
|
+
items.push({
|
|
238
|
+
label: `${tierIcon} ${c.bold(m.name)}${thinkTag}`,
|
|
239
|
+
hint: `${ctxStr} ctx · ${costLabel} · ${m.description}`,
|
|
240
|
+
value: id,
|
|
241
|
+
});
|
|
253
242
|
}
|
|
254
243
|
|
|
255
244
|
console.log('');
|
|
@@ -257,79 +246,14 @@ async function selectModel(GEMINI_MODELS, currentModel) {
|
|
|
257
246
|
console.log(c.heading(' │ 🤖 Gemini Model Selection │'));
|
|
258
247
|
console.log(c.heading(' └──────────────────────────────────────────────────────────────────────────────┘'));
|
|
259
248
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
for (const m of tier.models) {
|
|
267
|
-
const isDefault = m.id === currentModel;
|
|
268
|
-
const marker = isDefault ? c.green(' ← default') : '';
|
|
269
|
-
const thinkTag = m.thinking ? c.magenta(' [thinking]') : '';
|
|
270
|
-
|
|
271
|
-
// Line 1: number, name, description
|
|
272
|
-
console.log(` ${c.cyan(`[${m.idx}]`)} ${c.bold(m.name)}${thinkTag}${marker}`);
|
|
273
|
-
console.log(` ${c.dim(m.description)}`);
|
|
274
|
-
|
|
275
|
-
// Line 2: specs
|
|
276
|
-
const ctxStr = fmtContext(m.contextWindow);
|
|
277
|
-
const outStr = fmtContext(m.maxOutput);
|
|
278
|
-
const inPrice = `$${m.pricing.inputPerM.toFixed(m.pricing.inputPerM < 0.1 ? 4 : 2)}/1M in`;
|
|
279
|
-
const outPrice = `$${m.pricing.outputPerM.toFixed(m.pricing.outputPerM < 1 ? 2 : 2)}/1M out`;
|
|
280
|
-
const thinkPrice = m.thinking ? ` · $${m.pricing.thinkingPerM.toFixed(2)}/1M think` : '';
|
|
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}`);
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
console.log('');
|
|
287
|
-
|
|
288
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
289
|
-
return new Promise(resolve => {
|
|
290
|
-
rl.question(` Select model [1-${idx}] (Enter = keep default): `, answer => {
|
|
291
|
-
rl.close();
|
|
292
|
-
const trimmed = (answer || '').trim();
|
|
293
|
-
|
|
294
|
-
// Enter = keep default
|
|
295
|
-
if (!trimmed) {
|
|
296
|
-
console.log(c.success(`Using ${GEMINI_MODELS[currentModel].name}`));
|
|
297
|
-
resolve(currentModel);
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// Number selection
|
|
302
|
-
const num = parseInt(trimmed, 10);
|
|
303
|
-
if (!isNaN(num) && indexMap[num]) {
|
|
304
|
-
const chosen = indexMap[num];
|
|
305
|
-
console.log(c.success(`Selected ${GEMINI_MODELS[chosen].name}`));
|
|
306
|
-
resolve(chosen);
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// Direct model ID input
|
|
311
|
-
if (GEMINI_MODELS[trimmed]) {
|
|
312
|
-
console.log(c.success(`Selected ${GEMINI_MODELS[trimmed].name}`));
|
|
313
|
-
resolve(trimmed);
|
|
314
|
-
return;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// Fuzzy match: partial name
|
|
318
|
-
const lower = trimmed.toLowerCase();
|
|
319
|
-
const match = modelIds.find(id =>
|
|
320
|
-
id.toLowerCase().includes(lower) ||
|
|
321
|
-
GEMINI_MODELS[id].name.toLowerCase().includes(lower)
|
|
322
|
-
);
|
|
323
|
-
if (match) {
|
|
324
|
-
console.log(c.success(`Matched ${GEMINI_MODELS[match].name}`));
|
|
325
|
-
resolve(match);
|
|
326
|
-
return;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
console.log(c.warn(`Unknown selection "${trimmed}" — using default (${currentModel})`));
|
|
330
|
-
resolve(currentModel);
|
|
331
|
-
});
|
|
249
|
+
const result = await selectOne({
|
|
250
|
+
title: null, // banner already printed
|
|
251
|
+
items,
|
|
252
|
+
default: defaultIdx,
|
|
253
|
+
footer: '↑↓ navigate · Enter select',
|
|
332
254
|
});
|
|
255
|
+
|
|
256
|
+
return result.value;
|
|
333
257
|
}
|
|
334
258
|
|
|
335
259
|
/**
|
|
@@ -552,59 +476,32 @@ module.exports.RUN_PRESETS = RUN_PRESETS;
|
|
|
552
476
|
* @returns {Promise<string>} Preset key: 'fast' | 'balanced' | 'detailed' | 'custom'
|
|
553
477
|
*/
|
|
554
478
|
async function selectRunMode() {
|
|
555
|
-
const readline = require('readline');
|
|
556
479
|
const presetKeys = Object.keys(RUN_PRESETS);
|
|
557
480
|
|
|
558
481
|
console.log('');
|
|
559
482
|
console.log(c.heading(' ┌──────────────────────────────────────────────────────────────────────────────┐'));
|
|
560
483
|
console.log(c.heading(' │ 🚀 Run Mode │'));
|
|
561
484
|
console.log(c.heading(' └──────────────────────────────────────────────────────────────────────────────┘'));
|
|
562
|
-
console.log('');
|
|
563
485
|
|
|
564
|
-
presetKeys.
|
|
486
|
+
const defaultIdx = presetKeys.indexOf('balanced');
|
|
487
|
+
|
|
488
|
+
const items = presetKeys.map(key => {
|
|
565
489
|
const p = RUN_PRESETS[key];
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
console.log(` ${desc}`);
|
|
490
|
+
return {
|
|
491
|
+
label: `${p.icon} ${c.bold(p.label)}`,
|
|
492
|
+
hint: p.description,
|
|
493
|
+
value: key,
|
|
494
|
+
};
|
|
572
495
|
});
|
|
573
|
-
console.log('');
|
|
574
496
|
|
|
575
|
-
const
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
if (!trimmed) {
|
|
582
|
-
console.log(c.success('Using balanced mode'));
|
|
583
|
-
resolve('balanced');
|
|
584
|
-
return;
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
const num = parseInt(trimmed, 10);
|
|
588
|
-
if (num >= 1 && num <= presetKeys.length) {
|
|
589
|
-
const key = presetKeys[num - 1];
|
|
590
|
-
console.log(c.success(`Using ${RUN_PRESETS[key].label} mode`));
|
|
591
|
-
resolve(key);
|
|
592
|
-
return;
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
// Try matching by name
|
|
596
|
-
const lower = trimmed.toLowerCase();
|
|
597
|
-
const match = presetKeys.find(k => k === lower || RUN_PRESETS[k].label.toLowerCase() === lower);
|
|
598
|
-
if (match) {
|
|
599
|
-
console.log(c.success(`Using ${RUN_PRESETS[match].label} mode`));
|
|
600
|
-
resolve(match);
|
|
601
|
-
return;
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
console.log(c.warn(`Unknown "${trimmed}" — using balanced mode`));
|
|
605
|
-
resolve('balanced');
|
|
606
|
-
});
|
|
497
|
+
const result = await selectOne({
|
|
498
|
+
title: null, // banner already printed
|
|
499
|
+
items,
|
|
500
|
+
default: defaultIdx >= 0 ? defaultIdx : 0,
|
|
501
|
+
footer: '↑↓ navigate · Enter select',
|
|
607
502
|
});
|
|
503
|
+
|
|
504
|
+
return result.value;
|
|
608
505
|
}
|
|
609
506
|
|
|
610
507
|
// ======================== FORMAT PICKER ========================
|
|
@@ -624,56 +521,27 @@ const ALL_FORMATS = [
|
|
|
624
521
|
* @returns {Promise<Set<string>>}
|
|
625
522
|
*/
|
|
626
523
|
async function selectFormats() {
|
|
627
|
-
const
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
524
|
+
const items = ALL_FORMATS.map(f => ({
|
|
525
|
+
label: `${f.icon} ${c.bold(f.label)}`,
|
|
526
|
+
hint: f.desc,
|
|
527
|
+
value: f.key,
|
|
528
|
+
}));
|
|
529
|
+
|
|
530
|
+
// Default: all selected
|
|
531
|
+
const defaultSelected = new Set(items.map((_, i) => i));
|
|
532
|
+
|
|
533
|
+
const result = await selectMany({
|
|
534
|
+
title: c.bold('📦 Output Formats'),
|
|
535
|
+
items,
|
|
536
|
+
defaultSelected,
|
|
636
537
|
});
|
|
637
|
-
console.log(` ${c.cyan('[A]')} ${c.bold('All formats')}`);
|
|
638
|
-
console.log('');
|
|
639
|
-
|
|
640
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
641
|
-
return new Promise(resolve => {
|
|
642
|
-
rl.question(' Formats (e.g. 1,2,3 or A for all) [Enter = all]: ', answer => {
|
|
643
|
-
rl.close();
|
|
644
|
-
const trimmed = (answer || '').trim().toLowerCase();
|
|
645
|
-
|
|
646
|
-
if (!trimmed || trimmed === 'a' || trimmed === 'all') {
|
|
647
|
-
const all = new Set(ALL_FORMATS.map(f => f.key));
|
|
648
|
-
console.log(c.success('All formats selected'));
|
|
649
|
-
resolve(all);
|
|
650
|
-
return;
|
|
651
|
-
}
|
|
652
538
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
if (num >= 1 && num <= ALL_FORMATS.length) {
|
|
658
|
-
chosen.add(ALL_FORMATS[num - 1].key);
|
|
659
|
-
} else {
|
|
660
|
-
// Try matching format key by name
|
|
661
|
-
const match = ALL_FORMATS.find(f => f.key === p || f.label.toLowerCase() === p);
|
|
662
|
-
if (match) chosen.add(match.key);
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
if (chosen.size === 0) {
|
|
667
|
-
console.log(c.warn('No valid formats selected — using all'));
|
|
668
|
-
resolve(new Set(ALL_FORMATS.map(f => f.key)));
|
|
669
|
-
return;
|
|
670
|
-
}
|
|
539
|
+
// If none selected, default to all
|
|
540
|
+
if (result.values.length === 0) {
|
|
541
|
+
return new Set(ALL_FORMATS.map(f => f.key));
|
|
542
|
+
}
|
|
671
543
|
|
|
672
|
-
|
|
673
|
-
console.log(c.success(`Formats: ${labels.join(', ')}`));
|
|
674
|
-
resolve(chosen);
|
|
675
|
-
});
|
|
676
|
-
});
|
|
544
|
+
return new Set(result.values);
|
|
677
545
|
}
|
|
678
546
|
|
|
679
547
|
// ======================== CONFIDENCE PICKER ========================
|
|
@@ -685,59 +553,27 @@ async function selectFormats() {
|
|
|
685
553
|
* @returns {Promise<string|null>}
|
|
686
554
|
*/
|
|
687
555
|
async function selectConfidence() {
|
|
688
|
-
const readline = require('readline');
|
|
689
|
-
|
|
690
556
|
const levels = [
|
|
691
|
-
{ key: null, icon: '🌐', label: 'All',
|
|
692
|
-
{ key: 'low', icon: '🟡', label: 'Low+',
|
|
557
|
+
{ key: null, icon: '🌐', label: 'All', desc: 'Keep everything — no filtering' },
|
|
558
|
+
{ key: 'low', icon: '🟡', label: 'Low+', desc: 'Keep low, medium & high confidence' },
|
|
693
559
|
{ key: 'medium', icon: '🟠', label: 'Medium+', desc: 'Keep medium & high confidence' },
|
|
694
|
-
{ key: 'high', icon: '🔴', label: 'High',
|
|
560
|
+
{ key: 'high', icon: '🔴', label: 'High', desc: 'Only high-confidence items' },
|
|
695
561
|
];
|
|
696
562
|
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
563
|
+
const items = levels.map(l => ({
|
|
564
|
+
label: `${l.icon} ${c.bold(l.label)}`,
|
|
565
|
+
hint: l.desc,
|
|
566
|
+
value: l.key,
|
|
567
|
+
}));
|
|
568
|
+
|
|
569
|
+
const result = await selectOne({
|
|
570
|
+
title: c.bold('🎯 Confidence Filter'),
|
|
571
|
+
items,
|
|
572
|
+
default: 0,
|
|
573
|
+
footer: '↑↓ navigate · Enter select',
|
|
705
574
|
});
|
|
706
|
-
console.log('');
|
|
707
|
-
|
|
708
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
709
|
-
return new Promise(resolve => {
|
|
710
|
-
rl.question(' Confidence filter [1-4] (Enter = keep all): ', answer => {
|
|
711
|
-
rl.close();
|
|
712
|
-
const trimmed = (answer || '').trim();
|
|
713
|
-
|
|
714
|
-
if (!trimmed) {
|
|
715
|
-
console.log(c.success('Keeping all confidence levels'));
|
|
716
|
-
resolve(null);
|
|
717
|
-
return;
|
|
718
|
-
}
|
|
719
575
|
|
|
720
|
-
|
|
721
|
-
if (num >= 1 && num <= levels.length) {
|
|
722
|
-
const chosen = levels[num - 1];
|
|
723
|
-
console.log(c.success(`Confidence filter: ${chosen.label}`));
|
|
724
|
-
resolve(chosen.key);
|
|
725
|
-
return;
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
// Try matching by name
|
|
729
|
-
const lower = trimmed.toLowerCase();
|
|
730
|
-
const match = levels.find(l => l.key === lower || l.label.toLowerCase() === lower);
|
|
731
|
-
if (match) {
|
|
732
|
-
console.log(c.success(`Confidence filter: ${match.label}`));
|
|
733
|
-
resolve(match.key);
|
|
734
|
-
return;
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
console.log(c.warn(`Unknown "${trimmed}" — keeping all`));
|
|
738
|
-
resolve(null);
|
|
739
|
-
});
|
|
740
|
-
});
|
|
576
|
+
return result.value;
|
|
741
577
|
}
|
|
742
578
|
|
|
743
579
|
// ======================== DEEP SUMMARY DOC EXCLUSION PICKER ========================
|
|
@@ -750,8 +586,6 @@ async function selectConfidence() {
|
|
|
750
586
|
* @returns {Promise<string[]>} Array of excluded fileName strings
|
|
751
587
|
*/
|
|
752
588
|
async function selectDocsToExclude(contextDocs) {
|
|
753
|
-
const readline = require('readline');
|
|
754
|
-
|
|
755
589
|
// Only show inlineText docs with actual content
|
|
756
590
|
const eligible = contextDocs
|
|
757
591
|
.filter(d => d.type === 'inlineText' && d.content && d.content.length > 0)
|
|
@@ -764,58 +598,28 @@ async function selectDocsToExclude(contextDocs) {
|
|
|
764
598
|
if (eligible.length === 0) return [];
|
|
765
599
|
|
|
766
600
|
console.log('');
|
|
767
|
-
console.log(` ${c.
|
|
768
|
-
console.log(c.
|
|
769
|
-
console.log('');
|
|
770
|
-
console.log(` ${c.dim('To save processing time, we can create short summaries of your')}`);
|
|
771
|
-
console.log(` ${c.dim('reference documents. The AI will still read them — just faster.')}`);
|
|
772
|
-
console.log('');
|
|
773
|
-
console.log(` ${c.bold('If a document is especially important to you, select it below')}`);
|
|
774
|
-
console.log(` ${c.bold('to keep it in full.')} The rest will be smartly condensed.`);
|
|
601
|
+
console.log(` ${c.dim('Deep Summary will create short summaries of your reference documents.')}`);
|
|
602
|
+
console.log(` ${c.bold('Select documents to keep in FULL')} ${c.dim('(the rest will be condensed).')}`);
|
|
775
603
|
console.log('');
|
|
776
604
|
|
|
777
|
-
eligible.
|
|
778
|
-
const num = c.cyan(`[${i + 1}]`);
|
|
605
|
+
const items = eligible.map(d => {
|
|
779
606
|
const size = d.tokensEst >= 1000
|
|
780
|
-
?
|
|
781
|
-
:
|
|
782
|
-
|
|
607
|
+
? `~${(d.tokensEst / 1000).toFixed(0)}K words`
|
|
608
|
+
: `~${d.tokensEst} words`;
|
|
609
|
+
return {
|
|
610
|
+
label: c.bold(d.fileName),
|
|
611
|
+
hint: size,
|
|
612
|
+
value: d.fileName,
|
|
613
|
+
};
|
|
783
614
|
});
|
|
784
|
-
console.log('');
|
|
785
|
-
console.log(c.dim(' Tip: Enter = condense all · Type numbers to keep full (e.g. 1,3)'));
|
|
786
|
-
console.log('');
|
|
787
615
|
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
if (!trimmed) {
|
|
795
|
-
console.log(c.success('Got it — all documents will be condensed for faster processing'));
|
|
796
|
-
resolve([]);
|
|
797
|
-
return;
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
const parts = trimmed.split(/[\s,]+/).filter(Boolean);
|
|
801
|
-
const excluded = [];
|
|
802
|
-
|
|
803
|
-
for (const p of parts) {
|
|
804
|
-
const num = parseInt(p, 10);
|
|
805
|
-
if (num >= 1 && num <= eligible.length) {
|
|
806
|
-
excluded.push(eligible[num - 1].fileName);
|
|
807
|
-
}
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
if (excluded.length === 0) {
|
|
811
|
-
console.log(c.warn('No valid selections — condensing all documents'));
|
|
812
|
-
resolve([]);
|
|
813
|
-
return;
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
console.log(c.success(`Keeping ${excluded.length} doc(s) in full — the rest will be condensed:`));
|
|
817
|
-
excluded.forEach(f => console.log(` ${c.dim('•')} ${c.cyan(f)}`));
|
|
818
|
-
resolve(excluded);
|
|
819
|
-
});
|
|
616
|
+
// Default: none selected (all will be condensed)
|
|
617
|
+
const result = await selectMany({
|
|
618
|
+
title: c.bold('📋 Deep Summary — Keep in Full'),
|
|
619
|
+
items,
|
|
620
|
+
defaultSelected: new Set(),
|
|
621
|
+
footer: '↑↓ navigate · Space toggle · A all/none · Enter confirm (Enter = condense all)',
|
|
820
622
|
});
|
|
623
|
+
|
|
624
|
+
return result.values;
|
|
821
625
|
}
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive prompt engine — zero dependencies, arrow-key navigation.
|
|
3
|
+
*
|
|
4
|
+
* Provides:
|
|
5
|
+
* selectOne() — single-select with ↑↓ arrows + Enter
|
|
6
|
+
* selectMany() — multi-select with ↑↓ arrows + Space (toggle) + Enter (confirm)
|
|
7
|
+
*
|
|
8
|
+
* Falls back to number input when stdin is not a TTY.
|
|
9
|
+
*
|
|
10
|
+
* Rendering strategy:
|
|
11
|
+
* After every draw(), the terminal cursor sits on the LAST rendered line
|
|
12
|
+
* (bottom of the block). Before each subsequent redraw we move UP by
|
|
13
|
+
* (totalRenderedLines − 1) to reach the first item line, then overwrite
|
|
14
|
+
* every line top-to-bottom. This keeps positioning deterministic and
|
|
15
|
+
* avoids the overlap / garble bugs of relative-to-highlight approaches.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
const { c, strip } = require('./colors');
|
|
21
|
+
|
|
22
|
+
// ── ANSI helpers ──────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
const HIDE_CURSOR = '\x1b[?25l';
|
|
25
|
+
const SHOW_CURSOR = '\x1b[?25h';
|
|
26
|
+
const CLEAR_LINE = '\x1b[2K';
|
|
27
|
+
|
|
28
|
+
/** Move cursor up N lines */
|
|
29
|
+
const UP = (n) => n > 0 ? `\x1b[${n}A` : '';
|
|
30
|
+
/** Move cursor down N lines */
|
|
31
|
+
const DOWN = (n) => n > 0 ? `\x1b[${n}B` : '';
|
|
32
|
+
/** Carriage return — column 0 */
|
|
33
|
+
const CR = '\r';
|
|
34
|
+
|
|
35
|
+
// ── Render helpers ────────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Build display strings for each item.
|
|
39
|
+
*
|
|
40
|
+
* @param {Array<{label: string, hint?: string}>} items
|
|
41
|
+
* @param {number} cursor Currently highlighted index
|
|
42
|
+
* @param {Set<number>} [selected] For multi-select: selected indices
|
|
43
|
+
* @param {boolean} [multi=false]
|
|
44
|
+
* @returns {string[]}
|
|
45
|
+
*/
|
|
46
|
+
function renderList(items, cursor, selected, multi = false) {
|
|
47
|
+
return items.map((item, i) => {
|
|
48
|
+
const isCursor = i === cursor;
|
|
49
|
+
const prefix = isCursor ? c.cyan('❯') : ' ';
|
|
50
|
+
|
|
51
|
+
let checkbox = '';
|
|
52
|
+
if (multi) {
|
|
53
|
+
const isChecked = selected && selected.has(i);
|
|
54
|
+
checkbox = isChecked ? c.green(' ◉') : c.dim(' ○');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const label = isCursor ? c.bold(c.cyan(strip(item.label))) : item.label;
|
|
58
|
+
const hint = item.hint
|
|
59
|
+
? (isCursor ? c.dim(` ${strip(item.hint)}`) : c.dim(` ${item.hint}`))
|
|
60
|
+
: '';
|
|
61
|
+
|
|
62
|
+
return ` ${prefix}${checkbox} ${label}${hint}`;
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Write an array of strings to stdout, one per line.
|
|
68
|
+
* Each line is preceded by CR + CLEAR_LINE so the entire row is wiped first.
|
|
69
|
+
*/
|
|
70
|
+
function writeLines(lines) {
|
|
71
|
+
for (let i = 0; i < lines.length; i++) {
|
|
72
|
+
if (i > 0) process.stdout.write('\n');
|
|
73
|
+
process.stdout.write(`${CR}${CLEAR_LINE}${lines[i]}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ── Key decoder ───────────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Decode a raw keypress buffer into a named action.
|
|
81
|
+
* @param {Buffer} buf
|
|
82
|
+
* @returns {'up'|'down'|'space'|'enter'|'escape'|'a'|null}
|
|
83
|
+
*/
|
|
84
|
+
function decodeKey(buf) {
|
|
85
|
+
if (buf[0] === 0x1b && buf[1] === 0x5b) {
|
|
86
|
+
if (buf[2] === 0x41) return 'up';
|
|
87
|
+
if (buf[2] === 0x42) return 'down';
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
if (buf[0] === 0x0d || buf[0] === 0x0a) return 'enter';
|
|
91
|
+
if (buf[0] === 0x20) return 'space';
|
|
92
|
+
if (buf[0] === 0x03) return 'escape';
|
|
93
|
+
if (buf[0] === 0x61 || buf[0] === 0x41) return 'a';
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ── Core: selectOne ───────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Interactive single-select with arrow-key navigation.
|
|
101
|
+
*
|
|
102
|
+
* @param {Object} opts
|
|
103
|
+
* @param {string} opts.title - Heading text (printed once)
|
|
104
|
+
* @param {Array<{label: string, hint?: string, value: any}>} opts.items
|
|
105
|
+
* @param {number} [opts.default=0] - Default highlighted index
|
|
106
|
+
* @param {string} [opts.footer] - Hint line below the list
|
|
107
|
+
* @returns {Promise<{index: number, value: any}>}
|
|
108
|
+
*/
|
|
109
|
+
function selectOne({ title, items, default: defaultIdx = 0, footer }) {
|
|
110
|
+
if (!process.stdin.isTTY) {
|
|
111
|
+
return _fallbackSelectOne({ title, items, default: defaultIdx });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return new Promise((resolve) => {
|
|
115
|
+
let cursor = defaultIdx;
|
|
116
|
+
const total = items.length;
|
|
117
|
+
const hasFooter = !!footer;
|
|
118
|
+
const renderedLines = total + (hasFooter ? 1 : 0); // lines we overwrite
|
|
119
|
+
let firstDraw = true;
|
|
120
|
+
|
|
121
|
+
// ── Title (printed once, never overwritten) ────────
|
|
122
|
+
if (title) {
|
|
123
|
+
console.log('');
|
|
124
|
+
console.log(` ${title}`);
|
|
125
|
+
console.log(c.dim(' ' + '─'.repeat(60)));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
process.stdout.write(HIDE_CURSOR);
|
|
129
|
+
|
|
130
|
+
// ── Draw / redraw ──────────────────────────────────
|
|
131
|
+
const draw = () => {
|
|
132
|
+
if (!firstDraw) {
|
|
133
|
+
// Terminal cursor is on the last rendered line — go back to first
|
|
134
|
+
process.stdout.write(UP(renderedLines - 1) + CR);
|
|
135
|
+
}
|
|
136
|
+
const lines = renderList(items, cursor);
|
|
137
|
+
writeLines(lines);
|
|
138
|
+
if (hasFooter) {
|
|
139
|
+
process.stdout.write('\n');
|
|
140
|
+
process.stdout.write(`${CR}${CLEAR_LINE}${c.dim(` ${footer}`)}`);
|
|
141
|
+
}
|
|
142
|
+
// Terminal cursor is now on the LAST rendered line
|
|
143
|
+
firstDraw = false;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
draw(); // initial render
|
|
147
|
+
|
|
148
|
+
process.stdin.setRawMode(true);
|
|
149
|
+
process.stdin.resume();
|
|
150
|
+
|
|
151
|
+
const cleanup = () => {
|
|
152
|
+
process.stdin.setRawMode(false);
|
|
153
|
+
process.stdin.pause();
|
|
154
|
+
process.stdin.removeListener('data', onKey);
|
|
155
|
+
process.stdout.write(CR + SHOW_CURSOR + '\n');
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const onKey = (buf) => {
|
|
159
|
+
const key = decodeKey(buf);
|
|
160
|
+
if (key === 'up') {
|
|
161
|
+
cursor = (cursor - 1 + total) % total;
|
|
162
|
+
draw();
|
|
163
|
+
} else if (key === 'down') {
|
|
164
|
+
cursor = (cursor + 1) % total;
|
|
165
|
+
draw();
|
|
166
|
+
} else if (key === 'enter') {
|
|
167
|
+
cleanup();
|
|
168
|
+
const chosen = items[cursor];
|
|
169
|
+
console.log(c.success(`${strip(chosen.label)}`));
|
|
170
|
+
resolve({ index: cursor, value: chosen.value });
|
|
171
|
+
} else if (key === 'escape') {
|
|
172
|
+
cleanup();
|
|
173
|
+
const chosen = items[defaultIdx];
|
|
174
|
+
console.log(c.success(`${strip(chosen.label)}`));
|
|
175
|
+
resolve({ index: defaultIdx, value: chosen.value });
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
process.stdin.on('data', onKey);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ── Core: selectMany ──────────────────────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Interactive multi-select with arrow-key navigation + Space toggle.
|
|
187
|
+
*
|
|
188
|
+
* @param {Object} opts
|
|
189
|
+
* @param {string} opts.title
|
|
190
|
+
* @param {Array<{label: string, hint?: string, value: any}>} opts.items
|
|
191
|
+
* @param {Set<number>} [opts.defaultSelected] - Pre-selected indices
|
|
192
|
+
* @param {string} [opts.footer]
|
|
193
|
+
* @returns {Promise<{indices: number[], values: any[]}>}
|
|
194
|
+
*/
|
|
195
|
+
function selectMany({ title, items, defaultSelected, footer }) {
|
|
196
|
+
if (!process.stdin.isTTY) {
|
|
197
|
+
return _fallbackSelectMany({ title, items, defaultSelected });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return new Promise((resolve) => {
|
|
201
|
+
let cursor = 0;
|
|
202
|
+
const selected = new Set(defaultSelected || []);
|
|
203
|
+
const total = items.length;
|
|
204
|
+
|
|
205
|
+
const footerText = footer || '↑↓ navigate · Space toggle · A all/none · Enter confirm';
|
|
206
|
+
const renderedLines = total + 1; // items + footer (always shown)
|
|
207
|
+
let firstDraw = true;
|
|
208
|
+
|
|
209
|
+
if (title) {
|
|
210
|
+
console.log('');
|
|
211
|
+
console.log(` ${title}`);
|
|
212
|
+
console.log(c.dim(' ' + '─'.repeat(60)));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
process.stdout.write(HIDE_CURSOR);
|
|
216
|
+
|
|
217
|
+
const draw = () => {
|
|
218
|
+
if (!firstDraw) {
|
|
219
|
+
process.stdout.write(UP(renderedLines - 1) + CR);
|
|
220
|
+
}
|
|
221
|
+
const lines = renderList(items, cursor, selected, true);
|
|
222
|
+
writeLines(lines);
|
|
223
|
+
process.stdout.write('\n');
|
|
224
|
+
process.stdout.write(`${CR}${CLEAR_LINE}${c.dim(` ${footerText}`)}`);
|
|
225
|
+
firstDraw = false;
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
draw();
|
|
229
|
+
|
|
230
|
+
process.stdin.setRawMode(true);
|
|
231
|
+
process.stdin.resume();
|
|
232
|
+
|
|
233
|
+
const cleanup = () => {
|
|
234
|
+
process.stdin.setRawMode(false);
|
|
235
|
+
process.stdin.pause();
|
|
236
|
+
process.stdin.removeListener('data', onKey);
|
|
237
|
+
process.stdout.write(CR + SHOW_CURSOR + '\n');
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const onKey = (buf) => {
|
|
241
|
+
const key = decodeKey(buf);
|
|
242
|
+
if (key === 'up') {
|
|
243
|
+
cursor = (cursor - 1 + total) % total;
|
|
244
|
+
draw();
|
|
245
|
+
} else if (key === 'down') {
|
|
246
|
+
cursor = (cursor + 1) % total;
|
|
247
|
+
draw();
|
|
248
|
+
} else if (key === 'space') {
|
|
249
|
+
if (selected.has(cursor)) selected.delete(cursor);
|
|
250
|
+
else selected.add(cursor);
|
|
251
|
+
draw();
|
|
252
|
+
} else if (key === 'a') {
|
|
253
|
+
if (selected.size === total) selected.clear();
|
|
254
|
+
else { for (let i = 0; i < total; i++) selected.add(i); }
|
|
255
|
+
draw();
|
|
256
|
+
} else if (key === 'enter') {
|
|
257
|
+
cleanup();
|
|
258
|
+
const indices = [...selected].sort((a, b) => a - b);
|
|
259
|
+
const values = indices.map(i => items[i].value);
|
|
260
|
+
const labels = indices.map(i => strip(items[i].label));
|
|
261
|
+
if (labels.length === 0) {
|
|
262
|
+
console.log(c.dim(' None selected'));
|
|
263
|
+
} else if (labels.length === total) {
|
|
264
|
+
console.log(c.success('All selected'));
|
|
265
|
+
} else {
|
|
266
|
+
console.log(c.success(labels.join(', ')));
|
|
267
|
+
}
|
|
268
|
+
resolve({ indices, values });
|
|
269
|
+
} else if (key === 'escape') {
|
|
270
|
+
cleanup();
|
|
271
|
+
const indices = [...(defaultSelected || [])].sort((a, b) => a - b);
|
|
272
|
+
const values = indices.map(i => items[i].value);
|
|
273
|
+
resolve({ indices, values });
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
process.stdin.on('data', onKey);
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ── Fallback implementations (non-TTY) ───────────────────────────────────────
|
|
282
|
+
|
|
283
|
+
async function _fallbackSelectOne({ title, items, default: defaultIdx }) {
|
|
284
|
+
const readline = require('readline');
|
|
285
|
+
if (title) {
|
|
286
|
+
console.log('');
|
|
287
|
+
console.log(` ${title}`);
|
|
288
|
+
console.log(c.dim(' ' + '─'.repeat(50)));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
items.forEach((item, i) => {
|
|
292
|
+
const marker = i === defaultIdx ? c.green(' ← default') : '';
|
|
293
|
+
console.log(` ${c.cyan(`[${i + 1}]`)} ${item.label}${item.hint ? c.dim(` ${item.hint}`) : ''}${marker}`);
|
|
294
|
+
});
|
|
295
|
+
console.log('');
|
|
296
|
+
|
|
297
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
298
|
+
return new Promise(resolve => {
|
|
299
|
+
rl.question(` Select [1-${items.length}] (Enter = default): `, answer => {
|
|
300
|
+
rl.close();
|
|
301
|
+
const trimmed = (answer || '').trim();
|
|
302
|
+
if (!trimmed) {
|
|
303
|
+
resolve({ index: defaultIdx, value: items[defaultIdx].value });
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const num = parseInt(trimmed, 10);
|
|
307
|
+
if (num >= 1 && num <= items.length) {
|
|
308
|
+
resolve({ index: num - 1, value: items[num - 1].value });
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
resolve({ index: defaultIdx, value: items[defaultIdx].value });
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async function _fallbackSelectMany({ title, items, defaultSelected }) {
|
|
317
|
+
const readline = require('readline');
|
|
318
|
+
if (title) {
|
|
319
|
+
console.log('');
|
|
320
|
+
console.log(` ${title}`);
|
|
321
|
+
console.log(c.dim(' ' + '─'.repeat(50)));
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
items.forEach((item, i) => {
|
|
325
|
+
console.log(` ${c.cyan(`[${i + 1}]`)} ${item.label}${item.hint ? c.dim(` ${item.hint}`) : ''}`);
|
|
326
|
+
});
|
|
327
|
+
console.log(` ${c.cyan('[A]')} All`);
|
|
328
|
+
console.log('');
|
|
329
|
+
|
|
330
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
331
|
+
return new Promise(resolve => {
|
|
332
|
+
rl.question(' Select (e.g. 1,3 or A for all) [Enter = all]: ', answer => {
|
|
333
|
+
rl.close();
|
|
334
|
+
const trimmed = (answer || '').trim().toLowerCase();
|
|
335
|
+
if (!trimmed || trimmed === 'a' || trimmed === 'all') {
|
|
336
|
+
const all = items.map((_, i) => i);
|
|
337
|
+
resolve({ indices: all, values: items.map(it => it.value) });
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
const parts = trimmed.split(/[\s,]+/).filter(Boolean);
|
|
341
|
+
const indices = [];
|
|
342
|
+
for (const p of parts) {
|
|
343
|
+
const num = parseInt(p, 10);
|
|
344
|
+
if (num >= 1 && num <= items.length) indices.push(num - 1);
|
|
345
|
+
}
|
|
346
|
+
if (indices.length === 0) {
|
|
347
|
+
const all = items.map((_, i) => i);
|
|
348
|
+
resolve({ indices: all, values: items.map(it => it.value) });
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
resolve({ indices, values: indices.map(i => items[i].value) });
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
module.exports = { selectOne, selectMany };
|