@claude-transcripts/cli 0.7.2 → 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)
@@ -3273,6 +3737,138 @@ async function runBackfill(argv) {
3273
3737
  return failed > 0 ? 1 : 0;
3274
3738
  }
3275
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
+
3276
3872
  // packages/cli/src/commands/completions.ts
3277
3873
  var BIN = "claude-transcripts";
3278
3874
  async function runCompletions(argv) {
@@ -3469,13 +4065,13 @@ async function runDoctor(argv) {
3469
4065
  }
3470
4066
 
3471
4067
  // packages/cli/src/commands/export.ts
3472
- import { mkdirSync as mkdirSync2 } from "fs";
4068
+ import { mkdirSync as mkdirSync3 } from "fs";
3473
4069
  import { hostname as hostname3 } from "os";
3474
- import { join as join5 } from "path";
4070
+ import { join as join6 } from "path";
3475
4071
 
3476
4072
  // packages/cli/src/lib/app-config.ts
3477
- import { existsSync, readFileSync as readFileSync2 } from "fs";
3478
- import { join as join3 } from "path";
4073
+ import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
4074
+ import { join as join4 } from "path";
3479
4075
 
3480
4076
  // packages/cli/src/lib/assets.generated.ts
3481
4077
  var EMBEDDED_ASSETS = {
@@ -3858,7 +4454,11 @@ exec fossil server "$@"
3858
4454
  "logging": {
3859
4455
  "chunk": { "maxEntriesPerChunk": 200, "flushIntervalMs": 15000 }
3860
4456
  },
3861
- "sessions": { "liveWindowMs": 86400000, "idleThresholdMs": 300000 },
4457
+ "sessions": {
4458
+ "liveWindowMs": 86400000,
4459
+ "idleThresholdMs": 300000,
4460
+ "reconcile": { "follower": true, "intervalMs": 3600000, "batchSize": 50 }
4461
+ },
3862
4462
  "search": { "flushIntervalMs": 180000, "taskRetentionMs": 3600000 }
3863
4463
  },
3864
4464
  "couchdb": {
@@ -3916,10 +4516,10 @@ exec fossil server "$@"
3916
4516
 
3917
4517
  // packages/cli/src/lib/app-config.ts
3918
4518
  function repoConfig() {
3919
- const root = join3(import.meta.dir, "..", "..", "..", "..");
4519
+ const root = join4(import.meta.dir, "..", "..", "..", "..");
3920
4520
  for (const name of ["config.json", "config.template.json"]) {
3921
- const p = join3(root, "config", name);
3922
- if (existsSync(p))
4521
+ const p = join4(root, "config", name);
4522
+ if (existsSync2(p))
3923
4523
  return p;
3924
4524
  }
3925
4525
  return null;
@@ -3929,16 +4529,16 @@ function loadAppConfig() {
3929
4529
  const candidates = [paths.appConfig, repoConfig()].filter((p) => Boolean(p));
3930
4530
  for (const path of candidates) {
3931
4531
  try {
3932
- if (existsSync(path))
3933
- return JSON.parse(readFileSync2(path, "utf8"));
4532
+ if (existsSync2(path))
4533
+ return JSON.parse(readFileSync3(path, "utf8"));
3934
4534
  } catch {}
3935
4535
  }
3936
4536
  return JSON.parse(EMBEDDED_ASSETS.appConfig.content);
3937
4537
  }
3938
4538
 
3939
4539
  // packages/cli/src/lib/bundle.ts
3940
- import { chmodSync, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync3, writeFileSync } from "fs";
3941
- 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";
3942
4542
  var BUNDLE_FORMAT = 1;
3943
4543
  var MANIFEST_FILE = "manifest.json";
3944
4544
  var DOCS_FILE = "docs.ndjson";
@@ -3955,28 +4555,28 @@ function isExportableDoc(id, type) {
3955
4555
  function bundlePaths(dir) {
3956
4556
  return {
3957
4557
  dir,
3958
- manifest: join4(dir, MANIFEST_FILE),
3959
- docs: join4(dir, DOCS_FILE),
3960
- blobs: join4(dir, BLOBS_DIR),
3961
- 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)
3962
4562
  };
3963
4563
  }
3964
4564
  function prepareBundleDir(dir) {
3965
- mkdirSync(dir, { recursive: true, mode: 448 });
3966
- 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 });
3967
4567
  }
3968
4568
  function writeManifest(dir, manifest) {
3969
- writeFileSync(bundlePaths(dir).manifest, `${JSON.stringify(manifest, null, 2)}
4569
+ writeFileSync2(bundlePaths(dir).manifest, `${JSON.stringify(manifest, null, 2)}
3970
4570
  `, {
3971
4571
  mode: 384
3972
4572
  });
3973
4573
  }
3974
4574
  function readManifest(dir) {
3975
4575
  const p = bundlePaths(dir).manifest;
3976
- if (!existsSync2(p)) {
4576
+ if (!existsSync3(p)) {
3977
4577
  throw new Error(`no ${MANIFEST_FILE} in ${dir} \u2014 not a bundle, or the export was interrupted`);
3978
4578
  }
3979
- return JSON.parse(readFileSync3(p, "utf8"));
4579
+ return JSON.parse(readFileSync4(p, "utf8"));
3980
4580
  }
3981
4581
  function toBundleDoc(doc) {
3982
4582
  const { _rev, ...rest } = doc;
@@ -4023,7 +4623,7 @@ function formatBytes(n) {
4023
4623
 
4024
4624
  // packages/cli/src/commands/export.ts
4025
4625
  var PAGE = 500;
4026
- async function getJson(path) {
4626
+ async function getJson2(path) {
4027
4627
  const res = await fetch(`${webapiUrl()}${path}`);
4028
4628
  if (!res.ok)
4029
4629
  throw new Error(`GET ${path} \u2192 ${res.status} ${res.statusText}`);
@@ -4031,7 +4631,7 @@ async function getJson(path) {
4031
4631
  }
4032
4632
  async function schemaVersion() {
4033
4633
  try {
4034
- const s = await getJson("/api/migrate/status");
4634
+ const s = await getJson2("/api/migrate/status");
4035
4635
  return s.currentVersion ?? 0;
4036
4636
  } catch {
4037
4637
  return 0;
@@ -4039,7 +4639,7 @@ async function schemaVersion() {
4039
4639
  }
4040
4640
  async function appVersion() {
4041
4641
  try {
4042
- const h = await getJson("/health");
4642
+ const h = await getJson2("/health");
4043
4643
  return h.version ?? "unknown";
4044
4644
  } catch {
4045
4645
  return "unknown";
@@ -4078,7 +4678,7 @@ async function runExport(argv) {
4078
4678
  });
4079
4679
  if (startkey)
4080
4680
  params.set("startkey", JSON.stringify(startkey));
4081
- const page = await getJson(`/api/couch/${encodeURIComponent(db)}/_all_docs?${params}`);
4681
+ const page = await getJson2(`/api/couch/${encodeURIComponent(db)}/_all_docs?${params}`);
4082
4682
  const rows = page.rows ?? [];
4083
4683
  const batch = rows.slice(0, PAGE);
4084
4684
  if (!batch.length)
@@ -4129,8 +4729,8 @@ async function runExport(argv) {
4129
4729
  const res = await fetch(`${webapiUrl()}/api/s3/sessions/${sid}/${name}`);
4130
4730
  if (!res.ok || !res.body)
4131
4731
  continue;
4132
- mkdirSync2(paths.blobDir(sid), { recursive: true, mode: 448 });
4133
- const target = join5(paths.blobDir(sid), name);
4732
+ mkdirSync3(paths.blobDir(sid), { recursive: true, mode: 448 });
4733
+ const target = join6(paths.blobDir(sid), name);
4134
4734
  const { bytes, sha256 } = await streamToFile(res.body, target);
4135
4735
  checksums[`blobs/${sid}/${name}`] = sha256;
4136
4736
  blobCount++;
@@ -4163,7 +4763,7 @@ async function runExport(argv) {
4163
4763
  }
4164
4764
 
4165
4765
  // packages/cli/src/commands/hook.ts
4166
- 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";
4167
4767
  import { dirname as dirname2 } from "path";
4168
4768
 
4169
4769
  // packages/cli/src/lib/statusline.ts
@@ -4352,11 +4952,11 @@ function osc8(url, text) {
4352
4952
  import {
4353
4953
  closeSync,
4354
4954
  openSync,
4355
- readFileSync as readFileSync4,
4955
+ readFileSync as readFileSync5,
4356
4956
  readSync,
4357
4957
  statSync as statSync2,
4358
4958
  unlinkSync,
4359
- writeFileSync as writeFileSync2
4959
+ writeFileSync as writeFileSync3
4360
4960
  } from "fs";
4361
4961
  import { hostname as hostname4 } from "os";
4362
4962
  var {S3Client } = globalThis.Bun;
@@ -4386,6 +4986,26 @@ function uploadArgv(sessionId, transcriptPath) {
4386
4986
  return [...cliArgv(), "hook", "upload", "--session", sessionId, "--transcript", transcriptPath];
4387
4987
  }
4388
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
+
4389
5009
  // packages/cli/src/hook/mirror.ts
4390
5010
  var DEFAULT_TIMEOUT_MS = 2000;
4391
5011
  var DEFAULT_BLOB_TIMEOUT_MS = 60000;
@@ -4511,7 +5131,7 @@ function normalizeHookConfig(raw) {
4511
5131
  }
4512
5132
  function loadHookConfig(path) {
4513
5133
  try {
4514
- const raw = JSON.parse(readFileSync4(path, "utf8"));
5134
+ const raw = JSON.parse(readFileSync5(path, "utf8"));
4515
5135
  if (!raw?.couch?.url)
4516
5136
  return null;
4517
5137
  return normalizeHookConfig(raw);
@@ -4622,14 +5242,14 @@ function makeCounts(sessionId) {
4622
5242
  const file = `/tmp/claude-transcripts-${sessionId}.counts`;
4623
5243
  const read = () => {
4624
5244
  try {
4625
- return JSON.parse(readFileSync4(file, "utf8"));
5245
+ return JSON.parse(readFileSync5(file, "utf8"));
4626
5246
  } catch {
4627
5247
  return emptyCounts();
4628
5248
  }
4629
5249
  };
4630
5250
  const write = (c) => {
4631
5251
  try {
4632
- writeFileSync2(file, JSON.stringify(c));
5252
+ writeFileSync3(file, JSON.stringify(c));
4633
5253
  } catch {}
4634
5254
  };
4635
5255
  return {
@@ -4659,14 +5279,14 @@ function makeTargets(sessionId) {
4659
5279
  const file = targetsFile(sessionId);
4660
5280
  const read = () => {
4661
5281
  try {
4662
- return JSON.parse(readFileSync4(file, "utf8"));
5282
+ return JSON.parse(readFileSync5(file, "utf8"));
4663
5283
  } catch {
4664
5284
  return null;
4665
5285
  }
4666
5286
  };
4667
5287
  const write = (t) => {
4668
5288
  try {
4669
- writeFileSync2(file, JSON.stringify(t));
5289
+ writeFileSync3(file, JSON.stringify(t));
4670
5290
  } catch {}
4671
5291
  };
4672
5292
  return {
@@ -4746,14 +5366,14 @@ function makeChunkState(sessionId) {
4746
5366
  const lockFile = `/tmp/claude-transcripts-${sessionId}.chunklock`;
4747
5367
  const load = () => {
4748
5368
  try {
4749
- return JSON.parse(readFileSync4(stateFile, "utf8"));
5369
+ return JSON.parse(readFileSync5(stateFile, "utf8"));
4750
5370
  } catch {
4751
5371
  return { offset: 0, lastFlushMs: 0 };
4752
5372
  }
4753
5373
  };
4754
5374
  const save = (s) => {
4755
5375
  try {
4756
- writeFileSync2(stateFile, JSON.stringify(s));
5376
+ writeFileSync3(stateFile, JSON.stringify(s));
4757
5377
  } catch {}
4758
5378
  };
4759
5379
  return {
@@ -4829,6 +5449,7 @@ function buildContext(payload, config) {
4829
5449
  const sessionsDb = config.couch.databases.sessions ?? DEFAULT_SESSIONS_DB;
4830
5450
  return {
4831
5451
  output: {},
5452
+ hookCompat: () => hookCompatLines(),
4832
5453
  chunkHighWater: () => fetchChunkHighWater(config, sessionsDb, sessionId),
4833
5454
  event,
4834
5455
  sessionId,
@@ -4872,10 +5493,12 @@ function recordingBanner(targets, sessionId) {
4872
5493
  }
4873
5494
  var NOT_RECORDING_BANNER = "\u25CB Claude Transcripts not recording \u2014 no instance configured. Run `claude-transcripts install`.";
4874
5495
  function sessionStartEnvelope(out) {
4875
- if (!out.systemMessage && !out.additionalContext)
5496
+ const systemMessage = [out.systemMessage, ...out.notices ?? []].filter(Boolean).join(`
5497
+ `);
5498
+ if (!systemMessage && !out.additionalContext)
4876
5499
  return null;
4877
5500
  return {
4878
- ...out.systemMessage ? { systemMessage: out.systemMessage } : {},
5501
+ ...systemMessage ? { systemMessage } : {},
4879
5502
  ...out.additionalContext ? {
4880
5503
  hookSpecificOutput: {
4881
5504
  hookEventName: "SessionStart",
@@ -4894,7 +5517,7 @@ function emitSessionStart(out) {
4894
5517
  }
4895
5518
 
4896
5519
  // packages/cli/src/hook/handlers.ts
4897
- import { readFileSync as readFileSync5, statSync as statSync3 } from "fs";
5520
+ import { readFileSync as readFileSync6, statSync as statSync3 } from "fs";
4898
5521
 
4899
5522
  // packages/cli/src/hook/recall.ts
4900
5523
  var FACTS_TIMEOUT_MS = 2000;
@@ -4984,6 +5607,13 @@ var announceRecording = async (ctx) => {
4984
5607
  const targets = ctx.targets.read() ?? resolveTargets(ctx.config, safeWebapiUrl());
4985
5608
  ctx.output.systemMessage = recordingBanner(targets, ctx.sessionId);
4986
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
+ };
4987
5617
  var injectRecallPolicy = async (ctx) => {
4988
5618
  const primer = await buildPrimer(ctx);
4989
5619
  if (primer)
@@ -5112,7 +5742,7 @@ var writeSummary = async (ctx) => {
5112
5742
  let transcriptBytes = 0;
5113
5743
  if (ctx.transcriptPath) {
5114
5744
  try {
5115
- jsonl = readFileSync5(ctx.transcriptPath, "utf8");
5745
+ jsonl = readFileSync6(ctx.transcriptPath, "utf8");
5116
5746
  transcriptBytes = Buffer.byteLength(jsonl);
5117
5747
  } catch {}
5118
5748
  }
@@ -5138,7 +5768,7 @@ var writeSummary = async (ctx) => {
5138
5768
  async function uploadTranscript(blob, bucket, sessionId, transcriptPath) {
5139
5769
  let data;
5140
5770
  try {
5141
- data = readFileSync5(transcriptPath, "utf8");
5771
+ data = readFileSync6(transcriptPath, "utf8");
5142
5772
  } catch {
5143
5773
  return;
5144
5774
  }
@@ -5154,6 +5784,7 @@ var uploadBlobs = async (ctx) => {
5154
5784
  var HANDLERS = {
5155
5785
  "seed-session-start": seedSessionStart,
5156
5786
  "announce-recording": announceRecording,
5787
+ "check-hook-compat": checkHookCompat,
5157
5788
  "inject-recall-policy": injectRecallPolicy,
5158
5789
  "update-counts": updateCounts,
5159
5790
  "write-event-marker": writeEventMarker,
@@ -5218,35 +5849,35 @@ async function dispatch(raw, configPath) {
5218
5849
  }
5219
5850
 
5220
5851
  // packages/cli/src/lib/plugin.ts
5221
- import { readFileSync as readFileSync6 } from "fs";
5222
- import { dirname, join as join6 } from "path";
5852
+ import { readFileSync as readFileSync7 } from "fs";
5853
+ import { dirname, join as join7 } from "path";
5223
5854
  var PLUGIN_NAME = "claude-transcripts";
5224
5855
  function readJson(path) {
5225
5856
  try {
5226
- const parsed = JSON.parse(readFileSync6(path, "utf8"));
5857
+ const parsed = JSON.parse(readFileSync7(path, "utf8"));
5227
5858
  return parsed && typeof parsed === "object" ? parsed : null;
5228
5859
  } catch {
5229
5860
  return null;
5230
5861
  }
5231
5862
  }
5232
5863
  function pluginsDir(settingsPath) {
5233
- return join6(dirname(settingsPath), "plugins");
5864
+ return join7(dirname(settingsPath), "plugins");
5234
5865
  }
5235
5866
  function pluginEvents(settingsPath, key) {
5236
- const installed = readJson(join6(pluginsDir(settingsPath), "installed_plugins.json"));
5867
+ const installed = readJson(join7(pluginsDir(settingsPath), "installed_plugins.json"));
5237
5868
  const entry = installed?.plugins?.[key];
5238
5869
  const first = Array.isArray(entry) ? entry[0] : undefined;
5239
5870
  const installPath = typeof first?.installPath === "string" ? first.installPath : undefined;
5240
5871
  if (!installPath)
5241
5872
  return;
5242
- const hooks = readJson(join6(installPath, "hooks", "hooks.json"));
5873
+ const hooks = readJson(join7(installPath, "hooks", "hooks.json"));
5243
5874
  const events = hooks?.hooks;
5244
5875
  if (!events || typeof events !== "object")
5245
5876
  return;
5246
5877
  return Object.keys(events);
5247
5878
  }
5248
5879
  function pluginVersion(settingsPath, key) {
5249
- const installed = readJson(join6(pluginsDir(settingsPath), "installed_plugins.json"));
5880
+ const installed = readJson(join7(pluginsDir(settingsPath), "installed_plugins.json"));
5250
5881
  const entry = installed?.plugins?.[key];
5251
5882
  const first = Array.isArray(entry) ? entry[0] : undefined;
5252
5883
  return typeof first?.version === "string" ? first.version : undefined;
@@ -5272,7 +5903,7 @@ function pluginRegistration(settingsPath, settings) {
5272
5903
  // packages/cli/src/commands/hook.ts
5273
5904
  function readSettings(path) {
5274
5905
  try {
5275
- return JSON.parse(readFileSync7(path, "utf8"));
5906
+ return JSON.parse(readFileSync8(path, "utf8"));
5276
5907
  } catch {
5277
5908
  return {};
5278
5909
  }
@@ -5369,8 +6000,8 @@ function install(argv) {
5369
6000
  console.log(`hook: would register ${added} event(s)` + `${replaced ? `, replacing ${replaced} stale one(s)` : ""}` + `${corrected ? `, correcting ${corrected} existing one(s)` : ""} in ${paths.claudeSettings}`);
5370
6001
  return 0;
5371
6002
  }
5372
- mkdirSync3(dirname2(paths.claudeSettings), { recursive: true });
5373
- 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)}
5374
6005
  `);
5375
6006
  const notes = [
5376
6007
  replaced ? `replaced ${replaced} stale registration(s)` : "",
@@ -5401,7 +6032,7 @@ function uninstall() {
5401
6032
  if (kept.length)
5402
6033
  hooks[event] = kept;
5403
6034
  }
5404
- writeFileSync3(paths.claudeSettings, `${JSON.stringify({ ...settings, hooks }, null, 2)}
6035
+ writeFileSync4(paths.claudeSettings, `${JSON.stringify({ ...settings, hooks }, null, 2)}
5405
6036
  `);
5406
6037
  console.log(`hook: removed ${removed} registration(s) from ${paths.claudeSettings}`);
5407
6038
  const plugin = pluginRegistration(paths.claudeSettings, settings);
@@ -5411,7 +6042,7 @@ function uninstall() {
5411
6042
  }
5412
6043
  function configuredMirrors(path) {
5413
6044
  try {
5414
- const parsed = JSON.parse(readFileSync7(path, "utf8"));
6045
+ const parsed = JSON.parse(readFileSync8(path, "utf8"));
5415
6046
  return (parsed.mirrors ?? []).map((m) => m?.url ?? "(no url)");
5416
6047
  } catch {
5417
6048
  return [];
@@ -5428,7 +6059,7 @@ function status() {
5428
6059
  const found = findRegistered(settings);
5429
6060
  const plugin = pluginRegistration(paths.claudeSettings, settings);
5430
6061
  console.log(`hook: settings ${paths.claudeSettings}`);
5431
- 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`)"}`);
5432
6063
  console.log(`hook: command ${hookCommand()}`);
5433
6064
  if (plugin)
5434
6065
  console.log(`hook: plugin ${pluginSummary(plugin)}`);
@@ -5502,8 +6133,8 @@ async function runHook(argv) {
5502
6133
  }
5503
6134
 
5504
6135
  // packages/cli/src/commands/import.ts
5505
- import { existsSync as existsSync4, statSync as statSync4 } from "fs";
5506
- import { join as join7 } from "path";
6136
+ import { existsSync as existsSync5, statSync as statSync4 } from "fs";
6137
+ import { join as join8 } from "path";
5507
6138
  var BATCH = 500;
5508
6139
  function num(n) {
5509
6140
  return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
@@ -5573,8 +6204,8 @@ function checkSchema(bundleVersion, target, registry = REAL_REGISTRY) {
5573
6204
  async function verifyChecksums(dir, manifest) {
5574
6205
  const bad = [];
5575
6206
  for (const [rel, expected] of Object.entries(manifest.checksums)) {
5576
- const path = join7(dir, rel);
5577
- if (!existsSync4(path)) {
6207
+ const path = join8(dir, rel);
6208
+ if (!existsSync5(path)) {
5578
6209
  bad.push(`${rel} (missing)`);
5579
6210
  continue;
5580
6211
  }
@@ -5707,7 +6338,7 @@ async function runImport(argv) {
5707
6338
  let blobs = 0;
5708
6339
  let blobBytes = 0;
5709
6340
  let skipped = 0;
5710
- if (withBlobs && existsSync4(paths.blobs)) {
6341
+ if (withBlobs && existsSync5(paths.blobs)) {
5711
6342
  for (const rel of Object.keys(manifest.checksums)) {
5712
6343
  if (!rel.startsWith("blobs/"))
5713
6344
  continue;
@@ -5718,7 +6349,7 @@ async function runImport(argv) {
5718
6349
  skipped++;
5719
6350
  continue;
5720
6351
  }
5721
- const path = join7(dir, rel);
6352
+ const path = join8(dir, rel);
5722
6353
  await putStream(`/api/ingest/${sessionId}/transcript`, path, "application/x-ndjson");
5723
6354
  blobs++;
5724
6355
  blobBytes += statSync4(path).size;
@@ -5742,58 +6373,58 @@ async function runImport(argv) {
5742
6373
  }
5743
6374
 
5744
6375
  // packages/cli/src/commands/install.ts
5745
- import { existsSync as existsSync8, readFileSync as readFileSync12 } from "fs";
5746
- import { join as join9 } from "path";
6376
+ import { existsSync as existsSync9, readFileSync as readFileSync13 } from "fs";
6377
+ import { join as join10 } from "path";
5747
6378
 
5748
6379
  // packages/cli/src/lib/assets.ts
5749
6380
  import {
5750
- existsSync as existsSync5,
6381
+ existsSync as existsSync6,
5751
6382
  lstatSync,
5752
- mkdirSync as mkdirSync4,
5753
- readFileSync as readFileSync8,
6383
+ mkdirSync as mkdirSync5,
6384
+ readFileSync as readFileSync9,
5754
6385
  symlinkSync,
5755
6386
  unlinkSync as unlinkSync2,
5756
- writeFileSync as writeFileSync4
6387
+ writeFileSync as writeFileSync5
5757
6388
  } from "fs";
5758
- import { dirname as dirname3, join as join8 } from "path";
6389
+ import { dirname as dirname3, join as join9 } from "path";
5759
6390
  function writeAssets(paths) {
5760
6391
  const out = [];
5761
6392
  for (const asset of Object.values(EMBEDDED_ASSETS)) {
5762
6393
  const isUserOwned = asset.path.startsWith("config/");
5763
- const target = isUserOwned ? paths.appConfig : join8(paths.dataDir, asset.path);
5764
- if (isUserOwned && existsSync5(target)) {
6394
+ const target = isUserOwned ? paths.appConfig : join9(paths.dataDir, asset.path);
6395
+ if (isUserOwned && existsSync6(target)) {
5765
6396
  out.push({ path: target, changed: false });
5766
6397
  continue;
5767
6398
  }
5768
- const previous = existsSync5(target) ? readFileSync8(target, "utf8") : null;
6399
+ const previous = existsSync6(target) ? readFileSync9(target, "utf8") : null;
5769
6400
  if (previous === asset.content) {
5770
6401
  out.push({ path: target, changed: false });
5771
6402
  continue;
5772
6403
  }
5773
- mkdirSync4(dirname3(target), { recursive: true });
5774
- writeFileSync4(target, asset.content);
6404
+ mkdirSync5(dirname3(target), { recursive: true });
6405
+ writeFileSync5(target, asset.content);
5775
6406
  out.push({ path: target, changed: true });
5776
6407
  }
5777
6408
  return out;
5778
6409
  }
5779
6410
  function linkInstanceEnv(paths) {
5780
- const link = join8(paths.dataDir, ".env");
6411
+ const link = join9(paths.dataDir, ".env");
5781
6412
  try {
5782
6413
  if (lstatSync(link, { throwIfNoEntry: false }))
5783
6414
  unlinkSync2(link);
5784
6415
  } catch {}
5785
- mkdirSync4(paths.dataDir, { recursive: true });
6416
+ mkdirSync5(paths.dataDir, { recursive: true });
5786
6417
  symlinkSync(paths.instanceEnv, link);
5787
6418
  }
5788
6419
  function writeVersionStamp(paths, version) {
5789
- mkdirSync4(dirname3(paths.versionFile), { recursive: true });
5790
- writeFileSync4(paths.versionFile, `${version}
6420
+ mkdirSync5(dirname3(paths.versionFile), { recursive: true });
6421
+ writeFileSync5(paths.versionFile, `${version}
5791
6422
  `);
5792
6423
  }
5793
6424
  function composeFiles(paths, upstream) {
5794
- const files = [join8(paths.dataDir, EMBEDDED_ASSETS.compose.path)];
6425
+ const files = [join9(paths.dataDir, EMBEDDED_ASSETS.compose.path)];
5795
6426
  if (upstream)
5796
- files.push(join8(paths.dataDir, EMBEDDED_ASSETS.composeUpstream.path));
6427
+ files.push(join9(paths.dataDir, EMBEDDED_ASSETS.composeUpstream.path));
5797
6428
  return files;
5798
6429
  }
5799
6430
 
@@ -5861,7 +6492,7 @@ function healthTargets(env) {
5861
6492
  }
5862
6493
 
5863
6494
  // packages/cli/src/lib/hook-config.ts
5864
- 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";
5865
6496
  import { dirname as dirname4 } from "path";
5866
6497
  function buildHookConfig(app, env) {
5867
6498
  const user = env.COUCHDB_USER;
@@ -5895,7 +6526,7 @@ function webapiFromEnv(env) {
5895
6526
  var PRESERVED_KEYS = ["mirrors", "webapi"];
5896
6527
  function readExisting(path) {
5897
6528
  try {
5898
- const parsed = JSON.parse(readFileSync9(path, "utf8"));
6529
+ const parsed = JSON.parse(readFileSync10(path, "utf8"));
5899
6530
  return parsed && typeof parsed === "object" ? parsed : {};
5900
6531
  } catch {
5901
6532
  return {};
@@ -5911,8 +6542,8 @@ function mergePreserved(next, existing) {
5911
6542
  }
5912
6543
  function writeHookConfig(path, config) {
5913
6544
  const merged = mergePreserved(config ?? {}, readExisting(path));
5914
- mkdirSync5(dirname4(path), { recursive: true });
5915
- writeFileSync5(path, `${JSON.stringify(merged, null, 2)}
6545
+ mkdirSync6(dirname4(path), { recursive: true });
6546
+ writeFileSync6(path, `${JSON.stringify(merged, null, 2)}
5916
6547
  `);
5917
6548
  chmodSync2(path, 384);
5918
6549
  }
@@ -5989,7 +6620,7 @@ function renderPruneReport(report, repo) {
5989
6620
 
5990
6621
  // packages/cli/src/lib/instance-env.ts
5991
6622
  import { randomBytes } from "crypto";
5992
- 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";
5993
6624
  import { dirname as dirname5 } from "path";
5994
6625
  var DEFAULT_PORT_BASE = 7650;
5995
6626
  var PORT_KEYS = [
@@ -6089,7 +6720,7 @@ function backfillPorts(existing) {
6089
6720
  }
6090
6721
  function loadOrCreateInstanceEnv(path, opts = {}) {
6091
6722
  const generated = buildInstanceEnv(opts);
6092
- const existing = existsSync6(path) ? parseEnv(readFileSync10(path, "utf8")) : {};
6723
+ const existing = existsSync7(path) ? parseEnv(readFileSync11(path, "utf8")) : {};
6093
6724
  const merged = { ...generated, ...backfillPorts(existing), ...existing };
6094
6725
  for (const key of INSTALLER_OWNED) {
6095
6726
  if (generated[key] !== undefined)
@@ -6103,12 +6734,12 @@ function loadOrCreateInstanceEnv(path, opts = {}) {
6103
6734
  return merged;
6104
6735
  }
6105
6736
  function writeInstanceEnv(path, env) {
6106
- mkdirSync6(dirname5(path), { recursive: true });
6107
- writeFileSync6(path, formatEnv(env, HEADER));
6737
+ mkdirSync7(dirname5(path), { recursive: true });
6738
+ writeFileSync7(path, formatEnv(env, HEADER));
6108
6739
  chmodSync3(path, 384);
6109
6740
  }
6110
6741
  function updateInstanceEnv(path, patch) {
6111
- const env = existsSync6(path) ? parseEnv(readFileSync10(path, "utf8")) : {};
6742
+ const env = existsSync7(path) ? parseEnv(readFileSync11(path, "utf8")) : {};
6112
6743
  const next = { ...env, ...patch };
6113
6744
  writeInstanceEnv(path, next);
6114
6745
  return next;
@@ -6390,11 +7021,11 @@ async function removeStaleKeys(api, keep) {
6390
7021
  }
6391
7022
 
6392
7023
  // packages/cli/src/commands/statusline.ts
6393
- 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";
6394
7025
  import { dirname as dirname6 } from "path";
6395
7026
  function readSettings2(path) {
6396
7027
  try {
6397
- return JSON.parse(readFileSync11(path, "utf8"));
7028
+ return JSON.parse(readFileSync12(path, "utf8"));
6398
7029
  } catch {
6399
7030
  return {};
6400
7031
  }
@@ -6412,7 +7043,7 @@ async function render() {
6412
7043
  try {
6413
7044
  input = JSON.parse(await Bun.stdin.text());
6414
7045
  } catch {}
6415
- const configured = existsSync7(installPaths().hookConfig);
7046
+ const configured = existsSync8(installPaths().hookConfig);
6416
7047
  const id = input.session_id;
6417
7048
  const targets = id ? makeTargets(id).read() : null;
6418
7049
  const line = renderStatusline({
@@ -6457,8 +7088,8 @@ function install2(argv) {
6457
7088
  console.log(` ${JSON.stringify(next)}`);
6458
7089
  return 0;
6459
7090
  }
6460
- mkdirSync7(dirname6(paths.claudeSettings), { recursive: true });
6461
- 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)}
6462
7093
  `);
6463
7094
  console.log(`statusline: registered \u2192 ${paths.claudeSettings}`);
6464
7095
  console.log("statusline: restart any open Claude Code session for it to take effect.");
@@ -6476,7 +7107,7 @@ function uninstall2() {
6476
7107
  return 1;
6477
7108
  }
6478
7109
  const { statusLine: _drop, ...rest } = settings;
6479
- writeFileSync7(paths.claudeSettings, `${JSON.stringify(rest, null, 2)}
7110
+ writeFileSync8(paths.claudeSettings, `${JSON.stringify(rest, null, 2)}
6480
7111
  `);
6481
7112
  console.log(`statusline: removed from ${paths.claudeSettings}`);
6482
7113
  return 0;
@@ -6600,7 +7231,7 @@ async function runInstall(argv) {
6600
7231
  const noPrune = options["no-prune"] === true;
6601
7232
  const portBaseOpt = strOpt(options, "port-base");
6602
7233
  const paths = installPaths();
6603
- 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;
6604
7235
  const TOTAL = 8;
6605
7236
  console.log(`claude-transcripts install (${VERSION})`);
6606
7237
  console.log(` config ${paths.configDir}`);
@@ -6628,8 +7259,8 @@ async function runInstall(argv) {
6628
7259
  portBase = found;
6629
7260
  }
6630
7261
  step(2, TOTAL, "Configuration");
6631
- const firstRun = !existsSync8(paths.instanceEnv);
6632
- 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"))));
6633
7264
  let env = loadOrCreateInstanceEnv(paths.instanceEnv, {
6634
7265
  portBase,
6635
7266
  appTag: VERSION === DEV_VERSION ? "main" : VERSION,
@@ -6733,7 +7364,7 @@ async function runInstall(argv) {
6733
7364
  install: done.`);
6734
7365
  console.log(` UI http://127.0.0.1:${env.WEBAPI_PORT}/app`);
6735
7366
  console.log(` API http://127.0.0.1:${env.WEBAPI_PORT}/api/docs`);
6736
- console.log(` data ${join9(paths.deployDir, "data")} (survives uninstall unless --purge)`);
7367
+ console.log(` data ${join10(paths.deployDir, "data")} (survives uninstall unless --purge)`);
6737
7368
  console.log(" remove claude-transcripts uninstall");
6738
7369
  return 0;
6739
7370
  }
@@ -6793,10 +7424,10 @@ async function runMigrate(argv) {
6793
7424
  }
6794
7425
 
6795
7426
  // packages/cli/src/commands/provision.ts
6796
- import { existsSync as existsSync9 } from "fs";
7427
+ import { existsSync as existsSync10 } from "fs";
6797
7428
  async function runProvision(_argv) {
6798
7429
  const paths = installPaths();
6799
- if (!existsSync9(paths.instanceEnv)) {
7430
+ if (!existsSync10(paths.instanceEnv)) {
6800
7431
  console.error("provision: this machine has no instance \u2014 run `claude-transcripts install` first.");
6801
7432
  return 1;
6802
7433
  }
@@ -6827,6 +7458,112 @@ async function runProvision(_argv) {
6827
7458
  return ok ? 0 : 1;
6828
7459
  }
6829
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
+
6830
7567
  // packages/cli/src/commands/reindex.ts
6831
7568
  function num2(n) {
6832
7569
  return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
@@ -7145,18 +7882,18 @@ async function runSessions(argv) {
7145
7882
  }
7146
7883
 
7147
7884
  // packages/cli/src/commands/setup.ts
7148
- 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";
7149
7886
  import { homedir as homedir3 } from "os";
7150
- import { dirname as dirname7, join as join10 } from "path";
7151
- var REPO_ROOT = join10(import.meta.dir, "..", "..", "..", "..");
7152
- var HOOK_DIR = join10(REPO_ROOT, "hooks");
7153
- 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");
7154
7891
  var GLOBAL_SETTINGS = installPaths().claudeSettings;
7155
- var PROJECT_SETTINGS = join10(process.cwd(), ".claude", "settings.json");
7892
+ var PROJECT_SETTINGS = join11(process.cwd(), ".claude", "settings.json");
7156
7893
  function loadRepoConfig() {
7157
- const live = join10(REPO_ROOT, "config", "config.json");
7158
- const template = join10(REPO_ROOT, "config", "config.template.json");
7159
- 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"));
7160
7897
  }
7161
7898
  function couchHeaders(auth) {
7162
7899
  const h = { "Content-Type": "application/json" };
@@ -7206,7 +7943,7 @@ async function probeGarage(blob) {
7206
7943
  }
7207
7944
  function readSettings3(path) {
7208
7945
  try {
7209
- const parsed = JSON.parse(readFileSync13(path, "utf8"));
7946
+ const parsed = JSON.parse(readFileSync14(path, "utf8"));
7210
7947
  return parsed && typeof parsed === "object" ? parsed : {};
7211
7948
  } catch {
7212
7949
  return {};
@@ -7215,7 +7952,7 @@ function readSettings3(path) {
7215
7952
  function globalRegistrationBlockedBy(settingsPath = GLOBAL_SETTINGS) {
7216
7953
  return pluginRegistration(settingsPath, readSettings3(settingsPath));
7217
7954
  }
7218
- var DISPATCH_PATH = join10(HOOK_DIR, "scripts", "dispatch.ts");
7955
+ var DISPATCH_PATH = join11(HOOK_DIR, "scripts", "dispatch.ts");
7219
7956
  function dispatchCommand() {
7220
7957
  return hookLaunch(`bun run "${DISPATCH_PATH}"`);
7221
7958
  }
@@ -7226,7 +7963,7 @@ function isRegistered(path) {
7226
7963
  }
7227
7964
  function registerGlobal(dryRun) {
7228
7965
  const cmd = dispatchCommand();
7229
- 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"));
7230
7967
  const settings = readSettings3(GLOBAL_SETTINGS);
7231
7968
  const hooks = settings.hooks ?? {};
7232
7969
  for (const [event, groups] of Object.entries(generated.hooks)) {
@@ -7246,8 +7983,8 @@ function registerGlobal(dryRun) {
7246
7983
  console.log(` [check] would register ${Object.keys(generated.hooks).length} events in ${GLOBAL_SETTINGS}`);
7247
7984
  return;
7248
7985
  }
7249
- mkdirSync8(dirname7(GLOBAL_SETTINGS), { recursive: true });
7250
- 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)}
7251
7988
  `);
7252
7989
  console.log(` registered ${Object.keys(generated.hooks).length} events (system-wide) in ${GLOBAL_SETTINGS}`);
7253
7990
  }
@@ -7256,7 +7993,7 @@ function registerProject() {
7256
7993
  }
7257
7994
  function reportExisting() {
7258
7995
  const g = isRegistered(GLOBAL_SETTINGS);
7259
- const p = existsSync10(PROJECT_SETTINGS) && isRegistered(PROJECT_SETTINGS);
7996
+ const p = existsSync11(PROJECT_SETTINGS) && isRegistered(PROJECT_SETTINGS);
7260
7997
  console.log(` existing registration \u2192 global: ${g ? "yes" : "no"}, this project: ${p ? "yes" : "no"}`);
7261
7998
  const plugin = globalRegistrationBlockedBy();
7262
7999
  if (plugin) {
@@ -7292,7 +8029,7 @@ async function runSetup(argv) {
7292
8029
  reportExisting();
7293
8030
  if (check) {
7294
8031
  console.log("setup --check (read-only):");
7295
- 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})`);
7296
8033
  console.log(` couch: ${couch.url}`);
7297
8034
  await provisionCouch2(couch, true);
7298
8035
  await probeGarage(hookConfig.blob);
@@ -7323,12 +8060,12 @@ async function runSetup(argv) {
7323
8060
  }
7324
8061
 
7325
8062
  // packages/cli/src/commands/stack.ts
7326
- import { existsSync as existsSync11 } from "fs";
8063
+ import { existsSync as existsSync12 } from "fs";
7327
8064
  async function runStack(argv) {
7328
8065
  const { positionals, options } = parseFlags(argv);
7329
8066
  const cmd = positionals[0] ?? "ps";
7330
8067
  const paths = installPaths();
7331
- if (!existsSync11(paths.instanceEnv)) {
8068
+ if (!existsSync12(paths.instanceEnv)) {
7332
8069
  console.error("stack: this machine has no instance \u2014 run `claude-transcripts install` first.");
7333
8070
  return 1;
7334
8071
  }
@@ -7466,8 +8203,8 @@ async function runTurns(argv) {
7466
8203
  }
7467
8204
 
7468
8205
  // packages/cli/src/commands/uninstall.ts
7469
- import { existsSync as existsSync12, readdirSync, rmSync } from "fs";
7470
- import { join as join11 } from "path";
8206
+ import { existsSync as existsSync13, readdirSync, rmSync } from "fs";
8207
+ import { join as join12 } from "path";
7471
8208
  var DATA_SUBDIR = "data";
7472
8209
  function removeDeployDir(deployDir, purge) {
7473
8210
  if (purge) {
@@ -7482,7 +8219,7 @@ function removeDeployDir(deployDir, purge) {
7482
8219
  }
7483
8220
  for (const entry of entries) {
7484
8221
  if (entry !== DATA_SUBDIR)
7485
- rmSync(join11(deployDir, entry), { recursive: true, force: true });
8222
+ rmSync(join12(deployDir, entry), { recursive: true, force: true });
7486
8223
  }
7487
8224
  }
7488
8225
  async function confirmPurge(auto) {
@@ -7505,7 +8242,7 @@ async function runUninstall(argv) {
7505
8242
  console.log("uninstall: deregistering the hook and the statusline");
7506
8243
  await runHook(["uninstall"]);
7507
8244
  await runStatusline(["uninstall"]);
7508
- if (existsSync12(paths.instanceEnv)) {
8245
+ if (existsSync13(paths.instanceEnv)) {
7509
8246
  console.log("uninstall: stopping the stack");
7510
8247
  try {
7511
8248
  const env = loadOrCreateInstanceEnv(paths.instanceEnv);
@@ -7520,7 +8257,7 @@ async function runUninstall(argv) {
7520
8257
  rmSync(target, { recursive: true, force: true });
7521
8258
  } catch {}
7522
8259
  }
7523
- const dataDir = join11(paths.deployDir, DATA_SUBDIR);
8260
+ const dataDir = join12(paths.deployDir, DATA_SUBDIR);
7524
8261
  try {
7525
8262
  removeDeployDir(paths.deployDir, purge);
7526
8263
  } catch (err) {
@@ -7557,6 +8294,8 @@ var COMMANDS = {
7557
8294
  migrate: runMigrate,
7558
8295
  reindex: runReindex,
7559
8296
  doctor: runDoctor,
8297
+ reconcile: runReconcile,
8298
+ compat: runCompat,
7560
8299
  sessions: runSessions,
7561
8300
  search: runSearch,
7562
8301
  turns: runTurns,