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,332 @@
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 { UNUSED_TOKEN_LAPSE_MS } from "../daemon/token-ring.js";
8
+ import { daemonRequest, resolveDaemonEndpoint, warnIfVersionMismatch, } from "../daemon/client.js";
9
+ /** What the CLI prints before a request the daemon holds for the presence
10
+ * check (src/daemon/presence-gate.ts). The daemon is on this machine, so
11
+ * this process's platform names its prompt. On stderr, so it never lands in
12
+ * output a script captures. */
13
+ export function presenceWaitNote(platform = process.platform) {
14
+ const prompt = platform === "darwin" ? "Touch ID (or your login password)" : platform === "win32" ? "Windows Hello" : "the system's authentication prompt";
15
+ return `Confirm with ${prompt} on this machine to continue…`;
16
+ }
17
+ /** Print `presenceWaitNote()` before a gated request, unless this is an
18
+ * agent's session: the daemon refuses an agent unasked, so there is nothing
19
+ * to wait for. */
20
+ export function announcePresenceWait(env = process.env) {
21
+ if (!env.BRANE_NARRATIVE_SECRET)
22
+ console.error(presenceWaitNote());
23
+ }
24
+ /** Ask the daemon for a fresh device token. The daemon keeps only its hash,
25
+ * so the QR this goes into is the one and only copy — every scan of every
26
+ * meld is its own device, revocable alone. From this machine the daemon
27
+ * first asks the operator to confirm at the OS prompt. */
28
+ export async function mintDeviceToken(endpoint, label) {
29
+ announcePresenceWait();
30
+ const { token } = await daemonRequest(endpoint, "/api/tokens", {
31
+ method: "POST",
32
+ body: label ? { label } : {},
33
+ });
34
+ return token;
35
+ }
36
+ /** What `brane meld` and `brane spawn --meld` print under the QR. */
37
+ export function meldLapseNote() {
38
+ return `This QR is valid for ${UNUSED_TOKEN_LAPSE_MS / 60_000} minutes; once scanned, the device stays signed in until revoked.`;
39
+ }
40
+ /** Build a meld URL pointing at the daemon (optionally targeting a specific
41
+ * in-app path). Shared by `brane meld` and `brane spawn --meld`. */
42
+ export function buildMeldUrl(opts) {
43
+ const { db, daemon } = opts;
44
+ const { port, token } = daemon;
45
+ const tailscaleHost = "tailscaleHost" in opts ? opts.tailscaleHost : getTailscaleHost();
46
+ const host = opts.host ?? tailscaleHost ?? "127.0.0.1";
47
+ const isLoopbackFallback = !opts.host && !tailscaleHost && host === "127.0.0.1";
48
+ const tlsRow = db.prepare("SELECT value FROM meta WHERE key = 'daemon_tls'").get();
49
+ const tls = tlsRow?.value === "true";
50
+ const useTls = tls && host === tailscaleHost;
51
+ const useLocal = opts.local === true || !useTls;
52
+ let effectivePort = port;
53
+ if (useLocal && tls && !useTls) {
54
+ const httpPortRow = db.prepare("SELECT value FROM meta WHERE key = 'daemon_http_port'").get();
55
+ if (httpPortRow)
56
+ effectivePort = httpPortRow.value;
57
+ }
58
+ const scheme = useTls ? "https" : "http";
59
+ const portSuffix = useTls && effectivePort === "443" ? "" : `:${effectivePort}`;
60
+ const daemonHost = `${host}${portSuffix}`;
61
+ // Tailscale is present and we'd advertise its hostname, but there's no cert,
62
+ // so we're silently emitting plain HTTP instead of the expected HTTPS.
63
+ const tlsDegraded = !!tailscaleHost && host === tailscaleHost && !tls && opts.local !== true;
64
+ let path = opts.path ?? "/";
65
+ if (!path.startsWith("/"))
66
+ path = "/" + path;
67
+ const url = meldLink({ daemonHost, scheme, useLocal }, token, path);
68
+ return { url, daemonHost, scheme, useLocal, isLoopbackFallback, tlsDegraded };
69
+ }
70
+ /** A meld URL for `token`, at the daemon `parts` describes. */
71
+ export function meldLink(parts, token, path = "/") {
72
+ // The daemon's address and token ride in the URL fragment, which the
73
+ // browser never sends to any server — so the shared SPA's host (Cloudflare)
74
+ // does not see the token, a pasted link's unfurler does not fetch it, and
75
+ // the daemon's own request log does not record it. The app reads it from
76
+ // `location.hash` and strips it once it has been used.
77
+ const conn = { host: parts.daemonHost, token };
78
+ return parts.useLocal
79
+ ? `${parts.scheme}://${parts.daemonHost}${path}${connectionFragment(conn, false)}`
80
+ : `https://brane.organelle.dev${path}${connectionFragment(conn, true)}`;
81
+ }
82
+ /** Ask this machine's daemon for a device token for a daemon on another
83
+ * machine, and its hash. The daemon stores neither, and first asks the
84
+ * operator to confirm at the OS prompt, as it does for `brane meld`. */
85
+ export async function mintRemoteDeviceToken(endpoint) {
86
+ announcePresenceWait();
87
+ return daemonRequest(endpoint, "/api/tokens/remote", { method: "POST", body: {} });
88
+ }
89
+ /** What one run of the `--ask` command told us. `output` is its stdout, or
90
+ * null when the command itself failed (machine not reachable yet). Pure, so
91
+ * the suite pins every case:
92
+ *
93
+ * - `address`: the daemon is up and reachable over Tailscale;
94
+ * - `wait`: not yet — no answer, no daemon, or no Tailscale name yet (a QR
95
+ * for 127.0.0.1 would be useless, and a wrong QR costs a restart);
96
+ * - `old`: the remote answered without the JSON line, so its brane
97
+ * predates `--connection` and ran a plain meld instead. */
98
+ export function readMeldReply(output) {
99
+ if (output === null)
100
+ return { kind: "wait", reason: "the remote machine is not answering yet" };
101
+ for (const line of output.split(/\r?\n/).reverse()) {
102
+ const trimmed = line.trim();
103
+ if (!trimmed.startsWith("{"))
104
+ continue;
105
+ let r;
106
+ try {
107
+ r = JSON.parse(trimmed);
108
+ }
109
+ catch {
110
+ continue;
111
+ }
112
+ if (r.ready === false)
113
+ return { kind: "wait", reason: typeof r.reason === "string" ? r.reason : "not ready" };
114
+ if (r.ready !== true)
115
+ continue;
116
+ if (typeof r.daemonHost !== "string" || (r.scheme !== "http" && r.scheme !== "https") || typeof r.useLocal !== "boolean")
117
+ continue;
118
+ if (r.isLoopbackFallback)
119
+ return { kind: "wait", reason: "the remote machine has no Tailscale name yet" };
120
+ const { ready: _ready, ...connection } = r;
121
+ return { kind: "address", connection: connection };
122
+ }
123
+ return { kind: "old" };
124
+ }
125
+ /** The `--send` command with the hash in place of `{hash}`. A hash is 64 hex
126
+ * digits, so it needs no quoting in a shell command. */
127
+ const fillHashError = "--send must contain {hash}, where the token's hash goes";
128
+ export function fillHash(template, hash) {
129
+ if (!template.includes("{hash}"))
130
+ throw new Error(fillHashError);
131
+ return template.replaceAll("{hash}", hash);
132
+ }
133
+ /** How long `brane meld --remote` waits for the remote daemon's address. */
134
+ const REMOTE_MELD_WAIT_MS = 3 * 60_000;
135
+ /** `--send` waits for whatever its command waits for (`fly secrets set` waits
136
+ * for the machines to restart), which can outlast a couple of minutes. */
137
+ const REMOTE_SEND_TIMEOUT_MS = 10 * 60_000;
138
+ /** One `--ask` try; a slow `fly ssh` connect is retried, not waited out. */
139
+ const REMOTE_ASK_TIMEOUT_MS = 60_000;
140
+ /** Why `brane meld --remote` won't run here, or null if it will. The token is
141
+ * printed into the QR and the URL; in an agent's session that output is the
142
+ * agent's to read, and a device token is the operator's. This only saves a
143
+ * round trip: the gate is the daemon's, which refuses an agent's token and
144
+ * asks the shell for presence (`POST /api/tokens/remote`). */
145
+ export function refusesRemoteMeld(env = process.env) {
146
+ return env.BRANE_NARRATIVE_SECRET
147
+ ? "brane meld --remote prints a device token; run it from a shell outside an agent session."
148
+ : null;
149
+ }
150
+ /** `brane meld --remote`: meld a device to a daemon on a machine that can
151
+ * show no presence prompt (a Fly machine, a VPS). The token is minted by
152
+ * this machine's daemon, behind its presence prompt; `--send` hands the
153
+ * remote only its hash, and `--ask` reads back only its address. Nothing
154
+ * here knows which host it is. */
155
+ async function meldRemote(args) {
156
+ const refusal = refusesRemoteMeld();
157
+ if (refusal) {
158
+ console.error(refusal);
159
+ process.exit(1);
160
+ }
161
+ if (!args.send || !args.ask) {
162
+ console.error("brane meld --remote needs --send '<command with {hash}>' and --ask '<command that runs brane meld --connection there>'.");
163
+ process.exit(1);
164
+ }
165
+ // Before the prompt, so a bad --send never asks the operator anything.
166
+ if (!args.send.includes("{hash}")) {
167
+ console.error(fillHashError);
168
+ process.exit(1);
169
+ }
170
+ let token;
171
+ let send;
172
+ try {
173
+ const endpoint = await resolveDaemonEndpoint();
174
+ warnIfVersionMismatch(endpoint);
175
+ const minted = await mintRemoteDeviceToken(endpoint);
176
+ token = minted.token;
177
+ send = fillHash(args.send, minted.hash);
178
+ }
179
+ catch (err) {
180
+ console.error(`Failed to mint a device token: ${err.message}`);
181
+ process.exit(1);
182
+ }
183
+ console.log("Sending the remote daemon the new device token's hash...");
184
+ const sent = spawnSync("sh", ["-c", send], { stdio: "inherit", timeout: REMOTE_SEND_TIMEOUT_MS });
185
+ if (sent.status !== 0) {
186
+ console.error(`--send failed (${sent.error?.message ?? `exit ${sent.status}`}); nothing was shown, so nothing is lost.`);
187
+ process.exit(1);
188
+ }
189
+ console.log("Waiting for the remote daemon...");
190
+ const deadline = Date.now() + REMOTE_MELD_WAIT_MS;
191
+ let connection = null;
192
+ let waitingFor = "the remote daemon";
193
+ // The last failed try's stderr, shown if we give up: the difference between
194
+ // a wrong app name, an ssh login and a daemon that never came up.
195
+ let lastError = "";
196
+ while (!connection && Date.now() < deadline) {
197
+ const asked = spawnSync("sh", ["-c", args.ask], { stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8", timeout: REMOTE_ASK_TIMEOUT_MS });
198
+ if (asked.status !== 0)
199
+ lastError = (asked.stderr ?? "").trim() || (asked.error?.message ?? `exit ${asked.status}`);
200
+ const reply = readMeldReply(asked.status === 0 ? asked.stdout : null);
201
+ if (reply.kind === "old") {
202
+ console.error("The remote runs a brane that predates `brane meld --connection`. Update it, then run this again.");
203
+ process.exit(1);
204
+ }
205
+ if (reply.kind === "address")
206
+ connection = reply.connection;
207
+ else {
208
+ waitingFor = reply.reason;
209
+ await new Promise((r) => setTimeout(r, 5_000));
210
+ }
211
+ }
212
+ if (!connection) {
213
+ console.error(`Gave up waiting: ${waitingFor}.`);
214
+ if (lastError)
215
+ console.error(`The last --ask failed with:\n${lastError.split("\n").slice(-10).join("\n")}`);
216
+ console.error("Run this again once it's up; the token sent this time was never shown, so nothing is lost.");
217
+ process.exit(1);
218
+ }
219
+ const url = meldLink(connection, token);
220
+ console.log(`\nScan this QR code to meld:\n`);
221
+ qrcode.generate(url, { small: args.small ?? true }, (code) => {
222
+ console.log(code);
223
+ });
224
+ console.log(`\n${url}\n`);
225
+ console.log(`Daemon: ${connection.daemonHost} (${connection.scheme})`);
226
+ // The clock started with the remote daemon's restart, not now.
227
+ 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.`);
228
+ if (connection.tlsDegraded) {
229
+ console.log("\nWarning: the remote daemon has no TLS cert, so this QR uses plain HTTP.");
230
+ }
231
+ }
232
+ export const meldCommand = defineCommand({
233
+ meta: {
234
+ name: "meld",
235
+ description: "Generate a QR code to connect a device to this brane",
236
+ },
237
+ args: {
238
+ host: {
239
+ type: "string",
240
+ description: "Override the host (default: Tailscale DNS > 127.0.0.1)",
241
+ },
242
+ small: {
243
+ type: "boolean",
244
+ description: "Use small QR code (half-block characters)",
245
+ },
246
+ local: {
247
+ type: "boolean",
248
+ description: "Generate direct daemon URL instead of brane.organelle.dev",
249
+ },
250
+ connection: {
251
+ type: "boolean",
252
+ description: "Print where a meld URL would point, as JSON, and mint nothing (what `--ask` runs on the remote machine)",
253
+ },
254
+ remote: {
255
+ type: "boolean",
256
+ description: "Meld to a daemon on another machine: mint the token here, hand it only the hash (--send), read back its address (--ask)",
257
+ },
258
+ send: {
259
+ type: "string",
260
+ description: "With --remote: a shell command that gives the remote daemon BRANE_DEVICE_TOKEN_SHA256={hash} and restarts it",
261
+ },
262
+ ask: {
263
+ type: "string",
264
+ description: "With --remote: a shell command that runs `brane meld --connection` on the remote machine",
265
+ },
266
+ },
267
+ async run({ args }) {
268
+ if (args.remote) {
269
+ await meldRemote(args);
270
+ return;
271
+ }
272
+ if (args.connection) {
273
+ // No token, so no presence check: an address is not a secret. Never
274
+ // starts a daemon — over `fly ssh` it would die with the session. Always
275
+ // one JSON line on stdout and exit 0, so the caller reads one channel
276
+ // and depends on nothing about how ssh passes on exit codes.
277
+ const db = openDatabase();
278
+ try {
279
+ const address = getDaemonAddress(db);
280
+ const reply = address
281
+ ? (({ url: _url, ...connection }) => ({ ready: true, ...connection }))(buildMeldUrl({ db, daemon: { port: address.port, token: "" }, host: args.host, local: args.local }))
282
+ : { ready: false, reason: "no daemon is running yet" };
283
+ console.log(JSON.stringify(reply));
284
+ }
285
+ finally {
286
+ db.close();
287
+ }
288
+ return;
289
+ }
290
+ let endpoint;
291
+ let token;
292
+ try {
293
+ endpoint = await resolveDaemonEndpoint();
294
+ warnIfVersionMismatch(endpoint);
295
+ token = await mintDeviceToken(endpoint);
296
+ }
297
+ catch (err) {
298
+ console.error(`Failed to mint a device token: ${err.message}`);
299
+ process.exit(1);
300
+ }
301
+ const daemon = { port: endpoint.port, token };
302
+ const db = openDatabase();
303
+ const { url, daemonHost, scheme, useLocal, isLoopbackFallback, tlsDegraded } = buildMeldUrl({
304
+ db,
305
+ daemon,
306
+ host: args.host,
307
+ local: args.local,
308
+ });
309
+ db.close();
310
+ console.log(`\nScan this QR code to meld:\n`);
311
+ qrcode.generate(url, { small: args.small ?? true }, (code) => {
312
+ console.log(code);
313
+ });
314
+ console.log(`\n${url}\n`);
315
+ console.log(`Daemon: ${daemonHost} (${scheme})`);
316
+ if (!useLocal) {
317
+ console.log(`App: brane.organelle.dev`);
318
+ }
319
+ console.log(meldLapseNote());
320
+ if (isLoopbackFallback) {
321
+ console.log("\nNo Tailscale detected. This QR points at 127.0.0.1 and will only work from this machine.");
322
+ console.log("For cross-device access, install Tailscale and run `brane certify`,");
323
+ console.log("or pass `--host <address>` to advertise an address you trust.");
324
+ }
325
+ else if (tlsDegraded) {
326
+ console.log("\nWarning: Tailscale is up but the daemon has no TLS cert, so this QR uses plain HTTP.");
327
+ console.log("Run `brane certify` to provision one — it'll tell you if \"HTTPS for your tailnet\"");
328
+ console.log("needs enabling at https://login.tailscale.com/admin/dns.");
329
+ }
330
+ },
331
+ });
332
+ //# 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