clideck 1.31.12 → 1.31.13

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/README.md CHANGED
@@ -51,14 +51,12 @@ clideck --port 4001
51
51
 
52
52
  **Session resume** - close the lid, reopen tomorrow, pick up where things left off. each agent's session ID is captured automatically.
53
53
 
54
- **Autopilot** - enable autopilot on a project, walk away. it watches for one agent to finish, hands the output to the next one, and keeps going until the work is done or blocked. this is the part that makes sleep possible. routes content verbatim, no rewriting or summarizing. fingerprints each output and tracks handoff history to guard against repeat loops. ~50 output tokens per routing decision. supports Anthropic, OpenAI, Google, Groq, xAI, Mistral, OpenRouter, Cerebras.
54
+ **Ask another session** - from inside any CliDeck session, an agent can consult another session and get the answer back as command output:
55
55
 
56
56
  <p align="center">
57
- <img src="assets/autopilot.gif" width="720" alt="Autopilot routing work between agents">
57
+ <img src="assets/clideck-ask.png" width="720" alt="One agent asking another session and getting findings back">
58
58
  </p>
59
59
 
60
- **Ask another session** - from inside any CliDeck session, an agent can consult another session and get the answer back as command output:
61
-
62
60
  ```bash
63
61
  clideck agents
64
62
  clideck ask --session "Reviewer" --message "Review this output and return findings." --timeout 10m
Binary file
package/handlers.js CHANGED
@@ -100,9 +100,9 @@ function configRootFor(preset, cmd) {
100
100
  return os.homedir();
101
101
  }
102
102
 
103
- function checkRemoteUpdate(ws) {
103
+ function checkRemoteUpdate(ws, force = false) {
104
104
  const now = Date.now();
105
- if (remoteUpdateCache && now - remoteUpdateCheckedAt < REMOTE_UPDATE_INTERVAL) {
105
+ if (!force && remoteUpdateCache && now - remoteUpdateCheckedAt < REMOTE_UPDATE_INTERVAL) {
106
106
  ws.send(JSON.stringify({ type: 'remote.update', checked: true, ...remoteUpdateCache }));
107
107
  return;
108
108
  }
@@ -573,7 +573,7 @@ function onConnection(ws) {
573
573
  try { ws.send(JSON.stringify({ type: 'remote.status', installed: true, ...JSON.parse(stdout) })); }
574
574
  catch { ws.send(JSON.stringify({ type: 'remote.status', installed: true })); }
575
575
  });
576
- checkRemoteUpdate(ws);
576
+ checkRemoteUpdate(ws, !!msg.forceUpdate);
577
577
  break;
578
578
  }
579
579
 
@@ -602,7 +602,25 @@ function onConnection(ws) {
602
602
  break;
603
603
  }
604
604
 
605
+ case 'remote.voice.transcribe': {
606
+ const requestId = String(msg.requestId || '');
607
+ const replyError = (error) => ws.send(JSON.stringify({ type: 'remote.voice.error', requestId, error }));
608
+ if (!plugins.hasCapability('voice-input', 'transcribeAudio')) {
609
+ replyError('Install the Voice Input plugin in CliDeck first.');
610
+ break;
611
+ }
612
+ if (typeof msg.audio !== 'string' || !msg.audio) {
613
+ replyError('No audio received.');
614
+ break;
615
+ }
616
+ plugins.invoke('voice-input', 'transcribeAudio', { audio: msg.audio })
617
+ .then(result => ws.send(JSON.stringify({ type: 'remote.voice.result', requestId, ...result })))
618
+ .catch(e => replyError(e.message || 'Voice transcription failed.'));
619
+ break;
620
+ }
621
+
605
622
  case 'remote.install': {
623
+ const update = !!msg.update;
606
624
  const proc = require('child_process').spawn('npm', ['install', '-g', 'clideck-remote'], {
607
625
  shell: true, stdio: ['ignore', 'pipe', 'pipe'],
608
626
  });
@@ -610,7 +628,19 @@ function onConnection(ws) {
610
628
  proc.stderr.on('data', d => ws.send(JSON.stringify({ type: 'remote.install.progress', text: d.toString() })));
611
629
  proc.on('close', code => {
612
630
  remoteUpdateCache = null;
613
- ws.send(JSON.stringify({ type: 'remote.install.done', success: code === 0 }));
631
+ if (code !== 0 || !update) {
632
+ ws.send(JSON.stringify({ type: 'remote.install.done', success: code === 0, update }));
633
+ return;
634
+ }
635
+ require('child_process').execFile('clideck-remote', ['restart', '--json'], { timeout: 10000, shell: process.platform === 'win32', env: remoteCliEnv() }, (err, stdout) => {
636
+ if (err) {
637
+ ws.send(JSON.stringify({ type: 'remote.install.done', success: false, update, error: err.message }));
638
+ return;
639
+ }
640
+ let restart = null;
641
+ try { restart = JSON.parse(stdout); } catch {}
642
+ ws.send(JSON.stringify({ type: 'remote.install.done', success: true, update, restart }));
643
+ });
614
644
  });
615
645
  break;
616
646
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clideck",
3
- "version": "1.31.12",
3
+ "version": "1.31.13",
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": {
package/plugin-loader.js CHANGED
@@ -67,6 +67,7 @@ let createSessionFn = null;
67
67
  let closeSessionFn = null;
68
68
  const settingsChangeHandlers = new Map(); // pluginId → [fn]
69
69
  const sessionPills = new Map(); // pillId → { pluginId, id, title, projectId, working, statusText, icon, logs[] }
70
+ const backendHandlers = new Map(); // pluginId.name → fn
70
71
 
71
72
  function removeHooks(pluginId) {
72
73
  for (const arr of [inputHooks, outputHooks, statusHooks, transcriptHooks, menuHooks, configHooks]) {
@@ -77,6 +78,9 @@ function removeHooks(pluginId) {
77
78
  for (const key of frontendHandlers.keys()) {
78
79
  if (key.startsWith(`plugin.${pluginId}.`)) frontendHandlers.delete(key);
79
80
  }
81
+ for (const key of backendHandlers.keys()) {
82
+ if (key.startsWith(`${pluginId}.`)) backendHandlers.delete(key);
83
+ }
80
84
  settingsChangeHandlers.delete(pluginId);
81
85
  for (const [id, pill] of sessionPills) {
82
86
  if (pill.pluginId === pluginId) {
@@ -202,6 +206,11 @@ function buildApi(pluginId, pluginDir, state) {
202
206
  onFrontendMessage(event, fn) {
203
207
  frontendHandlers.set(`plugin.${pluginId}.${event}`, fn);
204
208
  },
209
+ expose(name, fn) {
210
+ if (typeof name === 'string' && name && typeof fn === 'function') {
211
+ backendHandlers.set(`${pluginId}.${name}`, fn);
212
+ }
213
+ },
205
214
 
206
215
  getSession(id) {
207
216
  const s = sessionsFn?.()?.get(id);
@@ -423,6 +432,17 @@ function handleMessage(msg) {
423
432
  return true;
424
433
  }
425
434
 
435
+ function hasCapability(pluginId, name) {
436
+ return backendHandlers.has(`${pluginId}.${name}`);
437
+ }
438
+
439
+ async function invoke(pluginId, name, data = {}) {
440
+ const key = `${pluginId}.${name}`;
441
+ const fn = backendHandlers.get(key);
442
+ if (!fn) throw new Error(`Plugin capability not available: ${key}`);
443
+ return await fn(data);
444
+ }
445
+
426
446
  function getInfo() {
427
447
  const cfg = getConfigFn?.();
428
448
  const installed = [...plugins.values()].map(p => ({
@@ -561,6 +581,6 @@ module.exports = {
561
581
  PLUGINS_DIR, BUNDLED_IDS,
562
582
  init, shutdown,
563
583
  transformInput, notifyOutput, notifyStatus, notifyTranscript, notifyMenu, notifyConfig, clearStatus, isWorking, shouldAutoApproveMenu,
564
- handleMessage, updateSetting, getInfo, resolveFile, installPlugin, removePlugin,
584
+ handleMessage, hasCapability, invoke, updateSetting, getInfo, resolveFile, installPlugin, removePlugin,
565
585
  getPills, getPillLogs,
566
586
  };
@@ -244,28 +244,38 @@ module.exports = {
244
244
  return { text: data.text || '', language: data.language || 'unknown', avg_logprob: null };
245
245
  }
246
246
 
247
+ // --- Transcription API ---
248
+
249
+ async function transcribeAudio(audio) {
250
+ if (!api.getSetting('enabled')) {
251
+ throw new Error('Enable the Voice Input plugin in CliDeck first.');
252
+ }
253
+ const backend = api.getSetting('backend');
254
+ let result;
255
+ if (backend === 'local') {
256
+ if (!worker) await startLocal();
257
+ if (!worker) throw new Error('Local model not running. Enable plugin with local backend to start.');
258
+ result = await workerCmd('transcribe', {
259
+ audio,
260
+ lang: api.getSetting('language') || 'auto',
261
+ });
262
+ } else {
263
+ result = await transcribeOpenAI(audio);
264
+ }
265
+
266
+ const text = processText(result.text || '');
267
+ if (!text) return { text: '', skipped: true };
268
+ return { text, language: result.language, inferenceTime: result.inference_time };
269
+ }
270
+
271
+ api.expose('transcribeAudio', ({ audio }) => transcribeAudio(audio));
272
+
247
273
  // --- Message handlers ---
248
274
 
249
275
  api.onFrontendMessage('transcribe', async (msg) => {
250
- const backend = api.getSetting('backend');
251
276
  try {
252
- let result;
253
- if (backend === 'local') {
254
- if (!worker) { api.sendToFrontend('error', { error: 'Local model not running. Enable plugin with local backend to start.' }); return; }
255
- result = await workerCmd('transcribe', {
256
- audio: msg.audio,
257
- lang: api.getSetting('language') || 'auto',
258
- });
259
- } else {
260
- result = await transcribeOpenAI(msg.audio);
261
- }
262
-
263
- const text = processText(result.text || '');
264
- if (!text) {
265
- api.sendToFrontend('result', { text: '', skipped: true, sessionId: msg.sessionId });
266
- return;
267
- }
268
- api.sendToFrontend('result', { text, language: result.language, inferenceTime: result.inference_time, sessionId: msg.sessionId });
277
+ const result = await transcribeAudio(msg.audio);
278
+ api.sendToFrontend('result', { ...result, sessionId: msg.sessionId });
269
279
  } catch (e) {
270
280
  api.log(`transcribe: ${e.message}`);
271
281
  api.sendToFrontend('error', { error: e.message });
package/public/js/app.js CHANGED
@@ -45,7 +45,7 @@ function connect() {
45
45
  reconnectReplaySkip = new Set(state.terms.keys());
46
46
  setServerConnectionState(true);
47
47
  flushQueuedSends();
48
- send({ type: 'remote.status' });
48
+ send({ type: 'remote.status', forceUpdate: true });
49
49
  };
50
50
 
51
51
  state.ws.onmessage = ({ data }) => {
@@ -364,10 +364,13 @@ function connect() {
364
364
  appendInstallLog(msg.text);
365
365
  break;
366
366
  case 'remote.install.done':
367
- handleInstallDone(msg.success);
367
+ handleInstallDone(msg);
368
368
  break;
369
369
  case 'remote.update':
370
370
  remoteUpdateInfo = msg?.available ? msg : null;
371
+ if (remoteUpdateInfo?.available && remoteInstalled && !remoteInstallMode) {
372
+ startRemoteInstall({ update: true, auto: true });
373
+ }
371
374
  if (remotePreflight?.pending) {
372
375
  remotePreflight.updateSeen = true;
373
376
  finishRemotePreflight();
@@ -1161,6 +1164,7 @@ let remoteStatsTimer = null;
1161
1164
  let remoteUpdateInfo = null;
1162
1165
  let remotePreflight = null;
1163
1166
  let remoteLastStatus = null;
1167
+ let remoteInstallMode = null;
1164
1168
 
1165
1169
  function startRemotePoll() {
1166
1170
  stopRemotePoll();
@@ -1192,15 +1196,6 @@ function showRemoteIntro(opts = {}) {
1192
1196
  setRemotePane('intro');
1193
1197
  }
1194
1198
 
1195
- function showRemoteUpdateRequired() {
1196
- showRemoteIntro({
1197
- title: 'Update Required',
1198
- text: `Version ${remoteUpdateInfo.latest} is available. Update CliDeck Remote to continue with mobile pairing on this machine.`,
1199
- foot: `Installed: <code class="text-slate-500">${esc(remoteUpdateInfo.installed)}</code> · Latest: <code class="text-slate-500">${esc(remoteUpdateInfo.latest)}</code>`,
1200
- button: 'Update to Continue',
1201
- });
1202
- }
1203
-
1204
1199
  function finishRemotePreflight() {
1205
1200
  if (!remotePreflight?.pending || !remotePreflight.statusSeen || !remotePreflight.updateSeen) return;
1206
1201
  remotePreflight = null;
@@ -1209,7 +1204,7 @@ function finishRemotePreflight() {
1209
1204
  return;
1210
1205
  }
1211
1206
  if (remoteUpdateInfo?.available) {
1212
- showRemoteUpdateRequired();
1207
+ startRemoteInstall({ update: true, auto: true });
1213
1208
  return;
1214
1209
  }
1215
1210
  if (remoteState === 'idle') {
@@ -1368,8 +1363,9 @@ function handleRemoteStatus(msg) {
1368
1363
  stopRemotePoll();
1369
1364
  if (wasPaired) { stopRemoteStats(); setRemoteLock(false); }
1370
1365
  }
1371
- if (remoteUpdateInfo?.available && remoteModalOpen) {
1372
- showRemoteUpdateRequired();
1366
+ if (remoteUpdateInfo?.available && remoteInstalled && !remoteInstallMode) {
1367
+ startRemoteInstall({ update: true, auto: true });
1368
+ return;
1373
1369
  }
1374
1370
  updateRemoteButton();
1375
1371
  if (remotePreflight?.pending) {
@@ -1387,8 +1383,8 @@ function handleRemotePaired(msg) {
1387
1383
  else qrImg.classList.add('hidden');
1388
1384
  updateRemoteButton();
1389
1385
  startRemotePoll();
1390
- if (remoteUpdateInfo?.available && remoteModalOpen) {
1391
- showRemoteUpdateRequired();
1386
+ if (remoteUpdateInfo?.available && remoteInstalled && !remoteInstallMode) {
1387
+ startRemoteInstall({ update: true, auto: true });
1392
1388
  return;
1393
1389
  }
1394
1390
  if (remotePreflight?.pending) {
@@ -1422,17 +1418,39 @@ function appendInstallLog(text) {
1422
1418
  log.scrollTop = log.scrollHeight;
1423
1419
  }
1424
1420
 
1425
- function handleInstallDone(success) {
1421
+ function startRemoteInstall(opts = {}) {
1422
+ remoteInstallMode = { update: !!opts.update, auto: !!opts.auto };
1423
+ const log = document.getElementById('remote-install-log');
1424
+ log.textContent = '';
1425
+ if (remoteInstallMode.update) {
1426
+ appendInstallLog(`Updating clideck-remote to ${remoteUpdateInfo?.latest || 'latest'}...\n`);
1427
+ }
1428
+ setRemotePane('installing');
1429
+ if (!remoteModalOpen) openRemoteModal();
1430
+ send({ type: 'remote.install', update: remoteInstallMode.update });
1431
+ }
1432
+
1433
+ function handleInstallDone(msg) {
1434
+ const success = !!msg?.success;
1435
+ const wasUpdate = !!msg?.update || !!remoteInstallMode?.update;
1436
+ remoteInstallMode = null;
1426
1437
  if (success) {
1427
1438
  remoteInstalled = true;
1428
1439
  remoteUpdateInfo = null;
1440
+ if (wasUpdate) {
1441
+ remoteState = 'connecting';
1442
+ setRemotePane('connecting');
1443
+ send({ type: 'remote.status', forceUpdate: true });
1444
+ startRemotePoll();
1445
+ return;
1446
+ }
1429
1447
  // Installed — go straight to pairing
1430
1448
  remoteState = 'connecting';
1431
1449
  setRemotePane('connecting');
1432
1450
  send({ type: 'remote.pair' });
1433
1451
  } else {
1434
1452
  const log = document.getElementById('remote-install-log');
1435
- log.textContent += '\n— Install failed. Check permissions or run manually:\n npm install -g clideck-remote\n';
1453
+ log.textContent += `\n— ${msg?.error || 'Install failed'}. Check permissions or run manually:\n npm install -g clideck-remote\n`;
1436
1454
  log.scrollTop = log.scrollHeight;
1437
1455
  }
1438
1456
  }
@@ -1450,14 +1468,12 @@ btnRemote.addEventListener('click', () => {
1450
1468
  remotePreflight = { pending: true, statusSeen: false, updateSeen: false };
1451
1469
  setRemotePane('connecting');
1452
1470
  openRemoteModal();
1453
- send({ type: 'remote.status' });
1471
+ send({ type: 'remote.status', forceUpdate: true });
1454
1472
  });
1455
1473
 
1456
1474
  // Install button
1457
1475
  document.getElementById('remote-add').addEventListener('click', () => {
1458
- document.getElementById('remote-install-log').textContent = '';
1459
- setRemotePane('installing');
1460
- send({ type: 'remote.install' });
1476
+ startRemoteInstall({ update: !!(remoteInstalled && remoteUpdateInfo?.available) });
1461
1477
  });
1462
1478
 
1463
1479
  // Close / disconnect