@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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +144 -0
  3. package/commands/multi-consult.md +109 -0
  4. package/commands/multi-review.md +139 -0
  5. package/dist/adapters/base.d.ts +120 -0
  6. package/dist/adapters/base.js +98 -0
  7. package/dist/adapters/claude.d.ts +25 -0
  8. package/dist/adapters/claude.js +217 -0
  9. package/dist/adapters/codex.d.ts +20 -0
  10. package/dist/adapters/codex.js +227 -0
  11. package/dist/adapters/gemini.d.ts +20 -0
  12. package/dist/adapters/gemini.js +197 -0
  13. package/dist/adapters/index.d.ts +12 -0
  14. package/dist/adapters/index.js +15 -0
  15. package/dist/cli/check.d.ts +20 -0
  16. package/dist/cli/check.js +78 -0
  17. package/dist/cli/codex.d.ts +11 -0
  18. package/dist/cli/codex.js +255 -0
  19. package/dist/cli/gemini.d.ts +12 -0
  20. package/dist/cli/gemini.js +253 -0
  21. package/dist/commands.d.ts +28 -0
  22. package/dist/commands.js +105 -0
  23. package/dist/config.d.ts +244 -0
  24. package/dist/config.js +179 -0
  25. package/dist/consult-prompt.d.ts +10 -0
  26. package/dist/consult-prompt.js +72 -0
  27. package/dist/context.d.ts +1538 -0
  28. package/dist/context.js +383 -0
  29. package/dist/decoders/claude.d.ts +53 -0
  30. package/dist/decoders/claude.js +106 -0
  31. package/dist/decoders/codex.d.ts +71 -0
  32. package/dist/decoders/codex.js +145 -0
  33. package/dist/decoders/gemini.d.ts +33 -0
  34. package/dist/decoders/gemini.js +58 -0
  35. package/dist/decoders/index.d.ts +6 -0
  36. package/dist/decoders/index.js +3 -0
  37. package/dist/errors.d.ts +46 -0
  38. package/dist/errors.js +192 -0
  39. package/dist/executor.d.ts +103 -0
  40. package/dist/executor.js +244 -0
  41. package/dist/handoff.d.ts +270 -0
  42. package/dist/handoff.js +599 -0
  43. package/dist/index.d.ts +18 -0
  44. package/dist/index.js +134 -0
  45. package/dist/pipeline.d.ts +135 -0
  46. package/dist/pipeline.js +462 -0
  47. package/dist/prompt-v2.d.ts +38 -0
  48. package/dist/prompt-v2.js +391 -0
  49. package/dist/prompt.d.ts +71 -0
  50. package/dist/prompt.js +309 -0
  51. package/dist/schema.d.ts +660 -0
  52. package/dist/schema.js +536 -0
  53. package/dist/tools/consult.d.ts +104 -0
  54. package/dist/tools/consult.js +220 -0
  55. package/dist/tools/feedback.d.ts +91 -0
  56. package/dist/tools/feedback.js +117 -0
  57. package/dist/types.d.ts +105 -0
  58. package/dist/types.js +31 -0
  59. package/package.json +54 -0
@@ -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
+ }