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,61 @@
1
+ /**
2
+ * The color a foreign speaker's bubbles wear in a narrative — a hue hashed from
3
+ * its nid, so `claude-4c7289d1` is the same purple in every conversation it
4
+ * joins. That global stability is the feature, and it has one failure: two
5
+ * speakers in the *same* narrative can hash a few degrees apart, and a
6
+ * synthesis between two agents becomes a conversation between two purples.
7
+ *
8
+ * `salt` is the 🎨 Recolor escape hatch. Salt 0 is the palette every narrative
9
+ * has always had (the legacy hash, kept byte-for-byte so nothing already on
10
+ * screen shifts). A nonzero salt reshuffles *per speaker*: the legacy hash is
11
+ * pushed through a nonlinear finalizer, because merely seeding or prefixing the
12
+ * polynomial hash adds the same constant to every equal-length nid — a uniform
13
+ * rotation that leaves two close hues exactly as close as they were.
14
+ *
15
+ * Window-free, like `view-url.ts`, so `src/test/speaker-color.test.ts` can pin
16
+ * it; the salt itself lives in the app's storage (`expo/lib/recolor.ts`).
17
+ */
18
+ /** The hash every brane app has used for speaker hues (matches Stolon). Kept
19
+ * verbatim — including its overflow past int32 on the last character — so
20
+ * salt 0 reproduces existing colors exactly. */
21
+ function legacyHash(speaker) {
22
+ let hash = 0;
23
+ for (let i = 0; i < speaker.length; i++) {
24
+ hash = speaker.charCodeAt(i) + ((hash << 5) - hash);
25
+ }
26
+ return hash;
27
+ }
28
+ /** MurmurHash3's 32-bit finalizer: every input bit affects every output bit,
29
+ * which is what makes the reshuffle per speaker rather than per palette. */
30
+ function fmix32(h) {
31
+ h ^= h >>> 16;
32
+ h = Math.imul(h, 0x85ebca6b);
33
+ h ^= h >>> 13;
34
+ h = Math.imul(h, 0xc2b2ae35);
35
+ h ^= h >>> 16;
36
+ return h | 0;
37
+ }
38
+ export function speakerHue(speaker, salt = 0) {
39
+ let hash = legacyHash(speaker);
40
+ if (salt)
41
+ hash = fmix32(hash ^ Math.imul(salt, 0x9e3779b1));
42
+ return Math.abs(hash) % 360;
43
+ }
44
+ /** Attribution text: a light pastel. */
45
+ export function speakerColor(speaker, salt = 0) {
46
+ return `hsl(${speakerHue(speaker, salt)}, 60%, 80%)`;
47
+ }
48
+ /** Bubble background: the same hue, dark. */
49
+ export function speakerBgColor(speaker, salt = 0) {
50
+ return `hsl(${speakerHue(speaker, salt)}, 30%, 20%)`;
51
+ }
52
+ /** Where a narrative's salt is kept, per device. */
53
+ export function recolorStorageKey(nid) {
54
+ return `brane:recolor:${nid}`;
55
+ }
56
+ /** Parse a stored salt; anything unreadable is the default palette. */
57
+ export function parseRecolorSalt(raw) {
58
+ const n = Number(raw);
59
+ return Number.isInteger(n) && n > 0 ? n : 0;
60
+ }
61
+ //# sourceMappingURL=speaker-color.js.map
@@ -0,0 +1,14 @@
1
+ /**
2
+ * What the daemon and the app agree on about token rotation, window-free so
3
+ * both can import it. The mechanism itself lives in `src/daemon/token-ring.ts`.
4
+ */
5
+ /** WebSocket close code the daemon sends to a viewer socket it is signing out
6
+ * because the token it authenticated with has been retired. A client that
7
+ * sees it must stop reconnecting — the credential it holds is dead, and a
8
+ * 2 s retry loop against a 401 would run forever and silently. Anything
9
+ * else (1006 above all) is a network blip and reconnects as before. */
10
+ export const WS_CLOSE_TOKEN_ROTATED = 4401;
11
+ export function isSignedOutClose(code) {
12
+ return code === WS_CLOSE_TOKEN_ROTATED;
13
+ }
14
+ //# sourceMappingURL=token-rotation.js.map
@@ -0,0 +1,216 @@
1
+ /** The URL contract for a narrative view: which tab is active, which tabs are
2
+ * pinned beside it, and which file (and line range) the Files pane is showing.
3
+ *
4
+ * This lives in `shared/` rather than in the screen that uses it because the
5
+ * URL is the only carrier of view state between two *origins*. Tab layout and
6
+ * last-visited file are also kept in `sessionStorage`, which is origin-scoped,
7
+ * and the Isolate menu item hops between the shared app's origin and a
8
+ * daemon's own host —
9
+ * so whatever isn't in the query string does not survive the jump. Keeping
10
+ * the serialization and the link builder in one pure module means the URL you
11
+ * get from tapping around and the URL Isolate hands the other origin are the
12
+ * same URL, and means both can be tested without a browser.
13
+ */
14
+ /** Tab order, as rendered in the tab bar. Pins serialize in this order no
15
+ * matter what order they were toggled in, so a URL is stable. */
16
+ export const CANONICAL_TABS = ["chat", "terminal", "files"];
17
+ const VALID_TABS = new Set(CANONICAL_TABS);
18
+ /** True when this page is served by the daemon it is connected to — a
19
+ * daemon's own bundle — so "isolated" is the *current* state and the menu
20
+ * offers the way back out. Everything else is a shared app: the hosted
21
+ * shell, a staging hostname, a fork's deployment, a dev server. The app
22
+ * carries no host of its own, so this is a comparison with no constant in
23
+ * it: `window.location.host` against the connection's host, which a
24
+ * daemon's bundle fills from the same `location.host` (expo/lib/api.ts).
25
+ * Hosts are case-insensitive; the port is part of the answer, since a
26
+ * daemon's bundle is reached on its port. */
27
+ export function isIsolated(pageHost, daemonHost) {
28
+ return pageHost.toLowerCase() === daemonHost.toLowerCase();
29
+ }
30
+ export function parseActiveTab(tab) {
31
+ return VALID_TABS.has(tab) ? tab : "chat";
32
+ }
33
+ export function parsePinnedTabs(pinned) {
34
+ if (!pinned)
35
+ return new Set();
36
+ return new Set(pinned.split(",").map((s) => s.trim()).filter((s) => VALID_TABS.has(s)));
37
+ }
38
+ export function serializePinned(pins) {
39
+ return CANONICAL_TABS.filter((t) => pins.has(t)).join(",");
40
+ }
41
+ /** The view-state query params as a plain record, with `undefined` for anything
42
+ * at its default — which is both how `router.setParams` clears a param and how
43
+ * `buildIsolateUrl` knows to drop a stale one. */
44
+ export function viewParams(tab, pins, path, line, lineEnd) {
45
+ const pinsSerialized = serializePinned(pins);
46
+ const lineStr = line
47
+ ? (lineEnd && lineEnd > line ? `${line}-${lineEnd}` : String(line))
48
+ : undefined;
49
+ return {
50
+ tab: tab !== "chat" ? tab : undefined,
51
+ pinned: pinsSerialized || undefined,
52
+ path: path || undefined,
53
+ line: lineStr,
54
+ };
55
+ }
56
+ /** The narrative's terminal, alone — where the two lifecycle actions land.
57
+ *
58
+ * Both Revive and End put you in front of a prompt you have to answer: a
59
+ * resumed agent often comes up sitting on one, and a termination is just as
60
+ * often *blocked* by one. So they navigate to a **view**, not merely to a
61
+ * narrative.
62
+ *
63
+ * It goes in the URL rather than being set after arrival because an explicit
64
+ * `tab` param is what outranks the tab layout the screen would otherwise
65
+ * restore from `sessionStorage`; state set on arrival would be racing that
66
+ * restore. No pins, per the "alone" half: whatever was pinned belongs to the
67
+ * view you were in, not to the prompt you now have to answer.
68
+ */
69
+ export function terminalOnlyHref(nid) {
70
+ const params = new URLSearchParams();
71
+ for (const [key, value] of Object.entries(viewParams("terminal", new Set()))) {
72
+ if (value)
73
+ params.set(key, value);
74
+ }
75
+ return `/${nid}?${params.toString()}`;
76
+ }
77
+ /** Reads the `line` param back: `"10"` or `"10-20"`. The counterpart of the
78
+ * `line` that `viewParams` writes, and the reason both live here — a param
79
+ * whose writer and reader sit in different modules drifts.
80
+ *
81
+ * Note the deliberate asymmetry at `lineEnd === line`: the writer collapses
82
+ * that to a bare `"10"`, so it reads back as a single line rather than a
83
+ * one-line range. Same selection either way. */
84
+ export function parseLineSpec(s) {
85
+ if (!s)
86
+ return {};
87
+ const m = s.match(/^(\d+)(?:-(\d+))?$/);
88
+ if (!m)
89
+ return {};
90
+ const line = Number(m[1]);
91
+ const end = m[2] ? Number(m[2]) : undefined;
92
+ return end !== undefined && end >= line ? { line, lineEnd: end } : { line };
93
+ }
94
+ /** The params that describe *which daemon*, as opposed to what you are looking
95
+ * at. They are origin-scoped: meaningful to the page that received them, never
96
+ * to be carried onward. */
97
+ export const CONNECTION_PARAMS = ["host", "token"];
98
+ /** A search string with the connection params removed and everything else left
99
+ * alone, ready to hand to `history.replaceState`.
100
+ *
101
+ * The shared SPA cleans a meld URL as soon as it has used it, so the token
102
+ * doesn't linger in the address bar or the back stack. Cleaning by resetting
103
+ * to the bare pathname would take the view with it — tab, pins, open file,
104
+ * scroll target — which is precisely what an Isolate hop just finished
105
+ * carrying across. Returns `""` or a string with a leading `?`.
106
+ *
107
+ * New links carry the connection in the fragment (see `connectionFragment`),
108
+ * so this is the legacy half of the cleaner: old QR codes and bookmarks still
109
+ * arrive with `?token=`, and they are still honored and still scrubbed.
110
+ */
111
+ export function withoutConnectionParams(search) {
112
+ const params = new URLSearchParams(search);
113
+ for (const key of CONNECTION_PARAMS)
114
+ params.delete(key);
115
+ const rest = params.toString();
116
+ return rest ? `?${rest}` : "";
117
+ }
118
+ /** The fragment counterpart of `withoutConnectionParams`: a hash with the
119
+ * connection params removed and anything else left alone. Returns `""` or a
120
+ * string with a leading `#`. Accepts the hash with or without its `#`. */
121
+ export function withoutConnectionHash(hash) {
122
+ const raw = hash.startsWith("#") ? hash.slice(1) : hash;
123
+ if (!raw)
124
+ return "";
125
+ const params = new URLSearchParams(raw);
126
+ for (const key of CONNECTION_PARAMS)
127
+ params.delete(key);
128
+ const rest = params.toString();
129
+ return rest ? `#${rest}` : "";
130
+ }
131
+ /** Where a link puts the daemon's address and token: the URL **fragment**.
132
+ *
133
+ * A query string is part of the HTTP request, so `?host=…&token=…` on a
134
+ * shared-app link reached its host's edge with the daemon's
135
+ * bearer token in it, landed in the daemon's own request log on an Isolate
136
+ * hop, and was fetched by every chat app's link unfurler when a meld URL was
137
+ * pasted. A fragment is never sent to any server: the browser keeps it, the
138
+ * page reads it from `location.hash`, and the app strips it once consumed.
139
+ *
140
+ * The shared SPA serves every daemon and must be told which one; a daemon's
141
+ * own bundle already knows, so it gets the token alone.
142
+ */
143
+ export function connectionFragment(conn, includeHost) {
144
+ const params = new URLSearchParams();
145
+ if (includeHost)
146
+ params.set("host", conn.host);
147
+ params.set("token", conn.token);
148
+ return `#${params.toString()}`;
149
+ }
150
+ /** The same narrative, in the same view, somewhere else — the one builder
151
+ * behind both menu items.
152
+ *
153
+ * Params come from two places, and the order matters. The current search
154
+ * string seeds the set, which picks up params the screen doesn't model —
155
+ * `scrollToSeq`, so the new page opens at the message you'd jumped to. Live
156
+ * state is then overlaid and wins on everything it does own, because a layout
157
+ * restored from `sessionStorage` never writes itself back to the URL: seeding
158
+ * alone would miss exactly the case with the most to lose.
159
+ *
160
+ * Connection params are always dropped and re-set, never carried: they
161
+ * describe the origin building the link, and the target needs its own. The
162
+ * view rides in the query string; the connection rides in the fragment, which
163
+ * no server ever sees (`connectionFragment`).
164
+ */
165
+ export function buildViewUrl(target, opts) {
166
+ const { protocol, search, path, conn, view } = opts;
167
+ const params = new URLSearchParams(search);
168
+ for (const [key, value] of Object.entries(view)) {
169
+ if (value)
170
+ params.set(key, value);
171
+ else
172
+ params.delete(key);
173
+ }
174
+ for (const key of CONNECTION_PARAMS)
175
+ params.delete(key);
176
+ const query = params.toString();
177
+ const rest = (query ? `?${query}` : "") + connectionFragment(conn, target === "shared");
178
+ if (target === "shared") {
179
+ // A shared app serves every daemon, so it has to be told which one.
180
+ const origin = sharedOrigin(opts);
181
+ return origin === null ? null : `${origin}${path}${rest}`;
182
+ }
183
+ // A daemon's own bundle already knows which daemon it is; only the token is
184
+ // needed. The scheme follows the page building the link, so a plain-http LAN
185
+ // session doesn't hand out https links that won't load.
186
+ return `${pageScheme(protocol)}://${conn.host}${path}${rest}`;
187
+ }
188
+ function pageScheme(protocol) {
189
+ return protocol === "https:" ? "https" : "http";
190
+ }
191
+ /** The shared app's origin, from where this page stands. On a shared app it
192
+ * is this page's own origin, scheme included (a dev server is plain http).
193
+ * On a daemon's own bundle it is whatever the daemon reports as its
194
+ * `app_domain`, over https, since that is a hosted shell; with none
195
+ * reported there is no shared origin to name, and the answer is null. */
196
+ function sharedOrigin(opts) {
197
+ if (!isIsolated(opts.host, opts.conn.host))
198
+ return `${pageScheme(opts.protocol)}://${opts.host}`;
199
+ return opts.appHost ? `https://${opts.appHost}` : null;
200
+ }
201
+ /** Isolate / Unisolate: the same view, on the *other* kind of origin. Which
202
+ * way it goes is just where you are now. Null only from a daemon's own
203
+ * bundle that was told no `appHost`: there is nowhere to hop out to, and
204
+ * the menu should say so rather than offer a link. */
205
+ export function buildIsolateUrl(opts) {
206
+ return buildViewUrl(isIsolated(opts.host, opts.conn.host) ? "shared" : "daemon", opts);
207
+ }
208
+ /** Meld: the same view, on another *device*. The origin kind is the one thing
209
+ * that must not change — someone reaching the daemon directly can't be handed
210
+ * a shared-app link, and someone on a shared app expects to stay on *that*
211
+ * one — so where Isolate flips, this stays put. It never needs `appHost`:
212
+ * the shared origin it names is the page's own, so it is never null. */
213
+ export function buildMeldUrl(opts) {
214
+ return buildViewUrl(isIsolated(opts.host, opts.conn.host) ? "daemon" : "shared", opts);
215
+ }
216
+ //# sourceMappingURL=view-url.js.map
@@ -0,0 +1,70 @@
1
+ /** Link resolution for wiki pages, shared by the daemon and the web reader so
2
+ * the two can never disagree about where a link points.
3
+ *
4
+ * A fold's wiki is `wiki/` plus at most one subdirectory, `wiki/shared/`. A
5
+ * page's *slug* is its path relative to `wiki/` — `foo.md` is local,
6
+ * `shared/foo.md` is shared — so a citation means the same thing to every
7
+ * reader. Markdown links inside a page are filesystem-relative, the way
8
+ * GitHub and every editor already render them: from `shared/a.md`, `b.md` is
9
+ * `shared/b.md` and `../c.md` is `c.md`; from local `c.md`, `shared/a.md` is
10
+ * the shared page. This module is that arithmetic and nothing else — whether
11
+ * the target exists, or is visible to the caller, is decided by whoever holds
12
+ * the directory.
13
+ *
14
+ * Window-free and dependency-free so `src/test/wiki-links.test.ts` can run it
15
+ * over a grid, and so metro needs no alias for it.
16
+ */
17
+ /** The one subdirectory a wiki may contain. Pages under it are visible from
18
+ * other folds; everything directly under `wiki/` is fold-local. */
19
+ export const WIKI_SHARED_DIR = "shared";
20
+ /** Whether a slug (with or without `.md`) names a shared page. */
21
+ export function isSharedSlug(slug) {
22
+ return slug.startsWith(`${WIKI_SHARED_DIR}/`);
23
+ }
24
+ /** Canonical slug form: no leading `/` or `./`, `.md` appended when the last
25
+ * segment has no extension. Does not touch `..` — callers that need safety
26
+ * go through {@link resolveWikiLink}, which rejects escapes. */
27
+ export function normalizeWikiSlug(slug) {
28
+ let s = slug.trim().replace(/^\/+/, "");
29
+ while (s.startsWith("./"))
30
+ s = s.slice(2);
31
+ const last = s.split("/").pop() ?? "";
32
+ return last.includes(".") ? s : `${s}.md`;
33
+ }
34
+ /** Resolve a markdown `href` found in the page `fromSlug`. Returns the target
35
+ * slug (canonical, relative to `wiki/`) and any `#anchor`, or `null` when the
36
+ * link is not an intra-wiki page link: a URL with a scheme, an absolute
37
+ * path, or a relative path that climbs above `wiki/`. A bare `#anchor` stays
38
+ * on the current page. */
39
+ export function resolveWikiLink(fromSlug, href) {
40
+ const raw = href.trim();
41
+ const hashIdx = raw.indexOf("#");
42
+ const pathPart = hashIdx >= 0 ? raw.slice(0, hashIdx) : raw;
43
+ const anchor = hashIdx >= 0 ? raw.slice(hashIdx + 1).trim() || undefined : undefined;
44
+ const from = normalizeWikiSlug(fromSlug);
45
+ if (pathPart === "") {
46
+ return anchor !== undefined ? { slug: from, anchor } : { slug: from };
47
+ }
48
+ if (/^[a-z][a-z0-9+.-]*:/i.test(pathPart))
49
+ return null;
50
+ if (pathPart.startsWith("/") || pathPart.startsWith("\\"))
51
+ return null;
52
+ const baseDir = from.includes("/") ? from.slice(0, from.lastIndexOf("/")).split("/") : [];
53
+ const stack = [...baseDir];
54
+ for (const seg of pathPart.split("/")) {
55
+ if (seg === "" || seg === ".")
56
+ continue;
57
+ if (seg === "..") {
58
+ if (stack.length === 0)
59
+ return null;
60
+ stack.pop();
61
+ continue;
62
+ }
63
+ stack.push(seg);
64
+ }
65
+ if (stack.length === 0)
66
+ return null;
67
+ const slug = normalizeWikiSlug(stack.join("/"));
68
+ return anchor !== undefined ? { slug, anchor } : { slug };
69
+ }
70
+ //# sourceMappingURL=wiki-links.js.map
@@ -0,0 +1,4 @@
1
+ export function neighborColumns(prev, next) {
2
+ return { above: [...prev].reverse(), below: [...next] };
3
+ }
4
+ //# sourceMappingURL=wiki-window.js.map
@@ -0,0 +1,55 @@
1
+ import { execFile, execSync } from "node:child_process";
2
+ function getTailscaleStatus() {
3
+ try {
4
+ const json = execSync("tailscale status --json", {
5
+ stdio: ["ignore", "pipe", "ignore"],
6
+ timeout: 3000,
7
+ }).toString();
8
+ const status = JSON.parse(json);
9
+ const dnsName = status?.Self?.DNSName?.replace(/\.$/, "");
10
+ return { dnsName, ips: parseTailscaleIps(json) };
11
+ }
12
+ catch {
13
+ // Tailscale not installed or not running
14
+ return {};
15
+ }
16
+ }
17
+ /** Try to get the Tailscale DNS name (e.g., "myhost.tailnet.ts.net") */
18
+ export function getTailscaleHost() {
19
+ return getTailscaleStatus().dnsName;
20
+ }
21
+ /** Get Tailscale IPs for this machine (e.g., ["100.x.y.z", "fd7a:..."]) */
22
+ export function getTailscaleIps() {
23
+ return getTailscaleStatus().ips ?? [];
24
+ }
25
+ /**
26
+ * `Self.TailscaleIPs` out of `tailscale status --json` output — or `[]`.
27
+ *
28
+ * Shared by the sync and async readers so they cannot disagree. Every way
29
+ * the input can be wrong (not JSON, no `Self`, `TailscaleIPs` missing or
30
+ * holding non-strings) yields an empty list rather than a throw: "no
31
+ * Tailscale" is an ordinary state the daemon handles, and a malformed answer
32
+ * must look like that state, never like a crash.
33
+ */
34
+ export function parseTailscaleIps(stdout) {
35
+ try {
36
+ const status = JSON.parse(stdout);
37
+ const ips = status?.Self?.TailscaleIPs;
38
+ return Array.isArray(ips) ? ips.filter((x) => typeof x === "string") : [];
39
+ }
40
+ catch {
41
+ return [];
42
+ }
43
+ }
44
+ /**
45
+ * Async twin of `getTailscaleIps` for callers on the daemon's event loop.
46
+ * The sync form blocks for up to its 3 s timeout when the Tailscale
47
+ * extension is hung — acceptable once at startup, not every minute for the
48
+ * lifetime of the daemon. Resolves to `[]` on any failure, like the sync form.
49
+ */
50
+ export function getTailscaleIpsAsync(timeoutMs = 3000) {
51
+ return new Promise((resolve) => {
52
+ execFile("tailscale", ["status", "--json"], { timeout: timeoutMs, maxBuffer: 4 * 1024 * 1024 }, (err, stdout) => resolve(err ? [] : parseTailscaleIps(String(stdout))));
53
+ });
54
+ }
55
+ //# sourceMappingURL=tailscale.js.map