@link-assistant/agent 0.3.1 → 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 +27 -0
- package/package.json +1 -1
- package/src/auth/plugins.ts +2 -0
- package/src/cli/continuous-mode.js +450 -0
- package/src/cli/input-queue.js +197 -0
- package/src/index.js +341 -59
package/README.md
CHANGED
|
@@ -181,6 +181,18 @@ See [MODELS.md](MODELS.md) for complete list of available models and pricing.
|
|
|
181
181
|
See [docs/groq.md](docs/groq.md) for Groq provider documentation.
|
|
182
182
|
See [docs/claude-oauth.md](docs/claude-oauth.md) for Claude OAuth provider documentation.
|
|
183
183
|
|
|
184
|
+
### Direct Prompt Mode
|
|
185
|
+
|
|
186
|
+
Use `-p`/`--prompt` to send a prompt directly without reading from stdin:
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
# Direct prompt (bypasses stdin)
|
|
190
|
+
agent -p "What is 2+2?"
|
|
191
|
+
|
|
192
|
+
# Useful in scripts
|
|
193
|
+
result=$(agent -p "Summarize: $(cat file.txt)")
|
|
194
|
+
```
|
|
195
|
+
|
|
184
196
|
### CLI Options
|
|
185
197
|
|
|
186
198
|
```bash
|
|
@@ -197,6 +209,19 @@ Options:
|
|
|
197
209
|
--system-message-file Full override of the system message from file
|
|
198
210
|
--append-system-message Append to the default system message
|
|
199
211
|
--append-system-message-file Append to the default system message from file
|
|
212
|
+
|
|
213
|
+
Stdin Mode Options:
|
|
214
|
+
-p, --prompt Direct prompt (bypasses stdin reading)
|
|
215
|
+
--disable-stdin Disable stdin streaming (requires --prompt)
|
|
216
|
+
--stdin-stream-timeout Timeout in ms for stdin reading (default: none)
|
|
217
|
+
--interactive Accept plain text input (default: true)
|
|
218
|
+
--no-interactive Only accept JSON input
|
|
219
|
+
--auto-merge-queued-messages Merge rapidly arriving lines (default: true)
|
|
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
|
|
224
|
+
|
|
200
225
|
--help Show help
|
|
201
226
|
--version Show version number
|
|
202
227
|
|
|
@@ -208,6 +233,8 @@ Commands:
|
|
|
208
233
|
mcp MCP server commands
|
|
209
234
|
```
|
|
210
235
|
|
|
236
|
+
See [docs/stdin-mode.md](docs/stdin-mode.md) for comprehensive stdin mode documentation.
|
|
237
|
+
|
|
211
238
|
### JSON Output Standards
|
|
212
239
|
|
|
213
240
|
The agent supports two JSON output format standards via the `--json-standard` option:
|
package/package.json
CHANGED
package/src/auth/plugins.ts
CHANGED
|
@@ -849,6 +849,8 @@ const GOOGLE_OAUTH_SCOPES = [
|
|
|
849
849
|
'https://www.googleapis.com/auth/cloud-platform',
|
|
850
850
|
'https://www.googleapis.com/auth/userinfo.email',
|
|
851
851
|
'https://www.googleapis.com/auth/userinfo.profile',
|
|
852
|
+
'https://www.googleapis.com/auth/generative-language.tuning',
|
|
853
|
+
'https://www.googleapis.com/auth/generative-language.retriever',
|
|
852
854
|
];
|
|
853
855
|
|
|
854
856
|
// Google OAuth endpoints
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input queue for managing continuous stdin input.
|
|
3
|
+
* Supports queuing of multiple messages and auto-merging of rapidly arriving lines.
|
|
4
|
+
*/
|
|
5
|
+
export class InputQueue {
|
|
6
|
+
constructor(options = {}) {
|
|
7
|
+
this.queue = [];
|
|
8
|
+
this.autoMerge = options.autoMerge !== false; // enabled by default
|
|
9
|
+
this.mergeDelayMs = options.mergeDelayMs || 50; // delay to wait for more lines
|
|
10
|
+
this.pendingLines = [];
|
|
11
|
+
this.mergeTimer = null;
|
|
12
|
+
this.onMessage = options.onMessage || (() => {});
|
|
13
|
+
this.interactive = options.interactive !== false; // enabled by default
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Parse input and determine if it's JSON or plain text
|
|
18
|
+
* @param {string} input - Raw input string
|
|
19
|
+
* @returns {object} - Parsed message object
|
|
20
|
+
*/
|
|
21
|
+
parseInput(input) {
|
|
22
|
+
const trimmed = input.trim();
|
|
23
|
+
if (!trimmed) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const parsed = JSON.parse(trimmed);
|
|
29
|
+
// If it has a message field, use it directly
|
|
30
|
+
if (typeof parsed === 'object' && parsed !== null) {
|
|
31
|
+
return parsed;
|
|
32
|
+
}
|
|
33
|
+
// Otherwise wrap it
|
|
34
|
+
return { message: JSON.stringify(parsed) };
|
|
35
|
+
} catch (_e) {
|
|
36
|
+
// Not JSON, treat as plain text message
|
|
37
|
+
return { message: trimmed };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Add a line to the queue, potentially merging with pending lines
|
|
43
|
+
* @param {string} line - Input line
|
|
44
|
+
*/
|
|
45
|
+
addLine(line) {
|
|
46
|
+
if (!this.interactive && !line.trim().startsWith('{')) {
|
|
47
|
+
// In non-interactive mode, only accept JSON
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (this.autoMerge) {
|
|
52
|
+
// Add to pending lines and schedule merge
|
|
53
|
+
this.pendingLines.push(line);
|
|
54
|
+
this.scheduleMerge();
|
|
55
|
+
} else {
|
|
56
|
+
// No merging, queue immediately
|
|
57
|
+
const parsed = this.parseInput(line);
|
|
58
|
+
if (parsed) {
|
|
59
|
+
this.queue.push(parsed);
|
|
60
|
+
this.notifyMessage(parsed);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Schedule a merge of pending lines after a short delay
|
|
67
|
+
*/
|
|
68
|
+
scheduleMerge() {
|
|
69
|
+
if (this.mergeTimer) {
|
|
70
|
+
clearTimeout(this.mergeTimer);
|
|
71
|
+
}
|
|
72
|
+
this.mergeTimer = setTimeout(() => {
|
|
73
|
+
this.flushPendingLines();
|
|
74
|
+
}, this.mergeDelayMs);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Flush pending lines into a single merged message
|
|
79
|
+
*/
|
|
80
|
+
flushPendingLines() {
|
|
81
|
+
if (this.pendingLines.length === 0) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const mergedText = this.pendingLines.join('\n');
|
|
86
|
+
this.pendingLines = [];
|
|
87
|
+
this.mergeTimer = null;
|
|
88
|
+
|
|
89
|
+
const parsed = this.parseInput(mergedText);
|
|
90
|
+
if (parsed) {
|
|
91
|
+
this.queue.push(parsed);
|
|
92
|
+
this.notifyMessage(parsed);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Immediately flush any pending lines (for shutdown)
|
|
98
|
+
*/
|
|
99
|
+
flush() {
|
|
100
|
+
if (this.mergeTimer) {
|
|
101
|
+
clearTimeout(this.mergeTimer);
|
|
102
|
+
this.mergeTimer = null;
|
|
103
|
+
}
|
|
104
|
+
this.flushPendingLines();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Get next message from the queue
|
|
109
|
+
* @returns {object|null} - Next message or null if queue is empty
|
|
110
|
+
*/
|
|
111
|
+
dequeue() {
|
|
112
|
+
return this.queue.shift() || null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Check if queue has messages
|
|
117
|
+
* @returns {boolean}
|
|
118
|
+
*/
|
|
119
|
+
hasMessages() {
|
|
120
|
+
return this.queue.length > 0;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Notify listener of new message
|
|
125
|
+
* @param {object} message - The message object
|
|
126
|
+
*/
|
|
127
|
+
notifyMessage(message) {
|
|
128
|
+
if (this.onMessage) {
|
|
129
|
+
this.onMessage(message);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Get queue size
|
|
135
|
+
* @returns {number}
|
|
136
|
+
*/
|
|
137
|
+
size() {
|
|
138
|
+
return this.queue.length;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Create a continuous stdin reader that queues input lines
|
|
144
|
+
* @param {object} options - Options for the reader
|
|
145
|
+
* @returns {object} - Reader with queue and control methods
|
|
146
|
+
*/
|
|
147
|
+
// Note: This function is prepared for future continuous input mode
|
|
148
|
+
export function createContinuousStdinReader(options = {}) {
|
|
149
|
+
const inputQueue = new InputQueue(options);
|
|
150
|
+
let isRunning = true;
|
|
151
|
+
let lineBuffer = '';
|
|
152
|
+
|
|
153
|
+
const handleData = (chunk) => {
|
|
154
|
+
if (!isRunning) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
lineBuffer += chunk.toString();
|
|
159
|
+
const lines = lineBuffer.split('\n');
|
|
160
|
+
|
|
161
|
+
// Keep the last incomplete line in buffer
|
|
162
|
+
lineBuffer = lines.pop() || '';
|
|
163
|
+
|
|
164
|
+
// Process complete lines
|
|
165
|
+
for (const line of lines) {
|
|
166
|
+
if (line.trim()) {
|
|
167
|
+
inputQueue.addLine(line);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const handleEnd = () => {
|
|
173
|
+
// Process any remaining data in buffer
|
|
174
|
+
if (lineBuffer.trim()) {
|
|
175
|
+
inputQueue.addLine(lineBuffer);
|
|
176
|
+
}
|
|
177
|
+
inputQueue.flush();
|
|
178
|
+
isRunning = false;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
process.stdin.on('data', handleData);
|
|
182
|
+
process.stdin.on('end', handleEnd);
|
|
183
|
+
process.stdin.on('error', () => {
|
|
184
|
+
isRunning = false;
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
queue: inputQueue,
|
|
189
|
+
stop: () => {
|
|
190
|
+
isRunning = false;
|
|
191
|
+
inputQueue.flush();
|
|
192
|
+
process.stdin.removeListener('data', handleData);
|
|
193
|
+
process.stdin.removeListener('end', handleEnd);
|
|
194
|
+
},
|
|
195
|
+
isRunning: () => isRunning,
|
|
196
|
+
};
|
|
197
|
+
}
|
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';
|
|
@@ -73,50 +77,80 @@ process.on('unhandledRejection', (reason, _promise) => {
|
|
|
73
77
|
process.exit(1);
|
|
74
78
|
});
|
|
75
79
|
|
|
76
|
-
|
|
77
|
-
|
|
80
|
+
/**
|
|
81
|
+
* Read stdin with optional timeout.
|
|
82
|
+
* @param {number|null} timeout - Timeout in milliseconds. If null, wait indefinitely until EOF.
|
|
83
|
+
* @returns {Promise<string>} - The stdin content
|
|
84
|
+
*/
|
|
85
|
+
function readStdinWithTimeout(timeout = null) {
|
|
86
|
+
return new Promise((resolve) => {
|
|
78
87
|
let data = '';
|
|
88
|
+
let hasData = false;
|
|
89
|
+
let timer = null;
|
|
90
|
+
|
|
91
|
+
const cleanup = () => {
|
|
92
|
+
if (timer) {
|
|
93
|
+
clearTimeout(timer);
|
|
94
|
+
}
|
|
95
|
+
process.stdin.removeListener('data', onData);
|
|
96
|
+
process.stdin.removeListener('end', onEnd);
|
|
97
|
+
process.stdin.removeListener('error', onError);
|
|
98
|
+
};
|
|
99
|
+
|
|
79
100
|
const onData = (chunk) => {
|
|
101
|
+
hasData = true;
|
|
102
|
+
if (timer) {
|
|
103
|
+
clearTimeout(timer);
|
|
104
|
+
}
|
|
80
105
|
data += chunk;
|
|
81
106
|
};
|
|
107
|
+
|
|
82
108
|
const onEnd = () => {
|
|
83
109
|
cleanup();
|
|
84
110
|
resolve(data);
|
|
85
111
|
};
|
|
86
|
-
|
|
112
|
+
|
|
113
|
+
const onError = () => {
|
|
87
114
|
cleanup();
|
|
88
|
-
|
|
89
|
-
};
|
|
90
|
-
const cleanup = () => {
|
|
91
|
-
process.stdin.removeListener('data', onData);
|
|
92
|
-
process.stdin.removeListener('end', onEnd);
|
|
93
|
-
process.stdin.removeListener('error', onError);
|
|
115
|
+
resolve('');
|
|
94
116
|
};
|
|
117
|
+
|
|
118
|
+
// Only set timeout if specified (not null)
|
|
119
|
+
if (timeout !== null) {
|
|
120
|
+
timer = setTimeout(() => {
|
|
121
|
+
if (!hasData) {
|
|
122
|
+
process.stdin.pause();
|
|
123
|
+
cleanup();
|
|
124
|
+
resolve('');
|
|
125
|
+
}
|
|
126
|
+
}, timeout);
|
|
127
|
+
}
|
|
128
|
+
|
|
95
129
|
process.stdin.on('data', onData);
|
|
96
130
|
process.stdin.on('end', onEnd);
|
|
97
131
|
process.stdin.on('error', onError);
|
|
98
132
|
});
|
|
99
133
|
}
|
|
100
134
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
// Log dry-run mode if enabled
|
|
114
|
-
if (Flag.OPENCODE_DRY_RUN) {
|
|
115
|
-
console.error(
|
|
116
|
-
`[DRY RUN MODE] No actual API calls or package installations will be made`
|
|
117
|
-
);
|
|
118
|
-
}
|
|
135
|
+
/**
|
|
136
|
+
* Output JSON status message to stderr
|
|
137
|
+
* This prevents the status message from interfering with JSON output parsing
|
|
138
|
+
* @param {object} status - Status object to output
|
|
139
|
+
* @param {boolean} compact - If true, output compact JSON (single line)
|
|
140
|
+
*/
|
|
141
|
+
function outputStatus(status, compact = false) {
|
|
142
|
+
const json = compact
|
|
143
|
+
? JSON.stringify(status)
|
|
144
|
+
: JSON.stringify(status, null, 2);
|
|
145
|
+
console.error(json);
|
|
146
|
+
}
|
|
119
147
|
|
|
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) {
|
|
120
154
|
// Parse model argument (handle model IDs with slashes like groq/qwen/qwen3-32b)
|
|
121
155
|
const modelParts = argv.model.split('/');
|
|
122
156
|
let providerID = modelParts[0] || 'opencode';
|
|
@@ -131,13 +165,15 @@ async function runAgentMode(argv) {
|
|
|
131
165
|
const creds = await ClaudeOAuth.getCredentials();
|
|
132
166
|
|
|
133
167
|
if (!creds?.accessToken) {
|
|
134
|
-
|
|
135
|
-
|
|
168
|
+
const compactJson = argv['compact-json'] === true;
|
|
169
|
+
outputStatus(
|
|
170
|
+
{
|
|
136
171
|
type: 'error',
|
|
137
172
|
errorType: 'AuthenticationError',
|
|
138
173
|
message:
|
|
139
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)',
|
|
140
|
-
}
|
|
175
|
+
},
|
|
176
|
+
compactJson
|
|
141
177
|
);
|
|
142
178
|
process.exit(1);
|
|
143
179
|
}
|
|
@@ -152,26 +188,27 @@ async function runAgentMode(argv) {
|
|
|
152
188
|
modelID = 'claude-sonnet-4-5';
|
|
153
189
|
} else if (!['claude-oauth', 'anthropic'].includes(providerID)) {
|
|
154
190
|
// If user specified a different provider, warn them
|
|
155
|
-
|
|
156
|
-
|
|
191
|
+
const compactJson = argv['compact-json'] === true;
|
|
192
|
+
outputStatus(
|
|
193
|
+
{
|
|
157
194
|
type: 'warning',
|
|
158
195
|
message: `--use-existing-claude-oauth is set but model uses provider "${providerID}". Using OAuth credentials anyway.`,
|
|
159
|
-
}
|
|
196
|
+
},
|
|
197
|
+
compactJson
|
|
160
198
|
);
|
|
161
199
|
providerID = 'claude-oauth';
|
|
162
200
|
}
|
|
163
201
|
}
|
|
164
202
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
if (!isValidJsonStandard(jsonStandard)) {
|
|
168
|
-
console.error(
|
|
169
|
-
`Invalid JSON standard: ${jsonStandard}. Use "opencode" or "claude".`
|
|
170
|
-
);
|
|
171
|
-
process.exit(1);
|
|
172
|
-
}
|
|
203
|
+
return { providerID, modelID };
|
|
204
|
+
}
|
|
173
205
|
|
|
174
|
-
|
|
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) {
|
|
175
212
|
let systemMessage = argv['system-message'];
|
|
176
213
|
let appendSystemMessage = argv['append-system-message'];
|
|
177
214
|
|
|
@@ -205,23 +242,43 @@ async function runAgentMode(argv) {
|
|
|
205
242
|
appendSystemMessage = await file.text();
|
|
206
243
|
}
|
|
207
244
|
|
|
208
|
-
|
|
245
|
+
return { systemMessage, appendSystemMessage };
|
|
246
|
+
}
|
|
209
247
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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()
|
|
213
251
|
|
|
214
|
-
//
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
+
);
|
|
223
265
|
}
|
|
224
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
|
+
|
|
280
|
+
// Logging is already initialized in middleware, no need to call Log.init() again
|
|
281
|
+
|
|
225
282
|
// Wrap in Instance.provide for OpenCode infrastructure
|
|
226
283
|
await Instance.provide({
|
|
227
284
|
directory: process.cwd(),
|
|
@@ -254,6 +311,81 @@ async function runAgentMode(argv) {
|
|
|
254
311
|
process.exit(hasError ? 1 : 0);
|
|
255
312
|
}
|
|
256
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
|
+
|
|
257
389
|
async function runServerMode(
|
|
258
390
|
request,
|
|
259
391
|
providerID,
|
|
@@ -533,7 +665,7 @@ async function runDirectMode(
|
|
|
533
665
|
async function main() {
|
|
534
666
|
try {
|
|
535
667
|
// Parse command line arguments with subcommands
|
|
536
|
-
const
|
|
668
|
+
const yargsInstance = yargs(hideBin(process.argv))
|
|
537
669
|
.scriptName('agent')
|
|
538
670
|
.usage('$0 [command] [options]')
|
|
539
671
|
.version(pkg.version)
|
|
@@ -593,6 +725,46 @@ async function main() {
|
|
|
593
725
|
'Use existing Claude OAuth credentials from ~/.claude/.credentials.json (from Claude Code CLI)',
|
|
594
726
|
default: false,
|
|
595
727
|
})
|
|
728
|
+
.option('prompt', {
|
|
729
|
+
alias: 'p',
|
|
730
|
+
type: 'string',
|
|
731
|
+
description: 'Prompt message to send directly (bypasses stdin reading)',
|
|
732
|
+
})
|
|
733
|
+
.option('disable-stdin', {
|
|
734
|
+
type: 'boolean',
|
|
735
|
+
description:
|
|
736
|
+
'Disable stdin streaming mode (requires --prompt or shows help)',
|
|
737
|
+
default: false,
|
|
738
|
+
})
|
|
739
|
+
.option('stdin-stream-timeout', {
|
|
740
|
+
type: 'number',
|
|
741
|
+
description:
|
|
742
|
+
'Optional timeout in milliseconds for stdin reading (default: no timeout)',
|
|
743
|
+
})
|
|
744
|
+
.option('auto-merge-queued-messages', {
|
|
745
|
+
type: 'boolean',
|
|
746
|
+
description:
|
|
747
|
+
'Enable auto-merging of rapidly arriving input lines into single messages (default: true)',
|
|
748
|
+
default: true,
|
|
749
|
+
})
|
|
750
|
+
.option('interactive', {
|
|
751
|
+
type: 'boolean',
|
|
752
|
+
description:
|
|
753
|
+
'Enable interactive mode to accept manual input as plain text strings (default: true). Use --no-interactive to only accept JSON input.',
|
|
754
|
+
default: true,
|
|
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
|
+
})
|
|
596
768
|
// Initialize logging early for all CLI commands
|
|
597
769
|
// This prevents debug output from appearing in CLI unless --verbose is used
|
|
598
770
|
.middleware(async (argv) => {
|
|
@@ -641,15 +813,125 @@ async function main() {
|
|
|
641
813
|
process.exit(1);
|
|
642
814
|
}
|
|
643
815
|
})
|
|
644
|
-
.help()
|
|
816
|
+
.help();
|
|
817
|
+
|
|
818
|
+
const argv = await yargsInstance.argv;
|
|
645
819
|
|
|
646
820
|
// If a command was executed (like mcp), yargs handles it
|
|
647
821
|
// Otherwise, check if we should run in agent mode (stdin piped)
|
|
648
822
|
const commandExecuted = argv._ && argv._.length > 0;
|
|
649
823
|
|
|
650
824
|
if (!commandExecuted) {
|
|
651
|
-
|
|
652
|
-
|
|
825
|
+
const compactJson = argv['compact-json'] === true;
|
|
826
|
+
|
|
827
|
+
// Check if --prompt flag was provided
|
|
828
|
+
if (argv.prompt) {
|
|
829
|
+
// Direct prompt mode - bypass stdin entirely
|
|
830
|
+
const request = { message: argv.prompt };
|
|
831
|
+
await runAgentMode(argv, request);
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// Check if --disable-stdin was set without --prompt
|
|
836
|
+
if (argv['disable-stdin']) {
|
|
837
|
+
// Output a helpful message suggesting to use --prompt
|
|
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
|
+
);
|
|
847
|
+
process.exit(1);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// Check if stdin is a TTY (interactive terminal)
|
|
851
|
+
// If it is, show help instead of waiting for input
|
|
852
|
+
if (process.stdin.isTTY) {
|
|
853
|
+
yargsInstance.showHelp();
|
|
854
|
+
process.exit(0);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// stdin is piped - enter stdin listening mode
|
|
858
|
+
// Output status message to inform user what's happening
|
|
859
|
+
const isInteractive = argv.interactive !== false;
|
|
860
|
+
const autoMerge = argv['auto-merge-queued-messages'] !== false;
|
|
861
|
+
const alwaysAcceptStdin = argv['always-accept-stdin'] !== false;
|
|
862
|
+
|
|
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
|
+
},
|
|
880
|
+
},
|
|
881
|
+
compactJson
|
|
882
|
+
);
|
|
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)
|
|
891
|
+
// Read stdin with optional timeout
|
|
892
|
+
const timeout = argv['stdin-stream-timeout'] ?? null;
|
|
893
|
+
const input = await readStdinWithTimeout(timeout);
|
|
894
|
+
const trimmedInput = input.trim();
|
|
895
|
+
|
|
896
|
+
if (trimmedInput === '') {
|
|
897
|
+
outputStatus(
|
|
898
|
+
{
|
|
899
|
+
type: 'status',
|
|
900
|
+
message: 'No input received. Exiting.',
|
|
901
|
+
},
|
|
902
|
+
compactJson
|
|
903
|
+
);
|
|
904
|
+
yargsInstance.showHelp();
|
|
905
|
+
process.exit(0);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// Try to parse as JSON, if it fails treat it as plain text message
|
|
909
|
+
let request;
|
|
910
|
+
try {
|
|
911
|
+
request = JSON.parse(trimmedInput);
|
|
912
|
+
} catch (_e) {
|
|
913
|
+
// Not JSON
|
|
914
|
+
if (!isInteractive) {
|
|
915
|
+
// In non-interactive mode, only accept JSON
|
|
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
|
+
);
|
|
925
|
+
process.exit(1);
|
|
926
|
+
}
|
|
927
|
+
// In interactive mode, treat as plain text message
|
|
928
|
+
request = {
|
|
929
|
+
message: trimmedInput,
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// Run agent mode
|
|
934
|
+
await runAgentMode(argv, request);
|
|
653
935
|
}
|
|
654
936
|
} catch (error) {
|
|
655
937
|
hasError = true;
|