@indigoai-us/hq-cli 5.103.19 → 5.103.21

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 CHANGED
@@ -2,6 +2,33 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.103.21] — 2026-08-24
6
+
7
+ ### Fixed
8
+
9
+ - `hq agents jobs list|pause|cancel` now fails closed on an agent box instead of
10
+ provoking a cross-agent rejection on the server. These off-box operator
11
+ commands are reachable on agent boxes, where the CLI authenticates with the
12
+ box's own agent machine identity; forwarding another agent's identifier into
13
+ `GET /v1/agents/{uid}/jobs` was refused server-side by hq-pro's self-only jobs
14
+ guard and logged as an unexpected warning, while the CLI mislabeled the 403 as
15
+ "You need owner/admin on this company." — a role problem a box can never
16
+ satisfy. The commands now resolve the agent reference through the company
17
+ roster (a slug or name becomes an `agt_` uid, or fails locally), and refuse a
18
+ cross-agent target before any request leaves the box, naming the on-box
19
+ `hq-agent-jobs` tool and the person session required to manage another agent's
20
+ jobs. A server-side `CROSS_AGENT_JOB` denial is now rendered as identity-scoped
21
+ guidance rather than the owner/admin copy. Separately, every authenticated
22
+ vault-API request now carries an `x-hq-client-name` header so its origin is
23
+ attributable in telemetry and error reports. (Sentry 7617401436)
24
+
25
+ ## [5.103.20] — 2026-08-24
26
+
27
+ ### Changed
28
+
29
+ - The bundled sync engine now requires `@indigoai-us/hq-cloud` 6.15.37, the
30
+ current production release.
31
+
5
32
  ## [5.103.19] — 2026-08-22
6
33
 
7
34
  ### Fixed
@@ -267,6 +267,29 @@ export interface DmThreadMessage {
267
267
  * membership).
268
268
  */
269
269
  export declare function resolveAgentUid(token: string, ref: string, companySlug: string | undefined): Promise<string>;
270
+ /**
271
+ * Fail-closed local guard for the off-box `hq agents jobs` operator commands.
272
+ *
273
+ * hq-pro's jobs endpoints are self-only for an agent machine identity: an
274
+ * agent-authenticated list/pause/cancel against `/v1/agents/{uid}/jobs` whose
275
+ * `{uid}` is not the caller's OWN agt_ uid is refused server-side as 403
276
+ * `CROSS_AGENT_JOB` and logged loud (`caller_uid_mismatch`, Sentry hq-pro
277
+ * 7617401436). That happens when these commands are run ON an agent box, where
278
+ * {@link resolveVaultCredential} silently returns the box's own agent ID token —
279
+ * the operator meant to act as a person with owner/admin, not as the box.
280
+ *
281
+ * Catch it locally so the call never leaves the box: decode the (already
282
+ * locally trusted, signature-unverified) bearer token and, ONLY when it proves
283
+ * an agent identity acting on a DIFFERENT agent, return a refusal string for the
284
+ * command to print before any HTTP request. Every other shape fails OPEN
285
+ * (returns `null`) and proceeds to the server exactly as today: a person session
286
+ * (no `custom:entityType` claim), an opaque / non-JWT credential (an `hqk_` API
287
+ * key), a token with a missing or malformed uid claim, or an agent acting on its
288
+ * OWN jobs.
289
+ *
290
+ * Pure: no network, no logging; never prints or returns the token itself.
291
+ */
292
+ export declare function crossAgentJobsRefusal(token: string, resolvedAgentUid: string): string | null;
270
293
  /** Send a DM to an agent. Returns nothing meaningful beyond success. */
271
294
  export declare function sendAgentDm(token: string, agentUid: string, message: string): Promise<void>;
272
295
  /** Read the two-way DM conversation with an agent (most recent `limit`). */
@@ -30,6 +30,7 @@ import * as readline from "node:readline";
30
30
  import { resolveVaultCredential } from "../utils/resolve-vault-credential.js";
31
31
  import { gateApiKeyCapabilities } from "../utils/api-key-command-gate.js";
32
32
  import { vaultApiFetch, getCompanyUid } from "../utils/vault-api.js";
33
+ import { peekIdToken } from "../utils/id-token.js";
33
34
  import { isPlanGateError } from "../utils/plan-gate-error.js";
34
35
  import { confirmChargeOrExit, formatUsd, parseBillingPayload, surfaceBillingBlocked, } from "../utils/billing-gate.js";
35
36
  /** Reasoning-effort values hq-pro accepts on `runtime-config`. */
@@ -345,6 +346,17 @@ export async function cancelAgentJob(token, agentUid, jobId) {
345
346
  export function formatJobsHttpError(err, jobId) {
346
347
  if (err.status === 401)
347
348
  return "Not authenticated — run `hq login`.";
349
+ // A 403 CROSS_AGENT_JOB is hq-pro's self-only jobs guard, not a role problem:
350
+ // the caller authenticated as one agent and asked about another. Naming it as
351
+ // "owner/admin required" (below) is misleading copy an agent box can never
352
+ // satisfy, so distinguish it here (defence in depth — the CLI now also fails
353
+ // closed locally before the request, see crossAgentJobsRefusal).
354
+ if (err.status === 403 && err.code === "CROSS_AGENT_JOB") {
355
+ return ("Jobs are scoped to the calling agent's own identity — an agent may only " +
356
+ "list, pause, or cancel its OWN jobs (use the on-box `hq-agent-jobs` tool). " +
357
+ "Managing another agent's jobs needs a person session with owner/admin on " +
358
+ "the company.");
359
+ }
348
360
  if (err.status === 403)
349
361
  return "You need owner/admin on this company.";
350
362
  if (err.status === 404 && err.code === "JOB_NOT_FOUND") {
@@ -420,6 +432,77 @@ export async function resolveAgentUid(token, ref, companySlug) {
420
432
  }
421
433
  return match.uid;
422
434
  }
435
+ /**
436
+ * Fail-closed local guard for the off-box `hq agents jobs` operator commands.
437
+ *
438
+ * hq-pro's jobs endpoints are self-only for an agent machine identity: an
439
+ * agent-authenticated list/pause/cancel against `/v1/agents/{uid}/jobs` whose
440
+ * `{uid}` is not the caller's OWN agt_ uid is refused server-side as 403
441
+ * `CROSS_AGENT_JOB` and logged loud (`caller_uid_mismatch`, Sentry hq-pro
442
+ * 7617401436). That happens when these commands are run ON an agent box, where
443
+ * {@link resolveVaultCredential} silently returns the box's own agent ID token —
444
+ * the operator meant to act as a person with owner/admin, not as the box.
445
+ *
446
+ * Catch it locally so the call never leaves the box: decode the (already
447
+ * locally trusted, signature-unverified) bearer token and, ONLY when it proves
448
+ * an agent identity acting on a DIFFERENT agent, return a refusal string for the
449
+ * command to print before any HTTP request. Every other shape fails OPEN
450
+ * (returns `null`) and proceeds to the server exactly as today: a person session
451
+ * (no `custom:entityType` claim), an opaque / non-JWT credential (an `hqk_` API
452
+ * key), a token with a missing or malformed uid claim, or an agent acting on its
453
+ * OWN jobs.
454
+ *
455
+ * Pure: no network, no logging; never prints or returns the token itself.
456
+ */
457
+ export function crossAgentJobsRefusal(token, resolvedAgentUid) {
458
+ const claims = peekIdToken(token);
459
+ const entityType = typeof claims["custom:entityType"] === "string"
460
+ ? claims["custom:entityType"]
461
+ : undefined;
462
+ const entityUid = typeof claims["custom:entityUid"] === "string"
463
+ ? claims["custom:entityUid"].trim()
464
+ : "";
465
+ // Positive proof required before refusing: an agent identity, a well-formed
466
+ // agt_ own-uid, and a target that differs from it. Anything else proceeds.
467
+ if (entityType !== "agent")
468
+ return null;
469
+ if (!AGENT_UID_PATTERN.test(entityUid))
470
+ return null;
471
+ if (entityUid === resolvedAgentUid.trim())
472
+ return null;
473
+ return (`Refusing to manage ${resolvedAgentUid}'s scheduled jobs using this agent's ` +
474
+ `own identity (${entityUid}). On an agent box, \`hq agents jobs\` authenticates ` +
475
+ `as the box itself, and hq-pro scopes an agent to its OWN jobs only — inspect ` +
476
+ `those with the on-box \`hq-agent-jobs\` tool. To manage another agent's jobs, ` +
477
+ `run this from a person session with owner/admin on the company (\`hq login\`), ` +
478
+ `not on the agent box.`);
479
+ }
480
+ /**
481
+ * Shared preamble for the three `hq agents jobs` actions: resolve the caller's
482
+ * credential, turn the (possibly slug/name) reference into an `agt_` uid via the
483
+ * roster, and fail closed BEFORE any jobs request when an agent identity targets
484
+ * another agent ({@link crossAgentJobsRefusal}). Returns the token + resolved
485
+ * uid for the caller's single jobs request. Never returns on a refusal or a
486
+ * resolution error — it prints and exits non-zero. The refusal exit lives
487
+ * OUTSIDE the resolution try so it is not re-wrapped as a generic failure.
488
+ */
489
+ async function resolveJobsTargetOrExit(ref, company) {
490
+ let token;
491
+ let targetUid;
492
+ try {
493
+ token = (await resolveVaultCredential()).token;
494
+ targetUid = await resolveAgentUid(token, ref, company);
495
+ }
496
+ catch (err) {
497
+ return failJobs(err);
498
+ }
499
+ const refusal = crossAgentJobsRefusal(token, targetUid);
500
+ if (refusal) {
501
+ console.error(chalk.red(refusal));
502
+ process.exit(1);
503
+ }
504
+ return { token, targetUid };
505
+ }
423
506
  /** Send a DM to an agent. Returns nothing meaningful beyond success. */
424
507
  export async function sendAgentDm(token, agentUid, message) {
425
508
  await agentsRequest({
@@ -934,11 +1017,12 @@ export function registerAgentsCommand(program) {
934
1017
  jobs
935
1018
  .command("list <agentUid>")
936
1019
  .description("List an agent's scheduled jobs")
1020
+ .option("--company <slug>", "Company slug (to resolve an agent by name/slug)")
937
1021
  .option("--json", "Emit raw JSON")
938
- .action(async (agentUid, opts) => {
1022
+ .action(async function (agentUid, opts) {
1023
+ const { token, targetUid } = await resolveJobsTargetOrExit(agentUid, companyOf(this));
939
1024
  try {
940
- const token = (await resolveVaultCredential()).token;
941
- const roster = await listAgentJobs(token, agentUid);
1025
+ const roster = await listAgentJobs(token, targetUid);
942
1026
  if (opts.json) {
943
1027
  process.stdout.write(JSON.stringify(roster, null, 2) + "\n");
944
1028
  return;
@@ -956,10 +1040,11 @@ export function registerAgentsCommand(program) {
956
1040
  jobs
957
1041
  .command("pause <agentUid> <jobId>")
958
1042
  .description("Pause a job's schedule (reversible)")
959
- .action(async (agentUid, jobId) => {
1043
+ .option("--company <slug>", "Company slug (to resolve an agent by name/slug)")
1044
+ .action(async function (agentUid, jobId) {
1045
+ const { token, targetUid } = await resolveJobsTargetOrExit(agentUid, companyOf(this));
960
1046
  try {
961
- const token = (await resolveVaultCredential()).token;
962
- const result = await pauseAgentJob(token, agentUid, jobId);
1047
+ const result = await pauseAgentJob(token, targetUid, jobId);
963
1048
  console.log(chalk.green(formatPauseResult(result)));
964
1049
  }
965
1050
  catch (err) {
@@ -969,10 +1054,11 @@ export function registerAgentsCommand(program) {
969
1054
  jobs
970
1055
  .command("cancel <agentUid> <jobId>")
971
1056
  .description("Cancel a job and delete its schedule")
972
- .action(async (agentUid, jobId) => {
1057
+ .option("--company <slug>", "Company slug (to resolve an agent by name/slug)")
1058
+ .action(async function (agentUid, jobId) {
1059
+ const { token, targetUid } = await resolveJobsTargetOrExit(agentUid, companyOf(this));
973
1060
  try {
974
- const token = (await resolveVaultCredential()).token;
975
- const result = await cancelAgentJob(token, agentUid, jobId);
1061
+ const result = await cancelAgentJob(token, targetUid, jobId);
976
1062
  console.log(chalk.green(`Cancelled ${result.jobId}.`));
977
1063
  }
978
1064
  catch (err) {
@@ -134,6 +134,22 @@ export interface InstallInput {
134
134
  docsUrl?: string;
135
135
  authMode?: "none" | "bearer";
136
136
  bearerToken?: string;
137
+ /** Preserve a non-default placement for a pasted key (for example X-API-Key). */
138
+ authScheme?: {
139
+ placement: "authorization";
140
+ format: "bearer";
141
+ } | {
142
+ placement: "authorization";
143
+ format: "prefix";
144
+ prefix: string;
145
+ } | {
146
+ placement: "authorization";
147
+ format: "basic";
148
+ username?: string;
149
+ } | {
150
+ placement: "header";
151
+ header: string;
152
+ };
137
153
  }
138
154
  export declare function installIntegration(token: string, companyUid: string, input: InstallInput): Promise<InstallResult>;
139
155
  export declare function uninstallIntegration(token: string, companyUid: string, installationId: string): Promise<{
@@ -278,6 +278,14 @@ async function findCatalogEntry(token, companyUid, domain) {
278
278
  function domainLabel(domain) {
279
279
  return domain.trim().toLowerCase().split(".")[0] ?? "";
280
280
  }
281
+ /** A catalog display name's copy-pasteable command-line slug. */
282
+ function displayNameSlug(name) {
283
+ return name
284
+ .trim()
285
+ .toLowerCase()
286
+ .replace(/[^a-z0-9]+/g, "-")
287
+ .replace(/^-+|-+$/g, "");
288
+ }
281
289
  /**
282
290
  * Best-effort resolution of a bare name to a single catalog entry. Matches the
283
291
  * entry's registrable domain label (`notion` → `notion.com`) or an exact
@@ -293,7 +301,8 @@ async function findCatalogEntryByName(token, companyUid, name) {
293
301
  return null;
294
302
  const entries = await listCatalog(token, companyUid, { query: name, limit: 20 });
295
303
  const matches = entries.filter((entry) => domainLabel(entry.domain) === want ||
296
- entry.name?.trim().toLowerCase() === want);
304
+ entry.name?.trim().toLowerCase() === want ||
305
+ (entry.name !== undefined && displayNameSlug(entry.name) === displayNameSlug(want)));
297
306
  if (matches.length === 0)
298
307
  return null;
299
308
  // Collapse rows that point at the same app (same domain) before deciding
@@ -201,8 +201,10 @@ export declare function fetchAdminSurface(token: string, companyUid: string): Pr
201
201
  export declare function fetchConnections(token: string, companyUid: string): Promise<AdminConnection[]>;
202
202
  /**
203
203
  * Resolve one connection by `--connection acct_…` or `--provider linear`
204
- * (matches `factory:<slug>` and bare provider ids, case-insensitive). Errors
205
- * list what IS connected so the fix is one command away.
204
+ * (matches `factory:<slug>`, bare provider ids, and an installation's human
205
+ * display-name slug, case-insensitive). The legacy provider id remains a
206
+ * first-class match, so scripts that saved opaque historical slugs keep
207
+ * working. Errors list what IS connected so the fix is one command away.
206
208
  */
207
209
  export declare function selectConnection(connections: AdminConnection[], opts: {
208
210
  connection?: string;
@@ -199,6 +199,14 @@ export function toolPrefixForProvider(provider) {
199
199
  export function bareProvider(provider) {
200
200
  return provider.replace(/^factory:/, "");
201
201
  }
202
+ /** Turn a factory display name into the human-friendly slug people type. */
203
+ function humanSlug(value) {
204
+ return value
205
+ .trim()
206
+ .toLowerCase()
207
+ .replace(/[^a-z0-9]+/g, "-")
208
+ .replace(/^-+|-+$/g, "");
209
+ }
202
210
  /**
203
211
  * Read the whole admin surface: connections with their governance state, the
204
212
  * viewer's role, and the recent audit feed. Several verbs need more than the
@@ -231,8 +239,10 @@ export async function fetchConnections(token, companyUid) {
231
239
  }
232
240
  /**
233
241
  * Resolve one connection by `--connection acct_…` or `--provider linear`
234
- * (matches `factory:<slug>` and bare provider ids, case-insensitive). Errors
235
- * list what IS connected so the fix is one command away.
242
+ * (matches `factory:<slug>`, bare provider ids, and an installation's human
243
+ * display-name slug, case-insensitive). The legacy provider id remains a
244
+ * first-class match, so scripts that saved opaque historical slugs keep
245
+ * working. Errors list what IS connected so the fix is one command away.
236
246
  */
237
247
  export function selectConnection(connections, opts) {
238
248
  const active = connections.filter((c) => c.status !== "revoked");
@@ -245,16 +255,28 @@ export function selectConnection(connections, opts) {
245
255
  }
246
256
  if (opts.provider) {
247
257
  const want = opts.provider.trim().toLowerCase();
248
- const match = active.find((c) => {
249
- const bare = bareProvider(c.provider).toLowerCase();
250
- return bare === want || c.provider.toLowerCase() === want;
251
- });
252
- if (!match) {
253
- const available = active.map((c) => bareProvider(c.provider)).join(", ");
254
- throw new IntegrationsCliError(`No connected app matches '${opts.provider}'.` +
255
- (available ? ` Connected: ${available}.` : " Nothing is connected yet — connect apps with `hq integrations connect <app>`."), { expected: true });
258
+ const wantHumanSlug = humanSlug(opts.provider);
259
+ const providerMatch = active.find((c) => bareProvider(c.provider).toLowerCase() === want || c.provider.toLowerCase() === want);
260
+ if (providerMatch)
261
+ return providerMatch;
262
+ const displayNameMatch = active.find((c) => c.installation?.displayName?.trim().toLowerCase() === want);
263
+ if (displayNameMatch)
264
+ return displayNameMatch;
265
+ const aliasMatches = wantHumanSlug
266
+ ? active.filter((c) => {
267
+ const displayName = c.installation?.displayName;
268
+ const displayNameSlug = displayName ? humanSlug(displayName) : "";
269
+ return displayNameSlug !== "" && displayNameSlug === wantHumanSlug;
270
+ })
271
+ : [];
272
+ if (aliasMatches.length === 1)
273
+ return aliasMatches[0];
274
+ if (aliasMatches.length > 1) {
275
+ throw new IntegrationsCliError(`Display-name alias '${opts.provider}' matches multiple connected apps. Use --connection to choose one.`, { expected: true });
256
276
  }
257
- return match;
277
+ const available = active.map((c) => bareProvider(c.provider)).join(", ");
278
+ throw new IntegrationsCliError(`No connected app matches '${opts.provider}'.` +
279
+ (available ? ` Connected: ${available}.` : " Nothing is connected yet — connect apps with `hq integrations connect <app>`."), { expected: true });
258
280
  }
259
281
  if (active.length === 1)
260
282
  return active[0];
@@ -0,0 +1,24 @@
1
+ import { Command } from "commander";
2
+ import { type McpManifest } from "./mcp-registration.js";
3
+ export type DesktopConnector = McpManifest & {
4
+ [key: string]: unknown;
5
+ };
6
+ export type ImportStatus = "imported" | "needs-signin" | "shared-for-local" | "skipped";
7
+ export interface ImportOutcome {
8
+ name: string;
9
+ status: ImportStatus;
10
+ reason?: string;
11
+ provider?: string;
12
+ path?: string;
13
+ installCommand?: string;
14
+ authorizationUrl?: string;
15
+ redactedArgumentCredentials?: boolean;
16
+ }
17
+ /** Claude Desktop's documented connector-config location for the current OS. */
18
+ export declare function claudeDesktopConfigPath(platform?: NodeJS.Platform, home?: string, env?: NodeJS.ProcessEnv): string;
19
+ /** Read only the supported Claude Desktop `mcpServers` object. */
20
+ export declare function readClaudeDesktopConnectors(configPath?: string): Record<string, DesktopConnector>;
21
+ /** Never persist local desktop credentials in a company-synced connector file. */
22
+ export declare function stripConnectorSecrets(entry: DesktopConnector): McpManifest;
23
+ export declare function registerImportCommands(integrations: Command): void;
24
+ //# sourceMappingURL=integrations-import.d.ts.map
@@ -0,0 +1,364 @@
1
+ /**
2
+ * Import Claude Desktop MCP connectors into an HQ company.
3
+ *
4
+ * Remote servers are handed to integration-factory; stdio servers remain local
5
+ * and are shared as secret-stripped manifests for each teammate to install.
6
+ */
7
+ import * as fs from "node:fs";
8
+ import * as os from "node:os";
9
+ import * as path from "node:path";
10
+ import chalk from "chalk";
11
+ import { ensureCognitoIdToken, ensureCognitoToken, resolveDefaultHqRoot } from "../utils/cognito-session.js";
12
+ import { getCompanyUid, vaultApiFetch } from "../utils/vault-api.js";
13
+ import { bareProvider, IntegrationsCliError, printJson } from "./integrations-core.js";
14
+ import { installIntegration, startOAuth } from "./integrations-api.js";
15
+ import { loadRevealedSecrets } from "./secrets.js";
16
+ import { registerServer } from "./mcp-registration.js";
17
+ const OAUTH_REQUIRED_CODE = "INTEGRATION_FACTORY_OAUTH_REQUIRED";
18
+ /** Claude Desktop's documented connector-config location for the current OS. */
19
+ export function claudeDesktopConfigPath(platform = process.platform, home = os.homedir(), env = process.env) {
20
+ if (platform === "darwin") {
21
+ return path.join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
22
+ }
23
+ if (platform === "win32") {
24
+ const appData = env.APPDATA?.trim();
25
+ return path.join(appData || path.join(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
26
+ }
27
+ return path.join(home, ".config", "Claude", "claude_desktop_config.json");
28
+ }
29
+ /** Read only the supported Claude Desktop `mcpServers` object. */
30
+ export function readClaudeDesktopConnectors(configPath = claudeDesktopConfigPath()) {
31
+ if (!fs.existsSync(configPath))
32
+ return {};
33
+ let parsed;
34
+ try {
35
+ parsed = JSON.parse(fs.readFileSync(configPath, "utf8"));
36
+ }
37
+ catch (error) {
38
+ const message = error instanceof Error ? error.message : String(error);
39
+ throw new IntegrationsCliError(`Could not parse Claude Desktop connector config at ${configPath}: ${message}`, {
40
+ expected: true,
41
+ });
42
+ }
43
+ if (!parsed || typeof parsed !== "object")
44
+ return {};
45
+ const servers = parsed.mcpServers;
46
+ if (!servers || typeof servers !== "object" || Array.isArray(servers))
47
+ return {};
48
+ return Object.fromEntries(Object.entries(servers).filter((entry) => Boolean(entry[1]) && typeof entry[1] === "object" && !Array.isArray(entry[1])));
49
+ }
50
+ function selectedNames(value) {
51
+ if (value === undefined)
52
+ return undefined;
53
+ const names = value.split(",").map((name) => name.trim()).filter(Boolean);
54
+ if (names.length === 0) {
55
+ throw new IntegrationsCliError("--only needs at least one connector name.", { expected: true });
56
+ }
57
+ return new Set(names);
58
+ }
59
+ function providerSlug(name) {
60
+ const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
61
+ return slug || "desktop-mcp";
62
+ }
63
+ function safeConnectorName(name) {
64
+ return /^[a-z0-9_-]+$/.test(name) ? name : null;
65
+ }
66
+ function secretReference(name) {
67
+ const normalized = name.toUpperCase().replace(/[^A-Z0-9_]/g, "_");
68
+ const resolverSafe = /^[A-Z]/.test(normalized) ? normalized : `SECRET_${normalized.replace(/^_+/, "")}`;
69
+ return `\${secret:${resolverSafe || "CONNECTOR_SECRET"}}`;
70
+ }
71
+ const CREDENTIAL_ARGUMENT_FLAG = /^(?:--)?(?:api[-_]?key|token|password|secret|apikey|auth|bearer|client[-_]?secret)$/i;
72
+ function argumentSecretName(flag) {
73
+ return flag.replace(/^--?/, "").toUpperCase().replace(/[^A-Z0-9]+/g, "_") || "ARGUMENT_SECRET";
74
+ }
75
+ /** Redact explicit credential flags and standalone token-shaped argument values. */
76
+ function redactConnectorArguments(args) {
77
+ let redacted = false;
78
+ const clean = args.map((arg, index) => {
79
+ const [flag, inlineValue] = arg.split("=", 2);
80
+ if (CREDENTIAL_ARGUMENT_FLAG.test(flag)) {
81
+ redacted = true;
82
+ return inlineValue === undefined ? arg : `${flag}=${secretReference(argumentSecretName(flag))}`;
83
+ }
84
+ if (index > 0 && CREDENTIAL_ARGUMENT_FLAG.test(args[index - 1])) {
85
+ redacted = true;
86
+ return secretReference(argumentSecretName(args[index - 1]));
87
+ }
88
+ // Avoid treating package names and URLs as tokens, but err on the side of
89
+ // safety for opaque, mixed-character strings commonly used as credentials.
90
+ if (!arg.startsWith("-") && !/[/@:]/.test(arg) && arg.length >= 16 && /[A-Za-z]/.test(arg) && /\d/.test(arg)) {
91
+ redacted = true;
92
+ return secretReference("ARGUMENT_SECRET");
93
+ }
94
+ return arg;
95
+ });
96
+ return { args: clean, redacted };
97
+ }
98
+ function connectorArguments(entry) {
99
+ return Array.isArray(entry.args) && entry.args.every((arg) => typeof arg === "string") ? entry.args : [];
100
+ }
101
+ function hasRedactedArgumentCredentials(entry) {
102
+ return redactConnectorArguments(connectorArguments(entry)).redacted;
103
+ }
104
+ /** Never persist local desktop credentials in a company-synced connector file. */
105
+ export function stripConnectorSecrets(entry) {
106
+ const stringMap = (value) => {
107
+ if (!value || typeof value !== "object" || Array.isArray(value))
108
+ return undefined;
109
+ const clean = Object.entries(value).filter((pair) => typeof pair[1] === "string")
110
+ .map(([key]) => [key, secretReference(key)]);
111
+ return clean.length > 0 ? Object.fromEntries(clean) : undefined;
112
+ };
113
+ const args = redactConnectorArguments(connectorArguments(entry)).args;
114
+ return {
115
+ type: typeof entry.command === "string" ? "stdio" : entry.type,
116
+ ...(typeof entry.command === "string" ? { command: entry.command } : {}),
117
+ ...(typeof entry.url === "string" ? { url: entry.url } : {}),
118
+ ...(typeof entry.command === "string" ? { args } : {}),
119
+ ...(stringMap(entry.env) ? { env: stringMap(entry.env) } : {}),
120
+ ...(stringMap(entry.headers) ? { headers: stringMap(entry.headers) } : {}),
121
+ };
122
+ }
123
+ function isLocal(entry) {
124
+ return typeof entry.command === "string";
125
+ }
126
+ function isRemote(entry) {
127
+ return typeof entry.url === "string";
128
+ }
129
+ function staticCredential(entry) {
130
+ if (!entry.headers || typeof entry.headers !== "object" || Array.isArray(entry.headers))
131
+ return undefined;
132
+ for (const [key, raw] of Object.entries(entry.headers)) {
133
+ if (typeof raw !== "string" || raw.trim() === "")
134
+ continue;
135
+ if (!/(authorization|api[-_]?key|token|secret)/i.test(key))
136
+ continue;
137
+ if (/^authorization$/i.test(key) && /^Bearer\s+/i.test(raw)) {
138
+ return { value: raw.replace(/^Bearer\s+/i, "").trim() };
139
+ }
140
+ return { value: raw.trim(), authScheme: { placement: "header", header: key } };
141
+ }
142
+ return undefined;
143
+ }
144
+ function oauthRequired(error) {
145
+ return error instanceof IntegrationsCliError &&
146
+ (error.code === OAUTH_REQUIRED_CODE || error.oauthProtected === true);
147
+ }
148
+ function reconnectCommand(name, provider) {
149
+ return `hq integrations reconnect ${name} --provider ${bareProvider(provider)}`;
150
+ }
151
+ function connectorPath(hqRoot, company, name) {
152
+ return path.join(hqRoot, "companies", company, "settings", "connectors", `${name}.json`);
153
+ }
154
+ /**
155
+ * The API's default-company resolver returns a UID, while the synced on-disk
156
+ * layout is deliberately slug-addressed. Read the same active membership set
157
+ * to retain its canonical slug instead of inventing a folder from an ID.
158
+ */
159
+ async function companyFolderSlug(token, companyUid) {
160
+ const response = await vaultApiFetch({ token, path: "/membership/me" });
161
+ if (!response.ok) {
162
+ throw new IntegrationsCliError("Could not resolve the active company's slug for the shared connector folder.", {
163
+ expected: true,
164
+ });
165
+ }
166
+ const body = (await response.json());
167
+ const membership = body.memberships?.find((candidate) => candidate.status === "active" && candidate.companyUid === companyUid);
168
+ const folderSlug = membership?.companyFolderSlug ?? membership?.companySlug;
169
+ if (!folderSlug) {
170
+ throw new IntegrationsCliError("The active company has no slug. Re-run with --company <slug> to choose the shared connector folder.", { expected: true });
171
+ }
172
+ return folderSlug;
173
+ }
174
+ function writeLocalConnector(hqRoot, company, name, entry) {
175
+ const target = connectorPath(hqRoot, company, name);
176
+ fs.mkdirSync(path.dirname(target), { recursive: true });
177
+ fs.writeFileSync(target, `${JSON.stringify(stripConnectorSecrets(entry), null, 2)}\n`, { mode: 0o600 });
178
+ return target;
179
+ }
180
+ async function importRemote(token, companyUid, name, entry, dryRun) {
181
+ const provider = providerSlug(name);
182
+ if (dryRun)
183
+ return { name, status: "imported", reason: "would connect remote MCP endpoint", provider };
184
+ const credential = staticCredential(entry);
185
+ try {
186
+ const result = await installIntegration(token, companyUid, {
187
+ mcpUrl: entry.url,
188
+ provider,
189
+ displayName: name,
190
+ ...(credential ? { authMode: "bearer", bearerToken: credential.value, ...(credential.authScheme ? { authScheme: credential.authScheme } : {}) } : {}),
191
+ });
192
+ const resultProvider = bareProvider(result.connection.provider);
193
+ if (result.installation.status === "needs_credentials" || result.credential?.configured === false) {
194
+ return {
195
+ name,
196
+ status: "needs-signin",
197
+ provider: resultProvider,
198
+ reason: reconnectCommand(name, resultProvider),
199
+ };
200
+ }
201
+ return { name, status: "imported", provider: resultProvider };
202
+ }
203
+ catch (error) {
204
+ if (!oauthRequired(error))
205
+ throw error;
206
+ // Start the server-authoritative OAuth record but deliberately do not open a
207
+ // browser: a Desktop session cannot be transferred headlessly to HQ cloud.
208
+ const pending = await startOAuth(token, companyUid, {
209
+ mcpUrl: entry.url,
210
+ provider,
211
+ displayName: name,
212
+ });
213
+ return {
214
+ name,
215
+ status: "needs-signin",
216
+ provider: bareProvider(pending.provider),
217
+ reason: reconnectCommand(name, pending.provider),
218
+ authorizationUrl: pending.authorizationUrl,
219
+ };
220
+ }
221
+ }
222
+ function renderOutcomes(outcomes) {
223
+ console.log("Connector Outcome Details");
224
+ for (const outcome of outcomes) {
225
+ console.log(`${outcome.name.padEnd(25)} ${outcome.status.padEnd(19)} ${outcome.reason ?? outcome.path ?? ""}`);
226
+ }
227
+ for (const outcome of outcomes.filter((item) => item.status === "shared-for-local")) {
228
+ console.log(chalk.dim(` Shared manifest: ${outcome.path}`));
229
+ console.log(chalk.dim(` Teammate install: ${outcome.installCommand}`));
230
+ }
231
+ if (outcomes.some((item) => item.status === "shared-for-local")) {
232
+ console.log(chalk.yellow(" Local connector credentials were replaced with ${secret:ENV_NAME}. Set them with `hq secrets` before installing."));
233
+ }
234
+ if (outcomes.some((item) => item.redactedArgumentCredentials)) {
235
+ console.log(chalk.yellow(" Local connector argument credentials were replaced with ${secret:FLAG_NAME}. Set them with `hq secrets` before installing."));
236
+ }
237
+ for (const outcome of outcomes.filter((item) => item.status === "needs-signin")) {
238
+ console.log(chalk.yellow(` ${outcome.name} needs sign-in: ${outcome.reason}`));
239
+ if (outcome.authorizationUrl)
240
+ console.log(chalk.yellow(` Open this URL to sign in:\n ${outcome.authorizationUrl}`));
241
+ }
242
+ }
243
+ export function registerImportCommands(integrations) {
244
+ integrations
245
+ .command("import")
246
+ .description("Import Claude Desktop connectors into company Integrations")
247
+ .option("--company <slug>", "Company slug (defaults to your single active company)")
248
+ .option("--dry-run", "Detect and classify connectors without writing or connecting")
249
+ .option("--only <name[,name...]>", "Import only these Claude Desktop connector names")
250
+ .option("--json", "Machine-readable per-connector outcomes")
251
+ .option("--config <path>", "Claude Desktop config path (defaults to this OS's standard location)")
252
+ .action(async (opts) => {
253
+ const configPath = opts.config ?? claudeDesktopConfigPath();
254
+ const connectors = readClaudeDesktopConnectors(configPath);
255
+ const only = selectedNames(opts.only);
256
+ const entries = Object.entries(connectors).filter(([name]) => !only || only.has(name));
257
+ if (entries.length === 0) {
258
+ if (opts.json) {
259
+ printJson([]);
260
+ }
261
+ else {
262
+ console.log(`No Claude Desktop connectors found at ${configPath}`);
263
+ }
264
+ return;
265
+ }
266
+ const token = await ensureCognitoIdToken();
267
+ // getCompanyUid owns the single-membership fallback and multi-company
268
+ // disambiguation, exactly like every other integrations verb.
269
+ const companyUid = await getCompanyUid(token, opts.company);
270
+ const outcomes = [];
271
+ let company;
272
+ let hqRoot;
273
+ const localCompany = async () => company ??= await companyFolderSlug(token, companyUid);
274
+ const localHqRoot = () => hqRoot ??= resolveDefaultHqRoot({ onMissing: "throw" });
275
+ const normalizedNames = new Map();
276
+ for (const [name] of entries) {
277
+ if (!safeConnectorName(name))
278
+ continue;
279
+ const provider = providerSlug(name);
280
+ normalizedNames.set(provider, [...(normalizedNames.get(provider) ?? []), name]);
281
+ }
282
+ for (const [name, entry] of entries) {
283
+ const safeName = safeConnectorName(name);
284
+ if (!safeName) {
285
+ outcomes.push({ name, status: "skipped", reason: "name must use lowercase letters, digits, _ or -" });
286
+ continue;
287
+ }
288
+ const collisions = normalizedNames.get(providerSlug(name)) ?? [];
289
+ if (collisions.length > 1) {
290
+ outcomes.push({
291
+ name,
292
+ status: "skipped",
293
+ reason: `normalized provider name '${providerSlug(name)}' collides with ${collisions.filter((other) => other !== name).join(", ")}`,
294
+ });
295
+ continue;
296
+ }
297
+ if (isLocal(entry)) {
298
+ const resolvedCompany = await localCompany();
299
+ const target = connectorPath(localHqRoot(), resolvedCompany, safeName);
300
+ outcomes.push({
301
+ name,
302
+ status: "shared-for-local",
303
+ ...(opts.dryRun ? { reason: "would write secret-stripped local manifest" } : { path: writeLocalConnector(localHqRoot(), resolvedCompany, safeName, entry) }),
304
+ installCommand: `hq integrations install-local ${safeName} --company ${resolvedCompany}`,
305
+ ...(hasRedactedArgumentCredentials(entry) ? { redactedArgumentCredentials: true } : {}),
306
+ });
307
+ // Keep dry-run output useful without leaking a filesystem write target.
308
+ if (opts.dryRun)
309
+ outcomes[outcomes.length - 1].path = target;
310
+ continue;
311
+ }
312
+ if (isRemote(entry) && typeof entry.url === "string") {
313
+ outcomes.push(await importRemote(token, companyUid, name, entry, Boolean(opts.dryRun)));
314
+ continue;
315
+ }
316
+ outcomes.push({ name, status: "skipped", reason: "unsupported MCP entry (expected command for local stdio or url for remote MCP)" });
317
+ }
318
+ if (opts.json)
319
+ printJson(outcomes);
320
+ else
321
+ renderOutcomes(outcomes);
322
+ });
323
+ integrations
324
+ .command("install-local <name>")
325
+ .description("Register a company-shared local connector in this machine's Claude/Codex MCP config")
326
+ .option("--company <slug>", "Company slug (defaults to your single active company)")
327
+ .option("--json", "Machine-readable registration result")
328
+ .action(async (name, opts) => {
329
+ const safeName = safeConnectorName(name);
330
+ if (!safeName) {
331
+ throw new IntegrationsCliError("Connector name must use lowercase letters, digits, _ or -.", { expected: true });
332
+ }
333
+ const token = await ensureCognitoToken();
334
+ const companyUid = await getCompanyUid(token, opts.company);
335
+ const company = await companyFolderSlug(token, companyUid);
336
+ const hqRoot = resolveDefaultHqRoot({ onMissing: "throw" });
337
+ const manifestPath = connectorPath(hqRoot, company, safeName);
338
+ if (!fs.existsSync(manifestPath)) {
339
+ throw new IntegrationsCliError(`Shared connector not found: ${manifestPath}`, { expected: true });
340
+ }
341
+ let manifest;
342
+ try {
343
+ manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
344
+ }
345
+ catch (error) {
346
+ const message = error instanceof Error ? error.message : String(error);
347
+ throw new IntegrationsCliError(`Could not parse shared connector ${manifestPath}: ${message}`, { expected: true });
348
+ }
349
+ const secretNames = [...Object.values({ ...(manifest.env ?? {}), ...(manifest.headers ?? {}) }), ...(manifest.args ?? [])]
350
+ .flatMap((value) => [...value.matchAll(/\$\{secret:([A-Z][A-Z0-9_]*(?:\/[A-Z][A-Z0-9_]*)*)\}/g)].map((match) => match[1]));
351
+ const secrets = await loadRevealedSecrets(token, companyUid, secretNames);
352
+ const result = registerServer({
353
+ name: safeName,
354
+ manifest,
355
+ pack: `company-${company}-connectors`,
356
+ resolveSecret: (secret) => secrets.get(secret) ?? null,
357
+ });
358
+ if (opts.json)
359
+ printJson(result);
360
+ else
361
+ console.log(`Registered ${safeName} in local MCP config (Claude${"skipped" in result.codex ? "; Codex not installed" : " and Codex"}).`);
362
+ });
363
+ }
364
+ //# sourceMappingURL=integrations-import.js.map
@@ -14,6 +14,7 @@
14
14
  * hq integrations discover <docsUrl> Find a server from its docs page.
15
15
  * hq integrations connect <app> Connect it (no-auth, key, OAuth).
16
16
  * hq integrations reconnect [app] Re-authenticate a broken app.
17
+ * hq integrations import Import Claude Desktop connectors.
17
18
  *
18
19
  * Use:
19
20
  * hq integrations list --company indigo Connected apps for a company.
@@ -14,6 +14,7 @@
14
14
  * hq integrations discover <docsUrl> Find a server from its docs page.
15
15
  * hq integrations connect <app> Connect it (no-auth, key, OAuth).
16
16
  * hq integrations reconnect [app] Re-authenticate a broken app.
17
+ * hq integrations import Import Claude Desktop connectors.
17
18
  *
18
19
  * Use:
19
20
  * hq integrations list --company indigo Connected apps for a company.
@@ -51,8 +52,40 @@ import { ensureCognitoIdToken } from "../utils/cognito-session.js";
51
52
  import { getCompanyUid, vaultApiFetch } from "../utils/vault-api.js";
52
53
  import { IntegrationsCliError, bareProvider, callGateway, fetchConnections, isClientError, printJson, raiseIfUnauthorized, raiseIfUpstreamUnavailable, gatewayResultIsError, selectConnection, toolPrefixForProvider, unwrapGatewayResult, queuedOutcome, } from "./integrations-core.js";
53
54
  import { registerConnectCommands } from "./integrations-connect.js";
55
+ import { registerImportCommands } from "./integrations-import.js";
54
56
  import { registerManageCommands } from "./integrations-manage.js";
55
57
  export { IntegrationsCliError, callGateway, fetchConnections, queuedOutcome, selectConnection, toolPrefixForProvider, unwrapGatewayResult, } from "./integrations-core.js";
58
+ /**
59
+ * The gateway relays MCP tool schemas verbatim. Keep the default listing
60
+ * compact, but make required inputs visible before a caller has to discover
61
+ * them through a validation error.
62
+ */
63
+ function requiredInputHint(inputSchema) {
64
+ if (!inputSchema || typeof inputSchema !== "object")
65
+ return null;
66
+ const required = inputSchema.required;
67
+ if (!Array.isArray(required))
68
+ return null;
69
+ const names = required.filter((name) => typeof name === "string");
70
+ return names.length > 0 ? `requires: ${names.join(", ")}` : null;
71
+ }
72
+ /** The user-facing slug stays first; the factory's display name is additive. */
73
+ function connectionLabel(connection) {
74
+ const slug = bareProvider(connection.provider);
75
+ const displayName = connection.installation?.displayName?.trim();
76
+ return displayName && displayName.toLowerCase() !== slug.toLowerCase()
77
+ ? `${slug} (${displayName})`
78
+ : slug;
79
+ }
80
+ function printToolSchema(tool) {
81
+ if (tool.inputSchema === undefined) {
82
+ console.log(chalk.dim(" Input schema was not reported by this app."));
83
+ return;
84
+ }
85
+ const schema = JSON.stringify(tool.inputSchema, null, 2);
86
+ console.log(chalk.dim(" Input schema:"));
87
+ console.log(chalk.dim(schema.split("\n").map((line) => ` ${line}`).join("\n")));
88
+ }
56
89
  export function registerIntegrationsCommand(program) {
57
90
  const integrations = program
58
91
  .command("integrations")
@@ -75,7 +108,6 @@ export function registerIntegrationsCommand(program) {
75
108
  return;
76
109
  }
77
110
  for (const c of connections) {
78
- const name = c.installation?.displayName ?? bareProvider(c.provider);
79
111
  const flags = [
80
112
  c.status,
81
113
  c.writePolicy ? `writes: ${c.writePolicy}` : null,
@@ -83,16 +115,18 @@ export function registerIntegrationsCommand(program) {
83
115
  ]
84
116
  .filter(Boolean)
85
117
  .join(" · ");
86
- console.log(`${chalk.bold(name)} (${bareProvider(c.provider)}) ${chalk.dim(flags)}`);
118
+ console.log(`${chalk.bold(connectionLabel(c))} ${chalk.dim(flags)}`);
87
119
  console.log(chalk.dim(` connection: ${c.id}`));
88
120
  }
89
121
  });
122
+ registerImportCommands(integrations);
90
123
  integrations
91
124
  .command("tools")
92
125
  .description("List what a connected app can do")
93
126
  .option("--provider <slug>", "Connected app (e.g. linear)")
94
127
  .option("--connection <id>", "Connection id (acct_…)")
95
128
  .option("--company <slug>", "Company slug, e.g. indigo")
129
+ .option("--describe <tool>", "Print the full input schema for one tool")
96
130
  .option("--json", "Machine-readable output")
97
131
  .action(async (opts) => {
98
132
  const token = await ensureCognitoIdToken();
@@ -105,20 +139,41 @@ export function registerIntegrationsCommand(program) {
105
139
  arguments: { companyUid, connectionId: connection.id },
106
140
  });
107
141
  const payload = unwrapGatewayResult(message.result);
108
- if (opts.json) {
109
- printJson(payload);
110
- return;
111
- }
112
142
  const tools = payload?.tools ?? [];
113
143
  if (tools.length === 0) {
144
+ if (opts.json) {
145
+ printJson(payload);
146
+ return;
147
+ }
114
148
  console.log("The app reported no tools.");
115
149
  return;
116
150
  }
151
+ if (opts.describe) {
152
+ const tool = tools.find((candidate) => candidate.name === opts.describe);
153
+ if (!tool) {
154
+ throw new IntegrationsCliError(`No tool named '${opts.describe}' on ${connectionLabel(connection)}.`, { expected: true });
155
+ }
156
+ if (opts.json) {
157
+ printJson(tool);
158
+ return;
159
+ }
160
+ console.log(`${chalk.bold(tool.name)} ${chalk.dim(`on ${connectionLabel(connection)}`)}`);
161
+ if (tool.description)
162
+ console.log(chalk.dim(` ${tool.description}`));
163
+ printToolSchema(tool);
164
+ return;
165
+ }
166
+ if (opts.json) {
167
+ printJson(payload);
168
+ return;
169
+ }
170
+ console.log(chalk.dim(`App: ${connectionLabel(connection)}`));
117
171
  for (const tool of tools) {
118
172
  const label = tool.title && tool.title !== tool.name ? ` ${chalk.dim(tool.title)}` : "";
119
- console.log(`${chalk.bold(tool.name)}${label}`);
173
+ const required = requiredInputHint(tool.inputSchema);
174
+ console.log(`${chalk.bold(tool.name)}${label}${required ? ` ${chalk.dim(required)}` : ""}`);
120
175
  }
121
- console.log(chalk.dim(`\n${tools.length} tools. Call one with: hq integrations call <tool> --provider ${bareProvider(connection.provider)} --args '<json>'`));
176
+ console.log(chalk.dim(`\n${tools.length} tools. Describe inputs: hq integrations tools --provider ${bareProvider(connection.provider)} --describe <tool>\nCall one with: hq integrations call <tool> --provider ${bareProvider(connection.provider)} --args '<json>'`));
122
177
  });
123
178
  integrations
124
179
  .command("call <tool>")
@@ -471,7 +471,7 @@ export declare const claudeConfigFormat: ConfigFormat<Record<string, unknown>>;
471
471
  /**
472
472
  * Build the Claude server definition emitted into `mcpServers.<name>` from a
473
473
  * manifest: pass the transport fields through, resolve `${secret:}` in every
474
- * header/env value (recording the plaintexts in `secretSink` for redaction), and
474
+ * argument/header/env value (recording the plaintexts in `secretSink` for redaction), and
475
475
  * stamp `_hqPack` provenance. The output object's key order is deterministic so a
476
476
  * re-run produces a byte-identical def (idempotency depends on stable serialize).
477
477
  */
@@ -673,7 +673,7 @@ export declare const codexConfigFormat: ConfigFormat<CodexTomlDoc>;
673
673
  export declare function appendCodexTable(originalText: string, name: string, def: TomlTable): string;
674
674
  /**
675
675
  * Build the Codex server table emitted as `[mcp_servers.<name>]`: pass the
676
- * transport fields through, resolve `${secret:}` in every header/env value
676
+ * transport fields through, resolve `${secret:}` in every argument/header/env value
677
677
  * (recording plaintexts in `secretSink` for redaction), stamp `_hqPack` provenance,
678
678
  * and write the per-tool `approval_mode` from the manifest where present (the Codex-
679
679
  * specific field — `[mcp_servers.<name>.tools.<t>]` with `approval_mode = "…"`).
@@ -934,7 +934,7 @@ export const claudeConfigFormat = {
934
934
  /**
935
935
  * Build the Claude server definition emitted into `mcpServers.<name>` from a
936
936
  * manifest: pass the transport fields through, resolve `${secret:}` in every
937
- * header/env value (recording the plaintexts in `secretSink` for redaction), and
937
+ * argument/header/env value (recording the plaintexts in `secretSink` for redaction), and
938
938
  * stamp `_hqPack` provenance. The output object's key order is deterministic so a
939
939
  * re-run produces a byte-identical def (idempotency depends on stable serialize).
940
940
  */
@@ -944,8 +944,9 @@ export function buildClaudeServerDef(manifest, pack, resolve, secretSink) {
944
944
  def.url = manifest.url;
945
945
  if (manifest.command !== undefined)
946
946
  def.command = manifest.command;
947
- if (manifest.args !== undefined)
948
- def.args = manifest.args;
947
+ if (manifest.args !== undefined) {
948
+ def.args = manifest.args.map((arg) => resolveSecretRefs(arg, resolve, secretSink));
949
+ }
949
950
  if (manifest.headers !== undefined) {
950
951
  def.headers = resolveStringMap(manifest.headers, resolve, secretSink);
951
952
  }
@@ -1371,7 +1372,7 @@ export function appendCodexTable(originalText, name, def) {
1371
1372
  }
1372
1373
  /**
1373
1374
  * Build the Codex server table emitted as `[mcp_servers.<name>]`: pass the
1374
- * transport fields through, resolve `${secret:}` in every header/env value
1375
+ * transport fields through, resolve `${secret:}` in every argument/header/env value
1375
1376
  * (recording plaintexts in `secretSink` for redaction), stamp `_hqPack` provenance,
1376
1377
  * and write the per-tool `approval_mode` from the manifest where present (the Codex-
1377
1378
  * specific field — `[mcp_servers.<name>.tools.<t>]` with `approval_mode = "…"`).
@@ -1384,8 +1385,9 @@ export function buildCodexServerDef(manifest, pack, resolve, secretSink) {
1384
1385
  def.url = manifest.url;
1385
1386
  if (manifest.command !== undefined)
1386
1387
  def.command = manifest.command;
1387
- if (manifest.args !== undefined)
1388
- def.args = manifest.args;
1388
+ if (manifest.args !== undefined) {
1389
+ def.args = manifest.args.map((arg) => resolveSecretRefs(arg, resolve, secretSink));
1390
+ }
1389
1391
  if (manifest.headers !== undefined) {
1390
1392
  def.headers = resolveStringMap(manifest.headers, resolve, secretSink);
1391
1393
  }
@@ -34,6 +34,18 @@ export interface VaultApiOptions {
34
34
  * network call; this stays the backstop for a new call site that forgets to.
35
35
  */
36
36
  export declare function rewritePathForApiKey(path: string): string | null;
37
+ /**
38
+ * Client-family identifier sent on every authenticated vault-API request.
39
+ *
40
+ * hq-pro reads this exact header into its closed `RosterClientFamily` enum
41
+ * (`roster-client-family.ts`) and tags each agents-request Sentry event with it
42
+ * (`handler.ts` setSentryClientFamily). Sending it means an event this CLI
43
+ * raises reads `hq_client=hq_cli` and names its origin instead of `absent` — the
44
+ * missing attribution behind Sentry hq-pro 7617401436. It is a fixed literal,
45
+ * never caller-supplied text, used only for telemetry/attribution: no
46
+ * authorization or request signing depends on it.
47
+ */
48
+ export declare const HQ_CLIENT_NAME = "@indigoai-us/hq-cli";
37
49
  export declare function vaultApiFetch(opts: VaultApiOptions): Promise<Response>;
38
50
  /**
39
51
  * Public (NONE-auth) GET against the vault API — no bearer token. The
@@ -124,6 +124,18 @@ export function rewritePathForApiKey(path) {
124
124
  }
125
125
  return null;
126
126
  }
127
+ /**
128
+ * Client-family identifier sent on every authenticated vault-API request.
129
+ *
130
+ * hq-pro reads this exact header into its closed `RosterClientFamily` enum
131
+ * (`roster-client-family.ts`) and tags each agents-request Sentry event with it
132
+ * (`handler.ts` setSentryClientFamily). Sending it means an event this CLI
133
+ * raises reads `hq_client=hq_cli` and names its origin instead of `absent` — the
134
+ * missing attribution behind Sentry hq-pro 7617401436. It is a fixed literal,
135
+ * never caller-supplied text, used only for telemetry/attribution: no
136
+ * authorization or request signing depends on it.
137
+ */
138
+ export const HQ_CLIENT_NAME = "@indigoai-us/hq-cli";
127
139
  export async function vaultApiFetch(opts) {
128
140
  let path = opts.path;
129
141
  if (opts.token.startsWith("hqk_")) {
@@ -156,6 +168,7 @@ export async function vaultApiFetch(opts) {
156
168
  headers: {
157
169
  Authorization: `Bearer ${opts.token}`,
158
170
  'Content-Type': 'application/json',
171
+ 'x-hq-client-name': HQ_CLIENT_NAME,
159
172
  },
160
173
  body: opts.body ? JSON.stringify(opts.body) : undefined,
161
174
  signal: opts.signal,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.103.19",
3
+ "version": "5.103.21",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -30,7 +30,7 @@
30
30
  "dependencies": {
31
31
  "@aws-sdk/client-iot-data-plane": "^3.1096.0",
32
32
  "@aws-sdk/client-s3": "^3.1049.0",
33
- "@indigoai-us/hq-cloud": "~6.15.30",
33
+ "@indigoai-us/hq-cloud": "~6.15.37",
34
34
  "@indigoai-us/hq-onboarding": "^0.1.0",
35
35
  "@sentry/node": "^10.49.0",
36
36
  "@tobilu/qmd": "2.5.3",