@iinm/plain-agent 1.11.5 → 1.11.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
@@ -3,7 +3,7 @@
3
3
  [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/iinm/plain-agent)
4
4
  [![npm version](https://img.shields.io/npm/v/@iinm/plain-agent)](https://www.npmjs.com/package/@iinm/plain-agent)
5
5
  [![install size](https://packagephobia.com/badge?p=@iinm/plain-agent)](https://packagephobia.com/result?p=@iinm/plain-agent)
6
- [![Socket Badge](https://badge.socket.dev/npm/package/@iinm/plain-agent/1.11.5)](https://socket.dev/npm/package/@iinm/plain-agent)
6
+ [![Socket Badge](https://badge.socket.dev/npm/package/@iinm/plain-agent/1.11.7)](https://socket.dev/npm/package/@iinm/plain-agent)
7
7
  [![CodeQL](https://github.com/iinm/plain-agent/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/iinm/plain-agent/actions/workflows/github-code-scanning/codeql)
8
8
 
9
9
  A lightweight terminal-based coding agent focused on safety and low token cost
@@ -1553,6 +1553,21 @@
1553
1553
 
1554
1554
  {
1555
1555
  "name": "kimi-k2.6",
1556
+ "variant": "azure",
1557
+ "platform": {
1558
+ "name": "azure",
1559
+ "variant": "default"
1560
+ },
1561
+ "model": {
1562
+ "format": "openai-messages",
1563
+ "config": {
1564
+ "model": "Kimi-K2.6",
1565
+ "reasoning_effort": "high"
1566
+ }
1567
+ }
1568
+ },
1569
+ {
1570
+ "name": "kimi-k2.7-code",
1556
1571
  "variant": "fireworks",
1557
1572
  "platform": {
1558
1573
  "name": "openai-compatible",
@@ -1561,7 +1576,7 @@
1561
1576
  "model": {
1562
1577
  "format": "openai-messages",
1563
1578
  "config": {
1564
- "model": "accounts/fireworks/models/kimi-k2p6"
1579
+ "model": "accounts/fireworks/models/kimi-k2p7-code"
1565
1580
  }
1566
1581
  },
1567
1582
  "cost": {
@@ -1569,13 +1584,13 @@
1569
1584
  "unit": "1M",
1570
1585
  "prices": {
1571
1586
  "prompt_tokens": 0.95,
1572
- "prompt_tokens_details.cached_tokens": -0.79,
1587
+ "prompt_tokens_details.cached_tokens": -0.76,
1573
1588
  "completion_tokens": 4
1574
1589
  }
1575
1590
  }
1576
1591
  },
1577
1592
  {
1578
- "name": "kimi-k2.6",
1593
+ "name": "kimi-k2.7-code",
1579
1594
  "variant": "novita",
1580
1595
  "platform": {
1581
1596
  "name": "openai-compatible",
@@ -1584,7 +1599,7 @@
1584
1599
  "model": {
1585
1600
  "format": "openai-messages",
1586
1601
  "config": {
1587
- "model": "moonshotai/kimi-k2.6"
1602
+ "model": "moonshotai/kimi-k2.7-code"
1588
1603
  }
1589
1604
  },
1590
1605
  "cost": {
@@ -1592,26 +1607,11 @@
1592
1607
  "unit": "1M",
1593
1608
  "prices": {
1594
1609
  "prompt_tokens": 0.95,
1595
- "prompt_tokens_details.cached_tokens": -0.79,
1610
+ "prompt_tokens_details.cached_tokens": -0.76,
1596
1611
  "completion_tokens": 4
1597
1612
  }
1598
1613
  }
1599
1614
  },
1600
- {
1601
- "name": "kimi-k2.6",
1602
- "variant": "azure",
1603
- "platform": {
1604
- "name": "azure",
1605
- "variant": "default"
1606
- },
1607
- "model": {
1608
- "format": "openai-messages",
1609
- "config": {
1610
- "model": "Kimi-K2.6",
1611
- "reasoning_effort": "high"
1612
- }
1613
- }
1614
- },
1615
1615
 
1616
1616
  {
1617
1617
  "name": "deepseek-v4-pro",
@@ -1676,7 +1676,7 @@
1676
1676
  },
1677
1677
 
1678
1678
  {
1679
- "name": "minimax-m2.7",
1679
+ "name": "minimax-m3",
1680
1680
  "variant": "fireworks",
1681
1681
  "platform": {
1682
1682
  "name": "openai-compatible",
@@ -1685,30 +1685,7 @@
1685
1685
  "model": {
1686
1686
  "format": "openai-messages",
1687
1687
  "config": {
1688
- "model": "accounts/fireworks/models/minimax-m2p7"
1689
- }
1690
- },
1691
- "cost": {
1692
- "currency": "USD",
1693
- "unit": "1M",
1694
- "prices": {
1695
- "prompt_tokens": 0.3,
1696
- "prompt_tokens_details.cached_tokens": -0.24,
1697
- "completion_tokens": 1.2
1698
- }
1699
- }
1700
- },
1701
- {
1702
- "name": "minimax-m2.7",
1703
- "variant": "novita",
1704
- "platform": {
1705
- "name": "openai-compatible",
1706
- "variant": "novita"
1707
- },
1708
- "model": {
1709
- "format": "openai-messages",
1710
- "config": {
1711
- "model": "minimax/minimax-m2.7"
1688
+ "model": "accounts/fireworks/models/minimax-m3"
1712
1689
  }
1713
1690
  },
1714
1691
  "cost": {
@@ -1746,7 +1723,7 @@
1746
1723
  },
1747
1724
 
1748
1725
  {
1749
- "name": "qwen3.6-plus",
1726
+ "name": "qwen3.7-plus",
1750
1727
  "variant": "fireworks",
1751
1728
  "platform": {
1752
1729
  "name": "openai-compatible",
@@ -1755,38 +1732,16 @@
1755
1732
  "model": {
1756
1733
  "format": "openai-messages",
1757
1734
  "config": {
1758
- "model": "accounts/fireworks/models/qwen3p6-plus"
1759
- }
1760
- },
1761
- "cost": {
1762
- "currency": "USD",
1763
- "unit": "1M",
1764
- "prices": {
1765
- "prompt_tokens": 0.5,
1766
- "prompt_tokens_details.cached_tokens": -0.4,
1767
- "completion_tokens": 3
1768
- }
1769
- }
1770
- },
1771
- {
1772
- "name": "qwen3.6-27b",
1773
- "variant": "novita",
1774
- "platform": {
1775
- "name": "openai-compatible",
1776
- "variant": "novita"
1777
- },
1778
- "model": {
1779
- "format": "openai-messages",
1780
- "config": {
1781
- "model": "qwen/qwen3.6-27b"
1735
+ "model": "accounts/fireworks/models/qwen3p7-plus"
1782
1736
  }
1783
1737
  },
1784
1738
  "cost": {
1785
1739
  "currency": "USD",
1786
1740
  "unit": "1M",
1787
1741
  "prices": {
1788
- "prompt_tokens": 0.6,
1789
- "completion_tokens": 3.6
1742
+ "prompt_tokens": 0.4,
1743
+ "prompt_tokens_details.cached_tokens": -0.32,
1744
+ "completion_tokens": 1.6
1790
1745
  }
1791
1746
  }
1792
1747
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iinm/plain-agent",
3
- "version": "1.11.5",
3
+ "version": "1.11.7",
4
4
  "description": "A lightweight terminal-based coding agent focused on safety and low token cost",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -18,6 +18,9 @@ import { noThrow } from "../utils/noThrow.mjs";
18
18
  /** Length above which a single-line arg forces block-form rendering. */
19
19
  const ARG_BLOCK_LENGTH_THRESHOLD = 60;
20
20
 
21
+ /** Total JSON length above which args are rendered in block form even if each individual arg is short. */
22
+ const ARGS_TOTAL_BLOCK_LENGTH_THRESHOLD = 160;
23
+
21
24
  /**
22
25
  * Format an args array for display.
23
26
  * Uses compact JSON for short single-line args; switches to a YAML-style
@@ -32,11 +35,13 @@ export function formatCommandArgs(args) {
32
35
  return `args: ${JSON.stringify(args ?? [])}`;
33
36
  }
34
37
 
35
- const needsBlock = args.some(
36
- (a) =>
37
- typeof a === "string" &&
38
- (a.includes("\n") || a.length > ARG_BLOCK_LENGTH_THRESHOLD),
39
- );
38
+ const needsBlock =
39
+ JSON.stringify(args).length > ARGS_TOTAL_BLOCK_LENGTH_THRESHOLD ||
40
+ args.some(
41
+ (a) =>
42
+ typeof a === "string" &&
43
+ (a.includes("\n") || a.length > ARG_BLOCK_LENGTH_THRESHOLD),
44
+ );
40
45
  if (!needsBlock) {
41
46
  return `args: ${highlightCommandArgs(JSON.stringify(args))}`;
42
47
  }
@@ -78,19 +83,29 @@ function highlightCommandArgs(args) {
78
83
  );
79
84
  }
80
85
 
86
+ /**
87
+ * @import { SandboxModeProvider } from "../tool"
88
+ */
89
+
81
90
  /**
82
91
  * Format tool use for display.
83
92
  * @param {MessageContentToolUse} toolUse
93
+ * @param {{ execCommandTool?: SandboxModeProvider }} [options]
84
94
  * @returns {Promise<string>}
85
95
  */
86
- export async function formatToolUse(toolUse) {
96
+ export async function formatToolUse(toolUse, options = {}) {
87
97
  const { toolName, input } = toolUse;
88
98
 
89
99
  if (toolName === "exec_command") {
90
100
  /** @type {Partial<ExecCommandInput>} */
91
101
  const execCommandInput = input;
102
+ const mode = options.execCommandTool?.getSandboxMode?.(input);
103
+ const toolNameLine =
104
+ mode === "unsandboxed"
105
+ ? `${toolName}${styleText("yellow", " [unsandboxed]")}`
106
+ : toolName;
92
107
  return [
93
- `${toolName}`,
108
+ toolNameLine,
94
109
  `command: ${JSON.stringify(execCommandInput.command)}`,
95
110
  formatCommandArgs(execCommandInput.args),
96
111
  ].join("\n");
@@ -375,9 +390,10 @@ export function formatCostForBatch(summary) {
375
390
  /**
376
391
  * Print a message to the console.
377
392
  * @param {Message} message
393
+ * @param {{ execCommandTool?: SandboxModeProvider }} [options]
378
394
  * @returns {Promise<void>}
379
395
  */
380
- export async function printMessage(message) {
396
+ export async function printMessage(message, options = {}) {
381
397
  switch (message.role) {
382
398
  case "assistant": {
383
399
  // console.log(styleText("bold", "\nAgent:"));
@@ -386,7 +402,7 @@ export async function printMessage(message) {
386
402
  (part) => part.type === "tool_use",
387
403
  );
388
404
  const formattedToolUses = await Promise.all(
389
- toolUseParts.map((part) => formatToolUse(part)),
405
+ toolUseParts.map((part) => formatToolUse(part, options)),
390
406
  );
391
407
  let toolUseIndex = 0;
392
408
  for (const part of message.content) {
@@ -405,7 +421,7 @@ export async function printMessage(message) {
405
421
  // console.log(part.text);
406
422
  // break;
407
423
  case "tool_use":
408
- console.log(styleText("bold", "\nTool call:"));
424
+ console.log(styleText("bold", "\nTool use:"));
409
425
  console.log(formattedToolUses[toolUseIndex++]);
410
426
  break;
411
427
  }
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * @import { UserEventEmitter, AgentEventEmitter, AgentCommands } from "../agent"
3
3
  * @import { ClaudeCodePlugin } from "../claudeCodePlugin.mjs"
4
+ * @import { Tool, SandboxModeProvider } from "../tool"
4
5
  * @import { VoiceInputConfig } from "../voice/input.mjs"
5
6
  * @import { VoiceSession } from "../voice/session.mjs"
6
7
  */
@@ -67,6 +68,7 @@ const HELP_MESSAGE = [
67
68
  * @property {() => Promise<void>} onStop
68
69
  * @property {ClaudeCodePlugin[]} [claudeCodePlugins]
69
70
  * @property {VoiceInputConfig} [voiceInput]
71
+ * @property {Tool & SandboxModeProvider} [execCommandTool]
70
72
  */
71
73
 
72
74
  /**
@@ -111,6 +113,7 @@ export function startInteractiveSession({
111
113
  onStop,
112
114
  claudeCodePlugins,
113
115
  voiceInput,
116
+ execCommandTool,
114
117
  }) {
115
118
  /** @type {{ turn: boolean, multiLineBuffer: string[] | null, subagentName: string, toolSpinnerIndex: number, toolSpinnerLastTime: number }} */
116
119
  const state = {
@@ -517,7 +520,7 @@ export function startInteractiveSession({
517
520
 
518
521
  agentEventEmitter.on("message", (message) => {
519
522
  enqueueOutput(() =>
520
- printMessage(message).catch((err) => {
523
+ printMessage(message, { execCommandTool }).catch((err) => {
521
524
  console.error(
522
525
  styleText("red", `Error rendering message: ${err.message}`),
523
526
  );
package/src/main.mjs CHANGED
@@ -308,8 +308,9 @@ export async function main(argv = process.argv) {
308
308
  skills: Array.from(prompts.values()).filter((p) => p.isSkill),
309
309
  });
310
310
 
311
+ const execCommandTool = createExecCommandTool({ sandbox: appConfig.sandbox });
311
312
  const builtinTools = [
312
- createExecCommandTool({ sandbox: appConfig.sandbox }),
313
+ execCommandTool,
313
314
  readFileTool,
314
315
  writeFileTool,
315
316
  createPatchFileTool(),
@@ -443,6 +444,7 @@ export async function main(argv = process.argv) {
443
444
  } else {
444
445
  startInteractiveSession({
445
446
  ...sessionOptions,
447
+ execCommandTool,
446
448
  notifyCmd: appConfig.notifyCmd,
447
449
  claudeCodePlugins: resolvePluginPaths(appConfig.claudeCodePlugins ?? []),
448
450
  voiceInput: appConfig.voiceInput,
@@ -145,7 +145,7 @@ export async function callAnthropicModel(
145
145
 
146
146
  const response = await runFetch();
147
147
 
148
- if (response.status === 429 || response.status >= 500) {
148
+ if ((response.status === 429 || response.status >= 500) && retryCount < 5) {
149
149
  const interval = Math.min(2 * 2 ** retryCount, 16);
150
150
  console.error(
151
151
  styleText(
@@ -107,7 +107,7 @@ export async function callBedrockConverseModel(
107
107
  (response.status === 429 ||
108
108
  response.status === 502 ||
109
109
  response.status === 503) &&
110
- retryCount < 3
110
+ retryCount < 5
111
111
  ) {
112
112
  const retryInterval = Math.min(2 * 2 ** retryCount, 16);
113
113
  console.error(
@@ -145,7 +145,10 @@ export function createCacheEnabledGeminiModelCaller(
145
145
  signal: AbortSignal.timeout(8 * 60 * 1000),
146
146
  });
147
147
 
148
- if (response.status === 429 || response.status >= 500) {
148
+ if (
149
+ (response.status === 429 || response.status >= 500) &&
150
+ retryCount < 5
151
+ ) {
149
152
  const interval = Math.min(2 * 2 ** retryCount, 16);
150
153
  console.error(
151
154
  styleText(
@@ -219,15 +222,20 @@ export function createCacheEnabledGeminiModelCaller(
219
222
  message instanceof GeminiNoCandidateError ||
220
223
  message instanceof GeminiMalformedFunctionCallError
221
224
  ) {
222
- const interval = Math.min(2 * 2 ** retryCount, 16);
223
- console.error(
224
- styleText(
225
- "yellow",
226
- `${message.name}: Retrying in ${interval} seconds...`,
227
- ),
225
+ if (retryCount < 5) {
226
+ const interval = Math.min(2 * 2 ** retryCount, 16);
227
+ console.error(
228
+ styleText(
229
+ "yellow",
230
+ `${message.name}: Retrying in ${interval} seconds...`,
231
+ ),
232
+ );
233
+ await new Promise((resolve) => setTimeout(resolve, interval * 1000));
234
+ return modelCaller(config, input, retryCount + 1);
235
+ }
236
+ return new Error(
237
+ `${message.name}: Retry limit (${retryCount}) exceeded: ${message.message}`,
228
238
  );
229
- await new Promise((resolve) => setTimeout(resolve, interval * 1000));
230
- return modelCaller(config, input, retryCount + 1);
231
239
  }
232
240
 
233
241
  // Create context cache for next request
@@ -68,7 +68,7 @@ export async function callOpenAIModel(
68
68
  });
69
69
 
70
70
  const retryInterval = Math.min(2 * 2 ** retryCount, 16);
71
- if (response.status === 429 || response.status >= 500) {
71
+ if ((response.status === 429 || response.status >= 500) && retryCount < 5) {
72
72
  console.error(
73
73
  styleText(
74
74
  "yellow",
@@ -179,7 +179,7 @@ export async function callOpenAICompatibleModel(
179
179
  maxAttempt: 5,
180
180
  });
181
181
 
182
- if (response.status === 429 || response.status >= 500) {
182
+ if ((response.status === 429 || response.status >= 500) && retryCount < 5) {
183
183
  console.error(
184
184
  styleText(
185
185
  "yellow",
@@ -236,18 +236,25 @@ export async function callOpenAICompatibleModel(
236
236
 
237
237
  const chatCompletion = convertOpenAIStreamDataToChatCompletion(dataList);
238
238
  if (chatCompletion instanceof Error) {
239
- console.error(
240
- styleText(
241
- "yellow",
242
- `Failed to process stream: ${chatCompletion.message}; Retry in ${retryInterval} seconds...`,
243
- ),
244
- );
245
- await new Promise((resolve) => setTimeout(resolve, retryInterval * 1000));
246
- return callOpenAICompatibleModel(
247
- platformConfig,
248
- modelConfig,
249
- input,
250
- retryCount + 1,
239
+ if (retryCount < 5) {
240
+ console.error(
241
+ styleText(
242
+ "yellow",
243
+ `Failed to process stream: ${chatCompletion.message}; Retry in ${retryInterval} seconds...`,
244
+ ),
245
+ );
246
+ await new Promise((resolve) =>
247
+ setTimeout(resolve, retryInterval * 1000),
248
+ );
249
+ return callOpenAICompatibleModel(
250
+ platformConfig,
251
+ modelConfig,
252
+ input,
253
+ retryCount + 1,
254
+ );
255
+ }
256
+ throw new Error(
257
+ `Failed to process OpenAI compatible stream after ${retryCount} retries: ${chatCompletion.message}`,
251
258
  );
252
259
  }
253
260
 
package/src/tool.d.ts CHANGED
@@ -10,6 +10,20 @@ export type Tool = {
10
10
  injectImpl?: (impl: ToolImplementation) => void;
11
11
  };
12
12
 
13
+ export type SandboxMode = "sandbox" | "unsandboxed" | null;
14
+
15
+ /**
16
+ * Implemented by tools that can report the sandbox mode for a given input.
17
+ * - `null` if the tool has no sandbox configuration
18
+ * - `"unsandboxed"` if a sandbox rule matched with `mode: "unsandboxed"`
19
+ * - `"sandbox"` otherwise (sandbox config exists, default execution is sandboxed)
20
+ * Used by the CLI to display an `[unsandboxed]` badge for tool calls that
21
+ * will execute outside the sandbox.
22
+ */
23
+ export type SandboxModeProvider = {
24
+ getSandboxMode: (input: unknown) => SandboxMode;
25
+ };
26
+
13
27
  export type ToolDefinition = {
14
28
  name: string;
15
29
  description: string;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @import { Tool } from '../tool'
2
+ * @import { Tool, SandboxModeProvider } from '../tool'
3
3
  * @import { ExecCommandConfig, ExecCommandInput, ExecCommandSanboxConfig } from './execCommand'
4
4
  */
5
5
 
@@ -13,10 +13,10 @@ const OUTPUT_TRUNCATED_LENGTH = 1024 * 2;
13
13
 
14
14
  /**
15
15
  * @param {ExecCommandConfig=} config
16
- * @returns {Tool}
16
+ * @returns {Tool & SandboxModeProvider}
17
17
  */
18
18
  export function createExecCommandTool(config) {
19
- /** @type {Tool} */
19
+ /** @type {Tool & SandboxModeProvider} */
20
20
  return {
21
21
  def: {
22
22
  name: "exec_command",
@@ -190,6 +190,21 @@ Examples:
190
190
  child.stdin?.end();
191
191
  });
192
192
  }),
193
+
194
+ /**
195
+ * Report the sandbox mode for a given tool input. Mirrors
196
+ * `rewriteInputForSandbox`'s rule-matching logic so the CLI can preview
197
+ * the mode before execution.
198
+ * @param {unknown} input
199
+ * @returns {"sandbox" | "unsandboxed" | null}
200
+ */
201
+ getSandboxMode: (input) => {
202
+ if (!config?.sandbox) return null;
203
+ const matchedRule = (config.sandbox.rules || []).find((rule) =>
204
+ matchValue(/** @type {ExecCommandInput} */ (input), rule.pattern),
205
+ );
206
+ return matchedRule?.mode === "unsandboxed" ? "unsandboxed" : "sandbox";
207
+ },
193
208
  };
194
209
  }
195
210