@stage-labs/metro 0.1.0-beta.324 → 0.1.0-beta.326

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 (176) hide show
  1. package/dist/agent.js +1 -1
  2. package/dist/cli.js +1 -1
  3. package/dist/lock.js +15 -3
  4. package/dist/root-release-io.js +1 -2
  5. package/dist/root-release.js +2 -3
  6. package/dist/route.js +1 -1
  7. package/dist/service.js +4 -4
  8. package/dist/version.js +1 -1
  9. package/package.json +1 -1
  10. package/runtime/node_modules/@metro-labs/core/src/automation-store.ts +1 -1
  11. package/runtime/node_modules/@metro-labs/core/src/automation-types.ts +2 -2
  12. package/runtime/node_modules/@metro-labs/core/src/lines.ts +2 -8
  13. package/runtime/node_modules/@metro-labs/core/src/protocol.ts +0 -6
  14. package/runtime/node_modules/@metro-labs/core/src/skill-source.ts +1 -1
  15. package/runtime/node_modules/@metro-labs/core/src/stations/account-status.ts +1 -1
  16. package/runtime/node_modules/@metro-labs/core/src/stations/attachments.ts +5 -9
  17. package/runtime/node_modules/@metro-labs/core/src/stations/channel-directory.ts +1 -1
  18. package/runtime/node_modules/@metro-labs/core/src/stations/mail-text.ts +4 -4
  19. package/runtime/node_modules/@metro-labs/core/src/stations/mail.ts +3 -2
  20. package/runtime/node_modules/@metro-labs/core/src/stations/messaging-normalize.ts +1 -1
  21. package/runtime/node_modules/@metro-labs/core/src/stations/oauth.ts +1 -1
  22. package/runtime/node_modules/@metro-labs/core/src/stations/profile.ts +2 -2
  23. package/runtime/node_modules/@metro-labs/core/src/stations/types.ts +2 -2
  24. package/runtime/node_modules/@metro-labs/core/src/train-error.ts +4 -4
  25. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/files.ts +2 -6
  26. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/schedules.ts +6 -6
  27. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/user.ts +0 -24
  28. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/view.ts +3 -3
  29. package/runtime/node_modules/@metro-labs/daemon/src/agents/bundle.ts +3 -4
  30. package/runtime/node_modules/@metro-labs/daemon/src/agents/file-admin.ts +4 -9
  31. package/runtime/node_modules/@metro-labs/daemon/src/agents/files.ts +1 -6
  32. package/runtime/node_modules/@metro-labs/daemon/src/channels/inbound.ts +3 -3
  33. package/runtime/node_modules/@metro-labs/daemon/src/channels/pending.ts +2 -2
  34. package/runtime/node_modules/@metro-labs/daemon/src/claude/files.ts +1 -1
  35. package/runtime/node_modules/@metro-labs/daemon/src/claude/skills.ts +1 -1
  36. package/runtime/node_modules/@metro-labs/daemon/src/files/attach-grant.ts +3 -2
  37. package/runtime/node_modules/@metro-labs/daemon/src/files/attach-serve.ts +1 -1
  38. package/runtime/node_modules/@metro-labs/daemon/src/files/upload-api.ts +3 -7
  39. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-api.ts +2 -2
  40. package/runtime/node_modules/@metro-labs/daemon/src/mcp/accounts.ts +10 -19
  41. package/runtime/node_modules/@metro-labs/daemon/src/mcp/call-tools.ts +1 -5
  42. package/runtime/node_modules/@metro-labs/daemon/src/mcp/event-store.ts +3 -4
  43. package/runtime/node_modules/@metro-labs/daemon/src/mcp/group-tools.ts +1 -2
  44. package/runtime/node_modules/@metro-labs/daemon/src/mcp/index.ts +2 -4
  45. package/runtime/node_modules/@metro-labs/daemon/src/mcp/policy-gate.ts +5 -6
  46. package/runtime/node_modules/@metro-labs/daemon/src/mcp/server-approvals.ts +2 -5
  47. package/runtime/node_modules/@metro-labs/daemon/src/mcp/session.ts +1 -5
  48. package/runtime/node_modules/@metro-labs/daemon/src/mcp/tool-catalog.ts +2 -4
  49. package/runtime/node_modules/@metro-labs/daemon/src/mcp-server/announced.ts +1 -1
  50. package/runtime/node_modules/@metro-labs/daemon/src/metro-user/helper-script.ts +0 -1
  51. package/runtime/node_modules/@metro-labs/daemon/src/metro-user/install.ts +2 -0
  52. package/runtime/node_modules/@metro-labs/daemon/src/metro-user/tailscale-guard.ts +26 -0
  53. package/runtime/node_modules/@metro-labs/daemon/src/monitor/api.ts +1 -3
  54. package/runtime/node_modules/@metro-labs/daemon/src/routes/gmail-attach.ts +3 -3
  55. package/runtime/node_modules/@metro-labs/daemon/src/routes/http.ts +6 -21
  56. package/runtime/node_modules/@metro-labs/daemon/src/routes/local-mode.ts +1 -5
  57. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-resolve.ts +6 -6
  58. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach.ts +0 -1
  59. package/runtime/node_modules/@metro-labs/daemon/src/stations/materialize.ts +12 -61
  60. package/runtime/node_modules/@metro-labs/daemon/src/stations/supervisor.ts +13 -25
  61. package/runtime/node_modules/@metro-labs/daemon/src/stations/threema-callbacks.ts +5 -5
  62. package/runtime/node_modules/@metro-labs/daemon/src/stations/webhook-endpoints.ts +2 -7
  63. package/runtime/node_modules/@metro-labs/daemon/src/voice/call.ts +2 -6
  64. package/runtime/node_modules/@metro-labs/daemon/src/voice/calls.ts +12 -12
  65. package/runtime/node_modules/@metro-labs/discord-bot/src/actions.ts +5 -23
  66. package/runtime/node_modules/@metro-labs/discord-bot/src/attachments.ts +1 -7
  67. package/runtime/node_modules/@metro-labs/discord-bot/src/channels.ts +2 -2
  68. package/runtime/node_modules/@metro-labs/discord-bot/src/group.ts +10 -35
  69. package/runtime/node_modules/@metro-labs/discord-bot/src/index.ts +5 -11
  70. package/runtime/node_modules/@metro-labs/discord-bot/src/members.ts +6 -34
  71. package/runtime/node_modules/@metro-labs/discord-bot/src/profile.ts +11 -14
  72. package/runtime/node_modules/@metro-labs/discord-bot/src/send-files.ts +1 -3
  73. package/runtime/node_modules/@metro-labs/discord-bot/src/verify.ts +2 -3
  74. package/runtime/node_modules/@metro-labs/gmail/src/accounts.ts +3 -3
  75. package/runtime/node_modules/@metro-labs/gmail/src/auth.ts +8 -8
  76. package/runtime/node_modules/@metro-labs/gmail/src/managed.ts +3 -2
  77. package/runtime/node_modules/@metro-labs/http/src/agent-signature.ts +1 -1
  78. package/runtime/node_modules/@metro-labs/http/src/api-http.ts +3 -4
  79. package/runtime/node_modules/@metro-labs/http/src/connector-discovery.ts +0 -113
  80. package/runtime/node_modules/@metro-labs/http/src/connector-input.ts +1 -1
  81. package/runtime/node_modules/@metro-labs/http/src/connector-mcp.ts +12 -13
  82. package/runtime/node_modules/@metro-labs/http/src/connector-oauth.ts +1 -1
  83. package/runtime/node_modules/@metro-labs/http/src/connector-policy.ts +1 -6
  84. package/runtime/node_modules/@metro-labs/http/src/connector-public-url.ts +4 -2
  85. package/runtime/node_modules/@metro-labs/http/src/connector-tools.ts +1 -11
  86. package/runtime/node_modules/@metro-labs/http/src/connector-url.ts +0 -6
  87. package/runtime/node_modules/@metro-labs/http/src/mcp-server-protocol.ts +1 -1
  88. package/runtime/node_modules/@metro-labs/http/src/mcp-server-tools.ts +0 -2
  89. package/runtime/node_modules/@metro-labs/http/src/mode-api.ts +1 -1
  90. package/runtime/node_modules/@metro-labs/http/src/return-to.ts +2 -2
  91. package/runtime/node_modules/@metro-labs/http/src/workos-token.ts +6 -7
  92. package/runtime/node_modules/@metro-labs/inference/package.json +1 -0
  93. package/runtime/node_modules/@metro-labs/inference/src/attempts.ts +3 -3
  94. package/runtime/node_modules/@metro-labs/inference/src/bedrock.ts +1 -1
  95. package/runtime/node_modules/@metro-labs/inference/src/codex-translate.ts +2 -2
  96. package/runtime/node_modules/@metro-labs/inference/src/delivered.ts +1 -1
  97. package/runtime/node_modules/@metro-labs/inference/src/effort.ts +0 -6
  98. package/runtime/node_modules/@metro-labs/inference/src/fallback.ts +7 -5
  99. package/runtime/node_modules/@metro-labs/inference/src/forward.ts +5 -5
  100. package/runtime/node_modules/@metro-labs/inference/src/measure.ts +2 -3
  101. package/runtime/node_modules/@metro-labs/inference/src/named.ts +3 -3
  102. package/runtime/node_modules/@metro-labs/inference/src/probes.ts +98 -0
  103. package/runtime/node_modules/@metro-labs/inference/src/usage.ts +5 -5
  104. package/runtime/node_modules/@metro-labs/outlook/src/accounts.ts +6 -8
  105. package/runtime/node_modules/@metro-labs/outlook/src/auth.ts +2 -1
  106. package/runtime/node_modules/@metro-labs/outlook/src/inbound.ts +3 -5
  107. package/runtime/node_modules/@metro-labs/outlook/src/me.ts +6 -6
  108. package/runtime/node_modules/@metro-labs/outlook/src/outbound.ts +10 -13
  109. package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +7 -26
  110. package/runtime/node_modules/@metro-labs/telegram/src/attachments.ts +1 -1
  111. package/runtime/node_modules/@metro-labs/telegram/src/login.ts +3 -6
  112. package/runtime/node_modules/@metro-labs/telegram/src/profile.ts +3 -2
  113. package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +6 -20
  114. package/runtime/node_modules/@metro-labs/telegram-bot/src/attachments.ts +1 -7
  115. package/runtime/node_modules/@metro-labs/telegram-bot/src/format.ts +0 -2
  116. package/runtime/node_modules/@metro-labs/telegram-bot/src/media-actions.ts +1 -16
  117. package/runtime/node_modules/@metro-labs/telegram-bot/src/members.ts +1 -8
  118. package/runtime/node_modules/@metro-labs/telegram-bot/src/profile.ts +2 -1
  119. package/runtime/node_modules/@metro-labs/threema/src/actions.ts +1 -8
  120. package/runtime/node_modules/@metro-labs/threema/src/chats.ts +4 -3
  121. package/runtime/node_modules/@metro-labs/threema/src/format.ts +15 -17
  122. package/runtime/node_modules/@metro-labs/threema/src/groups.ts +2 -4
  123. package/runtime/node_modules/@metro-labs/threema/src/inbound.ts +2 -1
  124. package/runtime/node_modules/@metro-labs/threema/src/messages.ts +2 -1
  125. package/runtime/node_modules/@metro-labs/threema/src/outbound.ts +7 -7
  126. package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +1 -3
  127. package/runtime/node_modules/@metro-labs/whatsapp/src/attachments.ts +0 -2
  128. package/runtime/node_modules/@metro-labs/whatsapp/src/client.ts +1 -1
  129. package/runtime/node_modules/@metro-labs/whatsapp/src/login.ts +1 -1
  130. package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +7 -0
  131. package/runtime/node_modules/@metro-labs/xmtp/src/actions-conv.ts +4 -10
  132. package/runtime/node_modules/@metro-labs/xmtp/src/actions-meta.ts +4 -5
  133. package/runtime/node_modules/@metro-labs/xmtp/src/actions.ts +14 -26
  134. package/runtime/node_modules/@metro-labs/xmtp/src/attachments.ts +1 -7
  135. package/runtime/node_modules/@metro-labs/xmtp/src/codecs.ts +17 -58
  136. package/runtime/node_modules/@metro-labs/xmtp/src/conv-helpers.ts +1 -6
  137. package/runtime/node_modules/@metro-labs/xmtp/src/emit-payloads.ts +10 -31
  138. package/runtime/node_modules/@metro-labs/xmtp/src/frames.ts +12 -14
  139. package/runtime/node_modules/@metro-labs/xmtp/src/group.ts +2 -3
  140. package/runtime/node_modules/@metro-labs/xmtp/src/identity.ts +3 -9
  141. package/runtime/node_modules/@metro-labs/xmtp/src/labels.ts +8 -9
  142. package/runtime/node_modules/@metro-labs/xmtp/src/names.ts +4 -2
  143. package/runtime/node_modules/@metro-labs/xmtp/src/network.ts +2 -3
  144. package/runtime/node_modules/@metro-labs/xmtp/src/profile.ts +2 -1
  145. package/runtime/node_modules/@metro-labs/xmtp/src/read-attachments.ts +2 -2
  146. package/runtime/node_modules/@metro-labs/xmtp/src/sender.ts +2 -1
  147. package/runtime/node_modules/@metro-labs/xmtp/src/transcribe.ts +23 -46
  148. package/runtime/node_modules/@metro-labs/xmtp/src/wallet.ts +5 -4
  149. package/runtime/node_modules/@metro-labs/xmtp/src/wire.ts +12 -15
  150. package/runtime/runtime.json +1 -1
  151. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/automation-store.ts +1 -1
  152. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/automation-types.ts +2 -2
  153. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/lines.ts +2 -8
  154. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/protocol.ts +0 -6
  155. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/skill-source.ts +1 -1
  156. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/account-status.ts +1 -1
  157. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/attachments.ts +5 -9
  158. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/channel-directory.ts +1 -1
  159. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/mail-text.ts +4 -4
  160. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/mail.ts +3 -2
  161. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/messaging-normalize.ts +1 -1
  162. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/oauth.ts +1 -1
  163. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/profile.ts +2 -2
  164. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/types.ts +2 -2
  165. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/train-error.ts +4 -4
  166. package/runtime/sdk-runner/runner.json +1 -1
  167. package/runtime/sdk-runner/src/approvals.ts +2 -2
  168. package/runtime/sdk-runner/src/automation-command.ts +2 -2
  169. package/runtime/sdk-runner/src/calls.ts +0 -6
  170. package/runtime/sdk-runner/src/errors.ts +1 -1
  171. package/runtime/sdk-runner/src/link.ts +1 -1
  172. package/runtime/sdk-runner/src/runner.ts +2 -2
  173. package/runtime/sdk-runner/src/session-watch.ts +5 -34
  174. package/runtime/sdk-runner/src/task-state.ts +1 -1
  175. package/runtime/sdk-runner/src/tool-proxy.ts +1 -1
  176. package/runtime/node_modules/@metro-labs/xmtp/src/member-args.ts +0 -40
@@ -1,4 +1,5 @@
1
1
  import type { IncomingMessage, OutgoingHttpHeader, OutgoingHttpHeaders, ServerResponse } from 'node:http';
2
+ import { isRecord } from '@metro-labs/core/is-record';
2
3
  import { StreamErrors, type FailureSite } from './failures.js';
3
4
  import { UsageScanner } from './usage.js';
4
5
 
@@ -108,9 +109,8 @@ export function parseJson(raw: Buffer): Record<string, unknown> {
108
109
  } catch {
109
110
  throw new GatewayError(400, 'invalid_request_error', 'body must be JSON');
110
111
  }
111
- if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
112
- throw new GatewayError(400, 'invalid_request_error', 'body must be a JSON object');
113
- return parsed as Record<string, unknown>;
112
+ if (!isRecord(parsed)) throw new GatewayError(400, 'invalid_request_error', 'body must be a JSON object');
113
+ return parsed;
114
114
  }
115
115
 
116
116
  export function forwardedHeaders(req: IncomingMessage): Record<string, string> {
@@ -153,7 +153,7 @@ export function addBeta(headers: Record<string, string>, beta: string): Record<s
153
153
  return { ...headers, 'anthropic-beta': [...listed, beta].join(',') };
154
154
  }
155
155
 
156
- export interface PipeOptions {
156
+ interface PipeOptions {
157
157
  keepalive?: boolean;
158
158
  ownCredential?: boolean;
159
159
  scanner?: UsageScanner;
@@ -215,7 +215,7 @@ export async function pipeResponse(upstream: Response, res: ServerResponse, watc
215
215
  }
216
216
  }
217
217
 
218
- export interface Relayed {
218
+ interface Relayed {
219
219
  push: (chunk: Uint8Array, emit: (frames: string) => void) => void;
220
220
  close: () => string;
221
221
  idle: (message: string) => string;
@@ -2,7 +2,7 @@ import type { ServerResponse } from 'node:http';
2
2
  import { errMsg, log } from '@metro-labs/core/log';
3
3
  import { Attempt, inAttempt, tracked, type AttemptCall, type OnCall } from './attempts.js';
4
4
  import type { RequestKind } from './effort.js';
5
- import { GatewayError, SubscriptionRefused } from './forward.js';
5
+ import { GatewayError } from './forward.js';
6
6
  import type { Route } from './model-config.js';
7
7
 
8
8
  const MESSAGES = '/v1/messages';
@@ -15,8 +15,7 @@ export interface Asked {
15
15
 
16
16
  function statusOf(failure: unknown, res: ServerResponse): number {
17
17
  if (failure === null) return res.statusCode;
18
- if (failure instanceof GatewayError) return failure.status;
19
- return failure instanceof SubscriptionRefused ? 401 : 502;
18
+ return failure instanceof GatewayError ? failure.status : 502;
20
19
  }
21
20
 
22
21
  function report(onCall: OnCall, call: AttemptCall): void {
@@ -10,8 +10,8 @@ export interface Chosen {
10
10
 
11
11
  const NO_ROUTE = "metro gateway: no model is set up for this agent yet: choose one on the organization's Gateway page";
12
12
 
13
- const noRoute = (message: string): never => {
14
- throw new GatewayError(503, 'api_error', message);
13
+ const noRoute = (): never => {
14
+ throw new GatewayError(503, 'api_error', NO_ROUTE);
15
15
  };
16
16
 
17
17
  const MAIN = 'metro';
@@ -32,5 +32,5 @@ export function namedChoice(requested: string, setup: ModelSetup): Chosen {
32
32
  if (requested !== MAIN && requested !== WORKERS) throw new GatewayError(400, 'invalid_request_error', NAMES);
33
33
  const helpers = requested === WORKERS ? helperSetup(setup) : null;
34
34
  const chain = helpers ?? setup;
35
- return { lane: helpers === null ? 'main' : 'helpers', setup: chain, primary: primaryRoute(chain) ?? noRoute(NO_ROUTE) };
35
+ return { lane: helpers === null ? 'main' : 'helpers', setup: chain, primary: primaryRoute(chain) ?? noRoute() };
36
36
  }
@@ -0,0 +1,98 @@
1
+ import { isRecord } from '@metro-labs/core/is-record';
2
+ import { userAgent } from './codex.js';
3
+ import { OAUTH_BETA } from './forward.js';
4
+ import type { Connection } from './model-config.js';
5
+ import { openrouterKey } from './openrouter.js';
6
+ import { anthropicUsage, clamp, creditsLeft, mergeUsage, openrouterUsage, PER_MINUTE, whenFrom, windowLabel, type Reported, type UsageWindow } from './usage.js';
7
+
8
+ export const PROBE_EVERY_MS = 5 * 60_000;
9
+ const PROBE_TIMEOUT_MS = 10_000;
10
+ const SECONDS_PER_MINUTE = 60;
11
+ const CHATGPT_BACKEND = 'https://chatgpt.com/backend-api';
12
+ const ANTHROPIC_API = 'https://api.anthropic.com';
13
+ const COUNT_MODEL = 'claude-haiku-4-5';
14
+
15
+ export interface ProbeDeps {
16
+ fetchImpl?: typeof fetch;
17
+ openrouterBase?: string;
18
+ chatgptBase?: string;
19
+ anthropicBase?: string;
20
+ }
21
+
22
+ const numberOf = (value: unknown): number | null => {
23
+ const parsed = typeof value === 'number' ? value : typeof value === 'string' && value.trim() !== '' ? Number(value) : Number.NaN;
24
+ return Number.isFinite(parsed) ? parsed : null;
25
+ };
26
+
27
+ const textOf = (value: unknown): string | null => (typeof value === 'string' || typeof value === 'number' ? String(value) : null);
28
+
29
+ function chatgptWindow(raw: unknown, now: Date): UsageWindow | null {
30
+ if (!isRecord(raw)) return null;
31
+ const percent = numberOf(raw.used_percent);
32
+ if (percent === null) return null;
33
+ const seconds = numberOf(raw.limit_window_seconds);
34
+ const minutes = seconds === null || seconds <= 0 ? null : Math.ceil(seconds / SECONDS_PER_MINUTE);
35
+ const resetAt = whenFrom(textOf(raw.reset_at), now) ?? whenFrom(textOf(raw.reset_after_seconds), now, true);
36
+ return { label: windowLabel(minutes), used: clamp(percent / 100), resetAt, detail: null };
37
+ }
38
+
39
+ function chatgptWall(limit: Record<string, unknown>, reached: unknown): string | null {
40
+ if (limit.limit_reached !== true && limit.allowed !== false) return null;
41
+ const kind = isRecord(reached) && typeof reached.type === 'string' ? reached.type : '';
42
+ return kind === '' ? 'limit reached' : kind.replaceAll('_', ' ');
43
+ }
44
+
45
+ export function chatgptUsage(body: unknown, now = new Date()): Reported | null {
46
+ if (!isRecord(body)) return null;
47
+ const limit = isRecord(body.rate_limit) ? body.rate_limit : {};
48
+ const credits = isRecord(body.credits) ? body.credits : {};
49
+ const windows = [chatgptWindow(limit.primary_window, now), chatgptWindow(limit.secondary_window, now), creditsLeft(credits.has_credits === true, textOf(credits.balance))].filter(
50
+ (w): w is UsageWindow => w !== null,
51
+ );
52
+ return windows.length === 0 ? null : { windows, note: chatgptWall(limit, body.rate_limit_reached_type), at: now.toISOString() };
53
+ }
54
+
55
+ async function answer(res: Response, provider: string): Promise<Response> {
56
+ if (res.ok) return res;
57
+ await res.body?.cancel();
58
+ throw new Error(`${provider} would not report its usage (${String(res.status)})`);
59
+ }
60
+
61
+ async function chatgptProbe(conn: Connection, deps: ProbeDeps, now: Date): Promise<Reported | null> {
62
+ const access = conn.codex;
63
+ if (access?.method !== 'code') return null;
64
+ const headers = { accept: 'application/json', authorization: `Bearer ${access.accessToken}`, 'chatgpt-account-id': access.accountId ?? '', originator: 'codex_cli_rs', 'user-agent': userAgent() };
65
+ const res = await (deps.fetchImpl ?? fetch)(`${deps.chatgptBase ?? CHATGPT_BACKEND}/wham/usage`, { headers, redirect: 'manual', signal: AbortSignal.timeout(PROBE_TIMEOUT_MS) });
66
+ return chatgptUsage(await (await answer(res, 'ChatGPT')).json(), now);
67
+ }
68
+
69
+ async function claudeProbe(conn: Connection, deps: ProbeDeps, now: Date): Promise<Reported | null> {
70
+ if (conn.claudeToken === '') return null;
71
+ const res = await (deps.fetchImpl ?? fetch)(`${deps.anthropicBase ?? ANTHROPIC_API}/v1/messages/count_tokens`, {
72
+ method: 'POST',
73
+ headers: { authorization: `Bearer ${conn.claudeToken}`, 'anthropic-version': '2023-06-01', 'anthropic-beta': OAUTH_BETA, 'content-type': 'application/json' },
74
+ body: JSON.stringify({ model: COUNT_MODEL, messages: [{ role: 'user', content: 'usage' }] }),
75
+ redirect: 'manual',
76
+ signal: AbortSignal.timeout(PROBE_TIMEOUT_MS),
77
+ });
78
+ await (await answer(res, 'Anthropic')).body?.cancel();
79
+ const usage = anthropicUsage(res.headers, now);
80
+ return usage === null || usage.windows.every((w) => w.label === PER_MINUTE) ? null : usage;
81
+ }
82
+
83
+ async function reported(conn: Connection, deps: ProbeDeps, now: Date): Promise<Reported | null> {
84
+ if (conn.provider === 'openrouter') return conn.apiKey === '' ? null : openrouterUsage(await openrouterKey(conn.apiKey, deps.openrouterBase, deps.fetchImpl), now);
85
+ if (conn.provider === 'codex') return chatgptProbe(conn, deps, now);
86
+ if (conn.provider === 'anthropic') return claudeProbe(conn, deps, now);
87
+ return null;
88
+ }
89
+
90
+ export const probes = (conn: Connection): boolean =>
91
+ (conn.provider === 'openrouter' && conn.apiKey !== '') || (conn.provider === 'codex' && conn.codex?.method === 'code') || (conn.provider === 'anthropic' && conn.claudeToken !== '');
92
+
93
+ export async function probeUsage(conn: Connection, deps: ProbeDeps = {}, now = new Date()): Promise<boolean> {
94
+ const usage = await reported(conn, deps, now);
95
+ if (usage === null) return false;
96
+ mergeUsage(conn.id, usage);
97
+ return true;
98
+ }
@@ -35,7 +35,7 @@ export const noteUsage = (key: Key, usage: Reported): void => {
35
35
  stamps.set(key, new Map(usage.windows.map((w) => [w.label, Date.parse(usage.at)])));
36
36
  };
37
37
 
38
- function mergeUsage(key: Key, usage: Reported): void {
38
+ export function mergeUsage(key: Key, usage: Reported): void {
39
39
  const seen = latest.get(key);
40
40
  const marks = stamps.get(key);
41
41
  if (seen === undefined || marks === undefined) {
@@ -76,7 +76,7 @@ export function mayProbe(key: Key, now: number, every: number): boolean {
76
76
  return true;
77
77
  }
78
78
 
79
- export interface Counted {
79
+ interface Counted {
80
80
  input: number;
81
81
  output: number;
82
82
  cached: number;
@@ -158,7 +158,7 @@ export class UsageScanner {
158
158
  }
159
159
  }
160
160
 
161
- const clamp = (value: number): number => Math.min(1, Math.max(0, value));
161
+ export const clamp = (value: number): number => Math.min(1, Math.max(0, value));
162
162
 
163
163
  function fraction(raw: string | null): number | null {
164
164
  if (raw === null) return null;
@@ -168,7 +168,7 @@ function fraction(raw: string | null): number | null {
168
168
 
169
169
  const MS_THRESHOLD = 1e12;
170
170
 
171
- function whenFrom(raw: string | null, now: Date, relative = false): string | null {
171
+ export function whenFrom(raw: string | null, now: Date, relative = false): string | null {
172
172
  if (raw === null || raw.trim() === '') return null;
173
173
  const numeric = Number(raw);
174
174
  if (Number.isFinite(numeric)) {
@@ -247,7 +247,7 @@ function codexWindow(headers: Headers, kind: 'primary' | 'secondary', now: Date)
247
247
  return { label: windowLabel(Number.isFinite(minutes) ? minutes : null), used, resetAt, detail: null };
248
248
  }
249
249
 
250
- function creditsLeft(hasCredits: boolean, raw: string | null): UsageWindow | null {
250
+ export function creditsLeft(hasCredits: boolean, raw: string | null): UsageWindow | null {
251
251
  if (!hasCredits) return null;
252
252
  const balance = Number(raw ?? 'none');
253
253
  if (!Number.isFinite(balance)) return null;
@@ -1,3 +1,4 @@
1
+ import { errMsg } from '@metro-labs/core/log';
1
2
  import { makeAccountStore, resolveAccountId, type Die } from '@metro-labs/core/stations/account-store';
2
3
  import { threadOfLine } from '@metro-labs/core/stations/mail';
3
4
  import { TokenKeeper, type FetchLike } from '@metro-labs/core/stations/oauth';
@@ -27,7 +28,7 @@ export const { loadAccounts } = makeAccountStore<AccountConfig>({
27
28
  });
28
29
 
29
30
  const signedOut = (err: unknown): never => {
30
- throw new TrainError('outlook_signed_out', err instanceof Error ? err.message : String(err), { retryable: false });
31
+ throw new TrainError('outlook_signed_out', errMsg(err), { retryable: false });
31
32
  };
32
33
 
33
34
  export class Account {
@@ -65,14 +66,11 @@ export class Account {
65
66
 
66
67
  export const accounts = new Map<string, Account>();
67
68
 
68
- function accountFor(id: string): Account {
69
- const acct = accounts.get(id);
70
- if (!acct) throw new Error(`unknown account '${id}' (have: ${[...accounts.keys()].join(', ')})`);
71
- return acct;
72
- }
73
-
74
69
  export function accountOf(args: { account?: unknown; line?: unknown }): Account {
75
70
  const line = typeof args.line === 'string' && args.line !== '' ? args.line : undefined;
76
71
  const account = typeof args.account === 'string' && args.account !== '' ? args.account : undefined;
77
- return accountFor(resolveAccountId(accounts, { account, line }, (l) => threadOfLine('outlook', l)?.accountId));
72
+ const id = resolveAccountId(accounts, { account, line }, (l) => threadOfLine('outlook', l)?.accountId);
73
+ const acct = accounts.get(id);
74
+ if (!acct) throw new Error(`unknown account '${id}'`);
75
+ return acct;
78
76
  }
@@ -1,3 +1,4 @@
1
+ import { isRecord } from '@metro-labs/core/is-record';
1
2
  import { postForm, seconds, SignInError, tokensOf, type FetchLike, type OAuthBody, type Tokens } from '@metro-labs/core/stations/oauth';
2
3
  import { clientId, loginBase, NOT_SET_UP, SCOPES } from './config.js';
3
4
 
@@ -64,7 +65,7 @@ function decodeClaims(jwt: string): Body | null {
64
65
  if (part === undefined) return null;
65
66
  try {
66
67
  const raw: unknown = JSON.parse(Buffer.from(part, 'base64url').toString('utf8'));
67
- return typeof raw === 'object' && raw !== null ? (raw as Body) : null;
68
+ return isRecord(raw) ? raw : null;
68
69
  } catch {
69
70
  return null;
70
71
  }
@@ -1,4 +1,4 @@
1
- import { log } from '@metro-labs/core/log';
1
+ import { errMsg, log } from '@metro-labs/core/log';
2
2
  import { mailEnvelope, noteSeen, pollForever } from '@metro-labs/core/stations/mail';
3
3
  import type { Screened } from '@metro-labs/core/stations/mail-trust';
4
4
  import { emitInbound, reportAttachment } from '@metro-labs/core/stations/train-events';
@@ -26,11 +26,9 @@ function fresh(acct: Account, m: GraphMessage, since: string): boolean {
26
26
  return Date.parse(m.receivedDateTime ?? '') >= Date.parse(since);
27
27
  }
28
28
 
29
- const reasonOf = (err: unknown): string => (err instanceof Error ? err.message : String(err));
30
-
31
29
  async function screened(acct: Account, m: GraphMessage): Promise<Screened> {
32
30
  return screen(acct, m).catch((err: unknown) => {
33
- process.stderr.write(`outlook[${acct.id}] could not read the headers of a message, so it counts as unverified: ${reasonOf(err)}\n`);
31
+ process.stderr.write(`outlook[${acct.id}] could not read the headers of a message, so it counts as unverified: ${errMsg(err)}\n`);
34
32
  return { verified: false };
35
33
  });
36
34
  }
@@ -44,7 +42,7 @@ async function deliver(acct: Account, m: GraphMessage): Promise<boolean> {
44
42
  const files: MailFile[] =
45
43
  m.hasAttachments === true
46
44
  ? await listFiles(acct, m.id).catch((err: unknown) => {
47
- process.stderr.write(`outlook[${acct.id}] could not list the files of a message: ${reasonOf(err)}\n`);
45
+ process.stderr.write(`outlook[${acct.id}] could not list the files of a message: ${errMsg(err)}\n`);
48
46
  return [];
49
47
  })
50
48
  : [];
@@ -1,3 +1,5 @@
1
+ import { errMsg } from '@metro-labs/core/log';
2
+ import { str } from '@metro-labs/core/stations/mail';
1
3
  import { SignInError, type FetchLike } from '@metro-labs/core/stations/oauth';
2
4
  import { graphBase } from './config.js';
3
5
 
@@ -6,8 +8,6 @@ export interface Mailbox {
6
8
  name: string;
7
9
  }
8
10
 
9
- const text = (v: unknown): string => (typeof v === 'string' ? v.trim() : '');
10
-
11
11
  const wrongMailbox = (actual: string, wanted: string): string =>
12
12
  `You signed in as ${actual}, not ${wanted}. Nothing was connected. Start again and pick ${wanted} on Microsoft's page, or use "Use another account".`;
13
13
 
@@ -18,14 +18,14 @@ export async function verifyMailbox(accessToken: string, fetchImpl: FetchLike, w
18
18
  headers: { authorization: `Bearer ${accessToken}`, accept: 'application/json' },
19
19
  });
20
20
  } catch (err) {
21
- throw new SignInError(`Metro could not reach Microsoft Graph: ${err instanceof Error ? err.message : String(err)}`);
21
+ throw new SignInError(`Metro could not reach Microsoft Graph: ${errMsg(err)}`);
22
22
  }
23
23
  if (!res.ok) throw new SignInError(`Microsoft Graph refused the new sign-in (${String(res.status)}), so nothing was connected.`);
24
24
  const body = (await res.json().catch(() => ({}))) as Record<string, unknown>;
25
- const mail = text(body.mail).toLowerCase();
26
- const principal = text(body.userPrincipalName).toLowerCase();
25
+ const mail = str(body.mail).toLowerCase();
26
+ const principal = str(body.userPrincipalName).toLowerCase();
27
27
  const email = mail || principal;
28
28
  if (email === '') throw new SignInError('Microsoft Graph did not say which mailbox this is, so nothing was connected.');
29
29
  if (wanted !== null && wanted !== mail && wanted !== principal) throw new SignInError(wrongMailbox(email, wanted));
30
- return { email, name: text(body.displayName) };
30
+ return { email, name: str(body.displayName) };
31
31
  }
@@ -1,5 +1,6 @@
1
1
  import { checkAddress, filesOf, isAddress, mailLine, str, subjectOf, threadOfLine, type OutgoingFile } from '@metro-labs/core/stations/mail';
2
2
  import { escapeHtml } from '@metro-labs/core/stations/mail-text';
3
+ import { errMsg } from '@metro-labs/core/log';
3
4
  import { respond } from '@metro-labs/core/stations/station-runtime';
4
5
  import { TrainError } from '@metro-labs/core/train-error';
5
6
  import { accountOf, type Account } from './accounts.js';
@@ -22,14 +23,11 @@ export async function latestInConversation(acct: Account, conversationId: string
22
23
 
23
24
  async function discard(acct: Account, draftId: string): Promise<void> {
24
25
  await graph(acct, messagePath(draftId), { method: 'DELETE' }).catch((err: unknown) => {
25
- process.stderr.write(`outlook[${acct.id}] could not remove an unsent draft: ${err instanceof Error ? err.message : String(err)}\n`);
26
+ process.stderr.write(`outlook[${acct.id}] could not remove an unsent draft: ${errMsg(err)}\n`);
26
27
  });
27
28
  }
28
29
 
29
- async function withFiles(acct: Account, target: string, verb: 'reply' | 'replyAll', comment: string, files: OutgoingFile[]): Promise<string[]> {
30
- const draft = await graphJson<{ id?: string }>(acct, `${messagePath(target)}/${verb === 'reply' ? 'createReply' : 'createReplyAll'}`, jsonInit('POST', { comment }));
31
- const draftId = draft.id;
32
- if (draftId === undefined) throw new TrainError('outlook_graph_error', 'Outlook did not open a reply draft', { retryable: true });
30
+ async function sendDraft(acct: Account, draftId: string, files: OutgoingFile[]): Promise<string[]> {
33
31
  const labels: string[] = [];
34
32
  try {
35
33
  for (const file of files) labels.push(await attachFile(acct, draftId, file));
@@ -41,6 +39,12 @@ async function withFiles(acct: Account, target: string, verb: 'reply' | 'replyAl
41
39
  return labels;
42
40
  }
43
41
 
42
+ async function withFiles(acct: Account, target: string, verb: 'reply' | 'replyAll', comment: string, files: OutgoingFile[]): Promise<string[]> {
43
+ const draft = await graphJson<{ id?: string }>(acct, `${messagePath(target)}/${verb === 'reply' ? 'createReply' : 'createReplyAll'}`, jsonInit('POST', { comment }));
44
+ if (draft.id === undefined) throw new TrainError('outlook_graph_error', 'Outlook did not open a reply draft', { retryable: true });
45
+ return sendDraft(acct, draft.id, files);
46
+ }
47
+
44
48
  async function compose(id: string, acct: Account, to: string, args: Args, text: string, files: OutgoingFile[]): Promise<void> {
45
49
  checkAddress('outlook', to);
46
50
  const draft = await graphJson<{ id?: string; conversationId?: string }>(acct, '/me/messages', jsonInit('POST', {
@@ -49,14 +53,7 @@ async function compose(id: string, acct: Account, to: string, args: Args, text:
49
53
  toRecipients: [{ emailAddress: { address: to } }],
50
54
  }));
51
55
  if (draft.id === undefined) throw new TrainError('outlook_graph_error', 'Outlook did not open a draft', { retryable: true });
52
- const labels: string[] = [];
53
- try {
54
- for (const file of files) labels.push(await attachFile(acct, draft.id, file));
55
- await graph(acct, `${messagePath(draft.id)}/send`, { method: 'POST' });
56
- } catch (err) {
57
- await discard(acct, draft.id);
58
- throw err;
59
- }
56
+ const labels = await sendDraft(acct, draft.id, files);
60
57
  const line = draft.conversationId === undefined ? undefined : mailLine('outlook', acct.id, draft.conversationId);
61
58
  respond(id, { result: { account: acct.id, messageId: draft.id, ...(line === undefined ? {} : { line }), ...(labels.length > 0 ? { attachments: labels } : {}) } });
62
59
  }
@@ -1,4 +1,5 @@
1
1
  import { TrainError } from '@metro-labs/core/train-error';
2
+ import type { GroupResult } from '@metro-labs/core/stations/types';
2
3
  import { errMsg } from '@metro-labs/core/log';
3
4
  import {
4
5
  makeStation,
@@ -204,33 +205,13 @@ function makeGroupCreate(clientFor: ClientFor): StationHandler {
204
205
  };
205
206
  }
206
207
 
207
- function makeGroupAdd(clientFor: ClientFor): StationHandler {
208
- return async (id, args) => {
209
- const { client, chatId } = resolve(args, clientFor);
210
- const line = lineOfArgs(args);
211
- respond(id, {
212
- result: await guard(() => groupAddMembers(client, chatId, line, args)),
213
- });
214
- };
215
- }
208
+ type GroupOp = (client: UserClient, chatId: number, line: string, args: Args) => Promise<GroupResult>;
216
209
 
217
- function makeGroupRemove(clientFor: ClientFor): StationHandler {
210
+ function makeGroupOp(clientFor: ClientFor, op: GroupOp): StationHandler {
218
211
  return async (id, args) => {
219
212
  const { client, chatId } = resolve(args, clientFor);
220
213
  const line = lineOfArgs(args);
221
- respond(id, {
222
- result: await guard(() => groupRemoveMembers(client, chatId, line, args)),
223
- });
224
- };
225
- }
226
-
227
- function makeGroupInvite(clientFor: ClientFor): StationHandler {
228
- return async (id, args) => {
229
- const { client, chatId } = resolve(args, clientFor);
230
- const line = lineOfArgs(args);
231
- respond(id, {
232
- result: await guard(() => groupInviteLink(client, chatId, line)),
233
- });
214
+ respond(id, { result: await guard(() => op(client, chatId, line, args)) });
234
215
  };
235
216
  }
236
217
 
@@ -264,9 +245,9 @@ export function makeHandleCall(
264
245
  listChannels: makeListChannels(clientFor),
265
246
  listMembers: makeListMembers(clientFor),
266
247
  groupCreate: makeGroupCreate(clientFor),
267
- groupAddMembers: makeGroupAdd(clientFor),
268
- groupRemoveMembers: makeGroupRemove(clientFor),
269
- groupInviteLink: makeGroupInvite(clientFor),
248
+ groupAddMembers: makeGroupOp(clientFor, groupAddMembers),
249
+ groupRemoveMembers: makeGroupOp(clientFor, groupRemoveMembers),
250
+ groupInviteLink: makeGroupOp(clientFor, groupInviteLink),
270
251
  set_profile: makeSetProfile(clientFor),
271
252
  profile: makeReadProfile(clientFor),
272
253
  },
@@ -19,7 +19,7 @@ interface MediaMeta {
19
19
  }
20
20
 
21
21
  function fallbackMime(media: DownloadableMedia): string | undefined {
22
- const type = (media as { type?: unknown }).type;
22
+ const type = mediaType(media);
23
23
  if (type === 'photo') return 'image/jpeg';
24
24
  if (type === 'sticker') return 'image/webp';
25
25
  return undefined;
@@ -1,4 +1,5 @@
1
1
  import { TelegramClient } from '@mtcute/bun';
2
+ import { isRecord } from '@metro-labs/core/is-record';
2
3
 
3
4
  export class TelegramLoginError extends Error {}
4
5
 
@@ -19,9 +20,7 @@ const PHONE_RE = /^[0-9]{6,15}$/;
19
20
  const API_HASH_RE = /^[a-f0-9]{32}$/i;
20
21
 
21
22
  export function rpcErrorText(err: unknown): string {
22
- if (typeof err !== 'object' || err === null) return '';
23
- const text = (err as { text?: unknown }).text;
24
- return typeof text === 'string' ? text : '';
23
+ return isRecord(err) && typeof err.text === 'string' ? err.text : '';
25
24
  }
26
25
 
27
26
  export function needsPassword(err: unknown): boolean {
@@ -75,9 +74,7 @@ interface SentCodeLike {
75
74
  }
76
75
 
77
76
  const isSentCode = (v: unknown): v is SentCodeLike =>
78
- typeof v === 'object' &&
79
- v !== null &&
80
- typeof (v as { phoneCodeHash?: unknown }).phoneCodeHash === 'string';
77
+ isRecord(v) && typeof v.phoneCodeHash === 'string';
81
78
 
82
79
  interface UserLike {
83
80
  id: number;
@@ -1,4 +1,5 @@
1
1
  import type { TelegramClient } from '@mtcute/bun';
2
+ import { errMsg } from '@metro-labs/core/log';
2
3
  import {
3
4
  assertImage,
4
5
  fieldsOf,
@@ -54,7 +55,7 @@ function sendersOf(client: UserClient): ProfileCache<SenderProfile> {
54
55
  ...(nonEmpty(full.bio) === undefined ? {} : { about: nonEmpty(full.bio) }),
55
56
  };
56
57
  },
57
- { onError: (user, err) => process.stderr.write(`telegram[${client.account.id}] could not read the profile of ${user}: ${err instanceof Error ? err.message : String(err)}\n`) },
58
+ { onError: (user, err) => process.stderr.write(`telegram[${client.account.id}] could not read the profile of ${user}: ${errMsg(err)}\n`) },
58
59
  );
59
60
  senders.set(client.account.id, made);
60
61
  return made;
@@ -63,7 +64,7 @@ function sendersOf(client: UserClient): ProfileCache<SenderProfile> {
63
64
  export function makeReadProfile(clientFor: (accountId: string) => UserClient): StationHandler {
64
65
  return async (id, args) => {
65
66
  const accountId = accountFor({ account: str(args.account) });
66
- const user = str(args.user) ?? '';
67
+ const user = str(args.user);
67
68
  if (!/^\d+$/.test(user)) throw new TrainError('telegram_user_required', 'profile needs the numeric Telegram id of the person', { retryable: false });
68
69
  respond(id, { result: (await sendersOf(clientFor(accountId)).get(user)) ?? { id: user } });
69
70
  };
@@ -6,19 +6,12 @@ import {
6
6
  inaccessibleMemberList,
7
7
  type TgChatMember,
8
8
  } from './members.js';
9
- import { makeStation, respond, type CallMsg, type StationHandler } from '@metro-labs/core/stations/station-runtime';
9
+ import { makeStation, respond, type StationHandler } from '@metro-labs/core/stations/station-runtime';
10
10
  import { mediaKindOf } from './attachments.js';
11
- import {
12
- emitOutbound,
13
- finishSend,
14
- MEDIA_METHOD_FIELD,
15
- sendMedia,
16
- } from './media-actions.js';
11
+ import { emitOutbound, MEDIA_METHOD_FIELD, sendMedia } from './media-actions.js';
17
12
  import { readProfile, setProfile } from './profile.js';
18
13
  import { listChannels } from './channels.js';
19
14
 
20
- export type { CallMsg };
21
-
22
15
  const meCache = new Map<string, { id: number; username: string | null }>();
23
16
  async function getMe(
24
17
  accountId: string,
@@ -70,7 +63,6 @@ interface SendArgs {
70
63
  }[];
71
64
  }
72
65
 
73
-
74
66
  type WireAttachment = NonNullable<SendArgs['attachments']>[number];
75
67
 
76
68
  const attachmentKind = (att: WireAttachment): string =>
@@ -139,7 +131,8 @@ async function send(id: string, args: Record<string, unknown>): Promise<void> {
139
131
  if (topicId !== undefined) body.message_thread_id = topicId;
140
132
  if (replyTo) body.reply_parameters = { message_id: Number(replyTo) };
141
133
  const sent = await tg<{ message_id: number }>(accountId, 'sendMessage', body);
142
- finishSend(id, accountId, line, String(sent.message_id), text, replyTo);
134
+ emitOutbound(accountId, line, String(sent.message_id), text, replyTo);
135
+ respond(id, { result: { messageId: String(sent.message_id), account: accountId } });
143
136
  }
144
137
 
145
138
  async function react(id: string, args: Record<string, unknown>): Promise<void> {
@@ -212,19 +205,12 @@ async function listMembers(
212
205
  respond(id, { result: inaccessibleMemberList(errMsg(e)) });
213
206
  return;
214
207
  }
215
- let total: number | null = null;
216
- try {
217
- total = await tg<number>(accountId, 'getChatMemberCount', {
218
- chat_id: chatId,
219
- });
220
- } catch {
221
- total = null;
222
- }
208
+ const total = await tg<number>(accountId, 'getChatMemberCount', { chat_id: chatId }).catch(() => null);
223
209
  respond(id, { result: adminMemberList(admins, total) });
224
210
  }
225
211
 
226
212
  const HANDLERS: Record<string, StationHandler> = {
227
- accounts: (id) => listAccounts(id),
213
+ accounts: listAccounts,
228
214
  send,
229
215
  react,
230
216
  typing,
@@ -1,13 +1,7 @@
1
- import {
2
- saveBufferToCache,
3
- assertContentLength,
4
- } from '@metro-labs/core/stations/attachments';
5
- import type { SavedAttachment } from '@metro-labs/core/stations/attachments';
1
+ import { assertContentLength, saveBufferToCache, type SavedAttachment } from '@metro-labs/core/stations/attachments';
6
2
  import { tg, accounts } from './accounts.js';
7
3
  import type { TgMsg } from './types.js';
8
4
 
9
- export type { SavedAttachment };
10
-
11
5
  function isOggRef(mime: string | undefined, ref: string | undefined): boolean {
12
6
  const ext = ref?.split('?')[0]?.split('.').pop()?.toLowerCase();
13
7
  return mime === 'audio/ogg' || ext === 'ogg' || ext === 'oga';
@@ -4,8 +4,6 @@ import { mintId } from '@metro-labs/core/stations/station-runtime';
4
4
  import { mediaRefOf, saveTelegramMedia } from './attachments.js';
5
5
  import type { TgMsg, TgReaction, TgReactionCount, TgUser } from './types.js';
6
6
 
7
- export type { TgMsg, TgReaction, TgReactionCount };
8
-
9
7
  function lineForMsg(
10
8
  accountId: string,
11
9
  m: TgMsg,
@@ -1,6 +1,6 @@
1
1
  import { tg, targetOf } from './accounts.js';
2
2
  import { selfUri } from '@metro-labs/core/stations/train-events';
3
- import { emit, mintId, respond } from '@metro-labs/core/stations/station-runtime';
3
+ import { emit, mintId } from '@metro-labs/core/stations/station-runtime';
4
4
  import { appendFile } from '@metro-labs/core/stations/attachments';
5
5
 
6
6
  export function emitOutbound(
@@ -25,21 +25,6 @@ export function emitOutbound(
25
25
  });
26
26
  }
27
27
 
28
- export function finishSend(
29
- id: string,
30
- accountId: string,
31
- line: string,
32
- messageId: string,
33
- label: string,
34
- replyTo?: string,
35
- extra?: Record<string, unknown>,
36
- ): void {
37
- emitOutbound(accountId, line, messageId, label, replyTo);
38
- respond(id, {
39
- result: { messageId, account: accountId, ...extra },
40
- });
41
- }
42
-
43
28
  interface MediaArgs {
44
29
  line: string;
45
30
  path: string;
@@ -1,12 +1,5 @@
1
1
  import type { MemberList, MetroMember } from '@metro-labs/core/stations/types';
2
-
3
- export interface TgUser {
4
- id: number;
5
- username?: string;
6
- first_name?: string;
7
- last_name?: string;
8
- is_bot?: boolean;
9
- }
2
+ import type { TgUser } from './types.js';
10
3
 
11
4
  export interface TgChatMember {
12
5
  user: TgUser;
@@ -1,3 +1,4 @@
1
+ import { errMsg } from '@metro-labs/core/log';
1
2
  import { appendFile } from '@metro-labs/core/stations/attachments';
2
3
  import {
3
4
  assertImage,
@@ -74,7 +75,7 @@ const senders = makeProfileCache<SenderProfile>(
74
75
  ...(avatar === undefined ? {} : { avatar }),
75
76
  };
76
77
  },
77
- { onError: (key, err) => process.stderr.write(`telegram-bot: could not read the profile of ${key}: ${err instanceof Error ? err.message : String(err)}\n`) },
78
+ { onError: (key, err) => process.stderr.write(`telegram-bot: could not read the profile of ${key}: ${errMsg(err)}\n`) },
78
79
  );
79
80
 
80
81
  export async function readProfile(id: string, args: Record<string, unknown>): Promise<void> {