@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.
- package/README.md +2 -0
- package/dist/{app-r8h0jf83.js → app-v7rh39yp.js} +1 -1
- package/dist/{cli-wv3x4a04.js → cli-3dqna555.js} +865 -126
- package/dist/cli.js +2 -2
- package/package.json +1 -1
|
@@ -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: "
|
|
56
|
-
implemented:
|
|
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
|
|
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
|
|
4068
|
+
import { mkdirSync as mkdirSync3 } from "fs";
|
|
3473
4069
|
import { hostname as hostname3 } from "os";
|
|
3474
|
-
import { join as
|
|
4070
|
+
import { join as join6 } from "path";
|
|
3475
4071
|
|
|
3476
4072
|
// packages/cli/src/lib/app-config.ts
|
|
3477
|
-
import { existsSync, readFileSync as
|
|
3478
|
-
import { join as
|
|
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": {
|
|
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 =
|
|
4519
|
+
const root = join4(import.meta.dir, "..", "..", "..", "..");
|
|
3920
4520
|
for (const name of ["config.json", "config.template.json"]) {
|
|
3921
|
-
const p =
|
|
3922
|
-
if (
|
|
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 (
|
|
3933
|
-
return JSON.parse(
|
|
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
|
|
3941
|
-
import { join as
|
|
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:
|
|
3959
|
-
docs:
|
|
3960
|
-
blobs:
|
|
3961
|
-
blobDir: (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
|
-
|
|
3966
|
-
|
|
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
|
-
|
|
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 (!
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
4133
|
-
const target =
|
|
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
|
|
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
|
|
4955
|
+
readFileSync as readFileSync5,
|
|
4356
4956
|
readSync,
|
|
4357
4957
|
statSync as statSync2,
|
|
4358
4958
|
unlinkSync,
|
|
4359
|
-
writeFileSync as
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
5496
|
+
const systemMessage = [out.systemMessage, ...out.notices ?? []].filter(Boolean).join(`
|
|
5497
|
+
`);
|
|
5498
|
+
if (!systemMessage && !out.additionalContext)
|
|
4876
5499
|
return null;
|
|
4877
5500
|
return {
|
|
4878
|
-
...
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
5222
|
-
import { dirname, join as
|
|
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(
|
|
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
|
|
5864
|
+
return join7(dirname(settingsPath), "plugins");
|
|
5234
5865
|
}
|
|
5235
5866
|
function pluginEvents(settingsPath, key) {
|
|
5236
|
-
const installed = readJson(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
5373
|
-
|
|
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
|
-
|
|
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(
|
|
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} ${
|
|
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
|
|
5506
|
-
import { join as
|
|
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 =
|
|
5577
|
-
if (!
|
|
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 &&
|
|
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 =
|
|
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
|
|
5746
|
-
import { join as
|
|
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
|
|
6381
|
+
existsSync as existsSync6,
|
|
5751
6382
|
lstatSync,
|
|
5752
|
-
mkdirSync as
|
|
5753
|
-
readFileSync as
|
|
6383
|
+
mkdirSync as mkdirSync5,
|
|
6384
|
+
readFileSync as readFileSync9,
|
|
5754
6385
|
symlinkSync,
|
|
5755
6386
|
unlinkSync as unlinkSync2,
|
|
5756
|
-
writeFileSync as
|
|
6387
|
+
writeFileSync as writeFileSync5
|
|
5757
6388
|
} from "fs";
|
|
5758
|
-
import { dirname as dirname3, join as
|
|
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 :
|
|
5764
|
-
if (isUserOwned &&
|
|
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 =
|
|
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
|
-
|
|
5774
|
-
|
|
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 =
|
|
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
|
-
|
|
6416
|
+
mkdirSync5(paths.dataDir, { recursive: true });
|
|
5786
6417
|
symlinkSync(paths.instanceEnv, link);
|
|
5787
6418
|
}
|
|
5788
6419
|
function writeVersionStamp(paths, version) {
|
|
5789
|
-
|
|
5790
|
-
|
|
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 = [
|
|
6425
|
+
const files = [join9(paths.dataDir, EMBEDDED_ASSETS.compose.path)];
|
|
5795
6426
|
if (upstream)
|
|
5796
|
-
files.push(
|
|
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
|
|
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(
|
|
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
|
-
|
|
5915
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
6107
|
-
|
|
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 =
|
|
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
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
6461
|
-
|
|
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
|
-
|
|
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 =
|
|
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 = !
|
|
6632
|
-
const priorKeys = new Set(firstRun ? [] : Object.keys(parseEnv(
|
|
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 ${
|
|
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
|
|
7427
|
+
import { existsSync as existsSync10 } from "fs";
|
|
6797
7428
|
async function runProvision(_argv) {
|
|
6798
7429
|
const paths = installPaths();
|
|
6799
|
-
if (!
|
|
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
|
|
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
|
|
7151
|
-
var REPO_ROOT =
|
|
7152
|
-
var HOOK_DIR =
|
|
7153
|
-
var HOOK_CONFIG_PATH = process.env.CT_HOOK_CONFIG ??
|
|
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 =
|
|
7892
|
+
var PROJECT_SETTINGS = join11(process.cwd(), ".claude", "settings.json");
|
|
7156
7893
|
function loadRepoConfig() {
|
|
7157
|
-
const live =
|
|
7158
|
-
const template =
|
|
7159
|
-
return JSON.parse(
|
|
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(
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
7250
|
-
|
|
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 =
|
|
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: ${
|
|
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
|
|
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 (!
|
|
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
|
|
7470
|
-
import { join as
|
|
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(
|
|
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 (
|
|
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 =
|
|
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,
|