@iinm/plain-agent 1.6.1 → 1.7.1

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
@@ -66,7 +66,7 @@ Create the configuration.
66
66
  {
67
67
  // Requires Azure CLI to get access token
68
68
  "name": "azure",
69
- "variant": "default",
69
+ "variant": "openai",
70
70
  "baseURL": "https://<resource>.openai.azure.com/openai",
71
71
  // Optional
72
72
  "azureConfigDir": "/home/xxx/.azure-for-agent"
@@ -131,21 +131,27 @@ Create the configuration.
131
131
  {
132
132
  "platforms": [
133
133
  {
134
- "name": "openai",
134
+ "name": "openai-compatible",
135
135
  "variant": "ollama",
136
136
  "baseURL": "https://ollama.com",
137
137
  "apiKey": "FIXME"
138
138
  },
139
139
  {
140
- "name": "openai",
140
+ "name": "openai-compatible",
141
141
  "variant": "huggingface",
142
142
  "baseURL": "https://router.huggingface.co",
143
143
  "apiKey": "FIXME"
144
144
  },
145
145
  {
146
- "name": "openai",
146
+ "name": "openai-compatible",
147
147
  "variant": "xai",
148
148
  "apiKey": "FIXME"
149
+ },
150
+ {
151
+ "name": "openai-compatible",
152
+ "variant": "fireworks",
153
+ "baseURL": "https://api.fireworks.ai/inference",
154
+ "apiKey": "FIXME"
149
155
  }
150
156
  ]
151
157
  }
@@ -238,9 +244,10 @@ Run in batch mode (non-interactive).
238
244
  In batch mode, config files are not loaded automatically. Only the files specified with `--config` are loaded.
239
245
 
240
246
  ```sh
241
- plain batch "Add tests for src/main.mjs" \
242
- --config ~/.config/plain-agent/config.local.json \
243
- --config .plain-agent/config.json
247
+ plain batch \
248
+ -c ~/.config/plain-agent/config.local.json \
249
+ -c .plain-agent/config.json \
250
+ "Add tests for ..."
244
251
  ```
245
252
 
246
253
  Display the help message.
@@ -249,11 +256,9 @@ Display the help message.
249
256
  /help
250
257
  ```
251
258
 
252
- Interrupt the agent while it's running by providing additional instructions:
259
+ Interrupt the agent while it's running:
253
260
 
254
- ```sh
255
- plain-interrupt Stop and report the progress
256
- ```
261
+ Press **Ctrl-C** to pause auto-approve. The agent will finish the current tool call, then return to the prompt.
257
262
 
258
263
  ## Available Tools
259
264
 
@@ -281,7 +286,6 @@ The agent can use the following tools to assist with tasks:
281
286
  \__ .plain-agent/
282
287
  \__ config.json # Project-specific configuration
283
288
  \__ config.local.json # Project-specific local configuration (including secrets)
284
- \__ interrupt-message.txt # Interrupt message consumed by the agent
285
289
  \__ memory/ # Task-specific memory files
286
290
  \__ prompts/ # Project-specific prompts
287
291
  \__ agents/ # Project-specific agent roles
@@ -891,7 +891,7 @@
891
891
  "variant": "thinking-medium-azure",
892
892
  "platform": {
893
893
  "name": "azure",
894
- "variant": "default"
894
+ "variant": "openai"
895
895
  },
896
896
  "model": {
897
897
  "format": "openai-responses",
@@ -972,10 +972,33 @@
972
972
  }
973
973
  },
974
974
  {
975
- "name": "glm-5",
975
+ "name": "glm-5.1",
976
+ "variant": "fireworks",
977
+ "platform": {
978
+ "name": "openai-compatible",
979
+ "variant": "fireworks"
980
+ },
981
+ "model": {
982
+ "format": "openai-messages",
983
+ "config": {
984
+ "model": "accounts/fireworks/models/glm-5p1"
985
+ }
986
+ },
987
+ "cost": {
988
+ "currency": "USD",
989
+ "unit": "1M",
990
+ "costs": {
991
+ "prompt_tokens": 1.4,
992
+ "prompt_tokens_details.cached_tokens": -1.14,
993
+ "completion_tokens": 4.4
994
+ }
995
+ }
996
+ },
997
+ {
998
+ "name": "glm-5.1",
976
999
  "variant": "ollama",
977
1000
  "platform": {
978
- "name": "openai",
1001
+ "name": "openai-compatible",
979
1002
  "variant": "ollama"
980
1003
  },
981
1004
  "model": {
@@ -986,16 +1009,16 @@
986
1009
  }
987
1010
  },
988
1011
  {
989
- "name": "glm-5",
1012
+ "name": "glm-5.1",
990
1013
  "variant": "huggingface",
991
1014
  "platform": {
992
- "name": "openai",
1015
+ "name": "openai-compatible",
993
1016
  "variant": "huggingface"
994
1017
  },
995
1018
  "model": {
996
1019
  "format": "openai-messages",
997
1020
  "config": {
998
- "model": "zai-org/GLM-5:novita"
1021
+ "model": "zai-org/GLM-5.1:together"
999
1022
  }
1000
1023
  }
1001
1024
  },
@@ -1066,7 +1089,7 @@
1066
1089
  "name": "minimax-m2.7",
1067
1090
  "variant": "ollama",
1068
1091
  "platform": {
1069
- "name": "openai",
1092
+ "name": "openai-compatible",
1070
1093
  "variant": "ollama"
1071
1094
  },
1072
1095
  "model": {
@@ -1091,6 +1114,58 @@
1091
1114
  }
1092
1115
  }
1093
1116
  },
1117
+ {
1118
+ "name": "qwen3.6-plus",
1119
+ "variant": "fireworks",
1120
+ "platform": {
1121
+ "name": "openai-compatible",
1122
+ "variant": "fireworks"
1123
+ },
1124
+ "model": {
1125
+ "format": "openai-messages",
1126
+ "config": {
1127
+ "model": "accounts/fireworks/models/qwen3p6-plus"
1128
+ }
1129
+ },
1130
+ "cost": {
1131
+ "currency": "USD",
1132
+ "unit": "1M",
1133
+ "costs": {
1134
+ "prompt_tokens": 0.5,
1135
+ "prompt_tokens_details.cached_tokens": -0.4,
1136
+ "completion_tokens": 3
1137
+ }
1138
+ }
1139
+ },
1140
+
1141
+ {
1142
+ "name": "gemma4",
1143
+ "variant": "ollama",
1144
+ "platform": {
1145
+ "name": "openai-compatible",
1146
+ "variant": "ollama"
1147
+ },
1148
+ "model": {
1149
+ "format": "openai-responses",
1150
+ "config": {
1151
+ "model": "gemma4:31b-cloud"
1152
+ }
1153
+ }
1154
+ },
1155
+ {
1156
+ "name": "gemma4",
1157
+ "variant": "huggingface",
1158
+ "platform": {
1159
+ "name": "openai-compatible",
1160
+ "variant": "huggingface"
1161
+ },
1162
+ "model": {
1163
+ "format": "openai-messages",
1164
+ "config": {
1165
+ "model": "google/gemma-4-31B-it:together"
1166
+ }
1167
+ }
1168
+ },
1094
1169
 
1095
1170
  {
1096
1171
  "name": "nova-2-lite",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iinm/plain-agent",
3
- "version": "1.6.1",
3
+ "version": "1.7.1",
4
4
  "description": "A lightweight CLI-based coding agent",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -18,7 +18,7 @@
18
18
  "files": [
19
19
  "bin",
20
20
  "sandbox/bin",
21
- ".config",
21
+ "config",
22
22
  "src/**/*.mjs",
23
23
  "src/**/*.d.ts",
24
24
  "!src/**/*.test.mjs",
package/src/agent.d.ts CHANGED
@@ -21,6 +21,7 @@ export type AgentCommands = {
21
21
  dumpMessages: () => Promise<void>;
22
22
  loadMessages: () => Promise<void>;
23
23
  getCostSummary: () => CostSummary;
24
+ pauseAutoApprove: () => void;
24
25
  };
25
26
 
26
27
  type UserEventMap = {
package/src/agent.mjs CHANGED
@@ -144,6 +144,16 @@ export function createAgent({
144
144
  }
145
145
  }
146
146
 
147
+ // Pause signal: set by Ctrl-C during agent execution, checked after each tool batch completes
148
+ let paused = false;
149
+ /** @type {import("./agentLoop.mjs").PauseSignal} */
150
+ const pauseSignal = {
151
+ isPaused: () => paused,
152
+ reset: () => {
153
+ paused = false;
154
+ },
155
+ };
156
+
147
157
  const agentLoop = createAgentLoop({
148
158
  callModel,
149
159
  stateManager,
@@ -152,6 +162,7 @@ export function createAgent({
152
162
  agentEventEmitter,
153
163
  toolUseApprover,
154
164
  subagentManager,
165
+ pauseSignal,
155
166
  });
156
167
 
157
168
  userEventEmitter.on("userInput", agentLoop.handleUserInput);
@@ -163,6 +174,9 @@ export function createAgent({
163
174
  dumpMessages,
164
175
  loadMessages,
165
176
  getCostSummary: () => costTracker.calculateCost(),
177
+ pauseAutoApprove: () => {
178
+ paused = true;
179
+ },
166
180
  },
167
181
  };
168
182
  }
package/src/agentLoop.mjs CHANGED
@@ -7,7 +7,12 @@
7
7
  */
8
8
 
9
9
  import { styleText } from "node:util";
10
- import { consumeInterruptMessage } from "./context/consumeInterruptMessage.mjs";
10
+
11
+ /**
12
+ * @typedef {Object} PauseSignal
13
+ * @property {() => boolean} isPaused - Returns true if auto-approve should be paused
14
+ * @property {() => void} reset - Resets the paused state
15
+ */
11
16
 
12
17
  /**
13
18
  * @typedef {Object} AgentLoopConfig
@@ -18,6 +23,7 @@ import { consumeInterruptMessage } from "./context/consumeInterruptMessage.mjs";
18
23
  * @property {AgentEventEmitter} agentEventEmitter - Event emitter for agent events
19
24
  * @property {ToolUseApprover} toolUseApprover - Tool use approval checker
20
25
  * @property {SubagentManager} subagentManager - Subagent manager instance
26
+ * @property {PauseSignal} pauseSignal - Signal to pause auto-approve after current tool completes
21
27
  */
22
28
 
23
29
  /**
@@ -36,6 +42,7 @@ export function createAgentLoop({
36
42
  agentEventEmitter,
37
43
  toolUseApprover,
38
44
  subagentManager,
45
+ pauseSignal,
39
46
  }) {
40
47
  const inputHandler = createInputHandler({
41
48
  stateManager,
@@ -198,14 +205,10 @@ export function createAgentLoop({
198
205
  stateManager.appendMessages([{ role: "user", content: toolResults }]);
199
206
  }
200
207
 
201
- const interruptMessage = await consumeInterruptMessage();
202
- if (interruptMessage) {
203
- stateManager.appendMessages([
204
- {
205
- role: "user",
206
- content: [{ type: "text", text: interruptMessage }],
207
- },
208
- ]);
208
+ // Check if auto-approve was paused by Ctrl-C during tool execution
209
+ if (pauseSignal.isPaused()) {
210
+ pauseSignal.reset();
211
+ break;
209
212
  }
210
213
  }
211
214
  }
package/src/cliArgs.mjs CHANGED
@@ -125,10 +125,12 @@ Usage: plain [options]
125
125
  Options:
126
126
  -m, --model <model+variant> Model to use
127
127
  -h, --help Show this help message
128
- -c, --config <file> Config file to load
128
+ -c, --config <file> Config file to load (repeatable)
129
129
 
130
130
  Subcommands:
131
- batch <task> Run in batch mode with the given task instruction
131
+ batch <task> Run in batch mode with the given task instruction.
132
+ Config files are NOT auto-loaded in batch mode;
133
+ use -c to specify config files explicitly.
132
134
  list-models List available models
133
135
  install-claude-code-plugins Install Claude Code plugins
134
136
 
@@ -137,9 +139,7 @@ Examples:
137
139
  plain batch \\
138
140
  -c ~/.config/plain-agent/config.local.json \\
139
141
  -c .plain-agent/config.json \\
140
- "Add tests for src/main.mjs"
141
- plain list-models
142
- plain install-claude-code-plugins
142
+ "Add tests for ..."
143
143
  `);
144
144
  process.exit(exitCode);
145
145
  }
@@ -200,8 +200,8 @@ export function createCompleter(getCliRef, claudeCodePlugins) {
200
200
  const name = typeof cmd === "string" ? cmd : cmd.name;
201
201
  return (
202
202
  name !== "/<id>" &&
203
- (name === "/agents:" || !name.startsWith("/agent:")) &&
204
- (name === "/prompts:" || !name.startsWith("/prompt:"))
203
+ (name === "/agents:" || !name.startsWith("/agents:")) &&
204
+ (name === "/prompts:" || !name.startsWith("/prompts:"))
205
205
  );
206
206
  },
207
207
  );
@@ -16,7 +16,6 @@ import {
16
16
  createPasteTransform,
17
17
  resolvePastePlaceholders,
18
18
  } from "./cliPasteTransform.mjs";
19
- import { consumeInterruptMessage } from "./context/consumeInterruptMessage.mjs";
20
19
  import { notify } from "./utils/notify.mjs";
21
20
 
22
21
  const HELP_MESSAGE = [
@@ -121,7 +120,19 @@ export function startInteractiveSession({
121
120
  let lastExitAttempt = 0;
122
121
  const EXIT_CONFIRM_TIMEOUT = 1500;
123
122
 
124
- const confirmExit = () => {
123
+ const handleCtrlC = () => {
124
+ // If agent is running, pause auto-approve instead of exiting
125
+ if (!state.turn) {
126
+ agentCommands.pauseAutoApprove();
127
+ console.log(
128
+ styleText(
129
+ "yellow",
130
+ "\n⚠ Ctrl-C: Auto-approve paused. Finishing current tool...",
131
+ ),
132
+ );
133
+ return;
134
+ }
135
+
125
136
  const now = Date.now();
126
137
  if (now - lastExitAttempt < EXIT_CONFIRM_TIMEOUT) {
127
138
  handleExit();
@@ -132,7 +143,7 @@ export function startInteractiveSession({
132
143
  };
133
144
 
134
145
  // Create a transform stream to handle bracketed paste before readline
135
- const pasteTransform = createPasteTransform(confirmExit);
146
+ const pasteTransform = createPasteTransform(handleCtrlC);
136
147
 
137
148
  // Set up transformed stdin for readline
138
149
  process.stdin.pipe(pasteTransform);
@@ -198,7 +209,6 @@ export function startInteractiveSession({
198
209
  }
199
210
 
200
211
  cli.setPrompt(currentCliPrompt);
201
- await consumeInterruptMessage();
202
212
 
203
213
  const result = await handleCommand(inputTrimmed);
204
214
  if (result === "prompt") {
@@ -53,10 +53,10 @@ export function resolvePastePlaceholders(input) {
53
53
 
54
54
  /**
55
55
  * Create a Transform stream to handle bracketed paste before readline.
56
- * @param {() => void} onExitRequest - Called when Ctrl-C or Ctrl-D is detected
56
+ * @param {() => void} onCtrlC - Called when Ctrl-C or Ctrl-D is detected
57
57
  * @returns {Transform}
58
58
  */
59
- export function createPasteTransform(onExitRequest) {
59
+ export function createPasteTransform(onCtrlC) {
60
60
  let inPasteMode = false;
61
61
  let pasteBuffer = "";
62
62
 
@@ -67,8 +67,7 @@ export function createPasteTransform(onExitRequest) {
67
67
 
68
68
  // Handle Ctrl-C and Ctrl-D
69
69
  if (data.includes("\x03") || data.includes("\x04")) {
70
- // Ctrl-C / Ctrl-D: request exit (handled by confirmExit)
71
- onExitRequest();
70
+ onCtrlC();
72
71
  callback();
73
72
  return;
74
73
  }
package/src/config.mjs CHANGED
@@ -34,7 +34,7 @@ export async function loadAppConfig(options = {}) {
34
34
  } = options;
35
35
 
36
36
  // Always load predefined config
37
- const paths = [`${AGENT_ROOT}/.config/config.predefined.json`];
37
+ const paths = [`${AGENT_ROOT}/config/config.predefined.json`];
38
38
 
39
39
  if (!skipUserConfig) {
40
40
  paths.push(
@@ -30,7 +30,7 @@ export async function loadAgentRoles(claudeCodePlugins) {
30
30
  /** @type {Array<{dir: string, idPrefix: string, only?: RegExp}>} */
31
31
  const agentDirs = [
32
32
  {
33
- dir: path.resolve(AGENT_ROOT, ".config", "agents.predefined"),
33
+ dir: path.resolve(AGENT_ROOT, "config", "agents.predefined"),
34
34
  idPrefix: "",
35
35
  },
36
36
  { dir: path.resolve(AGENT_USER_CONFIG_DIR, "agents"), idPrefix: "" },
@@ -34,7 +34,7 @@ export async function loadPrompts(claudeCodePlugins) {
34
34
  /** @type {Array<{dir: string, idPrefix: string, only?: RegExp}>} */
35
35
  const promptDirs = [
36
36
  {
37
- dir: path.resolve(AGENT_ROOT, ".config", "prompts.predefined"),
37
+ dir: path.resolve(AGENT_ROOT, "config", "prompts.predefined"),
38
38
  idPrefix: "",
39
39
  },
40
40
  { dir: path.resolve(AGENT_USER_CONFIG_DIR, "prompts"), idPrefix: "" },
package/src/env.mjs CHANGED
@@ -38,9 +38,4 @@ export const AGENT_NOTIFY_CMD_DEFAULT = path.join(
38
38
  "plain-notify-terminal-bell",
39
39
  );
40
40
 
41
- export const AGENT_INTERRUPT_MESSAGE_FILE_PATH = path.join(
42
- AGENT_PROJECT_METADATA_DIR,
43
- "interrupt-message.txt",
44
- );
45
-
46
41
  export const USER_NAME = process.env.USER || "unknown";
@@ -34,6 +34,13 @@ export type PlatformConfig =
34
34
  customHeaders?: Record<string, string>;
35
35
  apiKey: string;
36
36
  }
37
+ | {
38
+ name: "openai-compatible";
39
+ variant: string;
40
+ baseURL: string;
41
+ customHeaders?: Record<string, string>;
42
+ apiKey: string;
43
+ }
37
44
  | {
38
45
  name: "azure";
39
46
  variant: string;
@@ -36,6 +36,7 @@ export async function callOpenAICompatibleModel(
36
36
 
37
37
  switch (platformConfig.name) {
38
38
  case "openai":
39
+ case "openai-compatible":
39
40
  return `${baseURL}/v1/chat/completions`;
40
41
  case "bedrock":
41
42
  return `${baseURL}/model/${modelConfig.model}/invoke-with-response-stream`;
@@ -50,6 +51,7 @@ export async function callOpenAICompatibleModel(
50
51
  const headers = await (async () => {
51
52
  switch (platformConfig.name) {
52
53
  case "openai":
54
+ case "openai-compatible":
53
55
  return {
54
56
  ...platformConfig.customHeaders,
55
57
  Authorization: `Bearer ${platformConfig.apiKey}`,
@@ -68,6 +70,7 @@ export async function callOpenAICompatibleModel(
68
70
  const platformRequest = (() => {
69
71
  switch (platformConfig.name) {
70
72
  case "openai":
73
+ case "openai-compatible":
71
74
  return {
72
75
  ...modelConfig,
73
76
  stream: true,
package/src/subagent.mjs CHANGED
@@ -25,6 +25,7 @@ import { reportAsSubagentToolName } from "./tools/reportAsSubagent.mjs";
25
25
  export function createSubagentManager(agentRoles, handlers) {
26
26
  /** @type {{name: string; goal: string; delegationMessageIndex: number}[]} */
27
27
  const subagents = [];
28
+ let subagentCount = 0;
28
29
 
29
30
  /**
30
31
  * @typedef {DelegateSuccess | DelegateFailure} DelegateResult
@@ -79,6 +80,9 @@ export function createSubagentManager(agentRoles, handlers) {
79
80
  : role.content;
80
81
  }
81
82
 
83
+ subagentCount++;
84
+ const sequenceNumber = String(subagentCount).padStart(2, "0");
85
+
82
86
  subagents.push({
83
87
  name: actualName,
84
88
  goal,
@@ -89,11 +93,11 @@ export function createSubagentManager(agentRoles, handlers) {
89
93
  return {
90
94
  success: true,
91
95
  value: [
92
- `✓ Delegation successful. You are now the subagent "${actualName}".`,
96
+ `You are now the subagent "${actualName}". Start working on the following goal.`,
93
97
  `Your goal: ${goal}`,
94
98
  `Role: ${actualName}\n---\n${roleContent}\n---`,
95
- `Memory file path format: ${AGENT_PROJECT_METADATA_DIR}/memory/<session-id>--${actualName}--<kebab-case-title>.md (Replace <kebab-case-title> to match the parent task)`,
96
- `Start working on this goal now. When finished, call "report_as_subagent" with the memory file path.`,
99
+ `Memory file path format: ${AGENT_PROJECT_METADATA_DIR}/memory/<session-id>--${sequenceNumber}--${actualName}--<kebab-case-title>.md (Replace <kebab-case-title> with a short title describing your own goal)`,
100
+ `When finished, call "report_as_subagent" with the memory file path.`,
97
101
  ].join("\n\n"),
98
102
  };
99
103
  }
@@ -1,6 +0,0 @@
1
- #!/usr/bin/env bash
2
-
3
- set -eu -o pipefail
4
-
5
- mkdir -p .plain-agent
6
- echo -e "$@" >> .plain-agent/interrupt-message.txt
@@ -1,30 +0,0 @@
1
- import fs from "node:fs/promises";
2
- import { AGENT_INTERRUPT_MESSAGE_FILE_PATH } from "../env.mjs";
3
-
4
- /**
5
- * @returns {Promise<string | undefined>}
6
- */
7
- export async function consumeInterruptMessage() {
8
- try {
9
- const content = await fs.readFile(
10
- AGENT_INTERRUPT_MESSAGE_FILE_PATH,
11
- "utf8",
12
- );
13
- await fs.truncate(AGENT_INTERRUPT_MESSAGE_FILE_PATH, 0);
14
-
15
- if (content.trim() === "") {
16
- return undefined;
17
- }
18
- return content;
19
- } catch (err) {
20
- if (
21
- err instanceof Error &&
22
- "code" in err &&
23
- typeof err.code === "string" &&
24
- err.code === "ENOENT"
25
- ) {
26
- return undefined;
27
- }
28
- throw err;
29
- }
30
- }
File without changes