clideck 1.31.16 → 1.31.18
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 +4 -4
- package/activity.js +11 -66
- package/agent-presets.json +19 -1
- package/ansi-utils.js +7 -0
- package/bin/claude-hook.js +3 -0
- package/claude-session.js +17 -0
- package/config.js +3 -5
- package/handlers.js +92 -14
- package/http-util.js +20 -0
- package/package.json +1 -1
- package/pi-bridge.js +51 -0
- package/pi-extension/clideck-bridge.ts +34 -0
- package/preset-utils.js +13 -0
- package/public/img/pi.svg +13 -0
- package/public/index.html +4 -1
- package/public/js/app.js +36 -11
- package/public/js/hotkeys.js +11 -0
- package/public/js/settings.js +1 -0
- package/public/js/terminals.js +2 -1
- package/public/tailwind.css +1 -1
- package/server.js +28 -18
- package/session-agents.js +1 -18
- package/session-ask.js +1 -19
- package/sessions.js +26 -21
- package/telemetry-receiver.js +8 -27
- package/transcript.js +3 -2
- package/skills/autonomous-session/SKILL.md +0 -211
- package/skills/research-experiment/SKILL.md +0 -411
- package/skills/research-experiment/SKILL.md.bak +0 -224
- package/skills/research-experiment/scripts/init-research-layout.mjs +0 -184
package/README.md
CHANGED
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
<img src="assets/clideck-themes.jpg" width="720" alt="clideck dashboard">
|
|
19
19
|
</p>
|
|
20
20
|
|
|
21
|
-
clideck is a local app for running multiple AI coding agents without juggling terminals. Claude Code, Codex, Gemini CLI, and
|
|
21
|
+
clideck is a local app for running multiple AI coding agents without juggling terminals. Claude Code, Codex, Gemini CLI, OpenCode, and Pi all live in one browser window with a chat-style sidebar, live status, message previews, session resume, and projects to keep things organized. an autopilot routes work between agents automatically, and an E2E encrypted mobile relay gives full control over all agents from a phone.
|
|
22
22
|
|
|
23
|
-
the main problem with using multiple agents is not starting them. it is managing them. terminals pile up, finished work gets missed, good sessions disappear after a restart. clideck does not sit in the middle rewriting prompts or output - it only watches lightweight status signals
|
|
23
|
+
the main problem with using multiple agents is not starting them. it is managing them. terminals pile up, finished work gets missed, good sessions disappear after a restart. clideck does not sit in the middle rewriting prompts or output - it only watches lightweight status signals from each agent so it can tell which agent is working, which is idle, and which is waiting. everything runs locally, no data leaves your machine.
|
|
24
24
|
|
|
25
25
|
## Why this exists
|
|
26
26
|
|
|
@@ -47,7 +47,7 @@ clideck --port 4001
|
|
|
47
47
|
|
|
48
48
|
## What makes it useful
|
|
49
49
|
|
|
50
|
-
**Live status** - see which agent is working and which is waiting. Status detection for Claude Code, Codex, Gemini CLI, and
|
|
50
|
+
**Live status** - see which agent is working and which is waiting. Status detection for Claude Code, Codex, Gemini CLI, OpenCode, and Pi.
|
|
51
51
|
|
|
52
52
|
**Session resume** - close the lid, reopen tomorrow, pick up where things left off. each agent's session ID is captured automatically.
|
|
53
53
|
|
|
@@ -79,7 +79,7 @@ If project or session names contain spaces, quote the whole target. The target i
|
|
|
79
79
|
|
|
80
80
|
## Supported agents
|
|
81
81
|
|
|
82
|
-
Claude Code, Codex, Gemini CLI, OpenCode, Shell, and any other terminal tool.
|
|
82
|
+
Claude Code, Codex, Gemini CLI, OpenCode, Pi, Shell, and any other terminal tool.
|
|
83
83
|
|
|
84
84
|
## Also
|
|
85
85
|
|
package/activity.js
CHANGED
|
@@ -1,77 +1,22 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
let interval = null;
|
|
5
|
-
const net = {}; // id → byte counters
|
|
6
|
-
const stream = {}; // id → burst timing
|
|
1
|
+
// Lightweight per-session I/O counters. The old per-second stats broadcast was
|
|
2
|
+
// removed after frontend status tracking moved away from I/O heuristics.
|
|
3
|
+
const net = {}; // id -> byte counters
|
|
7
4
|
|
|
8
5
|
function ensure(id) {
|
|
9
|
-
if (!net[id]) net[id] = { in: 0, out: 0
|
|
10
|
-
|
|
6
|
+
if (!net[id]) net[id] = { in: 0, out: 0 };
|
|
7
|
+
return net[id];
|
|
11
8
|
}
|
|
12
9
|
|
|
13
10
|
function trackIn(id, bytes) {
|
|
14
|
-
ensure(id);
|
|
15
|
-
|
|
16
|
-
stream[id].lastInAt = Date.now();
|
|
11
|
+
const n = ensure(id);
|
|
12
|
+
n.in += bytes;
|
|
17
13
|
}
|
|
18
14
|
|
|
19
|
-
function lastInputAt(id) { return stream[id]?.lastInAt || 0; }
|
|
20
|
-
|
|
21
15
|
function trackOut(id, data) {
|
|
22
|
-
ensure(id);
|
|
23
|
-
|
|
24
|
-
const s = stream[id];
|
|
25
|
-
net[id].out += data.length;
|
|
26
|
-
if (now - s.lastOutAt > 2000) s.burstStart = now;
|
|
27
|
-
s.lastOutAt = now;
|
|
28
|
-
s.lastChunk = data;
|
|
16
|
+
const n = ensure(id);
|
|
17
|
+
n.out += data.length;
|
|
29
18
|
}
|
|
30
19
|
|
|
31
|
-
function
|
|
32
|
-
if (interval) return;
|
|
33
|
-
interval = setInterval(() => {
|
|
34
|
-
if (!sessions.size) return;
|
|
35
|
-
const now = Date.now();
|
|
36
|
-
const stats = {};
|
|
37
|
-
for (const [id] of sessions) {
|
|
38
|
-
ensure(id);
|
|
39
|
-
const n = net[id];
|
|
40
|
-
const s = stream[id];
|
|
41
|
-
const rawRateOut = n.out - n.prevOut;
|
|
42
|
-
const rawRateIn = n.in - n.prevIn;
|
|
43
|
-
n.prevOut = n.out;
|
|
44
|
-
n.prevIn = n.in;
|
|
45
|
-
const silence = s.lastOutAt ? now - s.lastOutAt : 0;
|
|
46
|
-
const burstMs = s.burstStart && silence < 2000 ? now - s.burstStart : 0;
|
|
47
|
-
stats[id] = { rawRateOut, rawRateIn, burstMs };
|
|
48
|
-
}
|
|
49
|
-
if (Object.keys(stats).length) broadcast({ type: 'stats', stats });
|
|
50
|
-
// Debug: PTY active state + last output chars per session (1s tick)
|
|
51
|
-
const active = [];
|
|
52
|
-
for (const [id] of sessions) {
|
|
53
|
-
const s = stream[id]; if (!s?.lastOutAt) continue;
|
|
54
|
-
const state = now - s.lastOutAt < 2000 ? 'ACTIVE' : 'SILENT';
|
|
55
|
-
const last = (s.lastChunk || '').replace(/\x1b\[[0-9;?]*[ -/]*[@-~]|\x1b\].*?(?:\x07|\x1b\\)|\x1b./g, '').replace(/[\r\n]+/g, ' ').trim().slice(-80);
|
|
56
|
-
active.push(`${id.slice(0,8)}=${state} [${last}]`);
|
|
57
|
-
}
|
|
58
|
-
// if (active.length) console.log(`[pty] ${active.join(' | ')}`);
|
|
59
|
-
}, 1000);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function stop() {
|
|
63
|
-
clearInterval(interval);
|
|
64
|
-
interval = null;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function isActive(id) {
|
|
68
|
-
const s = stream[id];
|
|
69
|
-
return s ? (Date.now() - s.lastOutAt < 2000) : false;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function lastOutputAt(id) { return stream[id]?.lastOutAt || 0; }
|
|
73
|
-
function lastChunk(id) { return stream[id]?.lastChunk || ''; }
|
|
74
|
-
|
|
75
|
-
function clear(id) { delete net[id]; delete stream[id]; }
|
|
20
|
+
function clear(id) { delete net[id]; }
|
|
76
21
|
|
|
77
|
-
module.exports = {
|
|
22
|
+
module.exports = { trackIn, trackOut, clear };
|
package/agent-presets.json
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"sessionIdPattern": "Session ID:\\s+([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})",
|
|
13
13
|
"outputMarker": "⏺",
|
|
14
14
|
"telemetryConfigPath": "~/.claude/settings.json",
|
|
15
|
-
"telemetrySetup": "Required for working/idle status, Autopilot, notifications, and mobile remote.\n\nCliDeck will add
|
|
15
|
+
"telemetrySetup": "Required for working/idle status, Autopilot, notifications, and mobile remote.\n\nCliDeck will add lifecycle hooks to ~/.claude/settings.json. Claude will ask for one-time approval on next launch.",
|
|
16
16
|
"telemetryAutoSetup": {
|
|
17
17
|
"label": "Patch Claude"
|
|
18
18
|
},
|
|
@@ -85,6 +85,24 @@
|
|
|
85
85
|
"label": "Install plugin"
|
|
86
86
|
}
|
|
87
87
|
},
|
|
88
|
+
{
|
|
89
|
+
"presetId": "pi",
|
|
90
|
+
"name": "Pi",
|
|
91
|
+
"icon": "/img/pi.svg",
|
|
92
|
+
"command": "pi",
|
|
93
|
+
"minVersion": "0.79.3",
|
|
94
|
+
"installCmd": "npm install -g --ignore-scripts @earendil-works/pi-coding-agent",
|
|
95
|
+
"isAgent": true,
|
|
96
|
+
"canResume": true,
|
|
97
|
+
"resumeCommand": "pi --session {{sessionId}}",
|
|
98
|
+
"sessionIdPattern": null,
|
|
99
|
+
"outputMarker": "•",
|
|
100
|
+
"telemetryConfigPath": "~/.pi/agent/extensions/clideck-bridge.ts",
|
|
101
|
+
"telemetrySetup": "Required for working/idle status, resume, Autopilot, notifications, and mobile remote.\n\nCliDeck will install a small Pi extension to ~/.pi/agent/extensions/clideck-bridge.ts.",
|
|
102
|
+
"telemetryAutoSetup": {
|
|
103
|
+
"label": "Install extension"
|
|
104
|
+
}
|
|
105
|
+
},
|
|
88
106
|
{
|
|
89
107
|
"presetId": "clideck-agent",
|
|
90
108
|
"name": "Clideck Agent",
|
package/ansi-utils.js
ADDED
package/bin/claude-hook.js
CHANGED
|
@@ -15,6 +15,9 @@ process.stdin.on('end', () => {
|
|
|
15
15
|
const body = JSON.stringify({
|
|
16
16
|
clideck_id: process.env.CLIDECK_SESSION_ID || '',
|
|
17
17
|
session_id: hook.session_id || '',
|
|
18
|
+
hook_event_name: hook.hook_event_name || '',
|
|
19
|
+
source: hook.source || '',
|
|
20
|
+
reason: hook.reason || '',
|
|
18
21
|
payload: stdin.trim() || undefined,
|
|
19
22
|
});
|
|
20
23
|
const req = http.request({
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const CLAUDE_SESSION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
2
|
+
|
|
3
|
+
function updateClaudeSessionToken(sess, token, clideckId, options = {}) {
|
|
4
|
+
const next = String(token || '').trim();
|
|
5
|
+
if (!sess || sess.presetId !== 'claude-code' || !CLAUDE_SESSION_ID_RE.test(next)) return false;
|
|
6
|
+
if (sess.sessionToken === next) return false;
|
|
7
|
+
const prev = sess.sessionToken;
|
|
8
|
+
sess.sessionToken = next;
|
|
9
|
+
|
|
10
|
+
const label = options.label || 'Claude';
|
|
11
|
+
const source = options.source ? ` via ${options.source}` : '';
|
|
12
|
+
const previous = prev ? `${prev.slice(0, 12)}... -> ` : '';
|
|
13
|
+
console.log(`${label}: updated Claude session ID for ${clideckId.slice(0, 8)}${source}: ${previous}${next.slice(0, 12)}...`);
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
module.exports = { CLAUDE_SESSION_ID_RE, updateClaudeSessionToken };
|
package/config.js
CHANGED
|
@@ -3,7 +3,8 @@ const { join } = require('path');
|
|
|
3
3
|
const crypto = require('crypto');
|
|
4
4
|
const os = require('os');
|
|
5
5
|
const { DATA_DIR } = require('./paths');
|
|
6
|
-
const { defaultShell
|
|
6
|
+
const { defaultShell } = require('./utils');
|
|
7
|
+
const { presetForCommand } = require('./preset-utils');
|
|
7
8
|
|
|
8
9
|
const CONFIG_PATH = join(DATA_DIR, 'config.json');
|
|
9
10
|
|
|
@@ -63,10 +64,7 @@ function isPresetEnabled(preset) {
|
|
|
63
64
|
}
|
|
64
65
|
const EXPOSED_PRESETS = PRESETS.filter(isPresetEnabled);
|
|
65
66
|
|
|
66
|
-
function matchPreset(cmd) {
|
|
67
|
-
const bin = binName(cmd.command);
|
|
68
|
-
return PRESETS.find(p => binName(p.command) === bin);
|
|
69
|
-
}
|
|
67
|
+
function matchPreset(cmd) { return presetForCommand(cmd, PRESETS, { usePresetId: false }); }
|
|
70
68
|
|
|
71
69
|
function firstCommandToken(command) {
|
|
72
70
|
const raw = String(command || '').trim();
|
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 { presetForCommand: findPresetForCommand } = require('./preset-utils');
|
|
10
11
|
const { PORT } = require('./runtime');
|
|
11
12
|
for (const p of presets) if (p.presetId === 'shell') p.command = defaultShell;
|
|
12
13
|
function isPresetEnabled(preset) {
|
|
@@ -20,7 +21,11 @@ function clientPresets() {
|
|
|
20
21
|
function filterClientCommands(commands) {
|
|
21
22
|
const allowedPresetIds = new Set(clientPresets().map(p => p.presetId));
|
|
22
23
|
const knownPresetIds = new Set(presets.map(p => p.presetId));
|
|
23
|
-
return (commands || []).filter(cmd =>
|
|
24
|
+
return (commands || []).filter(cmd => {
|
|
25
|
+
if (cmd.presetId && !allowedPresetIds.has(cmd.presetId) && knownPresetIds.has(cmd.presetId)) return false;
|
|
26
|
+
const preset = cmd.presetId ? presets.find(p => p.presetId === cmd.presetId) : null;
|
|
27
|
+
return !(preset?.available === false && String(cmd.command || '').trim() === String(preset.command || '').trim());
|
|
28
|
+
});
|
|
24
29
|
}
|
|
25
30
|
const transcript = require('./transcript');
|
|
26
31
|
const plugins = require('./plugin-loader');
|
|
@@ -72,15 +77,10 @@ function getInstalledVersion(bin) {
|
|
|
72
77
|
}
|
|
73
78
|
|
|
74
79
|
function presetForCommand(cmd) {
|
|
75
|
-
|
|
76
|
-
const preset = presets.find(p => p.presetId === cmd.presetId);
|
|
77
|
-
if (preset) return preset;
|
|
78
|
-
}
|
|
79
|
-
const bin = binName(cmd?.command);
|
|
80
|
-
return presets.find(p => binName(p.command) === bin);
|
|
80
|
+
return findPresetForCommand(cmd, presets);
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
function
|
|
83
|
+
function rawCommandEnv(cmd) {
|
|
84
84
|
return cmd?.env && typeof cmd.env === 'object' && !Array.isArray(cmd.env) ? cmd.env : {};
|
|
85
85
|
}
|
|
86
86
|
|
|
@@ -93,10 +93,11 @@ function expandHomePath(value) {
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
function configRootFor(preset, cmd) {
|
|
96
|
-
const env =
|
|
96
|
+
const env = rawCommandEnv(cmd);
|
|
97
97
|
if (preset?.presetId === 'claude-code') return expandHomePath(env.CLAUDE_CONFIG_DIR) || join(os.homedir(), '.claude');
|
|
98
98
|
if (preset?.presetId === 'codex') return expandHomePath(env.CODEX_HOME) || join(os.homedir(), '.codex');
|
|
99
99
|
if (preset?.presetId === 'gemini-cli') return join(expandHomePath(env.GEMINI_CLI_HOME) || os.homedir(), '.gemini');
|
|
100
|
+
if (preset?.presetId === 'pi') return expandHomePath(env.PI_CODING_AGENT_DIR) || join(os.homedir(), '.pi', 'agent');
|
|
100
101
|
return os.homedir();
|
|
101
102
|
}
|
|
102
103
|
|
|
@@ -177,6 +178,14 @@ function hasExistingHook(arr, hookFile, route) {
|
|
|
177
178
|
}));
|
|
178
179
|
}
|
|
179
180
|
|
|
181
|
+
function hasAnyExistingHook(hooks, hookFile) {
|
|
182
|
+
return Object.values(hooks || {}).some(arr => arr?.some(h => h.hooks?.some(x => {
|
|
183
|
+
if (!x.command?.includes(hookFile)) return false;
|
|
184
|
+
const hookPath = extractQuotedPath(x.command, hookFile);
|
|
185
|
+
return !!hookPath && existsSync(hookPath);
|
|
186
|
+
})));
|
|
187
|
+
}
|
|
188
|
+
|
|
180
189
|
function codexHooksFeatureEnabled(content) {
|
|
181
190
|
let inFeatures = false;
|
|
182
191
|
for (const line of String(content || '').split(/\r?\n/)) {
|
|
@@ -215,6 +224,23 @@ function opencodeBridgeLooksHealthy() {
|
|
|
215
224
|
}
|
|
216
225
|
}
|
|
217
226
|
|
|
227
|
+
function piBridgePath(cmd) {
|
|
228
|
+
return join(configRootFor({ presetId: 'pi' }, cmd), 'extensions', 'clideck-bridge.ts');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function piBridgeLooksHealthy(cmd) {
|
|
232
|
+
const bridgePath = piBridgePath(cmd);
|
|
233
|
+
if (!existsSync(bridgePath)) return false;
|
|
234
|
+
try {
|
|
235
|
+
const content = readFileSync(bridgePath, 'utf8');
|
|
236
|
+
return content.includes('/hook/pi')
|
|
237
|
+
&& content.includes('CLIDECK_SESSION_ID')
|
|
238
|
+
&& content.includes('sessionManager.getSessionId');
|
|
239
|
+
} catch {
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
218
244
|
function detectTelemetryConfig(c) {
|
|
219
245
|
const port = String(PORT);
|
|
220
246
|
let changed = false;
|
|
@@ -227,15 +253,23 @@ function detectTelemetryConfig(c) {
|
|
|
227
253
|
if (!preset) continue;
|
|
228
254
|
let detected = false;
|
|
229
255
|
let reason = '';
|
|
256
|
+
let repairAllowed = cmd.telemetrySetupConsent === true;
|
|
230
257
|
if (preset.presetId === 'claude-code') {
|
|
231
258
|
try {
|
|
232
259
|
const s = JSON.parse(readFileSync(join(configRootFor(preset, cmd), 'settings.json'), 'utf8'));
|
|
233
260
|
const hooks = s.hooks || {};
|
|
261
|
+
repairAllowed = repairAllowed || hasAnyExistingHook(hooks, 'claude-hook.js');
|
|
234
262
|
detected = hasExistingHook(hooks.UserPromptSubmit, 'claude-hook.js', 'start')
|
|
235
263
|
&& hasExistingHook(hooks.Stop, 'claude-hook.js', 'stop')
|
|
236
264
|
&& hasExistingHook(hooks.StopFailure, 'claude-hook.js', 'stop')
|
|
265
|
+
&& hasExistingHook(hooks.SessionStart, 'claude-hook.js', 'session-start')
|
|
266
|
+
&& hasExistingHook(hooks.SessionEnd, 'claude-hook.js', 'session-end')
|
|
237
267
|
&& hasExistingHook(hooks.PreToolUse, 'claude-hook.js', 'menu')
|
|
238
268
|
&& hooks.Notification?.some(h => h.matcher === 'idle_prompt' && hasExistingHook([h], 'claude-hook.js', 'idle'));
|
|
269
|
+
if (detected && cmd.telemetrySetupConsent !== true) {
|
|
270
|
+
cmd.telemetrySetupConsent = true;
|
|
271
|
+
changed = true;
|
|
272
|
+
}
|
|
239
273
|
if (!detected) reason = 'Needs re-patch';
|
|
240
274
|
} catch {}
|
|
241
275
|
} else if (preset.presetId === 'codex') {
|
|
@@ -258,11 +292,14 @@ function detectTelemetryConfig(c) {
|
|
|
258
292
|
} else if (preset.presetId === 'opencode') {
|
|
259
293
|
detected = opencodeBridgeLooksHealthy();
|
|
260
294
|
if (!detected) reason = 'Needs re-patch';
|
|
295
|
+
} else if (preset.presetId === 'pi') {
|
|
296
|
+
detected = piBridgeLooksHealthy(cmd);
|
|
297
|
+
if (!detected) reason = 'Needs re-patch';
|
|
261
298
|
} else { continue; }
|
|
262
299
|
if (preset.available && preset.minVersion && !preset.versionOk) {
|
|
263
300
|
detected = false;
|
|
264
301
|
reason = `Update required (${preset.minVersion}+)`;
|
|
265
|
-
} else if (!detected && cmd.telemetryEnabled && preset.telemetryAutoSetup && preset.available && preset.versionOk && !attemptedRepairs.has(cmd.id || preset.presetId)) {
|
|
302
|
+
} else if (!detected && cmd.telemetryEnabled && repairAllowed && preset.telemetryAutoSetup && preset.available && preset.versionOk && !attemptedRepairs.has(cmd.id || preset.presetId)) {
|
|
266
303
|
attemptedRepairs.add(cmd.id || preset.presetId);
|
|
267
304
|
const repaired = applyTelemetryConfig(preset, cmd);
|
|
268
305
|
if (repaired.success) {
|
|
@@ -368,6 +405,7 @@ function onConnection(ws) {
|
|
|
368
405
|
if (choices) transcript.commitAgentCandidate(msg.id, sess.presetId);
|
|
369
406
|
if (key !== (sess._menuKey || '')) {
|
|
370
407
|
sess._menuKey = key;
|
|
408
|
+
sess._menuStartsWork = !(sess.presetId === 'claude-code' && !msg.menuVersion);
|
|
371
409
|
sessions.broadcast({ type: 'session.menu', id: msg.id, choices: choices || [] });
|
|
372
410
|
if (choices) {
|
|
373
411
|
if (sess.presetId === 'claude-code' && msg.menuVersion) sess._menuActiveVersion = msg.menuVersion;
|
|
@@ -391,6 +429,7 @@ function onConnection(ws) {
|
|
|
391
429
|
checkAvailability();
|
|
392
430
|
if (detectTelemetryConfig(cfg)) config.save(cfg);
|
|
393
431
|
ws.send(JSON.stringify({ type: 'presets', presets: clientPresets() }));
|
|
432
|
+
ws.send(JSON.stringify({ type: 'config', config: configForClient() }));
|
|
394
433
|
break;
|
|
395
434
|
|
|
396
435
|
case 'config.update':
|
|
@@ -399,6 +438,7 @@ function onConnection(ws) {
|
|
|
399
438
|
cfg = { ...cfg, ...msg.config };
|
|
400
439
|
detectTelemetryConfig(cfg);
|
|
401
440
|
config.save(cfg);
|
|
441
|
+
plugins.notifyConfig(cfg);
|
|
402
442
|
sessions.broadcast({ type: 'config', config: configForClient() });
|
|
403
443
|
break;
|
|
404
444
|
|
|
@@ -412,16 +452,28 @@ function onConnection(ws) {
|
|
|
412
452
|
const targetCmd = msg.commandId ? cfg.commands.find(c => c.id === msg.commandId) : null;
|
|
413
453
|
const preset = targetCmd ? presetForCommand(targetCmd) : presets.find(p => p.presetId === msg.presetId);
|
|
414
454
|
if (!preset?.telemetryAutoSetup) break;
|
|
455
|
+
if (preset.available === false) {
|
|
456
|
+
ws.send(JSON.stringify({
|
|
457
|
+
type: 'telemetry.autosetup.result',
|
|
458
|
+
presetId: preset.presetId,
|
|
459
|
+
commandId: msg.commandId || null,
|
|
460
|
+
success: false,
|
|
461
|
+
output: `${preset.name} is not installed`,
|
|
462
|
+
}));
|
|
463
|
+
break;
|
|
464
|
+
}
|
|
415
465
|
const result = applyTelemetryConfig(preset, targetCmd);
|
|
416
466
|
for (const cmd of cfg.commands) {
|
|
417
467
|
if (targetCmd ? cmd.id === targetCmd.id : presetForCommand(cmd)?.presetId === preset.presetId) {
|
|
418
468
|
cmd.telemetryEnabled = result.success;
|
|
419
469
|
cmd.telemetryStatus = result.success ? { ok: true } : { ok: false, error: result.message };
|
|
470
|
+
if (result.success) cmd.telemetrySetupConsent = true;
|
|
420
471
|
// Enable the agent when setup succeeds, disable if it fails
|
|
421
472
|
if (result.success) cmd.enabled = true;
|
|
422
473
|
}
|
|
423
474
|
}
|
|
424
475
|
config.save(cfg);
|
|
476
|
+
plugins.notifyConfig(cfg);
|
|
425
477
|
sessions.broadcast({ type: 'config', config: configForClient() });
|
|
426
478
|
ws.send(JSON.stringify({
|
|
427
479
|
type: 'telemetry.autosetup.result',
|
|
@@ -448,12 +500,14 @@ function onConnection(ws) {
|
|
|
448
500
|
for (const cmd of cfg.commands) {
|
|
449
501
|
if (targetCmd ? cmd.id === targetCmd.id : presetForCommand(cmd)?.presetId === preset.presetId) {
|
|
450
502
|
cmd.telemetryEnabled = enable && result.success;
|
|
503
|
+
cmd.telemetrySetupConsent = enable && result.success;
|
|
451
504
|
cmd.telemetryStatus = enable
|
|
452
505
|
? (result.success ? { ok: true } : { ok: false, error: result.message })
|
|
453
506
|
: null;
|
|
454
507
|
}
|
|
455
508
|
}
|
|
456
509
|
config.save(cfg);
|
|
510
|
+
plugins.notifyConfig(cfg);
|
|
457
511
|
sessions.broadcast({ type: 'config', config: configForClient() });
|
|
458
512
|
break;
|
|
459
513
|
}
|
|
@@ -659,6 +713,7 @@ function onConnection(ws) {
|
|
|
659
713
|
|
|
660
714
|
case 'remote.install': {
|
|
661
715
|
const update = !!msg.update;
|
|
716
|
+
const restartAfterUpdate = !!msg.restart;
|
|
662
717
|
const proc = require('child_process').spawn('npm', ['install', '-g', 'clideck-remote'], {
|
|
663
718
|
shell: true, stdio: ['ignore', 'pipe', 'pipe'],
|
|
664
719
|
});
|
|
@@ -666,8 +721,8 @@ function onConnection(ws) {
|
|
|
666
721
|
proc.stderr.on('data', d => ws.send(JSON.stringify({ type: 'remote.install.progress', text: d.toString() })));
|
|
667
722
|
proc.on('close', code => {
|
|
668
723
|
remoteUpdateCache = null;
|
|
669
|
-
if (code !== 0 || !update) {
|
|
670
|
-
ws.send(JSON.stringify({ type: 'remote.install.done', success: code === 0, update }));
|
|
724
|
+
if (code !== 0 || !update || !restartAfterUpdate) {
|
|
725
|
+
ws.send(JSON.stringify({ type: 'remote.install.done', success: code === 0, update, restarted: false }));
|
|
671
726
|
return;
|
|
672
727
|
}
|
|
673
728
|
require('child_process').execFile('clideck-remote', ['restart', '--json'], { timeout: 10000, shell: process.platform === 'win32', env: remoteCliEnv() }, (err, stdout) => {
|
|
@@ -707,18 +762,28 @@ function applyTelemetryConfig(preset, cmd = null) {
|
|
|
707
762
|
const hookCmd = (route) => `"${process.execPath.replace(/\\/g, '/')}" "${join(__dirname, 'bin', 'claude-hook.js').replace(/\\/g, '/')}" ${port} ${route}`;
|
|
708
763
|
const clideckHook = (route) => ({ hooks: [{ type: 'command', command: hookCmd(route) }] });
|
|
709
764
|
const hasClideck = (arr, path) => arr?.some(h => h.hooks?.some(x => x.command === hookCmd(path)));
|
|
710
|
-
if (hasClideck(hooks.UserPromptSubmit, 'start')
|
|
765
|
+
if (hasClideck(hooks.UserPromptSubmit, 'start')
|
|
766
|
+
&& hasClideck(hooks.Stop, 'stop')
|
|
767
|
+
&& hasClideck(hooks.StopFailure, 'stop')
|
|
768
|
+
&& hasClideck(hooks.SessionStart, 'session-start')
|
|
769
|
+
&& hasClideck(hooks.SessionEnd, 'session-end')
|
|
770
|
+
&& hasClideck(hooks.PreToolUse, 'menu')
|
|
771
|
+
&& hooks.Notification?.some(h => h.matcher === 'idle_prompt' && h.hooks?.some(x => x.command === hookCmd('idle')))) {
|
|
711
772
|
return { success: true, message: 'Already configured' };
|
|
712
773
|
}
|
|
713
774
|
const stripOld = (arr) => (arr || []).filter(h => !h.hooks?.some(x => x.url?.includes('/hook/claude/') || x.command?.includes('claude-hook.js')));
|
|
714
775
|
hooks.UserPromptSubmit = stripOld(hooks.UserPromptSubmit);
|
|
715
776
|
hooks.Stop = stripOld(hooks.Stop);
|
|
716
777
|
hooks.StopFailure = stripOld(hooks.StopFailure);
|
|
778
|
+
hooks.SessionStart = stripOld(hooks.SessionStart);
|
|
779
|
+
hooks.SessionEnd = stripOld(hooks.SessionEnd);
|
|
717
780
|
hooks.PreToolUse = stripOld(hooks.PreToolUse);
|
|
718
781
|
hooks.Notification = stripOld(hooks.Notification);
|
|
719
782
|
if (!hasClideck(hooks.UserPromptSubmit, 'start')) hooks.UserPromptSubmit = [...(hooks.UserPromptSubmit || []), clideckHook('start')];
|
|
720
783
|
if (!hasClideck(hooks.Stop, 'stop')) hooks.Stop = [...(hooks.Stop || []), clideckHook('stop')];
|
|
721
784
|
if (!hasClideck(hooks.StopFailure, 'stop')) hooks.StopFailure = [...(hooks.StopFailure || []), clideckHook('stop')];
|
|
785
|
+
if (!hasClideck(hooks.SessionStart, 'session-start')) hooks.SessionStart = [...(hooks.SessionStart || []), clideckHook('session-start')];
|
|
786
|
+
if (!hasClideck(hooks.SessionEnd, 'session-end')) hooks.SessionEnd = [...(hooks.SessionEnd || []), clideckHook('session-end')];
|
|
722
787
|
if (!hasClideck(hooks.Notification, 'idle')) hooks.Notification = [...(hooks.Notification || []), { matcher: 'idle_prompt', ...clideckHook('idle') }];
|
|
723
788
|
if (!hasClideck(hooks.PreToolUse, 'menu')) hooks.PreToolUse = [...(hooks.PreToolUse || []), clideckHook('menu')];
|
|
724
789
|
settings.hooks = hooks;
|
|
@@ -795,6 +860,14 @@ function applyTelemetryConfig(preset, cmd = null) {
|
|
|
795
860
|
return { success: true, message: `Installed bridge plugin to ${opencodePluginDir}` };
|
|
796
861
|
}
|
|
797
862
|
|
|
863
|
+
if (preset.presetId === 'pi') {
|
|
864
|
+
const src = join(__dirname, 'pi-extension', 'clideck-bridge.ts');
|
|
865
|
+
const dest = piBridgePath(cmd);
|
|
866
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
867
|
+
copyFileSync(src, dest);
|
|
868
|
+
return { success: true, message: `Installed Pi extension to ${dest}` };
|
|
869
|
+
}
|
|
870
|
+
|
|
798
871
|
return { success: false, message: `No auto-setup for ${preset.presetId}` };
|
|
799
872
|
} catch (err) {
|
|
800
873
|
return { success: false, message: err.message };
|
|
@@ -809,7 +882,7 @@ function removeTelemetryConfig(preset, cmd = null) {
|
|
|
809
882
|
let settings = {};
|
|
810
883
|
try { settings = JSON.parse(readFileSync(configPath, 'utf8')); } catch {}
|
|
811
884
|
if (!settings.hooks) return { success: true, message: 'No hooks to remove' };
|
|
812
|
-
for (const event of ['UserPromptSubmit', 'Stop', 'StopFailure', 'Notification', 'PreToolUse']) {
|
|
885
|
+
for (const event of ['UserPromptSubmit', 'Stop', 'StopFailure', 'SessionStart', 'SessionEnd', 'Notification', 'PreToolUse']) {
|
|
813
886
|
const arr = settings.hooks[event];
|
|
814
887
|
if (!arr) continue;
|
|
815
888
|
settings.hooks[event] = arr.filter(h => !h.hooks?.some(x => x.url?.includes('/hook/claude/') || x.command?.includes('claude-hook.js')));
|
|
@@ -856,6 +929,11 @@ function removeTelemetryConfig(preset, cmd = null) {
|
|
|
856
929
|
return { success: true, message: 'Removed bridge plugin' };
|
|
857
930
|
}
|
|
858
931
|
|
|
932
|
+
if (preset.presetId === 'pi') {
|
|
933
|
+
try { unlinkSync(piBridgePath(cmd)); } catch {}
|
|
934
|
+
return { success: true, message: 'Removed Pi extension' };
|
|
935
|
+
}
|
|
936
|
+
|
|
859
937
|
return { success: false, message: `No removal logic for ${preset.presetId}` };
|
|
860
938
|
} catch (err) {
|
|
861
939
|
return { success: false, message: err.message };
|
package/http-util.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
function sendJson(res, status, payload) {
|
|
2
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
3
|
+
res.end(JSON.stringify(payload));
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function isLoopback(req) {
|
|
7
|
+
const addr = req.socket?.remoteAddress || '';
|
|
8
|
+
return addr === '::1' || addr === '127.0.0.1' || addr.startsWith('127.') || addr.startsWith('::ffff:127.');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function sameProject(a, b) {
|
|
12
|
+
return (a.projectId || null) === (b.projectId || null);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function projectName(projects, projectId) {
|
|
16
|
+
if (!projectId) return 'No project';
|
|
17
|
+
return projects.find(p => p.id === projectId)?.name || projectId;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = { sendJson, isLoopback, sameProject, projectName };
|
package/package.json
CHANGED
package/pi-bridge.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Pi bridge — receives lifecycle events from the CliDeck Pi extension.
|
|
2
|
+
|
|
3
|
+
let broadcastFn = null;
|
|
4
|
+
let sessionsFn = null;
|
|
5
|
+
|
|
6
|
+
function init(broadcast, getSessions) {
|
|
7
|
+
broadcastFn = broadcast;
|
|
8
|
+
sessionsFn = getSessions;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function findByToken(token) {
|
|
12
|
+
if (!token) return null;
|
|
13
|
+
for (const [id, session] of sessionsFn?.() || []) {
|
|
14
|
+
if (session.sessionToken === token) return id;
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function capture(id) {
|
|
20
|
+
setTimeout(() => broadcastFn?.({ type: 'terminal.capture', id }), 500);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function handleEvent(payload) {
|
|
24
|
+
if (!payload || !payload.event) return;
|
|
25
|
+
const sessions = sessionsFn?.();
|
|
26
|
+
if (!sessions) return;
|
|
27
|
+
|
|
28
|
+
const clideckId = payload.clideck_id && sessions.has(payload.clideck_id)
|
|
29
|
+
? payload.clideck_id
|
|
30
|
+
: findByToken(payload.session_id);
|
|
31
|
+
if (!clideckId) return;
|
|
32
|
+
|
|
33
|
+
const session = sessions.get(clideckId);
|
|
34
|
+
if (session && payload.session_id && payload.event !== 'session_shutdown') {
|
|
35
|
+
session.sessionToken = payload.session_id;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (payload.event === 'agent_start') {
|
|
39
|
+
broadcastFn?.({ type: 'session.status', id: clideckId, working: true, source: 'hook' });
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (payload.event === 'agent_end' || payload.event === 'session_start' || payload.event === 'session_shutdown') {
|
|
44
|
+
broadcastFn?.({ type: 'session.status', id: clideckId, working: false, source: 'hook' });
|
|
45
|
+
capture(clideckId);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function clear() {}
|
|
50
|
+
|
|
51
|
+
module.exports = { init, handleEvent, clear };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// CliDeck bridge extension for Pi.
|
|
2
|
+
// Pi auto-loads this file from ~/.pi/agent/extensions/.
|
|
3
|
+
|
|
4
|
+
const env = globalThis.process?.env || {};
|
|
5
|
+
const baseUrl = env.CLIDECK_URL || `http://localhost:${env.CLIDECK_PORT || "4000"}`;
|
|
6
|
+
const endpoint = `${baseUrl.replace(/\/$/, "")}/hook/pi`;
|
|
7
|
+
|
|
8
|
+
function safeCall(fn: (() => string | undefined) | undefined): string | undefined {
|
|
9
|
+
try { return fn?.(); } catch { return undefined; }
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function post(event: any, ctx: any): void {
|
|
13
|
+
const payload = {
|
|
14
|
+
event: event?.type || "",
|
|
15
|
+
reason: event?.reason || "",
|
|
16
|
+
clideck_id: env.CLIDECK_SESSION_ID || "",
|
|
17
|
+
session_id: safeCall(() => ctx.sessionManager.getSessionId()) || "",
|
|
18
|
+
session_file: safeCall(() => ctx.sessionManager.getSessionFile()) || "",
|
|
19
|
+
cwd: ctx?.cwd || "",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
fetch(endpoint, {
|
|
23
|
+
method: "POST",
|
|
24
|
+
headers: { "Content-Type": "application/json" },
|
|
25
|
+
body: JSON.stringify(payload),
|
|
26
|
+
}).catch(() => {});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default function (pi: any): void {
|
|
30
|
+
pi.on("session_start", (event: any, ctx: any) => post(event, ctx));
|
|
31
|
+
pi.on("session_shutdown", (event: any, ctx: any) => post(event, ctx));
|
|
32
|
+
pi.on("agent_start", (event: any, ctx: any) => post(event, ctx));
|
|
33
|
+
pi.on("agent_end", (event: any, ctx: any) => post(event, ctx));
|
|
34
|
+
}
|
package/preset-utils.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const { binName } = require('./utils');
|
|
2
|
+
|
|
3
|
+
function presetForCommand(cmd, presets, options = {}) {
|
|
4
|
+
const usePresetId = options.usePresetId !== false;
|
|
5
|
+
if (usePresetId && cmd?.presetId) {
|
|
6
|
+
const preset = presets.find(p => p.presetId === cmd.presetId);
|
|
7
|
+
if (preset) return preset;
|
|
8
|
+
}
|
|
9
|
+
const bin = binName(cmd?.command);
|
|
10
|
+
return presets.find(p => binName(p.command) === bin);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
module.exports = { presetForCommand };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Pi">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="pi-bg" x1="10" y1="8" x2="56" y2="58" gradientUnits="userSpaceOnUse">
|
|
4
|
+
<stop stop-color="#2dd4bf"/>
|
|
5
|
+
<stop offset="0.5" stop-color="#60a5fa"/>
|
|
6
|
+
<stop offset="1" stop-color="#a78bfa"/>
|
|
7
|
+
</linearGradient>
|
|
8
|
+
</defs>
|
|
9
|
+
<rect width="64" height="64" rx="18" fill="#0f172a"/>
|
|
10
|
+
<path d="M15 20.5c5.4-2.6 11.5-3.9 18.3-3.9 6.1 0 11.4 1.1 15.9 3.2" fill="none" stroke="url(#pi-bg)" stroke-width="5.5" stroke-linecap="round"/>
|
|
11
|
+
<path d="M24 23.5v22M40 23.5v22" fill="none" stroke="#f8fafc" stroke-width="5.5" stroke-linecap="round"/>
|
|
12
|
+
<path d="M17 45.5h30" fill="none" stroke="url(#pi-bg)" stroke-width="5.5" stroke-linecap="round"/>
|
|
13
|
+
</svg>
|
package/public/index.html
CHANGED
|
@@ -236,7 +236,10 @@
|
|
|
236
236
|
<div class="px-2.5 pb-2.5 flex flex-col gap-2">
|
|
237
237
|
<div class="relative">
|
|
238
238
|
<svg class="absolute left-2.5 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500 pointer-events-none" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
|
|
239
|
-
<input id="search-input" type="text" placeholder="Search" class="w-full pl-9 pr-
|
|
239
|
+
<input id="search-input" type="text" placeholder="Search" class="w-full pl-9 pr-9 py-[7px] text-[13px] bg-slate-800/50 rounded-full text-slate-300 placeholder-slate-600 outline-none border border-transparent focus:border-slate-600/60 focus:bg-slate-800/80 transition-all">
|
|
240
|
+
<button id="search-clear" class="hidden absolute right-3 top-1/2 -translate-y-1/2 w-6 h-6 rounded-full border border-slate-700/40 bg-slate-800/40 text-slate-500 hover:text-slate-200 hover:bg-slate-700/60 transition-all flex items-center justify-center" title="Clear search" aria-label="Clear search">
|
|
241
|
+
<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"><path d="M18 6 6 18M6 6l12 12"/></svg>
|
|
242
|
+
</button>
|
|
240
243
|
</div>
|
|
241
244
|
<div class="flex bg-slate-800/30 rounded-lg p-[3px]">
|
|
242
245
|
<button class="filter-tab flex-1 text-[11px] font-medium py-[5px] rounded-md transition-all bg-slate-700/60 text-slate-200" data-tab="all">All</button>
|