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/README.md +7 -0
- package/agent-presets.json +7 -9
- package/bin/claude-hook.js +31 -0
- package/bin/gemini-hook.js +31 -0
- package/bin/notify-helper.js +9 -2
- package/codex-config.js +77 -0
- package/config.js +2 -0
- package/handlers.js +164 -56
- package/package.json +1 -2
- package/plugin-loader.js +176 -50
- package/plugins/autopilot/clideck-plugin.json +5 -3
- package/plugins/autopilot/index.js +24 -30
- package/plugins/autopilot/package.json +7 -0
- package/plugins/trim-clip/clideck-plugin.json +1 -0
- package/plugins/voice-input/clideck-plugin.json +1 -0
- package/public/index.html +2 -4
- package/public/js/app.js +62 -5
- package/public/js/creator.js +14 -2
- package/public/js/settings.js +8 -6
- package/public/js/terminals.js +126 -26
- package/public/js/toast.js +2 -17
- package/public/js/utils.js +9 -0
- package/public/tailwind.css +1 -1
- package/server.js +87 -21
- package/sessions.js +41 -8
- package/telemetry-receiver.js +83 -80
- package/tools/merge-jsonl-roles.mjs +182 -0
- package/transcript-builder.js +53 -0
- package/transcript-parser.js +135 -0
- package/transcript.js +115 -181
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
|
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
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
273
|
-
sessionStatus.
|
|
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
|
-
|
|
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.
|
|
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-
|
|
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
|
|
86
|
-
if (
|
|
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
|
|
206
|
-
if (
|
|
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-
|
|
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
|
-
|
|
434
|
-
if (
|
|
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
|
-
|
|
558
|
-
if (
|
|
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
|
|
656
|
-
if (!
|
|
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
|
});
|
|
@@ -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="
|
|
328
|
-
<option value="10"
|
|
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">
|