@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 +3 -0
- package/package.json +1 -1
- package/src/cli/continuous-mode.js +450 -0
- package/src/index.js +210 -65
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
|
@@ -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
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
174
|
-
|
|
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
|
-
|
|
195
|
-
|
|
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
|
-
|
|
205
|
-
|
|
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
|
-
|
|
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
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
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
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
:
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
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
|
-
|
|
760
|
-
|
|
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
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
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
|