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.
- package/AGENTS.md +362 -0
- package/CHANGELOG.md +33 -0
- package/CONTRIBUTING.md +91 -0
- package/DESIGN.md +402 -0
- package/Dockerfile +61 -0
- package/LICENSE +656 -17
- package/README.md +174 -2
- package/SECURITY.md +205 -0
- package/dist/absorb/brane-id.js +11 -0
- package/dist/absorb/chatgpt.js +161 -0
- package/dist/absorb/claude-code.js +504 -0
- package/dist/absorb/index.js +543 -0
- package/dist/absorb/organelle.js +345 -0
- package/dist/absorb/run.js +520 -0
- package/dist/absorb/sources.js +168 -0
- package/dist/absorb/types.js +3 -0
- package/dist/agent-env.js +45 -0
- package/dist/agents.js +1168 -0
- package/dist/claude-live-session.js +124 -0
- package/dist/cli.js +50 -0
- package/dist/codex-tool-log.js +144 -0
- package/dist/commands/absorb.js +291 -0
- package/dist/commands/attach.js +31 -0
- package/dist/commands/certify.js +62 -0
- package/dist/commands/config.js +116 -0
- package/dist/commands/cost.js +126 -0
- package/dist/commands/daemon.js +350 -0
- package/dist/commands/doctor.js +262 -0
- package/dist/commands/fold.js +95 -0
- package/dist/commands/list.js +89 -0
- package/dist/commands/mcp.js +84 -0
- package/dist/commands/meld.js +332 -0
- package/dist/commands/memory.js +215 -0
- package/dist/commands/revive.js +74 -0
- package/dist/commands/run.js +85 -0
- package/dist/commands/spawn.js +1094 -0
- package/dist/commands/token.js +153 -0
- package/dist/copilot-tool-log.js +145 -0
- package/dist/cost/pricing.js +116 -0
- package/dist/cost/reprice.js +136 -0
- package/dist/crush-tool-log.js +293 -0
- package/dist/daemon/agent-scope.js +116 -0
- package/dist/daemon/awaiting.js +54 -0
- package/dist/daemon/cert-manager.js +349 -0
- package/dist/daemon/client.js +262 -0
- package/dist/daemon/gated-routes.js +112 -0
- package/dist/daemon/keep-awake.js +103 -0
- package/dist/daemon/local-socket.js +145 -0
- package/dist/daemon/presence-gate.js +206 -0
- package/dist/daemon/server.js +6349 -0
- package/dist/daemon/signal-source.js +35 -0
- package/dist/daemon/tailscale-rebind.js +58 -0
- package/dist/daemon/token-ring.js +263 -0
- package/dist/daemon/token-routes.js +190 -0
- package/dist/db/events.js +152 -0
- package/dist/db/mentions.js +179 -0
- package/dist/db/schema.js +633 -0
- package/dist/db/tokens.js +583 -0
- package/dist/db/tool-uses.js +647 -0
- package/dist/diagram/index.js +85 -0
- package/dist/diagram/label.js +455 -0
- package/dist/diagram/layout.js +476 -0
- package/dist/diagram/links.js +50 -0
- package/dist/diagram/metrics.js +152 -0
- package/dist/diagram/parse.js +487 -0
- package/dist/diagram/svg.js +389 -0
- package/dist/diagram/types.js +9 -0
- package/dist/export.js +629 -0
- package/dist/files/archive-types.js +2 -0
- package/dist/files/archive.js +182 -0
- package/dist/files/browse.js +327 -0
- package/dist/files/bytes.js +162 -0
- package/dist/files/codecs.js +33 -0
- package/dist/files/find.js +212 -0
- package/dist/files/tar.js +223 -0
- package/dist/files/zip.js +209 -0
- package/dist/folds.js +72 -0
- package/dist/grammars/ebnf.js +92 -0
- package/dist/highlight.js +250 -0
- package/dist/ids.js +24 -0
- package/dist/markdown-diagnostics.js +233 -0
- package/dist/math.js +168 -0
- package/dist/mcp/server.js +886 -0
- package/dist/memory/embedder.js +145 -0
- package/dist/memory/embedding-worker.js +71 -0
- package/dist/memory/embeddings.js +91 -0
- package/dist/memory/index.js +232 -0
- package/dist/memory/storage.js +587 -0
- package/dist/memory/wiki.js +688 -0
- package/dist/memory/wordpiece.js +247 -0
- package/dist/mentions-scan.js +40 -0
- package/dist/presence.js +259 -0
- package/dist/session-affinity.js +185 -0
- package/dist/shared/api-request.js +124 -0
- package/dist/shared/block-glyphs.js +114 -0
- package/dist/shared/diagram-css.js +17 -0
- package/dist/shared/export-selection.js +101 -0
- package/dist/shared/find-in-text.js +110 -0
- package/dist/shared/glow-hues.js +225 -0
- package/dist/shared/hex-dump.js +62 -0
- package/dist/shared/hex-pages.js +52 -0
- package/dist/shared/hit-drum.js +79 -0
- package/dist/shared/known-nids.js +58 -0
- package/dist/shared/list-scroll-keys.js +56 -0
- package/dist/shared/markdown-lists.js +72 -0
- package/dist/shared/markdown-math.js +485 -0
- package/dist/shared/markdown-mentions.js +79 -0
- package/dist/shared/markdown-nids.js +41 -0
- package/dist/shared/markdown-protocols.js +53 -0
- package/dist/shared/markdown-refs.js +123 -0
- package/dist/shared/membrane-invasion.js +342 -0
- package/dist/shared/membrane-motion.js +83 -0
- package/dist/shared/membrane-palette.js +253 -0
- package/dist/shared/membrane-terrain.js +212 -0
- package/dist/shared/message-selection.js +123 -0
- package/dist/shared/mo-glyph.js +57 -0
- package/dist/shared/narrative-format.js +237 -0
- package/dist/shared/narrative-menu.js +82 -0
- package/dist/shared/narrative-sort.js +47 -0
- package/dist/shared/narrative-status.js +52 -0
- package/dist/shared/no-pan.js +85 -0
- package/dist/shared/pane-layout.js +50 -0
- package/dist/shared/payload-inline.js +83 -0
- package/dist/shared/speaker-color.js +61 -0
- package/dist/shared/token-rotation.js +14 -0
- package/dist/shared/view-url.js +197 -0
- package/dist/shared/wiki-links.js +70 -0
- package/dist/shared/wiki-window.js +4 -0
- package/dist/tailscale.js +55 -0
- package/dist/test/absorb-claude.test.js +892 -0
- package/dist/test/agent-scope.test.js +193 -0
- package/dist/test/agents.test.js +55 -0
- package/dist/test/api-request.test.js +122 -0
- package/dist/test/awaiting-glow.test.js +54 -0
- package/dist/test/awaiting.test.js +122 -0
- package/dist/test/bind-token.test.js +88 -0
- package/dist/test/block-glyphs.test.js +68 -0
- package/dist/test/cert-manager.test.js +262 -0
- package/dist/test/claude-live-session.test.js +110 -0
- package/dist/test/codex-tool-log.test.js +568 -0
- package/dist/test/config.test.js +71 -0
- package/dist/test/copilot-tool-log.test.js +93 -0
- package/dist/test/cors-preflight.test.js +49 -0
- package/dist/test/cost-linger.test.js +31 -0
- package/dist/test/cost-pricing.test.js +72 -0
- package/dist/test/cost-reprice.test.js +139 -0
- package/dist/test/cost-sessions.test.js +301 -0
- package/dist/test/crush-tool-log.test.js +289 -0
- package/dist/test/daemon-client.test.js +109 -0
- package/dist/test/daemon-socket.test.js +293 -0
- package/dist/test/diagram-arrows.test.js +205 -0
- package/dist/test/diagram-export.test.js +210 -0
- package/dist/test/diagram-labels.test.js +313 -0
- package/dist/test/diagram-parse.test.js +191 -0
- package/dist/test/diagram-render.test.js +175 -0
- package/dist/test/diagram-review.test.js +288 -0
- package/dist/test/diagram-self-loop-labels.test.js +303 -0
- package/dist/test/embedder.test.js +84 -0
- package/dist/test/events.test.js +123 -0
- package/dist/test/export-selection.test.js +102 -0
- package/dist/test/export.test.js +505 -0
- package/dist/test/files-browse.test.js +393 -0
- package/dist/test/files-find.test.js +272 -0
- package/dist/test/find-in-text.test.js +86 -0
- package/dist/test/first-paint-viewport.test.js +59 -0
- package/dist/test/fixtures/embedding-golden.js +23 -0
- package/dist/test/fixtures/wordpiece-golden.js +167 -0
- package/dist/test/glow-hues.test.js +202 -0
- package/dist/test/harness-cost.test.js +423 -0
- package/dist/test/helpers.js +21 -0
- package/dist/test/hex-pages.test.js +68 -0
- package/dist/test/highlight.test.js +69 -0
- package/dist/test/hit-drum.test.js +71 -0
- package/dist/test/hook-config.test.js +262 -0
- package/dist/test/json-gzip.test.js +70 -0
- package/dist/test/key-routing.test.js +58 -0
- package/dist/test/known-nids.test.js +106 -0
- package/dist/test/link-preview.test.js +67 -0
- package/dist/test/list-scroll-keys.test.js +66 -0
- package/dist/test/listen-own-words.test.js +147 -0
- package/dist/test/listen-timeout.test.js +33 -0
- package/dist/test/markdown-diagnostics.test.js +121 -0
- package/dist/test/markdown-lists.test.js +182 -0
- package/dist/test/markdown-math.test.js +256 -0
- package/dist/test/markdown-nids.test.js +146 -0
- package/dist/test/markdown-protocols.test.js +139 -0
- package/dist/test/mcp-bridge-cancel.test.js +98 -0
- package/dist/test/mcp-bridge-reconnect.test.js +121 -0
- package/dist/test/mcp-narrative.test.js +193 -0
- package/dist/test/mcp.test.js +548 -0
- package/dist/test/meld.test.js +85 -0
- package/dist/test/membrane-invasion.test.js +482 -0
- package/dist/test/membrane-motion.test.js +101 -0
- package/dist/test/membrane-palette.test.js +166 -0
- package/dist/test/membrane-terrain.test.js +170 -0
- package/dist/test/memory-log.test.js +202 -0
- package/dist/test/mentions-e2e.test.js +261 -0
- package/dist/test/mentions-ledger.test.js +265 -0
- package/dist/test/mentions-listen.test.js +265 -0
- package/dist/test/mentions.test.js +238 -0
- package/dist/test/message-selection.test.js +114 -0
- package/dist/test/mo-glyph.test.js +22 -0
- package/dist/test/model-resolution.test.js +166 -0
- package/dist/test/narrate-validate.test.js +77 -0
- package/dist/test/narrate-wait.test.js +160 -0
- package/dist/test/narrative-format.test.js +219 -0
- package/dist/test/narrative-menu.test.js +137 -0
- package/dist/test/narrative-sort.test.js +75 -0
- package/dist/test/no-pan.test.js +60 -0
- package/dist/test/pane-layout.test.js +153 -0
- package/dist/test/paths.test.js +37 -0
- package/dist/test/payload-inline.test.js +91 -0
- package/dist/test/precache-fonts.test.js +72 -0
- package/dist/test/presence-gate.test.js +440 -0
- package/dist/test/presence.test.js +160 -0
- package/dist/test/read-events.test.js +220 -0
- package/dist/test/remote-meld.test.js +115 -0
- package/dist/test/revive-cli.test.js +93 -0
- package/dist/test/search-floor.test.js +206 -0
- package/dist/test/search-fusion.test.js +168 -0
- package/dist/test/search.test.js +205 -0
- package/dist/test/security.test.js +48 -0
- package/dist/test/selection-wiring.test.js +61 -0
- package/dist/test/session-affinity.test.js +295 -0
- package/dist/test/signal-source.test.js +32 -0
- package/dist/test/socket-account.test.js +268 -0
- package/dist/test/spawn-toml.test.js +72 -0
- package/dist/test/speaker-color.test.js +90 -0
- package/dist/test/tailscale-rebind.test.js +79 -0
- package/dist/test/terminal-cursor.test.js +61 -0
- package/dist/test/terminal-resync.test.js +23 -0
- package/dist/test/thin-cli.test.js +382 -0
- package/dist/test/token-bootstrap.test.js +272 -0
- package/dist/test/token-devices.test.js +587 -0
- package/dist/test/token-lapse.test.js +94 -0
- package/dist/test/token-ring.test.js +188 -0
- package/dist/test/token-rotate-cli.test.js +58 -0
- package/dist/test/token-routes.test.js +95 -0
- package/dist/test/tool-uses.test.js +1006 -0
- package/dist/test/usage-ledger.test.js +126 -0
- package/dist/test/view-url.test.js +581 -0
- package/dist/test/wiki-links.test.js +57 -0
- package/dist/test/wiki-sources.test.js +85 -0
- package/dist/test/wiki-window.test.js +31 -0
- package/dist/test/wiki.test.js +399 -0
- package/dist/test/wordpiece.test.js +68 -0
- package/dist/tls.js +148 -0
- package/dist/tmux.js +202 -0
- package/dist/user-agent.js +46 -0
- package/docker-entrypoint.sh +141 -0
- package/docs/README.md +3 -0
- package/docs/absorb.md +231 -0
- package/docs/cost-ledger.md +125 -0
- package/docs/daemon-api.md +189 -0
- package/docs/diagrams.md +163 -0
- package/docs/doctor.md +36 -0
- package/docs/export-scope.md +69 -0
- package/docs/files-view.md +133 -0
- package/docs/link-previews.md +38 -0
- package/docs/math.md +32 -0
- package/docs/mcp.md +69 -0
- package/docs/memory-search.md +75 -0
- package/docs/mentions.md +101 -0
- package/docs/multi-brane.md +14 -0
- package/docs/passkeys.md +165 -0
- package/docs/presence.md +202 -0
- package/docs/releasing.md +53 -0
- package/docs/rendering-warnings.md +22 -0
- package/docs/request-layer.md +72 -0
- package/docs/selection-store.md +50 -0
- package/docs/session-affinity.md +61 -0
- package/docs/terminal-mirroring.md +211 -0
- package/docs/token-rotation.md +413 -0
- package/docs/view-urls.md +114 -0
- package/docs/web-app.md +23 -0
- package/docs/wiki.md +43 -0
- package/expo/dist/_expo/static/js/web/entry-e56efc1291dca7258ea00b8108b033a4.js +1097 -0
- package/expo/dist/apple-touch-icon.png +0 -0
- package/expo/dist/favicon.svg +3 -0
- package/expo/dist/index.html +177 -0
- package/expo/dist/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- package/expo/dist/katex/katex.min.css +1 -0
- package/expo/dist/manifest.json +16 -0
- package/expo/dist/metadata.json +1 -0
- package/expo/dist/og/membrane.v1.png +0 -0
- package/expo/dist/pdf.min.mjs +28 -0
- package/expo/dist/pdf.worker.min.mjs +28 -0
- package/expo/dist/sw.js +154 -0
- package/package.json +81 -23
- package/protocols/LIVE_loop.md +407 -0
- package/protocols/bebrief.md +41 -0
- package/protocols/digest.md +231 -0
- package/protocols/fly.md +266 -0
- package/protocols/idle.md +81 -0
- package/protocols/keeper.md +829 -0
- package/protocols/markdown.md +80 -0
- package/protocols/memory.md +419 -0
- package/protocols/mentions.md +93 -0
- package/protocols/payloads.md +53 -0
- package/protocols/prelude.md +91 -0
- package/protocols/ramify.md +105 -0
- package/protocols/review.md +356 -0
- package/protocols/rrr.md +232 -0
- package/protocols/scribe.md +429 -0
- package/protocols/summarize.md +203 -0
- package/protocols/twin.md +174 -0
- package/protocols/wiki.md +438 -0
- package/protocols/worker.md +38 -0
- package/wiki/shared/append-only-event-log.md +148 -0
- package/wiki/shared/index.md +48 -0
- package/wiki/shared/owning-your-narratives.md +161 -0
- package/wiki/shared/premature-ontologies.md +435 -0
- package/wiki/shared/sedimentary-superintelligence.md +128 -0
- package/wiki/shared/sequestering-the-narrative.md +143 -0
- package/wiki/shared/why-wikis-work.md +216 -0
- package/.npmignore +0 -2
- package/.travis.yml +0 -5
- package/index.js +0 -8
- 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,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
|