@stage-labs/metro 0.1.0-beta.8 → 0.1.0-beta.80

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 (282) hide show
  1. package/dist/api.js +14 -0
  2. package/dist/claude-settings.js +40 -0
  3. package/dist/claude.js +107 -0
  4. package/dist/cli.js +101 -0
  5. package/dist/control.js +74 -0
  6. package/dist/hold.js +211 -0
  7. package/dist/local.js +93 -0
  8. package/dist/node-name.js +71 -0
  9. package/dist/plugin.js +80 -0
  10. package/dist/provider-flags.js +9 -0
  11. package/dist/runtime-install.js +87 -0
  12. package/dist/runtime.js +74 -0
  13. package/dist/serve.js +146 -0
  14. package/dist/service.js +242 -0
  15. package/dist/tail.js +119 -0
  16. package/dist/update.js +45 -0
  17. package/dist/version.js +97 -0
  18. package/package.json +14 -25
  19. package/runtime/node_modules/@metro-labs/core/package.json +43 -0
  20. package/runtime/node_modules/@metro-labs/core/src/address.ts +6 -0
  21. package/runtime/node_modules/@metro-labs/core/src/endpoints.ts +7 -0
  22. package/runtime/node_modules/@metro-labs/core/src/events.ts +120 -0
  23. package/runtime/node_modules/@metro-labs/core/src/identity.ts +7 -0
  24. package/runtime/node_modules/@metro-labs/core/src/ids.ts +21 -0
  25. package/runtime/node_modules/@metro-labs/core/src/is-record.ts +2 -0
  26. package/runtime/node_modules/@metro-labs/core/src/lines.ts +82 -0
  27. package/runtime/node_modules/@metro-labs/core/src/log.ts +42 -0
  28. package/runtime/node_modules/@metro-labs/core/src/protocol.ts +226 -0
  29. package/runtime/node_modules/@metro-labs/core/src/secure-fs.ts +57 -0
  30. package/runtime/node_modules/@metro-labs/core/src/station-names.ts +12 -0
  31. package/runtime/node_modules/@metro-labs/core/src/stations/account-store.ts +60 -0
  32. package/runtime/node_modules/@metro-labs/core/src/stations/attachments.ts +196 -0
  33. package/runtime/node_modules/@metro-labs/core/src/stations/messaging-normalize.ts +88 -0
  34. package/runtime/node_modules/@metro-labs/core/src/stations/station-runtime.ts +64 -0
  35. package/runtime/node_modules/@metro-labs/core/src/stations/types.ts +107 -0
  36. package/runtime/node_modules/@metro-labs/core/src/str.ts +5 -0
  37. package/runtime/node_modules/@metro-labs/core/src/train-error.ts +53 -0
  38. package/runtime/node_modules/@metro-labs/core/src/version.ts +2 -0
  39. package/runtime/node_modules/@metro-labs/daemon/package.json +47 -0
  40. package/runtime/node_modules/@metro-labs/daemon/src/agents/account-attach.ts +19 -0
  41. package/runtime/node_modules/@metro-labs/daemon/src/agents/accounts-api.ts +311 -0
  42. package/runtime/node_modules/@metro-labs/daemon/src/agents/admin.ts +43 -0
  43. package/runtime/node_modules/@metro-labs/daemon/src/agents/api.ts +302 -0
  44. package/runtime/node_modules/@metro-labs/daemon/src/agents/bundle.ts +111 -0
  45. package/runtime/node_modules/@metro-labs/daemon/src/agents/file-admin.ts +329 -0
  46. package/runtime/node_modules/@metro-labs/daemon/src/agents/files.ts +155 -0
  47. package/runtime/node_modules/@metro-labs/daemon/src/agents/keys.ts +45 -0
  48. package/runtime/node_modules/@metro-labs/daemon/src/agents/map.ts +84 -0
  49. package/runtime/node_modules/@metro-labs/daemon/src/agents/scope.ts +64 -0
  50. package/runtime/node_modules/@metro-labs/daemon/src/boot/boot.ts +176 -0
  51. package/runtime/node_modules/@metro-labs/daemon/src/boot/connect-hint.ts +39 -0
  52. package/runtime/node_modules/@metro-labs/daemon/src/boot/crash-guard.ts +49 -0
  53. package/runtime/node_modules/@metro-labs/daemon/src/boot/local-owner.ts +14 -0
  54. package/runtime/node_modules/@metro-labs/daemon/src/boot/paths.ts +106 -0
  55. package/runtime/node_modules/@metro-labs/daemon/src/channels/addressed.ts +25 -0
  56. package/runtime/node_modules/@metro-labs/daemon/src/channels/dedupe.ts +24 -0
  57. package/runtime/node_modules/@metro-labs/daemon/src/channels/inbound.ts +377 -0
  58. package/runtime/node_modules/@metro-labs/daemon/src/channels/media-note.ts +85 -0
  59. package/runtime/node_modules/@metro-labs/daemon/src/channels/pending.ts +75 -0
  60. package/runtime/node_modules/@metro-labs/daemon/src/channels/relay.ts +129 -0
  61. package/runtime/node_modules/@metro-labs/daemon/src/channels/webhook-note.ts +51 -0
  62. package/runtime/node_modules/@metro-labs/daemon/src/claude/api.ts +148 -0
  63. package/runtime/node_modules/@metro-labs/daemon/src/claude/files.ts +329 -0
  64. package/runtime/node_modules/@metro-labs/daemon/src/claude/login.ts +167 -0
  65. package/runtime/node_modules/@metro-labs/daemon/src/claude/settings.ts +117 -0
  66. package/runtime/node_modules/@metro-labs/daemon/src/connectors/api.ts +307 -0
  67. package/runtime/node_modules/@metro-labs/daemon/src/connectors/cli-api.ts +63 -0
  68. package/runtime/node_modules/@metro-labs/daemon/src/connectors/config.ts +144 -0
  69. package/runtime/node_modules/@metro-labs/daemon/src/connectors/json.ts +21 -0
  70. package/runtime/node_modules/@metro-labs/daemon/src/connectors/model.ts +69 -0
  71. package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-client.ts +159 -0
  72. package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-discovery.ts +117 -0
  73. package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-pending.ts +65 -0
  74. package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-routes.ts +157 -0
  75. package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth.ts +127 -0
  76. package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay-target.ts +39 -0
  77. package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay.ts +295 -0
  78. package/runtime/node_modules/@metro-labs/daemon/src/connectors/store.ts +277 -0
  79. package/runtime/node_modules/@metro-labs/daemon/src/connectors/tools.ts +98 -0
  80. package/runtime/node_modules/@metro-labs/daemon/src/connectors/url.ts +70 -0
  81. package/runtime/node_modules/@metro-labs/daemon/src/connectors/verify.ts +330 -0
  82. package/runtime/node_modules/@metro-labs/daemon/src/files/attach-grant.ts +99 -0
  83. package/runtime/node_modules/@metro-labs/daemon/src/files/attach-owner.ts +32 -0
  84. package/runtime/node_modules/@metro-labs/daemon/src/files/attach-serve.ts +121 -0
  85. package/runtime/node_modules/@metro-labs/daemon/src/files/upload-api.ts +294 -0
  86. package/runtime/node_modules/@metro-labs/daemon/src/files/upload-store.ts +181 -0
  87. package/runtime/node_modules/@metro-labs/daemon/src/gateway/bedrock.ts +252 -0
  88. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-auth.ts +231 -0
  89. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-device.ts +91 -0
  90. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-stream.ts +288 -0
  91. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-translate.ts +203 -0
  92. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex.ts +220 -0
  93. package/runtime/node_modules/@metro-labs/daemon/src/gateway/eventstream.ts +123 -0
  94. package/runtime/node_modules/@metro-labs/daemon/src/gateway/forward.ts +182 -0
  95. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gateway.ts +197 -0
  96. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-api.ts +195 -0
  97. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-config.ts +198 -0
  98. package/runtime/node_modules/@metro-labs/daemon/src/gateway/openrouter.ts +40 -0
  99. package/runtime/node_modules/@metro-labs/daemon/src/gateway/served.ts +17 -0
  100. package/runtime/node_modules/@metro-labs/daemon/src/mcp/accounts.ts +137 -0
  101. package/runtime/node_modules/@metro-labs/daemon/src/mcp/call-tools.ts +216 -0
  102. package/runtime/node_modules/@metro-labs/daemon/src/mcp/channel-owner.ts +29 -0
  103. package/runtime/node_modules/@metro-labs/daemon/src/mcp/ctx.ts +62 -0
  104. package/runtime/node_modules/@metro-labs/daemon/src/mcp/event-store.ts +91 -0
  105. package/runtime/node_modules/@metro-labs/daemon/src/mcp/frame-scope.ts +21 -0
  106. package/runtime/node_modules/@metro-labs/daemon/src/mcp/group-tools.ts +129 -0
  107. package/runtime/node_modules/@metro-labs/daemon/src/mcp/index.ts +191 -0
  108. package/runtime/node_modules/@metro-labs/daemon/src/mcp/member-tools.ts +72 -0
  109. package/runtime/node_modules/@metro-labs/daemon/src/mcp/permission-relay.ts +66 -0
  110. package/runtime/node_modules/@metro-labs/daemon/src/mcp/raw-get-stream.ts +207 -0
  111. package/runtime/node_modules/@metro-labs/daemon/src/mcp/request-identity.ts +47 -0
  112. package/runtime/node_modules/@metro-labs/daemon/src/mcp/session-registry.ts +172 -0
  113. package/runtime/node_modules/@metro-labs/daemon/src/mcp/session-route.ts +43 -0
  114. package/runtime/node_modules/@metro-labs/daemon/src/mcp/session.ts +215 -0
  115. package/runtime/node_modules/@metro-labs/daemon/src/mcp/tool-dispatch.ts +215 -0
  116. package/runtime/node_modules/@metro-labs/daemon/src/mcp/tool-schemas.ts +400 -0
  117. package/runtime/node_modules/@metro-labs/daemon/src/mcp/upload-tool.ts +66 -0
  118. package/runtime/node_modules/@metro-labs/daemon/src/monitor/api.ts +237 -0
  119. package/runtime/node_modules/@metro-labs/daemon/src/net/tunnel.ts +389 -0
  120. package/runtime/node_modules/@metro-labs/daemon/src/routes/http.ts +390 -0
  121. package/runtime/node_modules/@metro-labs/daemon/src/routes/identity.ts +54 -0
  122. package/runtime/node_modules/@metro-labs/daemon/src/routes/local-mode.ts +187 -0
  123. package/runtime/node_modules/@metro-labs/daemon/src/routes/session-apis.ts +60 -0
  124. package/runtime/node_modules/@metro-labs/daemon/src/routes/session.ts +29 -0
  125. package/runtime/node_modules/@metro-labs/daemon/src/server/control.ts +64 -0
  126. package/runtime/node_modules/@metro-labs/daemon/src/server/machine.ts +61 -0
  127. package/runtime/node_modules/@metro-labs/daemon/src/server/update.ts +116 -0
  128. package/runtime/node_modules/@metro-labs/daemon/src/server.ts +2 -0
  129. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-inline.ts +100 -0
  130. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-interactive.ts +161 -0
  131. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-resolve.ts +202 -0
  132. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-session.ts +280 -0
  133. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-xmtp.ts +111 -0
  134. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach.ts +184 -0
  135. package/runtime/node_modules/@metro-labs/daemon/src/stations/materialize.ts +248 -0
  136. package/runtime/node_modules/@metro-labs/daemon/src/stations/registry.ts +35 -0
  137. package/runtime/node_modules/@metro-labs/daemon/src/stations/runtime-deps.ts +82 -0
  138. package/runtime/node_modules/@metro-labs/daemon/src/stations/supervisor-io.ts +97 -0
  139. package/runtime/node_modules/@metro-labs/daemon/src/stations/supervisor.ts +320 -0
  140. package/runtime/node_modules/@metro-labs/daemon/src/stations/train-call.ts +22 -0
  141. package/runtime/node_modules/@metro-labs/daemon/src/terminal/api.ts +71 -0
  142. package/runtime/node_modules/@metro-labs/daemon/src/terminal/socket.ts +123 -0
  143. package/runtime/node_modules/@metro-labs/daemon/src/terminal/tickets.ts +40 -0
  144. package/runtime/node_modules/@metro-labs/discord-bot/package.json +28 -0
  145. package/runtime/node_modules/@metro-labs/discord-bot/src/accounts.ts +118 -0
  146. package/runtime/node_modules/@metro-labs/discord-bot/src/actions.ts +388 -0
  147. package/runtime/node_modules/@metro-labs/discord-bot/src/api-base.ts +1 -0
  148. package/runtime/node_modules/@metro-labs/discord-bot/src/attachments.ts +29 -0
  149. package/runtime/node_modules/@metro-labs/discord-bot/src/format.ts +220 -0
  150. package/runtime/node_modules/@metro-labs/discord-bot/src/group-actions.ts +50 -0
  151. package/runtime/node_modules/@metro-labs/discord-bot/src/group.ts +115 -0
  152. package/runtime/node_modules/@metro-labs/discord-bot/src/index.ts +127 -0
  153. package/runtime/node_modules/@metro-labs/discord-bot/src/members.ts +215 -0
  154. package/runtime/node_modules/@metro-labs/discord-bot/src/send-files.ts +43 -0
  155. package/runtime/node_modules/@metro-labs/discord-bot/src/station.ts +19 -0
  156. package/runtime/node_modules/@metro-labs/discord-bot/src/verify.ts +82 -0
  157. package/runtime/node_modules/@metro-labs/discord-bot/src/voice-speak.ts +195 -0
  158. package/runtime/node_modules/@metro-labs/discord-bot/src/voice-transcribe.ts +292 -0
  159. package/runtime/node_modules/@metro-labs/discord-bot/src/voice.ts +197 -0
  160. package/runtime/node_modules/@metro-labs/discord-bot/src/wire.ts +5 -0
  161. package/runtime/node_modules/@metro-labs/http/package.json +33 -0
  162. package/runtime/node_modules/@metro-labs/http/src/api-error.ts +8 -0
  163. package/runtime/node_modules/@metro-labs/http/src/api-http.ts +95 -0
  164. package/runtime/node_modules/@metro-labs/http/src/cors.ts +29 -0
  165. package/runtime/node_modules/@metro-labs/http/src/identity-key.ts +33 -0
  166. package/runtime/node_modules/@metro-labs/http/src/identity-registry.ts +13 -0
  167. package/runtime/node_modules/@metro-labs/http/src/mode-api.ts +32 -0
  168. package/runtime/node_modules/@metro-labs/http/src/return-to.ts +21 -0
  169. package/runtime/node_modules/@metro-labs/http/src/signed-identity.ts +36 -0
  170. package/runtime/node_modules/@metro-labs/telegram/package.json +26 -0
  171. package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +80 -0
  172. package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +254 -0
  173. package/runtime/node_modules/@metro-labs/telegram/src/attachments.ts +87 -0
  174. package/runtime/node_modules/@metro-labs/telegram/src/client.ts +77 -0
  175. package/runtime/node_modules/@metro-labs/telegram/src/format.ts +156 -0
  176. package/runtime/node_modules/@metro-labs/telegram/src/group.ts +261 -0
  177. package/runtime/node_modules/@metro-labs/telegram/src/history.ts +52 -0
  178. package/runtime/node_modules/@metro-labs/telegram/src/inbound.ts +78 -0
  179. package/runtime/node_modules/@metro-labs/telegram/src/index.ts +46 -0
  180. package/runtime/node_modules/@metro-labs/telegram/src/login.ts +166 -0
  181. package/runtime/node_modules/@metro-labs/telegram/src/media-actions.ts +80 -0
  182. package/runtime/node_modules/@metro-labs/telegram/src/members.ts +75 -0
  183. package/runtime/node_modules/@metro-labs/telegram/src/normalize.ts +22 -0
  184. package/runtime/node_modules/@metro-labs/telegram/src/reactions.ts +167 -0
  185. package/runtime/node_modules/@metro-labs/telegram/src/station.ts +24 -0
  186. package/runtime/node_modules/@metro-labs/telegram/src/types.ts +7 -0
  187. package/runtime/node_modules/@metro-labs/telegram/src/wire.ts +1 -0
  188. package/runtime/node_modules/@metro-labs/telegram-bot/package.json +25 -0
  189. package/runtime/node_modules/@metro-labs/telegram-bot/src/accounts.ts +120 -0
  190. package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +295 -0
  191. package/runtime/node_modules/@metro-labs/telegram-bot/src/attachments.ts +128 -0
  192. package/runtime/node_modules/@metro-labs/telegram-bot/src/format.ts +201 -0
  193. package/runtime/node_modules/@metro-labs/telegram-bot/src/index.ts +109 -0
  194. package/runtime/node_modules/@metro-labs/telegram-bot/src/media-actions.ts +166 -0
  195. package/runtime/node_modules/@metro-labs/telegram-bot/src/members.ts +47 -0
  196. package/runtime/node_modules/@metro-labs/telegram-bot/src/station.ts +17 -0
  197. package/runtime/node_modules/@metro-labs/telegram-bot/src/types.ts +58 -0
  198. package/runtime/node_modules/@metro-labs/telegram-bot/src/verify.ts +38 -0
  199. package/runtime/node_modules/@metro-labs/telegram-bot/src/wire.ts +3 -0
  200. package/runtime/node_modules/@metro-labs/webhook/package.json +23 -0
  201. package/runtime/node_modules/@metro-labs/webhook/src/station.ts +37 -0
  202. package/runtime/node_modules/@metro-labs/whatsapp/package.json +27 -0
  203. package/runtime/node_modules/@metro-labs/whatsapp/src/accounts.ts +66 -0
  204. package/runtime/node_modules/@metro-labs/whatsapp/src/ack.ts +101 -0
  205. package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +178 -0
  206. package/runtime/node_modules/@metro-labs/whatsapp/src/attachments.ts +72 -0
  207. package/runtime/node_modules/@metro-labs/whatsapp/src/auth-state.ts +129 -0
  208. package/runtime/node_modules/@metro-labs/whatsapp/src/client.ts +327 -0
  209. package/runtime/node_modules/@metro-labs/whatsapp/src/delivery.ts +49 -0
  210. package/runtime/node_modules/@metro-labs/whatsapp/src/format.ts +138 -0
  211. package/runtime/node_modules/@metro-labs/whatsapp/src/inbound.ts +57 -0
  212. package/runtime/node_modules/@metro-labs/whatsapp/src/index.ts +40 -0
  213. package/runtime/node_modules/@metro-labs/whatsapp/src/keys.ts +59 -0
  214. package/runtime/node_modules/@metro-labs/whatsapp/src/logger.ts +60 -0
  215. package/runtime/node_modules/@metro-labs/whatsapp/src/login.ts +176 -0
  216. package/runtime/node_modules/@metro-labs/whatsapp/src/media.ts +84 -0
  217. package/runtime/node_modules/@metro-labs/whatsapp/src/normalize.ts +22 -0
  218. package/runtime/node_modules/@metro-labs/whatsapp/src/outbox.ts +38 -0
  219. package/runtime/node_modules/@metro-labs/whatsapp/src/parse.ts +185 -0
  220. package/runtime/node_modules/@metro-labs/whatsapp/src/station.ts +17 -0
  221. package/runtime/node_modules/@metro-labs/whatsapp/src/token-store.ts +98 -0
  222. package/runtime/node_modules/@metro-labs/whatsapp/src/types.ts +6 -0
  223. package/runtime/node_modules/@metro-labs/whatsapp/src/wire.ts +5 -0
  224. package/runtime/node_modules/@metro-labs/xmtp/package.json +33 -0
  225. package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +112 -0
  226. package/runtime/node_modules/@metro-labs/xmtp/src/actions-close.ts +110 -0
  227. package/runtime/node_modules/@metro-labs/xmtp/src/actions-conv.ts +299 -0
  228. package/runtime/node_modules/@metro-labs/xmtp/src/actions-meta.ts +157 -0
  229. package/runtime/node_modules/@metro-labs/xmtp/src/actions-push.ts +87 -0
  230. package/runtime/node_modules/@metro-labs/xmtp/src/actions.ts +375 -0
  231. package/runtime/node_modules/@metro-labs/xmtp/src/attachments.ts +107 -0
  232. package/runtime/node_modules/@metro-labs/xmtp/src/codecs.ts +171 -0
  233. package/runtime/node_modules/@metro-labs/xmtp/src/conv-helpers.ts +239 -0
  234. package/runtime/node_modules/@metro-labs/xmtp/src/emit-core.ts +55 -0
  235. package/runtime/node_modules/@metro-labs/xmtp/src/emit-payloads.ts +335 -0
  236. package/runtime/node_modules/@metro-labs/xmtp/src/emit.ts +80 -0
  237. package/runtime/node_modules/@metro-labs/xmtp/src/group-members.ts +34 -0
  238. package/runtime/node_modules/@metro-labs/xmtp/src/group.ts +56 -0
  239. package/runtime/node_modules/@metro-labs/xmtp/src/identity.ts +33 -0
  240. package/runtime/node_modules/@metro-labs/xmtp/src/index.ts +125 -0
  241. package/runtime/node_modules/@metro-labs/xmtp/src/labels.ts +157 -0
  242. package/runtime/node_modules/@metro-labs/xmtp/src/member-args.ts +40 -0
  243. package/runtime/node_modules/@metro-labs/xmtp/src/push.ts +349 -0
  244. package/runtime/node_modules/@metro-labs/xmtp/src/station.ts +13 -0
  245. package/runtime/node_modules/@metro-labs/xmtp/src/tools-handlers.ts +126 -0
  246. package/runtime/node_modules/@metro-labs/xmtp/src/tools.ts +212 -0
  247. package/runtime/node_modules/@metro-labs/xmtp/src/transcribe.ts +90 -0
  248. package/runtime/node_modules/@metro-labs/xmtp/src/verify.ts +84 -0
  249. package/runtime/node_modules/@metro-labs/xmtp/src/wire.ts +53 -0
  250. package/runtime/runtime.json +3 -0
  251. package/runtime/server.ts +1 -0
  252. package/runtime/stations.json +36 -0
  253. package/runtime/trains/.keep +0 -0
  254. package/LICENSE +0 -21
  255. package/README.md +0 -249
  256. package/dist/cache.js +0 -75
  257. package/dist/cli/actions.js +0 -139
  258. package/dist/cli/config.js +0 -185
  259. package/dist/cli/index.js +0 -177
  260. package/dist/cli/skill.js +0 -62
  261. package/dist/cli/util.js +0 -72
  262. package/dist/cli/webhook.js +0 -83
  263. package/dist/codex-rc.js +0 -245
  264. package/dist/dispatcher.js +0 -122
  265. package/dist/history.js +0 -99
  266. package/dist/ipc.js +0 -72
  267. package/dist/log.js +0 -15
  268. package/dist/paths.js +0 -68
  269. package/dist/registry.js +0 -48
  270. package/dist/stations/claude.js +0 -45
  271. package/dist/stations/codex.js +0 -68
  272. package/dist/stations/discord.js +0 -200
  273. package/dist/stations/index.js +0 -133
  274. package/dist/stations/telegram-md.js +0 -34
  275. package/dist/stations/telegram-upload.js +0 -113
  276. package/dist/stations/telegram.js +0 -213
  277. package/dist/stations/webhook.js +0 -100
  278. package/dist/tunnel.js +0 -49
  279. package/dist/webhooks.js +0 -40
  280. package/docs/agents.md +0 -216
  281. package/docs/uri-scheme.md +0 -125
  282. package/skills/metro/SKILL.md +0 -249
@@ -0,0 +1,51 @@
1
+ const MAX_BODY_CHARS = 8 * 1024;
2
+
3
+ const HEADERS_KEPT = [
4
+ 'x-github-event',
5
+ 'x-github-delivery',
6
+ 'x-intercom-topic',
7
+ 'x-request-id',
8
+ 'user-agent',
9
+ ];
10
+
11
+ interface WebhookPayload {
12
+ headers?: Record<string, string>;
13
+ body?: unknown;
14
+ }
15
+
16
+ function renderBody(body: unknown): string {
17
+ if (body === undefined || body === null) return '';
18
+ const text =
19
+ typeof body === 'string' ? body : JSON.stringify(body, null, 2);
20
+ if (text.length <= MAX_BODY_CHARS) return text;
21
+ return (
22
+ text.slice(0, MAX_BODY_CHARS) +
23
+ `\n[truncated: ${String(text.length - MAX_BODY_CHARS)} more characters]`
24
+ );
25
+ }
26
+
27
+ function renderHeaders(headers: Record<string, string> | undefined): string {
28
+ if (!headers) return '';
29
+ const kept = HEADERS_KEPT.flatMap((k) =>
30
+ headers[k] === undefined ? [] : [`${k}: ${headers[k]}`],
31
+ );
32
+ return kept.length === 0 ? '' : kept.join('\n') + '\n';
33
+ }
34
+
35
+ export function buildWebhookNote(
36
+ summary: string,
37
+ lineName: string | undefined,
38
+ payload: unknown,
39
+ ): string {
40
+ const p = (
41
+ payload !== null && typeof payload === 'object' ? payload : {}
42
+ ) as WebhookPayload;
43
+ const label = lineName ? `${lineName}: ` : '';
44
+ const body = renderBody(p.body);
45
+ return (
46
+ `[webhook received] ${label}${summary}\n` +
47
+ renderHeaders(p.headers) +
48
+ (body ? `\n${body}\n` : '') +
49
+ 'Inbound only: this line takes no send, reply or react.'
50
+ );
51
+ }
@@ -0,0 +1,148 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http';
2
+ import { errMsg, log } from '@metro-labs/core/log';
3
+ import { apiFailure, apiSession, cors, readJsonBody, sendJson } from '@metro-labs/http/api-http';
4
+ import { ApiError } from '@metro-labs/http/api-error';
5
+ import { isRecord } from '@metro-labs/core/is-record';
6
+ import { listClaudeSettings, SETTINGS_MAX, writeClaudeSettings } from './settings.js';
7
+ import {
8
+ answerClaudeLogin,
9
+ claudeAccount,
10
+ claudeInstalled,
11
+ claudeLoginView,
12
+ endClaudeLogin,
13
+ startClaudeLogin,
14
+ type LoginDeps,
15
+ } from './login.js';
16
+ import {
17
+ claudeDir,
18
+ deleteClaudeSession,
19
+ listClaudeProjects,
20
+ listClaudeSessions,
21
+ listMemory,
22
+ readMemoryFile,
23
+ readTranscript,
24
+ } from './files.js';
25
+
26
+ const PREFIX = '/api/claude';
27
+ const BODY_MAX = SETTINGS_MAX + 4096;
28
+ const WRITABLE = new Set(['GET', 'DELETE', 'PUT', 'POST']);
29
+ const PAGE = 100;
30
+ const PAGE_MAX = 500;
31
+
32
+ export interface ClaudeApiDeps {
33
+ authorize: (subject: string) => void;
34
+ dir?: () => string;
35
+ login?: LoginDeps;
36
+ }
37
+
38
+ function projectOf(query: URLSearchParams): string {
39
+ const project = query.get('project');
40
+ if (project === null || project === '') throw new ApiError('project is required', 400);
41
+ return project;
42
+ }
43
+
44
+ function pageOf(query: URLSearchParams): { offset: number; limit: number } {
45
+ const offset = Math.max(0, Math.floor(Number(query.get('offset') ?? '0')) || 0);
46
+ const limit = Math.min(PAGE_MAX, Math.max(1, Math.floor(Number(query.get('limit') ?? String(PAGE))) || PAGE));
47
+ return { offset, limit };
48
+ }
49
+
50
+ type Handler = (query: URLSearchParams, dir: string, item: string) => unknown;
51
+
52
+ const COLLECTIONS: Record<string, Handler> = {
53
+ projects: (_query, dir) => ({ projects: listClaudeProjects(dir) }),
54
+ sessions: (query, dir) => ({ sessions: listClaudeSessions(projectOf(query), dir) }),
55
+ memory: (query, dir) => listMemory(projectOf(query), dir),
56
+ settings: (_query, dir) => ({ files: listClaudeSettings(dir) }),
57
+ };
58
+
59
+ const ITEMS: Record<string, Handler> = {
60
+ sessions: (query, dir, id) => {
61
+ const { offset, limit } = pageOf(query);
62
+ return readTranscript(projectOf(query), id, offset, limit, dir);
63
+ },
64
+ memory: (query, dir, name) => ({ name, content: readMemoryFile(projectOf(query), name, dir) }),
65
+ };
66
+
67
+ const parts = (path: string): string[] => path.slice(PREFIX.length + 1).split('/').filter(Boolean);
68
+
69
+ async function writeAnswer(req: IncomingMessage, path: string, dir: string): Promise<unknown> {
70
+ const [head = '', item = ''] = parts(path);
71
+ if (head !== 'settings' || item === '') throw new ApiError('method not allowed', 405);
72
+ const body = await readJsonBody(req, BODY_MAX);
73
+ if (!isRecord(body) || typeof body.text !== 'string') throw new ApiError('text is required', 400);
74
+ const seenAt = 'seenAt' in body ? (typeof body.seenAt === 'string' ? body.seenAt : null) : undefined;
75
+ return writeClaudeSettings(item, body.text, seenAt, dir);
76
+ }
77
+
78
+ const LOGIN = 'login';
79
+
80
+ async function loginAnswer(req: IncomingMessage, id: string, deps: ClaudeApiDeps): Promise<unknown> {
81
+ const method = req.method ?? 'GET';
82
+ if (id === '') {
83
+ if (method === 'POST') return startClaudeLogin(deps.login);
84
+ if (method === 'GET') return { available: claudeInstalled(), ...claudeAccount() };
85
+ throw new ApiError('method not allowed', 405);
86
+ }
87
+ if (method === 'GET') return claudeLoginView(id);
88
+ if (method === 'DELETE') return endClaudeLogin(id);
89
+ if (method !== 'POST') throw new ApiError('method not allowed', 405);
90
+ const body = await readJsonBody(req);
91
+ if (!isRecord(body) || typeof body.text !== 'string') throw new ApiError('text is required', 400);
92
+ return answerClaudeLogin(id, body.text);
93
+ }
94
+
95
+ function removed(rest: string[], query: URLSearchParams, dir: string): unknown {
96
+ const [head = '', item = ''] = rest;
97
+ if (rest.length !== 2 || head !== 'sessions') throw new ApiError('method not allowed', 405);
98
+ deleteClaudeSession(projectOf(query), item, dir);
99
+ return { deleted: item };
100
+ }
101
+
102
+ function answer(method: string, path: string, query: URLSearchParams, dir: string): unknown {
103
+ const rest = parts(path);
104
+ const [head = '', item = ''] = rest;
105
+ if (method === 'DELETE') return removed(rest, query, dir);
106
+ if (method !== 'GET') throw new ApiError('method not allowed', 405);
107
+ const handler = rest.length === 1 ? COLLECTIONS[head] : rest.length === 2 ? ITEMS[head] : undefined;
108
+ if (handler === undefined) throw new ApiError('no such route', 404);
109
+ return handler(query, dir, item);
110
+ }
111
+
112
+ export function handleClaudeRequest(
113
+ req: IncomingMessage,
114
+ res: ServerResponse,
115
+ deps: ClaudeApiDeps,
116
+ ): boolean {
117
+ const [path = '', search = ''] = (req.url ?? '').split('?');
118
+ if (path !== PREFIX && !path.startsWith(`${PREFIX}/`)) return false;
119
+ if (req.method === 'OPTIONS') {
120
+ res.writeHead(204, cors(req)).end();
121
+ return true;
122
+ }
123
+ if (!WRITABLE.has(req.method ?? '')) {
124
+ sendJson(req, res, 405, { error: 'method not allowed' });
125
+ return true;
126
+ }
127
+ apiSession(req)
128
+ .then((session) => {
129
+ if (!session) throw new ApiError('unauthorized', 401);
130
+ deps.authorize(session.subject);
131
+ const dir = (deps.dir ?? claudeDir)();
132
+ const [head = '', item = ''] = parts(path);
133
+ if (head === LOGIN) return loginAnswer(req, item, deps);
134
+ if (req.method === 'PUT') return writeAnswer(req, path, dir);
135
+ return answer(req.method ?? 'GET', path, new URLSearchParams(search), dir);
136
+ })
137
+ .then((body) => {
138
+ sendJson(req, res, 200, body);
139
+ })
140
+ .catch((err: unknown) => {
141
+ if (err instanceof ApiError) apiFailure(req, res, err, 'claude-api');
142
+ else {
143
+ log.warn({ err: errMsg(err) }, 'claude-api: request failed');
144
+ if (!res.headersSent) sendJson(req, res, 500, { error: 'claude-api failed' });
145
+ }
146
+ });
147
+ return true;
148
+ }
@@ -0,0 +1,329 @@
1
+ import {
2
+ closeSync,
3
+ createReadStream,
4
+ existsSync,
5
+ openSync,
6
+ readdirSync,
7
+ readSync,
8
+ rmSync,
9
+ statSync,
10
+ } from 'node:fs';
11
+ import { homedir } from 'node:os';
12
+ import { join } from 'node:path';
13
+ import { createInterface } from 'node:readline';
14
+ import { ApiError } from '@metro-labs/http/api-error';
15
+ import { isRecord } from '@metro-labs/core/is-record';
16
+
17
+ const PROJECT_RE = /^[A-Za-z0-9._-]{1,200}$/;
18
+ const SESSION_RE = /^[A-Za-z0-9][A-Za-z0-9-]{7,63}$/;
19
+ const MEMORY_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,119}\.md$/;
20
+ const EDGE_BYTES = 64 * 1024;
21
+ const TEXT_CAP = 20_000;
22
+ const RESULT_CAP = 8_000;
23
+ const INPUT_CAP = 4_000;
24
+ const FILE_CAP = 1_000_000;
25
+ const NOISE = ['<task-notification>', '<system-reminder>', '<local-command', '<command-name>'];
26
+
27
+ export function claudeDir(): string {
28
+ const explicit = process.env.METRO_CLAUDE_DIR?.trim();
29
+ if (explicit !== undefined && explicit !== '') return explicit;
30
+ const cfg = process.env.CLAUDE_CONFIG_DIR?.trim();
31
+ if (cfg !== undefined && cfg !== '') return cfg;
32
+ return join(homedir(), '.claude');
33
+ }
34
+
35
+ const str = (v: unknown): string | null => (typeof v === 'string' && v !== '' ? v : null);
36
+
37
+ function safeName(value: string, re: RegExp, what: string): string {
38
+ if (!re.test(value) || value === '.' || value === '..')
39
+ throw new ApiError(`that is not a ${what}`, 400);
40
+ return value;
41
+ }
42
+
43
+ function projectDir(project: string, dir: string): string {
44
+ const path = join(dir, 'projects', safeName(project, PROJECT_RE, 'Claude project id'));
45
+ if (!existsSync(path)) throw new ApiError('no such Claude project', 404);
46
+ return path;
47
+ }
48
+
49
+ function parseLines(text: string): Record<string, unknown>[] {
50
+ const out: Record<string, unknown>[] = [];
51
+ for (const line of text.split('\n')) {
52
+ if (line.trim() === '') continue;
53
+ try {
54
+ const parsed: unknown = JSON.parse(line);
55
+ if (isRecord(parsed)) out.push(parsed);
56
+ } catch {
57
+ continue;
58
+ }
59
+ }
60
+ return out;
61
+ }
62
+
63
+ function readEdge(path: string, fromEnd: boolean): Record<string, unknown>[] {
64
+ const size = statSync(path).size;
65
+ const length = Math.min(EDGE_BYTES, size);
66
+ const fd = openSync(path, 'r');
67
+ try {
68
+ const buffer = Buffer.alloc(length);
69
+ readSync(fd, buffer, 0, length, fromEnd ? size - length : 0);
70
+ const text = buffer.toString('utf8');
71
+ const cut = fromEnd && length < size ? text.indexOf('\n') + 1 : 0;
72
+ return parseLines(text.slice(cut));
73
+ } finally {
74
+ closeSync(fd);
75
+ }
76
+ }
77
+
78
+ export interface ClaudeProject {
79
+ id: string;
80
+ cwd: string | null;
81
+ sessions: number;
82
+ lastActiveAt: string | null;
83
+ hasMemory: boolean;
84
+ }
85
+
86
+ function sessionFiles(path: string): { name: string; mtime: number; size: number }[] {
87
+ return readdirSync(path)
88
+ .filter((name) => name.endsWith('.jsonl'))
89
+ .map((name) => {
90
+ const s = statSync(join(path, name));
91
+ return { name, mtime: s.mtimeMs, size: s.size };
92
+ })
93
+ .sort((a, b) => b.mtime - a.mtime);
94
+ }
95
+
96
+ function cwdOf(path: string): string | null {
97
+ for (const line of readEdge(path, false)) {
98
+ const cwd = str(line.cwd);
99
+ if (cwd !== null) return cwd;
100
+ }
101
+ return null;
102
+ }
103
+
104
+ export function listClaudeProjects(dir = claudeDir()): ClaudeProject[] {
105
+ const root = join(dir, 'projects');
106
+ if (!existsSync(root)) return [];
107
+ return readdirSync(root, { withFileTypes: true })
108
+ .filter((entry) => entry.isDirectory() && PROJECT_RE.test(entry.name))
109
+ .map((entry) => {
110
+ const path = join(root, entry.name);
111
+ const files = sessionFiles(path);
112
+ const newest = files[0];
113
+ return {
114
+ id: entry.name,
115
+ cwd: newest === undefined ? null : cwdOf(join(path, newest.name)),
116
+ sessions: files.length,
117
+ lastActiveAt: newest === undefined ? null : new Date(newest.mtime).toISOString(),
118
+ hasMemory: existsSync(join(path, 'memory')),
119
+ };
120
+ })
121
+ .sort((a, b) => (b.lastActiveAt ?? '').localeCompare(a.lastActiveAt ?? ''));
122
+ }
123
+
124
+ export interface ClaudeSession {
125
+ id: string;
126
+ title: string;
127
+ startedAt: string | null;
128
+ lastAt: string;
129
+ bytes: number;
130
+ gitBranch: string | null;
131
+ version: string | null;
132
+ }
133
+
134
+ function firstPrompt(lines: Record<string, unknown>[]): string | null {
135
+ for (const line of lines) {
136
+ if (line.type !== 'user' || line.isSidechain === true) continue;
137
+ const message = isRecord(line.message) ? line.message : {};
138
+ const text = promptText(message.content);
139
+ if (text !== null && !NOISE.some((n) => text.startsWith(n))) return text.slice(0, 120);
140
+ }
141
+ return null;
142
+ }
143
+
144
+ function promptText(content: unknown): string | null {
145
+ if (typeof content === 'string') return content.trim() === '' ? null : content.trim();
146
+ if (!Array.isArray(content)) return null;
147
+ for (const block of content)
148
+ if (isRecord(block) && block.type === 'text') {
149
+ const text = str(block.text);
150
+ if (text !== null) return text.trim();
151
+ }
152
+ return null;
153
+ }
154
+
155
+ function sessionOf(path: string, id: string, mtime: number, size: number): ClaudeSession {
156
+ const head = readEdge(path, false);
157
+ const tail = readEdge(path, true);
158
+ const titled = [...tail].reverse().find((l) => l.type === 'ai-title' && str(l.aiTitle) !== null);
159
+ const meta = head.find((l) => l.type === 'user' || l.type === 'assistant') ?? {};
160
+ return {
161
+ id,
162
+ title: (titled === undefined ? null : str(titled.aiTitle)) ?? firstPrompt(head) ?? 'Untitled session',
163
+ startedAt: str(head.find((l) => str(l.timestamp) !== null)?.timestamp) ?? null,
164
+ lastAt: new Date(mtime).toISOString(),
165
+ bytes: size,
166
+ gitBranch: str(meta.gitBranch),
167
+ version: str(meta.version),
168
+ };
169
+ }
170
+
171
+ export function listClaudeSessions(project: string, dir = claudeDir()): ClaudeSession[] {
172
+ const path = projectDir(project, dir);
173
+ return sessionFiles(path).map((f) =>
174
+ sessionOf(join(path, f.name), f.name.slice(0, -'.jsonl'.length), f.mtime, f.size),
175
+ );
176
+ }
177
+
178
+ export type Block =
179
+ | { kind: 'text'; text: string }
180
+ | { kind: 'tool_use'; name: string; input: string }
181
+ | { kind: 'tool_result'; text: string; isError: boolean }
182
+ | { kind: 'thinking' }
183
+ | { kind: 'image' };
184
+
185
+ export interface TranscriptEntry {
186
+ uuid: string;
187
+ at: string | null;
188
+ role: 'user' | 'assistant';
189
+ blocks: Block[];
190
+ }
191
+
192
+ export interface TranscriptPage {
193
+ entries: TranscriptEntry[];
194
+ total: number;
195
+ next: number | null;
196
+ }
197
+
198
+ const cap = (text: string, max: number): string =>
199
+ text.length > max ? `${text.slice(0, max)}\n… (${String(text.length - max)} more characters)` : text;
200
+
201
+ function resultText(content: unknown): string {
202
+ if (typeof content === 'string') return content;
203
+ if (!Array.isArray(content)) return '';
204
+ return content
205
+ .map((b) => (isRecord(b) && b.type === 'text' ? (str(b.text) ?? '') : ''))
206
+ .filter((t) => t !== '')
207
+ .join('\n');
208
+ }
209
+
210
+ function textBlock(raw: Record<string, unknown>): Block | null {
211
+ const text = str(raw.text);
212
+ if (text === null || NOISE.some((n) => text.startsWith(n))) return null;
213
+ return { kind: 'text', text: cap(text, TEXT_CAP) };
214
+ }
215
+
216
+ const BLOCKS: Record<string, (raw: Record<string, unknown>) => Block | null> = {
217
+ text: textBlock,
218
+ tool_use: (raw) => ({
219
+ kind: 'tool_use',
220
+ name: str(raw.name) ?? 'tool',
221
+ input: cap(JSON.stringify(raw.input ?? {}, null, 1), INPUT_CAP),
222
+ }),
223
+ tool_result: (raw) => ({
224
+ kind: 'tool_result',
225
+ text: cap(resultText(raw.content), RESULT_CAP),
226
+ isError: raw.is_error === true,
227
+ }),
228
+ thinking: () => ({ kind: 'thinking' }),
229
+ image: () => ({ kind: 'image' }),
230
+ };
231
+
232
+ function blockOf(raw: unknown): Block | null {
233
+ if (!isRecord(raw) || typeof raw.type !== 'string') return null;
234
+ const make = BLOCKS[raw.type];
235
+ return make === undefined ? null : make(raw);
236
+ }
237
+
238
+ function entryOf(line: Record<string, unknown>): TranscriptEntry | null {
239
+ if ((line.type !== 'user' && line.type !== 'assistant') || line.isSidechain === true) return null;
240
+ const message = isRecord(line.message) ? line.message : {};
241
+ const content = message.content;
242
+ const blocks =
243
+ typeof content === 'string'
244
+ ? [blockOf({ type: 'text', text: content })]
245
+ : Array.isArray(content)
246
+ ? content.map(blockOf)
247
+ : [];
248
+ const kept = blocks.filter((b): b is Block => b !== null);
249
+ if (kept.length === 0) return null;
250
+ return { uuid: str(line.uuid) ?? '', at: str(line.timestamp), role: line.type, blocks: kept };
251
+ }
252
+
253
+ export async function readTranscript(
254
+ project: string,
255
+ session: string,
256
+ offset: number,
257
+ limit: number,
258
+ dir = claudeDir(),
259
+ ): Promise<TranscriptPage> {
260
+ const path = join(projectDir(project, dir), `${safeName(session, SESSION_RE, 'session id')}.jsonl`);
261
+ if (!existsSync(path)) throw new ApiError('no such session', 404);
262
+ const entries: TranscriptEntry[] = [];
263
+ let total = 0;
264
+ const lines = createInterface({ input: createReadStream(path, { encoding: 'utf8' }), crlfDelay: Infinity });
265
+ for await (const line of lines) {
266
+ const parsed = parseLines(line)[0];
267
+ const entry = parsed === undefined ? null : entryOf(parsed);
268
+ if (entry === null) continue;
269
+ if (total >= offset && entries.length < limit) entries.push(entry);
270
+ total += 1;
271
+ }
272
+ return { entries, total, next: offset + limit < total ? offset + limit : null };
273
+ }
274
+
275
+ export function deleteClaudeSession(project: string, session: string, dir = claudeDir()): void {
276
+ const root = projectDir(project, dir);
277
+ const id = safeName(session, SESSION_RE, 'session id');
278
+ const file = join(root, `${id}.jsonl`);
279
+ if (!existsSync(file)) throw new ApiError('no such session', 404);
280
+ rmSync(file);
281
+ rmSync(join(root, id), { recursive: true, force: true });
282
+ }
283
+
284
+ export interface MemoryFile {
285
+ name: string;
286
+ bytes: number;
287
+ modifiedAt: string;
288
+ }
289
+
290
+ export interface MemoryListing {
291
+ files: MemoryFile[];
292
+ index: string | null;
293
+ }
294
+
295
+ function memoryDir(project: string, dir: string): string {
296
+ return join(projectDir(project, dir), 'memory');
297
+ }
298
+
299
+ function readCapped(path: string): string {
300
+ const size = statSync(path).size;
301
+ const fd = openSync(path, 'r');
302
+ try {
303
+ const buffer = Buffer.alloc(Math.min(size, FILE_CAP));
304
+ readSync(fd, buffer, 0, buffer.length, 0);
305
+ return size > FILE_CAP ? `${buffer.toString('utf8')}\n\n… truncated` : buffer.toString('utf8');
306
+ } finally {
307
+ closeSync(fd);
308
+ }
309
+ }
310
+
311
+ export function listMemory(project: string, dir = claudeDir()): MemoryListing {
312
+ const path = memoryDir(project, dir);
313
+ if (!existsSync(path)) return { files: [], index: null };
314
+ const files = readdirSync(path)
315
+ .filter((name) => MEMORY_RE.test(name) && name !== 'MEMORY.md')
316
+ .map((name) => {
317
+ const s = statSync(join(path, name));
318
+ return { name, bytes: s.size, modifiedAt: s.mtime.toISOString() };
319
+ })
320
+ .sort((a, b) => a.name.localeCompare(b.name));
321
+ const index = join(path, 'MEMORY.md');
322
+ return { files, index: existsSync(index) ? readCapped(index) : null };
323
+ }
324
+
325
+ export function readMemoryFile(project: string, name: string, dir = claudeDir()): string {
326
+ const path = join(memoryDir(project, dir), safeName(name, MEMORY_RE, 'memory file name'));
327
+ if (!existsSync(path)) throw new ApiError('no such memory file', 404);
328
+ return readCapped(path);
329
+ }
@@ -0,0 +1,167 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { spawnSync } from 'node:child_process';
3
+ import { ApiError } from '@metro-labs/http/api-error';
4
+ import { isRecord } from '@metro-labs/core/is-record';
5
+ import { errMsg, log } from '@metro-labs/core/log';
6
+
7
+ const LOGIN_COMMAND = ['claude', 'auth', 'login', '--claudeai'];
8
+ const STATUS_COMMAND = ['auth', 'status', '--json'];
9
+ const TTL_MS = 10 * 60_000;
10
+ const OUTPUT_MAX = 8_000;
11
+ const COLS = 100;
12
+ const ROWS = 40;
13
+ const ESC = String.fromCharCode(27);
14
+ const CSI = /^\[[0-9;?]*[a-zA-Z]/;
15
+ const URL_RE = /https:\/\/[^\s"'`]+/g;
16
+
17
+ export type LoginState = 'pending' | 'done' | 'failed';
18
+
19
+ export interface LoginView {
20
+ id: string;
21
+ state: LoginState;
22
+ url: string | null;
23
+ output: string;
24
+ error: string | null;
25
+ }
26
+
27
+ interface Live {
28
+ id: string;
29
+ startedAt: number;
30
+ state: LoginState;
31
+ output: string;
32
+ error: string | null;
33
+ write: (text: string) => void;
34
+ kill: () => void;
35
+ }
36
+
37
+ export interface LoginDeps {
38
+ command?: string[];
39
+ }
40
+
41
+ let live: Live | null = null;
42
+
43
+ export function plainText(raw: string): string {
44
+ return raw
45
+ .split(ESC)
46
+ .map((part, index) => (index === 0 ? part : part.replace(CSI, '')))
47
+ .join('');
48
+ }
49
+
50
+ export function loginUrlIn(output: string): string | null {
51
+ const found = output.match(URL_RE) ?? [];
52
+ const trimmed = found.map((url) => url.replace(/[.,)]+$/, ''));
53
+ return trimmed.find((url) => url.includes('claude.ai') || url.includes('anthropic.com')) ?? trimmed[0] ?? null;
54
+ }
55
+
56
+ const view = (session: Live): LoginView => ({
57
+ id: session.id,
58
+ state: session.state,
59
+ url: loginUrlIn(session.output),
60
+ output: session.output,
61
+ error: session.error,
62
+ });
63
+
64
+ function sweep(now: number): void {
65
+ if (live !== null && live.state !== 'pending') return;
66
+ if (live !== null && now - live.startedAt > TTL_MS) {
67
+ live.kill();
68
+ live = null;
69
+ }
70
+ }
71
+
72
+ export function claudeInstalled(): boolean {
73
+ const run = spawnSync('claude', ['--version'], { stdio: 'ignore' });
74
+ return run.error === undefined && run.status === 0;
75
+ }
76
+
77
+ export function claudeAccount(): { signedIn: boolean; account: string | null } {
78
+ const run = spawnSync('claude', STATUS_COMMAND, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
79
+ if (run.error !== undefined || typeof run.stdout !== 'string') return { signedIn: false, account: null };
80
+ try {
81
+ const parsed: unknown = JSON.parse(run.stdout);
82
+ if (!isRecord(parsed)) return { signedIn: false, account: null };
83
+ const account = typeof parsed.email === 'string' ? parsed.email : typeof parsed.organization === 'string' ? parsed.organization : null;
84
+ return { signedIn: parsed.authenticated === true || parsed.loggedIn === true || account !== null, account };
85
+ } catch {
86
+ return { signedIn: false, account: null };
87
+ }
88
+ }
89
+
90
+ function note(session: Live, chunk: string): void {
91
+ const next = session.output + plainText(chunk);
92
+ session.output = next.length > OUTPUT_MAX ? next.slice(next.length - OUTPUT_MAX) : next;
93
+ }
94
+
95
+ export function startClaudeLogin(deps: LoginDeps = {}, now = Date.now()): LoginView {
96
+ sweep(now);
97
+ if (live !== null && live.state === 'pending') return view(live);
98
+ const command = deps.command ?? LOGIN_COMMAND;
99
+ const session: Live = {
100
+ id: randomBytes(9).toString('base64url'),
101
+ startedAt: now,
102
+ state: 'pending',
103
+ output: '',
104
+ error: null,
105
+ write: () => undefined,
106
+ kill: () => undefined,
107
+ };
108
+ const terminal = new Bun.Terminal({
109
+ cols: COLS,
110
+ rows: ROWS,
111
+ data: (_term, chunk: Uint8Array) => {
112
+ note(session, Buffer.from(chunk).toString('utf8'));
113
+ },
114
+ });
115
+ const proc = Bun.spawn(command, { terminal, env: { ...process.env, TERM: 'xterm-256color' } });
116
+ session.write = (text: string) => {
117
+ terminal.write(Buffer.from(`${text}\r`, 'utf8'));
118
+ };
119
+ session.kill = () => {
120
+ proc.kill();
121
+ terminal.close();
122
+ };
123
+ proc.exited
124
+ .then((code) => {
125
+ session.state = code === 0 ? 'done' : 'failed';
126
+ if (code !== 0) session.error = `the login ended with status ${String(code)}`;
127
+ terminal.close();
128
+ log.info({ state: session.state }, 'claude-login: the official login finished');
129
+ })
130
+ .catch((err: unknown) => {
131
+ session.state = 'failed';
132
+ session.error = errMsg(err);
133
+ });
134
+ live = session;
135
+ log.info({ command: command[0] }, 'claude-login: started');
136
+ return view(session);
137
+ }
138
+
139
+ function current(id: string): Live {
140
+ const session = live;
141
+ if (session?.id !== id) throw new ApiError('that sign-in is over; start another one', 404);
142
+ return session;
143
+ }
144
+
145
+ export function claudeLoginView(id: string, now = Date.now()): LoginView {
146
+ sweep(now);
147
+ return view(current(id));
148
+ }
149
+
150
+ export function answerClaudeLogin(id: string, text: string): LoginView {
151
+ const session = current(id);
152
+ if (session.state !== 'pending') throw new ApiError('that sign-in has already finished', 409);
153
+ session.write(text);
154
+ return view(session);
155
+ }
156
+
157
+ export function endClaudeLogin(id: string): { ended: true } {
158
+ const session = current(id);
159
+ session.kill();
160
+ live = null;
161
+ return { ended: true };
162
+ }
163
+
164
+ export const forgetClaudeLogin = (): void => {
165
+ live?.kill();
166
+ live = null;
167
+ };