@iinm/plain-agent 1.7.5 → 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/README.md +1 -6
- package/config/config.predefined.json +70 -1
- package/package.json +1 -1
- package/src/cliFormatter.mjs +26 -1
- package/src/main.mjs +2 -2
- package/src/tools/askURL.mjs +11 -3
- package/src/tools/askWeb.mjs +8 -0
- package/src/tools/patchFile.mjs +80 -70
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ A lightweight CLI-based coding agent.
|
|
|
14
14
|
|
|
15
15
|
## Safety Controls
|
|
16
16
|
|
|
17
|
-
**Auto-Approval**: Tools with no side effects and no sensitive data access are automatically approved based on patterns defined in [`config.predefined.json#autoApproval`](https://github.com/iinm/plain-agent/blob/main
|
|
17
|
+
**Auto-Approval**: Tools with no side effects and no sensitive data access are automatically approved based on patterns defined in [`config.predefined.json#autoApproval`](https://github.com/iinm/plain-agent/blob/main/config/config.predefined.json).
|
|
18
18
|
|
|
19
19
|
**Path Validation**: All file paths in tool inputs are validated to remain within the working directory and under git control.
|
|
20
20
|
|
|
@@ -146,11 +146,6 @@ Create the configuration.
|
|
|
146
146
|
"baseURL": "https://router.huggingface.co",
|
|
147
147
|
"apiKey": "FIXME"
|
|
148
148
|
},
|
|
149
|
-
{
|
|
150
|
-
"name": "openai-compatible",
|
|
151
|
-
"variant": "xai",
|
|
152
|
-
"apiKey": "FIXME"
|
|
153
|
-
},
|
|
154
149
|
{
|
|
155
150
|
"name": "openai-compatible",
|
|
156
151
|
"variant": "fireworks",
|
|
@@ -1038,6 +1038,29 @@
|
|
|
1038
1038
|
}
|
|
1039
1039
|
}
|
|
1040
1040
|
},
|
|
1041
|
+
{
|
|
1042
|
+
"name": "kimi-k2.5",
|
|
1043
|
+
"variant": "fireworks",
|
|
1044
|
+
"platform": {
|
|
1045
|
+
"name": "openai-compatible",
|
|
1046
|
+
"variant": "fireworks"
|
|
1047
|
+
},
|
|
1048
|
+
"model": {
|
|
1049
|
+
"format": "openai-messages",
|
|
1050
|
+
"config": {
|
|
1051
|
+
"model": "accounts/fireworks/models/kimi-k2p5"
|
|
1052
|
+
}
|
|
1053
|
+
},
|
|
1054
|
+
"cost": {
|
|
1055
|
+
"currency": "USD",
|
|
1056
|
+
"unit": "1M",
|
|
1057
|
+
"costs": {
|
|
1058
|
+
"prompt_tokens": 0.6,
|
|
1059
|
+
"prompt_tokens_details.cached_tokens": -0.5,
|
|
1060
|
+
"completion_tokens": 3
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
},
|
|
1041
1064
|
|
|
1042
1065
|
{
|
|
1043
1066
|
"name": "deepseek-v3.2",
|
|
@@ -1069,7 +1092,29 @@
|
|
|
1069
1092
|
}
|
|
1070
1093
|
}
|
|
1071
1094
|
},
|
|
1072
|
-
|
|
1095
|
+
{
|
|
1096
|
+
"name": "deepseek-v3.2",
|
|
1097
|
+
"variant": "fireworks",
|
|
1098
|
+
"platform": {
|
|
1099
|
+
"name": "openai-compatible",
|
|
1100
|
+
"variant": "fireworks"
|
|
1101
|
+
},
|
|
1102
|
+
"model": {
|
|
1103
|
+
"format": "openai-messages",
|
|
1104
|
+
"config": {
|
|
1105
|
+
"model": "accounts/fireworks/models/deepseek-v3p2"
|
|
1106
|
+
}
|
|
1107
|
+
},
|
|
1108
|
+
"cost": {
|
|
1109
|
+
"currency": "USD",
|
|
1110
|
+
"unit": "1M",
|
|
1111
|
+
"costs": {
|
|
1112
|
+
"prompt_tokens": 0.56,
|
|
1113
|
+
"prompt_tokens_details.cached_tokens": -0.28,
|
|
1114
|
+
"completion_tokens": 1.68
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
},
|
|
1073
1118
|
{
|
|
1074
1119
|
"name": "minimax-m2.1",
|
|
1075
1120
|
"variant": "bedrock",
|
|
@@ -1085,6 +1130,30 @@
|
|
|
1085
1130
|
}
|
|
1086
1131
|
}
|
|
1087
1132
|
},
|
|
1133
|
+
{
|
|
1134
|
+
"name": "minimax-m2.5",
|
|
1135
|
+
"variant": "fireworks",
|
|
1136
|
+
"platform": {
|
|
1137
|
+
"name": "openai-compatible",
|
|
1138
|
+
"variant": "fireworks"
|
|
1139
|
+
},
|
|
1140
|
+
"model": {
|
|
1141
|
+
"format": "openai-messages",
|
|
1142
|
+
"config": {
|
|
1143
|
+
"model": "accounts/fireworks/models/minimax-m2p5"
|
|
1144
|
+
}
|
|
1145
|
+
},
|
|
1146
|
+
"cost": {
|
|
1147
|
+
"currency": "USD",
|
|
1148
|
+
"unit": "1M",
|
|
1149
|
+
"costs": {
|
|
1150
|
+
"prompt_tokens": 0.3,
|
|
1151
|
+
"prompt_tokens_details.cached_tokens": -0.27,
|
|
1152
|
+
"completion_tokens": 1.2
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
},
|
|
1156
|
+
|
|
1088
1157
|
{
|
|
1089
1158
|
"name": "minimax-m2.7",
|
|
1090
1159
|
"variant": "ollama",
|
package/package.json
CHANGED
package/src/cliFormatter.mjs
CHANGED
|
@@ -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
|
|
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 {
|
|
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
|
-
|
|
165
|
+
createPatchFileTool(),
|
|
166
166
|
createTmuxCommandTool({ sandbox: appConfig.sandbox }),
|
|
167
167
|
createDelegateToSubagentTool(),
|
|
168
168
|
createReportAsSubagentTool(),
|
package/src/tools/askURL.mjs
CHANGED
|
@@ -25,7 +25,7 @@ import { noThrow } from "../utils/noThrow.mjs";
|
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
|
-
* @typedef {Object}
|
|
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 {
|
|
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 {
|
|
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
|
}
|
package/src/tools/askWeb.mjs
CHANGED
|
@@ -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
|
}
|
package/src/tools/patchFile.mjs
CHANGED
|
@@ -6,91 +6,101 @@
|
|
|
6
6
|
import fs from "node:fs/promises";
|
|
7
7
|
import { noThrow } from "../utils/noThrow.mjs";
|
|
8
8
|
|
|
9
|
-
/**
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
46
|
+
type: "string",
|
|
47
|
+
},
|
|
41
48
|
},
|
|
49
|
+
required: ["filePath", "diff"],
|
|
42
50
|
},
|
|
43
|
-
required: ["filePath", "diff"],
|
|
44
51
|
},
|
|
45
|
-
},
|
|
46
52
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
}),
|
|
91
|
+
await fs.writeFile(filePath, newContent);
|
|
92
|
+
return `Patched file: ${filePath}`;
|
|
93
|
+
}),
|
|
85
94
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
+
}
|