brane 0.1.1 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +367 -0
- package/CHANGELOG.md +66 -0
- package/CONTRIBUTING.md +101 -0
- package/DESIGN.md +405 -0
- package/Dockerfile +61 -0
- package/LICENSE +656 -17
- package/README.md +174 -2
- package/SECURITY.md +213 -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 +133 -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 +360 -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 +1101 -0
- package/dist/commands/token.js +154 -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 +6353 -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 +643 -0
- package/dist/db/tokens.js +624 -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/app-origin.js +26 -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 +84 -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 +216 -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/app-host-literal.test.js +53 -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 +56 -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 +299 -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 +143 -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 +150 -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 +128 -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/terminal-size.test.js +36 -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 +646 -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 +627 -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/test/wrangler-route.test.js +27 -0
- package/dist/tls.js +148 -0
- package/dist/tmux.js +213 -0
- package/dist/user-agent.js +46 -0
- package/docker-entrypoint.sh +154 -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 +104 -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 +243 -0
- package/docs/token-rotation.md +431 -0
- package/docs/view-urls.md +134 -0
- package/docs/web-app.md +23 -0
- package/docs/wiki.md +43 -0
- package/expo/dist/_expo/static/js/web/entry-e94247f119fdcd3b424a1764233a41d2.js +1099 -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 +79 -27
- 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 +53 -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/.travis.yml +0 -7
- package/lib/brane.cjs.js +0 -12
- package/lib/brane.cjs.js.map +0 -1
- package/lib/brane.d.ts +0 -11
- package/lib/brane.js +0 -6
- package/lib/brane.js.map +0 -1
- package/lib/tests.d.ts +0 -1
- package/lib/tests.js +0 -8
- package/lib/tests.js.map +0 -1
- package/rollup.config.js +0 -21
- package/scripts/test.sh +0 -8
- package/src/brane.ts +0 -21
- package/src/tests.ts +0 -8
- package/tsconfig.json +0 -16
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import { mkdirSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { ensureBraneDir } from "../db/schema.js";
|
|
5
|
+
import { getTailscaleHost } from "../tailscale.js";
|
|
6
|
+
import { provisionTlsCert } from "../tls.js";
|
|
7
|
+
function getCertsDir() {
|
|
8
|
+
const dir = join(ensureBraneDir(), "certs");
|
|
9
|
+
mkdirSync(dir, { recursive: true });
|
|
10
|
+
return dir;
|
|
11
|
+
}
|
|
12
|
+
export function getTlsCertPath() {
|
|
13
|
+
return join(getCertsDir(), "tls.crt");
|
|
14
|
+
}
|
|
15
|
+
export function getTlsKeyPath() {
|
|
16
|
+
return join(getCertsDir(), "tls.key");
|
|
17
|
+
}
|
|
18
|
+
export const certifyCommand = defineCommand({
|
|
19
|
+
meta: {
|
|
20
|
+
name: "certify",
|
|
21
|
+
description: "Provision TLS certificates for Tailscale HTTPS access",
|
|
22
|
+
},
|
|
23
|
+
args: {
|
|
24
|
+
host: {
|
|
25
|
+
type: "string",
|
|
26
|
+
description: "Tailscale hostname to certify (default: auto-detect)",
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
async run({ args }) {
|
|
30
|
+
const host = args.host ?? getTailscaleHost();
|
|
31
|
+
if (!host) {
|
|
32
|
+
console.error("Could not detect Tailscale hostname. Is Tailscale running?\n" +
|
|
33
|
+
"Use --host to specify the hostname manually.");
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
const certPath = getTlsCertPath();
|
|
37
|
+
const keyPath = getTlsKeyPath();
|
|
38
|
+
console.log(`Provisioning TLS certificate for ${host}...`);
|
|
39
|
+
const result = await provisionTlsCert({
|
|
40
|
+
host,
|
|
41
|
+
certPath,
|
|
42
|
+
keyPath,
|
|
43
|
+
});
|
|
44
|
+
if (!result.ok) {
|
|
45
|
+
console.error(`\nFailed to provision certificate (${result.reason}): ${result.message}`);
|
|
46
|
+
if (result.stderr)
|
|
47
|
+
console.error(result.stderr);
|
|
48
|
+
if (result.reason === "https_not_enabled") {
|
|
49
|
+
console.error("\nEnable HTTPS for your tailnet at https://login.tailscale.com/admin/dns");
|
|
50
|
+
}
|
|
51
|
+
else if (result.reason === "tailscale_missing") {
|
|
52
|
+
console.error("\nInstall Tailscale: https://tailscale.com/download");
|
|
53
|
+
}
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
console.log(`\nCertificate written to ${result.certPath}`);
|
|
57
|
+
console.log(`Key written to ${result.keyPath}`);
|
|
58
|
+
console.log(`Expires: ${result.notAfter.toISOString()}`);
|
|
59
|
+
console.log(`\nA running daemon will pick up the new cert automatically via fs.watch — no restart needed.`);
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
//# sourceMappingURL=certify.js.map
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import { openDatabase } from "../db/schema.js";
|
|
3
|
+
import { cleanAppHost } from "../shared/app-origin.js";
|
|
4
|
+
// Well-known config keys and their defaults
|
|
5
|
+
export const CONFIG_DEFAULTS = {
|
|
6
|
+
// The hostname that serves the shared app: where `brane meld` points a QR
|
|
7
|
+
// unless `--app` says otherwise, and what `GET /api/status` reports so a
|
|
8
|
+
// daemon's own bundle knows where Isolate hops out to. Never a trust
|
|
9
|
+
// decision: the daemon accepts any origin (SECURITY.md, Authentication).
|
|
10
|
+
// A fork changes this one line, and its daemons point at its own shell.
|
|
11
|
+
app_domain: "brane.organelle.dev",
|
|
12
|
+
"session-affinity.enabled": "true",
|
|
13
|
+
};
|
|
14
|
+
/** The shared app's host from a configured or given `app_domain` value,
|
|
15
|
+
* reduced to a host (`cleanAppHost`); a blank one means the default, so
|
|
16
|
+
* `brane config app_domain ""` or `brane meld --app ""` can never make a QR
|
|
17
|
+
* for `https:///`. Every reader applies this one rule — `brane meld`
|
|
18
|
+
* (`buildMeldUrl`) and `GET /api/status` (`statusAppDomain`) — so a daemon's
|
|
19
|
+
* bundle and the QR always agree on where the shared app is. Lives here,
|
|
20
|
+
* beside the default it falls back to, so the daemon need not import the
|
|
21
|
+
* meld command to read it. */
|
|
22
|
+
export function resolveAppHost(value) {
|
|
23
|
+
return cleanAppHost(value ?? "") || cleanAppHost(CONFIG_DEFAULTS.app_domain);
|
|
24
|
+
}
|
|
25
|
+
/** Reads a config key. Pass the caller's `db` whenever it has one: the daemon
|
|
26
|
+
* and every test do. Without it this opens `~/.brane/brane.db` itself — right
|
|
27
|
+
* for a CLI command, and exactly wrong anywhere else. That fallback is how a
|
|
28
|
+
* test that drove a mention receipt used to open the operator's live database
|
|
29
|
+
* and run the migration ladder on it, and how the daemon used to open (and
|
|
30
|
+
* never close) a fresh connection on every `listen` reply. */
|
|
31
|
+
export function getConfig(key, db) {
|
|
32
|
+
const conn = db ?? openDatabase();
|
|
33
|
+
try {
|
|
34
|
+
const row = conn.prepare("SELECT value FROM meta WHERE key = ?").get(`config.${key}`);
|
|
35
|
+
return row?.value ?? CONFIG_DEFAULTS[key];
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
if (!db)
|
|
39
|
+
conn.close();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export const configCommand = defineCommand({
|
|
43
|
+
meta: {
|
|
44
|
+
name: "config",
|
|
45
|
+
description: "Get or set brane configuration",
|
|
46
|
+
},
|
|
47
|
+
args: {
|
|
48
|
+
key: {
|
|
49
|
+
type: "positional",
|
|
50
|
+
description: "Configuration key",
|
|
51
|
+
required: false,
|
|
52
|
+
},
|
|
53
|
+
value: {
|
|
54
|
+
type: "positional",
|
|
55
|
+
description: "Value to set (omit to get current value)",
|
|
56
|
+
required: false,
|
|
57
|
+
},
|
|
58
|
+
list: {
|
|
59
|
+
type: "boolean",
|
|
60
|
+
description: "List all configuration values",
|
|
61
|
+
},
|
|
62
|
+
unset: {
|
|
63
|
+
type: "boolean",
|
|
64
|
+
description: "Remove a configuration key",
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
run({ args }) {
|
|
68
|
+
const db = openDatabase();
|
|
69
|
+
// --list: show all config
|
|
70
|
+
if (args.list) {
|
|
71
|
+
const rows = db
|
|
72
|
+
.prepare("SELECT key, value FROM meta WHERE key LIKE 'config.%' ORDER BY key")
|
|
73
|
+
.all();
|
|
74
|
+
if (rows.length === 0 && Object.keys(CONFIG_DEFAULTS).length === 0) {
|
|
75
|
+
console.log("No configuration set.");
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
// Show defaults that haven't been overridden
|
|
79
|
+
const setKeys = new Set(rows.map((r) => r.key.replace(/^config\./, "")));
|
|
80
|
+
for (const [key, value] of Object.entries(CONFIG_DEFAULTS)) {
|
|
81
|
+
if (!setKeys.has(key)) {
|
|
82
|
+
console.log(`${key}=${value} (default)`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Show explicitly set values
|
|
86
|
+
for (const row of rows) {
|
|
87
|
+
const key = row.key.replace(/^config\./, "");
|
|
88
|
+
console.log(`${key}=${row.value}`);
|
|
89
|
+
}
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const key = args.key;
|
|
93
|
+
if (!key) {
|
|
94
|
+
// No key provided and no --list flag: show usage
|
|
95
|
+
console.log("Usage: brane config <key> [value]");
|
|
96
|
+
console.log(" brane config --list");
|
|
97
|
+
console.log(" brane config --unset <key>");
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
// --unset: remove a key
|
|
101
|
+
if (args.unset) {
|
|
102
|
+
const deleted = db.prepare("DELETE FROM meta WHERE key = ?").run(`config.${key}`);
|
|
103
|
+
if (deleted.changes > 0) {
|
|
104
|
+
console.log(`Unset ${key}`);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
console.log(`${key} is not set`);
|
|
108
|
+
}
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const value = args.value;
|
|
112
|
+
if (value === undefined) {
|
|
113
|
+
// Get
|
|
114
|
+
const row = db.prepare("SELECT value FROM meta WHERE key = ?").get(`config.${key}`);
|
|
115
|
+
if (row) {
|
|
116
|
+
console.log(row.value);
|
|
117
|
+
}
|
|
118
|
+
else if (CONFIG_DEFAULTS[key] !== undefined) {
|
|
119
|
+
console.log(`${CONFIG_DEFAULTS[key]} (default)`);
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
// Key not found, exit silently like git config
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
// Set
|
|
128
|
+
db.prepare("INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)").run(`config.${key}`, value);
|
|
129
|
+
console.log(`${key}=${value}`);
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import { resolveDaemonEndpoint, daemonRequest, warnIfVersionMismatch } from "../daemon/client.js";
|
|
3
|
+
import { formatCost } from "../shared/narrative-format.js";
|
|
4
|
+
const backfillCommand = defineCommand({
|
|
5
|
+
meta: {
|
|
6
|
+
name: "backfill",
|
|
7
|
+
description: "Sweep the usage ledger over every narrative whose transcripts still exist",
|
|
8
|
+
},
|
|
9
|
+
args: {
|
|
10
|
+
"dry-run": {
|
|
11
|
+
type: "boolean",
|
|
12
|
+
description: "Report what would be recorded without writing anything",
|
|
13
|
+
},
|
|
14
|
+
all: {
|
|
15
|
+
type: "boolean",
|
|
16
|
+
description: "List every readable narrative, not just the ones that would change",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
async run({ args }) {
|
|
20
|
+
try {
|
|
21
|
+
// The sweep runs in the daemon — readers resolve transcripts against its
|
|
22
|
+
// HOME and memoize in its process — so this is a client of
|
|
23
|
+
// POST /api/cost/backfill, in the same shape as `brane revive`.
|
|
24
|
+
const endpoint = await resolveDaemonEndpoint();
|
|
25
|
+
warnIfVersionMismatch(endpoint);
|
|
26
|
+
const result = await daemonRequest(endpoint, "/api/cost/backfill", {
|
|
27
|
+
method: "POST",
|
|
28
|
+
body: { dry_run: !!args["dry-run"] },
|
|
29
|
+
});
|
|
30
|
+
console.log(formatBackfillResult(result, { all: !!args.all }));
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
export function formatBackfillResult(result, opts = {}) {
|
|
39
|
+
const changed = result.samples.filter((s) => s.new_rows > 0 || s.new_amount > 0 || s.after !== s.before);
|
|
40
|
+
const shown = opts.all ? result.samples : changed;
|
|
41
|
+
const verb = result.dry_run ? "would record" : "recorded";
|
|
42
|
+
const lines = [];
|
|
43
|
+
for (const s of shown) {
|
|
44
|
+
const delta = formatCost(s.new_amount, s.unit) ?? "0";
|
|
45
|
+
const before = formatCost(s.before, s.unit) ?? "—";
|
|
46
|
+
const after = formatCost(s.after, s.unit) ?? "—";
|
|
47
|
+
lines.push(`${s.nid.padEnd(18)} ${s.agent_type.padEnd(8)} ${s.readable}/${s.sessions} sessions +${s.new_rows} rows (${delta}) ${before} → ${after}`);
|
|
48
|
+
}
|
|
49
|
+
// Per-unit totals of what this pass adds, never summed across units.
|
|
50
|
+
const byUnit = new Map();
|
|
51
|
+
for (const s of result.samples)
|
|
52
|
+
byUnit.set(s.unit, (byUnit.get(s.unit) ?? 0) + s.new_amount);
|
|
53
|
+
const totals = [...byUnit].map(([u, a]) => formatCost(a, u)).filter(Boolean).join(" · ") || "nothing";
|
|
54
|
+
lines.push(`${result.dry_run ? "[dry run] " : ""}${result.scanned} narratives scanned, ${result.samples.length} readable, ${changed.length} changed; ${verb} ${totals}`);
|
|
55
|
+
return lines.join("\n");
|
|
56
|
+
}
|
|
57
|
+
const repriceCommand = defineCommand({
|
|
58
|
+
meta: {
|
|
59
|
+
name: "reprice",
|
|
60
|
+
description: "Re-price recorded ledger rows under the current price table",
|
|
61
|
+
},
|
|
62
|
+
args: {
|
|
63
|
+
"dry-run": {
|
|
64
|
+
type: "boolean",
|
|
65
|
+
description: "Report what would change without writing anything",
|
|
66
|
+
},
|
|
67
|
+
all: {
|
|
68
|
+
type: "boolean",
|
|
69
|
+
description: "List every narrative that changes, not just the ten largest",
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
async run({ args }) {
|
|
73
|
+
try {
|
|
74
|
+
// Rewrites the daemon's ledger, so it goes through the daemon like
|
|
75
|
+
// backfill does — one writer, and the app hears about the new totals.
|
|
76
|
+
const endpoint = await resolveDaemonEndpoint();
|
|
77
|
+
warnIfVersionMismatch(endpoint);
|
|
78
|
+
const result = await daemonRequest(endpoint, "/api/cost/reprice", {
|
|
79
|
+
method: "POST",
|
|
80
|
+
body: { dry_run: !!args["dry-run"] },
|
|
81
|
+
});
|
|
82
|
+
console.log(formatRepriceResult(result, { all: !!args.all }));
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
const usd = (n) => (n === null ? "—" : `$${n.toFixed(2)}`);
|
|
91
|
+
export function formatRepriceResult(result, opts = {}) {
|
|
92
|
+
const lines = [];
|
|
93
|
+
for (const m of result.models) {
|
|
94
|
+
const mark = m.changed ? "" : " (unchanged)";
|
|
95
|
+
lines.push(`${m.model.padEnd(20)} ${String(m.rows).padStart(6)} rows ${usd(m.before)} → ${usd(m.after)}${mark}`);
|
|
96
|
+
}
|
|
97
|
+
// Largest corrections first; the full list only on request.
|
|
98
|
+
const shown = opts.all ? result.narratives : result.narratives.slice(0, 10);
|
|
99
|
+
if (shown.length)
|
|
100
|
+
lines.push("");
|
|
101
|
+
for (const n of shown) {
|
|
102
|
+
lines.push(`${n.nid.padEnd(18)} ledger ${usd(n.ledger_before)} → ${usd(n.ledger_after)} card ${usd(n.column_before)} → ${usd(n.column_after)}`);
|
|
103
|
+
}
|
|
104
|
+
if (shown.length < result.narratives.length) {
|
|
105
|
+
lines.push(`… ${result.narratives.length - shown.length} more (--all to list them)`);
|
|
106
|
+
}
|
|
107
|
+
const before = result.models.reduce((a, m) => a + m.before, 0);
|
|
108
|
+
const after = result.models.reduce((a, m) => a + m.after, 0);
|
|
109
|
+
const verb = result.dry_run ? "would change" : "changed";
|
|
110
|
+
lines.push(`${result.dry_run ? "[dry run] " : ""}${result.scanned} priced rows, ${verb} ${result.changed} ` +
|
|
111
|
+
`across ${result.narratives.length} narratives: ${usd(before)} → ${usd(after)}` +
|
|
112
|
+
`; ${result.recovered} cache splits recovered` +
|
|
113
|
+
(result.unrecoverable ? `, ${result.unrecoverable} unrecoverable (left as recorded)` : ""));
|
|
114
|
+
return lines.join("\n");
|
|
115
|
+
}
|
|
116
|
+
export const costCommand = defineCommand({
|
|
117
|
+
meta: {
|
|
118
|
+
name: "cost",
|
|
119
|
+
description: "Cost ledger maintenance",
|
|
120
|
+
},
|
|
121
|
+
subCommands: {
|
|
122
|
+
backfill: backfillCommand,
|
|
123
|
+
reprice: repriceCommand,
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
//# sourceMappingURL=cost.js.map
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import { spawn, execFileSync } from "node:child_process";
|
|
3
|
+
import { openSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { openDatabase, ensureBraneDir, getBraneDir, getDaemonAddress } from "../db/schema.js";
|
|
6
|
+
import { startServer } from "../daemon/server.js";
|
|
7
|
+
import { legacyTokenWarning } from "../db/tokens.js";
|
|
8
|
+
import { withoutAgentEnv } from "../agent-env.js";
|
|
9
|
+
import { getTlsCertPath } from "./certify.js";
|
|
10
|
+
import { inspectCert } from "../tls.js";
|
|
11
|
+
function getLogPath() {
|
|
12
|
+
return join(getBraneDir(), "daemon.log");
|
|
13
|
+
}
|
|
14
|
+
// During `daemon restart` the running daemon's `caffeinate -i` child is
|
|
15
|
+
// killed when the old daemon shuts down, and a fresh one isn't established
|
|
16
|
+
// until the new daemon boots far enough to call `keepAwake.start()`. That
|
|
17
|
+
// leaves a brief window (death-poll granularity + new-daemon startup) with
|
|
18
|
+
// no PreventUserIdleSystemSleep assertion held — and because releasing the
|
|
19
|
+
// assertion doesn't reset macOS's idle timer, a machine already idle past
|
|
20
|
+
// its sleep timeout becomes immediately sleep-eligible during that gap.
|
|
21
|
+
//
|
|
22
|
+
// To make the handoff break-free, the restart command itself spawns a
|
|
23
|
+
// short-lived bridging assertion before tearing down the old daemon. It is
|
|
24
|
+
// detached + unref'd so it outlives this CLI process, and self-expires via
|
|
25
|
+
// `-t` so it never lingers as a duplicate of the new daemon's own child.
|
|
26
|
+
// Darwin-only, matching KeepAwake; a no-op elsewhere.
|
|
27
|
+
const RESTART_BRIDGE_SECONDS = 30;
|
|
28
|
+
function spawnKeepAwakeBridge() {
|
|
29
|
+
if (process.platform !== "darwin")
|
|
30
|
+
return;
|
|
31
|
+
try {
|
|
32
|
+
const bridge = spawn("caffeinate", ["-i", "-t", String(RESTART_BRIDGE_SECONDS)], { stdio: "ignore", detached: true });
|
|
33
|
+
bridge.on("error", () => { }); // caffeinate missing — best effort, ignore
|
|
34
|
+
bridge.unref();
|
|
35
|
+
console.log(`Holding idle-sleep bridge for ${RESTART_BRIDGE_SECONDS}s across restart (caffeinate pid ${bridge.pid})`);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// Best effort: if the bridge can't spawn, restart still proceeds.
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// Ask V8 to dump a heap snapshot when it's about to OOM and have Node
|
|
42
|
+
// write a diagnostic report on any fatal error, both into ~/.brane so
|
|
43
|
+
// they're easy to find after a crash.
|
|
44
|
+
export function buildDaemonEnv() {
|
|
45
|
+
const diagnosticFlags = [
|
|
46
|
+
"--heapsnapshot-near-heap-limit=1",
|
|
47
|
+
"--report-on-fatalerror",
|
|
48
|
+
`--report-directory=${getBraneDir()}`,
|
|
49
|
+
].join(" ");
|
|
50
|
+
const existing = process.env.NODE_OPTIONS;
|
|
51
|
+
// A daemon restarted from an agent's shell must not carry that agent's
|
|
52
|
+
// session: its token and its harness's session id would ride into every
|
|
53
|
+
// tmux server it starts, and from there into sessions that are not that
|
|
54
|
+
// agent (claude-jrjh1e8w:10, claude-eiuzl79r:33). See src/agent-env.ts.
|
|
55
|
+
const env = withoutAgentEnv(process.env);
|
|
56
|
+
return {
|
|
57
|
+
...env,
|
|
58
|
+
BRANE_DAEMON_DETACHED: "1",
|
|
59
|
+
NODE_OPTIONS: existing ? `${existing} ${diagnosticFlags}` : diagnosticFlags,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/** Resolve the daemon port: explicit flag > previous port > 0 (OS-assigned) */
|
|
63
|
+
function resolvePort(explicit, db) {
|
|
64
|
+
if (explicit)
|
|
65
|
+
return parseInt(explicit, 10);
|
|
66
|
+
// Try to reuse previous port
|
|
67
|
+
const prevRow = db.prepare("SELECT value FROM meta WHERE key = 'daemon_port'").get();
|
|
68
|
+
if (prevRow) {
|
|
69
|
+
return parseInt(prevRow.value, 10);
|
|
70
|
+
}
|
|
71
|
+
// Fall back to OS-assigned
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
74
|
+
const daemonStart = defineCommand({
|
|
75
|
+
meta: {
|
|
76
|
+
name: "start",
|
|
77
|
+
description: "Start the brane daemon",
|
|
78
|
+
},
|
|
79
|
+
args: {
|
|
80
|
+
port: {
|
|
81
|
+
type: "string",
|
|
82
|
+
description: "Port to listen on (default: previous port, then random)",
|
|
83
|
+
},
|
|
84
|
+
token: {
|
|
85
|
+
type: "string",
|
|
86
|
+
description: "Also accept this token, stored as a hashed device token. " +
|
|
87
|
+
"Adopted once: after a rotation revokes it, passing it again does nothing",
|
|
88
|
+
},
|
|
89
|
+
bind: {
|
|
90
|
+
type: "string",
|
|
91
|
+
description: "Bind address (default: auto-detect localhost + Tailscale)",
|
|
92
|
+
},
|
|
93
|
+
foreground: {
|
|
94
|
+
type: "boolean",
|
|
95
|
+
description: "Run in foreground (default: detach)",
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
async run({ args }) {
|
|
99
|
+
const db = openDatabase();
|
|
100
|
+
const port = resolvePort(args.port, db);
|
|
101
|
+
// Check if daemon is already running (cleans up stale entries). The
|
|
102
|
+
// address is enough: a running daemon this shell cannot reach is still
|
|
103
|
+
// running, and a second one would only lose the claim.
|
|
104
|
+
const existing = getDaemonAddress(db);
|
|
105
|
+
if (existing) {
|
|
106
|
+
console.log(`Daemon already running. Exiting.`);
|
|
107
|
+
db.close();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (!args.foreground) {
|
|
111
|
+
db.close();
|
|
112
|
+
// Detach: re-spawn ourselves with --foreground in a detached child
|
|
113
|
+
ensureBraneDir();
|
|
114
|
+
const logFd = openSync(getLogPath(), "a");
|
|
115
|
+
const child = spawn(process.execPath, [...process.execArgv, ...process.argv.slice(1), "--foreground"], {
|
|
116
|
+
detached: true,
|
|
117
|
+
stdio: ["ignore", logFd, logFd],
|
|
118
|
+
env: buildDaemonEnv(),
|
|
119
|
+
});
|
|
120
|
+
child.unref();
|
|
121
|
+
console.log(`Daemon started in background (pid ${child.pid})`);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
await startServer({ port, db, token: args.token, bind: args.bind });
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
const daemonStop = defineCommand({
|
|
128
|
+
meta: {
|
|
129
|
+
name: "stop",
|
|
130
|
+
description: "Stop the brane daemon",
|
|
131
|
+
},
|
|
132
|
+
run() {
|
|
133
|
+
const db = openDatabase();
|
|
134
|
+
const pidRow = db
|
|
135
|
+
.prepare("SELECT value FROM meta WHERE key = 'daemon_pid'")
|
|
136
|
+
.get();
|
|
137
|
+
if (!pidRow) {
|
|
138
|
+
console.log("No daemon running.");
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const pid = parseInt(pidRow.value, 10);
|
|
142
|
+
try {
|
|
143
|
+
process.kill(pid, "SIGTERM");
|
|
144
|
+
console.log(`Sent SIGTERM to daemon (pid ${pid})`);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
console.log(`Daemon process ${pid} not found. Cleaning up stale entry.`);
|
|
148
|
+
db.prepare("DELETE FROM meta WHERE key IN ('daemon_token', 'daemon_pid', 'daemon_version', 'daemon_tls', 'daemon_socket')").run();
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
const daemonStatus = defineCommand({
|
|
153
|
+
meta: {
|
|
154
|
+
name: "status",
|
|
155
|
+
description: "Show daemon status",
|
|
156
|
+
},
|
|
157
|
+
run() {
|
|
158
|
+
const db = openDatabase();
|
|
159
|
+
const pidRow = db.prepare("SELECT value FROM meta WHERE key = 'daemon_pid'").get();
|
|
160
|
+
const portRow = db.prepare("SELECT value FROM meta WHERE key = 'daemon_port'").get();
|
|
161
|
+
if (!pidRow) {
|
|
162
|
+
console.log("Daemon is not running.");
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const pid = parseInt(pidRow.value, 10);
|
|
166
|
+
let alive = false;
|
|
167
|
+
try {
|
|
168
|
+
process.kill(pid, 0);
|
|
169
|
+
alive = true;
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
alive = false;
|
|
173
|
+
}
|
|
174
|
+
console.log(`Daemon status: ${alive ? "running" : "dead (stale entry)"}`);
|
|
175
|
+
console.log(` pid: ${pid}`);
|
|
176
|
+
console.log(` port: ${portRow?.value ?? "unknown"}`);
|
|
177
|
+
// This machine's CLI holds no token: it reaches the daemon over this
|
|
178
|
+
// socket, which only this account can open.
|
|
179
|
+
const socketRow = db.prepare("SELECT value FROM meta WHERE key = 'daemon_socket'").get();
|
|
180
|
+
console.log(` sock: ${socketRow?.value ?? "none (this shell cannot reach it; see `brane daemon logs`)"}`);
|
|
181
|
+
const legacyWarning = legacyTokenWarning(db);
|
|
182
|
+
if (legacyWarning)
|
|
183
|
+
console.log(`\nNote: ${legacyWarning}`);
|
|
184
|
+
// TLS cert status — read directly from disk so we can report even if the
|
|
185
|
+
// daemon isn't responsive. The daemon's own CertManager is the source of
|
|
186
|
+
// truth for renewal scheduling; here we just surface the expiry.
|
|
187
|
+
const cert = inspectCert(getTlsCertPath());
|
|
188
|
+
if (cert) {
|
|
189
|
+
const daysLeft = Math.floor((cert.notAfter.getTime() - Date.now()) / 86_400_000);
|
|
190
|
+
const expiryLabel = daysLeft < 0 ? `EXPIRED ${-daysLeft}d ago`
|
|
191
|
+
: daysLeft === 0 ? "expires today"
|
|
192
|
+
: `${daysLeft}d remaining`;
|
|
193
|
+
console.log(` tls: ${cert.notAfter.toISOString()} (${expiryLabel})`);
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
console.log(` tls: none (daemon will provision one if Tailscale is running)`);
|
|
197
|
+
}
|
|
198
|
+
if (!alive) {
|
|
199
|
+
db.prepare("DELETE FROM meta WHERE key IN ('daemon_token', 'daemon_pid', 'daemon_version', 'daemon_tls', 'daemon_socket')").run();
|
|
200
|
+
console.log("\nCleaned up stale daemon entry.");
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
const daemonLogs = defineCommand({
|
|
205
|
+
meta: {
|
|
206
|
+
name: "logs",
|
|
207
|
+
description: "Show daemon logs",
|
|
208
|
+
},
|
|
209
|
+
args: {
|
|
210
|
+
follow: {
|
|
211
|
+
type: "boolean",
|
|
212
|
+
alias: "f",
|
|
213
|
+
description: "Follow log output (like tail -f)",
|
|
214
|
+
},
|
|
215
|
+
lines: {
|
|
216
|
+
type: "string",
|
|
217
|
+
alias: "n",
|
|
218
|
+
description: "Number of lines to show (default: 50)",
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
run({ args }) {
|
|
222
|
+
const logPath = getLogPath();
|
|
223
|
+
const lines = args.lines ?? "50";
|
|
224
|
+
const tailArgs = ["-n", lines];
|
|
225
|
+
if (args.follow)
|
|
226
|
+
tailArgs.push("-f");
|
|
227
|
+
tailArgs.push(logPath);
|
|
228
|
+
try {
|
|
229
|
+
execFileSync("tail", tailArgs, { stdio: "inherit" });
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
console.error(`No log file found at ${logPath}`);
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
const daemonRestart = defineCommand({
|
|
237
|
+
meta: {
|
|
238
|
+
name: "restart",
|
|
239
|
+
description: "Restart the brane daemon",
|
|
240
|
+
},
|
|
241
|
+
args: {
|
|
242
|
+
port: {
|
|
243
|
+
type: "string",
|
|
244
|
+
description: "Port to listen on (default: previous port, then random)",
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
async run({ args }) {
|
|
248
|
+
const db = openDatabase();
|
|
249
|
+
// Capture the previous port before stopping. No token needs carrying:
|
|
250
|
+
// this machine's CLI holds none, and the new daemon finds the device
|
|
251
|
+
// tokens' hashes in the database.
|
|
252
|
+
const prevPortRow = db
|
|
253
|
+
.prepare("SELECT value FROM meta WHERE key = 'daemon_port'")
|
|
254
|
+
.get();
|
|
255
|
+
const prevPort = args.port ?? prevPortRow?.value;
|
|
256
|
+
// Hold an idle-sleep assertion across the handoff *before* killing the
|
|
257
|
+
// old daemon, so there's no window where nothing prevents idle sleep.
|
|
258
|
+
spawnKeepAwakeBridge();
|
|
259
|
+
// Stop existing daemon if running
|
|
260
|
+
const pidRow = db
|
|
261
|
+
.prepare("SELECT value FROM meta WHERE key = 'daemon_pid'")
|
|
262
|
+
.get();
|
|
263
|
+
if (pidRow) {
|
|
264
|
+
const pid = parseInt(pidRow.value, 10);
|
|
265
|
+
try {
|
|
266
|
+
process.kill(pid, "SIGTERM");
|
|
267
|
+
console.log(`Sent SIGTERM to daemon (pid ${pid})`);
|
|
268
|
+
// Wait for it to die
|
|
269
|
+
for (let i = 0; i < 20; i++) {
|
|
270
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
271
|
+
try {
|
|
272
|
+
process.kill(pid, 0);
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
// Already dead
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
db.close();
|
|
284
|
+
// Start fresh daemon (detached)
|
|
285
|
+
ensureBraneDir();
|
|
286
|
+
const logFd = openSync(getLogPath(), "a");
|
|
287
|
+
const startArgs = [...process.argv.slice(1)].filter((a) => a !== "restart" && !a.startsWith("--port") && !a.startsWith("--token"));
|
|
288
|
+
// Replace "restart" with "start" in the args
|
|
289
|
+
const idx = startArgs.indexOf("daemon");
|
|
290
|
+
const childArgs = [
|
|
291
|
+
...process.execArgv,
|
|
292
|
+
...startArgs.slice(0, idx + 1),
|
|
293
|
+
"start",
|
|
294
|
+
"--foreground",
|
|
295
|
+
...(prevPort ? ["--port", prevPort] : []),
|
|
296
|
+
];
|
|
297
|
+
const child = spawn(process.execPath, childArgs, {
|
|
298
|
+
detached: true,
|
|
299
|
+
stdio: ["ignore", logFd, logFd],
|
|
300
|
+
env: buildDaemonEnv(),
|
|
301
|
+
});
|
|
302
|
+
child.unref();
|
|
303
|
+
console.log(`Daemon restarted (pid ${child.pid})`);
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
/** Start the daemon in the background, unless one is already running, and
|
|
307
|
+
* wait for it to be ready. Returns its address only: what a caller presents
|
|
308
|
+
* is its own business (`resolveCredential` in daemon/client.ts). */
|
|
309
|
+
export async function ensureDaemonRunning() {
|
|
310
|
+
ensureBraneDir();
|
|
311
|
+
const check = openDatabase();
|
|
312
|
+
const running = getDaemonAddress(check);
|
|
313
|
+
check.close();
|
|
314
|
+
if (running)
|
|
315
|
+
return running;
|
|
316
|
+
const logFd = openSync(getLogPath(), "a");
|
|
317
|
+
const cliArgs = process.argv.slice(0, 2);
|
|
318
|
+
const child = spawn(process.execPath, [...process.execArgv, cliArgs[1], "daemon", "start", "--foreground"], {
|
|
319
|
+
detached: true,
|
|
320
|
+
stdio: ["ignore", logFd, logFd],
|
|
321
|
+
env: buildDaemonEnv(),
|
|
322
|
+
});
|
|
323
|
+
child.unref();
|
|
324
|
+
// Poll for daemon to be ready (up to 5s)
|
|
325
|
+
for (let i = 0; i < 50; i++) {
|
|
326
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
327
|
+
// The daemon writes its port only after bootstrapping tokens and binding
|
|
328
|
+
// its socket, so once the address is up the socket is too.
|
|
329
|
+
const db = openDatabase();
|
|
330
|
+
const address = getDaemonAddress(db);
|
|
331
|
+
db.close();
|
|
332
|
+
if (address)
|
|
333
|
+
return address;
|
|
334
|
+
}
|
|
335
|
+
throw new Error("Daemon failed to start within 5s");
|
|
336
|
+
}
|
|
337
|
+
export const daemonCommand = defineCommand({
|
|
338
|
+
meta: {
|
|
339
|
+
name: "daemon",
|
|
340
|
+
description: "Manage the brane daemon",
|
|
341
|
+
},
|
|
342
|
+
subCommands: {
|
|
343
|
+
start: daemonStart,
|
|
344
|
+
stop: daemonStop,
|
|
345
|
+
restart: daemonRestart,
|
|
346
|
+
status: daemonStatus,
|
|
347
|
+
logs: daemonLogs,
|
|
348
|
+
},
|
|
349
|
+
});
|
|
350
|
+
//# sourceMappingURL=daemon.js.map
|