@claude-transcripts/cli 0.0.14 → 0.0.16
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/cli.js +720 -45
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -90,6 +90,7 @@ upgrades in place: it's idempotent and keeps your history.
|
|
|
90
90
|
|---|---|
|
|
91
91
|
| `sessions [id] [options]` | List / inspect sessions (via the webapi) |
|
|
92
92
|
| `search <query> [options]` | Search the corpus |
|
|
93
|
+
| `turns [session] [options]` | Speaker-split turns: one session, or one speaker across all sessions |
|
|
93
94
|
| `backfill [options]` | Adopt on-disk ~/.claude transcripts as first-class history |
|
|
94
95
|
|
|
95
96
|
**Portability**
|
|
@@ -107,6 +108,7 @@ upgrades in place: it's idempotent and keeps your history.
|
|
|
107
108
|
| `reindex` | Rebuild the search indexes from CouchDB |
|
|
108
109
|
| `doctor [options]` | Smoke-test the write/read/search path end-to-end |
|
|
109
110
|
| `hook [action] [options]` | The Claude Code hook, and its registration |
|
|
111
|
+
| `statusline [action] [options]` | The Claude Code statusline indicator (recording / off), and its registration |
|
|
110
112
|
|
|
111
113
|
**Global options** (every command)
|
|
112
114
|
|
package/dist/cli.js
CHANGED
|
@@ -29659,6 +29659,16 @@ var ACTIONS = [
|
|
|
29659
29659
|
summary: "Reset counters + seed the chunk offset at start",
|
|
29660
29660
|
implemented: false
|
|
29661
29661
|
},
|
|
29662
|
+
{
|
|
29663
|
+
key: "announce-recording",
|
|
29664
|
+
summary: "Tell the user, in the transcript, whether and where this session is recorded",
|
|
29665
|
+
implemented: false
|
|
29666
|
+
},
|
|
29667
|
+
{
|
|
29668
|
+
key: "inject-recall-policy",
|
|
29669
|
+
summary: "Prime the session with the recall policy + how much history this cwd has",
|
|
29670
|
+
implemented: false
|
|
29671
|
+
},
|
|
29662
29672
|
{ key: "enrich-metadata", summary: "Post additional session metadata", implemented: false },
|
|
29663
29673
|
{
|
|
29664
29674
|
key: "extract-feature",
|
|
@@ -29679,7 +29689,15 @@ var ACTIONS = [
|
|
|
29679
29689
|
}
|
|
29680
29690
|
];
|
|
29681
29691
|
var BINDINGS = [
|
|
29682
|
-
{
|
|
29692
|
+
{
|
|
29693
|
+
event: "SessionStart",
|
|
29694
|
+
actions: [
|
|
29695
|
+
"seed-session-start",
|
|
29696
|
+
"write-event-marker",
|
|
29697
|
+
"announce-recording",
|
|
29698
|
+
"inject-recall-policy"
|
|
29699
|
+
]
|
|
29700
|
+
},
|
|
29683
29701
|
{
|
|
29684
29702
|
event: "UserPromptSubmit",
|
|
29685
29703
|
actions: ["write-event-marker", "update-counts", "flush-transcript-chunk"]
|
|
@@ -29706,6 +29724,10 @@ var WEBAPI = {
|
|
|
29706
29724
|
type: "string",
|
|
29707
29725
|
description: "webapi base URL (default: $CT_WEBAPI_URL)"
|
|
29708
29726
|
};
|
|
29727
|
+
var JSON_OUT = {
|
|
29728
|
+
name: "--json",
|
|
29729
|
+
description: "print the webapi response as JSON instead of a table"
|
|
29730
|
+
};
|
|
29709
29731
|
var CLI_SPEC = {
|
|
29710
29732
|
groups: [
|
|
29711
29733
|
{ key: "lifecycle", title: "Lifecycle" },
|
|
@@ -29732,6 +29754,7 @@ var CLI_SPEC = {
|
|
|
29732
29754
|
},
|
|
29733
29755
|
{ name: "--meili-key", description: "generate a Meilisearch master key" },
|
|
29734
29756
|
{ name: "--no-hook", description: "skip Claude Code registration" },
|
|
29757
|
+
{ name: "--no-statusline", description: "register the hook but not the statusline" },
|
|
29735
29758
|
{ name: "--no-app", description: "skip the app container (run the webapi yourself)" },
|
|
29736
29759
|
{ name: "--no-prune", description: "keep superseded app images" },
|
|
29737
29760
|
{ name: "--skip-preflight", description: "continue past failed preflight checks" },
|
|
@@ -29792,9 +29815,10 @@ var CLI_SPEC = {
|
|
|
29792
29815
|
name: "--limit",
|
|
29793
29816
|
type: "number",
|
|
29794
29817
|
description: "rows to list (default 50) / transcript entries to preview (default 30)"
|
|
29795
|
-
}
|
|
29818
|
+
},
|
|
29819
|
+
JSON_OUT
|
|
29796
29820
|
],
|
|
29797
|
-
examples: ["sessions", "sessions --limit 10", "sessions 3f9a2c1e --limit 80"]
|
|
29821
|
+
examples: ["sessions", "sessions --limit 10", "sessions 3f9a2c1e --limit 80 --json"]
|
|
29798
29822
|
},
|
|
29799
29823
|
{
|
|
29800
29824
|
name: "search",
|
|
@@ -29815,9 +29839,42 @@ var CLI_SPEC = {
|
|
|
29815
29839
|
name: "--source",
|
|
29816
29840
|
type: "string",
|
|
29817
29841
|
description: "only this provenance (live | backfill | \u2026)"
|
|
29818
|
-
}
|
|
29842
|
+
},
|
|
29843
|
+
JSON_OUT
|
|
29819
29844
|
],
|
|
29820
|
-
examples: ['search "retry policy"', "search deploy --cwd ~/proj --limit 5"]
|
|
29845
|
+
examples: ['search "retry policy"', "search deploy --cwd ~/proj --limit 5 --json"]
|
|
29846
|
+
},
|
|
29847
|
+
{
|
|
29848
|
+
name: "turns",
|
|
29849
|
+
group: "daily",
|
|
29850
|
+
summary: "Speaker-split turns: one session, or one speaker across all sessions",
|
|
29851
|
+
args: [
|
|
29852
|
+
{ name: "session", description: "session id \u2014 that session's turns (omit: all sessions)" },
|
|
29853
|
+
{
|
|
29854
|
+
name: "--role",
|
|
29855
|
+
type: "string",
|
|
29856
|
+
choices: ["user", "assistant", "tool_result", "system", "other"],
|
|
29857
|
+
description: "only this speaker"
|
|
29858
|
+
},
|
|
29859
|
+
{
|
|
29860
|
+
name: "--from",
|
|
29861
|
+
type: "string",
|
|
29862
|
+
description: "all sessions: only turns at/after this ISO instant"
|
|
29863
|
+
},
|
|
29864
|
+
{
|
|
29865
|
+
name: "--to",
|
|
29866
|
+
type: "string",
|
|
29867
|
+
description: "all sessions: only turns at/before this ISO instant"
|
|
29868
|
+
},
|
|
29869
|
+
{ name: "--limit", type: "number", default: 50, description: "turns to show" },
|
|
29870
|
+
{ name: "--skip", type: "number", description: "skip this many (paging)" },
|
|
29871
|
+
JSON_OUT
|
|
29872
|
+
],
|
|
29873
|
+
examples: [
|
|
29874
|
+
"turns --role user --limit 200",
|
|
29875
|
+
"turns --role user --from 2026-08-01 --json",
|
|
29876
|
+
"turns 3f9a2c1e --role assistant"
|
|
29877
|
+
]
|
|
29821
29878
|
},
|
|
29822
29879
|
{
|
|
29823
29880
|
name: "backfill",
|
|
@@ -29924,6 +29981,21 @@ var CLI_SPEC = {
|
|
|
29924
29981
|
{ name: "--dry-run", description: "with `install`: show the change without writing" }
|
|
29925
29982
|
],
|
|
29926
29983
|
examples: ["hook status", "hook install --dry-run"]
|
|
29984
|
+
},
|
|
29985
|
+
{
|
|
29986
|
+
name: "statusline",
|
|
29987
|
+
group: "admin",
|
|
29988
|
+
summary: "The Claude Code statusline indicator (recording / off), and its registration",
|
|
29989
|
+
args: [
|
|
29990
|
+
{
|
|
29991
|
+
name: "action",
|
|
29992
|
+
choices: ["render", "install", "uninstall", "status"],
|
|
29993
|
+
default: "status",
|
|
29994
|
+
description: "`render` reads Claude Code's statusline JSON from stdin and prints one line (no network)"
|
|
29995
|
+
},
|
|
29996
|
+
{ name: "--dry-run", description: "with `install`: show the change without writing" }
|
|
29997
|
+
],
|
|
29998
|
+
examples: ["statusline install", "statusline status"]
|
|
29927
29999
|
}
|
|
29928
30000
|
]
|
|
29929
30001
|
};
|
|
@@ -30248,6 +30320,97 @@ var HOOK_TYPES = [
|
|
|
30248
30320
|
}
|
|
30249
30321
|
];
|
|
30250
30322
|
|
|
30323
|
+
// packages/shared/src/model/recall.ts
|
|
30324
|
+
var RECALL_MODES = ["off", "suggest", "auto"];
|
|
30325
|
+
var RECALL_SCOPES = ["project", "host", "all"];
|
|
30326
|
+
var DEFAULT_RECALL = {
|
|
30327
|
+
mode: "auto",
|
|
30328
|
+
scope: "project",
|
|
30329
|
+
maxResults: 5,
|
|
30330
|
+
maxSnippetChars: 400,
|
|
30331
|
+
triggers: { priorWorkQuestion: true, repeatedError: true, beforeRederiving: true },
|
|
30332
|
+
excludeCwdGlobs: [],
|
|
30333
|
+
primer: { onSessionStart: true, maxTokens: 200 }
|
|
30334
|
+
};
|
|
30335
|
+
var RECALL_ENV = {
|
|
30336
|
+
mode: "CLAUDE_PLUGIN_OPTION_RECALL_MODE",
|
|
30337
|
+
scope: "CLAUDE_PLUGIN_OPTION_RECALL_SCOPE",
|
|
30338
|
+
maxResults: "CLAUDE_PLUGIN_OPTION_MAX_RESULTS"
|
|
30339
|
+
};
|
|
30340
|
+
function resolveRecall(fromConfig, env = {}) {
|
|
30341
|
+
const c = fromConfig ?? {};
|
|
30342
|
+
const merged = {
|
|
30343
|
+
...DEFAULT_RECALL,
|
|
30344
|
+
...stripUndefined(c),
|
|
30345
|
+
triggers: { ...DEFAULT_RECALL.triggers, ...stripUndefined(c.triggers ?? {}) },
|
|
30346
|
+
primer: { ...DEFAULT_RECALL.primer, ...stripUndefined(c.primer ?? {}) },
|
|
30347
|
+
excludeCwdGlobs: c.excludeCwdGlobs ?? DEFAULT_RECALL.excludeCwdGlobs
|
|
30348
|
+
};
|
|
30349
|
+
const mode = env[RECALL_ENV.mode];
|
|
30350
|
+
if (mode && RECALL_MODES.includes(mode))
|
|
30351
|
+
merged.mode = mode;
|
|
30352
|
+
const scope = env[RECALL_ENV.scope];
|
|
30353
|
+
if (scope && RECALL_SCOPES.includes(scope))
|
|
30354
|
+
merged.scope = scope;
|
|
30355
|
+
const max = Number(env[RECALL_ENV.maxResults]);
|
|
30356
|
+
if (Number.isInteger(max) && max > 0)
|
|
30357
|
+
merged.maxResults = max;
|
|
30358
|
+
return merged;
|
|
30359
|
+
}
|
|
30360
|
+
function stripUndefined(o) {
|
|
30361
|
+
const out = {};
|
|
30362
|
+
for (const [k, v] of Object.entries(o))
|
|
30363
|
+
if (v !== undefined)
|
|
30364
|
+
out[k] = v;
|
|
30365
|
+
return out;
|
|
30366
|
+
}
|
|
30367
|
+
function scopeFlag(scope, cwd, hostname) {
|
|
30368
|
+
switch (scope) {
|
|
30369
|
+
case "project":
|
|
30370
|
+
return `--cwd "${cwd}"`;
|
|
30371
|
+
case "host":
|
|
30372
|
+
return `--hostname "${hostname}"`;
|
|
30373
|
+
default:
|
|
30374
|
+
return "";
|
|
30375
|
+
}
|
|
30376
|
+
}
|
|
30377
|
+
function daysAgo(iso, now) {
|
|
30378
|
+
if (!iso)
|
|
30379
|
+
return "";
|
|
30380
|
+
const d = Math.floor((now - Date.parse(iso)) / 86400000);
|
|
30381
|
+
if (!Number.isFinite(d))
|
|
30382
|
+
return "";
|
|
30383
|
+
return d <= 0 ? "today" : d === 1 ? "yesterday" : `${d} days ago`;
|
|
30384
|
+
}
|
|
30385
|
+
function recallPrimer(policy, facts, where, now = Date.now()) {
|
|
30386
|
+
if (policy.mode === "off" || !policy.primer.onSessionStart)
|
|
30387
|
+
return null;
|
|
30388
|
+
if (facts.sessionCount <= 0)
|
|
30389
|
+
return null;
|
|
30390
|
+
const scopeWord = { project: "this project", host: "this host", all: "all projects" }[policy.scope];
|
|
30391
|
+
const recent = daysAgo(facts.mostRecent, now);
|
|
30392
|
+
const flag = scopeFlag(policy.scope, where.cwd, where.hostname);
|
|
30393
|
+
const when = [];
|
|
30394
|
+
if (policy.triggers.priorWorkQuestion)
|
|
30395
|
+
when.push("a question about why existing code or a decision is the way it is");
|
|
30396
|
+
if (policy.triggers.repeatedError)
|
|
30397
|
+
when.push("an error that looks familiar");
|
|
30398
|
+
if (policy.triggers.beforeRederiving)
|
|
30399
|
+
when.push("re-deriving something that looks like prior work here");
|
|
30400
|
+
const action = policy.mode === "auto" ? "search history first" : "offer to search history, and search if the user agrees";
|
|
30401
|
+
return [
|
|
30402
|
+
`Session history for ${scopeWord} is available via \`claude-transcripts search "<query>" ${flag} --json --limit ${policy.maxResults}\`.`,
|
|
30403
|
+
`${facts.sessionCount} recorded session${facts.sessionCount === 1 ? "" : "s"} in scope${recent ? `, most recent ${recent}` : ""}.`,
|
|
30404
|
+
when.length ? `Before answering ${joinOr(when)}, ${action}.` : "",
|
|
30405
|
+
`Cite the session id and date. Snippets only, never a whole transcript; max ${policy.maxResults} results. See the \`recall\` skill.`
|
|
30406
|
+
].filter(Boolean).join(" ");
|
|
30407
|
+
}
|
|
30408
|
+
function joinOr(items) {
|
|
30409
|
+
if (items.length <= 1)
|
|
30410
|
+
return items[0] ?? "";
|
|
30411
|
+
return `${items.slice(0, -1).join(", ")}, or ${items[items.length - 1]}`;
|
|
30412
|
+
}
|
|
30413
|
+
|
|
30251
30414
|
// packages/shared/src/model/routes.ts
|
|
30252
30415
|
var ROUTES = [
|
|
30253
30416
|
{ path: "/", serves: "machine-readable app manifest (agent entrypoint)" },
|
|
@@ -30685,6 +30848,7 @@ function buildAppModel(config, env = {}) {
|
|
|
30685
30848
|
features: config.features,
|
|
30686
30849
|
servicesMenu: buildServicesMenu(services, config.servicesMenu),
|
|
30687
30850
|
system: config.system,
|
|
30851
|
+
recall: resolveRecall(config.recall, env),
|
|
30688
30852
|
cliSpec: CLI_SPEC
|
|
30689
30853
|
};
|
|
30690
30854
|
}
|
|
@@ -39061,6 +39225,38 @@ var getSessionTranscript = async (id, params, options) => {
|
|
|
39061
39225
|
method: "GET"
|
|
39062
39226
|
});
|
|
39063
39227
|
};
|
|
39228
|
+
var getGetSessionTurnsUrl = (id, params) => {
|
|
39229
|
+
const normalizedParams = new URLSearchParams;
|
|
39230
|
+
Object.entries(params || {}).forEach(([key, value]) => {
|
|
39231
|
+
if (value !== undefined) {
|
|
39232
|
+
normalizedParams.append(key, value === null ? "null" : value.toString());
|
|
39233
|
+
}
|
|
39234
|
+
});
|
|
39235
|
+
const stringifiedParams = normalizedParams.toString();
|
|
39236
|
+
return stringifiedParams.length > 0 ? `/api/sessions/${id}/turns?${stringifiedParams}` : `/api/sessions/${id}/turns`;
|
|
39237
|
+
};
|
|
39238
|
+
var getSessionTurns = async (id, params, options) => {
|
|
39239
|
+
return customFetch(getGetSessionTurnsUrl(id, params), {
|
|
39240
|
+
...options,
|
|
39241
|
+
method: "GET"
|
|
39242
|
+
});
|
|
39243
|
+
};
|
|
39244
|
+
var getGetTurnsUrl = (params) => {
|
|
39245
|
+
const normalizedParams = new URLSearchParams;
|
|
39246
|
+
Object.entries(params || {}).forEach(([key, value]) => {
|
|
39247
|
+
if (value !== undefined) {
|
|
39248
|
+
normalizedParams.append(key, value === null ? "null" : value.toString());
|
|
39249
|
+
}
|
|
39250
|
+
});
|
|
39251
|
+
const stringifiedParams = normalizedParams.toString();
|
|
39252
|
+
return stringifiedParams.length > 0 ? `/api/turns?${stringifiedParams}` : `/api/turns`;
|
|
39253
|
+
};
|
|
39254
|
+
var getTurns = async (params, options) => {
|
|
39255
|
+
return customFetch(getGetTurnsUrl(params), {
|
|
39256
|
+
...options,
|
|
39257
|
+
method: "GET"
|
|
39258
|
+
});
|
|
39259
|
+
};
|
|
39064
39260
|
var getIngestSummaryUrl = () => {
|
|
39065
39261
|
return `/api/ingest/summary`;
|
|
39066
39262
|
};
|
|
@@ -39738,7 +39934,20 @@ api_bind_addr = "[::]:3903"
|
|
|
39738
39934
|
"garageWebui": "http://127.0.0.1:7655/",
|
|
39739
39935
|
"meilisearch": "http://127.0.0.1:7656/"
|
|
39740
39936
|
},
|
|
39741
|
-
"userSettings": {}
|
|
39937
|
+
"userSettings": {},
|
|
39938
|
+
"recall": {
|
|
39939
|
+
"mode": "auto",
|
|
39940
|
+
"scope": "project",
|
|
39941
|
+
"maxResults": 5,
|
|
39942
|
+
"maxSnippetChars": 400,
|
|
39943
|
+
"triggers": {
|
|
39944
|
+
"priorWorkQuestion": true,
|
|
39945
|
+
"repeatedError": true,
|
|
39946
|
+
"beforeRederiving": true
|
|
39947
|
+
},
|
|
39948
|
+
"excludeCwdGlobs": [],
|
|
39949
|
+
"primer": { "onSessionStart": true, "maxTokens": 200 }
|
|
39950
|
+
}
|
|
39742
39951
|
}
|
|
39743
39952
|
`
|
|
39744
39953
|
}
|
|
@@ -39996,9 +40205,95 @@ async function runExport(argv) {
|
|
|
39996
40205
|
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "fs";
|
|
39997
40206
|
import { dirname } from "path";
|
|
39998
40207
|
|
|
40208
|
+
// packages/cli/src/hook/announce.ts
|
|
40209
|
+
function sessionLink(webapiUrl2, sessionId) {
|
|
40210
|
+
return webapiUrl2 ? `${webapiUrl2.replace(/\/$/, "")}/app/sessions/${sessionId}` : null;
|
|
40211
|
+
}
|
|
40212
|
+
function recordingBanner(targets, sessionId) {
|
|
40213
|
+
const where = [`couchdb://${targets.couchUrl.replace(/^https?:\/\//, "")}/${targets.sessionsDb}`];
|
|
40214
|
+
if (targets.bucket)
|
|
40215
|
+
where.push(`s3://${targets.bucket}`);
|
|
40216
|
+
if (targets.mirrors.length)
|
|
40217
|
+
where.push(`${targets.mirrors.length} mirror(s)`);
|
|
40218
|
+
const link2 = sessionLink(targets.webapiUrl, sessionId);
|
|
40219
|
+
return `Claude Transcripts \u2014 recording to ${where.join(" + ")}${link2 ? ` \xB7 ${link2}` : ""}`;
|
|
40220
|
+
}
|
|
40221
|
+
var NOT_RECORDING_BANNER = "Claude Transcripts \u2014 not recording (no instance configured). Run `claude-transcripts install`.";
|
|
40222
|
+
function sessionStartEnvelope(out) {
|
|
40223
|
+
if (!out.systemMessage && !out.additionalContext)
|
|
40224
|
+
return null;
|
|
40225
|
+
return {
|
|
40226
|
+
...out.systemMessage ? { systemMessage: out.systemMessage } : {},
|
|
40227
|
+
...out.additionalContext ? {
|
|
40228
|
+
hookSpecificOutput: {
|
|
40229
|
+
hookEventName: "SessionStart",
|
|
40230
|
+
additionalContext: out.additionalContext
|
|
40231
|
+
}
|
|
40232
|
+
} : {}
|
|
40233
|
+
};
|
|
40234
|
+
}
|
|
40235
|
+
function emitSessionStart(out) {
|
|
40236
|
+
try {
|
|
40237
|
+
const envelope = sessionStartEnvelope(out);
|
|
40238
|
+
if (envelope)
|
|
40239
|
+
process.stdout.write(`${JSON.stringify(envelope)}
|
|
40240
|
+
`);
|
|
40241
|
+
} catch {}
|
|
40242
|
+
}
|
|
40243
|
+
|
|
39999
40244
|
// packages/cli/src/hook/handlers.ts
|
|
40000
40245
|
import { readFileSync as readFileSync6 } from "fs";
|
|
40001
40246
|
|
|
40247
|
+
// packages/cli/src/hook/recall.ts
|
|
40248
|
+
var FACTS_TIMEOUT_MS = 2000;
|
|
40249
|
+
function isExcluded(cwd2, globs) {
|
|
40250
|
+
for (const g of globs) {
|
|
40251
|
+
try {
|
|
40252
|
+
if (new Bun.Glob(g).match(cwd2))
|
|
40253
|
+
return true;
|
|
40254
|
+
} catch {}
|
|
40255
|
+
}
|
|
40256
|
+
return false;
|
|
40257
|
+
}
|
|
40258
|
+
async function corpusFacts(webapiUrl2, policy, cwd2, hostname4) {
|
|
40259
|
+
const q = new URLSearchParams({ limit: "1" });
|
|
40260
|
+
if (policy.scope === "project")
|
|
40261
|
+
q.set("cwd", cwd2);
|
|
40262
|
+
else if (policy.scope === "host")
|
|
40263
|
+
q.set("hostname", hostname4);
|
|
40264
|
+
try {
|
|
40265
|
+
const res = await fetch(`${webapiUrl2}/api/sessions?${q}`, {
|
|
40266
|
+
signal: AbortSignal.timeout(FACTS_TIMEOUT_MS)
|
|
40267
|
+
});
|
|
40268
|
+
if (!res.ok)
|
|
40269
|
+
return null;
|
|
40270
|
+
const body = await res.json();
|
|
40271
|
+
return {
|
|
40272
|
+
sessionCount: body.totalCount ?? 0,
|
|
40273
|
+
mostRecent: body.sessions?.[0]?.timestamp
|
|
40274
|
+
};
|
|
40275
|
+
} catch {
|
|
40276
|
+
return null;
|
|
40277
|
+
}
|
|
40278
|
+
}
|
|
40279
|
+
async function buildPrimer(ctx) {
|
|
40280
|
+
const policy = resolveRecall(ctx.config.recall, process.env);
|
|
40281
|
+
if (policy.mode === "off" || !policy.primer.onSessionStart)
|
|
40282
|
+
return null;
|
|
40283
|
+
if (!ctx.cwd || isExcluded(ctx.cwd, policy.excludeCwdGlobs))
|
|
40284
|
+
return null;
|
|
40285
|
+
let webapi;
|
|
40286
|
+
try {
|
|
40287
|
+
webapi = resolveWebapiUrl();
|
|
40288
|
+
} catch {
|
|
40289
|
+
return null;
|
|
40290
|
+
}
|
|
40291
|
+
const facts = await corpusFacts(webapi, policy, ctx.cwd, ctx.hostname);
|
|
40292
|
+
if (!facts)
|
|
40293
|
+
return null;
|
|
40294
|
+
return recallPrimer(policy, facts, { cwd: ctx.cwd, hostname: ctx.hostname });
|
|
40295
|
+
}
|
|
40296
|
+
|
|
40002
40297
|
// packages/cli/src/hook/runtime.ts
|
|
40003
40298
|
import {
|
|
40004
40299
|
closeSync,
|
|
@@ -40116,7 +40411,7 @@ function loadHookConfig(path) {
|
|
|
40116
40411
|
return null;
|
|
40117
40412
|
}
|
|
40118
40413
|
}
|
|
40119
|
-
function makeDirectCouch(config) {
|
|
40414
|
+
function makeDirectCouch(config, onWrite) {
|
|
40120
40415
|
const root = config.couch.url;
|
|
40121
40416
|
const headers = { "Content-Type": "application/json" };
|
|
40122
40417
|
if (config.couch.auth)
|
|
@@ -40124,29 +40419,35 @@ function makeDirectCouch(config) {
|
|
|
40124
40419
|
return {
|
|
40125
40420
|
async postDoc(db, doc) {
|
|
40126
40421
|
try {
|
|
40127
|
-
await fetch(`${root}/${db}`, {
|
|
40422
|
+
const res = await fetch(`${root}/${db}`, {
|
|
40128
40423
|
method: "POST",
|
|
40129
40424
|
headers,
|
|
40130
40425
|
body: JSON.stringify(doc),
|
|
40131
40426
|
signal: AbortSignal.timeout(5000)
|
|
40132
40427
|
});
|
|
40133
|
-
|
|
40428
|
+
onWrite?.(res.ok);
|
|
40429
|
+
} catch {
|
|
40430
|
+
onWrite?.(false);
|
|
40431
|
+
}
|
|
40134
40432
|
},
|
|
40135
40433
|
async putDoc(db, id, doc, timeoutMs = 5000) {
|
|
40136
40434
|
try {
|
|
40137
|
-
await fetch(`${root}/${db}/${encodeURIComponent(id)}`, {
|
|
40435
|
+
const res = await fetch(`${root}/${db}/${encodeURIComponent(id)}`, {
|
|
40138
40436
|
method: "PUT",
|
|
40139
40437
|
headers,
|
|
40140
40438
|
body: JSON.stringify(doc),
|
|
40141
40439
|
signal: AbortSignal.timeout(timeoutMs)
|
|
40142
40440
|
});
|
|
40143
|
-
|
|
40441
|
+
onWrite?.(res.ok);
|
|
40442
|
+
} catch {
|
|
40443
|
+
onWrite?.(false);
|
|
40444
|
+
}
|
|
40144
40445
|
}
|
|
40145
40446
|
};
|
|
40146
40447
|
}
|
|
40147
|
-
function makeCouch(config) {
|
|
40448
|
+
function makeCouch(config, onWrite) {
|
|
40148
40449
|
const mirrors = config.mirrors ?? [];
|
|
40149
|
-
return fanOutCouch([makeDirectCouch(config), ...mirrors.map((m) => makeMirrorCouch(m))]);
|
|
40450
|
+
return fanOutCouch([makeDirectCouch(config, onWrite), ...mirrors.map((m) => makeMirrorCouch(m))]);
|
|
40150
40451
|
}
|
|
40151
40452
|
function makeDirectBlob(config) {
|
|
40152
40453
|
const cfg = config.blob;
|
|
@@ -40206,6 +40507,67 @@ function makeCounts(sessionId) {
|
|
|
40206
40507
|
}
|
|
40207
40508
|
};
|
|
40208
40509
|
}
|
|
40510
|
+
function targetsFile(sessionId) {
|
|
40511
|
+
return `/tmp/claude-transcripts-${sessionId}.targets`;
|
|
40512
|
+
}
|
|
40513
|
+
function makeTargets(sessionId) {
|
|
40514
|
+
const file = targetsFile(sessionId);
|
|
40515
|
+
const read = () => {
|
|
40516
|
+
try {
|
|
40517
|
+
return JSON.parse(readFileSync5(file, "utf8"));
|
|
40518
|
+
} catch {
|
|
40519
|
+
return null;
|
|
40520
|
+
}
|
|
40521
|
+
};
|
|
40522
|
+
const write = (t) => {
|
|
40523
|
+
try {
|
|
40524
|
+
writeFileSync2(file, JSON.stringify(t));
|
|
40525
|
+
} catch {}
|
|
40526
|
+
};
|
|
40527
|
+
return {
|
|
40528
|
+
read,
|
|
40529
|
+
write,
|
|
40530
|
+
markWrite(ok) {
|
|
40531
|
+
const t = read();
|
|
40532
|
+
if (!t)
|
|
40533
|
+
return;
|
|
40534
|
+
if (ok) {
|
|
40535
|
+
t.lastWriteMs = Date.now();
|
|
40536
|
+
t.lastFailureMs = 0;
|
|
40537
|
+
} else {
|
|
40538
|
+
t.lastFailureMs = Date.now();
|
|
40539
|
+
}
|
|
40540
|
+
write(t);
|
|
40541
|
+
},
|
|
40542
|
+
clear() {
|
|
40543
|
+
try {
|
|
40544
|
+
unlinkSync(file);
|
|
40545
|
+
} catch {}
|
|
40546
|
+
}
|
|
40547
|
+
};
|
|
40548
|
+
}
|
|
40549
|
+
function redactUrl(url) {
|
|
40550
|
+
try {
|
|
40551
|
+
const u = new URL(url);
|
|
40552
|
+
u.username = "";
|
|
40553
|
+
u.password = "";
|
|
40554
|
+
return u.toString().replace(/\/$/, "");
|
|
40555
|
+
} catch {
|
|
40556
|
+
return url;
|
|
40557
|
+
}
|
|
40558
|
+
}
|
|
40559
|
+
function resolveTargets(config, webapiUrl2) {
|
|
40560
|
+
return {
|
|
40561
|
+
couchUrl: redactUrl(config.couch.url),
|
|
40562
|
+
sessionsDb: config.couch.databases.sessions ?? DEFAULT_SESSIONS_DB,
|
|
40563
|
+
bucket: config.blob?.accessKey ? config.blob.buckets.sessions : undefined,
|
|
40564
|
+
webapiUrl: webapiUrl2,
|
|
40565
|
+
features: Object.entries(config.features ?? {}).filter(([, on]) => on).map(([k]) => k),
|
|
40566
|
+
mirrors: (config.mirrors ?? []).map((m) => redactUrl(m.url)),
|
|
40567
|
+
lastWriteMs: 0,
|
|
40568
|
+
lastFailureMs: 0
|
|
40569
|
+
};
|
|
40570
|
+
}
|
|
40209
40571
|
var STALE_LOCK_MS = 30000;
|
|
40210
40572
|
function makeChunkState(sessionId) {
|
|
40211
40573
|
const stateFile = `/tmp/claude-transcripts-${sessionId}.chunkstate`;
|
|
@@ -40268,12 +40630,15 @@ function makeChunkState(sessionId) {
|
|
|
40268
40630
|
}
|
|
40269
40631
|
};
|
|
40270
40632
|
}
|
|
40633
|
+
var DEFAULT_SESSIONS_DB = "claude-transcripts-sessions";
|
|
40271
40634
|
function buildContext(payload, config) {
|
|
40272
40635
|
const event = payload?.hook_event_name;
|
|
40273
40636
|
const sessionId = payload?.session_id;
|
|
40274
40637
|
if (!event || !sessionId)
|
|
40275
40638
|
return null;
|
|
40639
|
+
const targets = makeTargets(sessionId);
|
|
40276
40640
|
return {
|
|
40641
|
+
output: {},
|
|
40277
40642
|
event,
|
|
40278
40643
|
sessionId,
|
|
40279
40644
|
cwd: payload?.cwd ?? "",
|
|
@@ -40282,10 +40647,11 @@ function buildContext(payload, config) {
|
|
|
40282
40647
|
transcriptPath: payload?.transcript_path,
|
|
40283
40648
|
payload,
|
|
40284
40649
|
config,
|
|
40285
|
-
couch: makeCouch(config),
|
|
40650
|
+
couch: makeCouch(config, (ok) => targets.markWrite(ok)),
|
|
40286
40651
|
blob: makeBlob(config),
|
|
40287
40652
|
counts: makeCounts(sessionId),
|
|
40288
|
-
|
|
40653
|
+
targets,
|
|
40654
|
+
sessionsDb: config.couch.databases.sessions ?? DEFAULT_SESSIONS_DB,
|
|
40289
40655
|
sessionsBucket: config.blob?.buckets.sessions
|
|
40290
40656
|
};
|
|
40291
40657
|
}
|
|
@@ -40306,6 +40672,23 @@ var seedSessionStart = async (ctx) => {
|
|
|
40306
40672
|
ctx.counts.reset();
|
|
40307
40673
|
makeChunkState(ctx.sessionId).seed();
|
|
40308
40674
|
}
|
|
40675
|
+
ctx.targets.write(resolveTargets(ctx.config, safeWebapiUrl()));
|
|
40676
|
+
};
|
|
40677
|
+
function safeWebapiUrl() {
|
|
40678
|
+
try {
|
|
40679
|
+
return resolveWebapiUrl();
|
|
40680
|
+
} catch {
|
|
40681
|
+
return;
|
|
40682
|
+
}
|
|
40683
|
+
}
|
|
40684
|
+
var announceRecording = async (ctx) => {
|
|
40685
|
+
const targets = ctx.targets.read() ?? resolveTargets(ctx.config, safeWebapiUrl());
|
|
40686
|
+
ctx.output.systemMessage = recordingBanner(targets, ctx.sessionId);
|
|
40687
|
+
};
|
|
40688
|
+
var injectRecallPolicy = async (ctx) => {
|
|
40689
|
+
const primer = await buildPrimer(ctx);
|
|
40690
|
+
if (primer)
|
|
40691
|
+
ctx.output.additionalContext = primer;
|
|
40309
40692
|
};
|
|
40310
40693
|
var updateCounts = async (ctx) => {
|
|
40311
40694
|
ctx.counts.inc("events");
|
|
@@ -40454,6 +40837,7 @@ var writeSummary = async (ctx) => {
|
|
|
40454
40837
|
await ctx.blob.put(ctx.sessionsBucket, `${ctx.sessionId}/summary.json`, JSON.stringify(doc), "application/json");
|
|
40455
40838
|
}
|
|
40456
40839
|
ctx.counts.clear();
|
|
40840
|
+
ctx.targets.clear();
|
|
40457
40841
|
};
|
|
40458
40842
|
var uploadBlobs = async (ctx) => {
|
|
40459
40843
|
if (!ctx.blob.enabled || !ctx.sessionsBucket || !ctx.transcriptPath)
|
|
@@ -40468,6 +40852,8 @@ var uploadBlobs = async (ctx) => {
|
|
|
40468
40852
|
};
|
|
40469
40853
|
var HANDLERS = {
|
|
40470
40854
|
"seed-session-start": seedSessionStart,
|
|
40855
|
+
"announce-recording": announceRecording,
|
|
40856
|
+
"inject-recall-policy": injectRecallPolicy,
|
|
40471
40857
|
"update-counts": updateCounts,
|
|
40472
40858
|
"write-event-marker": writeEventMarker,
|
|
40473
40859
|
"flush-transcript-chunk": flushTranscriptChunk,
|
|
@@ -40495,8 +40881,12 @@ async function dispatch(raw, configPath) {
|
|
|
40495
40881
|
return { ran: false, actions: [] };
|
|
40496
40882
|
}
|
|
40497
40883
|
const config = loadHookConfig(configPath);
|
|
40498
|
-
if (!config)
|
|
40884
|
+
if (!config) {
|
|
40885
|
+
if (payload?.hook_event_name === "SessionStart") {
|
|
40886
|
+
emitSessionStart({ systemMessage: NOT_RECORDING_BANNER });
|
|
40887
|
+
}
|
|
40499
40888
|
return { ran: false, actions: [] };
|
|
40889
|
+
}
|
|
40500
40890
|
const ctx = buildContext(payload, config);
|
|
40501
40891
|
if (!ctx)
|
|
40502
40892
|
return { ran: false, actions: [] };
|
|
@@ -40511,6 +40901,8 @@ async function dispatch(raw, configPath) {
|
|
|
40511
40901
|
console.error(`[hook] action ${key} failed (ignored):`, err);
|
|
40512
40902
|
}
|
|
40513
40903
|
}));
|
|
40904
|
+
if (ctx.event === "SessionStart")
|
|
40905
|
+
emitSessionStart(ctx.output);
|
|
40514
40906
|
return { ran: true, event: ctx.event, actions: actions2 };
|
|
40515
40907
|
}
|
|
40516
40908
|
|
|
@@ -40523,12 +40915,14 @@ function readSettings(path) {
|
|
|
40523
40915
|
}
|
|
40524
40916
|
}
|
|
40525
40917
|
function hookCommand() {
|
|
40918
|
+
return `${cliInvocation()} hook run`;
|
|
40919
|
+
}
|
|
40920
|
+
function cliInvocation() {
|
|
40526
40921
|
const exec3 = process.execPath;
|
|
40527
40922
|
const isCompiled = !/\/bun$/.test(exec3);
|
|
40528
40923
|
if (isCompiled)
|
|
40529
|
-
return
|
|
40530
|
-
|
|
40531
|
-
return `${exec3} run ${entry} hook run`;
|
|
40924
|
+
return exec3;
|
|
40925
|
+
return `${exec3} run ${Bun.main}`;
|
|
40532
40926
|
}
|
|
40533
40927
|
function isOurCommand(command) {
|
|
40534
40928
|
if (!command?.includes("hook run"))
|
|
@@ -40911,7 +41305,7 @@ async function runImport(argv) {
|
|
|
40911
41305
|
}
|
|
40912
41306
|
|
|
40913
41307
|
// packages/cli/src/commands/install.ts
|
|
40914
|
-
import { existsSync as
|
|
41308
|
+
import { existsSync as existsSync9, readFileSync as readFileSync12 } from "fs";
|
|
40915
41309
|
|
|
40916
41310
|
// packages/cli/src/lib/assets.ts
|
|
40917
41311
|
import {
|
|
@@ -41291,7 +41685,8 @@ function buildHookConfig(app, env4) {
|
|
|
41291
41685
|
buckets: app.s3.buckets
|
|
41292
41686
|
} : undefined,
|
|
41293
41687
|
features: app.features,
|
|
41294
|
-
system: app.system
|
|
41688
|
+
system: app.system,
|
|
41689
|
+
...app.recall ? { recall: app.recall } : {}
|
|
41295
41690
|
};
|
|
41296
41691
|
}
|
|
41297
41692
|
var PRESERVED_KEYS = ["mirrors"];
|
|
@@ -41514,6 +41909,163 @@ var hasFailures = (checks) => checks.some((c) => c.status === "fail");
|
|
|
41514
41909
|
var DEV_VERSION = "0.0.0-dev";
|
|
41515
41910
|
var VERSION = process.env.CT_VERSION ?? DEV_VERSION;
|
|
41516
41911
|
|
|
41912
|
+
// packages/cli/src/commands/statusline.ts
|
|
41913
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync7, readFileSync as readFileSync11, writeFileSync as writeFileSync7 } from "fs";
|
|
41914
|
+
import { dirname as dirname5 } from "path";
|
|
41915
|
+
|
|
41916
|
+
// packages/cli/src/lib/statusline.ts
|
|
41917
|
+
var STALL_AFTER_MS = 60000;
|
|
41918
|
+
function ago(ms, now2) {
|
|
41919
|
+
const s = Math.max(0, Math.round((now2 - ms) / 1000));
|
|
41920
|
+
if (s < 60)
|
|
41921
|
+
return `${s}s ago`;
|
|
41922
|
+
const m = Math.round(s / 60);
|
|
41923
|
+
if (m < 60)
|
|
41924
|
+
return `${m}m ago`;
|
|
41925
|
+
return `${Math.round(m / 60)}h ago`;
|
|
41926
|
+
}
|
|
41927
|
+
function whereLabel(t) {
|
|
41928
|
+
const host = t.couchUrl.replace(/^https?:\/\//, "").replace(/\/.*$/, "");
|
|
41929
|
+
return `${t.sessionsDb}@${host}`;
|
|
41930
|
+
}
|
|
41931
|
+
function renderStatusline(state, now2 = Date.now()) {
|
|
41932
|
+
if (!state.configured)
|
|
41933
|
+
return "\u25CB ct off \xB7 no instance configured";
|
|
41934
|
+
const t = state.targets;
|
|
41935
|
+
if (!t)
|
|
41936
|
+
return "\u25CB ct off \xB7 not recording this session";
|
|
41937
|
+
const c = state.counts;
|
|
41938
|
+
const toolCalls = c ? Object.values(c.tools).reduce((a, b) => a + b, 0) : 0;
|
|
41939
|
+
const counts = c ? `${c.events} ev \xB7 ${toolCalls} tools` : "0 ev";
|
|
41940
|
+
const written = t.lastWriteMs > 0;
|
|
41941
|
+
const failing = t.lastFailureMs > t.lastWriteMs && now2 - t.lastWriteMs > STALL_AFTER_MS;
|
|
41942
|
+
if (failing) {
|
|
41943
|
+
const last2 = written ? `last write ${ago(t.lastWriteMs, now2)}` : "no write landed";
|
|
41944
|
+
return `\u25D0 ct stalled \xB7 ${counts} \xB7 ${last2} \u2192 ${whereLabel(t)}`;
|
|
41945
|
+
}
|
|
41946
|
+
if (!written)
|
|
41947
|
+
return `\u25CC ct ready \xB7 ${counts} \xB7 no write yet \u2192 ${whereLabel(t)}`;
|
|
41948
|
+
return `\u25CF ct rec \xB7 ${counts} \xB7 ${ago(t.lastWriteMs, now2)} \u2192 ${whereLabel(t)}`;
|
|
41949
|
+
}
|
|
41950
|
+
|
|
41951
|
+
// packages/cli/src/commands/statusline.ts
|
|
41952
|
+
function readSettings2(path) {
|
|
41953
|
+
try {
|
|
41954
|
+
return JSON.parse(readFileSync11(path, "utf8"));
|
|
41955
|
+
} catch {
|
|
41956
|
+
return {};
|
|
41957
|
+
}
|
|
41958
|
+
}
|
|
41959
|
+
function statuslineCommand() {
|
|
41960
|
+
return `${cliInvocation()} statusline render`;
|
|
41961
|
+
}
|
|
41962
|
+
function isOurStatusline(s) {
|
|
41963
|
+
const c = s?.command ?? "";
|
|
41964
|
+
return c.includes("claude-transcripts-statusline") || c.includes("statusline render");
|
|
41965
|
+
}
|
|
41966
|
+
async function render2() {
|
|
41967
|
+
try {
|
|
41968
|
+
let input = {};
|
|
41969
|
+
try {
|
|
41970
|
+
input = JSON.parse(await Bun.stdin.text());
|
|
41971
|
+
} catch {}
|
|
41972
|
+
const configured = existsSync8(installPaths().hookConfig);
|
|
41973
|
+
const id = input.session_id;
|
|
41974
|
+
const line = renderStatusline({
|
|
41975
|
+
configured,
|
|
41976
|
+
targets: id ? makeTargets(id).read() : null,
|
|
41977
|
+
counts: id ? makeCounts(id).read() : null
|
|
41978
|
+
});
|
|
41979
|
+
process.stdout.write(`${line}
|
|
41980
|
+
`);
|
|
41981
|
+
} catch {
|
|
41982
|
+
process.stdout.write(`\u25CB ct off
|
|
41983
|
+
`);
|
|
41984
|
+
}
|
|
41985
|
+
return 0;
|
|
41986
|
+
}
|
|
41987
|
+
function install2(argv) {
|
|
41988
|
+
const { options } = parseFlags(argv);
|
|
41989
|
+
const paths = installPaths();
|
|
41990
|
+
const settings = readSettings2(paths.claudeSettings);
|
|
41991
|
+
const command = statuslineCommand();
|
|
41992
|
+
const next = { type: "command", command };
|
|
41993
|
+
const existing = settings.statusLine;
|
|
41994
|
+
if (existing && !isOurStatusline(existing)) {
|
|
41995
|
+
console.log(`statusline: ${paths.claudeSettings} already has a statusLine that isn't ours:`);
|
|
41996
|
+
console.log(` ${JSON.stringify(existing)}`);
|
|
41997
|
+
console.log("statusline: not overwriting it. To show the indicator alongside your own,");
|
|
41998
|
+
console.log("statusline: have your command append the output of:");
|
|
41999
|
+
console.log(` ${command}`);
|
|
42000
|
+
return 1;
|
|
42001
|
+
}
|
|
42002
|
+
if (existing?.command === command) {
|
|
42003
|
+
console.log(`statusline: already registered \u2192 ${paths.claudeSettings}`);
|
|
42004
|
+
return 0;
|
|
42005
|
+
}
|
|
42006
|
+
if (options["dry-run"] === true) {
|
|
42007
|
+
console.log(`statusline: would set statusLine in ${paths.claudeSettings} to`);
|
|
42008
|
+
console.log(` ${JSON.stringify(next)}`);
|
|
42009
|
+
return 0;
|
|
42010
|
+
}
|
|
42011
|
+
mkdirSync7(dirname5(paths.claudeSettings), { recursive: true });
|
|
42012
|
+
writeFileSync7(paths.claudeSettings, `${JSON.stringify({ ...settings, statusLine: next }, null, 2)}
|
|
42013
|
+
`);
|
|
42014
|
+
console.log(`statusline: registered \u2192 ${paths.claudeSettings}`);
|
|
42015
|
+
console.log("statusline: restart any open Claude Code session for it to take effect.");
|
|
42016
|
+
return 0;
|
|
42017
|
+
}
|
|
42018
|
+
function uninstall2() {
|
|
42019
|
+
const paths = installPaths();
|
|
42020
|
+
const settings = readSettings2(paths.claudeSettings);
|
|
42021
|
+
if (!settings.statusLine) {
|
|
42022
|
+
console.log("statusline: nothing registered.");
|
|
42023
|
+
return 0;
|
|
42024
|
+
}
|
|
42025
|
+
if (!isOurStatusline(settings.statusLine)) {
|
|
42026
|
+
console.log("statusline: the registered statusLine isn't ours; leaving it alone.");
|
|
42027
|
+
return 1;
|
|
42028
|
+
}
|
|
42029
|
+
const { statusLine: _drop, ...rest2 } = settings;
|
|
42030
|
+
writeFileSync7(paths.claudeSettings, `${JSON.stringify(rest2, null, 2)}
|
|
42031
|
+
`);
|
|
42032
|
+
console.log(`statusline: removed from ${paths.claudeSettings}`);
|
|
42033
|
+
return 0;
|
|
42034
|
+
}
|
|
42035
|
+
function status2() {
|
|
42036
|
+
const paths = installPaths();
|
|
42037
|
+
const settings = readSettings2(paths.claudeSettings);
|
|
42038
|
+
console.log(`statusline: settings ${paths.claudeSettings}`);
|
|
42039
|
+
console.log(`statusline: command ${statuslineCommand()}`);
|
|
42040
|
+
const s = settings.statusLine;
|
|
42041
|
+
if (!s) {
|
|
42042
|
+
console.log("statusline: not registered (run `statusline install`).");
|
|
42043
|
+
return 1;
|
|
42044
|
+
}
|
|
42045
|
+
if (!isOurStatusline(s)) {
|
|
42046
|
+
console.log(`statusline: another statusLine is registered: ${JSON.stringify(s)}`);
|
|
42047
|
+
return 1;
|
|
42048
|
+
}
|
|
42049
|
+
console.log("statusline: registered.");
|
|
42050
|
+
return 0;
|
|
42051
|
+
}
|
|
42052
|
+
async function runStatusline(argv) {
|
|
42053
|
+
const [sub = "status", ...rest2] = argv;
|
|
42054
|
+
switch (sub) {
|
|
42055
|
+
case "render":
|
|
42056
|
+
return render2();
|
|
42057
|
+
case "install":
|
|
42058
|
+
return install2(rest2);
|
|
42059
|
+
case "uninstall":
|
|
42060
|
+
return uninstall2();
|
|
42061
|
+
case "status":
|
|
42062
|
+
return status2();
|
|
42063
|
+
default:
|
|
42064
|
+
console.error(`statusline: unknown subcommand "${sub}" (render | install | uninstall | status)`);
|
|
42065
|
+
return 2;
|
|
42066
|
+
}
|
|
42067
|
+
}
|
|
42068
|
+
|
|
41517
42069
|
// packages/cli/src/commands/install.ts
|
|
41518
42070
|
function step(n, total, title) {
|
|
41519
42071
|
console.log(`
|
|
@@ -41570,12 +42122,13 @@ async function setUpSearch(webapiUrl2) {
|
|
|
41570
42122
|
async function runInstall(argv) {
|
|
41571
42123
|
const { options } = parseFlags(argv);
|
|
41572
42124
|
const noHook = options["no-hook"] === true;
|
|
42125
|
+
const noStatusline = options["no-statusline"] === true;
|
|
41573
42126
|
const noApp = options["no-app"] === true;
|
|
41574
42127
|
const meiliKey = options["meili-key"] === true;
|
|
41575
42128
|
const noPrune = options["no-prune"] === true;
|
|
41576
42129
|
const portBaseOpt = strOpt(options, "port-base");
|
|
41577
42130
|
const paths = installPaths();
|
|
41578
|
-
const priorAppTag =
|
|
42131
|
+
const priorAppTag = existsSync9(paths.instanceEnv) ? /^APP_TAG=(\S+)\s*$/m.exec(readFileSync12(paths.instanceEnv, "utf8"))?.[1] : undefined;
|
|
41579
42132
|
const TOTAL = 8;
|
|
41580
42133
|
console.log(`claude-transcripts install (${VERSION})`);
|
|
41581
42134
|
console.log(` config ${paths.configDir}`);
|
|
@@ -41603,7 +42156,7 @@ async function runInstall(argv) {
|
|
|
41603
42156
|
portBase = found;
|
|
41604
42157
|
}
|
|
41605
42158
|
step(2, TOTAL, "Configuration");
|
|
41606
|
-
const firstRun = !
|
|
42159
|
+
const firstRun = !existsSync9(paths.instanceEnv);
|
|
41607
42160
|
let env4 = loadOrCreateInstanceEnv(paths.instanceEnv, {
|
|
41608
42161
|
portBase,
|
|
41609
42162
|
appTag: VERSION === DEV_VERSION ? "main" : VERSION,
|
|
@@ -41687,6 +42240,10 @@ async function runInstall(argv) {
|
|
|
41687
42240
|
console.log(" --no-hook: skipped registration");
|
|
41688
42241
|
} else {
|
|
41689
42242
|
await runHook(["install"]);
|
|
42243
|
+
if (noStatusline)
|
|
42244
|
+
console.log(" --no-statusline: skipped the statusline indicator");
|
|
42245
|
+
else
|
|
42246
|
+
await runStatusline(["install"]);
|
|
41690
42247
|
}
|
|
41691
42248
|
step(7, TOTAL, "Search");
|
|
41692
42249
|
if (noApp) {
|
|
@@ -41761,10 +42318,10 @@ async function runMigrate(argv) {
|
|
|
41761
42318
|
}
|
|
41762
42319
|
|
|
41763
42320
|
// packages/cli/src/commands/provision.ts
|
|
41764
|
-
import { existsSync as
|
|
42321
|
+
import { existsSync as existsSync10 } from "fs";
|
|
41765
42322
|
async function runProvision(_argv) {
|
|
41766
42323
|
const paths = installPaths();
|
|
41767
|
-
if (!
|
|
42324
|
+
if (!existsSync10(paths.instanceEnv)) {
|
|
41768
42325
|
console.error("provision: this machine has no instance \u2014 run `claude-transcripts install` first.");
|
|
41769
42326
|
return 1;
|
|
41770
42327
|
}
|
|
@@ -41894,6 +42451,10 @@ async function runSearch(argv) {
|
|
|
41894
42451
|
...strOpt(options, "hostname") ? { hostname: strOpt(options, "hostname") } : {},
|
|
41895
42452
|
...strOpt(options, "source") ? { source: strOpt(options, "source") } : {}
|
|
41896
42453
|
});
|
|
42454
|
+
if (options.json === true) {
|
|
42455
|
+
console.log(JSON.stringify(res, null, 2));
|
|
42456
|
+
return res.enabled ? 0 : 1;
|
|
42457
|
+
}
|
|
41897
42458
|
if (!res.enabled) {
|
|
41898
42459
|
console.error("search: search is not enabled on this instance.");
|
|
41899
42460
|
console.error("search: turn on `features.meilisearch` in config/config.json, bring up the stack,");
|
|
@@ -41975,8 +42536,12 @@ function summaryLine(s) {
|
|
|
41975
42536
|
[s.tokenUsage ? num3(s.tokenUsage.total) : "\u2014", 9]
|
|
41976
42537
|
], RIGHT_FROM);
|
|
41977
42538
|
}
|
|
41978
|
-
async function showList(limit) {
|
|
42539
|
+
async function showList(limit, json) {
|
|
41979
42540
|
const res = await listSessions({ limit });
|
|
42541
|
+
if (json) {
|
|
42542
|
+
console.log(JSON.stringify(res, null, 2));
|
|
42543
|
+
return 0;
|
|
42544
|
+
}
|
|
41980
42545
|
console.log(`sessions: ${num3(res.totalCount)} total (${webapiUrl()})`);
|
|
41981
42546
|
if (res.sessions.length === 0) {
|
|
41982
42547
|
console.log("sessions: none recorded yet");
|
|
@@ -42001,8 +42566,13 @@ function entryLine(entry, i) {
|
|
|
42001
42566
|
const preview = (text && tools2 ? `${text} ${tools2}` : text || tools2).replace(/\s+/g, " ").trim().slice(0, 100);
|
|
42002
42567
|
return ` ${padL(`#${i}`, 4)} ${pad2(entry.role, 12)} ${preview}`;
|
|
42003
42568
|
}
|
|
42004
|
-
async function showDetail(id, limit) {
|
|
42569
|
+
async function showDetail(id, limit, json) {
|
|
42005
42570
|
const s = await getSession(id);
|
|
42571
|
+
if (json) {
|
|
42572
|
+
const transcript = s.hasTranscript ? await getSessionTranscript(id, { limit }) : null;
|
|
42573
|
+
console.log(JSON.stringify({ session: s, transcript }, null, 2));
|
|
42574
|
+
return 0;
|
|
42575
|
+
}
|
|
42006
42576
|
console.log(`session ${s.sessionId} [${s.status}]`);
|
|
42007
42577
|
console.log(` started ${s.timestamp ?? "\u2014"}`);
|
|
42008
42578
|
console.log(` project ${s.cwd || "\u2014"}`);
|
|
@@ -42034,8 +42604,9 @@ async function runSessions(argv) {
|
|
|
42034
42604
|
const limitOpt = strOpt(options, "limit");
|
|
42035
42605
|
const limit = limitOpt ? Number(limitOpt) : positionals[0] ? 30 : 50;
|
|
42036
42606
|
const id = positionals[0];
|
|
42607
|
+
const json = options.json === true;
|
|
42037
42608
|
try {
|
|
42038
|
-
return id ? await showDetail(id, limit) : await showList(limit);
|
|
42609
|
+
return id ? await showDetail(id, limit, json) : await showList(limit, json);
|
|
42039
42610
|
} catch (err) {
|
|
42040
42611
|
console.error(`sessions: failed \u2014 ${err.message}`);
|
|
42041
42612
|
console.error(`sessions: is the webapi reachable at ${webapiUrl()}? (set --webapi or $CT_WEBAPI_URL)`);
|
|
@@ -42044,9 +42615,9 @@ async function runSessions(argv) {
|
|
|
42044
42615
|
}
|
|
42045
42616
|
|
|
42046
42617
|
// packages/cli/src/commands/setup.ts
|
|
42047
|
-
import { existsSync as
|
|
42618
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync8, readFileSync as readFileSync13, writeFileSync as writeFileSync8 } from "fs";
|
|
42048
42619
|
import { homedir as homedir3 } from "os";
|
|
42049
|
-
import { dirname as
|
|
42620
|
+
import { dirname as dirname6, join as join9 } from "path";
|
|
42050
42621
|
var REPO_ROOT = join9(import.meta.dir, "..", "..", "..", "..");
|
|
42051
42622
|
var HOOK_DIR = join9(REPO_ROOT, "hooks");
|
|
42052
42623
|
var HOOK_CONFIG_PATH = process.env.CT_HOOK_CONFIG ?? join9(homedir3(), ".config", "claude-transcripts", "config.json");
|
|
@@ -42055,7 +42626,7 @@ var PROJECT_SETTINGS = join9(process.cwd(), ".claude", "settings.json");
|
|
|
42055
42626
|
function loadRepoConfig() {
|
|
42056
42627
|
const live = join9(REPO_ROOT, "config", "config.json");
|
|
42057
42628
|
const template2 = join9(REPO_ROOT, "config", "config.template.json");
|
|
42058
|
-
return JSON.parse(
|
|
42629
|
+
return JSON.parse(readFileSync13(existsSync11(live) ? live : template2, "utf8"));
|
|
42059
42630
|
}
|
|
42060
42631
|
function buildHookConfig2(cfg) {
|
|
42061
42632
|
const env4 = process.env;
|
|
@@ -42120,9 +42691,9 @@ async function probeGarage(blob) {
|
|
|
42120
42691
|
console.warn(` garage bucket ${bucket}: NOT reachable/created \u2014 create it + a key on the Garage cluster (admin), then set S3_* in .env. (${err.message})`);
|
|
42121
42692
|
}
|
|
42122
42693
|
}
|
|
42123
|
-
function
|
|
42694
|
+
function readSettings3(path) {
|
|
42124
42695
|
try {
|
|
42125
|
-
return JSON.parse(
|
|
42696
|
+
return JSON.parse(readFileSync13(path, "utf8"));
|
|
42126
42697
|
} catch {
|
|
42127
42698
|
return {};
|
|
42128
42699
|
}
|
|
@@ -42132,13 +42703,13 @@ function dispatchCommand() {
|
|
|
42132
42703
|
}
|
|
42133
42704
|
function isRegistered(path) {
|
|
42134
42705
|
const cmd = dispatchCommand();
|
|
42135
|
-
const hooks2 =
|
|
42706
|
+
const hooks2 = readSettings3(path).hooks ?? {};
|
|
42136
42707
|
return Object.values(hooks2).some((groups) => groups.some((g) => g.hooks.some((h) => h.command === cmd)));
|
|
42137
42708
|
}
|
|
42138
42709
|
function registerGlobal(dryRun) {
|
|
42139
42710
|
const cmd = dispatchCommand();
|
|
42140
|
-
const generated = JSON.parse(
|
|
42141
|
-
const settings =
|
|
42711
|
+
const generated = JSON.parse(readFileSync13(join9(HOOK_DIR, "hooks", "hooks.json"), "utf8"));
|
|
42712
|
+
const settings = readSettings3(GLOBAL_SETTINGS);
|
|
42142
42713
|
const hooks2 = settings.hooks ?? {};
|
|
42143
42714
|
for (const [event, groups] of Object.entries(generated.hooks)) {
|
|
42144
42715
|
const timeout = groups[0]?.hooks[0]?.timeout;
|
|
@@ -42152,8 +42723,8 @@ function registerGlobal(dryRun) {
|
|
|
42152
42723
|
console.log(` [check] would register ${Object.keys(generated.hooks).length} events in ${GLOBAL_SETTINGS}`);
|
|
42153
42724
|
return;
|
|
42154
42725
|
}
|
|
42155
|
-
|
|
42156
|
-
|
|
42726
|
+
mkdirSync8(dirname6(GLOBAL_SETTINGS), { recursive: true });
|
|
42727
|
+
writeFileSync8(GLOBAL_SETTINGS, `${JSON.stringify({ ...settings, hooks: hooks2 }, null, 2)}
|
|
42157
42728
|
`);
|
|
42158
42729
|
console.log(` registered ${Object.keys(generated.hooks).length} events (system-wide) in ${GLOBAL_SETTINGS}`);
|
|
42159
42730
|
}
|
|
@@ -42162,7 +42733,7 @@ function registerProject() {
|
|
|
42162
42733
|
}
|
|
42163
42734
|
function reportExisting() {
|
|
42164
42735
|
const g = isRegistered(GLOBAL_SETTINGS);
|
|
42165
|
-
const p =
|
|
42736
|
+
const p = existsSync11(PROJECT_SETTINGS) && isRegistered(PROJECT_SETTINGS);
|
|
42166
42737
|
console.log(` existing registration \u2192 global: ${g ? "yes" : "no"}, this project: ${p ? "yes" : "no"}`);
|
|
42167
42738
|
}
|
|
42168
42739
|
async function reportSearch() {
|
|
@@ -42193,7 +42764,7 @@ async function runSetup(argv) {
|
|
|
42193
42764
|
reportExisting();
|
|
42194
42765
|
if (check2) {
|
|
42195
42766
|
console.log("setup --check (read-only):");
|
|
42196
|
-
console.log(` hook config: ${
|
|
42767
|
+
console.log(` hook config: ${existsSync11(HOOK_CONFIG_PATH) ? "present" : "MISSING"} (${HOOK_CONFIG_PATH})`);
|
|
42197
42768
|
console.log(` couch: ${couch.url}`);
|
|
42198
42769
|
await provisionCouch2(couch, true);
|
|
42199
42770
|
await probeGarage(hookConfig.blob);
|
|
@@ -42216,12 +42787,12 @@ async function runSetup(argv) {
|
|
|
42216
42787
|
}
|
|
42217
42788
|
|
|
42218
42789
|
// packages/cli/src/commands/stack.ts
|
|
42219
|
-
import { existsSync as
|
|
42790
|
+
import { existsSync as existsSync12 } from "fs";
|
|
42220
42791
|
async function runStack(argv) {
|
|
42221
42792
|
const { positionals, options } = parseFlags(argv);
|
|
42222
42793
|
const cmd = positionals[0] ?? "ps";
|
|
42223
42794
|
const paths = installPaths();
|
|
42224
|
-
if (!
|
|
42795
|
+
if (!existsSync12(paths.instanceEnv)) {
|
|
42225
42796
|
console.error("stack: this machine has no instance \u2014 run `claude-transcripts install` first.");
|
|
42226
42797
|
return 1;
|
|
42227
42798
|
}
|
|
@@ -42255,8 +42826,110 @@ async function runStack(argv) {
|
|
|
42255
42826
|
}
|
|
42256
42827
|
}
|
|
42257
42828
|
|
|
42829
|
+
// packages/cli/src/commands/turns.ts
|
|
42830
|
+
var ROLE_W2 = 11;
|
|
42831
|
+
function textWidth() {
|
|
42832
|
+
const cols = process.stdout.columns ?? 0;
|
|
42833
|
+
const before2 = 8 + 2 + 16 + 2 + ROLE_W2 + 2 + 18 + 2;
|
|
42834
|
+
return Math.max(40, (cols > 0 ? cols : 120) - before2);
|
|
42835
|
+
}
|
|
42836
|
+
function oneLine(text, width) {
|
|
42837
|
+
const t = text.replace(/\s+/g, " ").trim();
|
|
42838
|
+
return t.length > width ? `${t.slice(0, width - 1)}\u2026` : t;
|
|
42839
|
+
}
|
|
42840
|
+
function crossTurnLine(t, width = textWidth()) {
|
|
42841
|
+
return row([
|
|
42842
|
+
[t.sessionId.slice(0, 8), 8],
|
|
42843
|
+
[when(t.timestamp), 16],
|
|
42844
|
+
[t.role, ROLE_W2],
|
|
42845
|
+
[project2(t.cwd), 18],
|
|
42846
|
+
[oneLine(t.text, width), width]
|
|
42847
|
+
]);
|
|
42848
|
+
}
|
|
42849
|
+
function sessionTurnLine(t, i, width = textWidth()) {
|
|
42850
|
+
const tools2 = (t.toolUses ?? []).map((u) => `\u2699 ${u.name}`).join(" ");
|
|
42851
|
+
const text = t.text ?? "";
|
|
42852
|
+
return row([
|
|
42853
|
+
[`#${i}`, 5],
|
|
42854
|
+
[when(t.timestamp), 16],
|
|
42855
|
+
[t.role, ROLE_W2],
|
|
42856
|
+
[oneLine(text && tools2 ? `${text} ${tools2}` : text || tools2, width), width]
|
|
42857
|
+
]);
|
|
42858
|
+
}
|
|
42859
|
+
async function runTurns(argv) {
|
|
42860
|
+
const { positionals, options } = parseFlags(argv);
|
|
42861
|
+
const webapiOverride = strOpt(options, "webapi");
|
|
42862
|
+
if (webapiOverride)
|
|
42863
|
+
setWebapiUrl(webapiOverride);
|
|
42864
|
+
const json = options.json === true;
|
|
42865
|
+
const role = strOpt(options, "role");
|
|
42866
|
+
const limitOpt = strOpt(options, "limit");
|
|
42867
|
+
const limit = limitOpt ? Number(limitOpt) : 50;
|
|
42868
|
+
const id = positionals[0];
|
|
42869
|
+
try {
|
|
42870
|
+
if (id) {
|
|
42871
|
+
const offsetOpt = strOpt(options, "skip");
|
|
42872
|
+
const res2 = await getSessionTurns(id, {
|
|
42873
|
+
...role ? { role } : {},
|
|
42874
|
+
limit,
|
|
42875
|
+
...offsetOpt ? { offset: Number(offsetOpt) } : {}
|
|
42876
|
+
});
|
|
42877
|
+
if (json) {
|
|
42878
|
+
console.log(JSON.stringify(res2, null, 2));
|
|
42879
|
+
return 0;
|
|
42880
|
+
}
|
|
42881
|
+
console.log(`turns: session ${id.slice(0, 8)} \u2014 ${num3(res2.turns.length)} of ${num3(res2.totalCount)}${role ? ` (${role})` : ""} (${webapiUrl()})`);
|
|
42882
|
+
const width2 = textWidth();
|
|
42883
|
+
console.log(row([
|
|
42884
|
+
["#", 5],
|
|
42885
|
+
["WHEN", 16],
|
|
42886
|
+
["WHO", ROLE_W2],
|
|
42887
|
+
["TEXT", width2]
|
|
42888
|
+
]));
|
|
42889
|
+
for (const [i, t] of res2.turns.entries())
|
|
42890
|
+
console.log(sessionTurnLine(t, i, width2));
|
|
42891
|
+
return 0;
|
|
42892
|
+
}
|
|
42893
|
+
const skipOpt = strOpt(options, "skip");
|
|
42894
|
+
const res = await getTurns({
|
|
42895
|
+
...role ? { role } : {},
|
|
42896
|
+
...strOpt(options, "from") ? { from: strOpt(options, "from") } : {},
|
|
42897
|
+
...strOpt(options, "to") ? { to: strOpt(options, "to") } : {},
|
|
42898
|
+
limit,
|
|
42899
|
+
...skipOpt ? { skip: Number(skipOpt) } : {}
|
|
42900
|
+
});
|
|
42901
|
+
if (json) {
|
|
42902
|
+
console.log(JSON.stringify(res, null, 2));
|
|
42903
|
+
return 0;
|
|
42904
|
+
}
|
|
42905
|
+
console.log(`turns: ${num3(res.turns.length)}${res.hasMore ? "+" : ""}${role ? ` (${role})` : ""}, all sessions, time order (${webapiUrl()})`);
|
|
42906
|
+
if (res.turns.length === 0) {
|
|
42907
|
+
console.log("turns: none \u2014 sessions logged without couchFullContentChunks have no turns");
|
|
42908
|
+
return 0;
|
|
42909
|
+
}
|
|
42910
|
+
const width = textWidth();
|
|
42911
|
+
console.log(row([
|
|
42912
|
+
["SESSION", 8],
|
|
42913
|
+
["WHEN", 16],
|
|
42914
|
+
["WHO", ROLE_W2],
|
|
42915
|
+
["PROJECT", 18],
|
|
42916
|
+
["TEXT", width]
|
|
42917
|
+
]));
|
|
42918
|
+
for (const t of res.turns)
|
|
42919
|
+
console.log(crossTurnLine(t, width));
|
|
42920
|
+
if (res.hasMore)
|
|
42921
|
+
console.log(`
|
|
42922
|
+
\u2026 more \u2014 raise --limit, or page with --skip`);
|
|
42923
|
+
return 0;
|
|
42924
|
+
} catch (err) {
|
|
42925
|
+
console.error(`turns: failed \u2014 ${err.message}`);
|
|
42926
|
+
console.error(`turns: is the webapi reachable at ${webapiUrl()}? (set --webapi or $CT_WEBAPI_URL)`);
|
|
42927
|
+
return 1;
|
|
42928
|
+
}
|
|
42929
|
+
}
|
|
42930
|
+
|
|
42258
42931
|
// packages/cli/src/commands/uninstall.ts
|
|
42259
|
-
import { existsSync as
|
|
42932
|
+
import { existsSync as existsSync13, rmSync } from "fs";
|
|
42260
42933
|
async function confirmPurge(auto) {
|
|
42261
42934
|
if (auto)
|
|
42262
42935
|
return true;
|
|
@@ -42276,7 +42949,7 @@ async function runUninstall(argv) {
|
|
|
42276
42949
|
}
|
|
42277
42950
|
console.log("uninstall: deregistering the hook");
|
|
42278
42951
|
await runHook(["uninstall"]);
|
|
42279
|
-
if (
|
|
42952
|
+
if (existsSync13(paths.instanceEnv)) {
|
|
42280
42953
|
console.log(`uninstall: stopping the stack${purge ? " and deleting volumes" : ""}`);
|
|
42281
42954
|
try {
|
|
42282
42955
|
const env4 = loadOrCreateInstanceEnv(paths.instanceEnv);
|
|
@@ -42309,6 +42982,7 @@ var COMMANDS = {
|
|
|
42309
42982
|
stack: runStack,
|
|
42310
42983
|
provision: runProvision,
|
|
42311
42984
|
hook: runHook,
|
|
42985
|
+
statusline: runStatusline,
|
|
42312
42986
|
export: runExport,
|
|
42313
42987
|
import: runImport,
|
|
42314
42988
|
setup: runSetup,
|
|
@@ -42317,7 +42991,8 @@ var COMMANDS = {
|
|
|
42317
42991
|
reindex: runReindex,
|
|
42318
42992
|
doctor: runDoctor,
|
|
42319
42993
|
sessions: runSessions,
|
|
42320
|
-
search: runSearch
|
|
42994
|
+
search: runSearch,
|
|
42995
|
+
turns: runTurns
|
|
42321
42996
|
};
|
|
42322
42997
|
|
|
42323
42998
|
// packages/cli/src/app.tsx
|
package/package.json
CHANGED