@stage-labs/metro 0.1.0-beta.3 → 0.1.0-beta.31

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 (235) hide show
  1. package/dist/api.js +131 -0
  2. package/dist/bedrock-eventstream.js +120 -0
  3. package/dist/bedrock-proxy.js +260 -0
  4. package/dist/bedrock-upstream.js +72 -0
  5. package/dist/bedrock.js +106 -0
  6. package/dist/claude.js +15 -0
  7. package/dist/cli.js +164 -253
  8. package/dist/control.js +158 -0
  9. package/dist/plugin.js +82 -0
  10. package/dist/prompt.js +61 -0
  11. package/dist/runtime.js +94 -0
  12. package/dist/store.js +93 -0
  13. package/dist/tail.js +120 -0
  14. package/dist/update.js +33 -0
  15. package/dist/version.js +97 -0
  16. package/package.json +34 -21
  17. package/runtime/node_modules/@metro-labs/discord-bot/package.json +28 -0
  18. package/runtime/node_modules/@metro-labs/discord-bot/src/accounts.ts +119 -0
  19. package/runtime/node_modules/@metro-labs/discord-bot/src/actions.ts +388 -0
  20. package/runtime/node_modules/@metro-labs/discord-bot/src/api-base.ts +1 -0
  21. package/runtime/node_modules/@metro-labs/discord-bot/src/attachments.ts +29 -0
  22. package/runtime/node_modules/@metro-labs/discord-bot/src/format.ts +210 -0
  23. package/runtime/node_modules/@metro-labs/discord-bot/src/group-actions.ts +50 -0
  24. package/runtime/node_modules/@metro-labs/discord-bot/src/group.ts +115 -0
  25. package/runtime/node_modules/@metro-labs/discord-bot/src/index.ts +127 -0
  26. package/runtime/node_modules/@metro-labs/discord-bot/src/members.ts +215 -0
  27. package/runtime/node_modules/@metro-labs/discord-bot/src/send-files.ts +43 -0
  28. package/runtime/node_modules/@metro-labs/discord-bot/src/station.ts +19 -0
  29. package/runtime/node_modules/@metro-labs/discord-bot/src/verify.ts +82 -0
  30. package/runtime/node_modules/@metro-labs/discord-bot/src/voice-speak.ts +195 -0
  31. package/runtime/node_modules/@metro-labs/discord-bot/src/voice-transcribe.ts +292 -0
  32. package/runtime/node_modules/@metro-labs/discord-bot/src/voice.ts +197 -0
  33. package/runtime/node_modules/@metro-labs/discord-bot/src/wire.ts +5 -0
  34. package/runtime/node_modules/@metro-labs/mcp/package.json +66 -0
  35. package/runtime/node_modules/@metro-labs/mcp/src/channels/dedupe.ts +24 -0
  36. package/runtime/node_modules/@metro-labs/mcp/src/channels/inbound.ts +367 -0
  37. package/runtime/node_modules/@metro-labs/mcp/src/channels/media-note.ts +85 -0
  38. package/runtime/node_modules/@metro-labs/mcp/src/channels/pending.ts +75 -0
  39. package/runtime/node_modules/@metro-labs/mcp/src/channels/relay.ts +129 -0
  40. package/runtime/node_modules/@metro-labs/mcp/src/channels/webhook-note.ts +51 -0
  41. package/runtime/node_modules/@metro-labs/mcp/src/daemon/account-api.ts +311 -0
  42. package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-api.ts +332 -0
  43. package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-connector-api.ts +150 -0
  44. package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-pair.ts +20 -0
  45. package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-error.ts +8 -0
  46. package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-http.ts +152 -0
  47. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-grant.ts +99 -0
  48. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-owner.ts +32 -0
  49. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-serve.ts +129 -0
  50. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-session.ts +278 -0
  51. package/runtime/node_modules/@metro-labs/mcp/src/daemon/boot.ts +330 -0
  52. package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair-api.ts +125 -0
  53. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-api.ts +313 -0
  54. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-json.ts +21 -0
  55. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth-routes.ts +157 -0
  56. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth.ts +127 -0
  57. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-tools.ts +100 -0
  58. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-url.ts +57 -0
  59. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-verify.ts +333 -0
  60. package/runtime/node_modules/@metro-labs/mcp/src/daemon/cors.ts +29 -0
  61. package/runtime/node_modules/@metro-labs/mcp/src/daemon/crash-guard.ts +49 -0
  62. package/runtime/node_modules/@metro-labs/mcp/src/daemon/events.ts +117 -0
  63. package/runtime/node_modules/@metro-labs/mcp/src/daemon/google-auth.ts +164 -0
  64. package/runtime/node_modules/@metro-labs/mcp/src/daemon/google-oauth.ts +208 -0
  65. package/runtime/node_modules/@metro-labs/mcp/src/daemon/http.ts +359 -0
  66. package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity.ts +79 -0
  67. package/runtime/node_modules/@metro-labs/mcp/src/daemon/log.ts +42 -0
  68. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-client.ts +162 -0
  69. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-discovery.ts +120 -0
  70. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-pending.ts +65 -0
  71. package/runtime/node_modules/@metro-labs/mcp/src/daemon/pair-codes.ts +58 -0
  72. package/runtime/node_modules/@metro-labs/mcp/src/daemon/paths.ts +134 -0
  73. package/runtime/node_modules/@metro-labs/mcp/src/daemon/project-api.ts +214 -0
  74. package/runtime/node_modules/@metro-labs/mcp/src/daemon/protocol.ts +224 -0
  75. package/runtime/node_modules/@metro-labs/mcp/src/daemon/relay.ts +303 -0
  76. package/runtime/node_modules/@metro-labs/mcp/src/daemon/run-api.ts +139 -0
  77. package/runtime/node_modules/@metro-labs/mcp/src/daemon/runtime-source.ts +105 -0
  78. package/runtime/node_modules/@metro-labs/mcp/src/daemon/secure-fs.ts +57 -0
  79. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-api.ts +27 -0
  80. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-apis.ts +48 -0
  81. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session.ts +218 -0
  82. package/runtime/node_modules/@metro-labs/mcp/src/daemon/supervisor-io.ts +97 -0
  83. package/runtime/node_modules/@metro-labs/mcp/src/daemon/supervisor.ts +323 -0
  84. package/runtime/node_modules/@metro-labs/mcp/src/daemon/train-call.ts +22 -0
  85. package/runtime/node_modules/@metro-labs/mcp/src/daemon/train-error.ts +53 -0
  86. package/runtime/node_modules/@metro-labs/mcp/src/daemon/tunnel.ts +169 -0
  87. package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-api.ts +295 -0
  88. package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-store.ts +181 -0
  89. package/runtime/node_modules/@metro-labs/mcp/src/db/account-attach.ts +91 -0
  90. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-admin.ts +215 -0
  91. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-connectors.ts +143 -0
  92. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-map.ts +71 -0
  93. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-scope.ts +64 -0
  94. package/runtime/node_modules/@metro-labs/mcp/src/db/client.ts +37 -0
  95. package/runtime/node_modules/@metro-labs/mcp/src/db/connector-config.ts +147 -0
  96. package/runtime/node_modules/@metro-labs/mcp/src/db/connector-relay.ts +126 -0
  97. package/runtime/node_modules/@metro-labs/mcp/src/db/connectors.ts +308 -0
  98. package/runtime/node_modules/@metro-labs/mcp/src/db/ids.ts +19 -0
  99. package/runtime/node_modules/@metro-labs/mcp/src/db/key-map.ts +45 -0
  100. package/runtime/node_modules/@metro-labs/mcp/src/db/materialize.ts +382 -0
  101. package/runtime/node_modules/@metro-labs/mcp/src/db/projects.ts +300 -0
  102. package/runtime/node_modules/@metro-labs/mcp/src/db/runtime-admin.ts +24 -0
  103. package/runtime/node_modules/@metro-labs/mcp/src/db/runtimes.ts +105 -0
  104. package/runtime/node_modules/@metro-labs/mcp/src/db/schema.ts +117 -0
  105. package/runtime/node_modules/@metro-labs/mcp/src/db/token-carry.ts +45 -0
  106. package/runtime/node_modules/@metro-labs/mcp/src/db/users.ts +62 -0
  107. package/runtime/node_modules/@metro-labs/mcp/src/db/whatsapp-login.ts +18 -0
  108. package/runtime/node_modules/@metro-labs/mcp/src/mcp/accounts.ts +145 -0
  109. package/runtime/node_modules/@metro-labs/mcp/src/mcp/call-tools.ts +203 -0
  110. package/runtime/node_modules/@metro-labs/mcp/src/mcp/channel-owner.ts +29 -0
  111. package/runtime/node_modules/@metro-labs/mcp/src/mcp/ctx.ts +62 -0
  112. package/runtime/node_modules/@metro-labs/mcp/src/mcp/event-store.ts +91 -0
  113. package/runtime/node_modules/@metro-labs/mcp/src/mcp/frame-scope.ts +21 -0
  114. package/runtime/node_modules/@metro-labs/mcp/src/mcp/group-tools.ts +129 -0
  115. package/runtime/node_modules/@metro-labs/mcp/src/mcp/index.ts +192 -0
  116. package/runtime/node_modules/@metro-labs/mcp/src/mcp/member-tools.ts +72 -0
  117. package/runtime/node_modules/@metro-labs/mcp/src/mcp/permission-relay.ts +66 -0
  118. package/runtime/node_modules/@metro-labs/mcp/src/mcp/raw-get-stream.ts +207 -0
  119. package/runtime/node_modules/@metro-labs/mcp/src/mcp/request-identity.ts +73 -0
  120. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-registry.ts +172 -0
  121. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-route.ts +43 -0
  122. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session.ts +211 -0
  123. package/runtime/node_modules/@metro-labs/mcp/src/mcp/str.ts +5 -0
  124. package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-dispatch.ts +212 -0
  125. package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-schemas.ts +397 -0
  126. package/runtime/node_modules/@metro-labs/mcp/src/mcp/upload-tool.ts +66 -0
  127. package/runtime/node_modules/@metro-labs/mcp/src/monitor/api.ts +240 -0
  128. package/runtime/node_modules/@metro-labs/mcp/src/server.ts +2 -0
  129. package/runtime/node_modules/@metro-labs/mcp/src/stations/account-store.ts +80 -0
  130. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-inline.ts +100 -0
  131. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-interactive.ts +161 -0
  132. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-resolve.ts +200 -0
  133. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-xmtp.ts +111 -0
  134. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach.ts +182 -0
  135. package/runtime/node_modules/@metro-labs/mcp/src/stations/attachments.ts +196 -0
  136. package/runtime/node_modules/@metro-labs/mcp/src/stations/lines.ts +82 -0
  137. package/runtime/node_modules/@metro-labs/mcp/src/stations/messaging-normalize.ts +88 -0
  138. package/runtime/node_modules/@metro-labs/mcp/src/stations/registry.ts +35 -0
  139. package/runtime/node_modules/@metro-labs/mcp/src/stations/station-runtime.ts +64 -0
  140. package/runtime/node_modules/@metro-labs/mcp/src/stations/types.ts +107 -0
  141. package/runtime/node_modules/@metro-labs/telegram/package.json +28 -0
  142. package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +81 -0
  143. package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +254 -0
  144. package/runtime/node_modules/@metro-labs/telegram/src/attachments.ts +87 -0
  145. package/runtime/node_modules/@metro-labs/telegram/src/client.ts +77 -0
  146. package/runtime/node_modules/@metro-labs/telegram/src/format.ts +141 -0
  147. package/runtime/node_modules/@metro-labs/telegram/src/group.ts +261 -0
  148. package/runtime/node_modules/@metro-labs/telegram/src/history.ts +52 -0
  149. package/runtime/node_modules/@metro-labs/telegram/src/inbound.ts +78 -0
  150. package/runtime/node_modules/@metro-labs/telegram/src/index.ts +46 -0
  151. package/runtime/node_modules/@metro-labs/telegram/src/login.ts +166 -0
  152. package/runtime/node_modules/@metro-labs/telegram/src/media-actions.ts +80 -0
  153. package/runtime/node_modules/@metro-labs/telegram/src/members.ts +75 -0
  154. package/runtime/node_modules/@metro-labs/telegram/src/normalize.ts +22 -0
  155. package/runtime/node_modules/@metro-labs/telegram/src/reactions.ts +167 -0
  156. package/runtime/node_modules/@metro-labs/telegram/src/station.ts +24 -0
  157. package/runtime/node_modules/@metro-labs/telegram/src/types.ts +7 -0
  158. package/runtime/node_modules/@metro-labs/telegram/src/wire.ts +1 -0
  159. package/runtime/node_modules/@metro-labs/telegram-bot/package.json +25 -0
  160. package/runtime/node_modules/@metro-labs/telegram-bot/src/accounts.ts +120 -0
  161. package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +295 -0
  162. package/runtime/node_modules/@metro-labs/telegram-bot/src/attachments.ts +128 -0
  163. package/runtime/node_modules/@metro-labs/telegram-bot/src/format.ts +172 -0
  164. package/runtime/node_modules/@metro-labs/telegram-bot/src/index.ts +103 -0
  165. package/runtime/node_modules/@metro-labs/telegram-bot/src/media-actions.ts +166 -0
  166. package/runtime/node_modules/@metro-labs/telegram-bot/src/members.ts +47 -0
  167. package/runtime/node_modules/@metro-labs/telegram-bot/src/station.ts +17 -0
  168. package/runtime/node_modules/@metro-labs/telegram-bot/src/types.ts +48 -0
  169. package/runtime/node_modules/@metro-labs/telegram-bot/src/verify.ts +38 -0
  170. package/runtime/node_modules/@metro-labs/telegram-bot/src/wire.ts +3 -0
  171. package/runtime/node_modules/@metro-labs/webhook/package.json +23 -0
  172. package/runtime/node_modules/@metro-labs/webhook/src/station.ts +40 -0
  173. package/runtime/node_modules/@metro-labs/whatsapp/package.json +29 -0
  174. package/runtime/node_modules/@metro-labs/whatsapp/src/accounts.ts +67 -0
  175. package/runtime/node_modules/@metro-labs/whatsapp/src/ack.ts +101 -0
  176. package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +178 -0
  177. package/runtime/node_modules/@metro-labs/whatsapp/src/attachments.ts +72 -0
  178. package/runtime/node_modules/@metro-labs/whatsapp/src/auth-state.ts +129 -0
  179. package/runtime/node_modules/@metro-labs/whatsapp/src/client.ts +318 -0
  180. package/runtime/node_modules/@metro-labs/whatsapp/src/delivery.ts +49 -0
  181. package/runtime/node_modules/@metro-labs/whatsapp/src/format.ts +132 -0
  182. package/runtime/node_modules/@metro-labs/whatsapp/src/inbound.ts +57 -0
  183. package/runtime/node_modules/@metro-labs/whatsapp/src/index.ts +40 -0
  184. package/runtime/node_modules/@metro-labs/whatsapp/src/keys.ts +59 -0
  185. package/runtime/node_modules/@metro-labs/whatsapp/src/logger.ts +60 -0
  186. package/runtime/node_modules/@metro-labs/whatsapp/src/login.ts +176 -0
  187. package/runtime/node_modules/@metro-labs/whatsapp/src/media.ts +84 -0
  188. package/runtime/node_modules/@metro-labs/whatsapp/src/normalize.ts +22 -0
  189. package/runtime/node_modules/@metro-labs/whatsapp/src/outbox.ts +38 -0
  190. package/runtime/node_modules/@metro-labs/whatsapp/src/parse.ts +141 -0
  191. package/runtime/node_modules/@metro-labs/whatsapp/src/station.ts +17 -0
  192. package/runtime/node_modules/@metro-labs/whatsapp/src/token-store.ts +98 -0
  193. package/runtime/node_modules/@metro-labs/whatsapp/src/types.ts +6 -0
  194. package/runtime/node_modules/@metro-labs/whatsapp/src/wire.ts +5 -0
  195. package/runtime/node_modules/@metro-labs/xmtp/package.json +33 -0
  196. package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +113 -0
  197. package/runtime/node_modules/@metro-labs/xmtp/src/actions-close.ts +110 -0
  198. package/runtime/node_modules/@metro-labs/xmtp/src/actions-conv.ts +299 -0
  199. package/runtime/node_modules/@metro-labs/xmtp/src/actions-meta.ts +157 -0
  200. package/runtime/node_modules/@metro-labs/xmtp/src/actions-push.ts +87 -0
  201. package/runtime/node_modules/@metro-labs/xmtp/src/actions.ts +375 -0
  202. package/runtime/node_modules/@metro-labs/xmtp/src/attachments.ts +107 -0
  203. package/runtime/node_modules/@metro-labs/xmtp/src/codecs.ts +171 -0
  204. package/runtime/node_modules/@metro-labs/xmtp/src/conv-helpers.ts +239 -0
  205. package/runtime/node_modules/@metro-labs/xmtp/src/emit-core.ts +55 -0
  206. package/runtime/node_modules/@metro-labs/xmtp/src/emit-payloads.ts +333 -0
  207. package/runtime/node_modules/@metro-labs/xmtp/src/emit.ts +78 -0
  208. package/runtime/node_modules/@metro-labs/xmtp/src/group-members.ts +34 -0
  209. package/runtime/node_modules/@metro-labs/xmtp/src/group.ts +56 -0
  210. package/runtime/node_modules/@metro-labs/xmtp/src/identity.ts +33 -0
  211. package/runtime/node_modules/@metro-labs/xmtp/src/index.ts +125 -0
  212. package/runtime/node_modules/@metro-labs/xmtp/src/labels.ts +157 -0
  213. package/runtime/node_modules/@metro-labs/xmtp/src/member-args.ts +40 -0
  214. package/runtime/node_modules/@metro-labs/xmtp/src/push.ts +349 -0
  215. package/runtime/node_modules/@metro-labs/xmtp/src/station.ts +13 -0
  216. package/runtime/node_modules/@metro-labs/xmtp/src/tools-handlers.ts +126 -0
  217. package/runtime/node_modules/@metro-labs/xmtp/src/tools.ts +212 -0
  218. package/runtime/node_modules/@metro-labs/xmtp/src/transcribe.ts +90 -0
  219. package/runtime/node_modules/@metro-labs/xmtp/src/verify.ts +84 -0
  220. package/runtime/node_modules/@metro-labs/xmtp/src/wire.ts +39 -0
  221. package/runtime/runtime.json +3 -0
  222. package/runtime/trains/.keep +0 -0
  223. package/LICENSE +0 -21
  224. package/README.md +0 -114
  225. package/dist/agents/claude.js +0 -229
  226. package/dist/agents/codex.js +0 -282
  227. package/dist/agents/types.js +0 -3
  228. package/dist/channels/discord.js +0 -142
  229. package/dist/channels/telegram.js +0 -209
  230. package/dist/lib/dotenv.js +0 -31
  231. package/dist/lib/scope-cache.js +0 -85
  232. package/dist/lib/streaming.js +0 -207
  233. package/dist/log.js +0 -12
  234. package/dist/orchestrator.js +0 -399
  235. package/dist/paths.js +0 -33
@@ -1,282 +0,0 @@
1
- // Codex agent adapter. Spawns `codex app-server --listen unix://…` as a
2
- // child process, talks to it over WebSocket-over-UDS, manages one codex
3
- // thread per scope, and streams per-turn events back to the orchestrator
4
- // (text deltas, tool-call lifecycle, completion).
5
- //
6
- // The orchestrator never speaks codex directly — it asks this adapter to
7
- // `ensureThread(scopeKey)` and `sendTurn(threadId, text, callbacks)`.
8
- import { spawn } from 'node:child_process';
9
- import { existsSync, unlinkSync } from 'node:fs';
10
- import { createConnection } from 'node:net';
11
- import { join } from 'node:path';
12
- import { WebSocket } from 'ws';
13
- import { errMsg, log } from '../log.js';
14
- import { STATE_DIR } from '../paths.js';
15
- const SOCKET_PATH = join(STATE_DIR, 'codex-app-server.sock');
16
- const READY_TIMEOUT_MS = 15_000;
17
- const READY_POLL_MS = 100;
18
- export class CodexAgent {
19
- clientVersion;
20
- ws = null;
21
- daemon = null;
22
- nextId = 1;
23
- pending = new Map();
24
- turnCallbacks = new Map(); // keyed by thread_id
25
- constructor(clientVersion) {
26
- this.clientVersion = clientVersion;
27
- }
28
- async start() {
29
- // Fail loud at boot if codex isn't installed, rather than 15s later
30
- // via a "socket didn't appear" timeout.
31
- await this.checkCodexInstalled();
32
- // Stale socket file from a previous run would let us connect to nothing
33
- // (no listener) — wipe before spawning so codex binds fresh.
34
- try {
35
- unlinkSync(SOCKET_PATH);
36
- }
37
- catch { /* missing is fine */ }
38
- log.info({ socket: SOCKET_PATH }, 'codex agent: starting app-server');
39
- // Spawn the daemon listening on our UDS. Inherits CODEX_HOME so it picks
40
- // up the user's auth, settings, MCPs, etc.
41
- this.daemon = spawn('codex', ['app-server', '--listen', `unix://${SOCKET_PATH}`], {
42
- stdio: ['ignore', 'pipe', 'pipe'],
43
- });
44
- // Capture daemon stderr so a startup failure surfaces with the actual
45
- // error message, not just a generic timeout. Once the daemon is ready,
46
- // stderr goes back to trace (its own tracing is noisy).
47
- let bootStderr = '';
48
- let daemonExited = false;
49
- let daemonExitCode = null;
50
- const onBootStderr = (d) => {
51
- bootStderr += String(d);
52
- log.trace({ src: 'codex-stderr' }, String(d).trim());
53
- };
54
- this.daemon.stdout?.on('data', d => log.trace({ src: 'codex-stdout' }, String(d).trim()));
55
- this.daemon.stderr?.on('data', onBootStderr);
56
- this.daemon.on('exit', code => {
57
- daemonExited = true;
58
- daemonExitCode = code;
59
- log.warn({ code }, 'codex daemon exited');
60
- });
61
- try {
62
- await this.waitForSocket(() => daemonExited, () => bootStderr.trim() || `exit code ${daemonExitCode}`);
63
- await this.connect();
64
- }
65
- catch (err) {
66
- // Make the failure self-explanatory in the orchestrator's catch log.
67
- const detail = bootStderr.trim() ? ` (codex stderr: ${bootStderr.trim().slice(0, 200)})` : '';
68
- throw new Error(`${errMsg(err)}${detail}`);
69
- }
70
- log.info('codex agent: ready');
71
- }
72
- async checkCodexInstalled() {
73
- await new Promise((resolve, reject) => {
74
- const c = spawn('codex', ['--version'], { stdio: ['ignore', 'ignore', 'pipe'] });
75
- let stderr = '';
76
- c.stderr?.on('data', d => { stderr += String(d); });
77
- c.on('error', err => reject(new Error(`codex CLI not found on PATH: ${errMsg(err)}`)));
78
- c.on('exit', code => code === 0
79
- ? resolve()
80
- : reject(new Error(`codex --version exited ${code}: ${stderr.trim()}`)));
81
- });
82
- }
83
- async stop() {
84
- this.ws?.close();
85
- this.ws = null;
86
- this.daemon?.kill('SIGTERM');
87
- this.daemon = null;
88
- }
89
- /**
90
- * Create a new codex thread and return its id. The caller is responsible
91
- * for caching the mapping `scopeKey → threadId` and only calling this
92
- * once per scope (subsequent inbounds reuse the thread via `sendTurn`).
93
- */
94
- async createThread() {
95
- const result = await this.call('thread/start', {});
96
- log.info({ thread: result.thread.id }, 'codex agent: thread created');
97
- return result.thread.id;
98
- }
99
- /**
100
- * Send a user message to a thread and stream the agent's response via
101
- * the provided callbacks. Resolves immediately after `turn/start`
102
- * acknowledges; callbacks fire as notifications arrive and conclude
103
- * with `onComplete` or `onError`.
104
- */
105
- async sendTurn(threadId, text, callbacks) {
106
- this.turnCallbacks.set(threadId, callbacks);
107
- try {
108
- // Wire format per codex's generated TS bindings: `text_elements`
109
- // (snake_case), not camelCase. Sending camelCase silently degrades
110
- // — accepted by the server but doesn't echo back in items.
111
- await this.call('turn/start', {
112
- threadId,
113
- input: [{ type: 'text', text, text_elements: [] }],
114
- });
115
- }
116
- catch (err) {
117
- this.turnCallbacks.delete(threadId);
118
- callbacks.onError(err instanceof Error ? err : new Error(String(err)));
119
- }
120
- }
121
- // --- transport ---
122
- async waitForSocket(exited, exitReason) {
123
- const deadline = Date.now() + READY_TIMEOUT_MS;
124
- while (Date.now() < deadline) {
125
- if (exited())
126
- throw new Error(`codex app-server exited before listening: ${exitReason()}`);
127
- if (existsSync(SOCKET_PATH))
128
- return;
129
- await new Promise(r => setTimeout(r, READY_POLL_MS));
130
- }
131
- throw new Error(`codex app-server didn't appear at ${SOCKET_PATH} within ${READY_TIMEOUT_MS}ms`);
132
- }
133
- async connect() {
134
- // perMessageDeflate disabled: codex 0.130's WS upgrade handler closes
135
- // the connection if a client offers `Sec-WebSocket-Extensions:
136
- // permessage-deflate` (the `ws` library's default). Disabling it makes
137
- // the upgrade succeed cleanly. Compression is irrelevant over a UDS.
138
- const ws = new WebSocket('ws://localhost/', {
139
- perMessageDeflate: false,
140
- createConnection: () => {
141
- const sock = createConnection({ path: SOCKET_PATH });
142
- // Swallow socket-level errors so a transport hiccup during the HTTP
143
- // upgrade doesn't surface as an uncaught error on the http_client
144
- // request (Node's http.ClientRequest re-emits this).
145
- sock.on('error', err => log.warn({ err: errMsg(err) }, 'codex agent: socket error'));
146
- return sock;
147
- },
148
- });
149
- this.ws = ws;
150
- ws.on('error', err => log.warn({ err: errMsg(err) }, 'codex agent: websocket error'));
151
- await new Promise((resolve, reject) => {
152
- ws.once('open', () => resolve());
153
- ws.once('error', err => reject(err));
154
- });
155
- ws.on('message', data => this.onMessage(data));
156
- ws.on('close', () => {
157
- log.warn('codex agent: websocket closed');
158
- this.ws = null;
159
- // Drain stranded turns + RPC promises so the orchestrator can
160
- // release its in-flight gate and the user sees an error rather
161
- // than a permanent "Thinking…".
162
- this.drainPending('codex websocket closed');
163
- });
164
- await this.call('initialize', {
165
- clientInfo: { name: 'metro', version: this.clientVersion, title: null },
166
- });
167
- }
168
- onMessage(raw) {
169
- let msg;
170
- try {
171
- msg = JSON.parse(raw.toString());
172
- }
173
- catch (err) {
174
- log.warn({ err: errMsg(err) }, 'codex agent: malformed message');
175
- return;
176
- }
177
- // RPC response
178
- if (msg.id !== undefined && this.pending.has(msg.id)) {
179
- const p = this.pending.get(msg.id);
180
- this.pending.delete(msg.id);
181
- if (msg.error)
182
- p.reject(new Error(msg.error.message ?? 'rpc error'));
183
- else
184
- p.resolve(msg.result);
185
- return;
186
- }
187
- // Notifications.
188
- if (!msg.method)
189
- return;
190
- log.trace({ method: msg.method }, 'codex agent: notification');
191
- const params = msg.params;
192
- const threadId = params?.threadId;
193
- if (!threadId)
194
- return;
195
- const cb = this.turnCallbacks.get(threadId);
196
- if (!cb)
197
- return; // no active turn for this thread (yet or anymore)
198
- if (msg.method === 'item/agentMessage/delta') {
199
- const p = msg.params;
200
- cb.onDelta(p.delta);
201
- }
202
- else if (msg.method === 'item/started') {
203
- const p = msg.params;
204
- const summary = summarizeItem(p.item);
205
- if (summary && p.item.type !== 'agentMessage' && p.item.type !== 'userMessage') {
206
- cb.onToolStart(p.item.type, summary);
207
- }
208
- }
209
- else if (msg.method === 'item/completed') {
210
- const p = msg.params;
211
- if (p.item.type !== 'agentMessage' && p.item.type !== 'userMessage')
212
- cb.onToolEnd(p.item.type);
213
- }
214
- else if (msg.method === 'thread/status/changed') {
215
- // Codex 0.130 doesn't reliably emit `turn/completed` — `thread/status`
216
- // returning to `idle` is the dependable completion signal. The same
217
- // notification fires on `active` when a turn starts; ignore those.
218
- const p = msg.params;
219
- if (p.status?.type === 'idle') {
220
- this.turnCallbacks.delete(threadId);
221
- cb.onComplete();
222
- }
223
- else if (p.status?.type === 'systemError') {
224
- this.turnCallbacks.delete(threadId);
225
- cb.onError(new Error('codex thread entered systemError'));
226
- }
227
- }
228
- else if (msg.method === 'turn/completed') {
229
- // Belt-and-braces: if codex does emit it, take it.
230
- this.turnCallbacks.delete(threadId);
231
- cb.onComplete();
232
- }
233
- else if (msg.method === 'error') {
234
- const p = msg.params;
235
- this.turnCallbacks.delete(threadId);
236
- cb.onError(new Error(p.error?.message ?? 'codex error notification'));
237
- }
238
- }
239
- call(method, params) {
240
- if (!this.ws)
241
- return Promise.reject(new Error('codex agent: not connected'));
242
- const id = this.nextId++;
243
- const ws = this.ws;
244
- return new Promise((resolve, reject) => {
245
- this.pending.set(id, { resolve: resolve, reject });
246
- ws.send(JSON.stringify({ jsonrpc: '2.0', id, method, params }));
247
- });
248
- }
249
- /** Fail every in-flight turn + RPC. Used when the transport dies. */
250
- drainPending(reason) {
251
- const err = new Error(reason);
252
- for (const cb of this.turnCallbacks.values()) {
253
- try {
254
- cb.onError(err);
255
- }
256
- catch (e) {
257
- log.warn({ err: errMsg(e) }, 'codex agent: drain callback threw');
258
- }
259
- }
260
- this.turnCallbacks.clear();
261
- for (const p of this.pending.values())
262
- p.reject(err);
263
- this.pending.clear();
264
- }
265
- }
266
- function summarizeItem(item) {
267
- if (item.type === 'commandExecution' && 'command' in item) {
268
- return `Running: ${truncate(item.command ?? '', 60)}`;
269
- }
270
- if (item.type === 'fileChange' && 'changes' in item) {
271
- const n = item.changes?.length ?? 0;
272
- return n > 0 ? `Editing ${n} file${n === 1 ? '' : 's'}` : 'Editing files';
273
- }
274
- if (item.type === 'reasoning')
275
- return 'Thinking…';
276
- if (item.type === 'agentMessage')
277
- return ''; // text deltas handled separately
278
- return item.type;
279
- }
280
- function truncate(s, n) {
281
- return s.length > n ? s.slice(0, n - 1) + '…' : s;
282
- }
@@ -1,3 +0,0 @@
1
- // Shared interface so the orchestrator can talk to either Codex or Claude
2
- // Code (or future agents) through the same surface.
3
- export {};
@@ -1,142 +0,0 @@
1
- import { Client, Events, GatewayIntentBits, Partials } from 'discord.js';
2
- import { errMsg, log } from '../log.js';
3
- // Receive path: discord.js gateway, owned by the orchestrator.
4
- // Send path: raw REST against discord.com/api — no gateway login required,
5
- // so callers outside the orchestrator (e.g. cli.ts validation) stay one-shot.
6
- const API_BASE = 'https://discord.com/api/v10';
7
- function token() {
8
- const t = process.env.DISCORD_BOT_TOKEN;
9
- if (!t)
10
- throw new Error('DISCORD_BOT_TOKEN is not set');
11
- return t;
12
- }
13
- async function rest(method, path, body, timeoutMs = 30_000, retriesLeft = 2) {
14
- const res = await fetch(`${API_BASE}${path}`, {
15
- method,
16
- headers: {
17
- 'Authorization': `Bot ${token()}`,
18
- 'User-Agent': 'metro (https://github.com/bonustrack/metro, dev)',
19
- ...(body !== undefined ? { 'Content-Type': 'application/json' } : {}),
20
- },
21
- body: body !== undefined ? JSON.stringify(body) : undefined,
22
- signal: AbortSignal.timeout(timeoutMs),
23
- });
24
- // 429: honor Discord's retry_after (seconds) and try again, up to a few
25
- // hops. Common during streaming edits when the agent emits deltas faster
26
- // than the per-channel edit cap.
27
- if (res.status === 429 && retriesLeft > 0) {
28
- const retryAfter = Number(res.headers.get('retry-after')) || 1;
29
- log.debug({ path, retryAfter }, 'discord 429; backing off');
30
- await new Promise(r => setTimeout(r, Math.max(retryAfter * 1000, 250)));
31
- return rest(method, path, body, timeoutMs, retriesLeft - 1);
32
- }
33
- if (!res.ok) {
34
- const text = await res.text().catch(() => '');
35
- throw new Error(`discord ${method} ${path}: ${res.status} ${text}`);
36
- }
37
- // 204 No Content for typing/reactions/clear.
38
- if (res.status === 204)
39
- return undefined;
40
- return (await res.json());
41
- }
42
- // ---------- Receive path (gateway, discord.js) -----------------------------
43
- let client = null;
44
- function getClient() {
45
- if (client)
46
- return client;
47
- client = new Client({
48
- intents: [
49
- GatewayIntentBits.DirectMessages,
50
- GatewayIntentBits.Guilds,
51
- GatewayIntentBits.GuildMessages,
52
- GatewayIntentBits.MessageContent,
53
- ],
54
- // DM channels arrive partial; without this messageCreate never fires.
55
- partials: [Partials.Channel],
56
- });
57
- return client;
58
- }
59
- let onInboundHandler = () => { };
60
- export function onInbound(handler) {
61
- onInboundHandler = handler;
62
- }
63
- export async function shutdownGateway() {
64
- if (!client)
65
- return;
66
- await client.destroy();
67
- client = null;
68
- }
69
- export async function startGateway() {
70
- const c = getClient();
71
- c.on(Events.MessageCreate, m => {
72
- if (m.author.bot)
73
- return;
74
- // Forward every human message; the orchestrator decides what to route
75
- // where (scoped thread vs new bootstrap mention vs ignore). The bot's
76
- // own @-mention is preserved in `m.content` so the orchestrator can
77
- // make that decision from the same payload.
78
- const tags = [...m.attachments.values()]
79
- .map(a => {
80
- if (a.contentType?.startsWith('image/'))
81
- return '[image]';
82
- if (a.contentType?.startsWith('audio/'))
83
- return `[audio: ${a.name}]`;
84
- return `[file: ${a.name}]`;
85
- })
86
- .join(' ');
87
- const text = [m.content, tags].filter(Boolean).join(' ').trim();
88
- if (!text)
89
- return;
90
- onInboundHandler({
91
- channel_id: m.channelId,
92
- message_id: m.id,
93
- text,
94
- in_guild: !!m.guildId,
95
- mentions_bot: c.user ? m.mentions.has(c.user.id) : false,
96
- });
97
- });
98
- c.on(Events.Error, err => log.error({ err: errMsg(err) }, 'discord error'));
99
- await c.login(process.env.DISCORD_BOT_TOKEN);
100
- await new Promise(r => c.once(Events.ClientReady, () => r()));
101
- }
102
- export async function getMe() {
103
- const me = await rest('GET', '/users/@me');
104
- return { username: me.username };
105
- }
106
- export async function sendMessage(channelId, text) {
107
- const sent = await rest('POST', `/channels/${channelId}/messages`, { content: text });
108
- return sent.id;
109
- }
110
- /**
111
- * Create a public thread anchored to an existing message. The user's
112
- * @-mention becomes the thread's starter, so the thread is visually
113
- * tied to the request that opened it. Returns the new thread's channel id.
114
- */
115
- export async function createThreadFromMessage(channelId, messageId, name) {
116
- const created = await rest('POST', `/channels/${channelId}/messages/${messageId}/threads`, {
117
- name,
118
- auto_archive_duration: 1440,
119
- });
120
- return created.id;
121
- }
122
- export async function editMessage(channelId, messageId, text) {
123
- const sent = await rest('PATCH', `/channels/${channelId}/messages/${messageId}`, { content: text });
124
- return sent.id;
125
- }
126
- /**
127
- * Fetch all messages newer than `afterMessageId` in a channel. Used for
128
- * catchup-on-restart so messages sent while metro was down still get a
129
- * reply once it comes back. Caps at 100 messages — anything older than
130
- * that during downtime is dropped on the floor.
131
- */
132
- export async function fetchMessagesSince(channelId, afterMessageId) {
133
- const msgs = await rest('GET', `/channels/${channelId}/messages?after=${afterMessageId}&limit=100`);
134
- // Discord returns newest-first; flip to chronological so replay order
135
- // matches what the user actually typed.
136
- return [...msgs].reverse().map(m => ({
137
- message_id: m.id,
138
- text: m.content,
139
- author_id: m.author.id,
140
- author_is_bot: !!m.author.bot,
141
- }));
142
- }
@@ -1,209 +0,0 @@
1
- import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
- import { join } from 'node:path';
3
- import { STATE_DIR } from '../paths.js';
4
- import { errMsg, log } from '../log.js';
5
- const API_BASE = 'https://api.telegram.org';
6
- function token() {
7
- const t = process.env.TELEGRAM_BOT_TOKEN;
8
- if (!t)
9
- throw new Error('TELEGRAM_BOT_TOKEN is not set');
10
- return t;
11
- }
12
- async function tg(method, body, opts = {}) {
13
- const timeout = opts.timeoutMs ?? 30_000;
14
- const signals = [AbortSignal.timeout(timeout)];
15
- if (opts.signal)
16
- signals.push(opts.signal);
17
- const signal = AbortSignal.any(signals);
18
- const res = await fetch(`${API_BASE}/bot${token()}/${method}`, {
19
- method: 'POST',
20
- headers: { 'Content-Type': 'application/json' },
21
- body: JSON.stringify(body),
22
- signal,
23
- });
24
- const json = (await res.json());
25
- if (!json.ok)
26
- throw new Error(`telegram ${method}: ${json.description ?? 'unknown error'}`);
27
- return json.result;
28
- }
29
- // ---------- Identity (cached after getMe) ----------------------------------
30
- let botUsername = null;
31
- let botUserId = null;
32
- export async function getMe() {
33
- const me = await tg('getMe', {});
34
- botUsername = me.username;
35
- botUserId = me.id;
36
- return me;
37
- }
38
- let onInboundHandler = () => { };
39
- export function onInbound(handler) {
40
- onInboundHandler = handler;
41
- }
42
- const offsetFile = join(STATE_DIR, 'telegram-offset.json');
43
- let pollOffset = 0;
44
- let pollAbort = null;
45
- function loadOffset() {
46
- try {
47
- if (!existsSync(offsetFile))
48
- return 0;
49
- return Number(readFileSync(offsetFile, 'utf8').trim()) || 0;
50
- }
51
- catch {
52
- return 0;
53
- }
54
- }
55
- function saveOffset(o) {
56
- try {
57
- writeFileSync(offsetFile, String(o));
58
- }
59
- catch (err) {
60
- log.warn({ err: errMsg(err) }, 'telegram offset save failed');
61
- }
62
- }
63
- export async function startPolling() {
64
- // A registered webhook short-circuits getUpdates — clear it defensively.
65
- await tg('deleteWebhook', { drop_pending_updates: false }).catch(() => { });
66
- const persisted = loadOffset();
67
- if (persisted > 0) {
68
- // Resume from where we left off — telegram queues updates for ~24h, so
69
- // any messages sent while metro was down come back through this poll.
70
- pollOffset = persisted;
71
- log.info({ offset: pollOffset }, 'telegram polling: resuming from persisted offset');
72
- }
73
- else {
74
- // First-ever run: skip historical backlog by anchoring on the latest
75
- // update id (-1 returns just the most recent one without consuming it).
76
- const initial = await tg('getUpdates', { offset: -1, timeout: 0 });
77
- pollOffset = initial.length ? initial[0].update_id + 1 : 0;
78
- saveOffset(pollOffset);
79
- log.info({ offset: pollOffset }, 'telegram polling: starting fresh');
80
- }
81
- pollAbort = new AbortController();
82
- void pollLoop();
83
- }
84
- async function pollLoop() {
85
- const abortSignal = pollAbort?.signal;
86
- while (pollAbort && !pollAbort.signal.aborted) {
87
- try {
88
- const updates = await tg('getUpdates', { offset: pollOffset, timeout: 25 }, { timeoutMs: 60_000, signal: abortSignal });
89
- for (const u of updates) {
90
- pollOffset = u.update_id + 1;
91
- await dispatchUpdate(u);
92
- }
93
- if (updates.length)
94
- saveOffset(pollOffset);
95
- }
96
- catch (err) {
97
- if (pollAbort?.signal.aborted)
98
- break;
99
- log.warn({ err: errMsg(err) }, 'telegram poll error; backing off');
100
- await new Promise(r => setTimeout(r, 2000));
101
- }
102
- }
103
- log.info('telegram polling stopped');
104
- }
105
- export async function shutdownPolling() {
106
- pollAbort?.abort();
107
- pollAbort = null;
108
- }
109
- async function dispatchUpdate(u) {
110
- const m = u.message;
111
- if (!m?.chat?.id || typeof m.message_id !== 'number') {
112
- log.trace({ update_id: u.update_id }, 'telegram: non-message update');
113
- return;
114
- }
115
- if (m.from?.is_bot) {
116
- log.trace({ chat: m.chat.id }, 'telegram: skipping bot author');
117
- return;
118
- }
119
- const text = await messageToText(m);
120
- if (!text) {
121
- log.trace({ chat: m.chat.id }, 'telegram: no text/caption');
122
- return;
123
- }
124
- const msg = {
125
- chat_id: m.chat.id,
126
- message_id: m.message_id,
127
- message_thread_id: m.is_topic_message ? m.message_thread_id : undefined,
128
- text,
129
- is_private: m.chat.type === 'private',
130
- is_forum_topic: !!m.is_topic_message,
131
- in_forum: !!m.chat.is_forum,
132
- mentions_bot: detectMentionsBot(m),
133
- };
134
- log.debug({
135
- chat: msg.chat_id,
136
- topic: msg.message_thread_id,
137
- is_private: msg.is_private,
138
- is_forum_topic: msg.is_forum_topic,
139
- in_forum: msg.in_forum,
140
- mentions_bot: msg.mentions_bot,
141
- }, 'telegram: inbound');
142
- onInboundHandler(msg);
143
- }
144
- function detectMentionsBot(m) {
145
- // In DMs, every message to the bot is implicitly addressed to it.
146
- if (m.chat?.type === 'private')
147
- return true;
148
- const text = m.text ?? m.caption ?? '';
149
- const entities = m.entities ?? m.caption_entities ?? [];
150
- for (const e of entities) {
151
- if (e.type === 'mention' && botUsername) {
152
- const slice = text.substring(e.offset, e.offset + e.length);
153
- if (slice.toLowerCase() === `@${botUsername.toLowerCase()}`)
154
- return true;
155
- }
156
- else if (e.type === 'text_mention' && e.user?.id === botUserId) {
157
- return true;
158
- }
159
- }
160
- return false;
161
- }
162
- async function messageToText(m) {
163
- if (m.text)
164
- return m.text;
165
- const caption = m.caption ?? '';
166
- if (m.photo?.length)
167
- return [caption, '[image]'].filter(Boolean).join(' ');
168
- if (m.document?.mime_type?.startsWith('image/'))
169
- return [caption, '[image]'].filter(Boolean).join(' ');
170
- if (m.voice)
171
- return [caption, '[voice]'].filter(Boolean).join(' ');
172
- if (m.audio)
173
- return [caption, '[audio]'].filter(Boolean).join(' ');
174
- return caption || null;
175
- }
176
- // ---------- Outbound (REST) ------------------------------------------------
177
- /**
178
- * Create a new forum topic in a supergroup. Returns the topic's
179
- * message_thread_id. Requires the bot to be an admin with the
180
- * `can_manage_topics` privilege.
181
- */
182
- export async function createForumTopic(chatId, name) {
183
- // Telegram caps topic names at 128 chars.
184
- const trimmedName = name.slice(0, 128) || 'metro';
185
- const r = await tg('createForumTopic', {
186
- chat_id: chatId,
187
- name: trimmedName,
188
- });
189
- return r.message_thread_id;
190
- }
191
- export async function sendMessage(chatId, threadId, text) {
192
- const body = { chat_id: chatId, text };
193
- if (threadId !== undefined)
194
- body.message_thread_id = threadId;
195
- const r = await tg('sendMessage', body);
196
- return r.message_id;
197
- }
198
- export async function editMessageText(chatId, messageId, text) {
199
- try {
200
- await tg('editMessageText', { chat_id: chatId, message_id: messageId, text });
201
- }
202
- catch (err) {
203
- // Telegram rejects edits that match the existing content — ignore that
204
- // specific case so debounced no-op flushes don't surface as errors.
205
- if (errMsg(err).includes('message is not modified'))
206
- return;
207
- throw err;
208
- }
209
- }
@@ -1,31 +0,0 @@
1
- // Tiny .env reader/writer. Used by `metro setup` (read/write the global
2
- // config file) and by paths.ts (load env vars into process.env at startup).
3
- import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
4
- import { dirname } from 'node:path';
5
- const LINE_RE = /^\s*([A-Za-z_]\w*)\s*=\s*(.*?)\s*$/;
6
- const QUOTED_RE = /^(['"])(.*)\1$/;
7
- export function readDotenv(path) {
8
- if (!existsSync(path))
9
- return {};
10
- const out = {};
11
- for (const line of readFileSync(path, 'utf8').split('\n')) {
12
- const m = line.match(LINE_RE);
13
- if (m)
14
- out[m[1]] = m[2].replace(QUOTED_RE, '$2');
15
- }
16
- return out;
17
- }
18
- export function writeDotenv(path, env) {
19
- mkdirSync(dirname(path), { recursive: true });
20
- writeFileSync(path, Object.entries(env).map(([k, v]) => `${k}=${v}`).join('\n') + '\n');
21
- chmodSync(path, 0o600);
22
- }
23
- // Load .env at `path` into process.env, but only for keys that aren't already
24
- // set — first definer wins, so callers control precedence by the order they
25
- // invoke this.
26
- export function loadDotenvIntoProcess(path) {
27
- for (const [k, v] of Object.entries(readDotenv(path))) {
28
- if (process.env[k] === undefined)
29
- process.env[k] = v;
30
- }
31
- }