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,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,197 @@
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 `brane.organelle.dev` and a daemon's
8
+ * 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
+ /** The multi-daemon SPA, which can be melded to any number of daemons. Every
19
+ * other host is a single daemon serving its own bundle. */
20
+ export const SHARED_SPA_HOST = "brane.organelle.dev";
21
+ /** True when this page is a single daemon's own bundle rather than the shared
22
+ * SPA — i.e. when "isolated" is the *current* state and the menu offers the
23
+ * way back out. */
24
+ export function isIsolatedHost(hostname) {
25
+ return hostname !== SHARED_SPA_HOST;
26
+ }
27
+ export function parseActiveTab(tab) {
28
+ return VALID_TABS.has(tab) ? tab : "chat";
29
+ }
30
+ export function parsePinnedTabs(pinned) {
31
+ if (!pinned)
32
+ return new Set();
33
+ return new Set(pinned.split(",").map((s) => s.trim()).filter((s) => VALID_TABS.has(s)));
34
+ }
35
+ export function serializePinned(pins) {
36
+ return CANONICAL_TABS.filter((t) => pins.has(t)).join(",");
37
+ }
38
+ /** The view-state query params as a plain record, with `undefined` for anything
39
+ * at its default — which is both how `router.setParams` clears a param and how
40
+ * `buildIsolateUrl` knows to drop a stale one. */
41
+ export function viewParams(tab, pins, path, line, lineEnd) {
42
+ const pinsSerialized = serializePinned(pins);
43
+ const lineStr = line
44
+ ? (lineEnd && lineEnd > line ? `${line}-${lineEnd}` : String(line))
45
+ : undefined;
46
+ return {
47
+ tab: tab !== "chat" ? tab : undefined,
48
+ pinned: pinsSerialized || undefined,
49
+ path: path || undefined,
50
+ line: lineStr,
51
+ };
52
+ }
53
+ /** The narrative's terminal, alone — where the two lifecycle actions land.
54
+ *
55
+ * Both Revive and End put you in front of a prompt you have to answer: a
56
+ * resumed agent often comes up sitting on one, and a termination is just as
57
+ * often *blocked* by one. So they navigate to a **view**, not merely to a
58
+ * narrative.
59
+ *
60
+ * It goes in the URL rather than being set after arrival because an explicit
61
+ * `tab` param is what outranks the tab layout the screen would otherwise
62
+ * restore from `sessionStorage`; state set on arrival would be racing that
63
+ * restore. No pins, per the "alone" half: whatever was pinned belongs to the
64
+ * view you were in, not to the prompt you now have to answer.
65
+ */
66
+ export function terminalOnlyHref(nid) {
67
+ const params = new URLSearchParams();
68
+ for (const [key, value] of Object.entries(viewParams("terminal", new Set()))) {
69
+ if (value)
70
+ params.set(key, value);
71
+ }
72
+ return `/${nid}?${params.toString()}`;
73
+ }
74
+ /** Reads the `line` param back: `"10"` or `"10-20"`. The counterpart of the
75
+ * `line` that `viewParams` writes, and the reason both live here — a param
76
+ * whose writer and reader sit in different modules drifts.
77
+ *
78
+ * Note the deliberate asymmetry at `lineEnd === line`: the writer collapses
79
+ * that to a bare `"10"`, so it reads back as a single line rather than a
80
+ * one-line range. Same selection either way. */
81
+ export function parseLineSpec(s) {
82
+ if (!s)
83
+ return {};
84
+ const m = s.match(/^(\d+)(?:-(\d+))?$/);
85
+ if (!m)
86
+ return {};
87
+ const line = Number(m[1]);
88
+ const end = m[2] ? Number(m[2]) : undefined;
89
+ return end !== undefined && end >= line ? { line, lineEnd: end } : { line };
90
+ }
91
+ /** The params that describe *which daemon*, as opposed to what you are looking
92
+ * at. They are origin-scoped: meaningful to the page that received them, never
93
+ * to be carried onward. */
94
+ export const CONNECTION_PARAMS = ["host", "token"];
95
+ /** A search string with the connection params removed and everything else left
96
+ * alone, ready to hand to `history.replaceState`.
97
+ *
98
+ * The shared SPA cleans a meld URL as soon as it has used it, so the token
99
+ * doesn't linger in the address bar or the back stack. Cleaning by resetting
100
+ * to the bare pathname would take the view with it — tab, pins, open file,
101
+ * scroll target — which is precisely what an Isolate hop just finished
102
+ * carrying across. Returns `""` or a string with a leading `?`.
103
+ *
104
+ * New links carry the connection in the fragment (see `connectionFragment`),
105
+ * so this is the legacy half of the cleaner: old QR codes and bookmarks still
106
+ * arrive with `?token=`, and they are still honored and still scrubbed.
107
+ */
108
+ export function withoutConnectionParams(search) {
109
+ const params = new URLSearchParams(search);
110
+ for (const key of CONNECTION_PARAMS)
111
+ params.delete(key);
112
+ const rest = params.toString();
113
+ return rest ? `?${rest}` : "";
114
+ }
115
+ /** The fragment counterpart of `withoutConnectionParams`: a hash with the
116
+ * connection params removed and anything else left alone. Returns `""` or a
117
+ * string with a leading `#`. Accepts the hash with or without its `#`. */
118
+ export function withoutConnectionHash(hash) {
119
+ const raw = hash.startsWith("#") ? hash.slice(1) : hash;
120
+ if (!raw)
121
+ return "";
122
+ const params = new URLSearchParams(raw);
123
+ for (const key of CONNECTION_PARAMS)
124
+ params.delete(key);
125
+ const rest = params.toString();
126
+ return rest ? `#${rest}` : "";
127
+ }
128
+ /** Where a link puts the daemon's address and token: the URL **fragment**.
129
+ *
130
+ * A query string is part of the HTTP request, so `?host=…&token=…` on a
131
+ * `brane.organelle.dev` link reached Cloudflare's edge with the daemon's
132
+ * bearer token in it, landed in the daemon's own request log on an Isolate
133
+ * hop, and was fetched by every chat app's link unfurler when a meld URL was
134
+ * pasted. A fragment is never sent to any server: the browser keeps it, the
135
+ * page reads it from `location.hash`, and the app strips it once consumed.
136
+ *
137
+ * The shared SPA serves every daemon and must be told which one; a daemon's
138
+ * own bundle already knows, so it gets the token alone.
139
+ */
140
+ export function connectionFragment(conn, includeHost) {
141
+ const params = new URLSearchParams();
142
+ if (includeHost)
143
+ params.set("host", conn.host);
144
+ params.set("token", conn.token);
145
+ return `#${params.toString()}`;
146
+ }
147
+ /** The same narrative, in the same view, somewhere else — the one builder
148
+ * behind both menu items.
149
+ *
150
+ * Params come from two places, and the order matters. The current search
151
+ * string seeds the set, which picks up params the screen doesn't model —
152
+ * `scrollToSeq`, so the new page opens at the message you'd jumped to. Live
153
+ * state is then overlaid and wins on everything it does own, because a layout
154
+ * restored from `sessionStorage` never writes itself back to the URL: seeding
155
+ * alone would miss exactly the case with the most to lose.
156
+ *
157
+ * Connection params are always dropped and re-set, never carried: they
158
+ * describe the origin building the link, and the target needs its own. The
159
+ * view rides in the query string; the connection rides in the fragment, which
160
+ * no server ever sees (`connectionFragment`).
161
+ */
162
+ export function buildViewUrl(target, opts) {
163
+ const { protocol, search, path, conn, view } = opts;
164
+ const params = new URLSearchParams(search);
165
+ for (const [key, value] of Object.entries(view)) {
166
+ if (value)
167
+ params.set(key, value);
168
+ else
169
+ params.delete(key);
170
+ }
171
+ for (const key of CONNECTION_PARAMS)
172
+ params.delete(key);
173
+ const query = params.toString();
174
+ const rest = (query ? `?${query}` : "") + connectionFragment(conn, target === "shared");
175
+ if (target === "shared") {
176
+ // The shared SPA serves every daemon, so it has to be told which one.
177
+ return `https://${SHARED_SPA_HOST}${path}${rest}`;
178
+ }
179
+ // A daemon's own bundle already knows which daemon it is; only the token is
180
+ // needed. The scheme follows the page building the link, so a plain-http LAN
181
+ // session doesn't hand out https links that won't load.
182
+ const scheme = protocol === "https:" ? "https" : "http";
183
+ return `${scheme}://${conn.host}${path}${rest}`;
184
+ }
185
+ /** Isolate / Unisolate: the same view, on the *other* kind of origin. Which
186
+ * way it goes is just where you are now. */
187
+ export function buildIsolateUrl(opts) {
188
+ return buildViewUrl(isIsolatedHost(opts.hostname) ? "shared" : "daemon", opts);
189
+ }
190
+ /** Meld: the same view, on another *device*. The origin kind is the one thing
191
+ * that must not change — someone reaching the daemon directly can't be handed
192
+ * a shared-SPA link, and someone on the shared app expects to stay there — so
193
+ * where Isolate flips, this stays put. */
194
+ export function buildMeldUrl(opts) {
195
+ return buildViewUrl(isIsolatedHost(opts.hostname) ? "daemon" : "shared", opts);
196
+ }
197
+ //# 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