@iinm/plain-agent 1.10.0 → 1.10.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 +6 -58
- package/package.json +1 -1
- package/src/claudeCodePlugin.mjs +2 -2
- package/src/cliCommands.mjs +5 -5
- package/src/cliInteractive.mjs +23 -12
- package/src/config.mjs +1 -1
- package/src/context/loadAgentRoles.mjs +2 -2
- package/src/context/loadPrompts.mjs +2 -2
- package/src/context/loadUserMessageContext.mjs +1 -1
- package/src/utils/createSequentialExecutor.mjs +28 -0
package/README.md
CHANGED
|
@@ -2,62 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
A lightweight, capable coding agent for the terminal.
|
|
4
4
|
|
|
5
|
-
- **Multi-provider** — Use Claude, GPT, Gemini, or any OpenAI-compatible model.
|
|
6
|
-
|
|
7
|
-
|
|
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 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.
|
|
59
|
-
- **Extensible** — Define prompts and subagents in Markdown. Connect MCP servers.
|
|
60
|
-
Supports Claude Code plugins and `.claude/` commands, subagents, and skills.
|
|
5
|
+
- **Multi-provider** — Use Claude, GPT, Gemini, or any OpenAI-compatible model via Bedrock, Vertex AI, or direct APIs.
|
|
6
|
+
- **Fine-grained auto-approval** — Auto-approve tool calls by name and arguments using regex patterns.
|
|
7
|
+
- **Sandboxed execution** — Run commands in a Docker container with filesystem and network isolation.
|
|
8
|
+
- **Claude Code compatible** — Use Claude Code plugins, commands, subagents, and skills from `.claude/`.
|
|
9
|
+
- **Zero external dependencies** — Built with Node.js standard libraries only.
|
|
61
10
|
|
|
62
11
|
## Limitations
|
|
63
12
|
|
|
@@ -388,8 +337,7 @@ Files are loaded in the following order. Settings in later files override earlie
|
|
|
388
337
|
├── (3) config.json # Project-specific configuration
|
|
389
338
|
├── (4) config.local.json # Project-specific local configuration (including secrets)
|
|
390
339
|
├── prompts/ # Project-specific prompts
|
|
391
|
-
|
|
392
|
-
└── sandbox/ # Sandbox runner scripts
|
|
340
|
+
└── agents/ # Project-specific agent roles
|
|
393
341
|
```
|
|
394
342
|
|
|
395
343
|
### Example
|
package/package.json
CHANGED
package/src/claudeCodePlugin.mjs
CHANGED
|
@@ -32,7 +32,7 @@ export function resolvePluginPaths(repos) {
|
|
|
32
32
|
for (const repo of repos) {
|
|
33
33
|
const ownerRepo = extractOwnerRepo(repo.source);
|
|
34
34
|
if (!ownerRepo) {
|
|
35
|
-
console.
|
|
35
|
+
console.error(`Invalid source URL: ${repo.source}`);
|
|
36
36
|
continue;
|
|
37
37
|
}
|
|
38
38
|
|
|
@@ -43,7 +43,7 @@ export function resolvePluginPaths(repos) {
|
|
|
43
43
|
try {
|
|
44
44
|
only = new RegExp(plugin.only);
|
|
45
45
|
} catch (err) {
|
|
46
|
-
console.
|
|
46
|
+
console.error(
|
|
47
47
|
`Invalid regex pattern "${plugin.only}" for plugin "${plugin.name}":`,
|
|
48
48
|
err instanceof Error ? err.message : String(err),
|
|
49
49
|
);
|
package/src/cliCommands.mjs
CHANGED
|
@@ -75,7 +75,7 @@ export function createCommandHandler({
|
|
|
75
75
|
const prompt = prompts.get(id);
|
|
76
76
|
|
|
77
77
|
if (!prompt) {
|
|
78
|
-
console.
|
|
78
|
+
console.error(styleText("red", `\nPrompt not found: ${id}`));
|
|
79
79
|
return "prompt";
|
|
80
80
|
}
|
|
81
81
|
|
|
@@ -116,13 +116,13 @@ export function createCommandHandler({
|
|
|
116
116
|
if (inputTrimmed.startsWith("!")) {
|
|
117
117
|
const fileRange = parseFileRange(inputTrimmed.slice(1));
|
|
118
118
|
if (fileRange instanceof Error) {
|
|
119
|
-
console.
|
|
119
|
+
console.error(styleText("red", `\n${fileRange.message}`));
|
|
120
120
|
return "prompt";
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
const fileContent = await readFileRange(fileRange);
|
|
124
124
|
if (fileContent instanceof Error) {
|
|
125
|
-
console.
|
|
125
|
+
console.error(styleText("red", `\n${fileContent.message}`));
|
|
126
126
|
return "prompt";
|
|
127
127
|
}
|
|
128
128
|
|
|
@@ -174,7 +174,7 @@ export function createCommandHandler({
|
|
|
174
174
|
if (inputTrimmed.startsWith("/agents:")) {
|
|
175
175
|
const match = inputTrimmed.match(/^\/agents:([^ ]+)(?:\s+(.*))?$/s);
|
|
176
176
|
if (!match) {
|
|
177
|
-
console.
|
|
177
|
+
console.error(styleText("red", "\nInvalid agent invocation format."));
|
|
178
178
|
return "prompt";
|
|
179
179
|
}
|
|
180
180
|
return await invokeAgent(match[1], match[2] || "");
|
|
@@ -204,7 +204,7 @@ export function createCommandHandler({
|
|
|
204
204
|
if (inputTrimmed.startsWith("/prompts:")) {
|
|
205
205
|
const match = inputTrimmed.match(/^\/prompts:([^ ]+)(?:\s+(.*))?$/s);
|
|
206
206
|
if (!match) {
|
|
207
|
-
console.
|
|
207
|
+
console.error(styleText("red", "\nInvalid prompt invocation format."));
|
|
208
208
|
return "prompt";
|
|
209
209
|
}
|
|
210
210
|
return await invokePrompt(
|
package/src/cliInteractive.mjs
CHANGED
|
@@ -17,6 +17,7 @@ import { createInterruptTransform } from "./cliInterruptTransform.mjs";
|
|
|
17
17
|
import { createMuteTransform } from "./cliMuteTransform.mjs";
|
|
18
18
|
import { createPasteHandler } from "./cliPasteTransform.mjs";
|
|
19
19
|
import { appendUsageRecord, buildUsageRecord } from "./usageStore.mjs";
|
|
20
|
+
import { createSequentialExecutor } from "./utils/createSequentialExecutor.mjs";
|
|
20
21
|
import { notify } from "./utils/notify.mjs";
|
|
21
22
|
import { parseVoiceToggleKey, startVoiceSession } from "./voiceInput.mjs";
|
|
22
23
|
|
|
@@ -280,7 +281,7 @@ export function startInteractiveSession({
|
|
|
280
281
|
// Agent turn: pause auto-approve; do not clear input.
|
|
281
282
|
if (!state.turn) {
|
|
282
283
|
agentCommands.pauseAutoApprove();
|
|
283
|
-
console.
|
|
284
|
+
console.error(
|
|
284
285
|
styleText(
|
|
285
286
|
"yellow",
|
|
286
287
|
"\n\n⚠️ Ctrl-C: Auto-approve paused. Finishing current tool...\nPress Ctrl-D twice to exit.\n",
|
|
@@ -327,7 +328,7 @@ export function startInteractiveSession({
|
|
|
327
328
|
);
|
|
328
329
|
cli.prompt();
|
|
329
330
|
} else {
|
|
330
|
-
console.
|
|
331
|
+
console.error(styleText("yellow", "\n\n⚠️ Press Ctrl-D again to exit.\n"));
|
|
331
332
|
}
|
|
332
333
|
};
|
|
333
334
|
|
|
@@ -417,7 +418,7 @@ export function startInteractiveSession({
|
|
|
417
418
|
|
|
418
419
|
cli.on("line", async (lineInput) => {
|
|
419
420
|
if (!state.turn) {
|
|
420
|
-
console.
|
|
421
|
+
console.error(
|
|
421
422
|
styleText(
|
|
422
423
|
"yellow",
|
|
423
424
|
`\nAgent is working. Ignore input: ${lineInput.trim()}`,
|
|
@@ -473,12 +474,16 @@ export function startInteractiveSession({
|
|
|
473
474
|
}
|
|
474
475
|
});
|
|
475
476
|
|
|
477
|
+
const enqueueOutput = createSequentialExecutor();
|
|
478
|
+
|
|
476
479
|
agentEventEmitter.on("message", (message) => {
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
480
|
+
enqueueOutput(() =>
|
|
481
|
+
printMessage(message).catch((err) => {
|
|
482
|
+
console.error(
|
|
483
|
+
styleText("red", `Error rendering message: ${err.message}`),
|
|
484
|
+
);
|
|
485
|
+
}),
|
|
486
|
+
);
|
|
482
487
|
});
|
|
483
488
|
|
|
484
489
|
agentEventEmitter.on("toolUseRequest", () => {
|
|
@@ -500,11 +505,13 @@ export function startInteractiveSession({
|
|
|
500
505
|
});
|
|
501
506
|
|
|
502
507
|
agentEventEmitter.on("providerTokenUsage", (usage) => {
|
|
503
|
-
|
|
508
|
+
enqueueOutput(() => {
|
|
509
|
+
console.log(formatProviderTokenUsage(usage));
|
|
510
|
+
});
|
|
504
511
|
});
|
|
505
512
|
|
|
506
513
|
agentEventEmitter.on("error", (error) => {
|
|
507
|
-
console.
|
|
514
|
+
console.error(
|
|
508
515
|
styleText(
|
|
509
516
|
"red",
|
|
510
517
|
`\nError: message=${error.message}, stack=${error.stack}`,
|
|
@@ -519,8 +526,12 @@ export function startInteractiveSession({
|
|
|
519
526
|
styleText("yellow", `\nNotification error: ${err.message}`),
|
|
520
527
|
);
|
|
521
528
|
}
|
|
522
|
-
|
|
523
|
-
|
|
529
|
+
|
|
530
|
+
// Wait for all output operations to complete
|
|
531
|
+
await enqueueOutput(() => {});
|
|
532
|
+
|
|
533
|
+
// Defer prompt rendering to ensure terminal output is visible
|
|
534
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
524
535
|
|
|
525
536
|
state.turn = true;
|
|
526
537
|
cli.prompt();
|
package/src/config.mjs
CHANGED
|
@@ -128,7 +128,7 @@ export async function loadConfigFile(filePath, skipTrustCheck = false) {
|
|
|
128
128
|
|
|
129
129
|
if (!isTrusted) {
|
|
130
130
|
if (!process.stdout.isTTY) {
|
|
131
|
-
console.
|
|
131
|
+
console.error(
|
|
132
132
|
styleText(
|
|
133
133
|
"yellow",
|
|
134
134
|
`WARNING: Config file found at '${filePath}' but cannot ask for approval without a TTY. Skipping.`,
|
|
@@ -54,7 +54,7 @@ export async function loadAgentRoles(claudeCodePlugins) {
|
|
|
54
54
|
agentDirs.map(async ({ dir, idPrefix, only }) => {
|
|
55
55
|
const files = await getMarkdownFiles(dir).catch((err) => {
|
|
56
56
|
if (err.code !== "ENOENT") {
|
|
57
|
-
console.
|
|
57
|
+
console.error(`Failed to list agent roles in ${dir}:`, err);
|
|
58
58
|
}
|
|
59
59
|
return /** @type {string[]} */ ([]);
|
|
60
60
|
});
|
|
@@ -72,7 +72,7 @@ export async function loadAgentRoles(claudeCodePlugins) {
|
|
|
72
72
|
files.map(async ({ dir, file, idPrefix }) => {
|
|
73
73
|
const fullPath = path.join(dir, file);
|
|
74
74
|
const content = await fs.readFile(fullPath, "utf-8").catch((err) => {
|
|
75
|
-
console.
|
|
75
|
+
console.error(`Failed to read agent role file ${fullPath}:`, err);
|
|
76
76
|
return null;
|
|
77
77
|
});
|
|
78
78
|
|
|
@@ -69,7 +69,7 @@ export async function loadPrompts(claudeCodePlugins) {
|
|
|
69
69
|
promptDirs.map(async ({ dir, idPrefix, only }) => {
|
|
70
70
|
const files = await getMarkdownFiles(dir).catch((err) => {
|
|
71
71
|
if (err.code !== "ENOENT") {
|
|
72
|
-
console.
|
|
72
|
+
console.error(`Failed to list prompts in ${dir}:`, err);
|
|
73
73
|
}
|
|
74
74
|
return /** @type {string[]} */ ([]);
|
|
75
75
|
});
|
|
@@ -95,7 +95,7 @@ export async function loadPrompts(claudeCodePlugins) {
|
|
|
95
95
|
files.map(async ({ dir, file, idPrefix }) => {
|
|
96
96
|
const fullPath = path.join(dir, file);
|
|
97
97
|
const content = await fs.readFile(fullPath, "utf-8").catch((err) => {
|
|
98
|
-
console.
|
|
98
|
+
console.error(`Failed to read prompt file ${fullPath}:`, err);
|
|
99
99
|
return null;
|
|
100
100
|
});
|
|
101
101
|
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create a sequential executor that enqueues async operations
|
|
3
|
+
* and executes them in order.
|
|
4
|
+
*
|
|
5
|
+
* @returns {(fn: () => Promise<void> | void) => Promise<void>}
|
|
6
|
+
* A function that enqueues an operation and returns a promise
|
|
7
|
+
* that resolves when all queued operations (including this one) complete.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* const enqueue = createSequentialExecutor();
|
|
11
|
+
*
|
|
12
|
+
* // These will execute in order, even if called from different event handlers
|
|
13
|
+
* enqueue(() => console.log("first"));
|
|
14
|
+
* enqueue(async () => { await something(); });
|
|
15
|
+
* enqueue(() => console.log("second"));
|
|
16
|
+
*/
|
|
17
|
+
export function createSequentialExecutor() {
|
|
18
|
+
/** @type {Promise<void>} */
|
|
19
|
+
let chain = Promise.resolve();
|
|
20
|
+
|
|
21
|
+
return (fn) => {
|
|
22
|
+
chain = chain.then(fn).catch((err) => {
|
|
23
|
+
// Prevent unhandled rejection, but log the error
|
|
24
|
+
console.error("Sequential executor error:", err);
|
|
25
|
+
});
|
|
26
|
+
return chain;
|
|
27
|
+
};
|
|
28
|
+
}
|