clideck 1.30.9 → 1.31.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 CHANGED
@@ -39,6 +39,12 @@ Open [localhost:4000](http://localhost:4000). Click **+**, pick an agent, start
39
39
 
40
40
  Or just run it once with `npx clideck`. Works on macOS and Windows. Node 18+. Linux: untested - if you try it, [open an issue](https://github.com/rustykuntz/clideck/issues).
41
41
 
42
+ If port `4000` is already in use:
43
+
44
+ ```bash
45
+ clideck --port 4001
46
+ ```
47
+
42
48
  ## What makes it useful
43
49
 
44
50
  **Live status** - see which agent is working and which is waiting. Status detection for Claude Code, Codex, Gemini CLI, and OpenCode.
@@ -20,7 +20,8 @@ function parseDuration(value) {
20
20
  }
21
21
 
22
22
  function parseArgs(args) {
23
- const out = { timeoutMs: 10 * 60 * 1000, url: process.env.CLIDECK_URL || 'http://127.0.0.1:4000' };
23
+ const port = process.env.CLIDECK_PORT || process.env.PORT || '4000';
24
+ const out = { timeoutMs: 10 * 60 * 1000, url: process.env.CLIDECK_URL || `http://127.0.0.1:${port}` };
24
25
  const positional = [];
25
26
  for (let i = 0; i < args.length; i++) {
26
27
  const arg = args[i];
package/handlers.js CHANGED
@@ -7,6 +7,7 @@ const sessions = require('./sessions');
7
7
  const themes = require('./themes');
8
8
  const presets = JSON.parse(readFileSync(join(__dirname, 'agent-presets.json'), 'utf8'));
9
9
  const { listDirs, binName, defaultShell } = require('./utils');
10
+ const { PORT } = require('./runtime');
10
11
  for (const p of presets) if (p.presetId === 'shell') p.command = defaultShell;
11
12
  function isPresetEnabled(preset) {
12
13
  if (!preset?.enabledIfEnv) return true;
@@ -150,7 +151,7 @@ function codexConfigLooksHealthy(content, port) {
150
151
 
151
152
  function detectTelemetryConfig(c) {
152
153
  const home = os.homedir();
153
- const port = '4000';
154
+ const port = String(PORT);
154
155
  let changed = false;
155
156
  const attemptedRepairs = new Set();
156
157
 
@@ -555,7 +556,7 @@ function onConnection(ws) {
555
556
 
556
557
  // Deterministic telemetry config writers per agent — no AI, no YOLO
557
558
  function applyTelemetryConfig(preset) {
558
- const port = '4000';
559
+ const port = String(PORT);
559
560
  const home = os.homedir();
560
561
 
561
562
  try {
@@ -595,9 +596,10 @@ function applyTelemetryConfig(preset) {
595
596
  let content = '';
596
597
  if (existsSync(configPath)) content = readFileSync(configPath, 'utf8');
597
598
  const hasOtel = content.includes('[otel]');
599
+ const hasCurrentOtel = content.includes(`localhost:${port}`);
598
600
  const hasNotify = /^\s*notify\s*=.*notify-helper/m.test(content);
599
601
  const hasWrongOtel = content.includes(`endpoint = "http://localhost:${port}/v1/logs"`);
600
- if (hasOtel && hasNotify && !hasWrongOtel && !existsSync(hooksPath) && !/(^|\n)\[features\][\s\S]*?codex_hooks\s*=/.test(content)) {
602
+ if (hasOtel && hasCurrentOtel && hasNotify && !hasWrongOtel && !existsSync(hooksPath) && !/(^|\n)\[features\][\s\S]*?codex_hooks\s*=/.test(content)) {
601
603
  return { success: true, message: 'Already configured' };
602
604
  }
603
605
  const notifyHelperPath = join(__dirname, 'bin', 'notify-helper.js').replace(/\\/g, '/');
@@ -619,20 +621,26 @@ function applyTelemetryConfig(preset) {
619
621
  const hooks = settings.hooks || {};
620
622
  const helperPath = join(__dirname, 'bin', 'gemini-hook.js').replace(/\\/g, '/');
621
623
  const nodePath = process.execPath.replace(/\\/g, '/');
624
+ const hookCmd = (route) => `"${nodePath}" "${helperPath}" ${port} ${route}`;
622
625
  const geminiHook = (route) => ({
623
626
  matcher: '*',
624
- hooks: [{ type: 'command', command: `"${nodePath}" "${helperPath}" ${port} ${route}`, name: `clideck-${route}`, timeout: 5000 }],
627
+ hooks: [{ type: 'command', command: hookCmd(route), name: `clideck-${route}`, timeout: 5000 }],
625
628
  });
626
- const has = (arr, route) => arr?.some(h => h.hooks?.some(x => x.command?.includes('gemini-hook.js') && x.command?.includes(` ${route}`)));
629
+ const has = (arr, route) => arr?.some(h => h.hooks?.some(x => x.command === hookCmd(route)));
627
630
  if (has(hooks.BeforeAgent, 'start') && has(hooks.AfterAgent, 'stop') && has(hooks.SessionEnd, 'stop') && has(hooks.BeforeTool, 'menu')) {
628
631
  return { success: true, message: 'Already configured' };
629
632
  }
633
+ const stripOld = (arr) => (arr || []).filter(h => !h.hooks?.some(x => x.command?.includes('gemini-hook.js')));
634
+ hooks.BeforeAgent = stripOld(hooks.BeforeAgent);
635
+ hooks.AfterAgent = stripOld(hooks.AfterAgent);
636
+ hooks.SessionEnd = stripOld(hooks.SessionEnd);
637
+ hooks.BeforeTool = stripOld(hooks.BeforeTool);
630
638
  if (!has(hooks.BeforeAgent, 'start')) hooks.BeforeAgent = [...(hooks.BeforeAgent || []), geminiHook('start')];
631
639
  if (!has(hooks.AfterAgent, 'stop')) hooks.AfterAgent = [...(hooks.AfterAgent || []), geminiHook('stop')];
632
640
  if (!has(hooks.SessionEnd, 'stop')) hooks.SessionEnd = [...(hooks.SessionEnd || []), geminiHook('stop')];
633
641
  if (!has(hooks.BeforeTool, 'menu')) hooks.BeforeTool = [...(hooks.BeforeTool || []), geminiHook('menu')];
634
642
  settings.hooks = hooks;
635
- if (settings.telemetry?.target === 'local' && String(settings.telemetry?.otlpEndpoint || '').includes(`localhost:${port}`)) delete settings.telemetry;
643
+ if (settings.telemetry?.target === 'local' && /localhost:\d+/.test(String(settings.telemetry?.otlpEndpoint || ''))) delete settings.telemetry;
636
644
  mkdirSync(dirname(configPath), { recursive: true });
637
645
  writeFileSync(configPath, JSON.stringify(settings, null, 2) + '\n');
638
646
  return { success: true, message: 'Added CliDeck hooks to ~/.gemini/settings.json' };
@@ -700,7 +708,7 @@ function removeTelemetryConfig(preset) {
700
708
  if (!settings.hooks[event].length) delete settings.hooks[event];
701
709
  }
702
710
  if (settings.hooks && !Object.keys(settings.hooks).length) delete settings.hooks;
703
- if (settings.telemetry?.target === 'local' && String(settings.telemetry?.otlpEndpoint || '').includes('localhost:4000')) delete settings.telemetry;
711
+ if (settings.telemetry?.target === 'local' && /localhost:\d+/.test(String(settings.telemetry?.otlpEndpoint || ''))) delete settings.telemetry;
704
712
  writeFileSync(configPath, JSON.stringify(settings, null, 2) + '\n');
705
713
  return { success: true, message: 'Removed CliDeck hooks from ~/.gemini/settings.json' };
706
714
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clideck",
3
- "version": "1.30.9",
3
+ "version": "1.31.0",
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/runtime.js ADDED
@@ -0,0 +1,29 @@
1
+ function argValue(name) {
2
+ const idx = process.argv.indexOf(name);
3
+ if (idx < 0) return '';
4
+ const value = process.argv[idx + 1];
5
+ return value && !value.startsWith('-') ? value : '';
6
+ }
7
+
8
+ function parsePort(value) {
9
+ const port = Number.parseInt(String(value || ''), 10);
10
+ return Number.isInteger(port) && port > 0 && port <= 65535 ? port : null;
11
+ }
12
+
13
+ const PORT = parsePort(argValue('--port'))
14
+ || parsePort(process.env.CLIDECK_PORT)
15
+ || parsePort(process.env.PORT)
16
+ || 4000;
17
+
18
+ const HOST = (() => {
19
+ const idx = process.argv.indexOf('--host');
20
+ const value = idx >= 0 ? process.argv[idx + 1] : '';
21
+ if (idx < 0) return '127.0.0.1';
22
+ return value && !value.startsWith('-') ? value : '0.0.0.0';
23
+ })();
24
+
25
+ function localUrl(host = HOST, port = PORT) {
26
+ return `http://${host === '0.0.0.0' ? 'localhost' : host}:${port}`;
27
+ }
28
+
29
+ module.exports = { PORT, HOST, localUrl };
package/server.js CHANGED
@@ -3,6 +3,7 @@ const { readFileSync, existsSync } = require('fs');
3
3
  const { join, extname, resolve } = require('path');
4
4
  const { WebSocketServer } = require('ws');
5
5
  const { ensurePtyHelper } = require('./utils');
6
+ const { PORT, HOST, localUrl } = require('./runtime');
6
7
 
7
8
  function terminalLink(url, text = url) {
8
9
  return `\u001B]8;;${url}\u0007${text}\u001B]8;;\u0007`;
@@ -63,10 +64,6 @@ require('./opencode-bridge').init(sessions.broadcast, sessions.getSessions);
63
64
  const config = require('./config');
64
65
  plugins.init(sessions.broadcast, sessions.getSessions, () => require('./handlers').getConfig(), (cfg) => config.save(cfg), sessions.input, sessions.createProgrammatic, sessions.close);
65
66
 
66
- const PORT = 4000;
67
- const hostIdx = process.argv.indexOf('--host');
68
- const hostArg = hostIdx >= 0 ? process.argv[hostIdx + 1] : undefined;
69
- const HOST = hostIdx < 0 ? '127.0.0.1' : (hostArg && !hostArg.startsWith('-') ? hostArg : '0.0.0.0');
70
67
  const MIME = { '.html': 'text/html', '.css': 'text/css', '.js': 'application/javascript', '.png': 'image/png', '.svg': 'image/svg+xml', '.mp3': 'audio/mpeg' };
71
68
  const ALIASES = {
72
69
  '/xterm.css': join(__dirname, 'node_modules/@xterm/xterm/css/xterm.css'),
@@ -293,7 +290,7 @@ process.on('SIGTERM', onShutdown);
293
290
 
294
291
  server.listen(PORT, HOST, () => {
295
292
  const v = require('./package.json').version;
296
- const url = `http://${HOST === '0.0.0.0' ? 'localhost' : HOST}:${PORT}`;
293
+ const url = localUrl();
297
294
  const clickableUrl = terminalLink(url);
298
295
  const urlHint = openUrlHint();
299
296
  console.log(`
package/sessions.js CHANGED
@@ -10,7 +10,7 @@ const plugins = require('./plugin-loader');
10
10
 
11
11
  const THEMES = require('./themes');
12
12
  const MAX_BUFFER = 200 * 1024;
13
- const PORT = 4000;
13
+ const { PORT, localUrl } = require('./runtime');
14
14
  const ANSI_RE = /\x1b\[[0-9;?]*[ -/]*[@-~]|\x1b\].*?(?:\x07|\x1b\\)|\x1b./g;
15
15
  const PRESETS = JSON.parse(require('fs').readFileSync(join(__dirname, 'agent-presets.json'), 'utf8'));
16
16
  for (const p of PRESETS) if (p.presetId === 'shell') p.command = defaultShell;
@@ -62,7 +62,7 @@ function buildTelemetryEnv(id, cmd) {
62
62
  const bin = binName(cmd.command);
63
63
  const preset = PRESETS.find(p => binName(p.command) === bin);
64
64
  const telemetryEnabled = cmd.telemetryEnabled ?? (preset?.presetId === 'claude-code');
65
- const env = { CLIDECK_SESSION_ID: id };
65
+ const env = { CLIDECK_SESSION_ID: id, CLIDECK_PORT: String(PORT), CLIDECK_URL: localUrl() };
66
66
  if (!preset?.telemetryEnv || !telemetryEnabled) return env;
67
67
  for (const [k, v] of Object.entries(preset.telemetryEnv)) {
68
68
  env[k] = v.replace('{{port}}', String(PORT));