claude-all-hands 1.0.7 → 1.0.8
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/.claude/envoy/src/commands/{gemini.ts → oracle.ts} +251 -358
- package/.claude/envoy/src/lib/gemini-provider.ts +38 -0
- package/.claude/envoy/src/lib/index.ts +17 -1
- package/.claude/envoy/src/lib/observability.ts +12 -0
- package/.claude/envoy/src/lib/openai-provider.ts +90 -0
- package/.claude/envoy/src/lib/providers.ts +71 -0
- package/.claude/envoy/src/lib/retry.ts +11 -4
- package/.claude/settings.json +2 -1
- package/package.json +1 -1
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Oracle: Multi-provider LLM commands.
|
|
3
|
+
* Supports Gemini and OpenAI with unified interface.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { GoogleGenAI } from "@google/genai";
|
|
7
6
|
import { Command } from "commander";
|
|
8
|
-
import { readFileSync, existsSync
|
|
7
|
+
import { readFileSync, existsSync } from "fs";
|
|
9
8
|
import { join, extname } from "path";
|
|
10
9
|
import { spawnSync } from "child_process";
|
|
11
10
|
import { getBaseBranch, getBranch, getDiff, getPlanDir, isDirectModeBranch } from "../lib/git.js";
|
|
@@ -28,19 +27,19 @@ import {
|
|
|
28
27
|
planExists,
|
|
29
28
|
getPlanPaths,
|
|
30
29
|
getPromptId,
|
|
31
|
-
parsePromptId,
|
|
32
30
|
} from "../lib/index.js";
|
|
33
31
|
import { watchForDone } from "../lib/watcher.js";
|
|
34
|
-
import { withRetry,
|
|
35
|
-
import {
|
|
32
|
+
import { withRetry, ORACLE_FALLBACKS } from "../lib/retry.js";
|
|
33
|
+
import { recordOracleCall } from "../lib/observability.js";
|
|
34
|
+
import {
|
|
35
|
+
createProvider,
|
|
36
|
+
getDefaultProvider,
|
|
37
|
+
type LLMProvider,
|
|
38
|
+
type ProviderName,
|
|
39
|
+
type ContentPart,
|
|
40
|
+
} from "../lib/providers.js";
|
|
36
41
|
import { BaseCommand, type CommandResult } from "./base.js";
|
|
37
42
|
|
|
38
|
-
// Default model for most operations
|
|
39
|
-
const DEFAULT_MODEL = "gemini-2.0-flash";
|
|
40
|
-
// Pro model for complex audit/review operations
|
|
41
|
-
const PRO_MODEL = "gemini-3-pro-preview";
|
|
42
|
-
|
|
43
|
-
// Blocking gate timeout (12 hours default)
|
|
44
43
|
const DEFAULT_BLOCKING_GATE_TIMEOUT_MS = 12 * 60 * 60 * 1000;
|
|
45
44
|
|
|
46
45
|
function getBlockingGateTimeout(): number {
|
|
@@ -54,9 +53,6 @@ function getBlockingGateTimeout(): number {
|
|
|
54
53
|
return DEFAULT_BLOCKING_GATE_TIMEOUT_MS;
|
|
55
54
|
}
|
|
56
55
|
|
|
57
|
-
/**
|
|
58
|
-
* Parse JSON from a response that may contain markdown code blocks.
|
|
59
|
-
*/
|
|
60
56
|
function parseJsonResponse(response: string): Record<string, unknown> {
|
|
61
57
|
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
|
62
58
|
if (jsonMatch) {
|
|
@@ -69,9 +65,6 @@ function parseJsonResponse(response: string): Record<string, unknown> {
|
|
|
69
65
|
return { raw_response: response };
|
|
70
66
|
}
|
|
71
67
|
|
|
72
|
-
/**
|
|
73
|
-
* Read an image file and return base64 data with mime type.
|
|
74
|
-
*/
|
|
75
68
|
function readImageAsBase64(filePath: string): { data: string; mimeType: string } | null {
|
|
76
69
|
if (!existsSync(filePath)) return null;
|
|
77
70
|
|
|
@@ -95,46 +88,140 @@ function readImageAsBase64(filePath: string): { data: string; mimeType: string }
|
|
|
95
88
|
}
|
|
96
89
|
}
|
|
97
90
|
|
|
98
|
-
/**
|
|
99
|
-
* Get commit summaries for a branch since divergence from base.
|
|
100
|
-
*/
|
|
101
91
|
function getCommitSummaries(baseRef: string): string {
|
|
102
92
|
try {
|
|
103
|
-
const result = spawnSync(
|
|
104
|
-
"
|
|
105
|
-
|
|
106
|
-
{ encoding: "utf-8" }
|
|
107
|
-
);
|
|
93
|
+
const result = spawnSync("git", ["log", "--oneline", `${baseRef}..HEAD`], {
|
|
94
|
+
encoding: "utf-8",
|
|
95
|
+
});
|
|
108
96
|
return result.status === 0 ? result.stdout.trim() : "(No commits)";
|
|
109
97
|
} catch {
|
|
110
98
|
return "(Unable to get commits)";
|
|
111
99
|
}
|
|
112
100
|
}
|
|
113
101
|
|
|
102
|
+
function addProviderOption(cmd: Command): Command {
|
|
103
|
+
return cmd.option(
|
|
104
|
+
"--provider <provider>",
|
|
105
|
+
"LLM provider (gemini | openai)",
|
|
106
|
+
getDefaultProvider()
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ============================================================================
|
|
111
|
+
// Base Oracle Command
|
|
112
|
+
// ============================================================================
|
|
113
|
+
|
|
114
|
+
abstract class OracleCommand extends BaseCommand {
|
|
115
|
+
protected provider!: LLMProvider;
|
|
116
|
+
|
|
117
|
+
protected async initProvider(args: Record<string, unknown>): Promise<CommandResult | null> {
|
|
118
|
+
const providerName = (args.provider as ProviderName) ?? getDefaultProvider();
|
|
119
|
+
this.provider = await createProvider(providerName);
|
|
120
|
+
|
|
121
|
+
const apiKey = this.provider.getApiKey();
|
|
122
|
+
if (!apiKey) {
|
|
123
|
+
return this.error("auth_error", `${this.provider.config.apiKeyEnvVar} not set`);
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
protected async callProvider(
|
|
129
|
+
contents: string | ContentPart[],
|
|
130
|
+
endpoint: string,
|
|
131
|
+
usePro: boolean = false
|
|
132
|
+
): Promise<{ result: Awaited<ReturnType<typeof withRetry<{ text: string; model: string }>>>; durationMs: number }> {
|
|
133
|
+
const start = performance.now();
|
|
134
|
+
const result = await withRetry(
|
|
135
|
+
() => this.provider.generate(contents, { usePro }),
|
|
136
|
+
`oracle.${this.provider.config.name}.${endpoint}`,
|
|
137
|
+
{},
|
|
138
|
+
ORACLE_FALLBACKS[endpoint]
|
|
139
|
+
);
|
|
140
|
+
const durationMs = Math.round(performance.now() - start);
|
|
141
|
+
|
|
142
|
+
recordOracleCall({
|
|
143
|
+
provider: this.provider.config.name,
|
|
144
|
+
endpoint,
|
|
145
|
+
duration_ms: durationMs,
|
|
146
|
+
success: result.success,
|
|
147
|
+
retries: result.retries,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
return { result, durationMs };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
protected handleError(
|
|
154
|
+
result: { success: false; error: string; retries: number; fallback_suggestion?: string },
|
|
155
|
+
durationMs: number
|
|
156
|
+
): CommandResult {
|
|
157
|
+
return {
|
|
158
|
+
status: "error",
|
|
159
|
+
error: {
|
|
160
|
+
type: result.error,
|
|
161
|
+
message: "LLM API unavailable after retries",
|
|
162
|
+
suggestion: result.fallback_suggestion,
|
|
163
|
+
},
|
|
164
|
+
metadata: { retries: result.retries, duration_ms: durationMs },
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
protected async callAndParse(
|
|
169
|
+
endpoint: string,
|
|
170
|
+
contents: string | ContentPart[],
|
|
171
|
+
usePro: boolean = false
|
|
172
|
+
): Promise<
|
|
173
|
+
| { error: CommandResult; parsed?: never; metadata?: never }
|
|
174
|
+
| { error?: never; parsed: Record<string, unknown>; metadata: Record<string, unknown> }
|
|
175
|
+
> {
|
|
176
|
+
const { result, durationMs } = await this.callProvider(contents, endpoint, usePro);
|
|
177
|
+
|
|
178
|
+
if (!result.success) {
|
|
179
|
+
const failedResult = result as { success: false; error: string; retries: number; fallback_suggestion?: string };
|
|
180
|
+
return {
|
|
181
|
+
error: {
|
|
182
|
+
status: "error",
|
|
183
|
+
error: {
|
|
184
|
+
type: failedResult.error,
|
|
185
|
+
message: "LLM API unavailable after retries",
|
|
186
|
+
suggestion: failedResult.fallback_suggestion,
|
|
187
|
+
},
|
|
188
|
+
metadata: { retries: failedResult.retries, duration_ms: durationMs },
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const parsed = parseJsonResponse(result.data.text);
|
|
194
|
+
const metadata = {
|
|
195
|
+
provider: this.provider.config.name,
|
|
196
|
+
command: `oracle ${endpoint}`,
|
|
197
|
+
duration_ms: durationMs,
|
|
198
|
+
retries: result.retries,
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
return { parsed, metadata };
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
114
205
|
// ============================================================================
|
|
115
206
|
// Ask Command
|
|
116
207
|
// ============================================================================
|
|
117
208
|
|
|
118
|
-
class
|
|
209
|
+
class OracleAskCommand extends OracleCommand {
|
|
119
210
|
readonly name = "ask";
|
|
120
|
-
readonly description = "Raw
|
|
211
|
+
readonly description = "Raw LLM inference with retry";
|
|
121
212
|
|
|
122
213
|
defineArguments(cmd: Command): void {
|
|
123
|
-
cmd
|
|
124
|
-
.argument("<query>", "Query for
|
|
214
|
+
addProviderOption(cmd)
|
|
215
|
+
.argument("<query>", "Query for LLM")
|
|
125
216
|
.option("--files <files...>", "Files to include as context")
|
|
126
|
-
.option("--context <context>", "Additional context")
|
|
127
|
-
.option("--model <model>", "Model to use", DEFAULT_MODEL);
|
|
217
|
+
.option("--context <context>", "Additional context");
|
|
128
218
|
}
|
|
129
219
|
|
|
130
220
|
async execute(args: Record<string, unknown>): Promise<CommandResult> {
|
|
131
|
-
const
|
|
132
|
-
if (
|
|
133
|
-
return this.error("auth_error", "VERTEX_API_KEY not set");
|
|
134
|
-
}
|
|
221
|
+
const authError = await this.initProvider(args);
|
|
222
|
+
if (authError) return authError;
|
|
135
223
|
|
|
136
224
|
const query = args.query as string;
|
|
137
|
-
const model = (args.model as string) ?? DEFAULT_MODEL;
|
|
138
225
|
const files = args.files as string[] | undefined;
|
|
139
226
|
const context = args.context as string | undefined;
|
|
140
227
|
|
|
@@ -154,39 +241,22 @@ class GeminiAskCommand extends BaseCommand {
|
|
|
154
241
|
parts.push(query);
|
|
155
242
|
const prompt = parts.join("\n\n");
|
|
156
243
|
|
|
157
|
-
const
|
|
158
|
-
const result = await withRetry(
|
|
159
|
-
async () => {
|
|
160
|
-
const client = new GoogleGenAI({ vertexai: true, apiKey });
|
|
161
|
-
const genResult = await client.models.generateContent({
|
|
162
|
-
model,
|
|
163
|
-
contents: prompt,
|
|
164
|
-
});
|
|
165
|
-
return genResult.text ?? "";
|
|
166
|
-
},
|
|
167
|
-
"gemini.ask",
|
|
168
|
-
{},
|
|
169
|
-
GEMINI_FALLBACKS.ask
|
|
170
|
-
);
|
|
171
|
-
|
|
172
|
-
const durationMs = Math.round(performance.now() - start);
|
|
173
|
-
recordGeminiCall({ endpoint: "ask", duration_ms: durationMs, success: result.success, retries: result.retries });
|
|
244
|
+
const { result, durationMs } = await this.callProvider(prompt, "ask");
|
|
174
245
|
|
|
175
246
|
if (!result.success) {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
error: {
|
|
179
|
-
type: result.error,
|
|
180
|
-
message: "Gemini API unavailable after retries",
|
|
181
|
-
suggestion: result.fallback_suggestion,
|
|
182
|
-
},
|
|
183
|
-
metadata: { retries: result.retries, duration_ms: durationMs },
|
|
184
|
-
};
|
|
247
|
+
const failedResult = result as { success: false; error: string; retries: number; fallback_suggestion?: string };
|
|
248
|
+
return this.handleError(failedResult, durationMs);
|
|
185
249
|
}
|
|
186
250
|
|
|
187
251
|
return this.success(
|
|
188
|
-
{ content: result.data },
|
|
189
|
-
{
|
|
252
|
+
{ content: result.data.text },
|
|
253
|
+
{
|
|
254
|
+
model: result.data.model,
|
|
255
|
+
provider: this.provider.config.name,
|
|
256
|
+
command: "oracle ask",
|
|
257
|
+
duration_ms: durationMs,
|
|
258
|
+
retries: result.retries,
|
|
259
|
+
}
|
|
190
260
|
);
|
|
191
261
|
}
|
|
192
262
|
}
|
|
@@ -195,7 +265,7 @@ class GeminiAskCommand extends BaseCommand {
|
|
|
195
265
|
// Validate Command
|
|
196
266
|
// ============================================================================
|
|
197
267
|
|
|
198
|
-
class
|
|
268
|
+
class OracleValidateCommand extends OracleCommand {
|
|
199
269
|
readonly name = "validate";
|
|
200
270
|
readonly description = "Validate plan against requirements (anti-overengineering)";
|
|
201
271
|
|
|
@@ -228,16 +298,14 @@ Output JSON:
|
|
|
228
298
|
}`;
|
|
229
299
|
|
|
230
300
|
defineArguments(cmd: Command): void {
|
|
231
|
-
cmd
|
|
301
|
+
addProviderOption(cmd)
|
|
232
302
|
.option("--queries <path>", "Queries file path (optional)")
|
|
233
303
|
.option("--context <context>", "Additional context");
|
|
234
304
|
}
|
|
235
305
|
|
|
236
306
|
async execute(args: Record<string, unknown>): Promise<CommandResult> {
|
|
237
|
-
const
|
|
238
|
-
if (
|
|
239
|
-
return this.error("auth_error", "VERTEX_API_KEY not set");
|
|
240
|
-
}
|
|
307
|
+
const authError = await this.initProvider(args);
|
|
308
|
+
if (authError) return authError;
|
|
241
309
|
|
|
242
310
|
if (isDirectModeBranch(getBranch())) {
|
|
243
311
|
return this.error("direct_mode", "No plan in direct mode");
|
|
@@ -282,41 +350,10 @@ ${additional}
|
|
|
282
350
|
|
|
283
351
|
Respond with JSON only.`;
|
|
284
352
|
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
async () => {
|
|
288
|
-
const client = new GoogleGenAI({ vertexai: true, apiKey });
|
|
289
|
-
const genResult = await client.models.generateContent({
|
|
290
|
-
model: DEFAULT_MODEL,
|
|
291
|
-
contents: fullPrompt,
|
|
292
|
-
});
|
|
293
|
-
return genResult.text ?? "";
|
|
294
|
-
},
|
|
295
|
-
"gemini.validate",
|
|
296
|
-
{},
|
|
297
|
-
"Skip validation and proceed with user review only"
|
|
298
|
-
);
|
|
299
|
-
|
|
300
|
-
const durationMs = Math.round(performance.now() - start);
|
|
353
|
+
const { error, parsed, metadata } = await this.callAndParse("validate", fullPrompt);
|
|
354
|
+
if (error) return error;
|
|
301
355
|
|
|
302
|
-
|
|
303
|
-
return {
|
|
304
|
-
status: "error",
|
|
305
|
-
error: {
|
|
306
|
-
type: result.error,
|
|
307
|
-
message: "Gemini API unavailable after retries",
|
|
308
|
-
suggestion: result.fallback_suggestion,
|
|
309
|
-
},
|
|
310
|
-
metadata: { retries: result.retries, duration_ms: durationMs },
|
|
311
|
-
};
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
const parsed = parseJsonResponse(result.data);
|
|
315
|
-
return this.success(parsed, {
|
|
316
|
-
command: "gemini validate",
|
|
317
|
-
duration_ms: durationMs,
|
|
318
|
-
retries: result.retries,
|
|
319
|
-
});
|
|
356
|
+
return this.success(parsed, metadata);
|
|
320
357
|
}
|
|
321
358
|
}
|
|
322
359
|
|
|
@@ -324,7 +361,7 @@ Respond with JSON only.`;
|
|
|
324
361
|
// Architect Command
|
|
325
362
|
// ============================================================================
|
|
326
363
|
|
|
327
|
-
class
|
|
364
|
+
class OracleArchitectCommand extends OracleCommand {
|
|
328
365
|
readonly name = "architect";
|
|
329
366
|
readonly description = "Solutions architecture for complex features";
|
|
330
367
|
|
|
@@ -355,17 +392,15 @@ Output JSON:
|
|
|
355
392
|
}`;
|
|
356
393
|
|
|
357
394
|
defineArguments(cmd: Command): void {
|
|
358
|
-
cmd
|
|
395
|
+
addProviderOption(cmd)
|
|
359
396
|
.argument("<query>", "Feature/system description")
|
|
360
397
|
.option("--files <files...>", "Relevant code files")
|
|
361
398
|
.option("--context <context>", "Additional context or constraints");
|
|
362
399
|
}
|
|
363
400
|
|
|
364
401
|
async execute(args: Record<string, unknown>): Promise<CommandResult> {
|
|
365
|
-
const
|
|
366
|
-
if (
|
|
367
|
-
return this.error("auth_error", "VERTEX_API_KEY not set");
|
|
368
|
-
}
|
|
402
|
+
const authError = await this.initProvider(args);
|
|
403
|
+
if (authError) return authError;
|
|
369
404
|
|
|
370
405
|
const query = args.query as string;
|
|
371
406
|
const files = args.files as string[] | undefined;
|
|
@@ -394,49 +429,18 @@ ${additional}
|
|
|
394
429
|
|
|
395
430
|
Respond with JSON only.`;
|
|
396
431
|
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
-
async () => {
|
|
400
|
-
const client = new GoogleGenAI({ vertexai: true, apiKey });
|
|
401
|
-
const genResult = await client.models.generateContent({
|
|
402
|
-
model: DEFAULT_MODEL,
|
|
403
|
-
contents: fullPrompt,
|
|
404
|
-
});
|
|
405
|
-
return genResult.text ?? "";
|
|
406
|
-
},
|
|
407
|
-
"gemini.architect",
|
|
408
|
-
{},
|
|
409
|
-
"Proceed without architect analysis"
|
|
410
|
-
);
|
|
411
|
-
|
|
412
|
-
const durationMs = Math.round(performance.now() - start);
|
|
432
|
+
const { error, parsed, metadata } = await this.callAndParse("architect", fullPrompt);
|
|
433
|
+
if (error) return error;
|
|
413
434
|
|
|
414
|
-
|
|
415
|
-
return {
|
|
416
|
-
status: "error",
|
|
417
|
-
error: {
|
|
418
|
-
type: result.error,
|
|
419
|
-
message: "Gemini API unavailable after retries",
|
|
420
|
-
suggestion: result.fallback_suggestion,
|
|
421
|
-
},
|
|
422
|
-
metadata: { retries: result.retries, duration_ms: durationMs },
|
|
423
|
-
};
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
const parsed = parseJsonResponse(result.data);
|
|
427
|
-
return this.success(parsed, {
|
|
428
|
-
command: "gemini architect",
|
|
429
|
-
duration_ms: durationMs,
|
|
430
|
-
retries: result.retries,
|
|
431
|
-
});
|
|
435
|
+
return this.success(parsed, metadata);
|
|
432
436
|
}
|
|
433
437
|
}
|
|
434
438
|
|
|
435
439
|
// ============================================================================
|
|
436
|
-
// Audit Command
|
|
440
|
+
// Audit Command
|
|
437
441
|
// ============================================================================
|
|
438
442
|
|
|
439
|
-
class
|
|
443
|
+
class OracleAuditCommand extends OracleCommand {
|
|
440
444
|
readonly name = "audit";
|
|
441
445
|
readonly description = "Audit plan for completeness and coherence";
|
|
442
446
|
|
|
@@ -461,14 +465,12 @@ Output JSON:
|
|
|
461
465
|
}`;
|
|
462
466
|
|
|
463
467
|
defineArguments(cmd: Command): void {
|
|
464
|
-
|
|
468
|
+
addProviderOption(cmd);
|
|
465
469
|
}
|
|
466
470
|
|
|
467
|
-
async execute(
|
|
468
|
-
const
|
|
469
|
-
if (
|
|
470
|
-
return this.error("auth_error", "VERTEX_API_KEY not set");
|
|
471
|
-
}
|
|
471
|
+
async execute(args: Record<string, unknown>): Promise<CommandResult> {
|
|
472
|
+
const authError = await this.initProvider(args);
|
|
473
|
+
if (authError) return authError;
|
|
472
474
|
|
|
473
475
|
if (isDirectModeBranch(getBranch())) {
|
|
474
476
|
return this.error("direct_mode", "No plan in direct mode");
|
|
@@ -478,7 +480,6 @@ Output JSON:
|
|
|
478
480
|
return this.error("no_plan", "No plan directory exists for this branch");
|
|
479
481
|
}
|
|
480
482
|
|
|
481
|
-
// Gather all plan context
|
|
482
483
|
const plan = readPlan();
|
|
483
484
|
if (!plan) {
|
|
484
485
|
return this.error("file_not_found", "Plan file not found");
|
|
@@ -489,9 +490,8 @@ Output JSON:
|
|
|
489
490
|
const designManifest = readDesignManifest();
|
|
490
491
|
const paths = getPlanPaths();
|
|
491
492
|
|
|
492
|
-
// Build prompts section
|
|
493
493
|
const promptsSection = allPrompts
|
|
494
|
-
.map((p
|
|
494
|
+
.map((p) => {
|
|
495
495
|
const id = getPromptId(p.number, p.variant);
|
|
496
496
|
return `### Prompt ${id}
|
|
497
497
|
**Description:** ${p.frontMatter.description}
|
|
@@ -503,15 +503,13 @@ ${p.content}`;
|
|
|
503
503
|
})
|
|
504
504
|
.join("\n\n");
|
|
505
505
|
|
|
506
|
-
// Build design assets section with images
|
|
507
506
|
let designSection = "";
|
|
508
|
-
const contentParts:
|
|
507
|
+
const contentParts: ContentPart[] = [];
|
|
509
508
|
|
|
510
509
|
if (designManifest && designManifest.designs.length > 0) {
|
|
511
510
|
designSection = "## Design Assets\n\n";
|
|
512
511
|
for (const design of designManifest.designs) {
|
|
513
512
|
designSection += `- **${design.screenshot_file_name}**: ${design.description}\n`;
|
|
514
|
-
// Try to read the image
|
|
515
513
|
const imagePath = join(paths.design, design.screenshot_file_name);
|
|
516
514
|
const imageData = readImageAsBase64(imagePath);
|
|
517
515
|
if (imageData) {
|
|
@@ -535,47 +533,15 @@ ${designSection}
|
|
|
535
533
|
|
|
536
534
|
Review the plan and respond with JSON only.`;
|
|
537
535
|
|
|
538
|
-
// Add text prompt to content parts
|
|
539
536
|
contentParts.unshift(fullPrompt);
|
|
540
537
|
|
|
541
|
-
const
|
|
542
|
-
|
|
543
|
-
async () => {
|
|
544
|
-
const client = new GoogleGenAI({ vertexai: true, apiKey });
|
|
545
|
-
const genResult = await client.models.generateContent({
|
|
546
|
-
model: PRO_MODEL,
|
|
547
|
-
contents: contentParts,
|
|
548
|
-
});
|
|
549
|
-
return genResult.text ?? "";
|
|
550
|
-
},
|
|
551
|
-
"gemini.audit",
|
|
552
|
-
{},
|
|
553
|
-
GEMINI_FALLBACKS.audit
|
|
554
|
-
);
|
|
555
|
-
|
|
556
|
-
const durationMs = Math.round(performance.now() - start);
|
|
557
|
-
recordGeminiCall({ endpoint: "audit", duration_ms: durationMs, success: result.success, retries: result.retries });
|
|
538
|
+
const { error, parsed, metadata } = await this.callAndParse("audit", contentParts, true);
|
|
539
|
+
if (error) return error;
|
|
558
540
|
|
|
559
|
-
if (!result.success) {
|
|
560
|
-
return {
|
|
561
|
-
status: "error",
|
|
562
|
-
error: {
|
|
563
|
-
type: result.error,
|
|
564
|
-
message: "Gemini API unavailable after retries",
|
|
565
|
-
suggestion: result.fallback_suggestion,
|
|
566
|
-
},
|
|
567
|
-
metadata: { retries: result.retries, duration_ms: durationMs },
|
|
568
|
-
};
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
const parsed = parseJsonResponse(result.data);
|
|
572
541
|
const questions = (parsed.clarifying_questions as string[]) ?? [];
|
|
573
542
|
|
|
574
|
-
// If there are clarifying questions, block for user answers
|
|
575
543
|
if (questions.length > 0) {
|
|
576
544
|
const feedbackPath = writeAuditQuestionsFeedback(questions);
|
|
577
|
-
|
|
578
|
-
// Block until done: true
|
|
579
545
|
await watchForDone(feedbackPath, getBlockingGateTimeout());
|
|
580
546
|
const feedback = readAuditQuestionsFeedback();
|
|
581
547
|
|
|
@@ -583,62 +549,57 @@ Review the plan and respond with JSON only.`;
|
|
|
583
549
|
return this.error("invalid_feedback", feedback.error);
|
|
584
550
|
}
|
|
585
551
|
|
|
586
|
-
// Append thoughts and Q&A to user_input.md
|
|
587
552
|
const qaContent = feedback.data.questions
|
|
588
553
|
.map((q) => `**Q:** ${q.question}\n**A:** ${q.answer}`)
|
|
589
554
|
.join("\n\n");
|
|
590
|
-
const userThoughts = feedback.data.thoughts
|
|
555
|
+
const userThoughts = feedback.data.thoughts
|
|
556
|
+
? `**User Thoughts:** ${feedback.data.thoughts}\n\n`
|
|
557
|
+
: "";
|
|
591
558
|
appendUserInput(`## Audit Clarifications\n\n${userThoughts}${qaContent}`);
|
|
592
|
-
|
|
593
|
-
// Delete feedback file
|
|
594
559
|
deleteFeedbackFile("audit_questions");
|
|
595
560
|
|
|
596
|
-
// Record audit entry
|
|
597
561
|
appendPlanAudit({
|
|
598
|
-
review_context: parsed.thoughts as string ?? "",
|
|
562
|
+
review_context: (parsed.thoughts as string) ?? "",
|
|
599
563
|
decision: "needs_clarification",
|
|
600
564
|
total_questions: questions.length,
|
|
601
565
|
were_changes_suggested: ((parsed.suggested_edits as unknown[]) ?? []).length > 0,
|
|
602
566
|
});
|
|
603
567
|
|
|
604
|
-
return this.success(
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
});
|
|
568
|
+
return this.success(
|
|
569
|
+
{
|
|
570
|
+
verdict: "needs_clarification",
|
|
571
|
+
thoughts: parsed.thoughts,
|
|
572
|
+
answered_questions: feedback.data.questions,
|
|
573
|
+
suggested_edits: parsed.suggested_edits,
|
|
574
|
+
},
|
|
575
|
+
metadata
|
|
576
|
+
);
|
|
614
577
|
}
|
|
615
578
|
|
|
616
|
-
|
|
617
|
-
const verdict = parsed.verdict as string ?? "passed";
|
|
579
|
+
const verdict = (parsed.verdict as string) ?? "passed";
|
|
618
580
|
appendPlanAudit({
|
|
619
|
-
review_context: parsed.thoughts as string ?? "",
|
|
581
|
+
review_context: (parsed.thoughts as string) ?? "",
|
|
620
582
|
decision: verdict === "passed" ? "approved" : "rejected",
|
|
621
583
|
total_questions: 0,
|
|
622
584
|
were_changes_suggested: ((parsed.suggested_edits as unknown[]) ?? []).length > 0,
|
|
623
585
|
});
|
|
624
586
|
|
|
625
|
-
return this.success(
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
});
|
|
587
|
+
return this.success(
|
|
588
|
+
{
|
|
589
|
+
verdict,
|
|
590
|
+
thoughts: parsed.thoughts,
|
|
591
|
+
suggested_edits: parsed.suggested_edits,
|
|
592
|
+
},
|
|
593
|
+
metadata
|
|
594
|
+
);
|
|
634
595
|
}
|
|
635
596
|
}
|
|
636
597
|
|
|
637
598
|
// ============================================================================
|
|
638
|
-
// Review Command
|
|
599
|
+
// Review Command
|
|
639
600
|
// ============================================================================
|
|
640
601
|
|
|
641
|
-
class
|
|
602
|
+
class OracleReviewCommand extends OracleCommand {
|
|
642
603
|
readonly name = "review";
|
|
643
604
|
readonly description = "Review implementation against requirements";
|
|
644
605
|
|
|
@@ -673,17 +634,15 @@ Output JSON:
|
|
|
673
634
|
}`;
|
|
674
635
|
|
|
675
636
|
defineArguments(cmd: Command): void {
|
|
676
|
-
cmd
|
|
637
|
+
addProviderOption(cmd)
|
|
677
638
|
.argument("[prompt_num]", "Prompt number to review")
|
|
678
639
|
.argument("[variant]", "Optional variant letter (A, B, etc.)")
|
|
679
640
|
.option("--full", "Review entire plan implementation");
|
|
680
641
|
}
|
|
681
642
|
|
|
682
643
|
async execute(args: Record<string, unknown>): Promise<CommandResult> {
|
|
683
|
-
const
|
|
684
|
-
if (
|
|
685
|
-
return this.error("auth_error", "VERTEX_API_KEY not set");
|
|
686
|
-
}
|
|
644
|
+
const authError = await this.initProvider(args);
|
|
645
|
+
if (authError) return authError;
|
|
687
646
|
|
|
688
647
|
if (isDirectModeBranch(getBranch())) {
|
|
689
648
|
return this.error("direct_mode", "No plan in direct mode");
|
|
@@ -698,18 +657,17 @@ Output JSON:
|
|
|
698
657
|
const variant = args.variant as string | undefined;
|
|
699
658
|
|
|
700
659
|
if (isFull) {
|
|
701
|
-
return this.executeFullReview(
|
|
660
|
+
return this.executeFullReview();
|
|
702
661
|
}
|
|
703
662
|
|
|
704
663
|
if (!promptNum) {
|
|
705
664
|
return this.error("missing_argument", "Either --full or prompt_num is required");
|
|
706
665
|
}
|
|
707
666
|
|
|
708
|
-
return this.executePromptReview(
|
|
667
|
+
return this.executePromptReview(parseInt(promptNum, 10), variant ?? null);
|
|
709
668
|
}
|
|
710
669
|
|
|
711
670
|
private async executePromptReview(
|
|
712
|
-
apiKey: string,
|
|
713
671
|
promptNum: number,
|
|
714
672
|
variant: string | null
|
|
715
673
|
): Promise<CommandResult> {
|
|
@@ -721,8 +679,6 @@ Output JSON:
|
|
|
721
679
|
|
|
722
680
|
const userInput = readUserInput() ?? "";
|
|
723
681
|
const promptId = getPromptId(promptNum, variant);
|
|
724
|
-
|
|
725
|
-
// Get diff for worktree branch if available, otherwise current branch
|
|
726
682
|
const baseBranch = getBaseBranch();
|
|
727
683
|
const diffContent = getDiff(baseBranch);
|
|
728
684
|
const commits = getCommitSummaries(baseBranch);
|
|
@@ -746,43 +702,14 @@ ${commits}
|
|
|
746
702
|
|
|
747
703
|
Review and respond with JSON only.`;
|
|
748
704
|
|
|
749
|
-
const
|
|
750
|
-
|
|
751
|
-
async () => {
|
|
752
|
-
const client = new GoogleGenAI({ vertexai: true, apiKey });
|
|
753
|
-
const genResult = await client.models.generateContent({
|
|
754
|
-
model: PRO_MODEL,
|
|
755
|
-
contents: fullPrompt,
|
|
756
|
-
});
|
|
757
|
-
return genResult.text ?? "";
|
|
758
|
-
},
|
|
759
|
-
"gemini.review",
|
|
760
|
-
{},
|
|
761
|
-
GEMINI_FALLBACKS.review
|
|
762
|
-
);
|
|
763
|
-
|
|
764
|
-
const durationMs = Math.round(performance.now() - start);
|
|
765
|
-
recordGeminiCall({ endpoint: "review", duration_ms: durationMs, success: result.success, retries: result.retries });
|
|
705
|
+
const { error, parsed, metadata } = await this.callAndParse("review", fullPrompt, true);
|
|
706
|
+
if (error) return error;
|
|
766
707
|
|
|
767
|
-
|
|
768
|
-
return {
|
|
769
|
-
status: "error",
|
|
770
|
-
error: {
|
|
771
|
-
type: result.error,
|
|
772
|
-
message: "Gemini API unavailable after retries",
|
|
773
|
-
suggestion: result.fallback_suggestion,
|
|
774
|
-
},
|
|
775
|
-
metadata: { retries: result.retries, duration_ms: durationMs },
|
|
776
|
-
};
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
const parsed = parseJsonResponse(result.data);
|
|
708
|
+
const promptMeta = { ...metadata, prompt_id: promptId };
|
|
780
709
|
const questions = (parsed.clarifying_questions as string[]) ?? [];
|
|
781
710
|
|
|
782
|
-
// If there are clarifying questions, block for user answers
|
|
783
711
|
if (questions.length > 0) {
|
|
784
712
|
const feedbackPath = writeReviewQuestionsFeedback(promptId, questions);
|
|
785
|
-
|
|
786
713
|
await watchForDone(feedbackPath, getBlockingGateTimeout());
|
|
787
714
|
const feedback = readReviewQuestionsFeedback(promptId);
|
|
788
715
|
|
|
@@ -790,61 +717,58 @@ Review and respond with JSON only.`;
|
|
|
790
717
|
return this.error("invalid_feedback", feedback.error);
|
|
791
718
|
}
|
|
792
719
|
|
|
793
|
-
// Append to user_input.md
|
|
794
720
|
const qaContent = feedback.data.questions
|
|
795
721
|
.map((q) => `**Q:** ${q.question}\n**A:** ${q.answer}`)
|
|
796
722
|
.join("\n\n");
|
|
797
|
-
const userThoughts = feedback.data.thoughts
|
|
798
|
-
|
|
799
|
-
|
|
723
|
+
const userThoughts = feedback.data.thoughts
|
|
724
|
+
? `**User Thoughts:** ${feedback.data.thoughts}\n\n`
|
|
725
|
+
: "";
|
|
726
|
+
appendUserInput(
|
|
727
|
+
`## Review Clarifications (Prompt ${promptId})\n\n${userThoughts}${qaContent}`
|
|
728
|
+
);
|
|
800
729
|
deleteFeedbackFile(`${promptId}_review_questions`);
|
|
801
730
|
|
|
802
731
|
appendPromptReview(promptNum, variant, {
|
|
803
|
-
review_context: parsed.thoughts as string ?? "",
|
|
732
|
+
review_context: (parsed.thoughts as string) ?? "",
|
|
804
733
|
decision: "needs_changes",
|
|
805
734
|
total_questions: questions.length,
|
|
806
735
|
were_changes_suggested: !!(parsed.suggested_changes as string),
|
|
807
736
|
});
|
|
808
737
|
|
|
809
|
-
return this.success(
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
prompt_id: promptId,
|
|
819
|
-
});
|
|
738
|
+
return this.success(
|
|
739
|
+
{
|
|
740
|
+
verdict: "needs_clarification",
|
|
741
|
+
thoughts: parsed.thoughts,
|
|
742
|
+
answered_questions: feedback.data.questions,
|
|
743
|
+
suggested_changes: parsed.suggested_changes,
|
|
744
|
+
},
|
|
745
|
+
promptMeta
|
|
746
|
+
);
|
|
820
747
|
}
|
|
821
748
|
|
|
822
|
-
|
|
823
|
-
const verdict = parsed.verdict as string ?? "passed";
|
|
749
|
+
const verdict = (parsed.verdict as string) ?? "passed";
|
|
824
750
|
if (verdict === "passed") {
|
|
825
751
|
updatePromptStatus(promptNum, variant, "reviewed");
|
|
826
752
|
}
|
|
827
753
|
|
|
828
754
|
appendPromptReview(promptNum, variant, {
|
|
829
|
-
review_context: parsed.thoughts as string ?? "",
|
|
755
|
+
review_context: (parsed.thoughts as string) ?? "",
|
|
830
756
|
decision: verdict === "passed" ? "approved" : "needs_changes",
|
|
831
757
|
total_questions: 0,
|
|
832
758
|
were_changes_suggested: !!(parsed.suggested_changes as string),
|
|
833
759
|
});
|
|
834
760
|
|
|
835
|
-
return this.success(
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
prompt_id: promptId,
|
|
844
|
-
});
|
|
761
|
+
return this.success(
|
|
762
|
+
{
|
|
763
|
+
verdict,
|
|
764
|
+
thoughts: parsed.thoughts,
|
|
765
|
+
suggested_changes: parsed.suggested_changes,
|
|
766
|
+
},
|
|
767
|
+
promptMeta
|
|
768
|
+
);
|
|
845
769
|
}
|
|
846
770
|
|
|
847
|
-
private async executeFullReview(
|
|
771
|
+
private async executeFullReview(): Promise<CommandResult> {
|
|
848
772
|
const plan = readPlan();
|
|
849
773
|
if (!plan) {
|
|
850
774
|
return this.error("file_not_found", "Plan file not found");
|
|
@@ -854,7 +778,6 @@ Review and respond with JSON only.`;
|
|
|
854
778
|
const allPrompts = readAllPrompts();
|
|
855
779
|
const paths = getPlanPaths();
|
|
856
780
|
|
|
857
|
-
// Read curator.md if exists
|
|
858
781
|
const curatorPath = paths.curator;
|
|
859
782
|
const curatorContent = existsSync(curatorPath)
|
|
860
783
|
? readFileSync(curatorPath, "utf-8")
|
|
@@ -899,42 +822,14 @@ ${commits}
|
|
|
899
822
|
|
|
900
823
|
Review and respond with JSON only.`;
|
|
901
824
|
|
|
902
|
-
const
|
|
903
|
-
|
|
904
|
-
async () => {
|
|
905
|
-
const client = new GoogleGenAI({ vertexai: true, apiKey });
|
|
906
|
-
const genResult = await client.models.generateContent({
|
|
907
|
-
model: PRO_MODEL,
|
|
908
|
-
contents: fullPrompt,
|
|
909
|
-
});
|
|
910
|
-
return genResult.text ?? "";
|
|
911
|
-
},
|
|
912
|
-
"gemini.review",
|
|
913
|
-
{},
|
|
914
|
-
GEMINI_FALLBACKS.review
|
|
915
|
-
);
|
|
916
|
-
|
|
917
|
-
const durationMs = Math.round(performance.now() - start);
|
|
918
|
-
recordGeminiCall({ endpoint: "review", duration_ms: durationMs, success: result.success, retries: result.retries });
|
|
825
|
+
const { error, parsed, metadata } = await this.callAndParse("review", fullPrompt, true);
|
|
826
|
+
if (error) return error;
|
|
919
827
|
|
|
920
|
-
|
|
921
|
-
return {
|
|
922
|
-
status: "error",
|
|
923
|
-
error: {
|
|
924
|
-
type: result.error,
|
|
925
|
-
message: "Gemini API unavailable after retries",
|
|
926
|
-
suggestion: result.fallback_suggestion,
|
|
927
|
-
},
|
|
928
|
-
metadata: { retries: result.retries, duration_ms: durationMs },
|
|
929
|
-
};
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
const parsed = parseJsonResponse(result.data);
|
|
828
|
+
const fullMeta = { ...metadata, command: "oracle review --full" };
|
|
933
829
|
const questions = (parsed.clarifying_questions as string[]) ?? [];
|
|
934
830
|
|
|
935
831
|
if (questions.length > 0) {
|
|
936
832
|
const feedbackPath = writeReviewQuestionsFeedback("full", questions);
|
|
937
|
-
|
|
938
833
|
await watchForDone(feedbackPath, getBlockingGateTimeout());
|
|
939
834
|
const feedback = readReviewQuestionsFeedback("full");
|
|
940
835
|
|
|
@@ -945,55 +840,53 @@ Review and respond with JSON only.`;
|
|
|
945
840
|
const qaContent = feedback.data.questions
|
|
946
841
|
.map((q) => `**Q:** ${q.question}\n**A:** ${q.answer}`)
|
|
947
842
|
.join("\n\n");
|
|
948
|
-
const userThoughts = feedback.data.thoughts
|
|
843
|
+
const userThoughts = feedback.data.thoughts
|
|
844
|
+
? `**User Thoughts:** ${feedback.data.thoughts}\n\n`
|
|
845
|
+
: "";
|
|
949
846
|
appendUserInput(`## Full Review Clarifications\n\n${userThoughts}${qaContent}`);
|
|
950
|
-
|
|
951
847
|
deleteFeedbackFile("full_review_questions");
|
|
952
848
|
|
|
953
849
|
appendPlanReview({
|
|
954
|
-
review_context: parsed.thoughts as string ?? "",
|
|
850
|
+
review_context: (parsed.thoughts as string) ?? "",
|
|
955
851
|
decision: "needs_changes",
|
|
956
852
|
total_questions: questions.length,
|
|
957
853
|
were_changes_suggested: !!(parsed.suggested_changes as string),
|
|
958
854
|
});
|
|
959
855
|
|
|
960
|
-
return this.success(
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
});
|
|
856
|
+
return this.success(
|
|
857
|
+
{
|
|
858
|
+
verdict: "needs_clarification",
|
|
859
|
+
thoughts: parsed.thoughts,
|
|
860
|
+
answered_questions: feedback.data.questions,
|
|
861
|
+
suggested_changes: parsed.suggested_changes,
|
|
862
|
+
},
|
|
863
|
+
fullMeta
|
|
864
|
+
);
|
|
970
865
|
}
|
|
971
866
|
|
|
972
|
-
const verdict = parsed.verdict as string ?? "passed";
|
|
867
|
+
const verdict = (parsed.verdict as string) ?? "passed";
|
|
973
868
|
appendPlanReview({
|
|
974
|
-
review_context: parsed.thoughts as string ?? "",
|
|
869
|
+
review_context: (parsed.thoughts as string) ?? "",
|
|
975
870
|
decision: verdict === "passed" ? "approved" : "needs_changes",
|
|
976
871
|
total_questions: 0,
|
|
977
872
|
were_changes_suggested: !!(parsed.suggested_changes as string),
|
|
978
873
|
});
|
|
979
874
|
|
|
980
|
-
return this.success(
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
});
|
|
875
|
+
return this.success(
|
|
876
|
+
{
|
|
877
|
+
verdict,
|
|
878
|
+
thoughts: parsed.thoughts,
|
|
879
|
+
suggested_changes: parsed.suggested_changes,
|
|
880
|
+
},
|
|
881
|
+
fullMeta
|
|
882
|
+
);
|
|
989
883
|
}
|
|
990
884
|
}
|
|
991
885
|
|
|
992
|
-
// Auto-discovered by cli.ts
|
|
993
886
|
export const COMMANDS = {
|
|
994
|
-
ask:
|
|
995
|
-
validate:
|
|
996
|
-
architect:
|
|
997
|
-
audit:
|
|
998
|
-
review:
|
|
887
|
+
ask: OracleAskCommand,
|
|
888
|
+
validate: OracleValidateCommand,
|
|
889
|
+
architect: OracleArchitectCommand,
|
|
890
|
+
audit: OracleAuditCommand,
|
|
891
|
+
review: OracleReviewCommand,
|
|
999
892
|
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gemini provider implementation using @google/genai SDK.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { GoogleGenAI } from "@google/genai";
|
|
6
|
+
import type {
|
|
7
|
+
LLMProvider,
|
|
8
|
+
GenerateOptions,
|
|
9
|
+
GenerateResult,
|
|
10
|
+
ProviderConfig,
|
|
11
|
+
ContentPart,
|
|
12
|
+
} from "./providers.js";
|
|
13
|
+
import { PROVIDER_CONFIGS } from "./providers.js";
|
|
14
|
+
|
|
15
|
+
export class GeminiProvider implements LLMProvider {
|
|
16
|
+
readonly config: ProviderConfig = PROVIDER_CONFIGS.gemini;
|
|
17
|
+
|
|
18
|
+
getApiKey(): string | undefined {
|
|
19
|
+
return process.env.VERTEX_API_KEY;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async generate(
|
|
23
|
+
contents: string | ContentPart[],
|
|
24
|
+
options?: GenerateOptions
|
|
25
|
+
): Promise<GenerateResult> {
|
|
26
|
+
const apiKey = this.getApiKey();
|
|
27
|
+
if (!apiKey) {
|
|
28
|
+
throw new Error(`${this.config.apiKeyEnvVar} not set`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const model =
|
|
32
|
+
options?.model ?? (options?.usePro ? this.config.proModel : this.config.defaultModel);
|
|
33
|
+
const client = new GoogleGenAI({ vertexai: true, apiKey });
|
|
34
|
+
|
|
35
|
+
const result = await client.models.generateContent({ model, contents });
|
|
36
|
+
return { text: result.text ?? "", model };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -29,6 +29,7 @@ export {
|
|
|
29
29
|
recordPromptCompleted,
|
|
30
30
|
recordGateCompleted,
|
|
31
31
|
recordGeminiCall,
|
|
32
|
+
recordOracleCall,
|
|
32
33
|
recordDiscoveryCompleted,
|
|
33
34
|
recordDocumentationExtracted,
|
|
34
35
|
} from "./observability.js";
|
|
@@ -167,9 +168,24 @@ export {
|
|
|
167
168
|
export type { TreeEntry, RepomixResult } from "./repomix.js";
|
|
168
169
|
|
|
169
170
|
// Retry utilities
|
|
170
|
-
export { withRetry, GEMINI_FALLBACKS } from "./retry.js";
|
|
171
|
+
export { withRetry, ORACLE_FALLBACKS, GEMINI_FALLBACKS } from "./retry.js";
|
|
171
172
|
export type { RetryOptions, RetryResult } from "./retry.js";
|
|
172
173
|
|
|
174
|
+
// Provider utilities
|
|
175
|
+
export {
|
|
176
|
+
createProvider,
|
|
177
|
+
getDefaultProvider,
|
|
178
|
+
PROVIDER_CONFIGS,
|
|
179
|
+
} from "./providers.js";
|
|
180
|
+
export type {
|
|
181
|
+
ProviderName,
|
|
182
|
+
ProviderConfig,
|
|
183
|
+
GenerateOptions,
|
|
184
|
+
GenerateResult,
|
|
185
|
+
ContentPart,
|
|
186
|
+
LLMProvider,
|
|
187
|
+
} from "./providers.js";
|
|
188
|
+
|
|
173
189
|
// Protocol utilities (Phase 9)
|
|
174
190
|
export {
|
|
175
191
|
getProtocolsDir,
|
|
@@ -242,6 +242,18 @@ export function recordGeminiCall(data: {
|
|
|
242
242
|
recordMetric({ type: "gemini_call", ...data });
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
+
export function recordOracleCall(data: {
|
|
246
|
+
provider: string;
|
|
247
|
+
endpoint: string;
|
|
248
|
+
duration_ms: number;
|
|
249
|
+
success: boolean;
|
|
250
|
+
retries: number;
|
|
251
|
+
verdict?: string;
|
|
252
|
+
plan_name?: string;
|
|
253
|
+
}): void {
|
|
254
|
+
recordMetric({ type: "oracle_call", ...data });
|
|
255
|
+
}
|
|
256
|
+
|
|
245
257
|
export function recordDiscoveryCompleted(data: {
|
|
246
258
|
specialist: string;
|
|
247
259
|
approach_count: number;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI provider implementation using direct fetch (no SDK dependency).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
LLMProvider,
|
|
7
|
+
GenerateOptions,
|
|
8
|
+
GenerateResult,
|
|
9
|
+
ProviderConfig,
|
|
10
|
+
ContentPart,
|
|
11
|
+
} from "./providers.js";
|
|
12
|
+
import { PROVIDER_CONFIGS } from "./providers.js";
|
|
13
|
+
|
|
14
|
+
interface OpenAIResponse {
|
|
15
|
+
choices?: Array<{ message?: { content?: string } }>;
|
|
16
|
+
model?: string;
|
|
17
|
+
error?: { message?: string };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface OpenAIMessage {
|
|
21
|
+
role: string;
|
|
22
|
+
content: string | Array<{ type: string; text?: string; image_url?: { url: string } }>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class OpenAIProvider implements LLMProvider {
|
|
26
|
+
readonly config: ProviderConfig = PROVIDER_CONFIGS.openai;
|
|
27
|
+
|
|
28
|
+
getApiKey(): string | undefined {
|
|
29
|
+
return process.env.OPENAI_API_KEY;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async generate(
|
|
33
|
+
contents: string | ContentPart[],
|
|
34
|
+
options?: GenerateOptions
|
|
35
|
+
): Promise<GenerateResult> {
|
|
36
|
+
const apiKey = this.getApiKey();
|
|
37
|
+
if (!apiKey) {
|
|
38
|
+
throw new Error(`${this.config.apiKeyEnvVar} not set`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const model =
|
|
42
|
+
options?.model ?? (options?.usePro ? this.config.proModel : this.config.defaultModel);
|
|
43
|
+
const messages = this.formatMessages(contents);
|
|
44
|
+
|
|
45
|
+
const response = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
46
|
+
method: "POST",
|
|
47
|
+
headers: {
|
|
48
|
+
Authorization: `Bearer ${apiKey}`,
|
|
49
|
+
"Content-Type": "application/json",
|
|
50
|
+
},
|
|
51
|
+
body: JSON.stringify({ model, messages }),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if (!response.ok) {
|
|
55
|
+
const text = await response.text();
|
|
56
|
+
throw new Error(`HTTP ${response.status}: ${text}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const data = (await response.json()) as OpenAIResponse;
|
|
60
|
+
if (data.error) {
|
|
61
|
+
throw new Error(data.error.message ?? "OpenAI API error");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
text: data.choices?.[0]?.message?.content ?? "",
|
|
66
|
+
model: data.model ?? model,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private formatMessages(contents: string | ContentPart[]): OpenAIMessage[] {
|
|
71
|
+
if (typeof contents === "string") {
|
|
72
|
+
return [{ role: "user", content: contents }];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const parts: Array<{ type: string; text?: string; image_url?: { url: string } }> = [];
|
|
76
|
+
for (const item of contents) {
|
|
77
|
+
if (typeof item === "string") {
|
|
78
|
+
parts.push({ type: "text", text: item });
|
|
79
|
+
} else if (item.inlineData) {
|
|
80
|
+
parts.push({
|
|
81
|
+
type: "image_url",
|
|
82
|
+
image_url: {
|
|
83
|
+
url: `data:${item.inlineData.mimeType};base64,${item.inlineData.data}`,
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return [{ role: "user", content: parts }];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider abstraction layer for multi-LLM oracle commands.
|
|
3
|
+
* Supports Gemini and OpenAI with standardized interface.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type ProviderName = "gemini" | "openai";
|
|
7
|
+
|
|
8
|
+
export interface ProviderConfig {
|
|
9
|
+
name: ProviderName;
|
|
10
|
+
apiKeyEnvVar: string;
|
|
11
|
+
defaultModel: string;
|
|
12
|
+
proModel: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface GenerateOptions {
|
|
16
|
+
model?: string;
|
|
17
|
+
usePro?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface GenerateResult {
|
|
21
|
+
text: string;
|
|
22
|
+
model: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type ContentPart = string | { inlineData: { mimeType: string; data: string } };
|
|
26
|
+
|
|
27
|
+
export interface LLMProvider {
|
|
28
|
+
readonly config: ProviderConfig;
|
|
29
|
+
getApiKey(): string | undefined;
|
|
30
|
+
generate(
|
|
31
|
+
contents: string | ContentPart[],
|
|
32
|
+
options?: GenerateOptions
|
|
33
|
+
): Promise<GenerateResult>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const PROVIDER_CONFIGS: Record<ProviderName, ProviderConfig> = {
|
|
37
|
+
gemini: {
|
|
38
|
+
name: "gemini",
|
|
39
|
+
apiKeyEnvVar: "VERTEX_API_KEY",
|
|
40
|
+
defaultModel: "gemini-2.0-flash",
|
|
41
|
+
proModel: "gemini-3-pro-preview",
|
|
42
|
+
},
|
|
43
|
+
openai: {
|
|
44
|
+
name: "openai",
|
|
45
|
+
apiKeyEnvVar: "OPENAI_API_KEY",
|
|
46
|
+
defaultModel: "gpt-5.2",
|
|
47
|
+
proModel: "gpt-5.2",
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export function getDefaultProvider(): ProviderName {
|
|
52
|
+
const envProvider = process.env.ORACLE_DEFAULT_PROVIDER;
|
|
53
|
+
if (envProvider === "openai" || envProvider === "gemini") {
|
|
54
|
+
return envProvider;
|
|
55
|
+
}
|
|
56
|
+
return "gemini";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Lazy imports to avoid loading unused SDKs
|
|
60
|
+
export async function createProvider(name: ProviderName): Promise<LLMProvider> {
|
|
61
|
+
switch (name) {
|
|
62
|
+
case "gemini": {
|
|
63
|
+
const { GeminiProvider } = await import("./gemini-provider.js");
|
|
64
|
+
return new GeminiProvider();
|
|
65
|
+
}
|
|
66
|
+
case "openai": {
|
|
67
|
+
const { OpenAIProvider } = await import("./openai-provider.js");
|
|
68
|
+
return new OpenAIProvider();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -129,10 +129,17 @@ export async function withRetry<T>(
|
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
/**
|
|
132
|
-
* Pre-defined fallback suggestions for
|
|
132
|
+
* Pre-defined fallback suggestions for oracle endpoints (provider-agnostic).
|
|
133
133
|
*/
|
|
134
|
-
export const
|
|
135
|
-
audit: "Skip audit and proceed with user review only
|
|
134
|
+
export const ORACLE_FALLBACKS: Record<string, string> = {
|
|
135
|
+
audit: "Skip audit and proceed with user review only",
|
|
136
136
|
review: "Mark prompt as needs_manual_review for user verification",
|
|
137
|
-
ask: "Proceed without
|
|
137
|
+
ask: "Proceed without LLM response, use agent judgment",
|
|
138
|
+
validate: "Skip validation and proceed with user review only",
|
|
139
|
+
architect: "Proceed without architect analysis",
|
|
138
140
|
};
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* @deprecated Use ORACLE_FALLBACKS instead
|
|
144
|
+
*/
|
|
145
|
+
export const GEMINI_FALLBACKS = ORACLE_FALLBACKS;
|
package/.claude/settings.json
CHANGED