@indigoai-us/hq-cli 5.345.30 → 5.345.32

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 (34) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/command-catalog.generated.d.ts +8 -0
  3. package/dist/command-catalog.generated.js +10 -0
  4. package/dist/commands/anywhere-runtime-flag.js +1 -1
  5. package/dist/commands/daemon.d.ts +2 -0
  6. package/dist/commands/daemon.js +6 -3
  7. package/dist/commands/install-global.d.ts +3 -1
  8. package/dist/commands/install-global.js +17 -7
  9. package/dist/commands/integrations-core.d.ts +40 -0
  10. package/dist/commands/integrations-core.js +90 -0
  11. package/dist/commands/integrations.d.ts +2 -2
  12. package/dist/commands/integrations.js +27 -3
  13. package/dist/commands/link.d.ts +7 -3
  14. package/dist/commands/link.js +14 -9
  15. package/dist/commands/mcp-serve.d.ts +2 -0
  16. package/dist/commands/mcp-serve.js +5 -3
  17. package/dist/commands/packs.d.ts +26 -0
  18. package/dist/commands/packs.js +12 -1
  19. package/dist/commands/pkg-install.js +10 -9
  20. package/dist/commands/update.d.ts +1 -0
  21. package/dist/commands/update.js +2 -2
  22. package/dist/lib/anywhere-person-setting.d.ts +17 -0
  23. package/dist/lib/anywhere-person-setting.js +41 -0
  24. package/dist/lib/daemon/mcp-oauth.d.ts +1 -1
  25. package/dist/lib/daemon/mcp-oauth.js +3 -3
  26. package/dist/lib/daemon/run.js +4 -1
  27. package/dist/lib/daemon/server.d.ts +2 -0
  28. package/dist/lib/daemon/server.js +11 -3
  29. package/dist/lib/doctor/__testing__/run-doctor.js +10 -1
  30. package/dist/lib/doctor/checks/anywhere.d.ts +2 -0
  31. package/dist/lib/doctor/checks/anywhere.js +12 -3
  32. package/dist/utils/resident-package-preload.js +7 -1
  33. package/docs/anywhere-runtime.md +13 -7
  34. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.345.32] — 2026-10-06
6
+ - `hq integrations list --usable` lists only the apps you can use right now: connected, shared with you under the same access rule the HQ gateway applies, and backed by a provider the gateway has registered. Each row shows the app name, its website domain, and the flag that selects exactly that connection in `hq integrations tools` and `call` (`--provider <slug>`, or `--connection <id>` when two connections share a provider). `--json` returns `{companyUid, viewerAccessKnown, apps}`. `--no-login` makes `list` fail instead of opening a browser sign-in, for hooks and scripts.
7
+
8
+ ## [5.345.31] — 2026-10-06
9
+ - HQ Anywhere commands now require both the operator runtime switch and the signed-in person's `hq-anywhere` setting. `hq doctor` reports the setting state and access stays off when it cannot be read.
10
+
5
11
  ## [5.345.30] — 2026-10-06
6
12
  - Sanitize malformed journal scope identifiers before composing per-scope check IDs.
7
13
 
@@ -4054,6 +4054,14 @@ export declare const COMMAND_CATALOG: readonly [{
4054
4054
  readonly flags: "--json";
4055
4055
  readonly description: "Machine-readable output";
4056
4056
  readonly hidden: false;
4057
+ }, {
4058
+ readonly flags: "--usable";
4059
+ readonly description: "Only apps you can use right now (connected and shared with you), with the --provider value to call them";
4060
+ readonly hidden: false;
4061
+ }, {
4062
+ readonly flags: "--no-login";
4063
+ readonly description: "Fail instead of opening a browser sign-in when there is no HQ session";
4064
+ readonly hidden: false;
4057
4065
  }];
4058
4066
  readonly subcommands: readonly [];
4059
4067
  }, {
@@ -4897,6 +4897,16 @@ export const COMMAND_CATALOG = [
4897
4897
  "flags": "--json",
4898
4898
  "description": "Machine-readable output",
4899
4899
  "hidden": false
4900
+ },
4901
+ {
4902
+ "flags": "--usable",
4903
+ "description": "Only apps you can use right now (connected and shared with you), with the --provider value to call them",
4904
+ "hidden": false
4905
+ },
4906
+ {
4907
+ "flags": "--no-login",
4908
+ "description": "Fail instead of opening a browser sign-in when there is no HQ session",
4909
+ "hidden": false
4900
4910
  }
4901
4911
  ],
4902
4912
  "subcommands": []
@@ -4,7 +4,7 @@ export { HQ_ANYWHERE_RUNTIME_FLAG };
4
4
  export class AnywhereRuntimeDisabledError extends Error {
5
5
  expected = true;
6
6
  constructor() {
7
- super("HQ Anywhere is not enabled. Enable the hq-anywhere-runtime flag to use this command.");
7
+ super("HQ Anywhere is not enabled. The hq-anywhere-runtime flag and your HQ Anywhere person setting must both be on.");
8
8
  this.name = "AnywhereRuntimeDisabledError";
9
9
  }
10
10
  }
@@ -43,6 +43,8 @@ export interface DaemonCommandDeps {
43
43
  userHome: (user: string) => string;
44
44
  /** The pid is a live `hq daemon run`. */
45
45
  daemonAlive: (pid: number) => boolean;
46
+ /** Test seam for the combined hq-flags and person-setting gate. */
47
+ runtimeAllowed?: () => boolean | Promise<boolean>;
46
48
  /** Test seam for the company sync-mode commands. */
47
49
  syncModeClient?: () => Promise<SyncModeVaultClient>;
48
50
  /** Reads stdin for the one-shot daemon machine login; never reads env or argv credentials. */
@@ -33,7 +33,7 @@ import { runDaemonCall } from "../lib/daemon/command-call.js";
33
33
  import { isSyncRunnerCommandLine, readWatchRunnerOwnerStatus, syncRunnerPathFromCommandLine, watchRunnerOwnerStatusPath } from "../lib/daemon/sync-runner.js";
34
34
  import { stagedSyncRunnerVersion, syncRunnerBundleStageRoot } from "../lib/daemon/sync-runner-bundle.js";
35
35
  import { getSyncModeForCompany, setSyncMode } from "./sync-mode.js";
36
- import { requireAnywhereRuntime } from "./anywhere-runtime-flag.js";
36
+ import { anywhereRuntimeAllowed } from "../lib/anywhere-person-setting.js";
37
37
  import { hqdRequest, hqdSocketPath, HqdClientError } from "../lib/daemon/protocol.js";
38
38
  import { parseStdinMachineCredentials } from "../lib/daemon/machine-login.js";
39
39
  import { resolveDefaultHqRoot } from "../utils/cognito-session.js";
@@ -130,6 +130,7 @@ async function loadDefaults(over) {
130
130
  windowsHeadlessLauncher: over.windowsHeadlessLauncher ?? (platform === "win32" ? mesh.resolveWindowsHeadlessLauncher(env) : undefined),
131
131
  systemUnitDir: over.systemUnitDir ?? SYSTEM_UNIT_DIR,
132
132
  syncModeClient: over.syncModeClient,
133
+ runtimeAllowed: over.runtimeAllowed,
133
134
  systemRecordPath: over.systemRecordPath ?? path.join(SYSTEM_ADOPTED_ROOT, "adopted.json"),
134
135
  };
135
136
  }
@@ -892,7 +893,8 @@ export function registerDaemonCommand(program, over = {}) {
892
893
  .option("--status", "Show whether hqd has an in-memory machine sign-in")
893
894
  .allowUnknownOption(true)
894
895
  .action(wrap(async (d, extra, opts) => {
895
- requireAnywhereRuntime();
896
+ if (!(await (d.runtimeAllowed ?? (() => anywhereRuntimeAllowed(d.env)))()))
897
+ throw new DaemonCommandError("HQ Anywhere is not enabled for this person or the hq-anywhere-runtime switch is off.");
896
898
  if (extra?.length)
897
899
  throw new DaemonCommandError("Machine credentials are accepted from stdin only; do not pass credentials as arguments.");
898
900
  const socketPath = hqdSocketPath(d.home, d.env, d.platform);
@@ -930,7 +932,8 @@ export function registerDaemonCommand(program, over = {}) {
930
932
  .option("--json <args>", "Request args as a JSON object", "{}")
931
933
  .option("--timeout <ms>", "Give up after this many milliseconds")
932
934
  .action(wrap(async (d, op, opts) => {
933
- requireAnywhereRuntime();
935
+ if (!(await (d.runtimeAllowed ?? (() => anywhereRuntimeAllowed(d.env)))()))
936
+ throw new DaemonCommandError("HQ Anywhere is not enabled for this person or the hq-anywhere-runtime switch is off.");
934
937
  if (op === "auth.login") {
935
938
  throw new Error("Use `hq daemon login --stdin`; credentials are accepted from stdin only.");
936
939
  }
@@ -73,9 +73,11 @@ export interface GlobalCommandOpts {
73
73
  /** Dependency injection for tests. */
74
74
  env?: NodeJS.ProcessEnv;
75
75
  log?: (line: string) => void;
76
+ /** Test seam for the hq-pro person preference read. */
77
+ personSettingReader?: () => Promise<boolean | null>;
76
78
  }
77
79
  export declare function resolveInstallRoot(explicit: string | undefined): string;
78
80
  /** Entry used by `hq install --global` (wired from pkg-install.ts). */
79
- export declare function runInstallGlobal(opts: GlobalCommandOpts): void;
81
+ export declare function runInstallGlobal(opts: GlobalCommandOpts): Promise<void>;
80
82
  export declare function registerUninstallCommand(program: Command): void;
81
83
  //# sourceMappingURL=install-global.d.ts.map
@@ -17,6 +17,7 @@ import { resolveHqRoot } from './doctor.js';
17
17
  import { anywhereRuntimeEnabled, AnywhereRuntimeDisabledError } from './anywhere-runtime-flag.js';
18
18
  import { resolveHqMcpEndpoint } from '../lib/daemon/mcp-endpoint.js';
19
19
  import { chatgptConnectorInstructions } from '../lib/install/chatgpt-targets.js';
20
+ import { anywhereRuntimeAllowed } from '../lib/anywhere-person-setting.js';
20
21
  import { grokConnectorInstructions } from '../lib/install/grok-targets.js';
21
22
  import { readRegistry, registryDir, registryFile, writeRegistry } from '../lib/registry/registry.js';
22
23
  import { DAEMON_LAUNCHD_LABEL, DAEMON_SYSTEMD_UNIT, DAEMON_WINDOWS_TASK, SYSTEM_UNIT_DIR } from '../lib/daemon/install.js';
@@ -294,8 +295,6 @@ export function installGlobalClaude(opts) {
294
295
  const log = opts.log ?? ((l) => console.log(l));
295
296
  const hqRoot = opts.hqRoot;
296
297
  assertHqRoot(hqRoot);
297
- if (!isAnywhereEnabled(home, env))
298
- throw new AnywhereRuntimeDisabledError();
299
298
  const t = claudeTargets(home);
300
299
  const prior = readState(t);
301
300
  const pluginSource = opts.via === 'plugin' ? resolveClaudePluginSource(opts.marketplaceDir, env, opts.marketplaceRepository) : undefined;
@@ -680,8 +679,6 @@ export function installGlobalCodex(opts) {
680
679
  expected: true,
681
680
  });
682
681
  }
683
- if (!isAnywhereEnabled(home, env))
684
- throw new AnywhereRuntimeDisabledError();
685
682
  const t = codexTargets(home);
686
683
  const hqPointer = claudeTargets(home).hqPointer;
687
684
  const prior = readCodexState(t);
@@ -957,10 +954,15 @@ export function resolveInstallRoot(explicit) {
957
954
  return root;
958
955
  }
959
956
  /** Entry used by `hq install --global` (wired from pkg-install.ts). */
960
- export function runInstallGlobal(opts) {
957
+ export async function runInstallGlobal(opts) {
961
958
  const runtime = assertRuntime(opts.runtime);
959
+ const env = opts.env ?? process.env;
962
960
  if (runtime === 'chatgpt' || runtime === 'grok') {
963
- if (!anywhereRuntimeEnabled(opts.env ?? process.env))
961
+ const enabled = await anywhereRuntimeAllowed(env, {
962
+ ...(opts.personSettingReader ? { readSetting: opts.personSettingReader } : {}),
963
+ onError: (name) => (opts.log ?? console.log)(`HQ Anywhere person setting unavailable (${name}); access remains off.`),
964
+ });
965
+ if (!enabled)
964
966
  throw new AnywhereRuntimeDisabledError();
965
967
  const endpoint = resolveHqMcpEndpoint(opts.env ?? process.env).toString();
966
968
  const lines = runtime === 'chatgpt' ? chatgptConnectorInstructions(endpoint) : grokConnectorInstructions(endpoint);
@@ -969,7 +971,15 @@ export function runInstallGlobal(opts) {
969
971
  return;
970
972
  }
971
973
  const install = runtime === 'codex' ? installGlobalCodex : installGlobalClaude;
972
- install({ hqRoot: resolveInstallRoot(opts.hqRoot), dryRun: opts.dryRun, via: opts.via, marketplaceDir: opts.marketplaceDir, marketplaceRepository: opts.marketplaceRepository });
974
+ install({
975
+ hqRoot: resolveInstallRoot(opts.hqRoot),
976
+ dryRun: opts.dryRun,
977
+ via: opts.via,
978
+ marketplaceDir: opts.marketplaceDir,
979
+ marketplaceRepository: opts.marketplaceRepository,
980
+ env,
981
+ log: opts.log,
982
+ });
973
983
  }
974
984
  export function registerUninstallCommand(program) {
975
985
  program
@@ -225,6 +225,46 @@ export declare function toolPrefixForProvider(provider: string): string;
225
225
  */
226
226
  export declare function fetchAdminSurface(token: string, companyUid: string): Promise<AdminSurface>;
227
227
  export declare function fetchConnections(token: string, companyUid: string): Promise<AdminConnection[]>;
228
+ /**
229
+ * One app the caller can use right now through the HQ gateway. Field names
230
+ * follow the bot `company_apps` projection (name, provider, domain, status),
231
+ * so session context and bot context describe an app the same way. No
232
+ * endpoint URLs, credentials, scopes, or creator identities.
233
+ */
234
+ export interface UsableApp {
235
+ connectionId: string;
236
+ /** Display name, stripped of control characters and capped in length. */
237
+ name: string;
238
+ provider: string;
239
+ domain: string | null;
240
+ status: string;
241
+ /** Bare provider slug, e.g. `notion`. */
242
+ providerArg: string;
243
+ /**
244
+ * The flag that resolves to exactly this connection in `hq integrations
245
+ * tools|call`: `--provider <slug>` when no other active row shares the
246
+ * provider, else `--connection <id>`.
247
+ */
248
+ selector: string;
249
+ }
250
+ export interface UsableAppsResult {
251
+ companyUid: string;
252
+ /**
253
+ * False when the server ignored `view=summary` (an older deploy). Its rows
254
+ * carry no `viewerAccess`, so nothing can be confirmed usable and `apps` is
255
+ * empty rather than guessed.
256
+ */
257
+ viewerAccessKnown: boolean;
258
+ apps: UsableApp[];
259
+ }
260
+ /**
261
+ * The apps the caller may use, from `GET /v1/integrations/admin?view=summary`.
262
+ * A row counts only when all of these hold, mirroring the gateway's checks:
263
+ * the server's per-caller ACL decision (`viewerAccess.usable`) is true, the
264
+ * status is one the gateway serves, and the gateway has a provider registered
265
+ * for it. Fails closed: a row without `viewerAccess` is not listed.
266
+ */
267
+ export declare function fetchUsableApps(token: string, companyUid: string): Promise<UsableAppsResult>;
228
268
  /**
229
269
  * Resolve one connection by `--connection acct_…` or `--provider linear`
230
270
  * (matches `factory:<slug>`, bare provider ids, and an installation's human
@@ -355,6 +355,96 @@ export async function fetchAdminSurface(token, companyUid) {
355
355
  export async function fetchConnections(token, companyUid) {
356
356
  return (await fetchAdminSurface(token, companyUid)).connections;
357
357
  }
358
+ /**
359
+ * Statuses the gateway serves. `needs-attention` is derived by the summary
360
+ * view from a stored `connected` row whose token renewal is failing, so the
361
+ * gateway still dispatches to it.
362
+ */
363
+ const GATEWAY_SERVED_STATUSES = new Set(["connected", "needs-attention"]);
364
+ /**
365
+ * Providers hq-pro registers statically (`createIntegrationProviderRegistry`).
366
+ * Every other provider is registered per company from a factory installation
367
+ * keyed by its `runtimeProviderId`; a connection with neither is refused by the
368
+ * gateway as `ProviderUnsupported`.
369
+ */
370
+ const STATIC_GATEWAY_PROVIDERS = new Set(["google-workspace", "linear", "slack"]);
371
+ const MAX_APP_NAME_LENGTH = 60;
372
+ /**
373
+ * Display names are set by whoever installed the app and end up in agent
374
+ * context, so keep them to one short line of printable text.
375
+ */
376
+ function safeAppName(raw, fallback) {
377
+ const cleaned = (raw ?? "")
378
+ .replace(/[\p{Cc}\p{Cf}\u2028\u2029]/gu, " ")
379
+ .replace(/\s+/g, " ")
380
+ .trim();
381
+ if (!cleaned)
382
+ return fallback;
383
+ const chars = Array.from(cleaned);
384
+ return chars.length > MAX_APP_NAME_LENGTH
385
+ ? `${chars.slice(0, MAX_APP_NAME_LENGTH - 1).join("").trimEnd()}…`
386
+ : cleaned;
387
+ }
388
+ /** Case-insensitive code-point order, so the list reads the same in every locale. */
389
+ function compareNames(a, b) {
390
+ const x = a.toLowerCase();
391
+ const y = b.toLowerCase();
392
+ return x < y ? -1 : x > y ? 1 : 0;
393
+ }
394
+ function gatewayHasProvider(row) {
395
+ if (STATIC_GATEWAY_PROVIDERS.has(row.provider))
396
+ return true;
397
+ return row.installation?.runtimeProviderId === row.provider;
398
+ }
399
+ /**
400
+ * The apps the caller may use, from `GET /v1/integrations/admin?view=summary`.
401
+ * A row counts only when all of these hold, mirroring the gateway's checks:
402
+ * the server's per-caller ACL decision (`viewerAccess.usable`) is true, the
403
+ * status is one the gateway serves, and the gateway has a provider registered
404
+ * for it. Fails closed: a row without `viewerAccess` is not listed.
405
+ */
406
+ export async function fetchUsableApps(token, companyUid) {
407
+ const res = await vaultApiFetch({
408
+ token,
409
+ path: "/v1/integrations/admin",
410
+ query: { companyUid, view: "summary" },
411
+ });
412
+ if (!res.ok)
413
+ await raiseForResponse(res, "Failed to list integrations");
414
+ const data = (await res.json());
415
+ const viewerAccessKnown = data.view === "summary";
416
+ const rows = Array.isArray(data.connections) ? data.connections : [];
417
+ // `--provider` picks the first non-revoked row with that provider, whoever
418
+ // may use it, so a shared provider needs `--connection` to be exact.
419
+ const activeByProvider = new Map();
420
+ for (const row of rows) {
421
+ if (row.status === "revoked")
422
+ continue;
423
+ const key = bareProvider(row.provider).toLowerCase();
424
+ activeByProvider.set(key, (activeByProvider.get(key) ?? 0) + 1);
425
+ }
426
+ const apps = viewerAccessKnown
427
+ ? rows
428
+ .filter((row) => row.viewerAccess?.usable === true &&
429
+ GATEWAY_SERVED_STATUSES.has(row.status) &&
430
+ gatewayHasProvider(row))
431
+ .map((row) => {
432
+ const providerArg = bareProvider(row.provider);
433
+ const shared = (activeByProvider.get(providerArg.toLowerCase()) ?? 0) > 1;
434
+ return {
435
+ connectionId: row.id,
436
+ name: safeAppName(row.installation?.displayName, providerArg),
437
+ provider: row.provider,
438
+ domain: typeof row.domain === "string" && row.domain ? row.domain : null,
439
+ status: row.status,
440
+ providerArg,
441
+ selector: shared ? `--connection ${row.id}` : `--provider ${providerArg}`,
442
+ };
443
+ })
444
+ .sort((a, b) => compareNames(a.name, b.name))
445
+ : [];
446
+ return { companyUid: data.companyUid ?? companyUid, viewerAccessKnown, apps };
447
+ }
358
448
  /**
359
449
  * Resolve one connection by `--connection acct_…` or `--provider linear`
360
450
  * (matches `factory:<slug>`, bare provider ids, and an installation's human
@@ -51,8 +51,8 @@
51
51
  */
52
52
  import { Command } from "commander";
53
53
  import { type PendingApproval } from "./integrations-api.js";
54
- export { IntegrationsCliError, callGateway, fetchConnections, queuedOutcome, selectConnection, toolPrefixForProvider, unwrapGatewayResult, } from "./integrations-core.js";
55
- export type { AdminConnection } from "./integrations-core.js";
54
+ export { IntegrationsCliError, callGateway, fetchConnections, fetchUsableApps, queuedOutcome, selectConnection, toolPrefixForProvider, unwrapGatewayResult, } from "./integrations-core.js";
55
+ export type { AdminConnection, UsableApp, UsableAppsResult } from "./integrations-core.js";
56
56
  /** Wait for a decided queue item without resending the approve request. */
57
57
  export declare function waitForApprovalTerminal(token: string, companyUid: string, queueId: string, options?: {
58
58
  timeoutMs?: number;
@@ -54,12 +54,12 @@ import chalk from "chalk";
54
54
  import { ensureCognitoIdToken } from "../utils/cognito-session.js";
55
55
  import { getCompanyUid, vaultApiFetch } from "../utils/vault-api.js";
56
56
  import { redactSensitiveUrl, redactSensitiveUrls, redactUrlsInText } from "../utils/redact-url.js";
57
- import { IntegrationsCliError, bareProvider, callGateway, fetchConnections, isClientError, printHealthNotice, printJson, printIntegrationCallResultJson, raiseIfUnauthorized, raiseIfUpstreamUnavailable, gatewayResultIsError, selectConnection, revokedConnectionDetails, toolPrefixForProvider, unwrapGatewayResult, queuedOutcome, } from "./integrations-core.js";
57
+ import { IntegrationsCliError, bareProvider, callGateway, fetchConnections, fetchUsableApps, isClientError, printHealthNotice, printJson, printIntegrationCallResultJson, raiseIfUnauthorized, raiseIfUpstreamUnavailable, gatewayResultIsError, selectConnection, revokedConnectionDetails, toolPrefixForProvider, unwrapGatewayResult, queuedOutcome, } from "./integrations-core.js";
58
58
  import { registerConnectCommands } from "./integrations-connect.js";
59
59
  import { registerImportCommands } from "./integrations-import.js";
60
60
  import { registerManageCommands } from "./integrations-manage.js";
61
61
  import { fetchPendingApprovals } from "./integrations-api.js";
62
- export { IntegrationsCliError, callGateway, fetchConnections, queuedOutcome, selectConnection, toolPrefixForProvider, unwrapGatewayResult, } from "./integrations-core.js";
62
+ export { IntegrationsCliError, callGateway, fetchConnections, fetchUsableApps, queuedOutcome, selectConnection, toolPrefixForProvider, unwrapGatewayResult, } from "./integrations-core.js";
63
63
  /**
64
64
  * Gateway metadata is additive and may be absent when talking to an older
65
65
  * gateway. Parse it defensively so an unknown or malformed value never breaks
@@ -220,9 +220,33 @@ export function registerIntegrationsCommand(program) {
220
220
  .description("List the company's connected apps")
221
221
  .option("--company <slug>", "Company slug, e.g. indigo (defaults to your single active company)")
222
222
  .option("--json", "Machine-readable output")
223
+ .option("--usable", "Only apps you can use right now (connected and shared with you), with the --provider value to call them")
224
+ .option("--no-login", "Fail instead of opening a browser sign-in when there is no HQ session")
223
225
  .action(async (opts) => {
224
- const token = await ensureCognitoIdToken();
226
+ const token = await ensureCognitoIdToken(opts.login === false ? { interactive: false } : {});
225
227
  const companyUid = await getCompanyUid(token, opts.company);
228
+ if (opts.usable) {
229
+ const result = await fetchUsableApps(token, companyUid);
230
+ if (opts.json) {
231
+ printJson(result);
232
+ return;
233
+ }
234
+ if (!result.viewerAccessKnown) {
235
+ console.log("This HQ server cannot report which apps are shared with you yet. Run `hq integrations list` to see every connected app.");
236
+ return;
237
+ }
238
+ if (result.apps.length === 0) {
239
+ console.log("No connected apps are available to you in this company. Run `hq integrations list` to see what is connected, or ask an admin to share one.");
240
+ return;
241
+ }
242
+ for (const app of result.apps) {
243
+ const where = app.domain ? ` (${app.domain})` : "";
244
+ const flag = app.status === "needs-attention" ? chalk.yellow(" needs attention") : "";
245
+ console.log(`${chalk.bold(app.name)}${chalk.dim(where)}${flag}`);
246
+ console.log(chalk.dim(` hq integrations tools ${app.selector}`));
247
+ }
248
+ return;
249
+ }
226
250
  const connections = await fetchConnections(token, companyUid);
227
251
  if (opts.json) {
228
252
  printJson(connections);
@@ -15,6 +15,10 @@ export interface LinkDeps {
15
15
  listMemberships?: () => Promise<MembershipCompany[]>;
16
16
  now?: () => Date;
17
17
  }
18
+ export interface LinkCommandDeps {
19
+ /** Test seam for the combined hq-flags and person-setting gate. */
20
+ runtimeAllowed?: () => boolean | Promise<boolean>;
21
+ }
18
22
  /** Link `dir` to `company` after confirming the caller is an active member. */
19
23
  export declare function linkFolder(company: string, dir: string | undefined, deps?: LinkDeps): Promise<{
20
24
  key: string;
@@ -24,7 +28,7 @@ export declare function unlinkFolder(dir: string | undefined, deps?: LinkDeps):
24
28
  export declare function resolveCompany(dir: string | undefined, deps?: LinkDeps): import("../lib/registry/registry.js").ResolveResult;
25
29
  /** Seed manifest-sourced entries; called by `hq install --global` (US-005). */
26
30
  export declare function seedRegistryFromHqRoot(hqRoot: string, deps?: LinkDeps): import("../lib/registry/registry.js").SeedResult;
27
- export declare function registerLinkCommand(program: Command): void;
28
- export declare function registerUnlinkCommand(program: Command): void;
29
- export declare function registerResolveCompanyCommand(program: Command): void;
31
+ export declare function registerLinkCommand(program: Command, deps?: LinkCommandDeps): void;
32
+ export declare function registerUnlinkCommand(program: Command, deps?: LinkCommandDeps): void;
33
+ export declare function registerResolveCompanyCommand(program: Command, deps?: LinkCommandDeps): void;
30
34
  //# sourceMappingURL=link.d.ts.map
@@ -9,9 +9,14 @@ import { Option } from "commander";
9
9
  import chalk from "chalk";
10
10
  import { ensureCognitoToken } from "../utils/cognito-session.js";
11
11
  import { listActiveMembershipCompanies } from "../lib/mesh/api.js";
12
- import { requireAnywhereRuntime } from "./anywhere-runtime-flag.js";
12
+ import { anywhereRuntimeAllowed } from "../lib/anywhere-person-setting.js";
13
13
  import { readCompaniesManifestMap } from "../lib/work-context/repo-remote.js";
14
14
  import { canonicalDir, keyForDir, readRegistry, registryDir, registryFile, removeEntriesForDir, resolveCompanyForDir, seedFromManifest, setEntry, writeRegistry, } from "../lib/registry/registry.js";
15
+ async function requireAnywhereRuntime(deps) {
16
+ if (!(await (deps.runtimeAllowed ?? (() => anywhereRuntimeAllowed()))())) {
17
+ throw new Error("HQ Anywhere is disabled until both the runtime flag and your person setting are on.");
18
+ }
19
+ }
15
20
  async function defaultListMemberships() {
16
21
  const token = await ensureCognitoToken({ interactive: false });
17
22
  return listActiveMembershipCompanies(token);
@@ -90,7 +95,7 @@ function fail(err) {
90
95
  console.error(chalk.red(err instanceof Error ? err.message : String(err)));
91
96
  process.exitCode = 1;
92
97
  }
93
- export function registerLinkCommand(program) {
98
+ export function registerLinkCommand(program, deps = {}) {
94
99
  program
95
100
  .command("link [company]")
96
101
  .description("Link this folder (or its git remote) to an HQ company")
@@ -100,7 +105,7 @@ export function registerLinkCommand(program) {
100
105
  .addOption(new Option("--seed-manifest <hqRoot>", "Seed entries from an HQ companies/manifest.yaml").hideHelp())
101
106
  .action(async (company, opts) => {
102
107
  try {
103
- requireAnywhereRuntime();
108
+ await requireAnywhereRuntime(deps);
104
109
  if (opts.list) {
105
110
  const registry = readRegistry();
106
111
  if (opts.json)
@@ -132,14 +137,14 @@ export function registerLinkCommand(program) {
132
137
  }
133
138
  });
134
139
  }
135
- export function registerUnlinkCommand(program) {
140
+ export function registerUnlinkCommand(program, deps = {}) {
136
141
  program
137
142
  .command("unlink")
138
143
  .description("Remove the HQ company link for this folder")
139
144
  .option("--path <dir>", "Folder to unlink (default: current directory)")
140
- .action((opts) => {
145
+ .action(async (opts) => {
141
146
  try {
142
- requireAnywhereRuntime();
147
+ await requireAnywhereRuntime(deps);
143
148
  const removed = unlinkFolder(opts.path);
144
149
  if (removed.length === 0)
145
150
  console.log(chalk.dim("Nothing linked here."));
@@ -152,15 +157,15 @@ export function registerUnlinkCommand(program) {
152
157
  }
153
158
  });
154
159
  }
155
- export function registerResolveCompanyCommand(program) {
160
+ export function registerResolveCompanyCommand(program, deps = {}) {
156
161
  program
157
162
  .command("resolve-company")
158
163
  .description("Print the HQ company linked to this folder (never guesses)")
159
164
  .option("--path <dir>", "Folder to resolve (default: current directory)")
160
165
  .option("--json", "Print JSON output")
161
- .action((opts) => {
166
+ .action(async (opts) => {
162
167
  try {
163
- requireAnywhereRuntime();
168
+ await requireAnywhereRuntime(deps);
164
169
  }
165
170
  catch (err) {
166
171
  fail(err);
@@ -24,6 +24,8 @@ export interface McpServeOptions {
24
24
  preloadResidentModules?: (residentPath: "mcp", modules: readonly string[]) => Promise<void>;
25
25
  /** Test seam for releasing the process package lease after resident preload. */
26
26
  releasePackageUseLease?: () => void;
27
+ /** Test seam for combined hq-flags and person-setting authorization. */
28
+ runtimeAllowed?: () => boolean | Promise<boolean>;
27
29
  }
28
30
  /** HQ root for prompts: HQ_ROOT, else the ~/.hq/root pointer written by `hq install --global`. */
29
31
  export declare function readHqRoot(env: NodeJS.ProcessEnv, home: string): string | null;
@@ -16,7 +16,7 @@ import * as os from "node:os";
16
16
  import * as path from "node:path";
17
17
  import { CLI_VERSION } from "../cli-version.js";
18
18
  import { HQ_ANYWHERE_RUNTIME_FLAG } from "../lib/anywhere-runtime-flag-key.js";
19
- import { anywhereRuntimeEnabledFromSnapshot } from "../lib/anywhere-runtime-fast-flag.js";
19
+ import { anywhereRuntimeAllowed } from "../lib/anywhere-person-setting.js";
20
20
  import { createLineSplitter, HqdClientError, hqdRequest, hqdSocketPath } from "../lib/daemon/protocol.js";
21
21
  import { releaseCurrentPackageUseLease, releasePackageUseLeaseAfterResidentPreload } from "../utils/package-use-lease.js";
22
22
  import { preloadResidentPackageModules, reportResidentPackagePreloadFailure, RESIDENT_PACKAGE_MODULES } from "../utils/resident-package-preload.js";
@@ -54,8 +54,10 @@ export async function runMcpServe(opts = {}) {
54
54
  const cwd = opts.cwd ?? process.cwd();
55
55
  const socketPath = hqdSocketPath(home, env);
56
56
  const timeoutMs = opts.timeoutMs ?? 120_000;
57
- if (!anywhereRuntimeEnabledFromSnapshot(env)) {
58
- process.stderr.write(`HQ Anywhere is not enabled. Enable the ${HQ_ANYWHERE_RUNTIME_FLAG} flag to use hq mcp serve.\n`);
57
+ if (!(await (opts.runtimeAllowed ?? (() => anywhereRuntimeAllowed(env, {
58
+ onError: (name) => process.stderr.write(`HQ Anywhere person setting unavailable (${name}); access remains off.\n`),
59
+ })))())) {
60
+ process.stderr.write(`HQ Anywhere is not enabled. The ${HQ_ANYWHERE_RUNTIME_FLAG} flag and your HQ Anywhere person setting must both be on.\n`);
59
61
  process.exitCode = 1;
60
62
  return;
61
63
  }
@@ -20,6 +20,10 @@ import { Command } from 'commander';
20
20
  import { type LatestResult } from './pack-install.js';
21
21
  import { type InstalledPack, type LinkStatus } from '../utils/pack-contributions.js';
22
22
  import type { PackContributeKey } from '../types.js';
23
+ interface CommonOpts {
24
+ json?: boolean;
25
+ hqRoot?: string;
26
+ }
23
27
  interface InstalledPackView {
24
28
  name: string;
25
29
  version?: string;
@@ -49,6 +53,28 @@ interface InstalledPackView {
49
53
  error?: string;
50
54
  }
51
55
  export declare function buildInstalledView(hqRoot: string, hqVersion: string | null, pack: InstalledPack, installedSources: Set<string>, checkUpdates: boolean, latestProbe?: LatestResult): InstalledPackView;
56
+ interface UpdateCheck {
57
+ name: string;
58
+ transport: string;
59
+ current?: string;
60
+ latest?: string;
61
+ updateAvailable: boolean | null;
62
+ applied: boolean;
63
+ reason?: string;
64
+ error?: string;
65
+ }
66
+ interface UpdateOpts extends CommonOpts {
67
+ checkOnly?: boolean;
68
+ refresh?: boolean;
69
+ yes?: boolean;
70
+ allowHooks?: boolean;
71
+ allowMcp?: boolean;
72
+ branch?: boolean;
73
+ }
74
+ export interface UpdateDependencies {
75
+ runtimeAllowed?: () => Promise<boolean>;
76
+ }
77
+ export declare function runUpdate(name: string | undefined, opts: UpdateOpts, deps?: UpdateDependencies): Promise<UpdateCheck[]>;
52
78
  export declare function registerPacksListCommand(parent: Command): void;
53
79
  export declare function registerPacksCommand(parent: Command): void;
54
80
  export {};
@@ -26,6 +26,7 @@ import chalk from 'chalk';
26
26
  import semverSatisfies from 'semver/functions/satisfies.js';
27
27
  import { findHqRoot } from '../utils/manifest.js';
28
28
  import { resolveHqRoot as resolveDetectedHqRoot } from './doctor.js';
29
+ import { anywhereRuntimeAllowed } from '../lib/anywhere-person-setting.js';
29
30
  import { classify, PINNED_HQ_ANYWHERE_SOURCE, resolveLatest, resolveLatestMarketplace, runScanPackages, installPack, } from './pack-install.js';
30
31
  import { contributionLinks, linkStatus, listInstalledPacks, findDependentPacks, readPackManifest, unwirePack, unwirePackMcp, readHqVersion, readRecommendedPackages, packagesDir, } from '../utils/pack-contributions.js';
31
32
  import { syncCodexPackHooks, syncCodexPackSkills } from '../lib/install/codex-pack-contributions.js';
@@ -207,7 +208,7 @@ function printListHuman(view) {
207
208
  console.log(chalk.dim('\n Install with: hq install <source>\n'));
208
209
  }
209
210
  }
210
- async function runUpdate(name, opts) {
211
+ export async function runUpdate(name, opts, deps = {}) {
211
212
  const hqRoot = resolveRoot(opts);
212
213
  let packs = listInstalledPacks(hqRoot).filter((p) => p.manifest);
213
214
  if (name) {
@@ -225,6 +226,16 @@ async function runUpdate(name, opts) {
225
226
  results.push({ name: pname, transport: 'unknown', updateAvailable: null, applied: false, error: 'no stamped source -- cannot update' });
226
227
  continue;
227
228
  }
229
+ if (pname === 'hq-anywhere' && !(await (deps.runtimeAllowed ?? (() => anywhereRuntimeAllowed(process.env)))())) {
230
+ results.push({
231
+ name: pname,
232
+ transport: safeClassify(source) ?? 'unknown',
233
+ updateAvailable: null,
234
+ applied: false,
235
+ error: 'Updating hq-anywhere requires both the hq-anywhere-runtime flag and your person setting.',
236
+ });
237
+ continue;
238
+ }
228
239
  // US-006: marketplace sources need the async listings probe (network) —
229
240
  // the sync `resolveLatest` cannot make that call and returns null. Every
230
241
  // other transport keeps the existing synchronous probe unchanged.
@@ -1,7 +1,7 @@
1
1
  import chalk from 'chalk';
2
2
  import { peekHqApiKey, requireApiKeyCapability, } from '../utils/resolve-vault-credential.js';
3
3
  import { MARKETPLACE_PREFIX, PINNED_HQ_ANYWHERE_SOURCE, installPack, sourceMatchesPackPattern, } from './pack-install.js';
4
- import { anywhereRuntimeEnabled } from './anywhere-runtime-flag.js';
4
+ import { anywhereRuntimeAllowed } from '../lib/anywhere-person-setting.js';
5
5
  /**
6
6
  * True when `source` installs through the marketplace — an explicit
7
7
  * `marketplace:` source, or a bare slug (which the dispatch below rewrites into
@@ -68,15 +68,16 @@ export function registerPackageInstallCommand(parent) {
68
68
  process.exitCode = 1;
69
69
  return;
70
70
  }
71
- let marketplaceDir = opts.marketplaceDir;
71
+ const marketplaceDir = opts.marketplaceDir;
72
72
  if (opts.via === 'plugin' &&
73
73
  !marketplaceDir?.trim() &&
74
- !process.env.HQ_CLAUDE_PLUGIN_MARKETPLACE?.trim() &&
75
- anywhereRuntimeEnabled(process.env)) {
76
- runInstallGlobal({ ...opts, marketplaceRepository: 'indigoai-us/hq-packages' });
77
- return;
74
+ !process.env.HQ_CLAUDE_PLUGIN_MARKETPLACE?.trim()) {
75
+ if (await anywhereRuntimeAllowed(process.env)) {
76
+ await runInstallGlobal({ ...opts, marketplaceRepository: 'indigoai-us/hq-packages' });
77
+ return;
78
+ }
78
79
  }
79
- runInstallGlobal({ ...opts, marketplaceDir });
80
+ await runInstallGlobal({ ...opts, marketplaceDir });
80
81
  return;
81
82
  }
82
83
  if (!source) {
@@ -95,8 +96,8 @@ export function registerPackageInstallCommand(parent) {
95
96
  requireApiKeyCapability('packs:install', 'hq install');
96
97
  assertApiKeyCanAuthorizeInstall(source);
97
98
  if (source === '@indigoai-us/hq-anywhere') {
98
- if (!anywhereRuntimeEnabled()) {
99
- console.error(chalk.red('Install failed:'), 'HQ Anywhere installation is disabled by the hq-anywhere-runtime flag.');
99
+ if (!(await anywhereRuntimeAllowed(process.env))) {
100
+ console.error(chalk.red('Install failed:'), 'HQ Anywhere installation requires both the hq-anywhere-runtime flag and your person setting.');
100
101
  process.exit(1);
101
102
  }
102
103
  await installPack(PINNED_HQ_ANYWHERE_SOURCE, {
@@ -15,6 +15,7 @@ export interface RestartAndVerifyDeps extends ConvergenceDeps {
15
15
  env?: NodeJS.ProcessEnv;
16
16
  installedVersion?: () => string | null;
17
17
  log?: (line: string) => void;
18
+ runtimeAllowed?: () => boolean | Promise<boolean>;
18
19
  }
19
20
  /**
20
21
  * Restart the daemon after an update and verify it converged on the installed
@@ -10,14 +10,14 @@ import { daemonPaths } from "../lib/daemon/paths.js";
10
10
  import { writeControlRequest } from "../lib/daemon/requests.js";
11
11
  import { hqdSocketPath } from "../lib/daemon/protocol.js";
12
12
  import { describeNonConvergence, installedCliVersion, pingHqdVersion, verifyDaemonConvergence, } from "../lib/daemon/convergence.js";
13
- import { anywhereRuntimeEnabled } from "./anywhere-runtime-flag.js";
13
+ import { anywhereRuntimeAllowed } from "../lib/anywhere-person-setting.js";
14
14
  /**
15
15
  * Restart the daemon after an update and verify it converged on the installed
16
16
  * hq-cli (policy hq-self-updater-must-verify-convergence-after-install).
17
17
  * Returns false when it did not; the caller must report that as a failure.
18
18
  */
19
19
  export async function restartAndVerifyDaemon(hqRoot, deps = {}) {
20
- if (!anywhereRuntimeEnabled())
20
+ if (!(await (deps.runtimeAllowed ?? (() => anywhereRuntimeAllowed(deps.env ?? process.env)))()))
21
21
  return true;
22
22
  const log = deps.log ?? ((line) => console.log(line));
23
23
  const env = deps.env ?? process.env;
@@ -0,0 +1,17 @@
1
+ export interface ReadHqAnywherePersonSettingOptions {
2
+ readSetting?: () => Promise<boolean | null>;
3
+ getPersonToken?: () => Promise<string>;
4
+ fetchImpl?: typeof fetch;
5
+ apiBaseUrl?: string;
6
+ env?: NodeJS.ProcessEnv;
7
+ onError?: (errorName: string) => void;
8
+ }
9
+ /** Returns null when the person preference cannot be read; callers must fail closed. */
10
+ export declare function readHqAnywherePersonSetting(options?: ReadHqAnywherePersonSettingOptions): Promise<boolean | null>;
11
+ export declare function evaluateAnywhereRuntime(flagEnabled: boolean, setting: boolean | null): {
12
+ enabled: boolean;
13
+ setting: boolean | null;
14
+ };
15
+ /** The operator flag always wins; the person preference is fetched only while it is on. */
16
+ export declare function anywhereRuntimeAllowed(env?: NodeJS.ProcessEnv, options?: ReadHqAnywherePersonSettingOptions): Promise<boolean>;
17
+ //# sourceMappingURL=anywhere-person-setting.d.ts.map
@@ -0,0 +1,41 @@
1
+ import { anywhereRuntimeEnabledFromSnapshot } from "./anywhere-runtime-fast-flag.js";
2
+ /** Returns null when the person preference cannot be read; callers must fail closed. */
3
+ export async function readHqAnywherePersonSetting(options = {}) {
4
+ const env = options.env ?? process.env;
5
+ try {
6
+ const getPersonToken = options.getPersonToken ?? (async () => {
7
+ const { ensureCognitoToken } = await import("../utils/cognito-session.js");
8
+ return ensureCognitoToken({ interactive: false, tokenSource: "person" });
9
+ });
10
+ const token = await getPersonToken();
11
+ const apiBaseUrl = options.apiBaseUrl ?? env.HQ_PRO_API_URL ?? env.HQ_API_URL ?? env.HQ_VAULT_API_URL ?? "https://hqapi.hq.computer";
12
+ const response = await (options.fetchImpl ?? fetch)(`${apiBaseUrl.replace(/\/$/, "")}/v1/bot/auto-schedule?setting=hq-anywhere`, { method: "GET", headers: { Authorization: `Bearer ${token}` }, signal: AbortSignal.timeout(5_000) });
13
+ if (!response.ok)
14
+ throw new Error(`Http${response.status}`);
15
+ const body = await response.json();
16
+ if (typeof body !== "object" || body === null || Array.isArray(body))
17
+ throw new Error("InvalidEnvelope");
18
+ const envelope = body;
19
+ if (envelope.key !== "hq-anywhere" || typeof envelope.value !== "boolean")
20
+ throw new Error("InvalidEnvelope");
21
+ return envelope.value;
22
+ }
23
+ catch (error) {
24
+ const errorName = error instanceof Error && /^[A-Za-z][A-Za-z0-9]{0,63}$/.test(error.name)
25
+ ? error.name
26
+ : "UnknownError";
27
+ (options.onError ?? ((name) => console.error(`HQ Anywhere person setting unavailable (${name}); access remains off.`)))(errorName);
28
+ return null;
29
+ }
30
+ }
31
+ export function evaluateAnywhereRuntime(flagEnabled, setting) {
32
+ return { enabled: flagEnabled && setting === true, setting };
33
+ }
34
+ /** The operator flag always wins; the person preference is fetched only while it is on. */
35
+ export async function anywhereRuntimeAllowed(env = process.env, options = {}) {
36
+ if (!anywhereRuntimeEnabledFromSnapshot(env))
37
+ return false;
38
+ const setting = options.readSetting ? await options.readSetting() : await readHqAnywherePersonSetting({ ...options, env });
39
+ return evaluateAnywhereRuntime(true, setting).enabled;
40
+ }
41
+ //# sourceMappingURL=anywhere-person-setting.js.map
@@ -9,7 +9,7 @@ export interface McpOAuthOptions {
9
9
  fetchImpl?: typeof fetch;
10
10
  openBrowser?: (url: string) => Promise<unknown>;
11
11
  writeOutput?: (message: string) => void;
12
- isEnabled?: () => boolean;
12
+ isEnabled?: () => boolean | Promise<boolean>;
13
13
  isMachine?: () => boolean;
14
14
  now?: () => number;
15
15
  callbackTimeoutMs?: number;
@@ -6,7 +6,7 @@ import * as path from "node:path";
6
6
  import open from "open";
7
7
  import { resolveCognitoTokenSource } from "../../utils/cognito-session.js";
8
8
  import { markExpectedUserError } from "../../utils/expected-cli-error.js";
9
- import { anywhereRuntimeEnabled } from "../../commands/anywhere-runtime-flag.js";
9
+ import { anywhereRuntimeAllowed } from "../anywhere-person-setting.js";
10
10
  import { resolveHqMcpOAuthOrigin } from "./mcp-endpoint.js";
11
11
  const MCP_SCOPE = "openid email profile hq-mcp/use";
12
12
  const EXPIRY_SKEW_MS = 60_000;
@@ -128,8 +128,8 @@ function newPkce() {
128
128
  }
129
129
  async function runLogin(options) {
130
130
  const env = options.env ?? process.env;
131
- if (!(options.isEnabled ?? (() => anywhereRuntimeEnabled(env)))()) {
132
- throw markExpectedUserError(new McpOAuthError("HQ Anywhere is not enabled. Enable the hq-anywhere-runtime flag to use `hq mcp login`."));
131
+ if (!(await (options.isEnabled ?? (() => anywhereRuntimeAllowed(env)))())) {
132
+ throw markExpectedUserError(new McpOAuthError("HQ Anywhere is not enabled. The hq-anywhere-runtime flag and your HQ Anywhere person setting must both be on to use `hq mcp login`."));
133
133
  }
134
134
  if ((options.isMachine ?? (() => resolveCognitoTokenSource(env) === "machine"))()) {
135
135
  throw new McpOAuthError("`hq mcp login` is for person accounts; this box uses machine credentials.");
@@ -14,6 +14,7 @@ import { fileURLToPath } from "node:url";
14
14
  import { loadCachedTokens } from "@indigoai-us/hq-cloud";
15
15
  import { CLI_VERSION } from "../../cli-version.js";
16
16
  import { anywhereRuntimeEnabled } from "../../commands/anywhere-runtime-flag.js";
17
+ import { anywhereRuntimeAllowed } from "../anywhere-person-setting.js";
17
18
  import { currentPackageUseLeaseInstall, currentPackageUseUpdateRequested } from "../../utils/package-use-lease.js";
18
19
  import { isLocalDependencyInstall, resolveRunningInstall } from "../../utils/version-gate.js";
19
20
  import { agentKitPaths } from "../agent-kit/paths.js";
@@ -836,7 +837,9 @@ export async function runDaemon(opts) {
836
837
  // hqd: the session/company socket every runtime talks to (hq-anywhere US-008).
837
838
  let hqd = null;
838
839
  let hqdShutdown = null;
839
- if (shouldStartHqd(anywhereRuntimeEnabled())) {
840
+ if (shouldStartHqd(await anywhereRuntimeAllowed(env, {
841
+ onError: (name) => log("warn", `hqd person setting read failed (${name}); runtime remains off`),
842
+ }))) {
840
843
  try {
841
844
  const startConfig = readDaemonConfig(paths.configPath);
842
845
  hqd = await startHqdServer({
@@ -43,6 +43,8 @@ export interface HqdServerOptions {
43
43
  mcpEnabled?: () => boolean | Promise<boolean>;
44
44
  /** Test seam for the live hq-flags read used by the daemon's bounded runtime monitor. */
45
45
  runtimeFlagReader?: (options: ReadDaemonFlagOptions) => Promise<boolean>;
46
+ /** Test seam for the human opt-in setting; production reads hq-pro. */
47
+ personSettingReader?: () => Promise<boolean | null>;
46
48
  /** Test seam for the hosted Streamable HTTP MCP transport. */
47
49
  mcpBridge?: HqMcpBridge;
48
50
  /** Test seam for machine login while keeping production credentials off disk. */
@@ -23,6 +23,7 @@ import { defaultMcpToken, HqMcpBridge, jsonResult, McpOpError, runWorker } from
23
23
  import { authenticateMachineCredentials, machineTokenExpiresAt, MachineLoginError, parseStdinMachineCredentials } from "./machine-login.js";
24
24
  import { HQ_MCP_TOOL_NAMES, HQD_NATIVE_TOOL_NAMES } from "../mcp/tools.js";
25
25
  import { HQ_ANYWHERE_RUNTIME_FLAG } from "../../commands/anywhere-runtime-flag.js";
26
+ import { readHqAnywherePersonSetting } from "../anywhere-person-setting.js";
26
27
  import { createBoundedDaemonFlagMonitor } from "./flag-monitor.js";
27
28
  import { readDaemonFlag } from "./flag.js";
28
29
  import { startWindowsPipeHost, validateWindowsPipeAcl, WINDOWS_PIPE_STARTUP_REFUSAL } from "./windows-pipe-host.js";
@@ -396,16 +397,23 @@ export async function startHqdServer(opts) {
396
397
  }
397
398
  }
398
399
  const runtimeFlagMonitor = opts.mcpEnabled ? undefined : createBoundedDaemonFlagMonitor({
399
- read: () => {
400
+ read: async () => {
400
401
  const options = {
401
402
  env,
402
403
  throwOnError: true,
403
404
  // A local opt-out is honored, while a local opt-in cannot override the operator kill switch.
404
405
  localOverrideMode: "off-only",
405
406
  };
406
- return opts.runtimeFlagReader
407
+ const flagEnabled = await (opts.runtimeFlagReader
407
408
  ? opts.runtimeFlagReader(options)
408
- : readDaemonFlag(HQ_ANYWHERE_RUNTIME_FLAG, options);
409
+ : readDaemonFlag(HQ_ANYWHERE_RUNTIME_FLAG, options));
410
+ if (!flagEnabled)
411
+ return false;
412
+ const personSetting = await (opts.personSettingReader ?? (() => readHqAnywherePersonSetting({
413
+ env,
414
+ onError: (name) => log("warn", `hqd person setting refresh failed (${name}); keeping HQ Anywhere off`),
415
+ })))();
416
+ return personSetting === true;
409
417
  },
410
418
  onChange: (enabled) => {
411
419
  if (!enabled) {
@@ -2,6 +2,8 @@ import * as fs from "node:fs";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
4
  import { runDoctor as runDoctorCommand, } from "../../../commands/doctor.js";
5
+ import { createAnywhereFamily } from "../checks/anywhere.js";
6
+ import { createDefaultRegistry } from "../registry.js";
5
7
  /**
6
8
  * Run doctor tests against synthetic state. The production registry normally
7
9
  * reads local HQ work-context, sync, daemon, and session state; tests must not
@@ -37,7 +39,14 @@ export async function runDoctorForTest(options = {}) {
37
39
  else
38
40
  process.env[key] = value;
39
41
  }
40
- return await runDoctorCommand({ ...options, integrations: false });
42
+ let registry = options.registry;
43
+ if (!registry) {
44
+ registry = createDefaultRegistry();
45
+ // Command tests exercise doctor rendering and exit behavior offline. The
46
+ // real person preference is covered in the anywhere-family tests.
47
+ registry.register(createAnywhereFamily({ personSettingReader: async () => false }));
48
+ }
49
+ return await runDoctorCommand({ ...options, registry, integrations: false });
41
50
  }
42
51
  finally {
43
52
  for (const [key, value] of originalEnv) {
@@ -31,6 +31,8 @@ export interface AnywhereDeps {
31
31
  health?: (cwd: string) => Promise<HqdHealthResult>;
32
32
  /** Test seam for the local runtime gate used to keep other checks out of a disabled runtime. */
33
33
  runtimeEnabled?: () => boolean;
34
+ /** Test seam for the per-person HQ Anywhere setting; production reads hq-pro. */
35
+ personSettingReader?: () => Promise<boolean | null>;
34
36
  /** The hq-cli version installed on this machine. Default: this CLI's version. */
35
37
  cliVersion?: string;
36
38
  serviceState?: () => DaemonServiceState;
@@ -24,6 +24,7 @@ import { DAEMON_LAUNCHD_LABEL, DAEMON_SYSTEMD_UNIT, DAEMON_WINDOWS_TASK } from "
24
24
  import { hqdSocketPath } from "../../daemon/protocol.js";
25
25
  import { readRegistry, registryDir, registryFile } from "../../registry/registry.js";
26
26
  import { anywhereRuntimeEnabled } from "../../../commands/anywhere-runtime-flag.js";
27
+ import { readHqAnywherePersonSetting } from "../../anywhere-person-setting.js";
27
28
  const ANYWHERE_FAMILY_ID = "anywhere";
28
29
  export const REMEDIATION = {
29
30
  installClaude: "hq install --global --runtime claude",
@@ -44,7 +45,15 @@ async function runAnywhereChecks(context, deps = {}) {
44
45
  const env = deps.env ?? process.env;
45
46
  const socketPath = hqdSocketPath(home, env);
46
47
  const cwd = context.cwd ?? process.cwd();
47
- const runtimeEnabled = (deps.runtimeEnabled ?? anywhereRuntimeEnabled)();
48
+ const flagEnabled = (deps.runtimeEnabled ?? anywhereRuntimeEnabled)();
49
+ const personSetting = await (deps.personSettingReader ?? (deps.runtimeEnabled ? async () => true : () => readHqAnywherePersonSetting({
50
+ env,
51
+ onError: (name) => process.stderr.write(`HQ Anywhere person setting unavailable (${name}); access remains off.\n`),
52
+ })))();
53
+ const runtimeEnabled = flagEnabled && personSetting === true;
54
+ const settingCheck = personSetting === null
55
+ ? result("UNKNOWN", "person-setting", "HQ Anywhere person setting could not be read; runtime access is off")
56
+ : result(personSetting ? "PASS" : "WARN", "person-setting", `HQ Anywhere person setting is ${personSetting ? "on" : "off"}`);
48
57
  let health;
49
58
  let healthUnsupported = false;
50
59
  try {
@@ -107,12 +116,12 @@ async function runAnywhereChecks(context, deps = {}) {
107
116
  : undefined;
108
117
  const healthCheck = runtimeHealthCheck(health, healthUnsupported, serviceState);
109
118
  if (!runtimeEnabled)
110
- return [healthCheck];
119
+ return [healthCheck, settingCheck];
111
120
  const remoteModes = [
112
121
  result("WARN", "chatgpt-mode", `ChatGPT integration mode: ${CHATGPT_MODE}; host connector setup is managed in ChatGPT`, "hq install --global --runtime chatgpt"),
113
122
  result("WARN", "grok-mode", `Grok integration mode: ${GROK_MODE}; host connector setup is managed in Grok`, "hq install --global --runtime grok"),
114
123
  ];
115
- const out = [healthCheck, ...remoteModes];
124
+ const out = [healthCheck, settingCheck, ...remoteModes];
116
125
  if (!installed) {
117
126
  return [...out, result("NA", "installed", "HQ local anywhere runtime is not installed on this machine", REMEDIATION.installClaude)];
118
127
  }
@@ -40,7 +40,7 @@ export const RESIDENT_PACKAGE_MODULES = {
40
40
  "@indigoai-us/hq-cloud",
41
41
  ],
42
42
  "bot-logs-follow": [],
43
- mcp: ["../lib/mcp/tools.js", "../lib/mcp/prompts.js"],
43
+ mcp: ["../lib/mcp/tools.js", "../lib/mcp/prompts.js", "../utils/cognito-session.js", "@indigoai-us/hq-cloud", "../sentry.js"],
44
44
  childWrapper: ["../main.js"],
45
45
  coreJobRun: ["../lib/flag-registry.js"],
46
46
  "daemon-call": ["../lib/daemon/protocol.js"],
@@ -170,6 +170,12 @@ const LOADERS = {
170
170
  mcp: [
171
171
  () => import("../lib/mcp/tools.js"),
172
172
  () => import("../lib/mcp/prompts.js"),
173
+ () => import("../utils/cognito-session.js"),
174
+ () => import("@indigoai-us/hq-cloud"),
175
+ async () => {
176
+ const { preloadSentrySdk } = await import("../sentry.js");
177
+ preloadSentrySdk();
178
+ },
173
179
  ],
174
180
  childWrapper: [
175
181
  () => import("../main.js"),
@@ -5,7 +5,9 @@ local hqd daemon on macOS, Linux, and Windows. On Windows, hqd uses a bundled
5
5
  PowerShell pipe host that applies and verifies a user-only named-pipe DACL
6
6
  before becoming ready. A per-user
7
7
  registry links repositories to HQ companies. The `hq-anywhere-runtime` flag
8
- controls these features and is off by default.
8
+ is the operator kill switch and is off by default. Runtime access also
9
+ requires the current person to enable the `hq-anywhere` setting in HQ. If that
10
+ setting cannot be read, runtime access stays off.
9
11
 
10
12
  ## Install
11
13
 
@@ -18,10 +20,12 @@ hq daemon status
18
20
  hq install --global --runtime claude
19
21
  ```
20
22
 
21
- `hq install --global` defaults to Claude. Select other supported runtimes
22
- explicitly with `--runtime codex`, `--runtime chatgpt`, or `--runtime grok`.
23
+ `hq install --global` defaults to Claude and requires both the runtime flag
24
+ and the current person's `hq-anywhere` setting. Select other supported
25
+ runtimes explicitly with `--runtime codex`, `--runtime chatgpt`, or
26
+ `--runtime grok`.
23
27
  Claude and Codex receive local hook configuration. ChatGPT and Grok print
24
- connector setup instructions and require the runtime flag to be enabled.
28
+ connector setup instructions and require both gates to be enabled.
25
29
 
26
30
  The CLI stores the HQ root at `~/.hq/root` and keeps the folder registry in
27
31
  `~/.hq/registry.json`. These files belong to the current host. They are not
@@ -66,7 +70,9 @@ request error. The daemon process itself does not terminate.
66
70
 
67
71
  `hq doctor` reports hqd reachability, version and uptime, the last successful
68
72
  runtime-flag read and its age, and whether hqd is holding that decision after a
69
- read error. It also reports the company selected for the current directory or
73
+ read error. It reports the current person's `hq-anywhere` setting as on, off,
74
+ or unknown when the setting endpoint cannot be read; unknown keeps runtime
75
+ access off. It also reports the company selected for the current directory or
70
76
  the reason it fell back to personal context. On Unix, the socket line includes
71
77
  its permission mode, owner check and whether it is user-only. Windows reports
72
78
  the named-pipe transport without Unix mode or owner fields. Use `hq doctor
@@ -80,8 +86,8 @@ resident daemon does not support the health operation, doctor falls back to
80
86
  `ping` for reachability, version and uptime, then warns that a restart is needed
81
87
  for full diagnostics.
82
88
 
83
- The hq-anywhere-runtime flag also gates the published Claude plugin marketplace
84
- and Codex pack source. When enabled, hq-cli adds the `indigoai-us/hq-packages`
89
+ The flag and person setting also gate the published Claude plugin marketplace
90
+ and Codex pack source. When both are on, hq-cli adds the `indigoai-us/hq-packages`
85
91
  Claude marketplace and installs the `hq` plugin, or resolves the exact
86
92
  `@indigoai-us/hq-anywhere` alias to
87
93
  `github:indigoai-us/hq-packages#packages/hq-pack-hq-anywhere`. The flag-off
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.345.30",
3
+ "version": "5.345.32",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {