clideck 1.27.0 → 1.29.0

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/plugin-loader.js CHANGED
@@ -1,5 +1,9 @@
1
1
  const { readdirSync, readFileSync, existsSync, mkdirSync, cpSync, rmSync } = require('fs');
2
+ const { createHash } = require('crypto');
2
3
  const { join, sep } = require('path');
4
+ const { execFile: _execFile } = require('child_process');
5
+ // Windows needs shell:true for npm (it's npm.cmd, not a binary)
6
+ const npmExec = (args, opts, cb) => _execFile('npm', args, { ...opts, shell: process.platform === 'win32' }, cb);
3
7
  const { DATA_DIR } = require('./paths');
4
8
  const transcript = require('./transcript');
5
9
 
@@ -8,6 +12,7 @@ mkdirSync(PLUGINS_DIR, { recursive: true });
8
12
 
9
13
  // Seed bundled plugins — copy if missing, update if bundled version is newer
10
14
  const BUNDLED_DIR = join(__dirname, 'plugins');
15
+ const depsChanged = new Set(); // plugins whose install inputs changed — need reinstall
11
16
  if (existsSync(BUNDLED_DIR)) {
12
17
  for (const entry of readdirSync(BUNDLED_DIR, { withFileTypes: true })) {
13
18
  if (!entry.isDirectory()) continue;
@@ -21,7 +26,20 @@ if (existsSync(BUNDLED_DIR)) {
21
26
  const installedManifestFile = existsSync(join(target, 'clideck-plugin.json')) ? join(target, 'clideck-plugin.json') : join(target, 'termix-plugin.json');
22
27
  const installedManifest = JSON.parse(readFileSync(installedManifestFile, 'utf8'));
23
28
  if (bundledManifest.version !== installedManifest.version) {
29
+ // Check if install inputs changed before copying
30
+ let needsReinstall = false;
31
+ if (bundledManifest.install) {
32
+ const installHash = (dir) => {
33
+ const h = createHash('sha256');
34
+ for (const f of ['package.json', 'package-lock.json']) {
35
+ try { h.update(readFileSync(join(dir, f))); } catch {}
36
+ }
37
+ return h.digest('hex');
38
+ };
39
+ needsReinstall = installHash(target) !== installHash(join(BUNDLED_DIR, entry.name));
40
+ }
24
41
  cpSync(join(BUNDLED_DIR, entry.name), target, { recursive: true });
42
+ if (needsReinstall) depsChanged.add(bundledManifest.id || entry.name);
25
43
  console.log(`[plugin] updated ${entry.name} ${installedManifest.version} → ${bundledManifest.version}`);
26
44
  }
27
45
  } catch {}
@@ -30,11 +48,13 @@ if (existsSync(BUNDLED_DIR)) {
30
48
  }
31
49
 
32
50
  const plugins = new Map();
51
+ const uninstalledPlugins = new Map(); // id → { manifest, dir }
33
52
  const inputHooks = [];
34
53
  const outputHooks = [];
35
54
  const statusHooks = [];
36
55
  const transcriptHooks = [];
37
56
  const menuHooks = [];
57
+ const configHooks = [];
38
58
  const sessionStatus = new Map(); // sessionId → boolean (dedup multi-client reports)
39
59
  const autoApproveMenus = new Set(); // sessionIds where menus should be auto-approved
40
60
  const frontendHandlers = new Map();
@@ -49,7 +69,7 @@ const settingsChangeHandlers = new Map(); // pluginId → [fn]
49
69
  const sessionPills = new Map(); // pillId → { pluginId, id, title, projectId, working, statusText, icon, logs[] }
50
70
 
51
71
  function removeHooks(pluginId) {
52
- for (const arr of [inputHooks, outputHooks, statusHooks, transcriptHooks, menuHooks]) {
72
+ for (const arr of [inputHooks, outputHooks, statusHooks, transcriptHooks, menuHooks, configHooks]) {
53
73
  for (let i = arr.length - 1; i >= 0; i--) {
54
74
  if (arr[i].pluginId === pluginId) arr.splice(i, 1);
55
75
  }
@@ -66,6 +86,57 @@ function removeHooks(pluginId) {
66
86
  }
67
87
  }
68
88
 
89
+ // Check if a plugin with install: "npm" has been installed.
90
+ // Config is the source of truth, but we self-correct if node_modules is missing.
91
+ function isInstalled(dir, manifest) {
92
+ if (!manifest.install) return true; // no install step declared
93
+ const cfg = getConfigFn?.();
94
+ if (!cfg?.pluginInstalled?.[manifest.id]) return false;
95
+ // Self-correct: config says installed but files are gone
96
+ if (!existsSync(join(dir, 'node_modules'))) {
97
+ console.log(`[plugin] ${manifest.name}: node_modules missing, resetting install state`);
98
+ delete cfg.pluginInstalled[manifest.id];
99
+ saveConfigFn?.(cfg);
100
+ return false;
101
+ }
102
+ return true;
103
+ }
104
+
105
+ function readManifest(dir, name) {
106
+ let manifest = { id: name, name, version: '0.0.0' };
107
+ const manifestFile = existsSync(join(dir, 'clideck-plugin.json')) ? join(dir, 'clideck-plugin.json') : join(dir, 'termix-plugin.json');
108
+ if (existsSync(manifestFile)) {
109
+ try { manifest = { ...manifest, ...JSON.parse(readFileSync(manifestFile, 'utf8')) }; }
110
+ catch (e) { console.error(`[plugin:${name}] bad manifest: ${e.message}`); return null; }
111
+ }
112
+ if (manifest.settings != null) {
113
+ if (!Array.isArray(manifest.settings)) {
114
+ console.error(`[plugin:${name}] manifest.settings must be an array, ignoring`);
115
+ manifest.settings = [];
116
+ } else {
117
+ manifest.settings = manifest.settings.filter(s =>
118
+ s && typeof s === 'object' && typeof s.key === 'string' && s.key
119
+ );
120
+ }
121
+ }
122
+ return manifest;
123
+ }
124
+
125
+ function loadPlugin(manifest, dir) {
126
+ if (plugins.has(manifest.id)) return;
127
+ const state = { manifest, dir, shutdownFns: [], actions: [], dynamicOptions: {} };
128
+ plugins.set(manifest.id, state);
129
+ try {
130
+ const mod = require(join(dir, 'index.js'));
131
+ if (typeof mod.init === 'function') mod.init(buildApi(manifest.id, dir, state));
132
+ console.log(`[plugin] ${manifest.name} v${manifest.version}`);
133
+ } catch (e) {
134
+ console.error(`[plugin:${manifest.id}] init failed: ${e.message}`);
135
+ removeHooks(manifest.id);
136
+ plugins.delete(manifest.id);
137
+ }
138
+ }
139
+
69
140
  function init(broadcast, getSessions, getConfig, saveConfig, sessionInput, createProgrammatic, closeSession) {
70
141
  broadcastFn = broadcast;
71
142
  sessionsFn = getSessions;
@@ -75,47 +146,40 @@ function init(broadcast, getSessions, getConfig, saveConfig, sessionInput, creat
75
146
  createSessionFn = createProgrammatic;
76
147
  closeSessionFn = closeSession;
77
148
 
149
+ // Clear install state only for bundled plugins whose dependencies changed
150
+ if (depsChanged.size) {
151
+ const cfg = getConfig();
152
+ if (cfg?.pluginInstalled) {
153
+ for (const id of depsChanged) {
154
+ if (cfg.pluginInstalled[id]) {
155
+ delete cfg.pluginInstalled[id];
156
+ console.log(`[plugin] install inputs changed, cleared install state for ${id}`);
157
+ }
158
+ }
159
+ saveConfig(cfg);
160
+ }
161
+ }
162
+
78
163
  for (const entry of readdirSync(PLUGINS_DIR, { withFileTypes: true })) {
79
164
  if (!entry.isDirectory()) continue;
80
165
  const dir = join(PLUGINS_DIR, entry.name);
81
- const entryFile = join(dir, 'index.js');
82
- if (!existsSync(entryFile)) continue;
83
-
84
- let manifest = { id: entry.name, name: entry.name, version: '0.0.0' };
85
- const manifestFile = existsSync(join(dir, 'clideck-plugin.json')) ? join(dir, 'clideck-plugin.json') : join(dir, 'termix-plugin.json');
86
- if (existsSync(manifestFile)) {
87
- try { manifest = { ...manifest, ...JSON.parse(readFileSync(manifestFile, 'utf8')) }; }
88
- catch (e) { console.error(`[plugin:${entry.name}] bad manifest: ${e.message}`); continue; }
89
- }
90
- // Validate settings shape
91
- if (manifest.settings != null) {
92
- if (!Array.isArray(manifest.settings)) {
93
- console.error(`[plugin:${entry.name}] manifest.settings must be an array, ignoring`);
94
- manifest.settings = [];
95
- } else {
96
- manifest.settings = manifest.settings.filter(s =>
97
- s && typeof s === 'object' && typeof s.key === 'string' && s.key
98
- );
99
- }
100
- }
166
+ if (!existsSync(join(dir, 'index.js'))) continue;
101
167
 
102
- if (plugins.has(manifest.id)) {
168
+ const manifest = readManifest(dir, entry.name);
169
+ if (!manifest) continue;
170
+
171
+ if (plugins.has(manifest.id) || uninstalledPlugins.has(manifest.id)) {
103
172
  console.error(`[plugin:${manifest.id}] duplicate ID, skipping ${dir}`);
104
173
  continue;
105
174
  }
106
175
 
107
- const state = { manifest, dir, shutdownFns: [], actions: [], dynamicOptions: {} };
108
- plugins.set(manifest.id, state);
109
-
110
- try {
111
- const mod = require(entryFile);
112
- if (typeof mod.init === 'function') mod.init(buildApi(manifest.id, dir, state));
113
- console.log(`[plugin] ${manifest.name} v${manifest.version}`);
114
- } catch (e) {
115
- console.error(`[plugin:${manifest.id}] init failed: ${e.message}`);
116
- removeHooks(manifest.id);
117
- plugins.delete(manifest.id);
176
+ if (!isInstalled(dir, manifest)) {
177
+ uninstalledPlugins.set(manifest.id, { manifest, dir });
178
+ console.log(`[plugin] ${manifest.name} v${manifest.version} (not installed)`);
179
+ continue;
118
180
  }
181
+
182
+ loadPlugin(manifest, dir);
119
183
  }
120
184
  }
121
185
 
@@ -130,6 +194,7 @@ function buildApi(pluginId, pluginDir, state) {
130
194
  onStatusChange(fn) { statusHooks.push({ pluginId, fn }); },
131
195
  onTranscriptEntry(fn) { transcriptHooks.push({ pluginId, fn }); },
132
196
  onMenuDetected(fn) { menuHooks.push({ pluginId, fn }); },
197
+ onConfigChange(fn) { configHooks.push({ pluginId, fn }); },
133
198
 
134
199
  sendToFrontend(event, data = {}) {
135
200
  broadcastFn?.({ ...data, type: `plugin.${pluginId}.${event}` });
@@ -169,8 +234,6 @@ function buildApi(pluginId, pluginDir, state) {
169
234
  getRoles() { return JSON.parse(JSON.stringify(getConfigFn?.()?.roles || [])); },
170
235
  getProjects() { return JSON.parse(JSON.stringify(getConfigFn?.()?.projects || [])); },
171
236
  getTranscript(id, n) { return transcript.getLastTurns(id, n || 20); },
172
- getScreenTurns(id, agent, opts) { return transcript.getScreenTurns(id, agent, opts); },
173
- getScreen(id) { return transcript.getScreen(id); },
174
237
  detectMenu(lines, presetId) { return transcript.detectMenu(lines, presetId); },
175
238
 
176
239
  addToolbarAction(opts) { state.actions.push({ ...opts, pluginId, slot: 'toolbar' }); },
@@ -232,17 +295,19 @@ function buildApi(pluginId, pluginDir, state) {
232
295
  },
233
296
 
234
297
  resolve(specifier) {
235
- // Resolve a package from the app's node_modules. Intended for bundled
236
- // plugins that ship with CliDeck and can rely on app-level dependencies.
237
- // Third-party plugins should bundle their own deps or be self-contained.
238
- try { return require.resolve(specifier); } catch {}
298
+ // Resolve from plugin-local node_modules first, then app-level
239
299
  const parts = specifier.startsWith('@') ? specifier.split('/').slice(0, 2) : [specifier.split('/')[0]];
240
- const pkgDir = join(__dirname, 'node_modules', ...parts);
241
- const pkg = JSON.parse(readFileSync(join(pkgDir, 'package.json'), 'utf8'));
242
- const entry = typeof pkg.exports === 'string' ? pkg.exports
243
- : pkg.exports?.['.']?.import || pkg.exports?.['.']?.default || pkg.exports?.['.']
244
- || pkg.module || pkg.main || 'index.js';
245
- return join(pkgDir, entry);
300
+ for (const base of [join(pluginDir, 'node_modules'), join(__dirname, 'node_modules')]) {
301
+ const pkgDir = join(base, ...parts);
302
+ try {
303
+ const pkg = JSON.parse(readFileSync(join(pkgDir, 'package.json'), 'utf8'));
304
+ const entry = typeof pkg.exports === 'string' ? pkg.exports
305
+ : pkg.exports?.['.']?.import || pkg.exports?.['.']?.default || pkg.exports?.['.']
306
+ || pkg.module || pkg.main || 'index.js';
307
+ return join(pkgDir, entry);
308
+ } catch {}
309
+ }
310
+ return require.resolve(specifier);
246
311
  },
247
312
  onShutdown(fn) { state.shutdownFns.push(fn); },
248
313
  log(msg) { console.log(`[plugin:${pluginId}] ${msg}`); },
@@ -269,8 +334,9 @@ function notifyOutput(id, data) {
269
334
  }
270
335
 
271
336
  function notifyStatus(id, working, source) {
272
- if (sessionStatus.get(id) === working) return;
273
- sessionStatus.set(id, working);
337
+ const next = `${working ? 1 : 0}:${source || ''}`;
338
+ if (sessionStatus.get(id) === next) return;
339
+ sessionStatus.set(id, next);
274
340
  for (const h of statusHooks) {
275
341
  try { h.fn(id, working, source); }
276
342
  catch (e) { console.error(`[plugin:${h.pluginId}] status error: ${e.message}`); }
@@ -291,6 +357,13 @@ function notifyMenu(id, choices) {
291
357
  }
292
358
  }
293
359
 
360
+ function notifyConfig(cfg) {
361
+ for (const h of configHooks) {
362
+ try { h.fn(cfg); }
363
+ catch (e) { console.error(`[plugin:${h.pluginId}] config error: ${e.message}`); }
364
+ }
365
+ }
366
+
294
367
 
295
368
  function updateSetting(pluginId, key, value) {
296
369
  // Validate plugin exists (also prevents __proto__ pollution — Map lookup returns undefined)
@@ -352,19 +425,37 @@ function handleMessage(msg) {
352
425
 
353
426
  function getInfo() {
354
427
  const cfg = getConfigFn?.();
355
- return [...plugins.values()].map(p => ({
428
+ const installed = [...plugins.values()].map(p => ({
356
429
  id: p.manifest.id,
357
430
  name: p.manifest.name,
358
431
  version: p.manifest.version,
359
432
  author: p.manifest.author || '',
360
433
  description: p.manifest.description || '',
434
+ icon: p.manifest.icon || '',
361
435
  settings: p.manifest.settings || [],
362
436
  settingValues: cfg?.pluginSettings?.[p.manifest.id] || {},
363
437
  dynamicOptions: p.dynamicOptions || {},
364
438
  actions: p.actions,
365
439
  hasClient: existsSync(join(p.dir, 'client.js')),
366
440
  bundled: BUNDLED_IDS.has(p.manifest.id),
441
+ installed: true,
442
+ }));
443
+ const pending = [...uninstalledPlugins.values()].map(u => ({
444
+ id: u.manifest.id,
445
+ name: u.manifest.name,
446
+ version: u.manifest.version,
447
+ author: u.manifest.author || '',
448
+ description: u.manifest.description || '',
449
+ icon: u.manifest.icon || '',
450
+ settings: [],
451
+ settingValues: {},
452
+ dynamicOptions: {},
453
+ actions: [],
454
+ hasClient: false,
455
+ bundled: BUNDLED_IDS.has(u.manifest.id),
456
+ installed: false,
367
457
  }));
458
+ return [...installed, ...pending];
368
459
  }
369
460
 
370
461
  function resolveFile(urlPath) {
@@ -413,6 +504,35 @@ const BUNDLED_IDS = new Set(
413
504
  : []
414
505
  );
415
506
 
507
+ function installPlugin(pluginId, callback) {
508
+ const entry = uninstalledPlugins.get(pluginId);
509
+ if (!entry) return callback(new Error('Plugin not found or already installed'));
510
+ const { manifest, dir } = entry;
511
+ if (manifest.install !== 'npm') return callback(new Error(`Unknown install type: ${manifest.install}`));
512
+ console.log(`[plugin] installing ${manifest.name}...`);
513
+ npmExec(['install', '--production'], { cwd: dir, timeout: 120000 }, (err) => {
514
+ if (err) {
515
+ console.error(`[plugin:${pluginId}] install failed: ${err.message}`);
516
+ return callback(err);
517
+ }
518
+ uninstalledPlugins.delete(pluginId);
519
+ loadPlugin(manifest, dir);
520
+ // Only persist install state if plugin actually loaded
521
+ if (!plugins.has(pluginId)) {
522
+ uninstalledPlugins.set(pluginId, { manifest, dir });
523
+ return callback(new Error('Plugin installed but failed to load'));
524
+ }
525
+ const cfg = getConfigFn?.();
526
+ if (cfg) {
527
+ if (!cfg.pluginInstalled) cfg.pluginInstalled = {};
528
+ cfg.pluginInstalled[pluginId] = true;
529
+ saveConfigFn?.(cfg);
530
+ }
531
+ console.log(`[plugin] ${manifest.name} installed`);
532
+ callback(null);
533
+ });
534
+ }
535
+
416
536
  function removePlugin(pluginId) {
417
537
  if (BUNDLED_IDS.has(pluginId)) return { success: false, message: 'Cannot remove a built-in plugin' };
418
538
  const state = plugins.get(pluginId);
@@ -427,6 +547,12 @@ function removePlugin(pluginId) {
427
547
  for (const fn of state.shutdownFns) { try { fn(); } catch {} }
428
548
  removeHooks(pluginId);
429
549
  plugins.delete(pluginId);
550
+ // Clear persisted install state
551
+ const cfg = getConfigFn?.();
552
+ if (cfg?.pluginInstalled?.[pluginId]) {
553
+ delete cfg.pluginInstalled[pluginId];
554
+ saveConfigFn?.(cfg);
555
+ }
430
556
  console.log(`[plugin] removed ${pluginId}`);
431
557
  return { success: true };
432
558
  }
@@ -434,7 +560,7 @@ function removePlugin(pluginId) {
434
560
  module.exports = {
435
561
  PLUGINS_DIR, BUNDLED_IDS,
436
562
  init, shutdown,
437
- transformInput, notifyOutput, notifyStatus, notifyTranscript, notifyMenu, clearStatus, isWorking, shouldAutoApproveMenu,
438
- handleMessage, updateSetting, getInfo, resolveFile, removePlugin,
563
+ transformInput, notifyOutput, notifyStatus, notifyTranscript, notifyMenu, notifyConfig, clearStatus, isWorking, shouldAutoApproveMenu,
564
+ handleMessage, updateSetting, getInfo, resolveFile, installPlugin, removePlugin,
439
565
  getPills, getPillLogs,
440
566
  };
@@ -1,9 +1,11 @@
1
1
  {
2
2
  "id": "autopilot",
3
3
  "name": "Autopilot",
4
- "version": "0.14.4",
4
+ "version": "0.16.0",
5
5
  "author": "CliDeck",
6
- "description": "Multi-agent orchestration — routes output between agents automatically",
6
+ "description": "Multi-agent orchestration — routes output between agents automatically. Uses ~50 output tokens per routing decision.",
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\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 6v6l4 2\"/></svg>",
8
+ "install": "npm",
7
9
  "settings": [
8
10
  {
9
11
  "key": "enabled",
@@ -31,7 +33,7 @@
31
33
  "key": "model",
32
34
  "label": "Model",
33
35
  "type": "dynamic-select",
34
- "default": "claude-haiku-4-5"
36
+ "default": "claude-opus-4-6"
35
37
  },
36
38
  {
37
39
  "key": "apiKey",
@@ -75,6 +75,12 @@ function addTokens(pid, usage) {
75
75
  api.sendToFrontend('tokens', { projectId: pid, ...u });
76
76
  }
77
77
 
78
+ function latestAgentOutput(id) {
79
+ const turns = api.getTranscript(id, 20);
80
+ const last = [...turns].reverse().find(t => t.role === 'agent');
81
+ return last?.text?.trim().slice(0, 8000) || null;
82
+ }
83
+
78
84
  // --- Consumed state (persisted per-project: role → boolean) ---
79
85
 
80
86
  function captureIdleOutput(id, pid, proj) {
@@ -82,9 +88,8 @@ function captureIdleOutput(id, pid, proj) {
82
88
  if (proj.status.get(id)) return;
83
89
  const w = proj.workers.get(id);
84
90
  if (w) {
85
- const turns = api.getScreenTurns(id, w.presetId, { raw: true });
86
- if (turns?.length && turns[turns.length - 1].role === 'agent') {
87
- const out = turns[turns.length - 1].text.trim().slice(0, 8000);
91
+ const out = latestAgentOutput(id);
92
+ if (out) {
88
93
  const oid = outputId(out);
89
94
  const prev = proj.lastOutput.get(id);
90
95
  const isNew = !prev || prev.outputId !== oid;
@@ -200,13 +205,9 @@ function refreshWorkers(pid, proj) {
200
205
  proj.workers.set(sid, w);
201
206
  proj.status.set(sid, liveStatus.get(sid));
202
207
  api.setAutoApproveMenu(sid, true);
203
- // Seed output for idle new workers from .screen
204
208
  if (!liveStatus.get(sid)) {
205
- const turns = api.getScreenTurns(sid, w.presetId, { raw: true });
206
- if (turns?.length && turns[turns.length - 1].role === 'agent') {
207
- const text = turns[turns.length - 1].text.trim().slice(0, 8000);
208
- proj.lastOutput.set(sid, { text, capturedAt: Date.now(), outputId: outputId(text) });
209
- }
209
+ const text = latestAgentOutput(sid);
210
+ if (text) proj.lastOutput.set(sid, { text, capturedAt: Date.now(), outputId: outputId(text) });
210
211
  }
211
212
  }
212
213
  }
@@ -398,7 +399,7 @@ async function consult(pid, proj) {
398
399
  }
399
400
 
400
401
  const provider = api.getSetting('provider') || 'anthropic';
401
- const modelId = api.getSetting('model') || 'claude-haiku-4-5';
402
+ const modelId = api.getSetting('model') || 'claude-opus-4-6';
402
403
  const apiKey = api.getSetting('apiKey') || m.getEnvApiKey(provider) || '';
403
404
 
404
405
  if (!apiKey) {
@@ -430,11 +431,8 @@ async function consult(pid, proj) {
430
431
  let capturedAt = stored?.capturedAt || 0;
431
432
  let oid = stored?.outputId || null;
432
433
  if (!text) {
433
- const turns = api.getScreenTurns(sid, w.presetId, { raw: true });
434
- if (turns?.length) {
435
- const last = [...turns].reverse().find(t => t.role === 'agent');
436
- if (last) { text = last.text.trim().slice(0, 8000); capturedAt = capturedAt || Date.now(); oid = outputId(text); }
437
- }
434
+ text = latestAgentOutput(sid);
435
+ if (text) { capturedAt = capturedAt || Date.now(); oid = outputId(text); }
438
436
  }
439
437
  entries.push({ sid, role: w.role, text, capturedAt, outputId: oid });
440
438
  }
@@ -554,14 +552,8 @@ function executeAction(pid, proj, action, args, pillId) {
554
552
  let out = stored?.text || null;
555
553
  let oid = stored?.outputId || null;
556
554
  if (!out && src) {
557
- const w = proj.workers.get(src);
558
- if (w) {
559
- const turns = api.getScreenTurns(src, w.presetId, { raw: true });
560
- if (turns?.length && turns[turns.length - 1].role === 'agent') {
561
- out = turns[turns.length - 1].text.trim().slice(0, 8000);
562
- oid = outputId(out);
563
- }
564
- }
555
+ out = latestAgentOutput(src);
556
+ if (out) oid = outputId(out);
565
557
  }
566
558
  if (!out) return `"${args.from}" has no output to route`;
567
559
 
@@ -616,10 +608,14 @@ function executeAction(pid, proj, action, args, pillId) {
616
608
 
617
609
  // --- Lifecycle ---
618
610
 
619
- function start(pid) {
611
+ async function start(pid) {
620
612
  if (projects.has(pid)) return { error: 'Already running' };
621
613
  if (!enabled()) return { error: 'Autopilot disabled' };
622
614
 
615
+ const provider = api.getSetting('provider') || 'anthropic';
616
+ const apiKey = api.getSetting('apiKey') || (await ai()).getEnvApiKey(provider) || '';
617
+ if (!apiKey) return { error: 'Set the API key in Autopilot settings (Plugins panel)' };
618
+
623
619
  const { workers, status } = discoverWorkers(pid);
624
620
  if (workers.size < 1) return { error: 'No agents with roles in this project' };
625
621
 
@@ -649,12 +645,10 @@ function start(pid) {
649
645
  // Flag all workers for core menu auto-approve
650
646
  for (const [sid] of workers) api.setAutoApproveMenu(sid, true);
651
647
 
652
- // Seed lastOutput from .screen for idle workers
653
648
  for (const [sid, w] of workers) {
654
649
  if (status.get(sid)) continue;
655
- const turns = api.getScreenTurns(sid, w.presetId, { raw: true });
656
- if (!turns?.length || turns[turns.length - 1].role !== 'agent') continue;
657
- const text = turns[turns.length - 1].text.trim().slice(0, 8000);
650
+ const text = latestAgentOutput(sid);
651
+ if (!text) continue;
658
652
  proj.lastOutput.set(sid, { text, capturedAt: Date.now(), outputId: outputId(text) });
659
653
  }
660
654
 
@@ -700,13 +694,13 @@ module.exports.init = function (pluginApi) {
700
694
  });
701
695
 
702
696
  // Toggle on/off
703
- api.onFrontendMessage('autopilot-toggle', (msg) => {
697
+ api.onFrontendMessage('autopilot-toggle', async (msg) => {
704
698
  if (projects.has(msg.projectId)) {
705
699
  stop(msg.projectId);
706
700
  } else {
707
701
  // Remove lingering pill from a previous finished run
708
702
  api.removeSessionPill(`autopilot-${msg.projectId}`);
709
- const r = start(msg.projectId);
703
+ const r = await start(msg.projectId);
710
704
  if (r.error) api.sendToFrontend('error', { msg: r.error });
711
705
  }
712
706
  });
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "clideck-plugin-autopilot",
3
+ "private": true,
4
+ "dependencies": {
5
+ "@mariozechner/pi-ai": ">=0.62.0"
6
+ }
7
+ }
@@ -4,6 +4,7 @@
4
4
  "version": "1.2.0",
5
5
  "author": "CliDeck",
6
6
  "description": "Copy selected terminal text with trailing whitespace trimmed",
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\"><circle cx=\"6\" cy=\"6\" r=\"3\"/><circle cx=\"6\" cy=\"18\" r=\"3\"/><line x1=\"20\" y1=\"4\" x2=\"8.12\" y2=\"15.88\"/><line x1=\"14.47\" y1=\"14.48\" x2=\"20\" y2=\"20\"/><line x1=\"8.12\" y1=\"8.12\" x2=\"12\" y2=\"12\"/></svg>",
7
8
  "settings": [
8
9
  {
9
10
  "key": "enabled",
@@ -4,6 +4,7 @@
4
4
  "version": "1.2.0",
5
5
  "author": "CliDeck",
6
6
  "description": "Dictate prompts with your voice using Whisper speech-to-text",
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>",
7
8
  "settings": [
8
9
  {
9
10
  "key": "enabled",
package/public/index.html CHANGED
@@ -324,11 +324,9 @@
324
324
  <div class="mb-5">
325
325
  <label class="block text-xs text-slate-400 mb-1.5 ml-6">Minimum working time before notifying</label>
326
326
  <select id="cfg-notify-min-work" class="ml-6 px-3 py-1.5 text-sm bg-slate-800 border border-slate-600 rounded-md text-slate-200 outline-none focus:border-blue-500 transition-colors cursor-pointer">
327
- <option value="5">5 seconds</option>
328
- <option value="10" selected>10 seconds</option>
329
- <option value="20">20 seconds</option>
327
+ <option value="0" selected>Always</option>
328
+ <option value="10">10 seconds</option>
330
329
  <option value="30">30 seconds</option>
331
- <option value="60">60 seconds</option>
332
330
  </select>
333
331
  </div>
334
332
  <div class="mt-6 px-4 py-3 rounded-lg bg-slate-800/50 border border-slate-700/40">