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 CHANGED
@@ -1,77 +1,22 @@
1
- // Per-session I/O tracking and burst detection.
2
- // Broadcasts rate + burst data so the frontend can derive working/idle status.
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, prevIn: 0, prevOut: 0 };
10
- if (!stream[id]) stream[id] = { lastOutAt: 0, burstStart: 0 };
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
- net[id].in += bytes;
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
- const now = Date.now();
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 start(sessions, broadcast) {
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 = { start, stop, trackIn, trackOut, isActive, lastOutputAt, lastInputAt, lastChunk, clear };
22
+ module.exports = { trackIn, trackOut, clear };
package/ansi-utils.js ADDED
@@ -0,0 +1,7 @@
1
+ const ANSI_RE = /\x1b[\[\]()#;?]*[0-9;?]*[ -/]*[@-~]|\x1b\].*?(?:\x07|\x1b\\)|\x1b.|\r|\x07/g;
2
+
3
+ function stripAnsi(value) {
4
+ return String(value || '').replace(ANSI_RE, '');
5
+ }
6
+
7
+ module.exports = { ANSI_RE, stripAnsi };
@@ -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, binName } = require('./utils');
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
- if (cmd?.presetId) {
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 commandEnv(cmd) {
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 = commandEnv(cmd);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clideck",
3
- "version": "1.31.16",
3
+ "version": "1.31.17",
4
4
  "description": "One screen for all your AI coding agents — run, monitor, and manage multiple CLI agents from a single browser tab",
5
5
  "main": "server.js",
6
6
  "bin": {
@@ -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
- remoteInstallMode = { update: !!opts.update, auto: !!opts.auto };
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
 
@@ -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
- 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
- }
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 = presetForCommand(cmd);
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 = presetForCommand(cmd);
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.replace(ANSI_RE, '').match(sessionIdRe);
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 = presetForCommand(cmd)?.presetId || 'shell';
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 = presetForCommand(cmd);
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 = presetForCommand(cmd)?.presetId || 'shell';
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 = presetForCommand(cmd)?.presetId || saved.presetId || 'shell';
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 = presetForCommand(cmd);
422
+ const preset = matchPreset(cmd);
426
423
  return { ...s, presetId: preset?.presetId || 'shell' };
427
424
  });
428
425
  }
@@ -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 ioActivity = require('./activity');
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
- // Returns true if we've received telemetry events for this session
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.replace(ANSI_RE, '').replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '');
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'));