@logicseek/hello-cc 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +462 -0
- package/README.zh-CN.md +409 -0
- package/bin/hcc.mjs +6355 -0
- package/docs/design.md +263 -0
- package/docs/implementation.md +345 -0
- package/lib/discover.mjs +477 -0
- package/lib/setup.mjs +766 -0
- package/package.json +54 -0
- package/scripts/regression.mjs +1221 -0
package/lib/setup.mjs
ADDED
|
@@ -0,0 +1,766 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hooks and shim installation for zero-config Claude Code / Codex integration.
|
|
3
|
+
*
|
|
4
|
+
* Claude Code hooks → ~/.claude/settings.json
|
|
5
|
+
* Shim scripts → ~/.hcc-shims/{claude,codex}
|
|
6
|
+
* PATH entry → ~/.bashrc or ~/.zshrc
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import os from 'node:os';
|
|
12
|
+
import { spawnSync } from 'node:child_process';
|
|
13
|
+
|
|
14
|
+
export const SHIM_DIR = path.join(os.homedir(), '.hcc-shims');
|
|
15
|
+
|
|
16
|
+
// ─── Claude Code hooks ────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
const CLAUDE_SETTINGS_PATH = path.join(os.homedir(), '.claude', 'settings.json');
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Hook events we install handlers for.
|
|
22
|
+
* - SessionStart : Inject initial coordination context
|
|
23
|
+
* - UserPromptSubmit : Inject fresh coordination context before the model answers
|
|
24
|
+
* - Stop : Claude goes idle after a turn → deliver inbox messages
|
|
25
|
+
* - PostToolUse : After every tool call → heartbeat + inbox check
|
|
26
|
+
* - PreToolUse : Before every tool call → register peer if not yet registered
|
|
27
|
+
*/
|
|
28
|
+
const HOOK_EVENTS = ['SessionStart', 'UserPromptSubmit', 'Stop', 'PostToolUse', 'PreToolUse'];
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Install (or update) hello-cc hooks in ~/.claude/settings.json.
|
|
32
|
+
* Non-destructive: only adds hcc entries, never removes existing ones.
|
|
33
|
+
* Returns the settings file path.
|
|
34
|
+
*/
|
|
35
|
+
export function installClaudeHooks(hccBin) {
|
|
36
|
+
const settings = readJsonSafe(CLAUDE_SETTINGS_PATH) || {};
|
|
37
|
+
|
|
38
|
+
if (!settings.hooks) settings.hooks = {};
|
|
39
|
+
|
|
40
|
+
const hookCmd = `${hccBin} hook`;
|
|
41
|
+
|
|
42
|
+
for (const event of HOOK_EVENTS) {
|
|
43
|
+
settings.hooks[event] = mergeHookEntry(
|
|
44
|
+
settings.hooks[event],
|
|
45
|
+
`${hookCmd} ${event.toLowerCase()}`
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
writeJsonSafe(CLAUDE_SETTINGS_PATH, settings);
|
|
50
|
+
return CLAUDE_SETTINGS_PATH;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Remove hello-cc hook entries from ~/.claude/settings.json.
|
|
55
|
+
*/
|
|
56
|
+
export function uninstallClaudeHooks() {
|
|
57
|
+
const settings = readJsonSafe(CLAUDE_SETTINGS_PATH);
|
|
58
|
+
if (!settings?.hooks) return false;
|
|
59
|
+
|
|
60
|
+
for (const event of Object.keys(settings.hooks)) {
|
|
61
|
+
if (!Array.isArray(settings.hooks[event])) continue;
|
|
62
|
+
settings.hooks[event] = settings.hooks[event].filter(entry => {
|
|
63
|
+
if (!Array.isArray(entry?.hooks)) return true;
|
|
64
|
+
return !entry.hooks.some(h => isHccHookCmd(h?.command));
|
|
65
|
+
});
|
|
66
|
+
if (settings.hooks[event].length === 0) delete settings.hooks[event];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
writeJsonSafe(CLAUDE_SETTINGS_PATH, settings);
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Returns true if hello-cc hooks are present in ~/.claude/settings.json.
|
|
75
|
+
*/
|
|
76
|
+
export function verifyClaudeHooks() {
|
|
77
|
+
const settings = readJsonSafe(CLAUDE_SETTINGS_PATH);
|
|
78
|
+
if (!settings?.hooks) return false;
|
|
79
|
+
return HOOK_EVENTS.every(event => hasHookEntry(settings.hooks[event]));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Append a hook entry, skipping if one already exists for hcc.
|
|
83
|
+
function mergeHookEntry(existing, command) {
|
|
84
|
+
const entries = Array.isArray(existing) ? [...existing] : [];
|
|
85
|
+
const alreadyInstalled = entries.some(
|
|
86
|
+
e => Array.isArray(e?.hooks) && e.hooks.some(h => isHccHookCmd(h?.command))
|
|
87
|
+
);
|
|
88
|
+
if (alreadyInstalled) return entries;
|
|
89
|
+
entries.push({ hooks: [{ type: 'command', command }] });
|
|
90
|
+
return entries;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function hasHookEntry(entries) {
|
|
94
|
+
if (!Array.isArray(entries)) return false;
|
|
95
|
+
return entries.some(e =>
|
|
96
|
+
Array.isArray(e?.hooks) && e.hooks.some(h => isHccHookCmd(h?.command))
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function isHccHookCmd(cmd) {
|
|
101
|
+
if (typeof cmd !== 'string') return false;
|
|
102
|
+
return /\bhcc\b.*\bhook\b/.test(cmd) || /hello-cc.*hook/.test(cmd);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ─── Shim scripts ─────────────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Install shim scripts for `claude` and `codex` into SHIM_DIR.
|
|
109
|
+
* Returns { shimDir, installed: [paths], changed: [paths], skipped: [names] }.
|
|
110
|
+
*/
|
|
111
|
+
export function installShims(hccBin) {
|
|
112
|
+
fs.mkdirSync(SHIM_DIR, { recursive: true });
|
|
113
|
+
|
|
114
|
+
const tools = [
|
|
115
|
+
{ name: 'claude', kind: 'claude', resumeFlag: '--resume' },
|
|
116
|
+
{ name: 'codex', kind: 'codex', resumeSubcmd: 'resume' },
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
const installed = [];
|
|
120
|
+
const changed = [];
|
|
121
|
+
const skipped = [];
|
|
122
|
+
|
|
123
|
+
for (const tool of tools) {
|
|
124
|
+
const realBin = findRealBinary(tool.name);
|
|
125
|
+
if (!realBin) {
|
|
126
|
+
skipped.push(`${tool.name} (not found on PATH)`);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const shimPath = path.join(SHIM_DIR, tool.name);
|
|
130
|
+
const content = generateShim(hccBin, realBin, tool);
|
|
131
|
+
let previous = null;
|
|
132
|
+
try { previous = fs.readFileSync(shimPath, 'utf8'); } catch {}
|
|
133
|
+
if (previous !== content) {
|
|
134
|
+
fs.writeFileSync(shimPath, content, { mode: 0o755 });
|
|
135
|
+
changed.push(shimPath);
|
|
136
|
+
} else {
|
|
137
|
+
try { fs.chmodSync(shimPath, 0o755); } catch {}
|
|
138
|
+
}
|
|
139
|
+
installed.push(shimPath);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return { shimDir: SHIM_DIR, installed, changed, skipped };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Remove shim scripts.
|
|
147
|
+
*/
|
|
148
|
+
export function uninstallShims() {
|
|
149
|
+
const removed = [];
|
|
150
|
+
for (const name of ['claude', 'codex']) {
|
|
151
|
+
const p = path.join(SHIM_DIR, name);
|
|
152
|
+
try { fs.unlinkSync(p); removed.push(p); } catch {}
|
|
153
|
+
}
|
|
154
|
+
try { fs.rmdirSync(SHIM_DIR); } catch {}
|
|
155
|
+
return removed;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Returns true if at least one shim is installed and in an HCC shim dir.
|
|
160
|
+
*/
|
|
161
|
+
export function verifyShims() {
|
|
162
|
+
return ['claude', 'codex'].some(name =>
|
|
163
|
+
fsExists(path.join(SHIM_DIR, name))
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Find the real binary for `name`, skipping the SHIM_DIR.
|
|
169
|
+
* Checks PATH, then npm global bin, then common install locations.
|
|
170
|
+
*/
|
|
171
|
+
export function findRealBinary(name) {
|
|
172
|
+
// which -a lists all matches; pick first one outside SHIM_DIR
|
|
173
|
+
const r = spawnSync('which', ['-a', name], { encoding: 'utf8', timeout: 3000 });
|
|
174
|
+
if (r.status === 0) {
|
|
175
|
+
for (const p of r.stdout.trim().split('\n')) {
|
|
176
|
+
const resolved = p.trim();
|
|
177
|
+
if (resolved && !resolved.startsWith(SHIM_DIR)) return resolved;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Try npm global bin
|
|
182
|
+
const npmR = spawnSync('npm', ['root', '-g'], { encoding: 'utf8', timeout: 3000 });
|
|
183
|
+
if (npmR.status === 0) {
|
|
184
|
+
const npmBin = path.join(path.dirname(npmR.stdout.trim()), 'bin', name);
|
|
185
|
+
if (fsExists(npmBin)) return npmBin;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Common locations
|
|
189
|
+
for (const dir of [
|
|
190
|
+
'/usr/local/bin',
|
|
191
|
+
'/usr/bin',
|
|
192
|
+
path.join(os.homedir(), '.local', 'bin'),
|
|
193
|
+
path.join(os.homedir(), '.nvm', 'current', 'bin'),
|
|
194
|
+
]) {
|
|
195
|
+
const p = path.join(dir, name);
|
|
196
|
+
if (fsExists(p)) return p;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ─── PATH entry in shell rc ───────────────────────────────────────────────────
|
|
203
|
+
|
|
204
|
+
function pathEntryLine(shellType) {
|
|
205
|
+
if (shellType === 'fish') return 'set -gx PATH $HOME/.hcc-shims $PATH # hello-cc shims';
|
|
206
|
+
return '_hcc_shims="$HOME/.hcc-shims"; PATH="${PATH//:${_hcc_shims}:/:}"; PATH="${PATH#${_hcc_shims}:}"; PATH="${PATH%:${_hcc_shims}}"; export PATH="${_hcc_shims}:$PATH"; unset _hcc_shims # hello-cc shims';
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function removePathEntryLines(content) {
|
|
210
|
+
return String(content || '')
|
|
211
|
+
.split('\n')
|
|
212
|
+
.filter(line => !line.includes('.hcc-shims'))
|
|
213
|
+
.join('\n')
|
|
214
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
215
|
+
.replace(/\s+$/, '');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function insertPathEntry(content, shellType) {
|
|
219
|
+
const line = pathEntryLine(shellType);
|
|
220
|
+
const cleaned = removePathEntryLines(content);
|
|
221
|
+
if (shellType === 'fish') return `${cleaned}${cleaned ? '\n' : ''}${line}\n`;
|
|
222
|
+
|
|
223
|
+
const lines = cleaned ? cleaned.split('\n') : [];
|
|
224
|
+
let insertAt = 0;
|
|
225
|
+
if (lines[0]?.startsWith('#!')) insertAt = 1;
|
|
226
|
+
for (let i = insertAt; i < lines.length; i += 1) {
|
|
227
|
+
const text = lines[i].trim();
|
|
228
|
+
if (!text || text.startsWith('#')) continue;
|
|
229
|
+
insertAt = i;
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
lines.splice(insertAt, 0, line);
|
|
233
|
+
const withEarlyEntry = lines.join('\n').replace(/\s+$/, '');
|
|
234
|
+
return `${withEarlyEntry}${withEarlyEntry ? '\n' : ''}${line}\n`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Install SHIM_DIR in the user's shell rc before common early-return guards.
|
|
239
|
+
* Returns { rcFile, shellType, alreadyPresent }.
|
|
240
|
+
*/
|
|
241
|
+
export function installPathEntry() {
|
|
242
|
+
const { path: rcFile, type: shellType } = detectShellRc();
|
|
243
|
+
let content = '';
|
|
244
|
+
try { content = fs.readFileSync(rcFile, 'utf8'); } catch {}
|
|
245
|
+
|
|
246
|
+
const next = insertPathEntry(content, shellType);
|
|
247
|
+
const alreadyPresent = next === (content.endsWith('\n') ? content : `${content}\n`);
|
|
248
|
+
if (!alreadyPresent) fs.writeFileSync(rcFile, next);
|
|
249
|
+
return { rcFile, shellType, alreadyPresent };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Remove SHIM_DIR PATH entry from shell rc.
|
|
254
|
+
*/
|
|
255
|
+
export function uninstallPathEntry() {
|
|
256
|
+
const { path: rcFile } = detectShellRc();
|
|
257
|
+
try {
|
|
258
|
+
const content = fs.readFileSync(rcFile, 'utf8');
|
|
259
|
+
const filtered = content
|
|
260
|
+
.split('\n')
|
|
261
|
+
.filter(line => !line.includes('.hcc-shims'))
|
|
262
|
+
.join('\n');
|
|
263
|
+
fs.writeFileSync(rcFile, filtered);
|
|
264
|
+
return { rcFile };
|
|
265
|
+
} catch {
|
|
266
|
+
return { rcFile, error: 'could not modify shell rc' };
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function detectShellRc() {
|
|
271
|
+
const shell = process.env.SHELL || '';
|
|
272
|
+
if (shell.includes('zsh')) return { path: path.join(os.homedir(), '.zshrc'), type: 'zsh' };
|
|
273
|
+
if (shell.includes('fish')) return { path: path.join(os.homedir(), '.config', 'fish', 'config.fish'), type: 'fish' };
|
|
274
|
+
return { path: path.join(os.homedir(), '.bashrc'), type: 'bash' };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ─── Shim script generation ───────────────────────────────────────────────────
|
|
278
|
+
|
|
279
|
+
function generateShim(hccBin, realBin, tool) {
|
|
280
|
+
if (tool.resumeFlag) {
|
|
281
|
+
// Claude: --resume/--continue/--session-id/--fork-session aware wrapper.
|
|
282
|
+
return `#!/usr/bin/env bash
|
|
283
|
+
# hello-cc shim for ${tool.name} (auto-generated; do not edit manually)
|
|
284
|
+
# Wraps ${tool.name} with hello-cc peer mesh integration.
|
|
285
|
+
# Real binary: ${realBin}
|
|
286
|
+
|
|
287
|
+
set -e
|
|
288
|
+
REAL_BIN="${realBin}"
|
|
289
|
+
HCC_BIN="${hccBin}"
|
|
290
|
+
|
|
291
|
+
if [ "\${HCC_SHIM_ENSURED:-}" != "1" ]; then
|
|
292
|
+
set +e
|
|
293
|
+
"$HCC_BIN" shim ensure "${tool.name}" "$0" >/dev/null 2>&1
|
|
294
|
+
ENSURE_STATUS=$?
|
|
295
|
+
set -e
|
|
296
|
+
if [ "$ENSURE_STATUS" = "75" ]; then
|
|
297
|
+
export HCC_SHIM_ENSURED=1
|
|
298
|
+
exec "$0" "$@"
|
|
299
|
+
fi
|
|
300
|
+
fi
|
|
301
|
+
|
|
302
|
+
should_passthrough() {
|
|
303
|
+
for arg in "$@"; do
|
|
304
|
+
case "$arg" in
|
|
305
|
+
-h|--help|-v|--version|-p|--print|--bare|--safe-mode)
|
|
306
|
+
return 0
|
|
307
|
+
;;
|
|
308
|
+
esac
|
|
309
|
+
done
|
|
310
|
+
|
|
311
|
+
case "\${1:-}" in
|
|
312
|
+
agents|auth|auto-mode|doctor|install|mcp|plugin|plugins|project|setup-token|ultrareview|update|upgrade)
|
|
313
|
+
return 0
|
|
314
|
+
;;
|
|
315
|
+
esac
|
|
316
|
+
|
|
317
|
+
return 1
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if should_passthrough "$@"; then
|
|
321
|
+
exec "$REAL_BIN" "$@"
|
|
322
|
+
fi
|
|
323
|
+
|
|
324
|
+
# Use the current directory as the hello-cc project root unless HCC_ROOT is set.
|
|
325
|
+
HCC_ROOT=$("$HCC_BIN" find-root 2>/dev/null || true)
|
|
326
|
+
|
|
327
|
+
if [ -z "$HCC_ROOT" ]; then
|
|
328
|
+
exec "$REAL_BIN" "$@"
|
|
329
|
+
fi
|
|
330
|
+
|
|
331
|
+
sanitize_peer_part() {
|
|
332
|
+
printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9._-]/-/g; s/^-*//; s/-*$//' | cut -c 1-32
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
tty_peer_part() {
|
|
336
|
+
TTY_NUM=$(tty 2>/dev/null | tr -dc '0-9' | tail -c 4)
|
|
337
|
+
PARENT_PID="\${PPID:-$$}"
|
|
338
|
+
if [ -n "$TTY_NUM" ]; then
|
|
339
|
+
printf '%s-%s' "$TTY_NUM" "$PARENT_PID"
|
|
340
|
+
else
|
|
341
|
+
printf '%s' "$PARENT_PID"
|
|
342
|
+
fi
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
RESUME_ID=""
|
|
346
|
+
SESSION_ID=""
|
|
347
|
+
SESSION_NAME=""
|
|
348
|
+
IS_CONTINUE=0
|
|
349
|
+
IS_FORK=0
|
|
350
|
+
PREV_ARG=""
|
|
351
|
+
for arg in "$@"; do
|
|
352
|
+
case "$PREV_ARG" in
|
|
353
|
+
--resume|-r) RESUME_ID="$arg" ;;
|
|
354
|
+
--session-id) SESSION_ID="$arg" ;;
|
|
355
|
+
--name|-n) SESSION_NAME="$arg" ;;
|
|
356
|
+
esac
|
|
357
|
+
case "$arg" in
|
|
358
|
+
--resume=*) RESUME_ID="\${arg#--resume=}" ;;
|
|
359
|
+
--session-id=*) SESSION_ID="\${arg#--session-id=}" ;;
|
|
360
|
+
--name=*) SESSION_NAME="\${arg#--name=}" ;;
|
|
361
|
+
--continue|-c) IS_CONTINUE=1 ;;
|
|
362
|
+
--fork-session) IS_FORK=1 ;;
|
|
363
|
+
esac
|
|
364
|
+
PREV_ARG="$arg"
|
|
365
|
+
done
|
|
366
|
+
|
|
367
|
+
if [ -n "$HCC_PEER" ]; then
|
|
368
|
+
PEER_ID="$HCC_PEER"
|
|
369
|
+
elif [ -n "$SESSION_ID" ]; then
|
|
370
|
+
PEER_ID="${tool.kind}-\$(sanitize_peer_part "\${SESSION_ID:0:8}")"
|
|
371
|
+
elif [ -n "$RESUME_ID" ] && [ "$IS_FORK" != "1" ]; then
|
|
372
|
+
PEER_ID="${tool.kind}-\$(sanitize_peer_part "\${RESUME_ID:0:8}")"
|
|
373
|
+
elif [ -n "$CLAUDE_CODE_SESSION_ID" ]; then
|
|
374
|
+
PEER_ID="${tool.kind}-\$(sanitize_peer_part "\${CLAUDE_CODE_SESSION_ID:0:8}")"
|
|
375
|
+
elif [ -n "$SESSION_NAME" ] && [ "$IS_FORK" != "1" ] && [ "$IS_CONTINUE" != "1" ]; then
|
|
376
|
+
PEER_ID="${tool.kind}-\$(sanitize_peer_part "$SESSION_NAME")"
|
|
377
|
+
else
|
|
378
|
+
PEER_ID="${tool.kind}-\$(tty_peer_part)"
|
|
379
|
+
fi
|
|
380
|
+
|
|
381
|
+
export HCC_PEER="$PEER_ID"
|
|
382
|
+
export HCC_ROOT
|
|
383
|
+
|
|
384
|
+
current_tmux_pane() {
|
|
385
|
+
if [ -n "\${TMUX_PANE:-}" ]; then
|
|
386
|
+
printf '%s\\n' "$TMUX_PANE"
|
|
387
|
+
return 0
|
|
388
|
+
fi
|
|
389
|
+
TTY_PATH=$(tty 2>/dev/null || true)
|
|
390
|
+
if [ -z "$TTY_PATH" ] || [ "$TTY_PATH" = "not a tty" ]; then
|
|
391
|
+
return 1
|
|
392
|
+
fi
|
|
393
|
+
tmux list-panes -a -F '#{pane_id} #{pane_tty}' 2>/dev/null | awk -v tty="$TTY_PATH" '$2 == tty { print $1; found = 1; exit } END { exit found ? 0 : 1 }'
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
CURRENT_HCC_PANE=""
|
|
397
|
+
CURRENT_HCC_SESSION=""
|
|
398
|
+
detect_current_hcc_pane() {
|
|
399
|
+
CURRENT_HCC_SESSION=""
|
|
400
|
+
CURRENT_HCC_PANE=$(current_tmux_pane || true)
|
|
401
|
+
if [ -z "$CURRENT_HCC_PANE" ]; then
|
|
402
|
+
return 1
|
|
403
|
+
fi
|
|
404
|
+
CURRENT_HCC_SESSION=$(tmux display-message -p -t "$CURRENT_HCC_PANE" '#S' 2>/dev/null || true)
|
|
405
|
+
case "$CURRENT_HCC_SESSION" in
|
|
406
|
+
hcc-*-*) return 0 ;;
|
|
407
|
+
esac
|
|
408
|
+
CURRENT_HCC_PANE=""
|
|
409
|
+
CURRENT_HCC_SESSION=""
|
|
410
|
+
return 1
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if detect_current_hcc_pane; then
|
|
414
|
+
SESSION_PEER_ID=$(printf '%s' "$CURRENT_HCC_SESSION" | sed 's/^hcc-[^-]*-//')
|
|
415
|
+
if [ -n "$SESSION_PEER_ID" ] && [ "$SESSION_PEER_ID" != "$CURRENT_HCC_SESSION" ]; then
|
|
416
|
+
PEER_ID="$SESSION_PEER_ID"
|
|
417
|
+
fi
|
|
418
|
+
export HCC_PEER="$PEER_ID"
|
|
419
|
+
"$HCC_BIN" web >/dev/null 2>&1 || true
|
|
420
|
+
"$HCC_BIN" peer attach "$PEER_ID" --kind "${tool.kind}" --pane "$CURRENT_HCC_PANE" --cwd "$PWD" --force >/dev/null 2>&1 || true
|
|
421
|
+
exec "$REAL_BIN" "$@"
|
|
422
|
+
fi
|
|
423
|
+
|
|
424
|
+
set +e
|
|
425
|
+
# Wrap in shell so pane_pid stays as bash; Ctrl+C drops back to bash
|
|
426
|
+
START_OUT=$("$HCC_BIN" web >/dev/null 2>&1 || true; "$HCC_BIN" peer start "$PEER_ID" --kind "${tool.kind}" --restart-env -- bash -lc 'REAL_BIN="$1"; shift; hcc_provider_state() { HCC_TMUX_SESSION=$(tmux display-message -p "#S" 2>/dev/null || true); if [ -n "$HCC_TMUX_SESSION" ]; then tmux set-environment -t "$HCC_TMUX_SESSION" HCC_PROVIDER_STATE "$1" >/dev/null 2>&1 || true; fi; }; hcc_provider_state running; "$REAL_BIN" "$@"; hcc_provider_state exited; exec bash' _ "$REAL_BIN" "$@" 2>&1)
|
|
427
|
+
START_STATUS=$?
|
|
428
|
+
set -e
|
|
429
|
+
if [ "$START_STATUS" -ne 0 ]; then
|
|
430
|
+
printf '%s\\n' "$START_OUT" >&2
|
|
431
|
+
exit "$START_STATUS"
|
|
432
|
+
fi
|
|
433
|
+
|
|
434
|
+
PANE=$(printf '%s\\n' "$START_OUT" | sed -n 's/.*pane=\\(%[0-9][0-9]*\\).*/\\1/p' | tail -n 1)
|
|
435
|
+
if [ -z "$PANE" ]; then
|
|
436
|
+
printf '%s\\n' "$START_OUT"
|
|
437
|
+
exit 0
|
|
438
|
+
fi
|
|
439
|
+
|
|
440
|
+
if [ "$HCC_SHIM_NO_ATTACH" = "1" ] || [ ! -t 0 ]; then
|
|
441
|
+
printf '%s\\n' "$START_OUT"
|
|
442
|
+
exit 0
|
|
443
|
+
fi
|
|
444
|
+
|
|
445
|
+
SESSION=$(tmux display-message -p -t "$PANE" '#S' 2>/dev/null || true)
|
|
446
|
+
if [ -z "$SESSION" ]; then
|
|
447
|
+
printf '%s\\n' "$START_OUT"
|
|
448
|
+
exit 0
|
|
449
|
+
fi
|
|
450
|
+
|
|
451
|
+
if [ -n "$TMUX" ]; then
|
|
452
|
+
exec tmux switch-client -t "$SESSION"
|
|
453
|
+
fi
|
|
454
|
+
|
|
455
|
+
exec tmux attach-session -t "$SESSION"
|
|
456
|
+
`;
|
|
457
|
+
} else {
|
|
458
|
+
// Codex: resume/fork subcommand aware wrapper.
|
|
459
|
+
return `#!/usr/bin/env bash
|
|
460
|
+
# hello-cc shim for ${tool.name} (auto-generated; do not edit manually)
|
|
461
|
+
# Wraps ${tool.name} with hello-cc peer mesh integration.
|
|
462
|
+
# Real binary: ${realBin}
|
|
463
|
+
|
|
464
|
+
set -e
|
|
465
|
+
REAL_BIN="${realBin}"
|
|
466
|
+
HCC_BIN="${hccBin}"
|
|
467
|
+
|
|
468
|
+
if [ "\${HCC_SHIM_ENSURED:-}" != "1" ]; then
|
|
469
|
+
set +e
|
|
470
|
+
"$HCC_BIN" shim ensure "${tool.name}" "$0" >/dev/null 2>&1
|
|
471
|
+
ENSURE_STATUS=$?
|
|
472
|
+
set -e
|
|
473
|
+
if [ "$ENSURE_STATUS" = "75" ]; then
|
|
474
|
+
export HCC_SHIM_ENSURED=1
|
|
475
|
+
exec "$0" "$@"
|
|
476
|
+
fi
|
|
477
|
+
fi
|
|
478
|
+
|
|
479
|
+
first_non_option() {
|
|
480
|
+
EXPECT_VALUE=0
|
|
481
|
+
for arg in "$@"; do
|
|
482
|
+
if [ "$EXPECT_VALUE" = "1" ]; then
|
|
483
|
+
EXPECT_VALUE=0
|
|
484
|
+
continue
|
|
485
|
+
fi
|
|
486
|
+
|
|
487
|
+
case "$arg" in
|
|
488
|
+
--)
|
|
489
|
+
EXPECT_VALUE=0
|
|
490
|
+
continue
|
|
491
|
+
;;
|
|
492
|
+
-c|--config|-i|--image|-m|--model|-p|--profile|-s|--sandbox|-C|--cd|--add-dir|-a|--ask-for-approval|--remote|--remote-auth-token-env)
|
|
493
|
+
EXPECT_VALUE=1
|
|
494
|
+
continue
|
|
495
|
+
;;
|
|
496
|
+
--config=*|--image=*|--model=*|--profile=*|--sandbox=*|--cd=*|--add-dir=*|--ask-for-approval=*|--remote=*|--remote-auth-token-env=*)
|
|
497
|
+
continue
|
|
498
|
+
;;
|
|
499
|
+
--*)
|
|
500
|
+
continue
|
|
501
|
+
;;
|
|
502
|
+
-*)
|
|
503
|
+
continue
|
|
504
|
+
;;
|
|
505
|
+
*)
|
|
506
|
+
printf '%s' "$arg"
|
|
507
|
+
return 0
|
|
508
|
+
;;
|
|
509
|
+
esac
|
|
510
|
+
done
|
|
511
|
+
return 1
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
should_passthrough() {
|
|
515
|
+
for arg in "$@"; do
|
|
516
|
+
case "$arg" in
|
|
517
|
+
-h|--help|-V|--version)
|
|
518
|
+
return 0
|
|
519
|
+
;;
|
|
520
|
+
esac
|
|
521
|
+
done
|
|
522
|
+
|
|
523
|
+
SUBCOMMAND=$(first_non_option "$@" || true)
|
|
524
|
+
case "$SUBCOMMAND" in
|
|
525
|
+
help|exec|e|review|login|logout|mcp|plugin|mcp-server|app-server|remote-control|completion|update|doctor|sandbox|debug|apply|a|archive|unarchive|cloud|exec-server|features)
|
|
526
|
+
return 0
|
|
527
|
+
;;
|
|
528
|
+
esac
|
|
529
|
+
|
|
530
|
+
return 1
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if should_passthrough "$@"; then
|
|
534
|
+
exec "$REAL_BIN" "$@"
|
|
535
|
+
fi
|
|
536
|
+
|
|
537
|
+
HCC_ROOT=$("$HCC_BIN" find-root 2>/dev/null || true)
|
|
538
|
+
|
|
539
|
+
if [ -z "$HCC_ROOT" ]; then
|
|
540
|
+
exec "$REAL_BIN" "$@"
|
|
541
|
+
fi
|
|
542
|
+
|
|
543
|
+
sanitize_peer_part() {
|
|
544
|
+
printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9._-]/-/g; s/^-*//; s/-*$//' | cut -c 1-32
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
tty_peer_part() {
|
|
548
|
+
TTY_NUM=$(tty 2>/dev/null | tr -dc '0-9' | tail -c 4)
|
|
549
|
+
PARENT_PID="\${PPID:-$$}"
|
|
550
|
+
if [ -n "$TTY_NUM" ]; then
|
|
551
|
+
printf '%s-%s' "$TTY_NUM" "$PARENT_PID"
|
|
552
|
+
else
|
|
553
|
+
printf '%s' "$PARENT_PID"
|
|
554
|
+
fi
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
SUBCMD=""
|
|
558
|
+
RESUME_ID=""
|
|
559
|
+
EXPECT_SESSION=0
|
|
560
|
+
for arg in "$@"; do
|
|
561
|
+
if [ -z "$SUBCMD" ]; then
|
|
562
|
+
if [ "$arg" = "resume" ] || [ "$arg" = "fork" ]; then
|
|
563
|
+
SUBCMD="$arg"
|
|
564
|
+
EXPECT_SESSION=1
|
|
565
|
+
fi
|
|
566
|
+
continue
|
|
567
|
+
fi
|
|
568
|
+
if [ "$EXPECT_SESSION" = "1" ]; then
|
|
569
|
+
case "$arg" in
|
|
570
|
+
--last) EXPECT_SESSION=0 ;;
|
|
571
|
+
-*) ;;
|
|
572
|
+
*) RESUME_ID="$arg"; EXPECT_SESSION=0 ;;
|
|
573
|
+
esac
|
|
574
|
+
fi
|
|
575
|
+
done
|
|
576
|
+
|
|
577
|
+
if [ -n "$HCC_PEER" ]; then
|
|
578
|
+
PEER_ID="$HCC_PEER"
|
|
579
|
+
elif [ "$SUBCMD" = "resume" ] && [ -n "$RESUME_ID" ]; then
|
|
580
|
+
PEER_ID="${tool.kind}-\$(sanitize_peer_part "\${RESUME_ID:0:8}")"
|
|
581
|
+
else
|
|
582
|
+
PEER_ID="${tool.kind}-\$(tty_peer_part)"
|
|
583
|
+
fi
|
|
584
|
+
|
|
585
|
+
export HCC_PEER="$PEER_ID"
|
|
586
|
+
export HCC_ROOT
|
|
587
|
+
|
|
588
|
+
current_tmux_pane() {
|
|
589
|
+
if [ -n "\${TMUX_PANE:-}" ]; then
|
|
590
|
+
printf '%s\\n' "$TMUX_PANE"
|
|
591
|
+
return 0
|
|
592
|
+
fi
|
|
593
|
+
TTY_PATH=$(tty 2>/dev/null || true)
|
|
594
|
+
if [ -z "$TTY_PATH" ] || [ "$TTY_PATH" = "not a tty" ]; then
|
|
595
|
+
return 1
|
|
596
|
+
fi
|
|
597
|
+
tmux list-panes -a -F '#{pane_id} #{pane_tty}' 2>/dev/null | awk -v tty="$TTY_PATH" '$2 == tty { print $1; found = 1; exit } END { exit found ? 0 : 1 }'
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
CURRENT_HCC_PANE=""
|
|
601
|
+
CURRENT_HCC_SESSION=""
|
|
602
|
+
detect_current_hcc_pane() {
|
|
603
|
+
CURRENT_HCC_SESSION=""
|
|
604
|
+
CURRENT_HCC_PANE=$(current_tmux_pane || true)
|
|
605
|
+
if [ -z "$CURRENT_HCC_PANE" ]; then
|
|
606
|
+
return 1
|
|
607
|
+
fi
|
|
608
|
+
CURRENT_HCC_SESSION=$(tmux display-message -p -t "$CURRENT_HCC_PANE" '#S' 2>/dev/null || true)
|
|
609
|
+
case "$CURRENT_HCC_SESSION" in
|
|
610
|
+
hcc-*-*) return 0 ;;
|
|
611
|
+
esac
|
|
612
|
+
CURRENT_HCC_PANE=""
|
|
613
|
+
CURRENT_HCC_SESSION=""
|
|
614
|
+
return 1
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
if detect_current_hcc_pane; then
|
|
618
|
+
SESSION_PEER_ID=$(printf '%s' "$CURRENT_HCC_SESSION" | sed 's/^hcc-[^-]*-//')
|
|
619
|
+
if [ -n "$SESSION_PEER_ID" ] && [ "$SESSION_PEER_ID" != "$CURRENT_HCC_SESSION" ]; then
|
|
620
|
+
PEER_ID="$SESSION_PEER_ID"
|
|
621
|
+
fi
|
|
622
|
+
export HCC_PEER="$PEER_ID"
|
|
623
|
+
"$HCC_BIN" web >/dev/null 2>&1 || true
|
|
624
|
+
"$HCC_BIN" peer attach "$PEER_ID" --kind "${tool.kind}" --pane "$CURRENT_HCC_PANE" --cwd "$PWD" --force >/dev/null 2>&1 || true
|
|
625
|
+
exec "$REAL_BIN" "$@"
|
|
626
|
+
fi
|
|
627
|
+
|
|
628
|
+
set +e
|
|
629
|
+
# Wrap in shell so pane_pid stays as bash; Ctrl+C drops back to bash
|
|
630
|
+
START_OUT=$("$HCC_BIN" web >/dev/null 2>&1 || true; "$HCC_BIN" peer start "$PEER_ID" --kind "${tool.kind}" --restart-env -- bash -lc 'REAL_BIN="$1"; shift; hcc_provider_state() { HCC_TMUX_SESSION=$(tmux display-message -p "#S" 2>/dev/null || true); if [ -n "$HCC_TMUX_SESSION" ]; then tmux set-environment -t "$HCC_TMUX_SESSION" HCC_PROVIDER_STATE "$1" >/dev/null 2>&1 || true; fi; }; hcc_provider_state running; "$REAL_BIN" "$@"; hcc_provider_state exited; exec bash' _ "$REAL_BIN" "$@" 2>&1)
|
|
631
|
+
START_STATUS=$?
|
|
632
|
+
set -e
|
|
633
|
+
if [ "$START_STATUS" -ne 0 ]; then
|
|
634
|
+
printf '%s\\n' "$START_OUT" >&2
|
|
635
|
+
exit "$START_STATUS"
|
|
636
|
+
fi
|
|
637
|
+
|
|
638
|
+
PANE=$(printf '%s\\n' "$START_OUT" | sed -n 's/.*pane=\\(%[0-9][0-9]*\\).*/\\1/p' | tail -n 1)
|
|
639
|
+
if [ -z "$PANE" ]; then
|
|
640
|
+
printf '%s\\n' "$START_OUT"
|
|
641
|
+
exit 0
|
|
642
|
+
fi
|
|
643
|
+
|
|
644
|
+
if [ "$HCC_SHIM_NO_ATTACH" = "1" ] || [ ! -t 0 ]; then
|
|
645
|
+
printf '%s\\n' "$START_OUT"
|
|
646
|
+
exit 0
|
|
647
|
+
fi
|
|
648
|
+
|
|
649
|
+
SESSION=$(tmux display-message -p -t "$PANE" '#S' 2>/dev/null || true)
|
|
650
|
+
if [ -z "$SESSION" ]; then
|
|
651
|
+
printf '%s\\n' "$START_OUT"
|
|
652
|
+
exit 0
|
|
653
|
+
fi
|
|
654
|
+
|
|
655
|
+
if [ -n "$TMUX" ]; then
|
|
656
|
+
exec tmux switch-client -t "$SESSION"
|
|
657
|
+
fi
|
|
658
|
+
|
|
659
|
+
exec tmux attach-session -t "$SESSION"
|
|
660
|
+
`;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// ─── Codex hooks ─────────────────────────────────────────────────────────────
|
|
665
|
+
|
|
666
|
+
const CODEX_HOOKS_PATH = path.join(os.homedir(), '.codex', 'hooks.json');
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Install hello-cc hooks in ~/.codex/hooks.json.
|
|
670
|
+
*
|
|
671
|
+
* Codex hooks.json format:
|
|
672
|
+
* { "hooks": { "PreToolUse": [{ "matcher": "Bash", "hooks": [...] }], "Stop": [...] } }
|
|
673
|
+
*/
|
|
674
|
+
export function installCodexHooks(hccBin) {
|
|
675
|
+
const hooks = readJsonSafe(CODEX_HOOKS_PATH) || {};
|
|
676
|
+
|
|
677
|
+
if (!hooks.hooks) hooks.hooks = {};
|
|
678
|
+
|
|
679
|
+
const hookCmd = `${hccBin} hook`;
|
|
680
|
+
|
|
681
|
+
// SessionStart: inject initial coordination context on startup/resume.
|
|
682
|
+
hooks.hooks.SessionStart = mergeCodexHookEntry(
|
|
683
|
+
hooks.hooks.SessionStart,
|
|
684
|
+
`${hookCmd} sessionstart`,
|
|
685
|
+
null
|
|
686
|
+
);
|
|
687
|
+
// UserPromptSubmit: inject fresh coordination context before each model turn.
|
|
688
|
+
hooks.hooks.UserPromptSubmit = mergeCodexHookEntry(
|
|
689
|
+
hooks.hooks.UserPromptSubmit,
|
|
690
|
+
`${hookCmd} userpromptsubmit`,
|
|
691
|
+
null
|
|
692
|
+
);
|
|
693
|
+
// PreToolUse: fires before every Bash tool call — heartbeat + peer registration
|
|
694
|
+
hooks.hooks.PreToolUse = mergeCodexHookEntry(
|
|
695
|
+
hooks.hooks.PreToolUse,
|
|
696
|
+
`${hookCmd} pretooluse`,
|
|
697
|
+
'Bash'
|
|
698
|
+
);
|
|
699
|
+
// Stop: fires when Codex goes idle — deliver inbox messages
|
|
700
|
+
hooks.hooks.Stop = mergeCodexHookEntry(
|
|
701
|
+
hooks.hooks.Stop,
|
|
702
|
+
`${hookCmd} stop`,
|
|
703
|
+
null
|
|
704
|
+
);
|
|
705
|
+
|
|
706
|
+
writeJsonSafe(CODEX_HOOKS_PATH, hooks);
|
|
707
|
+
return CODEX_HOOKS_PATH;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
export function uninstallCodexHooks() {
|
|
711
|
+
const hooks = readJsonSafe(CODEX_HOOKS_PATH);
|
|
712
|
+
if (!hooks?.hooks) return false;
|
|
713
|
+
|
|
714
|
+
for (const event of Object.keys(hooks.hooks)) {
|
|
715
|
+
if (!Array.isArray(hooks.hooks[event])) continue;
|
|
716
|
+
hooks.hooks[event] = hooks.hooks[event].filter(entry => {
|
|
717
|
+
if (!Array.isArray(entry?.hooks)) return true;
|
|
718
|
+
return !entry.hooks.some(h => isHccHookCmd(h?.command));
|
|
719
|
+
});
|
|
720
|
+
if (hooks.hooks[event].length === 0) delete hooks.hooks[event];
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
writeJsonSafe(CODEX_HOOKS_PATH, hooks);
|
|
724
|
+
return true;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
export function verifyCodexHooks() {
|
|
728
|
+
const hooks = readJsonSafe(CODEX_HOOKS_PATH);
|
|
729
|
+
if (!hooks?.hooks) return false;
|
|
730
|
+
return ['SessionStart', 'UserPromptSubmit', 'PreToolUse', 'Stop'].every(event =>
|
|
731
|
+
hasHookEntry(hooks.hooks[event])
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
function mergeCodexHookEntry(existing, command, matcher) {
|
|
736
|
+
const entries = Array.isArray(existing) ? [...existing] : [];
|
|
737
|
+
const alreadyInstalled = entries.some(
|
|
738
|
+
e => Array.isArray(e?.hooks) && e.hooks.some(h => isHccHookCmd(h?.command))
|
|
739
|
+
);
|
|
740
|
+
if (alreadyInstalled) return entries;
|
|
741
|
+
|
|
742
|
+
const entry = {
|
|
743
|
+
hooks: [{ type: 'command', command }],
|
|
744
|
+
};
|
|
745
|
+
if (matcher) entry.matcher = matcher;
|
|
746
|
+
entries.push(entry);
|
|
747
|
+
return entries;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// ─── Utilities ────────────────────────────────────────────────────────────────
|
|
751
|
+
|
|
752
|
+
function fsExists(p) {
|
|
753
|
+
try { fs.accessSync(p); return true; } catch { return false; }
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function readJsonSafe(filePath) {
|
|
757
|
+
try { return JSON.parse(fs.readFileSync(filePath, 'utf8')); }
|
|
758
|
+
catch { return null; }
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function writeJsonSafe(filePath, data) {
|
|
762
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
763
|
+
const tmpPath = filePath + '.tmp.' + process.pid;
|
|
764
|
+
fs.writeFileSync(tmpPath, JSON.stringify(data, null, 2) + '\n');
|
|
765
|
+
fs.renameSync(tmpPath, filePath);
|
|
766
|
+
}
|