@agimon-ai/doompi-file-edit 0.0.1-alpha.58 → 0.0.1-alpha.64

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 (134) hide show
  1. package/README.md +8 -3
  2. package/dist/adapters/EditTracker/EditTracker.cjs +1 -1
  3. package/dist/adapters/EditTracker/EditTracker.cjs.map +1 -1
  4. package/dist/adapters/EditTracker/EditTracker.mjs +1 -1
  5. package/dist/adapters/EditTracker/EditTracker.mjs.map +1 -1
  6. package/dist/adapters/EditorConfigService/EditorConfigService.cjs +1 -1
  7. package/dist/adapters/EditorConfigService/EditorConfigService.mjs +1 -1
  8. package/dist/adapters/EditorLauncher/EditorLauncher.cjs +1 -1
  9. package/dist/adapters/EditorLauncher/EditorLauncher.mjs +1 -1
  10. package/dist/adapters/FileEditPaths/FileEditPaths.cjs +1 -1
  11. package/dist/adapters/FileEditPaths/FileEditPaths.cjs.map +1 -1
  12. package/dist/adapters/FileEditPaths/FileEditPaths.mjs +1 -1
  13. package/dist/adapters/FileEditPaths/FileEditPaths.mjs.map +1 -1
  14. package/dist/adapters/GitDiffService/GitDiffService.cjs +3 -3
  15. package/dist/adapters/GitDiffService/GitDiffService.mjs +3 -3
  16. package/dist/adapters/TimelineStore/TimelineStore.cjs +1 -1
  17. package/dist/adapters/TimelineStore/TimelineStore.cjs.map +1 -1
  18. package/dist/adapters/TimelineStore/TimelineStore.mjs +1 -1
  19. package/dist/adapters/TimelineStore/TimelineStore.mjs.map +1 -1
  20. package/dist/adapters/fileEditsApi.cjs +1 -1
  21. package/dist/adapters/fileEditsApi.cjs.map +1 -1
  22. package/dist/adapters/fileEditsApi.d.cts +3 -4
  23. package/dist/adapters/fileEditsApi.d.cts.map +1 -1
  24. package/dist/adapters/fileEditsApi.d.mts +3 -4
  25. package/dist/adapters/fileEditsApi.d.mts.map +1 -1
  26. package/dist/adapters/fileEditsApi.mjs +1 -1
  27. package/dist/adapters/fileEditsApi.mjs.map +1 -1
  28. package/dist/adapters/node/sessionStateSweep.cjs +2 -0
  29. package/dist/adapters/node/sessionStateSweep.cjs.map +1 -0
  30. package/dist/adapters/node/sessionStateSweep.mjs +2 -0
  31. package/dist/adapters/node/sessionStateSweep.mjs.map +1 -0
  32. package/dist/adapters/node/snapshotStore.cjs +1 -1
  33. package/dist/adapters/node/snapshotStore.mjs +1 -1
  34. package/dist/adapters/node/treeManifest.cjs +1 -1
  35. package/dist/adapters/node/treeManifest.cjs.map +1 -1
  36. package/dist/adapters/node/treeManifest.mjs +1 -1
  37. package/dist/adapters/node/treeManifest.mjs.map +1 -1
  38. package/dist/adapters/pi/extension.cjs +1 -1
  39. package/dist/adapters/pi/extension.cjs.map +1 -1
  40. package/dist/adapters/pi/extension.d.cts +1 -2
  41. package/dist/adapters/pi/extension.d.cts.map +1 -1
  42. package/dist/adapters/pi/extension.d.mts +1 -2
  43. package/dist/adapters/pi/extension.d.mts.map +1 -1
  44. package/dist/adapters/pi/extension.mjs +1 -1
  45. package/dist/adapters/pi/extension.mjs.map +1 -1
  46. package/dist/adapters/webFilesChannel.cjs +1 -1
  47. package/dist/adapters/webFilesChannel.d.cts +5 -6
  48. package/dist/adapters/webFilesChannel.d.cts.map +1 -1
  49. package/dist/adapters/webFilesChannel.d.mts +5 -6
  50. package/dist/adapters/webFilesChannel.d.mts.map +1 -1
  51. package/dist/adapters/webFilesChannel.mjs +1 -1
  52. package/dist/container/index.d.cts +1 -2
  53. package/dist/container/index.d.cts.map +1 -1
  54. package/dist/container/index.d.mts +1 -2
  55. package/dist/container/index.d.mts.map +1 -1
  56. package/dist/services/doomIgnore.cjs +1 -1
  57. package/dist/services/doomIgnore.cjs.map +1 -1
  58. package/dist/services/doomIgnore.mjs +1 -1
  59. package/dist/services/doomIgnore.mjs.map +1 -1
  60. package/dist/services/fileChanges.cjs +2 -2
  61. package/dist/services/fileChanges.cjs.map +1 -1
  62. package/dist/services/fileChanges.mjs +2 -2
  63. package/dist/services/fileChanges.mjs.map +1 -1
  64. package/dist/services/lineDiff.cjs +2 -1
  65. package/dist/services/lineDiff.cjs.map +1 -1
  66. package/dist/services/lineDiff.mjs +2 -1
  67. package/dist/services/lineDiff.mjs.map +1 -1
  68. package/dist/tui/fileEditWorkflow.cjs +1 -1
  69. package/dist/tui/fileEditWorkflow.cjs.map +1 -1
  70. package/dist/tui/fileEditWorkflow.mjs +1 -1
  71. package/dist/tui/fileEditWorkflow.mjs.map +1 -1
  72. package/dist/types/domain.d.cts +20 -10
  73. package/dist/types/domain.d.cts.map +1 -1
  74. package/dist/types/domain.d.mts +20 -10
  75. package/dist/types/domain.d.mts.map +1 -1
  76. package/dist/types/editTracker.d.cts +5 -2
  77. package/dist/types/editTracker.d.cts.map +1 -1
  78. package/dist/types/editTracker.d.mts +5 -2
  79. package/dist/types/editTracker.d.mts.map +1 -1
  80. package/dist/types/editorConfigService.d.cts +1 -2
  81. package/dist/types/editorConfigService.d.cts.map +1 -1
  82. package/dist/types/editorConfigService.d.mts +1 -2
  83. package/dist/types/editorConfigService.d.mts.map +1 -1
  84. package/dist/types/editorLauncher.d.cts +3 -4
  85. package/dist/types/editorLauncher.d.cts.map +1 -1
  86. package/dist/types/editorLauncher.d.mts +3 -4
  87. package/dist/types/editorLauncher.d.mts.map +1 -1
  88. package/dist/types/fileEditPaths.d.cts +12 -2
  89. package/dist/types/fileEditPaths.d.cts.map +1 -1
  90. package/dist/types/fileEditPaths.d.mts +12 -2
  91. package/dist/types/fileEditPaths.d.mts.map +1 -1
  92. package/dist/types/fileEditWorkflow.d.cts +1 -2
  93. package/dist/types/fileEditWorkflow.d.cts.map +1 -1
  94. package/dist/types/fileEditWorkflow.d.mts +1 -2
  95. package/dist/types/fileEditWorkflow.d.mts.map +1 -1
  96. package/dist/types/gitDiffService.d.cts +1 -2
  97. package/dist/types/gitDiffService.d.cts.map +1 -1
  98. package/dist/types/gitDiffService.d.mts +1 -2
  99. package/dist/types/gitDiffService.d.mts.map +1 -1
  100. package/dist/types/index.d.cts +1 -2
  101. package/dist/types/index.d.cts.map +1 -1
  102. package/dist/types/index.d.mts +1 -2
  103. package/dist/types/index.d.mts.map +1 -1
  104. package/dist/types/snapshotStore.d.cts +1 -2
  105. package/dist/types/snapshotStore.d.cts.map +1 -1
  106. package/dist/types/snapshotStore.d.mts +1 -2
  107. package/dist/types/snapshotStore.d.mts.map +1 -1
  108. package/dist/types/timelineStore.d.cts +1 -2
  109. package/dist/types/timelineStore.d.cts.map +1 -1
  110. package/dist/types/timelineStore.d.mts +1 -2
  111. package/dist/types/timelineStore.d.mts.map +1 -1
  112. package/dist/types/treeManifest.d.cts +16 -3
  113. package/dist/types/treeManifest.d.cts.map +1 -1
  114. package/dist/types/treeManifest.d.mts +16 -3
  115. package/dist/types/treeManifest.d.mts.map +1 -1
  116. package/dist/types/webFiles.d.cts +1 -2
  117. package/dist/types/webFiles.d.cts.map +1 -1
  118. package/dist/types/webFiles.d.mts +1 -2
  119. package/dist/types/webFiles.d.mts.map +1 -1
  120. package/package.json +19 -19
  121. package/src/types/domain.ts +11 -0
  122. package/src/web/{filesApi.ts → api/filesApi.ts} +1 -1
  123. package/src/web/{CommentDraft.tsx → components/CommentDraft.tsx} +1 -1
  124. package/src/web/{DiffView.tsx → components/DiffView.tsx} +2 -2
  125. package/src/web/{FilePanel.tsx → components/FilePanel.tsx} +13 -5
  126. package/src/web/{FilePreviewPanel.tsx → components/FilePreviewPanel.tsx} +5 -4
  127. package/src/web/{FilesActivitySection.tsx → components/FilesActivitySection.tsx} +2 -2
  128. package/src/web/{FilesBrowser.tsx → components/FilesBrowser.tsx} +83 -20
  129. package/src/web/components/SessionMediaPreview.tsx +40 -0
  130. package/src/web/{fileLinks.ts → components/fileLinks.ts} +2 -2
  131. package/src/web/index.ts +3 -3
  132. package/src/web/{fileView.ts → lib/fileView.ts} +78 -4
  133. package/src/web/{filesStore.ts → stores/filesStore.ts} +3 -20
  134. /package/src/web/{DeleteFileDialog.tsx → components/DeleteFileDialog.tsx} +0 -0
package/README.md CHANGED
@@ -97,9 +97,14 @@ not passed through a shell, so shell operators and expansions are not interprete
97
97
  ## Data behavior
98
98
 
99
99
  The timeline contains file paths, and the snapshot store holds copies of the file content the
100
- session changed. That content can be sensitive. Both live in session-scoped package state beside the
101
- Pi agent directory, are removed when the session ends, and should be protected with the same access
102
- controls as Pi session data.
100
+ session changed. That content can be sensitive. Both live under the Pi agent directory,
101
+ `$PI_CODING_AGENT_DIR` or `~/.pi/agent/doom-file-edit`, never inside the working tree, and should be
102
+ protected with the same access controls as Pi session data.
103
+
104
+ A session removes its own timeline and snapshots when it ends. A session that is killed cannot, so
105
+ each session also sweeps state that no longer belongs to anyone: a timeline and its snapshots are
106
+ removed once nothing has written to them for a week. An older build kept this state in the
107
+ repository's git common directory, and that directory is cleared away under the same rule.
103
108
 
104
109
  The session API reads and writes only files the timeline already records, so a request cannot reach
105
110
  a file this session never changed.
@@ -1,2 +1,2 @@
1
- const e=require("../../_virtual/_rolldown/runtime.cjs"),t=require("../../services/lineDiff.cjs");let n=require("node:path");n=e.__toESM(n,1);const r=`bash`;function i(e,t){if(!e||typeof e!=`object`)return;let n=e[t];return typeof n==`string`?n:void 0}var a=class{timeline;snapshots;manifests;pending=new Map;bracketed=new Map;manifest;excluded=[];contents=new Map;now;git;constructor(e,t,n,r={}){this.timeline=e,this.snapshots=t,this.manifests=n,this.now=r.now??Date.now,this.git=r.git}reset(e={}){this.pending.clear(),this.bracketed.clear(),this.contents.clear(),this.manifest=void 0,this.excluded=e.exclude??[]}async start(e,t,a,o){if(t===`edit`||t===`write`){let r=i(a,`path`);if(!r)return;let s=n.default.resolve(o,r);this.pending.set(e,{tool:t,filePath:s,before:await this.snapshots.capture(s)});return}t===r&&(this.bracketed.set(e,this.now()),this.manifest??=await this.manifests.take(o,this.excluded))}async end(e,t,n){let r=this.pending.get(e);this.pending.delete(e);let i=this.bracketed.get(e);if(this.bracketed.delete(e),!t){if(r){await this.recordTool(r);return}i!==void 0&&await this.recordScan(n,i)}}async recordTool(e){let t=await this.snapshots.capture(e.filePath),n=await this.countChanges(e.before,t);t!==void 0&&this.contents.set(e.filePath,t),await this.append({version:2,path:e.filePath,tool:e.tool,at:this.now(),origin:`tool`,...e.before===void 0?{}:{before:e.before},...t===void 0?{}:{after:t},...n}),await this.remember(e.filePath)}async recordScan(e,t){let n=this.manifest;if(n===void 0)return;let i=await this.manifests.take(e,this.excluded);this.manifest=i;let a=await this.writtenDuring(this.manifests.changed(n,i),t);if(a.length===0)return;let o=await this.unchangedInGit(e,a);for(let e of a){let t=await this.snapshots.capture(e),n=this.contents.get(e);t!==void 0&&this.contents.set(e,t),(t===void 0||t!==n)&&(n===void 0&&o.has(e)||await this.append({version:2,path:e,tool:r,at:this.now(),origin:`scan`,verified:!0,...t===void 0?{}:{after:t}}))}}async writtenDuring(e,t){let n=t-1e3,r=[];for(let t of e){let e=await this.manifests.modifiedAt(t);(e===void 0||e>=n)&&r.push(t)}return r}async unchangedInGit(e,t){let n=t.filter(e=>!this.contents.has(e));return this.git===void 0||n.length===0?new Set:this.git.unchanged(e,n)}async countChanges(e,n){if(e===void 0||n===void 0)return{};if(e===n)return{additions:0,removals:0};let[r,i]=await Promise.all([this.snapshots.read(e),this.snapshots.read(n)]);if(r===void 0||i===void 0)return{};let a=t.lineDiff(r,i);return{additions:a.additions,removals:a.removals}}async append(e){(e.before===void 0||e.before!==e.after)&&await this.timeline.append(e)}async remember(e){if(this.manifest===void 0)return;let t=new Map(this.manifest.entries),n=await this.manifests.fingerprint(e);n===void 0?t.delete(e):t.set(e,n),this.manifest={entries:t,truncated:this.manifest.truncated}}};exports.EditTracker=a;
1
+ const e=require("../../_virtual/_rolldown/runtime.cjs"),t=require("../../services/lineDiff.cjs");let n=require("node:path");n=e.__toESM(n,1);const r=`bash`;function i(e,t){if(!e||typeof e!=`object`)return;let n=e[t];return typeof n==`string`?n:void 0}var a=class{timeline;snapshots;manifests;pending=new Map;bracketed=new Map;manifest;excluded=[];isIgnored;contents=new Map;now;git;constructor(e,t,n,r={}){this.timeline=e,this.snapshots=t,this.manifests=n,this.now=r.now??Date.now,this.git=r.git}reset(e={}){this.pending.clear(),this.bracketed.clear(),this.contents.clear(),this.manifest=void 0,this.excluded=e.exclude??[],this.isIgnored=e.isIgnored}async start(e,t,a,o){if(t===`edit`||t===`write`){let r=i(a,`path`);if(!r)return;let s=n.default.resolve(o,r),[c,l]=await Promise.all([this.snapshots.capture(s),this.manifests.fingerprint(s)]);this.pending.set(e,{tool:t,filePath:s,before:c,existed:l!==void 0});return}t===r&&(this.bracketed.set(e,this.now()),this.manifest??=await this.manifests.take(o,this.excluded))}async end(e,t,n){let r=this.pending.get(e);this.pending.delete(e);let i=this.bracketed.get(e);if(this.bracketed.delete(e),r){t||await this.recordTool(r);return}i!==void 0&&await this.recordScan(n,i)}async recordTool(e){let t=await this.snapshots.capture(e.filePath),n=!e.existed,r=await this.countChanges(e.before,t,n);t!==void 0&&this.contents.set(e.filePath,t),await this.append({version:2,path:e.filePath,tool:e.tool,at:this.now(),origin:`tool`,...e.before===void 0?{}:{before:e.before},...t===void 0?{}:{after:t},...n?{created:!0}:{},...r}),await this.remember(e.filePath)}async recordScan(e,t){let n=this.manifest;if(n===void 0)return;let i=await this.manifests.take(e,this.excluded);this.manifest=i;let a=this.isIgnored,o=this.manifests.changed(n,i).filter(e=>a===void 0||!a(e)),s=await this.writtenDuring(o,t);if(s.length===0)return;let c=await this.unchangedInGit(e,s);for(let e of s){let t=this.contents.get(e);if(t===void 0&&c.has(e))continue;let a=n.entries.get(e),o=i.entries.get(e),s=o===void 0?void 0:await this.snapshots.capture(e);if(s!==void 0&&this.contents.set(e,s),s!==void 0&&s===t)continue;let l=a===void 0||o===void 0||this.manifests.sizeChanged(a,o)||t!==void 0&&s!==void 0&&s!==t;await this.append({version:2,path:e,tool:r,at:this.now(),origin:`scan`,...l?{verified:!0}:{},...s===void 0?{}:{after:s}})}}async writtenDuring(e,t){let n=t-1e3,r=[];for(let t of e){let e=await this.manifests.modifiedAt(t);(e===void 0||e>=n)&&r.push(t)}return r}async unchangedInGit(e,t){let n=t.filter(e=>!this.contents.has(e));return this.git===void 0||n.length===0?new Set:this.git.unchanged(e,n)}async countChanges(e,n,r){if(n===void 0)return{};if(e===void 0){if(!r)return{};let e=await this.snapshots.read(n);if(e===void 0)return{};let i=t.lineDiffFromEmpty(e);return{additions:i.additions,removals:i.removals}}if(e===n)return{additions:0,removals:0};let[i,a]=await Promise.all([this.snapshots.read(e),this.snapshots.read(n)]);if(i===void 0||a===void 0)return{};let o=t.lineDiff(i,a);return{additions:o.additions,removals:o.removals}}async append(e){(e.before===void 0||e.before!==e.after)&&await this.timeline.append(e)}async remember(e){if(this.manifest===void 0)return;let t=new Map(this.manifest.entries),n=await this.manifests.fingerprint(e);n===void 0?t.delete(e):t.set(e,n),this.manifest={entries:t,truncated:this.manifest.truncated}}};exports.EditTracker=a;
2
2
  //# sourceMappingURL=EditTracker.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"EditTracker.cjs","names":["path","lineDiff"],"sources":["../../../src/adapters/EditTracker/EditTracker.ts"],"sourcesContent":["import path from 'node:path';\nimport { lineDiff } from '../../services/lineDiff.ts';\nimport type { FileEditTool, TimelineEvent } from '../../types/domain';\nimport type { IEditTracker } from '../../types/editTracker';\nimport type { GitStatusPort } from '../../types/gitStatus.ts';\nimport type { SnapshotStorePort } from '../../types/snapshotStore.ts';\nimport type { ITimelineStore } from '../../types/timelineStore';\nimport type { TreeManifest, TreeManifestPort } from '../../types/treeManifest.ts';\n\nconst EDIT_TOOL = 'edit';\nconst WRITE_TOOL = 'write';\nconst BASH_TOOL = 'bash';\n\n/**\n * Records what the session changed, and captures enough of it to be diffed.\n *\n * Two mechanisms, because one is not enough. `edit` and `write` name their file\n * in the call arguments, so the content is read before the tool runs and both\n * sides of the change are known exactly. `bash` names nothing reliable: the\n * agent can write a script and run it, and reading path-shaped tokens out of\n * the command would miss every file that script touches. So a bash call is\n * bracketed by tree manifests instead, and whatever moved between them is\n * recorded, however it was written.\n *\n * A manifest-found path is recorded without a baseline, because it was only\n * identified after it had already changed. That is a real limit and the wire\n * carries it as `origin: 'scan'` rather than pretending a diff exists.\n *\n * A manifest compares size and modification time, and it compares the tree as\n * this tracker last walked it against the tree now, so it answers whether a\n * file differs, never when it moved. A path a previous session left dirty, or\n * one a checkout or an install rewrote, differs exactly like a path the command\n * just wrote. So a candidate is confirmed three ways before it is recorded: its\n * modification time has to fall inside the call that is being closed, its\n * content hash has to differ from the last one this session saw, and a path\n * this session has never captured is put to git. Not every answer is available\n * for every file, an ignored temporary file is unknown to git and a first\n * sighting has no earlier hash, so an unconfirmable candidate is still\n * recorded; only a proven non-change is dropped.\n */\ninterface PendingEdit {\n tool: 'edit' | 'write';\n filePath: string;\n before: string | undefined;\n}\n\nfunction objectValue(value: unknown, key: string): string | undefined {\n if (!value || typeof value !== 'object') return undefined;\n const found = (value as Record<string, unknown>)[key];\n return typeof found === 'string' ? found : undefined;\n}\n\nexport interface EditTrackerOptions {\n /** Injectable so a test can pin the recorded timestamps. */\n now?: () => number;\n /**\n * Decides whether a first-seen scan candidate actually changed. Optional:\n * without it the tracker keeps recording every candidate it cannot disprove.\n */\n git?: GitStatusPort;\n}\n\n/**\n * How far before a call started a write may claim to have happened and still be\n * attributed to it. A working tree can sit on a filesystem that stores whole\n * seconds, which floors a write made just after the call began to a stamp just\n * before it, and the cost of being generous here is one second of the staleness\n * this check exists to remove.\n */\nconst MODIFIED_TOLERANCE_MS = 1000;\n\nexport class EditTracker implements IEditTracker {\n private readonly pending = new Map<string, PendingEdit>();\n /** Start time per in-flight bash call, so its end knows what it may claim. */\n private readonly bracketed = new Map<string, number>();\n /**\n * The tree as this tracker last saw it. A bash call compares against it and\n * then replaces it, so each call costs one walk rather than two, and a change\n * made between calls is still noticed on the next one.\n */\n private manifest: TreeManifest | undefined;\n /**\n * This package's own storage, which the walk must never report. The timeline\n * and its snapshots can land inside the tree being watched, and recording\n * them would make every change cause another one.\n */\n private excluded: readonly string[] = [];\n /**\n * The last content hash this session saw per path, from either mechanism.\n * A candidate whose hash has not moved was touched, not edited.\n */\n private readonly contents = new Map<string, string>();\n private readonly now: () => number;\n private readonly git: GitStatusPort | undefined;\n\n constructor(\n private readonly timeline: ITimelineStore,\n private readonly snapshots: SnapshotStorePort,\n private readonly manifests: TreeManifestPort,\n options: EditTrackerOptions = {},\n ) {\n this.now = options.now ?? Date.now;\n this.git = options.git;\n }\n\n /**\n * Forgets the previous session's tree so a new one does not inherit its\n * baseline, and takes the paths this session's own bookkeeping occupies.\n */\n reset(options: { exclude?: readonly string[] } = {}): void {\n this.pending.clear();\n this.bracketed.clear();\n this.contents.clear();\n this.manifest = undefined;\n this.excluded = options.exclude ?? [];\n }\n\n async start(id: string, tool: string, args: unknown, cwd: string): Promise<void> {\n if (tool === EDIT_TOOL || tool === WRITE_TOOL) {\n const supplied = objectValue(args, 'path');\n if (!supplied) return;\n const filePath = path.resolve(cwd, supplied);\n this.pending.set(id, { tool, filePath, before: await this.snapshots.capture(filePath) });\n return;\n }\n if (tool !== BASH_TOOL) return;\n // Read before the baseline walk, which is the one thing here that can take\n // long enough to matter: a write that races it belongs to this call.\n this.bracketed.set(id, this.now());\n // The first bash call of a session has nothing to compare against, so it\n // pays for the baseline walk; every later call reuses the previous end.\n this.manifest ??= await this.manifests.take(cwd, this.excluded);\n }\n\n async end(id: string, isError: boolean, cwd: string): Promise<void> {\n const pending = this.pending.get(id);\n this.pending.delete(id);\n const startedAt = this.bracketed.get(id);\n this.bracketed.delete(id);\n if (isError) return;\n if (pending) {\n await this.recordTool(pending);\n return;\n }\n if (startedAt !== undefined) await this.recordScan(cwd, startedAt);\n }\n\n /** An `edit` or `write` whose file was read on both sides of the call. */\n private async recordTool(pending: PendingEdit): Promise<void> {\n const after = await this.snapshots.capture(pending.filePath);\n const counts = await this.countChanges(pending.before, after);\n if (after !== undefined) this.contents.set(pending.filePath, after);\n await this.append({\n version: 2,\n path: pending.filePath,\n tool: pending.tool,\n at: this.now(),\n origin: 'tool',\n ...(pending.before === undefined ? {} : { before: pending.before }),\n ...(after === undefined ? {} : { after }),\n ...counts,\n });\n // The tool already accounted for this path, so the next bash comparison\n // must not report it a second time under its own name.\n await this.remember(pending.filePath);\n }\n\n /** Whatever a bash call actually edited, however the command wrote it. */\n private async recordScan(cwd: string, startedAt: number): Promise<void> {\n const before = this.manifest;\n if (before === undefined) return;\n const after = await this.manifests.take(cwd, this.excluded);\n this.manifest = after;\n const candidates = await this.writtenDuring(this.manifests.changed(before, after), startedAt);\n if (candidates.length === 0) return;\n const unchanged = await this.unchangedInGit(cwd, candidates);\n for (const filePath of candidates) {\n const captured = await this.snapshots.capture(filePath);\n const known = this.contents.get(filePath);\n if (captured !== undefined) this.contents.set(filePath, captured);\n // The bytes are the ones this session already recorded: the call moved the\n // modification time and nothing else.\n if (captured !== undefined && captured === known) continue;\n // Never captured here, but git still holds the file and says it matches.\n if (known === undefined && unchanged.has(filePath)) continue;\n await this.append({\n version: 2,\n path: filePath,\n tool: BASH_TOOL,\n at: this.now(),\n origin: 'scan',\n verified: true,\n ...(captured === undefined ? {} : { after: captured }),\n });\n }\n }\n\n /**\n * Narrows candidates to the ones that could have been written by the call\n * being closed.\n *\n * This is what keeps a working tree that was already dirty out of the list. A\n * file whose bytes were last written before the call began was differed by\n * something else, whatever moved its fingerprint. A file that is no longer\n * there has no time to read and a delete is a change the call may well have\n * made, so it stays a candidate.\n */\n private async writtenDuring(candidates: readonly string[], startedAt: number): Promise<string[]> {\n const floor = startedAt - MODIFIED_TOLERANCE_MS;\n const written: string[] = [];\n for (const filePath of candidates) {\n const modifiedAt = await this.manifests.modifiedAt(filePath);\n if (modifiedAt === undefined || modifiedAt >= floor) written.push(filePath);\n }\n return written;\n }\n\n /** Which candidates git tracks and reports as untouched; empty when it cannot say. */\n private async unchangedInGit(cwd: string, candidates: readonly string[]): Promise<ReadonlySet<string>> {\n const unseen = candidates.filter((filePath) => !this.contents.has(filePath));\n if (this.git === undefined || unseen.length === 0) return new Set();\n return this.git.unchanged(cwd, unseen);\n }\n\n /** How many lines moved, when both sides were captured. */\n private async countChanges(\n before: string | undefined,\n after: string | undefined,\n ): Promise<{ additions?: number; removals?: number }> {\n if (before === undefined || after === undefined) return {};\n if (before === after) return { additions: 0, removals: 0 };\n const [beforeText, afterText] = await Promise.all([this.snapshots.read(before), this.snapshots.read(after)]);\n if (beforeText === undefined || afterText === undefined) return {};\n const diff = lineDiff(beforeText, afterText);\n return { additions: diff.additions, removals: diff.removals };\n }\n\n /** Records the change unless it turned out to be no change at all. */\n private async append(event: TimelineEvent): Promise<void> {\n if (event.before !== undefined && event.before === event.after) return;\n await this.timeline.append(event);\n }\n\n /**\n * Folds a path the tool layer just handled into the tracked manifest, so the\n * next bash comparison sees it as already accounted for.\n *\n * The fingerprint has to be the one a walk would read, not a stand-in, or the\n * next comparison reports the file again under bash and every tool edit is\n * recorded twice.\n */\n private async remember(filePath: string): Promise<void> {\n if (this.manifest === undefined) return;\n const entries = new Map(this.manifest.entries);\n const fingerprint = await this.manifests.fingerprint(filePath);\n if (fingerprint === undefined) entries.delete(filePath);\n else entries.set(filePath, fingerprint);\n this.manifest = { entries, truncated: this.manifest.truncated };\n }\n}\n\n/** The tools this tracker knows how to attribute a change to. */\nexport const TRACKED_TOOLS: readonly FileEditTool[] = [EDIT_TOOL, WRITE_TOOL, BASH_TOOL];\n"],"mappings":"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"}
1
+ {"version":3,"file":"EditTracker.cjs","names":["path","lineDiffFromEmpty","lineDiff"],"sources":["../../../src/adapters/EditTracker/EditTracker.ts"],"sourcesContent":["import path from 'node:path';\nimport { lineDiff, lineDiffFromEmpty } from '../../services/lineDiff.ts';\nimport type { FileEditTool, TimelineEvent } from '../../types/domain';\nimport type { IEditTracker } from '../../types/editTracker';\nimport type { GitStatusPort } from '../../types/gitStatus.ts';\nimport type { SnapshotStorePort } from '../../types/snapshotStore.ts';\nimport type { ITimelineStore } from '../../types/timelineStore';\nimport type { TreeManifest, TreeManifestPort } from '../../types/treeManifest.ts';\n\nconst EDIT_TOOL = 'edit';\nconst WRITE_TOOL = 'write';\nconst BASH_TOOL = 'bash';\n\n/**\n * Records what the session changed, and captures enough of it to be diffed.\n *\n * Two mechanisms, because one is not enough. `edit` and `write` name their file\n * in the call arguments, so the content is read before the tool runs and both\n * sides of the change are known exactly. `bash` names nothing reliable: the\n * agent can write a script and run it, and reading path-shaped tokens out of\n * the command would miss every file that script touches. So a bash call is\n * bracketed by tree manifests instead, and whatever moved between them is\n * recorded, however it was written.\n *\n * A manifest-found path is recorded without a baseline, because it was only\n * identified after it had already changed. That is a real limit and the wire\n * carries it as `origin: 'scan'` rather than pretending a diff exists.\n *\n * A manifest compares size and modification time, and it compares the tree as\n * this tracker last walked it against the tree now, so it answers whether a\n * file differs, never when it moved. A path a previous session left dirty, or\n * one a checkout or an install rewrote, differs exactly like a path the command\n * just wrote. So a candidate is called verified only when something actually\n * proves its bytes moved: it appeared, it vanished, its recorded size differs,\n * or its content hash differs from one this session already took. Absence of\n * proof is not proof, so a candidate that shows none of those is still\n * recorded, because the timeline is the evidence, but it goes out unverified\n * and every surface leaves it out.\n */\ninterface PendingEdit {\n tool: 'edit' | 'write';\n filePath: string;\n before: string | undefined;\n /**\n * Whether the file was there when the call began. A missing `before` cannot\n * say on its own: content is left uncaptured both for a file that does not\n * exist and for one too large or too binary to store.\n */\n existed: boolean;\n}\n\nfunction objectValue(value: unknown, key: string): string | undefined {\n if (!value || typeof value !== 'object') return undefined;\n const found = (value as Record<string, unknown>)[key];\n return typeof found === 'string' ? found : undefined;\n}\n\nexport interface EditTrackerOptions {\n /** Injectable so a test can pin the recorded timestamps. */\n now?: () => number;\n /**\n * Decides whether a first-seen scan candidate actually changed. Optional:\n * without it the tracker keeps recording every candidate it cannot disprove.\n */\n git?: GitStatusPort;\n}\n\n/**\n * How far before a call started a write may claim to have happened and still be\n * attributed to it. A working tree can sit on a filesystem that stores whole\n * seconds, which floors a write made just after the call began to a stamp just\n * before it, and the cost of being generous here is one second of the staleness\n * this check exists to remove.\n */\nconst MODIFIED_TOLERANCE_MS = 1000;\n\nexport class EditTracker implements IEditTracker {\n private readonly pending = new Map<string, PendingEdit>();\n /** Start time per in-flight bash call, so its end knows what it may claim. */\n private readonly bracketed = new Map<string, number>();\n /**\n * The tree as this tracker last saw it. A bash call compares against it and\n * then replaces it, so each call costs one walk rather than two, and a change\n * made between calls is still noticed on the next one.\n */\n private manifest: TreeManifest | undefined;\n /**\n * This package's own storage, which the walk must never report. The timeline\n * and its snapshots can land inside the tree being watched, and recording\n * them would make every change cause another one.\n */\n private excluded: readonly string[] = [];\n /**\n * The project's own ignore rules. A path the project disowns is not worth a\n * git call, a read, or a stored copy of its content, so it is dropped before\n * it costs any of them.\n */\n private isIgnored: ((filePath: string) => boolean) | undefined;\n /**\n * The last content hash this session saw per path, from either mechanism.\n * A candidate whose hash has not moved was touched, not edited.\n */\n private readonly contents = new Map<string, string>();\n private readonly now: () => number;\n private readonly git: GitStatusPort | undefined;\n\n constructor(\n private readonly timeline: ITimelineStore,\n private readonly snapshots: SnapshotStorePort,\n private readonly manifests: TreeManifestPort,\n options: EditTrackerOptions = {},\n ) {\n this.now = options.now ?? Date.now;\n this.git = options.git;\n }\n\n /**\n * Forgets the previous session's tree so a new one does not inherit its\n * baseline, and takes the paths this session's own bookkeeping occupies.\n */\n reset(options: { exclude?: readonly string[]; isIgnored?: (filePath: string) => boolean } = {}): void {\n this.pending.clear();\n this.bracketed.clear();\n this.contents.clear();\n this.manifest = undefined;\n this.excluded = options.exclude ?? [];\n this.isIgnored = options.isIgnored;\n }\n\n async start(id: string, tool: string, args: unknown, cwd: string): Promise<void> {\n if (tool === EDIT_TOOL || tool === WRITE_TOOL) {\n const supplied = objectValue(args, 'path');\n if (!supplied) return;\n const filePath = path.resolve(cwd, supplied);\n // Both are read before the tool runs. The fingerprint is the only thing\n // that tells a file being created from one whose content could not be\n // captured, because a capture answers undefined for either.\n const [before, fingerprint] = await Promise.all([\n this.snapshots.capture(filePath),\n this.manifests.fingerprint(filePath),\n ]);\n this.pending.set(id, { tool, filePath, before, existed: fingerprint !== undefined });\n return;\n }\n if (tool !== BASH_TOOL) return;\n // Read before the baseline walk, which is the one thing here that can take\n // long enough to matter: a write that races it belongs to this call.\n this.bracketed.set(id, this.now());\n // The first bash call of a session has nothing to compare against, so it\n // pays for the baseline walk; every later call reuses the previous end.\n this.manifest ??= await this.manifests.take(cwd, this.excluded);\n }\n\n async end(id: string, isError: boolean, cwd: string): Promise<void> {\n const pending = this.pending.get(id);\n this.pending.delete(id);\n const startedAt = this.bracketed.get(id);\n this.bracketed.delete(id);\n if (pending) {\n // A failed edit or write never landed, and its arguments named the file,\n // so there is nothing left to look for.\n if (!isError) await this.recordTool(pending);\n return;\n }\n // A failed command may still have written before it failed. Skipping the\n // walk would leave the baseline stale and hand those writes to whichever\n // call closes next.\n if (startedAt !== undefined) await this.recordScan(cwd, startedAt);\n }\n\n /** An `edit` or `write` whose file was read on both sides of the call. */\n private async recordTool(pending: PendingEdit): Promise<void> {\n const after = await this.snapshots.capture(pending.filePath);\n const created = !pending.existed;\n const counts = await this.countChanges(pending.before, after, created);\n if (after !== undefined) this.contents.set(pending.filePath, after);\n await this.append({\n version: 2,\n path: pending.filePath,\n tool: pending.tool,\n at: this.now(),\n origin: 'tool',\n ...(pending.before === undefined ? {} : { before: pending.before }),\n ...(after === undefined ? {} : { after }),\n ...(created ? { created: true } : {}),\n ...counts,\n });\n // The tool already accounted for this path, so the next bash comparison\n // must not report it a second time under its own name.\n await this.remember(pending.filePath);\n }\n\n /** Whatever a bash call actually edited, however the command wrote it. */\n private async recordScan(cwd: string, startedAt: number): Promise<void> {\n const before = this.manifest;\n if (before === undefined) return;\n const after = await this.manifests.take(cwd, this.excluded);\n this.manifest = after;\n const disowned = this.isIgnored;\n const named = this.manifests\n .changed(before, after)\n .filter((filePath) => disowned === undefined || !disowned(filePath));\n const candidates = await this.writtenDuring(named, startedAt);\n if (candidates.length === 0) return;\n const unchanged = await this.unchangedInGit(cwd, candidates);\n for (const filePath of candidates) {\n const known = this.contents.get(filePath);\n // Asked before the file is read: a path git vouches for is worth neither\n // the read nor the stored copy that reading it leaves behind.\n if (known === undefined && unchanged.has(filePath)) continue;\n const beforePrint = before.entries.get(filePath);\n const afterPrint = after.entries.get(filePath);\n // A path that is gone has nothing to read, and reading it back would only\n // confirm that.\n const captured = afterPrint === undefined ? undefined : await this.snapshots.capture(filePath);\n if (captured !== undefined) this.contents.set(filePath, captured);\n // The bytes are the ones this session already recorded: the call moved the\n // modification time and nothing else.\n if (captured !== undefined && captured === known) continue;\n const proven =\n beforePrint === undefined ||\n afterPrint === undefined ||\n this.manifests.sizeChanged(beforePrint, afterPrint) ||\n (known !== undefined && captured !== undefined && captured !== known);\n await this.append({\n version: 2,\n path: filePath,\n tool: BASH_TOOL,\n at: this.now(),\n origin: 'scan',\n ...(proven ? { verified: true } : {}),\n ...(captured === undefined ? {} : { after: captured }),\n });\n }\n }\n\n /**\n * Narrows candidates to the ones that could have been written by the call\n * being closed.\n *\n * This is what keeps a working tree that was already dirty out of the list. A\n * file whose bytes were last written before the call began was differed by\n * something else, whatever moved its fingerprint. A file that is no longer\n * there has no time to read and a delete is a change the call may well have\n * made, so it stays a candidate.\n */\n private async writtenDuring(candidates: readonly string[], startedAt: number): Promise<string[]> {\n const floor = startedAt - MODIFIED_TOLERANCE_MS;\n const written: string[] = [];\n for (const filePath of candidates) {\n const modifiedAt = await this.manifests.modifiedAt(filePath);\n if (modifiedAt === undefined || modifiedAt >= floor) written.push(filePath);\n }\n return written;\n }\n\n /** Which candidates git tracks and reports as untouched; empty when it cannot say. */\n private async unchangedInGit(cwd: string, candidates: readonly string[]): Promise<ReadonlySet<string>> {\n const unseen = candidates.filter((filePath) => !this.contents.has(filePath));\n if (this.git === undefined || unseen.length === 0) return new Set();\n return this.git.unchanged(cwd, unseen);\n }\n\n /** How many lines moved, when there is enough captured content to say. */\n private async countChanges(\n before: string | undefined,\n after: string | undefined,\n created: boolean,\n ): Promise<{ additions?: number; removals?: number }> {\n if (after === undefined) return {};\n if (before === undefined) {\n // A file that did not exist has an empty baseline, so every line it now\n // holds is an addition. A file that existed but went uncaptured has no\n // baseline to count against at all.\n if (!created) return {};\n const createdText = await this.snapshots.read(after);\n if (createdText === undefined) return {};\n const fresh = lineDiffFromEmpty(createdText);\n return { additions: fresh.additions, removals: fresh.removals };\n }\n if (before === after) return { additions: 0, removals: 0 };\n const [beforeText, afterText] = await Promise.all([this.snapshots.read(before), this.snapshots.read(after)]);\n if (beforeText === undefined || afterText === undefined) return {};\n const diff = lineDiff(beforeText, afterText);\n return { additions: diff.additions, removals: diff.removals };\n }\n\n /** Records the change unless it turned out to be no change at all. */\n private async append(event: TimelineEvent): Promise<void> {\n if (event.before !== undefined && event.before === event.after) return;\n await this.timeline.append(event);\n }\n\n /**\n * Folds a path the tool layer just handled into the tracked manifest, so the\n * next bash comparison sees it as already accounted for.\n *\n * The fingerprint has to be the one a walk would read, not a stand-in, or the\n * next comparison reports the file again under bash and every tool edit is\n * recorded twice.\n */\n private async remember(filePath: string): Promise<void> {\n if (this.manifest === undefined) return;\n const entries = new Map(this.manifest.entries);\n const fingerprint = await this.manifests.fingerprint(filePath);\n if (fingerprint === undefined) entries.delete(filePath);\n else entries.set(filePath, fingerprint);\n this.manifest = { entries, truncated: this.manifest.truncated };\n }\n}\n\n/** The tools this tracker knows how to attribute a change to. */\nexport const TRACKED_TOOLS: readonly FileEditTool[] = [EDIT_TOOL, WRITE_TOOL, BASH_TOOL];\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- import{lineDiff as e}from"../../services/lineDiff.mjs";import t from"node:path";const n=`bash`;function r(e,t){if(!e||typeof e!=`object`)return;let n=e[t];return typeof n==`string`?n:void 0}var i=class{timeline;snapshots;manifests;pending=new Map;bracketed=new Map;manifest;excluded=[];contents=new Map;now;git;constructor(e,t,n,r={}){this.timeline=e,this.snapshots=t,this.manifests=n,this.now=r.now??Date.now,this.git=r.git}reset(e={}){this.pending.clear(),this.bracketed.clear(),this.contents.clear(),this.manifest=void 0,this.excluded=e.exclude??[]}async start(e,i,a,o){if(i===`edit`||i===`write`){let n=r(a,`path`);if(!n)return;let s=t.resolve(o,n);this.pending.set(e,{tool:i,filePath:s,before:await this.snapshots.capture(s)});return}i===n&&(this.bracketed.set(e,this.now()),this.manifest??=await this.manifests.take(o,this.excluded))}async end(e,t,n){let r=this.pending.get(e);this.pending.delete(e);let i=this.bracketed.get(e);if(this.bracketed.delete(e),!t){if(r){await this.recordTool(r);return}i!==void 0&&await this.recordScan(n,i)}}async recordTool(e){let t=await this.snapshots.capture(e.filePath),n=await this.countChanges(e.before,t);t!==void 0&&this.contents.set(e.filePath,t),await this.append({version:2,path:e.filePath,tool:e.tool,at:this.now(),origin:`tool`,...e.before===void 0?{}:{before:e.before},...t===void 0?{}:{after:t},...n}),await this.remember(e.filePath)}async recordScan(e,t){let r=this.manifest;if(r===void 0)return;let i=await this.manifests.take(e,this.excluded);this.manifest=i;let a=await this.writtenDuring(this.manifests.changed(r,i),t);if(a.length===0)return;let o=await this.unchangedInGit(e,a);for(let e of a){let t=await this.snapshots.capture(e),r=this.contents.get(e);t!==void 0&&this.contents.set(e,t),(t===void 0||t!==r)&&(r===void 0&&o.has(e)||await this.append({version:2,path:e,tool:n,at:this.now(),origin:`scan`,verified:!0,...t===void 0?{}:{after:t}}))}}async writtenDuring(e,t){let n=t-1e3,r=[];for(let t of e){let e=await this.manifests.modifiedAt(t);(e===void 0||e>=n)&&r.push(t)}return r}async unchangedInGit(e,t){let n=t.filter(e=>!this.contents.has(e));return this.git===void 0||n.length===0?new Set:this.git.unchanged(e,n)}async countChanges(t,n){if(t===void 0||n===void 0)return{};if(t===n)return{additions:0,removals:0};let[r,i]=await Promise.all([this.snapshots.read(t),this.snapshots.read(n)]);if(r===void 0||i===void 0)return{};let a=e(r,i);return{additions:a.additions,removals:a.removals}}async append(e){(e.before===void 0||e.before!==e.after)&&await this.timeline.append(e)}async remember(e){if(this.manifest===void 0)return;let t=new Map(this.manifest.entries),n=await this.manifests.fingerprint(e);n===void 0?t.delete(e):t.set(e,n),this.manifest={entries:t,truncated:this.manifest.truncated}}};export{i as EditTracker};
1
+ import{lineDiff as e,lineDiffFromEmpty as t}from"../../services/lineDiff.mjs";import n from"node:path";const r=`bash`;function i(e,t){if(!e||typeof e!=`object`)return;let n=e[t];return typeof n==`string`?n:void 0}var a=class{timeline;snapshots;manifests;pending=new Map;bracketed=new Map;manifest;excluded=[];isIgnored;contents=new Map;now;git;constructor(e,t,n,r={}){this.timeline=e,this.snapshots=t,this.manifests=n,this.now=r.now??Date.now,this.git=r.git}reset(e={}){this.pending.clear(),this.bracketed.clear(),this.contents.clear(),this.manifest=void 0,this.excluded=e.exclude??[],this.isIgnored=e.isIgnored}async start(e,t,a,o){if(t===`edit`||t===`write`){let r=i(a,`path`);if(!r)return;let s=n.resolve(o,r),[c,l]=await Promise.all([this.snapshots.capture(s),this.manifests.fingerprint(s)]);this.pending.set(e,{tool:t,filePath:s,before:c,existed:l!==void 0});return}t===r&&(this.bracketed.set(e,this.now()),this.manifest??=await this.manifests.take(o,this.excluded))}async end(e,t,n){let r=this.pending.get(e);this.pending.delete(e);let i=this.bracketed.get(e);if(this.bracketed.delete(e),r){t||await this.recordTool(r);return}i!==void 0&&await this.recordScan(n,i)}async recordTool(e){let t=await this.snapshots.capture(e.filePath),n=!e.existed,r=await this.countChanges(e.before,t,n);t!==void 0&&this.contents.set(e.filePath,t),await this.append({version:2,path:e.filePath,tool:e.tool,at:this.now(),origin:`tool`,...e.before===void 0?{}:{before:e.before},...t===void 0?{}:{after:t},...n?{created:!0}:{},...r}),await this.remember(e.filePath)}async recordScan(e,t){let n=this.manifest;if(n===void 0)return;let i=await this.manifests.take(e,this.excluded);this.manifest=i;let a=this.isIgnored,o=this.manifests.changed(n,i).filter(e=>a===void 0||!a(e)),s=await this.writtenDuring(o,t);if(s.length===0)return;let c=await this.unchangedInGit(e,s);for(let e of s){let t=this.contents.get(e);if(t===void 0&&c.has(e))continue;let a=n.entries.get(e),o=i.entries.get(e),s=o===void 0?void 0:await this.snapshots.capture(e);if(s!==void 0&&this.contents.set(e,s),s!==void 0&&s===t)continue;let l=a===void 0||o===void 0||this.manifests.sizeChanged(a,o)||t!==void 0&&s!==void 0&&s!==t;await this.append({version:2,path:e,tool:r,at:this.now(),origin:`scan`,...l?{verified:!0}:{},...s===void 0?{}:{after:s}})}}async writtenDuring(e,t){let n=t-1e3,r=[];for(let t of e){let e=await this.manifests.modifiedAt(t);(e===void 0||e>=n)&&r.push(t)}return r}async unchangedInGit(e,t){let n=t.filter(e=>!this.contents.has(e));return this.git===void 0||n.length===0?new Set:this.git.unchanged(e,n)}async countChanges(n,r,i){if(r===void 0)return{};if(n===void 0){if(!i)return{};let e=await this.snapshots.read(r);if(e===void 0)return{};let n=t(e);return{additions:n.additions,removals:n.removals}}if(n===r)return{additions:0,removals:0};let[a,o]=await Promise.all([this.snapshots.read(n),this.snapshots.read(r)]);if(a===void 0||o===void 0)return{};let s=e(a,o);return{additions:s.additions,removals:s.removals}}async append(e){(e.before===void 0||e.before!==e.after)&&await this.timeline.append(e)}async remember(e){if(this.manifest===void 0)return;let t=new Map(this.manifest.entries),n=await this.manifests.fingerprint(e);n===void 0?t.delete(e):t.set(e,n),this.manifest={entries:t,truncated:this.manifest.truncated}}};export{a as EditTracker};
2
2
  //# sourceMappingURL=EditTracker.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"EditTracker.mjs","names":[],"sources":["../../../src/adapters/EditTracker/EditTracker.ts"],"sourcesContent":["import path from 'node:path';\nimport { lineDiff } from '../../services/lineDiff.ts';\nimport type { FileEditTool, TimelineEvent } from '../../types/domain';\nimport type { IEditTracker } from '../../types/editTracker';\nimport type { GitStatusPort } from '../../types/gitStatus.ts';\nimport type { SnapshotStorePort } from '../../types/snapshotStore.ts';\nimport type { ITimelineStore } from '../../types/timelineStore';\nimport type { TreeManifest, TreeManifestPort } from '../../types/treeManifest.ts';\n\nconst EDIT_TOOL = 'edit';\nconst WRITE_TOOL = 'write';\nconst BASH_TOOL = 'bash';\n\n/**\n * Records what the session changed, and captures enough of it to be diffed.\n *\n * Two mechanisms, because one is not enough. `edit` and `write` name their file\n * in the call arguments, so the content is read before the tool runs and both\n * sides of the change are known exactly. `bash` names nothing reliable: the\n * agent can write a script and run it, and reading path-shaped tokens out of\n * the command would miss every file that script touches. So a bash call is\n * bracketed by tree manifests instead, and whatever moved between them is\n * recorded, however it was written.\n *\n * A manifest-found path is recorded without a baseline, because it was only\n * identified after it had already changed. That is a real limit and the wire\n * carries it as `origin: 'scan'` rather than pretending a diff exists.\n *\n * A manifest compares size and modification time, and it compares the tree as\n * this tracker last walked it against the tree now, so it answers whether a\n * file differs, never when it moved. A path a previous session left dirty, or\n * one a checkout or an install rewrote, differs exactly like a path the command\n * just wrote. So a candidate is confirmed three ways before it is recorded: its\n * modification time has to fall inside the call that is being closed, its\n * content hash has to differ from the last one this session saw, and a path\n * this session has never captured is put to git. Not every answer is available\n * for every file, an ignored temporary file is unknown to git and a first\n * sighting has no earlier hash, so an unconfirmable candidate is still\n * recorded; only a proven non-change is dropped.\n */\ninterface PendingEdit {\n tool: 'edit' | 'write';\n filePath: string;\n before: string | undefined;\n}\n\nfunction objectValue(value: unknown, key: string): string | undefined {\n if (!value || typeof value !== 'object') return undefined;\n const found = (value as Record<string, unknown>)[key];\n return typeof found === 'string' ? found : undefined;\n}\n\nexport interface EditTrackerOptions {\n /** Injectable so a test can pin the recorded timestamps. */\n now?: () => number;\n /**\n * Decides whether a first-seen scan candidate actually changed. Optional:\n * without it the tracker keeps recording every candidate it cannot disprove.\n */\n git?: GitStatusPort;\n}\n\n/**\n * How far before a call started a write may claim to have happened and still be\n * attributed to it. A working tree can sit on a filesystem that stores whole\n * seconds, which floors a write made just after the call began to a stamp just\n * before it, and the cost of being generous here is one second of the staleness\n * this check exists to remove.\n */\nconst MODIFIED_TOLERANCE_MS = 1000;\n\nexport class EditTracker implements IEditTracker {\n private readonly pending = new Map<string, PendingEdit>();\n /** Start time per in-flight bash call, so its end knows what it may claim. */\n private readonly bracketed = new Map<string, number>();\n /**\n * The tree as this tracker last saw it. A bash call compares against it and\n * then replaces it, so each call costs one walk rather than two, and a change\n * made between calls is still noticed on the next one.\n */\n private manifest: TreeManifest | undefined;\n /**\n * This package's own storage, which the walk must never report. The timeline\n * and its snapshots can land inside the tree being watched, and recording\n * them would make every change cause another one.\n */\n private excluded: readonly string[] = [];\n /**\n * The last content hash this session saw per path, from either mechanism.\n * A candidate whose hash has not moved was touched, not edited.\n */\n private readonly contents = new Map<string, string>();\n private readonly now: () => number;\n private readonly git: GitStatusPort | undefined;\n\n constructor(\n private readonly timeline: ITimelineStore,\n private readonly snapshots: SnapshotStorePort,\n private readonly manifests: TreeManifestPort,\n options: EditTrackerOptions = {},\n ) {\n this.now = options.now ?? Date.now;\n this.git = options.git;\n }\n\n /**\n * Forgets the previous session's tree so a new one does not inherit its\n * baseline, and takes the paths this session's own bookkeeping occupies.\n */\n reset(options: { exclude?: readonly string[] } = {}): void {\n this.pending.clear();\n this.bracketed.clear();\n this.contents.clear();\n this.manifest = undefined;\n this.excluded = options.exclude ?? [];\n }\n\n async start(id: string, tool: string, args: unknown, cwd: string): Promise<void> {\n if (tool === EDIT_TOOL || tool === WRITE_TOOL) {\n const supplied = objectValue(args, 'path');\n if (!supplied) return;\n const filePath = path.resolve(cwd, supplied);\n this.pending.set(id, { tool, filePath, before: await this.snapshots.capture(filePath) });\n return;\n }\n if (tool !== BASH_TOOL) return;\n // Read before the baseline walk, which is the one thing here that can take\n // long enough to matter: a write that races it belongs to this call.\n this.bracketed.set(id, this.now());\n // The first bash call of a session has nothing to compare against, so it\n // pays for the baseline walk; every later call reuses the previous end.\n this.manifest ??= await this.manifests.take(cwd, this.excluded);\n }\n\n async end(id: string, isError: boolean, cwd: string): Promise<void> {\n const pending = this.pending.get(id);\n this.pending.delete(id);\n const startedAt = this.bracketed.get(id);\n this.bracketed.delete(id);\n if (isError) return;\n if (pending) {\n await this.recordTool(pending);\n return;\n }\n if (startedAt !== undefined) await this.recordScan(cwd, startedAt);\n }\n\n /** An `edit` or `write` whose file was read on both sides of the call. */\n private async recordTool(pending: PendingEdit): Promise<void> {\n const after = await this.snapshots.capture(pending.filePath);\n const counts = await this.countChanges(pending.before, after);\n if (after !== undefined) this.contents.set(pending.filePath, after);\n await this.append({\n version: 2,\n path: pending.filePath,\n tool: pending.tool,\n at: this.now(),\n origin: 'tool',\n ...(pending.before === undefined ? {} : { before: pending.before }),\n ...(after === undefined ? {} : { after }),\n ...counts,\n });\n // The tool already accounted for this path, so the next bash comparison\n // must not report it a second time under its own name.\n await this.remember(pending.filePath);\n }\n\n /** Whatever a bash call actually edited, however the command wrote it. */\n private async recordScan(cwd: string, startedAt: number): Promise<void> {\n const before = this.manifest;\n if (before === undefined) return;\n const after = await this.manifests.take(cwd, this.excluded);\n this.manifest = after;\n const candidates = await this.writtenDuring(this.manifests.changed(before, after), startedAt);\n if (candidates.length === 0) return;\n const unchanged = await this.unchangedInGit(cwd, candidates);\n for (const filePath of candidates) {\n const captured = await this.snapshots.capture(filePath);\n const known = this.contents.get(filePath);\n if (captured !== undefined) this.contents.set(filePath, captured);\n // The bytes are the ones this session already recorded: the call moved the\n // modification time and nothing else.\n if (captured !== undefined && captured === known) continue;\n // Never captured here, but git still holds the file and says it matches.\n if (known === undefined && unchanged.has(filePath)) continue;\n await this.append({\n version: 2,\n path: filePath,\n tool: BASH_TOOL,\n at: this.now(),\n origin: 'scan',\n verified: true,\n ...(captured === undefined ? {} : { after: captured }),\n });\n }\n }\n\n /**\n * Narrows candidates to the ones that could have been written by the call\n * being closed.\n *\n * This is what keeps a working tree that was already dirty out of the list. A\n * file whose bytes were last written before the call began was differed by\n * something else, whatever moved its fingerprint. A file that is no longer\n * there has no time to read and a delete is a change the call may well have\n * made, so it stays a candidate.\n */\n private async writtenDuring(candidates: readonly string[], startedAt: number): Promise<string[]> {\n const floor = startedAt - MODIFIED_TOLERANCE_MS;\n const written: string[] = [];\n for (const filePath of candidates) {\n const modifiedAt = await this.manifests.modifiedAt(filePath);\n if (modifiedAt === undefined || modifiedAt >= floor) written.push(filePath);\n }\n return written;\n }\n\n /** Which candidates git tracks and reports as untouched; empty when it cannot say. */\n private async unchangedInGit(cwd: string, candidates: readonly string[]): Promise<ReadonlySet<string>> {\n const unseen = candidates.filter((filePath) => !this.contents.has(filePath));\n if (this.git === undefined || unseen.length === 0) return new Set();\n return this.git.unchanged(cwd, unseen);\n }\n\n /** How many lines moved, when both sides were captured. */\n private async countChanges(\n before: string | undefined,\n after: string | undefined,\n ): Promise<{ additions?: number; removals?: number }> {\n if (before === undefined || after === undefined) return {};\n if (before === after) return { additions: 0, removals: 0 };\n const [beforeText, afterText] = await Promise.all([this.snapshots.read(before), this.snapshots.read(after)]);\n if (beforeText === undefined || afterText === undefined) return {};\n const diff = lineDiff(beforeText, afterText);\n return { additions: diff.additions, removals: diff.removals };\n }\n\n /** Records the change unless it turned out to be no change at all. */\n private async append(event: TimelineEvent): Promise<void> {\n if (event.before !== undefined && event.before === event.after) return;\n await this.timeline.append(event);\n }\n\n /**\n * Folds a path the tool layer just handled into the tracked manifest, so the\n * next bash comparison sees it as already accounted for.\n *\n * The fingerprint has to be the one a walk would read, not a stand-in, or the\n * next comparison reports the file again under bash and every tool edit is\n * recorded twice.\n */\n private async remember(filePath: string): Promise<void> {\n if (this.manifest === undefined) return;\n const entries = new Map(this.manifest.entries);\n const fingerprint = await this.manifests.fingerprint(filePath);\n if (fingerprint === undefined) entries.delete(filePath);\n else entries.set(filePath, fingerprint);\n this.manifest = { entries, truncated: this.manifest.truncated };\n }\n}\n\n/** The tools this tracker knows how to attribute a change to. */\nexport const TRACKED_TOOLS: readonly FileEditTool[] = [EDIT_TOOL, WRITE_TOOL, BASH_TOOL];\n"],"mappings":"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"}
1
+ {"version":3,"file":"EditTracker.mjs","names":[],"sources":["../../../src/adapters/EditTracker/EditTracker.ts"],"sourcesContent":["import path from 'node:path';\nimport { lineDiff, lineDiffFromEmpty } from '../../services/lineDiff.ts';\nimport type { FileEditTool, TimelineEvent } from '../../types/domain';\nimport type { IEditTracker } from '../../types/editTracker';\nimport type { GitStatusPort } from '../../types/gitStatus.ts';\nimport type { SnapshotStorePort } from '../../types/snapshotStore.ts';\nimport type { ITimelineStore } from '../../types/timelineStore';\nimport type { TreeManifest, TreeManifestPort } from '../../types/treeManifest.ts';\n\nconst EDIT_TOOL = 'edit';\nconst WRITE_TOOL = 'write';\nconst BASH_TOOL = 'bash';\n\n/**\n * Records what the session changed, and captures enough of it to be diffed.\n *\n * Two mechanisms, because one is not enough. `edit` and `write` name their file\n * in the call arguments, so the content is read before the tool runs and both\n * sides of the change are known exactly. `bash` names nothing reliable: the\n * agent can write a script and run it, and reading path-shaped tokens out of\n * the command would miss every file that script touches. So a bash call is\n * bracketed by tree manifests instead, and whatever moved between them is\n * recorded, however it was written.\n *\n * A manifest-found path is recorded without a baseline, because it was only\n * identified after it had already changed. That is a real limit and the wire\n * carries it as `origin: 'scan'` rather than pretending a diff exists.\n *\n * A manifest compares size and modification time, and it compares the tree as\n * this tracker last walked it against the tree now, so it answers whether a\n * file differs, never when it moved. A path a previous session left dirty, or\n * one a checkout or an install rewrote, differs exactly like a path the command\n * just wrote. So a candidate is called verified only when something actually\n * proves its bytes moved: it appeared, it vanished, its recorded size differs,\n * or its content hash differs from one this session already took. Absence of\n * proof is not proof, so a candidate that shows none of those is still\n * recorded, because the timeline is the evidence, but it goes out unverified\n * and every surface leaves it out.\n */\ninterface PendingEdit {\n tool: 'edit' | 'write';\n filePath: string;\n before: string | undefined;\n /**\n * Whether the file was there when the call began. A missing `before` cannot\n * say on its own: content is left uncaptured both for a file that does not\n * exist and for one too large or too binary to store.\n */\n existed: boolean;\n}\n\nfunction objectValue(value: unknown, key: string): string | undefined {\n if (!value || typeof value !== 'object') return undefined;\n const found = (value as Record<string, unknown>)[key];\n return typeof found === 'string' ? found : undefined;\n}\n\nexport interface EditTrackerOptions {\n /** Injectable so a test can pin the recorded timestamps. */\n now?: () => number;\n /**\n * Decides whether a first-seen scan candidate actually changed. Optional:\n * without it the tracker keeps recording every candidate it cannot disprove.\n */\n git?: GitStatusPort;\n}\n\n/**\n * How far before a call started a write may claim to have happened and still be\n * attributed to it. A working tree can sit on a filesystem that stores whole\n * seconds, which floors a write made just after the call began to a stamp just\n * before it, and the cost of being generous here is one second of the staleness\n * this check exists to remove.\n */\nconst MODIFIED_TOLERANCE_MS = 1000;\n\nexport class EditTracker implements IEditTracker {\n private readonly pending = new Map<string, PendingEdit>();\n /** Start time per in-flight bash call, so its end knows what it may claim. */\n private readonly bracketed = new Map<string, number>();\n /**\n * The tree as this tracker last saw it. A bash call compares against it and\n * then replaces it, so each call costs one walk rather than two, and a change\n * made between calls is still noticed on the next one.\n */\n private manifest: TreeManifest | undefined;\n /**\n * This package's own storage, which the walk must never report. The timeline\n * and its snapshots can land inside the tree being watched, and recording\n * them would make every change cause another one.\n */\n private excluded: readonly string[] = [];\n /**\n * The project's own ignore rules. A path the project disowns is not worth a\n * git call, a read, or a stored copy of its content, so it is dropped before\n * it costs any of them.\n */\n private isIgnored: ((filePath: string) => boolean) | undefined;\n /**\n * The last content hash this session saw per path, from either mechanism.\n * A candidate whose hash has not moved was touched, not edited.\n */\n private readonly contents = new Map<string, string>();\n private readonly now: () => number;\n private readonly git: GitStatusPort | undefined;\n\n constructor(\n private readonly timeline: ITimelineStore,\n private readonly snapshots: SnapshotStorePort,\n private readonly manifests: TreeManifestPort,\n options: EditTrackerOptions = {},\n ) {\n this.now = options.now ?? Date.now;\n this.git = options.git;\n }\n\n /**\n * Forgets the previous session's tree so a new one does not inherit its\n * baseline, and takes the paths this session's own bookkeeping occupies.\n */\n reset(options: { exclude?: readonly string[]; isIgnored?: (filePath: string) => boolean } = {}): void {\n this.pending.clear();\n this.bracketed.clear();\n this.contents.clear();\n this.manifest = undefined;\n this.excluded = options.exclude ?? [];\n this.isIgnored = options.isIgnored;\n }\n\n async start(id: string, tool: string, args: unknown, cwd: string): Promise<void> {\n if (tool === EDIT_TOOL || tool === WRITE_TOOL) {\n const supplied = objectValue(args, 'path');\n if (!supplied) return;\n const filePath = path.resolve(cwd, supplied);\n // Both are read before the tool runs. The fingerprint is the only thing\n // that tells a file being created from one whose content could not be\n // captured, because a capture answers undefined for either.\n const [before, fingerprint] = await Promise.all([\n this.snapshots.capture(filePath),\n this.manifests.fingerprint(filePath),\n ]);\n this.pending.set(id, { tool, filePath, before, existed: fingerprint !== undefined });\n return;\n }\n if (tool !== BASH_TOOL) return;\n // Read before the baseline walk, which is the one thing here that can take\n // long enough to matter: a write that races it belongs to this call.\n this.bracketed.set(id, this.now());\n // The first bash call of a session has nothing to compare against, so it\n // pays for the baseline walk; every later call reuses the previous end.\n this.manifest ??= await this.manifests.take(cwd, this.excluded);\n }\n\n async end(id: string, isError: boolean, cwd: string): Promise<void> {\n const pending = this.pending.get(id);\n this.pending.delete(id);\n const startedAt = this.bracketed.get(id);\n this.bracketed.delete(id);\n if (pending) {\n // A failed edit or write never landed, and its arguments named the file,\n // so there is nothing left to look for.\n if (!isError) await this.recordTool(pending);\n return;\n }\n // A failed command may still have written before it failed. Skipping the\n // walk would leave the baseline stale and hand those writes to whichever\n // call closes next.\n if (startedAt !== undefined) await this.recordScan(cwd, startedAt);\n }\n\n /** An `edit` or `write` whose file was read on both sides of the call. */\n private async recordTool(pending: PendingEdit): Promise<void> {\n const after = await this.snapshots.capture(pending.filePath);\n const created = !pending.existed;\n const counts = await this.countChanges(pending.before, after, created);\n if (after !== undefined) this.contents.set(pending.filePath, after);\n await this.append({\n version: 2,\n path: pending.filePath,\n tool: pending.tool,\n at: this.now(),\n origin: 'tool',\n ...(pending.before === undefined ? {} : { before: pending.before }),\n ...(after === undefined ? {} : { after }),\n ...(created ? { created: true } : {}),\n ...counts,\n });\n // The tool already accounted for this path, so the next bash comparison\n // must not report it a second time under its own name.\n await this.remember(pending.filePath);\n }\n\n /** Whatever a bash call actually edited, however the command wrote it. */\n private async recordScan(cwd: string, startedAt: number): Promise<void> {\n const before = this.manifest;\n if (before === undefined) return;\n const after = await this.manifests.take(cwd, this.excluded);\n this.manifest = after;\n const disowned = this.isIgnored;\n const named = this.manifests\n .changed(before, after)\n .filter((filePath) => disowned === undefined || !disowned(filePath));\n const candidates = await this.writtenDuring(named, startedAt);\n if (candidates.length === 0) return;\n const unchanged = await this.unchangedInGit(cwd, candidates);\n for (const filePath of candidates) {\n const known = this.contents.get(filePath);\n // Asked before the file is read: a path git vouches for is worth neither\n // the read nor the stored copy that reading it leaves behind.\n if (known === undefined && unchanged.has(filePath)) continue;\n const beforePrint = before.entries.get(filePath);\n const afterPrint = after.entries.get(filePath);\n // A path that is gone has nothing to read, and reading it back would only\n // confirm that.\n const captured = afterPrint === undefined ? undefined : await this.snapshots.capture(filePath);\n if (captured !== undefined) this.contents.set(filePath, captured);\n // The bytes are the ones this session already recorded: the call moved the\n // modification time and nothing else.\n if (captured !== undefined && captured === known) continue;\n const proven =\n beforePrint === undefined ||\n afterPrint === undefined ||\n this.manifests.sizeChanged(beforePrint, afterPrint) ||\n (known !== undefined && captured !== undefined && captured !== known);\n await this.append({\n version: 2,\n path: filePath,\n tool: BASH_TOOL,\n at: this.now(),\n origin: 'scan',\n ...(proven ? { verified: true } : {}),\n ...(captured === undefined ? {} : { after: captured }),\n });\n }\n }\n\n /**\n * Narrows candidates to the ones that could have been written by the call\n * being closed.\n *\n * This is what keeps a working tree that was already dirty out of the list. A\n * file whose bytes were last written before the call began was differed by\n * something else, whatever moved its fingerprint. A file that is no longer\n * there has no time to read and a delete is a change the call may well have\n * made, so it stays a candidate.\n */\n private async writtenDuring(candidates: readonly string[], startedAt: number): Promise<string[]> {\n const floor = startedAt - MODIFIED_TOLERANCE_MS;\n const written: string[] = [];\n for (const filePath of candidates) {\n const modifiedAt = await this.manifests.modifiedAt(filePath);\n if (modifiedAt === undefined || modifiedAt >= floor) written.push(filePath);\n }\n return written;\n }\n\n /** Which candidates git tracks and reports as untouched; empty when it cannot say. */\n private async unchangedInGit(cwd: string, candidates: readonly string[]): Promise<ReadonlySet<string>> {\n const unseen = candidates.filter((filePath) => !this.contents.has(filePath));\n if (this.git === undefined || unseen.length === 0) return new Set();\n return this.git.unchanged(cwd, unseen);\n }\n\n /** How many lines moved, when there is enough captured content to say. */\n private async countChanges(\n before: string | undefined,\n after: string | undefined,\n created: boolean,\n ): Promise<{ additions?: number; removals?: number }> {\n if (after === undefined) return {};\n if (before === undefined) {\n // A file that did not exist has an empty baseline, so every line it now\n // holds is an addition. A file that existed but went uncaptured has no\n // baseline to count against at all.\n if (!created) return {};\n const createdText = await this.snapshots.read(after);\n if (createdText === undefined) return {};\n const fresh = lineDiffFromEmpty(createdText);\n return { additions: fresh.additions, removals: fresh.removals };\n }\n if (before === after) return { additions: 0, removals: 0 };\n const [beforeText, afterText] = await Promise.all([this.snapshots.read(before), this.snapshots.read(after)]);\n if (beforeText === undefined || afterText === undefined) return {};\n const diff = lineDiff(beforeText, afterText);\n return { additions: diff.additions, removals: diff.removals };\n }\n\n /** Records the change unless it turned out to be no change at all. */\n private async append(event: TimelineEvent): Promise<void> {\n if (event.before !== undefined && event.before === event.after) return;\n await this.timeline.append(event);\n }\n\n /**\n * Folds a path the tool layer just handled into the tracked manifest, so the\n * next bash comparison sees it as already accounted for.\n *\n * The fingerprint has to be the one a walk would read, not a stand-in, or the\n * next comparison reports the file again under bash and every tool edit is\n * recorded twice.\n */\n private async remember(filePath: string): Promise<void> {\n if (this.manifest === undefined) return;\n const entries = new Map(this.manifest.entries);\n const fingerprint = await this.manifests.fingerprint(filePath);\n if (fingerprint === undefined) entries.delete(filePath);\n else entries.set(filePath, fingerprint);\n this.manifest = { entries, truncated: this.manifest.truncated };\n }\n}\n\n/** The tools this tracker knows how to attribute a change to. */\nexport const TRACKED_TOOLS: readonly FileEditTool[] = [EDIT_TOOL, WRITE_TOOL, BASH_TOOL];\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- const e=require("../../_virtual/_rolldown/runtime.cjs");let t=require("node:fs/promises");t=e.__toESM(t,1);let n=require("node:os");n=e.__toESM(n,1);let r=require("node:path");r=e.__toESM(r,1);let i=require("@agimon-ai/doompi-config");const a=/^\s*command:\s*(.+?)\s*$/mu;function o(){return process.env.PI_CODING_AGENT_DIR||r.default.join(n.default.homedir(),`.pi`,`agent`)}var s=class{path(){return(0,i.globalDoomConfigPath)()}packagePath(){return r.default.join(o(),`doom-file-edit`,`config.yaml`)}async command(){return await this.read(this.path())??await this.read(this.packagePath())}async read(e){try{let n=await t.default.readFile(e,`utf8`),r=/^editor:\s*\n((?:[ \t]+.*(?:\n|$))*)/mu.exec(n)?.[1];if(!r)return;let i=a.exec(r)?.[1]?.trim();if(!i)return;if(/^(?:[-+]?\d+(?:\.\d+)?|true|false|null)$/u.test(i))throw Error(`${e}: editor.command must be a string`);return i.replace(/^(?:"([\s\S]*)"|'([\s\S]*)')$/u,`$1$2`)}catch(e){if(e instanceof Error&&`code`in e&&e.code===`ENOENT`)return;throw e}}};exports.EditorConfigService=s;
1
+ const e=require("../../_virtual/_rolldown/runtime.cjs");let t=require("node:path");t=e.__toESM(t,1);let n=require("node:fs/promises");n=e.__toESM(n,1);let r=require("node:os");r=e.__toESM(r,1);let i=require("@agimon-ai/doompi-config");const a=/^\s*command:\s*(.+?)\s*$/mu;function o(){return process.env.PI_CODING_AGENT_DIR||t.default.join(r.default.homedir(),`.pi`,`agent`)}var s=class{path(){return(0,i.globalDoomConfigPath)()}packagePath(){return t.default.join(o(),`doom-file-edit`,`config.yaml`)}async command(){return await this.read(this.path())??await this.read(this.packagePath())}async read(e){try{let t=await n.default.readFile(e,`utf8`),r=/^editor:\s*\n((?:[ \t]+.*(?:\n|$))*)/mu.exec(t)?.[1];if(!r)return;let i=a.exec(r)?.[1]?.trim();if(!i)return;if(/^(?:[-+]?\d+(?:\.\d+)?|true|false|null)$/u.test(i))throw Error(`${e}: editor.command must be a string`);return i.replace(/^(?:"([\s\S]*)"|'([\s\S]*)')$/u,`$1$2`)}catch(e){if(e instanceof Error&&`code`in e&&e.code===`ENOENT`)return;throw e}}};exports.EditorConfigService=s;
2
2
  //# sourceMappingURL=EditorConfigService.cjs.map
@@ -1,2 +1,2 @@
1
- import e from"node:fs/promises";import t from"node:os";import n from"node:path";import{globalDoomConfigPath as r}from"@agimon-ai/doompi-config";const i=/^\s*command:\s*(.+?)\s*$/mu;function a(){return process.env.PI_CODING_AGENT_DIR||n.join(t.homedir(),`.pi`,`agent`)}var o=class{path(){return r()}packagePath(){return n.join(a(),`doom-file-edit`,`config.yaml`)}async command(){return await this.read(this.path())??await this.read(this.packagePath())}async read(t){try{let n=await e.readFile(t,`utf8`),r=/^editor:\s*\n((?:[ \t]+.*(?:\n|$))*)/mu.exec(n)?.[1];if(!r)return;let a=i.exec(r)?.[1]?.trim();if(!a)return;if(/^(?:[-+]?\d+(?:\.\d+)?|true|false|null)$/u.test(a))throw Error(`${t}: editor.command must be a string`);return a.replace(/^(?:"([\s\S]*)"|'([\s\S]*)')$/u,`$1$2`)}catch(e){if(e instanceof Error&&`code`in e&&e.code===`ENOENT`)return;throw e}}};export{o as EditorConfigService};
1
+ import e from"node:path";import t from"node:fs/promises";import n from"node:os";import{globalDoomConfigPath as r}from"@agimon-ai/doompi-config";const i=/^\s*command:\s*(.+?)\s*$/mu;function a(){return process.env.PI_CODING_AGENT_DIR||e.join(n.homedir(),`.pi`,`agent`)}var o=class{path(){return r()}packagePath(){return e.join(a(),`doom-file-edit`,`config.yaml`)}async command(){return await this.read(this.path())??await this.read(this.packagePath())}async read(e){try{let n=await t.readFile(e,`utf8`),r=/^editor:\s*\n((?:[ \t]+.*(?:\n|$))*)/mu.exec(n)?.[1];if(!r)return;let a=i.exec(r)?.[1]?.trim();if(!a)return;if(/^(?:[-+]?\d+(?:\.\d+)?|true|false|null)$/u.test(a))throw Error(`${e}: editor.command must be a string`);return a.replace(/^(?:"([\s\S]*)"|'([\s\S]*)')$/u,`$1$2`)}catch(e){if(e instanceof Error&&`code`in e&&e.code===`ENOENT`)return;throw e}}};export{o as EditorConfigService};
2
2
  //# sourceMappingURL=EditorConfigService.mjs.map
@@ -1,2 +1,2 @@
1
- const e=require("../../_virtual/_rolldown/runtime.cjs"),t=require("../process/commandLine.cjs");let n=require("node:fs/promises");n=e.__toESM(n,1);let r=require("node:path");r=e.__toESM(r,1);let i=require("cross-spawn");i=e.__toESM(i,1);var a=class{config;constructor(e){this.config=e}async resolve(e=process.env,n=process.platform){let r=await this.config.command(),i=[...r?[{template:r,source:`configured`}]:[],...e.VISUAL?[{template:e.VISUAL,source:`VISUAL`}]:[],...e.EDITOR?[{template:e.EDITOR,source:`EDITOR`}]:[],{template:n===`win32`?`notepad {file}`:`nano {file}`,source:`fallback`}];for(let r of i){let i=t.splitCommandLine(r.template)[0];if(i&&await this.isExecutable(i,e,n))return r}}async launch(e,n,r){try{let a=await this.resolve();if(!a)return{success:!1,error:`No editor command could be resolved`};let[o,...s]=t.expandEditorTemplate(a.template,e,n);if(!o)return{success:!1,error:`Editor command must not be empty`};r.stop();try{await new Promise((e,t)=>{let n=(0,i.default)(o,s,{stdio:`inherit`,shell:!1});n.once(`error`,t),n.once(`exit`,()=>e())})}finally{r.start(),r.requestRender(!0)}return{success:!0}}catch(e){return{success:!1,error:e instanceof Error?e.message:String(e)}}}async isExecutable(e,t,i){let a=i===`win32`?(t.PATHEXT??`.EXE;.CMD;.BAT`).split(`;`):[``],o=r.default.isAbsolute(e)||e.includes(r.default.sep)?[``]:(t.PATH??``).split(r.default.delimiter);for(let t of o)for(let o of a){let a=t?r.default.join(t,`${e}${o}`):`${e}${o}`;try{return await n.default.access(a,i===`win32`?n.default.constants.F_OK:n.default.constants.X_OK),!0}catch(e){if(!(e instanceof Error&&`code`in e&&(e.code===`ENOENT`||e.code===`EACCES`)))throw e}}return!1}};exports.EditorLauncher=a;
1
+ const e=require("../../_virtual/_rolldown/runtime.cjs"),t=require("../process/commandLine.cjs");let n=require("node:path");n=e.__toESM(n,1);let r=require("node:fs/promises");r=e.__toESM(r,1);let i=require("cross-spawn");i=e.__toESM(i,1);var a=class{config;constructor(e){this.config=e}async resolve(e=process.env,n=process.platform){let r=await this.config.command(),i=[...r?[{template:r,source:`configured`}]:[],...e.VISUAL?[{template:e.VISUAL,source:`VISUAL`}]:[],...e.EDITOR?[{template:e.EDITOR,source:`EDITOR`}]:[],{template:n===`win32`?`notepad {file}`:`nano {file}`,source:`fallback`}];for(let r of i){let i=t.splitCommandLine(r.template)[0];if(i&&await this.isExecutable(i,e,n))return r}}async launch(e,n,r){try{let a=await this.resolve();if(!a)return{success:!1,error:`No editor command could be resolved`};let[o,...s]=t.expandEditorTemplate(a.template,e,n);if(!o)return{success:!1,error:`Editor command must not be empty`};r.stop();try{await new Promise((e,t)=>{let n=(0,i.default)(o,s,{stdio:`inherit`,shell:!1});n.once(`error`,t),n.once(`exit`,()=>e())})}finally{r.start(),r.requestRender(!0)}return{success:!0}}catch(e){return{success:!1,error:e instanceof Error?e.message:String(e)}}}async isExecutable(e,t,i){let a=i===`win32`?(t.PATHEXT??`.EXE;.CMD;.BAT`).split(`;`):[``],o=n.default.isAbsolute(e)||e.includes(n.default.sep)?[``]:(t.PATH??``).split(n.default.delimiter);for(let t of o)for(let o of a){let a=t?n.default.join(t,`${e}${o}`):`${e}${o}`;try{return await r.default.access(a,i===`win32`?r.default.constants.F_OK:r.default.constants.X_OK),!0}catch(e){if(!(e instanceof Error&&`code`in e&&(e.code===`ENOENT`||e.code===`EACCES`)))throw e}}return!1}};exports.EditorLauncher=a;
2
2
  //# sourceMappingURL=EditorLauncher.cjs.map
@@ -1,2 +1,2 @@
1
- import{expandEditorTemplate as e,splitCommandLine as t}from"../process/commandLine.mjs";import n from"node:fs/promises";import r from"node:path";import i from"cross-spawn";var a=class{config;constructor(e){this.config=e}async resolve(e=process.env,n=process.platform){let r=await this.config.command(),i=[...r?[{template:r,source:`configured`}]:[],...e.VISUAL?[{template:e.VISUAL,source:`VISUAL`}]:[],...e.EDITOR?[{template:e.EDITOR,source:`EDITOR`}]:[],{template:n===`win32`?`notepad {file}`:`nano {file}`,source:`fallback`}];for(let r of i){let i=t(r.template)[0];if(i&&await this.isExecutable(i,e,n))return r}}async launch(t,n,r){try{let a=await this.resolve();if(!a)return{success:!1,error:`No editor command could be resolved`};let[o,...s]=e(a.template,t,n);if(!o)return{success:!1,error:`Editor command must not be empty`};r.stop();try{await new Promise((e,t)=>{let n=i(o,s,{stdio:`inherit`,shell:!1});n.once(`error`,t),n.once(`exit`,()=>e())})}finally{r.start(),r.requestRender(!0)}return{success:!0}}catch(e){return{success:!1,error:e instanceof Error?e.message:String(e)}}}async isExecutable(e,t,i){let a=i===`win32`?(t.PATHEXT??`.EXE;.CMD;.BAT`).split(`;`):[``],o=r.isAbsolute(e)||e.includes(r.sep)?[``]:(t.PATH??``).split(r.delimiter);for(let t of o)for(let o of a){let a=t?r.join(t,`${e}${o}`):`${e}${o}`;try{return await n.access(a,i===`win32`?n.constants.F_OK:n.constants.X_OK),!0}catch(e){if(!(e instanceof Error&&`code`in e&&(e.code===`ENOENT`||e.code===`EACCES`)))throw e}}return!1}};export{a as EditorLauncher};
1
+ import{expandEditorTemplate as e,splitCommandLine as t}from"../process/commandLine.mjs";import n from"node:path";import r from"node:fs/promises";import i from"cross-spawn";var a=class{config;constructor(e){this.config=e}async resolve(e=process.env,n=process.platform){let r=await this.config.command(),i=[...r?[{template:r,source:`configured`}]:[],...e.VISUAL?[{template:e.VISUAL,source:`VISUAL`}]:[],...e.EDITOR?[{template:e.EDITOR,source:`EDITOR`}]:[],{template:n===`win32`?`notepad {file}`:`nano {file}`,source:`fallback`}];for(let r of i){let i=t(r.template)[0];if(i&&await this.isExecutable(i,e,n))return r}}async launch(t,n,r){try{let a=await this.resolve();if(!a)return{success:!1,error:`No editor command could be resolved`};let[o,...s]=e(a.template,t,n);if(!o)return{success:!1,error:`Editor command must not be empty`};r.stop();try{await new Promise((e,t)=>{let n=i(o,s,{stdio:`inherit`,shell:!1});n.once(`error`,t),n.once(`exit`,()=>e())})}finally{r.start(),r.requestRender(!0)}return{success:!0}}catch(e){return{success:!1,error:e instanceof Error?e.message:String(e)}}}async isExecutable(e,t,i){let a=i===`win32`?(t.PATHEXT??`.EXE;.CMD;.BAT`).split(`;`):[``],o=n.isAbsolute(e)||e.includes(n.sep)?[``]:(t.PATH??``).split(n.delimiter);for(let t of o)for(let o of a){let a=t?n.join(t,`${e}${o}`):`${e}${o}`;try{return await r.access(a,i===`win32`?r.constants.F_OK:r.constants.X_OK),!0}catch(e){if(!(e instanceof Error&&`code`in e&&(e.code===`ENOENT`||e.code===`EACCES`)))throw e}}return!1}};export{a as EditorLauncher};
2
2
  //# sourceMappingURL=EditorLauncher.mjs.map
@@ -1,2 +1,2 @@
1
- const e=require("../../_virtual/_rolldown/runtime.cjs");let t=require("@agimon-ai/doompi-extension-contracts/child-process"),n=require("node:os");n=e.__toESM(n,1);let r=require("node:path");r=e.__toESM(r,1);let i=require("node:child_process"),a=require("node:crypto"),o=require("node:fs");o=e.__toESM(o,1);function s(e){return(0,a.createHash)(`sha256`).update(e).digest(`hex`).slice(0,16)}var c=class{sessionKey(e,n=process.env){if(n[t.SUBAGENT_CHILD_ENV]){let e=n[t.SUBAGENT_PARENT_SESSION_ENV];if(e)return e}return e}timelinePath(e,t){let i=this.gitCommonDirectory(e),a=s(o.default.realpathSync(e)),c=process.env.PI_CODING_AGENT_DIR||r.default.join(n.default.homedir(),`.pi`,`agent`),l=r.default.join(i??c,`doom-file-edit`);return o.default.mkdirSync(l,{recursive:!0}),r.default.join(l,`${a}-${s(t)}.jsonl`)}snapshotsPath(e,t){return`${this.timelinePath(e,t).replace(/\.jsonl$/u,``)}.blobs`}gitCommonDirectory(e){try{return(0,i.execFileSync)(`git`,[`rev-parse`,`--path-format=absolute`,`--git-common-dir`],{cwd:e,encoding:`utf8`,stdio:[`ignore`,`pipe`,`ignore`]}).trim()||void 0}catch(e){if(e instanceof Error)return;throw e}}};exports.FileEditPaths=c;
1
+ const e=require("../../_virtual/_rolldown/runtime.cjs");let t=require("node:fs");t=e.__toESM(t,1);let n=require("node:path");n=e.__toESM(n,1);let r=require("@agimon-ai/doompi-extension-contracts/child-process"),i=require("node:os");i=e.__toESM(i,1);let a=require("node:child_process"),o=require("node:crypto");const s=`doom-file-edit`,c=[`.pi`,`agent`];function l(e){return(0,o.createHash)(`sha256`).update(e).digest(`hex`).slice(0,16)}var u=class{sessionKey(e,t=process.env){if(t[r.SUBAGENT_CHILD_ENV]){let e=t[r.SUBAGENT_PARENT_SESSION_ENV];if(e)return e}return e}stateDirectory(){let e=process.env.PI_CODING_AGENT_DIR?.trim(),t=e===void 0||e===``?n.default.join(i.default.homedir(),...c):e;return n.default.join(t,s)}timelinePath(e,r){let i=this.stateDirectory();return t.default.mkdirSync(i,{recursive:!0}),n.default.join(i,`${l(t.default.realpathSync(e))}-${l(r)}.jsonl`)}snapshotsPath(e,t){return`${this.timelinePath(e,t).replace(/\.jsonl$/u,``)}.blobs`}legacyStateDirectory(e){let t=this.gitCommonDirectory(e);return t===void 0?void 0:n.default.join(t,s)}gitCommonDirectory(e){try{return(0,a.execFileSync)(`git`,[`rev-parse`,`--path-format=absolute`,`--git-common-dir`],{cwd:e,encoding:`utf8`,stdio:[`ignore`,`pipe`,`ignore`]}).trim()||void 0}catch(e){if(e instanceof Error)return;throw e}}};exports.FileEditPaths=u;
2
2
  //# sourceMappingURL=FileEditPaths.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"FileEditPaths.cjs","names":["createHash","SUBAGENT_CHILD_ENV","SUBAGENT_PARENT_SESSION_ENV","fs","path","os","execFileSync"],"sources":["../../../src/adapters/FileEditPaths/FileEditPaths.ts"],"sourcesContent":["import { execFileSync } from 'node:child_process';\nimport { createHash } from 'node:crypto';\nimport fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { SUBAGENT_CHILD_ENV, SUBAGENT_PARENT_SESSION_ENV } from '@agimon-ai/doompi-extension-contracts/child-process';\nimport type { IFileEditPaths } from '../../types/fileEditPaths';\n\nfunction hash(value: string): string {\n return createHash('sha256').update(value).digest('hex').slice(0, 16);\n}\n\nexport class FileEditPaths implements IFileEditPaths {\n sessionKey(sessionId: string, env: NodeJS.ProcessEnv = process.env): string {\n if (env[SUBAGENT_CHILD_ENV]) {\n const parent = env[SUBAGENT_PARENT_SESSION_ENV];\n if (parent) return parent;\n }\n return sessionId;\n }\n\n timelinePath(cwd: string, sessionKey: string): string {\n const commonDirectory = this.gitCommonDirectory(cwd);\n const worktree = hash(fs.realpathSync(cwd));\n const agentDirectory = process.env.PI_CODING_AGENT_DIR || path.join(os.homedir(), '.pi', 'agent');\n const directory = path.join(commonDirectory ?? agentDirectory, 'doom-file-edit');\n fs.mkdirSync(directory, { recursive: true });\n return path.join(directory, `${worktree}-${hash(sessionKey)}.jsonl`);\n }\n\n /**\n * Where this session's content snapshots live: beside its timeline and named\n * after it, so the two are found together and cleared together.\n */\n snapshotsPath(cwd: string, sessionKey: string): string {\n return `${this.timelinePath(cwd, sessionKey).replace(/\\.jsonl$/u, '')}.blobs`;\n }\n\n private gitCommonDirectory(cwd: string): string | undefined {\n try {\n const result = execFileSync('git', ['rev-parse', '--path-format=absolute', '--git-common-dir'], {\n cwd,\n encoding: 'utf8',\n stdio: ['ignore', 'pipe', 'ignore'],\n }).trim();\n return result || undefined;\n } catch (error) {\n if (error instanceof Error) return undefined;\n throw error;\n }\n }\n}\n"],"mappings":"kTAQA,SAAS,EAAK,EAAuB,CACnC,OAAA,EAAOA,EAAAA,WAAAA,CAAW,QAAQ,CAAC,CAAC,OAAO,CAAK,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,MAAM,EAAG,EAAE,CACrE,CAEA,IAAa,EAAb,KAAqD,CACnD,WAAW,EAAmB,EAAyB,QAAQ,IAAa,CAC1E,GAAI,EAAIC,EAAAA,oBAAqB,CAC3B,IAAM,EAAS,EAAIC,EAAAA,6BACnB,GAAI,EAAQ,OAAO,CACrB,CACA,OAAO,CACT,CAEA,aAAa,EAAa,EAA4B,CACpD,IAAM,EAAkB,KAAK,mBAAmB,CAAG,EAC7C,EAAW,EAAKC,EAAAA,QAAG,aAAa,CAAG,CAAC,EACpC,EAAiB,QAAQ,IAAI,qBAAuBC,EAAAA,QAAK,KAAKC,EAAAA,QAAG,QAAQ,EAAG,MAAO,OAAO,EAC1F,EAAYD,EAAAA,QAAK,KAAK,GAAmB,EAAgB,gBAAgB,EAE/E,OADA,EAAA,QAAG,UAAU,EAAW,CAAE,UAAW,EAAK,CAAC,EACpCA,EAAAA,QAAK,KAAK,EAAW,GAAG,EAAS,GAAG,EAAK,CAAU,EAAE,OAAO,CACrE,CAMA,cAAc,EAAa,EAA4B,CACrD,MAAO,GAAG,KAAK,aAAa,EAAK,CAAU,CAAC,CAAC,QAAQ,YAAa,EAAE,EAAE,OACxE,CAEA,mBAA2B,EAAiC,CAC1D,GAAI,CAMF,OAAA,EALeE,EAAAA,aAAAA,CAAa,MAAO,CAAC,YAAa,yBAA0B,kBAAkB,EAAG,CAC9F,MACA,SAAU,OACV,MAAO,CAAC,SAAU,OAAQ,QAAQ,CACpC,CAAC,CAAC,CAAC,KACS,GAAK,IAAA,EACnB,OAAS,EAAO,CACd,GAAI,aAAiB,MAAO,OAC5B,MAAM,CACR,CACF,CACF"}
1
+ {"version":3,"file":"FileEditPaths.cjs","names":["createHash","SUBAGENT_CHILD_ENV","SUBAGENT_PARENT_SESSION_ENV","path","os","fs","execFileSync"],"sources":["../../../src/adapters/FileEditPaths/FileEditPaths.ts"],"sourcesContent":["import { execFileSync } from 'node:child_process';\nimport { createHash } from 'node:crypto';\nimport fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { SUBAGENT_CHILD_ENV, SUBAGENT_PARENT_SESSION_ENV } from '@agimon-ai/doompi-extension-contracts/child-process';\nimport type { IFileEditPaths } from '../../types/fileEditPaths';\n\n/** The directory this package owns, under whichever root holds agent state. */\nconst STATE_DIRECTORY = 'doom-file-edit';\nconst AGENT_DIRECTORY = ['.pi', 'agent'];\n\nfunction hash(value: string): string {\n return createHash('sha256').update(value).digest('hex').slice(0, 16);\n}\n\nexport class FileEditPaths implements IFileEditPaths {\n sessionKey(sessionId: string, env: NodeJS.ProcessEnv = process.env): string {\n if (env[SUBAGENT_CHILD_ENV]) {\n const parent = env[SUBAGENT_PARENT_SESSION_ENV];\n if (parent) return parent;\n }\n return sessionId;\n }\n\n /**\n * Where every session's state lives: the Pi agent directory, the same root\n * every other Doom package writes to, and never inside the working tree.\n *\n * An earlier layout put this in the repository's git common directory. That\n * is git's own storage: nothing in git's lifecycle prunes a directory we add\n * to it, a linked worktree writes into the main checkout rather than its own,\n * a submodule writes inside `.git/modules`, and `PI_CODING_AGENT_DIR` was\n * ignored outright. The snapshots are verbatim copies of files the session\n * touched, so where they land is a question about handling content, not a\n * question about convenience.\n */\n stateDirectory(): string {\n const configured = process.env.PI_CODING_AGENT_DIR?.trim();\n const root =\n configured === undefined || configured === '' ? path.join(os.homedir(), ...AGENT_DIRECTORY) : configured;\n return path.join(root, STATE_DIRECTORY);\n }\n\n /**\n * One session's timeline, named from its working directory and its session so\n * the hub and the session API can find it from another process knowing only\n * those two things.\n */\n timelinePath(cwd: string, sessionKey: string): string {\n const directory = this.stateDirectory();\n fs.mkdirSync(directory, { recursive: true });\n return path.join(directory, `${hash(fs.realpathSync(cwd))}-${hash(sessionKey)}.jsonl`);\n }\n\n /**\n * Where this session's content snapshots live: beside its timeline and named\n * after it, so the two are found together and cleared together.\n */\n snapshotsPath(cwd: string, sessionKey: string): string {\n return `${this.timelinePath(cwd, sessionKey).replace(/\\.jsonl$/u, '')}.blobs`;\n }\n\n legacyStateDirectory(cwd: string): string | undefined {\n const common = this.gitCommonDirectory(cwd);\n return common === undefined ? undefined : path.join(common, STATE_DIRECTORY);\n }\n\n /** Only still read so the state an older build left behind can be cleared away. */\n private gitCommonDirectory(cwd: string): string | undefined {\n try {\n const result = execFileSync('git', ['rev-parse', '--path-format=absolute', '--git-common-dir'], {\n cwd,\n encoding: 'utf8',\n stdio: ['ignore', 'pipe', 'ignore'],\n }).trim();\n return result || undefined;\n } catch (error) {\n if (error instanceof Error) return undefined;\n throw error;\n }\n }\n}\n"],"mappings":"sTASA,MAAM,EAAkB,iBAClB,EAAkB,CAAC,MAAO,OAAO,EAEvC,SAAS,EAAK,EAAuB,CACnC,OAAA,EAAOA,EAAAA,WAAAA,CAAW,QAAQ,CAAC,CAAC,OAAO,CAAK,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,MAAM,EAAG,EAAE,CACrE,CAEA,IAAa,EAAb,KAAqD,CACnD,WAAW,EAAmB,EAAyB,QAAQ,IAAa,CAC1E,GAAI,EAAIC,EAAAA,oBAAqB,CAC3B,IAAM,EAAS,EAAIC,EAAAA,6BACnB,GAAI,EAAQ,OAAO,CACrB,CACA,OAAO,CACT,CAcA,gBAAyB,CACvB,IAAM,EAAa,QAAQ,IAAI,qBAAqB,KAAK,EACnD,EACJ,IAAe,IAAA,IAAa,IAAe,GAAKC,EAAAA,QAAK,KAAKC,EAAAA,QAAG,QAAQ,EAAG,GAAG,CAAe,EAAI,EAChG,OAAOD,EAAAA,QAAK,KAAK,EAAM,CAAe,CACxC,CAOA,aAAa,EAAa,EAA4B,CACpD,IAAM,EAAY,KAAK,eAAe,EAEtC,OADA,EAAA,QAAG,UAAU,EAAW,CAAE,UAAW,EAAK,CAAC,EACpCA,EAAAA,QAAK,KAAK,EAAW,GAAG,EAAKE,EAAAA,QAAG,aAAa,CAAG,CAAC,EAAE,GAAG,EAAK,CAAU,EAAE,OAAO,CACvF,CAMA,cAAc,EAAa,EAA4B,CACrD,MAAO,GAAG,KAAK,aAAa,EAAK,CAAU,CAAC,CAAC,QAAQ,YAAa,EAAE,EAAE,OACxE,CAEA,qBAAqB,EAAiC,CACpD,IAAM,EAAS,KAAK,mBAAmB,CAAG,EAC1C,OAAO,IAAW,IAAA,GAAY,IAAA,GAAYF,EAAAA,QAAK,KAAK,EAAQ,CAAe,CAC7E,CAGA,mBAA2B,EAAiC,CAC1D,GAAI,CAMF,OAAA,EALeG,EAAAA,aAAAA,CAAa,MAAO,CAAC,YAAa,yBAA0B,kBAAkB,EAAG,CAC9F,MACA,SAAU,OACV,MAAO,CAAC,SAAU,OAAQ,QAAQ,CACpC,CAAC,CAAC,CAAC,KACS,GAAK,IAAA,EACnB,OAAS,EAAO,CACd,GAAI,aAAiB,MAAO,OAC5B,MAAM,CACR,CACF,CACF"}
@@ -1,2 +1,2 @@
1
- import{SUBAGENT_CHILD_ENV as e,SUBAGENT_PARENT_SESSION_ENV as t}from"@agimon-ai/doompi-extension-contracts/child-process";import n from"node:os";import r from"node:path";import{execFileSync as i}from"node:child_process";import{createHash as a}from"node:crypto";import o from"node:fs";function s(e){return a(`sha256`).update(e).digest(`hex`).slice(0,16)}var c=class{sessionKey(n,r=process.env){if(r[e]){let e=r[t];if(e)return e}return n}timelinePath(e,t){let i=this.gitCommonDirectory(e),a=s(o.realpathSync(e)),c=process.env.PI_CODING_AGENT_DIR||r.join(n.homedir(),`.pi`,`agent`),l=r.join(i??c,`doom-file-edit`);return o.mkdirSync(l,{recursive:!0}),r.join(l,`${a}-${s(t)}.jsonl`)}snapshotsPath(e,t){return`${this.timelinePath(e,t).replace(/\.jsonl$/u,``)}.blobs`}gitCommonDirectory(e){try{return i(`git`,[`rev-parse`,`--path-format=absolute`,`--git-common-dir`],{cwd:e,encoding:`utf8`,stdio:[`ignore`,`pipe`,`ignore`]}).trim()||void 0}catch(e){if(e instanceof Error)return;throw e}}};export{c as FileEditPaths};
1
+ import e from"node:fs";import t from"node:path";import{SUBAGENT_CHILD_ENV as n,SUBAGENT_PARENT_SESSION_ENV as r}from"@agimon-ai/doompi-extension-contracts/child-process";import i from"node:os";import{execFileSync as a}from"node:child_process";import{createHash as o}from"node:crypto";const s=`doom-file-edit`,c=[`.pi`,`agent`];function l(e){return o(`sha256`).update(e).digest(`hex`).slice(0,16)}var u=class{sessionKey(e,t=process.env){if(t[n]){let e=t[r];if(e)return e}return e}stateDirectory(){let e=process.env.PI_CODING_AGENT_DIR?.trim(),n=e===void 0||e===``?t.join(i.homedir(),...c):e;return t.join(n,s)}timelinePath(n,r){let i=this.stateDirectory();return e.mkdirSync(i,{recursive:!0}),t.join(i,`${l(e.realpathSync(n))}-${l(r)}.jsonl`)}snapshotsPath(e,t){return`${this.timelinePath(e,t).replace(/\.jsonl$/u,``)}.blobs`}legacyStateDirectory(e){let n=this.gitCommonDirectory(e);return n===void 0?void 0:t.join(n,s)}gitCommonDirectory(e){try{return a(`git`,[`rev-parse`,`--path-format=absolute`,`--git-common-dir`],{cwd:e,encoding:`utf8`,stdio:[`ignore`,`pipe`,`ignore`]}).trim()||void 0}catch(e){if(e instanceof Error)return;throw e}}};export{u as FileEditPaths};
2
2
  //# sourceMappingURL=FileEditPaths.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"FileEditPaths.mjs","names":[],"sources":["../../../src/adapters/FileEditPaths/FileEditPaths.ts"],"sourcesContent":["import { execFileSync } from 'node:child_process';\nimport { createHash } from 'node:crypto';\nimport fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { SUBAGENT_CHILD_ENV, SUBAGENT_PARENT_SESSION_ENV } from '@agimon-ai/doompi-extension-contracts/child-process';\nimport type { IFileEditPaths } from '../../types/fileEditPaths';\n\nfunction hash(value: string): string {\n return createHash('sha256').update(value).digest('hex').slice(0, 16);\n}\n\nexport class FileEditPaths implements IFileEditPaths {\n sessionKey(sessionId: string, env: NodeJS.ProcessEnv = process.env): string {\n if (env[SUBAGENT_CHILD_ENV]) {\n const parent = env[SUBAGENT_PARENT_SESSION_ENV];\n if (parent) return parent;\n }\n return sessionId;\n }\n\n timelinePath(cwd: string, sessionKey: string): string {\n const commonDirectory = this.gitCommonDirectory(cwd);\n const worktree = hash(fs.realpathSync(cwd));\n const agentDirectory = process.env.PI_CODING_AGENT_DIR || path.join(os.homedir(), '.pi', 'agent');\n const directory = path.join(commonDirectory ?? agentDirectory, 'doom-file-edit');\n fs.mkdirSync(directory, { recursive: true });\n return path.join(directory, `${worktree}-${hash(sessionKey)}.jsonl`);\n }\n\n /**\n * Where this session's content snapshots live: beside its timeline and named\n * after it, so the two are found together and cleared together.\n */\n snapshotsPath(cwd: string, sessionKey: string): string {\n return `${this.timelinePath(cwd, sessionKey).replace(/\\.jsonl$/u, '')}.blobs`;\n }\n\n private gitCommonDirectory(cwd: string): string | undefined {\n try {\n const result = execFileSync('git', ['rev-parse', '--path-format=absolute', '--git-common-dir'], {\n cwd,\n encoding: 'utf8',\n stdio: ['ignore', 'pipe', 'ignore'],\n }).trim();\n return result || undefined;\n } catch (error) {\n if (error instanceof Error) return undefined;\n throw error;\n }\n }\n}\n"],"mappings":"4RAQA,SAAS,EAAK,EAAuB,CACnC,OAAO,EAAW,QAAQ,CAAC,CAAC,OAAO,CAAK,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,MAAM,EAAG,EAAE,CACrE,CAEA,IAAa,EAAb,KAAqD,CACnD,WAAW,EAAmB,EAAyB,QAAQ,IAAa,CAC1E,GAAI,EAAI,GAAqB,CAC3B,IAAM,EAAS,EAAI,GACnB,GAAI,EAAQ,OAAO,CACrB,CACA,OAAO,CACT,CAEA,aAAa,EAAa,EAA4B,CACpD,IAAM,EAAkB,KAAK,mBAAmB,CAAG,EAC7C,EAAW,EAAK,EAAG,aAAa,CAAG,CAAC,EACpC,EAAiB,QAAQ,IAAI,qBAAuB,EAAK,KAAK,EAAG,QAAQ,EAAG,MAAO,OAAO,EAC1F,EAAY,EAAK,KAAK,GAAmB,EAAgB,gBAAgB,EAE/E,OADA,EAAG,UAAU,EAAW,CAAE,UAAW,EAAK,CAAC,EACpC,EAAK,KAAK,EAAW,GAAG,EAAS,GAAG,EAAK,CAAU,EAAE,OAAO,CACrE,CAMA,cAAc,EAAa,EAA4B,CACrD,MAAO,GAAG,KAAK,aAAa,EAAK,CAAU,CAAC,CAAC,QAAQ,YAAa,EAAE,EAAE,OACxE,CAEA,mBAA2B,EAAiC,CAC1D,GAAI,CAMF,OALe,EAAa,MAAO,CAAC,YAAa,yBAA0B,kBAAkB,EAAG,CAC9F,MACA,SAAU,OACV,MAAO,CAAC,SAAU,OAAQ,QAAQ,CACpC,CAAC,CAAC,CAAC,KACS,GAAK,IAAA,EACnB,OAAS,EAAO,CACd,GAAI,aAAiB,MAAO,OAC5B,MAAM,CACR,CACF,CACF"}
1
+ {"version":3,"file":"FileEditPaths.mjs","names":[],"sources":["../../../src/adapters/FileEditPaths/FileEditPaths.ts"],"sourcesContent":["import { execFileSync } from 'node:child_process';\nimport { createHash } from 'node:crypto';\nimport fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { SUBAGENT_CHILD_ENV, SUBAGENT_PARENT_SESSION_ENV } from '@agimon-ai/doompi-extension-contracts/child-process';\nimport type { IFileEditPaths } from '../../types/fileEditPaths';\n\n/** The directory this package owns, under whichever root holds agent state. */\nconst STATE_DIRECTORY = 'doom-file-edit';\nconst AGENT_DIRECTORY = ['.pi', 'agent'];\n\nfunction hash(value: string): string {\n return createHash('sha256').update(value).digest('hex').slice(0, 16);\n}\n\nexport class FileEditPaths implements IFileEditPaths {\n sessionKey(sessionId: string, env: NodeJS.ProcessEnv = process.env): string {\n if (env[SUBAGENT_CHILD_ENV]) {\n const parent = env[SUBAGENT_PARENT_SESSION_ENV];\n if (parent) return parent;\n }\n return sessionId;\n }\n\n /**\n * Where every session's state lives: the Pi agent directory, the same root\n * every other Doom package writes to, and never inside the working tree.\n *\n * An earlier layout put this in the repository's git common directory. That\n * is git's own storage: nothing in git's lifecycle prunes a directory we add\n * to it, a linked worktree writes into the main checkout rather than its own,\n * a submodule writes inside `.git/modules`, and `PI_CODING_AGENT_DIR` was\n * ignored outright. The snapshots are verbatim copies of files the session\n * touched, so where they land is a question about handling content, not a\n * question about convenience.\n */\n stateDirectory(): string {\n const configured = process.env.PI_CODING_AGENT_DIR?.trim();\n const root =\n configured === undefined || configured === '' ? path.join(os.homedir(), ...AGENT_DIRECTORY) : configured;\n return path.join(root, STATE_DIRECTORY);\n }\n\n /**\n * One session's timeline, named from its working directory and its session so\n * the hub and the session API can find it from another process knowing only\n * those two things.\n */\n timelinePath(cwd: string, sessionKey: string): string {\n const directory = this.stateDirectory();\n fs.mkdirSync(directory, { recursive: true });\n return path.join(directory, `${hash(fs.realpathSync(cwd))}-${hash(sessionKey)}.jsonl`);\n }\n\n /**\n * Where this session's content snapshots live: beside its timeline and named\n * after it, so the two are found together and cleared together.\n */\n snapshotsPath(cwd: string, sessionKey: string): string {\n return `${this.timelinePath(cwd, sessionKey).replace(/\\.jsonl$/u, '')}.blobs`;\n }\n\n legacyStateDirectory(cwd: string): string | undefined {\n const common = this.gitCommonDirectory(cwd);\n return common === undefined ? undefined : path.join(common, STATE_DIRECTORY);\n }\n\n /** Only still read so the state an older build left behind can be cleared away. */\n private gitCommonDirectory(cwd: string): string | undefined {\n try {\n const result = execFileSync('git', ['rev-parse', '--path-format=absolute', '--git-common-dir'], {\n cwd,\n encoding: 'utf8',\n stdio: ['ignore', 'pipe', 'ignore'],\n }).trim();\n return result || undefined;\n } catch (error) {\n if (error instanceof Error) return undefined;\n throw error;\n }\n }\n}\n"],"mappings":"4RASA,MAAM,EAAkB,iBAClB,EAAkB,CAAC,MAAO,OAAO,EAEvC,SAAS,EAAK,EAAuB,CACnC,OAAO,EAAW,QAAQ,CAAC,CAAC,OAAO,CAAK,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,MAAM,EAAG,EAAE,CACrE,CAEA,IAAa,EAAb,KAAqD,CACnD,WAAW,EAAmB,EAAyB,QAAQ,IAAa,CAC1E,GAAI,EAAI,GAAqB,CAC3B,IAAM,EAAS,EAAI,GACnB,GAAI,EAAQ,OAAO,CACrB,CACA,OAAO,CACT,CAcA,gBAAyB,CACvB,IAAM,EAAa,QAAQ,IAAI,qBAAqB,KAAK,EACnD,EACJ,IAAe,IAAA,IAAa,IAAe,GAAK,EAAK,KAAK,EAAG,QAAQ,EAAG,GAAG,CAAe,EAAI,EAChG,OAAO,EAAK,KAAK,EAAM,CAAe,CACxC,CAOA,aAAa,EAAa,EAA4B,CACpD,IAAM,EAAY,KAAK,eAAe,EAEtC,OADA,EAAG,UAAU,EAAW,CAAE,UAAW,EAAK,CAAC,EACpC,EAAK,KAAK,EAAW,GAAG,EAAK,EAAG,aAAa,CAAG,CAAC,EAAE,GAAG,EAAK,CAAU,EAAE,OAAO,CACvF,CAMA,cAAc,EAAa,EAA4B,CACrD,MAAO,GAAG,KAAK,aAAa,EAAK,CAAU,CAAC,CAAC,QAAQ,YAAa,EAAE,EAAE,OACxE,CAEA,qBAAqB,EAAiC,CACpD,IAAM,EAAS,KAAK,mBAAmB,CAAG,EAC1C,OAAO,IAAW,IAAA,GAAY,IAAA,GAAY,EAAK,KAAK,EAAQ,CAAe,CAC7E,CAGA,mBAA2B,EAAiC,CAC1D,GAAI,CAMF,OALe,EAAa,MAAO,CAAC,YAAa,yBAA0B,kBAAkB,EAAG,CAC9F,MACA,SAAU,OACV,MAAO,CAAC,SAAU,OAAQ,QAAQ,CACpC,CAAC,CAAC,CAAC,KACS,GAAK,IAAA,EACnB,OAAS,EAAO,CACd,GAAI,aAAiB,MAAO,OAC5B,MAAM,CACR,CACF,CACF"}
@@ -1,5 +1,5 @@
1
- const e=require("../../_virtual/_rolldown/runtime.cjs");let t=require("node:fs/promises");t=e.__toESM(t,1);let n=require("node:path");n=e.__toESM(n,1);let r=require("node:child_process");const i=262144,a=8192;function o(e,t){let n=t||e.length>200,r=e.slice(0,200);return n&&r.push(`... diff truncated at 200 lines / ${i} bytes ...`),{lines:r,truncated:n}}function s(e,t,n,r){let s=r.byteLength>=i;if(r.subarray(0,a).includes(0))return{path:e,state:`binary`,lines:[`Binary file changed (${r.byteLength} bytes shown by git)`],additions:0,removals:0,tracked:n,truncated:s,suggestedLine:1};let c=o(r.subarray(0,i).toString(`utf8`).split(`
2
- `),s),l=c.lines.filter(e=>e.startsWith(`+`)&&!e.startsWith(`+++`)).length,u=c.lines.filter(e=>e.startsWith(`-`)&&!e.startsWith(`---`)).length,d=c.lines.find(e=>e.startsWith(`@@`))?.match(/\+(\d+)/u);return{path:e,state:t,lines:c.lines,additions:l,removals:u,tracked:n,truncated:c.truncated,suggestedLine:Number(d?.[1]??1)}}var c=class{async diff(e,i){let a=n.default.resolve(e,i),o=n.default.relative(e,a),c=this.isTracked(e,o),l=!0;try{await t.default.access(a)}catch(e){if(e instanceof Error&&`code`in e&&e.code===`ENOENT`)l=!1;else throw e}if(!c&&!l)return s(a,`deleted`,!1,Buffer.from(`File no longer exists`));if(!c)return this.fullAdd(a);let u=(0,r.spawnSync)(`git`,[`diff`,`--no-ext-diff`,`--no-color`,`--unified=3`,`HEAD`,`--`,o],{cwd:e,encoding:`buffer`,maxBuffer:262144}).stdout??Buffer.alloc(0);return l&&/^(Binary files .* differ|GIT binary patch)$/mu.test(u.toString(`utf8`))?{path:a,state:`binary`,lines:[`Binary tracked file changed`],additions:0,removals:0,tracked:!0,truncated:!1,suggestedLine:1}:s(a,l?u.length>0?`modified`:`unchanged`:`deleted`,!0,u)}isTracked(e,t){return(0,r.spawnSync)(`git`,[`ls-files`,`--error-unmatch`,`--`,t],{cwd:e,stdio:`ignore`}).status===0}async fullAdd(e){let r=await t.default.open(e,`r`);try{let t=Buffer.alloc(i),{bytesRead:o}=await r.read(t,0,t.length,0),c=t.subarray(0,o);if(c.subarray(0,a).includes(0))return s(e,`binary`,!1,Buffer.from(`Binary untracked file (${o}+ bytes)`));let l=c.toString(`utf8`).split(`
1
+ const e=require("../../_virtual/_rolldown/runtime.cjs");let t=require("node:path");t=e.__toESM(t,1);let n=require("node:fs/promises");n=e.__toESM(n,1);let r=require("node:child_process");const i=262144,a=8192;function o(e,t){let n=t||e.length>200,r=e.slice(0,200);return n&&r.push(`... diff truncated at 200 lines / ${i} bytes ...`),{lines:r,truncated:n}}function s(e,t,n,r){let s=r.byteLength>=i;if(r.subarray(0,a).includes(0))return{path:e,state:`binary`,lines:[`Binary file changed (${r.byteLength} bytes shown by git)`],additions:0,removals:0,tracked:n,truncated:s,suggestedLine:1};let c=o(r.subarray(0,i).toString(`utf8`).split(`
2
+ `),s),l=c.lines.filter(e=>e.startsWith(`+`)&&!e.startsWith(`+++`)).length,u=c.lines.filter(e=>e.startsWith(`-`)&&!e.startsWith(`---`)).length,d=c.lines.find(e=>e.startsWith(`@@`))?.match(/\+(\d+)/u);return{path:e,state:t,lines:c.lines,additions:l,removals:u,tracked:n,truncated:c.truncated,suggestedLine:Number(d?.[1]??1)}}var c=class{async diff(e,i){let a=t.default.resolve(e,i),o=t.default.relative(e,a),c=this.isTracked(e,o),l=!0;try{await n.default.access(a)}catch(e){if(e instanceof Error&&`code`in e&&e.code===`ENOENT`)l=!1;else throw e}if(!c&&!l)return s(a,`deleted`,!1,Buffer.from(`File no longer exists`));if(!c)return this.fullAdd(a);let u=(0,r.spawnSync)(`git`,[`diff`,`--no-ext-diff`,`--no-color`,`--unified=3`,`HEAD`,`--`,o],{cwd:e,encoding:`buffer`,maxBuffer:262144}).stdout??Buffer.alloc(0);return l&&/^(Binary files .* differ|GIT binary patch)$/mu.test(u.toString(`utf8`))?{path:a,state:`binary`,lines:[`Binary tracked file changed`],additions:0,removals:0,tracked:!0,truncated:!1,suggestedLine:1}:s(a,l?u.length>0?`modified`:`unchanged`:`deleted`,!0,u)}isTracked(e,t){return(0,r.spawnSync)(`git`,[`ls-files`,`--error-unmatch`,`--`,t],{cwd:e,stdio:`ignore`}).status===0}async fullAdd(e){let r=await n.default.open(e,`r`);try{let n=Buffer.alloc(i),{bytesRead:o}=await r.read(n,0,n.length,0),c=n.subarray(0,o);if(c.subarray(0,a).includes(0))return s(e,`binary`,!1,Buffer.from(`Binary untracked file (${o}+ bytes)`));let l=c.toString(`utf8`).split(`
3
3
  `).map(e=>`+${e}`).join(`
4
- `),u=`--- /dev/null\n+++ b/${n.default.basename(e)}\n@@ -0,0 +1 @@\n`;return s(e,`added`,!1,Buffer.from(u+l))}finally{await r.close()}}};exports.GitDiffService=c,exports.MAX_DIFF_BYTES=i;
4
+ `),u=`--- /dev/null\n+++ b/${t.default.basename(e)}\n@@ -0,0 +1 @@\n`;return s(e,`added`,!1,Buffer.from(u+l))}finally{await r.close()}}};exports.GitDiffService=c,exports.MAX_DIFF_BYTES=i;
5
5
  //# sourceMappingURL=GitDiffService.cjs.map
@@ -1,5 +1,5 @@
1
- import e from"node:fs/promises";import t from"node:path";import{spawnSync as n}from"node:child_process";const r=262144,i=8192;function a(e,t){let n=t||e.length>200,i=e.slice(0,200);return n&&i.push(`... diff truncated at 200 lines / ${r} bytes ...`),{lines:i,truncated:n}}function o(e,t,n,o){let s=o.byteLength>=r;if(o.subarray(0,i).includes(0))return{path:e,state:`binary`,lines:[`Binary file changed (${o.byteLength} bytes shown by git)`],additions:0,removals:0,tracked:n,truncated:s,suggestedLine:1};let c=a(o.subarray(0,r).toString(`utf8`).split(`
2
- `),s),l=c.lines.filter(e=>e.startsWith(`+`)&&!e.startsWith(`+++`)).length,u=c.lines.filter(e=>e.startsWith(`-`)&&!e.startsWith(`---`)).length,d=c.lines.find(e=>e.startsWith(`@@`))?.match(/\+(\d+)/u);return{path:e,state:t,lines:c.lines,additions:l,removals:u,tracked:n,truncated:c.truncated,suggestedLine:Number(d?.[1]??1)}}var s=class{async diff(r,i){let a=t.resolve(r,i),s=t.relative(r,a),c=this.isTracked(r,s),l=!0;try{await e.access(a)}catch(e){if(e instanceof Error&&`code`in e&&e.code===`ENOENT`)l=!1;else throw e}if(!c&&!l)return o(a,`deleted`,!1,Buffer.from(`File no longer exists`));if(!c)return this.fullAdd(a);let u=n(`git`,[`diff`,`--no-ext-diff`,`--no-color`,`--unified=3`,`HEAD`,`--`,s],{cwd:r,encoding:`buffer`,maxBuffer:262144}).stdout??Buffer.alloc(0);return l&&/^(Binary files .* differ|GIT binary patch)$/mu.test(u.toString(`utf8`))?{path:a,state:`binary`,lines:[`Binary tracked file changed`],additions:0,removals:0,tracked:!0,truncated:!1,suggestedLine:1}:o(a,l?u.length>0?`modified`:`unchanged`:`deleted`,!0,u)}isTracked(e,t){return n(`git`,[`ls-files`,`--error-unmatch`,`--`,t],{cwd:e,stdio:`ignore`}).status===0}async fullAdd(n){let a=await e.open(n,`r`);try{let e=Buffer.alloc(r),{bytesRead:s}=await a.read(e,0,e.length,0),c=e.subarray(0,s);if(c.subarray(0,i).includes(0))return o(n,`binary`,!1,Buffer.from(`Binary untracked file (${s}+ bytes)`));let l=c.toString(`utf8`).split(`
1
+ import e from"node:path";import t from"node:fs/promises";import{spawnSync as n}from"node:child_process";const r=262144,i=8192;function a(e,t){let n=t||e.length>200,i=e.slice(0,200);return n&&i.push(`... diff truncated at 200 lines / ${r} bytes ...`),{lines:i,truncated:n}}function o(e,t,n,o){let s=o.byteLength>=r;if(o.subarray(0,i).includes(0))return{path:e,state:`binary`,lines:[`Binary file changed (${o.byteLength} bytes shown by git)`],additions:0,removals:0,tracked:n,truncated:s,suggestedLine:1};let c=a(o.subarray(0,r).toString(`utf8`).split(`
2
+ `),s),l=c.lines.filter(e=>e.startsWith(`+`)&&!e.startsWith(`+++`)).length,u=c.lines.filter(e=>e.startsWith(`-`)&&!e.startsWith(`---`)).length,d=c.lines.find(e=>e.startsWith(`@@`))?.match(/\+(\d+)/u);return{path:e,state:t,lines:c.lines,additions:l,removals:u,tracked:n,truncated:c.truncated,suggestedLine:Number(d?.[1]??1)}}var s=class{async diff(r,i){let a=e.resolve(r,i),s=e.relative(r,a),c=this.isTracked(r,s),l=!0;try{await t.access(a)}catch(e){if(e instanceof Error&&`code`in e&&e.code===`ENOENT`)l=!1;else throw e}if(!c&&!l)return o(a,`deleted`,!1,Buffer.from(`File no longer exists`));if(!c)return this.fullAdd(a);let u=n(`git`,[`diff`,`--no-ext-diff`,`--no-color`,`--unified=3`,`HEAD`,`--`,s],{cwd:r,encoding:`buffer`,maxBuffer:262144}).stdout??Buffer.alloc(0);return l&&/^(Binary files .* differ|GIT binary patch)$/mu.test(u.toString(`utf8`))?{path:a,state:`binary`,lines:[`Binary tracked file changed`],additions:0,removals:0,tracked:!0,truncated:!1,suggestedLine:1}:o(a,l?u.length>0?`modified`:`unchanged`:`deleted`,!0,u)}isTracked(e,t){return n(`git`,[`ls-files`,`--error-unmatch`,`--`,t],{cwd:e,stdio:`ignore`}).status===0}async fullAdd(n){let a=await t.open(n,`r`);try{let t=Buffer.alloc(r),{bytesRead:s}=await a.read(t,0,t.length,0),c=t.subarray(0,s);if(c.subarray(0,i).includes(0))return o(n,`binary`,!1,Buffer.from(`Binary untracked file (${s}+ bytes)`));let l=c.toString(`utf8`).split(`
3
3
  `).map(e=>`+${e}`).join(`
4
- `),u=`--- /dev/null\n+++ b/${t.basename(n)}\n@@ -0,0 +1 @@\n`;return o(n,`added`,!1,Buffer.from(u+l))}finally{await a.close()}}};export{s as GitDiffService,r as MAX_DIFF_BYTES};
4
+ `),u=`--- /dev/null\n+++ b/${e.basename(n)}\n@@ -0,0 +1 @@\n`;return o(n,`added`,!1,Buffer.from(u+l))}finally{await a.close()}}};export{s as GitDiffService,r as MAX_DIFF_BYTES};
5
5
  //# sourceMappingURL=GitDiffService.mjs.map
@@ -1,2 +1,2 @@
1
- const e=require("../../_virtual/_rolldown/runtime.cjs"),t=require("../../services/fileChanges.cjs");let n=require("node:fs/promises");n=e.__toESM(n,1);function r(e,t){return e instanceof Error&&`code`in e&&e.code===t}var i=class{filePath;initialize(e){this.filePath=e}async append(e){await this.withLock(async()=>n.default.appendFile(this.requirePath(),`${JSON.stringify(e)}\n`,`utf8`))}async list(){return t.foldEntries(t.confirmedChanges(await this.events()))}async versions(e){return t.foldVersions(await this.events(),e)}async clear(){try{await n.default.unlink(this.requirePath())}catch(e){if(!r(e,`ENOENT`))throw e}}async events(){let e;try{e=await n.default.readFile(this.requirePath(),`utf8`)}catch(e){if(r(e,`ENOENT`))return[];throw e}return t.parseTimeline(e,e=>console.warn(`Ignoring malformed file edit timeline line: ${e.slice(0,200)}`))}requirePath(){if(!this.filePath)throw Error(`Timeline store is not initialized`);return this.filePath}async withLock(e){let t=`${this.requirePath()}.lock`,n=await this.acquire(t);try{return await e()}finally{await n.close(),await this.release(t)}}async acquire(e){for(let t=0;t<400;t+=1)try{return await n.default.open(e,`wx`)}catch(t){if(!r(t,`EEXIST`))throw t;if(await this.breakStale(e))continue;await new Promise(e=>setTimeout(e,25))}throw Error(`Timed out acquiring timeline lock ${e}`)}async breakStale(e){try{let t=await n.default.stat(e);return Date.now()-t.mtimeMs<3e4?!1:(await n.default.unlink(e),!0)}catch(e){if(r(e,`ENOENT`))return!0;throw e}}async release(e){try{await n.default.unlink(e)}catch(e){r(e,`ENOENT`)||console.warn(`Could not remove timeline lock: ${String(e)}`)}}};exports.TimelineStore=i;
1
+ const e=require("../../_virtual/_rolldown/runtime.cjs"),t=require("../../services/fileChanges.cjs");let n=require("node:fs/promises");n=e.__toESM(n,1);function r(e,t){return e instanceof Error&&`code`in e&&e.code===t}var i=class{filePath;initialize(e){this.filePath=e}async append(e){await this.withLock(async()=>n.default.appendFile(this.requirePath(),`${JSON.stringify(e)}\n`,`utf8`))}async list(){return t.foldEntries(t.confirmedChanges(await this.events()))}async versions(e){return t.foldVersions(t.confirmedChanges(await this.events()),e)}async clear(){try{await n.default.unlink(this.requirePath())}catch(e){if(!r(e,`ENOENT`))throw e}}async events(){let e;try{e=await n.default.readFile(this.requirePath(),`utf8`)}catch(e){if(r(e,`ENOENT`))return[];throw e}return t.parseTimeline(e,e=>console.warn(`Ignoring malformed file edit timeline line: ${e.slice(0,200)}`))}requirePath(){if(!this.filePath)throw Error(`Timeline store is not initialized`);return this.filePath}async withLock(e){let t=`${this.requirePath()}.lock`,n=await this.acquire(t);try{return await e()}finally{await n.close(),await this.release(t)}}async acquire(e){for(let t=0;t<400;t+=1)try{return await n.default.open(e,`wx`)}catch(t){if(!r(t,`EEXIST`))throw t;if(await this.breakStale(e))continue;await new Promise(e=>setTimeout(e,25))}throw Error(`Timed out acquiring timeline lock ${e}`)}async breakStale(e){try{let t=await n.default.stat(e);return Date.now()-t.mtimeMs<3e4?!1:(await n.default.unlink(e),!0)}catch(e){if(r(e,`ENOENT`))return!0;throw e}}async release(e){try{await n.default.unlink(e)}catch(e){r(e,`ENOENT`)||console.warn(`Could not remove timeline lock: ${String(e)}`)}}};exports.TimelineStore=i;
2
2
  //# sourceMappingURL=TimelineStore.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"TimelineStore.cjs","names":["fs","foldEntries","confirmedChanges","foldVersions","parseTimeline"],"sources":["../../../src/adapters/TimelineStore/TimelineStore.ts"],"sourcesContent":["import fs, { type FileHandle } from 'node:fs/promises';\nimport {\n type AnyTimelineEvent,\n confirmedChanges,\n foldEntries,\n foldVersions,\n parseTimeline,\n} from '../../services/fileChanges.ts';\nimport type { FileEditEntry, FileEditVersion, TimelineEvent } from '../../types/domain';\nimport type { ITimelineStore } from '../../types/timelineStore';\n\nconst LOCK_RETRY_MS = 25;\nconst LOCK_RETRIES = 400;\n/**\n * How long a lock file may sit untouched before a waiter treats it as abandoned.\n *\n * An append writes one line, so a holder that has been there for half a minute\n * is a process that died without unlinking. Without this, that one file would\n * block every later append to the same timeline permanently.\n */\nconst LOCK_STALE_MS = 30_000;\n\nfunction hasCode(error: unknown, code: string): boolean {\n return error instanceof Error && 'code' in error && error.code === code;\n}\n\nexport class TimelineStore implements ITimelineStore {\n private filePath: string | undefined;\n\n initialize(filePath: string): void {\n this.filePath = filePath;\n }\n\n async append(event: TimelineEvent): Promise<void> {\n await this.withLock(async () => fs.appendFile(this.requirePath(), `${JSON.stringify(event)}\\n`, 'utf8'));\n }\n\n async list(): Promise<FileEditEntry[]> {\n // Listing shows what was edited, so a path a command only touched is left out.\n return foldEntries(confirmedChanges(await this.events()));\n }\n\n async versions(filePath: string): Promise<FileEditVersion[]> {\n return foldVersions(await this.events(), filePath);\n }\n\n async clear(): Promise<void> {\n try {\n await fs.unlink(this.requirePath());\n } catch (error) {\n if (!hasCode(error, 'ENOENT')) throw error;\n }\n }\n\n /** Every recorded change, or none at all when the session has not written yet. */\n private async events(): Promise<AnyTimelineEvent[]> {\n let content: string;\n try {\n content = await fs.readFile(this.requirePath(), 'utf8');\n } catch (error) {\n if (hasCode(error, 'ENOENT')) return [];\n throw error;\n }\n return parseTimeline(content, (line) =>\n console.warn(`Ignoring malformed file edit timeline line: ${line.slice(0, 200)}`),\n );\n }\n\n private requirePath(): string {\n if (!this.filePath) throw new Error('Timeline store is not initialized');\n return this.filePath;\n }\n\n private async withLock<T>(operation: () => Promise<T>): Promise<T> {\n const lockPath = `${this.requirePath()}.lock`;\n const lock = await this.acquire(lockPath);\n try {\n return await operation();\n } finally {\n await lock.close();\n await this.release(lockPath);\n }\n }\n\n /** Waits for the lock file, breaking one an earlier process left behind. */\n private async acquire(lockPath: string): Promise<FileHandle> {\n for (let attempt = 0; attempt < LOCK_RETRIES; attempt += 1) {\n try {\n return await fs.open(lockPath, 'wx');\n } catch (error) {\n if (!hasCode(error, 'EEXIST')) throw error;\n if (await this.breakStale(lockPath)) continue;\n await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_MS));\n }\n }\n throw new Error(`Timed out acquiring timeline lock ${lockPath}`);\n }\n\n /** Whether the lock is now free to take, having removed it when its holder is gone. */\n private async breakStale(lockPath: string): Promise<boolean> {\n try {\n const stats = await fs.stat(lockPath);\n if (Date.now() - stats.mtimeMs < LOCK_STALE_MS) return false;\n await fs.unlink(lockPath);\n return true;\n } catch (error) {\n // Already gone, or another waiter broke it first: either way, try again.\n if (hasCode(error, 'ENOENT')) return true;\n throw error;\n }\n }\n\n private async release(lockPath: string): Promise<void> {\n try {\n await fs.unlink(lockPath);\n } catch (error) {\n if (!hasCode(error, 'ENOENT')) console.warn(`Could not remove timeline lock: ${String(error)}`);\n }\n }\n}\n"],"mappings":"uJAsBA,SAAS,EAAQ,EAAgB,EAAuB,CACtD,OAAO,aAAiB,OAAS,SAAU,GAAS,EAAM,OAAS,CACrE,CAEA,IAAa,EAAb,KAAqD,CACnD,SAEA,WAAW,EAAwB,CACjC,KAAK,SAAW,CAClB,CAEA,MAAM,OAAO,EAAqC,CAChD,MAAM,KAAK,SAAS,SAAYA,EAAAA,QAAG,WAAW,KAAK,YAAY,EAAG,GAAG,KAAK,UAAU,CAAK,EAAE,IAAK,MAAM,CAAC,CACzG,CAEA,MAAM,MAAiC,CAErC,OAAOC,EAAAA,YAAYC,EAAAA,iBAAiB,MAAM,KAAK,OAAO,CAAC,CAAC,CAC1D,CAEA,MAAM,SAAS,EAA8C,CAC3D,OAAOC,EAAAA,aAAa,MAAM,KAAK,OAAO,EAAG,CAAQ,CACnD,CAEA,MAAM,OAAuB,CAC3B,GAAI,CACF,MAAMH,EAAAA,QAAG,OAAO,KAAK,YAAY,CAAC,CACpC,OAAS,EAAO,CACd,GAAI,CAAC,EAAQ,EAAO,QAAQ,EAAG,MAAM,CACvC,CACF,CAGA,MAAc,QAAsC,CAClD,IAAI,EACJ,GAAI,CACF,EAAU,MAAMA,EAAAA,QAAG,SAAS,KAAK,YAAY,EAAG,MAAM,CACxD,OAAS,EAAO,CACd,GAAI,EAAQ,EAAO,QAAQ,EAAG,MAAO,CAAC,EACtC,MAAM,CACR,CACA,OAAOI,EAAAA,cAAc,EAAU,GAC7B,QAAQ,KAAK,+CAA+C,EAAK,MAAM,EAAG,GAAG,GAAG,CAClF,CACF,CAEA,aAA8B,CAC5B,GAAI,CAAC,KAAK,SAAU,MAAU,MAAM,mCAAmC,EACvE,OAAO,KAAK,QACd,CAEA,MAAc,SAAY,EAAyC,CACjE,IAAM,EAAW,GAAG,KAAK,YAAY,EAAE,OACjC,EAAO,MAAM,KAAK,QAAQ,CAAQ,EACxC,GAAI,CACF,OAAO,MAAM,EAAU,CACzB,QAAU,CACR,MAAM,EAAK,MAAM,EACjB,MAAM,KAAK,QAAQ,CAAQ,CAC7B,CACF,CAGA,MAAc,QAAQ,EAAuC,CAC3D,IAAK,IAAI,EAAU,EAAG,EAAU,IAAc,GAAW,EACvD,GAAI,CACF,OAAO,MAAMJ,EAAAA,QAAG,KAAK,EAAU,IAAI,CACrC,OAAS,EAAO,CACd,GAAI,CAAC,EAAQ,EAAO,QAAQ,EAAG,MAAM,EACrC,GAAI,MAAM,KAAK,WAAW,CAAQ,EAAG,SACrC,MAAM,IAAI,QAAS,GAAY,WAAW,EAAS,EAAa,CAAC,CACnE,CAEF,MAAU,MAAM,qCAAqC,GAAU,CACjE,CAGA,MAAc,WAAW,EAAoC,CAC3D,GAAI,CACF,IAAM,EAAQ,MAAMA,EAAAA,QAAG,KAAK,CAAQ,EAGpC,OAFI,KAAK,IAAI,EAAI,EAAM,QAAU,IAAsB,IACvD,MAAMA,EAAAA,QAAG,OAAO,CAAQ,EACjB,GACT,OAAS,EAAO,CAEd,GAAI,EAAQ,EAAO,QAAQ,EAAG,MAAO,GACrC,MAAM,CACR,CACF,CAEA,MAAc,QAAQ,EAAiC,CACrD,GAAI,CACF,MAAMA,EAAAA,QAAG,OAAO,CAAQ,CAC1B,OAAS,EAAO,CACT,EAAQ,EAAO,QAAQ,GAAG,QAAQ,KAAK,mCAAmC,OAAO,CAAK,GAAG,CAChG,CACF,CACF"}
1
+ {"version":3,"file":"TimelineStore.cjs","names":["fs","foldEntries","confirmedChanges","foldVersions","parseTimeline"],"sources":["../../../src/adapters/TimelineStore/TimelineStore.ts"],"sourcesContent":["import fs, { type FileHandle } from 'node:fs/promises';\nimport {\n type AnyTimelineEvent,\n confirmedChanges,\n foldEntries,\n foldVersions,\n parseTimeline,\n} from '../../services/fileChanges.ts';\nimport type { FileEditEntry, FileEditVersion, TimelineEvent } from '../../types/domain';\nimport type { ITimelineStore } from '../../types/timelineStore';\n\nconst LOCK_RETRY_MS = 25;\nconst LOCK_RETRIES = 400;\n/**\n * How long a lock file may sit untouched before a waiter treats it as abandoned.\n *\n * An append writes one line, so a holder that has been there for half a minute\n * is a process that died without unlinking. Without this, that one file would\n * block every later append to the same timeline permanently.\n */\nconst LOCK_STALE_MS = 30_000;\n\nfunction hasCode(error: unknown, code: string): boolean {\n return error instanceof Error && 'code' in error && error.code === code;\n}\n\nexport class TimelineStore implements ITimelineStore {\n private filePath: string | undefined;\n\n initialize(filePath: string): void {\n this.filePath = filePath;\n }\n\n async append(event: TimelineEvent): Promise<void> {\n await this.withLock(async () => fs.appendFile(this.requirePath(), `${JSON.stringify(event)}\\n`, 'utf8'));\n }\n\n async list(): Promise<FileEditEntry[]> {\n // Listing shows what was edited, so a path a command only touched is left out.\n return foldEntries(confirmedChanges(await this.events()));\n }\n\n async versions(filePath: string): Promise<FileEditVersion[]> {\n // Same bar as the listing above: a file's history must not reintroduce the\n // touches the list already left out.\n return foldVersions(confirmedChanges(await this.events()), filePath);\n }\n\n async clear(): Promise<void> {\n try {\n await fs.unlink(this.requirePath());\n } catch (error) {\n if (!hasCode(error, 'ENOENT')) throw error;\n }\n }\n\n /** Every recorded change, or none at all when the session has not written yet. */\n private async events(): Promise<AnyTimelineEvent[]> {\n let content: string;\n try {\n content = await fs.readFile(this.requirePath(), 'utf8');\n } catch (error) {\n if (hasCode(error, 'ENOENT')) return [];\n throw error;\n }\n return parseTimeline(content, (line) =>\n console.warn(`Ignoring malformed file edit timeline line: ${line.slice(0, 200)}`),\n );\n }\n\n private requirePath(): string {\n if (!this.filePath) throw new Error('Timeline store is not initialized');\n return this.filePath;\n }\n\n private async withLock<T>(operation: () => Promise<T>): Promise<T> {\n const lockPath = `${this.requirePath()}.lock`;\n const lock = await this.acquire(lockPath);\n try {\n return await operation();\n } finally {\n await lock.close();\n await this.release(lockPath);\n }\n }\n\n /** Waits for the lock file, breaking one an earlier process left behind. */\n private async acquire(lockPath: string): Promise<FileHandle> {\n for (let attempt = 0; attempt < LOCK_RETRIES; attempt += 1) {\n try {\n return await fs.open(lockPath, 'wx');\n } catch (error) {\n if (!hasCode(error, 'EEXIST')) throw error;\n if (await this.breakStale(lockPath)) continue;\n await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_MS));\n }\n }\n throw new Error(`Timed out acquiring timeline lock ${lockPath}`);\n }\n\n /** Whether the lock is now free to take, having removed it when its holder is gone. */\n private async breakStale(lockPath: string): Promise<boolean> {\n try {\n const stats = await fs.stat(lockPath);\n if (Date.now() - stats.mtimeMs < LOCK_STALE_MS) return false;\n await fs.unlink(lockPath);\n return true;\n } catch (error) {\n // Already gone, or another waiter broke it first: either way, try again.\n if (hasCode(error, 'ENOENT')) return true;\n throw error;\n }\n }\n\n private async release(lockPath: string): Promise<void> {\n try {\n await fs.unlink(lockPath);\n } catch (error) {\n if (!hasCode(error, 'ENOENT')) console.warn(`Could not remove timeline lock: ${String(error)}`);\n }\n }\n}\n"],"mappings":"uJAsBA,SAAS,EAAQ,EAAgB,EAAuB,CACtD,OAAO,aAAiB,OAAS,SAAU,GAAS,EAAM,OAAS,CACrE,CAEA,IAAa,EAAb,KAAqD,CACnD,SAEA,WAAW,EAAwB,CACjC,KAAK,SAAW,CAClB,CAEA,MAAM,OAAO,EAAqC,CAChD,MAAM,KAAK,SAAS,SAAYA,EAAAA,QAAG,WAAW,KAAK,YAAY,EAAG,GAAG,KAAK,UAAU,CAAK,EAAE,IAAK,MAAM,CAAC,CACzG,CAEA,MAAM,MAAiC,CAErC,OAAOC,EAAAA,YAAYC,EAAAA,iBAAiB,MAAM,KAAK,OAAO,CAAC,CAAC,CAC1D,CAEA,MAAM,SAAS,EAA8C,CAG3D,OAAOC,EAAAA,aAAaD,EAAAA,iBAAiB,MAAM,KAAK,OAAO,CAAC,EAAG,CAAQ,CACrE,CAEA,MAAM,OAAuB,CAC3B,GAAI,CACF,MAAMF,EAAAA,QAAG,OAAO,KAAK,YAAY,CAAC,CACpC,OAAS,EAAO,CACd,GAAI,CAAC,EAAQ,EAAO,QAAQ,EAAG,MAAM,CACvC,CACF,CAGA,MAAc,QAAsC,CAClD,IAAI,EACJ,GAAI,CACF,EAAU,MAAMA,EAAAA,QAAG,SAAS,KAAK,YAAY,EAAG,MAAM,CACxD,OAAS,EAAO,CACd,GAAI,EAAQ,EAAO,QAAQ,EAAG,MAAO,CAAC,EACtC,MAAM,CACR,CACA,OAAOI,EAAAA,cAAc,EAAU,GAC7B,QAAQ,KAAK,+CAA+C,EAAK,MAAM,EAAG,GAAG,GAAG,CAClF,CACF,CAEA,aAA8B,CAC5B,GAAI,CAAC,KAAK,SAAU,MAAU,MAAM,mCAAmC,EACvE,OAAO,KAAK,QACd,CAEA,MAAc,SAAY,EAAyC,CACjE,IAAM,EAAW,GAAG,KAAK,YAAY,EAAE,OACjC,EAAO,MAAM,KAAK,QAAQ,CAAQ,EACxC,GAAI,CACF,OAAO,MAAM,EAAU,CACzB,QAAU,CACR,MAAM,EAAK,MAAM,EACjB,MAAM,KAAK,QAAQ,CAAQ,CAC7B,CACF,CAGA,MAAc,QAAQ,EAAuC,CAC3D,IAAK,IAAI,EAAU,EAAG,EAAU,IAAc,GAAW,EACvD,GAAI,CACF,OAAO,MAAMJ,EAAAA,QAAG,KAAK,EAAU,IAAI,CACrC,OAAS,EAAO,CACd,GAAI,CAAC,EAAQ,EAAO,QAAQ,EAAG,MAAM,EACrC,GAAI,MAAM,KAAK,WAAW,CAAQ,EAAG,SACrC,MAAM,IAAI,QAAS,GAAY,WAAW,EAAS,EAAa,CAAC,CACnE,CAEF,MAAU,MAAM,qCAAqC,GAAU,CACjE,CAGA,MAAc,WAAW,EAAoC,CAC3D,GAAI,CACF,IAAM,EAAQ,MAAMA,EAAAA,QAAG,KAAK,CAAQ,EAGpC,OAFI,KAAK,IAAI,EAAI,EAAM,QAAU,IAAsB,IACvD,MAAMA,EAAAA,QAAG,OAAO,CAAQ,EACjB,GACT,OAAS,EAAO,CAEd,GAAI,EAAQ,EAAO,QAAQ,EAAG,MAAO,GACrC,MAAM,CACR,CACF,CAEA,MAAc,QAAQ,EAAiC,CACrD,GAAI,CACF,MAAMA,EAAAA,QAAG,OAAO,CAAQ,CAC1B,OAAS,EAAO,CACT,EAAQ,EAAO,QAAQ,GAAG,QAAQ,KAAK,mCAAmC,OAAO,CAAK,GAAG,CAChG,CACF,CACF"}
@@ -1,2 +1,2 @@
1
- import{confirmedChanges as e,foldEntries as t,foldVersions as n,parseTimeline as r}from"../../services/fileChanges.mjs";import i from"node:fs/promises";function a(e,t){return e instanceof Error&&`code`in e&&e.code===t}var o=class{filePath;initialize(e){this.filePath=e}async append(e){await this.withLock(async()=>i.appendFile(this.requirePath(),`${JSON.stringify(e)}\n`,`utf8`))}async list(){return t(e(await this.events()))}async versions(e){return n(await this.events(),e)}async clear(){try{await i.unlink(this.requirePath())}catch(e){if(!a(e,`ENOENT`))throw e}}async events(){let e;try{e=await i.readFile(this.requirePath(),`utf8`)}catch(e){if(a(e,`ENOENT`))return[];throw e}return r(e,e=>console.warn(`Ignoring malformed file edit timeline line: ${e.slice(0,200)}`))}requirePath(){if(!this.filePath)throw Error(`Timeline store is not initialized`);return this.filePath}async withLock(e){let t=`${this.requirePath()}.lock`,n=await this.acquire(t);try{return await e()}finally{await n.close(),await this.release(t)}}async acquire(e){for(let t=0;t<400;t+=1)try{return await i.open(e,`wx`)}catch(t){if(!a(t,`EEXIST`))throw t;if(await this.breakStale(e))continue;await new Promise(e=>setTimeout(e,25))}throw Error(`Timed out acquiring timeline lock ${e}`)}async breakStale(e){try{let t=await i.stat(e);return Date.now()-t.mtimeMs<3e4?!1:(await i.unlink(e),!0)}catch(e){if(a(e,`ENOENT`))return!0;throw e}}async release(e){try{await i.unlink(e)}catch(e){a(e,`ENOENT`)||console.warn(`Could not remove timeline lock: ${String(e)}`)}}};export{o as TimelineStore};
1
+ import{confirmedChanges as e,foldEntries as t,foldVersions as n,parseTimeline as r}from"../../services/fileChanges.mjs";import i from"node:fs/promises";function a(e,t){return e instanceof Error&&`code`in e&&e.code===t}var o=class{filePath;initialize(e){this.filePath=e}async append(e){await this.withLock(async()=>i.appendFile(this.requirePath(),`${JSON.stringify(e)}\n`,`utf8`))}async list(){return t(e(await this.events()))}async versions(t){return n(e(await this.events()),t)}async clear(){try{await i.unlink(this.requirePath())}catch(e){if(!a(e,`ENOENT`))throw e}}async events(){let e;try{e=await i.readFile(this.requirePath(),`utf8`)}catch(e){if(a(e,`ENOENT`))return[];throw e}return r(e,e=>console.warn(`Ignoring malformed file edit timeline line: ${e.slice(0,200)}`))}requirePath(){if(!this.filePath)throw Error(`Timeline store is not initialized`);return this.filePath}async withLock(e){let t=`${this.requirePath()}.lock`,n=await this.acquire(t);try{return await e()}finally{await n.close(),await this.release(t)}}async acquire(e){for(let t=0;t<400;t+=1)try{return await i.open(e,`wx`)}catch(t){if(!a(t,`EEXIST`))throw t;if(await this.breakStale(e))continue;await new Promise(e=>setTimeout(e,25))}throw Error(`Timed out acquiring timeline lock ${e}`)}async breakStale(e){try{let t=await i.stat(e);return Date.now()-t.mtimeMs<3e4?!1:(await i.unlink(e),!0)}catch(e){if(a(e,`ENOENT`))return!0;throw e}}async release(e){try{await i.unlink(e)}catch(e){a(e,`ENOENT`)||console.warn(`Could not remove timeline lock: ${String(e)}`)}}};export{o as TimelineStore};
2
2
  //# sourceMappingURL=TimelineStore.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"TimelineStore.mjs","names":[],"sources":["../../../src/adapters/TimelineStore/TimelineStore.ts"],"sourcesContent":["import fs, { type FileHandle } from 'node:fs/promises';\nimport {\n type AnyTimelineEvent,\n confirmedChanges,\n foldEntries,\n foldVersions,\n parseTimeline,\n} from '../../services/fileChanges.ts';\nimport type { FileEditEntry, FileEditVersion, TimelineEvent } from '../../types/domain';\nimport type { ITimelineStore } from '../../types/timelineStore';\n\nconst LOCK_RETRY_MS = 25;\nconst LOCK_RETRIES = 400;\n/**\n * How long a lock file may sit untouched before a waiter treats it as abandoned.\n *\n * An append writes one line, so a holder that has been there for half a minute\n * is a process that died without unlinking. Without this, that one file would\n * block every later append to the same timeline permanently.\n */\nconst LOCK_STALE_MS = 30_000;\n\nfunction hasCode(error: unknown, code: string): boolean {\n return error instanceof Error && 'code' in error && error.code === code;\n}\n\nexport class TimelineStore implements ITimelineStore {\n private filePath: string | undefined;\n\n initialize(filePath: string): void {\n this.filePath = filePath;\n }\n\n async append(event: TimelineEvent): Promise<void> {\n await this.withLock(async () => fs.appendFile(this.requirePath(), `${JSON.stringify(event)}\\n`, 'utf8'));\n }\n\n async list(): Promise<FileEditEntry[]> {\n // Listing shows what was edited, so a path a command only touched is left out.\n return foldEntries(confirmedChanges(await this.events()));\n }\n\n async versions(filePath: string): Promise<FileEditVersion[]> {\n return foldVersions(await this.events(), filePath);\n }\n\n async clear(): Promise<void> {\n try {\n await fs.unlink(this.requirePath());\n } catch (error) {\n if (!hasCode(error, 'ENOENT')) throw error;\n }\n }\n\n /** Every recorded change, or none at all when the session has not written yet. */\n private async events(): Promise<AnyTimelineEvent[]> {\n let content: string;\n try {\n content = await fs.readFile(this.requirePath(), 'utf8');\n } catch (error) {\n if (hasCode(error, 'ENOENT')) return [];\n throw error;\n }\n return parseTimeline(content, (line) =>\n console.warn(`Ignoring malformed file edit timeline line: ${line.slice(0, 200)}`),\n );\n }\n\n private requirePath(): string {\n if (!this.filePath) throw new Error('Timeline store is not initialized');\n return this.filePath;\n }\n\n private async withLock<T>(operation: () => Promise<T>): Promise<T> {\n const lockPath = `${this.requirePath()}.lock`;\n const lock = await this.acquire(lockPath);\n try {\n return await operation();\n } finally {\n await lock.close();\n await this.release(lockPath);\n }\n }\n\n /** Waits for the lock file, breaking one an earlier process left behind. */\n private async acquire(lockPath: string): Promise<FileHandle> {\n for (let attempt = 0; attempt < LOCK_RETRIES; attempt += 1) {\n try {\n return await fs.open(lockPath, 'wx');\n } catch (error) {\n if (!hasCode(error, 'EEXIST')) throw error;\n if (await this.breakStale(lockPath)) continue;\n await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_MS));\n }\n }\n throw new Error(`Timed out acquiring timeline lock ${lockPath}`);\n }\n\n /** Whether the lock is now free to take, having removed it when its holder is gone. */\n private async breakStale(lockPath: string): Promise<boolean> {\n try {\n const stats = await fs.stat(lockPath);\n if (Date.now() - stats.mtimeMs < LOCK_STALE_MS) return false;\n await fs.unlink(lockPath);\n return true;\n } catch (error) {\n // Already gone, or another waiter broke it first: either way, try again.\n if (hasCode(error, 'ENOENT')) return true;\n throw error;\n }\n }\n\n private async release(lockPath: string): Promise<void> {\n try {\n await fs.unlink(lockPath);\n } catch (error) {\n if (!hasCode(error, 'ENOENT')) console.warn(`Could not remove timeline lock: ${String(error)}`);\n }\n }\n}\n"],"mappings":"wJAsBA,SAAS,EAAQ,EAAgB,EAAuB,CACtD,OAAO,aAAiB,OAAS,SAAU,GAAS,EAAM,OAAS,CACrE,CAEA,IAAa,EAAb,KAAqD,CACnD,SAEA,WAAW,EAAwB,CACjC,KAAK,SAAW,CAClB,CAEA,MAAM,OAAO,EAAqC,CAChD,MAAM,KAAK,SAAS,SAAY,EAAG,WAAW,KAAK,YAAY,EAAG,GAAG,KAAK,UAAU,CAAK,EAAE,IAAK,MAAM,CAAC,CACzG,CAEA,MAAM,MAAiC,CAErC,OAAO,EAAY,EAAiB,MAAM,KAAK,OAAO,CAAC,CAAC,CAC1D,CAEA,MAAM,SAAS,EAA8C,CAC3D,OAAO,EAAa,MAAM,KAAK,OAAO,EAAG,CAAQ,CACnD,CAEA,MAAM,OAAuB,CAC3B,GAAI,CACF,MAAM,EAAG,OAAO,KAAK,YAAY,CAAC,CACpC,OAAS,EAAO,CACd,GAAI,CAAC,EAAQ,EAAO,QAAQ,EAAG,MAAM,CACvC,CACF,CAGA,MAAc,QAAsC,CAClD,IAAI,EACJ,GAAI,CACF,EAAU,MAAM,EAAG,SAAS,KAAK,YAAY,EAAG,MAAM,CACxD,OAAS,EAAO,CACd,GAAI,EAAQ,EAAO,QAAQ,EAAG,MAAO,CAAC,EACtC,MAAM,CACR,CACA,OAAO,EAAc,EAAU,GAC7B,QAAQ,KAAK,+CAA+C,EAAK,MAAM,EAAG,GAAG,GAAG,CAClF,CACF,CAEA,aAA8B,CAC5B,GAAI,CAAC,KAAK,SAAU,MAAU,MAAM,mCAAmC,EACvE,OAAO,KAAK,QACd,CAEA,MAAc,SAAY,EAAyC,CACjE,IAAM,EAAW,GAAG,KAAK,YAAY,EAAE,OACjC,EAAO,MAAM,KAAK,QAAQ,CAAQ,EACxC,GAAI,CACF,OAAO,MAAM,EAAU,CACzB,QAAU,CACR,MAAM,EAAK,MAAM,EACjB,MAAM,KAAK,QAAQ,CAAQ,CAC7B,CACF,CAGA,MAAc,QAAQ,EAAuC,CAC3D,IAAK,IAAI,EAAU,EAAG,EAAU,IAAc,GAAW,EACvD,GAAI,CACF,OAAO,MAAM,EAAG,KAAK,EAAU,IAAI,CACrC,OAAS,EAAO,CACd,GAAI,CAAC,EAAQ,EAAO,QAAQ,EAAG,MAAM,EACrC,GAAI,MAAM,KAAK,WAAW,CAAQ,EAAG,SACrC,MAAM,IAAI,QAAS,GAAY,WAAW,EAAS,EAAa,CAAC,CACnE,CAEF,MAAU,MAAM,qCAAqC,GAAU,CACjE,CAGA,MAAc,WAAW,EAAoC,CAC3D,GAAI,CACF,IAAM,EAAQ,MAAM,EAAG,KAAK,CAAQ,EAGpC,OAFI,KAAK,IAAI,EAAI,EAAM,QAAU,IAAsB,IACvD,MAAM,EAAG,OAAO,CAAQ,EACjB,GACT,OAAS,EAAO,CAEd,GAAI,EAAQ,EAAO,QAAQ,EAAG,MAAO,GACrC,MAAM,CACR,CACF,CAEA,MAAc,QAAQ,EAAiC,CACrD,GAAI,CACF,MAAM,EAAG,OAAO,CAAQ,CAC1B,OAAS,EAAO,CACT,EAAQ,EAAO,QAAQ,GAAG,QAAQ,KAAK,mCAAmC,OAAO,CAAK,GAAG,CAChG,CACF,CACF"}
1
+ {"version":3,"file":"TimelineStore.mjs","names":[],"sources":["../../../src/adapters/TimelineStore/TimelineStore.ts"],"sourcesContent":["import fs, { type FileHandle } from 'node:fs/promises';\nimport {\n type AnyTimelineEvent,\n confirmedChanges,\n foldEntries,\n foldVersions,\n parseTimeline,\n} from '../../services/fileChanges.ts';\nimport type { FileEditEntry, FileEditVersion, TimelineEvent } from '../../types/domain';\nimport type { ITimelineStore } from '../../types/timelineStore';\n\nconst LOCK_RETRY_MS = 25;\nconst LOCK_RETRIES = 400;\n/**\n * How long a lock file may sit untouched before a waiter treats it as abandoned.\n *\n * An append writes one line, so a holder that has been there for half a minute\n * is a process that died without unlinking. Without this, that one file would\n * block every later append to the same timeline permanently.\n */\nconst LOCK_STALE_MS = 30_000;\n\nfunction hasCode(error: unknown, code: string): boolean {\n return error instanceof Error && 'code' in error && error.code === code;\n}\n\nexport class TimelineStore implements ITimelineStore {\n private filePath: string | undefined;\n\n initialize(filePath: string): void {\n this.filePath = filePath;\n }\n\n async append(event: TimelineEvent): Promise<void> {\n await this.withLock(async () => fs.appendFile(this.requirePath(), `${JSON.stringify(event)}\\n`, 'utf8'));\n }\n\n async list(): Promise<FileEditEntry[]> {\n // Listing shows what was edited, so a path a command only touched is left out.\n return foldEntries(confirmedChanges(await this.events()));\n }\n\n async versions(filePath: string): Promise<FileEditVersion[]> {\n // Same bar as the listing above: a file's history must not reintroduce the\n // touches the list already left out.\n return foldVersions(confirmedChanges(await this.events()), filePath);\n }\n\n async clear(): Promise<void> {\n try {\n await fs.unlink(this.requirePath());\n } catch (error) {\n if (!hasCode(error, 'ENOENT')) throw error;\n }\n }\n\n /** Every recorded change, or none at all when the session has not written yet. */\n private async events(): Promise<AnyTimelineEvent[]> {\n let content: string;\n try {\n content = await fs.readFile(this.requirePath(), 'utf8');\n } catch (error) {\n if (hasCode(error, 'ENOENT')) return [];\n throw error;\n }\n return parseTimeline(content, (line) =>\n console.warn(`Ignoring malformed file edit timeline line: ${line.slice(0, 200)}`),\n );\n }\n\n private requirePath(): string {\n if (!this.filePath) throw new Error('Timeline store is not initialized');\n return this.filePath;\n }\n\n private async withLock<T>(operation: () => Promise<T>): Promise<T> {\n const lockPath = `${this.requirePath()}.lock`;\n const lock = await this.acquire(lockPath);\n try {\n return await operation();\n } finally {\n await lock.close();\n await this.release(lockPath);\n }\n }\n\n /** Waits for the lock file, breaking one an earlier process left behind. */\n private async acquire(lockPath: string): Promise<FileHandle> {\n for (let attempt = 0; attempt < LOCK_RETRIES; attempt += 1) {\n try {\n return await fs.open(lockPath, 'wx');\n } catch (error) {\n if (!hasCode(error, 'EEXIST')) throw error;\n if (await this.breakStale(lockPath)) continue;\n await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_MS));\n }\n }\n throw new Error(`Timed out acquiring timeline lock ${lockPath}`);\n }\n\n /** Whether the lock is now free to take, having removed it when its holder is gone. */\n private async breakStale(lockPath: string): Promise<boolean> {\n try {\n const stats = await fs.stat(lockPath);\n if (Date.now() - stats.mtimeMs < LOCK_STALE_MS) return false;\n await fs.unlink(lockPath);\n return true;\n } catch (error) {\n // Already gone, or another waiter broke it first: either way, try again.\n if (hasCode(error, 'ENOENT')) return true;\n throw error;\n }\n }\n\n private async release(lockPath: string): Promise<void> {\n try {\n await fs.unlink(lockPath);\n } catch (error) {\n if (!hasCode(error, 'ENOENT')) console.warn(`Could not remove timeline lock: ${String(error)}`);\n }\n }\n}\n"],"mappings":"wJAsBA,SAAS,EAAQ,EAAgB,EAAuB,CACtD,OAAO,aAAiB,OAAS,SAAU,GAAS,EAAM,OAAS,CACrE,CAEA,IAAa,EAAb,KAAqD,CACnD,SAEA,WAAW,EAAwB,CACjC,KAAK,SAAW,CAClB,CAEA,MAAM,OAAO,EAAqC,CAChD,MAAM,KAAK,SAAS,SAAY,EAAG,WAAW,KAAK,YAAY,EAAG,GAAG,KAAK,UAAU,CAAK,EAAE,IAAK,MAAM,CAAC,CACzG,CAEA,MAAM,MAAiC,CAErC,OAAO,EAAY,EAAiB,MAAM,KAAK,OAAO,CAAC,CAAC,CAC1D,CAEA,MAAM,SAAS,EAA8C,CAG3D,OAAO,EAAa,EAAiB,MAAM,KAAK,OAAO,CAAC,EAAG,CAAQ,CACrE,CAEA,MAAM,OAAuB,CAC3B,GAAI,CACF,MAAM,EAAG,OAAO,KAAK,YAAY,CAAC,CACpC,OAAS,EAAO,CACd,GAAI,CAAC,EAAQ,EAAO,QAAQ,EAAG,MAAM,CACvC,CACF,CAGA,MAAc,QAAsC,CAClD,IAAI,EACJ,GAAI,CACF,EAAU,MAAM,EAAG,SAAS,KAAK,YAAY,EAAG,MAAM,CACxD,OAAS,EAAO,CACd,GAAI,EAAQ,EAAO,QAAQ,EAAG,MAAO,CAAC,EACtC,MAAM,CACR,CACA,OAAO,EAAc,EAAU,GAC7B,QAAQ,KAAK,+CAA+C,EAAK,MAAM,EAAG,GAAG,GAAG,CAClF,CACF,CAEA,aAA8B,CAC5B,GAAI,CAAC,KAAK,SAAU,MAAU,MAAM,mCAAmC,EACvE,OAAO,KAAK,QACd,CAEA,MAAc,SAAY,EAAyC,CACjE,IAAM,EAAW,GAAG,KAAK,YAAY,EAAE,OACjC,EAAO,MAAM,KAAK,QAAQ,CAAQ,EACxC,GAAI,CACF,OAAO,MAAM,EAAU,CACzB,QAAU,CACR,MAAM,EAAK,MAAM,EACjB,MAAM,KAAK,QAAQ,CAAQ,CAC7B,CACF,CAGA,MAAc,QAAQ,EAAuC,CAC3D,IAAK,IAAI,EAAU,EAAG,EAAU,IAAc,GAAW,EACvD,GAAI,CACF,OAAO,MAAM,EAAG,KAAK,EAAU,IAAI,CACrC,OAAS,EAAO,CACd,GAAI,CAAC,EAAQ,EAAO,QAAQ,EAAG,MAAM,EACrC,GAAI,MAAM,KAAK,WAAW,CAAQ,EAAG,SACrC,MAAM,IAAI,QAAS,GAAY,WAAW,EAAS,EAAa,CAAC,CACnE,CAEF,MAAU,MAAM,qCAAqC,GAAU,CACjE,CAGA,MAAc,WAAW,EAAoC,CAC3D,GAAI,CACF,IAAM,EAAQ,MAAM,EAAG,KAAK,CAAQ,EAGpC,OAFI,KAAK,IAAI,EAAI,EAAM,QAAU,IAAsB,IACvD,MAAM,EAAG,OAAO,CAAQ,EACjB,GACT,OAAS,EAAO,CAEd,GAAI,EAAQ,EAAO,QAAQ,EAAG,MAAO,GACrC,MAAM,CACR,CACF,CAEA,MAAc,QAAQ,EAAiC,CACrD,GAAI,CACF,MAAM,EAAG,OAAO,CAAQ,CAC1B,OAAS,EAAO,CACT,EAAQ,EAAO,QAAQ,GAAG,QAAQ,KAAK,mCAAmC,OAAO,CAAK,GAAG,CAChG,CACF,CACF"}
@@ -1,2 +1,2 @@
1
- const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("../services/lineDiff.cjs"),n=require("./FileEditPaths/FileEditPaths.cjs"),r=require("./node/snapshotStore.cjs"),i=require("../services/fileChanges.cjs"),a=require("./TimelineStore/TimelineStore.cjs"),o=require("../types/fileEditsApi.cjs");let s=require("node:fs/promises");s=e.__toESM(s,1);let c=require("node:path");c=e.__toESM(c,1);let l=require("node:crypto"),u=require("hono");const d=1048576,f=`changed by a command, so no baseline was captured; showing the file as it stands`,p=`the changed region was too large to match line by line`;function m(e){return(0,l.createHash)(`sha256`).update(e,`utf8`).digest(`hex`)}function h(e,t){return e instanceof Error&&`code`in e&&e.code===t}function g(e){return e instanceof Error?e.message:String(e)}async function _(e){let t;try{t=await s.default.readFile(e)}catch{return{content:``,hash:``,unavailable:!0,reason:`the file no longer exists`}}if(t.byteLength>1048576)return{content:``,hash:``,unavailable:!0,reason:`the file is larger than ${d} bytes`};if(t.subarray(0,8192).includes(0))return{content:``,hash:``,unavailable:!0,reason:`the file is binary`};let n=t.toString(`utf8`);return{content:n,hash:m(n),unavailable:!1}}function v(e,t){return t===e||t.startsWith(`${e}${c.default.sep}`)}async function y(e,t){let n,r;try{n=await s.default.realpath(e),r=await s.default.realpath(t)}catch{return!0}return v(n,r)}async function b(e,n){let r={index:e.index,tool:e.tool,at:e.at,origin:e.origin,additions:e.additions??0,removals:e.removals??0};if(e.before===void 0||e.after===void 0)return e.origin===`tool`?{...r,note:`this change could not be captured: the file is binary or past the size cap`}:{...r,note:e.tool===`bash`?f:`recorded before this session began capturing content, so there is no diff for it`};let[i,a]=await Promise.all([n.read(e.before),n.read(e.after)]);if(i===void 0||a===void 0)return{...r,note:`the content behind this change is no longer held`};let o=t.lineDiff(i,a);return{...r,additions:o.additions,removals:o.removals,hunks:o.hunks,...o.approximate?{note:p}:{}}}async function x(e,n,r){let a=i.baselineOf(e);if(a===void 0)return{additions:0,removals:0,note:f};if(n.unavailable)return{additions:0,removals:0,note:n.reason??`the file cannot be read`};let o=await r.read(a);if(o===void 0)return{additions:0,removals:0,note:`the content this session started from is no longer held`};let s=t.lineDiff(o,n.content);return{additions:s.additions,removals:s.removals,hunks:s.hunks,...s.approximate?{note:p}:{}}}function S(e={}){let t=e.timeline??new a.TimelineStore,i=e.snapshots??new r.NodeSnapshotStoreAdapter,l=e.paths??new n.FileEditPaths,d=e.cwd??``,f=new u.Hono;if(e.sessionId!==void 0&&d!==``){let n=l.sessionKey(e.sessionId);e.timeline===void 0&&t.initialize(l.timelinePath(d,n)),e.snapshots===void 0&&i.initialize(l.snapshotsPath(d,n))}let p=async e=>{if(e===void 0||e===``)return;let n=await t.versions(c.default.resolve(d,e));return n.length===0?void 0:n};return f.get(`/detail`,async e=>{let t=e.req.query(o.PATH_QUERY_PARAM),n=await p(t);if(n===void 0||t===void 0)return e.json({error:`This session recorded no changes to that file.`},404);let r=c.default.resolve(d,t),a=await _(r),s={path:r,relPath:d===``?r:c.default.relative(d,r),versions:await Promise.all(n.map(e=>b(e,i))),cumulative:await x(n,a,i),working:a};return e.json(s)}),f.get(`/preview`,async e=>{let t=e.req.query(o.PATH_QUERY_PARAM);if(t===void 0||t===``||t.includes(`\0`))return e.json({error:`A preview names a path.`},400);if(d===``)return e.json({error:`This session has no working directory to read from.`},403);let n=c.default.resolve(d),r=c.default.resolve(n,t);if(!v(n,r)||!await y(n,r))return e.json({error:`The path leaves the session directory.`},403);let i={path:r,relPath:c.default.relative(n,r),working:await _(r)};return e.json(i)}),f.put(`/content`,async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:`The save body is not JSON.`},400)}if(typeof t.path!=`string`||typeof t.content!=`string`)return e.json({error:`A save names a path and the content to write.`},400);if(await p(t.path)===void 0)return e.json({error:`This session recorded no changes to that file.`},404);let n=c.default.resolve(d,t.path),r=await _(n);if(r.unavailable)return e.json({error:r.reason??`The file cannot be read.`},409);if(r.hash!==t.expectedHash)return e.json({error:`The file changed since it was opened.`,hash:r.hash},409);await s.default.writeFile(n,t.content,`utf8`);let i={hash:m(t.content)};return e.json(i)}),f.delete(`/content`,async e=>{let t=e.req.query(o.PATH_QUERY_PARAM);if(await p(t)===void 0||t===void 0)return e.json({error:`This session recorded no changes to that file.`},404);let n=c.default.resolve(d,t);try{await s.default.unlink(n)}catch(t){if(!h(t,`ENOENT`))return e.json({error:`The file could not be deleted: ${g(t)}`},409)}return e.body(null,204)}),f}const C={basePath:o.API_BASE_PATH,start(e){let t=S({...e.sessionId===void 0?{}:{sessionId:e.sessionId},...e.cwd===void 0?{}:{cwd:e.cwd}});return{fetch:e=>t.fetch(e),close:()=>void 0}}};exports.MAX_WORKING_BYTES=d,exports.api=C,exports.createFileEditsApi=S;
1
+ const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("../services/lineDiff.cjs"),n=require("./FileEditPaths/FileEditPaths.cjs"),r=require("./node/snapshotStore.cjs"),i=require("../services/fileChanges.cjs"),a=require("./TimelineStore/TimelineStore.cjs"),o=require("../types/fileEditsApi.cjs");let s=require("node:path");s=e.__toESM(s,1);let c=require("node:fs/promises");c=e.__toESM(c,1);let l=require("node:crypto"),u=require("hono");const d=1048576,f=`changed by a command, so no baseline was captured; showing the file as it stands`,p=`the previous content could not be captured: it was binary or past the size cap`,m=`the changed region was too large to match line by line`;function h(e){return(0,l.createHash)(`sha256`).update(e,`utf8`).digest(`hex`)}function g(e,t){return e instanceof Error&&`code`in e&&e.code===t}function _(e){return e instanceof Error?e.message:String(e)}async function v(e){let t;try{t=await c.default.readFile(e)}catch{return{content:``,hash:``,unavailable:!0,reason:`the file no longer exists`}}if(t.byteLength>1048576)return{content:``,hash:``,unavailable:!0,reason:`the file is larger than ${d} bytes`};if(t.subarray(0,8192).includes(0))return{content:``,hash:``,unavailable:!0,reason:`the file is binary`};let n=t.toString(`utf8`);return{content:n,hash:h(n),unavailable:!1}}function y(e,t){return t===e||t.startsWith(`${e}${s.default.sep}`)}async function b(e,t){let n,r;try{n=await c.default.realpath(e),r=await c.default.realpath(t)}catch{return!0}return y(n,r)}async function x(e,n){let r={index:e.index,tool:e.tool,at:e.at,origin:e.origin,additions:e.additions??0,removals:e.removals??0},i=e.before===void 0&&e.created===!0;if(e.after===void 0||e.before===void 0&&!i)return e.origin===`tool`?{...r,note:e.after===void 0?`the new content could not be captured: the file is binary or past the size cap`:p}:{...r,note:e.tool===`bash`?f:`recorded before this session began capturing content, so there is no diff for it`};let a=await n.read(e.after);if(a===void 0)return{...r,note:`the content behind this change is no longer held`};if(e.before===void 0){let e=t.lineDiffFromEmpty(a);return{...r,additions:e.additions,removals:e.removals,hunks:e.hunks,...e.approximate?{note:m}:{}}}let o=await n.read(e.before);if(o===void 0)return{...r,note:`the content behind this change is no longer held`};let s=t.lineDiff(o,a);return{...r,additions:s.additions,removals:s.removals,hunks:s.hunks,...s.approximate?{note:m}:{}}}async function S(e,n,r){let a=e[0]?.created===!0,o=i.baselineOf(e);if(!a&&o===void 0)return{additions:0,removals:0,note:e[0]?.origin===`tool`?p:f};if(n.unavailable)return{additions:0,removals:0,note:n.reason??`the file cannot be read`};if(a||o===void 0){let e=t.lineDiffFromEmpty(n.content);return{additions:e.additions,removals:e.removals,hunks:e.hunks,...e.approximate?{note:m}:{}}}let s=await r.read(o);if(s===void 0)return{additions:0,removals:0,note:`the content this session started from is no longer held`};let c=t.lineDiff(s,n.content);return{additions:c.additions,removals:c.removals,hunks:c.hunks,...c.approximate?{note:m}:{}}}function C(e={}){let t=e.timeline??new a.TimelineStore,i=e.snapshots??new r.NodeSnapshotStoreAdapter,l=e.paths??new n.FileEditPaths,d=e.cwd??``,f=new u.Hono;if(e.sessionId!==void 0&&d!==``){let n=l.sessionKey(e.sessionId);e.timeline===void 0&&t.initialize(l.timelinePath(d,n)),e.snapshots===void 0&&i.initialize(l.snapshotsPath(d,n))}let p=async e=>{if(e===void 0||e===``)return;let n=await t.versions(s.default.resolve(d,e));return n.length===0?void 0:n};return f.get(`/detail`,async e=>{let t=e.req.query(o.PATH_QUERY_PARAM),n=await p(t);if(n===void 0||t===void 0)return e.json({error:`This session recorded no changes to that file.`},404);let r=s.default.resolve(d,t),a=await v(r),c={path:r,relPath:d===``?r:s.default.relative(d,r),versions:await Promise.all(n.map(e=>x(e,i))),cumulative:await S(n,a,i),working:a};return e.json(c)}),f.get(`/preview`,async e=>{let t=e.req.query(o.PATH_QUERY_PARAM);if(t===void 0||t===``||t.includes(`\0`))return e.json({error:`A preview names a path.`},400);if(d===``)return e.json({error:`This session has no working directory to read from.`},403);let n=s.default.resolve(d),r=s.default.resolve(n,t);if(!y(n,r)||!await b(n,r))return e.json({error:`The path leaves the session directory.`},403);let i={path:r,relPath:s.default.relative(n,r),working:await v(r)};return e.json(i)}),f.put(`/content`,async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:`The save body is not JSON.`},400)}if(typeof t.path!=`string`||typeof t.content!=`string`)return e.json({error:`A save names a path and the content to write.`},400);if(await p(t.path)===void 0)return e.json({error:`This session recorded no changes to that file.`},404);let n=s.default.resolve(d,t.path),r=await v(n);if(r.unavailable)return e.json({error:r.reason??`The file cannot be read.`},409);if(r.hash!==t.expectedHash)return e.json({error:`The file changed since it was opened.`,hash:r.hash},409);await c.default.writeFile(n,t.content,`utf8`);let i={hash:h(t.content)};return e.json(i)}),f.delete(`/content`,async e=>{let t=e.req.query(o.PATH_QUERY_PARAM);if(await p(t)===void 0||t===void 0)return e.json({error:`This session recorded no changes to that file.`},404);let n=s.default.resolve(d,t);try{await c.default.unlink(n)}catch(t){if(!g(t,`ENOENT`))return e.json({error:`The file could not be deleted: ${_(t)}`},409)}return e.body(null,204)}),f}const w={basePath:o.API_BASE_PATH,start(e){let t=C({...e.sessionId===void 0?{}:{sessionId:e.sessionId},...e.cwd===void 0?{}:{cwd:e.cwd}});return{fetch:e=>t.fetch(e),close:()=>void 0}}};exports.MAX_WORKING_BYTES=d,exports.api=w,exports.createFileEditsApi=C;
2
2
  //# sourceMappingURL=fileEditsApi.cjs.map