@agimon-ai/doompi-file-edit 0.0.1-alpha.7 → 0.0.1-alpha.71
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +115 -18
- package/dist/api-contracts.cjs +1 -0
- package/dist/api-contracts.d.cts +2 -0
- package/dist/api-contracts.d.mts +2 -0
- package/dist/api-contracts.mjs +1 -0
- package/dist/constants/package.cjs +2 -0
- package/dist/constants/package.cjs.map +1 -0
- package/dist/constants/package.mjs +2 -0
- package/dist/constants/package.mjs.map +1 -0
- package/dist/controllers/fileEditRuntime.cjs +2 -0
- package/dist/controllers/fileEditRuntime.cjs.map +1 -0
- package/dist/controllers/fileEditRuntime.mjs +2 -0
- package/dist/controllers/fileEditRuntime.mjs.map +1 -0
- package/dist/controllers/fileEditSession.cjs +2 -0
- package/dist/controllers/fileEditSession.cjs.map +1 -0
- package/dist/controllers/fileEditSession.mjs +2 -0
- package/dist/controllers/fileEditSession.mjs.map +1 -0
- package/dist/controllers/fileEditsApi.cjs +2 -0
- package/dist/controllers/fileEditsApi.cjs.map +1 -0
- package/dist/controllers/fileEditsApi.mjs +2 -0
- package/dist/controllers/fileEditsApi.mjs.map +1 -0
- package/dist/controllers/webFilesChannel.cjs +2 -0
- package/dist/controllers/webFilesChannel.cjs.map +1 -0
- package/dist/controllers/webFilesChannel.mjs +2 -0
- package/dist/controllers/webFilesChannel.mjs.map +1 -0
- package/dist/extensions/pi.cjs +2 -1
- package/dist/extensions/pi.cjs.map +1 -0
- package/dist/extensions/pi.d.cts +5 -2
- package/dist/extensions/pi.d.cts.map +1 -0
- package/dist/extensions/pi.d.mts +5 -2
- package/dist/extensions/pi.d.mts.map +1 -0
- package/dist/extensions/pi.mjs +2 -1
- package/dist/extensions/pi.mjs.map +1 -0
- package/dist/extensions/server.cjs +2 -0
- package/dist/extensions/server.cjs.map +1 -0
- package/dist/extensions/server.d.cts +5 -0
- package/dist/extensions/server.d.cts.map +1 -0
- package/dist/extensions/server.d.mts +5 -0
- package/dist/extensions/server.d.mts.map +1 -0
- package/dist/extensions/server.mjs +2 -0
- package/dist/extensions/server.mjs.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +4 -5
- package/dist/index.d.mts +4 -5
- package/dist/index.mjs +1 -1
- package/dist/schemas/apiContracts.cjs +2 -0
- package/dist/schemas/apiContracts.cjs.map +1 -0
- package/dist/schemas/apiContracts.d.cts +53 -0
- package/dist/schemas/apiContracts.d.cts.map +1 -0
- package/dist/schemas/apiContracts.d.mts +53 -0
- package/dist/schemas/apiContracts.d.mts.map +1 -0
- package/dist/schemas/apiContracts.mjs +2 -0
- package/dist/schemas/apiContracts.mjs.map +1 -0
- package/dist/{commandLine.cjs → services/commandLine/index.cjs} +1 -1
- package/dist/services/commandLine/index.cjs.map +1 -0
- package/dist/{commandLine.mjs → services/commandLine/index.mjs} +1 -1
- package/dist/services/commandLine/index.mjs.map +1 -0
- package/dist/services/doomIgnore/index.cjs +2 -0
- package/dist/services/doomIgnore/index.cjs.map +1 -0
- package/dist/services/doomIgnore/index.mjs +2 -0
- package/dist/services/doomIgnore/index.mjs.map +1 -0
- package/dist/services/editTracker/index.cjs +2 -0
- package/dist/services/editTracker/index.cjs.map +1 -0
- package/dist/services/editTracker/index.mjs +2 -0
- package/dist/services/editTracker/index.mjs.map +1 -0
- package/dist/services/editorConfigService/index.cjs +2 -0
- package/dist/services/editorConfigService/index.cjs.map +1 -0
- package/dist/services/editorConfigService/index.mjs +2 -0
- package/dist/services/editorConfigService/index.mjs.map +1 -0
- package/dist/services/editorLauncher/index.cjs +2 -0
- package/dist/services/editorLauncher/index.cjs.map +1 -0
- package/dist/services/editorLauncher/index.mjs +2 -0
- package/dist/services/editorLauncher/index.mjs.map +1 -0
- package/dist/services/fileChanges/index.cjs +3 -0
- package/dist/services/fileChanges/index.cjs.map +1 -0
- package/dist/services/fileChanges/index.mjs +3 -0
- package/dist/services/fileChanges/index.mjs.map +1 -0
- package/dist/services/fileEditPaths/index.cjs +2 -0
- package/dist/services/fileEditPaths/index.cjs.map +1 -0
- package/dist/services/fileEditPaths/index.mjs +2 -0
- package/dist/services/fileEditPaths/index.mjs.map +1 -0
- package/dist/services/gitDiffService/index.cjs +5 -0
- package/dist/services/gitDiffService/index.cjs.map +1 -0
- package/dist/services/gitDiffService/index.mjs +5 -0
- package/dist/services/gitDiffService/index.mjs.map +1 -0
- package/dist/services/gitStatus/index.cjs +2 -0
- package/dist/services/gitStatus/index.cjs.map +1 -0
- package/dist/services/gitStatus/index.mjs +2 -0
- package/dist/services/gitStatus/index.mjs.map +1 -0
- package/dist/services/lineDiff/index.cjs +7 -0
- package/dist/services/lineDiff/index.cjs.map +1 -0
- package/dist/services/lineDiff/index.mjs +7 -0
- package/dist/services/lineDiff/index.mjs.map +1 -0
- package/dist/services/sessionStateSweep/index.cjs +2 -0
- package/dist/services/sessionStateSweep/index.cjs.map +1 -0
- package/dist/services/sessionStateSweep/index.mjs +2 -0
- package/dist/services/sessionStateSweep/index.mjs.map +1 -0
- package/dist/services/snapshotStore/index.cjs +2 -0
- package/dist/services/snapshotStore/index.cjs.map +1 -0
- package/dist/services/snapshotStore/index.mjs +2 -0
- package/dist/services/snapshotStore/index.mjs.map +1 -0
- package/dist/services/timelineStore/index.cjs +2 -0
- package/dist/services/timelineStore/index.cjs.map +1 -0
- package/dist/services/timelineStore/index.mjs +2 -0
- package/dist/services/timelineStore/index.mjs.map +1 -0
- package/dist/services/treeManifest/index.cjs +2 -0
- package/dist/services/treeManifest/index.cjs.map +1 -0
- package/dist/services/treeManifest/index.mjs +2 -0
- package/dist/services/treeManifest/index.mjs.map +1 -0
- package/dist/tui/fileEditDependencies.cjs +2 -0
- package/dist/tui/fileEditDependencies.cjs.map +1 -0
- package/dist/tui/fileEditDependencies.d.cts +12 -0
- package/dist/tui/fileEditDependencies.d.cts.map +1 -0
- package/dist/tui/fileEditDependencies.d.mts +12 -0
- package/dist/tui/fileEditDependencies.d.mts.map +1 -0
- package/dist/tui/fileEditDependencies.mjs +2 -0
- package/dist/tui/fileEditDependencies.mjs.map +1 -0
- package/dist/tui/fileEditOverlay.cjs +1 -1
- package/dist/tui/fileEditOverlay.cjs.map +1 -1
- package/dist/tui/fileEditOverlay.mjs +1 -1
- package/dist/tui/fileEditOverlay.mjs.map +1 -1
- package/dist/tui/fileEditWorkflow.cjs +2 -0
- package/dist/tui/fileEditWorkflow.cjs.map +1 -0
- package/dist/tui/fileEditWorkflow.mjs +2 -0
- package/dist/tui/fileEditWorkflow.mjs.map +1 -0
- package/dist/types/domain.d.cts +91 -0
- package/dist/types/domain.d.cts.map +1 -0
- package/dist/types/domain.d.mts +91 -0
- package/dist/types/domain.d.mts.map +1 -0
- package/dist/types/editTracker.d.cts +23 -0
- package/dist/types/editTracker.d.cts.map +1 -0
- package/dist/types/editTracker.d.mts +23 -0
- package/dist/types/editTracker.d.mts.map +1 -0
- package/dist/types/editorConfigService.d.cts +11 -0
- package/dist/types/editorConfigService.d.cts.map +1 -0
- package/dist/types/editorConfigService.d.mts +11 -0
- package/dist/types/editorConfigService.d.mts.map +1 -0
- package/dist/types/editorLauncher.d.cts +17 -0
- package/dist/types/editorLauncher.d.cts.map +1 -0
- package/dist/types/editorLauncher.d.mts +17 -0
- package/dist/types/editorLauncher.d.mts.map +1 -0
- package/dist/types/fileEditPaths.d.cts +20 -0
- package/dist/types/fileEditPaths.d.cts.map +1 -0
- package/dist/types/fileEditPaths.d.mts +20 -0
- package/dist/types/fileEditPaths.d.mts.map +1 -0
- package/dist/types/fileEditWorkflow.d.cts +7 -0
- package/dist/types/fileEditWorkflow.d.cts.map +1 -0
- package/dist/types/fileEditWorkflow.d.mts +7 -0
- package/dist/types/fileEditWorkflow.d.mts.map +1 -0
- package/dist/types/fileEditsApi.cjs +2 -0
- package/dist/types/fileEditsApi.cjs.map +1 -0
- package/dist/types/fileEditsApi.mjs +2 -0
- package/dist/types/fileEditsApi.mjs.map +1 -0
- package/dist/types/gitDiffService.d.cts +7 -0
- package/dist/types/gitDiffService.d.cts.map +1 -0
- package/dist/types/gitDiffService.d.mts +7 -0
- package/dist/types/gitDiffService.d.mts.map +1 -0
- package/dist/types/index.d.cts +24 -0
- package/dist/types/index.d.cts.map +1 -0
- package/dist/types/index.d.mts +24 -0
- package/dist/types/index.d.mts.map +1 -0
- package/dist/types/snapshotStore.d.cts +35 -0
- package/dist/types/snapshotStore.d.cts.map +1 -0
- package/dist/types/snapshotStore.d.mts +35 -0
- package/dist/types/snapshotStore.d.mts.map +1 -0
- package/dist/types/timelineStore.d.cts +12 -0
- package/dist/types/timelineStore.d.cts.map +1 -0
- package/dist/types/timelineStore.d.mts +12 -0
- package/dist/types/timelineStore.d.mts.map +1 -0
- package/dist/types/treeManifest.d.cts +70 -0
- package/dist/types/treeManifest.d.cts.map +1 -0
- package/dist/types/treeManifest.d.mts +70 -0
- package/dist/types/treeManifest.d.mts.map +1 -0
- package/dist/types/webFiles.cjs +2 -0
- package/dist/types/webFiles.cjs.map +1 -0
- package/dist/types/webFiles.mjs +2 -0
- package/dist/types/webFiles.mjs.map +1 -0
- package/package.json +83 -23
- package/src/extensions/web.ts +54 -0
- package/src/types/domain.ts +95 -0
- package/src/types/fileEditsApi.ts +162 -0
- package/src/types/webFiles.ts +37 -0
- package/src/web/api/filesApi.ts +150 -0
- package/src/web/components/CommentDraft.tsx +70 -0
- package/src/web/components/DeleteFileDialog.tsx +56 -0
- package/src/web/components/DiffView.tsx +96 -0
- package/src/web/components/FilePanel.tsx +522 -0
- package/src/web/components/FilePreviewPanel.tsx +152 -0
- package/src/web/components/FilesActivitySection.tsx +62 -0
- package/src/web/components/FilesBrowser.tsx +262 -0
- package/src/web/components/SessionMediaPreview.tsx +40 -0
- package/src/web/components/fileLinks.ts +59 -0
- package/src/web/lib/fileView.ts +195 -0
- package/src/web/stores/filesStore.ts +109 -0
- package/src/web/tsconfig.json +22 -0
- package/dist/_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorate.cjs +0 -1
- package/dist/_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorate.mjs +0 -1
- package/dist/_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorateMetadata.cjs +0 -1
- package/dist/_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorateMetadata.mjs +0 -1
- package/dist/_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorateParam.cjs +0 -1
- package/dist/_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorateParam.mjs +0 -1
- package/dist/commandLine.cjs.map +0 -1
- package/dist/commandLine.mjs.map +0 -1
- package/dist/container/index.cjs +0 -2
- package/dist/container/index.cjs.map +0 -1
- package/dist/container/index.d.cts +0 -7
- package/dist/container/index.d.cts.map +0 -1
- package/dist/container/index.d.mts +0 -7
- package/dist/container/index.d.mts.map +0 -1
- package/dist/container/index.mjs +0 -2
- package/dist/container/index.mjs.map +0 -1
- package/dist/domain.d.cts +0 -32
- package/dist/domain.d.cts.map +0 -1
- package/dist/domain.d.mts +0 -32
- package/dist/domain.d.mts.map +0 -1
- package/dist/extension.cjs +0 -2
- package/dist/extension.cjs.map +0 -1
- package/dist/extension.d.cts +0 -6
- package/dist/extension.d.cts.map +0 -1
- package/dist/extension.d.mts +0 -6
- package/dist/extension.d.mts.map +0 -1
- package/dist/extension.mjs +0 -2
- package/dist/extension.mjs.map +0 -1
- package/dist/extensions/doom.cjs +0 -2
- package/dist/extensions/doom.cjs.map +0 -1
- package/dist/extensions/doom.d.cts +0 -6
- package/dist/extensions/doom.d.cts.map +0 -1
- package/dist/extensions/doom.d.mts +0 -6
- package/dist/extensions/doom.d.mts.map +0 -1
- package/dist/extensions/doom.mjs +0 -2
- package/dist/extensions/doom.mjs.map +0 -1
- package/dist/services/EditTracker/EditTracker.cjs +0 -2
- package/dist/services/EditTracker/EditTracker.cjs.map +0 -1
- package/dist/services/EditTracker/EditTracker.mjs +0 -2
- package/dist/services/EditTracker/EditTracker.mjs.map +0 -1
- package/dist/services/EditorConfigService/EditorConfigService.cjs +0 -2
- package/dist/services/EditorConfigService/EditorConfigService.cjs.map +0 -1
- package/dist/services/EditorConfigService/EditorConfigService.mjs +0 -2
- package/dist/services/EditorConfigService/EditorConfigService.mjs.map +0 -1
- package/dist/services/EditorLauncher/EditorLauncher.cjs +0 -2
- package/dist/services/EditorLauncher/EditorLauncher.cjs.map +0 -1
- package/dist/services/EditorLauncher/EditorLauncher.mjs +0 -2
- package/dist/services/EditorLauncher/EditorLauncher.mjs.map +0 -1
- package/dist/services/FileEditPaths/FileEditPaths.cjs +0 -2
- package/dist/services/FileEditPaths/FileEditPaths.cjs.map +0 -1
- package/dist/services/FileEditPaths/FileEditPaths.mjs +0 -2
- package/dist/services/FileEditPaths/FileEditPaths.mjs.map +0 -1
- package/dist/services/FileEditWorkflow/FileEditWorkflow.cjs +0 -2
- package/dist/services/FileEditWorkflow/FileEditWorkflow.cjs.map +0 -1
- package/dist/services/FileEditWorkflow/FileEditWorkflow.mjs +0 -2
- package/dist/services/FileEditWorkflow/FileEditWorkflow.mjs.map +0 -1
- package/dist/services/GitDiffService/GitDiffService.cjs +0 -5
- package/dist/services/GitDiffService/GitDiffService.cjs.map +0 -1
- package/dist/services/GitDiffService/GitDiffService.mjs +0 -5
- package/dist/services/GitDiffService/GitDiffService.mjs.map +0 -1
- package/dist/services/TimelineStore/TimelineStore.cjs +0 -3
- package/dist/services/TimelineStore/TimelineStore.cjs.map +0 -1
- package/dist/services/TimelineStore/TimelineStore.mjs +0 -3
- package/dist/services/TimelineStore/TimelineStore.mjs.map +0 -1
- package/dist/types.cjs +0 -2
- package/dist/types.cjs.map +0 -1
- package/dist/types.d.cts +0 -13
- package/dist/types.d.cts.map +0 -1
- package/dist/types.d.mts +0 -13
- package/dist/types.d.mts.map +0 -1
- package/dist/types.mjs +0 -2
- package/dist/types.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -1,38 +1,135 @@
|
|
|
1
1
|
# @agimon-ai/doompi-file-edit
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
The package tracks files changed through session tool calls and captures content for diffs, without
|
|
8
|
+
requiring a Git repository. `edit` and `write` name their file in the call, so the package can capture
|
|
9
|
+
the before and after content within its size and format limits. Bash calls instead trigger a bounded
|
|
10
|
+
tree comparison. See [What this cannot see](#what-this-cannot-see) for gaps in that coverage.
|
|
11
|
+
|
|
12
|
+
> **Alpha:** timeline and UI contracts may change between releases.
|
|
13
|
+
|
|
14
|
+
## Requirements
|
|
15
|
+
|
|
16
|
+
- Node.js 22.19.0 or newer
|
|
17
|
+
- Pi 0.85.0 and Pi TUI 0.85.0
|
|
18
|
+
- Git for diffs against `HEAD`
|
|
10
19
|
|
|
11
20
|
## Install
|
|
12
21
|
|
|
22
|
+
`doompi init` and `dpi init` include File Edit in `default.packages` in `.doom/modes.yaml`.
|
|
23
|
+
Remove it or move it to a named layer to change which major modes load it. Standalone Pi loads the
|
|
24
|
+
same extension:
|
|
25
|
+
|
|
13
26
|
```bash
|
|
14
|
-
|
|
27
|
+
pi install npm:@agimon-ai/doompi-file-edit
|
|
15
28
|
```
|
|
16
29
|
|
|
17
|
-
|
|
30
|
+
| Entry | Purpose |
|
|
31
|
+
| ----------------------------------------------- | -------------------------------------------------------- |
|
|
32
|
+
| `@agimon-ai/doompi-file-edit/extensions/pi` | Timeline and DoomPi Leader shortcut |
|
|
33
|
+
| `@agimon-ai/doompi-file-edit/extensions/server` | Server-owned file API and changed-file channel lifecycle |
|
|
34
|
+
| `@agimon-ai/doompi-file-edit` | Library API |
|
|
35
|
+
|
|
36
|
+
## Open the timeline
|
|
37
|
+
|
|
38
|
+
Use `SPC e f` in DoomPi. Select a file to inspect its diff or open it in the configured editor.
|
|
39
|
+
|
|
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
|
|
42
|
+
timeline services but cannot display the panel.
|
|
43
|
+
|
|
44
|
+
## In the cockpit
|
|
18
45
|
|
|
19
|
-
|
|
20
|
-
|
|
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:
|
|
21
48
|
|
|
22
|
-
|
|
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.
|
|
23
54
|
|
|
24
|
-
|
|
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
|
+
|
|
75
|
+
## Configure the editor
|
|
76
|
+
|
|
77
|
+
Set the shared DoomPi configuration in `~/.pi/.doom/config.yaml`:
|
|
78
|
+
|
|
79
|
+
```yaml
|
|
80
|
+
editor:
|
|
81
|
+
command: nvim +{line} {file}
|
|
82
|
+
```
|
|
25
83
|
|
|
26
|
-
|
|
84
|
+
For a standalone installation, the package falls back to
|
|
85
|
+
`~/.pi/agent/doom-file-edit/config.yaml`. Resolution order is:
|
|
27
86
|
|
|
28
|
-
|
|
87
|
+
1. DoomPi `editor.command`;
|
|
88
|
+
2. package fallback configuration;
|
|
89
|
+
3. `$VISUAL`;
|
|
90
|
+
4. `$EDITOR`;
|
|
91
|
+
5. `nano {file}` on Unix-like systems.
|
|
29
92
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
93
|
+
Only `{file}` and `{line}` placeholders are expanded. The command is split and spawned directly,
|
|
94
|
+
not passed through a shell, so shell operators and expansions are not interpreted.
|
|
95
|
+
|
|
96
|
+
## Data behavior
|
|
97
|
+
|
|
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 under the Pi agent directory,
|
|
100
|
+
`$PI_CODING_AGENT_DIR` or `~/.pi/agent/doom-file-edit`, never inside the working tree, and should be
|
|
101
|
+
protected with the same access controls as Pi session data.
|
|
102
|
+
|
|
103
|
+
A session removes its own timeline and snapshots when it ends. A session that is killed cannot, so
|
|
104
|
+
each session also sweeps state that no longer belongs to anyone: a timeline and its snapshots are
|
|
105
|
+
removed once nothing has written to them for a week. An older build kept this state in the
|
|
106
|
+
repository's git common directory, and that directory is cleared away under the same rule.
|
|
107
|
+
|
|
108
|
+
The session API reads and writes only files the timeline already records, so a request cannot reach
|
|
109
|
+
a file this session never changed.
|
|
110
|
+
|
|
111
|
+
## Public API
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
import { createFileEditDependencies, fileEditExtension } from '@agimon-ai/doompi-file-edit';
|
|
115
|
+
import type { FileDiff, FileEditEntry } from '@agimon-ai/doompi-file-edit';
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The root also exports editor-resolution and timeline domain types for host integrations.
|
|
119
|
+
|
|
120
|
+
## Development
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
pnpm build
|
|
124
|
+
pnpm typecheck
|
|
125
|
+
pnpm test
|
|
126
|
+
pnpm lint
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Maintained by [Agimon](https://agimon.ai/about).
|
|
35
130
|
|
|
36
131
|
## License
|
|
37
132
|
|
|
38
133
|
MIT
|
|
134
|
+
|
|
135
|
+
The public Pi and server loaders live in `src/extensions`. Controllers declare commands, events, APIs, and tracking activities. Services own filesystem and timeline logic; TUI modules assemble and present the review workflow. Public reusable types and dependency construction are exposed through flat `src/exports` entries. The Pi factory owns its optional UI service and awaits cleanup through `onDispose`.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:`Module`}});const e=require("./schemas/apiContracts.cjs");exports.apiContracts=e.apiContracts,exports.default=e.apiContracts;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{apiContracts as e}from"./schemas/apiContracts.mjs";export{e as apiContracts,e as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"package.cjs","names":[],"sources":["../../src/constants/package.ts"],"sourcesContent":["export const FILES_COMMAND = 'file-edits';\nexport const PACKAGE_SOURCE = '@agimon-ai/doompi-file-edit';\nexport const DOOM_IGNORE_FILE = '.doomignore';\n"],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"package.mjs","names":[],"sources":["../../src/constants/package.ts"],"sourcesContent":["export const FILES_COMMAND = 'file-edits';\nexport const PACKAGE_SOURCE = '@agimon-ai/doompi-file-edit';\nexport const DOOM_IGNORE_FILE = '.doomignore';\n"],"mappings":"AAAA,MAAa,EAAgB,aAChB,EAAiB,8BACjB,EAAmB"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("../constants/package.cjs"),n=require("../services/doomIgnore/index.cjs"),r=require("../services/sessionStateSweep/index.cjs"),i=require("../types/webFiles.cjs");let a=require("node:path");a=e.__toESM(a,1);let o=require("node:fs");o=e.__toESM(o,1);let s=require("@agimon-ai/doompi-core/child-process"),c=require("@agimon-ai/doompi-core/ui-hub"),l=require("@agimon-ai/doompi-telemetry");function u(e){let r;try{r=o.default.readFileSync(a.default.join(e,t.DOOM_IGNORE_FILE),`utf8`)}catch{return}let i=n.createDoomIgnoreMatcher(r);if(i!==void 0)return t=>i(a.default.relative(e,t))}const d={source:t.PACKAGE_SOURCE,bindings:[{id:`editor.files`,path:[{key:`e`,label:`extension`,detail:`tools, skills and config`,order:50},{key:`f`,label:`files`,detail:`session edits`}],command:{name:t.FILES_COMMAND}}]};function f(e){let{paths:n,timeline:a,snapshots:o,editTracker:f,workflow:p}=e,m=(0,l.createDoomTelemetry)({serviceName:`doom-file-edit`,packageName:t.PACKAGE_SOURCE,env:process.env,enableLogs:!0,enableTraces:!0}),h=new Map,g=!0,_=0,v,y=async()=>{if(!g||!v?.hasUI)return;let e=await a.list();v.ui.setStatus(i.filesStatusKey,e.length===0?``:`${e.length} ${e.length===1?`file`:`files`}`)},b=async()=>{g=!1,_+=1,h.clear(),f.reset();let e=v;v=void 0;try{process.env[s.SUBAGENT_CHILD_ENV]||(await a.clear(),await o.clear()),e?.hasUI&&e.ui.setStatus(i.filesStatusKey,void 0),await m.recordEvent(`doom_file_edit.timeline_finished`,{outcome:`closed`})}finally{await m.shutdown()}},x=async(e,t)=>{try{await r.sweepSessionState({directory:n.stateDirectory(),keep:t});let i=n.legacyStateDirectory(e);if(i===void 0||i===n.stateDirectory())return;await r.sweepSessionState({directory:i,removeDirectory:!0})}catch{}};return{onDispose:b,services:[e=>{e.inject([c.DOOM_UI_HUB_SERVICE],e=>{let t=(0,c.requireDoomUiHub)(e).registerLeader(d);return()=>t.dispose()})}],commands:[[t.FILES_COMMAND,{description:`Review files edited in this session`,handler:async(e,t)=>{if(g){if(!t.hasUI){t.ui.notify(`/file-edits requires interactive mode`,`error`);return}await p.open(t)}}}]],events:{session_start:(e,t)=>{if(!g)return;_+=1,h.clear(),v=t;let r=n.sessionKey(t.sessionManager.getSessionId()),i=n.timelinePath(t.cwd,r),s=n.snapshotsPath(t.cwd,r);a.initialize(i),o.initialize(s),x(t.cwd,i);let c=u(t.cwd);f.reset({exclude:[i,`${i}.lock`,s],...c===void 0?{}:{isIgnored:c}}),y(),m.recordEvent(`doom_file_edit.timeline_started`,{outcome:`initialized`})},tool_execution_start:async(e,t)=>{if(!g)return;let n=_;if(h.set(e.toolCallId,Date.now()),await f.start(e.toolCallId,e.toolName,e.args,t.cwd),!g||n!==_){h.delete(e.toolCallId);return}m.recordEvent(`doom_file_edit.edit_started`,{"tool.name":e.toolName})},tool_execution_end:async(e,t)=>{if(!g)return;let n=h.get(e.toolCallId);h.delete(e.toolCallId),await f.end(e.toolCallId,e.isError,t.cwd),g&&(await y(),m.recordEvent(`doom_file_edit.edit_finished`,{"tool.name":e.toolName,"tool.result.error":e.isError,...n===void 0?{}:{duration_ms:Date.now()-n},outcome:e.isError?`failed`:`completed`}))}}}}exports.FILE_EDIT_LEADER_CONTRIBUTION=d,exports.createFileEditRuntime=f;
|
|
2
|
+
//# sourceMappingURL=fileEditRuntime.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fileEditRuntime.cjs","names":["fs","path","DOOM_IGNORE_FILE","createDoomIgnoreMatcher","PACKAGE_SOURCE","FILES_COMMAND","createDoomTelemetry","filesStatusKey","SUBAGENT_CHILD_ENV","sweepSessionState","DOOM_UI_HUB_SERVICE","requireDoomUiHub"],"sources":["../../src/controllers/fileEditRuntime.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\n\nimport { SUBAGENT_CHILD_ENV } from '@agimon-ai/doompi-core/child-process';\nimport type { LeaderContribution } from '@agimon-ai/doompi-core/leader';\nimport type { PiPluginContributions } from '@agimon-ai/doompi-core/pi-extension';\nimport { DOOM_UI_HUB_SERVICE, requireDoomUiHub } from '@agimon-ai/doompi-core/ui-hub';\nimport { createDoomTelemetry, type DoomTelemetry } from '@agimon-ai/doompi-telemetry';\nimport type { Context } from '@deepseek-ai/cordis';\nimport type { ExtensionContext } from '@earendil-works/pi-coding-agent';\n\nimport { FILES_COMMAND, PACKAGE_SOURCE, DOOM_IGNORE_FILE } from '../constants/package';\nimport { createDoomIgnoreMatcher } from '../services/doomIgnore';\nimport { sweepSessionState } from '../services/sessionStateSweep';\nimport type { FileEditDependencies } from '../types';\nimport { filesStatusKey } from '../types/webFiles';\n/**\n * The project's ignore rules as a test over absolute paths, or nothing when the\n * file is absent, empty, or unreadable.\n *\n * Read once per session, like the exclude list it is passed beside. The web\n * reader re-reads the same file per request, so a mid-session edit reaches the\n * list before it reaches the recorder; that is a pre-existing difference and\n * not one this read introduces.\n */\nfunction readDoomIgnore(cwd: string): ((filePath: string) => boolean) | undefined {\n let content: string;\n try {\n content = fs.readFileSync(path.join(cwd, DOOM_IGNORE_FILE), 'utf8');\n } catch {\n return undefined;\n }\n const matcher = createDoomIgnoreMatcher(content);\n if (matcher === undefined) return undefined;\n return (filePath: string): boolean => matcher(path.relative(cwd, filePath));\n}\n\nexport const FILE_EDIT_LEADER_CONTRIBUTION = {\n source: PACKAGE_SOURCE,\n bindings: [\n {\n id: 'editor.files',\n path: [\n { key: 'e', label: 'extension', detail: 'tools, skills and config', order: 50 },\n { key: 'f', label: 'files', detail: 'session edits' },\n ],\n command: { name: FILES_COMMAND },\n },\n ],\n} as const satisfies LeaderContribution;\nexport function createFileEditRuntime(dependencies: FileEditDependencies): PiPluginContributions {\n const { paths, timeline, snapshots, editTracker: tracker, workflow } = dependencies;\n const telemetry: DoomTelemetry = createDoomTelemetry({\n serviceName: 'doom-file-edit',\n packageName: PACKAGE_SOURCE,\n env: process.env,\n enableLogs: true,\n enableTraces: true,\n });\n const toolStartedAt = new Map<string, number>();\n let active = true;\n let sessionGeneration = 0;\n let sessionContext: ExtensionContext | undefined;\n\n /**\n * Publishes the count the cockpit's activity group keys off. The group is\n * hidden until this status exists, so it is written on every append rather\n * than only when the count changes.\n */\n const publishStatus = async (): Promise<void> => {\n if (!active || !sessionContext?.hasUI) return;\n const entries = await timeline.list();\n sessionContext.ui.setStatus(\n filesStatusKey,\n entries.length === 0 ? '' : `${entries.length} ${entries.length === 1 ? 'file' : 'files'}`,\n );\n };\n\n const dispose = async () => {\n active = false;\n sessionGeneration += 1;\n toolStartedAt.clear();\n tracker.reset();\n const closing = sessionContext;\n sessionContext = undefined;\n try {\n if (!process.env[SUBAGENT_CHILD_ENV]) {\n await timeline.clear();\n // The snapshots exist only to diff this session, so they go with it\n // rather than accumulating a copy of the tree on every run.\n await snapshots.clear();\n }\n if (closing?.hasUI) closing.ui.setStatus(filesStatusKey, undefined);\n await telemetry.recordEvent('doom_file_edit.timeline_finished', { outcome: 'closed' });\n } finally {\n await telemetry.shutdown();\n }\n };\n\n /**\n * Clears state no session will claim again. Never awaited and never allowed\n * to throw: a sweep that cannot run is not a reason to fail a session.\n */\n const sweepAbandonedState = async (cwd: string, keep: string): Promise<void> => {\n try {\n await sweepSessionState({ directory: paths.stateDirectory(), keep });\n const legacy = paths.legacyStateDirectory(cwd);\n if (legacy === undefined || legacy === paths.stateDirectory()) return;\n // Same age rule, so a build still running against the old location is\n // not robbed of the session it is in the middle of.\n await sweepSessionState({ directory: legacy, removeDirectory: true });\n } catch {\n // Nothing here is worth interrupting a session that is starting.\n }\n };\n return {\n onDispose: dispose,\n services: [\n (cordis: Context) => {\n cordis.inject([DOOM_UI_HUB_SERVICE], (uiContext) => {\n const contribution = requireDoomUiHub(uiContext).registerLeader(FILE_EDIT_LEADER_CONTRIBUTION);\n return () => contribution.dispose();\n });\n },\n ],\n commands: [\n [\n FILES_COMMAND,\n {\n description: 'Review files edited in this session',\n handler: async (_args, ctx) => {\n if (!active) return;\n if (!ctx.hasUI) {\n ctx.ui.notify('/file-edits requires interactive mode', 'error');\n return;\n }\n await workflow.open(ctx);\n },\n },\n ],\n ],\n events: {\n session_start: (_event, ctx) => {\n if (!active) return;\n sessionGeneration += 1;\n toolStartedAt.clear();\n sessionContext = ctx;\n const sessionKey = paths.sessionKey(ctx.sessionManager.getSessionId());\n const timelinePath = paths.timelinePath(ctx.cwd, sessionKey);\n const snapshotsPath = paths.snapshotsPath(ctx.cwd, sessionKey);\n timeline.initialize(timelinePath);\n snapshots.initialize(snapshotsPath);\n // Inside a git worktree these land under the repository's own git\n // directory, which can sit inside the tree the tracker walks. Naming them\n // here is what stops the package's bookkeeping reading as session edits.\n // Whatever earlier sessions were killed before they could clean up, plus\n // anything an older build left in the repository's own git directory.\n void sweepAbandonedState(ctx.cwd, timelinePath);\n const isIgnored = readDoomIgnore(ctx.cwd);\n tracker.reset({\n exclude: [timelinePath, `${timelinePath}.lock`, snapshotsPath],\n ...(isIgnored === undefined ? {} : { isIgnored }),\n });\n // A resumed session already has a timeline, so the group must show its\n // count from the first paint rather than waiting for the next edit.\n void publishStatus();\n void telemetry.recordEvent('doom_file_edit.timeline_started', { outcome: 'initialized' });\n },\n tool_execution_start: async (event, ctx) => {\n if (!active) return;\n const ownGeneration = sessionGeneration;\n toolStartedAt.set(event.toolCallId, Date.now());\n await tracker.start(event.toolCallId, event.toolName, event.args, ctx.cwd);\n if (!active || ownGeneration !== sessionGeneration) {\n toolStartedAt.delete(event.toolCallId);\n return;\n }\n void telemetry.recordEvent('doom_file_edit.edit_started', { 'tool.name': event.toolName });\n },\n tool_execution_end: async (event, ctx) => {\n if (!active) return;\n const startedAt = toolStartedAt.get(event.toolCallId);\n toolStartedAt.delete(event.toolCallId);\n await tracker.end(event.toolCallId, event.isError, ctx.cwd);\n if (!active) return;\n await publishStatus();\n void telemetry.recordEvent('doom_file_edit.edit_finished', {\n 'tool.name': event.toolName,\n 'tool.result.error': event.isError,\n ...(startedAt === undefined ? {} : { duration_ms: Date.now() - startedAt }),\n outcome: event.isError ? 'failed' : 'completed',\n });\n },\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{DOOM_IGNORE_FILE as e,FILES_COMMAND as t,PACKAGE_SOURCE as n}from"../constants/package.mjs";import{createDoomIgnoreMatcher as r}from"../services/doomIgnore/index.mjs";import{sweepSessionState as i}from"../services/sessionStateSweep/index.mjs";import{filesStatusKey as a}from"../types/webFiles.mjs";import o from"node:path";import s from"node:fs";import{SUBAGENT_CHILD_ENV as c}from"@agimon-ai/doompi-core/child-process";import{DOOM_UI_HUB_SERVICE as l,requireDoomUiHub as u}from"@agimon-ai/doompi-core/ui-hub";import{createDoomTelemetry as d}from"@agimon-ai/doompi-telemetry";function f(t){let n;try{n=s.readFileSync(o.join(t,e),`utf8`)}catch{return}let i=r(n);if(i!==void 0)return e=>i(o.relative(t,e))}const p={source:n,bindings:[{id:`editor.files`,path:[{key:`e`,label:`extension`,detail:`tools, skills and config`,order:50},{key:`f`,label:`files`,detail:`session edits`}],command:{name:t}}]};function m(e){let{paths:r,timeline:o,snapshots:s,editTracker:m,workflow:h}=e,g=d({serviceName:`doom-file-edit`,packageName:n,env:process.env,enableLogs:!0,enableTraces:!0}),_=new Map,v=!0,y=0,b,x=async()=>{if(!v||!b?.hasUI)return;let e=await o.list();b.ui.setStatus(a,e.length===0?``:`${e.length} ${e.length===1?`file`:`files`}`)},S=async()=>{v=!1,y+=1,_.clear(),m.reset();let e=b;b=void 0;try{process.env[c]||(await o.clear(),await s.clear()),e?.hasUI&&e.ui.setStatus(a,void 0),await g.recordEvent(`doom_file_edit.timeline_finished`,{outcome:`closed`})}finally{await g.shutdown()}},C=async(e,t)=>{try{await i({directory:r.stateDirectory(),keep:t});let n=r.legacyStateDirectory(e);if(n===void 0||n===r.stateDirectory())return;await i({directory:n,removeDirectory:!0})}catch{}};return{onDispose:S,services:[e=>{e.inject([l],e=>{let t=u(e).registerLeader(p);return()=>t.dispose()})}],commands:[[t,{description:`Review files edited in this session`,handler:async(e,t)=>{if(v){if(!t.hasUI){t.ui.notify(`/file-edits requires interactive mode`,`error`);return}await h.open(t)}}}]],events:{session_start:(e,t)=>{if(!v)return;y+=1,_.clear(),b=t;let n=r.sessionKey(t.sessionManager.getSessionId()),i=r.timelinePath(t.cwd,n),a=r.snapshotsPath(t.cwd,n);o.initialize(i),s.initialize(a),C(t.cwd,i);let c=f(t.cwd);m.reset({exclude:[i,`${i}.lock`,a],...c===void 0?{}:{isIgnored:c}}),x(),g.recordEvent(`doom_file_edit.timeline_started`,{outcome:`initialized`})},tool_execution_start:async(e,t)=>{if(!v)return;let n=y;if(_.set(e.toolCallId,Date.now()),await m.start(e.toolCallId,e.toolName,e.args,t.cwd),!v||n!==y){_.delete(e.toolCallId);return}g.recordEvent(`doom_file_edit.edit_started`,{"tool.name":e.toolName})},tool_execution_end:async(e,t)=>{if(!v)return;let n=_.get(e.toolCallId);_.delete(e.toolCallId),await m.end(e.toolCallId,e.isError,t.cwd),v&&(await x(),g.recordEvent(`doom_file_edit.edit_finished`,{"tool.name":e.toolName,"tool.result.error":e.isError,...n===void 0?{}:{duration_ms:Date.now()-n},outcome:e.isError?`failed`:`completed`}))}}}}export{p as FILE_EDIT_LEADER_CONTRIBUTION,m as createFileEditRuntime};
|
|
2
|
+
//# sourceMappingURL=fileEditRuntime.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fileEditRuntime.mjs","names":[],"sources":["../../src/controllers/fileEditRuntime.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\n\nimport { SUBAGENT_CHILD_ENV } from '@agimon-ai/doompi-core/child-process';\nimport type { LeaderContribution } from '@agimon-ai/doompi-core/leader';\nimport type { PiPluginContributions } from '@agimon-ai/doompi-core/pi-extension';\nimport { DOOM_UI_HUB_SERVICE, requireDoomUiHub } from '@agimon-ai/doompi-core/ui-hub';\nimport { createDoomTelemetry, type DoomTelemetry } from '@agimon-ai/doompi-telemetry';\nimport type { Context } from '@deepseek-ai/cordis';\nimport type { ExtensionContext } from '@earendil-works/pi-coding-agent';\n\nimport { FILES_COMMAND, PACKAGE_SOURCE, DOOM_IGNORE_FILE } from '../constants/package';\nimport { createDoomIgnoreMatcher } from '../services/doomIgnore';\nimport { sweepSessionState } from '../services/sessionStateSweep';\nimport type { FileEditDependencies } from '../types';\nimport { filesStatusKey } from '../types/webFiles';\n/**\n * The project's ignore rules as a test over absolute paths, or nothing when the\n * file is absent, empty, or unreadable.\n *\n * Read once per session, like the exclude list it is passed beside. The web\n * reader re-reads the same file per request, so a mid-session edit reaches the\n * list before it reaches the recorder; that is a pre-existing difference and\n * not one this read introduces.\n */\nfunction readDoomIgnore(cwd: string): ((filePath: string) => boolean) | undefined {\n let content: string;\n try {\n content = fs.readFileSync(path.join(cwd, DOOM_IGNORE_FILE), 'utf8');\n } catch {\n return undefined;\n }\n const matcher = createDoomIgnoreMatcher(content);\n if (matcher === undefined) return undefined;\n return (filePath: string): boolean => matcher(path.relative(cwd, filePath));\n}\n\nexport const FILE_EDIT_LEADER_CONTRIBUTION = {\n source: PACKAGE_SOURCE,\n bindings: [\n {\n id: 'editor.files',\n path: [\n { key: 'e', label: 'extension', detail: 'tools, skills and config', order: 50 },\n { key: 'f', label: 'files', detail: 'session edits' },\n ],\n command: { name: FILES_COMMAND },\n },\n ],\n} as const satisfies LeaderContribution;\nexport function createFileEditRuntime(dependencies: FileEditDependencies): PiPluginContributions {\n const { paths, timeline, snapshots, editTracker: tracker, workflow } = dependencies;\n const telemetry: DoomTelemetry = createDoomTelemetry({\n serviceName: 'doom-file-edit',\n packageName: PACKAGE_SOURCE,\n env: process.env,\n enableLogs: true,\n enableTraces: true,\n });\n const toolStartedAt = new Map<string, number>();\n let active = true;\n let sessionGeneration = 0;\n let sessionContext: ExtensionContext | undefined;\n\n /**\n * Publishes the count the cockpit's activity group keys off. The group is\n * hidden until this status exists, so it is written on every append rather\n * than only when the count changes.\n */\n const publishStatus = async (): Promise<void> => {\n if (!active || !sessionContext?.hasUI) return;\n const entries = await timeline.list();\n sessionContext.ui.setStatus(\n filesStatusKey,\n entries.length === 0 ? '' : `${entries.length} ${entries.length === 1 ? 'file' : 'files'}`,\n );\n };\n\n const dispose = async () => {\n active = false;\n sessionGeneration += 1;\n toolStartedAt.clear();\n tracker.reset();\n const closing = sessionContext;\n sessionContext = undefined;\n try {\n if (!process.env[SUBAGENT_CHILD_ENV]) {\n await timeline.clear();\n // The snapshots exist only to diff this session, so they go with it\n // rather than accumulating a copy of the tree on every run.\n await snapshots.clear();\n }\n if (closing?.hasUI) closing.ui.setStatus(filesStatusKey, undefined);\n await telemetry.recordEvent('doom_file_edit.timeline_finished', { outcome: 'closed' });\n } finally {\n await telemetry.shutdown();\n }\n };\n\n /**\n * Clears state no session will claim again. Never awaited and never allowed\n * to throw: a sweep that cannot run is not a reason to fail a session.\n */\n const sweepAbandonedState = async (cwd: string, keep: string): Promise<void> => {\n try {\n await sweepSessionState({ directory: paths.stateDirectory(), keep });\n const legacy = paths.legacyStateDirectory(cwd);\n if (legacy === undefined || legacy === paths.stateDirectory()) return;\n // Same age rule, so a build still running against the old location is\n // not robbed of the session it is in the middle of.\n await sweepSessionState({ directory: legacy, removeDirectory: true });\n } catch {\n // Nothing here is worth interrupting a session that is starting.\n }\n };\n return {\n onDispose: dispose,\n services: [\n (cordis: Context) => {\n cordis.inject([DOOM_UI_HUB_SERVICE], (uiContext) => {\n const contribution = requireDoomUiHub(uiContext).registerLeader(FILE_EDIT_LEADER_CONTRIBUTION);\n return () => contribution.dispose();\n });\n },\n ],\n commands: [\n [\n FILES_COMMAND,\n {\n description: 'Review files edited in this session',\n handler: async (_args, ctx) => {\n if (!active) return;\n if (!ctx.hasUI) {\n ctx.ui.notify('/file-edits requires interactive mode', 'error');\n return;\n }\n await workflow.open(ctx);\n },\n },\n ],\n ],\n events: {\n session_start: (_event, ctx) => {\n if (!active) return;\n sessionGeneration += 1;\n toolStartedAt.clear();\n sessionContext = ctx;\n const sessionKey = paths.sessionKey(ctx.sessionManager.getSessionId());\n const timelinePath = paths.timelinePath(ctx.cwd, sessionKey);\n const snapshotsPath = paths.snapshotsPath(ctx.cwd, sessionKey);\n timeline.initialize(timelinePath);\n snapshots.initialize(snapshotsPath);\n // Inside a git worktree these land under the repository's own git\n // directory, which can sit inside the tree the tracker walks. Naming them\n // here is what stops the package's bookkeeping reading as session edits.\n // Whatever earlier sessions were killed before they could clean up, plus\n // anything an older build left in the repository's own git directory.\n void sweepAbandonedState(ctx.cwd, timelinePath);\n const isIgnored = readDoomIgnore(ctx.cwd);\n tracker.reset({\n exclude: [timelinePath, `${timelinePath}.lock`, snapshotsPath],\n ...(isIgnored === undefined ? {} : { isIgnored }),\n });\n // A resumed session already has a timeline, so the group must show its\n // count from the first paint rather than waiting for the next edit.\n void publishStatus();\n void telemetry.recordEvent('doom_file_edit.timeline_started', { outcome: 'initialized' });\n },\n tool_execution_start: async (event, ctx) => {\n if (!active) return;\n const ownGeneration = sessionGeneration;\n toolStartedAt.set(event.toolCallId, Date.now());\n await tracker.start(event.toolCallId, event.toolName, event.args, ctx.cwd);\n if (!active || ownGeneration !== sessionGeneration) {\n toolStartedAt.delete(event.toolCallId);\n return;\n }\n void telemetry.recordEvent('doom_file_edit.edit_started', { 'tool.name': event.toolName });\n },\n tool_execution_end: async (event, ctx) => {\n if (!active) return;\n const startedAt = toolStartedAt.get(event.toolCallId);\n toolStartedAt.delete(event.toolCallId);\n await tracker.end(event.toolCallId, event.isError, ctx.cwd);\n if (!active) return;\n await publishStatus();\n void telemetry.recordEvent('doom_file_edit.edit_finished', {\n 'tool.name': event.toolName,\n 'tool.result.error': event.isError,\n ...(startedAt === undefined ? {} : { duration_ms: Date.now() - startedAt }),\n outcome: event.isError ? 'failed' : 'completed',\n });\n },\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("../services/editTracker/index.cjs"),n=require("../services/fileEditPaths/index.cjs"),r=require("../services/gitStatus/index.cjs"),i=require("../services/snapshotStore/index.cjs"),a=require("../services/timelineStore/index.cjs"),o=require("../services/treeManifest/index.cjs"),s=require("../services/doomIgnore/index.cjs"),c=require("../types/webFiles.cjs"),l=require("./fileEditsApi.cjs"),u=require("./webFilesChannel.cjs");let d=require("node:path");d=e.__toESM(d,1);let f=require("node:fs");f=e.__toESM(f,1);function p(e){try{let t=s.createDoomIgnoreMatcher(f.default.readFileSync(d.default.join(e,`.doomignore`),`utf8`));return t===void 0?void 0:n=>t(d.default.relative(e,n))}catch{return}}function m(e,t){let n=e[t];return typeof n==`string`&&n.length>0?n:void 0}function h({agent:e,host:s}){if(!e)return{api:[l.api]};let d=new n.FileEditPaths,f=new a.TimelineStore,h=new i.NodeSnapshotStoreAdapter,g=new t.EditTracker(f,h,new o.NodeTreeManifestAdapter,{git:new r.NodeGitStatusAdapter});if(s.context.directEvents===void 0)throw Error(`File-edit headless facet requires the session direct event bus.`);let _=s.context.directEvents,v,y=e=>{_.publish(c.filesChannelType,e.context.sessionId,{items:u.readSessionFiles(e.timelinePath,e.context.cwd)})};return{api:[l.api],activities:[{name:`file-edits`,start:async e=>{let t=d.sessionKey(e.sessionId),n=d.timelinePath(e.cwd,t),r=d.snapshotsPath(e.cwd,t);f.initialize(n),h.initialize(r);let i=p(e.cwd);g.reset({exclude:[n,`${n}.lock`,r],...i===void 0?{}:{isIgnored:i}});let a={context:e,timelinePath:n,snapshotsPath:r};return v=a,y(a),async()=>{v===a&&(v=void 0,g.reset())}}}],hooks:[{event:`tool_execution_start`,handle:async(e,t)=>{let n=v,r=m(e,`toolCallId`),i=m(e,`toolName`);n!==void 0&&n.context.sessionId===t.sessionId&&r&&i&&await g.start(r,i,e.args,t.cwd)}},{event:`tool_execution_end`,handle:async(e,t)=>{let n=v,r=m(e,`toolCallId`);n!==void 0&&n.context.sessionId===t.sessionId&&r&&(await g.end(r,e.isError===!0,t.cwd),v===n&&y(n))}}]}}exports.createFileEditSession=h;
|
|
2
|
+
//# sourceMappingURL=fileEditSession.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fileEditSession.cjs","names":["createDoomIgnoreMatcher","fs","path","api","FileEditPaths","TimelineStore","NodeSnapshotStoreAdapter","EditTracker","NodeTreeManifestAdapter","NodeGitStatusAdapter","filesChannelType","readSessionFiles"],"sources":["../../src/controllers/fileEditSession.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\n\nimport {\n type DoomHeadlessExecutionContext,\n type DoomHeadlessActivity,\n type DoomHeadlessHook,\n} from '@agimon-ai/doompi-core/headless';\nimport type { DoomServerPluginContext, DoomServerSessionPlugin } from '@agimon-ai/doompi-core/server-facet';\n\nimport { createDoomIgnoreMatcher } from '../services/doomIgnore';\nimport { EditTracker } from '../services/editTracker';\nimport { FileEditPaths } from '../services/fileEditPaths';\nimport { NodeGitStatusAdapter } from '../services/gitStatus';\nimport { NodeSnapshotStoreAdapter } from '../services/snapshotStore';\nimport { TimelineStore } from '../services/timelineStore';\nimport { NodeTreeManifestAdapter } from '../services/treeManifest';\nimport { filesChannelType } from '../types/webFiles';\nimport { api } from './fileEditsApi';\nimport { readSessionFiles } from './webFilesChannel';\n\nconst DOOM_IGNORE_FILE = '.doomignore';\n\nfunction readDoomIgnore(cwd: string): ((filePath: string) => boolean) | undefined {\n try {\n const matcher = createDoomIgnoreMatcher(fs.readFileSync(path.join(cwd, DOOM_IGNORE_FILE), 'utf8'));\n return matcher === undefined ? undefined : (filePath: string) => matcher(path.relative(cwd, filePath));\n } catch {\n return undefined;\n }\n}\n\nfunction stringValue(value: Readonly<Record<string, unknown>>, key: string): string | undefined {\n const candidate = value[key];\n return typeof candidate === 'string' && candidate.length > 0 ? candidate : undefined;\n}\n\nexport function createFileEditSession({ agent, host }: DoomServerPluginContext): DoomServerSessionPlugin {\n if (!agent) return { api: [api] };\n const paths = new FileEditPaths();\n const timeline = new TimelineStore();\n const snapshots = new NodeSnapshotStoreAdapter();\n const editTracker = new EditTracker(timeline, snapshots, new NodeTreeManifestAdapter(), {\n git: new NodeGitStatusAdapter(),\n });\n if (host.context.directEvents === undefined)\n throw new Error('File-edit headless facet requires the session direct event bus.');\n const directEvents = host.context.directEvents;\n let active:\n | {\n readonly context: DoomHeadlessExecutionContext;\n readonly timelinePath: string;\n readonly snapshotsPath: string;\n }\n | undefined;\n const publish = (runtime: NonNullable<typeof active>): void => {\n directEvents.publish(filesChannelType, runtime.context.sessionId, {\n items: readSessionFiles(runtime.timelinePath, runtime.context.cwd),\n });\n };\n const activity: DoomHeadlessActivity = {\n name: 'file-edits',\n start: async (executionContext) => {\n const sessionKey = paths.sessionKey(executionContext.sessionId);\n const timelinePath = paths.timelinePath(executionContext.cwd, sessionKey);\n const snapshotsPath = paths.snapshotsPath(executionContext.cwd, sessionKey);\n timeline.initialize(timelinePath);\n snapshots.initialize(snapshotsPath);\n const isIgnored = readDoomIgnore(executionContext.cwd);\n editTracker.reset({\n exclude: [timelinePath, `${timelinePath}.lock`, snapshotsPath],\n ...(isIgnored === undefined ? {} : { isIgnored }),\n });\n const runtime = { context: executionContext, timelinePath, snapshotsPath };\n active = runtime;\n publish(runtime);\n return async () => {\n if (active !== runtime) return;\n active = undefined;\n editTracker.reset();\n };\n },\n };\n const startHook: DoomHeadlessHook = {\n event: 'tool_execution_start',\n handle: async (event, executionContext) => {\n const runtime = active;\n const toolCallId = stringValue(event, 'toolCallId');\n const toolName = stringValue(event, 'toolName');\n if (runtime === undefined || runtime.context.sessionId !== executionContext.sessionId || !toolCallId || !toolName)\n return;\n await editTracker.start(toolCallId, toolName, event.args, executionContext.cwd);\n },\n };\n const endHook: DoomHeadlessHook = {\n event: 'tool_execution_end',\n handle: async (event, executionContext) => {\n const runtime = active;\n const toolCallId = stringValue(event, 'toolCallId');\n if (runtime === undefined || runtime.context.sessionId !== executionContext.sessionId || !toolCallId) return;\n await editTracker.end(toolCallId, event.isError === true, executionContext.cwd);\n if (active === runtime) publish(runtime);\n },\n };\n return { api: [api], activities: [activity], hooks: [startHook, endHook] };\n}\n"],"mappings":"8jBAuBA,SAAS,EAAe,EAA0D,CAChF,GAAI,CACF,IAAM,EAAUA,EAAAA,wBAAwBC,EAAAA,QAAG,aAAaC,EAAAA,QAAK,KAAK,EAAK,aAAgB,EAAG,MAAM,CAAC,EACjG,OAAO,IAAY,IAAA,GAAY,IAAA,GAAa,GAAqB,EAAQA,EAAAA,QAAK,SAAS,EAAK,CAAQ,CAAC,CACvG,MAAQ,CACN,MACF,CACF,CAEA,SAAS,EAAY,EAA0C,EAAiC,CAC9F,IAAM,EAAY,EAAM,GACxB,OAAO,OAAO,GAAc,UAAY,EAAU,OAAS,EAAI,EAAY,IAAA,EAC7E,CAEA,SAAgB,EAAsB,CAAE,QAAO,QAA0D,CACvG,GAAI,CAAC,EAAO,MAAO,CAAE,IAAK,CAACC,EAAAA,GAAG,CAAE,EAChC,IAAM,EAAQ,IAAIC,EAAAA,cACZ,EAAW,IAAIC,EAAAA,cACf,EAAY,IAAIC,EAAAA,yBAChB,EAAc,IAAIC,EAAAA,YAAY,EAAU,EAAW,IAAIC,EAAAA,wBAA2B,CACtF,IAAK,IAAIC,EAAAA,oBACX,CAAC,EACD,GAAI,EAAK,QAAQ,eAAiB,IAAA,GAChC,MAAU,MAAM,iEAAiE,EACnF,IAAM,EAAe,EAAK,QAAQ,aAC9B,EAOE,EAAW,GAA8C,CAC7D,EAAa,QAAQC,EAAAA,iBAAkB,EAAQ,QAAQ,UAAW,CAChE,MAAOC,EAAAA,iBAAiB,EAAQ,aAAc,EAAQ,QAAQ,GAAG,CACnE,CAAC,CACH,EA6CA,MAAO,CAAE,IAAK,CAACR,EAAAA,GAAG,EAAG,WAAY,CAAC,CA3ChC,KAAM,aACN,MAAO,KAAO,IAAqB,CACjC,IAAM,EAAa,EAAM,WAAW,EAAiB,SAAS,EACxD,EAAe,EAAM,aAAa,EAAiB,IAAK,CAAU,EAClE,EAAgB,EAAM,cAAc,EAAiB,IAAK,CAAU,EAC1E,EAAS,WAAW,CAAY,EAChC,EAAU,WAAW,CAAa,EAClC,IAAM,EAAY,EAAe,EAAiB,GAAG,EACrD,EAAY,MAAM,CAChB,QAAS,CAAC,EAAc,GAAG,EAAa,OAAQ,CAAa,EAC7D,GAAI,IAAc,IAAA,GAAY,CAAC,EAAI,CAAE,WAAU,CACjD,CAAC,EACD,IAAM,EAAU,CAAE,QAAS,EAAkB,eAAc,eAAc,EAGzE,MAFA,GAAS,EACT,EAAQ,CAAO,EACR,SAAY,CACb,IAAW,IACf,EAAS,IAAA,GACT,EAAY,MAAM,EACpB,CACF,CAuBuC,CAAC,EAAG,MAAO,CAAC,CApBnD,MAAO,uBACP,OAAQ,MAAO,EAAO,IAAqB,CACzC,IAAM,EAAU,EACV,EAAa,EAAY,EAAO,YAAY,EAC5C,EAAW,EAAY,EAAO,UAAU,EAC1C,IAAY,IAAA,IAAa,EAAQ,QAAQ,YAAc,EAAiB,WAAc,GAAe,GAEzG,MAAM,EAAY,MAAM,EAAY,EAAU,EAAM,KAAM,EAAiB,GAAG,CAChF,CAY2D,EAAG,CAT9D,MAAO,qBACP,OAAQ,MAAO,EAAO,IAAqB,CACzC,IAAM,EAAU,EACV,EAAa,EAAY,EAAO,YAAY,EAC9C,IAAY,IAAA,IAAa,EAAQ,QAAQ,YAAc,EAAiB,WAAc,IAC1F,MAAM,EAAY,IAAI,EAAY,EAAM,UAAY,GAAM,EAAiB,GAAG,EAC1E,IAAW,GAAS,EAAQ,CAAO,EACzC,CAEoE,CAAC,CAAE,CAC3E"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{EditTracker as e}from"../services/editTracker/index.mjs";import{FileEditPaths as t}from"../services/fileEditPaths/index.mjs";import{NodeGitStatusAdapter as n}from"../services/gitStatus/index.mjs";import{NodeSnapshotStoreAdapter as r}from"../services/snapshotStore/index.mjs";import{TimelineStore as i}from"../services/timelineStore/index.mjs";import{NodeTreeManifestAdapter as a}from"../services/treeManifest/index.mjs";import{createDoomIgnoreMatcher as o}from"../services/doomIgnore/index.mjs";import{filesChannelType as s}from"../types/webFiles.mjs";import{api as c}from"./fileEditsApi.mjs";import{readSessionFiles as l}from"./webFilesChannel.mjs";import u from"node:path";import d from"node:fs";function f(e){try{let t=o(d.readFileSync(u.join(e,`.doomignore`),`utf8`));return t===void 0?void 0:n=>t(u.relative(e,n))}catch{return}}function p(e,t){let n=e[t];return typeof n==`string`&&n.length>0?n:void 0}function m({agent:o,host:u}){if(!o)return{api:[c]};let d=new t,m=new i,h=new r,g=new e(m,h,new a,{git:new n});if(u.context.directEvents===void 0)throw Error(`File-edit headless facet requires the session direct event bus.`);let _=u.context.directEvents,v,y=e=>{_.publish(s,e.context.sessionId,{items:l(e.timelinePath,e.context.cwd)})};return{api:[c],activities:[{name:`file-edits`,start:async e=>{let t=d.sessionKey(e.sessionId),n=d.timelinePath(e.cwd,t),r=d.snapshotsPath(e.cwd,t);m.initialize(n),h.initialize(r);let i=f(e.cwd);g.reset({exclude:[n,`${n}.lock`,r],...i===void 0?{}:{isIgnored:i}});let a={context:e,timelinePath:n,snapshotsPath:r};return v=a,y(a),async()=>{v===a&&(v=void 0,g.reset())}}}],hooks:[{event:`tool_execution_start`,handle:async(e,t)=>{let n=v,r=p(e,`toolCallId`),i=p(e,`toolName`);n!==void 0&&n.context.sessionId===t.sessionId&&r&&i&&await g.start(r,i,e.args,t.cwd)}},{event:`tool_execution_end`,handle:async(e,t)=>{let n=v,r=p(e,`toolCallId`);n!==void 0&&n.context.sessionId===t.sessionId&&r&&(await g.end(r,e.isError===!0,t.cwd),v===n&&y(n))}}]}}export{m as createFileEditSession};
|
|
2
|
+
//# sourceMappingURL=fileEditSession.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fileEditSession.mjs","names":[],"sources":["../../src/controllers/fileEditSession.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\n\nimport {\n type DoomHeadlessExecutionContext,\n type DoomHeadlessActivity,\n type DoomHeadlessHook,\n} from '@agimon-ai/doompi-core/headless';\nimport type { DoomServerPluginContext, DoomServerSessionPlugin } from '@agimon-ai/doompi-core/server-facet';\n\nimport { createDoomIgnoreMatcher } from '../services/doomIgnore';\nimport { EditTracker } from '../services/editTracker';\nimport { FileEditPaths } from '../services/fileEditPaths';\nimport { NodeGitStatusAdapter } from '../services/gitStatus';\nimport { NodeSnapshotStoreAdapter } from '../services/snapshotStore';\nimport { TimelineStore } from '../services/timelineStore';\nimport { NodeTreeManifestAdapter } from '../services/treeManifest';\nimport { filesChannelType } from '../types/webFiles';\nimport { api } from './fileEditsApi';\nimport { readSessionFiles } from './webFilesChannel';\n\nconst DOOM_IGNORE_FILE = '.doomignore';\n\nfunction readDoomIgnore(cwd: string): ((filePath: string) => boolean) | undefined {\n try {\n const matcher = createDoomIgnoreMatcher(fs.readFileSync(path.join(cwd, DOOM_IGNORE_FILE), 'utf8'));\n return matcher === undefined ? undefined : (filePath: string) => matcher(path.relative(cwd, filePath));\n } catch {\n return undefined;\n }\n}\n\nfunction stringValue(value: Readonly<Record<string, unknown>>, key: string): string | undefined {\n const candidate = value[key];\n return typeof candidate === 'string' && candidate.length > 0 ? candidate : undefined;\n}\n\nexport function createFileEditSession({ agent, host }: DoomServerPluginContext): DoomServerSessionPlugin {\n if (!agent) return { api: [api] };\n const paths = new FileEditPaths();\n const timeline = new TimelineStore();\n const snapshots = new NodeSnapshotStoreAdapter();\n const editTracker = new EditTracker(timeline, snapshots, new NodeTreeManifestAdapter(), {\n git: new NodeGitStatusAdapter(),\n });\n if (host.context.directEvents === undefined)\n throw new Error('File-edit headless facet requires the session direct event bus.');\n const directEvents = host.context.directEvents;\n let active:\n | {\n readonly context: DoomHeadlessExecutionContext;\n readonly timelinePath: string;\n readonly snapshotsPath: string;\n }\n | undefined;\n const publish = (runtime: NonNullable<typeof active>): void => {\n directEvents.publish(filesChannelType, runtime.context.sessionId, {\n items: readSessionFiles(runtime.timelinePath, runtime.context.cwd),\n });\n };\n const activity: DoomHeadlessActivity = {\n name: 'file-edits',\n start: async (executionContext) => {\n const sessionKey = paths.sessionKey(executionContext.sessionId);\n const timelinePath = paths.timelinePath(executionContext.cwd, sessionKey);\n const snapshotsPath = paths.snapshotsPath(executionContext.cwd, sessionKey);\n timeline.initialize(timelinePath);\n snapshots.initialize(snapshotsPath);\n const isIgnored = readDoomIgnore(executionContext.cwd);\n editTracker.reset({\n exclude: [timelinePath, `${timelinePath}.lock`, snapshotsPath],\n ...(isIgnored === undefined ? {} : { isIgnored }),\n });\n const runtime = { context: executionContext, timelinePath, snapshotsPath };\n active = runtime;\n publish(runtime);\n return async () => {\n if (active !== runtime) return;\n active = undefined;\n editTracker.reset();\n };\n },\n };\n const startHook: DoomHeadlessHook = {\n event: 'tool_execution_start',\n handle: async (event, executionContext) => {\n const runtime = active;\n const toolCallId = stringValue(event, 'toolCallId');\n const toolName = stringValue(event, 'toolName');\n if (runtime === undefined || runtime.context.sessionId !== executionContext.sessionId || !toolCallId || !toolName)\n return;\n await editTracker.start(toolCallId, toolName, event.args, executionContext.cwd);\n },\n };\n const endHook: DoomHeadlessHook = {\n event: 'tool_execution_end',\n handle: async (event, executionContext) => {\n const runtime = active;\n const toolCallId = stringValue(event, 'toolCallId');\n if (runtime === undefined || runtime.context.sessionId !== executionContext.sessionId || !toolCallId) return;\n await editTracker.end(toolCallId, event.isError === true, executionContext.cwd);\n if (active === runtime) publish(runtime);\n },\n };\n return { api: [api], activities: [activity], hooks: [startHook, endHook] };\n}\n"],"mappings":"isBAuBA,SAAS,EAAe,EAA0D,CAChF,GAAI,CACF,IAAM,EAAU,EAAwB,EAAG,aAAa,EAAK,KAAK,EAAK,aAAgB,EAAG,MAAM,CAAC,EACjG,OAAO,IAAY,IAAA,GAAY,IAAA,GAAa,GAAqB,EAAQ,EAAK,SAAS,EAAK,CAAQ,CAAC,CACvG,MAAQ,CACN,MACF,CACF,CAEA,SAAS,EAAY,EAA0C,EAAiC,CAC9F,IAAM,EAAY,EAAM,GACxB,OAAO,OAAO,GAAc,UAAY,EAAU,OAAS,EAAI,EAAY,IAAA,EAC7E,CAEA,SAAgB,EAAsB,CAAE,QAAO,QAA0D,CACvG,GAAI,CAAC,EAAO,MAAO,CAAE,IAAK,CAAC,CAAG,CAAE,EAChC,IAAM,EAAQ,IAAI,EACZ,EAAW,IAAI,EACf,EAAY,IAAI,EAChB,EAAc,IAAI,EAAY,EAAU,EAAW,IAAI,EAA2B,CACtF,IAAK,IAAI,CACX,CAAC,EACD,GAAI,EAAK,QAAQ,eAAiB,IAAA,GAChC,MAAU,MAAM,iEAAiE,EACnF,IAAM,EAAe,EAAK,QAAQ,aAC9B,EAOE,EAAW,GAA8C,CAC7D,EAAa,QAAQ,EAAkB,EAAQ,QAAQ,UAAW,CAChE,MAAO,EAAiB,EAAQ,aAAc,EAAQ,QAAQ,GAAG,CACnE,CAAC,CACH,EA6CA,MAAO,CAAE,IAAK,CAAC,CAAG,EAAG,WAAY,CAAC,CA3ChC,KAAM,aACN,MAAO,KAAO,IAAqB,CACjC,IAAM,EAAa,EAAM,WAAW,EAAiB,SAAS,EACxD,EAAe,EAAM,aAAa,EAAiB,IAAK,CAAU,EAClE,EAAgB,EAAM,cAAc,EAAiB,IAAK,CAAU,EAC1E,EAAS,WAAW,CAAY,EAChC,EAAU,WAAW,CAAa,EAClC,IAAM,EAAY,EAAe,EAAiB,GAAG,EACrD,EAAY,MAAM,CAChB,QAAS,CAAC,EAAc,GAAG,EAAa,OAAQ,CAAa,EAC7D,GAAI,IAAc,IAAA,GAAY,CAAC,EAAI,CAAE,WAAU,CACjD,CAAC,EACD,IAAM,EAAU,CAAE,QAAS,EAAkB,eAAc,eAAc,EAGzE,MAFA,GAAS,EACT,EAAQ,CAAO,EACR,SAAY,CACb,IAAW,IACf,EAAS,IAAA,GACT,EAAY,MAAM,EACpB,CACF,CAuBuC,CAAC,EAAG,MAAO,CAAC,CApBnD,MAAO,uBACP,OAAQ,MAAO,EAAO,IAAqB,CACzC,IAAM,EAAU,EACV,EAAa,EAAY,EAAO,YAAY,EAC5C,EAAW,EAAY,EAAO,UAAU,EAC1C,IAAY,IAAA,IAAa,EAAQ,QAAQ,YAAc,EAAiB,WAAc,GAAe,GAEzG,MAAM,EAAY,MAAM,EAAY,EAAU,EAAM,KAAM,EAAiB,GAAG,CAChF,CAY2D,EAAG,CAT9D,MAAO,qBACP,OAAQ,MAAO,EAAO,IAAqB,CACzC,IAAM,EAAU,EACV,EAAa,EAAY,EAAO,YAAY,EAC9C,IAAY,IAAA,IAAa,EAAQ,QAAQ,YAAc,EAAiB,WAAc,IAC1F,MAAM,EAAY,IAAI,EAAY,EAAM,UAAY,GAAM,EAAiB,GAAG,EAC1E,IAAW,GAAS,EAAQ,CAAO,EACzC,CAEoE,CAAC,CAAE,CAC3E"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("../services/lineDiff/index.cjs"),n=require("../services/fileEditPaths/index.cjs"),r=require("../services/snapshotStore/index.cjs"),i=require("../services/fileChanges/index.cjs"),a=require("../services/timelineStore/index.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 previous content could not be captured: it was binary or past the size cap`,m=`the changed region was too large to match line by line`;function h(e){return(0,l.createHash)(`sha256`).update(e,`utf8`).digest(`hex`)}function g(e,t){return e instanceof Error&&`code`in e&&e.code===t}function _(e){return e instanceof Error?e.message:String(e)}async function v(e){let t;try{t=await 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:h(n),unavailable:!1}}function y(e,t){return t===e||t.startsWith(`${e}${c.default.sep}`)}async function b(e,t){let n,r;try{n=await s.default.realpath(e),r=await s.default.realpath(t)}catch{return!0}return y(n,r)}async function x(e,n){let r={index:e.index,tool:e.tool,at:e.at,origin:e.origin,additions:e.additions??0,removals:e.removals??0},i=e.before===void 0&&e.created===!0;if(e.after===void 0||e.before===void 0&&!i)return e.origin===`tool`?{...r,note:e.after===void 0?`the new content could not be captured: the file is binary or past the size cap`:p}:{...r,note:e.tool===`bash`?f:`recorded before this session began capturing content, so there is no diff for it`};let a=await n.read(e.after);if(a===void 0)return{...r,note:`the content behind this change is no longer held`};if(e.before===void 0){let e=t.lineDiffFromEmpty(a);return{...r,additions:e.additions,removals:e.removals,hunks:e.hunks,...e.approximate?{note:m}:{}}}let o=await n.read(e.before);if(o===void 0)return{...r,note:`the content behind this change is no longer held`};let s=t.lineDiff(o,a);return{...r,additions:s.additions,removals:s.removals,hunks:s.hunks,...s.approximate?{note:m}:{}}}async function S(e,n,r){let a=e[0]?.created===!0,o=i.baselineOf(e);if(!a&&o===void 0)return{additions:0,removals:0,note:e[0]?.origin===`tool`?p:f};if(n.unavailable)return{additions:0,removals:0,note:n.reason??`the file cannot be read`};if(a||o===void 0){let e=t.lineDiffFromEmpty(n.content);return{additions:e.additions,removals:e.removals,hunks:e.hunks,...e.approximate?{note:m}:{}}}let s=await r.read(o);if(s===void 0)return{additions:0,removals:0,note:`the content this session started from is no longer held`};let c=t.lineDiff(s,n.content);return{additions:c.additions,removals:c.removals,hunks:c.hunks,...c.approximate?{note:m}:{}}}function C(e={}){let t=e.timeline??new a.TimelineStore,i=e.snapshots??new r.NodeSnapshotStoreAdapter,l=e.paths??new n.FileEditPaths,d=e.cwd??``,f=new u.Hono;if(e.sessionId!==void 0&&d!==``){let n=l.sessionKey(e.sessionId);e.timeline===void 0&&t.initialize(l.timelinePath(d,n)),e.snapshots===void 0&&i.initialize(l.snapshotsPath(d,n))}let p=async e=>{if(e===void 0||e===``)return;let n=await t.versions(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 v(r),s={path:r,relPath:d===``?r:c.default.relative(d,r),versions:await Promise.all(n.map(e=>x(e,i))),cumulative:await S(n,a,i),working:a};return e.json(s)}),f.get(`/preview`,async e=>{let t=e.req.query(o.PATH_QUERY_PARAM);if(t===void 0||t===``||t.includes(`\0`))return e.json({error:`A preview names a path.`},400);if(d===``)return e.json({error:`This session has no working directory to read from.`},403);let n=c.default.resolve(d),r=c.default.resolve(n,t);if(!y(n,r)||!await b(n,r))return e.json({error:`The path leaves the session directory.`},403);let i={path:r,relPath:c.default.relative(n,r),working:await v(r)};return e.json(i)}),f.put(`/content`,async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:`The save body is not JSON.`},400)}if(typeof t.path!=`string`||typeof t.content!=`string`)return e.json({error:`A save names a path and the content to write.`},400);if(await p(t.path)===void 0)return e.json({error:`This session recorded no changes to that file.`},404);let n=c.default.resolve(d,t.path),r=await v(n);if(r.unavailable)return e.json({error:r.reason??`The file cannot be read.`},409);if(r.hash!==t.expectedHash)return e.json({error:`The file changed since it was opened.`,hash:r.hash},409);await s.default.writeFile(n,t.content,`utf8`);let i={hash:h(t.content)};return e.json(i)}),f.delete(`/content`,async e=>{let t=e.req.query(o.PATH_QUERY_PARAM);if(await p(t)===void 0||t===void 0)return e.json({error:`This session recorded no changes to that file.`},404);let n=c.default.resolve(d,t);try{await s.default.unlink(n)}catch(t){if(!g(t,`ENOENT`))return e.json({error:`The file could not be deleted: ${_(t)}`},409)}return e.body(null,204)}),f}const w={basePath:o.API_BASE_PATH,start(e){let t=C({...e.sessionId===void 0?{}:{sessionId:e.sessionId},...e.cwd===void 0?{}:{cwd:e.cwd}});return{fetch:e=>t.fetch(e),close:()=>void 0}}};exports.MAX_WORKING_BYTES=d,exports.api=w,exports.createFileEditsApi=C;
|
|
2
|
+
//# sourceMappingURL=fileEditsApi.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fileEditsApi.cjs","names":["createHash","fs","path","lineDiffFromEmpty","lineDiff","baselineOf","TimelineStore","NodeSnapshotStoreAdapter","FileEditPaths","Hono","PATH_QUERY_PARAM","API_BASE_PATH"],"sources":["../../src/controllers/fileEditsApi.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport fs from 'node:fs/promises';\nimport path from 'node:path';\n\nimport type { DoomApi, DoomApiContext, DoomApiHandler } from '@agimon-ai/doompi-core/package-api';\nimport { Hono } from 'hono';\n\nimport { baselineOf } from '../services/fileChanges';\nimport { FileEditPaths } from '../services/fileEditPaths';\nimport { lineDiff, lineDiffFromEmpty } from '../services/lineDiff';\nimport { NodeSnapshotStoreAdapter } from '../services/snapshotStore';\nimport { TimelineStore } from '../services/timelineStore';\nimport type { FileEditVersion } from '../types/domain';\nimport type { IFileEditPaths } from '../types/fileEditPaths';\nimport {\n API_BASE_PATH,\n type FileEditsCumulativeView,\n type FileEditsDetailView,\n type FileEditsPreviewView,\n type FileEditsSaveRequest,\n type FileEditsSaveView,\n type FileEditsVersionView,\n type FileEditsWorkingView,\n PATH_QUERY_PARAM,\n} from '../types/fileEditsApi';\nimport type { SnapshotStorePort } from '../types/snapshotStore';\nimport type { ITimelineStore } from '../types/timelineStore';\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 changed file,\n * because splitting it would make the page stitch three responses together for\n * one click. The save route is the only mutation, and it refuses unless the\n * reader proves they were looking at the content that is still on disk. The\n * preview route is the read-only way in to a file the session never changed,\n * bounded by the working directory instead of by the timeline.\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 NO_AFTER_NOTE = 'the new content could not be captured: the file is binary or past the size cap';\nconst NO_BEFORE_NOTE = 'the previous content could not be captured: it was binary or past the size cap';\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\nfunction isInside(root: string, candidate: string): boolean {\n return candidate === root || candidate.startsWith(`${root}${path.sep}`);\n}\n\n/**\n * The same containment check after symlinks are followed. A path that does not\n * exist yet resolves to nothing, and there is nothing to hand out, so it\n * passes and the read below reports it missing.\n */\nasync function isRealPathInside(root: string, candidate: string): Promise<boolean> {\n let realRoot: string;\n let real: string;\n try {\n realRoot = await fs.realpath(root);\n real = await fs.realpath(candidate);\n } catch {\n return true;\n }\n return isInside(realRoot, real);\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 *\n * A file this session created counts as captured on both sides. Its baseline is\n * empty rather than missing, so the whole file reads as arriving at once, which\n * is exactly what happened.\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 const createdHere = version.before === undefined && version.created === true;\n if (version.after === undefined || (version.before === undefined && !createdHere)) {\n // A scan found the path only after it changed, so no baseline exists. A\n // tool named the path but content was not stored: either it did not fit, or\n // the record predates content capture, which the tool tells apart.\n if (version.origin === 'tool') {\n return { ...view, note: version.after === undefined ? NO_AFTER_NOTE : NO_BEFORE_NOTE };\n }\n return { ...view, note: version.tool === 'bash' ? NO_BASELINE_NOTE : NO_SNAPSHOT_NOTE };\n }\n const after = await snapshots.read(version.after);\n if (after === undefined) {\n return { ...view, note: 'the content behind this change is no longer held' };\n }\n if (version.before === undefined) {\n const fresh = lineDiffFromEmpty(after);\n return {\n ...view,\n additions: fresh.additions,\n removals: fresh.removals,\n hunks: fresh.hunks,\n ...(fresh.approximate ? { note: APPROXIMATE_NOTE } : {}),\n };\n }\n const before = await snapshots.read(version.before);\n if (before === 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 // A file this session created starts from nothing, and nothing is a baseline\n // the snapshot store never has to hold.\n const createdHere = versions[0]?.created === true;\n const baseline = baselineOf(versions);\n if (!createdHere && baseline === undefined) {\n return {\n additions: 0,\n removals: 0,\n note: versions[0]?.origin === 'tool' ? NO_BEFORE_NOTE : NO_BASELINE_NOTE,\n };\n }\n if (working.unavailable) {\n return { additions: 0, removals: 0, note: working.reason ?? 'the file cannot be read' };\n }\n if (createdHere || baseline === undefined) {\n const fresh = lineDiffFromEmpty(working.content);\n return {\n additions: fresh.additions,\n removals: fresh.removals,\n hunks: fresh.hunks,\n ...(fresh.approximate ? { note: APPROXIMATE_NOTE } : {}),\n };\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 /**\n * The file a request names, read only, for a file the timeline knows nothing\n * about: a path the agent read but never changed is still a path the reader\n * clicked, and refusing it would leave the link dead.\n *\n * The working directory is the boundary here, since the timeline is not.\n * Containment is checked lexically and again on the real path, so a symlink\n * inside the tree cannot hand out something beyond it, and a host that gave\n * these routes no working directory has no boundary to enforce and so serves\n * nothing.\n */\n app.get('/preview', async (context) => {\n const requested = context.req.query(PATH_QUERY_PARAM);\n if (requested === undefined || requested === '' || requested.includes('\\0')) {\n return context.json({ error: 'A preview names a path.' }, 400);\n }\n if (cwd === '') return context.json({ error: 'This session has no working directory to read from.' }, 403);\n const root = path.resolve(cwd);\n const filePath = path.resolve(root, requested);\n if (!isInside(root, filePath) || !(await isRealPathInside(root, filePath))) {\n return context.json({ error: 'The path leaves the session directory.' }, 403);\n }\n const body: FileEditsPreviewView = {\n path: filePath,\n relPath: path.relative(root, filePath),\n working: await readWorking(filePath),\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
|
+
import{lineDiff as e,lineDiffFromEmpty as t}from"../services/lineDiff/index.mjs";import{FileEditPaths as n}from"../services/fileEditPaths/index.mjs";import{NodeSnapshotStoreAdapter as r}from"../services/snapshotStore/index.mjs";import{baselineOf as i}from"../services/fileChanges/index.mjs";import{TimelineStore as a}from"../services/timelineStore/index.mjs";import{API_BASE_PATH as o,PATH_QUERY_PARAM as s}from"../types/fileEditsApi.mjs";import c from"node:fs/promises";import l from"node:path";import{createHash as u}from"node:crypto";import{Hono as d}from"hono";const f=1048576,p=`changed by a command, so no baseline was captured; showing the file as it stands`,m=`the previous content could not be captured: it was binary or past the size cap`,h=`the changed region was too large to match line by line`;function g(e){return u(`sha256`).update(e,`utf8`).digest(`hex`)}function _(e,t){return e instanceof Error&&`code`in e&&e.code===t}function v(e){return e instanceof Error?e.message:String(e)}async function y(e){let t;try{t=await c.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 ${f} 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:g(n),unavailable:!1}}function b(e,t){return t===e||t.startsWith(`${e}${l.sep}`)}async function x(e,t){let n,r;try{n=await c.realpath(e),r=await c.realpath(t)}catch{return!0}return b(n,r)}async function S(n,r){let i={index:n.index,tool:n.tool,at:n.at,origin:n.origin,additions:n.additions??0,removals:n.removals??0},a=n.before===void 0&&n.created===!0;if(n.after===void 0||n.before===void 0&&!a)return n.origin===`tool`?{...i,note:n.after===void 0?`the new content could not be captured: the file is binary or past the size cap`:m}:{...i,note:n.tool===`bash`?p:`recorded before this session began capturing content, so there is no diff for it`};let o=await r.read(n.after);if(o===void 0)return{...i,note:`the content behind this change is no longer held`};if(n.before===void 0){let e=t(o);return{...i,additions:e.additions,removals:e.removals,hunks:e.hunks,...e.approximate?{note:h}:{}}}let s=await r.read(n.before);if(s===void 0)return{...i,note:`the content behind this change is no longer held`};let c=e(s,o);return{...i,additions:c.additions,removals:c.removals,hunks:c.hunks,...c.approximate?{note:h}:{}}}async function C(n,r,a){let o=n[0]?.created===!0,s=i(n);if(!o&&s===void 0)return{additions:0,removals:0,note:n[0]?.origin===`tool`?m:p};if(r.unavailable)return{additions:0,removals:0,note:r.reason??`the file cannot be read`};if(o||s===void 0){let e=t(r.content);return{additions:e.additions,removals:e.removals,hunks:e.hunks,...e.approximate?{note:h}:{}}}let c=await a.read(s);if(c===void 0)return{additions:0,removals:0,note:`the content this session started from is no longer held`};let l=e(c,r.content);return{additions:l.additions,removals:l.removals,hunks:l.hunks,...l.approximate?{note:h}:{}}}function w(e={}){let t=e.timeline??new a,i=e.snapshots??new r,o=e.paths??new n,u=e.cwd??``,f=new d;if(e.sessionId!==void 0&&u!==``){let n=o.sessionKey(e.sessionId);e.timeline===void 0&&t.initialize(o.timelinePath(u,n)),e.snapshots===void 0&&i.initialize(o.snapshotsPath(u,n))}let p=async e=>{if(e===void 0||e===``)return;let n=await t.versions(l.resolve(u,e));return n.length===0?void 0:n};return f.get(`/detail`,async e=>{let t=e.req.query(s),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=l.resolve(u,t),a=await y(r),o={path:r,relPath:u===``?r:l.relative(u,r),versions:await Promise.all(n.map(e=>S(e,i))),cumulative:await C(n,a,i),working:a};return e.json(o)}),f.get(`/preview`,async e=>{let t=e.req.query(s);if(t===void 0||t===``||t.includes(`\0`))return e.json({error:`A preview names a path.`},400);if(u===``)return e.json({error:`This session has no working directory to read from.`},403);let n=l.resolve(u),r=l.resolve(n,t);if(!b(n,r)||!await x(n,r))return e.json({error:`The path leaves the session directory.`},403);let i={path:r,relPath:l.relative(n,r),working:await y(r)};return e.json(i)}),f.put(`/content`,async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:`The save body is not JSON.`},400)}if(typeof t.path!=`string`||typeof t.content!=`string`)return e.json({error:`A save names a path and the content to write.`},400);if(await p(t.path)===void 0)return e.json({error:`This session recorded no changes to that file.`},404);let n=l.resolve(u,t.path),r=await y(n);if(r.unavailable)return e.json({error:r.reason??`The file cannot be read.`},409);if(r.hash!==t.expectedHash)return e.json({error:`The file changed since it was opened.`,hash:r.hash},409);await c.writeFile(n,t.content,`utf8`);let i={hash:g(t.content)};return e.json(i)}),f.delete(`/content`,async e=>{let t=e.req.query(s);if(await p(t)===void 0||t===void 0)return e.json({error:`This session recorded no changes to that file.`},404);let n=l.resolve(u,t);try{await c.unlink(n)}catch(t){if(!_(t,`ENOENT`))return e.json({error:`The file could not be deleted: ${v(t)}`},409)}return e.body(null,204)}),f}const T={basePath:o,start(e){let t=w({...e.sessionId===void 0?{}:{sessionId:e.sessionId},...e.cwd===void 0?{}:{cwd:e.cwd}});return{fetch:e=>t.fetch(e),close:()=>void 0}}};export{f as MAX_WORKING_BYTES,T as api,w as createFileEditsApi};
|
|
2
|
+
//# sourceMappingURL=fileEditsApi.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fileEditsApi.mjs","names":[],"sources":["../../src/controllers/fileEditsApi.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport fs from 'node:fs/promises';\nimport path from 'node:path';\n\nimport type { DoomApi, DoomApiContext, DoomApiHandler } from '@agimon-ai/doompi-core/package-api';\nimport { Hono } from 'hono';\n\nimport { baselineOf } from '../services/fileChanges';\nimport { FileEditPaths } from '../services/fileEditPaths';\nimport { lineDiff, lineDiffFromEmpty } from '../services/lineDiff';\nimport { NodeSnapshotStoreAdapter } from '../services/snapshotStore';\nimport { TimelineStore } from '../services/timelineStore';\nimport type { FileEditVersion } from '../types/domain';\nimport type { IFileEditPaths } from '../types/fileEditPaths';\nimport {\n API_BASE_PATH,\n type FileEditsCumulativeView,\n type FileEditsDetailView,\n type FileEditsPreviewView,\n type FileEditsSaveRequest,\n type FileEditsSaveView,\n type FileEditsVersionView,\n type FileEditsWorkingView,\n PATH_QUERY_PARAM,\n} from '../types/fileEditsApi';\nimport type { SnapshotStorePort } from '../types/snapshotStore';\nimport type { ITimelineStore } from '../types/timelineStore';\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 changed file,\n * because splitting it would make the page stitch three responses together for\n * one click. The save route is the only mutation, and it refuses unless the\n * reader proves they were looking at the content that is still on disk. The\n * preview route is the read-only way in to a file the session never changed,\n * bounded by the working directory instead of by the timeline.\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 NO_AFTER_NOTE = 'the new content could not be captured: the file is binary or past the size cap';\nconst NO_BEFORE_NOTE = 'the previous content could not be captured: it was binary or past the size cap';\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\nfunction isInside(root: string, candidate: string): boolean {\n return candidate === root || candidate.startsWith(`${root}${path.sep}`);\n}\n\n/**\n * The same containment check after symlinks are followed. A path that does not\n * exist yet resolves to nothing, and there is nothing to hand out, so it\n * passes and the read below reports it missing.\n */\nasync function isRealPathInside(root: string, candidate: string): Promise<boolean> {\n let realRoot: string;\n let real: string;\n try {\n realRoot = await fs.realpath(root);\n real = await fs.realpath(candidate);\n } catch {\n return true;\n }\n return isInside(realRoot, real);\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 *\n * A file this session created counts as captured on both sides. Its baseline is\n * empty rather than missing, so the whole file reads as arriving at once, which\n * is exactly what happened.\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 const createdHere = version.before === undefined && version.created === true;\n if (version.after === undefined || (version.before === undefined && !createdHere)) {\n // A scan found the path only after it changed, so no baseline exists. A\n // tool named the path but content was not stored: either it did not fit, or\n // the record predates content capture, which the tool tells apart.\n if (version.origin === 'tool') {\n return { ...view, note: version.after === undefined ? NO_AFTER_NOTE : NO_BEFORE_NOTE };\n }\n return { ...view, note: version.tool === 'bash' ? NO_BASELINE_NOTE : NO_SNAPSHOT_NOTE };\n }\n const after = await snapshots.read(version.after);\n if (after === undefined) {\n return { ...view, note: 'the content behind this change is no longer held' };\n }\n if (version.before === undefined) {\n const fresh = lineDiffFromEmpty(after);\n return {\n ...view,\n additions: fresh.additions,\n removals: fresh.removals,\n hunks: fresh.hunks,\n ...(fresh.approximate ? { note: APPROXIMATE_NOTE } : {}),\n };\n }\n const before = await snapshots.read(version.before);\n if (before === 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 // A file this session created starts from nothing, and nothing is a baseline\n // the snapshot store never has to hold.\n const createdHere = versions[0]?.created === true;\n const baseline = baselineOf(versions);\n if (!createdHere && baseline === undefined) {\n return {\n additions: 0,\n removals: 0,\n note: versions[0]?.origin === 'tool' ? NO_BEFORE_NOTE : NO_BASELINE_NOTE,\n };\n }\n if (working.unavailable) {\n return { additions: 0, removals: 0, note: working.reason ?? 'the file cannot be read' };\n }\n if (createdHere || baseline === undefined) {\n const fresh = lineDiffFromEmpty(working.content);\n return {\n additions: fresh.additions,\n removals: fresh.removals,\n hunks: fresh.hunks,\n ...(fresh.approximate ? { note: APPROXIMATE_NOTE } : {}),\n };\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 /**\n * The file a request names, read only, for a file the timeline knows nothing\n * about: a path the agent read but never changed is still a path the reader\n * clicked, and refusing it would leave the link dead.\n *\n * The working directory is the boundary here, since the timeline is not.\n * Containment is checked lexically and again on the real path, so a symlink\n * inside the tree cannot hand out something beyond it, and a host that gave\n * these routes no working directory has no boundary to enforce and so serves\n * nothing.\n */\n app.get('/preview', async (context) => {\n const requested = context.req.query(PATH_QUERY_PARAM);\n if (requested === undefined || requested === '' || requested.includes('\\0')) {\n return context.json({ error: 'A preview names a path.' }, 400);\n }\n if (cwd === '') return context.json({ error: 'This session has no working directory to read from.' }, 403);\n const root = path.resolve(cwd);\n const filePath = path.resolve(root, requested);\n if (!isInside(root, filePath) || !(await isRealPathInside(root, filePath))) {\n return context.json({ error: 'The path leaves the session directory.' }, 403);\n }\n const body: FileEditsPreviewView = {\n path: filePath,\n relPath: path.relative(root, filePath),\n working: await readWorking(filePath),\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"),t=require("../services/fileEditPaths/index.cjs"),n=require("../services/fileChanges/index.cjs"),r=require("../services/doomIgnore/index.cjs"),i=require("../types/webFiles.cjs");let a=require("node:path");a=e.__toESM(a,1);let o=require("node:fs");o=e.__toESM(o,1);function s(e){try{return o.default.statSync(e).isFile()}catch{return!1}}function c(e,t){let i;try{i=o.default.readFileSync(e,`utf8`)}catch{return[]}let c=n.confirmedChanges(n.parseTimeline(i)),l=n.foldEntries(c).filter(e=>s(e.path)).map(e=>({path:e.path,relPath:a.default.relative(t,e.path)||e.path,tool:e.tool,at:e.at,count:e.count,diffable:n.isDiffable(n.foldVersions(c,e.path))}));try{let e=o.default.readFileSync(a.default.join(t,`.doomignore`),`utf8`);return r.filterDoomIgnoredFiles(l,e)}catch{return l}}function l(e){return typeof e==`object`&&!!e&&Array.isArray(e.items)}function u(){return{frameType:i.filesChannelType,start(e){let n=new Map,r=new Map,a=new t.FileEditPaths;return{payloadFor(e){let t=n.get(e.sessionId);return t===void 0?void 0:{items:t}},sessionAdded(t){r.get(t.sessionId)?.();let o;try{o=a.timelinePath(t.cwd,a.sessionKey(t.sessionId))}catch{}let s=o===void 0?[]:c(o,t.cwd);n.set(t.sessionId,s),e.publish(t.sessionId,{items:s});let u=e.directEvents.subscribe(i.filesChannelType,t.sessionId,r=>{l(r)&&(n.set(t.sessionId,r.items),e.publish(t.sessionId,r))},{replayLatest:!0});r.set(t.sessionId,u)},sessionRemoved(e){r.get(e)?.(),r.delete(e),n.delete(e)},close(){for(let e of r.values())e();r.clear(),n.clear()}}}}}exports.createFilesChannel=u,exports.readSessionFiles=c;
|
|
2
|
+
//# sourceMappingURL=webFilesChannel.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webFilesChannel.cjs","names":["fs","confirmedChanges","parseTimeline","foldEntries","path","isDiffable","foldVersions","filterDoomIgnoredFiles","filesChannelType","FileEditPaths"],"sources":["../../src/controllers/webFilesChannel.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\n\nimport type { DoomHubChannel, DoomHubChannelSource } from '@agimon-ai/doompi-core/hub-channel';\n\nimport { filterDoomIgnoredFiles } from '../services/doomIgnore';\nimport { confirmedChanges, foldEntries, foldVersions, isDiffable, parseTimeline } from '../services/fileChanges';\nimport { FileEditPaths } from '../services/fileEditPaths';\nimport { filesChannelType, type FilesItemView } from '../types/webFiles';\n\n/**\n * The changed-files channel consumes snapshots emitted by the owning session\n * facet. Durable timeline reads seed a channel that starts after session startup.\n */\n\n/** Whether the file is still there to be opened. */\nfunction stillExists(filePath: string): boolean {\n try {\n return fs.statSync(filePath).isFile();\n } catch {\n return false;\n }\n}\n\n/**\n * Reads one session's timeline and presents it as the rows the dock lists.\n *\n * A file the session changed and then removed is left out, and so is a path a\n * bash call only touched: a command that moves a modification time without\n * changing a byte is not an edit, and listing it buries the ones that are. The\n * dock is a list of things to open, and a row that can only ever answer \"this\n * is gone\" is a dead end; the timeline still holds the change, so a tab already\n * open on the file keeps working and says so.\n */\nexport function readSessionFiles(timelinePath: string, cwd: string): FilesItemView[] {\n let content: string;\n try {\n content = fs.readFileSync(timelinePath, 'utf8');\n } catch {\n return [];\n }\n const events = confirmedChanges(parseTimeline(content));\n const items = foldEntries(events)\n .filter((entry) => stillExists(entry.path))\n .map((entry) => ({\n path: entry.path,\n relPath: path.relative(cwd, entry.path) || entry.path,\n tool: entry.tool,\n at: entry.at,\n count: entry.count,\n diffable: isDiffable(foldVersions(events, entry.path)),\n }));\n\n try {\n const doomIgnore = fs.readFileSync(path.join(cwd, '.doomignore'), 'utf8');\n return filterDoomIgnoredFiles(items, doomIgnore);\n } catch {\n return items;\n }\n}\n\nfunction filesPayload(value: unknown): value is { items: FilesItemView[] } {\n return typeof value === 'object' && value !== null && Array.isArray((value as { items?: unknown }).items);\n}\n\n/**\n * The files data channel: one direct-event subscription per managed session,\n * published as { items } payloads under the 'file_edits' frame type.\n */\nexport function createFilesChannel(): DoomHubChannel {\n return {\n frameType: filesChannelType,\n start(host) {\n const latest = new Map<string, FilesItemView[]>();\n const subscriptions = new Map<string, () => void>();\n const paths = new FileEditPaths();\n const channelSource: DoomHubChannelSource = {\n payloadFor(scope) {\n const items = latest.get(scope.sessionId);\n return items === undefined ? undefined : { items };\n },\n sessionAdded(scope) {\n subscriptions.get(scope.sessionId)?.();\n let timelinePath: string | undefined;\n try {\n timelinePath = paths.timelinePath(scope.cwd, paths.sessionKey(scope.sessionId));\n } catch {\n // The session has no readable state path yet.\n }\n const items = timelinePath === undefined ? [] : readSessionFiles(timelinePath, scope.cwd);\n latest.set(scope.sessionId, items);\n host.publish(scope.sessionId, { items });\n const unsubscribe = host.directEvents.subscribe(\n filesChannelType,\n scope.sessionId,\n (payload) => {\n if (!filesPayload(payload)) return;\n latest.set(scope.sessionId, payload.items);\n host.publish(scope.sessionId, payload);\n },\n { replayLatest: true },\n );\n subscriptions.set(scope.sessionId, unsubscribe);\n },\n sessionRemoved(sessionId) {\n subscriptions.get(sessionId)?.();\n subscriptions.delete(sessionId);\n latest.delete(sessionId);\n },\n close() {\n for (const unsubscribe of subscriptions.values()) unsubscribe();\n subscriptions.clear();\n latest.clear();\n },\n };\n return channelSource;\n },\n };\n}\n"],"mappings":"4TAgBA,SAAS,EAAY,EAA2B,CAC9C,GAAI,CACF,OAAOA,EAAAA,QAAG,SAAS,CAAQ,CAAC,CAAC,OAAO,CACtC,MAAQ,CACN,MAAO,EACT,CACF,CAYA,SAAgB,EAAiB,EAAsB,EAA8B,CACnF,IAAI,EACJ,GAAI,CACF,EAAUA,EAAAA,QAAG,aAAa,EAAc,MAAM,CAChD,MAAQ,CACN,MAAO,CAAC,CACV,CACA,IAAM,EAASC,EAAAA,iBAAiBC,EAAAA,cAAc,CAAO,CAAC,EAChD,EAAQC,EAAAA,YAAY,CAAM,CAAC,CAC9B,OAAQ,GAAU,EAAY,EAAM,IAAI,CAAC,CAAC,CAC1C,IAAK,IAAW,CACf,KAAM,EAAM,KACZ,QAASC,EAAAA,QAAK,SAAS,EAAK,EAAM,IAAI,GAAK,EAAM,KACjD,KAAM,EAAM,KACZ,GAAI,EAAM,GACV,MAAO,EAAM,MACb,SAAUC,EAAAA,WAAWC,EAAAA,aAAa,EAAQ,EAAM,IAAI,CAAC,CACvD,EAAE,EAEJ,GAAI,CACF,IAAM,EAAaN,EAAAA,QAAG,aAAaI,EAAAA,QAAK,KAAK,EAAK,aAAa,EAAG,MAAM,EACxE,OAAOG,EAAAA,uBAAuB,EAAO,CAAU,CACjD,MAAQ,CACN,OAAO,CACT,CACF,CAEA,SAAS,EAAa,EAAqD,CACzE,OAAO,OAAO,GAAU,YAAY,GAAkB,MAAM,QAAS,EAA8B,KAAK,CAC1G,CAMA,SAAgB,GAAqC,CACnD,MAAO,CACL,UAAWC,EAAAA,iBACX,MAAM,EAAM,CACV,IAAM,EAAS,IAAI,IACb,EAAgB,IAAI,IACpB,EAAQ,IAAIC,EAAAA,cAwClB,MAAO,CAtCL,WAAW,EAAO,CAChB,IAAM,EAAQ,EAAO,IAAI,EAAM,SAAS,EACxC,OAAO,IAAU,IAAA,GAAY,IAAA,GAAY,CAAE,OAAM,CACnD,EACA,aAAa,EAAO,CAClB,EAAc,IAAI,EAAM,SAAS,CAAC,GAAG,EACrC,IAAI,EACJ,GAAI,CACF,EAAe,EAAM,aAAa,EAAM,IAAK,EAAM,WAAW,EAAM,SAAS,CAAC,CAChF,MAAQ,CAER,CACA,IAAM,EAAQ,IAAiB,IAAA,GAAY,CAAC,EAAI,EAAiB,EAAc,EAAM,GAAG,EACxF,EAAO,IAAI,EAAM,UAAW,CAAK,EACjC,EAAK,QAAQ,EAAM,UAAW,CAAE,OAAM,CAAC,EACvC,IAAM,EAAc,EAAK,aAAa,UACpCD,EAAAA,iBACA,EAAM,UACL,GAAY,CACN,EAAa,CAAO,IACzB,EAAO,IAAI,EAAM,UAAW,EAAQ,KAAK,EACzC,EAAK,QAAQ,EAAM,UAAW,CAAO,EACvC,EACA,CAAE,aAAc,EAAK,CACvB,EACA,EAAc,IAAI,EAAM,UAAW,CAAW,CAChD,EACA,eAAe,EAAW,CACxB,EAAc,IAAI,CAAS,CAAC,GAAG,EAC/B,EAAc,OAAO,CAAS,EAC9B,EAAO,OAAO,CAAS,CACzB,EACA,OAAQ,CACN,IAAK,IAAM,KAAe,EAAc,OAAO,EAAG,EAAY,EAC9D,EAAc,MAAM,EACpB,EAAO,MAAM,CACf,CAEiB,CACrB,CACF,CACF"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{FileEditPaths as e}from"../services/fileEditPaths/index.mjs";import{confirmedChanges as t,foldEntries as n,foldVersions as r,isDiffable as i,parseTimeline as a}from"../services/fileChanges/index.mjs";import{filterDoomIgnoredFiles as o}from"../services/doomIgnore/index.mjs";import{filesChannelType as s}from"../types/webFiles.mjs";import c from"node:path";import l from"node:fs";function u(e){try{return l.statSync(e).isFile()}catch{return!1}}function d(e,s){let d;try{d=l.readFileSync(e,`utf8`)}catch{return[]}let f=t(a(d)),p=n(f).filter(e=>u(e.path)).map(e=>({path:e.path,relPath:c.relative(s,e.path)||e.path,tool:e.tool,at:e.at,count:e.count,diffable:i(r(f,e.path))}));try{let e=l.readFileSync(c.join(s,`.doomignore`),`utf8`);return o(p,e)}catch{return p}}function f(e){return typeof e==`object`&&!!e&&Array.isArray(e.items)}function p(){return{frameType:s,start(t){let n=new Map,r=new Map,i=new e;return{payloadFor(e){let t=n.get(e.sessionId);return t===void 0?void 0:{items:t}},sessionAdded(e){r.get(e.sessionId)?.();let a;try{a=i.timelinePath(e.cwd,i.sessionKey(e.sessionId))}catch{}let o=a===void 0?[]:d(a,e.cwd);n.set(e.sessionId,o),t.publish(e.sessionId,{items:o});let c=t.directEvents.subscribe(s,e.sessionId,r=>{f(r)&&(n.set(e.sessionId,r.items),t.publish(e.sessionId,r))},{replayLatest:!0});r.set(e.sessionId,c)},sessionRemoved(e){r.get(e)?.(),r.delete(e),n.delete(e)},close(){for(let e of r.values())e();r.clear(),n.clear()}}}}}export{p as createFilesChannel,d as readSessionFiles};
|
|
2
|
+
//# sourceMappingURL=webFilesChannel.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webFilesChannel.mjs","names":[],"sources":["../../src/controllers/webFilesChannel.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\n\nimport type { DoomHubChannel, DoomHubChannelSource } from '@agimon-ai/doompi-core/hub-channel';\n\nimport { filterDoomIgnoredFiles } from '../services/doomIgnore';\nimport { confirmedChanges, foldEntries, foldVersions, isDiffable, parseTimeline } from '../services/fileChanges';\nimport { FileEditPaths } from '../services/fileEditPaths';\nimport { filesChannelType, type FilesItemView } from '../types/webFiles';\n\n/**\n * The changed-files channel consumes snapshots emitted by the owning session\n * facet. Durable timeline reads seed a channel that starts after session startup.\n */\n\n/** Whether the file is still there to be opened. */\nfunction stillExists(filePath: string): boolean {\n try {\n return fs.statSync(filePath).isFile();\n } catch {\n return false;\n }\n}\n\n/**\n * Reads one session's timeline and presents it as the rows the dock lists.\n *\n * A file the session changed and then removed is left out, and so is a path a\n * bash call only touched: a command that moves a modification time without\n * changing a byte is not an edit, and listing it buries the ones that are. The\n * dock is a list of things to open, and a row that can only ever answer \"this\n * is gone\" is a dead end; the timeline still holds the change, so a tab already\n * open on the file keeps working and says so.\n */\nexport function readSessionFiles(timelinePath: string, cwd: string): FilesItemView[] {\n let content: string;\n try {\n content = fs.readFileSync(timelinePath, 'utf8');\n } catch {\n return [];\n }\n const events = confirmedChanges(parseTimeline(content));\n const items = foldEntries(events)\n .filter((entry) => stillExists(entry.path))\n .map((entry) => ({\n path: entry.path,\n relPath: path.relative(cwd, entry.path) || entry.path,\n tool: entry.tool,\n at: entry.at,\n count: entry.count,\n diffable: isDiffable(foldVersions(events, entry.path)),\n }));\n\n try {\n const doomIgnore = fs.readFileSync(path.join(cwd, '.doomignore'), 'utf8');\n return filterDoomIgnoredFiles(items, doomIgnore);\n } catch {\n return items;\n }\n}\n\nfunction filesPayload(value: unknown): value is { items: FilesItemView[] } {\n return typeof value === 'object' && value !== null && Array.isArray((value as { items?: unknown }).items);\n}\n\n/**\n * The files data channel: one direct-event subscription per managed session,\n * published as { items } payloads under the 'file_edits' frame type.\n */\nexport function createFilesChannel(): DoomHubChannel {\n return {\n frameType: filesChannelType,\n start(host) {\n const latest = new Map<string, FilesItemView[]>();\n const subscriptions = new Map<string, () => void>();\n const paths = new FileEditPaths();\n const channelSource: DoomHubChannelSource = {\n payloadFor(scope) {\n const items = latest.get(scope.sessionId);\n return items === undefined ? undefined : { items };\n },\n sessionAdded(scope) {\n subscriptions.get(scope.sessionId)?.();\n let timelinePath: string | undefined;\n try {\n timelinePath = paths.timelinePath(scope.cwd, paths.sessionKey(scope.sessionId));\n } catch {\n // The session has no readable state path yet.\n }\n const items = timelinePath === undefined ? [] : readSessionFiles(timelinePath, scope.cwd);\n latest.set(scope.sessionId, items);\n host.publish(scope.sessionId, { items });\n const unsubscribe = host.directEvents.subscribe(\n filesChannelType,\n scope.sessionId,\n (payload) => {\n if (!filesPayload(payload)) return;\n latest.set(scope.sessionId, payload.items);\n host.publish(scope.sessionId, payload);\n },\n { replayLatest: true },\n );\n subscriptions.set(scope.sessionId, unsubscribe);\n },\n sessionRemoved(sessionId) {\n subscriptions.get(sessionId)?.();\n subscriptions.delete(sessionId);\n latest.delete(sessionId);\n },\n close() {\n for (const unsubscribe of subscriptions.values()) unsubscribe();\n subscriptions.clear();\n latest.clear();\n },\n };\n return channelSource;\n },\n };\n}\n"],"mappings":"kYAgBA,SAAS,EAAY,EAA2B,CAC9C,GAAI,CACF,OAAO,EAAG,SAAS,CAAQ,CAAC,CAAC,OAAO,CACtC,MAAQ,CACN,MAAO,EACT,CACF,CAYA,SAAgB,EAAiB,EAAsB,EAA8B,CACnF,IAAI,EACJ,GAAI,CACF,EAAU,EAAG,aAAa,EAAc,MAAM,CAChD,MAAQ,CACN,MAAO,CAAC,CACV,CACA,IAAM,EAAS,EAAiB,EAAc,CAAO,CAAC,EAChD,EAAQ,EAAY,CAAM,CAAC,CAC9B,OAAQ,GAAU,EAAY,EAAM,IAAI,CAAC,CAAC,CAC1C,IAAK,IAAW,CACf,KAAM,EAAM,KACZ,QAAS,EAAK,SAAS,EAAK,EAAM,IAAI,GAAK,EAAM,KACjD,KAAM,EAAM,KACZ,GAAI,EAAM,GACV,MAAO,EAAM,MACb,SAAU,EAAW,EAAa,EAAQ,EAAM,IAAI,CAAC,CACvD,EAAE,EAEJ,GAAI,CACF,IAAM,EAAa,EAAG,aAAa,EAAK,KAAK,EAAK,aAAa,EAAG,MAAM,EACxE,OAAO,EAAuB,EAAO,CAAU,CACjD,MAAQ,CACN,OAAO,CACT,CACF,CAEA,SAAS,EAAa,EAAqD,CACzE,OAAO,OAAO,GAAU,YAAY,GAAkB,MAAM,QAAS,EAA8B,KAAK,CAC1G,CAMA,SAAgB,GAAqC,CACnD,MAAO,CACL,UAAW,EACX,MAAM,EAAM,CACV,IAAM,EAAS,IAAI,IACb,EAAgB,IAAI,IACpB,EAAQ,IAAI,EAwClB,MAAO,CAtCL,WAAW,EAAO,CAChB,IAAM,EAAQ,EAAO,IAAI,EAAM,SAAS,EACxC,OAAO,IAAU,IAAA,GAAY,IAAA,GAAY,CAAE,OAAM,CACnD,EACA,aAAa,EAAO,CAClB,EAAc,IAAI,EAAM,SAAS,CAAC,GAAG,EACrC,IAAI,EACJ,GAAI,CACF,EAAe,EAAM,aAAa,EAAM,IAAK,EAAM,WAAW,EAAM,SAAS,CAAC,CAChF,MAAQ,CAER,CACA,IAAM,EAAQ,IAAiB,IAAA,GAAY,CAAC,EAAI,EAAiB,EAAc,EAAM,GAAG,EACxF,EAAO,IAAI,EAAM,UAAW,CAAK,EACjC,EAAK,QAAQ,EAAM,UAAW,CAAE,OAAM,CAAC,EACvC,IAAM,EAAc,EAAK,aAAa,UACpC,EACA,EAAM,UACL,GAAY,CACN,EAAa,CAAO,IACzB,EAAO,IAAI,EAAM,UAAW,EAAQ,KAAK,EACzC,EAAK,QAAQ,EAAM,UAAW,CAAO,EACvC,EACA,CAAE,aAAc,EAAK,CACvB,EACA,EAAc,IAAI,EAAM,UAAW,CAAW,CAChD,EACA,eAAe,EAAW,CACxB,EAAc,IAAI,CAAS,CAAC,GAAG,EAC/B,EAAc,OAAO,CAAS,EAC9B,EAAO,OAAO,CAAS,CACzB,EACA,OAAQ,CACN,IAAK,IAAM,KAAe,EAAc,OAAO,EAAG,EAAY,EAC9D,EAAc,MAAM,EACpB,EAAO,MAAM,CACf,CAEiB,CACrB,CACF,CACF"}
|
package/dist/extensions/pi.cjs
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
const e=require("../
|
|
1
|
+
Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:`Module`}});const e=require("../tui/fileEditDependencies.cjs"),t=require("../constants/package.cjs"),n=require("../controllers/fileEditRuntime.cjs"),r=(0,require("@agimon-ai/doompi-core/pi-extension").definePiExtension)(t.PACKAGE_SOURCE,()=>n.createFileEditRuntime(e.createFileEditDependencies()));exports.default=r,exports.fileEditExtension=r;
|
|
2
|
+
//# sourceMappingURL=pi.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pi.cjs","names":["definePiExtension","PACKAGE_SOURCE","createFileEditRuntime","createFileEditDependencies"],"sources":["../../src/extensions/pi.ts"],"sourcesContent":["import { definePiExtension } from '@agimon-ai/doompi-core/pi-extension';\n\nimport { PACKAGE_SOURCE } from '../constants/package';\nimport { createFileEditRuntime } from '../controllers/fileEditRuntime';\nimport { createFileEditDependencies } from '../tui/fileEditDependencies';\nexport const fileEditExtension = definePiExtension(PACKAGE_SOURCE, () =>\n createFileEditRuntime(createFileEditDependencies()),\n);\nexport default fileEditExtension;\n"],"mappings":"wOAKa,GAAA,+CAAoBA,CAAAA,CAAAA,kBAAAA,CAAkBC,EAAAA,mBACjDC,EAAAA,sBAAsBC,EAAAA,2BAA2B,CAAC,CACpD"}
|
package/dist/extensions/pi.d.cts
CHANGED
|
@@ -1,2 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
1
|
+
//#region src/extensions/pi.d.ts
|
|
2
|
+
export declare const fileEditExtension: import("@agimon-ai/doompi-core/runtime-pi-extension").DefinedPiExtension<undefined>;
|
|
3
|
+
//#endregion
|
|
4
|
+
export { fileEditExtension as default };
|
|
5
|
+
//# sourceMappingURL=pi.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pi.d.cts","names":[],"sources":["../../src/extensions/pi.ts"],"mappings":";qBAKa,yEAAiB"}
|
package/dist/extensions/pi.d.mts
CHANGED
|
@@ -1,2 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
1
|
+
//#region src/extensions/pi.d.ts
|
|
2
|
+
export declare const fileEditExtension: import("@agimon-ai/doompi-core/runtime-pi-extension").DefinedPiExtension<undefined>;
|
|
3
|
+
//#endregion
|
|
4
|
+
export { fileEditExtension as default };
|
|
5
|
+
//# sourceMappingURL=pi.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pi.d.mts","names":[],"sources":["../../src/extensions/pi.ts"],"mappings":";qBAKa,yEAAiB"}
|
package/dist/extensions/pi.mjs
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{createFileEditDependencies as e}from"../tui/fileEditDependencies.mjs";import{PACKAGE_SOURCE as t}from"../constants/package.mjs";import{createFileEditRuntime as n}from"../controllers/fileEditRuntime.mjs";import{definePiExtension as r}from"@agimon-ai/doompi-core/pi-extension";const i=r(t,()=>n(e()));export{i as default,i as fileEditExtension};
|
|
2
|
+
//# sourceMappingURL=pi.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pi.mjs","names":[],"sources":["../../src/extensions/pi.ts"],"sourcesContent":["import { definePiExtension } from '@agimon-ai/doompi-core/pi-extension';\n\nimport { PACKAGE_SOURCE } from '../constants/package';\nimport { createFileEditRuntime } from '../controllers/fileEditRuntime';\nimport { createFileEditDependencies } from '../tui/fileEditDependencies';\nexport const fileEditExtension = definePiExtension(PACKAGE_SOURCE, () =>\n createFileEditRuntime(createFileEditDependencies()),\n);\nexport default fileEditExtension;\n"],"mappings":"0RAKA,MAAa,EAAoB,EAAkB,MACjD,EAAsB,EAA2B,CAAC,CACpD"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:`Module`}});const e=require("../constants/package.cjs"),t=require("../controllers/webFilesChannel.cjs"),n=require("../controllers/fileEditSession.cjs"),r=(0,require("@agimon-ai/doompi-core/server-facet").defineServerPlugin)({name:e.PACKAGE_SOURCE,global:{channels:[t.createFilesChannel]},workspace:{channels:[t.createFilesChannel]},session:n.createFileEditSession});exports.default=r,exports.fileEditsServerFacet=r;
|
|
2
|
+
//# sourceMappingURL=server.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.cjs","names":["defineServerPlugin","PACKAGE_SOURCE","createFilesChannel","createFileEditSession"],"sources":["../../src/extensions/server.ts"],"sourcesContent":["import { defineServerPlugin } from '@agimon-ai/doompi-core/server-facet';\n\nimport { PACKAGE_SOURCE } from '../constants/package';\nimport { createFileEditSession } from '../controllers/fileEditSession';\nimport { createFilesChannel } from '../controllers/webFilesChannel';\nexport const fileEditsServerFacet = defineServerPlugin({\n name: PACKAGE_SOURCE,\n global: { channels: [createFilesChannel] },\n workspace: { channels: [createFilesChannel] },\n session: createFileEditSession,\n});\nexport default fileEditsServerFacet;\n"],"mappings":"2OAKa,GAAA,+CAAuBA,CAAAA,CAAAA,mBAAAA,CAAmB,CACrD,KAAMC,EAAAA,eACN,OAAQ,CAAE,SAAU,CAACC,EAAAA,kBAAkB,CAAE,EACzC,UAAW,CAAE,SAAU,CAACA,EAAAA,kBAAkB,CAAE,EAC5C,QAASC,EAAAA,qBACX,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.cts","names":[],"sources":["../../src/extensions/server.ts"],"mappings":";qBAKa,oEAAoB"}
|