clideck 1.30.8 → 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 +6 -0
- package/clideck-ask-cli.js +2 -1
- package/handlers.js +15 -7
- package/package.json +1 -1
- package/plugins/trim-clip/clideck-plugin.json +8 -1
- package/plugins/trim-clip/client.js +38 -16
- package/runtime.js +29 -0
- package/server.js +2 -5
- package/sessions.js +2 -2
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.
|
package/clideck-ask-cli.js
CHANGED
|
@@ -20,7 +20,8 @@ function parseDuration(value) {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
function parseArgs(args) {
|
|
23
|
-
const
|
|
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 =
|
|
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 =
|
|
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:
|
|
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
|
|
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 || '')
|
|
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 || '')
|
|
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,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "trim-clip",
|
|
3
3
|
"name": "Trim Clip",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.3.0",
|
|
5
5
|
"author": "CliDeck",
|
|
6
6
|
"description": "Copy selected terminal text with trailing whitespace trimmed",
|
|
7
7
|
"icon": "<svg class=\"w-4 h-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"6\" cy=\"6\" r=\"3\"/><circle cx=\"6\" cy=\"18\" r=\"3\"/><line x1=\"20\" y1=\"4\" x2=\"8.12\" y2=\"15.88\"/><line x1=\"14.47\" y1=\"14.48\" x2=\"20\" y2=\"20\"/><line x1=\"8.12\" y1=\"8.12\" x2=\"12\" y2=\"12\"/></svg>",
|
|
@@ -11,6 +11,13 @@
|
|
|
11
11
|
"label": "Enabled",
|
|
12
12
|
"type": "toggle",
|
|
13
13
|
"default": true
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"key": "hotkey",
|
|
17
|
+
"label": "Trim Key",
|
|
18
|
+
"type": "text",
|
|
19
|
+
"default": "F8",
|
|
20
|
+
"description": "Key to copy selected terminal text with whitespace trimmed (e.g. F8, Ctrl+Shift+C)"
|
|
14
21
|
}
|
|
15
22
|
]
|
|
16
23
|
}
|
|
@@ -1,31 +1,53 @@
|
|
|
1
1
|
let enabled = true;
|
|
2
2
|
let btnEl = null;
|
|
3
|
+
let apiRef = null;
|
|
4
|
+
let currentHotkey = null;
|
|
5
|
+
|
|
6
|
+
async function trimAndCopy() {
|
|
7
|
+
if (!enabled) return;
|
|
8
|
+
const text = apiRef.getTerminalSelection();
|
|
9
|
+
if (!text || !text.trim()) { apiRef.toast('Select text to copy & trim', { type: 'warn' }); return; }
|
|
10
|
+
const trimmed = text
|
|
11
|
+
.split('\n')
|
|
12
|
+
.map(l => l.trimEnd())
|
|
13
|
+
.join('\n')
|
|
14
|
+
.replace(/^\n+/, '').replace(/\n+$/, '');
|
|
15
|
+
try {
|
|
16
|
+
await navigator.clipboard.writeText(trimmed);
|
|
17
|
+
const saved = text.length - trimmed.length;
|
|
18
|
+
apiRef.toast(saved ? `Copied & trimmed ${saved} char${saved !== 1 ? 's' : ''}` : 'Copied', { type: 'success' });
|
|
19
|
+
} catch {
|
|
20
|
+
apiRef.toast('Clipboard access denied — allow it in browser site settings', { type: 'error' });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function bindHotkey(hotkey) {
|
|
25
|
+
const next = hotkey || 'F8';
|
|
26
|
+
if (next === currentHotkey) return;
|
|
27
|
+
const prev = currentHotkey;
|
|
28
|
+
if (prev) apiRef.unregisterHotkey(prev);
|
|
29
|
+
if (apiRef.registerHotkey(next, trimAndCopy)) {
|
|
30
|
+
currentHotkey = next;
|
|
31
|
+
} else if (prev) {
|
|
32
|
+
apiRef.registerHotkey(prev, trimAndCopy);
|
|
33
|
+
apiRef.toast(`Hotkey "${next}" is taken, keeping "${prev}"`, { type: 'warn' });
|
|
34
|
+
} else {
|
|
35
|
+
apiRef.toast(`Hotkey "${next}" is unavailable`, { type: 'warn' });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
3
38
|
|
|
4
39
|
export function init(api) {
|
|
40
|
+
apiRef = api;
|
|
5
41
|
api.onMessage('settings', (msg) => {
|
|
6
42
|
enabled = msg.enabled !== false;
|
|
7
43
|
if (btnEl) btnEl.style.display = enabled ? '' : 'none';
|
|
44
|
+
bindHotkey(msg.hotkey || 'F8');
|
|
8
45
|
});
|
|
9
46
|
api.send('getSettings');
|
|
10
47
|
|
|
11
48
|
btnEl = api.addToolbarButton({
|
|
12
49
|
title: 'Trim & Copy',
|
|
13
50
|
icon: '<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M20 4 8.12 15.88M14.47 14.48 20 20M8.12 8.12 12 12"/></svg>',
|
|
14
|
-
|
|
15
|
-
const text = api.getTerminalSelection();
|
|
16
|
-
if (!text || !text.trim()) { api.toast('Select text to copy & trim', { type: 'warn' }); return; }
|
|
17
|
-
const trimmed = text
|
|
18
|
-
.split('\n')
|
|
19
|
-
.map(l => l.trimEnd())
|
|
20
|
-
.join('\n')
|
|
21
|
-
.replace(/^\n+/, '').replace(/\n+$/, '');
|
|
22
|
-
try {
|
|
23
|
-
await navigator.clipboard.writeText(trimmed);
|
|
24
|
-
const saved = text.length - trimmed.length;
|
|
25
|
-
api.toast(saved ? `Copied & trimmed ${saved} char${saved !== 1 ? 's' : ''}` : 'Copied', { type: 'success' });
|
|
26
|
-
} catch {
|
|
27
|
-
api.toast('Clipboard access denied — allow it in browser site settings', { type: 'error' });
|
|
28
|
-
}
|
|
29
|
-
}
|
|
51
|
+
onClick: trimAndCopy,
|
|
30
52
|
});
|
|
31
53
|
}
|
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 =
|
|
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 =
|
|
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));
|