@stage-labs/metro 0.1.0-beta.203 → 0.1.0-beta.205
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/orchestrator.md +3 -1
- package/runtime/node_modules/@metro-labs/core/src/stations/types.ts +0 -1
- 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 +4 -16
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/tool-schemas.ts +1 -2
- package/runtime/node_modules/@metro-labs/discord-bot/src/actions.ts +8 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/station.ts +1 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +4 -6
- package/runtime/node_modules/@metro-labs/telegram-bot/src/station.ts +0 -1
- package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +1 -1
- package/runtime/node_modules/@metro-labs/whatsapp/src/client.ts +3 -3
- package/runtime/node_modules/@metro-labs/whatsapp/src/station.ts +0 -1
- package/runtime/runtime.json +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/typing.ts +0 -46
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.205",
|
|
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",
|
|
@@ -5,7 +5,9 @@ description: How this box's agent works on metro. Acknowledge, delegate, relay,
|
|
|
5
5
|
|
|
6
6
|
You are the main thread of a Claude Code session that is connected to chat through metro, on a machine nobody watches. The main thread is an orchestrator: it may only delegate and talk. A guard in the metro plugin denies every other tool on this thread, so this is the shape of the work, not a preference.
|
|
7
7
|
|
|
8
|
-
**Inbound.** A message arrives as a `<channel source="metro" …>` block. Read `addressed` first: `direct`, `mention` or `reply` means it is for you; without it the message merely happened in a room you watch, so treat it as context and stay silent unless the person plainly wants you. For a message that is for you, the first tool call is `react` on it (👀, then 👍 or ✅ once done) so the person knows you saw it, and any reply goes on the same `line`, passed verbatim.
|
|
8
|
+
**Inbound.** A message arrives as a `<channel source="metro" …>` block. Read `addressed` first: `direct`, `mention` or `reply` means it is for you; without it the message merely happened in a room you watch, so treat it as context and stay silent unless the person plainly wants you. For a message that is for you, the first tool call is `react` on it (👀, then 👍 or ✅ once done) so the person knows you saw it, and any reply goes on the same `line`, passed verbatim.
|
|
9
|
+
|
|
10
|
+
**Typing.** Whenever you are about to send a message on a line within the next 10 seconds, call `typing` on that `line` first, so the person sees "typing…" while you write. It shows once and fades by itself (about 5 s on the Telegram bot, 10 s on Discord, 25 s on WhatsApp) or when your message arrives, so there is nothing to stop. Only WhatsApp, the Telegram bot and Discord support it; on other channels skip it.
|
|
9
11
|
|
|
10
12
|
**Delegate everything that is work.** Reading files, running commands, searching, fetching pages, editing, and every connector tool: hand it to a subagent with the `Agent` tool, `subagent_type: "worker"`, always `run_in_background: true`, then keep answering chat while it runs and relay the result when the task notification arrives. A foreground subagent blinds you to chat and the guard refuses it. You may read image files yourself, so a screenshot someone sends is yours to look at.
|
|
11
13
|
|
|
@@ -105,7 +105,6 @@ export interface Station {
|
|
|
105
105
|
hasAccounts: boolean;
|
|
106
106
|
hasTrain: boolean;
|
|
107
107
|
messageVerbs: ReadonlySet<Verb>;
|
|
108
|
-
typingRefreshMs?: number;
|
|
109
108
|
groupOps?: ReadonlySet<GroupOp>;
|
|
110
109
|
resolvesSenders?: boolean;
|
|
111
110
|
profileFields?: ReadonlySet<ProfileField>;
|
|
@@ -134,6 +134,7 @@ const PRIOR_SKILL: ReadonlySet<string> = new Set([
|
|
|
134
134
|
'0b3d122ed95beff09d579cf912cd4238e1db524c41fce4b314de57d6ff5908ad',
|
|
135
135
|
'36f4fb57f231a119d717b5e3d6ccb654f94960fcca708683f376679aead8df25',
|
|
136
136
|
'3840bc50253e51f431691376cf29a925b8245d87b77b8b20804a159e378a6e67',
|
|
137
|
+
'e6b59eba825b4568dfa9ace5049fd9568a2e10d731719976ad2657084fd4e5f9',
|
|
137
138
|
]);
|
|
138
139
|
|
|
139
140
|
const digest = (text: string): string => createHash('sha256').update(text).digest('hex');
|
|
@@ -10,7 +10,6 @@ import { errResult, makeCtx, ok, toErr } from './ctx.js';
|
|
|
10
10
|
import { runRead } from './read-tool.js';
|
|
11
11
|
import { allowedAgents, currentIdentity } from './request-identity.js';
|
|
12
12
|
import { str } from '@metro-labs/core/str';
|
|
13
|
-
import { startTyping, stopTyping, TYPING_MAX_MS } from './typing.js';
|
|
14
13
|
|
|
15
14
|
type Station = NonNullable<ReturnType<typeof stationForLine>>;
|
|
16
15
|
|
|
@@ -187,13 +186,9 @@ function makeVerbHandler(verb: string, spec: VerbSpec): MessageHandler {
|
|
|
187
186
|
};
|
|
188
187
|
}
|
|
189
188
|
|
|
190
|
-
const handleTyping = async ({ line,
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
return ok('typing stopped');
|
|
194
|
-
}
|
|
195
|
-
await startTyping(line, (on) => ctx.call('typing', { line, on }), station.typingRefreshMs);
|
|
196
|
-
return ok(`typing shown; it stops when you send or reply here, or after ${String(TYPING_MAX_MS / 60_000)} minutes`);
|
|
189
|
+
const handleTyping = async ({ line, ctx }: MessageArgs): Promise<ToolResult> => {
|
|
190
|
+
await ctx.call('typing', { line });
|
|
191
|
+
return ok('typing shown');
|
|
197
192
|
};
|
|
198
193
|
|
|
199
194
|
const MESSAGE_HANDLERS: Record<string, MessageHandler> = {
|
|
@@ -205,13 +200,6 @@ const MESSAGE_HANDLERS: Record<string, MessageHandler> = {
|
|
|
205
200
|
),
|
|
206
201
|
};
|
|
207
202
|
|
|
208
|
-
const SENDS = new Set(['send', 'reply']);
|
|
209
|
-
|
|
210
|
-
function endTypingOnSend(name: string, line: string, result: ToolResult): ToolResult {
|
|
211
|
-
if (SENDS.has(name) && result.isError !== true) stopTyping(line);
|
|
212
|
-
return result;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
203
|
export interface ToolHooks {
|
|
216
204
|
onSent?: (messageId: string) => void;
|
|
217
205
|
}
|
|
@@ -237,7 +225,7 @@ export async function dispatchMessageTool(
|
|
|
237
225
|
`${station.name} does not support ${name}; it supports ${[...verbs].join(', ')}.`,
|
|
238
226
|
);
|
|
239
227
|
try {
|
|
240
|
-
return
|
|
228
|
+
return await handler({ line, a, ctx: makeCtx(station.name), station, onSent: hooks.onSent });
|
|
241
229
|
} catch (e) {
|
|
242
230
|
return toErr(name, e);
|
|
243
231
|
}
|
|
@@ -89,12 +89,11 @@ export const COMMON_TOOLS: ToolDef[] = [
|
|
|
89
89
|
name: 'typing',
|
|
90
90
|
group: 'write',
|
|
91
91
|
description:
|
|
92
|
-
'Show "typing…" in a chat while you prepare an answer
|
|
92
|
+
'Show "typing…" once in a chat while you prepare an answer. The network clears it by itself after its own time (about 5 s on the Telegram bot, 10 s on Discord, 25 s on WhatsApp) or when you send. Call it again to show it again. Args: line.',
|
|
93
93
|
inputSchema: {
|
|
94
94
|
type: 'object',
|
|
95
95
|
properties: {
|
|
96
96
|
line: lineProp,
|
|
97
|
-
on: { type: 'boolean', description: 'false stops it. Default true.' },
|
|
98
97
|
},
|
|
99
98
|
required: ['line'],
|
|
100
99
|
},
|
|
@@ -92,6 +92,13 @@ function listAccounts(id: string): void {
|
|
|
92
92
|
});
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
async function typing(id: string, args: Record<string, unknown>): Promise<void> {
|
|
96
|
+
const { line, account } = args as { line: string; account?: string };
|
|
97
|
+
const { accountId, channelId } = routeOf(line, account);
|
|
98
|
+
await rest(accountId, 'POST', `/channels/${channelId}/typing`);
|
|
99
|
+
respond(id, { result: { ok: true, account: accountId } });
|
|
100
|
+
}
|
|
101
|
+
|
|
95
102
|
async function react(id: string, args: Record<string, unknown>): Promise<void> {
|
|
96
103
|
const { line, messageId, emoji, account } = args as {
|
|
97
104
|
line: string;
|
|
@@ -198,6 +205,7 @@ const HANDLERS: Record<string, StationHandler> = {
|
|
|
198
205
|
groupRemoveMembers: groupRemoveHandler,
|
|
199
206
|
send,
|
|
200
207
|
react,
|
|
208
|
+
typing,
|
|
201
209
|
edit,
|
|
202
210
|
delete: remove,
|
|
203
211
|
read,
|
|
@@ -158,13 +158,11 @@ async function react(id: string, args: Record<string, unknown>): Promise<void> {
|
|
|
158
158
|
}
|
|
159
159
|
|
|
160
160
|
async function typing(id: string, args: Record<string, unknown>): Promise<void> {
|
|
161
|
-
const { line,
|
|
161
|
+
const { line, account } = args as { line: string; account?: string };
|
|
162
162
|
const { accountId, chatId, topicId } = targetOf(line, account);
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
await tg(accountId, 'sendChatAction', body);
|
|
167
|
-
}
|
|
163
|
+
const body: Record<string, unknown> = { chat_id: chatId, action: 'typing' };
|
|
164
|
+
if (topicId !== undefined) body.message_thread_id = topicId;
|
|
165
|
+
await tg(accountId, 'sendChatAction', body);
|
|
168
166
|
respond(id, { result: { ok: true, account: accountId } });
|
|
169
167
|
}
|
|
170
168
|
|
|
@@ -130,7 +130,7 @@ function makeReact(clientFor: ClientFor): StationHandler {
|
|
|
130
130
|
function makeTyping(clientFor: ClientFor): StationHandler {
|
|
131
131
|
return async (id, args) => {
|
|
132
132
|
const { accountId, client, jid } = resolve(args, clientFor);
|
|
133
|
-
await guard(() => client.
|
|
133
|
+
await guard(() => client.showTyping(jid));
|
|
134
134
|
respond(id, { result: { ok: true, account: accountId } });
|
|
135
135
|
};
|
|
136
136
|
}
|
|
@@ -50,7 +50,7 @@ export interface WAClient {
|
|
|
50
50
|
sendText(jid: string, text: string, quotedId?: string): Promise<string>;
|
|
51
51
|
sendMedia(jid: string, media: WAMedia, quotedId?: string): Promise<string>;
|
|
52
52
|
sendReaction(jid: string, messageId: string, emoji: string): Promise<void>;
|
|
53
|
-
|
|
53
|
+
showTyping(jid: string): Promise<void>;
|
|
54
54
|
editMessage(jid: string, messageId: string, text: string): Promise<void>;
|
|
55
55
|
deleteMessage(jid: string, messageId: string): Promise<void>;
|
|
56
56
|
reuploadMedia(m: WAMessage): Promise<WAMessage>;
|
|
@@ -340,9 +340,9 @@ export function createClient(account: WhatsAppAccount): WAClient {
|
|
|
340
340
|
quotedId ? quotedOpts(st, jid, quotedId) : undefined,
|
|
341
341
|
);
|
|
342
342
|
},
|
|
343
|
-
async
|
|
343
|
+
async showTyping(jid) {
|
|
344
344
|
const sock = await ready(st);
|
|
345
|
-
await sock.sendPresenceUpdate(
|
|
345
|
+
await sock.sendPresenceUpdate('composing', jid);
|
|
346
346
|
},
|
|
347
347
|
async sendReaction(jid, messageId, emoji) {
|
|
348
348
|
const target = targetKey(st.keys, jid, messageId, 'react to');
|
package/runtime/runtime.json
CHANGED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { errMsg, log } from '@metro-labs/core/log';
|
|
2
|
-
|
|
3
|
-
export const TYPING_MAX_MS = 120_000;
|
|
4
|
-
const DEFAULT_REFRESH_MS = 5_000;
|
|
5
|
-
|
|
6
|
-
export type TypingCall = (on: boolean) => Promise<unknown>;
|
|
7
|
-
|
|
8
|
-
interface Held {
|
|
9
|
-
timer: ReturnType<typeof setInterval>;
|
|
10
|
-
call: TypingCall;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const held = new Map<string, Held>();
|
|
14
|
-
|
|
15
|
-
const quietly = (line: string, run: Promise<unknown>): void => {
|
|
16
|
-
run.catch((err: unknown) => {
|
|
17
|
-
log.debug({ line, err: errMsg(err) }, 'typing: a refresh failed');
|
|
18
|
-
});
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
export function stopTyping(line: string): void {
|
|
22
|
-
const current = held.get(line);
|
|
23
|
-
if (current === undefined) return;
|
|
24
|
-
clearInterval(current.timer);
|
|
25
|
-
held.delete(line);
|
|
26
|
-
quietly(line, current.call(false));
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export async function startTyping(line: string, call: TypingCall, refreshMs = DEFAULT_REFRESH_MS, now = Date.now): Promise<void> {
|
|
30
|
-
const previous = held.get(line);
|
|
31
|
-
if (previous !== undefined) clearInterval(previous.timer);
|
|
32
|
-
held.delete(line);
|
|
33
|
-
await call(true);
|
|
34
|
-
const stopAt = now() + TYPING_MAX_MS;
|
|
35
|
-
const timer = setInterval(() => {
|
|
36
|
-
if (now() >= stopAt) {
|
|
37
|
-
stopTyping(line);
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
quietly(line, call(true));
|
|
41
|
-
}, refreshMs);
|
|
42
|
-
timer.unref();
|
|
43
|
-
held.set(line, { timer, call });
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export const typingLines = (): string[] => [...held.keys()];
|