@stage-labs/metro 0.1.0-beta.201 → 0.1.0-beta.202

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 (163) hide show
  1. package/dist/claude.js +4 -15
  2. package/dist/cli.js +5 -3
  3. package/dist/hold.js +1 -2
  4. package/dist/runtime-install.js +4 -5
  5. package/dist/runtime.js +1 -4
  6. package/dist/serve.js +4 -5
  7. package/dist/service.js +82 -21
  8. package/dist/update.js +1 -1
  9. package/package.json +2 -2
  10. package/runtime/marketplace/plugin/.claude-plugin/plugin.json +1 -1
  11. package/runtime/node_modules/@metro-labs/core/src/events.ts +4 -2
  12. package/runtime/node_modules/@metro-labs/core/src/lines.ts +12 -25
  13. package/runtime/node_modules/@metro-labs/core/src/stations/account-store.ts +45 -22
  14. package/runtime/node_modules/@metro-labs/core/src/stations/attachments.ts +0 -1
  15. package/runtime/node_modules/@metro-labs/core/src/stations/messaging-normalize.ts +8 -80
  16. package/runtime/node_modules/@metro-labs/core/src/stations/station-runtime.ts +3 -5
  17. package/runtime/node_modules/@metro-labs/core/src/stations/train-events.ts +4 -9
  18. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/files.ts +9 -39
  19. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/job-env.ts +1 -1
  20. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/provision.ts +9 -28
  21. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/schedule-detail.ts +0 -1
  22. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/schedules-api.ts +3 -3
  23. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/schedules.ts +8 -116
  24. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/unit-files.ts +11 -35
  25. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/user.ts +7 -9
  26. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/view.ts +1 -1
  27. package/runtime/node_modules/@metro-labs/daemon/src/agents/account-lookups.ts +1 -1
  28. package/runtime/node_modules/@metro-labs/daemon/src/agents/accounts-api.ts +1 -1
  29. package/runtime/node_modules/@metro-labs/daemon/src/agents/allowlist.ts +1 -1
  30. package/runtime/node_modules/@metro-labs/daemon/src/agents/bundle.ts +3 -3
  31. package/runtime/node_modules/@metro-labs/daemon/src/agents/file-admin.ts +2 -2
  32. package/runtime/node_modules/@metro-labs/daemon/src/agents/files.ts +3 -29
  33. package/runtime/node_modules/@metro-labs/daemon/src/approvals/pending.ts +5 -8
  34. package/runtime/node_modules/@metro-labs/daemon/src/approvals/preview.ts +1 -1
  35. package/runtime/node_modules/@metro-labs/daemon/src/boot/boot.ts +5 -8
  36. package/runtime/node_modules/@metro-labs/daemon/src/boot/connect-hint.ts +3 -6
  37. package/runtime/node_modules/@metro-labs/daemon/src/channels/inbound.ts +25 -3
  38. package/runtime/node_modules/@metro-labs/daemon/src/channels/media-note.ts +2 -3
  39. package/runtime/node_modules/@metro-labs/daemon/src/claude/files.ts +2 -2
  40. package/runtime/node_modules/@metro-labs/daemon/src/claude/login.ts +1 -1
  41. package/runtime/node_modules/@metro-labs/daemon/src/claude/memory.ts +6 -56
  42. package/runtime/node_modules/@metro-labs/daemon/src/claude/plugin-install.ts +3 -3
  43. package/runtime/node_modules/@metro-labs/daemon/src/claude/session-files.ts +1 -1
  44. package/runtime/node_modules/@metro-labs/daemon/src/claude/session.ts +7 -6
  45. package/runtime/node_modules/@metro-labs/daemon/src/claude/settings.ts +1 -1
  46. package/runtime/node_modules/@metro-labs/daemon/src/claude/setup.ts +7 -7
  47. package/runtime/node_modules/@metro-labs/daemon/src/claude/skills.ts +4 -4
  48. package/runtime/node_modules/@metro-labs/daemon/src/claude/version.ts +1 -1
  49. package/runtime/node_modules/@metro-labs/daemon/src/connectors/api.ts +2 -4
  50. package/runtime/node_modules/@metro-labs/daemon/src/connectors/config.ts +19 -22
  51. package/runtime/node_modules/@metro-labs/daemon/src/connectors/gates.ts +1 -2
  52. package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-discovery.ts +6 -8
  53. package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-routes.ts +3 -6
  54. package/runtime/node_modules/@metro-labs/daemon/src/connectors/plugin-sync.ts +1 -1
  55. package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay.ts +2 -3
  56. package/runtime/node_modules/@metro-labs/daemon/src/connectors/store.ts +28 -1
  57. package/runtime/node_modules/@metro-labs/daemon/src/files/attach-serve.ts +1 -1
  58. package/runtime/node_modules/@metro-labs/daemon/src/gateway/bedrock.ts +4 -4
  59. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-auth.ts +1 -1
  60. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-device.ts +1 -1
  61. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-stream.ts +1 -1
  62. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-translate.ts +3 -2
  63. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex.ts +1 -1
  64. package/runtime/node_modules/@metro-labs/daemon/src/gateway/effort.ts +2 -2
  65. package/runtime/node_modules/@metro-labs/daemon/src/gateway/eventstream.ts +1 -1
  66. package/runtime/node_modules/@metro-labs/daemon/src/gateway/forward.ts +3 -3
  67. package/runtime/node_modules/@metro-labs/daemon/src/gateway/frames.ts +1 -1
  68. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gateway.ts +3 -3
  69. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-auth.ts +6 -6
  70. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-client.ts +7 -7
  71. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-stream.ts +1 -1
  72. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-translate.ts +3 -2
  73. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini.ts +1 -1
  74. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-config.ts +10 -31
  75. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-signin.ts +1 -1
  76. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-store.ts +1 -1
  77. package/runtime/node_modules/@metro-labs/daemon/src/gateway/openrouter.ts +2 -2
  78. package/runtime/node_modules/@metro-labs/daemon/src/gateway/provider-models.ts +3 -3
  79. package/runtime/node_modules/@metro-labs/daemon/src/gateway/text.ts +1 -2
  80. package/runtime/node_modules/@metro-labs/daemon/src/gateway/usage.ts +2 -2
  81. package/runtime/node_modules/@metro-labs/daemon/src/mcp/policy-gate.ts +5 -6
  82. package/runtime/node_modules/@metro-labs/daemon/src/mcp/policy-snapshot.ts +3 -3
  83. package/runtime/node_modules/@metro-labs/daemon/src/mcp/profile-lookup.ts +2 -2
  84. package/runtime/node_modules/@metro-labs/daemon/src/mcp/read-tool.ts +2 -2
  85. package/runtime/node_modules/@metro-labs/daemon/src/metro-user/helper-script.ts +2 -2
  86. package/runtime/node_modules/@metro-labs/daemon/src/metro-user/install.ts +37 -0
  87. package/runtime/node_modules/@metro-labs/daemon/src/net/tunnel.ts +2 -2
  88. package/runtime/node_modules/@metro-labs/daemon/src/policy/policy.ts +1 -1
  89. package/runtime/node_modules/@metro-labs/daemon/src/routes/local-mode.ts +1 -4
  90. package/runtime/node_modules/@metro-labs/daemon/src/routes/session-apis.ts +3 -8
  91. package/runtime/node_modules/@metro-labs/daemon/src/server/machine.ts +2 -2
  92. package/runtime/node_modules/@metro-labs/daemon/src/server/update.ts +2 -2
  93. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-session.ts +2 -2
  94. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-xmtp.ts +6 -7
  95. package/runtime/node_modules/@metro-labs/daemon/src/stations/supervisor-io.ts +1 -1
  96. package/runtime/node_modules/@metro-labs/daemon/src/stations/supervisor.ts +2 -2
  97. package/runtime/node_modules/@metro-labs/daemon/src/terminal/api.ts +2 -2
  98. package/runtime/node_modules/@metro-labs/daemon/src/terminal/socket.ts +1 -1
  99. package/runtime/node_modules/@metro-labs/daemon/src/vault/firewall.ts +5 -44
  100. package/runtime/node_modules/@metro-labs/daemon/src/vault/index.ts +4 -4
  101. package/runtime/node_modules/@metro-labs/daemon/src/vault/proxy.ts +6 -7
  102. package/runtime/node_modules/@metro-labs/daemon/src/vault/store.ts +12 -13
  103. package/runtime/node_modules/@metro-labs/daemon/src/vault/trust.ts +5 -22
  104. package/runtime/node_modules/@metro-labs/discord-bot/src/accounts.ts +1 -19
  105. package/runtime/node_modules/@metro-labs/discord-bot/src/actions.ts +7 -35
  106. package/runtime/node_modules/@metro-labs/discord-bot/src/format.ts +5 -10
  107. package/runtime/node_modules/@metro-labs/discord-bot/src/group-actions.ts +1 -1
  108. package/runtime/node_modules/@metro-labs/discord-bot/src/index.ts +1 -2
  109. package/runtime/node_modules/@metro-labs/discord-bot/src/profile.ts +1 -1
  110. package/runtime/node_modules/@metro-labs/discord-bot/src/send-files.ts +11 -12
  111. package/runtime/node_modules/@metro-labs/http/src/api-http.ts +2 -5
  112. package/runtime/node_modules/@metro-labs/http/src/mode-api.ts +0 -1
  113. package/runtime/node_modules/@metro-labs/outlook/src/accounts.ts +1 -15
  114. package/runtime/node_modules/@metro-labs/outlook/src/actions.ts +0 -6
  115. package/runtime/node_modules/@metro-labs/outlook/src/format.ts +0 -1
  116. package/runtime/node_modules/@metro-labs/outlook/src/state.ts +1 -1
  117. package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +4 -22
  118. package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +2 -2
  119. package/runtime/node_modules/@metro-labs/telegram/src/format.ts +1 -3
  120. package/runtime/node_modules/@metro-labs/telegram/src/inbound.ts +1 -1
  121. package/runtime/node_modules/@metro-labs/telegram/src/media-actions.ts +1 -2
  122. package/runtime/node_modules/@metro-labs/telegram/src/reactions.ts +1 -1
  123. package/runtime/node_modules/@metro-labs/telegram-bot/src/accounts.ts +4 -37
  124. package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +7 -23
  125. package/runtime/node_modules/@metro-labs/telegram-bot/src/format.ts +1 -4
  126. package/runtime/node_modules/@metro-labs/telegram-bot/src/media-actions.ts +4 -9
  127. package/runtime/node_modules/@metro-labs/telegram-bot/src/profile.ts +3 -3
  128. package/runtime/node_modules/@metro-labs/threema/src/accounts.ts +0 -11
  129. package/runtime/node_modules/@metro-labs/threema/src/actions.ts +2 -12
  130. package/runtime/node_modules/@metro-labs/threema/src/files.ts +4 -15
  131. package/runtime/node_modules/@metro-labs/threema/src/format.ts +2 -7
  132. package/runtime/node_modules/@metro-labs/whatsapp/package.json +1 -2
  133. package/runtime/node_modules/@metro-labs/whatsapp/src/accounts.ts +5 -26
  134. package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +5 -6
  135. package/runtime/node_modules/@metro-labs/whatsapp/src/format.ts +1 -3
  136. package/runtime/node_modules/@metro-labs/whatsapp/src/inbound.ts +1 -1
  137. package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +1 -12
  138. package/runtime/node_modules/@metro-labs/xmtp/src/actions-close.ts +1 -1
  139. package/runtime/node_modules/@metro-labs/xmtp/src/actions-conv.ts +14 -5
  140. package/runtime/node_modules/@metro-labs/xmtp/src/actions-meta.ts +1 -1
  141. package/runtime/node_modules/@metro-labs/xmtp/src/actions.ts +31 -78
  142. package/runtime/node_modules/@metro-labs/xmtp/src/emit.ts +3 -4
  143. package/runtime/node_modules/@metro-labs/xmtp/src/index.ts +1 -6
  144. package/runtime/node_modules/@metro-labs/xmtp/src/profile.ts +1 -1
  145. package/runtime/node_modules/@metro-labs/xmtp/src/sender.ts +1 -1
  146. package/runtime/node_modules/@metro-labs/xmtp/src/tools-handlers.ts +1 -1
  147. package/runtime/node_modules/@metro-labs/xmtp/src/transcribe.ts +2 -2
  148. package/runtime/node_modules/@metro-labs/xmtp/src/wire.ts +0 -2
  149. package/runtime/runtime.json +1 -1
  150. package/runtime/node_modules/@metro-labs/core/src/identity.ts +0 -7
  151. package/runtime/node_modules/@metro-labs/daemon/src/approvals/needs.ts +0 -2
  152. package/runtime/node_modules/@metro-labs/daemon/src/channels/dedupe.ts +0 -12
  153. package/runtime/node_modules/@metro-labs/daemon/src/channels/react-note.ts +0 -13
  154. package/runtime/node_modules/@metro-labs/daemon/src/channels/reply-id.ts +0 -1
  155. package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay-target.ts +0 -29
  156. package/runtime/node_modules/@metro-labs/daemon/src/metro-user/api.ts +0 -43
  157. package/runtime/node_modules/@metro-labs/daemon/src/metro-user/move.ts +0 -165
  158. package/runtime/node_modules/@metro-labs/discord-bot/src/wire.ts +0 -1
  159. package/runtime/node_modules/@metro-labs/telegram/src/normalize.ts +0 -22
  160. package/runtime/node_modules/@metro-labs/telegram/src/wire.ts +0 -1
  161. package/runtime/node_modules/@metro-labs/telegram-bot/src/wire.ts +0 -1
  162. package/runtime/node_modules/@metro-labs/whatsapp/src/normalize.ts +0 -22
  163. package/runtime/node_modules/@metro-labs/whatsapp/src/wire.ts +0 -1
@@ -4,8 +4,8 @@ import { ApiError } from '@metro-labs/http/api-error';
4
4
  import { claudeDir } from './files.js';
5
5
  import { removeHome, writeHomeText } from '../agent-user/home-fs.js';
6
6
 
7
- export const SKILL_MAX = 256 * 1024;
8
- export const SKILL_NAME_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
7
+ const SKILL_MAX = 256 * 1024;
8
+ const SKILL_NAME_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
9
9
  const FILE = 'SKILL.md';
10
10
  const DEFAULT_MODE = 0o644;
11
11
  const SUMMARY_MAX = 300;
@@ -44,7 +44,7 @@ function entryOf(name: string, path: string): ClaudeSkill {
44
44
  };
45
45
  }
46
46
 
47
- export const userSkillsRoot = (dir: string): string => join(dir, 'skills');
47
+ const userSkillsRoot = (dir: string): string => join(dir, 'skills');
48
48
 
49
49
  function isFolder(path: string): boolean {
50
50
  try {
@@ -97,7 +97,7 @@ export function writeClaudeSkill(
97
97
  return entryOf(skill.name, skill.path);
98
98
  }
99
99
 
100
- export const skillTemplate = (name: string): string =>
100
+ const skillTemplate = (name: string): string =>
101
101
  ['---', `name: ${name}`, 'description: what this skill does, and when Claude should reach for it', '---', '', `# ${name}`, '', 'Write the instructions here.', ''].join('\n');
102
102
 
103
103
  export function createClaudeSkill(name: string, text: string | undefined, dir = claudeDir()): ClaudeSkill {
@@ -61,7 +61,7 @@ export function newerThan(a: string, b: string): boolean {
61
61
  return false;
62
62
  }
63
63
 
64
- export async function latestClaudeVersion(deps: VersionDeps = {}): Promise<string | null> {
64
+ async function latestClaudeVersion(deps: VersionDeps = {}): Promise<string | null> {
65
65
  const now = (deps.now ?? Date.now)();
66
66
  if (cached !== null && now - cached.at < CACHE_MS) return cached.latest;
67
67
  const fetchImpl = deps.fetchImpl ?? fetch;
@@ -10,6 +10,7 @@ import {
10
10
  cors,
11
11
  readJsonBody,
12
12
  sendJson,
13
+ stringOf,
13
14
  } from '@metro-labs/http/api-http';
14
15
  import { parseId } from '@metro-labs/core/ids';
15
16
  import {
@@ -29,9 +30,6 @@ import type {
29
30
 
30
31
  const PREFIX = '/api/connectors';
31
32
 
32
- const asText = (value: unknown): string =>
33
- typeof value === 'string' ? value : '';
34
-
35
33
  export interface ConnectorApiDeps extends OAuthRouteDeps {
36
34
  listConnectors: () => Promise<Connector[]>;
37
35
  createConnector: (input: ConnectorInput) => Promise<Connector>;
@@ -112,7 +110,7 @@ async function handleCreate(
112
110
  deps: ConnectorApiDeps,
113
111
  ): Promise<void> {
114
112
  const body = await readJsonBody(req);
115
- const offered = asText(bodyField(body, 'value')).trim() !== '';
113
+ const offered = stringOf(bodyField(body, 'value')).trim() !== '';
116
114
  try {
117
115
  const created = await deps.createConnector({
118
116
  name: bodyField(body, 'name'),
@@ -1,4 +1,5 @@
1
1
  import { ApiError } from '@metro-labs/http/api-error';
2
+ import { stringOf } from '@metro-labs/http/api-http';
2
3
  import type {
3
4
  ConnectorAuth,
4
5
  VerifiedRecord,
@@ -30,10 +31,6 @@ export interface ConnectorConfig {
30
31
  toolGroups?: Record<string, ToolGroup>;
31
32
  }
32
33
 
33
- function text(value: unknown): string {
34
- return typeof value === 'string' ? value : '';
35
- }
36
-
37
34
  function hasControlChar(value: string): boolean {
38
35
  for (const ch of value) {
39
36
  const code = ch.codePointAt(0) ?? 0;
@@ -62,8 +59,8 @@ export function connectorAuth(
62
59
  rawHeader: unknown,
63
60
  rawValue: unknown,
64
61
  ): ConnectorAuth {
65
- const header = text(rawHeader).trim();
66
- const value = text(rawValue).trim();
62
+ const header = stringOf(rawHeader).trim();
63
+ const value = stringOf(rawValue).trim();
67
64
  if (header === '' && value === '') return { kind: 'none' };
68
65
  if (value === '')
69
66
  throw new ConnectorError(
@@ -82,8 +79,8 @@ export function connectorClient(
82
79
  rawId: unknown,
83
80
  rawSecret: unknown,
84
81
  ): OAuthClient | null {
85
- const clientId = text(rawId).trim();
86
- const clientSecret = text(rawSecret).trim();
82
+ const clientId = stringOf(rawId).trim();
83
+ const clientSecret = stringOf(rawSecret).trim();
87
84
  if (clientId === '' && clientSecret === '') return null;
88
85
  if (clientId === '')
89
86
  throw new ConnectorError('a client secret needs the client ID it belongs to', 400);
@@ -96,27 +93,27 @@ export function connectorClient(
96
93
 
97
94
  function readClient(raw: unknown): OAuthClient | null {
98
95
  if (!isRecord(raw)) return null;
99
- const clientId = text(raw.clientId);
96
+ const clientId = stringOf(raw.clientId);
100
97
  if (clientId === '') return null;
101
- const clientSecret = text(raw.clientSecret);
98
+ const clientSecret = stringOf(raw.clientSecret);
102
99
  return { clientId, ...(clientSecret === '' ? {} : { clientSecret }) };
103
100
  }
104
101
 
105
102
  function readOAuth(raw: Record<string, unknown>): ConnectorAuth {
106
- const accessToken = text(raw.accessToken);
107
- const clientId = text(raw.clientId);
108
- const tokenEndpoint = text(raw.tokenEndpoint);
103
+ const accessToken = stringOf(raw.accessToken);
104
+ const clientId = stringOf(raw.clientId);
105
+ const tokenEndpoint = stringOf(raw.tokenEndpoint);
109
106
  if (accessToken === '' || clientId === '' || tokenEndpoint === '')
110
107
  return { kind: 'none' };
111
- const refreshToken = text(raw.refreshToken);
112
- const clientSecret = text(raw.clientSecret);
113
- const scope = text(raw.scope);
108
+ const refreshToken = stringOf(raw.refreshToken);
109
+ const clientSecret = stringOf(raw.clientSecret);
110
+ const scope = stringOf(raw.scope);
114
111
  return {
115
112
  kind: 'oauth',
116
113
  accessToken,
117
114
  clientId,
118
115
  tokenEndpoint,
119
- issuer: text(raw.issuer),
116
+ issuer: stringOf(raw.issuer),
120
117
  ...(refreshToken === '' ? {} : { refreshToken }),
121
118
  ...(clientSecret === '' ? {} : { clientSecret }),
122
119
  ...(scope === '' ? {} : { scope }),
@@ -128,21 +125,21 @@ function readAuth(raw: unknown): ConnectorAuth {
128
125
  if (!isRecord(raw)) return { kind: 'none' };
129
126
  if (raw.kind === 'oauth') return readOAuth(raw);
130
127
  if (raw.kind !== 'header') return { kind: 'none' };
131
- const name = text(raw.name);
132
- const value = text(raw.value);
128
+ const name = stringOf(raw.name);
129
+ const value = stringOf(raw.value);
133
130
  if (name === '' || value === '') return { kind: 'none' };
134
131
  return { kind: 'header', name, value };
135
132
  }
136
133
 
137
134
  function readVerified(raw: unknown): VerifiedRecord {
138
135
  const record = isRecord(raw) ? raw : {};
139
- return { at: text(record.at), server: text(record.server) };
136
+ return { at: stringOf(record.at), server: stringOf(record.server) };
140
137
  }
141
138
 
142
139
  const TOOL_NAME_MAX = 128;
143
140
  const TOOL_GROUPS_MAX = 1000;
144
141
 
145
- export function readToolGroups(raw: unknown): Record<string, ToolGroup> | undefined {
142
+ function readToolGroups(raw: unknown): Record<string, ToolGroup> | undefined {
146
143
  if (!isRecord(raw)) return undefined;
147
144
  const kept = Object.entries(raw)
148
145
  .filter((entry): entry is [string, ToolGroup] => entry[0].length <= TOOL_NAME_MAX && (entry[1] === 'read' || entry[1] === 'write'))
@@ -157,7 +154,7 @@ export function readConfig(raw: unknown): ConnectorConfig {
157
154
  const toolGroups = readToolGroups(record.toolGroups);
158
155
  return {
159
156
  auth,
160
- createdAt: text(record.createdAt),
157
+ createdAt: stringOf(record.createdAt),
161
158
  verified: readVerified(record.verified),
162
159
  oauth: record.oauth === true || auth.kind === 'oauth',
163
160
  client: readClient(record.client),
@@ -1,8 +1,7 @@
1
1
  import type { ToolGroup } from '@metro-labs/core/stations/types';
2
2
  import { policyFor, setPolicies, type PolicyTarget, type ToolPolicy } from '../policy/policy.js';
3
3
  import { serverKeysOf } from './plugin-sync.js';
4
- import { takeGrant } from '../approvals/pending.js';
5
- import { NEEDS_APPROVAL } from '../approvals/needs.js';
4
+ import { NEEDS_APPROVAL, takeGrant } from '../approvals/pending.js';
6
5
 
7
6
  interface GateRow {
8
7
  id: string;
@@ -1,4 +1,5 @@
1
1
  import { parseConnectorUrl, refused } from './verify.js';
2
+ import { stringOf } from '@metro-labs/http/api-http';
2
3
  import { isRecord } from '@metro-labs/core/is-record';
3
4
 
4
5
  const DISCOVERY_TIMEOUT_MS = 10_000;
@@ -17,9 +18,6 @@ interface ResourceMetadata {
17
18
  scopes: string[];
18
19
  }
19
20
 
20
- const str = (value: unknown): string =>
21
- typeof value === 'string' ? value : '';
22
-
23
21
  async function getJson(url: URL): Promise<Record<string, unknown> | null> {
24
22
  let res: Response;
25
23
  try {
@@ -72,7 +70,7 @@ function scopesOf(body: Record<string, unknown> | null): string[] {
72
70
  if (!Array.isArray(raw)) return [];
73
71
  const out: string[] = [];
74
72
  for (const entry of raw) {
75
- const scope = str(entry).trim();
73
+ const scope = stringOf(entry).trim();
76
74
  if (scope !== '' && !/\s/.test(scope) && !out.includes(scope)) out.push(scope);
77
75
  }
78
76
  return out;
@@ -94,16 +92,16 @@ function toServer(
94
92
  issuer: URL,
95
93
  scopes: string[],
96
94
  ): OAuthServer {
97
- const authorizationEndpoint = str(body.authorization_endpoint);
98
- const tokenEndpoint = str(body.token_endpoint);
95
+ const authorizationEndpoint = stringOf(body.authorization_endpoint);
96
+ const tokenEndpoint = stringOf(body.token_endpoint);
99
97
  if (authorizationEndpoint === '' || tokenEndpoint === '')
100
98
  throw refused(
101
99
  `${issuer.hostname} advertises OAuth but not where to sign in.`,
102
100
  );
103
101
  const methods = body.code_challenge_methods_supported;
104
- const registration = str(body.registration_endpoint);
102
+ const registration = stringOf(body.registration_endpoint);
105
103
  return {
106
- issuer: str(body.issuer) === '' ? issuer.origin : str(body.issuer),
104
+ issuer: stringOf(body.issuer) === '' ? issuer.origin : stringOf(body.issuer),
107
105
  authorizationEndpoint,
108
106
  tokenEndpoint,
109
107
  registrationEndpoint: registration === '' ? null : registration,
@@ -1,6 +1,6 @@
1
1
  import type { IncomingMessage, ServerResponse } from 'node:http';
2
2
  import { errMsg, log } from '@metro-labs/core/log';
3
- import { bodyField, readJsonBody, sendJson } from '@metro-labs/http/api-http';
3
+ import { bodyField, readJsonBody, sendJson, stringOf } from '@metro-labs/http/api-http';
4
4
  import {
5
5
  beginOAuth,
6
6
  completeOAuth,
@@ -13,9 +13,6 @@ import { parseConnectorUrl } from './verify.js';
13
13
  import type { OAuthAuth } from './verify.js';
14
14
  import type { Connector, PendingConnectorInput } from './store.js';
15
15
 
16
- const asText = (value: unknown): string =>
17
- typeof value === 'string' ? value : '';
18
-
19
16
  export function hostOf(url: string): string {
20
17
  try {
21
18
  return new URL(url).hostname;
@@ -38,7 +35,7 @@ export async function startOAuth(
38
35
  payload: (row: Connector) => Record<string, unknown>,
39
36
  ): Promise<void> {
40
37
  const url = parseConnectorUrl(bodyField(body, 'url'));
41
- const returnTo = asText(bodyField(body, 'returnTo'));
38
+ const returnTo = stringOf(bodyField(body, 'returnTo'));
42
39
  const clientId = bodyField(body, 'clientId');
43
40
  const clientSecret = bodyField(body, 'clientSecret');
44
41
  const prepared = await prepareOAuth({
@@ -74,7 +71,7 @@ export async function handleConnect(
74
71
  const row = await deps.getConnector(id);
75
72
  const url = parseConnectorUrl(row.url);
76
73
  const body = await readJsonBody(req);
77
- const returnTo = asText(bodyField(body, 'returnTo'));
74
+ const returnTo = stringOf(bodyField(body, 'returnTo'));
78
75
  const prepared = await prepareOAuth({ url, client: row.client, returnTo });
79
76
  const authorize = beginOAuth(prepared, {
80
77
  name: row.name,
@@ -69,7 +69,7 @@ function pluginRootsUnder(dir: string, depth: number, out: string[]): void {
69
69
  pluginRootsUnder(join(dir, entry.name), depth - 1, out);
70
70
  }
71
71
 
72
- export function stagedPluginDir(env: NodeJS.ProcessEnv = process.env): string | null {
72
+ function stagedPluginDir(env: NodeJS.ProcessEnv = process.env): string | null {
73
73
  const store = env.METRO_RUNTIME_STORE?.trim() ?? '';
74
74
  if (store === '') return null;
75
75
  const user = agentUser();
@@ -1,15 +1,14 @@
1
1
  import type { IncomingMessage, ServerResponse } from 'node:http';
2
2
  import { whyUnreachable } from './reach.js';
3
3
  import { noteHealth } from './health.js';
4
- import type { RelayTarget } from './relay-target.js';
5
- import type { AgentIdentity } from '@metro-labs/http/api-http';
4
+ import type { RelayTarget } from './store.js';
6
5
  import { ApiError } from '@metro-labs/http/api-error';
7
6
  import { errMsg, log } from '@metro-labs/core/log';
8
7
  import { answerBlocked, mergeBlocked, screenCalls, type BlockedReason } from './relay-policy.js';
9
8
 
10
9
  export interface RelayApiDeps {
11
10
  target: (connectorId: string, force: boolean) => Promise<RelayTarget>;
12
- identify: (req: IncomingMessage) => AgentIdentity | null;
11
+ identify: (req: IncomingMessage) => { subject: string; agentId: string } | null;
13
12
  signedOut: (connectorId: string) => void;
14
13
  blocked?: BlockedReason;
15
14
  }
@@ -30,7 +30,6 @@ import {
30
30
  type ConnectorSignIn,
31
31
  } from './config.js';
32
32
  import type { OAuthClient } from './oauth-client.js';
33
- import { fixedTarget, unrefreshedTarget, type RelayTarget } from './relay-target.js';
34
33
  import { agentsDir } from '../agents/files.js';
35
34
  import { newId } from '@metro-labs/core/ids';
36
35
  import type { LoadedConnector } from '../stations/materialize.js';
@@ -333,6 +332,34 @@ function refreshOnce(row: LocalConnectorRow, auth: OAuthAuth, dir: string): Prom
333
332
  return job;
334
333
  }
335
334
 
335
+ export type RelayTarget =
336
+ | { kind: 'ok'; url: string; headers: Record<string, string> }
337
+ | { kind: 'missing' }
338
+ | { kind: 'signin' };
339
+
340
+ function staleUsable(auth: OAuthAuth, now = Date.now()): boolean {
341
+ return auth.expiresAt === undefined || auth.expiresAt > now;
342
+ }
343
+
344
+ function unrefreshedTarget(
345
+ url: string,
346
+ auth: OAuthAuth,
347
+ force: boolean,
348
+ now = Date.now(),
349
+ ): RelayTarget {
350
+ if (force) return { kind: 'signin' };
351
+ return { kind: 'ok', url, headers: staleUsable(auth, now) ? authHeaders(auth) : {} };
352
+ }
353
+
354
+ function fixedTarget(
355
+ url: string,
356
+ auth: Exclude<ConnectorAuth, OAuthAuth>,
357
+ force: boolean,
358
+ ): RelayTarget {
359
+ if (force) return { kind: 'signin' };
360
+ return { kind: 'ok', url, headers: authHeaders(auth) };
361
+ }
362
+
336
363
  async function oauthTarget(row: LocalConnectorRow, auth: OAuthAuth, force: boolean, dir: string): Promise<RelayTarget> {
337
364
  if (!force && !oauthExpired(auth)) return { kind: 'ok', url: row.url, headers: authHeaders(auth) };
338
365
  try {
@@ -56,7 +56,7 @@ export function attachmentEventUrl(
56
56
  ): string | null {
57
57
  if (payload.contentType !== 'attachmentSaved') return null;
58
58
  if (typeof payload.url === 'string' && payload.url.length > 0) return null;
59
- const p = payload.attachmentPath ?? payload.localPath;
59
+ const p = payload.attachmentPath;
60
60
  return typeof p === 'string' ? attachmentUrl(p, agentId) : null;
61
61
  }
62
62
 
@@ -43,12 +43,12 @@ const PREFIXES: [RegExp, string][] = [
43
43
  [/^ap-/, 'apac'],
44
44
  ];
45
45
 
46
- export function regionPrefix(region: string): string {
46
+ function regionPrefix(region: string): string {
47
47
  for (const [pattern, prefix] of PREFIXES) if (pattern.test(region)) return prefix;
48
48
  return 'global';
49
49
  }
50
50
 
51
- export function bedrockModelId(requested: string, region: string): string {
51
+ function bedrockModelId(requested: string, region: string): string {
52
52
  if (requested.includes('anthropic.')) return requested;
53
53
  return `${regionPrefix(region)}.anthropic.${requested}`;
54
54
  }
@@ -63,7 +63,7 @@ function splitBetas(header: string | string[] | undefined): string[] {
63
63
  .filter((b) => b !== '');
64
64
  }
65
65
 
66
- export function rewriteForBedrock(
66
+ function rewriteForBedrock(
67
67
  body: Record<string, unknown>,
68
68
  model: string,
69
69
  betaHeader: string | string[] | undefined,
@@ -78,7 +78,7 @@ export function rewriteForBedrock(
78
78
  return { modelId: bedrockModelId(model, region), stream: body.stream === true, body: out, betas };
79
79
  }
80
80
 
81
- export function errorKind(status: number, errorType: string | null): string {
81
+ function errorKind(status: number, errorType: string | null): string {
82
82
  if (status === 429 || errorType === 'ThrottlingException') return 'rate_limit_error';
83
83
  if (status === 400) return 'invalid_request_error';
84
84
  if (status === 401 || status === 403) return 'permission_error';
@@ -10,7 +10,7 @@ export const CODEX_ISSUER = 'https://auth.openai.com';
10
10
  export const CODEX_REDIRECT = 'http://localhost:1455/auth/callback';
11
11
  const SCOPE = 'openid profile email offline_access api.connectors.read api.connectors.invoke';
12
12
  const AUTH_CLAIM = 'https://api.openai.com/auth';
13
- export const ACCESS_TOKEN_TTL_MS = 55 * 60_000;
13
+ const ACCESS_TOKEN_TTL_MS = 55 * 60_000;
14
14
 
15
15
  export interface CodexTokens {
16
16
  accessToken: string;
@@ -2,7 +2,7 @@ import { randomBytes } from 'node:crypto';
2
2
  import { isRecord } from '@metro-labs/core/is-record';
3
3
  import { errMsg } from '@metro-labs/core/log';
4
4
  import { CODEX_CLIENT_ID, CODEX_ISSUER, CodexAuthError, exchangeCode, type CodexTokens } from './codex-auth.js';
5
- import { stringOf } from './text.js';
5
+ import { stringOf } from '@metro-labs/http/api-http';
6
6
 
7
7
  const VERIFY_PATH = '/codex/device';
8
8
  const DEVICE_TTL_MS = 15 * 60_000;
@@ -2,7 +2,7 @@ import { isRecord } from '@metro-labs/core/is-record';
2
2
  import { encodeSignature } from './codex-translate.js';
3
3
  import { errorFrame } from './forward.js';
4
4
  import { frame, messageEnd, messageStart, type Usage } from './frames.js';
5
- import { stringOf } from './text.js';
5
+ import { stringOf } from '@metro-labs/http/api-http';
6
6
 
7
7
  type Item = Record<string, unknown>;
8
8
 
@@ -1,8 +1,9 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { isRecord } from '@metro-labs/core/is-record';
3
- import { resultText, stringOf } from './text.js';
3
+ import { resultText } from './text.js';
4
+ import { stringOf } from '@metro-labs/http/api-http';
4
5
 
5
- export const SIGNATURE_PREFIX = 'metro-codex:';
6
+ const SIGNATURE_PREFIX = 'metro-codex:';
6
7
  const DEFAULT_EFFORT = 'medium';
7
8
  const EFFORTS = new Set(['minimal', 'low', 'medium', 'high', 'xhigh']);
8
9
  const EFFORT_ALIAS: Record<string, string> = { max: 'xhigh' };
@@ -11,7 +11,7 @@ import type { Connection } from './model-config.js';
11
11
  import { answerWhole, currentOf, errorKind, reach, refreshed, relayTranslated, sessionHeader, type TokenSource, type TokenState } from './subscription.js';
12
12
  import { noteUsageHeaders } from './usage.js';
13
13
 
14
- export const CODEX_BASE = 'https://chatgpt.com/backend-api/codex';
14
+ const CODEX_BASE = 'https://chatgpt.com/backend-api/codex';
15
15
  const CODEX_VERSION = '0.153.4';
16
16
  const VERSION_RE = /^\d+\.\d+\.\d+(?:-[A-Za-z0-9.]+)?$/;
17
17
  const INVALID = [400, 404, 422];
@@ -1,8 +1,8 @@
1
1
  import type { IncomingMessage } from 'node:http';
2
2
  import { isRecord } from '@metro-labs/core/is-record';
3
3
 
4
- export const MAIN_EFFORT = 'low';
5
- export const SUBAGENT_EFFORT = 'max';
4
+ const MAIN_EFFORT = 'low';
5
+ const SUBAGENT_EFFORT = 'max';
6
6
  export const BINDING_BETA = 'thinking-binding-controls-2026-08-01';
7
7
 
8
8
  const SUBAGENT_HEADER = 'x-claude-code-agent-id';
@@ -56,7 +56,7 @@ function readHeaders(buf: Buffer): Record<string, string> {
56
56
  return out;
57
57
  }
58
58
 
59
- export function decodeFrame(buf: Buffer, offset: number): { message: EventStreamMessage; end: number } | null {
59
+ function decodeFrame(buf: Buffer, offset: number): { message: EventStreamMessage; end: number } | null {
60
60
  if (buf.length - offset < PRELUDE_BYTES) return null;
61
61
  const total = buf.readUInt32BE(offset);
62
62
  const headersLen = buf.readUInt32BE(offset + 4);
@@ -11,7 +11,7 @@ export class GatewayError extends Error {
11
11
  }
12
12
  }
13
13
 
14
- export const BODY_MAX = 64 * 1024 * 1024;
14
+ const BODY_MAX = 64 * 1024 * 1024;
15
15
  const DRAIN_FACTOR = 2;
16
16
  const PING_MS = 25_000;
17
17
  const PING_FRAME = 'event: ping\ndata: {"type":"ping"}\n\n';
@@ -40,9 +40,9 @@ export const errorFrame = (kind: string, message: string): string => `event: err
40
40
 
41
41
  export const providerStatus = (status: number): number => (status === 401 ? 403 : status);
42
42
 
43
- export const idleMs = (): number => Number(process.env.METRO_GATEWAY_IDLE_MS) || IDLE_MS;
43
+ const idleMs = (): number => Number(process.env.METRO_GATEWAY_IDLE_MS) || IDLE_MS;
44
44
 
45
- export const idleMessage = (ms: number): string => `metro gateway: the provider sent nothing for ${String(Math.round(ms / 1000))}s; giving up on this request`;
45
+ const idleMessage = (ms: number): string => `metro gateway: the provider sent nothing for ${String(Math.round(ms / 1000))}s; giving up on this request`;
46
46
 
47
47
  export interface Watch {
48
48
  signal: AbortSignal;
@@ -1,5 +1,5 @@
1
1
  import { isRecord } from '@metro-labs/core/is-record';
2
- import { stringOf } from './text.js';
2
+ import { stringOf } from '@metro-labs/http/api-http';
3
3
 
4
4
  type Item = Record<string, unknown>;
5
5
 
@@ -24,8 +24,8 @@ import { forgetServed, noteServed } from './served.js';
24
24
  import { forgetUsage, noteUsageHeaders, UsageScanner } from './usage.js';
25
25
  import type { CodexTokens } from './codex-auth.js';
26
26
 
27
- export const GATEWAY_PREFIX = '/gateway';
28
- export const ANTHROPIC_BASE = 'https://api.anthropic.com';
27
+ const GATEWAY_PREFIX = '/gateway';
28
+ const ANTHROPIC_BASE = 'https://api.anthropic.com';
29
29
  const MESSAGES = '/v1/messages';
30
30
  const COUNT = '/v1/messages/count_tokens';
31
31
  const MODELS = '/v1/models';
@@ -178,7 +178,7 @@ async function toOpenRouter(
178
178
 
179
179
  const thinkingOff = (body: Record<string, unknown>): boolean => isRecord(body.thinking) && body.thinking.type === 'disabled';
180
180
 
181
- export function openrouterBody(body: Record<string, unknown>, model: string, zdr: boolean): Record<string, unknown> {
181
+ function openrouterBody(body: Record<string, unknown>, model: string, zdr: boolean): Record<string, unknown> {
182
182
  const sent: Record<string, unknown> = { ...body, model };
183
183
  if (thinkingOff(body)) delete sent.thinking;
184
184
  const effort = effortToApply(body);
@@ -2,11 +2,11 @@ import { isRecord } from '@metro-labs/core/is-record';
2
2
  import { PendingLogins } from './pkce.js';
3
3
  import { nonEmpty } from './text.js';
4
4
 
5
- export const GEMINI_CLIENT_ID = '1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com';
6
- export const GEMINI_CLIENT_SECRET = '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';
7
- export const GEMINI_AUTH_BASE = 'https://accounts.google.com';
8
- export const GEMINI_TOKEN_BASE = 'https://oauth2.googleapis.com';
9
- export const GEMINI_REDIRECT = 'http://localhost:51121/oauth-callback';
5
+ const GEMINI_CLIENT_ID = '1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com';
6
+ const GEMINI_CLIENT_SECRET = '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';
7
+ const GEMINI_AUTH_BASE = 'https://accounts.google.com';
8
+ const GEMINI_TOKEN_BASE = 'https://oauth2.googleapis.com';
9
+ const GEMINI_REDIRECT = 'http://localhost:51121/oauth-callback';
10
10
  const SCOPE = [
11
11
  'https://www.googleapis.com/auth/cloud-platform',
12
12
  'https://www.googleapis.com/auth/userinfo.email',
@@ -93,7 +93,7 @@ async function tokenCall(form: Record<string, string>, base: string, fetchImpl:
93
93
  return body;
94
94
  }
95
95
 
96
- export function codeIn(pasted: string, state: string): string {
96
+ function codeIn(pasted: string, state: string): string {
97
97
  const trimmed = pasted.trim();
98
98
  if (trimmed === '') throw new GeminiAuthError('paste the address the browser landed on, or the code in it');
99
99
  if (!/^https?:\/\//i.test(trimmed)) return trimmed;
@@ -1,13 +1,13 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
 
3
- export const CODE_ASSIST_BASE = 'https://cloudcode-pa.googleapis.com';
4
- export const CODE_ASSIST_DAILY = 'https://daily-cloudcode-pa.googleapis.com';
3
+ const CODE_ASSIST_BASE = 'https://cloudcode-pa.googleapis.com';
4
+ const CODE_ASSIST_DAILY = 'https://daily-cloudcode-pa.googleapis.com';
5
5
  export const API = 'v1internal';
6
- export const APP_VERSION = '2.0.3';
7
- export const CLIENT_VERSION = '1.110.0';
6
+ const APP_VERSION = '2.0.3';
7
+ const CLIENT_VERSION = '1.110.0';
8
8
  export const CLIENT_NAME = 'antigravity';
9
- export const IDE_TYPE = 9;
10
- export const PLUGIN_TYPE = 2;
9
+ const IDE_TYPE = 9;
10
+ const PLUGIN_TYPE = 2;
11
11
  export const SYSTEM_PREFIX =
12
12
  'You are Antigravity, a powerful agentic AI coding assistant designed by the Google Deepmind team working on Advanced Agentic Coding.You are pair programming with a USER to solve their coding task. The task may require creating a new codebase, modifying or debugging an existing codebase, or simply answering a question.**Absolute paths only****Proactiveness**';
13
13
 
@@ -15,7 +15,7 @@ const PLATFORMS: Record<string, number> = { 'darwin-x64': 1, 'darwin-arm64': 2,
15
15
 
16
16
  const osName = (): string => (process.platform === 'darwin' ? 'darwin' : process.platform === 'win32' ? 'win32' : 'linux');
17
17
 
18
- export const platformCode = (): number => PLATFORMS[`${process.platform}-${process.arch}`] ?? 0;
18
+ const platformCode = (): number => PLATFORMS[`${process.platform}-${process.arch}`] ?? 0;
19
19
 
20
20
  export const clientMetadata = (project: string | null): Record<string, unknown> => ({
21
21
  ideType: IDE_TYPE,
@@ -2,7 +2,7 @@ import { isRecord } from '@metro-labs/core/is-record';
2
2
  import { errorFrame } from './forward.js';
3
3
  import { frame, messageEnd, messageStart, type Usage } from './frames.js';
4
4
  import { encodeSignature, newCallId, rememberSignature } from './gemini-translate.js';
5
- import { stringOf } from './text.js';
5
+ import { stringOf } from '@metro-labs/http/api-http';
6
6
 
7
7
  type Item = Record<string, unknown>;
8
8
 
@@ -3,11 +3,12 @@ import { isRecord } from '@metro-labs/core/is-record';
3
3
  import { ToolNames } from './codex-translate.js';
4
4
  import { CLIENT_NAME, requestId, SYSTEM_PREFIX } from './gemini-client.js';
5
5
  import { cappedEffort, effortToApply } from './effort.js';
6
- import { resultText, stringOf } from './text.js';
6
+ import { resultText } from './text.js';
7
+ import { stringOf } from '@metro-labs/http/api-http';
7
8
 
8
9
  type Item = Record<string, unknown>;
9
10
 
10
- export const SIGNATURE_PREFIX = 'metro-gemini:';
11
+ const SIGNATURE_PREFIX = 'metro-gemini:';
11
12
  export const SKIP_SIGNATURE = 'skip_thought_signature_validator';
12
13
  const SCHEMA_KEEP = new Set([
13
14
  'type',
@@ -91,7 +91,7 @@ const detailText = (detail: Record<string, unknown>): string[] => {
91
91
  return out.filter((x) => x !== '');
92
92
  };
93
93
 
94
- export function refusalMessage(text: string, status: number): string {
94
+ function refusalMessage(text: string, status: number): string {
95
95
  const message = upstreamMessage(text, `Gemini answered ${String(status)}`);
96
96
  let details: unknown = null;
97
97
  try {