@stage-labs/metro 0.1.0-beta.232 → 0.1.0-beta.233
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/marketplace/plugin/STAGE.md +4 -4
- package/runtime/node_modules/@metro-labs/core/src/stations/types.ts +1 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/setup.ts +1 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/call-tools.ts +32 -18
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/permission-prompt.ts +12 -7
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/tool-schemas.ts +25 -2
- package/runtime/node_modules/@metro-labs/xmtp/src/actions.ts +10 -13
- package/runtime/node_modules/@metro-labs/xmtp/src/frames.ts +16 -12
- package/runtime/node_modules/@metro-labs/xmtp/src/station.ts +1 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/tools.ts +0 -40
- 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.233",
|
|
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",
|
|
@@ -42,11 +42,11 @@ Move it from Backlog or To-do to In progress, then In review, then Done. Use Blo
|
|
|
42
42
|
- File: `create_upload {"name": "report.pdf"}`, run the `curl` line it returns in a shell, then `send {"line": "…", "attachments": [{"upload": "<upload_id>"}]}`. Files that are not images are refused over about 190 KiB.
|
|
43
43
|
|
|
44
44
|
**Frames.** A frame is a small interactive view: a report, a choice, a form. Stage shows it in the chat as a card with a title and a description, and opens it full screen on a tap.
|
|
45
|
-
- Send: `
|
|
45
|
+
- Send: `send {"line": "…", "frame": {"title": "Deploy", "description": "Version 2.4 is ready", "widget": {…}}}`. A frame goes with `send`, like a file: there is no separate tool. `title` and `description` are optional: they come from the first `Title` and text in the widget when missing. With `text` as well, the text goes first, and the `message_id` you get back is the frame's.
|
|
46
46
|
- `widget` is OpenAI ChatKit widget JSON, with ChatKit's names and props (https://openai.github.io/chatkit-js/): a `Card`, `ListView` or `Basic` root, with `Row`, `Col`, `Box`, `Text`, `Title`, `Caption`, `Markdown`, `Badge`, `Icon`, `Image`, `Button`, `Divider`, `Spacer`, `Form`, `Input`, `Textarea`, `Select`, `DatePicker`, `Checkbox`, `RadioGroup` and `Table`. Images must be `https://`. Number spacing is in steps of 4px. At most 64K characters, 16 levels deep and 500 nodes. Stage shows a small notice for anything it does not know.
|
|
47
|
-
- Example
|
|
47
|
+
- Example, the arguments of one `send`:
|
|
48
48
|
```json
|
|
49
|
-
{"type": "Card", "children": [
|
|
49
|
+
{"line": "…", "frame": {"widget": {"type": "Card", "children": [
|
|
50
50
|
{"type": "Title", "value": "Deploy version 2.4?"},
|
|
51
51
|
{"type": "Text", "value": "3 fixes, no migration.", "color": "secondary"},
|
|
52
52
|
{"type": "Input", "name": "note", "placeholder": "Note (optional)"},
|
|
@@ -54,6 +54,6 @@ Move it from Backlog or To-do to In progress, then In review, then Done. Use Blo
|
|
|
54
54
|
{"type": "Button", "label": "Deploy", "onClickAction": {"type": "deploy", "payload": {"version": "2.4"}}},
|
|
55
55
|
{"type": "Button", "label": "Not now", "color": "secondary", "onClickAction": {"type": "deploy.cancel"}}
|
|
56
56
|
]}
|
|
57
|
-
]}
|
|
57
|
+
]}}}
|
|
58
58
|
```
|
|
59
59
|
- A tap comes back to you as a reply to the frame: `Frame action: deploy {"version":"2.4","note":"…"} (tapped "Deploy")`. The values of the fields (`Input`, `Select`, `Checkbox`, …) are added to the payload by their `name`. `Button.onClickAction`, `ListViewItem.onClickAction`, a `Card` `confirm`/`cancel` (`{"label", "action"}`) and a `Form` `onSubmitAction` send one. Answer it like any message.
|
|
@@ -112,6 +112,7 @@ export interface Station {
|
|
|
112
112
|
readsProfiles?: boolean;
|
|
113
113
|
readFilters?: ReadonlySet<ReadFilter>;
|
|
114
114
|
approvals?: boolean;
|
|
115
|
+
sendsFrames?: boolean;
|
|
115
116
|
attachmentMode: AttachmentMode;
|
|
116
117
|
forget?: (accountId: string) => void;
|
|
117
118
|
forgetExcept?: (keptAccountIds: readonly string[]) => void;
|
|
@@ -163,6 +163,7 @@ const PRIOR_METRO: ReadonlySet<string> = new Set([
|
|
|
163
163
|
const PRIOR_STAGE: ReadonlySet<string> = new Set([
|
|
164
164
|
'dd7d30474765de3865ddc87c1cf3220161ded09502027cdff18597e5df0aab43',
|
|
165
165
|
'47439ecbe661ca70471a6839ab30a963714c296ad8c14ca23c6eebc3929038cc',
|
|
166
|
+
'e3b8f9bbdb205311bc643aaf91d9d903a91204fd5b55ca949b85cdfa6fd00136',
|
|
166
167
|
]);
|
|
167
168
|
const PRIOR_MEMORY: ReadonlySet<string> = new Set();
|
|
168
169
|
|
|
@@ -60,6 +60,21 @@ function assertDelivered(
|
|
|
60
60
|
);
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
const frameOf = (m: MessageArgs): Record<string, unknown> =>
|
|
64
|
+
m.a.frame === undefined ? {} : { frame: m.a.frame };
|
|
65
|
+
|
|
66
|
+
const sentLabels = (m: MessageArgs, text: string | undefined): string[] => [
|
|
67
|
+
...(text ? ['text'] : []),
|
|
68
|
+
...(m.a.frame === undefined ? [] : ['frame']),
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
function nativeBody(m: MessageArgs, text: string | undefined, replyTo: string | undefined): Record<string, unknown> {
|
|
72
|
+
const body: Record<string, unknown> = { line: m.line, ...frameOf(m) };
|
|
73
|
+
if (text) body.text = text;
|
|
74
|
+
if (text && replyTo) body.replyTo = replyTo;
|
|
75
|
+
return body;
|
|
76
|
+
}
|
|
77
|
+
|
|
63
78
|
async function sendNative(
|
|
64
79
|
m: MessageArgs,
|
|
65
80
|
text: string | undefined,
|
|
@@ -67,16 +82,9 @@ async function sendNative(
|
|
|
67
82
|
atts: ResolvedAttachment[],
|
|
68
83
|
): Promise<Sent> {
|
|
69
84
|
const { line, ctx, station } = m;
|
|
70
|
-
const labels
|
|
85
|
+
const labels = sentLabels(m, text);
|
|
71
86
|
let messageId: string | undefined;
|
|
72
|
-
if (text)
|
|
73
|
-
const response = await ctx.call(
|
|
74
|
-
'send',
|
|
75
|
-
replyTo ? { line, text, replyTo } : { line, text },
|
|
76
|
-
);
|
|
77
|
-
messageId = messageIdOf(response);
|
|
78
|
-
labels.push('text');
|
|
79
|
-
}
|
|
87
|
+
if (labels.length) messageId = messageIdOf(await ctx.call('send', nativeBody(m, text, replyTo)));
|
|
80
88
|
if (!atts.length) return { labels, messageId };
|
|
81
89
|
const delivered = station.sendAttachments
|
|
82
90
|
? await station.sendAttachments(line, atts, ctx)
|
|
@@ -93,7 +101,7 @@ async function sendForwarded(
|
|
|
93
101
|
atts: ResolvedAttachment[],
|
|
94
102
|
): Promise<Sent> {
|
|
95
103
|
const { line, ctx, station } = m;
|
|
96
|
-
const args: Record<string, unknown> = { line };
|
|
104
|
+
const args: Record<string, unknown> = { line, ...frameOf(m) };
|
|
97
105
|
if (text) args.text = text;
|
|
98
106
|
if (typeof m.a.subject === 'string' && m.a.subject.trim() !== '') args.subject = m.a.subject;
|
|
99
107
|
if (replyTo) args.replyTo = replyTo;
|
|
@@ -101,8 +109,7 @@ async function sendForwarded(
|
|
|
101
109
|
const response = await ctx.call('send', args);
|
|
102
110
|
const messageId = messageIdOf(response);
|
|
103
111
|
const thread = threadOf(response, line);
|
|
104
|
-
const labels
|
|
105
|
-
if (text) labels.push('text');
|
|
112
|
+
const labels = sentLabels(m, text);
|
|
106
113
|
if (atts.length) {
|
|
107
114
|
const delivered = deliveredLabels(response);
|
|
108
115
|
assertDelivered(station, delivered, atts);
|
|
@@ -116,6 +123,16 @@ const unsupported = (station: Station, atts: CanonicalAttachment[]): string =>
|
|
|
116
123
|
.map((a) => kindOf(a.mime ?? '', a.path ?? a.url ?? a.name ?? ''))
|
|
117
124
|
.join(', ')}); send a link in \`text\` instead`;
|
|
118
125
|
|
|
126
|
+
const NOTHING_TO_SEND = 'send requires `text`, `attachments` or `frame`';
|
|
127
|
+
|
|
128
|
+
function refusal(m: MessageArgs, text: string | undefined, requested: CanonicalAttachment[]): string | undefined {
|
|
129
|
+
if (!text && !requested.length && m.a.frame === undefined) return NOTHING_TO_SEND;
|
|
130
|
+
if (m.a.frame !== undefined && m.station.sendsFrames !== true)
|
|
131
|
+
return `${m.station.name} cannot send a frame; frames are Stage (XMTP) only`;
|
|
132
|
+
if (requested.length && m.station.attachmentMode === 'none') return unsupported(m.station, requested);
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
|
|
119
136
|
function noteSent(m: MessageArgs, messageId: string | undefined): void {
|
|
120
137
|
if (messageId !== undefined) m.onSent?.(messageId);
|
|
121
138
|
}
|
|
@@ -124,10 +141,8 @@ async function handleSend(m: MessageArgs): Promise<ToolResult> {
|
|
|
124
141
|
const text = m.a.text as string | undefined;
|
|
125
142
|
const replyTo = m.a.reply_to as string | undefined;
|
|
126
143
|
const requested = (m.a.attachments as CanonicalAttachment[] | undefined) ?? [];
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
if (requested.length && m.station.attachmentMode === 'none')
|
|
130
|
-
return errResult(unsupported(m.station, requested));
|
|
144
|
+
const refused = refusal(m, text, requested);
|
|
145
|
+
if (refused !== undefined) return errResult(refused);
|
|
131
146
|
const atts = await resolveAttachments(requested, {
|
|
132
147
|
allowed: allowedAgents(currentIdentity()),
|
|
133
148
|
});
|
|
@@ -138,8 +153,7 @@ async function handleSend(m: MessageArgs): Promise<ToolResult> {
|
|
|
138
153
|
const sent = native
|
|
139
154
|
? await sendNative(m, text, replyTo, atts)
|
|
140
155
|
: await sendForwarded(m, text, replyTo, atts);
|
|
141
|
-
if (!sent.labels.length)
|
|
142
|
-
return errResult('send requires `text` or `attachments`');
|
|
156
|
+
if (!sent.labels.length) return errResult(NOTHING_TO_SEND);
|
|
143
157
|
noteSent(m, sent.messageId);
|
|
144
158
|
const done = withId(`sent: ${sent.labels.join(', ')}`, sent.messageId);
|
|
145
159
|
return ok(sent.thread === undefined ? done : `${done} — new thread line: ${sent.thread}`);
|
|
@@ -37,6 +37,17 @@ function channelOf(input: Record<string, unknown>): string | undefined {
|
|
|
37
37
|
return typeof input.station === 'string' && input.station !== '' ? input.station : undefined;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
function argText(value: unknown): string | undefined {
|
|
41
|
+
const plain = valueText(value);
|
|
42
|
+
if (plain !== undefined || value === null || typeof value !== 'object') return plain;
|
|
43
|
+
return JSON.stringify(value);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function fieldText(key: string, value: unknown): string | undefined {
|
|
47
|
+
if (key === 'attachments' && Array.isArray(value)) return `${String(value.length)} file(s)`;
|
|
48
|
+
return key === 'frame' ? argText(value) : valueText(value);
|
|
49
|
+
}
|
|
50
|
+
|
|
40
51
|
function metroLines(tool: string, input: Record<string, unknown>): string[] {
|
|
41
52
|
const lines = [`Approval needed: ${tool}`];
|
|
42
53
|
const channel = channelOf(input);
|
|
@@ -44,7 +55,7 @@ function metroLines(tool: string, input: Record<string, unknown>): string[] {
|
|
|
44
55
|
const keys = [...LEAD, ...Object.keys(input).filter((k) => !LEAD.includes(k))];
|
|
45
56
|
for (const key of keys) {
|
|
46
57
|
if (PLACE.has(key)) continue;
|
|
47
|
-
const text = key
|
|
58
|
+
const text = fieldText(key, input[key]);
|
|
48
59
|
if (text !== undefined) lines.push(`${key === 'text' ? 'Text' : key}: ${key === 'text' ? `"${shorten(text)}"` : shorten(text)}`);
|
|
49
60
|
}
|
|
50
61
|
return lines;
|
|
@@ -52,12 +63,6 @@ function metroLines(tool: string, input: Record<string, unknown>): string[] {
|
|
|
52
63
|
|
|
53
64
|
const ARGS_MAX = 8;
|
|
54
65
|
|
|
55
|
-
function argText(value: unknown): string | undefined {
|
|
56
|
-
const plain = valueText(value);
|
|
57
|
-
if (plain !== undefined || value === null || typeof value !== 'object') return plain;
|
|
58
|
-
return JSON.stringify(value);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
66
|
function connectorLines(name: string, tool: string, input: Record<string, unknown> | undefined): string[] {
|
|
62
67
|
const lines = [`Approval needed: ${tool}`, `Connector: ${name}`];
|
|
63
68
|
const args = Object.entries(input ?? {}).flatMap(([key, value]) => {
|
|
@@ -34,7 +34,7 @@ export const COMMON_TOOLS: ToolDef[] = [
|
|
|
34
34
|
group: 'write',
|
|
35
35
|
description:
|
|
36
36
|
'Send a message (and/or media) to a Metro conversation. Args: line, text?, reply_to?, ' +
|
|
37
|
-
'subject?, attachments?. The station is derived from the line. Outlook and Gmail: send to ' +
|
|
37
|
+
'subject?, attachments?, frame?. The station is derived from the line. Outlook and Gmail: send to ' +
|
|
38
38
|
'metro://<outlook|gmail>/<account>/<email address> to start a new email (subject defaults to the ' +
|
|
39
39
|
'first line of text); the result names the new thread line. Each attachment names EXACTLY ONE ' +
|
|
40
40
|
'source, and the choice matters: `upload` (an `upload_id` from `create_upload`, the ' +
|
|
@@ -42,7 +42,8 @@ export const COMMON_TOOLS: ToolDef[] = [
|
|
|
42
42
|
'`data` (base64 inline -- TINY FILES ONLY, a few KB, because the base64 has to be ' +
|
|
43
43
|
'written out verbatim in the call and longer than that it corrupts); `url` (the daemon ' +
|
|
44
44
|
'fetches it, so the file must already be public); `path` (resolved on the DAEMON host, ' +
|
|
45
|
-
'which is not your machine).
|
|
45
|
+
'which is not your machine). Stage (XMTP) only: `frame` posts an interactive view. At least one ' +
|
|
46
|
+
'of text/attachments/frame is required. ' +
|
|
46
47
|
'The success line names each attachment the station actually delivered; a station that ' +
|
|
47
48
|
'cannot carry a file errors instead of reporting success.',
|
|
48
49
|
inputSchema: {
|
|
@@ -66,6 +67,28 @@ export const COMMON_TOOLS: ToolDef[] = [
|
|
|
66
67
|
description: 'Optional files to attach.',
|
|
67
68
|
items: attachmentItem,
|
|
68
69
|
},
|
|
70
|
+
frame: {
|
|
71
|
+
type: 'object',
|
|
72
|
+
description:
|
|
73
|
+
'Stage (XMTP) only: a frame, a small interactive view that Stage shows as a card (title and ' +
|
|
74
|
+
'description) and opens full screen. `widget` is OpenAI ChatKit widget JSON (a Card, ListView ' +
|
|
75
|
+
'or Basic root, 64K characters at most); a missing `title` or `description` is taken from the ' +
|
|
76
|
+
'widget. A tap comes back to you as a reply to the frame: "Frame action: <type> <payload ' +
|
|
77
|
+
"JSON>\". It is sent after the text, and the message_id returned is the frame's. The stage " +
|
|
78
|
+
'skill lists the widget nodes, with an example.',
|
|
79
|
+
properties: {
|
|
80
|
+
widget: {
|
|
81
|
+
type: 'object',
|
|
82
|
+
description:
|
|
83
|
+
'ChatKit widget JSON, e.g. {"type":"Card","children":[{"type":"Title","value":"Deploy?"},' +
|
|
84
|
+
'{"type":"Button","label":"Ship it","onClickAction":{"type":"deploy","payload":{"env":"prod"}}}]}.',
|
|
85
|
+
additionalProperties: true,
|
|
86
|
+
},
|
|
87
|
+
title: { type: 'string', description: 'Card title in the chat (200 characters at most).' },
|
|
88
|
+
description: { type: 'string', description: 'Card description in the chat (1000 characters at most).' },
|
|
89
|
+
},
|
|
90
|
+
required: ['widget'],
|
|
91
|
+
},
|
|
69
92
|
},
|
|
70
93
|
required: ['line'],
|
|
71
94
|
},
|
|
@@ -31,10 +31,18 @@ const badArgs = (message: string): TrainError =>
|
|
|
31
31
|
|
|
32
32
|
async function send(id: string, args: Args): Promise<void> {
|
|
33
33
|
const { line, text } = args as { line: string; text: string };
|
|
34
|
+
const built = args.frame === undefined ? undefined : buildFrameContent(args.frame);
|
|
34
35
|
const { acct, conv } = await convOf(line);
|
|
35
36
|
if (!conv) throw noConv(line);
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
let messageId: string | undefined;
|
|
38
|
+
if (text || !built) {
|
|
39
|
+
messageId = await conv.sendText(text);
|
|
40
|
+
emitOutbound(acct.cfg.id, line, messageId, text);
|
|
41
|
+
}
|
|
42
|
+
if (built) {
|
|
43
|
+
messageId = await conv.send(new FrameCodec().encode(built.frame));
|
|
44
|
+
emitOutbound(acct.cfg.id, line, messageId, `Frame: ${built.title}`);
|
|
45
|
+
}
|
|
38
46
|
respond(id, { result: { messageId } });
|
|
39
47
|
}
|
|
40
48
|
|
|
@@ -49,16 +57,6 @@ async function ask(id: string, args: Args): Promise<void> {
|
|
|
49
57
|
respond(id, { result: { messageId: sentId, pollId: mintedId } });
|
|
50
58
|
}
|
|
51
59
|
|
|
52
|
-
async function sendFrame(id: string, args: Args): Promise<void> {
|
|
53
|
-
const { line } = args as { line: string };
|
|
54
|
-
const { acct, conv } = await convOf(line);
|
|
55
|
-
if (!conv) throw noConv(line);
|
|
56
|
-
const { frame, title } = buildFrameContent(args);
|
|
57
|
-
const sentId = await conv.send(new FrameCodec().encode(frame));
|
|
58
|
-
emitOutbound(acct.cfg.id, line, sentId, `Frame: ${title}`);
|
|
59
|
-
respond(id, { result: { messageId: sentId, title } });
|
|
60
|
-
}
|
|
61
|
-
|
|
62
60
|
async function referenceInboxOf(conv: Conversation, xmtpMsgId: string): Promise<string> {
|
|
63
61
|
const recent = await conv.messages({ limit: 200, direction: 1 });
|
|
64
62
|
const inbox = recent.find((m) => m.id === xmtpMsgId)?.senderInboxId;
|
|
@@ -222,7 +220,6 @@ const handlers: Record<string, (id: string, args: Args) => Promise<void>> = {
|
|
|
222
220
|
resolve_sender: resolveSenderAction,
|
|
223
221
|
send,
|
|
224
222
|
ask,
|
|
225
|
-
sendFrame,
|
|
226
223
|
react,
|
|
227
224
|
reply,
|
|
228
225
|
delete: remove,
|
|
@@ -48,29 +48,33 @@ export function frameSummary(widget: Node): { title?: string; description?: stri
|
|
|
48
48
|
|
|
49
49
|
function clip(value: unknown, max: number, name: string): string | undefined {
|
|
50
50
|
if (value === undefined) return undefined;
|
|
51
|
-
if (typeof value !== 'string') throw bad(`
|
|
51
|
+
if (typeof value !== 'string') throw bad(`frame ${name} must be a string`);
|
|
52
52
|
const out = value.trim().slice(0, max);
|
|
53
53
|
return out === '' ? undefined : out;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
function
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
throw bad('send_frame widget is not valid JSON');
|
|
63
|
-
}
|
|
56
|
+
function parseJson(raw: unknown, name: string): unknown {
|
|
57
|
+
if (typeof raw !== 'string') return raw;
|
|
58
|
+
try {
|
|
59
|
+
return JSON.parse(raw);
|
|
60
|
+
} catch {
|
|
61
|
+
throw bad(`${name} is not valid JSON`);
|
|
64
62
|
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function parseWidget(raw: unknown): Node {
|
|
66
|
+
const widget = parseJson(raw, 'frame widget');
|
|
65
67
|
if (!isNode(widget) || typeof widget.type !== 'string' || widget.type === '')
|
|
66
|
-
throw bad('
|
|
68
|
+
throw bad('frame widget must be a ChatKit widget object with a `type` (Card, ListView or Basic)');
|
|
67
69
|
const chars = JSON.stringify(widget).length;
|
|
68
70
|
if (chars > FRAME_MAX_CHARS)
|
|
69
|
-
throw bad(`
|
|
71
|
+
throw bad(`frame widget is ${chars} characters; the limit is ${FRAME_MAX_CHARS}`);
|
|
70
72
|
return widget;
|
|
71
73
|
}
|
|
72
74
|
|
|
73
|
-
export function buildFrameContent(
|
|
75
|
+
export function buildFrameContent(raw: unknown): { frame: FrameContent; title: string } {
|
|
76
|
+
const args = parseJson(raw, 'frame');
|
|
77
|
+
if (!isNode(args)) throw bad('frame must be an object: {widget, title?, description?}');
|
|
74
78
|
const widget = parseWidget(args.widget);
|
|
75
79
|
const derived = frameSummary(widget);
|
|
76
80
|
const title = clip(args.title, MAX_TITLE, 'title') ?? derived.title?.slice(0, MAX_TITLE);
|
|
@@ -79,46 +79,6 @@ export const XMTP_TOOLS: StationTool[] = [
|
|
|
79
79
|
return ctx.okJson(await ctx.call('ask', args));
|
|
80
80
|
},
|
|
81
81
|
},
|
|
82
|
-
{
|
|
83
|
-
name: 'send_frame',
|
|
84
|
-
group: 'write',
|
|
85
|
-
description:
|
|
86
|
-
'Post a frame in a Stage conversation: a small interactive view described in OpenAI ChatKit ' +
|
|
87
|
-
'widget JSON (the same nodes and props as ChatKit: a Card, ListView or Basic root with Row, Col, ' +
|
|
88
|
-
'Box, Text, Title, Caption, Markdown, Badge, Icon, Image, Button, Divider, Spacer, Form, Input, ' +
|
|
89
|
-
'Textarea, Select, DatePicker, Checkbox, RadioGroup, Table...). Stage shows it in the chat as a card ' +
|
|
90
|
-
'(title and description) that opens full screen. A tap on a Button onClickAction, a ListViewItem ' +
|
|
91
|
-
'onClickAction, a Card confirm/cancel or a Form submit comes back to you as a reply to the frame: ' +
|
|
92
|
-
'"Frame action: <type> <payload JSON>", with the values of the form fields added to the payload by ' +
|
|
93
|
-
'name. Images must be https. Args: line (required), widget (required, the ChatKit widget object, at ' +
|
|
94
|
-
'most 64K characters of JSON), title? and description? (shown on the card; taken from the first ' +
|
|
95
|
-
'Title and text in the widget when missing). xmtp-only. Returns the frame messageId.',
|
|
96
|
-
inputSchema: {
|
|
97
|
-
type: 'object',
|
|
98
|
-
properties: {
|
|
99
|
-
line: lineProp,
|
|
100
|
-
widget: {
|
|
101
|
-
type: 'object',
|
|
102
|
-
description:
|
|
103
|
-
'ChatKit widget JSON, e.g. {"type":"Card","children":[{"type":"Title","value":"Deploy?"},' +
|
|
104
|
-
'{"type":"Button","label":"Ship it","onClickAction":{"type":"deploy","payload":{"env":"prod"}}}]}.',
|
|
105
|
-
additionalProperties: true,
|
|
106
|
-
},
|
|
107
|
-
title: { type: 'string', description: 'Card title in the chat (200 characters at most).' },
|
|
108
|
-
description: { type: 'string', description: 'Card description in the chat (1000 characters at most).' },
|
|
109
|
-
},
|
|
110
|
-
required: ['line', 'widget'],
|
|
111
|
-
},
|
|
112
|
-
async handle(a, ctx) {
|
|
113
|
-
const line = str(a.line);
|
|
114
|
-
if (!line) return ctx.err('send_frame requires `line`');
|
|
115
|
-
if (a.widget === undefined) return ctx.err('send_frame requires `widget`');
|
|
116
|
-
const args: Record<string, unknown> = { line, widget: a.widget };
|
|
117
|
-
if (a.title !== undefined) args.title = a.title;
|
|
118
|
-
if (a.description !== undefined) args.description = a.description;
|
|
119
|
-
return ctx.okJson(await ctx.call('sendFrame', args));
|
|
120
|
-
},
|
|
121
|
-
},
|
|
122
82
|
{
|
|
123
83
|
name: 'dm',
|
|
124
84
|
group: 'write',
|
package/runtime/runtime.json
CHANGED