@pdhaku0/gemini-cli-agent-sdk 0.1.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.
Files changed (54) hide show
  1. package/README.md +109 -0
  2. package/client/index.d.ts +1 -0
  3. package/client/index.js +1 -0
  4. package/client/package.json +1 -0
  5. package/dist/client.d.ts +5 -0
  6. package/dist/client.js +5 -0
  7. package/dist/client.js.map +1 -0
  8. package/dist/common/types.d.ts +191 -0
  9. package/dist/common/types.js +18 -0
  10. package/dist/common/types.js.map +1 -0
  11. package/dist/core/AcpWebSocketTransport.d.ts +25 -0
  12. package/dist/core/AcpWebSocketTransport.js +222 -0
  13. package/dist/core/AcpWebSocketTransport.js.map +1 -0
  14. package/dist/core/AgentChatClient.d.ts +75 -0
  15. package/dist/core/AgentChatClient.js +679 -0
  16. package/dist/core/AgentChatClient.js.map +1 -0
  17. package/dist/core/ToolPermissionManager.d.ts +26 -0
  18. package/dist/core/ToolPermissionManager.js +88 -0
  19. package/dist/core/ToolPermissionManager.js.map +1 -0
  20. package/dist/core/diff-utils.d.ts +1 -0
  21. package/dist/core/diff-utils.js +7 -0
  22. package/dist/core/diff-utils.js.map +1 -0
  23. package/dist/core/stream-utils.d.ts +14 -0
  24. package/dist/core/stream-utils.js +57 -0
  25. package/dist/core/stream-utils.js.map +1 -0
  26. package/dist/extras/index.d.ts +1 -0
  27. package/dist/extras/index.js +2 -0
  28. package/dist/extras/index.js.map +1 -0
  29. package/dist/extras/sys-tags.d.ts +38 -0
  30. package/dist/extras/sys-tags.js +150 -0
  31. package/dist/extras/sys-tags.js.map +1 -0
  32. package/dist/index.d.ts +1 -0
  33. package/dist/index.js +2 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/server/GeminiBridge.d.ts +50 -0
  36. package/dist/server/GeminiBridge.js +500 -0
  37. package/dist/server/GeminiBridge.js.map +1 -0
  38. package/dist/server.d.ts +7 -0
  39. package/dist/server.js +7 -0
  40. package/dist/server.js.map +1 -0
  41. package/dist/ui/AgentChatStore.d.ts +16 -0
  42. package/dist/ui/AgentChatStore.js +59 -0
  43. package/dist/ui/AgentChatStore.js.map +1 -0
  44. package/docs/API.md +100 -0
  45. package/docs/EVENTS.md +100 -0
  46. package/docs/INTEGRATION.md +109 -0
  47. package/docs/SPECIFICATION.md +93 -0
  48. package/docs/TROUBLESHOOTING.md +44 -0
  49. package/docs/USAGE.md +270 -0
  50. package/docs/design.md +62 -0
  51. package/package.json +71 -0
  52. package/server/index.d.ts +1 -0
  53. package/server/index.js +1 -0
  54. package/server/package.json +1 -0
@@ -0,0 +1,679 @@
1
+ import { EventEmitter } from 'events';
2
+ import { AcpWebSocketTransport } from './AcpWebSocketTransport.js';
3
+ import { extractNewStreamSegment } from './stream-utils.js';
4
+ import { createUnifiedDiff } from './diff-utils.js';
5
+ export class AgentChatClient extends EventEmitter {
6
+ transport;
7
+ sessionId = null;
8
+ messages = [];
9
+ authUrl = null;
10
+ pendingApproval = null;
11
+ options;
12
+ connectionState = 'disconnected';
13
+ inTurn = false;
14
+ activeAssistantId = null;
15
+ lastFinalizedAssistantId = null;
16
+ timeOverride = null;
17
+ idCounter = 0;
18
+ replayNonce = null;
19
+ currentTurnHidden = 'none';
20
+ constructor(options) {
21
+ super();
22
+ const baseCwd = typeof process !== 'undefined' && process.cwd ? process.cwd() : undefined;
23
+ this.options = { cwd: baseCwd, ...options };
24
+ if (options.sessionId)
25
+ this.sessionId = options.sessionId;
26
+ const url = this.buildUrlWithReplay(options.url, options.replay);
27
+ this.transport = new AcpWebSocketTransport({ url, reconnect: true });
28
+ this.setupHandlers();
29
+ }
30
+ async connect(options = {}) {
31
+ const { autoSession = true } = options;
32
+ return new Promise((resolve, reject) => {
33
+ this.transport.once('connected', async () => {
34
+ if (this.sessionId) {
35
+ this.emit('session_ready', this.sessionId);
36
+ resolve();
37
+ return;
38
+ }
39
+ if (!autoSession) {
40
+ resolve();
41
+ return;
42
+ }
43
+ try {
44
+ await this.initializeSession();
45
+ resolve();
46
+ }
47
+ catch (err) {
48
+ console.error('[AgentChat] Session init failed:', JSON.stringify(err, null, 2));
49
+ reject(err);
50
+ }
51
+ });
52
+ this.transport.on('error', (err) => reject(err));
53
+ this.transport.connect();
54
+ });
55
+ }
56
+ async initializeSession() {
57
+ const result = await this.transport.sendRequest('session/new', {
58
+ cwd: this.options.cwd,
59
+ model: this.options.model,
60
+ mcpServers: []
61
+ });
62
+ this.sessionId = result.sessionId;
63
+ this.emit('session_ready', this.sessionId);
64
+ }
65
+ setSessionId(sessionId) {
66
+ this.sessionId = sessionId;
67
+ }
68
+ getSessionId() {
69
+ return this.sessionId;
70
+ }
71
+ async sendMessage(text, options = {}) {
72
+ if (!this.sessionId)
73
+ throw new Error('Session not initialized');
74
+ const hiddenMode = options.hidden ?? 'none';
75
+ this.currentTurnHidden = hiddenMode;
76
+ const userMsg = {
77
+ id: this.makeId('user'),
78
+ role: 'user',
79
+ text,
80
+ hidden: hiddenMode === 'user' || hiddenMode === 'turn',
81
+ ts: Date.now(),
82
+ };
83
+ this.messages.push(userMsg);
84
+ if (this.shouldEmitUser(hiddenMode)) {
85
+ this.emit('message', userMsg);
86
+ }
87
+ this.inTurn = true;
88
+ this.activeAssistantId = null;
89
+ if (this.shouldEmitAssistant(hiddenMode)) {
90
+ this.emit('turn_started', { userMessageId: userMsg.id });
91
+ }
92
+ let result;
93
+ try {
94
+ result = await this.transport.sendRequest('session/prompt', {
95
+ sessionId: this.sessionId,
96
+ prompt: [{ type: 'text', text }],
97
+ });
98
+ }
99
+ catch (err) {
100
+ if (this.shouldEmitAssistant(hiddenMode)) {
101
+ this.emit('turn_completed', 'error');
102
+ }
103
+ this.inTurn = false;
104
+ this.currentTurnHidden = 'none';
105
+ throw err;
106
+ }
107
+ if (result?.stopReason) {
108
+ const last = this.getOrCreateAssistantMessage();
109
+ last.stopReason = result.stopReason;
110
+ if (this.shouldEmitAssistant(hiddenMode)) {
111
+ this.emit('message_update', last);
112
+ this.emit('turn_completed', result.stopReason);
113
+ if (this.lastFinalizedAssistantId !== last.id) {
114
+ this.emit('assistant_text_final', { messageId: last.id, text: last.text });
115
+ this.lastFinalizedAssistantId = last.id;
116
+ }
117
+ }
118
+ this.inTurn = false;
119
+ this.currentTurnHidden = 'none';
120
+ }
121
+ }
122
+ async submitAuthCode(code) {
123
+ // Send as a special notification that bridge intercepts
124
+ await this.transport.sendNotification('gemini/submitAuthCode', { code });
125
+ this.authUrl = null;
126
+ this.emit('auth_resolved');
127
+ }
128
+ async cancel() {
129
+ if (!this.sessionId)
130
+ return;
131
+ try {
132
+ await this.transport.sendRequest('session/cancel', { sessionId: this.sessionId });
133
+ // Optimistically finish the turn
134
+ this.emit('turn_completed', 'canceled');
135
+ this.inTurn = false;
136
+ }
137
+ catch (error) {
138
+ console.error('[AgentChatClient] Failed to cancel:', error);
139
+ }
140
+ }
141
+ async approveTool(optionId) {
142
+ if (!this.pendingApproval)
143
+ return;
144
+ await this.resolveApproval(this.pendingApproval.requestId, optionId);
145
+ }
146
+ getMessages(options = {}) {
147
+ if (options.includeHidden)
148
+ return [...this.messages];
149
+ return this.messages.filter((msg) => !msg.hidden);
150
+ }
151
+ prependMessages(messages) {
152
+ if (!messages.length)
153
+ return;
154
+ this.messages = [...messages, ...this.messages];
155
+ this.emit('messages_replayed', { count: messages.length });
156
+ const last = this.messages[this.messages.length - 1];
157
+ if (!last.hidden) {
158
+ this.emit('message_update', last);
159
+ }
160
+ }
161
+ getAuthUrl() {
162
+ return this.authUrl;
163
+ }
164
+ getPendingApproval() {
165
+ return this.pendingApproval;
166
+ }
167
+ getConnectionState() {
168
+ return this.connectionState;
169
+ }
170
+ dispose() {
171
+ this.transport.dispose();
172
+ this.messages = [];
173
+ this.removeAllListeners();
174
+ }
175
+ setupHandlers() {
176
+ this.transport.on('connection_state', (state) => {
177
+ this.connectionState = state;
178
+ this.emit('connection_state_changed', { state });
179
+ });
180
+ this.transport.on('error', (err) => {
181
+ this.emit('error', err);
182
+ });
183
+ this.transport.on('notification', (msg) => {
184
+ switch (msg.method) {
185
+ case 'session/update':
186
+ this.handleSessionUpdate(msg);
187
+ break;
188
+ case 'gemini/authUrl':
189
+ this.handleAuthUrl(msg);
190
+ break;
191
+ case 'bridge/replay': {
192
+ const payload = msg?.params?.data;
193
+ const ts = msg?.params?.timestamp;
194
+ const replayId = msg?.params?.replayId;
195
+ if (typeof ts === 'number')
196
+ this.timeOverride = ts;
197
+ if (typeof replayId === 'string')
198
+ this.replayNonce = replayId;
199
+ if (payload?.method === 'session/update') {
200
+ this.handleSessionUpdate(payload);
201
+ }
202
+ else if (payload?.method === 'session/prompt') {
203
+ this.handleReplayPrompt(payload?.params?.prompt);
204
+ }
205
+ else if (payload?.method === 'gemini/authUrl') {
206
+ this.handleAuthUrl(payload);
207
+ }
208
+ else {
209
+ const update = payload?.params?.update;
210
+ if (update?.sessionUpdate) {
211
+ this.handleSessionUpdate({ method: 'session/update', params: { update } });
212
+ }
213
+ }
214
+ this.timeOverride = null;
215
+ this.replayNonce = null;
216
+ break;
217
+ }
218
+ default: {
219
+ const update = msg?.params?.update;
220
+ if (update?.sessionUpdate) {
221
+ this.handleSessionUpdate({ method: 'session/update', params: { update } });
222
+ }
223
+ break;
224
+ }
225
+ }
226
+ });
227
+ this.transport.on('method:session/request_permission', (msg) => {
228
+ this.handlePermissionRequest(msg);
229
+ });
230
+ }
231
+ handleSessionUpdate(notification) {
232
+ const update = notification.params.update;
233
+ switch (update.sessionUpdate) {
234
+ case 'agent_thought_chunk':
235
+ this.updateAssistantMessageNormalized({ thought: update.content?.text });
236
+ break;
237
+ case 'agent_message_chunk':
238
+ this.updateAssistantMessageNormalized({ text: update.content?.text });
239
+ break;
240
+ case 'tool_call':
241
+ this.handleToolCall(update);
242
+ break;
243
+ case 'tool_call_update':
244
+ this.handleToolUpdate(update);
245
+ break;
246
+ case 'end_of_turn':
247
+ if (this.shouldEmitAssistant(this.currentTurnHidden)) {
248
+ this.emit('turn_completed', update.stopReason);
249
+ }
250
+ this.inTurn = false;
251
+ if (this.activeAssistantId) {
252
+ const last = this.messages.find(m => m.id === this.activeAssistantId);
253
+ if (last && this.lastFinalizedAssistantId !== last.id) {
254
+ if (this.shouldEmitAssistant(this.currentTurnHidden)) {
255
+ this.emit('assistant_text_final', { messageId: last.id, text: last.text });
256
+ }
257
+ this.lastFinalizedAssistantId = last.id;
258
+ }
259
+ }
260
+ this.currentTurnHidden = 'none';
261
+ break;
262
+ }
263
+ }
264
+ updateAssistantMessageNormalized(delta) {
265
+ const last = this.getOrCreateAssistantMessage();
266
+ const shouldEmitAssistant = this.shouldEmitAssistant(this.currentTurnHidden);
267
+ if (delta.thought) {
268
+ const rawChunk = delta.thought;
269
+ // Logic change: rectify against the CURRENT active thought part
270
+ let lastPart = last.content[last.content.length - 1];
271
+ if (!lastPart || lastPart.type !== 'thought') {
272
+ lastPart = { type: 'thought', thought: '' };
273
+ last.content.push(lastPart);
274
+ }
275
+ const currentPartThought = lastPart.type === 'thought' ? lastPart.thought : '';
276
+ const newSegment = extractNewStreamSegment(currentPartThought, rawChunk);
277
+ if (newSegment && lastPart.type === 'thought') {
278
+ lastPart.thought += newSegment;
279
+ last.thought += newSegment; // Update global thought for legacy
280
+ if (shouldEmitAssistant) {
281
+ this.emit('thought_delta', { messageId: last.id, delta: newSegment, thought: last.thought });
282
+ this.emit('assistant_thought_delta', { messageId: last.id, delta: newSegment, thought: last.thought });
283
+ this.emit('message_update', last);
284
+ }
285
+ }
286
+ }
287
+ if (delta.text) {
288
+ const rawChunk = delta.text;
289
+ // Logic change: we must rectify against the CURRENT active text part, not the global text
290
+ // 1. Find or create active text part
291
+ let lastPart = last.content[last.content.length - 1];
292
+ if (!lastPart || lastPart.type !== 'text') {
293
+ lastPart = { type: 'text', text: '' };
294
+ last.content.push(lastPart);
295
+ }
296
+ const currentPartText = lastPart.type === 'text' ? lastPart.text : ''; // Should be text
297
+ // 2. Rectify relative to THAT part
298
+ const newSegment = extractNewStreamSegment(currentPartText, rawChunk);
299
+ if (newSegment && lastPart.type === 'text') {
300
+ lastPart.text += newSegment; // Update structured content
301
+ last.text += newSegment; // Update flat text (legacy)
302
+ if (shouldEmitAssistant) {
303
+ this.emit('text_delta', { messageId: last.id, delta: newSegment, text: last.text });
304
+ this.emit('assistant_text_delta', { messageId: last.id, delta: newSegment, text: last.text });
305
+ this.emit('message_update', last);
306
+ }
307
+ }
308
+ }
309
+ }
310
+ handleToolCall(update) {
311
+ const last = this.getOrCreateAssistantMessage();
312
+ const toolCallId = update.toolCallId;
313
+ const name = toolCallId?.split('-')[0] || 'unknown';
314
+ const parsed = this.parseToolTitle(update.title);
315
+ let status = (update.status || 'running');
316
+ if (update.status === 'in_progress') {
317
+ status = 'running';
318
+ }
319
+ const toolCall = {
320
+ id: toolCallId,
321
+ name,
322
+ title: update.title || '',
323
+ status,
324
+ args: parsed.args,
325
+ input: parsed.input,
326
+ description: parsed.description,
327
+ workingDir: parsed.workingDir,
328
+ ts: this.now(),
329
+ };
330
+ last.toolCalls.push(toolCall);
331
+ // Add to ordered content
332
+ last.content.push({ type: 'tool_call', call: toolCall });
333
+ if (this.shouldEmitAssistant(this.currentTurnHidden)) {
334
+ this.emit('tool_update', { messageId: last.id, toolCall });
335
+ this.emit('tool_call_started', { messageId: last.id, toolCall });
336
+ this.emit('message_update', last);
337
+ }
338
+ }
339
+ handleToolUpdate(update) {
340
+ const last = this.getOrCreateAssistantMessage();
341
+ const toolCallId = update.toolCallId;
342
+ let toolCall = last.toolCalls.find(t => t.id === toolCallId);
343
+ if (!toolCall && this.pendingApproval && this.pendingApproval.toolCall?.toolCallId === toolCallId) {
344
+ toolCall = this.createToolCallFromPermission(last, this.pendingApproval);
345
+ }
346
+ if (toolCall) {
347
+ if (update.status) {
348
+ toolCall.status = (update.status === 'in_progress' ? 'running' : update.status);
349
+ }
350
+ if (update.content) {
351
+ const contentList = Array.isArray(update.content) ? update.content : [update.content];
352
+ for (const contentObj of contentList) {
353
+ let text = '';
354
+ if (typeof contentObj === 'string')
355
+ text = contentObj;
356
+ else if (contentObj?.content?.text)
357
+ text = contentObj.content.text;
358
+ else {
359
+ const diff = this.extractDiffData(contentObj);
360
+ if (diff) {
361
+ toolCall.diff = diff;
362
+ text = diff.unified;
363
+ }
364
+ }
365
+ if (text) {
366
+ toolCall.result = toolCall.result ? `${toolCall.result}\n${text}` : text;
367
+ }
368
+ }
369
+ }
370
+ if (this.shouldEmitAssistant(this.currentTurnHidden)) {
371
+ this.emit('tool_update', { messageId: last.id, toolCall });
372
+ this.emit('tool_call_updated', { messageId: last.id, toolCall });
373
+ }
374
+ if (toolCall.status === 'completed' || toolCall.status === 'failed' || toolCall.status === 'cancelled') {
375
+ if (this.shouldEmitAssistant(this.currentTurnHidden)) {
376
+ this.emit('tool_call_completed', { messageId: last.id, toolCall });
377
+ }
378
+ }
379
+ if (this.shouldEmitAssistant(this.currentTurnHidden)) {
380
+ this.emit('message_update', last);
381
+ }
382
+ }
383
+ }
384
+ getOrCreateAssistantMessage() {
385
+ let last = this.messages[this.messages.length - 1];
386
+ if (!last || last.role !== 'assistant') {
387
+ last = {
388
+ id: this.makeId('assistant'),
389
+ role: 'assistant',
390
+ text: '',
391
+ thought: '',
392
+ content: [],
393
+ toolCalls: [],
394
+ ts: this.now(),
395
+ hidden: this.currentTurnHidden === 'assistant' || this.currentTurnHidden === 'turn',
396
+ };
397
+ this.messages.push(last);
398
+ if (this.shouldEmitAssistant(this.currentTurnHidden)) {
399
+ this.emit('message', last);
400
+ }
401
+ if (this.inTurn)
402
+ this.activeAssistantId = last.id;
403
+ }
404
+ return last;
405
+ }
406
+ handleAuthUrl(notif) {
407
+ this.authUrl = notif.params.url;
408
+ this.emit('auth_required', this.authUrl);
409
+ }
410
+ handlePermissionRequest(req) {
411
+ if (this.shouldAutoRejectApproval(this.currentTurnHidden)) {
412
+ const denyOption = req.params.options.find((opt) => opt.kind.startsWith('deny') || opt.kind.startsWith('reject'));
413
+ if (denyOption) {
414
+ this.resolveApproval(req.id ?? 'unknown-id', denyOption.optionId).catch(() => { });
415
+ }
416
+ return;
417
+ }
418
+ const parsed = this.parseToolTitle(req.params.toolCall?.title || '');
419
+ this.pendingApproval = {
420
+ requestId: req.id ?? 'unknown-id',
421
+ toolCall: req.params.toolCall,
422
+ options: req.params.options,
423
+ };
424
+ this.pendingApproval.toolCall.input = parsed.input;
425
+ this.pendingApproval.toolCall.description = parsed.description;
426
+ this.pendingApproval.toolCall.workingDir = parsed.workingDir;
427
+ this.pendingApproval.toolCall.args = parsed.args;
428
+ const last = this.getOrCreateAssistantMessage();
429
+ this.createToolCallFromPermission(last, this.pendingApproval);
430
+ // Pure SDK: just notify app. App handles policy.
431
+ if (this.shouldEmitAssistant(this.currentTurnHidden)) {
432
+ this.emit('approval_required', this.pendingApproval);
433
+ this.emit('permission_required', this.pendingApproval);
434
+ }
435
+ }
436
+ disconnect() {
437
+ this.dispose();
438
+ }
439
+ shouldEmitUser(hiddenMode) {
440
+ return hiddenMode === 'none' || hiddenMode === 'assistant';
441
+ }
442
+ shouldEmitAssistant(hiddenMode) {
443
+ return hiddenMode === 'none' || hiddenMode === 'user';
444
+ }
445
+ shouldAutoRejectApproval(hiddenMode) {
446
+ return hiddenMode === 'assistant' || hiddenMode === 'turn';
447
+ }
448
+ async resolveApproval(requestId, optionId) {
449
+ await this.transport.sendResponse(requestId, {
450
+ sessionId: this.sessionId,
451
+ outcome: { outcome: 'selected', optionId },
452
+ });
453
+ await this.transport.sendNotification('session/provide_permission', {
454
+ sessionId: this.sessionId,
455
+ outcome: { outcome: 'selected', optionId },
456
+ });
457
+ this.pendingApproval = null;
458
+ this.emit('approval_resolved');
459
+ }
460
+ parseToolTitle(title) {
461
+ let args = null;
462
+ let description;
463
+ let workingDir;
464
+ let input = title || '';
465
+ if (title) {
466
+ const jsonMatch = title.match(/inputs?:\s*(\{.*\})/);
467
+ if (jsonMatch) {
468
+ try {
469
+ args = JSON.parse(jsonMatch[1]);
470
+ }
471
+ catch (e) {
472
+ args = jsonMatch[1];
473
+ }
474
+ }
475
+ else {
476
+ // Ensure input is a string
477
+ input = input || '';
478
+ // Extract Working Directory [...] first
479
+ // Use a more specific regex for CWD to avoid false positives, but keep fallback
480
+ const cwdMatch = title.match(/\s*\[(current working directory [^\]]+)\]/);
481
+ if (cwdMatch) {
482
+ const cwdRaw = cwdMatch[1];
483
+ workingDir = cwdRaw.replace(/^current working directory\s*/i, '');
484
+ // Remove CWD from input, being careful if it appears in the middle
485
+ input = input.replace(cwdMatch[0], '');
486
+ }
487
+ // Extract Description (...) at the end
488
+ // Use manual backward scanning to handle nested parentheses
489
+ const trimmedInput = input.trimEnd();
490
+ if (trimmedInput.endsWith(')')) {
491
+ let balance = 0;
492
+ let startIndex = -1;
493
+ for (let i = trimmedInput.length - 1; i >= 0; i--) {
494
+ if (trimmedInput[i] === ')')
495
+ balance++;
496
+ if (trimmedInput[i] === '(')
497
+ balance--;
498
+ if (balance === 0) {
499
+ startIndex = i;
500
+ break;
501
+ }
502
+ }
503
+ if (startIndex !== -1) {
504
+ description = trimmedInput.substring(startIndex + 1, trimmedInput.length - 1);
505
+ // Remove description from input, handling the whitespace before it
506
+ const fullMatch = trimmedInput.substring(startIndex);
507
+ // finding the actual match in the original input string to preserve robust replacement
508
+ const matchIndex = input.lastIndexOf(fullMatch);
509
+ if (matchIndex !== -1) {
510
+ input = input.substring(0, matchIndex);
511
+ }
512
+ }
513
+ }
514
+ input = input.trim();
515
+ }
516
+ }
517
+ return { args, input, description, workingDir };
518
+ }
519
+ createToolCallFromPermission(last, approval) {
520
+ const toolCallId = approval.toolCall?.toolCallId || 'unknown';
521
+ let toolCall = last.toolCalls.find(t => t.id === toolCallId);
522
+ if (toolCall)
523
+ return toolCall;
524
+ const name = toolCallId.split('-')[0] || 'unknown';
525
+ toolCall = {
526
+ id: toolCallId,
527
+ name,
528
+ title: approval.toolCall?.title || '',
529
+ status: 'queued',
530
+ args: approval.toolCall?.args,
531
+ input: approval.toolCall?.input,
532
+ description: approval.toolCall?.description,
533
+ workingDir: approval.toolCall?.workingDir,
534
+ ts: this.now(),
535
+ };
536
+ last.toolCalls.push(toolCall);
537
+ last.content.push({ type: 'tool_call', call: toolCall }); // Fix: Add to ordered content
538
+ this.emit('tool_update', { messageId: last.id, toolCall });
539
+ this.emit('tool_call_started', { messageId: last.id, toolCall });
540
+ this.emit('message_update', last);
541
+ return toolCall;
542
+ }
543
+ extractDiffData(contentObj) {
544
+ if (!contentObj || typeof contentObj !== 'object')
545
+ return null;
546
+ const diffPayload = (contentObj.type === 'diff' ? contentObj : null) ||
547
+ contentObj.diff ||
548
+ contentObj.content?.diff ||
549
+ null;
550
+ if (!diffPayload || typeof diffPayload !== 'object')
551
+ return null;
552
+ const path = diffPayload.path ?? contentObj.path;
553
+ const oldText = diffPayload.oldText ?? diffPayload.before ?? '';
554
+ const newText = diffPayload.newText ?? diffPayload.after ?? '';
555
+ const unifiedFromPayload = diffPayload.unified ?? diffPayload.patch ?? diffPayload.diff;
556
+ let unified = '';
557
+ if (typeof unifiedFromPayload === 'string' && unifiedFromPayload.trim()) {
558
+ unified = unifiedFromPayload.trimEnd();
559
+ }
560
+ else {
561
+ const contextLines = this.normalizeDiffContextLines(this.options.diffContextLines);
562
+ unified = createUnifiedDiff(String(oldText ?? ''), String(newText ?? ''), path, contextLines);
563
+ }
564
+ if (!unified)
565
+ return null;
566
+ const oldLen = typeof oldText === 'string' ? oldText.length : undefined;
567
+ const newLen = typeof newText === 'string' ? newText.length : undefined;
568
+ return {
569
+ path,
570
+ unified,
571
+ oldTextLength: oldLen,
572
+ newTextLength: newLen,
573
+ };
574
+ }
575
+ normalizeDiffContextLines(value) {
576
+ if (typeof value !== 'number' || !Number.isFinite(value) || value < 0)
577
+ return 3;
578
+ return Math.floor(value);
579
+ }
580
+ now() {
581
+ return this.timeOverride ?? Date.now();
582
+ }
583
+ handleReplayPrompt(prompt) {
584
+ const first = Array.isArray(prompt) ? prompt[0] : prompt;
585
+ const text = first?.text ?? '';
586
+ if (typeof text !== 'string' || !text.trim())
587
+ return;
588
+ const userMsg = {
589
+ id: this.makeId('user'),
590
+ role: 'user',
591
+ text,
592
+ ts: this.now(),
593
+ };
594
+ this.messages.push(userMsg);
595
+ this.emit('message', userMsg);
596
+ this.emit('message_update', userMsg);
597
+ }
598
+ makeId(prefix) {
599
+ this.idCounter += 1;
600
+ const nonce = this.replayNonce ? `-${this.replayNonce}` : '';
601
+ return `${prefix}-${this.now()}${nonce}-${this.idCounter}`;
602
+ }
603
+ buildUrlWithReplay(baseUrl, replay) {
604
+ if (!replay || Object.values(replay).every((v) => v === undefined))
605
+ return baseUrl;
606
+ try {
607
+ const url = new URL(baseUrl);
608
+ if (replay.limit !== undefined)
609
+ url.searchParams.set('limit', String(replay.limit));
610
+ if (replay.since !== undefined)
611
+ url.searchParams.set('since', String(replay.since));
612
+ if (replay.before !== undefined)
613
+ url.searchParams.set('before', String(replay.before));
614
+ return url.toString();
615
+ }
616
+ catch {
617
+ const params = [];
618
+ if (replay.limit !== undefined)
619
+ params.push(`limit=${encodeURIComponent(String(replay.limit))}`);
620
+ if (replay.since !== undefined)
621
+ params.push(`since=${encodeURIComponent(String(replay.since))}`);
622
+ if (replay.before !== undefined)
623
+ params.push(`before=${encodeURIComponent(String(replay.before))}`);
624
+ const joiner = baseUrl.includes('?') ? '&' : '?';
625
+ return params.length ? `${baseUrl}${joiner}${params.join('&')}` : baseUrl;
626
+ }
627
+ }
628
+ static async fetchReplay(baseUrl, replay, options = {}) {
629
+ const client = new AgentChatClient({ url: baseUrl, replay });
630
+ const idleMs = options.idleMs ?? 200;
631
+ let idleTimer = null;
632
+ let firstTimer = null;
633
+ let receivedAny = false;
634
+ const bump = () => {
635
+ receivedAny = true;
636
+ if (firstTimer) {
637
+ clearTimeout(firstTimer);
638
+ firstTimer = null;
639
+ }
640
+ if (idleTimer)
641
+ clearTimeout(idleTimer);
642
+ idleTimer = setTimeout(() => {
643
+ const messages = client.getMessages();
644
+ cleanup();
645
+ resolve(messages);
646
+ }, idleMs);
647
+ };
648
+ let resolve;
649
+ let reject;
650
+ const promise = new Promise((res, rej) => {
651
+ resolve = res;
652
+ reject = rej;
653
+ });
654
+ const cleanup = () => {
655
+ if (idleTimer)
656
+ clearTimeout(idleTimer);
657
+ if (firstTimer)
658
+ clearTimeout(firstTimer);
659
+ client.dispose();
660
+ };
661
+ client.on('message', bump);
662
+ client.on('message_update', bump);
663
+ client.on('tool_update', bump);
664
+ client.connect({ autoSession: false }).catch((err) => {
665
+ cleanup();
666
+ reject(err);
667
+ });
668
+ // In case no messages arrive at all
669
+ firstTimer = setTimeout(() => {
670
+ if (receivedAny)
671
+ return;
672
+ const messages = client.getMessages();
673
+ cleanup();
674
+ resolve(messages);
675
+ }, idleMs);
676
+ return promise;
677
+ }
678
+ }
679
+ //# sourceMappingURL=AgentChatClient.js.map