@iinm/plain-agent 1.8.5 → 1.8.6
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iinm/plain-agent",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.6",
|
|
4
4
|
"description": "A lightweight CLI-based coding agent",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -33,10 +33,7 @@
|
|
|
33
33
|
"lint": "npx @biomejs/biome check",
|
|
34
34
|
"fix": "npx @biomejs/biome check --fix"
|
|
35
35
|
},
|
|
36
|
-
"dependencies": {
|
|
37
|
-
"diff": "^8.0.4",
|
|
38
|
-
"yaml": "^2.8.3"
|
|
39
|
-
},
|
|
36
|
+
"dependencies": {},
|
|
40
37
|
"devDependencies": {
|
|
41
38
|
"@biomejs/biome": "^2.4.12",
|
|
42
39
|
"@types/node": "^22.19.17",
|
package/src/cliFormatter.mjs
CHANGED
|
@@ -8,8 +8,12 @@
|
|
|
8
8
|
* @import { SwitchToSubagentInput } from "./tools/switchToSubagent"
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { execFile } from "node:child_process";
|
|
12
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
13
|
+
import os from "node:os";
|
|
14
|
+
import path from "node:path";
|
|
11
15
|
import { styleText } from "node:util";
|
|
12
|
-
import {
|
|
16
|
+
import { noThrow } from "./utils/noThrow.mjs";
|
|
13
17
|
|
|
14
18
|
/** Length above which a single-line arg forces block-form rendering. */
|
|
15
19
|
const ARG_BLOCK_LENGTH_THRESHOLD = 60;
|
|
@@ -57,9 +61,11 @@ export function formatArgs(args) {
|
|
|
57
61
|
/**
|
|
58
62
|
* Format tool use for display.
|
|
59
63
|
* @param {MessageContentToolUse} toolUse
|
|
60
|
-
* @
|
|
64
|
+
* @param {{ createDiff?: (oldContent: string, newContent: string) => Promise<string | null> }} [options]
|
|
65
|
+
* @returns {Promise<string>}
|
|
61
66
|
*/
|
|
62
|
-
export function formatToolUse(toolUse) {
|
|
67
|
+
export async function formatToolUse(toolUse, options = {}) {
|
|
68
|
+
const { createDiff = tryGitDiff } = options;
|
|
63
69
|
const { toolName, input } = toolUse;
|
|
64
70
|
|
|
65
71
|
if (toolName === "exec_command") {
|
|
@@ -99,23 +105,26 @@ export function formatToolUse(toolUse) {
|
|
|
99
105
|
diffs.push({ search, replace });
|
|
100
106
|
}
|
|
101
107
|
|
|
102
|
-
const highlightedDiff =
|
|
103
|
-
.map(
|
|
104
|
-
(
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
108
|
+
const highlightedDiff = await Promise.all(
|
|
109
|
+
diffs.map(async ({ search, replace }) => {
|
|
110
|
+
const gitDiffOutput = await createDiff(search, replace);
|
|
111
|
+
if (gitDiffOutput) {
|
|
112
|
+
return `${gitDiffOutput}\n-------\n${replace}`;
|
|
113
|
+
}
|
|
114
|
+
return [
|
|
115
|
+
`${styleText("yellow", "(git diff unavailable, showing plain diff)")}`,
|
|
116
|
+
"--- old",
|
|
117
|
+
`${search}`,
|
|
118
|
+
"+++ new",
|
|
119
|
+
`${replace}`,
|
|
120
|
+
].join("\n");
|
|
121
|
+
}),
|
|
122
|
+
);
|
|
114
123
|
|
|
115
124
|
return [
|
|
116
125
|
`tool: ${toolName}`,
|
|
117
126
|
`path: ${patchFileInput.filePath}`,
|
|
118
|
-
`diff:\n${highlightedDiff}`,
|
|
127
|
+
`diff:\n${highlightedDiff.join("\n\n")}`,
|
|
119
128
|
].join("\n");
|
|
120
129
|
}
|
|
121
130
|
|
|
@@ -349,11 +358,20 @@ export function formatCostForBatch(summary) {
|
|
|
349
358
|
/**
|
|
350
359
|
* Print a message to the console.
|
|
351
360
|
* @param {Message} message
|
|
361
|
+
* @returns {Promise<void>}
|
|
352
362
|
*/
|
|
353
|
-
export function printMessage(message) {
|
|
363
|
+
export async function printMessage(message) {
|
|
354
364
|
switch (message.role) {
|
|
355
365
|
case "assistant": {
|
|
356
366
|
// console.log(styleText("bold", "\nAgent:"));
|
|
367
|
+
// Pre-format all tool_use parts in parallel to avoid sequential awaits
|
|
368
|
+
const toolUseParts = message.content.filter(
|
|
369
|
+
(part) => part.type === "tool_use",
|
|
370
|
+
);
|
|
371
|
+
const formattedToolUses = await Promise.all(
|
|
372
|
+
toolUseParts.map((part) => formatToolUse(part)),
|
|
373
|
+
);
|
|
374
|
+
let toolUseIndex = 0;
|
|
357
375
|
for (const part of message.content) {
|
|
358
376
|
switch (part.type) {
|
|
359
377
|
// Note: Streamで表示するためここでは表示しない
|
|
@@ -371,7 +389,7 @@ export function printMessage(message) {
|
|
|
371
389
|
// break;
|
|
372
390
|
case "tool_use":
|
|
373
391
|
console.log(styleText("bold", "\nTool call:"));
|
|
374
|
-
console.log(
|
|
392
|
+
console.log(formattedToolUses[toolUseIndex++]);
|
|
375
393
|
break;
|
|
376
394
|
}
|
|
377
395
|
}
|
|
@@ -411,3 +429,90 @@ export function printMessage(message) {
|
|
|
411
429
|
}
|
|
412
430
|
}
|
|
413
431
|
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Generate a colored unified diff using `git diff --color`.
|
|
435
|
+
* Falls back to `null` if git is unavailable or if any step fails
|
|
436
|
+
* (temp directory creation, file writing, git execution, or cleanup).
|
|
437
|
+
* @param {string} oldContent
|
|
438
|
+
* @param {string} newContent
|
|
439
|
+
* @returns {Promise<string | null>}
|
|
440
|
+
*/
|
|
441
|
+
async function tryGitDiff(oldContent, newContent) {
|
|
442
|
+
const tmpDir = await noThrow(() =>
|
|
443
|
+
mkdtemp(path.join(os.tmpdir(), "git-diff-")),
|
|
444
|
+
);
|
|
445
|
+
if (tmpDir instanceof Error) {
|
|
446
|
+
console.error(
|
|
447
|
+
styleText("yellow", `git diff: mkdtemp failed: ${tmpDir.message}`),
|
|
448
|
+
);
|
|
449
|
+
return null;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const oldPath = path.join(tmpDir, "old");
|
|
453
|
+
const newPath = path.join(tmpDir, "new");
|
|
454
|
+
|
|
455
|
+
try {
|
|
456
|
+
const w1 = await noThrow(() => writeFile(oldPath, oldContent, "utf8"));
|
|
457
|
+
if (w1 instanceof Error) {
|
|
458
|
+
console.error(
|
|
459
|
+
styleText("yellow", `git diff: writeFile(old) failed: ${w1.message}`),
|
|
460
|
+
);
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const w2 = await noThrow(() => writeFile(newPath, newContent, "utf8"));
|
|
465
|
+
if (w2 instanceof Error) {
|
|
466
|
+
console.error(
|
|
467
|
+
styleText("yellow", `git diff: writeFile(new) failed: ${w2.message}`),
|
|
468
|
+
);
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const diffResult = await noThrow(() => execGitDiff(oldPath, newPath));
|
|
473
|
+
if (diffResult instanceof Error) {
|
|
474
|
+
console.error(
|
|
475
|
+
styleText("yellow", `git diff: exec failed: ${diffResult.message}`),
|
|
476
|
+
);
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
return diffResult;
|
|
481
|
+
} finally {
|
|
482
|
+
const cleanup = await noThrow(() =>
|
|
483
|
+
rm(tmpDir, { recursive: true, force: true }),
|
|
484
|
+
);
|
|
485
|
+
if (cleanup instanceof Error) {
|
|
486
|
+
console.error(
|
|
487
|
+
styleText("yellow", `git diff: cleanup failed: ${cleanup.message}`),
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Execute git diff accepting exit code 1 as success (differences found).
|
|
495
|
+
* @param {string} oldPath
|
|
496
|
+
* @param {string} newPath
|
|
497
|
+
* @returns {Promise<string>}
|
|
498
|
+
*/
|
|
499
|
+
function execGitDiff(oldPath, newPath) {
|
|
500
|
+
return new Promise((resolve, reject) => {
|
|
501
|
+
execFile(
|
|
502
|
+
"git",
|
|
503
|
+
["--no-pager", "diff", "--color", "--no-index", "--", oldPath, newPath],
|
|
504
|
+
{ encoding: "utf8", maxBuffer: 10 * 1024 * 1024 },
|
|
505
|
+
(error, stdout, stderr) => {
|
|
506
|
+
if (stderr) {
|
|
507
|
+
console.error(styleText("yellow", `git diff stderr: ${stderr}`));
|
|
508
|
+
}
|
|
509
|
+
// git diff returns exit code 1 when there are differences, which is expected
|
|
510
|
+
if (error && error.code !== 1) {
|
|
511
|
+
reject(error);
|
|
512
|
+
} else {
|
|
513
|
+
resolve(stdout);
|
|
514
|
+
}
|
|
515
|
+
},
|
|
516
|
+
);
|
|
517
|
+
});
|
|
518
|
+
}
|
package/src/cliInteractive.mjs
CHANGED
|
@@ -473,7 +473,11 @@ export function startInteractiveSession({
|
|
|
473
473
|
});
|
|
474
474
|
|
|
475
475
|
agentEventEmitter.on("message", (message) => {
|
|
476
|
-
printMessage(message)
|
|
476
|
+
printMessage(message).catch((err) => {
|
|
477
|
+
console.error(
|
|
478
|
+
styleText("red", `Error rendering message: ${err.message}`),
|
|
479
|
+
);
|
|
480
|
+
});
|
|
477
481
|
});
|
|
478
482
|
|
|
479
483
|
agentEventEmitter.on("toolUseRequest", () => {
|
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
import crypto from "node:crypto";
|
|
4
4
|
import fs from "node:fs/promises";
|
|
5
5
|
import path from "node:path";
|
|
6
|
-
import { parse as parseYaml } from "yaml";
|
|
7
6
|
import {
|
|
8
7
|
AGENT_CACHE_DIR,
|
|
9
8
|
AGENT_PROJECT_METADATA_DIR,
|
|
10
9
|
AGENT_ROOT,
|
|
11
10
|
AGENT_USER_CONFIG_DIR,
|
|
12
11
|
} from "../env.mjs";
|
|
12
|
+
import { parseFrontmatter } from "../utils/parseFrontmatter.mjs";
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* @typedef {Object} AgentRole
|
|
@@ -253,21 +253,7 @@ function parseAgentRole(relativePath, fileContent, fullPath, idPrefix = "") {
|
|
|
253
253
|
};
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
-
|
|
257
|
-
let frontmatter;
|
|
258
|
-
try {
|
|
259
|
-
frontmatter = /** @type {{description?:string; import?:string}} */ (
|
|
260
|
-
parseYaml(match[1])
|
|
261
|
-
);
|
|
262
|
-
} catch (_err) {
|
|
263
|
-
return {
|
|
264
|
-
id,
|
|
265
|
-
description: parseFrontmatterField(match[1], "description") ?? "",
|
|
266
|
-
content: fileContent.trim(),
|
|
267
|
-
filePath: fullPath,
|
|
268
|
-
claudeOriginated,
|
|
269
|
-
};
|
|
270
|
-
}
|
|
256
|
+
const frontmatter = parseFrontmatter(match[1]);
|
|
271
257
|
const content = match[2].trim();
|
|
272
258
|
|
|
273
259
|
return {
|
|
@@ -279,16 +265,3 @@ function parseAgentRole(relativePath, fileContent, fullPath, idPrefix = "") {
|
|
|
279
265
|
import: frontmatter.import,
|
|
280
266
|
};
|
|
281
267
|
}
|
|
282
|
-
|
|
283
|
-
/**
|
|
284
|
-
* Parse a field from YAML frontmatter.
|
|
285
|
-
* @param {string} frontmatter
|
|
286
|
-
* @param {string} field
|
|
287
|
-
* @returns {string | undefined}
|
|
288
|
-
*/
|
|
289
|
-
|
|
290
|
-
function parseFrontmatterField(frontmatter, field) {
|
|
291
|
-
const regex = new RegExp(`^${field}:\\s*(.*)$`, "m");
|
|
292
|
-
const match = frontmatter.match(regex);
|
|
293
|
-
return match ? match[1].trim() : undefined;
|
|
294
|
-
}
|
|
@@ -4,13 +4,13 @@ import { execFileSync } from "node:child_process";
|
|
|
4
4
|
import crypto from "node:crypto";
|
|
5
5
|
import fs from "node:fs/promises";
|
|
6
6
|
import path from "node:path";
|
|
7
|
-
import { parse as parseYaml } from "yaml";
|
|
8
7
|
import {
|
|
9
8
|
AGENT_CACHE_DIR,
|
|
10
9
|
AGENT_PROJECT_METADATA_DIR,
|
|
11
10
|
AGENT_ROOT,
|
|
12
11
|
AGENT_USER_CONFIG_DIR,
|
|
13
12
|
} from "../env.mjs";
|
|
13
|
+
import { parseFrontmatter } from "../utils/parseFrontmatter.mjs";
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* @typedef {Object} Prompt
|
|
@@ -287,29 +287,7 @@ function parsePrompt(relativePath, fileContent, fullPath, idPrefix = "") {
|
|
|
287
287
|
|
|
288
288
|
const content = match[2].trim();
|
|
289
289
|
|
|
290
|
-
|
|
291
|
-
let frontmatter;
|
|
292
|
-
try {
|
|
293
|
-
frontmatter =
|
|
294
|
-
/** @type {{description?:string; import?:string; "user-invocable"?:boolean}} */ (
|
|
295
|
-
parseYaml(match[1])
|
|
296
|
-
);
|
|
297
|
-
} catch (_err) {
|
|
298
|
-
return {
|
|
299
|
-
id,
|
|
300
|
-
description: parseFrontmatterField(match[1], "description") ?? "",
|
|
301
|
-
content,
|
|
302
|
-
filePath: fullPath,
|
|
303
|
-
claudeOriginated,
|
|
304
|
-
import: parseFrontmatterField(match[1], "import"),
|
|
305
|
-
userInvocable:
|
|
306
|
-
parseFrontmatterField(match[1], "user-invocable") === "true" ||
|
|
307
|
-
undefined,
|
|
308
|
-
isShortcut,
|
|
309
|
-
isSkill,
|
|
310
|
-
};
|
|
311
|
-
}
|
|
312
|
-
const userInvocable = frontmatter["user-invocable"];
|
|
290
|
+
const frontmatter = parseFrontmatter(match[1]);
|
|
313
291
|
|
|
314
292
|
return {
|
|
315
293
|
id,
|
|
@@ -318,20 +296,8 @@ function parsePrompt(relativePath, fileContent, fullPath, idPrefix = "") {
|
|
|
318
296
|
filePath: fullPath,
|
|
319
297
|
claudeOriginated,
|
|
320
298
|
import: frontmatter.import,
|
|
321
|
-
userInvocable:
|
|
299
|
+
userInvocable: frontmatter["user-invocable"] === "true" ? true : undefined,
|
|
322
300
|
isShortcut,
|
|
323
301
|
isSkill: relativePath.endsWith("SKILL.md"),
|
|
324
302
|
};
|
|
325
303
|
}
|
|
326
|
-
|
|
327
|
-
/**
|
|
328
|
-
* Parse a field from YAML frontmatter.
|
|
329
|
-
* @param {string} frontmatter
|
|
330
|
-
* @param {string} field
|
|
331
|
-
* @returns {string | undefined}
|
|
332
|
-
*/
|
|
333
|
-
function parseFrontmatterField(frontmatter, field) {
|
|
334
|
-
const regex = new RegExp(`^${field}:\\s*(.*)$`, "m");
|
|
335
|
-
const match = frontmatter.match(regex);
|
|
336
|
-
return match ? match[1].trim() : undefined;
|
|
337
|
-
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse simple key-value frontmatter using regex.
|
|
3
|
+
* Only supports `key: value` format. No multiline strings.
|
|
4
|
+
* @param {string} frontmatter - The YAML frontmatter content (without --- delimiters)
|
|
5
|
+
* @returns {Record<string, string>} Parsed key-value pairs
|
|
6
|
+
*/
|
|
7
|
+
export function parseFrontmatter(frontmatter) {
|
|
8
|
+
/** @type {Record<string, string>} */
|
|
9
|
+
const result = {};
|
|
10
|
+
|
|
11
|
+
for (const line of frontmatter.split(/\r?\n/)) {
|
|
12
|
+
const match = line.match(/^(\w[\w-]*):\s?(.*)$/);
|
|
13
|
+
if (match) {
|
|
14
|
+
result[match[1]] = match[2].trimEnd();
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return result;
|
|
19
|
+
}
|