termbeam 1.18.1 → 1.19.1
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-SVV3-IdA.js → _basePickBy-D2x9UR-Z.js} +1 -1
- package/public/assets/{_baseUniq-DOa_cDXJ.js → _baseUniq-C01jsmVS.js} +1 -1
- package/public/assets/{arc-CiPooz6h.js → arc-t3uO9VFT.js} +1 -1
- package/public/assets/architectureDiagram-Q4EWVU46-DWUIuXit.js +36 -0
- package/public/assets/{blockDiagram-WCTKOSBZ-BmfWKjiE.js → blockDiagram-DXYQGD6D-DnP4lNOB.js} +6 -6
- package/public/assets/c4Diagram-AHTNJAMY-B29P8b7E.js +10 -0
- package/public/assets/channel-Du2155FM.js +1 -0
- package/public/assets/{chunk-4BX2VUAB-DslNFup8.js → chunk-4BX2VUAB-BH7Ixc1K.js} +1 -1
- package/public/assets/chunk-4TB4RGXK-h7uQ9ZtR.js +206 -0
- package/public/assets/{chunk-55IACEB6-DF8ZBVw6.js → chunk-55IACEB6-D9ZHEhWx.js} +1 -1
- package/public/assets/{chunk-KX2RTZJC-DR85cVBM.js → chunk-EDXVE4YY-BEKltVR7.js} +1 -1
- package/public/assets/{chunk-FMBD7UC4-CVO2u-Sv.js → chunk-FMBD7UC4-BPkcv-bj.js} +1 -1
- package/public/assets/chunk-OYMX7WX6-C-wnBny1.js +231 -0
- package/public/assets/{chunk-QZHKN3VN-D1hlvsPG.js → chunk-QZHKN3VN-DBZnU2yp.js} +1 -1
- package/public/assets/{chunk-JSJVCQXG-CdSwCmzy.js → chunk-YZCP3GAM-C8GNavGc.js} +1 -1
- package/public/assets/classDiagram-6PBFFD2Q-Dzf6e5xB.js +1 -0
- package/public/assets/classDiagram-v2-HSJHXN6E-Dzf6e5xB.js +1 -0
- package/public/assets/clone-VT9_rs7L.js +1 -0
- package/public/assets/{cose-bilkent-S5V4N54A-BHgn8K1Z.js → cose-bilkent-S5V4N54A-BeFh7BYc.js} +1 -1
- package/public/assets/{dagre-KLK3FWXG-CK-UftHZ.js → dagre-KV5264BT-DlsYCBSj.js} +2 -2
- package/public/assets/diagram-5BDNPKRD-CnTlMSc9.js +10 -0
- package/public/assets/diagram-G4DWMVQ6-CKODi7zI.js +24 -0
- package/public/assets/diagram-MMDJMWI5-DEJGgmOX.js +43 -0
- package/public/assets/{diagram-P4PSJMXO-Cb0uoPeh.js → diagram-TYMM5635-Dju-tIVS.js} +1 -1
- package/public/assets/erDiagram-SMLLAGMA-CqPQSqot.js +85 -0
- package/public/assets/flowDiagram-DWJPFMVM-BeIRzZQp.js +162 -0
- package/public/assets/{ganttDiagram-A5KZAMGK-oBrRehQA.js → ganttDiagram-T4ZO3ILL-B6BnA7VR.js} +4 -4
- package/public/assets/gitGraphDiagram-UUTBAWPF-BoSi7fJX.js +106 -0
- package/public/assets/{graph-NJUd-2rr.js → graph-uVutBrOm.js} +1 -1
- package/public/assets/index-C0J_Dxjj.css +32 -0
- package/public/assets/index-NvPavSM9.js +447 -0
- package/public/assets/{infoDiagram-LFFYTUFH-C0cd4K10.js → infoDiagram-42DDH7IO-DD-KdApo.js} +1 -1
- package/public/assets/{ishikawaDiagram-PHBUUO56-Ny_4n8vD.js → ishikawaDiagram-UXIWVN3A-D36iFaUH.js} +2 -2
- package/public/assets/journeyDiagram-VCZTEJTY-BMQDm-H-.js +139 -0
- package/public/assets/{kanban-definition-K7BYSVSG-DDGFShX4.js → kanban-definition-6JOO6SKY-D1FZXkK7.js} +8 -8
- package/public/assets/{layout-DTAwaKXg.js → layout-xVUStQT2.js} +1 -1
- package/public/assets/{linear-CSYWJnE5.js → linear-BTv56PNK.js} +1 -1
- package/public/assets/mindmap-definition-QFDTVHPH-CvhBJGrR.js +96 -0
- package/public/assets/pieDiagram-DEJITSTG-DcxBOIJ2.js +30 -0
- package/public/assets/{quadrantDiagram-337W2JSQ-DWGyMcZ6.js → quadrantDiagram-34T5L4WZ-D79TxdrP.js} +1 -1
- package/public/assets/requirementDiagram-MS252O5E-gOOiR6tu.js +84 -0
- package/public/assets/{sankeyDiagram-WA2Y5GQK-GoTjgmGo.js → sankeyDiagram-XADWPNL6-YUncdO2g.js} +1 -1
- package/public/assets/sequenceDiagram-FGHM5R23-eoBFRqV1.js +157 -0
- package/public/assets/stateDiagram-FHFEXIEX-DeQeLuN0.js +1 -0
- package/public/assets/stateDiagram-v2-QKLJ7IA2-BhqrHPnX.js +1 -0
- package/public/assets/timeline-definition-GMOUNBTQ-CV0p2TOx.js +120 -0
- package/public/assets/{vennDiagram-LZ73GAT5-DzlX55oz.js → vennDiagram-DHZGUBPP-CciIt7hk.js} +5 -5
- package/public/assets/wardley-RL74JXVD-DpAn0g0p.js +162 -0
- package/public/assets/wardleyDiagram-NUSXRM2D-BcEpTQV4.js +20 -0
- package/public/assets/xychartDiagram-5P7HB3ND-B-PklpIN.js +7 -0
- package/public/index.html +2 -2
- package/public/sw.js +1 -1
- package/src/server/routes.js +48 -0
- package/src/utils/agent-sessions.js +288 -0
- package/src/utils/agents.js +118 -0
- package/src/utils/update-check.js +19 -0
- package/public/assets/architectureDiagram-2XIMDMQ5-DLhsY97x.js +0 -36
- package/public/assets/c4Diagram-IC4MRINW-CHJjiNgt.js +0 -10
- package/public/assets/channel-BK2nwbl-.js +0 -1
- package/public/assets/chunk-NQ4KR5QH-DENB8fT9.js +0 -220
- package/public/assets/chunk-WL4C6EOR-BFkfV9d-.js +0 -189
- package/public/assets/classDiagram-VBA2DB6C-Ca3s5d8r.js +0 -1
- package/public/assets/classDiagram-v2-RAHNMMFH-Ca3s5d8r.js +0 -1
- package/public/assets/clone-Zyw2C-Y3.js +0 -1
- package/public/assets/diagram-E7M64L7V-B7XX1Mqr.js +0 -24
- package/public/assets/diagram-IFDJBPK2-DtSlXz7u.js +0 -43
- package/public/assets/erDiagram-INFDFZHY-D2aAhxDx.js +0 -70
- package/public/assets/flowDiagram-PKNHOUZH-BxCqn1aV.js +0 -162
- package/public/assets/gitGraphDiagram-K3NZZRJ6-CMuK7hOw.js +0 -65
- package/public/assets/index-BzV_FYAW.css +0 -32
- package/public/assets/index-DBnflEQZ.js +0 -394
- package/public/assets/journeyDiagram-4ABVD52K-2QGufcXt.js +0 -139
- package/public/assets/mindmap-definition-YRQLILUH-C6Jy7Tz1.js +0 -68
- package/public/assets/pieDiagram-SKSYHLDU-DCNv1DYe.js +0 -30
- package/public/assets/requirementDiagram-Z7DCOOCP-BC1_RS6p.js +0 -73
- package/public/assets/sequenceDiagram-2WXFIKYE-CjTa5Eua.js +0 -145
- package/public/assets/stateDiagram-RAJIS63D-wf6b0MTk.js +0 -1
- package/public/assets/stateDiagram-v2-FVOUBMTO-lYMaTr7v.js +0 -1
- package/public/assets/timeline-definition-YZTLITO2-BHWYxexH.js +0 -61
- package/public/assets/treemap-KZPCXAKY-0OLZO-76.js +0 -162
- package/public/assets/xychartDiagram-JWTSCODW-DafJ-a-f.js +0 -7
|
@@ -0,0 +1,288 @@
|
|
|
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
|
+
try {
|
|
71
|
+
if (!fs.statSync(fullProjDir).isDirectory()) continue;
|
|
72
|
+
} catch {
|
|
73
|
+
continue; // Directory may have been removed
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Decode CWD from directory name: -Users-foo-bar → /Users/foo/bar
|
|
77
|
+
const cwd = projDir.replace(/^-/, '/').replace(/-/g, '/');
|
|
78
|
+
|
|
79
|
+
const jsonlFiles = fs
|
|
80
|
+
.readdirSync(fullProjDir)
|
|
81
|
+
.filter((f) => f.endsWith('.jsonl'))
|
|
82
|
+
.map((f) => {
|
|
83
|
+
const fullPath = path.join(fullProjDir, f);
|
|
84
|
+
const stat = fs.statSync(fullPath);
|
|
85
|
+
return { file: f, path: fullPath, mtime: stat.mtime, size: stat.size };
|
|
86
|
+
})
|
|
87
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
88
|
+
|
|
89
|
+
for (const fileInfo of jsonlFiles.slice(0, 10)) {
|
|
90
|
+
try {
|
|
91
|
+
const sessionId = path.basename(fileInfo.file, '.jsonl');
|
|
92
|
+
|
|
93
|
+
// Read file — cap at 100KB to avoid blocking the event loop on large JSONL files
|
|
94
|
+
let content;
|
|
95
|
+
if (fileInfo.size > 100_000) {
|
|
96
|
+
const fd = fs.openSync(fileInfo.path, 'r');
|
|
97
|
+
const buf = Buffer.alloc(100_000);
|
|
98
|
+
const bytesRead = fs.readSync(fd, buf, 0, 100_000, 0);
|
|
99
|
+
fs.closeSync(fd);
|
|
100
|
+
content = buf.toString('utf8', 0, bytesRead);
|
|
101
|
+
} else {
|
|
102
|
+
content = fs.readFileSync(fileInfo.path, 'utf8');
|
|
103
|
+
}
|
|
104
|
+
const rawLines = content.split('\n');
|
|
105
|
+
|
|
106
|
+
let cwdFromFile = cwd;
|
|
107
|
+
let branch = null;
|
|
108
|
+
let userTurnCount = 0;
|
|
109
|
+
let firstUserMsg = null;
|
|
110
|
+
|
|
111
|
+
for (const line of rawLines) {
|
|
112
|
+
if (!line.trim()) continue;
|
|
113
|
+
try {
|
|
114
|
+
const entry = JSON.parse(line);
|
|
115
|
+
if (!branch && entry.gitBranch) branch = entry.gitBranch;
|
|
116
|
+
if (entry.cwd) cwdFromFile = entry.cwd;
|
|
117
|
+
if (entry.type === 'user') {
|
|
118
|
+
userTurnCount++;
|
|
119
|
+
if (!firstUserMsg) {
|
|
120
|
+
// Claude stores user message at entry.message.content (not entry.data)
|
|
121
|
+
const msg = entry.message;
|
|
122
|
+
if (msg && typeof msg === 'object') {
|
|
123
|
+
const content = msg.content;
|
|
124
|
+
if (typeof content === 'string') {
|
|
125
|
+
// Skip meta/command messages (XML-tagged system entries)
|
|
126
|
+
if (!content.startsWith('<') && content.trim().length > 5) {
|
|
127
|
+
firstUserMsg = content.slice(0, 200);
|
|
128
|
+
}
|
|
129
|
+
} else if (Array.isArray(content)) {
|
|
130
|
+
for (const item of content) {
|
|
131
|
+
if (item && item.type === 'text' && typeof item.text === 'string') {
|
|
132
|
+
if (!item.text.startsWith('<') && item.text.trim().length > 5) {
|
|
133
|
+
firstUserMsg = item.text.slice(0, 200);
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
} catch {
|
|
143
|
+
// skip malformed line
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Skip empty sessions (no user interaction)
|
|
148
|
+
if (userTurnCount === 0) continue;
|
|
149
|
+
|
|
150
|
+
sessions.push({
|
|
151
|
+
id: sessionId,
|
|
152
|
+
agent: 'claude',
|
|
153
|
+
agentName: 'Claude Code',
|
|
154
|
+
agentIcon: 'claude',
|
|
155
|
+
summary: firstUserMsg || null,
|
|
156
|
+
cwd: cwdFromFile,
|
|
157
|
+
repo: null,
|
|
158
|
+
branch,
|
|
159
|
+
updatedAt: fileInfo.mtime.toISOString(),
|
|
160
|
+
turnCount: userTurnCount,
|
|
161
|
+
});
|
|
162
|
+
} catch (err) {
|
|
163
|
+
log.debug(`Failed to parse Claude session ${fileInfo.file}: ${err.message}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Sort by updated time descending
|
|
169
|
+
sessions.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt));
|
|
170
|
+
return sessions.slice(0, limit);
|
|
171
|
+
} catch (err) {
|
|
172
|
+
log.warn(`Failed to read Claude sessions: ${err.message}`);
|
|
173
|
+
return [];
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Read OpenCode sessions from SQLite store.
|
|
179
|
+
* DB at ~/.local/share/opencode/opencode.db
|
|
180
|
+
*/
|
|
181
|
+
function readOpenCodeSessions(limit = 50) {
|
|
182
|
+
let Database;
|
|
183
|
+
try {
|
|
184
|
+
Database = require('better-sqlite3');
|
|
185
|
+
} catch {
|
|
186
|
+
return [];
|
|
187
|
+
}
|
|
188
|
+
const dbPath = path.join(os.homedir(), '.local', 'share', 'opencode', 'opencode.db');
|
|
189
|
+
if (!fs.existsSync(dbPath)) return [];
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
const db = new Database(dbPath, { readonly: true, fileMustExist: true });
|
|
193
|
+
const sessions = db
|
|
194
|
+
.prepare(
|
|
195
|
+
`
|
|
196
|
+
SELECT s.id, s.title, s.directory, s.time_created, s.time_updated,
|
|
197
|
+
(SELECT COUNT(*) FROM message m WHERE m.session_id = s.id) as msg_count
|
|
198
|
+
FROM session s
|
|
199
|
+
WHERE s.time_archived IS NULL
|
|
200
|
+
ORDER BY s.time_updated DESC
|
|
201
|
+
LIMIT ?
|
|
202
|
+
`,
|
|
203
|
+
)
|
|
204
|
+
.all(limit);
|
|
205
|
+
db.close();
|
|
206
|
+
|
|
207
|
+
return sessions
|
|
208
|
+
.filter((s) => s.msg_count > 0)
|
|
209
|
+
.map((s) => ({
|
|
210
|
+
id: s.id,
|
|
211
|
+
agent: 'opencode',
|
|
212
|
+
agentName: 'OpenCode',
|
|
213
|
+
agentIcon: 'opencode',
|
|
214
|
+
summary: s.title || null,
|
|
215
|
+
cwd: s.directory || null,
|
|
216
|
+
repo: null,
|
|
217
|
+
branch: null,
|
|
218
|
+
updatedAt: s.time_updated || s.time_created || null,
|
|
219
|
+
turnCount: s.msg_count || 0,
|
|
220
|
+
}));
|
|
221
|
+
} catch (err) {
|
|
222
|
+
log.warn(`Failed to read OpenCode sessions: ${err.message}`);
|
|
223
|
+
return [];
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Get all agent sessions from all sources, unified and sorted.
|
|
229
|
+
*/
|
|
230
|
+
async function getAgentSessions({ limit = 100, agent = null, search = null } = {}) {
|
|
231
|
+
const results = [];
|
|
232
|
+
|
|
233
|
+
if (!agent || agent === 'copilot') {
|
|
234
|
+
results.push(...readCopilotSessions(limit));
|
|
235
|
+
}
|
|
236
|
+
if (!agent || agent === 'claude') {
|
|
237
|
+
results.push(...readClaudeSessions(limit));
|
|
238
|
+
}
|
|
239
|
+
if (!agent || agent === 'opencode') {
|
|
240
|
+
results.push(...readOpenCodeSessions(limit));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Sort all by updatedAt descending
|
|
244
|
+
results.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt));
|
|
245
|
+
|
|
246
|
+
// Apply search filter (case-insensitive substring match)
|
|
247
|
+
if (search) {
|
|
248
|
+
const q = search.toLowerCase();
|
|
249
|
+
const filtered = results.filter(
|
|
250
|
+
(s) =>
|
|
251
|
+
(s.summary && s.summary.toLowerCase().includes(q)) ||
|
|
252
|
+
(s.cwd && s.cwd.toLowerCase().includes(q)) ||
|
|
253
|
+
(s.repo && s.repo.toLowerCase().includes(q)) ||
|
|
254
|
+
(s.branch && s.branch.toLowerCase().includes(q)),
|
|
255
|
+
);
|
|
256
|
+
return filtered.slice(0, limit);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return results.slice(0, limit);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Build the resume command for a given agent session.
|
|
264
|
+
*/
|
|
265
|
+
function getResumeCommand(session) {
|
|
266
|
+
// Validate session ID to prevent command injection
|
|
267
|
+
// UUID format for copilot/claude, ses_xxx format for opencode
|
|
268
|
+
if (!/^[a-z0-9_-]{8,}$/i.test(session.id)) return null;
|
|
269
|
+
|
|
270
|
+
switch (session.agent) {
|
|
271
|
+
case 'copilot':
|
|
272
|
+
return `copilot --resume=${session.id}`;
|
|
273
|
+
case 'claude':
|
|
274
|
+
return `claude --resume ${session.id}`;
|
|
275
|
+
case 'opencode':
|
|
276
|
+
return `opencode --session ${session.id}`;
|
|
277
|
+
default:
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
module.exports = {
|
|
283
|
+
getAgentSessions,
|
|
284
|
+
getResumeCommand,
|
|
285
|
+
readCopilotSessions,
|
|
286
|
+
readClaudeSessions,
|
|
287
|
+
readOpenCodeSessions,
|
|
288
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
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
|
+
id: 'opencode',
|
|
44
|
+
name: 'OpenCode',
|
|
45
|
+
cmd: 'opencode',
|
|
46
|
+
icon: 'opencode',
|
|
47
|
+
detect: ['opencode', ['--version']],
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
let cachedAgents = null;
|
|
52
|
+
let cacheTime = 0;
|
|
53
|
+
const CACHE_TTL = 60_000; // 60 seconds
|
|
54
|
+
|
|
55
|
+
function tryDetectAgent(agent) {
|
|
56
|
+
const [cmd, args] = agent.detect;
|
|
57
|
+
const isWindows = os.platform() === 'win32';
|
|
58
|
+
const candidates = isWindows ? [cmd, `${cmd}.cmd`, `${cmd}.exe`] : [cmd];
|
|
59
|
+
|
|
60
|
+
return new Promise((resolve) => {
|
|
61
|
+
let resolved = false;
|
|
62
|
+
let remaining = candidates.length;
|
|
63
|
+
|
|
64
|
+
for (const bin of candidates) {
|
|
65
|
+
child_process.execFile(bin, args, { timeout: 5000, encoding: 'utf8' }, (err, stdout) => {
|
|
66
|
+
remaining--;
|
|
67
|
+
if (resolved) return;
|
|
68
|
+
if (!err) {
|
|
69
|
+
resolved = true;
|
|
70
|
+
const version = (stdout || '').trim().split('\n')[0] || 'unknown';
|
|
71
|
+
resolve({
|
|
72
|
+
id: agent.id,
|
|
73
|
+
name: agent.name,
|
|
74
|
+
cmd: agent.cmd,
|
|
75
|
+
args: agent.args || [],
|
|
76
|
+
icon: agent.icon,
|
|
77
|
+
version,
|
|
78
|
+
});
|
|
79
|
+
} else if (remaining === 0) {
|
|
80
|
+
resolve(null);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function detectAgents() {
|
|
88
|
+
log.debug('Detecting available AI agents...');
|
|
89
|
+
const results = await Promise.allSettled(KNOWN_AGENTS.map(tryDetectAgent));
|
|
90
|
+
|
|
91
|
+
const agents = [];
|
|
92
|
+
for (const result of results) {
|
|
93
|
+
if (result.status === 'fulfilled' && result.value) {
|
|
94
|
+
agents.push(result.value);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Deduplicate: prefer standalone copilot over gh copilot
|
|
99
|
+
const hasCopilot = agents.some((a) => a.id === 'copilot');
|
|
100
|
+
const deduped = hasCopilot ? agents.filter((a) => a.id !== 'gh-copilot') : agents;
|
|
101
|
+
|
|
102
|
+
log.debug(
|
|
103
|
+
`Detected ${deduped.length} AI agent(s): ${deduped.map((a) => a.name).join(', ') || 'none'}`,
|
|
104
|
+
);
|
|
105
|
+
return deduped;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function getAvailableAgents() {
|
|
109
|
+
const now = Date.now();
|
|
110
|
+
if (cachedAgents && now - cacheTime < CACHE_TTL) {
|
|
111
|
+
return cachedAgents;
|
|
112
|
+
}
|
|
113
|
+
cachedAgents = await detectAgents();
|
|
114
|
+
cacheTime = Date.now();
|
|
115
|
+
return cachedAgents;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
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.
|