@stage-labs/metro 0.1.0-beta.21 → 0.1.0-beta.23
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/cli.js +18 -12
- package/dist/control.js +68 -12
- package/dist/prompt.js +51 -0
- package/package.json +1 -1
- package/runtime/node_modules/@metro-labs/{discord → discord-bot}/package.json +1 -1
- package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/accounts.ts +10 -10
- package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/actions.ts +1 -1
- package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/attachments.ts +1 -1
- package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/format.ts +7 -7
- package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/group-actions.ts +1 -1
- package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/index.ts +10 -10
- package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/station.ts +2 -2
- package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/voice-transcribe.ts +6 -6
- package/runtime/node_modules/@metro-labs/mcp/package.json +2 -2
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-api.ts +14 -8
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/boot.ts +2 -4
- package/runtime/node_modules/@metro-labs/mcp/src/db/materialize.ts +49 -27
- package/runtime/node_modules/@metro-labs/mcp/src/db/schema.ts +2 -2
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-schemas.ts +6 -6
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-interactive.ts +6 -6
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attach.ts +6 -6
- package/runtime/node_modules/@metro-labs/mcp/src/stations/lines.ts +2 -2
- package/runtime/node_modules/@metro-labs/mcp/src/stations/registry.ts +4 -4
- package/runtime/node_modules/@metro-labs/telegram/package.json +5 -2
- package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +47 -86
- package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +210 -251
- package/runtime/node_modules/@metro-labs/telegram/src/attachments.ts +66 -107
- package/runtime/node_modules/@metro-labs/{telegram-user → telegram}/src/client.ts +4 -4
- package/runtime/node_modules/@metro-labs/telegram/src/format.ts +110 -141
- package/runtime/node_modules/@metro-labs/{telegram-user → telegram}/src/group.ts +1 -1
- package/runtime/node_modules/@metro-labs/{telegram-user → telegram}/src/history.ts +1 -1
- package/runtime/node_modules/@metro-labs/{telegram-user → telegram}/src/inbound.ts +6 -6
- package/runtime/node_modules/@metro-labs/telegram/src/index.ts +35 -92
- package/runtime/node_modules/@metro-labs/{telegram-user → telegram}/src/login.ts +20 -20
- package/runtime/node_modules/@metro-labs/telegram/src/media-actions.ts +65 -151
- package/runtime/node_modules/@metro-labs/telegram/src/members.ts +60 -32
- package/runtime/node_modules/@metro-labs/{telegram-user → telegram}/src/reactions.ts +1 -1
- package/runtime/node_modules/@metro-labs/telegram/src/station.ts +8 -1
- package/runtime/node_modules/@metro-labs/telegram/src/types.ts +6 -47
- package/runtime/node_modules/@metro-labs/telegram/src/wire.ts +1 -3
- package/runtime/node_modules/@metro-labs/{telegram-user → telegram-bot}/package.json +3 -6
- package/runtime/node_modules/@metro-labs/telegram-bot/src/accounts.ts +120 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +295 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/attachments.ts +128 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/format.ts +172 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/index.ts +103 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/media-actions.ts +166 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/members.ts +47 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/station.ts +17 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/types.ts +48 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/wire.ts +3 -0
- package/runtime/runtime.json +1 -1
- package/runtime/node_modules/@metro-labs/telegram-user/src/accounts.ts +0 -81
- package/runtime/node_modules/@metro-labs/telegram-user/src/actions.ts +0 -254
- package/runtime/node_modules/@metro-labs/telegram-user/src/attachments.ts +0 -87
- package/runtime/node_modules/@metro-labs/telegram-user/src/format.ts +0 -141
- package/runtime/node_modules/@metro-labs/telegram-user/src/index.ts +0 -46
- package/runtime/node_modules/@metro-labs/telegram-user/src/media-actions.ts +0 -80
- package/runtime/node_modules/@metro-labs/telegram-user/src/members.ts +0 -75
- package/runtime/node_modules/@metro-labs/telegram-user/src/station.ts +0 -24
- package/runtime/node_modules/@metro-labs/telegram-user/src/types.ts +0 -7
- package/runtime/node_modules/@metro-labs/telegram-user/src/wire.ts +0 -1
- /package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/api-base.ts +0 -0
- /package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/group.ts +0 -0
- /package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/members.ts +0 -0
- /package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/send-files.ts +0 -0
- /package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/verify.ts +0 -0
- /package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/voice-speak.ts +0 -0
- /package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/voice.ts +0 -0
- /package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/wire.ts +0 -0
- /package/runtime/node_modules/@metro-labs/{telegram-user → telegram}/src/normalize.ts +0 -0
- /package/runtime/node_modules/@metro-labs/{telegram → telegram-bot}/src/verify.ts +0 -0
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { hostname } from 'node:os';
|
|
3
3
|
import { claimCode, claimRuntime, mcpServers, NotSignedIn, whoisAuthorized, } from './api.js';
|
|
4
|
-
import {
|
|
4
|
+
import { askSecret } from './prompt.js';
|
|
5
5
|
import { clearToken, credentialsPath, metroUrl, metroWebUrl, writeToken, } from './store.js';
|
|
6
|
-
import { detach, probe, runningPid,
|
|
6
|
+
import { detach, lockedBy, probe, runningPid, stopAll, tail } from './control.js';
|
|
7
7
|
import { assertAgentId, daemonPlan, localUrl, readRunToken, runDaemon, runtimeDir, writeRunToken, } from './runtime.js';
|
|
8
8
|
import { update } from './update.js';
|
|
9
9
|
import { currentVersion } from './version.js';
|
|
@@ -11,8 +11,8 @@ const USAGE = `metro — the command line for your MCP connectors
|
|
|
11
11
|
|
|
12
12
|
metro start <agent-id> [--detach]
|
|
13
13
|
run that agent's stations on this machine
|
|
14
|
-
metro stop
|
|
15
|
-
stop
|
|
14
|
+
metro stop [agent-id]
|
|
15
|
+
stop every metro daemon on this machine, however it was started
|
|
16
16
|
metro status <agent-id>
|
|
17
17
|
is it running, and is it healthy
|
|
18
18
|
metro logs <agent-id> [-f]
|
|
@@ -40,7 +40,7 @@ function hostLabel() {
|
|
|
40
40
|
}
|
|
41
41
|
async function authorizeRuntime(agentId) {
|
|
42
42
|
process.stderr.write(`Authorize this machine at ${metroWebUrl()}/#/authorize/${agentId}\n`);
|
|
43
|
-
const code = (await
|
|
43
|
+
const code = (await askSecret('Paste the code (input is hidden): ')).trim();
|
|
44
44
|
if (code === '')
|
|
45
45
|
throw new Error('no code given');
|
|
46
46
|
const claimed = await claimRuntime(code, hostLabel());
|
|
@@ -50,8 +50,9 @@ async function authorizeRuntime(agentId) {
|
|
|
50
50
|
}
|
|
51
51
|
async function start(argv) {
|
|
52
52
|
const agentId = assertAgentId(argv[0]);
|
|
53
|
-
if (runningPid(agentId) !== null)
|
|
54
|
-
throw new Error(
|
|
53
|
+
if (runningPid(agentId) !== null || lockedBy() !== null)
|
|
54
|
+
throw new Error('a metro daemon is already running on this machine. ' +
|
|
55
|
+
'Stop it first: metro stop');
|
|
55
56
|
const dir = runtimeDir();
|
|
56
57
|
const token = readRunToken(agentId) ?? (await authorizeRuntime(agentId));
|
|
57
58
|
const detached = argv.includes('--detach');
|
|
@@ -65,10 +66,15 @@ async function start(argv) {
|
|
|
65
66
|
return 0;
|
|
66
67
|
}
|
|
67
68
|
async function stopDaemon(argv) {
|
|
68
|
-
const agentId = assertAgentId(argv[0]);
|
|
69
|
-
const stopped = await
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
const agentId = argv[0] === undefined ? undefined : assertAgentId(argv[0]);
|
|
70
|
+
const stopped = await stopAll(agentId);
|
|
71
|
+
if (stopped.length === 0) {
|
|
72
|
+
process.stderr.write('no metro daemon is running on this machine\n');
|
|
73
|
+
return 1;
|
|
74
|
+
}
|
|
75
|
+
for (const d of stopped)
|
|
76
|
+
process.stderr.write(`Stopped metro (pid ${String(d.pid)}, via ${d.via})\n`);
|
|
77
|
+
return 0;
|
|
72
78
|
}
|
|
73
79
|
async function status(argv) {
|
|
74
80
|
const agentId = assertAgentId(argv[0]);
|
|
@@ -90,7 +96,7 @@ function logs(argv) {
|
|
|
90
96
|
}
|
|
91
97
|
async function login() {
|
|
92
98
|
process.stderr.write(`Choose a connector collection at ${metroWebUrl()}/#/authorize\n`);
|
|
93
|
-
const { token, email, collection } = await claimCode(await
|
|
99
|
+
const { token, email, collection } = await claimCode(await askSecret('Paste the code here (input is hidden): '));
|
|
94
100
|
writeToken(token);
|
|
95
101
|
process.stderr.write(`Authorized '${collection}' for ${email}. Stored in ${credentialsPath()}\n`);
|
|
96
102
|
}
|
package/dist/control.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
|
-
import { existsSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync, } from 'node:fs';
|
|
1
|
+
import { execFileSync, spawn } from 'node:child_process';
|
|
2
|
+
import { existsSync, mkdirSync, openSync, readdirSync, readFileSync, rmSync, writeFileSync, } from 'node:fs';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
const STATE = () => join(homedir(), '.metro');
|
|
@@ -58,22 +58,78 @@ export function detach(opts) {
|
|
|
58
58
|
writePid(opts.agentId, pid);
|
|
59
59
|
return pid;
|
|
60
60
|
}
|
|
61
|
-
|
|
62
|
-
const pid = runningPid(agentId);
|
|
63
|
-
if (pid === null)
|
|
64
|
-
return false;
|
|
61
|
+
async function stopPid(pid, waitMs) {
|
|
65
62
|
process.kill(pid, 'SIGTERM');
|
|
66
63
|
const until = Date.now() + waitMs;
|
|
67
64
|
while (Date.now() < until) {
|
|
68
|
-
if (!isAlive(pid))
|
|
69
|
-
|
|
70
|
-
return true;
|
|
71
|
-
}
|
|
65
|
+
if (!isAlive(pid))
|
|
66
|
+
return;
|
|
72
67
|
await new Promise((r) => setTimeout(r, 100));
|
|
73
68
|
}
|
|
74
69
|
process.kill(pid, 'SIGKILL');
|
|
75
|
-
|
|
76
|
-
|
|
70
|
+
}
|
|
71
|
+
const lockPath = () => process.env.METRO_STATE_DIR?.trim()
|
|
72
|
+
? join(process.env.METRO_STATE_DIR.trim(), '.tail-lock')
|
|
73
|
+
: join(homedir(), '.cache', 'metro', '.tail-lock');
|
|
74
|
+
function commandOf(pid) {
|
|
75
|
+
try {
|
|
76
|
+
return execFileSync('ps', ['-o', 'command=', '-p', String(pid)], {
|
|
77
|
+
encoding: 'utf8',
|
|
78
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return '';
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const looksLikeMetro = (command) => command.includes('server.ts') || command.includes('metro');
|
|
86
|
+
export function lockedBy() {
|
|
87
|
+
let pid;
|
|
88
|
+
try {
|
|
89
|
+
pid = Number(readFileSync(lockPath(), 'utf8').trim());
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
if (!Number.isInteger(pid) || pid <= 0 || !isAlive(pid))
|
|
95
|
+
return null;
|
|
96
|
+
return looksLikeMetro(commandOf(pid)) ? pid : null;
|
|
97
|
+
}
|
|
98
|
+
function trackedAgents(agentId) {
|
|
99
|
+
const tracked = new Set();
|
|
100
|
+
if (agentId !== undefined)
|
|
101
|
+
tracked.add(agentId);
|
|
102
|
+
try {
|
|
103
|
+
for (const name of readdirSync(STATE())) {
|
|
104
|
+
const found = /^run-(.+)\.pid$/.exec(name);
|
|
105
|
+
if (found?.[1] !== undefined)
|
|
106
|
+
tracked.add(found[1]);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return tracked;
|
|
111
|
+
}
|
|
112
|
+
return tracked;
|
|
113
|
+
}
|
|
114
|
+
export async function stopAll(agentId, waitMs = 10_000) {
|
|
115
|
+
const stopped = [];
|
|
116
|
+
const seen = new Set();
|
|
117
|
+
for (const id of trackedAgents(agentId)) {
|
|
118
|
+
const pid = runningPid(id);
|
|
119
|
+
if (pid === null || seen.has(pid))
|
|
120
|
+
continue;
|
|
121
|
+
seen.add(pid);
|
|
122
|
+
await stopPid(pid, waitMs);
|
|
123
|
+
clearPid(id);
|
|
124
|
+
stopped.push({ pid, via: id });
|
|
125
|
+
}
|
|
126
|
+
const holder = lockedBy();
|
|
127
|
+
if (holder !== null && !seen.has(holder)) {
|
|
128
|
+
await stopPid(holder, waitMs);
|
|
129
|
+
rmSync(lockPath(), { force: true });
|
|
130
|
+
stopped.push({ pid: holder, via: 'the machine lock' });
|
|
131
|
+
}
|
|
132
|
+
return stopped;
|
|
77
133
|
}
|
|
78
134
|
export function tail(agentId, follow) {
|
|
79
135
|
const path = logPath(agentId);
|
package/dist/prompt.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { createInterface } from 'node:readline';
|
|
2
|
+
const CTRL_C = '\u0003';
|
|
3
|
+
const BACKSPACE = '\u007f';
|
|
2
4
|
export function askLine(question) {
|
|
3
5
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
4
6
|
return new Promise((resolve) => {
|
|
@@ -8,3 +10,52 @@ export function askLine(question) {
|
|
|
8
10
|
});
|
|
9
11
|
});
|
|
10
12
|
}
|
|
13
|
+
export function feedSecret(value, chunk) {
|
|
14
|
+
let out = value;
|
|
15
|
+
for (const ch of chunk) {
|
|
16
|
+
if (ch === CTRL_C)
|
|
17
|
+
return { kind: 'cancelled' };
|
|
18
|
+
if (ch === '\r' || ch === '\n')
|
|
19
|
+
return { kind: 'done', value: out };
|
|
20
|
+
if (ch === BACKSPACE || ch === '\b') {
|
|
21
|
+
out = out.slice(0, -1);
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (ch >= ' ')
|
|
25
|
+
out += ch;
|
|
26
|
+
}
|
|
27
|
+
return { kind: 'more', value: out };
|
|
28
|
+
}
|
|
29
|
+
export function askSecret(question) {
|
|
30
|
+
const stdin = process.stdin;
|
|
31
|
+
if (!stdin.isTTY)
|
|
32
|
+
return askLine(question);
|
|
33
|
+
process.stderr.write(question);
|
|
34
|
+
stdin.setRawMode(true);
|
|
35
|
+
stdin.resume();
|
|
36
|
+
stdin.setEncoding('utf8');
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
let value = '\u0003';
|
|
39
|
+
const finish = () => {
|
|
40
|
+
stdin.setRawMode(false);
|
|
41
|
+
stdin.pause();
|
|
42
|
+
stdin.off('data', onData);
|
|
43
|
+
process.stderr.write('\n');
|
|
44
|
+
};
|
|
45
|
+
const onData = (chunk) => {
|
|
46
|
+
const step = feedSecret(value, chunk);
|
|
47
|
+
if (step.kind === 'cancelled') {
|
|
48
|
+
finish();
|
|
49
|
+
reject(new Error('cancelled'));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (step.kind === 'done') {
|
|
53
|
+
finish();
|
|
54
|
+
resolve(step.value.trim());
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
value = step.value;
|
|
58
|
+
};
|
|
59
|
+
stdin.on('data', onData);
|
|
60
|
+
});
|
|
61
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stage-labs/metro",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.23",
|
|
4
4
|
"description": "The metro command line. Sign in once per machine, then hand your MCP connector list to Claude Code without the credentials touching disk, argv or shell history.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -10,8 +10,8 @@ import { emit } from './wire.js';
|
|
|
10
10
|
import { API } from './api-base.js';
|
|
11
11
|
|
|
12
12
|
const ACCOUNTS_FILE =
|
|
13
|
-
process.env.
|
|
14
|
-
join(homedir(), '.metro', 'discord-accounts.json');
|
|
13
|
+
process.env.DISCORD_BOT_ACCOUNTS_FILE ??
|
|
14
|
+
join(homedir(), '.metro', 'discord-bot-accounts.json');
|
|
15
15
|
|
|
16
16
|
export interface AccountConfig {
|
|
17
17
|
id: string;
|
|
@@ -20,9 +20,9 @@ export interface AccountConfig {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export const { loadAccounts } = makeAccountStore<AccountConfig>({
|
|
23
|
-
prefix: 'discord',
|
|
23
|
+
prefix: 'discord-bot',
|
|
24
24
|
file: ACCOUNTS_FILE,
|
|
25
|
-
allowlistEnv: ['
|
|
25
|
+
allowlistEnv: ['DISCORD_BOT_ONLY_ACCOUNTS', 'DISCORD_BOT_ACCOUNTS'],
|
|
26
26
|
validate(raw, die) {
|
|
27
27
|
const seen = new Set<string>();
|
|
28
28
|
for (const a of raw) {
|
|
@@ -67,11 +67,11 @@ export async function rest<T = unknown>(
|
|
|
67
67
|
if (!acct) throw new Error(`unknown account '${accountId}'`);
|
|
68
68
|
const headers: Record<string, string> = {
|
|
69
69
|
Authorization: `Bot ${acct.cfg.token}`,
|
|
70
|
-
'User-Agent': 'metro-discord-train (https://github.com/bonustrack/stage)',
|
|
70
|
+
'User-Agent': 'metro-discord-bot-train (https://github.com/bonustrack/stage)',
|
|
71
71
|
};
|
|
72
72
|
if (body !== undefined && !isForm)
|
|
73
73
|
headers['Content-Type'] = 'application/json';
|
|
74
|
-
emit({ op: 'log', text: `discord[${accountId}] api ${method} ${path}` });
|
|
74
|
+
emit({ op: 'log', text: `discord-bot[${accountId}] api ${method} ${path}` });
|
|
75
75
|
const res = await fetch(`${API}${path}`, {
|
|
76
76
|
method,
|
|
77
77
|
headers,
|
|
@@ -80,17 +80,17 @@ export async function rest<T = unknown>(
|
|
|
80
80
|
});
|
|
81
81
|
emit({
|
|
82
82
|
op: 'log',
|
|
83
|
-
text: `discord[${accountId}] api ${method} ${path} -> ${res.status}`,
|
|
83
|
+
text: `discord-bot[${accountId}] api ${method} ${path} -> ${res.status}`,
|
|
84
84
|
});
|
|
85
85
|
if (!res.ok) {
|
|
86
86
|
const text = await res.text().catch(() => '');
|
|
87
|
-
throw new Error(`discord ${method} ${path}: ${res.status} ${text}`);
|
|
87
|
+
throw new Error(`discord-bot ${method} ${path}: ${res.status} ${text}`);
|
|
88
88
|
}
|
|
89
89
|
return restResult<T>(res);
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
export function lineOf(accountId: string, channelId: string): string {
|
|
93
|
-
return `metro://discord/${accountId}/${channelId}`;
|
|
93
|
+
return `metro://discord-bot/${accountId}/${channelId}`;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
export function parseLine(
|
|
@@ -109,7 +109,7 @@ export function routeOf(
|
|
|
109
109
|
account?: string,
|
|
110
110
|
): { accountId: string; channelId: string } {
|
|
111
111
|
const parsed = parseLine(line);
|
|
112
|
-
if (!parsed) throw new Error(`bad discord line: ${line}`);
|
|
112
|
+
if (!parsed) throw new Error(`bad discord-bot line: ${line}`);
|
|
113
113
|
const accountId = account ?? parsed.accountId;
|
|
114
114
|
if (!accounts.has(accountId))
|
|
115
115
|
throw new Error(`unknown account '${accountId}' in line ${line}`);
|
|
@@ -267,7 +267,7 @@ async function download(
|
|
|
267
267
|
for (const att of msg.attachments) {
|
|
268
268
|
const res = await fetch(att.url, { signal: AbortSignal.timeout(60_000) });
|
|
269
269
|
if (!res.ok)
|
|
270
|
-
throw new Error(`discord download ${res.status} for ${att.url}`);
|
|
270
|
+
throw new Error(`discord-bot download ${res.status} for ${att.url}`);
|
|
271
271
|
assertContentLength(res.headers.get('content-length'));
|
|
272
272
|
const buf = await res.arrayBuffer();
|
|
273
273
|
const path = `${outDir}/${messageId}-${att.filename}`;
|
|
@@ -19,7 +19,7 @@ export async function saveDiscordAttachment(
|
|
|
19
19
|
): Promise<SavedAttachment> {
|
|
20
20
|
const res = await fetch(a.url, { signal: AbortSignal.timeout(60_000) });
|
|
21
21
|
if (!res.ok)
|
|
22
|
-
throw new Error(`discord attachment fetch ${res.status} for ${a.url}`);
|
|
22
|
+
throw new Error(`discord-bot attachment fetch ${res.status} for ${a.url}`);
|
|
23
23
|
assertContentLength(res.headers.get('content-length'));
|
|
24
24
|
const data = new Uint8Array(await res.arrayBuffer());
|
|
25
25
|
return saveBufferToCache(data, messageId, index, {
|
|
@@ -29,7 +29,7 @@ function emitAttachmentSaved(
|
|
|
29
29
|
kind: 'inbound',
|
|
30
30
|
id: mintId(),
|
|
31
31
|
ts: new Date().toISOString(),
|
|
32
|
-
station: 'discord',
|
|
32
|
+
station: 'discord-bot',
|
|
33
33
|
line,
|
|
34
34
|
from: SELF_URI,
|
|
35
35
|
text: `📎 saved: ${saved.path}`,
|
|
@@ -46,7 +46,7 @@ function emitAttachmentSaved(
|
|
|
46
46
|
})
|
|
47
47
|
.catch((err: unknown) =>
|
|
48
48
|
process.stderr.write(
|
|
49
|
-
`discord attachment save failed: ${err instanceof Error ? err.message : String(err)}\n`,
|
|
49
|
+
`discord-bot attachment save failed: ${err instanceof Error ? err.message : String(err)}\n`,
|
|
50
50
|
),
|
|
51
51
|
);
|
|
52
52
|
}
|
|
@@ -102,10 +102,10 @@ export function messageEnvelope(
|
|
|
102
102
|
kind: 'inbound',
|
|
103
103
|
id: envId,
|
|
104
104
|
ts: new Date(m.createdTimestamp).toISOString(),
|
|
105
|
-
station: 'discord',
|
|
105
|
+
station: 'discord-bot',
|
|
106
106
|
line,
|
|
107
107
|
line_name: 'name' in m.channel ? m.channel.name : undefined,
|
|
108
|
-
from: `metro://discord/${accountId}/user/${m.author.id}`,
|
|
108
|
+
from: `metro://discord-bot/${accountId}/user/${m.author.id}`,
|
|
109
109
|
from_name: m.author.username,
|
|
110
110
|
from_display_name: m.author.globalName ?? undefined,
|
|
111
111
|
message_id: m.id,
|
|
@@ -129,9 +129,9 @@ export function reactionEnvelope(
|
|
|
129
129
|
kind: 'react',
|
|
130
130
|
id: mintId(),
|
|
131
131
|
ts: new Date().toISOString(),
|
|
132
|
-
station: 'discord',
|
|
132
|
+
station: 'discord-bot',
|
|
133
133
|
line: lineOf(accountId, r.message.channelId),
|
|
134
|
-
from: `metro://discord/${accountId}/user/${u.id}`,
|
|
134
|
+
from: `metro://discord-bot/${accountId}/user/${u.id}`,
|
|
135
135
|
from_name: u.username,
|
|
136
136
|
from_display_name: u.globalName ?? undefined,
|
|
137
137
|
message_id: r.message.id,
|
|
@@ -162,7 +162,7 @@ function outbound(
|
|
|
162
162
|
kind,
|
|
163
163
|
id: mintId(),
|
|
164
164
|
ts: new Date().toISOString(),
|
|
165
|
-
station: 'discord',
|
|
165
|
+
station: 'discord-bot',
|
|
166
166
|
line,
|
|
167
167
|
from: SELF_URI,
|
|
168
168
|
to: line,
|
|
@@ -17,7 +17,7 @@ export async function groupCreateHandler(
|
|
|
17
17
|
capability: {
|
|
18
18
|
supported: false,
|
|
19
19
|
reason:
|
|
20
|
-
'discord create_group requires `parent` (the metro:// line of the channel to open the thread under)',
|
|
20
|
+
'discord-bot create_group requires `parent` (the metro:// line of the channel to open the thread under)',
|
|
21
21
|
},
|
|
22
22
|
},
|
|
23
23
|
});
|
|
@@ -19,7 +19,7 @@ import { mintId } from './wire.js';
|
|
|
19
19
|
import { readCalls } from '@metro-labs/mcp/trains/protocol';
|
|
20
20
|
import { handleCall } from './actions.js';
|
|
21
21
|
|
|
22
|
-
readCalls('discord', handleCall);
|
|
22
|
+
readCalls('discord-bot', handleCall);
|
|
23
23
|
|
|
24
24
|
function makeClient(): Client {
|
|
25
25
|
return new Client({
|
|
@@ -47,9 +47,9 @@ function onEdit(accountId: string, m: Message): void {
|
|
|
47
47
|
kind: 'edit',
|
|
48
48
|
id: mintId(),
|
|
49
49
|
ts: new Date(m.editedTimestamp ?? Date.now()).toISOString(),
|
|
50
|
-
station: 'discord',
|
|
50
|
+
station: 'discord-bot',
|
|
51
51
|
line: lineOf(accountId, m.channelId),
|
|
52
|
-
from: `metro://discord/${accountId}/user/${m.author.id}`,
|
|
52
|
+
from: `metro://discord-bot/${accountId}/user/${m.author.id}`,
|
|
53
53
|
from_name: m.author.username,
|
|
54
54
|
from_display_name: m.author.globalName ?? undefined,
|
|
55
55
|
message_id: m.id,
|
|
@@ -80,7 +80,7 @@ async function bootAccount(cfg: AccountConfig): Promise<void> {
|
|
|
80
80
|
if (env) emitInbound(accountId, env);
|
|
81
81
|
})().catch((err: unknown) => {
|
|
82
82
|
process.stderr.write(
|
|
83
|
-
`discord[${accountId}] reaction handler failed: ${errMsg(err)}\n`,
|
|
83
|
+
`discord-bot[${accountId}] reaction handler failed: ${errMsg(err)}\n`,
|
|
84
84
|
);
|
|
85
85
|
});
|
|
86
86
|
});
|
|
@@ -91,12 +91,12 @@ async function bootAccount(cfg: AccountConfig): Promise<void> {
|
|
|
91
91
|
onEdit(accountId, _new.partial ? await _new.fetch() : _new);
|
|
92
92
|
} catch (err) {
|
|
93
93
|
process.stderr.write(
|
|
94
|
-
`discord[${accountId}] message update fetch failed: ${errMsg(err)}\n`,
|
|
94
|
+
`discord-bot[${accountId}] message update fetch failed: ${errMsg(err)}\n`,
|
|
95
95
|
);
|
|
96
96
|
}
|
|
97
97
|
})().catch((err: unknown) => {
|
|
98
98
|
process.stderr.write(
|
|
99
|
-
`discord[${accountId}] message update handler failed: ${errMsg(err)}\n`,
|
|
99
|
+
`discord-bot[${accountId}] message update handler failed: ${errMsg(err)}\n`,
|
|
100
100
|
);
|
|
101
101
|
});
|
|
102
102
|
});
|
|
@@ -104,7 +104,7 @@ async function bootAccount(cfg: AccountConfig): Promise<void> {
|
|
|
104
104
|
accounts.set(accountId, { cfg, client });
|
|
105
105
|
await client.login(cfg.token);
|
|
106
106
|
process.stderr.write(
|
|
107
|
-
`discord[${accountId}] ready — ${client.user?.tag ?? '?'} (owner=${cfg.owner ?? '(broadcast)'})\n`,
|
|
107
|
+
`discord-bot[${accountId}] ready — ${client.user?.tag ?? '?'} (owner=${cfg.owner ?? '(broadcast)'})\n`,
|
|
108
108
|
);
|
|
109
109
|
}
|
|
110
110
|
|
|
@@ -114,14 +114,14 @@ for (const cfg of cfgs) {
|
|
|
114
114
|
await bootAccount(cfg);
|
|
115
115
|
} catch (err) {
|
|
116
116
|
process.stderr.write(
|
|
117
|
-
`discord[${cfg.id}] boot FAILED: ${errMsg(err)}\n`,
|
|
117
|
+
`discord-bot[${cfg.id}] boot FAILED: ${errMsg(err)}\n`,
|
|
118
118
|
);
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
121
|
if (accounts.size === 0) {
|
|
122
|
-
process.stderr.write('discord: no accounts booted, exiting\n');
|
|
122
|
+
process.stderr.write('discord-bot: no accounts booted, exiting\n');
|
|
123
123
|
process.exit(2);
|
|
124
124
|
}
|
|
125
125
|
process.stderr.write(
|
|
126
|
-
`discord train ready — ${accounts.size} account(s): ${[...accounts.keys()].join(', ')}\n`,
|
|
126
|
+
`discord-bot train ready — ${accounts.size} account(s): ${[...accounts.keys()].join(', ')}\n`,
|
|
127
127
|
);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { GroupOp, Station, Verb } from '@metro-labs/mcp/stations/types';
|
|
2
2
|
|
|
3
|
-
export const
|
|
4
|
-
name: 'discord',
|
|
3
|
+
export const discordBotStation: Station = {
|
|
4
|
+
name: 'discord-bot',
|
|
5
5
|
hasAccounts: true,
|
|
6
6
|
hasTrain: true,
|
|
7
7
|
messageVerbs: new Set<Verb>([
|
|
@@ -178,10 +178,10 @@ function emitTranscript(
|
|
|
178
178
|
kind: 'inbound',
|
|
179
179
|
id: mintId(),
|
|
180
180
|
ts: new Date().toISOString(),
|
|
181
|
-
station: 'discord',
|
|
182
|
-
line: `metro://discord/${s.accountId}/voice/${s.channelId}`,
|
|
181
|
+
station: 'discord-bot',
|
|
182
|
+
line: `metro://discord-bot/${s.accountId}/voice/${s.channelId}`,
|
|
183
183
|
line_name: 'voice',
|
|
184
|
-
from: `metro://discord/${s.accountId}/user/${userId}`,
|
|
184
|
+
from: `metro://discord-bot/${s.accountId}/user/${userId}`,
|
|
185
185
|
from_name: speakerName(s, userId),
|
|
186
186
|
message_id: stamp,
|
|
187
187
|
text,
|
|
@@ -228,7 +228,7 @@ async function handleSpeaker(s: Session, userId: string): Promise<void> {
|
|
|
228
228
|
await transcribeUtterance(s, userId, pcmPath, wavPath, stamp);
|
|
229
229
|
} catch (err) {
|
|
230
230
|
process.stderr.write(
|
|
231
|
-
`discord voice transcribe error (${userId}): ${errMsg(err)}\n`,
|
|
231
|
+
`discord-bot voice transcribe error (${userId}): ${errMsg(err)}\n`,
|
|
232
232
|
);
|
|
233
233
|
} finally {
|
|
234
234
|
s.active.delete(userId);
|
|
@@ -258,14 +258,14 @@ export function startTranscription(
|
|
|
258
258
|
s.onStart = (userId: string) => {
|
|
259
259
|
handleSpeaker(s, userId).catch((err: unknown) => {
|
|
260
260
|
process.stderr.write(
|
|
261
|
-
`discord[${accountId}] voice speaker failed: ${errMsg(err)}\n`,
|
|
261
|
+
`discord-bot[${accountId}] voice speaker failed: ${errMsg(err)}\n`,
|
|
262
262
|
);
|
|
263
263
|
});
|
|
264
264
|
};
|
|
265
265
|
receiver.speaking.on('start', s.onStart);
|
|
266
266
|
sessions.set(guildId, s);
|
|
267
267
|
process.stderr.write(
|
|
268
|
-
`discord[${accountId}] voice transcription armed on ${channelId}\n`,
|
|
268
|
+
`discord-bot[${accountId}] voice transcription armed on ${channelId}\n`,
|
|
269
269
|
);
|
|
270
270
|
}
|
|
271
271
|
|
|
@@ -42,9 +42,9 @@
|
|
|
42
42
|
"test": "tsc --noEmit && DYLD_FALLBACK_LIBRARY_PATH=/usr/lib METRO_STATE_DIR=\"$(mktemp -d /tmp/metro-test.XXXXXX)\" bun test test/"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@metro-labs/discord": "workspace:*",
|
|
45
|
+
"@metro-labs/discord-bot": "workspace:*",
|
|
46
46
|
"@metro-labs/telegram": "workspace:*",
|
|
47
|
-
"@metro-labs/telegram-
|
|
47
|
+
"@metro-labs/telegram-bot": "workspace:*",
|
|
48
48
|
"@metro-labs/webhook": "workspace:*",
|
|
49
49
|
"@metro-labs/whatsapp": "workspace:*",
|
|
50
50
|
"@metro-labs/xmtp": "workspace:*",
|
|
@@ -102,18 +102,23 @@ interface KeyPayload {
|
|
|
102
102
|
command: string | null;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
|
|
105
|
+
const LOCAL_MCP_ENDPOINT = 'http://127.0.0.1:8420/mcp';
|
|
106
|
+
|
|
107
|
+
function credentials(key: string, heldLocally: boolean): KeyPayload {
|
|
108
|
+
const url = heldLocally
|
|
109
|
+
? `${LOCAL_MCP_ENDPOINT}?token=${key}`
|
|
110
|
+
: `${mcpEndpoint()}?token=${key}`;
|
|
106
111
|
return {
|
|
107
112
|
key,
|
|
108
|
-
endpoint:
|
|
109
|
-
command:
|
|
113
|
+
endpoint: url,
|
|
114
|
+
command: `claude mcp add --transport http ${SERVER_NAME} "${url}"`,
|
|
110
115
|
};
|
|
111
116
|
}
|
|
112
117
|
|
|
113
|
-
function keyPayload(agent: AgentSummary): KeyPayload {
|
|
118
|
+
function keyPayload(agent: AgentSummary, heldLocally: boolean): KeyPayload {
|
|
114
119
|
const value = agent.owned ? agent.key : null;
|
|
115
120
|
if (value === null) return { key: null, endpoint: null, command: null };
|
|
116
|
-
return credentials(value);
|
|
121
|
+
return credentials(value, heldLocally);
|
|
117
122
|
}
|
|
118
123
|
|
|
119
124
|
function livenessPayload(
|
|
@@ -139,7 +144,7 @@ function agentPayload(
|
|
|
139
144
|
owned: agent.owned,
|
|
140
145
|
runtime: agent.owned ? (runtimes.get(agent.id) ?? null) : null,
|
|
141
146
|
...livenessPayload(agent, live),
|
|
142
|
-
...keyPayload(agent),
|
|
147
|
+
...keyPayload(agent, agent.owned && runtimes.has(agent.id)),
|
|
143
148
|
};
|
|
144
149
|
}
|
|
145
150
|
|
|
@@ -219,7 +224,7 @@ async function handleCreate(
|
|
|
219
224
|
sendJson(req, res, 201, {
|
|
220
225
|
id: created.id,
|
|
221
226
|
name: created.name,
|
|
222
|
-
...credentials(created.key),
|
|
227
|
+
...credentials(created.key, false),
|
|
223
228
|
});
|
|
224
229
|
}
|
|
225
230
|
|
|
@@ -231,6 +236,7 @@ async function handleResetKey(
|
|
|
231
236
|
id: string,
|
|
232
237
|
): Promise<void> {
|
|
233
238
|
const reset = await deps.resetKey(session.email, id);
|
|
239
|
+
const held = (await deps.runtimes([id])).has(id);
|
|
234
240
|
log.info(
|
|
235
241
|
{ agent: reset.name, id: reset.id, owner: session.email },
|
|
236
242
|
'agent-api: reset agent key',
|
|
@@ -239,7 +245,7 @@ async function handleResetKey(
|
|
|
239
245
|
id: reset.id,
|
|
240
246
|
name: reset.name,
|
|
241
247
|
reset: true,
|
|
242
|
-
...credentials(reset.key),
|
|
248
|
+
...credentials(reset.key, held),
|
|
243
249
|
});
|
|
244
250
|
}
|
|
245
251
|
|
|
@@ -144,11 +144,9 @@ async function applyStations(removed: StationName[]): Promise<void> {
|
|
|
144
144
|
|
|
145
145
|
async function syncLocal(): Promise<void> {
|
|
146
146
|
if (localSource === null) return;
|
|
147
|
-
const
|
|
148
|
-
const { active, removed } = await reloadFrom(localSource);
|
|
147
|
+
const { removed, changed } = await reloadFrom(localSource);
|
|
149
148
|
await applyStations(removed);
|
|
150
|
-
for (const station of
|
|
151
|
-
if (before.has(station)) supervisor.requestReload(station);
|
|
149
|
+
for (const station of changed) supervisor.requestReload(station);
|
|
152
150
|
}
|
|
153
151
|
|
|
154
152
|
async function stopLocalStations(): Promise<void> {
|