@indigoai-us/hq-cli 5.103.21 → 5.103.23

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,21 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.103.23] — 2026-08-26
6
+
7
+ ### Changed
8
+
9
+ - The bundled sync engine now requires `@indigoai-us/hq-cloud` 6.15.71, so new
10
+ company skills are registered and stamped with a stable skill UID before
11
+ their first upload while comment-only skill proposals remain available from
12
+ the CLI.
13
+
14
+ ## [5.103.22] — 2026-08-25
15
+
16
+ ### Fixed
17
+
18
+ - `hq auth login` now points account signup guidance at the canonical `onboarding.hq.computer` host instead of the retired Indigo onboarding domain.
19
+
5
20
  ## [5.103.21] — 2026-08-24
6
21
 
7
22
  ### Fixed
@@ -8,7 +8,7 @@
8
8
  * hq auth status — show whether a valid session is cached + expiry
9
9
  *
10
10
  * Sign-up is owned by the onboarding web app at
11
- * https://onboarding.indigo-hq.com. `hq auth login` signs an existing account
11
+ * https://onboarding.hq.computer. `hq auth login` signs an existing account
12
12
  * into this machine by writing ~/.hq/cognito-tokens.json; once cached, the
13
13
  * session is kept valid by `hq auth refresh` / `hq-auth-refresh` and consumed
14
14
  * by the deploy + sync skills.
@@ -8,7 +8,7 @@
8
8
  * hq auth status — show whether a valid session is cached + expiry
9
9
  *
10
10
  * Sign-up is owned by the onboarding web app at
11
- * https://onboarding.indigo-hq.com. `hq auth login` signs an existing account
11
+ * https://onboarding.hq.computer. `hq auth login` signs an existing account
12
12
  * into this machine by writing ~/.hq/cognito-tokens.json; once cached, the
13
13
  * session is kept valid by `hq auth refresh` / `hq-auth-refresh` and consumed
14
14
  * by the deploy + sync skills.
@@ -96,7 +96,7 @@ export function registerAuthCommands(program) {
96
96
  console.error(chalk.dim(callbackPortCollisionGuidance(DEFAULT_COGNITO.port ?? 8765)));
97
97
  }
98
98
  else {
99
- console.error(chalk.dim(" If you do not have an account, sign up at https://onboarding.indigo-hq.com"));
99
+ console.error(chalk.dim(" If you do not have an account, sign up at https://onboarding.hq.computer"));
100
100
  }
101
101
  process.exit(1);
102
102
  }
@@ -156,6 +156,14 @@ export declare function uninstallIntegration(token: string, companyUid: string,
156
156
  installationId: string;
157
157
  connectionId: string;
158
158
  }>;
159
+ /**
160
+ * Permanently remove a revoked connection tombstone. The admin purge endpoint
161
+ * deliberately accepts only the connection id: it derives the company and
162
+ * enforces owner authorization from the authenticated connection record.
163
+ */
164
+ export declare function purgeConnection(token: string, companyUid: string, connectionId: string): Promise<{
165
+ connectionId: string;
166
+ }>;
159
167
  export interface OAuthStartResult {
160
168
  provider: string;
161
169
  displayName: string;
@@ -197,6 +205,11 @@ export declare function updateGovernance(token: string, companyUid: string, inpu
197
205
  writePolicy?: WritePolicy;
198
206
  writeAllowlist: WriteAllowlistGrant[];
199
207
  }>;
208
+ /**
209
+ * Mark or unmark one tool as read-safe. The service owns tool classification:
210
+ * it refuses attempts to mark write/destructive tools read-safe.
211
+ */
212
+ export declare function setReadSafe(token: string, companyUid: string, connectionId: string, toolName: string, readSafe: boolean): Promise<void>;
200
213
  export interface ConnectionAccess {
201
214
  connectionId: string;
202
215
  provider: string;
@@ -74,6 +74,27 @@ export async function uninstallIntegration(token, companyUid, installationId) {
74
74
  await raiseForResponse(res, "Failed to disconnect the app");
75
75
  return (await res.json());
76
76
  }
77
+ /**
78
+ * Permanently remove a revoked connection tombstone. The admin purge endpoint
79
+ * deliberately accepts only the connection id: it derives the company and
80
+ * enforces owner authorization from the authenticated connection record.
81
+ */
82
+ export async function purgeConnection(token, companyUid, connectionId) {
83
+ // Keep the company in this client's call signature with the other
84
+ // connection mutations. It is resolved before the target connection so a
85
+ // caller cannot use a slug from a different company, but the server's purge
86
+ // contract intentionally takes only connectionId in its body.
87
+ void companyUid;
88
+ const res = await vaultApiFetch({
89
+ token,
90
+ path: "/v1/integrations/admin/purge",
91
+ method: "POST",
92
+ body: { connectionId },
93
+ });
94
+ if (!res.ok)
95
+ await raiseForResponse(res, "Failed to purge the revoked connection");
96
+ return (await res.json());
97
+ }
77
98
  export async function startOAuth(token, companyUid, input) {
78
99
  const res = await vaultApiFetch({
79
100
  token,
@@ -110,6 +131,20 @@ export async function updateGovernance(token, companyUid, input) {
110
131
  await raiseForResponse(res, "Failed to update the app's settings");
111
132
  return (await res.json());
112
133
  }
134
+ /**
135
+ * Mark or unmark one tool as read-safe. The service owns tool classification:
136
+ * it refuses attempts to mark write/destructive tools read-safe.
137
+ */
138
+ export async function setReadSafe(token, companyUid, connectionId, toolName, readSafe) {
139
+ const res = await vaultApiFetch({
140
+ token,
141
+ path: "/v1/integrations/admin/read-safe",
142
+ method: "POST",
143
+ body: { companyUid, connectionId, toolName, readSafe },
144
+ });
145
+ if (!res.ok)
146
+ await raiseForResponse(res, "Failed to update the tool's read-safe setting");
147
+ }
113
148
  /**
114
149
  * Live open-approval list, read from hq-pro's confirm-queue state (not the
115
150
  * audit feed). Unlike the audit-derived reconstruction this replaces, a call
@@ -20,7 +20,7 @@ import chalk from "chalk";
20
20
  import open from "open";
21
21
  import { ensureCognitoIdToken } from "../utils/cognito-session.js";
22
22
  import { getCompanyUid } from "../utils/vault-api.js";
23
- import { IntegrationsCliError, bareProvider, printJson, resolveConnection, } from "./integrations-core.js";
23
+ import { IntegrationsCliError, bareProvider, connectionDomain, printJson, revokedConnectionDetails, resolveConnection, } from "./integrations-core.js";
24
24
  import { completeOAuth, discoverDocs, installIntegration, listCatalog, pullBlueprint, startOAuth, } from "./integrations-api.js";
25
25
  import { startLoopbackListener } from "./integrations-oauth.js";
26
26
  /** hq-pro's machine code for "this endpoint needs a browser sign-in". */
@@ -183,7 +183,7 @@ async function resolveKey(opts, appLabel) {
183
183
  * most-specific-first so an explicit flag always wins over the positional
184
184
  * argument's heuristics.
185
185
  */
186
- async function resolveTarget(token, companyUid, app, opts) {
186
+ async function resolveTarget(token, companyUid, app, opts, preserveDomain = false) {
187
187
  if (opts.docsUrl) {
188
188
  const found = await discoverDocs(token, companyUid, opts.docsUrl);
189
189
  if (!found.discovery || !found.discoveryReceiptId) {
@@ -222,6 +222,17 @@ async function resolveTarget(token, companyUid, app, opts) {
222
222
  // authClass, both of which make the connect cleaner than a raw domain
223
223
  // lookup. Missing it is fine — the domain path still works.
224
224
  const match = await findCatalogEntry(token, companyUid, app);
225
+ if (preserveDomain) {
226
+ // Reviving a revoked connection is keyed by its canonical domain in
227
+ // hq-pro. A catalog entry id is useful auth metadata, but replacing the
228
+ // domain with it can create a distinct connection instead of reviving
229
+ // the original acct_ row.
230
+ return {
231
+ ref: { domain: app },
232
+ ...(match?.authClass ? { authClass: match.authClass } : {}),
233
+ label: match?.name || app,
234
+ };
235
+ }
225
236
  return catalogEntryToTarget(match, { ref: { domain: app }, label: app });
226
237
  }
227
238
  // A bare name (`notion`, `atlassian`) is how the catalog reads to a person —
@@ -424,14 +435,15 @@ async function completeCredentialIfNeeded(token, companyUid, target, opts, resul
424
435
  });
425
436
  }
426
437
  /** Print the outcome of a successful connect. */
427
- function reportInstall(result, opts) {
438
+ function reportInstall(result, opts, expectedRevivedConnectionId) {
428
439
  if (opts.json) {
429
440
  printJson(result);
430
441
  return;
431
442
  }
432
443
  const { installation, connection } = result;
433
444
  const toolCount = result.mcp?.tools?.length;
434
- console.log(chalk.green(`Connected ${chalk.bold(installation.displayName)}`) +
445
+ const revived = connection.id === expectedRevivedConnectionId;
446
+ console.log(chalk.green(`${revived ? "Revived" : "Connected"} ${chalk.bold(installation.displayName)}`) +
435
447
  (typeof toolCount === "number" ? chalk.dim(` — ${toolCount} tools available`) : ""));
436
448
  console.log(chalk.dim(` connection: ${connection.id}`));
437
449
  if (installation.status === "needs_credentials") {
@@ -439,6 +451,49 @@ function reportInstall(result, opts) {
439
451
  }
440
452
  console.log(chalk.dim(` Try it: hq integrations tools --provider ${bareProvider(connection.provider)}`));
441
453
  }
454
+ /**
455
+ * The shared `connect <domain>` execution path. Reconnect's revoked fallback
456
+ * intentionally comes through here rather than re-installing its saved MCP
457
+ * URL: hq-pro recognizes the domain and revives the revoked row in place.
458
+ */
459
+ async function connectApp(token, companyUid, app, opts, expectedRevivedConnectionId) {
460
+ const target = await resolveTarget(token, companyUid, app, opts, expectedRevivedConnectionId !== undefined);
461
+ const authMode = opts.auth ?? target.authClass;
462
+ if (authMode === "oauth") {
463
+ const result = await connectViaOAuth(token, companyUid, target, opts);
464
+ if (result)
465
+ reportInstall(result, opts, expectedRevivedConnectionId);
466
+ return;
467
+ }
468
+ // A key is only collected when something already says one is needed, or
469
+ // the caller supplied one — otherwise a no-auth app would pointlessly
470
+ // prompt.
471
+ const wantsKey = authMode === "key" || Boolean(opts.token || opts.tokenStdin);
472
+ const bearerToken = wantsKey ? await resolveKey(opts, target.label) : undefined;
473
+ try {
474
+ const result = await installIntegration(token, companyUid, {
475
+ ...target.ref,
476
+ ...(bearerToken
477
+ ? { authMode: "bearer", bearerToken }
478
+ : authMode === "none"
479
+ ? { authMode: "none" }
480
+ : {}),
481
+ });
482
+ reportInstall(await completeCredentialIfNeeded(token, companyUid, target, opts, result), opts, expectedRevivedConnectionId);
483
+ }
484
+ catch (err) {
485
+ // Server-authoritative detection: the endpoint turned out to be
486
+ // OAuth-protected, so run the browser flow instead of making the
487
+ // caller re-issue the command with --auth oauth.
488
+ if (isOAuthRequiredError(err)) {
489
+ const result = await connectViaOAuth(token, companyUid, target, opts);
490
+ if (result)
491
+ reportInstall(result, opts, expectedRevivedConnectionId);
492
+ return;
493
+ }
494
+ throw err;
495
+ }
496
+ }
442
497
  export function registerConnectCommands(integrations) {
443
498
  integrations
444
499
  .command("catalog [query]")
@@ -561,46 +616,11 @@ export function registerConnectCommands(integrations) {
561
616
  assertAuthMode(opts.auth);
562
617
  const token = await ensureCognitoIdToken();
563
618
  const companyUid = await getCompanyUid(token, opts.company);
564
- const target = await resolveTarget(token, companyUid, app, opts);
565
- const authMode = opts.auth ?? target.authClass;
566
- if (authMode === "oauth") {
567
- const result = await connectViaOAuth(token, companyUid, target, opts);
568
- if (result)
569
- reportInstall(result, opts);
570
- return;
571
- }
572
- // A key is only collected when something already says one is needed, or
573
- // the caller supplied one — otherwise a no-auth app would pointlessly
574
- // prompt.
575
- const wantsKey = authMode === "key" || Boolean(opts.token || opts.tokenStdin);
576
- const bearerToken = wantsKey ? await resolveKey(opts, target.label) : undefined;
577
- try {
578
- const result = await installIntegration(token, companyUid, {
579
- ...target.ref,
580
- ...(bearerToken
581
- ? { authMode: "bearer", bearerToken }
582
- : authMode === "none"
583
- ? { authMode: "none" }
584
- : {}),
585
- });
586
- reportInstall(await completeCredentialIfNeeded(token, companyUid, target, opts, result), opts);
587
- }
588
- catch (err) {
589
- // Server-authoritative detection: the endpoint turned out to be
590
- // OAuth-protected, so run the browser flow instead of making the
591
- // caller re-issue the command with --auth oauth.
592
- if (isOAuthRequiredError(err)) {
593
- const result = await connectViaOAuth(token, companyUid, target, opts);
594
- if (result)
595
- reportInstall(result, opts);
596
- return;
597
- }
598
- throw err;
599
- }
619
+ await connectApp(token, companyUid, app, opts);
600
620
  });
601
621
  integrations
602
622
  .command("reconnect [app]")
603
- .description("Re-authenticate a connected app whose credentials stopped working")
623
+ .description("Re-authenticate a connected app; use --connect to re-add a revoked app")
604
624
  .option("--company <slug>", "Company slug, e.g. indigo")
605
625
  .option("--provider <slug>", "Connected app (e.g. linear)")
606
626
  .option("--connection <id>", "Connection id (acct_…)")
@@ -609,6 +629,7 @@ export function registerConnectCommands(integrations) {
609
629
  .option("--auth <mode>", "Force the auth mode: none, key, or oauth (default: detect)")
610
630
  .option("--no-browser", "Print the sign-in URL instead of opening a browser")
611
631
  .option("--timeout <seconds>", "How long to wait for a browser sign-in (default 300)")
632
+ .option("--connect", "For a revoked row, run `connect <domain>` to re-add and revive it")
612
633
  .option("--json", "Machine-readable output")
613
634
  .action(async (app, opts) => {
614
635
  // Same validation as `connect`. Without it a typo like `--auth oauth2`
@@ -618,7 +639,23 @@ export function registerConnectCommands(integrations) {
618
639
  assertAuthMode(opts.auth);
619
640
  const token = await ensureCognitoIdToken();
620
641
  const companyUid = await getCompanyUid(token, opts.company);
621
- const connection = await resolveConnection(token, companyUid, app, opts);
642
+ const connection = await resolveConnection(token, companyUid, app, opts, {
643
+ allowSingleRevoked: Boolean(opts.connect),
644
+ });
645
+ if (connection.status === "revoked") {
646
+ const details = revokedConnectionDetails(connection, opts.company);
647
+ if (!opts.connect) {
648
+ if (opts.json)
649
+ printJson(details);
650
+ else {
651
+ console.log(chalk.yellow(details.reason));
652
+ console.log(chalk.yellow(`Re-add it with: ${details.fixPath}`));
653
+ }
654
+ return;
655
+ }
656
+ await connectApp(token, companyUid, connectionDomain(connection), opts, connection.id);
657
+ return;
658
+ }
622
659
  const url = connection.installation?.surface?.url;
623
660
  if (!url) {
624
661
  throw new IntegrationsCliError(`${bareProvider(connection.provider)} was not installed through the app catalog, so it cannot be reconnected from here.`, { expected: true });
@@ -74,6 +74,16 @@ export interface AdminConnection {
74
74
  };
75
75
  installation?: FactoryInstallation | null;
76
76
  }
77
+ /**
78
+ * The actionable state returned instead of attempting to use a revoked
79
+ * connection. Keep this machine-readable so commands and scripts get the same
80
+ * recovery path instead of treating a listed row as absent.
81
+ */
82
+ export interface RevokedConnectionDetails {
83
+ status: "revoked";
84
+ reason: string;
85
+ fixPath: string;
86
+ }
77
87
  export interface AdminAuditEvent {
78
88
  timestamp: string;
79
89
  memberOrAgent: string;
@@ -210,6 +220,14 @@ export declare function selectConnection(connections: AdminConnection[], opts: {
210
220
  connection?: string;
211
221
  provider?: string;
212
222
  }): AdminConnection;
223
+ /**
224
+ * The hostname re-add needs. Prefer the server's canonical installation domain;
225
+ * an older row may only retain its MCP URL, and provider is the last-resort
226
+ * human-safe query when neither was stored.
227
+ */
228
+ export declare function connectionDomain(connection: AdminConnection): string;
229
+ /** A revoked row is still addressable, but it cannot make a live MCP call. */
230
+ export declare function revokedConnectionDetails(connection: AdminConnection, companySlug?: string): RevokedConnectionDetails;
213
231
  /**
214
232
  * Resolve a connection the caller named positionally OR through the
215
233
  * `--provider` / `--connection` flags. Every management verb takes an optional
@@ -219,6 +237,8 @@ export declare function selectConnection(connections: AdminConnection[], opts: {
219
237
  export declare function resolveConnection(token: string, companyUid: string, app: string | undefined, opts: {
220
238
  provider?: string;
221
239
  connection?: string;
240
+ }, recoveryOpts?: {
241
+ allowSingleRevoked?: boolean;
222
242
  }): Promise<AdminConnection>;
223
243
  export declare function callGateway(token: string, params: Record<string, unknown>): Promise<GatewayMessage>;
224
244
  /**
@@ -256,14 +256,26 @@ export function selectConnection(connections, opts) {
256
256
  if (opts.provider) {
257
257
  const want = opts.provider.trim().toLowerCase();
258
258
  const wantHumanSlug = humanSlug(opts.provider);
259
- const providerMatch = active.find((c) => bareProvider(c.provider).toLowerCase() === want || c.provider.toLowerCase() === want);
259
+ // A reconnect normally leaves a revoked historical row alongside its new
260
+ // active connection. Prefer the active inventory before applying the
261
+ // provider/name/alias precedence below; fall back to history only when no
262
+ // active row matches this selector at all. `--connection` above remains
263
+ // the explicit way to inspect or act on a particular historical row.
264
+ const activeMatches = connections.filter((c) => c.status !== "revoked" &&
265
+ (bareProvider(c.provider).toLowerCase() === want ||
266
+ c.provider.toLowerCase() === want ||
267
+ c.installation?.displayName?.trim().toLowerCase() === want ||
268
+ (wantHumanSlug !== "" &&
269
+ humanSlug(c.installation?.displayName ?? "") === wantHumanSlug)));
270
+ const candidates = activeMatches.length > 0 ? activeMatches : connections;
271
+ const providerMatch = candidates.find((c) => bareProvider(c.provider).toLowerCase() === want || c.provider.toLowerCase() === want);
260
272
  if (providerMatch)
261
273
  return providerMatch;
262
- const displayNameMatch = active.find((c) => c.installation?.displayName?.trim().toLowerCase() === want);
274
+ const displayNameMatch = candidates.find((c) => c.installation?.displayName?.trim().toLowerCase() === want);
263
275
  if (displayNameMatch)
264
276
  return displayNameMatch;
265
277
  const aliasMatches = wantHumanSlug
266
- ? active.filter((c) => {
278
+ ? candidates.filter((c) => {
267
279
  const displayName = c.installation?.displayName;
268
280
  const displayNameSlug = displayName ? humanSlug(displayName) : "";
269
281
  return displayNameSlug !== "" && displayNameSlug === wantHumanSlug;
@@ -274,7 +286,7 @@ export function selectConnection(connections, opts) {
274
286
  if (aliasMatches.length > 1) {
275
287
  throw new IntegrationsCliError(`Display-name alias '${opts.provider}' matches multiple connected apps. Use --connection to choose one.`, { expected: true });
276
288
  }
277
- const available = active.map((c) => bareProvider(c.provider)).join(", ");
289
+ const available = connections.map((c) => bareProvider(c.provider)).join(", ");
278
290
  throw new IntegrationsCliError(`No connected app matches '${opts.provider}'.` +
279
291
  (available ? ` Connected: ${available}.` : " Nothing is connected yet — connect apps with `hq integrations connect <app>`."), { expected: true });
280
292
  }
@@ -286,17 +298,59 @@ export function selectConnection(connections, opts) {
286
298
  throw new IntegrationsCliError(`Multiple apps are connected — pick one with --provider:\n` +
287
299
  active.map((c) => ` --provider ${bareProvider(c.provider)}`).join("\n"), { expected: true });
288
300
  }
301
+ /**
302
+ * The hostname re-add needs. Prefer the server's canonical installation domain;
303
+ * an older row may only retain its MCP URL, and provider is the last-resort
304
+ * human-safe query when neither was stored.
305
+ */
306
+ export function connectionDomain(connection) {
307
+ const domain = connection.installation?.domain?.trim();
308
+ if (domain)
309
+ return domain;
310
+ const url = connection.installation?.surface?.url;
311
+ if (url) {
312
+ try {
313
+ const host = new URL(url).hostname;
314
+ if (host)
315
+ return host;
316
+ }
317
+ catch {
318
+ // The saved endpoint is advisory here. A malformed legacy URL must not
319
+ // prevent recovery when the provider slug can still be re-added.
320
+ }
321
+ }
322
+ return bareProvider(connection.provider);
323
+ }
324
+ /** A revoked row is still addressable, but it cannot make a live MCP call. */
325
+ export function revokedConnectionDetails(connection, companySlug) {
326
+ const domain = connectionDomain(connection);
327
+ return {
328
+ status: "revoked",
329
+ reason: "This connection was revoked and cannot be used until it is re-added.",
330
+ fixPath: `hq integrations connect ${domain}` +
331
+ (companySlug ? ` --company ${companySlug}` : ""),
332
+ };
333
+ }
289
334
  /**
290
335
  * Resolve a connection the caller named positionally OR through the
291
336
  * `--provider` / `--connection` flags. Every management verb takes an optional
292
337
  * `<app>` argument for ergonomics (`hq integrations policy linear …`), which is
293
338
  * matched exactly like `--provider` unless it looks like a connection id.
294
339
  */
295
- export async function resolveConnection(token, companyUid, app, opts) {
340
+ export async function resolveConnection(token, companyUid, app, opts, recoveryOpts = {}) {
296
341
  const connections = await fetchConnections(token, companyUid);
297
342
  if (app && !opts.provider && !opts.connection) {
298
343
  return selectConnection(connections, app.startsWith("acct_") ? { connection: app } : { provider: app });
299
344
  }
345
+ // Normal management verbs deliberately ignore revoked rows for implicit
346
+ // selection. Reconnect's explicit --connect recovery is the one exception:
347
+ // a sole revoked row is unambiguous and needs its saved domain to revive.
348
+ if (recoveryOpts.allowSingleRevoked &&
349
+ !opts.provider &&
350
+ !opts.connection &&
351
+ connections.length === 1) {
352
+ return connections[0];
353
+ }
300
354
  return selectConnection(connections, opts);
301
355
  }
302
356
  export async function callGateway(token, params) {
@@ -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: