@iinm/plain-agent 1.0.5 → 1.0.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 CHANGED
@@ -2,25 +2,24 @@
2
2
 
3
3
  A lightweight CLI-based coding agent.
4
4
 
5
- - **Safety controls** — Configure per-tool approval rules and run in a sandbox for stronger isolation
6
- - **Multi-provider** — Works with Anthropic, OpenAI, Gemini, AWS Bedrock, Azure, Vertex AI, and more
7
- - **Sequential subagent delegation** — Delegate subtasks to specialized subagents with full visibility into their actions
5
+ - **Safety controls** — Configure approval rules and sandboxing for safe execution
6
+ - **Multi-provider** — Supports Anthropic, OpenAI, Gemini, Bedrock, Azure, Vertex AI, and more
7
+ - **Sequential subagent delegation** — Delegate subtasks to specialized subagents with full visibility
8
8
  - **MCP support** — Connect to external MCP servers to extend available tools
9
- - **Claude Code compatible** *(experimental)* — Reuse Claude Code plugins, agents, commands, and skills
9
+ - **Claude Code compatible** — Reuse Claude Code plugins, agents, commands, and skills
10
10
 
11
11
  ## Safety Controls
12
12
 
13
- This CLI tool automatically allows the execution of certain tools but requires explicit approval for security-sensitive operations, such as accessing parent directories.
14
- The security rules are defined in [`config.predefined.json`](https://github.com/iinm/plain-agent/blob/main/.config/config.predefined.json) and [`toolInputValidator.mjs`](https://github.com/iinm/plain-agent/blob/main/src/toolInputValidator.mjs) within this repository.
13
+ **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).
15
14
 
16
- ⚠️ The `write_file` and `patch_file` tools block direct access to git-ignored files. However, `exec_command` can access any files in your environment.
17
- Use a sandbox for stronger isolation.
15
+ **Path Validation**: All file paths in tool inputs are validated to remain within the working directory and under git control.
16
+
17
+ ⚠️ `write_file` and `patch_file` require explicit path arguments. However, `exec_command` can run arbitrary code where file access cannot be validated. Use a sandbox for stronger isolation.
18
18
 
19
19
  ## Requirements
20
20
 
21
- - Linux or macOS
22
21
  - Node.js 22 or later
23
- - LLM provider credentials (API keys, AWS SSO, gcloud CLI, or Azure CLI)
22
+ - LLM provider credentials
24
23
  - bash / docker for sandboxed execution
25
24
  - [ripgrep](https://github.com/burntsushi/ripgrep)
26
25
  - [fd](https://github.com/sharkdp/fd)
@@ -34,8 +33,7 @@ npm install -g @iinm/plain-agent
34
33
  List available models.
35
34
 
36
35
  ```sh
37
- curl https://raw.githubusercontent.com/iinm/plain-agent/refs/heads/main/.config/config.predefined.json \
38
- | jq -r '.models[] | "\(.name)+\(.variant)"'
36
+ plain --list-models
39
37
  ```
40
38
 
41
39
  Create the configuration.
@@ -43,9 +41,10 @@ Create the configuration.
43
41
  ```js
44
42
  // ~/.config/plain-agent/config.local.json
45
43
  {
46
- // Default model
47
44
  "model": "gpt-5.4+thinking-high",
45
+ // "model": "claude-sonnet-4-6+thinking-16k",
48
46
 
47
+ // Configure the providers you want to use
49
48
  "platforms": [
50
49
  {
51
50
  "name": "anthropic",
@@ -61,13 +60,35 @@ Create the configuration.
61
60
  "name": "openai",
62
61
  "variant": "default",
63
62
  "apiKey": "FIXME"
63
+ },
64
+ {
65
+ // Requires Azure CLI to get access token
66
+ "name": "azure",
67
+ "variant": "default",
68
+ "baseURL": "https://<resource>.openai.azure.com/openai",
69
+ // Optional
70
+ "azureConfigDir": "/home/xxx/.azure-for-agent"
71
+ },
72
+ {
73
+ "name": "bedrock",
74
+ "variant": "default",
75
+ "baseURL": "https://bedrock-runtime.<region>.amazonaws.com",
76
+ "awsProfile": "FIXME"
77
+ },
78
+ {
79
+ // Requires gcloud CLI to get authentication token
80
+ "name": "vertex-ai",
81
+ "variant": "default",
82
+ "baseURL": "https://aiplatform.googleapis.com/v1beta1/projects/<project>/locations/<location>",
83
+ // Optional
84
+ "account": "<service_account_email>"
64
85
  }
65
86
  ],
66
87
 
67
88
  // Optional
68
89
  "tools": {
69
90
  "askGoogle": {
70
- "model": "gemini-3-flash-preview"
91
+ "model": "gemini-3-flash-preview",
71
92
 
72
93
  // Google AI Studio
73
94
  "apiKey": "FIXME"
@@ -76,42 +97,22 @@ Create the configuration.
76
97
  // "platform": "vertex-ai",
77
98
  // "baseURL": "https://aiplatform.googleapis.com/v1beta1/projects/<project_id>/locations/<location>",
78
99
  },
79
- "tavily": {
80
- "apiKey": "FIXME"
100
+
101
+ "searchWeb": {
102
+ "tavilyApiKey": "FIXME"
81
103
  }
82
- }
104
+ },
105
+
83
106
  }
84
107
 
85
108
  ```
86
109
 
87
110
  <details>
88
- <summary>Extra provider examples</summary>
111
+ <summary>Other provider examples</summary>
89
112
 
90
113
  ```js
91
114
  {
92
115
  "platforms": [
93
- {
94
- // Requires Azure CLI to get access token
95
- "name": "azure",
96
- "variant": "default",
97
- "baseURL": "https://<resource>.openai.azure.com/openai",
98
- // Optional
99
- "azureConfigDir": "/home/xxx/.azure-for-agent"
100
- },
101
- {
102
- "name": "bedrock",
103
- "variant": "default",
104
- "baseURL": "https://bedrock-runtime.<region>.amazonaws.com",
105
- "awsProfile": "FIXME"
106
- },
107
- {
108
- // Requires gcloud CLI to get authentication token
109
- "name": "vertex-ai",
110
- "variant": "default",
111
- "baseURL": "https://aiplatform.googleapis.com/v1beta1/projects/<project>/locations/<location>",
112
- // Optional
113
- "account": "<service_account_email>"
114
- },
115
116
  {
116
117
  "name": "openai",
117
118
  "variant": "ollama",
@@ -139,7 +140,7 @@ Run the agent.
139
140
  ```sh
140
141
  plain
141
142
 
142
- # Or specify a specific model
143
+ # Or
143
144
  plain -m <model>+<variant>
144
145
  ```
145
146
 
@@ -164,8 +165,8 @@ The agent can use the following tools to assist with tasks:
164
165
  - **patch_file**: Patch a file.
165
166
  - **tmux_command**: Run a tmux command.
166
167
  - **fetch_web_page**: Fetch and extract web page content from a given URL, returning it as Markdown.
168
+ - **ask_google**: Ask Google a question using natural language (requires Google API key or Vertex AI configuration).
167
169
  - **search_web**: Search the web for information (requires Tavily API key).
168
- - **ask_google**: Ask Google a question using natural language (requires Gemini API key).
169
170
  - **delegate_to_subagent**: Delegate a subtask to a subagent. The agent switches to a subagent role within the same conversation, focusing on the specified goal.
170
171
  - **report_as_subagent**: Report completion and return to the main agent. Used by subagents to communicate results and restore the main agent role. After reporting, the subagent's conversation history is removed from the context.
171
172
 
@@ -205,13 +206,10 @@ The agent loads configuration files in the following order. Settings in later fi
205
206
  ```js
206
207
  {
207
208
  "autoApproval": {
208
- // Automatically deny unmatched tools instead of asking
209
209
  "defaultAction": "deny",
210
- // The maximum number of automatic approvals.
211
210
  "maxApprovals": 100,
212
- // Patterns are evaluated in order. First match wins.
213
211
  "patterns": [
214
- // Prohibit direct access to external URLs
212
+ // Prohibit direct access to external URLs (even GET requests can leak data via URL parameters)
215
213
  {
216
214
  "toolName": "fetch_web_page",
217
215
  "action": "deny",
@@ -268,7 +266,7 @@ The agent loads configuration files in the following order. Settings in later fi
268
266
  "patterns": [
269
267
  {
270
268
  "toolName": { "$regex": "^(write_file|patch_file)$" },
271
- "input": { "filePath": { "$regex": "^\\.plain-agent/memory/.+\\.md$" } },
269
+ "input": { "filePath": { "$regex": "^(\\./)?\\.plain-agent/memory/.+\\.md$" } },
272
270
  "action": "allow"
273
271
  },
274
272
  {
@@ -277,8 +275,7 @@ The agent loads configuration files in the following order. Settings in later fi
277
275
  "action": "allow"
278
276
  },
279
277
 
280
- // ⚠️ `npm run test` may execute arbitrary code and access git-ignored files.
281
- // It must be run in a sandbox.
278
+ // ⚠️ Arbitrary code execution can access unauthorized files and networks. Always use a sandbox.
282
279
  {
283
280
  "toolName": "exec_command",
284
281
  "input": { "command": "npm", "args": ["run", { "$regex": "^(check|test|lint|fix)$" }] },
@@ -326,7 +323,7 @@ The agent loads configuration files in the following order. Settings in later fi
326
323
  ]
327
324
  },
328
325
 
329
- // Configure MCP servers for extended functionality
326
+ // Configure MCP servers
330
327
  "mcpServers": {
331
328
  "chrome_devtools": {
332
329
  "command": "npx",
@@ -364,12 +361,10 @@ The agent loads configuration files in the following order. Settings in later fi
364
361
 
365
362
  ## Prompts
366
363
 
367
- You can define reusable prompts in Markdown files. These are especially useful for common tasks like creating commit messages or conducting retrospectives.
364
+ You can define reusable prompts in Markdown files.
368
365
 
369
366
  ### Prompt File Format
370
367
 
371
- Prompts are Markdown files with a YAML frontmatter:
372
-
373
368
  ```md
374
369
  ---
375
370
  description: Create a commit message based on staged changes
@@ -406,25 +401,23 @@ Remote prompts are fetched and cached locally. The local content will be appende
406
401
 
407
402
  The agent searches for prompts in the following directories:
408
403
 
409
- - `~/.config/plain-agent/prompts/` (Global/User-defined prompts)
410
- - `.plain-agent/prompts/` (Project-specific prompts)
411
- - `.claude/commands/` (Claude-specific commands, prefixed with `claude/commands:`)
412
- - `.claude/skills/` (Claude-specific skills, prefixed with `claude/skills:`)
404
+ - `~/.config/plain-agent/prompts/`
405
+ - `.plain-agent/prompts/`
406
+ - `.claude/commands/`
407
+ - `.claude/skills/`
413
408
 
414
- The prompt ID is the relative path of the file without the `.md` extension. For example, `.plain-agent/prompts/retro.md` becomes `/prompts:retro`.
409
+ The prompt ID is the relative path of the file without the `.md` extension. For example, `.plain-agent/prompts/commit.md` becomes `/prompts:commit`.
415
410
 
416
411
  ### Shortcuts
417
412
 
418
- Prompts located in a `shortcuts/` subdirectory (e.g., `.plain-agent/prompts/shortcuts/review.md`) can be invoked directly as a top-level command (e.g., `/review`). This is useful for frequently used tasks. If a prompt is in a `shortcuts/` subdirectory, its ID is simplified by removing the `shortcuts/` prefix for use as a shortcut (e.g., `shortcuts/review` becomes `/review`).
413
+ Prompts located in a `shortcuts/` subdirectory (e.g., `.plain-agent/prompts/shortcuts/commit.md`) can be invoked directly as a top-level command (e.g., `/commit`).
419
414
 
420
415
  ## Subagents
421
416
 
422
- Subagents are specialized agents that can be delegated specific tasks. They allow you to break down complex workflows into focused, manageable components.
417
+ Subagents are specialized agents designed for specific tasks.
423
418
 
424
419
  ### Subagent File Format
425
420
 
426
- Subagent definitions are Markdown files with a YAML frontmatter:
427
-
428
421
  ```md
429
422
  ---
430
423
  description: Simplifies and refines code for clarity and maintainability
@@ -449,11 +442,9 @@ Remote subagents are fetched and cached locally. The local content will be appen
449
442
 
450
443
  The agent searches for subagent definitions in the following directories:
451
444
 
452
- - `~/.config/plain-agent/agents/` (Global/User-defined agents)
453
- - `.plain-agent/agents/` (Project-specific agents)
454
- - `.claude/agents/` (Claude-specific agents)
455
-
456
- The subagent ID is the relative path of the file without the `.md` extension. For example, `.plain-agent/agents/worker.md` becomes `worker`.
445
+ - `~/.config/plain-agent/agents/`
446
+ - `.plain-agent/agents/`
447
+ - `.claude/agents/`
457
448
 
458
449
  ## Claude Code Plugin Support
459
450
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iinm/plain-agent",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "A lightweight CLI-based coding agent",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/cliArgs.mjs CHANGED
@@ -2,6 +2,7 @@
2
2
  * @typedef {Object} CliArgs
3
3
  * @property {string|null} model - Model name with variant
4
4
  * @property {boolean} showHelp - Whether to show help message
5
+ * @property {boolean} listModels - Whether to list available models
5
6
  */
6
7
 
7
8
  /**
@@ -12,13 +13,15 @@
12
13
  export function parseCliArgs(argv) {
13
14
  const args = argv.slice(2);
14
15
  /** @type {CliArgs} */
15
- const result = { model: null, showHelp: false };
16
+ const result = { model: null, showHelp: false, listModels: false };
16
17
 
17
18
  for (let i = 0; i < args.length; i++) {
18
19
  if (args[i] === "-m" && args[i + 1]) {
19
20
  result.model = args[++i];
20
21
  } else if (args[i] === "-h" || args[i] === "--help") {
21
22
  result.showHelp = true;
23
+ } else if (args[i] === "-l" || args[i] === "--list-models") {
24
+ result.listModels = true;
22
25
  }
23
26
  }
24
27
 
@@ -35,6 +38,7 @@ Usage: agent [options]
35
38
 
36
39
  Options:
37
40
  -m <model+variant> Model to use
41
+ -l, --list-models List available models
38
42
  -h, --help Show this help message
39
43
 
40
44
  Examples:
package/src/config.d.ts CHANGED
@@ -18,9 +18,6 @@ export type AppConfig = {
18
18
  };
19
19
  sandbox?: ExecCommandSanboxConfig;
20
20
  tools?: {
21
- tavily?: {
22
- apiKey?: string;
23
- };
24
21
  /**
25
22
  * - Vertex AI: requires baseURL and account
26
23
  * - AI Studio: requires apiKey
@@ -32,6 +29,9 @@ export type AppConfig = {
32
29
  apiKey?: string;
33
30
  model?: string;
34
31
  };
32
+ searchWeb?: {
33
+ tavilyApiKey?: string;
34
+ };
35
35
  };
36
36
  mcpServers?: Record<string, MCPServerConfig>;
37
37
  notifyCmd?: string;
package/src/config.mjs CHANGED
@@ -16,9 +16,12 @@ import {
16
16
  import { evalJSONConfig } from "./utils/evalJSONConfig.mjs";
17
17
 
18
18
  /**
19
+ * @param {Object} [options]
20
+ * @param {boolean} [options.skipTrustCheck] - Skip trust check for config files
19
21
  * @returns {Promise<{appConfig: AppConfig, loadedConfigPath: string[]}>}
20
22
  */
21
- export async function loadAppConfig() {
23
+ export async function loadAppConfig(options = {}) {
24
+ const { skipTrustCheck = false } = options;
22
25
  const paths = [
23
26
  `${AGENT_ROOT}/.config/config.predefined.json`,
24
27
  `${AGENT_USER_CONFIG_DIR}/config.json`,
@@ -33,7 +36,7 @@ export async function loadAppConfig() {
33
36
  let merged = {};
34
37
 
35
38
  for (const filePath of paths) {
36
- const config = await loadConfigFile(path.resolve(filePath));
39
+ const config = await loadConfigFile(path.resolve(filePath), skipTrustCheck);
37
40
  if (Object.keys(config).length) {
38
41
  loadedConfigPath.push(filePath);
39
42
  }
@@ -55,9 +58,9 @@ export async function loadAppConfig() {
55
58
  },
56
59
  sandbox: config.sandbox ?? merged.sandbox,
57
60
  tools: {
58
- tavily: {
59
- ...(merged.tools?.tavily ?? {}),
60
- ...(config.tools?.tavily ?? {}),
61
+ searchWeb: {
62
+ ...(merged.tools?.searchWeb ?? {}),
63
+ ...(config.tools?.searchWeb ?? {}),
61
64
  },
62
65
  askGoogle: {
63
66
  ...(merged.tools?.askGoogle ?? {}),
@@ -81,9 +84,10 @@ export async function loadAppConfig() {
81
84
 
82
85
  /**
83
86
  * @param {string} filePath
87
+ * @param {boolean} [skipTrustCheck=false]
84
88
  * @returns {Promise<AppConfig>}
85
89
  */
86
- export async function loadConfigFile(filePath) {
90
+ export async function loadConfigFile(filePath, skipTrustCheck = false) {
87
91
  let content;
88
92
  try {
89
93
  content = await fs.readFile(filePath, "utf-8");
@@ -95,7 +99,7 @@ export async function loadConfigFile(filePath) {
95
99
  }
96
100
 
97
101
  const hash = crypto.createHash("sha256").update(content).digest("hex");
98
- const isTrusted = await isConfigHashTrusted(hash);
102
+ const isTrusted = skipTrustCheck || (await isConfigHashTrusted(hash));
99
103
 
100
104
  if (!isTrusted) {
101
105
  if (!process.stdout.isTTY) {
package/src/main.mjs CHANGED
@@ -33,6 +33,21 @@ if (cliArgs.showHelp) {
33
33
  printHelp();
34
34
  }
35
35
 
36
+ if (cliArgs.listModels) {
37
+ const { appConfig } = await loadAppConfig({ skipTrustCheck: true });
38
+ if (!appConfig.models || appConfig.models.length === 0) {
39
+ console.error("No models found in configuration.");
40
+ process.exit(1);
41
+ }
42
+ for (const model of appConfig.models) {
43
+ const platform = model.platform;
44
+ console.log(
45
+ `${model.name}+${model.variant} (platform: ${platform.name}+${platform.variant})`,
46
+ );
47
+ }
48
+ process.exit(0);
49
+ }
50
+
36
51
  (async () => {
37
52
  const startTime = new Date();
38
53
  const sessionId = [
@@ -118,8 +133,8 @@ if (cliArgs.showHelp) {
118
133
  createReportAsSubagentTool(),
119
134
  ];
120
135
 
121
- if (appConfig.tools?.tavily?.apiKey) {
122
- builtinTools.push(createTavilySearchTool(appConfig.tools.tavily));
136
+ if (appConfig.tools?.searchWeb?.tavilyApiKey) {
137
+ builtinTools.push(createTavilySearchTool(appConfig.tools.searchWeb));
123
138
  }
124
139
 
125
140
  if (appConfig.tools?.askGoogle) {
@@ -6,7 +6,7 @@
6
6
  import { noThrow } from "../utils/noThrow.mjs";
7
7
 
8
8
  /**
9
- * @param {{apiKey?: string}} config
9
+ * @param {{tavilyApiKey?: string}} config
10
10
  * @returns {Tool}
11
11
  */
12
12
  export function createTavilySearchTool(config) {
@@ -34,7 +34,7 @@ export function createTavilySearchTool(config) {
34
34
  const response = await fetch("https://api.tavily.com/search", {
35
35
  method: "POST",
36
36
  headers: {
37
- Authorization: `Bearer ${config.apiKey}`,
37
+ Authorization: `Bearer ${config.tavilyApiKey}`,
38
38
  "Content-Type": "application/json",
39
39
  },
40
40
  body: JSON.stringify({