@cuylabs/agent-core 0.4.0 → 0.6.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.
Files changed (66) hide show
  1. package/README.md +81 -323
  2. package/dist/builder-BKkipazh.d.ts +34 -0
  3. package/dist/capabilities/index.d.ts +97 -0
  4. package/dist/capabilities/index.js +46 -0
  5. package/dist/chunk-3C4VKG4P.js +2149 -0
  6. package/dist/chunk-6TDTQJ4P.js +116 -0
  7. package/dist/chunk-7MUFEN4K.js +559 -0
  8. package/dist/chunk-BDBZ3SLK.js +745 -0
  9. package/dist/chunk-DWYX7ASF.js +26 -0
  10. package/dist/chunk-FG4MD5MU.js +54 -0
  11. package/dist/chunk-IVUJDISU.js +556 -0
  12. package/dist/chunk-LRHOS4ZN.js +584 -0
  13. package/dist/chunk-O2ZCFQL6.js +764 -0
  14. package/dist/chunk-P6YF7USR.js +182 -0
  15. package/dist/chunk-QAQADS4X.js +258 -0
  16. package/dist/chunk-QWFMX226.js +879 -0
  17. package/dist/{chunk-6VKLWNRE.js → chunk-SDSBEQXG.js} +1 -132
  18. package/dist/chunk-VBWWUHWI.js +724 -0
  19. package/dist/chunk-VEKUXUVF.js +41 -0
  20. package/dist/chunk-X635CM2F.js +305 -0
  21. package/dist/chunk-YUUJK53A.js +91 -0
  22. package/dist/chunk-ZXAKHMWH.js +283 -0
  23. package/dist/config-D2xeGEHK.d.ts +52 -0
  24. package/dist/context/index.d.ts +259 -0
  25. package/dist/context/index.js +26 -0
  26. package/dist/identifiers-BLUxFqV_.d.ts +12 -0
  27. package/dist/index-DZQJD_hp.d.ts +1067 -0
  28. package/dist/index-ipP3_ztp.d.ts +198 -0
  29. package/dist/index.d.ts +210 -5736
  30. package/dist/index.js +2132 -7767
  31. package/dist/mcp/index.d.ts +26 -0
  32. package/dist/mcp/index.js +14 -0
  33. package/dist/messages-BYWGn8TY.d.ts +110 -0
  34. package/dist/middleware/index.d.ts +8 -0
  35. package/dist/middleware/index.js +12 -0
  36. package/dist/models/index.d.ts +33 -0
  37. package/dist/models/index.js +12 -0
  38. package/dist/network-D76DS5ot.d.ts +5 -0
  39. package/dist/prompt/index.d.ts +225 -0
  40. package/dist/prompt/index.js +45 -0
  41. package/dist/reasoning/index.d.ts +71 -0
  42. package/dist/reasoning/index.js +47 -0
  43. package/dist/registry-CuRWWtcT.d.ts +164 -0
  44. package/dist/resolver-DOfZ-xuk.d.ts +254 -0
  45. package/dist/runner-G1wxEgac.d.ts +852 -0
  46. package/dist/runtime/index.d.ts +357 -0
  47. package/dist/runtime/index.js +64 -0
  48. package/dist/session-manager-Uawm2Le7.d.ts +274 -0
  49. package/dist/skill/index.d.ts +103 -0
  50. package/dist/skill/index.js +39 -0
  51. package/dist/storage/index.d.ts +167 -0
  52. package/dist/storage/index.js +50 -0
  53. package/dist/sub-agent/index.d.ts +14 -0
  54. package/dist/sub-agent/index.js +15 -0
  55. package/dist/tool/index.d.ts +174 -1
  56. package/dist/tool/index.js +12 -3
  57. package/dist/tool-DYp6-cC3.d.ts +239 -0
  58. package/dist/tool-pFAnJc5Y.d.ts +419 -0
  59. package/dist/tracker-DClqYqTj.d.ts +96 -0
  60. package/dist/tracking/index.d.ts +109 -0
  61. package/dist/tracking/index.js +20 -0
  62. package/dist/types-BWo810L_.d.ts +648 -0
  63. package/dist/types-CQaXbRsS.d.ts +47 -0
  64. package/dist/types-VQgymC1N.d.ts +156 -0
  65. package/package.json +89 -5
  66. package/dist/index-BlSTfS-W.d.ts +0 -470
@@ -0,0 +1,724 @@
1
+ // src/tracking/checkpoint/manager.ts
2
+ import { rm, unlink } from "fs/promises";
3
+ import { join as join2, relative, resolve } from "path";
4
+
5
+ // src/tracking/checkpoint/git.ts
6
+ import { spawn } from "child_process";
7
+ import { createHash } from "crypto";
8
+ import { access, mkdir } from "fs/promises";
9
+ import { join } from "path";
10
+ import { homedir } from "os";
11
+ async function runCheckpointGitCommand(gitDir, workTree, args) {
12
+ return await new Promise((resolve3) => {
13
+ const fullArgs = ["--git-dir", gitDir, "--work-tree", workTree, ...args];
14
+ const proc = spawn("git", fullArgs, {
15
+ cwd: workTree,
16
+ stdio: ["pipe", "pipe", "pipe"],
17
+ env: {
18
+ ...process.env,
19
+ GIT_TERMINAL_PROMPT: "0",
20
+ GIT_ASKPASS: "true"
21
+ }
22
+ });
23
+ let stdout = "";
24
+ let stderr = "";
25
+ proc.stdout.on("data", (data) => {
26
+ stdout += data.toString();
27
+ });
28
+ proc.stderr.on("data", (data) => {
29
+ stderr += data.toString();
30
+ });
31
+ proc.on("close", (code) => {
32
+ resolve3({
33
+ stdout: stdout.trim(),
34
+ stderr: stderr.trim(),
35
+ exitCode: code ?? 0
36
+ });
37
+ });
38
+ proc.on("error", (error) => {
39
+ resolve3({
40
+ stdout: "",
41
+ stderr: error.message,
42
+ exitCode: 1
43
+ });
44
+ });
45
+ });
46
+ }
47
+ function getCheckpointProjectId(workDir) {
48
+ return createHash("sha256").update(workDir).digest("hex").slice(0, 16);
49
+ }
50
+ function getDefaultCheckpointStorageDir(workDir) {
51
+ return join(homedir(), ".cuylabs", "checkpoints", getCheckpointProjectId(workDir));
52
+ }
53
+ async function initializeCheckpointRepository(gitDir, workDir) {
54
+ await mkdir(gitDir, { recursive: true });
55
+ const headPath = join(gitDir, "HEAD");
56
+ let needsInit = false;
57
+ try {
58
+ await access(headPath);
59
+ } catch {
60
+ needsInit = true;
61
+ }
62
+ if (!needsInit) {
63
+ return;
64
+ }
65
+ const initResult = await new Promise(
66
+ (resolve3) => {
67
+ const proc = spawn("git", ["init", "--bare"], {
68
+ cwd: gitDir,
69
+ stdio: ["pipe", "pipe", "pipe"]
70
+ });
71
+ let stderr = "";
72
+ proc.stderr.on("data", (data) => {
73
+ stderr += data.toString();
74
+ });
75
+ proc.on("close", (code) => {
76
+ resolve3({ exitCode: code ?? 0, stderr });
77
+ });
78
+ proc.on("error", (error) => {
79
+ resolve3({ exitCode: 1, stderr: error.message });
80
+ });
81
+ }
82
+ );
83
+ if (initResult.exitCode !== 0) {
84
+ throw new Error(`Failed to init checkpoint repo: ${initResult.stderr}`);
85
+ }
86
+ await runCheckpointGitCommand(gitDir, workDir, ["config", "gc.auto", "0"]);
87
+ await runCheckpointGitCommand(gitDir, workDir, [
88
+ "config",
89
+ "core.autocrlf",
90
+ "false"
91
+ ]);
92
+ }
93
+
94
+ // src/tracking/checkpoint/log.ts
95
+ import { dirname } from "path";
96
+ import { mkdir as mkdir2, readFile, writeFile } from "fs/promises";
97
+ async function readCheckpointLog(logPath) {
98
+ try {
99
+ const content = await readFile(logPath, "utf-8");
100
+ const lines = content.trim().split("\n").filter(Boolean);
101
+ return lines.map((line) => {
102
+ const entry = JSON.parse(line);
103
+ return {
104
+ ...entry,
105
+ createdAt: new Date(entry.createdAt)
106
+ };
107
+ });
108
+ } catch {
109
+ return [];
110
+ }
111
+ }
112
+ async function appendCheckpointLog(logPath, checkpoint) {
113
+ const line = `${JSON.stringify({
114
+ ...checkpoint,
115
+ createdAt: checkpoint.createdAt.toISOString()
116
+ })}
117
+ `;
118
+ await mkdir2(dirname(logPath), { recursive: true });
119
+ try {
120
+ const existing = await readFile(logPath, "utf-8");
121
+ await writeFile(logPath, `${existing}${line}`);
122
+ } catch {
123
+ await writeFile(logPath, line);
124
+ }
125
+ }
126
+ async function removeCheckpointFromLog(logPath, checkpointId) {
127
+ try {
128
+ const content = await readFile(logPath, "utf-8");
129
+ const lines = content.trim().split("\n").filter(Boolean);
130
+ const filtered = lines.filter((line) => {
131
+ const entry = JSON.parse(line);
132
+ return entry.id !== checkpointId;
133
+ });
134
+ await writeFile(
135
+ logPath,
136
+ filtered.join("\n") + (filtered.length > 0 ? "\n" : "")
137
+ );
138
+ } catch {
139
+ }
140
+ }
141
+
142
+ // src/tracking/checkpoint/manager.ts
143
+ function classifyChangeType(status) {
144
+ switch (status?.charAt(0)) {
145
+ case "A":
146
+ return "added";
147
+ case "D":
148
+ return "deleted";
149
+ default:
150
+ return "modified";
151
+ }
152
+ }
153
+ async function createCheckpointManager(config) {
154
+ const workDir = resolve(config.workDir);
155
+ const storageDir = config.storageDir ?? getDefaultCheckpointStorageDir(workDir);
156
+ const gitDir = join2(storageDir, "git");
157
+ const logPath = join2(storageDir, "checkpoints.jsonl");
158
+ const state = {
159
+ config: {
160
+ workDir,
161
+ storageDir,
162
+ trackContent: config.trackContent ?? true,
163
+ exclude: config.exclude ?? [],
164
+ maxCheckpoints: config.maxCheckpoints ?? 100
165
+ },
166
+ gitDir,
167
+ initialized: false,
168
+ checkpointLog: []
169
+ };
170
+ async function initialize() {
171
+ if (state.initialized) {
172
+ return;
173
+ }
174
+ await initializeCheckpointRepository(gitDir, workDir);
175
+ state.checkpointLog = await readCheckpointLog(logPath);
176
+ state.initialized = true;
177
+ }
178
+ await initialize();
179
+ const manager = {
180
+ async save(label = "checkpoint", metadata) {
181
+ await initialize();
182
+ const addResult = await runCheckpointGitCommand(gitDir, workDir, [
183
+ "add",
184
+ "--all",
185
+ "."
186
+ ]);
187
+ if (addResult.exitCode !== 0) {
188
+ throw new Error(`Failed to stage files: ${addResult.stderr}`);
189
+ }
190
+ const treeResult = await runCheckpointGitCommand(gitDir, workDir, [
191
+ "write-tree"
192
+ ]);
193
+ if (treeResult.exitCode !== 0) {
194
+ throw new Error(`Failed to write tree: ${treeResult.stderr}`);
195
+ }
196
+ const id = treeResult.stdout.trim();
197
+ const existing = state.checkpointLog.find((checkpoint2) => checkpoint2.id === id);
198
+ if (existing) {
199
+ return existing;
200
+ }
201
+ const checkpoint = {
202
+ id,
203
+ label,
204
+ createdAt: /* @__PURE__ */ new Date(),
205
+ metadata
206
+ };
207
+ await appendCheckpointLog(logPath, checkpoint);
208
+ state.checkpointLog.push(checkpoint);
209
+ if (state.checkpointLog.length > state.config.maxCheckpoints) {
210
+ await manager.prune();
211
+ }
212
+ return checkpoint;
213
+ },
214
+ async restore(checkpointId) {
215
+ await initialize();
216
+ const readTreeResult = await runCheckpointGitCommand(gitDir, workDir, [
217
+ "read-tree",
218
+ checkpointId
219
+ ]);
220
+ if (readTreeResult.exitCode !== 0) {
221
+ throw new Error(`Failed to read checkpoint: ${readTreeResult.stderr}`);
222
+ }
223
+ const checkoutResult = await runCheckpointGitCommand(gitDir, workDir, [
224
+ "checkout-index",
225
+ "-a",
226
+ "-f"
227
+ ]);
228
+ if (checkoutResult.exitCode !== 0) {
229
+ throw new Error(`Failed to restore files: ${checkoutResult.stderr}`);
230
+ }
231
+ const changes = await manager.changes(checkpointId);
232
+ for (const change of changes.files) {
233
+ if (change.type === "added") {
234
+ try {
235
+ await unlink(join2(workDir, change.path));
236
+ } catch {
237
+ }
238
+ }
239
+ }
240
+ },
241
+ async changes(checkpointId) {
242
+ await initialize();
243
+ await runCheckpointGitCommand(gitDir, workDir, ["add", "--all", "."]);
244
+ const diffStatResult = await runCheckpointGitCommand(gitDir, workDir, [
245
+ "diff",
246
+ "--name-status",
247
+ checkpointId
248
+ ]);
249
+ const files = [];
250
+ if (diffStatResult.exitCode === 0 && diffStatResult.stdout) {
251
+ for (const line of diffStatResult.stdout.split("\n")) {
252
+ if (!line.trim()) {
253
+ continue;
254
+ }
255
+ const [status, ...pathParts] = line.split(" ");
256
+ files.push({
257
+ path: pathParts.join(" "),
258
+ type: classifyChangeType(status)
259
+ });
260
+ }
261
+ }
262
+ const diffResult = await runCheckpointGitCommand(gitDir, workDir, [
263
+ "diff",
264
+ "--no-ext-diff",
265
+ checkpointId,
266
+ "--"
267
+ ]);
268
+ return {
269
+ fromCheckpoint: checkpointId,
270
+ files,
271
+ diff: diffResult.stdout
272
+ };
273
+ },
274
+ async undoFiles(checkpointId, files) {
275
+ await initialize();
276
+ for (const file of files) {
277
+ const relativePath = relative(workDir, resolve(workDir, file));
278
+ const result = await runCheckpointGitCommand(gitDir, workDir, [
279
+ "checkout",
280
+ checkpointId,
281
+ "--",
282
+ relativePath
283
+ ]);
284
+ if (result.exitCode === 0) {
285
+ continue;
286
+ }
287
+ const lsResult = await runCheckpointGitCommand(gitDir, workDir, [
288
+ "ls-tree",
289
+ checkpointId,
290
+ "--",
291
+ relativePath
292
+ ]);
293
+ if (!lsResult.stdout.trim()) {
294
+ try {
295
+ await unlink(join2(workDir, relativePath));
296
+ } catch {
297
+ }
298
+ }
299
+ }
300
+ },
301
+ async list() {
302
+ return [...state.checkpointLog].reverse();
303
+ },
304
+ async remove(checkpointId) {
305
+ await removeCheckpointFromLog(logPath, checkpointId);
306
+ state.checkpointLog = state.checkpointLog.filter(
307
+ (checkpoint) => checkpoint.id !== checkpointId
308
+ );
309
+ },
310
+ async prune() {
311
+ const toRemove = state.checkpointLog.length - state.config.maxCheckpoints;
312
+ if (toRemove <= 0) {
313
+ return 0;
314
+ }
315
+ const removeIds = state.checkpointLog.slice(0, toRemove).map((checkpoint) => checkpoint.id);
316
+ for (const checkpointId of removeIds) {
317
+ await removeCheckpointFromLog(logPath, checkpointId);
318
+ }
319
+ state.checkpointLog = state.checkpointLog.slice(toRemove);
320
+ await runCheckpointGitCommand(gitDir, workDir, ["gc", "--prune=now"]);
321
+ return removeIds.length;
322
+ },
323
+ async latest() {
324
+ return state.checkpointLog[state.checkpointLog.length - 1] ?? null;
325
+ },
326
+ async getFileAt(checkpointId, filePath) {
327
+ await initialize();
328
+ const relativePath = relative(workDir, resolve(workDir, filePath));
329
+ const result = await runCheckpointGitCommand(gitDir, workDir, [
330
+ "show",
331
+ `${checkpointId}:${relativePath}`
332
+ ]);
333
+ return result.exitCode === 0 ? result.stdout : null;
334
+ },
335
+ isInitialized() {
336
+ return state.initialized;
337
+ },
338
+ async close() {
339
+ state.initialized = false;
340
+ }
341
+ };
342
+ return manager;
343
+ }
344
+ async function clearCheckpoints(workDir) {
345
+ const storageDir = getDefaultCheckpointStorageDir(workDir);
346
+ try {
347
+ await rm(storageDir, { recursive: true, force: true });
348
+ } catch {
349
+ }
350
+ }
351
+
352
+ // src/tracking/turn-tracker/tracker.ts
353
+ import { spawn as spawn2 } from "child_process";
354
+ import { normalize, relative as relative3, resolve as resolve2 } from "path";
355
+
356
+ // src/tracking/turn-tracker/diff.ts
357
+ import { createHash as createHash2 } from "crypto";
358
+ import { mkdir as mkdir3, readFile as readFile2, stat, unlink as unlink2, writeFile as writeFile2 } from "fs/promises";
359
+ import { dirname as dirname2, relative as relative2 } from "path";
360
+ async function captureTurnFileBaseline(absPath) {
361
+ try {
362
+ const content = await readFile2(absPath, "utf-8");
363
+ const stats = await stat(absPath);
364
+ return {
365
+ path: absPath,
366
+ content,
367
+ mode: stats.mode,
368
+ hash: hashTurnTrackerContent(content),
369
+ capturedAt: /* @__PURE__ */ new Date()
370
+ };
371
+ } catch (error) {
372
+ if (error.code === "ENOENT") {
373
+ return {
374
+ path: absPath,
375
+ content: null,
376
+ mode: null,
377
+ hash: null,
378
+ capturedAt: /* @__PURE__ */ new Date()
379
+ };
380
+ }
381
+ throw error;
382
+ }
383
+ }
384
+ async function restoreTurnTrackedFile(absPath, baseline) {
385
+ if (baseline.content === null) {
386
+ try {
387
+ await unlink2(absPath);
388
+ } catch (error) {
389
+ if (error.code !== "ENOENT") {
390
+ throw error;
391
+ }
392
+ }
393
+ return;
394
+ }
395
+ await mkdir3(dirname2(absPath), { recursive: true });
396
+ await writeFile2(absPath, baseline.content, {
397
+ mode: baseline.mode ?? void 0
398
+ });
399
+ }
400
+ async function computeTurnTrackerFileChange(options) {
401
+ const { cwd, absPath, baseline, preferGitDiff, canUseGitDiff } = options;
402
+ const relPath = relative2(cwd, absPath);
403
+ const currentContent = await readTrackedFileContent(absPath);
404
+ let type;
405
+ if (baseline.content === null && currentContent === null) {
406
+ type = "unchanged";
407
+ } else if (baseline.content === null && currentContent !== null) {
408
+ type = "created";
409
+ } else if (baseline.content !== null && currentContent === null) {
410
+ type = "deleted";
411
+ } else if (baseline.content === currentContent) {
412
+ type = "unchanged";
413
+ } else {
414
+ type = "modified";
415
+ }
416
+ if (type === "unchanged") {
417
+ return { path: relPath, type, additions: 0, deletions: 0 };
418
+ }
419
+ const diff = await generateTurnTrackerDiff({
420
+ relPath,
421
+ oldContent: baseline.content,
422
+ newContent: currentContent,
423
+ preferGitDiff,
424
+ canUseGitDiff
425
+ });
426
+ const { additions, deletions } = countUnifiedDiffStats(diff);
427
+ return {
428
+ path: relPath,
429
+ type,
430
+ additions,
431
+ deletions,
432
+ diff
433
+ };
434
+ }
435
+ async function readTrackedFileContent(absPath) {
436
+ try {
437
+ return await readFile2(absPath, "utf-8");
438
+ } catch (error) {
439
+ if (error.code === "ENOENT") {
440
+ return null;
441
+ }
442
+ throw error;
443
+ }
444
+ }
445
+ async function generateTurnTrackerDiff(options) {
446
+ const { relPath, oldContent, newContent, preferGitDiff, canUseGitDiff } = options;
447
+ const oldLines = (oldContent ?? "").split("\n");
448
+ const newLines = (newContent ?? "").split("\n");
449
+ if (preferGitDiff && canUseGitDiff) {
450
+ const gitDiff = await createGitStyleDiffPlaceholder();
451
+ if (gitDiff) {
452
+ return gitDiff;
453
+ }
454
+ }
455
+ return buildSimpleUnifiedDiff({
456
+ path: relPath,
457
+ oldLines,
458
+ newLines,
459
+ wasCreated: oldContent === null,
460
+ wasDeleted: newContent === null
461
+ });
462
+ }
463
+ function buildSimpleUnifiedDiff(options) {
464
+ const { path, oldLines, newLines, wasCreated, wasDeleted } = options;
465
+ const header = [
466
+ `--- a/${path}${wasCreated ? " (new file)" : ""}`,
467
+ `+++ b/${path}${wasDeleted ? " (deleted)" : ""}`
468
+ ];
469
+ const hunks = [];
470
+ if (wasCreated) {
471
+ hunks.push(`@@ -0,0 +1,${newLines.length} @@`);
472
+ for (const line of newLines) {
473
+ hunks.push(`+${line}`);
474
+ }
475
+ } else if (wasDeleted) {
476
+ hunks.push(`@@ -1,${oldLines.length} +0,0 @@`);
477
+ for (const line of oldLines) {
478
+ hunks.push(`-${line}`);
479
+ }
480
+ } else {
481
+ hunks.push(`@@ -1,${oldLines.length} +1,${newLines.length} @@`);
482
+ for (const line of oldLines) {
483
+ hunks.push(`-${line}`);
484
+ }
485
+ for (const line of newLines) {
486
+ hunks.push(`+${line}`);
487
+ }
488
+ }
489
+ return [...header, ...hunks].join("\n");
490
+ }
491
+ function countUnifiedDiffStats(diff) {
492
+ let additions = 0;
493
+ let deletions = 0;
494
+ for (const line of diff.split("\n")) {
495
+ if (line.startsWith("+") && !line.startsWith("+++")) {
496
+ additions++;
497
+ } else if (line.startsWith("-") && !line.startsWith("---")) {
498
+ deletions++;
499
+ }
500
+ }
501
+ return { additions, deletions };
502
+ }
503
+ function hashTurnTrackerContent(content) {
504
+ return createHash2("sha256").update(content).digest("hex").slice(0, 16);
505
+ }
506
+ async function createGitStyleDiffPlaceholder() {
507
+ return null;
508
+ }
509
+
510
+ // src/tracking/turn-tracker/tracker.ts
511
+ var TurnChangeTracker = class {
512
+ config;
513
+ currentTurn = null;
514
+ gitDetected = null;
515
+ constructor(config) {
516
+ this.config = {
517
+ cwd: resolve2(config.cwd),
518
+ useGit: config.useGit ?? true,
519
+ maxTrackedFiles: config.maxTrackedFiles ?? 100
520
+ };
521
+ }
522
+ startTurn(turnId) {
523
+ if (this.currentTurn && !this.currentTurn.completed) {
524
+ this.currentTurn.completed = true;
525
+ }
526
+ this.currentTurn = {
527
+ id: turnId,
528
+ startedAt: /* @__PURE__ */ new Date(),
529
+ baselines: /* @__PURE__ */ new Map(),
530
+ completed: false
531
+ };
532
+ }
533
+ async endTurn() {
534
+ if (!this.currentTurn) {
535
+ return {
536
+ turnId: "",
537
+ files: [],
538
+ totalTracked: 0,
539
+ additions: 0,
540
+ deletions: 0,
541
+ diff: null,
542
+ duration: 0
543
+ };
544
+ }
545
+ const turn = this.currentTurn;
546
+ turn.completed = true;
547
+ const files = [];
548
+ let additions = 0;
549
+ let deletions = 0;
550
+ const diffs = [];
551
+ for (const [absPath, baseline] of turn.baselines) {
552
+ const change = await this.computeFileChange(absPath, baseline);
553
+ files.push(change);
554
+ additions += change.additions;
555
+ deletions += change.deletions;
556
+ if (change.diff) {
557
+ diffs.push(change.diff);
558
+ }
559
+ }
560
+ files.sort((left, right) => left.path.localeCompare(right.path));
561
+ return {
562
+ turnId: turn.id,
563
+ files,
564
+ totalTracked: turn.baselines.size,
565
+ additions,
566
+ deletions,
567
+ diff: diffs.length > 0 ? diffs.join("\n") : null,
568
+ duration: Date.now() - turn.startedAt.getTime()
569
+ };
570
+ }
571
+ isInTurn() {
572
+ return this.currentTurn !== null && !this.currentTurn.completed;
573
+ }
574
+ getCurrentTurnId() {
575
+ return this.currentTurn?.id ?? null;
576
+ }
577
+ async beforeWrite(filePath) {
578
+ if (!this.currentTurn || this.currentTurn.completed) {
579
+ return false;
580
+ }
581
+ const absPath = resolve2(this.config.cwd, filePath);
582
+ const normalizedPath = normalize(absPath);
583
+ if (this.currentTurn.baselines.has(normalizedPath)) {
584
+ return false;
585
+ }
586
+ if (this.currentTurn.baselines.size >= this.config.maxTrackedFiles) {
587
+ console.warn(
588
+ `[TurnTracker] Max tracked files (${this.config.maxTrackedFiles}) reached, skipping: ${filePath}`
589
+ );
590
+ return false;
591
+ }
592
+ const baseline = await captureTurnFileBaseline(normalizedPath);
593
+ this.currentTurn.baselines.set(normalizedPath, baseline);
594
+ return true;
595
+ }
596
+ getTrackedFiles() {
597
+ if (!this.currentTurn) {
598
+ return [];
599
+ }
600
+ return Array.from(this.currentTurn.baselines.keys()).map(
601
+ (path) => relative3(this.config.cwd, path)
602
+ );
603
+ }
604
+ isTracking(filePath) {
605
+ if (!this.currentTurn) {
606
+ return false;
607
+ }
608
+ const absPath = resolve2(this.config.cwd, filePath);
609
+ return this.currentTurn.baselines.has(normalize(absPath));
610
+ }
611
+ async getDiff() {
612
+ if (!this.currentTurn || this.currentTurn.baselines.size === 0) {
613
+ return null;
614
+ }
615
+ const diffs = [];
616
+ for (const [absPath, baseline] of this.currentTurn.baselines) {
617
+ const change = await this.computeFileChange(absPath, baseline);
618
+ if (change.diff) {
619
+ diffs.push(change.diff);
620
+ }
621
+ }
622
+ return diffs.length > 0 ? diffs.join("\n") : null;
623
+ }
624
+ async getFileDiff(filePath) {
625
+ if (!this.currentTurn) {
626
+ return null;
627
+ }
628
+ const absPath = resolve2(this.config.cwd, filePath);
629
+ const baseline = this.currentTurn.baselines.get(normalize(absPath));
630
+ if (!baseline) {
631
+ return null;
632
+ }
633
+ const change = await this.computeFileChange(absPath, baseline);
634
+ return change.diff ?? null;
635
+ }
636
+ async undoTurn() {
637
+ if (!this.currentTurn) {
638
+ return { restored: [], failed: [] };
639
+ }
640
+ const result = { restored: [], failed: [] };
641
+ for (const [absPath, baseline] of this.currentTurn.baselines) {
642
+ const relPath = relative3(this.config.cwd, absPath);
643
+ try {
644
+ await restoreTurnTrackedFile(absPath, baseline);
645
+ result.restored.push(relPath);
646
+ } catch (error) {
647
+ result.failed.push({
648
+ path: relPath,
649
+ reason: error instanceof Error ? error.message : String(error)
650
+ });
651
+ }
652
+ }
653
+ return result;
654
+ }
655
+ async undoFiles(filePaths) {
656
+ if (!this.currentTurn) {
657
+ return { restored: [], failed: [] };
658
+ }
659
+ const result = { restored: [], failed: [] };
660
+ for (const filePath of filePaths) {
661
+ const absPath = resolve2(this.config.cwd, filePath);
662
+ const normalizedPath = normalize(absPath);
663
+ const baseline = this.currentTurn.baselines.get(normalizedPath);
664
+ if (!baseline) {
665
+ result.failed.push({
666
+ path: filePath,
667
+ reason: "File not tracked in current turn"
668
+ });
669
+ continue;
670
+ }
671
+ try {
672
+ await restoreTurnTrackedFile(absPath, baseline);
673
+ result.restored.push(filePath);
674
+ } catch (error) {
675
+ result.failed.push({
676
+ path: filePath,
677
+ reason: error instanceof Error ? error.message : String(error)
678
+ });
679
+ }
680
+ }
681
+ return result;
682
+ }
683
+ async computeFileChange(absPath, baseline) {
684
+ return await computeTurnTrackerFileChange({
685
+ cwd: this.config.cwd,
686
+ absPath,
687
+ baseline,
688
+ preferGitDiff: this.config.useGit,
689
+ canUseGitDiff: await this.isInGitRepo()
690
+ });
691
+ }
692
+ async isInGitRepo() {
693
+ if (!this.config.useGit) {
694
+ return false;
695
+ }
696
+ if (this.gitDetected !== null) {
697
+ return this.gitDetected;
698
+ }
699
+ return await new Promise((resolvePromise) => {
700
+ const proc = spawn2("git", ["rev-parse", "--git-dir"], {
701
+ cwd: this.config.cwd,
702
+ stdio: ["ignore", "pipe", "pipe"]
703
+ });
704
+ proc.on("close", (code) => {
705
+ this.gitDetected = code === 0;
706
+ resolvePromise(this.gitDetected);
707
+ });
708
+ proc.on("error", () => {
709
+ this.gitDetected = false;
710
+ resolvePromise(false);
711
+ });
712
+ });
713
+ }
714
+ };
715
+ function createTurnTracker(config) {
716
+ return new TurnChangeTracker(config);
717
+ }
718
+
719
+ export {
720
+ TurnChangeTracker,
721
+ createTurnTracker,
722
+ createCheckpointManager,
723
+ clearCheckpoints
724
+ };