@indigoai-us/hq-cli 5.324.0 → 5.326.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 +9 -0
- package/dist/bin/hq-auth-refresh.js +2 -3
- package/dist/command-catalog.generated.d.ts +10 -1
- package/dist/command-catalog.generated.js +11 -1
- package/dist/commands/anywhere-runtime-flag.d.ts +2 -2
- package/dist/commands/anywhere-runtime-flag.js +2 -2
- package/dist/commands/core-derive-trigger-facts-native.js +37 -3
- package/dist/commands/core-job-remote-launcher.js +2 -3
- package/dist/commands/daemon.js +1 -1
- package/dist/commands/install-global.d.ts +10 -0
- package/dist/commands/install-global.js +96 -41
- package/dist/commands/mcp-serve.d.ts +28 -0
- package/dist/commands/mcp-serve.js +165 -0
- package/dist/commands/mcp-status.js +7 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +9 -0
- package/dist/lanes-drain-hook.js +2 -2
- package/dist/lib/anywhere-runtime-fast-flag.d.ts +3 -0
- package/dist/lib/anywhere-runtime-fast-flag.js +23 -0
- package/dist/lib/anywhere-runtime-flag-key.d.ts +3 -0
- package/dist/lib/anywhere-runtime-flag-key.js +3 -0
- package/dist/lib/daemon/ops/mcp.d.ts +54 -0
- package/dist/lib/daemon/ops/mcp.js +217 -0
- package/dist/lib/daemon/protocol.d.ts +2 -1
- package/dist/lib/daemon/protocol.js +3 -1
- package/dist/lib/daemon/server.d.ts +5 -0
- package/dist/lib/daemon/server.js +78 -11
- package/dist/lib/install/claude-targets.d.ts +11 -4
- package/dist/lib/install/claude-targets.js +21 -9
- package/dist/lib/install/codex-targets.d.ts +8 -4
- package/dist/lib/install/codex-targets.js +18 -9
- package/dist/lib/lanes/watcher-main.js +2 -4
- package/dist/lib/mcp/hosted-search-tools.d.ts +4 -0
- package/dist/lib/mcp/hosted-search-tools.js +28 -0
- package/dist/lib/mcp/hq-mcp-tools.generated.d.ts +3 -0
- package/dist/lib/mcp/hq-mcp-tools.generated.js +538 -0
- package/dist/lib/mcp/prompts.d.ts +30 -0
- package/dist/lib/mcp/prompts.js +48 -0
- package/dist/lib/mcp/tools.d.ts +22 -0
- package/dist/lib/mcp/tools.js +82 -0
- package/dist/lib/mcp/types.d.ts +6 -0
- package/dist/lib/mcp/types.js +2 -0
- package/dist/utils/is-main-module.d.ts +3 -0
- package/dist/utils/is-main-module.js +19 -0
- package/dist/utils/staged-npm-install.js +2 -1
- package/package.json +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.326.0] — 2026-10-02
|
|
6
|
+
- Company-scoped daemon tools now reject an explicit company that conflicts with the active session or repository binding.
|
|
7
|
+
- Added a flag-gated `hq mcp serve` command and global install hook for the Anywhere runtime.
|
|
8
|
+
- `hq mcp serve` can forward supported calls to hosted hq-mcp, including search and fetch, without a local hq-mcp process.
|
|
9
|
+
- Make standalone HQ auth refresh entry points run through npm bin symlinks.
|
|
10
|
+
|
|
11
|
+
## [5.325.0] — 2026-10-02
|
|
12
|
+
- Match Bash pathname expansion in native derive-trigger-facts when a glob has missing suffixes, a trailing slash, or symlink path components.
|
|
13
|
+
|
|
5
14
|
## [5.324.0] — 2026-10-02
|
|
6
15
|
- Keep the bundled shell runner out of native fast-route startup and defer timeout-guard flag snapshot imports until a timeout would block.
|
|
7
16
|
- Reuse the parsed fast-route entry when dispatching the selected command.
|
|
@@ -15,8 +15,7 @@
|
|
|
15
15
|
import "../node-preflight.js";
|
|
16
16
|
import { acquireCurrentPackageUseLease } from "../utils/package-use-lease.js";
|
|
17
17
|
import "../node-network-compat.js";
|
|
18
|
-
import {
|
|
19
|
-
import path from "node:path";
|
|
18
|
+
import { isMainModule } from "../utils/is-main-module.js";
|
|
20
19
|
const MIN_VALIDITY_FLAG = "--min-validity-seconds";
|
|
21
20
|
function parseMinimumValiditySeconds(argv, env) {
|
|
22
21
|
let raw;
|
|
@@ -109,7 +108,7 @@ export async function startAuthRefresh(acquireLease = acquireCurrentPackageUseLe
|
|
|
109
108
|
initSentry();
|
|
110
109
|
return runAuthRefresh();
|
|
111
110
|
}
|
|
112
|
-
if (
|
|
111
|
+
if (isMainModule(import.meta.url)) {
|
|
113
112
|
void startAuthRefresh().then((exitCode) => process.exit(exitCode));
|
|
114
113
|
}
|
|
115
114
|
//# sourceMappingURL=hq-auth-refresh.js.map
|
|
@@ -3720,6 +3720,15 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
3720
3720
|
readonly arguments: readonly [];
|
|
3721
3721
|
readonly options: readonly [];
|
|
3722
3722
|
readonly subcommands: readonly [{
|
|
3723
|
+
readonly name: "serve";
|
|
3724
|
+
readonly description: "Run the HQ stdio MCP server; every tool call is answered by hqd (`hq daemon status`)";
|
|
3725
|
+
readonly aliases: readonly [];
|
|
3726
|
+
readonly hidden: false;
|
|
3727
|
+
readonly usage: "[options]";
|
|
3728
|
+
readonly arguments: readonly [];
|
|
3729
|
+
readonly options: readonly [];
|
|
3730
|
+
readonly subcommands: readonly [];
|
|
3731
|
+
}, {
|
|
3723
3732
|
readonly name: "status";
|
|
3724
3733
|
readonly description: "Show installed MCP packs/servers and their health in BOTH Claude and Codex (PARTIAL/DRIFT flagged; secrets redacted)";
|
|
3725
3734
|
readonly aliases: readonly ["list"];
|
|
@@ -9927,7 +9936,7 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
9927
9936
|
readonly subcommands: readonly [];
|
|
9928
9937
|
}, {
|
|
9929
9938
|
readonly name: "call";
|
|
9930
|
-
readonly description: "Send one request to hqd (policy.check, journal.append, memory.get, memory.put, mesh.presence, …) and print the result as JSON";
|
|
9939
|
+
readonly description: "Send one request to hqd (policy.check, journal.append, memory.get, memory.put, mesh.presence, mcp.call, …) and print the result as JSON";
|
|
9931
9940
|
readonly aliases: readonly [];
|
|
9932
9941
|
readonly hidden: false;
|
|
9933
9942
|
readonly usage: "[options] <op>";
|
|
@@ -4801,6 +4801,16 @@ export const COMMAND_CATALOG = [
|
|
|
4801
4801
|
"arguments": [],
|
|
4802
4802
|
"options": [],
|
|
4803
4803
|
"subcommands": [
|
|
4804
|
+
{
|
|
4805
|
+
"name": "serve",
|
|
4806
|
+
"description": "Run the HQ stdio MCP server; every tool call is answered by hqd (`hq daemon status`)",
|
|
4807
|
+
"aliases": [],
|
|
4808
|
+
"hidden": false,
|
|
4809
|
+
"usage": "[options]",
|
|
4810
|
+
"arguments": [],
|
|
4811
|
+
"options": [],
|
|
4812
|
+
"subcommands": []
|
|
4813
|
+
},
|
|
4804
4814
|
{
|
|
4805
4815
|
"name": "status",
|
|
4806
4816
|
"description": "Show installed MCP packs/servers and their health in BOTH Claude and Codex (PARTIAL/DRIFT flagged; secrets redacted)",
|
|
@@ -12789,7 +12799,7 @@ export const COMMAND_CATALOG = [
|
|
|
12789
12799
|
},
|
|
12790
12800
|
{
|
|
12791
12801
|
"name": "call",
|
|
12792
|
-
"description": "Send one request to hqd (policy.check, journal.append, memory.get, memory.put, mesh.presence, …) and print the result as JSON",
|
|
12802
|
+
"description": "Send one request to hqd (policy.check, journal.append, memory.get, memory.put, mesh.presence, mcp.call, …) and print the result as JSON",
|
|
12793
12803
|
"aliases": [],
|
|
12794
12804
|
"hidden": false,
|
|
12795
12805
|
"usage": "[options] <op>",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
1
|
+
import { HQ_ANYWHERE_RUNTIME_FLAG } from "../lib/anywhere-runtime-flag-key.js";
|
|
2
|
+
export { HQ_ANYWHERE_RUNTIME_FLAG };
|
|
3
3
|
export declare class AnywhereRuntimeDisabledError extends Error {
|
|
4
4
|
readonly expected: true;
|
|
5
5
|
constructor();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { resolveFlagGate, resolveProcessFlagGate } from "../lib/flag-registry.js";
|
|
2
|
-
|
|
3
|
-
export
|
|
2
|
+
import { HQ_ANYWHERE_RUNTIME_FLAG } from "../lib/anywhere-runtime-flag-key.js";
|
|
3
|
+
export { HQ_ANYWHERE_RUNTIME_FLAG };
|
|
4
4
|
export class AnywhereRuntimeDisabledError extends Error {
|
|
5
5
|
expected = true;
|
|
6
6
|
constructor() {
|
|
@@ -62,22 +62,50 @@ function globSegmentMatches(pattern, value) {
|
|
|
62
62
|
return false;
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
|
+
function filesystemPath(candidate, cwd) {
|
|
66
|
+
const value = candidate || ".";
|
|
67
|
+
return path.isAbsolute(value) ? value : `${cwd}${path.sep}${value}`;
|
|
68
|
+
}
|
|
69
|
+
function isDirectoryPath(candidate, cwd) {
|
|
70
|
+
try {
|
|
71
|
+
return fs.statSync(filesystemPath(candidate, cwd)).isDirectory();
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function pathEntryExists(candidate, cwd) {
|
|
78
|
+
try {
|
|
79
|
+
fs.lstatSync(filesystemPath(candidate, cwd));
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
65
86
|
function expandBashGlob(token, cwd) {
|
|
66
87
|
if (!hasGlobPattern(token))
|
|
67
88
|
return [token];
|
|
68
89
|
const absolute = token.startsWith(path.sep);
|
|
90
|
+
const trailingSlash = token.endsWith(path.sep);
|
|
69
91
|
const parts = token.split(path.sep).filter((part, index) => !(absolute && index === 0));
|
|
92
|
+
if (trailingSlash)
|
|
93
|
+
parts.pop();
|
|
70
94
|
let candidates = [absolute ? path.sep : ""];
|
|
71
95
|
let matchedPattern = false;
|
|
72
|
-
for (const part of parts) {
|
|
96
|
+
for (const [index, part] of parts.entries()) {
|
|
73
97
|
if (!hasGlobPattern(part)) {
|
|
74
98
|
candidates = candidates.map((parent) => !parent ? part : parent === path.sep ? `${path.sep}${part}` : `${parent}${path.sep}${part}`);
|
|
99
|
+
if (matchedPattern) {
|
|
100
|
+
const needsDirectory = index < parts.length - 1 || trailingSlash;
|
|
101
|
+
candidates = candidates.filter((candidate) => needsDirectory ? isDirectoryPath(candidate, cwd) : pathEntryExists(candidate, cwd));
|
|
102
|
+
}
|
|
75
103
|
continue;
|
|
76
104
|
}
|
|
77
105
|
matchedPattern = true;
|
|
78
106
|
const expanded = [];
|
|
79
107
|
for (const parent of candidates) {
|
|
80
|
-
const directory =
|
|
108
|
+
const directory = filesystemPath(parent || ".", cwd);
|
|
81
109
|
let names;
|
|
82
110
|
try {
|
|
83
111
|
names = fs.readdirSync(directory).sort();
|
|
@@ -97,7 +125,13 @@ function expandBashGlob(token, cwd) {
|
|
|
97
125
|
return [token];
|
|
98
126
|
candidates = expanded;
|
|
99
127
|
}
|
|
100
|
-
|
|
128
|
+
if (trailingSlash) {
|
|
129
|
+
candidates = candidates
|
|
130
|
+
.filter((candidate) => isDirectoryPath(candidate, cwd))
|
|
131
|
+
.map((candidate) => candidate.endsWith(path.sep) ? candidate : `${candidate}${path.sep}`)
|
|
132
|
+
.sort();
|
|
133
|
+
}
|
|
134
|
+
return matchedPattern && candidates.length > 0 ? candidates : [token];
|
|
101
135
|
}
|
|
102
136
|
function bashUnquotedWords(facts, cwd) {
|
|
103
137
|
return facts.join(" ").split(/[\t\n ]+/).filter(Boolean).flatMap((word) => expandBashGlob(word, cwd));
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
|
-
import
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { isMainModule } from "../utils/is-main-module.js";
|
|
5
4
|
function signalNumber(signal) {
|
|
6
5
|
if (!signal)
|
|
7
6
|
return 0;
|
|
@@ -40,7 +39,7 @@ export async function runRemoteLauncher(argv, injected = {}) {
|
|
|
40
39
|
await deps.sleep(15_000);
|
|
41
40
|
return exitCode;
|
|
42
41
|
}
|
|
43
|
-
if (
|
|
42
|
+
if (isMainModule(import.meta.url)) {
|
|
44
43
|
void runRemoteLauncher(process.argv.slice(2))
|
|
45
44
|
.then((exitCode) => { process.exitCode = exitCode; })
|
|
46
45
|
.catch((error) => {
|
package/dist/commands/daemon.js
CHANGED
|
@@ -769,7 +769,7 @@ export function registerDaemonCommand(program, over = {}) {
|
|
|
769
769
|
}));
|
|
770
770
|
daemon
|
|
771
771
|
.command("call <op>")
|
|
772
|
-
.description("Send one request to hqd (policy.check, journal.append, memory.get, memory.put, mesh.presence, …) and print the result as JSON")
|
|
772
|
+
.description("Send one request to hqd (policy.check, journal.append, memory.get, memory.put, mesh.presence, mcp.call, …) and print the result as JSON")
|
|
773
773
|
.option("--json <args>", "Request args as a JSON object", "{}")
|
|
774
774
|
.option("--timeout <ms>", "Give up after this many milliseconds")
|
|
775
775
|
.action(wrap(async (d, op, opts) => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { AnywhereRuntimeDisabledError } from './anywhere-runtime-flag.js';
|
|
3
|
+
import { type HookTarget } from '../lib/install/claude-targets.js';
|
|
3
4
|
export interface GlobalInstallOptions {
|
|
4
5
|
/** HQ checkout to install from (absolute). */
|
|
5
6
|
hqRoot: string;
|
|
@@ -14,7 +15,16 @@ export interface GlobalInstallOptions {
|
|
|
14
15
|
/** Registry file override (default ~/.hq/registry.json under `home`). */
|
|
15
16
|
registryFile?: string;
|
|
16
17
|
log?: (line: string) => void;
|
|
18
|
+
/** Is the hq daemon installed? Default: look for its launchd/systemd unit under `home`. */
|
|
19
|
+
daemonInstalled?: boolean;
|
|
17
20
|
}
|
|
21
|
+
/** True when an hq daemon unit (launchd plist or systemd unit) exists. */
|
|
22
|
+
export declare function isDaemonInstalled(home: string, platform?: NodeJS.Platform, queryTask?: (task: string) => boolean): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* US-010: with hqd installed (and the checkout shipping the shim), hooks go to
|
|
25
|
+
* the hqd shim; otherwise to master-hook.sh / the Codex adapter.
|
|
26
|
+
*/
|
|
27
|
+
export declare function hookTargetFor(hqRoot: string, home: string, daemonInstalled?: boolean, env?: NodeJS.ProcessEnv): HookTarget;
|
|
18
28
|
export { AnywhereRuntimeDisabledError as AnywhereDisabledError };
|
|
19
29
|
export declare const isAnywhereEnabled: (_home: string, env?: NodeJS.ProcessEnv) => boolean;
|
|
20
30
|
/** Install HQ into Claude Code user scope. Returns the files touched (or that would be). */
|
|
@@ -10,13 +10,38 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import * as fs from 'fs';
|
|
12
12
|
import * as path from 'path';
|
|
13
|
-
import {
|
|
13
|
+
import { spawnSync } from 'node:child_process';
|
|
14
|
+
import { buildCodexServerDef, claudeConfigFormat, codexConfigFormat, isCodexInstalled, jsonFormat, mergeCodexServer, registerCodexServer, unregisterCodexServer, mergeClaudeServer, readConfigDoc, realpathOrSelf, registerClaudeServer, resolveEnv, unregisterClaudeServer, writeConfigAtomic, } from './mcp-registration.js';
|
|
14
15
|
import { seedRegistryFromHqRoot } from './link.js';
|
|
15
16
|
import { resolveHqRoot } from './doctor.js';
|
|
16
17
|
import { anywhereRuntimeEnabled, AnywhereRuntimeDisabledError } from './anywhere-runtime-flag.js';
|
|
17
18
|
import { readRegistry, registryDir, registryFile, writeRegistry } from '../lib/registry/registry.js';
|
|
19
|
+
import { DAEMON_LAUNCHD_LABEL, DAEMON_SYSTEMD_UNIT, DAEMON_WINDOWS_TASK, SYSTEM_UNIT_DIR } from '../lib/daemon/install.js';
|
|
20
|
+
import { readDaemonConfig } from '../lib/daemon/config.js';
|
|
21
|
+
import { daemonPaths } from '../lib/daemon/paths.js';
|
|
18
22
|
import { addAgentsMdBlock, addCodexHooks, classifyLegacySkills, codexAdapterPath, codexCharterSource, codexTargets, hasAllCodexHooks, HQ_OWNED_MARKER, LEGACY_BACKUP_NAME, LEGACY_SKILLS_NAME, removeAgentsMdBlock, removeCodexHooks, } from '../lib/install/codex-targets.js';
|
|
19
|
-
import { addClaudeMdBlock, addHqHooks, charterPath, claudeTargets, hasAllHqHooks, HQ_ANYWHERE_PACK, HQ_MCP_SERVER_NAME, hqSkillsSource, lineDiff, masterHookPath, removeClaudeMdBlock, removeHqHooks, SKILL_LINK_PREFIX, } from '../lib/install/claude-targets.js';
|
|
23
|
+
import { addClaudeMdBlock, addHqHooks, charterPath, claudeTargets, hasAllHqHooks, HQ_ANYWHERE_PACK, HQ_MCP_SERVER_NAME, hqSkillsSource, lineDiff, masterHookPath, shimPath, MCP_SUBPROCESS_PATH, removeClaudeMdBlock, removeHqHooks, SKILL_LINK_PREFIX, } from '../lib/install/claude-targets.js';
|
|
24
|
+
/** True when an hq daemon unit (launchd plist or systemd unit) exists. */
|
|
25
|
+
export function isDaemonInstalled(home, platform = process.platform, queryTask = (task) => spawnSync('schtasks.exe', ['/Query', '/TN', task], { encoding: 'utf8', windowsHide: true }).status === 0) {
|
|
26
|
+
if (platform === 'win32')
|
|
27
|
+
return queryTask(DAEMON_WINDOWS_TASK);
|
|
28
|
+
const candidates = platform === 'darwin'
|
|
29
|
+
? [path.join(home, 'Library', 'LaunchAgents', `${DAEMON_LAUNCHD_LABEL}.plist`)]
|
|
30
|
+
: platform === 'linux'
|
|
31
|
+
? [path.join(home, '.config', 'systemd', 'user', DAEMON_SYSTEMD_UNIT), path.join(SYSTEM_UNIT_DIR, DAEMON_SYSTEMD_UNIT)]
|
|
32
|
+
: [];
|
|
33
|
+
return candidates.some((p) => fs.existsSync(p));
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* US-010: with hqd installed (and the checkout shipping the shim), hooks go to
|
|
37
|
+
* the hqd shim; otherwise to master-hook.sh / the Codex adapter.
|
|
38
|
+
*/
|
|
39
|
+
export function hookTargetFor(hqRoot, home, daemonInstalled, env = process.env) {
|
|
40
|
+
const installed = daemonInstalled ?? isDaemonInstalled(home);
|
|
41
|
+
const configuredRoot = readDaemonConfig(daemonPaths(home, env).configPath).hqRoot;
|
|
42
|
+
const daemonMatchesRoot = configuredRoot !== undefined && realpathOrSelf(configuredRoot) === realpathOrSelf(hqRoot);
|
|
43
|
+
return installed && daemonMatchesRoot && fs.existsSync(shimPath(hqRoot)) ? 'shim' : 'master';
|
|
44
|
+
}
|
|
20
45
|
export { AnywhereRuntimeDisabledError as AnywhereDisabledError };
|
|
21
46
|
export const isAnywhereEnabled = (_home, env = process.env) => anywhereRuntimeEnabled(env);
|
|
22
47
|
function readOrNull(file) {
|
|
@@ -30,11 +55,10 @@ function readOrNull(file) {
|
|
|
30
55
|
}
|
|
31
56
|
}
|
|
32
57
|
function writeFileAtomic(file, data) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const tmp = `${target}.hq-tmp-${process.pid}`;
|
|
58
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
59
|
+
const tmp = `${file}.hq-tmp-${process.pid}`;
|
|
36
60
|
fs.writeFileSync(tmp, data);
|
|
37
|
-
fs.renameSync(tmp,
|
|
61
|
+
fs.renameSync(tmp, file);
|
|
38
62
|
}
|
|
39
63
|
function canonical(v) {
|
|
40
64
|
return JSON.stringify(v, (_k, val) => val && typeof val === 'object' && !Array.isArray(val)
|
|
@@ -42,12 +66,12 @@ function canonical(v) {
|
|
|
42
66
|
: val);
|
|
43
67
|
}
|
|
44
68
|
/** JSON settings format whose verify step checks every HQ hook is registered. */
|
|
45
|
-
function settingsInstallFormat(hqRoot) {
|
|
69
|
+
function settingsInstallFormat(hqRoot, target) {
|
|
46
70
|
return {
|
|
47
71
|
...jsonFormat,
|
|
48
72
|
emptyDoc: () => ({}),
|
|
49
73
|
assertEntry(doc) {
|
|
50
|
-
if (!hasAllHqHooks(doc, hqRoot))
|
|
74
|
+
if (!hasAllHqHooks(doc, hqRoot, target))
|
|
51
75
|
throw new Error('HQ hooks not present after write');
|
|
52
76
|
},
|
|
53
77
|
};
|
|
@@ -84,9 +108,18 @@ function restoringRemovalFormat(original, stillPresent) {
|
|
|
84
108
|
},
|
|
85
109
|
};
|
|
86
110
|
}
|
|
87
|
-
function
|
|
111
|
+
function mcpManifest(opts) {
|
|
112
|
+
const command = opts.nodePath ?? process.execPath;
|
|
113
|
+
const entry = opts.cliEntry ?? fs.realpathSync(process.argv[1] ?? '');
|
|
114
|
+
for (const p of [command, entry]) {
|
|
115
|
+
if (!path.isAbsolute(p))
|
|
116
|
+
throw new Error(`HQ MCP server path must be absolute: ${p}`);
|
|
117
|
+
}
|
|
118
|
+
return { type: 'stdio', command, args: [entry, 'mcp', 'serve'], env: { PATH: MCP_SUBPROCESS_PATH } };
|
|
119
|
+
}
|
|
120
|
+
function planSkills(hqRoot, t) {
|
|
88
121
|
const source = hqSkillsSource(hqRoot);
|
|
89
|
-
const plan = { link: [],
|
|
122
|
+
const plan = { link: [], already: [], skipped: [] };
|
|
90
123
|
let names;
|
|
91
124
|
try {
|
|
92
125
|
names = fs
|
|
@@ -115,10 +148,6 @@ function planSkills(hqRoot, t, previouslyManaged = []) {
|
|
|
115
148
|
plan.link.push(name);
|
|
116
149
|
else if (st.isSymbolicLink() && path.resolve(t.skillsDir, fs.readlinkSync(link)) === target)
|
|
117
150
|
plan.already.push(name);
|
|
118
|
-
else if (st.isSymbolicLink() && previouslyManaged.includes(name)) {
|
|
119
|
-
plan.link.push(name);
|
|
120
|
-
plan.replace.push(name);
|
|
121
|
-
}
|
|
122
151
|
else
|
|
123
152
|
plan.skipped.push(name);
|
|
124
153
|
}
|
|
@@ -164,15 +193,23 @@ export function installGlobalClaude(opts) {
|
|
|
164
193
|
const t = claudeTargets(home);
|
|
165
194
|
const prior = readState(t);
|
|
166
195
|
const regFile = opts.registryFile ?? registryFile(registryDir(home, env));
|
|
196
|
+
const manifest = mcpManifest(opts);
|
|
167
197
|
const touched = [];
|
|
168
198
|
const pointerOld = readOrNull(t.hqPointer);
|
|
169
199
|
const pointerNew = `${hqRoot}\n`;
|
|
170
200
|
const mdOld = readOrNull(t.claudeMd);
|
|
171
201
|
const mdNew = addClaudeMdBlock(mdOld, hqRoot);
|
|
172
|
-
const skills = planSkills(hqRoot, t
|
|
202
|
+
const skills = planSkills(hqRoot, t);
|
|
173
203
|
const settingsOld = readOrNull(t.settingsJson);
|
|
174
204
|
const settingsDoc = readConfigDoc(t.settingsJson, jsonFormat).doc;
|
|
175
|
-
const
|
|
205
|
+
const hookTarget = hookTargetFor(hqRoot, home, opts.daemonInstalled, env);
|
|
206
|
+
const settingsNext = addHqHooks(settingsDoc, hqRoot, hookTarget);
|
|
207
|
+
const claudeDoc = readConfigDoc(t.claudeJson, claudeConfigFormat).doc;
|
|
208
|
+
// Collision check runs here too, so a dry run surfaces a conflicting "hq" server.
|
|
209
|
+
const claudeNext = mergeClaudeServer(HQ_MCP_SERVER_NAME, {
|
|
210
|
+
...manifest,
|
|
211
|
+
_hqPack: HQ_ANYWHERE_PACK,
|
|
212
|
+
})(claudeDoc);
|
|
176
213
|
if (pointerOld !== pointerNew)
|
|
177
214
|
touched.push(t.hqPointer);
|
|
178
215
|
if (mdOld !== mdNew)
|
|
@@ -181,6 +218,8 @@ export function installGlobalClaude(opts) {
|
|
|
181
218
|
touched.push(path.join(t.skillsDir, `${SKILL_LINK_PREFIX}${s}`));
|
|
182
219
|
if (settingsNext !== settingsDoc)
|
|
183
220
|
touched.push(t.settingsJson);
|
|
221
|
+
if (claudeNext !== claudeDoc)
|
|
222
|
+
touched.push(t.claudeJson);
|
|
184
223
|
touched.push(regFile, t.stateFile);
|
|
185
224
|
if (opts.dryRun) {
|
|
186
225
|
log(`hq install --global --runtime claude --dry-run (HQ root ${hqRoot})`);
|
|
@@ -192,29 +231,33 @@ export function installGlobalClaude(opts) {
|
|
|
192
231
|
}
|
|
193
232
|
log(`\n--- ${t.settingsJson}`);
|
|
194
233
|
log(lineDiff(jsonFormat.serialize(settingsDoc), jsonFormat.serialize(settingsNext)));
|
|
234
|
+
log(`\n--- ${t.claudeJson}`);
|
|
235
|
+
log(lineDiff(jsonFormat.serialize(claudeDoc), jsonFormat.serialize(claudeNext)));
|
|
195
236
|
return touched;
|
|
196
237
|
}
|
|
197
238
|
writeFileAtomic(t.hqPointer, pointerNew);
|
|
198
239
|
writeFileAtomic(t.claudeMd, mdNew);
|
|
199
240
|
fs.mkdirSync(t.skillsDir, { recursive: true });
|
|
200
|
-
for (const name of skills.replace)
|
|
201
|
-
fs.unlinkSync(path.join(t.skillsDir, `${SKILL_LINK_PREFIX}${name}`));
|
|
202
241
|
for (const name of skills.link) {
|
|
203
242
|
fs.symlinkSync(path.join(hqSkillsSource(hqRoot), name), path.join(t.skillsDir, `${SKILL_LINK_PREFIX}${name}`));
|
|
204
243
|
}
|
|
205
244
|
writeConfigAtomic({
|
|
206
245
|
target: t.settingsJson,
|
|
207
246
|
pack: HQ_ANYWHERE_PACK,
|
|
208
|
-
format: settingsInstallFormat(hqRoot),
|
|
209
|
-
merge: (doc) => addHqHooks(doc, hqRoot),
|
|
247
|
+
format: settingsInstallFormat(hqRoot, hookTarget),
|
|
248
|
+
merge: (doc) => addHqHooks(doc, hqRoot, hookTarget),
|
|
210
249
|
assertName: 'hooks',
|
|
211
250
|
env: { home },
|
|
212
251
|
});
|
|
252
|
+
registerClaudeServer({ name: HQ_MCP_SERVER_NAME, manifest, pack: HQ_ANYWHERE_PACK, env: { home } });
|
|
213
253
|
// Seeding is best-effort: the runtime wiring above is already in place, so a
|
|
214
254
|
// missing or unreadable manifest is reported rather than aborting the install.
|
|
215
255
|
let seeded = [];
|
|
256
|
+
let ownedSeeded = [];
|
|
216
257
|
try {
|
|
217
|
-
|
|
258
|
+
const result = seedRegistryFromHqRoot(hqRoot, { file: regFile });
|
|
259
|
+
seeded = result.added;
|
|
260
|
+
ownedSeeded = result.owned;
|
|
218
261
|
}
|
|
219
262
|
catch (e) {
|
|
220
263
|
log(`Registry not seeded from companies/manifest.yaml: ${e.message}`);
|
|
@@ -231,13 +274,13 @@ export function installGlobalClaude(opts) {
|
|
|
231
274
|
skipped: skills.skipped,
|
|
232
275
|
},
|
|
233
276
|
mcp: { name: HQ_MCP_SERVER_NAME, pack: HQ_ANYWHERE_PACK },
|
|
234
|
-
seeded: [...new Set([...(prior?.seeded ?? []), ...
|
|
277
|
+
seeded: [...new Set([...(prior?.seeded ?? []), ...ownedSeeded])].sort(),
|
|
235
278
|
};
|
|
236
279
|
writeFileAtomic(t.stateFile, `${JSON.stringify(state, null, 2)}\n`);
|
|
237
280
|
log(`HQ installed for Claude Code from ${hqRoot}.`);
|
|
238
281
|
log(` skills linked: ${skills.link.length} new, ${skills.already.length} already present, ${skills.skipped.length} skipped`);
|
|
239
|
-
log(` hooks: master-hook.sh registered in ${t.settingsJson}`);
|
|
240
|
-
log(
|
|
282
|
+
log(` hooks: ${hookTarget === 'shim' ? 'hqd-hook-shim.sh' : 'master-hook.sh'} registered in ${t.settingsJson}`);
|
|
283
|
+
log(` MCP: "${HQ_MCP_SERVER_NAME}" registered in ${t.claudeJson}`);
|
|
241
284
|
log(` registry: ${seeded.length} manifest entr${seeded.length === 1 ? 'y' : 'ies'} seeded`);
|
|
242
285
|
return touched;
|
|
243
286
|
}
|
|
@@ -367,12 +410,12 @@ function assertCodexHqRoot(hqRoot) {
|
|
|
367
410
|
throw new Error(`${hqRoot} is not an HQ checkout (missing ${p})`);
|
|
368
411
|
}
|
|
369
412
|
}
|
|
370
|
-
function hooksInstallFormat(hqRoot) {
|
|
413
|
+
function hooksInstallFormat(hqRoot, target) {
|
|
371
414
|
return {
|
|
372
415
|
...jsonFormat,
|
|
373
416
|
emptyDoc: () => ({}),
|
|
374
417
|
assertEntry(doc) {
|
|
375
|
-
if (!hasAllCodexHooks(doc, hqRoot))
|
|
418
|
+
if (!hasAllCodexHooks(doc, hqRoot, target))
|
|
376
419
|
throw new Error('HQ Codex hooks not present after write');
|
|
377
420
|
},
|
|
378
421
|
};
|
|
@@ -399,6 +442,7 @@ export function installGlobalCodex(opts) {
|
|
|
399
442
|
const hqPointer = claudeTargets(home).hqPointer;
|
|
400
443
|
const prior = readCodexState(t);
|
|
401
444
|
const regFile = opts.registryFile ?? registryFile(registryDir(home, env));
|
|
445
|
+
const manifest = mcpManifest(opts);
|
|
402
446
|
const touched = [];
|
|
403
447
|
const pointerOld = readOrNull(hqPointer);
|
|
404
448
|
const pointerNew = `${hqRoot}\n`;
|
|
@@ -406,11 +450,20 @@ export function installGlobalCodex(opts) {
|
|
|
406
450
|
const mdOld = readOrNull(t.agentsMd);
|
|
407
451
|
const mdNew = addAgentsMdBlock(mdOld, charter);
|
|
408
452
|
const legacy = classifyLegacySkills(t.skillsDir, hqRoot);
|
|
409
|
-
const
|
|
453
|
+
const legacyPath = path.join(t.skillsDir, LEGACY_SKILLS_NAME);
|
|
454
|
+
const legacyBackupPath = path.join(t.skillsDir, LEGACY_BACKUP_NAME);
|
|
455
|
+
if (legacy.kind === 'hq-dir' && fs.existsSync(legacyBackupPath)) {
|
|
456
|
+
throw new Error(`${legacyBackupPath} already exists; move it away before installing.`);
|
|
457
|
+
}
|
|
458
|
+
const skills = planSkills(hqRoot, t);
|
|
410
459
|
const hooksOld = readOrNull(t.hooksJson);
|
|
411
|
-
const tomlOld = readOrNull(t.configToml);
|
|
412
460
|
const hooksDoc = readConfigDoc(t.hooksJson, jsonFormat).doc;
|
|
413
|
-
const
|
|
461
|
+
const hookTarget = hookTargetFor(hqRoot, home, opts.daemonInstalled, env);
|
|
462
|
+
const hooksNext = addCodexHooks(hooksDoc, hqRoot, hookTarget);
|
|
463
|
+
const tomlOld = readOrNull(t.configToml);
|
|
464
|
+
const tomlDoc = readConfigDoc(t.configToml, codexConfigFormat).doc;
|
|
465
|
+
// Collision check runs here too, so a dry run surfaces a conflicting "hq" server.
|
|
466
|
+
const tomlNext = mergeCodexServer(HQ_MCP_SERVER_NAME, buildCodexServerDef(manifest, HQ_ANYWHERE_PACK, () => null, new Set()))(tomlDoc);
|
|
414
467
|
if (pointerOld !== pointerNew)
|
|
415
468
|
touched.push(hqPointer);
|
|
416
469
|
if (mdOld !== mdNew)
|
|
@@ -421,6 +474,8 @@ export function installGlobalCodex(opts) {
|
|
|
421
474
|
touched.push(path.join(t.skillsDir, `${SKILL_LINK_PREFIX}${s}`));
|
|
422
475
|
if (hooksNext !== hooksDoc)
|
|
423
476
|
touched.push(t.hooksJson);
|
|
477
|
+
if (tomlNext !== tomlDoc)
|
|
478
|
+
touched.push(t.configToml);
|
|
424
479
|
touched.push(regFile, t.stateFile);
|
|
425
480
|
if (opts.dryRun) {
|
|
426
481
|
log(`hq install --global --runtime codex --dry-run (HQ root ${hqRoot})`);
|
|
@@ -437,16 +492,14 @@ export function installGlobalCodex(opts) {
|
|
|
437
492
|
}
|
|
438
493
|
log(`\n--- ${t.hooksJson}`);
|
|
439
494
|
log(lineDiff(jsonFormat.serialize(hooksDoc), jsonFormat.serialize(hooksNext)));
|
|
495
|
+
log(`\n--- ${t.configToml}`);
|
|
496
|
+
log(lineDiff(codexConfigFormat.serialize(tomlDoc), codexConfigFormat.serialize(tomlNext)));
|
|
440
497
|
return touched;
|
|
441
498
|
}
|
|
442
|
-
if (legacy.kind === 'hq-dir' && fs.existsSync(path.join(t.skillsDir, LEGACY_BACKUP_NAME))) {
|
|
443
|
-
throw new Error(`${path.join(t.skillsDir, LEGACY_BACKUP_NAME)} already exists; move it away before installing.`);
|
|
444
|
-
}
|
|
445
499
|
writeFileAtomic(hqPointer, pointerNew);
|
|
446
500
|
writeFileAtomic(t.agentsMd, mdNew);
|
|
447
501
|
// Legacy mirror: replace only when HQ-owned; record enough to put it back.
|
|
448
502
|
let legacyRecord = prior?.legacy ?? { kind: 'none' };
|
|
449
|
-
const legacyPath = path.join(t.skillsDir, LEGACY_SKILLS_NAME);
|
|
450
503
|
if (legacy.kind === 'hq-symlink') {
|
|
451
504
|
fs.unlinkSync(legacyPath);
|
|
452
505
|
legacyRecord = { kind: 'symlink', target: legacy.target };
|
|
@@ -459,22 +512,24 @@ export function installGlobalCodex(opts) {
|
|
|
459
512
|
else if (legacy.kind === 'foreign')
|
|
460
513
|
log(legacyWarning(t.skillsDir));
|
|
461
514
|
fs.mkdirSync(t.skillsDir, { recursive: true });
|
|
462
|
-
for (const name of skills.replace)
|
|
463
|
-
fs.unlinkSync(path.join(t.skillsDir, `${SKILL_LINK_PREFIX}${name}`));
|
|
464
515
|
for (const name of skills.link) {
|
|
465
516
|
fs.symlinkSync(path.join(hqSkillsSource(hqRoot), name), path.join(t.skillsDir, `${SKILL_LINK_PREFIX}${name}`));
|
|
466
517
|
}
|
|
467
518
|
writeConfigAtomic({
|
|
468
519
|
target: t.hooksJson,
|
|
469
520
|
pack: HQ_ANYWHERE_PACK,
|
|
470
|
-
format: hooksInstallFormat(hqRoot),
|
|
471
|
-
merge: (doc) => addCodexHooks(doc, hqRoot),
|
|
521
|
+
format: hooksInstallFormat(hqRoot, hookTarget),
|
|
522
|
+
merge: (doc) => addCodexHooks(doc, hqRoot, hookTarget),
|
|
472
523
|
assertName: 'hooks',
|
|
473
524
|
env: { home },
|
|
474
525
|
});
|
|
526
|
+
registerCodexServer({ name: HQ_MCP_SERVER_NAME, manifest, pack: HQ_ANYWHERE_PACK, env: { home } });
|
|
475
527
|
let seeded = [];
|
|
528
|
+
let ownedSeeded = [];
|
|
476
529
|
try {
|
|
477
|
-
|
|
530
|
+
const result = seedRegistryFromHqRoot(hqRoot, { file: regFile });
|
|
531
|
+
seeded = result.added;
|
|
532
|
+
ownedSeeded = result.owned;
|
|
478
533
|
}
|
|
479
534
|
catch (e) {
|
|
480
535
|
log(`Registry not seeded from companies/manifest.yaml: ${e.message}`);
|
|
@@ -492,13 +547,13 @@ export function installGlobalCodex(opts) {
|
|
|
492
547
|
},
|
|
493
548
|
legacy: legacyRecord,
|
|
494
549
|
mcp: { name: HQ_MCP_SERVER_NAME, pack: HQ_ANYWHERE_PACK },
|
|
495
|
-
seeded: [...new Set([...(prior?.seeded ?? []), ...
|
|
550
|
+
seeded: [...new Set([...(prior?.seeded ?? []), ...ownedSeeded])].sort(),
|
|
496
551
|
};
|
|
497
552
|
writeFileAtomic(t.stateFile, `${JSON.stringify(state, null, 2)}\n`);
|
|
498
553
|
log(`HQ installed for Codex from ${hqRoot}.`);
|
|
499
554
|
log(` skills linked: ${skills.link.length} new, ${skills.already.length} already present, ${skills.skipped.length} skipped`);
|
|
500
|
-
log(` hooks: ${codexAdapterPath(hqRoot)} registered in ${t.hooksJson}`);
|
|
501
|
-
log(
|
|
555
|
+
log(` hooks: ${hookTarget === 'shim' ? shimPath(hqRoot) : codexAdapterPath(hqRoot)} registered in ${t.hooksJson}`);
|
|
556
|
+
log(` MCP: "${HQ_MCP_SERVER_NAME}" registered in ${t.configToml}`);
|
|
502
557
|
return touched;
|
|
503
558
|
}
|
|
504
559
|
/** Reverse every Codex install step; non-HQ entries are left untouched. */
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `hq mcp serve` (hq-anywhere US-011): a stdio MCP server that forwards every
|
|
3
|
+
* tool call to hqd over its socket.
|
|
4
|
+
*
|
|
5
|
+
* It keeps no state between requests, so an orphaned instance costs nothing
|
|
6
|
+
* (policy mcp-process-cleanup). src/index.ts routes `hq mcp serve` straight to
|
|
7
|
+
* {@link runMcpServe} without loading the command graph, which keeps start-up
|
|
8
|
+
* under 100ms. The MCP SDK is not loaded either: the stdio transport is
|
|
9
|
+
* newline-delimited JSON-RPC 2.0 and is handled here directly.
|
|
10
|
+
*
|
|
11
|
+
* When hqd is unreachable every tool call returns an isError result naming
|
|
12
|
+
* `hq daemon status`, and the server keeps running.
|
|
13
|
+
*/
|
|
14
|
+
export declare const MCP_PROTOCOL_VERSIONS: readonly ["2025-06-18", "2025-03-26", "2024-11-05"];
|
|
15
|
+
export interface McpServeOptions {
|
|
16
|
+
input?: NodeJS.ReadableStream;
|
|
17
|
+
output?: NodeJS.WritableStream;
|
|
18
|
+
env?: NodeJS.ProcessEnv;
|
|
19
|
+
home?: string;
|
|
20
|
+
cwd?: string;
|
|
21
|
+
/** Per-call hqd budget. Default 120s; policy checks and hq-mcp tools can be slow. */
|
|
22
|
+
timeoutMs?: number;
|
|
23
|
+
}
|
|
24
|
+
/** HQ root for prompts: HQ_ROOT, else the ~/.hq/root pointer written by `hq install --global`. */
|
|
25
|
+
export declare function readHqRoot(env: NodeJS.ProcessEnv, home: string): string | null;
|
|
26
|
+
/** Run the server until input ends. Resolves when stdin closes. */
|
|
27
|
+
export declare function runMcpServe(opts?: McpServeOptions): Promise<void>;
|
|
28
|
+
//# sourceMappingURL=mcp-serve.d.ts.map
|