@stage-labs/metro 0.1.0-beta.201 → 0.1.0-beta.202

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.
Files changed (163) hide show
  1. package/dist/claude.js +4 -15
  2. package/dist/cli.js +5 -3
  3. package/dist/hold.js +1 -2
  4. package/dist/runtime-install.js +4 -5
  5. package/dist/runtime.js +1 -4
  6. package/dist/serve.js +4 -5
  7. package/dist/service.js +82 -21
  8. package/dist/update.js +1 -1
  9. package/package.json +2 -2
  10. package/runtime/marketplace/plugin/.claude-plugin/plugin.json +1 -1
  11. package/runtime/node_modules/@metro-labs/core/src/events.ts +4 -2
  12. package/runtime/node_modules/@metro-labs/core/src/lines.ts +12 -25
  13. package/runtime/node_modules/@metro-labs/core/src/stations/account-store.ts +45 -22
  14. package/runtime/node_modules/@metro-labs/core/src/stations/attachments.ts +0 -1
  15. package/runtime/node_modules/@metro-labs/core/src/stations/messaging-normalize.ts +8 -80
  16. package/runtime/node_modules/@metro-labs/core/src/stations/station-runtime.ts +3 -5
  17. package/runtime/node_modules/@metro-labs/core/src/stations/train-events.ts +4 -9
  18. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/files.ts +9 -39
  19. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/job-env.ts +1 -1
  20. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/provision.ts +9 -28
  21. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/schedule-detail.ts +0 -1
  22. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/schedules-api.ts +3 -3
  23. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/schedules.ts +8 -116
  24. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/unit-files.ts +11 -35
  25. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/user.ts +7 -9
  26. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/view.ts +1 -1
  27. package/runtime/node_modules/@metro-labs/daemon/src/agents/account-lookups.ts +1 -1
  28. package/runtime/node_modules/@metro-labs/daemon/src/agents/accounts-api.ts +1 -1
  29. package/runtime/node_modules/@metro-labs/daemon/src/agents/allowlist.ts +1 -1
  30. package/runtime/node_modules/@metro-labs/daemon/src/agents/bundle.ts +3 -3
  31. package/runtime/node_modules/@metro-labs/daemon/src/agents/file-admin.ts +2 -2
  32. package/runtime/node_modules/@metro-labs/daemon/src/agents/files.ts +3 -29
  33. package/runtime/node_modules/@metro-labs/daemon/src/approvals/pending.ts +5 -8
  34. package/runtime/node_modules/@metro-labs/daemon/src/approvals/preview.ts +1 -1
  35. package/runtime/node_modules/@metro-labs/daemon/src/boot/boot.ts +5 -8
  36. package/runtime/node_modules/@metro-labs/daemon/src/boot/connect-hint.ts +3 -6
  37. package/runtime/node_modules/@metro-labs/daemon/src/channels/inbound.ts +25 -3
  38. package/runtime/node_modules/@metro-labs/daemon/src/channels/media-note.ts +2 -3
  39. package/runtime/node_modules/@metro-labs/daemon/src/claude/files.ts +2 -2
  40. package/runtime/node_modules/@metro-labs/daemon/src/claude/login.ts +1 -1
  41. package/runtime/node_modules/@metro-labs/daemon/src/claude/memory.ts +6 -56
  42. package/runtime/node_modules/@metro-labs/daemon/src/claude/plugin-install.ts +3 -3
  43. package/runtime/node_modules/@metro-labs/daemon/src/claude/session-files.ts +1 -1
  44. package/runtime/node_modules/@metro-labs/daemon/src/claude/session.ts +7 -6
  45. package/runtime/node_modules/@metro-labs/daemon/src/claude/settings.ts +1 -1
  46. package/runtime/node_modules/@metro-labs/daemon/src/claude/setup.ts +7 -7
  47. package/runtime/node_modules/@metro-labs/daemon/src/claude/skills.ts +4 -4
  48. package/runtime/node_modules/@metro-labs/daemon/src/claude/version.ts +1 -1
  49. package/runtime/node_modules/@metro-labs/daemon/src/connectors/api.ts +2 -4
  50. package/runtime/node_modules/@metro-labs/daemon/src/connectors/config.ts +19 -22
  51. package/runtime/node_modules/@metro-labs/daemon/src/connectors/gates.ts +1 -2
  52. package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-discovery.ts +6 -8
  53. package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-routes.ts +3 -6
  54. package/runtime/node_modules/@metro-labs/daemon/src/connectors/plugin-sync.ts +1 -1
  55. package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay.ts +2 -3
  56. package/runtime/node_modules/@metro-labs/daemon/src/connectors/store.ts +28 -1
  57. package/runtime/node_modules/@metro-labs/daemon/src/files/attach-serve.ts +1 -1
  58. package/runtime/node_modules/@metro-labs/daemon/src/gateway/bedrock.ts +4 -4
  59. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-auth.ts +1 -1
  60. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-device.ts +1 -1
  61. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-stream.ts +1 -1
  62. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-translate.ts +3 -2
  63. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex.ts +1 -1
  64. package/runtime/node_modules/@metro-labs/daemon/src/gateway/effort.ts +2 -2
  65. package/runtime/node_modules/@metro-labs/daemon/src/gateway/eventstream.ts +1 -1
  66. package/runtime/node_modules/@metro-labs/daemon/src/gateway/forward.ts +3 -3
  67. package/runtime/node_modules/@metro-labs/daemon/src/gateway/frames.ts +1 -1
  68. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gateway.ts +3 -3
  69. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-auth.ts +6 -6
  70. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-client.ts +7 -7
  71. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-stream.ts +1 -1
  72. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-translate.ts +3 -2
  73. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini.ts +1 -1
  74. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-config.ts +10 -31
  75. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-signin.ts +1 -1
  76. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-store.ts +1 -1
  77. package/runtime/node_modules/@metro-labs/daemon/src/gateway/openrouter.ts +2 -2
  78. package/runtime/node_modules/@metro-labs/daemon/src/gateway/provider-models.ts +3 -3
  79. package/runtime/node_modules/@metro-labs/daemon/src/gateway/text.ts +1 -2
  80. package/runtime/node_modules/@metro-labs/daemon/src/gateway/usage.ts +2 -2
  81. package/runtime/node_modules/@metro-labs/daemon/src/mcp/policy-gate.ts +5 -6
  82. package/runtime/node_modules/@metro-labs/daemon/src/mcp/policy-snapshot.ts +3 -3
  83. package/runtime/node_modules/@metro-labs/daemon/src/mcp/profile-lookup.ts +2 -2
  84. package/runtime/node_modules/@metro-labs/daemon/src/mcp/read-tool.ts +2 -2
  85. package/runtime/node_modules/@metro-labs/daemon/src/metro-user/helper-script.ts +2 -2
  86. package/runtime/node_modules/@metro-labs/daemon/src/metro-user/install.ts +37 -0
  87. package/runtime/node_modules/@metro-labs/daemon/src/net/tunnel.ts +2 -2
  88. package/runtime/node_modules/@metro-labs/daemon/src/policy/policy.ts +1 -1
  89. package/runtime/node_modules/@metro-labs/daemon/src/routes/local-mode.ts +1 -4
  90. package/runtime/node_modules/@metro-labs/daemon/src/routes/session-apis.ts +3 -8
  91. package/runtime/node_modules/@metro-labs/daemon/src/server/machine.ts +2 -2
  92. package/runtime/node_modules/@metro-labs/daemon/src/server/update.ts +2 -2
  93. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-session.ts +2 -2
  94. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-xmtp.ts +6 -7
  95. package/runtime/node_modules/@metro-labs/daemon/src/stations/supervisor-io.ts +1 -1
  96. package/runtime/node_modules/@metro-labs/daemon/src/stations/supervisor.ts +2 -2
  97. package/runtime/node_modules/@metro-labs/daemon/src/terminal/api.ts +2 -2
  98. package/runtime/node_modules/@metro-labs/daemon/src/terminal/socket.ts +1 -1
  99. package/runtime/node_modules/@metro-labs/daemon/src/vault/firewall.ts +5 -44
  100. package/runtime/node_modules/@metro-labs/daemon/src/vault/index.ts +4 -4
  101. package/runtime/node_modules/@metro-labs/daemon/src/vault/proxy.ts +6 -7
  102. package/runtime/node_modules/@metro-labs/daemon/src/vault/store.ts +12 -13
  103. package/runtime/node_modules/@metro-labs/daemon/src/vault/trust.ts +5 -22
  104. package/runtime/node_modules/@metro-labs/discord-bot/src/accounts.ts +1 -19
  105. package/runtime/node_modules/@metro-labs/discord-bot/src/actions.ts +7 -35
  106. package/runtime/node_modules/@metro-labs/discord-bot/src/format.ts +5 -10
  107. package/runtime/node_modules/@metro-labs/discord-bot/src/group-actions.ts +1 -1
  108. package/runtime/node_modules/@metro-labs/discord-bot/src/index.ts +1 -2
  109. package/runtime/node_modules/@metro-labs/discord-bot/src/profile.ts +1 -1
  110. package/runtime/node_modules/@metro-labs/discord-bot/src/send-files.ts +11 -12
  111. package/runtime/node_modules/@metro-labs/http/src/api-http.ts +2 -5
  112. package/runtime/node_modules/@metro-labs/http/src/mode-api.ts +0 -1
  113. package/runtime/node_modules/@metro-labs/outlook/src/accounts.ts +1 -15
  114. package/runtime/node_modules/@metro-labs/outlook/src/actions.ts +0 -6
  115. package/runtime/node_modules/@metro-labs/outlook/src/format.ts +0 -1
  116. package/runtime/node_modules/@metro-labs/outlook/src/state.ts +1 -1
  117. package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +4 -22
  118. package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +2 -2
  119. package/runtime/node_modules/@metro-labs/telegram/src/format.ts +1 -3
  120. package/runtime/node_modules/@metro-labs/telegram/src/inbound.ts +1 -1
  121. package/runtime/node_modules/@metro-labs/telegram/src/media-actions.ts +1 -2
  122. package/runtime/node_modules/@metro-labs/telegram/src/reactions.ts +1 -1
  123. package/runtime/node_modules/@metro-labs/telegram-bot/src/accounts.ts +4 -37
  124. package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +7 -23
  125. package/runtime/node_modules/@metro-labs/telegram-bot/src/format.ts +1 -4
  126. package/runtime/node_modules/@metro-labs/telegram-bot/src/media-actions.ts +4 -9
  127. package/runtime/node_modules/@metro-labs/telegram-bot/src/profile.ts +3 -3
  128. package/runtime/node_modules/@metro-labs/threema/src/accounts.ts +0 -11
  129. package/runtime/node_modules/@metro-labs/threema/src/actions.ts +2 -12
  130. package/runtime/node_modules/@metro-labs/threema/src/files.ts +4 -15
  131. package/runtime/node_modules/@metro-labs/threema/src/format.ts +2 -7
  132. package/runtime/node_modules/@metro-labs/whatsapp/package.json +1 -2
  133. package/runtime/node_modules/@metro-labs/whatsapp/src/accounts.ts +5 -26
  134. package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +5 -6
  135. package/runtime/node_modules/@metro-labs/whatsapp/src/format.ts +1 -3
  136. package/runtime/node_modules/@metro-labs/whatsapp/src/inbound.ts +1 -1
  137. package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +1 -12
  138. package/runtime/node_modules/@metro-labs/xmtp/src/actions-close.ts +1 -1
  139. package/runtime/node_modules/@metro-labs/xmtp/src/actions-conv.ts +14 -5
  140. package/runtime/node_modules/@metro-labs/xmtp/src/actions-meta.ts +1 -1
  141. package/runtime/node_modules/@metro-labs/xmtp/src/actions.ts +31 -78
  142. package/runtime/node_modules/@metro-labs/xmtp/src/emit.ts +3 -4
  143. package/runtime/node_modules/@metro-labs/xmtp/src/index.ts +1 -6
  144. package/runtime/node_modules/@metro-labs/xmtp/src/profile.ts +1 -1
  145. package/runtime/node_modules/@metro-labs/xmtp/src/sender.ts +1 -1
  146. package/runtime/node_modules/@metro-labs/xmtp/src/tools-handlers.ts +1 -1
  147. package/runtime/node_modules/@metro-labs/xmtp/src/transcribe.ts +2 -2
  148. package/runtime/node_modules/@metro-labs/xmtp/src/wire.ts +0 -2
  149. package/runtime/runtime.json +1 -1
  150. package/runtime/node_modules/@metro-labs/core/src/identity.ts +0 -7
  151. package/runtime/node_modules/@metro-labs/daemon/src/approvals/needs.ts +0 -2
  152. package/runtime/node_modules/@metro-labs/daemon/src/channels/dedupe.ts +0 -12
  153. package/runtime/node_modules/@metro-labs/daemon/src/channels/react-note.ts +0 -13
  154. package/runtime/node_modules/@metro-labs/daemon/src/channels/reply-id.ts +0 -1
  155. package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay-target.ts +0 -29
  156. package/runtime/node_modules/@metro-labs/daemon/src/metro-user/api.ts +0 -43
  157. package/runtime/node_modules/@metro-labs/daemon/src/metro-user/move.ts +0 -165
  158. package/runtime/node_modules/@metro-labs/discord-bot/src/wire.ts +0 -1
  159. package/runtime/node_modules/@metro-labs/telegram/src/normalize.ts +0 -22
  160. package/runtime/node_modules/@metro-labs/telegram/src/wire.ts +0 -1
  161. package/runtime/node_modules/@metro-labs/telegram-bot/src/wire.ts +0 -1
  162. package/runtime/node_modules/@metro-labs/whatsapp/src/normalize.ts +0 -22
  163. package/runtime/node_modules/@metro-labs/whatsapp/src/wire.ts +0 -1
@@ -58,12 +58,12 @@ async function turnOn(user: AgentUser, dir: string): Promise<Record<string, stri
58
58
  trustSystem(ca);
59
59
  memory.browsers = trustBrowsers(user);
60
60
  await runProxy(bin, configFile(dir));
61
- applyFirewall(user.uid);
61
+ applyFirewall();
62
62
  return proxyEnvFor(TRUSTED_CA, SYSTEM_BUNDLE, secrets);
63
63
  }
64
64
 
65
- function turnOff(user: AgentUser): void {
66
- removeFirewall(user.uid);
65
+ function turnOff(): void {
66
+ removeFirewall();
67
67
  stopProxy();
68
68
  untrustSystem();
69
69
  }
@@ -72,7 +72,7 @@ export async function applyVault(user: AgentUser | null = agentUser(), dir = vau
72
72
  if (user === null) return { status: vaultStatus(null, dir), envChanged: false };
73
73
  const enabled = readVault(dir).enabled;
74
74
  try {
75
- const env = enabled ? await turnOn(user, dir) : (turnOff(user), {});
75
+ const env = enabled ? await turnOn(user, dir) : (turnOff(), {});
76
76
  memory.problem = null;
77
77
  const envChanged = setEnv(env);
78
78
  if (envChanged || enabled) exportForJobs(user, env);
@@ -1,4 +1,5 @@
1
1
  import { spawn, type ChildProcess } from 'node:child_process';
2
+ import { stringOf } from '@metro-labs/http/api-http';
2
3
  import { randomBytes } from 'node:crypto';
3
4
  import { basename } from 'node:path';
4
5
  import { createInterface } from 'node:readline';
@@ -30,8 +31,6 @@ const state: { child: ChildProcess | null; wanted: { bin: string; config: string
30
31
  startedAt: 0,
31
32
  };
32
33
 
33
- const text = (v: unknown): string => (typeof v === 'string' ? v : '');
34
-
35
34
  function swappedIds(transforms: unknown): string[] {
36
35
  if (!Array.isArray(transforms)) return [];
37
36
  return transforms.flatMap((t) => {
@@ -51,12 +50,12 @@ export function requestOf(line: string): ProxyRequest | null {
51
50
  const audit = parsed.audit;
52
51
  if (audit.method === 'CONNECT') return null;
53
52
  return {
54
- at: text(parsed.time),
55
- method: text(audit.method),
56
- host: text(audit.host),
57
- path: text(audit.path),
53
+ at: stringOf(parsed.time),
54
+ method: stringOf(audit.method),
55
+ host: stringOf(audit.host),
56
+ path: stringOf(audit.path),
58
57
  status: typeof audit.status_code === 'number' ? audit.status_code : null,
59
- action: text(audit.action),
58
+ action: stringOf(audit.action),
60
59
  swapped: swappedIds(parsed.request_transforms),
61
60
  };
62
61
  }
@@ -1,4 +1,5 @@
1
1
  import { existsSync, rmSync } from 'node:fs';
2
+ import { stringOf } from '@metro-labs/http/api-http';
2
3
  import { ApiError } from '@metro-labs/http/api-error';
3
4
  import { isRecord } from '@metro-labs/core/is-record';
4
5
  import { newId } from '@metro-labs/core/ids';
@@ -34,12 +35,10 @@ const MAX_SECRETS = 100;
34
35
  const MAX_HOSTS = 20;
35
36
  const MAX_VALUE = 16 * 1024;
36
37
 
37
- const str = (v: unknown): string => (typeof v === 'string' ? v : '');
38
-
39
38
  function secretOf(raw: unknown): VaultSecret | null {
40
- if (!isRecord(raw) || typeof raw.id !== 'string' || !ENV_RE.test(str(raw.env))) return null;
39
+ if (!isRecord(raw) || typeof raw.id !== 'string' || !ENV_RE.test(stringOf(raw.env))) return null;
41
40
  const hosts = Array.isArray(raw.hosts) ? raw.hosts.filter((h): h is string => typeof h === 'string' && HOST_RE.test(h)) : [];
42
- return { id: raw.id, name: str(raw.name) || str(raw.env), env: str(raw.env), hosts, createdAt: str(raw.createdAt), updatedAt: str(raw.updatedAt) };
41
+ return { id: raw.id, name: stringOf(raw.name) || stringOf(raw.env), env: stringOf(raw.env), hosts, createdAt: stringOf(raw.createdAt), updatedAt: stringOf(raw.updatedAt) };
43
42
  }
44
43
 
45
44
  export function readVault(dir = vaultDir()): VaultState {
@@ -49,35 +48,35 @@ export function readVault(dir = vaultDir()): VaultState {
49
48
  return { version: 1, enabled: raw.enabled === true, secrets };
50
49
  }
51
50
 
52
- export function writeVault(state: VaultState, dir = vaultDir()): void {
51
+ function writeVault(state: VaultState, dir = vaultDir()): void {
53
52
  ensureSecureDir(dir);
54
53
  writeSecure(stateFile(dir), `${JSON.stringify(state, null, 2)}\n`);
55
54
  }
56
55
 
57
- export function normalizeName(raw: unknown): string {
58
- const name = str(raw).trim();
56
+ function normalizeName(raw: unknown): string {
57
+ const name = stringOf(raw).trim();
59
58
  if (name === '' || name.length > 80) throw new ApiError('a name of 1 to 80 characters is required', 400);
60
59
  return name;
61
60
  }
62
61
 
63
- export function normalizeEnv(raw: unknown): string {
64
- const env = str(raw).trim();
62
+ function normalizeEnv(raw: unknown): string {
63
+ const env = stringOf(raw).trim();
65
64
  if (!ENV_RE.test(env) || RESERVED_ENV.has(env))
66
65
  throw new ApiError('the variable name must be capital letters, digits and _, 3 to 64 long, like OPENAI_API_KEY', 400);
67
66
  return env;
68
67
  }
69
68
 
70
- export function normalizeHosts(raw: unknown): string[] {
69
+ function normalizeHosts(raw: unknown): string[] {
71
70
  if (!Array.isArray(raw)) throw new ApiError('hosts must be a list of websites, like api.openai.com', 400);
72
- const hosts = [...new Set(raw.map((h) => str(h).trim().toLowerCase().replace(/^https?:\/\//, '').replace(/\/.*$/, '')).filter((h) => h !== ''))];
71
+ const hosts = [...new Set(raw.map((h) => stringOf(h).trim().toLowerCase().replace(/^https?:\/\//, '').replace(/\/.*$/, '')).filter((h) => h !== ''))];
73
72
  if (hosts.length === 0 || hosts.length > MAX_HOSTS) throw new ApiError(`a secret needs 1 to ${String(MAX_HOSTS)} websites`, 400);
74
73
  const bad = hosts.find((h) => !HOST_RE.test(h));
75
74
  if (bad !== undefined) throw new ApiError(`'${bad}' is not a website name; write it as api.example.com or *.example.com`, 400);
76
75
  return hosts;
77
76
  }
78
77
 
79
- export function normalizeValue(raw: unknown): string {
80
- const value = str(raw).trim();
78
+ function normalizeValue(raw: unknown): string {
79
+ const value = stringOf(raw).trim();
81
80
  if (value === '' || value.length > MAX_VALUE) throw new ApiError('a value of 1 to 16384 characters is required', 400);
82
81
  return value;
83
82
  }
@@ -1,33 +1,19 @@
1
1
  import { spawnSync } from 'node:child_process';
2
- import { copyFileSync, chmodSync, existsSync, readFileSync, rmSync } from 'node:fs';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
3
  import { errMsg, log } from '@metro-labs/core/log';
4
4
  import { asUser, type AgentUser } from '../agent-user/user.js';
5
5
  import { TRUSTED_CA } from './paths.js';
6
- import { mustHelper, rootHelper, runningAsMetro } from '../metro-user/privilege.js';
6
+ import { mustHelper, rootHelper } from '../metro-user/privilege.js';
7
7
 
8
8
  const has = (bin: string): boolean => spawnSync('sh', ['-c', `command -v ${bin}`], { stdio: 'ignore' }).status === 0;
9
9
 
10
10
  export function trustSystem(caFile: string): void {
11
11
  const same = existsSync(TRUSTED_CA) && readFileSync(TRUSTED_CA, 'utf8') === readFileSync(caFile, 'utf8');
12
- if (same) return;
13
- if (runningAsMetro()) {
14
- mustHelper(['trust-ca']);
15
- return;
16
- }
17
- copyFileSync(caFile, TRUSTED_CA);
18
- chmodSync(TRUSTED_CA, 0o644);
19
- const run = spawnSync('update-ca-certificates', [], { encoding: 'utf8' });
20
- if (run.status !== 0) throw new Error(`update-ca-certificates failed: ${run.stderr.trim()}`);
12
+ if (!same) mustHelper(['trust-ca']);
21
13
  }
22
14
 
23
15
  export function untrustSystem(): void {
24
- if (!existsSync(TRUSTED_CA)) return;
25
- if (runningAsMetro()) {
26
- mustHelper(['untrust-ca']);
27
- return;
28
- }
29
- rmSync(TRUSTED_CA, { force: true });
30
- spawnSync('update-ca-certificates', ['--fresh'], { stdio: 'ignore' });
16
+ if (existsSync(TRUSTED_CA)) mustHelper(['untrust-ca']);
31
17
  }
32
18
 
33
19
  const NSS_SCRIPT = [
@@ -41,10 +27,7 @@ const NSS_SCRIPT = [
41
27
 
42
28
  function ensureCertutil(): boolean {
43
29
  if (has('certutil')) return true;
44
- if (runningAsMetro()) return rootHelper(['install-nss-tools']).status === 0 && has('certutil');
45
- if (!has('apt-get')) return false;
46
- const run = spawnSync('apt-get', ['install', '-y', '-q', 'libnss3-tools'], { stdio: 'ignore', timeout: 180_000, env: { ...process.env, DEBIAN_FRONTEND: 'noninteractive' } });
47
- return run.status === 0 && has('certutil');
30
+ return rootHelper(['install-nss-tools']).status === 0 && has('certutil');
48
31
  }
49
32
 
50
33
  export function trustBrowsers(user: AgentUser): string | null {
@@ -1,18 +1,11 @@
1
1
  import { Client } from 'discord.js';
2
- import { homedir } from 'node:os';
3
- import { join } from 'node:path';
4
2
  import {
5
3
  makeAccountStore,
6
4
  resolveAccountId,
7
5
  } from '@metro-labs/core/stations/account-store';
8
6
  import { Line } from '@metro-labs/core/lines';
9
- import { emit } from './wire.js';
10
7
  import { API } from './api-base.js';
11
8
 
12
- const ACCOUNTS_FILE =
13
- process.env.DISCORD_BOT_ACCOUNTS_FILE ??
14
- join(homedir(), '.metro', 'discord-bot-accounts.json');
15
-
16
9
  export interface AccountConfig {
17
10
  id: string;
18
11
  token: string;
@@ -20,16 +13,10 @@ export interface AccountConfig {
20
13
 
21
14
  export const { loadAccounts } = makeAccountStore<AccountConfig>({
22
15
  prefix: 'discord-bot',
23
- file: ACCOUNTS_FILE,
24
16
  validate(raw, die) {
25
- const seen = new Set<string>();
26
- for (const a of raw) {
27
- if (!a.id) die('account missing id');
17
+ for (const a of raw)
28
18
  if (!a.token || typeof a.token !== 'string')
29
19
  die(`account '${a.id}' missing token`);
30
- if (seen.has(a.id)) die(`duplicate account id '${a.id}'`);
31
- seen.add(a.id);
32
- }
33
20
  },
34
21
  });
35
22
 
@@ -69,17 +56,12 @@ export async function rest<T = unknown>(
69
56
  };
70
57
  if (body !== undefined && !isForm)
71
58
  headers['Content-Type'] = 'application/json';
72
- emit({ op: 'log', text: `discord-bot[${accountId}] api ${method} ${path}` });
73
59
  const res = await fetch(`${API}${path}`, {
74
60
  method,
75
61
  headers,
76
62
  body: restBody(body, isForm),
77
63
  signal: AbortSignal.timeout(30_000),
78
64
  });
79
- emit({
80
- op: 'log',
81
- text: `discord-bot[${accountId}] api ${method} ${path} -> ${res.status}`,
82
- });
83
65
  if (!res.ok) {
84
66
  const text = await res.text().catch(() => '');
85
67
  throw new Error(`discord-bot ${method} ${path}: ${res.status} ${text}`);
@@ -6,18 +6,13 @@ import {
6
6
  routeOf,
7
7
  } from './accounts.js';
8
8
  import { emitOutbound, emitOutboundEdit, emitOutboundReact } from './format.js';
9
- import { respond } from './wire.js';
10
- import { normalizeDiscord } from '@metro-labs/core/stations/messaging-normalize';
9
+ import { messagingAliases } from '@metro-labs/core/stations/messaging-normalize';
11
10
  import {
12
11
  appendFiles,
13
12
  outgoingFiles,
14
13
  type OutgoingFile,
15
14
  } from './send-files.js';
16
- import {
17
- makeStation,
18
- type CallMsg,
19
- type StationHandler,
20
- } from '@metro-labs/core/stations/station-runtime';
15
+ import { makeStation, respond, type CallMsg, type StationHandler } from '@metro-labs/core/stations/station-runtime';
21
16
  import { readProfile, setProfile } from './profile.js';
22
17
  import { discordMembers } from './members.js';
23
18
  import {
@@ -57,40 +52,17 @@ async function sendMessage(
57
52
  export type { CallMsg };
58
53
 
59
54
  async function send(id: string, args: Record<string, unknown>): Promise<void> {
60
- const {
61
- line,
62
- text,
63
- replyTo,
64
- embeds,
65
- stickerIds,
66
- images,
67
- files,
68
- account,
69
- attachmentKinds,
70
- attachmentNames,
71
- } = args as {
55
+ const { line, text, replyTo, account } = args as {
72
56
  line: string;
73
57
  text?: string;
74
58
  replyTo?: string;
75
- embeds?: unknown[];
76
- stickerIds?: string[];
77
- images?: string[];
78
- files?: string[];
79
59
  account?: string;
80
- attachmentKinds?: string[];
81
- attachmentNames?: string[];
82
60
  };
83
61
  const { accountId, channelId } = routeOf(line, account);
84
62
  const body: Record<string, unknown> = { flags: 4 };
85
63
  if (text !== undefined) body.content = text;
86
64
  if (replyTo) body.message_reference = { message_id: replyTo };
87
- if (embeds) body.embeds = embeds;
88
- if (stickerIds) body.sticker_ids = stickerIds;
89
- const outgoing = outgoingFiles(
90
- [...(images ?? []), ...(files ?? [])],
91
- attachmentNames,
92
- attachmentKinds,
93
- );
65
+ const outgoing = outgoingFiles(args.attachments);
94
66
  const res = await sendMessage(accountId, channelId, body, outgoing);
95
67
  emitOutbound(accountId, line, res.id, text ?? '', replyTo);
96
68
  respond(id, {
@@ -175,7 +147,7 @@ async function remove(
175
147
  respond(id, { result: { ok: true, account: accountId } });
176
148
  }
177
149
 
178
- async function fetchMessages(
150
+ async function read(
179
151
  id: string,
180
152
  args: Record<string, unknown>,
181
153
  ): Promise<void> {
@@ -228,7 +200,7 @@ const HANDLERS: Record<string, StationHandler> = {
228
200
  react,
229
201
  edit,
230
202
  delete: remove,
231
- fetch: fetchMessages,
203
+ read,
232
204
  listMembers,
233
205
  set_profile: setProfile,
234
206
  profile: readProfile,
@@ -236,5 +208,5 @@ const HANDLERS: Record<string, StationHandler> = {
236
208
 
237
209
  export const handleCall = makeStation({
238
210
  handlers: HANDLERS,
239
- normalize: normalizeDiscord,
211
+ normalize: messagingAliases(),
240
212
  });
@@ -1,7 +1,7 @@
1
1
  import { MessageFlags, type Message, type MessageReaction, type User } from 'discord.js';
2
2
  import { reportAttachment, selfUri } from '@metro-labs/core/stations/train-events';
3
3
  import { accounts, lineOf } from './accounts.js';
4
- import { emit, mintId } from './wire.js';
4
+ import { emit, mintId } from '@metro-labs/core/stations/station-runtime';
5
5
  import { saveDiscordAttachment } from './attachments.js';
6
6
 
7
7
  const AV_TAG: Record<string, string> = { audio: 'audio', video: 'video' };
@@ -63,7 +63,6 @@ export function messageEnvelope(
63
63
  );
64
64
  });
65
65
  return {
66
- kind: 'inbound',
67
66
  id: envId,
68
67
  ts: new Date(m.createdTimestamp).toISOString(),
69
68
  station: 'discord-bot',
@@ -90,7 +89,6 @@ export function reactionEnvelope(
90
89
  ): Record<string, unknown> | null {
91
90
  if (u.bot) return null;
92
91
  return {
93
- kind: 'react',
94
92
  id: mintId(),
95
93
  ts: new Date().toISOString(),
96
94
  station: 'discord-bot',
@@ -116,23 +114,20 @@ export function reactionEnvelope(
116
114
  }
117
115
 
118
116
  function outbound(
119
- kind: string,
120
117
  accountId: string,
121
118
  line: string,
122
119
  messageId: string,
123
120
  extra: object,
124
121
  ): void {
125
122
  emit({
126
- kind,
127
123
  id: mintId(),
128
124
  ts: new Date().toISOString(),
129
125
  station: 'discord-bot',
130
126
  line,
131
- from: selfUri('discord-bot', accountId),
127
+ from: selfUri(),
132
128
  to: line,
133
129
  message_id: messageId,
134
130
  ...extra,
135
- account: accountId,
136
131
  payload: { account: accountId },
137
132
  });
138
133
  }
@@ -144,7 +139,7 @@ export function emitOutbound(
144
139
  text: string,
145
140
  replyTo?: string,
146
141
  ): void {
147
- outbound('outbound', accountId, line, messageId, {
142
+ outbound(accountId, line, messageId, {
148
143
  text,
149
144
  reply_to: replyTo,
150
145
  ...(replyTo ? { event: { type: 'reply', replyTo } } : {}),
@@ -156,7 +151,7 @@ export function emitOutboundReact(
156
151
  messageId: string,
157
152
  emoji: string,
158
153
  ): void {
159
- outbound('react', accountId, line, messageId, {
154
+ outbound(accountId, line, messageId, {
160
155
  emoji,
161
156
  event: { type: 'react', emoji, targetId: messageId },
162
157
  });
@@ -167,7 +162,7 @@ export function emitOutboundEdit(
167
162
  messageId: string,
168
163
  text: string,
169
164
  ): void {
170
- outbound('edit', accountId, line, messageId, {
165
+ outbound(accountId, line, messageId, {
171
166
  text,
172
167
  event: { type: 'edit', targetId: messageId },
173
168
  });
@@ -1,5 +1,5 @@
1
1
  import { routeOf, rest } from './accounts.js';
2
- import { respond } from './wire.js';
2
+ import { respond } from '@metro-labs/core/stations/station-runtime';
3
3
  import {
4
4
  groupAddMembers,
5
5
  groupCreate,
@@ -16,7 +16,7 @@ import {
16
16
  } from './accounts.js';
17
17
  import { emitInbound } from '@metro-labs/core/stations/train-events';
18
18
  import { messageEnvelope, reactionEnvelope } from './format.js';
19
- import { mintId } from './wire.js';
19
+ import { mintId } from '@metro-labs/core/stations/station-runtime';
20
20
  import { readCalls } from '@metro-labs/core/trains/protocol';
21
21
  import { announceAccounts } from '@metro-labs/core/stations/train-boot';
22
22
  import { handleCall } from './actions.js';
@@ -45,7 +45,6 @@ function makeClient(): Client {
45
45
  function onEdit(accountId: string, m: Message): void {
46
46
  if (m.author.bot) return;
47
47
  emitInbound(accountId, {
48
- kind: 'edit',
49
48
  id: mintId(),
50
49
  ts: new Date(m.editedTimestamp ?? Date.now()).toISOString(),
51
50
  station: 'discord-bot',
@@ -10,7 +10,7 @@ import {
10
10
  } from '@metro-labs/core/stations/profile';
11
11
  import type { SenderProfile } from '@metro-labs/core/stations/sender-profile';
12
12
  import { accountFor, accounts } from './accounts.js';
13
- import { respond } from './wire.js';
13
+ import { respond } from '@metro-labs/core/stations/station-runtime';
14
14
 
15
15
  export async function applyProfile(
16
16
  client: Client,
@@ -1,4 +1,5 @@
1
1
  import { appendFile } from '@metro-labs/core/stations/attachments';
2
+ import type { CanonicalAttachment } from '@metro-labs/core/stations/types';
2
3
 
3
4
  export interface OutgoingFile {
4
5
  path: string;
@@ -6,25 +7,23 @@ export interface OutgoingFile {
6
7
  kind: string;
7
8
  }
8
9
 
10
+ type WireAttachment = CanonicalAttachment & { kind?: string };
11
+
9
12
  const basenameOf = (path: string, index: number): string =>
10
13
  path.split('/').pop() ?? `file-${index}`;
11
14
 
12
- export function outgoingFiles(
13
- paths: string[],
14
- names: string[] | undefined,
15
- kinds: string[] | undefined,
16
- ): OutgoingFile[] {
15
+ export function outgoingFiles(attachments: unknown): OutgoingFile[] {
16
+ if (!Array.isArray(attachments)) return [];
17
17
  const out: OutgoingFile[] = [];
18
- for (let i = 0; i < paths.length; i++) {
19
- const path = paths[i];
20
- if (path === undefined || path === '') continue;
21
- const given = names?.[i];
18
+ (attachments as WireAttachment[]).forEach((a, i) => {
19
+ const path = a.path;
20
+ if (path === undefined || path === '') return;
22
21
  out.push({
23
22
  path,
24
- name: given !== undefined && given !== '' ? given : basenameOf(path, i),
25
- kind: kinds?.[i] ?? 'file',
23
+ name: a.name !== undefined && a.name !== '' ? a.name : basenameOf(path, i),
24
+ kind: a.kind ?? 'file',
26
25
  });
27
- }
26
+ });
28
27
  return out;
29
28
  }
30
29
 
@@ -52,11 +52,6 @@ export function apiSession(req: IncomingMessage): Promise<ApiSession | null> {
52
52
  return bearerSessions === null ? Promise.resolve(null) : bearerSessions(req);
53
53
  }
54
54
 
55
- export interface AgentIdentity {
56
- subject: string;
57
- agentId: string;
58
- }
59
-
60
55
  export async function readJsonBody(req: IncomingMessage, max = BODY_MAX): Promise<unknown> {
61
56
  const chunks: Buffer[] = [];
62
57
  let total = 0;
@@ -136,3 +131,5 @@ export function bodyField(body: unknown, key: string): unknown {
136
131
  ? (body as Record<string, unknown>)[key]
137
132
  : undefined;
138
133
  }
134
+
135
+ export const stringOf = (value: unknown): string => (typeof value === 'string' ? value : '');
@@ -8,7 +8,6 @@ export type DaemonMode = 'hosted' | 'local';
8
8
  export interface ModeInfo {
9
9
  mode: DaemonMode;
10
10
  owner: string | null;
11
- project: string | null;
12
11
  version: string;
13
12
  }
14
13
 
@@ -1,13 +1,9 @@
1
- import { homedir } from 'node:os';
2
- import { join } from 'node:path';
3
1
  import { makeAccountStore, resolveAccountId, type Die } from '@metro-labs/core/stations/account-store';
4
2
  import { TrainError } from '@metro-labs/core/train-error';
5
3
  import { refreshTokens, type FetchLike } from './auth.js';
6
4
  import { conversationOfLine } from './format.js';
7
5
  import { loadState, saveState, type AccountState } from './state.js';
8
6
 
9
- const ACCOUNTS_FILE = process.env.OUTLOOK_ACCOUNTS_FILE ?? join(homedir(), '.metro', 'outlook-accounts.json');
10
-
11
7
  const RENEW_BEFORE_MS = 5 * 60_000;
12
8
 
13
9
  export interface AccountConfig {
@@ -16,27 +12,18 @@ export interface AccountConfig {
16
12
  refreshToken: string;
17
13
  accessToken?: string;
18
14
  expiresAt?: number;
19
- tenantId?: string | null;
20
- deltaLink?: string | null;
21
15
  includeAutomated?: boolean;
22
16
  }
23
17
 
24
18
  function checkAccount(a: AccountConfig, die: Die): void {
25
- if (!a.id) die('account missing id');
26
19
  if (typeof a.accountEmail !== 'string' || !a.accountEmail.includes('@')) die(`account '${a.id}' has no mailbox address`);
27
20
  if (typeof a.refreshToken !== 'string' || a.refreshToken === '') die(`account '${a.id}' has no refresh token`);
28
21
  }
29
22
 
30
23
  export const { loadAccounts } = makeAccountStore<AccountConfig>({
31
24
  prefix: 'outlook',
32
- file: ACCOUNTS_FILE,
33
25
  validate(raw, die) {
34
- const seen = new Set<string>();
35
- for (const a of raw) {
36
- checkAccount(a, die);
37
- if (seen.has(a.id)) die(`duplicate account id '${a.id}'`);
38
- seen.add(a.id);
39
- }
26
+ for (const a of raw) checkAccount(a, die);
40
27
  },
41
28
  });
42
29
 
@@ -54,7 +41,6 @@ export class Account {
54
41
  refreshToken: cfg.refreshToken,
55
42
  accessToken: cfg.accessToken ?? '',
56
43
  expiresAt: cfg.expiresAt ?? 0,
57
- deltaLink: cfg.deltaLink ?? null,
58
44
  });
59
45
  }
60
46
 
@@ -1,20 +1,14 @@
1
1
  import { makeStation, respond } from '@metro-labs/core/stations/station-runtime';
2
- import type { Normalized } from '@metro-labs/core/stations/messaging-normalize';
3
2
  import { accounts } from './accounts.js';
4
3
  import { MAILBOX_URL } from './config.js';
5
4
  import { reply, send } from './outbound.js';
6
5
  import { read } from './query.js';
7
6
 
8
- type Args = Record<string, unknown>;
9
-
10
7
  function listAccounts(id: string): void {
11
8
  const list = [...accounts.values()].map((a) => ({ id: a.id, handle: a.email, url: MAILBOX_URL, email: a.email }));
12
9
  respond(id, { result: { accounts: list } });
13
10
  }
14
11
 
15
- const normalizeOutlook = (action: string, args: Args): Normalized => ({ action, args });
16
-
17
12
  export const handleCall = makeStation({
18
13
  handlers: { accounts: listAccounts, send, reply, read },
19
- normalize: normalizeOutlook,
20
14
  });
@@ -86,7 +86,6 @@ export function inboundEnvelope(accountId: string, self: string, m: GraphMessage
86
86
  const from = addressOf(m.from);
87
87
  const title = titleOf(m);
88
88
  return {
89
- kind: 'inbound',
90
89
  id: mintId(),
91
90
  ts: m.receivedDateTime ?? new Date().toISOString(),
92
91
  station: 'outlook',
@@ -27,7 +27,7 @@ export function loadState(accountId: string, seed: Partial<AccountState>): Accou
27
27
  refreshToken,
28
28
  accessToken: fromFile ? text(saved.accessToken) : text(seed.accessToken),
29
29
  expiresAt: fromFile ? Number(saved.expiresAt) || 0 : Number(seed.expiresAt) || 0,
30
- deltaLink: nullable(saved.deltaLink) ?? seed.deltaLink ?? null,
30
+ deltaLink: nullable(saved.deltaLink),
31
31
  syncedAt: nullable(saved.syncedAt),
32
32
  seen: Array.isArray(saved.seen) ? saved.seen.filter((s): s is string => typeof s === 'string') : [],
33
33
  };
@@ -1,5 +1,3 @@
1
- import { homedir } from 'node:os';
2
- import { join } from 'node:path';
3
1
  import {
4
2
  makeAccountStore,
5
3
  resolveAccountId,
@@ -7,31 +5,22 @@ import {
7
5
  } from '@metro-labs/core/stations/account-store';
8
6
  import type { UserAccount } from './types.js';
9
7
 
10
- const ACCOUNTS_FILE =
11
- process.env.TELEGRAM_ACCOUNTS_FILE ??
12
- join(homedir(), '.metro', 'telegram-accounts.json');
13
-
14
8
  const isSignedInt = (s: string): boolean => /^-?\d+$/.test(s);
15
9
  const isTopic = (s: string): boolean => /^\d+$/.test(s);
16
10
 
17
- function validateAccount(a: UserAccount, seen: Set<string>, die: Die): void {
18
- if (!a.id) die('account missing id');
11
+ function validateAccount(a: UserAccount, die: Die): void {
19
12
  if (!a.session || typeof a.session !== 'string')
20
13
  die(`account '${a.id}' missing session`);
21
14
  if (!Number.isInteger(a.apiId) || (a.apiId ?? 0) <= 0)
22
15
  die(`account '${a.id}' missing apiId`);
23
16
  if (!a.apiHash || typeof a.apiHash !== 'string')
24
17
  die(`account '${a.id}' missing apiHash`);
25
- if (seen.has(a.id)) die(`duplicate account id '${a.id}'`);
26
- seen.add(a.id);
27
18
  }
28
19
 
29
20
  export const { loadAccounts } = makeAccountStore<UserAccount>({
30
21
  prefix: 'telegram',
31
- file: ACCOUNTS_FILE,
32
22
  validate(raw, die) {
33
- const seen = new Set<string>();
34
- for (const a of raw) validateAccount(a, seen, die);
23
+ for (const a of raw) validateAccount(a, die);
35
24
  },
36
25
  });
37
26
 
@@ -56,20 +45,13 @@ interface Target {
56
45
  topicId?: number;
57
46
  }
58
47
 
59
- function splitScoped(path: string[]): { accountId: string; rest: string[] } {
60
- const first = path[0];
61
- if (path.length >= 2 && first !== undefined && !isSignedInt(first))
62
- return { accountId: first, rest: path.slice(1) };
63
- return { accountId: 'default', rest: path };
64
- }
65
-
66
48
  export function targetOf(line: string): Target | undefined {
67
49
  const prefix = 'metro://telegram/';
68
50
  if (!line.startsWith(prefix)) return undefined;
69
51
  const path = line.slice(prefix.length).split('/').filter(Boolean);
70
- const { accountId, rest } = splitScoped(path);
52
+ const [accountId, ...rest] = path;
71
53
  const [chatId, topicId] = rest;
72
- if (rest.length < 1 || rest.length > 2 || chatId === undefined) return undefined;
54
+ if (accountId === undefined || chatId === undefined || rest.length > 2) return undefined;
73
55
  if (!isSignedInt(chatId)) return undefined;
74
56
  if (topicId !== undefined && !isTopic(topicId)) return undefined;
75
57
  return {