@iinm/plain-agent 1.0.7 → 1.1.0

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
@@ -141,7 +141,16 @@ Run the agent.
141
141
  plain
142
142
 
143
143
  # Or
144
- plain -m <model>+<variant>
144
+ plain -m <model+variant>
145
+ ```
146
+
147
+ Run in batch mode (non-interactive).
148
+ In batch mode, config files are not loaded automatically. Only the files specified with `--config` are loaded.
149
+
150
+ ```sh
151
+ plain --batch "Add tests for src/main.mjs" \
152
+ --config ~/.config/plain-agent/config.local.json \
153
+ --config .plain-agent/config.json
145
154
  ```
146
155
 
147
156
  Display the help message.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iinm/plain-agent",
3
- "version": "1.0.7",
3
+ "version": "1.1.0",
4
4
  "description": "A lightweight CLI-based coding agent",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/cliArgs.mjs CHANGED
@@ -3,6 +3,8 @@
3
3
  * @property {string|null} model - Model name with variant
4
4
  * @property {boolean} showHelp - Whether to show help message
5
5
  * @property {boolean} listModels - Whether to list available models
6
+ * @property {string|null} batch - Task instruction for batch mode
7
+ * @property {string[]} config - Paths to additional config files for batch mode
6
8
  */
7
9
 
8
10
  /**
@@ -13,15 +15,25 @@
13
15
  export function parseCliArgs(argv) {
14
16
  const args = argv.slice(2);
15
17
  /** @type {CliArgs} */
16
- const result = { model: null, showHelp: false, listModels: false };
18
+ const result = {
19
+ model: null,
20
+ showHelp: false,
21
+ listModels: false,
22
+ batch: null,
23
+ config: [],
24
+ };
17
25
 
18
26
  for (let i = 0; i < args.length; i++) {
19
- if (args[i] === "-m" && args[i + 1]) {
27
+ if ((args[i] === "-m" || args[i] === "--model") && args[i + 1]) {
20
28
  result.model = args[++i];
21
29
  } else if (args[i] === "-h" || args[i] === "--help") {
22
30
  result.showHelp = true;
23
31
  } else if (args[i] === "-l" || args[i] === "--list-models") {
24
32
  result.listModels = true;
33
+ } else if (args[i] === "--batch" && args[i + 1]) {
34
+ result.batch = args[++i];
35
+ } else if (args[i] === "--config" && args[i + 1]) {
36
+ result.config.push(args[++i]);
25
37
  }
26
38
  }
27
39
 
@@ -35,15 +47,21 @@ export function parseCliArgs(argv) {
35
47
  export function printHelp(exitCode = 0) {
36
48
  console.log(`
37
49
  Usage: agent [options]
50
+ agent --batch "task instruction" [options]
38
51
 
39
52
  Options:
40
- -m <model+variant> Model to use
41
- -l, --list-models List available models
53
+ -m, --model <model+variant> Model to use
54
+ -l, --list-models List available models
42
55
  -h, --help Show this help message
56
+ --batch <task> Run in batch mode with the given task instruction
57
+ --config <file> Config file to load (required in batch mode)
58
+ In batch mode, only explicitly specified config files are loaded
43
59
 
44
60
  Examples:
45
- agent -m claude-sonnet-4-6+thinking-16k
46
61
  agent -m gpt-5.4+thinking-medium
62
+ plain --batch "Add tests for src/main.mjs" \\
63
+ --config ~/.config/plain-agent/config.local.json \\
64
+ --config .plain-agent/config.json
47
65
  `);
48
66
  process.exit(exitCode);
49
67
  }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * @import { UserEventEmitter, AgentEventEmitter } from "./agent"
3
+ */
4
+
5
+ /**
6
+ * @typedef {object} BatchSessionOptions
7
+ * @property {UserEventEmitter} userEventEmitter
8
+ * @property {AgentEventEmitter} agentEventEmitter
9
+ * @property {string} task - Task instruction to execute
10
+ * @property {string} sessionId
11
+ * @property {string} modelName
12
+ * @property {boolean} sandbox
13
+ * @property {() => Promise<void>} onStop
14
+ */
15
+
16
+ /**
17
+ * Start a batch session and execute the task.
18
+ * Events are output as JSON Lines (1 line = 1 JSON object).
19
+ *
20
+ * @param {BatchSessionOptions} options
21
+ * @returns {Promise<void>}
22
+ */
23
+ export async function startBatchSession({
24
+ userEventEmitter,
25
+ agentEventEmitter,
26
+ task,
27
+ sessionId,
28
+ modelName,
29
+ sandbox,
30
+ onStop,
31
+ }) {
32
+ setupEventHandlers(agentEventEmitter, { sessionId, modelName, sandbox });
33
+
34
+ userEventEmitter.emit("userInput", [{ type: "text", text: task }]);
35
+
36
+ await new Promise((/** @type {(value?: void) => void} */ resolve) => {
37
+ agentEventEmitter.on("turnEnd", async () => {
38
+ outputEvent({
39
+ type: "session_end",
40
+ timestamp: new Date().toISOString(),
41
+ });
42
+ await onStop();
43
+ resolve();
44
+ });
45
+ });
46
+
47
+ process.exit(0);
48
+ }
49
+
50
+ /**
51
+ * Setup event handlers for batch mode.
52
+ * Output events as JSON Lines.
53
+ *
54
+ * @param {AgentEventEmitter} agentEventEmitter
55
+ * @param {{ sessionId: string, modelName: string, sandbox: boolean }} meta
56
+ */
57
+ function setupEventHandlers(
58
+ agentEventEmitter,
59
+ { sessionId, modelName, sandbox },
60
+ ) {
61
+ outputEvent({
62
+ type: "session_start",
63
+ sessionId,
64
+ modelName,
65
+ sandbox,
66
+ timestamp: new Date().toISOString(),
67
+ });
68
+
69
+ agentEventEmitter.on("message", (message) => {
70
+ outputEvent({
71
+ type: "message",
72
+ message,
73
+ timestamp: new Date().toISOString(),
74
+ });
75
+ });
76
+
77
+ agentEventEmitter.on("error", (error) => {
78
+ outputEvent({
79
+ type: "error",
80
+ error: {
81
+ message: error.message,
82
+ stack: error.stack,
83
+ },
84
+ timestamp: new Date().toISOString(),
85
+ });
86
+
87
+ process.exit(1);
88
+ });
89
+
90
+ agentEventEmitter.on("subagentSwitched", (subagent) => {
91
+ outputEvent({
92
+ type: "subagent_switched",
93
+ subagent,
94
+ timestamp: new Date().toISOString(),
95
+ });
96
+ });
97
+
98
+ agentEventEmitter.on("providerTokenUsage", (usage) => {
99
+ outputEvent({
100
+ type: "token_usage",
101
+ usage,
102
+ timestamp: new Date().toISOString(),
103
+ });
104
+ });
105
+ }
106
+
107
+ /**
108
+ * Output an event as JSON Lines format.
109
+ * Each event is a single line of JSON.
110
+ *
111
+ * @param {object} event
112
+ */
113
+ function outputEvent(event) {
114
+ console.log(JSON.stringify(event));
115
+ }
package/src/config.mjs CHANGED
@@ -16,19 +16,37 @@ 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
+ * @typedef {Object} LoadAppConfigOptions
20
+ * @property {boolean} [skipTrustCheck] - Skip trust check for config files
21
+ * @property {string[]} [configFiles] - Additional config files to load (for batch mode)
22
+ * @property {boolean} [skipUserConfig] - Skip default user config files (for batch mode)
23
+ */
24
+
25
+ /**
26
+ * @param {LoadAppConfigOptions} [options]
21
27
  * @returns {Promise<{appConfig: AppConfig, loadedConfigPath: string[]}>}
22
28
  */
23
29
  export async function loadAppConfig(options = {}) {
24
- const { skipTrustCheck = false } = options;
25
- const paths = [
26
- `${AGENT_ROOT}/.config/config.predefined.json`,
27
- `${AGENT_USER_CONFIG_DIR}/config.json`,
28
- `${AGENT_USER_CONFIG_DIR}/config.local.json`,
29
- `${AGENT_PROJECT_METADATA_DIR}/config.json`,
30
- `${AGENT_PROJECT_METADATA_DIR}/config.local.json`,
31
- ];
30
+ const {
31
+ skipTrustCheck = false,
32
+ configFiles = [],
33
+ skipUserConfig = false,
34
+ } = options;
35
+
36
+ // Always load predefined config
37
+ const paths = [`${AGENT_ROOT}/.config/config.predefined.json`];
38
+
39
+ if (!skipUserConfig) {
40
+ paths.push(
41
+ `${AGENT_USER_CONFIG_DIR}/config.json`,
42
+ `${AGENT_USER_CONFIG_DIR}/config.local.json`,
43
+ `${AGENT_PROJECT_METADATA_DIR}/config.json`,
44
+ `${AGENT_PROJECT_METADATA_DIR}/config.local.json`,
45
+ );
46
+ }
47
+
48
+ // Add explicitly specified config files
49
+ paths.push(...configFiles);
32
50
 
33
51
  /** @type {string[]} */
34
52
  const loadedConfigPath = [];
package/src/main.mjs CHANGED
@@ -5,6 +5,7 @@
5
5
  import { styleText } from "node:util";
6
6
  import { createAgent } from "./agent.mjs";
7
7
  import { parseCliArgs, printHelp } from "./cliArgs.mjs";
8
+ import { startBatchSession } from "./cliBatch.mjs";
8
9
  import { startInteractiveSession } from "./cliInteractive.mjs";
9
10
  import { loadAppConfig } from "./config.mjs";
10
11
  import { loadAgentRoles } from "./context/loadAgentRoles.mjs";
@@ -56,22 +57,31 @@ if (cliArgs.listModels) {
56
57
  `0${startTime.getMinutes()}`.slice(-2),
57
58
  ].join("-");
58
59
  const tmuxSessionId = `agent-${sessionId}`;
59
- const { appConfig, loadedConfigPath } = await loadAppConfig();
60
+ const isBatchMode = Boolean(cliArgs.batch);
60
61
 
61
- if (loadedConfigPath.length > 0) {
62
- console.log(styleText("green", "\n⚡ Loaded configuration files"));
63
- console.log(loadedConfigPath.map((p) => ` ⤷ ${p}`).join("\n"));
64
- }
62
+ const { appConfig, loadedConfigPath } = await loadAppConfig({
63
+ skipUserConfig: isBatchMode,
64
+ skipTrustCheck: isBatchMode,
65
+ configFiles: cliArgs.config,
66
+ });
65
67
 
66
- if (appConfig.sandbox) {
67
- const sandboxStr = [
68
- appConfig.sandbox.command,
69
- ...(appConfig.sandbox.args || []),
70
- ].join(" ");
71
- console.log(styleText("green", "\n📦 Sandbox: on"));
72
- console.log(` ⤷ ${sandboxStr}`);
73
- } else {
74
- console.log(styleText("yellow", "\n📦 Sandbox: off"));
68
+ // In batch mode, skip human-readable output
69
+ if (!isBatchMode) {
70
+ if (loadedConfigPath.length > 0) {
71
+ console.log(styleText("green", "\n⚡ Loaded configuration files"));
72
+ console.log(loadedConfigPath.map((p) => ` ⤷ ${p}`).join("\n"));
73
+ }
74
+
75
+ if (appConfig.sandbox) {
76
+ const sandboxStr = [
77
+ appConfig.sandbox.command,
78
+ ...(appConfig.sandbox.args || []),
79
+ ].join(" ");
80
+ console.log(styleText("green", "\n📦 Sandbox: on"));
81
+ console.log(` ⤷ ${sandboxStr}`);
82
+ } else {
83
+ console.log(styleText("yellow", "\n📦 Sandbox: off"));
84
+ }
75
85
  }
76
86
 
77
87
  /** @type {(() => Promise<void>)[]} */
@@ -82,11 +92,13 @@ if (cliArgs.listModels) {
82
92
  if (appConfig.mcpServers) {
83
93
  const mcpServerEntries = Object.entries(appConfig.mcpServers);
84
94
 
85
- console.log();
86
- for (const [serverName] of mcpServerEntries) {
87
- console.log(
88
- styleText("blue", `🔌 Connecting to MCP server: ${serverName}...`),
89
- );
95
+ if (!isBatchMode) {
96
+ console.log();
97
+ for (const [serverName] of mcpServerEntries) {
98
+ console.log(
99
+ styleText("blue", `🔌 Connecting to MCP server: ${serverName}...`),
100
+ );
101
+ }
90
102
  }
91
103
 
92
104
  const mcpResults = await Promise.all(
@@ -99,12 +111,14 @@ if (cliArgs.listModels) {
99
111
  for (const { serverName, tools, cleanup } of mcpResults) {
100
112
  mcpTools.push(...tools);
101
113
  mcpCleanups.push(cleanup);
102
- console.log(
103
- styleText(
104
- "green",
105
- `✅ Successfully connected to MCP server: ${serverName}`,
106
- ),
107
- );
114
+ if (!isBatchMode) {
115
+ console.log(
116
+ styleText(
117
+ "green",
118
+ `✅ Successfully connected to MCP server: ${serverName}`,
119
+ ),
120
+ );
121
+ }
108
122
  }
109
123
  }
110
124
 
@@ -196,21 +210,35 @@ if (cliArgs.listModels) {
196
210
  agentRoles,
197
211
  });
198
212
 
199
- startInteractiveSession({
213
+ const sessionOptions = {
200
214
  userEventEmitter,
201
215
  agentEventEmitter,
202
216
  agentCommands,
203
217
  sessionId,
204
218
  modelName: modelNameWithVariant,
205
- notifyCmd: appConfig.notifyCmd || AGENT_NOTIFY_CMD_DEFAULT,
206
219
  sandbox: Boolean(appConfig.sandbox),
207
220
  onStop: async () => {
208
221
  for (const cleanup of mcpCleanups) {
209
222
  await cleanup();
210
223
  }
211
224
  },
212
- claudeCodePlugins: appConfig.claudeCodePlugins,
213
- });
225
+ };
226
+
227
+ if (isBatchMode) {
228
+ if (!cliArgs.batch) {
229
+ throw new Error("Batch task is required in batch mode");
230
+ }
231
+ await startBatchSession({
232
+ ...sessionOptions,
233
+ task: cliArgs.batch,
234
+ });
235
+ } else {
236
+ startInteractiveSession({
237
+ ...sessionOptions,
238
+ notifyCmd: appConfig.notifyCmd || AGENT_NOTIFY_CMD_DEFAULT,
239
+ claudeCodePlugins: appConfig.claudeCodePlugins,
240
+ });
241
+ }
214
242
  })().catch((err) => {
215
243
  console.error(err);
216
244
  process.exit(1);