@logicseek/hello-cc 1.0.2 → 1.0.4

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 (40) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/docs/dsh.md +3 -3
  3. package/docs/dsh.zh-CN.md +3 -3
  4. package/lib/cli/app.mjs +1 -1
  5. package/lib/cli/commands/hook.mjs +3 -1
  6. package/lib/cli/commands/lifecycle.mjs +14 -3
  7. package/lib/cli/commands/query.mjs +15 -10
  8. package/lib/cli/commands/run.mjs +7 -6
  9. package/lib/db/connection.mjs +82 -9
  10. package/lib/discover.mjs +4 -4
  11. package/lib/integrations/hooks.mjs +132 -40
  12. package/lib/integrations/peers/identity.mjs +42 -7
  13. package/lib/process/identity.mjs +36 -15
  14. package/lib/runtime/client.mjs +9 -1
  15. package/lib/runtime/native/service.mjs +107 -24
  16. package/lib/runtime/native/store.mjs +63 -6
  17. package/lib/runtime/project-path.mjs +90 -7
  18. package/lib/shared/file-lock.mjs +62 -17
  19. package/lib/shared/socket-lock-worker.mjs +15 -6
  20. package/lib/web/codex-app-server.mjs +19 -1
  21. package/lib/web/codex-sessions.mjs +2 -0
  22. package/lib/web/cookie-auth.mjs +1 -1
  23. package/lib/web/http-routes.mjs +11 -7
  24. package/lib/web/http.mjs +20 -2
  25. package/lib/web/native-sessions.mjs +13 -6
  26. package/lib/web/peer-actions.mjs +27 -27
  27. package/lib/web/runtime-main.mjs +24 -7
  28. package/lib/web/runtime.mjs +18 -4
  29. package/lib/web/session-serialize.mjs +1 -1
  30. package/lib/web/startup.mjs +84 -10
  31. package/lib/web/task-results.mjs +8 -5
  32. package/lib/web/terminal-assets.mjs +18 -0
  33. package/lib/web/tmux-client-snapshot.mjs +85 -0
  34. package/lib/web/tmux-stream.mjs +3 -4
  35. package/lib/web/ui-codex.mjs +137 -20
  36. package/lib/web/ui-interactions.mjs +37 -3
  37. package/lib/web/ui-native.mjs +21 -9
  38. package/lib/web/ui-template.mjs +8 -2
  39. package/package.json +1 -1
  40. package/scripts/regression.mjs +212 -47
@@ -1,6 +1,10 @@
1
+ import fs from 'node:fs';
1
2
  import os from 'node:os';
2
3
  import path from 'node:path';
3
- import { readJsonSafe, writeJsonSafe } from '../shared/json-file.mjs';
4
+ import { commandPath } from '../cli-runtime.mjs';
5
+ import { shellQuoteArg } from '../format.mjs';
6
+ import { CliError } from '../shared/errors.mjs';
7
+ import { writeJsonSafe } from '../shared/json-file.mjs';
4
8
 
5
9
  const CLAUDE_SETTINGS_PATH = path.join(os.homedir(), '.claude', 'settings.json');
6
10
  const CODEX_HOOKS_PATH = path.join(os.homedir(), '.codex', 'hooks.json');
@@ -18,15 +22,15 @@ const CODEX_HOOK_EVENTS = ['SessionStart', 'UserPromptSubmit', 'PreToolUse', 'St
18
22
 
19
23
  /**
20
24
  * Install (or update) hello-cc hooks in ~/.claude/settings.json.
21
- * Non-destructive: only adds hcc entries, never removes existing ones.
25
+ * Replaces stale hcc entries while retaining unrelated hooks.
22
26
  * Returns the settings file path.
23
27
  */
24
28
  export function installClaudeHooks(hccBin) {
25
- const settings = readJsonSafe(CLAUDE_SETTINGS_PATH) || {};
29
+ const settings = readHookConfig(CLAUDE_SETTINGS_PATH) || {};
26
30
 
27
31
  if (!settings.hooks) settings.hooks = {};
28
32
 
29
- const hookCmd = `${hccBin} hook`;
33
+ const hookCmd = `${shellQuoteArg(hccBin)} hook`;
30
34
 
31
35
  for (const event of CLAUDE_HOOK_EVENTS) {
32
36
  settings.hooks[event] = mergeHookEntry(
@@ -43,29 +47,31 @@ export function installClaudeHooks(hccBin) {
43
47
  * Remove hello-cc hook entries from ~/.claude/settings.json.
44
48
  */
45
49
  export function uninstallClaudeHooks() {
46
- const settings = readJsonSafe(CLAUDE_SETTINGS_PATH);
50
+ const settings = readHookConfig(CLAUDE_SETTINGS_PATH);
47
51
  if (!settings?.hooks) return false;
48
52
 
53
+ let removed = false;
49
54
  for (const event of Object.keys(settings.hooks)) {
50
55
  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
- });
56
+ const result = removeHccHooks(settings.hooks[event]);
57
+ settings.hooks[event] = result.entries;
58
+ removed ||= result.removed;
55
59
  if (settings.hooks[event].length === 0) delete settings.hooks[event];
56
60
  }
57
61
 
58
- writeJsonSafe(CLAUDE_SETTINGS_PATH, settings);
59
- return true;
62
+ if (removed) writeJsonSafe(CLAUDE_SETTINGS_PATH, settings);
63
+ return removed;
60
64
  }
61
65
 
62
66
  /**
63
67
  * Returns true if hello-cc hooks are present in ~/.claude/settings.json.
64
68
  */
65
- export function verifyClaudeHooks() {
66
- const settings = readJsonSafe(CLAUDE_SETTINGS_PATH);
69
+ export function verifyClaudeHooks(hccBin = commandPath()) {
70
+ const settings = readHookConfig(CLAUDE_SETTINGS_PATH);
67
71
  if (!settings?.hooks) return false;
68
- return CLAUDE_HOOK_EVENTS.every(event => hasHookEntry(settings.hooks[event]));
72
+ const hookCmd = `${shellQuoteArg(hccBin)} hook`;
73
+ return CLAUDE_HOOK_EVENTS.every(event =>
74
+ hasHookEntry(settings.hooks[event], `${hookCmd} ${event.toLowerCase()}`));
69
75
  }
70
76
 
71
77
  /**
@@ -75,11 +81,11 @@ export function verifyClaudeHooks() {
75
81
  * { "hooks": { "PreToolUse": [{ "matcher": "Bash", "hooks": [...] }], "Stop": [...] } }
76
82
  */
77
83
  export function installCodexHooks(hccBin) {
78
- const hooks = readJsonSafe(CODEX_HOOKS_PATH) || {};
84
+ const hooks = readHookConfig(CODEX_HOOKS_PATH) || {};
79
85
 
80
86
  if (!hooks.hooks) hooks.hooks = {};
81
87
 
82
- const hookCmd = `${hccBin} hook`;
88
+ const hookCmd = `${shellQuoteArg(hccBin)} hook`;
83
89
 
84
90
  // SessionStart: inject initial coordination context on startup/resume.
85
91
  hooks.hooks.SessionStart = mergeCodexHookEntry(
@@ -111,56 +117,61 @@ export function installCodexHooks(hccBin) {
111
117
  }
112
118
 
113
119
  export function uninstallCodexHooks() {
114
- const hooks = readJsonSafe(CODEX_HOOKS_PATH);
120
+ const hooks = readHookConfig(CODEX_HOOKS_PATH);
115
121
  if (!hooks?.hooks) return false;
116
122
 
123
+ let removed = false;
117
124
  for (const event of Object.keys(hooks.hooks)) {
118
125
  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
- });
126
+ const result = removeHccHooks(hooks.hooks[event]);
127
+ hooks.hooks[event] = result.entries;
128
+ removed ||= result.removed;
123
129
  if (hooks.hooks[event].length === 0) delete hooks.hooks[event];
124
130
  }
125
131
 
126
- writeJsonSafe(CODEX_HOOKS_PATH, hooks);
127
- return true;
132
+ if (removed) writeJsonSafe(CODEX_HOOKS_PATH, hooks);
133
+ return removed;
128
134
  }
129
135
 
130
- export function verifyCodexHooks() {
131
- const hooks = readJsonSafe(CODEX_HOOKS_PATH);
136
+ export function verifyCodexHooks(hccBin = commandPath()) {
137
+ const hooks = readHookConfig(CODEX_HOOKS_PATH);
132
138
  if (!hooks?.hooks) return false;
133
- return CODEX_HOOK_EVENTS.every(event => hasHookEntry(hooks.hooks[event]));
139
+ const hookCmd = `${shellQuoteArg(hccBin)} hook`;
140
+ return CODEX_HOOK_EVENTS.every(event =>
141
+ hasHookEntry(hooks.hooks[event], `${hookCmd} ${event.toLowerCase()}`,
142
+ event === 'PreToolUse' ? 'Bash' : undefined));
134
143
  }
135
144
 
136
145
  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;
146
+ const entries = removeHccHooks(hookEntries(existing)).entries;
142
147
  entries.push({ hooks: [{ type: 'command', command }] });
143
148
  return entries;
144
149
  }
145
150
 
146
- function hasHookEntry(entries) {
151
+ function hasHookEntry(entries, command, matcher) {
147
152
  if (!Array.isArray(entries)) return false;
148
153
  return entries.some(e =>
149
- Array.isArray(e?.hooks) && e.hooks.some(h => isHccHookCmd(h?.command))
154
+ (matcher === undefined || e?.matcher === matcher) &&
155
+ Array.isArray(e?.hooks) && e.hooks.some(h => h?.type === 'command' && h.command === command)
150
156
  );
151
157
  }
152
158
 
153
159
  function isHccHookCmd(cmd) {
154
- if (typeof cmd !== 'string') return false;
155
- return /\bhcc\b.*\bhook\b/.test(cmd) || /hello-cc.*hook/.test(cmd);
160
+ const words = shellWords(cmd);
161
+ if (words?.length) {
162
+ const offset = ['node', 'nodejs', 'node.exe'].includes(path.basename(words[0])) ? 1 : 0;
163
+ if (['hcc', 'hello-cc', 'hcc.mjs'].includes(path.basename(words[offset] || '')) &&
164
+ words[offset + 1] === 'hook') return true;
165
+ }
166
+ // Older installers wrote the absolute script path without shell quoting.
167
+ // Match only that entire generated form, not a command that merely mentions hcc.
168
+ const legacy = typeof cmd === 'string' &&
169
+ /^(\/[^\r\n;&|<>$`()\\]*\/hcc\.mjs) hook (?:sessionstart|userpromptsubmit|pretooluse|posttooluse|stop)$/i.exec(cmd);
170
+ return Boolean(legacy && !/\s\//.test(legacy[1]));
156
171
  }
157
172
 
158
173
  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;
174
+ const entries = removeHccHooks(hookEntries(existing)).entries;
164
175
 
165
176
  const entry = {
166
177
  hooks: [{ type: 'command', command }],
@@ -169,3 +180,84 @@ function mergeCodexHookEntry(existing, command, matcher) {
169
180
  entries.push(entry);
170
181
  return entries;
171
182
  }
183
+
184
+ function readHookConfig(filePath) {
185
+ let contents;
186
+ try { contents = fs.readFileSync(filePath, 'utf8'); }
187
+ catch (error) {
188
+ if (error?.code === 'ENOENT') return null;
189
+ throw error;
190
+ }
191
+ let config;
192
+ try { config = JSON.parse(contents); }
193
+ catch { throw new CliError('INVALID_HOOK_CONFIG', `Invalid JSON in ${filePath}; hooks were not changed`); }
194
+ if (!config || typeof config !== 'object' || Array.isArray(config) ||
195
+ (config.hooks !== undefined && (!config.hooks || typeof config.hooks !== 'object' || Array.isArray(config.hooks)))) {
196
+ throw new CliError('INVALID_HOOK_CONFIG', `Invalid hooks structure in ${filePath}; hooks were not changed`);
197
+ }
198
+ if (config.hooks && Object.values(config.hooks).some(entries => !Array.isArray(entries))) {
199
+ throw new CliError('INVALID_HOOK_CONFIG', `Invalid hook event in ${filePath}; hooks were not changed`);
200
+ }
201
+ return config;
202
+ }
203
+
204
+ function hookEntries(existing) {
205
+ if (existing === undefined) return [];
206
+ if (Array.isArray(existing)) return existing;
207
+ throw new CliError('INVALID_HOOK_CONFIG', 'Hook event must be an array; hooks were not changed');
208
+ }
209
+
210
+ function removeHccHooks(entries) {
211
+ let removed = false;
212
+ const kept = [];
213
+ for (const entry of entries) {
214
+ if (!Array.isArray(entry?.hooks)) {
215
+ kept.push(entry);
216
+ continue;
217
+ }
218
+ const hooks = entry.hooks.filter(hook => {
219
+ if (!isHccHookCmd(hook?.command)) return true;
220
+ removed = true;
221
+ return false;
222
+ });
223
+ if (hooks.length === entry.hooks.length) kept.push(entry);
224
+ else if (hooks.length) kept.push({ ...entry, hooks });
225
+ }
226
+ return { entries: kept, removed };
227
+ }
228
+
229
+ function shellWords(command) {
230
+ if (typeof command !== 'string') return null;
231
+ const words = [];
232
+ let word = '';
233
+ let started = false;
234
+ let quote = null;
235
+ for (let i = 0; i < command.length; i++) {
236
+ const char = command[i];
237
+ if (quote === "'") {
238
+ if (char === "'") quote = null;
239
+ else word += char;
240
+ } else if (quote === '"') {
241
+ if (char === '"') quote = null;
242
+ else if (char === '\\') word += command[++i] || '';
243
+ else if (char === '$' || char === '`') return null;
244
+ else word += char;
245
+ } else if (/\s/.test(char)) {
246
+ if (started) { words.push(word); word = ''; started = false; }
247
+ } else if (char === "'" || char === '"') {
248
+ quote = char;
249
+ started = true;
250
+ } else if (char === '\\') {
251
+ word += command[++i] || '';
252
+ started = true;
253
+ } else if (';&|<>$`()'.includes(char)) {
254
+ return null;
255
+ } else {
256
+ word += char;
257
+ started = true;
258
+ }
259
+ }
260
+ if (quote) return null;
261
+ if (started) words.push(word);
262
+ return words;
263
+ }
@@ -105,6 +105,29 @@ function detectCliKindFromProcess(args, env) {
105
105
  return null;
106
106
  }
107
107
 
108
+ function executableCliKind(args) {
109
+ if (argsLookLikeDsh(args)) return 'dsh';
110
+ if (!Array.isArray(args) || !args.length) return null;
111
+ const executable = path.basename(String(args[0] || '')).toLowerCase();
112
+ if (executable === 'codex') return 'codex';
113
+ if (executable === 'claude' || executable === 'claude-code') return 'claude';
114
+ if (!['node', 'nodejs', 'bun'].includes(executable)) return null;
115
+
116
+ let index = 1;
117
+ const valueFlags = new Set(['-r', '--require', '--import', '--loader', '--experimental-loader', '--conditions']);
118
+ while (index < args.length && String(args[index]).startsWith('-')) {
119
+ const flag = String(args[index]);
120
+ if (flag === '--') { index += 1; break; }
121
+ if (/^-[ep]/.test(flag) || ['--eval', '--print'].some(name => flag === name || flag.startsWith(`${name}=`))) return null;
122
+ index += valueFlags.has(flag) ? 2 : 1;
123
+ }
124
+ const entry = String(args[index] || '').replaceAll('\\', '/').toLowerCase();
125
+ const basename = path.posix.basename(entry);
126
+ if (entry.includes('/@openai/codex/') || ['codex', 'codex.js', 'codex.mjs'].includes(basename)) return 'codex';
127
+ if (entry.includes('/@anthropic-ai/claude-code/') || ['claude', 'claude.js', 'claude.mjs'].includes(basename)) return 'claude';
128
+ return null;
129
+ }
130
+
108
131
  export function findLinuxAncestorCliInfo(startPid, options = {}) {
109
132
  const read = typeof options.read === 'function'
110
133
  ? options.read
@@ -115,17 +138,22 @@ export function findLinuxAncestorCliInfo(startPid, options = {}) {
115
138
  });
116
139
  let pid = Number(startPid);
117
140
  const seen = new Set();
141
+ let fallback = null;
118
142
  for (let depth = 0; Number.isInteger(pid) && pid > 1 && depth < 12 && !seen.has(pid); depth += 1) {
119
143
  seen.add(pid);
120
144
  const row = read(pid);
121
- if (!row) return null;
145
+ if (!row) return fallback;
122
146
  const args = Array.isArray(row.args) ? row.args : [];
123
147
  const env = row.env && typeof row.env === 'object' && !Array.isArray(row.env) ? row.env : {};
148
+ const executableKind = executableCliKind(args);
149
+ if (executableKind && (!options.kind || options.kind === executableKind)) {
150
+ return { pid, kind: executableKind, args, env };
151
+ }
124
152
  const kind = detectCliKindFromProcess(args, env);
125
- if (kind && (!options.kind || options.kind === kind)) return { pid, kind, args, env };
153
+ if (!fallback && kind && (!options.kind || options.kind === kind)) fallback = { pid, kind, args, env };
126
154
  pid = Number(row.parent);
127
155
  }
128
- return null;
156
+ return fallback;
129
157
  }
130
158
 
131
159
  export function parseMacAncestorLine(text) {
@@ -153,21 +181,28 @@ export function findMacAncestorCliInfo(startPid, options = {}) {
153
181
  const inspect = typeof options.inspect === 'function' ? options.inspect : inspectMacAncestor;
154
182
  let pid = Number(startPid);
155
183
  const seen = new Set();
184
+ let fallback = null;
156
185
  for (let depth = 0; Number.isInteger(pid) && pid > 1 && depth < 12 && !seen.has(pid); depth += 1) {
157
186
  seen.add(pid);
158
187
  const parsed = parseMacAncestorLine(inspect(pid));
159
- if (!parsed) return null;
188
+ if (!parsed) return fallback;
160
189
  // ps provides a rendered command, not lossless argv. Only tokenize enough
161
190
  // to recognize the dsh executable/Node script; resume parsing remains off.
162
191
  const commandArgs = parsed.command.match(/"(?:\\.|[^"\\])*"|'[^']*'|\S+/g)?.map((arg) =>
163
192
  /^("|')/.test(arg) ? arg.slice(1, -1) : arg) || [];
164
- const kind = argsLookLikeDsh(commandArgs) ? 'dsh' : detectCliKindFromProcess([parsed.command], {});
193
+ const executableKind = executableCliKind(commandArgs);
194
+ if (executableKind && (!options.kind || options.kind === executableKind)) {
195
+ return { pid, kind: executableKind, args: [], env: {}, command: parsed.command };
196
+ }
197
+ const kind = detectCliKindFromProcess([parsed.command], {});
165
198
  // `ps command` is sufficient for provider-kind detection but is not a
166
199
  // lossless argv encoding. Do not feed it to resume-ID parsing.
167
- if (kind && (!options.kind || options.kind === kind)) return { pid, kind, args: [], env: {}, command: parsed.command };
200
+ if (!fallback && kind && (!options.kind || options.kind === kind)) {
201
+ fallback = { pid, kind, args: [], env: {}, command: parsed.command };
202
+ }
168
203
  pid = parsed.ppid;
169
204
  }
170
- return null;
205
+ return fallback;
171
206
  }
172
207
 
173
208
  export function readAncestorCliInfo(kindHint = null) {
@@ -97,35 +97,52 @@ function parseMacBootToken(text) {
97
97
  return `${seconds}:${microseconds}`;
98
98
  }
99
99
 
100
- function macIdentity(pid) {
100
+ function parseMacStateStart(text) {
101
+ const match = /^(\S+)\s+([^\r\n]+)$/.exec(String(text ?? '').trim());
102
+ // Darwin ps uses X only as a traced/debugged modifier; Z is its exit state.
103
+ if (!match || !/^[IRSTUZ][+<>AELNSsVWX]*$/.test(match[1])) return null;
104
+ return { state: match[1][0], startToken: match[2].trim() };
105
+ }
106
+
107
+ function macObservation(pid) {
108
+ const unknown = { state: 'unknown', identity: null };
101
109
  let boot = run('sysctl', ['-n', 'kern.boottime']);
102
110
  // Launchers often omit /usr/sbin from PATH; macOS still provides sysctl here.
103
111
  if (boot.error?.code === 'ENOENT') boot = run('/usr/sbin/sysctl', ['-n', 'kern.boottime']);
104
- if (boot.error || boot.status !== 0) return null;
112
+ if (boot.error || boot.status !== 0) return unknown;
105
113
  const bootToken = parseMacBootToken(boot.stdout);
106
- if (!bootToken) return null;
114
+ if (!bootToken) return unknown;
107
115
 
108
116
  const psEnvironment = { ...process.env, TZ: 'UTC', LC_ALL: 'C', LANG: 'C' };
109
- const started = run('ps', ['-p', String(pid), '-o', 'lstart='], psEnvironment);
117
+ // kill(pid, 0) also succeeds for an unreaped zombie on Darwin. Read state
118
+ // alongside each existing start-time probe, without adding subprocesses.
119
+ const started = run('ps', ['-p', String(pid), '-o', 'stat=', '-o', 'lstart='], psEnvironment);
120
+ const first = parseMacStateStart(started.stdout);
121
+ if (started.error || started.status !== 0 || !first) return unknown;
122
+ if (first.state === 'Z') return { state: 'dead', identity: null };
110
123
  const command = run('ps', ['-p', String(pid), '-o', 'command='], psEnvironment);
111
- const confirmedStarted = run('ps', ['-p', String(pid), '-o', 'lstart='], psEnvironment);
112
- if (started.error || command.error || confirmedStarted.error) return null;
113
- if (started.status !== 0 ||
114
- command.status !== 0 || confirmedStarted.status !== 0) return null;
124
+ const confirmedStarted = run('ps', ['-p', String(pid), '-o', 'stat=', '-o', 'lstart='], psEnvironment);
125
+ const last = parseMacStateStart(confirmedStarted.stdout);
126
+ if (confirmedStarted.error || confirmedStarted.status !== 0 || !last) return unknown;
127
+ if (last.state === 'Z') return { state: 'dead', identity: null };
128
+ if (command.error || command.status !== 0) return unknown;
115
129
 
116
130
  const commandOutput = String(command.stdout ?? '').trim();
117
131
  const parsed = parsePsStartIdentity(
118
- `${String(started.stdout ?? '').trim()}\t${commandOutput}\n`
132
+ `${first.startToken}\t${commandOutput}\n`
119
133
  );
120
134
  const confirmed = parsePsStartIdentity(
121
- `${String(confirmedStarted.stdout ?? '').trim()}\t${commandOutput}\n`
135
+ `${last.startToken}\t${commandOutput}\n`
122
136
  );
123
- if (!parsed || !confirmed || confirmed.startToken !== parsed.startToken) return null;
137
+ if (!parsed || !confirmed || confirmed.startToken !== parsed.startToken) return unknown;
124
138
 
125
139
  return {
126
- pid,
127
- startToken: `${bootToken}:${parsed.startToken}`,
128
- commandHash: hashCommand(parsed.command)
140
+ state: 'live',
141
+ identity: {
142
+ pid,
143
+ startToken: `${bootToken}:${parsed.startToken}`,
144
+ commandHash: hashCommand(parsed.command)
145
+ }
129
146
  };
130
147
  }
131
148
 
@@ -204,7 +221,11 @@ export function inspectProcessIdentity(value) {
204
221
  if (observed.state === 'dead') return observed;
205
222
  identity = observed.identity;
206
223
  }
207
- else if (process.platform === 'darwin') identity = macIdentity(pid);
224
+ else if (process.platform === 'darwin') {
225
+ const observed = macObservation(pid);
226
+ if (observed.state === 'dead') return observed;
227
+ identity = observed.identity;
228
+ }
208
229
  } catch {
209
230
  identity = null;
210
231
  }
@@ -2,6 +2,7 @@ import { CliError } from '../shared/errors.mjs';
2
2
  import { readRuntime } from './state.mjs';
3
3
  import { runtimeApiUrl, runtimeHttpRequest } from '../web/runtime.mjs';
4
4
  import { withRuntimeApiVersionHeader } from '../web/api-version.mjs';
5
+ import { performance } from 'node:perf_hooks';
5
6
 
6
7
  const DEFAULT_CLI_NAME = 'hcc';
7
8
  export const RUNTIME_SESSION_START_TIMEOUT_MS = 60_000;
@@ -56,7 +57,9 @@ export async function runtimeRequest(ctx, method, route, body = null, runtime =
56
57
  let res;
57
58
  let text;
58
59
  const abortScope = requestAbortScope(opts.signal, opts.timeoutMs);
60
+ const startedAt = performance.now();
59
61
  try {
62
+ abortScope.signal.throwIfAborted();
60
63
  res = await waitForAbort(runtimeHttpRequest(rt, route, {
61
64
  method,
62
65
  headers,
@@ -68,7 +71,12 @@ export async function runtimeRequest(ctx, method, route, body = null, runtime =
68
71
  } catch (err) {
69
72
  throw new CliError('RUNTIME_UNREACHABLE', `Runtime is not reachable at ${rt.base_url}. Start ${cliName} web again.`, {
70
73
  runtime: rt.source || rt.base_url,
71
- message: err.message
74
+ message: err.message,
75
+ method,
76
+ path: url.pathname,
77
+ elapsedMs: Math.round(performance.now() - startedAt),
78
+ timeoutMs: opts.timeoutMs === undefined ? 8000 : opts.timeoutMs,
79
+ errorName: err.name || 'Error'
72
80
  });
73
81
  } finally {
74
82
  abortScope.cleanup();