@stage-labs/metro 0.1.0-beta.227 → 0.1.0-beta.228
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/package.json +1 -1
- package/runtime/marketplace/plugin/.claude-plugin/plugin.json +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-translate.ts +19 -5
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-translate.ts +16 -5
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/text.ts +25 -4
- package/runtime/runtime.json +1 -1
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.228",
|
|
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",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { isRecord } from '@metro-labs/core/is-record';
|
|
3
|
-
import { resultText } from './text.js';
|
|
3
|
+
import { inlineImage, partText, resultImages, resultParts, resultText, TOOL_ERROR, type InlineImage } from './text.js';
|
|
4
4
|
import { stringOf } from '@metro-labs/http/api-http';
|
|
5
5
|
|
|
6
6
|
const SIGNATURE_PREFIX = 'metro-codex:';
|
|
@@ -82,11 +82,25 @@ export function decodeSignature(signature: unknown): Item | null {
|
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
const imagePart = (image: InlineImage): Item => ({ type: 'input_image', image_url: `data:${image.mediaType};base64,${image.data}` });
|
|
86
|
+
|
|
85
87
|
function userPart(block: Item): Item | null {
|
|
86
88
|
if (block.type === 'text') return { type: 'input_text', text: stringOf(block.text) };
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
89
|
+
const image = inlineImage(block);
|
|
90
|
+
return image === null ? null : imagePart(image);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function outputPart(part: Item): Item | null {
|
|
94
|
+
const image = inlineImage(part);
|
|
95
|
+
if (image !== null) return imagePart(image);
|
|
96
|
+
const text = partText(part);
|
|
97
|
+
return text === '' ? null : { type: 'input_text', text };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function toolOutput(block: Item): string | Item[] {
|
|
101
|
+
if (resultImages(block).length === 0) return resultText(block);
|
|
102
|
+
const parts = resultParts(block).map(outputPart).filter((part): part is Item => part !== null);
|
|
103
|
+
return block.is_error === true ? [{ type: 'input_text', text: TOOL_ERROR }, ...parts] : parts;
|
|
90
104
|
}
|
|
91
105
|
|
|
92
106
|
function userItems(content: unknown): Item[] {
|
|
@@ -101,7 +115,7 @@ function userItems(content: unknown): Item[] {
|
|
|
101
115
|
for (const block of content.filter(isRecord)) {
|
|
102
116
|
if (block.type === 'tool_result') {
|
|
103
117
|
flush();
|
|
104
|
-
out.push({ type: 'function_call_output', call_id: stringOf(block.tool_use_id), output:
|
|
118
|
+
out.push({ type: 'function_call_output', call_id: stringOf(block.tool_use_id), output: toolOutput(block) });
|
|
105
119
|
continue;
|
|
106
120
|
}
|
|
107
121
|
const part = userPart(block);
|
|
@@ -3,7 +3,7 @@ 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 } from './text.js';
|
|
6
|
+
import { inlineImage, resultImages, resultText, type InlineImage } from './text.js';
|
|
7
7
|
import { stringOf } from '@metro-labs/http/api-http';
|
|
8
8
|
|
|
9
9
|
type Item = Record<string, unknown>;
|
|
@@ -63,11 +63,22 @@ export function systemText(system: unknown): string {
|
|
|
63
63
|
.trim();
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
const imagePart = (image: InlineImage): Item => ({ inlineData: { mimeType: image.mediaType, data: image.data } });
|
|
67
|
+
|
|
68
|
+
const GEMINI_IMAGES = new Set(['image/png', 'image/jpeg', 'image/webp', 'image/heic', 'image/heif']);
|
|
69
|
+
const geminiSees = (mediaType: string): boolean => GEMINI_IMAGES.has(mediaType);
|
|
70
|
+
|
|
71
|
+
function toolParts(block: Item, id: string, name: string): Item[] {
|
|
72
|
+
const images = resultImages(block, geminiSees);
|
|
73
|
+
const text = resultText(block, geminiSees);
|
|
74
|
+
const result = text === '' && images.length > 0 ? `Binary content provided (${String(images.length)} item(s)).` : text;
|
|
75
|
+
return [{ functionResponse: { id, name, response: { result } } }, ...images.map(imagePart)];
|
|
76
|
+
}
|
|
77
|
+
|
|
66
78
|
function userPart(block: Item): Item | null {
|
|
67
79
|
if (block.type === 'text') return { text: stringOf(block.text) };
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
return null;
|
|
80
|
+
const image = inlineImage(block);
|
|
81
|
+
return image === null ? null : imagePart(image);
|
|
71
82
|
}
|
|
72
83
|
|
|
73
84
|
function userParts(content: unknown, calls: Map<string, string>): Item[] {
|
|
@@ -77,7 +88,7 @@ function userParts(content: unknown, calls: Map<string, string>): Item[] {
|
|
|
77
88
|
for (const block of content.filter(isRecord)) {
|
|
78
89
|
if (block.type === 'tool_result') {
|
|
79
90
|
const id = stringOf(block.tool_use_id);
|
|
80
|
-
out.push(
|
|
91
|
+
out.push(...toolParts(block, id, calls.get(id) ?? 'tool'));
|
|
81
92
|
continue;
|
|
82
93
|
}
|
|
83
94
|
const part = userPart(block);
|
|
@@ -2,13 +2,34 @@ import { isRecord } from '@metro-labs/core/is-record';
|
|
|
2
2
|
import { stringOf } from '@metro-labs/http/api-http';
|
|
3
3
|
|
|
4
4
|
const IMAGE_NOTE = '[an image was attached here; this model cannot see it]';
|
|
5
|
+
export const TOOL_ERROR = '[tool error]';
|
|
5
6
|
|
|
6
7
|
export const nonEmpty = (value: unknown): string | null => (typeof value === 'string' && value !== '' ? value : null);
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
export interface InlineImage {
|
|
10
|
+
mediaType: string;
|
|
11
|
+
data: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type Part = Record<string, unknown>;
|
|
15
|
+
type Accepts = (mediaType: string) => boolean;
|
|
16
|
+
const anyImage: Accepts = () => true;
|
|
17
|
+
|
|
18
|
+
export function inlineImage(block: Part, accepts = anyImage): InlineImage | null {
|
|
19
|
+
if (block.type !== 'image' || !isRecord(block.source) || block.source.type !== 'base64') return null;
|
|
20
|
+
const image = { mediaType: stringOf(block.source.media_type), data: stringOf(block.source.data) };
|
|
21
|
+
return image.mediaType !== '' && image.data !== '' && accepts(image.mediaType) ? image : null;
|
|
22
|
+
}
|
|
9
23
|
|
|
10
|
-
export
|
|
24
|
+
export const resultParts = (block: Part): Part[] => (Array.isArray(block.content) ? block.content.filter(isRecord) : []);
|
|
25
|
+
|
|
26
|
+
export const partText = (part: Part, accepts = anyImage): string =>
|
|
27
|
+
part.type === 'text' ? stringOf(part.text) : part.type === 'image' && inlineImage(part, accepts) === null ? IMAGE_NOTE : '';
|
|
28
|
+
|
|
29
|
+
export function resultText(block: Part, accepts = anyImage): string {
|
|
11
30
|
const content = block.content;
|
|
12
|
-
const text = typeof content === 'string' ? content :
|
|
13
|
-
return block.is_error === true ?
|
|
31
|
+
const text = typeof content === 'string' ? content : resultParts(block).map((part) => partText(part, accepts)).filter((t) => t !== '').join('\n');
|
|
32
|
+
return block.is_error === true ? `${TOOL_ERROR} ${text}` : text;
|
|
14
33
|
}
|
|
34
|
+
|
|
35
|
+
export const resultImages = (block: Part, accepts = anyImage): InlineImage[] => resultParts(block).flatMap((part) => inlineImage(part, accepts) ?? []);
|
package/runtime/runtime.json
CHANGED