@i4ctime/q-ring 0.14.1 → 0.16.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.
package/dist/index.js CHANGED
@@ -38,13 +38,14 @@ import {
38
38
  removeHook,
39
39
  revokeApproval,
40
40
  serviceForScope,
41
+ setAuditAgentLabel,
41
42
  setSecret,
42
43
  tunnelCreate,
43
44
  tunnelDestroy,
44
45
  tunnelList,
45
46
  tunnelRead,
46
47
  verifyAuditChain
47
- } from "./chunk-C2TFJ2EH.js";
48
+ } from "./chunk-5LFKBZ3Q.js";
48
49
 
49
50
  // src/cli/commands.ts
50
51
  import { Command, Help } from "commander";
@@ -764,6 +765,79 @@ var ProviderRegistry = class {
764
765
  return [...this.providers.values()];
765
766
  }
766
767
  };
768
+ function livenessProvider(cfg) {
769
+ return {
770
+ name: cfg.name,
771
+ description: cfg.description,
772
+ prefixes: cfg.prefixes,
773
+ async validate(value) {
774
+ const start = Date.now();
775
+ try {
776
+ const { statusCode } = await makeRequest(cfg.url, {
777
+ "User-Agent": "q-ring-validator/1.0",
778
+ ...cfg.headers(value)
779
+ });
780
+ const latencyMs = Date.now() - start;
781
+ if (statusCode === 200)
782
+ return { valid: true, status: "valid", message: "API key is valid", latencyMs, provider: cfg.name };
783
+ if (statusCode === 401 || statusCode === 403)
784
+ return { valid: false, status: "invalid", message: `Invalid or revoked API key (${statusCode})`, latencyMs, provider: cfg.name };
785
+ if (statusCode === 429)
786
+ return { valid: true, status: "error", message: "Rate limited \u2014 key may be valid", latencyMs, provider: cfg.name };
787
+ return { valid: false, status: "error", message: `Unexpected status ${statusCode}`, latencyMs, provider: cfg.name };
788
+ } catch (err) {
789
+ return { valid: false, status: "error", message: `${err instanceof Error ? err.message : "Network error"}`, latencyMs: Date.now() - start, provider: cfg.name };
790
+ }
791
+ }
792
+ };
793
+ }
794
+ var anthropicProvider = livenessProvider({
795
+ name: "anthropic",
796
+ description: "Anthropic API key validation",
797
+ prefixes: ["sk-ant-"],
798
+ url: "https://api.anthropic.com/v1/models?limit=1",
799
+ headers: (value) => ({ "x-api-key": value, "anthropic-version": "2023-06-01" })
800
+ });
801
+ var openrouterProvider = livenessProvider({
802
+ name: "openrouter",
803
+ description: "OpenRouter API key validation",
804
+ prefixes: ["sk-or-"],
805
+ url: "https://openrouter.ai/api/v1/key",
806
+ headers: (value) => ({ Authorization: `Bearer ${value}` })
807
+ });
808
+ var googleAiProvider = livenessProvider({
809
+ name: "google-ai",
810
+ description: "Google AI (Gemini) API key validation",
811
+ prefixes: ["AIza"],
812
+ url: "https://generativelanguage.googleapis.com/v1beta/models?pageSize=1",
813
+ headers: (value) => ({ "x-goog-api-key": value })
814
+ });
815
+ var groqProvider = livenessProvider({
816
+ name: "groq",
817
+ description: "Groq API key validation",
818
+ prefixes: ["gsk_"],
819
+ url: "https://api.groq.com/openai/v1/models",
820
+ headers: (value) => ({ Authorization: `Bearer ${value}` })
821
+ });
822
+ var huggingfaceProvider = livenessProvider({
823
+ name: "huggingface",
824
+ description: "Hugging Face token validation",
825
+ prefixes: ["hf_"],
826
+ url: "https://huggingface.co/api/whoami-v2",
827
+ headers: (value) => ({ Authorization: `Bearer ${value}` })
828
+ });
829
+ var elevenlabsProvider = livenessProvider({
830
+ name: "elevenlabs",
831
+ description: "ElevenLabs API key validation (explicit-only \u2014 set provider on the secret)",
832
+ url: "https://api.elevenlabs.io/v1/user",
833
+ headers: (value) => ({ "xi-api-key": value })
834
+ });
835
+ var vercelProvider = livenessProvider({
836
+ name: "vercel",
837
+ description: "Vercel token validation (explicit-only \u2014 set provider on the secret)",
838
+ url: "https://api.vercel.com/v2/user",
839
+ headers: (value) => ({ Authorization: `Bearer ${value}` })
840
+ });
767
841
  var openaiProvider = {
768
842
  name: "openai",
769
843
  description: "OpenAI API key validation",
@@ -890,7 +964,14 @@ var httpProvider = {
890
964
  }
891
965
  };
892
966
  var registry2 = new ProviderRegistry();
967
+ registry2.register(anthropicProvider);
968
+ registry2.register(openrouterProvider);
893
969
  registry2.register(openaiProvider);
970
+ registry2.register(googleAiProvider);
971
+ registry2.register(groqProvider);
972
+ registry2.register(huggingfaceProvider);
973
+ registry2.register(elevenlabsProvider);
974
+ registry2.register(vercelProvider);
894
975
  registry2.register(stripeProvider);
895
976
  registry2.register(githubProvider);
896
977
  registry2.register(awsProvider);
@@ -1984,10 +2065,9 @@ var RedactionTransform = class extends Transform {
1984
2065
  callback();
1985
2066
  }
1986
2067
  };
1987
- async function execCommand(opts) {
1988
- const profile = getProfile(opts.profile);
1989
- const fullCommand = [opts.command, ...opts.args].join(" ");
1990
- const policyDecision = checkExecPolicy(fullCommand, opts.projectPath);
2068
+ function enforceExecPolicy(profile, command, args, projectPath) {
2069
+ const fullCommand = [command, ...args].join(" ");
2070
+ const policyDecision = checkExecPolicy(fullCommand, projectPath);
1991
2071
  if (!policyDecision.allowed) {
1992
2072
  throw new Error(`Policy Denied: ${policyDecision.reason}`);
1993
2073
  }
@@ -2003,9 +2083,13 @@ async function execCommand(opts) {
2003
2083
  if (profile.allowCommands) {
2004
2084
  const allowed = profile.allowCommands.some((a) => fullCommand.startsWith(a));
2005
2085
  if (!allowed) {
2006
- throw new Error(`Exec profile "${profile.name}" does not allow command "${opts.command}"`);
2086
+ throw new Error(`Exec profile "${profile.name}" does not allow command "${command}"`);
2007
2087
  }
2008
2088
  }
2089
+ }
2090
+ async function execCommand(opts) {
2091
+ const profile = getProfile(opts.profile);
2092
+ enforceExecPolicy(profile, opts.command, opts.args, opts.projectPath);
2009
2093
  const envMap = {};
2010
2094
  for (const [k, v] of Object.entries(process.env)) {
2011
2095
  if (v !== void 0) envMap[k] = v;
@@ -2052,6 +2136,18 @@ async function execCommand(opts) {
2052
2136
  }
2053
2137
  }
2054
2138
  }
2139
+ return spawnRedacted({
2140
+ profile,
2141
+ command: opts.command,
2142
+ args: opts.args,
2143
+ envMap,
2144
+ secretsToRedact: [...secretsToRedact],
2145
+ captureOutput: opts.captureOutput,
2146
+ projectPath: opts.projectPath
2147
+ });
2148
+ }
2149
+ function spawnRedacted(opts) {
2150
+ const { profile, secretsToRedact, envMap } = opts;
2055
2151
  const maxRuntime = profile.maxRuntimeSeconds ?? getExecMaxRuntime(opts.projectPath);
2056
2152
  return new Promise((resolve, reject) => {
2057
2153
  const networkTools = /* @__PURE__ */ new Set([
@@ -2492,7 +2588,7 @@ function registerToolingCommands(program2) {
2492
2588
  }
2493
2589
  });
2494
2590
  program2.command("status").description("Launch the quantum status dashboard in your browser").option("--port <port>", "Port to serve on", "9876").option("--no-open", "Don't auto-open the browser").action(async (cmd) => {
2495
- const { startDashboardServer } = await import("./dashboard-PYYAED45.js");
2591
+ const { startDashboardServer } = await import("./dashboard-A3GJQCJX.js");
2496
2592
  const { exec } = await import("child_process");
2497
2593
  const { platform } = await import("os");
2498
2594
  const port = Number(cmd.port);
@@ -3407,12 +3503,819 @@ ${SYMBOLS.lock} Approval Tokens
3407
3503
  });
3408
3504
  }
3409
3505
 
3410
- // src/cli/commands/doctor.ts
3411
- import { existsSync as existsSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync5, rmSync } from "fs";
3412
- import { join as join3, delimiter } from "path";
3506
+ // src/core/run.ts
3507
+ import { existsSync as existsSync4, readFileSync as readFileSync6 } from "fs";
3508
+ import { join as join3, resolve as resolvePath } from "path";
3509
+
3510
+ // src/core/refs.ts
3511
+ var REF_PREFIX = "qring://";
3512
+ var KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
3513
+ var REF_PATTERN = /^qring:\/\/(global|project|)\/([^/?#]+)(?:\?([^#]*))?$/;
3514
+ function isRef(value) {
3515
+ return value.startsWith(REF_PREFIX);
3516
+ }
3517
+ function parseRef(raw) {
3518
+ if (!isRef(raw)) {
3519
+ throw new Error(`Not a qring:// reference: "${raw}"`);
3520
+ }
3521
+ const match = REF_PATTERN.exec(raw);
3522
+ if (!match) {
3523
+ const hostOnly = /^qring:\/\/([^/?#]+)\/?$/.exec(raw);
3524
+ if (hostOnly && KEY_PATTERN.test(hostOnly[1]) && !["global", "project"].includes(hostOnly[1])) {
3525
+ throw new Error(
3526
+ `Invalid ref "${raw}": the key belongs in the path, not the host (URL hosts are lowercased by parsers). Use "qring:///${hostOnly[1]}" (auto scope) or "qring://project/${hostOnly[1]}".`
3527
+ );
3528
+ }
3529
+ throw new Error(
3530
+ `Invalid ref "${raw}": expected qring://<global|project|>/KEY[?env=<env>]`
3531
+ );
3532
+ }
3533
+ const [, scopeRaw, key, query] = match;
3534
+ if (!KEY_PATTERN.test(key)) {
3535
+ throw new Error(
3536
+ `Invalid ref "${raw}": "${key}" is not a valid secret key (expected [A-Za-z_][A-Za-z0-9_]*)`
3537
+ );
3538
+ }
3539
+ let env;
3540
+ if (query) {
3541
+ for (const part of query.split("&")) {
3542
+ if (!part) continue;
3543
+ const eq = part.indexOf("=");
3544
+ const name = eq === -1 ? part : part.slice(0, eq);
3545
+ const value = eq === -1 ? "" : decodeURIComponent(part.slice(eq + 1));
3546
+ if (name === "env") {
3547
+ env = value;
3548
+ } else {
3549
+ throw new Error(`Invalid ref "${raw}": unknown query parameter "${name}"`);
3550
+ }
3551
+ }
3552
+ }
3553
+ return {
3554
+ scope: scopeRaw === "" ? void 0 : scopeRaw,
3555
+ key,
3556
+ env,
3557
+ raw
3558
+ };
3559
+ }
3560
+ function resolveRef(ref, opts = {}) {
3561
+ const scopes = ref.scope ? [ref.scope] : ["project", "global"];
3562
+ for (const scope of scopes) {
3563
+ const value = getSecret(ref.key, {
3564
+ ...opts,
3565
+ scope,
3566
+ env: ref.env ?? opts.env
3567
+ });
3568
+ if (value !== null) return value;
3569
+ }
3570
+ return null;
3571
+ }
3572
+ function resolveRefsInMap(map, opts = {}) {
3573
+ const resolved = {};
3574
+ const secretValues = [];
3575
+ const missing = [];
3576
+ for (const [name, value] of Object.entries(map)) {
3577
+ if (!isRef(value)) {
3578
+ resolved[name] = value;
3579
+ continue;
3580
+ }
3581
+ const ref = parseRef(value);
3582
+ const secret = resolveRef(ref, opts);
3583
+ if (secret === null) {
3584
+ missing.push({ name, ref });
3585
+ continue;
3586
+ }
3587
+ resolved[name] = secret;
3588
+ secretValues.push(secret);
3589
+ }
3590
+ return { resolved, secretValues, missing };
3591
+ }
3592
+
3593
+ // src/core/run.ts
3594
+ function buildRunPlan(opts) {
3595
+ const projectPath = opts.projectPath ?? process.cwd();
3596
+ const keyringOpts = {
3597
+ projectPath,
3598
+ env: opts.env,
3599
+ source: opts.source ?? "cli",
3600
+ silent: opts.silent
3601
+ };
3602
+ const envMap = {};
3603
+ for (const [k, v] of Object.entries(process.env)) {
3604
+ if (v !== void 0) envMap[k] = v;
3605
+ }
3606
+ const injected = [];
3607
+ const missingRequired = [];
3608
+ const missingOptional = [];
3609
+ const secretsToRedact = /* @__PURE__ */ new Set();
3610
+ if (opts.useManifest !== false) {
3611
+ const config = readProjectConfig(projectPath);
3612
+ for (const [key, entry] of Object.entries(config?.secrets ?? {})) {
3613
+ const value = resolveRef(
3614
+ { key, raw: `manifest:${key}` },
3615
+ keyringOpts
3616
+ );
3617
+ if (value === null) {
3618
+ if (entry.required !== false) missingRequired.push(key);
3619
+ else if (opts.strict) missingRequired.push(key);
3620
+ else missingOptional.push(key);
3621
+ continue;
3622
+ }
3623
+ envMap[key] = value;
3624
+ injected.push({ name: key, source: "manifest" });
3625
+ if (value.length > 5) secretsToRedact.add(value);
3626
+ }
3627
+ }
3628
+ const envFiles = opts.envFiles ?? (existsSync4(join3(projectPath, ".env")) ? [join3(projectPath, ".env")] : []);
3629
+ for (const file of envFiles) {
3630
+ const path = resolvePath(projectPath, file);
3631
+ if (!existsSync4(path)) {
3632
+ throw new Error(`Env file not found: ${path}`);
3633
+ }
3634
+ const parsed = Object.fromEntries(parseDotenv(readFileSync6(path, "utf8")));
3635
+ const { resolved, secretValues, missing } = resolveRefsInMap(parsed, keyringOpts);
3636
+ for (const [name, value] of Object.entries(resolved)) {
3637
+ envMap[name] = value;
3638
+ injected.push({
3639
+ name,
3640
+ source: isRef(parsed[name]) ? "env-file-ref" : "env-file"
3641
+ });
3642
+ }
3643
+ for (const value of secretValues) {
3644
+ if (value.length > 5) secretsToRedact.add(value);
3645
+ }
3646
+ for (const m of missing) missingRequired.push(m.name);
3647
+ }
3648
+ return {
3649
+ envFiles: envFiles.map((f) => resolvePath(projectPath, f)),
3650
+ injected,
3651
+ missingRequired,
3652
+ missingOptional,
3653
+ envMap,
3654
+ secretsToRedact: [...secretsToRedact]
3655
+ };
3656
+ }
3657
+ async function runCommand(opts) {
3658
+ const profile = getProfile(opts.profile);
3659
+ enforceExecPolicy(profile, opts.command, opts.args, opts.projectPath);
3660
+ const plan = buildRunPlan(opts);
3661
+ if (plan.missingRequired.length > 0) {
3662
+ throw new Error(
3663
+ `Missing required secrets: ${plan.missingRequired.join(", ")}. Store them with \`qring set <KEY>\` or drop them from the manifest/.env refs.`
3664
+ );
3665
+ }
3666
+ if (profile.stripEnvVars) {
3667
+ for (const key of profile.stripEnvVars) {
3668
+ delete plan.envMap[key];
3669
+ }
3670
+ }
3671
+ const result = await spawnRedacted({
3672
+ profile,
3673
+ command: opts.command,
3674
+ args: opts.args,
3675
+ envMap: plan.envMap,
3676
+ secretsToRedact: plan.secretsToRedact,
3677
+ captureOutput: opts.captureOutput,
3678
+ projectPath: opts.projectPath
3679
+ });
3680
+ return { ...result, plan };
3681
+ }
3682
+
3683
+ // src/core/setup.ts
3684
+ import { existsSync as existsSync5, mkdirSync, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
3413
3685
  import { homedir } from "os";
3686
+ import { dirname, join as join4 } from "path";
3687
+ var EDITORS = ["cursor", "kiro", "claude"];
3688
+ var READ_ONLY_TOOLS = [
3689
+ "list_secrets",
3690
+ "has_secret",
3691
+ "inspect_secret",
3692
+ "detect_environment",
3693
+ "get_project_context",
3694
+ "list_providers",
3695
+ "list_hooks",
3696
+ "get_policy_summary",
3697
+ "audit_log",
3698
+ "verify_audit_chain",
3699
+ "health_check",
3700
+ "analyze_secrets"
3701
+ ];
3702
+ var SERVER_NAME = "q-ring";
3703
+ function serverEntry(editor) {
3704
+ switch (editor) {
3705
+ case "cursor":
3706
+ return { command: "qring-mcp", args: [], env: {} };
3707
+ case "kiro":
3708
+ return {
3709
+ command: "qring-mcp",
3710
+ args: [],
3711
+ env: {},
3712
+ disabled: false,
3713
+ autoApprove: READ_ONLY_TOOLS
3714
+ };
3715
+ case "claude":
3716
+ return { type: "stdio", command: "qring-mcp", args: [], env: {} };
3717
+ }
3718
+ }
3719
+ function configPathFor(editor, global, projectPath) {
3720
+ const home = homedir();
3721
+ switch (editor) {
3722
+ case "cursor":
3723
+ return global ? join4(home, ".cursor", "mcp.json") : join4(projectPath, ".cursor", "mcp.json");
3724
+ case "kiro":
3725
+ return global ? join4(home, ".kiro", "settings", "mcp.json") : join4(projectPath, ".kiro", "settings", "mcp.json");
3726
+ case "claude":
3727
+ if (global) {
3728
+ throw new Error(
3729
+ "Claude Code user-scoped MCP servers are registered via the claude CLI, not a config file.\nRun: claude mcp add --scope user q-ring qring-mcp\nOr install the plugin: /plugin marketplace add I4cTime/q-ring && /plugin install qring@q-ring"
3730
+ );
3731
+ }
3732
+ return join4(projectPath, ".mcp.json");
3733
+ }
3734
+ }
3735
+ function setupEditor(opts) {
3736
+ const projectPath = opts.projectPath ?? process.cwd();
3737
+ const configPath = configPathFor(opts.editor, opts.global ?? false, projectPath);
3738
+ const warnings = [];
3739
+ let config = {};
3740
+ if (existsSync5(configPath)) {
3741
+ const raw = readFileSync7(configPath, "utf8");
3742
+ try {
3743
+ config = JSON.parse(raw);
3744
+ } catch {
3745
+ throw new Error(
3746
+ `${configPath} exists but is not valid JSON \u2014 fix or remove it, then re-run.`
3747
+ );
3748
+ }
3749
+ }
3750
+ const servers = config.mcpServers ?? {};
3751
+ const desired = serverEntry(opts.editor);
3752
+ const existing = servers[SERVER_NAME];
3753
+ let action;
3754
+ if (existing === void 0) {
3755
+ action = existsSync5(configPath) ? "updated" : "created";
3756
+ servers[SERVER_NAME] = desired;
3757
+ } else if (JSON.stringify(existing) === JSON.stringify(desired)) {
3758
+ action = "unchanged";
3759
+ } else if (opts.force) {
3760
+ action = "updated";
3761
+ servers[SERVER_NAME] = desired;
3762
+ } else {
3763
+ action = "unchanged";
3764
+ warnings.push(
3765
+ `A different "${SERVER_NAME}" entry already exists in ${configPath} \u2014 left as-is (use --force to replace it).`
3766
+ );
3767
+ }
3768
+ config.mcpServers = servers;
3769
+ if (action !== "unchanged" && !opts.dryRun) {
3770
+ mkdirSync(dirname(configPath), { recursive: true });
3771
+ writeFileSync5(configPath, JSON.stringify(config, null, 2) + "\n");
3772
+ }
3773
+ return {
3774
+ editor: opts.editor,
3775
+ configPath,
3776
+ serverName: SERVER_NAME,
3777
+ action,
3778
+ dryRun: opts.dryRun ?? false,
3779
+ warnings
3780
+ };
3781
+ }
3782
+
3783
+ // src/core/push.ts
3784
+ import { spawnSync } from "child_process";
3785
+ var PUSH_TARGETS = ["github", "vercel", "cloudflare"];
3786
+ var TARGETS = {
3787
+ github: {
3788
+ binary: "gh",
3789
+ args: (key, opts) => [
3790
+ ["secret", "set", key, ...opts.repo ? ["--repo", opts.repo] : []]
3791
+ ],
3792
+ installHint: "install the GitHub CLI: https://cli.github.com (then `gh auth login`)"
3793
+ },
3794
+ vercel: {
3795
+ binary: "vercel",
3796
+ // One invocation per environment — `vercel env add` takes a single target.
3797
+ args: (key, opts) => (opts.vercelEnvs ?? ["production"]).map((env) => [
3798
+ "env",
3799
+ "add",
3800
+ key,
3801
+ env,
3802
+ "--force"
3803
+ ]),
3804
+ installHint: "install the Vercel CLI: npm i -g vercel (then `vercel link` in the project)"
3805
+ },
3806
+ cloudflare: {
3807
+ binary: "wrangler",
3808
+ args: (key) => [["secret", "put", key]],
3809
+ installHint: "install Wrangler: npm i -g wrangler (then `wrangler login`)"
3810
+ }
3811
+ };
3812
+ function binaryAvailable(binary) {
3813
+ const probe = spawnSync(binary, ["--version"], { stdio: "ignore", shell: false });
3814
+ return !probe.error;
3815
+ }
3816
+ function resolvePushKeys(opts) {
3817
+ if (opts.keys?.length) return opts.keys;
3818
+ const config = readProjectConfig(opts.projectPath);
3819
+ const manifestKeys = Object.keys(config?.secrets ?? {});
3820
+ if (manifestKeys.length === 0) {
3821
+ throw new Error(
3822
+ "Nothing to push: no --keys given and no secrets manifest in .q-ring.json. Declare the project's secrets there or pass --keys KEY1,KEY2."
3823
+ );
3824
+ }
3825
+ return manifestKeys;
3826
+ }
3827
+ function pushSecrets(opts) {
3828
+ const target = TARGETS[opts.target];
3829
+ const keys = resolvePushKeys(opts);
3830
+ if (!opts.dryRun && !binaryAvailable(target.binary)) {
3831
+ throw new Error(`"${target.binary}" CLI not found \u2014 ${target.installHint}`);
3832
+ }
3833
+ const result = { target: opts.target, pushed: [], failed: [], missing: [] };
3834
+ for (const key of keys) {
3835
+ const value = resolveRef(
3836
+ { key, raw: `push:${key}` },
3837
+ {
3838
+ projectPath: opts.projectPath,
3839
+ env: opts.env,
3840
+ source: opts.source ?? "cli",
3841
+ silent: opts.silent
3842
+ }
3843
+ );
3844
+ if (value === null) {
3845
+ result.missing.push(key);
3846
+ continue;
3847
+ }
3848
+ if (opts.dryRun) {
3849
+ result.pushed.push(key);
3850
+ continue;
3851
+ }
3852
+ let failedInvocation = null;
3853
+ for (const args of target.args(key, opts)) {
3854
+ const child = spawnSync(target.binary, args, {
3855
+ input: value,
3856
+ cwd: opts.projectPath,
3857
+ encoding: "utf8",
3858
+ shell: false
3859
+ });
3860
+ if (child.status !== 0) {
3861
+ failedInvocation = child.stderr?.trim() || child.error?.message || `exit ${child.status}`;
3862
+ break;
3863
+ }
3864
+ }
3865
+ if (failedInvocation) {
3866
+ result.failed.push({ key, error: failedInvocation });
3867
+ continue;
3868
+ }
3869
+ result.pushed.push(key);
3870
+ if (!opts.silent) {
3871
+ logAudit({
3872
+ action: "push",
3873
+ key,
3874
+ env: opts.env,
3875
+ source: opts.source ?? "cli",
3876
+ detail: `pushed to ${opts.target}${opts.repo ? ` (${opts.repo})` : ""}`
3877
+ });
3878
+ }
3879
+ }
3880
+ return result;
3881
+ }
3882
+
3883
+ // src/cli/commands/bridges.ts
3884
+ function registerBridgeCommands(program2) {
3885
+ program2.command("run <command...>").description(
3886
+ "Run a command with only declared secrets injected \u2014 .q-ring.json manifest keys plus qring:// refs from .env (output auto-redacted)"
3887
+ ).option("--project-path <path>", "Explicit project path").option("-e, --env <env>", "Environment context").option("--env-file <files...>", "Env file(s) to load (default: ./.env when present)").option("--no-manifest", "Skip .q-ring.json manifest keys").option("--profile <name>", "Exec profile: unrestricted, restricted, ci").option("--strict", "Missing optional manifest keys are fatal too").option("--dry-run", "Show what would be injected without running").action(async (commandArgs, cmd) => {
3888
+ const opts = {
3889
+ command: commandArgs[0],
3890
+ args: commandArgs.slice(1),
3891
+ projectPath: cmd.projectPath ?? process.cwd(),
3892
+ env: cmd.env,
3893
+ envFiles: cmd.envFile,
3894
+ useManifest: cmd.manifest !== false,
3895
+ profile: cmd.profile,
3896
+ strict: cmd.strict === true,
3897
+ source: "cli"
3898
+ };
3899
+ try {
3900
+ if (cmd.dryRun) {
3901
+ const plan2 = buildRunPlan({ ...opts, silent: true });
3902
+ console.log(`
3903
+ ${SYMBOLS.zap} ${c.bold("qring run")} ${c.dim("(dry run)")}`);
3904
+ if (plan2.envFiles.length > 0) {
3905
+ console.log(` Env files: ${plan2.envFiles.map((f) => c.cyan(f)).join(", ")}`);
3906
+ }
3907
+ for (const v of plan2.injected) {
3908
+ console.log(` ${c.green(SYMBOLS.check)} ${v.name} ${c.dim(`(${v.source})`)}`);
3909
+ }
3910
+ for (const name of plan2.missingOptional) {
3911
+ console.log(` ${c.yellow("?")} ${name} ${c.dim("(optional, missing)")}`);
3912
+ }
3913
+ for (const name of plan2.missingRequired) {
3914
+ console.log(` ${c.red(SYMBOLS.cross)} ${name} ${c.dim("(required, MISSING)")}`);
3915
+ }
3916
+ process.exit(plan2.missingRequired.length > 0 ? 1 : 0);
3917
+ }
3918
+ const { code, plan } = await runCommand(opts);
3919
+ if (plan.missingOptional.length > 0) {
3920
+ console.error(
3921
+ c.yellow(
3922
+ `${SYMBOLS.warning} Optional secrets not found: ${plan.missingOptional.join(", ")}`
3923
+ )
3924
+ );
3925
+ }
3926
+ process.exit(code);
3927
+ } catch (err) {
3928
+ console.error(
3929
+ c.red(
3930
+ `${SYMBOLS.cross} Run failed: ${err instanceof Error ? err.message : String(err)}`
3931
+ )
3932
+ );
3933
+ process.exit(1);
3934
+ }
3935
+ });
3936
+ program2.command("push <target>").description(
3937
+ `Push manifest secrets to a deployment platform via its own CLI (${PUSH_TARGETS.join(", ")}) \u2014 values travel over stdin, never argv`
3938
+ ).option("-k, --keys <keys>", "Comma-separated keys (default: .q-ring.json manifest)").option("--project-path <path>", "Explicit project path").option("-e, --env <env>", "Environment context for superposition collapse").option("--repo <owner/name>", "GitHub repository (github target)").option(
3939
+ "--vercel-env <envs>",
3940
+ "Comma-separated Vercel environments (default: production)"
3941
+ ).option("--dry-run", "Show what would be pushed without pushing").option("--json", "Output as JSON").action((targetArg, cmd) => {
3942
+ if (!PUSH_TARGETS.includes(targetArg)) {
3943
+ console.error(
3944
+ c.red(`${SYMBOLS.cross} Unknown target "${targetArg}" \u2014 expected one of: ${PUSH_TARGETS.join(", ")}`)
3945
+ );
3946
+ process.exit(1);
3947
+ }
3948
+ try {
3949
+ const result = pushSecrets({
3950
+ target: targetArg,
3951
+ keys: cmd.keys?.split(",").map((k) => k.trim()),
3952
+ projectPath: cmd.projectPath ?? process.cwd(),
3953
+ env: cmd.env,
3954
+ repo: cmd.repo,
3955
+ vercelEnvs: cmd.vercelEnv?.split(",").map((e) => e.trim()),
3956
+ dryRun: cmd.dryRun === true,
3957
+ source: "cli"
3958
+ });
3959
+ if (wantsJsonOutput(program2, cmd)) {
3960
+ console.log(JSON.stringify(result, null, 2));
3961
+ process.exit(result.failed.length > 0 ? 1 : 0);
3962
+ }
3963
+ const prefix = cmd.dryRun ? c.dim("[dry-run] ") : "";
3964
+ console.log(`
3965
+ ${SYMBOLS.link} ${prefix}${c.bold(`qring push ${targetArg}`)}`);
3966
+ for (const key of result.pushed) {
3967
+ console.log(` ${c.green(SYMBOLS.check)} ${key}`);
3968
+ }
3969
+ for (const key of result.missing) {
3970
+ console.log(` ${c.yellow("?")} ${key} ${c.dim("(not in keyring \u2014 skipped)")}`);
3971
+ }
3972
+ for (const { key, error } of result.failed) {
3973
+ console.log(` ${c.red(SYMBOLS.cross)} ${key} ${c.dim(`\u2014 ${error}`)}`);
3974
+ }
3975
+ console.log(
3976
+ c.dim(
3977
+ ` ${result.pushed.length} pushed${cmd.dryRun ? " (dry run)" : ""}, ${result.missing.length} missing, ${result.failed.length} failed`
3978
+ )
3979
+ );
3980
+ process.exit(result.failed.length > 0 ? 1 : 0);
3981
+ } catch (err) {
3982
+ console.error(
3983
+ c.red(
3984
+ `${SYMBOLS.cross} Push failed: ${err instanceof Error ? err.message : String(err)}`
3985
+ )
3986
+ );
3987
+ process.exit(1);
3988
+ }
3989
+ });
3990
+ program2.command("setup <editor>").description(
3991
+ `Wire the q-ring MCP server into an editor's MCP config (${EDITORS.join(", ")})`
3992
+ ).option("-g, --global", "Write the per-user config instead of the project one").option("--project-path <path>", "Explicit project path").option("--force", "Replace an existing q-ring entry that differs").option("--dry-run", "Show what would change without writing").option("--json", "Output as JSON").action((editor, cmd) => {
3993
+ if (!EDITORS.includes(editor)) {
3994
+ console.error(
3995
+ c.red(`${SYMBOLS.cross} Unknown editor "${editor}" \u2014 expected one of: ${EDITORS.join(", ")}`)
3996
+ );
3997
+ process.exit(1);
3998
+ }
3999
+ try {
4000
+ const result = setupEditor({
4001
+ editor,
4002
+ global: cmd.global === true,
4003
+ projectPath: cmd.projectPath ?? process.cwd(),
4004
+ force: cmd.force === true,
4005
+ dryRun: cmd.dryRun === true
4006
+ });
4007
+ if (wantsJsonOutput(program2, cmd)) {
4008
+ console.log(JSON.stringify(result, null, 2));
4009
+ return;
4010
+ }
4011
+ const verb = result.action === "created" ? "Created" : result.action === "updated" ? "Updated" : "Already configured";
4012
+ const prefix = result.dryRun ? c.dim("[dry-run] ") : "";
4013
+ console.log(
4014
+ `
4015
+ ${SYMBOLS.check} ${prefix}${c.bold(verb)}: ${c.cyan(result.configPath)} ${c.dim(`(server "${result.serverName}")`)}`
4016
+ );
4017
+ for (const warning of result.warnings) {
4018
+ console.log(` ${c.yellow("!")} ${warning}`);
4019
+ }
4020
+ if (result.action !== "unchanged") {
4021
+ console.log(c.dim(` Restart ${editor} to pick up the MCP server. Verify with: qring doctor`));
4022
+ }
4023
+ } catch (err) {
4024
+ console.error(
4025
+ c.red(
4026
+ `${SYMBOLS.cross} Setup failed: ${err instanceof Error ? err.message : String(err)}`
4027
+ )
4028
+ );
4029
+ process.exit(1);
4030
+ }
4031
+ });
4032
+ }
4033
+
4034
+ // src/core/canary.ts
4035
+ var ALPHA_UPPER_NUM = () => generateSecret({ format: "alphanumeric", length: 16 }).toUpperCase();
4036
+ var CANARY_FORMATS = {
4037
+ aws: {
4038
+ name: "aws",
4039
+ description: "AWS access key id (AKIA\u2026)",
4040
+ generate: () => `AKIA${ALPHA_UPPER_NUM()}`
4041
+ },
4042
+ github: {
4043
+ name: "github",
4044
+ description: "GitHub personal access token (ghp_\u2026)",
4045
+ generate: () => generateSecret({ format: "api-key", prefix: "ghp_", length: 36 })
4046
+ },
4047
+ openai: {
4048
+ name: "openai",
4049
+ description: "OpenAI API key (sk-\u2026)",
4050
+ generate: () => generateSecret({ format: "api-key", prefix: "sk-", length: 48 })
4051
+ },
4052
+ anthropic: {
4053
+ name: "anthropic",
4054
+ description: "Anthropic API key (sk-ant-\u2026)",
4055
+ generate: () => generateSecret({ format: "api-key", prefix: "sk-ant-api03-", length: 80 })
4056
+ },
4057
+ stripe: {
4058
+ name: "stripe",
4059
+ description: "Stripe live secret key (sk_live_\u2026)",
4060
+ generate: () => generateSecret({ format: "api-key", prefix: "sk_live_", length: 24 })
4061
+ },
4062
+ generic: {
4063
+ name: "generic",
4064
+ description: "Generic high-entropy API key",
4065
+ generate: () => generateSecret({ format: "api-key", prefix: "qk_", length: 40 })
4066
+ }
4067
+ };
4068
+ var DEFAULT_CANARY_FORMAT = "generic";
4069
+ function plantCanary(key, opts = {}) {
4070
+ const formatName = opts.format ?? DEFAULT_CANARY_FORMAT;
4071
+ const format = CANARY_FORMATS[formatName];
4072
+ if (!format) {
4073
+ throw new Error(
4074
+ `Unknown canary format "${formatName}". Available: ${Object.keys(CANARY_FORMATS).join(", ")}`
4075
+ );
4076
+ }
4077
+ const value = opts.value ?? format.generate();
4078
+ const scope = opts.scope ?? "global";
4079
+ setSecret(key, value, {
4080
+ ...opts,
4081
+ scope,
4082
+ canary: true,
4083
+ canaryFormat: formatName,
4084
+ description: opts.value ? "Canary honeytoken (custom value)" : `Canary honeytoken (${format.description})`
4085
+ });
4086
+ return { key, value, format: formatName, scope };
4087
+ }
4088
+ function listCanaries(opts = {}) {
4089
+ const out = [];
4090
+ for (const entry of listSecrets({ ...opts, silent: true })) {
4091
+ const meta = entry.envelope?.meta;
4092
+ if (!meta?.canary) continue;
4093
+ out.push({
4094
+ key: entry.key,
4095
+ scope: entry.scope,
4096
+ format: meta.canaryFormat,
4097
+ plantedAt: meta.createdAt,
4098
+ tripCount: meta.accessCount,
4099
+ lastTrippedAt: meta.lastAccessedAt
4100
+ });
4101
+ }
4102
+ return out;
4103
+ }
4104
+
4105
+ // src/cli/commands/canary.ts
4106
+ function registerCanaryCommands(program2) {
4107
+ const canary = program2.command("canary").description("Honeytokens \u2014 fake credentials that raise the alarm on read");
4108
+ canary.command("plant <key>").description("Plant a fake credential; any read fires a loud alert").option(
4109
+ "-f, --format <format>",
4110
+ `Token shape to imitate (${Object.keys(CANARY_FORMATS).join(", ")})`,
4111
+ DEFAULT_CANARY_FORMAT
4112
+ ).option("--value <value>", "Plant this exact value instead of generating one").option("--force", "Overwrite an existing non-canary secret at this key").option("-g, --global", "Global scope (default)").option("-p, --project", "Project scope").option("--team <id>", "Team scope").option("--org <id>", "Org scope").option("--project-path <path>", "Project path (defaults to cwd)").action((key, cmd) => {
4113
+ const opts = buildOpts(cmd);
4114
+ const existing = getEnvelope(key, opts);
4115
+ if (existing && !existing.envelope.meta.canary && !cmd.force) {
4116
+ console.error(
4117
+ c.red(
4118
+ `${SYMBOLS.cross} "${key}" already holds a real secret in ${existing.scope} scope. Use --force to replace it with a canary.`
4119
+ )
4120
+ );
4121
+ process.exit(1);
4122
+ }
4123
+ const result = plantCanary(key, { ...opts, format: cmd.format, value: cmd.value });
4124
+ console.log(
4125
+ `${SYMBOLS.sparkle} ${c.yellow("canary planted")} ${c.bold(result.key)} ${c.dim(`(${result.format}, ${result.scope} scope)`)}`
4126
+ );
4127
+ console.log(c.dim(` value: ${result.value}`));
4128
+ console.log(
4129
+ c.dim(
4130
+ " Reads return this fake value and fire a desktop alert + a 'canary' audit event."
4131
+ )
4132
+ );
4133
+ console.log(
4134
+ c.dim(" Watch trips with: qring canary list \xB7 qring audit --action canary")
4135
+ );
4136
+ });
4137
+ canary.command("list").alias("ls").description("List planted canaries and their trip counts").option("--json", "Output as JSON").option("-g, --global", "Global scope only").option("-p, --project", "Project scope only").option("--team <id>", "Team scope").option("--org <id>", "Org scope").option("--project-path <path>", "Project path (defaults to cwd)").action((cmd) => {
4138
+ const canaries = listCanaries(buildOpts(cmd));
4139
+ if (emitJson(program2, cmd, { canaries })) return;
4140
+ if (canaries.length === 0) {
4141
+ console.log(c.dim("No canaries planted. Plant one: qring canary plant AWS_SECRET_ACCESS_KEY --format aws"));
4142
+ return;
4143
+ }
4144
+ console.log(c.bold(`
4145
+ ${SYMBOLS.eye} Canaries (${canaries.length})
4146
+ `));
4147
+ for (const entry of canaries) {
4148
+ const parts = [c.bold(entry.key)];
4149
+ parts.push(c.dim(`${entry.format ?? "generic"} \xB7 ${entry.scope}`));
4150
+ if (entry.tripCount > 0) {
4151
+ parts.push(c.red(`trips: ${entry.tripCount}`));
4152
+ if (entry.lastTrippedAt) {
4153
+ parts.push(c.red(`last: ${entry.lastTrippedAt}`));
4154
+ }
4155
+ } else {
4156
+ parts.push(c.green("no trips"));
4157
+ }
4158
+ console.log(` ${SYMBOLS.eye} ${parts.join(" ")}`);
4159
+ }
4160
+ console.log();
4161
+ });
4162
+ }
4163
+
4164
+ // src/core/wrap.ts
4165
+ import { randomUUID } from "crypto";
4166
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4167
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4168
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
4169
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
4170
+ import {
4171
+ ListToolsRequestSchema,
4172
+ CallToolRequestSchema
4173
+ } from "@modelcontextprotocol/sdk/types.js";
4174
+ function createAirlockServer(downstream, session) {
4175
+ const downstreamInfo = downstream.getServerVersion();
4176
+ const name = downstreamInfo ? `${downstreamInfo.name} (q-ring airlock)` : "q-ring-airlock";
4177
+ const proxy = new Server(
4178
+ { name, version: PACKAGE_VERSION },
4179
+ { capabilities: { tools: {} } }
4180
+ );
4181
+ proxy.oninitialized = () => {
4182
+ const info = proxy.getClientVersion();
4183
+ if (info) setAuditAgentLabel(`${info.name}@${info.version}`);
4184
+ };
4185
+ proxy.setRequestHandler(ListToolsRequestSchema, async (request) => {
4186
+ return downstream.listTools(request.params);
4187
+ });
4188
+ proxy.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
4189
+ const toolName = request.params.name;
4190
+ logAudit({
4191
+ action: "wrap",
4192
+ source: "mcp",
4193
+ detail: `tool call "${toolName}" \u2192 ${session.label}`,
4194
+ correlationId: session.correlationId
4195
+ });
4196
+ try {
4197
+ return await downstream.callTool(request.params, void 0, {
4198
+ signal: extra.signal
4199
+ });
4200
+ } catch (err) {
4201
+ const message = err instanceof Error ? err.message : String(err);
4202
+ logAudit({
4203
+ action: "wrap",
4204
+ source: "mcp",
4205
+ detail: `tool call "${toolName}" failed: ${message}`,
4206
+ correlationId: session.correlationId
4207
+ });
4208
+ return {
4209
+ content: [
4210
+ { type: "text", text: `airlock: downstream error: ${message}` }
4211
+ ],
4212
+ isError: true
4213
+ };
4214
+ }
4215
+ });
4216
+ return proxy;
4217
+ }
4218
+ async function connectDownstream(opts) {
4219
+ const client = new Client({
4220
+ name: "q-ring-airlock",
4221
+ version: PACKAGE_VERSION
4222
+ });
4223
+ const env = opts.inheritEnv ? Object.fromEntries(
4224
+ Object.entries(process.env).filter(([, v]) => v !== void 0)
4225
+ ) : void 0;
4226
+ const transport = new StdioClientTransport({
4227
+ command: opts.command,
4228
+ args: opts.args ?? [],
4229
+ env,
4230
+ stderr: "inherit"
4231
+ });
4232
+ await client.connect(transport);
4233
+ return client;
4234
+ }
4235
+ async function runWrap(opts) {
4236
+ const label = opts.label ?? [opts.command, ...opts.args ?? []].join(" ");
4237
+ const session = { label, correlationId: randomUUID() };
4238
+ const downstream = await connectDownstream(opts);
4239
+ const downstreamInfo = downstream.getServerVersion();
4240
+ logAudit({
4241
+ action: "wrap",
4242
+ source: "cli",
4243
+ detail: `airlock session started: ${label}${opts.inheritEnv ? " (env inherited)" : " (env stripped)"}`,
4244
+ correlationId: session.correlationId
4245
+ });
4246
+ console.error(
4247
+ `q-ring airlock: wrapping ${downstreamInfo?.name ?? label} \u2014 env ${opts.inheritEnv ? "inherited" : "stripped"}, tool calls audited (session ${session.correlationId.slice(0, 8)})`
4248
+ );
4249
+ const proxy = createAirlockServer(downstream, session);
4250
+ const transport = new StdioServerTransport();
4251
+ return new Promise((resolve) => {
4252
+ let settled = false;
4253
+ const finish = (code, reason) => {
4254
+ if (settled) return;
4255
+ settled = true;
4256
+ logAudit({
4257
+ action: "wrap",
4258
+ source: "cli",
4259
+ detail: `airlock session ended: ${reason}`,
4260
+ correlationId: session.correlationId
4261
+ });
4262
+ void proxy.close().catch(() => {
4263
+ });
4264
+ void downstream.close().catch(() => {
4265
+ });
4266
+ resolve(code);
4267
+ };
4268
+ downstream.onclose = () => finish(1, "wrapped server exited");
4269
+ transport.onclose = () => finish(0, "host disconnected");
4270
+ transport.onerror = (err) => finish(1, `transport error: ${err.message}`);
4271
+ process.stdin.on("end", () => finish(0, "host disconnected"));
4272
+ process.stdout.on("error", (err) => {
4273
+ if (err.code === "EPIPE") finish(0, "host disconnected");
4274
+ else finish(1, `stdout error: ${err.message}`);
4275
+ });
4276
+ proxy.connect(transport).catch((err) => {
4277
+ console.error(
4278
+ `q-ring airlock: failed to start: ${err instanceof Error ? err.message : String(err)}`
4279
+ );
4280
+ finish(1, "startup failure");
4281
+ });
4282
+ });
4283
+ }
4284
+
4285
+ // src/cli/commands/mcp.ts
4286
+ function registerMcpCommands(program2) {
4287
+ const mcp = program2.command("mcp").description("MCP airlock \u2014 run third-party MCP servers behind q-ring");
4288
+ mcp.command("wrap <command...>").description(
4289
+ "Proxy an MCP server through the airlock: spawned with a stripped env, every tool call audited. Use -- before the server command: qring mcp wrap -- npx some-server"
4290
+ ).option(
4291
+ "--inherit-env",
4292
+ "Pass the full parent environment to the wrapped server (default: minimal safe env)"
4293
+ ).option("--label <label>", "Session label for audit events (default: the command line)").action(async (commandArgs, cmd) => {
4294
+ try {
4295
+ const code = await runWrap({
4296
+ command: commandArgs[0],
4297
+ args: commandArgs.slice(1),
4298
+ inheritEnv: cmd.inheritEnv === true,
4299
+ label: cmd.label
4300
+ });
4301
+ process.exit(code);
4302
+ } catch (err) {
4303
+ console.error(
4304
+ c.red(
4305
+ `${SYMBOLS.cross} Airlock failed: ${err instanceof Error ? err.message : String(err)}`
4306
+ )
4307
+ );
4308
+ process.exit(1);
4309
+ }
4310
+ });
4311
+ }
4312
+
4313
+ // src/cli/commands/doctor.ts
4314
+ import { existsSync as existsSync6, readFileSync as readFileSync8, writeFileSync as writeFileSync6, rmSync } from "fs";
4315
+ import { join as join5, delimiter } from "path";
4316
+ import { homedir as homedir2 } from "os";
3414
4317
  function auditDir() {
3415
- return process.env.QRING_AUDIT_DIR ?? join3(homedir(), ".config", "q-ring");
4318
+ return process.env.QRING_AUDIT_DIR ?? join5(homedir2(), ".config", "q-ring");
3416
4319
  }
3417
4320
  function checkNode() {
3418
4321
  const major = Number(process.versions.node.split(".")[0]);
@@ -3452,8 +4355,8 @@ async function checkKeyringBackend() {
3452
4355
  function checkAuditLog() {
3453
4356
  const dir = auditDir();
3454
4357
  try {
3455
- const probeFile = join3(dir, ".doctor-probe");
3456
- writeFileSync5(probeFile, "ok");
4358
+ const probeFile = join5(dir, ".doctor-probe");
4359
+ writeFileSync6(probeFile, "ok");
3457
4360
  rmSync(probeFile);
3458
4361
  } catch (err) {
3459
4362
  return {
@@ -3480,8 +4383,8 @@ function checkAuditLog() {
3480
4383
  };
3481
4384
  }
3482
4385
  function checkManifest(projectPath) {
3483
- const manifestPath = join3(projectPath, ".q-ring.json");
3484
- if (!existsSync4(manifestPath)) {
4386
+ const manifestPath = join5(projectPath, ".q-ring.json");
4387
+ if (!existsSync6(manifestPath)) {
3485
4388
  return {
3486
4389
  name: "project manifest",
3487
4390
  status: "warn",
@@ -3489,7 +4392,7 @@ function checkManifest(projectPath) {
3489
4392
  };
3490
4393
  }
3491
4394
  try {
3492
- const config = JSON.parse(readFileSync6(manifestPath, "utf8"));
4395
+ const config = JSON.parse(readFileSync8(manifestPath, "utf8"));
3493
4396
  const declared = Object.keys(config.secrets ?? {}).length;
3494
4397
  const hasPolicy = !!config.policy;
3495
4398
  return {
@@ -3538,11 +4441,11 @@ function checkMcpBinary() {
3538
4441
  for (const dir of (process.env.PATH ?? "").split(delimiter)) {
3539
4442
  if (!dir) continue;
3540
4443
  for (const ext of exts) {
3541
- if (existsSync4(join3(dir, `qring-mcp${ext}`))) {
4444
+ if (existsSync6(join5(dir, `qring-mcp${ext}`))) {
3542
4445
  return {
3543
4446
  name: "qring-mcp binary",
3544
4447
  status: "ok",
3545
- detail: `found at ${join3(dir, `qring-mcp${ext}`)}`
4448
+ detail: `found at ${join5(dir, `qring-mcp${ext}`)}`
3546
4449
  };
3547
4450
  }
3548
4451
  }
@@ -3749,6 +4652,11 @@ var COMMAND_GROUPS = [
3749
4652
  symbol: SYMBOLS.zap,
3750
4653
  commands: ["exec", "scan", "lint", "status", "doctor", "completion"]
3751
4654
  },
4655
+ {
4656
+ name: "Bridges",
4657
+ symbol: SYMBOLS.link,
4658
+ commands: ["run", "setup", "push"]
4659
+ },
3752
4660
  {
3753
4661
  name: "Audit & Health",
3754
4662
  symbol: SYMBOLS.eye,
@@ -3772,7 +4680,7 @@ var COMMAND_GROUPS = [
3772
4680
  {
3773
4681
  name: "Security & Governance",
3774
4682
  symbol: SYMBOLS.lock,
3775
- commands: ["approve", "approvals", "policy"]
4683
+ commands: ["approve", "approvals", "policy", "canary", "mcp"]
3776
4684
  }
3777
4685
  ];
3778
4686
  var GroupedHelp = class extends Help {
@@ -3889,6 +4797,9 @@ function createProgram() {
3889
4797
  registerHookCommands(program2);
3890
4798
  registerAgentCommands(program2);
3891
4799
  registerSecurityCommands(program2);
4800
+ registerBridgeCommands(program2);
4801
+ registerCanaryCommands(program2);
4802
+ registerMcpCommands(program2);
3892
4803
  registerDoctorCommand(program2);
3893
4804
  registerCompletionCommand(program2);
3894
4805
  return program2;