@logicseek/hello-cc 0.1.7 → 0.1.8
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 +49 -0
- package/README.md +12 -6
- package/README.zh-CN.md +10 -6
- package/bin/hcc.mjs +341 -54
- package/docs/commands.md +13 -6
- package/docs/commands.zh-CN.md +11 -6
- package/docs/guide.md +10 -5
- package/docs/guide.zh-CN.md +8 -5
- package/lib/core/coordination/timeline.mjs +1 -0
- package/lib/core/sessions/launch.mjs +1 -0
- package/lib/integrations/shims/script.mjs +42 -22
- package/lib/integrations/shims.mjs +45 -6
- package/lib/setup.mjs +1 -0
- package/lib/shell-path.mjs +31 -16
- package/lib/ui/help.mjs +36 -1
- package/package.json +1 -1
- package/scripts/regression.mjs +401 -48
package/bin/hcc.mjs
CHANGED
|
@@ -227,6 +227,8 @@ const {
|
|
|
227
227
|
helpDown,
|
|
228
228
|
helpUpdate,
|
|
229
229
|
helpUninstall,
|
|
230
|
+
helpInstallHooks,
|
|
231
|
+
helpShim,
|
|
230
232
|
helpWeb
|
|
231
233
|
} = createHelpFunctions({
|
|
232
234
|
productName: PRODUCT_NAME,
|
|
@@ -279,7 +281,11 @@ function splitProcessArgs(line) {
|
|
|
279
281
|
|
|
280
282
|
function sameResolvedPath(a, b) {
|
|
281
283
|
if (!a || !b) return false;
|
|
282
|
-
|
|
284
|
+
function key(value) {
|
|
285
|
+
try { return fs.realpathSync(value); }
|
|
286
|
+
catch { return path.resolve(value); }
|
|
287
|
+
}
|
|
288
|
+
return key(a) === key(b);
|
|
283
289
|
}
|
|
284
290
|
|
|
285
291
|
const {
|
|
@@ -731,6 +737,7 @@ async function cmdTask(ctx, args) {
|
|
|
731
737
|
const sub = args[0];
|
|
732
738
|
if (!sub || wantsHelp(args)) return helpTask();
|
|
733
739
|
if (sub === 'create') return taskCreate(ctx, args.slice(1));
|
|
740
|
+
if (sub === 'dispatch') return taskDispatch(ctx, args.slice(1));
|
|
734
741
|
if (sub === 'list') return taskList(ctx, args.slice(1));
|
|
735
742
|
if (sub === 'claim') return taskClaim(ctx, args.slice(1));
|
|
736
743
|
if (sub === 'takeover') return taskTakeover(ctx, args.slice(1));
|
|
@@ -772,6 +779,201 @@ async function taskCreate(ctx, args) {
|
|
|
772
779
|
(data) => `created task #${data.id}: ${data.title}${data.assignee ? ` -> ${data.assignee}` : ''}${data.parent_id ? ` (child of #${data.parent_id})` : ''}`);
|
|
773
780
|
}
|
|
774
781
|
|
|
782
|
+
function dispatchPromptText(task, customMessage = null) {
|
|
783
|
+
if (customMessage) return customMessage;
|
|
784
|
+
return [
|
|
785
|
+
`Please pick up hello-cc task #${task.id}: ${task.title}.`,
|
|
786
|
+
`Run hcc task claim --id ${task.id}, then follow project coordination rules, create a handoff, and mark the task done when finished.`
|
|
787
|
+
].join(' ');
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function currentOwnedTaskForPeer(db, peer) {
|
|
791
|
+
return db.prepare(`
|
|
792
|
+
SELECT *
|
|
793
|
+
FROM tasks
|
|
794
|
+
WHERE owner = ?
|
|
795
|
+
AND status IN ('claimed', 'running', 'review', 'blocked')
|
|
796
|
+
ORDER BY
|
|
797
|
+
CASE status
|
|
798
|
+
WHEN 'running' THEN 0
|
|
799
|
+
WHEN 'claimed' THEN 1
|
|
800
|
+
WHEN 'review' THEN 2
|
|
801
|
+
WHEN 'blocked' THEN 3
|
|
802
|
+
ELSE 4
|
|
803
|
+
END,
|
|
804
|
+
priority ASC,
|
|
805
|
+
id ASC
|
|
806
|
+
LIMIT 1
|
|
807
|
+
`).get(peer);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
function findRuntimeSessionForPeer(runtimeData, peer) {
|
|
811
|
+
return (runtimeData?.sessions || []).find((session) => {
|
|
812
|
+
const sessionPeer = session.peer_id || session.id;
|
|
813
|
+
return session.status === 'running' && (session.id === peer || sessionPeer === peer);
|
|
814
|
+
}) || null;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function sessionLooksProviderInteractive(session) {
|
|
818
|
+
if (!['claude', 'codex'].includes(session?.kind)) return false;
|
|
819
|
+
if (session.provider_session_known) return true;
|
|
820
|
+
const command = String(session.command || '');
|
|
821
|
+
if (/^tmux\s+%/.test(command)) return false;
|
|
822
|
+
return /\b(?:claude|codex)(?:\s|$)/.test(command);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
async function taskDispatch(ctx, args) {
|
|
826
|
+
const opts = parseOpts(args, { booleans: ['force', 'no-inject'] });
|
|
827
|
+
const identity = resolveCurrentPeer(ctx, opts, 'from', 'shell');
|
|
828
|
+
const actor = identity.id;
|
|
829
|
+
const target = required(opts, 'to');
|
|
830
|
+
const requestedTaskId = intOpt(opts, 'id', intOpt({ id: opts._[0] }, 'id'));
|
|
831
|
+
const title = requestedTaskId ? (opts.title || null) : required(opts, 'title');
|
|
832
|
+
const body = opts.body || '';
|
|
833
|
+
const priority = intOpt(opts, 'priority', 100);
|
|
834
|
+
const customMessage = opts.message ? String(opts.message) : null;
|
|
835
|
+
const injectAllowed = !Boolean(opts['no-inject']);
|
|
836
|
+
|
|
837
|
+
let task = null;
|
|
838
|
+
let messageId = null;
|
|
839
|
+
let currentTask = null;
|
|
840
|
+
let previousAssignee = null;
|
|
841
|
+
const db = connect(ctx);
|
|
842
|
+
try {
|
|
843
|
+
touchCurrentPeer(db, ctx, identity, null, 'shell');
|
|
844
|
+
const t = now();
|
|
845
|
+
task = tx(db, () => {
|
|
846
|
+
if (requestedTaskId) {
|
|
847
|
+
const existing = db.prepare('SELECT * FROM tasks WHERE id = ?').get(requestedTaskId);
|
|
848
|
+
if (!existing) throw new CliError('NOT_FOUND', `Task #${requestedTaskId} does not exist`);
|
|
849
|
+
if (['done', 'abandoned'].includes(existing.status)) {
|
|
850
|
+
throw new CliError('BAD_STATE', `Task #${requestedTaskId} is ${existing.status}`);
|
|
851
|
+
}
|
|
852
|
+
if (existing.owner && existing.owner !== target) {
|
|
853
|
+
throw new CliError('TASK_OWNED', `Task #${requestedTaskId} is owned by ${existing.owner}`, {
|
|
854
|
+
owner: existing.owner,
|
|
855
|
+
task_id: requestedTaskId,
|
|
856
|
+
attempted_by: actor,
|
|
857
|
+
target
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
previousAssignee = existing.assignee || null;
|
|
861
|
+
db.prepare('UPDATE tasks SET assignee = ?, updated_at = ? WHERE id = ?').run(target, t, requestedTaskId);
|
|
862
|
+
return db.prepare('SELECT * FROM tasks WHERE id = ?').get(requestedTaskId);
|
|
863
|
+
}
|
|
864
|
+
const info = db.prepare(`
|
|
865
|
+
INSERT INTO tasks(title, body, status, assignee, owner, parent_id, team_role, priority, created_by, created_at, updated_at)
|
|
866
|
+
VALUES (?, ?, 'pending', ?, NULL, NULL, NULL, ?, ?, ?, ?)
|
|
867
|
+
`).run(title, body, target, priority, actor, t, t);
|
|
868
|
+
const taskId = Number(info.lastInsertRowid);
|
|
869
|
+
addEvent(db, 'task.created', actor, taskId, { title, assignee: target, priority, parent_id: null, team_role: null });
|
|
870
|
+
return db.prepare('SELECT * FROM tasks WHERE id = ?').get(taskId);
|
|
871
|
+
});
|
|
872
|
+
const durableMessage = dispatchPromptText(task, customMessage);
|
|
873
|
+
messageId = sendMessage(db, actor, target, task.id, 'task', durableMessage);
|
|
874
|
+
currentTask = currentOwnedTaskForPeer(db, target);
|
|
875
|
+
} finally {
|
|
876
|
+
db.close();
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
const durableMessage = dispatchPromptText(task, customMessage);
|
|
880
|
+
let session = null;
|
|
881
|
+
let injected = false;
|
|
882
|
+
let injectionReason = injectAllowed ? 'runtime_unavailable' : 'no_inject';
|
|
883
|
+
const busyTask = currentTask && Number(currentTask.id) !== Number(task.id) ? currentTask : null;
|
|
884
|
+
if (injectAllowed) {
|
|
885
|
+
let runtime = null;
|
|
886
|
+
let runtimeData = null;
|
|
887
|
+
try {
|
|
888
|
+
runtime = readRuntime(ctx);
|
|
889
|
+
runtimeData = await runtimeRequest(ctx, 'GET', '/api/sessions', null, runtime);
|
|
890
|
+
} catch (err) {
|
|
891
|
+
if (!(err instanceof CliError) || !['RUNTIME_NOT_RUNNING', 'RUNTIME_UNREACHABLE'].includes(err.code)) throw err;
|
|
892
|
+
}
|
|
893
|
+
session = runtimeData ? findRuntimeSessionForPeer(runtimeData, target) : null;
|
|
894
|
+
if (!session) {
|
|
895
|
+
injectionReason = runtimeData ? 'session_not_running' : 'runtime_unavailable';
|
|
896
|
+
} else if (!customMessage && !sessionLooksProviderInteractive(session)) {
|
|
897
|
+
injectionReason = 'unsupported_session_kind';
|
|
898
|
+
} else if (busyTask && !Boolean(opts.force)) {
|
|
899
|
+
injectionReason = 'target_busy';
|
|
900
|
+
} else {
|
|
901
|
+
try {
|
|
902
|
+
await injectPeer(ctx, target, durableMessage, true, runtime, actor);
|
|
903
|
+
injected = true;
|
|
904
|
+
injectionReason = 'injected';
|
|
905
|
+
} catch (err) {
|
|
906
|
+
if (!(err instanceof CliError)) throw err;
|
|
907
|
+
if (['RUNTIME_NOT_RUNNING', 'RUNTIME_UNREACHABLE'].includes(err.code)) {
|
|
908
|
+
injectionReason = 'runtime_unavailable';
|
|
909
|
+
} else if (['NOT_FOUND', 'SESSION_NOT_RUNNING'].includes(err.code)) {
|
|
910
|
+
injectionReason = 'session_not_running';
|
|
911
|
+
} else {
|
|
912
|
+
throw err;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
const eventDb = connect(ctx);
|
|
919
|
+
try {
|
|
920
|
+
addEvent(eventDb, 'task.dispatched', actor, task.id, auditPayload({
|
|
921
|
+
actor,
|
|
922
|
+
target,
|
|
923
|
+
source: 'cli',
|
|
924
|
+
admin: actor !== target,
|
|
925
|
+
peer: target,
|
|
926
|
+
title: task.title,
|
|
927
|
+
message_id: messageId,
|
|
928
|
+
injected,
|
|
929
|
+
delivery: injected ? 'message+inject' : 'message-only',
|
|
930
|
+
injection_reason: injectionReason,
|
|
931
|
+
session_id: session?.id || null,
|
|
932
|
+
session_kind: session?.kind || null,
|
|
933
|
+
previous_assignee: previousAssignee,
|
|
934
|
+
blocked_by_task_id: busyTask?.id || null
|
|
935
|
+
}));
|
|
936
|
+
} finally {
|
|
937
|
+
eventDb.close();
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
const result = {
|
|
941
|
+
task,
|
|
942
|
+
target,
|
|
943
|
+
message_id: messageId,
|
|
944
|
+
message: durableMessage,
|
|
945
|
+
injected,
|
|
946
|
+
delivery: injected ? 'message+inject' : 'message-only',
|
|
947
|
+
injection_reason: injectionReason,
|
|
948
|
+
session: session ? {
|
|
949
|
+
id: session.id,
|
|
950
|
+
peer_id: session.peer_id || session.id,
|
|
951
|
+
kind: session.kind,
|
|
952
|
+
status: session.status
|
|
953
|
+
} : null,
|
|
954
|
+
previous_assignee: previousAssignee,
|
|
955
|
+
blocked_by_task: busyTask ? {
|
|
956
|
+
id: busyTask.id,
|
|
957
|
+
status: busyTask.status,
|
|
958
|
+
title: busyTask.title
|
|
959
|
+
} : null
|
|
960
|
+
};
|
|
961
|
+
printResult(ctx, result, (data) => {
|
|
962
|
+
const base = `dispatched task #${data.task.id} to ${data.target} with message #${data.message_id}`;
|
|
963
|
+
if (data.injected) return `${base} and injected live input`;
|
|
964
|
+
if (data.injection_reason === 'target_busy' && data.blocked_by_task) {
|
|
965
|
+
return `${base} (not injected: ${data.target} already owns task #${data.blocked_by_task.id})`;
|
|
966
|
+
}
|
|
967
|
+
if (data.injection_reason === 'unsupported_session_kind' && data.session) {
|
|
968
|
+
return `${base} (not injected: managed ${data.session.kind} session needs an explicit shell-safe message)`;
|
|
969
|
+
}
|
|
970
|
+
if (data.injection_reason === 'session_not_running') return `${base} (not injected: target is not a running managed session)`;
|
|
971
|
+
if (data.injection_reason === 'runtime_unavailable') return `${base} (not injected: web runtime is unavailable)`;
|
|
972
|
+
if (data.injection_reason === 'no_inject') return `${base} (message only)`;
|
|
973
|
+
return `${base} (not injected: ${data.injection_reason})`;
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
|
|
775
977
|
async function taskList(ctx, args) {
|
|
776
978
|
const opts = parseOpts(args, { booleans: ['all'] });
|
|
777
979
|
const status = opts.status || null;
|
|
@@ -1195,7 +1397,7 @@ async function cmdAsk(ctx, args) {
|
|
|
1195
1397
|
const id = sendMessage(db, sender, recipient, taskId, kind, body);
|
|
1196
1398
|
let injected = false;
|
|
1197
1399
|
if (opts.inject) {
|
|
1198
|
-
await injectPeer(ctx, recipient, body, !opts['no-enter'], runtime);
|
|
1400
|
+
await injectPeer(ctx, recipient, body, !opts['no-enter'], runtime, sender);
|
|
1199
1401
|
injected = true;
|
|
1200
1402
|
}
|
|
1201
1403
|
printResult(ctx, { id, sender, recipient, task_id: taskId, kind, body, injected }, (data) => `asked ${data.recipient} with message #${data.id}${data.injected ? ' and injected terminal input' : ''}`);
|
|
@@ -1219,15 +1421,15 @@ async function cmdBroadcast(ctx, args) {
|
|
|
1219
1421
|
const sessions = await runtimeRequest(ctx, 'GET', '/api/sessions', null, runtime);
|
|
1220
1422
|
const running = (sessions.sessions || []).filter((session) => session.status === 'running');
|
|
1221
1423
|
for (const session of running) {
|
|
1222
|
-
await injectPeer(ctx, session.id, body, !opts['no-enter'], runtime);
|
|
1424
|
+
await injectPeer(ctx, session.id, body, !opts['no-enter'], runtime, sender);
|
|
1223
1425
|
injected += 1;
|
|
1224
1426
|
}
|
|
1225
1427
|
}
|
|
1226
1428
|
printResult(ctx, { id, sender, recipient: 'all', task_id: taskId, kind, body, injected }, (data) => `broadcast message #${data.id}${data.injected ? ` and injected ${data.injected} terminal(s)` : ''}`);
|
|
1227
1429
|
}
|
|
1228
1430
|
|
|
1229
|
-
async function injectPeer(ctx, peer, text, enter = true, runtime = null) {
|
|
1230
|
-
const actor = resolveCurrentPeer(ctx, {}, 'peer', 'shell').id;
|
|
1431
|
+
async function injectPeer(ctx, peer, text, enter = true, runtime = null, auditActor = null) {
|
|
1432
|
+
const actor = auditActor || resolveCurrentPeer(ctx, {}, 'peer', 'shell').id;
|
|
1231
1433
|
const db = connect(ctx);
|
|
1232
1434
|
try {
|
|
1233
1435
|
addEvent(db, 'web.session.input.requested', actor, null, auditPayload({
|
|
@@ -1731,6 +1933,7 @@ async function cmdUp(ctx, args) {
|
|
|
1731
1933
|
if (r.hooks.claudeInstalled) lines.push('Claude Code hooks installed');
|
|
1732
1934
|
if (r.hooks.codexInstalled) lines.push('Codex hooks installed');
|
|
1733
1935
|
if (r.detected.length) lines.push(`detected: ${r.detected.map((s) => s.peerId).join(', ')}`);
|
|
1936
|
+
if (r.warnings?.length) lines.push(...r.warnings.map((warning) => `warning: ${warning}`));
|
|
1734
1937
|
lines.push('web: run hcc web when you need browser terminal control');
|
|
1735
1938
|
return lines.join('\n');
|
|
1736
1939
|
});
|
|
@@ -1747,31 +1950,42 @@ async function prepareLocalBus(ctx, opts = {}) {
|
|
|
1747
1950
|
|
|
1748
1951
|
const hooks = { claudeInstalled: false, codexInstalled: false };
|
|
1749
1952
|
const shims = { installed: [], skipped: [], pathUpdated: false, rcFile: null };
|
|
1953
|
+
const warnings = [];
|
|
1750
1954
|
try {
|
|
1751
|
-
const
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
}
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1955
|
+
const setup = await loadSetup();
|
|
1956
|
+
try {
|
|
1957
|
+
if (!setup.verifyClaudeHooks()) {
|
|
1958
|
+
setup.installClaudeHooks(commandPath());
|
|
1959
|
+
hooks.claudeInstalled = true;
|
|
1960
|
+
}
|
|
1961
|
+
} catch (err) {
|
|
1962
|
+
warnings.push(`Claude Code hooks installation failed: ${err.message}`);
|
|
1963
|
+
}
|
|
1964
|
+
try {
|
|
1965
|
+
if (!setup.verifyCodexHooks()) {
|
|
1966
|
+
setup.installCodexHooks(commandPath());
|
|
1761
1967
|
hooks.codexInstalled = true;
|
|
1762
|
-
}
|
|
1968
|
+
}
|
|
1969
|
+
} catch (err) {
|
|
1970
|
+
warnings.push(`Codex hooks installation failed: ${err.message}`);
|
|
1763
1971
|
}
|
|
1764
1972
|
if (opts.installShims) {
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1973
|
+
try {
|
|
1974
|
+
const result = setup.installShims(commandPath());
|
|
1975
|
+
shims.installed = result.installed;
|
|
1976
|
+
shims.skipped = result.skipped;
|
|
1977
|
+
if (result.installed.length) {
|
|
1978
|
+
const pathResult = setup.installPathEntry();
|
|
1979
|
+
shims.pathUpdated = !pathResult.alreadyPresent;
|
|
1980
|
+
shims.rcFile = pathResult.rcFile;
|
|
1981
|
+
}
|
|
1982
|
+
} catch (err) {
|
|
1983
|
+
warnings.push(`shim installation failed: ${err.message}`);
|
|
1772
1984
|
}
|
|
1773
1985
|
}
|
|
1774
|
-
} catch {
|
|
1986
|
+
} catch (err) {
|
|
1987
|
+
warnings.push(`local integration setup failed: ${err.message}`);
|
|
1988
|
+
}
|
|
1775
1989
|
|
|
1776
1990
|
const detected = [];
|
|
1777
1991
|
if (!opts['no-discover']) {
|
|
@@ -1781,7 +1995,7 @@ async function prepareLocalBus(ctx, opts = {}) {
|
|
|
1781
1995
|
...scanClaudeSessions(),
|
|
1782
1996
|
...scanCodexSessions(),
|
|
1783
1997
|
...scanProcesses(),
|
|
1784
|
-
].filter((s) => s.hccRoot
|
|
1998
|
+
].filter((s) => sameResolvedPath(s.hccRoot, ctx.root));
|
|
1785
1999
|
const byId = new Map();
|
|
1786
2000
|
for (const s of found) {
|
|
1787
2001
|
if (!byId.has(s.peerId)) byId.set(s.peerId, s);
|
|
@@ -1814,7 +2028,8 @@ async function prepareLocalBus(ctx, opts = {}) {
|
|
|
1814
2028
|
guidance,
|
|
1815
2029
|
hooks,
|
|
1816
2030
|
shims,
|
|
1817
|
-
detected
|
|
2031
|
+
detected,
|
|
2032
|
+
warnings
|
|
1818
2033
|
};
|
|
1819
2034
|
}
|
|
1820
2035
|
|
|
@@ -1870,7 +2085,10 @@ async function startWebBackground(ctx, args) {
|
|
|
1870
2085
|
validateOpts('web', opts, ['host', 'port', 'token', 'local', 'no-token', 'no-guidance', 'no-discover']);
|
|
1871
2086
|
validateWebTokenOpts(opts);
|
|
1872
2087
|
ensureTmuxAvailable({ autoInstall: true });
|
|
1873
|
-
const setup = await prepareLocalBus(ctx, {
|
|
2088
|
+
const setup = await prepareLocalBus(ctx, {
|
|
2089
|
+
...opts,
|
|
2090
|
+
installShims: process.env.HCC_SKIP_SHIM_INSTALL === '1' ? false : true
|
|
2091
|
+
});
|
|
1874
2092
|
registerProject(ctx);
|
|
1875
2093
|
|
|
1876
2094
|
const existing = await readHealthyGlobalRuntime();
|
|
@@ -1994,6 +2212,9 @@ function printWebRuntime(ctx, runtime, opts = {}) {
|
|
|
1994
2212
|
lines.push(`PATH updated in ${opts.setup.shims.rcFile}; open a new terminal or source it`);
|
|
1995
2213
|
}
|
|
1996
2214
|
}
|
|
2215
|
+
if (opts.setup?.warnings?.length) {
|
|
2216
|
+
lines.push(...opts.setup.warnings.map((warning) => `warning: ${warning}`));
|
|
2217
|
+
}
|
|
1997
2218
|
lines.push(`stop: ${r.stop}`);
|
|
1998
2219
|
return lines.join('\n');
|
|
1999
2220
|
});
|
|
@@ -2041,7 +2262,7 @@ async function cmdWeb(ctx, args, startMeta = {}) {
|
|
|
2041
2262
|
|
|
2042
2263
|
function knownProjects() {
|
|
2043
2264
|
const rows = readProjectRegistry();
|
|
2044
|
-
if (!rows.some((p) => p.root
|
|
2265
|
+
if (!rows.some((p) => sameResolvedPath(p.root, ctx.root))) rows.unshift(projectRecord(ctx));
|
|
2045
2266
|
for (const project of rows) {
|
|
2046
2267
|
if (!projectContexts.has(project.root)) {
|
|
2047
2268
|
projectContexts.set(project.root, contextForProject(project.root, project.db, { json: ctx.json }));
|
|
@@ -2066,7 +2287,7 @@ async function cmdWeb(ctx, args, startMeta = {}) {
|
|
|
2066
2287
|
}
|
|
2067
2288
|
|
|
2068
2289
|
function sessionsForProject(projectCtx) {
|
|
2069
|
-
return [...sessions.values()].filter((session) => session.root
|
|
2290
|
+
return [...sessions.values()].filter((session) => sameResolvedPath(session.root, projectCtx.root));
|
|
2070
2291
|
}
|
|
2071
2292
|
|
|
2072
2293
|
function getSession(projectCtx, id, db = null) {
|
|
@@ -2538,6 +2759,18 @@ async function cmdWeb(ctx, args, startMeta = {}) {
|
|
|
2538
2759
|
return open;
|
|
2539
2760
|
}
|
|
2540
2761
|
|
|
2762
|
+
function closeSessionClients(session) {
|
|
2763
|
+
if (!session?.clients?.size) return;
|
|
2764
|
+
for (const client of [...session.clients]) {
|
|
2765
|
+
try {
|
|
2766
|
+
if (client.readyState === client.OPEN || client.readyState === 1) client.close(1001, 'runtime stopping');
|
|
2767
|
+
else if (typeof client.terminate === 'function') client.terminate();
|
|
2768
|
+
} catch {
|
|
2769
|
+
try { if (typeof client.terminate === 'function') client.terminate(); } catch {}
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
2773
|
+
|
|
2541
2774
|
function detachTmuxSession(session, status = 'detached') {
|
|
2542
2775
|
stopTmuxStream(session);
|
|
2543
2776
|
if (session.exitPoller) { clearInterval(session.exitPoller); session.exitPoller = null; }
|
|
@@ -4089,11 +4322,15 @@ async function cmdWeb(ctx, args, startMeta = {}) {
|
|
|
4089
4322
|
});
|
|
4090
4323
|
});
|
|
4091
4324
|
|
|
4325
|
+
let shuttingDown = false;
|
|
4092
4326
|
function shutdown() {
|
|
4327
|
+
if (shuttingDown) return;
|
|
4328
|
+
shuttingDown = true;
|
|
4093
4329
|
clearRuntime(ctx);
|
|
4094
4330
|
clearInterval(externalScanPoller);
|
|
4095
4331
|
clearInterval(autoAttachPoller);
|
|
4096
4332
|
for (const session of sessions.values()) {
|
|
4333
|
+
closeSessionClients(session);
|
|
4097
4334
|
if (session.status !== 'running') continue;
|
|
4098
4335
|
if (session.type === 'external') {
|
|
4099
4336
|
try { if (session.outputFd) fs.closeSync(session.outputFd); } catch {}
|
|
@@ -4106,7 +4343,28 @@ async function cmdWeb(ctx, args, startMeta = {}) {
|
|
|
4106
4343
|
try { session.pty.kill(); } catch {}
|
|
4107
4344
|
}
|
|
4108
4345
|
}
|
|
4109
|
-
|
|
4346
|
+
try { wss.close(); } catch {}
|
|
4347
|
+
const terminateClients = setTimeout(() => {
|
|
4348
|
+
for (const session of sessions.values()) {
|
|
4349
|
+
for (const client of [...(session.clients || [])]) {
|
|
4350
|
+
try { if (typeof client.terminate === 'function') client.terminate(); } catch {}
|
|
4351
|
+
}
|
|
4352
|
+
}
|
|
4353
|
+
try { server.closeAllConnections?.(); } catch {}
|
|
4354
|
+
}, 250);
|
|
4355
|
+
const forceExit = setTimeout(() => process.exit(0), 1500);
|
|
4356
|
+
try {
|
|
4357
|
+
server.close(() => {
|
|
4358
|
+
clearTimeout(terminateClients);
|
|
4359
|
+
clearTimeout(forceExit);
|
|
4360
|
+
process.exit(0);
|
|
4361
|
+
});
|
|
4362
|
+
try { server.closeIdleConnections?.(); } catch {}
|
|
4363
|
+
} catch {
|
|
4364
|
+
clearTimeout(terminateClients);
|
|
4365
|
+
clearTimeout(forceExit);
|
|
4366
|
+
process.exit(0);
|
|
4367
|
+
}
|
|
4110
4368
|
}
|
|
4111
4369
|
process.once('SIGINT', shutdown);
|
|
4112
4370
|
process.once('SIGTERM', shutdown);
|
|
@@ -4556,6 +4814,7 @@ async function cmdHook(ctx, args) {
|
|
|
4556
4814
|
// ─── hcc install-hooks ───────────────────────────────────────────────────────
|
|
4557
4815
|
|
|
4558
4816
|
async function cmdInstallHooks(ctx, args) {
|
|
4817
|
+
if (wantsHelp(args)) return helpInstallHooks();
|
|
4559
4818
|
const opts = parseOpts(args, { booleans: ['uninstall', 'status'] });
|
|
4560
4819
|
const { installClaudeHooks, uninstallClaudeHooks, verifyClaudeHooks,
|
|
4561
4820
|
installCodexHooks, uninstallCodexHooks, verifyCodexHooks } = await loadSetup();
|
|
@@ -4585,11 +4844,20 @@ async function cmdInstallHooks(ctx, args) {
|
|
|
4585
4844
|
`hooks installed: claude → ${cp}${cxp ? `, codex → ${cxp}` : ''}`);
|
|
4586
4845
|
}
|
|
4587
4846
|
|
|
4847
|
+
function pathEntryRemovalMessage(pathEntry) {
|
|
4848
|
+
if (pathEntry.error) return `PATH entry not removed: ${pathEntry.error}`;
|
|
4849
|
+
if (pathEntry.missing) return `PATH entry not present (${pathEntry.rcFile} not found)`;
|
|
4850
|
+
if (pathEntry.removed === false) return `PATH entry not present in ${pathEntry.rcFile}`;
|
|
4851
|
+
return `PATH entry removed from ${pathEntry.rcFile}`;
|
|
4852
|
+
}
|
|
4853
|
+
|
|
4588
4854
|
// ─── hcc shim ────────────────────────────────────────────────────────────────
|
|
4589
4855
|
|
|
4590
4856
|
async function cmdShim(ctx, args) {
|
|
4591
4857
|
const sub = args[0];
|
|
4592
|
-
const { installShims, uninstallShims,
|
|
4858
|
+
const { installShims, uninstallShims, shimStatus, installPathEntry, uninstallPathEntry, SHIM_DIR } = await loadSetup();
|
|
4859
|
+
|
|
4860
|
+
if (wantsHelp(args)) return helpShim();
|
|
4593
4861
|
|
|
4594
4862
|
if (sub === 'ensure') {
|
|
4595
4863
|
const name = args[1];
|
|
@@ -4597,9 +4865,10 @@ async function cmdShim(ctx, args) {
|
|
|
4597
4865
|
if (!['claude', 'codex'].includes(name) || !target) {
|
|
4598
4866
|
throw new CliError('BAD_ARGS', 'Usage: hcc shim ensure claude|codex PATH');
|
|
4599
4867
|
}
|
|
4600
|
-
const
|
|
4868
|
+
const realBin = args[3] || null;
|
|
4869
|
+
const result = installShims(commandPath(), realBin ? { realBins: { [name]: realBin } } : {});
|
|
4601
4870
|
const changed = (result.changed || []).map((p) => path.resolve(p));
|
|
4602
|
-
if (changed.
|
|
4871
|
+
if (changed.some((p) => sameResolvedPath(p, target))) {
|
|
4603
4872
|
process.exitCode = 75;
|
|
4604
4873
|
return;
|
|
4605
4874
|
}
|
|
@@ -4609,32 +4878,45 @@ async function cmdShim(ctx, args) {
|
|
|
4609
4878
|
if (!sub || sub === 'install') {
|
|
4610
4879
|
const hccBin = commandPath();
|
|
4611
4880
|
const result = installShims(hccBin);
|
|
4612
|
-
const { alreadyPresent, rcFile } = installPathEntry();
|
|
4613
4881
|
const lines = [
|
|
4614
4882
|
result.installed.length
|
|
4615
4883
|
? `shims installed:\n${result.installed.map((p) => ` ${p}`).join('\n')}`
|
|
4616
4884
|
: 'no shims installed (claude/codex not found on PATH)',
|
|
4617
4885
|
];
|
|
4618
4886
|
if (result.skipped.length) lines.push(`skipped: ${result.skipped.join(', ')}`);
|
|
4619
|
-
if (
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4887
|
+
if (result.installed.length) {
|
|
4888
|
+
const { alreadyPresent, rcFile } = installPathEntry();
|
|
4889
|
+
if (!alreadyPresent) {
|
|
4890
|
+
lines.push(`PATH updated in ${rcFile}`);
|
|
4891
|
+
lines.push(`run: source ${rcFile} (or open a new terminal)`);
|
|
4892
|
+
} else {
|
|
4893
|
+
lines.push(`PATH entry already present in ${rcFile}`);
|
|
4894
|
+
}
|
|
4624
4895
|
}
|
|
4625
4896
|
printResult(ctx, result, () => lines.join('\n'));
|
|
4626
4897
|
return;
|
|
4627
4898
|
}
|
|
4628
4899
|
if (sub === 'uninstall') {
|
|
4629
4900
|
const removed = uninstallShims();
|
|
4630
|
-
|
|
4901
|
+
const pathEntry = uninstallPathEntry();
|
|
4902
|
+
printResult(ctx, { removed, path_entry: pathEntry }, () => [
|
|
4903
|
+
removed.length ? `removed: ${removed.join(', ')}` : 'no shims to remove',
|
|
4904
|
+
pathEntryRemovalMessage(pathEntry)
|
|
4905
|
+
].join('\n'));
|
|
4631
4906
|
return;
|
|
4632
4907
|
}
|
|
4633
4908
|
if (sub === 'status') {
|
|
4634
|
-
const
|
|
4635
|
-
printResult(ctx,
|
|
4636
|
-
|
|
4637
|
-
|
|
4909
|
+
const status = shimStatus();
|
|
4910
|
+
printResult(ctx, status, (r) => [
|
|
4911
|
+
`shim dir: ${r.shimDir}`,
|
|
4912
|
+
`claude: ${r.tools.claude.installed ? 'installed' : 'missing'} (${r.tools.claude.path})`,
|
|
4913
|
+
`codex: ${r.tools.codex.installed ? 'installed' : 'missing'} (${r.tools.codex.path})`,
|
|
4914
|
+
r.complete
|
|
4915
|
+
? 'status: complete'
|
|
4916
|
+
: r.installed
|
|
4917
|
+
? 'status: partial (run: hcc shim install)'
|
|
4918
|
+
: 'status: not installed (run: hcc shim install)'
|
|
4919
|
+
].join('\n'));
|
|
4638
4920
|
return;
|
|
4639
4921
|
}
|
|
4640
4922
|
throw new CliError('BAD_ARGS', `Unknown shim subcommand: ${sub}`);
|
|
@@ -4660,7 +4942,7 @@ async function cmdSetup(ctx, args) {
|
|
|
4660
4942
|
|
|
4661
4943
|
// 2. Install Claude Code + Codex hooks
|
|
4662
4944
|
const { installClaudeHooks, verifyClaudeHooks, installCodexHooks, verifyCodexHooks,
|
|
4663
|
-
installShims,
|
|
4945
|
+
installShims, installPathEntry } = await loadSetup();
|
|
4664
4946
|
const hccBin = commandPath();
|
|
4665
4947
|
|
|
4666
4948
|
if (!verifyClaudeHooks()) {
|
|
@@ -4678,20 +4960,23 @@ async function cmdSetup(ctx, args) {
|
|
|
4678
4960
|
}
|
|
4679
4961
|
|
|
4680
4962
|
// 3. Install shims
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
if (result.
|
|
4684
|
-
log(`✓ shims installed → ${result.
|
|
4963
|
+
const result = installShims(hccBin);
|
|
4964
|
+
if (result.installed.length) {
|
|
4965
|
+
if (result.changed.length) {
|
|
4966
|
+
log(`✓ shims installed → ${result.changed.join(', ')}`);
|
|
4685
4967
|
} else {
|
|
4686
|
-
log(
|
|
4968
|
+
log(`✓ shims already installed → ${result.installed.join(', ')}`);
|
|
4687
4969
|
}
|
|
4970
|
+
if (result.skipped.length) log(`⚠ shims skipped: ${result.skipped.join(', ')}`);
|
|
4688
4971
|
const { alreadyPresent, rcFile } = installPathEntry();
|
|
4689
4972
|
if (!alreadyPresent) {
|
|
4690
4973
|
log(`✓ PATH updated in ${rcFile}`);
|
|
4691
4974
|
log(` run: source ${rcFile} (or open a new terminal)`);
|
|
4975
|
+
} else {
|
|
4976
|
+
log(`✓ PATH entry already present in ${rcFile}`);
|
|
4692
4977
|
}
|
|
4693
4978
|
} else {
|
|
4694
|
-
log('
|
|
4979
|
+
log('⚠ shims: claude/codex not found on PATH — skipped');
|
|
4695
4980
|
}
|
|
4696
4981
|
|
|
4697
4982
|
log('\nDone. Run `hcc web` for the default coordinated terminal experience.');
|
|
@@ -4771,13 +5056,15 @@ async function cmdUninstall(ctx, args) {
|
|
|
4771
5056
|
lines.push('runtime not running');
|
|
4772
5057
|
}
|
|
4773
5058
|
|
|
4774
|
-
const { uninstallClaudeHooks, uninstallCodexHooks, uninstallShims } = await loadSetup();
|
|
5059
|
+
const { uninstallClaudeHooks, uninstallCodexHooks, uninstallShims, uninstallPathEntry } = await loadSetup();
|
|
4775
5060
|
const claude = uninstallClaudeHooks();
|
|
4776
5061
|
const codex = uninstallCodexHooks();
|
|
4777
5062
|
const shims = uninstallShims();
|
|
5063
|
+
const pathEntry = uninstallPathEntry();
|
|
4778
5064
|
lines.push(claude ? 'Claude Code hooks removed' : 'Claude Code hooks not found');
|
|
4779
5065
|
lines.push(codex ? 'Codex hooks removed' : 'Codex hooks not found');
|
|
4780
5066
|
lines.push(shims.length ? `shims removed: ${shims.join(', ')}` : 'shims not found');
|
|
5067
|
+
lines.push(pathEntryRemovalMessage(pathEntry));
|
|
4781
5068
|
|
|
4782
5069
|
let guidance = [];
|
|
4783
5070
|
let purged = false;
|
|
@@ -4795,7 +5082,7 @@ async function cmdUninstall(ctx, args) {
|
|
|
4795
5082
|
lines.push('project data kept; run hcc uninstall --purge --yes to remove .hello-cc and guidance blocks');
|
|
4796
5083
|
}
|
|
4797
5084
|
|
|
4798
|
-
printResult(ctx, { runtime: Boolean(runtime), claude, codex, shims, purge: purged, guidance }, () => lines.join('\n'));
|
|
5085
|
+
printResult(ctx, { runtime: Boolean(runtime), claude, codex, shims, path_entry: pathEntry, purge: purged, guidance }, () => lines.join('\n'));
|
|
4799
5086
|
}
|
|
4800
5087
|
|
|
4801
5088
|
// ─── hcc scan ────────────────────────────────────────────────────────────────
|
|
@@ -4808,7 +5095,7 @@ async function cmdScan(ctx, args) {
|
|
|
4808
5095
|
...scanClaudeSessions(),
|
|
4809
5096
|
...scanCodexSessions(),
|
|
4810
5097
|
...scanProcesses(),
|
|
4811
|
-
].filter((s) => s.hccRoot
|
|
5098
|
+
].filter((s) => sameResolvedPath(s.hccRoot, ctx.root));
|
|
4812
5099
|
|
|
4813
5100
|
// Deduplicate by peerId
|
|
4814
5101
|
const byId = new Map();
|
package/docs/commands.md
CHANGED
|
@@ -12,7 +12,7 @@ hcc uninstall [--purge --yes]
|
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
`hcc update` updates the global npm install. `hcc uninstall` removes local
|
|
15
|
-
hooks and
|
|
15
|
+
hooks, shims, and the shell PATH entry; add `--purge --yes` only when you also want to remove the
|
|
16
16
|
current project's `.hello-cc` data and guidance blocks.
|
|
17
17
|
|
|
18
18
|
## Start And Stop
|
|
@@ -93,6 +93,8 @@ sender by default. Use `msg thread` to inspect the full thread for one message.
|
|
|
93
93
|
|
|
94
94
|
```text
|
|
95
95
|
hcc task create --title TEXT [--body TEXT] [--from ID] [--to ID] [--priority N]
|
|
96
|
+
hcc task dispatch --to ID --title TEXT [--body TEXT] [--from ID] [--message TEXT] [--no-inject] [--force]
|
|
97
|
+
hcc task dispatch --to ID --id N [--from ID] [--message TEXT] [--no-inject] [--force]
|
|
96
98
|
hcc task list [--status S] [--peer ID] [--all]
|
|
97
99
|
hcc task claim [--peer ID] --id N[,N] [--id N] [--ids N,N] [--force]
|
|
98
100
|
hcc task takeover [--peer ID] --id N --reason TEXT [--policy any|blocked|stale|blocked-or-stale] [--stale-after SECONDS]
|
|
@@ -107,11 +109,16 @@ Tasks are shared project facts. They remain visible until marked `done` or
|
|
|
107
109
|
task before claiming a new pending task; use `--force` only when intentionally
|
|
108
110
|
taking additional pending tasks, and combine it with `--count N` for explicit
|
|
109
111
|
batch claims. `task claim` also accepts repeated `--id` values and comma-separated
|
|
110
|
-
`--id` or `--ids` lists.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
112
|
+
`--id` or `--ids` lists. `task dispatch` is the explicit one-step form for
|
|
113
|
+
assigning a new or existing task to one peer, sending the durable task message,
|
|
114
|
+
and injecting the startup prompt only when that peer has a running managed
|
|
115
|
+
Claude/Codex terminal. Use `--no-inject` for message-only dispatch, and use
|
|
116
|
+
`--force` only when intentionally injecting while the target already owns another
|
|
117
|
+
active task. Use `task takeover` when explicitly taking a non-complete task from
|
|
118
|
+
another owner; it records the previous owner, requires a reason, and notifies
|
|
119
|
+
them. Add `--policy blocked`, `stale`, or `blocked-or-stale` when takeover
|
|
120
|
+
should only proceed under that auditable precondition. The default policy is
|
|
121
|
+
`any` for compatibility.
|
|
115
122
|
|
|
116
123
|
## Teams
|
|
117
124
|
|