@claude-transcripts/cli 0.0.15 → 0.1.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/dist/cli.js +451 -174
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -29899,7 +29899,18 @@ var CLI_SPEC = {
|
|
|
29899
29899
|
description: "entries per chunk doc"
|
|
29900
29900
|
},
|
|
29901
29901
|
{ name: "--no-content", description: "byte-range chunks only (no turn content)" },
|
|
29902
|
-
{
|
|
29902
|
+
{
|
|
29903
|
+
name: "--force",
|
|
29904
|
+
description: "re-process adopted sessions (deletes their derived docs, then rebuilds)"
|
|
29905
|
+
},
|
|
29906
|
+
{
|
|
29907
|
+
name: "--replace-live",
|
|
29908
|
+
description: "with --force: also replace live-recorded sessions (loses hook provenance)"
|
|
29909
|
+
},
|
|
29910
|
+
{
|
|
29911
|
+
name: "--repair",
|
|
29912
|
+
description: "add missing chunk docs to adopted sessions; leaves summary + events alone"
|
|
29913
|
+
},
|
|
29903
29914
|
{
|
|
29904
29915
|
name: "--session",
|
|
29905
29916
|
type: "string",
|
|
@@ -29978,7 +29989,11 @@ var CLI_SPEC = {
|
|
|
29978
29989
|
default: "status",
|
|
29979
29990
|
description: "`run` reads one event payload from stdin (Claude Code calls this)"
|
|
29980
29991
|
},
|
|
29981
|
-
{ name: "--dry-run", description: "with `install`: show the change without writing" }
|
|
29992
|
+
{ name: "--dry-run", description: "with `install`: show the change without writing" },
|
|
29993
|
+
{
|
|
29994
|
+
name: "--force",
|
|
29995
|
+
description: "with `install`: register even if the plugin already does"
|
|
29996
|
+
}
|
|
29982
29997
|
],
|
|
29983
29998
|
examples: ["hook status", "hook install --dry-run"]
|
|
29984
29999
|
},
|
|
@@ -39157,9 +39172,11 @@ async function errorDetail(res) {
|
|
|
39157
39172
|
return "";
|
|
39158
39173
|
}
|
|
39159
39174
|
}
|
|
39160
|
-
async function
|
|
39175
|
+
async function getOrNull(path) {
|
|
39161
39176
|
const res = await fetch(`${base2()}${path}`);
|
|
39162
|
-
|
|
39177
|
+
if (!res.ok)
|
|
39178
|
+
return null;
|
|
39179
|
+
return await res.json();
|
|
39163
39180
|
}
|
|
39164
39181
|
async function putStream(path, filePath, contentType) {
|
|
39165
39182
|
const res = await fetch(`${base2()}${path}`, {
|
|
@@ -39366,7 +39383,10 @@ var migrateDown = async (migrateDownBody, options) => {
|
|
|
39366
39383
|
// packages/cli/src/lib/sink.ts
|
|
39367
39384
|
class DryRunSink {
|
|
39368
39385
|
label = "dry-run";
|
|
39369
|
-
async
|
|
39386
|
+
async existingSession() {
|
|
39387
|
+
return null;
|
|
39388
|
+
}
|
|
39389
|
+
async hasTurns() {
|
|
39370
39390
|
return false;
|
|
39371
39391
|
}
|
|
39372
39392
|
async resetSession(sessionId) {
|
|
@@ -39392,8 +39412,12 @@ class DryRunSink {
|
|
|
39392
39412
|
|
|
39393
39413
|
class WebapiSink {
|
|
39394
39414
|
label = webapiUrl();
|
|
39395
|
-
async
|
|
39396
|
-
return
|
|
39415
|
+
async existingSession(sessionId) {
|
|
39416
|
+
return getOrNull(`/api/sessions/${encodeURIComponent(sessionId)}`);
|
|
39417
|
+
}
|
|
39418
|
+
async hasTurns(sessionId) {
|
|
39419
|
+
const res = await getOrNull(`/api/sessions/${encodeURIComponent(sessionId)}/turns?limit=1`);
|
|
39420
|
+
return (res?.turns?.length ?? 0) > 0;
|
|
39397
39421
|
}
|
|
39398
39422
|
async resetSession(sessionId) {
|
|
39399
39423
|
const res = await resetSession(sessionId);
|
|
@@ -39423,6 +39447,23 @@ function makeSink(opts) {
|
|
|
39423
39447
|
}
|
|
39424
39448
|
|
|
39425
39449
|
// packages/cli/src/commands/backfill.ts
|
|
39450
|
+
function planReingest(existing, opts) {
|
|
39451
|
+
if (!existing)
|
|
39452
|
+
return { action: "adopt" };
|
|
39453
|
+
if (opts.repair) {
|
|
39454
|
+
if (existing.status === "running")
|
|
39455
|
+
return { action: "skip", reason: "running" };
|
|
39456
|
+
if (opts.hasTurns)
|
|
39457
|
+
return { action: "skip", reason: "has-turns" };
|
|
39458
|
+
return { action: "repair" };
|
|
39459
|
+
}
|
|
39460
|
+
if (!opts.force)
|
|
39461
|
+
return { action: "skip", reason: "already-adopted" };
|
|
39462
|
+
if (existing.source === "live" && !opts.replaceLive) {
|
|
39463
|
+
return { action: "skip", reason: "live-record" };
|
|
39464
|
+
}
|
|
39465
|
+
return { action: "adopt" };
|
|
39466
|
+
}
|
|
39426
39467
|
async function runBackfill(argv) {
|
|
39427
39468
|
const { options } = parseFlags(argv);
|
|
39428
39469
|
const dryRun = options["dry-run"] === true;
|
|
@@ -39434,6 +39475,8 @@ async function runBackfill(argv) {
|
|
|
39434
39475
|
const maxEntriesPerChunk = chunkSizeOpt ? Number(chunkSizeOpt) : undefined;
|
|
39435
39476
|
const withContent = options["no-content"] !== true;
|
|
39436
39477
|
const force = options.force === true;
|
|
39478
|
+
const replaceLive = options["replace-live"] === true;
|
|
39479
|
+
const repair = options.repair === true;
|
|
39437
39480
|
const onlySession = strOpt(options, "session");
|
|
39438
39481
|
const only = new Set(onlySession ? [onlySession] : []);
|
|
39439
39482
|
const sink = makeSink({ dryRun, webapiUrl: strOpt(options, "webapi") });
|
|
@@ -39442,30 +39485,69 @@ async function runBackfill(argv) {
|
|
|
39442
39485
|
console.error("backfill: adopted anyway and existing ones are skipped).");
|
|
39443
39486
|
return 2;
|
|
39444
39487
|
}
|
|
39488
|
+
if (repair && (force || replaceLive)) {
|
|
39489
|
+
console.error("backfill: --repair cannot be combined with --force or --replace-live \u2014");
|
|
39490
|
+
console.error("backfill: repair adds what is missing, those replace what is there.");
|
|
39491
|
+
return 2;
|
|
39492
|
+
}
|
|
39493
|
+
if (replaceLive && !force) {
|
|
39494
|
+
console.error("backfill: --replace-live only applies with --force (it widens what --force");
|
|
39495
|
+
console.error("backfill: may overwrite; on its own it would widen nothing).");
|
|
39496
|
+
return 2;
|
|
39497
|
+
}
|
|
39445
39498
|
const who = `host=${host}${actor ? `, actor=${actor}` : ""}`;
|
|
39446
39499
|
console.log(`backfill: scanning ${root} \u2192 ${sink.label}${dryRun ? " (dry-run)" : ""} [${who}]`);
|
|
39447
39500
|
const found = await discoverTranscripts(root);
|
|
39448
39501
|
console.log(`backfill: ${found.length} transcript(s) found`);
|
|
39502
|
+
if (repair) {
|
|
39503
|
+
console.log("backfill: --repair \u2014 adding missing chunk docs only; summaries and event markers " + "are left untouched");
|
|
39504
|
+
}
|
|
39449
39505
|
if (force) {
|
|
39450
39506
|
const scope = only.size ? `${only.size} named session(s)` : "every already-adopted session";
|
|
39451
|
-
console.log(`backfill: --force \u2014 re-processing ${scope} (derived docs are
|
|
39507
|
+
console.log(`backfill: --force \u2014 re-processing ${scope} (derived docs are deleted, then rebuilt)`);
|
|
39508
|
+
console.log(replaceLive ? "backfill: --replace-live \u2014 live-recorded sessions WILL be replaced by reconstructions" : "backfill: live-recorded sessions are left alone (--replace-live overrides)");
|
|
39452
39509
|
}
|
|
39453
39510
|
let written = 0;
|
|
39454
39511
|
let skipped = 0;
|
|
39455
39512
|
let failed = 0;
|
|
39456
39513
|
let sidechains = 0;
|
|
39457
39514
|
let reprocessed = 0;
|
|
39515
|
+
let refusedLive = 0;
|
|
39516
|
+
let repaired = 0;
|
|
39458
39517
|
for (const t of found) {
|
|
39459
39518
|
try {
|
|
39460
39519
|
if (only.size && !only.has(t.sessionId)) {
|
|
39461
39520
|
skipped++;
|
|
39462
39521
|
continue;
|
|
39463
39522
|
}
|
|
39464
|
-
|
|
39465
|
-
|
|
39466
|
-
|
|
39467
|
-
|
|
39523
|
+
const existing = dryRun ? null : await sink.existingSession(t.sessionId);
|
|
39524
|
+
const hasTurns = repair && existing ? await sink.hasTurns(t.sessionId) : false;
|
|
39525
|
+
const plan = planReingest(existing, { force, replaceLive, repair, hasTurns });
|
|
39526
|
+
if (plan.action === "skip") {
|
|
39527
|
+
if (plan.reason === "live-record") {
|
|
39528
|
+
console.warn(` ! ${t.sessionId}: skipped \u2014 recorded live. --force would delete that record and ` + "rebuild it from the transcript, losing provenance only the hook had. Pass " + "--replace-live to do that deliberately.");
|
|
39529
|
+
refusedLive++;
|
|
39530
|
+
}
|
|
39531
|
+
if (plan.reason === "running") {
|
|
39532
|
+
console.log(` \xB7 ${t.sessionId}: skipped \u2014 still running`);
|
|
39533
|
+
}
|
|
39534
|
+
if (plan.reason === "has-turns") {
|
|
39535
|
+
console.log(` \xB7 ${t.sessionId}: skipped \u2014 already has turn content`);
|
|
39468
39536
|
}
|
|
39537
|
+
skipped++;
|
|
39538
|
+
continue;
|
|
39539
|
+
}
|
|
39540
|
+
if (plan.action === "repair") {
|
|
39541
|
+
const jsonl2 = await readTranscript(t.path);
|
|
39542
|
+
const facts2 = deriveSessionFacts(jsonl2, { hostname: host, sessionIdHint: t.sessionId });
|
|
39543
|
+
const chunks = buildChunkDocs(jsonl2, facts2, "backfill", maxEntriesPerChunk, withContent);
|
|
39544
|
+
await sink.putChunks(chunks);
|
|
39545
|
+
await sink.putTranscript(t.sessionId, new TextEncoder().encode(jsonl2));
|
|
39546
|
+
console.log(` + ${t.sessionId}: added ${chunks.length} chunk doc(s)`);
|
|
39547
|
+
repaired++;
|
|
39548
|
+
continue;
|
|
39549
|
+
}
|
|
39550
|
+
if (existing) {
|
|
39469
39551
|
const gone = await sink.resetSession(t.sessionId);
|
|
39470
39552
|
console.log(` ~ ${t.sessionId}: cleared ${gone.summary} summary, ${gone.events} event(s), ` + `${gone.chunks} chunk(s)`);
|
|
39471
39553
|
reprocessed++;
|
|
@@ -39484,7 +39566,7 @@ async function runBackfill(argv) {
|
|
|
39484
39566
|
console.error(` ! ${t.sessionId}: ${err.message}`);
|
|
39485
39567
|
}
|
|
39486
39568
|
}
|
|
39487
|
-
console.log(`backfill: ${written} backfilled${reprocessed ? ` (${reprocessed} re-processed)` : ""}, ` + `${skipped} skipped
|
|
39569
|
+
console.log(`backfill: ${written} backfilled${repaired ? `, ${repaired} repaired` : ""}` + `${reprocessed ? ` (${reprocessed} re-processed)` : ""}, ` + `${skipped} skipped${refusedLive ? ` (${refusedLive} live, not replaced)` : ""}, ` + `${failed} failed`);
|
|
39488
39570
|
console.log("backfill: NOTE \u2014 summary + per-event markers + chunk docs reconstructed" + `${sidechains ? `; subagent sub-transcripts (${sidechains} session(s) have them) still TODO` : ""}` + " (see docs/operate/tools.md).");
|
|
39489
39571
|
if (reprocessed && !dryRun) {
|
|
39490
39572
|
console.log("backfill: run `claude-transcripts reindex` \u2014 search still holds the old turns.");
|
|
@@ -40202,97 +40284,8 @@ async function runExport(argv) {
|
|
|
40202
40284
|
}
|
|
40203
40285
|
|
|
40204
40286
|
// packages/cli/src/commands/hook.ts
|
|
40205
|
-
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as
|
|
40206
|
-
import { dirname } from "path";
|
|
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
|
-
|
|
40244
|
-
// packages/cli/src/hook/handlers.ts
|
|
40245
|
-
import { readFileSync as readFileSync6 } from "fs";
|
|
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
|
-
}
|
|
40287
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync3 } from "fs";
|
|
40288
|
+
import { dirname as dirname2 } from "path";
|
|
40296
40289
|
|
|
40297
40290
|
// packages/cli/src/hook/runtime.ts
|
|
40298
40291
|
import {
|
|
@@ -40329,7 +40322,7 @@ function ingestRoute(doc) {
|
|
|
40329
40322
|
return null;
|
|
40330
40323
|
}
|
|
40331
40324
|
}
|
|
40332
|
-
function makeMirrorCouch(target) {
|
|
40325
|
+
function makeMirrorCouch(target, onWrite) {
|
|
40333
40326
|
const base3 = baseUrl(target);
|
|
40334
40327
|
const headers = { "Content-Type": "application/json", ...authHeaders(target) };
|
|
40335
40328
|
const fallback = target.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
@@ -40338,13 +40331,16 @@ function makeMirrorCouch(target) {
|
|
|
40338
40331
|
if (!route)
|
|
40339
40332
|
return;
|
|
40340
40333
|
try {
|
|
40341
|
-
await fetch(`${base3}${route.path}`, {
|
|
40334
|
+
const res = await fetch(`${base3}${route.path}`, {
|
|
40342
40335
|
method: "POST",
|
|
40343
40336
|
headers,
|
|
40344
40337
|
body: JSON.stringify(route.body),
|
|
40345
40338
|
signal: AbortSignal.timeout(timeoutMs)
|
|
40346
40339
|
});
|
|
40347
|
-
|
|
40340
|
+
onWrite?.(res.ok);
|
|
40341
|
+
} catch {
|
|
40342
|
+
onWrite?.(false);
|
|
40343
|
+
}
|
|
40348
40344
|
};
|
|
40349
40345
|
return {
|
|
40350
40346
|
async postDoc(_db, doc) {
|
|
@@ -40404,9 +40400,29 @@ function fanOutBlob(clients) {
|
|
|
40404
40400
|
}
|
|
40405
40401
|
|
|
40406
40402
|
// packages/cli/src/hook/runtime.ts
|
|
40403
|
+
var DEFAULT_CHUNK = { maxEntriesPerChunk: 200, flushIntervalMs: 15000 };
|
|
40404
|
+
function normalizeHookConfig(raw) {
|
|
40405
|
+
const chunk2 = raw.system?.logging?.chunk;
|
|
40406
|
+
return {
|
|
40407
|
+
...raw,
|
|
40408
|
+
system: {
|
|
40409
|
+
...raw.system,
|
|
40410
|
+
logging: {
|
|
40411
|
+
...raw.system?.logging,
|
|
40412
|
+
chunk: {
|
|
40413
|
+
maxEntriesPerChunk: chunk2?.maxEntriesPerChunk ?? DEFAULT_CHUNK.maxEntriesPerChunk,
|
|
40414
|
+
flushIntervalMs: chunk2?.flushIntervalMs ?? DEFAULT_CHUNK.flushIntervalMs
|
|
40415
|
+
}
|
|
40416
|
+
}
|
|
40417
|
+
}
|
|
40418
|
+
};
|
|
40419
|
+
}
|
|
40407
40420
|
function loadHookConfig(path) {
|
|
40408
40421
|
try {
|
|
40409
|
-
|
|
40422
|
+
const raw = JSON.parse(readFileSync5(path, "utf8"));
|
|
40423
|
+
if (!raw?.couch?.url)
|
|
40424
|
+
return null;
|
|
40425
|
+
return normalizeHookConfig(raw);
|
|
40410
40426
|
} catch {
|
|
40411
40427
|
return null;
|
|
40412
40428
|
}
|
|
@@ -40447,7 +40463,10 @@ function makeDirectCouch(config, onWrite) {
|
|
|
40447
40463
|
}
|
|
40448
40464
|
function makeCouch(config, onWrite) {
|
|
40449
40465
|
const mirrors = config.mirrors ?? [];
|
|
40450
|
-
return fanOutCouch([
|
|
40466
|
+
return fanOutCouch([
|
|
40467
|
+
makeDirectCouch(config, onWrite && ((ok) => onWrite(ok, 0))),
|
|
40468
|
+
...mirrors.map((m, i) => makeMirrorCouch(m, onWrite && ((ok) => onWrite(ok, i + 1))))
|
|
40469
|
+
]);
|
|
40451
40470
|
}
|
|
40452
40471
|
function makeDirectBlob(config) {
|
|
40453
40472
|
const cfg = config.blob;
|
|
@@ -40527,15 +40546,25 @@ function makeTargets(sessionId) {
|
|
|
40527
40546
|
return {
|
|
40528
40547
|
read,
|
|
40529
40548
|
write,
|
|
40530
|
-
markWrite(ok) {
|
|
40549
|
+
markWrite(ok, storeIndex = 0) {
|
|
40531
40550
|
const t = read();
|
|
40532
40551
|
if (!t)
|
|
40533
40552
|
return;
|
|
40553
|
+
const now2 = Date.now();
|
|
40554
|
+
const store = t.stores?.[storeIndex];
|
|
40555
|
+
if (store) {
|
|
40556
|
+
if (ok) {
|
|
40557
|
+
store.lastWriteMs = now2;
|
|
40558
|
+
store.lastFailureMs = 0;
|
|
40559
|
+
} else {
|
|
40560
|
+
store.lastFailureMs = now2;
|
|
40561
|
+
}
|
|
40562
|
+
}
|
|
40534
40563
|
if (ok) {
|
|
40535
|
-
t.lastWriteMs =
|
|
40564
|
+
t.lastWriteMs = now2;
|
|
40536
40565
|
t.lastFailureMs = 0;
|
|
40537
|
-
} else {
|
|
40538
|
-
t.lastFailureMs =
|
|
40566
|
+
} else if (!t.stores?.some((x) => x.lastWriteMs > x.lastFailureMs)) {
|
|
40567
|
+
t.lastFailureMs = now2;
|
|
40539
40568
|
}
|
|
40540
40569
|
write(t);
|
|
40541
40570
|
},
|
|
@@ -40556,6 +40585,9 @@ function redactUrl(url) {
|
|
|
40556
40585
|
return url;
|
|
40557
40586
|
}
|
|
40558
40587
|
}
|
|
40588
|
+
function hostOf(url) {
|
|
40589
|
+
return redactUrl(url).replace(/^https?:\/\//, "").replace(/\/.*$/, "");
|
|
40590
|
+
}
|
|
40559
40591
|
function resolveTargets(config, webapiUrl2) {
|
|
40560
40592
|
return {
|
|
40561
40593
|
couchUrl: redactUrl(config.couch.url),
|
|
@@ -40564,6 +40596,20 @@ function resolveTargets(config, webapiUrl2) {
|
|
|
40564
40596
|
webapiUrl: webapiUrl2,
|
|
40565
40597
|
features: Object.entries(config.features ?? {}).filter(([, on]) => on).map(([k]) => k),
|
|
40566
40598
|
mirrors: (config.mirrors ?? []).map((m) => redactUrl(m.url)),
|
|
40599
|
+
stores: [
|
|
40600
|
+
{
|
|
40601
|
+
label: `${config.couch.databases.sessions ?? DEFAULT_SESSIONS_DB}@${hostOf(config.couch.url)}`,
|
|
40602
|
+
kind: "direct",
|
|
40603
|
+
lastWriteMs: 0,
|
|
40604
|
+
lastFailureMs: 0
|
|
40605
|
+
},
|
|
40606
|
+
...(config.mirrors ?? []).map((m) => ({
|
|
40607
|
+
label: hostOf(m.url),
|
|
40608
|
+
kind: "mirror",
|
|
40609
|
+
lastWriteMs: 0,
|
|
40610
|
+
lastFailureMs: 0
|
|
40611
|
+
}))
|
|
40612
|
+
],
|
|
40567
40613
|
lastWriteMs: 0,
|
|
40568
40614
|
lastFailureMs: 0
|
|
40569
40615
|
};
|
|
@@ -40647,7 +40693,7 @@ function buildContext(payload, config) {
|
|
|
40647
40693
|
transcriptPath: payload?.transcript_path,
|
|
40648
40694
|
payload,
|
|
40649
40695
|
config,
|
|
40650
|
-
couch: makeCouch(config, (ok) => targets.markWrite(ok)),
|
|
40696
|
+
couch: makeCouch(config, (ok, storeIndex) => targets.markWrite(ok, storeIndex)),
|
|
40651
40697
|
blob: makeBlob(config),
|
|
40652
40698
|
counts: makeCounts(sessionId),
|
|
40653
40699
|
targets,
|
|
@@ -40665,6 +40711,96 @@ function commonFields(ctx) {
|
|
|
40665
40711
|
};
|
|
40666
40712
|
}
|
|
40667
40713
|
|
|
40714
|
+
// packages/cli/src/hook/announce.ts
|
|
40715
|
+
function sessionLink(webapiUrl2, sessionId) {
|
|
40716
|
+
return webapiUrl2 ? `${webapiUrl2.replace(/\/$/, "")}/app/sessions/${sessionId}` : null;
|
|
40717
|
+
}
|
|
40718
|
+
function recordingBanner(targets, sessionId) {
|
|
40719
|
+
const where = [`couchdb://${targets.couchUrl.replace(/^https?:\/\//, "")}/${targets.sessionsDb}`];
|
|
40720
|
+
if (targets.bucket)
|
|
40721
|
+
where.push(`s3://${targets.bucket}`);
|
|
40722
|
+
if (targets.mirrors.length) {
|
|
40723
|
+
where.push(`mirrors: ${targets.mirrors.map(hostOf).join(", ")}`);
|
|
40724
|
+
}
|
|
40725
|
+
const link2 = sessionLink(targets.webapiUrl, sessionId);
|
|
40726
|
+
return `Claude Transcripts \u2014 recording to ${where.join(" + ")}${link2 ? ` \xB7 ${link2}` : ""}`;
|
|
40727
|
+
}
|
|
40728
|
+
var NOT_RECORDING_BANNER = "Claude Transcripts \u2014 not recording (no instance configured). Run `claude-transcripts install`.";
|
|
40729
|
+
function sessionStartEnvelope(out) {
|
|
40730
|
+
if (!out.systemMessage && !out.additionalContext)
|
|
40731
|
+
return null;
|
|
40732
|
+
return {
|
|
40733
|
+
...out.systemMessage ? { systemMessage: out.systemMessage } : {},
|
|
40734
|
+
...out.additionalContext ? {
|
|
40735
|
+
hookSpecificOutput: {
|
|
40736
|
+
hookEventName: "SessionStart",
|
|
40737
|
+
additionalContext: out.additionalContext
|
|
40738
|
+
}
|
|
40739
|
+
} : {}
|
|
40740
|
+
};
|
|
40741
|
+
}
|
|
40742
|
+
function emitSessionStart(out) {
|
|
40743
|
+
try {
|
|
40744
|
+
const envelope = sessionStartEnvelope(out);
|
|
40745
|
+
if (envelope)
|
|
40746
|
+
process.stdout.write(`${JSON.stringify(envelope)}
|
|
40747
|
+
`);
|
|
40748
|
+
} catch {}
|
|
40749
|
+
}
|
|
40750
|
+
|
|
40751
|
+
// packages/cli/src/hook/handlers.ts
|
|
40752
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
40753
|
+
|
|
40754
|
+
// packages/cli/src/hook/recall.ts
|
|
40755
|
+
var FACTS_TIMEOUT_MS = 2000;
|
|
40756
|
+
function isExcluded(cwd2, globs) {
|
|
40757
|
+
for (const g of globs) {
|
|
40758
|
+
try {
|
|
40759
|
+
if (new Bun.Glob(g).match(cwd2))
|
|
40760
|
+
return true;
|
|
40761
|
+
} catch {}
|
|
40762
|
+
}
|
|
40763
|
+
return false;
|
|
40764
|
+
}
|
|
40765
|
+
async function corpusFacts(webapiUrl2, policy, cwd2, hostname5) {
|
|
40766
|
+
const q = new URLSearchParams({ limit: "1" });
|
|
40767
|
+
if (policy.scope === "project")
|
|
40768
|
+
q.set("cwd", cwd2);
|
|
40769
|
+
else if (policy.scope === "host")
|
|
40770
|
+
q.set("hostname", hostname5);
|
|
40771
|
+
try {
|
|
40772
|
+
const res = await fetch(`${webapiUrl2}/api/sessions?${q}`, {
|
|
40773
|
+
signal: AbortSignal.timeout(FACTS_TIMEOUT_MS)
|
|
40774
|
+
});
|
|
40775
|
+
if (!res.ok)
|
|
40776
|
+
return null;
|
|
40777
|
+
const body = await res.json();
|
|
40778
|
+
return {
|
|
40779
|
+
sessionCount: body.totalCount ?? 0,
|
|
40780
|
+
mostRecent: body.sessions?.[0]?.timestamp
|
|
40781
|
+
};
|
|
40782
|
+
} catch {
|
|
40783
|
+
return null;
|
|
40784
|
+
}
|
|
40785
|
+
}
|
|
40786
|
+
async function buildPrimer(ctx) {
|
|
40787
|
+
const policy = resolveRecall(ctx.config.recall, process.env);
|
|
40788
|
+
if (policy.mode === "off" || !policy.primer.onSessionStart)
|
|
40789
|
+
return null;
|
|
40790
|
+
if (!ctx.cwd || isExcluded(ctx.cwd, policy.excludeCwdGlobs))
|
|
40791
|
+
return null;
|
|
40792
|
+
let webapi;
|
|
40793
|
+
try {
|
|
40794
|
+
webapi = resolveWebapiUrl();
|
|
40795
|
+
} catch {
|
|
40796
|
+
return null;
|
|
40797
|
+
}
|
|
40798
|
+
const facts = await corpusFacts(webapi, policy, ctx.cwd, ctx.hostname);
|
|
40799
|
+
if (!facts)
|
|
40800
|
+
return null;
|
|
40801
|
+
return recallPrimer(policy, facts, { cwd: ctx.cwd, hostname: ctx.hostname });
|
|
40802
|
+
}
|
|
40803
|
+
|
|
40668
40804
|
// packages/cli/src/hook/handlers.ts
|
|
40669
40805
|
var seedSessionStart = async (ctx) => {
|
|
40670
40806
|
const source = ctx.payload?.source;
|
|
@@ -40873,6 +41009,10 @@ var LONG_TIMEOUT_EVENTS = new Set(["SessionStart", "SessionEnd"]);
|
|
|
40873
41009
|
function hookTimeout(event) {
|
|
40874
41010
|
return LONG_TIMEOUT_EVENTS.has(event) ? 180 : 5;
|
|
40875
41011
|
}
|
|
41012
|
+
var SYNCHRONOUS_EVENTS = new Set(["SessionStart", "SessionEnd", "UserPromptSubmit"]);
|
|
41013
|
+
function hookAsync(event) {
|
|
41014
|
+
return !SYNCHRONOUS_EVENTS.has(event);
|
|
41015
|
+
}
|
|
40876
41016
|
async function dispatch(raw, configPath) {
|
|
40877
41017
|
let payload;
|
|
40878
41018
|
try {
|
|
@@ -40887,7 +41027,13 @@ async function dispatch(raw, configPath) {
|
|
|
40887
41027
|
}
|
|
40888
41028
|
return { ran: false, actions: [] };
|
|
40889
41029
|
}
|
|
40890
|
-
|
|
41030
|
+
let ctx;
|
|
41031
|
+
try {
|
|
41032
|
+
ctx = buildContext(payload, config);
|
|
41033
|
+
} catch (err) {
|
|
41034
|
+
console.error("[hook] could not build context (ignored):", err);
|
|
41035
|
+
return { ran: false, actions: [] };
|
|
41036
|
+
}
|
|
40891
41037
|
if (!ctx)
|
|
40892
41038
|
return { ran: false, actions: [] };
|
|
40893
41039
|
const actions2 = hookBindings()[ctx.event] ?? [];
|
|
@@ -40906,10 +41052,62 @@ async function dispatch(raw, configPath) {
|
|
|
40906
41052
|
return { ran: true, event: ctx.event, actions: actions2 };
|
|
40907
41053
|
}
|
|
40908
41054
|
|
|
41055
|
+
// packages/cli/src/lib/plugin.ts
|
|
41056
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
41057
|
+
import { dirname, join as join7 } from "path";
|
|
41058
|
+
var PLUGIN_NAME = "claude-transcripts";
|
|
41059
|
+
function readJson(path) {
|
|
41060
|
+
try {
|
|
41061
|
+
const parsed = JSON.parse(readFileSync7(path, "utf8"));
|
|
41062
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
41063
|
+
} catch {
|
|
41064
|
+
return null;
|
|
41065
|
+
}
|
|
41066
|
+
}
|
|
41067
|
+
function pluginsDir(settingsPath) {
|
|
41068
|
+
return join7(dirname(settingsPath), "plugins");
|
|
41069
|
+
}
|
|
41070
|
+
function pluginEvents(settingsPath, key) {
|
|
41071
|
+
const installed = readJson(join7(pluginsDir(settingsPath), "installed_plugins.json"));
|
|
41072
|
+
const entry = installed?.plugins?.[key];
|
|
41073
|
+
const first = Array.isArray(entry) ? entry[0] : undefined;
|
|
41074
|
+
const installPath = typeof first?.installPath === "string" ? first.installPath : undefined;
|
|
41075
|
+
if (!installPath)
|
|
41076
|
+
return;
|
|
41077
|
+
const hooks2 = readJson(join7(installPath, "hooks", "hooks.json"));
|
|
41078
|
+
const events = hooks2?.hooks;
|
|
41079
|
+
if (!events || typeof events !== "object")
|
|
41080
|
+
return;
|
|
41081
|
+
return Object.keys(events);
|
|
41082
|
+
}
|
|
41083
|
+
function pluginVersion(settingsPath, key) {
|
|
41084
|
+
const installed = readJson(join7(pluginsDir(settingsPath), "installed_plugins.json"));
|
|
41085
|
+
const entry = installed?.plugins?.[key];
|
|
41086
|
+
const first = Array.isArray(entry) ? entry[0] : undefined;
|
|
41087
|
+
return typeof first?.version === "string" ? first.version : undefined;
|
|
41088
|
+
}
|
|
41089
|
+
function pluginRegistration(settingsPath, settings) {
|
|
41090
|
+
const enabled = settings.enabledPlugins;
|
|
41091
|
+
if (!enabled || typeof enabled !== "object")
|
|
41092
|
+
return null;
|
|
41093
|
+
for (const [key, on] of Object.entries(enabled)) {
|
|
41094
|
+
if (on !== true)
|
|
41095
|
+
continue;
|
|
41096
|
+
if (key.split("@")[0] !== PLUGIN_NAME)
|
|
41097
|
+
continue;
|
|
41098
|
+
return {
|
|
41099
|
+
key,
|
|
41100
|
+
version: pluginVersion(settingsPath, key),
|
|
41101
|
+
events: pluginEvents(settingsPath, key)
|
|
41102
|
+
};
|
|
41103
|
+
}
|
|
41104
|
+
return null;
|
|
41105
|
+
}
|
|
41106
|
+
|
|
40909
41107
|
// packages/cli/src/commands/hook.ts
|
|
40910
41108
|
function readSettings(path) {
|
|
40911
41109
|
try {
|
|
40912
|
-
return JSON.parse(
|
|
41110
|
+
return JSON.parse(readFileSync8(path, "utf8"));
|
|
40913
41111
|
} catch {
|
|
40914
41112
|
return {};
|
|
40915
41113
|
}
|
|
@@ -40948,8 +41146,19 @@ function install(argv) {
|
|
|
40948
41146
|
const settings = readSettings(paths.claudeSettings);
|
|
40949
41147
|
const hooks2 = settings.hooks ?? {};
|
|
40950
41148
|
const events = Object.keys(hookBindings());
|
|
41149
|
+
const plugin = pluginRegistration(paths.claudeSettings, settings);
|
|
41150
|
+
if (plugin && options.force !== true) {
|
|
41151
|
+
console.error(`hook: the plugin ${plugin.key} already registers these events.`);
|
|
41152
|
+
console.error("hook: registering here as well would write every event twice.");
|
|
41153
|
+
console.error("hook: pick one route \u2014");
|
|
41154
|
+
console.error(`hook: keep the plugin \u2192 nothing to do (\`hook status\` explains it)`);
|
|
41155
|
+
console.error(`hook: use this binary \u2192 disable the plugin, then re-run`);
|
|
41156
|
+
console.error("hook: --force registers anyway.");
|
|
41157
|
+
return 1;
|
|
41158
|
+
}
|
|
40951
41159
|
let added = 0;
|
|
40952
41160
|
let replaced = 0;
|
|
41161
|
+
let corrected = 0;
|
|
40953
41162
|
for (const event of events) {
|
|
40954
41163
|
const existing = hooks2[event] ?? [];
|
|
40955
41164
|
const cleaned = [];
|
|
@@ -40966,26 +41175,48 @@ function install(argv) {
|
|
|
40966
41175
|
if (kept.length)
|
|
40967
41176
|
cleaned.push({ ...g, hooks: kept });
|
|
40968
41177
|
}
|
|
41178
|
+
const timeout = hookTimeout(event);
|
|
41179
|
+
const wantAsync = hookAsync(event);
|
|
40969
41180
|
if (cleaned.some((g) => g.hooks?.some((h) => h.command === command))) {
|
|
41181
|
+
for (const g of cleaned) {
|
|
41182
|
+
for (const h of g.hooks ?? []) {
|
|
41183
|
+
if (h.command !== command)
|
|
41184
|
+
continue;
|
|
41185
|
+
if (h.timeout !== timeout) {
|
|
41186
|
+
h.timeout = timeout;
|
|
41187
|
+
corrected++;
|
|
41188
|
+
} else if (wantAsync ? h.async !== true : h.async !== undefined) {
|
|
41189
|
+
corrected++;
|
|
41190
|
+
}
|
|
41191
|
+
if (wantAsync)
|
|
41192
|
+
h.async = true;
|
|
41193
|
+
else
|
|
41194
|
+
delete h.async;
|
|
41195
|
+
}
|
|
41196
|
+
}
|
|
40970
41197
|
hooks2[event] = cleaned;
|
|
40971
41198
|
continue;
|
|
40972
41199
|
}
|
|
40973
41200
|
hooks2[event] = [
|
|
40974
41201
|
...cleaned,
|
|
40975
|
-
{ hooks: [{ type: "command", command, timeout:
|
|
41202
|
+
{ hooks: [{ type: "command", command, timeout, ...wantAsync ? { async: true } : {} }] }
|
|
40976
41203
|
];
|
|
40977
41204
|
added++;
|
|
40978
41205
|
}
|
|
40979
41206
|
if (options["dry-run"] === true) {
|
|
40980
|
-
console.log(`hook: would register ${added} event(s)` + `${replaced ? `, replacing ${replaced} stale one(s)` : ""} in ${paths.claudeSettings}`);
|
|
41207
|
+
console.log(`hook: would register ${added} event(s)` + `${replaced ? `, replacing ${replaced} stale one(s)` : ""}` + `${corrected ? `, correcting ${corrected} existing one(s)` : ""} in ${paths.claudeSettings}`);
|
|
40981
41208
|
return 0;
|
|
40982
41209
|
}
|
|
40983
|
-
mkdirSync3(
|
|
41210
|
+
mkdirSync3(dirname2(paths.claudeSettings), { recursive: true });
|
|
40984
41211
|
writeFileSync3(paths.claudeSettings, `${JSON.stringify({ ...settings, hooks: hooks2 }, null, 2)}
|
|
40985
41212
|
`);
|
|
40986
|
-
const
|
|
41213
|
+
const notes = [
|
|
41214
|
+
replaced ? `replaced ${replaced} stale registration(s)` : "",
|
|
41215
|
+
corrected ? `corrected ${corrected} existing registration(s)` : ""
|
|
41216
|
+
].filter(Boolean);
|
|
41217
|
+
const suffix = notes.length ? ` (${notes.join(", ")})` : "";
|
|
40987
41218
|
console.log(added ? `hook: registered ${added} event(s)${suffix} \u2192 ${paths.claudeSettings}` : `hook: already registered (${events.length} events)${suffix} \u2192 ${paths.claudeSettings}`);
|
|
40988
|
-
if (added)
|
|
41219
|
+
if (added || corrected)
|
|
40989
41220
|
console.log("hook: restart any open Claude Code session for it to take effect.");
|
|
40990
41221
|
return 0;
|
|
40991
41222
|
}
|
|
@@ -41011,27 +41242,48 @@ function uninstall() {
|
|
|
41011
41242
|
writeFileSync3(paths.claudeSettings, `${JSON.stringify({ ...settings, hooks: hooks2 }, null, 2)}
|
|
41012
41243
|
`);
|
|
41013
41244
|
console.log(`hook: removed ${removed} registration(s) from ${paths.claudeSettings}`);
|
|
41245
|
+
const plugin = pluginRegistration(paths.claudeSettings, settings);
|
|
41246
|
+
if (plugin)
|
|
41247
|
+
console.log(`hook: still recording via the plugin ${plugin.key}.`);
|
|
41014
41248
|
return 0;
|
|
41015
41249
|
}
|
|
41016
41250
|
function configuredMirrors(path) {
|
|
41017
41251
|
try {
|
|
41018
|
-
const parsed = JSON.parse(
|
|
41252
|
+
const parsed = JSON.parse(readFileSync8(path, "utf8"));
|
|
41019
41253
|
return (parsed.mirrors ?? []).map((m) => m?.url ?? "(no url)");
|
|
41020
41254
|
} catch {
|
|
41021
41255
|
return [];
|
|
41022
41256
|
}
|
|
41023
41257
|
}
|
|
41258
|
+
function pluginSummary(plugin) {
|
|
41259
|
+
const version = plugin.version ? ` v${plugin.version}` : "";
|
|
41260
|
+
const count = plugin.events?.length;
|
|
41261
|
+
return `${plugin.key}${version}${count ? ` \u2014 registers ${count} event(s)` : ""}`;
|
|
41262
|
+
}
|
|
41024
41263
|
function status() {
|
|
41025
41264
|
const paths = installPaths();
|
|
41026
41265
|
const settings = readSettings(paths.claudeSettings);
|
|
41027
41266
|
const found = findRegistered(settings);
|
|
41267
|
+
const plugin = pluginRegistration(paths.claudeSettings, settings);
|
|
41028
41268
|
console.log(`hook: settings ${paths.claudeSettings}`);
|
|
41029
41269
|
console.log(`hook: config ${paths.hookConfig} ${existsSync4(paths.hookConfig) ? "" : "(MISSING \u2014 run `install`)"}`);
|
|
41030
41270
|
console.log(`hook: command ${hookCommand()}`);
|
|
41271
|
+
if (plugin)
|
|
41272
|
+
console.log(`hook: plugin ${pluginSummary(plugin)}`);
|
|
41031
41273
|
const mirrors = configuredMirrors(paths.hookConfig);
|
|
41032
41274
|
for (const url of mirrors)
|
|
41033
41275
|
console.log(`hook: mirror ${url}`);
|
|
41276
|
+
if (plugin && found.length) {
|
|
41277
|
+
console.log(`hook: DOUBLE-REGISTERED \u2014 ${found.length} event(s) here AND via the plugin.`);
|
|
41278
|
+
console.log("hook: every event is being written twice. Run `hook uninstall` to keep");
|
|
41279
|
+
console.log("hook: the plugin's registration, or disable the plugin to keep this one.");
|
|
41280
|
+
return 1;
|
|
41281
|
+
}
|
|
41034
41282
|
if (!found.length) {
|
|
41283
|
+
if (plugin) {
|
|
41284
|
+
console.log("hook: registered by the plugin (nothing in settings.json \u2014 correct).");
|
|
41285
|
+
return 0;
|
|
41286
|
+
}
|
|
41035
41287
|
console.log("hook: not registered.");
|
|
41036
41288
|
return 1;
|
|
41037
41289
|
}
|
|
@@ -41066,7 +41318,7 @@ async function runHook(argv) {
|
|
|
41066
41318
|
|
|
41067
41319
|
// packages/cli/src/commands/import.ts
|
|
41068
41320
|
import { existsSync as existsSync5, statSync as statSync2 } from "fs";
|
|
41069
|
-
import { join as
|
|
41321
|
+
import { join as join8 } from "path";
|
|
41070
41322
|
var BATCH = 500;
|
|
41071
41323
|
function num(n) {
|
|
41072
41324
|
return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
@@ -41136,7 +41388,7 @@ function checkSchema(bundleVersion, target, registry2 = REAL_REGISTRY) {
|
|
|
41136
41388
|
async function verifyChecksums(dir, manifest) {
|
|
41137
41389
|
const bad = [];
|
|
41138
41390
|
for (const [rel, expected] of Object.entries(manifest.checksums)) {
|
|
41139
|
-
const path =
|
|
41391
|
+
const path = join8(dir, rel);
|
|
41140
41392
|
if (!existsSync5(path)) {
|
|
41141
41393
|
bad.push(`${rel} (missing)`);
|
|
41142
41394
|
continue;
|
|
@@ -41281,7 +41533,7 @@ async function runImport(argv) {
|
|
|
41281
41533
|
skipped++;
|
|
41282
41534
|
continue;
|
|
41283
41535
|
}
|
|
41284
|
-
const path =
|
|
41536
|
+
const path = join8(dir, rel);
|
|
41285
41537
|
await putStream(`/api/ingest/${sessionId}/transcript`, path, "application/x-ndjson");
|
|
41286
41538
|
blobs++;
|
|
41287
41539
|
blobBytes += statSync2(path).size;
|
|
@@ -41305,41 +41557,41 @@ async function runImport(argv) {
|
|
|
41305
41557
|
}
|
|
41306
41558
|
|
|
41307
41559
|
// packages/cli/src/commands/install.ts
|
|
41308
|
-
import { existsSync as existsSync9, readFileSync as
|
|
41560
|
+
import { existsSync as existsSync9, readFileSync as readFileSync13 } from "fs";
|
|
41309
41561
|
|
|
41310
41562
|
// packages/cli/src/lib/assets.ts
|
|
41311
41563
|
import {
|
|
41312
41564
|
existsSync as existsSync6,
|
|
41313
41565
|
lstatSync,
|
|
41314
41566
|
mkdirSync as mkdirSync4,
|
|
41315
|
-
readFileSync as
|
|
41567
|
+
readFileSync as readFileSync9,
|
|
41316
41568
|
symlinkSync,
|
|
41317
41569
|
unlinkSync as unlinkSync2,
|
|
41318
41570
|
writeFileSync as writeFileSync4
|
|
41319
41571
|
} from "fs";
|
|
41320
|
-
import { dirname as
|
|
41572
|
+
import { dirname as dirname3, join as join9 } from "path";
|
|
41321
41573
|
function writeAssets(paths) {
|
|
41322
41574
|
const out = [];
|
|
41323
41575
|
for (const asset of Object.values(EMBEDDED_ASSETS)) {
|
|
41324
41576
|
const isUserOwned = asset.path.startsWith("config/");
|
|
41325
|
-
const target = isUserOwned ? paths.appConfig :
|
|
41577
|
+
const target = isUserOwned ? paths.appConfig : join9(paths.dataDir, asset.path);
|
|
41326
41578
|
if (isUserOwned && existsSync6(target)) {
|
|
41327
41579
|
out.push({ path: target, changed: false });
|
|
41328
41580
|
continue;
|
|
41329
41581
|
}
|
|
41330
|
-
const previous = existsSync6(target) ?
|
|
41582
|
+
const previous = existsSync6(target) ? readFileSync9(target, "utf8") : null;
|
|
41331
41583
|
if (previous === asset.content) {
|
|
41332
41584
|
out.push({ path: target, changed: false });
|
|
41333
41585
|
continue;
|
|
41334
41586
|
}
|
|
41335
|
-
mkdirSync4(
|
|
41587
|
+
mkdirSync4(dirname3(target), { recursive: true });
|
|
41336
41588
|
writeFileSync4(target, asset.content);
|
|
41337
41589
|
out.push({ path: target, changed: true });
|
|
41338
41590
|
}
|
|
41339
41591
|
return out;
|
|
41340
41592
|
}
|
|
41341
41593
|
function linkInstanceEnv(paths) {
|
|
41342
|
-
const link2 =
|
|
41594
|
+
const link2 = join9(paths.dataDir, ".env");
|
|
41343
41595
|
try {
|
|
41344
41596
|
if (lstatSync(link2, { throwIfNoEntry: false }))
|
|
41345
41597
|
unlinkSync2(link2);
|
|
@@ -41348,14 +41600,14 @@ function linkInstanceEnv(paths) {
|
|
|
41348
41600
|
symlinkSync(paths.instanceEnv, link2);
|
|
41349
41601
|
}
|
|
41350
41602
|
function writeVersionStamp(paths, version) {
|
|
41351
|
-
mkdirSync4(
|
|
41603
|
+
mkdirSync4(dirname3(paths.versionFile), { recursive: true });
|
|
41352
41604
|
writeFileSync4(paths.versionFile, `${version}
|
|
41353
41605
|
`);
|
|
41354
41606
|
}
|
|
41355
41607
|
function composeFiles(paths, upstream) {
|
|
41356
|
-
const files = [
|
|
41608
|
+
const files = [join9(paths.dataDir, EMBEDDED_ASSETS.compose.path)];
|
|
41357
41609
|
if (upstream)
|
|
41358
|
-
files.push(
|
|
41610
|
+
files.push(join9(paths.dataDir, EMBEDDED_ASSETS.composeUpstream.path));
|
|
41359
41611
|
return files;
|
|
41360
41612
|
}
|
|
41361
41613
|
|
|
@@ -41420,13 +41672,13 @@ function healthTargets(env4) {
|
|
|
41420
41672
|
}
|
|
41421
41673
|
|
|
41422
41674
|
// packages/cli/src/lib/hook-config.ts
|
|
41423
|
-
import { chmodSync as chmodSync3, mkdirSync as mkdirSync6, readFileSync as
|
|
41424
|
-
import { dirname as
|
|
41675
|
+
import { chmodSync as chmodSync3, mkdirSync as mkdirSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync6 } from "fs";
|
|
41676
|
+
import { dirname as dirname5 } from "path";
|
|
41425
41677
|
|
|
41426
41678
|
// packages/cli/src/lib/instance-env.ts
|
|
41427
41679
|
import { randomBytes } from "crypto";
|
|
41428
|
-
import { chmodSync as chmodSync2, existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as
|
|
41429
|
-
import { dirname as
|
|
41680
|
+
import { chmodSync as chmodSync2, existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "fs";
|
|
41681
|
+
import { dirname as dirname4 } from "path";
|
|
41430
41682
|
var DEFAULT_PORT_BASE = 7650;
|
|
41431
41683
|
var PORT_KEYS = [
|
|
41432
41684
|
"WEBAPI_PORT",
|
|
@@ -41508,7 +41760,7 @@ var HEADER = [
|
|
|
41508
41760
|
var INSTALLER_OWNED = ["APP_TAG"];
|
|
41509
41761
|
function loadOrCreateInstanceEnv(path, opts = {}) {
|
|
41510
41762
|
const generated = buildInstanceEnv(opts);
|
|
41511
|
-
const existing = existsSync7(path) ? parseEnv(
|
|
41763
|
+
const existing = existsSync7(path) ? parseEnv(readFileSync10(path, "utf8")) : {};
|
|
41512
41764
|
const merged = { ...generated, ...existing };
|
|
41513
41765
|
for (const key of INSTALLER_OWNED) {
|
|
41514
41766
|
if (generated[key] !== undefined)
|
|
@@ -41522,12 +41774,12 @@ function loadOrCreateInstanceEnv(path, opts = {}) {
|
|
|
41522
41774
|
return merged;
|
|
41523
41775
|
}
|
|
41524
41776
|
function writeInstanceEnv(path, env4) {
|
|
41525
|
-
mkdirSync5(
|
|
41777
|
+
mkdirSync5(dirname4(path), { recursive: true });
|
|
41526
41778
|
writeFileSync5(path, formatEnv(env4, HEADER));
|
|
41527
41779
|
chmodSync2(path, 384);
|
|
41528
41780
|
}
|
|
41529
41781
|
function updateInstanceEnv(path, patch) {
|
|
41530
|
-
const env4 = existsSync7(path) ? parseEnv(
|
|
41782
|
+
const env4 = existsSync7(path) ? parseEnv(readFileSync10(path, "utf8")) : {};
|
|
41531
41783
|
const next = { ...env4, ...patch };
|
|
41532
41784
|
writeInstanceEnv(path, next);
|
|
41533
41785
|
return next;
|
|
@@ -41692,7 +41944,7 @@ function buildHookConfig(app, env4) {
|
|
|
41692
41944
|
var PRESERVED_KEYS = ["mirrors"];
|
|
41693
41945
|
function readExisting(path) {
|
|
41694
41946
|
try {
|
|
41695
|
-
const parsed = JSON.parse(
|
|
41947
|
+
const parsed = JSON.parse(readFileSync11(path, "utf8"));
|
|
41696
41948
|
return parsed && typeof parsed === "object" ? parsed : {};
|
|
41697
41949
|
} catch {
|
|
41698
41950
|
return {};
|
|
@@ -41708,7 +41960,7 @@ function mergePreserved(next, existing) {
|
|
|
41708
41960
|
}
|
|
41709
41961
|
function writeHookConfig(path, config) {
|
|
41710
41962
|
const merged = mergePreserved(config ?? {}, readExisting(path));
|
|
41711
|
-
mkdirSync6(
|
|
41963
|
+
mkdirSync6(dirname5(path), { recursive: true });
|
|
41712
41964
|
writeFileSync6(path, `${JSON.stringify(merged, null, 2)}
|
|
41713
41965
|
`);
|
|
41714
41966
|
chmodSync3(path, 384);
|
|
@@ -41872,8 +42124,8 @@ async function portsCheck(ports) {
|
|
|
41872
42124
|
}
|
|
41873
42125
|
async function claudeCheck(settingsPath) {
|
|
41874
42126
|
const dir = settingsPath.replace(/\/[^/]+$/, "");
|
|
41875
|
-
const
|
|
41876
|
-
if (
|
|
42127
|
+
const exists = await Bun.file(settingsPath).exists().catch(() => false);
|
|
42128
|
+
if (exists)
|
|
41877
42129
|
return { name: "claude code", status: "ok", detail: `settings at ${settingsPath}` };
|
|
41878
42130
|
return {
|
|
41879
42131
|
name: "claude code",
|
|
@@ -41910,8 +42162,8 @@ var DEV_VERSION = "0.0.0-dev";
|
|
|
41910
42162
|
var VERSION = process.env.CT_VERSION ?? DEV_VERSION;
|
|
41911
42163
|
|
|
41912
42164
|
// packages/cli/src/commands/statusline.ts
|
|
41913
|
-
import { existsSync as existsSync8, mkdirSync as mkdirSync7, readFileSync as
|
|
41914
|
-
import { dirname as
|
|
42165
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync7, readFileSync as readFileSync12, writeFileSync as writeFileSync7 } from "fs";
|
|
42166
|
+
import { dirname as dirname6 } from "path";
|
|
41915
42167
|
|
|
41916
42168
|
// packages/cli/src/lib/statusline.ts
|
|
41917
42169
|
var STALL_AFTER_MS = 60000;
|
|
@@ -41928,6 +42180,11 @@ function whereLabel(t) {
|
|
|
41928
42180
|
const host = t.couchUrl.replace(/^https?:\/\//, "").replace(/\/.*$/, "");
|
|
41929
42181
|
return `${t.sessionsDb}@${host}`;
|
|
41930
42182
|
}
|
|
42183
|
+
function storeState(s, now2) {
|
|
42184
|
+
if (s.lastFailureMs > s.lastWriteMs && now2 - s.lastWriteMs > STALL_AFTER_MS)
|
|
42185
|
+
return "failing";
|
|
42186
|
+
return s.lastWriteMs > 0 ? "healthy" : "idle";
|
|
42187
|
+
}
|
|
41931
42188
|
function renderStatusline(state, now2 = Date.now()) {
|
|
41932
42189
|
if (!state.configured)
|
|
41933
42190
|
return "\u25CB ct off \xB7 no instance configured";
|
|
@@ -41937,21 +42194,41 @@ function renderStatusline(state, now2 = Date.now()) {
|
|
|
41937
42194
|
const c = state.counts;
|
|
41938
42195
|
const toolCalls = c ? Object.values(c.tools).reduce((a, b) => a + b, 0) : 0;
|
|
41939
42196
|
const counts = c ? `${c.events} ev \xB7 ${toolCalls} tools` : "0 ev";
|
|
41940
|
-
const
|
|
41941
|
-
|
|
41942
|
-
|
|
41943
|
-
const
|
|
41944
|
-
|
|
41945
|
-
|
|
41946
|
-
|
|
41947
|
-
|
|
41948
|
-
|
|
42197
|
+
const stores = t.stores?.length ? t.stores : null;
|
|
42198
|
+
if (!stores) {
|
|
42199
|
+
const written = t.lastWriteMs > 0;
|
|
42200
|
+
const failing = t.lastFailureMs > t.lastWriteMs && now2 - t.lastWriteMs > STALL_AFTER_MS;
|
|
42201
|
+
if (failing) {
|
|
42202
|
+
const last2 = written ? `last write ${ago(t.lastWriteMs, now2)}` : "no write landed";
|
|
42203
|
+
return `\u25D0 ct stalled \xB7 ${counts} \xB7 ${last2} \u2192 ${whereLabel(t)}`;
|
|
42204
|
+
}
|
|
42205
|
+
if (!written)
|
|
42206
|
+
return `\u25CC ct ready \xB7 ${counts} \xB7 no write yet \u2192 ${whereLabel(t)}`;
|
|
42207
|
+
return `\u25CF ct rec \xB7 ${counts} \xB7 ${ago(t.lastWriteMs, now2)} \u2192 ${whereLabel(t)}`;
|
|
42208
|
+
}
|
|
42209
|
+
const direct = stores.find((s) => s.kind === "direct") ?? stores[0];
|
|
42210
|
+
const headline = direct ?? { label: whereLabel(t), lastWriteMs: 0, lastFailureMs: 0 };
|
|
42211
|
+
if (direct && storeState(direct, now2) === "healthy") {
|
|
42212
|
+
return `\u25CF ct rec \xB7 ${counts} \xB7 ${ago(direct.lastWriteMs, now2)} \u2192 ${direct.label}`;
|
|
42213
|
+
}
|
|
42214
|
+
const healthyMirrors = stores.filter((s) => s !== direct && storeState(s, now2) === "healthy");
|
|
42215
|
+
const first = healthyMirrors[0];
|
|
42216
|
+
if (first) {
|
|
42217
|
+
const more = healthyMirrors.length > 1 ? ` +${healthyMirrors.length - 1}` : "";
|
|
42218
|
+
return `\u25CF ct rec (mirror) \xB7 ${counts} \xB7 ${ago(first.lastWriteMs, now2)} \u2192 ${first.label}${more}`;
|
|
42219
|
+
}
|
|
42220
|
+
if (stores.some((s) => storeState(s, now2) === "failing")) {
|
|
42221
|
+
const best = Math.max(...stores.map((s) => s.lastWriteMs));
|
|
42222
|
+
const last2 = best > 0 ? `last write ${ago(best, now2)}` : "no write landed";
|
|
42223
|
+
return `\u25D0 ct stalled \xB7 ${counts} \xB7 ${last2} \u2192 ${headline.label}`;
|
|
42224
|
+
}
|
|
42225
|
+
return `\u25CC ct ready \xB7 ${counts} \xB7 no write yet \u2192 ${headline.label}`;
|
|
41949
42226
|
}
|
|
41950
42227
|
|
|
41951
42228
|
// packages/cli/src/commands/statusline.ts
|
|
41952
42229
|
function readSettings2(path) {
|
|
41953
42230
|
try {
|
|
41954
|
-
return JSON.parse(
|
|
42231
|
+
return JSON.parse(readFileSync12(path, "utf8"));
|
|
41955
42232
|
} catch {
|
|
41956
42233
|
return {};
|
|
41957
42234
|
}
|
|
@@ -42008,7 +42285,7 @@ function install2(argv) {
|
|
|
42008
42285
|
console.log(` ${JSON.stringify(next)}`);
|
|
42009
42286
|
return 0;
|
|
42010
42287
|
}
|
|
42011
|
-
mkdirSync7(
|
|
42288
|
+
mkdirSync7(dirname6(paths.claudeSettings), { recursive: true });
|
|
42012
42289
|
writeFileSync7(paths.claudeSettings, `${JSON.stringify({ ...settings, statusLine: next }, null, 2)}
|
|
42013
42290
|
`);
|
|
42014
42291
|
console.log(`statusline: registered \u2192 ${paths.claudeSettings}`);
|
|
@@ -42128,7 +42405,7 @@ async function runInstall(argv) {
|
|
|
42128
42405
|
const noPrune = options["no-prune"] === true;
|
|
42129
42406
|
const portBaseOpt = strOpt(options, "port-base");
|
|
42130
42407
|
const paths = installPaths();
|
|
42131
|
-
const priorAppTag = existsSync9(paths.instanceEnv) ? /^APP_TAG=(\S+)\s*$/m.exec(
|
|
42408
|
+
const priorAppTag = existsSync9(paths.instanceEnv) ? /^APP_TAG=(\S+)\s*$/m.exec(readFileSync13(paths.instanceEnv, "utf8"))?.[1] : undefined;
|
|
42132
42409
|
const TOTAL = 8;
|
|
42133
42410
|
console.log(`claude-transcripts install (${VERSION})`);
|
|
42134
42411
|
console.log(` config ${paths.configDir}`);
|
|
@@ -42615,18 +42892,18 @@ async function runSessions(argv) {
|
|
|
42615
42892
|
}
|
|
42616
42893
|
|
|
42617
42894
|
// packages/cli/src/commands/setup.ts
|
|
42618
|
-
import { existsSync as existsSync11, mkdirSync as mkdirSync8, readFileSync as
|
|
42895
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync8, readFileSync as readFileSync14, writeFileSync as writeFileSync8 } from "fs";
|
|
42619
42896
|
import { homedir as homedir3 } from "os";
|
|
42620
|
-
import { dirname as
|
|
42621
|
-
var REPO_ROOT =
|
|
42622
|
-
var HOOK_DIR =
|
|
42623
|
-
var HOOK_CONFIG_PATH = process.env.CT_HOOK_CONFIG ??
|
|
42624
|
-
var GLOBAL_SETTINGS =
|
|
42625
|
-
var PROJECT_SETTINGS =
|
|
42897
|
+
import { dirname as dirname7, join as join10 } from "path";
|
|
42898
|
+
var REPO_ROOT = join10(import.meta.dir, "..", "..", "..", "..");
|
|
42899
|
+
var HOOK_DIR = join10(REPO_ROOT, "hooks");
|
|
42900
|
+
var HOOK_CONFIG_PATH = process.env.CT_HOOK_CONFIG ?? join10(homedir3(), ".config", "claude-transcripts", "config.json");
|
|
42901
|
+
var GLOBAL_SETTINGS = join10(homedir3(), ".claude", "settings.json");
|
|
42902
|
+
var PROJECT_SETTINGS = join10(process.cwd(), ".claude", "settings.json");
|
|
42626
42903
|
function loadRepoConfig() {
|
|
42627
|
-
const live =
|
|
42628
|
-
const template2 =
|
|
42629
|
-
return JSON.parse(
|
|
42904
|
+
const live = join10(REPO_ROOT, "config", "config.json");
|
|
42905
|
+
const template2 = join10(REPO_ROOT, "config", "config.template.json");
|
|
42906
|
+
return JSON.parse(readFileSync14(existsSync11(live) ? live : template2, "utf8"));
|
|
42630
42907
|
}
|
|
42631
42908
|
function buildHookConfig2(cfg) {
|
|
42632
42909
|
const env4 = process.env;
|
|
@@ -42693,13 +42970,13 @@ async function probeGarage(blob) {
|
|
|
42693
42970
|
}
|
|
42694
42971
|
function readSettings3(path) {
|
|
42695
42972
|
try {
|
|
42696
|
-
return JSON.parse(
|
|
42973
|
+
return JSON.parse(readFileSync14(path, "utf8"));
|
|
42697
42974
|
} catch {
|
|
42698
42975
|
return {};
|
|
42699
42976
|
}
|
|
42700
42977
|
}
|
|
42701
42978
|
function dispatchCommand() {
|
|
42702
|
-
return `bun run ${
|
|
42979
|
+
return `bun run ${join10(HOOK_DIR, "scripts", "dispatch.ts")}`;
|
|
42703
42980
|
}
|
|
42704
42981
|
function isRegistered(path) {
|
|
42705
42982
|
const cmd = dispatchCommand();
|
|
@@ -42708,7 +42985,7 @@ function isRegistered(path) {
|
|
|
42708
42985
|
}
|
|
42709
42986
|
function registerGlobal(dryRun) {
|
|
42710
42987
|
const cmd = dispatchCommand();
|
|
42711
|
-
const generated = JSON.parse(
|
|
42988
|
+
const generated = JSON.parse(readFileSync14(join10(HOOK_DIR, "hooks", "hooks.json"), "utf8"));
|
|
42712
42989
|
const settings = readSettings3(GLOBAL_SETTINGS);
|
|
42713
42990
|
const hooks2 = settings.hooks ?? {};
|
|
42714
42991
|
for (const [event, groups] of Object.entries(generated.hooks)) {
|
|
@@ -42723,7 +43000,7 @@ function registerGlobal(dryRun) {
|
|
|
42723
43000
|
console.log(` [check] would register ${Object.keys(generated.hooks).length} events in ${GLOBAL_SETTINGS}`);
|
|
42724
43001
|
return;
|
|
42725
43002
|
}
|
|
42726
|
-
mkdirSync8(
|
|
43003
|
+
mkdirSync8(dirname7(GLOBAL_SETTINGS), { recursive: true });
|
|
42727
43004
|
writeFileSync8(GLOBAL_SETTINGS, `${JSON.stringify({ ...settings, hooks: hooks2 }, null, 2)}
|
|
42728
43005
|
`);
|
|
42729
43006
|
console.log(` registered ${Object.keys(generated.hooks).length} events (system-wide) in ${GLOBAL_SETTINGS}`);
|
package/package.json
CHANGED