@stage-labs/metro 0.1.0-beta.145 → 0.1.0-beta.147

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.
@@ -3,8 +3,9 @@ import { readJson, writeSecure } from '@metro-labs/core/secure-fs';
3
3
  import { agentsDir } from '../agents/files.js';
4
4
  import { isRecord } from '@metro-labs/core/is-record';
5
5
  import type { CodexTokens } from './codex-auth.js';
6
+ import { tokensFromDisk as geminiTokensFromDisk, type GeminiTokens } from './gemini-auth.js';
6
7
 
7
- export const PROVIDERS = ['anthropic', 'bedrock', 'openrouter', 'codex'] as const;
8
+ export const PROVIDERS = ['anthropic', 'bedrock', 'openrouter', 'codex', 'gemini'] as const;
8
9
  export type Provider = (typeof PROVIDERS)[number];
9
10
 
10
11
  export interface AnthropicSettings {
@@ -29,6 +30,11 @@ export interface CodexSettings {
29
30
  auth: CodexTokens | null;
30
31
  }
31
32
 
33
+ export interface GeminiSettings {
34
+ model: string;
35
+ auth: GeminiTokens | null;
36
+ }
37
+
32
38
  export interface ModelConfig {
33
39
  version: 1;
34
40
  provider: Provider;
@@ -36,6 +42,7 @@ export interface ModelConfig {
36
42
  bedrock: BedrockSettings;
37
43
  openrouter: OpenRouterSettings;
38
44
  codex: CodexSettings;
45
+ gemini: GeminiSettings;
39
46
  }
40
47
 
41
48
  export interface Route {
@@ -47,7 +54,7 @@ export class ModelConfigError extends Error {}
47
54
 
48
55
  export const MODEL_FILE = 'model.json';
49
56
  const MAX_FIELD = 512;
50
- const PREFIX_RE = /^(anthropic|bedrock|openrouter|codex):(.+)$/;
57
+ const PREFIX_RE = /^(anthropic|bedrock|openrouter|codex|gemini):(.+)$/;
51
58
  const SMALL_RE = /haiku/i;
52
59
 
53
60
  const empty = (): ModelConfig => ({
@@ -57,6 +64,7 @@ const empty = (): ModelConfig => ({
57
64
  bedrock: { region: '', apiKey: '', model: '' },
58
65
  openrouter: { apiKey: '', model: '', zdr: false },
59
66
  codex: { model: '', auth: null },
67
+ gemini: { model: '', auth: null },
60
68
  });
61
69
 
62
70
  const isProvider = (value: unknown): value is Provider =>
@@ -89,6 +97,7 @@ export function parseModelConfig(raw: unknown): ModelConfig {
89
97
  const bedrock = isRecord(raw.bedrock) ? raw.bedrock : {};
90
98
  const openrouter = isRecord(raw.openrouter) ? raw.openrouter : {};
91
99
  const codex = isRecord(raw.codex) ? raw.codex : {};
100
+ const gemini = isRecord(raw.gemini) ? raw.gemini : {};
92
101
  return {
93
102
  version: 1,
94
103
  provider: isProvider(raw.provider) ? raw.provider : 'anthropic',
@@ -96,6 +105,7 @@ export function parseModelConfig(raw: unknown): ModelConfig {
96
105
  bedrock: { region: text(bedrock.region), apiKey: text(bedrock.apiKey), model: text(bedrock.model) },
97
106
  openrouter: { apiKey: text(openrouter.apiKey), model: text(openrouter.model), zdr: openrouter.zdr === true },
98
107
  codex: { model: text(codex.model), auth: tokensFromDisk(codex.auth) },
108
+ gemini: { model: text(gemini.model), auth: geminiTokensFromDisk(gemini.auth) },
99
109
  };
100
110
  }
101
111
 
@@ -130,6 +140,7 @@ export function applyModelUpdate(cfg: ModelConfig, patch: unknown): ModelConfig
130
140
  const bedrock = isRecord(patch.bedrock) ? patch.bedrock : {};
131
141
  const openrouter = isRecord(patch.openrouter) ? patch.openrouter : {};
132
142
  const codex = isRecord(patch.codex) ? patch.codex : {};
143
+ const gemini = isRecord(patch.gemini) ? patch.gemini : {};
133
144
  return {
134
145
  version: 1,
135
146
  provider,
@@ -148,11 +159,14 @@ export function applyModelUpdate(cfg: ModelConfig, patch: unknown): ModelConfig
148
159
  zdr: flag(openrouter, 'zdr', cfg.openrouter.zdr, 'OpenRouter zero data retention'),
149
160
  },
150
161
  codex: { model: field(codex, 'model', cfg.codex.model, 'Codex model'), auth: cfg.codex.auth },
162
+ gemini: { model: field(gemini, 'model', cfg.gemini.model, 'Gemini model'), auth: cfg.gemini.auth },
151
163
  };
152
164
  }
153
165
 
154
166
  export const setCodexAuth = (cfg: ModelConfig, auth: CodexTokens | null): ModelConfig => ({ ...cfg, codex: { ...cfg.codex, auth } });
155
167
 
168
+ export const setGeminiAuth = (cfg: ModelConfig, auth: GeminiTokens | null): ModelConfig => ({ ...cfg, gemini: { ...cfg.gemini, auth } });
169
+
156
170
  const CHECKS: Record<Provider, [(cfg: ModelConfig) => boolean, string][]> = {
157
171
  anthropic: [],
158
172
  bedrock: [
@@ -167,6 +181,10 @@ const CHECKS: Record<Provider, [(cfg: ModelConfig) => boolean, string][]> = {
167
181
  [(cfg) => cfg.codex.auth === null, 'Codex is not connected: sign in with ChatGPT on the Model page.'],
168
182
  [(cfg) => cfg.codex.model === '', 'Codex needs a model id: choose one on the Model page.'],
169
183
  ],
184
+ gemini: [
185
+ [(cfg) => cfg.gemini.auth === null, 'Gemini is not connected: sign in with Google on the Model page.'],
186
+ [(cfg) => cfg.gemini.model === '', 'Gemini needs a model id: choose one on the Model page.'],
187
+ ],
170
188
  };
171
189
 
172
190
  export function notReady(cfg: ModelConfig, provider: Provider = cfg.provider): string | null {
@@ -175,6 +193,7 @@ export function notReady(cfg: ModelConfig, provider: Provider = cfg.provider): s
175
193
 
176
194
  export function publicModelConfig(cfg: ModelConfig): Record<string, unknown> {
177
195
  const auth = cfg.codex.auth;
196
+ const gemini = cfg.gemini.auth;
178
197
  return {
179
198
  provider: cfg.provider,
180
199
  ready: notReady(cfg) === null,
@@ -183,16 +202,22 @@ export function publicModelConfig(cfg: ModelConfig): Record<string, unknown> {
183
202
  bedrock: { region: cfg.bedrock.region, model: cfg.bedrock.model, hasKey: cfg.bedrock.apiKey !== '' },
184
203
  openrouter: { model: cfg.openrouter.model, hasKey: cfg.openrouter.apiKey !== '', zdr: cfg.openrouter.zdr },
185
204
  codex: { model: cfg.codex.model, signedIn: auth !== null, account: auth?.email ?? null, plan: auth?.plan ?? null },
205
+ gemini: { model: cfg.gemini.model, signedIn: gemini !== null, account: gemini?.email ?? null, plan: gemini?.tier ?? null },
186
206
  };
187
207
  }
188
208
 
189
209
  export const isSmallModel = (requested: string): boolean => SMALL_RE.test(requested);
190
210
 
211
+ const DEFAULTS: Record<Provider, (requested: string, cfg: ModelConfig) => string> = {
212
+ openrouter: (requested, cfg) => (requested.includes('/') ? requested : cfg.openrouter.model),
213
+ bedrock: (requested, cfg) => (cfg.bedrock.model === '' ? requested : cfg.bedrock.model),
214
+ codex: (requested, cfg) => (requested.startsWith('gpt-') ? requested : cfg.codex.model),
215
+ gemini: (requested, cfg) => (requested.startsWith('gemini-') ? requested : cfg.gemini.model),
216
+ anthropic: (requested, cfg) => (cfg.anthropic.model === '' || isSmallModel(requested) ? requested : cfg.anthropic.model),
217
+ };
218
+
191
219
  function defaultModelFor(provider: Provider, requested: string, cfg: ModelConfig): string {
192
- if (provider === 'openrouter') return requested.includes('/') ? requested : cfg.openrouter.model;
193
- if (provider === 'bedrock') return cfg.bedrock.model === '' ? requested : cfg.bedrock.model;
194
- if (provider === 'codex') return requested.startsWith('gpt-') ? requested : cfg.codex.model;
195
- return cfg.anthropic.model === '' || isSmallModel(requested) ? requested : cfg.anthropic.model;
220
+ return DEFAULTS[provider](requested, cfg);
196
221
  }
197
222
 
198
223
  export function resolveRoute(requested: string, cfg: ModelConfig): Route {
@@ -1,4 +1,4 @@
1
- export type UsageProvider = 'anthropic' | 'codex' | 'openrouter' | 'bedrock';
1
+ export type UsageProvider = 'anthropic' | 'codex' | 'openrouter' | 'bedrock' | 'gemini';
2
2
 
3
3
  export interface UsageWindow {
4
4
  label: string;
@@ -138,11 +138,6 @@ export function agentLiveness(): Map<string, AgentLiveness> {
138
138
  return activeRegistry?.liveness() ?? new Map<string, AgentLiveness>();
139
139
  }
140
140
 
141
- export async function closeAgentSession(agentId: string): Promise<boolean> {
142
- const scopeKey = sessionScopeKey({ kind: 'agent', agentId });
143
- return (await activeRegistry?.closeScope(scopeKey)) ?? false;
144
- }
145
-
146
141
  export async function createMetroMcp(): Promise<{
147
142
  httpHandler: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
148
143
  startInbound: () => void;
@@ -43,7 +43,6 @@ import {
43
43
  localListAgents,
44
44
  localOwnedAgentOrThrow,
45
45
  localOwner,
46
- localResetAgentKey,
47
46
  readLocalAgentFile,
48
47
  } from '../agents/file-admin.js';
49
48
  import { readModelConfig } from '../gateway/model-config.js';
@@ -54,8 +53,6 @@ export interface LocalModeDeps {
54
53
  reloadAgents: () => Promise<void>;
55
54
  restart: () => void;
56
55
  stop: () => void;
57
- closeAgentSession: (id: string) => Promise<boolean>;
58
- restartClaudeSession: () => boolean;
59
56
  gatherAccounts: AgentApiDeps['gatherAccounts'];
60
57
  capabilities: AgentApiDeps['capabilities'];
61
58
  liveness: AgentApiDeps['liveness'];
@@ -89,13 +86,6 @@ function agentApi(deps: LocalModeDeps): AgentApiDeps {
89
86
  attachSessions: attachSessions(deps),
90
87
  listAgents: localListAgents,
91
88
  deleteAgent: localDeleteAgent,
92
- resetKey: async (subject, id) => {
93
- const reset = await localResetAgentKey(subject, id);
94
- const closed = await deps.closeAgentSession(id);
95
- const restarted = deps.restartClaudeSession();
96
- log.info({ agent: reset.name, id, sessionClosed: closed, claudeRestarted: restarted }, 'local: key rotated');
97
- return reset;
98
- },
99
89
  gatherAccounts: deps.gatherAccounts,
100
90
  capabilities: deps.capabilities,
101
91
  attachable: ATTACHABLE.filter((s) => s !== 'webhook'),
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "0.1.0-beta.145"
2
+ "version": "0.1.0-beta.147"
3
3
  }