task-summary-extractor 9.0.0 → 9.1.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/package.json +1 -1
- package/prompt.json +2 -2
- package/src/logger.js +7 -4
- package/src/phases/init.js +22 -7
- package/src/phases/output.js +5 -6
- package/src/phases/process-media.js +1 -1
- package/src/phases/services.js +1 -1
- package/src/pipeline.js +2 -2
- package/src/renderers/html.js +4 -4
- package/src/renderers/shared.js +6 -2
- package/src/services/doc-parser.js +2 -2
- package/src/services/gemini.js +17 -1
- package/src/utils/cli.js +3 -2
- package/src/utils/confidence-filter.js +2 -1
- package/src/utils/quality-gate.js +6 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "task-summary-extractor",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.1.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/prompt.json
CHANGED
|
@@ -225,7 +225,7 @@
|
|
|
225
225
|
"what": "string (precise description of the change)",
|
|
226
226
|
"how": "string (implementation/execution approach discussed or specified)",
|
|
227
227
|
"why": "string (business, technical, legal, or strategic reason)",
|
|
228
|
-
"type": "bug_fix|feature|refactor|optimization|cleanup|upgrade|integration|configuration|process_change|policy_update|contract_revision|design_change",
|
|
228
|
+
"type": "bug_fix|feature|refactor|optimization|cleanup|upgrade|integration|configuration|process_change|policy_update|contract_revision|design_change|content_update|documentation",
|
|
229
229
|
"priority": "low|medium|high|critical",
|
|
230
230
|
"status": "actionable|pending_decision|blocked|completed",
|
|
231
231
|
"dependencies": ["string (IDs of other change requests this depends on)"],
|
|
@@ -316,7 +316,7 @@
|
|
|
316
316
|
"blockers": [
|
|
317
317
|
{
|
|
318
318
|
"id": "string (BLK-1, BLK-2, or matching ID from tracking docs like DB-1, LEGAL-1, INFRA-1)",
|
|
319
|
-
"type": "database_prerequisite|pending_decision|dba_work|external_dependency|testing|deployment|legal_review|budget_approval|vendor_dependency|regulatory|access_permission|resource_constraint",
|
|
319
|
+
"type": "database_prerequisite|pending_decision|dba_work|external_dependency|testing|deployment|legal_review|budget_approval|vendor_dependency|regulatory|access_permission|resource_constraint|backend_work",
|
|
320
320
|
"description": "string",
|
|
321
321
|
"owner": "string (who needs to resolve this — person, team, department, external party, etc.)",
|
|
322
322
|
"blocks": ["string (work-item IDs or action items blocked by this)"],
|
package/src/logger.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
const fs = require('fs');
|
|
15
15
|
const path = require('path');
|
|
16
|
+
const { strip: stripAnsi } = require('./utils/colors');
|
|
16
17
|
|
|
17
18
|
const LOG_LEVELS = { debug: 0, info: 1, warn: 2, error: 3 };
|
|
18
19
|
|
|
@@ -85,12 +86,12 @@ class Logger {
|
|
|
85
86
|
|
|
86
87
|
_writeDetailed(line) {
|
|
87
88
|
if (this.closed) return;
|
|
88
|
-
this._detailedBuffer.push(line + '\n');
|
|
89
|
+
this._detailedBuffer.push(stripAnsi(line) + '\n');
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
_writeMinimal(line) {
|
|
92
93
|
if (this.closed) return;
|
|
93
|
-
this._minimalBuffer.push(line + '\n');
|
|
94
|
+
this._minimalBuffer.push(stripAnsi(line) + '\n');
|
|
94
95
|
}
|
|
95
96
|
|
|
96
97
|
_writeBoth(line) {
|
|
@@ -104,6 +105,8 @@ class Logger {
|
|
|
104
105
|
*/
|
|
105
106
|
_writeStructured(entry) {
|
|
106
107
|
if (this.closed) return;
|
|
108
|
+
// Strip ANSI from message fields so structured logs are clean
|
|
109
|
+
if (entry.message) entry.message = stripAnsi(entry.message);
|
|
107
110
|
const enriched = {
|
|
108
111
|
...entry,
|
|
109
112
|
elapsedMs: this._elapsedMs(),
|
|
@@ -117,8 +120,8 @@ class Logger {
|
|
|
117
120
|
|
|
118
121
|
_flush(sync = false) {
|
|
119
122
|
const writeFn = sync
|
|
120
|
-
? (p, d) => fs.appendFileSync(p, d)
|
|
121
|
-
: (p, d) => fs.appendFile(p, d, () => {});
|
|
123
|
+
? (p, d) => fs.appendFileSync(p, d, 'utf8')
|
|
124
|
+
: (p, d) => fs.appendFile(p, d, 'utf8', () => {});
|
|
122
125
|
|
|
123
126
|
if (this._detailedBuffer.length > 0) {
|
|
124
127
|
const data = this._detailedBuffer.join('');
|
package/src/phases/init.js
CHANGED
|
@@ -23,6 +23,13 @@ const { loadHistory, analyzeHistory, printLearningInsights } = require('../utils
|
|
|
23
23
|
// --- Shared state ---
|
|
24
24
|
const { PKG_ROOT, PROJECT_ROOT, setLog, isShuttingDown, setShuttingDown } = require('./_shared');
|
|
25
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
|
+
|
|
26
33
|
// ======================== PHASE: INIT ========================
|
|
27
34
|
|
|
28
35
|
/**
|
|
@@ -49,12 +56,12 @@ async function phaseInit() {
|
|
|
49
56
|
reanalyze: !!flags.reanalyze,
|
|
50
57
|
dryRun: !!flags['dry-run'],
|
|
51
58
|
userName: flags.name || null,
|
|
52
|
-
parallel:
|
|
59
|
+
parallel: safeInt(flags.parallel, MAX_PARALLEL_UPLOADS),
|
|
53
60
|
logLevel: flags['log-level'] || LOG_LEVEL,
|
|
54
61
|
outputDir: flags.output || null,
|
|
55
|
-
thinkingBudget:
|
|
56
|
-
compilationThinkingBudget:
|
|
57
|
-
parallelAnalysis:
|
|
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
|
|
58
65
|
disableFocusedPass: !!flags['no-focused-pass'],
|
|
59
66
|
disableLearning: !!flags['no-learning'],
|
|
60
67
|
disableDiff: !!flags['no-diff'],
|
|
@@ -79,11 +86,19 @@ async function phaseInit() {
|
|
|
79
86
|
opts.minConfidence = check.normalised.toLowerCase();
|
|
80
87
|
}
|
|
81
88
|
|
|
82
|
-
// --- Validate --format flag ---
|
|
89
|
+
// --- Validate --format flag (supports comma-separated: md,html,pdf) ---
|
|
83
90
|
const VALID_FORMATS = new Set(['md', 'html', 'json', 'pdf', 'docx', 'all']);
|
|
84
|
-
|
|
85
|
-
|
|
91
|
+
const requestedFormats = opts.format.split(',').map(f => f.trim()).filter(Boolean);
|
|
92
|
+
const invalidFormats = requestedFormats.filter(f => !VALID_FORMATS.has(f));
|
|
93
|
+
if (invalidFormats.length > 0) {
|
|
94
|
+
throw new Error(`Invalid --format "${invalidFormats.join(', ')}". Valid: md, html, json, pdf, docx, all`);
|
|
86
95
|
}
|
|
96
|
+
// Normalise: "all" or set of specific formats
|
|
97
|
+
opts.formats = requestedFormats.includes('all')
|
|
98
|
+
? new Set(['md', 'html', 'json', 'pdf', 'docx'])
|
|
99
|
+
: new Set(requestedFormats);
|
|
100
|
+
// Keep opts.format as the original string for backwards compatibility
|
|
101
|
+
opts.format = requestedFormats.includes('all') ? 'all' : requestedFormats.join(',');
|
|
87
102
|
|
|
88
103
|
// --- Resolve folder: positional arg or interactive selection ---
|
|
89
104
|
let folderArg = positional[0];
|
package/src/phases/output.js
CHANGED
|
@@ -25,6 +25,8 @@ const { getLog, phaseTimer, PROJECT_ROOT } = require('./_shared');
|
|
|
25
25
|
|
|
26
26
|
/** Check whether a given output type should be rendered. */
|
|
27
27
|
function shouldRender(opts, type) {
|
|
28
|
+
if (opts.formats) return opts.formats.has(type);
|
|
29
|
+
// Fallback for legacy callers
|
|
28
30
|
if (opts.format === 'all') return true;
|
|
29
31
|
return opts.format === type;
|
|
30
32
|
}
|
|
@@ -59,14 +61,11 @@ async function phaseOutput(ctx, results, compiledAnalysis, compilationRun, compi
|
|
|
59
61
|
// Attach cost summary to results
|
|
60
62
|
results.costSummary = costTracker.getSummary();
|
|
61
63
|
|
|
62
|
-
// Write results JSON (always written
|
|
64
|
+
// Write results JSON (always written; logged only when JSON format is requested)
|
|
63
65
|
const jsonPath = path.join(runDir, 'results.json');
|
|
64
|
-
|
|
65
|
-
|
|
66
|
+
fs.writeFileSync(jsonPath, JSON.stringify(results, null, 2), 'utf8');
|
|
67
|
+
if (shouldRender(opts, 'json')) {
|
|
66
68
|
log.step(`Results JSON saved → ${jsonPath}`);
|
|
67
|
-
} else {
|
|
68
|
-
// Still write JSON internally (needed for uploads/diffs) but don't advertise
|
|
69
|
-
fs.writeFileSync(jsonPath, JSON.stringify(results, null, 2), 'utf8');
|
|
70
69
|
}
|
|
71
70
|
|
|
72
71
|
// Generate Markdown
|
|
@@ -440,7 +440,7 @@ async function phaseProcessVideo(ctx, videoPath, videoIndex) {
|
|
|
440
440
|
|
|
441
441
|
// === AUTO-RETRY on FAIL ===
|
|
442
442
|
if (qualityReport.shouldRetry && !isShuttingDown()) {
|
|
443
|
-
console.log(` ↻ Quality below threshold (${qualityReport.score}/${THRESHOLDS.
|
|
443
|
+
console.log(` ↻ Quality below threshold (${qualityReport.score}/${THRESHOLDS.FAIL_BELOW}) — retrying with enhanced hints...`);
|
|
444
444
|
log.step(`Quality gate FAIL for ${segName} (score: ${qualityReport.score}) — retrying`);
|
|
445
445
|
retried = true;
|
|
446
446
|
|
package/src/phases/services.js
CHANGED
package/src/pipeline.js
CHANGED
|
@@ -370,7 +370,7 @@ async function runDocOnly(ctx) {
|
|
|
370
370
|
const jsonPath = path.join(runDir, 'results.json');
|
|
371
371
|
fs.writeFileSync(jsonPath, JSON.stringify(results, null, 2), 'utf8');
|
|
372
372
|
|
|
373
|
-
const shouldRender = (type) => opts.format === 'all' || opts.format === type;
|
|
373
|
+
const shouldRender = (type) => opts.formats ? opts.formats.has(type) : (opts.format === 'all' || opts.format === type);
|
|
374
374
|
|
|
375
375
|
if (compiledAnalysis) {
|
|
376
376
|
const mdMeta = {
|
|
@@ -641,7 +641,7 @@ async function runDynamic(initCtx) {
|
|
|
641
641
|
const ext = path.extname(absPath).toLowerCase();
|
|
642
642
|
try {
|
|
643
643
|
if (INLINE_EXTS.includes(ext)) {
|
|
644
|
-
let content = fs.readFileSync(absPath, 'utf8');
|
|
644
|
+
let content = fs.readFileSync(absPath, 'utf8').replace(/^\uFEFF/, '');
|
|
645
645
|
if (content.length > 8000) {
|
|
646
646
|
content = content.slice(0, 8000) + '\n... (truncated)';
|
|
647
647
|
}
|
package/src/renderers/html.js
CHANGED
|
@@ -455,10 +455,10 @@ function renderResultsHtml({ compiled, meta }) {
|
|
|
455
455
|
const comments = t.comments || [];
|
|
456
456
|
if (comments.length > 0) {
|
|
457
457
|
ln('<h4>🗣️ Key Quotes</h4><ul>');
|
|
458
|
-
for (const
|
|
459
|
-
const speaker =
|
|
460
|
-
const ts =
|
|
461
|
-
ln(`<li>${ts}<strong>${e(speaker)}</strong>: "${e(
|
|
458
|
+
for (const cmt of comments) {
|
|
459
|
+
const speaker = cmt.speaker ? resolve(cmt.speaker, clusterMap) : 'Unknown';
|
|
460
|
+
const ts = cmt.timestamp ? `<code>${e(cmt.timestamp)}</code> ` : '';
|
|
461
|
+
ln(`<li>${ts}<strong>${e(speaker)}</strong>: "${e(cmt.text)}"</li>`);
|
|
462
462
|
}
|
|
463
463
|
ln('</ul>');
|
|
464
464
|
}
|
package/src/renderers/shared.js
CHANGED
|
@@ -54,7 +54,10 @@ function clusterNames(rawNames) {
|
|
|
54
54
|
|
|
55
55
|
let merged = false;
|
|
56
56
|
for (const [existNk, c] of normToCluster) {
|
|
57
|
-
if
|
|
57
|
+
// Only merge via substring if the shorter name is at least 5 chars
|
|
58
|
+
// to prevent false merges (e.g. "Ed" matching "Jeddah Dev")
|
|
59
|
+
const shorter = nk.length < existNk.length ? nk : existNk;
|
|
60
|
+
if (shorter.length >= 5 && (existNk.includes(nk) || nk.includes(existNk))) {
|
|
58
61
|
c.variants.add(raw);
|
|
59
62
|
normToCluster.set(nk, c);
|
|
60
63
|
if (stripped.length >= c.canonical.length && stripped[0] === stripped[0].toUpperCase()) {
|
|
@@ -92,7 +95,8 @@ function resolve(name, clusterMap) {
|
|
|
92
95
|
if (normalizeKey(v) === nk) return canonical;
|
|
93
96
|
}
|
|
94
97
|
const cnk = normalizeKey(canonical);
|
|
95
|
-
|
|
98
|
+
const shorter = nk.length < cnk.length ? nk : cnk;
|
|
99
|
+
if (shorter.length >= 5 && (cnk.includes(nk) || nk.includes(cnk))) return canonical;
|
|
96
100
|
}
|
|
97
101
|
return stripParens(name).trim() || name;
|
|
98
102
|
}
|
|
@@ -234,7 +234,7 @@ function stripHtml(html) {
|
|
|
234
234
|
*/
|
|
235
235
|
async function parseBuiltinText(filePath) {
|
|
236
236
|
try {
|
|
237
|
-
const content = await fs.promises.readFile(filePath, 'utf8');
|
|
237
|
+
const content = (await fs.promises.readFile(filePath, 'utf8')).replace(/^\uFEFF/, '');
|
|
238
238
|
return { text: content.trim(), warnings: [] };
|
|
239
239
|
} catch (err) {
|
|
240
240
|
return { text: '', warnings: [`Failed to read file: ${err.message}`] };
|
|
@@ -248,7 +248,7 @@ async function parseBuiltinText(filePath) {
|
|
|
248
248
|
*/
|
|
249
249
|
async function parseHtmlFile(filePath) {
|
|
250
250
|
try {
|
|
251
|
-
const html = await fs.promises.readFile(filePath, 'utf8');
|
|
251
|
+
const html = (await fs.promises.readFile(filePath, 'utf8')).replace(/^\uFEFF/, '');
|
|
252
252
|
const text = stripHtml(html);
|
|
253
253
|
return { text, warnings: [] };
|
|
254
254
|
} catch (err) {
|
package/src/services/gemini.js
CHANGED
|
@@ -61,7 +61,7 @@ async function prepareDocsForGemini(ai, docFileList) {
|
|
|
61
61
|
try {
|
|
62
62
|
if (INLINE_TEXT_EXTS.includes(ext)) {
|
|
63
63
|
console.log(` Reading ${name} (inline text)...`);
|
|
64
|
-
const content = await fs.promises.readFile(docPath, 'utf8');
|
|
64
|
+
const content = (await fs.promises.readFile(docPath, 'utf8')).replace(/^\uFEFF/, '');
|
|
65
65
|
prepared.push({ type: 'inlineText', fileName: name, content });
|
|
66
66
|
console.log(` ${c.success(`${name} ready (${(content.length / 1024).toFixed(1)} KB)`)}`);
|
|
67
67
|
} else if (DOC_PARSER_EXTS.includes(ext)) {
|
|
@@ -442,6 +442,22 @@ async function processWithGemini(ai, filePath, displayName, contextDocs = [], pr
|
|
|
442
442
|
console.error(` ${c.error(`File API fallback also failed: ${fallbackErr.message}`)}`);
|
|
443
443
|
throw fallbackErr;
|
|
444
444
|
}
|
|
445
|
+
} else if (!usedExternalUrl && errMsg.includes('INVALID_ARGUMENT')) {
|
|
446
|
+
// File API upload was used but still got INVALID_ARGUMENT — re-upload fresh and retry once
|
|
447
|
+
console.log(` ${c.warn('INVALID_ARGUMENT with File API — re-uploading and retrying...')}`);
|
|
448
|
+
try {
|
|
449
|
+
file = await uploadViaFileApi();
|
|
450
|
+
contentParts[0] = { fileData: { mimeType: file.mimeType, fileUri: file.uri } };
|
|
451
|
+
requestPayload.contents[0].parts = contentParts;
|
|
452
|
+
response = await withRetry(
|
|
453
|
+
() => ai.models.generateContent(requestPayload),
|
|
454
|
+
{ label: `Gemini segment analysis — re-upload retry (${displayName})`, maxRetries: 1, baseDelay: 5000 }
|
|
455
|
+
);
|
|
456
|
+
console.log(` ${c.success('Re-upload retry succeeded')}`);
|
|
457
|
+
} catch (reuploadErr) {
|
|
458
|
+
console.error(` ${c.error(`Re-upload retry also failed: ${reuploadErr.message}`)}`);
|
|
459
|
+
throw reuploadErr;
|
|
460
|
+
}
|
|
445
461
|
} else {
|
|
446
462
|
// Log request diagnostics for other errors to aid debugging
|
|
447
463
|
const partSummary = contentParts.map((p, i) => {
|
package/src/utils/cli.js
CHANGED
|
@@ -246,7 +246,7 @@ async function selectModel(GEMINI_MODELS, currentModel) {
|
|
|
246
246
|
const indexMap = {}; // index → modelId
|
|
247
247
|
for (const id of modelIds) {
|
|
248
248
|
const m = GEMINI_MODELS[id];
|
|
249
|
-
const tier = tiers[m.tier] || tiers.
|
|
249
|
+
const tier = tiers[m.tier] || tiers.economy;
|
|
250
250
|
idx++;
|
|
251
251
|
indexMap[idx] = id;
|
|
252
252
|
tier.models.push({ idx, id, ...m });
|
|
@@ -370,7 +370,7 @@ ${f('--deep-dive', 'Generate explanatory docs per topic')}
|
|
|
370
370
|
${h('CORE OPTIONS')}
|
|
371
371
|
${f('--name <name>', 'Your name (skip interactive prompt)')}
|
|
372
372
|
${f('--model <id>', 'Gemini model (skip interactive selector)')}
|
|
373
|
-
${f('--format <type>', 'Output
|
|
373
|
+
${f('--format <type>', 'Output: md, html, json, pdf, docx, all — comma-separated (default: all)')}
|
|
374
374
|
${f('--min-confidence <level>', 'Filter: high, medium, low (default: all)')}
|
|
375
375
|
${f('--output <dir>', 'Custom output directory for results')}
|
|
376
376
|
${f('--skip-upload', 'Skip Firebase Storage uploads')}
|
|
@@ -424,6 +424,7 @@ ${f('--version, -v', 'Show version')}
|
|
|
424
424
|
${c.dim('$')} taskex --dynamic --request "Plan API migration" "specs"
|
|
425
425
|
${c.dim('$')} taskex --min-confidence medium "call 1" ${c.dim('# Filter low-confidence')}
|
|
426
426
|
${c.dim('$')} taskex --format md "call 1" ${c.dim('# Markdown only')}
|
|
427
|
+
${c.dim('$')} taskex --format md,html,pdf "call 1" ${c.dim('# Multiple formats')}
|
|
427
428
|
${c.dim('$')} taskex --format pdf "call 1" ${c.dim('# PDF report')}
|
|
428
429
|
${c.dim('$')} taskex --format docx "call 1" ${c.dim('# Word document')}
|
|
429
430
|
${c.dim('$')} taskex --resume "call 1" ${c.dim('# Resume interrupted run')}
|
|
@@ -103,7 +103,8 @@ function filterByConfidence(compiled, minLevel = 'LOW') {
|
|
|
103
103
|
tasks_todo: filterArr(compiled.your_tasks.tasks_todo),
|
|
104
104
|
tasks_waiting_on_others: filterArr(compiled.your_tasks.tasks_waiting_on_others),
|
|
105
105
|
decisions_needed: filterArr(compiled.your_tasks.decisions_needed),
|
|
106
|
-
completed_in_call
|
|
106
|
+
// completed_in_call items are plain strings (no confidence field) — preserve unconditionally
|
|
107
|
+
completed_in_call: compiled.your_tasks.completed_in_call || [],
|
|
107
108
|
};
|
|
108
109
|
}
|
|
109
110
|
|
|
@@ -19,10 +19,10 @@ const { c } = require('./colors');
|
|
|
19
19
|
// ======================== QUALITY THRESHOLDS ========================
|
|
20
20
|
|
|
21
21
|
const THRESHOLDS = {
|
|
22
|
-
/** Minimum score to
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
|
|
22
|
+
/** Minimum score to avoid FAIL. Below this → FAIL + retry (0-100) */
|
|
23
|
+
FAIL_BELOW: 45,
|
|
24
|
+
/** Minimum score for a clean PASS. Between FAIL_BELOW and PASS_ABOVE → WARN (45-65 is typical) */
|
|
25
|
+
PASS_ABOVE: 65,
|
|
26
26
|
/** Maximum retries per segment */
|
|
27
27
|
MAX_RETRIES: 1,
|
|
28
28
|
};
|
|
@@ -301,9 +301,9 @@ function assessQuality(analysis, context = {}) {
|
|
|
301
301
|
];
|
|
302
302
|
|
|
303
303
|
let grade;
|
|
304
|
-
if (compositeScore >= THRESHOLDS.
|
|
304
|
+
if (compositeScore >= THRESHOLDS.PASS_ABOVE) {
|
|
305
305
|
grade = 'PASS';
|
|
306
|
-
} else if (compositeScore >= THRESHOLDS.
|
|
306
|
+
} else if (compositeScore >= THRESHOLDS.FAIL_BELOW) {
|
|
307
307
|
grade = 'WARN';
|
|
308
308
|
} else {
|
|
309
309
|
grade = 'FAIL';
|