brane 0.1.1 → 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 (348) 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 +79 -27
  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/.travis.yml +0 -7
  336. package/lib/brane.cjs.js +0 -12
  337. package/lib/brane.cjs.js.map +0 -1
  338. package/lib/brane.d.ts +0 -11
  339. package/lib/brane.js +0 -6
  340. package/lib/brane.js.map +0 -1
  341. package/lib/tests.d.ts +0 -1
  342. package/lib/tests.js +0 -8
  343. package/lib/tests.js.map +0 -1
  344. package/rollup.config.js +0 -21
  345. package/scripts/test.sh +0 -8
  346. package/src/brane.ts +0 -21
  347. package/src/tests.ts +0 -8
  348. package/tsconfig.json +0 -16
@@ -0,0 +1,68 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { blockGlyphClass, blockGlyphStylesheet, paintBlockGlyphs, } from "../shared/block-glyphs.js";
4
+ // The terminal mirror paints block elements rather than typesetting them, so
5
+ // `brane meld`'s QR code has no gap between rows. These pin the two halves
6
+ // that have to agree: every glyph the painter wraps has a rule that paints it,
7
+ // and the painter leaves everything else — nearly every line — alone.
8
+ const ALL_BLOCKS = Array.from({ length: 0x20 }, (_, i) => 0x2580 + i);
9
+ describe("paintBlockGlyphs", () => {
10
+ it("returns a line with no block element unchanged", () => {
11
+ const html = '<span style="color:rgb(0,187,0)">$ ls -la</span> ─│ ●';
12
+ assert.equal(paintBlockGlyphs(html), html);
13
+ });
14
+ it("wraps each block element in its own span, keeping the character", () => {
15
+ assert.equal(paintBlockGlyphs("a▀▄b"), 'a<span class="tb tb-2580">▀</span><span class="tb tb-2584">▄</span>b');
16
+ });
17
+ it("paints inside ansi_up's color spans, so currentColor is the SGR color", () => {
18
+ const html = '<span style="color:rgb(255,0,0)">█</span>';
19
+ assert.equal(paintBlockGlyphs(html), '<span style="color:rgb(255,0,0)"><span class="tb tb-2588">█</span></span>');
20
+ });
21
+ it("keeps the text content identical, so copy yields the real characters", () => {
22
+ const line = "▄▄▄▄▄▄▄ ▄ ▄▄ ░▒▓ ▙▟";
23
+ const stripped = paintBlockGlyphs(line).replace(/<[^>]*>/g, "");
24
+ assert.equal(stripped, line);
25
+ });
26
+ it("covers every codepoint in U+2580–U+259F", () => {
27
+ const line = String.fromCharCode(...ALL_BLOCKS);
28
+ const classes = [...paintBlockGlyphs(line).matchAll(/class="tb (tb-[0-9a-f]+)"/g)].map((m) => m[1]);
29
+ assert.deepEqual(classes, ALL_BLOCKS.map(blockGlyphClass));
30
+ });
31
+ it("leaves the neighbors of the range alone", () => {
32
+ const line = "╿■"; // ╿ and ■, just outside the block
33
+ assert.equal(paintBlockGlyphs(line), line);
34
+ });
35
+ });
36
+ describe("blockGlyphStylesheet", () => {
37
+ const css = blockGlyphStylesheet(1.4);
38
+ it("gives every class the painter emits a background rule", () => {
39
+ for (const cp of ALL_BLOCKS) {
40
+ assert.match(css, new RegExp(`\\.${blockGlyphClass(cp)}\\{background:`), `U+${cp.toString(16)}`);
41
+ }
42
+ });
43
+ it("makes the cell exactly one line box tall and one cell wide", () => {
44
+ assert.match(css, /\.tb\{[^}]*height:1\.4em/);
45
+ assert.match(css, /\.tb\{[^}]*width:1ch/);
46
+ assert.match(css, /\.tb\{[^}]*vertical-align:top/);
47
+ });
48
+ it("hides the glyph without touching color, which the paint is drawn in", () => {
49
+ assert.match(css, /-webkit-text-fill-color:transparent/);
50
+ assert.doesNotMatch(css, /[{;]color:/);
51
+ });
52
+ it("paints half blocks as one full-tile gradient with a hard stop", () => {
53
+ assert.match(css, /\.tb-2580\{background:linear-gradient\(to bottom,currentColor 50%,transparent 50%\)\}/);
54
+ assert.match(css, /\.tb-2584\{background:linear-gradient\(to top,currentColor 50%,transparent 50%\)\}/);
55
+ assert.match(css, /\.tb-2590\{background:linear-gradient\(to left,currentColor 50%,transparent 50%\)\}/);
56
+ assert.match(css, /\.tb-2588\{background:currentColor\}/);
57
+ });
58
+ it("paints quadrants as one conic sweep: ▚ is upper-left and lower-right", () => {
59
+ assert.match(css, /\.tb-259a\{background:conic-gradient\(at 50% 50%,transparent 0 25%,currentColor 0 50%,transparent 0 75%,currentColor 0\)\}/);
60
+ });
61
+ // A layer sized and positioned inside the cell anti-aliases its own edges on
62
+ // the fractional pixels a 16.8px row lands on, leaving a hairline between
63
+ // lit rows. Full-tile images take the box's snapped edges instead.
64
+ it("never sizes or positions a layer", () => {
65
+ assert.doesNotMatch(css, /no-repeat|\s\/\s/);
66
+ });
67
+ });
68
+ //# sourceMappingURL=block-glyphs.test.js.map
@@ -0,0 +1,262 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdtempSync, mkdirSync, writeFileSync, readFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { setTimeout as sleep } from "node:timers/promises";
7
+ import { execFileSync } from "node:child_process";
8
+ import { inspectCert, parseCertExpiry } from "../tls.js";
9
+ import { CertManager } from "../daemon/cert-manager.js";
10
+ function opensslAvailable() {
11
+ try {
12
+ execFileSync("openssl", ["version"], { stdio: "ignore" });
13
+ return true;
14
+ }
15
+ catch {
16
+ return false;
17
+ }
18
+ }
19
+ /**
20
+ * Returns true if the local openssl supports `-not_before`/`-not_after` on
21
+ * `openssl req`. macOS LibreSSL and OpenSSL 3.x do; older OpenSSL 1.x does
22
+ * not. Tests that need explicit date control skip when this is false.
23
+ */
24
+ function opensslSupportsExplicitDates() {
25
+ if (!HAS_OPENSSL)
26
+ return false;
27
+ try {
28
+ const dir = mkdtempSync(join(tmpdir(), "brane-openssl-probe-"));
29
+ execFileSync("openssl", [
30
+ "req", "-x509", "-newkey", "rsa:2048", "-nodes",
31
+ "-keyout", join(dir, "k"), "-out", join(dir, "c"),
32
+ "-subj", "/CN=probe",
33
+ "-not_before", "20200101000000Z",
34
+ "-not_after", "20200102000000Z",
35
+ ], { stdio: "ignore" });
36
+ return true;
37
+ }
38
+ catch {
39
+ return false;
40
+ }
41
+ }
42
+ function formatAsn1Time(d) {
43
+ // openssl wants YYYYMMDDHHMMSSZ (UTC).
44
+ const pad = (n) => String(n).padStart(2, "0");
45
+ return (String(d.getUTCFullYear()) +
46
+ pad(d.getUTCMonth() + 1) +
47
+ pad(d.getUTCDate()) +
48
+ pad(d.getUTCHours()) +
49
+ pad(d.getUTCMinutes()) +
50
+ pad(d.getUTCSeconds()) + "Z");
51
+ }
52
+ function generateSelfSignedCert(opts) {
53
+ const dir = mkdtempSync(join(tmpdir(), "brane-cert-gen-"));
54
+ const keyPath = join(dir, "k.pem");
55
+ const certPath = join(dir, "c.pem");
56
+ const args = [
57
+ "req", "-x509", "-newkey", "rsa:2048", "-nodes",
58
+ "-keyout", keyPath, "-out", certPath,
59
+ "-subj", `/CN=${opts.host}`,
60
+ ];
61
+ if (opts.notBefore) {
62
+ // Explicit notBefore/notAfter (used for expired-cert tests). Requires
63
+ // an openssl that supports the flags; callers gate on this.
64
+ const notBefore = opts.notBefore;
65
+ const notAfter = new Date(notBefore.getTime() + opts.notAfterOffsetMs);
66
+ args.push("-not_before", formatAsn1Time(notBefore));
67
+ args.push("-not_after", formatAsn1Time(notAfter));
68
+ }
69
+ else {
70
+ // Days-based path — what most tests use. Round up to >= 1 day.
71
+ const days = Math.max(1, Math.ceil(opts.notAfterOffsetMs / 86_400_000));
72
+ args.push("-days", String(days));
73
+ }
74
+ try {
75
+ execFileSync("openssl", args, { stdio: ["ignore", "ignore", "pipe"] });
76
+ }
77
+ catch (err) {
78
+ const e = err;
79
+ const stderr = e.stderr?.toString() ?? "";
80
+ throw new Error(`openssl req failed (exit ${e.status ?? "?"}): ${stderr.trim() || "(no stderr)"}\n` +
81
+ `args: ${args.join(" ")}`);
82
+ }
83
+ const cert = readFileSync(certPath);
84
+ const key = readFileSync(keyPath);
85
+ if (cert.length === 0) {
86
+ throw new Error(`openssl produced an empty cert file at ${certPath}`);
87
+ }
88
+ return { cert, key };
89
+ }
90
+ function setupCertDir(initial) {
91
+ const dir = mkdtempSync(join(tmpdir(), "brane-certmgr-"));
92
+ const certsDir = join(dir, "certs");
93
+ mkdirSync(certsDir, { recursive: true });
94
+ const certPath = join(certsDir, "tls.crt");
95
+ const keyPath = join(certsDir, "tls.key");
96
+ if (initial) {
97
+ writeFileSync(certPath, initial.cert);
98
+ writeFileSync(keyPath, initial.key);
99
+ }
100
+ return { certPath, keyPath, dir };
101
+ }
102
+ const HAS_OPENSSL = opensslAvailable();
103
+ const HAS_OPENSSL_DATES = HAS_OPENSSL && opensslSupportsExplicitDates();
104
+ describe("parseCertExpiry / inspectCert", () => {
105
+ it("returns null for a missing file", () => {
106
+ assert.equal(parseCertExpiry("/tmp/definitely-not-a-cert-file-xyz"), null);
107
+ assert.equal(inspectCert("/tmp/definitely-not-a-cert-file-xyz"), null);
108
+ });
109
+ it("returns null for a non-PEM file", () => {
110
+ const dir = mkdtempSync(join(tmpdir(), "brane-bad-cert-"));
111
+ const path = join(dir, "garbage.crt");
112
+ writeFileSync(path, "this is not a certificate\n");
113
+ assert.equal(parseCertExpiry(path), null);
114
+ assert.equal(inspectCert(path), null);
115
+ });
116
+ it("parses a real cert's expiry and subject", { skip: !HAS_OPENSSL }, () => {
117
+ const { cert } = generateSelfSignedCert({ host: "test.example.com", notAfterOffsetMs: 30 * 86_400_000 });
118
+ const dir = mkdtempSync(join(tmpdir(), "brane-good-cert-"));
119
+ const path = join(dir, "ok.crt");
120
+ writeFileSync(path, cert);
121
+ const inspected = inspectCert(path);
122
+ assert.ok(inspected, "expected cert to inspect");
123
+ assert.ok(inspected.notAfter.getTime() > Date.now(), "expected notAfter in the future");
124
+ assert.match(inspected.subject, /CN=test\.example\.com/);
125
+ });
126
+ });
127
+ describe("CertManager.ensureCert", () => {
128
+ it("reports disabled when there is no cert and no Tailscale", { skip: !HAS_OPENSSL }, async () => {
129
+ const { certPath, keyPath } = setupCertDir();
130
+ const mgr = new CertManager({
131
+ certPath, keyPath,
132
+ onReload: () => { },
133
+ resolveHost: () => undefined,
134
+ });
135
+ const ok = await mgr.ensureCert();
136
+ assert.equal(ok, false);
137
+ assert.equal(mgr.getStatus().state, "disabled");
138
+ assert.equal(mgr.getStatus().expiresAt, null);
139
+ mgr.stop();
140
+ });
141
+ it("treats a valid existing cert as the steady state without invoking tailscale", { skip: !HAS_OPENSSL }, async () => {
142
+ const fixture = generateSelfSignedCert({ host: "host.example.com", notAfterOffsetMs: 60 * 86_400_000 });
143
+ const { certPath, keyPath } = setupCertDir(fixture);
144
+ let reloadFired = false;
145
+ const mgr = new CertManager({
146
+ certPath, keyPath,
147
+ onReload: () => { reloadFired = true; },
148
+ resolveHost: () => "host.example.com",
149
+ });
150
+ const ok = await mgr.ensureCert();
151
+ assert.equal(ok, true);
152
+ assert.equal(mgr.getStatus().state, "valid");
153
+ const status = mgr.getStatus();
154
+ assert.ok(status.expiresAt && status.expiresAt.getTime() > Date.now());
155
+ // ensureCert must not reload — the file on disk is already what HTTPS
156
+ // would bind with.
157
+ assert.equal(reloadFired, false);
158
+ mgr.stop();
159
+ });
160
+ it("returns false when the cert is genuinely expired and Tailscale is gone", { skip: !HAS_OPENSSL_DATES }, async () => {
161
+ // Genuinely expired: notBefore six years ago, notAfter five years ago.
162
+ // Asserts that an expired-cert-on-disk + no-Tailscale path lands in
163
+ // "disabled" rather than mistakenly reporting "valid".
164
+ const sixYearsAgo = new Date(Date.now() - 6 * 365 * 86_400_000);
165
+ const fixture = generateSelfSignedCert({
166
+ host: "h.example.com",
167
+ notBefore: sixYearsAgo,
168
+ notAfterOffsetMs: 365 * 86_400_000, // one year after notBefore -> still expired
169
+ });
170
+ const { certPath, keyPath } = setupCertDir(fixture);
171
+ const mgr = new CertManager({
172
+ certPath, keyPath,
173
+ onReload: () => { },
174
+ resolveHost: () => undefined, // no tailscale
175
+ });
176
+ const ok = await mgr.ensureCert();
177
+ assert.equal(ok, false);
178
+ assert.equal(mgr.getStatus().state, "disabled");
179
+ // expiresAt reflects the on-disk cert (caller may want to log it).
180
+ const expiry = mgr.getStatus().expiresAt;
181
+ assert.ok(expiry && expiry.getTime() < Date.now());
182
+ mgr.stop();
183
+ });
184
+ it("falls through to provisioning when the existing cert's CN doesn't match the Tailscale host", { skip: !HAS_OPENSSL }, async () => {
185
+ // Regression for the review item: a valid-but-wrong-subject cert
186
+ // would be accepted as "valid" by ensureCert and the daemon would
187
+ // bind HTTPS with the wrong cert. We can't actually shell out to
188
+ // tailscale in a test, so instead we assert ensureCert detects the
189
+ // mismatch and tries to provision (failing because tailscale isn't
190
+ // really there) rather than reporting "valid".
191
+ const fixture = generateSelfSignedCert({
192
+ host: "old.example.com",
193
+ notAfterOffsetMs: 60 * 86_400_000,
194
+ });
195
+ const { certPath, keyPath } = setupCertDir(fixture);
196
+ let provisionAttempted = false;
197
+ const mgr = new CertManager({
198
+ certPath, keyPath,
199
+ onReload: () => { },
200
+ // Host has changed since the cert was issued.
201
+ resolveHost: () => "new.example.com",
202
+ // Quiet the "Provisioning..." log line during the test; also
203
+ // gives us a way to detect that we actually entered Case C.
204
+ log: (_level, msg) => {
205
+ if (msg.includes("does not match"))
206
+ provisionAttempted = true;
207
+ },
208
+ });
209
+ const ok = await mgr.ensureCert();
210
+ assert.equal(ok, false, "expected provisioning to fail (tailscale stub)");
211
+ assert.equal(provisionAttempted, true, "expected the subject-mismatch log line");
212
+ assert.notEqual(mgr.getStatus().state, "valid");
213
+ mgr.stop();
214
+ });
215
+ });
216
+ describe("CertManager.fs.watch hot reload", () => {
217
+ it("calls onReload when the cert file changes underneath us", { skip: !HAS_OPENSSL }, async () => {
218
+ const initial = generateSelfSignedCert({ host: "h.example.com", notAfterOffsetMs: 60 * 86_400_000 });
219
+ const { certPath, keyPath } = setupCertDir(initial);
220
+ let reloadCount = 0;
221
+ const reloadedCerts = [];
222
+ const mgr = new CertManager({
223
+ certPath, keyPath,
224
+ onReload: (cert) => { reloadCount++; reloadedCerts.push(cert); },
225
+ resolveHost: () => "h.example.com",
226
+ });
227
+ await mgr.ensureCert();
228
+ mgr.start();
229
+ // Give the watcher a tick to attach.
230
+ await sleep(50);
231
+ // Write a different cert: should trigger reload after debounce.
232
+ const replacement = generateSelfSignedCert({ host: "h.example.com", notAfterOffsetMs: 90 * 86_400_000 });
233
+ writeFileSync(certPath, replacement.cert);
234
+ writeFileSync(keyPath, replacement.key);
235
+ // Wait past the 200ms debounce.
236
+ await sleep(400);
237
+ assert.ok(reloadCount >= 1, `expected at least one reload, got ${reloadCount}`);
238
+ // The reloaded cert bytes must match what we wrote (not the original).
239
+ assert.ok(reloadedCerts[reloadedCerts.length - 1].equals(replacement.cert));
240
+ mgr.stop();
241
+ });
242
+ it("does not call onReload when written bytes have the same notAfter", { skip: !HAS_OPENSSL }, async () => {
243
+ const fixture = generateSelfSignedCert({ host: "h.example.com", notAfterOffsetMs: 60 * 86_400_000 });
244
+ const { certPath, keyPath } = setupCertDir(fixture);
245
+ let reloadCount = 0;
246
+ const mgr = new CertManager({
247
+ certPath, keyPath,
248
+ onReload: () => { reloadCount++; },
249
+ resolveHost: () => "h.example.com",
250
+ });
251
+ await mgr.ensureCert();
252
+ mgr.start();
253
+ await sleep(50);
254
+ // Touch the file with the same bytes — fs.watch fires, but the
255
+ // notAfter hasn't changed, so we should suppress the reload.
256
+ writeFileSync(certPath, fixture.cert);
257
+ await sleep(400);
258
+ assert.equal(reloadCount, 0);
259
+ mgr.stop();
260
+ });
261
+ });
262
+ //# sourceMappingURL=cert-manager.test.js.map
@@ -0,0 +1,110 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { createTestDb } from "./helpers.js";
4
+ import { liveSessionFromRegistryEntry, parsePanePids } from "../claude-live-session.js";
5
+ import { maybeFollowLiveSession, getSessionIds } from "../daemon/server.js";
6
+ import { appendEvent } from "../db/events.js";
7
+ // A registry entry as Claude Code 2.1.281 writes it, trimmed to the fields read.
8
+ const entry = (over = {}) => JSON.stringify({
9
+ pid: 50960,
10
+ sessionId: "f3238166-cc8a-4f28-bfd3-7586b2e1abd1",
11
+ cwd: "/Users/op/dev/app",
12
+ startedAt: 1_000_000,
13
+ kind: "interactive",
14
+ tmux: "claude-3j4583pl:@106.%106",
15
+ ...over,
16
+ });
17
+ const expect = { narrativeId: "claude-3j4583pl", pid: 50960, foldPath: "/Users/op/dev/app", launchedAt: 1_000_000 };
18
+ describe("liveSessionFromRegistryEntry", () => {
19
+ it("reads the session of the narrative's own pane process", () => {
20
+ assert.equal(liveSessionFromRegistryEntry(entry(), expect), "f3238166-cc8a-4f28-bfd3-7586b2e1abd1");
21
+ });
22
+ it("refuses an entry for another pid (a recycled or foreign file)", () => {
23
+ assert.equal(liveSessionFromRegistryEntry(entry({ pid: 50961 }), expect), undefined);
24
+ });
25
+ it("refuses an entry naming another tmux session, prefixes included", () => {
26
+ assert.equal(liveSessionFromRegistryEntry(entry({ tmux: "claude-0i460qwe:@1.%1" }), expect), undefined);
27
+ assert.equal(liveSessionFromRegistryEntry(entry({ tmux: "claude-3j4583pl2:@1.%1" }), expect), undefined);
28
+ assert.equal(liveSessionFromRegistryEntry(entry({ tmux: undefined }), expect), undefined);
29
+ });
30
+ it("refuses an entry from another directory", () => {
31
+ assert.equal(liveSessionFromRegistryEntry(entry({ cwd: "/Users/op/dev/other" }), expect), undefined);
32
+ });
33
+ it("refuses an entry left by the pane's previous process (started before this launch)", () => {
34
+ assert.equal(liveSessionFromRegistryEntry(entry({ startedAt: 1_000_000 - 3_600_000 }), expect), undefined);
35
+ });
36
+ it("refuses a non-interactive process, and anything unparseable", () => {
37
+ assert.equal(liveSessionFromRegistryEntry(entry({ kind: "print" }), expect), undefined);
38
+ assert.equal(liveSessionFromRegistryEntry("{not json", expect), undefined);
39
+ assert.equal(liveSessionFromRegistryEntry("null", expect), undefined);
40
+ assert.equal(liveSessionFromRegistryEntry(entry({ sessionId: "" }), expect), undefined);
41
+ });
42
+ });
43
+ describe("parsePanePids", () => {
44
+ it("maps each single-pane session to its pane pid", () => {
45
+ const pids = parsePanePids("claude-a 101\nclaude-b 202\n");
46
+ assert.equal(pids.get("claude-a"), 101);
47
+ assert.equal(pids.get("claude-b"), 202);
48
+ });
49
+ it("maps a multi-pane session to nothing — it has no single agent process", () => {
50
+ const pids = parsePanePids("claude-a 101\nclaude-a 303\nclaude-b 202\n");
51
+ assert.equal(pids.has("claude-a"), false);
52
+ assert.equal(pids.get("claude-b"), 202);
53
+ });
54
+ it("skips malformed lines", () => {
55
+ assert.equal(parsePanePids("\nclaude-a x\nclaude-b 1\n 7\n").size, 0);
56
+ });
57
+ });
58
+ describe("maybeFollowLiveSession", () => {
59
+ function seed(db, agent = "claude") {
60
+ db.prepare(`INSERT INTO narratives (id, brane_id, agent_type, role, created_at, status, fold_path, agent_session_id, session_ids)
61
+ VALUES ('n1', 'brane-test', ?, 'worker', 5000, 'active', '/tmp/fold', 'sess-launch', '["sess-launch"]')`).run(agent);
62
+ }
63
+ const pointer = (db) => db.prepare("SELECT agent_session_id FROM narratives WHERE id = 'n1'").get()
64
+ .agent_session_id;
65
+ it("follows a /clear: moves the pointer and keeps the old session in the cost set", () => {
66
+ const db = createTestDb();
67
+ seed(db);
68
+ const got = maybeFollowLiveSession(db, "n1", "claude", "/tmp/fold", 5000, "sess-launch", () => "sess-cleared");
69
+ assert.equal(got, "sess-cleared");
70
+ assert.equal(pointer(db), "sess-cleared");
71
+ assert.deepEqual(getSessionIds(db, "n1"), ["sess-launch", "sess-cleared"]);
72
+ });
73
+ it("is a no-op when the live session is the pointer, or there is no evidence", () => {
74
+ const db = createTestDb();
75
+ seed(db);
76
+ assert.equal(maybeFollowLiveSession(db, "n1", "claude", "/tmp/fold", 5000, "sess-launch", () => "sess-launch"), undefined);
77
+ assert.equal(maybeFollowLiveSession(db, "n1", "claude", "/tmp/fold", 5000, "sess-launch", () => undefined), undefined);
78
+ assert.equal(pointer(db), "sess-launch");
79
+ assert.deepEqual(getSessionIds(db, "n1"), ["sess-launch"]);
80
+ });
81
+ it("leaves capture-strategy agents to maybeCaptureSessionId", () => {
82
+ const db = createTestDb();
83
+ seed(db, "codex");
84
+ assert.equal(maybeFollowLiveSession(db, "n1", "codex", "/tmp/fold", 5000, "sess-launch", () => "other"), undefined);
85
+ assert.equal(pointer(db), "sess-launch");
86
+ });
87
+ it("anchors on the latest revive, not the spawn", () => {
88
+ const db = createTestDb();
89
+ seed(db);
90
+ appendEvent(db, "n1", "restarted", { content: "revived" });
91
+ const restartedAt = db.prepare("SELECT MAX(time) AS t FROM events WHERE type = 'restarted'").get().t;
92
+ let seen;
93
+ maybeFollowLiveSession(db, "n1", "claude", "/tmp/fold", 5000, "sess-launch", (o) => {
94
+ seen = o.launchedAt;
95
+ return undefined;
96
+ });
97
+ assert.equal(seen, restartedAt);
98
+ });
99
+ it("passes the fold as an absolute path", () => {
100
+ const db = createTestDb();
101
+ seed(db);
102
+ let fold;
103
+ maybeFollowLiveSession(db, "n1", "claude", "~/dev/app", 5000, "sess-launch", (o) => {
104
+ fold = o.foldPath;
105
+ return undefined;
106
+ });
107
+ assert.ok(fold && !fold.startsWith("~"), fold);
108
+ });
109
+ });
110
+ //# sourceMappingURL=claude-live-session.test.js.map