@iinm/plain-agent 1.0.4 → 1.0.6
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 +37 -36
- 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/providers/openai.mjs +0 -8
- package/src/tools/tavilySearch.mjs +2 -2
package/README.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
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
9
|
- **Claude Code compatible** *(experimental)* — Reuse Claude Code plugins, agents, commands, and skills
|
|
10
10
|
|
|
@@ -13,8 +13,7 @@ A lightweight CLI-based coding agent.
|
|
|
13
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
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.
|
|
15
15
|
|
|
16
|
-
⚠️
|
|
17
|
-
Use a sandbox for stronger isolation.
|
|
16
|
+
⚠️ `write_file` and `patch_file` block access to git-ignored files. `exec_command` blocks direct path arguments (e.g., `ls .env`), but cannot block access from executed programs (e.g., `node script.js`). Use a sandbox for stronger isolation.
|
|
18
17
|
|
|
19
18
|
## Requirements
|
|
20
19
|
|
|
@@ -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",
|
|
@@ -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.
|
|
167
|
-
- **search_web**: Search the web for information (requires Tavily API key).
|
|
168
168
|
- **ask_google**: Ask Google a question using natural language (requires Gemini API key).
|
|
169
|
+
- **search_web**: Search the web for information (requires Tavily 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
|
|
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) {
|
package/src/providers/openai.mjs
CHANGED
|
@@ -419,14 +419,6 @@ function convertOpenAIStreamDataToAgentPartialContent(streamEvent) {
|
|
|
419
419
|
};
|
|
420
420
|
}
|
|
421
421
|
|
|
422
|
-
if (streamEvent.type === "response.reasoning_summary_text.done") {
|
|
423
|
-
return {
|
|
424
|
-
type: "thinking",
|
|
425
|
-
position: "delta",
|
|
426
|
-
content: streamEvent.text,
|
|
427
|
-
};
|
|
428
|
-
}
|
|
429
|
-
|
|
430
422
|
if (streamEvent.type === "response.output_item.done") {
|
|
431
423
|
if (streamEvent.item.type === "reasoning") {
|
|
432
424
|
return {
|
|
@@ -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({
|