@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.
Files changed (126) hide show
  1. package/dist/adapters/claude.d.ts +31 -0
  2. package/dist/adapters/claude.d.ts.map +1 -0
  3. package/dist/adapters/claude.js +90 -0
  4. package/dist/adapters/claude.js.map +1 -0
  5. package/dist/adapters/gemini.d.ts +42 -0
  6. package/dist/adapters/gemini.d.ts.map +1 -0
  7. package/dist/adapters/gemini.js +133 -0
  8. package/dist/adapters/gemini.js.map +1 -0
  9. package/dist/adapters/index.d.ts +16 -0
  10. package/dist/adapters/index.d.ts.map +1 -0
  11. package/dist/adapters/index.js +15 -0
  12. package/dist/adapters/index.js.map +1 -0
  13. package/dist/adapters/mock.d.ts +23 -0
  14. package/dist/adapters/mock.d.ts.map +1 -0
  15. package/dist/adapters/mock.js +107 -0
  16. package/dist/adapters/mock.js.map +1 -0
  17. package/dist/adapters/openai.d.ts +38 -0
  18. package/dist/adapters/openai.d.ts.map +1 -0
  19. package/dist/adapters/openai.js +105 -0
  20. package/dist/adapters/openai.js.map +1 -0
  21. package/dist/adapters/types.d.ts +65 -0
  22. package/dist/adapters/types.d.ts.map +1 -0
  23. package/dist/adapters/types.js +26 -0
  24. package/dist/adapters/types.js.map +1 -0
  25. package/dist/config/load.d.ts +15 -0
  26. package/dist/config/load.d.ts.map +1 -0
  27. package/dist/config/load.js +46 -0
  28. package/dist/config/load.js.map +1 -0
  29. package/dist/config/schema.d.ts +260 -0
  30. package/dist/config/schema.d.ts.map +1 -0
  31. package/dist/config/schema.js +58 -0
  32. package/dist/config/schema.js.map +1 -0
  33. package/dist/context/manifest.d.ts +58 -0
  34. package/dist/context/manifest.d.ts.map +1 -0
  35. package/dist/context/manifest.js +49 -0
  36. package/dist/context/manifest.js.map +1 -0
  37. package/dist/context/redact.d.ts +26 -0
  38. package/dist/context/redact.d.ts.map +1 -0
  39. package/dist/context/redact.js +67 -0
  40. package/dist/context/redact.js.map +1 -0
  41. package/dist/git/status.d.ts +48 -0
  42. package/dist/git/status.d.ts.map +1 -0
  43. package/dist/git/status.js +79 -0
  44. package/dist/git/status.js.map +1 -0
  45. package/dist/index.d.ts +33 -0
  46. package/dist/index.d.ts.map +1 -0
  47. package/dist/index.js +38 -0
  48. package/dist/index.js.map +1 -0
  49. package/dist/review/budget.d.ts +46 -0
  50. package/dist/review/budget.d.ts.map +1 -0
  51. package/dist/review/budget.js +83 -0
  52. package/dist/review/budget.js.map +1 -0
  53. package/dist/review/diff.d.ts +21 -0
  54. package/dist/review/diff.d.ts.map +1 -0
  55. package/dist/review/diff.js +55 -0
  56. package/dist/review/diff.js.map +1 -0
  57. package/dist/review/events.d.ts +76 -0
  58. package/dist/review/events.d.ts.map +1 -0
  59. package/dist/review/events.js +13 -0
  60. package/dist/review/events.js.map +1 -0
  61. package/dist/review/memory.d.ts +23 -0
  62. package/dist/review/memory.d.ts.map +1 -0
  63. package/dist/review/memory.js +42 -0
  64. package/dist/review/memory.js.map +1 -0
  65. package/dist/review/prompts.d.ts +34 -0
  66. package/dist/review/prompts.d.ts.map +1 -0
  67. package/dist/review/prompts.js +174 -0
  68. package/dist/review/prompts.js.map +1 -0
  69. package/dist/review/run-diff-review.d.ts +52 -0
  70. package/dist/review/run-diff-review.d.ts.map +1 -0
  71. package/dist/review/run-diff-review.js +258 -0
  72. package/dist/review/run-diff-review.js.map +1 -0
  73. package/dist/review/run-worker-reviewer.d.ts +72 -0
  74. package/dist/review/run-worker-reviewer.d.ts.map +1 -0
  75. package/dist/review/run-worker-reviewer.js +407 -0
  76. package/dist/review/run-worker-reviewer.js.map +1 -0
  77. package/dist/review/write.d.ts +44 -0
  78. package/dist/review/write.d.ts.map +1 -0
  79. package/dist/review/write.js +152 -0
  80. package/dist/review/write.js.map +1 -0
  81. package/dist/runs/decide.d.ts +45 -0
  82. package/dist/runs/decide.d.ts.map +1 -0
  83. package/dist/runs/decide.js +93 -0
  84. package/dist/runs/decide.js.map +1 -0
  85. package/dist/runs/folder.d.ts +42 -0
  86. package/dist/runs/folder.d.ts.map +1 -0
  87. package/dist/runs/folder.js +82 -0
  88. package/dist/runs/folder.js.map +1 -0
  89. package/dist/runs/list.d.ts +58 -0
  90. package/dist/runs/list.d.ts.map +1 -0
  91. package/dist/runs/list.js +117 -0
  92. package/dist/runs/list.js.map +1 -0
  93. package/dist/runs/types.d.ts +96 -0
  94. package/dist/runs/types.d.ts.map +1 -0
  95. package/dist/runs/types.js +13 -0
  96. package/dist/runs/types.js.map +1 -0
  97. package/dist/templates/install.d.ts +49 -0
  98. package/dist/templates/install.d.ts.map +1 -0
  99. package/dist/templates/install.js +154 -0
  100. package/dist/templates/install.js.map +1 -0
  101. package/package.json +34 -0
  102. package/src/adapters/claude.ts +133 -0
  103. package/src/adapters/gemini.ts +183 -0
  104. package/src/adapters/index.ts +21 -0
  105. package/src/adapters/mock.ts +125 -0
  106. package/src/adapters/openai.ts +150 -0
  107. package/src/adapters/types.ts +73 -0
  108. package/src/config/load.ts +61 -0
  109. package/src/config/schema.ts +64 -0
  110. package/src/context/manifest.ts +94 -0
  111. package/src/context/redact.ts +93 -0
  112. package/src/git/status.ts +108 -0
  113. package/src/index.ts +127 -0
  114. package/src/review/budget.ts +116 -0
  115. package/src/review/diff.ts +85 -0
  116. package/src/review/events.ts +86 -0
  117. package/src/review/memory.ts +57 -0
  118. package/src/review/prompts.ts +208 -0
  119. package/src/review/run-diff-review.ts +353 -0
  120. package/src/review/run-worker-reviewer.ts +528 -0
  121. package/src/review/write.ts +208 -0
  122. package/src/runs/decide.ts +153 -0
  123. package/src/runs/folder.ts +137 -0
  124. package/src/runs/list.ts +152 -0
  125. package/src/runs/types.ts +98 -0
  126. 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>;