clideck 1.31.12 → 1.31.14

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,28 @@ 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
+ const voicePlugin = plugins.getInfo().find(p => p.id === 'voice-input' && p.installed);
610
+ replyError(voicePlugin
611
+ ? 'Restart CliDeck so the Voice Input plugin update can finish loading.'
612
+ : 'Install the Voice Input plugin in CliDeck first.');
613
+ break;
614
+ }
615
+ if (typeof msg.audio !== 'string' || !msg.audio) {
616
+ replyError('No audio received.');
617
+ break;
618
+ }
619
+ plugins.invoke('voice-input', 'transcribeAudio', { audio: msg.audio })
620
+ .then(result => ws.send(JSON.stringify({ type: 'remote.voice.result', requestId, ...result })))
621
+ .catch(e => replyError(e.message || 'Voice transcription failed.'));
622
+ break;
623
+ }
624
+
605
625
  case 'remote.install': {
626
+ const update = !!msg.update;
606
627
  const proc = require('child_process').spawn('npm', ['install', '-g', 'clideck-remote'], {
607
628
  shell: true, stdio: ['ignore', 'pipe', 'pipe'],
608
629
  });
@@ -610,7 +631,19 @@ function onConnection(ws) {
610
631
  proc.stderr.on('data', d => ws.send(JSON.stringify({ type: 'remote.install.progress', text: d.toString() })));
611
632
  proc.on('close', code => {
612
633
  remoteUpdateCache = null;
613
- ws.send(JSON.stringify({ type: 'remote.install.done', success: code === 0 }));
634
+ if (code !== 0 || !update) {
635
+ ws.send(JSON.stringify({ type: 'remote.install.done', success: code === 0, update }));
636
+ return;
637
+ }
638
+ require('child_process').execFile('clideck-remote', ['restart', '--json'], { timeout: 10000, shell: process.platform === 'win32', env: remoteCliEnv() }, (err, stdout) => {
639
+ if (err) {
640
+ ws.send(JSON.stringify({ type: 'remote.install.done', success: false, update, error: err.message }));
641
+ return;
642
+ }
643
+ let restart = null;
644
+ try { restart = JSON.parse(stdout); } catch {}
645
+ ws.send(JSON.stringify({ type: 'remote.install.done', success: true, update, restart }));
646
+ });
614
647
  });
615
648
  break;
616
649
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clideck",
3
- "version": "1.31.12",
3
+ "version": "1.31.14",
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,8 +432,25 @@ 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?.();
448
+ const capabilitiesFor = (pluginId) => {
449
+ const prefix = `${pluginId}.`;
450
+ return [...backendHandlers.keys()]
451
+ .filter(k => k.startsWith(prefix))
452
+ .map(k => k.slice(prefix.length));
453
+ };
428
454
  const installed = [...plugins.values()].map(p => ({
429
455
  id: p.manifest.id,
430
456
  name: p.manifest.name,
@@ -436,6 +462,7 @@ function getInfo() {
436
462
  settingValues: cfg?.pluginSettings?.[p.manifest.id] || {},
437
463
  dynamicOptions: p.dynamicOptions || {},
438
464
  actions: p.actions,
465
+ capabilities: capabilitiesFor(p.manifest.id),
439
466
  hasClient: existsSync(join(p.dir, 'client.js')),
440
467
  bundled: BUNDLED_IDS.has(p.manifest.id),
441
468
  installed: true,
@@ -451,6 +478,7 @@ function getInfo() {
451
478
  settingValues: {},
452
479
  dynamicOptions: {},
453
480
  actions: [],
481
+ capabilities: [],
454
482
  hasClient: false,
455
483
  bundled: BUNDLED_IDS.has(u.manifest.id),
456
484
  installed: false,
@@ -561,6 +589,6 @@ module.exports = {
561
589
  PLUGINS_DIR, BUNDLED_IDS,
562
590
  init, shutdown,
563
591
  transformInput, notifyOutput, notifyStatus, notifyTranscript, notifyMenu, notifyConfig, clearStatus, isWorking, shouldAutoApproveMenu,
564
- handleMessage, updateSetting, getInfo, resolveFile, installPlugin, removePlugin,
592
+ handleMessage, hasCapability, invoke, updateSetting, getInfo, resolveFile, installPlugin, removePlugin,
565
593
  getPills, getPillLogs,
566
594
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "voice-input",
3
3
  "name": "Voice Input",
4
- "version": "1.2.0",
4
+ "version": "1.2.1",
5
5
  "author": "CliDeck",
6
6
  "description": "Dictate prompts with your voice using Whisper speech-to-text",
7
7
  "icon": "<svg class=\"w-4 h-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z\"/><path d=\"M19 10v2a7 7 0 0 1-14 0v-2\"/><line x1=\"12\" y1=\"19\" x2=\"12\" y2=\"23\"/><line x1=\"8\" y1=\"23\" x2=\"16\" y2=\"23\"/></svg>",
@@ -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/index.html CHANGED
@@ -355,7 +355,7 @@
355
355
  <div id="remote-modal" class="absolute inset-0 z-[260] bg-black/60 backdrop-blur-sm hidden items-center justify-center">
356
356
  <div style="background:var(--color-dialog);border:1px solid color-mix(in srgb, var(--color-muted) 40%, transparent);box-shadow:0 25px 60px -12px var(--color-shadow)" class="rounded-2xl w-[340px] flex flex-col overflow-hidden">
357
357
  <div class="px-5 py-3.5 flex items-center justify-between">
358
- <span class="text-[13px] font-semibold text-slate-200">Mobile Remote</span>
358
+ <span id="remote-modal-title" class="text-[13px] font-semibold text-slate-200">Mobile Remote</span>
359
359
  <button id="remote-close" class="w-6 h-6 flex items-center justify-center rounded-md text-slate-500 hover:text-slate-300 hover:bg-slate-700/50 transition-colors">
360
360
  <svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"/></svg>
361
361
  </button>
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();
@@ -1180,27 +1184,34 @@ function setRemotePane(pane) {
1180
1184
  }
1181
1185
  }
1182
1186
 
1187
+ function remoteTitle() {
1188
+ const version = state.remoteVersion && state.remoteVersion !== 'not installed'
1189
+ ? ` v${state.remoteVersion}`
1190
+ : '';
1191
+ return `Mobile Remote${version}`;
1192
+ }
1193
+
1194
+ function updateRemoteTitle() {
1195
+ const title = remoteTitle();
1196
+ const modalTitle = document.getElementById('remote-modal-title');
1197
+ const introTitle = document.getElementById('remote-intro-title');
1198
+ if (modalTitle) modalTitle.textContent = title;
1199
+ if (introTitle) introTitle.textContent = `CliDeck ${title}`;
1200
+ }
1201
+
1183
1202
  function showRemoteIntro(opts = {}) {
1184
1203
  const title = document.getElementById('remote-intro-title');
1185
1204
  const text = document.getElementById('remote-intro-text');
1186
1205
  const foot = document.getElementById('remote-intro-foot');
1187
1206
  const btn = document.getElementById('remote-add');
1188
- title.textContent = opts.title || 'CliDeck Mobile Remote';
1207
+ updateRemoteTitle();
1208
+ if (opts.title) title.textContent = opts.title;
1189
1209
  text.textContent = opts.text || 'Control your AI agents from your phone. See live status, send messages, and get notifications — all end-to-end encrypted.';
1190
1210
  foot.innerHTML = opts.foot || 'Installs the <code class="text-slate-500">clideck-remote</code> package via npm';
1191
1211
  btn.textContent = opts.button || 'Add to CliDeck';
1192
1212
  setRemotePane('intro');
1193
1213
  }
1194
1214
 
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
1215
  function finishRemotePreflight() {
1205
1216
  if (!remotePreflight?.pending || !remotePreflight.statusSeen || !remotePreflight.updateSeen) return;
1206
1217
  remotePreflight = null;
@@ -1209,7 +1220,7 @@ function finishRemotePreflight() {
1209
1220
  return;
1210
1221
  }
1211
1222
  if (remoteUpdateInfo?.available) {
1212
- showRemoteUpdateRequired();
1223
+ startRemoteInstall({ update: true, auto: true });
1213
1224
  return;
1214
1225
  }
1215
1226
  if (remoteState === 'idle') {
@@ -1241,6 +1252,7 @@ function finishRemotePreflight() {
1241
1252
  }
1242
1253
 
1243
1254
  function openRemoteModal() {
1255
+ updateRemoteTitle();
1244
1256
  remoteModalOpen = true;
1245
1257
  remoteModal.classList.remove('hidden');
1246
1258
  remoteModal.style.display = 'flex';
@@ -1331,6 +1343,7 @@ function handleRemoteStatus(msg) {
1331
1343
  remoteLastStatus = msg;
1332
1344
  remoteInstalled = !!msg.installed;
1333
1345
  state.remoteVersion = msg.version || (msg.installed ? null : 'not installed');
1346
+ updateRemoteTitle();
1334
1347
  updateVersionFooter();
1335
1348
  const wasPaired = remoteState === 'paired';
1336
1349
  const preflighting = !!remotePreflight?.pending;
@@ -1368,8 +1381,9 @@ function handleRemoteStatus(msg) {
1368
1381
  stopRemotePoll();
1369
1382
  if (wasPaired) { stopRemoteStats(); setRemoteLock(false); }
1370
1383
  }
1371
- if (remoteUpdateInfo?.available && remoteModalOpen) {
1372
- showRemoteUpdateRequired();
1384
+ if (remoteUpdateInfo?.available && remoteInstalled && !remoteInstallMode) {
1385
+ startRemoteInstall({ update: true, auto: true });
1386
+ return;
1373
1387
  }
1374
1388
  updateRemoteButton();
1375
1389
  if (remotePreflight?.pending) {
@@ -1387,8 +1401,8 @@ function handleRemotePaired(msg) {
1387
1401
  else qrImg.classList.add('hidden');
1388
1402
  updateRemoteButton();
1389
1403
  startRemotePoll();
1390
- if (remoteUpdateInfo?.available && remoteModalOpen) {
1391
- showRemoteUpdateRequired();
1404
+ if (remoteUpdateInfo?.available && remoteInstalled && !remoteInstallMode) {
1405
+ startRemoteInstall({ update: true, auto: true });
1392
1406
  return;
1393
1407
  }
1394
1408
  if (remotePreflight?.pending) {
@@ -1422,17 +1436,39 @@ function appendInstallLog(text) {
1422
1436
  log.scrollTop = log.scrollHeight;
1423
1437
  }
1424
1438
 
1425
- function handleInstallDone(success) {
1439
+ function startRemoteInstall(opts = {}) {
1440
+ remoteInstallMode = { update: !!opts.update, auto: !!opts.auto };
1441
+ const log = document.getElementById('remote-install-log');
1442
+ log.textContent = '';
1443
+ if (remoteInstallMode.update) {
1444
+ appendInstallLog(`Updating clideck-remote to ${remoteUpdateInfo?.latest || 'latest'}...\n`);
1445
+ }
1446
+ setRemotePane('installing');
1447
+ if (!remoteModalOpen) openRemoteModal();
1448
+ send({ type: 'remote.install', update: remoteInstallMode.update });
1449
+ }
1450
+
1451
+ function handleInstallDone(msg) {
1452
+ const success = !!msg?.success;
1453
+ const wasUpdate = !!msg?.update || !!remoteInstallMode?.update;
1454
+ remoteInstallMode = null;
1426
1455
  if (success) {
1427
1456
  remoteInstalled = true;
1428
1457
  remoteUpdateInfo = null;
1458
+ if (wasUpdate) {
1459
+ remoteState = 'connecting';
1460
+ setRemotePane('connecting');
1461
+ send({ type: 'remote.status', forceUpdate: true });
1462
+ startRemotePoll();
1463
+ return;
1464
+ }
1429
1465
  // Installed — go straight to pairing
1430
1466
  remoteState = 'connecting';
1431
1467
  setRemotePane('connecting');
1432
1468
  send({ type: 'remote.pair' });
1433
1469
  } else {
1434
1470
  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';
1471
+ log.textContent += `\n— ${msg?.error || 'Install failed'}. Check permissions or run manually:\n npm install -g clideck-remote\n`;
1436
1472
  log.scrollTop = log.scrollHeight;
1437
1473
  }
1438
1474
  }
@@ -1450,14 +1486,12 @@ btnRemote.addEventListener('click', () => {
1450
1486
  remotePreflight = { pending: true, statusSeen: false, updateSeen: false };
1451
1487
  setRemotePane('connecting');
1452
1488
  openRemoteModal();
1453
- send({ type: 'remote.status' });
1489
+ send({ type: 'remote.status', forceUpdate: true });
1454
1490
  });
1455
1491
 
1456
1492
  // Install button
1457
1493
  document.getElementById('remote-add').addEventListener('click', () => {
1458
- document.getElementById('remote-install-log').textContent = '';
1459
- setRemotePane('installing');
1460
- send({ type: 'remote.install' });
1494
+ startRemoteInstall({ update: !!(remoteInstalled && remoteUpdateInfo?.available) });
1461
1495
  });
1462
1496
 
1463
1497
  // Close / disconnect