iteration-cli 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/claude-transcript.js +1 -0
- package/dist/collector.js +7 -4
- package/dist/redact-trace.d.ts +2 -0
- package/dist/redact-trace.js +182 -0
- package/dist/storage.d.ts +2 -0
- package/dist/storage.js +77 -3
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -11,6 +11,8 @@ The collectors run on macOS or Linux with Node.js 22 or later. Cursor's local Ag
|
|
|
11
11
|
|
|
12
12
|
Cursor's **This Mac** Agents sessions are read from `~/.cursor/projects/*/agent-transcripts/<conversation>/<conversation>.jsonl`. Connecting Cursor also installs user-level hooks in `~/.cursor/hooks.json` automatically. They capture tool output, thought blocks, model and usage fields, subagent transcripts, and other lifecycle records that the main transcript omits; the collector queues those records locally before upload. Existing hooks are preserved, and `iteration disconnect cursor` removes only Iteration's entries. No Cursor approval or settings step is needed. A custom Cursor directory can be selected with `iteration connect cursor --cursor-home DIR`. Local history predating connection has transcript messages and tool calls, but tool results that Cursor never wrote to its transcript cannot be recovered. Cloud Agents are outside this local collector.
|
|
13
13
|
|
|
14
|
+
The collector replaces detected secrets with `[REDACTED]` before writing trace events to the upload outbox. It also redacts pending events left by an older collector before uploading them. This applies to Codex, Claude Code, and Cursor, including Cursor subagent transcript bytes stored as base64. Detection is best effort; original local agent files and previously uploaded traces are unchanged.
|
|
15
|
+
|
|
14
16
|
Claude Code sessions are read from `~/.claude/projects`, including session subagents. If Claude uses `CLAUDE_CONFIG_DIR`, the collector follows that setting during connection; `iteration connect claude --claude-home DIR` selects another directory explicitly. The collector reads only saved local JSONL records. Claude Code may remove old transcripts under its own retention settings, so connect before local history is removed. The records can contain prompts, reasoning, tool input and output, and context snapshots; workspace members can read what uploads. Unknown record shapes are reported as capture gaps in the collector log. Remote Claude sessions are outside this local collector.
|
|
15
17
|
|
|
16
18
|
Connecting Codex or Claude Code also adds a Stop hook to the coding agent. After an agent turn in a git repository where you had friction episodes this week, it shows one line such as `◆ Iteration · 3 friction episodes in this repo this week · 1 new since yesterday`. The hook reads local files only and adds a few milliseconds to a turn. Codex asks you to approve it once with `/hooks`. `iteration disconnect` removes it.
|
package/dist/collector.js
CHANGED
|
@@ -5,8 +5,9 @@ import { collectorFetch } from "./collector-fetch.js";
|
|
|
5
5
|
import { parseClaudeLine } from "./claude-transcript.js";
|
|
6
6
|
import { cursorHookFiles } from "./cursor-hooks.js";
|
|
7
7
|
import { parseCursorHookLine, parseCursorTranscriptLine, } from "./cursor-transcript.js";
|
|
8
|
+
import { redactTraceEvent } from "./redact-trace.js";
|
|
8
9
|
import { parseRolloutLine } from "./rollout.js";
|
|
9
|
-
import { acknowledgeEvents, appendEvents, collectorSource, collectorSourceDirectory, loadState, nextBatch, outboxSize, pendingEventIds, sourceFiles, saveState, } from "./storage.js";
|
|
10
|
+
import { acknowledgeEvents, appendEvents, collectorSource, collectorSourceDirectory, loadState, nextBatch, outboxSize, pendingEventIds, redactPendingOutbox, sourceFiles, saveState, } from "./storage.js";
|
|
10
11
|
const MAX_SCAN_BYTES_PER_FILE = 16 * 1024 * 1024;
|
|
11
12
|
const MAX_SCAN_BYTES_PER_TICK = 16 * 1024 * 1024;
|
|
12
13
|
const MAX_RECORD_BYTES = 3 * 1024 * 1024;
|
|
@@ -152,13 +153,14 @@ export async function scanSourceOnce(config, paths, signal) {
|
|
|
152
153
|
for (const parsedEvent of parsed.events) {
|
|
153
154
|
if (queuedIds.has(parsedEvent.eventId))
|
|
154
155
|
continue;
|
|
155
|
-
const
|
|
156
|
+
const event = await redactTraceEvent(parsedEvent);
|
|
157
|
+
const eventBytes = Buffer.byteLength(JSON.stringify(event), "utf8");
|
|
156
158
|
if (eventBytes > MAX_EVENT_BYTES) {
|
|
157
159
|
logGap(filename, byteOffset, "normalized_event_exceeds_upload_limit");
|
|
158
160
|
}
|
|
159
161
|
else {
|
|
160
|
-
events.push(
|
|
161
|
-
queuedIds.add(
|
|
162
|
+
events.push(event);
|
|
163
|
+
queuedIds.add(event.eventId);
|
|
162
164
|
}
|
|
163
165
|
}
|
|
164
166
|
lineStart = index + 1;
|
|
@@ -239,6 +241,7 @@ async function uploadPendingOnce(config, paths, fetcher = collectorFetch, signal
|
|
|
239
241
|
return uploaded;
|
|
240
242
|
}
|
|
241
243
|
export async function collectorTick(config, paths, fetcher = collectorFetch, signal, onAcknowledged) {
|
|
244
|
+
await redactPendingOutbox(paths);
|
|
242
245
|
let uploaded = await uploadPendingOnce(config, paths, fetcher, signal, onAcknowledged);
|
|
243
246
|
const scan = (await outboxSize(paths)) >= SCAN_PAUSE_OUTBOX_BYTES
|
|
244
247
|
? { discovered: 0, caughtUp: false }
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { lintSource } from "@secretlint/core";
|
|
2
|
+
import { rules as recommendedRules } from "@secretlint/secretlint-rule-preset-recommend";
|
|
3
|
+
import * as z from "zod";
|
|
4
|
+
const REDACTED = "[REDACTED]";
|
|
5
|
+
const secretlintConfig = {
|
|
6
|
+
rules: recommendedRules
|
|
7
|
+
.filter((rule) => rule.meta.id !== "@secretlint/secretlint-rule-filter-comments")
|
|
8
|
+
.map((rule) => ({ id: rule.meta.id, rule })),
|
|
9
|
+
};
|
|
10
|
+
const assignment = /\b([A-Za-z][A-Za-z0-9_-]{0,79})["']?\s*[:=]\s*(?:"((?:\\.|[^"\\])*)"|'((?:\\.|[^'\\])*)'|([^\s"'`,;}&]+))/g;
|
|
11
|
+
const authorization = /\b(?:authorization|proxy-authorization)["']?\s*:\s*["']?(?:Bearer|Basic)\s+([A-Za-z0-9._~+/-]+={0,2})/gi;
|
|
12
|
+
const privateKey = /-----BEGIN ((?:RSA |EC |OPENSSH |DSA )?PRIVATE KEY)-----[\s\S]*?-----END \1-----/g;
|
|
13
|
+
function normalizedKey(key) {
|
|
14
|
+
return key
|
|
15
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
16
|
+
.replace(/[^A-Za-z0-9]+/g, "_")
|
|
17
|
+
.toLowerCase();
|
|
18
|
+
}
|
|
19
|
+
function sensitiveKey(key) {
|
|
20
|
+
const normalized = normalizedKey(key);
|
|
21
|
+
if (/^(?:next_public|public|vite|react_app)_/.test(normalized))
|
|
22
|
+
return false;
|
|
23
|
+
return (/(?:^|_)(?:secret|password|passwd|credential)(?:_|$)/.test(normalized) ||
|
|
24
|
+
/(?:^|_)(?:api_key|private_key|database_url|db_url|dsn|connection_string|authorization|proxy_authorization|auth_header|cookie|set_cookie|token)$/.test(normalized));
|
|
25
|
+
}
|
|
26
|
+
function placeholder(value) {
|
|
27
|
+
const trimmed = value.trim();
|
|
28
|
+
return (trimmed === REDACTED ||
|
|
29
|
+
/^(?:<[^>]+>|\$\{[^}]+\}|\*+|redacted|placeholder|example|changeme|undefined|null)$/i.test(trimmed) ||
|
|
30
|
+
/^(?:process\.env|env)\.[A-Za-z0-9_]+$/.test(trimmed));
|
|
31
|
+
}
|
|
32
|
+
function addRange(ranges, start, end, length) {
|
|
33
|
+
if (!Number.isSafeInteger(start) ||
|
|
34
|
+
!Number.isSafeInteger(end) ||
|
|
35
|
+
start < 0 ||
|
|
36
|
+
end > length ||
|
|
37
|
+
start >= end) {
|
|
38
|
+
throw new Error("Local trace secret scanner returned an invalid location.");
|
|
39
|
+
}
|
|
40
|
+
ranges.push({ start, end });
|
|
41
|
+
}
|
|
42
|
+
function textRanges(value) {
|
|
43
|
+
const ranges = [];
|
|
44
|
+
for (const match of value.matchAll(assignment)) {
|
|
45
|
+
const key = match[1];
|
|
46
|
+
const candidate = match[2] ?? match[3] ?? match[4];
|
|
47
|
+
if (key === undefined ||
|
|
48
|
+
candidate === undefined ||
|
|
49
|
+
candidate.length === 0 ||
|
|
50
|
+
!sensitiveKey(key) ||
|
|
51
|
+
placeholder(candidate) ||
|
|
52
|
+
((normalizedKey(key) === "authorization" ||
|
|
53
|
+
normalizedKey(key) === "proxy_authorization") &&
|
|
54
|
+
/^(?:Bearer|Basic)$/i.test(candidate)))
|
|
55
|
+
continue;
|
|
56
|
+
const start = match.index + match[0].lastIndexOf(candidate);
|
|
57
|
+
addRange(ranges, start, start + candidate.length, value.length);
|
|
58
|
+
}
|
|
59
|
+
for (const match of value.matchAll(authorization)) {
|
|
60
|
+
const candidate = match[1];
|
|
61
|
+
if (candidate === undefined || placeholder(candidate))
|
|
62
|
+
continue;
|
|
63
|
+
const start = match.index + match[0].lastIndexOf(candidate);
|
|
64
|
+
addRange(ranges, start, start + candidate.length, value.length);
|
|
65
|
+
}
|
|
66
|
+
for (const match of value.matchAll(privateKey)) {
|
|
67
|
+
addRange(ranges, match.index, match.index + match[0].length, value.length);
|
|
68
|
+
}
|
|
69
|
+
return ranges;
|
|
70
|
+
}
|
|
71
|
+
function replaceRanges(value, ranges) {
|
|
72
|
+
if (ranges.length === 0)
|
|
73
|
+
return value;
|
|
74
|
+
ranges.sort((a, b) => a.start - b.start || a.end - b.end);
|
|
75
|
+
const merged = [];
|
|
76
|
+
for (const range of ranges) {
|
|
77
|
+
const previous = merged.at(-1);
|
|
78
|
+
if (previous !== undefined && range.start <= previous.end) {
|
|
79
|
+
merged[merged.length - 1] = {
|
|
80
|
+
start: previous.start,
|
|
81
|
+
end: Math.max(previous.end, range.end),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
else
|
|
85
|
+
merged.push(range);
|
|
86
|
+
}
|
|
87
|
+
let result = value;
|
|
88
|
+
for (let index = merged.length - 1; index >= 0; index -= 1) {
|
|
89
|
+
const range = merged[index];
|
|
90
|
+
if (range === undefined)
|
|
91
|
+
continue;
|
|
92
|
+
result = `${result.slice(0, range.start)}${REDACTED}${result.slice(range.end)}`;
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
async function redactText(value) {
|
|
97
|
+
if (value.length === 0 || value === REDACTED)
|
|
98
|
+
return value;
|
|
99
|
+
const ranges = textRanges(value);
|
|
100
|
+
let result;
|
|
101
|
+
try {
|
|
102
|
+
result = await lintSource({
|
|
103
|
+
source: {
|
|
104
|
+
contentType: "text",
|
|
105
|
+
content: value,
|
|
106
|
+
filePath: "trace.txt",
|
|
107
|
+
ext: "txt",
|
|
108
|
+
},
|
|
109
|
+
options: {
|
|
110
|
+
maskSecrets: true,
|
|
111
|
+
noPhysicFilePath: true,
|
|
112
|
+
config: secretlintConfig,
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
throw new Error("Local trace secret scanning failed; the event was not queued.");
|
|
118
|
+
}
|
|
119
|
+
for (const message of result.messages) {
|
|
120
|
+
addRange(ranges, message.range[0], message.range[1], value.length);
|
|
121
|
+
}
|
|
122
|
+
return replaceRanges(value, ranges);
|
|
123
|
+
}
|
|
124
|
+
async function redactValue(value, cache, force) {
|
|
125
|
+
const parsedText = z.string().safeParse(value);
|
|
126
|
+
if (parsedText.success) {
|
|
127
|
+
const text = parsedText.data;
|
|
128
|
+
if (force)
|
|
129
|
+
return placeholder(text) ? text : REDACTED;
|
|
130
|
+
let result = cache.get(text);
|
|
131
|
+
if (result === undefined) {
|
|
132
|
+
result = redactText(text);
|
|
133
|
+
cache.set(text, result);
|
|
134
|
+
}
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
if (value === null || value === true || value === false)
|
|
138
|
+
return value;
|
|
139
|
+
const parsedNumber = z.number().safeParse(value);
|
|
140
|
+
if (parsedNumber.success)
|
|
141
|
+
return force ? REDACTED : parsedNumber.data;
|
|
142
|
+
if (Array.isArray(value)) {
|
|
143
|
+
const items = await Promise.all(value.map((item) => redactValue(item, cache, force)));
|
|
144
|
+
return items.every((item, index) => item === value[index]) ? value : items;
|
|
145
|
+
}
|
|
146
|
+
const record = value;
|
|
147
|
+
const entries = await Promise.all(Object.entries(record).map(async ([key, item]) => [
|
|
148
|
+
key,
|
|
149
|
+
await redactValue(item, cache, force || sensitiveKey(key)),
|
|
150
|
+
]));
|
|
151
|
+
return entries.every(([key, item]) => item === record[key])
|
|
152
|
+
? value
|
|
153
|
+
: Object.fromEntries(entries);
|
|
154
|
+
}
|
|
155
|
+
export async function redactTraceEvent(event) {
|
|
156
|
+
const cache = new Map();
|
|
157
|
+
if (event.payload.sourceType === "cursor_subagent_transcript") {
|
|
158
|
+
const parsed = z.string().safeParse(event.payload.base64);
|
|
159
|
+
if (!parsed.success)
|
|
160
|
+
throw new Error("Cursor transcript has invalid encoded content.");
|
|
161
|
+
const bytes = Buffer.from(parsed.data, "base64");
|
|
162
|
+
if (bytes.toString("base64") !== parsed.data)
|
|
163
|
+
throw new Error("Cursor transcript has invalid encoded content.");
|
|
164
|
+
const raw = bytes.toString("latin1");
|
|
165
|
+
const redacted = await redactText(raw);
|
|
166
|
+
const { base64: _encoded, ...remaining } = event.payload;
|
|
167
|
+
const payload = await redactValue(remaining, cache, false);
|
|
168
|
+
return {
|
|
169
|
+
...event,
|
|
170
|
+
payload: {
|
|
171
|
+
...payload,
|
|
172
|
+
base64: redacted === raw
|
|
173
|
+
? parsed.data
|
|
174
|
+
: Buffer.from(redacted, "latin1").toString("base64"),
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
const payload = await redactValue(event.payload, cache, false);
|
|
179
|
+
return payload === event.payload
|
|
180
|
+
? event
|
|
181
|
+
: { ...event, payload: payload };
|
|
182
|
+
}
|
package/dist/storage.d.ts
CHANGED
|
@@ -44,6 +44,7 @@ export type CollectorPaths = Readonly<{
|
|
|
44
44
|
config: string;
|
|
45
45
|
state: string;
|
|
46
46
|
outbox: string;
|
|
47
|
+
redactionMarker: string;
|
|
47
48
|
lock: string;
|
|
48
49
|
historyBackfill: string;
|
|
49
50
|
health: string;
|
|
@@ -63,6 +64,7 @@ export declare function saveState(state: CollectorState, paths: CollectorPaths):
|
|
|
63
64
|
export declare function outboxSize(paths: CollectorPaths): Promise<number>;
|
|
64
65
|
export declare function pendingEventIds(paths: CollectorPaths): Promise<Set<string>>;
|
|
65
66
|
export declare function appendEvents(events: readonly TraceEvent[], paths: CollectorPaths): Promise<void>;
|
|
67
|
+
export declare function redactPendingOutbox(paths: CollectorPaths): Promise<void>;
|
|
66
68
|
export declare function nextBatch(paths: CollectorPaths, maxBytes: number): Promise<readonly TraceEvent[]>;
|
|
67
69
|
export declare function acknowledgeEvents(acknowledgedIds: ReadonlySet<string>, paths: CollectorPaths): Promise<void>;
|
|
68
70
|
export declare function initializeState(source: CollectorSource, home: string, paths: CollectorPaths, includeHistory: boolean): Promise<void>;
|
package/dist/storage.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
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
11
|
return "cursorHome" in config
|
|
@@ -66,6 +67,7 @@ const healthSchema = z
|
|
|
66
67
|
.nullable(),
|
|
67
68
|
})
|
|
68
69
|
.strict();
|
|
70
|
+
const redactionMarkerSchema = z.object({ version: z.literal(1) }).strict();
|
|
69
71
|
function parseOutboxEvent(line) {
|
|
70
72
|
let decoded;
|
|
71
73
|
try {
|
|
@@ -116,6 +118,7 @@ export function collectorPaths(directory = defaultCollectorDirectory("codex")) {
|
|
|
116
118
|
config: path.join(directory, "config.json"),
|
|
117
119
|
state: path.join(directory, "state.json"),
|
|
118
120
|
outbox: path.join(directory, "outbox.jsonl"),
|
|
121
|
+
redactionMarker: path.join(directory, "redaction.json"),
|
|
119
122
|
lock: path.join(directory, "collector.lock"),
|
|
120
123
|
historyBackfill: path.join(directory, "history-backfill.json"),
|
|
121
124
|
health: path.join(directory, "health.json"),
|
|
@@ -330,6 +333,72 @@ export async function appendEvents(events, paths) {
|
|
|
330
333
|
await handle.close();
|
|
331
334
|
}
|
|
332
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
|
+
}
|
|
333
402
|
export async function nextBatch(paths, maxBytes) {
|
|
334
403
|
if (!Number.isSafeInteger(maxBytes) || maxBytes < 1) {
|
|
335
404
|
throw new Error("Maximum upload batch size must be a positive integer.");
|
|
@@ -493,9 +562,12 @@ export async function ensureHistoryBackfill(codexHome, paths) {
|
|
|
493
562
|
});
|
|
494
563
|
return true;
|
|
495
564
|
}
|
|
565
|
+
// Version 2 re-reads Claude history so stored sessions gain each response's
|
|
566
|
+
// final usage report and the PR links Claude records.
|
|
567
|
+
const CLAUDE_HISTORY_BACKFILL_VERSION = 2;
|
|
496
568
|
export async function ensureClaudeHistoryBackfill(claudeHome, paths) {
|
|
497
569
|
const version = await readHistoryBackfillVersion(paths);
|
|
498
|
-
if (version !== null && version >=
|
|
570
|
+
if (version !== null && version >= CLAUDE_HISTORY_BACKFILL_VERSION)
|
|
499
571
|
return false;
|
|
500
572
|
const state = await loadState(paths);
|
|
501
573
|
if (!state.initialized ||
|
|
@@ -504,7 +576,9 @@ export async function ensureClaudeHistoryBackfill(claudeHome, paths) {
|
|
|
504
576
|
}
|
|
505
577
|
await claudeFiles(path.join(claudeHome, "projects"));
|
|
506
578
|
await saveState({ ...state, cursors: {}, skipping: [] }, paths);
|
|
507
|
-
await writeJsonAtomic(paths.historyBackfill, {
|
|
579
|
+
await writeJsonAtomic(paths.historyBackfill, {
|
|
580
|
+
version: CLAUDE_HISTORY_BACKFILL_VERSION,
|
|
581
|
+
});
|
|
508
582
|
return true;
|
|
509
583
|
}
|
|
510
584
|
export async function ensureCursorHistoryBackfill(cursorHome, paths) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iteration-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -17,6 +17,8 @@
|
|
|
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
23
|
"iteration-trace-contracts": "0.7.0"
|
|
22
24
|
},
|