iteration-cli 0.7.3 → 0.8.1

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.
@@ -23,6 +23,7 @@ export type CliStatus = Readonly<{
23
23
  health: CollectorHealth;
24
24
  healthAvailable: boolean;
25
25
  turnSummaryNeedsApproval: boolean;
26
+ hookFailureAtMs?: number | null;
26
27
  tracesUrl: string;
27
28
  logPath: string;
28
29
  }>;
@@ -149,6 +149,10 @@ export function renderHome(status, nowMs = Date.now(), source = "codex") {
149
149
  lines.push(`${pendingDescription(status)}.`);
150
150
  lines.push(`Resume with iteration start ${source}.`);
151
151
  }
152
+ else if (status.hookFailureAtMs !== null &&
153
+ status.hookFailureAtMs !== undefined) {
154
+ lines.push(`${name} hook capture needs attention`, `A Cursor hook failed ${relativeTime(status.hookFailureAtMs, nowMs)}. Some data may be missing. See iteration status cursor.`);
155
+ }
152
156
  else if (!status.healthAvailable) {
153
157
  lines.push(`${name} is running; local status needs attention`, "Upload history could not be read. Collection continues in the background.");
154
158
  if (status.pendingEvents > 0)
@@ -242,6 +246,8 @@ export function renderDetailedStatus(status, nowMs = Date.now(), source = "codex
242
246
  : "Local collector error";
243
247
  lines.push(`Last issue: ${label} (${relativeTime(failure.atMs, nowMs)})`, `Collector log: ${status.logPath}`);
244
248
  }
249
+ if (status.hookFailureAtMs !== null && status.hookFailureAtMs !== undefined)
250
+ lines.push(`Last Cursor hook capture failure: ${relativeTime(status.hookFailureAtMs, nowMs)}. Some data may be missing; inspect Cursor hook logs.`);
245
251
  if (status.turnSummaryNeedsApproval)
246
252
  lines.push(CODEX_APPROVAL_STEP);
247
253
  if (!status.running)
package/dist/storage.d.ts CHANGED
@@ -11,6 +11,9 @@ export type CollectorConfig = (CollectorConnection & Readonly<{
11
11
  }>) | (CollectorConnection & Readonly<{
12
12
  source: "claude";
13
13
  claudeHome: string;
14
+ }>) | (CollectorConnection & Readonly<{
15
+ source: "cursor";
16
+ cursorHome: string;
14
17
  }>);
15
18
  export declare function collectorSource(config: CollectorConfig): CollectorSource;
16
19
  export declare function collectorSourceDirectory(config: CollectorConfig): string;
@@ -41,6 +44,7 @@ export type CollectorPaths = Readonly<{
41
44
  config: string;
42
45
  state: string;
43
46
  outbox: string;
47
+ redactionMarker: string;
44
48
  lock: string;
45
49
  historyBackfill: string;
46
50
  health: string;
@@ -60,6 +64,7 @@ export declare function saveState(state: CollectorState, paths: CollectorPaths):
60
64
  export declare function outboxSize(paths: CollectorPaths): Promise<number>;
61
65
  export declare function pendingEventIds(paths: CollectorPaths): Promise<Set<string>>;
62
66
  export declare function appendEvents(events: readonly TraceEvent[], paths: CollectorPaths): Promise<void>;
67
+ export declare function redactPendingOutbox(paths: CollectorPaths): Promise<void>;
63
68
  export declare function nextBatch(paths: CollectorPaths, maxBytes: number): Promise<readonly TraceEvent[]>;
64
69
  export declare function acknowledgeEvents(acknowledgedIds: ReadonlySet<string>, paths: CollectorPaths): Promise<void>;
65
70
  export declare function initializeState(source: CollectorSource, home: string, paths: CollectorPaths, includeHistory: boolean): Promise<void>;
@@ -67,6 +72,7 @@ export declare function prepareSourceInitialState(source: CollectorSource, home:
67
72
  export declare function ensureStartableState(config: CollectorConfig, paths: CollectorPaths): Promise<boolean>;
68
73
  export declare function ensureHistoryBackfill(codexHome: string, paths: CollectorPaths): Promise<boolean>;
69
74
  export declare function ensureClaudeHistoryBackfill(claudeHome: string, paths: CollectorPaths): Promise<boolean>;
75
+ export declare function ensureCursorHistoryBackfill(cursorHome: string, paths: CollectorPaths): Promise<boolean>;
70
76
  export declare function historyScanProgress(home: string, paths: CollectorPaths, source?: CollectorSource): Promise<Readonly<{
71
77
  kind: "waiting";
72
78
  } | {
@@ -84,4 +90,5 @@ export declare function historyImportSize(home: string, source?: CollectorSource
84
90
  }>>;
85
91
  export declare function claudeFiles(root: string): Promise<string[]>;
86
92
  export declare function sourceFiles(source: CollectorSource, home: string): Promise<string[]>;
93
+ export declare function cursorFiles(root: string): Promise<string[]>;
87
94
  export {};
package/dist/storage.js CHANGED
@@ -1,16 +1,25 @@
1
1
  import { once } from "node:events";
2
- import { createWriteStream, promises as fs } from "node:fs";
2
+ import { createReadStream, createWriteStream, promises as fs } from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { createInterface } from "node:readline";
6
6
  import { MAX_TRACE_EVENTS_PER_BATCH, parseTraceEventBatch, } from "iteration-trace-contracts";
7
7
  import * as z from "zod";
8
+ import { redactTraceEvent } from "./redact-trace.js";
8
9
  const MAX_OUTBOX_BYTES = 64 * 1024 * 1024;
9
10
  export function collectorSource(config) {
10
- return "claudeHome" in config ? "claude" : "codex";
11
+ return "cursorHome" in config
12
+ ? "cursor"
13
+ : "claudeHome" in config
14
+ ? "claude"
15
+ : "codex";
11
16
  }
12
17
  export function collectorSourceDirectory(config) {
13
- return "claudeHome" in config ? config.claudeHome : config.codexHome;
18
+ return "cursorHome" in config
19
+ ? config.cursorHome
20
+ : "claudeHome" in config
21
+ ? config.claudeHome
22
+ : config.codexHome;
14
23
  }
15
24
  const connectionSchema = {
16
25
  endpoint: z.string(),
@@ -28,6 +37,13 @@ const configSchema = z.union([
28
37
  claudeHome: z.string(),
29
38
  })
30
39
  .strict(),
40
+ z
41
+ .object({
42
+ ...connectionSchema,
43
+ source: z.literal("cursor"),
44
+ cursorHome: z.string(),
45
+ })
46
+ .strict(),
31
47
  ]);
32
48
  const stateSchema = z
33
49
  .object({
@@ -51,6 +67,7 @@ const healthSchema = z
51
67
  .nullable(),
52
68
  })
53
69
  .strict();
70
+ const redactionMarkerSchema = z.object({ version: z.literal(1) }).strict();
54
71
  function parseOutboxEvent(line) {
55
72
  let decoded;
56
73
  try {
@@ -101,6 +118,7 @@ export function collectorPaths(directory = defaultCollectorDirectory("codex")) {
101
118
  config: path.join(directory, "config.json"),
102
119
  state: path.join(directory, "state.json"),
103
120
  outbox: path.join(directory, "outbox.jsonl"),
121
+ redactionMarker: path.join(directory, "redaction.json"),
104
122
  lock: path.join(directory, "collector.lock"),
105
123
  historyBackfill: path.join(directory, "history-backfill.json"),
106
124
  health: path.join(directory, "health.json"),
@@ -315,6 +333,72 @@ export async function appendEvents(events, paths) {
315
333
  await handle.close();
316
334
  }
317
335
  }
336
+ export async function redactPendingOutbox(paths) {
337
+ let marker;
338
+ try {
339
+ marker = await fs.readFile(paths.redactionMarker, "utf8");
340
+ }
341
+ catch (error) {
342
+ if (error instanceof Error && "code" in error && error.code === "ENOENT")
343
+ marker = null;
344
+ else
345
+ throw error;
346
+ }
347
+ if (marker !== null) {
348
+ let decoded;
349
+ try {
350
+ decoded = JSON.parse(marker);
351
+ }
352
+ catch {
353
+ throw new Error("Collector redaction marker is not valid JSON.");
354
+ }
355
+ if (!redactionMarkerSchema.safeParse(decoded).success)
356
+ throw new Error("Collector redaction marker has an invalid shape.");
357
+ return;
358
+ }
359
+ const size = await outboxSize(paths);
360
+ if (size > MAX_OUTBOX_BYTES)
361
+ throw new Error("Trace outbox exceeds its configured capacity.");
362
+ if (size > 0) {
363
+ const temporary = `${paths.outbox}.${process.pid}.redacting`;
364
+ const output = await fs.open(temporary, "w", 0o600);
365
+ try {
366
+ const lines = createInterface({
367
+ input: createReadStream(paths.outbox, { encoding: "utf8" }),
368
+ crlfDelay: Infinity,
369
+ });
370
+ for await (const line of lines) {
371
+ const event = await redactTraceEvent(parseOutboxEvent(line));
372
+ await output.writeFile(`${JSON.stringify(event)}\n`, "utf8");
373
+ }
374
+ await output.sync();
375
+ }
376
+ catch (error) {
377
+ await output.close();
378
+ await fs.rm(temporary, { force: true });
379
+ throw error;
380
+ }
381
+ await output.close();
382
+ try {
383
+ if ((await fs.stat(temporary)).size > MAX_OUTBOX_BYTES)
384
+ throw new Error("Redacted trace outbox exceeds its configured capacity.");
385
+ await fs.rename(temporary, paths.outbox);
386
+ const directory = await fs.open(path.dirname(paths.outbox), "r");
387
+ try {
388
+ await directory.sync();
389
+ }
390
+ finally {
391
+ await directory.close();
392
+ }
393
+ }
394
+ catch (error) {
395
+ await fs.rm(temporary, { force: true });
396
+ throw error;
397
+ }
398
+ }
399
+ await ensurePrivateDirectory(path.dirname(paths.redactionMarker));
400
+ await writeJsonAtomic(paths.redactionMarker, { version: 1 });
401
+ }
318
402
  export async function nextBatch(paths, maxBytes) {
319
403
  if (!Number.isSafeInteger(maxBytes) || maxBytes < 1) {
320
404
  throw new Error("Maximum upload batch size must be a positive integer.");
@@ -492,6 +576,18 @@ export async function ensureClaudeHistoryBackfill(claudeHome, paths) {
492
576
  await writeJsonAtomic(paths.historyBackfill, { version: 1 });
493
577
  return true;
494
578
  }
579
+ export async function ensureCursorHistoryBackfill(cursorHome, paths) {
580
+ const version = await readHistoryBackfillVersion(paths);
581
+ if (version !== null && version >= 1)
582
+ return false;
583
+ const state = await loadState(paths);
584
+ if (!state.initialized || state.sourceDirectory !== path.resolve(cursorHome))
585
+ throw new Error("Collector state does not match the Cursor directory; reconnect Cursor before uploading.");
586
+ await cursorFiles(path.join(cursorHome, "projects"));
587
+ await saveState({ ...state, cursors: {}, skipping: [] }, paths);
588
+ await writeJsonAtomic(paths.historyBackfill, { version: 1 });
589
+ return true;
590
+ }
495
591
  export async function historyScanProgress(home, paths, source = "codex") {
496
592
  if ((await readHistoryBackfillVersion(paths)) === null)
497
593
  return { kind: "waiting" };
@@ -677,6 +773,32 @@ export function sourceFiles(source, home) {
677
773
  return rolloutFiles(path.join(directory, "sessions"));
678
774
  if (source === "claude")
679
775
  return claudeFiles(path.join(directory, "projects"));
776
+ if (source === "cursor")
777
+ return cursorFiles(path.join(directory, "projects"));
680
778
  const unhandled = source;
681
779
  throw new Error(`Unsupported collector source: ${unhandled}`);
682
780
  }
781
+ export async function cursorFiles(root) {
782
+ const files = [];
783
+ for (const project of await directoryEntries(root)) {
784
+ if (!project.isDirectory())
785
+ continue;
786
+ const transcripts = path.join(root, project.name, "agent-transcripts");
787
+ for (const conversation of await directoryEntries(transcripts)) {
788
+ if (!conversation.isDirectory())
789
+ continue;
790
+ const filename = path.join(transcripts, conversation.name, `${conversation.name}.jsonl`);
791
+ if ((await fs.stat(filename).catch((error) => {
792
+ if (error instanceof Error &&
793
+ "code" in error &&
794
+ error.code === "ENOENT")
795
+ return null;
796
+ throw error;
797
+ }))?.isFile())
798
+ files.push(filename);
799
+ if (files.length > 10_000)
800
+ throw new Error("Cursor session file count exceeds the scan limit.");
801
+ }
802
+ }
803
+ return files.sort();
804
+ }
@@ -55,6 +55,8 @@ function hookCommand(paths) {
55
55
  return `/bin/sh ${shellQuote(paths.script)}`;
56
56
  }
57
57
  function agentHookFile(config) {
58
+ if ("cursorHome" in config)
59
+ throw new Error("Cursor uses its own trace hooks.");
58
60
  return "claudeHome" in config
59
61
  ? path.join(config.claudeHome, "settings.json")
60
62
  : path.join(config.codexHome, "hooks.json");
@@ -337,7 +339,7 @@ function codexTrustedHash(configToml, key) {
337
339
  return trustedHash;
338
340
  }
339
341
  export async function codexTurnSummaryApproval(config) {
340
- if ("claudeHome" in config)
342
+ if ("claudeHome" in config || "cursorHome" in config)
341
343
  return "approved";
342
344
  const filename = agentHookFile(config);
343
345
  const { file } = await readHookFile(filename);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iteration-cli",
3
- "version": "0.7.3",
3
+ "version": "0.8.1",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -17,8 +17,10 @@
17
17
  "iteration": "./dist/cli.js"
18
18
  },
19
19
  "dependencies": {
20
+ "@secretlint/core": "13.0.6",
21
+ "@secretlint/secretlint-rule-preset-recommend": "13.0.6",
20
22
  "zod": "4.6.4",
21
- "iteration-trace-contracts": "0.6.0"
23
+ "iteration-trace-contracts": "0.7.0"
22
24
  },
23
25
  "scripts": {
24
26
  "build": "pnpm --filter iteration-trace-contracts build && tsc -p tsconfig.json"