@almightygpt/core 0.10.1 → 0.11.1

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/claude.d.ts.map +1 -1
  2. package/dist/adapters/claude.js +5 -0
  3. package/dist/adapters/claude.js.map +1 -1
  4. package/dist/adapters/defaults.d.ts.map +1 -1
  5. package/dist/adapters/defaults.js +5 -0
  6. package/dist/adapters/defaults.js.map +1 -1
  7. package/dist/adapters/factory.d.ts.map +1 -1
  8. package/dist/adapters/factory.js +11 -1
  9. package/dist/adapters/factory.js.map +1 -1
  10. package/dist/adapters/gemini.d.ts.map +1 -1
  11. package/dist/adapters/gemini.js +5 -0
  12. package/dist/adapters/gemini.js.map +1 -1
  13. package/dist/adapters/index.d.ts +9 -5
  14. package/dist/adapters/index.d.ts.map +1 -1
  15. package/dist/adapters/index.js +9 -5
  16. package/dist/adapters/index.js.map +1 -1
  17. package/dist/adapters/ollama.d.ts +47 -0
  18. package/dist/adapters/ollama.d.ts.map +1 -0
  19. package/dist/adapters/ollama.js +124 -0
  20. package/dist/adapters/ollama.js.map +1 -0
  21. package/dist/adapters/openai.d.ts.map +1 -1
  22. package/dist/adapters/openai.js +8 -0
  23. package/dist/adapters/openai.js.map +1 -1
  24. package/dist/adapters/openrouter.d.ts +50 -0
  25. package/dist/adapters/openrouter.d.ts.map +1 -0
  26. package/dist/adapters/openrouter.js +153 -0
  27. package/dist/adapters/openrouter.js.map +1 -0
  28. package/dist/auth/__tests__/resolver.test.js +18 -0
  29. package/dist/auth/__tests__/resolver.test.js.map +1 -1
  30. package/dist/auth/types.d.ts +1 -1
  31. package/dist/auth/types.d.ts.map +1 -1
  32. package/dist/auth/types.js +8 -0
  33. package/dist/auth/types.js.map +1 -1
  34. package/dist/auth/validator.d.ts.map +1 -1
  35. package/dist/auth/validator.js +127 -0
  36. package/dist/auth/validator.js.map +1 -1
  37. package/dist/config/schema.d.ts +12 -12
  38. package/dist/config/schema.d.ts.map +1 -1
  39. package/dist/config/schema.js +8 -1
  40. package/dist/config/schema.js.map +1 -1
  41. package/dist/index.d.ts +1 -1
  42. package/dist/index.js +1 -1
  43. package/package.json +1 -1
  44. package/src/adapters/claude.ts +8 -0
  45. package/src/adapters/defaults.ts +5 -0
  46. package/src/adapters/factory.ts +11 -1
  47. package/src/adapters/gemini.ts +8 -0
  48. package/src/adapters/index.ts +9 -5
  49. package/src/adapters/ollama.ts +157 -0
  50. package/src/adapters/openai.ts +11 -0
  51. package/src/adapters/openrouter.ts +202 -0
  52. package/src/auth/__tests__/resolver.test.ts +24 -0
  53. package/src/auth/types.ts +15 -1
  54. package/src/auth/validator.ts +135 -0
  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;
@@ -187,6 +201,105 @@ 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
+ // v0.12.1 (per Codex P2 #3): probe /api/tags AND check the
244
+ // configured default model is installed. Previously this only
245
+ // verified the daemon was running — users would see "valid" then
246
+ // their first review would fail because llama3.3:70b was never
247
+ // pulled. For a local-first provider, that's the wrong first
248
+ // impression.
249
+ const baseUrl = process.env["OLLAMA_BASE_URL"] ?? "http://localhost:11434";
250
+ const probeUrl = baseUrl.replace(/\/v1\/?$/, "") + "/api/tags";
251
+ const expectedModel = DEFAULT_MODELS.ollama;
252
+ const controller = new AbortController();
253
+ const timer = setTimeout(() => controller.abort(), 3000);
254
+ try {
255
+ const res = await fetch(probeUrl, { signal: controller.signal });
256
+ if (!res.ok) {
257
+ return {
258
+ ok: false,
259
+ statusCode: res.status,
260
+ error: `[${res.status}] Ollama daemon at ${baseUrl} responded with an error.`,
261
+ };
262
+ }
263
+ // Parse the model list — shape is { models: [{ name: string, ... }] }.
264
+ const data = (await res.json()) as { models?: Array<{ name?: string }> };
265
+ const installed = (data.models ?? [])
266
+ .map((m) => m.name)
267
+ .filter((n): n is string => typeof n === "string");
268
+
269
+ // Match strategy: exact name match, OR base-name match (e.g. the
270
+ // default "llama3.3:70b" should also be considered installed if
271
+ // the user only has "llama3.3:latest" or another tag of the same
272
+ // base model — close enough for "did you pull this family?").
273
+ const expectedBase = expectedModel.split(":")[0] ?? expectedModel;
274
+ const hasExact = installed.includes(expectedModel);
275
+ const hasBase = installed.some((n) => n.split(":")[0] === expectedBase);
276
+ if (!hasExact && !hasBase) {
277
+ return {
278
+ ok: false,
279
+ error:
280
+ `Ollama daemon at ${baseUrl} is running, but the configured ` +
281
+ `default model "${expectedModel}" is not installed locally. ` +
282
+ `Pull it first: ollama pull ${expectedModel}` +
283
+ (installed.length > 0
284
+ ? `\n(Installed models: ${installed.slice(0, 5).join(", ")}${installed.length > 5 ? "…" : ""})`
285
+ : "\n(No models installed yet.)"),
286
+ };
287
+ }
288
+
289
+ return { ok: true, model: expectedModel };
290
+ } catch (err) {
291
+ return {
292
+ ok: false,
293
+ error:
294
+ `Ollama daemon unreachable at ${baseUrl}. Is Ollama installed ` +
295
+ `and running? Try \`ollama serve\` or open the Ollama app.`,
296
+ rawBody: err instanceof Error ? err.message : String(err),
297
+ };
298
+ } finally {
299
+ clearTimeout(timer);
300
+ }
301
+ }
302
+
190
303
  // ─── Error normalization (Codex v0.8 P2 #6) ──────────────────────────
191
304
  //
192
305
  // Parse known provider JSON error shapes into short, user-safe messages.
@@ -258,6 +371,28 @@ function redactKey(msg: string, key: string): string {
258
371
  return msg.split(key).join("<redacted-key>");
259
372
  }
260
373
 
374
+ function normalizeOpenRouterError(
375
+ status: number,
376
+ rawBody: string,
377
+ submittedKey: string,
378
+ ): string {
379
+ // OpenRouter forwards provider errors but wraps them; shape is
380
+ // typically { "error": { "message": "...", "code": N } }.
381
+ try {
382
+ const parsed = JSON.parse(rawBody) as {
383
+ error?: { message?: string; code?: number };
384
+ };
385
+ let msg = parsed.error?.message ?? "";
386
+ if (submittedKey && msg.includes(submittedKey)) {
387
+ msg = msg.replace(submittedKey, "<redacted-key>");
388
+ }
389
+ if (msg) return `[${status}] OpenRouter: ${truncate(msg, 200)}`;
390
+ } catch {
391
+ /* fall through */
392
+ }
393
+ return statusOnlyMessage("OpenRouter", status);
394
+ }
395
+
261
396
  function statusOnlyMessage(provider: string, status: number): string {
262
397
  if (status === 401 || status === 403) {
263
398
  return `[${status}] ${provider} rejected the key (unauthorized).`;
@@ -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.1";
16
+ export const VERSION = "0.11.1";
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";