@agimon-ai/doompi-file-edit 0.0.1-alpha.39 → 0.0.1-alpha.40

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 (121) hide show
  1. package/README.md +51 -15
  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/FileEditPaths/FileEditPaths.cjs +1 -1
  7. package/dist/adapters/FileEditPaths/FileEditPaths.cjs.map +1 -1
  8. package/dist/adapters/FileEditPaths/FileEditPaths.mjs +1 -1
  9. package/dist/adapters/FileEditPaths/FileEditPaths.mjs.map +1 -1
  10. package/dist/adapters/TimelineStore/TimelineStore.cjs +1 -2
  11. package/dist/adapters/TimelineStore/TimelineStore.cjs.map +1 -1
  12. package/dist/adapters/TimelineStore/TimelineStore.mjs +1 -2
  13. package/dist/adapters/TimelineStore/TimelineStore.mjs.map +1 -1
  14. package/dist/adapters/fileEditsApi.cjs +2 -0
  15. package/dist/adapters/fileEditsApi.cjs.map +1 -0
  16. package/dist/adapters/fileEditsApi.d.cts +21 -0
  17. package/dist/adapters/fileEditsApi.d.cts.map +1 -0
  18. package/dist/adapters/fileEditsApi.d.mts +21 -0
  19. package/dist/adapters/fileEditsApi.d.mts.map +1 -0
  20. package/dist/adapters/fileEditsApi.mjs +2 -0
  21. package/dist/adapters/fileEditsApi.mjs.map +1 -0
  22. package/dist/adapters/node/snapshotStore.cjs +2 -0
  23. package/dist/adapters/node/snapshotStore.cjs.map +1 -0
  24. package/dist/adapters/node/snapshotStore.mjs +2 -0
  25. package/dist/adapters/node/snapshotStore.mjs.map +1 -0
  26. package/dist/adapters/node/treeManifest.cjs +2 -0
  27. package/dist/adapters/node/treeManifest.cjs.map +1 -0
  28. package/dist/adapters/node/treeManifest.mjs +2 -0
  29. package/dist/adapters/node/treeManifest.mjs.map +1 -0
  30. package/dist/adapters/pi/extension.cjs +1 -1
  31. package/dist/adapters/pi/extension.cjs.map +1 -1
  32. package/dist/adapters/pi/extension.d.cts.map +1 -1
  33. package/dist/adapters/pi/extension.d.mts.map +1 -1
  34. package/dist/adapters/pi/extension.mjs +1 -1
  35. package/dist/adapters/pi/extension.mjs.map +1 -1
  36. package/dist/adapters/webFilesChannel.cjs +2 -0
  37. package/dist/adapters/webFilesChannel.cjs.map +1 -0
  38. package/dist/adapters/webFilesChannel.d.cts +25 -0
  39. package/dist/adapters/webFilesChannel.d.cts.map +1 -0
  40. package/dist/adapters/webFilesChannel.d.mts +25 -0
  41. package/dist/adapters/webFilesChannel.d.mts.map +1 -0
  42. package/dist/adapters/webFilesChannel.mjs +2 -0
  43. package/dist/adapters/webFilesChannel.mjs.map +1 -0
  44. package/dist/container/index.cjs +1 -1
  45. package/dist/container/index.cjs.map +1 -1
  46. package/dist/container/index.d.cts.map +1 -1
  47. package/dist/container/index.d.mts.map +1 -1
  48. package/dist/container/index.mjs +1 -1
  49. package/dist/container/index.mjs.map +1 -1
  50. package/dist/index.d.cts +2 -2
  51. package/dist/index.d.mts +2 -2
  52. package/dist/services/fileChanges.cjs +3 -0
  53. package/dist/services/fileChanges.cjs.map +1 -0
  54. package/dist/services/fileChanges.mjs +3 -0
  55. package/dist/services/fileChanges.mjs.map +1 -0
  56. package/dist/services/lineDiff.cjs +6 -0
  57. package/dist/services/lineDiff.cjs.map +1 -0
  58. package/dist/services/lineDiff.mjs +6 -0
  59. package/dist/services/lineDiff.mjs.map +1 -0
  60. package/dist/sessionApi.cjs +1 -0
  61. package/dist/sessionApi.d.cts +2 -0
  62. package/dist/sessionApi.d.mts +2 -0
  63. package/dist/sessionApi.mjs +1 -0
  64. package/dist/types/domain.d.cts +45 -3
  65. package/dist/types/domain.d.cts.map +1 -1
  66. package/dist/types/domain.d.mts +45 -3
  67. package/dist/types/domain.d.mts.map +1 -1
  68. package/dist/types/editTracker.d.cts +13 -1
  69. package/dist/types/editTracker.d.cts.map +1 -1
  70. package/dist/types/editTracker.d.mts +13 -1
  71. package/dist/types/editTracker.d.mts.map +1 -1
  72. package/dist/types/fileEditPaths.d.cts +2 -0
  73. package/dist/types/fileEditPaths.d.cts.map +1 -1
  74. package/dist/types/fileEditPaths.d.mts +2 -0
  75. package/dist/types/fileEditPaths.d.mts.map +1 -1
  76. package/dist/types/fileEditsApi.cjs +2 -0
  77. package/dist/types/fileEditsApi.cjs.map +1 -0
  78. package/dist/types/fileEditsApi.mjs +2 -0
  79. package/dist/types/fileEditsApi.mjs.map +1 -0
  80. package/dist/types/index.d.cts +4 -0
  81. package/dist/types/index.d.cts.map +1 -1
  82. package/dist/types/index.d.mts +4 -0
  83. package/dist/types/index.d.mts.map +1 -1
  84. package/dist/types/snapshotStore.d.cts +36 -0
  85. package/dist/types/snapshotStore.d.cts.map +1 -0
  86. package/dist/types/snapshotStore.d.mts +36 -0
  87. package/dist/types/snapshotStore.d.mts.map +1 -0
  88. package/dist/types/timelineStore.d.cts +3 -1
  89. package/dist/types/timelineStore.d.cts.map +1 -1
  90. package/dist/types/timelineStore.d.mts +3 -1
  91. package/dist/types/timelineStore.d.mts.map +1 -1
  92. package/dist/types/treeManifest.d.cts +48 -0
  93. package/dist/types/treeManifest.d.cts.map +1 -0
  94. package/dist/types/treeManifest.d.mts +48 -0
  95. package/dist/types/treeManifest.d.mts.map +1 -0
  96. package/dist/types/webFiles.cjs +2 -0
  97. package/dist/types/webFiles.cjs.map +1 -0
  98. package/dist/types/webFiles.d.cts +33 -0
  99. package/dist/types/webFiles.d.cts.map +1 -0
  100. package/dist/types/webFiles.d.mts +33 -0
  101. package/dist/types/webFiles.d.mts.map +1 -0
  102. package/dist/types/webFiles.mjs +2 -0
  103. package/dist/types/webFiles.mjs.map +1 -0
  104. package/dist/webHub.cjs +1 -0
  105. package/dist/webHub.d.cts +2 -0
  106. package/dist/webHub.d.mts +2 -0
  107. package/dist/webHub.mjs +1 -0
  108. package/package.json +50 -7
  109. package/src/types/domain.ts +77 -0
  110. package/src/types/fileEditsApi.ts +139 -0
  111. package/src/types/webFiles.ts +37 -0
  112. package/web/CommentDraft.tsx +69 -0
  113. package/web/DeleteFileDialog.tsx +56 -0
  114. package/web/DiffView.tsx +96 -0
  115. package/web/FilePanel.tsx +513 -0
  116. package/web/FilesActivitySection.tsx +66 -0
  117. package/web/fileView.ts +112 -0
  118. package/web/filesApi.ts +131 -0
  119. package/web/filesStore.ts +125 -0
  120. package/web/index.ts +37 -0
  121. package/web/tsconfig.json +22 -0
package/README.md CHANGED
@@ -4,9 +4,11 @@ A session-scoped file-change timeline and external-editor launcher for Pi.
4
4
 
5
5
  Part of the [DoomPi distribution](https://www.npmjs.com/package/@agimon-ai/doompi).
6
6
 
7
- The package records files changed by successful `edit` and `write` calls and literal file paths
8
- detected in successful Bash changes. It presents the current session's work without relying on the
9
- transcript to remember every path.
7
+ The package records every file the session changed and keeps enough content to diff it, without
8
+ assuming the working directory is a Git repository. `edit` and `write` name their file in the call,
9
+ so both sides of the change are captured exactly. A Bash call names nothing reliable, because the
10
+ agent can put the paths in a script, so the tree is compared either side of the call and whatever
11
+ moved is recorded however it was written.
10
12
 
11
13
  > **Alpha:** timeline and UI contracts may change between releases.
12
14
 
@@ -24,22 +26,52 @@ DoomPi includes File Edit in every composition. Standalone Pi loads the same ext
24
26
  pi install npm:@agimon-ai/doompi-file-edit
25
27
  ```
26
28
 
27
- | Entry | Purpose |
28
- | ------------------------------------------- | ----------------------------------- |
29
- | `@agimon-ai/doompi-file-edit/extensions/pi` | Timeline and DoomPi Leader shortcut |
30
- | `@agimon-ai/doompi-file-edit` | Library API |
29
+ | Entry | Purpose |
30
+ | ------------------------------------------- | -------------------------------------------- |
31
+ | `@agimon-ai/doompi-file-edit/extensions/pi` | Timeline and DoomPi Leader shortcut |
32
+ | `@agimon-ai/doompi-file-edit/session-api` | HTTP routes the cockpit reads a file through |
33
+ | `@agimon-ai/doompi-file-edit/web-hub` | Hub channel publishing the changed-file list |
34
+ | `@agimon-ai/doompi-file-edit` | Library API |
31
35
 
32
36
  ## Open the timeline
33
37
 
34
38
  Use `SPC e f` in DoomPi. Select a file to inspect its diff or open it in the configured editor.
35
39
 
36
- Diffs compare the working tree with Git `HEAD` and are capped at 200 lines and 256 KiB. Files outside
37
- Git history or changes that cannot be attributed to a literal Bash path may have limited timeline
38
- detail.
39
-
40
- The overlay and external-editor handoff require an interactive TUI. Headless hosts can consume the
40
+ TUI diffs compare the working tree with Git `HEAD` and are capped at 200 lines and 256 KiB. The
41
+ overlay and external-editor handoff require an interactive TUI. Headless hosts can consume the
41
42
  timeline services but cannot display the panel.
42
43
 
44
+ ## In the cockpit
45
+
46
+ The `# files` group in the activity dock lists every file the session changed. Selecting one opens a
47
+ temporary tab holding three views of it:
48
+
49
+ - **diff**: what the session did to the file, followed by each recorded change on its own, so a file
50
+ edited four times shows four distinct diffs rather than one merged blur.
51
+ - **source**: the file as it stands, editable. Saving writes straight to disk and is refused if the
52
+ file changed since it was opened, so a hand edit cannot silently discard what the agent wrote.
53
+ - **preview**: rendered Markdown, or HTML in a sandboxed frame.
54
+
55
+ Selecting text in any view raises a comment box. Comments queue on the tab and one **send review**
56
+ builds a single message quoting each anchor, so several comments cost one turn.
57
+
58
+ Diffs here do not use Git. The package captures the file's content before an `edit` or `write` runs
59
+ and keeps it, content-addressed, beside the session timeline; both go when the session ends.
60
+
61
+ ### What this cannot see
62
+
63
+ - A file the session changed and then deleted. The dock lists things to open, so a row that can only
64
+ answer "this is gone" is left out. The timeline still holds the change, so a tab already open on
65
+ the file keeps working and says why it has nothing to show.
66
+ - A file changed by a background process that outlives its Bash call. The tree is compared when the
67
+ call ends.
68
+ - A file changed outside any tool. There is no always-on watcher.
69
+ - Content past 1 MiB, and binary files. These are listed as changed, without content.
70
+ - A file found only by comparing the tree has no baseline, so it is listed, readable and editable,
71
+ but never diffed. Rows and versions say so rather than showing an empty diff.
72
+ - The tree walk skips dependency and build directories and is capped on entries and depth, so a very
73
+ deep or very large checkout can under-report.
74
+
43
75
  ## Configure the editor
44
76
 
45
77
  Set the shared DoomPi configuration in `~/.pi/.doom/config.yaml`:
@@ -63,9 +95,13 @@ not passed through a shell, so shell operators and expansions are not interprete
63
95
 
64
96
  ## Data behavior
65
97
 
66
- The timeline contains file paths and Git-derived diff content from the current session. That content
67
- can be sensitive. It is stored in session-scoped package state and should be protected with the
68
- same access controls as Pi session data.
98
+ The timeline contains file paths, and the snapshot store holds copies of the file content the
99
+ session changed. That content can be sensitive. Both live in session-scoped package state beside the
100
+ Pi agent directory, are removed when the session ends, and should be protected with the same access
101
+ controls as Pi session data.
102
+
103
+ The session API reads and writes only files the timeline already records, so a request cannot reach
104
+ a file this session never changed.
69
105
 
70
106
  ## Public API
71
107
 
@@ -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:path");n=e.__toESM(n,1);const r=`edit`,i=`write`,a=`bash`,o=new Set([`ENOENT`,`ENAMETOOLONG`]);function s(e,t){if(!e||typeof e!=`object`)return;let n=e[t];return typeof n==`string`?n:void 0}var c=class{timeline;pending=new Map;constructor(e){this.timeline=e}async start(e,t,o,c){if(t===r||t===i){let r=s(o,`path`);if(!r)return;let i=n.default.resolve(c,r);this.pending.set(e,{tool:t,paths:new Map([[i,await this.fingerprint(i)]])});return}if(t!==a)return;let l=s(o,`command`);if(!l)return;let u=new Map;for(let e of this.literalPaths(l,c))u.set(e,await this.fingerprint(e));this.pending.set(e,{tool:a,paths:u})}async end(e,t){let n=this.pending.get(e);if(this.pending.delete(e),!(!n||t))for(let[e,t]of n.paths){let a=await this.fingerprint(e);(n.tool===r||n.tool===i||JSON.stringify(t)!==JSON.stringify(a))&&await this.timeline.append({version:1,path:e,tool:n.tool,at:Date.now()})}}literalPaths(e,t){let r=(e.match(/"[^"]+"|'[^']+'|[^\s|;&<>]+/gu)??[]).map(e=>e.replace(/^['"]|['"]$/gu,``)).filter(e=>!e.startsWith(`-`)&&(e.includes(`/`)||/\.[a-z0-9_-]+$/iu.test(e))).map(e=>n.default.resolve(t,e));return[...new Set(r)]}async fingerprint(e){try{let n=await t.default.stat(e);return{exists:n.isFile(),size:n.size,modified:n.mtimeMs}}catch(e){if(e instanceof Error&&`code`in e&&typeof e.code==`string`&&o.has(e.code))return{exists:!1,size:0,modified:0};throw e}}};exports.EditTracker=c;
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 Set;manifest;excluded=[];now;constructor(e,t,n,r={}){this.timeline=e,this.snapshots=t,this.manifests=n,this.now=r.now??Date.now}reset(e={}){this.pending.clear(),this.bracketed.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.add(e),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.delete(e);if(!t){if(r){await this.recordTool(r);return}i&&await this.recordScan(n)}}async recordTool(e){let t=await this.snapshots.capture(e.filePath),n=await this.countChanges(e.before,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){let t=this.manifest;if(t===void 0)return;let n=await this.manifests.take(e,this.excluded);this.manifest=n;for(let e of this.manifests.changed(t,n))await this.append({version:2,path:e,tool:r,at:this.now(),origin:`scan`,...await this.captured(e)})}async captured(e){let t=await this.snapshots.capture(e);return t===void 0?{}:{after:t}}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;
2
2
  //# sourceMappingURL=EditTracker.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"EditTracker.cjs","names":["path","fs"],"sources":["../../../src/adapters/EditTracker/EditTracker.ts"],"sourcesContent":["import fs from 'node:fs/promises';\nimport path from 'node:path';\nimport type { FileEditTool } from '../../types/domain';\nimport type { ITimelineStore } from '../../types/timelineStore';\nimport type { IEditTracker } from '../../types/editTracker';\n\nconst EDIT_TOOL = 'edit';\nconst WRITE_TOOL = 'write';\nconst BASH_TOOL = 'bash';\nconst MISSING_PATH_ERROR_CODES = new Set(['ENOENT', 'ENAMETOOLONG']);\n\ninterface Fingerprint {\n exists: boolean;\n size: number;\n modified: number;\n}\n\ninterface PendingEdit {\n tool: FileEditTool;\n paths: Map<string, Fingerprint>;\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 class EditTracker implements IEditTracker {\n private readonly pending = new Map<string, PendingEdit>();\n\n constructor(private readonly timeline: ITimelineStore) {}\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 absolute = path.resolve(cwd, supplied);\n this.pending.set(id, { tool, paths: new Map([[absolute, await this.fingerprint(absolute)]]) });\n return;\n }\n if (tool !== BASH_TOOL) return;\n const command = objectValue(args, 'command');\n if (!command) return;\n const paths = new Map<string, Fingerprint>();\n for (const candidate of this.literalPaths(command, cwd)) paths.set(candidate, await this.fingerprint(candidate));\n this.pending.set(id, { tool: BASH_TOOL, paths });\n }\n\n async end(id: string, isError: boolean): Promise<void> {\n const pending = this.pending.get(id);\n this.pending.delete(id);\n if (!pending || isError) return;\n for (const [filePath, before] of pending.paths) {\n const after = await this.fingerprint(filePath);\n const exactTool = pending.tool === EDIT_TOOL || pending.tool === WRITE_TOOL;\n if (exactTool || JSON.stringify(before) !== JSON.stringify(after)) {\n await this.timeline.append({ version: 1, path: filePath, tool: pending.tool, at: Date.now() });\n }\n }\n }\n\n private literalPaths(command: string, cwd: string): string[] {\n const tokens = command.match(/\"[^\"]+\"|'[^']+'|[^\\s|;&<>]+/gu) ?? [];\n const candidates = tokens\n .map((token) => token.replace(/^['\"]|['\"]$/gu, ''))\n .filter((token) => !token.startsWith('-') && (token.includes('/') || /\\.[a-z0-9_-]+$/iu.test(token)))\n .map((token) => path.resolve(cwd, token));\n return [...new Set(candidates)];\n }\n\n private async fingerprint(filePath: string): Promise<Fingerprint> {\n try {\n const stat = await fs.stat(filePath);\n return { exists: stat.isFile(), size: stat.size, modified: stat.mtimeMs };\n } catch (error) {\n if (\n error instanceof Error &&\n 'code' in error &&\n typeof error.code === 'string' &&\n MISSING_PATH_ERROR_CODES.has(error.code)\n ) {\n return { exists: false, size: 0, modified: 0 };\n }\n throw error;\n }\n }\n}\n"],"mappings":"uJAMA,MAAM,EAAY,OACZ,EAAa,QACb,EAAY,OACZ,EAA2B,IAAI,IAAI,CAAC,SAAU,cAAc,CAAC,EAanE,SAAS,EAAY,EAAgB,EAAiC,CACpE,GAAI,CAAC,GAAS,OAAO,GAAU,SAAU,OACzC,IAAM,EAAS,EAAkC,GACjD,OAAO,OAAO,GAAU,SAAW,EAAQ,IAAA,EAC7C,CAEA,IAAa,EAAb,KAAiD,CAGlB,SAF7B,QAA2B,IAAI,IAE/B,YAAY,EAA2C,CAA1B,KAAA,SAAA,CAA2B,CAExD,MAAM,MAAM,EAAY,EAAc,EAAe,EAA4B,CAC/E,GAAI,IAAS,GAAa,IAAS,EAAY,CAC7C,IAAM,EAAW,EAAY,EAAM,MAAM,EACzC,GAAI,CAAC,EAAU,OACf,IAAM,EAAWA,EAAAA,QAAK,QAAQ,EAAK,CAAQ,EAC3C,KAAK,QAAQ,IAAI,EAAI,CAAE,OAAM,MAAO,IAAI,IAAI,CAAC,CAAC,EAAU,MAAM,KAAK,YAAY,CAAQ,CAAC,CAAC,CAAC,CAAE,CAAC,EAC7F,MACF,CACA,GAAI,IAAS,EAAW,OACxB,IAAM,EAAU,EAAY,EAAM,SAAS,EAC3C,GAAI,CAAC,EAAS,OACd,IAAM,EAAQ,IAAI,IAClB,IAAK,IAAM,KAAa,KAAK,aAAa,EAAS,CAAG,EAAG,EAAM,IAAI,EAAW,MAAM,KAAK,YAAY,CAAS,CAAC,EAC/G,KAAK,QAAQ,IAAI,EAAI,CAAE,KAAM,EAAW,OAAM,CAAC,CACjD,CAEA,MAAM,IAAI,EAAY,EAAiC,CACrD,IAAM,EAAU,KAAK,QAAQ,IAAI,CAAE,EACnC,QAAK,QAAQ,OAAO,CAAE,EAClB,GAAC,GAAW,GAChB,IAAK,GAAM,CAAC,EAAU,KAAW,EAAQ,MAAO,CAC9C,IAAM,EAAQ,MAAM,KAAK,YAAY,CAAQ,GAC3B,EAAQ,OAAS,GAAa,EAAQ,OAAS,GAChD,KAAK,UAAU,CAAM,IAAM,KAAK,UAAU,CAAK,IAC9D,MAAM,KAAK,SAAS,OAAO,CAAE,QAAS,EAAG,KAAM,EAAU,KAAM,EAAQ,KAAM,GAAI,KAAK,IAAI,CAAE,CAAC,CAEjG,CACF,CAEA,aAAqB,EAAiB,EAAuB,CAE3D,IAAM,GADS,EAAQ,MAAM,+BAA+B,GAAK,CAAC,EAAA,CAE/D,IAAK,GAAU,EAAM,QAAQ,gBAAiB,EAAE,CAAC,CAAC,CAClD,OAAQ,GAAU,CAAC,EAAM,WAAW,GAAG,IAAM,EAAM,SAAS,GAAG,GAAK,mBAAmB,KAAK,CAAK,EAAE,CAAC,CACpG,IAAK,GAAUA,EAAAA,QAAK,QAAQ,EAAK,CAAK,CAAC,EAC1C,MAAO,CAAC,GAAG,IAAI,IAAI,CAAU,CAAC,CAChC,CAEA,MAAc,YAAY,EAAwC,CAChE,GAAI,CACF,IAAM,EAAO,MAAMC,EAAAA,QAAG,KAAK,CAAQ,EACnC,MAAO,CAAE,OAAQ,EAAK,OAAO,EAAG,KAAM,EAAK,KAAM,SAAU,EAAK,OAAQ,CAC1E,OAAS,EAAO,CACd,GACE,aAAiB,OACjB,SAAU,GACV,OAAO,EAAM,MAAS,UACtB,EAAyB,IAAI,EAAM,IAAI,EAEvC,MAAO,CAAE,OAAQ,GAAO,KAAM,EAAG,SAAU,CAAE,EAE/C,MAAM,CACR,CACF,CACF"}
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 { 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 */\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\nexport class EditTracker implements IEditTracker {\n private readonly pending = new Map<string, PendingEdit>();\n /** Set while a bash call is in flight, so its end knows a manifest is owed. */\n private readonly bracketed = new Set<string>();\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 private readonly now: () => number;\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 }\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.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 this.bracketed.add(id);\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 bracketed = this.bracketed.delete(id);\n if (isError) return;\n if (pending) {\n await this.recordTool(pending);\n return;\n }\n if (bracketed) await this.recordScan(cwd);\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 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 moved while a bash call ran, however the command wrote it. */\n private async recordScan(cwd: string): 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 for (const filePath of this.manifests.changed(before, after)) {\n await this.append({\n version: 2,\n path: filePath,\n tool: BASH_TOOL,\n at: this.now(),\n origin: 'scan',\n ...(await this.captured(filePath)),\n });\n }\n }\n\n /** The after-snapshot of a scan-found path, when it still exists and fits. */\n private async captured(filePath: string): Promise<{ after?: string }> {\n const after = await this.snapshots.capture(filePath);\n return after === undefined ? {} : { after };\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,2 +1,2 @@
1
- import e from"node:fs/promises";import t from"node:path";const n=`edit`,r=`write`,i=`bash`,a=new Set([`ENOENT`,`ENAMETOOLONG`]);function o(e,t){if(!e||typeof e!=`object`)return;let n=e[t];return typeof n==`string`?n:void 0}var s=class{timeline;pending=new Map;constructor(e){this.timeline=e}async start(e,a,s,c){if(a===n||a===r){let n=o(s,`path`);if(!n)return;let r=t.resolve(c,n);this.pending.set(e,{tool:a,paths:new Map([[r,await this.fingerprint(r)]])});return}if(a!==i)return;let l=o(s,`command`);if(!l)return;let u=new Map;for(let e of this.literalPaths(l,c))u.set(e,await this.fingerprint(e));this.pending.set(e,{tool:i,paths:u})}async end(e,t){let i=this.pending.get(e);if(this.pending.delete(e),!(!i||t))for(let[e,t]of i.paths){let a=await this.fingerprint(e);(i.tool===n||i.tool===r||JSON.stringify(t)!==JSON.stringify(a))&&await this.timeline.append({version:1,path:e,tool:i.tool,at:Date.now()})}}literalPaths(e,n){let r=(e.match(/"[^"]+"|'[^']+'|[^\s|;&<>]+/gu)??[]).map(e=>e.replace(/^['"]|['"]$/gu,``)).filter(e=>!e.startsWith(`-`)&&(e.includes(`/`)||/\.[a-z0-9_-]+$/iu.test(e))).map(e=>t.resolve(n,e));return[...new Set(r)]}async fingerprint(t){try{let n=await e.stat(t);return{exists:n.isFile(),size:n.size,modified:n.mtimeMs}}catch(e){if(e instanceof Error&&`code`in e&&typeof e.code==`string`&&a.has(e.code))return{exists:!1,size:0,modified:0};throw e}}};export{s as EditTracker};
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 Set;manifest;excluded=[];now;constructor(e,t,n,r={}){this.timeline=e,this.snapshots=t,this.manifests=n,this.now=r.now??Date.now}reset(e={}){this.pending.clear(),this.bracketed.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.add(e),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.delete(e);if(!t){if(r){await this.recordTool(r);return}i&&await this.recordScan(n)}}async recordTool(e){let t=await this.snapshots.capture(e.filePath),n=await this.countChanges(e.before,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){let t=this.manifest;if(t===void 0)return;let r=await this.manifests.take(e,this.excluded);this.manifest=r;for(let e of this.manifests.changed(t,r))await this.append({version:2,path:e,tool:n,at:this.now(),origin:`scan`,...await this.captured(e)})}async captured(e){let t=await this.snapshots.capture(e);return t===void 0?{}:{after:t}}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};
2
2
  //# sourceMappingURL=EditTracker.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"EditTracker.mjs","names":[],"sources":["../../../src/adapters/EditTracker/EditTracker.ts"],"sourcesContent":["import fs from 'node:fs/promises';\nimport path from 'node:path';\nimport type { FileEditTool } from '../../types/domain';\nimport type { ITimelineStore } from '../../types/timelineStore';\nimport type { IEditTracker } from '../../types/editTracker';\n\nconst EDIT_TOOL = 'edit';\nconst WRITE_TOOL = 'write';\nconst BASH_TOOL = 'bash';\nconst MISSING_PATH_ERROR_CODES = new Set(['ENOENT', 'ENAMETOOLONG']);\n\ninterface Fingerprint {\n exists: boolean;\n size: number;\n modified: number;\n}\n\ninterface PendingEdit {\n tool: FileEditTool;\n paths: Map<string, Fingerprint>;\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 class EditTracker implements IEditTracker {\n private readonly pending = new Map<string, PendingEdit>();\n\n constructor(private readonly timeline: ITimelineStore) {}\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 absolute = path.resolve(cwd, supplied);\n this.pending.set(id, { tool, paths: new Map([[absolute, await this.fingerprint(absolute)]]) });\n return;\n }\n if (tool !== BASH_TOOL) return;\n const command = objectValue(args, 'command');\n if (!command) return;\n const paths = new Map<string, Fingerprint>();\n for (const candidate of this.literalPaths(command, cwd)) paths.set(candidate, await this.fingerprint(candidate));\n this.pending.set(id, { tool: BASH_TOOL, paths });\n }\n\n async end(id: string, isError: boolean): Promise<void> {\n const pending = this.pending.get(id);\n this.pending.delete(id);\n if (!pending || isError) return;\n for (const [filePath, before] of pending.paths) {\n const after = await this.fingerprint(filePath);\n const exactTool = pending.tool === EDIT_TOOL || pending.tool === WRITE_TOOL;\n if (exactTool || JSON.stringify(before) !== JSON.stringify(after)) {\n await this.timeline.append({ version: 1, path: filePath, tool: pending.tool, at: Date.now() });\n }\n }\n }\n\n private literalPaths(command: string, cwd: string): string[] {\n const tokens = command.match(/\"[^\"]+\"|'[^']+'|[^\\s|;&<>]+/gu) ?? [];\n const candidates = tokens\n .map((token) => token.replace(/^['\"]|['\"]$/gu, ''))\n .filter((token) => !token.startsWith('-') && (token.includes('/') || /\\.[a-z0-9_-]+$/iu.test(token)))\n .map((token) => path.resolve(cwd, token));\n return [...new Set(candidates)];\n }\n\n private async fingerprint(filePath: string): Promise<Fingerprint> {\n try {\n const stat = await fs.stat(filePath);\n return { exists: stat.isFile(), size: stat.size, modified: stat.mtimeMs };\n } catch (error) {\n if (\n error instanceof Error &&\n 'code' in error &&\n typeof error.code === 'string' &&\n MISSING_PATH_ERROR_CODES.has(error.code)\n ) {\n return { exists: false, size: 0, modified: 0 };\n }\n throw error;\n }\n }\n}\n"],"mappings":"yDAMA,MAAM,EAAY,OACZ,EAAa,QACb,EAAY,OACZ,EAA2B,IAAI,IAAI,CAAC,SAAU,cAAc,CAAC,EAanE,SAAS,EAAY,EAAgB,EAAiC,CACpE,GAAI,CAAC,GAAS,OAAO,GAAU,SAAU,OACzC,IAAM,EAAS,EAAkC,GACjD,OAAO,OAAO,GAAU,SAAW,EAAQ,IAAA,EAC7C,CAEA,IAAa,EAAb,KAAiD,CAGlB,SAF7B,QAA2B,IAAI,IAE/B,YAAY,EAA2C,CAA1B,KAAA,SAAA,CAA2B,CAExD,MAAM,MAAM,EAAY,EAAc,EAAe,EAA4B,CAC/E,GAAI,IAAS,GAAa,IAAS,EAAY,CAC7C,IAAM,EAAW,EAAY,EAAM,MAAM,EACzC,GAAI,CAAC,EAAU,OACf,IAAM,EAAW,EAAK,QAAQ,EAAK,CAAQ,EAC3C,KAAK,QAAQ,IAAI,EAAI,CAAE,OAAM,MAAO,IAAI,IAAI,CAAC,CAAC,EAAU,MAAM,KAAK,YAAY,CAAQ,CAAC,CAAC,CAAC,CAAE,CAAC,EAC7F,MACF,CACA,GAAI,IAAS,EAAW,OACxB,IAAM,EAAU,EAAY,EAAM,SAAS,EAC3C,GAAI,CAAC,EAAS,OACd,IAAM,EAAQ,IAAI,IAClB,IAAK,IAAM,KAAa,KAAK,aAAa,EAAS,CAAG,EAAG,EAAM,IAAI,EAAW,MAAM,KAAK,YAAY,CAAS,CAAC,EAC/G,KAAK,QAAQ,IAAI,EAAI,CAAE,KAAM,EAAW,OAAM,CAAC,CACjD,CAEA,MAAM,IAAI,EAAY,EAAiC,CACrD,IAAM,EAAU,KAAK,QAAQ,IAAI,CAAE,EACnC,QAAK,QAAQ,OAAO,CAAE,EAClB,GAAC,GAAW,GAChB,IAAK,GAAM,CAAC,EAAU,KAAW,EAAQ,MAAO,CAC9C,IAAM,EAAQ,MAAM,KAAK,YAAY,CAAQ,GAC3B,EAAQ,OAAS,GAAa,EAAQ,OAAS,GAChD,KAAK,UAAU,CAAM,IAAM,KAAK,UAAU,CAAK,IAC9D,MAAM,KAAK,SAAS,OAAO,CAAE,QAAS,EAAG,KAAM,EAAU,KAAM,EAAQ,KAAM,GAAI,KAAK,IAAI,CAAE,CAAC,CAEjG,CACF,CAEA,aAAqB,EAAiB,EAAuB,CAE3D,IAAM,GADS,EAAQ,MAAM,+BAA+B,GAAK,CAAC,EAAA,CAE/D,IAAK,GAAU,EAAM,QAAQ,gBAAiB,EAAE,CAAC,CAAC,CAClD,OAAQ,GAAU,CAAC,EAAM,WAAW,GAAG,IAAM,EAAM,SAAS,GAAG,GAAK,mBAAmB,KAAK,CAAK,EAAE,CAAC,CACpG,IAAK,GAAU,EAAK,QAAQ,EAAK,CAAK,CAAC,EAC1C,MAAO,CAAC,GAAG,IAAI,IAAI,CAAU,CAAC,CAChC,CAEA,MAAc,YAAY,EAAwC,CAChE,GAAI,CACF,IAAM,EAAO,MAAM,EAAG,KAAK,CAAQ,EACnC,MAAO,CAAE,OAAQ,EAAK,OAAO,EAAG,KAAM,EAAK,KAAM,SAAU,EAAK,OAAQ,CAC1E,OAAS,EAAO,CACd,GACE,aAAiB,OACjB,SAAU,GACV,OAAO,EAAM,MAAS,UACtB,EAAyB,IAAI,EAAM,IAAI,EAEvC,MAAO,CAAE,OAAQ,GAAO,KAAM,EAAG,SAAU,CAAE,EAE/C,MAAM,CACR,CACF,CACF"}
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 { 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 */\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\nexport class EditTracker implements IEditTracker {\n private readonly pending = new Map<string, PendingEdit>();\n /** Set while a bash call is in flight, so its end knows a manifest is owed. */\n private readonly bracketed = new Set<string>();\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 private readonly now: () => number;\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 }\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.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 this.bracketed.add(id);\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 bracketed = this.bracketed.delete(id);\n if (isError) return;\n if (pending) {\n await this.recordTool(pending);\n return;\n }\n if (bracketed) await this.recordScan(cwd);\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 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 moved while a bash call ran, however the command wrote it. */\n private async recordScan(cwd: string): 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 for (const filePath of this.manifests.changed(before, after)) {\n await this.append({\n version: 2,\n path: filePath,\n tool: BASH_TOOL,\n at: this.now(),\n origin: 'scan',\n ...(await this.captured(filePath)),\n });\n }\n }\n\n /** The after-snapshot of a scan-found path, when it still exists and fits. */\n private async captured(filePath: string): Promise<{ after?: string }> {\n const after = await this.snapshots.capture(filePath);\n return after === undefined ? {} : { after };\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,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`)}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("@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;
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 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,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","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,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`)}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{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};
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 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,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\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,3 +1,2 @@
1
- const e=require("../../_virtual/_rolldown/runtime.cjs");let t=require("node:fs/promises");t=e.__toESM(t,1);function n(e,t){return e instanceof Error&&`code`in e&&e.code===t}function r(e){if(!e||typeof e!=`object`)return!1;let t=e;return t.version===1&&typeof t.path==`string`&&typeof t.at==`number`&&(t.tool===`edit`||t.tool===`write`||t.tool===`bash`)}var i=class{filePath;initialize(e){this.filePath=e}async append(e){await this.withLock(async()=>t.default.appendFile(this.requirePath(),`${JSON.stringify(e)}\n`,`utf8`))}async list(){let e;try{e=await t.default.readFile(this.requirePath(),`utf8`)}catch(e){if(n(e,`ENOENT`))return[];throw e}let i=new Map;for(let t of e.split(`
2
- `)){if(!t)continue;let e;try{e=JSON.parse(t)}catch(e){console.warn(`Ignoring malformed file edit timeline line: ${e instanceof Error?e.message:String(e)}`);continue}if(!r(e))continue;let n=i.get(e.path);i.set(e.path,{path:e.path,tool:e.at>=(n?.at??0)?e.tool:n?.tool??e.tool,at:Math.max(e.at,n?.at??0),count:(n?.count??0)+1})}return[...i.values()].sort((e,t)=>t.at-e.at)}async clear(){try{await t.default.unlink(this.requirePath())}catch(e){if(!n(e,`ENOENT`))throw e}}requirePath(){if(!this.filePath)throw Error(`Timeline store is not initialized`);return this.filePath}async withLock(e){let r=`${this.requirePath()}.lock`;for(let i=0;i<100;i+=1)try{let i=await t.default.open(r,`wx`);try{return await e()}finally{await i.close();try{await t.default.unlink(r)}catch(e){n(e,`ENOENT`)||console.warn(`Could not remove timeline lock: ${String(e)}`)}}}catch(e){if(!n(e,`EEXIST`))throw e;await new Promise(e=>setTimeout(e,10))}throw Error(`Timed out acquiring timeline lock ${r}`)}};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(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`;for(let i=0;i<100;i+=1)try{let i=await n.default.open(t,`wx`);try{return await e()}finally{await i.close();try{await n.default.unlink(t)}catch(e){r(e,`ENOENT`)||console.warn(`Could not remove timeline lock: ${String(e)}`)}}}catch(e){if(!r(e,`EEXIST`))throw e;await new Promise(e=>setTimeout(e,10))}throw Error(`Timed out acquiring timeline lock ${t}`)}};exports.TimelineStore=i;
3
2
  //# sourceMappingURL=TimelineStore.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"TimelineStore.cjs","names":["fs"],"sources":["../../../src/adapters/TimelineStore/TimelineStore.ts"],"sourcesContent":["import fs from 'node:fs/promises';\nimport type { FileEditEntry, TimelineEvent } from '../../types/domain';\nimport type { ITimelineStore } from '../../types/timelineStore';\n\nconst LOCK_RETRY_MS = 10;\nconst LOCK_RETRIES = 100;\n\nfunction hasCode(error: unknown, code: string): boolean {\n return error instanceof Error && 'code' in error && error.code === code;\n}\n\nfunction isTimelineEvent(value: unknown): value is TimelineEvent {\n if (!value || typeof value !== 'object') return false;\n const event = value as Partial<TimelineEvent>;\n return (\n event.version === 1 &&\n typeof event.path === 'string' &&\n typeof event.at === 'number' &&\n (event.tool === 'edit' || event.tool === 'write' || event.tool === 'bash')\n );\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 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 const folded = new Map<string, FileEditEntry>();\n for (const line of content.split('\\n')) {\n if (!line) continue;\n let parsed: unknown;\n try {\n parsed = JSON.parse(line);\n } catch (error) {\n console.warn(\n `Ignoring malformed file edit timeline line: ${error instanceof Error ? error.message : String(error)}`,\n );\n continue;\n }\n if (!isTimelineEvent(parsed)) continue;\n const current = folded.get(parsed.path);\n folded.set(parsed.path, {\n path: parsed.path,\n tool: parsed.at >= (current?.at ?? 0) ? parsed.tool : (current?.tool ?? parsed.tool),\n at: Math.max(parsed.at, current?.at ?? 0),\n count: (current?.count ?? 0) + 1,\n });\n }\n return [...folded.values()].sort((left, right) => right.at - left.at);\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 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 for (let attempt = 0; attempt < LOCK_RETRIES; attempt += 1) {\n try {\n const lock = await fs.open(lockPath, 'wx');\n try {\n return await operation();\n } finally {\n await lock.close();\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 } catch (error) {\n if (!hasCode(error, 'EEXIST')) throw error;\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"],"mappings":"2GAOA,SAAS,EAAQ,EAAgB,EAAuB,CACtD,OAAO,aAAiB,OAAS,SAAU,GAAS,EAAM,OAAS,CACrE,CAEA,SAAS,EAAgB,EAAwC,CAC/D,GAAI,CAAC,GAAS,OAAO,GAAU,SAAU,MAAO,GAChD,IAAM,EAAQ,EACd,OACE,EAAM,UAAY,GAClB,OAAO,EAAM,MAAS,UACtB,OAAO,EAAM,IAAO,WACnB,EAAM,OAAS,QAAU,EAAM,OAAS,SAAW,EAAM,OAAS,OAEvE,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,CACrC,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,IAAM,EAAS,IAAI,IACnB,IAAK,IAAM,KAAQ,EAAQ,MAAM;CAAI,EAAG,CACtC,GAAI,CAAC,EAAM,SACX,IAAI,EACJ,GAAI,CACF,EAAS,KAAK,MAAM,CAAI,CAC1B,OAAS,EAAO,CACd,QAAQ,KACN,+CAA+C,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,GACtG,EACA,QACF,CACA,GAAI,CAAC,EAAgB,CAAM,EAAG,SAC9B,IAAM,EAAU,EAAO,IAAI,EAAO,IAAI,EACtC,EAAO,IAAI,EAAO,KAAM,CACtB,KAAM,EAAO,KACb,KAAM,EAAO,KAAO,GAAS,IAAM,GAAK,EAAO,KAAQ,GAAS,MAAQ,EAAO,KAC/E,GAAI,KAAK,IAAI,EAAO,GAAI,GAAS,IAAM,CAAC,EACxC,OAAQ,GAAS,OAAS,GAAK,CACjC,CAAC,CACH,CACA,MAAO,CAAC,GAAG,EAAO,OAAO,CAAC,CAAC,CAAC,MAAM,EAAM,IAAU,EAAM,GAAK,EAAK,EAAE,CACtE,CAEA,MAAM,OAAuB,CAC3B,GAAI,CACF,MAAMA,EAAAA,QAAG,OAAO,KAAK,YAAY,CAAC,CACpC,OAAS,EAAO,CACd,GAAI,CAAC,EAAQ,EAAO,QAAQ,EAAG,MAAM,CACvC,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,OACvC,IAAK,IAAI,EAAU,EAAG,EAAU,IAAc,GAAW,EACvD,GAAI,CACF,IAAM,EAAO,MAAMA,EAAAA,QAAG,KAAK,EAAU,IAAI,EACzC,GAAI,CACF,OAAO,MAAM,EAAU,CACzB,QAAU,CACR,MAAM,EAAK,MAAM,EACjB,GAAI,CACF,MAAMA,EAAAA,QAAG,OAAO,CAAQ,CAC1B,OAAS,EAAO,CACT,EAAQ,EAAO,QAAQ,GAAG,QAAQ,KAAK,mCAAmC,OAAO,CAAK,GAAG,CAChG,CACF,CACF,OAAS,EAAO,CACd,GAAI,CAAC,EAAQ,EAAO,QAAQ,EAAG,MAAM,EACrC,MAAM,IAAI,QAAS,GAAY,WAAW,EAAS,EAAa,CAAC,CACnE,CAEF,MAAU,MAAM,qCAAqC,GAAU,CACjE,CACF"}
1
+ {"version":3,"file":"TimelineStore.cjs","names":["fs","foldEntries","foldVersions","parseTimeline"],"sources":["../../../src/adapters/TimelineStore/TimelineStore.ts"],"sourcesContent":["import fs from 'node:fs/promises';\nimport { type AnyTimelineEvent, foldEntries, foldVersions, parseTimeline } from '../../services/fileChanges.ts';\nimport type { FileEditEntry, FileEditVersion, TimelineEvent } from '../../types/domain';\nimport type { ITimelineStore } from '../../types/timelineStore';\n\nconst LOCK_RETRY_MS = 10;\nconst LOCK_RETRIES = 100;\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 return foldEntries(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 for (let attempt = 0; attempt < LOCK_RETRIES; attempt += 1) {\n try {\n const lock = await fs.open(lockPath, 'wx');\n try {\n return await operation();\n } finally {\n await lock.close();\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 } catch (error) {\n if (!hasCode(error, 'EEXIST')) throw error;\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"],"mappings":"uJAQA,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,CACrC,OAAOC,EAAAA,YAAY,MAAM,KAAK,OAAO,CAAC,CACxC,CAEA,MAAM,SAAS,EAA8C,CAC3D,OAAOC,EAAAA,aAAa,MAAM,KAAK,OAAO,EAAG,CAAQ,CACnD,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,OAAOG,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,OACvC,IAAK,IAAI,EAAU,EAAG,EAAU,IAAc,GAAW,EACvD,GAAI,CACF,IAAM,EAAO,MAAMH,EAAAA,QAAG,KAAK,EAAU,IAAI,EACzC,GAAI,CACF,OAAO,MAAM,EAAU,CACzB,QAAU,CACR,MAAM,EAAK,MAAM,EACjB,GAAI,CACF,MAAMA,EAAAA,QAAG,OAAO,CAAQ,CAC1B,OAAS,EAAO,CACT,EAAQ,EAAO,QAAQ,GAAG,QAAQ,KAAK,mCAAmC,OAAO,CAAK,GAAG,CAChG,CACF,CACF,OAAS,EAAO,CACd,GAAI,CAAC,EAAQ,EAAO,QAAQ,EAAG,MAAM,EACrC,MAAM,IAAI,QAAS,GAAY,WAAW,EAAS,EAAa,CAAC,CACnE,CAEF,MAAU,MAAM,qCAAqC,GAAU,CACjE,CACF"}
@@ -1,3 +1,2 @@
1
- import e from"node:fs/promises";function t(e,t){return e instanceof Error&&`code`in e&&e.code===t}function n(e){if(!e||typeof e!=`object`)return!1;let t=e;return t.version===1&&typeof t.path==`string`&&typeof t.at==`number`&&(t.tool===`edit`||t.tool===`write`||t.tool===`bash`)}var r=class{filePath;initialize(e){this.filePath=e}async append(t){await this.withLock(async()=>e.appendFile(this.requirePath(),`${JSON.stringify(t)}\n`,`utf8`))}async list(){let r;try{r=await e.readFile(this.requirePath(),`utf8`)}catch(e){if(t(e,`ENOENT`))return[];throw e}let i=new Map;for(let e of r.split(`
2
- `)){if(!e)continue;let t;try{t=JSON.parse(e)}catch(e){console.warn(`Ignoring malformed file edit timeline line: ${e instanceof Error?e.message:String(e)}`);continue}if(!n(t))continue;let r=i.get(t.path);i.set(t.path,{path:t.path,tool:t.at>=(r?.at??0)?t.tool:r?.tool??t.tool,at:Math.max(t.at,r?.at??0),count:(r?.count??0)+1})}return[...i.values()].sort((e,t)=>t.at-e.at)}async clear(){try{await e.unlink(this.requirePath())}catch(e){if(!t(e,`ENOENT`))throw e}}requirePath(){if(!this.filePath)throw Error(`Timeline store is not initialized`);return this.filePath}async withLock(n){let r=`${this.requirePath()}.lock`;for(let i=0;i<100;i+=1)try{let i=await e.open(r,`wx`);try{return await n()}finally{await i.close();try{await e.unlink(r)}catch(e){t(e,`ENOENT`)||console.warn(`Could not remove timeline lock: ${String(e)}`)}}}catch(e){if(!t(e,`EEXIST`))throw e;await new Promise(e=>setTimeout(e,10))}throw Error(`Timed out acquiring timeline lock ${r}`)}};export{r as TimelineStore};
1
+ import{foldEntries as e,foldVersions as t,parseTimeline as n}from"../../services/fileChanges.mjs";import r from"node:fs/promises";function i(e,t){return e instanceof Error&&`code`in e&&e.code===t}var a=class{filePath;initialize(e){this.filePath=e}async append(e){await this.withLock(async()=>r.appendFile(this.requirePath(),`${JSON.stringify(e)}\n`,`utf8`))}async list(){return e(await this.events())}async versions(e){return t(await this.events(),e)}async clear(){try{await r.unlink(this.requirePath())}catch(e){if(!i(e,`ENOENT`))throw e}}async events(){let e;try{e=await r.readFile(this.requirePath(),`utf8`)}catch(e){if(i(e,`ENOENT`))return[];throw e}return n(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`;for(let n=0;n<100;n+=1)try{let n=await r.open(t,`wx`);try{return await e()}finally{await n.close();try{await r.unlink(t)}catch(e){i(e,`ENOENT`)||console.warn(`Could not remove timeline lock: ${String(e)}`)}}}catch(e){if(!i(e,`EEXIST`))throw e;await new Promise(e=>setTimeout(e,10))}throw Error(`Timed out acquiring timeline lock ${t}`)}};export{a as TimelineStore};
3
2
  //# sourceMappingURL=TimelineStore.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"TimelineStore.mjs","names":[],"sources":["../../../src/adapters/TimelineStore/TimelineStore.ts"],"sourcesContent":["import fs from 'node:fs/promises';\nimport type { FileEditEntry, TimelineEvent } from '../../types/domain';\nimport type { ITimelineStore } from '../../types/timelineStore';\n\nconst LOCK_RETRY_MS = 10;\nconst LOCK_RETRIES = 100;\n\nfunction hasCode(error: unknown, code: string): boolean {\n return error instanceof Error && 'code' in error && error.code === code;\n}\n\nfunction isTimelineEvent(value: unknown): value is TimelineEvent {\n if (!value || typeof value !== 'object') return false;\n const event = value as Partial<TimelineEvent>;\n return (\n event.version === 1 &&\n typeof event.path === 'string' &&\n typeof event.at === 'number' &&\n (event.tool === 'edit' || event.tool === 'write' || event.tool === 'bash')\n );\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 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 const folded = new Map<string, FileEditEntry>();\n for (const line of content.split('\\n')) {\n if (!line) continue;\n let parsed: unknown;\n try {\n parsed = JSON.parse(line);\n } catch (error) {\n console.warn(\n `Ignoring malformed file edit timeline line: ${error instanceof Error ? error.message : String(error)}`,\n );\n continue;\n }\n if (!isTimelineEvent(parsed)) continue;\n const current = folded.get(parsed.path);\n folded.set(parsed.path, {\n path: parsed.path,\n tool: parsed.at >= (current?.at ?? 0) ? parsed.tool : (current?.tool ?? parsed.tool),\n at: Math.max(parsed.at, current?.at ?? 0),\n count: (current?.count ?? 0) + 1,\n });\n }\n return [...folded.values()].sort((left, right) => right.at - left.at);\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 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 for (let attempt = 0; attempt < LOCK_RETRIES; attempt += 1) {\n try {\n const lock = await fs.open(lockPath, 'wx');\n try {\n return await operation();\n } finally {\n await lock.close();\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 } catch (error) {\n if (!hasCode(error, 'EEXIST')) throw error;\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"],"mappings":"gCAOA,SAAS,EAAQ,EAAgB,EAAuB,CACtD,OAAO,aAAiB,OAAS,SAAU,GAAS,EAAM,OAAS,CACrE,CAEA,SAAS,EAAgB,EAAwC,CAC/D,GAAI,CAAC,GAAS,OAAO,GAAU,SAAU,MAAO,GAChD,IAAM,EAAQ,EACd,OACE,EAAM,UAAY,GAClB,OAAO,EAAM,MAAS,UACtB,OAAO,EAAM,IAAO,WACnB,EAAM,OAAS,QAAU,EAAM,OAAS,SAAW,EAAM,OAAS,OAEvE,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,CACrC,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,IAAM,EAAS,IAAI,IACnB,IAAK,IAAM,KAAQ,EAAQ,MAAM;CAAI,EAAG,CACtC,GAAI,CAAC,EAAM,SACX,IAAI,EACJ,GAAI,CACF,EAAS,KAAK,MAAM,CAAI,CAC1B,OAAS,EAAO,CACd,QAAQ,KACN,+CAA+C,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,GACtG,EACA,QACF,CACA,GAAI,CAAC,EAAgB,CAAM,EAAG,SAC9B,IAAM,EAAU,EAAO,IAAI,EAAO,IAAI,EACtC,EAAO,IAAI,EAAO,KAAM,CACtB,KAAM,EAAO,KACb,KAAM,EAAO,KAAO,GAAS,IAAM,GAAK,EAAO,KAAQ,GAAS,MAAQ,EAAO,KAC/E,GAAI,KAAK,IAAI,EAAO,GAAI,GAAS,IAAM,CAAC,EACxC,OAAQ,GAAS,OAAS,GAAK,CACjC,CAAC,CACH,CACA,MAAO,CAAC,GAAG,EAAO,OAAO,CAAC,CAAC,CAAC,MAAM,EAAM,IAAU,EAAM,GAAK,EAAK,EAAE,CACtE,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,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,OACvC,IAAK,IAAI,EAAU,EAAG,EAAU,IAAc,GAAW,EACvD,GAAI,CACF,IAAM,EAAO,MAAM,EAAG,KAAK,EAAU,IAAI,EACzC,GAAI,CACF,OAAO,MAAM,EAAU,CACzB,QAAU,CACR,MAAM,EAAK,MAAM,EACjB,GAAI,CACF,MAAM,EAAG,OAAO,CAAQ,CAC1B,OAAS,EAAO,CACT,EAAQ,EAAO,QAAQ,GAAG,QAAQ,KAAK,mCAAmC,OAAO,CAAK,GAAG,CAChG,CACF,CACF,OAAS,EAAO,CACd,GAAI,CAAC,EAAQ,EAAO,QAAQ,EAAG,MAAM,EACrC,MAAM,IAAI,QAAS,GAAY,WAAW,EAAS,EAAa,CAAC,CACnE,CAEF,MAAU,MAAM,qCAAqC,GAAU,CACjE,CACF"}
1
+ {"version":3,"file":"TimelineStore.mjs","names":[],"sources":["../../../src/adapters/TimelineStore/TimelineStore.ts"],"sourcesContent":["import fs from 'node:fs/promises';\nimport { type AnyTimelineEvent, foldEntries, foldVersions, parseTimeline } from '../../services/fileChanges.ts';\nimport type { FileEditEntry, FileEditVersion, TimelineEvent } from '../../types/domain';\nimport type { ITimelineStore } from '../../types/timelineStore';\n\nconst LOCK_RETRY_MS = 10;\nconst LOCK_RETRIES = 100;\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 return foldEntries(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 for (let attempt = 0; attempt < LOCK_RETRIES; attempt += 1) {\n try {\n const lock = await fs.open(lockPath, 'wx');\n try {\n return await operation();\n } finally {\n await lock.close();\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 } catch (error) {\n if (!hasCode(error, 'EEXIST')) throw error;\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"],"mappings":"kIAQA,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,CACrC,OAAO,EAAY,MAAM,KAAK,OAAO,CAAC,CACxC,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,OACvC,IAAK,IAAI,EAAU,EAAG,EAAU,IAAc,GAAW,EACvD,GAAI,CACF,IAAM,EAAO,MAAM,EAAG,KAAK,EAAU,IAAI,EACzC,GAAI,CACF,OAAO,MAAM,EAAU,CACzB,QAAU,CACR,MAAM,EAAK,MAAM,EACjB,GAAI,CACF,MAAM,EAAG,OAAO,CAAQ,CAC1B,OAAS,EAAO,CACT,EAAQ,EAAO,QAAQ,GAAG,QAAQ,KAAK,mCAAmC,OAAO,CAAK,GAAG,CAChG,CACF,CACF,OAAS,EAAO,CACd,GAAI,CAAC,EAAQ,EAAO,QAAQ,EAAG,MAAM,EACrC,MAAM,IAAI,QAAS,GAAY,WAAW,EAAS,EAAa,CAAC,CACnE,CAEF,MAAU,MAAM,qCAAqC,GAAU,CACjE,CACF"}
@@ -0,0 +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}}async function v(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 y(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 b(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=>v(e,i))),cumulative:await y(n,a,i),working:a};return e.json(s)}),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 x={basePath:o.API_BASE_PATH,start(e){let t=b({...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=x,exports.createFileEditsApi=b;
2
+ //# sourceMappingURL=fileEditsApi.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fileEditsApi.cjs","names":["createHash","fs","lineDiff","baselineOf","TimelineStore","NodeSnapshotStoreAdapter","FileEditPaths","Hono","path","PATH_QUERY_PARAM","API_BASE_PATH"],"sources":["../../src/adapters/fileEditsApi.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport fs from 'node:fs/promises';\nimport path from 'node:path';\nimport type { DoomApi, DoomApiContext, DoomApiHandler } from '@agimon-ai/doompi-extension-contracts/package-api';\nimport { Hono } from 'hono';\nimport { baselineOf } from '../services/fileChanges.ts';\nimport { lineDiff } from '../services/lineDiff.ts';\nimport type { FileEditVersion } from '../types/domain.ts';\nimport {\n API_BASE_PATH,\n type FileEditsCumulativeView,\n type FileEditsDetailView,\n type FileEditsSaveRequest,\n type FileEditsSaveView,\n type FileEditsVersionView,\n type FileEditsWorkingView,\n PATH_QUERY_PARAM,\n} from '../types/fileEditsApi.ts';\nimport type { IFileEditPaths } from '../types/fileEditPaths.ts';\nimport type { SnapshotStorePort } from '../types/snapshotStore.ts';\nimport type { ITimelineStore } from '../types/timelineStore.ts';\nimport { FileEditPaths } from './FileEditPaths/FileEditPaths.ts';\nimport { NodeSnapshotStoreAdapter } from './node/snapshotStore.ts';\nimport { TimelineStore } from './TimelineStore/TimelineStore.ts';\n\n/**\n * This package's HTTP surface: one file's history, and the manual save.\n *\n * Routes are relative to the mount its host gives it, so nothing here repeats\n * where it was mounted. The host is one session's own server, which is what\n * makes the session id and working directory available without the page\n * supplying either: a page names a file, never a session's private paths.\n *\n * The detail route answers everything a reader needs to open a file, because\n * splitting it would make the page stitch three responses together for one\n * click. The save route is the only mutation, and it refuses unless the reader\n * proves they were looking at the content that is still on disk.\n */\n\n/** Past this, the working view reports the file rather than carrying it. */\nexport const MAX_WORKING_BYTES = 1024 * 1024;\n\nconst BINARY_SAMPLE_BYTES = 8192;\nconst NO_BASELINE_NOTE = 'changed by a command, so no baseline was captured; showing the file as it stands';\n/**\n * A change the tool layer named but never captured content for. That is what\n * every record written before this package learned to snapshot looks like, and\n * calling those \"changed by a command\" would be wrong: the tool is right there\n * in the record.\n */\nconst NO_SNAPSHOT_NOTE = 'recorded before this session began capturing content, so there is no diff for it';\nconst APPROXIMATE_NOTE = 'the changed region was too large to match line by line';\n\nexport interface FileEditsApiOptions {\n /** The session these routes answer for; absent when the host is the hub. */\n sessionId?: string;\n /** The session's working directory, which relative paths are shown against. */\n cwd?: string;\n timeline?: ITimelineStore;\n snapshots?: SnapshotStorePort;\n paths?: IFileEditPaths;\n}\n\nfunction hashOf(content: string): string {\n return createHash('sha256').update(content, 'utf8').digest('hex');\n}\n\nfunction hasCode(error: unknown, code: string): boolean {\n return error instanceof Error && 'code' in error && error.code === code;\n}\n\nfunction describe(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\n/** Reads the file as the source view will show it, or says why it cannot. */\nasync function readWorking(filePath: string): Promise<FileEditsWorkingView> {\n let raw: Buffer;\n try {\n raw = await fs.readFile(filePath);\n } catch {\n return { content: '', hash: '', unavailable: true, reason: 'the file no longer exists' };\n }\n if (raw.byteLength > MAX_WORKING_BYTES) {\n return { content: '', hash: '', unavailable: true, reason: `the file is larger than ${MAX_WORKING_BYTES} bytes` };\n }\n if (raw.subarray(0, BINARY_SAMPLE_BYTES).includes(0)) {\n return { content: '', hash: '', unavailable: true, reason: 'the file is binary' };\n }\n const content = raw.toString('utf8');\n return { content, hash: hashOf(content), unavailable: false };\n}\n\n/**\n * One recorded change as the page renders it. A change with both sides captured\n * carries its own diff; one without says why, rather than showing nothing.\n */\nasync function toVersionView(version: FileEditVersion, snapshots: SnapshotStorePort): Promise<FileEditsVersionView> {\n const view: FileEditsVersionView = {\n index: version.index,\n tool: version.tool,\n at: version.at,\n origin: version.origin,\n additions: version.additions ?? 0,\n removals: version.removals ?? 0,\n };\n if (version.before === undefined || version.after === undefined) {\n // A scan found the path only after it changed, so no baseline exists. A\n // tool named the path but the content was not stored: either it did not\n // fit, or the record predates content capture, which the tool tells apart.\n if (version.origin === 'tool') {\n return { ...view, note: 'this change could not be captured: the file is binary or past the size cap' };\n }\n return { ...view, note: version.tool === 'bash' ? NO_BASELINE_NOTE : NO_SNAPSHOT_NOTE };\n }\n const [before, after] = await Promise.all([snapshots.read(version.before), snapshots.read(version.after)]);\n if (before === undefined || after === undefined) {\n return { ...view, note: 'the content behind this change is no longer held' };\n }\n const diff = lineDiff(before, after);\n return {\n ...view,\n additions: diff.additions,\n removals: diff.removals,\n hunks: diff.hunks,\n ...(diff.approximate ? { note: APPROXIMATE_NOTE } : {}),\n };\n}\n\n/** Everything the session did to the file: its oldest baseline against what is on disk now. */\nasync function toCumulativeView(\n versions: readonly FileEditVersion[],\n working: FileEditsWorkingView,\n snapshots: SnapshotStorePort,\n): Promise<FileEditsCumulativeView> {\n const baseline = baselineOf(versions);\n if (baseline === undefined) return { additions: 0, removals: 0, note: NO_BASELINE_NOTE };\n if (working.unavailable) {\n return { additions: 0, removals: 0, note: working.reason ?? 'the file cannot be read' };\n }\n const before = await snapshots.read(baseline);\n if (before === undefined) {\n return { additions: 0, removals: 0, note: 'the content this session started from is no longer held' };\n }\n const diff = lineDiff(before, working.content);\n return {\n additions: diff.additions,\n removals: diff.removals,\n hunks: diff.hunks,\n ...(diff.approximate ? { note: APPROXIMATE_NOTE } : {}),\n };\n}\n\nexport function createFileEditsApi(options: FileEditsApiOptions = {}): Hono {\n const timeline = options.timeline ?? new TimelineStore();\n const snapshots = options.snapshots ?? new NodeSnapshotStoreAdapter();\n const paths = options.paths ?? new FileEditPaths();\n const cwd = options.cwd ?? '';\n const app = new Hono();\n\n // The routes and the extension run in different processes, so this half\n // opens the same two locations from the same inputs rather than sharing an\n // instance with the half that writes them.\n //\n // Only what this function built gets pointed at them. A store handed in\n // through the options arrives already configured, and repointing it would\n // make the option a lie: the caller would be injecting a store the routes\n // then read somewhere else entirely.\n if (options.sessionId !== undefined && cwd !== '') {\n const sessionKey = paths.sessionKey(options.sessionId);\n if (options.timeline === undefined) timeline.initialize(paths.timelinePath(cwd, sessionKey));\n if (options.snapshots === undefined) snapshots.initialize(paths.snapshotsPath(cwd, sessionKey));\n }\n\n /**\n * The file a request names, rejected unless this session actually recorded a\n * change to it. That check is the authorization: these routes read and write\n * whatever path they are handed, so the timeline is what bounds them to files\n * the session already touched.\n */\n const recordedVersions = async (filePath: string | undefined): Promise<FileEditVersion[] | undefined> => {\n if (filePath === undefined || filePath === '') return undefined;\n const versions = await timeline.versions(path.resolve(cwd, filePath));\n return versions.length === 0 ? undefined : versions;\n };\n\n app.get('/detail', async (context) => {\n const requested = context.req.query(PATH_QUERY_PARAM);\n const versions = await recordedVersions(requested);\n if (versions === undefined || requested === undefined) {\n return context.json({ error: 'This session recorded no changes to that file.' }, 404);\n }\n const filePath = path.resolve(cwd, requested);\n const working = await readWorking(filePath);\n const body: FileEditsDetailView = {\n path: filePath,\n relPath: cwd === '' ? filePath : path.relative(cwd, filePath),\n versions: await Promise.all(versions.map((version) => toVersionView(version, snapshots))),\n cumulative: await toCumulativeView(versions, working, snapshots),\n working,\n };\n return context.json(body);\n });\n\n app.put('/content', async (context) => {\n let request: FileEditsSaveRequest;\n try {\n request = (await context.req.json()) as FileEditsSaveRequest;\n } catch {\n return context.json({ error: 'The save body is not JSON.' }, 400);\n }\n if (typeof request.path !== 'string' || typeof request.content !== 'string') {\n return context.json({ error: 'A save names a path and the content to write.' }, 400);\n }\n if ((await recordedVersions(request.path)) === undefined) {\n return context.json({ error: 'This session recorded no changes to that file.' }, 404);\n }\n const filePath = path.resolve(cwd, request.path);\n const current = await readWorking(filePath);\n if (current.unavailable) {\n return context.json({ error: current.reason ?? 'The file cannot be read.' }, 409);\n }\n // The agent runs in the same working directory, so it can rewrite the file\n // while a reader is still editing it. Refusing on a moved hash is what\n // stops the save from silently discarding whatever it did.\n if (current.hash !== request.expectedHash) {\n return context.json({ error: 'The file changed since it was opened.', hash: current.hash }, 409);\n }\n await fs.writeFile(filePath, request.content, 'utf8');\n const body: FileEditsSaveView = { hash: hashOf(request.content) };\n return context.json(body);\n });\n\n app.delete('/content', async (context) => {\n const requested = context.req.query(PATH_QUERY_PARAM);\n if ((await recordedVersions(requested)) === undefined || requested === undefined) {\n return context.json({ error: 'This session recorded no changes to that file.' }, 404);\n }\n const filePath = path.resolve(cwd, requested);\n try {\n await fs.unlink(filePath);\n } catch (error) {\n // Already gone is the outcome the caller wanted, so it is not a failure.\n if (!hasCode(error, 'ENOENT')) {\n return context.json({ error: `The file could not be deleted: ${describe(error)}` }, 409);\n }\n }\n return context.body(null, 204);\n });\n\n return app;\n}\n\n/** The named export a host imports from this package's built entry. */\nexport const api: DoomApi = {\n basePath: API_BASE_PATH,\n start(context: DoomApiContext): DoomApiHandler {\n const app = createFileEditsApi({\n ...(context.sessionId === undefined ? {} : { sessionId: context.sessionId }),\n ...(context.cwd === undefined ? {} : { cwd: context.cwd }),\n });\n return {\n fetch: (request) => app.fetch(request),\n // Nothing outlives a request here: every route reads, answers and closes.\n close: () => undefined,\n };\n },\n};\n"],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import { IFileEditPaths } from "../types/fileEditPaths.cjs";
2
+ import { SnapshotStorePort } from "../types/snapshotStore.cjs";
3
+ import { ITimelineStore } from "../types/timelineStore.cjs";
4
+ import { DoomApi } from "@agimon-ai/doompi-extension-contracts/package-api";
5
+ import { Hono } from "hono";
6
+ //#region src/adapters/fileEditsApi.d.ts
7
+ interface FileEditsApiOptions {
8
+ /** The session these routes answer for; absent when the host is the hub. */
9
+ sessionId?: string;
10
+ /** The session's working directory, which relative paths are shown against. */
11
+ cwd?: string;
12
+ timeline?: ITimelineStore;
13
+ snapshots?: SnapshotStorePort;
14
+ paths?: IFileEditPaths;
15
+ }
16
+ declare function createFileEditsApi(options?: FileEditsApiOptions): Hono;
17
+ /** The named export a host imports from this package's built entry. */
18
+ declare const api: DoomApi;
19
+ //#endregion
20
+ export { FileEditsApiOptions, api, createFileEditsApi };
21
+ //# sourceMappingURL=fileEditsApi.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fileEditsApi.d.cts","names":[],"sources":["../../src/adapters/fileEditsApi.ts"],"mappings":";;;;;;UAqDiB;;EAEf;;EAEA;EACA,WAAW;EACX,YAAY;EACZ,QAAQ;;iBA6FM,mBAAmB,UAAS,sBAA2B;;cAqG1D,KAAK"}
@@ -0,0 +1,21 @@
1
+ import { IFileEditPaths } from "../types/fileEditPaths.mjs";
2
+ import { SnapshotStorePort } from "../types/snapshotStore.mjs";
3
+ import { ITimelineStore } from "../types/timelineStore.mjs";
4
+ import { Hono } from "hono";
5
+ import { DoomApi } from "@agimon-ai/doompi-extension-contracts/package-api";
6
+ //#region src/adapters/fileEditsApi.d.ts
7
+ interface FileEditsApiOptions {
8
+ /** The session these routes answer for; absent when the host is the hub. */
9
+ sessionId?: string;
10
+ /** The session's working directory, which relative paths are shown against. */
11
+ cwd?: string;
12
+ timeline?: ITimelineStore;
13
+ snapshots?: SnapshotStorePort;
14
+ paths?: IFileEditPaths;
15
+ }
16
+ declare function createFileEditsApi(options?: FileEditsApiOptions): Hono;
17
+ /** The named export a host imports from this package's built entry. */
18
+ declare const api: DoomApi;
19
+ //#endregion
20
+ export { FileEditsApiOptions, api, createFileEditsApi };
21
+ //# sourceMappingURL=fileEditsApi.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fileEditsApi.d.mts","names":[],"sources":["../../src/adapters/fileEditsApi.ts"],"mappings":";;;;;;UAqDiB;;EAEf;;EAEA;EACA,WAAW;EACX,YAAY;EACZ,QAAQ;;iBA6FM,mBAAmB,UAAS,sBAA2B;;cAqG1D,KAAK"}
@@ -0,0 +1,2 @@
1
+ import{lineDiff as e}from"../services/lineDiff.mjs";import{FileEditPaths as t}from"./FileEditPaths/FileEditPaths.mjs";import{NodeSnapshotStoreAdapter as n}from"./node/snapshotStore.mjs";import{baselineOf as r}from"../services/fileChanges.mjs";import{TimelineStore as i}from"./TimelineStore/TimelineStore.mjs";import{API_BASE_PATH as a,PATH_QUERY_PARAM as o}from"../types/fileEditsApi.mjs";import s from"node:fs/promises";import c from"node:path";import{createHash as l}from"node:crypto";import{Hono as u}from"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 l(`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.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}}async function v(t,n){let r={index:t.index,tool:t.tool,at:t.at,origin:t.origin,additions:t.additions??0,removals:t.removals??0};if(t.before===void 0||t.after===void 0)return t.origin===`tool`?{...r,note:`this change could not be captured: the file is binary or past the size cap`}:{...r,note:t.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(t.before),n.read(t.after)]);if(i===void 0||a===void 0)return{...r,note:`the content behind this change is no longer held`};let o=e(i,a);return{...r,additions:o.additions,removals:o.removals,hunks:o.hunks,...o.approximate?{note:p}:{}}}async function y(t,n,i){let a=r(t);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 i.read(a);if(o===void 0)return{additions:0,removals:0,note:`the content this session started from is no longer held`};let s=e(o,n.content);return{additions:s.additions,removals:s.removals,hunks:s.hunks,...s.approximate?{note:p}:{}}}function b(e={}){let r=e.timeline??new i,a=e.snapshots??new n,l=e.paths??new t,d=e.cwd??``,f=new u;if(e.sessionId!==void 0&&d!==``){let t=l.sessionKey(e.sessionId);e.timeline===void 0&&r.initialize(l.timelinePath(d,t)),e.snapshots===void 0&&a.initialize(l.snapshotsPath(d,t))}let p=async e=>{if(e===void 0||e===``)return;let t=await r.versions(c.resolve(d,e));return t.length===0?void 0:t};return f.get(`/detail`,async e=>{let t=e.req.query(o),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.resolve(d,t),i=await _(r),s={path:r,relPath:d===``?r:c.relative(d,r),versions:await Promise.all(n.map(e=>v(e,a))),cumulative:await y(n,i,a),working:i};return e.json(s)}),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.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.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);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.resolve(d,t);try{await s.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 x={basePath:a,start(e){let t=b({...e.sessionId===void 0?{}:{sessionId:e.sessionId},...e.cwd===void 0?{}:{cwd:e.cwd}});return{fetch:e=>t.fetch(e),close:()=>void 0}}};export{d as MAX_WORKING_BYTES,x as api,b as createFileEditsApi};
2
+ //# sourceMappingURL=fileEditsApi.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fileEditsApi.mjs","names":[],"sources":["../../src/adapters/fileEditsApi.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport fs from 'node:fs/promises';\nimport path from 'node:path';\nimport type { DoomApi, DoomApiContext, DoomApiHandler } from '@agimon-ai/doompi-extension-contracts/package-api';\nimport { Hono } from 'hono';\nimport { baselineOf } from '../services/fileChanges.ts';\nimport { lineDiff } from '../services/lineDiff.ts';\nimport type { FileEditVersion } from '../types/domain.ts';\nimport {\n API_BASE_PATH,\n type FileEditsCumulativeView,\n type FileEditsDetailView,\n type FileEditsSaveRequest,\n type FileEditsSaveView,\n type FileEditsVersionView,\n type FileEditsWorkingView,\n PATH_QUERY_PARAM,\n} from '../types/fileEditsApi.ts';\nimport type { IFileEditPaths } from '../types/fileEditPaths.ts';\nimport type { SnapshotStorePort } from '../types/snapshotStore.ts';\nimport type { ITimelineStore } from '../types/timelineStore.ts';\nimport { FileEditPaths } from './FileEditPaths/FileEditPaths.ts';\nimport { NodeSnapshotStoreAdapter } from './node/snapshotStore.ts';\nimport { TimelineStore } from './TimelineStore/TimelineStore.ts';\n\n/**\n * This package's HTTP surface: one file's history, and the manual save.\n *\n * Routes are relative to the mount its host gives it, so nothing here repeats\n * where it was mounted. The host is one session's own server, which is what\n * makes the session id and working directory available without the page\n * supplying either: a page names a file, never a session's private paths.\n *\n * The detail route answers everything a reader needs to open a file, because\n * splitting it would make the page stitch three responses together for one\n * click. The save route is the only mutation, and it refuses unless the reader\n * proves they were looking at the content that is still on disk.\n */\n\n/** Past this, the working view reports the file rather than carrying it. */\nexport const MAX_WORKING_BYTES = 1024 * 1024;\n\nconst BINARY_SAMPLE_BYTES = 8192;\nconst NO_BASELINE_NOTE = 'changed by a command, so no baseline was captured; showing the file as it stands';\n/**\n * A change the tool layer named but never captured content for. That is what\n * every record written before this package learned to snapshot looks like, and\n * calling those \"changed by a command\" would be wrong: the tool is right there\n * in the record.\n */\nconst NO_SNAPSHOT_NOTE = 'recorded before this session began capturing content, so there is no diff for it';\nconst APPROXIMATE_NOTE = 'the changed region was too large to match line by line';\n\nexport interface FileEditsApiOptions {\n /** The session these routes answer for; absent when the host is the hub. */\n sessionId?: string;\n /** The session's working directory, which relative paths are shown against. */\n cwd?: string;\n timeline?: ITimelineStore;\n snapshots?: SnapshotStorePort;\n paths?: IFileEditPaths;\n}\n\nfunction hashOf(content: string): string {\n return createHash('sha256').update(content, 'utf8').digest('hex');\n}\n\nfunction hasCode(error: unknown, code: string): boolean {\n return error instanceof Error && 'code' in error && error.code === code;\n}\n\nfunction describe(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\n/** Reads the file as the source view will show it, or says why it cannot. */\nasync function readWorking(filePath: string): Promise<FileEditsWorkingView> {\n let raw: Buffer;\n try {\n raw = await fs.readFile(filePath);\n } catch {\n return { content: '', hash: '', unavailable: true, reason: 'the file no longer exists' };\n }\n if (raw.byteLength > MAX_WORKING_BYTES) {\n return { content: '', hash: '', unavailable: true, reason: `the file is larger than ${MAX_WORKING_BYTES} bytes` };\n }\n if (raw.subarray(0, BINARY_SAMPLE_BYTES).includes(0)) {\n return { content: '', hash: '', unavailable: true, reason: 'the file is binary' };\n }\n const content = raw.toString('utf8');\n return { content, hash: hashOf(content), unavailable: false };\n}\n\n/**\n * One recorded change as the page renders it. A change with both sides captured\n * carries its own diff; one without says why, rather than showing nothing.\n */\nasync function toVersionView(version: FileEditVersion, snapshots: SnapshotStorePort): Promise<FileEditsVersionView> {\n const view: FileEditsVersionView = {\n index: version.index,\n tool: version.tool,\n at: version.at,\n origin: version.origin,\n additions: version.additions ?? 0,\n removals: version.removals ?? 0,\n };\n if (version.before === undefined || version.after === undefined) {\n // A scan found the path only after it changed, so no baseline exists. A\n // tool named the path but the content was not stored: either it did not\n // fit, or the record predates content capture, which the tool tells apart.\n if (version.origin === 'tool') {\n return { ...view, note: 'this change could not be captured: the file is binary or past the size cap' };\n }\n return { ...view, note: version.tool === 'bash' ? NO_BASELINE_NOTE : NO_SNAPSHOT_NOTE };\n }\n const [before, after] = await Promise.all([snapshots.read(version.before), snapshots.read(version.after)]);\n if (before === undefined || after === undefined) {\n return { ...view, note: 'the content behind this change is no longer held' };\n }\n const diff = lineDiff(before, after);\n return {\n ...view,\n additions: diff.additions,\n removals: diff.removals,\n hunks: diff.hunks,\n ...(diff.approximate ? { note: APPROXIMATE_NOTE } : {}),\n };\n}\n\n/** Everything the session did to the file: its oldest baseline against what is on disk now. */\nasync function toCumulativeView(\n versions: readonly FileEditVersion[],\n working: FileEditsWorkingView,\n snapshots: SnapshotStorePort,\n): Promise<FileEditsCumulativeView> {\n const baseline = baselineOf(versions);\n if (baseline === undefined) return { additions: 0, removals: 0, note: NO_BASELINE_NOTE };\n if (working.unavailable) {\n return { additions: 0, removals: 0, note: working.reason ?? 'the file cannot be read' };\n }\n const before = await snapshots.read(baseline);\n if (before === undefined) {\n return { additions: 0, removals: 0, note: 'the content this session started from is no longer held' };\n }\n const diff = lineDiff(before, working.content);\n return {\n additions: diff.additions,\n removals: diff.removals,\n hunks: diff.hunks,\n ...(diff.approximate ? { note: APPROXIMATE_NOTE } : {}),\n };\n}\n\nexport function createFileEditsApi(options: FileEditsApiOptions = {}): Hono {\n const timeline = options.timeline ?? new TimelineStore();\n const snapshots = options.snapshots ?? new NodeSnapshotStoreAdapter();\n const paths = options.paths ?? new FileEditPaths();\n const cwd = options.cwd ?? '';\n const app = new Hono();\n\n // The routes and the extension run in different processes, so this half\n // opens the same two locations from the same inputs rather than sharing an\n // instance with the half that writes them.\n //\n // Only what this function built gets pointed at them. A store handed in\n // through the options arrives already configured, and repointing it would\n // make the option a lie: the caller would be injecting a store the routes\n // then read somewhere else entirely.\n if (options.sessionId !== undefined && cwd !== '') {\n const sessionKey = paths.sessionKey(options.sessionId);\n if (options.timeline === undefined) timeline.initialize(paths.timelinePath(cwd, sessionKey));\n if (options.snapshots === undefined) snapshots.initialize(paths.snapshotsPath(cwd, sessionKey));\n }\n\n /**\n * The file a request names, rejected unless this session actually recorded a\n * change to it. That check is the authorization: these routes read and write\n * whatever path they are handed, so the timeline is what bounds them to files\n * the session already touched.\n */\n const recordedVersions = async (filePath: string | undefined): Promise<FileEditVersion[] | undefined> => {\n if (filePath === undefined || filePath === '') return undefined;\n const versions = await timeline.versions(path.resolve(cwd, filePath));\n return versions.length === 0 ? undefined : versions;\n };\n\n app.get('/detail', async (context) => {\n const requested = context.req.query(PATH_QUERY_PARAM);\n const versions = await recordedVersions(requested);\n if (versions === undefined || requested === undefined) {\n return context.json({ error: 'This session recorded no changes to that file.' }, 404);\n }\n const filePath = path.resolve(cwd, requested);\n const working = await readWorking(filePath);\n const body: FileEditsDetailView = {\n path: filePath,\n relPath: cwd === '' ? filePath : path.relative(cwd, filePath),\n versions: await Promise.all(versions.map((version) => toVersionView(version, snapshots))),\n cumulative: await toCumulativeView(versions, working, snapshots),\n working,\n };\n return context.json(body);\n });\n\n app.put('/content', async (context) => {\n let request: FileEditsSaveRequest;\n try {\n request = (await context.req.json()) as FileEditsSaveRequest;\n } catch {\n return context.json({ error: 'The save body is not JSON.' }, 400);\n }\n if (typeof request.path !== 'string' || typeof request.content !== 'string') {\n return context.json({ error: 'A save names a path and the content to write.' }, 400);\n }\n if ((await recordedVersions(request.path)) === undefined) {\n return context.json({ error: 'This session recorded no changes to that file.' }, 404);\n }\n const filePath = path.resolve(cwd, request.path);\n const current = await readWorking(filePath);\n if (current.unavailable) {\n return context.json({ error: current.reason ?? 'The file cannot be read.' }, 409);\n }\n // The agent runs in the same working directory, so it can rewrite the file\n // while a reader is still editing it. Refusing on a moved hash is what\n // stops the save from silently discarding whatever it did.\n if (current.hash !== request.expectedHash) {\n return context.json({ error: 'The file changed since it was opened.', hash: current.hash }, 409);\n }\n await fs.writeFile(filePath, request.content, 'utf8');\n const body: FileEditsSaveView = { hash: hashOf(request.content) };\n return context.json(body);\n });\n\n app.delete('/content', async (context) => {\n const requested = context.req.query(PATH_QUERY_PARAM);\n if ((await recordedVersions(requested)) === undefined || requested === undefined) {\n return context.json({ error: 'This session recorded no changes to that file.' }, 404);\n }\n const filePath = path.resolve(cwd, requested);\n try {\n await fs.unlink(filePath);\n } catch (error) {\n // Already gone is the outcome the caller wanted, so it is not a failure.\n if (!hasCode(error, 'ENOENT')) {\n return context.json({ error: `The file could not be deleted: ${describe(error)}` }, 409);\n }\n }\n return context.body(null, 204);\n });\n\n return app;\n}\n\n/** The named export a host imports from this package's built entry. */\nexport const api: DoomApi = {\n basePath: API_BASE_PATH,\n start(context: DoomApiContext): DoomApiHandler {\n const app = createFileEditsApi({\n ...(context.sessionId === undefined ? {} : { sessionId: context.sessionId }),\n ...(context.cwd === undefined ? {} : { cwd: context.cwd }),\n });\n return {\n fetch: (request) => app.fetch(request),\n // Nothing outlives a request here: every route reads, answers and closes.\n close: () => undefined,\n };\n },\n};\n"],"mappings":"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"}
@@ -0,0 +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:path");n=e.__toESM(n,1);let r=require("node:crypto");const i=new Set([`ENOENT`,`ENOTDIR`,`EISDIR`,`ENAMETOOLONG`]);function a(e,t){return e instanceof Error&&`code`in e&&typeof e.code==`string`&&t.has(e.code)}var o=class{directory;initialize(e){this.directory=e}async capture(e){let n;try{n=await t.default.readFile(e)}catch(e){if(a(e,i))return;throw e}if(!(n.byteLength>1048576)&&!n.subarray(0,8192).includes(0))return this.put(n.toString(`utf8`))}async put(e){let o=(0,r.createHash)(`sha256`).update(e,`utf8`).digest(`hex`),s=n.default.join(this.requireDirectory(),o);try{return await t.default.access(s),o}catch(e){if(!a(e,i))throw e}return await t.default.mkdir(this.requireDirectory(),{recursive:!0}),await t.default.writeFile(s,e,`utf8`),o}async read(e){if(/^[0-9a-f]{64}$/u.test(e))try{return await t.default.readFile(n.default.join(this.requireDirectory(),e),`utf8`)}catch(e){if(a(e,i))return;throw e}}async clear(){this.directory!==void 0&&await t.default.rm(this.directory,{recursive:!0,force:!0})}requireDirectory(){if(this.directory===void 0)throw Error(`Snapshot store is not initialized`);return this.directory}};exports.MAX_SNAPSHOT_BYTES=1048576,exports.NodeSnapshotStoreAdapter=o;
2
+ //# sourceMappingURL=snapshotStore.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshotStore.cjs","names":["fs","createHash","path"],"sources":["../../../src/adapters/node/snapshotStore.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport fs from 'node:fs/promises';\nimport path from 'node:path';\nimport type { SnapshotStorePort } from '../../types/snapshotStore.ts';\n\n/**\n * Store and read the file content this session's diffs are built from.\n *\n * One technology per adapter. This is where node enters the package; the\n * services that depend on the capability keep importing the port.\n *\n * Snapshots are content-addressed, so a file written twice with the same bytes\n * occupies one blob and the second capture is a hash and an exists check. The\n * whole directory goes when the session's timeline does, which is what keeps\n * this from growing without bound across a machine's lifetime.\n */\n\n/** Past this, a file is listed as changed without content rather than snapshotted. */\nexport const MAX_SNAPSHOT_BYTES = 1024 * 1024;\n\n/** How much of a file is read before deciding it is binary. */\nconst BINARY_SAMPLE_BYTES = 8192;\n\nconst MISSING_CODES = new Set(['ENOENT', 'ENOTDIR', 'EISDIR', 'ENAMETOOLONG']);\n\nfunction hasCode(error: unknown, codes: ReadonlySet<string>): boolean {\n return error instanceof Error && 'code' in error && typeof error.code === 'string' && codes.has(error.code);\n}\n\nexport class NodeSnapshotStoreAdapter implements SnapshotStorePort {\n private directory: string | undefined;\n\n initialize(directory: string): void {\n this.directory = directory;\n }\n\n async capture(filePath: string): Promise<string | undefined> {\n let raw: Buffer;\n try {\n raw = await fs.readFile(filePath);\n } catch (error) {\n if (hasCode(error, MISSING_CODES)) return undefined;\n throw error;\n }\n if (raw.byteLength > MAX_SNAPSHOT_BYTES) return undefined;\n if (raw.subarray(0, BINARY_SAMPLE_BYTES).includes(0)) return undefined;\n return this.put(raw.toString('utf8'));\n }\n\n async put(content: string): Promise<string> {\n const hash = createHash('sha256').update(content, 'utf8').digest('hex');\n const blobPath = path.join(this.requireDirectory(), hash);\n // An existing blob already holds exactly this content, by construction, so\n // rewriting it would only cost a write.\n try {\n await fs.access(blobPath);\n return hash;\n } catch (error) {\n if (!hasCode(error, MISSING_CODES)) throw error;\n }\n await fs.mkdir(this.requireDirectory(), { recursive: true });\n await fs.writeFile(blobPath, content, 'utf8');\n return hash;\n }\n\n async read(hash: string): Promise<string | undefined> {\n if (!/^[0-9a-f]{64}$/u.test(hash)) return undefined;\n try {\n return await fs.readFile(path.join(this.requireDirectory(), hash), 'utf8');\n } catch (error) {\n if (hasCode(error, MISSING_CODES)) return undefined;\n throw error;\n }\n }\n\n async clear(): Promise<void> {\n if (this.directory === undefined) return;\n await fs.rm(this.directory, { recursive: true, force: true });\n }\n\n private requireDirectory(): string {\n if (this.directory === undefined) throw new Error('Snapshot store is not initialized');\n return this.directory;\n }\n}\n"],"mappings":"oLAkBA,MAKM,EAAgB,IAAI,IAAI,CAAC,SAAU,UAAW,SAAU,cAAc,CAAC,EAE7E,SAAS,EAAQ,EAAgB,EAAqC,CACpE,OAAO,aAAiB,OAAS,SAAU,GAAS,OAAO,EAAM,MAAS,UAAY,EAAM,IAAI,EAAM,IAAI,CAC5G,CAEA,IAAa,EAAb,KAAmE,CACjE,UAEA,WAAW,EAAyB,CAClC,KAAK,UAAY,CACnB,CAEA,MAAM,QAAQ,EAA+C,CAC3D,IAAI,EACJ,GAAI,CACF,EAAM,MAAMA,EAAAA,QAAG,SAAS,CAAQ,CAClC,OAAS,EAAO,CACd,GAAI,EAAQ,EAAO,CAAa,EAAG,OACnC,MAAM,CACR,CACI,OAAI,WAAA,UACJ,GAAI,SAAS,EAAG,IAAmB,CAAC,CAAC,SAAS,CAAC,EACnD,OAAO,KAAK,IAAI,EAAI,SAAS,MAAM,CAAC,CACtC,CAEA,MAAM,IAAI,EAAkC,CAC1C,IAAM,GAAA,EAAOC,EAAAA,WAAAA,CAAW,QAAQ,CAAC,CAAC,OAAO,EAAS,MAAM,CAAC,CAAC,OAAO,KAAK,EAChE,EAAWC,EAAAA,QAAK,KAAK,KAAK,iBAAiB,EAAG,CAAI,EAGxD,GAAI,CAEF,OADA,MAAMF,EAAAA,QAAG,OAAO,CAAQ,EACjB,CACT,OAAS,EAAO,CACd,GAAI,CAAC,EAAQ,EAAO,CAAa,EAAG,MAAM,CAC5C,CAGA,OAFA,MAAMA,EAAAA,QAAG,MAAM,KAAK,iBAAiB,EAAG,CAAE,UAAW,EAAK,CAAC,EAC3D,MAAMA,EAAAA,QAAG,UAAU,EAAU,EAAS,MAAM,EACrC,CACT,CAEA,MAAM,KAAK,EAA2C,CAC/C,qBAAkB,KAAK,CAAI,EAChC,GAAI,CACF,OAAO,MAAMA,EAAAA,QAAG,SAASE,EAAAA,QAAK,KAAK,KAAK,iBAAiB,EAAG,CAAI,EAAG,MAAM,CAC3E,OAAS,EAAO,CACd,GAAI,EAAQ,EAAO,CAAa,EAAG,OACnC,MAAM,CACR,CACF,CAEA,MAAM,OAAuB,CACvB,KAAK,YAAc,IAAA,IACvB,MAAMF,EAAAA,QAAG,GAAG,KAAK,UAAW,CAAE,UAAW,GAAM,MAAO,EAAK,CAAC,CAC9D,CAEA,kBAAmC,CACjC,GAAI,KAAK,YAAc,IAAA,GAAW,MAAU,MAAM,mCAAmC,EACrF,OAAO,KAAK,SACd,CACF"}
@@ -0,0 +1,2 @@
1
+ import e from"node:fs/promises";import t from"node:path";import{createHash as n}from"node:crypto";const r=1048576,i=new Set([`ENOENT`,`ENOTDIR`,`EISDIR`,`ENAMETOOLONG`]);function a(e,t){return e instanceof Error&&`code`in e&&typeof e.code==`string`&&t.has(e.code)}var o=class{directory;initialize(e){this.directory=e}async capture(t){let n;try{n=await e.readFile(t)}catch(e){if(a(e,i))return;throw e}if(!(n.byteLength>1048576)&&!n.subarray(0,8192).includes(0))return this.put(n.toString(`utf8`))}async put(r){let o=n(`sha256`).update(r,`utf8`).digest(`hex`),s=t.join(this.requireDirectory(),o);try{return await e.access(s),o}catch(e){if(!a(e,i))throw e}return await e.mkdir(this.requireDirectory(),{recursive:!0}),await e.writeFile(s,r,`utf8`),o}async read(n){if(/^[0-9a-f]{64}$/u.test(n))try{return await e.readFile(t.join(this.requireDirectory(),n),`utf8`)}catch(e){if(a(e,i))return;throw e}}async clear(){this.directory!==void 0&&await e.rm(this.directory,{recursive:!0,force:!0})}requireDirectory(){if(this.directory===void 0)throw Error(`Snapshot store is not initialized`);return this.directory}};export{r as MAX_SNAPSHOT_BYTES,o as NodeSnapshotStoreAdapter};
2
+ //# sourceMappingURL=snapshotStore.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshotStore.mjs","names":[],"sources":["../../../src/adapters/node/snapshotStore.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport fs from 'node:fs/promises';\nimport path from 'node:path';\nimport type { SnapshotStorePort } from '../../types/snapshotStore.ts';\n\n/**\n * Store and read the file content this session's diffs are built from.\n *\n * One technology per adapter. This is where node enters the package; the\n * services that depend on the capability keep importing the port.\n *\n * Snapshots are content-addressed, so a file written twice with the same bytes\n * occupies one blob and the second capture is a hash and an exists check. The\n * whole directory goes when the session's timeline does, which is what keeps\n * this from growing without bound across a machine's lifetime.\n */\n\n/** Past this, a file is listed as changed without content rather than snapshotted. */\nexport const MAX_SNAPSHOT_BYTES = 1024 * 1024;\n\n/** How much of a file is read before deciding it is binary. */\nconst BINARY_SAMPLE_BYTES = 8192;\n\nconst MISSING_CODES = new Set(['ENOENT', 'ENOTDIR', 'EISDIR', 'ENAMETOOLONG']);\n\nfunction hasCode(error: unknown, codes: ReadonlySet<string>): boolean {\n return error instanceof Error && 'code' in error && typeof error.code === 'string' && codes.has(error.code);\n}\n\nexport class NodeSnapshotStoreAdapter implements SnapshotStorePort {\n private directory: string | undefined;\n\n initialize(directory: string): void {\n this.directory = directory;\n }\n\n async capture(filePath: string): Promise<string | undefined> {\n let raw: Buffer;\n try {\n raw = await fs.readFile(filePath);\n } catch (error) {\n if (hasCode(error, MISSING_CODES)) return undefined;\n throw error;\n }\n if (raw.byteLength > MAX_SNAPSHOT_BYTES) return undefined;\n if (raw.subarray(0, BINARY_SAMPLE_BYTES).includes(0)) return undefined;\n return this.put(raw.toString('utf8'));\n }\n\n async put(content: string): Promise<string> {\n const hash = createHash('sha256').update(content, 'utf8').digest('hex');\n const blobPath = path.join(this.requireDirectory(), hash);\n // An existing blob already holds exactly this content, by construction, so\n // rewriting it would only cost a write.\n try {\n await fs.access(blobPath);\n return hash;\n } catch (error) {\n if (!hasCode(error, MISSING_CODES)) throw error;\n }\n await fs.mkdir(this.requireDirectory(), { recursive: true });\n await fs.writeFile(blobPath, content, 'utf8');\n return hash;\n }\n\n async read(hash: string): Promise<string | undefined> {\n if (!/^[0-9a-f]{64}$/u.test(hash)) return undefined;\n try {\n return await fs.readFile(path.join(this.requireDirectory(), hash), 'utf8');\n } catch (error) {\n if (hasCode(error, MISSING_CODES)) return undefined;\n throw error;\n }\n }\n\n async clear(): Promise<void> {\n if (this.directory === undefined) return;\n await fs.rm(this.directory, { recursive: true, force: true });\n }\n\n private requireDirectory(): string {\n if (this.directory === undefined) throw new Error('Snapshot store is not initialized');\n return this.directory;\n }\n}\n"],"mappings":"kGAkBA,MAAa,EAAqB,QAK5B,EAAgB,IAAI,IAAI,CAAC,SAAU,UAAW,SAAU,cAAc,CAAC,EAE7E,SAAS,EAAQ,EAAgB,EAAqC,CACpE,OAAO,aAAiB,OAAS,SAAU,GAAS,OAAO,EAAM,MAAS,UAAY,EAAM,IAAI,EAAM,IAAI,CAC5G,CAEA,IAAa,EAAb,KAAmE,CACjE,UAEA,WAAW,EAAyB,CAClC,KAAK,UAAY,CACnB,CAEA,MAAM,QAAQ,EAA+C,CAC3D,IAAI,EACJ,GAAI,CACF,EAAM,MAAM,EAAG,SAAS,CAAQ,CAClC,OAAS,EAAO,CACd,GAAI,EAAQ,EAAO,CAAa,EAAG,OACnC,MAAM,CACR,CACI,OAAI,WAAA,UACJ,GAAI,SAAS,EAAG,IAAmB,CAAC,CAAC,SAAS,CAAC,EACnD,OAAO,KAAK,IAAI,EAAI,SAAS,MAAM,CAAC,CACtC,CAEA,MAAM,IAAI,EAAkC,CAC1C,IAAM,EAAO,EAAW,QAAQ,CAAC,CAAC,OAAO,EAAS,MAAM,CAAC,CAAC,OAAO,KAAK,EAChE,EAAW,EAAK,KAAK,KAAK,iBAAiB,EAAG,CAAI,EAGxD,GAAI,CAEF,OADA,MAAM,EAAG,OAAO,CAAQ,EACjB,CACT,OAAS,EAAO,CACd,GAAI,CAAC,EAAQ,EAAO,CAAa,EAAG,MAAM,CAC5C,CAGA,OAFA,MAAM,EAAG,MAAM,KAAK,iBAAiB,EAAG,CAAE,UAAW,EAAK,CAAC,EAC3D,MAAM,EAAG,UAAU,EAAU,EAAS,MAAM,EACrC,CACT,CAEA,MAAM,KAAK,EAA2C,CAC/C,qBAAkB,KAAK,CAAI,EAChC,GAAI,CACF,OAAO,MAAM,EAAG,SAAS,EAAK,KAAK,KAAK,iBAAiB,EAAG,CAAI,EAAG,MAAM,CAC3E,OAAS,EAAO,CACd,GAAI,EAAQ,EAAO,CAAa,EAAG,OACnC,MAAM,CACR,CACF,CAEA,MAAM,OAAuB,CACvB,KAAK,YAAc,IAAA,IACvB,MAAM,EAAG,GAAG,KAAK,UAAW,CAAE,UAAW,GAAM,MAAO,EAAK,CAAC,CAC9D,CAEA,kBAAmC,CACjC,GAAI,KAAK,YAAc,IAAA,GAAW,MAAU,MAAM,mCAAmC,EACrF,OAAO,KAAK,SACd,CACF"}
@@ -0,0 +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:path");n=e.__toESM(n,1);const r=new Set([`.git`,`.hg`,`.svn`,`.cache`,`.gradle`,`.next`,`.nuxt`,`.nx`,`.pnpm-store`,`.turbo`,`.venv`,`__pycache__`,`build`,`coverage`,`dist`,`node_modules`,`out`,`target`,`vendor`,`venv`]);function i(e,t){return`${e}:${t}`}var a=class{maxEntries;maxDepth;ignored;constructor(e={}){this.maxEntries=e.maxEntries??2e4,this.maxDepth=e.maxDepth??12,this.ignored=e.ignoredDirectories??r}async take(e,t=[]){let r=new Map,i=new Set(t.map(e=>n.default.resolve(e)));return{entries:r,truncated:await this.walk(e,0,r,i)}}async fingerprint(e){try{let n=await t.default.stat(e);return n.isFile()?i(n.size,n.mtimeMs):void 0}catch{return}}changed(e,t){let n=new Set;for(let[r,i]of t.entries)e.entries.get(r)!==i&&n.add(r);for(let r of e.entries.keys())t.entries.has(r)||n.add(r);return[...n].sort()}async walk(e,r,a,o){if(r>this.maxDepth)return!0;let s;try{s=await t.default.readdir(e,{withFileTypes:!0})}catch{return!1}let c=!1;for(let l of s){if(a.size>=this.maxEntries)return!0;let s=n.default.join(e,l.name);if(!o.has(s)&&!l.isSymbolicLink()){if(l.isDirectory()){if(this.ignored.has(l.name))continue;c=await this.walk(s,r+1,a,o)||c;continue}if(l.isFile())try{let e=await t.default.stat(s);a.set(s,i(e.size,e.mtimeMs))}catch{}}}return c}};exports.IGNORED_DIRECTORIES=r,exports.MAX_ENTRIES=2e4,exports.NodeTreeManifestAdapter=a;
2
+ //# sourceMappingURL=treeManifest.cjs.map