@logicseek/hello-cc 0.1.5 → 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 (85) hide show
  1. package/CHANGELOG.md +75 -0
  2. package/bin/hcc.mjs +1541 -3430
  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/layout-split-stack-audit.md +298 -0
  9. package/lib/automation.mjs +8 -0
  10. package/lib/cli-args.mjs +1 -1
  11. package/lib/cli-runtime.mjs +44 -0
  12. package/lib/coordination-state.mjs +218 -0
  13. package/lib/core/coordination/automation.mjs +222 -0
  14. package/lib/core/coordination/locks.mjs +46 -0
  15. package/lib/core/coordination/messages.mjs +116 -0
  16. package/lib/core/coordination/tasks.mjs +224 -0
  17. package/lib/core/coordination/teams.mjs +73 -0
  18. package/lib/core/coordination/timeline.mjs +187 -0
  19. package/lib/core/peers/bindings.mjs +83 -0
  20. package/lib/core/peers/format.mjs +13 -0
  21. package/lib/core/peers/identity.mjs +53 -0
  22. package/lib/core/peers/liveness.mjs +90 -0
  23. package/lib/core/peers/reconcile.mjs +238 -0
  24. package/lib/core/peers/session.mjs +14 -0
  25. package/lib/core/sessions/launch.mjs +89 -0
  26. package/lib/db/schema.mjs +324 -0
  27. package/lib/db/stores/peers.mjs +231 -0
  28. package/lib/db-schema.mjs +7 -324
  29. package/lib/errors.mjs +1 -7
  30. package/lib/handoff.mjs +26 -0
  31. package/lib/help.mjs +3 -0
  32. package/lib/integrations/hooks.mjs +171 -0
  33. package/lib/integrations/peers/identity.mjs +208 -0
  34. package/lib/integrations/peers/processes.mjs +151 -0
  35. package/lib/integrations/providers.mjs +282 -0
  36. package/lib/integrations/shims/script.mjs +384 -0
  37. package/lib/integrations/shims.mjs +106 -0
  38. package/lib/json-file.mjs +1 -14
  39. package/lib/locks.mjs +10 -0
  40. package/lib/messages.mjs +3 -0
  41. package/lib/package-meta.mjs +6 -24
  42. package/lib/peer-bindings.mjs +14 -0
  43. package/lib/peer-format.mjs +4 -0
  44. package/lib/peer-identity.mjs +15 -0
  45. package/lib/project-context.mjs +6 -0
  46. package/lib/project-registry.mjs +7 -0
  47. package/lib/provider-commands.mjs +13 -0
  48. package/lib/release/package-meta.mjs +31 -0
  49. package/lib/release/release-notes.mjs +40 -0
  50. package/lib/release-notes.mjs +6 -40
  51. package/lib/runtime/client.mjs +42 -0
  52. package/lib/runtime/paths.mjs +45 -0
  53. package/lib/runtime/project-context.mjs +32 -0
  54. package/lib/runtime/projects.mjs +76 -0
  55. package/lib/runtime/state.mjs +128 -0
  56. package/lib/runtime-client.mjs +3 -0
  57. package/lib/runtime-paths.mjs +11 -45
  58. package/lib/runtime-state.mjs +11 -0
  59. package/lib/session-launch.mjs +17 -0
  60. package/lib/setup.mjs +17 -293
  61. package/lib/shared/errors.mjs +7 -0
  62. package/lib/shared/json-file.mjs +14 -0
  63. package/lib/shim-script.mjs +1 -384
  64. package/lib/state-render.mjs +5 -0
  65. package/lib/task-cli.mjs +45 -0
  66. package/lib/task-liveness.mjs +8 -0
  67. package/lib/task-store.mjs +3 -0
  68. package/lib/team-planning.mjs +7 -0
  69. package/lib/terminal/tmux.mjs +372 -0
  70. package/lib/timeline.mjs +12 -0
  71. package/lib/tmux.mjs +18 -0
  72. package/lib/ui/help.mjs +419 -0
  73. package/lib/ui/state-render.mjs +61 -0
  74. package/lib/web/http.mjs +55 -0
  75. package/lib/web/peer-actions.mjs +270 -0
  76. package/lib/web/runtime.mjs +154 -0
  77. package/lib/web/ui-template.mjs +2055 -0
  78. package/lib/web-http.mjs +7 -55
  79. package/lib/web-peer-actions.mjs +3 -0
  80. package/lib/web-runtime.mjs +17 -108
  81. package/lib/web-ui-template.mjs +3 -1909
  82. package/package.json +1 -1
  83. package/scripts/github-release.mjs +2 -2
  84. package/scripts/regression.mjs +2468 -17
  85. package/scripts/release-notes.mjs +2 -2
@@ -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
+ }
@@ -0,0 +1,282 @@
1
+ import { CliError } from '../shared/errors.mjs';
2
+ import { shellQuoteArg } from '../format.mjs';
3
+ import { providerSessionParts } from '../core/peers/session.mjs';
4
+
5
+ export {
6
+ providerSessionParts,
7
+ providerSessionPeerId
8
+ } from '../core/peers/session.mjs';
9
+
10
+ function shellCommand(args) {
11
+ return args.map(shellQuoteArg).join(' ');
12
+ }
13
+
14
+ export function inferPeerKind(id, explicitKind, firstCommand) {
15
+ if (explicitKind) return explicitKind;
16
+ if (['codex', 'claude'].includes(firstCommand)) return firstCommand;
17
+ if (String(id).startsWith('codex')) return 'codex';
18
+ if (String(id).startsWith('claude')) return 'claude';
19
+ return 'shell';
20
+ }
21
+
22
+ export function hasResumeOpts(opts) {
23
+ return opts.resume !== undefined || Boolean(opts.last) || Boolean(opts.continue) || Boolean(opts.fork) || opts.session !== undefined;
24
+ }
25
+
26
+ export function defaultSessionCommand(kind) {
27
+ if (kind === 'codex') return 'codex';
28
+ if (kind === 'claude') return 'claude';
29
+ return process.env.SHELL || 'bash';
30
+ }
31
+
32
+ export function buildPeerCommand(id, kind, opts, cmdArgs) {
33
+ const explicitCommand = opts.command || cmdArgs.length > 0;
34
+ if (explicitCommand && hasResumeOpts(opts)) {
35
+ throw new CliError('BAD_ARGS', 'Use resume options without an explicit -- COMMAND');
36
+ }
37
+ if (explicitCommand) {
38
+ const command = opts.command || shellCommand(cmdArgs);
39
+ return {
40
+ command,
41
+ binding: {
42
+ peer: id,
43
+ provider: kind,
44
+ resume_mode: 'command',
45
+ resume_arg: null,
46
+ command
47
+ }
48
+ };
49
+ }
50
+ if (kind === 'codex') return buildCodexCommand(id, opts);
51
+ if (kind === 'claude') return buildClaudeCommand(id, opts);
52
+ if (hasResumeOpts(opts)) throw new CliError('BAD_ARGS', `Resume options are only supported for codex and claude peers`);
53
+ const command = defaultSessionCommand(kind);
54
+ return {
55
+ command,
56
+ binding: {
57
+ peer: id,
58
+ provider: kind,
59
+ resume_mode: 'new',
60
+ resume_arg: null,
61
+ command
62
+ }
63
+ };
64
+ }
65
+
66
+ export function buildCodexCommand(id, opts) {
67
+ let command;
68
+ let resumeMode = 'new';
69
+ let resumeArg = null;
70
+ let session = { provider_session_id: null, provider_session_name: null };
71
+ if (opts.fork) {
72
+ resumeMode = opts.last ? 'fork-last' : 'fork';
73
+ if (opts.last) {
74
+ command = 'codex fork --last';
75
+ resumeArg = '--last';
76
+ } else if (opts.resume) {
77
+ command = `codex fork ${shellQuoteArg(opts.resume)}`;
78
+ resumeArg = opts.resume;
79
+ } else {
80
+ command = 'codex fork';
81
+ }
82
+ } else if (opts.last) {
83
+ command = 'codex resume --last';
84
+ resumeMode = 'last';
85
+ resumeArg = '--last';
86
+ } else if (opts.resume) {
87
+ command = `codex resume ${shellQuoteArg(opts.resume)}`;
88
+ resumeMode = 'resume';
89
+ resumeArg = opts.resume;
90
+ session = providerSessionParts(opts.resume);
91
+ } else {
92
+ command = 'codex';
93
+ }
94
+ return {
95
+ command,
96
+ binding: {
97
+ peer: id,
98
+ provider: 'codex',
99
+ ...session,
100
+ resume_mode: resumeMode,
101
+ resume_arg: resumeArg,
102
+ command
103
+ }
104
+ };
105
+ }
106
+
107
+ export function buildClaudeCommand(id, opts) {
108
+ let command = 'claude';
109
+ let resumeMode = 'new';
110
+ let resumeArg = null;
111
+ let session = { provider_session_id: null, provider_session_name: null };
112
+ if (opts.continue) {
113
+ command += ' --continue';
114
+ resumeMode = opts.fork ? 'fork-continue' : 'continue';
115
+ resumeArg = '--continue';
116
+ } else if (opts.resume) {
117
+ command += ` --resume ${shellQuoteArg(opts.resume)}`;
118
+ resumeMode = opts.fork ? 'fork-resume' : 'resume';
119
+ resumeArg = opts.resume;
120
+ if (!opts.fork) session = providerSessionParts(opts.resume);
121
+ } else if (opts.session) {
122
+ command += ` --session-id ${shellQuoteArg(opts.session)}`;
123
+ resumeMode = 'session';
124
+ resumeArg = opts.session;
125
+ session = { provider_session_id: opts.session, provider_session_name: null };
126
+ }
127
+ if (opts.fork && (opts.continue || opts.resume)) command += ' --fork-session';
128
+ if (opts.name) command += ` --name ${shellQuoteArg(opts.name)}`;
129
+ return {
130
+ command,
131
+ binding: {
132
+ peer: id,
133
+ provider: 'claude',
134
+ ...session,
135
+ resume_mode: resumeMode,
136
+ resume_arg: resumeArg,
137
+ command
138
+ }
139
+ };
140
+ }
141
+
142
+ export function bindingFromRun(id, kind, command, commandArgs, transport) {
143
+ const cmdline = [command, ...commandArgs];
144
+ const provider = kind || 'other';
145
+ let resumeMode = 'command';
146
+ let resumeArg = null;
147
+ let session = { provider_session_id: null, provider_session_name: null };
148
+ if (provider === 'codex') {
149
+ const parsed = parseCodexCommandArgs(cmdline);
150
+ resumeMode = parsed.resume_mode;
151
+ resumeArg = parsed.resume_arg;
152
+ session = parsed.session;
153
+ } else if (provider === 'claude') {
154
+ const parsed = parseClaudeCommandArgs(cmdline);
155
+ resumeMode = parsed.resume_mode;
156
+ resumeArg = parsed.resume_arg;
157
+ session = parsed.session;
158
+ }
159
+ return {
160
+ peer: id,
161
+ provider,
162
+ ...session,
163
+ resume_mode: resumeMode,
164
+ resume_arg: resumeArg,
165
+ command: cmdline.join(' '),
166
+ transport,
167
+ runtime_session_id: id
168
+ };
169
+ }
170
+
171
+ function optionValue(args, names) {
172
+ const nameSet = new Set(names);
173
+ for (let i = 0; i < args.length; i += 1) {
174
+ const arg = args[i];
175
+ if (nameSet.has(arg) && args[i + 1]) return args[i + 1];
176
+ for (const name of names) {
177
+ if (name.startsWith('--') && arg.startsWith(`${name}=`)) return arg.slice(name.length + 1);
178
+ }
179
+ }
180
+ return null;
181
+ }
182
+
183
+ function hasFlag(args, names) {
184
+ const nameSet = new Set(names);
185
+ return args.some((arg) => nameSet.has(arg));
186
+ }
187
+
188
+ export function parseClaudeCommandArgs(args) {
189
+ const resumeId = optionValue(args, ['--resume', '-r']);
190
+ const sessionId = optionValue(args, ['--session-id']);
191
+ const name = optionValue(args, ['--name', '-n']);
192
+ const continuing = hasFlag(args, ['--continue', '-c']);
193
+ const fork = hasFlag(args, ['--fork-session']);
194
+ let resumeMode = 'command';
195
+ let resumeArg = null;
196
+ let session = { provider_session_id: null, provider_session_name: null };
197
+
198
+ if (sessionId) {
199
+ resumeMode = 'session';
200
+ resumeArg = sessionId;
201
+ session = { provider_session_id: sessionId, provider_session_name: null };
202
+ } else if (resumeId) {
203
+ resumeMode = fork ? 'fork-resume' : 'resume';
204
+ resumeArg = resumeId;
205
+ if (!fork) session = providerSessionParts(resumeId);
206
+ } else if (continuing) {
207
+ resumeMode = fork ? 'fork-continue' : 'continue';
208
+ resumeArg = '--continue';
209
+ } else if (fork) {
210
+ resumeMode = 'fork';
211
+ } else if (name) {
212
+ resumeMode = 'named';
213
+ resumeArg = name;
214
+ }
215
+
216
+ return { resume_mode: resumeMode, resume_arg: resumeArg, session };
217
+ }
218
+
219
+ function codexOptionTakesValue(arg) {
220
+ if (!arg || arg.includes('=')) return false;
221
+ return new Set([
222
+ '-c', '--config',
223
+ '--remote',
224
+ '--remote-auth-token-env',
225
+ '--enable',
226
+ '--disable',
227
+ '-i', '--image',
228
+ '-m', '--model',
229
+ '--local-provider',
230
+ '-p', '--profile',
231
+ '-s', '--sandbox',
232
+ '-C', '--cd',
233
+ '--add-dir',
234
+ '-a', '--ask-for-approval'
235
+ ]).has(arg);
236
+ }
237
+
238
+ function firstCodexSessionArg(args, startIndex) {
239
+ for (let i = startIndex; i < args.length; i += 1) {
240
+ const arg = args[i];
241
+ if (arg === '--last') continue;
242
+ if (arg.startsWith('-')) {
243
+ if (codexOptionTakesValue(arg)) i += 1;
244
+ continue;
245
+ }
246
+ return arg;
247
+ }
248
+ return null;
249
+ }
250
+
251
+ export function parseCodexCommandArgs(args) {
252
+ const subIndex = args.findIndex((arg) => arg === 'resume' || arg === 'fork');
253
+ if (subIndex < 0) {
254
+ return {
255
+ resume_mode: 'command',
256
+ resume_arg: null,
257
+ session: { provider_session_id: null, provider_session_name: null }
258
+ };
259
+ }
260
+
261
+ const subcommand = args[subIndex];
262
+ const last = args.slice(subIndex + 1).includes('--last');
263
+ const sessionArg = firstCodexSessionArg(args, subIndex + 1);
264
+ let resumeMode = subcommand;
265
+ let resumeArg = sessionArg || null;
266
+ let session = { provider_session_id: null, provider_session_name: null };
267
+
268
+ if (subcommand === 'resume') {
269
+ if (last && !sessionArg) {
270
+ resumeMode = 'last';
271
+ resumeArg = '--last';
272
+ } else if (sessionArg) {
273
+ resumeMode = 'resume';
274
+ session = providerSessionParts(sessionArg);
275
+ }
276
+ } else if (subcommand === 'fork') {
277
+ resumeMode = last && !sessionArg ? 'fork-last' : 'fork';
278
+ resumeArg = sessionArg || (last ? '--last' : null);
279
+ }
280
+
281
+ return { resume_mode: resumeMode, resume_arg: resumeArg, session };
282
+ }