brane 0.1.1 → 0.3.1

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 (352) hide show
  1. package/AGENTS.md +367 -0
  2. package/CHANGELOG.md +66 -0
  3. package/CONTRIBUTING.md +101 -0
  4. package/DESIGN.md +405 -0
  5. package/Dockerfile +61 -0
  6. package/LICENSE +656 -17
  7. package/README.md +174 -2
  8. package/SECURITY.md +213 -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 +133 -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 +360 -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 +1101 -0
  37. package/dist/commands/token.js +154 -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 +6353 -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 +643 -0
  58. package/dist/db/tokens.js +624 -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/app-origin.js +26 -0
  96. package/dist/shared/block-glyphs.js +114 -0
  97. package/dist/shared/diagram-css.js +17 -0
  98. package/dist/shared/export-selection.js +101 -0
  99. package/dist/shared/find-in-text.js +110 -0
  100. package/dist/shared/glow-hues.js +225 -0
  101. package/dist/shared/hex-dump.js +62 -0
  102. package/dist/shared/hex-pages.js +52 -0
  103. package/dist/shared/hit-drum.js +79 -0
  104. package/dist/shared/known-nids.js +58 -0
  105. package/dist/shared/list-scroll-keys.js +56 -0
  106. package/dist/shared/markdown-lists.js +72 -0
  107. package/dist/shared/markdown-math.js +485 -0
  108. package/dist/shared/markdown-mentions.js +79 -0
  109. package/dist/shared/markdown-nids.js +41 -0
  110. package/dist/shared/markdown-protocols.js +53 -0
  111. package/dist/shared/markdown-refs.js +123 -0
  112. package/dist/shared/membrane-invasion.js +342 -0
  113. package/dist/shared/membrane-motion.js +83 -0
  114. package/dist/shared/membrane-palette.js +253 -0
  115. package/dist/shared/membrane-terrain.js +212 -0
  116. package/dist/shared/message-selection.js +123 -0
  117. package/dist/shared/mo-glyph.js +57 -0
  118. package/dist/shared/narrative-format.js +237 -0
  119. package/dist/shared/narrative-menu.js +84 -0
  120. package/dist/shared/narrative-sort.js +47 -0
  121. package/dist/shared/narrative-status.js +52 -0
  122. package/dist/shared/no-pan.js +85 -0
  123. package/dist/shared/pane-layout.js +50 -0
  124. package/dist/shared/payload-inline.js +83 -0
  125. package/dist/shared/speaker-color.js +61 -0
  126. package/dist/shared/token-rotation.js +14 -0
  127. package/dist/shared/view-url.js +216 -0
  128. package/dist/shared/wiki-links.js +70 -0
  129. package/dist/shared/wiki-window.js +4 -0
  130. package/dist/tailscale.js +55 -0
  131. package/dist/test/absorb-claude.test.js +892 -0
  132. package/dist/test/agent-scope.test.js +193 -0
  133. package/dist/test/agents.test.js +55 -0
  134. package/dist/test/api-request.test.js +122 -0
  135. package/dist/test/app-host-literal.test.js +53 -0
  136. package/dist/test/awaiting-glow.test.js +54 -0
  137. package/dist/test/awaiting.test.js +122 -0
  138. package/dist/test/bind-token.test.js +88 -0
  139. package/dist/test/block-glyphs.test.js +68 -0
  140. package/dist/test/cert-manager.test.js +262 -0
  141. package/dist/test/claude-live-session.test.js +110 -0
  142. package/dist/test/codex-tool-log.test.js +568 -0
  143. package/dist/test/config.test.js +71 -0
  144. package/dist/test/copilot-tool-log.test.js +93 -0
  145. package/dist/test/cors-preflight.test.js +56 -0
  146. package/dist/test/cost-linger.test.js +31 -0
  147. package/dist/test/cost-pricing.test.js +72 -0
  148. package/dist/test/cost-reprice.test.js +139 -0
  149. package/dist/test/cost-sessions.test.js +301 -0
  150. package/dist/test/crush-tool-log.test.js +289 -0
  151. package/dist/test/daemon-client.test.js +109 -0
  152. package/dist/test/daemon-socket.test.js +293 -0
  153. package/dist/test/diagram-arrows.test.js +205 -0
  154. package/dist/test/diagram-export.test.js +210 -0
  155. package/dist/test/diagram-labels.test.js +313 -0
  156. package/dist/test/diagram-parse.test.js +191 -0
  157. package/dist/test/diagram-render.test.js +175 -0
  158. package/dist/test/diagram-review.test.js +288 -0
  159. package/dist/test/diagram-self-loop-labels.test.js +303 -0
  160. package/dist/test/embedder.test.js +84 -0
  161. package/dist/test/events.test.js +123 -0
  162. package/dist/test/export-selection.test.js +102 -0
  163. package/dist/test/export.test.js +505 -0
  164. package/dist/test/files-browse.test.js +393 -0
  165. package/dist/test/files-find.test.js +272 -0
  166. package/dist/test/find-in-text.test.js +86 -0
  167. package/dist/test/first-paint-viewport.test.js +59 -0
  168. package/dist/test/fixtures/embedding-golden.js +23 -0
  169. package/dist/test/fixtures/wordpiece-golden.js +167 -0
  170. package/dist/test/glow-hues.test.js +202 -0
  171. package/dist/test/harness-cost.test.js +423 -0
  172. package/dist/test/helpers.js +21 -0
  173. package/dist/test/hex-pages.test.js +68 -0
  174. package/dist/test/highlight.test.js +69 -0
  175. package/dist/test/hit-drum.test.js +71 -0
  176. package/dist/test/hook-config.test.js +299 -0
  177. package/dist/test/json-gzip.test.js +70 -0
  178. package/dist/test/key-routing.test.js +58 -0
  179. package/dist/test/known-nids.test.js +106 -0
  180. package/dist/test/link-preview.test.js +67 -0
  181. package/dist/test/list-scroll-keys.test.js +66 -0
  182. package/dist/test/listen-own-words.test.js +147 -0
  183. package/dist/test/listen-timeout.test.js +33 -0
  184. package/dist/test/markdown-diagnostics.test.js +121 -0
  185. package/dist/test/markdown-lists.test.js +182 -0
  186. package/dist/test/markdown-math.test.js +256 -0
  187. package/dist/test/markdown-nids.test.js +146 -0
  188. package/dist/test/markdown-protocols.test.js +139 -0
  189. package/dist/test/mcp-bridge-cancel.test.js +98 -0
  190. package/dist/test/mcp-bridge-reconnect.test.js +121 -0
  191. package/dist/test/mcp-narrative.test.js +193 -0
  192. package/dist/test/mcp.test.js +548 -0
  193. package/dist/test/meld.test.js +143 -0
  194. package/dist/test/membrane-invasion.test.js +482 -0
  195. package/dist/test/membrane-motion.test.js +101 -0
  196. package/dist/test/membrane-palette.test.js +166 -0
  197. package/dist/test/membrane-terrain.test.js +170 -0
  198. package/dist/test/memory-log.test.js +202 -0
  199. package/dist/test/mentions-e2e.test.js +261 -0
  200. package/dist/test/mentions-ledger.test.js +265 -0
  201. package/dist/test/mentions-listen.test.js +265 -0
  202. package/dist/test/mentions.test.js +238 -0
  203. package/dist/test/message-selection.test.js +114 -0
  204. package/dist/test/mo-glyph.test.js +22 -0
  205. package/dist/test/model-resolution.test.js +166 -0
  206. package/dist/test/narrate-validate.test.js +77 -0
  207. package/dist/test/narrate-wait.test.js +160 -0
  208. package/dist/test/narrative-format.test.js +219 -0
  209. package/dist/test/narrative-menu.test.js +150 -0
  210. package/dist/test/narrative-sort.test.js +75 -0
  211. package/dist/test/no-pan.test.js +60 -0
  212. package/dist/test/pane-layout.test.js +153 -0
  213. package/dist/test/paths.test.js +37 -0
  214. package/dist/test/payload-inline.test.js +91 -0
  215. package/dist/test/precache-fonts.test.js +72 -0
  216. package/dist/test/presence-gate.test.js +440 -0
  217. package/dist/test/presence.test.js +160 -0
  218. package/dist/test/read-events.test.js +220 -0
  219. package/dist/test/remote-meld.test.js +128 -0
  220. package/dist/test/revive-cli.test.js +93 -0
  221. package/dist/test/search-floor.test.js +206 -0
  222. package/dist/test/search-fusion.test.js +168 -0
  223. package/dist/test/search.test.js +205 -0
  224. package/dist/test/security.test.js +48 -0
  225. package/dist/test/selection-wiring.test.js +61 -0
  226. package/dist/test/session-affinity.test.js +295 -0
  227. package/dist/test/signal-source.test.js +32 -0
  228. package/dist/test/socket-account.test.js +268 -0
  229. package/dist/test/spawn-toml.test.js +72 -0
  230. package/dist/test/speaker-color.test.js +90 -0
  231. package/dist/test/tailscale-rebind.test.js +79 -0
  232. package/dist/test/terminal-cursor.test.js +61 -0
  233. package/dist/test/terminal-resync.test.js +23 -0
  234. package/dist/test/terminal-size.test.js +36 -0
  235. package/dist/test/thin-cli.test.js +382 -0
  236. package/dist/test/token-bootstrap.test.js +272 -0
  237. package/dist/test/token-devices.test.js +646 -0
  238. package/dist/test/token-lapse.test.js +94 -0
  239. package/dist/test/token-ring.test.js +188 -0
  240. package/dist/test/token-rotate-cli.test.js +58 -0
  241. package/dist/test/token-routes.test.js +95 -0
  242. package/dist/test/tool-uses.test.js +1006 -0
  243. package/dist/test/usage-ledger.test.js +126 -0
  244. package/dist/test/view-url.test.js +627 -0
  245. package/dist/test/wiki-links.test.js +57 -0
  246. package/dist/test/wiki-sources.test.js +85 -0
  247. package/dist/test/wiki-window.test.js +31 -0
  248. package/dist/test/wiki.test.js +399 -0
  249. package/dist/test/wordpiece.test.js +68 -0
  250. package/dist/test/wrangler-route.test.js +27 -0
  251. package/dist/tls.js +148 -0
  252. package/dist/tmux.js +213 -0
  253. package/dist/user-agent.js +46 -0
  254. package/docker-entrypoint.sh +154 -0
  255. package/docs/README.md +3 -0
  256. package/docs/absorb.md +231 -0
  257. package/docs/cost-ledger.md +125 -0
  258. package/docs/daemon-api.md +189 -0
  259. package/docs/diagrams.md +163 -0
  260. package/docs/doctor.md +36 -0
  261. package/docs/export-scope.md +69 -0
  262. package/docs/files-view.md +133 -0
  263. package/docs/link-previews.md +38 -0
  264. package/docs/math.md +32 -0
  265. package/docs/mcp.md +69 -0
  266. package/docs/memory-search.md +75 -0
  267. package/docs/mentions.md +101 -0
  268. package/docs/multi-brane.md +14 -0
  269. package/docs/passkeys.md +165 -0
  270. package/docs/presence.md +202 -0
  271. package/docs/releasing.md +104 -0
  272. package/docs/rendering-warnings.md +22 -0
  273. package/docs/request-layer.md +72 -0
  274. package/docs/selection-store.md +50 -0
  275. package/docs/session-affinity.md +61 -0
  276. package/docs/terminal-mirroring.md +243 -0
  277. package/docs/token-rotation.md +431 -0
  278. package/docs/view-urls.md +134 -0
  279. package/docs/web-app.md +23 -0
  280. package/docs/wiki.md +43 -0
  281. package/expo/dist/_expo/static/js/web/entry-e94247f119fdcd3b424a1764233a41d2.js +1099 -0
  282. package/expo/dist/apple-touch-icon.png +0 -0
  283. package/expo/dist/favicon.svg +3 -0
  284. package/expo/dist/index.html +177 -0
  285. package/expo/dist/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  286. package/expo/dist/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  287. package/expo/dist/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  288. package/expo/dist/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  289. package/expo/dist/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  290. package/expo/dist/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
  291. package/expo/dist/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  292. package/expo/dist/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
  293. package/expo/dist/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
  294. package/expo/dist/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  295. package/expo/dist/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
  296. package/expo/dist/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  297. package/expo/dist/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  298. package/expo/dist/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  299. package/expo/dist/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
  300. package/expo/dist/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  301. package/expo/dist/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  302. package/expo/dist/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  303. package/expo/dist/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  304. package/expo/dist/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  305. package/expo/dist/katex/katex.min.css +1 -0
  306. package/expo/dist/manifest.json +16 -0
  307. package/expo/dist/metadata.json +1 -0
  308. package/expo/dist/og/membrane.v1.png +0 -0
  309. package/expo/dist/pdf.min.mjs +28 -0
  310. package/expo/dist/pdf.worker.min.mjs +28 -0
  311. package/expo/dist/sw.js +154 -0
  312. package/package.json +79 -27
  313. package/protocols/LIVE_loop.md +407 -0
  314. package/protocols/bebrief.md +41 -0
  315. package/protocols/digest.md +231 -0
  316. package/protocols/fly.md +266 -0
  317. package/protocols/idle.md +81 -0
  318. package/protocols/keeper.md +829 -0
  319. package/protocols/markdown.md +80 -0
  320. package/protocols/memory.md +419 -0
  321. package/protocols/mentions.md +93 -0
  322. package/protocols/payloads.md +53 -0
  323. package/protocols/prelude.md +91 -0
  324. package/protocols/ramify.md +105 -0
  325. package/protocols/review.md +356 -0
  326. package/protocols/rrr.md +232 -0
  327. package/protocols/scribe.md +429 -0
  328. package/protocols/summarize.md +203 -0
  329. package/protocols/twin.md +174 -0
  330. package/protocols/wiki.md +438 -0
  331. package/protocols/worker.md +38 -0
  332. package/wiki/shared/append-only-event-log.md +148 -0
  333. package/wiki/shared/index.md +53 -0
  334. package/wiki/shared/owning-your-narratives.md +161 -0
  335. package/wiki/shared/premature-ontologies.md +435 -0
  336. package/wiki/shared/sedimentary-superintelligence.md +128 -0
  337. package/wiki/shared/sequestering-the-narrative.md +143 -0
  338. package/wiki/shared/why-wikis-work.md +216 -0
  339. package/.travis.yml +0 -7
  340. package/lib/brane.cjs.js +0 -12
  341. package/lib/brane.cjs.js.map +0 -1
  342. package/lib/brane.d.ts +0 -11
  343. package/lib/brane.js +0 -6
  344. package/lib/brane.js.map +0 -1
  345. package/lib/tests.d.ts +0 -1
  346. package/lib/tests.js +0 -8
  347. package/lib/tests.js.map +0 -1
  348. package/rollup.config.js +0 -21
  349. package/scripts/test.sh +0 -8
  350. package/src/brane.ts +0 -21
  351. package/src/tests.ts +0 -8
  352. package/tsconfig.json +0 -16
@@ -0,0 +1,360 @@
1
+ import { defineCommand } from "citty";
2
+ import { spawnSync } from "node:child_process";
3
+ import qrcode from "qrcode-terminal";
4
+ import { getDaemonAddress, openDatabase } from "../db/schema.js";
5
+ import { getTailscaleHost } from "../tailscale.js";
6
+ import { connectionFragment } from "../shared/view-url.js";
7
+ import { CONFIG_DEFAULTS, getConfig, resolveAppHost } from "./config.js";
8
+ import { cleanAppHost } from "../shared/app-origin.js";
9
+ import { UNUSED_TOKEN_LAPSE_MS } from "../daemon/token-ring.js";
10
+ import { daemonRequest, resolveDaemonEndpoint, warnIfVersionMismatch, } from "../daemon/client.js";
11
+ /** What the CLI prints before a request the daemon holds for the presence
12
+ * check (src/daemon/presence-gate.ts). The daemon is on this machine, so
13
+ * this process's platform names its prompt. On stderr, so it never lands in
14
+ * output a script captures. */
15
+ export function presenceWaitNote(platform = process.platform) {
16
+ const prompt = platform === "darwin" ? "Touch ID (or your login password)" : platform === "win32" ? "Windows Hello" : "the system's authentication prompt";
17
+ return `Confirm with ${prompt} on this machine to continue…`;
18
+ }
19
+ /** Print `presenceWaitNote()` before a gated request, unless this is an
20
+ * agent's session: the daemon refuses an agent unasked, so there is nothing
21
+ * to wait for. */
22
+ export function announcePresenceWait(env = process.env) {
23
+ if (!env.BRANE_NARRATIVE_SECRET)
24
+ console.error(presenceWaitNote());
25
+ }
26
+ /** Ask the daemon for a fresh device token. The daemon keeps only its hash,
27
+ * so the QR this goes into is the one and only copy — every scan of every
28
+ * meld is its own device, revocable alone. From this machine the daemon
29
+ * first asks the operator to confirm at the OS prompt. */
30
+ export async function mintDeviceToken(endpoint, label) {
31
+ announcePresenceWait();
32
+ const { token } = await daemonRequest(endpoint, "/api/tokens", {
33
+ method: "POST",
34
+ body: label ? { label } : {},
35
+ });
36
+ return token;
37
+ }
38
+ /** What `brane meld` and `brane spawn --meld` print under the QR. */
39
+ export function meldLapseNote() {
40
+ return `This QR is valid for ${UNUSED_TOKEN_LAPSE_MS / 60_000} minutes; once scanned, the device stays signed in until revoked.`;
41
+ }
42
+ /** Build a meld URL pointing at the daemon (optionally targeting a specific
43
+ * in-app path). Shared by `brane meld` and `brane spawn --meld`. */
44
+ export function buildMeldUrl(opts) {
45
+ const { db, daemon } = opts;
46
+ const { port, token } = daemon;
47
+ const tailscaleHost = "tailscaleHost" in opts ? opts.tailscaleHost : getTailscaleHost();
48
+ const host = opts.host ?? tailscaleHost ?? "127.0.0.1";
49
+ const isLoopbackFallback = !opts.host && !tailscaleHost && host === "127.0.0.1";
50
+ const tlsRow = db.prepare("SELECT value FROM meta WHERE key = 'daemon_tls'").get();
51
+ const tls = tlsRow?.value === "true";
52
+ const useTls = tls && host === tailscaleHost;
53
+ const useLocal = opts.local === true || !useTls;
54
+ let effectivePort = port;
55
+ if (useLocal && tls && !useTls) {
56
+ const httpPortRow = db.prepare("SELECT value FROM meta WHERE key = 'daemon_http_port'").get();
57
+ if (httpPortRow)
58
+ effectivePort = httpPortRow.value;
59
+ }
60
+ const scheme = useTls ? "https" : "http";
61
+ const portSuffix = useTls && effectivePort === "443" ? "" : `:${effectivePort}`;
62
+ const daemonHost = `${host}${portSuffix}`;
63
+ // Tailscale is present and we'd advertise its hostname, but there's no cert,
64
+ // so we're silently emitting plain HTTP instead of the expected HTTPS.
65
+ const tlsDegraded = !!tailscaleHost && host === tailscaleHost && !tls && opts.local !== true;
66
+ let path = opts.path ?? "/";
67
+ if (!path.startsWith("/"))
68
+ path = "/" + path;
69
+ // Where the shared app lives is config, never a literal: a fork sets its
70
+ // own default (CONFIG_DEFAULTS), an operator sets `app_domain`, and a
71
+ // one-off `--app` wins over both. The daemon trusts none of them — the
72
+ // token is the credential — so this only decides which copy of the app
73
+ // the phone loads.
74
+ const appHost = resolveAppHost(opts.app ?? getConfig("app_domain", db));
75
+ const url = meldLink({ daemonHost, scheme, useLocal, appHost }, token, path);
76
+ return { url, daemonHost, scheme, useLocal, appHost, isLoopbackFallback, tlsDegraded };
77
+ }
78
+ /** A meld URL for `token`, at the daemon `parts` describes. */
79
+ export function meldLink(parts, token, path = "/") {
80
+ // The daemon's address and token ride in the URL fragment, which the
81
+ // browser never sends to any server — so the shared SPA's host (Cloudflare)
82
+ // does not see the token, a pasted link's unfurler does not fetch it, and
83
+ // the daemon's own request log does not record it. The app reads it from
84
+ // `location.hash` and strips it once it has been used.
85
+ const conn = { host: parts.daemonHost, token };
86
+ // A remote brane from before `appHost` was part of the connection reply
87
+ // (readMeldReply) names no shell; this install's default stands in.
88
+ const appHost = parts.appHost ?? CONFIG_DEFAULTS.app_domain;
89
+ return parts.useLocal
90
+ ? `${parts.scheme}://${parts.daemonHost}${path}${connectionFragment(conn, false)}`
91
+ : `https://${appHost}${path}${connectionFragment(conn, true)}`;
92
+ }
93
+ /** Ask this machine's daemon for a device token for a daemon on another
94
+ * machine, and its hash. The daemon stores neither, and first asks the
95
+ * operator to confirm at the OS prompt, as it does for `brane meld`. */
96
+ export async function mintRemoteDeviceToken(endpoint) {
97
+ announcePresenceWait();
98
+ return daemonRequest(endpoint, "/api/tokens/remote", { method: "POST", body: {} });
99
+ }
100
+ /** What one run of the `--ask` command told us. `output` is its stdout, or
101
+ * null when the command itself failed (machine not reachable yet). Pure, so
102
+ * the suite pins every case:
103
+ *
104
+ * - `address`: the daemon is up and reachable over Tailscale;
105
+ * - `wait`: not yet — no answer, no daemon, or no Tailscale name yet (a QR
106
+ * for 127.0.0.1 would be useless, and a wrong QR costs a restart);
107
+ * - `old`: the remote answered without the JSON line, so its brane
108
+ * predates `--connection` and ran a plain meld instead. */
109
+ export function readMeldReply(output) {
110
+ if (output === null)
111
+ return { kind: "wait", reason: "the remote machine is not answering yet" };
112
+ for (const line of output.split(/\r?\n/).reverse()) {
113
+ const trimmed = line.trim();
114
+ if (!trimmed.startsWith("{"))
115
+ continue;
116
+ let r;
117
+ try {
118
+ r = JSON.parse(trimmed);
119
+ }
120
+ catch {
121
+ continue;
122
+ }
123
+ if (r.ready === false)
124
+ return { kind: "wait", reason: typeof r.reason === "string" ? r.reason : "not ready" };
125
+ if (r.ready !== true)
126
+ continue;
127
+ if (typeof r.daemonHost !== "string" || (r.scheme !== "http" && r.scheme !== "https") || typeof r.useLocal !== "boolean")
128
+ continue;
129
+ if (r.isLoopbackFallback)
130
+ return { kind: "wait", reason: "the remote machine has no Tailscale name yet" };
131
+ const { ready: _ready, appHost, ...connection } = r;
132
+ // A remote that names its shell does so as a string; anything else is
133
+ // treated as unnamed, and meldLink falls back to this install's default.
134
+ return {
135
+ kind: "address",
136
+ connection: {
137
+ ...connection,
138
+ ...(typeof appHost === "string" && appHost ? { appHost: cleanAppHost(appHost) } : {}),
139
+ },
140
+ };
141
+ }
142
+ return { kind: "old" };
143
+ }
144
+ /** The `--send` command with the hash in place of `{hash}`. A hash is 64 hex
145
+ * digits, so it needs no quoting in a shell command. */
146
+ const fillHashError = "--send must contain {hash}, where the token's hash goes";
147
+ export function fillHash(template, hash) {
148
+ if (!template.includes("{hash}"))
149
+ throw new Error(fillHashError);
150
+ return template.replaceAll("{hash}", hash);
151
+ }
152
+ /** How long `brane meld --remote` waits for the remote daemon's address. */
153
+ const REMOTE_MELD_WAIT_MS = 3 * 60_000;
154
+ /** `--send` waits for whatever its command waits for (`fly secrets set` waits
155
+ * for the machines to restart), which can outlast a couple of minutes. */
156
+ const REMOTE_SEND_TIMEOUT_MS = 10 * 60_000;
157
+ /** One `--ask` try; a slow `fly ssh` connect is retried, not waited out. */
158
+ const REMOTE_ASK_TIMEOUT_MS = 60_000;
159
+ /** Why `brane meld --remote` won't run here, or null if it will. The token is
160
+ * printed into the QR and the URL; in an agent's session that output is the
161
+ * agent's to read, and a device token is the operator's. This only saves a
162
+ * round trip: the gate is the daemon's, which refuses an agent's token and
163
+ * asks the shell for presence (`POST /api/tokens/remote`). */
164
+ export function refusesRemoteMeld(env = process.env) {
165
+ return env.BRANE_NARRATIVE_SECRET
166
+ ? "brane meld --remote prints a device token; run it from a shell outside an agent session."
167
+ : null;
168
+ }
169
+ /** `brane meld --remote`: meld a device to a daemon on a machine that can
170
+ * show no presence prompt (a Fly machine, a VPS). The token is minted by
171
+ * this machine's daemon, behind its presence prompt; `--send` hands the
172
+ * remote only its hash, and `--ask` reads back only its address. Nothing
173
+ * here knows which host it is. */
174
+ async function meldRemote(args) {
175
+ const refusal = refusesRemoteMeld();
176
+ if (refusal) {
177
+ console.error(refusal);
178
+ process.exit(1);
179
+ }
180
+ if (!args.send || !args.ask) {
181
+ console.error("brane meld --remote needs --send '<command with {hash}>' and --ask '<command that runs brane meld --connection there>'.");
182
+ process.exit(1);
183
+ }
184
+ // Before the prompt, so a bad --send never asks the operator anything.
185
+ if (!args.send.includes("{hash}")) {
186
+ console.error(fillHashError);
187
+ process.exit(1);
188
+ }
189
+ let token;
190
+ let send;
191
+ try {
192
+ const endpoint = await resolveDaemonEndpoint();
193
+ warnIfVersionMismatch(endpoint);
194
+ const minted = await mintRemoteDeviceToken(endpoint);
195
+ token = minted.token;
196
+ send = fillHash(args.send, minted.hash);
197
+ }
198
+ catch (err) {
199
+ console.error(`Failed to mint a device token: ${err.message}`);
200
+ process.exit(1);
201
+ }
202
+ console.log("Sending the remote daemon the new device token's hash...");
203
+ const sent = spawnSync("sh", ["-c", send], { stdio: "inherit", timeout: REMOTE_SEND_TIMEOUT_MS });
204
+ if (sent.status !== 0) {
205
+ console.error(`--send failed (${sent.error?.message ?? `exit ${sent.status}`}); nothing was shown, so nothing is lost.`);
206
+ process.exit(1);
207
+ }
208
+ console.log("Waiting for the remote daemon...");
209
+ const deadline = Date.now() + REMOTE_MELD_WAIT_MS;
210
+ let connection = null;
211
+ let waitingFor = "the remote daemon";
212
+ // The last failed try's stderr, shown if we give up: the difference between
213
+ // a wrong app name, an ssh login and a daemon that never came up.
214
+ let lastError = "";
215
+ while (!connection && Date.now() < deadline) {
216
+ const asked = spawnSync("sh", ["-c", args.ask], { stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8", timeout: REMOTE_ASK_TIMEOUT_MS });
217
+ if (asked.status !== 0)
218
+ lastError = (asked.stderr ?? "").trim() || (asked.error?.message ?? `exit ${asked.status}`);
219
+ const reply = readMeldReply(asked.status === 0 ? asked.stdout : null);
220
+ if (reply.kind === "old") {
221
+ console.error("The remote runs a brane that predates `brane meld --connection`. Update it, then run this again.");
222
+ process.exit(1);
223
+ }
224
+ if (reply.kind === "address")
225
+ connection = reply.connection;
226
+ else {
227
+ waitingFor = reply.reason;
228
+ await new Promise((r) => setTimeout(r, 5_000));
229
+ }
230
+ }
231
+ if (!connection) {
232
+ console.error(`Gave up waiting: ${waitingFor}.`);
233
+ if (lastError)
234
+ console.error(`The last --ask failed with:\n${lastError.split("\n").slice(-10).join("\n")}`);
235
+ console.error("Run this again once it's up; the token sent this time was never shown, so nothing is lost.");
236
+ process.exit(1);
237
+ }
238
+ // The remote daemon's `app_domain` names the shell, unless --app does.
239
+ const link = args.app ? { ...connection, appHost: cleanAppHost(args.app) } : connection;
240
+ const url = meldLink(link, token);
241
+ console.log(`\nScan this QR code to meld:\n`);
242
+ qrcode.generate(url, { small: args.small ?? true }, (code) => {
243
+ console.log(code);
244
+ });
245
+ console.log(`\n${url}\n`);
246
+ console.log(`Daemon: ${connection.daemonHost} (${connection.scheme})`);
247
+ if (!connection.useLocal)
248
+ console.log(`App: ${link.appHost ?? CONFIG_DEFAULTS.app_domain}`);
249
+ // The clock started with the remote daemon's restart, not now.
250
+ console.log(`This QR is valid for ${UNUSED_TOKEN_LAPSE_MS / 60_000} minutes from the remote daemon's start; once scanned, the device stays signed in until revoked.`);
251
+ if (connection.tlsDegraded) {
252
+ console.log("\nWarning: the remote daemon has no TLS cert, so this QR uses plain HTTP.");
253
+ }
254
+ }
255
+ export const meldCommand = defineCommand({
256
+ meta: {
257
+ name: "meld",
258
+ description: "Generate a QR code to connect a device to this brane",
259
+ },
260
+ args: {
261
+ host: {
262
+ type: "string",
263
+ description: "Override the host (default: Tailscale DNS > 127.0.0.1)",
264
+ },
265
+ small: {
266
+ type: "boolean",
267
+ description: "Use small QR code (half-block characters)",
268
+ },
269
+ local: {
270
+ type: "boolean",
271
+ description: "Generate a direct daemon URL instead of the shared app's (see --app)",
272
+ },
273
+ app: {
274
+ type: "string",
275
+ description: "The shared app's host to point the QR at (default: the `app_domain` config, brane.organelle.dev unless set)",
276
+ },
277
+ connection: {
278
+ type: "boolean",
279
+ description: "Print where a meld URL would point, as JSON, and mint nothing (what `--ask` runs on the remote machine)",
280
+ },
281
+ remote: {
282
+ type: "boolean",
283
+ description: "Meld to a daemon on another machine: mint the token here, hand it only the hash (--send), read back its address (--ask)",
284
+ },
285
+ send: {
286
+ type: "string",
287
+ description: "With --remote: a shell command that gives the remote daemon BRANE_DEVICE_TOKEN_SHA256={hash} and restarts it",
288
+ },
289
+ ask: {
290
+ type: "string",
291
+ description: "With --remote: a shell command that runs `brane meld --connection` on the remote machine",
292
+ },
293
+ },
294
+ async run({ args }) {
295
+ if (args.remote) {
296
+ await meldRemote(args);
297
+ return;
298
+ }
299
+ if (args.connection) {
300
+ // No token, so no presence check: an address is not a secret. Never
301
+ // starts a daemon — over `fly ssh` it would die with the session. Always
302
+ // one JSON line on stdout and exit 0, so the caller reads one channel
303
+ // and depends on nothing about how ssh passes on exit codes.
304
+ const db = openDatabase();
305
+ try {
306
+ const address = getDaemonAddress(db);
307
+ const reply = address
308
+ ? (({ url: _url, ...connection }) => ({ ready: true, ...connection }))(buildMeldUrl({ db, daemon: { port: address.port, token: "" }, host: args.host, local: args.local, app: args.app }))
309
+ : { ready: false, reason: "no daemon is running yet" };
310
+ console.log(JSON.stringify(reply));
311
+ }
312
+ finally {
313
+ db.close();
314
+ }
315
+ return;
316
+ }
317
+ let endpoint;
318
+ let token;
319
+ try {
320
+ endpoint = await resolveDaemonEndpoint();
321
+ warnIfVersionMismatch(endpoint);
322
+ token = await mintDeviceToken(endpoint);
323
+ }
324
+ catch (err) {
325
+ console.error(`Failed to mint a device token: ${err.message}`);
326
+ process.exit(1);
327
+ }
328
+ const daemon = { port: endpoint.port, token };
329
+ const db = openDatabase();
330
+ const { url, daemonHost, scheme, useLocal, appHost, isLoopbackFallback, tlsDegraded } = buildMeldUrl({
331
+ db,
332
+ daemon,
333
+ host: args.host,
334
+ local: args.local,
335
+ app: args.app,
336
+ });
337
+ db.close();
338
+ console.log(`\nScan this QR code to meld:\n`);
339
+ qrcode.generate(url, { small: args.small ?? true }, (code) => {
340
+ console.log(code);
341
+ });
342
+ console.log(`\n${url}\n`);
343
+ console.log(`Daemon: ${daemonHost} (${scheme})`);
344
+ if (!useLocal) {
345
+ console.log(`App: ${appHost}`);
346
+ }
347
+ console.log(meldLapseNote());
348
+ if (isLoopbackFallback) {
349
+ console.log("\nNo Tailscale detected. This QR points at 127.0.0.1 and will only work from this machine.");
350
+ console.log("For cross-device access, install Tailscale and run `brane certify`,");
351
+ console.log("or pass `--host <address>` to advertise an address you trust.");
352
+ }
353
+ else if (tlsDegraded) {
354
+ console.log("\nWarning: Tailscale is up but the daemon has no TLS cert, so this QR uses plain HTTP.");
355
+ console.log("Run `brane certify` to provision one — it'll tell you if \"HTTPS for your tailnet\"");
356
+ console.log("needs enabling at https://login.tailscale.com/admin/dns.");
357
+ }
358
+ },
359
+ });
360
+ //# sourceMappingURL=meld.js.map
@@ -0,0 +1,215 @@
1
+ import { resolve } from "node:path";
2
+ import { defineCommand } from "citty";
3
+ import { resolveDaemonEndpoint, daemonRequest, warnIfVersionMismatch } from "../daemon/client.js";
4
+ /** Parse a duration string like "1h", "2d", "30m", "1w" into milliseconds. */
5
+ export function parseDuration(input) {
6
+ const match = input.match(/^(\d+)\s*(ms|s|m|h|d|w)$/);
7
+ if (!match)
8
+ throw new Error(`Invalid duration: "${input}". Use e.g. 30m, 1h, 2d, 1w`);
9
+ const value = parseInt(match[1], 10);
10
+ const unit = match[2];
11
+ const multipliers = {
12
+ ms: 1,
13
+ s: 1000,
14
+ m: 60 * 1000,
15
+ h: 60 * 60 * 1000,
16
+ d: 24 * 60 * 60 * 1000,
17
+ w: 7 * 24 * 60 * 60 * 1000,
18
+ };
19
+ return value * multipliers[unit];
20
+ }
21
+ /** Format a timestamp as a relative time string like "3m ago". */
22
+ export function timeSince(timestampMs, now = Date.now()) {
23
+ const seconds = Math.floor((now - timestampMs) / 1000);
24
+ if (seconds < 60)
25
+ return `${seconds}s ago`;
26
+ const minutes = Math.floor(seconds / 60);
27
+ if (minutes < 60)
28
+ return `${minutes}m ago`;
29
+ const hours = Math.floor(minutes / 60);
30
+ if (hours < 24)
31
+ return `${hours}h ago`;
32
+ const days = Math.floor(hours / 24);
33
+ return `${days}d ago`;
34
+ }
35
+ export function formatRecentNarratives(rows, since) {
36
+ if (rows.length === 0)
37
+ return `No narratives active in the last ${since}.`;
38
+ const lines = [];
39
+ for (const row of rows) {
40
+ const icon = row.status === "active" ? "●" : "○";
41
+ const title = row.title ? `"${row.title}"` : `(${row.agent_type})`;
42
+ const events = `${row.event_count} event${row.event_count === 1 ? "" : "s"}`;
43
+ lines.push(`${icon} ${row.id} ${title} ${row.last_activity_ago} (${events})`);
44
+ if (row.summary)
45
+ lines.push(` Summary: ${row.summary}`);
46
+ }
47
+ return lines.join("\n");
48
+ }
49
+ /** Query string for `/api/narratives/recent`. A relative `--fold` is resolved
50
+ * against this shell's cwd, since the daemon's cwd means nothing here. */
51
+ export function buildRecentQuery(args, cwd) {
52
+ const q = new URLSearchParams();
53
+ q.set("since", args.since);
54
+ if (args.archived)
55
+ q.set("archived", "1");
56
+ if (args.search)
57
+ q.set("search", args.search);
58
+ if (args.status)
59
+ q.set("status", args.status);
60
+ q.set("limit", String(parseInt(args.limit, 10) || 20));
61
+ if (args.fold)
62
+ q.set("fold", args.fold.startsWith("~") ? args.fold : resolve(cwd, args.fold));
63
+ return q.toString();
64
+ }
65
+ const memoryLog = defineCommand({
66
+ meta: {
67
+ name: "log",
68
+ description: "Show recently active narratives with search/filtering",
69
+ },
70
+ args: {
71
+ since: {
72
+ type: "string",
73
+ default: "24h",
74
+ description: "Time window for recent activity (e.g. 30m, 1h, 2d, 1w)",
75
+ },
76
+ search: {
77
+ type: "string",
78
+ description: "Filter to narratives containing matching event content",
79
+ },
80
+ status: {
81
+ type: "string",
82
+ description: "Filter by narrative status (active, ended)",
83
+ },
84
+ limit: {
85
+ type: "string",
86
+ default: "20",
87
+ description: "Maximum number of narratives to show",
88
+ },
89
+ fold: {
90
+ type: "string",
91
+ description: "Filter by fold path (absolute or portable, e.g. ~/dev/brane)",
92
+ },
93
+ archived: {
94
+ type: "boolean",
95
+ description: "Include archived narratives (absorbed history is archived by construction)",
96
+ default: false,
97
+ },
98
+ json: {
99
+ type: "boolean",
100
+ description: "Output as JSON",
101
+ },
102
+ },
103
+ async run({ args }) {
104
+ try {
105
+ const endpoint = await resolveDaemonEndpoint();
106
+ warnIfVersionMismatch(endpoint);
107
+ const { narratives, since } = await daemonRequest(endpoint, `/api/narratives/recent?${buildRecentQuery(args, process.cwd())}`);
108
+ if (args.json) {
109
+ console.log(JSON.stringify(narratives, null, 2));
110
+ return;
111
+ }
112
+ console.log(formatRecentNarratives(narratives, since));
113
+ }
114
+ catch (err) {
115
+ console.error(err instanceof Error ? err.message : String(err));
116
+ process.exit(1);
117
+ }
118
+ },
119
+ });
120
+ export function formatSearchResults(results) {
121
+ if (results.length === 0)
122
+ return "No results found.";
123
+ const lines = [`Found ${results.length} results:`, ""];
124
+ results.forEach((r, i) => {
125
+ const preview = r.content.length > 120 ? r.content.slice(0, 120) + "..." : r.content;
126
+ lines.push(`${i + 1}. ${r.narrative_id} (seq ${r.seq}) - Score: ${r.score.toFixed(2)}`);
127
+ lines.push(` "${preview}"`);
128
+ lines.push(` > brane attach ${r.narrative_id}`, "");
129
+ });
130
+ return lines.join("\n");
131
+ }
132
+ const memorySearch = defineCommand({
133
+ meta: {
134
+ name: "search",
135
+ description: "Semantic search across narrative history",
136
+ },
137
+ args: {
138
+ query: {
139
+ type: "positional",
140
+ required: true,
141
+ description: "Search query",
142
+ },
143
+ limit: {
144
+ type: "string",
145
+ default: "10",
146
+ description: "Maximum number of results",
147
+ },
148
+ },
149
+ async run({ args }) {
150
+ try {
151
+ // The daemon's embedding model is already warm; loading a second copy
152
+ // here cost every search a 30 s cold start. Indexing of new events is
153
+ // the daemon's background job (and `brane memory index` on demand).
154
+ const endpoint = await resolveDaemonEndpoint();
155
+ warnIfVersionMismatch(endpoint);
156
+ console.log(`Searching: "${args.query}"\n`);
157
+ const { results } = await daemonRequest(endpoint, "/api/search", {
158
+ method: "POST",
159
+ body: { query: args.query, limit: parseInt(args.limit, 10) || 10 },
160
+ });
161
+ console.log(formatSearchResults(results));
162
+ }
163
+ catch (err) {
164
+ console.error(err instanceof Error ? err.message : String(err));
165
+ process.exit(1);
166
+ }
167
+ },
168
+ });
169
+ export function formatIndexResult(stats) {
170
+ return (`Done. Scanned ${stats.narratives_scanned} narratives, ` +
171
+ `indexed ${stats.narratives_indexed}, ` +
172
+ `created ${stats.chunks_created} chunks.`);
173
+ }
174
+ const memoryIndex = defineCommand({
175
+ meta: {
176
+ name: "index",
177
+ description: "Re-index narrative content for semantic search",
178
+ },
179
+ args: {
180
+ rebuild: {
181
+ type: "boolean",
182
+ description: "Rebuild entire index from scratch",
183
+ },
184
+ },
185
+ async run({ args }) {
186
+ try {
187
+ // Runs through the daemon's indexer, queued behind any background
188
+ // pass already in flight — two processes indexing the same cursors
189
+ // was the race the in-process version had.
190
+ const endpoint = await resolveDaemonEndpoint();
191
+ warnIfVersionMismatch(endpoint);
192
+ const stats = await daemonRequest(endpoint, "/api/memory/index", {
193
+ method: "POST",
194
+ body: { rebuild: args.rebuild ?? false },
195
+ });
196
+ console.log(formatIndexResult(stats));
197
+ }
198
+ catch (err) {
199
+ console.error(err instanceof Error ? err.message : String(err));
200
+ process.exit(1);
201
+ }
202
+ },
203
+ });
204
+ export const memoryCommand = defineCommand({
205
+ meta: {
206
+ name: "memory",
207
+ description: "Semantic memory search and indexing",
208
+ },
209
+ subCommands: {
210
+ log: memoryLog,
211
+ search: memorySearch,
212
+ index: memoryIndex,
213
+ },
214
+ });
215
+ //# sourceMappingURL=memory.js.map
@@ -0,0 +1,74 @@
1
+ import { defineCommand } from "citty";
2
+ import { resolveDaemonEndpoint, daemonRequest, warnIfVersionMismatch } from "../daemon/client.js";
3
+ export const reviveCommand = defineCommand({
4
+ meta: {
5
+ name: "revive",
6
+ description: "Restart a dead agent session",
7
+ },
8
+ args: {
9
+ id: {
10
+ type: "positional",
11
+ required: true,
12
+ description: "Session/narrative ID to revive",
13
+ },
14
+ prompt: {
15
+ type: "string",
16
+ description: "Custom prompt to inject after revival",
17
+ },
18
+ "no-prompt": {
19
+ type: "boolean",
20
+ description: "Suppress the initial prompt",
21
+ },
22
+ agent: {
23
+ type: "string",
24
+ description: "Continue under a different agent (spawns a twin of this narrative)",
25
+ },
26
+ },
27
+ async run({ args }) {
28
+ try {
29
+ // Reviving goes through the daemon, exactly as the web app's revive menu
30
+ // item and the `revive` MCP tool do — `POST /api/revive/:nid` is the
31
+ // single revive path, and the CLI is one of its clients rather than a
32
+ // second implementation. Everything below this call is printing.
33
+ const endpoint = await resolveDaemonEndpoint();
34
+ warnIfVersionMismatch(endpoint);
35
+ const result = await daemonRequest(endpoint, `/api/revive/${encodeURIComponent(args.id)}`, { method: "POST", body: buildReviveBody(args) });
36
+ console.log(formatReviveResult(args.id, result));
37
+ if (result.status === "already_running" && args.prompt && !args["no-prompt"]) {
38
+ console.log(`Sent prompt to ${result.nid}`);
39
+ }
40
+ }
41
+ catch (err) {
42
+ console.error(err instanceof Error ? err.message : String(err));
43
+ process.exit(1);
44
+ }
45
+ },
46
+ });
47
+ /** Map CLI flags onto the `POST /api/revive/:nid` payload. Absent flags are
48
+ * omitted rather than sent as `undefined`/`false`, so the daemon's own
49
+ * defaults stay in charge of anything the operator didn't ask about. */
50
+ export function buildReviveBody(args) {
51
+ return {
52
+ ...(args.prompt ? { prompt: args.prompt } : {}),
53
+ ...(args["no-prompt"] ? { no_prompt: true } : {}),
54
+ ...(args.agent ? { agent: args.agent } : {}),
55
+ };
56
+ }
57
+ /** The one line `brane revive` prints for each outcome. Split out so the
58
+ * three-way distinction — which the old in-process CLI could not make at all
59
+ * — is testable without a daemon. */
60
+ export function formatReviveResult(requestedNid, result) {
61
+ const suffix = result.agent ? ` (${result.agent})` : "";
62
+ switch (result.status) {
63
+ case "already_running":
64
+ return `Session ${result.nid} is already running.`;
65
+ case "continued":
66
+ // A twin: `result.nid` is a *new* narrative that reads the requested
67
+ // one's history. Printing the requested id here would name a pane that
68
+ // isn't the one now running.
69
+ return `Continued ${requestedNid} as ${result.nid}${suffix}`;
70
+ default:
71
+ return `Revived ${result.nid}${suffix}`;
72
+ }
73
+ }
74
+ //# sourceMappingURL=revive.js.map