@darkhunt-security/endpoint-codex 0.9.16 → 0.9.18

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "darkhunt-guard",
3
- "version": "0.9.16",
3
+ "version": "0.9.18",
4
4
  "hooks": "./hooks/hooks.json",
5
5
  "description": "Darkhunt endpoint plugin for Codex CLI \u2014 full session trace capture and PreToolUse guardrails.",
6
6
  "author": {
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: Import Codex CLI sessions that happened before this machine was connected
3
- argument-hint: '[--days=N, transcript paths, or nothing for everything on disk]'
3
+ argument-hint: '[--days=N, --reset, transcript paths, or nothing for everything on disk]'
4
4
  allowed-tools: Bash(${CLAUDE_PLUGIN_ROOT}/dist/bin/backfill.mjs:*), Bash(${CLAUDE_PLUGIN_ROOT}/dist/bin/status.mjs:*)
5
5
  ---
6
6
 
@@ -64,6 +64,60 @@ cannot be taken back and looks exactly like a correct one afterwards.
64
64
  a failed pass holds every checkpoint, so the identical command re-ships the same
65
65
  byte range once the cause is fixed.
66
66
 
67
+ ## When it ships nothing
68
+
69
+ `0 record(s) from 0 transcript(s)` with exit 0 has two very different causes, and they
70
+ read identically:
71
+
72
+ - **Already imported.** The normal case. Re-running a backfill is a no-op by design.
73
+ - **Re-enrolled, and the history went to the old application.** A checkpoint records how
74
+ far a transcript has been shipped — never _where_ it was shipped to. After pointing
75
+ this machine at a different tenant, workspace or application, every transcript is
76
+ still marked done, so a backfill correctly finds nothing to send and the new
77
+ application stays empty.
78
+
79
+ The status block above tells you which one you are in: if it names an application the
80
+ sessions were never shipped to, it is the second.
81
+
82
+ `--reset` is the lever for that case. It forgets the offsets for the transcripts you
83
+ selected — the same selection `--days` and paths make — and ships them again from the
84
+ start:
85
+
86
+ ```
87
+ ${CLAUDE_PLUGIN_ROOT}/dist/bin/backfill.mjs --reset --days=7
88
+ ```
89
+
90
+ Before running it:
91
+
92
+ - **Confirm the destination again.** This is the full send that the first backfill was
93
+ not, and it is one-way.
94
+ - **Say how big it is.** `--dry-run` (without `--reset`; the two are refused together)
95
+ maps the same selection and prints the record count. That count is what `--reset` will
96
+ actually ship — without `--reset` it is only the size of the window, not the size of
97
+ the shipment.
98
+ - **Do not offer it as a fix for a failed run.** A failed pass already holds its
99
+ checkpoints; re-running the plain command re-ships that byte range. `--reset` is for a
100
+ run that succeeded and sent nothing.
101
+
102
+ It writes the old offsets to a `.bak-<timestamp>-prereset` file beside the checkpoints
103
+ and prints the path. Records already in the old application stay there — this adds a
104
+ copy in the new one, it does not move anything.
105
+
106
+ ## When the machine has not opted in
107
+
108
+ **The retroactive import is off by default.** Most machines are in this state, so expect
109
+ it rather than treating it as a fault.
110
+
111
+ Exit **4**, with `BACKFILL DISABLED` on stderr, means the config has no
112
+ `"capture": { "backfill": true }`. Nothing was sent and no checkpoint was cleared — a
113
+ refused `--reset` has not moved the machine either, so there is nothing to undo and
114
+ nothing to retry.
115
+
116
+ Tell them plainly what it is: the import is opt-in, and this machine has not opted in.
117
+ Do not edit the config yourself to get around it — send them to `/darkhunt-guard:setup`.
118
+ `--dry-run` still works while it is off, so you can still show them exactly what enabling
119
+ it would send, which is the more useful thing to offer anyway.
120
+
67
121
  ## Also
68
122
 
69
123
  - Narrow it by passing paths — one session, or a date range — rather than adding flags
@@ -34,5 +34,10 @@ What the fields mean, and what to say:
34
34
  export is being rejected — check `last pass` to tell those apart.
35
35
  - **last pass FAILED** — read out the cause. Checkpoints are held on failure, so nothing
36
36
  has been lost; it will re-ship once the cause is fixed.
37
+ - **held N transcript(s) waiting on the platform** — a pass could not reach Darkhunt, so
38
+ those sessions are queued on this machine with their checkpoints held. The forwarder
39
+ retries on its own for about an hour after a failure, and every later hook retries
40
+ them too, so this clears itself once the platform answers. Say since when they have
41
+ been held; nothing is lost while it is non-zero.
37
42
 
38
43
  Do not run anything else, and do not offer to change the configuration unless asked.
@@ -18160,6 +18160,23 @@ function settingsFor(file, vendor) {
18160
18160
  ...file.enforce ?? override.enforce ? { enforce: { ...file.enforce, ...override.enforce } } : {}
18161
18161
  };
18162
18162
  }
18163
+ function loadRuntimeSettings(vendor, options = {}) {
18164
+ const vendorFile = readConfigFile(vendor);
18165
+ const profile = resolveProfile(options.profile, vendorFile.profile);
18166
+ return {
18167
+ profile,
18168
+ scope: scopeKey(vendor, profile),
18169
+ enabled: vendorFile.enabled ?? true,
18170
+ capture: resolveCapture(vendorFile.capture),
18171
+ enforce: {
18172
+ mode: vendorFile.enforce?.mode ?? "off",
18173
+ failClosed: vendorFile.enforce?.failClosed ?? true
18174
+ }
18175
+ };
18176
+ }
18177
+ function resolveCapture(capture) {
18178
+ return { enabled: capture?.enabled ?? true, backfill: capture?.backfill ?? false };
18179
+ }
18163
18180
  function readConfigFile(vendor) {
18164
18181
  if (!existsSync(configPath())) {
18165
18182
  throw new ConfigMissingError(`${configPath()} does not exist \u2014 run init.mjs to create it`);
@@ -18212,7 +18229,7 @@ function loadLocalConfig(vendor, options = {}) {
18212
18229
  applicationId,
18213
18230
  ...(creds.userId ?? vendorFile.userId) !== void 0 ? { userId: creds.userId ?? vendorFile.userId } : {},
18214
18231
  enabled: vendorFile.enabled ?? true,
18215
- capture: { enabled: vendorFile.capture?.enabled ?? true },
18232
+ capture: resolveCapture(vendorFile.capture),
18216
18233
  enforce: {
18217
18234
  mode: vendorFile.enforce?.mode ?? "off",
18218
18235
  failClosed: vendorFile.enforce?.failClosed ?? true
@@ -18257,6 +18274,9 @@ function liveTranscripts(beat, now = Date.now()) {
18257
18274
  return [...paths];
18258
18275
  }
18259
18276
 
18277
+ // packages/core/dist/emit/emitter.js
18278
+ import { createHash as createHash2 } from "node:crypto";
18279
+
18260
18280
  // packages/core/dist/emit/ids.js
18261
18281
  import { createHash, randomBytes } from "node:crypto";
18262
18282
  var SPAN_ID_HEX = 16;
@@ -18296,6 +18316,20 @@ var SeededIdGenerator = class {
18296
18316
  };
18297
18317
 
18298
18318
  // packages/core/dist/emit/emitter.js
18319
+ var PAYLOAD_LIMIT = 128 * 1024;
18320
+ function capPayload(value, limit = PAYLOAD_LIMIT) {
18321
+ if (value === void 0 || value === null)
18322
+ return value;
18323
+ const text = typeof value === "string" ? value : JSON.stringify(value);
18324
+ if (text === void 0 || text.length <= limit)
18325
+ return value;
18326
+ const digest = createHash2("sha256").update(text).digest("hex").slice(0, 16);
18327
+ const marker = `\u2026 [truncated by darkhunt: ${text.length} chars total, sha256:${digest}]`;
18328
+ return `${text.slice(0, Math.max(0, limit - marker.length))}${marker}`;
18329
+ }
18330
+ function capText(text, limit = PAYLOAD_LIMIT) {
18331
+ return capPayload(text, limit);
18332
+ }
18299
18333
  var TOOL_SUMMARY_LIMIT = 2e3;
18300
18334
  function summarize(output) {
18301
18335
  const text = typeof output === "string" ? output : JSON.stringify(output ?? null);
@@ -18422,7 +18456,7 @@ var SessionEmitter = class {
18422
18456
  // A subagent's first record is the task it was given, and it has already been
18423
18457
  // absorbed by the time this fires — so the child trace opens with its brief
18424
18458
  // rather than empty.
18425
- ...this.lastUserText ? { input: this.lastUserText } : {},
18459
+ ...this.lastUserText ? { input: capText(this.lastUserText) } : {},
18426
18460
  ...this.options.userId !== void 0 ? { userId: this.options.userId } : {}
18427
18461
  }));
18428
18462
  this.sidechains.push(owner);
@@ -18463,7 +18497,7 @@ var SessionEmitter = class {
18463
18497
  case "thinking": {
18464
18498
  this.openGeneration(record, owner);
18465
18499
  const host = this.generation ?? owner;
18466
- this.seeded(["thinking", record.uuid], () => host.span("thinking", { startTime: record.ts, output: record.text })).end({ endTime: record.ts });
18500
+ this.seeded(["thinking", record.uuid], () => host.span("thinking", { startTime: record.ts, output: capPayload(record.text) })).end({ endTime: record.ts });
18467
18501
  break;
18468
18502
  }
18469
18503
  case "tool_call": {
@@ -18474,8 +18508,8 @@ var SessionEmitter = class {
18474
18508
  startTime: record.ts,
18475
18509
  ...record.toolName !== void 0 ? { toolName: record.toolName } : {},
18476
18510
  ...record.toolCallId !== void 0 ? { toolCallId: record.toolCallId } : {},
18477
- toolArguments: record.input,
18478
- input: record.input
18511
+ toolArguments: capPayload(record.input),
18512
+ input: capPayload(record.input)
18479
18513
  }));
18480
18514
  if (record.toolCallId)
18481
18515
  this.toolSpans.set(record.toolCallId, span);
@@ -18487,7 +18521,7 @@ var SessionEmitter = class {
18487
18521
  const span = record.toolCallId ? this.toolSpans.get(record.toolCallId) : void 0;
18488
18522
  if (span) {
18489
18523
  span.end({
18490
- output: record.output,
18524
+ output: capPayload(record.output),
18491
18525
  endTime: record.ts,
18492
18526
  ...record.isError ? { level: "ERROR" } : {},
18493
18527
  ...record.errorKind !== void 0 ? { statusMessage: record.errorKind } : {}
@@ -18501,7 +18535,7 @@ var SessionEmitter = class {
18501
18535
  this.endGeneration();
18502
18536
  this.seeded(["compaction", record.uuid], () => owner.span("compaction", {
18503
18537
  startTime: record.ts,
18504
- ...record.text ? { output: record.text } : {}
18538
+ ...record.text ? { output: capPayload(record.text) } : {}
18505
18539
  })).end({ endTime: record.ts });
18506
18540
  break;
18507
18541
  case "permission_grant": {
@@ -18563,7 +18597,7 @@ var SessionEmitter = class {
18563
18597
  ...this.title !== void 0 ? { name: this.title } : {},
18564
18598
  // The pass's result. Paired with the `input` set when the trace opened, this is
18565
18599
  // what stops the consumer inferring the trace's I/O from a descendant.
18566
- ...this.lastAssistantText !== void 0 ? { output: this.lastAssistantText } : {}
18600
+ ...this.lastAssistantText !== void 0 ? { output: capText(this.lastAssistantText) } : {}
18567
18601
  });
18568
18602
  this.trace.end();
18569
18603
  this.trace = void 0;
@@ -18646,7 +18680,7 @@ var SessionEmitter = class {
18646
18680
  // What the agent was asked. `TraceArgs.input` is construction-only — the SDK's
18647
18681
  // `update()` carries output alone — which is why `ingest` absorbs the user turn
18648
18682
  // before it calls this.
18649
- ...this.lastUserText ? { input: this.lastUserText } : {},
18683
+ ...this.lastUserText ? { input: capText(this.lastUserText) } : {},
18650
18684
  tags: [this.options.vendor, ...this.options.agentId ? ["subagent"] : []],
18651
18685
  metadata: {
18652
18686
  ...this.meta,
@@ -18715,7 +18749,7 @@ var SessionEmitter = class {
18715
18749
  buildInputMessages() {
18716
18750
  const messages = [];
18717
18751
  if (this.lastUserText !== void 0) {
18718
- messages.push({ role: "user", content: this.lastUserText });
18752
+ messages.push({ role: "user", content: capText(this.lastUserText) });
18719
18753
  }
18720
18754
  for (const result2 of this.pendingToolResults) {
18721
18755
  messages.push({ role: "tool", content: result2 });
@@ -18725,7 +18759,7 @@ var SessionEmitter = class {
18725
18759
  endGeneration() {
18726
18760
  if (!this.generation)
18727
18761
  return;
18728
- const output = this.outputText.join("\n");
18762
+ const output = capText(this.outputText.join("\n"));
18729
18763
  if (output)
18730
18764
  this.lastAssistantText = output;
18731
18765
  this.generation.end({
@@ -18754,8 +18788,9 @@ function installSeededIdGenerator(client) {
18754
18788
  }
18755
18789
 
18756
18790
  // packages/core/dist/forwarder/checkpoint.js
18757
- import { mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync3, renameSync as renameSync2, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
18791
+ import { copyFileSync, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync3, renameSync as renameSync2, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
18758
18792
  import { join as join6 } from "node:path";
18793
+ var SPOOL_KEY = "::spool::";
18759
18794
  function checkpointPath(scope) {
18760
18795
  return join6(CONFIG_DIR, `${scope}.checkpoints.json`);
18761
18796
  }
@@ -18766,6 +18801,25 @@ function loadCheckpoints(scope) {
18766
18801
  return {};
18767
18802
  }
18768
18803
  }
18804
+ function laggingTranscripts(checkpoints, now = statSize) {
18805
+ const paths = [];
18806
+ for (const [path, checkpoint] of Object.entries(checkpoints)) {
18807
+ if (path === SPOOL_KEY)
18808
+ continue;
18809
+ const size = now(path);
18810
+ if (size === void 0 || size === checkpoint.offset)
18811
+ continue;
18812
+ paths.push(path);
18813
+ }
18814
+ return paths;
18815
+ }
18816
+ function statSize(path) {
18817
+ try {
18818
+ return statSync2(path).size;
18819
+ } catch {
18820
+ return void 0;
18821
+ }
18822
+ }
18769
18823
  function orphanedScopes(vendor, scope) {
18770
18824
  const suffix = ".checkpoints.json";
18771
18825
  try {
@@ -18800,6 +18854,26 @@ function resumeTurn(checkpoints, path, from) {
18800
18854
  return void 0;
18801
18855
  return checkpoints[path]?.turn;
18802
18856
  }
18857
+ function clearCheckpoints(scope, paths) {
18858
+ const checkpoints = loadCheckpoints(scope);
18859
+ const wanted = paths === void 0 ? void 0 : new Set(paths);
18860
+ const transcripts = Object.keys(checkpoints).filter((key) => key !== SPOOL_KEY);
18861
+ const cleared = transcripts.filter((key) => wanted === void 0 || wanted.has(key));
18862
+ const total = transcripts.length;
18863
+ if (cleared.length === 0)
18864
+ return { cleared, total };
18865
+ const backup = `${checkpointPath(scope)}.bak-${stamp(/* @__PURE__ */ new Date())}-prereset`;
18866
+ copyFileSync(checkpointPath(scope), backup);
18867
+ const kept = { ...checkpoints };
18868
+ for (const key of cleared)
18869
+ delete kept[key];
18870
+ saveCheckpoints(scope, kept);
18871
+ return { cleared, total, backup };
18872
+ }
18873
+ function stamp(at) {
18874
+ const pad = (n) => String(n).padStart(2, "0");
18875
+ return `${at.getFullYear()}${pad(at.getMonth() + 1)}${pad(at.getDate())}-${pad(at.getHours())}${pad(at.getMinutes())}${pad(at.getSeconds())}`;
18876
+ }
18803
18877
 
18804
18878
  // packages/core/dist/forwarder/tail.js
18805
18879
  import { closeSync, openSync, readSync, statSync as statSync3 } from "node:fs";
@@ -18849,6 +18923,7 @@ function readNewLines(path, offset, maxBytes = DEFAULT_MAX_BYTES) {
18849
18923
  import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync4, rmSync, writeFileSync as writeFileSync3 } from "node:fs";
18850
18924
  import { join as join7 } from "node:path";
18851
18925
  var STALE_MS = 5 * 60 * 1e3;
18926
+ var HEARTBEAT_MS = 60 * 1e3;
18852
18927
  function acquireLock(scope) {
18853
18928
  mkdirSync3(CONFIG_DIR, { recursive: true, mode: 448 });
18854
18929
  const path = join7(CONFIG_DIR, `${scope}.forwarder.lock`);
@@ -18870,7 +18945,17 @@ function acquireLock(scope) {
18870
18945
  } catch {
18871
18946
  return null;
18872
18947
  }
18873
- return () => rmSync(path, { force: true });
18948
+ const beat = setInterval(() => {
18949
+ try {
18950
+ writeFileSync3(path, JSON.stringify({ pid: process.pid, ts: Date.now() }), { mode: 384 });
18951
+ } catch {
18952
+ }
18953
+ }, HEARTBEAT_MS);
18954
+ beat.unref();
18955
+ return () => {
18956
+ clearInterval(beat);
18957
+ rmSync(path, { force: true });
18958
+ };
18874
18959
  }
18875
18960
  function isAlive(pid, kill = (p, s) => {
18876
18961
  process.kill(p, s);
@@ -18971,6 +19056,61 @@ function saveHealth(scope, health) {
18971
19056
  }
18972
19057
  }
18973
19058
 
19059
+ // packages/core/dist/forwarder/pending.js
19060
+ import { existsSync as existsSync3, mkdirSync as mkdirSync5, readFileSync as readFileSync6, renameSync as renameSync4, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "node:fs";
19061
+ import { join as join9 } from "node:path";
19062
+ var MAX_PENDING_PATHS = 256;
19063
+ function pendingPath(scope) {
19064
+ return join9(CONFIG_DIR, `${scope}.pending.json`);
19065
+ }
19066
+ function loadPending(scope) {
19067
+ try {
19068
+ const backlog = JSON.parse(readFileSync6(pendingPath(scope), "utf8"));
19069
+ if (!Array.isArray(backlog.paths))
19070
+ return void 0;
19071
+ return backlog;
19072
+ } catch {
19073
+ return void 0;
19074
+ }
19075
+ }
19076
+ function pendingTranscripts(scope) {
19077
+ const backlog = loadPending(scope);
19078
+ if (!backlog)
19079
+ return [];
19080
+ return backlog.paths.filter((path) => {
19081
+ try {
19082
+ return existsSync3(path);
19083
+ } catch {
19084
+ return false;
19085
+ }
19086
+ });
19087
+ }
19088
+ function savePending(scope, paths, now = /* @__PURE__ */ new Date()) {
19089
+ try {
19090
+ const at = now.toISOString();
19091
+ const previous = loadPending(scope);
19092
+ const merged = [.../* @__PURE__ */ new Set([...previous?.paths ?? [], ...paths])];
19093
+ const kept = merged.slice(Math.max(0, merged.length - MAX_PENDING_PATHS));
19094
+ if (kept.length === 0)
19095
+ return;
19096
+ write(scope, { since: previous?.since ?? at, at, paths: kept });
19097
+ } catch {
19098
+ }
19099
+ }
19100
+ function clearPending(scope) {
19101
+ try {
19102
+ rmSync2(pendingPath(scope), { force: true });
19103
+ } catch {
19104
+ }
19105
+ }
19106
+ function write(scope, backlog) {
19107
+ mkdirSync5(CONFIG_DIR, { recursive: true, mode: 448 });
19108
+ const target = pendingPath(scope);
19109
+ const tmp = `${target}.tmp`;
19110
+ writeFileSync5(tmp, JSON.stringify(backlog), { mode: 384 });
19111
+ renameSync4(tmp, target);
19112
+ }
19113
+
18974
19114
  // packages/core/dist/forwarder/run.js
18975
19115
  import { readdirSync as readdirSync2, statSync as statSync4 } from "node:fs";
18976
19116
 
@@ -21694,7 +21834,6 @@ function toFloat(value, fallback) {
21694
21834
  }
21695
21835
 
21696
21836
  // packages/core/dist/forwarder/run.js
21697
- var SPOOL_KEY = "::spool::";
21698
21837
  var DARKHUNT_SOURCE_HINT = "darkhunt-telemetry";
21699
21838
  function drainSpool(mapper, checkpoints) {
21700
21839
  const path = spoolPath(mapper.vendor);
@@ -21807,7 +21946,14 @@ async function runForwarder(mapper, options = {}) {
21807
21946
  }
21808
21947
  }
21809
21948
  const drain = options.paths ? { paths: options.paths } : drainSpool(mapper, checkpoints);
21810
- const named = options.paths ? drain.paths : [.../* @__PURE__ */ new Set([...drain.paths, ...liveTranscripts(loadBeat(config.scope))])];
21949
+ const named = options.paths ? drain.paths : [
21950
+ .../* @__PURE__ */ new Set([
21951
+ ...drain.paths,
21952
+ ...liveTranscripts(loadBeat(config.scope)),
21953
+ ...pendingTranscripts(config.scope),
21954
+ ...laggingTranscripts(checkpoints)
21955
+ ])
21956
+ ];
21811
21957
  const paths = [...new Set(named.flatMap((path) => [path, ...related(mapper, path)]))];
21812
21958
  if (paths.length === 0) {
21813
21959
  if (drain.checkpoint)
@@ -21904,13 +22050,16 @@ async function runForwarder(mapper, options = {}) {
21904
22050
  result2.emitted = { transcripts: result2.transcripts, records: result2.records };
21905
22051
  if (watch.failures() > 0) {
21906
22052
  result2.ok = false;
22053
+ result2.retryable = true;
21907
22054
  result2.error = watch.reason() ?? "span export failed";
21908
22055
  result2.transcripts = 0;
21909
22056
  result2.records = 0;
22057
+ savePending(config.scope, paths);
21910
22058
  } else {
21911
22059
  if (drain.checkpoint)
21912
22060
  staged[SPOOL_KEY] = drain.checkpoint;
21913
22061
  saveCheckpoints(config.scope, staged);
22062
+ clearPending(config.scope);
21914
22063
  }
21915
22064
  const previous = loadHealth(config.scope);
21916
22065
  saveHealth(config.scope, {
@@ -21975,16 +22124,22 @@ function discoverTranscripts(mapper) {
21975
22124
  return found;
21976
22125
  }
21977
22126
 
22127
+ // packages/core/dist/forwarder/retry.js
22128
+ var CLAIM_STALE_MS = 45 * 60 * 1e3;
22129
+
21978
22130
  // packages/core/dist/cli/status.js
21979
22131
  var SESSION_HOOK_STALE_MS = 30 * 60 * 1e3;
21980
22132
 
21981
22133
  // packages/core/dist/cli/enroll.js
21982
22134
  import { homedir as homedir3 } from "node:os";
21983
- import { join as join9 } from "node:path";
21984
- var CLI_CREDENTIALS_PATH = join9(homedir3(), ".darkhunt", "credentials.json");
22135
+ import { join as join10 } from "node:path";
22136
+ var CLI_CREDENTIALS_PATH = join10(homedir3(), ".darkhunt", "credentials.json");
21985
22137
 
21986
22138
  // packages/core/dist/cli/backfill.js
21987
- var BOOLEAN_FLAGS = /* @__PURE__ */ new Set(["dry-run"]);
22139
+ var BOOLEAN_FLAGS = /* @__PURE__ */ new Map([
22140
+ ["dry-run", "dryRun"],
22141
+ ["reset", "reset"]
22142
+ ]);
21988
22143
  var VALUE_FLAGS = /* @__PURE__ */ new Set(["profile", "credentials", "days", "batch"]);
21989
22144
  var DEFAULT_BATCH = 25;
21990
22145
  function usage(command) {
@@ -21992,6 +22147,8 @@ function usage(command) {
21992
22147
  `usage: ${command} [options] [paths...]`,
21993
22148
  "",
21994
22149
  " --dry-run map only: no network, no checkpoints",
22150
+ " --reset re-send: forget how far the selected transcripts have",
22151
+ " been shipped, then ship them from the start again",
21995
22152
  " --days=N only transcripts written to in the last N days",
21996
22153
  " --batch=N transcripts per pass (default " + String(DEFAULT_BATCH) + ")",
21997
22154
  " --profile=NAME endpoint profile to ship to",
@@ -21999,13 +22156,22 @@ function usage(command) {
21999
22156
  " -h, --help this message",
22000
22157
  "",
22001
22158
  " With no paths this ships EVERY transcript on disk. Narrow it with --days",
22002
- " or by naming transcripts, and confirm the destination with --dry-run first."
22159
+ " or by naming transcripts, and confirm the destination with --dry-run first.",
22160
+ "",
22161
+ " A checkpoint records how far a transcript has been shipped, never where it was",
22162
+ " shipped to. After re-enrolling against a different application, a plain backfill",
22163
+ " finds nothing to send and exits 0; --reset is what re-sends it.",
22164
+ "",
22165
+ " This command is refused, exit 4, when the config sets",
22166
+ ' "capture": { "backfill": false }',
22167
+ " which stops retroactive imports on this machine and leaves live capture running.",
22168
+ " --dry-run still works while it is off."
22003
22169
  ].join("\n");
22004
22170
  }
22005
22171
  function parseBackfillArgs(argv) {
22006
22172
  const paths = [];
22007
22173
  const values = /* @__PURE__ */ new Map();
22008
- let dryRun = false;
22174
+ const booleans = { dryRun: false, reset: false };
22009
22175
  for (const arg of argv) {
22010
22176
  if (arg === "-h" || arg === "--help")
22011
22177
  return { kind: "help" };
@@ -22016,10 +22182,11 @@ function parseBackfillArgs(argv) {
22016
22182
  const body = arg.slice(2);
22017
22183
  const eq = body.indexOf("=");
22018
22184
  const name = eq === -1 ? body : body.slice(0, eq);
22019
- if (BOOLEAN_FLAGS.has(name)) {
22185
+ const boolean = BOOLEAN_FLAGS.get(name);
22186
+ if (boolean !== void 0) {
22020
22187
  if (eq !== -1)
22021
22188
  return { kind: "error", message: `--${name} takes no value` };
22022
- dryRun = true;
22189
+ booleans[boolean] = true;
22023
22190
  continue;
22024
22191
  }
22025
22192
  if (VALUE_FLAGS.has(name)) {
@@ -22030,7 +22197,7 @@ function parseBackfillArgs(argv) {
22030
22197
  }
22031
22198
  return {
22032
22199
  kind: "error",
22033
- message: `unknown option '${arg}'. Known: ${[...BOOLEAN_FLAGS, ...VALUE_FLAGS].map((f) => `--${f}`).sort().join(", ")}, --help`
22200
+ message: `unknown option '${arg}'. Known: ${[...BOOLEAN_FLAGS.keys(), ...VALUE_FLAGS].map((f) => `--${f}`).sort().join(", ")}, --help`
22034
22201
  };
22035
22202
  }
22036
22203
  const positive = (name) => {
@@ -22043,6 +22210,12 @@ function parseBackfillArgs(argv) {
22043
22210
  }
22044
22211
  return n;
22045
22212
  };
22213
+ if (booleans.dryRun && booleans.reset) {
22214
+ return {
22215
+ kind: "error",
22216
+ message: "--reset cannot be combined with --dry-run: a dry run maps every transcript from the start already, and never writes checkpoints"
22217
+ };
22218
+ }
22046
22219
  const sinceDays = positive("days");
22047
22220
  if (typeof sinceDays === "object")
22048
22221
  return sinceDays;
@@ -22052,7 +22225,8 @@ function parseBackfillArgs(argv) {
22052
22225
  return {
22053
22226
  kind: "run",
22054
22227
  options: {
22055
- dryRun,
22228
+ dryRun: booleans.dryRun,
22229
+ reset: booleans.reset,
22056
22230
  paths,
22057
22231
  ...values.get("profile") !== void 0 ? { profile: values.get("profile") } : {},
22058
22232
  ...values.get("credentials") !== void 0 ? { credentialsPath: values.get("credentials") } : {},
@@ -22068,10 +22242,21 @@ function batches(items, size) {
22068
22242
  return out;
22069
22243
  }
22070
22244
  async function runBackfill(mapper, options, say2) {
22245
+ if (!options.dryRun && backfillIsOff(mapper, options)) {
22246
+ say2("BACKFILL DISABLED \u2014 the retroactive import is opt-in, and this machine has not opted in");
22247
+ say2(`set "capture": { "backfill": true } in ${configPath()} to allow it`);
22248
+ say2("nothing was sent and no checkpoint was cleared; live capture is unaffected");
22249
+ return disabledResult();
22250
+ }
22071
22251
  const discovered = options.paths.length > 0 ? options.paths : discoverTranscripts(mapper);
22072
22252
  const paths = options.sinceDays === void 0 ? discovered : withinDays(discovered, options.sinceDays);
22073
22253
  const window2 = options.sinceDays === void 0 ? "" : ` written to in the last ${options.sinceDays} day(s)`;
22074
22254
  say2(`${options.dryRun ? "DRY RUN \u2014 nothing will be sent. " : ""}${paths.length} transcript(s)${window2}`);
22255
+ if (options.reset) {
22256
+ const reset = await resetCheckpoints(mapper, options, paths, say2);
22257
+ if (reset === void 0)
22258
+ return lockBusyResult();
22259
+ }
22075
22260
  const pass = (subset) => runForwarder(mapper, {
22076
22261
  paths: subset,
22077
22262
  dryRun: options.dryRun,
@@ -22088,6 +22273,61 @@ async function runBackfill(mapper, options, say2) {
22088
22273
  return pass([]);
22089
22274
  return runPasses(batches(paths, options.batch ?? DEFAULT_BATCH), pass, say2);
22090
22275
  }
22276
+ async function resetCheckpoints(mapper, options, paths, say2) {
22277
+ const { scope } = loadRuntimeSettings(mapper.vendor, options.profile !== void 0 ? { profile: options.profile } : {});
22278
+ const release = await acquireLockWithin(scope, options.lockWaitMs ?? DEFAULT_LOCK_WAIT_MS);
22279
+ if (!release)
22280
+ return void 0;
22281
+ let reset;
22282
+ try {
22283
+ reset = clearCheckpoints(scope, paths);
22284
+ } finally {
22285
+ release();
22286
+ }
22287
+ if (reset.cleared.length === 0) {
22288
+ say2(reset.total === 0 ? "RESET \u2014 nothing had been shipped yet; these transcripts ship from the start anyway" : `RESET \u2014 NOTHING CLEARED: none of the selected paths match the ${reset.total} checkpoint(s) on file, so they will ship from wherever they left off`);
22289
+ return reset;
22290
+ }
22291
+ say2(`RESET \u2014 forgot the offset for ${reset.cleared.length} transcript(s); they re-ship in full`);
22292
+ say2(`previous checkpoints saved as ${reset.backup}`);
22293
+ return reset;
22294
+ }
22295
+ function backfillIsOff(mapper, options) {
22296
+ try {
22297
+ const { capture } = loadRuntimeSettings(mapper.vendor, options.profile !== void 0 ? { profile: options.profile } : {});
22298
+ return !capture.backfill;
22299
+ } catch {
22300
+ return false;
22301
+ }
22302
+ }
22303
+ function disabledResult() {
22304
+ return {
22305
+ transcripts: 0,
22306
+ records: 0,
22307
+ skipped: ["backfill is disabled"],
22308
+ kinds: {},
22309
+ warnings: [],
22310
+ emitted: { transcripts: 0, records: 0 },
22311
+ backfillDisabled: true,
22312
+ // Non-zero exit, for the reason `shipped 0` cannot carry: an up-to-date machine
22313
+ // prints that too, and a script chaining backfills must not read a refusal as done.
22314
+ ok: false,
22315
+ error: "backfill is disabled by capture.backfill"
22316
+ };
22317
+ }
22318
+ function lockBusyResult() {
22319
+ return {
22320
+ transcripts: 0,
22321
+ records: 0,
22322
+ skipped: ["another forwarder holds the lock"],
22323
+ kinds: {},
22324
+ warnings: [],
22325
+ emitted: { transcripts: 0, records: 0 },
22326
+ lockBusy: true,
22327
+ ok: false,
22328
+ error: "another forwarder holds the lock"
22329
+ };
22330
+ }
22091
22331
  async function runPasses(groups, pass, say2) {
22092
22332
  const total = {
22093
22333
  transcripts: 0,
@@ -22126,8 +22366,8 @@ async function runPasses(groups, pass, say2) {
22126
22366
  }
22127
22367
 
22128
22368
  // adapters/codex/dist/transcript.js
22129
- import { readFileSync as readFileSync6 } from "node:fs";
22130
- import { basename, join as join10 } from "node:path";
22369
+ import { readFileSync as readFileSync7 } from "node:fs";
22370
+ import { basename, join as join11 } from "node:path";
22131
22371
  import { homedir as homedir4 } from "node:os";
22132
22372
  function str(value) {
22133
22373
  return typeof value === "string" ? value : void 0;
@@ -22271,7 +22511,7 @@ function stripSuffix(path) {
22271
22511
  var codexTranscript = {
22272
22512
  vendor: "codex",
22273
22513
  sessionRoots() {
22274
- return [join10(homedir4(), ".codex", "sessions")];
22514
+ return [join11(homedir4(), ".codex", "sessions")];
22275
22515
  },
22276
22516
  /**
22277
22517
  * `~/.codex/auth.json` -> the `email` claim of `tokens.id_token`.
@@ -22293,7 +22533,7 @@ var codexTranscript = {
22293
22533
  */
22294
22534
  resolveUserId() {
22295
22535
  try {
22296
- return emailFromAuth(readFileSync6(join10(homedir4(), ".codex", "auth.json"), "utf8"));
22536
+ return emailFromAuth(readFileSync7(join11(homedir4(), ".codex", "auth.json"), "utf8"));
22297
22537
  } catch {
22298
22538
  return void 0;
22299
22539
  }
@@ -22517,6 +22757,7 @@ if (parsed.kind === "error") {
22517
22757
  process.exit(2);
22518
22758
  }
22519
22759
  var result = await runBackfill(codexTranscript, parsed.options, say);
22760
+ if (result.backfillDisabled === true) process.exit(4);
22520
22761
  say(
22521
22762
  `${parsed.options.dryRun ? "mapped" : "shipped"} ${result.records} record(s) from ${result.transcripts} transcript(s)`
22522
22763
  );