brane 0.0.2 → 0.3.0

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 (338) hide show
  1. package/AGENTS.md +362 -0
  2. package/CHANGELOG.md +33 -0
  3. package/CONTRIBUTING.md +91 -0
  4. package/DESIGN.md +402 -0
  5. package/Dockerfile +61 -0
  6. package/LICENSE +656 -17
  7. package/README.md +174 -2
  8. package/SECURITY.md +205 -0
  9. package/dist/absorb/brane-id.js +11 -0
  10. package/dist/absorb/chatgpt.js +161 -0
  11. package/dist/absorb/claude-code.js +504 -0
  12. package/dist/absorb/index.js +543 -0
  13. package/dist/absorb/organelle.js +345 -0
  14. package/dist/absorb/run.js +520 -0
  15. package/dist/absorb/sources.js +168 -0
  16. package/dist/absorb/types.js +3 -0
  17. package/dist/agent-env.js +45 -0
  18. package/dist/agents.js +1168 -0
  19. package/dist/claude-live-session.js +124 -0
  20. package/dist/cli.js +50 -0
  21. package/dist/codex-tool-log.js +144 -0
  22. package/dist/commands/absorb.js +291 -0
  23. package/dist/commands/attach.js +31 -0
  24. package/dist/commands/certify.js +62 -0
  25. package/dist/commands/config.js +116 -0
  26. package/dist/commands/cost.js +126 -0
  27. package/dist/commands/daemon.js +350 -0
  28. package/dist/commands/doctor.js +262 -0
  29. package/dist/commands/fold.js +95 -0
  30. package/dist/commands/list.js +89 -0
  31. package/dist/commands/mcp.js +84 -0
  32. package/dist/commands/meld.js +332 -0
  33. package/dist/commands/memory.js +215 -0
  34. package/dist/commands/revive.js +74 -0
  35. package/dist/commands/run.js +85 -0
  36. package/dist/commands/spawn.js +1094 -0
  37. package/dist/commands/token.js +153 -0
  38. package/dist/copilot-tool-log.js +145 -0
  39. package/dist/cost/pricing.js +116 -0
  40. package/dist/cost/reprice.js +136 -0
  41. package/dist/crush-tool-log.js +293 -0
  42. package/dist/daemon/agent-scope.js +116 -0
  43. package/dist/daemon/awaiting.js +54 -0
  44. package/dist/daemon/cert-manager.js +349 -0
  45. package/dist/daemon/client.js +262 -0
  46. package/dist/daemon/gated-routes.js +112 -0
  47. package/dist/daemon/keep-awake.js +103 -0
  48. package/dist/daemon/local-socket.js +145 -0
  49. package/dist/daemon/presence-gate.js +206 -0
  50. package/dist/daemon/server.js +6349 -0
  51. package/dist/daemon/signal-source.js +35 -0
  52. package/dist/daemon/tailscale-rebind.js +58 -0
  53. package/dist/daemon/token-ring.js +263 -0
  54. package/dist/daemon/token-routes.js +190 -0
  55. package/dist/db/events.js +152 -0
  56. package/dist/db/mentions.js +179 -0
  57. package/dist/db/schema.js +633 -0
  58. package/dist/db/tokens.js +583 -0
  59. package/dist/db/tool-uses.js +647 -0
  60. package/dist/diagram/index.js +85 -0
  61. package/dist/diagram/label.js +455 -0
  62. package/dist/diagram/layout.js +476 -0
  63. package/dist/diagram/links.js +50 -0
  64. package/dist/diagram/metrics.js +152 -0
  65. package/dist/diagram/parse.js +487 -0
  66. package/dist/diagram/svg.js +389 -0
  67. package/dist/diagram/types.js +9 -0
  68. package/dist/export.js +629 -0
  69. package/dist/files/archive-types.js +2 -0
  70. package/dist/files/archive.js +182 -0
  71. package/dist/files/browse.js +327 -0
  72. package/dist/files/bytes.js +162 -0
  73. package/dist/files/codecs.js +33 -0
  74. package/dist/files/find.js +212 -0
  75. package/dist/files/tar.js +223 -0
  76. package/dist/files/zip.js +209 -0
  77. package/dist/folds.js +72 -0
  78. package/dist/grammars/ebnf.js +92 -0
  79. package/dist/highlight.js +250 -0
  80. package/dist/ids.js +24 -0
  81. package/dist/markdown-diagnostics.js +233 -0
  82. package/dist/math.js +168 -0
  83. package/dist/mcp/server.js +886 -0
  84. package/dist/memory/embedder.js +145 -0
  85. package/dist/memory/embedding-worker.js +71 -0
  86. package/dist/memory/embeddings.js +91 -0
  87. package/dist/memory/index.js +232 -0
  88. package/dist/memory/storage.js +587 -0
  89. package/dist/memory/wiki.js +688 -0
  90. package/dist/memory/wordpiece.js +247 -0
  91. package/dist/mentions-scan.js +40 -0
  92. package/dist/presence.js +259 -0
  93. package/dist/session-affinity.js +185 -0
  94. package/dist/shared/api-request.js +124 -0
  95. package/dist/shared/block-glyphs.js +114 -0
  96. package/dist/shared/diagram-css.js +17 -0
  97. package/dist/shared/export-selection.js +101 -0
  98. package/dist/shared/find-in-text.js +110 -0
  99. package/dist/shared/glow-hues.js +225 -0
  100. package/dist/shared/hex-dump.js +62 -0
  101. package/dist/shared/hex-pages.js +52 -0
  102. package/dist/shared/hit-drum.js +79 -0
  103. package/dist/shared/known-nids.js +58 -0
  104. package/dist/shared/list-scroll-keys.js +56 -0
  105. package/dist/shared/markdown-lists.js +72 -0
  106. package/dist/shared/markdown-math.js +485 -0
  107. package/dist/shared/markdown-mentions.js +79 -0
  108. package/dist/shared/markdown-nids.js +41 -0
  109. package/dist/shared/markdown-protocols.js +53 -0
  110. package/dist/shared/markdown-refs.js +123 -0
  111. package/dist/shared/membrane-invasion.js +342 -0
  112. package/dist/shared/membrane-motion.js +83 -0
  113. package/dist/shared/membrane-palette.js +253 -0
  114. package/dist/shared/membrane-terrain.js +212 -0
  115. package/dist/shared/message-selection.js +123 -0
  116. package/dist/shared/mo-glyph.js +57 -0
  117. package/dist/shared/narrative-format.js +237 -0
  118. package/dist/shared/narrative-menu.js +82 -0
  119. package/dist/shared/narrative-sort.js +47 -0
  120. package/dist/shared/narrative-status.js +52 -0
  121. package/dist/shared/no-pan.js +85 -0
  122. package/dist/shared/pane-layout.js +50 -0
  123. package/dist/shared/payload-inline.js +83 -0
  124. package/dist/shared/speaker-color.js +61 -0
  125. package/dist/shared/token-rotation.js +14 -0
  126. package/dist/shared/view-url.js +197 -0
  127. package/dist/shared/wiki-links.js +70 -0
  128. package/dist/shared/wiki-window.js +4 -0
  129. package/dist/tailscale.js +55 -0
  130. package/dist/test/absorb-claude.test.js +892 -0
  131. package/dist/test/agent-scope.test.js +193 -0
  132. package/dist/test/agents.test.js +55 -0
  133. package/dist/test/api-request.test.js +122 -0
  134. package/dist/test/awaiting-glow.test.js +54 -0
  135. package/dist/test/awaiting.test.js +122 -0
  136. package/dist/test/bind-token.test.js +88 -0
  137. package/dist/test/block-glyphs.test.js +68 -0
  138. package/dist/test/cert-manager.test.js +262 -0
  139. package/dist/test/claude-live-session.test.js +110 -0
  140. package/dist/test/codex-tool-log.test.js +568 -0
  141. package/dist/test/config.test.js +71 -0
  142. package/dist/test/copilot-tool-log.test.js +93 -0
  143. package/dist/test/cors-preflight.test.js +49 -0
  144. package/dist/test/cost-linger.test.js +31 -0
  145. package/dist/test/cost-pricing.test.js +72 -0
  146. package/dist/test/cost-reprice.test.js +139 -0
  147. package/dist/test/cost-sessions.test.js +301 -0
  148. package/dist/test/crush-tool-log.test.js +289 -0
  149. package/dist/test/daemon-client.test.js +109 -0
  150. package/dist/test/daemon-socket.test.js +293 -0
  151. package/dist/test/diagram-arrows.test.js +205 -0
  152. package/dist/test/diagram-export.test.js +210 -0
  153. package/dist/test/diagram-labels.test.js +313 -0
  154. package/dist/test/diagram-parse.test.js +191 -0
  155. package/dist/test/diagram-render.test.js +175 -0
  156. package/dist/test/diagram-review.test.js +288 -0
  157. package/dist/test/diagram-self-loop-labels.test.js +303 -0
  158. package/dist/test/embedder.test.js +84 -0
  159. package/dist/test/events.test.js +123 -0
  160. package/dist/test/export-selection.test.js +102 -0
  161. package/dist/test/export.test.js +505 -0
  162. package/dist/test/files-browse.test.js +393 -0
  163. package/dist/test/files-find.test.js +272 -0
  164. package/dist/test/find-in-text.test.js +86 -0
  165. package/dist/test/first-paint-viewport.test.js +59 -0
  166. package/dist/test/fixtures/embedding-golden.js +23 -0
  167. package/dist/test/fixtures/wordpiece-golden.js +167 -0
  168. package/dist/test/glow-hues.test.js +202 -0
  169. package/dist/test/harness-cost.test.js +423 -0
  170. package/dist/test/helpers.js +21 -0
  171. package/dist/test/hex-pages.test.js +68 -0
  172. package/dist/test/highlight.test.js +69 -0
  173. package/dist/test/hit-drum.test.js +71 -0
  174. package/dist/test/hook-config.test.js +262 -0
  175. package/dist/test/json-gzip.test.js +70 -0
  176. package/dist/test/key-routing.test.js +58 -0
  177. package/dist/test/known-nids.test.js +106 -0
  178. package/dist/test/link-preview.test.js +67 -0
  179. package/dist/test/list-scroll-keys.test.js +66 -0
  180. package/dist/test/listen-own-words.test.js +147 -0
  181. package/dist/test/listen-timeout.test.js +33 -0
  182. package/dist/test/markdown-diagnostics.test.js +121 -0
  183. package/dist/test/markdown-lists.test.js +182 -0
  184. package/dist/test/markdown-math.test.js +256 -0
  185. package/dist/test/markdown-nids.test.js +146 -0
  186. package/dist/test/markdown-protocols.test.js +139 -0
  187. package/dist/test/mcp-bridge-cancel.test.js +98 -0
  188. package/dist/test/mcp-bridge-reconnect.test.js +121 -0
  189. package/dist/test/mcp-narrative.test.js +193 -0
  190. package/dist/test/mcp.test.js +548 -0
  191. package/dist/test/meld.test.js +85 -0
  192. package/dist/test/membrane-invasion.test.js +482 -0
  193. package/dist/test/membrane-motion.test.js +101 -0
  194. package/dist/test/membrane-palette.test.js +166 -0
  195. package/dist/test/membrane-terrain.test.js +170 -0
  196. package/dist/test/memory-log.test.js +202 -0
  197. package/dist/test/mentions-e2e.test.js +261 -0
  198. package/dist/test/mentions-ledger.test.js +265 -0
  199. package/dist/test/mentions-listen.test.js +265 -0
  200. package/dist/test/mentions.test.js +238 -0
  201. package/dist/test/message-selection.test.js +114 -0
  202. package/dist/test/mo-glyph.test.js +22 -0
  203. package/dist/test/model-resolution.test.js +166 -0
  204. package/dist/test/narrate-validate.test.js +77 -0
  205. package/dist/test/narrate-wait.test.js +160 -0
  206. package/dist/test/narrative-format.test.js +219 -0
  207. package/dist/test/narrative-menu.test.js +137 -0
  208. package/dist/test/narrative-sort.test.js +75 -0
  209. package/dist/test/no-pan.test.js +60 -0
  210. package/dist/test/pane-layout.test.js +153 -0
  211. package/dist/test/paths.test.js +37 -0
  212. package/dist/test/payload-inline.test.js +91 -0
  213. package/dist/test/precache-fonts.test.js +72 -0
  214. package/dist/test/presence-gate.test.js +440 -0
  215. package/dist/test/presence.test.js +160 -0
  216. package/dist/test/read-events.test.js +220 -0
  217. package/dist/test/remote-meld.test.js +115 -0
  218. package/dist/test/revive-cli.test.js +93 -0
  219. package/dist/test/search-floor.test.js +206 -0
  220. package/dist/test/search-fusion.test.js +168 -0
  221. package/dist/test/search.test.js +205 -0
  222. package/dist/test/security.test.js +48 -0
  223. package/dist/test/selection-wiring.test.js +61 -0
  224. package/dist/test/session-affinity.test.js +295 -0
  225. package/dist/test/signal-source.test.js +32 -0
  226. package/dist/test/socket-account.test.js +268 -0
  227. package/dist/test/spawn-toml.test.js +72 -0
  228. package/dist/test/speaker-color.test.js +90 -0
  229. package/dist/test/tailscale-rebind.test.js +79 -0
  230. package/dist/test/terminal-cursor.test.js +61 -0
  231. package/dist/test/terminal-resync.test.js +23 -0
  232. package/dist/test/thin-cli.test.js +382 -0
  233. package/dist/test/token-bootstrap.test.js +272 -0
  234. package/dist/test/token-devices.test.js +587 -0
  235. package/dist/test/token-lapse.test.js +94 -0
  236. package/dist/test/token-ring.test.js +188 -0
  237. package/dist/test/token-rotate-cli.test.js +58 -0
  238. package/dist/test/token-routes.test.js +95 -0
  239. package/dist/test/tool-uses.test.js +1006 -0
  240. package/dist/test/usage-ledger.test.js +126 -0
  241. package/dist/test/view-url.test.js +581 -0
  242. package/dist/test/wiki-links.test.js +57 -0
  243. package/dist/test/wiki-sources.test.js +85 -0
  244. package/dist/test/wiki-window.test.js +31 -0
  245. package/dist/test/wiki.test.js +399 -0
  246. package/dist/test/wordpiece.test.js +68 -0
  247. package/dist/tls.js +148 -0
  248. package/dist/tmux.js +202 -0
  249. package/dist/user-agent.js +46 -0
  250. package/docker-entrypoint.sh +141 -0
  251. package/docs/README.md +3 -0
  252. package/docs/absorb.md +231 -0
  253. package/docs/cost-ledger.md +125 -0
  254. package/docs/daemon-api.md +189 -0
  255. package/docs/diagrams.md +163 -0
  256. package/docs/doctor.md +36 -0
  257. package/docs/export-scope.md +69 -0
  258. package/docs/files-view.md +133 -0
  259. package/docs/link-previews.md +38 -0
  260. package/docs/math.md +32 -0
  261. package/docs/mcp.md +69 -0
  262. package/docs/memory-search.md +75 -0
  263. package/docs/mentions.md +101 -0
  264. package/docs/multi-brane.md +14 -0
  265. package/docs/passkeys.md +165 -0
  266. package/docs/presence.md +202 -0
  267. package/docs/releasing.md +53 -0
  268. package/docs/rendering-warnings.md +22 -0
  269. package/docs/request-layer.md +72 -0
  270. package/docs/selection-store.md +50 -0
  271. package/docs/session-affinity.md +61 -0
  272. package/docs/terminal-mirroring.md +211 -0
  273. package/docs/token-rotation.md +413 -0
  274. package/docs/view-urls.md +114 -0
  275. package/docs/web-app.md +23 -0
  276. package/docs/wiki.md +43 -0
  277. package/expo/dist/_expo/static/js/web/entry-e56efc1291dca7258ea00b8108b033a4.js +1097 -0
  278. package/expo/dist/apple-touch-icon.png +0 -0
  279. package/expo/dist/favicon.svg +3 -0
  280. package/expo/dist/index.html +177 -0
  281. package/expo/dist/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  282. package/expo/dist/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  283. package/expo/dist/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  284. package/expo/dist/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  285. package/expo/dist/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  286. package/expo/dist/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
  287. package/expo/dist/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  288. package/expo/dist/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
  289. package/expo/dist/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
  290. package/expo/dist/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  291. package/expo/dist/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
  292. package/expo/dist/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  293. package/expo/dist/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  294. package/expo/dist/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  295. package/expo/dist/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
  296. package/expo/dist/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  297. package/expo/dist/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  298. package/expo/dist/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  299. package/expo/dist/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  300. package/expo/dist/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  301. package/expo/dist/katex/katex.min.css +1 -0
  302. package/expo/dist/manifest.json +16 -0
  303. package/expo/dist/metadata.json +1 -0
  304. package/expo/dist/og/membrane.v1.png +0 -0
  305. package/expo/dist/pdf.min.mjs +28 -0
  306. package/expo/dist/pdf.worker.min.mjs +28 -0
  307. package/expo/dist/sw.js +154 -0
  308. package/package.json +81 -23
  309. package/protocols/LIVE_loop.md +407 -0
  310. package/protocols/bebrief.md +41 -0
  311. package/protocols/digest.md +231 -0
  312. package/protocols/fly.md +266 -0
  313. package/protocols/idle.md +81 -0
  314. package/protocols/keeper.md +829 -0
  315. package/protocols/markdown.md +80 -0
  316. package/protocols/memory.md +419 -0
  317. package/protocols/mentions.md +93 -0
  318. package/protocols/payloads.md +53 -0
  319. package/protocols/prelude.md +91 -0
  320. package/protocols/ramify.md +105 -0
  321. package/protocols/review.md +356 -0
  322. package/protocols/rrr.md +232 -0
  323. package/protocols/scribe.md +429 -0
  324. package/protocols/summarize.md +203 -0
  325. package/protocols/twin.md +174 -0
  326. package/protocols/wiki.md +438 -0
  327. package/protocols/worker.md +38 -0
  328. package/wiki/shared/append-only-event-log.md +148 -0
  329. package/wiki/shared/index.md +48 -0
  330. package/wiki/shared/owning-your-narratives.md +161 -0
  331. package/wiki/shared/premature-ontologies.md +435 -0
  332. package/wiki/shared/sedimentary-superintelligence.md +128 -0
  333. package/wiki/shared/sequestering-the-narrative.md +143 -0
  334. package/wiki/shared/why-wikis-work.md +216 -0
  335. package/.npmignore +0 -2
  336. package/.travis.yml +0 -5
  337. package/index.js +0 -8
  338. package/index.js~ +0 -12
@@ -0,0 +1,124 @@
1
+ // The session a brane-spawned Claude Code is running *now*.
2
+ //
3
+ // The daemon assigns a claude narrative's session id at launch
4
+ // (`--session-id`), but Claude Code can move to a new session without the
5
+ // process ever exiting: `/clear` starts a fresh transcript under an id it mints
6
+ // itself, and so does `/resume <other>` from inside a running session. Nothing
7
+ // brane holds learns about it — the MCP bridge read `CLAUDE_CODE_SESSION_ID`
8
+ // once when it started, and `/clear` doesn't restart MCP servers — so cost
9
+ // kept reading a transcript that had stopped growing, and revive would resume
10
+ // the conversation from before the `/clear`.
11
+ //
12
+ // Claude Code keeps a registry of its interactive processes at
13
+ // `~/.claude/sessions/<pid>.json`, and rewrites its own entry's `sessionId`
14
+ // when the session changes. The daemon reads it for the one process it knows
15
+ // is the narrative's agent: the pane of tmux session `<nid>`, whose command
16
+ // tmux exec'd, so its `pane_pid` *is* the claude process.
17
+ //
18
+ // That choice is what keeps this free of races. It is a pull, not a push: no
19
+ // hook payload or bridge report is trusted, so a subagent (which runs
20
+ // in-process and has no entry of its own), a nested `claude -p` in the agent's
21
+ // Bash (a different pid, though it inherits the narrative's env and hooks),
22
+ // and any number of MCP reconnects have no way to move the pointer. The
23
+ // registry's one writer is the process it describes.
24
+ import { execFileSync } from "node:child_process";
25
+ import { readFileSync } from "node:fs";
26
+ import { homedir } from "node:os";
27
+ import { join, resolve } from "node:path";
28
+ import { resolveTmux } from "./tmux.js";
29
+ /** Slack on `launchedAt`: the daemon stamps a launch just before tmux starts
30
+ * the process, and the two clocks are the same machine's, so a few seconds
31
+ * covers it with room to spare. */
32
+ const LAUNCH_SLACK_MS = 60_000;
33
+ /** Parse one registry entry and return its `sessionId` only if it provably
34
+ * describes this narrative's current agent. Each guard answers a way a file
35
+ * could be read that isn't: the registry keeps entries for exited processes,
36
+ * so a recycled pid can point at a stranger's (`pid`, `tmux`, `cwd`), or at
37
+ * this narrative's own previous launch (`startedAt`). Pure, for tests. */
38
+ export function liveSessionFromRegistryEntry(rawJson, expect) {
39
+ let entry;
40
+ try {
41
+ entry = JSON.parse(rawJson);
42
+ }
43
+ catch {
44
+ return undefined;
45
+ }
46
+ if (!entry || typeof entry !== "object")
47
+ return undefined;
48
+ if (typeof entry.sessionId !== "string" || !entry.sessionId)
49
+ return undefined;
50
+ if (entry.pid !== expect.pid)
51
+ return undefined;
52
+ // `<session>:@<window>.%<pane>` — the name up to the first colon, exactly.
53
+ if (typeof entry.tmux !== "string" || entry.tmux.split(":")[0] !== expect.narrativeId)
54
+ return undefined;
55
+ if (typeof entry.cwd !== "string" || resolve(entry.cwd) !== resolve(expect.foldPath))
56
+ return undefined;
57
+ if (typeof entry.startedAt !== "number" || entry.startedAt < expect.launchedAt - LAUNCH_SLACK_MS)
58
+ return undefined;
59
+ if (entry.kind !== undefined && entry.kind !== "interactive")
60
+ return undefined;
61
+ return entry.sessionId;
62
+ }
63
+ /** Every tmux session's pane pid, from one `list-panes -a`. The cost tick asks
64
+ * for each connected claude narrative in turn, and this runs on the daemon's
65
+ * only thread, so one exec per tick rather than one per narrative; the short
66
+ * memo spans a tick and nothing longer. */
67
+ let paneMemo;
68
+ const PANE_MEMO_MS = 1000;
69
+ /** Parse `list-panes -a -F "#{session_name} #{pane_pid}"`. A session with
70
+ * several panes has no single agent process, so it maps to nothing. Pure. */
71
+ export function parsePanePids(out) {
72
+ const pids = new Map();
73
+ const ambiguous = new Set();
74
+ for (const line of out.split("\n")) {
75
+ const at = line.lastIndexOf(" ");
76
+ if (at <= 0)
77
+ continue;
78
+ const name = line.slice(0, at);
79
+ const pid = Number(line.slice(at + 1));
80
+ if (!Number.isInteger(pid) || pid <= 1)
81
+ continue;
82
+ if (pids.has(name))
83
+ ambiguous.add(name);
84
+ pids.set(name, pid);
85
+ }
86
+ for (const name of ambiguous)
87
+ pids.delete(name);
88
+ return pids;
89
+ }
90
+ function panePid(narrativeId) {
91
+ const now = Date.now();
92
+ if (!paneMemo || now - paneMemo.at > PANE_MEMO_MS) {
93
+ let out = "";
94
+ try {
95
+ out = execFileSync(resolveTmux(), ["list-panes", "-a", "-F", "#{session_name} #{pane_pid}"], {
96
+ encoding: "utf8",
97
+ stdio: ["ignore", "pipe", "ignore"],
98
+ timeout: 2000,
99
+ });
100
+ }
101
+ catch {
102
+ // No server, or no sessions: nothing is live.
103
+ }
104
+ paneMemo = { at: now, pids: parsePanePids(out) };
105
+ }
106
+ return paneMemo.pids.get(narrativeId);
107
+ }
108
+ /** Read the session id Claude Code's registry holds for this narrative's pane,
109
+ * or undefined when there is no pane, no entry, or an entry that fails a
110
+ * guard. Never throws. */
111
+ export function readClaudeLiveSession(opts) {
112
+ const pid = panePid(opts.narrativeId);
113
+ if (!pid)
114
+ return undefined;
115
+ let raw;
116
+ try {
117
+ raw = readFileSync(join(homedir(), ".claude", "sessions", `${pid}.json`), "utf8");
118
+ }
119
+ catch {
120
+ return undefined;
121
+ }
122
+ return liveSessionFromRegistryEntry(raw, { ...opts, pid });
123
+ }
124
+ //# sourceMappingURL=claude-live-session.js.map
package/dist/cli.js ADDED
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { defineCommand, runMain } from "citty";
6
+ import { foldCommand } from "./commands/fold.js";
7
+ import { daemonCommand } from "./commands/daemon.js";
8
+ import { spawnCommand } from "./commands/spawn.js";
9
+ import { listCommand } from "./commands/list.js";
10
+ import { attachCommand } from "./commands/attach.js";
11
+ import { reviveCommand } from "./commands/revive.js";
12
+ import { costCommand } from "./commands/cost.js";
13
+ import { tokenCommand } from "./commands/token.js";
14
+ import { memoryCommand } from "./commands/memory.js";
15
+ import { mcpCommand } from "./commands/mcp.js";
16
+ import { meldCommand } from "./commands/meld.js";
17
+ import { configCommand } from "./commands/config.js";
18
+ import { certifyCommand } from "./commands/certify.js";
19
+ import { absorbCommand } from "./commands/absorb.js";
20
+ import { doctorCommand } from "./commands/doctor.js";
21
+ // dist/cli.js sits one level below package.json both in the checkout and in
22
+ // the published tarball, so the version the CLI reports is the version that
23
+ // was published rather than a string that has to be bumped by hand.
24
+ const packageJson = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf8"));
25
+ const main = defineCommand({
26
+ meta: {
27
+ name: "brane",
28
+ version: packageJson.version,
29
+ description: packageJson.description,
30
+ },
31
+ subCommands: {
32
+ fold: foldCommand,
33
+ daemon: daemonCommand,
34
+ spawn: spawnCommand,
35
+ list: listCommand,
36
+ attach: attachCommand,
37
+ revive: reviveCommand,
38
+ cost: costCommand,
39
+ token: tokenCommand,
40
+ memory: memoryCommand,
41
+ mcp: mcpCommand,
42
+ meld: meldCommand,
43
+ config: configCommand,
44
+ certify: certifyCommand,
45
+ doctor: doctorCommand,
46
+ absorb: absorbCommand,
47
+ },
48
+ });
49
+ runMain(main);
50
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1,144 @@
1
+ import { closeSync, openSync, readSync, statSync } from "node:fs";
2
+ import { findCodexRollout, codexSessionsRoot } from "./agents.js";
3
+ import { readToolUse, recordToolUse } from "./db/tool-uses.js";
4
+ export function newCodexLogCursor() {
5
+ return { offset: 0, pending: Buffer.alloc(0), names: new Map() };
6
+ }
7
+ /** Codex's `custom_tool_call_output` carries an array of content parts rather
8
+ * than a string. Flatten the text so the stored row reads as output instead of
9
+ * as a serialized wire envelope; anything else passes through untouched for
10
+ * `prepareCaptureText` to serialize. */
11
+ function flattenOutput(output) {
12
+ if (!Array.isArray(output))
13
+ return output;
14
+ const parts = output
15
+ .map((p) => p?.text)
16
+ .filter((t) => typeof t === "string");
17
+ return parts.length === output.length ? parts.join("") : output;
18
+ }
19
+ /** Translate one rollout record into the common row shape, or undefined if it
20
+ * isn't a tool call. Pure, so the mapping is pinned by tests rather than
21
+ * reachable only through a filesystem fixture.
22
+ *
23
+ * `names` supplies the fallback for an output whose call line was consumed in
24
+ * an earlier read. It is only ever a fallback: the upsert never overwrites
25
+ * `name`, so an already-recorded call keeps its real one regardless. */
26
+ export function toolUseFromCodexRecord(record, names) {
27
+ if (typeof record !== "object" || record === null)
28
+ return undefined;
29
+ const outer = record;
30
+ if (outer.type !== "response_item")
31
+ return undefined;
32
+ if (typeof outer.payload !== "object" || outer.payload === null)
33
+ return undefined;
34
+ const payload = outer.payload;
35
+ const callId = payload.call_id;
36
+ if (typeof callId !== "string")
37
+ return undefined;
38
+ if (payload.type === "function_call" || payload.type === "custom_tool_call") {
39
+ const name = typeof payload.name === "string" ? payload.name : "unknown";
40
+ names?.set(callId, name);
41
+ return {
42
+ dedupKey: callId,
43
+ name,
44
+ // `function_call` carries `arguments`, `custom_tool_call` carries `input`.
45
+ input: payload.arguments ?? payload.input,
46
+ status: "pending",
47
+ source: "session_log",
48
+ agentType: "codex",
49
+ };
50
+ }
51
+ if (payload.type === "function_call_output" || payload.type === "custom_tool_call_output") {
52
+ return {
53
+ dedupKey: callId,
54
+ name: names?.get(callId) ?? "unknown",
55
+ output: flattenOutput(payload.output),
56
+ status: "completed",
57
+ source: "session_log",
58
+ agentType: "codex",
59
+ };
60
+ }
61
+ return undefined;
62
+ }
63
+ /** Whether writing this record would tell the database anything new.
64
+ *
65
+ * Every `recordToolUse` bumps the row's `rev`, and `rev` is the client sync
66
+ * cursor, so a redundant write is not free — it re-pushes the row to every
67
+ * connected client. That matters when a cursor starts cold (a daemon restart,
68
+ * or a revived narrative) and replays a rollout already recorded in full:
69
+ * without this, every restart would restate hundreds of unchanged rows as a
70
+ * sync storm.
71
+ *
72
+ * A `pending` record is skipped against any existing row, because it can only
73
+ * restate or regress what is stored. The upsert already refuses the
74
+ * regression; this avoids paying a `rev` for the refusal. */
75
+ function wouldChangeNothing(db, narrativeId, toolUse) {
76
+ const existing = readToolUse(db, narrativeId, toolUse.dedupKey);
77
+ if (!existing)
78
+ return false;
79
+ return toolUse.status === "pending" || existing.status === toolUse.status;
80
+ }
81
+ /** Ingest every tool call codex has written since this cursor last ran, and
82
+ * return the rows actually written so the caller can push them.
83
+ *
84
+ * Driven from two directions over one cursor — a timer, and the agent's own
85
+ * MCP calls — which is safe because the cursor is a byte offset: whichever
86
+ * runs first advances it and the other finds nothing. See
87
+ * `startCodexToolSampler` in the daemon.
88
+ *
89
+ * Errors are swallowed and logged: this is telemetry, and an unreadable log
90
+ * must never surface as a tool error on the agent's own call. */
91
+ export function pollCodexToolLog(db, narrativeId, sessionId, cursor, sessionsRoot = codexSessionsRoot()) {
92
+ const path = findCodexRollout(sessionId, sessionsRoot);
93
+ if (!path)
94
+ return [];
95
+ try {
96
+ const size = statSync(path).size;
97
+ if (size < cursor.offset) {
98
+ // The file shrank, so it was replaced or truncated and the offset no
99
+ // longer means anything. Start over rather than read garbage.
100
+ cursor.offset = 0;
101
+ cursor.pending = Buffer.alloc(0);
102
+ cursor.names.clear();
103
+ }
104
+ if (size === cursor.offset)
105
+ return [];
106
+ const fd = openSync(path, "r");
107
+ let chunk = Buffer.allocUnsafe(size - cursor.offset);
108
+ try {
109
+ const n = readSync(fd, chunk, 0, chunk.length, cursor.offset);
110
+ chunk = chunk.subarray(0, n);
111
+ cursor.offset += n;
112
+ }
113
+ finally {
114
+ closeSync(fd);
115
+ }
116
+ const buf = cursor.pending.length ? Buffer.concat([cursor.pending, chunk]) : chunk;
117
+ const lastNewline = buf.lastIndexOf(0x0a);
118
+ if (lastNewline === -1) {
119
+ cursor.pending = buf; // no complete line yet — hold it all for next time
120
+ return [];
121
+ }
122
+ cursor.pending = buf.subarray(lastNewline + 1);
123
+ const written = [];
124
+ for (const line of buf.subarray(0, lastNewline).toString("utf8").split("\n")) {
125
+ if (!line)
126
+ continue;
127
+ try {
128
+ const toolUse = toolUseFromCodexRecord(JSON.parse(line), cursor.names);
129
+ if (!toolUse || wouldChangeNothing(db, narrativeId, toolUse))
130
+ continue;
131
+ written.push(recordToolUse(db, narrativeId, toolUse));
132
+ }
133
+ catch {
134
+ // A malformed record cannot prevent later well-formed entries.
135
+ }
136
+ }
137
+ return written;
138
+ }
139
+ catch (err) {
140
+ console.error(`[codex-log] reading ${path} for ${narrativeId} failed: ${err}`);
141
+ return [];
142
+ }
143
+ }
144
+ //# sourceMappingURL=codex-tool-log.js.map
@@ -0,0 +1,291 @@
1
+ /** CLI command: brane absorb <provider> <path> — a client of POST /api/absorb.
2
+ *
3
+ * The import itself runs in the daemon (`src/absorb/run.ts`): payloads, the
4
+ * organelle search.db and the niche registry all resolve against the
5
+ * daemon's HOME, the same reason `brane cost backfill` delegates. What is
6
+ * left here is terminal-shaped: resolving the operator's relative paths
7
+ * against *this* shell's cwd, and printing the daemon's NDJSON stream —
8
+ * the pre-flight report, a `\r` progress line, the summary — as it arrives.
9
+ */
10
+ import { resolve } from "node:path";
11
+ import { defineCommand, showUsage } from "citty";
12
+ import { resolveDaemonEndpoint, daemonRequest, daemonStream, warnIfVersionMismatch } from "../daemon/client.js";
13
+ import { listProviders } from "../absorb/index.js";
14
+ /** A path the operator typed, made meaningful to a daemon whose cwd is not
15
+ * this shell's: `~/…` is left for the daemon to expand (it is portable
16
+ * by definition), anything else is resolved here. */
17
+ function absolutize(p, cwd) {
18
+ return p.startsWith("~") ? p : resolve(cwd, p);
19
+ }
20
+ /** The request body for `POST /api/absorb`. Only what the operator said is
21
+ * sent, so the daemon's defaults (organelle's `~/.organelle`, the derived
22
+ * brane id, the niche path as fold) stay in charge of everything else. */
23
+ export function buildAbsorbBody(args, cwd) {
24
+ return {
25
+ provider: args.provider,
26
+ ...(args.path ? { path: absolutize(args.path, cwd) } : {}),
27
+ ...(args.fold ? { fold: absolutize(args.fold, cwd) } : {}),
28
+ ...(args.identity ? { identity: args.identity } : {}),
29
+ ...(args.brane ? { brane: args.brane } : {}),
30
+ ...(args.dryRun ? { dry_run: true } : {}),
31
+ ...(args.since ? { since: args.since } : {}),
32
+ ...(args.session ? { session: args.session } : {}),
33
+ ...(args.rebuild ? { rebuild: true } : {}),
34
+ };
35
+ }
36
+ export const absorbCommand = defineCommand({
37
+ meta: {
38
+ name: "absorb",
39
+ description: "Absorb conversation history: from LLM provider exports, or from agent session stores on this machine. `brane absorb survey` lists what this machine has.",
40
+ },
41
+ args: {
42
+ provider: {
43
+ type: "positional",
44
+ required: false,
45
+ description: `Source: ${listProviders().join(", ")} — or "survey" to list the session stores on this machine without reading them`,
46
+ },
47
+ path: {
48
+ type: "positional",
49
+ required: false,
50
+ description: "Path to an extracted export directory (chatgpt), a niche directory (organelle, default ~/.organelle), or a session store (claude, default ~/.claude/projects).",
51
+ },
52
+ fold: {
53
+ type: "string",
54
+ description: "Target fold for absorbed conversations (required for chatgpt; defaults to the niche path for organelle). For a local source (claude) it is a filter: only sessions run in this directory.",
55
+ },
56
+ since: {
57
+ type: "string",
58
+ description: "Local sources only: sessions active in the last 30m/12h/7d/2w, or since an ISO date",
59
+ },
60
+ session: {
61
+ type: "string",
62
+ description: "Local sources only: a single session, by id or id prefix",
63
+ },
64
+ rebuild: {
65
+ type: "boolean",
66
+ description: "Local sources only: for sessions already absorbed into the target brane, drop their events and tool rows and import the file again (same narrative id; seqs renumbered). A narrative someone has signalled or narrated into is not rebuilt, only appended to. Composes with --fold, --since, --session, --dry-run.",
67
+ default: false,
68
+ },
69
+ identity: {
70
+ type: "string",
71
+ description: "User identity label (e.g. email). Auto-detected when available.",
72
+ },
73
+ brane: {
74
+ type: "string",
75
+ description: 'Target brane ID. Use "self" for home brane. Default: auto-generated from provider+identity.',
76
+ },
77
+ dryRun: {
78
+ type: "boolean",
79
+ alias: ["dry-run", "n"],
80
+ description: "Parse and summarize without writing to the database",
81
+ default: false,
82
+ },
83
+ },
84
+ async run({ args }) {
85
+ if (!args.provider) {
86
+ // Usage, not a survey: a bare `brane absorb` should say what it can do
87
+ // (claude-xiy9e8wq:15), and the survey is one of the things it names.
88
+ await showUsage(absorbCommand);
89
+ return;
90
+ }
91
+ if (args.provider === "survey") {
92
+ try {
93
+ const endpoint = await resolveDaemonEndpoint();
94
+ warnIfVersionMismatch(endpoint);
95
+ const survey = await daemonRequest(endpoint, "/api/absorb/survey");
96
+ for (const l of formatSurvey(survey))
97
+ console.log(l);
98
+ }
99
+ catch (err) {
100
+ console.error(err instanceof Error ? err.message : String(err));
101
+ process.exit(1);
102
+ }
103
+ return;
104
+ }
105
+ const dryRun = args.dryRun;
106
+ const body = buildAbsorbBody({
107
+ provider: args.provider,
108
+ path: args.path,
109
+ fold: args.fold,
110
+ identity: args.identity,
111
+ brane: args.brane,
112
+ dryRun,
113
+ since: args.since,
114
+ session: args.session,
115
+ rebuild: args.rebuild,
116
+ }, process.cwd());
117
+ let failed = false;
118
+ let progressShown = false;
119
+ const clearProgress = () => {
120
+ if (!progressShown)
121
+ return;
122
+ process.stdout.write("\r" + " ".repeat(80) + "\r");
123
+ progressShown = false;
124
+ };
125
+ try {
126
+ const endpoint = await resolveDaemonEndpoint();
127
+ warnIfVersionMismatch(endpoint);
128
+ await daemonStream(endpoint, "/api/absorb", { method: "POST", body }, (line) => {
129
+ switch (line.type) {
130
+ case "info":
131
+ console.log(line.message);
132
+ break;
133
+ case "progress":
134
+ // A carriage-returned line only means something to a terminal;
135
+ // piped or captured (a `!` command in an agent session), every
136
+ // frame of it lands in the output ahead of the summary.
137
+ if (!process.stdout.isTTY)
138
+ break;
139
+ process.stdout.write(formatProgress(line.done, line.title, dryRun));
140
+ progressShown = true;
141
+ break;
142
+ case "summary":
143
+ clearProgress();
144
+ for (const l of formatSummary(line.summary, line.dry_run))
145
+ console.log(l);
146
+ if (line.dry_run) {
147
+ console.log(`\nRe-run without --dry-run to absorb these conversations.`);
148
+ }
149
+ else {
150
+ for (const l of nextSteps(line.provider))
151
+ console.log(l);
152
+ }
153
+ break;
154
+ case "error":
155
+ clearProgress();
156
+ console.error(line.error);
157
+ failed = true;
158
+ break;
159
+ }
160
+ });
161
+ }
162
+ catch (err) {
163
+ clearProgress();
164
+ console.error(err instanceof Error ? err.message : String(err));
165
+ process.exit(1);
166
+ }
167
+ if (failed)
168
+ process.exit(1);
169
+ },
170
+ });
171
+ /** The one-line, carriage-returned progress display. */
172
+ export function formatProgress(done, title, dryRun) {
173
+ const displayTitle = title
174
+ ? title.length > 50
175
+ ? title.slice(0, 47) + "..."
176
+ : title
177
+ : "(untitled)";
178
+ return `\r ${done} ${dryRun ? "scanned" : "absorbed"}: ${displayTitle}`.padEnd(80);
179
+ }
180
+ function formatBytes(bytes) {
181
+ if (bytes < 1024)
182
+ return `${bytes} B`;
183
+ if (bytes < 1024 * 1024)
184
+ return `${(bytes / 1024).toFixed(1)} KB`;
185
+ if (bytes < 1024 * 1024 * 1024)
186
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
187
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
188
+ }
189
+ export function formatSummary(summary, dryRun) {
190
+ const lines = [];
191
+ const verb = dryRun ? "Would absorb" : "Absorbed";
192
+ const total = summary.conversations + summary.skipped + summary.appended;
193
+ lines.push(`${verb}: ${summary.conversations} conversations, ${summary.messages} messages (${total} total parsed)`);
194
+ if (summary.appended > 0) {
195
+ const aVerb = dryRun ? "Would update" : "Updated";
196
+ lines.push(`${aVerb}: ${summary.appended} (new messages to append)`);
197
+ }
198
+ if (summary.skipped > 0) {
199
+ lines.push(`Skipped: ${summary.skipped} (already absorbed)`);
200
+ }
201
+ if (summary.toolCalls) {
202
+ lines.push(`Tool calls: ${summary.toolCalls}${dryRun ? " (would be recorded)" : ""}`);
203
+ }
204
+ if (summary.managed) {
205
+ lines.push(`Managed by brane: ${summary.managed} (live narratives own these sessions; left alone)`);
206
+ }
207
+ if (summary.unchanged) {
208
+ lines.push(`Unchanged: ${summary.unchanged} (not read; same size and mtime as last absorb)`);
209
+ }
210
+ if (summary.rebuilt) {
211
+ lines.push(`${dryRun ? "Would rebuild" : "Rebuilt"}: ${summary.rebuilt} (events re-derived from source; narrative ids kept, seqs renumbered)`);
212
+ }
213
+ if (summary.locked) {
214
+ lines.push(`${dryRun ? "Would not rebuild" : "Not rebuilt"}: ${summary.locked} (hold signals or narrations no import wrote; read as an ordinary re-run)`);
215
+ }
216
+ if (summary.assets > 0 || summary.unresolvedAssets > 0) {
217
+ const totalAssets = summary.assets + summary.unresolvedAssets;
218
+ const aVerb = dryRun ? "Would copy" : "Copied";
219
+ lines.push(`Assets: ${summary.assets}/${totalAssets} resolved (${formatBytes(summary.assetsBytes)})${dryRun ? ` — ${aVerb.toLowerCase()} to ~/.brane/payloads/` : ""}`);
220
+ if (summary.unresolvedAssets > 0) {
221
+ lines.push(` Unresolved: ${summary.unresolvedAssets} (files not included in export)`);
222
+ }
223
+ }
224
+ if (summary.oldest) {
225
+ lines.push(`Oldest: ${new Date(summary.oldest.time).toISOString().slice(0, 10)} ${JSON.stringify(summary.oldest.title)}`);
226
+ }
227
+ if (summary.newest) {
228
+ lines.push(`Newest: ${new Date(summary.newest.time).toISOString().slice(0, 10)} ${JSON.stringify(summary.newest.title)}`);
229
+ }
230
+ const modelEntries = Object.entries(summary.models).sort(([, a], [, b]) => b - a);
231
+ if (modelEntries.length > 0) {
232
+ const modelStr = modelEntries.map(([m, c]) => `${m} (${c})`).join(", ");
233
+ lines.push(`${summary.provider === "organelle" ? "Agent types" : "Models"}: ${modelStr}`);
234
+ }
235
+ return lines;
236
+ }
237
+ /** The bare `brane absorb` inventory: what this machine has, what an import
238
+ * would do with it, and what brane recognised but cannot read yet. */
239
+ export function formatSurvey(survey) {
240
+ const lines = [];
241
+ lines.push(`Session stores under ${survey.home}:`);
242
+ lines.push("");
243
+ const supported = survey.sources.filter((s) => s.supported);
244
+ const probes = survey.sources.filter((s) => !s.supported && s.found);
245
+ for (const s of supported) {
246
+ if (!s.found || !s.sessions) {
247
+ lines.push(` ${s.label.padEnd(36)} not found (${s.path})`);
248
+ continue;
249
+ }
250
+ const n = s.sessions;
251
+ const parts = [];
252
+ if (n.new > 0)
253
+ parts.push(`${n.new} new`);
254
+ if (n.changed > 0)
255
+ parts.push(`${n.changed} changed`);
256
+ if (n.reparse > 0)
257
+ parts.push(`${n.reparse} to re-read (newer parser)`);
258
+ if (n.unchanged > 0)
259
+ parts.push(`${n.unchanged} unchanged`);
260
+ if (n.managed > 0)
261
+ parts.push(`${n.managed} managed by brane`);
262
+ lines.push(` ${s.label.padEnd(36)} ${n.total} sessions, ${formatBytes(n.bytes)}, ${n.folds} director${n.folds === 1 ? "y" : "ies"} (${s.path})`);
263
+ if (parts.length > 0)
264
+ lines.push(` ${"".padEnd(36)} ${parts.join(", ")}`);
265
+ if (n.oldest !== null && n.newest !== null) {
266
+ lines.push(` ${"".padEnd(36)} ${new Date(n.oldest).toISOString().slice(0, 10)} … ${new Date(n.newest).toISOString().slice(0, 10)}`);
267
+ }
268
+ if (n.new + n.changed + n.reparse > 0) {
269
+ lines.push(` ${"".padEnd(36)} brane absorb ${s.name} --dry-run`);
270
+ }
271
+ }
272
+ if (probes.length > 0) {
273
+ lines.push("");
274
+ lines.push("Found, no extractor yet:");
275
+ for (const s of probes)
276
+ lines.push(` ${s.label.padEnd(36)} ${s.path}`);
277
+ }
278
+ lines.push("");
279
+ lines.push("Nothing was read or imported. An import reads the store and writes ~/.brane/brane.db; nothing leaves this machine.");
280
+ return lines;
281
+ }
282
+ function nextSteps(provider) {
283
+ return [
284
+ "",
285
+ "Next:",
286
+ ` brane meld View in the app`,
287
+ ` brane memory index Index for semantic search`,
288
+ ` brane memory search "that thing we discussed" Search your ${provider} history`,
289
+ ];
290
+ }
291
+ //# sourceMappingURL=absorb.js.map
@@ -0,0 +1,31 @@
1
+ import { defineCommand } from "citty";
2
+ import { execFileSync } from "node:child_process";
3
+ import { resolveTmux } from "../tmux.js";
4
+ export const attachCommand = defineCommand({
5
+ meta: {
6
+ name: "attach",
7
+ description: "Attach to a tmux session",
8
+ },
9
+ args: {
10
+ id: {
11
+ type: "positional",
12
+ required: true,
13
+ description: "Session/narrative ID to attach to",
14
+ },
15
+ },
16
+ run({ args }) {
17
+ // Check session exists
18
+ try {
19
+ execFileSync(resolveTmux(), ["has-session", "-t", args.id], { stdio: "ignore" });
20
+ }
21
+ catch {
22
+ console.error(`No tmux session found: ${args.id}`);
23
+ process.exit(1);
24
+ }
25
+ // Replace current process with tmux attach
26
+ execFileSync(resolveTmux(), ["attach-session", "-t", args.id], {
27
+ stdio: "inherit",
28
+ });
29
+ },
30
+ });
31
+ //# sourceMappingURL=attach.js.map