@indigoai-us/hq-cli 5.50.0 → 5.50.1
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/pack-install.d.ts +62 -0
- package/dist/commands/pack-install.js +422 -14
- package/dist/commands/packs.js +28 -4
- package/dist/commands/pkg-install.js +5 -2
- package/dist/index.js +7 -2
- package/dist/types.d.ts +8 -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 +86 -10
- package/dist/utils/pack-contributions.js +130 -48
- package/dist/utils/secrets-cache.d.ts +9 -0
- package/dist/utils/secrets-cache.js +24 -2
- 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/pack-install.test.ts +589 -0
- package/src/commands/pack-install.ts +497 -13
- package/src/commands/packs.ts +26 -1
- package/src/commands/pkg-install.ts +4 -1
- package/src/index.ts +6 -0
- package/src/types.ts +9 -8
- package/src/utils/contribution-table.ts +83 -0
- package/src/utils/pack-contributions.test.ts +257 -25
- package/src/utils/pack-contributions.ts +177 -47
- package/src/utils/secrets-cache.ts +22 -0
- 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
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
* from each pack's package.yaml; rationale lives in the layout-fix PR.)
|
|
35
35
|
*/
|
|
36
36
|
|
|
37
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
37
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="fbfbcf69-d410-51cb-ae59-886c451b3a55")}catch(e){}}();
|
|
38
38
|
import * as fs from 'fs';
|
|
39
39
|
import * as os from 'os';
|
|
40
40
|
import * as path from 'path';
|
|
@@ -48,9 +48,12 @@ import semverValid from 'semver/functions/valid.js';
|
|
|
48
48
|
import semverValidRange from 'semver/ranges/valid.js';
|
|
49
49
|
import semverGt from 'semver/functions/gt.js';
|
|
50
50
|
import { findHqRoot } from '../utils/manifest.js';
|
|
51
|
-
import { readHqVersion } from '../utils/pack-contributions.js';
|
|
51
|
+
import { readHqVersion, routeContribution } from '../utils/pack-contributions.js';
|
|
52
|
+
import { CONTRIBUTION_TABLE, payloadFor } from '../utils/contribution-table.js';
|
|
52
53
|
import { safeExtractTarball } from './safe-extract.js';
|
|
53
54
|
import { vaultApiFetchPublic } from '../utils/vault-api.js';
|
|
55
|
+
import { redactSecrets, SECRET_REDACTION, registerMcpServers, McpManifestError, } from './mcp-registration.js';
|
|
56
|
+
import { readCache, listSecretCacheScopes } from '../utils/secrets-cache.js';
|
|
54
57
|
/** Prefix that routes a source through the HQ marketplace transport (US-006). */
|
|
55
58
|
export const MARKETPLACE_PREFIX = 'marketplace:';
|
|
56
59
|
export function classify(source) {
|
|
@@ -712,6 +715,150 @@ export function verifyArtifact(input) {
|
|
|
712
715
|
}
|
|
713
716
|
}
|
|
714
717
|
// ---------------------------------------------------------------------------
|
|
718
|
+
// MCP per-server manifest validation (US-005, the TS counterpart of the bash
|
|
719
|
+
// `validate_mcp_manifest` arm in core/scripts/scan-packages.sh).
|
|
720
|
+
//
|
|
721
|
+
// The `mcp` contributes key is `wire: 'merge'` — its per-server manifests are
|
|
722
|
+
// NEVER symlinked; they are validated here and (US-006) merged into the agent
|
|
723
|
+
// configs via registerMcpServers. This function MIRRORS the bash transport
|
|
724
|
+
// rules hand-coded against the draft-07 schema
|
|
725
|
+
// (repos/public/knowledge-hq-core/mcp-manifest.schema.json). There is no `ajv`
|
|
726
|
+
// dependency in hq-cli; the schema file is single-source documentation, so the
|
|
727
|
+
// rules are replicated here (dependency-free, robust if the schema is absent).
|
|
728
|
+
// ---------------------------------------------------------------------------
|
|
729
|
+
/** Allowed top-level keys (schema `additionalProperties: false`). */
|
|
730
|
+
const MCP_ALLOWED_KEYS = ['type', 'url', 'headers', 'command', 'args', 'env', 'tools'];
|
|
731
|
+
/** Schema `$id` for an at-a-glance error reference (kept inline; no file read). */
|
|
732
|
+
const MCP_SCHEMA_ID = 'mcp-manifest.schema.json';
|
|
733
|
+
/** True iff a header/env value carries a `${secret:NAME}` reference. */
|
|
734
|
+
function hasSecretRef(value) {
|
|
735
|
+
return /\$\{secret:/.test(value);
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Validate that a record (`headers` or `env`) is an object of STRING values,
|
|
739
|
+
* pushing one error per offending entry into `errs`. Mirrors the schema's
|
|
740
|
+
* `additionalProperties: { type: 'string' }`.
|
|
741
|
+
*/
|
|
742
|
+
function checkStringMap(errs, label, value) {
|
|
743
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
744
|
+
errs.push(`${label} must be an object`);
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
for (const [k, v] of Object.entries(value)) {
|
|
748
|
+
if (typeof v !== 'string') {
|
|
749
|
+
errs.push(`${label} "${k}" value must be a string`);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Parse + shape-validate one pack's per-server MCP manifest (`mcp/{item}.json`),
|
|
755
|
+
* mirroring the bash `validate_mcp_manifest` arm. EXPORTED so the acceptance
|
|
756
|
+
* test (and US-006's registration engine) can call it directly.
|
|
757
|
+
*
|
|
758
|
+
* Throws an `Error` (whose message names the pack-relative payload path, e.g.
|
|
759
|
+
* `mcp/foo.json`) when the file does not parse as JSON or violates a transport
|
|
760
|
+
* rule:
|
|
761
|
+
* - `type` required, one of `http|stdio|sse`;
|
|
762
|
+
* - no unknown top-level keys (allowed: {@link MCP_ALLOWED_KEYS});
|
|
763
|
+
* - http/sse: `url` required + `^https?://`; `command`/`args`/`env` forbidden;
|
|
764
|
+
* - stdio: non-empty `command` required; `url`/`headers` forbidden;
|
|
765
|
+
* - `headers`/`env` (if present) are objects of string values;
|
|
766
|
+
* - NO inline literal `Bearer ` secret — such a value MUST carry a
|
|
767
|
+
* `${secret:NAME}` reference, never a literal token.
|
|
768
|
+
*
|
|
769
|
+
* @param payloadDir the pack payload root (holds `mcp/{item}.json`)
|
|
770
|
+
* @param item the bare server name declared under `contributes.mcp`
|
|
771
|
+
*/
|
|
772
|
+
export function validateMcpManifest(payloadDir, item) {
|
|
773
|
+
const rel = payloadFor('mcp', item); // mcp/{item}.json
|
|
774
|
+
const abs = path.join(payloadDir, rel);
|
|
775
|
+
// EXISTS — the existsSync gate in validateManifest already covers this, but
|
|
776
|
+
// re-check so the helper is safe to call standalone (tests, US-006).
|
|
777
|
+
if (!fs.existsSync(abs)) {
|
|
778
|
+
throw new Error(`MCP manifest missing: ${rel} (declared under contributes.mcp but no such file)`);
|
|
779
|
+
}
|
|
780
|
+
// PARSE — a non-JSON file is a hard error that names the file.
|
|
781
|
+
let doc;
|
|
782
|
+
try {
|
|
783
|
+
doc = JSON.parse(fs.readFileSync(abs, 'utf-8'));
|
|
784
|
+
}
|
|
785
|
+
catch (e) {
|
|
786
|
+
throw new Error(`MCP manifest malformed: ${rel} is not valid JSON (failed to parse: ${e.message})`);
|
|
787
|
+
}
|
|
788
|
+
if (doc === null || typeof doc !== 'object' || Array.isArray(doc)) {
|
|
789
|
+
throw new Error(`MCP manifest malformed: ${rel} must be a JSON object`);
|
|
790
|
+
}
|
|
791
|
+
const m = doc;
|
|
792
|
+
const errs = [];
|
|
793
|
+
// type: required + enum.
|
|
794
|
+
const type = m.type;
|
|
795
|
+
if (!('type' in m)) {
|
|
796
|
+
errs.push('missing required field: type');
|
|
797
|
+
}
|
|
798
|
+
else if (type !== 'http' && type !== 'stdio' && type !== 'sse') {
|
|
799
|
+
errs.push(`type must be one of http|stdio|sse (got: ${JSON.stringify(type)})`);
|
|
800
|
+
}
|
|
801
|
+
// No unknown top-level keys (schema additionalProperties: false).
|
|
802
|
+
for (const k of Object.keys(m)) {
|
|
803
|
+
if (!MCP_ALLOWED_KEYS.includes(k)) {
|
|
804
|
+
errs.push(`unknown top-level key: ${k}`);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
// http / sse transport: url required (^https?://); command/args/env forbidden.
|
|
808
|
+
if (type === 'http' || type === 'sse') {
|
|
809
|
+
if (!('url' in m)) {
|
|
810
|
+
errs.push(`${type} transport requires a url`);
|
|
811
|
+
}
|
|
812
|
+
else if (typeof m.url !== 'string' || !/^https?:\/\//.test(m.url)) {
|
|
813
|
+
errs.push('url must be an absolute http(s) URL');
|
|
814
|
+
}
|
|
815
|
+
for (const forbidden of ['command', 'args', 'env']) {
|
|
816
|
+
if (forbidden in m)
|
|
817
|
+
errs.push(`${type} transport forbids: ${forbidden}`);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
// stdio transport: non-empty command required; url/headers forbidden.
|
|
821
|
+
if (type === 'stdio') {
|
|
822
|
+
if (typeof m.command !== 'string' || m.command.length === 0) {
|
|
823
|
+
errs.push('stdio transport requires a non-empty command');
|
|
824
|
+
}
|
|
825
|
+
for (const forbidden of ['url', 'headers']) {
|
|
826
|
+
if (forbidden in m)
|
|
827
|
+
errs.push(`stdio transport forbids: ${forbidden}`);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
// headers / env values must be strings.
|
|
831
|
+
if ('headers' in m)
|
|
832
|
+
checkStringMap(errs, 'headers', m.headers);
|
|
833
|
+
if ('env' in m)
|
|
834
|
+
checkStringMap(errs, 'env', m.env);
|
|
835
|
+
if (errs.length > 0) {
|
|
836
|
+
throw new Error(`MCP manifest fails shape validation: ${rel} (schema: ${MCP_SCHEMA_ID})\n` +
|
|
837
|
+
errs.map((e) => ` - ${e}`).join('\n'));
|
|
838
|
+
}
|
|
839
|
+
// Reject INLINE LITERAL Bearer secrets. A header/env value containing
|
|
840
|
+
// 'Bearer ' MUST use a ${secret:NAME} reference, never a literal token baked
|
|
841
|
+
// into a synced/committed artifact.
|
|
842
|
+
const bad = [];
|
|
843
|
+
for (const [label, mapVal] of [
|
|
844
|
+
['header', m.headers],
|
|
845
|
+
['env', m.env],
|
|
846
|
+
]) {
|
|
847
|
+
if (mapVal === null || typeof mapVal !== 'object' || Array.isArray(mapVal))
|
|
848
|
+
continue;
|
|
849
|
+
for (const [k, v] of Object.entries(mapVal)) {
|
|
850
|
+
if (typeof v === 'string' && v.includes('Bearer ') && !hasSecretRef(v)) {
|
|
851
|
+
bad.push(`${label} "${k}"`);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
if (bad.length > 0) {
|
|
856
|
+
throw new Error(`MCP manifest contains an inline literal Bearer secret: ${rel} (${bad.join(', ')}) — ` +
|
|
857
|
+
'a \'Bearer \' header/env value MUST use a ${secret:NAME} reference, never a literal token ' +
|
|
858
|
+
'(a resolved secret is never written to a synced/committed artifact)');
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
// ---------------------------------------------------------------------------
|
|
715
862
|
// Manifest validation (spec §Validation, 10 checks)
|
|
716
863
|
// ---------------------------------------------------------------------------
|
|
717
864
|
export function validateManifest(payloadDir, hqVersion) {
|
|
@@ -766,25 +913,32 @@ export function validateManifest(payloadDir, hqVersion) {
|
|
|
766
913
|
if (!nonEmpty) {
|
|
767
914
|
throw new Error('contributes must have at least one non-empty subfield');
|
|
768
915
|
}
|
|
769
|
-
// 10. payload files exist (hooks check happens separately in step 8)
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
skills: (i) => path.join('skills', i),
|
|
774
|
-
commands: (i) => path.join('commands', `${i}.md`),
|
|
775
|
-
hooks: (i) => path.join('hooks', `${i}.sh`),
|
|
776
|
-
policies: (i) => path.join('policies', `${i}.md`),
|
|
777
|
-
scripts: (i) => path.join('scripts', i),
|
|
778
|
-
};
|
|
916
|
+
// 10. payload files exist (hooks check happens separately in step 8).
|
|
917
|
+
// Payload paths are READ from the single-source CONTRIBUTION_TABLE (US-003)
|
|
918
|
+
// via payloadFor -- no restated `subpaths` record. An unknown key (not in the
|
|
919
|
+
// table) is rejected here so a typo can't silently install unwired.
|
|
779
920
|
for (const [key, items] of Object.entries(contributes)) {
|
|
921
|
+
if (!(key in CONTRIBUTION_TABLE)) {
|
|
922
|
+
throw new Error(`contributes.${key} is not a known contribution type`);
|
|
923
|
+
}
|
|
780
924
|
if (!Array.isArray(items))
|
|
781
925
|
continue;
|
|
782
926
|
for (const item of items) {
|
|
783
|
-
const rel =
|
|
927
|
+
const rel = payloadFor(key, item);
|
|
784
928
|
const abs = path.join(payloadDir, rel);
|
|
785
929
|
if (!fs.existsSync(abs)) {
|
|
786
930
|
throw new Error(`contributes.${key} declares "${item}" but payload file missing: ${rel}`);
|
|
787
931
|
}
|
|
932
|
+
// The `mcp` key (wire: 'merge') is NOT existsSync-only: after the file is
|
|
933
|
+
// confirmed present it must PARSE + SHAPE-validate against the per-server
|
|
934
|
+
// MCP schema (US-005), mirroring the bash validate_mcp_manifest arm. Other
|
|
935
|
+
// (symlink) keys keep their existsSync-only behavior. We dispatch on the
|
|
936
|
+
// ROUTE from the single-source table (not a hardcoded key) so the merge
|
|
937
|
+
// path is table-driven; the merge itself (registering into the agent
|
|
938
|
+
// configs via registerMcpServers, US-006) is NEVER a symlink.
|
|
939
|
+
if (routeContribution(key) === 'merge') {
|
|
940
|
+
validateMcpManifest(payloadDir, item);
|
|
941
|
+
}
|
|
788
942
|
}
|
|
789
943
|
}
|
|
790
944
|
// author + capabilities (US-001) — both OPTIONAL and backwards-compatible.
|
|
@@ -901,6 +1055,122 @@ async function confirmHooks(pkg, allowHooks) {
|
|
|
901
1055
|
return /^(y|yes)$/i.test(answer.trim());
|
|
902
1056
|
}
|
|
903
1057
|
// ---------------------------------------------------------------------------
|
|
1058
|
+
// MCP confirmation (US-010)
|
|
1059
|
+
//
|
|
1060
|
+
// The MCP equivalent of confirmHooks. MCP servers are a high-trust surface: an
|
|
1061
|
+
// http/sse server points your agent at a REMOTE endpoint (typosquat / phishing
|
|
1062
|
+
// risk — the FULL url is disclosed verbatim so the operator can eyeball it), and
|
|
1063
|
+
// a stdio server runs a LOCAL binary with your shell permissions. We GATE on a
|
|
1064
|
+
// non-empty `contributes.mcp` (NEVER the advisory `capabilities` field, which is
|
|
1065
|
+
// reserved/unenforced — gating on it would let a pack omit a capability to dodge
|
|
1066
|
+
// the prompt) and render per-server transport + url-or-command + a count of ALL
|
|
1067
|
+
// servers being added.
|
|
1068
|
+
//
|
|
1069
|
+
// REDACTION: header/env VALUES are never printed. The manifests use
|
|
1070
|
+
// `${secret:NAME}` references (never literal Bearers), but we STILL run the
|
|
1071
|
+
// rendered prompt through `redactSecrets` against the raw header/env values so
|
|
1072
|
+
// that even a LITERAL secret could not leak. We show header KEY names with a
|
|
1073
|
+
// redacted value marker.
|
|
1074
|
+
//
|
|
1075
|
+
// FAIL-SAFE: a manifest that fails to load/parse here is NOT a bypass — we
|
|
1076
|
+
// surface the server name with a `[manifest unreadable]` note and STILL require
|
|
1077
|
+
// confirmation (a malformed manifest should make the operator MORE cautious).
|
|
1078
|
+
// ---------------------------------------------------------------------------
|
|
1079
|
+
/**
|
|
1080
|
+
* Render one declared MCP server's prompt line, redacting every header/env value.
|
|
1081
|
+
* EXPORTED so the acceptance/redaction self-test can assert no secret/Bearer
|
|
1082
|
+
* substring ever appears in the rendered output.
|
|
1083
|
+
*/
|
|
1084
|
+
export function renderMcpServerLine(payloadDir, item) {
|
|
1085
|
+
let manifest;
|
|
1086
|
+
try {
|
|
1087
|
+
const abs = path.join(payloadDir, payloadFor('mcp', item));
|
|
1088
|
+
manifest = JSON.parse(fs.readFileSync(abs, 'utf-8'));
|
|
1089
|
+
if (manifest === null || typeof manifest !== 'object')
|
|
1090
|
+
throw new Error('not an object');
|
|
1091
|
+
}
|
|
1092
|
+
catch {
|
|
1093
|
+
// Fail-safe: do not crash the prompt; flag the server and keep requiring
|
|
1094
|
+
// confirmation.
|
|
1095
|
+
return ` - ${item} [manifest unreadable] — could not load mcp/${item}.json; treat with caution`;
|
|
1096
|
+
}
|
|
1097
|
+
const transport = manifest.type;
|
|
1098
|
+
// Accumulate every raw header/env value so a final redactSecrets pass scrubs
|
|
1099
|
+
// even a literal token that slipped past `${secret:}` (defense in depth — we
|
|
1100
|
+
// already replace the values with the redaction marker below).
|
|
1101
|
+
const rawValues = new Set();
|
|
1102
|
+
for (const v of Object.values(manifest.headers ?? {}))
|
|
1103
|
+
rawValues.add(v);
|
|
1104
|
+
for (const v of Object.values(manifest.env ?? {}))
|
|
1105
|
+
rawValues.add(v);
|
|
1106
|
+
// Header KEYS shown, VALUES always redacted (never the raw value).
|
|
1107
|
+
const headerKeys = Object.keys(manifest.headers ?? {});
|
|
1108
|
+
const headerStr = headerKeys.length > 0
|
|
1109
|
+
? ` headers: { ${headerKeys.map((k) => `${k}: ${SECRET_REDACTION}`).join(', ')} }`
|
|
1110
|
+
: '';
|
|
1111
|
+
let line;
|
|
1112
|
+
if (transport === 'http' || transport === 'sse') {
|
|
1113
|
+
// FULL url verbatim — typosquat disclosure is the whole point.
|
|
1114
|
+
line =
|
|
1115
|
+
` - ${item} [${transport}] url: ${manifest.url ?? '(missing)'}` +
|
|
1116
|
+
headerStr +
|
|
1117
|
+
` (contacts a remote endpoint)`;
|
|
1118
|
+
}
|
|
1119
|
+
else {
|
|
1120
|
+
// stdio — local binary with shell permissions.
|
|
1121
|
+
const args = manifest.args ?? [];
|
|
1122
|
+
const argsStr = args.length > 0 ? ` args: [${args.join(', ')}]` : '';
|
|
1123
|
+
line =
|
|
1124
|
+
` - ${item} [stdio] command: ${manifest.command ?? '(missing)'}` +
|
|
1125
|
+
argsStr +
|
|
1126
|
+
headerStr +
|
|
1127
|
+
` (runs a local binary with your shell permissions)`;
|
|
1128
|
+
}
|
|
1129
|
+
// Final defense-in-depth scrub: even though values are already replaced with
|
|
1130
|
+
// the marker above, run the whole line through redactSecrets so a stray
|
|
1131
|
+
// literal token (e.g. inside command/args) can never reach the terminal.
|
|
1132
|
+
return redactSecrets(line, rawValues);
|
|
1133
|
+
}
|
|
1134
|
+
/**
|
|
1135
|
+
* Install-time MCP trust prompt. EXPORTED so the acceptance test can drive the
|
|
1136
|
+
* gate / bypass / non-TTY branches directly (capture stdout, assert no secret
|
|
1137
|
+
* substring leaks, assert deny returns false). See {@link confirmHooks} for the
|
|
1138
|
+
* voice/shape this mirrors.
|
|
1139
|
+
*/
|
|
1140
|
+
export async function confirmMcp(pkg, payloadDir, allowMcp) {
|
|
1141
|
+
// GATE on non-empty `contributes.mcp` — mirrors confirmHooks. Never on the
|
|
1142
|
+
// advisory `capabilities` field.
|
|
1143
|
+
const servers = pkg.contributes.mcp ?? [];
|
|
1144
|
+
if (servers.length === 0)
|
|
1145
|
+
return true;
|
|
1146
|
+
if (allowMcp) {
|
|
1147
|
+
console.log(chalk.yellow(`--allow-mcp set; registering ${servers.length} MCP server(s) without prompting.`));
|
|
1148
|
+
return true;
|
|
1149
|
+
}
|
|
1150
|
+
console.log('');
|
|
1151
|
+
console.log(chalk.yellow(`Pack ${pkg.publisher}/${pkg.name} declares ${servers.length} MCP server(s):`));
|
|
1152
|
+
for (const item of servers) {
|
|
1153
|
+
console.log(chalk.yellow(renderMcpServerLine(payloadDir, item)));
|
|
1154
|
+
}
|
|
1155
|
+
console.log(chalk.yellow('These servers will be registered into your Claude and Codex agent configs.'));
|
|
1156
|
+
// Non-TTY guard (mirrors the secrets.ts pattern): do NOT call rl.question in a
|
|
1157
|
+
// non-interactive shell — it would hang. Instruct the operator to re-run with
|
|
1158
|
+
// --allow-mcp and DENY (return false -> abort, no partial state).
|
|
1159
|
+
if (!process.stdin.isTTY) {
|
|
1160
|
+
console.log(chalk.red(`Refusing to register ${servers.length} MCP server(s) without confirmation ` +
|
|
1161
|
+
'in a non-interactive shell. Re-run with --allow-mcp to approve.'));
|
|
1162
|
+
return false;
|
|
1163
|
+
}
|
|
1164
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1165
|
+
const answer = await new Promise((resolve) => {
|
|
1166
|
+
rl.question('Install anyway? [y/N] ', (a) => {
|
|
1167
|
+
rl.close();
|
|
1168
|
+
resolve(a);
|
|
1169
|
+
});
|
|
1170
|
+
});
|
|
1171
|
+
return /^(y|yes)$/i.test(answer.trim());
|
|
1172
|
+
}
|
|
1173
|
+
// ---------------------------------------------------------------------------
|
|
904
1174
|
// Conditional predicate
|
|
905
1175
|
//
|
|
906
1176
|
// `package.yaml:conditional` is arbitrary bash sourced from a remote pack.
|
|
@@ -935,6 +1205,119 @@ function evalConditional(expr) {
|
|
|
935
1205
|
return r.status === 0;
|
|
936
1206
|
}
|
|
937
1207
|
// ---------------------------------------------------------------------------
|
|
1208
|
+
// MCP registration wiring (US-007) — call registerMcpServers from installPack.
|
|
1209
|
+
//
|
|
1210
|
+
// The registration CORE + Claude/Codex emitters live in mcp-registration.ts and
|
|
1211
|
+
// are fully unit-tested; this is the install-time SEAM that feeds them a real
|
|
1212
|
+
// manifest loader and a production, vault-backed secret resolver, with per-server
|
|
1213
|
+
// secret-deferral so a fresh install (before `/connect-shopify` has minted the
|
|
1214
|
+
// key) still SUCCEEDS, skipping only the server whose secret is not yet present.
|
|
1215
|
+
// ---------------------------------------------------------------------------
|
|
1216
|
+
/**
|
|
1217
|
+
* Build the install-time {@link SecretResolver} bound to the HQ vault's local
|
|
1218
|
+
* secrets-cache (`~/.hq/secrets-cache/<scope>/<NAME>`, AES-256-GCM, 0600, TTL'd).
|
|
1219
|
+
*
|
|
1220
|
+
* Active-company resolution at install time is INDIRECT by design: `hq install`
|
|
1221
|
+
* has no `--company` flag and runs OFFLINE (no token), so we cannot resolve a
|
|
1222
|
+
* single active company UID the way `hq run` / `hq secrets` do (via
|
|
1223
|
+
* `getEntityUid` over the network). Instead we probe EVERY cached scope
|
|
1224
|
+
* (`cmp_*`/`prs_*` — whichever has minted secrets locally) for the requested
|
|
1225
|
+
* name and return the first hit. This naturally resolves to whichever company
|
|
1226
|
+
* context just provisioned the secret (e.g. the one `/connect-shopify` minted
|
|
1227
|
+
* `VYG_API_KEY` under), without guessing, and works whether the vault scoped the
|
|
1228
|
+
* key under a company or person entity.
|
|
1229
|
+
*
|
|
1230
|
+
* On MISS across all scopes (no cache, expired TTL, or key never minted) it
|
|
1231
|
+
* returns `null` — which is exactly what {@link registerMcpServers}'s
|
|
1232
|
+
* unresolvable-secret path keys off to defer that server gracefully. With no
|
|
1233
|
+
* cached scopes at all (`listSecretCacheScopes()` → `[]`) it simply returns
|
|
1234
|
+
* `null` for every name, the desired graceful-deferral behavior.
|
|
1235
|
+
*/
|
|
1236
|
+
export function makeInstallSecretResolver() {
|
|
1237
|
+
return (name) => {
|
|
1238
|
+
for (const scope of listSecretCacheScopes()) {
|
|
1239
|
+
const value = readCache(scope, name);
|
|
1240
|
+
if (value !== null)
|
|
1241
|
+
return value;
|
|
1242
|
+
}
|
|
1243
|
+
return null;
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
/**
|
|
1247
|
+
* Load + shape-validate + parse a pack's per-server MCP manifest from disk.
|
|
1248
|
+
* `validateMcpManifest(destDir, name)` is the SAME shape gate `validateManifest`
|
|
1249
|
+
* runs at install; it returns void and THROWS on bad shape, so we call it first
|
|
1250
|
+
* (re-using one source of truth), then parse `mcp/<name>.json` and return it
|
|
1251
|
+
* typed as {@link McpManifest}. `destDir` is the installed pack root
|
|
1252
|
+
* (`core/packages/<pkg>/`), the realpath `registerMcpServers` emits from.
|
|
1253
|
+
*/
|
|
1254
|
+
function loadMcpManifestFrom(destDir, name) {
|
|
1255
|
+
validateMcpManifest(destDir, name); // throws on bad shape (void on success)
|
|
1256
|
+
const abs = path.join(destDir, 'mcp', `${name}.json`);
|
|
1257
|
+
return JSON.parse(fs.readFileSync(abs, 'utf-8'));
|
|
1258
|
+
}
|
|
1259
|
+
/**
|
|
1260
|
+
* Derive the per-server "needs secret" remedy command from the pack's
|
|
1261
|
+
* `initialization.entrypoint` (e.g. the vyg pack's `connect-shopify` →
|
|
1262
|
+
* `/connect-shopify`). Falls back to a generic "provision the secret then re-run
|
|
1263
|
+
* `hq install`" when the pack declares no entrypoint.
|
|
1264
|
+
*/
|
|
1265
|
+
function mcpSecretRemedy(initialization) {
|
|
1266
|
+
const entrypoint = initialization?.entrypoint;
|
|
1267
|
+
if (typeof entrypoint === 'string' && entrypoint.trim() !== '') {
|
|
1268
|
+
const command = '/' + entrypoint.trim().replace(/^\/+/, '');
|
|
1269
|
+
return `run \`${command}\` to provision it, then re-run \`hq install\``;
|
|
1270
|
+
}
|
|
1271
|
+
return 'provision the secret then re-run `hq install`';
|
|
1272
|
+
}
|
|
1273
|
+
/** Pull the `${secret:NAME}` token out of a `cannot resolve ${secret:NAME}` error message. */
|
|
1274
|
+
function extractDeferredSecretName(message) {
|
|
1275
|
+
const m = /\$\{secret:([^}]+)\}/.exec(message);
|
|
1276
|
+
return m ? m[1] : 'a required secret';
|
|
1277
|
+
}
|
|
1278
|
+
/**
|
|
1279
|
+
* Register one pack's `contributes.mcp` servers into the shared Claude/Codex
|
|
1280
|
+
* agent configs, PER SERVER, so a single unresolvable `${secret:NAME}` defers
|
|
1281
|
+
* ONLY that server instead of aborting the whole install. Returns the registered
|
|
1282
|
+
* and skipped (deferred) server-name lists for the one-line summary.
|
|
1283
|
+
*
|
|
1284
|
+
* Per-server policy:
|
|
1285
|
+
* - SUCCESS → push to `registered`.
|
|
1286
|
+
* - McpManifestError matching
|
|
1287
|
+
* `/cannot resolve \$\{secret:/` → SKIP (push to `skipped`), warn on stderr,
|
|
1288
|
+
* install still succeeds (the key gets minted later, then a re-install wires it).
|
|
1289
|
+
* - ANY OTHER error → RE-THROW (abort install). Only the
|
|
1290
|
+
* unresolvable-secret case is swallowed.
|
|
1291
|
+
*/
|
|
1292
|
+
function wireMcpServers(pkg, destDir) {
|
|
1293
|
+
const resolveSecret = makeInstallSecretResolver();
|
|
1294
|
+
const loadManifest = (name) => loadMcpManifestFrom(destDir, name);
|
|
1295
|
+
const registered = [];
|
|
1296
|
+
const skipped = [];
|
|
1297
|
+
for (const name of pkg.contributes.mcp ?? []) {
|
|
1298
|
+
try {
|
|
1299
|
+
// Per-server call: registerMcpServers throws on the FIRST unresolvable
|
|
1300
|
+
// secret, so calling it one name at a time lets us catch + continue.
|
|
1301
|
+
registerMcpServers(pkg.name, [name], { loadManifest, resolveSecret });
|
|
1302
|
+
registered.push(name);
|
|
1303
|
+
}
|
|
1304
|
+
catch (e) {
|
|
1305
|
+
const isUnresolvableSecret = e instanceof McpManifestError && /cannot resolve \$\{secret:/.test(e.message);
|
|
1306
|
+
if (!isUnresolvableSecret) {
|
|
1307
|
+
// ConfigParseError / ConfigPermissionError / McpNameCollisionError / a
|
|
1308
|
+
// malformed-manifest McpManifestError / etc. — propagate, abort install.
|
|
1309
|
+
throw e;
|
|
1310
|
+
}
|
|
1311
|
+
skipped.push(name);
|
|
1312
|
+
const secret = extractDeferredSecretName(e.message);
|
|
1313
|
+
// Always stderr (never suppressed by --quiet): the user must see WHY a
|
|
1314
|
+
// server was deferred and exactly how to finish wiring it.
|
|
1315
|
+
process.stderr.write(`MCP server '${name}' needs secret ${secret} — ${mcpSecretRemedy(pkg.initialization)}.\n`);
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
return { registered, skipped };
|
|
1319
|
+
}
|
|
1320
|
+
// ---------------------------------------------------------------------------
|
|
938
1321
|
// Move into core/packages/ + run core/scripts/scan-packages.sh
|
|
939
1322
|
// ---------------------------------------------------------------------------
|
|
940
1323
|
/**
|
|
@@ -1116,6 +1499,17 @@ export async function installPack(source, opts = {}) {
|
|
|
1116
1499
|
say(chalk.red('Install aborted (hooks denied).'));
|
|
1117
1500
|
return;
|
|
1118
1501
|
}
|
|
1502
|
+
// US-010 — install-time MCP trust prompt. Positioned AFTER confirmHooks and
|
|
1503
|
+
// BEFORE installToPackages so a deny aborts with NO partial state (no config
|
|
1504
|
+
// written, no pack moved into core/packages, no registerMcpServers call). The
|
|
1505
|
+
// actual registration (US-007) now fires AFTER installToPackages +
|
|
1506
|
+
// runScanPackages below (pack on disk + symlinks wired), once this gate has
|
|
1507
|
+
// confirmed the servers — so a deny here means registration never runs.
|
|
1508
|
+
const mcpConfirmed = await confirmMcp(pkg, fetched.payloadDir, opts.allowMcp ?? false);
|
|
1509
|
+
if (!mcpConfirmed) {
|
|
1510
|
+
say(chalk.red('Install aborted (MCP servers denied).'));
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1119
1513
|
const destDir = installToPackages(fetched.payloadDir, pkg, hqRoot);
|
|
1120
1514
|
// Under the v12+ HQ layout, packs live at `core/packages/<name>/` and
|
|
1121
1515
|
// are tracked by filesystem presence — no `modules.yaml` write (that
|
|
@@ -1133,6 +1527,20 @@ export async function installPack(source, opts = {}) {
|
|
|
1133
1527
|
const stampedSource = transport === 'marketplace' ? fetched.resolvedSource : source;
|
|
1134
1528
|
stampInstallSource(destDir, stampedSource);
|
|
1135
1529
|
runScanPackages(hqRoot, { quiet: opts.quiet });
|
|
1530
|
+
// US-007 — register `contributes.mcp` servers into the shared Claude/Codex
|
|
1531
|
+
// agent configs. Fires HERE (after installToPackages + runScanPackages, i.e.
|
|
1532
|
+
// pack on disk + symlinks wired) and only once the confirmMcp gate above
|
|
1533
|
+
// approved them. The `mcp` key is wire:'merge' — NEVER symlinked; this merge
|
|
1534
|
+
// is its only wiring path. registerMcpServers honors the
|
|
1535
|
+
// HQ_DISABLE_MCP_REGISTRATION kill-switch internally, so we never re-check it.
|
|
1536
|
+
// Per-server secret-deferral keeps a fresh install (key not yet minted) at
|
|
1537
|
+
// exit 0, deferring only the unresolvable server (see wireMcpServers).
|
|
1538
|
+
if (Array.isArray(pkg.contributes.mcp) && pkg.contributes.mcp.length > 0) {
|
|
1539
|
+
const { registered, skipped } = wireMcpServers(pkg, destDir);
|
|
1540
|
+
// One-line summary (server NAMES only — never resolved secret VALUES).
|
|
1541
|
+
say(chalk.dim(` MCP servers: registered [${registered.join(', ')}]; ` +
|
|
1542
|
+
`skipped [${skipped.join(', ')}].`));
|
|
1543
|
+
}
|
|
1136
1544
|
say(chalk.green(`\nOK Installed ${pkg.name}@${pkg.version} -> ${path.relative(hqRoot, destDir)}/`));
|
|
1137
1545
|
say(chalk.dim(` Wired ${Object.values(pkg.contributes).flat().filter(Boolean).length} ` +
|
|
1138
1546
|
`contribution(s) into host-side paths.`));
|
|
@@ -1150,4 +1558,4 @@ export async function installPack(source, opts = {}) {
|
|
|
1150
1558
|
}
|
|
1151
1559
|
}
|
|
1152
1560
|
//# sourceMappingURL=pack-install.js.map
|
|
1153
|
-
//# debugId=
|
|
1561
|
+
//# debugId=fbfbcf69-d410-51cb-ae59-886c451b3a55
|
package/dist/commands/packs.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* Spec: knowledge/public/hq-core/package-yaml-spec.md.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
21
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="570f7491-8d05-577c-8bae-6f6cf7dfb2ec")}catch(e){}}();
|
|
22
22
|
import * as fs from 'fs';
|
|
23
23
|
import * as path from 'path';
|
|
24
24
|
import * as readline from 'readline';
|
|
@@ -27,7 +27,7 @@ import chalk from 'chalk';
|
|
|
27
27
|
import semverSatisfies from 'semver/functions/satisfies.js';
|
|
28
28
|
import { findHqRoot } from '../utils/manifest.js';
|
|
29
29
|
import { classify, resolveLatest, resolveLatestMarketplace, runScanPackages, installPack, } from './pack-install.js';
|
|
30
|
-
import { contributionLinks, linkStatus, listInstalledPacks, readPackManifest, unwirePack, readHqVersion, readRecommendedPackages, packagesDir, } from '../utils/pack-contributions.js';
|
|
30
|
+
import { contributionLinks, linkStatus, listInstalledPacks, readPackManifest, unwirePack, unwirePackMcp, readHqVersion, readRecommendedPackages, packagesDir, } from '../utils/pack-contributions.js';
|
|
31
31
|
function resolveRoot(opts) {
|
|
32
32
|
return opts.hqRoot ? path.resolve(opts.hqRoot) : findHqRoot();
|
|
33
33
|
}
|
|
@@ -246,6 +246,7 @@ async function runUpdate(name, opts) {
|
|
|
246
246
|
try {
|
|
247
247
|
await installPack(source, {
|
|
248
248
|
allowHooks: opts.yes || opts.allowHooks,
|
|
249
|
+
allowMcp: opts.yes || opts.allowMcp,
|
|
249
250
|
followBranch: opts.branch,
|
|
250
251
|
quiet: wantsJson(opts),
|
|
251
252
|
});
|
|
@@ -279,6 +280,28 @@ async function runUninstall(name, opts) {
|
|
|
279
280
|
}
|
|
280
281
|
// 1. Un-wire only our symlinks.
|
|
281
282
|
const { unlinked, skipped } = unwirePack(hqRoot, packDir, contributes);
|
|
283
|
+
// 1b. Un-register the pack's MCP (`wire: 'merge'`) servers — invisible to the
|
|
284
|
+
// symlink unwire above. Provenance-scoped: removes ONLY entries stamped with this
|
|
285
|
+
// pack's `_hqPack`, and skip-and-warns on any foreign/unstamped same-named entry.
|
|
286
|
+
// Tolerant of a Codex-less host / absent config; idempotent on re-run.
|
|
287
|
+
try {
|
|
288
|
+
const mcp = unwirePackMcp(name, contributes);
|
|
289
|
+
for (const server of mcp.servers) {
|
|
290
|
+
if ('skipped' in server.claude)
|
|
291
|
+
continue; // (claude is always inspected; never skipped)
|
|
292
|
+
if (server.claude.outcome === 'skipped-foreign' && server.claude.reason) {
|
|
293
|
+
warnings.push(server.claude.reason);
|
|
294
|
+
}
|
|
295
|
+
if (!('skipped' in server.codex) && server.codex.outcome === 'skipped-foreign' && server.codex.reason) {
|
|
296
|
+
warnings.push(server.codex.reason);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
catch (e) {
|
|
301
|
+
// Never let an MCP un-registration failure abort the rest of the uninstall
|
|
302
|
+
// (symlink unwire already ran; the pack dir still gets archived). Surface it.
|
|
303
|
+
warnings.push(`MCP un-registration encountered an error: ${e.message}`);
|
|
304
|
+
}
|
|
282
305
|
// 2. Archive (or delete) the pack dir -- BEFORE re-scan so it isn't re-wired.
|
|
283
306
|
let archived = null;
|
|
284
307
|
if (opts.archive === false) {
|
|
@@ -358,8 +381,9 @@ export function registerPacksCommand(parent) {
|
|
|
358
381
|
.option('--json', 'Machine-readable JSON output')
|
|
359
382
|
.option('--hq-root <path>', 'HQ root (default: auto-detect)')
|
|
360
383
|
.option('--check-only', 'Report availability without installing')
|
|
361
|
-
.option('-y, --yes', 'Non-interactive (implies --allow-hooks)')
|
|
384
|
+
.option('-y, --yes', 'Non-interactive (implies --allow-hooks and --allow-mcp)')
|
|
362
385
|
.option('--allow-hooks', 'Install pack hooks without prompting')
|
|
386
|
+
.option('--allow-mcp', 'Register pack MCP servers without prompting')
|
|
363
387
|
.option('--branch', 'Follow the source branch instead of SHA-pinning')
|
|
364
388
|
.action(async (name, opts) => {
|
|
365
389
|
try {
|
|
@@ -425,4 +449,4 @@ export function registerPacksCommand(parent) {
|
|
|
425
449
|
});
|
|
426
450
|
}
|
|
427
451
|
//# sourceMappingURL=packs.js.map
|
|
428
|
-
//# debugId=
|
|
452
|
+
//# debugId=570f7491-8d05-577c-8bae-6f6cf7dfb2ec
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* 9. Print next-step message
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
16
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="ccc2e70c-f577-5a47-9a29-31a206194392")}catch(e){}}();
|
|
17
17
|
import * as fs from 'fs';
|
|
18
18
|
import * as os from 'os';
|
|
19
19
|
import * as path from 'path';
|
|
@@ -33,12 +33,14 @@ export function registerPackageInstallCommand(parent) {
|
|
|
33
33
|
'@scope/name[@ver] (npm pack), git URL[#ref], or local path.')
|
|
34
34
|
.option('--company <co>', 'Scope the package to a specific company (registry flow only)')
|
|
35
35
|
.option('--allow-hooks', 'Skip the hooks confirmation prompt (content-pack flow)')
|
|
36
|
+
.option('--allow-mcp', 'Skip the MCP server confirmation prompt (content-pack flow)')
|
|
36
37
|
.option('--branch', 'Follow a ref instead of SHA-pinning (git content-pack flow)')
|
|
37
38
|
.action(async (source, opts) => {
|
|
38
39
|
try {
|
|
39
40
|
if (sourceMatchesPackPattern(source)) {
|
|
40
41
|
await installPack(source, {
|
|
41
42
|
allowHooks: opts.allowHooks,
|
|
43
|
+
allowMcp: opts.allowMcp,
|
|
42
44
|
followBranch: opts.branch,
|
|
43
45
|
});
|
|
44
46
|
}
|
|
@@ -49,6 +51,7 @@ export function registerPackageInstallCommand(parent) {
|
|
|
49
51
|
// listings transport — the live install path — instead.
|
|
50
52
|
await installPack(`${MARKETPLACE_PREFIX}${source}`, {
|
|
51
53
|
allowHooks: opts.allowHooks,
|
|
54
|
+
allowMcp: opts.allowMcp,
|
|
52
55
|
followBranch: opts.branch,
|
|
53
56
|
});
|
|
54
57
|
}
|
|
@@ -163,4 +166,4 @@ async function installPackage(slug, company) {
|
|
|
163
166
|
}
|
|
164
167
|
}
|
|
165
168
|
//# sourceMappingURL=pkg-install.js.map
|
|
166
|
-
//# debugId=
|
|
169
|
+
//# debugId=ccc2e70c-f577-5a47-9a29-31a206194392
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* HQ CLI - Module management, package management, and cloud sync for HQ
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
6
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="34e9e978-4c0a-5113-82af-9a33e30686c6")}catch(e){}}();
|
|
7
7
|
import { Command } from "commander";
|
|
8
8
|
import { initSentry, Sentry } from "./sentry.js";
|
|
9
9
|
import { registerAddCommand } from "./commands/add.js";
|
|
@@ -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 { maybeWarnNewVersion, refreshVersionCache, } from "./utils/version-check.js";
|
|
@@ -159,6 +160,10 @@ registerReindexCommand(program);
|
|
|
159
160
|
// the HQ Sync app's "Update / Restore" pill; drives the same replace-rescue.sh
|
|
160
161
|
// shipped from @indigoai-us/hq-cloud.
|
|
161
162
|
registerRescueCommand(program);
|
|
163
|
+
// MCP pack observability (subcommand group — `hq mcp status`). Read-only
|
|
164
|
+
// provenance-based status across BOTH Claude + Codex runtimes (reads `_hqPack`
|
|
165
|
+
// off the configs, NOT linkStatus), with secret-redacted output + `--json`.
|
|
166
|
+
registerMcpCommand(program);
|
|
162
167
|
(async () => {
|
|
163
168
|
try {
|
|
164
169
|
Sentry.addBreadcrumb({
|
|
@@ -198,4 +203,4 @@ registerRescueCommand(program);
|
|
|
198
203
|
}
|
|
199
204
|
})();
|
|
200
205
|
//# sourceMappingURL=index.js.map
|
|
201
|
-
//# debugId=
|
|
206
|
+
//# debugId=34e9e978-4c0a-5113-82af-9a33e30686c6
|