@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.
- package/dist/commands/mcp-registration.d.ts +905 -0
- package/dist/commands/mcp-registration.js +2001 -0
- package/dist/commands/mcp-status.d.ts +130 -0
- package/dist/commands/mcp-status.js +406 -0
- package/dist/commands/onboard-warning.d.ts +7 -0
- package/dist/commands/onboard-warning.js +14 -0
- package/dist/commands/onboard.js +5 -5
- package/dist/commands/pack-install.d.ts +74 -0
- package/dist/commands/pack-install.js +493 -14
- package/dist/commands/packs.js +42 -4
- package/dist/commands/pkg-install.js +5 -2
- package/dist/index.js +7 -2
- package/dist/types.d.ts +26 -1
- package/dist/utils/contribution-table.d.ts +103 -0
- package/dist/utils/contribution-table.js +65 -0
- package/dist/utils/pack-contributions.d.ts +93 -10
- package/dist/utils/pack-contributions.js +140 -48
- package/dist/utils/secrets-cache.d.ts +9 -0
- package/dist/utils/secrets-cache.js +24 -2
- package/dist/utils/version-gate.d.ts +40 -1
- package/dist/utils/version-gate.js +91 -20
- package/package.json +3 -2
- package/scripts/generate-scan-packages-table.mjs +113 -0
- package/src/commands/mcp-registration.test.ts +2787 -0
- package/src/commands/mcp-registration.ts +2612 -0
- package/src/commands/mcp-status.test.ts +483 -0
- package/src/commands/mcp-status.ts +575 -0
- package/src/commands/mcp-status.us011.test.ts +243 -0
- package/src/commands/onboard-warning.test.ts +26 -0
- package/src/commands/onboard-warning.ts +12 -0
- package/src/commands/onboard.ts +4 -7
- package/src/commands/pack-install.test.ts +733 -0
- package/src/commands/pack-install.ts +582 -13
- package/src/commands/packs.ts +45 -1
- package/src/commands/pkg-install.ts +4 -1
- package/src/index.ts +6 -0
- package/src/types.ts +28 -9
- package/src/utils/contribution-table.ts +83 -0
- package/src/utils/pack-contributions.test.ts +310 -25
- package/src/utils/pack-contributions.ts +194 -47
- package/src/utils/secrets-cache.ts +22 -0
- package/src/utils/version-gate.test.ts +122 -0
- package/src/utils/version-gate.ts +109 -13
- package/test/e2e/smoke-install-mcp.sh +113 -0
- package/test/fixtures/hq-pack-smoke-mcp/mcp/smoke-http.json +1 -0
- package/test/fixtures/hq-pack-smoke-mcp/package.yaml +11 -0
package/src/commands/packs.ts
CHANGED
|
@@ -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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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
|
+
);
|