@link-assistant/agent 0.22.10 → 0.23.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/package.json +1 -1
- package/src/cli/continuous-mode.js +115 -27
- package/src/cli/input-queue.js +169 -5
- package/src/cli/output.ts +1 -1
- package/src/cli/run-options.js +20 -0
- package/src/index.js +83 -23
package/package.json
CHANGED
|
@@ -8,7 +8,7 @@ import { Instance } from '../project/instance.ts';
|
|
|
8
8
|
import { Bus } from '../bus/index.ts';
|
|
9
9
|
import { Session } from '../session/index.ts';
|
|
10
10
|
import { SessionPrompt } from '../session/prompt.ts';
|
|
11
|
-
import { createEventHandler } from '../json-standard/index.ts';
|
|
11
|
+
import { createEventHandler, serializeOutput } from '../json-standard/index.ts';
|
|
12
12
|
import { createContinuousStdinReader } from './input-queue.js';
|
|
13
13
|
import { Log } from '../util/log.ts';
|
|
14
14
|
import { config } from '../config/config.ts';
|
|
@@ -40,6 +40,64 @@ export function getHasError() {
|
|
|
40
40
|
// Logger for resume operations
|
|
41
41
|
const log = Log.create({ service: 'resume' });
|
|
42
42
|
|
|
43
|
+
function getInputFormat(argv) {
|
|
44
|
+
return argv['input-format'] || argv.inputFormat || 'text';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function outputConsumedInput({
|
|
48
|
+
message,
|
|
49
|
+
jsonStandard,
|
|
50
|
+
sessionID,
|
|
51
|
+
compactJson,
|
|
52
|
+
}) {
|
|
53
|
+
const raw = message.raw || message.message || message.system || '';
|
|
54
|
+
if (
|
|
55
|
+
jsonStandard === 'claude' &&
|
|
56
|
+
message.kind === 'message' &&
|
|
57
|
+
message.format === 'stream-json'
|
|
58
|
+
) {
|
|
59
|
+
process.stdout.write(
|
|
60
|
+
serializeOutput(
|
|
61
|
+
{
|
|
62
|
+
type: 'message',
|
|
63
|
+
timestamp: new Date().toISOString(),
|
|
64
|
+
session_id: sessionID,
|
|
65
|
+
role: 'user',
|
|
66
|
+
content: [{ type: 'text', text: message.message ?? '' }],
|
|
67
|
+
},
|
|
68
|
+
'claude'
|
|
69
|
+
)
|
|
70
|
+
);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (jsonStandard === 'claude') {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
outputInput(
|
|
79
|
+
{
|
|
80
|
+
raw,
|
|
81
|
+
parsed: message.parsed || message,
|
|
82
|
+
format: message.format || 'text',
|
|
83
|
+
kind: message.kind || 'message',
|
|
84
|
+
},
|
|
85
|
+
compactJson
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function outputInputParseError(error, line, compactJson) {
|
|
90
|
+
hasError = true;
|
|
91
|
+
outputError(
|
|
92
|
+
{
|
|
93
|
+
errorType: 'ValidationError',
|
|
94
|
+
message: error instanceof Error ? error.message : String(error),
|
|
95
|
+
raw: line,
|
|
96
|
+
},
|
|
97
|
+
compactJson
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
43
101
|
/**
|
|
44
102
|
* Resolve the session to use based on --resume, --continue, and --no-fork options.
|
|
45
103
|
* Returns the session ID to use, handling forking as needed.
|
|
@@ -200,8 +258,12 @@ export async function runContinuousServerMode(
|
|
|
200
258
|
) {
|
|
201
259
|
// Check both CLI flag and environment variable for compact JSON mode
|
|
202
260
|
const compactJson = argv['compact-json'] === true || config.compactJson;
|
|
261
|
+
const inputFormat = getInputFormat(argv);
|
|
203
262
|
const isInteractive = argv.interactive !== false;
|
|
204
|
-
const autoMerge =
|
|
263
|
+
const autoMerge =
|
|
264
|
+
inputFormat === 'stream-json'
|
|
265
|
+
? false
|
|
266
|
+
: argv['auto-merge-queued-messages'] !== false;
|
|
205
267
|
|
|
206
268
|
// Start server like OpenCode does
|
|
207
269
|
const server = Server.listen({ port: 0, hostname: '127.0.0.1' });
|
|
@@ -244,9 +306,23 @@ export async function runContinuousServerMode(
|
|
|
244
306
|
// Track if we're currently processing a message
|
|
245
307
|
let isProcessing = false;
|
|
246
308
|
const pendingMessages = [];
|
|
309
|
+
let currentSystemMessage = systemMessage;
|
|
310
|
+
const currentAppendSystemMessage = appendSystemMessage;
|
|
247
311
|
|
|
248
312
|
// Process messages from the queue
|
|
249
313
|
const processMessage = async (message) => {
|
|
314
|
+
if (message.kind === 'interrupt') {
|
|
315
|
+
SessionPrompt.cancel(sessionID);
|
|
316
|
+
outputConsumedInput({ message, jsonStandard, sessionID, compactJson });
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (message.kind === 'system') {
|
|
321
|
+
currentSystemMessage = message.system;
|
|
322
|
+
outputConsumedInput({ message, jsonStandard, sessionID, compactJson });
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
250
326
|
if (isProcessing) {
|
|
251
327
|
pendingMessages.push(message);
|
|
252
328
|
return;
|
|
@@ -255,16 +331,9 @@ export async function runContinuousServerMode(
|
|
|
255
331
|
isProcessing = true;
|
|
256
332
|
|
|
257
333
|
// Output input confirmation in JSON format
|
|
258
|
-
|
|
259
|
-
{
|
|
260
|
-
raw: message.raw || message.message,
|
|
261
|
-
parsed: message,
|
|
262
|
-
format: message.format || 'text',
|
|
263
|
-
},
|
|
264
|
-
compactJson
|
|
265
|
-
);
|
|
334
|
+
outputConsumedInput({ message, jsonStandard, sessionID, compactJson });
|
|
266
335
|
|
|
267
|
-
const messageText = message.message
|
|
336
|
+
const messageText = message.message ?? 'hi';
|
|
268
337
|
const parts = [{ type: 'text', text: messageText }];
|
|
269
338
|
|
|
270
339
|
// Create a promise to wait for this message to complete
|
|
@@ -290,8 +359,8 @@ export async function runContinuousServerMode(
|
|
|
290
359
|
parts,
|
|
291
360
|
model: { providerID, modelID },
|
|
292
361
|
compactionModel,
|
|
293
|
-
system:
|
|
294
|
-
appendSystem:
|
|
362
|
+
system: currentSystemMessage,
|
|
363
|
+
appendSystem: currentAppendSystemMessage,
|
|
295
364
|
temperature,
|
|
296
365
|
}),
|
|
297
366
|
}
|
|
@@ -377,11 +446,15 @@ export async function runContinuousServerMode(
|
|
|
377
446
|
|
|
378
447
|
// Create continuous stdin reader
|
|
379
448
|
stdinReader = createContinuousStdinReader({
|
|
380
|
-
interactive: isInteractive,
|
|
449
|
+
interactive: inputFormat === 'stream-json' ? false : isInteractive,
|
|
381
450
|
autoMerge,
|
|
451
|
+
inputFormat,
|
|
382
452
|
onMessage: (message) => {
|
|
383
453
|
processMessage(message);
|
|
384
454
|
},
|
|
455
|
+
onError: (error, line) => {
|
|
456
|
+
outputInputParseError(error, line, compactJson);
|
|
457
|
+
},
|
|
385
458
|
});
|
|
386
459
|
|
|
387
460
|
// Wait for stdin to end (EOF or close)
|
|
@@ -454,8 +527,12 @@ export async function runContinuousDirectMode(
|
|
|
454
527
|
) {
|
|
455
528
|
// Check both CLI flag and environment variable for compact JSON mode
|
|
456
529
|
const compactJson = argv['compact-json'] === true || config.compactJson;
|
|
530
|
+
const inputFormat = getInputFormat(argv);
|
|
457
531
|
const isInteractive = argv.interactive !== false;
|
|
458
|
-
const autoMerge =
|
|
532
|
+
const autoMerge =
|
|
533
|
+
inputFormat === 'stream-json'
|
|
534
|
+
? false
|
|
535
|
+
: argv['auto-merge-queued-messages'] !== false;
|
|
459
536
|
|
|
460
537
|
let unsub = null;
|
|
461
538
|
let stdinReader = null;
|
|
@@ -483,9 +560,23 @@ export async function runContinuousDirectMode(
|
|
|
483
560
|
// Track if we're currently processing a message
|
|
484
561
|
let isProcessing = false;
|
|
485
562
|
const pendingMessages = [];
|
|
563
|
+
let currentSystemMessage = systemMessage;
|
|
564
|
+
const currentAppendSystemMessage = appendSystemMessage;
|
|
486
565
|
|
|
487
566
|
// Process messages from the queue
|
|
488
567
|
const processMessage = async (message) => {
|
|
568
|
+
if (message.kind === 'interrupt') {
|
|
569
|
+
SessionPrompt.cancel(sessionID);
|
|
570
|
+
outputConsumedInput({ message, jsonStandard, sessionID, compactJson });
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if (message.kind === 'system') {
|
|
575
|
+
currentSystemMessage = message.system;
|
|
576
|
+
outputConsumedInput({ message, jsonStandard, sessionID, compactJson });
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
|
|
489
580
|
if (isProcessing) {
|
|
490
581
|
pendingMessages.push(message);
|
|
491
582
|
return;
|
|
@@ -494,16 +585,9 @@ export async function runContinuousDirectMode(
|
|
|
494
585
|
isProcessing = true;
|
|
495
586
|
|
|
496
587
|
// Output input confirmation in JSON format
|
|
497
|
-
|
|
498
|
-
{
|
|
499
|
-
raw: message.raw || message.message,
|
|
500
|
-
parsed: message,
|
|
501
|
-
format: message.format || 'text',
|
|
502
|
-
},
|
|
503
|
-
compactJson
|
|
504
|
-
);
|
|
588
|
+
outputConsumedInput({ message, jsonStandard, sessionID, compactJson });
|
|
505
589
|
|
|
506
|
-
const messageText = message.message
|
|
590
|
+
const messageText = message.message ?? 'hi';
|
|
507
591
|
const parts = [{ type: 'text', text: messageText }];
|
|
508
592
|
|
|
509
593
|
// Create a promise to wait for this message to complete
|
|
@@ -525,8 +609,8 @@ export async function runContinuousDirectMode(
|
|
|
525
609
|
parts,
|
|
526
610
|
model: { providerID, modelID },
|
|
527
611
|
compactionModel,
|
|
528
|
-
system:
|
|
529
|
-
appendSystem:
|
|
612
|
+
system: currentSystemMessage,
|
|
613
|
+
appendSystem: currentAppendSystemMessage,
|
|
530
614
|
temperature,
|
|
531
615
|
}).catch((error) => {
|
|
532
616
|
hasError = true;
|
|
@@ -610,11 +694,15 @@ export async function runContinuousDirectMode(
|
|
|
610
694
|
|
|
611
695
|
// Create continuous stdin reader
|
|
612
696
|
stdinReader = createContinuousStdinReader({
|
|
613
|
-
interactive: isInteractive,
|
|
697
|
+
interactive: inputFormat === 'stream-json' ? false : isInteractive,
|
|
614
698
|
autoMerge,
|
|
699
|
+
inputFormat,
|
|
615
700
|
onMessage: (message) => {
|
|
616
701
|
processMessage(message);
|
|
617
702
|
},
|
|
703
|
+
onError: (error, line) => {
|
|
704
|
+
outputInputParseError(error, line, compactJson);
|
|
705
|
+
},
|
|
618
706
|
});
|
|
619
707
|
|
|
620
708
|
// Wait for stdin to end (EOF or close)
|
package/src/cli/input-queue.js
CHANGED
|
@@ -5,11 +5,13 @@
|
|
|
5
5
|
export class InputQueue {
|
|
6
6
|
constructor(options = {}) {
|
|
7
7
|
this.queue = [];
|
|
8
|
+
this.inputFormat = options.inputFormat || 'text';
|
|
8
9
|
this.autoMerge = options.autoMerge !== false; // enabled by default
|
|
9
10
|
this.mergeDelayMs = options.mergeDelayMs || 50; // delay to wait for more lines
|
|
10
11
|
this.pendingLines = [];
|
|
11
12
|
this.mergeTimer = null;
|
|
12
13
|
this.onMessage = options.onMessage || (() => {});
|
|
14
|
+
this.onError = options.onError || (() => {});
|
|
13
15
|
this.interactive = options.interactive !== false; // enabled by default
|
|
14
16
|
}
|
|
15
17
|
|
|
@@ -24,6 +26,10 @@ export class InputQueue {
|
|
|
24
26
|
return null;
|
|
25
27
|
}
|
|
26
28
|
|
|
29
|
+
if (this.inputFormat === 'stream-json') {
|
|
30
|
+
return parseStreamJsonInput(trimmed);
|
|
31
|
+
}
|
|
32
|
+
|
|
27
33
|
try {
|
|
28
34
|
const parsed = JSON.parse(trimmed);
|
|
29
35
|
// If it has a message field, use it directly
|
|
@@ -43,6 +49,11 @@ export class InputQueue {
|
|
|
43
49
|
* @param {string} line - Input line
|
|
44
50
|
*/
|
|
45
51
|
addLine(line) {
|
|
52
|
+
if (this.inputFormat === 'stream-json') {
|
|
53
|
+
this.queueLine(line);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
46
57
|
if (!this.interactive && !line.trim().startsWith('{')) {
|
|
47
58
|
// In non-interactive mode, only accept JSON
|
|
48
59
|
return;
|
|
@@ -54,11 +65,7 @@ export class InputQueue {
|
|
|
54
65
|
this.scheduleMerge();
|
|
55
66
|
} else {
|
|
56
67
|
// No merging, queue immediately
|
|
57
|
-
|
|
58
|
-
if (parsed) {
|
|
59
|
-
this.queue.push(parsed);
|
|
60
|
-
this.notifyMessage(parsed);
|
|
61
|
-
}
|
|
68
|
+
this.queueLine(line);
|
|
62
69
|
}
|
|
63
70
|
}
|
|
64
71
|
|
|
@@ -130,6 +137,33 @@ export class InputQueue {
|
|
|
130
137
|
}
|
|
131
138
|
}
|
|
132
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Notify listener of an input parse error
|
|
142
|
+
* @param {Error} error - Parse error
|
|
143
|
+
* @param {string} line - Raw input line
|
|
144
|
+
*/
|
|
145
|
+
notifyError(error, line) {
|
|
146
|
+
if (this.onError) {
|
|
147
|
+
this.onError(error, line);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Parse and enqueue one complete input line
|
|
153
|
+
* @param {string} line - Raw input line
|
|
154
|
+
*/
|
|
155
|
+
queueLine(line) {
|
|
156
|
+
try {
|
|
157
|
+
const parsed = this.parseInput(line);
|
|
158
|
+
if (parsed) {
|
|
159
|
+
this.queue.push(parsed);
|
|
160
|
+
this.notifyMessage(parsed);
|
|
161
|
+
}
|
|
162
|
+
} catch (error) {
|
|
163
|
+
this.notifyError(error, line);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
133
167
|
/**
|
|
134
168
|
* Get queue size
|
|
135
169
|
* @returns {number}
|
|
@@ -139,6 +173,136 @@ export class InputQueue {
|
|
|
139
173
|
}
|
|
140
174
|
}
|
|
141
175
|
|
|
176
|
+
function extractContentText(content) {
|
|
177
|
+
if (typeof content === 'string') {
|
|
178
|
+
return content;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (Array.isArray(content)) {
|
|
182
|
+
const parts = [];
|
|
183
|
+
for (const part of content) {
|
|
184
|
+
if (typeof part === 'string') {
|
|
185
|
+
parts.push(part);
|
|
186
|
+
} else if (part && typeof part === 'object') {
|
|
187
|
+
if (typeof part.text === 'string') {
|
|
188
|
+
parts.push(part.text);
|
|
189
|
+
} else if (typeof part.content === 'string') {
|
|
190
|
+
parts.push(part.content);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return parts.length > 0 ? parts.join('\n') : null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (content && typeof content === 'object') {
|
|
198
|
+
if (typeof content.text === 'string') {
|
|
199
|
+
return content.text;
|
|
200
|
+
}
|
|
201
|
+
if ('content' in content) {
|
|
202
|
+
return extractContentText(content.content);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function extractFrameText(frame) {
|
|
210
|
+
if (typeof frame.message === 'string') {
|
|
211
|
+
return frame.message;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (frame.message && typeof frame.message === 'object') {
|
|
215
|
+
const messageText = extractContentText(frame.message.content);
|
|
216
|
+
if (messageText !== null) {
|
|
217
|
+
return messageText;
|
|
218
|
+
}
|
|
219
|
+
if (typeof frame.message.text === 'string') {
|
|
220
|
+
return frame.message.text;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const contentText = extractContentText(frame.content);
|
|
225
|
+
if (contentText !== null) {
|
|
226
|
+
return contentText;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (typeof frame.text === 'string') {
|
|
230
|
+
return frame.text;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Parse one Claude-compatible stream-json input frame.
|
|
238
|
+
* @param {string} input - One JSONL frame
|
|
239
|
+
* @returns {object} Normalized queue message
|
|
240
|
+
*/
|
|
241
|
+
export function parseStreamJsonInput(input) {
|
|
242
|
+
let frame;
|
|
243
|
+
try {
|
|
244
|
+
frame = JSON.parse(input);
|
|
245
|
+
} catch (error) {
|
|
246
|
+
throw new Error(
|
|
247
|
+
`Invalid stream-json input frame: ${
|
|
248
|
+
error instanceof Error ? error.message : String(error)
|
|
249
|
+
}`
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (!frame || typeof frame !== 'object' || Array.isArray(frame)) {
|
|
254
|
+
throw new Error('Invalid stream-json input frame: expected JSON object');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const type = frame.type;
|
|
258
|
+
|
|
259
|
+
if (type === 'interrupt') {
|
|
260
|
+
return {
|
|
261
|
+
kind: 'interrupt',
|
|
262
|
+
raw: input,
|
|
263
|
+
parsed: frame,
|
|
264
|
+
format: 'stream-json',
|
|
265
|
+
inputType: type,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (type === 'system') {
|
|
270
|
+
const system = extractFrameText(frame);
|
|
271
|
+
if (system === null) {
|
|
272
|
+
throw new Error(
|
|
273
|
+
'Invalid stream-json system frame: expected content text'
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
return {
|
|
277
|
+
kind: 'system',
|
|
278
|
+
system,
|
|
279
|
+
raw: input,
|
|
280
|
+
parsed: frame,
|
|
281
|
+
format: 'stream-json',
|
|
282
|
+
inputType: type,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (type === 'user' || type === 'user_prompt' || type === undefined) {
|
|
287
|
+
const message = extractFrameText(frame);
|
|
288
|
+
if (message === null) {
|
|
289
|
+
throw new Error(
|
|
290
|
+
'Invalid stream-json user frame: expected message content text'
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
return {
|
|
294
|
+
kind: 'message',
|
|
295
|
+
message,
|
|
296
|
+
raw: input,
|
|
297
|
+
parsed: frame,
|
|
298
|
+
format: 'stream-json',
|
|
299
|
+
inputType: type || 'message',
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
throw new Error(`Unsupported stream-json input frame type: ${String(type)}`);
|
|
304
|
+
}
|
|
305
|
+
|
|
142
306
|
/**
|
|
143
307
|
* Create a continuous stdin reader that queues input lines
|
|
144
308
|
* @param {object} options - Options for the reader
|
package/src/cli/output.ts
CHANGED
package/src/cli/run-options.js
CHANGED
|
@@ -29,6 +29,19 @@ export function buildRunOptions(yargs, defaultOptions = {}) {
|
|
|
29
29
|
default: 'opencode',
|
|
30
30
|
choices: ['opencode', 'claude'],
|
|
31
31
|
})
|
|
32
|
+
.option('output-format', {
|
|
33
|
+
type: 'string',
|
|
34
|
+
description:
|
|
35
|
+
'Claude-compatible output format alias: "json" (OpenCode JSON) or "stream-json" (Claude NDJSON)',
|
|
36
|
+
choices: ['json', 'stream-json'],
|
|
37
|
+
})
|
|
38
|
+
.option('input-format', {
|
|
39
|
+
type: 'string',
|
|
40
|
+
description:
|
|
41
|
+
'Input format: "text" (default) or Claude-compatible "stream-json" JSONL frames',
|
|
42
|
+
default: 'text',
|
|
43
|
+
choices: ['text', 'stream-json'],
|
|
44
|
+
})
|
|
32
45
|
.option('system-message', {
|
|
33
46
|
type: 'string',
|
|
34
47
|
description: 'Full override of the system message',
|
|
@@ -135,6 +148,13 @@ export function buildRunOptions(yargs, defaultOptions = {}) {
|
|
|
135
148
|
argv['no-fork'] = true;
|
|
136
149
|
argv.noFork = true;
|
|
137
150
|
}
|
|
151
|
+
if (argv.outputFormat === 'stream-json') {
|
|
152
|
+
argv['json-standard'] = 'claude';
|
|
153
|
+
argv.jsonStandard = 'claude';
|
|
154
|
+
} else if (argv.outputFormat === 'json') {
|
|
155
|
+
argv['json-standard'] = 'opencode';
|
|
156
|
+
argv.jsonStandard = 'opencode';
|
|
157
|
+
}
|
|
138
158
|
}, true)
|
|
139
159
|
: parser;
|
|
140
160
|
|
package/src/index.js
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
runContinuousDirectMode,
|
|
37
37
|
resolveResumeSession,
|
|
38
38
|
} from './cli/continuous-mode.js';
|
|
39
|
+
import { parseStreamJsonInput } from './cli/input-queue.js';
|
|
39
40
|
import { createBusEventSubscription } from './cli/event-handler.js';
|
|
40
41
|
import {
|
|
41
42
|
outputStatus,
|
|
@@ -259,6 +260,19 @@ async function readSystemMessages(argv) {
|
|
|
259
260
|
return { systemMessage, appendSystemMessage };
|
|
260
261
|
}
|
|
261
262
|
|
|
263
|
+
function getInputFormat(argv) {
|
|
264
|
+
return argv['input-format'] || argv.inputFormat || 'text';
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function getAcceptedInputFormats(inputFormat, isInteractive) {
|
|
268
|
+
if (inputFormat === 'stream-json') {
|
|
269
|
+
return ['Claude stream-json user frames'];
|
|
270
|
+
}
|
|
271
|
+
return isInteractive
|
|
272
|
+
? ['JSON object with "message" field', 'Plain text']
|
|
273
|
+
: ['JSON object with "message" field'];
|
|
274
|
+
}
|
|
275
|
+
|
|
262
276
|
async function runAgentMode(argv, request) {
|
|
263
277
|
// Log version and command info in verbose mode using lazy logging
|
|
264
278
|
Log.Default.info(() => ({
|
|
@@ -663,8 +677,15 @@ async function main() {
|
|
|
663
677
|
// Check if stdin is a TTY (interactive terminal)
|
|
664
678
|
if (process.stdin.isTTY) {
|
|
665
679
|
// Enter interactive terminal mode with continuous listening
|
|
666
|
-
const
|
|
667
|
-
const
|
|
680
|
+
const inputFormat = getInputFormat(argv);
|
|
681
|
+
const isInteractive =
|
|
682
|
+
inputFormat === 'stream-json'
|
|
683
|
+
? false
|
|
684
|
+
: argv.interactive !== false;
|
|
685
|
+
const autoMerge =
|
|
686
|
+
inputFormat === 'stream-json'
|
|
687
|
+
? false
|
|
688
|
+
: argv['auto-merge-queued-messages'] !== false;
|
|
668
689
|
const alwaysAcceptStdin = argv['always-accept-stdin'] !== false;
|
|
669
690
|
|
|
670
691
|
// Exit if --no-always-accept-stdin is set (single message mode not supported in TTY)
|
|
@@ -688,10 +709,12 @@ async function main() {
|
|
|
688
709
|
message:
|
|
689
710
|
'Agent CLI in interactive terminal mode. Type your message and press Enter.',
|
|
690
711
|
hint: 'Press CTRL+C to exit. Use --help for options.',
|
|
691
|
-
acceptedFormats:
|
|
692
|
-
|
|
693
|
-
|
|
712
|
+
acceptedFormats: getAcceptedInputFormats(
|
|
713
|
+
inputFormat,
|
|
714
|
+
isInteractive
|
|
715
|
+
),
|
|
694
716
|
options: {
|
|
717
|
+
inputFormat,
|
|
695
718
|
interactive: isInteractive,
|
|
696
719
|
autoMergeQueuedMessages: autoMerge,
|
|
697
720
|
alwaysAcceptStdin,
|
|
@@ -707,8 +730,13 @@ async function main() {
|
|
|
707
730
|
}
|
|
708
731
|
|
|
709
732
|
// stdin is piped - enter stdin listening mode
|
|
710
|
-
const
|
|
711
|
-
const
|
|
733
|
+
const inputFormat = getInputFormat(argv);
|
|
734
|
+
const isInteractive =
|
|
735
|
+
inputFormat === 'stream-json' ? false : argv.interactive !== false;
|
|
736
|
+
const autoMerge =
|
|
737
|
+
inputFormat === 'stream-json'
|
|
738
|
+
? false
|
|
739
|
+
: argv['auto-merge-queued-messages'] !== false;
|
|
712
740
|
const alwaysAcceptStdin = argv['always-accept-stdin'] !== false;
|
|
713
741
|
|
|
714
742
|
outputStatus(
|
|
@@ -719,10 +747,12 @@ async function main() {
|
|
|
719
747
|
? 'Agent CLI in continuous listening mode. Accepts JSON and plain text input.'
|
|
720
748
|
: 'Agent CLI in single-message mode. Accepts JSON and plain text input.',
|
|
721
749
|
hint: 'Press CTRL+C to exit. Use --help for options.',
|
|
722
|
-
acceptedFormats:
|
|
723
|
-
|
|
724
|
-
|
|
750
|
+
acceptedFormats: getAcceptedInputFormats(
|
|
751
|
+
inputFormat,
|
|
752
|
+
isInteractive
|
|
753
|
+
),
|
|
725
754
|
options: {
|
|
755
|
+
inputFormat,
|
|
726
756
|
interactive: isInteractive,
|
|
727
757
|
autoMergeQueuedMessages: autoMerge,
|
|
728
758
|
alwaysAcceptStdin,
|
|
@@ -757,27 +787,52 @@ async function main() {
|
|
|
757
787
|
|
|
758
788
|
// Try to parse as JSON, if it fails treat it as plain text message
|
|
759
789
|
let request;
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
790
|
+
if (inputFormat === 'stream-json') {
|
|
791
|
+
try {
|
|
792
|
+
const firstFrame = trimmedInput
|
|
793
|
+
.split(/\r?\n/)
|
|
794
|
+
.find((line) => line.trim());
|
|
795
|
+
request = parseStreamJsonInput(firstFrame || '');
|
|
796
|
+
if (request.kind !== 'message') {
|
|
797
|
+
throw new Error(
|
|
798
|
+
`Expected stream-json user frame, received ${request.kind}`
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
} catch (error) {
|
|
766
802
|
outputError(
|
|
767
803
|
{
|
|
768
804
|
errorType: 'ValidationError',
|
|
769
805
|
message:
|
|
770
|
-
|
|
771
|
-
hint: '
|
|
806
|
+
error instanceof Error ? error.message : String(error),
|
|
807
|
+
hint: 'Provide one Claude stream-json user frame per line.',
|
|
772
808
|
},
|
|
773
809
|
compactJson
|
|
774
810
|
);
|
|
775
811
|
process.exit(1);
|
|
776
812
|
}
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
}
|
|
813
|
+
} else {
|
|
814
|
+
try {
|
|
815
|
+
request = JSON.parse(trimmedInput);
|
|
816
|
+
} catch (_e) {
|
|
817
|
+
// Not JSON
|
|
818
|
+
if (!isInteractive) {
|
|
819
|
+
// In non-interactive mode, only accept JSON
|
|
820
|
+
outputError(
|
|
821
|
+
{
|
|
822
|
+
errorType: 'ValidationError',
|
|
823
|
+
message:
|
|
824
|
+
'Invalid JSON input. In non-interactive mode (--no-interactive), only JSON input is accepted.',
|
|
825
|
+
hint: 'Use --interactive to accept plain text, or provide valid JSON: {"message": "your text"}',
|
|
826
|
+
},
|
|
827
|
+
compactJson
|
|
828
|
+
);
|
|
829
|
+
process.exit(1);
|
|
830
|
+
}
|
|
831
|
+
// In interactive mode, treat as plain text message
|
|
832
|
+
request = {
|
|
833
|
+
message: trimmedInput,
|
|
834
|
+
};
|
|
835
|
+
}
|
|
781
836
|
}
|
|
782
837
|
|
|
783
838
|
// Output input confirmation in JSON format
|
|
@@ -785,7 +840,12 @@ async function main() {
|
|
|
785
840
|
{
|
|
786
841
|
raw: trimmedInput,
|
|
787
842
|
parsed: request,
|
|
788
|
-
format:
|
|
843
|
+
format:
|
|
844
|
+
inputFormat === 'stream-json'
|
|
845
|
+
? 'stream-json'
|
|
846
|
+
: isInteractive
|
|
847
|
+
? 'text'
|
|
848
|
+
: 'json',
|
|
789
849
|
},
|
|
790
850
|
compactJson
|
|
791
851
|
);
|