@indigoai-us/hq-cli 5.325.0 → 5.327.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.
Files changed (77) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/bin/hq-auth-refresh.js +2 -3
  3. package/dist/command-catalog.generated.d.ts +19 -1
  4. package/dist/command-catalog.generated.js +21 -1
  5. package/dist/commands/anywhere-runtime-flag.d.ts +2 -2
  6. package/dist/commands/anywhere-runtime-flag.js +2 -2
  7. package/dist/commands/auth.js +3 -1
  8. package/dist/commands/core-detect-stale-review-base-fast.d.ts +2 -0
  9. package/dist/commands/core-detect-stale-review-base-fast.js +23 -0
  10. package/dist/commands/core-job-remote-launcher.js +2 -3
  11. package/dist/commands/core-register-project-native.js +1 -1
  12. package/dist/commands/core-repo-run-registry-fast.d.ts +2 -0
  13. package/dist/commands/core-repo-run-registry-fast.js +35 -0
  14. package/dist/commands/core-rrr-worktree-scope-fix-fast-flag.d.ts +7 -0
  15. package/dist/commands/core-rrr-worktree-scope-fix-fast-flag.js +25 -0
  16. package/dist/commands/core-session-journal-native.js +1 -1
  17. package/dist/commands/core-share-suggestion-state-fast.d.ts +2 -0
  18. package/dist/commands/core-share-suggestion-state-fast.js +25 -0
  19. package/dist/commands/daemon.js +1 -1
  20. package/dist/commands/install-global.d.ts +10 -0
  21. package/dist/commands/install-global.js +96 -41
  22. package/dist/commands/logout.js +2 -0
  23. package/dist/commands/mcp-serve.d.ts +28 -0
  24. package/dist/commands/mcp-serve.js +165 -0
  25. package/dist/commands/mcp-status.js +14 -0
  26. package/dist/fast-core.js +3 -6
  27. package/dist/fast-scaffold.js +5 -1
  28. package/dist/index.d.ts +3 -0
  29. package/dist/index.js +9 -0
  30. package/dist/lanes-drain-hook.js +2 -2
  31. package/dist/lib/anywhere-runtime-fast-flag.d.ts +3 -0
  32. package/dist/lib/anywhere-runtime-fast-flag.js +23 -0
  33. package/dist/lib/anywhere-runtime-flag-key.d.ts +3 -0
  34. package/dist/lib/anywhere-runtime-flag-key.js +3 -0
  35. package/dist/lib/daemon/mcp-endpoint.d.ts +8 -0
  36. package/dist/lib/daemon/mcp-endpoint.js +30 -0
  37. package/dist/lib/daemon/mcp-oauth.d.ts +24 -0
  38. package/dist/lib/daemon/mcp-oauth.js +298 -0
  39. package/dist/lib/daemon/ops/mcp.d.ts +53 -0
  40. package/dist/lib/daemon/ops/mcp.js +217 -0
  41. package/dist/lib/daemon/protocol.d.ts +2 -1
  42. package/dist/lib/daemon/protocol.js +3 -1
  43. package/dist/lib/daemon/server.d.ts +5 -0
  44. package/dist/lib/daemon/server.js +78 -11
  45. package/dist/lib/install/claude-targets.d.ts +11 -4
  46. package/dist/lib/install/claude-targets.js +21 -9
  47. package/dist/lib/install/codex-targets.d.ts +8 -4
  48. package/dist/lib/install/codex-targets.js +18 -9
  49. package/dist/lib/lanes/watcher-main.js +2 -4
  50. package/dist/lib/mcp/hosted-search-tools.d.ts +4 -0
  51. package/dist/lib/mcp/hosted-search-tools.js +28 -0
  52. package/dist/lib/mcp/hq-mcp-tools.generated.d.ts +3 -0
  53. package/dist/lib/mcp/hq-mcp-tools.generated.js +538 -0
  54. package/dist/lib/mcp/prompts.d.ts +30 -0
  55. package/dist/lib/mcp/prompts.js +48 -0
  56. package/dist/lib/mcp/tools.d.ts +22 -0
  57. package/dist/lib/mcp/tools.js +82 -0
  58. package/dist/lib/mcp/types.d.ts +6 -0
  59. package/dist/lib/mcp/types.js +2 -0
  60. package/dist/lib/process-flag-company-context-fast.d.ts +4 -0
  61. package/dist/lib/process-flag-company-context-fast.js +20 -0
  62. package/dist/lib/process-flag-company-context-light.d.ts +9 -0
  63. package/dist/lib/process-flag-company-context-light.js +81 -0
  64. package/dist/lib/process-flag-company-context.d.ts +2 -4
  65. package/dist/lib/process-flag-company-context.js +2 -46
  66. package/dist/utils/hq-roots-core.d.ts +130 -0
  67. package/dist/utils/hq-roots-core.js +354 -0
  68. package/dist/utils/hq-roots.d.ts +2 -129
  69. package/dist/utils/hq-roots.js +2 -353
  70. package/dist/utils/is-main-module.d.ts +3 -0
  71. package/dist/utils/is-main-module.js +19 -0
  72. package/dist/utils/package-root-diagnostics.d.ts +5 -36
  73. package/dist/utils/package-root-diagnostics.js +4 -83
  74. package/dist/utils/package-root-resolution-error.d.ts +32 -0
  75. package/dist/utils/package-root-resolution-error.js +79 -0
  76. package/dist/utils/staged-npm-install.js +2 -1
  77. package/package.json +3 -1
@@ -0,0 +1,3 @@
1
+ /** hq-flags key for the staged HQ Anywhere runtime. The registry default is off. */
2
+ export declare const HQ_ANYWHERE_RUNTIME_FLAG = "hq-anywhere-runtime";
3
+ //# sourceMappingURL=anywhere-runtime-flag-key.d.ts.map
@@ -0,0 +1,3 @@
1
+ /** hq-flags key for the staged HQ Anywhere runtime. The registry default is off. */
2
+ export const HQ_ANYWHERE_RUNTIME_FLAG = "hq-anywhere-runtime";
3
+ //# sourceMappingURL=anywhere-runtime-flag-key.js.map
@@ -0,0 +1,8 @@
1
+ export declare class McpEndpointError extends Error {
2
+ readonly code = "hq_mcp_config_error";
3
+ }
4
+ /** Derive the hosted MCP sibling from the same hq-pro URL used by hq-cli. */
5
+ export declare function resolveHqMcpEndpoint(env?: NodeJS.ProcessEnv): URL;
6
+ /** OAuth endpoints share the origin with the Streamable HTTP MCP endpoint. */
7
+ export declare function resolveHqMcpOAuthOrigin(env?: NodeJS.ProcessEnv): URL;
8
+ //# sourceMappingURL=mcp-endpoint.d.ts.map
@@ -0,0 +1,30 @@
1
+ import { DEFAULT_VAULT_API_URL } from "../../utils/cognito-session.js";
2
+ export class McpEndpointError extends Error {
3
+ code = "hq_mcp_config_error";
4
+ }
5
+ /** Derive the hosted MCP sibling from the same hq-pro URL used by hq-cli. */
6
+ export function resolveHqMcpEndpoint(env = process.env) {
7
+ const apiUrl = env.HQ_VAULT_API_URL?.trim() || DEFAULT_VAULT_API_URL;
8
+ let api;
9
+ try {
10
+ api = new URL(apiUrl);
11
+ }
12
+ catch {
13
+ throw new McpEndpointError("HQ's configured API URL is invalid; check HQ_VAULT_API_URL.");
14
+ }
15
+ if (api.protocol !== "https:" || !api.hostname.startsWith("hqapi.")) {
16
+ throw new McpEndpointError("Hosted hq-mcp requires an HTTPS hqapi host in HQ_VAULT_API_URL.");
17
+ }
18
+ api.hostname = `hq-mcp.${api.hostname.slice("hqapi.".length)}`;
19
+ api.pathname = "/mcp";
20
+ api.search = "";
21
+ api.hash = "";
22
+ return api;
23
+ }
24
+ /** OAuth endpoints share the origin with the Streamable HTTP MCP endpoint. */
25
+ export function resolveHqMcpOAuthOrigin(env = process.env) {
26
+ const endpoint = resolveHqMcpEndpoint(env);
27
+ endpoint.pathname = "/";
28
+ return endpoint;
29
+ }
30
+ //# sourceMappingURL=mcp-endpoint.js.map
@@ -0,0 +1,24 @@
1
+ export declare class McpOAuthError extends Error {
2
+ readonly code = "hq_mcp_auth_required";
3
+ constructor(message: string);
4
+ }
5
+ export interface McpOAuthOptions {
6
+ env?: NodeJS.ProcessEnv;
7
+ home?: string;
8
+ oauthOrigin?: URL;
9
+ fetchImpl?: typeof fetch;
10
+ openBrowser?: (url: string) => Promise<unknown>;
11
+ writeOutput?: (message: string) => void;
12
+ isEnabled?: () => boolean;
13
+ isMachine?: () => boolean;
14
+ now?: () => number;
15
+ callbackTimeoutMs?: number;
16
+ }
17
+ export declare function mcpOAuthCacheFile(options?: McpOAuthOptions): string;
18
+ /** Perform hosted DCR + PKCE login and cache only the hq-mcp audience tokens. */
19
+ export declare function loginHostedMcp(options?: McpOAuthOptions): Promise<void>;
20
+ /** Load or refresh a person's hosted MCP token without printing token material. */
21
+ export declare function getMcpAudienceToken(options?: McpOAuthOptions): Promise<string>;
22
+ /** Remove all per-origin hosted MCP OAuth caches during either HQ logout flow. */
23
+ export declare function clearMcpOAuthCaches(options?: Pick<McpOAuthOptions, "env" | "home">): void;
24
+ //# sourceMappingURL=mcp-oauth.d.ts.map
@@ -0,0 +1,298 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import * as fs from "node:fs";
3
+ import * as http from "node:http";
4
+ import * as os from "node:os";
5
+ import * as path from "node:path";
6
+ import open from "open";
7
+ import { resolveCognitoTokenSource } from "../../utils/cognito-session.js";
8
+ import { anywhereRuntimeEnabled } from "../../commands/anywhere-runtime-flag.js";
9
+ import { resolveHqMcpOAuthOrigin } from "./mcp-endpoint.js";
10
+ const MCP_SCOPE = "openid email profile hq-mcp/use";
11
+ const EXPIRY_SKEW_MS = 60_000;
12
+ const DEFAULT_CALLBACK_TIMEOUT_MS = 5 * 60_000;
13
+ const MAX_RESPONSE_BYTES = 64 * 1024;
14
+ export class McpOAuthError extends Error {
15
+ code = "hq_mcp_auth_required";
16
+ constructor(message) {
17
+ super(message);
18
+ this.name = "McpOAuthError";
19
+ }
20
+ }
21
+ function cacheFile(env, home, origin) {
22
+ const stateDir = env.HQ_STATE_DIR?.trim() || path.join(home, ".hq");
23
+ const originHash = createHash("sha256").update(origin.origin).digest("hex").slice(0, 16);
24
+ return path.join(path.resolve(stateDir), `mcp-oauth-tokens-${originHash}.json`);
25
+ }
26
+ function readCache(file) {
27
+ try {
28
+ const value = JSON.parse(fs.readFileSync(file, "utf8"));
29
+ if (!value || typeof value !== "object")
30
+ return undefined;
31
+ const cache = value;
32
+ if (cache.version !== 1 || typeof cache.origin !== "string" || !cache.origin ||
33
+ typeof cache.clientId !== "string" || !cache.clientId ||
34
+ typeof cache.accessToken !== "string" || !cache.accessToken ||
35
+ typeof cache.refreshToken !== "string" || !cache.refreshToken ||
36
+ typeof cache.expiresAt !== "number" || !Number.isFinite(cache.expiresAt))
37
+ return undefined;
38
+ return cache;
39
+ }
40
+ catch (error) {
41
+ if (error.code === "ENOENT" || error instanceof SyntaxError)
42
+ return undefined;
43
+ throw new McpOAuthError("The hosted MCP token cache could not be read. Check its permissions or run `hq mcp login` again.");
44
+ }
45
+ }
46
+ function writeCache(file, cache) {
47
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
48
+ const temporary = `${file}.tmp-${process.pid}-${randomBytes(6).toString("hex")}`;
49
+ const fd = fs.openSync(temporary, "wx", 0o600);
50
+ try {
51
+ fs.writeFileSync(fd, JSON.stringify(cache));
52
+ fs.fsyncSync(fd);
53
+ }
54
+ finally {
55
+ fs.closeSync(fd);
56
+ }
57
+ try {
58
+ fs.renameSync(temporary, file);
59
+ fs.chmodSync(file, 0o600);
60
+ }
61
+ catch (error) {
62
+ try {
63
+ fs.unlinkSync(temporary);
64
+ }
65
+ catch { /* retain the original write error */ }
66
+ throw error;
67
+ }
68
+ }
69
+ function asObject(value) {
70
+ return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
71
+ }
72
+ async function postForm(url, form, fetchImpl) {
73
+ let response;
74
+ try {
75
+ response = await fetchImpl(url, {
76
+ method: "POST",
77
+ headers: { "content-type": "application/x-www-form-urlencoded" },
78
+ body: form.toString(),
79
+ signal: AbortSignal.timeout(30_000),
80
+ });
81
+ }
82
+ catch {
83
+ throw new McpOAuthError("Hosted MCP sign-in could not reach the OAuth service. Retry `hq mcp login`.");
84
+ }
85
+ if (!response.ok) {
86
+ throw new McpOAuthError("Hosted MCP OAuth request failed (HTTP " + response.status + "). Retry `hq mcp login`.");
87
+ }
88
+ let data;
89
+ try {
90
+ const raw = await response.text();
91
+ if (raw.length > MAX_RESPONSE_BYTES)
92
+ throw new Error("response too large");
93
+ data = JSON.parse(raw);
94
+ }
95
+ catch {
96
+ throw new McpOAuthError("Hosted MCP OAuth returned an invalid response. Retry `hq mcp login`.");
97
+ }
98
+ const result = asObject(data);
99
+ if (!result)
100
+ throw new McpOAuthError("Hosted MCP OAuth returned an invalid response. Retry `hq mcp login`.");
101
+ return result;
102
+ }
103
+ function validTokens(data, now, previous) {
104
+ if (typeof data.access_token !== "string" || !data.access_token ||
105
+ typeof data.expires_in !== "number" || !Number.isFinite(data.expires_in) || data.expires_in <= 0)
106
+ throw new McpOAuthError("Hosted MCP OAuth did not return a usable access token.");
107
+ const refreshToken = typeof data.refresh_token === "string" && data.refresh_token
108
+ ? data.refresh_token
109
+ : previous?.refreshToken;
110
+ if (!refreshToken)
111
+ throw new McpOAuthError("Hosted MCP OAuth did not return a refresh token. Run `hq mcp login` again.");
112
+ return {
113
+ version: 1,
114
+ origin: previous?.origin ?? "",
115
+ clientId: previous?.clientId ?? "",
116
+ accessToken: data.access_token,
117
+ refreshToken,
118
+ expiresAt: now + data.expires_in * 1000,
119
+ };
120
+ }
121
+ function oauthUrl(origin, endpoint) {
122
+ return new URL(endpoint, origin);
123
+ }
124
+ function newPkce() {
125
+ const verifier = randomBytes(32).toString("base64url");
126
+ return { verifier, challenge: createHash("sha256").update(verifier).digest("base64url") };
127
+ }
128
+ async function runLogin(options) {
129
+ const env = options.env ?? process.env;
130
+ if (!(options.isEnabled ?? (() => anywhereRuntimeEnabled(env)))()) {
131
+ throw new McpOAuthError("HQ Anywhere is not enabled. Enable the hq-anywhere-runtime flag to use `hq mcp login`.");
132
+ }
133
+ if ((options.isMachine ?? (() => resolveCognitoTokenSource(env) === "machine"))()) {
134
+ throw new McpOAuthError("`hq mcp login` is for person accounts; this box uses machine credentials.");
135
+ }
136
+ const origin = options.oauthOrigin ?? resolveHqMcpOAuthOrigin(env);
137
+ const fetchImpl = options.fetchImpl ?? fetch;
138
+ const output = options.writeOutput ?? ((message) => process.stdout.write(`${message}\n`));
139
+ const pkce = newPkce();
140
+ const state = randomBytes(24).toString("base64url");
141
+ let authorizationCode;
142
+ let callbackError;
143
+ let resolveCallback;
144
+ const callback = new Promise((resolve) => { resolveCallback = resolve; });
145
+ const server = http.createServer((request, response) => {
146
+ const url = new URL(request.url ?? "/", "http://127.0.0.1");
147
+ if (request.method !== "GET" || url.pathname !== "/oauth/callback") {
148
+ response.writeHead(404).end("Not found");
149
+ return;
150
+ }
151
+ if (url.searchParams.get("state") !== state) {
152
+ callbackError = new Error("state");
153
+ }
154
+ else if (url.searchParams.has("error")) {
155
+ callbackError = new Error("authorization");
156
+ }
157
+ else {
158
+ authorizationCode = url.searchParams.get("code") ?? undefined;
159
+ if (!authorizationCode)
160
+ callbackError = new Error("code");
161
+ }
162
+ response.writeHead(callbackError ? 400 : 200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
163
+ response.end(callbackError ? "Sign-in failed. Return to the HQ CLI." : "Sign-in complete. Return to the HQ CLI.");
164
+ resolveCallback();
165
+ });
166
+ let timeout;
167
+ try {
168
+ await new Promise((resolve, reject) => {
169
+ server.once("error", reject);
170
+ server.listen(0, "127.0.0.1", resolve);
171
+ });
172
+ const address = server.address();
173
+ if (!address || typeof address === "string")
174
+ throw new McpOAuthError("Could not start the local OAuth callback listener.");
175
+ const redirectUri = `http://127.0.0.1:${address.port}/oauth/callback`;
176
+ let registrationResponse;
177
+ try {
178
+ registrationResponse = await fetchImpl(oauthUrl(origin, "/oauth/register"), {
179
+ method: "POST",
180
+ headers: { "content-type": "application/json" },
181
+ body: JSON.stringify({
182
+ redirect_uris: [redirectUri], token_endpoint_auth_method: "none",
183
+ grant_types: ["authorization_code", "refresh_token"], response_types: ["code"],
184
+ client_name: "HQ CLI",
185
+ }),
186
+ signal: AbortSignal.timeout(30_000),
187
+ });
188
+ }
189
+ catch {
190
+ throw new McpOAuthError("Hosted MCP client registration failed. Retry `hq mcp login`.");
191
+ }
192
+ if (!registrationResponse.ok)
193
+ throw new McpOAuthError(`Hosted MCP client registration failed (HTTP ${registrationResponse.status}).`);
194
+ let registration;
195
+ try {
196
+ registration = asObject(await registrationResponse.json());
197
+ }
198
+ catch { /* handled below */ }
199
+ if (!registration || typeof registration.client_id !== "string" || !registration.client_id.startsWith("dcr_")) {
200
+ throw new McpOAuthError("Hosted MCP returned an invalid client registration.");
201
+ }
202
+ const clientId = registration.client_id;
203
+ const authorize = oauthUrl(origin, "/oauth/authorize");
204
+ authorize.search = new URLSearchParams({
205
+ response_type: "code", client_id: clientId, redirect_uri: redirectUri,
206
+ code_challenge: pkce.challenge, code_challenge_method: "S256", state, scope: MCP_SCOPE,
207
+ }).toString();
208
+ const remoteShell = Boolean(env.SSH_CONNECTION || env.SSH_CLIENT || env.SSH_TTY);
209
+ if (remoteShell) {
210
+ output(`Open this URL on your workstation to sign in to hosted hq-mcp:\n${authorize.toString()}\nForward the callback port first with: ssh -L ${address.port}:127.0.0.1:${address.port} <this-host>`);
211
+ if (options.openBrowser)
212
+ await options.openBrowser(authorize.toString());
213
+ }
214
+ else
215
+ try {
216
+ await (options.openBrowser ?? ((url) => open(url)))(authorize.toString());
217
+ }
218
+ catch {
219
+ output(`Open this URL to sign in to hosted hq-mcp:\n${authorize.toString()}`);
220
+ }
221
+ timeout = setTimeout(() => {
222
+ callbackError = new Error("timeout");
223
+ resolveCallback();
224
+ }, options.callbackTimeoutMs ?? DEFAULT_CALLBACK_TIMEOUT_MS);
225
+ await callback;
226
+ if (callbackError || !authorizationCode)
227
+ throw new McpOAuthError("Hosted MCP sign-in did not complete. Retry `hq mcp login`.");
228
+ const tokens = await postForm(oauthUrl(origin, "/oauth/token"), new URLSearchParams({
229
+ grant_type: "authorization_code", client_id: clientId, code: authorizationCode,
230
+ redirect_uri: redirectUri, code_verifier: pkce.verifier,
231
+ }), fetchImpl);
232
+ const now = (options.now ?? Date.now)();
233
+ const cache = validTokens(tokens, now);
234
+ cache.origin = origin.origin;
235
+ cache.clientId = clientId;
236
+ return cache;
237
+ }
238
+ finally {
239
+ if (timeout)
240
+ clearTimeout(timeout);
241
+ await new Promise((resolve) => server.close(() => resolve()));
242
+ }
243
+ }
244
+ export function mcpOAuthCacheFile(options = {}) {
245
+ const env = options.env ?? process.env;
246
+ return cacheFile(env, options.home ?? os.homedir(), options.oauthOrigin ?? resolveHqMcpOAuthOrigin(env));
247
+ }
248
+ /** Perform hosted DCR + PKCE login and cache only the hq-mcp audience tokens. */
249
+ export async function loginHostedMcp(options = {}) {
250
+ const cache = await runLogin(options);
251
+ writeCache(mcpOAuthCacheFile(options), cache);
252
+ const writeOutput = options.writeOutput ?? ((message) => process.stdout.write(`${message}\n`));
253
+ writeOutput("Signed in to hosted hq-mcp. The MCP audience token is stored in the private HQ token cache.");
254
+ }
255
+ /** Load or refresh a person's hosted MCP token without printing token material. */
256
+ export async function getMcpAudienceToken(options = {}) {
257
+ const file = mcpOAuthCacheFile(options);
258
+ const cache = readCache(file);
259
+ const now = (options.now ?? Date.now)();
260
+ if (!cache || cache.origin !== (options.oauthOrigin ?? resolveHqMcpOAuthOrigin(options.env ?? process.env)).origin)
261
+ throw new McpOAuthError("Sign in to hosted hq-mcp with `hq mcp login` first.");
262
+ if (cache.expiresAt > now + EXPIRY_SKEW_MS)
263
+ return cache.accessToken;
264
+ const origin = options.oauthOrigin ?? resolveHqMcpOAuthOrigin(options.env ?? process.env);
265
+ const tokens = await postForm(oauthUrl(origin, "/oauth/token"), new URLSearchParams({
266
+ grant_type: "refresh_token", client_id: cache.clientId, refresh_token: cache.refreshToken,
267
+ }), options.fetchImpl ?? fetch);
268
+ const refreshed = validTokens(tokens, now, cache);
269
+ refreshed.clientId = cache.clientId;
270
+ writeCache(file, refreshed);
271
+ return refreshed.accessToken;
272
+ }
273
+ /** Remove all per-origin hosted MCP OAuth caches during either HQ logout flow. */
274
+ export function clearMcpOAuthCaches(options = {}) {
275
+ const env = options.env ?? process.env;
276
+ const stateDir = path.resolve(env.HQ_STATE_DIR?.trim() || path.join(options.home ?? os.homedir(), ".hq"));
277
+ let entries;
278
+ try {
279
+ entries = fs.readdirSync(stateDir);
280
+ }
281
+ catch (error) {
282
+ if (error.code === "ENOENT")
283
+ return;
284
+ throw new McpOAuthError("The hosted MCP token cache could not be cleared. Check its directory permissions.");
285
+ }
286
+ for (const entry of entries) {
287
+ if (entry !== "mcp-oauth-tokens.json" && !/^mcp-oauth-tokens-[a-f0-9]{16}\.json$/.test(entry))
288
+ continue;
289
+ try {
290
+ fs.unlinkSync(path.join(stateDir, entry));
291
+ }
292
+ catch (error) {
293
+ if (error.code !== "ENOENT")
294
+ throw new McpOAuthError("The hosted MCP token cache could not be cleared. Check its file permissions.");
295
+ }
296
+ }
297
+ }
298
+ //# sourceMappingURL=mcp-oauth.js.map
@@ -0,0 +1,53 @@
1
+ /** Hosted hq-mcp forwarding used by the hqd mcp.call operation. */
2
+ export declare class McpOpError extends Error {
3
+ readonly code: string;
4
+ constructor(code: string, message: string);
5
+ }
6
+ export interface McpToolResult {
7
+ content: Array<{
8
+ type: "text";
9
+ text: string;
10
+ }>;
11
+ structuredContent?: Record<string, unknown>;
12
+ isError?: boolean;
13
+ }
14
+ export declare function jsonResult(value: unknown): McpToolResult;
15
+ export declare const HQ_MCP_HTTP_TIMEOUT_MS = 30000;
16
+ export declare function resolveHqMcpEndpoint(env?: NodeJS.ProcessEnv): URL;
17
+ export interface HqMcpBridgeOptions {
18
+ endpoint?: URL;
19
+ env?: NodeJS.ProcessEnv;
20
+ getToken?: () => Promise<string>;
21
+ fetchImpl?: typeof fetch;
22
+ timeoutMs?: number;
23
+ }
24
+ /** A short-lived Streamable HTTP MCP client; no local hq-mcp process is started. */
25
+ export declare class HqMcpBridge {
26
+ private readonly endpoint;
27
+ private readonly getToken;
28
+ private readonly fetchImpl;
29
+ private readonly timeoutMs;
30
+ private readonly activeClients;
31
+ constructor(options?: HqMcpBridgeOptions);
32
+ listTools(): Promise<Array<{
33
+ name: string;
34
+ description?: string;
35
+ inputSchema: Record<string, unknown>;
36
+ }>>;
37
+ callTool(name: string, args: Record<string, unknown>, timeoutMs?: number): Promise<unknown>;
38
+ close(): void;
39
+ private withClient;
40
+ private toSafeError;
41
+ }
42
+ export interface RunWorkerRequest {
43
+ worker: string;
44
+ skill?: string;
45
+ args?: string;
46
+ company?: string;
47
+ }
48
+ /**
49
+ * Dispatch `/run <worker> <skill> <args>` in a detached headless Claude Code
50
+ * session rooted at HQ_ROOT. HQ_RUN_WORKER_CMD overrides the binary (tests).
51
+ */
52
+ export declare function runWorker(req: RunWorkerRequest, hqRoot: string, env: NodeJS.ProcessEnv, now: Date): Promise<Record<string, unknown>>;
53
+ //# sourceMappingURL=mcp.d.ts.map
@@ -0,0 +1,217 @@
1
+ /** Hosted hq-mcp forwarding used by the hqd mcp.call operation. */
2
+ import { spawn } from "node:child_process";
3
+ import * as fs from "node:fs";
4
+ import * as path from "node:path";
5
+ import { McpEndpointError, resolveHqMcpEndpoint as resolveEndpoint } from "../mcp-endpoint.js";
6
+ import { ensurePrivateDir, openPrivateAppend } from "../paths.js";
7
+ export class McpOpError extends Error {
8
+ code;
9
+ constructor(code, message) {
10
+ super(message);
11
+ this.code = code;
12
+ this.name = "McpOpError";
13
+ }
14
+ }
15
+ export function jsonResult(value) {
16
+ const structured = value && typeof value === "object" && !Array.isArray(value) ? value : { value };
17
+ return { content: [{ type: "text", text: JSON.stringify(value, null, 2) }], structuredContent: structured };
18
+ }
19
+ export const HQ_MCP_HTTP_TIMEOUT_MS = 30_000;
20
+ export function resolveHqMcpEndpoint(env = process.env) {
21
+ try {
22
+ return resolveEndpoint(env);
23
+ }
24
+ catch (error) {
25
+ if (error instanceof McpEndpointError)
26
+ throw new McpOpError(error.code, error.message);
27
+ throw error;
28
+ }
29
+ }
30
+ async function defaultMcpToken() {
31
+ const { ensureCognitoToken, resolveCognitoTokenSource } = await import("../../../utils/cognito-session.js");
32
+ if (resolveCognitoTokenSource() !== "machine") {
33
+ const { getMcpAudienceToken, McpOAuthError } = await import("../mcp-oauth.js");
34
+ try {
35
+ return await getMcpAudienceToken();
36
+ }
37
+ catch (error) {
38
+ if (error instanceof McpOAuthError)
39
+ throw new McpOpError(error.code, error.message);
40
+ throw new McpOpError("hq_mcp_auth_required", "Hosted MCP sign-in is required. Run `hq mcp login` and retry.");
41
+ }
42
+ }
43
+ try {
44
+ // Send this box's existing machine token. The hosted hq-mcp audience check
45
+ // accepts it only when it was issued for hqMcpMachineClient; otherwise the
46
+ // server rejects it. This helper does not inspect the credential's client ID.
47
+ return await ensureCognitoToken({ tokenSource: "machine", interactive: false });
48
+ }
49
+ catch {
50
+ throw new McpOpError("hq_mcp_auth_required", "An hq-mcp machine token is unavailable. Configure this box with hq-pro's hqMcpMachineClient ID, then retry the daemon call.");
51
+ }
52
+ }
53
+ /** A short-lived Streamable HTTP MCP client; no local hq-mcp process is started. */
54
+ export class HqMcpBridge {
55
+ endpoint;
56
+ getToken;
57
+ fetchImpl;
58
+ timeoutMs;
59
+ activeClients = new Set();
60
+ constructor(options = {}) {
61
+ this.endpoint = options.endpoint ?? resolveHqMcpEndpoint(options.env ?? process.env);
62
+ this.getToken = options.getToken ?? defaultMcpToken;
63
+ this.fetchImpl = options.fetchImpl ?? fetch;
64
+ this.timeoutMs = options.timeoutMs ?? HQ_MCP_HTTP_TIMEOUT_MS;
65
+ }
66
+ async listTools() {
67
+ return this.withClient(async (client) => {
68
+ const result = await client.listTools();
69
+ return result.tools.map((tool) => ({
70
+ name: tool.name,
71
+ ...(tool.description ? { description: tool.description } : {}),
72
+ inputSchema: tool.inputSchema,
73
+ }));
74
+ });
75
+ }
76
+ async callTool(name, args, timeoutMs = this.timeoutMs) {
77
+ return this.withClient(async (client, token) => {
78
+ const { tools } = await client.listTools();
79
+ if (!tools.some((tool) => tool.name === name)) {
80
+ throw new McpOpError("hq_mcp_tool_unavailable", `Hosted hq-mcp does not expose ${name}.`);
81
+ }
82
+ return await client.callTool({ name, arguments: args });
83
+ }, timeoutMs);
84
+ }
85
+ close() {
86
+ for (const client of this.activeClients) {
87
+ void client.close().catch(() => {
88
+ // The in-flight call is already being cancelled; shutdown owns its result.
89
+ });
90
+ }
91
+ }
92
+ async withClient(work, timeoutMs = this.timeoutMs) {
93
+ let token = "";
94
+ let transport;
95
+ let client;
96
+ let timeout;
97
+ let timedOut = false;
98
+ const abortController = new AbortController();
99
+ let httpErrorClass;
100
+ try {
101
+ const [clientModule, httpModule] = await Promise.all([
102
+ import("@modelcontextprotocol/sdk/client/index.js"),
103
+ import("@modelcontextprotocol/sdk/client/streamableHttp.js"),
104
+ ]);
105
+ httpErrorClass = httpModule.StreamableHTTPError;
106
+ token = await this.getToken();
107
+ if (!token.trim())
108
+ throw new McpOpError("hq_mcp_auth_required", "An hq-mcp audience token is required.");
109
+ transport = new httpModule.StreamableHTTPClientTransport(this.endpoint, {
110
+ requestInit: {
111
+ headers: { Authorization: `Bearer ${token}` },
112
+ },
113
+ fetch: (input, init) => this.fetchImpl(input, {
114
+ ...init,
115
+ signal: init?.signal ? AbortSignal.any([abortController.signal, init.signal]) : abortController.signal,
116
+ }),
117
+ });
118
+ client = new clientModule.Client({ name: "hqd", version: "1" }, { capabilities: {} });
119
+ this.activeClients.add(client);
120
+ timeout = setTimeout(() => {
121
+ timedOut = true;
122
+ abortController.abort();
123
+ void client?.close().catch(() => {
124
+ // The deadline already ended the request.
125
+ });
126
+ }, timeoutMs);
127
+ await client.connect(transport);
128
+ return await work(client, token);
129
+ }
130
+ catch (error) {
131
+ if (timedOut) {
132
+ throw new McpOpError("hq_mcp_timeout", `Hosted hq-mcp request did not finish within ${timeoutMs}ms.`);
133
+ }
134
+ throw this.toSafeError(error, token, httpErrorClass);
135
+ }
136
+ finally {
137
+ if (timeout)
138
+ clearTimeout(timeout);
139
+ if (client) {
140
+ this.activeClients.delete(client);
141
+ try {
142
+ await client.close();
143
+ }
144
+ catch {
145
+ // Closing a finished HTTP session cannot change the request result.
146
+ }
147
+ }
148
+ else if (transport) {
149
+ try {
150
+ await transport.close();
151
+ }
152
+ catch {
153
+ // Same as above; the transport owns no child process or token cache.
154
+ }
155
+ }
156
+ }
157
+ }
158
+ toSafeError(error, token, StreamableHTTPErrorClass) {
159
+ if (error instanceof McpOpError)
160
+ return error;
161
+ if (error instanceof McpEndpointError)
162
+ return new McpOpError(error.code, error.message);
163
+ const status = StreamableHTTPErrorClass && error instanceof StreamableHTTPErrorClass ? error.code : undefined;
164
+ if (status === 401 || status === 403) {
165
+ return new McpOpError("hq_mcp_auth_required", "Hosted hq-mcp rejected this token. Use the DCR plus PKCE person flow or configure the hq-mcp machine client ID on this box.");
166
+ }
167
+ const message = error instanceof Error ? error.message : "request failed";
168
+ const safe = token ? message.split(token).join("[redacted]") : message;
169
+ return new McpOpError("hq_mcp_error", `Hosted hq-mcp request failed: ${safe}`);
170
+ }
171
+ }
172
+ const SLUG_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
173
+ /**
174
+ * Dispatch `/run <worker> <skill> <args>` in a detached headless Claude Code
175
+ * session rooted at HQ_ROOT. HQ_RUN_WORKER_CMD overrides the binary (tests).
176
+ */
177
+ export async function runWorker(req, hqRoot, env, now) {
178
+ if (!SLUG_RE.test(req.worker))
179
+ throw new McpOpError("bad_args", "worker must be a worker id (letters, digits, '.', '_', '-')");
180
+ if (req.skill !== undefined && !SLUG_RE.test(req.skill))
181
+ throw new McpOpError("bad_args", "skill must be a skill name");
182
+ const prompt = ["/run", req.worker, req.skill, req.args?.trim()].filter(Boolean).join(" ");
183
+ const logDir = path.join(hqRoot, "workspace", "orchestrator", "hqd-run-worker");
184
+ ensurePrivateDir(logDir);
185
+ const logPath = path.join(logDir, `${now.toISOString().replace(/[:.]/g, "-")}-${req.worker}.log`);
186
+ const fd = openPrivateAppend(logPath);
187
+ fs.chmodSync(logPath, 0o600);
188
+ const bin = env.HQ_RUN_WORKER_CMD?.trim() || "claude";
189
+ try {
190
+ const workerEnv = { ...env };
191
+ for (const key of Object.keys(workerEnv)) {
192
+ if (key.toUpperCase() === "HQ_SPAWN_COMPANY")
193
+ delete workerEnv[key];
194
+ }
195
+ if (req.company)
196
+ workerEnv.HQ_SPAWN_COMPANY = req.company;
197
+ const child = spawn(bin, ["-p", prompt], {
198
+ cwd: hqRoot,
199
+ env: workerEnv,
200
+ detached: true,
201
+ stdio: ["ignore", fd, fd],
202
+ });
203
+ await new Promise((resolve, reject) => {
204
+ child.once("spawn", resolve);
205
+ child.once("error", (err) => {
206
+ fs.appendFileSync(logPath, `\nhqd could not start ${bin}: ${err.message}\n`, { mode: 0o600 });
207
+ reject(new McpOpError("worker_spawn_failed", `could not start ${bin}: ${err.message}`));
208
+ });
209
+ });
210
+ child.unref();
211
+ return { dispatched: true, prompt, pid: child.pid ?? null, logPath, company: req.company ?? null };
212
+ }
213
+ finally {
214
+ fs.closeSync(fd);
215
+ }
216
+ }
217
+ //# sourceMappingURL=mcp.js.map
@@ -7,10 +7,11 @@
7
7
  * out of order, so clients match them by `id`.
8
8
  */
9
9
  export declare const HQD_SOCKET_NAME = "hqd.sock";
10
- export declare const HQD_OPS: readonly ["ping", "resolve", "session.open", "session.get", "session.close", "policy.check", "policy.index", "journal.append", "memory.get", "memory.put", "mesh.presence", "shutdown"];
10
+ export declare const HQD_OPS: readonly ["ping", "resolve", "session.open", "session.get", "session.close", "policy.check", "policy.index", "journal.append", "memory.get", "memory.put", "mesh.presence", "mcp.call", "shutdown"];
11
11
  export type HqdOp = (typeof HQD_OPS)[number];
12
12
  /** A request line larger than this is rejected and the connection closed. */
13
13
  export declare const HQD_MAX_LINE_BYTES: number;
14
+ export declare const HQD_DEFAULT_REQUEST_TIMEOUT_MS = 2000;
14
15
  export interface HqdRequest {
15
16
  id: string | number;
16
17
  op: string;