@agent-link/agent 0.1.235 → 0.1.237
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/dist/backends/index.js +2 -0
- package/dist/backends/index.js.map +1 -1
- package/dist/backends/types.d.ts +7 -1
- package/dist/backends/types.js +62 -1
- package/dist/backends/types.js.map +1 -1
- package/dist/connection.js +197 -15
- package/dist/connection.js.map +1 -1
- package/dist/copilot.d.ts +69 -0
- package/dist/copilot.js +676 -0
- package/dist/copilot.js.map +1 -0
- package/dist/providers.d.ts +7 -0
- package/dist/providers.js +30 -0
- package/dist/providers.js.map +1 -1
- package/dist/runtime.d.ts +4 -0
- package/dist/runtime.js +13 -0
- package/dist/runtime.js.map +1 -1
- package/package.json +1 -1
package/dist/copilot.js
ADDED
|
@@ -0,0 +1,676 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copilot CLI process driver for AgentLink.
|
|
3
|
+
*
|
|
4
|
+
* Parallel to claude.ts but simpler:
|
|
5
|
+
* - Each turn spawns a new `copilot -p "..." --output-format json` process
|
|
6
|
+
* - No persistent process, no stdin input stream, no control_request protocol
|
|
7
|
+
* - JSONL stdout parsed and emitted as backend_event messages
|
|
8
|
+
* - Session state stored at ~/.copilot/session-state/<id>/
|
|
9
|
+
*/
|
|
10
|
+
import { spawn, execFileSync } from 'child_process';
|
|
11
|
+
import { createInterface } from 'readline';
|
|
12
|
+
import { readdirSync, readFileSync, writeFileSync, existsSync, rmSync, statSync } from 'fs';
|
|
13
|
+
import { join, resolve } from 'path';
|
|
14
|
+
import { homedir, platform } from 'os';
|
|
15
|
+
import { randomUUID } from 'crypto';
|
|
16
|
+
import { copilotCapabilities } from './backends/types.js';
|
|
17
|
+
// ── Module state ───────────────────────────────────────────────────────────
|
|
18
|
+
const conversations = new Map();
|
|
19
|
+
const sendFns = new Set();
|
|
20
|
+
let globalModel = null;
|
|
21
|
+
// ── Send fanout (same pattern as claude.ts) ────────────────────────────────
|
|
22
|
+
function sendFn(msg) {
|
|
23
|
+
const eventType = msg.type === 'backend_event'
|
|
24
|
+
? `backend_event:${msg.event?.type || '?'}`
|
|
25
|
+
: String(msg.type);
|
|
26
|
+
console.log(`[Copilot] send: ${eventType} (${sendFns.size} listeners)`);
|
|
27
|
+
for (const fn of sendFns) {
|
|
28
|
+
try {
|
|
29
|
+
fn(msg);
|
|
30
|
+
}
|
|
31
|
+
catch (e) { /* ignore */ }
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export function addCopilotSendFn(fn) {
|
|
35
|
+
sendFns.add(fn);
|
|
36
|
+
return () => { sendFns.delete(fn); };
|
|
37
|
+
}
|
|
38
|
+
// ── Session ref helper ─────────────────────────────────────────────────────
|
|
39
|
+
function sessionRef(sessionId) {
|
|
40
|
+
return {
|
|
41
|
+
backendType: 'copilot',
|
|
42
|
+
backendSessionId: sessionId,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
// ── Resolve copilot executable path ────────────────────────────────────────
|
|
46
|
+
let resolvedCopilotPath = null;
|
|
47
|
+
function resolveCopilotPath() {
|
|
48
|
+
if (resolvedCopilotPath)
|
|
49
|
+
return resolvedCopilotPath;
|
|
50
|
+
const isWin = platform() === 'win32';
|
|
51
|
+
// Try where/which
|
|
52
|
+
try {
|
|
53
|
+
const bin = isWin ? 'where' : 'which';
|
|
54
|
+
const output = execFileSync(bin, ['copilot'], {
|
|
55
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
56
|
+
timeout: 5000,
|
|
57
|
+
windowsHide: true,
|
|
58
|
+
}).toString().trim();
|
|
59
|
+
const lines = output.split('\n').map(l => l.trim()).filter(Boolean);
|
|
60
|
+
if (isWin && lines.length > 1) {
|
|
61
|
+
resolvedCopilotPath = lines.find(l => /\.(cmd|exe)$/i.test(l)) || lines[0];
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
resolvedCopilotPath = lines[0] || null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// where/which failed — try PowerShell on Windows
|
|
69
|
+
if (isWin) {
|
|
70
|
+
try {
|
|
71
|
+
const psOutput = execFileSync('powershell.exe', [
|
|
72
|
+
'-NoProfile', '-Command',
|
|
73
|
+
'(Get-Command copilot -ErrorAction SilentlyContinue).Source',
|
|
74
|
+
], {
|
|
75
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
76
|
+
timeout: 8000,
|
|
77
|
+
windowsHide: true,
|
|
78
|
+
}).toString().trim();
|
|
79
|
+
if (psOutput && existsSync(psOutput)) {
|
|
80
|
+
resolvedCopilotPath = psOutput;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch { /* ignore */ }
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (!resolvedCopilotPath) {
|
|
87
|
+
// Fallback: just use 'copilot' and hope it's in PATH
|
|
88
|
+
resolvedCopilotPath = 'copilot';
|
|
89
|
+
}
|
|
90
|
+
console.log(`[Copilot] Resolved command: ${resolvedCopilotPath}`);
|
|
91
|
+
return resolvedCopilotPath;
|
|
92
|
+
}
|
|
93
|
+
// ── Conversation management ────────────────────────────────────────────────
|
|
94
|
+
export function getCopilotConversation(conversationId) {
|
|
95
|
+
return conversations.get(conversationId);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Rebind a running copilot conversation to a new conversationId.
|
|
99
|
+
* Used when web client refreshes and generates a new UUID for the same session.
|
|
100
|
+
* Returns true if a running conversation was found and rebound.
|
|
101
|
+
*/
|
|
102
|
+
export function rebindCopilotConversation(copilotSessionId, newConvId) {
|
|
103
|
+
for (const [oldConvId, state] of conversations) {
|
|
104
|
+
if (state.copilotSessionId === copilotSessionId && oldConvId !== newConvId) {
|
|
105
|
+
conversations.delete(oldConvId);
|
|
106
|
+
state.conversationId = newConvId;
|
|
107
|
+
conversations.set(newConvId, state);
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
export function createCopilotPlaceholder(conversationId, opts) {
|
|
114
|
+
if (conversations.has(conversationId))
|
|
115
|
+
return;
|
|
116
|
+
conversations.set(conversationId, {
|
|
117
|
+
conversationId,
|
|
118
|
+
child: null,
|
|
119
|
+
abortController: null,
|
|
120
|
+
copilotSessionId: null,
|
|
121
|
+
lastCopilotSessionId: null,
|
|
122
|
+
workDir: process.cwd(),
|
|
123
|
+
turnActive: false,
|
|
124
|
+
providerId: opts?.providerId || 'copilot',
|
|
125
|
+
model: null,
|
|
126
|
+
sessionNotified: false,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
export function restartCopilotConversation(conversationId, opts) {
|
|
130
|
+
const state = conversations.get(conversationId);
|
|
131
|
+
const wasTurnActive = state?.turnActive || false;
|
|
132
|
+
const copilotSessionId = state?.copilotSessionId || null;
|
|
133
|
+
if (state) {
|
|
134
|
+
cleanupCopilotConversation(conversationId);
|
|
135
|
+
}
|
|
136
|
+
conversations.set(conversationId, {
|
|
137
|
+
conversationId,
|
|
138
|
+
child: null,
|
|
139
|
+
abortController: null,
|
|
140
|
+
copilotSessionId: null,
|
|
141
|
+
lastCopilotSessionId: copilotSessionId,
|
|
142
|
+
workDir: state?.workDir || process.cwd(),
|
|
143
|
+
turnActive: false,
|
|
144
|
+
providerId: opts?.providerId || state?.providerId || 'copilot',
|
|
145
|
+
model: opts?.model || state?.model || null,
|
|
146
|
+
sessionNotified: false,
|
|
147
|
+
});
|
|
148
|
+
return { wasTurnActive, copilotSessionId };
|
|
149
|
+
}
|
|
150
|
+
export function setModel(conversationId, model) {
|
|
151
|
+
if (conversationId) {
|
|
152
|
+
const state = conversations.get(conversationId);
|
|
153
|
+
if (state)
|
|
154
|
+
state.model = model;
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
globalModel = model;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
export function handleCopilotChat(conversationId, text, workDir, options, _files) {
|
|
161
|
+
let state = conversations.get(conversationId);
|
|
162
|
+
if (!state) {
|
|
163
|
+
state = {
|
|
164
|
+
conversationId,
|
|
165
|
+
child: null,
|
|
166
|
+
abortController: null,
|
|
167
|
+
copilotSessionId: null,
|
|
168
|
+
lastCopilotSessionId: null,
|
|
169
|
+
workDir,
|
|
170
|
+
turnActive: false,
|
|
171
|
+
providerId: 'copilot',
|
|
172
|
+
model: null,
|
|
173
|
+
sessionNotified: false,
|
|
174
|
+
};
|
|
175
|
+
conversations.set(conversationId, state);
|
|
176
|
+
}
|
|
177
|
+
state.workDir = workDir;
|
|
178
|
+
// Determine session to resume
|
|
179
|
+
const resumeId = options?.resumeSessionId
|
|
180
|
+
|| state.copilotSessionId
|
|
181
|
+
|| state.lastCopilotSessionId;
|
|
182
|
+
// For new sessions, pre-assign a sessionId via --session-id so we know it
|
|
183
|
+
// immediately (don't have to wait for the `result` event).
|
|
184
|
+
if (!resumeId) {
|
|
185
|
+
state.copilotSessionId = randomUUID();
|
|
186
|
+
}
|
|
187
|
+
// Build args
|
|
188
|
+
const args = [
|
|
189
|
+
'-p', text,
|
|
190
|
+
'--output-format', 'json',
|
|
191
|
+
'--allow-all-tools',
|
|
192
|
+
'-s',
|
|
193
|
+
'--no-ask-user',
|
|
194
|
+
];
|
|
195
|
+
if (resumeId) {
|
|
196
|
+
args.push(`--resume=${resumeId}`);
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
args.push(`--session-id=${state.copilotSessionId}`);
|
|
200
|
+
}
|
|
201
|
+
const model = state.model || globalModel;
|
|
202
|
+
if (model) {
|
|
203
|
+
args.push('--model', model);
|
|
204
|
+
}
|
|
205
|
+
args.push('-C', workDir);
|
|
206
|
+
// Spawn copilot process
|
|
207
|
+
const controller = new AbortController();
|
|
208
|
+
state.abortController = controller;
|
|
209
|
+
state.turnActive = true;
|
|
210
|
+
// Emit turn_started
|
|
211
|
+
sendFn({
|
|
212
|
+
type: 'backend_event',
|
|
213
|
+
conversationId,
|
|
214
|
+
event: {
|
|
215
|
+
type: 'turn_started',
|
|
216
|
+
session: sessionRef(resumeId || 'pending'),
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
const copilotCmd = resolveCopilotPath();
|
|
220
|
+
// On Windows, .cmd files can't be spawned directly — use cmd.exe /c
|
|
221
|
+
const isWin = platform() === 'win32';
|
|
222
|
+
const spawnCmd = isWin ? (process.env.COMSPEC || 'cmd.exe') : copilotCmd;
|
|
223
|
+
const spawnArgs = isWin ? ['/c', copilotCmd, ...args] : args;
|
|
224
|
+
const child = spawn(spawnCmd, spawnArgs, {
|
|
225
|
+
cwd: workDir,
|
|
226
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
227
|
+
windowsHide: true,
|
|
228
|
+
signal: controller.signal,
|
|
229
|
+
});
|
|
230
|
+
state.child = child;
|
|
231
|
+
// Parse JSONL stdout
|
|
232
|
+
const rl = createInterface({ input: child.stdout });
|
|
233
|
+
let stderrBuf = '';
|
|
234
|
+
child.stderr?.on('data', (chunk) => {
|
|
235
|
+
stderrBuf += chunk.toString();
|
|
236
|
+
});
|
|
237
|
+
rl.on('line', (line) => {
|
|
238
|
+
if (!line.trim())
|
|
239
|
+
return;
|
|
240
|
+
try {
|
|
241
|
+
const event = JSON.parse(line);
|
|
242
|
+
handleCopilotEvent(state, event);
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
// Non-JSON output, ignore
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
child.on('close', (code) => {
|
|
249
|
+
state.child = null;
|
|
250
|
+
state.abortController = null;
|
|
251
|
+
if (state.turnActive) {
|
|
252
|
+
state.turnActive = false;
|
|
253
|
+
// If turn wasn't completed by a result event, emit error or cancellation
|
|
254
|
+
if (code !== 0 && code !== null) {
|
|
255
|
+
const errMsg = stderrBuf.trim() || `Copilot process exited with code ${code}`;
|
|
256
|
+
sendFn({
|
|
257
|
+
type: 'backend_event',
|
|
258
|
+
conversationId,
|
|
259
|
+
event: { type: 'error', message: errMsg },
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
// Emit backend_event turn_completed so web clears processing state
|
|
263
|
+
sendFn({
|
|
264
|
+
type: 'backend_event',
|
|
265
|
+
conversationId,
|
|
266
|
+
event: {
|
|
267
|
+
type: 'turn_completed',
|
|
268
|
+
session: sessionRef(state.copilotSessionId || ''),
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
// Also emit top-level turn_completed for backward compat
|
|
272
|
+
sendFn({
|
|
273
|
+
type: 'turn_completed',
|
|
274
|
+
conversationId,
|
|
275
|
+
workDir: state.workDir,
|
|
276
|
+
claudeSessionId: state.copilotSessionId,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
child.on('error', (err) => {
|
|
281
|
+
state.child = null;
|
|
282
|
+
state.turnActive = false;
|
|
283
|
+
sendFn({
|
|
284
|
+
type: 'backend_event',
|
|
285
|
+
conversationId,
|
|
286
|
+
event: { type: 'error', message: `Failed to start copilot: ${err.message}` },
|
|
287
|
+
});
|
|
288
|
+
sendFn({
|
|
289
|
+
type: 'backend_event',
|
|
290
|
+
conversationId,
|
|
291
|
+
event: {
|
|
292
|
+
type: 'turn_completed',
|
|
293
|
+
session: sessionRef(state.copilotSessionId || ''),
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
sendFn({
|
|
297
|
+
type: 'turn_completed',
|
|
298
|
+
conversationId,
|
|
299
|
+
workDir: state.workDir,
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
// ── JSONL event handler ────────────────────────────────────────────────────
|
|
304
|
+
function handleCopilotEvent(state, event) {
|
|
305
|
+
const convId = state.conversationId;
|
|
306
|
+
// On first non-ephemeral event, notify web so sidebar refreshes
|
|
307
|
+
// and currentClaudeSessionId is set correctly.
|
|
308
|
+
if (!state.sessionNotified && !event.ephemeral) {
|
|
309
|
+
state.sessionNotified = true;
|
|
310
|
+
sendFn({
|
|
311
|
+
type: 'backend_event',
|
|
312
|
+
conversationId: convId,
|
|
313
|
+
event: { type: 'session_started', session: sessionRef(state.copilotSessionId || 'pending') },
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
const session = sessionRef(state.copilotSessionId || 'pending');
|
|
317
|
+
switch (event.type) {
|
|
318
|
+
case 'assistant.message_delta': {
|
|
319
|
+
const data = event.data;
|
|
320
|
+
if (data?.deltaContent) {
|
|
321
|
+
sendFn({
|
|
322
|
+
type: 'backend_event',
|
|
323
|
+
conversationId: convId,
|
|
324
|
+
event: { type: 'text_delta', session, text: data.deltaContent },
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
case 'assistant.reasoning_delta': {
|
|
330
|
+
const data = event.data;
|
|
331
|
+
if (data?.deltaContent) {
|
|
332
|
+
sendFn({
|
|
333
|
+
type: 'backend_event',
|
|
334
|
+
conversationId: convId,
|
|
335
|
+
event: { type: 'reasoning_delta', session, text: data.deltaContent },
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
break;
|
|
339
|
+
}
|
|
340
|
+
case 'assistant.message': {
|
|
341
|
+
const data = event.data;
|
|
342
|
+
// Emit tool_started for each tool request (skip report_intent)
|
|
343
|
+
if (data?.toolRequests) {
|
|
344
|
+
for (const tr of data.toolRequests) {
|
|
345
|
+
if (tr.name === 'report_intent')
|
|
346
|
+
continue;
|
|
347
|
+
sendFn({
|
|
348
|
+
type: 'backend_event',
|
|
349
|
+
conversationId: convId,
|
|
350
|
+
event: {
|
|
351
|
+
type: 'tool_started',
|
|
352
|
+
session,
|
|
353
|
+
tool: {
|
|
354
|
+
id: tr.toolCallId,
|
|
355
|
+
name: tr.name,
|
|
356
|
+
itemKind: tr.name,
|
|
357
|
+
input: tr.arguments,
|
|
358
|
+
status: 'in_progress',
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
break;
|
|
365
|
+
}
|
|
366
|
+
case 'tool.execution_complete': {
|
|
367
|
+
const data = event.data;
|
|
368
|
+
if (data?.toolCallId) {
|
|
369
|
+
// Skip report_intent results
|
|
370
|
+
if (data.toolName === 'report_intent')
|
|
371
|
+
break;
|
|
372
|
+
sendFn({
|
|
373
|
+
type: 'backend_event',
|
|
374
|
+
conversationId: convId,
|
|
375
|
+
event: {
|
|
376
|
+
type: 'tool_completed',
|
|
377
|
+
session,
|
|
378
|
+
tool: {
|
|
379
|
+
id: data.toolCallId,
|
|
380
|
+
name: data.toolName || '',
|
|
381
|
+
output: data.success
|
|
382
|
+
? (data.result?.content || data.result?.detailedContent)
|
|
383
|
+
: (data.error?.message || 'Tool execution failed'),
|
|
384
|
+
isError: !data.success,
|
|
385
|
+
status: 'completed',
|
|
386
|
+
},
|
|
387
|
+
},
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
break;
|
|
391
|
+
}
|
|
392
|
+
case 'result': {
|
|
393
|
+
const sessionId = event.sessionId;
|
|
394
|
+
if (sessionId) {
|
|
395
|
+
state.copilotSessionId = sessionId;
|
|
396
|
+
}
|
|
397
|
+
state.turnActive = false;
|
|
398
|
+
// Emit backend_event turn_completed
|
|
399
|
+
sendFn({
|
|
400
|
+
type: 'backend_event',
|
|
401
|
+
conversationId: convId,
|
|
402
|
+
event: {
|
|
403
|
+
type: 'turn_completed',
|
|
404
|
+
session: sessionRef(state.copilotSessionId || ''),
|
|
405
|
+
},
|
|
406
|
+
});
|
|
407
|
+
// Also emit top-level turn_completed (for backward compat with web)
|
|
408
|
+
const usage = event.usage;
|
|
409
|
+
sendFn({
|
|
410
|
+
type: 'turn_completed',
|
|
411
|
+
conversationId: convId,
|
|
412
|
+
workDir: state.workDir,
|
|
413
|
+
claudeSessionId: state.copilotSessionId,
|
|
414
|
+
usage: {
|
|
415
|
+
outputTokens: 0,
|
|
416
|
+
inputTokens: 0,
|
|
417
|
+
totalCost: 0,
|
|
418
|
+
durationMs: Number(usage?.totalApiDurationMs ?? 0),
|
|
419
|
+
model: '',
|
|
420
|
+
},
|
|
421
|
+
});
|
|
422
|
+
break;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
// ── Cancel ──────────────────────────────────────────────────────────────────
|
|
427
|
+
export function cancelCopilotExecution(conversationId) {
|
|
428
|
+
const state = conversations.get(conversationId);
|
|
429
|
+
if (!state?.child)
|
|
430
|
+
return;
|
|
431
|
+
state.turnActive = false;
|
|
432
|
+
// Notify web that execution was cancelled
|
|
433
|
+
sendFn({
|
|
434
|
+
type: 'backend_event',
|
|
435
|
+
conversationId,
|
|
436
|
+
event: { type: 'turn_cancelled', session: sessionRef(state.copilotSessionId || '') },
|
|
437
|
+
});
|
|
438
|
+
sendFn({ type: 'execution_cancelled', conversationId });
|
|
439
|
+
if (platform() === 'win32') {
|
|
440
|
+
try {
|
|
441
|
+
spawn('cmd', ['/c', 'taskkill', '/pid', String(state.child.pid), '/f', '/t'], {
|
|
442
|
+
stdio: 'ignore',
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
catch { /* ignore */ }
|
|
446
|
+
}
|
|
447
|
+
else {
|
|
448
|
+
state.child.kill('SIGTERM');
|
|
449
|
+
}
|
|
450
|
+
state.child = null;
|
|
451
|
+
state.abortController = null;
|
|
452
|
+
}
|
|
453
|
+
function cleanupCopilotConversation(conversationId) {
|
|
454
|
+
const state = conversations.get(conversationId);
|
|
455
|
+
if (!state)
|
|
456
|
+
return;
|
|
457
|
+
if (state.child) {
|
|
458
|
+
cancelCopilotExecution(conversationId);
|
|
459
|
+
}
|
|
460
|
+
// Preserve session ID for resume
|
|
461
|
+
if (state.copilotSessionId) {
|
|
462
|
+
state.lastCopilotSessionId = state.copilotSessionId;
|
|
463
|
+
}
|
|
464
|
+
conversations.delete(conversationId);
|
|
465
|
+
}
|
|
466
|
+
export function cleanupAllCopilotConversations() {
|
|
467
|
+
for (const convId of conversations.keys()) {
|
|
468
|
+
cleanupCopilotConversation(convId);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
// ── Session listing ────────────────────────────────────────────────────────
|
|
472
|
+
const COPILOT_SESSION_DIR = join(homedir(), '.copilot', 'session-state');
|
|
473
|
+
function parseSimpleYaml(content) {
|
|
474
|
+
const result = {};
|
|
475
|
+
for (const line of content.split('\n')) {
|
|
476
|
+
const match = line.match(/^(\w[\w_-]*)\s*:\s*(.*)$/);
|
|
477
|
+
if (match) {
|
|
478
|
+
let val = match[2].trim();
|
|
479
|
+
// Remove quotes
|
|
480
|
+
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
|
481
|
+
val = val.slice(1, -1);
|
|
482
|
+
}
|
|
483
|
+
result[match[1]] = val;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return result;
|
|
487
|
+
}
|
|
488
|
+
export function listCopilotSessions(workDir) {
|
|
489
|
+
if (!existsSync(COPILOT_SESSION_DIR))
|
|
490
|
+
return [];
|
|
491
|
+
const sessions = [];
|
|
492
|
+
const normalizedWorkDir = workDir.replace(/\\/g, '/').toLowerCase();
|
|
493
|
+
try {
|
|
494
|
+
const dirs = readdirSync(COPILOT_SESSION_DIR, { withFileTypes: true });
|
|
495
|
+
for (const dir of dirs) {
|
|
496
|
+
if (!dir.isDirectory())
|
|
497
|
+
continue;
|
|
498
|
+
const wsPath = join(COPILOT_SESSION_DIR, dir.name, 'workspace.yaml');
|
|
499
|
+
if (!existsSync(wsPath))
|
|
500
|
+
continue;
|
|
501
|
+
try {
|
|
502
|
+
const yaml = parseSimpleYaml(readFileSync(wsPath, 'utf-8'));
|
|
503
|
+
const cwd = (yaml.cwd || '').replace(/\\/g, '/').toLowerCase();
|
|
504
|
+
// Filter by workDir match
|
|
505
|
+
if (cwd !== normalizedWorkDir)
|
|
506
|
+
continue;
|
|
507
|
+
const updatedAt = yaml.updated_at ? new Date(yaml.updated_at).getTime() : 0;
|
|
508
|
+
const title = yaml.name || 'Untitled';
|
|
509
|
+
sessions.push({
|
|
510
|
+
sessionId: yaml.id || dir.name,
|
|
511
|
+
title,
|
|
512
|
+
preview: title.length > 60 ? title.substring(0, 60) + '...' : title,
|
|
513
|
+
lastModified: updatedAt || statSync(wsPath).mtimeMs,
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
catch {
|
|
517
|
+
// Skip broken workspace.yaml
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
catch {
|
|
522
|
+
// Session dir not readable
|
|
523
|
+
}
|
|
524
|
+
sessions.sort((a, b) => b.lastModified - a.lastModified);
|
|
525
|
+
return sessions;
|
|
526
|
+
}
|
|
527
|
+
// ── Session history ────────────────────────────────────────────────────────
|
|
528
|
+
export function readCopilotSessionMessages(_workDir, sessionId) {
|
|
529
|
+
const eventsPath = join(COPILOT_SESSION_DIR, sessionId, 'events.jsonl');
|
|
530
|
+
if (!existsSync(eventsPath))
|
|
531
|
+
return [];
|
|
532
|
+
const messages = [];
|
|
533
|
+
try {
|
|
534
|
+
const content = readFileSync(eventsPath, 'utf-8');
|
|
535
|
+
for (const line of content.split('\n')) {
|
|
536
|
+
if (!line.trim())
|
|
537
|
+
continue;
|
|
538
|
+
try {
|
|
539
|
+
const event = JSON.parse(line);
|
|
540
|
+
// Skip ephemeral events
|
|
541
|
+
if (event.ephemeral)
|
|
542
|
+
continue;
|
|
543
|
+
switch (event.type) {
|
|
544
|
+
case 'user.message': {
|
|
545
|
+
const data = event.data;
|
|
546
|
+
if (data?.content) {
|
|
547
|
+
messages.push({ role: 'user', content: data.content });
|
|
548
|
+
}
|
|
549
|
+
break;
|
|
550
|
+
}
|
|
551
|
+
case 'assistant.message': {
|
|
552
|
+
const data = event.data;
|
|
553
|
+
// Add thinking message if reasoning is present
|
|
554
|
+
if (data?.reasoningText) {
|
|
555
|
+
messages.push({ role: 'assistant', content: data.reasoningText, isThinking: true });
|
|
556
|
+
}
|
|
557
|
+
if (data?.content) {
|
|
558
|
+
messages.push({ role: 'assistant', content: data.content });
|
|
559
|
+
}
|
|
560
|
+
if (data?.toolRequests) {
|
|
561
|
+
for (const tr of data.toolRequests) {
|
|
562
|
+
if (tr.name === 'report_intent')
|
|
563
|
+
continue;
|
|
564
|
+
messages.push({
|
|
565
|
+
role: 'tool',
|
|
566
|
+
content: '',
|
|
567
|
+
toolName: tr.name,
|
|
568
|
+
toolInput: JSON.stringify(tr.arguments || {}),
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
break;
|
|
573
|
+
}
|
|
574
|
+
case 'tool.execution_complete': {
|
|
575
|
+
const data = event.data;
|
|
576
|
+
if (data?.toolName === 'report_intent')
|
|
577
|
+
break;
|
|
578
|
+
if (data) {
|
|
579
|
+
// Find the most recent tool message without output and update it
|
|
580
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
581
|
+
if (messages[i].role === 'tool' && messages[i].toolName === data.toolName && !messages[i].toolOutput) {
|
|
582
|
+
messages[i].toolOutput = data.success
|
|
583
|
+
? (data.result?.content || '')
|
|
584
|
+
: (data.error?.message || 'Failed');
|
|
585
|
+
break;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
break;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
catch {
|
|
594
|
+
// Skip unparseable lines
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
catch {
|
|
599
|
+
// File not readable
|
|
600
|
+
}
|
|
601
|
+
return messages;
|
|
602
|
+
}
|
|
603
|
+
export function listAllCopilotSessions(limit = 500) {
|
|
604
|
+
if (!existsSync(COPILOT_SESSION_DIR))
|
|
605
|
+
return [];
|
|
606
|
+
const sessions = [];
|
|
607
|
+
try {
|
|
608
|
+
const dirs = readdirSync(COPILOT_SESSION_DIR, { withFileTypes: true });
|
|
609
|
+
for (const dir of dirs) {
|
|
610
|
+
if (!dir.isDirectory())
|
|
611
|
+
continue;
|
|
612
|
+
const wsPath = join(COPILOT_SESSION_DIR, dir.name, 'workspace.yaml');
|
|
613
|
+
if (!existsSync(wsPath))
|
|
614
|
+
continue;
|
|
615
|
+
try {
|
|
616
|
+
const yaml = parseSimpleYaml(readFileSync(wsPath, 'utf-8'));
|
|
617
|
+
const updatedAt = yaml.updated_at ? new Date(yaml.updated_at).getTime() : 0;
|
|
618
|
+
const title = yaml.name || 'Untitled';
|
|
619
|
+
const cwd = yaml.cwd || '';
|
|
620
|
+
// Derive folder name from cwd (last segment)
|
|
621
|
+
const folderName = cwd.replace(/[\\/]+$/, '').split(/[\\/]/).pop() || cwd;
|
|
622
|
+
sessions.push({
|
|
623
|
+
sessionId: yaml.id || dir.name,
|
|
624
|
+
projectPath: cwd,
|
|
625
|
+
projectFolder: folderName,
|
|
626
|
+
title,
|
|
627
|
+
firstPrompt: title,
|
|
628
|
+
lastModified: updatedAt || statSync(wsPath).mtimeMs,
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
catch { /* skip */ }
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
catch { /* dir not readable */ }
|
|
635
|
+
sessions.sort((a, b) => b.lastModified - a.lastModified);
|
|
636
|
+
return sessions.slice(0, limit);
|
|
637
|
+
}
|
|
638
|
+
// ── Session rename ─────────────────────────────────────────────────────────
|
|
639
|
+
export function renameCopilotSession(_workDir, sessionId, newTitle) {
|
|
640
|
+
const wsPath = resolve(COPILOT_SESSION_DIR, sessionId, 'workspace.yaml');
|
|
641
|
+
if (!wsPath.startsWith(resolve(COPILOT_SESSION_DIR)))
|
|
642
|
+
return false;
|
|
643
|
+
if (!existsSync(wsPath))
|
|
644
|
+
return false;
|
|
645
|
+
try {
|
|
646
|
+
let content = readFileSync(wsPath, 'utf-8');
|
|
647
|
+
// Replace name: line
|
|
648
|
+
content = content.replace(/^name:\s*.*/m, `name: ${newTitle}`);
|
|
649
|
+
// Set user_named: true
|
|
650
|
+
content = content.replace(/^user_named:\s*.*/m, 'user_named: true');
|
|
651
|
+
writeFileSync(wsPath, content, 'utf-8');
|
|
652
|
+
return true;
|
|
653
|
+
}
|
|
654
|
+
catch {
|
|
655
|
+
return false;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
// ── Session deletion ───────────────────────────────────────────────────────
|
|
659
|
+
export function deleteCopilotSession(_workDir, sessionId) {
|
|
660
|
+
const sessionDir = resolve(COPILOT_SESSION_DIR, sessionId);
|
|
661
|
+
// Prevent path traversal — ensure resolved path is under session-state dir
|
|
662
|
+
if (!sessionDir.startsWith(resolve(COPILOT_SESSION_DIR)))
|
|
663
|
+
return false;
|
|
664
|
+
if (!existsSync(sessionDir))
|
|
665
|
+
return false;
|
|
666
|
+
try {
|
|
667
|
+
rmSync(sessionDir, { recursive: true, force: true });
|
|
668
|
+
return true;
|
|
669
|
+
}
|
|
670
|
+
catch {
|
|
671
|
+
return false;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
// ── Capabilities export ────────────────────────────────────────────────────
|
|
675
|
+
export { copilotCapabilities };
|
|
676
|
+
//# sourceMappingURL=copilot.js.map
|