@iinm/plain-agent 1.14.4 → 1.14.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/cli/formatter.mjs +8 -54
- package/src/tools/patchFile.mjs +87 -1
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Plain Agent
|
|
2
2
|
|
|
3
3
|
[](https://github.com/iinm/plain-agent/actions/workflows/github-code-scanning/codeql)
|
|
4
|
-
[](https://socket.dev/npm/package/@iinm/plain-agent)
|
|
5
5
|
[](https://packagephobia.com/result?p=@iinm/plain-agent)
|
|
6
6
|
|
|
7
7
|
A lightweight terminal-based coding agent focused on safety and low token cost
|
package/package.json
CHANGED
package/src/cli/formatter.mjs
CHANGED
|
@@ -11,8 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import fs from "node:fs/promises";
|
|
13
13
|
import { styleText } from "node:util";
|
|
14
|
-
import { parseBlocks } from "../tools/patchFile.mjs";
|
|
15
|
-
import { diffLines } from "../utils/diffLines.mjs";
|
|
14
|
+
import { parseBlocks, renderPatchBlock } from "../tools/patchFile.mjs";
|
|
16
15
|
import { noThrow } from "../utils/noThrow.mjs";
|
|
17
16
|
|
|
18
17
|
/** Length above which a single-line arg forces block-form rendering. */
|
|
@@ -863,61 +862,16 @@ async function renderPatch(filePath, patch) {
|
|
|
863
862
|
}
|
|
864
863
|
|
|
865
864
|
return blocks
|
|
866
|
-
.map((block) =>
|
|
865
|
+
.map((block) =>
|
|
866
|
+
renderPatchBlock(block, originalLines, nonce, {
|
|
867
|
+
header: (text) => styleText("cyan", text),
|
|
868
|
+
del: (text) => styleText("magenta", text),
|
|
869
|
+
add: (text) => styleText("green", text),
|
|
870
|
+
}),
|
|
871
|
+
)
|
|
867
872
|
.join("\n\n");
|
|
868
873
|
}
|
|
869
874
|
|
|
870
|
-
/**
|
|
871
|
-
* @param {PatchBlock} block
|
|
872
|
-
* @param {string[] | null} originalLines
|
|
873
|
-
* @param {string} nonce
|
|
874
|
-
* @returns {string}
|
|
875
|
-
*/
|
|
876
|
-
function renderPatchBlock(block, originalLines, nonce) {
|
|
877
|
-
/** @type {string[]} */
|
|
878
|
-
const out = [];
|
|
879
|
-
if (block.op === "replace") {
|
|
880
|
-
out.push(
|
|
881
|
-
styleText(
|
|
882
|
-
"cyan",
|
|
883
|
-
`REPLACE ${nonce} ${block.start}:${block.startHash}-${block.end}:${block.endHash}`,
|
|
884
|
-
),
|
|
885
|
-
);
|
|
886
|
-
if (originalLines) {
|
|
887
|
-
const safeStart = Math.max(1, block.start);
|
|
888
|
-
const safeEnd = Math.min(originalLines.length, block.end);
|
|
889
|
-
const oldSlice = originalLines.slice(safeStart - 1, safeEnd);
|
|
890
|
-
// Use a real line diff so unchanged lines render as context
|
|
891
|
-
// (no color, " " prefix) instead of being shown as both "- " and
|
|
892
|
-
// "+ ".
|
|
893
|
-
for (const op of diffLines(oldSlice, block.body)) {
|
|
894
|
-
if (op.type === "-") {
|
|
895
|
-
out.push(styleText("magenta", `- ${op.line}`));
|
|
896
|
-
} else if (op.type === "+") {
|
|
897
|
-
out.push(styleText("green", `+ ${op.line}`));
|
|
898
|
-
} else {
|
|
899
|
-
out.push(` ${op.line}`);
|
|
900
|
-
}
|
|
901
|
-
}
|
|
902
|
-
} else {
|
|
903
|
-
// No file context available — fall back to listing the body as
|
|
904
|
-
// additions so the user can still see the new content.
|
|
905
|
-
for (const line of block.body) {
|
|
906
|
-
out.push(styleText("green", `+ ${line}`));
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
} else {
|
|
910
|
-
const afterSuffix = block.afterHash ? `:${block.afterHash}` : "";
|
|
911
|
-
out.push(
|
|
912
|
-
styleText("cyan", `INSERT_AFTER ${nonce} ${block.after}${afterSuffix}`),
|
|
913
|
-
);
|
|
914
|
-
for (const line of block.body) {
|
|
915
|
-
out.push(styleText("green", `+ ${line}`));
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
return out.join("\n");
|
|
919
|
-
}
|
|
920
|
-
|
|
921
875
|
/**
|
|
922
876
|
* Verbatim highlighter used as fallback when block-aware rendering is not
|
|
923
877
|
* possible (parse error, missing nonce, etc.).
|
package/src/tools/patchFile.mjs
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import fs from "node:fs/promises";
|
|
7
|
+
import { diffLines } from "../utils/diffLines.mjs";
|
|
7
8
|
import { lineHash } from "../utils/lineHash.mjs";
|
|
8
9
|
import { noThrow } from "../utils/noThrow.mjs";
|
|
9
10
|
|
|
@@ -66,9 +67,14 @@ content at beginning of file
|
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
const original = await fs.readFile(filePath, "utf8");
|
|
70
|
+
const originalLines = splitLines(original);
|
|
69
71
|
const newContent = applyBlocks(original, blocks);
|
|
70
72
|
await fs.writeFile(filePath, newContent);
|
|
71
|
-
|
|
73
|
+
|
|
74
|
+
const diff = blocks
|
|
75
|
+
.map((block) => renderPatchBlock(block, originalLines, nonce))
|
|
76
|
+
.join("\n\n");
|
|
77
|
+
return `Patched file: ${filePath}\n${diff}`;
|
|
72
78
|
}),
|
|
73
79
|
|
|
74
80
|
/**
|
|
@@ -234,6 +240,71 @@ export function applyBlocks(original, blocks) {
|
|
|
234
240
|
return result;
|
|
235
241
|
}
|
|
236
242
|
|
|
243
|
+
/**
|
|
244
|
+
* @typedef {(text: string) => string} DiffStyler
|
|
245
|
+
*/
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Render a single patch block as a unified-style diff of the change against
|
|
249
|
+
* the original file: the block header, then removed lines ("- "), added lines
|
|
250
|
+
* ("+ "), and unchanged context (" ") for its range only.
|
|
251
|
+
*
|
|
252
|
+
* The optional `style` callbacks colorize the header and change lines; they
|
|
253
|
+
* default to identity so the output is plain text suitable for tool results,
|
|
254
|
+
* while the CLI passes styleText-based stylers for colored display.
|
|
255
|
+
*
|
|
256
|
+
* @param {PatchBlock} block
|
|
257
|
+
* @param {string[] | null} originalLines
|
|
258
|
+
* @param {string} nonce
|
|
259
|
+
* @param {{ header?: DiffStyler, del?: DiffStyler, add?: DiffStyler }} [style]
|
|
260
|
+
* @returns {string}
|
|
261
|
+
*/
|
|
262
|
+
export function renderPatchBlock(block, originalLines, nonce, style = {}) {
|
|
263
|
+
const header = style.header ?? ((text) => text);
|
|
264
|
+
const del = style.del ?? ((text) => text);
|
|
265
|
+
const add = style.add ?? ((text) => text);
|
|
266
|
+
|
|
267
|
+
/** @type {string[]} */
|
|
268
|
+
const out = [];
|
|
269
|
+
if (block.op === "replace") {
|
|
270
|
+
out.push(
|
|
271
|
+
header(
|
|
272
|
+
`REPLACE ${nonce} ${block.start}:${block.startHash}-${block.end}:${block.endHash}`,
|
|
273
|
+
),
|
|
274
|
+
);
|
|
275
|
+
if (originalLines) {
|
|
276
|
+
const safeStart = Math.max(1, block.start);
|
|
277
|
+
const safeEnd = Math.min(originalLines.length, block.end);
|
|
278
|
+
const oldSlice = originalLines.slice(safeStart - 1, safeEnd);
|
|
279
|
+
// Use a real line diff so unchanged lines render as context
|
|
280
|
+
// (no color, " " prefix) instead of being shown as both "- " and
|
|
281
|
+
// "+ ".
|
|
282
|
+
for (const op of diffLines(oldSlice, block.body)) {
|
|
283
|
+
if (op.type === "-") {
|
|
284
|
+
out.push(del(`- ${op.line}`));
|
|
285
|
+
} else if (op.type === "+") {
|
|
286
|
+
out.push(add(`+ ${op.line}`));
|
|
287
|
+
} else {
|
|
288
|
+
out.push(` ${op.line}`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
} else {
|
|
292
|
+
// No file context available — fall back to listing the body as
|
|
293
|
+
// additions so the new content is still visible.
|
|
294
|
+
for (const line of block.body) {
|
|
295
|
+
out.push(add(`+ ${line}`));
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
} else {
|
|
299
|
+
const afterSuffix = block.afterHash ? `:${block.afterHash}` : "";
|
|
300
|
+
out.push(header(`INSERT_AFTER ${nonce} ${block.after}${afterSuffix}`));
|
|
301
|
+
for (const line of block.body) {
|
|
302
|
+
out.push(add(`+ ${line}`));
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return out.join("\n");
|
|
306
|
+
}
|
|
307
|
+
|
|
237
308
|
/**
|
|
238
309
|
* @param {string} headerArgs
|
|
239
310
|
* @param {"replace" | "insert"} op
|
|
@@ -374,3 +445,18 @@ function detectConflicts(blocks) {
|
|
|
374
445
|
}
|
|
375
446
|
}
|
|
376
447
|
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Split file content into lines the same way applyBlocks does: drop the
|
|
451
|
+
* trailing empty element produced by split() when the content ends with a
|
|
452
|
+
* newline (or is empty), so line numbers match read_file.
|
|
453
|
+
* @param {string} content
|
|
454
|
+
* @returns {string[]}
|
|
455
|
+
*/
|
|
456
|
+
function splitLines(content) {
|
|
457
|
+
const lines = content.split("\n");
|
|
458
|
+
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
459
|
+
lines.pop();
|
|
460
|
+
}
|
|
461
|
+
return lines;
|
|
462
|
+
}
|