@link-assistant/agent 0.4.0 → 0.5.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
@@ -218,6 +218,9 @@ Stdin Mode Options:
218
218
  --no-interactive Only accept JSON input
219
219
  --auto-merge-queued-messages Merge rapidly arriving lines (default: true)
220
220
  --no-auto-merge-queued-messages Treat each line as separate message
221
+ --always-accept-stdin Keep accepting input after agent finishes (default: true)
222
+ --no-always-accept-stdin Single-message mode - exit after first response
223
+ --compact-json Output compact JSON for program-to-program use
221
224
 
222
225
  --help Show help
223
226
  --version Show version number
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/agent",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "A minimal, public domain AI CLI agent compatible with OpenCode's JSON interface. Bun-only runtime.",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -0,0 +1,450 @@
1
+ /**
2
+ * Continuous stdin mode for the Agent CLI.
3
+ * Keeps the session alive and processes messages as they arrive.
4
+ */
5
+
6
+ import { Server } from '../server/server.ts';
7
+ import { Instance } from '../project/instance.ts';
8
+ import { Bus } from '../bus/index.ts';
9
+ import { Session } from '../session/index.ts';
10
+ import { SessionPrompt } from '../session/prompt.ts';
11
+ import { createEventHandler } from '../json-standard/index.ts';
12
+ import { createContinuousStdinReader } from './input-queue.js';
13
+
14
+ // Shared error tracking
15
+ let hasError = false;
16
+
17
+ /**
18
+ * Set error state
19
+ * @param {boolean} value - Error state value
20
+ */
21
+ export function setHasError(value) {
22
+ hasError = value;
23
+ }
24
+
25
+ /**
26
+ * Get error state
27
+ * @returns {boolean}
28
+ */
29
+ export function getHasError() {
30
+ return hasError;
31
+ }
32
+
33
+ /**
34
+ * Output JSON status message to stderr
35
+ * @param {object} status - Status object to output
36
+ * @param {boolean} compact - If true, output compact JSON (single line)
37
+ */
38
+ function outputStatus(status, compact = false) {
39
+ const json = compact
40
+ ? JSON.stringify(status)
41
+ : JSON.stringify(status, null, 2);
42
+ console.error(json);
43
+ }
44
+
45
+ /**
46
+ * Run server mode with continuous stdin input
47
+ * Keeps the session alive and processes messages as they arrive
48
+ */
49
+ export async function runContinuousServerMode(
50
+ argv,
51
+ providerID,
52
+ modelID,
53
+ systemMessage,
54
+ appendSystemMessage,
55
+ jsonStandard
56
+ ) {
57
+ const compactJson = argv['compact-json'] === true;
58
+ const isInteractive = argv.interactive !== false;
59
+ const autoMerge = argv['auto-merge-queued-messages'] !== false;
60
+
61
+ // Start server like OpenCode does
62
+ const server = Server.listen({ port: 0, hostname: '127.0.0.1' });
63
+ let unsub = null;
64
+ let stdinReader = null;
65
+
66
+ try {
67
+ // Create a session
68
+ const createRes = await fetch(
69
+ `http://${server.hostname}:${server.port}/session`,
70
+ {
71
+ method: 'POST',
72
+ headers: { 'Content-Type': 'application/json' },
73
+ body: JSON.stringify({}),
74
+ }
75
+ );
76
+ const session = await createRes.json();
77
+ const sessionID = session.id;
78
+
79
+ if (!sessionID) {
80
+ throw new Error('Failed to create session');
81
+ }
82
+
83
+ // Create event handler for the selected JSON standard
84
+ const eventHandler = createEventHandler(jsonStandard, sessionID);
85
+
86
+ // Track if we're currently processing a message
87
+ let isProcessing = false;
88
+ const pendingMessages = [];
89
+
90
+ // Process messages from the queue
91
+ const processMessage = async (message) => {
92
+ if (isProcessing) {
93
+ pendingMessages.push(message);
94
+ return;
95
+ }
96
+
97
+ isProcessing = true;
98
+ const messageText = message.message || 'hi';
99
+ const parts = [{ type: 'text', text: messageText }];
100
+
101
+ // Create a promise to wait for this message to complete
102
+ const messagePromise = new Promise((resolve) => {
103
+ const checkIdle = Bus.subscribeAll((event) => {
104
+ if (
105
+ event.type === 'session.idle' &&
106
+ event.properties.sessionID === sessionID
107
+ ) {
108
+ checkIdle();
109
+ resolve();
110
+ }
111
+ });
112
+ });
113
+
114
+ // Send message
115
+ fetch(
116
+ `http://${server.hostname}:${server.port}/session/${sessionID}/message`,
117
+ {
118
+ method: 'POST',
119
+ headers: { 'Content-Type': 'application/json' },
120
+ body: JSON.stringify({
121
+ parts,
122
+ model: { providerID, modelID },
123
+ system: systemMessage,
124
+ appendSystem: appendSystemMessage,
125
+ }),
126
+ }
127
+ ).catch((error) => {
128
+ hasError = true;
129
+ eventHandler.output({
130
+ type: 'error',
131
+ timestamp: Date.now(),
132
+ sessionID,
133
+ error: error instanceof Error ? error.message : String(error),
134
+ });
135
+ });
136
+
137
+ await messagePromise;
138
+ isProcessing = false;
139
+
140
+ // Process next pending message if any
141
+ if (pendingMessages.length > 0) {
142
+ const nextMessage = pendingMessages.shift();
143
+ processMessage(nextMessage);
144
+ }
145
+ };
146
+
147
+ // Subscribe to all bus events and output in selected format
148
+ unsub = Bus.subscribeAll((event) => {
149
+ if (event.type === 'message.part.updated') {
150
+ const part = event.properties.part;
151
+ if (part.sessionID !== sessionID) {
152
+ return;
153
+ }
154
+
155
+ if (part.type === 'step-start') {
156
+ eventHandler.output({
157
+ type: 'step_start',
158
+ timestamp: Date.now(),
159
+ sessionID,
160
+ part,
161
+ });
162
+ }
163
+
164
+ if (part.type === 'step-finish') {
165
+ eventHandler.output({
166
+ type: 'step_finish',
167
+ timestamp: Date.now(),
168
+ sessionID,
169
+ part,
170
+ });
171
+ }
172
+
173
+ if (part.type === 'text' && part.time?.end) {
174
+ eventHandler.output({
175
+ type: 'text',
176
+ timestamp: Date.now(),
177
+ sessionID,
178
+ part,
179
+ });
180
+ }
181
+
182
+ if (part.type === 'tool' && part.state.status === 'completed') {
183
+ eventHandler.output({
184
+ type: 'tool_use',
185
+ timestamp: Date.now(),
186
+ sessionID,
187
+ part,
188
+ });
189
+ }
190
+ }
191
+
192
+ if (event.type === 'session.error') {
193
+ const props = event.properties;
194
+ if (props.sessionID !== sessionID || !props.error) {
195
+ return;
196
+ }
197
+ hasError = true;
198
+ eventHandler.output({
199
+ type: 'error',
200
+ timestamp: Date.now(),
201
+ sessionID,
202
+ error: props.error,
203
+ });
204
+ }
205
+ });
206
+
207
+ // Create continuous stdin reader
208
+ stdinReader = createContinuousStdinReader({
209
+ interactive: isInteractive,
210
+ autoMerge,
211
+ onMessage: (message) => {
212
+ processMessage(message);
213
+ },
214
+ });
215
+
216
+ // Wait for stdin to end (EOF or close)
217
+ await new Promise((resolve) => {
218
+ const checkRunning = setInterval(() => {
219
+ if (!stdinReader.isRunning()) {
220
+ clearInterval(checkRunning);
221
+ // Wait for any pending messages to complete
222
+ const waitForPending = () => {
223
+ if (!isProcessing && pendingMessages.length === 0) {
224
+ resolve();
225
+ } else {
226
+ setTimeout(waitForPending, 100);
227
+ }
228
+ };
229
+ waitForPending();
230
+ }
231
+ }, 100);
232
+
233
+ // Also handle SIGINT
234
+ process.on('SIGINT', () => {
235
+ outputStatus(
236
+ {
237
+ type: 'status',
238
+ message: 'Received SIGINT. Shutting down...',
239
+ },
240
+ compactJson
241
+ );
242
+ clearInterval(checkRunning);
243
+ resolve();
244
+ });
245
+ });
246
+ } finally {
247
+ if (stdinReader) {
248
+ stdinReader.stop();
249
+ }
250
+ if (unsub) {
251
+ unsub();
252
+ }
253
+ server.stop();
254
+ await Instance.dispose();
255
+ }
256
+ }
257
+
258
+ /**
259
+ * Run direct mode with continuous stdin input
260
+ * Keeps the session alive and processes messages as they arrive
261
+ */
262
+ export async function runContinuousDirectMode(
263
+ argv,
264
+ providerID,
265
+ modelID,
266
+ systemMessage,
267
+ appendSystemMessage,
268
+ jsonStandard
269
+ ) {
270
+ const compactJson = argv['compact-json'] === true;
271
+ const isInteractive = argv.interactive !== false;
272
+ const autoMerge = argv['auto-merge-queued-messages'] !== false;
273
+
274
+ let unsub = null;
275
+ let stdinReader = null;
276
+
277
+ try {
278
+ // Create a session directly
279
+ const session = await Session.createNext({
280
+ directory: process.cwd(),
281
+ });
282
+ const sessionID = session.id;
283
+
284
+ // Create event handler for the selected JSON standard
285
+ const eventHandler = createEventHandler(jsonStandard, sessionID);
286
+
287
+ // Track if we're currently processing a message
288
+ let isProcessing = false;
289
+ const pendingMessages = [];
290
+
291
+ // Process messages from the queue
292
+ const processMessage = async (message) => {
293
+ if (isProcessing) {
294
+ pendingMessages.push(message);
295
+ return;
296
+ }
297
+
298
+ isProcessing = true;
299
+ const messageText = message.message || 'hi';
300
+ const parts = [{ type: 'text', text: messageText }];
301
+
302
+ // Create a promise to wait for this message to complete
303
+ const messagePromise = new Promise((resolve) => {
304
+ const checkIdle = Bus.subscribeAll((event) => {
305
+ if (
306
+ event.type === 'session.idle' &&
307
+ event.properties.sessionID === sessionID
308
+ ) {
309
+ checkIdle();
310
+ resolve();
311
+ }
312
+ });
313
+ });
314
+
315
+ // Send message directly
316
+ SessionPrompt.prompt({
317
+ sessionID,
318
+ parts,
319
+ model: { providerID, modelID },
320
+ system: systemMessage,
321
+ appendSystem: appendSystemMessage,
322
+ }).catch((error) => {
323
+ hasError = true;
324
+ eventHandler.output({
325
+ type: 'error',
326
+ timestamp: Date.now(),
327
+ sessionID,
328
+ error: error instanceof Error ? error.message : String(error),
329
+ });
330
+ });
331
+
332
+ await messagePromise;
333
+ isProcessing = false;
334
+
335
+ // Process next pending message if any
336
+ if (pendingMessages.length > 0) {
337
+ const nextMessage = pendingMessages.shift();
338
+ processMessage(nextMessage);
339
+ }
340
+ };
341
+
342
+ // Subscribe to all bus events and output in selected format
343
+ unsub = Bus.subscribeAll((event) => {
344
+ if (event.type === 'message.part.updated') {
345
+ const part = event.properties.part;
346
+ if (part.sessionID !== sessionID) {
347
+ return;
348
+ }
349
+
350
+ if (part.type === 'step-start') {
351
+ eventHandler.output({
352
+ type: 'step_start',
353
+ timestamp: Date.now(),
354
+ sessionID,
355
+ part,
356
+ });
357
+ }
358
+
359
+ if (part.type === 'step-finish') {
360
+ eventHandler.output({
361
+ type: 'step_finish',
362
+ timestamp: Date.now(),
363
+ sessionID,
364
+ part,
365
+ });
366
+ }
367
+
368
+ if (part.type === 'text' && part.time?.end) {
369
+ eventHandler.output({
370
+ type: 'text',
371
+ timestamp: Date.now(),
372
+ sessionID,
373
+ part,
374
+ });
375
+ }
376
+
377
+ if (part.type === 'tool' && part.state.status === 'completed') {
378
+ eventHandler.output({
379
+ type: 'tool_use',
380
+ timestamp: Date.now(),
381
+ sessionID,
382
+ part,
383
+ });
384
+ }
385
+ }
386
+
387
+ if (event.type === 'session.error') {
388
+ const props = event.properties;
389
+ if (props.sessionID !== sessionID || !props.error) {
390
+ return;
391
+ }
392
+ hasError = true;
393
+ eventHandler.output({
394
+ type: 'error',
395
+ timestamp: Date.now(),
396
+ sessionID,
397
+ error: props.error,
398
+ });
399
+ }
400
+ });
401
+
402
+ // Create continuous stdin reader
403
+ stdinReader = createContinuousStdinReader({
404
+ interactive: isInteractive,
405
+ autoMerge,
406
+ onMessage: (message) => {
407
+ processMessage(message);
408
+ },
409
+ });
410
+
411
+ // Wait for stdin to end (EOF or close)
412
+ await new Promise((resolve) => {
413
+ const checkRunning = setInterval(() => {
414
+ if (!stdinReader.isRunning()) {
415
+ clearInterval(checkRunning);
416
+ // Wait for any pending messages to complete
417
+ const waitForPending = () => {
418
+ if (!isProcessing && pendingMessages.length === 0) {
419
+ resolve();
420
+ } else {
421
+ setTimeout(waitForPending, 100);
422
+ }
423
+ };
424
+ waitForPending();
425
+ }
426
+ }, 100);
427
+
428
+ // Also handle SIGINT
429
+ process.on('SIGINT', () => {
430
+ outputStatus(
431
+ {
432
+ type: 'status',
433
+ message: 'Received SIGINT. Shutting down...',
434
+ },
435
+ compactJson
436
+ );
437
+ clearInterval(checkRunning);
438
+ resolve();
439
+ });
440
+ });
441
+ } finally {
442
+ if (stdinReader) {
443
+ stdinReader.stop();
444
+ }
445
+ if (unsub) {
446
+ unsub();
447
+ }
448
+ await Instance.dispose();
449
+ }
450
+ }
package/src/index.js CHANGED
@@ -18,6 +18,10 @@ import { AuthCommand } from './cli/cmd/auth.ts';
18
18
  import { Flag } from './flag/flag.ts';
19
19
  import { FormatError } from './cli/error.ts';
20
20
  import { UI } from './cli/ui.ts';
21
+ import {
22
+ runContinuousServerMode,
23
+ runContinuousDirectMode,
24
+ } from './cli/continuous-mode.js';
21
25
  import { createRequire } from 'module';
22
26
  import { readFileSync } from 'fs';
23
27
  import { dirname, join } from 'path';
@@ -132,30 +136,21 @@ function readStdinWithTimeout(timeout = null) {
132
136
  * Output JSON status message to stderr
133
137
  * This prevents the status message from interfering with JSON output parsing
134
138
  * @param {object} status - Status object to output
139
+ * @param {boolean} compact - If true, output compact JSON (single line)
135
140
  */
136
- function outputStatus(status) {
137
- console.error(JSON.stringify(status));
141
+ function outputStatus(status, compact = false) {
142
+ const json = compact
143
+ ? JSON.stringify(status)
144
+ : JSON.stringify(status, null, 2);
145
+ console.error(json);
138
146
  }
139
147
 
140
- async function runAgentMode(argv, request) {
141
- // Note: verbose flag and logging are now initialized in middleware
142
- // See main() function for the middleware that sets up Flag and Log.init()
143
-
144
- // Log version and command info in verbose mode
145
- if (Flag.OPENCODE_VERBOSE) {
146
- console.error(`Agent version: ${pkg.version}`);
147
- console.error(`Command: ${process.argv.join(' ')}`);
148
- console.error(`Working directory: ${process.cwd()}`);
149
- console.error(`Script path: ${import.meta.path}`);
150
- }
151
-
152
- // Log dry-run mode if enabled
153
- if (Flag.OPENCODE_DRY_RUN) {
154
- console.error(
155
- `[DRY RUN MODE] No actual API calls or package installations will be made`
156
- );
157
- }
158
-
148
+ /**
149
+ * Parse model configuration from argv
150
+ * @param {object} argv - Command line arguments
151
+ * @returns {object} - { providerID, modelID }
152
+ */
153
+ async function parseModelConfig(argv) {
159
154
  // Parse model argument (handle model IDs with slashes like groq/qwen/qwen3-32b)
160
155
  const modelParts = argv.model.split('/');
161
156
  let providerID = modelParts[0] || 'opencode';
@@ -170,13 +165,15 @@ async function runAgentMode(argv, request) {
170
165
  const creds = await ClaudeOAuth.getCredentials();
171
166
 
172
167
  if (!creds?.accessToken) {
173
- console.error(
174
- JSON.stringify({
168
+ const compactJson = argv['compact-json'] === true;
169
+ outputStatus(
170
+ {
175
171
  type: 'error',
176
172
  errorType: 'AuthenticationError',
177
173
  message:
178
174
  'No Claude OAuth credentials found in ~/.claude/.credentials.json. Either authenticate with Claude Code CLI first, or use: agent auth login (select Anthropic > Claude Pro/Max)',
179
- })
175
+ },
176
+ compactJson
180
177
  );
181
178
  process.exit(1);
182
179
  }
@@ -191,26 +188,27 @@ async function runAgentMode(argv, request) {
191
188
  modelID = 'claude-sonnet-4-5';
192
189
  } else if (!['claude-oauth', 'anthropic'].includes(providerID)) {
193
190
  // If user specified a different provider, warn them
194
- console.error(
195
- JSON.stringify({
191
+ const compactJson = argv['compact-json'] === true;
192
+ outputStatus(
193
+ {
196
194
  type: 'warning',
197
195
  message: `--use-existing-claude-oauth is set but model uses provider "${providerID}". Using OAuth credentials anyway.`,
198
- })
196
+ },
197
+ compactJson
199
198
  );
200
199
  providerID = 'claude-oauth';
201
200
  }
202
201
  }
203
202
 
204
- // Validate and get JSON standard
205
- const jsonStandard = argv['json-standard'];
206
- if (!isValidJsonStandard(jsonStandard)) {
207
- console.error(
208
- `Invalid JSON standard: ${jsonStandard}. Use "opencode" or "claude".`
209
- );
210
- process.exit(1);
211
- }
203
+ return { providerID, modelID };
204
+ }
212
205
 
213
- // Read system message files
206
+ /**
207
+ * Read system message from files if specified
208
+ * @param {object} argv - Command line arguments
209
+ * @returns {object} - { systemMessage, appendSystemMessage }
210
+ */
211
+ async function readSystemMessages(argv) {
214
212
  let systemMessage = argv['system-message'];
215
213
  let appendSystemMessage = argv['append-system-message'];
216
214
 
@@ -244,6 +242,41 @@ async function runAgentMode(argv, request) {
244
242
  appendSystemMessage = await file.text();
245
243
  }
246
244
 
245
+ return { systemMessage, appendSystemMessage };
246
+ }
247
+
248
+ async function runAgentMode(argv, request) {
249
+ // Note: verbose flag and logging are now initialized in middleware
250
+ // See main() function for the middleware that sets up Flag and Log.init()
251
+
252
+ // Log version and command info in verbose mode
253
+ if (Flag.OPENCODE_VERBOSE) {
254
+ console.error(`Agent version: ${pkg.version}`);
255
+ console.error(`Command: ${process.argv.join(' ')}`);
256
+ console.error(`Working directory: ${process.cwd()}`);
257
+ console.error(`Script path: ${import.meta.path}`);
258
+ }
259
+
260
+ // Log dry-run mode if enabled
261
+ if (Flag.OPENCODE_DRY_RUN) {
262
+ console.error(
263
+ `[DRY RUN MODE] No actual API calls or package installations will be made`
264
+ );
265
+ }
266
+
267
+ const { providerID, modelID } = await parseModelConfig(argv);
268
+
269
+ // Validate and get JSON standard
270
+ const jsonStandard = argv['json-standard'];
271
+ if (!isValidJsonStandard(jsonStandard)) {
272
+ console.error(
273
+ `Invalid JSON standard: ${jsonStandard}. Use "opencode" or "claude".`
274
+ );
275
+ process.exit(1);
276
+ }
277
+
278
+ const { systemMessage, appendSystemMessage } = await readSystemMessages(argv);
279
+
247
280
  // Logging is already initialized in middleware, no need to call Log.init() again
248
281
 
249
282
  // Wrap in Instance.provide for OpenCode infrastructure
@@ -278,6 +311,81 @@ async function runAgentMode(argv, request) {
278
311
  process.exit(hasError ? 1 : 0);
279
312
  }
280
313
 
314
+ /**
315
+ * Run agent in continuous stdin mode
316
+ * Keeps accepting input until EOF or SIGINT
317
+ * @param {object} argv - Command line arguments
318
+ */
319
+ async function runContinuousAgentMode(argv) {
320
+ // Note: verbose flag and logging are now initialized in middleware
321
+ // See main() function for the middleware that sets up Flag and Log.init()
322
+
323
+ const compactJson = argv['compact-json'] === true;
324
+
325
+ // Log version and command info in verbose mode
326
+ if (Flag.OPENCODE_VERBOSE) {
327
+ console.error(`Agent version: ${pkg.version}`);
328
+ console.error(`Command: ${process.argv.join(' ')}`);
329
+ console.error(`Working directory: ${process.cwd()}`);
330
+ console.error(`Script path: ${import.meta.path}`);
331
+ }
332
+
333
+ // Log dry-run mode if enabled
334
+ if (Flag.OPENCODE_DRY_RUN) {
335
+ console.error(
336
+ `[DRY RUN MODE] No actual API calls or package installations will be made`
337
+ );
338
+ }
339
+
340
+ const { providerID, modelID } = await parseModelConfig(argv);
341
+
342
+ // Validate and get JSON standard
343
+ const jsonStandard = argv['json-standard'];
344
+ if (!isValidJsonStandard(jsonStandard)) {
345
+ outputStatus(
346
+ {
347
+ type: 'error',
348
+ message: `Invalid JSON standard: ${jsonStandard}. Use "opencode" or "claude".`,
349
+ },
350
+ compactJson
351
+ );
352
+ process.exit(1);
353
+ }
354
+
355
+ const { systemMessage, appendSystemMessage } = await readSystemMessages(argv);
356
+
357
+ // Wrap in Instance.provide for OpenCode infrastructure
358
+ await Instance.provide({
359
+ directory: process.cwd(),
360
+ fn: async () => {
361
+ if (argv.server) {
362
+ // SERVER MODE: Start server and communicate via HTTP
363
+ await runContinuousServerMode(
364
+ argv,
365
+ providerID,
366
+ modelID,
367
+ systemMessage,
368
+ appendSystemMessage,
369
+ jsonStandard
370
+ );
371
+ } else {
372
+ // DIRECT MODE: Run everything in single process
373
+ await runContinuousDirectMode(
374
+ argv,
375
+ providerID,
376
+ modelID,
377
+ systemMessage,
378
+ appendSystemMessage,
379
+ jsonStandard
380
+ );
381
+ }
382
+ },
383
+ });
384
+
385
+ // Explicitly exit to ensure process terminates
386
+ process.exit(hasError ? 1 : 0);
387
+ }
388
+
281
389
  async function runServerMode(
282
390
  request,
283
391
  providerID,
@@ -645,6 +753,18 @@ async function main() {
645
753
  'Enable interactive mode to accept manual input as plain text strings (default: true). Use --no-interactive to only accept JSON input.',
646
754
  default: true,
647
755
  })
756
+ .option('always-accept-stdin', {
757
+ type: 'boolean',
758
+ description:
759
+ 'Keep accepting stdin input even after the agent finishes work (default: true). Use --no-always-accept-stdin for single-message mode.',
760
+ default: true,
761
+ })
762
+ .option('compact-json', {
763
+ type: 'boolean',
764
+ description:
765
+ 'Output compact JSON (single line) instead of pretty-printed JSON (default: false). Useful for program-to-program communication.',
766
+ default: false,
767
+ })
648
768
  // Initialize logging early for all CLI commands
649
769
  // This prevents debug output from appearing in CLI unless --verbose is used
650
770
  .middleware(async (argv) => {
@@ -702,6 +822,8 @@ async function main() {
702
822
  const commandExecuted = argv._ && argv._.length > 0;
703
823
 
704
824
  if (!commandExecuted) {
825
+ const compactJson = argv['compact-json'] === true;
826
+
705
827
  // Check if --prompt flag was provided
706
828
  if (argv.prompt) {
707
829
  // Direct prompt mode - bypass stdin entirely
@@ -713,12 +835,15 @@ async function main() {
713
835
  // Check if --disable-stdin was set without --prompt
714
836
  if (argv['disable-stdin']) {
715
837
  // Output a helpful message suggesting to use --prompt
716
- outputStatus({
717
- type: 'error',
718
- message:
719
- 'No prompt provided. Use -p/--prompt to specify a message, or remove --disable-stdin to read from stdin.',
720
- hint: 'Example: agent -p "Hello, how are you?"',
721
- });
838
+ outputStatus(
839
+ {
840
+ type: 'error',
841
+ message:
842
+ 'No prompt provided. Use -p/--prompt to specify a message, or remove --disable-stdin to read from stdin.',
843
+ hint: 'Example: agent -p "Hello, how are you?"',
844
+ },
845
+ compactJson
846
+ );
722
847
  process.exit(1);
723
848
  }
724
849
 
@@ -733,32 +858,49 @@ async function main() {
733
858
  // Output status message to inform user what's happening
734
859
  const isInteractive = argv.interactive !== false;
735
860
  const autoMerge = argv['auto-merge-queued-messages'] !== false;
861
+ const alwaysAcceptStdin = argv['always-accept-stdin'] !== false;
736
862
 
737
- outputStatus({
738
- type: 'status',
739
- mode: 'stdin-stream',
740
- message:
741
- 'Agent CLI in stdin listening mode. Accepts JSON and plain text input.',
742
- hint: 'Press CTRL+C to exit. Use --help for options.',
743
- acceptedFormats: isInteractive
744
- ? ['JSON object with "message" field', 'Plain text']
745
- : ['JSON object with "message" field'],
746
- options: {
747
- interactive: isInteractive,
748
- autoMergeQueuedMessages: autoMerge,
863
+ outputStatus(
864
+ {
865
+ type: 'status',
866
+ mode: 'stdin-stream',
867
+ message: alwaysAcceptStdin
868
+ ? 'Agent CLI in continuous listening mode. Accepts JSON and plain text input.'
869
+ : 'Agent CLI in single-message mode. Accepts JSON and plain text input.',
870
+ hint: 'Press CTRL+C to exit. Use --help for options.',
871
+ acceptedFormats: isInteractive
872
+ ? ['JSON object with "message" field', 'Plain text']
873
+ : ['JSON object with "message" field'],
874
+ options: {
875
+ interactive: isInteractive,
876
+ autoMergeQueuedMessages: autoMerge,
877
+ alwaysAcceptStdin,
878
+ compactJson,
879
+ },
749
880
  },
750
- });
881
+ compactJson
882
+ );
751
883
 
884
+ // Use continuous mode if --always-accept-stdin is enabled (default)
885
+ if (alwaysAcceptStdin) {
886
+ await runContinuousAgentMode(argv);
887
+ return;
888
+ }
889
+
890
+ // Single-message mode (--no-always-accept-stdin)
752
891
  // Read stdin with optional timeout
753
892
  const timeout = argv['stdin-stream-timeout'] ?? null;
754
893
  const input = await readStdinWithTimeout(timeout);
755
894
  const trimmedInput = input.trim();
756
895
 
757
896
  if (trimmedInput === '') {
758
- outputStatus({
759
- type: 'status',
760
- message: 'No input received. Exiting.',
761
- });
897
+ outputStatus(
898
+ {
899
+ type: 'status',
900
+ message: 'No input received. Exiting.',
901
+ },
902
+ compactJson
903
+ );
762
904
  yargsInstance.showHelp();
763
905
  process.exit(0);
764
906
  }
@@ -771,12 +913,15 @@ async function main() {
771
913
  // Not JSON
772
914
  if (!isInteractive) {
773
915
  // In non-interactive mode, only accept JSON
774
- outputStatus({
775
- type: 'error',
776
- message:
777
- 'Invalid JSON input. In non-interactive mode (--no-interactive), only JSON input is accepted.',
778
- hint: 'Use --interactive to accept plain text, or provide valid JSON: {"message": "your text"}',
779
- });
916
+ outputStatus(
917
+ {
918
+ type: 'error',
919
+ message:
920
+ 'Invalid JSON input. In non-interactive mode (--no-interactive), only JSON input is accepted.',
921
+ hint: 'Use --interactive to accept plain text, or provide valid JSON: {"message": "your text"}',
922
+ },
923
+ compactJson
924
+ );
780
925
  process.exit(1);
781
926
  }
782
927
  // In interactive mode, treat as plain text message