@indigoai-us/hq-cli 5.103.20 → 5.103.22

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.
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * `hq integrations show | policy | grants | grant | ungrant | access | share |
3
- * unshare | audit | pending | disconnect`.
3
+ * unshare | audit | pending | disconnect | purge`.
4
4
  *
5
5
  * The govern-and-remove half of the lifecycle. Two different permission
6
6
  * surfaces live here and are easy to confuse, so they get separate verbs:
@@ -16,8 +16,8 @@ import readline from "node:readline";
16
16
  import chalk from "chalk";
17
17
  import { ensureCognitoIdToken } from "../utils/cognito-session.js";
18
18
  import { getCompanyUid } from "../utils/vault-api.js";
19
- import { IntegrationsCliError, bareProvider, fetchAdminSurface, printJson, resolveConnection, selectConnection, } from "./integrations-core.js";
20
- import { fetchPendingApprovals, getConnectionAccess, mutateConnectionAccess, uninstallIntegration, updateGovernance, } from "./integrations-api.js";
19
+ import { IntegrationsCliError, bareProvider, fetchConnections, fetchAdminSurface, printJson, revokedConnectionDetails, resolveConnection, selectConnection, } from "./integrations-core.js";
20
+ import { fetchPendingApprovals, getConnectionAccess, mutateConnectionAccess, purgeConnection, setReadSafe, uninstallIntegration, updateGovernance, } from "./integrations-api.js";
21
21
  const WRITE_POLICIES = ["auto-allow", "confirm", "deny"];
22
22
  const PERMISSIONS = ["read", "write", "admin"];
23
23
  /** Plain-English gloss for each write policy, used in every policy readout. */
@@ -97,6 +97,67 @@ function confirm(message) {
97
97
  function connectionLabel(connection) {
98
98
  return connection.installation?.displayName ?? bareProvider(connection.provider);
99
99
  }
100
+ /**
101
+ * The category of match must mirror `selectConnection`: an exact provider id
102
+ * takes precedence over a display name, which takes precedence over its human
103
+ * slug. Unlike ordinary verbs, purge deliberately operates on historical
104
+ * revoked rows, so it must not inherit selectConnection's active-first policy.
105
+ */
106
+ function purgeSelectorMatches(connections, selector) {
107
+ if (selector.connection) {
108
+ return connections.filter((connection) => connection.id === selector.connection);
109
+ }
110
+ if (selector.provider) {
111
+ const want = selector.provider.trim().toLowerCase();
112
+ const wantHumanSlug = humanSlug(selector.provider);
113
+ const providerMatches = connections.filter((connection) => bareProvider(connection.provider).toLowerCase() === want ||
114
+ connection.provider.toLowerCase() === want);
115
+ if (providerMatches.length > 0)
116
+ return providerMatches;
117
+ const displayNameMatches = connections.filter((connection) => connection.installation?.displayName?.trim().toLowerCase() === want);
118
+ if (displayNameMatches.length > 0)
119
+ return displayNameMatches;
120
+ if (wantHumanSlug) {
121
+ return connections.filter((connection) => {
122
+ const displayName = connection.installation?.displayName;
123
+ return displayName !== undefined && humanSlug(displayName) === wantHumanSlug;
124
+ });
125
+ }
126
+ return [];
127
+ }
128
+ return connections;
129
+ }
130
+ function humanSlug(value) {
131
+ return value
132
+ .trim()
133
+ .toLowerCase()
134
+ .replace(/[^a-z0-9]+/g, "-")
135
+ .replace(/^-+|-+$/g, "");
136
+ }
137
+ function purgeSelector(app, opts) {
138
+ if (app && !opts.provider && !opts.connection) {
139
+ return app.startsWith("acct_") ? { connection: app } : { provider: app };
140
+ }
141
+ return opts;
142
+ }
143
+ async function resolveRevokedPurgeConnection(token, companyUid, app, opts) {
144
+ const selector = purgeSelector(app, opts);
145
+ const connections = await fetchConnections(token, companyUid);
146
+ const matches = purgeSelectorMatches(connections, selector);
147
+ const revoked = matches.filter((connection) => connection.status === "revoked");
148
+ if (revoked.length === 1)
149
+ return revoked[0];
150
+ if (revoked.length > 1) {
151
+ const selected = selector.connection ?? selector.provider ?? "the supplied selector";
152
+ throw new IntegrationsCliError(`Multiple revoked connections match '${selected}'. Use --connection <acct_id> to choose one:\n` +
153
+ revoked.map((connection) => ` --connection ${connection.id} (${connectionLabel(connection)})`).join("\n"), { expected: true });
154
+ }
155
+ if (matches.length > 0) {
156
+ throw new IntegrationsCliError(`${connectionLabel(matches[0])} is still connected. Disconnect it first, then purge it.`, { expected: true });
157
+ }
158
+ // Keep the existing command group's not-found wording and error taxonomy.
159
+ return selectConnection(connections, selector);
160
+ }
100
161
  /**
101
162
  * Governance writes are owner-only on hq-pro AND the allowlist PATCH is a
102
163
  * whole-list REPLACE. A non-owner reads an identity-redacted grant list, so
@@ -144,7 +205,9 @@ export function registerManageCommands(integrations) {
144
205
  ? selectConnection(surface.connections, app.startsWith("acct_") ? { connection: app } : { provider: app })
145
206
  : selectConnection(surface.connections, opts);
146
207
  if (opts.json) {
147
- printJson(connection);
208
+ printJson(connection.status === "revoked"
209
+ ? { ...connection, ...revokedConnectionDetails(connection, opts.company) }
210
+ : connection);
148
211
  return;
149
212
  }
150
213
  const install = connection.installation;
@@ -153,6 +216,11 @@ export function registerManageCommands(integrations) {
153
216
  if (install?.id)
154
217
  console.log(chalk.dim(` installation: ${install.id}`));
155
218
  console.log(chalk.dim(` status: ${connection.status}`));
219
+ if (connection.status === "revoked") {
220
+ const details = revokedConnectionDetails(connection, opts.company);
221
+ console.log(chalk.yellow(` Revoked — ${details.reason}`));
222
+ console.log(chalk.yellow(` Re-add: ${details.fixPath}`));
223
+ }
156
224
  if (install?.domain)
157
225
  console.log(chalk.dim(` domain: ${install.domain}`));
158
226
  if (install?.surface?.url)
@@ -218,6 +286,47 @@ export function registerManageCommands(integrations) {
218
286
  }
219
287
  console.log(chalk.green(`${connectionLabel(connection)}: ${writePolicy} — ${POLICY_BLURB[writePolicy]}`));
220
288
  });
289
+ integrations
290
+ .command("read-safe <tool>")
291
+ .description("Mark a read-only tool as safe to run without approval (owner only)")
292
+ .option("--company <slug>", "Company slug, e.g. indigo")
293
+ .option("--provider <slug>", "Connected app (e.g. linear)")
294
+ .option("--connection <id>", "Connection id (acct_…)")
295
+ .option("--unset", "Remove the read-safe override")
296
+ .option("--off", "Alias for --unset")
297
+ .option("--json", "Machine-readable output")
298
+ .action(async (tool, opts) => {
299
+ const toolName = tool.trim();
300
+ if (!toolName) {
301
+ throw new IntegrationsCliError("Tool name cannot be empty.", { expected: true });
302
+ }
303
+ const token = await ensureCognitoIdToken();
304
+ const companyUid = await getCompanyUid(token, opts.company);
305
+ const connection = await resolveConnection(token, companyUid, undefined, opts);
306
+ const readSafe = !(opts.unset || opts.off);
307
+ try {
308
+ await setReadSafe(token, companyUid, connection.id, toolName, readSafe);
309
+ }
310
+ catch (error) {
311
+ if (error instanceof IntegrationsCliError) {
312
+ if (error.status === 403) {
313
+ throw new IntegrationsCliError("This command is owner only. Ask a company owner to manage read-safe tools.", { expected: true });
314
+ }
315
+ if (readSafe && (error.status === 409 || error.status === 422)) {
316
+ throw new IntegrationsCliError(`${toolName} is a write tool and can't be marked read-safe (its changes always need approval).`, { expected: true });
317
+ }
318
+ }
319
+ throw error;
320
+ }
321
+ const outcome = { connectionId: connection.id, toolName, readSafe };
322
+ if (opts.json) {
323
+ printJson(outcome);
324
+ return;
325
+ }
326
+ console.log(chalk.green(readSafe
327
+ ? `${toolName} on ${connectionLabel(connection)} is now read-safe and skips approval.`
328
+ : `${toolName} on ${connectionLabel(connection)} is no longer read-safe; its override was removed.`));
329
+ });
221
330
  integrations
222
331
  .command("grants [app]")
223
332
  .description("Show who is authorized to run an app's change-making tools")
@@ -518,6 +627,52 @@ export function registerManageCommands(integrations) {
518
627
  }
519
628
  console.log(chalk.green(`Disconnected ${connectionLabel(connection)}.`));
520
629
  });
630
+ integrations
631
+ .command("purge [app]")
632
+ .description("Permanently remove a revoked connection from the list (owner only)")
633
+ .option("--company <slug>", "Company slug, e.g. indigo")
634
+ .option("--provider <slug>", "Connected app (e.g. linear)")
635
+ .option("--connection <id>", "Connection id (acct_…)")
636
+ .option("--yes", "Skip the permanent-removal confirmation prompt")
637
+ .option("--json", "Machine-readable output")
638
+ .action(async (app, opts) => {
639
+ const token = await ensureCognitoIdToken();
640
+ const companyUid = await getCompanyUid(token, opts.company);
641
+ const connection = await resolveRevokedPurgeConnection(token, companyUid, app, opts);
642
+ const label = connectionLabel(connection);
643
+ if (!opts.yes) {
644
+ // Warning + prompt go to stderr: with --json, stdout must stay parseable.
645
+ console.error(`Purging ${chalk.bold(label)} permanently removes its revoked connection record. It cannot be restored.`);
646
+ const ok = await confirm(`Permanently purge ${label}?`);
647
+ if (!ok) {
648
+ // A non-TTY run lands here too: refusing is the only safe default
649
+ // when nobody can confirm a permanent deletion.
650
+ throw new IntegrationsCliError("Not purged. Re-run with --yes if you are sure.", { expected: true });
651
+ }
652
+ }
653
+ try {
654
+ await purgeConnection(token, companyUid, connection.id);
655
+ }
656
+ catch (error) {
657
+ if (error instanceof IntegrationsCliError && error.status === 409) {
658
+ throw new IntegrationsCliError(`${label} is still connected. Disconnect it first, then purge it.`, { expected: true, status: 409, code: error.code });
659
+ }
660
+ if (error instanceof IntegrationsCliError && error.status === 403) {
661
+ throw new IntegrationsCliError("Purging a connection is owner only. Ask a company owner to run this.", { expected: true, status: 403, code: error.code });
662
+ }
663
+ throw error;
664
+ }
665
+ const outcome = {
666
+ connectionId: connection.id,
667
+ provider: bareProvider(connection.provider),
668
+ status: "purged",
669
+ };
670
+ if (opts.json) {
671
+ printJson(outcome);
672
+ return;
673
+ }
674
+ console.log(chalk.green(`Purged ${label}. It will no longer appear in \`hq integrations list\`.`));
675
+ });
521
676
  }
522
677
  /** Order-insensitive identity of an allowlist, for change detection. */
523
678
  export function allowlistFingerprint(grants) {
@@ -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.
@@ -25,10 +26,12 @@
25
26
  *
26
27
  * Govern and remove:
27
28
  * hq integrations policy [app] --set <m> Approval setting for changes.
29
+ * hq integrations read-safe <tool> Mark/unmark a trusted read-only tool.
28
30
  * hq integrations grants|grant|ungrant Per-tool authorization to run change-making tools.
29
31
  * hq integrations access|share|unshare Who may use the app.
30
32
  * hq integrations audit Recent activity.
31
33
  * hq integrations disconnect [app] Remove it and its credentials.
34
+ * hq integrations purge [app] Permanently remove a revoked connection row.
32
35
  *
33
36
  * Governance: reads flow freely; calls that can change the app are subject to
34
37
  * the connection's write policy (default: a person approves first). A queued
@@ -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.
@@ -25,10 +26,12 @@
25
26
  *
26
27
  * Govern and remove:
27
28
  * hq integrations policy [app] --set <m> Approval setting for changes.
29
+ * hq integrations read-safe <tool> Mark/unmark a trusted read-only tool.
28
30
  * hq integrations grants|grant|ungrant Per-tool authorization to run change-making tools.
29
31
  * hq integrations access|share|unshare Who may use the app.
30
32
  * hq integrations audit Recent activity.
31
33
  * hq integrations disconnect [app] Remove it and its credentials.
34
+ * hq integrations purge [app] Permanently remove a revoked connection row.
32
35
  *
33
36
  * Governance: reads flow freely; calls that can change the app are subject to
34
37
  * the connection's write policy (default: a person approves first). A queued
@@ -49,10 +52,113 @@ import { randomUUID } from "node:crypto";
49
52
  import chalk from "chalk";
50
53
  import { ensureCognitoIdToken } from "../utils/cognito-session.js";
51
54
  import { getCompanyUid, vaultApiFetch } from "../utils/vault-api.js";
52
- import { IntegrationsCliError, bareProvider, callGateway, fetchConnections, isClientError, printJson, raiseIfUnauthorized, raiseIfUpstreamUnavailable, gatewayResultIsError, selectConnection, toolPrefixForProvider, unwrapGatewayResult, queuedOutcome, } from "./integrations-core.js";
55
+ import { IntegrationsCliError, bareProvider, callGateway, fetchConnections, isClientError, printJson, raiseIfUnauthorized, raiseIfUpstreamUnavailable, gatewayResultIsError, selectConnection, revokedConnectionDetails, toolPrefixForProvider, unwrapGatewayResult, queuedOutcome, } from "./integrations-core.js";
53
56
  import { registerConnectCommands } from "./integrations-connect.js";
57
+ import { registerImportCommands } from "./integrations-import.js";
54
58
  import { registerManageCommands } from "./integrations-manage.js";
55
59
  export { IntegrationsCliError, callGateway, fetchConnections, queuedOutcome, selectConnection, toolPrefixForProvider, unwrapGatewayResult, } from "./integrations-core.js";
60
+ /**
61
+ * Gateway metadata is additive and may be absent when talking to an older
62
+ * gateway. Parse it defensively so an unknown or malformed value never breaks
63
+ * tool discovery.
64
+ */
65
+ function effectiveToolMode(tool) {
66
+ const raw = tool._meta?.["hq/effective-mode"];
67
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
68
+ return null;
69
+ const mode = raw.mode;
70
+ const approvalRequired = raw.approvalRequired;
71
+ if ((mode !== "read" && mode !== "write") || typeof approvalRequired !== "boolean") {
72
+ return null;
73
+ }
74
+ const denied = raw.denied;
75
+ const policy = raw.policy;
76
+ return {
77
+ mode,
78
+ approvalRequired,
79
+ ...(denied === true ? { denied: true } : {}),
80
+ ...(policy === "deny" ? { policy: "deny" } : {}),
81
+ };
82
+ }
83
+ function toolBadge(tool) {
84
+ const effectiveMode = effectiveToolMode(tool);
85
+ if (!effectiveMode)
86
+ return null;
87
+ if (effectiveMode.mode === "read")
88
+ return "read";
89
+ if (effectiveMode.denied)
90
+ return "denied";
91
+ return effectiveMode.approvalRequired ? "approval-required" : "write";
92
+ }
93
+ function formatToolBadge(tool) {
94
+ const badge = toolBadge(tool);
95
+ if (!badge)
96
+ return "";
97
+ const label = `[${badge}]`;
98
+ switch (badge) {
99
+ case "read":
100
+ return chalk.green(label);
101
+ case "approval-required":
102
+ return chalk.yellow(label);
103
+ case "denied":
104
+ return chalk.red(label);
105
+ case "write":
106
+ return chalk.blue(label);
107
+ }
108
+ }
109
+ /** Add gateway policy metadata where scripts can consume it directly. */
110
+ function toolJsonView(tool) {
111
+ const effectiveMode = effectiveToolMode(tool);
112
+ return effectiveMode ? { ...tool, effectiveMode } : tool;
113
+ }
114
+ /**
115
+ * The gateway relays MCP tool schemas verbatim. Keep the default listing
116
+ * compact, but make required inputs visible before a caller has to discover
117
+ * them through a validation error.
118
+ */
119
+ function requiredInputHint(inputSchema) {
120
+ if (!inputSchema || typeof inputSchema !== "object")
121
+ return null;
122
+ const required = inputSchema.required;
123
+ if (!Array.isArray(required))
124
+ return null;
125
+ const names = required.filter((name) => typeof name === "string");
126
+ return names.length > 0 ? `requires: ${names.join(", ")}` : null;
127
+ }
128
+ /** The user-facing slug stays first; the factory's display name is additive. */
129
+ function connectionLabel(connection) {
130
+ const slug = bareProvider(connection.provider);
131
+ const displayName = connection.installation?.displayName?.trim();
132
+ return displayName && displayName.toLowerCase() !== slug.toLowerCase()
133
+ ? `${slug} (${displayName})`
134
+ : slug;
135
+ }
136
+ function printToolSchema(tool) {
137
+ if (tool.inputSchema === undefined) {
138
+ console.log(chalk.dim(" Input schema was not reported by this app."));
139
+ return;
140
+ }
141
+ const schema = JSON.stringify(tool.inputSchema, null, 2);
142
+ console.log(chalk.dim(" Input schema:"));
143
+ console.log(chalk.dim(schema.split("\n").map((line) => ` ${line}`).join("\n")));
144
+ }
145
+ function reportRevokedConnection(connection, opts) {
146
+ if (connection.status !== "revoked")
147
+ return false;
148
+ const details = revokedConnectionDetails(connection, opts.company);
149
+ if (opts.json) {
150
+ printJson(details);
151
+ }
152
+ else {
153
+ console.log(chalk.yellow(`${connectionLabel(connection)}: status=revoked`));
154
+ console.log(chalk.yellow(` ${details.reason}`));
155
+ console.log(` Re-add: ${details.fixPath}`);
156
+ }
157
+ if (opts.fail) {
158
+ throw new IntegrationsCliError(`status=revoked. ${details.reason} Re-add with: ${details.fixPath}`, { expected: true });
159
+ }
160
+ return true;
161
+ }
56
162
  export function registerIntegrationsCommand(program) {
57
163
  const integrations = program
58
164
  .command("integrations")
@@ -75,7 +181,6 @@ export function registerIntegrationsCommand(program) {
75
181
  return;
76
182
  }
77
183
  for (const c of connections) {
78
- const name = c.installation?.displayName ?? bareProvider(c.provider);
79
184
  const flags = [
80
185
  c.status,
81
186
  c.writePolicy ? `writes: ${c.writePolicy}` : null,
@@ -83,21 +188,25 @@ export function registerIntegrationsCommand(program) {
83
188
  ]
84
189
  .filter(Boolean)
85
190
  .join(" · ");
86
- console.log(`${chalk.bold(name)} (${bareProvider(c.provider)}) ${chalk.dim(flags)}`);
191
+ console.log(`${chalk.bold(connectionLabel(c))} ${chalk.dim(flags)}`);
87
192
  console.log(chalk.dim(` connection: ${c.id}`));
88
193
  }
89
194
  });
195
+ registerImportCommands(integrations);
90
196
  integrations
91
197
  .command("tools")
92
198
  .description("List what a connected app can do")
93
199
  .option("--provider <slug>", "Connected app (e.g. linear)")
94
200
  .option("--connection <id>", "Connection id (acct_…)")
95
201
  .option("--company <slug>", "Company slug, e.g. indigo")
202
+ .option("--describe <tool>", "Print the full input schema for one tool")
96
203
  .option("--json", "Machine-readable output")
97
204
  .action(async (opts) => {
98
205
  const token = await ensureCognitoIdToken();
99
206
  const companyUid = await getCompanyUid(token, opts.company);
100
207
  const connection = selectConnection(await fetchConnections(token, companyUid), opts);
208
+ if (reportRevokedConnection(connection, opts))
209
+ return;
101
210
  const prefix = toolPrefixForProvider(connection.provider);
102
211
  const message = await callGateway(token, {
103
212
  companyUid,
@@ -105,20 +214,43 @@ export function registerIntegrationsCommand(program) {
105
214
  arguments: { companyUid, connectionId: connection.id },
106
215
  });
107
216
  const payload = unwrapGatewayResult(message.result);
108
- if (opts.json) {
109
- printJson(payload);
110
- return;
111
- }
112
217
  const tools = payload?.tools ?? [];
113
218
  if (tools.length === 0) {
219
+ if (opts.json) {
220
+ printJson(payload);
221
+ return;
222
+ }
114
223
  console.log("The app reported no tools.");
115
224
  return;
116
225
  }
226
+ if (opts.describe) {
227
+ const tool = tools.find((candidate) => candidate.name === opts.describe);
228
+ if (!tool) {
229
+ throw new IntegrationsCliError(`No tool named '${opts.describe}' on ${connectionLabel(connection)}.`, { expected: true });
230
+ }
231
+ if (opts.json) {
232
+ printJson(toolJsonView(tool));
233
+ return;
234
+ }
235
+ const badge = formatToolBadge(tool);
236
+ console.log(`${chalk.bold(tool.name)}${badge ? ` ${badge}` : ""} ${chalk.dim(`on ${connectionLabel(connection)}`)}`);
237
+ if (tool.description)
238
+ console.log(chalk.dim(` ${tool.description}`));
239
+ printToolSchema(tool);
240
+ return;
241
+ }
242
+ if (opts.json) {
243
+ printJson({ ...payload, tools: tools.map(toolJsonView) });
244
+ return;
245
+ }
246
+ console.log(chalk.dim(`App: ${connectionLabel(connection)}`));
117
247
  for (const tool of tools) {
118
248
  const label = tool.title && tool.title !== tool.name ? ` ${chalk.dim(tool.title)}` : "";
119
- console.log(`${chalk.bold(tool.name)}${label}`);
249
+ const required = requiredInputHint(tool.inputSchema);
250
+ const badge = formatToolBadge(tool);
251
+ console.log(`${chalk.bold(tool.name)}${badge ? ` ${badge}` : ""}${label}${required ? ` ${chalk.dim(required)}` : ""}`);
120
252
  }
121
- console.log(chalk.dim(`\n${tools.length} tools. Call one with: hq integrations call <tool> --provider ${bareProvider(connection.provider)} --args '<json>'`));
253
+ 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
254
  });
123
255
  integrations
124
256
  .command("call <tool>")
@@ -144,6 +276,8 @@ export function registerIntegrationsCommand(program) {
144
276
  const token = await ensureCognitoIdToken();
145
277
  const companyUid = await getCompanyUid(token, opts.company);
146
278
  const connection = selectConnection(await fetchConnections(token, companyUid), opts);
279
+ if (reportRevokedConnection(connection, { ...opts, fail: true }))
280
+ return;
147
281
  const prefix = toolPrefixForProvider(connection.provider);
148
282
  const idempotencyKey = opts.idempotencyKey ?? `hq-cli-${randomUUID()}`;
149
283
  const message = await callGateway(token, {
@@ -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.20",
3
+ "version": "5.103.22",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {