termbeam 1.19.4 → 1.20.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 (66) hide show
  1. package/README.md +16 -0
  2. package/package.json +1 -1
  3. package/public/assets/{_basePickBy-D2x9UR-Z.js → _basePickBy-CsEFpeup.js} +1 -1
  4. package/public/assets/{_baseUniq-C01jsmVS.js → _baseUniq-ZNfm73QS.js} +1 -1
  5. package/public/assets/{arc-t3uO9VFT.js → arc-89CrHVpy.js} +1 -1
  6. package/public/assets/{architectureDiagram-Q4EWVU46-DWUIuXit.js → architectureDiagram-Q4EWVU46-oOX9WpjV.js} +1 -1
  7. package/public/assets/{blockDiagram-DXYQGD6D-DnP4lNOB.js → blockDiagram-DXYQGD6D-Dv7Qlwwa.js} +1 -1
  8. package/public/assets/{c4Diagram-AHTNJAMY-B29P8b7E.js → c4Diagram-AHTNJAMY-DH6nUY9v.js} +1 -1
  9. package/public/assets/channel-1hhrLuQG.js +1 -0
  10. package/public/assets/{chunk-4BX2VUAB-BH7Ixc1K.js → chunk-4BX2VUAB-br_MLFTb.js} +1 -1
  11. package/public/assets/{chunk-4TB4RGXK-h7uQ9ZtR.js → chunk-4TB4RGXK-D6RT0VpP.js} +1 -1
  12. package/public/assets/{chunk-55IACEB6-D9ZHEhWx.js → chunk-55IACEB6-B6WHKDLC.js} +1 -1
  13. package/public/assets/{chunk-EDXVE4YY-BEKltVR7.js → chunk-EDXVE4YY-CfyEh_gx.js} +1 -1
  14. package/public/assets/{chunk-FMBD7UC4-BPkcv-bj.js → chunk-FMBD7UC4-IxUFwHkQ.js} +1 -1
  15. package/public/assets/{chunk-OYMX7WX6-C-wnBny1.js → chunk-OYMX7WX6-BuoXiEMH.js} +1 -1
  16. package/public/assets/{chunk-QZHKN3VN-DBZnU2yp.js → chunk-QZHKN3VN-gwjweP0s.js} +1 -1
  17. package/public/assets/{chunk-YZCP3GAM-C8GNavGc.js → chunk-YZCP3GAM-AxzesjLV.js} +1 -1
  18. package/public/assets/classDiagram-6PBFFD2Q-BCCxmX4-.js +1 -0
  19. package/public/assets/classDiagram-v2-HSJHXN6E-BCCxmX4-.js +1 -0
  20. package/public/assets/clone-Zlu95tC9.js +1 -0
  21. package/public/assets/{cose-bilkent-S5V4N54A-BeFh7BYc.js → cose-bilkent-S5V4N54A-DA0VKF4u.js} +1 -1
  22. package/public/assets/{dagre-KV5264BT-DlsYCBSj.js → dagre-KV5264BT-TROeRxkl.js} +1 -1
  23. package/public/assets/{diagram-5BDNPKRD-CnTlMSc9.js → diagram-5BDNPKRD-Cy0d3UWt.js} +1 -1
  24. package/public/assets/{diagram-G4DWMVQ6-CKODi7zI.js → diagram-G4DWMVQ6-DLqyiLD9.js} +1 -1
  25. package/public/assets/{diagram-MMDJMWI5-DEJGgmOX.js → diagram-MMDJMWI5-COX5ltag.js} +1 -1
  26. package/public/assets/{diagram-TYMM5635-Dju-tIVS.js → diagram-TYMM5635-jNMi-Wxw.js} +1 -1
  27. package/public/assets/{erDiagram-SMLLAGMA-CqPQSqot.js → erDiagram-SMLLAGMA-DPNWOmMF.js} +1 -1
  28. package/public/assets/{flowDiagram-DWJPFMVM-BeIRzZQp.js → flowDiagram-DWJPFMVM-CtEH78aJ.js} +1 -1
  29. package/public/assets/{ganttDiagram-T4ZO3ILL-B6BnA7VR.js → ganttDiagram-T4ZO3ILL-DnmNU9Yo.js} +1 -1
  30. package/public/assets/{gitGraphDiagram-UUTBAWPF-BoSi7fJX.js → gitGraphDiagram-UUTBAWPF-BEBkdt6e.js} +1 -1
  31. package/public/assets/{graph-uVutBrOm.js → graph-BrV-Z5FX.js} +1 -1
  32. package/public/assets/index-BBM5qWeb.js +455 -0
  33. package/public/assets/index-C2HbSpIZ.css +32 -0
  34. package/public/assets/{infoDiagram-42DDH7IO-DD-KdApo.js → infoDiagram-42DDH7IO-5B0oQBTo.js} +1 -1
  35. package/public/assets/{ishikawaDiagram-UXIWVN3A-D36iFaUH.js → ishikawaDiagram-UXIWVN3A-JvObAFoj.js} +1 -1
  36. package/public/assets/{journeyDiagram-VCZTEJTY-BMQDm-H-.js → journeyDiagram-VCZTEJTY-B4x5Q9FT.js} +1 -1
  37. package/public/assets/{kanban-definition-6JOO6SKY-D1FZXkK7.js → kanban-definition-6JOO6SKY-BJnqk1Pz.js} +1 -1
  38. package/public/assets/{layout-xVUStQT2.js → layout-CFi5eoUP.js} +1 -1
  39. package/public/assets/{linear-BTv56PNK.js → linear-BdauaB9G.js} +1 -1
  40. package/public/assets/{mindmap-definition-QFDTVHPH-CvhBJGrR.js → mindmap-definition-QFDTVHPH-B5a5e6e4.js} +1 -1
  41. package/public/assets/{pieDiagram-DEJITSTG-DcxBOIJ2.js → pieDiagram-DEJITSTG-BasykeiV.js} +1 -1
  42. package/public/assets/{quadrantDiagram-34T5L4WZ-D79TxdrP.js → quadrantDiagram-34T5L4WZ-BpIn9pue.js} +1 -1
  43. package/public/assets/{requirementDiagram-MS252O5E-gOOiR6tu.js → requirementDiagram-MS252O5E-bScQOYzc.js} +1 -1
  44. package/public/assets/{sankeyDiagram-XADWPNL6-YUncdO2g.js → sankeyDiagram-XADWPNL6-CdwsbfFQ.js} +1 -1
  45. package/public/assets/{sequenceDiagram-FGHM5R23-eoBFRqV1.js → sequenceDiagram-FGHM5R23-BLSeDdFS.js} +1 -1
  46. package/public/assets/{stateDiagram-FHFEXIEX-DeQeLuN0.js → stateDiagram-FHFEXIEX-DwQJYdc7.js} +1 -1
  47. package/public/assets/stateDiagram-v2-QKLJ7IA2-BxWrp5cC.js +1 -0
  48. package/public/assets/{timeline-definition-GMOUNBTQ-CV0p2TOx.js → timeline-definition-GMOUNBTQ-BkLAz5O2.js} +1 -1
  49. package/public/assets/{vennDiagram-DHZGUBPP-CciIt7hk.js → vennDiagram-DHZGUBPP-DiiYjk-t.js} +1 -1
  50. package/public/assets/{wardley-RL74JXVD-DpAn0g0p.js → wardley-RL74JXVD-BsijixfH.js} +1 -1
  51. package/public/assets/{wardleyDiagram-NUSXRM2D-BcEpTQV4.js → wardleyDiagram-NUSXRM2D-DZ7LhIvm.js} +1 -1
  52. package/public/assets/{xychartDiagram-5P7HB3ND-B-PklpIN.js → xychartDiagram-5P7HB3ND-CJNPkFhM.js} +1 -1
  53. package/public/index.html +2 -2
  54. package/public/sw.js +1 -1
  55. package/src/server/copilot-sdk.js +617 -0
  56. package/src/server/index.js +11 -2
  57. package/src/server/routes.js +440 -10
  58. package/src/server/sessions.js +6 -0
  59. package/src/server/websocket.js +175 -2
  60. package/public/assets/channel-Du2155FM.js +0 -1
  61. package/public/assets/classDiagram-6PBFFD2Q-Dzf6e5xB.js +0 -1
  62. package/public/assets/classDiagram-v2-HSJHXN6E-Dzf6e5xB.js +0 -1
  63. package/public/assets/clone-VT9_rs7L.js +0 -1
  64. package/public/assets/index-C0J_Dxjj.css +0 -32
  65. package/public/assets/index-NvPavSM9.js +0 -447
  66. package/public/assets/stateDiagram-v2-QKLJ7IA2-BhqrHPnX.js +0 -1
@@ -0,0 +1,617 @@
1
+ 'use strict';
2
+
3
+ // Optional dependency — not available in CI or when not installed
4
+ let CopilotClient, approveAll;
5
+ try {
6
+ ({ CopilotClient, approveAll } = require('@github/copilot-sdk'));
7
+ } catch {
8
+ // SDK not installed — CopilotService will be unavailable
9
+ }
10
+ const log = require('../utils/logger');
11
+ const path = require('path');
12
+ const fs = require('fs');
13
+ const os = require('os');
14
+
15
+ const configDir = process.env.TERMBEAM_CONFIG_DIR || path.join(os.homedir(), '.termbeam');
16
+ const AUTH_URL_FILE = path.join(configDir, 'auth-urls.log');
17
+
18
+ /**
19
+ * Create a browser handler script that writes URLs to a file
20
+ * instead of opening a browser. This lets us forward OAuth URLs to mobile clients.
21
+ * Cross-platform: generates .cmd on Windows, .sh elsewhere.
22
+ */
23
+ function ensureBrowserHandler() {
24
+ fs.mkdirSync(configDir, { recursive: true, mode: 0o700 });
25
+ const ext = process.platform === 'win32' ? '.cmd' : '.sh';
26
+ const handlerPath = path.join(configDir, `browser-handler${ext}`);
27
+ const script =
28
+ process.platform === 'win32'
29
+ ? `@echo off\r\necho %1 >> "${AUTH_URL_FILE}"\r\n`
30
+ : `#!/bin/sh\necho "$1" >> "${AUTH_URL_FILE}"\n`;
31
+ const mode = process.platform === 'win32' ? 0o700 : 0o755;
32
+ try {
33
+ fs.writeFileSync(handlerPath, script, { mode });
34
+ } catch {
35
+ // Ignore — fallback to BROWSER=echo
36
+ }
37
+ return handlerPath;
38
+ }
39
+
40
+ /**
41
+ * Read MCP server configs from the user's Copilot config directory.
42
+ * Injects BROWSER env var into each local server to intercept OAuth flows.
43
+ */
44
+ function loadMcpConfig(browserHandler) {
45
+ const configDir = path.join(os.homedir(), '.copilot');
46
+ const mcpPath = path.join(configDir, 'mcp-config.json');
47
+ try {
48
+ const raw = fs.readFileSync(mcpPath, 'utf8');
49
+ const parsed = JSON.parse(raw);
50
+ const servers = parsed.mcpServers || null;
51
+ if (!servers) return null;
52
+ // Inject BROWSER handler into each local MCP server's env
53
+ for (const [, config] of Object.entries(servers)) {
54
+ if (!config || typeof config !== 'object') continue;
55
+ if (!config.type || config.type === 'local' || config.type === 'stdio') {
56
+ config.env = { ...config.env, BROWSER: browserHandler };
57
+ }
58
+ }
59
+ return servers;
60
+ } catch {
61
+ return null;
62
+ }
63
+ }
64
+
65
+ class CopilotService {
66
+ constructor() {
67
+ this.client = null;
68
+ this.sessions = new Map(); // sessionId -> { session, listeners }
69
+ this._startPromise = null;
70
+ this.browserHandler = ensureBrowserHandler();
71
+ this.mcpServers = loadMcpConfig(this.browserHandler);
72
+ if (this.mcpServers) {
73
+ const count = Object.keys(this.mcpServers).length;
74
+ log.info(`Loaded ${count} MCP server(s) from ~/.copilot/mcp-config.json`);
75
+ }
76
+ this._authUrlOffset = 0;
77
+ this._watchAuthUrls();
78
+ }
79
+
80
+ /**
81
+ * Watch the auth URL file for new OAuth URLs written by MCP servers.
82
+ * Forward them to all active WebSocket listeners.
83
+ */
84
+ _watchAuthUrls() {
85
+ // Truncate file on startup
86
+ try {
87
+ fs.writeFileSync(AUTH_URL_FILE, '');
88
+ } catch {
89
+ /* ignore */
90
+ }
91
+
92
+ this._authPollTimer = setInterval(() => {
93
+ try {
94
+ const content = fs.readFileSync(AUTH_URL_FILE, 'utf8');
95
+ if (content.length <= this._authUrlOffset) return;
96
+ const newContent = content.slice(this._authUrlOffset);
97
+ this._authUrlOffset = content.length;
98
+ const urls = newContent.split('\n').filter((u) => u.trim().startsWith('http'));
99
+ for (const url of urls) {
100
+ try {
101
+ log.info(`Auth URL intercepted: ${new URL(url).origin}/...`);
102
+ } catch {
103
+ log.info('Auth URL intercepted');
104
+ }
105
+ const event = { type: 'copilot.auth_url', data: { url: url.trim() } };
106
+ // Forward to ALL active session listeners
107
+ for (const [, entry] of this.sessions) {
108
+ if (entry.listener) entry.listener(event);
109
+ }
110
+ }
111
+ } catch {
112
+ /* file may not exist yet */
113
+ }
114
+ }, 500);
115
+ }
116
+
117
+ async ensureClient() {
118
+ if (this.client) return this.client;
119
+ if (this._startPromise) return this._startPromise;
120
+ this._startPromise = this._doStart();
121
+ return this._startPromise;
122
+ }
123
+
124
+ async _doStart() {
125
+ try {
126
+ if (!CopilotClient) throw new Error('Copilot SDK not installed');
127
+ process.env.BROWSER = this.browserHandler;
128
+ this.client = new CopilotClient({
129
+ cwd: process.cwd(),
130
+ });
131
+ await this.client.start();
132
+ log.info('Copilot SDK client started');
133
+ return this.client;
134
+ } catch (err) {
135
+ log.error('Failed to start Copilot SDK client:', err.message);
136
+ this.client = null;
137
+ if (this._authPollTimer) {
138
+ clearInterval(this._authPollTimer);
139
+ this._authPollTimer = null;
140
+ }
141
+ throw err;
142
+ } finally {
143
+ this._startPromise = null;
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Custom permission handler that intercepts URL requests (OAuth flows)
149
+ * and forwards them to the WebSocket client instead of opening a server-side browser.
150
+ */
151
+ _createPermissionHandler(sessionId, eventBuffer) {
152
+ return (request) => {
153
+ if (request.kind === 'url' && request.url) {
154
+ log.info(`Auth URL requested for session ${sessionId}`);
155
+ const entry = this.sessions.get(sessionId);
156
+ const event = {
157
+ type: 'copilot.auth_url',
158
+ data: { url: request.url },
159
+ };
160
+ if (entry?.listener) {
161
+ entry.listener(event);
162
+ } else if (entry) {
163
+ entry.eventBuffer.push(event);
164
+ } else if (eventBuffer) {
165
+ // Buffer pre-registration events until session entry exists
166
+ eventBuffer.push(event);
167
+ }
168
+ }
169
+ return { kind: 'approved' };
170
+ };
171
+ }
172
+
173
+ async resumeSession(sdkSessionId, options = {}) {
174
+ const client = await this.ensureClient();
175
+ const sessionId = require('crypto').randomBytes(16).toString('hex');
176
+
177
+ const eventBuffer = [];
178
+
179
+ const resumeCwd = options.cwd || process.cwd();
180
+ const configDir = path.join(os.homedir(), '.copilot');
181
+
182
+ const resumeConfig = {
183
+ onPermissionRequest: this._createPermissionHandler(sessionId, eventBuffer),
184
+ streaming: true,
185
+ workingDirectory: resumeCwd,
186
+ configDir,
187
+ onUserInputRequest: async (request) => {
188
+ const event = {
189
+ type: 'copilot.user_input_request',
190
+ data: {
191
+ question: request.question,
192
+ choices: request.choices,
193
+ allowFreeform: request.allowFreeform !== false,
194
+ },
195
+ };
196
+ const entry = this.sessions.get(sessionId);
197
+ const listener = entry?.listener;
198
+ if (listener) listener(event);
199
+ else eventBuffer.push(event);
200
+
201
+ return new Promise((resolve) => {
202
+ const timeout = setTimeout(() => {
203
+ resolve({ answer: '', wasFreeform: true });
204
+ }, 120000);
205
+
206
+ const storeResolve = (answer) => {
207
+ clearTimeout(timeout);
208
+ resolve(answer);
209
+ };
210
+
211
+ if (entry) {
212
+ entry.pendingInputResolve = storeResolve;
213
+ } else {
214
+ // Store on eventBuffer so it can be transferred after session registration
215
+ eventBuffer._pendingInputResolve = storeResolve;
216
+ }
217
+ });
218
+ },
219
+ };
220
+
221
+ if (this.mcpServers) {
222
+ resumeConfig.mcpServers = this.mcpServers;
223
+ }
224
+
225
+ const session = await client.resumeSession(sdkSessionId, resumeConfig);
226
+
227
+ const unsubscribe = session.on((event) => {
228
+ try {
229
+ const mapped = this._mapEvent(event);
230
+ if (mapped) {
231
+ const entry = this.sessions.get(sessionId);
232
+ if (entry) {
233
+ if (!entry.messageHistory) entry.messageHistory = [];
234
+ entry.messageHistory.push(mapped);
235
+ }
236
+ const cb = entry?.listener;
237
+ if (cb) cb(mapped);
238
+ else eventBuffer.push(mapped);
239
+ }
240
+ } catch (err) {
241
+ log.warn(`Error processing event for session ${sessionId}: ${err.message}`);
242
+ }
243
+ });
244
+
245
+ // Get existing messages for replay
246
+ const existingMessages = await session.getMessages().catch(() => []);
247
+
248
+ const now = new Date().toISOString();
249
+ this.sessions.set(sessionId, {
250
+ session,
251
+ unsubscribe,
252
+ eventBuffer,
253
+ listener: null,
254
+ pendingInputResolve: null,
255
+ cwd: options.cwd || process.cwd(),
256
+ model: options.model || 'claude-opus-4.6',
257
+ name: options.name || 'Resumed Session',
258
+ ptySessionId: options.ptySessionId || null,
259
+ createdAt: now,
260
+ lastActivity: now,
261
+ sdkSessionId,
262
+ existingMessages,
263
+ });
264
+
265
+ // Transfer any pending input resolve stored before session registration
266
+ if (eventBuffer._pendingInputResolve) {
267
+ this.sessions.get(sessionId).pendingInputResolve = eventBuffer._pendingInputResolve;
268
+ }
269
+
270
+ log.info(`Copilot SDK session resumed: ${sessionId} (from SDK session ${sdkSessionId})`);
271
+ return sessionId;
272
+ }
273
+
274
+ async createSession(options = {}) {
275
+ const client = await this.ensureClient();
276
+ const sessionId = require('crypto').randomBytes(16).toString('hex');
277
+
278
+ const eventBuffer = []; // Buffer events until a listener connects
279
+ let listener = null; // WebSocket event callback
280
+
281
+ const sessionCwd = options.cwd || process.cwd();
282
+ const configDir = path.join(os.homedir(), '.copilot');
283
+
284
+ const sessionConfig = {
285
+ model: options.model || 'claude-opus-4.6',
286
+ streaming: true,
287
+ workingDirectory: sessionCwd,
288
+ configDir,
289
+ onPermissionRequest: this._createPermissionHandler(sessionId, eventBuffer),
290
+ onUserInputRequest: async (request) => {
291
+ // Forward to WebSocket listener for UI to handle
292
+ const event = {
293
+ type: 'copilot.user_input_request',
294
+ data: {
295
+ question: request.question,
296
+ choices: request.choices,
297
+ allowFreeform: request.allowFreeform !== false,
298
+ },
299
+ };
300
+ const entry = this.sessions.get(sessionId);
301
+ listener = entry?.listener;
302
+ if (listener) listener(event);
303
+ else eventBuffer.push(event);
304
+
305
+ // Wait for response from UI (with timeout)
306
+ return new Promise((resolve) => {
307
+ const timeout = setTimeout(() => {
308
+ resolve({ answer: '', wasFreeform: true });
309
+ }, 120000); // 2 min timeout
310
+
311
+ const storeResolve = (answer) => {
312
+ clearTimeout(timeout);
313
+ resolve(answer);
314
+ };
315
+
316
+ // Store resolver for the WebSocket handler to call
317
+ if (entry) {
318
+ entry.pendingInputResolve = storeResolve;
319
+ } else {
320
+ // Store on eventBuffer so it can be transferred after session registration
321
+ eventBuffer._pendingInputResolve = storeResolve;
322
+ }
323
+ });
324
+ },
325
+ };
326
+
327
+ // Pass user's MCP servers so all tools are available
328
+ if (this.mcpServers) {
329
+ sessionConfig.mcpServers = this.mcpServers;
330
+ }
331
+
332
+ const session = await client.createSession(sessionConfig);
333
+
334
+ // Set up event forwarding
335
+ const unsubscribe = session.on((event) => {
336
+ try {
337
+ const mapped = this._mapEvent(event);
338
+ if (mapped) {
339
+ const entry = this.sessions.get(sessionId);
340
+ // Store for replay on reconnect
341
+ if (entry) {
342
+ if (!entry.messageHistory) entry.messageHistory = [];
343
+ entry.messageHistory.push(mapped);
344
+ }
345
+ const cb = entry?.listener;
346
+ if (cb) cb(mapped);
347
+ else eventBuffer.push(mapped);
348
+ }
349
+ } catch (err) {
350
+ log.warn(`Error processing event for session ${sessionId}: ${err.message}`);
351
+ }
352
+ });
353
+
354
+ const now = new Date().toISOString();
355
+ this.sessions.set(sessionId, {
356
+ session,
357
+ unsubscribe,
358
+ eventBuffer,
359
+ listener: null,
360
+ pendingInputResolve: null,
361
+ cwd: options.cwd || process.cwd(),
362
+ model: options.model || 'claude-opus-4.6',
363
+ name: options.name || 'Copilot Session',
364
+ ptySessionId: options.ptySessionId || null,
365
+ createdAt: now,
366
+ lastActivity: now,
367
+ });
368
+
369
+ // Transfer any pending input resolve stored before session registration
370
+ if (eventBuffer._pendingInputResolve) {
371
+ this.sessions.get(sessionId).pendingInputResolve = eventBuffer._pendingInputResolve;
372
+ }
373
+
374
+ log.info(`Copilot SDK session created: ${sessionId}`);
375
+ return sessionId;
376
+ }
377
+
378
+ setListener(sessionId, callback, owner) {
379
+ const entry = this.sessions.get(sessionId);
380
+ if (!entry) return false;
381
+ if (entry.listener && callback && entry.listener !== callback) {
382
+ log.warn(
383
+ `Overwriting existing listener for session ${sessionId} — only one client supported per copilot session`,
384
+ );
385
+ }
386
+ entry.listener = callback;
387
+ entry._listenerOwner = owner || null;
388
+ // Flush buffered events (only when attaching a real listener)
389
+ if (callback) {
390
+ while (entry.eventBuffer.length > 0) {
391
+ callback(entry.eventBuffer.shift());
392
+ }
393
+ }
394
+ return true;
395
+ }
396
+
397
+ async sendMessage(sessionId, prompt) {
398
+ const entry = this.sessions.get(sessionId);
399
+ if (!entry) throw new Error('Session not found');
400
+ entry.lastActivity = new Date().toISOString();
401
+ // Store user message in history for replay on reconnect
402
+ if (!entry.messageHistory) entry.messageHistory = [];
403
+ entry.messageHistory.push({
404
+ type: 'copilot.user_message',
405
+ data: { content: prompt },
406
+ });
407
+ await entry.session.send({ prompt });
408
+ }
409
+
410
+ respondToInput(sessionId, answer) {
411
+ const entry = this.sessions.get(sessionId);
412
+ if (!entry?.pendingInputResolve) return false;
413
+ entry.pendingInputResolve({
414
+ answer: answer.text,
415
+ wasFreeform: answer.wasFreeform !== false,
416
+ });
417
+ entry.pendingInputResolve = null;
418
+ return true;
419
+ }
420
+
421
+ getMessages(sessionId) {
422
+ const entry = this.sessions.get(sessionId);
423
+ if (!entry) return [];
424
+
425
+ // If we already have messageHistory, return it (it includes replayed existing messages)
426
+ if (entry.messageHistory && entry.messageHistory.length > 0) {
427
+ return entry.messageHistory;
428
+ }
429
+
430
+ // For resumed sessions with no messageHistory yet, convert existing SDK events
431
+ if (entry.existingMessages?.length) {
432
+ const existing = [];
433
+ for (const evt of entry.existingMessages) {
434
+ const mapped = this._mapEvent(evt);
435
+ if (mapped) existing.push(mapped);
436
+ if (evt.type === 'user.message' && evt.data?.content) {
437
+ existing.push({ type: 'copilot.user_message', data: { content: evt.data.content } });
438
+ }
439
+ }
440
+ return existing;
441
+ }
442
+
443
+ return [];
444
+ }
445
+
446
+ async setModel(sessionId, model) {
447
+ const entry = this.sessions.get(sessionId);
448
+ if (!entry) throw new Error('Session not found');
449
+ await entry.session.setModel(model);
450
+ entry.model = model;
451
+ }
452
+
453
+ async abortSession(sessionId) {
454
+ const entry = this.sessions.get(sessionId);
455
+ if (!entry) return;
456
+ try {
457
+ await entry.session.abort();
458
+ } catch {
459
+ // Ignore — session may already be idle
460
+ }
461
+ }
462
+
463
+ async disconnectSession(sessionId) {
464
+ const entry = this.sessions.get(sessionId);
465
+ if (!entry) return;
466
+ // Clean up listener first to prevent leaks
467
+ if (entry.unsubscribe) {
468
+ try {
469
+ entry.unsubscribe();
470
+ } catch {
471
+ /* ignore */
472
+ }
473
+ }
474
+ // Clear any pending input timeout
475
+ if (entry.pendingInputResolve) {
476
+ entry.pendingInputResolve({ answer: '', wasFreeform: true });
477
+ }
478
+ try {
479
+ await entry.session.disconnect();
480
+ } catch (err) {
481
+ log.warn(`Error disconnecting session ${sessionId}: ${err.message}`);
482
+ }
483
+ this.sessions.delete(sessionId);
484
+ log.info(`Copilot SDK session disconnected: ${sessionId}`);
485
+ }
486
+
487
+ listSessions() {
488
+ return Array.from(this.sessions.keys());
489
+ }
490
+
491
+ async listSdkSessions() {
492
+ try {
493
+ const client = await this.ensureClient();
494
+ const sessions = await client.listSessions();
495
+ return sessions.map((s) => ({
496
+ sessionId: s.sessionId,
497
+ startTime: s.startTime,
498
+ modifiedTime: s.modifiedTime,
499
+ summary: s.summary,
500
+ isRemote: s.isRemote,
501
+ cwd: s.context?.cwd,
502
+ repository: s.context?.repository,
503
+ branch: s.context?.branch,
504
+ }));
505
+ } catch {
506
+ return [];
507
+ }
508
+ }
509
+
510
+ listSessionsDetailed() {
511
+ const result = [];
512
+ for (const [id, entry] of this.sessions) {
513
+ result.push({
514
+ id,
515
+ name: entry.name || 'Copilot Session',
516
+ cwd: entry.cwd || process.cwd(),
517
+ model: entry.model || 'claude-opus-4.6',
518
+ ptySessionId: entry.ptySessionId || null,
519
+ createdAt: entry.createdAt || new Date().toISOString(),
520
+ lastActivity: entry.lastActivity || entry.createdAt || new Date().toISOString(),
521
+ });
522
+ }
523
+ return result;
524
+ }
525
+
526
+ async shutdown() {
527
+ if (this._authPollTimer) clearInterval(this._authPollTimer);
528
+ for (const [id] of this.sessions) {
529
+ await this.disconnectSession(id);
530
+ }
531
+ if (this.client) {
532
+ await this.client.stop();
533
+ this.client = null;
534
+ }
535
+ log.info('Copilot SDK service shut down');
536
+ }
537
+
538
+ _mapEvent(event) {
539
+ // Map SDK events to our WebSocket protocol
540
+ switch (event.type) {
541
+ case 'assistant.message':
542
+ return {
543
+ type: 'copilot.assistant_message',
544
+ data: {
545
+ content: event.data.content,
546
+ toolRequests: event.data.toolRequests,
547
+ },
548
+ };
549
+ case 'assistant.message_delta':
550
+ return {
551
+ type: 'copilot.message_delta',
552
+ data: { deltaContent: event.data.deltaContent },
553
+ };
554
+ case 'assistant.reasoning':
555
+ return {
556
+ type: 'copilot.reasoning',
557
+ data: { content: event.data.content },
558
+ };
559
+ case 'assistant.reasoning_delta':
560
+ return {
561
+ type: 'copilot.reasoning_delta',
562
+ data: { deltaContent: event.data.deltaContent },
563
+ };
564
+ case 'tool.execution_start':
565
+ return {
566
+ type: 'copilot.tool_start',
567
+ data: {
568
+ toolCallId: event.data?.toolCallId,
569
+ toolName: event.data?.toolName,
570
+ input: event.data?.arguments || event.data?.input || event.data?.args || {},
571
+ },
572
+ };
573
+ case 'tool.execution_complete':
574
+ return {
575
+ type: 'copilot.tool_complete',
576
+ data: {
577
+ toolCallId: event.data?.toolCallId,
578
+ toolName: event.data?.toolName,
579
+ result: event.data?.result || event.data?.output || event.data,
580
+ duration: event.data?.duration || event.data?.durationMs,
581
+ },
582
+ };
583
+ case 'subagent.started':
584
+ return {
585
+ type: 'copilot.subagent_start',
586
+ data: {
587
+ toolCallId: event.data?.toolCallId,
588
+ agentName: event.data?.agentName,
589
+ agentDisplayName: event.data?.agentDisplayName,
590
+ },
591
+ };
592
+ case 'subagent.completed':
593
+ return {
594
+ type: 'copilot.subagent_complete',
595
+ data: {
596
+ toolCallId: event.data?.toolCallId,
597
+ agentDisplayName: event.data?.agentDisplayName,
598
+ model: event.data?.model,
599
+ totalToolCalls: event.data?.totalToolCalls,
600
+ totalTokens: event.data?.totalTokens,
601
+ durationMs: event.data?.durationMs,
602
+ },
603
+ };
604
+ case 'session.idle':
605
+ return { type: 'copilot.idle', data: {} };
606
+ case 'session.mode_changed':
607
+ return {
608
+ type: 'copilot.mode_changed',
609
+ data: { mode: event.data?.mode },
610
+ };
611
+ default:
612
+ return null; // Skip internal events
613
+ }
614
+ }
615
+ }
616
+
617
+ module.exports = { CopilotService };
@@ -105,10 +105,18 @@ function createTermBeamServer(overrides = {}) {
105
105
  const server = http.createServer(app);
106
106
  const wss = new WebSocketServer({ server, path: '/ws', maxPayload: 1 * 1024 * 1024 });
107
107
 
108
+ let copilotService = null;
109
+ try {
110
+ const { CopilotService } = require('./copilot-sdk');
111
+ if (CopilotService) copilotService = new CopilotService();
112
+ } catch {
113
+ log.info('Copilot SDK not available — agent features disabled');
114
+ }
115
+
108
116
  const state = { shareBaseUrl: null, updateInfo: null, wss, tunnelStatus: null, getLoginInfo };
109
117
  app.use('/preview', auth.middleware, createPreviewProxy());
110
- setupRoutes(app, { auth, sessions, config, state, pushManager });
111
- setupWebSocket(wss, { auth, sessions });
118
+ setupRoutes(app, { auth, sessions, config, state, pushManager, copilotService });
119
+ setupWebSocket(wss, { auth, sessions, copilotService });
112
120
 
113
121
  // --- Lifecycle ---
114
122
  let shuttingDown = false;
@@ -118,6 +126,7 @@ function createTermBeamServer(overrides = {}) {
118
126
  log.info('Shutdown initiated');
119
127
  auth.cleanup();
120
128
  sessions.shutdown();
129
+ if (copilotService) copilotService.shutdown().catch(() => {});
121
130
  cleanupUploadedFiles();
122
131
  tunnelEvents.removeAllListeners();
123
132
  cleanupTunnel();