@almightygpt/core 0.2.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/dist/adapters/claude.d.ts +31 -0
- package/dist/adapters/claude.d.ts.map +1 -0
- package/dist/adapters/claude.js +90 -0
- package/dist/adapters/claude.js.map +1 -0
- package/dist/adapters/gemini.d.ts +42 -0
- package/dist/adapters/gemini.d.ts.map +1 -0
- package/dist/adapters/gemini.js +133 -0
- package/dist/adapters/gemini.js.map +1 -0
- package/dist/adapters/index.d.ts +16 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +15 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/mock.d.ts +23 -0
- package/dist/adapters/mock.d.ts.map +1 -0
- package/dist/adapters/mock.js +107 -0
- package/dist/adapters/mock.js.map +1 -0
- package/dist/adapters/openai.d.ts +38 -0
- package/dist/adapters/openai.d.ts.map +1 -0
- package/dist/adapters/openai.js +105 -0
- package/dist/adapters/openai.js.map +1 -0
- package/dist/adapters/types.d.ts +65 -0
- package/dist/adapters/types.d.ts.map +1 -0
- package/dist/adapters/types.js +26 -0
- package/dist/adapters/types.js.map +1 -0
- package/dist/config/load.d.ts +15 -0
- package/dist/config/load.d.ts.map +1 -0
- package/dist/config/load.js +46 -0
- package/dist/config/load.js.map +1 -0
- package/dist/config/schema.d.ts +260 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +58 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/context/manifest.d.ts +58 -0
- package/dist/context/manifest.d.ts.map +1 -0
- package/dist/context/manifest.js +49 -0
- package/dist/context/manifest.js.map +1 -0
- package/dist/context/redact.d.ts +26 -0
- package/dist/context/redact.d.ts.map +1 -0
- package/dist/context/redact.js +67 -0
- package/dist/context/redact.js.map +1 -0
- package/dist/git/status.d.ts +48 -0
- package/dist/git/status.d.ts.map +1 -0
- package/dist/git/status.js +79 -0
- package/dist/git/status.js.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +38 -0
- package/dist/index.js.map +1 -0
- package/dist/review/budget.d.ts +46 -0
- package/dist/review/budget.d.ts.map +1 -0
- package/dist/review/budget.js +83 -0
- package/dist/review/budget.js.map +1 -0
- package/dist/review/diff.d.ts +21 -0
- package/dist/review/diff.d.ts.map +1 -0
- package/dist/review/diff.js +55 -0
- package/dist/review/diff.js.map +1 -0
- package/dist/review/events.d.ts +76 -0
- package/dist/review/events.d.ts.map +1 -0
- package/dist/review/events.js +13 -0
- package/dist/review/events.js.map +1 -0
- package/dist/review/memory.d.ts +23 -0
- package/dist/review/memory.d.ts.map +1 -0
- package/dist/review/memory.js +42 -0
- package/dist/review/memory.js.map +1 -0
- package/dist/review/prompts.d.ts +34 -0
- package/dist/review/prompts.d.ts.map +1 -0
- package/dist/review/prompts.js +174 -0
- package/dist/review/prompts.js.map +1 -0
- package/dist/review/run-diff-review.d.ts +52 -0
- package/dist/review/run-diff-review.d.ts.map +1 -0
- package/dist/review/run-diff-review.js +258 -0
- package/dist/review/run-diff-review.js.map +1 -0
- package/dist/review/run-worker-reviewer.d.ts +72 -0
- package/dist/review/run-worker-reviewer.d.ts.map +1 -0
- package/dist/review/run-worker-reviewer.js +407 -0
- package/dist/review/run-worker-reviewer.js.map +1 -0
- package/dist/review/write.d.ts +44 -0
- package/dist/review/write.d.ts.map +1 -0
- package/dist/review/write.js +152 -0
- package/dist/review/write.js.map +1 -0
- package/dist/runs/decide.d.ts +45 -0
- package/dist/runs/decide.d.ts.map +1 -0
- package/dist/runs/decide.js +93 -0
- package/dist/runs/decide.js.map +1 -0
- package/dist/runs/folder.d.ts +42 -0
- package/dist/runs/folder.d.ts.map +1 -0
- package/dist/runs/folder.js +82 -0
- package/dist/runs/folder.js.map +1 -0
- package/dist/runs/list.d.ts +58 -0
- package/dist/runs/list.d.ts.map +1 -0
- package/dist/runs/list.js +117 -0
- package/dist/runs/list.js.map +1 -0
- package/dist/runs/types.d.ts +96 -0
- package/dist/runs/types.d.ts.map +1 -0
- package/dist/runs/types.js +13 -0
- package/dist/runs/types.js.map +1 -0
- package/dist/templates/install.d.ts +49 -0
- package/dist/templates/install.d.ts.map +1 -0
- package/dist/templates/install.js +154 -0
- package/dist/templates/install.js.map +1 -0
- package/package.json +34 -0
- package/src/adapters/claude.ts +133 -0
- package/src/adapters/gemini.ts +183 -0
- package/src/adapters/index.ts +21 -0
- package/src/adapters/mock.ts +125 -0
- package/src/adapters/openai.ts +150 -0
- package/src/adapters/types.ts +73 -0
- package/src/config/load.ts +61 -0
- package/src/config/schema.ts +64 -0
- package/src/context/manifest.ts +94 -0
- package/src/context/redact.ts +93 -0
- package/src/git/status.ts +108 -0
- package/src/index.ts +127 -0
- package/src/review/budget.ts +116 -0
- package/src/review/diff.ts +85 -0
- package/src/review/events.ts +86 -0
- package/src/review/memory.ts +57 -0
- package/src/review/prompts.ts +208 -0
- package/src/review/run-diff-review.ts +353 -0
- package/src/review/run-worker-reviewer.ts +528 -0
- package/src/review/write.ts +208 -0
- package/src/runs/decide.ts +153 -0
- package/src/runs/folder.ts +137 -0
- package/src/runs/list.ts +152 -0
- package/src/runs/types.ts +98 -0
- package/src/templates/install.ts +198 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google / Gemini adapter — third real provider.
|
|
3
|
+
*
|
|
4
|
+
* Reads GOOGLE_API_KEY (or GEMINI_API_KEY as a fallback) from the
|
|
5
|
+
* environment. Never stores or logs the key.
|
|
6
|
+
*
|
|
7
|
+
* Default model: gemini-2.5-pro. Configurable per-call. Earlier
|
|
8
|
+
* gemini-1.5-* models have been retired from the v1beta API as of
|
|
9
|
+
* late 2025/early 2026.
|
|
10
|
+
*
|
|
11
|
+
* Pricing (USD per 1M tokens, approximate as of 2026-05):
|
|
12
|
+
* gemini-2.5-pro: 1.25 in / 10.00 out
|
|
13
|
+
* gemini-2.5-flash: 0.30 in / 2.50 out
|
|
14
|
+
* gemini-2.0-flash: 0.10 in / 0.40 out
|
|
15
|
+
* gemini-3.x-*: pricing TBD — falls back to conservative defaults
|
|
16
|
+
*
|
|
17
|
+
* Pricing changes frequently — verify at ai.google.dev/pricing if cost
|
|
18
|
+
* accuracy matters for a given run.
|
|
19
|
+
*
|
|
20
|
+
* Note on system prompts: the @google/generative-ai SDK accepts a
|
|
21
|
+
* `systemInstruction` field on getGenerativeModel(). We use that
|
|
22
|
+
* instead of stitching system text into the user message.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { GoogleGenerativeAI } from "@google/generative-ai";
|
|
26
|
+
import {
|
|
27
|
+
AdapterError,
|
|
28
|
+
type Adapter,
|
|
29
|
+
type AdapterInput,
|
|
30
|
+
type AdapterOutput,
|
|
31
|
+
} from "./types.js";
|
|
32
|
+
|
|
33
|
+
const PRICING_USD_PER_1M: Record<string, { input: number; output: number }> = {
|
|
34
|
+
"gemini-2.5-pro": { input: 1.25, output: 10.0 },
|
|
35
|
+
"gemini-2.5-flash": { input: 0.3, output: 2.5 },
|
|
36
|
+
"gemini-2.0-flash": { input: 0.1, output: 0.4 },
|
|
37
|
+
// Older models kept for reproducibility on saved runs:
|
|
38
|
+
"gemini-1.5-pro": { input: 1.25, output: 5.0 },
|
|
39
|
+
"gemini-1.5-flash": { input: 0.075, output: 0.3 },
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Defaulting to flash, not pro, because Google's free tier typically has
|
|
43
|
+
// zero quota on the pro models but allows real usage on flash. Users with
|
|
44
|
+
// paid billing can override via the `model` field in config.yaml (when that
|
|
45
|
+
// field lands in v0.3) or by constructing the adapter directly. For code
|
|
46
|
+
// review of typical-sized diffs, flash quality is sufficient — pro pays for
|
|
47
|
+
// itself only on very large diffs or complex reasoning chains.
|
|
48
|
+
const DEFAULT_MODEL = "gemini-2.5-flash";
|
|
49
|
+
|
|
50
|
+
export interface GeminiAdapterOptions {
|
|
51
|
+
apiKey?: string;
|
|
52
|
+
defaultModel?: string;
|
|
53
|
+
defaultMaxOutputTokens?: number;
|
|
54
|
+
defaultTimeoutMs?: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export class GeminiAdapter implements Adapter {
|
|
58
|
+
readonly name: string;
|
|
59
|
+
readonly provider = "google";
|
|
60
|
+
|
|
61
|
+
private readonly client: GoogleGenerativeAI | null;
|
|
62
|
+
private readonly defaultModel: string;
|
|
63
|
+
private readonly defaultMaxOutputTokens: number;
|
|
64
|
+
private readonly defaultTimeoutMs: number;
|
|
65
|
+
|
|
66
|
+
constructor(name = "gemini", options: GeminiAdapterOptions = {}) {
|
|
67
|
+
this.name = name;
|
|
68
|
+
this.defaultModel = options.defaultModel ?? DEFAULT_MODEL;
|
|
69
|
+
// Bigger than other adapters by design: gemini-2.5-* spends a portion
|
|
70
|
+
// of the output budget on internal "thinking" tokens before producing
|
|
71
|
+
// visible output. 8192 leaves ~4-6k for the actual response after the
|
|
72
|
+
// model finishes its reasoning. We also disable thinking explicitly
|
|
73
|
+
// below to make sure the visible response gets the full budget.
|
|
74
|
+
this.defaultMaxOutputTokens = options.defaultMaxOutputTokens ?? 8192;
|
|
75
|
+
this.defaultTimeoutMs = options.defaultTimeoutMs ?? 120_000;
|
|
76
|
+
|
|
77
|
+
const apiKey =
|
|
78
|
+
options.apiKey ??
|
|
79
|
+
process.env["GOOGLE_API_KEY"] ??
|
|
80
|
+
process.env["GEMINI_API_KEY"];
|
|
81
|
+
this.client = apiKey ? new GoogleGenerativeAI(apiKey) : null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async isAvailable(): Promise<boolean> {
|
|
85
|
+
return this.client !== null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async execute(input: AdapterInput): Promise<AdapterOutput> {
|
|
89
|
+
if (!this.client) {
|
|
90
|
+
throw new AdapterError(
|
|
91
|
+
"GOOGLE_API_KEY (or GEMINI_API_KEY) is not set. " +
|
|
92
|
+
"Export one in your environment.",
|
|
93
|
+
this.name,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const model = input.model ?? this.defaultModel;
|
|
98
|
+
const maxOutputTokens = input.maxOutputTokens ?? this.defaultMaxOutputTokens;
|
|
99
|
+
const timeoutMs = input.timeoutMs ?? this.defaultTimeoutMs;
|
|
100
|
+
|
|
101
|
+
// gemini-2.5-* "thinks" before responding. Without an explicit cap the
|
|
102
|
+
// model can consume most/all of maxOutputTokens on internal reasoning
|
|
103
|
+
// tokens, leaving the user-facing response truncated mid-sentence. We
|
|
104
|
+
// pass thinkingConfig via a type-relaxed cast because @google/generative-ai
|
|
105
|
+
// 0.21 doesn't declare it on GenerationConfig yet, but the v1beta REST
|
|
106
|
+
// API accepts it.
|
|
107
|
+
const generationConfig: Record<string, unknown> = {
|
|
108
|
+
maxOutputTokens,
|
|
109
|
+
thinkingConfig: { thinkingBudget: 0 },
|
|
110
|
+
};
|
|
111
|
+
if (input.responseFormat === "json_object") {
|
|
112
|
+
generationConfig["responseMimeType"] = "application/json";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const generative = this.client.getGenerativeModel({
|
|
116
|
+
model,
|
|
117
|
+
systemInstruction: input.systemPrompt,
|
|
118
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
119
|
+
generationConfig: generationConfig as any,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const start = Date.now();
|
|
123
|
+
let response;
|
|
124
|
+
try {
|
|
125
|
+
// The SDK doesn't expose a per-call timeout option, so we race manually.
|
|
126
|
+
response = await Promise.race([
|
|
127
|
+
generative.generateContent(input.userMessage),
|
|
128
|
+
new Promise<never>((_, reject) =>
|
|
129
|
+
setTimeout(
|
|
130
|
+
() =>
|
|
131
|
+
reject(
|
|
132
|
+
new Error(`Gemini call exceeded timeout of ${timeoutMs}ms`),
|
|
133
|
+
),
|
|
134
|
+
timeoutMs,
|
|
135
|
+
),
|
|
136
|
+
),
|
|
137
|
+
]);
|
|
138
|
+
} catch (err) {
|
|
139
|
+
throw new AdapterError(
|
|
140
|
+
`Gemini call failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
141
|
+
this.name,
|
|
142
|
+
err,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const content = response.response.text();
|
|
147
|
+
if (!content) {
|
|
148
|
+
throw new AdapterError(
|
|
149
|
+
"Gemini returned no text content (filtered, empty, or tool-only response).",
|
|
150
|
+
this.name,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const usage = response.response.usageMetadata;
|
|
155
|
+
const tokensIn = usage?.promptTokenCount ?? 0;
|
|
156
|
+
const tokensOut = usage?.candidatesTokenCount ?? 0;
|
|
157
|
+
const costUsd = estimateCostUsd(model, tokensIn, tokensOut);
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
content,
|
|
161
|
+
tokensIn,
|
|
162
|
+
tokensOut,
|
|
163
|
+
costUsd,
|
|
164
|
+
latencyMs: Date.now() - start,
|
|
165
|
+
modelUsed: model,
|
|
166
|
+
provider: this.provider,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function estimateCostUsd(
|
|
172
|
+
model: string,
|
|
173
|
+
tokensIn: number,
|
|
174
|
+
tokensOut: number,
|
|
175
|
+
): number {
|
|
176
|
+
const key = Object.keys(PRICING_USD_PER_1M).find((k) =>
|
|
177
|
+
model.toLowerCase().startsWith(k),
|
|
178
|
+
);
|
|
179
|
+
if (!key) return 0;
|
|
180
|
+
const rates = PRICING_USD_PER_1M[key];
|
|
181
|
+
if (!rates) return 0;
|
|
182
|
+
return (tokensIn / 1_000_000) * rates.input + (tokensOut / 1_000_000) * rates.output;
|
|
183
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter exports.
|
|
3
|
+
*
|
|
4
|
+
* Add new adapters here as they're implemented:
|
|
5
|
+
* - Claude (Anthropic) — task #15
|
|
6
|
+
* - Gemini — post-MVP 1
|
|
7
|
+
* - OpenRouter — post-MVP 1 (for cost-conscious users; see
|
|
8
|
+
* docs/claude/competitor-ai-council.md)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export type {
|
|
12
|
+
Adapter,
|
|
13
|
+
AdapterInput,
|
|
14
|
+
AdapterOutput,
|
|
15
|
+
AgentRole,
|
|
16
|
+
} from "./types.js";
|
|
17
|
+
export { AdapterError } from "./types.js";
|
|
18
|
+
export { MockAdapter } from "./mock.js";
|
|
19
|
+
export { OpenAIAdapter, type OpenAIAdapterOptions } from "./openai.js";
|
|
20
|
+
export { ClaudeAdapter, type ClaudeAdapterOptions } from "./claude.js";
|
|
21
|
+
export { GeminiAdapter, type GeminiAdapterOptions } from "./gemini.js";
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock adapter — produces deterministic, realistic-shaped output without
|
|
3
|
+
* calling any external API. Used for end-to-end testing, demos without
|
|
4
|
+
* API keys, and to exercise the review pipeline in CI.
|
|
5
|
+
*
|
|
6
|
+
* The mock is deliberately "good enough to look real" so we can spot
|
|
7
|
+
* format problems. It is not a substitute for real model output.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Adapter, AdapterInput, AdapterOutput } from "./types.js";
|
|
11
|
+
|
|
12
|
+
export class MockAdapter implements Adapter {
|
|
13
|
+
readonly name = "mock";
|
|
14
|
+
readonly provider = "mock";
|
|
15
|
+
|
|
16
|
+
constructor(
|
|
17
|
+
private readonly opts: {
|
|
18
|
+
/** Optional simulated latency in ms. Default 50. */
|
|
19
|
+
latencyMs?: number;
|
|
20
|
+
} = {},
|
|
21
|
+
) {}
|
|
22
|
+
|
|
23
|
+
async isAvailable(): Promise<boolean> {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async execute(input: AdapterInput): Promise<AdapterOutput> {
|
|
28
|
+
const start = Date.now();
|
|
29
|
+
const latency = this.opts.latencyMs ?? 50;
|
|
30
|
+
await new Promise((r) => setTimeout(r, latency));
|
|
31
|
+
|
|
32
|
+
const content =
|
|
33
|
+
input.role === "reviewer"
|
|
34
|
+
? this.mockReview(input.userMessage)
|
|
35
|
+
: this.mockWork(input.userMessage);
|
|
36
|
+
|
|
37
|
+
// Token counts are rough — 4 chars/token heuristic. Cost is always 0
|
|
38
|
+
// because the mock never bills anyone.
|
|
39
|
+
const tokensIn = Math.ceil(
|
|
40
|
+
(input.systemPrompt.length + input.userMessage.length) / 4,
|
|
41
|
+
);
|
|
42
|
+
const tokensOut = Math.ceil(content.length / 4);
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
content,
|
|
46
|
+
tokensIn,
|
|
47
|
+
tokensOut,
|
|
48
|
+
costUsd: 0,
|
|
49
|
+
latencyMs: Date.now() - start,
|
|
50
|
+
modelUsed: "mock-1",
|
|
51
|
+
provider: "mock",
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private mockReview(userMessage: string): string {
|
|
56
|
+
// Extract any file paths the user mentioned to make the mock look at-the-code.
|
|
57
|
+
const fileHints = Array.from(
|
|
58
|
+
userMessage.matchAll(/[\w./-]+\.(ts|js|tsx|jsx|py|go|rb|md|json|yaml)/g),
|
|
59
|
+
)
|
|
60
|
+
.slice(0, 3)
|
|
61
|
+
.map((m) => m[0]);
|
|
62
|
+
const refs = fileHints.length
|
|
63
|
+
? fileHints
|
|
64
|
+
: ["src/index.ts", "src/lib/foo.ts", "src/utils/bar.ts"];
|
|
65
|
+
|
|
66
|
+
return [
|
|
67
|
+
"## Decision Required",
|
|
68
|
+
"Needs changes before merge. Three concrete issues identified.",
|
|
69
|
+
"",
|
|
70
|
+
"## Highest-Risk Findings",
|
|
71
|
+
"",
|
|
72
|
+
`### 1. (mock) Missing input validation in ${refs[0]}`,
|
|
73
|
+
"**Severity:** High",
|
|
74
|
+
"**Where:** entry point of the diff",
|
|
75
|
+
"**Why:** Untrusted input flows into a parser without bounds checks.",
|
|
76
|
+
"**Suggested fix:** Add a Zod schema at the boundary; reject malformed input early.",
|
|
77
|
+
"",
|
|
78
|
+
`### 2. (mock) Unhandled rejection in ${refs[1] ?? refs[0]}`,
|
|
79
|
+
"**Severity:** Medium",
|
|
80
|
+
"**Where:** the awaited call without try/catch",
|
|
81
|
+
"**Why:** A network failure here will surface as an uncaught promise rejection.",
|
|
82
|
+
"**Suggested fix:** Wrap in try/catch, log to the audit channel, return a typed error.",
|
|
83
|
+
"",
|
|
84
|
+
`### 3. (mock) Missing test coverage on ${refs[2] ?? refs[0]}`,
|
|
85
|
+
"**Severity:** Medium",
|
|
86
|
+
"**Where:** the branch added in this diff",
|
|
87
|
+
"**Why:** The new conditional has no exercise in the test suite.",
|
|
88
|
+
"**Suggested fix:** Add a unit test that exercises the new branch with a representative payload.",
|
|
89
|
+
"",
|
|
90
|
+
"## Concrete Weaknesses",
|
|
91
|
+
`1. ${refs[0]}: input validation gap (see Finding 1).`,
|
|
92
|
+
`2. ${refs[1] ?? refs[0]}: unhandled promise rejection (see Finding 2).`,
|
|
93
|
+
`3. ${refs[2] ?? refs[0]}: no test coverage for the new branch (see Finding 3).`,
|
|
94
|
+
"",
|
|
95
|
+
"## Test Plan",
|
|
96
|
+
"- Run the existing unit suite; expect no regressions.",
|
|
97
|
+
"- Add the test described in Finding 3 and verify it fails before fix, passes after.",
|
|
98
|
+
"- Manually trigger the network-error path described in Finding 2.",
|
|
99
|
+
"",
|
|
100
|
+
"## Human Decision",
|
|
101
|
+
"_(blank — record via `almightygpt decide`)_",
|
|
102
|
+
"",
|
|
103
|
+
"---",
|
|
104
|
+
"_Mock adapter output — no real model was invoked. Replace `--reviewer mock` with `--reviewer codex` or another real adapter for production review._",
|
|
105
|
+
].join("\n");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private mockWork(userMessage: string): string {
|
|
109
|
+
return [
|
|
110
|
+
"## Plan",
|
|
111
|
+
"1. Understand the change requested.",
|
|
112
|
+
"2. Identify affected files and surfaces.",
|
|
113
|
+
"3. Write the implementation in small, testable steps.",
|
|
114
|
+
"4. Add or update tests.",
|
|
115
|
+
"5. Verify locally.",
|
|
116
|
+
"",
|
|
117
|
+
"## Notes",
|
|
118
|
+
"_(mock worker output — real Worker adapters produce code or detailed plans here.)_",
|
|
119
|
+
"",
|
|
120
|
+
`## Echo of input (first 200 chars):`,
|
|
121
|
+
"",
|
|
122
|
+
"> " + userMessage.slice(0, 200) + (userMessage.length > 200 ? "…" : ""),
|
|
123
|
+
].join("\n");
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI adapter — first real provider in MVP 1.
|
|
3
|
+
*
|
|
4
|
+
* Reads OPENAI_API_KEY from the environment. Never stores or logs the key.
|
|
5
|
+
* Default model: gpt-4o (best balance of quality and structured-output maturity
|
|
6
|
+
* for the Reviewer's JSON schema needs in later tasks). Configurable per-call.
|
|
7
|
+
*
|
|
8
|
+
* Pricing as of 2026-05 (USD per 1M tokens, gpt-4o):
|
|
9
|
+
* input: 2.50
|
|
10
|
+
* output: 10.00
|
|
11
|
+
* These are recorded in PRICING_USD_PER_1M below. Update when OpenAI changes
|
|
12
|
+
* their published rates.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import OpenAI from "openai";
|
|
16
|
+
import { AdapterError, type Adapter, type AdapterInput, type AdapterOutput } from "./types.js";
|
|
17
|
+
|
|
18
|
+
/** USD per 1M tokens, by model. Lowercased keys. */
|
|
19
|
+
const PRICING_USD_PER_1M: Record<string, { input: number; output: number }> = {
|
|
20
|
+
"gpt-4o": { input: 2.5, output: 10.0 },
|
|
21
|
+
"gpt-4o-mini": { input: 0.15, output: 0.6 },
|
|
22
|
+
"gpt-4-turbo": { input: 10.0, output: 30.0 },
|
|
23
|
+
"gpt-3.5-turbo": { input: 0.5, output: 1.5 },
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const DEFAULT_MODEL = "gpt-4o";
|
|
27
|
+
|
|
28
|
+
export interface OpenAIAdapterOptions {
|
|
29
|
+
/** Override the API key source. Defaults to process.env.OPENAI_API_KEY. */
|
|
30
|
+
apiKey?: string;
|
|
31
|
+
/** Default model used when execute() does not supply one. */
|
|
32
|
+
defaultModel?: string;
|
|
33
|
+
/** Default response token cap. */
|
|
34
|
+
defaultMaxOutputTokens?: number;
|
|
35
|
+
/** Default wall-clock timeout. */
|
|
36
|
+
defaultTimeoutMs?: number;
|
|
37
|
+
/** OpenAI organization id (optional). */
|
|
38
|
+
organization?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class OpenAIAdapter implements Adapter {
|
|
42
|
+
readonly name: string;
|
|
43
|
+
readonly provider = "openai";
|
|
44
|
+
|
|
45
|
+
private readonly client: OpenAI | null;
|
|
46
|
+
private readonly defaultModel: string;
|
|
47
|
+
private readonly defaultMaxOutputTokens: number;
|
|
48
|
+
private readonly defaultTimeoutMs: number;
|
|
49
|
+
|
|
50
|
+
constructor(
|
|
51
|
+
name = "codex",
|
|
52
|
+
options: OpenAIAdapterOptions = {},
|
|
53
|
+
) {
|
|
54
|
+
this.name = name;
|
|
55
|
+
this.defaultModel = options.defaultModel ?? DEFAULT_MODEL;
|
|
56
|
+
this.defaultMaxOutputTokens = options.defaultMaxOutputTokens ?? 4096;
|
|
57
|
+
this.defaultTimeoutMs = options.defaultTimeoutMs ?? 120_000;
|
|
58
|
+
|
|
59
|
+
const apiKey = options.apiKey ?? process.env["OPENAI_API_KEY"];
|
|
60
|
+
if (apiKey && apiKey.length > 0) {
|
|
61
|
+
this.client = new OpenAI({
|
|
62
|
+
apiKey,
|
|
63
|
+
...(options.organization ? { organization: options.organization } : {}),
|
|
64
|
+
});
|
|
65
|
+
} else {
|
|
66
|
+
this.client = null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async isAvailable(): Promise<boolean> {
|
|
71
|
+
return this.client !== null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async execute(input: AdapterInput): Promise<AdapterOutput> {
|
|
75
|
+
if (!this.client) {
|
|
76
|
+
throw new AdapterError(
|
|
77
|
+
"OPENAI_API_KEY is not set. Export it in your environment or pass it " +
|
|
78
|
+
"via the adapter constructor.",
|
|
79
|
+
this.name,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const model = input.model ?? this.defaultModel;
|
|
84
|
+
const maxOutputTokens = input.maxOutputTokens ?? this.defaultMaxOutputTokens;
|
|
85
|
+
const timeoutMs = input.timeoutMs ?? this.defaultTimeoutMs;
|
|
86
|
+
|
|
87
|
+
const start = Date.now();
|
|
88
|
+
let response: OpenAI.Chat.Completions.ChatCompletion;
|
|
89
|
+
try {
|
|
90
|
+
response = await this.client.chat.completions.create(
|
|
91
|
+
{
|
|
92
|
+
model,
|
|
93
|
+
max_tokens: maxOutputTokens,
|
|
94
|
+
messages: [
|
|
95
|
+
{ role: "system", content: input.systemPrompt },
|
|
96
|
+
{ role: "user", content: input.userMessage },
|
|
97
|
+
],
|
|
98
|
+
...(input.responseFormat === "json_object"
|
|
99
|
+
? { response_format: { type: "json_object" } }
|
|
100
|
+
: {}),
|
|
101
|
+
},
|
|
102
|
+
{ timeout: timeoutMs },
|
|
103
|
+
);
|
|
104
|
+
} catch (err) {
|
|
105
|
+
throw new AdapterError(
|
|
106
|
+
`OpenAI call failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
107
|
+
this.name,
|
|
108
|
+
err,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const choice = response.choices[0];
|
|
113
|
+
if (!choice || !choice.message.content) {
|
|
114
|
+
throw new AdapterError(
|
|
115
|
+
"OpenAI returned an empty response (no choices or empty content).",
|
|
116
|
+
this.name,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const content = choice.message.content;
|
|
121
|
+
const tokensIn = response.usage?.prompt_tokens ?? 0;
|
|
122
|
+
const tokensOut = response.usage?.completion_tokens ?? 0;
|
|
123
|
+
const costUsd = estimateCostUsd(model, tokensIn, tokensOut);
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
content,
|
|
127
|
+
tokensIn,
|
|
128
|
+
tokensOut,
|
|
129
|
+
costUsd,
|
|
130
|
+
latencyMs: Date.now() - start,
|
|
131
|
+
modelUsed: response.model ?? model,
|
|
132
|
+
provider: this.provider,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function estimateCostUsd(
|
|
138
|
+
model: string,
|
|
139
|
+
tokensIn: number,
|
|
140
|
+
tokensOut: number,
|
|
141
|
+
): number {
|
|
142
|
+
// Match by prefix so versioned model names (gpt-4o-2024-08-06) still resolve.
|
|
143
|
+
const key = Object.keys(PRICING_USD_PER_1M).find((k) =>
|
|
144
|
+
model.toLowerCase().startsWith(k),
|
|
145
|
+
);
|
|
146
|
+
if (!key) return 0;
|
|
147
|
+
const rates = PRICING_USD_PER_1M[key];
|
|
148
|
+
if (!rates) return 0;
|
|
149
|
+
return (tokensIn / 1_000_000) * rates.input + (tokensOut / 1_000_000) * rates.output;
|
|
150
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter interface — every provider implementation satisfies this.
|
|
3
|
+
*
|
|
4
|
+
* Memory files (CLAUDE.md, CODEX_AGENT.md, AGENTS.md) and rule files
|
|
5
|
+
* (.almightygpt/rules.md) are assembled by the review pipeline into the
|
|
6
|
+
* `systemPrompt` field. Adapters do not read filesystem state themselves;
|
|
7
|
+
* they take a fully-assembled prompt and return a fully-typed result.
|
|
8
|
+
*
|
|
9
|
+
* This keeps adapters thin and testable, and means swapping providers is
|
|
10
|
+
* a single-file change.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export type AgentRole = "worker" | "reviewer";
|
|
14
|
+
|
|
15
|
+
export interface AdapterInput {
|
|
16
|
+
/** worker | reviewer — drives the default system framing if a custom prompt is not supplied. */
|
|
17
|
+
role: AgentRole;
|
|
18
|
+
/** Fully assembled system prompt (memory file + rules + role framing). */
|
|
19
|
+
systemPrompt: string;
|
|
20
|
+
/** The actual task or critique target. */
|
|
21
|
+
userMessage: string;
|
|
22
|
+
/** Optional structured output mode. Defaults to text. */
|
|
23
|
+
responseFormat?: "text" | "json_object";
|
|
24
|
+
/** Optional override of the model name. Falls back to the adapter's default. */
|
|
25
|
+
model?: string;
|
|
26
|
+
/** Hard upper bound on response tokens. Adapters should pass this through. */
|
|
27
|
+
maxOutputTokens?: number;
|
|
28
|
+
/** Hard wall-clock timeout in ms. Adapter aborts if exceeded. */
|
|
29
|
+
timeoutMs?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface AdapterOutput {
|
|
33
|
+
/** The model's text response. */
|
|
34
|
+
content: string;
|
|
35
|
+
/** Input token count (prompt). */
|
|
36
|
+
tokensIn: number;
|
|
37
|
+
/** Output token count (completion). */
|
|
38
|
+
tokensOut: number;
|
|
39
|
+
/** Estimated USD cost based on the model's published rates. */
|
|
40
|
+
costUsd: number;
|
|
41
|
+
/** Wall-clock latency in milliseconds. */
|
|
42
|
+
latencyMs: number;
|
|
43
|
+
/** Concrete model name used (e.g. "gpt-4o-2024-08-06"). */
|
|
44
|
+
modelUsed: string;
|
|
45
|
+
/** Provider name (e.g. "openai", "anthropic"). */
|
|
46
|
+
provider: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface Adapter {
|
|
50
|
+
/** Identifier used in config.yaml (e.g. "codex", "claude", "mock"). */
|
|
51
|
+
readonly name: string;
|
|
52
|
+
/** Underlying provider family (e.g. "openai", "anthropic", "mock"). */
|
|
53
|
+
readonly provider: string;
|
|
54
|
+
/** True if credentials are available and the adapter can be invoked. */
|
|
55
|
+
isAvailable(): Promise<boolean>;
|
|
56
|
+
/** Execute one model call. */
|
|
57
|
+
execute(input: AdapterInput): Promise<AdapterOutput>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Thrown when an adapter cannot complete the call. Callers should treat this
|
|
62
|
+
* as a recoverable run failure (record the error, do not crash the orchestrator).
|
|
63
|
+
*/
|
|
64
|
+
export class AdapterError extends Error {
|
|
65
|
+
override readonly name = "AdapterError";
|
|
66
|
+
constructor(
|
|
67
|
+
message: string,
|
|
68
|
+
public readonly adapterName: string,
|
|
69
|
+
public override readonly cause?: unknown,
|
|
70
|
+
) {
|
|
71
|
+
super(message);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load and validate .almightygpt/config.yaml from a repo root.
|
|
3
|
+
*
|
|
4
|
+
* Returns a fully-typed Config object. Throws a ConfigError with a useful
|
|
5
|
+
* message if the file is missing, malformed, or fails schema validation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFile } from "node:fs/promises";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { parse as parseYaml } from "yaml";
|
|
11
|
+
import { ConfigSchema, type Config } from "./schema.js";
|
|
12
|
+
|
|
13
|
+
export class ConfigError extends Error {
|
|
14
|
+
override readonly name = "ConfigError";
|
|
15
|
+
constructor(
|
|
16
|
+
message: string,
|
|
17
|
+
public readonly path: string,
|
|
18
|
+
public override readonly cause?: unknown,
|
|
19
|
+
) {
|
|
20
|
+
super(message);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function loadConfig(repoRoot: string): Promise<Config> {
|
|
25
|
+
const path = join(repoRoot, ".almightygpt", "config.yaml");
|
|
26
|
+
let raw: string;
|
|
27
|
+
try {
|
|
28
|
+
raw = await readFile(path, "utf8");
|
|
29
|
+
} catch (err) {
|
|
30
|
+
throw new ConfigError(
|
|
31
|
+
`Could not read AlmightyGPT config. Run \`almightygpt init\` first.`,
|
|
32
|
+
path,
|
|
33
|
+
err,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let parsed: unknown;
|
|
38
|
+
try {
|
|
39
|
+
parsed = parseYaml(raw);
|
|
40
|
+
} catch (err) {
|
|
41
|
+
throw new ConfigError(
|
|
42
|
+
`Config YAML is malformed: ${err instanceof Error ? err.message : String(err)}`,
|
|
43
|
+
path,
|
|
44
|
+
err,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const result = ConfigSchema.safeParse(parsed);
|
|
49
|
+
if (!result.success) {
|
|
50
|
+
const issues = result.error.issues
|
|
51
|
+
.map((i) => ` - ${i.path.join(".")}: ${i.message}`)
|
|
52
|
+
.join("\n");
|
|
53
|
+
throw new ConfigError(
|
|
54
|
+
`Config validation failed:\n${issues}`,
|
|
55
|
+
path,
|
|
56
|
+
result.error,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return result.data;
|
|
61
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zod schema for .almightygpt/config.yaml.
|
|
3
|
+
*
|
|
4
|
+
* The schema is intentionally permissive on unknown fields (passthrough) so
|
|
5
|
+
* future features can land without forcing a config-bump. Required fields
|
|
6
|
+
* are minimal — most behavior has sensible defaults.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
|
|
11
|
+
export const AgentRoleSchema = z.enum(["worker", "reviewer", "both", "optional"]);
|
|
12
|
+
|
|
13
|
+
export const AgentConfigSchema = z
|
|
14
|
+
.object({
|
|
15
|
+
enabled: z.boolean().default(true),
|
|
16
|
+
provider: z.enum(["openai", "anthropic", "google", "mock"]),
|
|
17
|
+
mode: z.enum(["api", "cli"]).default("api"),
|
|
18
|
+
role: AgentRoleSchema.default("optional"),
|
|
19
|
+
memoryFile: z.string().min(1),
|
|
20
|
+
})
|
|
21
|
+
.passthrough();
|
|
22
|
+
|
|
23
|
+
export const ConfigSchema = z
|
|
24
|
+
.object({
|
|
25
|
+
version: z.literal(1),
|
|
26
|
+
reviewsDir: z.string().min(1).default("docs/codex-reviews"),
|
|
27
|
+
runsDir: z.string().min(1).default(".almightygpt/runs"),
|
|
28
|
+
agents: z.record(z.string(), AgentConfigSchema).default({}),
|
|
29
|
+
defaults: z
|
|
30
|
+
.object({
|
|
31
|
+
worker: z.string().min(1).optional(),
|
|
32
|
+
reviewer: z.string().min(1).optional(),
|
|
33
|
+
})
|
|
34
|
+
.default({}),
|
|
35
|
+
context: z
|
|
36
|
+
.object({
|
|
37
|
+
include: z.array(z.string()).default([]),
|
|
38
|
+
exclude: z.array(z.string()).default([]),
|
|
39
|
+
maxFileBytes: z.number().int().positive().default(120_000),
|
|
40
|
+
})
|
|
41
|
+
.default({}),
|
|
42
|
+
security: z
|
|
43
|
+
.object({
|
|
44
|
+
redactSecrets: z.boolean().default(true),
|
|
45
|
+
requireExternalProviderConfirmation: z.boolean().default(true),
|
|
46
|
+
})
|
|
47
|
+
.default({}),
|
|
48
|
+
budget: z
|
|
49
|
+
.object({
|
|
50
|
+
maxCostPerRunUsd: z.number().positive().default(0.5),
|
|
51
|
+
maxTokensPerRun: z.number().int().positive().default(100_000),
|
|
52
|
+
})
|
|
53
|
+
.default({}),
|
|
54
|
+
review: z
|
|
55
|
+
.object({
|
|
56
|
+
requireConcreteWeaknesses: z.number().int().nonnegative().default(3),
|
|
57
|
+
warnOnZeroFileReferences: z.boolean().default(true),
|
|
58
|
+
})
|
|
59
|
+
.default({}),
|
|
60
|
+
})
|
|
61
|
+
.passthrough();
|
|
62
|
+
|
|
63
|
+
export type AgentConfig = z.infer<typeof AgentConfigSchema>;
|
|
64
|
+
export type Config = z.infer<typeof ConfigSchema>;
|