@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 +58 -67
- package/package.json +1 -1
- package/src/cliArgs.mjs +5 -1
- package/src/config.d.ts +3 -3
- package/src/config.mjs +11 -7
- package/src/main.mjs +17 -2
- package/src/tools/tavilySearch.mjs +2 -2
package/README.md
CHANGED
|
@@ -2,25 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
A lightweight CLI-based coding agent.
|
|
4
4
|
|
|
5
|
-
- **Safety controls** — Configure
|
|
6
|
-
- **Multi-provider** —
|
|
7
|
-
- **Sequential subagent delegation** — Delegate subtasks to specialized subagents with full visibility
|
|
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**
|
|
9
|
+
- **Claude Code compatible** — Reuse Claude Code plugins, agents, commands, and skills
|
|
10
10
|
|
|
11
11
|
## Safety Controls
|
|
12
12
|
|
|
13
|
-
|
|
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
|
-
|
|
17
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
80
|
-
|
|
100
|
+
|
|
101
|
+
"searchWeb": {
|
|
102
|
+
"tavilyApiKey": "FIXME"
|
|
81
103
|
}
|
|
82
|
-
}
|
|
104
|
+
},
|
|
105
|
+
|
|
83
106
|
}
|
|
84
107
|
|
|
85
108
|
```
|
|
86
109
|
|
|
87
110
|
<details>
|
|
88
|
-
<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
|
|
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": "
|
|
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
|
-
// ⚠️
|
|
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
|
|
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.
|
|
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/`
|
|
410
|
-
- `.plain-agent/prompts/`
|
|
411
|
-
- `.claude/commands/`
|
|
412
|
-
- `.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/
|
|
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/
|
|
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
|
|
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/`
|
|
453
|
-
- `.plain-agent/agents/`
|
|
454
|
-
- `.claude/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
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
|
-
|
|
59
|
-
...(merged.tools?.
|
|
60
|
-
...(config.tools?.
|
|
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?.
|
|
122
|
-
builtinTools.push(createTavilySearchTool(appConfig.tools.
|
|
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 {{
|
|
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.
|
|
37
|
+
Authorization: `Bearer ${config.tavilyApiKey}`,
|
|
38
38
|
"Content-Type": "application/json",
|
|
39
39
|
},
|
|
40
40
|
body: JSON.stringify({
|