@almightygpt/core 0.10.0 → 0.11.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 (56) hide show
  1. package/dist/adapters/defaults.d.ts.map +1 -1
  2. package/dist/adapters/defaults.js +5 -0
  3. package/dist/adapters/defaults.js.map +1 -1
  4. package/dist/adapters/factory.d.ts.map +1 -1
  5. package/dist/adapters/factory.js +11 -1
  6. package/dist/adapters/factory.js.map +1 -1
  7. package/dist/adapters/index.d.ts +9 -5
  8. package/dist/adapters/index.d.ts.map +1 -1
  9. package/dist/adapters/index.js +9 -5
  10. package/dist/adapters/index.js.map +1 -1
  11. package/dist/adapters/ollama.d.ts +47 -0
  12. package/dist/adapters/ollama.d.ts.map +1 -0
  13. package/dist/adapters/ollama.js +124 -0
  14. package/dist/adapters/ollama.js.map +1 -0
  15. package/dist/adapters/openrouter.d.ts +50 -0
  16. package/dist/adapters/openrouter.d.ts.map +1 -0
  17. package/dist/adapters/openrouter.js +148 -0
  18. package/dist/adapters/openrouter.js.map +1 -0
  19. package/dist/auth/__tests__/keychain.test.d.ts +18 -0
  20. package/dist/auth/__tests__/keychain.test.d.ts.map +1 -0
  21. package/dist/auth/__tests__/keychain.test.js +155 -0
  22. package/dist/auth/__tests__/keychain.test.js.map +1 -0
  23. package/dist/auth/__tests__/resolver.test.d.ts +13 -0
  24. package/dist/auth/__tests__/resolver.test.d.ts.map +1 -0
  25. package/dist/auth/__tests__/resolver.test.js +200 -0
  26. package/dist/auth/__tests__/resolver.test.js.map +1 -0
  27. package/dist/auth/__tests__/validator.test.d.ts +15 -0
  28. package/dist/auth/__tests__/validator.test.d.ts.map +1 -0
  29. package/dist/auth/__tests__/validator.test.js +197 -0
  30. package/dist/auth/__tests__/validator.test.js.map +1 -0
  31. package/dist/auth/types.d.ts +1 -1
  32. package/dist/auth/types.d.ts.map +1 -1
  33. package/dist/auth/types.js +8 -0
  34. package/dist/auth/types.js.map +1 -1
  35. package/dist/auth/validator.d.ts.map +1 -1
  36. package/dist/auth/validator.js +117 -11
  37. package/dist/auth/validator.js.map +1 -1
  38. package/dist/config/schema.d.ts +12 -12
  39. package/dist/config/schema.d.ts.map +1 -1
  40. package/dist/config/schema.js +8 -1
  41. package/dist/config/schema.js.map +1 -1
  42. package/dist/index.d.ts +1 -1
  43. package/dist/index.js +1 -1
  44. package/package.json +4 -2
  45. package/src/adapters/defaults.ts +5 -0
  46. package/src/adapters/factory.ts +11 -1
  47. package/src/adapters/index.ts +9 -5
  48. package/src/adapters/ollama.ts +157 -0
  49. package/src/adapters/openrouter.ts +194 -0
  50. package/src/auth/__tests__/keychain.test.ts +171 -0
  51. package/src/auth/__tests__/resolver.test.ts +255 -0
  52. package/src/auth/__tests__/validator.test.ts +241 -0
  53. package/src/auth/types.ts +15 -1
  54. package/src/auth/validator.ts +130 -11
  55. package/src/config/schema.ts +8 -1
  56. package/src/index.ts +1 -1
@@ -72,6 +72,20 @@ export async function validateKey(
72
72
  case "google":
73
73
  result = await validateGoogle(key);
74
74
  break;
75
+ case "openrouter":
76
+ result = await validateOpenRouter(key);
77
+ break;
78
+ case "ollama":
79
+ // Ollama has no key — "validation" means probing the local
80
+ // daemon. The key arg is ignored.
81
+ result = await validateOllama();
82
+ break;
83
+ default: {
84
+ // Exhaustiveness check — TypeScript will error here if a new
85
+ // ProviderId is added without a switch case.
86
+ const _exhaustive: never = provider;
87
+ throw new Error(`Unknown provider: ${String(_exhaustive)}`);
88
+ }
75
89
  }
76
90
  result.latencyMs = Date.now() - start;
77
91
  return result;
@@ -107,7 +121,7 @@ async function validateOpenAI(key: string): Promise<ValidationResult> {
107
121
  return {
108
122
  ok: false,
109
123
  statusCode: res.status,
110
- error: normalizeOpenAIError(res.status, rawBody),
124
+ error: normalizeOpenAIError(res.status, rawBody, key),
111
125
  rawBody,
112
126
  };
113
127
  }
@@ -142,7 +156,7 @@ async function validateAnthropic(key: string): Promise<ValidationResult> {
142
156
  return {
143
157
  ok: false,
144
158
  statusCode: res.status,
145
- error: normalizeAnthropicError(res.status, rawBody),
159
+ error: normalizeAnthropicError(res.status, rawBody, key),
146
160
  rawBody,
147
161
  };
148
162
  }
@@ -187,34 +201,112 @@ async function validateGoogle(key: string): Promise<ValidationResult> {
187
201
  }
188
202
  }
189
203
 
204
+ async function validateOpenRouter(key: string): Promise<ValidationResult> {
205
+ // OpenRouter is OpenAI-compatible at https://openrouter.ai/api/v1.
206
+ // Same chat-completions shape — point fetch at OR's base URL.
207
+ const model = DEFAULT_MODELS.openrouter;
208
+ const controller = new AbortController();
209
+ const timer = setTimeout(() => controller.abort(), VALIDATION_TIMEOUT_MS);
210
+ try {
211
+ const res = await fetch("https://openrouter.ai/api/v1/chat/completions", {
212
+ method: "POST",
213
+ headers: {
214
+ "content-type": "application/json",
215
+ authorization: `Bearer ${key}`,
216
+ "HTTP-Referer": "https://github.com/roxjayanath/almightygpt",
217
+ "X-Title": "AlmightyGPT validation",
218
+ },
219
+ body: JSON.stringify({
220
+ model,
221
+ messages: [{ role: "user", content: "hi" }],
222
+ max_tokens: 1,
223
+ }),
224
+ signal: controller.signal,
225
+ });
226
+ if (!res.ok) {
227
+ const rawBody = await res.text().catch(() => "");
228
+ return {
229
+ ok: false,
230
+ statusCode: res.status,
231
+ error: normalizeOpenRouterError(res.status, rawBody, key),
232
+ rawBody,
233
+ };
234
+ }
235
+ const data = (await res.json()) as { model?: string };
236
+ return { ok: true, model: data.model ?? model };
237
+ } finally {
238
+ clearTimeout(timer);
239
+ }
240
+ }
241
+
242
+ async function validateOllama(): Promise<ValidationResult> {
243
+ // No key. Probe the local daemon at /api/tags — cheap, no model
244
+ // invocation. If the user has Ollama running, this confirms the
245
+ // adapter can reach it. Doesn't confirm the default model is
246
+ // pulled — that surfaces on the first real call.
247
+ const baseUrl = process.env["OLLAMA_BASE_URL"] ?? "http://localhost:11434";
248
+ const probeUrl = baseUrl.replace(/\/v1\/?$/, "") + "/api/tags";
249
+ const controller = new AbortController();
250
+ const timer = setTimeout(() => controller.abort(), 3000);
251
+ try {
252
+ const res = await fetch(probeUrl, { signal: controller.signal });
253
+ if (!res.ok) {
254
+ return {
255
+ ok: false,
256
+ statusCode: res.status,
257
+ error: `[${res.status}] Ollama daemon at ${baseUrl} responded with an error.`,
258
+ };
259
+ }
260
+ return { ok: true, model: DEFAULT_MODELS.ollama };
261
+ } catch (err) {
262
+ return {
263
+ ok: false,
264
+ error:
265
+ `Ollama daemon unreachable at ${baseUrl}. Is Ollama installed ` +
266
+ `and running? Try \`ollama serve\` or open the Ollama app.`,
267
+ rawBody: err instanceof Error ? err.message : String(err),
268
+ };
269
+ } finally {
270
+ clearTimeout(timer);
271
+ }
272
+ }
273
+
190
274
  // ─── Error normalization (Codex v0.8 P2 #6) ──────────────────────────
191
275
  //
192
276
  // Parse known provider JSON error shapes into short, user-safe messages.
193
277
  // Never echo the raw key back even by accident (defense in depth: we
194
278
  // also redact anything that looks like the submitted key).
195
279
 
196
- function normalizeOpenAIError(status: number, rawBody: string): string {
280
+ function normalizeOpenAIError(
281
+ status: number,
282
+ rawBody: string,
283
+ submittedKey: string,
284
+ ): string {
197
285
  // OpenAI shape: { "error": { "message": "...", "type": "...", "code": "..." } }
198
286
  try {
199
287
  const parsed = JSON.parse(rawBody) as {
200
288
  error?: { message?: string; type?: string; code?: string };
201
289
  };
202
290
  const msg = parsed.error?.message;
203
- if (msg) return `[${status}] OpenAI: ${truncate(msg, 200)}`;
291
+ if (msg) return `[${status}] OpenAI: ${truncate(redactKey(msg, submittedKey), 200)}`;
204
292
  } catch {
205
293
  /* fall through */
206
294
  }
207
295
  return statusOnlyMessage("OpenAI", status);
208
296
  }
209
297
 
210
- function normalizeAnthropicError(status: number, rawBody: string): string {
298
+ function normalizeAnthropicError(
299
+ status: number,
300
+ rawBody: string,
301
+ submittedKey: string,
302
+ ): string {
211
303
  // Anthropic shape: { "type": "error", "error": { "type": "...", "message": "..." } }
212
304
  try {
213
305
  const parsed = JSON.parse(rawBody) as {
214
306
  error?: { type?: string; message?: string };
215
307
  };
216
308
  const msg = parsed.error?.message;
217
- if (msg) return `[${status}] Anthropic: ${truncate(msg, 200)}`;
309
+ if (msg) return `[${status}] Anthropic: ${truncate(redactKey(msg, submittedKey), 200)}`;
218
310
  } catch {
219
311
  /* fall through */
220
312
  }
@@ -231,18 +323,45 @@ function normalizeGoogleError(
231
323
  const parsed = JSON.parse(rawBody) as {
232
324
  error?: { code?: number; message?: string; status?: string };
233
325
  };
326
+ const msg = parsed.error?.message ?? "";
327
+ if (msg) return `[${status}] Google: ${truncate(redactKey(msg, submittedKey), 200)}`;
328
+ } catch {
329
+ /* fall through */
330
+ }
331
+ return statusOnlyMessage("Google", status);
332
+ }
333
+
334
+ /**
335
+ * Belt-and-braces: if a provider echoes the submitted key in its
336
+ * error body, redact before surfacing to the user. Codex's v0.8 P2 #6
337
+ * found this gap (originally Google-only); now applied to all three
338
+ * providers via this shared helper.
339
+ */
340
+ function redactKey(msg: string, key: string): string {
341
+ if (!key || !msg.includes(key)) return msg;
342
+ return msg.split(key).join("<redacted-key>");
343
+ }
344
+
345
+ function normalizeOpenRouterError(
346
+ status: number,
347
+ rawBody: string,
348
+ submittedKey: string,
349
+ ): string {
350
+ // OpenRouter forwards provider errors but wraps them; shape is
351
+ // typically { "error": { "message": "...", "code": N } }.
352
+ try {
353
+ const parsed = JSON.parse(rawBody) as {
354
+ error?: { message?: string; code?: number };
355
+ };
234
356
  let msg = parsed.error?.message ?? "";
235
- // Belt-and-braces redaction: Google sometimes echoes the key in
236
- // error messages (e.g. "API key not valid. Pass a valid API key.")
237
- // — we don't ship the actual key value if it ever ends up here.
238
357
  if (submittedKey && msg.includes(submittedKey)) {
239
358
  msg = msg.replace(submittedKey, "<redacted-key>");
240
359
  }
241
- if (msg) return `[${status}] Google: ${truncate(msg, 200)}`;
360
+ if (msg) return `[${status}] OpenRouter: ${truncate(msg, 200)}`;
242
361
  } catch {
243
362
  /* fall through */
244
363
  }
245
- return statusOnlyMessage("Google", status);
364
+ return statusOnlyMessage("OpenRouter", status);
246
365
  }
247
366
 
248
367
  function statusOnlyMessage(provider: string, status: number): string {
@@ -13,7 +13,14 @@ export const AgentRoleSchema = z.enum(["worker", "reviewer", "both", "optional"]
13
13
  export const AgentConfigSchema = z
14
14
  .object({
15
15
  enabled: z.boolean().default(true),
16
- provider: z.enum(["openai", "anthropic", "google", "mock"]),
16
+ provider: z.enum([
17
+ "openai",
18
+ "anthropic",
19
+ "google",
20
+ "openrouter",
21
+ "ollama",
22
+ "mock",
23
+ ]),
17
24
  mode: z.enum(["api", "cli"]).default("api"),
18
25
  role: AgentRoleSchema.default("optional"),
19
26
  memoryFile: z.string().min(1),
package/src/index.ts CHANGED
@@ -13,7 +13,7 @@
13
13
  * - budget/ ✅ task #14 BudgetTracker + BudgetExceededError
14
14
  */
15
15
 
16
- export const VERSION = "0.10.0";
16
+ export const VERSION = "0.11.0";
17
17
 
18
18
  // MCP server (v0.9.0+) — exposes AlmightyGPT's review surface as MCP tools.
19
19
  export { startMcpServer } from "./mcp/server.js";