@link-assistant/agent 0.22.9 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -5
- package/package.json +3 -3
- package/src/cli/argv.ts +1 -1
- package/src/cli/continuous-mode.js +117 -28
- package/src/cli/defaults.ts +1 -62
- package/src/cli/input-queue.js +169 -5
- package/src/cli/model-config.js +70 -34
- package/src/cli/output.ts +1 -1
- package/src/cli/run-options.js +50 -12
- package/src/config/defaults.ts +170 -0
- package/src/index.js +84 -24
- package/src/provider/opencode-zen.ts +138 -0
- package/src/provider/provider.ts +63 -12
- package/src/tool/task.ts +4 -3
package/README.md
CHANGED
|
@@ -195,6 +195,9 @@ echo "hi" | agent
|
|
|
195
195
|
|
|
196
196
|
# Other free models (in order of recommendation)
|
|
197
197
|
echo "hi" | agent --model opencode/minimax-m2.5-free
|
|
198
|
+
echo "hi" | agent --model opencode/ling-2.6-flash-free
|
|
199
|
+
echo "hi" | agent --model opencode/hy3-preview-free
|
|
200
|
+
echo "hi" | agent --model opencode/nemotron-3-super-free
|
|
198
201
|
echo "hi" | agent --model opencode/gpt-5-nano
|
|
199
202
|
echo "hi" | agent --model opencode/big-pickle
|
|
200
203
|
|
|
@@ -602,13 +605,13 @@ bun run src/index.js
|
|
|
602
605
|
|
|
603
606
|
```bash
|
|
604
607
|
# Run all tests
|
|
605
|
-
|
|
608
|
+
npm test
|
|
606
609
|
|
|
607
610
|
# Run specific test file
|
|
608
|
-
bun test tests/mcp.
|
|
609
|
-
bun test tests/websearch.tools.
|
|
610
|
-
bun test tests/batch.tools.
|
|
611
|
-
bun test tests/plaintext.input.
|
|
611
|
+
bun test ./tests/integration/mcp.js
|
|
612
|
+
bun test ./tests/integration/websearch.tools.js
|
|
613
|
+
bun test ./tests/integration/batch.tools.js
|
|
614
|
+
bun test ./tests/integration/plaintext.input.js
|
|
612
615
|
```
|
|
613
616
|
|
|
614
617
|
For detailed testing information including how to run tests manually and trigger CI tests, see [TESTING.md](../TESTING.md).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@link-assistant/agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "A minimal, public domain AI CLI agent compatible with OpenCode's JSON interface. Bun-only runtime.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
11
|
"dev": "bun run src/index.js",
|
|
12
|
-
"test": "bun test tests/*.
|
|
13
|
-
"test:integration": "bun test tests/integration/basic.
|
|
12
|
+
"test": "bun test ./tests/*.js ./tests/*.ts",
|
|
13
|
+
"test:integration": "bun test ./tests/integration/basic.js",
|
|
14
14
|
"lint": "eslint .",
|
|
15
15
|
"lint:fix": "eslint . --fix",
|
|
16
16
|
"format": "prettier --write .",
|
package/src/cli/argv.ts
CHANGED
|
@@ -105,7 +105,7 @@ export function getCompactionSafetyMarginFromProcessArgv(): string | null {
|
|
|
105
105
|
/**
|
|
106
106
|
* Extract --compaction-models argument directly from process.argv
|
|
107
107
|
* The value is a links notation references sequence, e.g.:
|
|
108
|
-
* "(big-pickle minimax-m2.5-free nemotron-3-super-free gpt-5-nano same)"
|
|
108
|
+
* "(big-pickle minimax-m2.5-free nemotron-3-super-free hy3-preview-free ling-2.6-flash-free gpt-5-nano same)"
|
|
109
109
|
* @returns The compaction models argument from CLI or null if not found
|
|
110
110
|
* @see https://github.com/link-assistant/agent/issues/232
|
|
111
111
|
*/
|
|
@@ -8,7 +8,7 @@ import { Instance } from '../project/instance.ts';
|
|
|
8
8
|
import { Bus } from '../bus/index.ts';
|
|
9
9
|
import { Session } from '../session/index.ts';
|
|
10
10
|
import { SessionPrompt } from '../session/prompt.ts';
|
|
11
|
-
import { createEventHandler } from '../json-standard/index.ts';
|
|
11
|
+
import { createEventHandler, serializeOutput } from '../json-standard/index.ts';
|
|
12
12
|
import { createContinuousStdinReader } from './input-queue.js';
|
|
13
13
|
import { Log } from '../util/log.ts';
|
|
14
14
|
import { config } from '../config/config.ts';
|
|
@@ -40,6 +40,64 @@ export function getHasError() {
|
|
|
40
40
|
// Logger for resume operations
|
|
41
41
|
const log = Log.create({ service: 'resume' });
|
|
42
42
|
|
|
43
|
+
function getInputFormat(argv) {
|
|
44
|
+
return argv['input-format'] || argv.inputFormat || 'text';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function outputConsumedInput({
|
|
48
|
+
message,
|
|
49
|
+
jsonStandard,
|
|
50
|
+
sessionID,
|
|
51
|
+
compactJson,
|
|
52
|
+
}) {
|
|
53
|
+
const raw = message.raw || message.message || message.system || '';
|
|
54
|
+
if (
|
|
55
|
+
jsonStandard === 'claude' &&
|
|
56
|
+
message.kind === 'message' &&
|
|
57
|
+
message.format === 'stream-json'
|
|
58
|
+
) {
|
|
59
|
+
process.stdout.write(
|
|
60
|
+
serializeOutput(
|
|
61
|
+
{
|
|
62
|
+
type: 'message',
|
|
63
|
+
timestamp: new Date().toISOString(),
|
|
64
|
+
session_id: sessionID,
|
|
65
|
+
role: 'user',
|
|
66
|
+
content: [{ type: 'text', text: message.message ?? '' }],
|
|
67
|
+
},
|
|
68
|
+
'claude'
|
|
69
|
+
)
|
|
70
|
+
);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (jsonStandard === 'claude') {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
outputInput(
|
|
79
|
+
{
|
|
80
|
+
raw,
|
|
81
|
+
parsed: message.parsed || message,
|
|
82
|
+
format: message.format || 'text',
|
|
83
|
+
kind: message.kind || 'message',
|
|
84
|
+
},
|
|
85
|
+
compactJson
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function outputInputParseError(error, line, compactJson) {
|
|
90
|
+
hasError = true;
|
|
91
|
+
outputError(
|
|
92
|
+
{
|
|
93
|
+
errorType: 'ValidationError',
|
|
94
|
+
message: error instanceof Error ? error.message : String(error),
|
|
95
|
+
raw: line,
|
|
96
|
+
},
|
|
97
|
+
compactJson
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
43
101
|
/**
|
|
44
102
|
* Resolve the session to use based on --resume, --continue, and --no-fork options.
|
|
45
103
|
* Returns the session ID to use, handling forking as needed.
|
|
@@ -51,7 +109,8 @@ const log = Log.create({ service: 'resume' });
|
|
|
51
109
|
export async function resolveResumeSession(argv, compactJson) {
|
|
52
110
|
const resumeSessionID = argv.resume;
|
|
53
111
|
const shouldContinue = argv.continue === true;
|
|
54
|
-
const noFork =
|
|
112
|
+
const noFork =
|
|
113
|
+
argv['no-fork'] === true || argv.noFork === true || argv.fork === false;
|
|
55
114
|
|
|
56
115
|
// If neither --resume nor --continue is specified, return null to create new session
|
|
57
116
|
if (!resumeSessionID && !shouldContinue) {
|
|
@@ -199,8 +258,12 @@ export async function runContinuousServerMode(
|
|
|
199
258
|
) {
|
|
200
259
|
// Check both CLI flag and environment variable for compact JSON mode
|
|
201
260
|
const compactJson = argv['compact-json'] === true || config.compactJson;
|
|
261
|
+
const inputFormat = getInputFormat(argv);
|
|
202
262
|
const isInteractive = argv.interactive !== false;
|
|
203
|
-
const autoMerge =
|
|
263
|
+
const autoMerge =
|
|
264
|
+
inputFormat === 'stream-json'
|
|
265
|
+
? false
|
|
266
|
+
: argv['auto-merge-queued-messages'] !== false;
|
|
204
267
|
|
|
205
268
|
// Start server like OpenCode does
|
|
206
269
|
const server = Server.listen({ port: 0, hostname: '127.0.0.1' });
|
|
@@ -243,9 +306,23 @@ export async function runContinuousServerMode(
|
|
|
243
306
|
// Track if we're currently processing a message
|
|
244
307
|
let isProcessing = false;
|
|
245
308
|
const pendingMessages = [];
|
|
309
|
+
let currentSystemMessage = systemMessage;
|
|
310
|
+
const currentAppendSystemMessage = appendSystemMessage;
|
|
246
311
|
|
|
247
312
|
// Process messages from the queue
|
|
248
313
|
const processMessage = async (message) => {
|
|
314
|
+
if (message.kind === 'interrupt') {
|
|
315
|
+
SessionPrompt.cancel(sessionID);
|
|
316
|
+
outputConsumedInput({ message, jsonStandard, sessionID, compactJson });
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (message.kind === 'system') {
|
|
321
|
+
currentSystemMessage = message.system;
|
|
322
|
+
outputConsumedInput({ message, jsonStandard, sessionID, compactJson });
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
249
326
|
if (isProcessing) {
|
|
250
327
|
pendingMessages.push(message);
|
|
251
328
|
return;
|
|
@@ -254,16 +331,9 @@ export async function runContinuousServerMode(
|
|
|
254
331
|
isProcessing = true;
|
|
255
332
|
|
|
256
333
|
// Output input confirmation in JSON format
|
|
257
|
-
|
|
258
|
-
{
|
|
259
|
-
raw: message.raw || message.message,
|
|
260
|
-
parsed: message,
|
|
261
|
-
format: message.format || 'text',
|
|
262
|
-
},
|
|
263
|
-
compactJson
|
|
264
|
-
);
|
|
334
|
+
outputConsumedInput({ message, jsonStandard, sessionID, compactJson });
|
|
265
335
|
|
|
266
|
-
const messageText = message.message
|
|
336
|
+
const messageText = message.message ?? 'hi';
|
|
267
337
|
const parts = [{ type: 'text', text: messageText }];
|
|
268
338
|
|
|
269
339
|
// Create a promise to wait for this message to complete
|
|
@@ -289,8 +359,8 @@ export async function runContinuousServerMode(
|
|
|
289
359
|
parts,
|
|
290
360
|
model: { providerID, modelID },
|
|
291
361
|
compactionModel,
|
|
292
|
-
system:
|
|
293
|
-
appendSystem:
|
|
362
|
+
system: currentSystemMessage,
|
|
363
|
+
appendSystem: currentAppendSystemMessage,
|
|
294
364
|
temperature,
|
|
295
365
|
}),
|
|
296
366
|
}
|
|
@@ -376,11 +446,15 @@ export async function runContinuousServerMode(
|
|
|
376
446
|
|
|
377
447
|
// Create continuous stdin reader
|
|
378
448
|
stdinReader = createContinuousStdinReader({
|
|
379
|
-
interactive: isInteractive,
|
|
449
|
+
interactive: inputFormat === 'stream-json' ? false : isInteractive,
|
|
380
450
|
autoMerge,
|
|
451
|
+
inputFormat,
|
|
381
452
|
onMessage: (message) => {
|
|
382
453
|
processMessage(message);
|
|
383
454
|
},
|
|
455
|
+
onError: (error, line) => {
|
|
456
|
+
outputInputParseError(error, line, compactJson);
|
|
457
|
+
},
|
|
384
458
|
});
|
|
385
459
|
|
|
386
460
|
// Wait for stdin to end (EOF or close)
|
|
@@ -453,8 +527,12 @@ export async function runContinuousDirectMode(
|
|
|
453
527
|
) {
|
|
454
528
|
// Check both CLI flag and environment variable for compact JSON mode
|
|
455
529
|
const compactJson = argv['compact-json'] === true || config.compactJson;
|
|
530
|
+
const inputFormat = getInputFormat(argv);
|
|
456
531
|
const isInteractive = argv.interactive !== false;
|
|
457
|
-
const autoMerge =
|
|
532
|
+
const autoMerge =
|
|
533
|
+
inputFormat === 'stream-json'
|
|
534
|
+
? false
|
|
535
|
+
: argv['auto-merge-queued-messages'] !== false;
|
|
458
536
|
|
|
459
537
|
let unsub = null;
|
|
460
538
|
let stdinReader = null;
|
|
@@ -482,9 +560,23 @@ export async function runContinuousDirectMode(
|
|
|
482
560
|
// Track if we're currently processing a message
|
|
483
561
|
let isProcessing = false;
|
|
484
562
|
const pendingMessages = [];
|
|
563
|
+
let currentSystemMessage = systemMessage;
|
|
564
|
+
const currentAppendSystemMessage = appendSystemMessage;
|
|
485
565
|
|
|
486
566
|
// Process messages from the queue
|
|
487
567
|
const processMessage = async (message) => {
|
|
568
|
+
if (message.kind === 'interrupt') {
|
|
569
|
+
SessionPrompt.cancel(sessionID);
|
|
570
|
+
outputConsumedInput({ message, jsonStandard, sessionID, compactJson });
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if (message.kind === 'system') {
|
|
575
|
+
currentSystemMessage = message.system;
|
|
576
|
+
outputConsumedInput({ message, jsonStandard, sessionID, compactJson });
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
|
|
488
580
|
if (isProcessing) {
|
|
489
581
|
pendingMessages.push(message);
|
|
490
582
|
return;
|
|
@@ -493,16 +585,9 @@ export async function runContinuousDirectMode(
|
|
|
493
585
|
isProcessing = true;
|
|
494
586
|
|
|
495
587
|
// Output input confirmation in JSON format
|
|
496
|
-
|
|
497
|
-
{
|
|
498
|
-
raw: message.raw || message.message,
|
|
499
|
-
parsed: message,
|
|
500
|
-
format: message.format || 'text',
|
|
501
|
-
},
|
|
502
|
-
compactJson
|
|
503
|
-
);
|
|
588
|
+
outputConsumedInput({ message, jsonStandard, sessionID, compactJson });
|
|
504
589
|
|
|
505
|
-
const messageText = message.message
|
|
590
|
+
const messageText = message.message ?? 'hi';
|
|
506
591
|
const parts = [{ type: 'text', text: messageText }];
|
|
507
592
|
|
|
508
593
|
// Create a promise to wait for this message to complete
|
|
@@ -524,8 +609,8 @@ export async function runContinuousDirectMode(
|
|
|
524
609
|
parts,
|
|
525
610
|
model: { providerID, modelID },
|
|
526
611
|
compactionModel,
|
|
527
|
-
system:
|
|
528
|
-
appendSystem:
|
|
612
|
+
system: currentSystemMessage,
|
|
613
|
+
appendSystem: currentAppendSystemMessage,
|
|
529
614
|
temperature,
|
|
530
615
|
}).catch((error) => {
|
|
531
616
|
hasError = true;
|
|
@@ -609,11 +694,15 @@ export async function runContinuousDirectMode(
|
|
|
609
694
|
|
|
610
695
|
// Create continuous stdin reader
|
|
611
696
|
stdinReader = createContinuousStdinReader({
|
|
612
|
-
interactive: isInteractive,
|
|
697
|
+
interactive: inputFormat === 'stream-json' ? false : isInteractive,
|
|
613
698
|
autoMerge,
|
|
699
|
+
inputFormat,
|
|
614
700
|
onMessage: (message) => {
|
|
615
701
|
processMessage(message);
|
|
616
702
|
},
|
|
703
|
+
onError: (error, line) => {
|
|
704
|
+
outputInputParseError(error, line, compactJson);
|
|
705
|
+
},
|
|
617
706
|
});
|
|
618
707
|
|
|
619
708
|
// Wait for stdin to end (EOF or close)
|
package/src/cli/defaults.ts
CHANGED
|
@@ -1,62 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Default CLI configuration values.
|
|
3
|
-
*
|
|
4
|
-
* Centralizing defaults here ensures all code references the same value (#208).
|
|
5
|
-
* When the default model changes, update this file only.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
/** Default model used when no `--model` CLI argument is provided. */
|
|
9
|
-
export const DEFAULT_MODEL = 'opencode/nemotron-3-super-free';
|
|
10
|
-
|
|
11
|
-
/** Default provider ID extracted from DEFAULT_MODEL. */
|
|
12
|
-
export const DEFAULT_PROVIDER_ID = DEFAULT_MODEL.split('/')[0];
|
|
13
|
-
|
|
14
|
-
/** Default model ID extracted from DEFAULT_MODEL. */
|
|
15
|
-
export const DEFAULT_MODEL_ID = DEFAULT_MODEL.split('/').slice(1).join('/');
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Default compaction model used when no `--compaction-model` CLI argument is provided.
|
|
19
|
-
* gpt-5-nano has a 400K context window, larger than most free base models (~200K),
|
|
20
|
-
* which allows compacting 100% of the base model's context without a safety margin.
|
|
21
|
-
* The special value "same" means use the same model as `--model`.
|
|
22
|
-
* @see https://github.com/link-assistant/agent/issues/219
|
|
23
|
-
*/
|
|
24
|
-
export const DEFAULT_COMPACTION_MODEL = 'opencode/gpt-5-nano';
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Default compaction models cascade, ordered from smallest/cheapest context to largest.
|
|
28
|
-
* During compaction, the system tries each model in order. If the used context exceeds
|
|
29
|
-
* a model's context limit, it skips to the next larger model. If a model's rate limit
|
|
30
|
-
* is reached, it also skips to the next model.
|
|
31
|
-
* The special value "same" means use the same model as `--model`.
|
|
32
|
-
*
|
|
33
|
-
* Parsed as links notation references sequence (single anonymous link):
|
|
34
|
-
* "(big-pickle minimax-m2.5-free nemotron-3-super-free gpt-5-nano same)"
|
|
35
|
-
*
|
|
36
|
-
* Context limits (approximate):
|
|
37
|
-
* big-pickle: ~200K
|
|
38
|
-
* minimax-m2.5-free: ~200K
|
|
39
|
-
* nemotron-3-super-free: ~262K (default model)
|
|
40
|
-
* gpt-5-nano: ~400K
|
|
41
|
-
* same: (base model's context)
|
|
42
|
-
*
|
|
43
|
-
* Note: qwen3.6-plus-free was removed — free promotion ended April 2026.
|
|
44
|
-
* @see https://github.com/link-assistant/agent/issues/242
|
|
45
|
-
* @see https://github.com/link-assistant/agent/issues/232
|
|
46
|
-
*/
|
|
47
|
-
export const DEFAULT_COMPACTION_MODELS =
|
|
48
|
-
'(big-pickle minimax-m2.5-free nemotron-3-super-free gpt-5-nano same)';
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Default compaction safety margin as a percentage of usable context window.
|
|
52
|
-
* Applied only when the compaction model has a context window equal to or smaller
|
|
53
|
-
* than the base model. When the compaction model has a larger context, the margin
|
|
54
|
-
* is automatically set to 0 (allowing 100% context usage).
|
|
55
|
-
*
|
|
56
|
-
* Increased from 15% to 25% to reduce probability of context overflow errors,
|
|
57
|
-
* especially when providers return inaccurate or zero token counts.
|
|
58
|
-
* Matches OpenCode upstream's 75% threshold (25% margin).
|
|
59
|
-
* @see https://github.com/link-assistant/agent/issues/219
|
|
60
|
-
* @see https://github.com/link-assistant/agent/issues/249
|
|
61
|
-
*/
|
|
62
|
-
export const DEFAULT_COMPACTION_SAFETY_MARGIN_PERCENT = 25;
|
|
1
|
+
export * from '../config/defaults.ts';
|
package/src/cli/input-queue.js
CHANGED
|
@@ -5,11 +5,13 @@
|
|
|
5
5
|
export class InputQueue {
|
|
6
6
|
constructor(options = {}) {
|
|
7
7
|
this.queue = [];
|
|
8
|
+
this.inputFormat = options.inputFormat || 'text';
|
|
8
9
|
this.autoMerge = options.autoMerge !== false; // enabled by default
|
|
9
10
|
this.mergeDelayMs = options.mergeDelayMs || 50; // delay to wait for more lines
|
|
10
11
|
this.pendingLines = [];
|
|
11
12
|
this.mergeTimer = null;
|
|
12
13
|
this.onMessage = options.onMessage || (() => {});
|
|
14
|
+
this.onError = options.onError || (() => {});
|
|
13
15
|
this.interactive = options.interactive !== false; // enabled by default
|
|
14
16
|
}
|
|
15
17
|
|
|
@@ -24,6 +26,10 @@ export class InputQueue {
|
|
|
24
26
|
return null;
|
|
25
27
|
}
|
|
26
28
|
|
|
29
|
+
if (this.inputFormat === 'stream-json') {
|
|
30
|
+
return parseStreamJsonInput(trimmed);
|
|
31
|
+
}
|
|
32
|
+
|
|
27
33
|
try {
|
|
28
34
|
const parsed = JSON.parse(trimmed);
|
|
29
35
|
// If it has a message field, use it directly
|
|
@@ -43,6 +49,11 @@ export class InputQueue {
|
|
|
43
49
|
* @param {string} line - Input line
|
|
44
50
|
*/
|
|
45
51
|
addLine(line) {
|
|
52
|
+
if (this.inputFormat === 'stream-json') {
|
|
53
|
+
this.queueLine(line);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
46
57
|
if (!this.interactive && !line.trim().startsWith('{')) {
|
|
47
58
|
// In non-interactive mode, only accept JSON
|
|
48
59
|
return;
|
|
@@ -54,11 +65,7 @@ export class InputQueue {
|
|
|
54
65
|
this.scheduleMerge();
|
|
55
66
|
} else {
|
|
56
67
|
// No merging, queue immediately
|
|
57
|
-
|
|
58
|
-
if (parsed) {
|
|
59
|
-
this.queue.push(parsed);
|
|
60
|
-
this.notifyMessage(parsed);
|
|
61
|
-
}
|
|
68
|
+
this.queueLine(line);
|
|
62
69
|
}
|
|
63
70
|
}
|
|
64
71
|
|
|
@@ -130,6 +137,33 @@ export class InputQueue {
|
|
|
130
137
|
}
|
|
131
138
|
}
|
|
132
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Notify listener of an input parse error
|
|
142
|
+
* @param {Error} error - Parse error
|
|
143
|
+
* @param {string} line - Raw input line
|
|
144
|
+
*/
|
|
145
|
+
notifyError(error, line) {
|
|
146
|
+
if (this.onError) {
|
|
147
|
+
this.onError(error, line);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Parse and enqueue one complete input line
|
|
153
|
+
* @param {string} line - Raw input line
|
|
154
|
+
*/
|
|
155
|
+
queueLine(line) {
|
|
156
|
+
try {
|
|
157
|
+
const parsed = this.parseInput(line);
|
|
158
|
+
if (parsed) {
|
|
159
|
+
this.queue.push(parsed);
|
|
160
|
+
this.notifyMessage(parsed);
|
|
161
|
+
}
|
|
162
|
+
} catch (error) {
|
|
163
|
+
this.notifyError(error, line);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
133
167
|
/**
|
|
134
168
|
* Get queue size
|
|
135
169
|
* @returns {number}
|
|
@@ -139,6 +173,136 @@ export class InputQueue {
|
|
|
139
173
|
}
|
|
140
174
|
}
|
|
141
175
|
|
|
176
|
+
function extractContentText(content) {
|
|
177
|
+
if (typeof content === 'string') {
|
|
178
|
+
return content;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (Array.isArray(content)) {
|
|
182
|
+
const parts = [];
|
|
183
|
+
for (const part of content) {
|
|
184
|
+
if (typeof part === 'string') {
|
|
185
|
+
parts.push(part);
|
|
186
|
+
} else if (part && typeof part === 'object') {
|
|
187
|
+
if (typeof part.text === 'string') {
|
|
188
|
+
parts.push(part.text);
|
|
189
|
+
} else if (typeof part.content === 'string') {
|
|
190
|
+
parts.push(part.content);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return parts.length > 0 ? parts.join('\n') : null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (content && typeof content === 'object') {
|
|
198
|
+
if (typeof content.text === 'string') {
|
|
199
|
+
return content.text;
|
|
200
|
+
}
|
|
201
|
+
if ('content' in content) {
|
|
202
|
+
return extractContentText(content.content);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function extractFrameText(frame) {
|
|
210
|
+
if (typeof frame.message === 'string') {
|
|
211
|
+
return frame.message;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (frame.message && typeof frame.message === 'object') {
|
|
215
|
+
const messageText = extractContentText(frame.message.content);
|
|
216
|
+
if (messageText !== null) {
|
|
217
|
+
return messageText;
|
|
218
|
+
}
|
|
219
|
+
if (typeof frame.message.text === 'string') {
|
|
220
|
+
return frame.message.text;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const contentText = extractContentText(frame.content);
|
|
225
|
+
if (contentText !== null) {
|
|
226
|
+
return contentText;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (typeof frame.text === 'string') {
|
|
230
|
+
return frame.text;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Parse one Claude-compatible stream-json input frame.
|
|
238
|
+
* @param {string} input - One JSONL frame
|
|
239
|
+
* @returns {object} Normalized queue message
|
|
240
|
+
*/
|
|
241
|
+
export function parseStreamJsonInput(input) {
|
|
242
|
+
let frame;
|
|
243
|
+
try {
|
|
244
|
+
frame = JSON.parse(input);
|
|
245
|
+
} catch (error) {
|
|
246
|
+
throw new Error(
|
|
247
|
+
`Invalid stream-json input frame: ${
|
|
248
|
+
error instanceof Error ? error.message : String(error)
|
|
249
|
+
}`
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (!frame || typeof frame !== 'object' || Array.isArray(frame)) {
|
|
254
|
+
throw new Error('Invalid stream-json input frame: expected JSON object');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const type = frame.type;
|
|
258
|
+
|
|
259
|
+
if (type === 'interrupt') {
|
|
260
|
+
return {
|
|
261
|
+
kind: 'interrupt',
|
|
262
|
+
raw: input,
|
|
263
|
+
parsed: frame,
|
|
264
|
+
format: 'stream-json',
|
|
265
|
+
inputType: type,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (type === 'system') {
|
|
270
|
+
const system = extractFrameText(frame);
|
|
271
|
+
if (system === null) {
|
|
272
|
+
throw new Error(
|
|
273
|
+
'Invalid stream-json system frame: expected content text'
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
return {
|
|
277
|
+
kind: 'system',
|
|
278
|
+
system,
|
|
279
|
+
raw: input,
|
|
280
|
+
parsed: frame,
|
|
281
|
+
format: 'stream-json',
|
|
282
|
+
inputType: type,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (type === 'user' || type === 'user_prompt' || type === undefined) {
|
|
287
|
+
const message = extractFrameText(frame);
|
|
288
|
+
if (message === null) {
|
|
289
|
+
throw new Error(
|
|
290
|
+
'Invalid stream-json user frame: expected message content text'
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
return {
|
|
294
|
+
kind: 'message',
|
|
295
|
+
message,
|
|
296
|
+
raw: input,
|
|
297
|
+
parsed: frame,
|
|
298
|
+
format: 'stream-json',
|
|
299
|
+
inputType: type || 'message',
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
throw new Error(`Unsupported stream-json input frame type: ${String(type)}`);
|
|
304
|
+
}
|
|
305
|
+
|
|
142
306
|
/**
|
|
143
307
|
* Create a continuous stdin reader that queues input lines
|
|
144
308
|
* @param {object} options - Options for the reader
|