@iinm/plain-agent 1.9.0 → 1.9.2
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 +59 -19
- package/config/config.predefined.json +24 -0
- package/package.json +1 -1
- package/src/cliCommands.mjs +5 -2
- package/src/cliCompleter.mjs +2 -1
- package/src/prompt.mjs +7 -16
- package/src/utils/parseFrontmatter.mjs +65 -2
- package/src/utils/toOneLine.mjs +11 -0
package/README.md
CHANGED
|
@@ -1,26 +1,68 @@
|
|
|
1
|
-
<p align="center">
|
|
2
|
-
<img src="https://pub-0bb49aa929f242d49c89ed8c297932b5.r2.dev/plain-agent/plain-agent-logo.png" alt="plain-agent logo" width="320">
|
|
3
|
-
</p>
|
|
4
|
-
|
|
5
1
|
# Plain Agent
|
|
6
2
|
|
|
7
|
-
A lightweight
|
|
3
|
+
A lightweight, capable coding agent for the terminal.
|
|
8
4
|
|
|
9
5
|
- **Multi-provider** — Use Claude, GPT, Gemini, or any OpenAI-compatible model.
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
-
|
|
17
|
-
|
|
18
|
-
(
|
|
6
|
+
<details>
|
|
7
|
+
<summary>Predefined models</summary>
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
# Model+Variant (Platform+Variant)
|
|
11
|
+
claude-haiku-4-5+thinking-16k (platform: anthropic+default)
|
|
12
|
+
claude-haiku-4-5+thinking-32k (platform: anthropic+default)
|
|
13
|
+
claude-sonnet-4-6+thinking-high (platform: anthropic+default)
|
|
14
|
+
claude-sonnet-4-6+thinking-max (platform: anthropic+default)
|
|
15
|
+
claude-opus-4-7+thinking-high (platform: anthropic+default)
|
|
16
|
+
claude-opus-4-7+thinking-max (platform: anthropic+default)
|
|
17
|
+
claude-haiku-4-5+thinking-16k-bedrock (platform: bedrock+default)
|
|
18
|
+
claude-haiku-4-5+thinking-32k-bedrock (platform: bedrock+default)
|
|
19
|
+
claude-sonnet-4-6+thinking-high-bedrock (platform: bedrock+default)
|
|
20
|
+
claude-sonnet-4-6+thinking-max-bedrock (platform: bedrock+default)
|
|
21
|
+
claude-opus-4-7+thinking-high-bedrock (platform: bedrock+default)
|
|
22
|
+
claude-opus-4-7+thinking-max-bedrock (platform: bedrock+default)
|
|
23
|
+
gemini-3-flash-preview+thinking-medium (platform: gemini+default)
|
|
24
|
+
gemini-3-flash-preview+thinking-high (platform: gemini+default)
|
|
25
|
+
gemini-3.1-pro-preview+thinking-medium (platform: gemini+default)
|
|
26
|
+
gemini-3.1-pro-preview+thinking-high (platform: gemini+default)
|
|
27
|
+
gemini-3-flash-preview+thinking-medium-vertex-ai (platform: vertex-ai+default)
|
|
28
|
+
gemini-3-flash-preview+thinking-high-vertex-ai (platform: vertex-ai+default)
|
|
29
|
+
gemini-3.1-pro-preview+thinking-medium-vertex-ai (platform: vertex-ai+default)
|
|
30
|
+
gemini-3.1-pro-preview+thinking-high-vertex-ai (platform: vertex-ai+default)
|
|
31
|
+
gpt-5.4-mini+thinking-medium (platform: openai+default)
|
|
32
|
+
gpt-5.4-mini+thinking-high (platform: openai+default)
|
|
33
|
+
gpt-5.4-mini+thinking-xhigh (platform: openai+default)
|
|
34
|
+
gpt-5.5+thinking-medium (platform: openai+default)
|
|
35
|
+
gpt-5.5+thinking-high (platform: openai+default)
|
|
36
|
+
gpt-5.5+thinking-xhigh (platform: openai+default)
|
|
37
|
+
gpt-5.2-chat+thinking-medium-azure (platform: azure+openai)
|
|
38
|
+
gpt-oss-120b+fireworks (platform: openai-compatible+fireworks)
|
|
39
|
+
glm-5+vertex-ai (platform: vertex-ai+default)
|
|
40
|
+
glm-5.1+fireworks (platform: openai-compatible+fireworks)
|
|
41
|
+
glm-5.1+novita (platform: openai-compatible+novita)
|
|
42
|
+
kimi-k2.6+fireworks (platform: openai-compatible+fireworks)
|
|
43
|
+
kimi-k2.6+novita (platform: openai-compatible+novita)
|
|
44
|
+
deepseek-v4-pro+novita (platform: openai-compatible+novita)
|
|
45
|
+
deepseek-v4-pro+fireworks (platform: openai-compatible+fireworks)
|
|
46
|
+
minimax-m2.7+fireworks (platform: openai-compatible+fireworks)
|
|
47
|
+
minimax-m2.7+novita (platform: openai-compatible+novita)
|
|
48
|
+
qwen3.6-plus+fireworks (platform: openai-compatible+fireworks)
|
|
49
|
+
qwen3.6-27b+novita (platform: openai-compatible+novita)
|
|
50
|
+
nova-2-lite+bedrock (platform: bedrock+default)
|
|
51
|
+
claude-haiku-4-5+thinking-16k-bedrock-converse (platform: bedrock+default)
|
|
52
|
+
```
|
|
53
|
+
</details>
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
- **Approval rules & path validation** — Auto-approve tool uses by name and arguments using regex patterns ([config.predefined.json#autoApproval](https://github.com/iinm/plain-agent/blob/main/config/config.predefined.json)); restrict file access to the working directory — git-ignored and untracked files require explicit approval ([src/toolInputValidator.mjs](https://github.com/iinm/plain-agent/blob/main/src/toolInputValidator.mjs)).
|
|
57
|
+
- **Sandboxed execution** — Run agent commands in a Docker container with a read-only project root and no network; writable mode and allowlisted network destinations can be enabled as needed.
|
|
58
|
+
- **Plain-text memory** — Task state is saved as Markdown files under `.plain-agent/memory/` for easy review.
|
|
19
59
|
- **Extensible** — Define prompts and subagents in Markdown. Connect MCP servers.
|
|
20
60
|
Supports Claude Code plugins and `.claude/` commands, subagents, and skills.
|
|
21
61
|
|
|
22
62
|
## Limitations
|
|
23
63
|
|
|
64
|
+
- **Path validation only covers tool arguments** — Path validation restricts only paths explicitly passed as tool-use arguments; it cannot control file access inside arbitrary scripts. Always use sandboxed execution when allowing arbitrary script execution.
|
|
65
|
+
- **No session persistence** — Sessions are not persisted. Start a fresh session and use memory files (`.plain-agent/memory/`) instead.
|
|
24
66
|
- **Sequential subagent execution** — Subagents run one at a time rather than
|
|
25
67
|
in parallel. The trade-off is full visibility: every step is streamed to
|
|
26
68
|
your terminal so you can follow exactly what each subagent is doing.
|
|
@@ -154,7 +196,7 @@ Create the configuration.
|
|
|
154
196
|
```
|
|
155
197
|
|
|
156
198
|
```js
|
|
157
|
-
//
|
|
199
|
+
// Ollama example with a custom model
|
|
158
200
|
{
|
|
159
201
|
"platforms": [
|
|
160
202
|
{
|
|
@@ -181,7 +223,6 @@ Create the configuration.
|
|
|
181
223
|
}
|
|
182
224
|
]
|
|
183
225
|
}
|
|
184
|
-
|
|
185
226
|
```
|
|
186
227
|
</details>
|
|
187
228
|
|
|
@@ -324,9 +365,7 @@ Files are loaded in the following order. Settings in later files override earlie
|
|
|
324
365
|
├── (4) config.local.json # Project-specific local configuration (including secrets)
|
|
325
366
|
├── prompts/ # Project-specific prompts
|
|
326
367
|
├── agents/ # Project-specific agent roles
|
|
327
|
-
|
|
328
|
-
├── sandbox/ # Sandbox runner scripts
|
|
329
|
-
└── tmp/ # Agent scratch space
|
|
368
|
+
└── sandbox/ # Sandbox runner scripts
|
|
330
369
|
```
|
|
331
370
|
|
|
332
371
|
### Example
|
|
@@ -495,6 +534,7 @@ Files are loaded in the following order. Settings in later files override earlie
|
|
|
495
534
|
|
|
496
535
|
The agent can use the following tools to assist with tasks:
|
|
497
536
|
|
|
537
|
+
- **read_file**: Read a file with line numbers (1-indexed). Supports `offset` and `limit` to read a specific range.
|
|
498
538
|
- **write_file**: Write a file.
|
|
499
539
|
- **patch_file**: Patch a file.
|
|
500
540
|
- **exec_command**: Run a command without shell interpretation.
|
|
@@ -914,6 +914,30 @@
|
|
|
914
914
|
}
|
|
915
915
|
},
|
|
916
916
|
|
|
917
|
+
{
|
|
918
|
+
"name": "gpt-oss-120b",
|
|
919
|
+
"variant": "fireworks",
|
|
920
|
+
"platform": {
|
|
921
|
+
"name": "openai-compatible",
|
|
922
|
+
"variant": "fireworks"
|
|
923
|
+
},
|
|
924
|
+
"model": {
|
|
925
|
+
"format": "openai-messages",
|
|
926
|
+
"config": {
|
|
927
|
+
"model": "accounts/fireworks/models/gpt-oss-120b"
|
|
928
|
+
}
|
|
929
|
+
},
|
|
930
|
+
"cost": {
|
|
931
|
+
"currency": "USD",
|
|
932
|
+
"unit": "1M",
|
|
933
|
+
"costs": {
|
|
934
|
+
"prompt_tokens": 0.15,
|
|
935
|
+
"prompt_tokens_details.cached_tokens": -0.14,
|
|
936
|
+
"completion_tokens": 0.6
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
},
|
|
940
|
+
|
|
917
941
|
{
|
|
918
942
|
"name": "glm-5",
|
|
919
943
|
"variant": "vertex-ai",
|
package/package.json
CHANGED
package/src/cliCommands.mjs
CHANGED
|
@@ -12,6 +12,7 @@ import { loadUserMessageContext } from "./context/loadUserMessageContext.mjs";
|
|
|
12
12
|
import { CLAUDE_CODE_COMPATIBILITY_NOTES } from "./prompt.mjs";
|
|
13
13
|
import { parseFileRange } from "./utils/parseFileRange.mjs";
|
|
14
14
|
import { readFileRange } from "./utils/readFileRange.mjs";
|
|
15
|
+
import { toOneLine } from "./utils/toOneLine.mjs";
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* @typedef {"prompt" | "continue"} CommandResult
|
|
@@ -172,7 +173,8 @@ export function createCommandHandler({
|
|
|
172
173
|
} else {
|
|
173
174
|
for (const role of agentRoles.values()) {
|
|
174
175
|
const maxLength = process.stdout.columns ?? 100;
|
|
175
|
-
const
|
|
176
|
+
const desc = toOneLine(role.description);
|
|
177
|
+
const line = ` ${styleText("cyan", role.id.padEnd(20))} - ${desc}`;
|
|
176
178
|
console.log(
|
|
177
179
|
line.length > maxLength ? `${line.slice(0, maxLength)}...` : line,
|
|
178
180
|
);
|
|
@@ -201,7 +203,8 @@ export function createCommandHandler({
|
|
|
201
203
|
} else {
|
|
202
204
|
for (const prompt of prompts.values()) {
|
|
203
205
|
const maxLength = process.stdout.columns ?? 100;
|
|
204
|
-
const
|
|
206
|
+
const desc = toOneLine(prompt.description);
|
|
207
|
+
const line = ` ${styleText("cyan", prompt.id.padEnd(20))} - ${desc}`;
|
|
205
208
|
console.log(
|
|
206
209
|
line.length > maxLength ? `${line.slice(0, maxLength)}...` : line,
|
|
207
210
|
);
|
package/src/cliCompleter.mjs
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { styleText } from "node:util";
|
|
6
6
|
import { loadAgentRoles } from "./context/loadAgentRoles.mjs";
|
|
7
7
|
import { loadPrompts } from "./context/loadPrompts.mjs";
|
|
8
|
+
import { toOneLine } from "./utils/toOneLine.mjs";
|
|
8
9
|
|
|
9
10
|
// Define available slash commands for tab completion
|
|
10
11
|
export const SLASH_COMMANDS = [
|
|
@@ -129,7 +130,7 @@ function showCompletions(rl, candidates, line, callback) {
|
|
|
129
130
|
if (typeof c === "string") return c;
|
|
130
131
|
const nameText = c.name.padEnd(25);
|
|
131
132
|
const separator = " - ";
|
|
132
|
-
const descText = c.description;
|
|
133
|
+
const descText = toOneLine(c.description);
|
|
133
134
|
|
|
134
135
|
// 画面幅に合わせて説明文をカット(色を付ける前に計算)
|
|
135
136
|
const availableWidth =
|
package/src/prompt.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { toOneLine } from "./utils/toOneLine.mjs";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* @typedef {object} PromptConfig
|
|
3
5
|
* @property {string} username
|
|
@@ -28,37 +30,27 @@ export function createPrompt({
|
|
|
28
30
|
}) {
|
|
29
31
|
const agentRoleDescriptions = Array.from(agentRoles.entries())
|
|
30
32
|
.map(([id, role]) => {
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
? `${role.description.substring(0, 100)}...`
|
|
34
|
-
: role.description;
|
|
33
|
+
const flat = toOneLine(role.description);
|
|
34
|
+
const desc = flat.length > 100 ? `${flat.substring(0, 100)}...` : flat;
|
|
35
35
|
return `- ${id}: ${desc}`;
|
|
36
36
|
})
|
|
37
37
|
.join("\n");
|
|
38
38
|
|
|
39
39
|
const skillDescriptions = skills
|
|
40
40
|
.map((skill) => {
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
? `${skill.description.substring(0, 100)}...`
|
|
44
|
-
: skill.description;
|
|
41
|
+
const flat = toOneLine(skill.description);
|
|
42
|
+
const desc = flat.length > 100 ? `${flat.substring(0, 100)}...` : flat;
|
|
45
43
|
return `- ${skill.filePath}\n ${desc}`;
|
|
46
44
|
})
|
|
47
45
|
.join("\n");
|
|
48
46
|
|
|
49
47
|
return `
|
|
50
|
-
# Communication Style
|
|
51
|
-
|
|
52
|
-
- Respond in the user's language.
|
|
53
|
-
- Address the user by their name, rather than "user".
|
|
54
|
-
- Use emojis sparingly to highlight key points.
|
|
55
|
-
|
|
56
48
|
# Memory Files
|
|
57
49
|
|
|
58
50
|
- Create/Update memory files after creating/updating a plan, completing milestones, encountering issues, or making decisions.
|
|
59
51
|
- Update existing task memory when continuing the same task.
|
|
60
|
-
- Write the memory content in the user's language.
|
|
61
52
|
- Ensure self-containment: The file must be standalone. A reader should fully understand the task context, logic and progress without any other references.
|
|
53
|
+
- Write the memory content in the user's language.
|
|
62
54
|
|
|
63
55
|
Memory files should include:
|
|
64
56
|
- Task overview: What the task is, why it's being done, requirements and constraints
|
|
@@ -88,7 +80,6 @@ Examples:
|
|
|
88
80
|
|
|
89
81
|
- Only use when the user explicitly requests it.
|
|
90
82
|
- Create a new session with the given tmux session id.
|
|
91
|
-
- Use relative paths.
|
|
92
83
|
|
|
93
84
|
Examples:
|
|
94
85
|
- Start session: new-session ["-d", "-s", "<tmux-session-id>"]
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Parse simple key-value frontmatter using regex.
|
|
3
|
-
*
|
|
3
|
+
* Supports `key: value` and YAML block scalars (`key: |` literal,
|
|
4
|
+
* `key: >` folded), with optional chomping indicators (`-`, `+`).
|
|
5
|
+
* Block scalar lines are read while indented further than column 0,
|
|
6
|
+
* using the first non-empty block line's indentation as the base.
|
|
4
7
|
* @param {string} frontmatter - The YAML frontmatter content (without --- delimiters)
|
|
5
8
|
* @returns {Record<string, string>} Parsed key-value pairs
|
|
6
9
|
*/
|
|
@@ -8,12 +11,72 @@ export function parseFrontmatter(frontmatter) {
|
|
|
8
11
|
/** @type {Record<string, string>} */
|
|
9
12
|
const result = {};
|
|
10
13
|
|
|
11
|
-
|
|
14
|
+
const lines = frontmatter.split(/\r?\n/);
|
|
15
|
+
let i = 0;
|
|
16
|
+
while (i < lines.length) {
|
|
17
|
+
const line = lines[i];
|
|
18
|
+
|
|
19
|
+
const blockMatch = line.match(/^(\w[\w-]*):\s*([|>])[+-]?\s*$/);
|
|
20
|
+
if (blockMatch) {
|
|
21
|
+
const key = blockMatch[1];
|
|
22
|
+
const style = blockMatch[2];
|
|
23
|
+
i++;
|
|
24
|
+
|
|
25
|
+
/** @type {string[]} */
|
|
26
|
+
const blockLines = [];
|
|
27
|
+
let indent = -1;
|
|
28
|
+
while (i < lines.length) {
|
|
29
|
+
const blockLine = lines[i];
|
|
30
|
+
if (blockLine.trim() === "") {
|
|
31
|
+
blockLines.push("");
|
|
32
|
+
i++;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const leadingSpaces = (blockLine.match(/^( *)/)?.[1] ?? "").length;
|
|
36
|
+
if (indent === -1) {
|
|
37
|
+
if (leadingSpaces === 0) break;
|
|
38
|
+
indent = leadingSpaces;
|
|
39
|
+
} else if (leadingSpaces < indent) {
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
blockLines.push(blockLine.slice(indent));
|
|
43
|
+
i++;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
while (blockLines.at(-1) === "") {
|
|
47
|
+
blockLines.pop();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
result[key] =
|
|
51
|
+
style === "|" ? blockLines.join("\n") : foldFolded(blockLines);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
12
55
|
const match = line.match(/^(\w[\w-]*):\s?(.*)$/);
|
|
13
56
|
if (match) {
|
|
14
57
|
result[match[1]] = match[2].trimEnd();
|
|
15
58
|
}
|
|
59
|
+
i++;
|
|
16
60
|
}
|
|
17
61
|
|
|
18
62
|
return result;
|
|
19
63
|
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @param {string[]} blockLines
|
|
67
|
+
* @returns {string}
|
|
68
|
+
*/
|
|
69
|
+
function foldFolded(blockLines) {
|
|
70
|
+
const paragraphs = [];
|
|
71
|
+
let current = [];
|
|
72
|
+
for (const line of blockLines) {
|
|
73
|
+
if (line === "") {
|
|
74
|
+
paragraphs.push(current.join(" "));
|
|
75
|
+
current = [];
|
|
76
|
+
} else {
|
|
77
|
+
current.push(line);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
paragraphs.push(current.join(" "));
|
|
81
|
+
return paragraphs.join("\n");
|
|
82
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collapse newlines (and any whitespace adjacent to them) into a single space
|
|
3
|
+
* and trim. Used for rendering frontmatter values such as `description` in
|
|
4
|
+
* single-line UI contexts (bullet lists, completer rows, console output).
|
|
5
|
+
*
|
|
6
|
+
* @param {string} s
|
|
7
|
+
* @returns {string}
|
|
8
|
+
*/
|
|
9
|
+
export function toOneLine(s) {
|
|
10
|
+
return s.replace(/\s*\n\s*/g, " ").trim();
|
|
11
|
+
}
|