@stage-labs/metro 0.1.0-beta.218 → 0.1.0-beta.219
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/dist/claude.js +3 -3
- package/dist/route.js +2 -12
- package/package.json +1 -1
- package/runtime/marketplace/plugin/.claude-plugin/plugin.json +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gateway.ts +7 -5
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/tool-search.ts +92 -28
- package/runtime/runtime.json +1 -1
package/dist/claude.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn, spawnSync } from 'node:child_process';
|
|
2
2
|
import { markOnboardingDone, seedChannels } from './onboarding.js';
|
|
3
3
|
import { writeMcpConfig } from './mcp-config.js';
|
|
4
|
-
import {
|
|
4
|
+
import { currentRoute, permissionMode, routeModelEnv, systemPrompt } from './route.js';
|
|
5
5
|
import { settingsConflicts, settingsFiles } from './claude-settings.js';
|
|
6
6
|
import { localAgent } from './local.js';
|
|
7
7
|
import { PROVIDER_FLAGS } from './provider-flags.js';
|
|
@@ -40,7 +40,7 @@ export function gatewayEnv(base, agentKey, port) {
|
|
|
40
40
|
};
|
|
41
41
|
}
|
|
42
42
|
export const channelEnv = (env) => set(env, 'MCP_PROTOCOL_NEGOTIATION') ? env : { ...env, MCP_PROTOCOL_NEGOTIATION: 'legacy' };
|
|
43
|
-
export const toolSearchEnv = (env
|
|
43
|
+
export const toolSearchEnv = (env) => set(env, 'ENABLE_TOOL_SEARCH') ? env : { ...env, ENABLE_TOOL_SEARCH: 'true' };
|
|
44
44
|
const OWN_CREDENTIALS = ['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN'];
|
|
45
45
|
export function credentialEnv(env, agentKey, signedIn) {
|
|
46
46
|
if (signedIn || OWN_CREDENTIALS.some((name) => set(env, name)))
|
|
@@ -125,7 +125,7 @@ function mcpConfigFor(key, port) {
|
|
|
125
125
|
function gatewayLaunchEnv(key, port) {
|
|
126
126
|
process.stderr.write(`metro claude: inference goes through the daemon at http://127.0.0.1:${String(port)}/gateway (the Model page decides where)\n`);
|
|
127
127
|
const pointed = routeModelEnv(gatewayEnv(process.env, key, port), currentRoute());
|
|
128
|
-
const routed = toolSearchEnv(pointed
|
|
128
|
+
const routed = toolSearchEnv(pointed);
|
|
129
129
|
if (routed !== pointed)
|
|
130
130
|
process.stderr.write('metro claude: MCP tool search is on, so connector tools load when Claude needs them instead of all at start\n');
|
|
131
131
|
const env = credentialEnv(routed, key, claudeSignedIn());
|
package/dist/route.js
CHANGED
|
@@ -16,27 +16,17 @@ function routeIn(cfg) {
|
|
|
16
16
|
const model = text(conn.model);
|
|
17
17
|
return PROVIDERS.has(provider) && model !== '' ? `${provider}:${model}` : null;
|
|
18
18
|
}
|
|
19
|
-
function
|
|
19
|
+
export function currentRoute(dir = agentsDir()) {
|
|
20
20
|
const path = join(dir, 'model.json');
|
|
21
21
|
if (!existsSync(path))
|
|
22
22
|
return null;
|
|
23
23
|
try {
|
|
24
|
-
return JSON.parse(readFileSync(path, 'utf8'));
|
|
24
|
+
return routeIn(JSON.parse(readFileSync(path, 'utf8')));
|
|
25
25
|
}
|
|
26
26
|
catch {
|
|
27
27
|
return null;
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
|
-
export const currentRoute = (dir = agentsDir()) => routeIn(modelConfig(dir));
|
|
31
|
-
const isRecord = (value) => typeof value === 'object' && value !== null;
|
|
32
|
-
export function anthropicRoute(dir = agentsDir()) {
|
|
33
|
-
const cfg = modelConfig(dir);
|
|
34
|
-
if (!isRecord(cfg) || !Array.isArray(cfg.connections))
|
|
35
|
-
return true;
|
|
36
|
-
const connections = cfg.connections.filter(isRecord);
|
|
37
|
-
const routed = connections.find((c) => c.id === cfg.route) ?? connections[0];
|
|
38
|
-
return routed === undefined || routed.provider === 'anthropic';
|
|
39
|
-
}
|
|
40
30
|
export function routeModelEnv(env, route) {
|
|
41
31
|
if (route === null || (env.ANTHROPIC_MODEL ?? '').trim() !== '')
|
|
42
32
|
return env;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "metro",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.219",
|
|
4
4
|
"description": "Metro for Claude Code, installed by the metro daemon on this machine: every connector the agent holds as its own MCP server relayed through the daemon, the orchestrator-only guard on the main thread, and the standing rules loaded at each session start. No vendor credential ever sits in a config file.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Bonustrack",
|
|
@@ -205,13 +205,15 @@ function shapedFor(req: IncomingMessage, sent: Record<string, unknown>): Record<
|
|
|
205
205
|
return effort === null ? sent : withEffort(sent, effort);
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
+
const bytesOf = (raw: Buffer, parsed: Record<string, unknown>, sent: Record<string, unknown>): Buffer => (sent === parsed ? raw : Buffer.from(JSON.stringify(sent)));
|
|
209
|
+
|
|
208
210
|
async function dispatch(req: IncomingMessage, res: ServerResponse, path: string, deps: GatewayDeps): Promise<void> {
|
|
209
211
|
const cfg = deps.config();
|
|
210
212
|
const raw = await readBody(req);
|
|
211
|
-
const
|
|
212
|
-
const
|
|
213
|
-
const route = resolveRoute(requestedModel(
|
|
214
|
-
const body =
|
|
213
|
+
const parsed = parseJson(raw);
|
|
214
|
+
const sent = fitToolSearch(req, parsed);
|
|
215
|
+
const route = resolveRoute(requestedModel(sent), cfg) ?? passthrough(sent);
|
|
216
|
+
const body = shapedFor(req, sent);
|
|
215
217
|
const conn = route.connection;
|
|
216
218
|
log.info({ route: routeLabel(route), connection: conn.label, path }, 'gateway: routing');
|
|
217
219
|
if (path === MESSAGES) noteServed({ connection: conn.id, provider: conn.provider, model: route.model, at: new Date().toISOString() });
|
|
@@ -231,7 +233,7 @@ async function dispatch(req: IncomingMessage, res: ServerResponse, path: string,
|
|
|
231
233
|
await toSubscription(req, res, path, body, route, deps);
|
|
232
234
|
return;
|
|
233
235
|
}
|
|
234
|
-
await toAnthropic(req, res, raw, sent, body, route, deps);
|
|
236
|
+
await toAnthropic(req, res, bytesOf(raw, parsed, sent), sent, body, route, deps);
|
|
235
237
|
}
|
|
236
238
|
|
|
237
239
|
function failed(res: ServerResponse, err: unknown): void {
|
|
@@ -1,46 +1,111 @@
|
|
|
1
1
|
import type { IncomingMessage } from 'node:http';
|
|
2
2
|
import { isRecord } from '@metro-labs/core/is-record';
|
|
3
3
|
import { stringOf } from '@metro-labs/http/api-http';
|
|
4
|
-
import type { Route } from './model-config.js';
|
|
5
4
|
|
|
6
5
|
type Body = Record<string, unknown>;
|
|
7
6
|
|
|
8
|
-
const TOOL_SEARCH_BETA = /^(advanced-tool-use|tool-search-tool)-/;
|
|
9
|
-
const
|
|
7
|
+
const TOOL_SEARCH_BETA = /^(advanced-tool-use|tool-search-tool|mid-conversation-tool-changes)-/;
|
|
8
|
+
const PLACEHOLDER = 'DeferredToolPlaceholder';
|
|
9
|
+
const NOTHING_FOUND = 'No matching deferred tools found';
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
route.connection.provider === 'anthropic' && !BEFORE_TOOL_REFERENCE.test(route.model);
|
|
11
|
+
const blocksOf = (message: unknown): unknown[] => (isRecord(message) && Array.isArray(message.content) ? message.content : []);
|
|
13
12
|
|
|
14
|
-
const
|
|
13
|
+
const isResult = (block: unknown): block is Body & { content: unknown[] } => isRecord(block) && block.type === 'tool_result' && Array.isArray(block.content);
|
|
15
14
|
|
|
16
|
-
const
|
|
17
|
-
|
|
15
|
+
const inlineDefinition = (block: unknown): Body | null =>
|
|
16
|
+
isRecord(block) && block.type === 'tool_addition' && isRecord(block.tool) && block.tool.type === 'tool_definition' && isRecord(block.tool.definition) ? block.tool.definition : null;
|
|
18
17
|
|
|
19
|
-
|
|
18
|
+
function loadedName(block: unknown): string | null {
|
|
19
|
+
if (!isRecord(block)) return null;
|
|
20
|
+
if (block.type === 'tool_reference') return stringOf(block.tool_name);
|
|
21
|
+
if (block.type !== 'tool_addition' || !isRecord(block.tool)) return null;
|
|
22
|
+
return stringOf(inlineDefinition(block)?.name ?? block.tool.name);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const removedName = (block: unknown): string | null => (isRecord(block) && block.type === 'tool_removal' && isRecord(block.tool) ? stringOf(block.tool.name) : null);
|
|
26
|
+
|
|
27
|
+
const changes = (block: unknown): boolean => loadedName(block) !== null || removedName(block) !== null;
|
|
28
|
+
|
|
29
|
+
const changing = (message: unknown): boolean => blocksOf(message).some((block) => changes(block) || (isResult(block) && block.content.some(changes)));
|
|
30
|
+
|
|
31
|
+
const functionLine = (tool: Body): string =>
|
|
32
|
+
`<function>${JSON.stringify({ description: stringOf(tool.description), name: tool.name, parameters: isRecord(tool.input_schema) ? tool.input_schema : {} })}</function>`;
|
|
33
|
+
|
|
34
|
+
function functionsText(blocks: unknown[], catalog: Map<string, Body>): string {
|
|
35
|
+
const lines = blocks.flatMap((block) => {
|
|
36
|
+
const tool = catalog.get(loadedName(block) ?? '');
|
|
37
|
+
return tool === undefined ? [] : [functionLine(tool)];
|
|
38
|
+
});
|
|
39
|
+
return lines.length === 0 ? NOTHING_FOUND : ['<functions>', ...lines, '</functions>'].join('\n');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const cacheOf = (blocks: unknown[]): Body => {
|
|
43
|
+
const marked = blocks.find((block) => isRecord(block) && block.cache_control !== undefined);
|
|
44
|
+
return isRecord(marked) ? { cache_control: marked.cache_control } : {};
|
|
45
|
+
};
|
|
20
46
|
|
|
21
|
-
|
|
47
|
+
function expanded(blocks: unknown[], catalog: Map<string, Body>): unknown[] {
|
|
48
|
+
const found = blocks.filter((block) => loadedName(block) !== null);
|
|
49
|
+
return blocks.flatMap((block) => {
|
|
50
|
+
if (block === found[0]) return [{ type: 'text', text: functionsText(found, catalog), ...cacheOf(found) }];
|
|
51
|
+
if (loadedName(block) !== null) return [];
|
|
52
|
+
const removed = removedName(block);
|
|
53
|
+
return removed === null ? [block] : [{ type: 'text', text: `Tool ${removed} is no longer available.`, ...cacheOf([block]) }];
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const expandedBlock = (block: unknown, catalog: Map<string, Body>): unknown =>
|
|
58
|
+
isResult(block) && block.content.some(changes) ? { ...block, content: expanded(block.content, catalog) } : block;
|
|
59
|
+
|
|
60
|
+
function expandedMessage(message: unknown, catalog: Map<string, Body>): unknown {
|
|
61
|
+
if (!changing(message) || !isRecord(message)) return message;
|
|
62
|
+
return { ...message, content: expanded(blocksOf(message).map((block) => expandedBlock(block, catalog)), catalog) };
|
|
63
|
+
}
|
|
22
64
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if (block.type === 'tool_result' && Array.isArray(block.content)) return { ...block, content: block.content.map(flatBlock) };
|
|
27
|
-
return block;
|
|
65
|
+
interface Found {
|
|
66
|
+
names: Set<string>;
|
|
67
|
+
inline: Map<string, Body>;
|
|
28
68
|
}
|
|
29
69
|
|
|
30
|
-
|
|
31
|
-
|
|
70
|
+
function foundIn(messages: unknown[]): Found {
|
|
71
|
+
const found: Found = { names: new Set(), inline: new Map() };
|
|
72
|
+
const blocks = messages.flatMap(blocksOf).flatMap((block) => (isResult(block) ? block.content : [block]));
|
|
73
|
+
for (const block of blocks) {
|
|
74
|
+
const name = loadedName(block);
|
|
75
|
+
const definition = inlineDefinition(block);
|
|
76
|
+
if (name !== null) found.names.add(name);
|
|
77
|
+
if (definition !== null) found.inline.set(stringOf(definition.name), definition);
|
|
78
|
+
const removed = removedName(block);
|
|
79
|
+
if (removed === null) continue;
|
|
80
|
+
found.names.delete(removed);
|
|
81
|
+
found.inline.delete(removed);
|
|
82
|
+
}
|
|
83
|
+
return found;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const deferred = (tool: unknown): boolean => isRecord(tool) && tool.defer_loading === true;
|
|
87
|
+
|
|
88
|
+
const loaded = (tool: unknown): unknown =>
|
|
89
|
+
isRecord(tool) && 'defer_loading' in tool ? Object.fromEntries(Object.entries(tool).filter(([key]) => key !== 'defer_loading')) : tool;
|
|
90
|
+
|
|
91
|
+
const deferring = (tools: unknown): boolean => Array.isArray(tools) && tools.some((tool) => isRecord(tool) && 'defer_loading' in tool);
|
|
32
92
|
|
|
33
|
-
const
|
|
34
|
-
deferred(tool) ? Object.fromEntries(Object.entries(tool).filter(([key]) => key !== 'defer_loading')) : tool;
|
|
93
|
+
const nameOf = (tool: unknown): string => (isRecord(tool) ? stringOf(tool.name) : '');
|
|
35
94
|
|
|
36
|
-
export function
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
95
|
+
export function resolveToolSearch(body: Body): Body {
|
|
96
|
+
const messages: unknown[] = Array.isArray(body.messages) ? body.messages : [];
|
|
97
|
+
if (!deferring(body.tools) && !messages.some(changing)) return body;
|
|
98
|
+
const declared: unknown[] = Array.isArray(body.tools) ? body.tools : [];
|
|
99
|
+
const found = foundIn(messages);
|
|
100
|
+
const shown = declared.filter((tool) => nameOf(tool) !== PLACEHOLDER && (!deferred(tool) || found.names.has(nameOf(tool)))).map(loaded);
|
|
101
|
+
const known = new Set(shown.map(nameOf));
|
|
102
|
+
const inline = [...found.inline.values()].filter((tool) => !known.has(nameOf(tool))).map(loaded);
|
|
103
|
+
const tools = [...shown, ...inline];
|
|
104
|
+
const catalog = new Map([...declared, ...inline].filter(isRecord).map((tool) => [nameOf(tool), tool]));
|
|
40
105
|
return {
|
|
41
106
|
...body,
|
|
42
|
-
...(Array.isArray(body.tools) ? { tools
|
|
43
|
-
...(Array.isArray(body.messages) ? { messages: messages.map(
|
|
107
|
+
...(Array.isArray(body.tools) || inline.length > 0 ? { tools } : {}),
|
|
108
|
+
...(Array.isArray(body.messages) ? { messages: messages.map((message) => expandedMessage(message, catalog)) } : {}),
|
|
44
109
|
};
|
|
45
110
|
}
|
|
46
111
|
|
|
@@ -55,8 +120,7 @@ function dropToolSearchBeta(req: IncomingMessage): void {
|
|
|
55
120
|
else delete req.headers['anthropic-beta'];
|
|
56
121
|
}
|
|
57
122
|
|
|
58
|
-
export function fitToolSearch(req: IncomingMessage, body: Body
|
|
59
|
-
if (takesToolSearch(route)) return body;
|
|
123
|
+
export function fitToolSearch(req: IncomingMessage, body: Body): Body {
|
|
60
124
|
dropToolSearchBeta(req);
|
|
61
|
-
return
|
|
125
|
+
return resolveToolSearch(body);
|
|
62
126
|
}
|
package/runtime/runtime.json
CHANGED