quiver-cli 1.1.0 → 1.2.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 +9 -3
- package/dist/cli.js +126 -25
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -160,9 +160,15 @@ This is the basis for `sync` and `check`.
|
|
|
160
160
|
poisoning), shown as a readable before/after.
|
|
161
161
|
|
|
162
162
|
The first successful introspection records a baseline; subsequent `check` runs
|
|
163
|
-
diff against it.
|
|
164
|
-
|
|
165
|
-
|
|
163
|
+
diff against it. stdio servers run foreign code and are only introspected with
|
|
164
|
+
`--introspect-stdio`.
|
|
165
|
+
|
|
166
|
+
**OAuth-protected servers** (e.g. Linear): `check` reuses opencode's MCP
|
|
167
|
+
credentials (`~/.local/share/opencode/mcp-auth.json`, read-only — quiver never
|
|
168
|
+
refreshes or rewrites them). Authenticate once with
|
|
169
|
+
`opencode mcp auth <name>`, then re-run `quiver-cli check` to record the tool
|
|
170
|
+
snapshot. Without a valid token the server is skipped with an actionable hint,
|
|
171
|
+
and `quiver-cli list` shows why the tool count is missing.
|
|
166
172
|
|
|
167
173
|
Pass `--offline` to skip MCP re-introspection entirely and check only digests
|
|
168
174
|
and provider shims — no network, no foreign code, useful for a fast local
|
package/dist/cli.js
CHANGED
|
@@ -2569,7 +2569,8 @@ var init_list = __esm({
|
|
|
2569
2569
|
transport: entry.transport,
|
|
2570
2570
|
enabled: !disabled.has(name),
|
|
2571
2571
|
detail: serverDetail2.get(name) ?? null,
|
|
2572
|
-
toolCount: entry.tools ? Object.keys(entry.tools).length : null
|
|
2572
|
+
toolCount: entry.tools ? Object.keys(entry.tools).length : null,
|
|
2573
|
+
authRequired: entry.authRequired ?? false
|
|
2573
2574
|
})),
|
|
2574
2575
|
plugins: plugins.map(({ name, entry }) => ({
|
|
2575
2576
|
name,
|
|
@@ -2609,6 +2610,7 @@ var init_list = __esm({
|
|
|
2609
2610
|
}
|
|
2610
2611
|
}
|
|
2611
2612
|
let missingTools = false;
|
|
2613
|
+
const needsAuth = [];
|
|
2612
2614
|
if (mcp.length) {
|
|
2613
2615
|
const nameW = Math.max(...mcp.map((e) => e.name.length));
|
|
2614
2616
|
const toolW = Math.max(
|
|
@@ -2620,7 +2622,10 @@ var init_list = __esm({
|
|
|
2620
2622
|
lines.push("", ` ${c.bold("mcp servers")}`);
|
|
2621
2623
|
for (const { name, entry } of mcp) {
|
|
2622
2624
|
const count = entry.tools ? Object.keys(entry.tools).length : null;
|
|
2623
|
-
if (count === null)
|
|
2625
|
+
if (count === null) {
|
|
2626
|
+
if (entry.authRequired) needsAuth.push(name);
|
|
2627
|
+
else missingTools = true;
|
|
2628
|
+
}
|
|
2624
2629
|
const tools = padCell(
|
|
2625
2630
|
`${count ?? "?"} tools`,
|
|
2626
2631
|
toolW,
|
|
@@ -2647,6 +2652,13 @@ var init_list = __esm({
|
|
|
2647
2652
|
`${skills.length} skills \xB7 ${commands.length} commands \xB7 ${mcp.length} MCP servers \xB7 ${plugins.length} plugins`
|
|
2648
2653
|
)} ${c.dim(`providers: ${providers2}`)}`
|
|
2649
2654
|
);
|
|
2655
|
+
for (const name of needsAuth) {
|
|
2656
|
+
lines.push(
|
|
2657
|
+
` ${c.yellow(`${name} requires OAuth`)} ${c.dim(
|
|
2658
|
+
`\u2014 run 'opencode mcp auth ${name}', then 'quiver-cli check'`
|
|
2659
|
+
)}`
|
|
2660
|
+
);
|
|
2661
|
+
}
|
|
2650
2662
|
if (missingTools) {
|
|
2651
2663
|
lines.push(` ${c.dim("run 'quiver-cli check' to populate tool counts")}`);
|
|
2652
2664
|
}
|
|
@@ -2698,7 +2710,7 @@ var init_diff = __esm({
|
|
|
2698
2710
|
});
|
|
2699
2711
|
|
|
2700
2712
|
// src/mcp/introspect.ts
|
|
2701
|
-
var CONNECT_TIMEOUT_MS, withTimeout, introspect, errMsg;
|
|
2713
|
+
var CONNECT_TIMEOUT_MS, withTimeout, introspect, errMsg, isAuthError;
|
|
2702
2714
|
var init_introspect = __esm({
|
|
2703
2715
|
"src/mcp/introspect.ts"() {
|
|
2704
2716
|
"use strict";
|
|
@@ -2714,13 +2726,20 @@ var init_introspect = __esm({
|
|
|
2714
2726
|
clearTimeout(timer);
|
|
2715
2727
|
}
|
|
2716
2728
|
};
|
|
2717
|
-
introspect = async (server, { allowStdio }) => {
|
|
2729
|
+
introspect = async (server, { allowStdio, authToken }) => {
|
|
2718
2730
|
const { Client } = await import("@modelcontextprotocol/sdk/client/index.js");
|
|
2719
2731
|
let transport;
|
|
2720
2732
|
try {
|
|
2721
2733
|
if (server.transport === "http") {
|
|
2722
2734
|
const { StreamableHTTPClientTransport } = await import("@modelcontextprotocol/sdk/client/streamableHttp.js");
|
|
2723
|
-
const
|
|
2735
|
+
const headers2 = { ...server.headers ?? {} };
|
|
2736
|
+
const hasAuthHeader = Object.keys(headers2).some(
|
|
2737
|
+
(k) => k.toLowerCase() === "authorization"
|
|
2738
|
+
);
|
|
2739
|
+
if (authToken && !hasAuthHeader) {
|
|
2740
|
+
headers2["Authorization"] = `Bearer ${authToken}`;
|
|
2741
|
+
}
|
|
2742
|
+
const requestInit = Object.keys(headers2).length ? { headers: headers2 } : {};
|
|
2724
2743
|
transport = new StreamableHTTPClientTransport(new URL(server.url), {
|
|
2725
2744
|
requestInit
|
|
2726
2745
|
});
|
|
@@ -2755,6 +2774,9 @@ var init_introspect = __esm({
|
|
|
2755
2774
|
}));
|
|
2756
2775
|
return { ok: true, tools };
|
|
2757
2776
|
} catch (e) {
|
|
2777
|
+
if (await isAuthError(e)) {
|
|
2778
|
+
return { ok: false, reason: errMsg(e), authRequired: true };
|
|
2779
|
+
}
|
|
2758
2780
|
return { ok: false, reason: errMsg(e) };
|
|
2759
2781
|
} finally {
|
|
2760
2782
|
try {
|
|
@@ -2764,6 +2786,55 @@ var init_introspect = __esm({
|
|
|
2764
2786
|
}
|
|
2765
2787
|
};
|
|
2766
2788
|
errMsg = (e) => e instanceof Error ? e.message : String(e);
|
|
2789
|
+
isAuthError = async (e) => {
|
|
2790
|
+
try {
|
|
2791
|
+
const { UnauthorizedError } = await import("@modelcontextprotocol/sdk/client/auth.js");
|
|
2792
|
+
if (e instanceof UnauthorizedError) return true;
|
|
2793
|
+
} catch {
|
|
2794
|
+
}
|
|
2795
|
+
if (typeof e === "object" && e !== null && e.code === 401) {
|
|
2796
|
+
return true;
|
|
2797
|
+
}
|
|
2798
|
+
return /\b401\b|unauthorized|invalid_token/i.test(errMsg(e));
|
|
2799
|
+
};
|
|
2800
|
+
}
|
|
2801
|
+
});
|
|
2802
|
+
|
|
2803
|
+
// src/mcp/opencode-auth.ts
|
|
2804
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
2805
|
+
import { homedir as homedir2 } from "os";
|
|
2806
|
+
import { resolve as resolve18 } from "path";
|
|
2807
|
+
var EXPIRY_SKEW_MS, authFilePath, normalizeUrl, findOpencodeToken;
|
|
2808
|
+
var init_opencode_auth = __esm({
|
|
2809
|
+
"src/mcp/opencode-auth.ts"() {
|
|
2810
|
+
"use strict";
|
|
2811
|
+
EXPIRY_SKEW_MS = 3e4;
|
|
2812
|
+
authFilePath = () => {
|
|
2813
|
+
const base = process.env["XDG_DATA_HOME"] || resolve18(homedir2(), ".local", "share");
|
|
2814
|
+
return resolve18(base, "opencode", "mcp-auth.json");
|
|
2815
|
+
};
|
|
2816
|
+
normalizeUrl = (url) => url.trim().replace(/\/+$/, "").toLowerCase();
|
|
2817
|
+
findOpencodeToken = (name, url) => {
|
|
2818
|
+
let data;
|
|
2819
|
+
try {
|
|
2820
|
+
data = JSON.parse(readFileSync10(authFilePath(), "utf8"));
|
|
2821
|
+
} catch {
|
|
2822
|
+
return { status: "none" };
|
|
2823
|
+
}
|
|
2824
|
+
if (typeof data !== "object" || data === null) return { status: "none" };
|
|
2825
|
+
const entries = data;
|
|
2826
|
+
const target = normalizeUrl(url);
|
|
2827
|
+
const entry = Object.values(entries).find(
|
|
2828
|
+
(e) => e?.serverUrl && normalizeUrl(e.serverUrl) === target
|
|
2829
|
+
) ?? entries[name];
|
|
2830
|
+
const tokens = entry?.tokens;
|
|
2831
|
+
if (!tokens?.accessToken) return { status: "none" };
|
|
2832
|
+
if (typeof tokens.expiresAt === "number") {
|
|
2833
|
+
const expiresMs = tokens.expiresAt > 1e12 ? tokens.expiresAt : tokens.expiresAt * 1e3;
|
|
2834
|
+
if (expiresMs - EXPIRY_SKEW_MS <= Date.now()) return { status: "expired" };
|
|
2835
|
+
}
|
|
2836
|
+
return { status: "ok", accessToken: tokens.accessToken };
|
|
2837
|
+
};
|
|
2767
2838
|
}
|
|
2768
2839
|
});
|
|
2769
2840
|
|
|
@@ -2789,13 +2860,14 @@ var init_snapshot = __esm({
|
|
|
2789
2860
|
// src/commands/check.ts
|
|
2790
2861
|
var check_exports = {};
|
|
2791
2862
|
__export(check_exports, {
|
|
2863
|
+
authHint: () => authHint,
|
|
2792
2864
|
check: () => check,
|
|
2793
2865
|
hasCommand: () => hasCommand,
|
|
2794
2866
|
summarize: () => summarize
|
|
2795
2867
|
});
|
|
2796
2868
|
import { accessSync as accessSync2, constants as constants2 } from "fs";
|
|
2797
|
-
import { delimiter, resolve as
|
|
2798
|
-
var check, report2, driftLines, list2, recommend, summarize, hasCommand, truncate2, fail;
|
|
2869
|
+
import { delimiter, resolve as resolve19 } from "path";
|
|
2870
|
+
var check, report2, driftLines, list2, recommend, summarize, authHint, hasCommand, truncate2, fail;
|
|
2799
2871
|
var init_check = __esm({
|
|
2800
2872
|
"src/commands/check.ts"() {
|
|
2801
2873
|
"use strict";
|
|
@@ -2804,6 +2876,7 @@ var init_check = __esm({
|
|
|
2804
2876
|
init_schema();
|
|
2805
2877
|
init_diff();
|
|
2806
2878
|
init_introspect();
|
|
2879
|
+
init_opencode_auth();
|
|
2807
2880
|
init_snapshot();
|
|
2808
2881
|
init_local_config();
|
|
2809
2882
|
init_write();
|
|
@@ -2867,13 +2940,27 @@ var init_check = __esm({
|
|
|
2867
2940
|
}
|
|
2868
2941
|
checked.mcp += 1;
|
|
2869
2942
|
const server = interpolateEnvVars(catMcp.server);
|
|
2870
|
-
const
|
|
2943
|
+
const mcpEntry = entry;
|
|
2944
|
+
const cred = server.transport === "http" ? findOpencodeToken(p.name, server.url) : { status: "none" };
|
|
2945
|
+
const res = await introspect(server, {
|
|
2946
|
+
allowStdio: options.introspectStdio,
|
|
2947
|
+
authToken: cred.status === "ok" ? cred.accessToken : void 0
|
|
2948
|
+
});
|
|
2871
2949
|
if (!res.ok) {
|
|
2872
|
-
|
|
2950
|
+
if (res.authRequired && !mcpEntry.authRequired) {
|
|
2951
|
+
mcpEntry.authRequired = true;
|
|
2952
|
+
lockChanged = true;
|
|
2953
|
+
}
|
|
2954
|
+
const reason = res.authRequired ? authHint(cred.status, p.name) : res.reason;
|
|
2955
|
+
mcpReports.push({
|
|
2956
|
+
id,
|
|
2957
|
+
status: "skipped",
|
|
2958
|
+
reason,
|
|
2959
|
+
...res.authRequired ? { authRequired: true } : {}
|
|
2960
|
+
});
|
|
2873
2961
|
continue;
|
|
2874
2962
|
}
|
|
2875
2963
|
const current = toSnapshot(res.tools);
|
|
2876
|
-
const mcpEntry = entry;
|
|
2877
2964
|
if (!mcpEntry.tools) {
|
|
2878
2965
|
mcpEntry.tools = current;
|
|
2879
2966
|
mcpEntry.toolsFetchedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -2942,7 +3029,15 @@ var init_check = __esm({
|
|
|
2942
3029
|
- ${shimProblems.join("\n - ")}`
|
|
2943
3030
|
);
|
|
2944
3031
|
}
|
|
2945
|
-
const
|
|
3032
|
+
const authSkipped = mcpReports.filter(
|
|
3033
|
+
(r) => r.status === "skipped" && r.authRequired
|
|
3034
|
+
);
|
|
3035
|
+
for (const r of authSkipped) {
|
|
3036
|
+
await warn(`${r.id}: ${r.reason}`);
|
|
3037
|
+
}
|
|
3038
|
+
const skipped = mcpReports.filter(
|
|
3039
|
+
(r) => r.status === "skipped" && !r.authRequired
|
|
3040
|
+
);
|
|
2946
3041
|
if (skipped.length) {
|
|
2947
3042
|
const names = skipped.map((r) => parseEntryId(r.id)?.name ?? r.id);
|
|
2948
3043
|
await info(
|
|
@@ -3036,13 +3131,19 @@ var init_check = __esm({
|
|
|
3036
3131
|
if (c.plugins) parts.push(plural(c.plugins, "plugin"));
|
|
3037
3132
|
return parts.length ? parts.join(", ") : "nothing";
|
|
3038
3133
|
};
|
|
3134
|
+
authHint = (cred, name) => {
|
|
3135
|
+
const reauth = `run 'opencode mcp auth ${name}', then 'quiver-cli check'`;
|
|
3136
|
+
if (cred === "expired") return `OAuth token expired \u2014 re-${reauth}`;
|
|
3137
|
+
if (cred === "ok") return `OAuth token rejected \u2014 re-${reauth}`;
|
|
3138
|
+
return `requires OAuth \u2014 ${reauth}`;
|
|
3139
|
+
};
|
|
3039
3140
|
hasCommand = (command) => {
|
|
3040
3141
|
if (!/^[A-Za-z0-9._-]+$/.test(command)) return false;
|
|
3041
3142
|
const extensions = process.platform === "win32" ? (process.env["PATHEXT"] ?? ".EXE;.CMD;.BAT;.COM").split(";") : [""];
|
|
3042
3143
|
for (const dir of (process.env["PATH"] ?? "").split(delimiter)) {
|
|
3043
3144
|
for (const extension of extensions) {
|
|
3044
3145
|
try {
|
|
3045
|
-
accessSync2(
|
|
3146
|
+
accessSync2(resolve19(dir, command + extension), constants2.X_OK);
|
|
3046
3147
|
return true;
|
|
3047
3148
|
} catch {
|
|
3048
3149
|
}
|
|
@@ -3064,12 +3165,12 @@ import { execFileSync as execFileSync3 } from "child_process";
|
|
|
3064
3165
|
import {
|
|
3065
3166
|
existsSync as existsSync14,
|
|
3066
3167
|
mkdtempSync as mkdtempSync2,
|
|
3067
|
-
readFileSync as
|
|
3168
|
+
readFileSync as readFileSync11,
|
|
3068
3169
|
rmSync as rmSync7,
|
|
3069
3170
|
writeFileSync as writeFileSync8
|
|
3070
3171
|
} from "fs";
|
|
3071
3172
|
import { tmpdir } from "os";
|
|
3072
|
-
import { join, resolve as
|
|
3173
|
+
import { join, resolve as resolve20 } from "path";
|
|
3073
3174
|
var UPSTREAMS_FILE, upstreamsPath, loadUpstreams, writeUpstreams, fetchLatestCommit, fetchUpstreamDir, short, evaluateOrigin;
|
|
3074
3175
|
var init_upstreams = __esm({
|
|
3075
3176
|
"src/catalog/upstreams.ts"() {
|
|
@@ -3077,11 +3178,11 @@ var init_upstreams = __esm({
|
|
|
3077
3178
|
init_auth();
|
|
3078
3179
|
init_auth();
|
|
3079
3180
|
UPSTREAMS_FILE = "upstreams.json";
|
|
3080
|
-
upstreamsPath = (catalog) =>
|
|
3181
|
+
upstreamsPath = (catalog) => resolve20(catalog.root, UPSTREAMS_FILE);
|
|
3081
3182
|
loadUpstreams = (catalog) => {
|
|
3082
3183
|
const path = upstreamsPath(catalog);
|
|
3083
3184
|
if (!existsSync14(path)) return {};
|
|
3084
|
-
return JSON.parse(
|
|
3185
|
+
return JSON.parse(readFileSync11(path, "utf8"));
|
|
3085
3186
|
};
|
|
3086
3187
|
writeUpstreams = (catalog, map) => {
|
|
3087
3188
|
writeFileSync8(upstreamsPath(catalog), JSON.stringify(map, null, 2) + "\n");
|
|
@@ -3149,8 +3250,8 @@ var init_upstreams = __esm({
|
|
|
3149
3250
|
const msg = err instanceof Error && "stderr" in err ? String(err.stderr).trim().split("\n").pop() : err instanceof Error ? err.message : "git clone failed";
|
|
3150
3251
|
return { ok: false, reason: msg || "git clone failed" };
|
|
3151
3252
|
}
|
|
3152
|
-
const dir =
|
|
3153
|
-
if (!existsSync14(
|
|
3253
|
+
const dir = resolve20(tmp, origin.path);
|
|
3254
|
+
if (!existsSync14(resolve20(dir, "SKILL.md"))) {
|
|
3154
3255
|
cleanup();
|
|
3155
3256
|
return { ok: false, reason: `no SKILL.md at ${origin.path} in ${origin.repo}` };
|
|
3156
3257
|
}
|
|
@@ -3443,9 +3544,9 @@ __export(notifier_exports, {
|
|
|
3443
3544
|
installHint: () => installHint,
|
|
3444
3545
|
notifierSuppressed: () => notifierSuppressed
|
|
3445
3546
|
});
|
|
3446
|
-
import { existsSync as existsSync15, mkdirSync as mkdirSync6, readFileSync as
|
|
3447
|
-
import { homedir as
|
|
3448
|
-
import { dirname as dirname6, resolve as
|
|
3547
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync6, readFileSync as readFileSync12, writeFileSync as writeFileSync9 } from "fs";
|
|
3548
|
+
import { homedir as homedir3 } from "os";
|
|
3549
|
+
import { dirname as dirname6, resolve as resolve21 } from "path";
|
|
3449
3550
|
var REGISTRY_URL, CHECK_TTL_MS, FETCH_TIMEOUT_MS, INSTALL_HINT, cacheFilePath, installHint, getCurrentVersion, compareSemver, readCache, writeCache, fetchLatestVersion, checkForUpdate, notifierSuppressed;
|
|
3450
3551
|
var init_notifier = __esm({
|
|
3451
3552
|
"src/version/notifier.ts"() {
|
|
@@ -3456,14 +3557,14 @@ var init_notifier = __esm({
|
|
|
3456
3557
|
FETCH_TIMEOUT_MS = 2e3;
|
|
3457
3558
|
INSTALL_HINT = "pnpm add -g quiver-cli";
|
|
3458
3559
|
cacheFilePath = () => {
|
|
3459
|
-
const base = process.env["XDG_CACHE_HOME"] ||
|
|
3460
|
-
return
|
|
3560
|
+
const base = process.env["XDG_CACHE_HOME"] || resolve21(homedir3(), ".cache");
|
|
3561
|
+
return resolve21(base, "quiver", "update-check.json");
|
|
3461
3562
|
};
|
|
3462
3563
|
installHint = () => INSTALL_HINT;
|
|
3463
3564
|
getCurrentVersion = () => {
|
|
3464
3565
|
try {
|
|
3465
3566
|
const pkg = JSON.parse(
|
|
3466
|
-
|
|
3567
|
+
readFileSync12(resolve21(packageRoot, "package.json"), "utf8")
|
|
3467
3568
|
);
|
|
3468
3569
|
return pkg.version;
|
|
3469
3570
|
} catch {
|
|
@@ -3491,7 +3592,7 @@ var init_notifier = __esm({
|
|
|
3491
3592
|
const path = cacheFilePath();
|
|
3492
3593
|
if (!existsSync15(path)) return null;
|
|
3493
3594
|
try {
|
|
3494
|
-
return JSON.parse(
|
|
3595
|
+
return JSON.parse(readFileSync12(path, "utf8"));
|
|
3495
3596
|
} catch {
|
|
3496
3597
|
return null;
|
|
3497
3598
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "quiver-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Compose selected skills, commands, plugins and MCP servers from a central catalog into any repo as native configs for opencode, Claude Code and Codex - with lockfile-based drift awareness.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|