@indigoai-us/hq-cli 5.50.0 → 5.50.2

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 (46) hide show
  1. package/dist/commands/mcp-registration.d.ts +905 -0
  2. package/dist/commands/mcp-registration.js +2001 -0
  3. package/dist/commands/mcp-status.d.ts +130 -0
  4. package/dist/commands/mcp-status.js +406 -0
  5. package/dist/commands/onboard-warning.d.ts +7 -0
  6. package/dist/commands/onboard-warning.js +14 -0
  7. package/dist/commands/onboard.js +5 -5
  8. package/dist/commands/pack-install.d.ts +74 -0
  9. package/dist/commands/pack-install.js +493 -14
  10. package/dist/commands/packs.js +42 -4
  11. package/dist/commands/pkg-install.js +5 -2
  12. package/dist/index.js +7 -2
  13. package/dist/types.d.ts +26 -1
  14. package/dist/utils/contribution-table.d.ts +103 -0
  15. package/dist/utils/contribution-table.js +65 -0
  16. package/dist/utils/pack-contributions.d.ts +93 -10
  17. package/dist/utils/pack-contributions.js +140 -48
  18. package/dist/utils/secrets-cache.d.ts +9 -0
  19. package/dist/utils/secrets-cache.js +24 -2
  20. package/dist/utils/version-gate.d.ts +40 -1
  21. package/dist/utils/version-gate.js +91 -20
  22. package/package.json +3 -2
  23. package/scripts/generate-scan-packages-table.mjs +113 -0
  24. package/src/commands/mcp-registration.test.ts +2787 -0
  25. package/src/commands/mcp-registration.ts +2612 -0
  26. package/src/commands/mcp-status.test.ts +483 -0
  27. package/src/commands/mcp-status.ts +575 -0
  28. package/src/commands/mcp-status.us011.test.ts +243 -0
  29. package/src/commands/onboard-warning.test.ts +26 -0
  30. package/src/commands/onboard-warning.ts +12 -0
  31. package/src/commands/onboard.ts +4 -7
  32. package/src/commands/pack-install.test.ts +733 -0
  33. package/src/commands/pack-install.ts +582 -13
  34. package/src/commands/packs.ts +45 -1
  35. package/src/commands/pkg-install.ts +4 -1
  36. package/src/index.ts +6 -0
  37. package/src/types.ts +28 -9
  38. package/src/utils/contribution-table.ts +83 -0
  39. package/src/utils/pack-contributions.test.ts +310 -25
  40. package/src/utils/pack-contributions.ts +194 -47
  41. package/src/utils/secrets-cache.ts +22 -0
  42. package/src/utils/version-gate.test.ts +122 -0
  43. package/src/utils/version-gate.ts +109 -13
  44. package/test/e2e/smoke-install-mcp.sh +113 -0
  45. package/test/fixtures/hq-pack-smoke-mcp/mcp/smoke-http.json +1 -0
  46. package/test/fixtures/hq-pack-smoke-mcp/package.yaml +11 -0
@@ -38,8 +38,10 @@ import {
38
38
  contributionLinks,
39
39
  linkStatus,
40
40
  listInstalledPacks,
41
+ findDependentPacks,
41
42
  readPackManifest,
42
43
  unwirePack,
44
+ unwirePackMcp,
43
45
  readHqVersion,
44
46
  readRecommendedPackages,
45
47
  packagesDir,
@@ -303,6 +305,7 @@ interface UpdateOpts extends CommonOpts {
303
305
  checkOnly?: boolean;
304
306
  yes?: boolean;
305
307
  allowHooks?: boolean;
308
+ allowMcp?: boolean;
306
309
  branch?: boolean;
307
310
  }
308
311
 
@@ -359,6 +362,7 @@ async function runUpdate(name: string | undefined, opts: UpdateOpts): Promise<Up
359
362
  try {
360
363
  await installPack(source, {
361
364
  allowHooks: opts.yes || opts.allowHooks,
365
+ allowMcp: opts.yes || opts.allowMcp,
362
366
  followBranch: opts.branch,
363
367
  quiet: wantsJson(opts),
364
368
  });
@@ -391,6 +395,7 @@ interface UninstallResult {
391
395
  interface UninstallOpts extends CommonOpts {
392
396
  yes?: boolean;
393
397
  archive?: boolean; // commander sets false for --no-archive
398
+ force?: boolean; // bypass the dependents guard (M0)
394
399
  }
395
400
 
396
401
  function archiveTimestamp(): string {
@@ -410,9 +415,46 @@ async function runUninstall(name: string, opts: UninstallOpts): Promise<Uninstal
410
415
  warnings.push('package.yaml unreadable -- host symlinks could not be computed precisely; ran a re-scan to reconcile.');
411
416
  }
412
417
 
418
+ // 0. Dependents guard (M0): refuse to remove a pack that another installed pack
419
+ // lists in its `requires.packs`, unless --force. Filesystem presence is the
420
+ // source of truth (listInstalledPacks), consistent with the install-time
421
+ // assertPackDependencies check. Runs BEFORE any un-wiring so a blocked uninstall
422
+ // leaves the pack fully intact.
423
+ if (!opts.force) {
424
+ const dependents = findDependentPacks(listInstalledPacks(hqRoot), name);
425
+ if (dependents.length > 0) {
426
+ const who = dependents.map((p) => p.manifest?.name ?? p.name).join(', ');
427
+ throw new Error(
428
+ `Cannot uninstall "${name}": required by ${who}. ` +
429
+ `Uninstall the dependent pack(s) first, or pass --force to override.`,
430
+ );
431
+ }
432
+ }
433
+
413
434
  // 1. Un-wire only our symlinks.
414
435
  const { unlinked, skipped } = unwirePack(hqRoot, packDir, contributes);
415
436
 
437
+ // 1b. Un-register the pack's MCP (`wire: 'merge'`) servers — invisible to the
438
+ // symlink unwire above. Provenance-scoped: removes ONLY entries stamped with this
439
+ // pack's `_hqPack`, and skip-and-warns on any foreign/unstamped same-named entry.
440
+ // Tolerant of a Codex-less host / absent config; idempotent on re-run.
441
+ try {
442
+ const mcp = unwirePackMcp(name, contributes);
443
+ for (const server of mcp.servers) {
444
+ if ('skipped' in server.claude) continue; // (claude is always inspected; never skipped)
445
+ if (server.claude.outcome === 'skipped-foreign' && server.claude.reason) {
446
+ warnings.push(server.claude.reason);
447
+ }
448
+ if (!('skipped' in server.codex) && server.codex.outcome === 'skipped-foreign' && server.codex.reason) {
449
+ warnings.push(server.codex.reason);
450
+ }
451
+ }
452
+ } catch (e) {
453
+ // Never let an MCP un-registration failure abort the rest of the uninstall
454
+ // (symlink unwire already ran; the pack dir still gets archived). Surface it.
455
+ warnings.push(`MCP un-registration encountered an error: ${(e as Error).message}`);
456
+ }
457
+
416
458
  // 2. Archive (or delete) the pack dir -- BEFORE re-scan so it isn't re-wired.
417
459
  let archived: string | null = null;
418
460
  if (opts.archive === false) {
@@ -495,8 +537,9 @@ export function registerPacksCommand(parent: Command): void {
495
537
  .option('--json', 'Machine-readable JSON output')
496
538
  .option('--hq-root <path>', 'HQ root (default: auto-detect)')
497
539
  .option('--check-only', 'Report availability without installing')
498
- .option('-y, --yes', 'Non-interactive (implies --allow-hooks)')
540
+ .option('-y, --yes', 'Non-interactive (implies --allow-hooks and --allow-mcp)')
499
541
  .option('--allow-hooks', 'Install pack hooks without prompting')
542
+ .option('--allow-mcp', 'Register pack MCP servers without prompting')
500
543
  .option('--branch', 'Follow the source branch instead of SHA-pinning')
501
544
  .action(async (name: string | undefined, opts: UpdateOpts) => {
502
545
  try {
@@ -525,6 +568,7 @@ export function registerPacksCommand(parent: Command): void {
525
568
  .option('--hq-root <path>', 'HQ root (default: auto-detect)')
526
569
  .option('-y, --yes', 'Skip confirmation')
527
570
  .option('--no-archive', 'Delete instead of archiving')
571
+ .option('--force', 'Uninstall even if other installed packs require this one')
528
572
  .action(async (name: string, opts: UninstallOpts) => {
529
573
  try {
530
574
  if (!opts.yes) {
@@ -43,16 +43,18 @@ export function registerPackageInstallCommand(parent: Command): void {
43
43
  )
44
44
  .option('--company <co>', 'Scope the package to a specific company (registry flow only)')
45
45
  .option('--allow-hooks', 'Skip the hooks confirmation prompt (content-pack flow)')
46
+ .option('--allow-mcp', 'Skip the MCP server confirmation prompt (content-pack flow)')
46
47
  .option('--branch', 'Follow a ref instead of SHA-pinning (git content-pack flow)')
47
48
  .action(
48
49
  async (
49
50
  source: string,
50
- opts: { company?: string; allowHooks?: boolean; branch?: boolean }
51
+ opts: { company?: string; allowHooks?: boolean; allowMcp?: boolean; branch?: boolean }
51
52
  ) => {
52
53
  try {
53
54
  if (sourceMatchesPackPattern(source)) {
54
55
  await installPack(source, {
55
56
  allowHooks: opts.allowHooks,
57
+ allowMcp: opts.allowMcp,
56
58
  followBranch: opts.branch,
57
59
  });
58
60
  } else {
@@ -62,6 +64,7 @@ export function registerPackageInstallCommand(parent: Command): void {
62
64
  // listings transport — the live install path — instead.
63
65
  await installPack(`${MARKETPLACE_PREFIX}${source}`, {
64
66
  allowHooks: opts.allowHooks,
67
+ allowMcp: opts.allowMcp,
65
68
  followBranch: opts.branch,
66
69
  });
67
70
  }
package/src/index.ts CHANGED
@@ -44,6 +44,7 @@ import { registerSourcesCommand } from "./commands/sources.js";
44
44
  import { registerSignalsCommand } from "./commands/signals.js";
45
45
  import { registerReindexCommand } from "./commands/reindex.js";
46
46
  import { registerRescueCommand } from "./commands/rescue.js";
47
+ import { registerMcpCommand } from "./commands/mcp-status.js";
47
48
  import { sanitizeArgv } from "./utils/feedback-diagnostics.js";
48
49
  import { environmentalFsErrorMessage } from "./utils/environmental-error.js";
49
50
  import {
@@ -199,6 +200,11 @@ registerReindexCommand(program);
199
200
  // shipped from @indigoai-us/hq-cloud.
200
201
  registerRescueCommand(program);
201
202
 
203
+ // MCP pack observability (subcommand group — `hq mcp status`). Read-only
204
+ // provenance-based status across BOTH Claude + Codex runtimes (reads `_hqPack`
205
+ // off the configs, NOT linkStatus), with secret-redacted output + `--json`.
206
+ registerMcpCommand(program);
207
+
202
208
  (async () => {
203
209
  try {
204
210
  Sentry.addBreadcrumb({
package/src/types.ts CHANGED
@@ -8,6 +8,8 @@
8
8
  * knowledge/public/hq-core/package-yaml-spec.md)
9
9
  */
10
10
 
11
+ import type { ContributionKey } from './utils/contribution-table.js';
12
+
11
13
  export type LegacyStrategy = 'link' | 'merge' | 'copy';
12
14
  export type SyncStrategy = LegacyStrategy | 'package';
13
15
  export type AccessLevel = 'public' | 'team' | `role:${string}`;
@@ -73,14 +75,13 @@ export interface SyncResult {
73
75
  // covers the entitlement-gated registry flow (`hq packages install <slug>`).
74
76
  // ---------------------------------------------------------------------------
75
77
 
76
- export type PackContributeKey =
77
- | 'workers'
78
- | 'knowledge'
79
- | 'skills'
80
- | 'commands'
81
- | 'hooks'
82
- | 'policies'
83
- | 'scripts';
78
+ /**
79
+ * The `contributes.*` keys a pack may declare. DERIVED (US-003) from the single
80
+ * declarative contribution registry in `utils/contribution-table.ts` -- adding
81
+ * a contribution type is one row there, and this union updates automatically.
82
+ * Do NOT restate the keys here.
83
+ */
84
+ export type PackContributeKey = ContributionKey;
84
85
 
85
86
  /**
86
87
  * Pack authorship attribution (US-001). OPTIONAL and backwards-compatible —
@@ -94,12 +95,30 @@ export interface PackAuthor {
94
95
  displayName: string;
95
96
  }
96
97
 
98
+ /**
99
+ * A pack-to-pack dependency (M0). OPTIONAL and backwards-compatible — packs
100
+ * published before this field omit `requires.packs` and install unchanged. When
101
+ * present, each entry names another content pack that MUST already be installed
102
+ * before this one (enforced at install time by `assertPackDependencies`, which
103
+ * tracks installed packs by FILESYSTEM PRESENCE — not `modules.yaml`). `version`
104
+ * is an optional semver RANGE the installed dependency must satisfy.
105
+ */
106
+ export interface PackDependency {
107
+ name: string; // ^hq-pack-[a-z0-9][a-z0-9-]*$
108
+ version?: string; // optional semver range
109
+ }
110
+
97
111
  export interface PackManifest {
98
112
  name: string; // ^hq-pack-[a-z0-9][a-z0-9-]*$
99
113
  version: string; // semver
100
114
  publisher: string; // @scope
101
115
  access: 'public' | 'private';
102
- requires: { hqCore: string }; // semver range
116
+ /**
117
+ * Host + pack prerequisites. `hqCore` is a required semver RANGE the host HQ
118
+ * must satisfy. `packs` (M0) is an OPTIONAL list of other content packs that
119
+ * must be installed first — see PackDependency.
120
+ */
121
+ requires: { hqCore: string; packs?: PackDependency[] };
103
122
  contributes: Partial<Record<PackContributeKey, string[]>>;
104
123
  description?: string;
105
124
  license?: string;
@@ -0,0 +1,83 @@
1
+ /**
2
+ * The contribution registry -- the SINGLE declarative source of truth (US-003)
3
+ * for the `contributes.* -> host` mapping. Every other surface DERIVES from
4
+ * this table:
5
+ *
6
+ * - `PackContributeKey` (types.ts) = `keyof typeof CONTRIBUTION_TABLE`
7
+ * - `linkFor` / `contributionLinks` read `payload` + `host`
8
+ * - `validateManifest`'s payload check reads `payload`
9
+ * - `core/scripts/scan-packages.sh` reads a data block GENERATED
10
+ * from this table
11
+ * (scripts/generate-scan-packages-table.mjs)
12
+ *
13
+ * Adding a contribution type is ONE row here, not a five-site edit. The parity
14
+ * test (`pack-contributions.test.ts`) asserts every surface agrees on the full
15
+ * key-set, the payload suffix, and the wire mode.
16
+ *
17
+ * Row fields:
18
+ * - `payload`: path INSIDE the pack, with the literal token `{item}` for the
19
+ * declared name. The suffix after `{item}` (e.g. `.md`, `.json`, or none)
20
+ * IS the load-bearing per-key shape.
21
+ * - `host`: for `wire: 'symlink'`, the host DIRECTORY (relative to the HQ
22
+ * root) the symlink is created under -- the symlink dst is
23
+ * `<host>/<expanded-item-basename>`. For `wire: 'merge'`, a non-path
24
+ * SENTINEL describing the merge target (e.g. `merge:claude+codex`); it is
25
+ * NEVER used as a filesystem path.
26
+ * - `wire`: `symlink` (a single `ln -s` into `host`) or `merge` (merged into
27
+ * a shared host config; see US-004/US-005). Symlink-only readers MUST skip
28
+ * `merge` rows.
29
+ *
30
+ * NOTE: the `mcp` row is DECLARED here as data only. US-003 does NOT wire any
31
+ * MCP behavior -- the merge engine lands in US-004/US-005. Declaring it now
32
+ * makes the table the single source so adding the merge wiring is a code change
33
+ * against an already-present row, and the parity test guards it from day one.
34
+ */
35
+
36
+ export type WireMode = 'symlink' | 'merge';
37
+
38
+ export interface ContributionRow {
39
+ /** Path inside the pack, with `{item}` substituted for the declared name. */
40
+ payload: string;
41
+ /**
42
+ * `wire: 'symlink'` -> host directory the symlink lives under (HQ-root
43
+ * relative). `wire: 'merge'` -> a non-path sentinel for the merge target.
44
+ */
45
+ host: string;
46
+ wire: WireMode;
47
+ }
48
+
49
+ /**
50
+ * The 8-key contribution registry. `as const` so `keyof typeof` yields the
51
+ * exact literal key union consumed by `PackContributeKey`.
52
+ */
53
+ export const CONTRIBUTION_TABLE = {
54
+ workers: { payload: 'workers/{item}', host: 'core/workers/public', wire: 'symlink' },
55
+ knowledge: { payload: 'knowledge/{item}', host: 'core/knowledge/public', wire: 'symlink' },
56
+ skills: { payload: 'skills/{item}', host: '.claude/skills', wire: 'symlink' },
57
+ commands: { payload: 'commands/{item}.md', host: '.claude/commands', wire: 'symlink' },
58
+ hooks: { payload: 'hooks/{item}.sh', host: '.claude/hooks', wire: 'symlink' },
59
+ policies: { payload: 'policies/{item}.md', host: 'core/policies', wire: 'symlink' },
60
+ scripts: { payload: 'scripts/{item}', host: 'core/scripts', wire: 'symlink' },
61
+ // wire:merge -- DECLARED as data (US-003); the merge engine ships in
62
+ // US-004/US-005. Symlink readers skip this row.
63
+ mcp: { payload: 'mcp/{item}.json', host: 'merge:claude+codex', wire: 'merge' },
64
+ } as const satisfies Record<string, ContributionRow>;
65
+
66
+ /** The declared contribution keys, derived once from the table. */
67
+ export type ContributionKey = keyof typeof CONTRIBUTION_TABLE;
68
+
69
+ /** All contribution keys, in declaration order. */
70
+ export const CONTRIBUTION_KEYS = Object.keys(CONTRIBUTION_TABLE) as ContributionKey[];
71
+
72
+ /**
73
+ * Expand a row's `payload` template for a concrete item name. Returns the
74
+ * pack-relative path of the contribution's payload (e.g. `commands/foo.md`).
75
+ */
76
+ export function payloadFor(key: ContributionKey, item: string): string {
77
+ return CONTRIBUTION_TABLE[key].payload.replace('{item}', item);
78
+ }
79
+
80
+ /** Keys whose contributions are wired by a host symlink (skip `merge` rows). */
81
+ export const SYMLINK_KEYS = CONTRIBUTION_KEYS.filter(
82
+ (k) => CONTRIBUTION_TABLE[k].wire === 'symlink',
83
+ );