@iinm/plain-agent 1.7.6 → 1.7.7

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.7.6",
3
+ "version": "1.7.7",
4
4
  "description": "A lightweight CLI-based coding agent",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -47,7 +47,7 @@ export function formatToolUse(toolUse) {
47
47
  const diffs = [];
48
48
  const matches = Array.from(
49
49
  diff.matchAll(
50
- /<<<<<<< SEARCH\n(.*?)\n?=======\n(.*?)\n?>>>>>>> REPLACE/gs,
50
+ /<<<<<<< SEARCH [0-9a-z]{3}\n(.*?)\n======= [0-9a-z]{3}\n(.*?)\n?>>>>>>> REPLACE [0-9a-z]{3}/gs,
51
51
  ),
52
52
  );
53
53
  for (const match of matches) {
@@ -95,6 +95,31 @@ export function formatToolUse(toolUse) {
95
95
  ].join("\n");
96
96
  }
97
97
 
98
+ if (toolName === "report_as_subagent") {
99
+ /** @type {Partial<import("./tools/reportAsSubagent").ReportAsSubagentInput>} */
100
+ const reportAsSubagentInput = input;
101
+ return [
102
+ `tool: ${toolName}`,
103
+ `memoryPath: ${reportAsSubagentInput.memoryPath}`,
104
+ ].join("\n");
105
+ }
106
+
107
+ if (toolName === "ask_web") {
108
+ /** @type {Partial<import("./tools/askWeb.mjs").AskWebInput>} */
109
+ const askWebInput = input;
110
+ return [`tool: ${toolName}`, `question: ${askWebInput.question}`].join(
111
+ "\n",
112
+ );
113
+ }
114
+
115
+ if (toolName === "ask_url") {
116
+ /** @type {Partial<import("./tools/askURL.mjs").AskURLInput>} */
117
+ const askURLInput = input;
118
+ return [`tool: ${toolName}`, `question: ${askURLInput.question}`].join(
119
+ "\n",
120
+ );
121
+ }
122
+
98
123
  const { provider: _, ...filteredToolUse } = toolUse;
99
124
 
100
125
  return JSON.stringify(filteredToolUse, null, 2);
package/src/main.mjs CHANGED
@@ -26,7 +26,7 @@ import { createAskURLTool } from "./tools/askURL.mjs";
26
26
  import { createAskWebTool } from "./tools/askWeb.mjs";
27
27
  import { createDelegateToSubagentTool } from "./tools/delegateToSubagent.mjs";
28
28
  import { createExecCommandTool } from "./tools/execCommand.mjs";
29
- import { patchFileTool } from "./tools/patchFile.mjs";
29
+ import { createPatchFileTool } from "./tools/patchFile.mjs";
30
30
  import { createReportAsSubagentTool } from "./tools/reportAsSubagent.mjs";
31
31
  import { createTmuxCommandTool } from "./tools/tmuxCommand.mjs";
32
32
  import { writeFileTool } from "./tools/writeFile.mjs";
@@ -162,7 +162,7 @@ if (cliArgs.subcommand.type === "install-claude-code-plugins") {
162
162
  const builtinTools = [
163
163
  createExecCommandTool({ sandbox: appConfig.sandbox }),
164
164
  writeFileTool,
165
- patchFileTool,
165
+ createPatchFileTool(),
166
166
  createTmuxCommandTool({ sandbox: appConfig.sandbox }),
167
167
  createDelegateToSubagentTool(),
168
168
  createReportAsSubagentTool(),
@@ -25,7 +25,7 @@ import { noThrow } from "../utils/noThrow.mjs";
25
25
  */
26
26
 
27
27
  /**
28
- * @typedef {Object} AskWebInput
28
+ * @typedef {Object} AskURLInput
29
29
  * @property {string} question
30
30
  */
31
31
 
@@ -35,7 +35,7 @@ import { noThrow } from "../utils/noThrow.mjs";
35
35
  */
36
36
  export function createAskURLTool(config) {
37
37
  /**
38
- * @param {AskWebInput} input
38
+ * @param {AskURLInput} input
39
39
  * @param {number} retryCount
40
40
  * @returns {Promise<string | Error>}
41
41
  */
@@ -193,9 +193,17 @@ Question: ${input.question}`,
193
193
  },
194
194
 
195
195
  /**
196
- * @param {AskWebInput} input
196
+ * @param {AskURLInput} input
197
197
  * @returns {Promise<string | Error>}
198
198
  */
199
199
  impl: async (input) => await noThrow(async () => askURL(input, 0)),
200
+
201
+ /**
202
+ * @param {Record<string, unknown>} _input
203
+ * @returns {Record<string, unknown>}
204
+ */
205
+ maskApprovalInput: (_input) => {
206
+ return {};
207
+ },
200
208
  };
201
209
  }
@@ -196,5 +196,13 @@ Question: ${input.question}`,
196
196
  * @returns {Promise<string | Error>}
197
197
  */
198
198
  impl: async (input) => await noThrow(async () => askWeb(input, 0)),
199
+
200
+ /**
201
+ * @param {Record<string, unknown>} _input
202
+ * @returns {Record<string, unknown>}
203
+ */
204
+ maskApprovalInput: (_input) => {
205
+ return {};
206
+ },
199
207
  };
200
208
  }
@@ -6,91 +6,101 @@
6
6
  import fs from "node:fs/promises";
7
7
  import { noThrow } from "../utils/noThrow.mjs";
8
8
 
9
- /** @type {Tool} */
10
- export const patchFileTool = {
11
- def: {
12
- name: "patch_file",
13
- description:
14
- "Modify a file by replacing specific content with new content.",
15
- inputSchema: {
16
- type: "object",
17
- properties: {
18
- filePath: {
19
- type: "string",
20
- },
21
- diff: {
22
- description: `
9
+ /**
10
+ * @param {string} [nonce]
11
+ * @returns {Tool}
12
+ */
13
+ export function createPatchFileTool(
14
+ nonce = Math.random().toString(36).substring(2, 5),
15
+ ) {
16
+ return {
17
+ def: {
18
+ name: "patch_file",
19
+ description:
20
+ "Modify a file by replacing specific content with new content.",
21
+ inputSchema: {
22
+ type: "object",
23
+ properties: {
24
+ filePath: {
25
+ type: "string",
26
+ },
27
+ diff: {
28
+ description: `
23
29
  - Content is searched as an exact match including indentation and line breaks.
24
30
  - The first match found will be replaced if there are multiple matches.
25
- - Use multiple SEARCH/REPLACE blocks to replace multiple contents.
31
+ - Use multiple SEARCH/REPLACE blocks with nonce (${nonce}) to replace multiple contents.
26
32
 
27
33
  Format:
28
- <<<<<<< SEARCH
34
+ <<<<<<< SEARCH ${nonce}
29
35
  old content
30
- =======
36
+ ======= ${nonce}
31
37
  new content
32
- >>>>>>> REPLACE
38
+ >>>>>>> REPLACE ${nonce}
33
39
 
34
- <<<<<<< SEARCH
40
+ <<<<<<< SEARCH ${nonce}
35
41
  other old content
36
- =======
42
+ ======= ${nonce}
37
43
  other new content
38
- >>>>>>> REPLACE
44
+ >>>>>>> REPLACE ${nonce}
39
45
  `.trim(),
40
- type: "string",
46
+ type: "string",
47
+ },
41
48
  },
49
+ required: ["filePath", "diff"],
42
50
  },
43
- required: ["filePath", "diff"],
44
51
  },
45
- },
46
52
 
47
- /**
48
- * @param {PatchFileInput} input
49
- * @returns {Promise<string | Error>}
50
- */
51
- impl: async (input) =>
52
- await noThrow(async () => {
53
- const { filePath, diff } = input;
53
+ /**
54
+ * @param {PatchFileInput} input
55
+ * @returns {Promise<string | Error>}
56
+ */
57
+ impl: async (input) =>
58
+ await noThrow(async () => {
59
+ const { filePath, diff } = input;
54
60
 
55
- const content = await fs.readFile(filePath, "utf8");
56
- const matches = Array.from(
57
- diff.matchAll(
58
- /<<<<<<< SEARCH\n(.*?)\n?=======\n(.*?)\n?>>>>>>> REPLACE/gs,
59
- ),
60
- );
61
- if (matches.length === 0) {
62
- throw new Error("No matches found in diff.");
63
- }
64
- let newContent = content;
65
- for (const match of matches) {
66
- const [_, search, replace] = match;
67
- if (!newContent.includes(search)) {
68
- throw new Error(
69
- JSON.stringify(`Search content not found: ${search}`),
70
- );
61
+ const content = await fs.readFile(filePath, "utf8");
62
+ const matches = Array.from(
63
+ diff.matchAll(
64
+ new RegExp(
65
+ `<<<<<<< SEARCH ${nonce}\\n(.*?)\\n======= ${nonce}\\n(.*?)\\n?>>>>>>> REPLACE ${nonce}`,
66
+ "gs",
67
+ ),
68
+ ),
69
+ );
70
+ if (matches.length === 0) {
71
+ throw new Error("No matches found in diff.");
71
72
  }
72
- // Escape $ characters in replacement string to prevent interpretation of $& $1 $$ patterns
73
- const escapedReplace = replace.replace(/\$/g, "$$$$");
74
- if (replace === "" && newContent.includes(`${search}\n`)) {
75
- newContent = newContent.replace(`${search}\n`, "");
76
- } else if (replace === "" && newContent.includes(`\n${search}`)) {
77
- newContent = newContent.replace(`\n${search}`, "");
78
- } else {
79
- newContent = newContent.replace(search, escapedReplace);
73
+ let newContent = content;
74
+ for (const match of matches) {
75
+ const [_, search, replace] = match;
76
+ if (!newContent.includes(search)) {
77
+ throw new Error(
78
+ JSON.stringify(`Search content not found: ${search}`),
79
+ );
80
+ }
81
+ // Escape $ characters in replacement string to prevent interpretation of $& $1 $$ patterns
82
+ const escapedReplace = replace.replace(/\$/g, "$$$$");
83
+ if (replace === "" && newContent.includes(`${search}\n`)) {
84
+ newContent = newContent.replace(`${search}\n`, "");
85
+ } else if (replace === "" && newContent.includes(`\n${search}`)) {
86
+ newContent = newContent.replace(`\n${search}`, "");
87
+ } else {
88
+ newContent = newContent.replace(search, escapedReplace);
89
+ }
80
90
  }
81
- }
82
- await fs.writeFile(filePath, newContent);
83
- return `Patched file: ${filePath}`;
84
- }),
91
+ await fs.writeFile(filePath, newContent);
92
+ return `Patched file: ${filePath}`;
93
+ }),
85
94
 
86
- /**
87
- * @param {Record<string, unknown>} input
88
- * @returns {Record<string, unknown>}
89
- */
90
- maskApprovalInput: (input) => {
91
- const patchFileInput = /** @type {PatchFileInput} */ (input);
92
- return {
93
- filePath: patchFileInput.filePath,
94
- };
95
- },
96
- };
95
+ /**
96
+ * @param {Record<string, unknown>} input
97
+ * @returns {Record<string, unknown>}
98
+ */
99
+ maskApprovalInput: (input) => {
100
+ const patchFileInput = /** @type {PatchFileInput} */ (input);
101
+ return {
102
+ filePath: patchFileInput.filePath,
103
+ };
104
+ },
105
+ };
106
+ }