@stage-labs/metro 0.1.0-beta.172 → 0.1.0-beta.174
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/api.js +6 -4
- package/dist/claude.js +5 -24
- package/dist/cli.js +3 -3
- package/dist/local.js +18 -52
- package/dist/serve.js +17 -14
- package/dist/service.js +4 -4
- package/dist/tail.js +8 -8
- package/package.json +1 -1
- package/runtime/marketplace/plugin/.claude-plugin/plugin.json +1 -1
- package/runtime/marketplace/plugin/bin/metro-plugin.mjs +9 -19
- package/runtime/node_modules/@metro-labs/core/package.json +4 -3
- package/runtime/node_modules/@metro-labs/core/src/events.ts +1 -3
- package/runtime/node_modules/@metro-labs/core/src/ids.ts +3 -0
- package/runtime/node_modules/@metro-labs/core/src/secure-fs.ts +17 -0
- package/runtime/node_modules/@metro-labs/core/src/station-names.ts +0 -1
- package/runtime/node_modules/@metro-labs/core/src/stations/station-runtime.ts +2 -5
- package/runtime/node_modules/@metro-labs/core/src/stations/train-boot.ts +42 -0
- package/runtime/node_modules/@metro-labs/core/src/stations/train-events.ts +89 -0
- package/runtime/node_modules/@metro-labs/core/src/tickets.ts +37 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/accounts-api.ts +11 -28
- package/runtime/node_modules/@metro-labs/daemon/src/agents/admin.ts +0 -9
- package/runtime/node_modules/@metro-labs/daemon/src/agents/api.ts +11 -91
- package/runtime/node_modules/@metro-labs/daemon/src/agents/bundle.ts +7 -8
- package/runtime/node_modules/@metro-labs/daemon/src/agents/file-admin.ts +16 -93
- package/runtime/node_modules/@metro-labs/daemon/src/agents/files.ts +1 -34
- package/runtime/node_modules/@metro-labs/daemon/src/agents/keys.ts +0 -5
- package/runtime/node_modules/@metro-labs/daemon/src/boot/boot.ts +6 -7
- package/runtime/node_modules/@metro-labs/daemon/src/boot/local-owner.ts +11 -20
- package/runtime/node_modules/@metro-labs/daemon/src/channels/dedupe.ts +1 -13
- package/runtime/node_modules/@metro-labs/daemon/src/channels/relay.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/claude/api.ts +5 -9
- package/runtime/node_modules/@metro-labs/daemon/src/claude/files.ts +2 -7
- package/runtime/node_modules/@metro-labs/daemon/src/claude/session-files.ts +1 -16
- package/runtime/node_modules/@metro-labs/daemon/src/claude/settings.ts +3 -20
- package/runtime/node_modules/@metro-labs/daemon/src/claude/setup.ts +15 -26
- package/runtime/node_modules/@metro-labs/daemon/src/claude/skills.ts +4 -10
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/api.ts +26 -55
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-client.ts +1 -8
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-discovery.ts +1 -5
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-routes.ts +8 -21
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth.ts +20 -9
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay-target.ts +4 -14
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/store.ts +75 -64
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/tools.ts +4 -32
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/url.ts +2 -24
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/verify.ts +25 -20
- package/runtime/node_modules/@metro-labs/daemon/src/files/attach-reaper.ts +174 -0
- package/runtime/node_modules/@metro-labs/daemon/src/files/attach-serve.ts +3 -1
- package/runtime/node_modules/@metro-labs/daemon/src/files/upload-api.ts +3 -19
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/bedrock.ts +15 -41
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-auth.ts +17 -40
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-device.ts +5 -5
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-stream.ts +17 -132
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-translate.ts +9 -18
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex.ts +35 -140
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/forward.ts +43 -2
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/frames.ts +118 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gateway.ts +5 -8
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-auth.ts +18 -40
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-setup.ts +15 -15
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-stream.ts +14 -26
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-translate.ts +12 -21
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini.ts +30 -133
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-api.ts +0 -1
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-store.ts +0 -1
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/openrouter.ts +4 -4
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/pkce.ts +36 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/provider-models.ts +5 -5
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/subscription.ts +120 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/text.ts +15 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/accounts.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/call-tools.ts +5 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/event-store.ts +14 -15
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/index.ts +22 -71
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/permission-relay.ts +4 -5
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/request-identity.ts +5 -6
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/session-slot.ts +48 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/session.ts +12 -31
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/upload-tool.ts +1 -6
- package/runtime/node_modules/@metro-labs/daemon/src/net/tunnel.ts +2 -75
- package/runtime/node_modules/@metro-labs/daemon/src/routes/http.ts +3 -8
- package/runtime/node_modules/@metro-labs/daemon/src/routes/local-mode.ts +19 -29
- package/runtime/node_modules/@metro-labs/daemon/src/routes/threema-callback.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/server/control.ts +15 -41
- package/runtime/node_modules/@metro-labs/daemon/src/server/machine.ts +3 -25
- package/runtime/node_modules/@metro-labs/daemon/src/server/owner.ts +3 -25
- package/runtime/node_modules/@metro-labs/daemon/src/server/update.ts +7 -34
- package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-session.ts +1 -6
- package/runtime/node_modules/@metro-labs/daemon/src/stations/materialize.ts +1 -13
- package/runtime/node_modules/@metro-labs/daemon/src/stations/webhook-endpoints.ts +51 -0
- package/runtime/node_modules/@metro-labs/daemon/src/terminal/api.ts +8 -31
- package/runtime/node_modules/@metro-labs/daemon/src/terminal/tickets.ts +6 -31
- package/runtime/node_modules/@metro-labs/discord-bot/package.json +1 -3
- package/runtime/node_modules/@metro-labs/discord-bot/src/accounts.ts +0 -1
- package/runtime/node_modules/@metro-labs/discord-bot/src/actions.ts +0 -8
- package/runtime/node_modules/@metro-labs/discord-bot/src/format.ts +7 -66
- package/runtime/node_modules/@metro-labs/discord-bot/src/index.ts +5 -10
- package/runtime/node_modules/@metro-labs/discord-bot/src/wire.ts +0 -4
- package/runtime/node_modules/@metro-labs/http/src/api-http.ts +39 -7
- package/runtime/node_modules/@metro-labs/http/src/mode-api.ts +1 -1
- package/runtime/node_modules/@metro-labs/http/src/workos-token.ts +3 -4
- package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +0 -1
- package/runtime/node_modules/@metro-labs/telegram/src/format.ts +0 -48
- package/runtime/node_modules/@metro-labs/telegram/src/inbound.ts +11 -33
- package/runtime/node_modules/@metro-labs/telegram/src/index.ts +4 -43
- package/runtime/node_modules/@metro-labs/telegram/src/types.ts +0 -1
- package/runtime/node_modules/@metro-labs/telegram-bot/src/accounts.ts +0 -1
- package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +0 -8
- package/runtime/node_modules/@metro-labs/telegram-bot/src/format.ts +9 -50
- package/runtime/node_modules/@metro-labs/telegram-bot/src/index.ts +7 -13
- package/runtime/node_modules/@metro-labs/telegram-bot/src/media-actions.ts +3 -2
- package/runtime/node_modules/@metro-labs/telegram-bot/src/wire.ts +0 -2
- package/runtime/node_modules/@metro-labs/threema/src/accounts.ts +0 -1
- package/runtime/node_modules/@metro-labs/threema/src/actions.ts +0 -1
- package/runtime/node_modules/@metro-labs/threema/src/files.ts +8 -43
- package/runtime/node_modules/@metro-labs/threema/src/format.ts +3 -16
- package/runtime/node_modules/@metro-labs/threema/src/inbound.ts +5 -6
- package/runtime/node_modules/@metro-labs/threema/src/index.ts +2 -7
- package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +0 -1
- package/runtime/node_modules/@metro-labs/whatsapp/src/format.ts +1 -56
- package/runtime/node_modules/@metro-labs/whatsapp/src/inbound.ts +10 -27
- package/runtime/node_modules/@metro-labs/whatsapp/src/index.ts +4 -37
- package/runtime/node_modules/@metro-labs/whatsapp/src/types.ts +0 -1
- package/runtime/node_modules/@metro-labs/whatsapp/src/wire.ts +0 -4
- package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +1 -2
- package/runtime/node_modules/@metro-labs/xmtp/src/actions-conv.ts +0 -27
- package/runtime/node_modules/@metro-labs/xmtp/src/actions.ts +0 -10
- package/runtime/node_modules/@metro-labs/xmtp/src/emit-payloads.ts +4 -23
- package/runtime/node_modules/@metro-labs/xmtp/src/emit.ts +3 -5
- package/runtime/node_modules/@metro-labs/xmtp/src/index.ts +4 -8
- package/runtime/node_modules/@metro-labs/xmtp/src/tools-handlers.ts +0 -47
- package/runtime/node_modules/@metro-labs/xmtp/src/tools.ts +23 -43
- package/runtime/node_modules/@metro-labs/xmtp/src/transcribe.ts +3 -5
- package/runtime/node_modules/@metro-labs/xmtp/src/wire.ts +0 -2
- package/runtime/runtime.json +1 -1
- package/runtime/stations.json +1 -3
- package/runtime/node_modules/@metro-labs/core/src/address.ts +0 -6
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/model.ts +0 -69
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-pending.ts +0 -65
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/channel-owner.ts +0 -29
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/frame-scope.ts +0 -21
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/session-registry.ts +0 -172
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/session-route.ts +0 -43
- package/runtime/node_modules/@metro-labs/discord-bot/src/voice-speak.ts +0 -195
- package/runtime/node_modules/@metro-labs/discord-bot/src/voice-transcribe.ts +0 -292
- package/runtime/node_modules/@metro-labs/discord-bot/src/voice.ts +0 -197
- package/runtime/node_modules/@metro-labs/xmtp/src/emit-core.ts +0 -63
package/dist/api.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { localAgent, localDaemonUp } from './local.js';
|
|
2
2
|
import { localUrl } from './runtime.js';
|
|
3
3
|
const NO_DAEMON = `no metro daemon on ${localUrl()} — start one with: metro serve`;
|
|
4
|
-
export async function whoisAuthorized(
|
|
4
|
+
export async function whoisAuthorized() {
|
|
5
5
|
if (!(await localDaemonUp()))
|
|
6
6
|
throw new Error(NO_DAEMON);
|
|
7
|
-
const agent =
|
|
8
|
-
|
|
7
|
+
const agent = localAgent();
|
|
8
|
+
if (agent === null)
|
|
9
|
+
throw new Error('no agent on this machine yet');
|
|
10
|
+
return { agent: agent.id, where: localUrl() };
|
|
9
11
|
}
|
package/dist/claude.js
CHANGED
|
@@ -3,7 +3,7 @@ import { markOnboardingDone, seedChannels } from './onboarding.js';
|
|
|
3
3
|
import { writeMcpConfig } from './mcp-config.js';
|
|
4
4
|
import { currentRoute, permissionMode, routeModelEnv, systemPrompt } from './route.js';
|
|
5
5
|
import { settingsConflicts, settingsFiles } from './claude-settings.js';
|
|
6
|
-
import {
|
|
6
|
+
import { localAgent } from './local.js';
|
|
7
7
|
import { PROVIDER_FLAGS } from './provider-flags.js';
|
|
8
8
|
import { localPort, localUrl } from './runtime.js';
|
|
9
9
|
const CHANNEL_FLAGS = ['--dangerously-load-development-channels', 'server:metro'];
|
|
@@ -76,27 +76,8 @@ async function daemonServing(base = localUrl()) {
|
|
|
76
76
|
return false;
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
|
-
export function agentKey(
|
|
80
|
-
|
|
81
|
-
return { skip: 'no agent lives on this machine yet, so Claude Code talks to Anthropic directly' };
|
|
82
|
-
try {
|
|
83
|
-
return { key: pickLocalAgent(agents, wanted).key };
|
|
84
|
-
}
|
|
85
|
-
catch {
|
|
86
|
-
return {
|
|
87
|
-
skip: wanted === undefined || wanted === ''
|
|
88
|
-
? 'this box has no agent yet (or several old ones), so Claude Code talks to Anthropic directly for now'
|
|
89
|
-
: `no agent named '${wanted}' lives here, so Claude Code talks to Anthropic directly`,
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
function localAgentList() {
|
|
94
|
-
try {
|
|
95
|
-
return localAgents();
|
|
96
|
-
}
|
|
97
|
-
catch {
|
|
98
|
-
return [];
|
|
99
|
-
}
|
|
79
|
+
export function agentKey(agent) {
|
|
80
|
+
return agent === null ? { skip: 'no agent lives on this machine yet, so Claude Code talks to Anthropic directly' } : { key: agent.key };
|
|
100
81
|
}
|
|
101
82
|
async function verdict() {
|
|
102
83
|
const pinned = pinnedBy(process.env);
|
|
@@ -105,7 +86,7 @@ async function verdict() {
|
|
|
105
86
|
const conflicts = settingsConflicts(settingsFiles());
|
|
106
87
|
if (conflicts.length > 0)
|
|
107
88
|
return { skip: `a settings file pins the provider (${conflicts.join(', ')}), so Claude Code keeps it` };
|
|
108
|
-
const picked = agentKey(
|
|
89
|
+
const picked = agentKey(localAgent());
|
|
109
90
|
if ('skip' in picked)
|
|
110
91
|
return picked;
|
|
111
92
|
if (await daemonServing())
|
|
@@ -131,7 +112,7 @@ export function runClaude(args, env) {
|
|
|
131
112
|
async function servedKey(decision) {
|
|
132
113
|
if ('key' in decision)
|
|
133
114
|
return decision.key;
|
|
134
|
-
const picked = agentKey(
|
|
115
|
+
const picked = agentKey(localAgent());
|
|
135
116
|
if ('skip' in picked)
|
|
136
117
|
return null;
|
|
137
118
|
return (await daemonServing()) ? picked.key : null;
|
package/dist/cli.js
CHANGED
|
@@ -22,7 +22,7 @@ const USAGE = `metro — run your agent on this machine
|
|
|
22
22
|
starts at boot and after a crash, so the page can stop, start and
|
|
23
23
|
restart it with no shell; metro service uninstall and status as well
|
|
24
24
|
metro stop stop metro on this machine, metro serve included
|
|
25
|
-
metro tail
|
|
25
|
+
metro tail [agent-id]
|
|
26
26
|
follow this machine's inbound events, one JSON line each
|
|
27
27
|
metro whoami print the agent this machine runs
|
|
28
28
|
metro claude [args...]
|
|
@@ -53,8 +53,8 @@ async function stopDaemon() {
|
|
|
53
53
|
const HELP = new Set([undefined, 'help', '--help', '-h']);
|
|
54
54
|
const COMMANDS = {
|
|
55
55
|
whoami: async () => {
|
|
56
|
-
const { agent, where } = await whoisAuthorized(
|
|
57
|
-
process.stdout.write(`agent
|
|
56
|
+
const { agent, where } = await whoisAuthorized();
|
|
57
|
+
process.stdout.write(`agent ${agent} on ${where}\n`);
|
|
58
58
|
return 0;
|
|
59
59
|
},
|
|
60
60
|
serve: () => serve(process.argv.slice(3)),
|
package/dist/local.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { localUrl } from './runtime.js';
|
|
@@ -7,64 +7,30 @@ export function agentsDir() {
|
|
|
7
7
|
const explicit = process.env.METRO_AGENTS_DIR?.trim();
|
|
8
8
|
return explicit !== undefined && explicit !== '' ? explicit : join(homedir(), '.metro', 'agents');
|
|
9
9
|
}
|
|
10
|
-
function
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
return
|
|
16
|
-
return readdirSync(dir, { withFileTypes: true })
|
|
17
|
-
.filter((entry) => entry.isDirectory())
|
|
18
|
-
.map((entry) => join(dir, entry.name, 'agent.json'))
|
|
19
|
-
.filter((path) => existsSync(path));
|
|
20
|
-
}
|
|
21
|
-
export function localAgents(dir = agentsDir()) {
|
|
22
|
-
const out = [];
|
|
23
|
-
for (const path of agentFiles(dir)) {
|
|
24
|
-
try {
|
|
25
|
-
const file = JSON.parse(readFileSync(path, 'utf8'));
|
|
26
|
-
if (typeof file.id === 'string' && typeof file.key === 'string')
|
|
27
|
-
out.push({ id: file.id, name: typeof file.name === 'string' ? file.name : 'agent', key: file.key });
|
|
28
|
-
}
|
|
29
|
-
catch {
|
|
30
|
-
continue;
|
|
31
|
-
}
|
|
10
|
+
function agentFile(dir) {
|
|
11
|
+
try {
|
|
12
|
+
return JSON.parse(readFileSync(join(dir, 'agent.json'), 'utf8'));
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return null;
|
|
32
16
|
}
|
|
33
|
-
|
|
17
|
+
}
|
|
18
|
+
export function localAgent(dir = agentsDir()) {
|
|
19
|
+
const file = agentFile(dir);
|
|
20
|
+
return typeof file?.id === 'string' && typeof file.key === 'string' ? { id: file.id, key: file.key } : null;
|
|
34
21
|
}
|
|
35
22
|
export function localStations(dir = agentsDir()) {
|
|
23
|
+
const stations = agentFile(dir)?.stations;
|
|
24
|
+
if (!Array.isArray(stations))
|
|
25
|
+
return [];
|
|
36
26
|
const out = new Set();
|
|
37
|
-
for (const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
continue;
|
|
42
|
-
for (const s of file.stations) {
|
|
43
|
-
const station = s.station;
|
|
44
|
-
if (typeof station === 'string')
|
|
45
|
-
out.add(station);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
catch {
|
|
49
|
-
continue;
|
|
50
|
-
}
|
|
27
|
+
for (const s of stations) {
|
|
28
|
+
const station = s.station;
|
|
29
|
+
if (typeof station === 'string')
|
|
30
|
+
out.add(station);
|
|
51
31
|
}
|
|
52
32
|
return [...out].sort();
|
|
53
33
|
}
|
|
54
|
-
export function pickLocalAgent(agents, wanted) {
|
|
55
|
-
if (wanted !== undefined && wanted !== '') {
|
|
56
|
-
const found = agents.find((a) => a.name === wanted || a.id === wanted);
|
|
57
|
-
if (found === undefined)
|
|
58
|
-
throw new Error(`no local agent named '${wanted}'`);
|
|
59
|
-
return found;
|
|
60
|
-
}
|
|
61
|
-
const only = agents[0];
|
|
62
|
-
if (only !== undefined && agents.length === 1)
|
|
63
|
-
return only;
|
|
64
|
-
throw new Error(agents.length === 0
|
|
65
|
-
? 'no agent on this machine yet'
|
|
66
|
-
: `several agents on this machine — name one: ${agents.map((a) => a.name).join(', ')}`);
|
|
67
|
-
}
|
|
68
34
|
export async function localDaemonUp(base = localUrl()) {
|
|
69
35
|
try {
|
|
70
36
|
const res = await fetch(`${base}/api/mode`, { signal: AbortSignal.timeout(PROBE_MS) });
|
package/dist/serve.js
CHANGED
|
@@ -8,7 +8,7 @@ import { findBun, localPort, spawnPlan } from './runtime.js';
|
|
|
8
8
|
import { prepareRuntime } from './runtime-install.js';
|
|
9
9
|
import { ensureNodeName } from './node-name.js';
|
|
10
10
|
import { holdUntilStart } from './hold.js';
|
|
11
|
-
const SCRUBBED = new Set(['
|
|
11
|
+
const SCRUBBED = new Set(['DATABASE_URL']);
|
|
12
12
|
const PORT_FLAG = /^--port=(.*)$/;
|
|
13
13
|
const OWNER_FLAG = /^--owner=(.*)$/;
|
|
14
14
|
const ADDRESS = /^0x[0-9a-fA-F]{40}$/;
|
|
@@ -24,14 +24,14 @@ function portOf(raw) {
|
|
|
24
24
|
const ORGANIZATION = /^org_[A-Za-z0-9]{10,64}$/;
|
|
25
25
|
function ownerOf(raw) {
|
|
26
26
|
if (raw !== undefined && ORGANIZATION.test(raw))
|
|
27
|
-
return raw;
|
|
28
|
-
if (raw
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
return { owner: raw };
|
|
28
|
+
if (raw !== undefined && ADDRESS.test(raw))
|
|
29
|
+
return { owner: null, ignoredOwner: raw };
|
|
30
|
+
throw new Error(`'${raw ?? ''}' is not an organization id (org_…) — ${USAGE}`);
|
|
31
31
|
}
|
|
32
32
|
export function parseServeArgs(argv) {
|
|
33
33
|
let port = localPort();
|
|
34
|
-
let owner = null;
|
|
34
|
+
let owner = { owner: null };
|
|
35
35
|
for (let i = 0; i < argv.length; i += 1) {
|
|
36
36
|
const arg = argv[i] ?? '';
|
|
37
37
|
if (TUNNEL_FLAG.test(arg))
|
|
@@ -58,10 +58,14 @@ export function parseServeArgs(argv) {
|
|
|
58
58
|
}
|
|
59
59
|
throw new Error(`unknown argument '${arg}' — ${USAGE}`);
|
|
60
60
|
}
|
|
61
|
-
return { port, owner };
|
|
61
|
+
return { port, ...owner };
|
|
62
62
|
}
|
|
63
|
-
export function requireOwner(
|
|
64
|
-
if (
|
|
63
|
+
export function requireOwner(args, dir = agentsDir()) {
|
|
64
|
+
if (args.ignoredOwner !== undefined) {
|
|
65
|
+
process.stderr.write(`--owner ${args.ignoredOwner} is a wallet address, which metro ignores: the organization id in ${join(dir, '.owner')} decides who signs in\n`);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (args.owner !== null || existsSync(join(dir, '.owner')))
|
|
65
69
|
return;
|
|
66
70
|
throw new Error('no owner is set for this machine, so nobody could sign in.\n' +
|
|
67
71
|
'Pass the organization that owns it once; it is remembered in ' +
|
|
@@ -71,9 +75,7 @@ export function requireOwner(owner, dir = agentsDir()) {
|
|
|
71
75
|
export function readOwner(dir = agentsDir()) {
|
|
72
76
|
try {
|
|
73
77
|
const raw = readFileSync(join(dir, '.owner'), 'utf8').trim();
|
|
74
|
-
|
|
75
|
-
return raw;
|
|
76
|
-
return ADDRESS.test(raw.toLowerCase()) ? raw.toLowerCase() : null;
|
|
78
|
+
return ORGANIZATION.test(raw) ? raw : null;
|
|
77
79
|
}
|
|
78
80
|
catch {
|
|
79
81
|
return null;
|
|
@@ -142,12 +144,13 @@ export function servePlan(opts) {
|
|
|
142
144
|
};
|
|
143
145
|
}
|
|
144
146
|
export function serve(argv) {
|
|
145
|
-
const
|
|
147
|
+
const args = parseServeArgs(argv);
|
|
148
|
+
const { port, owner } = args;
|
|
146
149
|
const running = serveLockedBy();
|
|
147
150
|
if (running !== null)
|
|
148
151
|
throw new Error(`a metro serve daemon is already running on this machine (pid ${String(running)}). ` +
|
|
149
152
|
'Stop it first: metro stop');
|
|
150
|
-
requireOwner(
|
|
153
|
+
requireOwner(args);
|
|
151
154
|
const tailscaleBin = findTailscale();
|
|
152
155
|
const node = ensureNodeName(tailscaleBin);
|
|
153
156
|
process.stderr.write(`Starting a metro daemon of your own on http://127.0.0.1:${String(port)}, published as ${node} on your tailnet\n`);
|
package/dist/service.js
CHANGED
|
@@ -126,13 +126,13 @@ export function servicePlan(host, serveArgs) {
|
|
|
126
126
|
throw new Error(`metro service supports Linux (systemd) and macOS (launchd), not ${host.platform}`);
|
|
127
127
|
}
|
|
128
128
|
function install(serveArgs, deps) {
|
|
129
|
-
const
|
|
129
|
+
const args = parseServeArgs(serveArgs);
|
|
130
130
|
const plan = servicePlan(deps.host, serveArgs);
|
|
131
131
|
if (deps.exists(plan.file)) {
|
|
132
132
|
deps.out(`metro is already installed as a ${plan.kind} service (${plan.file}) and restarts on its own; metro service status says whether it is running. To change its arguments: metro service uninstall, then install again`);
|
|
133
133
|
return 0;
|
|
134
134
|
}
|
|
135
|
-
deps.preflight(
|
|
135
|
+
deps.preflight(args);
|
|
136
136
|
const pid = deps.running();
|
|
137
137
|
if (pid !== null)
|
|
138
138
|
throw new Error(`a metro serve is running on this machine (pid ${String(pid)}). Stop it first (metro stop), then install: the service takes over from there`);
|
|
@@ -209,8 +209,8 @@ function realDeps() {
|
|
|
209
209
|
return { status: code, output: `${result.stdout}${result.stderr}` };
|
|
210
210
|
},
|
|
211
211
|
running: serveLockedBy,
|
|
212
|
-
preflight: (
|
|
213
|
-
requireOwner(
|
|
212
|
+
preflight: (args) => {
|
|
213
|
+
requireOwner(args);
|
|
214
214
|
findBun();
|
|
215
215
|
findTailscale();
|
|
216
216
|
},
|
package/dist/tail.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { localAgent } from './local.js';
|
|
2
2
|
import { assertAgentId, localUrl } from './runtime.js';
|
|
3
3
|
const BACKOFF_MS = [1_000, 2_000, 5_000];
|
|
4
4
|
const STALL_MS = 90_000;
|
|
@@ -22,9 +22,9 @@ function keySource(agentId) {
|
|
|
22
22
|
return { resolve: () => Promise.resolve(fromEnv), fixed: true };
|
|
23
23
|
return {
|
|
24
24
|
resolve: () => {
|
|
25
|
-
const agent =
|
|
26
|
-
if (agent === undefined)
|
|
27
|
-
throw new Error(`no agent ${agentId
|
|
25
|
+
const agent = localAgent();
|
|
26
|
+
if (agent === null || (agentId !== undefined && agent.id !== agentId))
|
|
27
|
+
throw new Error(`no agent ${agentId === undefined ? '' : `${agentId} `}on this machine — start metro serve, or set METRO_AGENT_KEY`);
|
|
28
28
|
return Promise.resolve(agent.key);
|
|
29
29
|
},
|
|
30
30
|
fixed: false,
|
|
@@ -89,7 +89,7 @@ function note(state, message) {
|
|
|
89
89
|
process.stderr.write(`metro: ${message}\n`);
|
|
90
90
|
}
|
|
91
91
|
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
92
|
-
async function settle(end, source, key,
|
|
92
|
+
async function settle(end, source, key, state) {
|
|
93
93
|
if (end === 'unauthorized') {
|
|
94
94
|
if (source.fixed)
|
|
95
95
|
throw new Error('the key in METRO_AGENT_KEY was refused by the local daemon');
|
|
@@ -97,13 +97,13 @@ async function settle(end, source, key, agentId, state) {
|
|
|
97
97
|
return source.resolve();
|
|
98
98
|
}
|
|
99
99
|
if (end === 'unreachable')
|
|
100
|
-
note(state, `no metro daemon on ${localUrl()} — run 'metro serve'
|
|
100
|
+
note(state, `no metro daemon on ${localUrl()} — run 'metro serve'`);
|
|
101
101
|
else
|
|
102
102
|
note(state, 'stream ended — reconnecting');
|
|
103
103
|
return key;
|
|
104
104
|
}
|
|
105
105
|
export async function tailEvents(argv) {
|
|
106
|
-
const agentId = assertAgentId(argv[0]);
|
|
106
|
+
const agentId = argv[0] === undefined ? undefined : assertAgentId(argv[0]);
|
|
107
107
|
const source = keySource(agentId);
|
|
108
108
|
let key = await source.resolve();
|
|
109
109
|
const state = { last: '' };
|
|
@@ -112,7 +112,7 @@ export async function tailEvents(argv) {
|
|
|
112
112
|
const end = await streamOnce(key);
|
|
113
113
|
if (end === 'ended')
|
|
114
114
|
attempt = 0;
|
|
115
|
-
key = await settle(end, source, key,
|
|
115
|
+
key = await settle(end, source, key, state);
|
|
116
116
|
await delay(backoffAt(attempt));
|
|
117
117
|
attempt += 1;
|
|
118
118
|
}
|
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.174",
|
|
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": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "metro",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.174",
|
|
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,30 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
|
|
6
6
|
const agentsDir = () => process.env.METRO_AGENTS_DIR || join(homedir(), '.metro', 'agents');
|
|
7
7
|
|
|
8
|
-
function readAgent(
|
|
8
|
+
function readAgent() {
|
|
9
|
+
const path = join(agentsDir(), 'agent.json');
|
|
10
|
+
let parsed;
|
|
9
11
|
try {
|
|
10
|
-
|
|
11
|
-
return typeof parsed?.key === 'string' ? parsed : null;
|
|
12
|
+
parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
12
13
|
} catch {
|
|
13
|
-
|
|
14
|
+
throw new Error(`no agent on this machine yet (${path})`);
|
|
14
15
|
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function pickAgent() {
|
|
18
|
-
const dir = agentsDir();
|
|
19
|
-
const fixed = readAgent(join(dir, 'agent.json'));
|
|
20
|
-
if (fixed !== null) return fixed;
|
|
21
|
-
if (!existsSync(dir)) throw new Error(`no agent on this machine yet (${dir})`);
|
|
22
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
23
|
-
if (!entry.isDirectory()) continue;
|
|
24
|
-
const found = readAgent(join(dir, entry.name, 'agent.json'));
|
|
25
|
-
if (found !== null) return found;
|
|
26
|
-
}
|
|
27
|
-
throw new Error(`no agent on this machine yet (${dir})`);
|
|
16
|
+
if (typeof parsed?.key !== 'string') throw new Error(`no agent key in ${path}`);
|
|
17
|
+
return parsed;
|
|
28
18
|
}
|
|
29
19
|
|
|
30
20
|
if (process.argv[2] !== 'headers') {
|
|
@@ -33,7 +23,7 @@ if (process.argv[2] !== 'headers') {
|
|
|
33
23
|
}
|
|
34
24
|
|
|
35
25
|
try {
|
|
36
|
-
process.stdout.write(`${JSON.stringify({ Authorization: `Bearer ${
|
|
26
|
+
process.stdout.write(`${JSON.stringify({ Authorization: `Bearer ${readAgent().key}` })}\n`);
|
|
37
27
|
} catch (err) {
|
|
38
28
|
process.stderr.write(`metro: ${err.message}\n`);
|
|
39
29
|
process.stdout.write('{}\n');
|
|
@@ -7,14 +7,13 @@
|
|
|
7
7
|
"exports": {
|
|
8
8
|
"./log": "./src/log.ts",
|
|
9
9
|
"./events": "./src/events.ts",
|
|
10
|
-
"./identity": "./src/identity.ts",
|
|
11
10
|
"./is-record": "./src/is-record.ts",
|
|
12
11
|
"./secure-fs": "./src/secure-fs.ts",
|
|
13
12
|
"./version": "./src/version.ts",
|
|
14
13
|
"./train-error": "./src/train-error.ts",
|
|
15
14
|
"./trains/protocol": "./src/protocol.ts",
|
|
16
15
|
"./ids": "./src/ids.ts",
|
|
17
|
-
"./
|
|
16
|
+
"./tickets": "./src/tickets.ts",
|
|
18
17
|
"./station-names": "./src/station-names.ts",
|
|
19
18
|
"./str": "./src/str.ts",
|
|
20
19
|
"./lines": "./src/lines.ts",
|
|
@@ -25,7 +24,9 @@
|
|
|
25
24
|
"./stations/attachments": "./src/stations/attachments.ts",
|
|
26
25
|
"./stations/messaging-normalize": "./src/stations/messaging-normalize.ts",
|
|
27
26
|
"./stations/profile": "./src/stations/profile.ts",
|
|
28
|
-
"./stations/sender-profile": "./src/stations/sender-profile.ts"
|
|
27
|
+
"./stations/sender-profile": "./src/stations/sender-profile.ts",
|
|
28
|
+
"./stations/train-events": "./src/stations/train-events.ts",
|
|
29
|
+
"./stations/train-boot": "./src/stations/train-boot.ts"
|
|
29
30
|
},
|
|
30
31
|
"scripts": {
|
|
31
32
|
"build": "tsc",
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { randomBytes } from 'node:crypto';
|
|
2
1
|
import { Line } from './lines.js';
|
|
3
2
|
|
|
4
3
|
export { userSelf, daemonSelf } from './identity.js';
|
|
@@ -74,8 +73,7 @@ export function formatDisplay(e: MetroEvent): string {
|
|
|
74
73
|
return `${headerFor('📩', [e.station, e.fromName ?? e.from, e.lineName])}\n> ${body}`;
|
|
75
74
|
}
|
|
76
75
|
|
|
77
|
-
export
|
|
78
|
-
`msg_${randomBytes(6).toString('base64url')}`;
|
|
76
|
+
export { mintId } from './ids.js';
|
|
79
77
|
|
|
80
78
|
export type BusListener = (event: MetroEvent, busSeq: number) => void;
|
|
81
79
|
|
|
@@ -5,6 +5,9 @@ const REROLLS = 16;
|
|
|
5
5
|
|
|
6
6
|
export const ID_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{10}$/;
|
|
7
7
|
|
|
8
|
+
export const mintId = (): string =>
|
|
9
|
+
`msg_${randomBytes(6).toString('base64url')}`;
|
|
10
|
+
|
|
8
11
|
export function newId(): string {
|
|
9
12
|
for (let attempt = 0; attempt < REROLLS; attempt += 1) {
|
|
10
13
|
const id = randomBytes(ID_BYTES).toString('base64url');
|
|
@@ -38,6 +38,23 @@ export function writeSecure(path: string, data: string): void {
|
|
|
38
38
|
chmodIfExists(path, 0o600);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
function modeOf(path: string): number | null {
|
|
42
|
+
try {
|
|
43
|
+
return statSync(path).mode & 0o777;
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function writeAtomic(path: string, data: string, mode?: number): void {
|
|
50
|
+
const chosen = mode ?? modeOf(path) ?? 0o644;
|
|
51
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
52
|
+
const tmp = `${path}.metro-${String(process.pid)}.tmp`;
|
|
53
|
+
writeFileSync(tmp, data, { mode: chosen });
|
|
54
|
+
chmodSync(tmp, chosen);
|
|
55
|
+
renameSync(tmp, path);
|
|
56
|
+
}
|
|
57
|
+
|
|
41
58
|
export function readJson<T>(
|
|
42
59
|
path: string,
|
|
43
60
|
fallback: T,
|
|
@@ -12,8 +12,7 @@ export const respond = (
|
|
|
12
12
|
JSON.stringify({ op: 'response', id, ...body }) + '\n',
|
|
13
13
|
);
|
|
14
14
|
|
|
15
|
-
export
|
|
16
|
-
`msg_${Math.random().toString(36).slice(2, 10)}`;
|
|
15
|
+
export { mintId } from '../ids.js';
|
|
17
16
|
|
|
18
17
|
type Args = Record<string, unknown>;
|
|
19
18
|
|
|
@@ -29,13 +28,12 @@ export type StationHandler = (id: string, args: Args) => void | Promise<void>;
|
|
|
29
28
|
export interface StationConfig {
|
|
30
29
|
handlers: Record<string, StationHandler>;
|
|
31
30
|
normalize: (action: string, args: Args) => Normalized;
|
|
32
|
-
preDispatch?: (id: string, action: string) => boolean;
|
|
33
31
|
}
|
|
34
32
|
|
|
35
33
|
const lineTag = (args: Args): string =>
|
|
36
34
|
typeof args.line === 'string' ? args.line : '?';
|
|
37
35
|
|
|
38
|
-
export function makeStation({ handlers, normalize
|
|
36
|
+
export function makeStation({ handlers, normalize }: StationConfig) {
|
|
39
37
|
const known = Object.keys(handlers).join(', ');
|
|
40
38
|
return async function handleCall(msg: CallMsg): Promise<void> {
|
|
41
39
|
const { id } = msg;
|
|
@@ -44,7 +42,6 @@ export function makeStation({ handlers, normalize, preDispatch }: StationConfig)
|
|
|
44
42
|
try {
|
|
45
43
|
({ action, args } = normalize(msg.action, msg.args));
|
|
46
44
|
emit({ op: 'log', text: `call ${action} recv (line=${lineTag(args)})` });
|
|
47
|
-
if (preDispatch?.(id, action)) return;
|
|
48
45
|
const handler = handlers[action];
|
|
49
46
|
if (!handler) {
|
|
50
47
|
respond(id, { error: `unknown action '${action}' (have: ${known})` });
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { errMsg } from '../log.js';
|
|
2
|
+
import { readCalls } from '../protocol.js';
|
|
3
|
+
import { TrainError } from '../train-error.js';
|
|
4
|
+
import type { CallMsg } from './station-runtime.js';
|
|
5
|
+
|
|
6
|
+
export function announceAccounts(station: string, ids: Iterable<string>): void {
|
|
7
|
+
const list = [...ids];
|
|
8
|
+
if (list.length === 0) {
|
|
9
|
+
process.stderr.write(`${station}: no accounts booted, exiting\n`);
|
|
10
|
+
process.exit(2);
|
|
11
|
+
}
|
|
12
|
+
process.stderr.write(`${station} train ready — ${String(list.length)} account(s): ${list.join(', ')}\n`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ClientTrain<A extends { id: string }, C> {
|
|
16
|
+
station: string;
|
|
17
|
+
accounts: Map<string, A>;
|
|
18
|
+
loadAccounts: () => A[];
|
|
19
|
+
createClient: (account: A) => C;
|
|
20
|
+
startInbound: (client: C) => Promise<void>;
|
|
21
|
+
makeHandleCall: (clientFor: (accountId: string) => C) => (msg: CallMsg) => Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function runClientTrain<A extends { id: string }, C>(train: ClientTrain<A, C>): void {
|
|
25
|
+
const clients = new Map<string, C>();
|
|
26
|
+
const clientFor = (accountId: string): C => {
|
|
27
|
+
const known = clients.get(accountId);
|
|
28
|
+
if (known !== undefined) return known;
|
|
29
|
+
const account = train.accounts.get(accountId);
|
|
30
|
+
if (!account) throw new TrainError('not_implemented', `unknown account '${accountId}'`);
|
|
31
|
+
const client = train.createClient(account);
|
|
32
|
+
clients.set(accountId, client);
|
|
33
|
+
return client;
|
|
34
|
+
};
|
|
35
|
+
readCalls(train.station, train.makeHandleCall(clientFor));
|
|
36
|
+
for (const cfg of train.loadAccounts()) train.accounts.set(cfg.id, cfg);
|
|
37
|
+
for (const id of train.accounts.keys())
|
|
38
|
+
train.startInbound(clientFor(id)).catch((err: unknown) => {
|
|
39
|
+
process.stderr.write(`${train.station}[${id}] inbound failed: ${errMsg(err)}\n`);
|
|
40
|
+
});
|
|
41
|
+
announceAccounts(train.station, train.accounts.keys());
|
|
42
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { mintId } from '../ids.js';
|
|
2
|
+
import { emit } from './station-runtime.js';
|
|
3
|
+
|
|
4
|
+
type Fields = Record<string, unknown>;
|
|
5
|
+
type Out = (e: unknown) => void;
|
|
6
|
+
|
|
7
|
+
export const selfUri = (station: string, account?: string): string =>
|
|
8
|
+
process.env.METRO_SELF_URI ??
|
|
9
|
+
(account === undefined ? `metro://${station}/self` : `metro://${station}/${account}/self`);
|
|
10
|
+
|
|
11
|
+
export function emitInbound(account: string, e: Fields, out: Out = emit): void {
|
|
12
|
+
out({
|
|
13
|
+
kind: 'inbound',
|
|
14
|
+
...e,
|
|
15
|
+
account,
|
|
16
|
+
payload: { ...(e.payload as Fields | undefined), account },
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface AttachmentAt {
|
|
21
|
+
station: string;
|
|
22
|
+
account: string;
|
|
23
|
+
line: string;
|
|
24
|
+
forId: string;
|
|
25
|
+
index: number;
|
|
26
|
+
from?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface SavedFile {
|
|
30
|
+
path: string;
|
|
31
|
+
mime?: string;
|
|
32
|
+
name?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function attachmentEvent(at: AttachmentAt, text: string, payload: Fields): Fields {
|
|
36
|
+
return {
|
|
37
|
+
kind: 'inbound',
|
|
38
|
+
id: mintId(),
|
|
39
|
+
ts: new Date().toISOString(),
|
|
40
|
+
station: at.station,
|
|
41
|
+
line: at.line,
|
|
42
|
+
from: at.from ?? selfUri(at.station, at.account),
|
|
43
|
+
text,
|
|
44
|
+
account: at.account,
|
|
45
|
+
payload: { account: at.account, attachmentFor: at.forId, index: at.index, ...payload },
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const attachmentSavedEvent = (at: AttachmentAt & { saved: SavedFile; extra?: Fields }): Fields =>
|
|
50
|
+
attachmentEvent(at, `📎 saved: ${at.saved.path}`, {
|
|
51
|
+
contentType: 'attachmentSaved',
|
|
52
|
+
...at.extra,
|
|
53
|
+
attachmentPath: at.saved.path,
|
|
54
|
+
localPath: at.saved.path,
|
|
55
|
+
mime: at.saved.mime,
|
|
56
|
+
name: at.saved.name,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
export const attachmentFailedEvent = (at: AttachmentAt & { reason: string; extra?: Fields }): Fields =>
|
|
60
|
+
attachmentEvent(at, `📎 not fetched: ${at.reason}`, {
|
|
61
|
+
contentType: 'attachmentFailed',
|
|
62
|
+
...at.extra,
|
|
63
|
+
reason: at.reason,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const reasonOf = (err: unknown): string => (err instanceof Error ? err.message : String(err));
|
|
67
|
+
|
|
68
|
+
export function reportAttachment(
|
|
69
|
+
save: Promise<SavedFile>,
|
|
70
|
+
at: AttachmentAt,
|
|
71
|
+
extra: { saved?: Fields; failed?: Fields } = {},
|
|
72
|
+
out: Out = emit,
|
|
73
|
+
): void {
|
|
74
|
+
const tag = `${at.station}[${at.account}]`;
|
|
75
|
+
save
|
|
76
|
+
.then(
|
|
77
|
+
(saved) => {
|
|
78
|
+
out(attachmentSavedEvent({ ...at, saved, extra: extra.saved }));
|
|
79
|
+
},
|
|
80
|
+
(err: unknown) => {
|
|
81
|
+
const reason = reasonOf(err);
|
|
82
|
+
process.stderr.write(`${tag} attachment save failed: ${reason}\n`);
|
|
83
|
+
out(attachmentFailedEvent({ ...at, reason, extra: extra.failed }));
|
|
84
|
+
},
|
|
85
|
+
)
|
|
86
|
+
.catch((err: unknown) => {
|
|
87
|
+
process.stderr.write(`${tag} attachment event not emitted: ${reasonOf(err)}\n`);
|
|
88
|
+
});
|
|
89
|
+
}
|