@indigoai-us/hq-cli 5.60.0 → 5.62.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/dist/commands/agents.d.ts +109 -0
  2. package/dist/commands/agents.js +385 -0
  3. package/dist/commands/db-migrate.d.ts +6 -0
  4. package/dist/commands/db-migrate.js +42 -0
  5. package/dist/commands/db-provision.d.ts +15 -0
  6. package/dist/commands/db-provision.js +78 -0
  7. package/dist/commands/db-sql.d.ts +9 -0
  8. package/dist/commands/db-sql.js +81 -0
  9. package/dist/commands/db-status.d.ts +7 -0
  10. package/dist/commands/db-status.js +70 -0
  11. package/dist/commands/db.d.ts +9 -0
  12. package/dist/commands/db.js +23 -0
  13. package/dist/commands/integrations.d.ts +78 -0
  14. package/dist/commands/integrations.js +309 -0
  15. package/dist/commands/members.js +4 -4
  16. package/dist/commands/outposts.d.ts +60 -0
  17. package/dist/commands/outposts.js +255 -0
  18. package/dist/commands/pack-install.d.ts +7 -1
  19. package/dist/commands/pack-install.js +86 -15
  20. package/dist/commands/packs.d.ts +2 -1
  21. package/dist/commands/packs.js +13 -8
  22. package/dist/commands/secrets.d.ts +13 -0
  23. package/dist/commands/secrets.js +149 -10
  24. package/dist/commands/skill.d.ts +153 -0
  25. package/dist/commands/skill.js +593 -0
  26. package/dist/commands/workers.d.ts +48 -0
  27. package/dist/commands/workers.js +229 -0
  28. package/dist/index.d.ts +5 -3
  29. package/dist/index.js +14 -240
  30. package/dist/lib/db/control-plane.d.ts +45 -0
  31. package/dist/lib/db/control-plane.js +81 -0
  32. package/dist/lib/db/local.d.ts +49 -0
  33. package/dist/lib/db/local.js +106 -0
  34. package/dist/lib/db/migrate.d.ts +41 -0
  35. package/dist/lib/db/migrate.js +104 -0
  36. package/dist/lib/db/paths.d.ts +56 -0
  37. package/dist/lib/db/paths.js +103 -0
  38. package/dist/lib/db/remote-engine.d.ts +58 -0
  39. package/dist/lib/db/remote-engine.js +90 -0
  40. package/dist/lib/db/remote-sql.d.ts +22 -0
  41. package/dist/lib/db/remote-sql.js +39 -0
  42. package/dist/lib/db/sql.d.ts +49 -0
  43. package/dist/lib/db/sql.js +132 -0
  44. package/dist/main.d.ts +7 -0
  45. package/dist/main.js +272 -0
  46. package/dist/utils/cognito-session.js +3 -3
  47. package/dist/utils/sandbox-runner-client.d.ts +13 -0
  48. package/dist/utils/sandbox-runner-client.js +83 -6
  49. package/dist/utils/version-check.d.ts +6 -0
  50. package/dist/utils/version-check.js +78 -2
  51. package/package.json +9 -1
  52. package/pnpm-workspace.yaml +2 -0
  53. package/src/commands/agents.test.ts +297 -0
  54. package/src/commands/agents.ts +561 -0
  55. package/src/commands/db-migrate.ts +55 -0
  56. package/src/commands/db-provision.ts +102 -0
  57. package/src/commands/db-sql.ts +124 -0
  58. package/src/commands/db-status.ts +100 -0
  59. package/src/commands/db.ts +26 -0
  60. package/src/commands/integrations.test.ts +284 -0
  61. package/src/commands/integrations.ts +438 -0
  62. package/src/commands/members.ts +2 -2
  63. package/src/commands/outposts.test.ts +177 -0
  64. package/src/commands/outposts.ts +338 -0
  65. package/src/commands/pack-install.ts +115 -18
  66. package/src/commands/pack-update-cache.test.ts +149 -0
  67. package/src/commands/packs.ts +28 -7
  68. package/src/commands/secrets.parse-destination.test.ts +38 -0
  69. package/src/commands/secrets.test.ts +342 -0
  70. package/src/commands/secrets.ts +227 -13
  71. package/src/commands/skill.test.ts +770 -0
  72. package/src/commands/skill.ts +796 -0
  73. package/src/commands/workers.test.ts +158 -0
  74. package/src/commands/workers.ts +298 -0
  75. package/src/index.test.ts +32 -0
  76. package/src/index.ts +11 -274
  77. package/src/lib/db/control-plane.test.ts +59 -0
  78. package/src/lib/db/control-plane.ts +113 -0
  79. package/src/lib/db/local.test.ts +81 -0
  80. package/src/lib/db/local.ts +148 -0
  81. package/src/lib/db/migrate.test.ts +133 -0
  82. package/src/lib/db/migrate.ts +137 -0
  83. package/src/lib/db/paths.test.ts +112 -0
  84. package/src/lib/db/paths.ts +128 -0
  85. package/src/lib/db/remote-engine.test.ts +44 -0
  86. package/src/lib/db/remote-engine.ts +148 -0
  87. package/src/lib/db/remote-sql.test.ts +32 -0
  88. package/src/lib/db/remote-sql.ts +62 -0
  89. package/src/lib/db/sql.test.ts +106 -0
  90. package/src/lib/db/sql.ts +192 -0
  91. package/src/main.ts +314 -0
  92. package/src/utils/cognito-session.ts +1 -1
  93. package/src/utils/sandbox-runner-client.test.ts +128 -0
  94. package/src/utils/sandbox-runner-client.ts +100 -4
  95. package/src/utils/version-check.test.ts +30 -0
  96. package/src/utils/version-check.ts +72 -0
  97. package/test/commands/db-tenant-isolation.test.ts +94 -0
  98. package/test/commands/db.test.ts +85 -0
@@ -279,6 +279,121 @@ function describeSecretAclPrincipal(principal: SecretAclPrincipal): string {
279
279
  : principal.granteeId;
280
280
  }
281
281
 
282
+ // secrets-proxy-per-secret-destination US-006: an InjectionRecipe as sent on
283
+ // the create/update POST body. Mirrors hq-pro's `InjectionRecipe`
284
+ // (src/vault-service/handlers/secrets.ts) — kept as a local shape (not
285
+ // imported) since the CLI does not depend on the hq-pro package.
286
+ export interface SecretInjectionRecipe {
287
+ header: string;
288
+ scheme: "raw" | "bearer";
289
+ extraHeaders?: Record<string, string>;
290
+ }
291
+
292
+ // Mirrors hq-pro's server-authoritative KNOWN_DESTINATION_REGISTRY
293
+ // (src/vault-service/handlers/destination-registry.ts) — a KNOWN host's
294
+ // --auth-style is optional because the server resolves the recipe itself.
295
+ // This client-side copy exists purely so an UNKNOWN host with no
296
+ // --auth-style can be rejected immediately with a clear, actionable message
297
+ // instead of a round trip; the server remains the authoritative validator
298
+ // (this list drifting stale merely means one extra CLI round trip, not a
299
+ // security gap — the server still 400s an unrecognized host with no recipe).
300
+ const KNOWN_DESTINATION_HOSTS = new Set([
301
+ "api.anthropic.com",
302
+ "api.openai.com",
303
+ "api.stripe.com",
304
+ ]);
305
+
306
+ // Parses `--auth-style` into the InjectionRecipe shape the server expects.
307
+ // Returns `null` (with a printed error) for an unrecognized value.
308
+ function parseAuthStyle(authStyle: string): SecretInjectionRecipe | null {
309
+ if (authStyle === "bearer") {
310
+ return { header: "authorization", scheme: "bearer" };
311
+ }
312
+ if (authStyle === "x-api-key") {
313
+ return { header: "x-api-key", scheme: "raw" };
314
+ }
315
+ const headerMatch = authStyle.match(/^header:(.+)$/);
316
+ if (headerMatch) {
317
+ const headerName = headerMatch[1].trim();
318
+ if (!headerName) {
319
+ console.error(
320
+ chalk.red(
321
+ `Invalid --auth-style 'header:': must name a header, e.g. header:X-Custom-Key`,
322
+ ),
323
+ );
324
+ return null;
325
+ }
326
+ return { header: headerName, scheme: "raw" };
327
+ }
328
+ console.error(
329
+ chalk.red(
330
+ `Invalid --auth-style '${authStyle}': must be one of bearer, x-api-key, or header:NAME`,
331
+ ),
332
+ );
333
+ return null;
334
+ }
335
+
336
+ // Validates `--destination` is a bare HTTPS scheme+host URL (no path, query,
337
+ // port). Mirrors hq-pro's `validateDestinations` server-side check
338
+ // (src/vault-service/handlers/secrets.ts) so a malformed URL is caught
339
+ // locally with an actionable message rather than a round trip — the server
340
+ // re-validates and remains authoritative.
341
+ export function parseDestinationUrl(
342
+ raw: string,
343
+ ): { ok: true; url: string; hostname: string } | { ok: false } {
344
+ let parsed: URL;
345
+ try {
346
+ parsed = new URL(raw);
347
+ } catch {
348
+ console.error(
349
+ chalk.red(`Invalid --destination '${raw}': must be a valid URL`),
350
+ );
351
+ return { ok: false };
352
+ }
353
+ if (parsed.protocol !== "https:") {
354
+ console.error(
355
+ chalk.red(`Invalid --destination '${raw}': must use https://`),
356
+ );
357
+ return { ok: false };
358
+ }
359
+ if (!parsed.hostname) {
360
+ console.error(
361
+ chalk.red(`Invalid --destination '${raw}': missing hostname`),
362
+ );
363
+ return { ok: false };
364
+ }
365
+ // Reject an embedded `user:pass@host` credential segment explicitly rather
366
+ // than letting `new URL()` silently drop it (mirrors hq-pro's authoritative
367
+ // `validateDestinations`; the server re-validates and remains authoritative).
368
+ if (parsed.username !== "" || parsed.password !== "") {
369
+ console.error(
370
+ chalk.red(
371
+ `Invalid --destination '${raw}': must not contain embedded userinfo (user:pass@) credentials`,
372
+ ),
373
+ );
374
+ return { ok: false };
375
+ }
376
+ if (
377
+ (parsed.pathname !== "" && parsed.pathname !== "/") ||
378
+ parsed.search !== "" ||
379
+ parsed.hash !== ""
380
+ ) {
381
+ console.error(
382
+ chalk.red(
383
+ `Invalid --destination '${raw}': must be a bare scheme+host URL with no path, query, or fragment (e.g. https://api.openai.com)`,
384
+ ),
385
+ );
386
+ return { ok: false };
387
+ }
388
+ if (parsed.port !== "") {
389
+ console.error(
390
+ chalk.red(`Invalid --destination '${raw}': must not specify a port`),
391
+ );
392
+ return { ok: false };
393
+ }
394
+ return { ok: true, url: `https://${parsed.hostname}`, hostname: parsed.hostname };
395
+ }
396
+
282
397
  function normalizeSecretTier(tier?: string): SecretTier {
283
398
  return tier === "sensitive" || tier === "nuclear" ? tier : "standard";
284
399
  }
@@ -323,8 +438,17 @@ async function buildSecretUsage(
323
438
  };
324
439
  }
325
440
 
326
- function parseSecretNameList(input: string): string[] {
327
- const keys = input.split(",").map((k) => k.trim()).filter(Boolean);
441
+ // Commander collector for `--only`: accumulates across REPEATED flags instead
442
+ // of the last one silently winning, and each value may still be
443
+ // comma-separated. So `--only A,B`, `--only A --only B`, and
444
+ // `--only A,B --only C` all resolve to the full list.
445
+ export function collectSecretNames(value: string, previous?: string[]): string[] {
446
+ const parsed = value.split(",").map((k) => k.trim()).filter(Boolean);
447
+ return (previous ?? []).concat(parsed);
448
+ }
449
+
450
+ function parseSecretNameList(input: string[] | undefined): string[] {
451
+ const keys = input ?? [];
328
452
  if (keys.length === 0) {
329
453
  console.error(chalk.red("Error: --only requires at least one secret name."));
330
454
  process.exit(1);
@@ -572,13 +696,87 @@ export function registerSecretsCommand(program: Command): void {
572
696
  .command("set <name>")
573
697
  .description("Create or update a secret")
574
698
  .option("--from-stdin", "Read secret value from piped stdin")
575
- .action(async (name: string, opts: { fromStdin?: boolean }) => {
699
+ .option(
700
+ "--high-security",
701
+ "Mark the secret high-security: it can never be revealed or injected locally, only used through the HQ secret proxy (requires --destination)",
702
+ )
703
+ .option(
704
+ "--destination <https-url>",
705
+ "Approved scheme+host HTTPS URL the proxy may forward this secret to (e.g. https://api.openai.com); required with --high-security",
706
+ )
707
+ .option(
708
+ "--auth-style <style>",
709
+ "How the proxy attaches the key upstream: bearer | x-api-key | header:NAME. Optional for known destinations (auto-resolved server-side); required for unknown ones",
710
+ )
711
+ .action(async (
712
+ name: string,
713
+ opts: {
714
+ fromStdin?: boolean;
715
+ highSecurity?: boolean;
716
+ destination?: string;
717
+ authStyle?: string;
718
+ },
719
+ ) => {
576
720
  try {
577
721
  if (!SECRET_NAME_PATTERN.test(name)) {
578
722
  console.error(chalk.red(`Invalid secret name '${name}': must match ^[A-Z][A-Z0-9_]*(/[A-Z][A-Z0-9_]+)*$ (e.g. MY_API_KEY or DEV/MY_KEY)`));
579
723
  process.exit(1);
580
724
  }
581
725
 
726
+ // secrets-proxy-per-secret-destination US-006: --high-security marks
727
+ // the secret so it can only ever be used through the server-side
728
+ // proxy (never revealed/injected locally — that refusal is the
729
+ // pre-existing consumption-side behavior in `get`/`exec`/`env` above,
730
+ // unchanged by this story). It REQUIRES a --destination: the proxy
731
+ // (hq-pro US-002) fails closed with no destination configured, so
732
+ // catching the missing pin here is a clear, immediate CLI error
733
+ // rather than a deferred proxy-time failure.
734
+ let destinations: string[] | undefined;
735
+ let injection: SecretInjectionRecipe | undefined;
736
+ if (opts.highSecurity) {
737
+ if (!opts.destination) {
738
+ console.error(
739
+ chalk.red(
740
+ "Error: --high-security requires --destination <https-url> (e.g. --destination https://api.openai.com).",
741
+ ),
742
+ );
743
+ process.exit(1);
744
+ }
745
+
746
+ const destResult = parseDestinationUrl(opts.destination);
747
+ if (!destResult.ok) {
748
+ process.exit(1);
749
+ }
750
+ destinations = [destResult.url];
751
+
752
+ if (opts.authStyle) {
753
+ const recipe = parseAuthStyle(opts.authStyle);
754
+ if (!recipe) {
755
+ process.exit(1);
756
+ }
757
+ injection = recipe;
758
+ } else if (!KNOWN_DESTINATION_HOSTS.has(destResult.hostname)) {
759
+ // Unknown host + no explicit recipe: the server would reject this
760
+ // 400 anyway (US-004 registry lookup only, never guesses) — fail
761
+ // fast locally with an actionable message instead of a round trip.
762
+ console.error(
763
+ chalk.red(
764
+ `Error: unknown destination host '${destResult.hostname}' — provide --auth-style <bearer|x-api-key|header:NAME> (known hosts auto-resolve: ${[...KNOWN_DESTINATION_HOSTS].join(", ")}).`,
765
+ ),
766
+ );
767
+ process.exit(1);
768
+ }
769
+ // Known host + no --auth-style: leave `injection` undefined so the
770
+ // server (US-004) auto-resolves the recipe from its registry.
771
+ } else if (opts.destination || opts.authStyle) {
772
+ console.error(
773
+ chalk.red(
774
+ "Error: --destination/--auth-style require --high-security.",
775
+ ),
776
+ );
777
+ process.exit(1);
778
+ }
779
+
582
780
  let value: string;
583
781
  if (opts.fromStdin) {
584
782
  if (process.stdin.isTTY) {
@@ -617,19 +815,35 @@ export function registerSecretsCommand(program: Command): void {
617
815
  token,
618
816
  path: `/secrets/${encodeURIComponent(companyUid)}`,
619
817
  method: "POST",
620
- body: { name, value },
818
+ body: {
819
+ name,
820
+ value,
821
+ // Only present when --high-security was passed — an ordinary
822
+ // `set` with no flags sends exactly `{ name, value }`, byte-for-
823
+ // byte unchanged from before this story.
824
+ ...(opts.highSecurity ? { highSecurity: true } : {}),
825
+ ...(destinations ? { destinations } : {}),
826
+ ...(injection ? { injection } : {}),
827
+ },
621
828
  });
622
829
 
623
830
  if (!res.ok) {
624
- const body = await res.json().catch(() => ({}));
831
+ const body = (await res.json().catch(() => ({}))) as Record<string, unknown>;
625
832
  console.error(
626
- chalk.red(`Failed to set secret: ${(body as Record<string, string>).error ?? res.statusText}`),
833
+ chalk.red(`Failed to set secret: ${extractApiMessage(body, res.statusText)}`),
627
834
  );
628
835
  process.exit(1);
629
836
  }
630
837
 
631
838
  removeCacheEntry(companyUid, name);
632
839
  console.log(chalk.green(formatSecretSaved(name, scopeLabel)));
840
+ if (opts.highSecurity) {
841
+ console.log(
842
+ chalk.dim(
843
+ ` High-security: destination pinned to ${destinations?.[0]}. This value can never be revealed or injected locally — only used through the HQ secret proxy.`,
844
+ ),
845
+ );
846
+ }
633
847
  } catch (err) {
634
848
  console.error(
635
849
  chalk.red("Error:"),
@@ -1253,11 +1467,11 @@ export function registerSecretsCommand(program: Command): void {
1253
1467
  "--personal",
1254
1468
  "Operate on the caller's personal vault (no sharing)",
1255
1469
  )
1256
- .option("--only <keys>", "Comma-separated list of secret names to inject (required)")
1470
+ .option("--only <keys>", "Secret names to inject (comma-separated; may be repeated) (required)", collectSecretNames)
1257
1471
  .allowUnknownOption(true)
1258
- .action(async (opts: { company?: string; personal?: boolean; only?: string }, cmd: Command) => {
1472
+ .action(async (opts: { company?: string; personal?: boolean; only?: string[] }, cmd: Command) => {
1259
1473
  try {
1260
- if (!opts.only || opts.only.trim().length === 0) {
1474
+ if (!opts.only || opts.only.length === 0) {
1261
1475
  console.error(chalk.red("Error: --only is required and must name at least one secret."));
1262
1476
  process.exit(1);
1263
1477
  }
@@ -1307,10 +1521,10 @@ export function registerSecretsCommand(program: Command): void {
1307
1521
  secrets
1308
1522
  .command("exec")
1309
1523
  .description("Run a command with secrets injected as env vars")
1310
- .requiredOption("--only <keys>", "Comma-separated list of secret names to inject (required)")
1524
+ .requiredOption("--only <keys>", "Secret names to inject (comma-separated; may be repeated) (required)", collectSecretNames)
1311
1525
  .option("--script <path>", "Attach local script identity for script-locked secrets")
1312
1526
  .allowUnknownOption(true)
1313
- .action(async (_opts: { only: string; script?: string }, cmd: Command) => {
1527
+ .action(async (_opts: { only: string[]; script?: string }, cmd: Command) => {
1314
1528
  try {
1315
1529
  const rawArgs = cmd.args;
1316
1530
  const dashIndex = process.argv.indexOf("--");
@@ -1381,9 +1595,9 @@ export function registerSecretsCommand(program: Command): void {
1381
1595
  secrets
1382
1596
  .command("env")
1383
1597
  .description("Print 'export KEY=VALUE' lines suitable for: source <(hq secrets env --only K1,K2)")
1384
- .requiredOption("--only <keys>", "Comma-separated list of secret names to print (required)")
1598
+ .requiredOption("--only <keys>", "Secret names to print (comma-separated; may be repeated) (required)", collectSecretNames)
1385
1599
  .option("--script <path>", "Attach local script identity for script-locked secrets")
1386
- .action(async (opts: { only: string; script?: string }) => {
1600
+ .action(async (opts: { only: string[]; script?: string }) => {
1387
1601
  try {
1388
1602
  const redact = process.stdout.isTTY;
1389
1603
  if (redact) {