@link-assistant/agent 0.22.10 → 0.24.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 +12 -0
- package/package.json +1 -1
- package/src/cli/continuous-mode.js +142 -27
- package/src/cli/event-handler.js +21 -0
- package/src/cli/input-queue.js +232 -12
- package/src/cli/output.ts +1 -1
- package/src/cli/run-options.js +43 -0
- package/src/config/config.ts +37 -0
- package/src/index.js +83 -23
- package/src/permission/index.ts +564 -0
- package/src/session/prompt.ts +6 -0
- package/src/session/system.ts +14 -0
- package/src/tool/bash.ts +41 -1
- package/src/tool/batch.ts +8 -0
- package/src/tool/edit.ts +12 -1
- package/src/tool/patch.ts +19 -1
- package/src/tool/registry.ts +36 -0
- package/src/tool/webfetch.ts +12 -1
- package/src/tool/write.ts +13 -1
package/README.md
CHANGED
|
@@ -293,6 +293,18 @@ Options:
|
|
|
293
293
|
--append-system-message Append to the default system message
|
|
294
294
|
--append-system-message-file Append to the default system message from file
|
|
295
295
|
|
|
296
|
+
Permission Options:
|
|
297
|
+
--read-only Enforceable read-only / planning mode.
|
|
298
|
+
Disables all filesystem-mutating and shell
|
|
299
|
+
tools (bash, edit, write, multiedit, patch)
|
|
300
|
+
so the agent can only read, search and plan.
|
|
301
|
+
Env: LINK_ASSISTANT_AGENT_READ_ONLY=true
|
|
302
|
+
--disable-tools <list> Comma-separated list of tool ids to disable
|
|
303
|
+
(e.g. "bash,write,edit"). Disabled tools are
|
|
304
|
+
never exposed to the model and are rejected if
|
|
305
|
+
invoked via batch.
|
|
306
|
+
Env: LINK_ASSISTANT_AGENT_DISABLE_TOOLS
|
|
307
|
+
|
|
296
308
|
Stdin Mode Options:
|
|
297
309
|
-p, --prompt Direct prompt (bypasses stdin reading)
|
|
298
310
|
--disable-stdin Disable stdin streaming (requires --prompt)
|
package/package.json
CHANGED
|
@@ -8,8 +8,9 @@ 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
|
+
import { Permission } from '../permission/index.ts';
|
|
13
14
|
import { Log } from '../util/log.ts';
|
|
14
15
|
import { config } from '../config/config.ts';
|
|
15
16
|
import { createVerboseFetch } from '../util/verbose-fetch.ts';
|
|
@@ -40,6 +41,64 @@ export function getHasError() {
|
|
|
40
41
|
// Logger for resume operations
|
|
41
42
|
const log = Log.create({ service: 'resume' });
|
|
42
43
|
|
|
44
|
+
function getInputFormat(argv) {
|
|
45
|
+
return argv['input-format'] || argv.inputFormat || 'text';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function outputConsumedInput({
|
|
49
|
+
message,
|
|
50
|
+
jsonStandard,
|
|
51
|
+
sessionID,
|
|
52
|
+
compactJson,
|
|
53
|
+
}) {
|
|
54
|
+
const raw = message.raw || message.message || message.system || '';
|
|
55
|
+
if (
|
|
56
|
+
jsonStandard === 'claude' &&
|
|
57
|
+
message.kind === 'message' &&
|
|
58
|
+
message.format === 'stream-json'
|
|
59
|
+
) {
|
|
60
|
+
process.stdout.write(
|
|
61
|
+
serializeOutput(
|
|
62
|
+
{
|
|
63
|
+
type: 'message',
|
|
64
|
+
timestamp: new Date().toISOString(),
|
|
65
|
+
session_id: sessionID,
|
|
66
|
+
role: 'user',
|
|
67
|
+
content: [{ type: 'text', text: message.message ?? '' }],
|
|
68
|
+
},
|
|
69
|
+
'claude'
|
|
70
|
+
)
|
|
71
|
+
);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (jsonStandard === 'claude') {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
outputInput(
|
|
80
|
+
{
|
|
81
|
+
raw,
|
|
82
|
+
parsed: message.parsed || message,
|
|
83
|
+
format: message.format || 'text',
|
|
84
|
+
kind: message.kind || 'message',
|
|
85
|
+
},
|
|
86
|
+
compactJson
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function outputInputParseError(error, line, compactJson) {
|
|
91
|
+
hasError = true;
|
|
92
|
+
outputError(
|
|
93
|
+
{
|
|
94
|
+
errorType: 'ValidationError',
|
|
95
|
+
message: error instanceof Error ? error.message : String(error),
|
|
96
|
+
raw: line,
|
|
97
|
+
},
|
|
98
|
+
compactJson
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
43
102
|
/**
|
|
44
103
|
* Resolve the session to use based on --resume, --continue, and --no-fork options.
|
|
45
104
|
* Returns the session ID to use, handling forking as needed.
|
|
@@ -200,8 +259,12 @@ export async function runContinuousServerMode(
|
|
|
200
259
|
) {
|
|
201
260
|
// Check both CLI flag and environment variable for compact JSON mode
|
|
202
261
|
const compactJson = argv['compact-json'] === true || config.compactJson;
|
|
262
|
+
const inputFormat = getInputFormat(argv);
|
|
203
263
|
const isInteractive = argv.interactive !== false;
|
|
204
|
-
const autoMerge =
|
|
264
|
+
const autoMerge =
|
|
265
|
+
inputFormat === 'stream-json'
|
|
266
|
+
? false
|
|
267
|
+
: argv['auto-merge-queued-messages'] !== false;
|
|
205
268
|
|
|
206
269
|
// Start server like OpenCode does
|
|
207
270
|
const server = Server.listen({ port: 0, hostname: '127.0.0.1' });
|
|
@@ -244,9 +307,36 @@ export async function runContinuousServerMode(
|
|
|
244
307
|
// Track if we're currently processing a message
|
|
245
308
|
let isProcessing = false;
|
|
246
309
|
const pendingMessages = [];
|
|
310
|
+
let currentSystemMessage = systemMessage;
|
|
311
|
+
const currentAppendSystemMessage = appendSystemMessage;
|
|
247
312
|
|
|
248
313
|
// Process messages from the queue
|
|
249
314
|
const processMessage = async (message) => {
|
|
315
|
+
if (message.kind === 'interrupt') {
|
|
316
|
+
SessionPrompt.cancel(sessionID);
|
|
317
|
+
outputConsumedInput({ message, jsonStandard, sessionID, compactJson });
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (message.kind === 'system') {
|
|
322
|
+
currentSystemMessage = message.system;
|
|
323
|
+
outputConsumedInput({ message, jsonStandard, sessionID, compactJson });
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Permission approval reply (issue #271). Resolve a pending tool
|
|
328
|
+
// permission request without interrupting the in-flight turn — this must
|
|
329
|
+
// bypass the `isProcessing` queue, since the turn is blocked waiting for it.
|
|
330
|
+
if (message.kind === 'permission_response') {
|
|
331
|
+
Permission.respond({
|
|
332
|
+
sessionID,
|
|
333
|
+
permissionID: message.permissionID,
|
|
334
|
+
response: message.response,
|
|
335
|
+
});
|
|
336
|
+
outputConsumedInput({ message, jsonStandard, sessionID, compactJson });
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
250
340
|
if (isProcessing) {
|
|
251
341
|
pendingMessages.push(message);
|
|
252
342
|
return;
|
|
@@ -255,16 +345,9 @@ export async function runContinuousServerMode(
|
|
|
255
345
|
isProcessing = true;
|
|
256
346
|
|
|
257
347
|
// 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
|
-
);
|
|
348
|
+
outputConsumedInput({ message, jsonStandard, sessionID, compactJson });
|
|
266
349
|
|
|
267
|
-
const messageText = message.message
|
|
350
|
+
const messageText = message.message ?? 'hi';
|
|
268
351
|
const parts = [{ type: 'text', text: messageText }];
|
|
269
352
|
|
|
270
353
|
// Create a promise to wait for this message to complete
|
|
@@ -290,8 +373,8 @@ export async function runContinuousServerMode(
|
|
|
290
373
|
parts,
|
|
291
374
|
model: { providerID, modelID },
|
|
292
375
|
compactionModel,
|
|
293
|
-
system:
|
|
294
|
-
appendSystem:
|
|
376
|
+
system: currentSystemMessage,
|
|
377
|
+
appendSystem: currentAppendSystemMessage,
|
|
295
378
|
temperature,
|
|
296
379
|
}),
|
|
297
380
|
}
|
|
@@ -377,11 +460,15 @@ export async function runContinuousServerMode(
|
|
|
377
460
|
|
|
378
461
|
// Create continuous stdin reader
|
|
379
462
|
stdinReader = createContinuousStdinReader({
|
|
380
|
-
interactive: isInteractive,
|
|
463
|
+
interactive: inputFormat === 'stream-json' ? false : isInteractive,
|
|
381
464
|
autoMerge,
|
|
465
|
+
inputFormat,
|
|
382
466
|
onMessage: (message) => {
|
|
383
467
|
processMessage(message);
|
|
384
468
|
},
|
|
469
|
+
onError: (error, line) => {
|
|
470
|
+
outputInputParseError(error, line, compactJson);
|
|
471
|
+
},
|
|
385
472
|
});
|
|
386
473
|
|
|
387
474
|
// Wait for stdin to end (EOF or close)
|
|
@@ -454,8 +541,12 @@ export async function runContinuousDirectMode(
|
|
|
454
541
|
) {
|
|
455
542
|
// Check both CLI flag and environment variable for compact JSON mode
|
|
456
543
|
const compactJson = argv['compact-json'] === true || config.compactJson;
|
|
544
|
+
const inputFormat = getInputFormat(argv);
|
|
457
545
|
const isInteractive = argv.interactive !== false;
|
|
458
|
-
const autoMerge =
|
|
546
|
+
const autoMerge =
|
|
547
|
+
inputFormat === 'stream-json'
|
|
548
|
+
? false
|
|
549
|
+
: argv['auto-merge-queued-messages'] !== false;
|
|
459
550
|
|
|
460
551
|
let unsub = null;
|
|
461
552
|
let stdinReader = null;
|
|
@@ -483,9 +574,36 @@ export async function runContinuousDirectMode(
|
|
|
483
574
|
// Track if we're currently processing a message
|
|
484
575
|
let isProcessing = false;
|
|
485
576
|
const pendingMessages = [];
|
|
577
|
+
let currentSystemMessage = systemMessage;
|
|
578
|
+
const currentAppendSystemMessage = appendSystemMessage;
|
|
486
579
|
|
|
487
580
|
// Process messages from the queue
|
|
488
581
|
const processMessage = async (message) => {
|
|
582
|
+
if (message.kind === 'interrupt') {
|
|
583
|
+
SessionPrompt.cancel(sessionID);
|
|
584
|
+
outputConsumedInput({ message, jsonStandard, sessionID, compactJson });
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if (message.kind === 'system') {
|
|
589
|
+
currentSystemMessage = message.system;
|
|
590
|
+
outputConsumedInput({ message, jsonStandard, sessionID, compactJson });
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Permission approval reply (issue #271). Resolve a pending tool
|
|
595
|
+
// permission request without interrupting the in-flight turn — this must
|
|
596
|
+
// bypass the `isProcessing` queue, since the turn is blocked waiting for it.
|
|
597
|
+
if (message.kind === 'permission_response') {
|
|
598
|
+
Permission.respond({
|
|
599
|
+
sessionID,
|
|
600
|
+
permissionID: message.permissionID,
|
|
601
|
+
response: message.response,
|
|
602
|
+
});
|
|
603
|
+
outputConsumedInput({ message, jsonStandard, sessionID, compactJson });
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
|
|
489
607
|
if (isProcessing) {
|
|
490
608
|
pendingMessages.push(message);
|
|
491
609
|
return;
|
|
@@ -494,16 +612,9 @@ export async function runContinuousDirectMode(
|
|
|
494
612
|
isProcessing = true;
|
|
495
613
|
|
|
496
614
|
// 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
|
-
);
|
|
615
|
+
outputConsumedInput({ message, jsonStandard, sessionID, compactJson });
|
|
505
616
|
|
|
506
|
-
const messageText = message.message
|
|
617
|
+
const messageText = message.message ?? 'hi';
|
|
507
618
|
const parts = [{ type: 'text', text: messageText }];
|
|
508
619
|
|
|
509
620
|
// Create a promise to wait for this message to complete
|
|
@@ -525,8 +636,8 @@ export async function runContinuousDirectMode(
|
|
|
525
636
|
parts,
|
|
526
637
|
model: { providerID, modelID },
|
|
527
638
|
compactionModel,
|
|
528
|
-
system:
|
|
529
|
-
appendSystem:
|
|
639
|
+
system: currentSystemMessage,
|
|
640
|
+
appendSystem: currentAppendSystemMessage,
|
|
530
641
|
temperature,
|
|
531
642
|
}).catch((error) => {
|
|
532
643
|
hasError = true;
|
|
@@ -610,11 +721,15 @@ export async function runContinuousDirectMode(
|
|
|
610
721
|
|
|
611
722
|
// Create continuous stdin reader
|
|
612
723
|
stdinReader = createContinuousStdinReader({
|
|
613
|
-
interactive: isInteractive,
|
|
724
|
+
interactive: inputFormat === 'stream-json' ? false : isInteractive,
|
|
614
725
|
autoMerge,
|
|
726
|
+
inputFormat,
|
|
615
727
|
onMessage: (message) => {
|
|
616
728
|
processMessage(message);
|
|
617
729
|
},
|
|
730
|
+
onError: (error, line) => {
|
|
731
|
+
outputInputParseError(error, line, compactJson);
|
|
732
|
+
},
|
|
618
733
|
});
|
|
619
734
|
|
|
620
735
|
// Wait for stdin to end (EOF or close)
|
package/src/cli/event-handler.js
CHANGED
|
@@ -81,6 +81,27 @@ export function createBusEventSubscription({
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
// Emit a JSON permission request when a tool needs approval (issue #271).
|
|
85
|
+
// The consumer replies with a `permission_response` frame over stdin which
|
|
86
|
+
// is routed to Permission.respond. No TUI is involved.
|
|
87
|
+
if (event.type === 'permission.updated') {
|
|
88
|
+
const permission = event.properties;
|
|
89
|
+
if (permission.sessionID !== sessionID) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
eventHandler.output({
|
|
93
|
+
type: 'permission_request',
|
|
94
|
+
timestamp: Date.now(),
|
|
95
|
+
sessionID,
|
|
96
|
+
permissionID: permission.id,
|
|
97
|
+
callID: permission.callID,
|
|
98
|
+
tool: permission.type,
|
|
99
|
+
pattern: permission.pattern,
|
|
100
|
+
title: permission.title,
|
|
101
|
+
metadata: permission.metadata,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
84
105
|
// Handle session idle to know when to stop
|
|
85
106
|
if (
|
|
86
107
|
event.type === 'session.idle' &&
|
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,18 +26,51 @@ export class InputQueue {
|
|
|
24
26
|
return null;
|
|
25
27
|
}
|
|
26
28
|
|
|
29
|
+
if (this.inputFormat === 'stream-json') {
|
|
30
|
+
return parseStreamJsonInput(trimmed);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let parsed;
|
|
27
34
|
try {
|
|
28
|
-
|
|
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
|
+
parsed = JSON.parse(trimmed);
|
|
35
36
|
} catch (_e) {
|
|
36
37
|
// Not JSON, treat as plain text message
|
|
37
38
|
return { message: trimmed };
|
|
38
39
|
}
|
|
40
|
+
|
|
41
|
+
// If it has a message field, use it directly
|
|
42
|
+
if (typeof parsed === 'object' && parsed !== null) {
|
|
43
|
+
// Permission approval reply (issue #271) — normalize to a `kind` frame
|
|
44
|
+
// so it is routed to Permission.respond, mirroring stream-json mode.
|
|
45
|
+
if (parsed.type === 'permission_response') {
|
|
46
|
+
const permissionID = parsed.permissionID ?? parsed.permission_id;
|
|
47
|
+
const response = parsed.response;
|
|
48
|
+
if (typeof permissionID !== 'string' || !permissionID) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
'Invalid permission_response: expected string "permissionID"'
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
if (
|
|
54
|
+
response !== 'once' &&
|
|
55
|
+
response !== 'always' &&
|
|
56
|
+
response !== 'reject'
|
|
57
|
+
) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
'Invalid permission_response: "response" must be "once", "always" or "reject"'
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
kind: 'permission_response',
|
|
64
|
+
permissionID,
|
|
65
|
+
response,
|
|
66
|
+
raw: trimmed,
|
|
67
|
+
parsed,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return parsed;
|
|
71
|
+
}
|
|
72
|
+
// Otherwise wrap it
|
|
73
|
+
return { message: JSON.stringify(parsed) };
|
|
39
74
|
}
|
|
40
75
|
|
|
41
76
|
/**
|
|
@@ -43,6 +78,11 @@ export class InputQueue {
|
|
|
43
78
|
* @param {string} line - Input line
|
|
44
79
|
*/
|
|
45
80
|
addLine(line) {
|
|
81
|
+
if (this.inputFormat === 'stream-json') {
|
|
82
|
+
this.queueLine(line);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
46
86
|
if (!this.interactive && !line.trim().startsWith('{')) {
|
|
47
87
|
// In non-interactive mode, only accept JSON
|
|
48
88
|
return;
|
|
@@ -54,11 +94,7 @@ export class InputQueue {
|
|
|
54
94
|
this.scheduleMerge();
|
|
55
95
|
} else {
|
|
56
96
|
// No merging, queue immediately
|
|
57
|
-
|
|
58
|
-
if (parsed) {
|
|
59
|
-
this.queue.push(parsed);
|
|
60
|
-
this.notifyMessage(parsed);
|
|
61
|
-
}
|
|
97
|
+
this.queueLine(line);
|
|
62
98
|
}
|
|
63
99
|
}
|
|
64
100
|
|
|
@@ -130,6 +166,33 @@ export class InputQueue {
|
|
|
130
166
|
}
|
|
131
167
|
}
|
|
132
168
|
|
|
169
|
+
/**
|
|
170
|
+
* Notify listener of an input parse error
|
|
171
|
+
* @param {Error} error - Parse error
|
|
172
|
+
* @param {string} line - Raw input line
|
|
173
|
+
*/
|
|
174
|
+
notifyError(error, line) {
|
|
175
|
+
if (this.onError) {
|
|
176
|
+
this.onError(error, line);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Parse and enqueue one complete input line
|
|
182
|
+
* @param {string} line - Raw input line
|
|
183
|
+
*/
|
|
184
|
+
queueLine(line) {
|
|
185
|
+
try {
|
|
186
|
+
const parsed = this.parseInput(line);
|
|
187
|
+
if (parsed) {
|
|
188
|
+
this.queue.push(parsed);
|
|
189
|
+
this.notifyMessage(parsed);
|
|
190
|
+
}
|
|
191
|
+
} catch (error) {
|
|
192
|
+
this.notifyError(error, line);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
133
196
|
/**
|
|
134
197
|
* Get queue size
|
|
135
198
|
* @returns {number}
|
|
@@ -139,6 +202,163 @@ export class InputQueue {
|
|
|
139
202
|
}
|
|
140
203
|
}
|
|
141
204
|
|
|
205
|
+
function extractContentText(content) {
|
|
206
|
+
if (typeof content === 'string') {
|
|
207
|
+
return content;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (Array.isArray(content)) {
|
|
211
|
+
const parts = [];
|
|
212
|
+
for (const part of content) {
|
|
213
|
+
if (typeof part === 'string') {
|
|
214
|
+
parts.push(part);
|
|
215
|
+
} else if (part && typeof part === 'object') {
|
|
216
|
+
if (typeof part.text === 'string') {
|
|
217
|
+
parts.push(part.text);
|
|
218
|
+
} else if (typeof part.content === 'string') {
|
|
219
|
+
parts.push(part.content);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return parts.length > 0 ? parts.join('\n') : null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (content && typeof content === 'object') {
|
|
227
|
+
if (typeof content.text === 'string') {
|
|
228
|
+
return content.text;
|
|
229
|
+
}
|
|
230
|
+
if ('content' in content) {
|
|
231
|
+
return extractContentText(content.content);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function extractFrameText(frame) {
|
|
239
|
+
if (typeof frame.message === 'string') {
|
|
240
|
+
return frame.message;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (frame.message && typeof frame.message === 'object') {
|
|
244
|
+
const messageText = extractContentText(frame.message.content);
|
|
245
|
+
if (messageText !== null) {
|
|
246
|
+
return messageText;
|
|
247
|
+
}
|
|
248
|
+
if (typeof frame.message.text === 'string') {
|
|
249
|
+
return frame.message.text;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const contentText = extractContentText(frame.content);
|
|
254
|
+
if (contentText !== null) {
|
|
255
|
+
return contentText;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (typeof frame.text === 'string') {
|
|
259
|
+
return frame.text;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Parse one Claude-compatible stream-json input frame.
|
|
267
|
+
* @param {string} input - One JSONL frame
|
|
268
|
+
* @returns {object} Normalized queue message
|
|
269
|
+
*/
|
|
270
|
+
export function parseStreamJsonInput(input) {
|
|
271
|
+
let frame;
|
|
272
|
+
try {
|
|
273
|
+
frame = JSON.parse(input);
|
|
274
|
+
} catch (error) {
|
|
275
|
+
throw new Error(
|
|
276
|
+
`Invalid stream-json input frame: ${
|
|
277
|
+
error instanceof Error ? error.message : String(error)
|
|
278
|
+
}`
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (!frame || typeof frame !== 'object' || Array.isArray(frame)) {
|
|
283
|
+
throw new Error('Invalid stream-json input frame: expected JSON object');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const type = frame.type;
|
|
287
|
+
|
|
288
|
+
if (type === 'interrupt') {
|
|
289
|
+
return {
|
|
290
|
+
kind: 'interrupt',
|
|
291
|
+
raw: input,
|
|
292
|
+
parsed: frame,
|
|
293
|
+
format: 'stream-json',
|
|
294
|
+
inputType: type,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (type === 'system') {
|
|
299
|
+
const system = extractFrameText(frame);
|
|
300
|
+
if (system === null) {
|
|
301
|
+
throw new Error(
|
|
302
|
+
'Invalid stream-json system frame: expected content text'
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
return {
|
|
306
|
+
kind: 'system',
|
|
307
|
+
system,
|
|
308
|
+
raw: input,
|
|
309
|
+
parsed: frame,
|
|
310
|
+
format: 'stream-json',
|
|
311
|
+
inputType: type,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Permission approval reply (issue #271). Resolves a pending
|
|
316
|
+
// `permission_request` emitted by the agent. `response` is one of
|
|
317
|
+
// `once` | `always` | `reject`.
|
|
318
|
+
if (type === 'permission_response') {
|
|
319
|
+
const permissionID = frame.permissionID ?? frame.permission_id;
|
|
320
|
+
const response = frame.response;
|
|
321
|
+
if (typeof permissionID !== 'string' || !permissionID) {
|
|
322
|
+
throw new Error(
|
|
323
|
+
'Invalid permission_response frame: expected string "permissionID"'
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
if (response !== 'once' && response !== 'always' && response !== 'reject') {
|
|
327
|
+
throw new Error(
|
|
328
|
+
'Invalid permission_response frame: "response" must be "once", "always" or "reject"'
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
return {
|
|
332
|
+
kind: 'permission_response',
|
|
333
|
+
permissionID,
|
|
334
|
+
response,
|
|
335
|
+
raw: input,
|
|
336
|
+
parsed: frame,
|
|
337
|
+
format: 'stream-json',
|
|
338
|
+
inputType: type,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (type === 'user' || type === 'user_prompt' || type === undefined) {
|
|
343
|
+
const message = extractFrameText(frame);
|
|
344
|
+
if (message === null) {
|
|
345
|
+
throw new Error(
|
|
346
|
+
'Invalid stream-json user frame: expected message content text'
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
return {
|
|
350
|
+
kind: 'message',
|
|
351
|
+
message,
|
|
352
|
+
raw: input,
|
|
353
|
+
parsed: frame,
|
|
354
|
+
format: 'stream-json',
|
|
355
|
+
inputType: type || 'message',
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
throw new Error(`Unsupported stream-json input frame type: ${String(type)}`);
|
|
360
|
+
}
|
|
361
|
+
|
|
142
362
|
/**
|
|
143
363
|
* Create a continuous stdin reader that queues input lines
|
|
144
364
|
* @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
|
|
|
@@ -191,5 +211,28 @@ export function buildRunOptions(yargs, defaultOptions = {}) {
|
|
|
191
211
|
type: 'number',
|
|
192
212
|
description:
|
|
193
213
|
'Override the temperature for model completions. When not set, the default per-model temperature is used.',
|
|
214
|
+
})
|
|
215
|
+
.option('read-only', {
|
|
216
|
+
type: 'boolean',
|
|
217
|
+
description:
|
|
218
|
+
'Enforceable read-only / planning mode. Disables all filesystem-mutating and shell tools (bash, edit, write, multiedit, patch) so the agent can only read, search and plan. Maps to agent-commander --read-only.',
|
|
219
|
+
default: false,
|
|
220
|
+
})
|
|
221
|
+
.option('disable-tools', {
|
|
222
|
+
type: 'string',
|
|
223
|
+
description:
|
|
224
|
+
'Comma-separated list of tool ids to disable (e.g. "bash,write,edit"). Disabled tools are never exposed to the model and are rejected if invoked via batch.',
|
|
225
|
+
})
|
|
226
|
+
.option('permission-mode', {
|
|
227
|
+
type: 'string',
|
|
228
|
+
choices: ['auto', 'plan', 'readonly', 'ask'],
|
|
229
|
+
description:
|
|
230
|
+
'Permission policy for mutating tools. "auto" (default) allows everything; "plan" allows read-only shell and asks before mutations; "readonly" denies all mutations; "ask" asks before every mutating tool. Approvals are driven over JSON (permission_request / permission_response).',
|
|
231
|
+
default: 'auto',
|
|
232
|
+
})
|
|
233
|
+
.option('permission', {
|
|
234
|
+
type: 'string',
|
|
235
|
+
description:
|
|
236
|
+
'OpenCode-compatible permission override as JSON, merged on top of --permission-mode. Example: \'{"edit":"ask","bash":{"git push*":"ask","*":"allow"},"webfetch":"allow"}\'.',
|
|
194
237
|
});
|
|
195
238
|
}
|