termbeam 1.18.0 → 1.19.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/package.json +4 -1
- package/public/assets/{_basePickBy-CVn0rIeA.js → _basePickBy-CPjv_JEU.js} +1 -1
- package/public/assets/{_baseUniq-D-ViDZI1.js → _baseUniq-BtieUu_k.js} +1 -1
- package/public/assets/{arc-BWX7iih_.js → arc-Op_Jfgj1.js} +1 -1
- package/public/assets/architectureDiagram-Q4EWVU46-BPBC-sFg.js +36 -0
- package/public/assets/{blockDiagram-WCTKOSBZ-RZQh_7bp.js → blockDiagram-DXYQGD6D-bhrY5jZb.js} +6 -6
- package/public/assets/c4Diagram-AHTNJAMY-Bbj59C07.js +10 -0
- package/public/assets/channel-BqlgTU2z.js +1 -0
- package/public/assets/{chunk-4BX2VUAB-Cz0Sexay.js → chunk-4BX2VUAB-NTfL5dnQ.js} +1 -1
- package/public/assets/chunk-4TB4RGXK-DI71V5fB.js +206 -0
- package/public/assets/{chunk-55IACEB6-DhjD4VKO.js → chunk-55IACEB6-DErzjt0v.js} +1 -1
- package/public/assets/{chunk-KX2RTZJC-BU-NTTwY.js → chunk-EDXVE4YY-D_8mpmH-.js} +1 -1
- package/public/assets/{chunk-FMBD7UC4-H_1l13Rz.js → chunk-FMBD7UC4-DxKx7vX-.js} +1 -1
- package/public/assets/chunk-OYMX7WX6-DWu1iUFO.js +231 -0
- package/public/assets/{chunk-QZHKN3VN-CrSItx1v.js → chunk-QZHKN3VN-Cnf_5dAG.js} +1 -1
- package/public/assets/{chunk-JSJVCQXG-D_GdzPQn.js → chunk-YZCP3GAM-mW6tkOzW.js} +1 -1
- package/public/assets/classDiagram-6PBFFD2Q-BUz6cVs_.js +1 -0
- package/public/assets/classDiagram-v2-HSJHXN6E-BUz6cVs_.js +1 -0
- package/public/assets/clone-RMCUsgPf.js +1 -0
- package/public/assets/{cose-bilkent-S5V4N54A-BNYNzK1p.js → cose-bilkent-S5V4N54A-913I4PUt.js} +1 -1
- package/public/assets/{dagre-KLK3FWXG-CvP5MaIU.js → dagre-KV5264BT-BDwVAKyX.js} +2 -2
- package/public/assets/diagram-5BDNPKRD-OcJJ0e9j.js +10 -0
- package/public/assets/diagram-G4DWMVQ6-Dm20tH1B.js +24 -0
- package/public/assets/diagram-MMDJMWI5-D-cm46Bv.js +43 -0
- package/public/assets/{diagram-P4PSJMXO-D7EqFseu.js → diagram-TYMM5635-CbPn6K5D.js} +1 -1
- package/public/assets/erDiagram-SMLLAGMA-4AUfKRLi.js +85 -0
- package/public/assets/flowDiagram-DWJPFMVM-BJ4qWDVH.js +162 -0
- package/public/assets/{ganttDiagram-A5KZAMGK-C55hiPqb.js → ganttDiagram-T4ZO3ILL-_q7m5Uo2.js} +4 -4
- package/public/assets/gitGraphDiagram-UUTBAWPF-Ctcn_coL.js +106 -0
- package/public/assets/{graph-mq5DkKwp.js → graph-mh6uBCte.js} +1 -1
- package/public/assets/index-2RolNYEW.css +32 -0
- package/public/assets/index-CuJ2Kdhb.js +447 -0
- package/public/assets/{infoDiagram-LFFYTUFH-Cisyfr_w.js → infoDiagram-42DDH7IO-Cbl2ZkB4.js} +1 -1
- package/public/assets/{ishikawaDiagram-PHBUUO56-BjaKmPoV.js → ishikawaDiagram-UXIWVN3A-Buq4uztC.js} +2 -2
- package/public/assets/journeyDiagram-VCZTEJTY-CLs6SdFG.js +139 -0
- package/public/assets/{kanban-definition-K7BYSVSG-D9TVNfL2.js → kanban-definition-6JOO6SKY-CflX5gNH.js} +8 -8
- package/public/assets/{layout-DIfi_3Cg.js → layout-e9E0SnY2.js} +1 -1
- package/public/assets/{linear-CNFortHj.js → linear-Bue4Zf_D.js} +1 -1
- package/public/assets/mindmap-definition-QFDTVHPH-B8iZj3K3.js +96 -0
- package/public/assets/pieDiagram-DEJITSTG-B5dciSBP.js +30 -0
- package/public/assets/{quadrantDiagram-337W2JSQ-Dcj0avLR.js → quadrantDiagram-34T5L4WZ-CZaQCVxe.js} +1 -1
- package/public/assets/requirementDiagram-MS252O5E-2wYKQi_y.js +84 -0
- package/public/assets/{sankeyDiagram-WA2Y5GQK-CpT1bDmZ.js → sankeyDiagram-XADWPNL6-CX5Cdm9d.js} +1 -1
- package/public/assets/sequenceDiagram-FGHM5R23-BUWxC38t.js +157 -0
- package/public/assets/stateDiagram-FHFEXIEX-85EGIrQk.js +1 -0
- package/public/assets/stateDiagram-v2-QKLJ7IA2-OKhwT9tv.js +1 -0
- package/public/assets/timeline-definition-GMOUNBTQ-C3BtHzdG.js +120 -0
- package/public/assets/{vennDiagram-LZ73GAT5-Ci8AKqD1.js → vennDiagram-DHZGUBPP-Chw5is4z.js} +5 -5
- package/public/assets/wardley-RL74JXVD-FEtUMcEE.js +162 -0
- package/public/assets/wardleyDiagram-NUSXRM2D-C_sFSy0G.js +20 -0
- package/public/assets/xychartDiagram-5P7HB3ND-C9Qs21NZ.js +7 -0
- package/public/index.html +2 -2
- package/public/sw.js +2 -2
- package/src/server/index.js +5 -49
- package/src/server/routes.js +59 -72
- package/src/server/sessions.js +20 -0
- package/src/tunnel/index.js +4 -6
- package/src/utils/agent-sessions.js +210 -0
- package/src/utils/agents.js +116 -0
- package/src/utils/update-check.js +19 -0
- package/public/assets/architectureDiagram-2XIMDMQ5-Dr3bqfT2.js +0 -36
- package/public/assets/c4Diagram-IC4MRINW-BM1jg1mo.js +0 -10
- package/public/assets/channel-N7LACYfb.js +0 -1
- package/public/assets/chunk-NQ4KR5QH-_X3IQmYu.js +0 -220
- package/public/assets/chunk-WL4C6EOR-XtpH4KKr.js +0 -189
- package/public/assets/classDiagram-VBA2DB6C-Ct_F3N6M.js +0 -1
- package/public/assets/classDiagram-v2-RAHNMMFH-Ct_F3N6M.js +0 -1
- package/public/assets/clone-DfaqXUXL.js +0 -1
- package/public/assets/diagram-E7M64L7V-C1ypW2kT.js +0 -24
- package/public/assets/diagram-IFDJBPK2-CH5mcu6V.js +0 -43
- package/public/assets/erDiagram-INFDFZHY-Dc_91GQC.js +0 -70
- package/public/assets/flowDiagram-PKNHOUZH-wUw5Mjvb.js +0 -162
- package/public/assets/gitGraphDiagram-K3NZZRJ6-D3HBxR11.js +0 -65
- package/public/assets/index-BxVq7AYs.js +0 -394
- package/public/assets/index-Cpm34cTy.css +0 -32
- package/public/assets/journeyDiagram-4ABVD52K-BNP3C6Ph.js +0 -139
- package/public/assets/mindmap-definition-YRQLILUH-CfNe7W8X.js +0 -68
- package/public/assets/pieDiagram-SKSYHLDU-C3T1FeFD.js +0 -30
- package/public/assets/requirementDiagram-Z7DCOOCP-D4yTZU0p.js +0 -73
- package/public/assets/sequenceDiagram-2WXFIKYE-CPsVjHHb.js +0 -145
- package/public/assets/stateDiagram-RAJIS63D-CbB0QqSa.js +0 -1
- package/public/assets/stateDiagram-v2-FVOUBMTO-CQnYvVz1.js +0 -1
- package/public/assets/timeline-definition-YZTLITO2-Bi12ioU0.js +0 -61
- package/public/assets/treemap-KZPCXAKY-Ct-pligZ.js +0 -162
- package/public/assets/xychartDiagram-JWTSCODW-BsOGuDAL.js +0 -7
package/src/server/routes.js
CHANGED
|
@@ -4,6 +4,8 @@ const fs = require('fs');
|
|
|
4
4
|
const crypto = require('crypto');
|
|
5
5
|
const express = require('express');
|
|
6
6
|
const { detectShells } = require('../utils/shells');
|
|
7
|
+
const { getAvailableAgents } = require('../utils/agents');
|
|
8
|
+
const { getAgentSessions, getResumeCommand } = require('../utils/agent-sessions');
|
|
7
9
|
const log = require('../utils/logger');
|
|
8
10
|
const rateLimit = require('express-rate-limit');
|
|
9
11
|
|
|
@@ -362,6 +364,44 @@ function setupRoutes(app, { auth, sessions, config, state, pushManager }) {
|
|
|
362
364
|
res.json({ shells, default: match ? match.cmd : ds, cwd: config.cwd });
|
|
363
365
|
});
|
|
364
366
|
|
|
367
|
+
// Available AI agents
|
|
368
|
+
app.get('/api/agents', apiRateLimit, auth.middleware, async (_req, res) => {
|
|
369
|
+
try {
|
|
370
|
+
const agents = await getAvailableAgents();
|
|
371
|
+
res.json({ agents });
|
|
372
|
+
} catch (err) {
|
|
373
|
+
log.warn(`Agent detection failed: ${err.message}`);
|
|
374
|
+
res.json({ agents: [] });
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
// Agent session history (for resume)
|
|
379
|
+
app.get('/api/agent-sessions', apiRateLimit, auth.middleware, async (req, res) => {
|
|
380
|
+
try {
|
|
381
|
+
const limit = Math.min(Math.max(parseInt(req.query.limit, 10) || 100, 1), 500);
|
|
382
|
+
const agent = req.query.agent || null;
|
|
383
|
+
const search = req.query.search || null;
|
|
384
|
+
const sessions = await getAgentSessions({ limit, agent, search });
|
|
385
|
+
res.json({ sessions });
|
|
386
|
+
} catch (err) {
|
|
387
|
+
log.warn(`Failed to read agent sessions: ${err.message}`);
|
|
388
|
+
res.json({ sessions: [] });
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
// Get resume command for a specific session
|
|
393
|
+
app.get(
|
|
394
|
+
'/api/agent-sessions/:agent/:id/resume-command',
|
|
395
|
+
apiRateLimit,
|
|
396
|
+
auth.middleware,
|
|
397
|
+
(req, res) => {
|
|
398
|
+
const { agent, id } = req.params;
|
|
399
|
+
const command = getResumeCommand({ agent, id });
|
|
400
|
+
if (!command) return res.status(400).json({ error: 'Unknown agent' });
|
|
401
|
+
res.json({ command });
|
|
402
|
+
},
|
|
403
|
+
);
|
|
404
|
+
|
|
365
405
|
app.get('/api/sessions/:id/detect-port', auth.middleware, (req, res) => {
|
|
366
406
|
log.debug(`Port detection requested for session ${req.params.id}`);
|
|
367
407
|
const session = sessions.get(req.params.id);
|
|
@@ -595,7 +635,7 @@ function setupRoutes(app, { auth, sessions, config, state, pushManager }) {
|
|
|
595
635
|
return res.status(400).json({ error: 'Invalid dir parameter' });
|
|
596
636
|
}
|
|
597
637
|
|
|
598
|
-
const rootDir = path.resolve(
|
|
638
|
+
const rootDir = path.resolve(sessions.getSessionCwd(req.params.id));
|
|
599
639
|
const dir = path.resolve(rootDir, req.query.dir || '.');
|
|
600
640
|
|
|
601
641
|
const MAX_ENTRIES = 1000;
|
|
@@ -666,7 +706,7 @@ function setupRoutes(app, { auth, sessions, config, state, pushManager }) {
|
|
|
666
706
|
depth = parsedDepth;
|
|
667
707
|
}
|
|
668
708
|
depth = Math.min(Math.max(depth, 1), MAX_DEPTH);
|
|
669
|
-
const rootDir = path.resolve(
|
|
709
|
+
const rootDir = path.resolve(sessions.getSessionCwd(req.params.id));
|
|
670
710
|
let totalEntries = 0;
|
|
671
711
|
|
|
672
712
|
function buildTree(dir, currentDepth) {
|
|
@@ -749,7 +789,7 @@ function setupRoutes(app, { auth, sessions, config, state, pushManager }) {
|
|
|
749
789
|
return res.status(400).json({ error: 'Missing file parameter' });
|
|
750
790
|
}
|
|
751
791
|
|
|
752
|
-
const rootDir = path.resolve(
|
|
792
|
+
const rootDir = path.resolve(sessions.getSessionCwd(req.params.id));
|
|
753
793
|
const filePath = path.resolve(rootDir, file);
|
|
754
794
|
|
|
755
795
|
try {
|
|
@@ -780,7 +820,7 @@ function setupRoutes(app, { auth, sessions, config, state, pushManager }) {
|
|
|
780
820
|
return res.status(400).json({ error: 'Missing file parameter' });
|
|
781
821
|
}
|
|
782
822
|
|
|
783
|
-
const rootDir = path.resolve(
|
|
823
|
+
const rootDir = path.resolve(sessions.getSessionCwd(req.params.id));
|
|
784
824
|
const filePath = path.resolve(rootDir, file);
|
|
785
825
|
|
|
786
826
|
try {
|
|
@@ -811,7 +851,7 @@ function setupRoutes(app, { auth, sessions, config, state, pushManager }) {
|
|
|
811
851
|
return res.status(400).json({ error: 'Missing file parameter' });
|
|
812
852
|
}
|
|
813
853
|
|
|
814
|
-
const rootDir = path.resolve(
|
|
854
|
+
const rootDir = path.resolve(sessions.getSessionCwd(req.params.id));
|
|
815
855
|
const filePath = path.resolve(rootDir, file);
|
|
816
856
|
|
|
817
857
|
try {
|
|
@@ -849,7 +889,7 @@ function setupRoutes(app, { auth, sessions, config, state, pushManager }) {
|
|
|
849
889
|
if (!session) return res.status(404).json({ error: 'Session not found' });
|
|
850
890
|
|
|
851
891
|
try {
|
|
852
|
-
const status = await getDetailedStatus(
|
|
892
|
+
const status = await getDetailedStatus(sessions.getSessionCwd(req.params.id));
|
|
853
893
|
res.json(status);
|
|
854
894
|
} catch (err) {
|
|
855
895
|
log.warn(`Git status failed: ${err.message}`);
|
|
@@ -876,7 +916,11 @@ function setupRoutes(app, { auth, sessions, config, state, pushManager }) {
|
|
|
876
916
|
}
|
|
877
917
|
}
|
|
878
918
|
try {
|
|
879
|
-
const diff = await getFileDiff(
|
|
919
|
+
const diff = await getFileDiff(sessions.getSessionCwd(req.params.id), file, {
|
|
920
|
+
staged,
|
|
921
|
+
untracked,
|
|
922
|
+
context,
|
|
923
|
+
});
|
|
880
924
|
res.json(diff);
|
|
881
925
|
} catch (err) {
|
|
882
926
|
log.warn(`Git diff failed: ${err.message}`);
|
|
@@ -894,7 +938,7 @@ function setupRoutes(app, { auth, sessions, config, state, pushManager }) {
|
|
|
894
938
|
}
|
|
895
939
|
|
|
896
940
|
try {
|
|
897
|
-
const blame = await getFileBlame(
|
|
941
|
+
const blame = await getFileBlame(sessions.getSessionCwd(req.params.id), file);
|
|
898
942
|
res.json(blame);
|
|
899
943
|
} catch (err) {
|
|
900
944
|
log.warn(`Git blame failed: ${err.message}`);
|
|
@@ -913,7 +957,10 @@ function setupRoutes(app, { auth, sessions, config, state, pushManager }) {
|
|
|
913
957
|
}
|
|
914
958
|
|
|
915
959
|
try {
|
|
916
|
-
const logResult = await getGitLog(
|
|
960
|
+
const logResult = await getGitLog(sessions.getSessionCwd(req.params.id), {
|
|
961
|
+
limit,
|
|
962
|
+
file: file || null,
|
|
963
|
+
});
|
|
917
964
|
res.json(logResult);
|
|
918
965
|
} catch (err) {
|
|
919
966
|
log.warn(`Git log failed: ${err.message}`);
|
|
@@ -991,69 +1038,9 @@ function setupRoutes(app, { auth, sessions, config, state, pushManager }) {
|
|
|
991
1038
|
});
|
|
992
1039
|
});
|
|
993
1040
|
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
const cmd = findDevtunnel() || 'devtunnel';
|
|
998
|
-
const proc = spawn(cmd, ['user', 'login', '-d'], {
|
|
999
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1000
|
-
});
|
|
1001
|
-
|
|
1002
|
-
let output = '';
|
|
1003
|
-
let responded = false;
|
|
1004
|
-
|
|
1005
|
-
const timeout = setTimeout(() => {
|
|
1006
|
-
if (!responded) {
|
|
1007
|
-
responded = true;
|
|
1008
|
-
proc.kill();
|
|
1009
|
-
res.status(504).json({ error: 'Timed out waiting for device code' });
|
|
1010
|
-
}
|
|
1011
|
-
}, 15000);
|
|
1012
|
-
|
|
1013
|
-
function tryParse(data) {
|
|
1014
|
-
if (responded || output.length > 10_000) return;
|
|
1015
|
-
output += data;
|
|
1016
|
-
// Entra: "open the page https://... and enter the code ABC123 to authenticate"
|
|
1017
|
-
// GitHub: "Browse to https://... and enter the code: AB12-CD34"
|
|
1018
|
-
const match =
|
|
1019
|
-
output.match(/open the page (https:\/\/[^\s]+) and enter the code ([A-Z0-9]+)/i) ||
|
|
1020
|
-
output.match(/Browse to (https:\/\/[^\s]+) and enter the code:?\s*([A-Z0-9-]+)/i);
|
|
1021
|
-
if (match) {
|
|
1022
|
-
responded = true;
|
|
1023
|
-
clearTimeout(timeout);
|
|
1024
|
-
// Stop reading output — we have what we need
|
|
1025
|
-
proc.stdout.removeAllListeners('data');
|
|
1026
|
-
proc.stderr.removeAllListeners('data');
|
|
1027
|
-
res.json({
|
|
1028
|
-
url: match[1],
|
|
1029
|
-
code: match[2],
|
|
1030
|
-
});
|
|
1031
|
-
}
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
proc.stdout.on('data', (d) => tryParse(d.toString()));
|
|
1035
|
-
proc.stderr.on('data', (d) => tryParse(d.toString()));
|
|
1036
|
-
|
|
1037
|
-
proc.on('close', (code) => {
|
|
1038
|
-
clearTimeout(timeout);
|
|
1039
|
-
if (!responded) {
|
|
1040
|
-
responded = true;
|
|
1041
|
-
if (code === 0) {
|
|
1042
|
-
res.json({ ok: true, message: 'Already authenticated' });
|
|
1043
|
-
} else {
|
|
1044
|
-
res.status(500).json({ error: 'DevTunnel login failed' });
|
|
1045
|
-
}
|
|
1046
|
-
}
|
|
1047
|
-
});
|
|
1048
|
-
|
|
1049
|
-
proc.on('error', (err) => {
|
|
1050
|
-
clearTimeout(timeout);
|
|
1051
|
-
if (!responded) {
|
|
1052
|
-
responded = true;
|
|
1053
|
-
res.status(500).json({ error: err.message });
|
|
1054
|
-
}
|
|
1055
|
-
});
|
|
1056
|
-
});
|
|
1041
|
+
// Tunnel renew endpoint removed — DevTunnel CLI auto-refreshes OAuth
|
|
1042
|
+
// tokens. If auth truly expires, user must run "devtunnel user login" on
|
|
1043
|
+
// the host machine; the watchdog auto-reconnects after re-auth.
|
|
1057
1044
|
}
|
|
1058
1045
|
|
|
1059
1046
|
function cleanupUploadedFiles() {
|
package/src/server/sessions.js
CHANGED
|
@@ -407,10 +407,30 @@ class SessionManager {
|
|
|
407
407
|
return true;
|
|
408
408
|
}
|
|
409
409
|
|
|
410
|
+
/** Get the live CWD for a session (detected from PTY process). */
|
|
411
|
+
getSessionCwd(id) {
|
|
412
|
+
const s = this.sessions.get(id);
|
|
413
|
+
if (!s) return null;
|
|
414
|
+
const cached = _gitCache.get(id);
|
|
415
|
+
if (cached && cached.cwd) {
|
|
416
|
+
// Update session.cwd so endpoints using session.cwd directly get fresh values
|
|
417
|
+
s.cwd = cached.cwd;
|
|
418
|
+
return cached.cwd;
|
|
419
|
+
}
|
|
420
|
+
// Trigger a refresh for next time
|
|
421
|
+
scheduleGitRefresh(id, s.pty.pid, s.cwd);
|
|
422
|
+
return s.cwd;
|
|
423
|
+
}
|
|
424
|
+
|
|
410
425
|
list() {
|
|
411
426
|
const list = [];
|
|
412
427
|
for (const [id, s] of this.sessions) {
|
|
413
428
|
const { cwd, git } = getCachedGitInfo(id, s.pty.pid, s.cwd);
|
|
429
|
+
// Keep session.cwd in sync with detected live CWD
|
|
430
|
+
if (cwd !== s.cwd) {
|
|
431
|
+
log.debug(`Session ${id} CWD changed: ${s.cwd} → ${cwd}`);
|
|
432
|
+
s.cwd = cwd;
|
|
433
|
+
}
|
|
414
434
|
list.push({
|
|
415
435
|
id,
|
|
416
436
|
name: s.name,
|
package/src/tunnel/index.js
CHANGED
|
@@ -293,13 +293,11 @@ function checkTokenExpiry() {
|
|
|
293
293
|
if (remaining <= TOKEN_EXPIRY_WARN_SECONDS && !expiryWarned) {
|
|
294
294
|
expiryWarned = true;
|
|
295
295
|
const minutes = Math.round(remaining / 60);
|
|
296
|
-
log.warn(`DevTunnel token expires in ${minutes}m`);
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
});
|
|
296
|
+
log.warn(`DevTunnel token expires in ${minutes}m (will auto-refresh)`);
|
|
297
|
+
} else if (remaining > TOKEN_EXPIRY_WARN_SECONDS && expiryWarned) {
|
|
298
|
+
expiryWarned = false;
|
|
299
|
+
log.info('DevTunnel token was auto-refreshed');
|
|
301
300
|
} else if (remaining > TOKEN_EXPIRY_WARN_SECONDS) {
|
|
302
|
-
// Reset the warning flag when token is refreshed
|
|
303
301
|
expiryWarned = false;
|
|
304
302
|
}
|
|
305
303
|
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const log = require('./logger');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Read Copilot sessions from SQLite store.
|
|
8
|
+
* Returns array of { id, agent, summary, cwd, repo, branch, updatedAt, turnCount }
|
|
9
|
+
*/
|
|
10
|
+
function readCopilotSessions(limit = 50) {
|
|
11
|
+
let Database;
|
|
12
|
+
try {
|
|
13
|
+
Database = require('better-sqlite3');
|
|
14
|
+
} catch {
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
17
|
+
const dbPath = path.join(os.homedir(), '.copilot', 'session-store.db');
|
|
18
|
+
if (!fs.existsSync(dbPath)) return [];
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const db = new Database(dbPath, { readonly: true, fileMustExist: true });
|
|
22
|
+
const sessions = db
|
|
23
|
+
.prepare(
|
|
24
|
+
`
|
|
25
|
+
SELECT s.id, s.summary, s.cwd, s.repository, s.branch, s.updated_at,
|
|
26
|
+
(SELECT COUNT(*) FROM turns t WHERE t.session_id = s.id) as turn_count,
|
|
27
|
+
(SELECT substr(t.user_message, 1, 200) FROM turns t WHERE t.session_id = s.id ORDER BY t.turn_index ASC LIMIT 1) as first_msg
|
|
28
|
+
FROM sessions s
|
|
29
|
+
ORDER BY s.updated_at DESC
|
|
30
|
+
LIMIT ?
|
|
31
|
+
`,
|
|
32
|
+
)
|
|
33
|
+
.all(limit);
|
|
34
|
+
db.close();
|
|
35
|
+
|
|
36
|
+
return sessions
|
|
37
|
+
.filter((s) => s.turn_count > 0)
|
|
38
|
+
.map((s) => ({
|
|
39
|
+
id: s.id,
|
|
40
|
+
agent: 'copilot',
|
|
41
|
+
agentName: 'GitHub Copilot',
|
|
42
|
+
agentIcon: 'copilot',
|
|
43
|
+
summary: s.summary || s.first_msg || null,
|
|
44
|
+
cwd: s.cwd || null,
|
|
45
|
+
repo: s.repository || null,
|
|
46
|
+
branch: s.branch || null,
|
|
47
|
+
updatedAt: s.updated_at || null,
|
|
48
|
+
turnCount: s.turn_count || 0,
|
|
49
|
+
}));
|
|
50
|
+
} catch (err) {
|
|
51
|
+
log.warn(`Failed to read Copilot sessions: ${err.message}`);
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Read Claude Code sessions from JSONL files.
|
|
58
|
+
* Returns array of unified session objects.
|
|
59
|
+
*/
|
|
60
|
+
function readClaudeSessions(limit = 50) {
|
|
61
|
+
const baseDir = path.join(os.homedir(), '.claude', 'projects');
|
|
62
|
+
if (!fs.existsSync(baseDir)) return [];
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const sessions = [];
|
|
66
|
+
const projectDirs = fs.readdirSync(baseDir);
|
|
67
|
+
|
|
68
|
+
for (const projDir of projectDirs) {
|
|
69
|
+
const fullProjDir = path.join(baseDir, projDir);
|
|
70
|
+
if (!fs.statSync(fullProjDir).isDirectory()) continue;
|
|
71
|
+
|
|
72
|
+
// Decode CWD from directory name: -Users-foo-bar → /Users/foo/bar
|
|
73
|
+
const cwd = projDir.replace(/^-/, '/').replace(/-/g, '/');
|
|
74
|
+
|
|
75
|
+
const jsonlFiles = fs
|
|
76
|
+
.readdirSync(fullProjDir)
|
|
77
|
+
.filter((f) => f.endsWith('.jsonl'))
|
|
78
|
+
.map((f) => {
|
|
79
|
+
const fullPath = path.join(fullProjDir, f);
|
|
80
|
+
const stat = fs.statSync(fullPath);
|
|
81
|
+
return { file: f, path: fullPath, mtime: stat.mtime, size: stat.size };
|
|
82
|
+
})
|
|
83
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
84
|
+
|
|
85
|
+
for (const fileInfo of jsonlFiles.slice(0, 10)) {
|
|
86
|
+
try {
|
|
87
|
+
const sessionId = path.basename(fileInfo.file, '.jsonl');
|
|
88
|
+
|
|
89
|
+
// Read file and split into lines
|
|
90
|
+
const content = fs.readFileSync(fileInfo.path, 'utf8');
|
|
91
|
+
const rawLines = content.split('\n');
|
|
92
|
+
|
|
93
|
+
let cwdFromFile = cwd;
|
|
94
|
+
let branch = null;
|
|
95
|
+
let userTurnCount = 0;
|
|
96
|
+
let firstUserMsg = null;
|
|
97
|
+
|
|
98
|
+
for (const line of rawLines) {
|
|
99
|
+
if (!line.trim()) continue;
|
|
100
|
+
try {
|
|
101
|
+
const entry = JSON.parse(line);
|
|
102
|
+
if (!branch && entry.gitBranch) branch = entry.gitBranch;
|
|
103
|
+
if (entry.cwd) cwdFromFile = entry.cwd;
|
|
104
|
+
if (entry.type === 'user') {
|
|
105
|
+
userTurnCount++;
|
|
106
|
+
if (!firstUserMsg) {
|
|
107
|
+
// Claude stores user message at entry.message.content (not entry.data)
|
|
108
|
+
const msg = entry.message;
|
|
109
|
+
if (msg && typeof msg === 'object') {
|
|
110
|
+
const content = msg.content;
|
|
111
|
+
if (typeof content === 'string') {
|
|
112
|
+
// Skip meta/command messages (XML-tagged system entries)
|
|
113
|
+
if (!content.startsWith('<') && content.trim().length > 5) {
|
|
114
|
+
firstUserMsg = content.slice(0, 200);
|
|
115
|
+
}
|
|
116
|
+
} else if (Array.isArray(content)) {
|
|
117
|
+
for (const item of content) {
|
|
118
|
+
if (item && item.type === 'text' && typeof item.text === 'string') {
|
|
119
|
+
if (!item.text.startsWith('<') && item.text.trim().length > 5) {
|
|
120
|
+
firstUserMsg = item.text.slice(0, 200);
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
} catch {
|
|
130
|
+
// skip malformed line
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Skip empty sessions (no user interaction)
|
|
135
|
+
if (userTurnCount === 0) continue;
|
|
136
|
+
|
|
137
|
+
sessions.push({
|
|
138
|
+
id: sessionId,
|
|
139
|
+
agent: 'claude',
|
|
140
|
+
agentName: 'Claude Code',
|
|
141
|
+
agentIcon: 'claude',
|
|
142
|
+
summary: firstUserMsg || null,
|
|
143
|
+
cwd: cwdFromFile,
|
|
144
|
+
repo: null,
|
|
145
|
+
branch,
|
|
146
|
+
updatedAt: fileInfo.mtime.toISOString(),
|
|
147
|
+
turnCount: userTurnCount,
|
|
148
|
+
});
|
|
149
|
+
} catch (err) {
|
|
150
|
+
log.debug(`Failed to parse Claude session ${fileInfo.file}: ${err.message}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Sort by updated time descending
|
|
156
|
+
sessions.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt));
|
|
157
|
+
return sessions.slice(0, limit);
|
|
158
|
+
} catch (err) {
|
|
159
|
+
log.warn(`Failed to read Claude sessions: ${err.message}`);
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Get all agent sessions from all sources, unified and sorted.
|
|
166
|
+
*/
|
|
167
|
+
async function getAgentSessions({ limit = 100, agent = null, search = null } = {}) {
|
|
168
|
+
const results = [];
|
|
169
|
+
|
|
170
|
+
if (!agent || agent === 'copilot') {
|
|
171
|
+
results.push(...readCopilotSessions(limit));
|
|
172
|
+
}
|
|
173
|
+
if (!agent || agent === 'claude') {
|
|
174
|
+
results.push(...readClaudeSessions(limit));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Sort all by updatedAt descending
|
|
178
|
+
results.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt));
|
|
179
|
+
|
|
180
|
+
// Apply search filter (case-insensitive substring match)
|
|
181
|
+
if (search) {
|
|
182
|
+
const q = search.toLowerCase();
|
|
183
|
+
const filtered = results.filter(
|
|
184
|
+
(s) =>
|
|
185
|
+
(s.summary && s.summary.toLowerCase().includes(q)) ||
|
|
186
|
+
(s.cwd && s.cwd.toLowerCase().includes(q)) ||
|
|
187
|
+
(s.repo && s.repo.toLowerCase().includes(q)) ||
|
|
188
|
+
(s.branch && s.branch.toLowerCase().includes(q)),
|
|
189
|
+
);
|
|
190
|
+
return filtered.slice(0, limit);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return results.slice(0, limit);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Build the resume command for a given agent session.
|
|
198
|
+
*/
|
|
199
|
+
function getResumeCommand(session) {
|
|
200
|
+
switch (session.agent) {
|
|
201
|
+
case 'copilot':
|
|
202
|
+
return `copilot --resume=${session.id}`;
|
|
203
|
+
case 'claude':
|
|
204
|
+
return `claude --resume ${session.id}`;
|
|
205
|
+
default:
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
module.exports = { getAgentSessions, getResumeCommand, readCopilotSessions, readClaudeSessions };
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
const child_process = require('child_process');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const log = require('./logger');
|
|
4
|
+
|
|
5
|
+
const KNOWN_AGENTS = [
|
|
6
|
+
{
|
|
7
|
+
id: 'copilot',
|
|
8
|
+
name: 'GitHub Copilot',
|
|
9
|
+
cmd: 'copilot',
|
|
10
|
+
icon: 'copilot',
|
|
11
|
+
detect: ['copilot', ['--version']],
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
id: 'gh-copilot',
|
|
15
|
+
name: 'GitHub Copilot (gh)',
|
|
16
|
+
cmd: 'gh',
|
|
17
|
+
args: ['copilot'],
|
|
18
|
+
icon: 'copilot',
|
|
19
|
+
detect: ['gh', ['copilot', '--version']],
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
id: 'claude',
|
|
23
|
+
name: 'Claude Code',
|
|
24
|
+
cmd: 'claude',
|
|
25
|
+
icon: 'claude',
|
|
26
|
+
detect: ['claude', ['--version']],
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: 'aider',
|
|
30
|
+
name: 'Aider',
|
|
31
|
+
cmd: 'aider',
|
|
32
|
+
icon: 'aider',
|
|
33
|
+
detect: ['aider', ['--version']],
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: 'codex',
|
|
37
|
+
name: 'Codex CLI',
|
|
38
|
+
cmd: 'codex',
|
|
39
|
+
icon: 'codex',
|
|
40
|
+
detect: ['codex', ['--version']],
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
let cachedAgents = null;
|
|
45
|
+
let cacheTime = 0;
|
|
46
|
+
const CACHE_TTL = 60_000; // 60 seconds
|
|
47
|
+
|
|
48
|
+
function tryDetectAgent(agent) {
|
|
49
|
+
const [cmd, args] = agent.detect;
|
|
50
|
+
const isWindows = os.platform() === 'win32';
|
|
51
|
+
const candidates = isWindows ? [cmd, `${cmd}.cmd`, `${cmd}.exe`] : [cmd];
|
|
52
|
+
|
|
53
|
+
return new Promise((resolve) => {
|
|
54
|
+
let resolved = false;
|
|
55
|
+
let remaining = candidates.length;
|
|
56
|
+
|
|
57
|
+
for (const bin of candidates) {
|
|
58
|
+
child_process.execFile(
|
|
59
|
+
bin,
|
|
60
|
+
args,
|
|
61
|
+
{ timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'], encoding: 'utf8' },
|
|
62
|
+
(err, stdout) => {
|
|
63
|
+
remaining--;
|
|
64
|
+
if (resolved) return;
|
|
65
|
+
if (!err) {
|
|
66
|
+
resolved = true;
|
|
67
|
+
const version = (stdout || '').trim().split('\n')[0] || 'unknown';
|
|
68
|
+
resolve({
|
|
69
|
+
id: agent.id,
|
|
70
|
+
name: agent.name,
|
|
71
|
+
cmd: agent.cmd,
|
|
72
|
+
args: agent.args || [],
|
|
73
|
+
icon: agent.icon,
|
|
74
|
+
version,
|
|
75
|
+
});
|
|
76
|
+
} else if (remaining === 0) {
|
|
77
|
+
resolve(null);
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function detectAgents() {
|
|
86
|
+
log.debug('Detecting available AI agents...');
|
|
87
|
+
const results = await Promise.allSettled(KNOWN_AGENTS.map(tryDetectAgent));
|
|
88
|
+
|
|
89
|
+
const agents = [];
|
|
90
|
+
for (const result of results) {
|
|
91
|
+
if (result.status === 'fulfilled' && result.value) {
|
|
92
|
+
agents.push(result.value);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Deduplicate: prefer standalone copilot over gh copilot
|
|
97
|
+
const hasCopilot = agents.some((a) => a.id === 'copilot');
|
|
98
|
+
const deduped = hasCopilot ? agents.filter((a) => a.id !== 'gh-copilot') : agents;
|
|
99
|
+
|
|
100
|
+
log.debug(
|
|
101
|
+
`Detected ${deduped.length} AI agent(s): ${deduped.map((a) => a.name).join(', ') || 'none'}`,
|
|
102
|
+
);
|
|
103
|
+
return deduped;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function getAvailableAgents() {
|
|
107
|
+
const now = Date.now();
|
|
108
|
+
if (cachedAgents && now - cacheTime < CACHE_TTL) {
|
|
109
|
+
return cachedAgents;
|
|
110
|
+
}
|
|
111
|
+
cachedAgents = await detectAgents();
|
|
112
|
+
cacheTime = Date.now();
|
|
113
|
+
return cachedAgents;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
module.exports = { detectAgents, getAvailableAgents, KNOWN_AGENTS };
|
|
@@ -78,6 +78,16 @@ function normalizeVersion(version) {
|
|
|
78
78
|
* Returns false if either version cannot be parsed.
|
|
79
79
|
*/
|
|
80
80
|
function isNewerVersion(current, latest) {
|
|
81
|
+
// Dev builds (e.g. 1.18.1-dev+dirty) of the same base version are running
|
|
82
|
+
// from source — never prompt to "update" to the same stable release.
|
|
83
|
+
if (isDevBuild(current)) {
|
|
84
|
+
const cur = normalizeVersion(current);
|
|
85
|
+
const lat = normalizeVersion(latest);
|
|
86
|
+
if (!cur || !lat) return false;
|
|
87
|
+
const sameBase = cur[0] === lat[0] && cur[1] === lat[1] && cur[2] === lat[2];
|
|
88
|
+
if (sameBase) return false;
|
|
89
|
+
// Different base version — fall through to normal comparison
|
|
90
|
+
}
|
|
81
91
|
const cur = normalizeVersion(current);
|
|
82
92
|
const lat = normalizeVersion(latest);
|
|
83
93
|
if (!cur || !lat) return false;
|
|
@@ -104,6 +114,15 @@ function isPreRelease(version) {
|
|
|
104
114
|
return v.includes('-');
|
|
105
115
|
}
|
|
106
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Check if a version is a local dev build (e.g. "1.18.1-dev+dirty", "1.18.1-dev.3+abcdef").
|
|
119
|
+
* Dev builds should never trigger update prompts — they're running from source.
|
|
120
|
+
*/
|
|
121
|
+
function isDevBuild(version) {
|
|
122
|
+
if (typeof version !== 'string') return false;
|
|
123
|
+
return /-(dev|dirty)/.test(version) || /\+(dirty|dev)/.test(version);
|
|
124
|
+
}
|
|
125
|
+
|
|
107
126
|
/**
|
|
108
127
|
* Strip ANSI escape sequences and control characters from a string.
|
|
109
128
|
* Prevents terminal injection if the registry returns malicious data.
|