@agimon-ai/doompi-file-edit 0.0.1-alpha.122 → 0.0.1-alpha.124
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.
|
@@ -173,8 +173,8 @@ async function isRealPathInside(root, candidate) {
|
|
|
173
173
|
try {
|
|
174
174
|
realRoot = await node_fs_promises.default.realpath(root);
|
|
175
175
|
real = await node_fs_promises.default.realpath(candidate);
|
|
176
|
-
} catch {
|
|
177
|
-
return
|
|
176
|
+
} catch (error) {
|
|
177
|
+
return hasCode(error, "ENOENT");
|
|
178
178
|
}
|
|
179
179
|
return isInside(realRoot, real);
|
|
180
180
|
}
|
|
@@ -294,6 +294,11 @@ function createFileEditsApi(options = {}) {
|
|
|
294
294
|
const versions = await timeline.versions(node_path.default.resolve(cwd, filePath));
|
|
295
295
|
return versions.length === 0 ? void 0 : versions;
|
|
296
296
|
};
|
|
297
|
+
const isInsideSession = async (filePath) => {
|
|
298
|
+
if (cwd === "") return false;
|
|
299
|
+
const root = node_path.default.resolve(cwd);
|
|
300
|
+
return isInside(root, filePath) && await isRealPathInside(root, filePath);
|
|
301
|
+
};
|
|
297
302
|
app.get(apiRoutes_default.detail.path, async (context) => {
|
|
298
303
|
const requested = context.req.query(PATH_QUERY_PARAM);
|
|
299
304
|
const versions = await recordedVersions(requested);
|
|
@@ -326,7 +331,7 @@ function createFileEditsApi(options = {}) {
|
|
|
326
331
|
if (cwd === "") return context.json({ error: "This session has no working directory to read from." }, 403);
|
|
327
332
|
const root = node_path.default.resolve(cwd);
|
|
328
333
|
const filePath = node_path.default.resolve(root, requested);
|
|
329
|
-
if (!
|
|
334
|
+
if (!await isInsideSession(filePath)) return context.json({ error: "The path leaves the session directory." }, 403);
|
|
330
335
|
const body = {
|
|
331
336
|
path: filePath,
|
|
332
337
|
relPath: node_path.default.relative(root, filePath),
|
|
@@ -344,6 +349,7 @@ function createFileEditsApi(options = {}) {
|
|
|
344
349
|
if (typeof request.path !== "string" || typeof request.content !== "string") return context.json({ error: "A save names a path and the content to write." }, 400);
|
|
345
350
|
if (await recordedVersions(request.path) === void 0) return context.json({ error: "This session recorded no changes to that file." }, 404);
|
|
346
351
|
const filePath = node_path.default.resolve(cwd, request.path);
|
|
352
|
+
if (!await isInsideSession(filePath)) return context.json({ error: "Files outside the session directory are read-only." }, 403);
|
|
347
353
|
const current = await readWorking(filePath);
|
|
348
354
|
if (current.unavailable) return context.json({ error: current.reason ?? "The file cannot be read." }, 409);
|
|
349
355
|
if (current.hash !== request.expectedHash) return context.json({
|
|
@@ -358,6 +364,7 @@ function createFileEditsApi(options = {}) {
|
|
|
358
364
|
const requested = context.req.query(PATH_QUERY_PARAM);
|
|
359
365
|
if (await recordedVersions(requested) === void 0 || requested === void 0) return context.json({ error: "This session recorded no changes to that file." }, 404);
|
|
360
366
|
const filePath = node_path.default.resolve(cwd, requested);
|
|
367
|
+
if (!await isInsideSession(filePath)) return context.json({ error: "Files outside the session directory are read-only." }, 403);
|
|
361
368
|
try {
|
|
362
369
|
await node_fs_promises.default.unlink(filePath);
|
|
363
370
|
} catch (error) {
|
|
@@ -549,7 +556,7 @@ function createFileEditSession({ agent, host }) {
|
|
|
549
556
|
}
|
|
550
557
|
}],
|
|
551
558
|
hooks: [{
|
|
552
|
-
event: "
|
|
559
|
+
event: "tool_call",
|
|
553
560
|
handle: async (event, executionContext) => {
|
|
554
561
|
const runtime = active;
|
|
555
562
|
const toolCallId = stringValue(event, "toolCallId");
|
|
@@ -558,7 +565,7 @@ function createFileEditSession({ agent, host }) {
|
|
|
558
565
|
await editTracker.start(toolCallId, toolName, event.args, executionContext.cwd);
|
|
559
566
|
}
|
|
560
567
|
}, {
|
|
561
|
-
event: "
|
|
568
|
+
event: "tool_result",
|
|
562
569
|
handle: async (event, executionContext) => {
|
|
563
570
|
const runtime = active;
|
|
564
571
|
const toolCallId = stringValue(event, "toolCallId");
|
|
@@ -598,11 +605,11 @@ var fileEdits_server_default = (0, _agimon_ai_doompi_core_extensionFile.defineCh
|
|
|
598
605
|
*/
|
|
599
606
|
var route_server_default = (0, _agimon_ai_doompi_core_extensionFile.defineRoute)(api);
|
|
600
607
|
//#endregion
|
|
601
|
-
//#region src/extensions/workspaces/sessions/(backend)/hook/tool-
|
|
602
|
-
var
|
|
608
|
+
//#region src/extensions/workspaces/sessions/(backend)/hook/tool-call.server.ts
|
|
609
|
+
var tool_call_server_default = (0, _agimon_ai_doompi_core_extensionFile.defineHook)((context) => context.root.hooks[0]);
|
|
603
610
|
//#endregion
|
|
604
|
-
//#region src/extensions/workspaces/sessions/(backend)/hook/tool-
|
|
605
|
-
var
|
|
611
|
+
//#region src/extensions/workspaces/sessions/(backend)/hook/tool-result.server.ts
|
|
612
|
+
var tool_result_server_default = (0, _agimon_ai_doompi_core_extensionFile.defineHook)((context) => context.root.hooks[1]);
|
|
606
613
|
//#endregion
|
|
607
614
|
//#region generated/server.ts
|
|
608
615
|
const at = (value, context) => typeof value === "function" ? value(context) : value;
|
|
@@ -633,7 +640,7 @@ const facet = (0, _agimon_ai_doompi_core_serverFacet.defineServerPlugin)({
|
|
|
633
640
|
return [() => via({ frameType: "file_edits" }, fileEdits_server_default())];
|
|
634
641
|
},
|
|
635
642
|
get hooks() {
|
|
636
|
-
return [via({ event: "
|
|
643
|
+
return [via({ event: "tool_call" }, at(tool_call_server_default, contextSession)), via({ event: "tool_result" }, at(tool_result_server_default, contextSession))];
|
|
637
644
|
},
|
|
638
645
|
services: [...scopeSession.services ?? []],
|
|
639
646
|
get activities() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.cjs","names":["defineApiRoutes","apiResponse","createHash","fs","path","lineDiffFromEmpty","lineDiff","baselineOf","TimelineStore","NodeSnapshotStoreAdapter","FileEditPaths","Hono","routes","fs","confirmedChanges","parseTimeline","foldEntries","path","isDiffable","foldVersions","filterDoomIgnoredFiles","filesChannelType","FileEditPaths","createDoomIgnoreMatcher","fs","path","FileEditPaths","TimelineStore","NodeSnapshotStoreAdapter","EditTracker","NodeTreeManifestAdapter","NodeGitStatusAdapter","filesChannelType","filesStatusKey","defineRoot","defineChannel","defineRoute","defineHook","defineHook","defineServerPlugin","channelFileEdits","rootSession","composeRootHooks","apiRoute","hookToolExecutionEnd","hookToolExecutionStart"],"sources":["../../src/types/apiRoutes.ts","../../src/types/fileEditsApi.ts","../../src/services/fileEditsApi/index.ts","../../src/services/webFilesChannel/index.ts","../../src/services/fileEditSession/index.ts","../../src/extensions/workspaces/sessions/(backend)/_lib/serverRoot.ts","../../src/extensions/workspaces/sessions/(backend)/root.server.ts","../../src/extensions/(backend)/channel/fileEdits.server.ts","../../src/extensions/workspaces/sessions/(backend)/api/file-edits/route.server.ts","../../src/extensions/workspaces/sessions/(backend)/hook/tool-execution-end.server.ts","../../src/extensions/workspaces/sessions/(backend)/hook/tool-execution-start.server.ts","../../generated/server.ts"],"sourcesContent":["import { apiResponse, defineApiRoutes } from '@agimon-ai/doompi-core/web';\n\nimport type { FileEditsDetailView, FileEditsPreviewView, FileEditsSaveView } from './fileEditsApi';\n\n/**\n * This package's routes, as data.\n *\n * No scope and no base path. The build reads both off\n * `src/extensions/workspaces/sessions/(backend)/api/file-edits/`, so the mount\n * is stated once, by the folder that creates it. They used to be written here,\n * in the contract and again inside every URL the page built, and the copies\n * were free to disagree.\n *\n * This is the one file the routes, the Hono app and the browser all read, so a\n * path cannot move on one side alone.\n */\nexport default defineApiRoutes({\n detail: {\n method: 'GET',\n path: '/detail',\n query: ['path'],\n response: apiResponse<FileEditsDetailView>(),\n },\n preview: {\n method: 'GET',\n path: '/preview',\n query: ['path'],\n response: apiResponse<FileEditsPreviewView>(),\n },\n save: {\n method: 'PUT',\n path: '/content',\n response: apiResponse<FileEditsSaveView>(),\n },\n /** The same path as `save`; the method is the whole of the difference. */\n remove: {\n method: 'DELETE',\n path: '/content',\n query: ['path'],\n },\n /**\n * The host's own bytes route, not this package's.\n *\n * `host: true` hangs it off the session root rather than under `/plugins`,\n * which is how the cockpit already serves it. Declaring it here is the first\n * time anything records that this package's previews depend on it.\n */\n file: {\n method: 'GET',\n path: '/file',\n query: ['path'],\n host: true,\n },\n});\n","import type { FileEditOrigin, FileEditTool } from './domain';\n\n/**\n * The wire vocabulary this package's API shares with whatever calls it.\n *\n * It lives under src/types because that is the one server root a browser\n * bundle may read: the cockpit plugin builds its URLs from these and the\n * routes answer them, so neither half can drift from the other.\n *\n * Routes live in `./apiRoutes`, which the contract, the Hono app and the\n * generated client all read. This file holds the shapes that travel over them.\n *\n * Opening a changed file wants its whole history at once, so `detail` answers\n * in one round trip rather than making the page stitch three reads together;\n * `content` takes the manual save back; `preview` reads a file the session\n * never changed, which has no history to answer with.\n */\n\n/** Where a host mounts this package's API; the segment after /api/plugins/. */\nexport const API_BASE_PATH = 'file-edits';\n\n/** Query parameter naming the file a request is about. */\nexport const PATH_QUERY_PARAM = 'path';\n\n/**\n * One line of a diff. The number is the new file's for context and additions\n * and the old file's for removals, so it always says where the line lives.\n */\nexport interface FileEditsDiffRow {\n marker: '+' | '-' | ' ';\n line: number;\n content: string;\n}\n\n/**\n * A run of changed lines with its surrounding context. Hunks are sent apart\n * rather than joined by an elision marker, so the page draws the gap between\n * them instead of parsing one out of a string.\n */\nexport interface FileEditsDiffHunk {\n /** The line number the hunk opens at. */\n start: number;\n rows: FileEditsDiffRow[];\n}\n\n/**\n * One change in a file's history, each rendered on its own so a file edited\n * four times shows four distinct diffs rather than one merged blur.\n */\nexport interface FileEditsVersionView {\n /** Position in the file's history, oldest first, starting at 1. */\n index: number;\n tool: FileEditTool;\n at: number;\n origin: FileEditOrigin;\n additions: number;\n removals: number;\n /** The changed lines of this one change; absent when no baseline was captured. */\n hunks?: FileEditsDiffHunk[];\n /** Why there is no diff, shown in its place. */\n note?: string;\n}\n\n/** Everything this session did to the file: its first baseline against what is on disk now. */\nexport interface FileEditsCumulativeView {\n additions: number;\n removals: number;\n hunks?: FileEditsDiffHunk[];\n note?: string;\n}\n\n/** The file as it stands on disk right now, which is what the source view edits. */\nexport interface FileEditsWorkingView {\n content: string;\n /** sha256 of the content; the token a save hands back to prove it is not stale. */\n hash: string;\n /** True when the file is binary, missing, or past the size cap, so `content` is empty. */\n unavailable: boolean;\n reason?: string;\n}\n\n/** What the detail route answers with. */\nexport interface FileEditsDetailView {\n path: string;\n relPath: string;\n versions: FileEditsVersionView[];\n cumulative: FileEditsCumulativeView;\n working: FileEditsWorkingView;\n}\n\n/**\n * What the preview route answers with: the file as it stands, and nothing\n * about how it got there. A file this session never touched has no history and\n * no baseline, so there is no diff to send and no save to take back.\n */\nexport interface FileEditsPreviewView {\n path: string;\n relPath: string;\n working: FileEditsWorkingView;\n}\n\n/** What the save route takes. */\nexport interface FileEditsSaveRequest {\n path: string;\n /** The hash the reader loaded; a mismatch means the file moved underneath them. */\n expectedHash: string;\n content: string;\n}\n\n/** What the save route answers with. */\nexport interface FileEditsSaveView {\n hash: string;\n}\n\n/** What a route reports when it refuses; the page shows `error` verbatim. */\nexport interface FileEditsErrorView {\n error: string;\n /** On a stale save, the hash the file actually has now. */\n hash?: string;\n}\n","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/packageApi';\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 routes from '../../types/apiRoutes';\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(routes.detail.path, 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(routes.preview.path, 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(routes.save.path, 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(routes.remove.path, 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","import fs from 'node:fs';\nimport path from 'node:path';\n\nimport type { DoomHubChannel, DoomHubChannelSource } from '@agimon-ai/doompi-core/hubChannel';\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","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/serverFacet';\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, filesStatusKey } 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 /**\n * The channel carries the rows; the status decides whether the group exists.\n *\n * A cockpit drives this facet and never the Pi runtime, so without the status\n * the dock had the file list on the wire and no frame to draw it in. The empty\n * string is deliberate: the group declares `hideWhenEmpty`, so a session that\n * has edited nothing publishes a key with no content and stays hidden.\n */\n const publish = (runtime: NonNullable<typeof active>): void => {\n const items = readSessionFiles(runtime.timelinePath, runtime.context.cwd);\n directEvents.publish(filesChannelType, runtime.context.sessionId, { items });\n runtime.context.client.setStatus(\n filesStatusKey,\n items.length === 0 ? '' : `${items.length} ${items.length === 1 ? 'file' : 'files'}`,\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 executionContext.client.setStatus(filesStatusKey, 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","import type { DoomServerPluginContext } from '@agimon-ai/doompi-core/serverFacet';\n\nimport { createFileEditSession } from '../../../../../services/fileEditSession';\nexport const createFileEditServerRoot = (context: DoomServerPluginContext) => {\n const runtime = createFileEditSession(context);\n return { value: runtime, activities: runtime.activities };\n};\nexport type FileEditServerScope = Awaited<ReturnType<typeof createFileEditServerRoot>>['value'];\n","import { defineRoot } from '@agimon-ai/doompi-core/extensionFile';\n\nimport { createFileEditServerRoot } from './_lib/serverRoot';\n\nexport default defineRoot(createFileEditServerRoot);\n","import { defineChannel } from '@agimon-ai/doompi-core/extensionFile';\nimport type { DoomHubChannel } from '@agimon-ai/doompi-core/hubChannel';\n\nimport { createFilesChannel } from '../../../services/webFilesChannel';\nexport default defineChannel(function fileEditsChannel(): DoomHubChannel {\n return createFilesChannel();\n});\n","import { defineRoute } from '@agimon-ai/doompi-core/extensionFile';\n\nimport { api } from '../../../../../../services/fileEditsApi';\n\n/**\n * Mounts this package's HTTP API at `file-edits`, which the folder names.\n *\n * Until this file existed the API was never mounted at all. The session root\n * returns a runtime carrying `api`, but a root's `api` is not folded into the\n * generated entry the way its services and activities are, so the registration\n * was dropped in silence and the cockpit called routes that answered 404.\n */\nexport default defineRoute(api);\n","import { defineHook, type WithRoot } from '@agimon-ai/doompi-core/extensionFile';\nimport type { DoomServerPluginContext } from '@agimon-ai/doompi-core/serverFacet';\n\nimport type { FileEditServerScope } from '../_lib/serverRoot';\nexport default defineHook((context: WithRoot<DoomServerPluginContext, FileEditServerScope>) => context.root.hooks![1]!);\n","import { defineHook, type WithRoot } from '@agimon-ai/doompi-core/extensionFile';\nimport type { DoomServerPluginContext } from '@agimon-ai/doompi-core/serverFacet';\n\nimport type { FileEditServerScope } from '../_lib/serverRoot';\nexport default defineHook((context: WithRoot<DoomServerPluginContext, FileEditServerScope>) => context.root.hooks![0]!);\n","// Generated by @agimon-ai/doompi-build. Do not edit by hand.\nimport { defineServerPlugin, type DoomServerSessionPlugin } from '@agimon-ai/doompi-core/serverFacet';\nimport { composeRootHooks } from '@agimon-ai/doompi-core/extensionFile';\n\nimport rootSession from '../src/extensions/workspaces/sessions/(backend)/root.server';\nimport channelFileEdits from '../src/extensions/(backend)/channel/fileEdits.server';\nimport apiRoute from '../src/extensions/workspaces/sessions/(backend)/api/file-edits/route.server';\nimport hookToolExecutionEnd from '../src/extensions/workspaces/sessions/(backend)/hook/tool-execution-end.server';\nimport hookToolExecutionStart from '../src/extensions/workspaces/sessions/(backend)/hook/tool-execution-start.server';\n\ntype Factory<T, C> = (context: C) => T;\nconst at = <T, C>(value: T | Factory<T, C>, context: C): T =>\n typeof value === 'function' ? (value as Factory<T, C>)(context) : value;\nconst via = <T>(identity: Record<string, unknown>, value: unknown): T =>\n (value === undefined ? undefined : { ...identity, ...(value as object) }) as T;\n\nexport const facet = defineServerPlugin({\n name: '@agimon-ai/doompi-file-edit',\n global: () => ({\n get channels(): DoomServerSessionPlugin['channels'] { return [() => via({ frameType: 'file_edits' }, channelFileEdits())]; },\n }),\n workspace: () => ({\n get channels(): DoomServerSessionPlugin['channels'] { return [() => via({ frameType: 'file_edits' }, channelFileEdits())]; },\n }),\n session: async (context) => {\n const scopeSession = await rootSession(context);\n const contextSession = { ...context, root: scopeSession.value };\n return {\n ...composeRootHooks(scopeSession),\n get api(): DoomServerSessionPlugin['api'] { return [at(apiRoute, contextSession)]; },\n get channels(): DoomServerSessionPlugin['channels'] { return [() => via({ frameType: 'file_edits' }, channelFileEdits())]; },\n get hooks(): DoomServerSessionPlugin['hooks'] { return [via({ event: 'tool_execution_end' }, at(hookToolExecutionEnd, contextSession)), via({ event: 'tool_execution_start' }, at(hookToolExecutionStart, contextSession))]; },\n services: [...(scopeSession.services ?? [])],\n get activities(): DoomServerSessionPlugin['activities'] { return [...(scopeSession.activities ?? [])]; },\n };\n },\n});\n\nexport default facet;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"server.cjs","names":["defineApiRoutes","apiResponse","createHash","fs","path","lineDiffFromEmpty","lineDiff","baselineOf","TimelineStore","NodeSnapshotStoreAdapter","FileEditPaths","Hono","routes","fs","confirmedChanges","parseTimeline","foldEntries","path","isDiffable","foldVersions","filterDoomIgnoredFiles","filesChannelType","FileEditPaths","createDoomIgnoreMatcher","fs","path","FileEditPaths","TimelineStore","NodeSnapshotStoreAdapter","EditTracker","NodeTreeManifestAdapter","NodeGitStatusAdapter","filesChannelType","filesStatusKey","defineRoot","defineChannel","defineRoute","defineHook","defineHook","defineServerPlugin","channelFileEdits","rootSession","composeRootHooks","apiRoute","hookToolCall","hookToolResult"],"sources":["../../src/types/apiRoutes.ts","../../src/types/fileEditsApi.ts","../../src/services/fileEditsApi/index.ts","../../src/services/webFilesChannel/index.ts","../../src/services/fileEditSession/index.ts","../../src/extensions/workspaces/sessions/(backend)/_lib/serverRoot.ts","../../src/extensions/workspaces/sessions/(backend)/root.server.ts","../../src/extensions/(backend)/channel/fileEdits.server.ts","../../src/extensions/workspaces/sessions/(backend)/api/file-edits/route.server.ts","../../src/extensions/workspaces/sessions/(backend)/hook/tool-call.server.ts","../../src/extensions/workspaces/sessions/(backend)/hook/tool-result.server.ts","../../generated/server.ts"],"sourcesContent":["import { apiResponse, defineApiRoutes } from '@agimon-ai/doompi-core/web';\n\nimport type { FileEditsDetailView, FileEditsPreviewView, FileEditsSaveView } from './fileEditsApi';\n\n/**\n * This package's routes, as data.\n *\n * No scope and no base path. The build reads both off\n * `src/extensions/workspaces/sessions/(backend)/api/file-edits/`, so the mount\n * is stated once, by the folder that creates it. They used to be written here,\n * in the contract and again inside every URL the page built, and the copies\n * were free to disagree.\n *\n * This is the one file the routes, the Hono app and the browser all read, so a\n * path cannot move on one side alone.\n */\nexport default defineApiRoutes({\n detail: {\n method: 'GET',\n path: '/detail',\n query: ['path'],\n response: apiResponse<FileEditsDetailView>(),\n },\n preview: {\n method: 'GET',\n path: '/preview',\n query: ['path'],\n response: apiResponse<FileEditsPreviewView>(),\n },\n save: {\n method: 'PUT',\n path: '/content',\n response: apiResponse<FileEditsSaveView>(),\n },\n /** The same path as `save`; the method is the whole of the difference. */\n remove: {\n method: 'DELETE',\n path: '/content',\n query: ['path'],\n },\n /**\n * The host's own bytes route, not this package's.\n *\n * `host: true` hangs it off the session root rather than under `/plugins`,\n * which is how the cockpit already serves it. Declaring it here is the first\n * time anything records that this package's previews depend on it.\n */\n file: {\n method: 'GET',\n path: '/file',\n query: ['path'],\n host: true,\n },\n});\n","import type { FileEditOrigin, FileEditTool } from './domain';\n\n/**\n * The wire vocabulary this package's API shares with whatever calls it.\n *\n * It lives under src/types because that is the one server root a browser\n * bundle may read: the cockpit plugin builds its URLs from these and the\n * routes answer them, so neither half can drift from the other.\n *\n * Routes live in `./apiRoutes`, which the contract, the Hono app and the\n * generated client all read. This file holds the shapes that travel over them.\n *\n * Opening a changed file wants its whole history at once, so `detail` answers\n * in one round trip rather than making the page stitch three reads together;\n * `content` takes the manual save back; `preview` reads a file the session\n * never changed, which has no history to answer with.\n */\n\n/** Where a host mounts this package's API; the segment after /api/plugins/. */\nexport const API_BASE_PATH = 'file-edits';\n\n/** Query parameter naming the file a request is about. */\nexport const PATH_QUERY_PARAM = 'path';\n\n/**\n * One line of a diff. The number is the new file's for context and additions\n * and the old file's for removals, so it always says where the line lives.\n */\nexport interface FileEditsDiffRow {\n marker: '+' | '-' | ' ';\n line: number;\n content: string;\n}\n\n/**\n * A run of changed lines with its surrounding context. Hunks are sent apart\n * rather than joined by an elision marker, so the page draws the gap between\n * them instead of parsing one out of a string.\n */\nexport interface FileEditsDiffHunk {\n /** The line number the hunk opens at. */\n start: number;\n rows: FileEditsDiffRow[];\n}\n\n/**\n * One change in a file's history, each rendered on its own so a file edited\n * four times shows four distinct diffs rather than one merged blur.\n */\nexport interface FileEditsVersionView {\n /** Position in the file's history, oldest first, starting at 1. */\n index: number;\n tool: FileEditTool;\n at: number;\n origin: FileEditOrigin;\n additions: number;\n removals: number;\n /** The changed lines of this one change; absent when no baseline was captured. */\n hunks?: FileEditsDiffHunk[];\n /** Why there is no diff, shown in its place. */\n note?: string;\n}\n\n/** Everything this session did to the file: its first baseline against what is on disk now. */\nexport interface FileEditsCumulativeView {\n additions: number;\n removals: number;\n hunks?: FileEditsDiffHunk[];\n note?: string;\n}\n\n/** The file as it stands on disk right now, which is what the source view edits. */\nexport interface FileEditsWorkingView {\n content: string;\n /** sha256 of the content; the token a save hands back to prove it is not stale. */\n hash: string;\n /** True when the file is binary, missing, or past the size cap, so `content` is empty. */\n unavailable: boolean;\n reason?: string;\n}\n\n/** What the detail route answers with. */\nexport interface FileEditsDetailView {\n path: string;\n relPath: string;\n versions: FileEditsVersionView[];\n cumulative: FileEditsCumulativeView;\n working: FileEditsWorkingView;\n}\n\n/**\n * What the preview route answers with: the file as it stands, and nothing\n * about how it got there. A file this session never touched has no history and\n * no baseline, so there is no diff to send and no save to take back.\n */\nexport interface FileEditsPreviewView {\n path: string;\n relPath: string;\n working: FileEditsWorkingView;\n}\n\n/** What the save route takes. */\nexport interface FileEditsSaveRequest {\n path: string;\n /** The hash the reader loaded; a mismatch means the file moved underneath them. */\n expectedHash: string;\n content: string;\n}\n\n/** What the save route answers with. */\nexport interface FileEditsSaveView {\n hash: string;\n}\n\n/** What a route reports when it refuses; the page shows `error` verbatim. */\nexport interface FileEditsErrorView {\n error: string;\n /** On a stale save, the hash the file actually has now. */\n hash?: string;\n}\n","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/packageApi';\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 routes from '../../types/apiRoutes';\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 (error) {\n return hasCode(error, 'ENOENT');\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 const isInsideSession = async (filePath: string): Promise<boolean> => {\n if (cwd === '') return false;\n const root = path.resolve(cwd);\n return isInside(root, filePath) && (await isRealPathInside(root, filePath));\n };\n app.get(routes.detail.path, 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(routes.preview.path, 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 (!(await isInsideSession(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(routes.save.path, 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 if (!(await isInsideSession(filePath))) {\n return context.json({ error: 'Files outside the session directory are read-only.' }, 403);\n }\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(routes.remove.path, 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 if (!(await isInsideSession(filePath))) {\n return context.json({ error: 'Files outside the session directory are read-only.' }, 403);\n }\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","import fs from 'node:fs';\nimport path from 'node:path';\n\nimport type { DoomHubChannel, DoomHubChannelSource } from '@agimon-ai/doompi-core/hubChannel';\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","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/serverFacet';\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, filesStatusKey } 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 /**\n * The channel carries the rows; the status decides whether the group exists.\n *\n * A cockpit drives this facet and never the Pi runtime, so without the status\n * the dock had the file list on the wire and no frame to draw it in. The empty\n * string is deliberate: the group declares `hideWhenEmpty`, so a session that\n * has edited nothing publishes a key with no content and stays hidden.\n */\n const publish = (runtime: NonNullable<typeof active>): void => {\n const items = readSessionFiles(runtime.timelinePath, runtime.context.cwd);\n directEvents.publish(filesChannelType, runtime.context.sessionId, { items });\n runtime.context.client.setStatus(\n filesStatusKey,\n items.length === 0 ? '' : `${items.length} ${items.length === 1 ? 'file' : 'files'}`,\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 executionContext.client.setStatus(filesStatusKey, undefined);\n editTracker.reset();\n };\n },\n };\n const startHook: DoomHeadlessHook = {\n event: 'tool_call',\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_result',\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","import type { DoomServerPluginContext } from '@agimon-ai/doompi-core/serverFacet';\n\nimport { createFileEditSession } from '../../../../../services/fileEditSession';\nexport const createFileEditServerRoot = (context: DoomServerPluginContext) => {\n const runtime = createFileEditSession(context);\n return { value: runtime, activities: runtime.activities };\n};\nexport type FileEditServerScope = Awaited<ReturnType<typeof createFileEditServerRoot>>['value'];\n","import { defineRoot } from '@agimon-ai/doompi-core/extensionFile';\n\nimport { createFileEditServerRoot } from './_lib/serverRoot';\n\nexport default defineRoot(createFileEditServerRoot);\n","import { defineChannel } from '@agimon-ai/doompi-core/extensionFile';\nimport type { DoomHubChannel } from '@agimon-ai/doompi-core/hubChannel';\n\nimport { createFilesChannel } from '../../../services/webFilesChannel';\nexport default defineChannel(function fileEditsChannel(): DoomHubChannel {\n return createFilesChannel();\n});\n","import { defineRoute } from '@agimon-ai/doompi-core/extensionFile';\n\nimport { api } from '../../../../../../services/fileEditsApi';\n\n/**\n * Mounts this package's HTTP API at `file-edits`, which the folder names.\n *\n * Until this file existed the API was never mounted at all. The session root\n * returns a runtime carrying `api`, but a root's `api` is not folded into the\n * generated entry the way its services and activities are, so the registration\n * was dropped in silence and the cockpit called routes that answered 404.\n */\nexport default defineRoute(api);\n","import { defineHook, type WithRoot } from '@agimon-ai/doompi-core/extensionFile';\nimport type { DoomServerPluginContext } from '@agimon-ai/doompi-core/serverFacet';\n\nimport type { FileEditServerScope } from '../_lib/serverRoot';\nexport default defineHook((context: WithRoot<DoomServerPluginContext, FileEditServerScope>) => context.root.hooks![0]!);\n","import { defineHook, type WithRoot } from '@agimon-ai/doompi-core/extensionFile';\nimport type { DoomServerPluginContext } from '@agimon-ai/doompi-core/serverFacet';\n\nimport type { FileEditServerScope } from '../_lib/serverRoot';\nexport default defineHook((context: WithRoot<DoomServerPluginContext, FileEditServerScope>) => context.root.hooks![1]!);\n","// Generated by @agimon-ai/doompi-build. Do not edit by hand.\nimport { defineServerPlugin, type DoomServerSessionPlugin } from '@agimon-ai/doompi-core/serverFacet';\nimport { composeRootHooks } from '@agimon-ai/doompi-core/extensionFile';\n\nimport rootSession from '../src/extensions/workspaces/sessions/(backend)/root.server';\nimport channelFileEdits from '../src/extensions/(backend)/channel/fileEdits.server';\nimport apiRoute from '../src/extensions/workspaces/sessions/(backend)/api/file-edits/route.server';\nimport hookToolCall from '../src/extensions/workspaces/sessions/(backend)/hook/tool-call.server';\nimport hookToolResult from '../src/extensions/workspaces/sessions/(backend)/hook/tool-result.server';\n\ntype Factory<T, C> = (context: C) => T;\nconst at = <T, C>(value: T | Factory<T, C>, context: C): T =>\n typeof value === 'function' ? (value as Factory<T, C>)(context) : value;\nconst via = <T>(identity: Record<string, unknown>, value: unknown): T =>\n (value === undefined ? undefined : { ...identity, ...(value as object) }) as T;\n\nexport const facet = defineServerPlugin({\n name: '@agimon-ai/doompi-file-edit',\n global: () => ({\n get channels(): DoomServerSessionPlugin['channels'] { return [() => via({ frameType: 'file_edits' }, channelFileEdits())]; },\n }),\n workspace: () => ({\n get channels(): DoomServerSessionPlugin['channels'] { return [() => via({ frameType: 'file_edits' }, channelFileEdits())]; },\n }),\n session: async (context) => {\n const scopeSession = await rootSession(context);\n const contextSession = { ...context, root: scopeSession.value };\n return {\n ...composeRootHooks(scopeSession),\n get api(): DoomServerSessionPlugin['api'] { return [at(apiRoute, contextSession)]; },\n get channels(): DoomServerSessionPlugin['channels'] { return [() => via({ frameType: 'file_edits' }, channelFileEdits())]; },\n get hooks(): DoomServerSessionPlugin['hooks'] { return [via({ event: 'tool_call' }, at(hookToolCall, contextSession)), via({ event: 'tool_result' }, at(hookToolResult, contextSession))]; },\n services: [...(scopeSession.services ?? [])],\n get activities(): DoomServerSessionPlugin['activities'] { return [...(scopeSession.activities ?? [])]; },\n };\n },\n});\n\nexport default facet;\n"],"mappings":"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"}
|
|
@@ -166,8 +166,8 @@ async function isRealPathInside(root, candidate) {
|
|
|
166
166
|
try {
|
|
167
167
|
realRoot = await fs.realpath(root);
|
|
168
168
|
real = await fs.realpath(candidate);
|
|
169
|
-
} catch {
|
|
170
|
-
return
|
|
169
|
+
} catch (error) {
|
|
170
|
+
return hasCode(error, "ENOENT");
|
|
171
171
|
}
|
|
172
172
|
return isInside(realRoot, real);
|
|
173
173
|
}
|
|
@@ -287,6 +287,11 @@ function createFileEditsApi(options = {}) {
|
|
|
287
287
|
const versions = await timeline.versions(path.resolve(cwd, filePath));
|
|
288
288
|
return versions.length === 0 ? void 0 : versions;
|
|
289
289
|
};
|
|
290
|
+
const isInsideSession = async (filePath) => {
|
|
291
|
+
if (cwd === "") return false;
|
|
292
|
+
const root = path.resolve(cwd);
|
|
293
|
+
return isInside(root, filePath) && await isRealPathInside(root, filePath);
|
|
294
|
+
};
|
|
290
295
|
app.get(apiRoutes_default.detail.path, async (context) => {
|
|
291
296
|
const requested = context.req.query(PATH_QUERY_PARAM);
|
|
292
297
|
const versions = await recordedVersions(requested);
|
|
@@ -319,7 +324,7 @@ function createFileEditsApi(options = {}) {
|
|
|
319
324
|
if (cwd === "") return context.json({ error: "This session has no working directory to read from." }, 403);
|
|
320
325
|
const root = path.resolve(cwd);
|
|
321
326
|
const filePath = path.resolve(root, requested);
|
|
322
|
-
if (!
|
|
327
|
+
if (!await isInsideSession(filePath)) return context.json({ error: "The path leaves the session directory." }, 403);
|
|
323
328
|
const body = {
|
|
324
329
|
path: filePath,
|
|
325
330
|
relPath: path.relative(root, filePath),
|
|
@@ -337,6 +342,7 @@ function createFileEditsApi(options = {}) {
|
|
|
337
342
|
if (typeof request.path !== "string" || typeof request.content !== "string") return context.json({ error: "A save names a path and the content to write." }, 400);
|
|
338
343
|
if (await recordedVersions(request.path) === void 0) return context.json({ error: "This session recorded no changes to that file." }, 404);
|
|
339
344
|
const filePath = path.resolve(cwd, request.path);
|
|
345
|
+
if (!await isInsideSession(filePath)) return context.json({ error: "Files outside the session directory are read-only." }, 403);
|
|
340
346
|
const current = await readWorking(filePath);
|
|
341
347
|
if (current.unavailable) return context.json({ error: current.reason ?? "The file cannot be read." }, 409);
|
|
342
348
|
if (current.hash !== request.expectedHash) return context.json({
|
|
@@ -351,6 +357,7 @@ function createFileEditsApi(options = {}) {
|
|
|
351
357
|
const requested = context.req.query(PATH_QUERY_PARAM);
|
|
352
358
|
if (await recordedVersions(requested) === void 0 || requested === void 0) return context.json({ error: "This session recorded no changes to that file." }, 404);
|
|
353
359
|
const filePath = path.resolve(cwd, requested);
|
|
360
|
+
if (!await isInsideSession(filePath)) return context.json({ error: "Files outside the session directory are read-only." }, 403);
|
|
354
361
|
try {
|
|
355
362
|
await fs.unlink(filePath);
|
|
356
363
|
} catch (error) {
|
|
@@ -542,7 +549,7 @@ function createFileEditSession({ agent, host }) {
|
|
|
542
549
|
}
|
|
543
550
|
}],
|
|
544
551
|
hooks: [{
|
|
545
|
-
event: "
|
|
552
|
+
event: "tool_call",
|
|
546
553
|
handle: async (event, executionContext) => {
|
|
547
554
|
const runtime = active;
|
|
548
555
|
const toolCallId = stringValue(event, "toolCallId");
|
|
@@ -551,7 +558,7 @@ function createFileEditSession({ agent, host }) {
|
|
|
551
558
|
await editTracker.start(toolCallId, toolName, event.args, executionContext.cwd);
|
|
552
559
|
}
|
|
553
560
|
}, {
|
|
554
|
-
event: "
|
|
561
|
+
event: "tool_result",
|
|
555
562
|
handle: async (event, executionContext) => {
|
|
556
563
|
const runtime = active;
|
|
557
564
|
const toolCallId = stringValue(event, "toolCallId");
|
|
@@ -591,11 +598,11 @@ var fileEdits_server_default = defineChannel(function fileEditsChannel() {
|
|
|
591
598
|
*/
|
|
592
599
|
var route_server_default = defineRoute(api);
|
|
593
600
|
//#endregion
|
|
594
|
-
//#region src/extensions/workspaces/sessions/(backend)/hook/tool-
|
|
595
|
-
var
|
|
601
|
+
//#region src/extensions/workspaces/sessions/(backend)/hook/tool-call.server.ts
|
|
602
|
+
var tool_call_server_default = defineHook((context) => context.root.hooks[0]);
|
|
596
603
|
//#endregion
|
|
597
|
-
//#region src/extensions/workspaces/sessions/(backend)/hook/tool-
|
|
598
|
-
var
|
|
604
|
+
//#region src/extensions/workspaces/sessions/(backend)/hook/tool-result.server.ts
|
|
605
|
+
var tool_result_server_default = defineHook((context) => context.root.hooks[1]);
|
|
599
606
|
//#endregion
|
|
600
607
|
//#region generated/server.ts
|
|
601
608
|
const at = (value, context) => typeof value === "function" ? value(context) : value;
|
|
@@ -626,7 +633,7 @@ const facet = defineServerPlugin({
|
|
|
626
633
|
return [() => via({ frameType: "file_edits" }, fileEdits_server_default())];
|
|
627
634
|
},
|
|
628
635
|
get hooks() {
|
|
629
|
-
return [via({ event: "
|
|
636
|
+
return [via({ event: "tool_call" }, at(tool_call_server_default, contextSession)), via({ event: "tool_result" }, at(tool_result_server_default, contextSession))];
|
|
630
637
|
},
|
|
631
638
|
services: [...scopeSession.services ?? []],
|
|
632
639
|
get activities() {
|