clideck 1.27.1 → 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 +152 -56
- package/package.json +1 -1
- package/plugin-loader.js +33 -12
- package/plugins/autopilot/clideck-plugin.json +3 -3
- package/plugins/autopilot/index.js +24 -30
- package/public/index.html +2 -2
- package/public/js/app.js +21 -5
- package/public/js/creator.js +14 -2
- package/public/js/settings.js +8 -6
- package/public/js/terminals.js +123 -24
- package/public/js/toast.js +2 -17
- package/public/js/utils.js +9 -0
- package/public/tailwind.css +1 -1
- package/server.js +87 -23
- 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
|
}
|
|
@@ -369,8 +449,7 @@ function onConnection(ws) {
|
|
|
369
449
|
}
|
|
370
450
|
|
|
371
451
|
case 'remote.getHistory': {
|
|
372
|
-
|
|
373
|
-
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) }));
|
|
374
453
|
break;
|
|
375
454
|
}
|
|
376
455
|
|
|
@@ -409,17 +488,23 @@ function applyTelemetryConfig(preset) {
|
|
|
409
488
|
try { settings = JSON.parse(readFileSync(configPath, 'utf8')); } catch {}
|
|
410
489
|
}
|
|
411
490
|
const hooks = settings.hooks || {};
|
|
412
|
-
const
|
|
413
|
-
const clideckHook = (
|
|
414
|
-
const hasClideck = (arr, path) => arr?.some(h => h.hooks?.some(x => x.
|
|
415
|
-
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')))) {
|
|
416
495
|
return { success: true, message: 'Already configured' };
|
|
417
496
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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')];
|
|
423
508
|
settings.hooks = hooks;
|
|
424
509
|
mkdirSync(dirname(configPath), { recursive: true });
|
|
425
510
|
writeFileSync(configPath, JSON.stringify(settings, null, 2) + '\n');
|
|
@@ -428,27 +513,22 @@ function applyTelemetryConfig(preset) {
|
|
|
428
513
|
|
|
429
514
|
if (preset.presetId === 'codex') {
|
|
430
515
|
const configPath = join(home, '.codex', 'config.toml');
|
|
516
|
+
const hooksPath = join(home, '.codex', 'hooks.json');
|
|
431
517
|
let content = '';
|
|
432
518
|
if (existsSync(configPath)) content = readFileSync(configPath, 'utf8');
|
|
433
519
|
const hasOtel = content.includes('[otel]');
|
|
434
|
-
const hasNotify =
|
|
435
|
-
|
|
436
|
-
if (!
|
|
437
|
-
|
|
438
|
-
const notifyLine = `notify = ["${process.execPath.replace(/\\/g, '/')}", "${helperPath}", "${port}"]\n`;
|
|
439
|
-
// Insert before the first [section] so it stays top-level
|
|
440
|
-
const firstSection = content.search(/^\[/m);
|
|
441
|
-
if (firstSection >= 0) {
|
|
442
|
-
content = content.slice(0, firstSection) + notifyLine + '\n' + content.slice(firstSection);
|
|
443
|
-
} else {
|
|
444
|
-
content = content + '\n' + notifyLine;
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
if (!hasOtel) {
|
|
448
|
-
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' };
|
|
449
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 };
|
|
450
529
|
mkdirSync(dirname(configPath), { recursive: true });
|
|
451
|
-
writeFileSync(configPath,
|
|
530
|
+
writeFileSync(configPath, nextContent);
|
|
531
|
+
if (existsSync(hooksPath)) try { unlinkSync(hooksPath); } catch {}
|
|
452
532
|
return { success: true, message: 'Added otel + notify to ~/.codex/config.toml' };
|
|
453
533
|
}
|
|
454
534
|
|
|
@@ -458,20 +538,26 @@ function applyTelemetryConfig(preset) {
|
|
|
458
538
|
if (existsSync(configPath)) {
|
|
459
539
|
try { settings = JSON.parse(readFileSync(configPath, 'utf8')); } catch {}
|
|
460
540
|
}
|
|
461
|
-
|
|
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')) {
|
|
462
550
|
return { success: true, message: 'Already configured' };
|
|
463
551
|
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
logPrompts: true,
|
|
471
|
-
};
|
|
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;
|
|
472
558
|
mkdirSync(dirname(configPath), { recursive: true });
|
|
473
559
|
writeFileSync(configPath, JSON.stringify(settings, null, 2) + '\n');
|
|
474
|
-
return { success: true, message: 'Added
|
|
560
|
+
return { success: true, message: 'Added CliDeck hooks to ~/.gemini/settings.json' };
|
|
475
561
|
}
|
|
476
562
|
|
|
477
563
|
if (preset.presetId === 'opencode') {
|
|
@@ -503,7 +589,7 @@ function removeTelemetryConfig(preset) {
|
|
|
503
589
|
for (const event of ['UserPromptSubmit', 'Stop', 'StopFailure', 'Notification', 'PreToolUse']) {
|
|
504
590
|
const arr = settings.hooks[event];
|
|
505
591
|
if (!arr) continue;
|
|
506
|
-
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')));
|
|
507
593
|
if (!settings.hooks[event].length) delete settings.hooks[event];
|
|
508
594
|
}
|
|
509
595
|
if (!Object.keys(settings.hooks).length) delete settings.hooks;
|
|
@@ -517,8 +603,11 @@ function removeTelemetryConfig(preset) {
|
|
|
517
603
|
let content = readFileSync(configPath, 'utf8');
|
|
518
604
|
content = content.replace(/\n?\[otel\][^\[]*/, '');
|
|
519
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');
|
|
520
607
|
writeFileSync(configPath, content.trimEnd() + '\n');
|
|
521
|
-
|
|
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' };
|
|
522
611
|
}
|
|
523
612
|
|
|
524
613
|
if (preset.presetId === 'gemini-cli') {
|
|
@@ -526,9 +615,16 @@ function removeTelemetryConfig(preset) {
|
|
|
526
615
|
if (!existsSync(configPath)) return { success: true, message: 'No config file to clean' };
|
|
527
616
|
let settings = {};
|
|
528
617
|
try { settings = JSON.parse(readFileSync(configPath, 'utf8')); } catch {}
|
|
529
|
-
|
|
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;
|
|
530
626
|
writeFileSync(configPath, JSON.stringify(settings, null, 2) + '\n');
|
|
531
|
-
return { success: true, message: 'Removed
|
|
627
|
+
return { success: true, message: 'Removed CliDeck hooks from ~/.gemini/settings.json' };
|
|
532
628
|
}
|
|
533
629
|
|
|
534
630
|
if (preset.presetId === 'opencode') {
|