@axhub/genie 0.2.7 → 0.2.9

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.
Files changed (131) hide show
  1. package/LICENSE +21 -675
  2. package/dist/api-docs.html +2 -2
  3. package/dist/assets/App-GBcTeeUS.js +460 -0
  4. package/dist/assets/App-qxJ8_QYu.css +32 -0
  5. package/dist/assets/ReviewApp-C9K--AQE.js +1 -0
  6. package/dist/assets/{_basePickBy-C19AekOu.js → _basePickBy-DR_8uFCo.js} +1 -1
  7. package/dist/assets/{_baseUniq-JsnevLw_.js → _baseUniq-D0njlQ_7.js} +1 -1
  8. package/dist/assets/{arc-BLpcuBlf.js → arc-CKlr_Rec.js} +1 -1
  9. package/dist/assets/architectureDiagram-2XIMDMQ5-BmO_uLUH.js +36 -0
  10. package/dist/assets/{blockDiagram-WCTKOSBZ-DQBLwsUS.js → blockDiagram-WCTKOSBZ-DhAeO-56.js} +3 -3
  11. package/dist/assets/c4Diagram-IC4MRINW-C67kFoXx.js +10 -0
  12. package/dist/assets/channel-V3MBjKys.js +1 -0
  13. package/dist/assets/{chunk-4BX2VUAB-De63kbgc.js → chunk-4BX2VUAB-mLLagvJi.js} +1 -1
  14. package/dist/assets/{chunk-55IACEB6-DtTDDdM9.js → chunk-55IACEB6-Lx-hOjlM.js} +1 -1
  15. package/dist/assets/{chunk-FMBD7UC4-DHuwd8tw.js → chunk-FMBD7UC4-Bt-XmVUV.js} +1 -1
  16. package/dist/assets/{chunk-JSJVCQXG-BgytFtmO.js → chunk-JSJVCQXG-Cya6gaDV.js} +1 -1
  17. package/dist/assets/{chunk-KX2RTZJC-nZdp86aN.js → chunk-KX2RTZJC-Bd7Ig6tF.js} +1 -1
  18. package/dist/assets/chunk-NQ4KR5QH-5UAE0Vg-.js +220 -0
  19. package/dist/assets/{chunk-QZHKN3VN-DvUQ3mnO.js → chunk-QZHKN3VN-BAxZ8m7w.js} +1 -1
  20. package/dist/assets/chunk-WL4C6EOR-DjDPvUUP.js +189 -0
  21. package/dist/assets/classDiagram-VBA2DB6C-C790yYiY.js +1 -0
  22. package/dist/assets/classDiagram-v2-RAHNMMFH-C790yYiY.js +1 -0
  23. package/dist/assets/clone-BbMGfZwt.js +1 -0
  24. package/dist/assets/cose-bilkent-S5V4N54A-D-60XrkJ.js +1 -0
  25. package/dist/assets/cytoscape.esm-2ZfV8NB5.js +331 -0
  26. package/dist/assets/{dagre-KLK3FWXG-CHYIvW47.js → dagre-KLK3FWXG-bqu3ZS4K.js} +1 -1
  27. package/dist/assets/diagram-E7M64L7V-BueeqoYm.js +24 -0
  28. package/dist/assets/{diagram-IFDJBPK2-Dzsiln_C.js → diagram-IFDJBPK2-D4fDv2E7.js} +1 -1
  29. package/dist/assets/{diagram-P4PSJMXO-DKnGbUpE.js → diagram-P4PSJMXO-WqipY3fN.js} +1 -1
  30. package/dist/assets/erDiagram-INFDFZHY-D0oVnO-x.js +70 -0
  31. package/dist/assets/{flowDiagram-PKNHOUZH-BAZ2-jKp.js → flowDiagram-PKNHOUZH-DzbGyxrr.js} +4 -4
  32. package/dist/assets/ganttDiagram-A5KZAMGK-BwhbbgCP.js +292 -0
  33. package/dist/assets/{gitGraphDiagram-K3NZZRJ6-BflpyjGy.js → gitGraphDiagram-K3NZZRJ6-DZgAh_KM.js} +1 -1
  34. package/dist/assets/{graph-suelaXFh.js → graph-DzKos-N0.js} +1 -1
  35. package/dist/assets/highlighted-body-TPN3WLV5-CKDMgz3X.js +1 -0
  36. package/dist/assets/index-DiQlHzGj.js +2 -0
  37. package/dist/assets/index-Drat2nB9.css +1 -0
  38. package/dist/assets/{infoDiagram-LFFYTUFH-pfD1FA3p.js → infoDiagram-LFFYTUFH-BFicZbTf.js} +1 -1
  39. package/dist/assets/ishikawaDiagram-PHBUUO56-CtihxDxl.js +70 -0
  40. package/dist/assets/journeyDiagram-4ABVD52K-Du00J8_d.js +139 -0
  41. package/dist/assets/{kanban-definition-K7BYSVSG-FWinmur1.js → kanban-definition-K7BYSVSG-BJi9S0iQ.js} +5 -5
  42. package/dist/assets/{layout-vcz43XvZ.js → layout-B80Sityu.js} +1 -1
  43. package/dist/assets/{linear-le4gc0vx.js → linear-sRQLOf5H.js} +1 -1
  44. package/dist/assets/mermaid-O7DHMXV3-CBuVs4eJ.js +1038 -0
  45. package/dist/assets/mindmap-definition-YRQLILUH-C5IL_xi-.js +68 -0
  46. package/dist/assets/{pieDiagram-SKSYHLDU-C7PKDh3b.js → pieDiagram-SKSYHLDU-CeTwlJ8z.js} +2 -2
  47. package/dist/assets/quadrantDiagram-337W2JSQ-COfUcLWt.js +7 -0
  48. package/dist/assets/requirementDiagram-Z7DCOOCP-DSb-CJ5B.js +73 -0
  49. package/dist/assets/{sankeyDiagram-WA2Y5GQK-4gulcOP4.js → sankeyDiagram-WA2Y5GQK-8jtuVb45.js} +3 -3
  50. package/dist/assets/sequenceDiagram-2WXFIKYE-C2VpkMwA.js +145 -0
  51. package/dist/assets/{stateDiagram-RAJIS63D-CB4Vl7qM.js → stateDiagram-RAJIS63D-fmwMqxxc.js} +1 -1
  52. package/dist/assets/stateDiagram-v2-FVOUBMTO-9GGXVWrR.js +1 -0
  53. package/dist/assets/timeline-definition-YZTLITO2-Dx1hP5lg.js +61 -0
  54. package/dist/assets/{treemap-KZPCXAKY-DZSEE6Hz.js → treemap-KZPCXAKY-CkLOdYCZ.js} +58 -58
  55. package/dist/assets/vendor-codemirror-BxPY6emf.js +39 -0
  56. package/dist/assets/vendor-react-xmA_f8ig.js +59 -0
  57. package/dist/assets/vendor-xterm-DfaPXD3y.js +66 -0
  58. package/dist/assets/{vennDiagram-LZ73GAT5-8E_G06fI.js → vennDiagram-LZ73GAT5-D6KWcnln.js} +4 -4
  59. package/dist/assets/xychartDiagram-JWTSCODW-6fh6qmzN.js +7 -0
  60. package/dist/index.html +5 -5
  61. package/package.json +36 -35
  62. package/server/acp-runtime/client.js +91 -17
  63. package/server/acp-runtime/index.js +5 -16
  64. package/server/acp-runtime/session-store.js +4 -4
  65. package/server/channels/runtime/AgentRuntimeAdapter.js +1 -10
  66. package/server/claude-sdk.js +1 -3
  67. package/server/cli.js +159 -2
  68. package/server/external-agent/service.js +24 -6
  69. package/server/external-agent/ws.js +63 -3
  70. package/server/gemini-cli.js +1 -3
  71. package/server/index.js +120 -19
  72. package/server/openai-codex.js +1 -3
  73. package/server/opencode-cli.js +1 -3
  74. package/server/projects.js +654 -236
  75. package/server/routes/cc-connect.js +1131 -0
  76. package/server/routes/cli-auth.js +1 -73
  77. package/server/routes/commands.js +4 -9
  78. package/server/routes/projects.js +45 -24
  79. package/server/routes/session-core.js +149 -86
  80. package/server/session-core/eventStore.js +45 -18
  81. package/server/session-core/providerAdapters.js +50 -13
  82. package/server/session-core/providerDiscovery.js +8 -3
  83. package/server/session-core/runtimeState.js +8 -0
  84. package/server/utils/ccConnectManager.js +390 -0
  85. package/server/utils/ccConnectState.js +575 -0
  86. package/server/utils/resolveCommandPath.js +71 -0
  87. package/server/utils/workspaceRoots.js +154 -0
  88. package/shared/conversationEvents.js +78 -14
  89. package/dist/assets/App-BWSqiXAT.js +0 -220
  90. package/dist/assets/App-DrlLKa8f.css +0 -1
  91. package/dist/assets/ReviewApp-nz3mbArg.js +0 -1
  92. package/dist/assets/architectureDiagram-2XIMDMQ5-CarjBOOv.js +0 -36
  93. package/dist/assets/c4Diagram-IC4MRINW-CGobwBIj.js +0 -10
  94. package/dist/assets/channel-DkFNxV_H.js +0 -1
  95. package/dist/assets/chunk-NQ4KR5QH-CMH6EDP2.js +0 -220
  96. package/dist/assets/chunk-WL4C6EOR-Dn7db_6t.js +0 -189
  97. package/dist/assets/classDiagram-VBA2DB6C-DtwCEe8S.js +0 -1
  98. package/dist/assets/classDiagram-v2-RAHNMMFH-DtwCEe8S.js +0 -1
  99. package/dist/assets/clone-C0lCEIEO.js +0 -1
  100. package/dist/assets/cose-bilkent-S5V4N54A-DD_nzqsz.js +0 -1
  101. package/dist/assets/cytoscape.esm-5J0xJHOV.js +0 -321
  102. package/dist/assets/diagram-E7M64L7V-TVdvHtGc.js +0 -24
  103. package/dist/assets/erDiagram-INFDFZHY-5Kw0bByo.js +0 -70
  104. package/dist/assets/ganttDiagram-A5KZAMGK-CsADFkcq.js +0 -292
  105. package/dist/assets/highlighted-body-OFNGDK62-CZrBMazC.js +0 -1
  106. package/dist/assets/index-B01NxbUv.css +0 -1
  107. package/dist/assets/index-DW5pGgQ_.js +0 -2
  108. package/dist/assets/ishikawaDiagram-PHBUUO56-ndm9snwO.js +0 -70
  109. package/dist/assets/journeyDiagram-4ABVD52K-HgF2t7z5.js +0 -139
  110. package/dist/assets/mermaid-GHXKKRXX-CK8m3lad.js +0 -870
  111. package/dist/assets/mindmap-definition-YRQLILUH-CNq9SKj4.js +0 -68
  112. package/dist/assets/quadrantDiagram-337W2JSQ-B7FnztNO.js +0 -7
  113. package/dist/assets/requirementDiagram-Z7DCOOCP-Bl_BM2Th.js +0 -73
  114. package/dist/assets/sequenceDiagram-2WXFIKYE-VEuJDwyJ.js +0 -145
  115. package/dist/assets/stateDiagram-v2-FVOUBMTO-C85ucl39.js +0 -1
  116. package/dist/assets/timeline-definition-YZTLITO2-BPGKhi7f.js +0 -61
  117. package/dist/assets/vendor-codemirror-CyOKkaQZ.js +0 -31
  118. package/dist/assets/vendor-react-CP4yFTs7.js +0 -8
  119. package/dist/assets/vendor-xterm-DfcmCpbH.js +0 -66
  120. package/dist/assets/xychartDiagram-JWTSCODW-CbBk50-O.js +0 -7
  121. package/server/_legacy-providers/README.md +0 -30
  122. package/server/_legacy-providers/claude-sdk.js +0 -956
  123. package/server/_legacy-providers/gemini-cli.js +0 -368
  124. package/server/_legacy-providers/openai-codex.js +0 -705
  125. package/server/_legacy-providers/opencode-cli.js +0 -674
  126. package/server/acp-runtime/client.test.js +0 -688
  127. package/server/acp-runtime/session-store.test.js +0 -89
  128. package/server/cli.test.js +0 -76
  129. package/server/external-agent/service.test.js +0 -53
  130. package/server/external-agent/ws.test.js +0 -289
  131. package/shared/conversationEvents.test.js +0 -403
@@ -1,688 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import test from 'node:test';
3
-
4
- import { AcpClient, normalizeTerminalCommandForSpawn, splitCommandLine } from './client.js';
5
-
6
- function createWriter() {
7
- const messages = [];
8
- return {
9
- messages,
10
- send(payload) {
11
- messages.push(payload);
12
- },
13
- setSessionId() {},
14
- getSessionId() {
15
- return null;
16
- }
17
- };
18
- }
19
-
20
- function getConversationEvents(writer) {
21
- return writer.messages
22
- .filter((payload) => payload.type === 'conversation-event')
23
- .map((payload) => payload.event);
24
- }
25
-
26
- test('splitCommandLine keeps quoted arguments together', () => {
27
- const parsed = splitCommandLine('npx -y "pkg with spaces" --flag=\'value one\'');
28
- assert.equal(parsed.command, 'npx');
29
- assert.deepEqual(parsed.args, ['-y', 'pkg with spaces', '--flag=value one']);
30
- });
31
-
32
- test('normalizeTerminalCommandForSpawn disables zsh nomatch for inline commands', () => {
33
- const normalized = normalizeTerminalCommandForSpawn('/bin/zsh', ['-lc', 'curl -sS https://wttr.in/Guangzhou?format=3']);
34
- assert.equal(normalized.command, '/bin/zsh');
35
- assert.deepEqual(normalized.args, ['-lc', 'setopt no_nomatch; curl -sS https://wttr.in/Guangzhou?format=3']);
36
- });
37
-
38
- test('normalizeTerminalCommandForSpawn leaves non-zsh commands unchanged', () => {
39
- const normalized = normalizeTerminalCommandForSpawn('/bin/bash', ['-lc', 'echo hello']);
40
- assert.equal(normalized.command, '/bin/bash');
41
- assert.deepEqual(normalized.args, ['-lc', 'echo hello']);
42
- });
43
-
44
- test('AcpClient streams assistant chunks into conversation events', async () => {
45
- const writer = createWriter();
46
- const client = new AcpClient({
47
- agentKey: 'claude',
48
- writer,
49
- projectPath: process.cwd()
50
- });
51
-
52
- client.sessionId = 'session-5';
53
-
54
- await client.handleSessionUpdate({
55
- sessionId: 'session-5',
56
- update: {
57
- sessionUpdate: 'agent_message_chunk',
58
- content: {
59
- type: 'text',
60
- text: 'Hello ACP'
61
- }
62
- }
63
- });
64
-
65
- client.flushOpenTextStreams('2026-03-30T12:00:00.000Z');
66
- client.resetTurnState();
67
-
68
- await client.handleSessionUpdate({
69
- sessionId: 'session-5',
70
- update: {
71
- sessionUpdate: 'agent_message_chunk',
72
- content: {
73
- type: 'text',
74
- text: 'Second turn'
75
- }
76
- }
77
- });
78
-
79
- client.flushOpenTextStreams('2026-03-30T12:00:01.000Z');
80
-
81
- const events = getConversationEvents(writer);
82
- const kinds = events.map((event) => event.kind);
83
- const assistantStartEvents = events.filter((event) => event.kind === 'assistant_text_start');
84
-
85
- assert.deepEqual(kinds, [
86
- 'assistant_text_start',
87
- 'assistant_text_delta',
88
- 'assistant_text_end',
89
- 'assistant_text_start',
90
- 'assistant_text_delta',
91
- 'assistant_text_end'
92
- ]);
93
- assert.equal(assistantStartEvents.length, 2);
94
- assert.notEqual(assistantStartEvents[0].payload.messageId, assistantStartEvents[1].payload.messageId);
95
- });
96
-
97
- test('AcpClient maps reasoning chunks and end_turn into reasoning events', async () => {
98
- const writer = createWriter();
99
- const client = new AcpClient({
100
- agentKey: 'claude',
101
- writer,
102
- projectPath: process.cwd()
103
- });
104
-
105
- client.sessionId = 'session-reasoning';
106
-
107
- await client.handleSessionUpdate({
108
- sessionId: 'session-reasoning',
109
- update: {
110
- sessionUpdate: 'agent_thought_chunk',
111
- content: {
112
- type: 'text',
113
- text: 'Thinking...'
114
- }
115
- }
116
- });
117
-
118
- await client.handleSessionUpdate({
119
- sessionId: 'session-reasoning',
120
- update: {
121
- sessionUpdate: 'end_turn'
122
- }
123
- });
124
-
125
- const events = getConversationEvents(writer);
126
- assert.deepEqual(
127
- events.map((event) => event.kind),
128
- ['reasoning_start', 'reasoning_delta', 'reasoning_end']
129
- );
130
- });
131
-
132
- test('AcpClient maps tool_call updates into tool lifecycle events', async () => {
133
- const writer = createWriter();
134
- const client = new AcpClient({
135
- agentKey: 'claude',
136
- writer,
137
- projectPath: process.cwd()
138
- });
139
-
140
- client.sessionId = 'session-tool';
141
-
142
- await client.handleSessionUpdate({
143
- sessionId: 'session-tool',
144
- update: {
145
- sessionUpdate: 'tool_call',
146
- toolCallId: 'tool-123',
147
- title: 'Read file',
148
- kind: 'read',
149
- status: 'in_progress',
150
- rawInput: { path: 'README.md' },
151
- locations: [{ path: 'README.md', line: 1 }]
152
- }
153
- });
154
-
155
- await client.handleSessionUpdate({
156
- sessionId: 'session-tool',
157
- update: {
158
- sessionUpdate: 'tool_call_update',
159
- toolCallId: 'tool-123',
160
- title: 'Read file',
161
- kind: 'read',
162
- status: 'completed',
163
- rawInput: { path: 'README.md' },
164
- rawOutput: 'file contents',
165
- content: [
166
- {
167
- type: 'content',
168
- content: {
169
- type: 'text',
170
- text: 'Opened README.md'
171
- }
172
- }
173
- ]
174
- }
175
- });
176
-
177
- const events = getConversationEvents(writer);
178
- assert.deepEqual(
179
- events.map((event) => event.kind),
180
- ['tool_call_start', 'tool_call_input', 'tool_call_input', 'tool_call_end', 'tool_result']
181
- );
182
- assert.equal(events[4].payload.content, 'file contents');
183
- assert.equal(events[0].payload.kind, 'read');
184
- assert.equal(events[1].payload.locations[0].path, 'README.md');
185
- assert.equal(events[4].payload.contentBlocks[0].type, 'content');
186
- });
187
-
188
- test('AcpClient splits assistant text around tool calls within the same turn', async () => {
189
- const writer = createWriter();
190
- const client = new AcpClient({
191
- agentKey: 'claude',
192
- writer,
193
- projectPath: process.cwd()
194
- });
195
-
196
- client.sessionId = 'session-tool-boundary';
197
-
198
- await client.handleSessionUpdate({
199
- sessionId: 'session-tool-boundary',
200
- update: {
201
- sessionUpdate: 'agent_message_chunk',
202
- content: {
203
- type: 'text',
204
- text: 'First, I will inspect the file.'
205
- }
206
- }
207
- });
208
-
209
- await client.handleSessionUpdate({
210
- sessionId: 'session-tool-boundary',
211
- update: {
212
- sessionUpdate: 'tool_call',
213
- toolCallId: 'tool-boundary-1',
214
- title: 'Read file',
215
- kind: 'read',
216
- status: 'in_progress',
217
- rawInput: { path: 'README.md' }
218
- }
219
- });
220
-
221
- await client.handleSessionUpdate({
222
- sessionId: 'session-tool-boundary',
223
- update: {
224
- sessionUpdate: 'tool_call_update',
225
- toolCallId: 'tool-boundary-1',
226
- title: 'Read file',
227
- kind: 'read',
228
- status: 'completed',
229
- rawInput: { path: 'README.md' },
230
- rawOutput: 'done'
231
- }
232
- });
233
-
234
- await client.handleSessionUpdate({
235
- sessionId: 'session-tool-boundary',
236
- update: {
237
- sessionUpdate: 'agent_message_chunk',
238
- content: {
239
- type: 'text',
240
- text: 'Here is what I found.'
241
- }
242
- }
243
- });
244
-
245
- client.flushOpenTextStreams('2026-03-30T12:00:03.000Z');
246
-
247
- const events = getConversationEvents(writer);
248
- const assistantStarts = events.filter((event) => event.kind === 'assistant_text_start');
249
- const assistantEnds = events.filter((event) => event.kind === 'assistant_text_end');
250
-
251
- assert.deepEqual(
252
- events.map((event) => event.kind),
253
- [
254
- 'assistant_text_start',
255
- 'assistant_text_delta',
256
- 'assistant_text_end',
257
- 'tool_call_start',
258
- 'tool_call_input',
259
- 'tool_call_input',
260
- 'tool_call_end',
261
- 'tool_result',
262
- 'assistant_text_start',
263
- 'assistant_text_delta',
264
- 'assistant_text_end'
265
- ]
266
- );
267
- assert.equal(assistantStarts.length, 2);
268
- assert.equal(assistantEnds.length, 2);
269
- assert.notEqual(assistantStarts[0].payload.messageId, assistantStarts[1].payload.messageId);
270
- });
271
-
272
- test('AcpClient waits for explicit permission decisions in default mode', async () => {
273
- const writer = createWriter();
274
- const client = new AcpClient({
275
- agentKey: 'claude',
276
- writer,
277
- projectPath: process.cwd(),
278
- permissionMode: 'default'
279
- });
280
-
281
- client.sessionId = 'session-6';
282
-
283
- const permissionPromise = client.handleRequestPermission({
284
- sessionId: 'session-6',
285
- toolCall: {
286
- toolCallId: 'tool-1',
287
- title: 'Write file',
288
- kind: 'edit',
289
- rawInput: {
290
- path: 'README.md'
291
- }
292
- },
293
- options: [
294
- {
295
- optionId: 'allow-once',
296
- kind: 'allow_once',
297
- name: 'Allow once'
298
- },
299
- {
300
- optionId: 'reject-once',
301
- kind: 'reject_once',
302
- name: 'Reject once'
303
- }
304
- ]
305
- });
306
-
307
- const requestPayload = writer.messages.find((payload) => payload.type === 'conversation-event' && payload.event.kind === 'approval_request');
308
- assert.ok(requestPayload);
309
-
310
- const requestId = requestPayload.event.payload.requestId;
311
- assert.ok(AcpClient.resolvePermissionRequest(requestId, { allow: true }));
312
-
313
- const response = await permissionPromise;
314
- assert.equal(response.outcome.outcome, 'selected');
315
- assert.equal(response.outcome.optionId, 'allow-once');
316
-
317
- const resolutionPayload = writer.messages.find((payload) => payload.type === 'conversation-event' && payload.event.kind === 'approval_resolved');
318
- assert.ok(resolutionPayload);
319
- assert.equal(resolutionPayload.event.payload.status, 'approved');
320
- });
321
-
322
- test('AcpClient accepts explicit ACP option selections and emits rich permission payload', async () => {
323
- const writer = createWriter();
324
- const client = new AcpClient({
325
- agentKey: 'claude',
326
- writer,
327
- projectPath: process.cwd(),
328
- permissionMode: 'default'
329
- });
330
-
331
- client.sessionId = 'session-6b';
332
-
333
- const permissionPromise = client.handleRequestPermission({
334
- sessionId: 'session-6b',
335
- toolCall: {
336
- toolCallId: 'tool-3',
337
- title: 'Edit file',
338
- kind: 'edit',
339
- rawInput: {
340
- path: 'README.md'
341
- },
342
- locations: [{ path: 'README.md', line: 9 }]
343
- },
344
- options: [
345
- {
346
- optionId: 'allow-always',
347
- kind: 'allow_always',
348
- name: 'Always allow'
349
- },
350
- {
351
- optionId: 'reject-once',
352
- kind: 'reject_once',
353
- name: 'Reject once'
354
- }
355
- ]
356
- });
357
-
358
- const requestPayload = writer.messages.find((payload) => payload.type === 'conversation-event' && payload.event.kind === 'approval_request');
359
- assert.ok(requestPayload);
360
- assert.equal(requestPayload.event.payload.toolCall.kind, 'edit');
361
- assert.equal(requestPayload.event.payload.options[0].optionId, 'allow-always');
362
-
363
- assert.ok(AcpClient.resolvePermissionRequest(requestPayload.event.payload.requestId, {
364
- outcome: {
365
- outcome: 'selected',
366
- optionId: 'allow-always'
367
- }
368
- }));
369
-
370
- const response = await permissionPromise;
371
- assert.equal(response.outcome.outcome, 'selected');
372
- assert.equal(response.outcome.optionId, 'allow-always');
373
- });
374
-
375
- test('AcpClient maps plan updates into conversation events', async () => {
376
- const writer = createWriter();
377
- const client = new AcpClient({
378
- agentKey: 'claude',
379
- writer,
380
- projectPath: process.cwd()
381
- });
382
-
383
- client.sessionId = 'session-plan';
384
-
385
- await client.handleSessionUpdate({
386
- sessionId: 'session-plan',
387
- update: {
388
- sessionUpdate: 'plan',
389
- entries: [
390
- { content: 'Inspect adapter', status: 'completed', priority: 'medium' },
391
- { content: 'Render plan UI', status: 'in_progress', priority: 'high' }
392
- ]
393
- }
394
- });
395
-
396
- const events = getConversationEvents(writer);
397
- assert.equal(events.length, 1);
398
- assert.equal(events[0].kind, 'plan_update');
399
- assert.equal(events[0].payload.entries.length, 2);
400
- assert.equal(events[0].payload.entries[1].priority, 'high');
401
- });
402
-
403
- test('AcpClient maps mode and available commands updates into conversation events', async () => {
404
- const writer = createWriter();
405
- const client = new AcpClient({
406
- agentKey: 'claude',
407
- writer,
408
- projectPath: process.cwd()
409
- });
410
-
411
- client.sessionId = 'session-mode';
412
-
413
- await client.handleSessionUpdate({
414
- sessionId: 'session-mode',
415
- update: {
416
- sessionUpdate: 'current_mode_update',
417
- currentModeId: 'code'
418
- }
419
- });
420
-
421
- await client.handleSessionUpdate({
422
- sessionId: 'session-mode',
423
- update: {
424
- sessionUpdate: 'available_commands_update',
425
- availableCommands: [
426
- {
427
- name: 'create_plan',
428
- description: 'Create a plan'
429
- }
430
- ]
431
- }
432
- });
433
-
434
- const events = getConversationEvents(writer);
435
- assert.equal(events[0].kind, 'mode_update');
436
- assert.equal(events[0].payload.currentModeId, 'code');
437
- assert.equal(events[1].kind, 'available_commands_update');
438
- assert.equal(events[1].payload.availableCommands[0].name, 'create_plan');
439
- });
440
-
441
- test('AcpClient maps non-text assistant chunks into content block events', async () => {
442
- const writer = createWriter();
443
- const client = new AcpClient({
444
- agentKey: 'claude',
445
- writer,
446
- projectPath: process.cwd()
447
- });
448
-
449
- client.sessionId = 'session-content-blocks';
450
-
451
- await client.handleSessionUpdate({
452
- sessionId: 'session-content-blocks',
453
- update: {
454
- sessionUpdate: 'agent_message_chunk',
455
- content: {
456
- type: 'resource_link',
457
- uri: '/tmp/spec.md',
458
- name: 'spec.md'
459
- }
460
- }
461
- });
462
-
463
- await client.handleSessionUpdate({
464
- sessionId: 'session-content-blocks',
465
- update: {
466
- sessionUpdate: 'agent_message_chunk',
467
- content: {
468
- type: 'text',
469
- text: 'See the attached spec.'
470
- }
471
- }
472
- });
473
-
474
- client.flushOpenTextStreams('2026-03-30T12:00:02.000Z');
475
-
476
- const events = getConversationEvents(writer);
477
- assert.equal(events[0].kind, 'assistant_content_block');
478
- assert.equal(events[0].payload.contentBlock.type, 'resource_link');
479
- assert.equal(events[1].kind, 'assistant_text_start');
480
- assert.equal(events[1].payload.messageId, events[0].payload.messageId);
481
- assert.equal(events[2].kind, 'assistant_text_delta');
482
- });
483
-
484
- test('AcpClient auto-approves editable tools in acceptEdits mode', async () => {
485
- const writer = createWriter();
486
- const client = new AcpClient({
487
- agentKey: 'claude',
488
- writer,
489
- projectPath: process.cwd(),
490
- permissionMode: 'acceptEdits'
491
- });
492
-
493
- client.sessionId = 'session-7';
494
-
495
- const response = await client.handleRequestPermission({
496
- sessionId: 'session-7',
497
- toolCall: {
498
- toolCallId: 'tool-2',
499
- title: 'Edit file',
500
- kind: 'edit'
501
- },
502
- options: [
503
- {
504
- optionId: 'allow-once',
505
- kind: 'allow_once',
506
- name: 'Allow once'
507
- },
508
- {
509
- optionId: 'reject-once',
510
- kind: 'reject_once',
511
- name: 'Reject once'
512
- }
513
- ]
514
- });
515
-
516
- assert.equal(response.outcome.outcome, 'selected');
517
- assert.equal(response.outcome.optionId, 'allow-once');
518
- assert.equal(writer.messages.length, 0);
519
- });
520
-
521
- test('AcpClient auto-approves all tools in bypassPermissions mode', async () => {
522
- const writer = createWriter();
523
- const client = new AcpClient({
524
- agentKey: 'claude',
525
- writer,
526
- projectPath: process.cwd(),
527
- permissionMode: 'bypassPermissions'
528
- });
529
-
530
- client.sessionId = 'session-8';
531
-
532
- const response = await client.handleRequestPermission({
533
- sessionId: 'session-8',
534
- toolCall: {
535
- toolCallId: 'tool-3',
536
- title: 'Run shell command',
537
- kind: 'execute'
538
- },
539
- options: [
540
- {
541
- optionId: 'allow-once',
542
- kind: 'allow_once',
543
- name: 'Allow once'
544
- },
545
- {
546
- optionId: 'reject-once',
547
- kind: 'reject_once',
548
- name: 'Reject once'
549
- }
550
- ]
551
- });
552
-
553
- assert.equal(response.outcome.outcome, 'selected');
554
- assert.equal(response.outcome.optionId, 'allow-once');
555
- assert.equal(writer.messages.length, 0);
556
- });
557
-
558
- test('AcpClient plan mode rejects non-read tools and allows read-like tools', async () => {
559
- const writer = createWriter();
560
- const client = new AcpClient({
561
- agentKey: 'claude',
562
- writer,
563
- projectPath: process.cwd(),
564
- permissionMode: 'plan'
565
- });
566
-
567
- client.sessionId = 'session-9';
568
-
569
- const denied = await client.handleRequestPermission({
570
- sessionId: 'session-9',
571
- toolCall: {
572
- toolCallId: 'tool-4',
573
- title: 'Edit file',
574
- kind: 'edit'
575
- },
576
- options: [
577
- {
578
- optionId: 'allow-once',
579
- kind: 'allow_once',
580
- name: 'Allow once'
581
- },
582
- {
583
- optionId: 'reject-once',
584
- kind: 'reject_once',
585
- name: 'Reject once'
586
- }
587
- ]
588
- });
589
-
590
- const allowed = await client.handleRequestPermission({
591
- sessionId: 'session-9',
592
- toolCall: {
593
- toolCallId: 'tool-5',
594
- title: 'Read file',
595
- kind: 'read'
596
- },
597
- options: [
598
- {
599
- optionId: 'allow-once',
600
- kind: 'allow_once',
601
- name: 'Allow once'
602
- },
603
- {
604
- optionId: 'reject-once',
605
- kind: 'reject_once',
606
- name: 'Reject once'
607
- }
608
- ]
609
- });
610
-
611
- assert.equal(denied.outcome.outcome, 'selected');
612
- assert.equal(denied.outcome.optionId, 'reject-once');
613
- assert.equal(allowed.outcome.outcome, 'selected');
614
- assert.equal(allowed.outcome.optionId, 'allow-once');
615
- assert.equal(writer.messages.length, 0);
616
- });
617
-
618
- test('AcpClient fails Gemini prompts quickly when stderr reports rate limiting', async () => {
619
- const writer = createWriter();
620
- const client = new AcpClient({
621
- agentKey: 'gemini',
622
- writer,
623
- projectPath: process.cwd()
624
- });
625
-
626
- client.sessionId = 'session-gemini-rate-limit';
627
- client.connection = {
628
- prompt: () => new Promise(() => {})
629
- };
630
-
631
- const promptPromise = client.sendPrompt('Reply with exactly GEMINI_OK.');
632
- client.handleAgentDiagnostic('Attempt 1 failed with status 429. reason: rateLimitExceeded. status: RESOURCE_EXHAUSTED');
633
-
634
- await assert.rejects(
635
- promptPromise,
636
- /rate limited \(429 RESOURCE_EXHAUSTED\)/i
637
- );
638
-
639
- const conversationEvents = getConversationEvents(writer);
640
- const stateChange = conversationEvents.find((event) => event.kind === 'session_state_changed' && event.payload?.state === 'errored');
641
- const statusPayloads = writer.messages.filter((payload) => payload.type === 'session-status');
642
- const rawErrorPayload = writer.messages.find((payload) => payload.type === 'error');
643
-
644
- assert.ok(stateChange);
645
- assert.equal(
646
- rawErrorPayload?.error,
647
- 'Gemini request failed because the logged-in account is currently rate limited (429 RESOURCE_EXHAUSTED). Please wait and try again later.'
648
- );
649
- assert.deepEqual(
650
- statusPayloads.map((payload) => payload.isProcessing),
651
- [true, false]
652
- );
653
- });
654
-
655
- test('AcpClient sends prompt resource links as ACP prompt blocks and user content blocks', async () => {
656
- const writer = createWriter();
657
- const client = new AcpClient({
658
- agentKey: 'claude',
659
- writer,
660
- projectPath: process.cwd()
661
- });
662
-
663
- client.sessionId = 'session-prompt-blocks';
664
- let promptParams = null;
665
- client.connection = {
666
- prompt: async (params) => {
667
- promptParams = params;
668
- return { stopReason: 'end_turn' };
669
- }
670
- };
671
-
672
- await client.sendPrompt('Review this file.', {
673
- resourceLinks: [
674
- {
675
- uri: '/tmp/spec.md',
676
- name: 'spec.md'
677
- }
678
- ]
679
- });
680
-
681
- assert.equal(promptParams.prompt[1].type, 'resource_link');
682
- assert.equal(promptParams.prompt[1].uri, '/tmp/spec.md');
683
-
684
- const userEvent = getConversationEvents(writer).find((event) => event.kind === 'user_message');
685
- assert.ok(userEvent);
686
- assert.equal(userEvent.payload.contentBlocks[0].type, 'resource_link');
687
- assert.equal(userEvent.payload.contentBlocks[0].uri, '/tmp/spec.md');
688
- });