@link-assistant/agent 0.24.0 → 0.24.1

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.1",
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
  }
@@ -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',