clideck 1.22.6 → 1.23.1

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.
@@ -0,0 +1,27 @@
1
+ ## What changed
2
+
3
+ <!-- One or two sentences. What does this PR do? -->
4
+
5
+ ## Why
6
+
7
+ <!-- What problem does this solve? Link to an issue or discussion if one exists. -->
8
+
9
+ ## How to verify
10
+
11
+ <!--
12
+ Steps to test this locally:
13
+ 1. Run `node server.js`
14
+ 2. Open http://localhost:4000
15
+ 3. ...
16
+ -->
17
+
18
+ ## What's out of scope
19
+
20
+ <!-- What did you intentionally NOT change? Helps reviewers stay focused. -->
21
+
22
+ ## Checklist
23
+
24
+ - [ ] Tested locally with `node server.js`
25
+ - [ ] One focused change, not bundled with unrelated fixes
26
+ - [ ] No new external service dependencies
27
+ - [ ] Does not read, store, or intercept agent prompts/responses
@@ -0,0 +1,53 @@
1
+ # Contributing to CliDeck
2
+
3
+ This document explains what we accept, what needs discussion first, and how to structure a PR so it gets reviewed fast.
4
+
5
+ ## What to contribute
6
+
7
+ **Bug fixes and small improvements** — open a PR directly.
8
+ Crashes, rendering glitches, typos, broken links, edge cases, performance fixes.
9
+
10
+ **New features, architecture changes, or new agent presets** — [start a Discussion](https://github.com/rustykuntz/clideck/discussions) first.
11
+ Describe the problem, your proposed approach, and any trade-offs. If the idea gets a thumbs up, then open a PR.
12
+
13
+ **Plugins** — the best way to contribute new functionality.
14
+ CliDeck has a plugin system so features can be added without touching core code. If your idea can be a plugin, it should be. See the [documentation](https://docs.clideck.dev/) for how to build one.
15
+
16
+ ## What we will reject
17
+
18
+ - PRs that change multiple unrelated things. One change per PR.
19
+ - PRs with no description of what changed or why.
20
+ - Formatting-only changes (whitespace, semicolons, reordering imports).
21
+ - Features that add external service dependencies or phone home.
22
+ - Changes that break the zero-interference guarantee — CliDeck never reads, stores, or intercepts agent prompts and responses.
23
+
24
+ ## Before you open a PR
25
+
26
+ 1. **Test locally.** Run `node server.js`, open `http://localhost:4000`, and verify your change works.
27
+ 2. **Keep it focused.** If you found a bug while working on a feature, that's two PRs.
28
+ 3. **Match the existing style.** No linter is enforced, but stay consistent with surrounding code.
29
+ 4. **Don't bundle dependency changes** unless your PR specifically requires them.
30
+
31
+ ## PR structure
32
+
33
+ Fill out the PR template. The key fields:
34
+
35
+ - **What changed** — one or two sentences.
36
+ - **Why** — the problem this solves.
37
+ - **How to verify** — steps to confirm it works.
38
+ - **What's out of scope** — what you intentionally did not change.
39
+
40
+ Small PRs get reviewed faster. If your diff is over 300 lines, consider splitting it.
41
+
42
+ ## Built a fork?
43
+
44
+ If you've forked CliDeck and built something — a new agent integration, a workflow improvement, a plugin — open a Discussion in **Show & Tell**. The best fork features get upstreamed.
45
+
46
+ ## Licensing of contributions
47
+
48
+ By submitting a contribution that is accepted into the repository, you agree
49
+ that it will be licensed under the MIT License.
50
+
51
+ ## Conduct
52
+
53
+ Be constructive. Review comments are about the code, not the person. If a PR is rejected, the reason will be explained.
package/README.md CHANGED
@@ -2,11 +2,14 @@
2
2
 
3
3
  One screen for all your AI coding agents.
4
4
 
5
+ [Documentation](https://docs.clideck.dev/) | [Video Demo](https://youtu.be/hICrtjGAeDk) | [Website](https://clideck.dev/)
6
+
5
7
  ![CliDeck dashboard](assets/clideck-themes.jpg)
6
8
 
7
- You're running Claude Code, Codex, and Gemini CLI in separate terminals. You switch between them constantly, forget which one finished, and lose sessions when you close the lid. CliDeck puts them all on one screen with live status, so you always know what's happening.
9
+ You're running Claude Code, Codex, Gemini CLI, and OpenCode in separate terminals. You switch between them constantly, forget which one finished, and lose sessions when you close the lid. CliDeck puts them all on one screen with live status, so you always know what's happening.
8
10
 
9
11
  CliDeck is a local dashboard that runs all your CLI agents in one browser tab. It tracks which agents are working, which are idle, and notifies you when they need attention. Everything runs on your machine — nothing leaves localhost.
12
+ Switch between agents as easily as switching between chats.
10
13
 
11
14
  ## Quick Start
12
15
 
@@ -72,6 +75,10 @@ Full setup guides, agent configuration, and plugin development:
72
75
 
73
76
  **[Documentation](https://docs.clideck.dev/)**
74
77
 
78
+ ## Acknowledgments
79
+
80
+ Built with [xterm.js](https://xtermjs.org/).
81
+
75
82
  ## License
76
83
 
77
- MIT
84
+ MIT — see [LICENSE](LICENSE).
package/activity.js CHANGED
@@ -51,6 +51,11 @@ function stop() {
51
51
  interval = null;
52
52
  }
53
53
 
54
+ function isActive(id) {
55
+ const s = stream[id];
56
+ return s ? (Date.now() - s.lastOutAt < 2000) : false;
57
+ }
58
+
54
59
  function clear(id) { delete net[id]; delete stream[id]; }
55
60
 
56
- module.exports = { start, stop, trackIn, trackOut, clear };
61
+ module.exports = { start, stop, trackIn, trackOut, isActive, clear };
Binary file
package/handlers.js CHANGED
@@ -11,6 +11,23 @@ for (const p of presets) if (p.presetId === 'shell') p.command = defaultShell;
11
11
  const transcript = require('./transcript');
12
12
  const plugins = require('./plugin-loader');
13
13
 
14
+ const opencodePluginDir = join(
15
+ process.platform === 'win32' ? (process.env.APPDATA || join(os.homedir(), 'AppData', 'Roaming')) : join(os.homedir(), '.config'),
16
+ 'opencode', 'plugins'
17
+ );
18
+ // Resolve opencode preset paths for current platform
19
+ for (const p of presets) {
20
+ if (p.presetId !== 'opencode') continue;
21
+ const bridgePath = join(opencodePluginDir, 'clideck-bridge.js');
22
+ if (p.pluginPath) p.pluginPath = bridgePath;
23
+ if (p.pluginSetup) {
24
+ const copyCmd = process.platform === 'win32'
25
+ ? `copy opencode-plugin\\clideck-bridge.js "${opencodePluginDir}\\"`
26
+ : `cp opencode-plugin/clideck-bridge.js ${opencodePluginDir}/`;
27
+ p.pluginSetup = `Install the CliDeck bridge plugin to enable real-time status and resume.\n\n${copyCmd}`;
28
+ }
29
+ }
30
+
14
31
  // Check which agent binaries are available on PATH
15
32
  const whichCmd = process.platform === 'win32' ? 'where' : 'which';
16
33
  function checkAvailability() {
@@ -47,8 +64,7 @@ function detectTelemetryConfig(c) {
47
64
  detected = !!s.telemetry?.enabled && (s.telemetry?.otlpEndpoint || '').includes(`localhost:${port}`);
48
65
  } catch {}
49
66
  } else if (preset.presetId === 'opencode') {
50
- const ocPlugins = join(home, '.config', 'opencode', 'plugins');
51
- detected = existsSync(join(ocPlugins, 'clideck-bridge.js')) || existsSync(join(ocPlugins, 'termix-bridge.js'));
67
+ detected = existsSync(join(opencodePluginDir, 'clideck-bridge.js')) || existsSync(join(opencodePluginDir, 'termix-bridge.js'));
52
68
  } else { continue; }
53
69
  if (detected !== !!cmd.telemetryEnabled) {
54
70
  cmd.telemetryEnabled = detected;
@@ -60,14 +76,18 @@ function detectTelemetryConfig(c) {
60
76
  return changed;
61
77
  }
62
78
 
79
+ function configForClient() {
80
+ return { ...cfg, pluginsDir: plugins.PLUGINS_DIR };
81
+ }
82
+
63
83
  function onConnection(ws) {
64
84
  sessions.clients.add(ws);
65
85
 
66
- ws.send(JSON.stringify({ type: 'config', config: cfg }));
86
+ ws.send(JSON.stringify({ type: 'config', config: configForClient() }));
67
87
  ws.send(JSON.stringify({ type: 'themes', themes }));
68
88
  ws.send(JSON.stringify({ type: 'presets', presets }));
69
89
  ws.send(JSON.stringify({ type: 'sessions', list: sessions.list() }));
70
- ws.send(JSON.stringify({ type: 'sessions.resumable', list: sessions.getResumable() }));
90
+ ws.send(JSON.stringify({ type: 'sessions.resumable', list: sessions.getResumable(cfg) }));
71
91
  ws.send(JSON.stringify({ type: 'transcript.cache', cache: transcript.getCache() }));
72
92
  ws.send(JSON.stringify({ type: 'plugins', list: plugins.getInfo() }));
73
93
  sessions.sendBuffers(ws);
@@ -82,14 +102,20 @@ function onConnection(ws) {
82
102
  case 'session.restart': console.log('[handler] session.restart', msg.id); sessions.restart(msg, ws, cfg); break;
83
103
  case 'input': sessions.input(msg); break;
84
104
  case 'session.statusReport':
85
- if (sessions.getSessions().has(msg.id)) plugins.notifyStatus(msg.id, !!msg.working);
105
+ if (sessions.getSessions().has(msg.id)) {
106
+ plugins.notifyStatus(msg.id, !!msg.working);
107
+ }
108
+ break;
109
+ case 'terminal.buffer':
110
+ require('./transcript').storeBuffer(msg.id, msg.lines);
111
+ sessions.broadcast({ type: 'screen.updated', id: msg.id });
86
112
  break;
87
113
  case 'resize': sessions.resize(msg); break;
88
114
  case 'rename': sessions.rename(msg); break;
89
- case 'close': sessions.close(msg); break;
115
+ case 'close': sessions.close(msg, cfg); break;
90
116
 
91
117
  case 'config.get':
92
- ws.send(JSON.stringify({ type: 'config', config: cfg }));
118
+ ws.send(JSON.stringify({ type: 'config', config: configForClient() }));
93
119
  break;
94
120
 
95
121
  case 'checkAvailability':
@@ -98,10 +124,11 @@ function onConnection(ws) {
98
124
  break;
99
125
 
100
126
  case 'config.update':
127
+ delete msg.config.pluginsDir;
101
128
  cfg = { ...cfg, ...msg.config };
102
129
  detectTelemetryConfig(cfg);
103
130
  config.save(cfg);
104
- sessions.broadcast({ type: 'config', config: cfg });
131
+ sessions.broadcast({ type: 'config', config: configForClient() });
105
132
  break;
106
133
 
107
134
  case 'session.theme': {
@@ -129,7 +156,7 @@ function onConnection(ws) {
129
156
  }
130
157
  }
131
158
  config.save(cfg);
132
- sessions.broadcast({ type: 'config', config: cfg });
159
+ sessions.broadcast({ type: 'config', config: configForClient() });
133
160
  ws.send(JSON.stringify({
134
161
  type: 'telemetry.autosetup.result',
135
162
  presetId: msg.presetId,
@@ -159,7 +186,7 @@ function onConnection(ws) {
159
186
  }
160
187
  }
161
188
  config.save(cfg);
162
- sessions.broadcast({ type: 'config', config: cfg });
189
+ sessions.broadcast({ type: 'config', config: configForClient() });
163
190
  break;
164
191
  }
165
192
 
@@ -185,11 +212,11 @@ function onConnection(ws) {
185
212
  if (!proj) break;
186
213
  // Kill all sessions in this project
187
214
  for (const s of sessions.list()) {
188
- if (s.projectId === msg.id) sessions.close({ id: s.id });
215
+ if (s.projectId === msg.id) sessions.close({ id: s.id }, cfg);
189
216
  }
190
217
  cfg.projects = cfg.projects.filter(p => p.id !== msg.id);
191
218
  config.save(cfg);
192
- sessions.broadcast({ type: 'config', config: cfg });
219
+ sessions.broadcast({ type: 'config', config: configForClient() });
193
220
  break;
194
221
  }
195
222
 
@@ -207,6 +234,55 @@ function onConnection(ws) {
207
234
  sessions.broadcast({ type: 'plugins', list: plugins.getInfo() });
208
235
  break;
209
236
 
237
+ case 'remote.status': {
238
+ let installed = false;
239
+ try { execFileSync(whichCmd, ['clideck-remote'], { stdio: 'ignore' }); installed = true; } catch {}
240
+ if (!installed) { ws.send(JSON.stringify({ type: 'remote.status', installed: false })); break; }
241
+ require('child_process').execFile('clideck-remote', ['status', '--json'], { timeout: 5000 }, (err, stdout) => {
242
+ if (err) { ws.send(JSON.stringify({ type: 'remote.status', installed: true })); return; }
243
+ try { ws.send(JSON.stringify({ type: 'remote.status', installed: true, ...JSON.parse(stdout) })); }
244
+ catch { ws.send(JSON.stringify({ type: 'remote.status', installed: true })); }
245
+ });
246
+ break;
247
+ }
248
+
249
+ case 'remote.pair': {
250
+ require('child_process').execFile('clideck-remote', ['pair', '--json'], { timeout: 15000 }, (err, stdout) => {
251
+ if (err) { ws.send(JSON.stringify({ type: 'remote.error', error: err.message })); return; }
252
+ try { ws.send(JSON.stringify({ type: 'remote.paired', ...JSON.parse(stdout) })); }
253
+ catch { ws.send(JSON.stringify({ type: 'remote.error', error: 'Invalid response from clideck-remote' })); }
254
+ });
255
+ break;
256
+ }
257
+
258
+ case 'remote.unpair': {
259
+ require('child_process').execFile('clideck-remote', ['unpair', '--json'], { timeout: 5000 }, (err) => {
260
+ if (err) {
261
+ ws.send(JSON.stringify({ type: 'remote.error', error: err.message }));
262
+ } else {
263
+ sessions.broadcast({ type: 'remote.unpaired' });
264
+ }
265
+ });
266
+ break;
267
+ }
268
+
269
+ case 'remote.getHistory': {
270
+ const turns = transcript.getScreenTurns(msg.id, sessions.getSessions().get(msg.id)?.presetId)
271
+ || transcript.getLastTurns(msg.id, msg.limit || 50);
272
+ ws.send(JSON.stringify({ type: 'remote.history', id: msg.id, turns: turns || [] }));
273
+ break;
274
+ }
275
+
276
+ case 'remote.install': {
277
+ const proc = require('child_process').spawn('npm', ['install', '-g', 'clideck-remote'], {
278
+ shell: true, stdio: ['ignore', 'pipe', 'pipe'],
279
+ });
280
+ proc.stdout.on('data', d => ws.send(JSON.stringify({ type: 'remote.install.progress', text: d.toString() })));
281
+ proc.stderr.on('data', d => ws.send(JSON.stringify({ type: 'remote.install.progress', text: d.toString() })));
282
+ proc.on('close', code => ws.send(JSON.stringify({ type: 'remote.install.done', success: code === 0 })));
283
+ break;
284
+ }
285
+
210
286
  default:
211
287
  if (msg.type?.startsWith('plugin.')) plugins.handleMessage(msg);
212
288
  break;
@@ -256,14 +332,13 @@ function applyTelemetryConfig(preset) {
256
332
  }
257
333
 
258
334
  if (preset.presetId === 'opencode') {
259
- const pluginDir = join(home, '.config', 'opencode', 'plugins');
260
335
  const src = join(__dirname, 'opencode-plugin', 'clideck-bridge.js');
261
- mkdirSync(pluginDir, { recursive: true });
262
- copyFileSync(src, join(pluginDir, 'clideck-bridge.js'));
336
+ mkdirSync(opencodePluginDir, { recursive: true });
337
+ copyFileSync(src, join(opencodePluginDir, 'clideck-bridge.js'));
263
338
  // Remove old termix-bridge.js if present
264
- const old = join(pluginDir, 'termix-bridge.js');
339
+ const old = join(opencodePluginDir, 'termix-bridge.js');
265
340
  if (existsSync(old)) try { unlinkSync(old); } catch {}
266
- return { success: true, message: 'Installed bridge plugin to ~/.config/opencode/plugins/' };
341
+ return { success: true, message: `Installed bridge plugin to ${opencodePluginDir}` };
267
342
  }
268
343
 
269
344
  return { success: false, message: `No auto-setup for ${preset.presetId}` };
@@ -297,9 +372,8 @@ function removeTelemetryConfig(preset) {
297
372
  }
298
373
 
299
374
  if (preset.presetId === 'opencode') {
300
- const dir = join(home, '.config', 'opencode', 'plugins');
301
- try { unlinkSync(join(dir, 'clideck-bridge.js')); } catch {}
302
- try { unlinkSync(join(dir, 'termix-bridge.js')); } catch {}
375
+ try { unlinkSync(join(opencodePluginDir, 'clideck-bridge.js')); } catch {}
376
+ try { unlinkSync(join(opencodePluginDir, 'termix-bridge.js')); } catch {}
303
377
  return { success: true, message: 'Removed bridge plugin' };
304
378
  }
305
379
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clideck",
3
- "version": "1.22.6",
3
+ "version": "1.23.1",
4
4
  "description": "One screen for all your AI coding agents — run, monitor, and manage multiple CLI agents from a single browser tab",
5
5
  "main": "server.js",
6
6
  "bin": {
package/plugin-loader.js CHANGED
@@ -32,6 +32,7 @@ const plugins = new Map();
32
32
  const inputHooks = [];
33
33
  const outputHooks = [];
34
34
  const statusHooks = [];
35
+ const transcriptHooks = [];
35
36
  const sessionStatus = new Map(); // sessionId → boolean (dedup multi-client reports)
36
37
  const frontendHandlers = new Map();
37
38
  let broadcastFn = null;
@@ -41,7 +42,7 @@ let saveConfigFn = null;
41
42
  const settingsChangeHandlers = new Map(); // pluginId → [fn]
42
43
 
43
44
  function removeHooks(pluginId) {
44
- for (const arr of [inputHooks, outputHooks, statusHooks]) {
45
+ for (const arr of [inputHooks, outputHooks, statusHooks, transcriptHooks]) {
45
46
  for (let i = arr.length - 1; i >= 0; i--) {
46
47
  if (arr[i].pluginId === pluginId) arr.splice(i, 1);
47
48
  }
@@ -111,6 +112,7 @@ function buildApi(pluginId, pluginDir, state) {
111
112
  onSessionInput(fn) { inputHooks.push({ pluginId, fn }); },
112
113
  onSessionOutput(fn) { outputHooks.push({ pluginId, fn }); },
113
114
  onStatusChange(fn) { statusHooks.push({ pluginId, fn }); },
115
+ onTranscriptEntry(fn) { transcriptHooks.push({ pluginId, fn }); },
114
116
 
115
117
  sendToFrontend(event, data = {}) {
116
118
  broadcastFn?.({ ...data, type: `plugin.${pluginId}.${event}` });
@@ -184,6 +186,13 @@ function notifyStatus(id, working) {
184
186
  }
185
187
  }
186
188
 
189
+ function notifyTranscript(id, role, text) {
190
+ for (const h of transcriptHooks) {
191
+ try { h.fn(id, role, text); }
192
+ catch (e) { console.error(`[plugin:${h.pluginId}] transcript error: ${e.message}`); }
193
+ }
194
+ }
195
+
187
196
  function updateSetting(pluginId, key, value) {
188
197
  // Validate plugin exists (also prevents __proto__ pollution — Map lookup returns undefined)
189
198
  const plugin = plugins.get(pluginId);
@@ -241,6 +250,8 @@ function getInfo() {
241
250
  id: p.manifest.id,
242
251
  name: p.manifest.name,
243
252
  version: p.manifest.version,
253
+ author: p.manifest.author || '',
254
+ description: p.manifest.description || '',
244
255
  settings: p.manifest.settings || [],
245
256
  settingValues: cfg?.pluginSettings?.[p.manifest.id] || {},
246
257
  actions: p.actions,
@@ -279,7 +290,8 @@ function shutdown() {
279
290
  function clearStatus(id) { sessionStatus.delete(id); }
280
291
 
281
292
  module.exports = {
293
+ PLUGINS_DIR,
282
294
  init, shutdown,
283
- transformInput, notifyOutput, notifyStatus, clearStatus,
295
+ transformInput, notifyOutput, notifyStatus, notifyTranscript, clearStatus,
284
296
  handleMessage, updateSetting, getInfo, resolveFile,
285
297
  };
@@ -1,7 +1,9 @@
1
1
  {
2
2
  "id": "trim-clip",
3
3
  "name": "Trim Clip",
4
- "version": "1.1.0",
4
+ "version": "1.2.0",
5
+ "author": "CliDeck",
6
+ "description": "Copy selected terminal text with trailing whitespace trimmed",
5
7
  "settings": [
6
8
  {
7
9
  "key": "enabled",
@@ -1,7 +1,9 @@
1
1
  {
2
2
  "id": "voice-input",
3
3
  "name": "Voice Input",
4
- "version": "1.1.0",
4
+ "version": "1.2.0",
5
+ "author": "CliDeck",
6
+ "description": "Dictate prompts with your voice using Whisper speech-to-text",
5
7
  "settings": [
6
8
  {
7
9
  "key": "enabled",
Binary file