clideck 1.27.0 → 1.29.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 +7 -0
- package/agent-presets.json +7 -9
- package/bin/claude-hook.js +31 -0
- package/bin/gemini-hook.js +31 -0
- package/bin/notify-helper.js +9 -2
- package/codex-config.js +77 -0
- package/config.js +2 -0
- package/handlers.js +164 -56
- package/package.json +1 -2
- package/plugin-loader.js +176 -50
- package/plugins/autopilot/clideck-plugin.json +5 -3
- package/plugins/autopilot/index.js +24 -30
- package/plugins/autopilot/package.json +7 -0
- package/plugins/trim-clip/clideck-plugin.json +1 -0
- package/plugins/voice-input/clideck-plugin.json +1 -0
- package/public/index.html +2 -4
- package/public/js/app.js +62 -5
- package/public/js/creator.js +14 -2
- package/public/js/settings.js +8 -6
- package/public/js/terminals.js +126 -26
- package/public/js/toast.js +2 -17
- package/public/js/utils.js +9 -0
- package/public/tailwind.css +1 -1
- package/server.js +87 -21
- package/sessions.js +41 -8
- package/telemetry-receiver.js +83 -80
- package/tools/merge-jsonl-roles.mjs +182 -0
- package/transcript-builder.js +53 -0
- package/transcript-parser.js +135 -0
- package/transcript.js +115 -181
package/README.md
CHANGED
|
@@ -72,6 +72,13 @@ clideck auto-detects whether each agent is working or idle:
|
|
|
72
72
|
|
|
73
73
|
Claude Code works out of the box. Other agents need a one-time setup that clideck walks you through.
|
|
74
74
|
|
|
75
|
+
Minimum supported agent versions:
|
|
76
|
+
|
|
77
|
+
- Gemini CLI `v0.36.0+`
|
|
78
|
+
- OpenAI Codex `v0.118.0+`
|
|
79
|
+
- Claude Code `v2.1.90+`
|
|
80
|
+
- OpenCode `v1.2.26+`
|
|
81
|
+
|
|
75
82
|
## How It Works
|
|
76
83
|
|
|
77
84
|
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.
|
package/agent-presets.json
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
"name": "Claude Code",
|
|
5
5
|
"icon": "/img/claude-code.png",
|
|
6
6
|
"command": "claude",
|
|
7
|
+
"minVersion": "2.1.90",
|
|
7
8
|
"installCmd": "npm install -g @anthropic-ai/claude-code",
|
|
8
9
|
"isAgent": true,
|
|
9
10
|
"canResume": true,
|
|
@@ -28,6 +29,7 @@
|
|
|
28
29
|
"name": "Codex",
|
|
29
30
|
"icon": "/img/codex.png",
|
|
30
31
|
"command": "codex --no-alt-screen",
|
|
32
|
+
"minVersion": "0.118.0",
|
|
31
33
|
"installCmd": "npm install -g @openai/codex",
|
|
32
34
|
"isAgent": true,
|
|
33
35
|
"canResume": true,
|
|
@@ -41,7 +43,7 @@
|
|
|
41
43
|
"OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:{{port}}",
|
|
42
44
|
"OTEL_LOGS_EXPORT_INTERVAL": "2000"
|
|
43
45
|
},
|
|
44
|
-
"telemetrySetup": "For best experience, add to ~/.codex/config.toml:\n\n[otel]\nexporter = { otlp-http = { endpoint = \"http://localhost:{{port}}
|
|
46
|
+
"telemetrySetup": "For best experience, add to ~/.codex/config.toml:\n\n[otel]\nexporter = { otlp-http = { endpoint = \"http://localhost:{{port}}\", protocol = \"json\" } }",
|
|
45
47
|
"telemetryAutoSetup": {
|
|
46
48
|
"label": "Configure automatically"
|
|
47
49
|
}
|
|
@@ -51,6 +53,7 @@
|
|
|
51
53
|
"name": "Gemini CLI",
|
|
52
54
|
"icon": "/img/gemini.png",
|
|
53
55
|
"command": "gemini",
|
|
56
|
+
"minVersion": "0.36.0",
|
|
54
57
|
"installCmd": "npm install -g @google/gemini-cli",
|
|
55
58
|
"isAgent": true,
|
|
56
59
|
"canResume": true,
|
|
@@ -58,15 +61,9 @@
|
|
|
58
61
|
"sessionIdPattern": "Session ID:\\s+([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})",
|
|
59
62
|
"outputMarker": "\u2726",
|
|
60
63
|
"telemetryConfigPath": "~/.gemini/settings.json",
|
|
61
|
-
"
|
|
62
|
-
"GEMINI_TELEMETRY_ENABLED": "true",
|
|
63
|
-
"GEMINI_TELEMETRY_TARGET": "local",
|
|
64
|
-
"GEMINI_TELEMETRY_OTLP_ENDPOINT": "http://localhost:{{port}}",
|
|
65
|
-
"GEMINI_TELEMETRY_OTLP_PROTOCOL": "http"
|
|
66
|
-
},
|
|
67
|
-
"telemetrySetup": "For best experience, add to ~/.gemini/settings.json:\n\n{\n \"telemetry\": {\n \"enabled\": true,\n \"target\": \"local\",\n \"otlpEndpoint\": \"http://localhost:{{port}}/v1/logs\",\n \"otlpProtocol\": \"http\",\n \"logPrompts\": true\n }\n}",
|
|
64
|
+
"telemetrySetup": "Required for working/idle status, resume, Autopilot, notifications, and mobile remote.\n\nCliDeck will add BeforeAgent/AfterAgent/SessionEnd/BeforeTool hooks to ~/.gemini/settings.json.",
|
|
68
65
|
"telemetryAutoSetup": {
|
|
69
|
-
"label": "
|
|
66
|
+
"label": "Patch Gemini"
|
|
70
67
|
}
|
|
71
68
|
},
|
|
72
69
|
{
|
|
@@ -74,6 +71,7 @@
|
|
|
74
71
|
"name": "OpenCode",
|
|
75
72
|
"icon": "/img/opencode.png",
|
|
76
73
|
"command": "opencode",
|
|
74
|
+
"minVersion": "1.2.26",
|
|
77
75
|
"installCmd": "npm install -g opencode-ai",
|
|
78
76
|
"isAgent": true,
|
|
79
77
|
"canResume": true,
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const http = require('http');
|
|
4
|
+
|
|
5
|
+
const port = parseInt(process.argv[2], 10);
|
|
6
|
+
const route = process.argv[3];
|
|
7
|
+
if (!port || !route) process.exit(0);
|
|
8
|
+
|
|
9
|
+
let stdin = '';
|
|
10
|
+
process.stdin.setEncoding('utf8');
|
|
11
|
+
process.stdin.on('data', (chunk) => { stdin += chunk; });
|
|
12
|
+
process.stdin.on('end', () => {
|
|
13
|
+
let hook = {};
|
|
14
|
+
try { hook = stdin ? JSON.parse(stdin) : {}; } catch {}
|
|
15
|
+
const body = JSON.stringify({
|
|
16
|
+
clideck_id: process.env.CLIDECK_SESSION_ID || '',
|
|
17
|
+
session_id: hook.session_id || '',
|
|
18
|
+
payload: stdin.trim() || undefined,
|
|
19
|
+
});
|
|
20
|
+
const req = http.request({
|
|
21
|
+
hostname: 'localhost',
|
|
22
|
+
port,
|
|
23
|
+
path: `/hook/claude/${route}`,
|
|
24
|
+
method: 'POST',
|
|
25
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
|
|
26
|
+
timeout: 2000,
|
|
27
|
+
});
|
|
28
|
+
req.on('error', () => {});
|
|
29
|
+
req.end(body);
|
|
30
|
+
});
|
|
31
|
+
process.stdin.resume();
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const http = require('http');
|
|
4
|
+
|
|
5
|
+
const port = parseInt(process.argv[2], 10);
|
|
6
|
+
const route = process.argv[3];
|
|
7
|
+
if (!port || !route) process.exit(0);
|
|
8
|
+
|
|
9
|
+
let stdin = '';
|
|
10
|
+
process.stdin.setEncoding('utf8');
|
|
11
|
+
process.stdin.on('data', (chunk) => { stdin += chunk; });
|
|
12
|
+
process.stdin.on('end', () => {
|
|
13
|
+
let hook = {};
|
|
14
|
+
try { hook = stdin ? JSON.parse(stdin) : {}; } catch {}
|
|
15
|
+
const body = JSON.stringify({
|
|
16
|
+
clideck_id: process.env.CLIDECK_SESSION_ID || '',
|
|
17
|
+
session_id: hook.session_id || process.env.GEMINI_SESSION_ID || '',
|
|
18
|
+
payload: stdin.trim() || undefined,
|
|
19
|
+
});
|
|
20
|
+
const req = http.request({
|
|
21
|
+
hostname: 'localhost',
|
|
22
|
+
port,
|
|
23
|
+
path: `/hook/gemini/${route}`,
|
|
24
|
+
method: 'POST',
|
|
25
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
|
|
26
|
+
timeout: 2000,
|
|
27
|
+
});
|
|
28
|
+
req.on('error', () => {});
|
|
29
|
+
req.end(body);
|
|
30
|
+
});
|
|
31
|
+
process.stdin.resume();
|
package/bin/notify-helper.js
CHANGED
|
@@ -5,8 +5,15 @@
|
|
|
5
5
|
// Port is passed as the first argument by the notify config.
|
|
6
6
|
|
|
7
7
|
const port = parseInt(process.argv[2], 10);
|
|
8
|
-
const
|
|
9
|
-
|
|
8
|
+
const raw = process.argv[process.argv.length - 1];
|
|
9
|
+
const clideckId = process.env.CLIDECK_SESSION_ID || '';
|
|
10
|
+
if (!port || !raw || raw === String(port)) process.exit(0);
|
|
11
|
+
|
|
12
|
+
let payload = raw;
|
|
13
|
+
try {
|
|
14
|
+
const parsed = JSON.parse(raw);
|
|
15
|
+
payload = JSON.stringify({ ...parsed, clideck_id: clideckId || undefined });
|
|
16
|
+
} catch {}
|
|
10
17
|
|
|
11
18
|
const http = require('http');
|
|
12
19
|
const req = http.request({
|
package/codex-config.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
function normalizeCodexToml(content) {
|
|
2
|
+
return String(content || '')
|
|
3
|
+
.replace(/\r\n/g, '\n')
|
|
4
|
+
.replace(/([^\n])(\[(?:projects|notice|plugins)\.[^\n]*\])/g, '$1\n$2')
|
|
5
|
+
.replace(/([^\n])(\[(?:features|otel)\])/g, '$1\n$2')
|
|
6
|
+
.replace(/([^\n])(\[\[skills\.[^\n]*\]\])/g, '$1\n$2');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function splitTomlSections(content) {
|
|
10
|
+
const lines = normalizeCodexToml(content).split('\n');
|
|
11
|
+
const top = [];
|
|
12
|
+
const sections = [];
|
|
13
|
+
let current = null;
|
|
14
|
+
for (const line of lines) {
|
|
15
|
+
if (/^\s*\[.*\]\s*$/.test(line)) {
|
|
16
|
+
current = { header: line.trim(), lines: [] };
|
|
17
|
+
sections.push(current);
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (current) current.lines.push(line);
|
|
21
|
+
else top.push(line);
|
|
22
|
+
}
|
|
23
|
+
return { top, sections };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function trimBlankEdges(lines) {
|
|
27
|
+
const out = [...lines];
|
|
28
|
+
while (out.length && !out[0].trim()) out.shift();
|
|
29
|
+
while (out.length && !out[out.length - 1].trim()) out.pop();
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function upsertCodexConfig(content, nodePath, notifyHelperPath, port) {
|
|
34
|
+
const { top, sections } = splitTomlSections(content);
|
|
35
|
+
const notifyLine = `notify = ["${nodePath}", "${notifyHelperPath}", "${port}"]`;
|
|
36
|
+
const cleanedTop = top.filter(line => !/^\s*notify\s*=/.test(line));
|
|
37
|
+
const topOut = trimBlankEdges([...cleanedTop, notifyLine]);
|
|
38
|
+
sections.forEach(section => {
|
|
39
|
+
section.lines = section.lines.filter(line => !/^\s*notify\s*=/.test(line));
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const keptSections = sections
|
|
43
|
+
.map(section => section.header === '[features]'
|
|
44
|
+
? { ...section, lines: trimBlankEdges(section.lines.filter(line => !/^\s*codex_hooks\s*=/.test(line))) }
|
|
45
|
+
: section)
|
|
46
|
+
.filter(section => section.header !== '[features]' || section.lines.length);
|
|
47
|
+
|
|
48
|
+
const otelHeader = '[otel]';
|
|
49
|
+
const otelBody = [`exporter = { otlp-http = { endpoint = "http://localhost:${port}", protocol = "json" } }`];
|
|
50
|
+
const withoutOtel = keptSections.filter(s => s.header !== otelHeader);
|
|
51
|
+
withoutOtel.push({ header: otelHeader, lines: otelBody });
|
|
52
|
+
|
|
53
|
+
const out = [];
|
|
54
|
+
if (topOut.length) out.push(...topOut, '');
|
|
55
|
+
withoutOtel.forEach((section, idx) => {
|
|
56
|
+
out.push(section.header, ...trimBlankEdges(section.lines));
|
|
57
|
+
if (idx < withoutOtel.length - 1) out.push('');
|
|
58
|
+
});
|
|
59
|
+
return out.join('\n').trimEnd() + '\n';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function validateCodexConfigToml(content) {
|
|
63
|
+
const lines = normalizeCodexToml(content).split('\n');
|
|
64
|
+
for (let i = 0; i < lines.length; i++) {
|
|
65
|
+
const line = lines[i];
|
|
66
|
+
const t = line.trim();
|
|
67
|
+
if (!t || t.startsWith('#')) continue;
|
|
68
|
+
if (/^\[\[.*\]\]$/.test(t) || /^\[.*\]$/.test(t)) continue;
|
|
69
|
+
if (!t.includes('=')) return { ok: false, error: `Invalid TOML line ${i + 1}: ${t}` };
|
|
70
|
+
if (/(?:^|[^\s=])\[(?:projects|notice|plugins)\.|(?:^|[^\s=])\[(?:features|otel)\]|(?:^|[^\s=])\[\[skills\./.test(t)) {
|
|
71
|
+
return { ok: false, error: `Glued TOML headers on line ${i + 1}` };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return { ok: true };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = { upsertCodexConfig, validateCodexConfigToml };
|
package/config.js
CHANGED
package/handlers.js
CHANGED
|
@@ -10,6 +10,7 @@ const { listDirs, binName, defaultShell } = require('./utils');
|
|
|
10
10
|
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
|
+
const { upsertCodexConfig, validateCodexConfigToml } = require('./codex-config');
|
|
13
14
|
|
|
14
15
|
const opencodePluginDir = join(
|
|
15
16
|
process.platform === 'win32' ? (process.env.APPDATA || join(os.homedir(), 'AppData', 'Roaming')) : join(os.homedir(), '.config'),
|
|
@@ -33,6 +34,28 @@ let remoteUpdateCache = null;
|
|
|
33
34
|
let remoteUpdateCheckedAt = 0;
|
|
34
35
|
const REMOTE_UPDATE_INTERVAL = 3600000;
|
|
35
36
|
|
|
37
|
+
function compareVersions(a, b) {
|
|
38
|
+
const pa = String(a || '').split('.').map(n => parseInt(n, 10) || 0);
|
|
39
|
+
const pb = String(b || '').split('.').map(n => parseInt(n, 10) || 0);
|
|
40
|
+
const len = Math.max(pa.length, pb.length);
|
|
41
|
+
for (let i = 0; i < len; i++) {
|
|
42
|
+
const diff = (pa[i] || 0) - (pb[i] || 0);
|
|
43
|
+
if (diff) return diff;
|
|
44
|
+
}
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parseVersion(text) {
|
|
49
|
+
const m = String(text || '').match(/\b(\d+\.\d+\.\d+)\b/);
|
|
50
|
+
return m ? m[1] : '';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getInstalledVersion(bin) {
|
|
54
|
+
try { return parseVersion(execFileSync(bin, ['--version'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] })); } catch {}
|
|
55
|
+
try { return parseVersion(execFileSync(bin, ['-v'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] })); } catch {}
|
|
56
|
+
return '';
|
|
57
|
+
}
|
|
58
|
+
|
|
36
59
|
function checkRemoteUpdate(ws) {
|
|
37
60
|
const now = Date.now();
|
|
38
61
|
if (remoteUpdateCache && now - remoteUpdateCheckedAt < REMOTE_UPDATE_INTERVAL) {
|
|
@@ -46,7 +69,7 @@ function checkRemoteUpdate(ws) {
|
|
|
46
69
|
require('child_process').execFile('npm', ['view', 'clideck-remote', 'version'], { shell: shellOpt, timeout: 10000 }, (err2, stdout2) => {
|
|
47
70
|
if (err2) return;
|
|
48
71
|
const latest = stdout2.trim();
|
|
49
|
-
remoteUpdateCache = { installed, latest, available: latest
|
|
72
|
+
remoteUpdateCache = { installed, latest, available: compareVersions(latest, installed) > 0 };
|
|
50
73
|
remoteUpdateCheckedAt = now;
|
|
51
74
|
if (remoteUpdateCache.available) ws.send(JSON.stringify({ type: 'remote.update', ...remoteUpdateCache }));
|
|
52
75
|
});
|
|
@@ -57,9 +80,20 @@ function checkRemoteUpdate(ws) {
|
|
|
57
80
|
const whichCmd = process.platform === 'win32' ? 'where' : 'which';
|
|
58
81
|
function checkAvailability() {
|
|
59
82
|
for (const p of presets) {
|
|
60
|
-
if (p.presetId === 'shell') { p.available = true; continue; }
|
|
61
|
-
|
|
62
|
-
|
|
83
|
+
if (p.presetId === 'shell') { p.available = true; p.version = ''; p.versionOk = true; p.health = { ok: true }; continue; }
|
|
84
|
+
const bin = binName(p.command);
|
|
85
|
+
try {
|
|
86
|
+
execFileSync(whichCmd, [bin], { stdio: 'ignore' });
|
|
87
|
+
p.available = true;
|
|
88
|
+
p.version = getInstalledVersion(bin);
|
|
89
|
+
p.versionOk = !p.minVersion || (p.version && compareVersions(p.version, p.minVersion) >= 0);
|
|
90
|
+
p.health = p.versionOk ? { ok: true } : { ok: false, reason: `Update required (${p.minVersion}+)` };
|
|
91
|
+
} catch {
|
|
92
|
+
p.available = false;
|
|
93
|
+
p.version = '';
|
|
94
|
+
p.versionOk = true;
|
|
95
|
+
p.health = { ok: false, reason: 'Not installed' };
|
|
96
|
+
}
|
|
63
97
|
}
|
|
64
98
|
}
|
|
65
99
|
checkAvailability();
|
|
@@ -71,39 +105,61 @@ function detectTelemetryConfig(c) {
|
|
|
71
105
|
const home = os.homedir();
|
|
72
106
|
const port = '4000';
|
|
73
107
|
let changed = false;
|
|
108
|
+
let repairedAny = false;
|
|
74
109
|
for (const cmd of c.commands || []) {
|
|
75
110
|
const bin = binName(cmd.command);
|
|
76
111
|
const preset = presets.find(p => binName(p.command) === bin);
|
|
77
112
|
if (!preset) continue;
|
|
78
113
|
let detected = false;
|
|
114
|
+
let reason = '';
|
|
79
115
|
if (preset.presetId === 'claude-code') {
|
|
80
116
|
try {
|
|
81
117
|
const s = JSON.parse(readFileSync(join(home, '.claude', 'settings.json'), 'utf8'));
|
|
82
118
|
const hooks = s.hooks || {};
|
|
83
|
-
const has = (arr, path) => arr?.some(h => h.hooks?.some(x => x.
|
|
119
|
+
const has = (arr, path) => arr?.some(h => h.hooks?.some(x => x.command?.includes('claude-hook.js') && x.command?.includes(` ${path}`)));
|
|
84
120
|
detected = has(hooks.UserPromptSubmit, 'start') && has(hooks.Stop, 'stop') && has(hooks.StopFailure, 'stop')
|
|
85
121
|
&& has(hooks.PreToolUse, 'menu')
|
|
86
|
-
&& hooks.Notification?.some(h => h.matcher === 'idle_prompt' && h.hooks?.some(x => x.
|
|
122
|
+
&& hooks.Notification?.some(h => h.matcher === 'idle_prompt' && h.hooks?.some(x => x.command?.includes('claude-hook.js') && x.command?.includes(' idle')));
|
|
123
|
+
if (!detected) reason = 'Needs re-patch';
|
|
87
124
|
} catch {}
|
|
88
125
|
} else if (preset.presetId === 'codex') {
|
|
89
126
|
try {
|
|
90
127
|
const content = readFileSync(join(home, '.codex', 'config.toml'), 'utf8');
|
|
91
|
-
detected = content.includes('[otel]') && content.includes(`localhost:${port}`)
|
|
128
|
+
detected = content.includes('[otel]') && /^\s*notify\s*=.*notify-helper/m.test(content) && content.includes(`localhost:${port}`);
|
|
129
|
+
if (!detected) reason = 'Needs re-patch';
|
|
92
130
|
} catch {}
|
|
93
131
|
} else if (preset.presetId === 'gemini-cli') {
|
|
94
132
|
try {
|
|
95
133
|
const s = JSON.parse(readFileSync(join(home, '.gemini', 'settings.json'), 'utf8'));
|
|
96
|
-
|
|
134
|
+
const hooks = s.hooks || {};
|
|
135
|
+
const has = (arr, route) => arr?.some(h => h.hooks?.some(x => x.command?.includes('gemini-hook.js') && x.command?.includes(` ${route}`)));
|
|
136
|
+
detected = has(hooks.BeforeAgent, 'start') && has(hooks.AfterAgent, 'stop') && has(hooks.SessionEnd, 'stop') && has(hooks.BeforeTool, 'menu');
|
|
137
|
+
if (!detected) reason = 'Needs re-patch';
|
|
97
138
|
} catch {}
|
|
98
139
|
} else if (preset.presetId === 'opencode') {
|
|
99
140
|
detected = existsSync(join(opencodePluginDir, 'clideck-bridge.js')) || existsSync(join(opencodePluginDir, 'termix-bridge.js'));
|
|
141
|
+
if (!detected) reason = 'Needs re-patch';
|
|
100
142
|
} else { continue; }
|
|
101
|
-
if (
|
|
102
|
-
|
|
103
|
-
|
|
143
|
+
if (preset.available && preset.minVersion && !preset.versionOk) {
|
|
144
|
+
detected = false;
|
|
145
|
+
reason = `Update required (${preset.minVersion}+)`;
|
|
146
|
+
} else if (!detected && cmd.telemetryEnabled && preset.telemetryAutoSetup && preset.available && preset.versionOk) {
|
|
147
|
+
const repaired = applyTelemetryConfig(preset);
|
|
148
|
+
if (repaired.success) {
|
|
149
|
+
repairedAny = true;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const nextEnabled = detected || (!!cmd.telemetryEnabled && !reason.startsWith('Update required'));
|
|
154
|
+
const nextStatus = detected ? { ok: true } : { ok: false, error: reason || 'Needs setup' };
|
|
155
|
+
if (cmd.telemetryEnabled !== nextEnabled || JSON.stringify(cmd.telemetryStatus || null) !== JSON.stringify(nextStatus)) {
|
|
156
|
+
cmd.telemetryEnabled = nextEnabled;
|
|
157
|
+
cmd.telemetryStatus = nextStatus;
|
|
104
158
|
changed = true;
|
|
105
159
|
}
|
|
160
|
+
preset.health = detected ? { ok: true } : { ok: false, reason: reason || 'Needs setup' };
|
|
106
161
|
}
|
|
162
|
+
if (repairedAny) return detectTelemetryConfig(c) || true;
|
|
107
163
|
if (changed) console.log('Config: synced telemetry/plugin state from detected config files');
|
|
108
164
|
return changed;
|
|
109
165
|
}
|
|
@@ -142,10 +198,16 @@ function onConnection(ws) {
|
|
|
142
198
|
}
|
|
143
199
|
break;
|
|
144
200
|
case 'terminal.buffer': {
|
|
145
|
-
require('./transcript')
|
|
146
|
-
sessions.broadcast({ type: 'screen.updated', id: msg.id });
|
|
201
|
+
const transcript = require('./transcript');
|
|
147
202
|
const sess = sessions.getSessions().get(msg.id);
|
|
148
203
|
if (sess) {
|
|
204
|
+
if (!sess.working && sess._finalizeOnIdle) {
|
|
205
|
+
sess._finalizeOnIdle = false;
|
|
206
|
+
// if (sess.presetId === 'claude-code') {
|
|
207
|
+
// console.log(`[claude] terminal.buffer finalize session=${msg.id.slice(0,8)} lines=${msg.lines?.length || 0}`);
|
|
208
|
+
// }
|
|
209
|
+
transcript.captureAgentTurn(msg.id, sess.presetId, msg.lines);
|
|
210
|
+
}
|
|
149
211
|
let choices = require('./transcript').detectMenu(msg.lines, sess.presetId);
|
|
150
212
|
// Codex: only trust menu detection if last OTEL event was response.completed
|
|
151
213
|
if (choices && sess.presetId === 'codex') {
|
|
@@ -157,15 +219,31 @@ function onConnection(ws) {
|
|
|
157
219
|
console.log(`[codex] menu accepted session=${msg.id.slice(0,8)}`);
|
|
158
220
|
}
|
|
159
221
|
}
|
|
222
|
+
if (choices && sess.presetId === 'claude-code' && msg.menuVersion && (sess._menuConsumedVersion || 0) >= msg.menuVersion) {
|
|
223
|
+
// console.log(`[claude] menu ignored stale version=${msg.menuVersion} consumed=${sess._menuConsumedVersion || 0} session=${msg.id.slice(0,8)}`);
|
|
224
|
+
choices = null;
|
|
225
|
+
}
|
|
226
|
+
let key = choices ? JSON.stringify(choices) : '';
|
|
227
|
+
// Claude can keep rendering the same approval menu briefly after Enter.
|
|
228
|
+
// Once that exact menu was approved, ignore repeated detections of the
|
|
229
|
+
// same signature until the next real turn starts.
|
|
230
|
+
if (choices && sess.presetId === 'claude-code' && key === (sess._resolvedMenuKey || '')) {
|
|
231
|
+
// console.log(`[claude] menu ignored resolved key session=${msg.id.slice(0,8)}`);
|
|
232
|
+
choices = null;
|
|
233
|
+
key = '';
|
|
234
|
+
}
|
|
160
235
|
// Auto-approve: send Enter immediately when menu detected
|
|
161
236
|
if (choices && plugins.shouldAutoApproveMenu(msg.id)) {
|
|
162
|
-
sessions.input({ id: msg.id, data: '\r' });
|
|
237
|
+
setTimeout(() => sessions.input({ id: msg.id, data: '\r' }), 500);
|
|
163
238
|
}
|
|
164
|
-
const key = choices ? JSON.stringify(choices) : '';
|
|
165
239
|
if (key !== (sess._menuKey || '')) {
|
|
166
240
|
sess._menuKey = key;
|
|
167
241
|
sessions.broadcast({ type: 'session.menu', id: msg.id, choices: choices || [] });
|
|
168
242
|
if (choices) {
|
|
243
|
+
if (sess.presetId === 'claude-code' && msg.menuVersion) sess._menuActiveVersion = msg.menuVersion;
|
|
244
|
+
// if (sess.presetId === 'claude-code') {
|
|
245
|
+
// console.log(`[claude] menu detected session=${msg.id.slice(0,8)} choices=${choices.length} version=${msg.menuVersion || 0}`);
|
|
246
|
+
// }
|
|
169
247
|
plugins.notifyMenu(msg.id, choices);
|
|
170
248
|
if (sess.presetId === 'codex') require('./telemetry-receiver').cancelCodexMenuPoll(msg.id);
|
|
171
249
|
sessions.broadcast({ type: 'session.status', id: msg.id, working: false, source: 'menu' });
|
|
@@ -184,6 +262,7 @@ function onConnection(ws) {
|
|
|
184
262
|
|
|
185
263
|
case 'checkAvailability':
|
|
186
264
|
checkAvailability();
|
|
265
|
+
if (detectTelemetryConfig(cfg)) config.save(cfg);
|
|
187
266
|
ws.send(JSON.stringify({ type: 'presets', presets }));
|
|
188
267
|
break;
|
|
189
268
|
|
|
@@ -275,6 +354,7 @@ function onConnection(ws) {
|
|
|
275
354
|
}
|
|
276
355
|
cfg.projects = cfg.projects.filter(p => p.id !== msg.id);
|
|
277
356
|
config.save(cfg);
|
|
357
|
+
plugins.notifyConfig(cfg);
|
|
278
358
|
sessions.broadcast({ type: 'config', config: configForClient() });
|
|
279
359
|
break;
|
|
280
360
|
}
|
|
@@ -309,6 +389,18 @@ function onConnection(ws) {
|
|
|
309
389
|
sessions.broadcast({ type: 'plugins', list: plugins.getInfo() });
|
|
310
390
|
break;
|
|
311
391
|
|
|
392
|
+
case 'plugin.install': {
|
|
393
|
+
ws.send(JSON.stringify({ type: 'plugin.install.progress', pluginId: msg.pluginId }));
|
|
394
|
+
plugins.installPlugin(msg.pluginId, (err) => {
|
|
395
|
+
if (err) {
|
|
396
|
+
ws.send(JSON.stringify({ type: 'plugin.install.result', pluginId: msg.pluginId, success: false, error: err.message }));
|
|
397
|
+
} else {
|
|
398
|
+
sessions.broadcast({ type: 'plugins', list: plugins.getInfo() });
|
|
399
|
+
ws.send(JSON.stringify({ type: 'plugin.install.result', pluginId: msg.pluginId, success: true }));
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
312
404
|
case 'plugin.delete': {
|
|
313
405
|
const result = plugins.removePlugin(msg.pluginId);
|
|
314
406
|
if (result.success) {
|
|
@@ -357,8 +449,7 @@ function onConnection(ws) {
|
|
|
357
449
|
}
|
|
358
450
|
|
|
359
451
|
case 'remote.getHistory': {
|
|
360
|
-
|
|
361
|
-
ws.send(JSON.stringify({ type: 'remote.history', id: msg.id, turns: turns || [] }));
|
|
452
|
+
ws.send(JSON.stringify({ type: 'remote.history', id: msg.id, turns: transcript.getLastTurns(msg.id, 20) }));
|
|
362
453
|
break;
|
|
363
454
|
}
|
|
364
455
|
|
|
@@ -397,17 +488,23 @@ function applyTelemetryConfig(preset) {
|
|
|
397
488
|
try { settings = JSON.parse(readFileSync(configPath, 'utf8')); } catch {}
|
|
398
489
|
}
|
|
399
490
|
const hooks = settings.hooks || {};
|
|
400
|
-
const
|
|
401
|
-
const clideckHook = (
|
|
402
|
-
const hasClideck = (arr, path) => arr?.some(h => h.hooks?.some(x => x.
|
|
403
|
-
if (hasClideck(hooks.UserPromptSubmit, 'start') && hasClideck(hooks.Stop, 'stop') && hasClideck(hooks.StopFailure, 'stop') && hasClideck(hooks.PreToolUse, 'menu') && hooks.Notification?.some(h => h.matcher === 'idle_prompt' && h.hooks?.some(x => x.
|
|
491
|
+
const hookCmd = (route) => `"${process.execPath.replace(/\\/g, '/')}" "${join(__dirname, 'bin', 'claude-hook.js').replace(/\\/g, '/')}" ${port} ${route}`;
|
|
492
|
+
const clideckHook = (route) => ({ hooks: [{ type: 'command', command: hookCmd(route) }] });
|
|
493
|
+
const hasClideck = (arr, path) => arr?.some(h => h.hooks?.some(x => x.command === hookCmd(path)));
|
|
494
|
+
if (hasClideck(hooks.UserPromptSubmit, 'start') && hasClideck(hooks.Stop, 'stop') && hasClideck(hooks.StopFailure, 'stop') && hasClideck(hooks.PreToolUse, 'menu') && hooks.Notification?.some(h => h.matcher === 'idle_prompt' && h.hooks?.some(x => x.command === hookCmd('idle')))) {
|
|
404
495
|
return { success: true, message: 'Already configured' };
|
|
405
496
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
497
|
+
const stripOld = (arr) => (arr || []).filter(h => !h.hooks?.some(x => x.url?.includes('/hook/claude/') || x.command?.includes('claude-hook.js')));
|
|
498
|
+
hooks.UserPromptSubmit = stripOld(hooks.UserPromptSubmit);
|
|
499
|
+
hooks.Stop = stripOld(hooks.Stop);
|
|
500
|
+
hooks.StopFailure = stripOld(hooks.StopFailure);
|
|
501
|
+
hooks.PreToolUse = stripOld(hooks.PreToolUse);
|
|
502
|
+
hooks.Notification = stripOld(hooks.Notification);
|
|
503
|
+
if (!hasClideck(hooks.UserPromptSubmit, 'start')) hooks.UserPromptSubmit = [...(hooks.UserPromptSubmit || []), clideckHook('start')];
|
|
504
|
+
if (!hasClideck(hooks.Stop, 'stop')) hooks.Stop = [...(hooks.Stop || []), clideckHook('stop')];
|
|
505
|
+
if (!hasClideck(hooks.StopFailure, 'stop')) hooks.StopFailure = [...(hooks.StopFailure || []), clideckHook('stop')];
|
|
506
|
+
if (!hasClideck(hooks.Notification, 'idle')) hooks.Notification = [...(hooks.Notification || []), { matcher: 'idle_prompt', ...clideckHook('idle') }];
|
|
507
|
+
if (!hasClideck(hooks.PreToolUse, 'menu')) hooks.PreToolUse = [...(hooks.PreToolUse || []), clideckHook('menu')];
|
|
411
508
|
settings.hooks = hooks;
|
|
412
509
|
mkdirSync(dirname(configPath), { recursive: true });
|
|
413
510
|
writeFileSync(configPath, JSON.stringify(settings, null, 2) + '\n');
|
|
@@ -416,27 +513,22 @@ function applyTelemetryConfig(preset) {
|
|
|
416
513
|
|
|
417
514
|
if (preset.presetId === 'codex') {
|
|
418
515
|
const configPath = join(home, '.codex', 'config.toml');
|
|
516
|
+
const hooksPath = join(home, '.codex', 'hooks.json');
|
|
419
517
|
let content = '';
|
|
420
518
|
if (existsSync(configPath)) content = readFileSync(configPath, 'utf8');
|
|
421
519
|
const hasOtel = content.includes('[otel]');
|
|
422
|
-
const hasNotify =
|
|
423
|
-
|
|
424
|
-
if (!
|
|
425
|
-
|
|
426
|
-
const notifyLine = `notify = ["${process.execPath.replace(/\\/g, '/')}", "${helperPath}", "${port}"]\n`;
|
|
427
|
-
// Insert before the first [section] so it stays top-level
|
|
428
|
-
const firstSection = content.search(/^\[/m);
|
|
429
|
-
if (firstSection >= 0) {
|
|
430
|
-
content = content.slice(0, firstSection) + notifyLine + '\n' + content.slice(firstSection);
|
|
431
|
-
} else {
|
|
432
|
-
content = content + '\n' + notifyLine;
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
if (!hasOtel) {
|
|
436
|
-
content = content.trimEnd() + `\n\n[otel]\nexporter = { otlp-http = { endpoint = "http://localhost:${port}/v1/logs", protocol = "json" } }\n`;
|
|
520
|
+
const hasNotify = /^\s*notify\s*=.*notify-helper/m.test(content);
|
|
521
|
+
const hasWrongOtel = content.includes(`endpoint = "http://localhost:${port}/v1/logs"`);
|
|
522
|
+
if (hasOtel && hasNotify && !hasWrongOtel && !existsSync(hooksPath) && !/(^|\n)\[features\][\s\S]*?codex_hooks\s*=/.test(content)) {
|
|
523
|
+
return { success: true, message: 'Already configured' };
|
|
437
524
|
}
|
|
525
|
+
const notifyHelperPath = join(__dirname, 'bin', 'notify-helper.js').replace(/\\/g, '/');
|
|
526
|
+
const nextContent = upsertCodexConfig(content, process.execPath.replace(/\\/g, '/'), notifyHelperPath, port);
|
|
527
|
+
const valid = validateCodexConfigToml(nextContent);
|
|
528
|
+
if (!valid.ok) return { success: false, message: valid.error };
|
|
438
529
|
mkdirSync(dirname(configPath), { recursive: true });
|
|
439
|
-
writeFileSync(configPath,
|
|
530
|
+
writeFileSync(configPath, nextContent);
|
|
531
|
+
if (existsSync(hooksPath)) try { unlinkSync(hooksPath); } catch {}
|
|
440
532
|
return { success: true, message: 'Added otel + notify to ~/.codex/config.toml' };
|
|
441
533
|
}
|
|
442
534
|
|
|
@@ -446,20 +538,26 @@ function applyTelemetryConfig(preset) {
|
|
|
446
538
|
if (existsSync(configPath)) {
|
|
447
539
|
try { settings = JSON.parse(readFileSync(configPath, 'utf8')); } catch {}
|
|
448
540
|
}
|
|
449
|
-
|
|
541
|
+
const hooks = settings.hooks || {};
|
|
542
|
+
const helperPath = join(__dirname, 'bin', 'gemini-hook.js').replace(/\\/g, '/');
|
|
543
|
+
const nodePath = process.execPath.replace(/\\/g, '/');
|
|
544
|
+
const geminiHook = (route) => ({
|
|
545
|
+
matcher: '*',
|
|
546
|
+
hooks: [{ type: 'command', command: `"${nodePath}" "${helperPath}" ${port} ${route}`, name: `clideck-${route}`, timeout: 5000 }],
|
|
547
|
+
});
|
|
548
|
+
const has = (arr, route) => arr?.some(h => h.hooks?.some(x => x.command?.includes('gemini-hook.js') && x.command?.includes(` ${route}`)));
|
|
549
|
+
if (has(hooks.BeforeAgent, 'start') && has(hooks.AfterAgent, 'stop') && has(hooks.SessionEnd, 'stop') && has(hooks.BeforeTool, 'menu')) {
|
|
450
550
|
return { success: true, message: 'Already configured' };
|
|
451
551
|
}
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
logPrompts: true,
|
|
459
|
-
};
|
|
552
|
+
if (!has(hooks.BeforeAgent, 'start')) hooks.BeforeAgent = [...(hooks.BeforeAgent || []), geminiHook('start')];
|
|
553
|
+
if (!has(hooks.AfterAgent, 'stop')) hooks.AfterAgent = [...(hooks.AfterAgent || []), geminiHook('stop')];
|
|
554
|
+
if (!has(hooks.SessionEnd, 'stop')) hooks.SessionEnd = [...(hooks.SessionEnd || []), geminiHook('stop')];
|
|
555
|
+
if (!has(hooks.BeforeTool, 'menu')) hooks.BeforeTool = [...(hooks.BeforeTool || []), geminiHook('menu')];
|
|
556
|
+
settings.hooks = hooks;
|
|
557
|
+
if (settings.telemetry?.target === 'local' && String(settings.telemetry?.otlpEndpoint || '').includes(`localhost:${port}`)) delete settings.telemetry;
|
|
460
558
|
mkdirSync(dirname(configPath), { recursive: true });
|
|
461
559
|
writeFileSync(configPath, JSON.stringify(settings, null, 2) + '\n');
|
|
462
|
-
return { success: true, message: 'Added
|
|
560
|
+
return { success: true, message: 'Added CliDeck hooks to ~/.gemini/settings.json' };
|
|
463
561
|
}
|
|
464
562
|
|
|
465
563
|
if (preset.presetId === 'opencode') {
|
|
@@ -491,7 +589,7 @@ function removeTelemetryConfig(preset) {
|
|
|
491
589
|
for (const event of ['UserPromptSubmit', 'Stop', 'StopFailure', 'Notification', 'PreToolUse']) {
|
|
492
590
|
const arr = settings.hooks[event];
|
|
493
591
|
if (!arr) continue;
|
|
494
|
-
settings.hooks[event] = arr.filter(h => !h.hooks?.some(x => x.url?.includes('/hook/claude/')));
|
|
592
|
+
settings.hooks[event] = arr.filter(h => !h.hooks?.some(x => x.url?.includes('/hook/claude/') || x.command?.includes('claude-hook.js')));
|
|
495
593
|
if (!settings.hooks[event].length) delete settings.hooks[event];
|
|
496
594
|
}
|
|
497
595
|
if (!Object.keys(settings.hooks).length) delete settings.hooks;
|
|
@@ -505,8 +603,11 @@ function removeTelemetryConfig(preset) {
|
|
|
505
603
|
let content = readFileSync(configPath, 'utf8');
|
|
506
604
|
content = content.replace(/\n?\[otel\][^\[]*/, '');
|
|
507
605
|
content = content.replace(/\n?notify\s*=\s*\[.*?notify-helper.*?\]\s*/g, '');
|
|
606
|
+
content = content.replace(/\n?codex_hooks\s*=\s*(true|false)\s*/g, '\n');
|
|
508
607
|
writeFileSync(configPath, content.trimEnd() + '\n');
|
|
509
|
-
|
|
608
|
+
const hooksPath = join(home, '.codex', 'hooks.json');
|
|
609
|
+
if (existsSync(hooksPath)) try { unlinkSync(hooksPath); } catch {}
|
|
610
|
+
return { success: true, message: 'Removed otel + notify from ~/.codex config' };
|
|
510
611
|
}
|
|
511
612
|
|
|
512
613
|
if (preset.presetId === 'gemini-cli') {
|
|
@@ -514,9 +615,16 @@ function removeTelemetryConfig(preset) {
|
|
|
514
615
|
if (!existsSync(configPath)) return { success: true, message: 'No config file to clean' };
|
|
515
616
|
let settings = {};
|
|
516
617
|
try { settings = JSON.parse(readFileSync(configPath, 'utf8')); } catch {}
|
|
517
|
-
|
|
618
|
+
for (const event of ['BeforeAgent', 'AfterAgent', 'SessionEnd', 'BeforeTool']) {
|
|
619
|
+
const arr = settings.hooks?.[event];
|
|
620
|
+
if (!arr) continue;
|
|
621
|
+
settings.hooks[event] = arr.filter(h => !h.hooks?.some(x => x.command?.includes('gemini-hook.js')));
|
|
622
|
+
if (!settings.hooks[event].length) delete settings.hooks[event];
|
|
623
|
+
}
|
|
624
|
+
if (settings.hooks && !Object.keys(settings.hooks).length) delete settings.hooks;
|
|
625
|
+
if (settings.telemetry?.target === 'local' && String(settings.telemetry?.otlpEndpoint || '').includes('localhost:4000')) delete settings.telemetry;
|
|
518
626
|
writeFileSync(configPath, JSON.stringify(settings, null, 2) + '\n');
|
|
519
|
-
return { success: true, message: 'Removed
|
|
627
|
+
return { success: true, message: 'Removed CliDeck hooks from ~/.gemini/settings.json' };
|
|
520
628
|
}
|
|
521
629
|
|
|
522
630
|
if (preset.presetId === 'opencode') {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clideck",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.29.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": {
|
|
@@ -33,7 +33,6 @@
|
|
|
33
33
|
},
|
|
34
34
|
"homepage": "https://clideck.dev/",
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@mariozechner/pi-ai": "^0.62.0",
|
|
37
36
|
"@xterm/addon-fit": "^0.11.0",
|
|
38
37
|
"@xterm/xterm": "^6.0.0",
|
|
39
38
|
"node-pty": "^1.1.0",
|