clideck 1.26.0 → 1.26.3

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
@@ -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** — ships with Voice Input and Trim Clip, or build your own
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
- Check on your agents from your phone. Start a task, walk away, glance at your phone see who's done, who's working, who needs input. Pair with one QR scan, no account needed. E2E encrypted — the relay cannot read your code.
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)) return deepCopy(DEFAULTS);
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) sessions.broadcast({ type: 'session.status', id: msg.id, working: false, source: 'menu' });
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;
@@ -262,13 +269,29 @@ function onConnection(ws) {
262
269
 
263
270
  case 'dirs.list': {
264
271
  const target = msg.path || cfg.defaultPath;
265
- const result = listDirs(target);
272
+ const result = listDirs(target, !!msg.showHidden);
266
273
  const entries = Array.isArray(result) ? result : [];
267
274
  const error = result.error || undefined;
268
275
  ws.send(JSON.stringify({ type: 'dirs', path: target, entries, error }));
269
276
  break;
270
277
  }
271
278
 
279
+ case 'dirs.mkdir': {
280
+ const name = (msg.name || '').trim();
281
+ if (!name || name.includes('/') || name.includes('\\') || name === '.' || name === '..') {
282
+ ws.send(JSON.stringify({ type: 'dirs.mkdir', success: false, error: 'Invalid folder name' }));
283
+ break;
284
+ }
285
+ const dirPath = join(msg.parent, name);
286
+ try {
287
+ mkdirSync(dirPath);
288
+ ws.send(JSON.stringify({ type: 'dirs.mkdir', success: true, path: dirPath }));
289
+ } catch (e) {
290
+ ws.send(JSON.stringify({ type: 'dirs.mkdir', success: false, error: e.message }));
291
+ }
292
+ break;
293
+ }
294
+
272
295
  case 'plugin.settings.update':
273
296
  plugins.updateSetting(msg.pluginId, msg.key, msg.value);
274
297
  sessions.broadcast({ type: 'plugins', list: plugins.getInfo() });
@@ -284,6 +307,10 @@ function onConnection(ws) {
284
307
  break;
285
308
  }
286
309
 
310
+ case 'pill.getLogs':
311
+ ws.send(JSON.stringify({ type: 'pill.logs', id: msg.id, logs: plugins.getPillLogs(msg.id) }));
312
+ break;
313
+
287
314
  case 'remote.status': {
288
315
  let installed = false;
289
316
  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.26.0",
3
+ "version": "1.26.3",
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 coerced = coerceSetting(settingDef, value);
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 clearStatus(id) { sessionStatus.delete(id); }
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
+ }