@agimon-ai/po-edit 0.0.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +83 -0
- package/dist/apiContracts.cjs +17 -0
- package/dist/apiContracts.cjs.map +1 -0
- package/dist/apiContracts.d.mts +53 -0
- package/dist/apiContracts.d.mts.map +1 -0
- package/dist/apiContracts.mjs +13 -0
- package/dist/apiContracts.mjs.map +1 -0
- package/dist/editTool--RaZ-yPH.mjs +157 -0
- package/dist/editTool--RaZ-yPH.mjs.map +1 -0
- package/dist/editTool-BKaEjUdm.cjs +186 -0
- package/dist/editTool-BKaEjUdm.cjs.map +1 -0
- package/dist/extensions/mcp-ui.mjs +22 -0
- package/dist/extensions/mcp-ui.mjs.map +1 -0
- package/dist/extensions/mcp.cjs +170 -0
- package/dist/extensions/mcp.cjs.map +1 -0
- package/dist/extensions/mcp.d.mts +5 -0
- package/dist/extensions/mcp.d.mts.map +1 -0
- package/dist/extensions/mcp.mjs +165 -0
- package/dist/extensions/mcp.mjs.map +1 -0
- package/dist/extensions/server.cjs +26 -0
- package/dist/extensions/server.cjs.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 +22 -0
- package/dist/extensions/server.mjs.map +1 -0
- package/dist/extensions/web.mjs +179 -0
- package/dist/extensions/web.mjs.map +1 -0
- package/dist/index.cjs +7 -0
- package/dist/index.d.mts +34 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +2 -0
- package/package.json +132 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vuong Ngo
|
|
4
|
+
Copyright (c) 2026 pulseaiclub
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# @agimon-ai/po-edit
|
|
2
|
+
|
|
3
|
+
Snapshot-bound hashline edit tool for Po.
|
|
4
|
+
|
|
5
|
+
Part of the [Po distribution](https://www.npmjs.com/package/@agimon-ai/po).
|
|
6
|
+
|
|
7
|
+
The native `edit` tool consumes the exact file tags and stable
|
|
8
|
+
three-letter anchors produced by `@agimon-ai/po-read`, `@agimon-ai/po-grep`, or another
|
|
9
|
+
compatible hashline producer. Stale or ambiguous mutations fail before any file is changed.
|
|
10
|
+
|
|
11
|
+
> **Alpha:** tool and protocol contracts may change between releases.
|
|
12
|
+
|
|
13
|
+
## Requirements
|
|
14
|
+
|
|
15
|
+
- Node.js 24 or newer
|
|
16
|
+
|
|
17
|
+
## Tool protocol
|
|
18
|
+
|
|
19
|
+
Compatible read and grep results start with a file header followed by tagged lines:
|
|
20
|
+
|
|
21
|
+
```text
|
|
22
|
+
@file src/example.ts#A1b2C3d4
|
|
23
|
+
1#abc|const value = 1;
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The shared protocol is implemented by `@agimon-ai/po-hashline`. The eight-character file tag is
|
|
27
|
+
the base64url SHA-256 prefix of the exact file bytes. The three-letter line anchor is a
|
|
28
|
+
whitespace-insensitive FNV-1a64 tag.
|
|
29
|
+
|
|
30
|
+
`edit` accepts:
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{
|
|
34
|
+
"path": "src/example.ts",
|
|
35
|
+
"hash": "A1b2C3d4",
|
|
36
|
+
"edits": [
|
|
37
|
+
{
|
|
38
|
+
"from": "1#abc",
|
|
39
|
+
"to": "1#abc",
|
|
40
|
+
"content": "const value = 2;"
|
|
41
|
+
}
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
All anchors refer to the original snapshot. Ranges are inclusive. Omit `content` to delete a range.
|
|
47
|
+
Exact duplicate edits are applied once, but any other overlapping ranges are rejected before
|
|
48
|
+
mutation.
|
|
49
|
+
|
|
50
|
+
Pass exactly one anchor in each `from` and `to` value, for example `5#abc`. Anchors copied with a
|
|
51
|
+
`>`, `+`, or `-` display prefix are accepted, as are uppercase anchor letters, whitespace around
|
|
52
|
+
the separator, and the legacy `5:abc` separator. Pasted multiline blocks are rejected.
|
|
53
|
+
|
|
54
|
+
## Attribution
|
|
55
|
+
|
|
56
|
+
The hashline protocol and FNV anchor approach are adapted from
|
|
57
|
+
[Phi's writetool](https://github.com/pulseaiclub/phi/tree/main/internal/tools/writetool), licensed
|
|
58
|
+
under MIT by pulseaiclub. Po uses an exact-byte SHA-256 file tag as an intentional compatibility
|
|
59
|
+
boundary.
|
|
60
|
+
|
|
61
|
+
## Public API
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import { EditParamsSchema, executeHashlineEdit, createHashlineEditTool } from '@agimon-ai/po-edit';
|
|
65
|
+
import type { EditParams, HashlineRange } from '@agimon-ai/po-edit';
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Development
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
pnpm build
|
|
72
|
+
pnpm typecheck
|
|
73
|
+
pnpm test
|
|
74
|
+
pnpm lint
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Maintained by [Agimon](https://agimon.ai/about).
|
|
78
|
+
|
|
79
|
+
## License
|
|
80
|
+
|
|
81
|
+
MIT
|
|
82
|
+
|
|
83
|
+
The native tool declaration consumes `services/editTool`, and reusable factories and schemas are exposed through flat `src/exports`.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Object.defineProperties(exports, {
|
|
2
|
+
__esModule: { value: true },
|
|
3
|
+
[Symbol.toStringTag]: { value: "Module" }
|
|
4
|
+
});
|
|
5
|
+
//#region src/schemas/apiContracts.ts
|
|
6
|
+
/** This facet contributes agent tools, commands or lifecycle only, with no separate HTTP or socket surface. */
|
|
7
|
+
const apiContracts = (0, require("@agimon-ai/po-core/apiContracts").defineApiContract)({
|
|
8
|
+
version: 1,
|
|
9
|
+
http: [],
|
|
10
|
+
sockets: [],
|
|
11
|
+
dynamic: []
|
|
12
|
+
});
|
|
13
|
+
//#endregion
|
|
14
|
+
exports.apiContracts = apiContracts;
|
|
15
|
+
exports.default = apiContracts;
|
|
16
|
+
|
|
17
|
+
//# sourceMappingURL=apiContracts.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apiContracts.cjs","names":["defineApiContract"],"sources":["../src/schemas/apiContracts.ts"],"sourcesContent":["import { defineApiContract } from '@agimon-ai/po-core/apiContracts';\n\n/** This facet contributes agent tools, commands or lifecycle only, with no separate HTTP or socket surface. */\nexport const apiContracts = defineApiContract({ version: 1, http: [], sockets: [], dynamic: [] });\nexport default apiContracts;\n"],"mappings":";;;;;;AAGA,MAAa,gBAAA,4CAAeA,CAAAA,CAAAA,kBAAAA,CAAkB;CAAE,SAAS;CAAG,MAAM,CAAC;CAAG,SAAS,CAAC;CAAG,SAAS,CAAC;AAAE,CAAC"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
//#region src/schemas/apiContracts.d.ts
|
|
2
|
+
/** This facet contributes agent tools, commands or lifecycle only, with no separate HTTP or socket surface. */
|
|
3
|
+
export declare const apiContracts: {
|
|
4
|
+
version: 1;
|
|
5
|
+
protocols?: Record<string, string> | undefined;
|
|
6
|
+
schemas?: Record<string, boolean | object> | undefined;
|
|
7
|
+
http: {
|
|
8
|
+
id: string;
|
|
9
|
+
scope: "global" | "session" | "workspace";
|
|
10
|
+
basePath?: string | undefined;
|
|
11
|
+
path: string;
|
|
12
|
+
method: "DELETE" | "GET" | "HEAD" | "OPTIONS" | "PATCH" | "POST" | "PUT";
|
|
13
|
+
description: string;
|
|
14
|
+
availability?: string | undefined;
|
|
15
|
+
authentication: "device" | "none" | "owner";
|
|
16
|
+
parameters?: {
|
|
17
|
+
name: string;
|
|
18
|
+
in: "header" | "path" | "query";
|
|
19
|
+
required: boolean;
|
|
20
|
+
schema: boolean | object;
|
|
21
|
+
}[] | undefined;
|
|
22
|
+
body?: {
|
|
23
|
+
contentType: string;
|
|
24
|
+
contentTypes?: string[] | undefined;
|
|
25
|
+
required: boolean;
|
|
26
|
+
schema: boolean | object;
|
|
27
|
+
} | undefined;
|
|
28
|
+
responses: Record<string, {
|
|
29
|
+
description: string;
|
|
30
|
+
contentType?: string | undefined;
|
|
31
|
+
schema?: boolean | object | undefined;
|
|
32
|
+
events?: Record<string, boolean | object> | undefined;
|
|
33
|
+
}>;
|
|
34
|
+
}[];
|
|
35
|
+
sockets: {
|
|
36
|
+
id: string;
|
|
37
|
+
scope: "global" | "session" | "workspace";
|
|
38
|
+
description: string;
|
|
39
|
+
availability?: string | undefined;
|
|
40
|
+
service: string;
|
|
41
|
+
member: string;
|
|
42
|
+
direction: "client-to-server" | "server-to-client";
|
|
43
|
+
kind: "channel" | "method" | "state";
|
|
44
|
+
input: boolean | object;
|
|
45
|
+
output?: boolean | object | undefined;
|
|
46
|
+
errors?: boolean | object | undefined;
|
|
47
|
+
}[];
|
|
48
|
+
dynamic: string[];
|
|
49
|
+
gaps?: string[] | undefined;
|
|
50
|
+
};
|
|
51
|
+
//#endregion
|
|
52
|
+
export { apiContracts as default };
|
|
53
|
+
//# sourceMappingURL=apiContracts.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apiContracts.d.mts","names":[],"sources":["../src/schemas/apiContracts.ts"],"mappings":";;qBAGa"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { defineApiContract } from "@agimon-ai/po-core/apiContracts";
|
|
2
|
+
//#region src/schemas/apiContracts.ts
|
|
3
|
+
/** This facet contributes agent tools, commands or lifecycle only, with no separate HTTP or socket surface. */
|
|
4
|
+
const apiContracts = defineApiContract({
|
|
5
|
+
version: 1,
|
|
6
|
+
http: [],
|
|
7
|
+
sockets: [],
|
|
8
|
+
dynamic: []
|
|
9
|
+
});
|
|
10
|
+
//#endregion
|
|
11
|
+
export { apiContracts, apiContracts as default };
|
|
12
|
+
|
|
13
|
+
//# sourceMappingURL=apiContracts.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apiContracts.mjs","names":[],"sources":["../src/schemas/apiContracts.ts"],"sourcesContent":["import { defineApiContract } from '@agimon-ai/po-core/apiContracts';\n\n/** This facet contributes agent tools, commands or lifecycle only, with no separate HTTP or socket surface. */\nexport const apiContracts = defineApiContract({ version: 1, http: [], sockets: [], dynamic: [] });\nexport default apiContracts;\n"],"mappings":";;;AAGA,MAAa,eAAe,kBAAkB;CAAE,SAAS;CAAG,MAAM,CAAC;CAAG,SAAS,CAAC;CAAG,SAAS,CAAC;AAAE,CAAC"}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { access, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { applyHashlineEdits, normalizeFileTag, normalizeToLf, stripBom } from "@agimon-ai/po-hashline";
|
|
4
|
+
import { computeFileTag, decodeUtf8, displayPath, resolveInputPath } from "@agimon-ai/po-hashline/files";
|
|
5
|
+
import { generateDiffString, generateUnifiedPatch, withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { Type } from "typebox";
|
|
7
|
+
//#region src/schemas/editTool.ts
|
|
8
|
+
const HashlineRangeSchema = Type.Object({
|
|
9
|
+
from: Type.String({ description: "One inclusive starting anchor, for example 5#abc. Do not paste multiple lines." }),
|
|
10
|
+
to: Type.String({ description: "One inclusive ending anchor, for example 8#def. Do not paste multiple lines." }),
|
|
11
|
+
content: Type.Optional(Type.Union([Type.String({ description: "Replacement content. Empty content deletes the selected lines." }), Type.Null({ description: "Delete the selected lines." })]))
|
|
12
|
+
});
|
|
13
|
+
const EditParamsSchema = Type.Object({
|
|
14
|
+
path: Type.String({ description: "Path to the file returned by a compatible hashline read or grep tool." }),
|
|
15
|
+
hash: Type.String({ description: "Eight-character exact-byte file tag returned by read or grep." }),
|
|
16
|
+
edits: Type.Array(HashlineRangeSchema, {
|
|
17
|
+
minItems: 1,
|
|
18
|
+
description: "Non-overlapping inclusive ranges from the same original snapshot."
|
|
19
|
+
})
|
|
20
|
+
});
|
|
21
|
+
//#endregion
|
|
22
|
+
//#region src/services/editTool/index.ts
|
|
23
|
+
function createHashlineEditTool() {
|
|
24
|
+
return {
|
|
25
|
+
name: "edit",
|
|
26
|
+
label: "edit",
|
|
27
|
+
description: "Edit one file using its exact snapshot hash and inclusive anchors such as 5#abc from read or grep. Each from and to value must contain one anchor, not a pasted block. All ranges refer to the original snapshot. Empty or omitted content deletes a range.",
|
|
28
|
+
promptSnippet: "Edit files with snapshot-bound hashline ranges",
|
|
29
|
+
promptGuidelines: [
|
|
30
|
+
"Copy path, hash, and anchors from the latest compatible read or grep result. Re-read after every successful edit.",
|
|
31
|
+
"Pass one anchor such as 5#abc in each from and to value. Do not paste tagged lines or multiline blocks.",
|
|
32
|
+
"Put multiple disjoint changes to one file in one edit call. All anchors must describe the original snapshot.",
|
|
33
|
+
"Omit content or pass an empty string to delete an inclusive range. Merge overlapping ranges before calling edit."
|
|
34
|
+
],
|
|
35
|
+
parameters: EditParamsSchema,
|
|
36
|
+
executionMode: "parallel",
|
|
37
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
38
|
+
return executeHashlineEdit(params, ctx.cwd, signal);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function createHeadlessEditTool() {
|
|
43
|
+
return {
|
|
44
|
+
name: "edit",
|
|
45
|
+
label: "edit",
|
|
46
|
+
description: "Edit one file using its exact snapshot hash and inclusive anchors such as 5#abc from read or grep. Each from and to value must contain one anchor, not a pasted block. All ranges refer to the original snapshot. Empty or omitted content deletes a range.",
|
|
47
|
+
promptSnippet: "Edit files with snapshot-bound hashline ranges",
|
|
48
|
+
promptGuidelines: ["Copy path, hash, and anchors from the latest compatible read or grep result. Re-read after every successful edit.", "Pass one anchor such as 5#abc in each from and to value. Do not paste tagged lines or multiline blocks."],
|
|
49
|
+
parameters: EditParamsSchema,
|
|
50
|
+
executionMode: "parallel",
|
|
51
|
+
execute: (_toolCallId, params, signal, _onUpdate, context) => executeHashlineEdit(params, context.cwd, signal)
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function assertNotAborted(signal) {
|
|
55
|
+
if (signal?.aborted) throw new Error("Operation aborted");
|
|
56
|
+
}
|
|
57
|
+
async function executeHashlineEdit(params, cwd, signal) {
|
|
58
|
+
const absolutePath = resolveInputPath(params.path, cwd);
|
|
59
|
+
const expectedHash = normalizeFileTag(params.hash);
|
|
60
|
+
return withFileMutationQueue(absolutePath, async () => {
|
|
61
|
+
assertNotAborted(signal);
|
|
62
|
+
await access(absolutePath, constants.R_OK | constants.W_OK);
|
|
63
|
+
const beforeBytes = await readFile(absolutePath);
|
|
64
|
+
assertNotAborted(signal);
|
|
65
|
+
const actualHash = computeFileTag(beforeBytes);
|
|
66
|
+
if (actualHash !== expectedHash) throw new Error(`Stale file hash ${expectedHash}. Current hash is ${actualHash}. Re-read the file and retry.`);
|
|
67
|
+
const hasBom = beforeBytes.subarray(0, 3).equals(Buffer.from([
|
|
68
|
+
239,
|
|
69
|
+
187,
|
|
70
|
+
191
|
|
71
|
+
]));
|
|
72
|
+
const decoded = decodeUtf8(beforeBytes, params.path);
|
|
73
|
+
const withoutBom = stripBom(decoded);
|
|
74
|
+
const before = normalizeToLf(withoutBom);
|
|
75
|
+
const applied = applyHashlineEdits(before, params.edits);
|
|
76
|
+
const editedText = restoreOriginalLineEndings(withoutBom, applied.edits);
|
|
77
|
+
if (normalizeToLf(editedText) !== applied.content) throw new Error("Could not preserve the file line endings safely. The file was not changed.");
|
|
78
|
+
const diff = generateDiffString(before, applied.content);
|
|
79
|
+
const patch = generateUnifiedPatch(displayPath(absolutePath, cwd), before, applied.content);
|
|
80
|
+
const details = {
|
|
81
|
+
diff: diff.diff,
|
|
82
|
+
patch,
|
|
83
|
+
firstChangedLine: diff.firstChangedLine
|
|
84
|
+
};
|
|
85
|
+
assertNotAborted(signal);
|
|
86
|
+
if (!(await readFile(absolutePath)).equals(beforeBytes)) throw new Error("The file changed while the edit was being prepared. Re-read it and retry.");
|
|
87
|
+
assertNotAborted(signal);
|
|
88
|
+
if (applied.content !== before) {
|
|
89
|
+
const output = Buffer.from(`${hasBom ? "" : ""}${editedText}`, "utf8");
|
|
90
|
+
await writeFile(absolutePath, output);
|
|
91
|
+
}
|
|
92
|
+
const count = applied.edits.length;
|
|
93
|
+
const noun = count === 1 ? "range" : "ranges";
|
|
94
|
+
return {
|
|
95
|
+
content: [{
|
|
96
|
+
type: "text",
|
|
97
|
+
text: `${applied.content === before ? `No changes needed in ${params.path}.` : `Edited ${params.path} (${count} ${noun}).`} Re-read before editing it again.`
|
|
98
|
+
}],
|
|
99
|
+
details
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
function restoreOriginalLineEndings(original, edits) {
|
|
104
|
+
const tokens = tokenizeLines(original);
|
|
105
|
+
const defaultEnding = tokens.find((token) => token.ending !== "")?.ending ?? "\n";
|
|
106
|
+
for (const edit of [...edits].reverse()) {
|
|
107
|
+
const start = edit.from.line - 1;
|
|
108
|
+
const count = edit.to.line - edit.from.line + 1;
|
|
109
|
+
const removed = tokens.slice(start, start + count);
|
|
110
|
+
const inheritedEnding = removed.at(-1)?.ending ?? "";
|
|
111
|
+
const internalEnding = removed.find((token) => token.ending !== "")?.ending ?? defaultEnding;
|
|
112
|
+
const lines = replacementLines(edit.content);
|
|
113
|
+
if (lines.length === 0 && removed.at(-1)?.ending === "" && start > 0) {
|
|
114
|
+
const previous = tokens[start - 1];
|
|
115
|
+
if (previous) tokens[start - 1] = {
|
|
116
|
+
...previous,
|
|
117
|
+
ending: ""
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
const replacements = lines.map((text, index) => ({
|
|
121
|
+
text,
|
|
122
|
+
ending: index === lines.length - 1 ? inheritedEnding : internalEnding
|
|
123
|
+
}));
|
|
124
|
+
tokens.splice(start, count, ...replacements);
|
|
125
|
+
}
|
|
126
|
+
return tokens.map((token) => `${token.text}${token.ending}`).join("");
|
|
127
|
+
}
|
|
128
|
+
function tokenizeLines(content) {
|
|
129
|
+
const tokens = [];
|
|
130
|
+
let start = 0;
|
|
131
|
+
for (let index = 0; index < content.length; index += 1) {
|
|
132
|
+
const character = content[index];
|
|
133
|
+
if (character !== "\r" && character !== "\n") continue;
|
|
134
|
+
const ending = character === "\r" && content[index + 1] === "\n" ? "\r\n" : character;
|
|
135
|
+
tokens.push({
|
|
136
|
+
text: content.slice(start, index),
|
|
137
|
+
ending
|
|
138
|
+
});
|
|
139
|
+
if (ending === "\r\n") index += 1;
|
|
140
|
+
start = index + 1;
|
|
141
|
+
}
|
|
142
|
+
tokens.push({
|
|
143
|
+
text: content.slice(start),
|
|
144
|
+
ending: ""
|
|
145
|
+
});
|
|
146
|
+
return tokens;
|
|
147
|
+
}
|
|
148
|
+
function replacementLines(content) {
|
|
149
|
+
if (content === null || content === "") return [];
|
|
150
|
+
const normalized = normalizeToLf(content);
|
|
151
|
+
const trimmed = normalized.endsWith("\n") ? normalized.slice(0, -1) : normalized;
|
|
152
|
+
return trimmed === "" ? [] : trimmed.split("\n");
|
|
153
|
+
}
|
|
154
|
+
//#endregion
|
|
155
|
+
export { HashlineRangeSchema as a, EditParamsSchema as i, createHeadlessEditTool as n, executeHashlineEdit as r, createHashlineEditTool as t };
|
|
156
|
+
|
|
157
|
+
//# sourceMappingURL=editTool--RaZ-yPH.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"editTool--RaZ-yPH.mjs","names":[],"sources":["../src/schemas/editTool.ts","../src/services/editTool/index.ts"],"sourcesContent":["import { type Static, Type } from 'typebox';\n\nexport const HashlineRangeSchema = Type.Object({\n from: Type.String({ description: 'One inclusive starting anchor, for example 5#abc. Do not paste multiple lines.' }),\n to: Type.String({ description: 'One inclusive ending anchor, for example 8#def. Do not paste multiple lines.' }),\n content: Type.Optional(\n Type.Union([\n Type.String({ description: 'Replacement content. Empty content deletes the selected lines.' }),\n Type.Null({ description: 'Delete the selected lines.' }),\n ]),\n ),\n});\n\nexport const EditParamsSchema = Type.Object({\n path: Type.String({ description: 'Path to the file returned by a compatible hashline read or grep tool.' }),\n hash: Type.String({ description: 'Eight-character exact-byte file tag returned by read or grep.' }),\n edits: Type.Array(HashlineRangeSchema, {\n minItems: 1,\n description: 'Non-overlapping inclusive ranges from the same original snapshot.',\n }),\n});\n\nexport type HashlineRange = Static<typeof HashlineRangeSchema>;\nexport type EditParams = Static<typeof EditParamsSchema>;\n","import { constants } from 'node:fs';\nimport { access, readFile, writeFile } from 'node:fs/promises';\n\nimport type {\n DoomHeadlessExecutionContext,\n DoomHeadlessTool,\n DoomHeadlessToolResult,\n} from '@agimon-ai/po-core/headless';\nimport {\n applyHashlineEdits,\n normalizeFileTag,\n normalizeToLf,\n stripBom,\n type PreparedHashlineEdit,\n} from '@agimon-ai/po-hashline';\nimport { computeFileTag, decodeUtf8, displayPath, resolveInputPath } from '@agimon-ai/po-hashline/files';\nimport {\n generateDiffString,\n generateUnifiedPatch,\n withFileMutationQueue,\n type EditToolDetails,\n} from '@earendil-works/pi-coding-agent';\nimport type { ToolDefinition } from '@earendil-works/pi-coding-agent';\n\nimport { EditParamsSchema, type EditParams } from '../../schemas/editTool';\n\nexport function createHashlineEditTool(): ToolDefinition<typeof EditParamsSchema> {\n return {\n name: 'edit',\n label: 'edit',\n description:\n 'Edit one file using its exact snapshot hash and inclusive anchors such as 5#abc from read or grep. Each from and to value must contain one anchor, not a pasted block. All ranges refer to the original snapshot. Empty or omitted content deletes a range.',\n promptSnippet: 'Edit files with snapshot-bound hashline ranges',\n promptGuidelines: [\n 'Copy path, hash, and anchors from the latest compatible read or grep result. Re-read after every successful edit.',\n 'Pass one anchor such as 5#abc in each from and to value. Do not paste tagged lines or multiline blocks.',\n 'Put multiple disjoint changes to one file in one edit call. All anchors must describe the original snapshot.',\n 'Omit content or pass an empty string to delete an inclusive range. Merge overlapping ranges before calling edit.',\n ],\n parameters: EditParamsSchema,\n executionMode: 'parallel',\n async execute(_toolCallId, params, signal, _onUpdate, ctx) {\n return executeHashlineEdit(params as EditParams, ctx.cwd, signal);\n },\n };\n}\n\nexport function createHeadlessEditTool(): DoomHeadlessTool<typeof EditParamsSchema> {\n return {\n name: 'edit',\n label: 'edit',\n description:\n 'Edit one file using its exact snapshot hash and inclusive anchors such as 5#abc from read or grep. Each from and to value must contain one anchor, not a pasted block. All ranges refer to the original snapshot. Empty or omitted content deletes a range.',\n promptSnippet: 'Edit files with snapshot-bound hashline ranges',\n promptGuidelines: [\n 'Copy path, hash, and anchors from the latest compatible read or grep result. Re-read after every successful edit.',\n 'Pass one anchor such as 5#abc in each from and to value. Do not paste tagged lines or multiline blocks.',\n ],\n parameters: EditParamsSchema,\n executionMode: 'parallel',\n execute: (\n _toolCallId: string,\n params: EditParams,\n signal: AbortSignal | undefined,\n _onUpdate: ((result: DoomHeadlessToolResult) => void) | undefined,\n context: DoomHeadlessExecutionContext,\n ) => executeHashlineEdit(params, context.cwd, signal),\n };\n}\n\nexport function assertNotAborted(signal: AbortSignal | undefined): void {\n if (signal?.aborted) throw new Error('Operation aborted');\n}\n\nexport async function executeHashlineEdit(\n params: EditParams,\n cwd: string,\n signal: AbortSignal | undefined,\n): Promise<{ content: [{ type: 'text'; text: string }]; details: EditToolDetails }> {\n const absolutePath = resolveInputPath(params.path, cwd);\n const expectedHash = normalizeFileTag(params.hash);\n return withFileMutationQueue(absolutePath, async () => {\n assertNotAborted(signal);\n await access(absolutePath, constants.R_OK | constants.W_OK);\n const beforeBytes = await readFile(absolutePath);\n assertNotAborted(signal);\n\n const actualHash = computeFileTag(beforeBytes);\n if (actualHash !== expectedHash) {\n throw new Error(`Stale file hash ${expectedHash}. Current hash is ${actualHash}. Re-read the file and retry.`);\n }\n\n const hasBom = beforeBytes.subarray(0, 3).equals(Buffer.from([0xef, 0xbb, 0xbf]));\n const decoded = decodeUtf8(beforeBytes, params.path);\n const withoutBom = stripBom(decoded);\n const before = normalizeToLf(withoutBom);\n const applied = applyHashlineEdits(before, params.edits);\n const editedText = restoreOriginalLineEndings(withoutBom, applied.edits);\n if (normalizeToLf(editedText) !== applied.content) {\n throw new Error('Could not preserve the file line endings safely. The file was not changed.');\n }\n const diff = generateDiffString(before, applied.content);\n const patch = generateUnifiedPatch(displayPath(absolutePath, cwd), before, applied.content);\n const details: EditToolDetails = { diff: diff.diff, patch, firstChangedLine: diff.firstChangedLine };\n\n assertNotAborted(signal);\n const currentBytes = await readFile(absolutePath);\n if (!currentBytes.equals(beforeBytes)) {\n throw new Error('The file changed while the edit was being prepared. Re-read it and retry.');\n }\n assertNotAborted(signal);\n\n if (applied.content !== before) {\n const output = Buffer.from(`${hasBom ? '\\ufeff' : ''}${editedText}`, 'utf8');\n await writeFile(absolutePath, output);\n }\n\n const count = applied.edits.length;\n const noun = count === 1 ? 'range' : 'ranges';\n const message =\n applied.content === before ? `No changes needed in ${params.path}.` : `Edited ${params.path} (${count} ${noun}).`;\n return { content: [{ type: 'text', text: `${message} Re-read before editing it again.` }], details };\n });\n}\n\ninterface LineToken {\n readonly text: string;\n readonly ending: '\\r\\n' | '\\n' | '\\r' | '';\n}\n\nfunction restoreOriginalLineEndings(original: string, edits: readonly PreparedHashlineEdit[]): string {\n const tokens = tokenizeLines(original);\n const defaultEnding = tokens.find((token) => token.ending !== '')?.ending ?? '\\n';\n for (const edit of [...edits].reverse()) {\n const start = edit.from.line - 1;\n const count = edit.to.line - edit.from.line + 1;\n const removed = tokens.slice(start, start + count);\n const inheritedEnding = removed.at(-1)?.ending ?? '';\n const internalEnding = removed.find((token) => token.ending !== '')?.ending ?? defaultEnding;\n const lines = replacementLines(edit.content);\n if (lines.length === 0 && removed.at(-1)?.ending === '' && start > 0) {\n const previous = tokens[start - 1];\n if (previous) tokens[start - 1] = { ...previous, ending: '' };\n }\n const replacements = lines.map<LineToken>((text, index) => ({\n text,\n ending: index === lines.length - 1 ? inheritedEnding : internalEnding,\n }));\n tokens.splice(start, count, ...replacements);\n }\n return tokens.map((token) => `${token.text}${token.ending}`).join('');\n}\n\nfunction tokenizeLines(content: string): LineToken[] {\n const tokens: LineToken[] = [];\n let start = 0;\n for (let index = 0; index < content.length; index += 1) {\n const character = content[index];\n if (character !== '\\r' && character !== '\\n') continue;\n const ending = character === '\\r' && content[index + 1] === '\\n' ? '\\r\\n' : character;\n tokens.push({ text: content.slice(start, index), ending });\n if (ending === '\\r\\n') index += 1;\n start = index + 1;\n }\n tokens.push({ text: content.slice(start), ending: '' });\n return tokens;\n}\n\nfunction replacementLines(content: string | null): string[] {\n if (content === null || content === '') return [];\n const normalized = normalizeToLf(content);\n const trimmed = normalized.endsWith('\\n') ? normalized.slice(0, -1) : normalized;\n return trimmed === '' ? [] : trimmed.split('\\n');\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
let node_fs = require("node:fs");
|
|
2
|
+
let node_fs_promises = require("node:fs/promises");
|
|
3
|
+
let _agimon_ai_po_hashline = require("@agimon-ai/po-hashline");
|
|
4
|
+
let _agimon_ai_po_hashline_files = require("@agimon-ai/po-hashline/files");
|
|
5
|
+
let _earendil_works_pi_coding_agent = require("@earendil-works/pi-coding-agent");
|
|
6
|
+
let typebox = require("typebox");
|
|
7
|
+
//#region src/schemas/editTool.ts
|
|
8
|
+
const HashlineRangeSchema = typebox.Type.Object({
|
|
9
|
+
from: typebox.Type.String({ description: "One inclusive starting anchor, for example 5#abc. Do not paste multiple lines." }),
|
|
10
|
+
to: typebox.Type.String({ description: "One inclusive ending anchor, for example 8#def. Do not paste multiple lines." }),
|
|
11
|
+
content: typebox.Type.Optional(typebox.Type.Union([typebox.Type.String({ description: "Replacement content. Empty content deletes the selected lines." }), typebox.Type.Null({ description: "Delete the selected lines." })]))
|
|
12
|
+
});
|
|
13
|
+
const EditParamsSchema = typebox.Type.Object({
|
|
14
|
+
path: typebox.Type.String({ description: "Path to the file returned by a compatible hashline read or grep tool." }),
|
|
15
|
+
hash: typebox.Type.String({ description: "Eight-character exact-byte file tag returned by read or grep." }),
|
|
16
|
+
edits: typebox.Type.Array(HashlineRangeSchema, {
|
|
17
|
+
minItems: 1,
|
|
18
|
+
description: "Non-overlapping inclusive ranges from the same original snapshot."
|
|
19
|
+
})
|
|
20
|
+
});
|
|
21
|
+
//#endregion
|
|
22
|
+
//#region src/services/editTool/index.ts
|
|
23
|
+
function createHashlineEditTool() {
|
|
24
|
+
return {
|
|
25
|
+
name: "edit",
|
|
26
|
+
label: "edit",
|
|
27
|
+
description: "Edit one file using its exact snapshot hash and inclusive anchors such as 5#abc from read or grep. Each from and to value must contain one anchor, not a pasted block. All ranges refer to the original snapshot. Empty or omitted content deletes a range.",
|
|
28
|
+
promptSnippet: "Edit files with snapshot-bound hashline ranges",
|
|
29
|
+
promptGuidelines: [
|
|
30
|
+
"Copy path, hash, and anchors from the latest compatible read or grep result. Re-read after every successful edit.",
|
|
31
|
+
"Pass one anchor such as 5#abc in each from and to value. Do not paste tagged lines or multiline blocks.",
|
|
32
|
+
"Put multiple disjoint changes to one file in one edit call. All anchors must describe the original snapshot.",
|
|
33
|
+
"Omit content or pass an empty string to delete an inclusive range. Merge overlapping ranges before calling edit."
|
|
34
|
+
],
|
|
35
|
+
parameters: EditParamsSchema,
|
|
36
|
+
executionMode: "parallel",
|
|
37
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
38
|
+
return executeHashlineEdit(params, ctx.cwd, signal);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function createHeadlessEditTool() {
|
|
43
|
+
return {
|
|
44
|
+
name: "edit",
|
|
45
|
+
label: "edit",
|
|
46
|
+
description: "Edit one file using its exact snapshot hash and inclusive anchors such as 5#abc from read or grep. Each from and to value must contain one anchor, not a pasted block. All ranges refer to the original snapshot. Empty or omitted content deletes a range.",
|
|
47
|
+
promptSnippet: "Edit files with snapshot-bound hashline ranges",
|
|
48
|
+
promptGuidelines: ["Copy path, hash, and anchors from the latest compatible read or grep result. Re-read after every successful edit.", "Pass one anchor such as 5#abc in each from and to value. Do not paste tagged lines or multiline blocks."],
|
|
49
|
+
parameters: EditParamsSchema,
|
|
50
|
+
executionMode: "parallel",
|
|
51
|
+
execute: (_toolCallId, params, signal, _onUpdate, context) => executeHashlineEdit(params, context.cwd, signal)
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function assertNotAborted(signal) {
|
|
55
|
+
if (signal?.aborted) throw new Error("Operation aborted");
|
|
56
|
+
}
|
|
57
|
+
async function executeHashlineEdit(params, cwd, signal) {
|
|
58
|
+
const absolutePath = (0, _agimon_ai_po_hashline_files.resolveInputPath)(params.path, cwd);
|
|
59
|
+
const expectedHash = (0, _agimon_ai_po_hashline.normalizeFileTag)(params.hash);
|
|
60
|
+
return (0, _earendil_works_pi_coding_agent.withFileMutationQueue)(absolutePath, async () => {
|
|
61
|
+
assertNotAborted(signal);
|
|
62
|
+
await (0, node_fs_promises.access)(absolutePath, node_fs.constants.R_OK | node_fs.constants.W_OK);
|
|
63
|
+
const beforeBytes = await (0, node_fs_promises.readFile)(absolutePath);
|
|
64
|
+
assertNotAborted(signal);
|
|
65
|
+
const actualHash = (0, _agimon_ai_po_hashline_files.computeFileTag)(beforeBytes);
|
|
66
|
+
if (actualHash !== expectedHash) throw new Error(`Stale file hash ${expectedHash}. Current hash is ${actualHash}. Re-read the file and retry.`);
|
|
67
|
+
const hasBom = beforeBytes.subarray(0, 3).equals(Buffer.from([
|
|
68
|
+
239,
|
|
69
|
+
187,
|
|
70
|
+
191
|
|
71
|
+
]));
|
|
72
|
+
const decoded = (0, _agimon_ai_po_hashline_files.decodeUtf8)(beforeBytes, params.path);
|
|
73
|
+
const withoutBom = (0, _agimon_ai_po_hashline.stripBom)(decoded);
|
|
74
|
+
const before = (0, _agimon_ai_po_hashline.normalizeToLf)(withoutBom);
|
|
75
|
+
const applied = (0, _agimon_ai_po_hashline.applyHashlineEdits)(before, params.edits);
|
|
76
|
+
const editedText = restoreOriginalLineEndings(withoutBom, applied.edits);
|
|
77
|
+
if ((0, _agimon_ai_po_hashline.normalizeToLf)(editedText) !== applied.content) throw new Error("Could not preserve the file line endings safely. The file was not changed.");
|
|
78
|
+
const diff = (0, _earendil_works_pi_coding_agent.generateDiffString)(before, applied.content);
|
|
79
|
+
const patch = (0, _earendil_works_pi_coding_agent.generateUnifiedPatch)((0, _agimon_ai_po_hashline_files.displayPath)(absolutePath, cwd), before, applied.content);
|
|
80
|
+
const details = {
|
|
81
|
+
diff: diff.diff,
|
|
82
|
+
patch,
|
|
83
|
+
firstChangedLine: diff.firstChangedLine
|
|
84
|
+
};
|
|
85
|
+
assertNotAborted(signal);
|
|
86
|
+
if (!(await (0, node_fs_promises.readFile)(absolutePath)).equals(beforeBytes)) throw new Error("The file changed while the edit was being prepared. Re-read it and retry.");
|
|
87
|
+
assertNotAborted(signal);
|
|
88
|
+
if (applied.content !== before) {
|
|
89
|
+
const output = Buffer.from(`${hasBom ? "" : ""}${editedText}`, "utf8");
|
|
90
|
+
await (0, node_fs_promises.writeFile)(absolutePath, output);
|
|
91
|
+
}
|
|
92
|
+
const count = applied.edits.length;
|
|
93
|
+
const noun = count === 1 ? "range" : "ranges";
|
|
94
|
+
return {
|
|
95
|
+
content: [{
|
|
96
|
+
type: "text",
|
|
97
|
+
text: `${applied.content === before ? `No changes needed in ${params.path}.` : `Edited ${params.path} (${count} ${noun}).`} Re-read before editing it again.`
|
|
98
|
+
}],
|
|
99
|
+
details
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
function restoreOriginalLineEndings(original, edits) {
|
|
104
|
+
const tokens = tokenizeLines(original);
|
|
105
|
+
const defaultEnding = tokens.find((token) => token.ending !== "")?.ending ?? "\n";
|
|
106
|
+
for (const edit of [...edits].reverse()) {
|
|
107
|
+
const start = edit.from.line - 1;
|
|
108
|
+
const count = edit.to.line - edit.from.line + 1;
|
|
109
|
+
const removed = tokens.slice(start, start + count);
|
|
110
|
+
const inheritedEnding = removed.at(-1)?.ending ?? "";
|
|
111
|
+
const internalEnding = removed.find((token) => token.ending !== "")?.ending ?? defaultEnding;
|
|
112
|
+
const lines = replacementLines(edit.content);
|
|
113
|
+
if (lines.length === 0 && removed.at(-1)?.ending === "" && start > 0) {
|
|
114
|
+
const previous = tokens[start - 1];
|
|
115
|
+
if (previous) tokens[start - 1] = {
|
|
116
|
+
...previous,
|
|
117
|
+
ending: ""
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
const replacements = lines.map((text, index) => ({
|
|
121
|
+
text,
|
|
122
|
+
ending: index === lines.length - 1 ? inheritedEnding : internalEnding
|
|
123
|
+
}));
|
|
124
|
+
tokens.splice(start, count, ...replacements);
|
|
125
|
+
}
|
|
126
|
+
return tokens.map((token) => `${token.text}${token.ending}`).join("");
|
|
127
|
+
}
|
|
128
|
+
function tokenizeLines(content) {
|
|
129
|
+
const tokens = [];
|
|
130
|
+
let start = 0;
|
|
131
|
+
for (let index = 0; index < content.length; index += 1) {
|
|
132
|
+
const character = content[index];
|
|
133
|
+
if (character !== "\r" && character !== "\n") continue;
|
|
134
|
+
const ending = character === "\r" && content[index + 1] === "\n" ? "\r\n" : character;
|
|
135
|
+
tokens.push({
|
|
136
|
+
text: content.slice(start, index),
|
|
137
|
+
ending
|
|
138
|
+
});
|
|
139
|
+
if (ending === "\r\n") index += 1;
|
|
140
|
+
start = index + 1;
|
|
141
|
+
}
|
|
142
|
+
tokens.push({
|
|
143
|
+
text: content.slice(start),
|
|
144
|
+
ending: ""
|
|
145
|
+
});
|
|
146
|
+
return tokens;
|
|
147
|
+
}
|
|
148
|
+
function replacementLines(content) {
|
|
149
|
+
if (content === null || content === "") return [];
|
|
150
|
+
const normalized = (0, _agimon_ai_po_hashline.normalizeToLf)(content);
|
|
151
|
+
const trimmed = normalized.endsWith("\n") ? normalized.slice(0, -1) : normalized;
|
|
152
|
+
return trimmed === "" ? [] : trimmed.split("\n");
|
|
153
|
+
}
|
|
154
|
+
//#endregion
|
|
155
|
+
Object.defineProperty(exports, "EditParamsSchema", {
|
|
156
|
+
enumerable: true,
|
|
157
|
+
get: function() {
|
|
158
|
+
return EditParamsSchema;
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
Object.defineProperty(exports, "HashlineRangeSchema", {
|
|
162
|
+
enumerable: true,
|
|
163
|
+
get: function() {
|
|
164
|
+
return HashlineRangeSchema;
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
Object.defineProperty(exports, "createHashlineEditTool", {
|
|
168
|
+
enumerable: true,
|
|
169
|
+
get: function() {
|
|
170
|
+
return createHashlineEditTool;
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
Object.defineProperty(exports, "createHeadlessEditTool", {
|
|
174
|
+
enumerable: true,
|
|
175
|
+
get: function() {
|
|
176
|
+
return createHeadlessEditTool;
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
Object.defineProperty(exports, "executeHashlineEdit", {
|
|
180
|
+
enumerable: true,
|
|
181
|
+
get: function() {
|
|
182
|
+
return executeHashlineEdit;
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
//# sourceMappingURL=editTool-BKaEjUdm.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"editTool-BKaEjUdm.cjs","names":["Type","resolveInputPath","normalizeFileTag","withFileMutationQueue","access","constants","readFile","computeFileTag","decodeUtf8","stripBom","normalizeToLf","applyHashlineEdits","generateDiffString","generateUnifiedPatch","displayPath","writeFile"],"sources":["../src/schemas/editTool.ts","../src/services/editTool/index.ts"],"sourcesContent":["import { type Static, Type } from 'typebox';\n\nexport const HashlineRangeSchema = Type.Object({\n from: Type.String({ description: 'One inclusive starting anchor, for example 5#abc. Do not paste multiple lines.' }),\n to: Type.String({ description: 'One inclusive ending anchor, for example 8#def. Do not paste multiple lines.' }),\n content: Type.Optional(\n Type.Union([\n Type.String({ description: 'Replacement content. Empty content deletes the selected lines.' }),\n Type.Null({ description: 'Delete the selected lines.' }),\n ]),\n ),\n});\n\nexport const EditParamsSchema = Type.Object({\n path: Type.String({ description: 'Path to the file returned by a compatible hashline read or grep tool.' }),\n hash: Type.String({ description: 'Eight-character exact-byte file tag returned by read or grep.' }),\n edits: Type.Array(HashlineRangeSchema, {\n minItems: 1,\n description: 'Non-overlapping inclusive ranges from the same original snapshot.',\n }),\n});\n\nexport type HashlineRange = Static<typeof HashlineRangeSchema>;\nexport type EditParams = Static<typeof EditParamsSchema>;\n","import { constants } from 'node:fs';\nimport { access, readFile, writeFile } from 'node:fs/promises';\n\nimport type {\n DoomHeadlessExecutionContext,\n DoomHeadlessTool,\n DoomHeadlessToolResult,\n} from '@agimon-ai/po-core/headless';\nimport {\n applyHashlineEdits,\n normalizeFileTag,\n normalizeToLf,\n stripBom,\n type PreparedHashlineEdit,\n} from '@agimon-ai/po-hashline';\nimport { computeFileTag, decodeUtf8, displayPath, resolveInputPath } from '@agimon-ai/po-hashline/files';\nimport {\n generateDiffString,\n generateUnifiedPatch,\n withFileMutationQueue,\n type EditToolDetails,\n} from '@earendil-works/pi-coding-agent';\nimport type { ToolDefinition } from '@earendil-works/pi-coding-agent';\n\nimport { EditParamsSchema, type EditParams } from '../../schemas/editTool';\n\nexport function createHashlineEditTool(): ToolDefinition<typeof EditParamsSchema> {\n return {\n name: 'edit',\n label: 'edit',\n description:\n 'Edit one file using its exact snapshot hash and inclusive anchors such as 5#abc from read or grep. Each from and to value must contain one anchor, not a pasted block. All ranges refer to the original snapshot. Empty or omitted content deletes a range.',\n promptSnippet: 'Edit files with snapshot-bound hashline ranges',\n promptGuidelines: [\n 'Copy path, hash, and anchors from the latest compatible read or grep result. Re-read after every successful edit.',\n 'Pass one anchor such as 5#abc in each from and to value. Do not paste tagged lines or multiline blocks.',\n 'Put multiple disjoint changes to one file in one edit call. All anchors must describe the original snapshot.',\n 'Omit content or pass an empty string to delete an inclusive range. Merge overlapping ranges before calling edit.',\n ],\n parameters: EditParamsSchema,\n executionMode: 'parallel',\n async execute(_toolCallId, params, signal, _onUpdate, ctx) {\n return executeHashlineEdit(params as EditParams, ctx.cwd, signal);\n },\n };\n}\n\nexport function createHeadlessEditTool(): DoomHeadlessTool<typeof EditParamsSchema> {\n return {\n name: 'edit',\n label: 'edit',\n description:\n 'Edit one file using its exact snapshot hash and inclusive anchors such as 5#abc from read or grep. Each from and to value must contain one anchor, not a pasted block. All ranges refer to the original snapshot. Empty or omitted content deletes a range.',\n promptSnippet: 'Edit files with snapshot-bound hashline ranges',\n promptGuidelines: [\n 'Copy path, hash, and anchors from the latest compatible read or grep result. Re-read after every successful edit.',\n 'Pass one anchor such as 5#abc in each from and to value. Do not paste tagged lines or multiline blocks.',\n ],\n parameters: EditParamsSchema,\n executionMode: 'parallel',\n execute: (\n _toolCallId: string,\n params: EditParams,\n signal: AbortSignal | undefined,\n _onUpdate: ((result: DoomHeadlessToolResult) => void) | undefined,\n context: DoomHeadlessExecutionContext,\n ) => executeHashlineEdit(params, context.cwd, signal),\n };\n}\n\nexport function assertNotAborted(signal: AbortSignal | undefined): void {\n if (signal?.aborted) throw new Error('Operation aborted');\n}\n\nexport async function executeHashlineEdit(\n params: EditParams,\n cwd: string,\n signal: AbortSignal | undefined,\n): Promise<{ content: [{ type: 'text'; text: string }]; details: EditToolDetails }> {\n const absolutePath = resolveInputPath(params.path, cwd);\n const expectedHash = normalizeFileTag(params.hash);\n return withFileMutationQueue(absolutePath, async () => {\n assertNotAborted(signal);\n await access(absolutePath, constants.R_OK | constants.W_OK);\n const beforeBytes = await readFile(absolutePath);\n assertNotAborted(signal);\n\n const actualHash = computeFileTag(beforeBytes);\n if (actualHash !== expectedHash) {\n throw new Error(`Stale file hash ${expectedHash}. Current hash is ${actualHash}. Re-read the file and retry.`);\n }\n\n const hasBom = beforeBytes.subarray(0, 3).equals(Buffer.from([0xef, 0xbb, 0xbf]));\n const decoded = decodeUtf8(beforeBytes, params.path);\n const withoutBom = stripBom(decoded);\n const before = normalizeToLf(withoutBom);\n const applied = applyHashlineEdits(before, params.edits);\n const editedText = restoreOriginalLineEndings(withoutBom, applied.edits);\n if (normalizeToLf(editedText) !== applied.content) {\n throw new Error('Could not preserve the file line endings safely. The file was not changed.');\n }\n const diff = generateDiffString(before, applied.content);\n const patch = generateUnifiedPatch(displayPath(absolutePath, cwd), before, applied.content);\n const details: EditToolDetails = { diff: diff.diff, patch, firstChangedLine: diff.firstChangedLine };\n\n assertNotAborted(signal);\n const currentBytes = await readFile(absolutePath);\n if (!currentBytes.equals(beforeBytes)) {\n throw new Error('The file changed while the edit was being prepared. Re-read it and retry.');\n }\n assertNotAborted(signal);\n\n if (applied.content !== before) {\n const output = Buffer.from(`${hasBom ? '\\ufeff' : ''}${editedText}`, 'utf8');\n await writeFile(absolutePath, output);\n }\n\n const count = applied.edits.length;\n const noun = count === 1 ? 'range' : 'ranges';\n const message =\n applied.content === before ? `No changes needed in ${params.path}.` : `Edited ${params.path} (${count} ${noun}).`;\n return { content: [{ type: 'text', text: `${message} Re-read before editing it again.` }], details };\n });\n}\n\ninterface LineToken {\n readonly text: string;\n readonly ending: '\\r\\n' | '\\n' | '\\r' | '';\n}\n\nfunction restoreOriginalLineEndings(original: string, edits: readonly PreparedHashlineEdit[]): string {\n const tokens = tokenizeLines(original);\n const defaultEnding = tokens.find((token) => token.ending !== '')?.ending ?? '\\n';\n for (const edit of [...edits].reverse()) {\n const start = edit.from.line - 1;\n const count = edit.to.line - edit.from.line + 1;\n const removed = tokens.slice(start, start + count);\n const inheritedEnding = removed.at(-1)?.ending ?? '';\n const internalEnding = removed.find((token) => token.ending !== '')?.ending ?? defaultEnding;\n const lines = replacementLines(edit.content);\n if (lines.length === 0 && removed.at(-1)?.ending === '' && start > 0) {\n const previous = tokens[start - 1];\n if (previous) tokens[start - 1] = { ...previous, ending: '' };\n }\n const replacements = lines.map<LineToken>((text, index) => ({\n text,\n ending: index === lines.length - 1 ? inheritedEnding : internalEnding,\n }));\n tokens.splice(start, count, ...replacements);\n }\n return tokens.map((token) => `${token.text}${token.ending}`).join('');\n}\n\nfunction tokenizeLines(content: string): LineToken[] {\n const tokens: LineToken[] = [];\n let start = 0;\n for (let index = 0; index < content.length; index += 1) {\n const character = content[index];\n if (character !== '\\r' && character !== '\\n') continue;\n const ending = character === '\\r' && content[index + 1] === '\\n' ? '\\r\\n' : character;\n tokens.push({ text: content.slice(start, index), ending });\n if (ending === '\\r\\n') index += 1;\n start = index + 1;\n }\n tokens.push({ text: content.slice(start), ending: '' });\n return tokens;\n}\n\nfunction replacementLines(content: string | null): string[] {\n if (content === null || content === '') return [];\n const normalized = normalizeToLf(content);\n const trimmed = normalized.endsWith('\\n') ? normalized.slice(0, -1) : normalized;\n return trimmed === '' ? [] : trimmed.split('\\n');\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { defineMcpWidget } from "@agimon-ai/po-core/web";
|
|
2
|
+
import { McpToolFields, McpToolFrame, McpToolOutput } from "@agimon-ai/po-web-components";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
//#region src/extensions/workspaces/sessions/(frontend)/tool/edit.mcp.tsx
|
|
5
|
+
function EditMcpWidget(props) {
|
|
6
|
+
return /* @__PURE__ */ jsxs(McpToolFrame, {
|
|
7
|
+
...props,
|
|
8
|
+
title: "Edit file",
|
|
9
|
+
children: [/* @__PURE__ */ jsx(McpToolFields, { fields: [
|
|
10
|
+
["Path", props.args.path],
|
|
11
|
+
["File revision", props.args.hash],
|
|
12
|
+
["Changes", Array.isArray(props.args.edits) ? props.args.edits.length : void 0]
|
|
13
|
+
] }), /* @__PURE__ */ jsx(McpToolOutput, { result: props.result })]
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region generated/mcp-ui.ts
|
|
18
|
+
const widgets = { "@agimon-ai/po-edit/edit": defineMcpWidget(EditMcpWidget) };
|
|
19
|
+
//#endregion
|
|
20
|
+
export { widgets };
|
|
21
|
+
|
|
22
|
+
//# sourceMappingURL=mcp-ui.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-ui.mjs","names":["Widget0"],"sources":["../../src/extensions/workspaces/sessions/(frontend)/tool/edit.mcp.tsx","../../generated/mcp-ui.ts"],"sourcesContent":["import { defineMcpWidget, type DoomMcpWidgetProps } from '@agimon-ai/po-core/web';\nimport { McpToolFrame, McpToolFields, McpToolOutput } from '@agimon-ai/po-web-components';\n\nfunction EditMcpWidget(props: DoomMcpWidgetProps) {\n return (\n <McpToolFrame {...props} title=\"Edit file\">\n <McpToolFields\n fields={[\n ['Path', props.args.path],\n ['File revision', props.args.hash],\n ['Changes', Array.isArray(props.args.edits) ? props.args.edits.length : undefined],\n ]}\n />\n <McpToolOutput result={props.result} />\n </McpToolFrame>\n );\n}\n\nexport default defineMcpWidget(EditMcpWidget);\n","// Generated by @agimon-ai/po-build. Do not edit by hand.\nimport type { ComponentType } from 'react';\nimport type { DoomMcpWidgetProps } from '@agimon-ai/po-core/mcpWidget';\nimport Widget0 from \"../src/extensions/workspaces/sessions/(frontend)/tool/edit.mcp\";\n\nexport const widgets: Readonly<Record<string, ComponentType<DoomMcpWidgetProps>>> = {\n \"@agimon-ai/po-edit/edit\": Widget0,\n};\n"],"mappings":";;;;AAGA,SAAS,cAAc,OAA2B;CAChD,OACE,qBAAC,cAAD;EAAc,GAAI;EAAO,OAAM;EAA/B,UAAA,CACE,oBAAC,eAAD,EACE,QAAQ;GACN,CAAC,QAAQ,MAAM,KAAK,IAAI;GACxB,CAAC,iBAAiB,MAAM,KAAK,IAAI;GACjC,CAAC,WAAW,MAAM,QAAQ,MAAM,KAAK,KAAK,IAAI,MAAM,KAAK,MAAM,SAAS,KAAA,CAAS;EACnF,EACD,CAAA,GACD,oBAAC,eAAD,EAAe,QAAQ,MAAM,OAAS,CAAA,CAC1B;;AAElB;;;ACXA,MAAa,UAAuE,EAClF,2BDYa,gBAAgB,aCZFA,EAC7B"}
|