clideck 1.31.16 → 1.31.17
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/activity.js +11 -66
- package/ansi-utils.js +7 -0
- package/claude-session.js +17 -0
- package/config.js +3 -5
- package/handlers.js +13 -11
- package/http-util.js +20 -0
- package/package.json +1 -1
- package/preset-utils.js +13 -0
- package/public/js/app.js +21 -7
- package/public/js/settings.js +1 -0
- package/server.js +2 -14
- package/session-agents.js +1 -18
- package/session-ask.js +1 -19
- package/sessions.js +14 -17
- package/telemetry-receiver.js +8 -27
- package/transcript.js +2 -2
package/activity.js
CHANGED
|
@@ -1,77 +1,22 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
let interval = null;
|
|
5
|
-
const net = {}; // id → byte counters
|
|
6
|
-
const stream = {}; // id → burst timing
|
|
1
|
+
// Lightweight per-session I/O counters. The old per-second stats broadcast was
|
|
2
|
+
// removed after frontend status tracking moved away from I/O heuristics.
|
|
3
|
+
const net = {}; // id -> byte counters
|
|
7
4
|
|
|
8
5
|
function ensure(id) {
|
|
9
|
-
if (!net[id]) net[id] = { in: 0, out: 0
|
|
10
|
-
|
|
6
|
+
if (!net[id]) net[id] = { in: 0, out: 0 };
|
|
7
|
+
return net[id];
|
|
11
8
|
}
|
|
12
9
|
|
|
13
10
|
function trackIn(id, bytes) {
|
|
14
|
-
ensure(id);
|
|
15
|
-
|
|
16
|
-
stream[id].lastInAt = Date.now();
|
|
11
|
+
const n = ensure(id);
|
|
12
|
+
n.in += bytes;
|
|
17
13
|
}
|
|
18
14
|
|
|
19
|
-
function lastInputAt(id) { return stream[id]?.lastInAt || 0; }
|
|
20
|
-
|
|
21
15
|
function trackOut(id, data) {
|
|
22
|
-
ensure(id);
|
|
23
|
-
|
|
24
|
-
const s = stream[id];
|
|
25
|
-
net[id].out += data.length;
|
|
26
|
-
if (now - s.lastOutAt > 2000) s.burstStart = now;
|
|
27
|
-
s.lastOutAt = now;
|
|
28
|
-
s.lastChunk = data;
|
|
16
|
+
const n = ensure(id);
|
|
17
|
+
n.out += data.length;
|
|
29
18
|
}
|
|
30
19
|
|
|
31
|
-
function
|
|
32
|
-
if (interval) return;
|
|
33
|
-
interval = setInterval(() => {
|
|
34
|
-
if (!sessions.size) return;
|
|
35
|
-
const now = Date.now();
|
|
36
|
-
const stats = {};
|
|
37
|
-
for (const [id] of sessions) {
|
|
38
|
-
ensure(id);
|
|
39
|
-
const n = net[id];
|
|
40
|
-
const s = stream[id];
|
|
41
|
-
const rawRateOut = n.out - n.prevOut;
|
|
42
|
-
const rawRateIn = n.in - n.prevIn;
|
|
43
|
-
n.prevOut = n.out;
|
|
44
|
-
n.prevIn = n.in;
|
|
45
|
-
const silence = s.lastOutAt ? now - s.lastOutAt : 0;
|
|
46
|
-
const burstMs = s.burstStart && silence < 2000 ? now - s.burstStart : 0;
|
|
47
|
-
stats[id] = { rawRateOut, rawRateIn, burstMs };
|
|
48
|
-
}
|
|
49
|
-
if (Object.keys(stats).length) broadcast({ type: 'stats', stats });
|
|
50
|
-
// Debug: PTY active state + last output chars per session (1s tick)
|
|
51
|
-
const active = [];
|
|
52
|
-
for (const [id] of sessions) {
|
|
53
|
-
const s = stream[id]; if (!s?.lastOutAt) continue;
|
|
54
|
-
const state = now - s.lastOutAt < 2000 ? 'ACTIVE' : 'SILENT';
|
|
55
|
-
const last = (s.lastChunk || '').replace(/\x1b\[[0-9;?]*[ -/]*[@-~]|\x1b\].*?(?:\x07|\x1b\\)|\x1b./g, '').replace(/[\r\n]+/g, ' ').trim().slice(-80);
|
|
56
|
-
active.push(`${id.slice(0,8)}=${state} [${last}]`);
|
|
57
|
-
}
|
|
58
|
-
// if (active.length) console.log(`[pty] ${active.join(' | ')}`);
|
|
59
|
-
}, 1000);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function stop() {
|
|
63
|
-
clearInterval(interval);
|
|
64
|
-
interval = null;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function isActive(id) {
|
|
68
|
-
const s = stream[id];
|
|
69
|
-
return s ? (Date.now() - s.lastOutAt < 2000) : false;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function lastOutputAt(id) { return stream[id]?.lastOutAt || 0; }
|
|
73
|
-
function lastChunk(id) { return stream[id]?.lastChunk || ''; }
|
|
74
|
-
|
|
75
|
-
function clear(id) { delete net[id]; delete stream[id]; }
|
|
20
|
+
function clear(id) { delete net[id]; }
|
|
76
21
|
|
|
77
|
-
module.exports = {
|
|
22
|
+
module.exports = { trackIn, trackOut, clear };
|
package/ansi-utils.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const CLAUDE_SESSION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
2
|
+
|
|
3
|
+
function updateClaudeSessionToken(sess, token, clideckId, options = {}) {
|
|
4
|
+
const next = String(token || '').trim();
|
|
5
|
+
if (!sess || sess.presetId !== 'claude-code' || !CLAUDE_SESSION_ID_RE.test(next)) return false;
|
|
6
|
+
if (sess.sessionToken === next) return false;
|
|
7
|
+
const prev = sess.sessionToken;
|
|
8
|
+
sess.sessionToken = next;
|
|
9
|
+
|
|
10
|
+
const label = options.label || 'Claude';
|
|
11
|
+
const source = options.source ? ` via ${options.source}` : '';
|
|
12
|
+
const previous = prev ? `${prev.slice(0, 12)}... -> ` : '';
|
|
13
|
+
console.log(`${label}: updated Claude session ID for ${clideckId.slice(0, 8)}${source}: ${previous}${next.slice(0, 12)}...`);
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
module.exports = { CLAUDE_SESSION_ID_RE, updateClaudeSessionToken };
|
package/config.js
CHANGED
|
@@ -3,7 +3,8 @@ const { join } = require('path');
|
|
|
3
3
|
const crypto = require('crypto');
|
|
4
4
|
const os = require('os');
|
|
5
5
|
const { DATA_DIR } = require('./paths');
|
|
6
|
-
const { defaultShell
|
|
6
|
+
const { defaultShell } = require('./utils');
|
|
7
|
+
const { presetForCommand } = require('./preset-utils');
|
|
7
8
|
|
|
8
9
|
const CONFIG_PATH = join(DATA_DIR, 'config.json');
|
|
9
10
|
|
|
@@ -63,10 +64,7 @@ function isPresetEnabled(preset) {
|
|
|
63
64
|
}
|
|
64
65
|
const EXPOSED_PRESETS = PRESETS.filter(isPresetEnabled);
|
|
65
66
|
|
|
66
|
-
function matchPreset(cmd) {
|
|
67
|
-
const bin = binName(cmd.command);
|
|
68
|
-
return PRESETS.find(p => binName(p.command) === bin);
|
|
69
|
-
}
|
|
67
|
+
function matchPreset(cmd) { return presetForCommand(cmd, PRESETS, { usePresetId: false }); }
|
|
70
68
|
|
|
71
69
|
function firstCommandToken(command) {
|
|
72
70
|
const raw = String(command || '').trim();
|
package/handlers.js
CHANGED
|
@@ -7,6 +7,7 @@ const sessions = require('./sessions');
|
|
|
7
7
|
const themes = require('./themes');
|
|
8
8
|
const presets = JSON.parse(readFileSync(join(__dirname, 'agent-presets.json'), 'utf8'));
|
|
9
9
|
const { listDirs, binName, defaultShell } = require('./utils');
|
|
10
|
+
const { presetForCommand: findPresetForCommand } = require('./preset-utils');
|
|
10
11
|
const { PORT } = require('./runtime');
|
|
11
12
|
for (const p of presets) if (p.presetId === 'shell') p.command = defaultShell;
|
|
12
13
|
function isPresetEnabled(preset) {
|
|
@@ -72,15 +73,10 @@ function getInstalledVersion(bin) {
|
|
|
72
73
|
}
|
|
73
74
|
|
|
74
75
|
function presetForCommand(cmd) {
|
|
75
|
-
|
|
76
|
-
const preset = presets.find(p => p.presetId === cmd.presetId);
|
|
77
|
-
if (preset) return preset;
|
|
78
|
-
}
|
|
79
|
-
const bin = binName(cmd?.command);
|
|
80
|
-
return presets.find(p => binName(p.command) === bin);
|
|
76
|
+
return findPresetForCommand(cmd, presets);
|
|
81
77
|
}
|
|
82
78
|
|
|
83
|
-
function
|
|
79
|
+
function rawCommandEnv(cmd) {
|
|
84
80
|
return cmd?.env && typeof cmd.env === 'object' && !Array.isArray(cmd.env) ? cmd.env : {};
|
|
85
81
|
}
|
|
86
82
|
|
|
@@ -93,7 +89,7 @@ function expandHomePath(value) {
|
|
|
93
89
|
}
|
|
94
90
|
|
|
95
91
|
function configRootFor(preset, cmd) {
|
|
96
|
-
const env =
|
|
92
|
+
const env = rawCommandEnv(cmd);
|
|
97
93
|
if (preset?.presetId === 'claude-code') return expandHomePath(env.CLAUDE_CONFIG_DIR) || join(os.homedir(), '.claude');
|
|
98
94
|
if (preset?.presetId === 'codex') return expandHomePath(env.CODEX_HOME) || join(os.homedir(), '.codex');
|
|
99
95
|
if (preset?.presetId === 'gemini-cli') return join(expandHomePath(env.GEMINI_CLI_HOME) || os.homedir(), '.gemini');
|
|
@@ -262,7 +258,7 @@ function detectTelemetryConfig(c) {
|
|
|
262
258
|
if (preset.available && preset.minVersion && !preset.versionOk) {
|
|
263
259
|
detected = false;
|
|
264
260
|
reason = `Update required (${preset.minVersion}+)`;
|
|
265
|
-
} else if (!detected && cmd.telemetryEnabled && preset.telemetryAutoSetup && preset.available && preset.versionOk && !attemptedRepairs.has(cmd.id || preset.presetId)) {
|
|
261
|
+
} else if (!detected && cmd.telemetryEnabled && cmd.telemetrySetupConsent === true && preset.telemetryAutoSetup && preset.available && preset.versionOk && !attemptedRepairs.has(cmd.id || preset.presetId)) {
|
|
266
262
|
attemptedRepairs.add(cmd.id || preset.presetId);
|
|
267
263
|
const repaired = applyTelemetryConfig(preset, cmd);
|
|
268
264
|
if (repaired.success) {
|
|
@@ -399,6 +395,7 @@ function onConnection(ws) {
|
|
|
399
395
|
cfg = { ...cfg, ...msg.config };
|
|
400
396
|
detectTelemetryConfig(cfg);
|
|
401
397
|
config.save(cfg);
|
|
398
|
+
plugins.notifyConfig(cfg);
|
|
402
399
|
sessions.broadcast({ type: 'config', config: configForClient() });
|
|
403
400
|
break;
|
|
404
401
|
|
|
@@ -417,11 +414,13 @@ function onConnection(ws) {
|
|
|
417
414
|
if (targetCmd ? cmd.id === targetCmd.id : presetForCommand(cmd)?.presetId === preset.presetId) {
|
|
418
415
|
cmd.telemetryEnabled = result.success;
|
|
419
416
|
cmd.telemetryStatus = result.success ? { ok: true } : { ok: false, error: result.message };
|
|
417
|
+
if (result.success) cmd.telemetrySetupConsent = true;
|
|
420
418
|
// Enable the agent when setup succeeds, disable if it fails
|
|
421
419
|
if (result.success) cmd.enabled = true;
|
|
422
420
|
}
|
|
423
421
|
}
|
|
424
422
|
config.save(cfg);
|
|
423
|
+
plugins.notifyConfig(cfg);
|
|
425
424
|
sessions.broadcast({ type: 'config', config: configForClient() });
|
|
426
425
|
ws.send(JSON.stringify({
|
|
427
426
|
type: 'telemetry.autosetup.result',
|
|
@@ -448,12 +447,14 @@ function onConnection(ws) {
|
|
|
448
447
|
for (const cmd of cfg.commands) {
|
|
449
448
|
if (targetCmd ? cmd.id === targetCmd.id : presetForCommand(cmd)?.presetId === preset.presetId) {
|
|
450
449
|
cmd.telemetryEnabled = enable && result.success;
|
|
450
|
+
cmd.telemetrySetupConsent = enable && result.success;
|
|
451
451
|
cmd.telemetryStatus = enable
|
|
452
452
|
? (result.success ? { ok: true } : { ok: false, error: result.message })
|
|
453
453
|
: null;
|
|
454
454
|
}
|
|
455
455
|
}
|
|
456
456
|
config.save(cfg);
|
|
457
|
+
plugins.notifyConfig(cfg);
|
|
457
458
|
sessions.broadcast({ type: 'config', config: configForClient() });
|
|
458
459
|
break;
|
|
459
460
|
}
|
|
@@ -659,6 +660,7 @@ function onConnection(ws) {
|
|
|
659
660
|
|
|
660
661
|
case 'remote.install': {
|
|
661
662
|
const update = !!msg.update;
|
|
663
|
+
const restartAfterUpdate = !!msg.restart;
|
|
662
664
|
const proc = require('child_process').spawn('npm', ['install', '-g', 'clideck-remote'], {
|
|
663
665
|
shell: true, stdio: ['ignore', 'pipe', 'pipe'],
|
|
664
666
|
});
|
|
@@ -666,8 +668,8 @@ function onConnection(ws) {
|
|
|
666
668
|
proc.stderr.on('data', d => ws.send(JSON.stringify({ type: 'remote.install.progress', text: d.toString() })));
|
|
667
669
|
proc.on('close', code => {
|
|
668
670
|
remoteUpdateCache = null;
|
|
669
|
-
if (code !== 0 || !update) {
|
|
670
|
-
ws.send(JSON.stringify({ type: 'remote.install.done', success: code === 0, update }));
|
|
671
|
+
if (code !== 0 || !update || !restartAfterUpdate) {
|
|
672
|
+
ws.send(JSON.stringify({ type: 'remote.install.done', success: code === 0, update, restarted: false }));
|
|
671
673
|
return;
|
|
672
674
|
}
|
|
673
675
|
require('child_process').execFile('clideck-remote', ['restart', '--json'], { timeout: 10000, shell: process.platform === 'win32', env: remoteCliEnv() }, (err, stdout) => {
|
package/http-util.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
function sendJson(res, status, payload) {
|
|
2
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
3
|
+
res.end(JSON.stringify(payload));
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function isLoopback(req) {
|
|
7
|
+
const addr = req.socket?.remoteAddress || '';
|
|
8
|
+
return addr === '::1' || addr === '127.0.0.1' || addr.startsWith('127.') || addr.startsWith('::ffff:127.');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function sameProject(a, b) {
|
|
12
|
+
return (a.projectId || null) === (b.projectId || null);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function projectName(projects, projectId) {
|
|
16
|
+
if (!projectId) return 'No project';
|
|
17
|
+
return projects.find(p => p.id === projectId)?.name || projectId;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = { sendJson, isLoopback, sameProject, projectName };
|
package/package.json
CHANGED
package/preset-utils.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const { binName } = require('./utils');
|
|
2
|
+
|
|
3
|
+
function presetForCommand(cmd, presets, options = {}) {
|
|
4
|
+
const usePresetId = options.usePresetId !== false;
|
|
5
|
+
if (usePresetId && cmd?.presetId) {
|
|
6
|
+
const preset = presets.find(p => p.presetId === cmd.presetId);
|
|
7
|
+
if (preset) return preset;
|
|
8
|
+
}
|
|
9
|
+
const bin = binName(cmd?.command);
|
|
10
|
+
return presets.find(p => binName(p.command) === bin);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
module.exports = { presetForCommand };
|
package/public/js/app.js
CHANGED
|
@@ -369,7 +369,7 @@ function connect() {
|
|
|
369
369
|
case 'remote.update':
|
|
370
370
|
remoteUpdateInfo = msg?.available ? msg : null;
|
|
371
371
|
if (remoteUpdateInfo?.available && remoteInstalled && !remoteInstallMode) {
|
|
372
|
-
startRemoteInstall({ update: true, auto: true });
|
|
372
|
+
startRemoteInstall({ update: true, auto: true, connectAfter: remoteState === 'waiting' || remoteState === 'paired' || remoteModalOpen });
|
|
373
373
|
}
|
|
374
374
|
if (remotePreflight?.pending) {
|
|
375
375
|
remotePreflight.updateSeen = true;
|
|
@@ -1222,7 +1222,7 @@ function finishRemotePreflight() {
|
|
|
1222
1222
|
return;
|
|
1223
1223
|
}
|
|
1224
1224
|
if (remoteUpdateInfo?.available) {
|
|
1225
|
-
startRemoteInstall({ update: true, auto: true });
|
|
1225
|
+
startRemoteInstall({ update: true, auto: true, connectAfter: true });
|
|
1226
1226
|
return;
|
|
1227
1227
|
}
|
|
1228
1228
|
if (remoteState === 'idle') {
|
|
@@ -1384,7 +1384,7 @@ function handleRemoteStatus(msg) {
|
|
|
1384
1384
|
if (wasPaired) { stopRemoteStats(); setRemoteLock(false); }
|
|
1385
1385
|
}
|
|
1386
1386
|
if (remoteUpdateInfo?.available && remoteInstalled && !remoteInstallMode) {
|
|
1387
|
-
startRemoteInstall({ update: true, auto: true });
|
|
1387
|
+
startRemoteInstall({ update: true, auto: true, connectAfter: remoteState === 'waiting' || remoteState === 'paired' || remoteModalOpen });
|
|
1388
1388
|
return;
|
|
1389
1389
|
}
|
|
1390
1390
|
updateRemoteButton();
|
|
@@ -1404,7 +1404,7 @@ function handleRemotePaired(msg) {
|
|
|
1404
1404
|
updateRemoteButton();
|
|
1405
1405
|
startRemotePoll();
|
|
1406
1406
|
if (remoteUpdateInfo?.available && remoteInstalled && !remoteInstallMode) {
|
|
1407
|
-
startRemoteInstall({ update: true, auto: true });
|
|
1407
|
+
startRemoteInstall({ update: true, auto: true, connectAfter: true });
|
|
1408
1408
|
return;
|
|
1409
1409
|
}
|
|
1410
1410
|
if (remotePreflight?.pending) {
|
|
@@ -1439,25 +1439,36 @@ function appendInstallLog(text) {
|
|
|
1439
1439
|
}
|
|
1440
1440
|
|
|
1441
1441
|
function startRemoteInstall(opts = {}) {
|
|
1442
|
-
|
|
1442
|
+
const update = !!opts.update;
|
|
1443
|
+
const auto = !!opts.auto;
|
|
1444
|
+
const connectAfter = opts.connectAfter !== undefined
|
|
1445
|
+
? !!opts.connectAfter
|
|
1446
|
+
: (!auto || remoteModalOpen || remoteState === 'waiting' || remoteState === 'paired');
|
|
1447
|
+
remoteInstallMode = { update, auto, connectAfter };
|
|
1443
1448
|
const log = document.getElementById('remote-install-log');
|
|
1444
1449
|
log.textContent = '';
|
|
1445
1450
|
if (remoteInstallMode.update) {
|
|
1446
1451
|
appendInstallLog(`Updating clideck-remote to ${remoteUpdateInfo?.latest || 'latest'}...\n`);
|
|
1447
1452
|
}
|
|
1448
1453
|
setRemotePane('installing');
|
|
1449
|
-
if (!remoteModalOpen) openRemoteModal();
|
|
1450
|
-
send({ type: 'remote.install', update: remoteInstallMode.update });
|
|
1454
|
+
if (!remoteModalOpen && remoteInstallMode.connectAfter) openRemoteModal();
|
|
1455
|
+
send({ type: 'remote.install', update: remoteInstallMode.update, restart: remoteInstallMode.connectAfter });
|
|
1451
1456
|
}
|
|
1452
1457
|
|
|
1453
1458
|
function handleInstallDone(msg) {
|
|
1454
1459
|
const success = !!msg?.success;
|
|
1455
1460
|
const wasUpdate = !!msg?.update || !!remoteInstallMode?.update;
|
|
1461
|
+
const mode = remoteInstallMode || {};
|
|
1456
1462
|
remoteInstallMode = null;
|
|
1457
1463
|
if (success) {
|
|
1458
1464
|
remoteInstalled = true;
|
|
1459
1465
|
remoteUpdateInfo = null;
|
|
1460
1466
|
if (wasUpdate) {
|
|
1467
|
+
if (!mode.connectAfter && !msg?.restart) {
|
|
1468
|
+
send({ type: 'remote.status', forceUpdate: true });
|
|
1469
|
+
updateRemoteButton();
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
1461
1472
|
remoteState = 'connecting';
|
|
1462
1473
|
setRemotePane('connecting');
|
|
1463
1474
|
send({ type: 'remote.status', forceUpdate: true });
|
|
@@ -1472,6 +1483,9 @@ function handleInstallDone(msg) {
|
|
|
1472
1483
|
const log = document.getElementById('remote-install-log');
|
|
1473
1484
|
log.textContent += `\n— ${msg?.error || 'Install failed'}. Check permissions or run manually:\n npm install -g clideck-remote\n`;
|
|
1474
1485
|
log.scrollTop = log.scrollHeight;
|
|
1486
|
+
if (!remoteModalOpen) {
|
|
1487
|
+
showToast('Mobile Remote update failed. Run `npm install -g clideck-remote` manually.', { type: 'error', duration: 6000 });
|
|
1488
|
+
}
|
|
1475
1489
|
}
|
|
1476
1490
|
}
|
|
1477
1491
|
|
package/public/js/settings.js
CHANGED
|
@@ -614,6 +614,7 @@ function saveConfig() {
|
|
|
614
614
|
outputMarker: existing.outputMarker || preset?.outputMarker || null,
|
|
615
615
|
env: parseEnvText(card.querySelector('.agent-env')?.value || ''),
|
|
616
616
|
telemetryEnabled: telemetryEnabledForCommand({ ...existing, presetId }, command),
|
|
617
|
+
telemetrySetupConsent: !!existing.telemetrySetupConsent,
|
|
617
618
|
telemetryStatus: existing.telemetryStatus || null,
|
|
618
619
|
bridge: existing.bridge || preset?.bridge,
|
|
619
620
|
userAdded: !!existing.userAdded,
|
package/server.js
CHANGED
|
@@ -4,6 +4,7 @@ const { join, extname, resolve } = require('path');
|
|
|
4
4
|
const { WebSocketServer } = require('ws');
|
|
5
5
|
const { ensurePtyHelper } = require('./utils');
|
|
6
6
|
const { PORT, HOST, localUrl } = require('./runtime');
|
|
7
|
+
const { updateClaudeSessionToken } = require('./claude-session');
|
|
7
8
|
|
|
8
9
|
function terminalLink(url, text = url) {
|
|
9
10
|
return `\u001B]8;;${url}\u0007${text}\u001B]8;;\u0007`;
|
|
@@ -17,16 +18,6 @@ function openUrlHint() {
|
|
|
17
18
|
const currentVersion = require('./package.json').version;
|
|
18
19
|
const { execFile, execSync } = require('child_process');
|
|
19
20
|
const shellOpt = process.platform === 'win32';
|
|
20
|
-
const CLAUDE_SESSION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
21
|
-
|
|
22
|
-
function updateClaudeSessionToken(sess, token, clideckId, source) {
|
|
23
|
-
const next = String(token || '').trim();
|
|
24
|
-
if (!sess || sess.presetId !== 'claude-code' || !CLAUDE_SESSION_ID_RE.test(next)) return;
|
|
25
|
-
if (sess.sessionToken === next) return;
|
|
26
|
-
const prev = sess.sessionToken;
|
|
27
|
-
sess.sessionToken = next;
|
|
28
|
-
console.log(`Claude: updated session ID for ${clideckId.slice(0, 8)} via ${source}: ${prev ? prev.slice(0, 12) + '... -> ' : ''}${next.slice(0, 12)}...`);
|
|
29
|
-
}
|
|
30
21
|
|
|
31
22
|
function checkSelfUpdate() {
|
|
32
23
|
return new Promise(ok => {
|
|
@@ -180,7 +171,7 @@ const server = http.createServer((req, res) => {
|
|
|
180
171
|
: null;
|
|
181
172
|
if (clideckId) {
|
|
182
173
|
const sess = allSessions.get(clideckId);
|
|
183
|
-
updateClaudeSessionToken(sess, sessionId, clideckId, `hook:${route}`);
|
|
174
|
+
updateClaudeSessionToken(sess, sessionId, clideckId, { label: 'Claude', source: `hook:${route}` });
|
|
184
175
|
if (route === 'start') {
|
|
185
176
|
sessions.broadcast({ type: 'session.status', id: clideckId, working: true, source: 'hook' });
|
|
186
177
|
} else if (route === 'stop' || route === 'idle') {
|
|
@@ -301,15 +292,12 @@ const wss = new WebSocketServer({
|
|
|
301
292
|
});
|
|
302
293
|
wss.on('connection', onConnection);
|
|
303
294
|
|
|
304
|
-
const activity = require('./activity');
|
|
305
|
-
activity.start(sessions.getSessions(), sessions.broadcast);
|
|
306
295
|
sessions.startAutoSave(() => require('./handlers').getConfig());
|
|
307
296
|
|
|
308
297
|
// Graceful shutdown: persist sessions before exit
|
|
309
298
|
const { getConfig } = require('./handlers');
|
|
310
299
|
function onShutdown() {
|
|
311
300
|
plugins.shutdown();
|
|
312
|
-
activity.stop();
|
|
313
301
|
sessions.shutdown(getConfig());
|
|
314
302
|
removeLockIfOwned();
|
|
315
303
|
process.exit(0);
|
package/session-agents.js
CHANGED
|
@@ -1,21 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
3
|
-
res.end(JSON.stringify(payload));
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
function isLoopback(req) {
|
|
7
|
-
const addr = req.socket?.remoteAddress || '';
|
|
8
|
-
return addr === '::1' || addr === '127.0.0.1' || addr.startsWith('127.') || addr.startsWith('::ffff:127.');
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function sameProject(a, b) {
|
|
12
|
-
return (a.projectId || null) === (b.projectId || null);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function projectName(projects, projectId) {
|
|
16
|
-
if (!projectId) return 'No project';
|
|
17
|
-
return projects.find(p => p.id === projectId)?.name || projectId;
|
|
18
|
-
}
|
|
1
|
+
const { sendJson, isLoopback, sameProject, projectName } = require('./http-util');
|
|
19
2
|
|
|
20
3
|
function agentRow(id, s, callerId, projects) {
|
|
21
4
|
const project = projectName(projects, s.projectId);
|
package/session-ask.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const transcript = require('./transcript');
|
|
2
|
+
const { sendJson, isLoopback, sameProject, projectName } = require('./http-util');
|
|
2
3
|
|
|
3
4
|
const MAX_BODY = 2 * 1024 * 1024;
|
|
4
5
|
const DEFAULT_TIMEOUT_MS = 10 * 60 * 1000;
|
|
@@ -6,11 +7,6 @@ const MAX_TIMEOUT_MS = 60 * 60 * 1000;
|
|
|
6
7
|
const BRACKETED_PASTE_START = '\x1b[200~';
|
|
7
8
|
const BRACKETED_PASTE_END = '\x1b[201~';
|
|
8
9
|
|
|
9
|
-
function sendJson(res, status, payload) {
|
|
10
|
-
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
11
|
-
res.end(JSON.stringify(payload));
|
|
12
|
-
}
|
|
13
|
-
|
|
14
10
|
function readJson(req) {
|
|
15
11
|
return new Promise((resolve, reject) => {
|
|
16
12
|
let body = '';
|
|
@@ -35,26 +31,12 @@ function jsonError(message, status = 400) {
|
|
|
35
31
|
return err;
|
|
36
32
|
}
|
|
37
33
|
|
|
38
|
-
function isLoopback(req) {
|
|
39
|
-
const addr = req.socket?.remoteAddress || '';
|
|
40
|
-
return addr === '::1' || addr === '127.0.0.1' || addr.startsWith('127.') || addr.startsWith('::ffff:127.');
|
|
41
|
-
}
|
|
42
|
-
|
|
43
34
|
function normalizeTimeout(ms) {
|
|
44
35
|
const n = Number(ms || DEFAULT_TIMEOUT_MS);
|
|
45
36
|
if (!Number.isFinite(n) || n <= 0) return DEFAULT_TIMEOUT_MS;
|
|
46
37
|
return Math.min(Math.round(n), MAX_TIMEOUT_MS);
|
|
47
38
|
}
|
|
48
39
|
|
|
49
|
-
function sameProject(a, b) {
|
|
50
|
-
return (a.projectId || null) === (b.projectId || null);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function projectName(projects, projectId) {
|
|
54
|
-
if (!projectId) return 'No project';
|
|
55
|
-
return projects.find(p => p.id === projectId)?.name || projectId;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
40
|
function parseScopedTarget(target) {
|
|
59
41
|
const text = String(target || '').trim();
|
|
60
42
|
if (!text.startsWith('@')) return null;
|
package/sessions.js
CHANGED
|
@@ -8,11 +8,12 @@ const transcript = require('./transcript');
|
|
|
8
8
|
const telemetry = require('./telemetry-receiver');
|
|
9
9
|
const opencodeBridge = require('./opencode-bridge');
|
|
10
10
|
const plugins = require('./plugin-loader');
|
|
11
|
+
const { presetForCommand } = require('./preset-utils');
|
|
12
|
+
const { stripAnsi } = require('./ansi-utils');
|
|
11
13
|
|
|
12
14
|
const THEMES = require('./themes');
|
|
13
15
|
const MAX_BUFFER = 2 * 1024 * 1024;
|
|
14
16
|
const { PORT, localUrl } = require('./runtime');
|
|
15
|
-
const ANSI_RE = /\x1b\[[0-9;?]*[ -/]*[@-~]|\x1b\].*?(?:\x07|\x1b\\)|\x1b./g;
|
|
16
17
|
const PRESETS = JSON.parse(require('fs').readFileSync(join(__dirname, 'agent-presets.json'), 'utf8'));
|
|
17
18
|
for (const p of PRESETS) if (p.presetId === 'shell') p.command = defaultShell;
|
|
18
19
|
const { DATA_DIR } = require('./paths');
|
|
@@ -37,6 +38,9 @@ function broadcast(msg) {
|
|
|
37
38
|
const raw = JSON.stringify(msg);
|
|
38
39
|
for (const c of clients) if (c.readyState === 1) c.send(raw);
|
|
39
40
|
if (msg.type === 'session.status') {
|
|
41
|
+
// Status broadcasts currently also apply the local state transition. This is
|
|
42
|
+
// intentional for now but couples transport with session state; if this area
|
|
43
|
+
// changes, split it into a dedicated setSessionStatus() transition first.
|
|
40
44
|
const s = sessions.get(msg.id);
|
|
41
45
|
if (s) {
|
|
42
46
|
s.working = !!msg.working;
|
|
@@ -59,14 +63,7 @@ function broadcast(msg) {
|
|
|
59
63
|
|
|
60
64
|
// --- Spawn a PTY and wire up a session ---
|
|
61
65
|
|
|
62
|
-
function presetForCommand(cmd)
|
|
63
|
-
if (cmd?.presetId) {
|
|
64
|
-
const preset = PRESETS.find(p => p.presetId === cmd.presetId);
|
|
65
|
-
if (preset) return preset;
|
|
66
|
-
}
|
|
67
|
-
const bin = binName(cmd?.command);
|
|
68
|
-
return PRESETS.find(p => binName(p.command) === bin);
|
|
69
|
-
}
|
|
66
|
+
function matchPreset(cmd) { return presetForCommand(cmd, PRESETS); }
|
|
70
67
|
|
|
71
68
|
function commandEnv(cmd) {
|
|
72
69
|
const env = {};
|
|
@@ -78,7 +75,7 @@ function commandEnv(cmd) {
|
|
|
78
75
|
}
|
|
79
76
|
|
|
80
77
|
function buildTelemetryEnv(id, cmd) {
|
|
81
|
-
const preset =
|
|
78
|
+
const preset = matchPreset(cmd);
|
|
82
79
|
const telemetryEnabled = cmd.telemetryEnabled ?? (preset?.presetId === 'claude-code');
|
|
83
80
|
const env = { CLIDECK_SESSION_ID: id, CLIDECK_PORT: String(PORT), CLIDECK_URL: localUrl() };
|
|
84
81
|
if (!preset?.telemetryEnv || !telemetryEnabled) return env;
|
|
@@ -114,7 +111,7 @@ function spawnSession(id, cmd, parts, cwd, name, themeId, commandId, savedToken,
|
|
|
114
111
|
}
|
|
115
112
|
|
|
116
113
|
const sessionIdRe = cmd.sessionIdPattern ? new RegExp(cmd.sessionIdPattern, 'i') : null;
|
|
117
|
-
const preset =
|
|
114
|
+
const preset = matchPreset(cmd);
|
|
118
115
|
const session = { name, themeId, commandId, cwd, pty: term, chunks: [], chunksSize: 0, sessionToken: savedToken || null, projectId: projectId || null, presetId: preset?.presetId || 'shell', working: undefined };
|
|
119
116
|
sessions.set(id, session);
|
|
120
117
|
transcript.setFinalizeOnIdle(id, ['claude-code', 'codex', 'gemini-cli', 'opencode', 'clideck-agent'].includes(session.presetId) ? session.presetId : null);
|
|
@@ -134,7 +131,7 @@ function spawnSession(id, cmd, parts, cwd, name, themeId, commandId, savedToken,
|
|
|
134
131
|
// Capture session ID from output
|
|
135
132
|
if (sessionIdRe && !session.sessionToken) {
|
|
136
133
|
const joined = session.chunks.join('');
|
|
137
|
-
const match = joined.match(sessionIdRe) || joined
|
|
134
|
+
const match = joined.match(sessionIdRe) || stripAnsi(joined).match(sessionIdRe);
|
|
138
135
|
if (match) {
|
|
139
136
|
session.sessionToken = match[1];
|
|
140
137
|
console.log(`Session ${id.slice(0, 8)}: captured token via output regex: ${match[1].slice(0, 12)}…`);
|
|
@@ -196,12 +193,12 @@ function create(msg, ws, cfg) {
|
|
|
196
193
|
return;
|
|
197
194
|
}
|
|
198
195
|
|
|
199
|
-
const createdPresetId =
|
|
196
|
+
const createdPresetId = matchPreset(cmd)?.presetId || 'shell';
|
|
200
197
|
const installId = msg.installId || undefined;
|
|
201
198
|
broadcast({ type: 'created', id, name, themeId, commandId: cmd.id, presetId: createdPresetId, projectId, installId });
|
|
202
199
|
|
|
203
200
|
// Immediate setup notification if config not detected
|
|
204
|
-
const preset =
|
|
201
|
+
const preset = matchPreset(cmd);
|
|
205
202
|
if (preset && (preset.telemetrySetup || preset.bridge) && !(cmd.telemetryEnabled && cmd.telemetryStatus?.ok)) {
|
|
206
203
|
broadcast({ type: 'session.needsSetup', id });
|
|
207
204
|
}
|
|
@@ -228,7 +225,7 @@ function createProgrammatic(opts, cfg) {
|
|
|
228
225
|
const s = sessions.get(id);
|
|
229
226
|
if (s && opts.ephemeral) s.ephemeral = true;
|
|
230
227
|
|
|
231
|
-
const presetId =
|
|
228
|
+
const presetId = matchPreset(cmd)?.presetId || 'shell';
|
|
232
229
|
broadcast({ type: 'created', id, name, themeId, commandId: cmd.id, presetId, projectId });
|
|
233
230
|
return { id };
|
|
234
231
|
}
|
|
@@ -278,7 +275,7 @@ function resume(msg, ws, cfg) {
|
|
|
278
275
|
resumable = resumable.filter(s => s.id !== id);
|
|
279
276
|
broadcast({ type: 'sessions.resumable', list: getResumable(cfg) });
|
|
280
277
|
|
|
281
|
-
const resumePresetId =
|
|
278
|
+
const resumePresetId = matchPreset(cmd)?.presetId || saved.presetId || 'shell';
|
|
282
279
|
broadcast({ type: 'created', id, name: saved.name, themeId: saved.themeId || saved.profileId || 'default', commandId: saved.commandId, presetId: resumePresetId, projectId: saved.projectId || null, muted: !!saved.muted, resumed: true, lastPreview: saved.lastPreview || '' });
|
|
283
280
|
}
|
|
284
281
|
|
|
@@ -422,7 +419,7 @@ function getResumable(cfg) {
|
|
|
422
419
|
if (s.presetId) return s;
|
|
423
420
|
const cmd = (cfg.commands || []).find(c => c.id === s.commandId);
|
|
424
421
|
if (!cmd) return { ...s, presetId: 'shell' };
|
|
425
|
-
const preset =
|
|
422
|
+
const preset = matchPreset(cmd);
|
|
426
423
|
return { ...s, presetId: preset?.presetId || 'shell' };
|
|
427
424
|
});
|
|
428
425
|
}
|
package/telemetry-receiver.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// CLI agents export telemetry events here; we capture agent session IDs
|
|
3
3
|
// (for resume) and detect whether telemetry is configured (setup prompts).
|
|
4
4
|
|
|
5
|
-
const
|
|
5
|
+
const { updateClaudeSessionToken } = require('./claude-session');
|
|
6
|
+
|
|
6
7
|
const activity = new Map(); // sessionId → has received events
|
|
7
8
|
const lastEvent = new Map(); // sessionId → last OTEL event name (+ kind)
|
|
8
9
|
const pendingSetup = new Map(); // sessionId → timer (waiting for first event)
|
|
@@ -10,21 +11,14 @@ const codexMenuPoll = new Map(); // sessionId → interval (polling for menu aft
|
|
|
10
11
|
const codexPendingStop = new Map(); // sessionId → ts (notify hook arrived; wait for next response.completed)
|
|
11
12
|
const codexOutputDone = new Map(); // sessionId → ts (fallback if notify never fires)
|
|
12
13
|
const codexPendingIdle = new Map(); // sessionId → timer (tiny settle before committing idle)
|
|
14
|
+
// Codex does not currently expose one reliable "fully idle" signal across streamed
|
|
15
|
+
// output, tool calls, and approval menus. This state machine is the best available
|
|
16
|
+
// approach for now; do not add more timing patches without fixture tests from real
|
|
17
|
+
// Codex event sequences.
|
|
13
18
|
const codexToolPhasePending = new Set(); // sessionId set once Codex has announced a tool-call phase, cleared when the phase resolves
|
|
14
19
|
const codexPendingTools = new Map(); // sessionId → Set(callId) for approved Codex tool calls still awaiting a result
|
|
15
20
|
let broadcastFn = null;
|
|
16
21
|
let sessionsFn = null;
|
|
17
|
-
const CLAUDE_SESSION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
18
|
-
|
|
19
|
-
function updateClaudeSessionToken(sess, token, clideckId) {
|
|
20
|
-
const next = String(token || '').trim();
|
|
21
|
-
if (!sess || sess.presetId !== 'claude-code' || !CLAUDE_SESSION_ID_RE.test(next)) return false;
|
|
22
|
-
if (sess.sessionToken === next) return false;
|
|
23
|
-
const prev = sess.sessionToken;
|
|
24
|
-
sess.sessionToken = next;
|
|
25
|
-
console.log(`Telemetry: updated Claude session ID for ${clideckId.slice(0, 8)}: ${prev ? prev.slice(0, 12) + '... -> ' : ''}${next.slice(0, 12)}...`);
|
|
26
|
-
return true;
|
|
27
|
-
}
|
|
28
22
|
|
|
29
23
|
function getPendingToolSet(id) {
|
|
30
24
|
let set = codexPendingTools.get(id);
|
|
@@ -218,7 +212,7 @@ function handleLogs(req, res) {
|
|
|
218
212
|
: (attrs['session.id'] || attrs['conversation.id']);
|
|
219
213
|
if (agentSessionId && sess) {
|
|
220
214
|
if (serviceName === 'claude-code') {
|
|
221
|
-
updateClaudeSessionToken(sess, agentSessionId, resolvedId);
|
|
215
|
+
updateClaudeSessionToken(sess, agentSessionId, resolvedId, { label: 'Telemetry' });
|
|
222
216
|
continue;
|
|
223
217
|
}
|
|
224
218
|
// Prefer interactive session ID (Gemini sends non-interactive init events first)
|
|
@@ -289,14 +283,6 @@ function markCodexStart(id, source = 'hook') {
|
|
|
289
283
|
broadcastFn?.({ type: 'session.status', id, working: true, source });
|
|
290
284
|
}
|
|
291
285
|
|
|
292
|
-
function markCodexIdle(id, source = 'hook') {
|
|
293
|
-
codexPendingStop.delete(id);
|
|
294
|
-
codexOutputDone.delete(id);
|
|
295
|
-
codexToolPhasePending.delete(id);
|
|
296
|
-
clearPendingTools(id);
|
|
297
|
-
scheduleCodexIdle(id, source);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
286
|
function scheduleCodexIdle(id, source) {
|
|
301
287
|
cancelCodexPendingIdle(id);
|
|
302
288
|
const timer = setTimeout(() => {
|
|
@@ -326,9 +312,4 @@ function clear(id) {
|
|
|
326
312
|
|
|
327
313
|
function getLastEvent(id) { return lastEvent.get(id) || ''; }
|
|
328
314
|
|
|
329
|
-
|
|
330
|
-
function hasEvents(id) {
|
|
331
|
-
return activity.has(id);
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
module.exports = { init, handleLogs, clear, hasEvents, getLastEvent, cancelCodexMenuPoll, watchSession, armCodexStop, markCodexStart, markCodexIdle };
|
|
315
|
+
module.exports = { init, handleLogs, clear, getLastEvent, cancelCodexMenuPoll, watchSession, armCodexStop, markCodexStart };
|
package/transcript.js
CHANGED
|
@@ -4,9 +4,9 @@ const { DATA_DIR } = require('./paths');
|
|
|
4
4
|
const builder = require('./transcript-normalizer');
|
|
5
5
|
const parser = require('./transcript-parser');
|
|
6
6
|
const candidate = require('./transcript-candidate');
|
|
7
|
+
const { stripAnsi } = require('./ansi-utils');
|
|
7
8
|
|
|
8
9
|
const DIR = join(DATA_DIR, 'transcripts');
|
|
9
|
-
const ANSI_RE = /\x1b[\[\]()#;?]*[0-9;]*[a-zA-Z@`~]|\x1b\].*?(?:\x07|\x1b\\)|\x1b.|\r|\x07/g;
|
|
10
10
|
const MAX_CACHE = 50 * 1024;
|
|
11
11
|
const LEGACY_SUFFIXES = ['-parsed.jsonl', '.screen'];
|
|
12
12
|
|
|
@@ -153,7 +153,7 @@ function trackOutput(id, data) {
|
|
|
153
153
|
function flush(id) {
|
|
154
154
|
const buf = outputBuf[id];
|
|
155
155
|
if (!buf?.text) return;
|
|
156
|
-
const clean = buf.text
|
|
156
|
+
const clean = stripAnsi(buf.text).replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '');
|
|
157
157
|
const lines = clean.split('\n').map(l => l.trim()).filter(l => l.length > 2);
|
|
158
158
|
buf.text = '';
|
|
159
159
|
if (lines.length) store(id, 'agent', lines.join('\n'));
|