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,153 @@
1
+ import { defineCommand } from "citty";
2
+ import { resolveDaemonEndpoint, daemonRequest, warnIfVersionMismatch, } from "../daemon/client.js";
3
+ import { announcePresenceWait } from "./meld.js";
4
+ /** Sign out every device. One request: this shell holds no token, so there
5
+ * is none to swap and nothing to confirm — once the operator passes the
6
+ * presence prompt the daemon revokes every device token before it answers
7
+ * (`signOutDevices`, src/daemon/token-ring.ts). A device's rotation is the
8
+ * two-step one (docs/token-rotation.md); an answer in that shape means the
9
+ * daemon is older than this CLI, and is reported rather than taken for a
10
+ * sign-out that did not happen. */
11
+ export async function rotateDaemonToken(endpoint) {
12
+ announcePresenceWait();
13
+ const rotation = await daemonRequest(endpoint, "/api/token/rotate", {
14
+ method: "POST",
15
+ });
16
+ if (typeof rotation.signed_out !== "number") {
17
+ throw new Error("The daemon answered as an older brane does, and nobody was signed out; run `brane daemon restart` and try again");
18
+ }
19
+ return { signed_out: rotation.signed_out, replaced: rotation.replaced === true };
20
+ }
21
+ export function formatRotateResult(rotation) {
22
+ const n = rotation.signed_out;
23
+ return [
24
+ `Signed out ${n === 0 ? "no devices (none were signed in)" : n === 1 ? "1 device" : `${n} devices`}` +
25
+ `${rotation.replaced ? " (a rotation a device began and never confirmed was pending; it is now void)" : ""}.`,
26
+ "This shell and running agents are unaffected.",
27
+ "Re-admit a device with `brane meld`.",
28
+ ].join("\n");
29
+ }
30
+ const rotateCommand = defineCommand({
31
+ meta: {
32
+ name: "rotate",
33
+ description: "Sign out every web client (device); this shell and running agents are unaffected",
34
+ },
35
+ async run() {
36
+ try {
37
+ const endpoint = await resolveDaemonEndpoint();
38
+ warnIfVersionMismatch(endpoint);
39
+ console.log(formatRotateResult(await rotateDaemonToken(endpoint)));
40
+ }
41
+ catch (err) {
42
+ console.error(err instanceof Error ? err.message : String(err));
43
+ process.exit(1);
44
+ }
45
+ },
46
+ });
47
+ function formatTime(ms) {
48
+ return new Date(ms).toISOString().slice(0, 16).replace("T", " ") + "Z";
49
+ }
50
+ /** The Devices list as text: one block per token, the id first so it can be
51
+ * pasted into `brane token revoke`. */
52
+ export function formatTokenList(tokens) {
53
+ if (tokens.length === 0)
54
+ return "No live tokens.";
55
+ return tokens
56
+ .map((t) => {
57
+ const name = t.kind === "local" ? "this machine (CLI)" : t.kind === "agent" ? `agent ${t.owner_nid ?? t.label}` : t.device ?? "unknown device";
58
+ const marks = [
59
+ t.current ? "this request" : null,
60
+ t.origin === "adopted" ? "adopted from before hashing" : null,
61
+ t.unused && t.expires_at !== null ? `unscanned, lapses ${formatTime(t.expires_at)}` : null,
62
+ ].filter(Boolean);
63
+ const seen = t.connected
64
+ ? "connected now"
65
+ : t.last_used_at !== null
66
+ ? `last seen ${formatTime(t.last_used_at)}`
67
+ : "never used";
68
+ const lines = [
69
+ `${t.id} ${name}${marks.length ? ` [${marks.join("; ")}]` : ""}`,
70
+ ` "${t.label}", created ${formatTime(t.created_at)}${t.minted_by_device ? ` by ${t.minted_by_device}` : ""}, ${seen}${t.remote_addr ? `, from ${t.remote_addr}` : ""}`,
71
+ ];
72
+ if (t.descendants?.length)
73
+ lines.push(` added ${t.descendants.length} device(s): ${t.descendants.join(", ")} (revoke --cascade takes them too)`);
74
+ const running = (t.agents ?? []).filter((a) => a.status !== "ended");
75
+ if (running.length)
76
+ lines.push(` started ${running.length} running agent(s): ${running.map((a) => a.id).join(", ")}`);
77
+ if (t.elsewhere_at != null) {
78
+ const where = [t.last_device, t.last_remote_addr].filter(Boolean).join(" at ");
79
+ lines.push(` ! also used from another address${where ? ` (latest: ${where})` : ""}, ${formatTime(t.elsewhere_at)}`);
80
+ }
81
+ return lines.join("\n");
82
+ })
83
+ .join("\n");
84
+ }
85
+ const listCommand = defineCommand({
86
+ meta: {
87
+ name: "list",
88
+ description: "List the daemon's live tokens: this machine's, and one per melded device",
89
+ },
90
+ async run() {
91
+ try {
92
+ const endpoint = await resolveDaemonEndpoint();
93
+ warnIfVersionMismatch(endpoint);
94
+ const { tokens } = await daemonRequest(endpoint, "/api/tokens");
95
+ console.log(formatTokenList(tokens));
96
+ }
97
+ catch (err) {
98
+ console.error(err instanceof Error ? err.message : String(err));
99
+ process.exit(1);
100
+ }
101
+ },
102
+ });
103
+ const revokeCommand = defineCommand({
104
+ meta: {
105
+ name: "revoke",
106
+ description: "Sign out one melded device (ids from `brane token list`)",
107
+ },
108
+ args: {
109
+ id: { type: "positional", description: "Token id", required: true },
110
+ cascade: {
111
+ type: "boolean",
112
+ description: "Also revoke every device it added (see `brane token list`)",
113
+ default: false,
114
+ },
115
+ },
116
+ async run({ args }) {
117
+ try {
118
+ const endpoint = await resolveDaemonEndpoint();
119
+ warnIfVersionMismatch(endpoint);
120
+ const id = String(args.id);
121
+ // Read first: what it is (an agent's revoke reads differently) and,
122
+ // with --cascade, what it added. Revoked rows aren't listed afterwards.
123
+ const { tokens } = await daemonRequest(endpoint, "/api/tokens");
124
+ const target = tokens.find((t) => t.id === id);
125
+ const also = args.cascade ? target?.descendants ?? [] : [];
126
+ const query = also.length ? `?also=${also.map(encodeURIComponent).join(",")}` : "";
127
+ // Signing out a device waits for the operator; stopping an agent doesn't.
128
+ if (target?.kind === "device" || also.length)
129
+ announcePresenceWait();
130
+ const { label } = await daemonRequest(endpoint, `/api/tokens/${encodeURIComponent(id)}/revoke${query}`, { method: "POST" });
131
+ console.log(`Revoked "${label}"${also.length ? ` and ${also.length} token(s) it added` : ""}. ` +
132
+ (target?.kind === "agent"
133
+ ? "That agent's brane tools stop; end its session and revive it to bring it back."
134
+ : "That device is signed out until it scans a fresh Meld QR."));
135
+ }
136
+ catch (err) {
137
+ console.error(err instanceof Error ? err.message : String(err));
138
+ process.exit(1);
139
+ }
140
+ },
141
+ });
142
+ export const tokenCommand = defineCommand({
143
+ meta: {
144
+ name: "token",
145
+ description: "Manage the daemon's bearer tokens",
146
+ },
147
+ subCommands: {
148
+ list: listCommand,
149
+ revoke: revokeCommand,
150
+ rotate: rotateCommand,
151
+ },
152
+ });
153
+ //# sourceMappingURL=token.js.map
@@ -0,0 +1,145 @@
1
+ import { closeSync, openSync, readSync, statSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { recordToolUse } from "./db/tool-uses.js";
4
+ export function copilotEventsPath(sessionId, stateRoot) {
5
+ return join(stateRoot ?? join(process.env.HOME ?? "/tmp", ".copilot", "session-state"), sessionId, "events.jsonl");
6
+ }
7
+ export function newCopilotLogCursor() {
8
+ return { offset: 0, signature: null, startedAt: new Map(), names: new Map(), inputs: new Map() };
9
+ }
10
+ function copilotToolName(data) {
11
+ if (typeof data.toolName !== "string")
12
+ return undefined;
13
+ if (data.mcpServerName === "brane" &&
14
+ (data.mcpToolName === "narrate" || data.mcpToolName === "listen")) {
15
+ return `mcp__brane__${data.mcpToolName}`;
16
+ }
17
+ return data.toolName;
18
+ }
19
+ function copilotResult(result) {
20
+ if (typeof result !== "object" || result === null || Array.isArray(result))
21
+ return result;
22
+ const record = result;
23
+ if (record.content !== undefined && record.content === record.detailedContent) {
24
+ const { detailedContent: _detailedContent, ...withoutDuplicate } = record;
25
+ if (typeof withoutDuplicate.content !== "string")
26
+ return withoutDuplicate;
27
+ try {
28
+ return JSON.parse(withoutDuplicate.content);
29
+ }
30
+ catch {
31
+ return withoutDuplicate;
32
+ }
33
+ }
34
+ return result;
35
+ }
36
+ /** Translate Copilot's persisted tool start/complete events into the common
37
+ * row shape. Its call id is stable across both halves, so no heuristic pairing
38
+ * is needed. */
39
+ export function toolUseFromCopilotEvent(event) {
40
+ if (typeof event !== "object" || event === null)
41
+ return undefined;
42
+ const e = event;
43
+ if (typeof e.data !== "object" || e.data === null)
44
+ return undefined;
45
+ const data = e.data;
46
+ const id = data.toolCallId;
47
+ if (typeof id !== "string")
48
+ return undefined;
49
+ const startedAt = typeof e.timestamp === "string" ? Date.parse(e.timestamp) : NaN;
50
+ const parentId = typeof data.parentToolCallId === "string" ? data.parentToolCallId : undefined;
51
+ if (e.type === "tool.execution_start" && typeof data.toolName === "string") {
52
+ const name = copilotToolName(data);
53
+ if (!name)
54
+ return undefined;
55
+ return {
56
+ dedupKey: id, name, input: data.arguments, status: "pending",
57
+ source: "session_log", agentType: "copilot", parentId,
58
+ ...(Number.isFinite(startedAt) ? { startedAt } : {}),
59
+ };
60
+ }
61
+ if (e.type === "tool.execution_complete" && typeof data.success === "boolean") {
62
+ return {
63
+ dedupKey: id, name: "unknown", output: data.success ? copilotResult(data.result) : undefined,
64
+ status: data.success ? "completed" : "failed",
65
+ error: data.success ? undefined : typeof data.error === "string" ? data.error : data.error === undefined ? undefined : JSON.stringify(data.error),
66
+ source: "session_log", agentType: "copilot", parentId,
67
+ ...(Number.isFinite(startedAt) ? { startedAt } : {}),
68
+ };
69
+ }
70
+ return undefined;
71
+ }
72
+ /** Read only appended JSONL bytes. Newlines are single-byte UTF-8 delimiters,
73
+ * so a byte cursor never splits a multi-byte payload character. An incomplete
74
+ * final line remains unconsumed until Copilot finishes writing it later. */
75
+ export function pollCopilotToolLog(db, narrativeId, sessionId, cursor, stateRoot) {
76
+ const path = copilotEventsPath(sessionId, stateRoot);
77
+ let signature;
78
+ try {
79
+ const stat = statSync(path);
80
+ signature = `${stat.size}:${stat.mtimeMs}`;
81
+ if (signature === cursor.signature)
82
+ return [];
83
+ if (stat.size < cursor.offset) {
84
+ cursor.offset = 0;
85
+ cursor.startedAt.clear();
86
+ cursor.names.clear();
87
+ cursor.inputs.clear();
88
+ }
89
+ const bytes = stat.size - cursor.offset;
90
+ if (bytes === 0) {
91
+ cursor.signature = signature;
92
+ return [];
93
+ }
94
+ const fd = openSync(path, "r");
95
+ const pending = Buffer.allocUnsafe(bytes);
96
+ let bytesRead;
97
+ try {
98
+ bytesRead = readSync(fd, pending, 0, bytes, cursor.offset);
99
+ }
100
+ finally {
101
+ closeSync(fd);
102
+ }
103
+ const content = pending.subarray(0, bytesRead);
104
+ const completeEnd = content.lastIndexOf(0x0a);
105
+ if (completeEnd < 0)
106
+ return [];
107
+ cursor.offset += completeEnd + 1;
108
+ cursor.signature = signature;
109
+ const written = [];
110
+ for (const line of content.subarray(0, completeEnd).toString("utf8").split("\n")) {
111
+ try {
112
+ const event = JSON.parse(line);
113
+ const toolUse = toolUseFromCopilotEvent(event);
114
+ if (!toolUse || !toolUse.dedupKey)
115
+ continue;
116
+ if (toolUse.status === "pending" && toolUse.startedAt !== undefined) {
117
+ cursor.startedAt.set(toolUse.dedupKey, toolUse.startedAt);
118
+ cursor.names.set(toolUse.dedupKey, toolUse.name);
119
+ cursor.inputs.set(toolUse.dedupKey, toolUse.input);
120
+ }
121
+ else if (toolUse.status !== "pending") {
122
+ toolUse.name = cursor.names.get(toolUse.dedupKey) ?? toolUse.name;
123
+ toolUse.input = cursor.inputs.get(toolUse.dedupKey);
124
+ const startedAt = cursor.startedAt.get(toolUse.dedupKey);
125
+ if (startedAt !== undefined && toolUse.startedAt !== undefined && toolUse.startedAt >= startedAt) {
126
+ toolUse.durationMs = toolUse.startedAt - startedAt;
127
+ }
128
+ cursor.startedAt.delete(toolUse.dedupKey);
129
+ cursor.names.delete(toolUse.dedupKey);
130
+ cursor.inputs.delete(toolUse.dedupKey);
131
+ }
132
+ written.push(recordToolUse(db, narrativeId, toolUse));
133
+ }
134
+ catch {
135
+ // A malformed record cannot prevent later well-formed entries.
136
+ }
137
+ }
138
+ return written;
139
+ }
140
+ catch (err) {
141
+ console.error(`[copilot-log] reading ${path} for ${narrativeId} failed: ${err}`);
142
+ return [];
143
+ }
144
+ }
145
+ //# sourceMappingURL=copilot-tool-log.js.map
@@ -0,0 +1,116 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ // Fast mode (Opus 5.5, Opus 5, Opus 4.8) bills every token category at 2× the
5
+ // model's standard rate, with the cache multipliers applied on top — $8/$40 on
6
+ // Opus 5.5, $10/$50 on Opus 5 and 4.8. One multiplier rather than a second
7
+ // table, because that is how the rates are defined.
8
+ const FAST_MULTIPLIER = 2;
9
+ // Insertion order is load-bearing: priceForModel returns the FIRST substring
10
+ // hit, so any key that is a substring of another must come after it. Hence
11
+ // `fable-5-1` above `fable`, `opus-5-5` above `opus`, `sonnet-5` above
12
+ // `sonnet`, and `kimi-k3-fast` above `kimi-k3` — each wrong order silently
13
+ // prices the specific model at its family's rate.
14
+ const DEFAULT_PRICES = {
15
+ // Fable 5.1 / Mythos 5.1 read cache at 0.025× input; Fable 5 / Mythos 5 at
16
+ // the standard 0.1×. Same model tier, same input and output price.
17
+ "fable-5-1": { input: 10.0, output: 50.0, cacheRead: 0.25 },
18
+ "mythos-5-1": { input: 10.0, output: 50.0, cacheRead: 0.25 },
19
+ fable: { input: 10.0, output: 50.0 },
20
+ mythos: { input: 10.0, output: 50.0 },
21
+ // Opus 5.5 is cheaper than the rest of the Opus line, and reads cache at
22
+ // 0.05× input.
23
+ "opus-5-5": { input: 4.0, output: 20.0, cacheRead: 0.2 },
24
+ opus: { input: 5.0, output: 25.0 },
25
+ // Sonnet 5's launch price, announced as introductory, became its standard
26
+ // price; the increase to $3/$15 scheduled for 2026-09-01 did not happen.
27
+ "sonnet-5": { input: 2.0, output: 10.0 },
28
+ sonnet: { input: 3.0, output: 15.0 },
29
+ haiku: { input: 1.0, output: 5.0 },
30
+ // Fireworks-hosted Kimi K3 (open-weight), used by the goose/cline/crush
31
+ // harnesses. Fireworks' published serverless rates; cached input is exactly
32
+ // 0.1× input, matching the default cache-read multiplier, and cache *writes*
33
+ // are free — so callers pass zero write tokens.
34
+ "kimi-k3-fast": { input: 4.5, output: 22.5 },
35
+ "kimi-k3": { input: 3.0, output: 15.0 },
36
+ };
37
+ // The table as it stood from #385 until this revision, frozen. Ledger rows
38
+ // written under it kept one `cache_write` count rather than the 5-minute /
39
+ // 1-hour split, and this is what lets `brane cost reprice` recover that split
40
+ // from the amount it produced (see src/cost/reprice.ts). Never edit it: it
41
+ // describes rows that already exist.
42
+ export const LEGACY_PRICES_V1 = [
43
+ ["fable", { input: 10.0, output: 50.0 }],
44
+ ["mythos", { input: 10.0, output: 50.0 }],
45
+ ["opus", { input: 5.0, output: 25.0 }],
46
+ ["sonnet", { input: 3.0, output: 15.0 }],
47
+ ["haiku", { input: 1.0, output: 5.0 }],
48
+ ["kimi-k3-fast", { input: 4.5, output: 22.5 }],
49
+ ["kimi-k3", { input: 3.0, output: 15.0 }],
50
+ ];
51
+ // Fallback when a model matches no known tier — assume the most expensive
52
+ // known tier (fable/mythos) so cost is never silently under-reported.
53
+ const DEFAULT_TIER = { input: 10.0, output: 50.0 };
54
+ let _table = null;
55
+ /** Price table = an optional operator-supplied `~/.brane/pricing.json` (same
56
+ * `{ substring: {input, output, cacheRead?} }` shape) ahead of the built-in
57
+ * defaults, so prices can be corrected without a rebuild. Override keys are
58
+ * matched first: appended after the defaults, as they once were, an entry
59
+ * for `opus-5-5` could never win against `opus`. Read once and memoized. */
60
+ function priceTable() {
61
+ if (_table)
62
+ return _table;
63
+ const overrides = [];
64
+ try {
65
+ const raw = readFileSync(join(homedir(), ".brane", "pricing.json"), "utf-8");
66
+ const parsed = JSON.parse(raw);
67
+ for (const [key, val] of Object.entries(parsed)) {
68
+ if (val && typeof val.input === "number" && typeof val.output === "number") {
69
+ const price = { input: val.input, output: val.output };
70
+ if (typeof val.cacheRead === "number")
71
+ price.cacheRead = val.cacheRead;
72
+ overrides.push([key.toLowerCase(), price]);
73
+ }
74
+ }
75
+ }
76
+ catch {
77
+ // No override file (the common case) — defaults stand.
78
+ }
79
+ _table = [...overrides, ...Object.entries(DEFAULT_PRICES)];
80
+ return _table;
81
+ }
82
+ function lookup(table, model) {
83
+ const m = model.toLowerCase();
84
+ for (const [substr, price] of table) {
85
+ if (m.includes(substr))
86
+ return price;
87
+ }
88
+ return DEFAULT_TIER;
89
+ }
90
+ function price(p, u) {
91
+ // `tokens * 0.1 * input`, in that order, rather than `tokens * (0.1 * input)`:
92
+ // the default multiplier keeps the float associativity amounts were always
93
+ // computed with, so an unchanged model reprices to the identical double.
94
+ const cacheRead = p.cacheRead !== undefined ? u.cacheRead * p.cacheRead : u.cacheRead * 0.1 * p.input;
95
+ const base = (u.input * p.input +
96
+ u.output * p.output +
97
+ cacheRead +
98
+ u.cacheWrite5m * 1.25 * p.input +
99
+ u.cacheWrite1h * 2 * p.input) /
100
+ 1e6;
101
+ return u.speed === "fast" ? base * FAST_MULTIPLIER : base;
102
+ }
103
+ /** USD cost of a single message's token usage, priced by its own model. */
104
+ export function priceMessage(model, u) {
105
+ return price(lookup(priceTable(), model), u);
106
+ }
107
+ /** What `priceMessage` returned for this usage before this revision's table,
108
+ * with no pricing.json override and no fast mode. Only reprice uses it. */
109
+ export function priceMessageV1(model, u) {
110
+ return price(lookup(LEGACY_PRICES_V1, model), { ...u, speed: undefined });
111
+ }
112
+ /** The input price the legacy table charged `model`, for split recovery. */
113
+ export function legacyInputPriceV1(model) {
114
+ return lookup(LEGACY_PRICES_V1, model).input;
115
+ }
116
+ //# sourceMappingURL=pricing.js.map
@@ -0,0 +1,136 @@
1
+ import { priceMessage, priceMessageV1, legacyInputPriceV1 } from "./pricing.js";
2
+ // `brane cost reprice`: the explicit re-pricing the Cost Ledger section has
3
+ // always said the ledger makes possible. Entry rows carry the model and token
4
+ // counts that priced them, so a corrected price table can be applied to what
5
+ // was already recorded — including messages whose transcript is long gone.
6
+ //
7
+ // One fact is missing from older rows: `cache_write` holds 5-minute and
8
+ // 1-hour writes together, and they bill at 1.25× and 2× input. It is
9
+ // recoverable, because the amount on the row was produced by a known table
10
+ // (LEGACY_PRICES_V1) from exactly those tokens: pricing every write as
11
+ // 5-minute undershoots the amount by 0.75 × input × (1-hour tokens). A
12
+ // recovered split is accepted only if it is a whole number of tokens inside
13
+ // [0, cache_write] that reproduces the recorded amount; anything else — a row
14
+ // priced under a pricing.json override, say — is left alone and counted as
15
+ // unrecoverable. The first reprice writes the split it recovers, so later
16
+ // ones read it and never infer again.
17
+ /** Relative tolerance for "reproduces the recorded amount". Amounts are
18
+ * doubles computed once from integers, so an honest row matches to ~1e-15. */
19
+ const EPSILON = 1e-9;
20
+ /** The 1-hour share of a V1-priced row's `cache_write`, or null when the
21
+ * recorded amount admits no consistent split. Exported for the tests. */
22
+ export function recoverCacheWrite1h(row) {
23
+ const all5m = {
24
+ input: row.input,
25
+ output: row.output,
26
+ cacheRead: row.cache_read,
27
+ cacheWrite5m: row.cache_write,
28
+ cacheWrite1h: 0,
29
+ };
30
+ const pin = legacyInputPriceV1(row.model);
31
+ const w1 = Math.round(((row.amount - priceMessageV1(row.model, all5m)) * 1e6) / (0.75 * pin));
32
+ if (!(w1 >= 0 && w1 <= row.cache_write))
33
+ return null;
34
+ const check = priceMessageV1(row.model, { ...all5m, cacheWrite5m: row.cache_write - w1, cacheWrite1h: w1 });
35
+ const scale = Math.max(Math.abs(row.amount), 1e-6);
36
+ return Math.abs(check - row.amount) <= EPSILON * scale ? w1 : null;
37
+ }
38
+ const round6 = (n) => Math.round(n * 1e6) / 1e6;
39
+ /** Re-price every USD entry row that carries a model and token counts under
40
+ * the current table, and move each affected narrative's cached
41
+ * `cost_amount` by the same delta. Moving the column by the delta, rather
42
+ * than resetting it to the ledger sum, is deliberate: a column above the
43
+ * ledger sum holds pre-ledger cost from transcripts that may be gone
44
+ * (sampleNarrativeCostDetailed keeps it for that reason), and a reprice must
45
+ * correct the rows it touched without discarding that. One transaction; a
46
+ * dry run computes the same result and writes nothing. */
47
+ export function repriceLedger(db, opts = {}) {
48
+ const rows = db
49
+ .prepare(`SELECT key, narrative_id, model, amount, input, output, cache_read, cache_write, cache_write_1h, speed
50
+ FROM usage_ledger
51
+ WHERE unit = 'USD' AND model IS NOT NULL
52
+ AND input IS NOT NULL AND output IS NOT NULL
53
+ AND cache_read IS NOT NULL AND cache_write IS NOT NULL
54
+ ORDER BY key`)
55
+ .all();
56
+ const models = new Map();
57
+ const deltas = new Map();
58
+ const updates = [];
59
+ let recovered = 0;
60
+ let unrecoverable = 0;
61
+ for (const row of rows) {
62
+ const line = models.get(row.model) ?? { model: row.model, rows: 0, changed: 0, before: 0, after: 0 };
63
+ models.set(row.model, line);
64
+ line.rows++;
65
+ line.before += row.amount;
66
+ let w1 = row.cache_write_1h;
67
+ if (w1 === null) {
68
+ w1 = recoverCacheWrite1h(row);
69
+ if (w1 === null) {
70
+ unrecoverable++;
71
+ line.after += row.amount;
72
+ continue;
73
+ }
74
+ recovered++;
75
+ }
76
+ const amount = priceMessage(row.model, {
77
+ input: row.input,
78
+ output: row.output,
79
+ cacheRead: row.cache_read,
80
+ cacheWrite5m: row.cache_write - w1,
81
+ cacheWrite1h: w1,
82
+ speed: row.speed ?? undefined,
83
+ });
84
+ line.after += amount;
85
+ const changed = Math.abs(amount - row.amount) > EPSILON * Math.max(Math.abs(row.amount), 1e-6);
86
+ if (changed) {
87
+ line.changed++;
88
+ deltas.set(row.narrative_id, (deltas.get(row.narrative_id) ?? 0) + (amount - row.amount));
89
+ }
90
+ if (changed || row.cache_write_1h === null) {
91
+ updates.push({ key: row.key, amount: changed ? amount : row.amount, w1 });
92
+ }
93
+ }
94
+ const ledgerSum = db.prepare("SELECT SUM(amount) AS amount FROM usage_ledger WHERE narrative_id = ? AND unit = 'USD'");
95
+ const column = db.prepare("SELECT cost_amount, cost_unit FROM narratives WHERE id = ?");
96
+ const narratives = [];
97
+ for (const [nid, delta] of deltas) {
98
+ const before = ledgerSum.get(nid).amount ?? 0;
99
+ const after = before + delta;
100
+ const col = column.get(nid);
101
+ const colBefore = col && col.cost_unit === "USD" ? col.cost_amount : null;
102
+ narratives.push({
103
+ nid,
104
+ ledger_before: round6(before),
105
+ ledger_after: round6(after),
106
+ column_before: colBefore,
107
+ column_after: colBefore === null ? null : round6(Math.max(after, colBefore + delta)),
108
+ });
109
+ }
110
+ narratives.sort((a, b) => a.ledger_after - a.ledger_before - (b.ledger_after - b.ledger_before));
111
+ if (!opts.dryRun) {
112
+ const setRow = db.prepare("UPDATE usage_ledger SET amount = ?, cache_write_1h = ? WHERE key = ?");
113
+ const setColumn = db.prepare("UPDATE narratives SET cost_amount = ? WHERE id = ? AND cost_unit = 'USD'");
114
+ db.transaction(() => {
115
+ for (const u of updates)
116
+ setRow.run(u.amount, u.w1, u.key);
117
+ for (const n of narratives) {
118
+ if (n.column_after !== null)
119
+ setColumn.run(n.column_after, n.nid);
120
+ }
121
+ })();
122
+ }
123
+ const modelLines = [...models.values()]
124
+ .map((m) => ({ ...m, before: round6(m.before), after: round6(m.after) }))
125
+ .sort((a, b) => b.before - a.before);
126
+ return {
127
+ dry_run: !!opts.dryRun,
128
+ scanned: rows.length,
129
+ changed: modelLines.reduce((n, m) => n + m.changed, 0),
130
+ recovered,
131
+ unrecoverable,
132
+ models: modelLines,
133
+ narratives,
134
+ };
135
+ }
136
+ //# sourceMappingURL=reprice.js.map