@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 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
- <details>
7
- <summary>Predefined models</summary>
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
- ├── agents/ # Project-specific agent roles
392
- └── sandbox/ # Sandbox runner scripts
340
+ └── agents/ # Project-specific agent roles
393
341
  ```
394
342
 
395
343
  ### Example
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iinm/plain-agent",
3
- "version": "1.10.0",
3
+ "version": "1.10.2",
4
4
  "description": "A lightweight CLI-based coding agent",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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.warn(`Invalid source URL: ${repo.source}`);
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.warn(
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
  );
@@ -75,7 +75,7 @@ export function createCommandHandler({
75
75
  const prompt = prompts.get(id);
76
76
 
77
77
  if (!prompt) {
78
- console.log(styleText("red", `\nPrompt not found: ${id}`));
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.log(styleText("red", `\n${fileRange.message}`));
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.log(styleText("red", `\n${fileContent.message}`));
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.log(styleText("red", "\nInvalid agent invocation format."));
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.log(styleText("red", "\nInvalid prompt invocation format."));
207
+ console.error(styleText("red", "\nInvalid prompt invocation format."));
208
208
  return "prompt";
209
209
  }
210
210
  return await invokePrompt(
@@ -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.log(
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.log(styleText("yellow", "\n\n⚠️ Press Ctrl-D again to exit.\n"));
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.warn(
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
- printMessage(message).catch((err) => {
478
- console.error(
479
- styleText("red", `Error rendering message: ${err.message}`),
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
- console.log(formatProviderTokenUsage(usage));
508
+ enqueueOutput(() => {
509
+ console.log(formatProviderTokenUsage(usage));
510
+ });
504
511
  });
505
512
 
506
513
  agentEventEmitter.on("error", (error) => {
507
- console.log(
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
- // 暫定対応: token usageのconsole出力を確実にflushするため、次のevent loop tickまで遅延
523
- await new Promise((resolve) => setTimeout(resolve, 0));
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.warn(
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.warn(`Failed to list agent roles in ${dir}:`, err);
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.warn(`Failed to read agent role file ${fullPath}:`, err);
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.warn(`Failed to list prompts in ${dir}:`, err);
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.warn(`Failed to read prompt file ${fullPath}:`, err);
98
+ console.error(`Failed to read prompt file ${fullPath}:`, err);
99
99
  return null;
100
100
  });
101
101
 
@@ -143,5 +143,5 @@ function inferMimeType(filePath) {
143
143
  * @returns {void}
144
144
  */
145
145
  function warn(message) {
146
- console.warn(styleText("yellow", message));
146
+ console.error(styleText("yellow", message));
147
147
  }
@@ -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
+ }