@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.
- package/CHANGELOG.md +126 -0
- package/bin/hcc.mjs +2739 -6482
- package/docs/README.md +2 -0
- package/docs/README.zh-CN.md +3 -1
- package/docs/architecture-split-review.md +217 -0
- package/docs/architecture.md +385 -0
- package/docs/architecture.zh-CN.md +364 -0
- package/docs/commands.md +21 -9
- package/docs/commands.zh-CN.md +19 -9
- package/docs/layout-split-stack-audit.md +298 -0
- package/lib/automation.mjs +8 -0
- package/lib/cli-args.mjs +89 -0
- package/lib/cli-runtime.mjs +44 -0
- package/lib/coordination-state.mjs +218 -0
- package/lib/core/coordination/automation.mjs +222 -0
- package/lib/core/coordination/locks.mjs +46 -0
- package/lib/core/coordination/messages.mjs +116 -0
- package/lib/core/coordination/tasks.mjs +224 -0
- package/lib/core/coordination/teams.mjs +73 -0
- package/lib/core/coordination/timeline.mjs +187 -0
- package/lib/core/peers/bindings.mjs +83 -0
- package/lib/core/peers/format.mjs +13 -0
- package/lib/core/peers/identity.mjs +53 -0
- package/lib/core/peers/liveness.mjs +90 -0
- package/lib/core/peers/reconcile.mjs +238 -0
- package/lib/core/peers/session.mjs +14 -0
- package/lib/core/sessions/launch.mjs +89 -0
- package/lib/db/schema.mjs +324 -0
- package/lib/db/stores/peers.mjs +231 -0
- package/lib/db-schema.mjs +7 -0
- package/lib/errors.mjs +1 -0
- package/lib/format.mjs +39 -0
- package/lib/guidance.mjs +10 -2
- package/lib/handoff.mjs +26 -0
- package/lib/help.mjs +3 -0
- package/lib/integrations/hooks.mjs +171 -0
- package/lib/integrations/peers/identity.mjs +208 -0
- package/lib/integrations/peers/processes.mjs +151 -0
- package/lib/integrations/providers.mjs +282 -0
- package/lib/integrations/shims/script.mjs +384 -0
- package/lib/integrations/shims.mjs +106 -0
- package/lib/json-file.mjs +1 -0
- package/lib/locks.mjs +10 -0
- package/lib/messages.mjs +3 -0
- package/lib/package-meta.mjs +6 -24
- package/lib/peer-bindings.mjs +14 -0
- package/lib/peer-format.mjs +4 -0
- package/lib/peer-identity.mjs +15 -0
- package/lib/project-context.mjs +6 -0
- package/lib/project-registry.mjs +7 -0
- package/lib/provider-commands.mjs +13 -0
- package/lib/release/package-meta.mjs +31 -0
- package/lib/release/release-notes.mjs +40 -0
- package/lib/release-notes.mjs +6 -40
- package/lib/runtime/client.mjs +42 -0
- package/lib/runtime/paths.mjs +45 -0
- package/lib/runtime/project-context.mjs +32 -0
- package/lib/runtime/projects.mjs +76 -0
- package/lib/runtime/state.mjs +128 -0
- package/lib/runtime-client.mjs +3 -0
- package/lib/runtime-paths.mjs +11 -0
- package/lib/runtime-state.mjs +11 -0
- package/lib/session-launch.mjs +17 -0
- package/lib/setup.mjs +18 -765
- package/lib/shared/errors.mjs +7 -0
- package/lib/shared/json-file.mjs +14 -0
- package/lib/shell-path.mjs +69 -0
- package/lib/shim-script.mjs +1 -0
- package/lib/state-render.mjs +5 -0
- package/lib/task-cli.mjs +45 -0
- package/lib/task-liveness.mjs +8 -0
- package/lib/task-store.mjs +3 -0
- package/lib/team-planning.mjs +7 -0
- package/lib/terminal/tmux.mjs +372 -0
- package/lib/timeline.mjs +12 -0
- package/lib/tmux.mjs +18 -0
- package/lib/ui/help.mjs +419 -0
- package/lib/ui/state-render.mjs +61 -0
- package/lib/web/http.mjs +55 -0
- package/lib/web/peer-actions.mjs +270 -0
- package/lib/web/runtime.mjs +154 -0
- package/lib/web/ui-template.mjs +2055 -0
- package/lib/web-http.mjs +7 -0
- package/lib/web-peer-actions.mjs +3 -0
- package/lib/web-runtime.mjs +17 -0
- package/lib/web-ui-template.mjs +3 -0
- package/package.json +1 -1
- package/scripts/github-release.mjs +2 -2
- package/scripts/regression.mjs +2738 -22
- 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
|
|
57
|
-
|
|
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
|
package/lib/handoff.mjs
ADDED
|
@@ -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,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
|
+
}
|