@iinm/plain-agent 1.10.22 → 1.10.24

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,17 +1,129 @@
1
1
  # Plain Agent
2
2
 
3
- A lightweight coding agent for the terminal.
3
+ A lightweight terminal-based coding agent focused on safety and low token cost
4
4
 
5
- - **Multi-provider** — Use Claude, GPT, Gemini, or any OpenAI-compatible model via direct APIs or through Bedrock, Vertex AI, or Azure.
6
- - **Fine-grained auto-approval** — Auto-approve tool calls by matching tool names and inputs against configurable patterns, while validating string inputs as paths for safety.
7
- - **Sandboxed execution** — Run commands in a Docker container with filesystem and network isolation.
8
- - **Supports Claude Code resources** — Use Claude Code plugins, commands, subagents, and skills from `.claude/`.
9
- - **Zero external dependencies** — Built using only Node.js standard libraries.
5
+ ## Design
10
6
 
11
- ## Limitations
7
+ ### Multi-provider support
12
8
 
13
- - **Path validation only covers tool arguments** — It blocks paths outside the working directory, directory traversal (`..`), symlinks that escape the project, and git-ignored files. However, it only applies to paths explicitly passed as tool-use arguments, so it cannot control file access inside arbitrary scripts. Always use sandboxed execution when running arbitrary scripts.
14
- - **Sequential subagent execution** — Subagents run one at a time rather than in parallel. The trade-off is that every step is streamed to your terminal, so you can follow exactly what each subagent is doing.
9
+ Supports Claude, OpenAI, Gemini, and any OpenAI-compatible provider. Bedrock, Vertex AI, and Azure are also supported for teams working in environments restricted to managed cloud providers.
10
+
11
+ ### Auto-approval
12
+
13
+ Configure what the agent can do automatically using a small DSL with regex matching. Below is an excerpt from the [default config](https://github.com/iinm/plain-agent/blob/main/config/config.predefined.json).
14
+
15
+ ```js
16
+ {
17
+ "autoApproval": {
18
+ // What to do when no pattern matches: ask - prompt the user, deny - reject automatically
19
+ "defaultAction": "ask",
20
+
21
+ // Patterns are evaluated top-to-bottom; first match wins
22
+ "patterns": [
23
+ // fd example:
24
+ // Ask for approval when risky flags like --exec or --no-ignore are present
25
+ {
26
+ "toolName": "exec_command",
27
+ "input": {
28
+ "command": "fd",
29
+ "args": { "$has": { "$regex": "^(--unrestricted|--no-ignore|--exec|--exec-batch|--follow|-[^-]*[uIxXL])" } }
30
+ },
31
+ "action": "ask"
32
+ },
33
+ // Allow all other fd calls
34
+ {
35
+ "toolName": "exec_command",
36
+ "input": { "command": "fd" },
37
+ "action": "allow"
38
+ },
39
+
40
+ // GitHub CLI example:
41
+ // Allow read access to PRs and issues
42
+ {
43
+ "toolName": "exec_command",
44
+ "input": {
45
+ "command": "gh",
46
+ "args": ["api", "--method", "GET", { "$regex": "^repos/[^/]+/[^/]+/(pulls|issues)/" }]
47
+ },
48
+ "action": "allow"
49
+ },
50
+ // Require --method to be explicit, so GET calls can be safely auto-approved
51
+ {
52
+ "toolName": "exec_command",
53
+ "input": { "command": "gh", "args": ["api", "--method"] },
54
+ "action": "ask"
55
+ },
56
+ {
57
+ "toolName": "exec_command",
58
+ "input": { "command": "gh", "args": ["api"] },
59
+ "action": "deny",
60
+ "reason": "--method must be specified"
61
+ }
62
+ ]
63
+ }
64
+ }
65
+ ```
66
+
67
+ ### Path Validation
68
+
69
+ String values in tool inputs are treated as file paths and validated against these rules. This takes precedence over `autoApproval` — even if a pattern marks an action as `allow`, a validation failure falls back to `defaultAction`.
70
+
71
+ - The path must be under the working directory or a path listed in `autoApproval.allowedPaths`
72
+ - No directory traversal (`..` is not allowed)
73
+ - The file must be tracked by Git (not ignored)
74
+
75
+ **Note**: validation only applies when the agent explicitly passes file paths to tools. It cannot catch file access inside scripts the agent writes — something like `bash -c "rm -rf /"` is beyond its reach. Always use a sandbox when allowing arbitrary script execution.
76
+
77
+ ### Sandbox
78
+
79
+ The agent can run arbitrary commands via `exec_command` and `tmux_command`. You can configure a wrapper command that intercepts both.
80
+ A Docker-based wrapper called `plain-sandbox` is included, but the interface is designed to work with other tools as well, such as [Anthropic Sandbox Runtime (srt)](https://github.com/anthropic-experimental/sandbox-runtime).
81
+
82
+ ```js
83
+ {
84
+ // Sandbox environment for the exec_command and tmux_command tools
85
+ "sandbox": {
86
+ // Commands are wrapped and executed with this command
87
+ "command": "plain-sandbox",
88
+ "args": ["--allow-write", "--skip-build", "--keep-alive", "30"],
89
+ // separator is inserted between sandbox flags and the user command to prevent bypasses
90
+ "separator": "--",
91
+
92
+ "rules": [
93
+ // Run specific commands outside the sandbox
94
+ {
95
+ "pattern": {
96
+ "command": { "$regex": "^(gh|docker)$" }
97
+ },
98
+ "mode": "unsandboxed"
99
+ },
100
+ // Run commands in the sandbox with network access
101
+ {
102
+ "pattern": {
103
+ "command": "npm",
104
+ "args": [{ "$regex": "^(install|ci)$" }]
105
+ },
106
+ "mode": "sandbox",
107
+ "additionalArgs": ["--allow-net", "registry.npmjs.org"]
108
+ }
109
+ ]
110
+ }
111
+ }
112
+ ```
113
+
114
+ ### Token Efficiency
115
+
116
+ A few design choices keep token usage low:
117
+
118
+ - Minimal system prompt — the [system prompt](https://github.com/iinm/plain-agent/blob/main/src/prompt.mjs) contains only what the agent needs to function.
119
+ - Output truncation — when a command or MCP tool produces large output, it is truncated and saved to a file. The agent can then read only the relevant parts.
120
+ - (Experimental) [Hashline-based](https://blog.can.ac/2026/02/12/the-harness-problem/) patch_file tool.
121
+
122
+ ### Claude Code Compatibility
123
+
124
+ Claude Code has a plugin ecosystem and is widely used across teams. plain-agent supports `.claude/` commands, subagents, and skills so you can share project skills with Claude Code users. Plugins can also be installed.
125
+
126
+ **Limitation:** Subagents run sequentially, not in parallel. The upside is that their activity is fully observable and they don't spike token usage. They also inherit the main context rather than starting fresh — a simplification chosen for ease of implementation, which also avoids redundant file reads and reduces the chance of losing context between handoffs.
15
127
 
16
128
  ## Requirements
17
129
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@iinm/plain-agent",
3
- "version": "1.10.22",
4
- "description": "A lightweight coding agent for the terminal.",
3
+ "version": "1.10.24",
4
+ "description": "A lightweight terminal-based coding agent focused on safety and low token cost",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "repository": {
@@ -18,8 +18,8 @@ Usage: $SCRIPT_NAME [--dockerfile FILE]
18
18
  [--env-file FILE]
19
19
  [--allow-write] [--allow-net [DESTINATIONS|--]]
20
20
  [--volume [NAME:]PATH]
21
- [--mount-writable HOST_DIR:CONTAINER_DIR]
22
- [--mount-readonly HOST_DIR:CONTAINER_DIR]
21
+ [--mount-writable HOST_PATH[:CONTAINER_PATH]]
22
+ [--mount-readonly HOST_PATH[:CONTAINER_PATH]]
23
23
  [--publish [HOST_ADDRESS:]HOST_PORT:CONTAINER_PORT]
24
24
  [--tty]
25
25
  [--no-cache]
@@ -50,11 +50,19 @@ Options:
50
50
  inside the container.
51
51
  If NAME is not given, the volume name is generated.
52
52
 
53
- --mount-readonly HOST_DIR:CONTAINER_DIR
53
+ --mount-readonly HOST_PATH[:CONTAINER_PATH]
54
54
  Mount a host file or directory to a container path as read-only.
55
+ If CONTAINER_PATH is omitted, the resolved absolute host path is used.
56
+ Relative HOST_PATH is resolved from the current working directory.
57
+ Relative CONTAINER_PATH is resolved from the working directory
58
+ inside the container.
55
59
 
56
- --mount-writable HOST_DIR:CONTAINER_DIR
60
+ --mount-writable HOST_PATH[:CONTAINER_PATH]
57
61
  Mount a host file or directory to a container path as writable.
62
+ If CONTAINER_PATH is omitted, the resolved absolute host path is used.
63
+ Relative HOST_PATH is resolved from the current working directory.
64
+ Relative CONTAINER_PATH is resolved from the working directory
65
+ inside the container.
58
66
 
59
67
  --publish [HOST_ADDRESS:]HOST_PORT:CONTAINER_PORT
60
68
  Publish container port(s) to the host.
@@ -384,12 +392,22 @@ main() {
384
392
  for mount in "${readonly_mounts[@]}"; do
385
393
  local host_path
386
394
  local container_path
387
- IFS=':' read -r host_path container_path <<< "$mount"
395
+ if grep -qE ':' <<< "$mount"; then
396
+ IFS=':' read -r host_path container_path <<< "$mount"
397
+ else
398
+ host_path="$mount"
399
+ container_path=""
400
+ fi
388
401
  local host_abs_path="$host_path"
389
402
  if ! grep -qE '^/' <<< "$host_abs_path"; then
390
403
  # shellcheck disable=SC2001
391
404
  host_abs_path=$(readlink -f "$(sed "s,~/,$HOME/," <<< "$host_abs_path")")
392
405
  fi
406
+ if test -z "$container_path"; then
407
+ container_path="$host_abs_path"
408
+ elif ! grep -qE '^/' <<< "$container_path"; then
409
+ container_path="${container_workdir}/${container_path}"
410
+ fi
393
411
  docker_run_opts+=(--mount "type=bind,source=${host_abs_path},target=${container_path},readonly,consistency=delegated")
394
412
  done
395
413
  fi
@@ -398,12 +416,22 @@ main() {
398
416
  for mount in "${writable_mounts[@]}"; do
399
417
  local host_path
400
418
  local container_path
401
- IFS=':' read -r host_path container_path <<< "$mount"
419
+ if grep -qE ':' <<< "$mount"; then
420
+ IFS=':' read -r host_path container_path <<< "$mount"
421
+ else
422
+ host_path="$mount"
423
+ container_path=""
424
+ fi
402
425
  local host_abs_path="$host_path"
403
426
  if ! grep -qE '^/' <<< "$host_abs_path"; then
404
427
  # shellcheck disable=SC2001
405
428
  host_abs_path=$(readlink -f "$(sed "s,~/,$HOME/," <<< "$host_abs_path")")
406
429
  fi
430
+ if test -z "$container_path"; then
431
+ container_path="$host_abs_path"
432
+ elif ! grep -qE '^/' <<< "$container_path"; then
433
+ container_path="${container_workdir}/${container_path}"
434
+ fi
407
435
  docker_run_opts+=(--mount "type=bind,source=${host_abs_path},target=${container_path},consistency=delegated")
408
436
  done
409
437
  fi
package/src/prompt.mjs CHANGED
@@ -49,38 +49,25 @@ Respond in the user's language.
49
49
 
50
50
  # Memory Files
51
51
 
52
- Memory files preserve task state so work can be resumed after a context reset.
52
+ Memory files preserve state to resume work after context resets.
53
53
 
54
- - Create/Update memory files when creating/updating a plan, completing milestones, encountering issues, or making decisions. Skip memory files for tasks that can be completed in a few steps.
55
- - Ensure self-containment: Write as if the reader has no prior knowledge of the conversation.
54
+ - Create/Update memory files when creating/updating a plan, completing milestones, encountering issues, or making decisions.
55
+ - Skip memory files for tasks that can be completed in a few steps.
56
56
  - Write the memory content in the user's language.
57
57
 
58
58
  Memory files should include:
59
59
  - Task overview: What the task is, why it's being done, requirements and constraints
60
- - References: AGENTS.md, skills, relevant documentation, source files, and commands
61
- - Progress tracking: Completed milestones with evidence, current status, and next steps
62
- - Decision records: Key decisions, alternatives considered, and rationale
60
+ - References: AGENTS.md, documentation, source files, commands
61
+ - Progress tracking: Completed milestones with results, current status, and next steps
62
+ - Decision records: Key decisions, alternatives considered, and reason
63
63
 
64
64
  # Tools
65
65
 
66
- Call multiple tools at once when they don't depend on each other's results.
67
-
68
- ## patch_file
69
-
70
- Always read the target lines with \`read_file\` first to verify line numbers and their 2-char hashes before calling \`patch_file\`.
71
-
72
- ## exec_command
73
-
66
+ - Run independent tools in parallel.
67
+ - Verify line numbers and hashes with read_file before calling patch_file.
74
68
  - Use relative paths for files inside the working directory, absolute paths for files outside.
75
69
  - Use ${projectMetadataDir}/tmp/ for temporary files.
76
70
 
77
- Examples:
78
- - List directories or find files: fd [".", "./", "--max-depth", "3", "--type", "d", "--hidden"]
79
- - Search for strings: rg ["--heading", "--line-number", "pattern", "./"]
80
- - Manage GitHub issues and PRs:
81
- Get PR details: gh ["pr", "view", "123", "--json", "title,body,url"]
82
- Get PR comment: gh ["api", "--method", "GET", "repos/<owner>/<repo>/pulls/comments/<id>", "--jq", "{user: .user.login, path: .path, line: .line, body: .body}"]
83
-
84
71
  # Project Rules and Skills
85
72
 
86
73
  Discover and apply project-specific rules and reusable skills.
@@ -99,12 +86,12 @@ ${skillDescriptions}
99
86
 
100
87
  # Environment
101
88
 
89
+ - Session id: ${sessionId}
90
+ - Memory file path: ${projectMetadataDir}/memory/${sessionId}--<kebab-case-title>.md
102
91
  - User name: ${username}
103
92
  - Your model name: ${modelName}
104
93
  - Current working directory: ${workingDir}
105
94
  - Today's date: ${today}
106
- - Session id: ${sessionId}
107
- - Memory file path: ${projectMetadataDir}/memory/${sessionId}--<kebab-case-title>.md
108
95
 
109
96
  Available subagents:
110
97
  ${agentRoleDescriptions}
@@ -20,7 +20,15 @@ export function createExecCommandTool(config) {
20
20
  return {
21
21
  def: {
22
22
  name: "exec_command",
23
- description: "Run a command without shell interpretation.",
23
+ description: `Run a command without shell interpretation.
24
+
25
+ Examples:
26
+ - List directories or find files: fd [".", "./", "--max-depth", "3", "--type", "d", "--hidden"]
27
+ - Search for strings: rg ["--heading", "--line-number", "pattern", "./"]
28
+ - Manage GitHub issues and PRs:
29
+ Get PR details: gh ["pr", "view", "123", "--json", "title,body,url"]
30
+ Get PR comment: gh ["api", "--method", "GET", "repos/<owner>/<repo>/pulls/comments/<id>", "--jq", "{user: .user.login, path: .path, line: .line, body: .body}"]
31
+ `.trim(),
24
32
  inputSchema: {
25
33
  type: "object",
26
34
  properties: {
@@ -29,9 +37,7 @@ export function createExecCommandTool(config) {
29
37
  type: "string",
30
38
  },
31
39
  args: {
32
- // Gemini 3 flashが command: rg, args: [rg, ...] のようにargsにコマンドを含めることがある
33
- description:
34
- "Array of arguments to pass to the command. Do not include the command name itself in this array.",
40
+ description: "Array of arguments to pass to the command.",
35
41
  type: "array",
36
42
  items: {
37
43
  type: "string",