clideck 1.25.8 → 1.26.2
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 +17 -4
- package/activity.js +15 -1
- package/config.js +74 -1
- package/handlers.js +14 -3
- package/package.json +2 -1
- package/plugin-loader.js +128 -11
- package/plugins/autopilot/clideck-plugin.json +52 -0
- package/plugins/autopilot/client.js +84 -0
- package/plugins/autopilot/index.js +797 -0
- package/plugins/autopilot/prompt.md +68 -0
- package/public/index.html +11 -3
- package/public/js/app.js +73 -6
- package/public/js/creator.js +72 -6
- package/public/js/nav.js +2 -2
- package/public/js/prompts.js +1 -1
- package/public/js/roles.js +112 -0
- package/public/js/state.js +2 -0
- package/public/js/terminals.js +219 -2
- package/public/js/toast.js +28 -9
- package/public/tailwind.css +1 -1
- package/server.js +7 -4
- package/sessions.js +89 -6
- package/telemetry-receiver.js +76 -41
- package/transcript.js +15 -3
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
> **Formerly `termix-cli`** — if you arrived here from an old link, you're in the right place. The project has been renamed to **CliDeck**. Update your install: `npm install -g clideck`
|
|
6
6
|
|
|
7
|
-
Manage your AI agents like WhatsApp chats.
|
|
7
|
+
Manage your AI agents like WhatsApp chats. Assign roles, let Autopilot route work between them, check in from your phone.
|
|
8
8
|
|
|
9
9
|
[Documentation](https://docs.clideck.dev/) | [Video Demo](https://youtu.be/hICrtjGAeDk) | [Website](https://clideck.dev/)
|
|
10
10
|
|
|
@@ -14,6 +14,8 @@ You run Claude Code, Codex, Gemini CLI in separate terminals. You alt-tab betwee
|
|
|
14
14
|
|
|
15
15
|
clideck puts all your agents in one screen — a sidebar with every session, live status, last message preview, and timestamps. Click a session, you're in its terminal. Exactly like switching between chats.
|
|
16
16
|
|
|
17
|
+
Give each agent a role (Programmer, Reviewer, Product Manager), turn on Autopilot, and walk away — it routes output between agents automatically until the task is done or it needs you. Check progress from your phone with a QR scan.
|
|
18
|
+
|
|
17
19
|
Native terminals. Your keystrokes go straight to the agent, nothing in between. clideck never reads your prompts or output.
|
|
18
20
|
|
|
19
21
|
## Quick Start
|
|
@@ -22,7 +24,9 @@ Native terminals. Your keystrokes go straight to the agent, nothing in between.
|
|
|
22
24
|
npx clideck
|
|
23
25
|
```
|
|
24
26
|
|
|
25
|
-
Open [http://localhost:4000](http://localhost:4000). Click **+**, pick an agent, start working.
|
|
27
|
+
Open [http://localhost:4000](http://localhost:4000). Click **+**, pick an agent and optionally a project and role, start working.
|
|
28
|
+
|
|
29
|
+
New users get 3 built-in demo roles (Programmer, Reviewer, Product Manager) and 3 starter prompts in the prompt library to get going quickly.
|
|
26
30
|
|
|
27
31
|
Or install globally:
|
|
28
32
|
|
|
@@ -33,6 +37,9 @@ clideck
|
|
|
33
37
|
|
|
34
38
|
## What You Get
|
|
35
39
|
|
|
40
|
+
- **Roles** — define reusable agent identities (Programmer, Reviewer, PM) and assign them when creating sessions. Instructions are injected into the agent automatically.
|
|
41
|
+
- **Autopilot** — project-level workflow routing. Watches your role-assigned agents, waits for them to finish, forwards output to the next specialist. Fingerprints each output, tracks handoff history, and guards against repeat loops. Works with any LLM provider (Anthropic, OpenAI, Google, Groq, xAI, Mistral, and more). Notifies you when work is complete or blocked.
|
|
42
|
+
- **Mobile access** — check on your agents from your phone with a QR scan. E2E encrypted.
|
|
36
43
|
- **Live working/idle status** — see which agent is thinking and which is waiting for you, without checking each terminal
|
|
37
44
|
- **Session resume** — close clideck, reopen it tomorrow, pick up where you left off
|
|
38
45
|
- **Notifications** — browser and sound alerts when an agent finishes or needs input
|
|
@@ -40,12 +47,16 @@ clideck
|
|
|
40
47
|
- **Projects** — group sessions by project with drag-and-drop
|
|
41
48
|
- **Search** — find any session by name or scroll back through transcript content
|
|
42
49
|
- **Prompt Library** — save reusable prompts, type `//` in any terminal to paste them
|
|
43
|
-
- **Plugins** —
|
|
50
|
+
- **Plugins** — full server + client API with hooks for input, output, status, transcript, and menus. Programmatic session control, toolbar and project actions, session pills, and a settings UI. Ships with Voice Input, Trim Clip, and Autopilot — or build your own.
|
|
44
51
|
- **15 themes** — dark and light, plus custom theme support
|
|
45
52
|
|
|
46
53
|
## Mobile Access
|
|
47
54
|
|
|
48
|
-
|
|
55
|
+
Start a task on your laptop, walk away, check progress from your phone. See who's working, who's idle, who needs input. Send messages, answer choice menus, browse conversation history, and resume sessions — all from the browser on your phone.
|
|
56
|
+
|
|
57
|
+
Pair with one QR scan, no account needed. End-to-end encrypted with AES-256-GCM — the relay sees only opaque blobs. Your code never leaves your machines.
|
|
58
|
+
|
|
59
|
+
Mobile access is provided by [`clideck-remote`](https://www.npmjs.com/package/clideck-remote), a separate optional package. Install it with `npm install -g clideck-remote`.
|
|
49
60
|
|
|
50
61
|
## Supported Agents
|
|
51
62
|
|
|
@@ -65,6 +76,8 @@ Claude Code works out of the box. Other agents need a one-time setup that clidec
|
|
|
65
76
|
|
|
66
77
|
Each agent runs in a real terminal (PTY) on your machine. clideck receives lightweight status signals via OpenTelemetry — it knows *that* an agent is working, not *what* it's working on.
|
|
67
78
|
|
|
79
|
+
Autopilot routes existing agent output between agents verbatim — it does not rewrite or summarize the routed content.
|
|
80
|
+
|
|
68
81
|
Everything runs locally. No data is collected, transmitted, or stored outside your machine.
|
|
69
82
|
|
|
70
83
|
## Platform Support
|
package/activity.js
CHANGED
|
@@ -13,8 +13,11 @@ function ensure(id) {
|
|
|
13
13
|
function trackIn(id, bytes) {
|
|
14
14
|
ensure(id);
|
|
15
15
|
net[id].in += bytes;
|
|
16
|
+
stream[id].lastInAt = Date.now();
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
function lastInputAt(id) { return stream[id]?.lastInAt || 0; }
|
|
20
|
+
|
|
18
21
|
function trackOut(id, data) {
|
|
19
22
|
ensure(id);
|
|
20
23
|
const now = Date.now();
|
|
@@ -22,6 +25,7 @@ function trackOut(id, data) {
|
|
|
22
25
|
net[id].out += data.length;
|
|
23
26
|
if (now - s.lastOutAt > 2000) s.burstStart = now;
|
|
24
27
|
s.lastOutAt = now;
|
|
28
|
+
s.lastChunk = data;
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
function start(sessions, broadcast) {
|
|
@@ -43,6 +47,15 @@ function start(sessions, broadcast) {
|
|
|
43
47
|
stats[id] = { rawRateOut, rawRateIn, burstMs };
|
|
44
48
|
}
|
|
45
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(' | ')}`);
|
|
46
59
|
}, 1000);
|
|
47
60
|
}
|
|
48
61
|
|
|
@@ -57,7 +70,8 @@ function isActive(id) {
|
|
|
57
70
|
}
|
|
58
71
|
|
|
59
72
|
function lastOutputAt(id) { return stream[id]?.lastOutAt || 0; }
|
|
73
|
+
function lastChunk(id) { return stream[id]?.lastChunk || ''; }
|
|
60
74
|
|
|
61
75
|
function clear(id) { delete net[id]; delete stream[id]; }
|
|
62
76
|
|
|
63
|
-
module.exports = { start, stop, trackIn, trackOut, isActive, lastOutputAt, clear };
|
|
77
|
+
module.exports = { start, stop, trackIn, trackOut, isActive, lastOutputAt, lastInputAt, lastChunk, clear };
|
package/config.js
CHANGED
|
@@ -6,6 +6,71 @@ const { defaultShell, binName } = require('./utils');
|
|
|
6
6
|
|
|
7
7
|
const CONFIG_PATH = join(DATA_DIR, 'config.json');
|
|
8
8
|
|
|
9
|
+
const STARTER_PROMPTS = [
|
|
10
|
+
{
|
|
11
|
+
id: 'starter-prompt-update-documentation',
|
|
12
|
+
name: 'Update documentation',
|
|
13
|
+
text: 'Our docs needs to be updated based on the latest diff changes. Please review the latest changes and udpate the docs accordingly. List the changes you did in concise points in your response. Thanks.',
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
id: 'starter-prompt-investigate-codebase',
|
|
17
|
+
name: 'Investigate codebase',
|
|
18
|
+
text: `Learn the codebase and investigate it for:
|
|
19
|
+
- Critical issues
|
|
20
|
+
- Serious logical issues
|
|
21
|
+
- Things you dont understand why they are there
|
|
22
|
+
- Redundent code
|
|
23
|
+
- Ugly workarounds / plasters / band-aids
|
|
24
|
+
|
|
25
|
+
list your fidings please.`,
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: 'starter-prompt-reviewer-findings',
|
|
29
|
+
name: 'Reviewer findings',
|
|
30
|
+
text: 'Here are the reviewer findings, if you find that any are valid and relevant, please fix with pure solutions, simple approaches, never apply workarounds / plasters.\nWhen finish, list what you fix and how:',
|
|
31
|
+
},
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
const STARTER_ROLES = [
|
|
35
|
+
{
|
|
36
|
+
id: 'starter-role-programmer',
|
|
37
|
+
name: 'Programmer',
|
|
38
|
+
instructions: `You are the main programmer of this project.
|
|
39
|
+
Do you not apply workarounds or bandaids, prefer pure solutions.
|
|
40
|
+
NEVER use plan tool/mode, start to build immediatly and ask questions along the way if any.
|
|
41
|
+
Check if any external findings are valid before applying changes, the reviewer doesnt always updated with the full scope.
|
|
42
|
+
When you done with changes, list concisely what you did.
|
|
43
|
+
|
|
44
|
+
Learn the project quickly if exist. Go over the structure, code and functionality.
|
|
45
|
+
Let me know when you are ready.`,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: 'starter-role-reviewer',
|
|
49
|
+
name: 'Reviewer',
|
|
50
|
+
instructions: `You are the code reviewer in this project.
|
|
51
|
+
Your task is check the coder output and list critical / logical design flow issues, ugly workarounds or functionalty you just dont understand why its there. Do not waste time and list insignificunt findings.
|
|
52
|
+
|
|
53
|
+
If you didnt find anything, response with no findings.
|
|
54
|
+
|
|
55
|
+
You never write code!.
|
|
56
|
+
|
|
57
|
+
Quickly learn the project if exist. Go over the structure, code and functionality and let me know when you are ready.`,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: 'starter-role-product-manager',
|
|
61
|
+
name: 'Product manager',
|
|
62
|
+
instructions: `You are the product manager of this project, you should understand why we do what we do and what is the best way to do it. You dont care about technical limitations or directions, the only thing matter to you is the user UI/UX and how this agents team will ship a top notch, professional deliveries.
|
|
63
|
+
Do not allow the team to round angles and skip small stuff that will basly impact the user.
|
|
64
|
+
|
|
65
|
+
You never write code!
|
|
66
|
+
You dont use your plan tool/mode - instead you are planning immediatly as you go.
|
|
67
|
+
|
|
68
|
+
Go over the project if exist and understand from the code, documentations and readme what is it and why we do it.
|
|
69
|
+
|
|
70
|
+
Let me know when you are ready`,
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
|
|
9
74
|
const DEFAULTS = {
|
|
10
75
|
defaultPath: join(os.homedir(), 'Documents'),
|
|
11
76
|
commands: [
|
|
@@ -20,6 +85,7 @@ const DEFAULTS = {
|
|
|
20
85
|
defaultTheme: 'catppuccin-mocha',
|
|
21
86
|
defaultShell,
|
|
22
87
|
prompts: [],
|
|
88
|
+
roles: [],
|
|
23
89
|
projects: [],
|
|
24
90
|
};
|
|
25
91
|
|
|
@@ -81,11 +147,18 @@ function migrate(cfg) {
|
|
|
81
147
|
}
|
|
82
148
|
}
|
|
83
149
|
if (!cfg.projects) cfg.projects = [];
|
|
150
|
+
if (!cfg.roles) cfg.roles = [];
|
|
84
151
|
return cfg;
|
|
85
152
|
}
|
|
86
153
|
|
|
87
154
|
function load() {
|
|
88
|
-
if (!existsSync(CONFIG_PATH))
|
|
155
|
+
if (!existsSync(CONFIG_PATH)) {
|
|
156
|
+
return {
|
|
157
|
+
...deepCopy(DEFAULTS),
|
|
158
|
+
prompts: deepCopy(STARTER_PROMPTS),
|
|
159
|
+
roles: deepCopy(STARTER_ROLES),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
89
162
|
try {
|
|
90
163
|
return migrate({ ...deepCopy(DEFAULTS), ...JSON.parse(readFileSync(CONFIG_PATH, 'utf8')) });
|
|
91
164
|
} catch { return deepCopy(DEFAULTS); }
|
package/handlers.js
CHANGED
|
@@ -117,6 +117,7 @@ function onConnection(ws) {
|
|
|
117
117
|
ws.send(JSON.stringify({ type: 'sessions.resumable', list: sessions.getResumable(cfg) }));
|
|
118
118
|
ws.send(JSON.stringify({ type: 'transcript.cache', cache: transcript.getCache() }));
|
|
119
119
|
ws.send(JSON.stringify({ type: 'plugins', list: plugins.getInfo() }));
|
|
120
|
+
ws.send(JSON.stringify({ type: 'pills', list: plugins.getPills() }));
|
|
120
121
|
sessions.sendBuffers(ws);
|
|
121
122
|
|
|
122
123
|
ws.on('message', (raw) => {
|
|
@@ -130,8 +131,7 @@ function onConnection(ws) {
|
|
|
130
131
|
case 'input': sessions.input(msg); break;
|
|
131
132
|
case 'session.statusReport':
|
|
132
133
|
if (sessions.getSessions().has(msg.id)) {
|
|
133
|
-
sessions.broadcast({ type: 'session.status', id: msg.id, working: !!msg.working });
|
|
134
|
-
plugins.notifyStatus(msg.id, !!msg.working);
|
|
134
|
+
sessions.broadcast({ type: 'session.status', id: msg.id, working: !!msg.working, source: 'client' });
|
|
135
135
|
}
|
|
136
136
|
break;
|
|
137
137
|
case 'terminal.buffer': {
|
|
@@ -140,11 +140,18 @@ function onConnection(ws) {
|
|
|
140
140
|
const sess = sessions.getSessions().get(msg.id);
|
|
141
141
|
if (sess) {
|
|
142
142
|
const choices = require('./transcript').detectMenu(msg.lines, sess.presetId);
|
|
143
|
+
// Auto-approve: send Enter immediately when menu detected
|
|
144
|
+
if (choices && plugins.shouldAutoApproveMenu(msg.id)) {
|
|
145
|
+
sessions.input({ id: msg.id, data: '\r' });
|
|
146
|
+
}
|
|
143
147
|
const key = choices ? JSON.stringify(choices) : '';
|
|
144
148
|
if (key !== (sess._menuKey || '')) {
|
|
145
149
|
sess._menuKey = key;
|
|
146
150
|
sessions.broadcast({ type: 'session.menu', id: msg.id, choices: choices || [] });
|
|
147
|
-
if (choices)
|
|
151
|
+
if (choices) {
|
|
152
|
+
plugins.notifyMenu(msg.id, choices);
|
|
153
|
+
sessions.broadcast({ type: 'session.status', id: msg.id, working: false, source: 'menu' });
|
|
154
|
+
}
|
|
148
155
|
}
|
|
149
156
|
}
|
|
150
157
|
break;
|
|
@@ -284,6 +291,10 @@ function onConnection(ws) {
|
|
|
284
291
|
break;
|
|
285
292
|
}
|
|
286
293
|
|
|
294
|
+
case 'pill.getLogs':
|
|
295
|
+
ws.send(JSON.stringify({ type: 'pill.logs', id: msg.id, logs: plugins.getPillLogs(msg.id) }));
|
|
296
|
+
break;
|
|
297
|
+
|
|
287
298
|
case 'remote.status': {
|
|
288
299
|
let installed = false;
|
|
289
300
|
try { execFileSync(whichCmd, ['clideck-remote'], { stdio: 'ignore' }); installed = true; } catch {}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clideck",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.26.2",
|
|
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": {
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"homepage": "https://clideck.dev/",
|
|
35
35
|
"dependencies": {
|
|
36
|
+
"@mariozechner/pi-ai": "^0.62.0",
|
|
36
37
|
"@xterm/addon-fit": "^0.11.0",
|
|
37
38
|
"@xterm/xterm": "^6.0.0",
|
|
38
39
|
"node-pty": "^1.1.0",
|
package/plugin-loader.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const { readdirSync, readFileSync, existsSync, mkdirSync, cpSync, rmSync } = require('fs');
|
|
2
2
|
const { join, sep } = require('path');
|
|
3
3
|
const { DATA_DIR } = require('./paths');
|
|
4
|
+
const transcript = require('./transcript');
|
|
4
5
|
|
|
5
6
|
const PLUGINS_DIR = join(DATA_DIR, 'plugins');
|
|
6
7
|
mkdirSync(PLUGINS_DIR, { recursive: true });
|
|
@@ -33,16 +34,22 @@ const inputHooks = [];
|
|
|
33
34
|
const outputHooks = [];
|
|
34
35
|
const statusHooks = [];
|
|
35
36
|
const transcriptHooks = [];
|
|
37
|
+
const menuHooks = [];
|
|
36
38
|
const sessionStatus = new Map(); // sessionId → boolean (dedup multi-client reports)
|
|
39
|
+
const autoApproveMenus = new Set(); // sessionIds where menus should be auto-approved
|
|
37
40
|
const frontendHandlers = new Map();
|
|
38
41
|
let broadcastFn = null;
|
|
39
42
|
let sessionsFn = null;
|
|
40
43
|
let getConfigFn = null;
|
|
41
44
|
let saveConfigFn = null;
|
|
45
|
+
let inputFn = null;
|
|
46
|
+
let createSessionFn = null;
|
|
47
|
+
let closeSessionFn = null;
|
|
42
48
|
const settingsChangeHandlers = new Map(); // pluginId → [fn]
|
|
49
|
+
const sessionPills = new Map(); // pillId → { pluginId, id, title, projectId, working, statusText, icon, logs[] }
|
|
43
50
|
|
|
44
51
|
function removeHooks(pluginId) {
|
|
45
|
-
for (const arr of [inputHooks, outputHooks, statusHooks, transcriptHooks]) {
|
|
52
|
+
for (const arr of [inputHooks, outputHooks, statusHooks, transcriptHooks, menuHooks]) {
|
|
46
53
|
for (let i = arr.length - 1; i >= 0; i--) {
|
|
47
54
|
if (arr[i].pluginId === pluginId) arr.splice(i, 1);
|
|
48
55
|
}
|
|
@@ -51,13 +58,22 @@ function removeHooks(pluginId) {
|
|
|
51
58
|
if (key.startsWith(`plugin.${pluginId}.`)) frontendHandlers.delete(key);
|
|
52
59
|
}
|
|
53
60
|
settingsChangeHandlers.delete(pluginId);
|
|
61
|
+
for (const [id, pill] of sessionPills) {
|
|
62
|
+
if (pill.pluginId === pluginId) {
|
|
63
|
+
sessionPills.delete(id);
|
|
64
|
+
broadcastFn?.({ type: 'pill.removed', id });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
54
67
|
}
|
|
55
68
|
|
|
56
|
-
function init(broadcast, getSessions, getConfig, saveConfig) {
|
|
69
|
+
function init(broadcast, getSessions, getConfig, saveConfig, sessionInput, createProgrammatic, closeSession) {
|
|
57
70
|
broadcastFn = broadcast;
|
|
58
71
|
sessionsFn = getSessions;
|
|
59
72
|
getConfigFn = getConfig;
|
|
60
73
|
saveConfigFn = saveConfig;
|
|
74
|
+
inputFn = sessionInput;
|
|
75
|
+
createSessionFn = createProgrammatic;
|
|
76
|
+
closeSessionFn = closeSession;
|
|
61
77
|
|
|
62
78
|
for (const entry of readdirSync(PLUGINS_DIR, { withFileTypes: true })) {
|
|
63
79
|
if (!entry.isDirectory()) continue;
|
|
@@ -88,7 +104,7 @@ function init(broadcast, getSessions, getConfig, saveConfig) {
|
|
|
88
104
|
continue;
|
|
89
105
|
}
|
|
90
106
|
|
|
91
|
-
const state = { manifest, dir, shutdownFns: [], actions: [] };
|
|
107
|
+
const state = { manifest, dir, shutdownFns: [], actions: [], dynamicOptions: {} };
|
|
92
108
|
plugins.set(manifest.id, state);
|
|
93
109
|
|
|
94
110
|
try {
|
|
@@ -113,6 +129,7 @@ function buildApi(pluginId, pluginDir, state) {
|
|
|
113
129
|
onSessionOutput(fn) { outputHooks.push({ pluginId, fn }); },
|
|
114
130
|
onStatusChange(fn) { statusHooks.push({ pluginId, fn }); },
|
|
115
131
|
onTranscriptEntry(fn) { transcriptHooks.push({ pluginId, fn }); },
|
|
132
|
+
onMenuDetected(fn) { menuHooks.push({ pluginId, fn }); },
|
|
116
133
|
|
|
117
134
|
sendToFrontend(event, data = {}) {
|
|
118
135
|
broadcastFn?.({ ...data, type: `plugin.${pluginId}.${event}` });
|
|
@@ -124,17 +141,69 @@ function buildApi(pluginId, pluginDir, state) {
|
|
|
124
141
|
getSession(id) {
|
|
125
142
|
const s = sessionsFn?.()?.get(id);
|
|
126
143
|
if (!s) return null;
|
|
127
|
-
return { id, name: s.name, cwd: s.cwd, commandId: s.commandId, themeId: s.themeId, projectId: s.projectId };
|
|
144
|
+
return { id, name: s.name, cwd: s.cwd, commandId: s.commandId, presetId: s.presetId || 'shell', themeId: s.themeId, projectId: s.projectId, roleName: s.roleName || null, working: !!sessionStatus.get(id) };
|
|
128
145
|
},
|
|
129
146
|
getSessions() {
|
|
130
147
|
const sessions = sessionsFn?.();
|
|
131
148
|
if (!sessions) return [];
|
|
132
149
|
return [...sessions].map(([id, s]) => ({
|
|
133
|
-
id, name: s.name, cwd: s.cwd, commandId: s.commandId, themeId: s.themeId, projectId: s.projectId,
|
|
150
|
+
id, name: s.name, cwd: s.cwd, commandId: s.commandId, presetId: s.presetId || 'shell', themeId: s.themeId, projectId: s.projectId, roleName: s.roleName || null, working: !!sessionStatus.get(id),
|
|
134
151
|
}));
|
|
135
152
|
},
|
|
136
153
|
|
|
154
|
+
createSession(opts) {
|
|
155
|
+
const cfg = getConfigFn?.();
|
|
156
|
+
if (!cfg || !createSessionFn) return null;
|
|
157
|
+
const result = createSessionFn(opts, cfg);
|
|
158
|
+
return result.error ? null : result.id;
|
|
159
|
+
},
|
|
160
|
+
closeSession(id) {
|
|
161
|
+
const cfg = getConfigFn?.();
|
|
162
|
+
if (!cfg || !closeSessionFn) return;
|
|
163
|
+
closeSessionFn({ id }, cfg);
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
inputToSession(id, data) { inputFn?.({ id, data }); },
|
|
167
|
+
setAutoApproveMenu(id, enabled) { enabled ? autoApproveMenus.add(id) : autoApproveMenus.delete(id); },
|
|
168
|
+
|
|
169
|
+
getRoles() { return JSON.parse(JSON.stringify(getConfigFn?.()?.roles || [])); },
|
|
170
|
+
getProjects() { return JSON.parse(JSON.stringify(getConfigFn?.()?.projects || [])); },
|
|
171
|
+
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
|
+
detectMenu(lines, presetId) { return transcript.detectMenu(lines, presetId); },
|
|
175
|
+
|
|
137
176
|
addToolbarAction(opts) { state.actions.push({ ...opts, pluginId, slot: 'toolbar' }); },
|
|
177
|
+
addProjectAction(opts) { state.actions.push({ ...opts, pluginId, slot: 'project-header' }); },
|
|
178
|
+
|
|
179
|
+
addSessionPill(opts) {
|
|
180
|
+
const pill = { pluginId, id: opts.id, title: opts.title, projectId: opts.projectId, working: false, statusText: '', icon: opts.icon || '', logs: [], startedAt: Date.now() };
|
|
181
|
+
sessionPills.set(opts.id, pill);
|
|
182
|
+
broadcastFn?.({ type: 'pill.added', pill: pillInfo(pill) });
|
|
183
|
+
},
|
|
184
|
+
updateSessionPill(id, updates) {
|
|
185
|
+
const pill = sessionPills.get(id);
|
|
186
|
+
if (!pill || pill.pluginId !== pluginId) return;
|
|
187
|
+
if (updates.title !== undefined) pill.title = updates.title;
|
|
188
|
+
if (updates.working !== undefined) pill.working = updates.working;
|
|
189
|
+
if (updates.statusText !== undefined) pill.statusText = updates.statusText;
|
|
190
|
+
if (updates.projectId !== undefined) pill.projectId = updates.projectId;
|
|
191
|
+
broadcastFn?.({ type: 'pill.updated', pill: pillInfo(pill) });
|
|
192
|
+
},
|
|
193
|
+
appendPillLog(id, text) {
|
|
194
|
+
const pill = sessionPills.get(id);
|
|
195
|
+
if (!pill || pill.pluginId !== pluginId) return;
|
|
196
|
+
const entry = { ts: Date.now(), text };
|
|
197
|
+
pill.logs.push(entry);
|
|
198
|
+
if (pill.logs.length > 200) pill.logs.splice(0, pill.logs.length - 200);
|
|
199
|
+
broadcastFn?.({ type: 'pill.log', id, entry });
|
|
200
|
+
},
|
|
201
|
+
removeSessionPill(id) {
|
|
202
|
+
const pill = sessionPills.get(id);
|
|
203
|
+
if (!pill || pill.pluginId !== pluginId) return;
|
|
204
|
+
sessionPills.delete(id);
|
|
205
|
+
broadcastFn?.({ type: 'pill.removed', id });
|
|
206
|
+
},
|
|
138
207
|
|
|
139
208
|
getSetting(key) {
|
|
140
209
|
const cfg = getConfigFn?.();
|
|
@@ -153,6 +222,28 @@ function buildApi(pluginId, pluginDir, state) {
|
|
|
153
222
|
settingsChangeHandlers.get(pluginId).push(fn);
|
|
154
223
|
},
|
|
155
224
|
|
|
225
|
+
setSettingOptions(key, options) {
|
|
226
|
+
state.dynamicOptions[key] = options;
|
|
227
|
+
broadcastFn?.({ type: 'plugins', list: getInfo() });
|
|
228
|
+
},
|
|
229
|
+
setSetting(key, value) {
|
|
230
|
+
updateSetting(pluginId, key, value);
|
|
231
|
+
broadcastFn?.({ type: 'plugins', list: getInfo() });
|
|
232
|
+
},
|
|
233
|
+
|
|
234
|
+
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 {}
|
|
239
|
+
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);
|
|
246
|
+
},
|
|
156
247
|
onShutdown(fn) { state.shutdownFns.push(fn); },
|
|
157
248
|
log(msg) { console.log(`[plugin:${pluginId}] ${msg}`); },
|
|
158
249
|
};
|
|
@@ -177,11 +268,11 @@ function notifyOutput(id, data) {
|
|
|
177
268
|
}
|
|
178
269
|
}
|
|
179
270
|
|
|
180
|
-
function notifyStatus(id, working) {
|
|
271
|
+
function notifyStatus(id, working, source) {
|
|
181
272
|
if (sessionStatus.get(id) === working) return;
|
|
182
273
|
sessionStatus.set(id, working);
|
|
183
274
|
for (const h of statusHooks) {
|
|
184
|
-
try { h.fn(id, working); }
|
|
275
|
+
try { h.fn(id, working, source); }
|
|
185
276
|
catch (e) { console.error(`[plugin:${h.pluginId}] status error: ${e.message}`); }
|
|
186
277
|
}
|
|
187
278
|
}
|
|
@@ -193,6 +284,14 @@ function notifyTranscript(id, role, text) {
|
|
|
193
284
|
}
|
|
194
285
|
}
|
|
195
286
|
|
|
287
|
+
function notifyMenu(id, choices) {
|
|
288
|
+
for (const h of menuHooks) {
|
|
289
|
+
try { h.fn(id, choices); }
|
|
290
|
+
catch (e) { console.error(`[plugin:${h.pluginId}] menu error: ${e.message}`); }
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
|
|
196
295
|
function updateSetting(pluginId, key, value) {
|
|
197
296
|
// Validate plugin exists (also prevents __proto__ pollution — Map lookup returns undefined)
|
|
198
297
|
const plugin = plugins.get(pluginId);
|
|
@@ -201,7 +300,8 @@ function updateSetting(pluginId, key, value) {
|
|
|
201
300
|
const settingDef = (plugin.manifest.settings || []).find(s => s.key === key);
|
|
202
301
|
if (!settingDef) return;
|
|
203
302
|
// Type-coerce/validate value against manifest type
|
|
204
|
-
const
|
|
303
|
+
const dynOpts = settingDef.type === 'dynamic-select' ? plugin.dynamicOptions?.[key] : null;
|
|
304
|
+
const coerced = coerceSetting(settingDef, value, dynOpts);
|
|
205
305
|
if (coerced === undefined) return;
|
|
206
306
|
|
|
207
307
|
const cfg = getConfigFn?.();
|
|
@@ -217,7 +317,7 @@ function updateSetting(pluginId, key, value) {
|
|
|
217
317
|
}
|
|
218
318
|
}
|
|
219
319
|
|
|
220
|
-
function coerceSetting(def, value) {
|
|
320
|
+
function coerceSetting(def, value, dynOpts) {
|
|
221
321
|
switch (def.type) {
|
|
222
322
|
case 'toggle': return !!value;
|
|
223
323
|
case 'number': {
|
|
@@ -232,6 +332,12 @@ function coerceSetting(def, value) {
|
|
|
232
332
|
const s = String(value);
|
|
233
333
|
return opts.includes(s) ? s : undefined;
|
|
234
334
|
}
|
|
335
|
+
case 'dynamic-select': {
|
|
336
|
+
const s = String(value);
|
|
337
|
+
if (!dynOpts?.length) return s; // options not loaded yet — accept
|
|
338
|
+
const opts = dynOpts.map(o => String(typeof o === 'object' ? o.value : o));
|
|
339
|
+
return opts.includes(s) ? s : undefined;
|
|
340
|
+
}
|
|
235
341
|
default: return String(value);
|
|
236
342
|
}
|
|
237
343
|
}
|
|
@@ -254,6 +360,7 @@ function getInfo() {
|
|
|
254
360
|
description: p.manifest.description || '',
|
|
255
361
|
settings: p.manifest.settings || [],
|
|
256
362
|
settingValues: cfg?.pluginSettings?.[p.manifest.id] || {},
|
|
363
|
+
dynamicOptions: p.dynamicOptions || {},
|
|
257
364
|
actions: p.actions,
|
|
258
365
|
hasClient: existsSync(join(p.dir, 'client.js')),
|
|
259
366
|
bundled: BUNDLED_IDS.has(p.manifest.id),
|
|
@@ -288,7 +395,16 @@ function shutdown() {
|
|
|
288
395
|
}
|
|
289
396
|
}
|
|
290
397
|
|
|
291
|
-
function
|
|
398
|
+
function pillInfo(pill) {
|
|
399
|
+
return { id: pill.id, pluginId: pill.pluginId, title: pill.title, projectId: pill.projectId, working: pill.working, statusText: pill.statusText, icon: pill.icon, startedAt: pill.startedAt };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function getPills() { return [...sessionPills.values()].map(pillInfo); }
|
|
403
|
+
function getPillLogs(id) { return sessionPills.get(id)?.logs || []; }
|
|
404
|
+
|
|
405
|
+
function clearStatus(id) { sessionStatus.delete(id); autoApproveMenus.delete(id); }
|
|
406
|
+
function isWorking(id) { return !!sessionStatus.get(id); }
|
|
407
|
+
function shouldAutoApproveMenu(id) { return autoApproveMenus.has(id); }
|
|
292
408
|
|
|
293
409
|
// Bundled plugin IDs — these ship with CliDeck and must not be deleted
|
|
294
410
|
const BUNDLED_IDS = new Set(
|
|
@@ -318,6 +434,7 @@ function removePlugin(pluginId) {
|
|
|
318
434
|
module.exports = {
|
|
319
435
|
PLUGINS_DIR, BUNDLED_IDS,
|
|
320
436
|
init, shutdown,
|
|
321
|
-
transformInput, notifyOutput, notifyStatus, notifyTranscript, clearStatus,
|
|
437
|
+
transformInput, notifyOutput, notifyStatus, notifyTranscript, notifyMenu, clearStatus, isWorking, shouldAutoApproveMenu,
|
|
322
438
|
handleMessage, updateSetting, getInfo, resolveFile, removePlugin,
|
|
439
|
+
getPills, getPillLogs,
|
|
323
440
|
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "autopilot",
|
|
3
|
+
"name": "Autopilot",
|
|
4
|
+
"version": "0.14.4",
|
|
5
|
+
"author": "CliDeck",
|
|
6
|
+
"description": "Multi-agent orchestration — routes output between agents automatically",
|
|
7
|
+
"settings": [
|
|
8
|
+
{
|
|
9
|
+
"key": "enabled",
|
|
10
|
+
"label": "Enabled",
|
|
11
|
+
"type": "toggle",
|
|
12
|
+
"default": true
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"key": "provider",
|
|
16
|
+
"label": "Provider",
|
|
17
|
+
"type": "select",
|
|
18
|
+
"default": "anthropic",
|
|
19
|
+
"options": [
|
|
20
|
+
{ "value": "anthropic", "label": "Anthropic" },
|
|
21
|
+
{ "value": "openai", "label": "OpenAI" },
|
|
22
|
+
{ "value": "google", "label": "Google AI" },
|
|
23
|
+
{ "value": "groq", "label": "Groq" },
|
|
24
|
+
{ "value": "openrouter", "label": "OpenRouter" },
|
|
25
|
+
{ "value": "xai", "label": "xAI" },
|
|
26
|
+
{ "value": "mistral", "label": "Mistral" },
|
|
27
|
+
{ "value": "cerebras", "label": "Cerebras" }
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"key": "model",
|
|
32
|
+
"label": "Model",
|
|
33
|
+
"type": "dynamic-select",
|
|
34
|
+
"default": "claude-haiku-4-5"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"key": "apiKey",
|
|
38
|
+
"label": "API Key",
|
|
39
|
+
"type": "text",
|
|
40
|
+
"default": "",
|
|
41
|
+
"placeholder": "Falls back to env var (ANTHROPIC_API_KEY, etc.)",
|
|
42
|
+
"description": "Leave empty to use the standard environment variable for the selected provider"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"key": "debugging",
|
|
46
|
+
"label": "Debugging",
|
|
47
|
+
"type": "toggle",
|
|
48
|
+
"default": false,
|
|
49
|
+
"description": "Save each router LLM turn (system prompt, KB history, agent outputs) to ~/.clideck/autopilot/logs/ for inspection"
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Autopilot client — toggle state, notifications, token display
|
|
2
|
+
|
|
3
|
+
let api = null;
|
|
4
|
+
const activeProjects = new Set();
|
|
5
|
+
|
|
6
|
+
export function init(pluginApi) {
|
|
7
|
+
api = pluginApi;
|
|
8
|
+
|
|
9
|
+
api.onMessage('started', (msg) => {
|
|
10
|
+
activeProjects.add(msg.projectId);
|
|
11
|
+
updateToggle(msg.projectId, true);
|
|
12
|
+
api.toast('Autopilot started');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
api.onMessage('stopped', (msg) => {
|
|
16
|
+
activeProjects.delete(msg.projectId);
|
|
17
|
+
updateToggle(msg.projectId, false);
|
|
18
|
+
api.toast('Autopilot stopped');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
api.onMessage('error', (msg) => api.toast(msg.msg || 'Autopilot error', { type: 'error' }));
|
|
22
|
+
api.onMessage('routed', (msg) => api.toast(`→ ${msg.to}`, { type: 'info' }));
|
|
23
|
+
api.onMessage('notify', (msg) => api.toast(msg.reason, { type: 'warn', duration: 0, markdown: true, title: `Autopilot \u2014 ${msg.projectName || 'Project'}` }));
|
|
24
|
+
api.onMessage('paused', (msg) => api.toast(`Autopilot: ${msg.question}`, { type: 'warn' }));
|
|
25
|
+
api.onMessage('resumed', () => api.toast('Autopilot resumed'));
|
|
26
|
+
|
|
27
|
+
api.onMessage('tokens', (msg) => {
|
|
28
|
+
const btn = toggleBtn(msg.projectId);
|
|
29
|
+
if (btn && activeProjects.has(msg.projectId)) {
|
|
30
|
+
const total = (msg.input || 0) + (msg.output || 0);
|
|
31
|
+
btn.title = `Autopilot ON — ${total.toLocaleString()} tokens`;
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
api.onMessage('status', (msg) => {
|
|
36
|
+
if (msg.active) activeProjects.add(msg.projectId);
|
|
37
|
+
else activeProjects.delete(msg.projectId);
|
|
38
|
+
updateToggle(msg.projectId, msg.active);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Re-apply toggle state when sidebar re-renders
|
|
42
|
+
const list = document.getElementById('session-list');
|
|
43
|
+
if (list) {
|
|
44
|
+
new MutationObserver(() => {
|
|
45
|
+
for (const pid of activeProjects) updateToggle(pid, true);
|
|
46
|
+
}).observe(list, { childList: true, subtree: true });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
activeProjects.clear();
|
|
50
|
+
api.send('sync');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function toggleBtn(pid) {
|
|
54
|
+
return document.querySelector(`.project-plugin-action[data-action-id="autopilot-toggle"][data-project-id="${pid}"]`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function updateToggle(pid, active) {
|
|
58
|
+
const btn = toggleBtn(pid);
|
|
59
|
+
if (!btn) return;
|
|
60
|
+
btn.classList.toggle('autopilot-active', active);
|
|
61
|
+
btn.title = active ? 'Autopilot ON — click to stop' : 'Autopilot — click to start';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const style = document.createElement('style');
|
|
65
|
+
style.textContent = `
|
|
66
|
+
@keyframes autopilot-pulse {
|
|
67
|
+
0%, 100% { opacity: 1; filter: drop-shadow(0 0 2px #818cf8); }
|
|
68
|
+
50% { opacity: 0.6; filter: drop-shadow(0 0 6px #818cf8); }
|
|
69
|
+
}
|
|
70
|
+
@keyframes clock-spin {
|
|
71
|
+
from { transform: rotate(0deg); }
|
|
72
|
+
to { transform: rotate(360deg); }
|
|
73
|
+
}
|
|
74
|
+
.autopilot-active {
|
|
75
|
+
color: #818cf8 !important;
|
|
76
|
+
opacity: 1 !important;
|
|
77
|
+
animation: autopilot-pulse 2s ease-in-out infinite;
|
|
78
|
+
}
|
|
79
|
+
.autopilot-active svg path {
|
|
80
|
+
transform-origin: 12px 12px;
|
|
81
|
+
animation: clock-spin 2s linear infinite;
|
|
82
|
+
}
|
|
83
|
+
`;
|
|
84
|
+
document.head.appendChild(style);
|