@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
|
@@ -2,9 +2,18 @@
|
|
|
2
2
|
const logger = require('./logger');
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Extract JSON from text responses using multiple strategies
|
|
6
|
-
* This is a shared utility to ensure consistent JSON extraction across the application
|
|
7
|
-
*
|
|
5
|
+
* Extract JSON from text responses using multiple strategies.
|
|
6
|
+
* This is a shared utility to ensure consistent JSON extraction across the application.
|
|
7
|
+
*
|
|
8
|
+
* Strategies are tried in order:
|
|
9
|
+
* 1. Markdown code blocks (```json ... ```)
|
|
10
|
+
* 2. Direct JSON.parse of the trimmed response
|
|
11
|
+
* 3. First { to last } substring
|
|
12
|
+
* 4. Known JSON key anchors (e.g. {"level", {"suggestions")
|
|
13
|
+
* 5. Forward scan: try JSON.parse from every top-level { in the text
|
|
14
|
+
* 6. Bracket-matched substring from the first {
|
|
15
|
+
*
|
|
16
|
+
* @param {string} response - Raw response text (may include preamble/postamble prose)
|
|
8
17
|
* @param {string|number} level - Level identifier for logging (e.g., 1, 2, 3, 'orchestration', 'unknown')
|
|
9
18
|
* @returns {Object} Extraction result with success flag and data/error
|
|
10
19
|
*/
|
|
@@ -35,51 +44,132 @@ function extractJSON(response, level = 'unknown') {
|
|
|
35
44
|
}
|
|
36
45
|
throw new Error('No JSON code block found');
|
|
37
46
|
},
|
|
38
|
-
|
|
39
|
-
// Strategy 2:
|
|
47
|
+
|
|
48
|
+
// Strategy 2: Try the entire response as JSON (fast path for clean responses)
|
|
49
|
+
() => {
|
|
50
|
+
return JSON.parse(response.trim());
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
// Strategy 3: Look for JSON between first { and last }
|
|
54
|
+
// Works when the response is just JSON or has minimal wrapping
|
|
40
55
|
() => {
|
|
41
56
|
const firstBrace = response.indexOf('{');
|
|
42
57
|
const lastBrace = response.lastIndexOf('}');
|
|
43
|
-
if (firstBrace !== -1 && lastBrace !== -1 && lastBrace
|
|
58
|
+
if (firstBrace !== -1 && lastBrace !== -1 && lastBrace > firstBrace) {
|
|
44
59
|
return JSON.parse(response.substring(firstBrace, lastBrace + 1));
|
|
45
60
|
}
|
|
46
61
|
throw new Error('No valid JSON braces found');
|
|
47
62
|
},
|
|
48
|
-
|
|
49
|
-
// Strategy
|
|
63
|
+
|
|
64
|
+
// Strategy 4: Anchor-based extraction — look for known JSON key patterns
|
|
65
|
+
// that mark the start of our expected response structures.
|
|
66
|
+
// This handles the common case where preamble text contains { characters
|
|
67
|
+
// (e.g. LLM discussing code: "the function handleEvent(event) { ... }")
|
|
68
|
+
// which would cause Strategy 3 to grab the wrong first brace.
|
|
50
69
|
() => {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
70
|
+
// Look for patterns that start our expected JSON structures
|
|
71
|
+
const anchors = [
|
|
72
|
+
/\{"level"\s*:/,
|
|
73
|
+
/\{"suggestions"\s*:/,
|
|
74
|
+
/\{"fileLevelSuggestions"\s*:/,
|
|
75
|
+
/\{"summary"\s*:/,
|
|
76
|
+
/\{"overview"\s*:/,
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
for (const anchor of anchors) {
|
|
80
|
+
const match = response.match(anchor);
|
|
81
|
+
if (match) {
|
|
82
|
+
const startIdx = match.index;
|
|
83
|
+
// Find the matching closing brace from the end
|
|
84
|
+
const lastBrace = response.lastIndexOf('}');
|
|
85
|
+
if (lastBrace > startIdx) {
|
|
86
|
+
const candidate = response.substring(startIdx, lastBrace + 1);
|
|
87
|
+
return JSON.parse(candidate);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
throw new Error('No known JSON anchor found');
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
// Strategy 5: Forward scan — try JSON.parse starting from each { in the text.
|
|
95
|
+
// Handles arbitrary preamble text with braces by trying every { as a potential
|
|
96
|
+
// JSON start. Stops at the first successful parse.
|
|
97
|
+
() => {
|
|
98
|
+
let searchFrom = 0;
|
|
99
|
+
// Limit attempts to avoid excessive parsing on very large non-JSON text
|
|
100
|
+
const maxAttempts = 20;
|
|
101
|
+
let attempts = 0;
|
|
102
|
+
const lastBrace = response.lastIndexOf('}');
|
|
103
|
+
|
|
104
|
+
while (searchFrom < response.length && attempts < maxAttempts) {
|
|
105
|
+
const braceIdx = response.indexOf('{', searchFrom);
|
|
106
|
+
if (braceIdx === -1) break;
|
|
107
|
+
|
|
108
|
+
attempts++;
|
|
109
|
+
try {
|
|
110
|
+
// Try parsing from this brace to the end of the response.
|
|
111
|
+
// JSON.parse is lenient about trailing content only if we trim to the
|
|
112
|
+
// right boundary, so use lastIndexOf('}') from the end.
|
|
113
|
+
if (lastBrace > braceIdx) {
|
|
114
|
+
const candidate = response.substring(braceIdx, lastBrace + 1);
|
|
115
|
+
const parsed = JSON.parse(candidate);
|
|
116
|
+
if (parsed && typeof parsed === 'object') {
|
|
117
|
+
return parsed;
|
|
66
118
|
}
|
|
67
119
|
}
|
|
120
|
+
} catch {
|
|
121
|
+
// This { wasn't the start of valid JSON, try the next one
|
|
68
122
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
123
|
+
searchFrom = braceIdx + 1;
|
|
124
|
+
}
|
|
125
|
+
throw new Error('Forward scan found no valid JSON');
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
// Strategy 6: Bracket-matched substring from the first {.
|
|
129
|
+
// Counts balanced braces (ignoring those inside JSON strings) to find
|
|
130
|
+
// the end of the first top-level object. No iteration cap — the loop
|
|
131
|
+
// runs for the full length of the matched region.
|
|
132
|
+
() => {
|
|
133
|
+
const firstBrace = response.indexOf('{');
|
|
134
|
+
if (firstBrace === -1) throw new Error('No opening brace found');
|
|
135
|
+
|
|
136
|
+
let braceCount = 0;
|
|
137
|
+
let inString = false;
|
|
138
|
+
let escaped = false;
|
|
139
|
+
|
|
140
|
+
for (let i = firstBrace; i < response.length; i++) {
|
|
141
|
+
const ch = response[i];
|
|
142
|
+
|
|
143
|
+
if (escaped) {
|
|
144
|
+
escaped = false;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (ch === '\\' && inString) {
|
|
149
|
+
escaped = true;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (ch === '"') {
|
|
154
|
+
inString = !inString;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (inString) continue;
|
|
159
|
+
|
|
160
|
+
if (ch === '{') braceCount++;
|
|
161
|
+
else if (ch === '}') {
|
|
162
|
+
braceCount--;
|
|
163
|
+
if (braceCount === 0) {
|
|
164
|
+
return JSON.parse(response.substring(firstBrace, i + 1));
|
|
165
|
+
}
|
|
72
166
|
}
|
|
73
167
|
}
|
|
74
168
|
throw new Error('No balanced JSON structure found');
|
|
75
169
|
},
|
|
76
|
-
|
|
77
|
-
// Strategy 4: Try the entire response as JSON (for simple cases)
|
|
78
|
-
() => {
|
|
79
|
-
return JSON.parse(response.trim());
|
|
80
|
-
}
|
|
81
170
|
];
|
|
82
171
|
|
|
172
|
+
const strategyErrors = [];
|
|
83
173
|
for (let i = 0; i < strategies.length; i++) {
|
|
84
174
|
try {
|
|
85
175
|
const data = strategies[i]();
|
|
@@ -88,17 +178,17 @@ function extractJSON(response, level = 'unknown') {
|
|
|
88
178
|
return { success: true, data };
|
|
89
179
|
}
|
|
90
180
|
} catch (error) {
|
|
91
|
-
|
|
92
|
-
if (i === strategies.length - 1) {
|
|
93
|
-
// Last strategy failed, log the error
|
|
94
|
-
logger.warn(`${levelPrefix} All JSON extraction strategies failed`);
|
|
95
|
-
logger.warn(`${levelPrefix} Response preview: ${response.substring(0, 200)}...`);
|
|
96
|
-
}
|
|
181
|
+
strategyErrors.push(`S${i + 1}: ${error.message}`);
|
|
97
182
|
}
|
|
98
183
|
}
|
|
99
184
|
|
|
100
|
-
|
|
101
|
-
|
|
185
|
+
// All strategies failed — log details for debugging
|
|
186
|
+
logger.warn(`${levelPrefix} All JSON extraction strategies failed`);
|
|
187
|
+
logger.warn(`${levelPrefix} Strategy errors: ${strategyErrors.join('; ')}`);
|
|
188
|
+
logger.warn(`${levelPrefix} Response length: ${response.length} chars, preview: ${response.substring(0, 200)}...`);
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
success: false,
|
|
102
192
|
error: 'Failed to extract JSON from response',
|
|
103
193
|
response: response.substring(0, 500) // Include preview for debugging
|
|
104
194
|
};
|