@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 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 CLI-based coding agent.
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
- - **Compact system prompt** — Under 3.5 KB, keeping per-request overhead and cost low ([src/prompt.mjs](https://github.com/iinm/plain-agent/blob/main/src/prompt.mjs)).
11
- - **Fine-grained approval rules** — Auto-approve commands by name, arguments,
12
- and file paths using regex patterns
13
- ([config.predefined.json#autoApproval](https://github.com/iinm/plain-agent/blob/main/config/config.predefined.json)).
14
- - **Path validation** — Restricts access to the working directory.
15
- Git-ignored and untracked files require explicit approval.
16
- - **Sandboxed execution** — Run the agent's shell commands inside a Docker
17
- container with network access restricted to allowlisted destinations
18
- (e.g., `registry.npmjs.org` only for `npm install`).
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
- // OpenAI-compatible provider (Ollama) example with a custom model
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
- ├── memory/ # Task-specific memory files
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iinm/plain-agent",
3
- "version": "1.9.0",
3
+ "version": "1.9.2",
4
4
  "description": "A lightweight CLI-based coding agent",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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 line = ` ${styleText("cyan", role.id.padEnd(20))} - ${role.description}`;
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 line = ` ${styleText("cyan", prompt.id.padEnd(20))} - ${prompt.description}`;
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
  );
@@ -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 desc =
32
- role.description.length > 100
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 desc =
42
- skill.description.length > 100
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
- * Only supports `key: value` format. No multiline strings.
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
- for (const line of frontmatter.split(/\r?\n/)) {
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
+ }