@i4ctime/q-ring 0.14.0 → 0.15.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/README.md +92 -7
- package/dist/{chunk-C2TFJ2EH.js → chunk-MLBJCPX2.js} +322 -119
- package/dist/chunk-MLBJCPX2.js.map +1 -0
- package/dist/{chunk-NNIEXAW5.js → chunk-TPPPTL4J.js} +319 -116
- package/dist/chunk-TPPPTL4J.js.map +1 -0
- package/dist/{dashboard-KAUEPLXO.js → dashboard-T2UG23KI.js} +2 -2
- package/dist/{dashboard-PYYAED45.js → dashboard-TFWCG23R.js} +2 -2
- package/dist/index.js +647 -18
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +102 -7
- package/dist/mcp.js.map +1 -1
- package/package.json +5 -4
- package/dist/chunk-C2TFJ2EH.js.map +0 -1
- package/dist/chunk-NNIEXAW5.js.map +0 -1
- /package/dist/{dashboard-KAUEPLXO.js.map → dashboard-T2UG23KI.js.map} +0 -0
- /package/dist/{dashboard-PYYAED45.js.map → dashboard-TFWCG23R.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -44,7 +44,7 @@ import {
|
|
|
44
44
|
tunnelList,
|
|
45
45
|
tunnelRead,
|
|
46
46
|
verifyAuditChain
|
|
47
|
-
} from "./chunk-
|
|
47
|
+
} from "./chunk-MLBJCPX2.js";
|
|
48
48
|
|
|
49
49
|
// src/cli/commands.ts
|
|
50
50
|
import { Command, Help } from "commander";
|
|
@@ -764,6 +764,79 @@ var ProviderRegistry = class {
|
|
|
764
764
|
return [...this.providers.values()];
|
|
765
765
|
}
|
|
766
766
|
};
|
|
767
|
+
function livenessProvider(cfg) {
|
|
768
|
+
return {
|
|
769
|
+
name: cfg.name,
|
|
770
|
+
description: cfg.description,
|
|
771
|
+
prefixes: cfg.prefixes,
|
|
772
|
+
async validate(value) {
|
|
773
|
+
const start = Date.now();
|
|
774
|
+
try {
|
|
775
|
+
const { statusCode } = await makeRequest(cfg.url, {
|
|
776
|
+
"User-Agent": "q-ring-validator/1.0",
|
|
777
|
+
...cfg.headers(value)
|
|
778
|
+
});
|
|
779
|
+
const latencyMs = Date.now() - start;
|
|
780
|
+
if (statusCode === 200)
|
|
781
|
+
return { valid: true, status: "valid", message: "API key is valid", latencyMs, provider: cfg.name };
|
|
782
|
+
if (statusCode === 401 || statusCode === 403)
|
|
783
|
+
return { valid: false, status: "invalid", message: `Invalid or revoked API key (${statusCode})`, latencyMs, provider: cfg.name };
|
|
784
|
+
if (statusCode === 429)
|
|
785
|
+
return { valid: true, status: "error", message: "Rate limited \u2014 key may be valid", latencyMs, provider: cfg.name };
|
|
786
|
+
return { valid: false, status: "error", message: `Unexpected status ${statusCode}`, latencyMs, provider: cfg.name };
|
|
787
|
+
} catch (err) {
|
|
788
|
+
return { valid: false, status: "error", message: `${err instanceof Error ? err.message : "Network error"}`, latencyMs: Date.now() - start, provider: cfg.name };
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
var anthropicProvider = livenessProvider({
|
|
794
|
+
name: "anthropic",
|
|
795
|
+
description: "Anthropic API key validation",
|
|
796
|
+
prefixes: ["sk-ant-"],
|
|
797
|
+
url: "https://api.anthropic.com/v1/models?limit=1",
|
|
798
|
+
headers: (value) => ({ "x-api-key": value, "anthropic-version": "2023-06-01" })
|
|
799
|
+
});
|
|
800
|
+
var openrouterProvider = livenessProvider({
|
|
801
|
+
name: "openrouter",
|
|
802
|
+
description: "OpenRouter API key validation",
|
|
803
|
+
prefixes: ["sk-or-"],
|
|
804
|
+
url: "https://openrouter.ai/api/v1/key",
|
|
805
|
+
headers: (value) => ({ Authorization: `Bearer ${value}` })
|
|
806
|
+
});
|
|
807
|
+
var googleAiProvider = livenessProvider({
|
|
808
|
+
name: "google-ai",
|
|
809
|
+
description: "Google AI (Gemini) API key validation",
|
|
810
|
+
prefixes: ["AIza"],
|
|
811
|
+
url: "https://generativelanguage.googleapis.com/v1beta/models?pageSize=1",
|
|
812
|
+
headers: (value) => ({ "x-goog-api-key": value })
|
|
813
|
+
});
|
|
814
|
+
var groqProvider = livenessProvider({
|
|
815
|
+
name: "groq",
|
|
816
|
+
description: "Groq API key validation",
|
|
817
|
+
prefixes: ["gsk_"],
|
|
818
|
+
url: "https://api.groq.com/openai/v1/models",
|
|
819
|
+
headers: (value) => ({ Authorization: `Bearer ${value}` })
|
|
820
|
+
});
|
|
821
|
+
var huggingfaceProvider = livenessProvider({
|
|
822
|
+
name: "huggingface",
|
|
823
|
+
description: "Hugging Face token validation",
|
|
824
|
+
prefixes: ["hf_"],
|
|
825
|
+
url: "https://huggingface.co/api/whoami-v2",
|
|
826
|
+
headers: (value) => ({ Authorization: `Bearer ${value}` })
|
|
827
|
+
});
|
|
828
|
+
var elevenlabsProvider = livenessProvider({
|
|
829
|
+
name: "elevenlabs",
|
|
830
|
+
description: "ElevenLabs API key validation (explicit-only \u2014 set provider on the secret)",
|
|
831
|
+
url: "https://api.elevenlabs.io/v1/user",
|
|
832
|
+
headers: (value) => ({ "xi-api-key": value })
|
|
833
|
+
});
|
|
834
|
+
var vercelProvider = livenessProvider({
|
|
835
|
+
name: "vercel",
|
|
836
|
+
description: "Vercel token validation (explicit-only \u2014 set provider on the secret)",
|
|
837
|
+
url: "https://api.vercel.com/v2/user",
|
|
838
|
+
headers: (value) => ({ Authorization: `Bearer ${value}` })
|
|
839
|
+
});
|
|
767
840
|
var openaiProvider = {
|
|
768
841
|
name: "openai",
|
|
769
842
|
description: "OpenAI API key validation",
|
|
@@ -890,7 +963,14 @@ var httpProvider = {
|
|
|
890
963
|
}
|
|
891
964
|
};
|
|
892
965
|
var registry2 = new ProviderRegistry();
|
|
966
|
+
registry2.register(anthropicProvider);
|
|
967
|
+
registry2.register(openrouterProvider);
|
|
893
968
|
registry2.register(openaiProvider);
|
|
969
|
+
registry2.register(googleAiProvider);
|
|
970
|
+
registry2.register(groqProvider);
|
|
971
|
+
registry2.register(huggingfaceProvider);
|
|
972
|
+
registry2.register(elevenlabsProvider);
|
|
973
|
+
registry2.register(vercelProvider);
|
|
894
974
|
registry2.register(stripeProvider);
|
|
895
975
|
registry2.register(githubProvider);
|
|
896
976
|
registry2.register(awsProvider);
|
|
@@ -1984,10 +2064,9 @@ var RedactionTransform = class extends Transform {
|
|
|
1984
2064
|
callback();
|
|
1985
2065
|
}
|
|
1986
2066
|
};
|
|
1987
|
-
|
|
1988
|
-
const
|
|
1989
|
-
const
|
|
1990
|
-
const policyDecision = checkExecPolicy(fullCommand, opts.projectPath);
|
|
2067
|
+
function enforceExecPolicy(profile, command, args, projectPath) {
|
|
2068
|
+
const fullCommand = [command, ...args].join(" ");
|
|
2069
|
+
const policyDecision = checkExecPolicy(fullCommand, projectPath);
|
|
1991
2070
|
if (!policyDecision.allowed) {
|
|
1992
2071
|
throw new Error(`Policy Denied: ${policyDecision.reason}`);
|
|
1993
2072
|
}
|
|
@@ -2003,9 +2082,13 @@ async function execCommand(opts) {
|
|
|
2003
2082
|
if (profile.allowCommands) {
|
|
2004
2083
|
const allowed = profile.allowCommands.some((a) => fullCommand.startsWith(a));
|
|
2005
2084
|
if (!allowed) {
|
|
2006
|
-
throw new Error(`Exec profile "${profile.name}" does not allow command "${
|
|
2085
|
+
throw new Error(`Exec profile "${profile.name}" does not allow command "${command}"`);
|
|
2007
2086
|
}
|
|
2008
2087
|
}
|
|
2088
|
+
}
|
|
2089
|
+
async function execCommand(opts) {
|
|
2090
|
+
const profile = getProfile(opts.profile);
|
|
2091
|
+
enforceExecPolicy(profile, opts.command, opts.args, opts.projectPath);
|
|
2009
2092
|
const envMap = {};
|
|
2010
2093
|
for (const [k, v] of Object.entries(process.env)) {
|
|
2011
2094
|
if (v !== void 0) envMap[k] = v;
|
|
@@ -2052,6 +2135,18 @@ async function execCommand(opts) {
|
|
|
2052
2135
|
}
|
|
2053
2136
|
}
|
|
2054
2137
|
}
|
|
2138
|
+
return spawnRedacted({
|
|
2139
|
+
profile,
|
|
2140
|
+
command: opts.command,
|
|
2141
|
+
args: opts.args,
|
|
2142
|
+
envMap,
|
|
2143
|
+
secretsToRedact: [...secretsToRedact],
|
|
2144
|
+
captureOutput: opts.captureOutput,
|
|
2145
|
+
projectPath: opts.projectPath
|
|
2146
|
+
});
|
|
2147
|
+
}
|
|
2148
|
+
function spawnRedacted(opts) {
|
|
2149
|
+
const { profile, secretsToRedact, envMap } = opts;
|
|
2055
2150
|
const maxRuntime = profile.maxRuntimeSeconds ?? getExecMaxRuntime(opts.projectPath);
|
|
2056
2151
|
return new Promise((resolve, reject) => {
|
|
2057
2152
|
const networkTools = /* @__PURE__ */ new Set([
|
|
@@ -2492,7 +2587,7 @@ function registerToolingCommands(program2) {
|
|
|
2492
2587
|
}
|
|
2493
2588
|
});
|
|
2494
2589
|
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-
|
|
2590
|
+
const { startDashboardServer } = await import("./dashboard-T2UG23KI.js");
|
|
2496
2591
|
const { exec } = await import("child_process");
|
|
2497
2592
|
const { platform } = await import("os");
|
|
2498
2593
|
const port = Number(cmd.port);
|
|
@@ -3407,12 +3502,540 @@ ${SYMBOLS.lock} Approval Tokens
|
|
|
3407
3502
|
});
|
|
3408
3503
|
}
|
|
3409
3504
|
|
|
3410
|
-
// src/
|
|
3411
|
-
import { existsSync as existsSync4, readFileSync as readFileSync6
|
|
3412
|
-
import { join as join3,
|
|
3505
|
+
// src/core/run.ts
|
|
3506
|
+
import { existsSync as existsSync4, readFileSync as readFileSync6 } from "fs";
|
|
3507
|
+
import { join as join3, resolve as resolvePath } from "path";
|
|
3508
|
+
|
|
3509
|
+
// src/core/refs.ts
|
|
3510
|
+
var REF_PREFIX = "qring://";
|
|
3511
|
+
var KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
3512
|
+
var REF_PATTERN = /^qring:\/\/(global|project|)\/([^/?#]+)(?:\?([^#]*))?$/;
|
|
3513
|
+
function isRef(value) {
|
|
3514
|
+
return value.startsWith(REF_PREFIX);
|
|
3515
|
+
}
|
|
3516
|
+
function parseRef(raw) {
|
|
3517
|
+
if (!isRef(raw)) {
|
|
3518
|
+
throw new Error(`Not a qring:// reference: "${raw}"`);
|
|
3519
|
+
}
|
|
3520
|
+
const match = REF_PATTERN.exec(raw);
|
|
3521
|
+
if (!match) {
|
|
3522
|
+
const hostOnly = /^qring:\/\/([^/?#]+)\/?$/.exec(raw);
|
|
3523
|
+
if (hostOnly && KEY_PATTERN.test(hostOnly[1]) && !["global", "project"].includes(hostOnly[1])) {
|
|
3524
|
+
throw new Error(
|
|
3525
|
+
`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]}".`
|
|
3526
|
+
);
|
|
3527
|
+
}
|
|
3528
|
+
throw new Error(
|
|
3529
|
+
`Invalid ref "${raw}": expected qring://<global|project|>/KEY[?env=<env>]`
|
|
3530
|
+
);
|
|
3531
|
+
}
|
|
3532
|
+
const [, scopeRaw, key, query] = match;
|
|
3533
|
+
if (!KEY_PATTERN.test(key)) {
|
|
3534
|
+
throw new Error(
|
|
3535
|
+
`Invalid ref "${raw}": "${key}" is not a valid secret key (expected [A-Za-z_][A-Za-z0-9_]*)`
|
|
3536
|
+
);
|
|
3537
|
+
}
|
|
3538
|
+
let env;
|
|
3539
|
+
if (query) {
|
|
3540
|
+
for (const part of query.split("&")) {
|
|
3541
|
+
if (!part) continue;
|
|
3542
|
+
const eq = part.indexOf("=");
|
|
3543
|
+
const name = eq === -1 ? part : part.slice(0, eq);
|
|
3544
|
+
const value = eq === -1 ? "" : decodeURIComponent(part.slice(eq + 1));
|
|
3545
|
+
if (name === "env") {
|
|
3546
|
+
env = value;
|
|
3547
|
+
} else {
|
|
3548
|
+
throw new Error(`Invalid ref "${raw}": unknown query parameter "${name}"`);
|
|
3549
|
+
}
|
|
3550
|
+
}
|
|
3551
|
+
}
|
|
3552
|
+
return {
|
|
3553
|
+
scope: scopeRaw === "" ? void 0 : scopeRaw,
|
|
3554
|
+
key,
|
|
3555
|
+
env,
|
|
3556
|
+
raw
|
|
3557
|
+
};
|
|
3558
|
+
}
|
|
3559
|
+
function resolveRef(ref, opts = {}) {
|
|
3560
|
+
const scopes = ref.scope ? [ref.scope] : ["project", "global"];
|
|
3561
|
+
for (const scope of scopes) {
|
|
3562
|
+
const value = getSecret(ref.key, {
|
|
3563
|
+
...opts,
|
|
3564
|
+
scope,
|
|
3565
|
+
env: ref.env ?? opts.env
|
|
3566
|
+
});
|
|
3567
|
+
if (value !== null) return value;
|
|
3568
|
+
}
|
|
3569
|
+
return null;
|
|
3570
|
+
}
|
|
3571
|
+
function resolveRefsInMap(map, opts = {}) {
|
|
3572
|
+
const resolved = {};
|
|
3573
|
+
const secretValues = [];
|
|
3574
|
+
const missing = [];
|
|
3575
|
+
for (const [name, value] of Object.entries(map)) {
|
|
3576
|
+
if (!isRef(value)) {
|
|
3577
|
+
resolved[name] = value;
|
|
3578
|
+
continue;
|
|
3579
|
+
}
|
|
3580
|
+
const ref = parseRef(value);
|
|
3581
|
+
const secret = resolveRef(ref, opts);
|
|
3582
|
+
if (secret === null) {
|
|
3583
|
+
missing.push({ name, ref });
|
|
3584
|
+
continue;
|
|
3585
|
+
}
|
|
3586
|
+
resolved[name] = secret;
|
|
3587
|
+
secretValues.push(secret);
|
|
3588
|
+
}
|
|
3589
|
+
return { resolved, secretValues, missing };
|
|
3590
|
+
}
|
|
3591
|
+
|
|
3592
|
+
// src/core/run.ts
|
|
3593
|
+
function buildRunPlan(opts) {
|
|
3594
|
+
const projectPath = opts.projectPath ?? process.cwd();
|
|
3595
|
+
const keyringOpts = {
|
|
3596
|
+
projectPath,
|
|
3597
|
+
env: opts.env,
|
|
3598
|
+
source: opts.source ?? "cli",
|
|
3599
|
+
silent: opts.silent
|
|
3600
|
+
};
|
|
3601
|
+
const envMap = {};
|
|
3602
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
3603
|
+
if (v !== void 0) envMap[k] = v;
|
|
3604
|
+
}
|
|
3605
|
+
const injected = [];
|
|
3606
|
+
const missingRequired = [];
|
|
3607
|
+
const missingOptional = [];
|
|
3608
|
+
const secretsToRedact = /* @__PURE__ */ new Set();
|
|
3609
|
+
if (opts.useManifest !== false) {
|
|
3610
|
+
const config = readProjectConfig(projectPath);
|
|
3611
|
+
for (const [key, entry] of Object.entries(config?.secrets ?? {})) {
|
|
3612
|
+
const value = resolveRef(
|
|
3613
|
+
{ key, raw: `manifest:${key}` },
|
|
3614
|
+
keyringOpts
|
|
3615
|
+
);
|
|
3616
|
+
if (value === null) {
|
|
3617
|
+
if (entry.required !== false) missingRequired.push(key);
|
|
3618
|
+
else if (opts.strict) missingRequired.push(key);
|
|
3619
|
+
else missingOptional.push(key);
|
|
3620
|
+
continue;
|
|
3621
|
+
}
|
|
3622
|
+
envMap[key] = value;
|
|
3623
|
+
injected.push({ name: key, source: "manifest" });
|
|
3624
|
+
if (value.length > 5) secretsToRedact.add(value);
|
|
3625
|
+
}
|
|
3626
|
+
}
|
|
3627
|
+
const envFiles = opts.envFiles ?? (existsSync4(join3(projectPath, ".env")) ? [join3(projectPath, ".env")] : []);
|
|
3628
|
+
for (const file of envFiles) {
|
|
3629
|
+
const path = resolvePath(projectPath, file);
|
|
3630
|
+
if (!existsSync4(path)) {
|
|
3631
|
+
throw new Error(`Env file not found: ${path}`);
|
|
3632
|
+
}
|
|
3633
|
+
const parsed = Object.fromEntries(parseDotenv(readFileSync6(path, "utf8")));
|
|
3634
|
+
const { resolved, secretValues, missing } = resolveRefsInMap(parsed, keyringOpts);
|
|
3635
|
+
for (const [name, value] of Object.entries(resolved)) {
|
|
3636
|
+
envMap[name] = value;
|
|
3637
|
+
injected.push({
|
|
3638
|
+
name,
|
|
3639
|
+
source: isRef(parsed[name]) ? "env-file-ref" : "env-file"
|
|
3640
|
+
});
|
|
3641
|
+
}
|
|
3642
|
+
for (const value of secretValues) {
|
|
3643
|
+
if (value.length > 5) secretsToRedact.add(value);
|
|
3644
|
+
}
|
|
3645
|
+
for (const m of missing) missingRequired.push(m.name);
|
|
3646
|
+
}
|
|
3647
|
+
return {
|
|
3648
|
+
envFiles: envFiles.map((f) => resolvePath(projectPath, f)),
|
|
3649
|
+
injected,
|
|
3650
|
+
missingRequired,
|
|
3651
|
+
missingOptional,
|
|
3652
|
+
envMap,
|
|
3653
|
+
secretsToRedact: [...secretsToRedact]
|
|
3654
|
+
};
|
|
3655
|
+
}
|
|
3656
|
+
async function runCommand(opts) {
|
|
3657
|
+
const profile = getProfile(opts.profile);
|
|
3658
|
+
enforceExecPolicy(profile, opts.command, opts.args, opts.projectPath);
|
|
3659
|
+
const plan = buildRunPlan(opts);
|
|
3660
|
+
if (plan.missingRequired.length > 0) {
|
|
3661
|
+
throw new Error(
|
|
3662
|
+
`Missing required secrets: ${plan.missingRequired.join(", ")}. Store them with \`qring set <KEY>\` or drop them from the manifest/.env refs.`
|
|
3663
|
+
);
|
|
3664
|
+
}
|
|
3665
|
+
if (profile.stripEnvVars) {
|
|
3666
|
+
for (const key of profile.stripEnvVars) {
|
|
3667
|
+
delete plan.envMap[key];
|
|
3668
|
+
}
|
|
3669
|
+
}
|
|
3670
|
+
const result = await spawnRedacted({
|
|
3671
|
+
profile,
|
|
3672
|
+
command: opts.command,
|
|
3673
|
+
args: opts.args,
|
|
3674
|
+
envMap: plan.envMap,
|
|
3675
|
+
secretsToRedact: plan.secretsToRedact,
|
|
3676
|
+
captureOutput: opts.captureOutput,
|
|
3677
|
+
projectPath: opts.projectPath
|
|
3678
|
+
});
|
|
3679
|
+
return { ...result, plan };
|
|
3680
|
+
}
|
|
3681
|
+
|
|
3682
|
+
// src/core/setup.ts
|
|
3683
|
+
import { existsSync as existsSync5, mkdirSync, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
|
|
3413
3684
|
import { homedir } from "os";
|
|
3685
|
+
import { dirname, join as join4 } from "path";
|
|
3686
|
+
var EDITORS = ["cursor", "kiro", "claude"];
|
|
3687
|
+
var READ_ONLY_TOOLS = [
|
|
3688
|
+
"list_secrets",
|
|
3689
|
+
"has_secret",
|
|
3690
|
+
"inspect_secret",
|
|
3691
|
+
"detect_environment",
|
|
3692
|
+
"get_project_context",
|
|
3693
|
+
"list_providers",
|
|
3694
|
+
"list_hooks",
|
|
3695
|
+
"get_policy_summary",
|
|
3696
|
+
"audit_log",
|
|
3697
|
+
"verify_audit_chain",
|
|
3698
|
+
"health_check",
|
|
3699
|
+
"analyze_secrets"
|
|
3700
|
+
];
|
|
3701
|
+
var SERVER_NAME = "q-ring";
|
|
3702
|
+
function serverEntry(editor) {
|
|
3703
|
+
switch (editor) {
|
|
3704
|
+
case "cursor":
|
|
3705
|
+
return { command: "qring-mcp", args: [], env: {} };
|
|
3706
|
+
case "kiro":
|
|
3707
|
+
return {
|
|
3708
|
+
command: "qring-mcp",
|
|
3709
|
+
args: [],
|
|
3710
|
+
env: {},
|
|
3711
|
+
disabled: false,
|
|
3712
|
+
autoApprove: READ_ONLY_TOOLS
|
|
3713
|
+
};
|
|
3714
|
+
case "claude":
|
|
3715
|
+
return { type: "stdio", command: "qring-mcp", args: [], env: {} };
|
|
3716
|
+
}
|
|
3717
|
+
}
|
|
3718
|
+
function configPathFor(editor, global, projectPath) {
|
|
3719
|
+
const home = homedir();
|
|
3720
|
+
switch (editor) {
|
|
3721
|
+
case "cursor":
|
|
3722
|
+
return global ? join4(home, ".cursor", "mcp.json") : join4(projectPath, ".cursor", "mcp.json");
|
|
3723
|
+
case "kiro":
|
|
3724
|
+
return global ? join4(home, ".kiro", "settings", "mcp.json") : join4(projectPath, ".kiro", "settings", "mcp.json");
|
|
3725
|
+
case "claude":
|
|
3726
|
+
if (global) {
|
|
3727
|
+
throw new Error(
|
|
3728
|
+
"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"
|
|
3729
|
+
);
|
|
3730
|
+
}
|
|
3731
|
+
return join4(projectPath, ".mcp.json");
|
|
3732
|
+
}
|
|
3733
|
+
}
|
|
3734
|
+
function setupEditor(opts) {
|
|
3735
|
+
const projectPath = opts.projectPath ?? process.cwd();
|
|
3736
|
+
const configPath = configPathFor(opts.editor, opts.global ?? false, projectPath);
|
|
3737
|
+
const warnings = [];
|
|
3738
|
+
let config = {};
|
|
3739
|
+
if (existsSync5(configPath)) {
|
|
3740
|
+
const raw = readFileSync7(configPath, "utf8");
|
|
3741
|
+
try {
|
|
3742
|
+
config = JSON.parse(raw);
|
|
3743
|
+
} catch {
|
|
3744
|
+
throw new Error(
|
|
3745
|
+
`${configPath} exists but is not valid JSON \u2014 fix or remove it, then re-run.`
|
|
3746
|
+
);
|
|
3747
|
+
}
|
|
3748
|
+
}
|
|
3749
|
+
const servers = config.mcpServers ?? {};
|
|
3750
|
+
const desired = serverEntry(opts.editor);
|
|
3751
|
+
const existing = servers[SERVER_NAME];
|
|
3752
|
+
let action;
|
|
3753
|
+
if (existing === void 0) {
|
|
3754
|
+
action = existsSync5(configPath) ? "updated" : "created";
|
|
3755
|
+
servers[SERVER_NAME] = desired;
|
|
3756
|
+
} else if (JSON.stringify(existing) === JSON.stringify(desired)) {
|
|
3757
|
+
action = "unchanged";
|
|
3758
|
+
} else if (opts.force) {
|
|
3759
|
+
action = "updated";
|
|
3760
|
+
servers[SERVER_NAME] = desired;
|
|
3761
|
+
} else {
|
|
3762
|
+
action = "unchanged";
|
|
3763
|
+
warnings.push(
|
|
3764
|
+
`A different "${SERVER_NAME}" entry already exists in ${configPath} \u2014 left as-is (use --force to replace it).`
|
|
3765
|
+
);
|
|
3766
|
+
}
|
|
3767
|
+
config.mcpServers = servers;
|
|
3768
|
+
if (action !== "unchanged" && !opts.dryRun) {
|
|
3769
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
3770
|
+
writeFileSync5(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
3771
|
+
}
|
|
3772
|
+
return {
|
|
3773
|
+
editor: opts.editor,
|
|
3774
|
+
configPath,
|
|
3775
|
+
serverName: SERVER_NAME,
|
|
3776
|
+
action,
|
|
3777
|
+
dryRun: opts.dryRun ?? false,
|
|
3778
|
+
warnings
|
|
3779
|
+
};
|
|
3780
|
+
}
|
|
3781
|
+
|
|
3782
|
+
// src/core/push.ts
|
|
3783
|
+
import { spawnSync } from "child_process";
|
|
3784
|
+
var PUSH_TARGETS = ["github", "vercel", "cloudflare"];
|
|
3785
|
+
var TARGETS = {
|
|
3786
|
+
github: {
|
|
3787
|
+
binary: "gh",
|
|
3788
|
+
args: (key, opts) => [
|
|
3789
|
+
["secret", "set", key, ...opts.repo ? ["--repo", opts.repo] : []]
|
|
3790
|
+
],
|
|
3791
|
+
installHint: "install the GitHub CLI: https://cli.github.com (then `gh auth login`)"
|
|
3792
|
+
},
|
|
3793
|
+
vercel: {
|
|
3794
|
+
binary: "vercel",
|
|
3795
|
+
// One invocation per environment — `vercel env add` takes a single target.
|
|
3796
|
+
args: (key, opts) => (opts.vercelEnvs ?? ["production"]).map((env) => [
|
|
3797
|
+
"env",
|
|
3798
|
+
"add",
|
|
3799
|
+
key,
|
|
3800
|
+
env,
|
|
3801
|
+
"--force"
|
|
3802
|
+
]),
|
|
3803
|
+
installHint: "install the Vercel CLI: npm i -g vercel (then `vercel link` in the project)"
|
|
3804
|
+
},
|
|
3805
|
+
cloudflare: {
|
|
3806
|
+
binary: "wrangler",
|
|
3807
|
+
args: (key) => [["secret", "put", key]],
|
|
3808
|
+
installHint: "install Wrangler: npm i -g wrangler (then `wrangler login`)"
|
|
3809
|
+
}
|
|
3810
|
+
};
|
|
3811
|
+
function binaryAvailable(binary) {
|
|
3812
|
+
const probe = spawnSync(binary, ["--version"], { stdio: "ignore", shell: false });
|
|
3813
|
+
return !probe.error;
|
|
3814
|
+
}
|
|
3815
|
+
function resolvePushKeys(opts) {
|
|
3816
|
+
if (opts.keys?.length) return opts.keys;
|
|
3817
|
+
const config = readProjectConfig(opts.projectPath);
|
|
3818
|
+
const manifestKeys = Object.keys(config?.secrets ?? {});
|
|
3819
|
+
if (manifestKeys.length === 0) {
|
|
3820
|
+
throw new Error(
|
|
3821
|
+
"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."
|
|
3822
|
+
);
|
|
3823
|
+
}
|
|
3824
|
+
return manifestKeys;
|
|
3825
|
+
}
|
|
3826
|
+
function pushSecrets(opts) {
|
|
3827
|
+
const target = TARGETS[opts.target];
|
|
3828
|
+
const keys = resolvePushKeys(opts);
|
|
3829
|
+
if (!opts.dryRun && !binaryAvailable(target.binary)) {
|
|
3830
|
+
throw new Error(`"${target.binary}" CLI not found \u2014 ${target.installHint}`);
|
|
3831
|
+
}
|
|
3832
|
+
const result = { target: opts.target, pushed: [], failed: [], missing: [] };
|
|
3833
|
+
for (const key of keys) {
|
|
3834
|
+
const value = resolveRef(
|
|
3835
|
+
{ key, raw: `push:${key}` },
|
|
3836
|
+
{
|
|
3837
|
+
projectPath: opts.projectPath,
|
|
3838
|
+
env: opts.env,
|
|
3839
|
+
source: opts.source ?? "cli",
|
|
3840
|
+
silent: opts.silent
|
|
3841
|
+
}
|
|
3842
|
+
);
|
|
3843
|
+
if (value === null) {
|
|
3844
|
+
result.missing.push(key);
|
|
3845
|
+
continue;
|
|
3846
|
+
}
|
|
3847
|
+
if (opts.dryRun) {
|
|
3848
|
+
result.pushed.push(key);
|
|
3849
|
+
continue;
|
|
3850
|
+
}
|
|
3851
|
+
let failedInvocation = null;
|
|
3852
|
+
for (const args of target.args(key, opts)) {
|
|
3853
|
+
const child = spawnSync(target.binary, args, {
|
|
3854
|
+
input: value,
|
|
3855
|
+
cwd: opts.projectPath,
|
|
3856
|
+
encoding: "utf8",
|
|
3857
|
+
shell: false
|
|
3858
|
+
});
|
|
3859
|
+
if (child.status !== 0) {
|
|
3860
|
+
failedInvocation = child.stderr?.trim() || child.error?.message || `exit ${child.status}`;
|
|
3861
|
+
break;
|
|
3862
|
+
}
|
|
3863
|
+
}
|
|
3864
|
+
if (failedInvocation) {
|
|
3865
|
+
result.failed.push({ key, error: failedInvocation });
|
|
3866
|
+
continue;
|
|
3867
|
+
}
|
|
3868
|
+
result.pushed.push(key);
|
|
3869
|
+
if (!opts.silent) {
|
|
3870
|
+
logAudit({
|
|
3871
|
+
action: "push",
|
|
3872
|
+
key,
|
|
3873
|
+
env: opts.env,
|
|
3874
|
+
source: opts.source ?? "cli",
|
|
3875
|
+
detail: `pushed to ${opts.target}${opts.repo ? ` (${opts.repo})` : ""}`
|
|
3876
|
+
});
|
|
3877
|
+
}
|
|
3878
|
+
}
|
|
3879
|
+
return result;
|
|
3880
|
+
}
|
|
3881
|
+
|
|
3882
|
+
// src/cli/commands/bridges.ts
|
|
3883
|
+
function registerBridgeCommands(program2) {
|
|
3884
|
+
program2.command("run <command...>").description(
|
|
3885
|
+
"Run a command with only declared secrets injected \u2014 .q-ring.json manifest keys plus qring:// refs from .env (output auto-redacted)"
|
|
3886
|
+
).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) => {
|
|
3887
|
+
const opts = {
|
|
3888
|
+
command: commandArgs[0],
|
|
3889
|
+
args: commandArgs.slice(1),
|
|
3890
|
+
projectPath: cmd.projectPath ?? process.cwd(),
|
|
3891
|
+
env: cmd.env,
|
|
3892
|
+
envFiles: cmd.envFile,
|
|
3893
|
+
useManifest: cmd.manifest !== false,
|
|
3894
|
+
profile: cmd.profile,
|
|
3895
|
+
strict: cmd.strict === true,
|
|
3896
|
+
source: "cli"
|
|
3897
|
+
};
|
|
3898
|
+
try {
|
|
3899
|
+
if (cmd.dryRun) {
|
|
3900
|
+
const plan2 = buildRunPlan({ ...opts, silent: true });
|
|
3901
|
+
console.log(`
|
|
3902
|
+
${SYMBOLS.zap} ${c.bold("qring run")} ${c.dim("(dry run)")}`);
|
|
3903
|
+
if (plan2.envFiles.length > 0) {
|
|
3904
|
+
console.log(` Env files: ${plan2.envFiles.map((f) => c.cyan(f)).join(", ")}`);
|
|
3905
|
+
}
|
|
3906
|
+
for (const v of plan2.injected) {
|
|
3907
|
+
console.log(` ${c.green(SYMBOLS.check)} ${v.name} ${c.dim(`(${v.source})`)}`);
|
|
3908
|
+
}
|
|
3909
|
+
for (const name of plan2.missingOptional) {
|
|
3910
|
+
console.log(` ${c.yellow("?")} ${name} ${c.dim("(optional, missing)")}`);
|
|
3911
|
+
}
|
|
3912
|
+
for (const name of plan2.missingRequired) {
|
|
3913
|
+
console.log(` ${c.red(SYMBOLS.cross)} ${name} ${c.dim("(required, MISSING)")}`);
|
|
3914
|
+
}
|
|
3915
|
+
process.exit(plan2.missingRequired.length > 0 ? 1 : 0);
|
|
3916
|
+
}
|
|
3917
|
+
const { code, plan } = await runCommand(opts);
|
|
3918
|
+
if (plan.missingOptional.length > 0) {
|
|
3919
|
+
console.error(
|
|
3920
|
+
c.yellow(
|
|
3921
|
+
`${SYMBOLS.warning} Optional secrets not found: ${plan.missingOptional.join(", ")}`
|
|
3922
|
+
)
|
|
3923
|
+
);
|
|
3924
|
+
}
|
|
3925
|
+
process.exit(code);
|
|
3926
|
+
} catch (err) {
|
|
3927
|
+
console.error(
|
|
3928
|
+
c.red(
|
|
3929
|
+
`${SYMBOLS.cross} Run failed: ${err instanceof Error ? err.message : String(err)}`
|
|
3930
|
+
)
|
|
3931
|
+
);
|
|
3932
|
+
process.exit(1);
|
|
3933
|
+
}
|
|
3934
|
+
});
|
|
3935
|
+
program2.command("push <target>").description(
|
|
3936
|
+
`Push manifest secrets to a deployment platform via its own CLI (${PUSH_TARGETS.join(", ")}) \u2014 values travel over stdin, never argv`
|
|
3937
|
+
).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(
|
|
3938
|
+
"--vercel-env <envs>",
|
|
3939
|
+
"Comma-separated Vercel environments (default: production)"
|
|
3940
|
+
).option("--dry-run", "Show what would be pushed without pushing").option("--json", "Output as JSON").action((targetArg, cmd) => {
|
|
3941
|
+
if (!PUSH_TARGETS.includes(targetArg)) {
|
|
3942
|
+
console.error(
|
|
3943
|
+
c.red(`${SYMBOLS.cross} Unknown target "${targetArg}" \u2014 expected one of: ${PUSH_TARGETS.join(", ")}`)
|
|
3944
|
+
);
|
|
3945
|
+
process.exit(1);
|
|
3946
|
+
}
|
|
3947
|
+
try {
|
|
3948
|
+
const result = pushSecrets({
|
|
3949
|
+
target: targetArg,
|
|
3950
|
+
keys: cmd.keys?.split(",").map((k) => k.trim()),
|
|
3951
|
+
projectPath: cmd.projectPath ?? process.cwd(),
|
|
3952
|
+
env: cmd.env,
|
|
3953
|
+
repo: cmd.repo,
|
|
3954
|
+
vercelEnvs: cmd.vercelEnv?.split(",").map((e) => e.trim()),
|
|
3955
|
+
dryRun: cmd.dryRun === true,
|
|
3956
|
+
source: "cli"
|
|
3957
|
+
});
|
|
3958
|
+
if (wantsJsonOutput(program2, cmd)) {
|
|
3959
|
+
console.log(JSON.stringify(result, null, 2));
|
|
3960
|
+
process.exit(result.failed.length > 0 ? 1 : 0);
|
|
3961
|
+
}
|
|
3962
|
+
const prefix = cmd.dryRun ? c.dim("[dry-run] ") : "";
|
|
3963
|
+
console.log(`
|
|
3964
|
+
${SYMBOLS.link} ${prefix}${c.bold(`qring push ${targetArg}`)}`);
|
|
3965
|
+
for (const key of result.pushed) {
|
|
3966
|
+
console.log(` ${c.green(SYMBOLS.check)} ${key}`);
|
|
3967
|
+
}
|
|
3968
|
+
for (const key of result.missing) {
|
|
3969
|
+
console.log(` ${c.yellow("?")} ${key} ${c.dim("(not in keyring \u2014 skipped)")}`);
|
|
3970
|
+
}
|
|
3971
|
+
for (const { key, error } of result.failed) {
|
|
3972
|
+
console.log(` ${c.red(SYMBOLS.cross)} ${key} ${c.dim(`\u2014 ${error}`)}`);
|
|
3973
|
+
}
|
|
3974
|
+
console.log(
|
|
3975
|
+
c.dim(
|
|
3976
|
+
` ${result.pushed.length} pushed${cmd.dryRun ? " (dry run)" : ""}, ${result.missing.length} missing, ${result.failed.length} failed`
|
|
3977
|
+
)
|
|
3978
|
+
);
|
|
3979
|
+
process.exit(result.failed.length > 0 ? 1 : 0);
|
|
3980
|
+
} catch (err) {
|
|
3981
|
+
console.error(
|
|
3982
|
+
c.red(
|
|
3983
|
+
`${SYMBOLS.cross} Push failed: ${err instanceof Error ? err.message : String(err)}`
|
|
3984
|
+
)
|
|
3985
|
+
);
|
|
3986
|
+
process.exit(1);
|
|
3987
|
+
}
|
|
3988
|
+
});
|
|
3989
|
+
program2.command("setup <editor>").description(
|
|
3990
|
+
`Wire the q-ring MCP server into an editor's MCP config (${EDITORS.join(", ")})`
|
|
3991
|
+
).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) => {
|
|
3992
|
+
if (!EDITORS.includes(editor)) {
|
|
3993
|
+
console.error(
|
|
3994
|
+
c.red(`${SYMBOLS.cross} Unknown editor "${editor}" \u2014 expected one of: ${EDITORS.join(", ")}`)
|
|
3995
|
+
);
|
|
3996
|
+
process.exit(1);
|
|
3997
|
+
}
|
|
3998
|
+
try {
|
|
3999
|
+
const result = setupEditor({
|
|
4000
|
+
editor,
|
|
4001
|
+
global: cmd.global === true,
|
|
4002
|
+
projectPath: cmd.projectPath ?? process.cwd(),
|
|
4003
|
+
force: cmd.force === true,
|
|
4004
|
+
dryRun: cmd.dryRun === true
|
|
4005
|
+
});
|
|
4006
|
+
if (wantsJsonOutput(program2, cmd)) {
|
|
4007
|
+
console.log(JSON.stringify(result, null, 2));
|
|
4008
|
+
return;
|
|
4009
|
+
}
|
|
4010
|
+
const verb = result.action === "created" ? "Created" : result.action === "updated" ? "Updated" : "Already configured";
|
|
4011
|
+
const prefix = result.dryRun ? c.dim("[dry-run] ") : "";
|
|
4012
|
+
console.log(
|
|
4013
|
+
`
|
|
4014
|
+
${SYMBOLS.check} ${prefix}${c.bold(verb)}: ${c.cyan(result.configPath)} ${c.dim(`(server "${result.serverName}")`)}`
|
|
4015
|
+
);
|
|
4016
|
+
for (const warning of result.warnings) {
|
|
4017
|
+
console.log(` ${c.yellow("!")} ${warning}`);
|
|
4018
|
+
}
|
|
4019
|
+
if (result.action !== "unchanged") {
|
|
4020
|
+
console.log(c.dim(` Restart ${editor} to pick up the MCP server. Verify with: qring doctor`));
|
|
4021
|
+
}
|
|
4022
|
+
} catch (err) {
|
|
4023
|
+
console.error(
|
|
4024
|
+
c.red(
|
|
4025
|
+
`${SYMBOLS.cross} Setup failed: ${err instanceof Error ? err.message : String(err)}`
|
|
4026
|
+
)
|
|
4027
|
+
);
|
|
4028
|
+
process.exit(1);
|
|
4029
|
+
}
|
|
4030
|
+
});
|
|
4031
|
+
}
|
|
4032
|
+
|
|
4033
|
+
// src/cli/commands/doctor.ts
|
|
4034
|
+
import { existsSync as existsSync6, readFileSync as readFileSync8, writeFileSync as writeFileSync6, rmSync } from "fs";
|
|
4035
|
+
import { join as join5, delimiter } from "path";
|
|
4036
|
+
import { homedir as homedir2 } from "os";
|
|
3414
4037
|
function auditDir() {
|
|
3415
|
-
return process.env.QRING_AUDIT_DIR ??
|
|
4038
|
+
return process.env.QRING_AUDIT_DIR ?? join5(homedir2(), ".config", "q-ring");
|
|
3416
4039
|
}
|
|
3417
4040
|
function checkNode() {
|
|
3418
4041
|
const major = Number(process.versions.node.split(".")[0]);
|
|
@@ -3452,8 +4075,8 @@ async function checkKeyringBackend() {
|
|
|
3452
4075
|
function checkAuditLog() {
|
|
3453
4076
|
const dir = auditDir();
|
|
3454
4077
|
try {
|
|
3455
|
-
const probeFile =
|
|
3456
|
-
|
|
4078
|
+
const probeFile = join5(dir, ".doctor-probe");
|
|
4079
|
+
writeFileSync6(probeFile, "ok");
|
|
3457
4080
|
rmSync(probeFile);
|
|
3458
4081
|
} catch (err) {
|
|
3459
4082
|
return {
|
|
@@ -3480,8 +4103,8 @@ function checkAuditLog() {
|
|
|
3480
4103
|
};
|
|
3481
4104
|
}
|
|
3482
4105
|
function checkManifest(projectPath) {
|
|
3483
|
-
const manifestPath =
|
|
3484
|
-
if (!
|
|
4106
|
+
const manifestPath = join5(projectPath, ".q-ring.json");
|
|
4107
|
+
if (!existsSync6(manifestPath)) {
|
|
3485
4108
|
return {
|
|
3486
4109
|
name: "project manifest",
|
|
3487
4110
|
status: "warn",
|
|
@@ -3489,7 +4112,7 @@ function checkManifest(projectPath) {
|
|
|
3489
4112
|
};
|
|
3490
4113
|
}
|
|
3491
4114
|
try {
|
|
3492
|
-
const config = JSON.parse(
|
|
4115
|
+
const config = JSON.parse(readFileSync8(manifestPath, "utf8"));
|
|
3493
4116
|
const declared = Object.keys(config.secrets ?? {}).length;
|
|
3494
4117
|
const hasPolicy = !!config.policy;
|
|
3495
4118
|
return {
|
|
@@ -3538,11 +4161,11 @@ function checkMcpBinary() {
|
|
|
3538
4161
|
for (const dir of (process.env.PATH ?? "").split(delimiter)) {
|
|
3539
4162
|
if (!dir) continue;
|
|
3540
4163
|
for (const ext of exts) {
|
|
3541
|
-
if (
|
|
4164
|
+
if (existsSync6(join5(dir, `qring-mcp${ext}`))) {
|
|
3542
4165
|
return {
|
|
3543
4166
|
name: "qring-mcp binary",
|
|
3544
4167
|
status: "ok",
|
|
3545
|
-
detail: `found at ${
|
|
4168
|
+
detail: `found at ${join5(dir, `qring-mcp${ext}`)}`
|
|
3546
4169
|
};
|
|
3547
4170
|
}
|
|
3548
4171
|
}
|
|
@@ -3749,6 +4372,11 @@ var COMMAND_GROUPS = [
|
|
|
3749
4372
|
symbol: SYMBOLS.zap,
|
|
3750
4373
|
commands: ["exec", "scan", "lint", "status", "doctor", "completion"]
|
|
3751
4374
|
},
|
|
4375
|
+
{
|
|
4376
|
+
name: "Bridges",
|
|
4377
|
+
symbol: SYMBOLS.link,
|
|
4378
|
+
commands: ["run", "setup", "push"]
|
|
4379
|
+
},
|
|
3752
4380
|
{
|
|
3753
4381
|
name: "Audit & Health",
|
|
3754
4382
|
symbol: SYMBOLS.eye,
|
|
@@ -3889,6 +4517,7 @@ function createProgram() {
|
|
|
3889
4517
|
registerHookCommands(program2);
|
|
3890
4518
|
registerAgentCommands(program2);
|
|
3891
4519
|
registerSecurityCommands(program2);
|
|
4520
|
+
registerBridgeCommands(program2);
|
|
3892
4521
|
registerDoctorCommand(program2);
|
|
3893
4522
|
registerCompletionCommand(program2);
|
|
3894
4523
|
return program2;
|