@indigoai-us/hq-cli 5.327.0 → 5.329.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/CHANGELOG.md +13 -0
- package/assets/scaffold/core/scripts/hq-dm-bind.sh +1 -1
- package/assets/scaffold/core/scripts/hq-job-run.sh +8 -8
- package/assets/scaffold/core/scripts/ontology-audience-grant.sh +1 -1
- package/assets/scaffold/core/scripts/ontology-candidate.sh +1 -1
- package/assets/scaffold/core/scripts/resolve-company.sh +1 -1
- package/dist/command-catalog.generated.d.ts +3699 -3
- package/dist/command-catalog.generated.js +5462 -1759
- package/dist/command-help.d.ts +2 -0
- package/dist/command-help.js +13 -0
- package/dist/commands/install-global.d.ts +21 -0
- package/dist/commands/install-global.js +267 -32
- package/dist/commands/mcp-serve.js +1 -1
- package/dist/commands/mcp-status.js +58 -1
- package/dist/commands/pack-install.d.ts +3 -1
- package/dist/commands/pack-install.js +17 -4
- package/dist/commands/packs.js +17 -0
- package/dist/commands/pkg-install.js +13 -1
- package/dist/fast-scaffold.d.ts +2 -0
- package/dist/fast-scaffold.js +16 -0
- package/dist/index.js +19 -2
- package/dist/lazy-commands.d.ts +1 -0
- package/dist/lazy-commands.js +64 -4
- package/dist/lib/cli-flag-keys.d.ts +3 -0
- package/dist/lib/cli-flag-keys.js +3 -0
- package/dist/lib/daemon/ops/mcp.js +11 -10
- package/dist/lib/daemon/ops/policy.d.ts +3 -2
- package/dist/lib/daemon/ops/policy.js +12 -6
- package/dist/lib/daemon/server.d.ts +3 -1
- package/dist/lib/daemon/server.js +63 -7
- package/dist/lib/doctor/checks/anywhere.js +58 -4
- package/dist/lib/install/chatgpt-targets.d.ts +8 -0
- package/dist/lib/install/chatgpt-targets.js +15 -0
- package/dist/lib/install/codex-pack-contributions.d.ts +8 -0
- package/dist/lib/install/codex-pack-contributions.js +205 -0
- package/dist/lib/install/codex-targets.d.ts +1 -1
- package/dist/lib/install/codex-targets.js +2 -1
- package/dist/lib/install/grok-targets.d.ts +8 -0
- package/dist/lib/install/grok-targets.js +15 -0
- package/dist/lib/lanes/keep-alive-audit.d.ts +3 -2
- package/dist/lib/lanes/keep-alive-audit.js +36 -8
- package/dist/main.d.ts +2 -6
- package/dist/main.js +3 -41
- package/dist/node-network-compat-fast.js +4 -0
- package/dist/stream-error-boundary.d.ts +7 -0
- package/dist/stream-error-boundary.js +33 -0
- package/dist/utils/vault-api.js +16 -1
- package/package.json +1 -1
|
@@ -16,6 +16,8 @@ import { CLI_VERSION } from "../../../cli-version.js";
|
|
|
16
16
|
import { computeMcpStatus } from "../../../commands/mcp-status.js";
|
|
17
17
|
import { HQ_MCP_SERVER_NAME, SKILL_LINK_PREFIX, claudeTargets } from "../../install/claude-targets.js";
|
|
18
18
|
import { codexTargets } from "../../install/codex-targets.js";
|
|
19
|
+
import { CHATGPT_MODE } from "../../install/chatgpt-targets.js";
|
|
20
|
+
import { GROK_MODE } from "../../install/grok-targets.js";
|
|
19
21
|
import { pingHqdVersion } from "../../daemon/convergence.js";
|
|
20
22
|
import { daemonPresenceLabel, heartbeatFile, readHeartbeat } from "../../daemon/heartbeat.js";
|
|
21
23
|
import { DAEMON_LAUNCHD_LABEL, DAEMON_SYSTEMD_UNIT, DAEMON_WINDOWS_TASK } from "../../daemon/install.js";
|
|
@@ -42,10 +44,16 @@ export async function runAnywhereChecks(_context, deps = {}) {
|
|
|
42
44
|
const claude = claudeTargets(home);
|
|
43
45
|
const codex = codexTargets(home);
|
|
44
46
|
const installed = [claude.hqPointer, claude.stateFile, codex.stateFile].some((p) => fs.existsSync(p));
|
|
47
|
+
const remoteModes = [
|
|
48
|
+
result("WARN", "chatgpt-mode", `ChatGPT integration mode: ${CHATGPT_MODE}; host connector setup is managed in ChatGPT`, "hq install --global --runtime chatgpt"),
|
|
49
|
+
result("WARN", "grok-mode", `Grok integration mode: ${GROK_MODE}; host connector setup is managed in Grok`, "hq install --global --runtime grok"),
|
|
50
|
+
];
|
|
45
51
|
if (!installed) {
|
|
46
|
-
return [result("NA", "installed", "HQ anywhere is not installed on this machine", REMEDIATION.installClaude)];
|
|
52
|
+
return [...remoteModes, result("NA", "installed", "HQ local anywhere runtime is not installed on this machine", REMEDIATION.installClaude)];
|
|
47
53
|
}
|
|
48
|
-
const out = [];
|
|
54
|
+
const out = [...remoteModes];
|
|
55
|
+
const pluginInstall = readClaudePluginInstall(claude.stateFile);
|
|
56
|
+
out.push(checkInstallMode(claude.stateFile));
|
|
49
57
|
out.push(checkPointer(claude.hqPointer));
|
|
50
58
|
out.push(checkRegistry(registryFile(registryDir(home, env))));
|
|
51
59
|
const service = (deps.serviceState ?? (() => defaultServiceState(home)))();
|
|
@@ -75,13 +83,59 @@ export async function runAnywhereChecks(_context, deps = {}) {
|
|
|
75
83
|
out.push(checkHeartbeat(heartbeatFile(home, env), (deps.now ?? (() => new Date()))()));
|
|
76
84
|
const mcp = computeMcpStatus({ home });
|
|
77
85
|
const server = mcp.servers.find((s) => s.server === HQ_MCP_SERVER_NAME);
|
|
78
|
-
|
|
86
|
+
if (pluginInstall)
|
|
87
|
+
out.push(checkClaudePlugin(claude.settingsJson, pluginInstall));
|
|
88
|
+
else
|
|
89
|
+
out.push(checkRegistration("claude", server?.claude, mcp.runtimes.claude, REMEDIATION.installClaude));
|
|
79
90
|
out.push(checkRegistration("codex", server?.codex, mcp.runtimes.codex, REMEDIATION.installCodex));
|
|
80
|
-
|
|
91
|
+
if (pluginInstall)
|
|
92
|
+
out.push(result("PASS", "claude-skills", `HQ Claude skills are provided by plugin ${pluginInstall.pluginId}`));
|
|
93
|
+
else
|
|
94
|
+
out.push(checkSkillLinks("claude", claude.skillsDir, REMEDIATION.installClaude));
|
|
81
95
|
if (mcp.runtimes.codex.installed)
|
|
82
96
|
out.push(checkSkillLinks("codex", codex.skillsDir, REMEDIATION.installCodex));
|
|
83
97
|
return out;
|
|
84
98
|
}
|
|
99
|
+
function checkInstallMode(file) {
|
|
100
|
+
try {
|
|
101
|
+
const state = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
102
|
+
if (typeof state.plugin?.pluginId === 'string') {
|
|
103
|
+
return result('PASS', 'install-mode', `Claude Code install mode: plugin (${state.plugin.pluginId})`, undefined, file);
|
|
104
|
+
}
|
|
105
|
+
return result('PASS', 'install-mode', 'Claude Code install mode: direct hooks and MCP', undefined, file);
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
if (error.code === 'ENOENT') {
|
|
109
|
+
return result('WARN', 'install-mode', 'Claude Code install mode is unknown (install state is missing)', REMEDIATION.installClaude, file);
|
|
110
|
+
}
|
|
111
|
+
return result('FAIL', 'install-mode', `Claude Code install state is unreadable: ${error.message}`, REMEDIATION.installClaude, file);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function readClaudePluginInstall(file) {
|
|
115
|
+
try {
|
|
116
|
+
const state = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
117
|
+
const plugin = state.plugin;
|
|
118
|
+
if (typeof plugin?.pluginId === 'string' && typeof plugin.marketplaceName === 'string' && typeof plugin.sourceDir === 'string')
|
|
119
|
+
return plugin;
|
|
120
|
+
}
|
|
121
|
+
catch { /* checkInstallMode reports missing or unreadable state */ }
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
function checkClaudePlugin(settingsFile, plugin) {
|
|
125
|
+
const remediation = `hq install --global --runtime claude --via plugin --marketplace-dir "${plugin.sourceDir}"`;
|
|
126
|
+
try {
|
|
127
|
+
const settings = JSON.parse(fs.readFileSync(settingsFile, 'utf8'));
|
|
128
|
+
if (settings.enabledPlugins?.[plugin.pluginId] !== true)
|
|
129
|
+
return result('FAIL', 'claude-registration', `HQ Claude plugin ${plugin.pluginId} is not enabled`, remediation, settingsFile);
|
|
130
|
+
const marketplace = settings.extraKnownMarketplaces?.[plugin.marketplaceName];
|
|
131
|
+
if (marketplace?.source?.source !== 'directory' || marketplace.source.path !== plugin.sourceDir)
|
|
132
|
+
return result('FAIL', 'claude-registration', `HQ Claude plugin marketplace ${plugin.marketplaceName} is not registered from ${plugin.sourceDir}`, remediation, settingsFile);
|
|
133
|
+
return result('PASS', 'claude-registration', `HQ Claude plugin ${plugin.pluginId} is enabled from its recorded marketplace`);
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
return result('FAIL', 'claude-registration', `Claude plugin settings are unreadable: ${error.message}`, remediation, settingsFile);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
85
139
|
function checkPointer(file) {
|
|
86
140
|
let root;
|
|
87
141
|
try {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** ChatGPT host integration for US-015.
|
|
2
|
+
*
|
|
3
|
+
* ChatGPT currently accepts remote MCP apps, not direct local stdio servers.
|
|
4
|
+
* Keep this target read-only: the user creates the hosted connector in ChatGPT.
|
|
5
|
+
*/
|
|
6
|
+
export declare const CHATGPT_MODE: "remote-connector";
|
|
7
|
+
export declare function chatgptConnectorInstructions(endpoint: string): string[];
|
|
8
|
+
//# sourceMappingURL=chatgpt-targets.d.ts.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** ChatGPT host integration for US-015.
|
|
2
|
+
*
|
|
3
|
+
* ChatGPT currently accepts remote MCP apps, not direct local stdio servers.
|
|
4
|
+
* Keep this target read-only: the user creates the hosted connector in ChatGPT.
|
|
5
|
+
*/
|
|
6
|
+
export const CHATGPT_MODE = "remote-connector";
|
|
7
|
+
export function chatgptConnectorInstructions(endpoint) {
|
|
8
|
+
return [
|
|
9
|
+
"ChatGPT supports hosted MCP connectors, not direct local MCP registration.",
|
|
10
|
+
`Hosted HQ MCP URL: ${endpoint}`,
|
|
11
|
+
"Enable it in ChatGPT web: Settings > Apps > Create, then add the URL as a custom MCP app. Workspace developer mode may need an admin to enable it first.",
|
|
12
|
+
"HQ did not write a ChatGPT configuration file.",
|
|
13
|
+
];
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=chatgpt-targets.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
type Doc = Record<string, unknown>;
|
|
2
|
+
export declare function syncCodexPackSkills(packDir: string, packName: string, names: string[], remove?: boolean, home?: string): string[];
|
|
3
|
+
export declare function mergeCodexPackHooks(doc: Doc, packDir: string, items: string[]): Doc;
|
|
4
|
+
export declare function removeCodexPackHooks(doc: Doc, packDir: string, items: string[]): Doc;
|
|
5
|
+
export declare function syncCodexPackHooks(packDir: string, items: string[], remove?: boolean, home?: string): void;
|
|
6
|
+
export declare function readCodexPackHooks(target: string): Doc;
|
|
7
|
+
export {};
|
|
8
|
+
//# sourceMappingURL=codex-pack-contributions.d.ts.map
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as os from 'node:os';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { anywhereRuntimeEnabled } from '../../commands/anywhere-runtime-flag.js';
|
|
5
|
+
import { jsonFormat, readConfigDoc, writeConfigAtomic } from '../../commands/mcp-registration.js';
|
|
6
|
+
const configFormat = {
|
|
7
|
+
parse: jsonFormat.parse,
|
|
8
|
+
serialize: jsonFormat.serialize,
|
|
9
|
+
emptyDoc: () => ({}),
|
|
10
|
+
assertEntry(doc, name) {
|
|
11
|
+
const hooks = doc.hooks;
|
|
12
|
+
if (name.startsWith('hooks-remove:')) {
|
|
13
|
+
const removedRoot = name.slice('hooks-remove:'.length);
|
|
14
|
+
if (hooks && Object.values(hooks).some((groups) => groups.some((group) => group.hooks?.some((hook) => typeof hook.command === 'string' && hook.command.includes(removedRoot))))) {
|
|
15
|
+
throw new Error(`Codex hook registrations for ${removedRoot} remain after removal`);
|
|
16
|
+
}
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (!hooks || !Object.values(hooks).some((groups) => groups.some((group) => group.hooks?.some((hook) => typeof hook.command === 'string' && hook.command.includes(name))))) {
|
|
20
|
+
throw new Error(`Codex hook registrations for ${name} were not written`);
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
function hooksPath(home) { return path.join(home, '.codex', 'hooks.json'); }
|
|
25
|
+
const ownerFile = '.hq-pack-owner';
|
|
26
|
+
const validContributionName = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
27
|
+
function assertContributionNames(items) {
|
|
28
|
+
for (const item of items)
|
|
29
|
+
if (!validContributionName.test(item))
|
|
30
|
+
throw new Error(`Invalid Codex pack contribution name: ${item}`);
|
|
31
|
+
}
|
|
32
|
+
function hasPackOwner(target, packName) {
|
|
33
|
+
try {
|
|
34
|
+
const dirStat = fs.lstatSync(target);
|
|
35
|
+
const markerPath = path.join(target, ownerFile);
|
|
36
|
+
const markerStat = fs.lstatSync(markerPath);
|
|
37
|
+
return dirStat.isDirectory() && !dirStat.isSymbolicLink() && markerStat.isFile() && !markerStat.isSymbolicLink() && fs.readFileSync(markerPath, 'utf8') === `${packName}\n`;
|
|
38
|
+
}
|
|
39
|
+
catch (e) {
|
|
40
|
+
if (e.code === 'ENOENT')
|
|
41
|
+
return false;
|
|
42
|
+
throw e;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function syncCodexPackSkills(packDir, packName, names, remove = false, home = os.homedir()) {
|
|
46
|
+
assertContributionNames(names);
|
|
47
|
+
if ((!remove && !anywhereRuntimeEnabled()) || !fs.existsSync(path.join(home, '.codex')))
|
|
48
|
+
return [];
|
|
49
|
+
const root = path.join(home, '.codex', 'skills');
|
|
50
|
+
const warnings = [];
|
|
51
|
+
for (const name of names) {
|
|
52
|
+
const source = path.join(packDir, 'skills', name);
|
|
53
|
+
const target = path.join(root, name);
|
|
54
|
+
if (!source.startsWith(`${path.resolve(packDir, 'skills')}${path.sep}`))
|
|
55
|
+
throw new Error(`Invalid Codex skill path: ${name}`);
|
|
56
|
+
if (remove) {
|
|
57
|
+
if (hasPackOwner(target, packName))
|
|
58
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
assertSafeSkillTree(source, packDir, name);
|
|
62
|
+
let targetExists = false;
|
|
63
|
+
try {
|
|
64
|
+
fs.lstatSync(target);
|
|
65
|
+
targetExists = true;
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
if (e.code !== 'ENOENT')
|
|
69
|
+
throw e;
|
|
70
|
+
}
|
|
71
|
+
if (targetExists) {
|
|
72
|
+
const owned = hasPackOwner(target, packName);
|
|
73
|
+
if (!owned) {
|
|
74
|
+
warnings.push(`left foreign Codex skill in place: ${name}`);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
78
|
+
}
|
|
79
|
+
fs.mkdirSync(root, { recursive: true });
|
|
80
|
+
copySafeSkillTree(source, target);
|
|
81
|
+
fs.writeFileSync(path.join(target, ownerFile), `${packName}\n`, { flag: 'wx', mode: 0o644 });
|
|
82
|
+
}
|
|
83
|
+
return warnings;
|
|
84
|
+
}
|
|
85
|
+
function assertSafeSkillTree(source, packDir, name) {
|
|
86
|
+
const root = path.resolve(packDir);
|
|
87
|
+
const resolved = path.resolve(source);
|
|
88
|
+
const stat = fs.lstatSync(source);
|
|
89
|
+
if (!resolved.startsWith(`${root}${path.sep}`) || !stat.isDirectory() || stat.isSymbolicLink()) {
|
|
90
|
+
throw new Error(`Codex skill payload must be a real directory inside the pack: ${name}`);
|
|
91
|
+
}
|
|
92
|
+
for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
|
|
93
|
+
const child = path.join(source, entry.name);
|
|
94
|
+
const childStat = fs.lstatSync(child);
|
|
95
|
+
if (childStat.isSymbolicLink())
|
|
96
|
+
throw new Error(`Codex skill payload contains a symlink: ${name}/${entry.name}`);
|
|
97
|
+
if (childStat.isDirectory())
|
|
98
|
+
assertSafeSkillTree(child, packDir, name);
|
|
99
|
+
else if (!childStat.isFile())
|
|
100
|
+
throw new Error(`Codex skill payload contains an unsupported entry: ${name}/${entry.name}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function copySafeSkillTree(source, target) {
|
|
104
|
+
fs.mkdirSync(target, { recursive: false });
|
|
105
|
+
for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
|
|
106
|
+
const from = path.join(source, entry.name);
|
|
107
|
+
const to = path.join(target, entry.name);
|
|
108
|
+
const stat = fs.lstatSync(from);
|
|
109
|
+
if (stat.isSymbolicLink())
|
|
110
|
+
throw new Error(`Codex skill payload contains a symlink: ${entry.name}`);
|
|
111
|
+
if (stat.isDirectory())
|
|
112
|
+
copySafeSkillTree(from, to);
|
|
113
|
+
else if (stat.isFile())
|
|
114
|
+
fs.copyFileSync(from, to, fs.constants.COPYFILE_EXCL);
|
|
115
|
+
else
|
|
116
|
+
throw new Error(`Codex skill payload contains an unsupported entry: ${entry.name}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function readRegistrations(packDir, items) {
|
|
120
|
+
assertContributionNames(items);
|
|
121
|
+
const all = {};
|
|
122
|
+
for (const item of items) {
|
|
123
|
+
const file = path.join(packDir, 'hooks', `${item}-hooks.json`);
|
|
124
|
+
if (!fs.existsSync(file))
|
|
125
|
+
continue;
|
|
126
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
127
|
+
if (!parsed || typeof parsed !== 'object' || !('hooks' in parsed) || !parsed.hooks || typeof parsed.hooks !== 'object' || Array.isArray(parsed.hooks)) {
|
|
128
|
+
throw new Error(`Invalid Codex hook registration template: ${file}`);
|
|
129
|
+
}
|
|
130
|
+
for (const [event, groups] of Object.entries(parsed.hooks)) {
|
|
131
|
+
if (!Array.isArray(groups))
|
|
132
|
+
throw new Error(`Invalid Codex hook event ${event} in ${file}`);
|
|
133
|
+
const resolved = groups.map((group) => ({
|
|
134
|
+
...group,
|
|
135
|
+
hooks: (group.hooks ?? []).map((hook) => {
|
|
136
|
+
if (typeof hook.command !== 'string' || !hook.command.startsWith('hooks/'))
|
|
137
|
+
throw new Error(`Invalid pack-relative hook command in ${file}`);
|
|
138
|
+
const rel = hook.command.slice('hooks/'.length);
|
|
139
|
+
const abs = path.resolve(packDir, 'hooks', rel);
|
|
140
|
+
if (!abs.startsWith(`${path.resolve(packDir)}${path.sep}`) || !fs.existsSync(abs))
|
|
141
|
+
throw new Error(`Hook target is missing or outside the installed pack: ${hook.command}`);
|
|
142
|
+
const timeout = event === 'SessionEnd' ? 3 : 300;
|
|
143
|
+
return { ...hook, command: `/bin/sh ${JSON.stringify(abs)}`, timeout };
|
|
144
|
+
}),
|
|
145
|
+
}));
|
|
146
|
+
all[event] = [...(all[event] ?? []), ...resolved];
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return all;
|
|
150
|
+
}
|
|
151
|
+
export function mergeCodexPackHooks(doc, packDir, items) {
|
|
152
|
+
const hooks = { ...(doc.hooks ?? {}) };
|
|
153
|
+
const registrations = readRegistrations(packDir, items);
|
|
154
|
+
removeCommands(hooks, registeredCommands(registrations));
|
|
155
|
+
for (const [event, groups] of Object.entries(registrations))
|
|
156
|
+
hooks[event] = [...(hooks[event] ?? []), ...groups];
|
|
157
|
+
return { ...doc, ...(Object.keys(hooks).length ? { hooks } : {}) };
|
|
158
|
+
}
|
|
159
|
+
export function removeCodexPackHooks(doc, packDir, items) {
|
|
160
|
+
const hooks = { ...(doc.hooks ?? {}) };
|
|
161
|
+
removeCommands(hooks, registeredCommands(readRegistrations(packDir, items)));
|
|
162
|
+
const next = { ...doc };
|
|
163
|
+
if (Object.keys(hooks).length)
|
|
164
|
+
next.hooks = hooks;
|
|
165
|
+
else
|
|
166
|
+
delete next.hooks;
|
|
167
|
+
return next;
|
|
168
|
+
}
|
|
169
|
+
function registeredCommands(registrations) {
|
|
170
|
+
const commands = new Set();
|
|
171
|
+
for (const groups of Object.values(registrations))
|
|
172
|
+
for (const group of groups) {
|
|
173
|
+
for (const hook of group.hooks ?? [])
|
|
174
|
+
if (typeof hook.command === 'string')
|
|
175
|
+
commands.add(hook.command);
|
|
176
|
+
}
|
|
177
|
+
return commands;
|
|
178
|
+
}
|
|
179
|
+
function removeCommands(hooks, commands) {
|
|
180
|
+
for (const [event, groups] of Object.entries(hooks)) {
|
|
181
|
+
hooks[event] = groups.flatMap((group) => {
|
|
182
|
+
const kept = (group.hooks ?? []).filter((hook) => typeof hook.command !== 'string' || !commands.has(hook.command));
|
|
183
|
+
return kept.length ? [{ ...group, hooks: kept }] : [];
|
|
184
|
+
});
|
|
185
|
+
if (hooks[event].length === 0)
|
|
186
|
+
delete hooks[event];
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
export function syncCodexPackHooks(packDir, items, remove = false, home = os.homedir()) {
|
|
190
|
+
assertContributionNames(items);
|
|
191
|
+
if ((!remove && !anywhereRuntimeEnabled()) || !fs.existsSync(path.join(home, '.codex')))
|
|
192
|
+
return;
|
|
193
|
+
const codexItems = items.filter((item) => fs.existsSync(path.join(packDir, 'hooks', `${item}-hooks.json`)));
|
|
194
|
+
if (codexItems.length === 0)
|
|
195
|
+
return;
|
|
196
|
+
const target = hooksPath(home);
|
|
197
|
+
writeConfigAtomic({
|
|
198
|
+
target, pack: 'hq-anywhere', format: configFormat,
|
|
199
|
+
merge: (doc) => remove ? removeCodexPackHooks(doc, packDir, codexItems) : mergeCodexPackHooks(doc, packDir, codexItems),
|
|
200
|
+
assertName: remove ? `hooks-remove:${path.resolve(packDir)}${path.sep}` : codexItems[0],
|
|
201
|
+
env: { home },
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
export function readCodexPackHooks(target) { return readConfigDoc(target, configFormat).doc; }
|
|
205
|
+
//# sourceMappingURL=codex-pack-contributions.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type HookTarget } from './claude-targets.js';
|
|
2
2
|
/** Events the Codex hook adapter is registered for at user scope. */
|
|
3
|
-
export declare const CODEX_HOOK_EVENTS: readonly ["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "Stop"];
|
|
3
|
+
export declare const CODEX_HOOK_EVENTS: readonly ["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "Stop", "SessionEnd"];
|
|
4
4
|
export declare const AGENTS_MD_BEGIN = "<!-- hq-anywhere:begin (managed by `hq install --global --runtime codex`; remove with `hq uninstall --global --runtime codex`) -->";
|
|
5
5
|
export declare const AGENTS_MD_END = "<!-- hq-anywhere:end -->";
|
|
6
6
|
/** Name of the legacy ad-hoc skills mirror (~/.codex/skills/hq). */
|
|
@@ -19,6 +19,7 @@ export const CODEX_HOOK_EVENTS = [
|
|
|
19
19
|
'PreToolUse',
|
|
20
20
|
'PostToolUse',
|
|
21
21
|
'Stop',
|
|
22
|
+
'SessionEnd',
|
|
22
23
|
];
|
|
23
24
|
export const AGENTS_MD_BEGIN = '<!-- hq-anywhere:begin (managed by `hq install --global --runtime codex`; remove with `hq uninstall --global --runtime codex`) -->';
|
|
24
25
|
export const AGENTS_MD_END = '<!-- hq-anywhere:end -->';
|
|
@@ -108,7 +109,7 @@ export function addCodexHooks(doc, hqRoot, target = 'master') {
|
|
|
108
109
|
if (present)
|
|
109
110
|
continue;
|
|
110
111
|
groups = removeCodexHooks({ hooks: { [event]: groups } }, hqRoot).hooks?.[event] ?? [];
|
|
111
|
-
groups.push({ hooks: [{ type: 'command', command: want, timeout: 300 }] });
|
|
112
|
+
groups.push({ hooks: [{ type: 'command', command: want, timeout: event === 'SessionEnd' ? 3 : 300 }] });
|
|
112
113
|
hooks[event] = groups;
|
|
113
114
|
changed = true;
|
|
114
115
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Grok host integration for US-015.
|
|
2
|
+
*
|
|
3
|
+
* Grok custom MCP connectors are remote URL integrations. The local CLI does
|
|
4
|
+
* not write host configuration; users add the hosted HQ MCP endpoint in Grok.
|
|
5
|
+
*/
|
|
6
|
+
export declare const GROK_MODE: "remote-connector";
|
|
7
|
+
export declare function grokConnectorInstructions(endpoint: string): string[];
|
|
8
|
+
//# sourceMappingURL=grok-targets.d.ts.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Grok host integration for US-015.
|
|
2
|
+
*
|
|
3
|
+
* Grok custom MCP connectors are remote URL integrations. The local CLI does
|
|
4
|
+
* not write host configuration; users add the hosted HQ MCP endpoint in Grok.
|
|
5
|
+
*/
|
|
6
|
+
export const GROK_MODE = "remote-connector";
|
|
7
|
+
export function grokConnectorInstructions(endpoint) {
|
|
8
|
+
return [
|
|
9
|
+
"Grok supports remote custom MCP connectors; it cannot register a local stdio server directly.",
|
|
10
|
+
`Hosted HQ MCP URL: ${endpoint}`,
|
|
11
|
+
"Enable it at grok.com/connectors > New Connector > Custom, then enter the URL and finish authentication.",
|
|
12
|
+
"HQ did not write a Grok configuration file.",
|
|
13
|
+
];
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=grok-targets.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
type KeepAliveAuditVerdict = "on_time" | "late" | "early_wake" | "message_wake" | "self_wake" | "early_by_keep_alive" | "not_restarted" | "keep_alive_off" | "waiting";
|
|
1
|
+
type KeepAliveAuditVerdict = "on_time" | "late" | "early_wake" | "message_wake" | "self_wake" | "early_by_keep_alive" | "restarted_untimed" | "not_restarted" | "keep_alive_off" | "waiting";
|
|
2
2
|
interface KeepAliveAuditRestart {
|
|
3
|
-
at: string;
|
|
3
|
+
at: string | null;
|
|
4
4
|
trigger: string;
|
|
5
5
|
caller: string | null;
|
|
6
6
|
}
|
|
@@ -18,6 +18,7 @@ interface KeepAliveAuditRow {
|
|
|
18
18
|
interface KeepAliveAuditSummary {
|
|
19
19
|
by_verdict: Record<KeepAliveAuditVerdict, number>;
|
|
20
20
|
not_restarted: number;
|
|
21
|
+
restarted_untimed: number;
|
|
21
22
|
late_over_10m: number;
|
|
22
23
|
early_wake_by_others: number;
|
|
23
24
|
self_wake: number;
|
|
@@ -4,6 +4,7 @@ import { LEAD_LANE_KEEP_ALIVE_BASE_BACKOFF_MS } from "./keep-alive.js";
|
|
|
4
4
|
import { LanesError } from "./errors.js";
|
|
5
5
|
import { laneRunDir } from "./spawn.js";
|
|
6
6
|
import { listQuestionIds, readAllLanes, tryReadQuestion } from "./store.js";
|
|
7
|
+
import { HISTORY_CAP } from "./types.js";
|
|
7
8
|
function readDoneClassifications(hqRoot, laneId) {
|
|
8
9
|
const root = fs.realpathSync(hqRoot);
|
|
9
10
|
const watcherLog = path.join(laneRunDir(root, laneId), "logs", "watcher.out");
|
|
@@ -56,6 +57,7 @@ const VERDICTS = [
|
|
|
56
57
|
"message_wake",
|
|
57
58
|
"self_wake",
|
|
58
59
|
"early_by_keep_alive",
|
|
60
|
+
"restarted_untimed",
|
|
59
61
|
"keep_alive_off",
|
|
60
62
|
"not_restarted",
|
|
61
63
|
"waiting",
|
|
@@ -285,17 +287,42 @@ function firstRestartAfter(lane, stop, nextEnvelopeMs) {
|
|
|
285
287
|
.map((entry) => ({ entry, at: parseTime(entry.at) }))
|
|
286
288
|
.filter(({ entry, at }) => at !== undefined && at > stop.mtimeMs && (nextEnvelopeMs === undefined || at < nextEnvelopeMs) && typeof entry.trigger === "string")
|
|
287
289
|
.sort((a, b) => a.at - b.at)[0];
|
|
288
|
-
if (
|
|
290
|
+
if (restart?.at !== undefined) {
|
|
291
|
+
return {
|
|
292
|
+
at: iso(restart.at),
|
|
293
|
+
trigger: restart.entry.trigger,
|
|
294
|
+
caller: typeof restart.entry.caller === "string" ? restart.entry.caller : null,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
// resume_history is capped and may no longer retain the launch that followed
|
|
298
|
+
// an older stop. keep_alive_history is the durable automatic-resume record;
|
|
299
|
+
// only count it when a later envelope confirms the queued turn actually ran.
|
|
300
|
+
if (nextEnvelopeMs === undefined)
|
|
289
301
|
return null;
|
|
290
|
-
|
|
291
|
-
at:
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
302
|
+
const keepAliveResume = (lane.keep_alive_history ?? [])
|
|
303
|
+
.map((entry) => ({ entry, at: parseTime(entry.at) }))
|
|
304
|
+
.filter(({ entry, at }) => entry.resumed_from === "done" && at !== undefined && at > stop.mtimeMs && at < nextEnvelopeMs)
|
|
305
|
+
.sort((a, b) => a.at - b.at)[0];
|
|
306
|
+
if (keepAliveResume) {
|
|
307
|
+
// This timestamp records when reconcile queued the resume, not when the
|
|
308
|
+
// provider actually launched. The next envelope confirms a turn ran, but
|
|
309
|
+
// cannot establish its launch time.
|
|
310
|
+
return { at: null, trigger: "keep_alive", caller: null };
|
|
311
|
+
}
|
|
312
|
+
const resumeHistory = lane.resume_history ?? [];
|
|
313
|
+
const keepAliveHistory = lane.keep_alive_history ?? [];
|
|
314
|
+
if (resumeHistory.length >= HISTORY_CAP && keepAliveHistory.length >= HISTORY_CAP) {
|
|
315
|
+
// Both append-only histories can roll this resume out. The later envelope
|
|
316
|
+
// proves a turn ran, while its trigger and launch time are no longer known.
|
|
317
|
+
return { at: null, trigger: "unknown", caller: null };
|
|
318
|
+
}
|
|
319
|
+
return null;
|
|
295
320
|
}
|
|
296
321
|
function classifyVerdict(input) {
|
|
297
322
|
const { dueAt, restart, lane, nowMs } = input;
|
|
298
323
|
if (restart) {
|
|
324
|
+
if (restart.at === null)
|
|
325
|
+
return "restarted_untimed";
|
|
299
326
|
const restartAt = Date.parse(restart.at);
|
|
300
327
|
if (restart.caller === lane.lane_id)
|
|
301
328
|
return "self_wake";
|
|
@@ -388,7 +415,7 @@ export function buildKeepAliveAudit(input) {
|
|
|
388
415
|
let lateOverTenMinutes = 0;
|
|
389
416
|
for (const row of rows) {
|
|
390
417
|
byVerdict[row.verdict] += 1;
|
|
391
|
-
if (row.verdict === "late" && row.restart && Date.parse(row.restart.at) - Date.parse(row.due_at) > LATE_KPI_THRESHOLD_MS) {
|
|
418
|
+
if (row.verdict === "late" && row.restart !== null && row.restart.at !== null && Date.parse(row.restart.at) - Date.parse(row.due_at) > LATE_KPI_THRESHOLD_MS) {
|
|
392
419
|
lateOverTenMinutes += 1;
|
|
393
420
|
}
|
|
394
421
|
}
|
|
@@ -399,6 +426,7 @@ export function buildKeepAliveAudit(input) {
|
|
|
399
426
|
summary: {
|
|
400
427
|
by_verdict: byVerdict,
|
|
401
428
|
not_restarted: byVerdict.not_restarted,
|
|
429
|
+
restarted_untimed: byVerdict.restarted_untimed,
|
|
402
430
|
late_over_10m: lateOverTenMinutes,
|
|
403
431
|
early_wake_by_others: byVerdict.early_wake,
|
|
404
432
|
self_wake: byVerdict.self_wake,
|
|
@@ -416,7 +444,7 @@ export function formatKeepAliveAuditText(result) {
|
|
|
416
444
|
row.stopped_at,
|
|
417
445
|
row.next_check_at ?? "—",
|
|
418
446
|
row.due_at,
|
|
419
|
-
row.restart ? `${row.restart.at} ${row.restart.trigger}${row.restart.caller ? ` (${row.restart.caller})` : ""}` : "—",
|
|
447
|
+
row.restart ? `${row.restart.at ?? "untimed"} ${row.restart.trigger}${row.restart.caller ? ` (${row.restart.caller})` : ""}` : "—",
|
|
420
448
|
row.verdict,
|
|
421
449
|
]);
|
|
422
450
|
const widths = headings.map((heading, index) => Math.max(heading.length, ...values.map((row) => row[index].length)));
|
package/dist/main.d.ts
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
import "./node-preflight.js";
|
|
6
6
|
import "./node-network-compat.js";
|
|
7
7
|
import { Sentry } from "./sentry.js";
|
|
8
|
+
export { handleStreamError } from "./stream-error-boundary.js";
|
|
9
|
+
export type { StreamErrorDependencies } from "./stream-error-boundary.js";
|
|
8
10
|
import { upgradeToTeam } from "./utils/team-upgrade.js";
|
|
9
11
|
/**
|
|
10
12
|
* Total time one CLI invocation may await foreground network work. This spans
|
|
@@ -31,11 +33,6 @@ export declare function createForegroundNetworkBudget(options?: {
|
|
|
31
33
|
budgetMs?: number;
|
|
32
34
|
now?: () => number;
|
|
33
35
|
}): ForegroundNetworkBudget;
|
|
34
|
-
export type StreamErrorDependencies = {
|
|
35
|
-
stderr: Pick<typeof process.stderr, "write">;
|
|
36
|
-
exit: (code: number) => void;
|
|
37
|
-
};
|
|
38
|
-
export declare function handleStreamError(err: NodeJS.ErrnoException, deps?: StreamErrorDependencies): void;
|
|
39
36
|
export declare function runCli(): Promise<void>;
|
|
40
37
|
export type TopLevelErrorDependencies = {
|
|
41
38
|
sentry: Pick<typeof Sentry, "captureException">;
|
|
@@ -49,5 +46,4 @@ export type TopLevelErrorDependencies = {
|
|
|
49
46
|
};
|
|
50
47
|
/** Classify a top-level failure without making the CLI process boundary opaque to tests. */
|
|
51
48
|
export declare function handleTopLevelError(err: unknown, deps?: TopLevelErrorDependencies): Promise<void>;
|
|
52
|
-
export {};
|
|
53
49
|
//# sourceMappingURL=main.d.ts.map
|
package/dist/main.js
CHANGED
|
@@ -31,6 +31,8 @@ import { incompleteInstallMessage, incompleteInstallCaptureContext, } from "./ut
|
|
|
31
31
|
import { isExpectedUserError } from "./utils/expected-cli-error.js";
|
|
32
32
|
import { isVarlockEnvError } from "./run/env-graph-guard.js";
|
|
33
33
|
import { isEpipe } from "./utils/epipe.js";
|
|
34
|
+
import { installStreamErrorBoundary } from "./stream-error-boundary.js";
|
|
35
|
+
export { handleStreamError } from "./stream-error-boundary.js";
|
|
34
36
|
import { isInterceptedProcessExit } from "./utils/intercepted-process-exit.js";
|
|
35
37
|
import { isAuthError } from "./utils/auth-error.js";
|
|
36
38
|
import { browserLoginAbandonedMessage } from "./utils/browser-login-abandoned.js";
|
|
@@ -123,47 +125,7 @@ let activeForegroundNetworkBudget;
|
|
|
123
125
|
* the rewrite window, while this path now holds the settled tree.
|
|
124
126
|
*/
|
|
125
127
|
const RUNNING_ENTRY_PATH = path.join(path.dirname(fileURLToPath(import.meta.url)), "index.js");
|
|
126
|
-
|
|
127
|
-
stderr: process.stderr,
|
|
128
|
-
exit: (code) => process.exit(code),
|
|
129
|
-
};
|
|
130
|
-
// Handle an 'error' event on process.stdout/stderr without turning a benign
|
|
131
|
-
// machine-state condition into a Sentry fatal.
|
|
132
|
-
// - EPIPE: a downstream reader closed the pipe early (`hq … | head`,
|
|
133
|
-
// `source <(hq …)`, a parent that exited) — a clean exit 0, no capture. The
|
|
134
|
-
// SYNCHRONOUS path (a `write EPIPE` thrown straight out of console.log) is
|
|
135
|
-
// handled in the top-level catch below; both share `isEpipe` (HQ-6B).
|
|
136
|
-
// - ENOSPC / EDQUOT / EROFS: a full disk, exhausted quota, or read-only mount
|
|
137
|
-
// (e.g. stdout redirected to a file that fills up) — the user's machine,
|
|
138
|
-
// not an hq-cli defect. Print one actionable line (best-effort; the same
|
|
139
|
-
// disk may reject it) and exit 1 WITHOUT rethrowing, so it never becomes an
|
|
140
|
-
// uncaughtException that ships a fatal (HQ-CLI-R). sentry.ts's
|
|
141
|
-
// path-independent beforeSend also drops it, covering the buffered-write
|
|
142
|
-
// route that never surfaces here.
|
|
143
|
-
// - Anything else: rethrow — an unknown stream failure must stay loud.
|
|
144
|
-
// Exported in dependency-injectable form (mirroring handleTopLevelError) so the
|
|
145
|
-
// process boundary stays unit-testable through the ./main.js import seam.
|
|
146
|
-
export function handleStreamError(err, deps = defaultStreamErrorDependencies) {
|
|
147
|
-
if (isEpipe(err)) {
|
|
148
|
-
deps.exit(0);
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
const envMessage = environmentalFsErrorMessage(err);
|
|
152
|
-
if (envMessage) {
|
|
153
|
-
try {
|
|
154
|
-
deps.stderr.write(`hq: ${envMessage}\n`);
|
|
155
|
-
}
|
|
156
|
-
catch {
|
|
157
|
-
// The same full disk / read-only mount can reject this write too; there
|
|
158
|
-
// is nothing more to surface, but we still exit non-zero below.
|
|
159
|
-
}
|
|
160
|
-
deps.exit(1);
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
throw err;
|
|
164
|
-
}
|
|
165
|
-
process.stdout.on("error", (err) => handleStreamError(err));
|
|
166
|
-
process.stderr.on("error", (err) => handleStreamError(err));
|
|
128
|
+
installStreamErrorBoundary();
|
|
167
129
|
runGlobalNetworkLifecycle(process.argv, () => initSentry());
|
|
168
130
|
const program = new Command();
|
|
169
131
|
program
|
|
@@ -11,6 +11,10 @@ export function shouldConfigureNodeNetworkCompatibility(argv) {
|
|
|
11
11
|
const fastCore = parseFastCore(argv);
|
|
12
12
|
if (fastCore)
|
|
13
13
|
return fastCore.entry.requiresNodeNetworkCompatibility === true;
|
|
14
|
+
// The MCP shim only forwards over the local hqd socket; it does not make
|
|
15
|
+
// outbound network requests and does not need the network-family patch.
|
|
16
|
+
if (argv.length === 4 && argv[2] === "mcp" && argv[3] === "serve")
|
|
17
|
+
return false;
|
|
14
18
|
// The lightweight secrets route still makes vault API requests.
|
|
15
19
|
if (parseSecretsEnvFastRequest(argv))
|
|
16
20
|
return true;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type StreamErrorDependencies = {
|
|
2
|
+
stderr: Pick<typeof process.stderr, "write">;
|
|
3
|
+
exit: (code: number) => void;
|
|
4
|
+
};
|
|
5
|
+
export declare function handleStreamError(err: NodeJS.ErrnoException, deps?: StreamErrorDependencies): void;
|
|
6
|
+
export declare function installStreamErrorBoundary(): void;
|
|
7
|
+
//# sourceMappingURL=stream-error-boundary.d.ts.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { environmentalFsErrorMessage } from "./utils/environmental-error.js";
|
|
2
|
+
import { isEpipe } from "./utils/epipe.js";
|
|
3
|
+
const defaults = {
|
|
4
|
+
stderr: process.stderr,
|
|
5
|
+
exit: (code) => process.exit(code),
|
|
6
|
+
};
|
|
7
|
+
export function handleStreamError(err, deps = defaults) {
|
|
8
|
+
if (isEpipe(err)) {
|
|
9
|
+
deps.exit(0);
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const message = environmentalFsErrorMessage(err);
|
|
13
|
+
if (message) {
|
|
14
|
+
try {
|
|
15
|
+
deps.stderr.write(`hq: ${message}\n`);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
// The same full disk or read-only mount can reject the diagnostic write.
|
|
19
|
+
}
|
|
20
|
+
deps.exit(1);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
throw err;
|
|
24
|
+
}
|
|
25
|
+
let installed = false;
|
|
26
|
+
export function installStreamErrorBoundary() {
|
|
27
|
+
if (installed)
|
|
28
|
+
return;
|
|
29
|
+
installed = true;
|
|
30
|
+
process.stdout.on("error", (err) => handleStreamError(err));
|
|
31
|
+
process.stderr.on("error", (err) => handleStreamError(err));
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=stream-error-boundary.js.map
|