quoroom 0.1.29 → 0.1.30

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/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
10
10
  [![npm version](https://img.shields.io/npm/v/quoroom)](https://www.npmjs.com/package/quoroom)
11
- [![Tests](https://img.shields.io/badge/tests-1083%20passing-brightgreen)](#)
11
+ [![Tests](https://img.shields.io/badge/tests-1088%20passing-brightgreen)](#)
12
12
  [![GitHub stars](https://img.shields.io/github/stars/quoroom-ai/room)](https://github.com/quoroom-ai/room/stargazers)
13
13
  [![macOS](https://img.shields.io/badge/macOS-.pkg-000000?logo=apple&logoColor=white)](https://github.com/quoroom-ai/room/releases/latest)
14
14
  [![Windows](https://img.shields.io/badge/Windows-.exe-0078D4?logo=windows&logoColor=white)](https://github.com/quoroom-ai/room/releases/latest)
@@ -59,7 +59,7 @@ The architecture draws from swarm intelligence research: decentralized decision-
59
59
 
60
60
  Quoroom is an open research project exploring autonomous agent collectives. Each collective (a **Room**) is a self-governing swarm of agents.
61
61
 
62
- - **Queen** — strategic brain, supports Claude/Codex subscriptions and OpenAI/Claude API
62
+ - **Queen** — strategic brain, supports Claude/Codex subscriptions and OpenAI/Claude/Gemini API
63
63
  - **Workers** — specialized agents that use the queen model
64
64
  - **Quorum** — agents deliberate and vote on decisions
65
65
  - **Keeper** — the human who sets goals and funds the wallet
@@ -250,7 +250,7 @@ Open **http://localhost:3700** (or the port shown in your terminal). The dashboa
250
250
  The **Clerk** tab is your global assistant for the whole local system (not a single room).
251
251
 
252
252
  - Clerk is a full assistant, not only commentary: it can reason, remember, and execute actions for the keeper
253
- - Setup paths: Claude subscription (`claude`), Codex subscription (`codex`), OpenAI API (`openai:gpt-4o-mini`), Anthropic API (`anthropic:claude-3-5-sonnet-latest`)
253
+ - Setup paths: Claude subscription (`claude`), Codex subscription (`codex`), OpenAI API (`openai:gpt-4o-mini`), Anthropic API (`anthropic:claude-3-5-sonnet-latest`), Gemini API (`gemini:gemini-2.5-flash`)
254
254
  - API keys entered in Clerk Setup are validated before saving
255
255
  - Clerk can answer and do: room lifecycle, room settings, task creation, reminders, inter-room messaging, and keeper communication
256
256
  - Clerk can act proactively through scheduled tasks/reminders and activity-driven commentary
@@ -260,9 +260,9 @@ The **Clerk** tab is your global assistant for the whole local system (not a sin
260
260
 
261
261
  API key resolution for Clerk API models:
262
262
 
263
- 1. Any room credential (`openai_api_key` or `anthropic_api_key`)
264
- 2. Clerk-saved API key (`clerk_openai_api_key` / `clerk_anthropic_api_key`)
265
- 3. Environment variable (`OPENAI_API_KEY` / `ANTHROPIC_API_KEY`)
263
+ 1. Any room credential (`openai_api_key`, `anthropic_api_key`, or `gemini_api_key`)
264
+ 2. Clerk-saved API key (`clerk_openai_api_key` / `clerk_anthropic_api_key` / `clerk_gemini_api_key`)
265
+ 3. Environment variable (`OPENAI_API_KEY` / `ANTHROPIC_API_KEY` / `GEMINI_API_KEY`)
266
266
 
267
267
  See full guide: [docs/CLERK.md](docs/CLERK.md)
268
268
 
@@ -536,10 +536,11 @@ Use your existing Claude or ChatGPT subscription, or bring an API key.
536
536
  | `codex` | OpenAI Codex CLI | Spawns CLI process | `npm i -g @openai/codex` |
537
537
  | `openai:gpt-4o-mini` | OpenAI API | HTTP REST | `OPENAI_API_KEY` |
538
538
  | `anthropic:claude-3-5-sonnet-latest` | Anthropic API | HTTP REST | `ANTHROPIC_API_KEY` |
539
+ | `gemini:gemini-2.5-flash` | Gemini API | HTTP REST | `GEMINI_API_KEY` |
539
540
 
540
541
  **CLI models** (`claude`, `codex`) — Full agentic loop with tool use via the CLI. Session continuity via `--resume`. On Windows, `.cmd` wrappers are auto-resolved to underlying `.js` scripts to bypass the cmd.exe 8191-char argument limit.
541
542
 
542
- **API models** (`openai:*`, `anthropic:*`) — Direct HTTP calls. Support multi-turn tool-calling loops. API keys resolve from: room credentials → Clerk-saved keys → environment variables. `anthropic:*` also accepts the `claude-api:` prefix.
543
+ **API models** (`openai:*`, `anthropic:*`, `gemini:*`) — Direct HTTP calls. Support multi-turn tool-calling loops. API keys resolve from: room credentials → Clerk-saved keys → environment variables. `anthropic:*` also accepts the `claude-api:` prefix. `gemini:*` uses Google's OpenAI-compatible endpoint.
543
544
 
544
545
  Workers inherit the queen's model by default, or can use a separate API model.
545
546
 
@@ -9914,7 +9914,7 @@ var require_package = __commonJS({
9914
9914
  "package.json"(exports2, module2) {
9915
9915
  module2.exports = {
9916
9916
  name: "quoroom",
9917
- version: "0.1.29",
9917
+ version: "0.1.30",
9918
9918
  description: "Autonomous AI agent collective engine \u2014 Queen, Workers, Quorum",
9919
9919
  main: "./out/mcp/server.js",
9920
9920
  bin: {
@@ -13045,7 +13045,9 @@ function getClerkUsageToday(db2, source) {
13045
13045
  };
13046
13046
  }
13047
13047
  function clerkApiKeySetting(provider) {
13048
- return provider === "openai_api" ? "clerk_openai_api_key" : "clerk_anthropic_api_key";
13048
+ if (provider === "openai_api") return "clerk_openai_api_key";
13049
+ if (provider === "gemini_api") return "clerk_gemini_api_key";
13050
+ return "clerk_anthropic_api_key";
13049
13051
  }
13050
13052
  function setClerkApiKey(db2, provider, value) {
13051
13053
  const trimmed = value.trim();
@@ -23388,7 +23390,7 @@ async function executeAgent(options) {
23388
23390
  if (model === "codex" || model.startsWith("codex:")) {
23389
23391
  return executeCodex(options);
23390
23392
  }
23391
- if (model === "openai" || model.startsWith("openai:")) {
23393
+ if (model === "openai" || model.startsWith("openai:") || model === "gemini" || model.startsWith("gemini:")) {
23392
23394
  if (options.toolDefs && options.toolDefs.length > 0 && options.onToolCall) {
23393
23395
  return executeOpenAiWithTools(options);
23394
23396
  }
@@ -23558,10 +23560,34 @@ async function executeCodex(options) {
23558
23560
  });
23559
23561
  });
23560
23562
  }
23563
+ function resolveOpenAiCompatible(model, apiKeyOverride) {
23564
+ const trimmed = model.trim();
23565
+ if (trimmed === "gemini" || trimmed.startsWith("gemini:")) {
23566
+ const apiKey2 = apiKeyOverride?.trim() || (process.env.GEMINI_API_KEY || "").trim();
23567
+ if (!apiKey2) return null;
23568
+ return {
23569
+ apiKey: apiKey2,
23570
+ url: "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions",
23571
+ defaultModel: "gemini-2.5-flash",
23572
+ label: "Gemini",
23573
+ prefix: "gemini"
23574
+ };
23575
+ }
23576
+ const apiKey = apiKeyOverride?.trim() || (process.env.OPENAI_API_KEY || "").trim();
23577
+ if (!apiKey) return null;
23578
+ return {
23579
+ apiKey,
23580
+ url: "https://api.openai.com/v1/chat/completions",
23581
+ defaultModel: "gpt-4o-mini",
23582
+ label: "OpenAI",
23583
+ prefix: "openai"
23584
+ };
23585
+ }
23561
23586
  async function executeOpenAiWithTools(options) {
23562
- const apiKey = options.apiKey?.trim() || (process.env.OPENAI_API_KEY || "").trim();
23563
- if (!apiKey) return immediateError("Missing OpenAI API key.");
23564
- const modelName = parseModelSuffix(options.model, "openai") || "gpt-4o-mini";
23587
+ const config = resolveOpenAiCompatible(options.model, options.apiKey);
23588
+ if (!config) return immediateError(`Missing ${options.model.startsWith("gemini") ? "Gemini" : "OpenAI"} API key.`);
23589
+ const { apiKey, url: apiUrl, defaultModel, label: providerLabel, prefix } = config;
23590
+ const modelName = parseModelSuffix(options.model, prefix) || defaultModel;
23565
23591
  const startTime = Date.now();
23566
23592
  const maxTurns = options.maxTurns ?? 10;
23567
23593
  const previousTurns = options.previousMessages ?? [];
@@ -23585,7 +23611,7 @@ Continue working toward the goal.` : options.prompt
23585
23611
  const timer = setTimeout(() => controller.abort(), timeoutMs);
23586
23612
  let json;
23587
23613
  try {
23588
- const response = await fetch("https://api.openai.com/v1/chat/completions", {
23614
+ const response = await fetch(apiUrl, {
23589
23615
  method: "POST",
23590
23616
  headers: { "Authorization": `Bearer ${apiKey}`, "Content-Type": "application/json" },
23591
23617
  body: JSON.stringify({ model: modelName, messages, tools: options.toolDefs }),
@@ -23593,7 +23619,7 @@ Continue working toward the goal.` : options.prompt
23593
23619
  });
23594
23620
  json = await response.json();
23595
23621
  if (!response.ok) {
23596
- return { output: `OpenAI API ${response.status}: ${extractApiError(json)}`, exitCode: 1, durationMs: Date.now() - startTime, sessionId: null, timedOut: false, usage: { inputTokens: totalInputTokens, outputTokens: totalOutputTokens } };
23622
+ return { output: `${providerLabel} API ${response.status}: ${extractApiError(json)}`, exitCode: 1, durationMs: Date.now() - startTime, sessionId: null, timedOut: false, usage: { inputTokens: totalInputTokens, outputTokens: totalOutputTokens } };
23597
23623
  }
23598
23624
  } catch (err) {
23599
23625
  const msg2 = err instanceof Error ? err.message : String(err);
@@ -23737,11 +23763,13 @@ Continue working toward the goal.` : options.prompt
23737
23763
  return { output: finalOutput || "Actions completed.", exitCode: 0, durationMs: Date.now() - startTime, sessionId: null, timedOut: false, usage: { inputTokens: totalInputTokens, outputTokens: totalOutputTokens } };
23738
23764
  }
23739
23765
  async function executeOpenAiApi(options) {
23740
- const apiKey = options.apiKey?.trim() || (process.env.OPENAI_API_KEY || "").trim();
23741
- if (!apiKey) {
23742
- return immediateError('Missing OpenAI API key. Set room credential "openai_api_key" or OPENAI_API_KEY.');
23766
+ const config = resolveOpenAiCompatible(options.model, options.apiKey);
23767
+ if (!config) {
23768
+ const isGemini = options.model.startsWith("gemini");
23769
+ return immediateError(`Missing ${isGemini ? "Gemini" : "OpenAI"} API key. Set room credential "${isGemini ? "gemini_api_key" : "openai_api_key"}" or ${isGemini ? "GEMINI_API_KEY" : "OPENAI_API_KEY"}.`);
23743
23770
  }
23744
- const modelName = parseModelSuffix(options.model, "openai") || "gpt-4o-mini";
23771
+ const { apiKey, url: apiUrl, defaultModel, label: providerLabel, prefix } = config;
23772
+ const modelName = parseModelSuffix(options.model, prefix) || defaultModel;
23745
23773
  const messages = [];
23746
23774
  if (options.systemPrompt) messages.push({ role: "system", content: options.systemPrompt });
23747
23775
  messages.push({ role: "user", content: options.prompt });
@@ -23750,7 +23778,7 @@ async function executeOpenAiApi(options) {
23750
23778
  const timeoutMs = options.timeoutMs ?? DEFAULT_HTTP_TIMEOUT_MS;
23751
23779
  const timer = setTimeout(() => controller.abort(), timeoutMs);
23752
23780
  try {
23753
- const response = await fetch("https://api.openai.com/v1/chat/completions", {
23781
+ const response = await fetch(apiUrl, {
23754
23782
  method: "POST",
23755
23783
  headers: {
23756
23784
  "Authorization": `Bearer ${apiKey}`,
@@ -23765,7 +23793,7 @@ async function executeOpenAiApi(options) {
23765
23793
  const json = await response.json();
23766
23794
  if (!response.ok) {
23767
23795
  return {
23768
- output: `OpenAI API ${response.status}: ${extractApiError(json)}`,
23796
+ output: `${providerLabel} API ${response.status}: ${extractApiError(json)}`,
23769
23797
  exitCode: 1,
23770
23798
  durationMs: Date.now() - startTime,
23771
23799
  sessionId: null,
@@ -23981,13 +24009,13 @@ Respond ONLY with a JSON object (no markdown, no explanation):
23981
24009
  }`;
23982
24010
  const timeoutMs = 6e4;
23983
24011
  try {
23984
- if (model === "openai" || model.startsWith("openai:")) {
23985
- const key = apiKey?.trim() || (process.env.OPENAI_API_KEY || "").trim();
23986
- if (!key) return null;
23987
- const modelName = parseModelSuffix(model, "openai") || "gpt-4o-mini";
23988
- const response = await fetch("https://api.openai.com/v1/chat/completions", {
24012
+ if (model === "openai" || model.startsWith("openai:") || model === "gemini" || model.startsWith("gemini:")) {
24013
+ const config = resolveOpenAiCompatible(model, apiKey);
24014
+ if (!config) return null;
24015
+ const modelName = parseModelSuffix(model, config.prefix) || config.defaultModel;
24016
+ const response = await fetch(config.url, {
23989
24017
  method: "POST",
23990
- headers: { "Authorization": `Bearer ${key}`, "Content-Type": "application/json" },
24018
+ headers: { "Authorization": `Bearer ${config.apiKey}`, "Content-Type": "application/json" },
23991
24019
  body: JSON.stringify({ model: modelName, messages: [{ role: "user", content: compressionPrompt }] }),
23992
24020
  signal: AbortSignal.timeout(timeoutMs)
23993
24021
  });
@@ -24017,16 +24045,17 @@ async function executeApiOnStation(cloudRoomId, stationId, options) {
24017
24045
  if (!apiKey) {
24018
24046
  return immediateError("Missing API key for station execution.");
24019
24047
  }
24020
- const isOpenAi = options.model.startsWith("openai:");
24048
+ const isOpenAiCompat = options.model.startsWith("openai:") || options.model.startsWith("gemini:");
24021
24049
  const messages = [];
24022
24050
  if (options.systemPrompt) messages.push({ role: "system", content: options.systemPrompt });
24023
24051
  messages.push({ role: "user", content: options.prompt });
24024
24052
  let url;
24025
24053
  let headers;
24026
24054
  let body;
24027
- if (isOpenAi) {
24028
- const modelName = parseModelSuffix(options.model, "openai") || "gpt-4o-mini";
24029
- url = "https://api.openai.com/v1/chat/completions";
24055
+ if (isOpenAiCompat) {
24056
+ const config = resolveOpenAiCompatible(options.model, apiKey);
24057
+ const modelName = config ? parseModelSuffix(options.model, config.prefix) || config.defaultModel : parseModelSuffix(options.model, "openai") || "gpt-4o-mini";
24058
+ url = config?.url ?? "https://api.openai.com/v1/chat/completions";
24030
24059
  headers = { "Authorization": `Bearer ${apiKey}`, "Content-Type": "application/json" };
24031
24060
  body = JSON.stringify({ model: modelName, messages });
24032
24061
  } else {
@@ -24064,7 +24093,7 @@ async function executeApiOnStation(cloudRoomId, stationId, options) {
24064
24093
  }
24065
24094
  try {
24066
24095
  const parsed = JSON.parse(result.stdout);
24067
- const output = isOpenAi ? extractOpenAiText(parsed) : extractAnthropicText(parsed);
24096
+ const output = isOpenAiCompat ? extractOpenAiText(parsed) : extractAnthropicText(parsed);
24068
24097
  return { output, exitCode: 0, durationMs: Date.now() - startTime, sessionId: null, timedOut: false };
24069
24098
  } catch {
24070
24099
  return {
@@ -24385,6 +24414,7 @@ function getModelProvider(model) {
24385
24414
  if (normalized === "anthropic" || normalized.startsWith("anthropic:") || normalized.startsWith("claude-api:")) {
24386
24415
  return "anthropic_api";
24387
24416
  }
24417
+ if (normalized === "gemini" || normalized.startsWith("gemini:")) return "gemini_api";
24388
24418
  return "claude_subscription";
24389
24419
  }
24390
24420
  async function getModelAuthStatus(db2, roomId, model) {
@@ -24395,6 +24425,9 @@ async function getModelAuthStatus(db2, roomId, model) {
24395
24425
  if (provider === "anthropic_api") {
24396
24426
  return resolveApiAuthStatus(db2, roomId, "anthropic_api_key", "ANTHROPIC_API_KEY", provider);
24397
24427
  }
24428
+ if (provider === "gemini_api") {
24429
+ return resolveApiAuthStatus(db2, roomId, "gemini_api_key", "GEMINI_API_KEY", provider);
24430
+ }
24398
24431
  let ready = false;
24399
24432
  if (provider === "claude_subscription") {
24400
24433
  ready = checkClaudeCliAvailable().available;
@@ -24420,6 +24453,9 @@ function resolveApiKeyForModel(db2, roomId, model) {
24420
24453
  if (provider === "anthropic_api") {
24421
24454
  return resolveApiKey(db2, roomId, "anthropic_api_key", "ANTHROPIC_API_KEY");
24422
24455
  }
24456
+ if (provider === "gemini_api") {
24457
+ return resolveApiKey(db2, roomId, "gemini_api_key", "GEMINI_API_KEY");
24458
+ }
24423
24459
  return void 0;
24424
24460
  }
24425
24461
  function resolveApiAuthStatus(db2, roomId, credentialName, envVar, provider) {
@@ -24471,6 +24507,9 @@ function getClerkCredential(db2, credentialName) {
24471
24507
  if (credentialName === "anthropic_api_key") {
24472
24508
  return getClerkApiKey(db2, "anthropic_api");
24473
24509
  }
24510
+ if (credentialName === "gemini_api_key") {
24511
+ return getClerkApiKey(db2, "gemini_api");
24512
+ }
24474
24513
  return null;
24475
24514
  }
24476
24515
  function getRoomCredential(db2, roomId, credentialName) {
@@ -25811,10 +25850,10 @@ async function runCycle(db2, roomId, worker, maxTurns, options) {
25811
25850
  options?.onCycleLifecycle?.("created", cycle.id, roomId);
25812
25851
  try {
25813
25852
  const provider = getModelProvider(model);
25814
- if (provider === "openai_api" || provider === "anthropic_api") {
25853
+ if (provider === "openai_api" || provider === "anthropic_api" || provider === "gemini_api") {
25815
25854
  const apiKeyCheck = resolveApiKeyForModel(db2, roomId, model);
25816
25855
  if (!apiKeyCheck) {
25817
- const label = provider === "openai_api" ? "OpenAI" : "Anthropic";
25856
+ const label = provider === "openai_api" ? "OpenAI" : provider === "gemini_api" ? "Gemini" : "Anthropic";
25818
25857
  const msg = `Missing ${label} API key. Set it in Room Settings or the Setup Guide.`;
25819
25858
  logBuffer2.addSynthetic("error", msg);
25820
25859
  logBuffer2.flush();
@@ -27835,8 +27874,8 @@ function maskKey2(key) {
27835
27874
  return `${trimmed.slice(0, 7)}...${trimmed.slice(-4)}`;
27836
27875
  }
27837
27876
  function getClerkApiAuthState(db2, provider) {
27838
- const credentialName = provider === "openai_api" ? "openai_api_key" : "anthropic_api_key";
27839
- const envVar = provider === "openai_api" ? "OPENAI_API_KEY" : "ANTHROPIC_API_KEY";
27877
+ const credentialName = provider === "openai_api" ? "openai_api_key" : provider === "gemini_api" ? "gemini_api_key" : "anthropic_api_key";
27878
+ const envVar = provider === "openai_api" ? "OPENAI_API_KEY" : provider === "gemini_api" ? "GEMINI_API_KEY" : "ANTHROPIC_API_KEY";
27840
27879
  const roomCredential = findAnyRoomCredential2(db2, credentialName);
27841
27880
  const savedKey = getClerkApiKey(db2, provider);
27842
27881
  const envKey = (process.env[envVar] || "").trim() || null;
@@ -27852,7 +27891,8 @@ function getClerkApiAuthState(db2, provider) {
27852
27891
  function getClerkApiAuth(db2) {
27853
27892
  return {
27854
27893
  openai: getClerkApiAuthState(db2, "openai_api"),
27855
- anthropic: getClerkApiAuthState(db2, "anthropic_api")
27894
+ anthropic: getClerkApiAuthState(db2, "anthropic_api"),
27895
+ gemini: getClerkApiAuthState(db2, "gemini_api")
27856
27896
  };
27857
27897
  }
27858
27898
  function autoConfigureClerkModel(db2) {
@@ -27872,6 +27912,9 @@ function resolveClerkApiKey(db2, model) {
27872
27912
  if (provider === "anthropic_api") {
27873
27913
  return findAnyRoomCredential2(db2, "anthropic_api_key") || getClerkApiKey(db2, "anthropic_api") || (process.env.ANTHROPIC_API_KEY || void 0);
27874
27914
  }
27915
+ if (provider === "gemini_api") {
27916
+ return findAnyRoomCredential2(db2, "gemini_api_key") || getClerkApiKey(db2, "gemini_api") || (process.env.GEMINI_API_KEY || void 0);
27917
+ }
27875
27918
  return void 0;
27876
27919
  }
27877
27920
  function clipText(value, max = 240) {
@@ -28019,6 +28062,11 @@ function buildClerkModelPlan(preferredModel) {
28019
28062
  uniquePush(plan, CLERK_FALLBACK_SUBSCRIPTION_MODEL);
28020
28063
  uniquePush(plan, CLERK_FALLBACK_OPENAI_MODEL);
28021
28064
  uniquePush(plan, DEFAULT_CLERK_MODEL);
28065
+ } else if (provider === "gemini_api") {
28066
+ uniquePush(plan, CLERK_FALLBACK_SUBSCRIPTION_MODEL);
28067
+ uniquePush(plan, CLERK_FALLBACK_OPENAI_MODEL);
28068
+ uniquePush(plan, DEFAULT_CLERK_MODEL);
28069
+ uniquePush(plan, CLERK_FALLBACK_ANTHROPIC_MODEL);
28022
28070
  }
28023
28071
  return plan;
28024
28072
  }
@@ -28027,7 +28075,7 @@ function buildExecutionCandidates(db2, preferredModel) {
28027
28075
  for (const model of buildClerkModelPlan(preferredModel)) {
28028
28076
  const provider = getModelProvider(model);
28029
28077
  const apiKey = resolveClerkApiKey(db2, model);
28030
- if ((provider === "openai_api" || provider === "anthropic_api") && !apiKey) continue;
28078
+ if ((provider === "openai_api" || provider === "anthropic_api" || provider === "gemini_api") && !apiKey) continue;
28031
28079
  candidates.push({ model, apiKey });
28032
28080
  }
28033
28081
  if (candidates.length === 0) {
@@ -29700,6 +29748,25 @@ async function validateOpenAiKey(value) {
29700
29748
  clearTimeout(timer);
29701
29749
  }
29702
29750
  }
29751
+ async function validateGeminiKey(value) {
29752
+ const controller = new AbortController();
29753
+ const timer = setTimeout(() => controller.abort(), VALIDATION_TIMEOUT_MS);
29754
+ try {
29755
+ const res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models?key=${encodeURIComponent(value)}`, {
29756
+ method: "GET",
29757
+ signal: controller.signal
29758
+ });
29759
+ if (res.ok) return { ok: true };
29760
+ const body = await res.json().catch(() => null);
29761
+ const message = extractApiError2(body) || `HTTP ${res.status}`;
29762
+ return { ok: false, error: `Gemini key validation failed: ${message}` };
29763
+ } catch (err) {
29764
+ const message = err instanceof Error ? err.message : String(err);
29765
+ return { ok: false, error: `Gemini key validation failed: ${message}` };
29766
+ } finally {
29767
+ clearTimeout(timer);
29768
+ }
29769
+ }
29703
29770
  async function validateAnthropicKey(value) {
29704
29771
  const controller = new AbortController();
29705
29772
  const timer = setTimeout(() => controller.abort(), VALIDATION_TIMEOUT_MS);
@@ -30037,11 +30104,11 @@ function registerClerkRoutes(router) {
30037
30104
  const body = ctx.body || {};
30038
30105
  const provider = typeof body.provider === "string" ? body.provider.trim() : "";
30039
30106
  const key = typeof body.key === "string" ? body.key.trim() : "";
30040
- if (provider !== "openai_api" && provider !== "anthropic_api") {
30041
- return { status: 400, error: "provider must be openai_api or anthropic_api" };
30107
+ if (provider !== "openai_api" && provider !== "anthropic_api" && provider !== "gemini_api") {
30108
+ return { status: 400, error: "provider must be openai_api, anthropic_api, or gemini_api" };
30042
30109
  }
30043
30110
  if (!key) return { status: 400, error: "key is required" };
30044
- const result = provider === "openai_api" ? await validateOpenAiKey(key) : await validateAnthropicKey(key);
30111
+ const result = provider === "openai_api" ? await validateOpenAiKey(key) : provider === "gemini_api" ? await validateGeminiKey(key) : await validateAnthropicKey(key);
30045
30112
  if (!result.ok) return { status: 400, error: result.error };
30046
30113
  setClerkApiKey(ctx.db, provider, key);
30047
30114
  return { data: { ok: true, apiAuth: getClerkApiAuth(ctx.db) } };
@@ -33111,7 +33178,7 @@ function semverGt(a, b) {
33111
33178
  }
33112
33179
  function getCurrentVersion() {
33113
33180
  try {
33114
- return true ? "0.1.29" : null.version;
33181
+ return true ? "0.1.30" : null.version;
33115
33182
  } catch {
33116
33183
  return "0.0.0";
33117
33184
  }
@@ -33270,7 +33337,7 @@ var cachedVersion = null;
33270
33337
  function getVersion3() {
33271
33338
  if (cachedVersion) return cachedVersion;
33272
33339
  try {
33273
- cachedVersion = true ? "0.1.29" : null.version;
33340
+ cachedVersion = true ? "0.1.30" : null.version;
33274
33341
  } catch {
33275
33342
  cachedVersion = "unknown";
33276
33343
  }
@@ -33614,7 +33681,7 @@ function maskCredential(credential) {
33614
33681
  return { ...credential, valueEncrypted: "***" };
33615
33682
  }
33616
33683
  var VALIDATION_TIMEOUT_MS2 = 8e3;
33617
- var SUPPORTED_CREDENTIALS = /* @__PURE__ */ new Set(["openai_api_key", "anthropic_api_key"]);
33684
+ var SUPPORTED_CREDENTIALS = /* @__PURE__ */ new Set(["openai_api_key", "anthropic_api_key", "gemini_api_key"]);
33618
33685
  function extractApiError3(payload) {
33619
33686
  if (!payload || typeof payload !== "object") return null;
33620
33687
  const record = payload;
@@ -33649,6 +33716,25 @@ async function validateOpenAiKey2(value) {
33649
33716
  clearTimeout(timer);
33650
33717
  }
33651
33718
  }
33719
+ async function validateGeminiKey2(value) {
33720
+ const controller = new AbortController();
33721
+ const timer = setTimeout(() => controller.abort(), VALIDATION_TIMEOUT_MS2);
33722
+ try {
33723
+ const res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models?key=${encodeURIComponent(value)}`, {
33724
+ method: "GET",
33725
+ signal: controller.signal
33726
+ });
33727
+ if (res.ok) return { ok: true };
33728
+ const body = await res.json().catch(() => null);
33729
+ const message = extractApiError3(body) || `HTTP ${res.status}`;
33730
+ return { ok: false, error: `Gemini key validation failed: ${message}` };
33731
+ } catch (err) {
33732
+ const message = err instanceof Error ? err.message : String(err);
33733
+ return { ok: false, error: `Gemini key validation failed: ${message}` };
33734
+ } finally {
33735
+ clearTimeout(timer);
33736
+ }
33737
+ }
33652
33738
  async function validateAnthropicKey2(value) {
33653
33739
  const controller = new AbortController();
33654
33740
  const timer = setTimeout(() => controller.abort(), VALIDATION_TIMEOUT_MS2);
@@ -33695,7 +33781,7 @@ function registerCredentialRoutes(router) {
33695
33781
  if (!SUPPORTED_CREDENTIALS.has(name)) {
33696
33782
  return { status: 400, error: `Validation not supported for credential: ${name}` };
33697
33783
  }
33698
- const result = name === "openai_api_key" ? await validateOpenAiKey2(value) : await validateAnthropicKey2(value);
33784
+ const result = name === "openai_api_key" ? await validateOpenAiKey2(value) : name === "gemini_api_key" ? await validateGeminiKey2(value) : await validateAnthropicKey2(value);
33699
33785
  if (!result.ok) return { status: 400, error: result.error };
33700
33786
  return { data: { ok: true } };
33701
33787
  });
package/out/mcp/cli.js CHANGED
@@ -24228,7 +24228,9 @@ function getClerkUsageToday(db3, source) {
24228
24228
  };
24229
24229
  }
24230
24230
  function clerkApiKeySetting(provider) {
24231
- return provider === "openai_api" ? "clerk_openai_api_key" : "clerk_anthropic_api_key";
24231
+ if (provider === "openai_api") return "clerk_openai_api_key";
24232
+ if (provider === "gemini_api") return "clerk_gemini_api_key";
24233
+ return "clerk_anthropic_api_key";
24232
24234
  }
24233
24235
  function setClerkApiKey(db3, provider, value) {
24234
24236
  const trimmed = value.trim();
@@ -26734,7 +26736,7 @@ async function executeAgent(options) {
26734
26736
  if (model === "codex" || model.startsWith("codex:")) {
26735
26737
  return executeCodex(options);
26736
26738
  }
26737
- if (model === "openai" || model.startsWith("openai:")) {
26739
+ if (model === "openai" || model.startsWith("openai:") || model === "gemini" || model.startsWith("gemini:")) {
26738
26740
  if (options.toolDefs && options.toolDefs.length > 0 && options.onToolCall) {
26739
26741
  return executeOpenAiWithTools(options);
26740
26742
  }
@@ -26904,10 +26906,34 @@ async function executeCodex(options) {
26904
26906
  });
26905
26907
  });
26906
26908
  }
26909
+ function resolveOpenAiCompatible(model, apiKeyOverride) {
26910
+ const trimmed = model.trim();
26911
+ if (trimmed === "gemini" || trimmed.startsWith("gemini:")) {
26912
+ const apiKey2 = apiKeyOverride?.trim() || (process.env.GEMINI_API_KEY || "").trim();
26913
+ if (!apiKey2) return null;
26914
+ return {
26915
+ apiKey: apiKey2,
26916
+ url: "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions",
26917
+ defaultModel: "gemini-2.5-flash",
26918
+ label: "Gemini",
26919
+ prefix: "gemini"
26920
+ };
26921
+ }
26922
+ const apiKey = apiKeyOverride?.trim() || (process.env.OPENAI_API_KEY || "").trim();
26923
+ if (!apiKey) return null;
26924
+ return {
26925
+ apiKey,
26926
+ url: "https://api.openai.com/v1/chat/completions",
26927
+ defaultModel: "gpt-4o-mini",
26928
+ label: "OpenAI",
26929
+ prefix: "openai"
26930
+ };
26931
+ }
26907
26932
  async function executeOpenAiWithTools(options) {
26908
- const apiKey = options.apiKey?.trim() || (process.env.OPENAI_API_KEY || "").trim();
26909
- if (!apiKey) return immediateError("Missing OpenAI API key.");
26910
- const modelName = parseModelSuffix(options.model, "openai") || "gpt-4o-mini";
26933
+ const config2 = resolveOpenAiCompatible(options.model, options.apiKey);
26934
+ if (!config2) return immediateError(`Missing ${options.model.startsWith("gemini") ? "Gemini" : "OpenAI"} API key.`);
26935
+ const { apiKey, url: apiUrl, defaultModel, label: providerLabel, prefix } = config2;
26936
+ const modelName = parseModelSuffix(options.model, prefix) || defaultModel;
26911
26937
  const startTime = Date.now();
26912
26938
  const maxTurns = options.maxTurns ?? 10;
26913
26939
  const previousTurns = options.previousMessages ?? [];
@@ -26931,7 +26957,7 @@ Continue working toward the goal.` : options.prompt
26931
26957
  const timer = setTimeout(() => controller.abort(), timeoutMs);
26932
26958
  let json;
26933
26959
  try {
26934
- const response = await fetch("https://api.openai.com/v1/chat/completions", {
26960
+ const response = await fetch(apiUrl, {
26935
26961
  method: "POST",
26936
26962
  headers: { "Authorization": `Bearer ${apiKey}`, "Content-Type": "application/json" },
26937
26963
  body: JSON.stringify({ model: modelName, messages, tools: options.toolDefs }),
@@ -26939,7 +26965,7 @@ Continue working toward the goal.` : options.prompt
26939
26965
  });
26940
26966
  json = await response.json();
26941
26967
  if (!response.ok) {
26942
- return { output: `OpenAI API ${response.status}: ${extractApiError(json)}`, exitCode: 1, durationMs: Date.now() - startTime, sessionId: null, timedOut: false, usage: { inputTokens: totalInputTokens, outputTokens: totalOutputTokens } };
26968
+ return { output: `${providerLabel} API ${response.status}: ${extractApiError(json)}`, exitCode: 1, durationMs: Date.now() - startTime, sessionId: null, timedOut: false, usage: { inputTokens: totalInputTokens, outputTokens: totalOutputTokens } };
26943
26969
  }
26944
26970
  } catch (err) {
26945
26971
  const msg2 = err instanceof Error ? err.message : String(err);
@@ -27083,11 +27109,13 @@ Continue working toward the goal.` : options.prompt
27083
27109
  return { output: finalOutput || "Actions completed.", exitCode: 0, durationMs: Date.now() - startTime, sessionId: null, timedOut: false, usage: { inputTokens: totalInputTokens, outputTokens: totalOutputTokens } };
27084
27110
  }
27085
27111
  async function executeOpenAiApi(options) {
27086
- const apiKey = options.apiKey?.trim() || (process.env.OPENAI_API_KEY || "").trim();
27087
- if (!apiKey) {
27088
- return immediateError('Missing OpenAI API key. Set room credential "openai_api_key" or OPENAI_API_KEY.');
27112
+ const config2 = resolveOpenAiCompatible(options.model, options.apiKey);
27113
+ if (!config2) {
27114
+ const isGemini = options.model.startsWith("gemini");
27115
+ return immediateError(`Missing ${isGemini ? "Gemini" : "OpenAI"} API key. Set room credential "${isGemini ? "gemini_api_key" : "openai_api_key"}" or ${isGemini ? "GEMINI_API_KEY" : "OPENAI_API_KEY"}.`);
27089
27116
  }
27090
- const modelName = parseModelSuffix(options.model, "openai") || "gpt-4o-mini";
27117
+ const { apiKey, url: apiUrl, defaultModel, label: providerLabel, prefix } = config2;
27118
+ const modelName = parseModelSuffix(options.model, prefix) || defaultModel;
27091
27119
  const messages = [];
27092
27120
  if (options.systemPrompt) messages.push({ role: "system", content: options.systemPrompt });
27093
27121
  messages.push({ role: "user", content: options.prompt });
@@ -27096,7 +27124,7 @@ async function executeOpenAiApi(options) {
27096
27124
  const timeoutMs = options.timeoutMs ?? DEFAULT_HTTP_TIMEOUT_MS;
27097
27125
  const timer = setTimeout(() => controller.abort(), timeoutMs);
27098
27126
  try {
27099
- const response = await fetch("https://api.openai.com/v1/chat/completions", {
27127
+ const response = await fetch(apiUrl, {
27100
27128
  method: "POST",
27101
27129
  headers: {
27102
27130
  "Authorization": `Bearer ${apiKey}`,
@@ -27111,7 +27139,7 @@ async function executeOpenAiApi(options) {
27111
27139
  const json = await response.json();
27112
27140
  if (!response.ok) {
27113
27141
  return {
27114
- output: `OpenAI API ${response.status}: ${extractApiError(json)}`,
27142
+ output: `${providerLabel} API ${response.status}: ${extractApiError(json)}`,
27115
27143
  exitCode: 1,
27116
27144
  durationMs: Date.now() - startTime,
27117
27145
  sessionId: null,
@@ -27327,13 +27355,13 @@ Respond ONLY with a JSON object (no markdown, no explanation):
27327
27355
  }`;
27328
27356
  const timeoutMs = 6e4;
27329
27357
  try {
27330
- if (model === "openai" || model.startsWith("openai:")) {
27331
- const key = apiKey?.trim() || (process.env.OPENAI_API_KEY || "").trim();
27332
- if (!key) return null;
27333
- const modelName = parseModelSuffix(model, "openai") || "gpt-4o-mini";
27334
- const response = await fetch("https://api.openai.com/v1/chat/completions", {
27358
+ if (model === "openai" || model.startsWith("openai:") || model === "gemini" || model.startsWith("gemini:")) {
27359
+ const config2 = resolveOpenAiCompatible(model, apiKey);
27360
+ if (!config2) return null;
27361
+ const modelName = parseModelSuffix(model, config2.prefix) || config2.defaultModel;
27362
+ const response = await fetch(config2.url, {
27335
27363
  method: "POST",
27336
- headers: { "Authorization": `Bearer ${key}`, "Content-Type": "application/json" },
27364
+ headers: { "Authorization": `Bearer ${config2.apiKey}`, "Content-Type": "application/json" },
27337
27365
  body: JSON.stringify({ model: modelName, messages: [{ role: "user", content: compressionPrompt }] }),
27338
27366
  signal: AbortSignal.timeout(timeoutMs)
27339
27367
  });
@@ -27363,16 +27391,17 @@ async function executeApiOnStation(cloudRoomId, stationId, options) {
27363
27391
  if (!apiKey) {
27364
27392
  return immediateError("Missing API key for station execution.");
27365
27393
  }
27366
- const isOpenAi = options.model.startsWith("openai:");
27394
+ const isOpenAiCompat = options.model.startsWith("openai:") || options.model.startsWith("gemini:");
27367
27395
  const messages = [];
27368
27396
  if (options.systemPrompt) messages.push({ role: "system", content: options.systemPrompt });
27369
27397
  messages.push({ role: "user", content: options.prompt });
27370
27398
  let url;
27371
27399
  let headers;
27372
27400
  let body;
27373
- if (isOpenAi) {
27374
- const modelName = parseModelSuffix(options.model, "openai") || "gpt-4o-mini";
27375
- url = "https://api.openai.com/v1/chat/completions";
27401
+ if (isOpenAiCompat) {
27402
+ const config2 = resolveOpenAiCompatible(options.model, apiKey);
27403
+ const modelName = config2 ? parseModelSuffix(options.model, config2.prefix) || config2.defaultModel : parseModelSuffix(options.model, "openai") || "gpt-4o-mini";
27404
+ url = config2?.url ?? "https://api.openai.com/v1/chat/completions";
27376
27405
  headers = { "Authorization": `Bearer ${apiKey}`, "Content-Type": "application/json" };
27377
27406
  body = JSON.stringify({ model: modelName, messages });
27378
27407
  } else {
@@ -27410,7 +27439,7 @@ async function executeApiOnStation(cloudRoomId, stationId, options) {
27410
27439
  }
27411
27440
  try {
27412
27441
  const parsed = JSON.parse(result.stdout);
27413
- const output = isOpenAi ? extractOpenAiText(parsed) : extractAnthropicText(parsed);
27442
+ const output = isOpenAiCompat ? extractOpenAiText(parsed) : extractAnthropicText(parsed);
27414
27443
  return { output, exitCode: 0, durationMs: Date.now() - startTime, sessionId: null, timedOut: false };
27415
27444
  } catch {
27416
27445
  return {
@@ -27450,6 +27479,7 @@ function getModelProvider(model) {
27450
27479
  if (normalized === "anthropic" || normalized.startsWith("anthropic:") || normalized.startsWith("claude-api:")) {
27451
27480
  return "anthropic_api";
27452
27481
  }
27482
+ if (normalized === "gemini" || normalized.startsWith("gemini:")) return "gemini_api";
27453
27483
  return "claude_subscription";
27454
27484
  }
27455
27485
  async function getModelAuthStatus(db3, roomId, model) {
@@ -27460,6 +27490,9 @@ async function getModelAuthStatus(db3, roomId, model) {
27460
27490
  if (provider === "anthropic_api") {
27461
27491
  return resolveApiAuthStatus(db3, roomId, "anthropic_api_key", "ANTHROPIC_API_KEY", provider);
27462
27492
  }
27493
+ if (provider === "gemini_api") {
27494
+ return resolveApiAuthStatus(db3, roomId, "gemini_api_key", "GEMINI_API_KEY", provider);
27495
+ }
27463
27496
  let ready = false;
27464
27497
  if (provider === "claude_subscription") {
27465
27498
  ready = checkClaudeCliAvailable().available;
@@ -27485,6 +27518,9 @@ function resolveApiKeyForModel(db3, roomId, model) {
27485
27518
  if (provider === "anthropic_api") {
27486
27519
  return resolveApiKey(db3, roomId, "anthropic_api_key", "ANTHROPIC_API_KEY");
27487
27520
  }
27521
+ if (provider === "gemini_api") {
27522
+ return resolveApiKey(db3, roomId, "gemini_api_key", "GEMINI_API_KEY");
27523
+ }
27488
27524
  return void 0;
27489
27525
  }
27490
27526
  function resolveApiAuthStatus(db3, roomId, credentialName, envVar, provider) {
@@ -27536,6 +27572,9 @@ function getClerkCredential(db3, credentialName) {
27536
27572
  if (credentialName === "anthropic_api_key") {
27537
27573
  return getClerkApiKey(db3, "anthropic_api");
27538
27574
  }
27575
+ if (credentialName === "gemini_api_key") {
27576
+ return getClerkApiKey(db3, "gemini_api");
27577
+ }
27539
27578
  return null;
27540
27579
  }
27541
27580
  function getRoomCredential(db3, roomId, credentialName) {
@@ -52537,7 +52576,7 @@ var server_exports = {};
52537
52576
  async function main() {
52538
52577
  const server = new McpServer({
52539
52578
  name: "quoroom",
52540
- version: true ? "0.1.29" : "0.0.0"
52579
+ version: true ? "0.1.30" : "0.0.0"
52541
52580
  });
52542
52581
  registerMemoryTools(server);
52543
52582
  registerSchedulerTools(server);
@@ -54056,10 +54095,10 @@ async function runCycle(db3, roomId, worker, maxTurns, options) {
54056
54095
  options?.onCycleLifecycle?.("created", cycle.id, roomId);
54057
54096
  try {
54058
54097
  const provider = getModelProvider(model);
54059
- if (provider === "openai_api" || provider === "anthropic_api") {
54098
+ if (provider === "openai_api" || provider === "anthropic_api" || provider === "gemini_api") {
54060
54099
  const apiKeyCheck = resolveApiKeyForModel(db3, roomId, model);
54061
54100
  if (!apiKeyCheck) {
54062
- const label = provider === "openai_api" ? "OpenAI" : "Anthropic";
54101
+ const label = provider === "openai_api" ? "OpenAI" : provider === "gemini_api" ? "Gemini" : "Anthropic";
54063
54102
  const msg = `Missing ${label} API key. Set it in Room Settings or the Setup Guide.`;
54064
54103
  logBuffer2.addSynthetic("error", msg);
54065
54104
  logBuffer2.flush();
@@ -54603,7 +54642,7 @@ var require_package = __commonJS({
54603
54642
  "package.json"(exports2, module2) {
54604
54643
  module2.exports = {
54605
54644
  name: "quoroom",
54606
- version: "0.1.29",
54645
+ version: "0.1.30",
54607
54646
  description: "Autonomous AI agent collective engine \u2014 Queen, Workers, Quorum",
54608
54647
  main: "./out/mcp/server.js",
54609
54648
  bin: {
@@ -55648,8 +55687,8 @@ function maskKey2(key) {
55648
55687
  return `${trimmed.slice(0, 7)}...${trimmed.slice(-4)}`;
55649
55688
  }
55650
55689
  function getClerkApiAuthState(db3, provider) {
55651
- const credentialName = provider === "openai_api" ? "openai_api_key" : "anthropic_api_key";
55652
- const envVar = provider === "openai_api" ? "OPENAI_API_KEY" : "ANTHROPIC_API_KEY";
55690
+ const credentialName = provider === "openai_api" ? "openai_api_key" : provider === "gemini_api" ? "gemini_api_key" : "anthropic_api_key";
55691
+ const envVar = provider === "openai_api" ? "OPENAI_API_KEY" : provider === "gemini_api" ? "GEMINI_API_KEY" : "ANTHROPIC_API_KEY";
55653
55692
  const roomCredential = findAnyRoomCredential2(db3, credentialName);
55654
55693
  const savedKey = getClerkApiKey(db3, provider);
55655
55694
  const envKey = (process.env[envVar] || "").trim() || null;
@@ -55665,7 +55704,8 @@ function getClerkApiAuthState(db3, provider) {
55665
55704
  function getClerkApiAuth(db3) {
55666
55705
  return {
55667
55706
  openai: getClerkApiAuthState(db3, "openai_api"),
55668
- anthropic: getClerkApiAuthState(db3, "anthropic_api")
55707
+ anthropic: getClerkApiAuthState(db3, "anthropic_api"),
55708
+ gemini: getClerkApiAuthState(db3, "gemini_api")
55669
55709
  };
55670
55710
  }
55671
55711
  function autoConfigureClerkModel(db3) {
@@ -55685,6 +55725,9 @@ function resolveClerkApiKey(db3, model) {
55685
55725
  if (provider === "anthropic_api") {
55686
55726
  return findAnyRoomCredential2(db3, "anthropic_api_key") || getClerkApiKey(db3, "anthropic_api") || (process.env.ANTHROPIC_API_KEY || void 0);
55687
55727
  }
55728
+ if (provider === "gemini_api") {
55729
+ return findAnyRoomCredential2(db3, "gemini_api_key") || getClerkApiKey(db3, "gemini_api") || (process.env.GEMINI_API_KEY || void 0);
55730
+ }
55688
55731
  return void 0;
55689
55732
  }
55690
55733
  function clipText(value, max = 240) {
@@ -55832,6 +55875,11 @@ function buildClerkModelPlan(preferredModel) {
55832
55875
  uniquePush(plan, CLERK_FALLBACK_SUBSCRIPTION_MODEL);
55833
55876
  uniquePush(plan, CLERK_FALLBACK_OPENAI_MODEL);
55834
55877
  uniquePush(plan, DEFAULT_CLERK_MODEL);
55878
+ } else if (provider === "gemini_api") {
55879
+ uniquePush(plan, CLERK_FALLBACK_SUBSCRIPTION_MODEL);
55880
+ uniquePush(plan, CLERK_FALLBACK_OPENAI_MODEL);
55881
+ uniquePush(plan, DEFAULT_CLERK_MODEL);
55882
+ uniquePush(plan, CLERK_FALLBACK_ANTHROPIC_MODEL);
55835
55883
  }
55836
55884
  return plan;
55837
55885
  }
@@ -55840,7 +55888,7 @@ function buildExecutionCandidates(db3, preferredModel) {
55840
55888
  for (const model of buildClerkModelPlan(preferredModel)) {
55841
55889
  const provider = getModelProvider(model);
55842
55890
  const apiKey = resolveClerkApiKey(db3, model);
55843
- if ((provider === "openai_api" || provider === "anthropic_api") && !apiKey) continue;
55891
+ if ((provider === "openai_api" || provider === "anthropic_api" || provider === "gemini_api") && !apiKey) continue;
55844
55892
  candidates.push({ model, apiKey });
55845
55893
  }
55846
55894
  if (candidates.length === 0) {
@@ -57546,6 +57594,25 @@ async function validateOpenAiKey(value) {
57546
57594
  clearTimeout(timer);
57547
57595
  }
57548
57596
  }
57597
+ async function validateGeminiKey(value) {
57598
+ const controller = new AbortController();
57599
+ const timer = setTimeout(() => controller.abort(), VALIDATION_TIMEOUT_MS);
57600
+ try {
57601
+ const res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models?key=${encodeURIComponent(value)}`, {
57602
+ method: "GET",
57603
+ signal: controller.signal
57604
+ });
57605
+ if (res.ok) return { ok: true };
57606
+ const body = await res.json().catch(() => null);
57607
+ const message = extractApiError2(body) || `HTTP ${res.status}`;
57608
+ return { ok: false, error: `Gemini key validation failed: ${message}` };
57609
+ } catch (err) {
57610
+ const message = err instanceof Error ? err.message : String(err);
57611
+ return { ok: false, error: `Gemini key validation failed: ${message}` };
57612
+ } finally {
57613
+ clearTimeout(timer);
57614
+ }
57615
+ }
57549
57616
  async function validateAnthropicKey(value) {
57550
57617
  const controller = new AbortController();
57551
57618
  const timer = setTimeout(() => controller.abort(), VALIDATION_TIMEOUT_MS);
@@ -57883,11 +57950,11 @@ function registerClerkRoutes(router) {
57883
57950
  const body = ctx.body || {};
57884
57951
  const provider = typeof body.provider === "string" ? body.provider.trim() : "";
57885
57952
  const key = typeof body.key === "string" ? body.key.trim() : "";
57886
- if (provider !== "openai_api" && provider !== "anthropic_api") {
57887
- return { status: 400, error: "provider must be openai_api or anthropic_api" };
57953
+ if (provider !== "openai_api" && provider !== "anthropic_api" && provider !== "gemini_api") {
57954
+ return { status: 400, error: "provider must be openai_api, anthropic_api, or gemini_api" };
57888
57955
  }
57889
57956
  if (!key) return { status: 400, error: "key is required" };
57890
- const result = provider === "openai_api" ? await validateOpenAiKey(key) : await validateAnthropicKey(key);
57957
+ const result = provider === "openai_api" ? await validateOpenAiKey(key) : provider === "gemini_api" ? await validateGeminiKey(key) : await validateAnthropicKey(key);
57891
57958
  if (!result.ok) return { status: 400, error: result.error };
57892
57959
  setClerkApiKey(ctx.db, provider, key);
57893
57960
  return { data: { ok: true, apiAuth: getClerkApiAuth(ctx.db) } };
@@ -60404,7 +60471,7 @@ function semverGt(a, b) {
60404
60471
  }
60405
60472
  function getCurrentVersion() {
60406
60473
  try {
60407
- return true ? "0.1.29" : null.version;
60474
+ return true ? "0.1.30" : null.version;
60408
60475
  } catch {
60409
60476
  return "0.0.0";
60410
60477
  }
@@ -60589,7 +60656,7 @@ var init_updateChecker = __esm({
60589
60656
  function getVersion3() {
60590
60657
  if (cachedVersion) return cachedVersion;
60591
60658
  try {
60592
- cachedVersion = true ? "0.1.29" : null.version;
60659
+ cachedVersion = true ? "0.1.30" : null.version;
60593
60660
  } catch {
60594
60661
  cachedVersion = "unknown";
60595
60662
  }
@@ -60992,6 +61059,25 @@ async function validateOpenAiKey2(value) {
60992
61059
  clearTimeout(timer);
60993
61060
  }
60994
61061
  }
61062
+ async function validateGeminiKey2(value) {
61063
+ const controller = new AbortController();
61064
+ const timer = setTimeout(() => controller.abort(), VALIDATION_TIMEOUT_MS2);
61065
+ try {
61066
+ const res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models?key=${encodeURIComponent(value)}`, {
61067
+ method: "GET",
61068
+ signal: controller.signal
61069
+ });
61070
+ if (res.ok) return { ok: true };
61071
+ const body = await res.json().catch(() => null);
61072
+ const message = extractApiError3(body) || `HTTP ${res.status}`;
61073
+ return { ok: false, error: `Gemini key validation failed: ${message}` };
61074
+ } catch (err) {
61075
+ const message = err instanceof Error ? err.message : String(err);
61076
+ return { ok: false, error: `Gemini key validation failed: ${message}` };
61077
+ } finally {
61078
+ clearTimeout(timer);
61079
+ }
61080
+ }
60995
61081
  async function validateAnthropicKey2(value) {
60996
61082
  const controller = new AbortController();
60997
61083
  const timer = setTimeout(() => controller.abort(), VALIDATION_TIMEOUT_MS2);
@@ -61038,7 +61124,7 @@ function registerCredentialRoutes(router) {
61038
61124
  if (!SUPPORTED_CREDENTIALS.has(name)) {
61039
61125
  return { status: 400, error: `Validation not supported for credential: ${name}` };
61040
61126
  }
61041
- const result = name === "openai_api_key" ? await validateOpenAiKey2(value) : await validateAnthropicKey2(value);
61127
+ const result = name === "openai_api_key" ? await validateOpenAiKey2(value) : name === "gemini_api_key" ? await validateGeminiKey2(value) : await validateAnthropicKey2(value);
61042
61128
  if (!result.ok) return { status: 400, error: result.error };
61043
61129
  return { data: { ok: true } };
61044
61130
  });
@@ -61078,7 +61164,7 @@ var init_credentials2 = __esm({
61078
61164
  init_db_queries();
61079
61165
  init_event_bus();
61080
61166
  VALIDATION_TIMEOUT_MS2 = 8e3;
61081
- SUPPORTED_CREDENTIALS = /* @__PURE__ */ new Set(["openai_api_key", "anthropic_api_key"]);
61167
+ SUPPORTED_CREDENTIALS = /* @__PURE__ */ new Set(["openai_api_key", "anthropic_api_key", "gemini_api_key"]);
61082
61168
  }
61083
61169
  });
61084
61170
 
@@ -67018,7 +67104,7 @@ __export(update_exports, {
67018
67104
  });
67019
67105
  function getCurrentVersion2() {
67020
67106
  try {
67021
- return true ? "0.1.29" : null.version;
67107
+ return true ? "0.1.30" : null.version;
67022
67108
  } catch {
67023
67109
  return "0.0.0";
67024
67110
  }
package/out/mcp/server.js CHANGED
@@ -9389,7 +9389,9 @@ function getClerkUsageToday(db2, source) {
9389
9389
  };
9390
9390
  }
9391
9391
  function clerkApiKeySetting(provider) {
9392
- return provider === "openai_api" ? "clerk_openai_api_key" : "clerk_anthropic_api_key";
9392
+ if (provider === "openai_api") return "clerk_openai_api_key";
9393
+ if (provider === "gemini_api") return "clerk_gemini_api_key";
9394
+ return "clerk_anthropic_api_key";
9393
9395
  }
9394
9396
  function setClerkApiKey(db2, provider, value) {
9395
9397
  const trimmed = value.trim();
@@ -35035,6 +35037,29 @@ async function fetchReferredRooms(cloudRoomId) {
35035
35037
  }
35036
35038
 
35037
35039
  // src/shared/agent-executor.ts
35040
+ function resolveOpenAiCompatible(model, apiKeyOverride) {
35041
+ const trimmed = model.trim();
35042
+ if (trimmed === "gemini" || trimmed.startsWith("gemini:")) {
35043
+ const apiKey2 = apiKeyOverride?.trim() || (process.env.GEMINI_API_KEY || "").trim();
35044
+ if (!apiKey2) return null;
35045
+ return {
35046
+ apiKey: apiKey2,
35047
+ url: "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions",
35048
+ defaultModel: "gemini-2.5-flash",
35049
+ label: "Gemini",
35050
+ prefix: "gemini"
35051
+ };
35052
+ }
35053
+ const apiKey = apiKeyOverride?.trim() || (process.env.OPENAI_API_KEY || "").trim();
35054
+ if (!apiKey) return null;
35055
+ return {
35056
+ apiKey,
35057
+ url: "https://api.openai.com/v1/chat/completions",
35058
+ defaultModel: "gpt-4o-mini",
35059
+ label: "OpenAI",
35060
+ prefix: "openai"
35061
+ };
35062
+ }
35038
35063
  function parseModelSuffix(model, prefix) {
35039
35064
  const trimmed = model.trim();
35040
35065
  if (trimmed === prefix) return "";
@@ -35095,16 +35120,17 @@ async function executeApiOnStation(cloudRoomId, stationId, options) {
35095
35120
  if (!apiKey) {
35096
35121
  return immediateError("Missing API key for station execution.");
35097
35122
  }
35098
- const isOpenAi = options.model.startsWith("openai:");
35123
+ const isOpenAiCompat = options.model.startsWith("openai:") || options.model.startsWith("gemini:");
35099
35124
  const messages = [];
35100
35125
  if (options.systemPrompt) messages.push({ role: "system", content: options.systemPrompt });
35101
35126
  messages.push({ role: "user", content: options.prompt });
35102
35127
  let url;
35103
35128
  let headers;
35104
35129
  let body;
35105
- if (isOpenAi) {
35106
- const modelName = parseModelSuffix(options.model, "openai") || "gpt-4o-mini";
35107
- url = "https://api.openai.com/v1/chat/completions";
35130
+ if (isOpenAiCompat) {
35131
+ const config2 = resolveOpenAiCompatible(options.model, apiKey);
35132
+ const modelName = config2 ? parseModelSuffix(options.model, config2.prefix) || config2.defaultModel : parseModelSuffix(options.model, "openai") || "gpt-4o-mini";
35133
+ url = config2?.url ?? "https://api.openai.com/v1/chat/completions";
35108
35134
  headers = { "Authorization": `Bearer ${apiKey}`, "Content-Type": "application/json" };
35109
35135
  body = JSON.stringify({ model: modelName, messages });
35110
35136
  } else {
@@ -35142,7 +35168,7 @@ async function executeApiOnStation(cloudRoomId, stationId, options) {
35142
35168
  }
35143
35169
  try {
35144
35170
  const parsed = JSON.parse(result.stdout);
35145
- const output = isOpenAi ? extractOpenAiText(parsed) : extractAnthropicText(parsed);
35171
+ const output = isOpenAiCompat ? extractOpenAiText(parsed) : extractAnthropicText(parsed);
35146
35172
  return { output, exitCode: 0, durationMs: Date.now() - startTime, sessionId: null, timedOut: false };
35147
35173
  } catch {
35148
35174
  return {
@@ -35168,6 +35194,7 @@ function getModelProvider(model) {
35168
35194
  if (normalized === "anthropic" || normalized.startsWith("anthropic:") || normalized.startsWith("claude-api:")) {
35169
35195
  return "anthropic_api";
35170
35196
  }
35197
+ if (normalized === "gemini" || normalized.startsWith("gemini:")) return "gemini_api";
35171
35198
  return "claude_subscription";
35172
35199
  }
35173
35200
  function resolveApiKeyForModel(db2, roomId, model) {
@@ -35178,6 +35205,9 @@ function resolveApiKeyForModel(db2, roomId, model) {
35178
35205
  if (provider === "anthropic_api") {
35179
35206
  return resolveApiKey(db2, roomId, "anthropic_api_key", "ANTHROPIC_API_KEY");
35180
35207
  }
35208
+ if (provider === "gemini_api") {
35209
+ return resolveApiKey(db2, roomId, "gemini_api_key", "GEMINI_API_KEY");
35210
+ }
35181
35211
  return void 0;
35182
35212
  }
35183
35213
  function resolveApiKey(db2, roomId, credentialName, envVar) {
@@ -35205,6 +35235,9 @@ function getClerkCredential(db2, credentialName) {
35205
35235
  if (credentialName === "anthropic_api_key") {
35206
35236
  return getClerkApiKey(db2, "anthropic_api");
35207
35237
  }
35238
+ if (credentialName === "gemini_api_key") {
35239
+ return getClerkApiKey(db2, "gemini_api");
35240
+ }
35208
35241
  return null;
35209
35242
  }
35210
35243
  function getRoomCredential(db2, roomId, credentialName) {
@@ -49220,7 +49253,7 @@ init_db();
49220
49253
  async function main() {
49221
49254
  const server = new McpServer({
49222
49255
  name: "quoroom",
49223
- version: true ? "0.1.29" : "0.0.0"
49256
+ version: true ? "0.1.30" : "0.0.0"
49224
49257
  });
49225
49258
  registerMemoryTools(server);
49226
49259
  registerSchedulerTools(server);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quoroom",
3
- "version": "0.1.29",
3
+ "version": "0.1.30",
4
4
  "description": "Autonomous AI agent collective engine — Queen, Workers, Quorum",
5
5
  "main": "./out/mcp/server.js",
6
6
  "bin": {