@claude-transcripts/cli 0.7.1 → 0.8.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.
@@ -32,6 +32,11 @@ var ACTIONS = [
32
32
  summary: "Tell the user, in the transcript, whether and where this session is recorded",
33
33
  implemented: true
34
34
  },
35
+ {
36
+ key: "check-hook-compat",
37
+ summary: "Check the running Claude Code's hook spec against ours and report any drift",
38
+ implemented: true
39
+ },
35
40
  {
36
41
  key: "inject-recall-policy",
37
42
  summary: "Prime the session with the recall policy + how much history this cwd has",
@@ -52,8 +57,9 @@ var ACTIONS = [
52
57
  },
53
58
  {
54
59
  key: "reconcile-session",
55
- summary: "Finalize a stale running session from chunks/S3",
56
- implemented: false
60
+ summary: "Write the missing summary for a session that never fired SessionEnd, from its events (and S3 transcript)",
61
+ implemented: true,
62
+ triggers: { cli: "reconcile", follower: true }
57
63
  }
58
64
  ];
59
65
  var BINDINGS = [
@@ -63,6 +69,7 @@ var BINDINGS = [
63
69
  "seed-session-start",
64
70
  "write-event-marker",
65
71
  "announce-recording",
72
+ "check-hook-compat",
66
73
  "inject-recall-policy"
67
74
  ]
68
75
  },
@@ -746,6 +753,24 @@ var CLI_SPEC = {
746
753
  summary: "Rebuild the search indexes from CouchDB",
747
754
  examples: ["reindex"]
748
755
  },
756
+ {
757
+ name: "reconcile",
758
+ group: "admin",
759
+ summary: "Finalise sessions that never ended (write their missing summary)",
760
+ args: [
761
+ {
762
+ name: "session-id",
763
+ description: "sessions to finalise (repeatable); default: every session that reads as `incomplete`"
764
+ },
765
+ { name: "--dry-run", description: "say what would be written, write nothing" },
766
+ {
767
+ name: "--force",
768
+ description: "also finalise a named session that still reads as `running`"
769
+ },
770
+ { name: "--json", description: "print the outcomes as JSON (what the follower reads)" }
771
+ ],
772
+ examples: ["reconcile --dry-run", "reconcile 3f9a2c1e-\u2026 --force", "reconcile --json"]
773
+ },
749
774
  {
750
775
  name: "doctor",
751
776
  group: "admin",
@@ -753,6 +778,25 @@ var CLI_SPEC = {
753
778
  args: [{ name: "--keep", description: "leave the synthetic session behind for inspection" }],
754
779
  examples: ["doctor", "doctor --keep"]
755
780
  },
781
+ {
782
+ name: "compat",
783
+ group: "admin",
784
+ summary: "Check Claude Code's hook spec against this build's (exit 1 on drift)",
785
+ args: [
786
+ {
787
+ name: "action",
788
+ choices: ["check", "fetch"],
789
+ default: "check",
790
+ description: "`check` reports drift; `fetch` caches a version's spec from npm (SessionStart " + "starts it detached when it meets a version it doesn't know)"
791
+ },
792
+ {
793
+ name: "--cc-version",
794
+ type: "string",
795
+ description: "the Claude Code version to check (default: the installed one)"
796
+ }
797
+ ],
798
+ examples: ["compat", "compat --cc-version 2.1.300 --json"]
799
+ },
756
800
  {
757
801
  name: "hook",
758
802
  group: "admin",
@@ -1810,6 +1854,397 @@ var WORKLOAD_ROLES = new Set(["backing", "admin-ui", "app"]);
1810
1854
  function toStoreEnv(model) {
1811
1855
  return { FOSSIL_REPOSITORIES: Object.values(model.stores.repositories).join(",") };
1812
1856
  }
1857
+ // packages/shared/src/claude-code-hook-spec.json
1858
+ var claude_code_hook_spec_default = {
1859
+ claudeCode: "2.1.310",
1860
+ sdk: "0.3.310",
1861
+ base: {
1862
+ agent_id: [],
1863
+ agent_type: [],
1864
+ cwd: [],
1865
+ effort: [],
1866
+ permission_mode: [],
1867
+ prompt_id: [],
1868
+ session_id: [],
1869
+ transcript_path: []
1870
+ },
1871
+ events: {
1872
+ PreToolUse: {
1873
+ mcp_server: [],
1874
+ tool_input: [],
1875
+ tool_name: [],
1876
+ tool_use_id: []
1877
+ },
1878
+ PostToolUse: {
1879
+ duration_ms: [],
1880
+ mcp_server: [],
1881
+ tool_input: [],
1882
+ tool_name: [],
1883
+ tool_response: [],
1884
+ tool_use_id: []
1885
+ },
1886
+ PostToolUseFailure: {
1887
+ duration_ms: [],
1888
+ error: [],
1889
+ is_interrupt: [],
1890
+ mcp_server: [],
1891
+ tool_input: [],
1892
+ tool_name: [],
1893
+ tool_use_id: []
1894
+ },
1895
+ PostToolBatch: {
1896
+ tool_calls: []
1897
+ },
1898
+ Notification: {
1899
+ message: [],
1900
+ notification_type: [],
1901
+ title: []
1902
+ },
1903
+ UserPromptSubmit: {
1904
+ prompt: [],
1905
+ session_title: [],
1906
+ source: ["loop_wakeup", "poll_event", "schedule_wakeup", "sdk", "system", "user"]
1907
+ },
1908
+ UserPromptExpansion: {
1909
+ command_args: [],
1910
+ command_name: [],
1911
+ command_source: [],
1912
+ expansion_type: ["mcp_prompt", "slash_command"],
1913
+ prompt: []
1914
+ },
1915
+ SessionStart: {
1916
+ agent_type: [],
1917
+ context_tokens: [],
1918
+ estimated_cache_write_usd: [],
1919
+ model: [],
1920
+ prompt_cache_likely_expired: [],
1921
+ seconds_since_last_response: [],
1922
+ session_title: [],
1923
+ source: ["clear", "compact", "fork", "resume", "startup"]
1924
+ },
1925
+ SessionEnd: {
1926
+ reason: ["clear", "logout", "other", "prompt_input_exit", "resume"]
1927
+ },
1928
+ Stop: {
1929
+ background_tasks: [],
1930
+ last_assistant_message: [],
1931
+ session_crons: [],
1932
+ stop_hook_active: []
1933
+ },
1934
+ StopFailure: {
1935
+ error: [
1936
+ "account_on_hold",
1937
+ "authentication_failed",
1938
+ "billing_error",
1939
+ "cloud_credential_error",
1940
+ "invalid_request",
1941
+ "max_output_tokens",
1942
+ "model_not_found",
1943
+ "oauth_org_not_allowed",
1944
+ "overloaded",
1945
+ "rate_limit",
1946
+ "server_error",
1947
+ "unknown",
1948
+ "verification_required"
1949
+ ],
1950
+ error_details: [],
1951
+ last_assistant_message: []
1952
+ },
1953
+ SubagentStart: {
1954
+ agent_id: [],
1955
+ agent_type: []
1956
+ },
1957
+ SubagentStop: {
1958
+ agent_id: [],
1959
+ agent_transcript_path: [],
1960
+ agent_type: [],
1961
+ background_tasks: [],
1962
+ last_assistant_message: [],
1963
+ session_crons: [],
1964
+ stop_hook_active: []
1965
+ },
1966
+ PreCompact: {
1967
+ custom_instructions: [],
1968
+ trigger: ["auto", "manual"]
1969
+ },
1970
+ PostCompact: {
1971
+ compact_summary: [],
1972
+ trigger: ["auto", "manual"]
1973
+ },
1974
+ PreModelSwitch: {
1975
+ cache_ttl: ["1h", "5m"],
1976
+ context_tokens: [],
1977
+ estimated_cache_write_usd: [],
1978
+ from_model: [],
1979
+ pricing: ["catalog", "configured", "default"],
1980
+ prompt_cache_warm: [],
1981
+ requested_model: [],
1982
+ source: ["command", "picker", "sdk"],
1983
+ to_model: []
1984
+ },
1985
+ PostModelSwitch: {
1986
+ cache_ttl: ["1h", "5m"],
1987
+ context_tokens: [],
1988
+ estimated_cache_write_usd: [],
1989
+ from_model: [],
1990
+ pricing: ["catalog", "configured", "default"],
1991
+ prompt_cache_warm: [],
1992
+ requested_model: [],
1993
+ source: ["auto", "command", "picker", "resume", "sdk"],
1994
+ to_model: []
1995
+ },
1996
+ PermissionRequest: {
1997
+ mcp_server: [],
1998
+ permission_suggestions: [],
1999
+ tool_input: [],
2000
+ tool_name: []
2001
+ },
2002
+ PermissionDenied: {
2003
+ mcp_server: [],
2004
+ reason: [],
2005
+ tool_input: [],
2006
+ tool_name: [],
2007
+ tool_use_id: []
2008
+ },
2009
+ Setup: {
2010
+ trigger: ["init", "maintenance"]
2011
+ },
2012
+ TeammateIdle: {
2013
+ team_name: [],
2014
+ teammate_name: []
2015
+ },
2016
+ TaskCreated: {
2017
+ task_description: [],
2018
+ task_id: [],
2019
+ task_subject: [],
2020
+ team_name: [],
2021
+ teammate_name: []
2022
+ },
2023
+ TaskCompleted: {
2024
+ task_description: [],
2025
+ task_id: [],
2026
+ task_subject: [],
2027
+ team_name: [],
2028
+ teammate_name: []
2029
+ },
2030
+ Elicitation: {
2031
+ elicitation_id: [],
2032
+ mcp_server_name: [],
2033
+ message: [],
2034
+ mode: ["form", "url"],
2035
+ requested_schema: [],
2036
+ url: []
2037
+ },
2038
+ ElicitationResult: {
2039
+ action: ["accept", "cancel", "decline"],
2040
+ content: [],
2041
+ elicitation_id: [],
2042
+ mcp_server_name: [],
2043
+ mode: ["form", "url"]
2044
+ },
2045
+ ConfigChange: {
2046
+ file_path: [],
2047
+ source: ["local_settings", "policy_settings", "project_settings", "skills", "user_settings"]
2048
+ },
2049
+ WorktreeCreate: {
2050
+ name: []
2051
+ },
2052
+ WorktreeRemove: {
2053
+ worktree_path: []
2054
+ },
2055
+ InstructionsLoaded: {
2056
+ file_path: [],
2057
+ globs: [],
2058
+ load_reason: ["compact", "include", "nested_traversal", "path_glob_match", "session_start"],
2059
+ memory_type: ["Local", "Managed", "Project", "User"],
2060
+ parent_file_path: [],
2061
+ trigger_file_path: []
2062
+ },
2063
+ CwdChanged: {
2064
+ new_cwd: [],
2065
+ old_cwd: []
2066
+ },
2067
+ FileChanged: {
2068
+ event: ["add", "change", "unlink"],
2069
+ file_path: []
2070
+ },
2071
+ DirectoryAdded: {
2072
+ directory: [],
2073
+ source: ["register_repo_root", "slash_command"]
2074
+ },
2075
+ MessageDisplay: {
2076
+ delta: [],
2077
+ final: [],
2078
+ index: [],
2079
+ message_id: [],
2080
+ turn_id: []
2081
+ }
2082
+ }
2083
+ };
2084
+
2085
+ // packages/shared/src/claude-code-spec.ts
2086
+ var VERIFIED_HOOK_SPEC = claude_code_hook_spec_default;
2087
+ function stripComments(src) {
2088
+ return src.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "");
2089
+ }
2090
+ function typeBody(dts, name) {
2091
+ const head = `export declare type ${name} = `;
2092
+ const start = dts.indexOf(head);
2093
+ if (start < 0)
2094
+ return null;
2095
+ let depth = 0;
2096
+ for (let i = start + head.length;i < dts.length; i++) {
2097
+ const c = dts[i];
2098
+ if (c === "{" || c === "(")
2099
+ depth++;
2100
+ else if (c === "}" || c === ")")
2101
+ depth--;
2102
+ else if (c === ";" && depth === 0)
2103
+ return dts.slice(start + head.length, i);
2104
+ }
2105
+ return null;
2106
+ }
2107
+ var LIT = `(?:'[^']*'|"[^"]*")`;
2108
+ var LIT_UNION = new RegExp(`^${LIT}(?:\\s*\\|\\s*${LIT})*$`);
2109
+ function literalAliases(dts) {
2110
+ const out = new Map;
2111
+ const re = /^export declare type (\w+) = (.+);$/gm;
2112
+ for (const m of dts.matchAll(re)) {
2113
+ if (LIT_UNION.test(m[2]))
2114
+ out.set(m[1], literals(m[2]));
2115
+ }
2116
+ return out;
2117
+ }
2118
+ function literals(src) {
2119
+ return [...src.matchAll(/'([^']*)'|"([^"]*)"/g)].map((m) => m[1] ?? m[2]).sort();
2120
+ }
2121
+ function fieldsOf(body, aliases) {
2122
+ const fields = {};
2123
+ let depth = 0;
2124
+ for (const line of body.split(`
2125
+ `)) {
2126
+ if (depth === 1) {
2127
+ const m = /^\s*(\w+)\??:\s*(.*)$/.exec(line);
2128
+ if (m && m[1] !== "hook_event_name") {
2129
+ const type = m[2].replace(/;\s*$/, "").trim();
2130
+ if (LIT_UNION.test(type))
2131
+ fields[m[1]] = literals(type);
2132
+ else
2133
+ fields[m[1]] = aliases.get(type) ?? [];
2134
+ }
2135
+ }
2136
+ for (const c of line) {
2137
+ if (c === "{")
2138
+ depth++;
2139
+ else if (c === "}")
2140
+ depth--;
2141
+ }
2142
+ }
2143
+ return Object.fromEntries(Object.entries(fields).sort(([a], [b]) => a.localeCompare(b)));
2144
+ }
2145
+ function parseSdkHookSpec(source, versions) {
2146
+ const dts = stripComments(source.replace(/\r\n/g, `
2147
+ `));
2148
+ const list = /export declare const HOOK_EVENTS: readonly \[([^\]]*)\]/.exec(dts);
2149
+ if (!list)
2150
+ throw new Error("sdk.d.ts declares no HOOK_EVENTS \u2014 the SDK's layout changed");
2151
+ const baseBody = typeBody(dts, "BaseHookInput");
2152
+ if (!baseBody)
2153
+ throw new Error("sdk.d.ts declares no BaseHookInput \u2014 the SDK's layout changed");
2154
+ const aliases = literalAliases(dts);
2155
+ const events = {};
2156
+ const names = [...list[1].matchAll(/'([^']+)'|"([^"]+)"/g)].map((m) => m[1] ?? m[2]);
2157
+ if (!names.length)
2158
+ throw new Error("sdk.d.ts lists no HOOK_EVENTS \u2014 the SDK's layout changed");
2159
+ for (const event of names) {
2160
+ const body = typeBody(dts, `${event}HookInput`);
2161
+ events[event] = body ? fieldsOf(body, aliases) : {};
2162
+ }
2163
+ return { ...versions, base: fieldsOf(baseBody, aliases), events };
2164
+ }
2165
+ function flatten(fields) {
2166
+ const out = new Set;
2167
+ for (const [name, values] of Object.entries(fields)) {
2168
+ out.add(name);
2169
+ for (const v of values)
2170
+ out.add(`${name}=${v}`);
2171
+ }
2172
+ return out;
2173
+ }
2174
+ function fieldDrift(event, before, after) {
2175
+ const a = flatten(before);
2176
+ const b = flatten(after);
2177
+ const whole = (s, other) => !other.has(s.split("=")[0]);
2178
+ const added = [...b].filter((f) => !a.has(f) && (!f.includes("=") || !whole(f, a))).sort();
2179
+ const removed = [...a].filter((f) => !b.has(f) && (!f.includes("=") || !whole(f, b))).sort();
2180
+ return added.length || removed.length ? { event, added, removed } : null;
2181
+ }
2182
+ function compareHookSpec(catalogue, verified, runningVersion, running) {
2183
+ const wired = catalogue.filter((h) => h.wired);
2184
+ const report = {
2185
+ running: runningVersion,
2186
+ verified: verified.claudeCode,
2187
+ known: running !== null,
2188
+ unlisted: [],
2189
+ goneWired: [],
2190
+ goneListed: [],
2191
+ fields: [],
2192
+ eventCount: running ? Object.keys(running.events).length : 0,
2193
+ wiredCount: wired.length
2194
+ };
2195
+ if (!running)
2196
+ return report;
2197
+ const listed = new Set(catalogue.map((h) => h.event));
2198
+ report.unlisted = Object.keys(running.events).filter((e) => !listed.has(e)).sort();
2199
+ for (const h of catalogue) {
2200
+ if (h.event in running.events)
2201
+ continue;
2202
+ (h.wired ? report.goneWired : report.goneListed).push(h.event);
2203
+ }
2204
+ const base = fieldDrift("*", verified.base, running.base);
2205
+ if (base)
2206
+ report.fields.push(base);
2207
+ for (const h of wired) {
2208
+ const after = running.events[h.event];
2209
+ if (!after)
2210
+ continue;
2211
+ const drift = fieldDrift(h.event, verified.events[h.event] ?? {}, after);
2212
+ if (drift)
2213
+ report.fields.push(drift);
2214
+ }
2215
+ return report;
2216
+ }
2217
+ function hasDrift(r) {
2218
+ return !r.known || r.unlisted.length > 0 || r.goneWired.length > 0 || r.goneListed.length > 0 || r.fields.length > 0;
2219
+ }
2220
+ function formatHookCompat(r) {
2221
+ if (!r.known) {
2222
+ return [
2223
+ `? Hook spec for Claude Code ${r.running} not checked yet (this build knows ${r.verified}) \u2014 fetching it; the result shows next session`
2224
+ ];
2225
+ }
2226
+ if (!hasDrift(r)) {
2227
+ return [
2228
+ `\u2713 Hook spec matches Claude Code ${r.running} (${r.eventCount} events, ${r.wiredCount} recorded)`
2229
+ ];
2230
+ }
2231
+ const against = r.running === r.verified ? "" : ` (this build was checked against ${r.verified})`;
2232
+ const lines = [`\u25B3 Hook spec drift with Claude Code ${r.running}${against}:`];
2233
+ const list = (xs) => xs.join(", ");
2234
+ if (r.goneWired.length)
2235
+ lines.push(` \xB7 recorded, but no longer fired: ${list(r.goneWired)}`);
2236
+ if (r.unlisted.length)
2237
+ lines.push(` \xB7 new events we don't handle: ${list(r.unlisted)}`);
2238
+ if (r.goneListed.length)
2239
+ lines.push(` \xB7 listed, but no longer fired: ${list(r.goneListed)}`);
2240
+ for (const f of r.fields) {
2241
+ const name = f.event === "*" ? "common fields" : f.event;
2242
+ const parts = [...f.added.map((x) => `+${x}`), ...f.removed.map((x) => `\u2212${x}`)];
2243
+ lines.push(` \xB7 ${name}: ${parts.join(" ")}`);
2244
+ }
2245
+ lines.push(" Details: claude-transcripts compat");
2246
+ return lines;
2247
+ }
1813
2248
  // packages/shared/src/couch-url.ts
1814
2249
  var DEFAULT_COUCHDB_HOST = "127.0.0.1";
1815
2250
  var DEFAULT_COUCHDB_PORT = "7652";
@@ -2563,6 +2998,14 @@ async function getOrNull(path) {
2563
2998
  return null;
2564
2999
  return await res.json();
2565
3000
  }
3001
+ async function getTextOrNull(path) {
3002
+ const res = await fetch(`${base()}${path}`);
3003
+ if (res.status === 404)
3004
+ return null;
3005
+ if (!res.ok)
3006
+ throw new Error(`GET ${path} \u2192 ${res.status} ${res.statusText}${await errorDetail(res)}`);
3007
+ return await res.text();
3008
+ }
2566
3009
  async function putStream(path, filePath, contentType) {
2567
3010
  const res = await fetch(`${base()}${path}`, {
2568
3011
  method: "PUT",
@@ -2730,6 +3173,27 @@ function buildSummaryDoc(facts, source, opts = {}) {
2730
3173
  ...opts.actor ? { actor: opts.actor } : {}
2731
3174
  };
2732
3175
  }
3176
+ function buildReconciledSummary(s, transcript, reconciledAt) {
3177
+ return {
3178
+ _id: `summary:${s.sessionId}`,
3179
+ type: "summary",
3180
+ event: "SessionEnd",
3181
+ session_id: s.sessionId,
3182
+ timestamp: s.lastActivity || s.timestamp,
3183
+ hostname: s.hostname,
3184
+ cwd: s.cwd,
3185
+ ...s.model ? { model: s.model } : {},
3186
+ end_reason: "unknown",
3187
+ event_count: s.eventCount,
3188
+ prompt_count: s.promptCount,
3189
+ error_count: s.errorCount,
3190
+ tool_counts: s.toolCounts,
3191
+ transcript_bytes: transcript ? Buffer.byteLength(transcript) : s.transcriptSize ?? 0,
3192
+ ...transcript ? { token_usage: sumTranscriptTokens(transcript) } : {},
3193
+ source: "reconcile",
3194
+ reconciled_at: reconciledAt
3195
+ };
3196
+ }
2733
3197
  function buildChunkDocs(jsonl, facts, source, maxEntriesPerChunk, withContent = true) {
2734
3198
  const slices = sliceIntoChunks(jsonl, maxEntriesPerChunk);
2735
3199
  const allEntries = withContent ? buildChunkEntries(jsonl) : undefined;
@@ -3065,7 +3529,7 @@ class DryRunSink {
3065
3529
  }
3066
3530
  async putSummary(doc) {
3067
3531
  const tools = Object.keys(doc.tool_counts).length;
3068
- console.log(` [dry-run] PUT ${doc._id} (${doc.token_usage.total} tok, ${doc.prompt_count} prompts, ${tools} tools)`);
3532
+ console.log(` [dry-run] PUT ${doc._id} (${doc.token_usage?.total ?? "?"} tok, ${doc.prompt_count} prompts, ${tools} tools)`);
3069
3533
  }
3070
3534
  async putEvents(docs) {
3071
3535
  if (docs.length)
@@ -3097,15 +3561,18 @@ class WebapiSink {
3097
3561
  return res.deleted;
3098
3562
  }
3099
3563
  async putSummary(doc) {
3100
- await ingestSummary(doc);
3564
+ await ingestSummary({ ...doc });
3101
3565
  }
3102
3566
  async putEvents(docs) {
3103
3567
  if (docs.length)
3104
3568
  await ingestEvents({ docs });
3105
3569
  }
3106
3570
  async putChunks(docs) {
3107
- if (docs.length)
3108
- await ingestChunks({ docs });
3571
+ if (!docs.length)
3572
+ return;
3573
+ await ingestChunks({
3574
+ docs: docs.map((d) => ({ ...d, entries: d.entries?.map((e) => ({ ...e })) }))
3575
+ });
3109
3576
  }
3110
3577
  async putTranscript(sessionId, bytes) {
3111
3578
  await putRaw(`/api/ingest/${encodeURIComponent(sessionId)}/transcript`, bytes, "application/x-ndjson");
@@ -3270,6 +3737,138 @@ async function runBackfill(argv) {
3270
3737
  return failed > 0 ? 1 : 0;
3271
3738
  }
3272
3739
 
3740
+ // packages/cli/src/lib/claude-code-spec.ts
3741
+ import { spawnSync } from "child_process";
3742
+ import { createHash } from "crypto";
3743
+ import { existsSync, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
3744
+ import { join as join3 } from "path";
3745
+ import { gunzipSync } from "zlib";
3746
+ var SDK_PACKAGE = "@anthropic-ai/claude-agent-sdk";
3747
+ var REGISTRY = process.env.NPM_CONFIG_REGISTRY?.replace(/\/$/, "") ?? "https://registry.npmjs.org";
3748
+ function claudeCodeVersion(env = process.env) {
3749
+ const candidates = [env.CLAUDE_CODE_EXECPATH, "claude"].filter(Boolean);
3750
+ for (const bin of candidates) {
3751
+ try {
3752
+ const res = spawnSync(bin, ["--version"], { encoding: "utf8", timeout: 3000, cwd: "/" });
3753
+ const m = /^(\d+\.\d+\.\d+)/.exec(res.stdout?.trim() ?? "");
3754
+ if (m)
3755
+ return m[1];
3756
+ } catch {}
3757
+ }
3758
+ return null;
3759
+ }
3760
+ function cacheDir() {
3761
+ return join3(installPaths().dataDir, "claude-code-specs");
3762
+ }
3763
+ function cacheFile(version) {
3764
+ return join3(cacheDir(), `${version}.json`);
3765
+ }
3766
+ function knownHookSpec(version) {
3767
+ if (version === VERIFIED_HOOK_SPEC.claudeCode)
3768
+ return VERIFIED_HOOK_SPEC;
3769
+ try {
3770
+ const file = cacheFile(version);
3771
+ return existsSync(file) ? JSON.parse(readFileSync2(file, "utf8")) : null;
3772
+ } catch {
3773
+ return null;
3774
+ }
3775
+ }
3776
+ function cacheHookSpec(spec) {
3777
+ mkdirSync(cacheDir(), { recursive: true });
3778
+ writeFileSync(cacheFile(spec.claudeCode), `${JSON.stringify(spec, null, 2)}
3779
+ `);
3780
+ }
3781
+ async function getJson(url) {
3782
+ const res = await fetch(url, { headers: { accept: "application/json" } });
3783
+ if (!res.ok)
3784
+ throw new Error(`GET ${url} \u2192 HTTP ${res.status}`);
3785
+ return await res.json();
3786
+ }
3787
+ async function sdkReleases() {
3788
+ const doc = await getJson(`${REGISTRY}/${SDK_PACKAGE.replace("/", "%2f")}`);
3789
+ const releases = Object.values(doc.versions).sort((a, b) => (doc.time[a.version] ?? "").localeCompare(doc.time[b.version] ?? ""));
3790
+ return { releases, latest: doc["dist-tags"].latest ?? "" };
3791
+ }
3792
+ function tarEntry(tgz, path) {
3793
+ const tar = gunzipSync(tgz);
3794
+ let off = 0;
3795
+ while (off + 512 <= tar.length) {
3796
+ const name = tar.subarray(off, off + 100).toString("utf8").replace(/\0.*$/s, "");
3797
+ if (!name)
3798
+ return null;
3799
+ const size = Number.parseInt(tar.subarray(off + 124, off + 136).toString("utf8").trim(), 8);
3800
+ const body = off + 512;
3801
+ if (name === path)
3802
+ return tar.subarray(body, body + size).toString("utf8");
3803
+ off = body + Math.ceil(size / 512) * 512;
3804
+ }
3805
+ return null;
3806
+ }
3807
+ async function fetchHookSpec(version) {
3808
+ const { releases } = await sdkReleases();
3809
+ const release = releases.filter((r) => r.claudeCodeVersion === version).at(-1);
3810
+ if (!release)
3811
+ throw new Error(`no ${SDK_PACKAGE} release names Claude Code ${version}`);
3812
+ const res = await fetch(release.dist.tarball);
3813
+ if (!res.ok)
3814
+ throw new Error(`GET ${release.dist.tarball} \u2192 HTTP ${res.status}`);
3815
+ const tgz = new Uint8Array(await res.arrayBuffer());
3816
+ const integrity = release.dist.integrity;
3817
+ if (integrity?.startsWith("sha512-")) {
3818
+ const got = `sha512-${createHash("sha512").update(tgz).digest("base64")}`;
3819
+ if (got !== integrity)
3820
+ throw new Error(`${SDK_PACKAGE}@${release.version}: integrity mismatch`);
3821
+ }
3822
+ const dts = tarEntry(tgz, "package/sdk.d.ts");
3823
+ if (!dts)
3824
+ throw new Error(`${SDK_PACKAGE}@${release.version} has no sdk.d.ts`);
3825
+ return parseSdkHookSpec(dts, { claudeCode: version, sdk: release.version });
3826
+ }
3827
+
3828
+ // packages/cli/src/commands/compat.ts
3829
+ async function specFor(version) {
3830
+ const known = knownHookSpec(version);
3831
+ if (known)
3832
+ return known;
3833
+ const spec = await fetchHookSpec(version);
3834
+ cacheHookSpec(spec);
3835
+ return spec;
3836
+ }
3837
+ async function runCompat(argv) {
3838
+ const { positionals, options } = parseFlags(argv);
3839
+ const action = positionals[0] ?? "check";
3840
+ const version = strOpt(options, "cc-version") ?? claudeCodeVersion();
3841
+ if (!version) {
3842
+ console.error("compat: can't tell which Claude Code is installed \u2014 pass --cc-version");
3843
+ return 2;
3844
+ }
3845
+ let spec;
3846
+ try {
3847
+ spec = await specFor(version);
3848
+ } catch (err) {
3849
+ console.error(`compat: no hook spec for Claude Code ${version}: ${err.message}`);
3850
+ return 2;
3851
+ }
3852
+ if (action === "fetch") {
3853
+ if (!options.json)
3854
+ console.log(`compat: hook spec for Claude Code ${version} (${spec.sdk})`);
3855
+ return 0;
3856
+ }
3857
+ const report = compareHookSpec(HOOK_TYPES, VERIFIED_HOOK_SPEC, version, spec);
3858
+ if (options.json) {
3859
+ console.log(JSON.stringify(report, null, 2));
3860
+ } else {
3861
+ for (const line of formatHookCompat(report)) {
3862
+ if (!line.includes("claude-transcripts compat"))
3863
+ console.log(line);
3864
+ }
3865
+ console.log(`
3866
+ checked against: Claude Code ${version} via @anthropic-ai/claude-agent-sdk@${spec.sdk}` + `
3867
+ this build: verified on Claude Code ${VERIFIED_HOOK_SPEC.claudeCode}`);
3868
+ }
3869
+ return hasDrift(report) ? 1 : 0;
3870
+ }
3871
+
3273
3872
  // packages/cli/src/commands/completions.ts
3274
3873
  var BIN = "claude-transcripts";
3275
3874
  async function runCompletions(argv) {
@@ -3466,13 +4065,13 @@ async function runDoctor(argv) {
3466
4065
  }
3467
4066
 
3468
4067
  // packages/cli/src/commands/export.ts
3469
- import { mkdirSync as mkdirSync2 } from "fs";
4068
+ import { mkdirSync as mkdirSync3 } from "fs";
3470
4069
  import { hostname as hostname3 } from "os";
3471
- import { join as join5 } from "path";
4070
+ import { join as join6 } from "path";
3472
4071
 
3473
4072
  // packages/cli/src/lib/app-config.ts
3474
- import { existsSync, readFileSync as readFileSync2 } from "fs";
3475
- import { join as join3 } from "path";
4073
+ import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
4074
+ import { join as join4 } from "path";
3476
4075
 
3477
4076
  // packages/cli/src/lib/assets.generated.ts
3478
4077
  var EMBEDDED_ASSETS = {
@@ -3855,7 +4454,11 @@ exec fossil server "$@"
3855
4454
  "logging": {
3856
4455
  "chunk": { "maxEntriesPerChunk": 200, "flushIntervalMs": 15000 }
3857
4456
  },
3858
- "sessions": { "liveWindowMs": 86400000, "idleThresholdMs": 300000 },
4457
+ "sessions": {
4458
+ "liveWindowMs": 86400000,
4459
+ "idleThresholdMs": 300000,
4460
+ "reconcile": { "follower": true, "intervalMs": 3600000, "batchSize": 50 }
4461
+ },
3859
4462
  "search": { "flushIntervalMs": 180000, "taskRetentionMs": 3600000 }
3860
4463
  },
3861
4464
  "couchdb": {
@@ -3913,10 +4516,10 @@ exec fossil server "$@"
3913
4516
 
3914
4517
  // packages/cli/src/lib/app-config.ts
3915
4518
  function repoConfig() {
3916
- const root = join3(import.meta.dir, "..", "..", "..", "..");
4519
+ const root = join4(import.meta.dir, "..", "..", "..", "..");
3917
4520
  for (const name of ["config.json", "config.template.json"]) {
3918
- const p = join3(root, "config", name);
3919
- if (existsSync(p))
4521
+ const p = join4(root, "config", name);
4522
+ if (existsSync2(p))
3920
4523
  return p;
3921
4524
  }
3922
4525
  return null;
@@ -3926,16 +4529,16 @@ function loadAppConfig() {
3926
4529
  const candidates = [paths.appConfig, repoConfig()].filter((p) => Boolean(p));
3927
4530
  for (const path of candidates) {
3928
4531
  try {
3929
- if (existsSync(path))
3930
- return JSON.parse(readFileSync2(path, "utf8"));
4532
+ if (existsSync2(path))
4533
+ return JSON.parse(readFileSync3(path, "utf8"));
3931
4534
  } catch {}
3932
4535
  }
3933
4536
  return JSON.parse(EMBEDDED_ASSETS.appConfig.content);
3934
4537
  }
3935
4538
 
3936
4539
  // packages/cli/src/lib/bundle.ts
3937
- import { chmodSync, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync3, writeFileSync } from "fs";
3938
- import { join as join4 } from "path";
4540
+ import { chmodSync, existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
4541
+ import { join as join5 } from "path";
3939
4542
  var BUNDLE_FORMAT = 1;
3940
4543
  var MANIFEST_FILE = "manifest.json";
3941
4544
  var DOCS_FILE = "docs.ndjson";
@@ -3952,28 +4555,28 @@ function isExportableDoc(id, type) {
3952
4555
  function bundlePaths(dir) {
3953
4556
  return {
3954
4557
  dir,
3955
- manifest: join4(dir, MANIFEST_FILE),
3956
- docs: join4(dir, DOCS_FILE),
3957
- blobs: join4(dir, BLOBS_DIR),
3958
- blobDir: (sessionId) => join4(dir, BLOBS_DIR, sessionId)
4558
+ manifest: join5(dir, MANIFEST_FILE),
4559
+ docs: join5(dir, DOCS_FILE),
4560
+ blobs: join5(dir, BLOBS_DIR),
4561
+ blobDir: (sessionId) => join5(dir, BLOBS_DIR, sessionId)
3959
4562
  };
3960
4563
  }
3961
4564
  function prepareBundleDir(dir) {
3962
- mkdirSync(dir, { recursive: true, mode: 448 });
3963
- mkdirSync(join4(dir, BLOBS_DIR), { recursive: true, mode: 448 });
4565
+ mkdirSync2(dir, { recursive: true, mode: 448 });
4566
+ mkdirSync2(join5(dir, BLOBS_DIR), { recursive: true, mode: 448 });
3964
4567
  }
3965
4568
  function writeManifest(dir, manifest) {
3966
- writeFileSync(bundlePaths(dir).manifest, `${JSON.stringify(manifest, null, 2)}
4569
+ writeFileSync2(bundlePaths(dir).manifest, `${JSON.stringify(manifest, null, 2)}
3967
4570
  `, {
3968
4571
  mode: 384
3969
4572
  });
3970
4573
  }
3971
4574
  function readManifest(dir) {
3972
4575
  const p = bundlePaths(dir).manifest;
3973
- if (!existsSync2(p)) {
4576
+ if (!existsSync3(p)) {
3974
4577
  throw new Error(`no ${MANIFEST_FILE} in ${dir} \u2014 not a bundle, or the export was interrupted`);
3975
4578
  }
3976
- return JSON.parse(readFileSync3(p, "utf8"));
4579
+ return JSON.parse(readFileSync4(p, "utf8"));
3977
4580
  }
3978
4581
  function toBundleDoc(doc) {
3979
4582
  const { _rev, ...rest } = doc;
@@ -4020,7 +4623,7 @@ function formatBytes(n) {
4020
4623
 
4021
4624
  // packages/cli/src/commands/export.ts
4022
4625
  var PAGE = 500;
4023
- async function getJson(path) {
4626
+ async function getJson2(path) {
4024
4627
  const res = await fetch(`${webapiUrl()}${path}`);
4025
4628
  if (!res.ok)
4026
4629
  throw new Error(`GET ${path} \u2192 ${res.status} ${res.statusText}`);
@@ -4028,7 +4631,7 @@ async function getJson(path) {
4028
4631
  }
4029
4632
  async function schemaVersion() {
4030
4633
  try {
4031
- const s = await getJson("/api/migrate/status");
4634
+ const s = await getJson2("/api/migrate/status");
4032
4635
  return s.currentVersion ?? 0;
4033
4636
  } catch {
4034
4637
  return 0;
@@ -4036,7 +4639,7 @@ async function schemaVersion() {
4036
4639
  }
4037
4640
  async function appVersion() {
4038
4641
  try {
4039
- const h = await getJson("/health");
4642
+ const h = await getJson2("/health");
4040
4643
  return h.version ?? "unknown";
4041
4644
  } catch {
4042
4645
  return "unknown";
@@ -4075,7 +4678,7 @@ async function runExport(argv) {
4075
4678
  });
4076
4679
  if (startkey)
4077
4680
  params.set("startkey", JSON.stringify(startkey));
4078
- const page = await getJson(`/api/couch/${encodeURIComponent(db)}/_all_docs?${params}`);
4681
+ const page = await getJson2(`/api/couch/${encodeURIComponent(db)}/_all_docs?${params}`);
4079
4682
  const rows = page.rows ?? [];
4080
4683
  const batch = rows.slice(0, PAGE);
4081
4684
  if (!batch.length)
@@ -4126,8 +4729,8 @@ async function runExport(argv) {
4126
4729
  const res = await fetch(`${webapiUrl()}/api/s3/sessions/${sid}/${name}`);
4127
4730
  if (!res.ok || !res.body)
4128
4731
  continue;
4129
- mkdirSync2(paths.blobDir(sid), { recursive: true, mode: 448 });
4130
- const target = join5(paths.blobDir(sid), name);
4732
+ mkdirSync3(paths.blobDir(sid), { recursive: true, mode: 448 });
4733
+ const target = join6(paths.blobDir(sid), name);
4131
4734
  const { bytes, sha256 } = await streamToFile(res.body, target);
4132
4735
  checksums[`blobs/${sid}/${name}`] = sha256;
4133
4736
  blobCount++;
@@ -4160,7 +4763,7 @@ async function runExport(argv) {
4160
4763
  }
4161
4764
 
4162
4765
  // packages/cli/src/commands/hook.ts
4163
- import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "fs";
4766
+ import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
4164
4767
  import { dirname as dirname2 } from "path";
4165
4768
 
4166
4769
  // packages/cli/src/lib/statusline.ts
@@ -4349,11 +4952,11 @@ function osc8(url, text) {
4349
4952
  import {
4350
4953
  closeSync,
4351
4954
  openSync,
4352
- readFileSync as readFileSync4,
4955
+ readFileSync as readFileSync5,
4353
4956
  readSync,
4354
4957
  statSync as statSync2,
4355
4958
  unlinkSync,
4356
- writeFileSync as writeFileSync2
4959
+ writeFileSync as writeFileSync3
4357
4960
  } from "fs";
4358
4961
  import { hostname as hostname4 } from "os";
4359
4962
  var {S3Client } = globalThis.Bun;
@@ -4383,6 +4986,26 @@ function uploadArgv(sessionId, transcriptPath) {
4383
4986
  return [...cliArgv(), "hook", "upload", "--session", sessionId, "--transcript", transcriptPath];
4384
4987
  }
4385
4988
 
4989
+ // packages/cli/src/hook/compat.ts
4990
+ var REAL = {
4991
+ version: () => claudeCodeVersion(),
4992
+ spec: knownHookSpec,
4993
+ fetch: (version) => spawnDetached([...cliArgv(), "compat", "fetch", "--cc-version", version])
4994
+ };
4995
+ function hookCompatLines(deps = REAL) {
4996
+ try {
4997
+ const version = deps.version();
4998
+ if (!version)
4999
+ return ["? Hook spec not checked \u2014 `claude --version` gave no answer"];
5000
+ const spec = deps.spec(version);
5001
+ if (!spec)
5002
+ deps.fetch(version);
5003
+ return formatHookCompat(compareHookSpec(HOOK_TYPES, VERIFIED_HOOK_SPEC, version, spec));
5004
+ } catch {
5005
+ return [];
5006
+ }
5007
+ }
5008
+
4386
5009
  // packages/cli/src/hook/mirror.ts
4387
5010
  var DEFAULT_TIMEOUT_MS = 2000;
4388
5011
  var DEFAULT_BLOB_TIMEOUT_MS = 60000;
@@ -4508,7 +5131,7 @@ function normalizeHookConfig(raw) {
4508
5131
  }
4509
5132
  function loadHookConfig(path) {
4510
5133
  try {
4511
- const raw = JSON.parse(readFileSync4(path, "utf8"));
5134
+ const raw = JSON.parse(readFileSync5(path, "utf8"));
4512
5135
  if (!raw?.couch?.url)
4513
5136
  return null;
4514
5137
  return normalizeHookConfig(raw);
@@ -4619,14 +5242,14 @@ function makeCounts(sessionId) {
4619
5242
  const file = `/tmp/claude-transcripts-${sessionId}.counts`;
4620
5243
  const read = () => {
4621
5244
  try {
4622
- return JSON.parse(readFileSync4(file, "utf8"));
5245
+ return JSON.parse(readFileSync5(file, "utf8"));
4623
5246
  } catch {
4624
5247
  return emptyCounts();
4625
5248
  }
4626
5249
  };
4627
5250
  const write = (c) => {
4628
5251
  try {
4629
- writeFileSync2(file, JSON.stringify(c));
5252
+ writeFileSync3(file, JSON.stringify(c));
4630
5253
  } catch {}
4631
5254
  };
4632
5255
  return {
@@ -4656,14 +5279,14 @@ function makeTargets(sessionId) {
4656
5279
  const file = targetsFile(sessionId);
4657
5280
  const read = () => {
4658
5281
  try {
4659
- return JSON.parse(readFileSync4(file, "utf8"));
5282
+ return JSON.parse(readFileSync5(file, "utf8"));
4660
5283
  } catch {
4661
5284
  return null;
4662
5285
  }
4663
5286
  };
4664
5287
  const write = (t) => {
4665
5288
  try {
4666
- writeFileSync2(file, JSON.stringify(t));
5289
+ writeFileSync3(file, JSON.stringify(t));
4667
5290
  } catch {}
4668
5291
  };
4669
5292
  return {
@@ -4743,14 +5366,14 @@ function makeChunkState(sessionId) {
4743
5366
  const lockFile = `/tmp/claude-transcripts-${sessionId}.chunklock`;
4744
5367
  const load = () => {
4745
5368
  try {
4746
- return JSON.parse(readFileSync4(stateFile, "utf8"));
5369
+ return JSON.parse(readFileSync5(stateFile, "utf8"));
4747
5370
  } catch {
4748
5371
  return { offset: 0, lastFlushMs: 0 };
4749
5372
  }
4750
5373
  };
4751
5374
  const save = (s) => {
4752
5375
  try {
4753
- writeFileSync2(stateFile, JSON.stringify(s));
5376
+ writeFileSync3(stateFile, JSON.stringify(s));
4754
5377
  } catch {}
4755
5378
  };
4756
5379
  return {
@@ -4826,6 +5449,7 @@ function buildContext(payload, config) {
4826
5449
  const sessionsDb = config.couch.databases.sessions ?? DEFAULT_SESSIONS_DB;
4827
5450
  return {
4828
5451
  output: {},
5452
+ hookCompat: () => hookCompatLines(),
4829
5453
  chunkHighWater: () => fetchChunkHighWater(config, sessionsDb, sessionId),
4830
5454
  event,
4831
5455
  sessionId,
@@ -4869,10 +5493,12 @@ function recordingBanner(targets, sessionId) {
4869
5493
  }
4870
5494
  var NOT_RECORDING_BANNER = "\u25CB Claude Transcripts not recording \u2014 no instance configured. Run `claude-transcripts install`.";
4871
5495
  function sessionStartEnvelope(out) {
4872
- if (!out.systemMessage && !out.additionalContext)
5496
+ const systemMessage = [out.systemMessage, ...out.notices ?? []].filter(Boolean).join(`
5497
+ `);
5498
+ if (!systemMessage && !out.additionalContext)
4873
5499
  return null;
4874
5500
  return {
4875
- ...out.systemMessage ? { systemMessage: out.systemMessage } : {},
5501
+ ...systemMessage ? { systemMessage } : {},
4876
5502
  ...out.additionalContext ? {
4877
5503
  hookSpecificOutput: {
4878
5504
  hookEventName: "SessionStart",
@@ -4891,7 +5517,7 @@ function emitSessionStart(out) {
4891
5517
  }
4892
5518
 
4893
5519
  // packages/cli/src/hook/handlers.ts
4894
- import { readFileSync as readFileSync5, statSync as statSync3 } from "fs";
5520
+ import { readFileSync as readFileSync6, statSync as statSync3 } from "fs";
4895
5521
 
4896
5522
  // packages/cli/src/hook/recall.ts
4897
5523
  var FACTS_TIMEOUT_MS = 2000;
@@ -4981,6 +5607,13 @@ var announceRecording = async (ctx) => {
4981
5607
  const targets = ctx.targets.read() ?? resolveTargets(ctx.config, safeWebapiUrl());
4982
5608
  ctx.output.systemMessage = recordingBanner(targets, ctx.sessionId);
4983
5609
  };
5610
+ var checkHookCompat = async (ctx) => {
5611
+ if (ctx.payload?.source === "compact")
5612
+ return;
5613
+ const lines = ctx.hookCompat();
5614
+ if (lines.length)
5615
+ ctx.output.notices = [...ctx.output.notices ?? [], ...lines];
5616
+ };
4984
5617
  var injectRecallPolicy = async (ctx) => {
4985
5618
  const primer = await buildPrimer(ctx);
4986
5619
  if (primer)
@@ -5109,7 +5742,7 @@ var writeSummary = async (ctx) => {
5109
5742
  let transcriptBytes = 0;
5110
5743
  if (ctx.transcriptPath) {
5111
5744
  try {
5112
- jsonl = readFileSync5(ctx.transcriptPath, "utf8");
5745
+ jsonl = readFileSync6(ctx.transcriptPath, "utf8");
5113
5746
  transcriptBytes = Buffer.byteLength(jsonl);
5114
5747
  } catch {}
5115
5748
  }
@@ -5135,7 +5768,7 @@ var writeSummary = async (ctx) => {
5135
5768
  async function uploadTranscript(blob, bucket, sessionId, transcriptPath) {
5136
5769
  let data;
5137
5770
  try {
5138
- data = readFileSync5(transcriptPath, "utf8");
5771
+ data = readFileSync6(transcriptPath, "utf8");
5139
5772
  } catch {
5140
5773
  return;
5141
5774
  }
@@ -5151,6 +5784,7 @@ var uploadBlobs = async (ctx) => {
5151
5784
  var HANDLERS = {
5152
5785
  "seed-session-start": seedSessionStart,
5153
5786
  "announce-recording": announceRecording,
5787
+ "check-hook-compat": checkHookCompat,
5154
5788
  "inject-recall-policy": injectRecallPolicy,
5155
5789
  "update-counts": updateCounts,
5156
5790
  "write-event-marker": writeEventMarker,
@@ -5215,35 +5849,35 @@ async function dispatch(raw, configPath) {
5215
5849
  }
5216
5850
 
5217
5851
  // packages/cli/src/lib/plugin.ts
5218
- import { readFileSync as readFileSync6 } from "fs";
5219
- import { dirname, join as join6 } from "path";
5852
+ import { readFileSync as readFileSync7 } from "fs";
5853
+ import { dirname, join as join7 } from "path";
5220
5854
  var PLUGIN_NAME = "claude-transcripts";
5221
5855
  function readJson(path) {
5222
5856
  try {
5223
- const parsed = JSON.parse(readFileSync6(path, "utf8"));
5857
+ const parsed = JSON.parse(readFileSync7(path, "utf8"));
5224
5858
  return parsed && typeof parsed === "object" ? parsed : null;
5225
5859
  } catch {
5226
5860
  return null;
5227
5861
  }
5228
5862
  }
5229
5863
  function pluginsDir(settingsPath) {
5230
- return join6(dirname(settingsPath), "plugins");
5864
+ return join7(dirname(settingsPath), "plugins");
5231
5865
  }
5232
5866
  function pluginEvents(settingsPath, key) {
5233
- const installed = readJson(join6(pluginsDir(settingsPath), "installed_plugins.json"));
5867
+ const installed = readJson(join7(pluginsDir(settingsPath), "installed_plugins.json"));
5234
5868
  const entry = installed?.plugins?.[key];
5235
5869
  const first = Array.isArray(entry) ? entry[0] : undefined;
5236
5870
  const installPath = typeof first?.installPath === "string" ? first.installPath : undefined;
5237
5871
  if (!installPath)
5238
5872
  return;
5239
- const hooks = readJson(join6(installPath, "hooks", "hooks.json"));
5873
+ const hooks = readJson(join7(installPath, "hooks", "hooks.json"));
5240
5874
  const events = hooks?.hooks;
5241
5875
  if (!events || typeof events !== "object")
5242
5876
  return;
5243
5877
  return Object.keys(events);
5244
5878
  }
5245
5879
  function pluginVersion(settingsPath, key) {
5246
- const installed = readJson(join6(pluginsDir(settingsPath), "installed_plugins.json"));
5880
+ const installed = readJson(join7(pluginsDir(settingsPath), "installed_plugins.json"));
5247
5881
  const entry = installed?.plugins?.[key];
5248
5882
  const first = Array.isArray(entry) ? entry[0] : undefined;
5249
5883
  return typeof first?.version === "string" ? first.version : undefined;
@@ -5269,7 +5903,7 @@ function pluginRegistration(settingsPath, settings) {
5269
5903
  // packages/cli/src/commands/hook.ts
5270
5904
  function readSettings(path) {
5271
5905
  try {
5272
- return JSON.parse(readFileSync7(path, "utf8"));
5906
+ return JSON.parse(readFileSync8(path, "utf8"));
5273
5907
  } catch {
5274
5908
  return {};
5275
5909
  }
@@ -5366,8 +6000,8 @@ function install(argv) {
5366
6000
  console.log(`hook: would register ${added} event(s)` + `${replaced ? `, replacing ${replaced} stale one(s)` : ""}` + `${corrected ? `, correcting ${corrected} existing one(s)` : ""} in ${paths.claudeSettings}`);
5367
6001
  return 0;
5368
6002
  }
5369
- mkdirSync3(dirname2(paths.claudeSettings), { recursive: true });
5370
- writeFileSync3(paths.claudeSettings, `${JSON.stringify({ ...settings, hooks }, null, 2)}
6003
+ mkdirSync4(dirname2(paths.claudeSettings), { recursive: true });
6004
+ writeFileSync4(paths.claudeSettings, `${JSON.stringify({ ...settings, hooks }, null, 2)}
5371
6005
  `);
5372
6006
  const notes = [
5373
6007
  replaced ? `replaced ${replaced} stale registration(s)` : "",
@@ -5398,7 +6032,7 @@ function uninstall() {
5398
6032
  if (kept.length)
5399
6033
  hooks[event] = kept;
5400
6034
  }
5401
- writeFileSync3(paths.claudeSettings, `${JSON.stringify({ ...settings, hooks }, null, 2)}
6035
+ writeFileSync4(paths.claudeSettings, `${JSON.stringify({ ...settings, hooks }, null, 2)}
5402
6036
  `);
5403
6037
  console.log(`hook: removed ${removed} registration(s) from ${paths.claudeSettings}`);
5404
6038
  const plugin = pluginRegistration(paths.claudeSettings, settings);
@@ -5408,7 +6042,7 @@ function uninstall() {
5408
6042
  }
5409
6043
  function configuredMirrors(path) {
5410
6044
  try {
5411
- const parsed = JSON.parse(readFileSync7(path, "utf8"));
6045
+ const parsed = JSON.parse(readFileSync8(path, "utf8"));
5412
6046
  return (parsed.mirrors ?? []).map((m) => m?.url ?? "(no url)");
5413
6047
  } catch {
5414
6048
  return [];
@@ -5425,7 +6059,7 @@ function status() {
5425
6059
  const found = findRegistered(settings);
5426
6060
  const plugin = pluginRegistration(paths.claudeSettings, settings);
5427
6061
  console.log(`hook: settings ${paths.claudeSettings}`);
5428
- console.log(`hook: config ${paths.hookConfig} ${existsSync3(paths.hookConfig) ? "" : "(MISSING \u2014 run `install`)"}`);
6062
+ console.log(`hook: config ${paths.hookConfig} ${existsSync4(paths.hookConfig) ? "" : "(MISSING \u2014 run `install`)"}`);
5429
6063
  console.log(`hook: command ${hookCommand()}`);
5430
6064
  if (plugin)
5431
6065
  console.log(`hook: plugin ${pluginSummary(plugin)}`);
@@ -5499,8 +6133,8 @@ async function runHook(argv) {
5499
6133
  }
5500
6134
 
5501
6135
  // packages/cli/src/commands/import.ts
5502
- import { existsSync as existsSync4, statSync as statSync4 } from "fs";
5503
- import { join as join7 } from "path";
6136
+ import { existsSync as existsSync5, statSync as statSync4 } from "fs";
6137
+ import { join as join8 } from "path";
5504
6138
  var BATCH = 500;
5505
6139
  function num(n) {
5506
6140
  return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
@@ -5570,8 +6204,8 @@ function checkSchema(bundleVersion, target, registry = REAL_REGISTRY) {
5570
6204
  async function verifyChecksums(dir, manifest) {
5571
6205
  const bad = [];
5572
6206
  for (const [rel, expected] of Object.entries(manifest.checksums)) {
5573
- const path = join7(dir, rel);
5574
- if (!existsSync4(path)) {
6207
+ const path = join8(dir, rel);
6208
+ if (!existsSync5(path)) {
5575
6209
  bad.push(`${rel} (missing)`);
5576
6210
  continue;
5577
6211
  }
@@ -5704,7 +6338,7 @@ async function runImport(argv) {
5704
6338
  let blobs = 0;
5705
6339
  let blobBytes = 0;
5706
6340
  let skipped = 0;
5707
- if (withBlobs && existsSync4(paths.blobs)) {
6341
+ if (withBlobs && existsSync5(paths.blobs)) {
5708
6342
  for (const rel of Object.keys(manifest.checksums)) {
5709
6343
  if (!rel.startsWith("blobs/"))
5710
6344
  continue;
@@ -5715,7 +6349,7 @@ async function runImport(argv) {
5715
6349
  skipped++;
5716
6350
  continue;
5717
6351
  }
5718
- const path = join7(dir, rel);
6352
+ const path = join8(dir, rel);
5719
6353
  await putStream(`/api/ingest/${sessionId}/transcript`, path, "application/x-ndjson");
5720
6354
  blobs++;
5721
6355
  blobBytes += statSync4(path).size;
@@ -5739,58 +6373,58 @@ async function runImport(argv) {
5739
6373
  }
5740
6374
 
5741
6375
  // packages/cli/src/commands/install.ts
5742
- import { existsSync as existsSync8, readFileSync as readFileSync12 } from "fs";
5743
- import { join as join9 } from "path";
6376
+ import { existsSync as existsSync9, readFileSync as readFileSync13 } from "fs";
6377
+ import { join as join10 } from "path";
5744
6378
 
5745
6379
  // packages/cli/src/lib/assets.ts
5746
6380
  import {
5747
- existsSync as existsSync5,
6381
+ existsSync as existsSync6,
5748
6382
  lstatSync,
5749
- mkdirSync as mkdirSync4,
5750
- readFileSync as readFileSync8,
6383
+ mkdirSync as mkdirSync5,
6384
+ readFileSync as readFileSync9,
5751
6385
  symlinkSync,
5752
6386
  unlinkSync as unlinkSync2,
5753
- writeFileSync as writeFileSync4
6387
+ writeFileSync as writeFileSync5
5754
6388
  } from "fs";
5755
- import { dirname as dirname3, join as join8 } from "path";
6389
+ import { dirname as dirname3, join as join9 } from "path";
5756
6390
  function writeAssets(paths) {
5757
6391
  const out = [];
5758
6392
  for (const asset of Object.values(EMBEDDED_ASSETS)) {
5759
6393
  const isUserOwned = asset.path.startsWith("config/");
5760
- const target = isUserOwned ? paths.appConfig : join8(paths.dataDir, asset.path);
5761
- if (isUserOwned && existsSync5(target)) {
6394
+ const target = isUserOwned ? paths.appConfig : join9(paths.dataDir, asset.path);
6395
+ if (isUserOwned && existsSync6(target)) {
5762
6396
  out.push({ path: target, changed: false });
5763
6397
  continue;
5764
6398
  }
5765
- const previous = existsSync5(target) ? readFileSync8(target, "utf8") : null;
6399
+ const previous = existsSync6(target) ? readFileSync9(target, "utf8") : null;
5766
6400
  if (previous === asset.content) {
5767
6401
  out.push({ path: target, changed: false });
5768
6402
  continue;
5769
6403
  }
5770
- mkdirSync4(dirname3(target), { recursive: true });
5771
- writeFileSync4(target, asset.content);
6404
+ mkdirSync5(dirname3(target), { recursive: true });
6405
+ writeFileSync5(target, asset.content);
5772
6406
  out.push({ path: target, changed: true });
5773
6407
  }
5774
6408
  return out;
5775
6409
  }
5776
6410
  function linkInstanceEnv(paths) {
5777
- const link = join8(paths.dataDir, ".env");
6411
+ const link = join9(paths.dataDir, ".env");
5778
6412
  try {
5779
6413
  if (lstatSync(link, { throwIfNoEntry: false }))
5780
6414
  unlinkSync2(link);
5781
6415
  } catch {}
5782
- mkdirSync4(paths.dataDir, { recursive: true });
6416
+ mkdirSync5(paths.dataDir, { recursive: true });
5783
6417
  symlinkSync(paths.instanceEnv, link);
5784
6418
  }
5785
6419
  function writeVersionStamp(paths, version) {
5786
- mkdirSync4(dirname3(paths.versionFile), { recursive: true });
5787
- writeFileSync4(paths.versionFile, `${version}
6420
+ mkdirSync5(dirname3(paths.versionFile), { recursive: true });
6421
+ writeFileSync5(paths.versionFile, `${version}
5788
6422
  `);
5789
6423
  }
5790
6424
  function composeFiles(paths, upstream) {
5791
- const files = [join8(paths.dataDir, EMBEDDED_ASSETS.compose.path)];
6425
+ const files = [join9(paths.dataDir, EMBEDDED_ASSETS.compose.path)];
5792
6426
  if (upstream)
5793
- files.push(join8(paths.dataDir, EMBEDDED_ASSETS.composeUpstream.path));
6427
+ files.push(join9(paths.dataDir, EMBEDDED_ASSETS.composeUpstream.path));
5794
6428
  return files;
5795
6429
  }
5796
6430
 
@@ -5858,7 +6492,7 @@ function healthTargets(env) {
5858
6492
  }
5859
6493
 
5860
6494
  // packages/cli/src/lib/hook-config.ts
5861
- import { chmodSync as chmodSync2, mkdirSync as mkdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "fs";
6495
+ import { chmodSync as chmodSync2, mkdirSync as mkdirSync6, readFileSync as readFileSync10, writeFileSync as writeFileSync6 } from "fs";
5862
6496
  import { dirname as dirname4 } from "path";
5863
6497
  function buildHookConfig(app, env) {
5864
6498
  const user = env.COUCHDB_USER;
@@ -5892,7 +6526,7 @@ function webapiFromEnv(env) {
5892
6526
  var PRESERVED_KEYS = ["mirrors", "webapi"];
5893
6527
  function readExisting(path) {
5894
6528
  try {
5895
- const parsed = JSON.parse(readFileSync9(path, "utf8"));
6529
+ const parsed = JSON.parse(readFileSync10(path, "utf8"));
5896
6530
  return parsed && typeof parsed === "object" ? parsed : {};
5897
6531
  } catch {
5898
6532
  return {};
@@ -5908,8 +6542,8 @@ function mergePreserved(next, existing) {
5908
6542
  }
5909
6543
  function writeHookConfig(path, config) {
5910
6544
  const merged = mergePreserved(config ?? {}, readExisting(path));
5911
- mkdirSync5(dirname4(path), { recursive: true });
5912
- writeFileSync5(path, `${JSON.stringify(merged, null, 2)}
6545
+ mkdirSync6(dirname4(path), { recursive: true });
6546
+ writeFileSync6(path, `${JSON.stringify(merged, null, 2)}
5913
6547
  `);
5914
6548
  chmodSync2(path, 384);
5915
6549
  }
@@ -5986,7 +6620,7 @@ function renderPruneReport(report, repo) {
5986
6620
 
5987
6621
  // packages/cli/src/lib/instance-env.ts
5988
6622
  import { randomBytes } from "crypto";
5989
- import { chmodSync as chmodSync3, existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync10, writeFileSync as writeFileSync6 } from "fs";
6623
+ import { chmodSync as chmodSync3, existsSync as existsSync7, mkdirSync as mkdirSync7, readFileSync as readFileSync11, writeFileSync as writeFileSync7 } from "fs";
5990
6624
  import { dirname as dirname5 } from "path";
5991
6625
  var DEFAULT_PORT_BASE = 7650;
5992
6626
  var PORT_KEYS = [
@@ -6086,7 +6720,7 @@ function backfillPorts(existing) {
6086
6720
  }
6087
6721
  function loadOrCreateInstanceEnv(path, opts = {}) {
6088
6722
  const generated = buildInstanceEnv(opts);
6089
- const existing = existsSync6(path) ? parseEnv(readFileSync10(path, "utf8")) : {};
6723
+ const existing = existsSync7(path) ? parseEnv(readFileSync11(path, "utf8")) : {};
6090
6724
  const merged = { ...generated, ...backfillPorts(existing), ...existing };
6091
6725
  for (const key of INSTALLER_OWNED) {
6092
6726
  if (generated[key] !== undefined)
@@ -6100,12 +6734,12 @@ function loadOrCreateInstanceEnv(path, opts = {}) {
6100
6734
  return merged;
6101
6735
  }
6102
6736
  function writeInstanceEnv(path, env) {
6103
- mkdirSync6(dirname5(path), { recursive: true });
6104
- writeFileSync6(path, formatEnv(env, HEADER));
6737
+ mkdirSync7(dirname5(path), { recursive: true });
6738
+ writeFileSync7(path, formatEnv(env, HEADER));
6105
6739
  chmodSync3(path, 384);
6106
6740
  }
6107
6741
  function updateInstanceEnv(path, patch) {
6108
- const env = existsSync6(path) ? parseEnv(readFileSync10(path, "utf8")) : {};
6742
+ const env = existsSync7(path) ? parseEnv(readFileSync11(path, "utf8")) : {};
6109
6743
  const next = { ...env, ...patch };
6110
6744
  writeInstanceEnv(path, next);
6111
6745
  return next;
@@ -6387,11 +7021,11 @@ async function removeStaleKeys(api, keep) {
6387
7021
  }
6388
7022
 
6389
7023
  // packages/cli/src/commands/statusline.ts
6390
- import { existsSync as existsSync7, mkdirSync as mkdirSync7, readFileSync as readFileSync11, writeFileSync as writeFileSync7 } from "fs";
7024
+ import { existsSync as existsSync8, mkdirSync as mkdirSync8, readFileSync as readFileSync12, writeFileSync as writeFileSync8 } from "fs";
6391
7025
  import { dirname as dirname6 } from "path";
6392
7026
  function readSettings2(path) {
6393
7027
  try {
6394
- return JSON.parse(readFileSync11(path, "utf8"));
7028
+ return JSON.parse(readFileSync12(path, "utf8"));
6395
7029
  } catch {
6396
7030
  return {};
6397
7031
  }
@@ -6409,7 +7043,7 @@ async function render() {
6409
7043
  try {
6410
7044
  input = JSON.parse(await Bun.stdin.text());
6411
7045
  } catch {}
6412
- const configured = existsSync7(installPaths().hookConfig);
7046
+ const configured = existsSync8(installPaths().hookConfig);
6413
7047
  const id = input.session_id;
6414
7048
  const targets = id ? makeTargets(id).read() : null;
6415
7049
  const line = renderStatusline({
@@ -6454,8 +7088,8 @@ function install2(argv) {
6454
7088
  console.log(` ${JSON.stringify(next)}`);
6455
7089
  return 0;
6456
7090
  }
6457
- mkdirSync7(dirname6(paths.claudeSettings), { recursive: true });
6458
- writeFileSync7(paths.claudeSettings, `${JSON.stringify({ ...settings, statusLine: next }, null, 2)}
7091
+ mkdirSync8(dirname6(paths.claudeSettings), { recursive: true });
7092
+ writeFileSync8(paths.claudeSettings, `${JSON.stringify({ ...settings, statusLine: next }, null, 2)}
6459
7093
  `);
6460
7094
  console.log(`statusline: registered \u2192 ${paths.claudeSettings}`);
6461
7095
  console.log("statusline: restart any open Claude Code session for it to take effect.");
@@ -6473,7 +7107,7 @@ function uninstall2() {
6473
7107
  return 1;
6474
7108
  }
6475
7109
  const { statusLine: _drop, ...rest } = settings;
6476
- writeFileSync7(paths.claudeSettings, `${JSON.stringify(rest, null, 2)}
7110
+ writeFileSync8(paths.claudeSettings, `${JSON.stringify(rest, null, 2)}
6477
7111
  `);
6478
7112
  console.log(`statusline: removed from ${paths.claudeSettings}`);
6479
7113
  return 0;
@@ -6597,7 +7231,7 @@ async function runInstall(argv) {
6597
7231
  const noPrune = options["no-prune"] === true;
6598
7232
  const portBaseOpt = strOpt(options, "port-base");
6599
7233
  const paths = installPaths();
6600
- const priorAppTag = existsSync8(paths.instanceEnv) ? /^APP_TAG=(\S+)\s*$/m.exec(readFileSync12(paths.instanceEnv, "utf8"))?.[1] : undefined;
7234
+ const priorAppTag = existsSync9(paths.instanceEnv) ? /^APP_TAG=(\S+)\s*$/m.exec(readFileSync13(paths.instanceEnv, "utf8"))?.[1] : undefined;
6601
7235
  const TOTAL = 8;
6602
7236
  console.log(`claude-transcripts install (${VERSION})`);
6603
7237
  console.log(` config ${paths.configDir}`);
@@ -6625,8 +7259,8 @@ async function runInstall(argv) {
6625
7259
  portBase = found;
6626
7260
  }
6627
7261
  step(2, TOTAL, "Configuration");
6628
- const firstRun = !existsSync8(paths.instanceEnv);
6629
- const priorKeys = new Set(firstRun ? [] : Object.keys(parseEnv(readFileSync12(paths.instanceEnv, "utf8"))));
7262
+ const firstRun = !existsSync9(paths.instanceEnv);
7263
+ const priorKeys = new Set(firstRun ? [] : Object.keys(parseEnv(readFileSync13(paths.instanceEnv, "utf8"))));
6630
7264
  let env = loadOrCreateInstanceEnv(paths.instanceEnv, {
6631
7265
  portBase,
6632
7266
  appTag: VERSION === DEV_VERSION ? "main" : VERSION,
@@ -6730,7 +7364,7 @@ async function runInstall(argv) {
6730
7364
  install: done.`);
6731
7365
  console.log(` UI http://127.0.0.1:${env.WEBAPI_PORT}/app`);
6732
7366
  console.log(` API http://127.0.0.1:${env.WEBAPI_PORT}/api/docs`);
6733
- console.log(` data ${join9(paths.deployDir, "data")} (survives uninstall unless --purge)`);
7367
+ console.log(` data ${join10(paths.deployDir, "data")} (survives uninstall unless --purge)`);
6734
7368
  console.log(" remove claude-transcripts uninstall");
6735
7369
  return 0;
6736
7370
  }
@@ -6790,10 +7424,10 @@ async function runMigrate(argv) {
6790
7424
  }
6791
7425
 
6792
7426
  // packages/cli/src/commands/provision.ts
6793
- import { existsSync as existsSync9 } from "fs";
7427
+ import { existsSync as existsSync10 } from "fs";
6794
7428
  async function runProvision(_argv) {
6795
7429
  const paths = installPaths();
6796
- if (!existsSync9(paths.instanceEnv)) {
7430
+ if (!existsSync10(paths.instanceEnv)) {
6797
7431
  console.error("provision: this machine has no instance \u2014 run `claude-transcripts install` first.");
6798
7432
  return 1;
6799
7433
  }
@@ -6824,6 +7458,112 @@ async function runProvision(_argv) {
6824
7458
  return ok ? 0 : 1;
6825
7459
  }
6826
7460
 
7461
+ // packages/cli/src/commands/reconcile.ts
7462
+ var PAGE2 = 500;
7463
+ async function incompleteSessions() {
7464
+ const out = [];
7465
+ for (let skip = 0;; skip += PAGE2) {
7466
+ const page = await listSessions({ status: "incomplete", limit: PAGE2, skip });
7467
+ out.push(...page.sessions);
7468
+ if (out.length >= page.totalCount || page.sessions.length === 0)
7469
+ return out;
7470
+ }
7471
+ }
7472
+ async function sessionOrNull(id) {
7473
+ try {
7474
+ return await getSession(id);
7475
+ } catch (err) {
7476
+ if (isConnectionError(err))
7477
+ throw err;
7478
+ return null;
7479
+ }
7480
+ }
7481
+ async function reconcileOne(s, opts) {
7482
+ const sessionId = s.sessionId;
7483
+ if (s.status === "ended")
7484
+ return { sessionId, result: "skipped", reason: "already ended" };
7485
+ if (s.status === "running" && !opts.force) {
7486
+ return { sessionId, result: "skipped", reason: "still running (--force to finalise it)" };
7487
+ }
7488
+ try {
7489
+ const transcript = opts.blobs ? await getTextOrNull(`/api/s3/sessions/${encodeURIComponent(sessionId)}/transcript.jsonl`) : null;
7490
+ const doc = buildReconciledSummary(s, transcript, opts.now);
7491
+ if (!opts.dryRun)
7492
+ await opts.sink.putSummary(doc);
7493
+ return {
7494
+ sessionId,
7495
+ result: opts.dryRun ? "would-reconcile" : "reconciled",
7496
+ tokens: transcript !== null
7497
+ };
7498
+ } catch (err) {
7499
+ return { sessionId, result: "failed", reason: err.message };
7500
+ }
7501
+ }
7502
+ function describe(o) {
7503
+ const tokens = o.tokens === false ? " (no S3 transcript \u2014 no token totals)" : "";
7504
+ switch (o.result) {
7505
+ case "reconciled":
7506
+ return ` finalised ${o.sessionId}${tokens}`;
7507
+ case "would-reconcile":
7508
+ return ` would finalise ${o.sessionId}${tokens}`;
7509
+ case "skipped":
7510
+ return ` skipped ${o.sessionId} \u2014 ${o.reason}`;
7511
+ case "failed":
7512
+ return ` FAILED ${o.sessionId} \u2014 ${o.reason}`;
7513
+ }
7514
+ }
7515
+ async function runReconcile(argv) {
7516
+ const { positionals, options } = parseFlags(argv);
7517
+ const webapi = strOpt(options, "webapi");
7518
+ if (webapi)
7519
+ setWebapiUrl(webapi);
7520
+ const json = options.json === true;
7521
+ const dryRun = options["dry-run"] === true;
7522
+ const force = options.force === true;
7523
+ if (!await webapiReachable()) {
7524
+ console.error(unreachableHint("reconcile"));
7525
+ return 1;
7526
+ }
7527
+ const outcomes = [];
7528
+ const now = new Date().toISOString();
7529
+ const sink = new WebapiSink;
7530
+ try {
7531
+ const manifest = await getOrNull("/");
7532
+ const blobs = manifest?.features?.s3Blobs !== false;
7533
+ const sessions = [];
7534
+ if (positionals.length) {
7535
+ for (const id of positionals) {
7536
+ const s = await sessionOrNull(id);
7537
+ if (s)
7538
+ sessions.push(s);
7539
+ else
7540
+ outcomes.push({ sessionId: id, result: "failed", reason: "no such session" });
7541
+ }
7542
+ } else {
7543
+ sessions.push(...await incompleteSessions());
7544
+ }
7545
+ for (const s of sessions) {
7546
+ outcomes.push(await reconcileOne(s, { dryRun, force, now, blobs, sink }));
7547
+ }
7548
+ } catch (err) {
7549
+ console.error(`reconcile: failed \u2014 ${err.message}`);
7550
+ console.error(unreachableHint("reconcile"));
7551
+ return 1;
7552
+ }
7553
+ const failed = outcomes.filter((o) => o.result === "failed").length;
7554
+ if (json) {
7555
+ console.log(JSON.stringify({ dryRun, outcomes }, null, 2));
7556
+ } else if (!outcomes.length) {
7557
+ console.log("reconcile: nothing to finalise \u2014 no session reads as incomplete");
7558
+ } else {
7559
+ for (const o of outcomes)
7560
+ console.log(describe(o));
7561
+ const done = outcomes.filter((o) => o.result === "reconciled" || o.result === "would-reconcile");
7562
+ console.log(`reconcile: ${dryRun ? "would finalise" : "finalised"} ${done.length}, skipped ${outcomes.length - done.length - failed}, failed ${failed}`);
7563
+ }
7564
+ return failed ? 1 : 0;
7565
+ }
7566
+
6827
7567
  // packages/cli/src/commands/reindex.ts
6828
7568
  function num2(n) {
6829
7569
  return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
@@ -7142,18 +7882,18 @@ async function runSessions(argv) {
7142
7882
  }
7143
7883
 
7144
7884
  // packages/cli/src/commands/setup.ts
7145
- import { existsSync as existsSync10, mkdirSync as mkdirSync8, readFileSync as readFileSync13, writeFileSync as writeFileSync8 } from "fs";
7885
+ import { existsSync as existsSync11, mkdirSync as mkdirSync9, readFileSync as readFileSync14, writeFileSync as writeFileSync9 } from "fs";
7146
7886
  import { homedir as homedir3 } from "os";
7147
- import { dirname as dirname7, join as join10 } from "path";
7148
- var REPO_ROOT = join10(import.meta.dir, "..", "..", "..", "..");
7149
- var HOOK_DIR = join10(REPO_ROOT, "hooks");
7150
- var HOOK_CONFIG_PATH = process.env.CT_HOOK_CONFIG ?? join10(homedir3(), ".config", "claude-transcripts", "config.json");
7887
+ import { dirname as dirname7, join as join11 } from "path";
7888
+ var REPO_ROOT = join11(import.meta.dir, "..", "..", "..", "..");
7889
+ var HOOK_DIR = join11(REPO_ROOT, "hooks");
7890
+ var HOOK_CONFIG_PATH = process.env.CT_HOOK_CONFIG ?? join11(homedir3(), ".config", "claude-transcripts", "config.json");
7151
7891
  var GLOBAL_SETTINGS = installPaths().claudeSettings;
7152
- var PROJECT_SETTINGS = join10(process.cwd(), ".claude", "settings.json");
7892
+ var PROJECT_SETTINGS = join11(process.cwd(), ".claude", "settings.json");
7153
7893
  function loadRepoConfig() {
7154
- const live = join10(REPO_ROOT, "config", "config.json");
7155
- const template = join10(REPO_ROOT, "config", "config.template.json");
7156
- return JSON.parse(readFileSync13(existsSync10(live) ? live : template, "utf8"));
7894
+ const live = join11(REPO_ROOT, "config", "config.json");
7895
+ const template = join11(REPO_ROOT, "config", "config.template.json");
7896
+ return JSON.parse(readFileSync14(existsSync11(live) ? live : template, "utf8"));
7157
7897
  }
7158
7898
  function couchHeaders(auth) {
7159
7899
  const h = { "Content-Type": "application/json" };
@@ -7203,7 +7943,7 @@ async function probeGarage(blob) {
7203
7943
  }
7204
7944
  function readSettings3(path) {
7205
7945
  try {
7206
- const parsed = JSON.parse(readFileSync13(path, "utf8"));
7946
+ const parsed = JSON.parse(readFileSync14(path, "utf8"));
7207
7947
  return parsed && typeof parsed === "object" ? parsed : {};
7208
7948
  } catch {
7209
7949
  return {};
@@ -7212,7 +7952,7 @@ function readSettings3(path) {
7212
7952
  function globalRegistrationBlockedBy(settingsPath = GLOBAL_SETTINGS) {
7213
7953
  return pluginRegistration(settingsPath, readSettings3(settingsPath));
7214
7954
  }
7215
- var DISPATCH_PATH = join10(HOOK_DIR, "scripts", "dispatch.ts");
7955
+ var DISPATCH_PATH = join11(HOOK_DIR, "scripts", "dispatch.ts");
7216
7956
  function dispatchCommand() {
7217
7957
  return hookLaunch(`bun run "${DISPATCH_PATH}"`);
7218
7958
  }
@@ -7223,7 +7963,7 @@ function isRegistered(path) {
7223
7963
  }
7224
7964
  function registerGlobal(dryRun) {
7225
7965
  const cmd = dispatchCommand();
7226
- const generated = JSON.parse(readFileSync13(join10(HOOK_DIR, "hooks", "hooks.json"), "utf8"));
7966
+ const generated = JSON.parse(readFileSync14(join11(HOOK_DIR, "hooks", "hooks.json"), "utf8"));
7227
7967
  const settings = readSettings3(GLOBAL_SETTINGS);
7228
7968
  const hooks = settings.hooks ?? {};
7229
7969
  for (const [event, groups] of Object.entries(generated.hooks)) {
@@ -7243,8 +7983,8 @@ function registerGlobal(dryRun) {
7243
7983
  console.log(` [check] would register ${Object.keys(generated.hooks).length} events in ${GLOBAL_SETTINGS}`);
7244
7984
  return;
7245
7985
  }
7246
- mkdirSync8(dirname7(GLOBAL_SETTINGS), { recursive: true });
7247
- writeFileSync8(GLOBAL_SETTINGS, `${JSON.stringify({ ...settings, hooks }, null, 2)}
7986
+ mkdirSync9(dirname7(GLOBAL_SETTINGS), { recursive: true });
7987
+ writeFileSync9(GLOBAL_SETTINGS, `${JSON.stringify({ ...settings, hooks }, null, 2)}
7248
7988
  `);
7249
7989
  console.log(` registered ${Object.keys(generated.hooks).length} events (system-wide) in ${GLOBAL_SETTINGS}`);
7250
7990
  }
@@ -7253,7 +7993,7 @@ function registerProject() {
7253
7993
  }
7254
7994
  function reportExisting() {
7255
7995
  const g = isRegistered(GLOBAL_SETTINGS);
7256
- const p = existsSync10(PROJECT_SETTINGS) && isRegistered(PROJECT_SETTINGS);
7996
+ const p = existsSync11(PROJECT_SETTINGS) && isRegistered(PROJECT_SETTINGS);
7257
7997
  console.log(` existing registration \u2192 global: ${g ? "yes" : "no"}, this project: ${p ? "yes" : "no"}`);
7258
7998
  const plugin = globalRegistrationBlockedBy();
7259
7999
  if (plugin) {
@@ -7289,7 +8029,7 @@ async function runSetup(argv) {
7289
8029
  reportExisting();
7290
8030
  if (check) {
7291
8031
  console.log("setup --check (read-only):");
7292
- console.log(` hook config: ${existsSync10(HOOK_CONFIG_PATH) ? "present" : "MISSING"} (${HOOK_CONFIG_PATH})`);
8032
+ console.log(` hook config: ${existsSync11(HOOK_CONFIG_PATH) ? "present" : "MISSING"} (${HOOK_CONFIG_PATH})`);
7293
8033
  console.log(` couch: ${couch.url}`);
7294
8034
  await provisionCouch2(couch, true);
7295
8035
  await probeGarage(hookConfig.blob);
@@ -7320,12 +8060,12 @@ async function runSetup(argv) {
7320
8060
  }
7321
8061
 
7322
8062
  // packages/cli/src/commands/stack.ts
7323
- import { existsSync as existsSync11 } from "fs";
8063
+ import { existsSync as existsSync12 } from "fs";
7324
8064
  async function runStack(argv) {
7325
8065
  const { positionals, options } = parseFlags(argv);
7326
8066
  const cmd = positionals[0] ?? "ps";
7327
8067
  const paths = installPaths();
7328
- if (!existsSync11(paths.instanceEnv)) {
8068
+ if (!existsSync12(paths.instanceEnv)) {
7329
8069
  console.error("stack: this machine has no instance \u2014 run `claude-transcripts install` first.");
7330
8070
  return 1;
7331
8071
  }
@@ -7463,8 +8203,8 @@ async function runTurns(argv) {
7463
8203
  }
7464
8204
 
7465
8205
  // packages/cli/src/commands/uninstall.ts
7466
- import { existsSync as existsSync12, readdirSync, rmSync } from "fs";
7467
- import { join as join11 } from "path";
8206
+ import { existsSync as existsSync13, readdirSync, rmSync } from "fs";
8207
+ import { join as join12 } from "path";
7468
8208
  var DATA_SUBDIR = "data";
7469
8209
  function removeDeployDir(deployDir, purge) {
7470
8210
  if (purge) {
@@ -7479,7 +8219,7 @@ function removeDeployDir(deployDir, purge) {
7479
8219
  }
7480
8220
  for (const entry of entries) {
7481
8221
  if (entry !== DATA_SUBDIR)
7482
- rmSync(join11(deployDir, entry), { recursive: true, force: true });
8222
+ rmSync(join12(deployDir, entry), { recursive: true, force: true });
7483
8223
  }
7484
8224
  }
7485
8225
  async function confirmPurge(auto) {
@@ -7502,7 +8242,7 @@ async function runUninstall(argv) {
7502
8242
  console.log("uninstall: deregistering the hook and the statusline");
7503
8243
  await runHook(["uninstall"]);
7504
8244
  await runStatusline(["uninstall"]);
7505
- if (existsSync12(paths.instanceEnv)) {
8245
+ if (existsSync13(paths.instanceEnv)) {
7506
8246
  console.log("uninstall: stopping the stack");
7507
8247
  try {
7508
8248
  const env = loadOrCreateInstanceEnv(paths.instanceEnv);
@@ -7517,7 +8257,7 @@ async function runUninstall(argv) {
7517
8257
  rmSync(target, { recursive: true, force: true });
7518
8258
  } catch {}
7519
8259
  }
7520
- const dataDir = join11(paths.deployDir, DATA_SUBDIR);
8260
+ const dataDir = join12(paths.deployDir, DATA_SUBDIR);
7521
8261
  try {
7522
8262
  removeDeployDir(paths.deployDir, purge);
7523
8263
  } catch (err) {
@@ -7554,6 +8294,8 @@ var COMMANDS = {
7554
8294
  migrate: runMigrate,
7555
8295
  reindex: runReindex,
7556
8296
  doctor: runDoctor,
8297
+ reconcile: runReconcile,
8298
+ compat: runCompat,
7557
8299
  sessions: runSessions,
7558
8300
  search: runSearch,
7559
8301
  turns: runTurns,