@link-assistant/agent 0.24.0 → 0.24.2

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 CHANGED
@@ -286,6 +286,10 @@ Options:
286
286
  Default: opencode/minimax-m2.5-free
287
287
  --json-standard JSON output format standard
288
288
  Choices: "opencode" (default), "claude" (experimental)
289
+ --output-format Alias for JSON output format
290
+ Choices: "json" (OpenCode), "stream-json" (Claude NDJSON)
291
+ --input-format Input format
292
+ Choices: "text" (default), "stream-json" (Claude JSONL)
289
293
  --use-existing-claude-oauth Use existing Claude OAuth credentials
290
294
  from ~/.claude/.credentials.json
291
295
  --system-message Full override of the system message
@@ -390,6 +394,21 @@ echo "your message here" | agent
390
394
  }
391
395
  ```
392
396
 
397
+ **Live `stream-json`:**
398
+
399
+ For bidirectional drivers, use:
400
+
401
+ ```bash
402
+ agent --input-format stream-json --output-format stream-json
403
+ ```
404
+
405
+ The live contract supports `user_prompt`, `system`, `interrupt`, and
406
+ `permission_response` frames on stdin. It emits replay acknowledgements for
407
+ consumed user frames and an explicit idle/turn-boundary event
408
+ (`session_idle` in OpenCode output, `idle` in Claude output). See
409
+ [docs/stdin-mode.md](../docs/stdin-mode.md#live-stream-json-contract) for the
410
+ session resume and steering semantics.
411
+
393
412
  ## Supported Tools
394
413
 
395
414
  All 13 tools are **enabled by default** with **no configuration required**. See [TOOLS.md](../TOOLS.md) for complete documentation.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/agent",
3
- "version": "0.24.0",
3
+ "version": "0.24.2",
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",
@@ -10,6 +10,7 @@ import { Session } from '../session/index.ts';
10
10
  import { SessionPrompt } from '../session/prompt.ts';
11
11
  import { createEventHandler, serializeOutput } from '../json-standard/index.ts';
12
12
  import { createContinuousStdinReader } from './input-queue.js';
13
+ import { outputBusEvent } from './event-handler.js';
13
14
  import { Permission } from '../permission/index.ts';
14
15
  import { Log } from '../util/log.ts';
15
16
  import { config } from '../config/config.ts';
@@ -400,62 +401,14 @@ export async function runContinuousServerMode(
400
401
 
401
402
  // Subscribe to all bus events and output in selected format
402
403
  unsub = Bus.subscribeAll((event) => {
403
- if (event.type === 'message.part.updated') {
404
- const part = event.properties.part;
405
- if (part.sessionID !== sessionID) {
406
- return;
407
- }
408
-
409
- if (part.type === 'step-start') {
410
- eventHandler.output({
411
- type: 'step_start',
412
- timestamp: Date.now(),
413
- sessionID,
414
- part,
415
- });
416
- }
417
-
418
- if (part.type === 'step-finish') {
419
- eventHandler.output({
420
- type: 'step_finish',
421
- timestamp: Date.now(),
422
- sessionID,
423
- part,
424
- });
425
- }
426
-
427
- if (part.type === 'text' && part.time?.end) {
428
- eventHandler.output({
429
- type: 'text',
430
- timestamp: Date.now(),
431
- sessionID,
432
- part,
433
- });
434
- }
435
-
436
- if (part.type === 'tool') {
437
- eventHandler.output({
438
- type: 'tool_use',
439
- timestamp: Date.now(),
440
- sessionID,
441
- part,
442
- });
443
- }
444
- }
445
-
446
- if (event.type === 'session.error') {
447
- const props = event.properties;
448
- if (props.sessionID !== sessionID || !props.error) {
449
- return;
450
- }
451
- hasError = true;
452
- eventHandler.output({
453
- type: 'error',
454
- timestamp: Date.now(),
455
- sessionID,
456
- error: props.error,
457
- });
458
- }
404
+ outputBusEvent({
405
+ event,
406
+ sessionID,
407
+ eventHandler,
408
+ onError: () => {
409
+ hasError = true;
410
+ },
411
+ });
459
412
  });
460
413
 
461
414
  // Create continuous stdin reader
@@ -661,62 +614,14 @@ export async function runContinuousDirectMode(
661
614
 
662
615
  // Subscribe to all bus events and output in selected format
663
616
  unsub = Bus.subscribeAll((event) => {
664
- if (event.type === 'message.part.updated') {
665
- const part = event.properties.part;
666
- if (part.sessionID !== sessionID) {
667
- return;
668
- }
669
-
670
- if (part.type === 'step-start') {
671
- eventHandler.output({
672
- type: 'step_start',
673
- timestamp: Date.now(),
674
- sessionID,
675
- part,
676
- });
677
- }
678
-
679
- if (part.type === 'step-finish') {
680
- eventHandler.output({
681
- type: 'step_finish',
682
- timestamp: Date.now(),
683
- sessionID,
684
- part,
685
- });
686
- }
687
-
688
- if (part.type === 'text' && part.time?.end) {
689
- eventHandler.output({
690
- type: 'text',
691
- timestamp: Date.now(),
692
- sessionID,
693
- part,
694
- });
695
- }
696
-
697
- if (part.type === 'tool') {
698
- eventHandler.output({
699
- type: 'tool_use',
700
- timestamp: Date.now(),
701
- sessionID,
702
- part,
703
- });
704
- }
705
- }
706
-
707
- if (event.type === 'session.error') {
708
- const props = event.properties;
709
- if (props.sessionID !== sessionID || !props.error) {
710
- return;
711
- }
712
- hasError = true;
713
- eventHandler.output({
714
- type: 'error',
715
- timestamp: Date.now(),
716
- sessionID,
717
- error: props.error,
718
- });
719
- }
617
+ outputBusEvent({
618
+ event,
619
+ sessionID,
620
+ eventHandler,
621
+ onError: () => {
622
+ hasError = true;
623
+ },
624
+ });
720
625
  });
721
626
 
722
627
  // Create continuous stdin reader
@@ -26,105 +26,126 @@ export function createBusEventSubscription({
26
26
  });
27
27
 
28
28
  const unsub = Bus.subscribeAll((event) => {
29
- // Output events in selected JSON format
30
- if (event.type === 'message.part.updated') {
31
- const part = event.properties.part;
32
- if (part.sessionID !== sessionID) {
33
- return;
34
- }
35
-
36
- // Output different event types
37
- if (part.type === 'step-start') {
38
- eventHandler.output({
39
- type: 'step_start',
40
- timestamp: Date.now(),
41
- sessionID,
42
- part,
43
- });
44
- }
45
-
46
- if (part.type === 'step-finish') {
47
- eventHandler.output({
48
- type: 'step_finish',
49
- timestamp: Date.now(),
50
- sessionID,
51
- part,
52
- });
53
- }
29
+ outputBusEvent({
30
+ event,
31
+ sessionID,
32
+ eventHandler,
33
+ onError,
34
+ onIdle: idleResolve,
35
+ });
36
+ });
54
37
 
55
- if (part.type === 'text' && part.time?.end) {
56
- eventHandler.output({
57
- type: 'text',
58
- timestamp: Date.now(),
59
- sessionID,
60
- part,
61
- });
62
- }
38
+ return { unsub, idlePromise };
39
+ }
63
40
 
64
- if (part.type === 'tool') {
65
- eventHandler.output({
66
- type: 'tool_use',
67
- timestamp: Date.now(),
68
- sessionID,
69
- part,
70
- });
41
+ export function outputBusEvent({
42
+ event,
43
+ sessionID,
44
+ eventHandler,
45
+ onError,
46
+ onIdle,
47
+ }) {
48
+ // Output events in selected JSON format
49
+ if (event.type === 'message.part.updated') {
50
+ const part = event.properties.part;
51
+ if (part.sessionID !== sessionID) {
52
+ return;
53
+ }
71
54
 
72
- // If tool failed, also output an error event
73
- if (part.state?.status === 'error') {
74
- eventHandler.output({
75
- type: 'error',
76
- timestamp: Date.now(),
77
- sessionID,
78
- error: part.state.error || 'Tool execution failed',
79
- });
80
- }
81
- }
55
+ // Output different event types
56
+ if (part.type === 'step-start') {
57
+ eventHandler.output({
58
+ type: 'step_start',
59
+ timestamp: Date.now(),
60
+ sessionID,
61
+ part,
62
+ });
82
63
  }
83
64
 
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
- }
65
+ if (part.type === 'step-finish') {
92
66
  eventHandler.output({
93
- type: 'permission_request',
67
+ type: 'step_finish',
94
68
  timestamp: Date.now(),
95
69
  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,
70
+ part,
102
71
  });
103
72
  }
104
73
 
105
- // Handle session idle to know when to stop
106
- if (
107
- event.type === 'session.idle' &&
108
- event.properties.sessionID === sessionID
109
- ) {
110
- idleResolve();
74
+ if (part.type === 'text' && part.time?.end) {
75
+ eventHandler.output({
76
+ type: 'text',
77
+ timestamp: Date.now(),
78
+ sessionID,
79
+ part,
80
+ });
111
81
  }
112
82
 
113
- // Handle errors
114
- if (event.type === 'session.error') {
115
- const props = event.properties;
116
- if (props.sessionID !== sessionID || !props.error) {
117
- return;
118
- }
119
- onError();
83
+ if (part.type === 'tool') {
120
84
  eventHandler.output({
121
- type: 'error',
85
+ type: 'tool_use',
122
86
  timestamp: Date.now(),
123
87
  sessionID,
124
- error: props.error,
88
+ part,
125
89
  });
90
+
91
+ // If tool failed, also output an error event
92
+ if (part.state?.status === 'error') {
93
+ eventHandler.output({
94
+ type: 'error',
95
+ timestamp: Date.now(),
96
+ sessionID,
97
+ error: part.state.error || 'Tool execution failed',
98
+ });
99
+ }
126
100
  }
127
- });
101
+ }
128
102
 
129
- return { unsub, idlePromise };
103
+ // Emit a JSON permission request when a tool needs approval (issue #271).
104
+ // The consumer replies with a `permission_response` frame over stdin which
105
+ // is routed to Permission.respond. No TUI is involved.
106
+ if (event.type === 'permission.updated') {
107
+ const permission = event.properties;
108
+ if (permission.sessionID !== sessionID) {
109
+ return;
110
+ }
111
+ eventHandler.output({
112
+ type: 'permission_request',
113
+ timestamp: Date.now(),
114
+ sessionID,
115
+ permissionID: permission.id,
116
+ callID: permission.callID,
117
+ tool: permission.type,
118
+ pattern: permission.pattern,
119
+ title: permission.title,
120
+ metadata: permission.metadata,
121
+ });
122
+ }
123
+
124
+ // Handle session idle to know when to stop
125
+ if (
126
+ event.type === 'session.idle' &&
127
+ event.properties.sessionID === sessionID
128
+ ) {
129
+ eventHandler.output({
130
+ type: 'session_idle',
131
+ timestamp: Date.now(),
132
+ sessionID,
133
+ });
134
+ onIdle?.();
135
+ }
136
+
137
+ // Handle errors
138
+ if (event.type === 'session.error') {
139
+ const props = event.properties;
140
+ if (props.sessionID !== sessionID || !props.error) {
141
+ return;
142
+ }
143
+ onError();
144
+ eventHandler.output({
145
+ type: 'error',
146
+ timestamp: Date.now(),
147
+ sessionID,
148
+ error: props.error,
149
+ });
150
+ }
130
151
  }
@@ -302,7 +302,10 @@ async function resolveCompactionModelEntry(
302
302
 
303
303
  // Short name resolution
304
304
  const { Provider } = await import('../provider/provider.ts');
305
- const resolved = await Provider.parseModelWithResolution(modelArg);
305
+ const resolved = await Provider.resolveShortModelName(modelArg);
306
+ if (!resolved) {
307
+ throw new Error('ProviderModelNotFoundError');
308
+ }
306
309
  return {
307
310
  providerID: resolved.providerID,
308
311
  modelID: resolved.modelID,
@@ -336,10 +339,17 @@ async function parseCompactionModelConfig(
336
339
 
337
340
  // Check for --compaction-models (cascade) first — it overrides --compaction-model
338
341
  const cliCompactionModelsArg = getCompactionModelsFromProcessArgv();
342
+ const defaultCompactionModels = getDefaultCompactionModels(defaultOptions);
343
+ const compactionModelsSource =
344
+ cliCompactionModelsArg ||
345
+ (argv['compaction-models'] &&
346
+ argv['compaction-models'] !== defaultCompactionModels)
347
+ ? 'cli'
348
+ : 'default';
339
349
  const compactionModelsArg =
340
350
  cliCompactionModelsArg ??
341
351
  argv['compaction-models'] ??
342
- getDefaultCompactionModels(defaultOptions);
352
+ defaultCompactionModels;
343
353
 
344
354
  // Parse the links notation sequence into an array of model names
345
355
  const modelNames = parseLinksNotationSequence(compactionModelsArg);
@@ -360,8 +370,11 @@ async function parseCompactionModelConfig(
360
370
  useSameModel: resolved.useSameModel,
361
371
  });
362
372
  } catch (err) {
363
- // If a model can't be resolved, log and skip it
364
- Log.Default.warn(() => ({
373
+ const logSkip =
374
+ compactionModelsSource === 'default'
375
+ ? Log.Default.debug
376
+ : Log.Default.warn;
377
+ logSkip(() => ({
365
378
  message: 'skipping unresolvable compaction model in cascade',
366
379
  model: name,
367
380
  error: err?.message,
@@ -374,7 +387,7 @@ async function parseCompactionModelConfig(
374
387
  models: compactionModels.map((m) =>
375
388
  m.useSameModel ? 'same' : `${m.providerID}/${m.modelID}`
376
389
  ),
377
- source: cliCompactionModelsArg ? 'cli' : 'default',
390
+ source: compactionModelsSource,
378
391
  }));
379
392
 
380
393
  // Use the first model as the primary compaction model (for backward compatibility)
@@ -18,7 +18,13 @@ export type JsonStandard = 'opencode' | 'claude';
18
18
  * OpenCode JSON event types
19
19
  */
20
20
  export interface OpenCodeEvent {
21
- type: 'step_start' | 'step_finish' | 'text' | 'tool_use' | 'error';
21
+ type:
22
+ | 'step_start'
23
+ | 'step_finish'
24
+ | 'text'
25
+ | 'tool_use'
26
+ | 'error'
27
+ | 'session_idle';
22
28
  timestamp: number;
23
29
  sessionID: string;
24
30
  part?: Record<string, unknown>;
@@ -29,7 +35,7 @@ export interface OpenCodeEvent {
29
35
  * Claude JSON event types (stream-json format)
30
36
  */
31
37
  export interface ClaudeEvent {
32
- type: 'init' | 'message' | 'tool_use' | 'tool_result' | 'result';
38
+ type: 'init' | 'message' | 'tool_use' | 'tool_result' | 'result' | 'idle';
33
39
  timestamp?: string;
34
40
  session_id?: string;
35
41
  role?: 'assistant' | 'user';
@@ -126,6 +132,13 @@ export function convertOpenCodeToClaude(
126
132
  duration_ms: event.timestamp - startTime,
127
133
  };
128
134
 
135
+ case 'session_idle':
136
+ return {
137
+ type: 'idle',
138
+ timestamp,
139
+ session_id,
140
+ };
141
+
129
142
  case 'error':
130
143
  return {
131
144
  type: 'result',