@logicseek/hello-cc 0.1.4 → 0.1.6

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.
Files changed (90) hide show
  1. package/CHANGELOG.md +126 -0
  2. package/bin/hcc.mjs +2739 -6482
  3. package/docs/README.md +2 -0
  4. package/docs/README.zh-CN.md +3 -1
  5. package/docs/architecture-split-review.md +217 -0
  6. package/docs/architecture.md +385 -0
  7. package/docs/architecture.zh-CN.md +364 -0
  8. package/docs/commands.md +21 -9
  9. package/docs/commands.zh-CN.md +19 -9
  10. package/docs/layout-split-stack-audit.md +298 -0
  11. package/lib/automation.mjs +8 -0
  12. package/lib/cli-args.mjs +89 -0
  13. package/lib/cli-runtime.mjs +44 -0
  14. package/lib/coordination-state.mjs +218 -0
  15. package/lib/core/coordination/automation.mjs +222 -0
  16. package/lib/core/coordination/locks.mjs +46 -0
  17. package/lib/core/coordination/messages.mjs +116 -0
  18. package/lib/core/coordination/tasks.mjs +224 -0
  19. package/lib/core/coordination/teams.mjs +73 -0
  20. package/lib/core/coordination/timeline.mjs +187 -0
  21. package/lib/core/peers/bindings.mjs +83 -0
  22. package/lib/core/peers/format.mjs +13 -0
  23. package/lib/core/peers/identity.mjs +53 -0
  24. package/lib/core/peers/liveness.mjs +90 -0
  25. package/lib/core/peers/reconcile.mjs +238 -0
  26. package/lib/core/peers/session.mjs +14 -0
  27. package/lib/core/sessions/launch.mjs +89 -0
  28. package/lib/db/schema.mjs +324 -0
  29. package/lib/db/stores/peers.mjs +231 -0
  30. package/lib/db-schema.mjs +7 -0
  31. package/lib/errors.mjs +1 -0
  32. package/lib/format.mjs +39 -0
  33. package/lib/guidance.mjs +10 -2
  34. package/lib/handoff.mjs +26 -0
  35. package/lib/help.mjs +3 -0
  36. package/lib/integrations/hooks.mjs +171 -0
  37. package/lib/integrations/peers/identity.mjs +208 -0
  38. package/lib/integrations/peers/processes.mjs +151 -0
  39. package/lib/integrations/providers.mjs +282 -0
  40. package/lib/integrations/shims/script.mjs +384 -0
  41. package/lib/integrations/shims.mjs +106 -0
  42. package/lib/json-file.mjs +1 -0
  43. package/lib/locks.mjs +10 -0
  44. package/lib/messages.mjs +3 -0
  45. package/lib/package-meta.mjs +6 -24
  46. package/lib/peer-bindings.mjs +14 -0
  47. package/lib/peer-format.mjs +4 -0
  48. package/lib/peer-identity.mjs +15 -0
  49. package/lib/project-context.mjs +6 -0
  50. package/lib/project-registry.mjs +7 -0
  51. package/lib/provider-commands.mjs +13 -0
  52. package/lib/release/package-meta.mjs +31 -0
  53. package/lib/release/release-notes.mjs +40 -0
  54. package/lib/release-notes.mjs +6 -40
  55. package/lib/runtime/client.mjs +42 -0
  56. package/lib/runtime/paths.mjs +45 -0
  57. package/lib/runtime/project-context.mjs +32 -0
  58. package/lib/runtime/projects.mjs +76 -0
  59. package/lib/runtime/state.mjs +128 -0
  60. package/lib/runtime-client.mjs +3 -0
  61. package/lib/runtime-paths.mjs +11 -0
  62. package/lib/runtime-state.mjs +11 -0
  63. package/lib/session-launch.mjs +17 -0
  64. package/lib/setup.mjs +18 -765
  65. package/lib/shared/errors.mjs +7 -0
  66. package/lib/shared/json-file.mjs +14 -0
  67. package/lib/shell-path.mjs +69 -0
  68. package/lib/shim-script.mjs +1 -0
  69. package/lib/state-render.mjs +5 -0
  70. package/lib/task-cli.mjs +45 -0
  71. package/lib/task-liveness.mjs +8 -0
  72. package/lib/task-store.mjs +3 -0
  73. package/lib/team-planning.mjs +7 -0
  74. package/lib/terminal/tmux.mjs +372 -0
  75. package/lib/timeline.mjs +12 -0
  76. package/lib/tmux.mjs +18 -0
  77. package/lib/ui/help.mjs +419 -0
  78. package/lib/ui/state-render.mjs +61 -0
  79. package/lib/web/http.mjs +55 -0
  80. package/lib/web/peer-actions.mjs +270 -0
  81. package/lib/web/runtime.mjs +154 -0
  82. package/lib/web/ui-template.mjs +2055 -0
  83. package/lib/web-http.mjs +7 -0
  84. package/lib/web-peer-actions.mjs +3 -0
  85. package/lib/web-runtime.mjs +17 -0
  86. package/lib/web-ui-template.mjs +3 -0
  87. package/package.json +1 -1
  88. package/scripts/github-release.mjs +2 -2
  89. package/scripts/regression.mjs +2738 -22
  90. package/scripts/release-notes.mjs +2 -2
package/lib/guidance.mjs CHANGED
@@ -53,8 +53,16 @@ Before mutating work:
53
53
  explicitly; do not create or claim hidden extra tasks.
54
54
 
55
55
  Before editing or mutating shared resources:
56
- - Acquire an advisory lock for the file, directory, module, or shared resource.
57
- - If another live peer holds the lock, message that peer instead of editing.
56
+ - Acquire an advisory lock for the file, directory, module scope, or shared
57
+ resource.
58
+ - Locks are coordination signals, not task ownership and not read barriers.
59
+ A broad lock such as \`--resource bin/hcc.mjs\` protects the whole file, while
60
+ scoped locks such as \`--resource bin/hcc.mjs --scope db-schema\` and
61
+ \`--scope web-ui\` can be held by different peers at the same time.
62
+ - If another live peer holds a conflicting lock, message that peer before
63
+ editing. When the work is in separate regions of the same file, agree on
64
+ narrower scoped locks and wait for all related scopes to hand off or release
65
+ before final tests, formatting, staging, or commit.
58
66
  - Acquire locks only before writes or shared-resource mutation, including file
59
67
  edits, formatting, staging/committing through \`.git/index\`, intentional DB
60
68
  data/schema changes, starting/stopping/restarting shared runtimes, or taking
@@ -0,0 +1,26 @@
1
+ import { runGit } from './runtime/project-context.mjs';
2
+
3
+ export function normalizeListText(value, fallback = []) {
4
+ if (value === undefined || value === null || value === '') return JSON.stringify(fallback);
5
+ const text = String(value);
6
+ try {
7
+ JSON.parse(text);
8
+ return text;
9
+ } catch {
10
+ return JSON.stringify(text.split(',').map((item) => item.trim()).filter(Boolean));
11
+ }
12
+ }
13
+
14
+ export function changedFiles(cwd) {
15
+ const unstaged = runGit(['diff', '--name-only'], cwd);
16
+ const staged = runGit(['diff', '--cached', '--name-only'], cwd);
17
+ const files = new Set();
18
+ for (const block of [unstaged, staged]) {
19
+ if (!block) continue;
20
+ for (const line of block.split('\n')) {
21
+ const trimmed = line.trim();
22
+ if (trimmed) files.add(trimmed);
23
+ }
24
+ }
25
+ return [...files].sort();
26
+ }
package/lib/help.mjs ADDED
@@ -0,0 +1,3 @@
1
+ export {
2
+ createHelpFunctions
3
+ } from './ui/help.mjs';
@@ -0,0 +1,171 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ import { readJsonSafe, writeJsonSafe } from '../shared/json-file.mjs';
4
+
5
+ const CLAUDE_SETTINGS_PATH = path.join(os.homedir(), '.claude', 'settings.json');
6
+ const CODEX_HOOKS_PATH = path.join(os.homedir(), '.codex', 'hooks.json');
7
+
8
+ /**
9
+ * Hook events we install handlers for.
10
+ * - SessionStart : Inject initial coordination context
11
+ * - UserPromptSubmit : Inject fresh coordination context before the model answers
12
+ * - Stop : Claude goes idle after a turn -> deliver inbox messages
13
+ * - PostToolUse : After every tool call -> heartbeat + inbox check
14
+ * - PreToolUse : Before every tool call -> register peer if not yet registered
15
+ */
16
+ const CLAUDE_HOOK_EVENTS = ['SessionStart', 'UserPromptSubmit', 'Stop', 'PostToolUse', 'PreToolUse'];
17
+ const CODEX_HOOK_EVENTS = ['SessionStart', 'UserPromptSubmit', 'PreToolUse', 'Stop'];
18
+
19
+ /**
20
+ * Install (or update) hello-cc hooks in ~/.claude/settings.json.
21
+ * Non-destructive: only adds hcc entries, never removes existing ones.
22
+ * Returns the settings file path.
23
+ */
24
+ export function installClaudeHooks(hccBin) {
25
+ const settings = readJsonSafe(CLAUDE_SETTINGS_PATH) || {};
26
+
27
+ if (!settings.hooks) settings.hooks = {};
28
+
29
+ const hookCmd = `${hccBin} hook`;
30
+
31
+ for (const event of CLAUDE_HOOK_EVENTS) {
32
+ settings.hooks[event] = mergeHookEntry(
33
+ settings.hooks[event],
34
+ `${hookCmd} ${event.toLowerCase()}`
35
+ );
36
+ }
37
+
38
+ writeJsonSafe(CLAUDE_SETTINGS_PATH, settings);
39
+ return CLAUDE_SETTINGS_PATH;
40
+ }
41
+
42
+ /**
43
+ * Remove hello-cc hook entries from ~/.claude/settings.json.
44
+ */
45
+ export function uninstallClaudeHooks() {
46
+ const settings = readJsonSafe(CLAUDE_SETTINGS_PATH);
47
+ if (!settings?.hooks) return false;
48
+
49
+ for (const event of Object.keys(settings.hooks)) {
50
+ if (!Array.isArray(settings.hooks[event])) continue;
51
+ settings.hooks[event] = settings.hooks[event].filter(entry => {
52
+ if (!Array.isArray(entry?.hooks)) return true;
53
+ return !entry.hooks.some(h => isHccHookCmd(h?.command));
54
+ });
55
+ if (settings.hooks[event].length === 0) delete settings.hooks[event];
56
+ }
57
+
58
+ writeJsonSafe(CLAUDE_SETTINGS_PATH, settings);
59
+ return true;
60
+ }
61
+
62
+ /**
63
+ * Returns true if hello-cc hooks are present in ~/.claude/settings.json.
64
+ */
65
+ export function verifyClaudeHooks() {
66
+ const settings = readJsonSafe(CLAUDE_SETTINGS_PATH);
67
+ if (!settings?.hooks) return false;
68
+ return CLAUDE_HOOK_EVENTS.every(event => hasHookEntry(settings.hooks[event]));
69
+ }
70
+
71
+ /**
72
+ * Install hello-cc hooks in ~/.codex/hooks.json.
73
+ *
74
+ * Codex hooks.json format:
75
+ * { "hooks": { "PreToolUse": [{ "matcher": "Bash", "hooks": [...] }], "Stop": [...] } }
76
+ */
77
+ export function installCodexHooks(hccBin) {
78
+ const hooks = readJsonSafe(CODEX_HOOKS_PATH) || {};
79
+
80
+ if (!hooks.hooks) hooks.hooks = {};
81
+
82
+ const hookCmd = `${hccBin} hook`;
83
+
84
+ // SessionStart: inject initial coordination context on startup/resume.
85
+ hooks.hooks.SessionStart = mergeCodexHookEntry(
86
+ hooks.hooks.SessionStart,
87
+ `${hookCmd} sessionstart`,
88
+ null
89
+ );
90
+ // UserPromptSubmit: inject fresh coordination context before each model turn.
91
+ hooks.hooks.UserPromptSubmit = mergeCodexHookEntry(
92
+ hooks.hooks.UserPromptSubmit,
93
+ `${hookCmd} userpromptsubmit`,
94
+ null
95
+ );
96
+ // PreToolUse: fires before every Bash tool call - heartbeat + peer registration
97
+ hooks.hooks.PreToolUse = mergeCodexHookEntry(
98
+ hooks.hooks.PreToolUse,
99
+ `${hookCmd} pretooluse`,
100
+ 'Bash'
101
+ );
102
+ // Stop: fires when Codex goes idle - deliver inbox messages
103
+ hooks.hooks.Stop = mergeCodexHookEntry(
104
+ hooks.hooks.Stop,
105
+ `${hookCmd} stop`,
106
+ null
107
+ );
108
+
109
+ writeJsonSafe(CODEX_HOOKS_PATH, hooks);
110
+ return CODEX_HOOKS_PATH;
111
+ }
112
+
113
+ export function uninstallCodexHooks() {
114
+ const hooks = readJsonSafe(CODEX_HOOKS_PATH);
115
+ if (!hooks?.hooks) return false;
116
+
117
+ for (const event of Object.keys(hooks.hooks)) {
118
+ if (!Array.isArray(hooks.hooks[event])) continue;
119
+ hooks.hooks[event] = hooks.hooks[event].filter(entry => {
120
+ if (!Array.isArray(entry?.hooks)) return true;
121
+ return !entry.hooks.some(h => isHccHookCmd(h?.command));
122
+ });
123
+ if (hooks.hooks[event].length === 0) delete hooks.hooks[event];
124
+ }
125
+
126
+ writeJsonSafe(CODEX_HOOKS_PATH, hooks);
127
+ return true;
128
+ }
129
+
130
+ export function verifyCodexHooks() {
131
+ const hooks = readJsonSafe(CODEX_HOOKS_PATH);
132
+ if (!hooks?.hooks) return false;
133
+ return CODEX_HOOK_EVENTS.every(event => hasHookEntry(hooks.hooks[event]));
134
+ }
135
+
136
+ function mergeHookEntry(existing, command) {
137
+ const entries = Array.isArray(existing) ? [...existing] : [];
138
+ const alreadyInstalled = entries.some(
139
+ e => Array.isArray(e?.hooks) && e.hooks.some(h => isHccHookCmd(h?.command))
140
+ );
141
+ if (alreadyInstalled) return entries;
142
+ entries.push({ hooks: [{ type: 'command', command }] });
143
+ return entries;
144
+ }
145
+
146
+ function hasHookEntry(entries) {
147
+ if (!Array.isArray(entries)) return false;
148
+ return entries.some(e =>
149
+ Array.isArray(e?.hooks) && e.hooks.some(h => isHccHookCmd(h?.command))
150
+ );
151
+ }
152
+
153
+ function isHccHookCmd(cmd) {
154
+ if (typeof cmd !== 'string') return false;
155
+ return /\bhcc\b.*\bhook\b/.test(cmd) || /hello-cc.*hook/.test(cmd);
156
+ }
157
+
158
+ function mergeCodexHookEntry(existing, command, matcher) {
159
+ const entries = Array.isArray(existing) ? [...existing] : [];
160
+ const alreadyInstalled = entries.some(
161
+ e => Array.isArray(e?.hooks) && e.hooks.some(h => isHccHookCmd(h?.command))
162
+ );
163
+ if (alreadyInstalled) return entries;
164
+
165
+ const entry = {
166
+ hooks: [{ type: 'command', command }],
167
+ };
168
+ if (matcher) entry.matcher = matcher;
169
+ entries.push(entry);
170
+ return entries;
171
+ }
@@ -0,0 +1,208 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import process from 'node:process';
4
+ import { spawnSync } from 'node:child_process';
5
+ import {
6
+ autoPeerBasis as coreAutoPeerBasis,
7
+ autoPeerId as coreAutoPeerId,
8
+ autoPeerKind as coreAutoPeerKind,
9
+ autoPeerProviderSession as coreAutoPeerProviderSession,
10
+ autoPeerResumeId as coreAutoPeerResumeId,
11
+ autoPeerSessionId as coreAutoPeerSessionId,
12
+ currentPeer as coreCurrentPeer,
13
+ resolveCurrentPeer as coreResolveCurrentPeer,
14
+ sanitizePeerPart,
15
+ shortHash
16
+ } from '../../core/peers/identity.mjs';
17
+
18
+ export { sanitizePeerPart, shortHash } from '../../core/peers/identity.mjs';
19
+
20
+ export function currentTtyName() {
21
+ if (process.env.TTY) return process.env.TTY;
22
+ const result = spawnSync('tty', [], {
23
+ encoding: 'utf8',
24
+ stdio: ['ignore', 'pipe', 'ignore']
25
+ });
26
+ if (result.status !== 0) return '';
27
+ const tty = result.stdout.trim();
28
+ return tty && tty !== 'not a tty' ? tty : '';
29
+ }
30
+
31
+ let ancestorCliInfoCache = undefined;
32
+
33
+ function readProcCmdline(pid) {
34
+ if (process.platform !== 'linux' || !pid) return [];
35
+ try {
36
+ return fs.readFileSync(`/proc/${pid}/cmdline`, 'utf8').split('\0').filter(Boolean);
37
+ } catch {
38
+ return [];
39
+ }
40
+ }
41
+
42
+ function readProcEnv(pid) {
43
+ const env = {};
44
+ if (process.platform !== 'linux' || !pid) return env;
45
+ try {
46
+ for (const entry of fs.readFileSync(`/proc/${pid}/environ`, 'utf8').split('\0')) {
47
+ const eq = entry.indexOf('=');
48
+ if (eq > 0) env[entry.slice(0, eq)] = entry.slice(eq + 1);
49
+ }
50
+ } catch {}
51
+ return env;
52
+ }
53
+
54
+ function readProcParentPid(pid) {
55
+ if (process.platform !== 'linux' || !pid) return null;
56
+ try {
57
+ const status = fs.readFileSync(`/proc/${pid}/status`, 'utf8');
58
+ const match = status.match(/^PPid:\s+(\d+)/m);
59
+ return match ? Number.parseInt(match[1], 10) : null;
60
+ } catch {
61
+ return null;
62
+ }
63
+ }
64
+
65
+ function argsLookLikeCli(args, kind) {
66
+ return args.some((arg) => {
67
+ const text = String(arg || '').toLowerCase();
68
+ const base = path.basename(text);
69
+ if (kind === 'codex') return base === 'codex' || text.includes('/codex') || text.includes('@openai/codex');
70
+ if (kind === 'claude') return base === 'claude' || base === 'claude-code' || text.includes('/claude') || text.includes('@anthropic-ai/claude-code');
71
+ return false;
72
+ });
73
+ }
74
+
75
+ function detectCliKindFromProcess(args, env) {
76
+ if (env.CLAUDE_CODE_SESSION_ID || env.CLAUDECODE === '1' || argsLookLikeCli(args, 'claude')) return 'claude';
77
+ if (
78
+ env.CODEX_SESSION_ID ||
79
+ env.CODEX_THREAD_ID ||
80
+ env.CODEX_MANAGED_BY_NPM === '1' ||
81
+ env.CODEX_MANAGED_BY_BUN === '1' ||
82
+ argsLookLikeCli(args, 'codex')
83
+ ) return 'codex';
84
+ return null;
85
+ }
86
+
87
+ export function readAncestorCliInfo() {
88
+ if (ancestorCliInfoCache !== undefined) return ancestorCliInfoCache;
89
+ if (process.platform !== 'linux') {
90
+ ancestorCliInfoCache = null;
91
+ return ancestorCliInfoCache;
92
+ }
93
+
94
+ let pid = process.ppid;
95
+ const seen = new Set();
96
+ for (let depth = 0; pid && pid > 1 && depth < 12 && !seen.has(pid); depth += 1) {
97
+ seen.add(pid);
98
+ const args = readProcCmdline(pid);
99
+ const env = readProcEnv(pid);
100
+ const kind = detectCliKindFromProcess(args, env);
101
+ if (kind) {
102
+ ancestorCliInfoCache = { pid, kind, args, env };
103
+ return ancestorCliInfoCache;
104
+ }
105
+ pid = readProcParentPid(pid);
106
+ }
107
+
108
+ ancestorCliInfoCache = null;
109
+ return ancestorCliInfoCache;
110
+ }
111
+
112
+ export function resumeIdFromArgs(kind, args) {
113
+ if (!Array.isArray(args)) return null;
114
+ if (kind === 'claude') {
115
+ if (args.includes('--fork-session')) return null;
116
+ for (let i = 0; i < args.length; i += 1) {
117
+ const arg = args[i];
118
+ if ((arg === '--resume' || arg === '-r') && args[i + 1]) return args[i + 1];
119
+ if (arg.startsWith('--resume=')) return arg.slice('--resume='.length);
120
+ }
121
+ }
122
+ if (kind === 'codex') {
123
+ const idx = args.indexOf('resume');
124
+ if (idx >= 0) {
125
+ for (let i = idx + 1; i < args.length; i += 1) {
126
+ const arg = args[i];
127
+ if (arg === '--last') return null;
128
+ if (arg.startsWith('-')) continue;
129
+ return arg;
130
+ }
131
+ }
132
+ }
133
+ return null;
134
+ }
135
+
136
+ function peerObservation(kind = null) {
137
+ const ancestor = readAncestorCliInfo();
138
+ const ancestorEnv = ancestor?.kind === kind ? ancestor.env || {} : {};
139
+ const sessionId = kind === 'claude'
140
+ ? (process.env.CLAUDE_CODE_SESSION_ID || ancestorEnv.CLAUDE_CODE_SESSION_ID || '')
141
+ : kind === 'codex'
142
+ ? (process.env.CODEX_SESSION_ID || process.env.CODEX_THREAD_ID || ancestorEnv.CODEX_SESSION_ID || ancestorEnv.CODEX_THREAD_ID || '')
143
+ : '';
144
+ const resumeId = ancestor?.kind === kind ? resumeIdFromArgs(kind, ancestor.args) : null;
145
+ return {
146
+ ancestor,
147
+ env: process.env,
148
+ ppid: process.ppid,
149
+ resumeId,
150
+ sessionId,
151
+ tmuxPane: process.env.TMUX_PANE || '',
152
+ tty: currentTtyName()
153
+ };
154
+ }
155
+
156
+ export function autoPeerProviderSession(kind) {
157
+ return coreAutoPeerProviderSession(kind, peerObservation(kind));
158
+ }
159
+
160
+ export function autoPeerSessionId(kind) {
161
+ return coreAutoPeerSessionId(kind, peerObservation(kind));
162
+ }
163
+
164
+ export function autoPeerResumeId(kind) {
165
+ return coreAutoPeerResumeId(kind, peerObservation(kind));
166
+ }
167
+
168
+ export function autoPeerKind(kindHint = 'shell') {
169
+ const ancestor = readAncestorCliInfo();
170
+ let kind = '';
171
+ if (process.env.CLAUDE_CODE_SESSION_ID || process.env.CLAUDECODE === '1') kind = 'claude';
172
+ else if (
173
+ process.env.CODEX_SESSION_ID ||
174
+ process.env.CODEX_THREAD_ID ||
175
+ process.env.CODEX_MANAGED_BY_NPM === '1' ||
176
+ process.env.CODEX_MANAGED_BY_BUN === '1'
177
+ ) kind = 'codex';
178
+ else kind = ancestor?.kind || '';
179
+ return coreAutoPeerKind(kindHint, { kind });
180
+ }
181
+
182
+ export function autoPeerBasis(kind = null) {
183
+ return coreAutoPeerBasis(kind, peerObservation(kind));
184
+ }
185
+
186
+ export function autoPeerId(ctx, kindHint = 'shell') {
187
+ const kind = autoPeerKind(kindHint);
188
+ return coreAutoPeerId(ctx, kindHint, {
189
+ ...peerObservation(kind),
190
+ kind
191
+ });
192
+ }
193
+
194
+ export function resolveCurrentPeer(ctx, opts = {}, key = 'peer', kindHint = 'shell') {
195
+ const kind = autoPeerKind(kindHint);
196
+ return coreResolveCurrentPeer(ctx, opts, key, kindHint, {
197
+ ...peerObservation(kind),
198
+ kind
199
+ });
200
+ }
201
+
202
+ export function currentPeer(ctx, opts = {}, key = 'peer', kindHint = 'shell') {
203
+ const kind = autoPeerKind(kindHint);
204
+ return coreCurrentPeer(ctx, opts, key, kindHint, {
205
+ ...peerObservation(kind),
206
+ kind
207
+ });
208
+ }
@@ -0,0 +1,151 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { resumeIdFromArgs } from './identity.mjs';
4
+
5
+ function readProcCmdline(pid) {
6
+ if (process.platform !== 'linux' || !pid) return [];
7
+ try {
8
+ return fs.readFileSync(`/proc/${pid}/cmdline`, 'utf8').split('\0').filter(Boolean);
9
+ } catch {
10
+ return [];
11
+ }
12
+ }
13
+
14
+ function readProcEnv(pid) {
15
+ const env = {};
16
+ if (process.platform !== 'linux' || !pid) return env;
17
+ try {
18
+ for (const entry of fs.readFileSync(`/proc/${pid}/environ`, 'utf8').split('\0')) {
19
+ const eq = entry.indexOf('=');
20
+ if (eq > 0) env[entry.slice(0, eq)] = entry.slice(eq + 1);
21
+ }
22
+ } catch {}
23
+ return env;
24
+ }
25
+
26
+ function readProcParentPid(pid) {
27
+ if (process.platform !== 'linux' || !pid) return null;
28
+ try {
29
+ const status = fs.readFileSync(`/proc/${pid}/status`, 'utf8');
30
+ const match = status.match(/^PPid:\s+(\d+)/m);
31
+ return match ? Number.parseInt(match[1], 10) : null;
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+
37
+ function readProcChildren(pid) {
38
+ if (process.platform !== 'linux' || !pid) return [];
39
+ try {
40
+ return fs.readFileSync(`/proc/${pid}/task/${pid}/children`, 'utf8')
41
+ .trim()
42
+ .split(/\s+/)
43
+ .filter(Boolean)
44
+ .map((value) => Number.parseInt(value, 10))
45
+ .filter((value) => Number.isFinite(value) && value > 1);
46
+ } catch {
47
+ return [];
48
+ }
49
+ }
50
+
51
+ function argsLookLikeCli(args, kind) {
52
+ return args.some((arg) => {
53
+ const text = String(arg || '').toLowerCase();
54
+ const base = path.basename(text);
55
+ if (kind === 'codex') return base === 'codex' || text.includes('/codex') || text.includes('@openai/codex');
56
+ if (kind === 'claude') return base === 'claude' || base === 'claude-code' || text.includes('/claude') || text.includes('@anthropic-ai/claude-code');
57
+ return false;
58
+ });
59
+ }
60
+
61
+ function detectKind(args, env, kindHint = null) {
62
+ if (kindHint && argsLookLikeCli(args, kindHint)) return kindHint;
63
+ if (env.CLAUDE_CODE_SESSION_ID || env.CLAUDECODE === '1' || argsLookLikeCli(args, 'claude')) return 'claude';
64
+ if (
65
+ env.CODEX_SESSION_ID ||
66
+ env.CODEX_THREAD_ID ||
67
+ env.CODEX_MANAGED_BY_NPM === '1' ||
68
+ env.CODEX_MANAGED_BY_BUN === '1' ||
69
+ argsLookLikeCli(args, 'codex')
70
+ ) return 'codex';
71
+ return null;
72
+ }
73
+
74
+ function sessionFromEnv(kind, env) {
75
+ if (kind === 'claude') return env.CLAUDE_CODE_SESSION_ID || '';
76
+ if (kind === 'codex') return env.CODEX_SESSION_ID || env.CODEX_THREAD_ID || '';
77
+ return '';
78
+ }
79
+
80
+ export function inspectProviderProcess(pid, kindHint = null, opts = {}) {
81
+ if (process.platform !== 'linux' || !pid) return null;
82
+ const maxDepth = Number.isFinite(opts.maxDepth) ? opts.maxDepth : 8;
83
+ const queue = [{ pid: Number(pid), depth: 0 }];
84
+ const seen = new Set();
85
+
86
+ while (queue.length) {
87
+ const current = queue.shift();
88
+ const currentPid = current?.pid;
89
+ const depth = current?.depth || 0;
90
+ if (!currentPid || currentPid <= 1 || depth > maxDepth || seen.has(currentPid)) continue;
91
+ seen.add(currentPid);
92
+ const args = readProcCmdline(currentPid);
93
+ const env = readProcEnv(currentPid);
94
+ const kind = detectKind(args, env, kindHint);
95
+ if (kind) {
96
+ const envSession = sessionFromEnv(kind, env);
97
+ if (envSession) {
98
+ return {
99
+ pid: currentPid,
100
+ kind,
101
+ provider_session: envSession,
102
+ source: `process.env.${kind}`
103
+ };
104
+ }
105
+ const resumeId = resumeIdFromArgs(kind, args);
106
+ if (resumeId) {
107
+ return {
108
+ pid: currentPid,
109
+ kind,
110
+ provider_session: resumeId,
111
+ source: `process.argv.${kind}`
112
+ };
113
+ }
114
+ }
115
+ for (const childPid of readProcChildren(currentPid)) {
116
+ queue.push({ pid: childPid, depth: depth + 1 });
117
+ }
118
+ }
119
+
120
+ let currentPid = Number(pid);
121
+ seen.clear();
122
+
123
+ for (let depth = 0; currentPid && currentPid > 1 && depth <= maxDepth && !seen.has(currentPid); depth += 1) {
124
+ seen.add(currentPid);
125
+ const args = readProcCmdline(currentPid);
126
+ const env = readProcEnv(currentPid);
127
+ const kind = detectKind(args, env, kindHint);
128
+ if (kind) {
129
+ const envSession = sessionFromEnv(kind, env);
130
+ if (envSession) {
131
+ return {
132
+ pid: currentPid,
133
+ kind,
134
+ provider_session: envSession,
135
+ source: `process.env.${kind}`
136
+ };
137
+ }
138
+ const resumeId = resumeIdFromArgs(kind, args);
139
+ if (resumeId) {
140
+ return {
141
+ pid: currentPid,
142
+ kind,
143
+ provider_session: resumeId,
144
+ source: `process.argv.${kind}`
145
+ };
146
+ }
147
+ }
148
+ currentPid = readProcParentPid(currentPid);
149
+ }
150
+ return null;
151
+ }