trajectories 0.0.0 → 0.1.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.
Files changed (204) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/LICENSE +201 -0
  3. package/README.md +274 -2
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.js +8542 -0
  6. package/dist/convert.d.ts +31 -0
  7. package/dist/convert.js +431 -0
  8. package/dist/index.d.ts +7 -0
  9. package/dist/index.js +8402 -0
  10. package/dist/log-baseline.d.ts +26 -0
  11. package/dist/log-baseline.js +104 -0
  12. package/dist/log-content.d.ts +16 -0
  13. package/dist/log-content.js +80 -0
  14. package/dist/log-io.d.ts +10 -0
  15. package/dist/log-io.js +121 -0
  16. package/dist/log-read.d.ts +24 -0
  17. package/dist/log-read.js +117 -0
  18. package/dist/log-reconcile.d.ts +5 -0
  19. package/dist/log-reconcile.js +38 -0
  20. package/dist/log-records.d.ts +35 -0
  21. package/dist/log-records.js +85 -0
  22. package/dist/log-recovery.d.ts +18 -0
  23. package/dist/log-recovery.js +123 -0
  24. package/dist/log-stage.d.ts +14 -0
  25. package/dist/log-stage.js +138 -0
  26. package/dist/log-state.d.ts +89 -0
  27. package/dist/log-state.js +92 -0
  28. package/dist/log-switch.d.ts +3 -0
  29. package/dist/log-switch.js +98 -0
  30. package/dist/log-types.d.ts +129 -0
  31. package/dist/log-types.js +8 -0
  32. package/dist/log.d.ts +8 -0
  33. package/dist/log.js +62 -0
  34. package/dist/migrate.d.ts +29 -0
  35. package/dist/migrate.js +173 -0
  36. package/dist/normalization-runtime.d.ts +1 -0
  37. package/dist/normalization-runtime.js +3150 -0
  38. package/dist/portable.d.ts +9 -0
  39. package/dist/portable.js +26 -0
  40. package/dist/readers/canonical.d.ts +13 -0
  41. package/dist/readers/canonical.js +54 -0
  42. package/dist/readers/cline.d.ts +10 -0
  43. package/dist/readers/cline.js +107 -0
  44. package/dist/readers/cursor.d.ts +20 -0
  45. package/dist/readers/cursor.js +374 -0
  46. package/dist/readers/deepagents.d.ts +41 -0
  47. package/dist/readers/deepagents.js +163 -0
  48. package/dist/readers/fx.d.ts +11 -0
  49. package/dist/readers/fx.js +573 -0
  50. package/dist/readers/github-copilot.d.ts +8 -0
  51. package/dist/readers/github-copilot.js +216 -0
  52. package/dist/readers/grok-build.d.ts +12 -0
  53. package/dist/readers/grok-build.js +391 -0
  54. package/dist/readers/guards.d.ts +4 -0
  55. package/dist/readers/guards.js +121 -0
  56. package/dist/readers/pi-compaction.d.ts +7 -0
  57. package/dist/readers/pi-compaction.js +112 -0
  58. package/dist/readers/pi-context.d.ts +13 -0
  59. package/dist/readers/pi-context.js +64 -0
  60. package/dist/store.d.ts +25 -0
  61. package/dist/store.js +218 -0
  62. package/dist/types.d.ts +91 -0
  63. package/dist/types.js +1 -0
  64. package/dist/writers/claude-code.d.ts +5 -0
  65. package/dist/writers/claude-code.js +51 -0
  66. package/dist/writers/cline.d.ts +3 -0
  67. package/dist/writers/cline.js +77 -0
  68. package/dist/writers/codex.d.ts +3 -0
  69. package/dist/writers/codex.js +32 -0
  70. package/dist/writers/cursor.d.ts +3 -0
  71. package/dist/writers/cursor.js +197 -0
  72. package/dist/writers/deepagents.d.ts +3 -0
  73. package/dist/writers/deepagents.js +116 -0
  74. package/dist/writers/entropy.d.ts +5 -0
  75. package/dist/writers/entropy.js +33 -0
  76. package/dist/writers/fx.d.ts +3 -0
  77. package/dist/writers/fx.js +135 -0
  78. package/dist/writers/github-copilot.d.ts +3 -0
  79. package/dist/writers/github-copilot.js +71 -0
  80. package/dist/writers/grok-build.d.ts +3 -0
  81. package/dist/writers/grok-build.js +43 -0
  82. package/dist/writers/helpers.d.ts +17 -0
  83. package/dist/writers/helpers.js +75 -0
  84. package/dist/writers/omp.d.ts +3 -0
  85. package/dist/writers/omp.js +7 -0
  86. package/dist/writers/opencode.d.ts +3 -0
  87. package/dist/writers/opencode.js +65 -0
  88. package/dist/writers/pi.d.ts +3 -0
  89. package/dist/writers/pi.js +45 -0
  90. package/docs/cline-native-format.md +43 -0
  91. package/docs/copilot-native-format.md +78 -0
  92. package/docs/cursor-native-format.md +103 -0
  93. package/docs/deepagents-native-format.md +98 -0
  94. package/docs/evidence-api.md +170 -0
  95. package/docs/fx-native-format.md +87 -0
  96. package/docs/harness-parity-bootstrap.md +78 -0
  97. package/docs/harness-support-parity.md +107 -0
  98. package/docs/inspirations.md +103 -0
  99. package/docs/ledger-migration.md +141 -0
  100. package/docs/pi-compaction-context.md +75 -0
  101. package/docs/pi-provider-context.md +76 -0
  102. package/docs/research/2026-10-06-huihua-and-oar.md +176 -0
  103. package/docs/research/ledger/README.md +96 -0
  104. package/docs/research/ledger/confer/2026-09-13-round-10/README.md +61 -0
  105. package/docs/research/ledger/confer/2026-09-13-round-2/cass.md +72 -0
  106. package/docs/research/ledger/confer/2026-09-13-round-3/README.md +70 -0
  107. package/docs/research/ledger/confer/2026-09-13-round-5/README.md +61 -0
  108. package/docs/research/ledger/confer/2026-09-13-round-6/README.md +68 -0
  109. package/docs/research/ledger/confer/2026-09-13-round-7/README.md +64 -0
  110. package/docs/research/ledger/confer/2026-09-13-round-9/README.md +59 -0
  111. package/docs/research/ledger/confer/2026-09-14-wake/wake.md +146 -0
  112. package/docs/research/ledger/docs/inspirations.md +320 -0
  113. package/docs/research/ledger/docs/pr2-pr6-integration.md +53 -0
  114. package/docs/research/ledger/docs/research/2026-09-07-confer-inspirations.md +163 -0
  115. package/docs/research/ledger/docs/research/2026-09-07-letta-continuous-service.md +104 -0
  116. package/docs/research/ledger/docs/research/2026-09-08-claude-history-retrieval.md +117 -0
  117. package/docs/research/ledger/docs/research/2026-09-09-agentsview-native-discovery.md +87 -0
  118. package/docs/research/ledger/docs/research/2026-09-09-daemon-observation.md +231 -0
  119. package/docs/research/ledger/docs/research/2026-09-09-discovery-pagination.md +114 -0
  120. package/docs/research/ledger/docs/research/2026-09-09-harness-transcript-contract.md +145 -0
  121. package/docs/research/ledger/docs/research/2026-09-09-native-history-discovery.md +180 -0
  122. package/docs/research/ledger/docs/research/2026-09-09-native-history-implementation.md +137 -0
  123. package/docs/research/ledger/docs/research/2026-09-09-other-inspirations.md +279 -0
  124. package/docs/research/ledger/docs/research/2026-09-09-trajectory-and-letta-code.md +128 -0
  125. package/docs/research/ledger/docs/research/2026-09-13-diagnostics-coverage.md +116 -0
  126. package/docs/research/ledger/docs/research/2026-09-13-fts-rebuild.md +83 -0
  127. package/docs/research/ledger/docs/research/2026-09-13-read-path-split.md +74 -0
  128. package/docs/research/ledger/docs/research/2026-09-13-runtime-rollout.md +102 -0
  129. package/docs/research/ledger/docs/research/2026-09-13-scan-outcomes.md +80 -0
  130. package/docs/research/ledger/docs/research/2026-09-13-search-quality.md +105 -0
  131. package/docs/research/ledger/docs/research/2026-09-13-source-coverage.md +64 -0
  132. package/docs/research/ledger/docs/research/2026-09-13-sync-wall-budget.md +82 -0
  133. package/docs/research/ledger/docs/research/2026-09-13-typed-read-gate.md +81 -0
  134. package/docs/research/ledger/docs/research/2026-09-13-writer-lease.md +84 -0
  135. package/docs/research/ledger/docs/research/2026-09-16-c1-copy-fallback.md +227 -0
  136. package/docs/research/ledger/docs/research/2026-09-16-c10-kimi.md +414 -0
  137. package/docs/research/ledger/docs/research/2026-09-16-c2-derive-counter.md +216 -0
  138. package/docs/research/ledger/docs/research/2026-09-16-c3-echo-filter.md +305 -0
  139. package/docs/research/ledger/docs/research/2026-09-16-c4-title-index.md +349 -0
  140. package/docs/research/ledger/docs/research/2026-09-16-c5-trigram.md +268 -0
  141. package/docs/research/ledger/docs/research/2026-09-16-c6-recency-prior.md +438 -0
  142. package/docs/research/ledger/docs/research/2026-09-16-c7-tombstones.md +467 -0
  143. package/docs/research/ledger/docs/research/2026-09-16-c8-remote-truth.md +390 -0
  144. package/docs/research/ledger/docs/research/2026-09-16-c9-grok.md +380 -0
  145. package/docs/research/ledger/docs/research/2026-09-16-wake-port-program.md +38 -0
  146. package/docs/research/ledger/docs/research/2026-09-17-cursor-blobstore.md +157 -0
  147. package/docs/research/ledger/manifest.json +540 -0
  148. package/legacy/NOTICE.md +50 -0
  149. package/legacy/bin/ledger-core.ts +7 -0
  150. package/legacy/bin/ledger.ts +7 -0
  151. package/legacy/bin/traj-bridge.ts +7 -0
  152. package/legacy/scripts/import-claude-history.ts +130 -0
  153. package/legacy/scripts/native-history-smoke.ts +52 -0
  154. package/legacy/scripts/observe-daemon.ts +570 -0
  155. package/legacy/scripts/retitle-sessions.md +19 -0
  156. package/legacy/scripts/verify-runtime-rollout.ts +151 -0
  157. package/legacy/src/canonical.ts +108 -0
  158. package/legacy/src/cli.ts +1293 -0
  159. package/legacy/src/db.ts +2254 -0
  160. package/legacy/src/diagnostics.ts +87 -0
  161. package/legacy/src/discovery.ts +56 -0
  162. package/legacy/src/embeddings.ts +382 -0
  163. package/legacy/src/filter.ts +602 -0
  164. package/legacy/src/gemini_embeddings.ts +103 -0
  165. package/legacy/src/identity.ts +9 -0
  166. package/legacy/src/index.ts +10 -0
  167. package/legacy/src/ingest.ts +791 -0
  168. package/legacy/src/managed.ts +178 -0
  169. package/legacy/src/native/common.ts +68 -0
  170. package/legacy/src/native/cursor.ts +647 -0
  171. package/legacy/src/native/grok.ts +540 -0
  172. package/legacy/src/native/kimi.ts +706 -0
  173. package/legacy/src/native/opencode.ts +470 -0
  174. package/legacy/src/native/sqlite-ro.ts +281 -0
  175. package/legacy/src/native.ts +97 -0
  176. package/legacy/src/normalizer.ts +457 -0
  177. package/legacy/src/parentage.ts +562 -0
  178. package/legacy/src/pressure.ts +162 -0
  179. package/legacy/src/private-storage.ts +74 -0
  180. package/legacy/src/reader.ts +1255 -0
  181. package/legacy/src/scanner.ts +591 -0
  182. package/legacy/src/schema-gate.ts +263 -0
  183. package/legacy/src/schema.ts +829 -0
  184. package/legacy/src/semantic-top-k.ts +71 -0
  185. package/legacy/src/session-hash.ts +49 -0
  186. package/legacy/src/store-status.ts +218 -0
  187. package/legacy/src/sync-preparation.ts +45 -0
  188. package/legacy/src/sync-truth.ts +843 -0
  189. package/legacy/src/sync.ts +1219 -0
  190. package/legacy/src/types.ts +436 -0
  191. package/legacy/src/vocab.txt +30522 -0
  192. package/legacy/src/watch.ts +673 -0
  193. package/legacy/src/writer-lease.ts +310 -0
  194. package/ops/README.md +120 -0
  195. package/ops/install-cli.sh +63 -0
  196. package/ops/install-release.sh +49 -0
  197. package/ops/linux/install-systemd-user.sh +103 -0
  198. package/ops/macos/install-launchd.sh +72 -0
  199. package/ops/sync-settings.sh +37 -0
  200. package/package.json +19 -5
  201. package/skills/trajectories/SKILL.md +138 -0
  202. package/skills/trajectories/scripts/trajectories_recent.sh +26 -0
  203. package/skills/trajectories/scripts/trajectories_search.sh +26 -0
  204. package/index.js +0 -1
@@ -0,0 +1,151 @@
1
+ #!/usr/bin/env bun
2
+ import { Database } from "bun:sqlite";
3
+ import { createHash } from "node:crypto";
4
+ import { createReadStream, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
+ import { dirname, join, resolve } from "node:path";
6
+
7
+ // Private artifacts only: fixtures may contain short fragments of user prompts.
8
+ const [mode, dbArg, outArg] = process.argv.slice(2);
9
+ if (!["snapshot", "freeze", "queries"].includes(mode ?? "") || !dbArg || !outArg) {
10
+ throw new Error("Usage: bun legacy/scripts/verify-runtime-rollout.ts snapshot|freeze|queries DB NEW_OUTPUT");
11
+ }
12
+ const dbPath = resolve(dbArg);
13
+ const out = resolve(outArg);
14
+ if (!existsSync(dbPath) || existsSync(out)) throw new Error("Database must exist and output must be new");
15
+ process.umask(0o077);
16
+ mkdirSync(dirname(out), { recursive: true, mode: 0o700 });
17
+ const started = new Date().toISOString();
18
+ const start = performance.now();
19
+ const hash = (text: string) => createHash("sha256").update(text).digest("hex");
20
+ const save = (value: unknown) => writeFileSync(out, JSON.stringify(value, null, 2) + "\n", { mode: 0o600, flag: "wx" });
21
+ function open() {
22
+ const db = new Database(dbPath, { readonly: true });
23
+ db.exec("PRAGMA query_only=ON; PRAGMA busy_timeout=1000");
24
+ return db;
25
+ }
26
+
27
+ if (mode === "snapshot") {
28
+ const db = open();
29
+ const tables: Record<string, { count: number; sha256: string }> = {};
30
+ // These projections exist in both the deployed v6 and the candidate v10.
31
+ const projections: Record<string, string> = {
32
+ sessions: "id,source,source_path,content_hash,title,cwd,model,started_at,ended_at,record_count,created_at,updated_at,parent_session_id,parent_tool_use_id,agent_nickname,thread_role",
33
+ records: "*",
34
+ records_embeddings: "*",
35
+ ingest_cursors: "source,listing_id,source_path,session_id,mtime,size,bytes_ingested,record_count,status,updated_at,file_identity,prefix_hash",
36
+ ingest_staged_records: "*",
37
+ };
38
+ const stagedColumns = db.query<{ name: string }, []>("PRAGMA table_info(ingest_staged_sessions)").all()
39
+ .map(({ name }) => name).filter((name) => name !== "diagnostics_json");
40
+ projections.ingest_staged_sessions = stagedColumns.map((name) => `"${name.replaceAll('"', '""')}"`).join(",");
41
+ db.exec("BEGIN");
42
+ try {
43
+ for (const [table, projection] of Object.entries(projections)) {
44
+ const digest = createHash("sha256");
45
+ let count = 0;
46
+ const order = table === "ingest_cursors" ? "source,listing_id,source_path" : "rowid";
47
+ for (const row of db.query(`SELECT ${projection} FROM ${table} ORDER BY ${order}`).iterate()) {
48
+ digest.update(JSON.stringify(row) + "\n");
49
+ count++;
50
+ }
51
+ tables[table] = { count, sha256: digest.digest("hex") };
52
+ }
53
+ const result = {
54
+ started_at: started,
55
+ schema: db.query("PRAGMA user_version").get(),
56
+ tables,
57
+ coverage: db.query("SELECT source,count(*) AS sessions,sum(record_count) AS records FROM sessions GROUP BY source ORDER BY source").all(),
58
+ cursors: db.query("SELECT source,status,count(*) AS count FROM ingest_cursors GROUP BY source,status ORDER BY source,status").all(),
59
+ quick_check: db.query("PRAGMA quick_check").all(),
60
+ foreign_key_violations: db.query("PRAGMA foreign_key_check").all().length,
61
+ elapsed_ms: performance.now() - start,
62
+ resource_usage: process.resourceUsage(),
63
+ };
64
+ save(result);
65
+ console.log(JSON.stringify({ mode, schema: result.schema, records: tables.records.count, elapsed_ms: result.elapsed_ms }));
66
+ } finally {
67
+ db.exec("ROLLBACK");
68
+ db.close();
69
+ }
70
+ } else if (mode === "freeze") {
71
+ const seedPath = process.argv[5];
72
+ if (!seedPath) throw new Error("freeze also requires a private seed-case JSON file with a cases array");
73
+ const seeds = JSON.parse(readFileSync(seedPath, "utf8"));
74
+ if (!Array.isArray(seeds.cases) || seeds.cases.some((item: any) =>
75
+ !item || typeof item.name !== "string" || typeof item.query !== "string" ||
76
+ typeof item.required !== "boolean" || (!item.negative && typeof item.session !== "string"))) {
77
+ throw new Error("Invalid private seed cases");
78
+ }
79
+ const db = open();
80
+ const cases: Array<{ name: string; query: string; source?: string; session?: string; required: boolean; negative?: boolean }> = [...seeds.cases];
81
+ const sources = db.query<{ source: string }, []>("SELECT DISTINCT source FROM sessions ORDER BY source").all();
82
+ const missingSeeds: string[] = [];
83
+ for (const { source } of sources) {
84
+ const rows = db.query<{ session_id: string; content: string }, [string]>(
85
+ "SELECT r.session_id,r.content FROM sessions s JOIN records r ON r.session_id=s.id WHERE s.source=? AND r.record_type='user' AND length(r.content) BETWEEN 80 AND 800 LIMIT 200",
86
+ ).all(source);
87
+ const row = rows.find(({ content }) =>
88
+ !/[A-Za-z0-9_+\/-]{32,}|api.?key|token|password|secret|cookie|authorization|BEGIN.*KEY/i.test(content) &&
89
+ (content.match(/[\p{L}\p{N}_]+/gu)?.length ?? 0) >= 16,
90
+ );
91
+ if (!row) { missingSeeds.push(source); continue; }
92
+ const words = row.content.match(/[\p{L}\p{N}_]+/gu)!;
93
+ const middle = Math.floor((words.length - 12) / 2);
94
+ cases.push({ name: `source-retrieval-${source}`, source, session: row.session_id, query: `"${words.slice(middle, middle + 12).join(" ")}"`, required: true });
95
+ }
96
+ db.close();
97
+ save({ started_at: started, limit: 50, cases, missing_seeds: missingSeeds });
98
+ console.log(JSON.stringify({ mode, cases: cases.length, missing_seeds: missingSeeds }));
99
+ } else {
100
+ const fixturePath = process.argv[5];
101
+ if (!fixturePath) throw new Error("queries also requires a frozen fixture JSON path");
102
+ const fixtureText = readFileSync(fixturePath, "utf8");
103
+ const fixture = JSON.parse(fixtureText);
104
+ const cli = join(import.meta.dir, "../bin/ledger.ts");
105
+ async function footprint() {
106
+ const result: Record<string, string | null> = {};
107
+ for (const suffix of ["", "-wal", "-write.lock"]) {
108
+ if (!existsSync(dbPath + suffix)) { result[suffix || "db"] = null; continue; }
109
+ const digest = createHash("sha256");
110
+ for await (const chunk of createReadStream(dbPath + suffix)) digest.update(chunk);
111
+ result[suffix || "db"] = digest.digest("hex");
112
+ }
113
+ return result;
114
+ }
115
+ const before = await footprint();
116
+ const attempts = [];
117
+ for (const item of fixture.cases) {
118
+ const args = [process.execPath, cli, "search", item.query, "--db", dbPath, "--limit", String(fixture.limit), "--robot", "--json", "--output-version", "2"];
119
+ if (item.source) args.push("--source", item.source);
120
+ const began = performance.now();
121
+ const child = Bun.spawn(args, { stdout: "pipe", stderr: "pipe", env: { ...process.env, LEDGER_WIDEN_MAX_RECORDS: "10000" } });
122
+ let timedOut = false;
123
+ const timer = setTimeout(() => { timedOut = true; child.kill("SIGKILL"); }, 30000);
124
+ const stdoutPromise = new Response(child.stdout).text();
125
+ const stderrPromise = new Response(child.stderr).text();
126
+ const code = await child.exited;
127
+ clearTimeout(timer);
128
+ const stdout = await stdoutPromise;
129
+ const stderr = await stderrPromise;
130
+ let payload: any = null;
131
+ try { payload = JSON.parse(stdout); } catch { /* Retain a sanitized failure below. */ }
132
+ const hits = Array.isArray(payload?.hits) ? payload.hits : [];
133
+ const rank = hits.findIndex((hit: any) => hit.session_id === item.session) + 1;
134
+ const shapeOk = payload?.search_meta?.type === "search_meta" && Array.isArray(payload.hits);
135
+ const passed = code === 0 && !timedOut && shapeOk && (item.negative ? hits.length === 0 : rank > 0) && (!item.source || hits.every((hit: any) => hit.source === item.source));
136
+ // Bun exposes these as non-enumerable getters, and CPU counters may be bigint.
137
+ const usage = child.resourceUsage();
138
+ const resources = usage ? {
139
+ cpu_us: { user: Number(usage.cpuTime.user), system: Number(usage.cpuTime.system), total: Number(usage.cpuTime.total) },
140
+ max_rss_bytes: usage.maxRSS,
141
+ io_operations: { input: usage.ops.in, output: usage.ops.out },
142
+ } : null;
143
+ attempts.push({ name: item.name, required: item.required, passed, exit_code: code, timed_out: timedOut, query_sha256: hash(item.query), expected_session_sha256: item.session ? hash(item.session) : null, target_rank: rank || null, returned_hits: hits.length, search_meta: payload?.search_meta ?? null, error_code: payload?.error?.code ?? null, stdout_sha256: hash(stdout), stderr_sha256: hash(stderr), elapsed_ms: performance.now() - began, resource_usage: resources });
144
+ }
145
+ const after = await footprint();
146
+ const unchanged = JSON.stringify(before) === JSON.stringify(after);
147
+ const passed = unchanged && fixture.missing_seeds.length === 0 && attempts.filter((a) => a.required).every((a) => a.passed);
148
+ save({ started_at: started, fixture_sha256: hash(fixtureText), attempts, read_only_unchanged: unchanged, footprint_before: before, footprint_after: after, passed, elapsed_ms: performance.now() - start });
149
+ console.log(JSON.stringify({ mode, passed, read_only_unchanged: unchanged, attempts: attempts.map(({ name, passed, required, target_rank, elapsed_ms }) => ({ name, passed, required, target_rank, elapsed_ms })) }));
150
+ if (!passed) process.exitCode = 1;
151
+ }
@@ -0,0 +1,108 @@
1
+ import { mkdirSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import { runCli } from "./cli.js";
5
+ import { ensurePrivateDirectory } from "./private-storage.js";
6
+
7
+ /**
8
+ * Database resolution for the canonical `ledger` executable:
9
+ * explicit --db PATH (or --db=PATH) > TRAJECTORIES_DB > LEDGER_DB >
10
+ * ${XDG_DATA_HOME:-$HOME/.local/share}/ledger/ledger.sqlite.
11
+ *
12
+ * Unlike the legacy entrypoints, the canonical executable never falls back to
13
+ * a database in the current working directory.
14
+ */
15
+ export function resolveCanonicalDbPath(
16
+ env: { [key: string]: string | undefined } = process.env,
17
+ ): string {
18
+ const envDb = env.TRAJECTORIES_DB || env.LEDGER_DB;
19
+ if (typeof envDb === "string" && envDb.length > 0) {
20
+ return envDb;
21
+ }
22
+ const dataHome =
23
+ typeof env.XDG_DATA_HOME === "string" && env.XDG_DATA_HOME.length > 0
24
+ ? env.XDG_DATA_HOME
25
+ : join(env.HOME || homedir(), ".local", "share");
26
+ return join(dataHome, "ledger", "ledger.sqlite");
27
+ }
28
+
29
+ /** Commands that (may) write the store; only these may create the default data dir. */
30
+ const WRITER_COMMANDS = new Set(["sync", "build", "watch"]);
31
+
32
+ function emitUsageError(argv: string[], message: string): never {
33
+ if (argv.includes("--robot")) {
34
+ console.log(JSON.stringify({ error: { code: "INVALID_ARGUMENT", message } }));
35
+ } else {
36
+ console.error(`Error: ${message}`);
37
+ }
38
+ process.exit(1);
39
+ }
40
+
41
+ function hasExplicitDb(argv: string[]): boolean {
42
+ let found = false;
43
+ for (let i = 0; i < argv.length; i++) {
44
+ const arg = argv[i];
45
+ if (arg === "--db") {
46
+ const value = argv[i + 1];
47
+ if (!value || value.startsWith("-")) {
48
+ emitUsageError(argv, "--db requires a path value.");
49
+ }
50
+ found = true;
51
+ i++;
52
+ }
53
+ if (arg === "--db=") {
54
+ emitUsageError(argv, "--db= requires a path value.");
55
+ }
56
+ if (arg.startsWith("--db=")) {
57
+ found = true;
58
+ }
59
+ }
60
+ return found;
61
+ }
62
+
63
+ /**
64
+ * Return argv with a resolved `--db` appended when the caller did not name a
65
+ * database explicitly. Reads stay pure; writer commands additionally ensure
66
+ * the default data directory exists so a first `ledger sync` does not fail on
67
+ * a missing parent directory.
68
+ */
69
+ export function canonicalArgv(argv: string[], env: NodeJS.ProcessEnv = process.env): string[] {
70
+ // Help/version must remain observational even for writer subcommands.
71
+ if (argv.some(arg => ["--help", "-h", "--version", "-v"].includes(arg))) return argv;
72
+ if (hasExplicitDb(argv)) return argv;
73
+
74
+ const head = argv[0];
75
+ if (
76
+ argv.length === 0 ||
77
+ head === "--help" || head === "-h" ||
78
+ head === "--version" || head === "-v"
79
+ ) {
80
+ return argv;
81
+ }
82
+
83
+ const dbPath = resolveCanonicalDbPath(env);
84
+ if (WRITER_COMMANDS.has(head)) {
85
+ // Tighten only our default directory, never a custom database's parent.
86
+ if (!env.TRAJECTORIES_DB && !env.LEDGER_DB) ensurePrivateDirectory(dirname(dbPath));
87
+ else mkdirSync(dirname(dbPath), { recursive: true, mode: 0o700 });
88
+ }
89
+ return [...argv, "--db", dbPath];
90
+ }
91
+
92
+ /** Entry point for bin/ledger.ts: the canonical Ledger CLI. */
93
+ export async function runCanonicalCli(argv: string[]): Promise<void> {
94
+ const dbPath = resolveCanonicalDbPath();
95
+ let patched: string[];
96
+ try { patched = canonicalArgv(argv); }
97
+ catch (error) {
98
+ if (argv.includes("--robot")) {
99
+ console.log(JSON.stringify({ error: { code: "UNSAFE_STORE_PATH", message: error instanceof Error ? error.message : String(error) } }));
100
+ process.exit(1);
101
+ }
102
+ throw error;
103
+ }
104
+ await runCli(patched, {
105
+ programName: "trajectories",
106
+ dbHelpNote: ` (default: ${dbPath})`,
107
+ });
108
+ }