@logicseek/hello-cc 0.1.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/LICENSE +201 -0
- package/README.md +462 -0
- package/README.zh-CN.md +409 -0
- package/bin/hcc.mjs +6355 -0
- package/docs/design.md +263 -0
- package/docs/implementation.md +345 -0
- package/lib/discover.mjs +477 -0
- package/lib/setup.mjs +766 -0
- package/package.json +54 -0
- package/scripts/regression.mjs +1221 -0
package/lib/discover.mjs
ADDED
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zero-config session discovery for Claude Code and Codex.
|
|
3
|
+
*
|
|
4
|
+
* Three layers:
|
|
5
|
+
* 1. File watchers — ~/.claude/sessions/ and ~/.codex/sessions/today/
|
|
6
|
+
* 2. Initial scan — read existing session files + /proc process scan
|
|
7
|
+
* 3. Process env scan — walk /proc to find running claude/codex PIDs
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import os from 'node:os';
|
|
13
|
+
import { spawnSync } from 'node:child_process';
|
|
14
|
+
|
|
15
|
+
// ─── Root detection ──────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Return the exact current hcc project path. Cross-path sharing is explicit via
|
|
19
|
+
* HCC_ROOT/HCC_DB, so discovery does not walk to parent directories.
|
|
20
|
+
*/
|
|
21
|
+
export function findHccRoot(cwd) {
|
|
22
|
+
if (!cwd) return null;
|
|
23
|
+
return path.resolve(cwd);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function fsExists(p) {
|
|
27
|
+
try { fs.accessSync(p); return true; } catch { return false; }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ─── Peer ID derivation ───────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Derive a stable peer ID.
|
|
34
|
+
*
|
|
35
|
+
* Stability contract:
|
|
36
|
+
* - `claude --resume <old-id>` → claude-<old-id[:8]> (same across restarts)
|
|
37
|
+
* - `codex resume <id>` → codex-<id[:8]> (same across restarts)
|
|
38
|
+
* - fresh session → <kind>-<sessionId[:8]> (unique per session)
|
|
39
|
+
*/
|
|
40
|
+
export function derivePeerId(kind, sessionId, resumeId, explicitPeer = null) {
|
|
41
|
+
if (explicitPeer) return explicitPeer;
|
|
42
|
+
|
|
43
|
+
// If HCC_PEER is already set (set by shim or hcc run), respect it
|
|
44
|
+
if (process.env.HCC_PEER) return process.env.HCC_PEER;
|
|
45
|
+
|
|
46
|
+
const base = resumeId || sessionId;
|
|
47
|
+
if (base && base.length >= 4) return `${kind}-${base.slice(0, 8)}`;
|
|
48
|
+
|
|
49
|
+
// Fallback: tty device number or pid
|
|
50
|
+
const ttyNum = getTtyNum();
|
|
51
|
+
return `${kind}-${ttyNum || process.ppid || Date.now().toString(36).slice(-6)}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getTtyNum() {
|
|
55
|
+
try {
|
|
56
|
+
const r = spawnSync('tty', [], { encoding: 'utf8', timeout: 1000 });
|
|
57
|
+
if (r.status === 0) {
|
|
58
|
+
return r.stdout.trim().replace(/[^0-9]/g, '').slice(-4) || null;
|
|
59
|
+
}
|
|
60
|
+
} catch {}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ─── Resume argument parsing ──────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Extract the session ID from `claude --resume <id>` or `claude -r <id>`.
|
|
68
|
+
* Returns null if no --resume flag found.
|
|
69
|
+
*/
|
|
70
|
+
export function parseClaudeResumeId(cmdArgs) {
|
|
71
|
+
for (let i = 0; i < cmdArgs.length; i++) {
|
|
72
|
+
const arg = cmdArgs[i];
|
|
73
|
+
if ((arg === '--resume' || arg === '-r') && cmdArgs[i + 1]) return cmdArgs[i + 1] || null;
|
|
74
|
+
if (arg.startsWith('--resume=')) return arg.slice('--resume='.length);
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function parseClaudeSessionId(cmdArgs) {
|
|
80
|
+
for (let i = 0; i < cmdArgs.length; i += 1) {
|
|
81
|
+
const arg = cmdArgs[i];
|
|
82
|
+
if (arg === '--session-id' && cmdArgs[i + 1]) return cmdArgs[i + 1] || null;
|
|
83
|
+
if (arg.startsWith('--session-id=')) return arg.slice('--session-id='.length);
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function parseClaudeFork(cmdArgs) {
|
|
89
|
+
return cmdArgs.includes('--fork-session');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Extract the session ID from `codex resume <id>` subcommand.
|
|
94
|
+
*/
|
|
95
|
+
export function parseCodexResumeId(cmdArgs) {
|
|
96
|
+
const parsed = parseCodexCommand(cmdArgs);
|
|
97
|
+
if (parsed.subcommand !== 'resume') return null;
|
|
98
|
+
return parsed.sessionId || null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function parseCodexCommand(cmdArgs) {
|
|
102
|
+
const idx = cmdArgs.findIndex(a => a === 'resume' || a === 'fork');
|
|
103
|
+
if (idx < 0) return { subcommand: null, sessionId: null, last: false };
|
|
104
|
+
const subcommand = cmdArgs[idx];
|
|
105
|
+
let sessionId = null;
|
|
106
|
+
let last = false;
|
|
107
|
+
for (let i = idx + 1; i < cmdArgs.length; i += 1) {
|
|
108
|
+
const arg = cmdArgs[i];
|
|
109
|
+
if (arg === '--last') { last = true; continue; }
|
|
110
|
+
if (arg.startsWith('-')) {
|
|
111
|
+
if (codexOptionTakesValue(arg)) i += 1;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
sessionId = arg;
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
return { subcommand, sessionId, last };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function codexOptionTakesValue(arg) {
|
|
121
|
+
if (!arg || arg.includes('=')) return false;
|
|
122
|
+
return new Set([
|
|
123
|
+
'-c', '--config',
|
|
124
|
+
'--remote',
|
|
125
|
+
'--remote-auth-token-env',
|
|
126
|
+
'--enable',
|
|
127
|
+
'--disable',
|
|
128
|
+
'-i', '--image',
|
|
129
|
+
'-m', '--model',
|
|
130
|
+
'--local-provider',
|
|
131
|
+
'-p', '--profile',
|
|
132
|
+
'-s', '--sandbox',
|
|
133
|
+
'-C', '--cd',
|
|
134
|
+
'--add-dir',
|
|
135
|
+
'-a', '--ask-for-approval'
|
|
136
|
+
]).has(arg);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function parseCodexForkSourceId(cmdArgs) {
|
|
140
|
+
const parsed = parseCodexCommand(cmdArgs);
|
|
141
|
+
if (parsed.subcommand !== 'fork') return null;
|
|
142
|
+
return parsed.sessionId || null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function parseCodexLast(cmdArgs) {
|
|
146
|
+
const parsed = parseCodexCommand(cmdArgs);
|
|
147
|
+
return Boolean(parsed.subcommand && parsed.last && !parsed.sessionId);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ─── Process utilities ────────────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
export function isAlive(pid) {
|
|
153
|
+
if (!pid) return false;
|
|
154
|
+
// Linux /proc fast check
|
|
155
|
+
if (process.platform === 'linux') {
|
|
156
|
+
try { fs.accessSync(`/proc/${pid}`); return true; } catch { return false; }
|
|
157
|
+
}
|
|
158
|
+
// POSIX kill(pid, 0)
|
|
159
|
+
try { process.kill(pid, 0); return true; }
|
|
160
|
+
catch (e) { return e.code === 'EPERM'; }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function readFileSafe(p) {
|
|
164
|
+
try { return fs.readFileSync(p, 'utf8'); } catch { return ''; }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function readEnvFile(envPath) {
|
|
168
|
+
const result = {};
|
|
169
|
+
const raw = readFileSafe(envPath);
|
|
170
|
+
if (!raw) return result;
|
|
171
|
+
for (const entry of raw.split('\0')) {
|
|
172
|
+
const eq = entry.indexOf('=');
|
|
173
|
+
if (eq > 0) result[entry.slice(0, eq)] = entry.slice(eq + 1);
|
|
174
|
+
}
|
|
175
|
+
return result;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ─── Claude Code session discovery ───────────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
const CLAUDE_SESSIONS_DIR = path.join(os.homedir(), '.claude', 'sessions');
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Read existing Claude Code session files and return info about live sessions
|
|
184
|
+
* that belong to a hello-cc project.
|
|
185
|
+
*/
|
|
186
|
+
export function scanClaudeSessions() {
|
|
187
|
+
const results = [];
|
|
188
|
+
let files;
|
|
189
|
+
try { files = fs.readdirSync(CLAUDE_SESSIONS_DIR); }
|
|
190
|
+
catch { return results; }
|
|
191
|
+
|
|
192
|
+
for (const file of files) {
|
|
193
|
+
if (!file.endsWith('.json')) continue;
|
|
194
|
+
try {
|
|
195
|
+
const content = JSON.parse(
|
|
196
|
+
fs.readFileSync(path.join(CLAUDE_SESSIONS_DIR, file), 'utf8')
|
|
197
|
+
);
|
|
198
|
+
const { pid, sessionId, cwd, status } = content;
|
|
199
|
+
if (!pid || !sessionId || !cwd) continue;
|
|
200
|
+
if (!isAlive(pid)) continue;
|
|
201
|
+
const env = readEnvFile(`/proc/${pid}/environ`);
|
|
202
|
+
|
|
203
|
+
const hccRoot = findHccRoot(cwd);
|
|
204
|
+
if (!hccRoot) continue;
|
|
205
|
+
|
|
206
|
+
results.push({
|
|
207
|
+
kind: 'claude',
|
|
208
|
+
sessionId,
|
|
209
|
+
peerId: derivePeerId('claude', sessionId, null, env.HCC_PEER),
|
|
210
|
+
pid,
|
|
211
|
+
cwd,
|
|
212
|
+
hccRoot,
|
|
213
|
+
status: status || 'unknown',
|
|
214
|
+
transport: 'detected',
|
|
215
|
+
});
|
|
216
|
+
} catch { /* ignore malformed files */ }
|
|
217
|
+
}
|
|
218
|
+
return results;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Watch ~/.claude/sessions/ for new session files.
|
|
223
|
+
* Calls `onNew(sessionInfo)` for each new live session in an HCC project.
|
|
224
|
+
* Returns the fs.FSWatcher or null.
|
|
225
|
+
*/
|
|
226
|
+
export function watchClaudeSessions(onNew) {
|
|
227
|
+
try {
|
|
228
|
+
fs.mkdirSync(CLAUDE_SESSIONS_DIR, { recursive: true });
|
|
229
|
+
} catch {}
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
return fs.watch(CLAUDE_SESSIONS_DIR, { persistent: false }, (event, filename) => {
|
|
233
|
+
if (event !== 'rename' || !filename?.endsWith('.json')) return;
|
|
234
|
+
// Small delay so Claude Code finishes writing the file
|
|
235
|
+
setTimeout(() => {
|
|
236
|
+
try {
|
|
237
|
+
const content = JSON.parse(
|
|
238
|
+
fs.readFileSync(path.join(CLAUDE_SESSIONS_DIR, filename), 'utf8')
|
|
239
|
+
);
|
|
240
|
+
const { pid, sessionId, cwd, status } = content;
|
|
241
|
+
if (!pid || !sessionId || !cwd) return;
|
|
242
|
+
if (!isAlive(pid)) return;
|
|
243
|
+
const env = readEnvFile(`/proc/${pid}/environ`);
|
|
244
|
+
const hccRoot = findHccRoot(cwd);
|
|
245
|
+
if (!hccRoot) return;
|
|
246
|
+
onNew({
|
|
247
|
+
kind: 'claude',
|
|
248
|
+
sessionId,
|
|
249
|
+
peerId: derivePeerId('claude', sessionId, null, env.HCC_PEER),
|
|
250
|
+
pid,
|
|
251
|
+
cwd,
|
|
252
|
+
hccRoot,
|
|
253
|
+
status: status || 'unknown',
|
|
254
|
+
transport: 'detected',
|
|
255
|
+
});
|
|
256
|
+
} catch {}
|
|
257
|
+
}, 300);
|
|
258
|
+
});
|
|
259
|
+
} catch {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ─── Codex session discovery ──────────────────────────────────────────────────
|
|
265
|
+
|
|
266
|
+
function codexSessionsDir() {
|
|
267
|
+
const d = new Date();
|
|
268
|
+
return path.join(
|
|
269
|
+
os.homedir(), '.codex', 'sessions',
|
|
270
|
+
String(d.getFullYear()),
|
|
271
|
+
String(d.getMonth() + 1).padStart(2, '0'),
|
|
272
|
+
String(d.getDate()).padStart(2, '0')
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function parseCodexSessionFile(filePath) {
|
|
277
|
+
try {
|
|
278
|
+
const firstLine = fs.readFileSync(filePath, 'utf8').split('\n')[0];
|
|
279
|
+
if (!firstLine) return null;
|
|
280
|
+
const parsed = JSON.parse(firstLine);
|
|
281
|
+
if (parsed.type !== 'session_meta') return null;
|
|
282
|
+
const { id, cwd } = parsed.payload || {};
|
|
283
|
+
if (!id || !cwd) return null;
|
|
284
|
+
return { sessionId: id, cwd };
|
|
285
|
+
} catch { return null; }
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Scan today's Codex session files. Match to running processes via cmdline.
|
|
290
|
+
*/
|
|
291
|
+
export function scanCodexSessions() {
|
|
292
|
+
const results = [];
|
|
293
|
+
const dir = codexSessionsDir();
|
|
294
|
+
let files;
|
|
295
|
+
try { files = fs.readdirSync(dir); } catch { return results; }
|
|
296
|
+
|
|
297
|
+
// Build maps from /proc. Fresh sessions and --last/fork launches usually do
|
|
298
|
+
// not expose the new session id in argv, so cwd is the fallback.
|
|
299
|
+
const runningCodex = getRunningCodexSessions();
|
|
300
|
+
|
|
301
|
+
for (const file of files.sort().reverse().slice(0, 30)) {
|
|
302
|
+
if (!file.endsWith('.jsonl')) continue;
|
|
303
|
+
const info = parseCodexSessionFile(path.join(dir, file));
|
|
304
|
+
if (!info) continue;
|
|
305
|
+
|
|
306
|
+
const proc = runningCodex.bySessionId.get(info.sessionId) || firstProcessForCwd(runningCodex.byCwd, info.cwd);
|
|
307
|
+
if (!proc) continue;
|
|
308
|
+
|
|
309
|
+
const hccRoot = findHccRoot(info.cwd);
|
|
310
|
+
if (!hccRoot) continue;
|
|
311
|
+
|
|
312
|
+
results.push({
|
|
313
|
+
kind: 'codex',
|
|
314
|
+
sessionId: info.sessionId,
|
|
315
|
+
peerId: derivePeerId('codex', info.sessionId, null, proc.peerId),
|
|
316
|
+
pid: proc.pid,
|
|
317
|
+
cwd: info.cwd,
|
|
318
|
+
hccRoot,
|
|
319
|
+
status: 'running',
|
|
320
|
+
transport: 'detected',
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
return results;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Walk /proc to find all running `codex` Node processes and map session ID → PID.
|
|
328
|
+
* Codex launches as: `node /path/to/codex [resume] <session-id>` or similar.
|
|
329
|
+
*/
|
|
330
|
+
function getRunningCodexSessions() {
|
|
331
|
+
const bySessionId = new Map();
|
|
332
|
+
const byCwd = new Map();
|
|
333
|
+
if (process.platform !== 'linux') return { bySessionId, byCwd };
|
|
334
|
+
|
|
335
|
+
let pids;
|
|
336
|
+
try { pids = fs.readdirSync('/proc').filter(e => /^\d+$/.test(e)); }
|
|
337
|
+
catch { return { bySessionId, byCwd }; }
|
|
338
|
+
|
|
339
|
+
for (const pid of pids) {
|
|
340
|
+
try {
|
|
341
|
+
const cmdline = readFileSafe(`/proc/${pid}/cmdline`).split('\0');
|
|
342
|
+
if (!cmdline.some(a => a.includes('codex'))) continue;
|
|
343
|
+
const cwd = fs.readlinkSync(`/proc/${pid}/cwd`);
|
|
344
|
+
const env = readEnvFile(`/proc/${pid}/environ`);
|
|
345
|
+
const procInfo = { pid: parseInt(pid), peerId: env.HCC_PEER || null };
|
|
346
|
+
if (!byCwd.has(cwd)) byCwd.set(cwd, []);
|
|
347
|
+
byCwd.get(cwd).push(procInfo);
|
|
348
|
+
|
|
349
|
+
// codex resume <uuid>
|
|
350
|
+
const resumeId = parseCodexResumeId(cmdline);
|
|
351
|
+
if (resumeId) {
|
|
352
|
+
bySessionId.set(resumeId, procInfo);
|
|
353
|
+
}
|
|
354
|
+
} catch {}
|
|
355
|
+
}
|
|
356
|
+
return { bySessionId, byCwd };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function firstProcessForCwd(byCwd, cwd) {
|
|
360
|
+
const direct = byCwd.get(cwd);
|
|
361
|
+
if (direct?.length) return direct[0];
|
|
362
|
+
for (const [procCwd, pids] of byCwd.entries()) {
|
|
363
|
+
if ((procCwd === cwd || procCwd.startsWith(`${cwd}${path.sep}`) || cwd.startsWith(`${procCwd}${path.sep}`)) && pids.length) {
|
|
364
|
+
return pids[0];
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Watch today's Codex sessions directory for new session files.
|
|
372
|
+
*/
|
|
373
|
+
export function watchCodexSessions(onNew) {
|
|
374
|
+
const dir = codexSessionsDir();
|
|
375
|
+
try { fs.mkdirSync(dir, { recursive: true }); } catch {}
|
|
376
|
+
|
|
377
|
+
try {
|
|
378
|
+
return fs.watch(dir, { persistent: false }, (event, filename) => {
|
|
379
|
+
if (event !== 'rename' || !filename?.endsWith('.jsonl')) return;
|
|
380
|
+
setTimeout(() => {
|
|
381
|
+
const info = parseCodexSessionFile(path.join(dir, filename));
|
|
382
|
+
if (!info) return;
|
|
383
|
+
|
|
384
|
+
const running = getRunningCodexSessions();
|
|
385
|
+
const proc = running.bySessionId.get(info.sessionId) || firstProcessForCwd(running.byCwd, info.cwd);
|
|
386
|
+
|
|
387
|
+
const hccRoot = findHccRoot(info.cwd);
|
|
388
|
+
if (!hccRoot) return;
|
|
389
|
+
|
|
390
|
+
onNew({
|
|
391
|
+
kind: 'codex',
|
|
392
|
+
sessionId: info.sessionId,
|
|
393
|
+
peerId: derivePeerId('codex', info.sessionId, null, proc?.peerId),
|
|
394
|
+
pid: proc?.pid || null,
|
|
395
|
+
cwd: info.cwd,
|
|
396
|
+
hccRoot,
|
|
397
|
+
status: 'running',
|
|
398
|
+
transport: 'detected',
|
|
399
|
+
});
|
|
400
|
+
}, 500);
|
|
401
|
+
});
|
|
402
|
+
} catch {
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// ─── Generic process scan (fallback) ─────────────────────────────────────────
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Scan /proc for any running claude/codex processes that belong to an HCC
|
|
411
|
+
* project. Used as a fallback when session files are not available.
|
|
412
|
+
* Returns an array of discovered session info objects.
|
|
413
|
+
*/
|
|
414
|
+
export function scanProcesses() {
|
|
415
|
+
const results = [];
|
|
416
|
+
if (process.platform !== 'linux') return results;
|
|
417
|
+
|
|
418
|
+
let pids;
|
|
419
|
+
try { pids = fs.readdirSync('/proc').filter(e => /^\d+$/.test(e)); }
|
|
420
|
+
catch { return results; }
|
|
421
|
+
|
|
422
|
+
const seen = new Set();
|
|
423
|
+
|
|
424
|
+
for (const pidStr of pids) {
|
|
425
|
+
try {
|
|
426
|
+
const cmdline = readFileSafe(`/proc/${pidStr}/cmdline`).split('\0');
|
|
427
|
+
const hasClaude = cmdline.some(a => /claude/.test(a) && !a.includes('hcc'));
|
|
428
|
+
const hasCodex = cmdline.some(a => /\bcodex\b/.test(a));
|
|
429
|
+
if (!hasClaude && !hasCodex) continue;
|
|
430
|
+
|
|
431
|
+
const cwd = fs.readlinkSync(`/proc/${pidStr}/cwd`);
|
|
432
|
+
const env = readEnvFile(`/proc/${pidStr}/environ`);
|
|
433
|
+
|
|
434
|
+
const kind = hasClaude ? 'claude' : 'codex';
|
|
435
|
+
|
|
436
|
+
// Skip subagent/helper processes (they share CLAUDE_CODE_SESSION_ID
|
|
437
|
+
// with the main session but are not the interactive terminal)
|
|
438
|
+
if (kind === 'claude') {
|
|
439
|
+
const entrypoint = env.CLAUDE_CODE_ENTRYPOINT || '';
|
|
440
|
+
if (entrypoint && entrypoint !== 'cli') continue;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const explicitClaudeSession = kind === 'claude' ? parseClaudeSessionId(cmdline) : '';
|
|
444
|
+
const sessionId = kind === 'claude'
|
|
445
|
+
? (env.CLAUDE_CODE_SESSION_ID || explicitClaudeSession || '')
|
|
446
|
+
: parseCodexResumeId(cmdline) || '';
|
|
447
|
+
|
|
448
|
+
let resumeId = null;
|
|
449
|
+
if (kind === 'claude') {
|
|
450
|
+
resumeId = parseClaudeFork(cmdline) ? null : parseClaudeResumeId(cmdline);
|
|
451
|
+
} else {
|
|
452
|
+
resumeId = parseCodexResumeId(cmdline);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const peerId = derivePeerId(kind, sessionId, resumeId, env.HCC_PEER);
|
|
456
|
+
if (seen.has(peerId)) continue;
|
|
457
|
+
seen.add(peerId);
|
|
458
|
+
|
|
459
|
+
const hccRoot = findHccRoot(cwd);
|
|
460
|
+
if (!hccRoot) continue;
|
|
461
|
+
|
|
462
|
+
results.push({
|
|
463
|
+
kind,
|
|
464
|
+
sessionId,
|
|
465
|
+
resumeId,
|
|
466
|
+
peerId,
|
|
467
|
+
pid: parseInt(pidStr),
|
|
468
|
+
cwd,
|
|
469
|
+
hccRoot,
|
|
470
|
+
status: 'running',
|
|
471
|
+
transport: 'detected',
|
|
472
|
+
});
|
|
473
|
+
} catch {}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return results;
|
|
477
|
+
}
|