@simonren/quorum 0.7.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/LICENSE +21 -0
- package/README.md +144 -0
- package/commands/multi-consult.md +109 -0
- package/commands/multi-review.md +139 -0
- package/dist/adapters/base.d.ts +120 -0
- package/dist/adapters/base.js +98 -0
- package/dist/adapters/claude.d.ts +25 -0
- package/dist/adapters/claude.js +217 -0
- package/dist/adapters/codex.d.ts +20 -0
- package/dist/adapters/codex.js +227 -0
- package/dist/adapters/gemini.d.ts +20 -0
- package/dist/adapters/gemini.js +197 -0
- package/dist/adapters/index.d.ts +12 -0
- package/dist/adapters/index.js +15 -0
- package/dist/cli/check.d.ts +20 -0
- package/dist/cli/check.js +78 -0
- package/dist/cli/codex.d.ts +11 -0
- package/dist/cli/codex.js +255 -0
- package/dist/cli/gemini.d.ts +12 -0
- package/dist/cli/gemini.js +253 -0
- package/dist/commands.d.ts +28 -0
- package/dist/commands.js +105 -0
- package/dist/config.d.ts +244 -0
- package/dist/config.js +179 -0
- package/dist/consult-prompt.d.ts +10 -0
- package/dist/consult-prompt.js +72 -0
- package/dist/context.d.ts +1538 -0
- package/dist/context.js +383 -0
- package/dist/decoders/claude.d.ts +53 -0
- package/dist/decoders/claude.js +106 -0
- package/dist/decoders/codex.d.ts +71 -0
- package/dist/decoders/codex.js +145 -0
- package/dist/decoders/gemini.d.ts +33 -0
- package/dist/decoders/gemini.js +58 -0
- package/dist/decoders/index.d.ts +6 -0
- package/dist/decoders/index.js +3 -0
- package/dist/errors.d.ts +46 -0
- package/dist/errors.js +192 -0
- package/dist/executor.d.ts +103 -0
- package/dist/executor.js +244 -0
- package/dist/handoff.d.ts +270 -0
- package/dist/handoff.js +599 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +134 -0
- package/dist/pipeline.d.ts +135 -0
- package/dist/pipeline.js +462 -0
- package/dist/prompt-v2.d.ts +38 -0
- package/dist/prompt-v2.js +391 -0
- package/dist/prompt.d.ts +71 -0
- package/dist/prompt.js +309 -0
- package/dist/schema.d.ts +660 -0
- package/dist/schema.js +536 -0
- package/dist/tools/consult.d.ts +104 -0
- package/dist/tools/consult.js +220 -0
- package/dist/tools/feedback.d.ts +91 -0
- package/dist/tools/feedback.js +117 -0
- package/dist/types.d.ts +105 -0
- package/dist/types.js +31 -0
- package/package.json +54 -0
package/dist/context.js
ADDED
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rich Context Protocol for Review Handoff
|
|
3
|
+
*
|
|
4
|
+
* Defines the structured information that should flow from CC to reviewers.
|
|
5
|
+
* This replaces the simple "ccOutput: string" with a rich, queryable context.
|
|
6
|
+
*/
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
// =============================================================================
|
|
9
|
+
// FILE CHANGE CONTEXT
|
|
10
|
+
// =============================================================================
|
|
11
|
+
/**
|
|
12
|
+
* Represents a change to a single file with semantic understanding
|
|
13
|
+
*/
|
|
14
|
+
export const FileChangeSchema = z.object({
|
|
15
|
+
path: z.string().describe('Relative path from working directory'),
|
|
16
|
+
language: z.string().optional().describe('Programming language'),
|
|
17
|
+
changeType: z.enum(['created', 'modified', 'deleted', 'renamed']),
|
|
18
|
+
// The actual changes
|
|
19
|
+
diff: z.string().optional().describe('Unified diff format'),
|
|
20
|
+
linesAdded: z.number().int().nonnegative().optional(),
|
|
21
|
+
linesRemoved: z.number().int().nonnegative().optional(),
|
|
22
|
+
// For small/new files, include full content
|
|
23
|
+
content: z.string().optional().describe('Full file content (for new/small files)'),
|
|
24
|
+
// Semantic understanding
|
|
25
|
+
changedSymbols: z.array(z.object({
|
|
26
|
+
name: z.string(),
|
|
27
|
+
type: z.enum(['function', 'class', 'variable', 'type', 'import', 'export', 'other']),
|
|
28
|
+
lineStart: z.number().int().positive().optional(),
|
|
29
|
+
lineEnd: z.number().int().positive().optional(),
|
|
30
|
+
})).optional().describe('Symbols that were modified'),
|
|
31
|
+
// Relationships
|
|
32
|
+
imports: z.array(z.string()).optional().describe('Modules this file imports'),
|
|
33
|
+
importedBy: z.array(z.string()).optional().describe('Files that import this module'),
|
|
34
|
+
testFile: z.string().optional().describe('Related test file path'),
|
|
35
|
+
});
|
|
36
|
+
// =============================================================================
|
|
37
|
+
// EXECUTION CONTEXT
|
|
38
|
+
// =============================================================================
|
|
39
|
+
/**
|
|
40
|
+
* Results from running tests, build, lint, etc.
|
|
41
|
+
*/
|
|
42
|
+
export const ExecutionContextSchema = z.object({
|
|
43
|
+
// Test results
|
|
44
|
+
tests: z.object({
|
|
45
|
+
ran: z.boolean(),
|
|
46
|
+
passed: z.number().int().nonnegative().optional(),
|
|
47
|
+
failed: z.number().int().nonnegative().optional(),
|
|
48
|
+
skipped: z.number().int().nonnegative().optional(),
|
|
49
|
+
failures: z.array(z.object({
|
|
50
|
+
testName: z.string(),
|
|
51
|
+
file: z.string().optional(),
|
|
52
|
+
error: z.string(),
|
|
53
|
+
})).optional(),
|
|
54
|
+
}).optional(),
|
|
55
|
+
// Build status
|
|
56
|
+
build: z.object({
|
|
57
|
+
ran: z.boolean(),
|
|
58
|
+
success: z.boolean().optional(),
|
|
59
|
+
errors: z.array(z.object({
|
|
60
|
+
file: z.string(),
|
|
61
|
+
line: z.number().int().positive().optional(),
|
|
62
|
+
message: z.string(),
|
|
63
|
+
})).optional(),
|
|
64
|
+
warnings: z.array(z.object({
|
|
65
|
+
file: z.string(),
|
|
66
|
+
line: z.number().int().positive().optional(),
|
|
67
|
+
message: z.string(),
|
|
68
|
+
})).optional(),
|
|
69
|
+
}).optional(),
|
|
70
|
+
// Type checking
|
|
71
|
+
typeCheck: z.object({
|
|
72
|
+
ran: z.boolean(),
|
|
73
|
+
errors: z.array(z.object({
|
|
74
|
+
file: z.string(),
|
|
75
|
+
line: z.number().int().positive().optional(),
|
|
76
|
+
message: z.string(),
|
|
77
|
+
code: z.string().optional(), // e.g., "TS2345"
|
|
78
|
+
})).optional(),
|
|
79
|
+
}).optional(),
|
|
80
|
+
// Linting
|
|
81
|
+
lint: z.object({
|
|
82
|
+
ran: z.boolean(),
|
|
83
|
+
issues: z.array(z.object({
|
|
84
|
+
file: z.string(),
|
|
85
|
+
line: z.number().int().positive().optional(),
|
|
86
|
+
rule: z.string().optional(),
|
|
87
|
+
severity: z.enum(['error', 'warning', 'info']),
|
|
88
|
+
message: z.string(),
|
|
89
|
+
})).optional(),
|
|
90
|
+
}).optional(),
|
|
91
|
+
});
|
|
92
|
+
// =============================================================================
|
|
93
|
+
// GIT CONTEXT
|
|
94
|
+
// =============================================================================
|
|
95
|
+
export const GitContextSchema = z.object({
|
|
96
|
+
branch: z.string().optional(),
|
|
97
|
+
baseBranch: z.string().optional().describe('Branch this was based on (e.g., main)'),
|
|
98
|
+
// Recent commits by CC
|
|
99
|
+
commits: z.array(z.object({
|
|
100
|
+
hash: z.string(),
|
|
101
|
+
message: z.string(),
|
|
102
|
+
filesChanged: z.array(z.string()),
|
|
103
|
+
})).optional(),
|
|
104
|
+
// If this is for a PR
|
|
105
|
+
pullRequest: z.object({
|
|
106
|
+
title: z.string().optional(),
|
|
107
|
+
description: z.string().optional(),
|
|
108
|
+
targetBranch: z.string().optional(),
|
|
109
|
+
}).optional(),
|
|
110
|
+
// Uncommitted changes
|
|
111
|
+
uncommittedChanges: z.boolean().optional(),
|
|
112
|
+
});
|
|
113
|
+
// =============================================================================
|
|
114
|
+
// CC'S ANALYSIS & DECISIONS
|
|
115
|
+
// =============================================================================
|
|
116
|
+
export const CCAnalysisSchema = z.object({
|
|
117
|
+
// What CC was asked to do
|
|
118
|
+
originalRequest: z.string().describe("User's original request/task"),
|
|
119
|
+
taskType: z.enum(['feature', 'bugfix', 'refactor', 'security-fix', 'performance', 'review', 'other']).optional(),
|
|
120
|
+
// What CC did
|
|
121
|
+
summary: z.string().describe('Brief summary of changes made'),
|
|
122
|
+
// CC's findings (if reviewing/analyzing)
|
|
123
|
+
findings: z.array(z.object({
|
|
124
|
+
category: z.string(),
|
|
125
|
+
description: z.string(),
|
|
126
|
+
location: z.string().optional(),
|
|
127
|
+
confidence: z.number().min(0).max(1).optional(),
|
|
128
|
+
addressed: z.boolean().optional().describe('Whether CC already fixed this'),
|
|
129
|
+
})).optional(),
|
|
130
|
+
// What CC is uncertain about
|
|
131
|
+
uncertainties: z.array(z.object({
|
|
132
|
+
topic: z.string(),
|
|
133
|
+
question: z.string(),
|
|
134
|
+
ccBestGuess: z.string().optional().describe("CC's current assumption"),
|
|
135
|
+
})).optional().describe('Things CC is unsure about - reviewer should verify'),
|
|
136
|
+
// Assumptions CC made
|
|
137
|
+
assumptions: z.array(z.string()).optional(),
|
|
138
|
+
// Decisions CC made and why
|
|
139
|
+
decisions: z.array(z.object({
|
|
140
|
+
decision: z.string(),
|
|
141
|
+
rationale: z.string(),
|
|
142
|
+
alternatives: z.array(z.string()).optional(),
|
|
143
|
+
})).optional(),
|
|
144
|
+
// Overall confidence
|
|
145
|
+
confidence: z.number().min(0).max(1).optional().describe("CC's overall confidence in the work"),
|
|
146
|
+
});
|
|
147
|
+
// =============================================================================
|
|
148
|
+
// REVIEW SCOPE & GUIDANCE
|
|
149
|
+
// =============================================================================
|
|
150
|
+
export const ReviewScopeSchema = z.object({
|
|
151
|
+
// What MUST be reviewed (critical paths)
|
|
152
|
+
mustReview: z.array(z.object({
|
|
153
|
+
path: z.string(),
|
|
154
|
+
reason: z.string(),
|
|
155
|
+
specificConcerns: z.array(z.string()).optional(),
|
|
156
|
+
})).optional(),
|
|
157
|
+
// What SHOULD be reviewed (important but not critical)
|
|
158
|
+
shouldReview: z.array(z.object({
|
|
159
|
+
path: z.string(),
|
|
160
|
+
reason: z.string(),
|
|
161
|
+
})).optional(),
|
|
162
|
+
// What MAY be reviewed (nice to have)
|
|
163
|
+
mayReview: z.array(z.string()).optional(),
|
|
164
|
+
// What to SKIP (already validated, unchanged, etc.)
|
|
165
|
+
skipReview: z.array(z.object({
|
|
166
|
+
path: z.string(),
|
|
167
|
+
reason: z.string(),
|
|
168
|
+
})).optional(),
|
|
169
|
+
// Specific questions CC wants answered
|
|
170
|
+
questions: z.array(z.object({
|
|
171
|
+
question: z.string(),
|
|
172
|
+
context: z.string().optional(),
|
|
173
|
+
relevantFiles: z.array(z.string()).optional(),
|
|
174
|
+
ccAnswer: z.string().optional().describe("What CC thinks - for comparison"),
|
|
175
|
+
})).optional(),
|
|
176
|
+
});
|
|
177
|
+
// =============================================================================
|
|
178
|
+
// FULL REVIEW CONTEXT
|
|
179
|
+
// =============================================================================
|
|
180
|
+
/**
|
|
181
|
+
* Complete context for a review request.
|
|
182
|
+
* This is what should be passed from CC to reviewers.
|
|
183
|
+
*/
|
|
184
|
+
export const ReviewContextSchema = z.object({
|
|
185
|
+
// Metadata
|
|
186
|
+
timestamp: z.string().datetime().optional(),
|
|
187
|
+
workingDir: z.string(),
|
|
188
|
+
// Code changes
|
|
189
|
+
changes: z.object({
|
|
190
|
+
files: z.array(FileChangeSchema),
|
|
191
|
+
totalLinesAdded: z.number().int().nonnegative().optional(),
|
|
192
|
+
totalLinesRemoved: z.number().int().nonnegative().optional(),
|
|
193
|
+
impactedModules: z.array(z.string()).optional(),
|
|
194
|
+
}),
|
|
195
|
+
// CC's work
|
|
196
|
+
analysis: CCAnalysisSchema,
|
|
197
|
+
// Execution results
|
|
198
|
+
execution: ExecutionContextSchema.optional(),
|
|
199
|
+
// Git info
|
|
200
|
+
git: GitContextSchema.optional(),
|
|
201
|
+
// Review guidance
|
|
202
|
+
scope: ReviewScopeSchema.optional(),
|
|
203
|
+
// Focus areas
|
|
204
|
+
focusAreas: z.array(z.string()).optional(),
|
|
205
|
+
// Custom instructions
|
|
206
|
+
customInstructions: z.string().optional(),
|
|
207
|
+
});
|
|
208
|
+
// =============================================================================
|
|
209
|
+
// CONTEXT BUILDERS
|
|
210
|
+
// =============================================================================
|
|
211
|
+
/**
|
|
212
|
+
* Build a minimal context from legacy inputs
|
|
213
|
+
*/
|
|
214
|
+
export function buildMinimalContext(workingDir, ccOutput, analyzedFiles, focusAreas, customPrompt) {
|
|
215
|
+
return {
|
|
216
|
+
workingDir,
|
|
217
|
+
changes: {
|
|
218
|
+
files: (analyzedFiles || []).map(path => ({
|
|
219
|
+
path,
|
|
220
|
+
changeType: 'modified',
|
|
221
|
+
})),
|
|
222
|
+
},
|
|
223
|
+
analysis: {
|
|
224
|
+
originalRequest: 'Not specified',
|
|
225
|
+
summary: ccOutput,
|
|
226
|
+
},
|
|
227
|
+
focusAreas,
|
|
228
|
+
customInstructions: customPrompt,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Build context from git diff
|
|
233
|
+
*/
|
|
234
|
+
export async function buildContextFromGitDiff(workingDir, baseBranch = 'main') {
|
|
235
|
+
// This would shell out to git to get actual diff info
|
|
236
|
+
// For now, return a placeholder
|
|
237
|
+
return {
|
|
238
|
+
workingDir,
|
|
239
|
+
git: {
|
|
240
|
+
baseBranch,
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Optimize context to fit within token limits while preserving important info
|
|
246
|
+
*/
|
|
247
|
+
export function optimizeContext(context, options) {
|
|
248
|
+
const optimized = { ...context };
|
|
249
|
+
// Prioritize files based on focus areas
|
|
250
|
+
if (options.focusAreas && options.focusAreas.length > 0) {
|
|
251
|
+
const priorityPatterns = getPriorityPatterns(options.focusAreas);
|
|
252
|
+
optimized.changes.files = optimized.changes.files.sort((a, b) => {
|
|
253
|
+
const aPriority = getPriority(a.path, priorityPatterns);
|
|
254
|
+
const bPriority = getPriority(b.path, priorityPatterns);
|
|
255
|
+
return bPriority - aPriority;
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
// Truncate diffs if too large
|
|
259
|
+
if (!options.includeDiffs) {
|
|
260
|
+
optimized.changes.files = optimized.changes.files.map(f => ({
|
|
261
|
+
...f,
|
|
262
|
+
diff: undefined,
|
|
263
|
+
}));
|
|
264
|
+
}
|
|
265
|
+
// Remove full content if not needed
|
|
266
|
+
if (!options.includeFullContent) {
|
|
267
|
+
optimized.changes.files = optimized.changes.files.map(f => ({
|
|
268
|
+
...f,
|
|
269
|
+
content: undefined,
|
|
270
|
+
}));
|
|
271
|
+
}
|
|
272
|
+
return optimized;
|
|
273
|
+
}
|
|
274
|
+
function getPriorityPatterns(focusAreas) {
|
|
275
|
+
const patterns = [];
|
|
276
|
+
if (focusAreas.includes('security')) {
|
|
277
|
+
patterns.push(/auth/i, /login/i, /password/i, /crypto/i, /token/i, /api/i);
|
|
278
|
+
}
|
|
279
|
+
if (focusAreas.includes('performance')) {
|
|
280
|
+
patterns.push(/database/i, /query/i, /cache/i, /service/i);
|
|
281
|
+
}
|
|
282
|
+
if (focusAreas.includes('testing')) {
|
|
283
|
+
patterns.push(/test/i, /spec/i, /mock/i);
|
|
284
|
+
}
|
|
285
|
+
return patterns;
|
|
286
|
+
}
|
|
287
|
+
function getPriority(path, patterns) {
|
|
288
|
+
return patterns.filter(p => p.test(path)).length;
|
|
289
|
+
}
|
|
290
|
+
// =============================================================================
|
|
291
|
+
// CONTEXT SERIALIZATION FOR PROMPTS
|
|
292
|
+
// =============================================================================
|
|
293
|
+
/**
|
|
294
|
+
* Convert context to a string suitable for inclusion in prompts
|
|
295
|
+
*/
|
|
296
|
+
export function contextToPromptString(context) {
|
|
297
|
+
const sections = [];
|
|
298
|
+
// Section 1: Task Overview
|
|
299
|
+
sections.push(`## Task Overview
|
|
300
|
+
**Original Request:** ${context.analysis.originalRequest}
|
|
301
|
+
**Summary:** ${context.analysis.summary}
|
|
302
|
+
${context.analysis.taskType ? `**Task Type:** ${context.analysis.taskType}` : ''}
|
|
303
|
+
${context.analysis.confidence !== undefined ? `**CC Confidence:** ${Math.round(context.analysis.confidence * 100)}%` : ''}`);
|
|
304
|
+
// Section 2: Files Changed
|
|
305
|
+
if (context.changes.files.length > 0) {
|
|
306
|
+
sections.push(`\n## Files Changed (${context.changes.files.length})`);
|
|
307
|
+
for (const file of context.changes.files) {
|
|
308
|
+
let fileInfo = `\n### ${file.path} [${file.changeType}]`;
|
|
309
|
+
if (file.changedSymbols && file.changedSymbols.length > 0) {
|
|
310
|
+
fileInfo += `\nModified: ${file.changedSymbols.map(s => `${s.name} (${s.type})`).join(', ')}`;
|
|
311
|
+
}
|
|
312
|
+
if (file.linesAdded !== undefined || file.linesRemoved !== undefined) {
|
|
313
|
+
fileInfo += `\nLines: +${file.linesAdded || 0} / -${file.linesRemoved || 0}`;
|
|
314
|
+
}
|
|
315
|
+
if (file.diff) {
|
|
316
|
+
fileInfo += `\n\`\`\`diff\n${file.diff}\n\`\`\``;
|
|
317
|
+
}
|
|
318
|
+
sections.push(fileInfo);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
// Section 3: CC's Uncertainties (IMPORTANT for reviewer)
|
|
322
|
+
if (context.analysis.uncertainties && context.analysis.uncertainties.length > 0) {
|
|
323
|
+
sections.push(`\n## CC's Uncertainties (Please Verify)`);
|
|
324
|
+
for (const u of context.analysis.uncertainties) {
|
|
325
|
+
sections.push(`\n**${u.topic}**
|
|
326
|
+
Question: ${u.question}
|
|
327
|
+
${u.ccBestGuess ? `CC's guess: ${u.ccBestGuess}` : ''}`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
// Section 4: Questions for Reviewer
|
|
331
|
+
if (context.scope?.questions && context.scope.questions.length > 0) {
|
|
332
|
+
sections.push(`\n## Specific Questions`);
|
|
333
|
+
for (const q of context.scope.questions) {
|
|
334
|
+
sections.push(`\n- ${q.question}${q.ccAnswer ? ` (CC thinks: ${q.ccAnswer})` : ''}`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
// Section 5: Execution Results
|
|
338
|
+
if (context.execution) {
|
|
339
|
+
const exec = context.execution;
|
|
340
|
+
const execLines = [];
|
|
341
|
+
if (exec.tests?.ran) {
|
|
342
|
+
const t = exec.tests;
|
|
343
|
+
execLines.push(`Tests: ${t.passed || 0} passed, ${t.failed || 0} failed, ${t.skipped || 0} skipped`);
|
|
344
|
+
if (t.failures && t.failures.length > 0) {
|
|
345
|
+
for (const f of t.failures.slice(0, 3)) {
|
|
346
|
+
execLines.push(` ❌ ${f.testName}: ${f.error.slice(0, 100)}`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
if (exec.build?.ran) {
|
|
351
|
+
execLines.push(`Build: ${exec.build.success ? '✓ Success' : '❌ Failed'}`);
|
|
352
|
+
}
|
|
353
|
+
if (exec.typeCheck?.ran && exec.typeCheck.errors && exec.typeCheck.errors.length > 0) {
|
|
354
|
+
execLines.push(`Type Errors: ${exec.typeCheck.errors.length}`);
|
|
355
|
+
}
|
|
356
|
+
if (execLines.length > 0) {
|
|
357
|
+
sections.push(`\n## Execution Results\n${execLines.join('\n')}`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
// Section 6: Review Priorities
|
|
361
|
+
if (context.scope?.mustReview && context.scope.mustReview.length > 0) {
|
|
362
|
+
sections.push(`\n## Priority Review Areas`);
|
|
363
|
+
for (const r of context.scope.mustReview) {
|
|
364
|
+
sections.push(`- **${r.path}**: ${r.reason}`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return sections.join('\n');
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Check if a file:line reference is valid
|
|
371
|
+
*/
|
|
372
|
+
export function verifyFileLineReference(reference, verification) {
|
|
373
|
+
if (!verification.existingFiles.has(reference.file)) {
|
|
374
|
+
return { valid: false, reason: `File does not exist: ${reference.file}` };
|
|
375
|
+
}
|
|
376
|
+
if (reference.line !== undefined) {
|
|
377
|
+
const lineCount = verification.fileLineCounts.get(reference.file);
|
|
378
|
+
if (lineCount && reference.line > lineCount) {
|
|
379
|
+
return { valid: false, reason: `Line ${reference.line} exceeds file length (${lineCount} lines)` };
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return { valid: true };
|
|
383
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClaudeEventDecoder — Parses Claude CLI stream-json JSONL events.
|
|
3
|
+
*
|
|
4
|
+
* Event stream format (with --output-format stream-json --verbose):
|
|
5
|
+
* {"type":"system","subtype":"init",...}
|
|
6
|
+
* {"type":"assistant","message":{"content":[{"type":"text","text":"..."}],...},...}
|
|
7
|
+
* {"type":"result","subtype":"success","result":"...","duration_ms":...,"usage":{...}}
|
|
8
|
+
*/
|
|
9
|
+
export interface ClaudeEvent {
|
|
10
|
+
type: string;
|
|
11
|
+
subtype?: string;
|
|
12
|
+
session_id?: string;
|
|
13
|
+
model?: string;
|
|
14
|
+
result?: string;
|
|
15
|
+
is_error?: boolean;
|
|
16
|
+
duration_ms?: number;
|
|
17
|
+
message?: {
|
|
18
|
+
content?: Array<{
|
|
19
|
+
type: string;
|
|
20
|
+
text?: string;
|
|
21
|
+
}>;
|
|
22
|
+
usage?: {
|
|
23
|
+
input_tokens: number;
|
|
24
|
+
output_tokens: number;
|
|
25
|
+
cache_read_input_tokens?: number;
|
|
26
|
+
cache_creation_input_tokens?: number;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
usage?: {
|
|
30
|
+
input_tokens: number;
|
|
31
|
+
output_tokens: number;
|
|
32
|
+
cache_read_input_tokens?: number;
|
|
33
|
+
cache_creation_input_tokens?: number;
|
|
34
|
+
};
|
|
35
|
+
tool_use_id?: string;
|
|
36
|
+
tool_name?: string;
|
|
37
|
+
}
|
|
38
|
+
export declare class ClaudeEventDecoder {
|
|
39
|
+
onProgress?: (eventType: string, detail?: string) => void;
|
|
40
|
+
private _finalResponse;
|
|
41
|
+
private _usage;
|
|
42
|
+
private _error;
|
|
43
|
+
private _eventCount;
|
|
44
|
+
private _durationMs;
|
|
45
|
+
processLine(line: string): void;
|
|
46
|
+
getFinalResponse(): string | null;
|
|
47
|
+
getUsage(): ClaudeEvent['usage'] | null;
|
|
48
|
+
getError(): string | null;
|
|
49
|
+
getDurationMs(): number | null;
|
|
50
|
+
hasNoOutput(): boolean;
|
|
51
|
+
private _handleEvent;
|
|
52
|
+
private _describeEvent;
|
|
53
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClaudeEventDecoder — Parses Claude CLI stream-json JSONL events.
|
|
3
|
+
*
|
|
4
|
+
* Event stream format (with --output-format stream-json --verbose):
|
|
5
|
+
* {"type":"system","subtype":"init",...}
|
|
6
|
+
* {"type":"assistant","message":{"content":[{"type":"text","text":"..."}],...},...}
|
|
7
|
+
* {"type":"result","subtype":"success","result":"...","duration_ms":...,"usage":{...}}
|
|
8
|
+
*/
|
|
9
|
+
// =============================================================================
|
|
10
|
+
// DECODER
|
|
11
|
+
// =============================================================================
|
|
12
|
+
export class ClaudeEventDecoder {
|
|
13
|
+
onProgress;
|
|
14
|
+
_finalResponse = null;
|
|
15
|
+
_usage = null;
|
|
16
|
+
_error = null;
|
|
17
|
+
_eventCount = 0;
|
|
18
|
+
_durationMs = null;
|
|
19
|
+
// =============================================================================
|
|
20
|
+
// PUBLIC API
|
|
21
|
+
// =============================================================================
|
|
22
|
+
processLine(line) {
|
|
23
|
+
const trimmed = line.trim();
|
|
24
|
+
if (trimmed.length === 0)
|
|
25
|
+
return;
|
|
26
|
+
let event;
|
|
27
|
+
try {
|
|
28
|
+
const parsed = JSON.parse(trimmed);
|
|
29
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
|
|
30
|
+
return;
|
|
31
|
+
event = parsed;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (!event.type)
|
|
37
|
+
return;
|
|
38
|
+
this._handleEvent(event);
|
|
39
|
+
}
|
|
40
|
+
getFinalResponse() {
|
|
41
|
+
return this._finalResponse;
|
|
42
|
+
}
|
|
43
|
+
getUsage() {
|
|
44
|
+
return this._usage;
|
|
45
|
+
}
|
|
46
|
+
getError() {
|
|
47
|
+
return this._error;
|
|
48
|
+
}
|
|
49
|
+
getDurationMs() {
|
|
50
|
+
return this._durationMs;
|
|
51
|
+
}
|
|
52
|
+
hasNoOutput() {
|
|
53
|
+
return this._eventCount > 0 && this._finalResponse === null;
|
|
54
|
+
}
|
|
55
|
+
// =============================================================================
|
|
56
|
+
// PRIVATE HELPERS
|
|
57
|
+
// =============================================================================
|
|
58
|
+
_handleEvent(event) {
|
|
59
|
+
this._eventCount++;
|
|
60
|
+
switch (event.type) {
|
|
61
|
+
case 'result':
|
|
62
|
+
// The result event contains the final text response
|
|
63
|
+
if (event.subtype === 'success' && typeof event.result === 'string') {
|
|
64
|
+
this._finalResponse = event.result;
|
|
65
|
+
}
|
|
66
|
+
if (event.is_error) {
|
|
67
|
+
this._error = event.result || 'Claude review failed';
|
|
68
|
+
}
|
|
69
|
+
if (event.usage) {
|
|
70
|
+
this._usage = event.usage;
|
|
71
|
+
}
|
|
72
|
+
if (event.duration_ms != null) {
|
|
73
|
+
this._durationMs = event.duration_ms;
|
|
74
|
+
}
|
|
75
|
+
break;
|
|
76
|
+
case 'assistant':
|
|
77
|
+
// Track usage from assistant messages
|
|
78
|
+
if (event.message?.usage) {
|
|
79
|
+
this._usage = event.message.usage;
|
|
80
|
+
}
|
|
81
|
+
break;
|
|
82
|
+
case 'error':
|
|
83
|
+
this._error = event.result || 'Unknown error from Claude CLI';
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
this.onProgress?.(event.type, this._describeEvent(event));
|
|
87
|
+
}
|
|
88
|
+
_describeEvent(event) {
|
|
89
|
+
switch (event.type) {
|
|
90
|
+
case 'system':
|
|
91
|
+
if (event.subtype === 'init')
|
|
92
|
+
return `model: ${event.model || 'opus'}`;
|
|
93
|
+
if (event.subtype)
|
|
94
|
+
return event.subtype;
|
|
95
|
+
return undefined;
|
|
96
|
+
case 'assistant':
|
|
97
|
+
return 'assistant message';
|
|
98
|
+
case 'tool_use':
|
|
99
|
+
return event.tool_name ? `tool: ${event.tool_name}` : 'tool use';
|
|
100
|
+
case 'result':
|
|
101
|
+
return `status: ${event.subtype || 'unknown'}`;
|
|
102
|
+
default:
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CodexEventDecoder
|
|
3
|
+
*
|
|
4
|
+
* Parses JSONL streaming events emitted by `codex exec --json` on stdout.
|
|
5
|
+
* Extracts the final agent_message text and usage statistics.
|
|
6
|
+
*
|
|
7
|
+
* Event stream format:
|
|
8
|
+
* {"type":"thread.started","thread_id":"..."}
|
|
9
|
+
* {"type":"turn.started"}
|
|
10
|
+
* {"type":"item.started","item":{...}}
|
|
11
|
+
* {"type":"item.completed","item":{...}}
|
|
12
|
+
* {"type":"turn.completed","usage":{...}}
|
|
13
|
+
*/
|
|
14
|
+
export interface CodexEvent {
|
|
15
|
+
type: string;
|
|
16
|
+
thread_id?: string;
|
|
17
|
+
item?: {
|
|
18
|
+
id: string;
|
|
19
|
+
type: string;
|
|
20
|
+
text?: string;
|
|
21
|
+
command?: string;
|
|
22
|
+
status?: string;
|
|
23
|
+
exit_code?: number;
|
|
24
|
+
message?: string;
|
|
25
|
+
};
|
|
26
|
+
usage?: {
|
|
27
|
+
input_tokens: number;
|
|
28
|
+
cached_input_tokens?: number;
|
|
29
|
+
output_tokens: number;
|
|
30
|
+
};
|
|
31
|
+
error?: {
|
|
32
|
+
message: string;
|
|
33
|
+
};
|
|
34
|
+
message?: string;
|
|
35
|
+
}
|
|
36
|
+
export declare class CodexEventDecoder {
|
|
37
|
+
/**
|
|
38
|
+
* Optional callback invoked for every successfully parsed event.
|
|
39
|
+
* @param eventType - The `type` field of the event (e.g. "item.completed").
|
|
40
|
+
* @param detail - A human-readable detail string for logging (may be undefined).
|
|
41
|
+
*/
|
|
42
|
+
onProgress?: (eventType: string, detail?: string) => void;
|
|
43
|
+
private _finalResponse;
|
|
44
|
+
private _usage;
|
|
45
|
+
private _error;
|
|
46
|
+
private _eventCount;
|
|
47
|
+
/**
|
|
48
|
+
* Parse a single JSONL line. Silently skips malformed or empty input.
|
|
49
|
+
*/
|
|
50
|
+
processLine(line: string): void;
|
|
51
|
+
/**
|
|
52
|
+
* Returns the text from the LAST `item.completed` event whose item type is
|
|
53
|
+
* `"agent_message"`, or `null` if no such event has been seen.
|
|
54
|
+
*/
|
|
55
|
+
getFinalResponse(): string | null;
|
|
56
|
+
/**
|
|
57
|
+
* Returns the usage stats from the most recent `turn.completed` event, or
|
|
58
|
+
* `null` if no such event has been seen.
|
|
59
|
+
*/
|
|
60
|
+
getUsage(): CodexEvent['usage'] | null;
|
|
61
|
+
/**
|
|
62
|
+
* Returns the error message from `error` or `turn.failed` events, or `null`.
|
|
63
|
+
*/
|
|
64
|
+
getError(): string | null;
|
|
65
|
+
/**
|
|
66
|
+
* Returns true if events were received but no agent_message was produced.
|
|
67
|
+
* Combined with a fast exit, this indicates rate limiting or instant rejection.
|
|
68
|
+
*/
|
|
69
|
+
hasNoOutput(): boolean;
|
|
70
|
+
private _handleEvent;
|
|
71
|
+
}
|