@in-the-loop-labs/pair-review 1.3.2 → 1.4.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 +67 -38
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin-code-critic/.claude-plugin/plugin.json +1 -1
- package/public/index.html +270 -623
- package/public/js/index.js +1071 -0
- package/public/js/local.js +80 -0
- package/public/js/modules/analysis-history.js +5 -1
- package/public/local.html +45 -2
- package/src/ai/claude-provider.js +12 -7
- package/src/ai/codex-provider.js +9 -7
- package/src/ai/cursor-agent-provider.js +9 -6
- package/src/ai/gemini-provider.js +9 -7
- package/src/ai/index.js +1 -0
- package/src/ai/opencode-provider.js +9 -7
- package/src/ai/pi-provider.js +859 -0
- package/src/ai/provider.js +32 -8
- package/src/ai/stream-parser.js +171 -2
- package/src/config.js +1 -1
- package/src/database.js +170 -40
- package/src/local-review.js +9 -0
- package/src/routes/local.js +390 -41
- package/src/utils/json-extractor.js +129 -39
package/public/js/local.js
CHANGED
|
@@ -1361,10 +1361,90 @@ class LocalManager {
|
|
|
1361
1361
|
});
|
|
1362
1362
|
}
|
|
1363
1363
|
|
|
1364
|
+
/**
|
|
1365
|
+
* Initialize inline name editing for the review title in the header
|
|
1366
|
+
*/
|
|
1367
|
+
initNameEditing() {
|
|
1368
|
+
const nameEl = document.getElementById('local-review-name');
|
|
1369
|
+
if (!nameEl || nameEl.dataset.listenerAttached) return;
|
|
1370
|
+
nameEl.dataset.listenerAttached = 'true';
|
|
1371
|
+
|
|
1372
|
+
const reviewId = this.reviewId;
|
|
1373
|
+
|
|
1374
|
+
nameEl.addEventListener('click', () => {
|
|
1375
|
+
if (nameEl.querySelector('input')) return; // already editing
|
|
1376
|
+
|
|
1377
|
+
const currentName = nameEl.dataset.currentName || '';
|
|
1378
|
+
const input = document.createElement('input');
|
|
1379
|
+
input.type = 'text';
|
|
1380
|
+
input.className = 'local-review-name-input';
|
|
1381
|
+
input.value = currentName;
|
|
1382
|
+
input.placeholder = 'Untitled';
|
|
1383
|
+
|
|
1384
|
+
nameEl.textContent = '';
|
|
1385
|
+
nameEl.appendChild(input);
|
|
1386
|
+
input.focus();
|
|
1387
|
+
input.select();
|
|
1388
|
+
|
|
1389
|
+
let saved = false;
|
|
1390
|
+
|
|
1391
|
+
async function save() {
|
|
1392
|
+
if (saved) return;
|
|
1393
|
+
saved = true;
|
|
1394
|
+
const newName = input.value.trim() || null;
|
|
1395
|
+
try {
|
|
1396
|
+
const response = await fetch(`/api/local/${reviewId}/name`, {
|
|
1397
|
+
method: 'PATCH',
|
|
1398
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1399
|
+
body: JSON.stringify({ name: newName })
|
|
1400
|
+
});
|
|
1401
|
+
if (!response.ok) throw new Error('Save failed');
|
|
1402
|
+
nameEl.dataset.currentName = newName || '';
|
|
1403
|
+
nameEl.textContent = newName || 'Untitled';
|
|
1404
|
+
nameEl.classList.toggle('unnamed', !newName);
|
|
1405
|
+
nameEl.title = 'Click to rename';
|
|
1406
|
+
} catch (error) {
|
|
1407
|
+
// Revert the display to the previous name on failure
|
|
1408
|
+
cancel();
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
function cancel() {
|
|
1413
|
+
nameEl.textContent = currentName || 'Untitled';
|
|
1414
|
+
nameEl.classList.toggle('unnamed', !currentName);
|
|
1415
|
+
nameEl.title = 'Click to rename';
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
input.addEventListener('blur', save);
|
|
1419
|
+
input.addEventListener('keydown', function(e) {
|
|
1420
|
+
if (e.key === 'Enter') {
|
|
1421
|
+
e.preventDefault();
|
|
1422
|
+
input.removeEventListener('blur', save);
|
|
1423
|
+
save();
|
|
1424
|
+
} else if (e.key === 'Escape') {
|
|
1425
|
+
e.preventDefault();
|
|
1426
|
+
input.removeEventListener('blur', save);
|
|
1427
|
+
cancel();
|
|
1428
|
+
}
|
|
1429
|
+
});
|
|
1430
|
+
});
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1364
1433
|
/**
|
|
1365
1434
|
* Update local header with review info
|
|
1366
1435
|
*/
|
|
1367
1436
|
updateLocalHeader(reviewData) {
|
|
1437
|
+
// Update review name/title in header
|
|
1438
|
+
const nameEl = document.getElementById('local-review-name');
|
|
1439
|
+
if (nameEl) {
|
|
1440
|
+
const name = reviewData.name || '';
|
|
1441
|
+
nameEl.textContent = name || 'Untitled';
|
|
1442
|
+
nameEl.dataset.currentName = name;
|
|
1443
|
+
nameEl.classList.toggle('unnamed', !name);
|
|
1444
|
+
nameEl.title = 'Click to rename';
|
|
1445
|
+
this.initNameEditing();
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1368
1448
|
// Update repository name
|
|
1369
1449
|
const repoName = document.getElementById('local-repo-name');
|
|
1370
1450
|
if (repoName) {
|
|
@@ -748,7 +748,11 @@ class AnalysisHistoryManager {
|
|
|
748
748
|
'o1': 'thorough',
|
|
749
749
|
'o1-mini': 'balanced',
|
|
750
750
|
// Copilot models
|
|
751
|
-
'gpt-4': 'balanced'
|
|
751
|
+
'gpt-4': 'balanced',
|
|
752
|
+
// Pi models
|
|
753
|
+
'default': 'balanced',
|
|
754
|
+
'multi-model': 'thorough',
|
|
755
|
+
'review-roulette': 'thorough'
|
|
752
756
|
};
|
|
753
757
|
|
|
754
758
|
return modelTiers[modelId] || null;
|
package/public/local.html
CHANGED
|
@@ -165,6 +165,49 @@
|
|
|
165
165
|
unicode-bidi: bidi-override;
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
+
/* Editable review name in header */
|
|
169
|
+
.local-review-name {
|
|
170
|
+
font-size: 15px;
|
|
171
|
+
font-weight: 500;
|
|
172
|
+
color: var(--color-text-primary);
|
|
173
|
+
cursor: text;
|
|
174
|
+
padding: 2px 6px;
|
|
175
|
+
border-radius: 4px;
|
|
176
|
+
border: 1px dashed var(--color-border-primary);
|
|
177
|
+
transition: all 0.15s ease;
|
|
178
|
+
max-width: 360px;
|
|
179
|
+
overflow: hidden;
|
|
180
|
+
text-overflow: ellipsis;
|
|
181
|
+
white-space: nowrap;
|
|
182
|
+
position: relative;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.local-review-name:hover {
|
|
186
|
+
background: var(--color-bg-secondary);
|
|
187
|
+
border-color: var(--ai-primary);
|
|
188
|
+
border-style: solid;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.local-review-name.unnamed {
|
|
192
|
+
color: var(--color-text-tertiary);
|
|
193
|
+
font-style: italic;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.local-review-name-input {
|
|
197
|
+
font-family: 'DM Sans', -apple-system, sans-serif;
|
|
198
|
+
font-size: 15px;
|
|
199
|
+
font-weight: 500;
|
|
200
|
+
padding: 2px 6px;
|
|
201
|
+
border: 1px solid #d97706;
|
|
202
|
+
border-radius: 4px;
|
|
203
|
+
background: var(--color-bg-primary);
|
|
204
|
+
color: var(--color-text-primary);
|
|
205
|
+
outline: none;
|
|
206
|
+
box-shadow: 0 0 0 2px rgba(217, 119, 6, 0.15);
|
|
207
|
+
width: 280px;
|
|
208
|
+
max-width: 360px;
|
|
209
|
+
}
|
|
210
|
+
|
|
168
211
|
</style>
|
|
169
212
|
</head>
|
|
170
213
|
<body>
|
|
@@ -205,8 +248,8 @@
|
|
|
205
248
|
</div>
|
|
206
249
|
</div>
|
|
207
250
|
<div class="header-center">
|
|
208
|
-
<!--
|
|
209
|
-
|
|
251
|
+
<!-- Editable review name/title -->
|
|
252
|
+
<span class="local-review-name" id="local-review-name" title="Click to rename">Untitled</span>
|
|
210
253
|
</div>
|
|
211
254
|
<div class="header-right">
|
|
212
255
|
<div class="header-icon-group">
|
|
@@ -396,24 +396,28 @@ class ClaudeProvider extends AIProvider {
|
|
|
396
396
|
} else {
|
|
397
397
|
// Regex extraction failed, try LLM-based extraction as fallback
|
|
398
398
|
logger.warn(`${levelPrefix} Regex extraction failed: ${parsed.error}`);
|
|
399
|
-
|
|
399
|
+
// Pass extracted text content to LLM fallback (not raw JSONL stdout).
|
|
400
|
+
// The text content is the actual LLM response text extracted from JSONL
|
|
401
|
+
// events and is much smaller and more relevant than the full JSONL stream.
|
|
402
|
+
const llmFallbackInput = parsed.textContent || stdout;
|
|
403
|
+
logger.info(`${levelPrefix} LLM fallback input length: ${llmFallbackInput.length} characters (${parsed.textContent ? 'text content' : 'raw stdout'})`);
|
|
400
404
|
logger.info(`${levelPrefix} Attempting LLM-based JSON extraction fallback...`);
|
|
401
405
|
|
|
402
406
|
// Use async IIFE to handle the async LLM extraction
|
|
403
407
|
(async () => {
|
|
404
408
|
try {
|
|
405
|
-
const llmExtracted = await this.extractJSONWithLLM(
|
|
409
|
+
const llmExtracted = await this.extractJSONWithLLM(llmFallbackInput, { level, analysisId, registerProcess });
|
|
406
410
|
if (llmExtracted.success) {
|
|
407
411
|
logger.success(`${levelPrefix} LLM extraction fallback succeeded`);
|
|
408
412
|
settle(resolve, llmExtracted.data);
|
|
409
413
|
} else {
|
|
410
414
|
logger.warn(`${levelPrefix} LLM extraction fallback also failed: ${llmExtracted.error}`);
|
|
411
|
-
logger.info(`${levelPrefix} Raw response preview: ${
|
|
412
|
-
settle(resolve, { raw:
|
|
415
|
+
logger.info(`${levelPrefix} Raw response preview: ${llmFallbackInput.substring(0, 500)}...`);
|
|
416
|
+
settle(resolve, { raw: llmFallbackInput, parsed: false });
|
|
413
417
|
}
|
|
414
418
|
} catch (llmError) {
|
|
415
419
|
logger.warn(`${levelPrefix} LLM extraction fallback error: ${llmError.message}`);
|
|
416
|
-
settle(resolve, { raw:
|
|
420
|
+
settle(resolve, { raw: llmFallbackInput, parsed: false });
|
|
417
421
|
}
|
|
418
422
|
})();
|
|
419
423
|
}
|
|
@@ -731,9 +735,10 @@ class ClaudeProvider extends AIProvider {
|
|
|
731
735
|
return extracted;
|
|
732
736
|
}
|
|
733
737
|
|
|
734
|
-
// If no JSON found, return the
|
|
738
|
+
// If no JSON found, return with textContent so the caller can
|
|
739
|
+
// pass it (not raw JSONL stdout) to the LLM extraction fallback
|
|
735
740
|
logger.warn(`${levelPrefix} Text content is not JSON, treating as raw text`);
|
|
736
|
-
return { success: false, error: 'Text content is not valid JSON' };
|
|
741
|
+
return { success: false, error: 'Text content is not valid JSON', textContent };
|
|
737
742
|
}
|
|
738
743
|
|
|
739
744
|
// No text content found - don't fall back to raw stdout extraction
|
package/src/ai/codex-provider.js
CHANGED
|
@@ -271,24 +271,25 @@ class CodexProvider extends AIProvider {
|
|
|
271
271
|
} else {
|
|
272
272
|
// Regex extraction failed, try LLM-based extraction as fallback
|
|
273
273
|
logger.warn(`${levelPrefix} Regex extraction failed: ${parsed.error}`);
|
|
274
|
-
|
|
274
|
+
const llmFallbackInput = parsed.textContent || stdout;
|
|
275
|
+
logger.info(`${levelPrefix} LLM fallback input length: ${llmFallbackInput.length} characters (${parsed.textContent ? 'text content' : 'raw stdout'})`);
|
|
275
276
|
logger.info(`${levelPrefix} Attempting LLM-based JSON extraction fallback...`);
|
|
276
277
|
|
|
277
278
|
// Use async IIFE to handle the async LLM extraction
|
|
278
279
|
(async () => {
|
|
279
280
|
try {
|
|
280
|
-
const llmExtracted = await this.extractJSONWithLLM(
|
|
281
|
+
const llmExtracted = await this.extractJSONWithLLM(llmFallbackInput, { level, analysisId, registerProcess });
|
|
281
282
|
if (llmExtracted.success) {
|
|
282
283
|
logger.success(`${levelPrefix} LLM extraction fallback succeeded`);
|
|
283
284
|
settle(resolve, llmExtracted.data);
|
|
284
285
|
} else {
|
|
285
286
|
logger.warn(`${levelPrefix} LLM extraction fallback also failed: ${llmExtracted.error}`);
|
|
286
|
-
logger.info(`${levelPrefix} Raw response preview: ${
|
|
287
|
-
settle(resolve, { raw:
|
|
287
|
+
logger.info(`${levelPrefix} Raw response preview: ${llmFallbackInput.substring(0, 500)}...`);
|
|
288
|
+
settle(resolve, { raw: llmFallbackInput, parsed: false });
|
|
288
289
|
}
|
|
289
290
|
} catch (llmError) {
|
|
290
291
|
logger.warn(`${levelPrefix} LLM extraction fallback error: ${llmError.message}`);
|
|
291
|
-
settle(resolve, { raw:
|
|
292
|
+
settle(resolve, { raw: llmFallbackInput, parsed: false });
|
|
292
293
|
}
|
|
293
294
|
})();
|
|
294
295
|
}
|
|
@@ -373,9 +374,10 @@ class CodexProvider extends AIProvider {
|
|
|
373
374
|
return extracted;
|
|
374
375
|
}
|
|
375
376
|
|
|
376
|
-
// If no JSON found, return the
|
|
377
|
+
// If no JSON found, return with textContent so the caller can
|
|
378
|
+
// pass it (not raw JSONL stdout) to the LLM extraction fallback
|
|
377
379
|
logger.warn(`${levelPrefix} Agent message is not JSON, treating as raw text`);
|
|
378
|
-
return { success: false, error: 'Agent message is not valid JSON' };
|
|
380
|
+
return { success: false, error: 'Agent message is not valid JSON', textContent: agentMessageText };
|
|
379
381
|
}
|
|
380
382
|
|
|
381
383
|
// No agent message found, try extracting JSON directly from stdout
|
|
@@ -314,7 +314,8 @@ class CursorAgentProvider extends AIProvider {
|
|
|
314
314
|
} else {
|
|
315
315
|
// Regex extraction failed, try LLM-based extraction as fallback
|
|
316
316
|
logger.warn(`${levelPrefix} Regex extraction failed: ${parsed.error}`);
|
|
317
|
-
|
|
317
|
+
const llmFallbackInput = parsed.textContent || stdout;
|
|
318
|
+
logger.info(`${levelPrefix} LLM fallback input length: ${llmFallbackInput.length} characters (${parsed.textContent ? 'text content' : 'raw stdout'})`);
|
|
318
319
|
logger.info(`${levelPrefix} Attempting LLM-based JSON extraction fallback...`);
|
|
319
320
|
|
|
320
321
|
// Use async IIFE to handle the async LLM extraction
|
|
@@ -324,18 +325,18 @@ class CursorAgentProvider extends AIProvider {
|
|
|
324
325
|
// orphan processes if timeout fired between close-handler entry
|
|
325
326
|
// and reaching this point.
|
|
326
327
|
if (settled) return;
|
|
327
|
-
const llmExtracted = await this.extractJSONWithLLM(
|
|
328
|
+
const llmExtracted = await this.extractJSONWithLLM(llmFallbackInput, { level, analysisId, registerProcess });
|
|
328
329
|
if (llmExtracted.success) {
|
|
329
330
|
logger.success(`${levelPrefix} LLM extraction fallback succeeded`);
|
|
330
331
|
settle(resolve, llmExtracted.data);
|
|
331
332
|
} else {
|
|
332
333
|
logger.warn(`${levelPrefix} LLM extraction fallback also failed: ${llmExtracted.error}`);
|
|
333
|
-
logger.info(`${levelPrefix} Raw response preview: ${
|
|
334
|
-
settle(resolve, { raw:
|
|
334
|
+
logger.info(`${levelPrefix} Raw response preview: ${llmFallbackInput.substring(0, 500)}...`);
|
|
335
|
+
settle(resolve, { raw: llmFallbackInput, parsed: false });
|
|
335
336
|
}
|
|
336
337
|
} catch (llmError) {
|
|
337
338
|
logger.warn(`${levelPrefix} LLM extraction fallback error: ${llmError.message}`);
|
|
338
|
-
settle(resolve, { raw:
|
|
339
|
+
settle(resolve, { raw: llmFallbackInput, parsed: false });
|
|
339
340
|
}
|
|
340
341
|
})();
|
|
341
342
|
}
|
|
@@ -466,7 +467,9 @@ class CursorAgentProvider extends AIProvider {
|
|
|
466
467
|
return extracted;
|
|
467
468
|
}
|
|
468
469
|
|
|
469
|
-
|
|
470
|
+
// Include textContent so the caller can pass it to LLM extraction fallback
|
|
471
|
+
const textContent = assistantText || resultText || null;
|
|
472
|
+
return { success: false, error: 'No valid JSON found in assistant or result text', textContent };
|
|
470
473
|
|
|
471
474
|
} catch (parseError) {
|
|
472
475
|
// stdout might not be valid JSONL at all, try extracting JSON from it
|
|
@@ -320,24 +320,25 @@ class GeminiProvider extends AIProvider {
|
|
|
320
320
|
} else {
|
|
321
321
|
// Regex extraction failed, try LLM-based extraction as fallback
|
|
322
322
|
logger.warn(`${levelPrefix} Regex extraction failed: ${parsed.error}`);
|
|
323
|
-
|
|
323
|
+
const llmFallbackInput = parsed.textContent || stdout;
|
|
324
|
+
logger.info(`${levelPrefix} LLM fallback input length: ${llmFallbackInput.length} characters (${parsed.textContent ? 'text content' : 'raw stdout'})`);
|
|
324
325
|
logger.info(`${levelPrefix} Attempting LLM-based JSON extraction fallback...`);
|
|
325
326
|
|
|
326
327
|
// Use async IIFE to handle the async LLM extraction
|
|
327
328
|
(async () => {
|
|
328
329
|
try {
|
|
329
|
-
const llmExtracted = await this.extractJSONWithLLM(
|
|
330
|
+
const llmExtracted = await this.extractJSONWithLLM(llmFallbackInput, { level, analysisId, registerProcess });
|
|
330
331
|
if (llmExtracted.success) {
|
|
331
332
|
logger.success(`${levelPrefix} LLM extraction fallback succeeded`);
|
|
332
333
|
settle(resolve, llmExtracted.data);
|
|
333
334
|
} else {
|
|
334
335
|
logger.warn(`${levelPrefix} LLM extraction fallback also failed: ${llmExtracted.error}`);
|
|
335
|
-
logger.info(`${levelPrefix} Raw response preview: ${
|
|
336
|
-
settle(resolve, { raw:
|
|
336
|
+
logger.info(`${levelPrefix} Raw response preview: ${llmFallbackInput.substring(0, 500)}...`);
|
|
337
|
+
settle(resolve, { raw: llmFallbackInput, parsed: false });
|
|
337
338
|
}
|
|
338
339
|
} catch (llmError) {
|
|
339
340
|
logger.warn(`${levelPrefix} LLM extraction fallback error: ${llmError.message}`);
|
|
340
|
-
settle(resolve, { raw:
|
|
341
|
+
settle(resolve, { raw: llmFallbackInput, parsed: false });
|
|
341
342
|
}
|
|
342
343
|
})();
|
|
343
344
|
}
|
|
@@ -424,9 +425,10 @@ class GeminiProvider extends AIProvider {
|
|
|
424
425
|
return extracted;
|
|
425
426
|
}
|
|
426
427
|
|
|
427
|
-
// If no JSON found, return the
|
|
428
|
+
// If no JSON found, return with textContent so the caller can
|
|
429
|
+
// pass it (not raw JSONL stdout) to the LLM extraction fallback
|
|
428
430
|
logger.warn(`${levelPrefix} Assistant message is not JSON, treating as raw text`);
|
|
429
|
-
return { success: false, error: 'Assistant message is not valid JSON' };
|
|
431
|
+
return { success: false, error: 'Assistant message is not valid JSON', textContent: assistantText };
|
|
430
432
|
}
|
|
431
433
|
|
|
432
434
|
// No assistant message found, try extracting JSON directly from stdout
|
package/src/ai/index.js
CHANGED
|
@@ -255,24 +255,25 @@ class OpenCodeProvider extends AIProvider {
|
|
|
255
255
|
} else {
|
|
256
256
|
// Regex extraction failed, try LLM-based extraction as fallback
|
|
257
257
|
logger.warn(`${levelPrefix} Regex extraction failed: ${parsed.error}`);
|
|
258
|
-
|
|
258
|
+
const llmFallbackInput = parsed.textContent || stdout;
|
|
259
|
+
logger.info(`${levelPrefix} LLM fallback input length: ${llmFallbackInput.length} characters (${parsed.textContent ? 'text content' : 'raw stdout'})`);
|
|
259
260
|
logger.info(`${levelPrefix} Attempting LLM-based JSON extraction fallback...`);
|
|
260
261
|
|
|
261
262
|
// Use async IIFE to handle the async LLM extraction
|
|
262
263
|
(async () => {
|
|
263
264
|
try {
|
|
264
|
-
const llmExtracted = await this.extractJSONWithLLM(
|
|
265
|
+
const llmExtracted = await this.extractJSONWithLLM(llmFallbackInput, { level, analysisId, registerProcess });
|
|
265
266
|
if (llmExtracted.success) {
|
|
266
267
|
logger.success(`${levelPrefix} LLM extraction fallback succeeded`);
|
|
267
268
|
settle(resolve, llmExtracted.data);
|
|
268
269
|
} else {
|
|
269
270
|
logger.warn(`${levelPrefix} LLM extraction fallback also failed: ${llmExtracted.error}`);
|
|
270
|
-
logger.info(`${levelPrefix} Raw response preview: ${
|
|
271
|
-
settle(resolve, { raw:
|
|
271
|
+
logger.info(`${levelPrefix} Raw response preview: ${llmFallbackInput.substring(0, 500)}...`);
|
|
272
|
+
settle(resolve, { raw: llmFallbackInput, parsed: false });
|
|
272
273
|
}
|
|
273
274
|
} catch (llmError) {
|
|
274
275
|
logger.warn(`${levelPrefix} LLM extraction fallback error: ${llmError.message}`);
|
|
275
|
-
settle(resolve, { raw:
|
|
276
|
+
settle(resolve, { raw: llmFallbackInput, parsed: false });
|
|
276
277
|
}
|
|
277
278
|
})();
|
|
278
279
|
}
|
|
@@ -495,9 +496,10 @@ class OpenCodeProvider extends AIProvider {
|
|
|
495
496
|
return extracted;
|
|
496
497
|
}
|
|
497
498
|
|
|
498
|
-
// If no JSON found, return the
|
|
499
|
+
// If no JSON found, return with textContent so the caller can
|
|
500
|
+
// pass it (not raw JSONL stdout) to the LLM extraction fallback
|
|
499
501
|
logger.warn(`${levelPrefix} Text content is not JSON, treating as raw text`);
|
|
500
|
-
return { success: false, error: 'Text content is not valid JSON' };
|
|
502
|
+
return { success: false, error: 'Text content is not valid JSON', textContent };
|
|
501
503
|
}
|
|
502
504
|
|
|
503
505
|
// No text content found, try extracting JSON directly from stdout
|