@stage-labs/metro 0.1.0-beta.285 → 0.1.0-beta.286

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage-labs/metro",
3
- "version": "0.1.0-beta.285",
3
+ "version": "0.1.0-beta.286",
4
4
  "description": "The metro command line: run your agent on this machine, relaying its chat channels and connectors to Claude Code.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metro",
3
- "version": "0.1.0-beta.285",
3
+ "version": "0.1.0-beta.286",
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",
@@ -0,0 +1,40 @@
1
+ export const frameSchema = {
2
+ type: 'object',
3
+ description:
4
+ 'Stage (XMTP) only: a frame, a small interactive view. Stage shows it in the chat at image size ' +
5
+ '(400 x 400 at most, its start screen, clipped with a fade) and opens it full screen on a tap; its ' +
6
+ 'buttons work only there. `widget` is OpenAI ChatKit widget JSON (a Card, ListView ' +
7
+ 'or Basic root, 64K characters at most); a missing `title` or `description` is taken from the ' +
8
+ 'widget. Or give `screens` instead of `widget` for several screens in one frame. A tap there comes back ' +
9
+ 'to you as a reply to the frame: "Frame action: <type> <payload ' +
10
+ "JSON>\". It is sent after the text, and the message_id returned is the frame's. The stage " +
11
+ 'skill lists the widget nodes, with an example.',
12
+ properties: {
13
+ widget: {
14
+ type: 'object',
15
+ description:
16
+ 'ChatKit widget JSON, e.g. {"type":"Card","children":[{"type":"Title","value":"Deploy?"},' +
17
+ '{"type":"Button","label":"Ship it","onClickAction":{"type":"deploy","payload":{"env":"prod"}}}]}.',
18
+ additionalProperties: true,
19
+ },
20
+ screens: {
21
+ type: 'object',
22
+ description:
23
+ 'Instead of `widget`: up to 50 screens by id, each a widget or {"title", "widget"}, 64K ' +
24
+ 'characters in all. An action {"type":"frame.open","payload":{"screen":"<id>"}} opens a ' +
25
+ 'screen and {"type":"frame.back"} goes back, in Stage, with no message to you.',
26
+ additionalProperties: true,
27
+ },
28
+ start: { type: 'string', description: 'Required with `screens`: the id of the first screen.' },
29
+ title: { type: 'string', description: 'Card title in the chat (200 characters at most).' },
30
+ description: { type: 'string', description: 'Card description in the chat (1000 characters at most).' },
31
+ source: {
32
+ type: 'object',
33
+ description:
34
+ 'Optional: {"url": "https://…"}, a node URL that serves this widget live (2048 characters at most). ' +
35
+ 'Add to dashboard on the frame then adds a live widget loaded from that URL instead of a copy.',
36
+ properties: { url: { type: 'string' } },
37
+ required: ['url'],
38
+ },
39
+ },
40
+ } as const;
@@ -1,5 +1,6 @@
1
1
  import type { ToolDef } from './tool-def.js';
2
2
  import { attachmentItem, CREATE_UPLOAD_TOOL } from './attachment-schema.js';
3
+ import { frameSchema } from './frame-schema.js';
3
4
  import { speechSchema } from './speech-schema.js';
4
5
 
5
6
  const lineProp = {
@@ -69,38 +70,7 @@ export const COMMON_TOOLS: ToolDef[] = [
69
70
  description: 'Optional files to attach.',
70
71
  items: attachmentItem,
71
72
  },
72
- frame: {
73
- type: 'object',
74
- description:
75
- 'Stage (XMTP) only: a frame, a small interactive view. Stage shows it in the chat at image size ' +
76
- '(400 x 400 at most, its start screen, clipped with a fade) and opens it full screen on a tap; its ' +
77
- 'buttons work only there. `widget` is OpenAI ChatKit widget JSON (a Card, ListView ' +
78
- 'or Basic root, 64K characters at most); a missing `title` or `description` is taken from the ' +
79
- 'widget. Or give `screens` instead of `widget` for several screens in one frame. A tap there comes back ' +
80
- 'to you as a reply to the frame: "Frame action: <type> <payload ' +
81
- "JSON>\". It is sent after the text, and the message_id returned is the frame's. The stage " +
82
- 'skill lists the widget nodes, with an example.',
83
- properties: {
84
- widget: {
85
- type: 'object',
86
- description:
87
- 'ChatKit widget JSON, e.g. {"type":"Card","children":[{"type":"Title","value":"Deploy?"},' +
88
- '{"type":"Button","label":"Ship it","onClickAction":{"type":"deploy","payload":{"env":"prod"}}}]}.',
89
- additionalProperties: true,
90
- },
91
- screens: {
92
- type: 'object',
93
- description:
94
- 'Instead of `widget`: up to 50 screens by id, each a widget or {"title", "widget"}, 64K ' +
95
- 'characters in all. An action {"type":"frame.open","payload":{"screen":"<id>"}} opens a ' +
96
- 'screen and {"type":"frame.back"} goes back, in Stage, with no message to you.',
97
- additionalProperties: true,
98
- },
99
- start: { type: 'string', description: 'Required with `screens`: the id of the first screen.' },
100
- title: { type: 'string', description: 'Card title in the chat (200 characters at most).' },
101
- description: { type: 'string', description: 'Card description in the chat (1000 characters at most).' },
102
- },
103
- },
73
+ frame: frameSchema,
104
74
  wallet: {
105
75
  type: 'object',
106
76
  description:
@@ -196,6 +196,7 @@ export interface FrameContent {
196
196
  widget?: Record<string, unknown>;
197
197
  screens?: Record<string, unknown>;
198
198
  start?: string;
199
+ source?: { url: string };
199
200
  }
200
201
  export const frameFallback = (c: FrameContent): string => {
201
202
  const head = c.title ? `Frame: ${c.title}` : 'Frame';
@@ -7,6 +7,7 @@ const MAX_SCREEN_ID = 120;
7
7
  const FRAME_OPEN = 'frame.open';
8
8
  const MAX_TITLE = 200;
9
9
  const MAX_DESCRIPTION = 1000;
10
+ const MAX_SOURCE_URL = 2048;
10
11
  const SUMMARY_SCAN = 80;
11
12
  const TEXT_TYPES = new Set(['Text', 'Caption', 'Markdown']);
12
13
 
@@ -150,6 +151,16 @@ function frameBody(args: Node): FrameBody {
150
151
  return parseScreens(args.screens, args.start);
151
152
  }
152
153
 
154
+ function sourcePart(raw: unknown): Pick<FrameContent, 'source'> {
155
+ if (raw === undefined) return {};
156
+ const url = isNode(raw) && typeof raw.url === 'string' ? raw.url.trim() : '';
157
+ if (url.length > MAX_SOURCE_URL)
158
+ throw bad(`frame source url is ${url.length} characters; the limit is ${MAX_SOURCE_URL}`);
159
+ if (!URL.canParse(url) || new URL(url).protocol !== 'https:')
160
+ throw bad('frame source must be {"url": "https://…"}, the node that serves this widget');
161
+ return { source: { url } };
162
+ }
163
+
153
164
  export function buildFrameContent(raw: unknown): { frame: FrameContent; title: string } {
154
165
  const args = parseJson(raw, 'frame');
155
166
  if (!isNode(args))
@@ -162,6 +173,7 @@ export function buildFrameContent(raw: unknown): { frame: FrameContent; title: s
162
173
  ...(title ? { title } : {}),
163
174
  ...(description && description !== title ? { description } : {}),
164
175
  ...body.content,
176
+ ...sourcePart(args.source),
165
177
  };
166
178
  return { frame, title: title ?? 'Frame' };
167
179
  }
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "0.1.0-beta.285"
2
+ "version": "0.1.0-beta.286"
3
3
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.1.0-beta.285",
2
+ "version": "0.1.0-beta.286",
3
3
  "dependencies": {
4
4
  "@anthropic-ai/claude-agent-sdk": "0.3.287",
5
5
  "@modelcontextprotocol/sdk": "^1.29.0",