task-summary-extractor 9.0.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "task-summary-extractor",
3
- "version": "9.0.0",
3
+ "version": "9.0.1",
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('');
@@ -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: parseInt(flags.parallel, 10) || MAX_PARALLEL_UPLOADS,
59
+ parallel: safeInt(flags.parallel, MAX_PARALLEL_UPLOADS),
53
60
  logLevel: flags['log-level'] || LOG_LEVEL,
54
61
  outputDir: flags.output || null,
55
- thinkingBudget: parseInt(flags['thinking-budget'], 10) || THINKING_BUDGET,
56
- compilationThinkingBudget: parseInt(flags['compilation-thinking-budget'], 10) || COMPILATION_THINKING_BUDGET,
57
- parallelAnalysis: parseInt(flags['parallel-analysis'], 10) || 2, // concurrent segment analysis
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'],
@@ -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.PASS}) — retrying with enhanced hints...`);
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
 
@@ -64,7 +64,7 @@ async function phaseServices(ctx) {
64
64
  .map(({ absPath, relPath }) => ({
65
65
  type: 'inlineText',
66
66
  fileName: relPath,
67
- content: fs.readFileSync(absPath, 'utf8'),
67
+ content: fs.readFileSync(absPath, 'utf8').replace(/^\uFEFF/, ''),
68
68
  }));
69
69
  }
70
70
 
package/src/pipeline.js CHANGED
@@ -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
  }
@@ -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 c of comments) {
459
- const speaker = c.speaker ? resolve(c.speaker, clusterMap) : 'Unknown';
460
- const ts = c.timestamp ? `<code>${e(c.timestamp)}</code> ` : '';
461
- ln(`<li>${ts}<strong>${e(speaker)}</strong>: "${e(c.text)}"</li>`);
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
  }
@@ -54,7 +54,10 @@ function clusterNames(rawNames) {
54
54
 
55
55
  let merged = false;
56
56
  for (const [existNk, c] of normToCluster) {
57
- if (existNk.includes(nk) || nk.includes(existNk)) {
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
- if (cnk.includes(nk) || nk.includes(cnk)) return canonical;
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) {
@@ -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.fast;
249
+ const tier = tiers[m.tier] || tiers.economy;
250
250
  idx++;
251
251
  indexMap[idx] = id;
252
252
  tier.models.push({ idx, id, ...m });
@@ -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: filterArr(compiled.your_tasks.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 PASS without retry (0-100) */
23
- PASS: 45,
24
- /** Score range for WARNING will pass but flag issues (45-65 is typical) */
25
- WARN: 65,
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.WARN) {
304
+ if (compositeScore >= THRESHOLDS.PASS_ABOVE) {
305
305
  grade = 'PASS';
306
- } else if (compositeScore >= THRESHOLDS.PASS) {
306
+ } else if (compositeScore >= THRESHOLDS.FAIL_BELOW) {
307
307
  grade = 'WARN';
308
308
  } else {
309
309
  grade = 'FAIL';