@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.
- package/README.md +109 -0
- package/client/index.d.ts +1 -0
- package/client/index.js +1 -0
- package/client/package.json +1 -0
- package/dist/client.d.ts +5 -0
- package/dist/client.js +5 -0
- package/dist/client.js.map +1 -0
- package/dist/common/types.d.ts +191 -0
- package/dist/common/types.js +18 -0
- package/dist/common/types.js.map +1 -0
- package/dist/core/AcpWebSocketTransport.d.ts +25 -0
- package/dist/core/AcpWebSocketTransport.js +222 -0
- package/dist/core/AcpWebSocketTransport.js.map +1 -0
- package/dist/core/AgentChatClient.d.ts +75 -0
- package/dist/core/AgentChatClient.js +679 -0
- package/dist/core/AgentChatClient.js.map +1 -0
- package/dist/core/ToolPermissionManager.d.ts +26 -0
- package/dist/core/ToolPermissionManager.js +88 -0
- package/dist/core/ToolPermissionManager.js.map +1 -0
- package/dist/core/diff-utils.d.ts +1 -0
- package/dist/core/diff-utils.js +7 -0
- package/dist/core/diff-utils.js.map +1 -0
- package/dist/core/stream-utils.d.ts +14 -0
- package/dist/core/stream-utils.js +57 -0
- package/dist/core/stream-utils.js.map +1 -0
- package/dist/extras/index.d.ts +1 -0
- package/dist/extras/index.js +2 -0
- package/dist/extras/index.js.map +1 -0
- package/dist/extras/sys-tags.d.ts +38 -0
- package/dist/extras/sys-tags.js +150 -0
- package/dist/extras/sys-tags.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/server/GeminiBridge.d.ts +50 -0
- package/dist/server/GeminiBridge.js +500 -0
- package/dist/server/GeminiBridge.js.map +1 -0
- package/dist/server.d.ts +7 -0
- package/dist/server.js +7 -0
- package/dist/server.js.map +1 -0
- package/dist/ui/AgentChatStore.d.ts +16 -0
- package/dist/ui/AgentChatStore.js +59 -0
- package/dist/ui/AgentChatStore.js.map +1 -0
- package/docs/API.md +100 -0
- package/docs/EVENTS.md +100 -0
- package/docs/INTEGRATION.md +109 -0
- package/docs/SPECIFICATION.md +93 -0
- package/docs/TROUBLESHOOTING.md +44 -0
- package/docs/USAGE.md +270 -0
- package/docs/design.md +62 -0
- package/package.json +71 -0
- package/server/index.d.ts +1 -0
- package/server/index.js +1 -0
- 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
|