quiver-cli 1.1.0 → 1.3.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 +362 -34
- 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
|
@@ -2499,6 +2499,31 @@ var init_update = __esm({
|
|
|
2499
2499
|
}
|
|
2500
2500
|
});
|
|
2501
2501
|
|
|
2502
|
+
// src/mcp/tokens.ts
|
|
2503
|
+
var estimateTokens, formatTokens, sumTokens;
|
|
2504
|
+
var init_tokens = __esm({
|
|
2505
|
+
"src/mcp/tokens.ts"() {
|
|
2506
|
+
"use strict";
|
|
2507
|
+
init_digest();
|
|
2508
|
+
estimateTokens = (tool) => Math.ceil(
|
|
2509
|
+
canonicalJson({
|
|
2510
|
+
name: tool.name,
|
|
2511
|
+
description: tool.description,
|
|
2512
|
+
inputSchema: tool.inputSchema
|
|
2513
|
+
}).length / 4
|
|
2514
|
+
);
|
|
2515
|
+
formatTokens = (n) => n < 1e3 ? `~${n} tok` : `~${(n / 1e3).toFixed(1)}k tok`;
|
|
2516
|
+
sumTokens = (tools) => {
|
|
2517
|
+
let total = 0;
|
|
2518
|
+
for (const tool of Object.values(tools)) {
|
|
2519
|
+
if (tool.tokens === void 0) return null;
|
|
2520
|
+
total += tool.tokens;
|
|
2521
|
+
}
|
|
2522
|
+
return total;
|
|
2523
|
+
};
|
|
2524
|
+
}
|
|
2525
|
+
});
|
|
2526
|
+
|
|
2502
2527
|
// src/commands/list.ts
|
|
2503
2528
|
var list_exports = {};
|
|
2504
2529
|
__export(list_exports, {
|
|
@@ -2511,6 +2536,7 @@ var init_list = __esm({
|
|
|
2511
2536
|
init_repo();
|
|
2512
2537
|
init_io();
|
|
2513
2538
|
init_schema();
|
|
2539
|
+
init_tokens();
|
|
2514
2540
|
init_local_config();
|
|
2515
2541
|
init_prompts();
|
|
2516
2542
|
truncate = (s, max) => {
|
|
@@ -2569,7 +2595,9 @@ var init_list = __esm({
|
|
|
2569
2595
|
transport: entry.transport,
|
|
2570
2596
|
enabled: !disabled.has(name),
|
|
2571
2597
|
detail: serverDetail2.get(name) ?? null,
|
|
2572
|
-
toolCount: entry.tools ? Object.keys(entry.tools).length : null
|
|
2598
|
+
toolCount: entry.tools ? Object.keys(entry.tools).length : null,
|
|
2599
|
+
tokenEstimate: entry.tools ? sumTokens(entry.tools) : null,
|
|
2600
|
+
authRequired: entry.authRequired ?? false
|
|
2573
2601
|
})),
|
|
2574
2602
|
plugins: plugins.map(({ name, entry }) => ({
|
|
2575
2603
|
name,
|
|
@@ -2609,6 +2637,7 @@ var init_list = __esm({
|
|
|
2609
2637
|
}
|
|
2610
2638
|
}
|
|
2611
2639
|
let missingTools = false;
|
|
2640
|
+
const needsAuth = [];
|
|
2612
2641
|
if (mcp.length) {
|
|
2613
2642
|
const nameW = Math.max(...mcp.map((e) => e.name.length));
|
|
2614
2643
|
const toolW = Math.max(
|
|
@@ -2617,19 +2646,35 @@ var init_list = __esm({
|
|
|
2617
2646
|
return `${n ?? "?"} tools`.length;
|
|
2618
2647
|
})
|
|
2619
2648
|
);
|
|
2649
|
+
const tokenCell = (entry) => {
|
|
2650
|
+
const total = entry.tools ? sumTokens(entry.tools) : null;
|
|
2651
|
+
return total === null ? "? tok" : formatTokens(total);
|
|
2652
|
+
};
|
|
2653
|
+
const tokW = Math.max(...mcp.map((e) => tokenCell(e.entry).length));
|
|
2620
2654
|
lines.push("", ` ${c.bold("mcp servers")}`);
|
|
2621
2655
|
for (const { name, entry } of mcp) {
|
|
2622
2656
|
const count = entry.tools ? Object.keys(entry.tools).length : null;
|
|
2623
|
-
|
|
2657
|
+
const tokenTotal = entry.tools ? sumTokens(entry.tools) : null;
|
|
2658
|
+
if (count === null) {
|
|
2659
|
+
if (entry.authRequired) needsAuth.push(name);
|
|
2660
|
+
else missingTools = true;
|
|
2661
|
+
} else if (tokenTotal === null) {
|
|
2662
|
+
missingTools = true;
|
|
2663
|
+
}
|
|
2624
2664
|
const tools = padCell(
|
|
2625
2665
|
`${count ?? "?"} tools`,
|
|
2626
2666
|
toolW,
|
|
2627
2667
|
count === null ? c.dim : c.green
|
|
2628
2668
|
);
|
|
2669
|
+
const tokens = padCell(
|
|
2670
|
+
tokenCell(entry),
|
|
2671
|
+
tokW,
|
|
2672
|
+
tokenTotal === null ? c.dim : c.cyan
|
|
2673
|
+
);
|
|
2629
2674
|
const detail = serverDetail2.get(name);
|
|
2630
2675
|
const off = disabled.has(name) ? ` ${c.yellow("disabled")}` : "";
|
|
2631
2676
|
lines.push(
|
|
2632
|
-
` ${name.padEnd(nameW)} ${entry.transport.padEnd(5)} ${tools}` + (detail ? ` ${c.dim(detail)}` : "") + off
|
|
2677
|
+
` ${name.padEnd(nameW)} ${entry.transport.padEnd(5)} ${tools} ${tokens}` + (detail ? ` ${c.dim(detail)}` : "") + off
|
|
2633
2678
|
);
|
|
2634
2679
|
}
|
|
2635
2680
|
}
|
|
@@ -2647,8 +2692,181 @@ var init_list = __esm({
|
|
|
2647
2692
|
`${skills.length} skills \xB7 ${commands.length} commands \xB7 ${mcp.length} MCP servers \xB7 ${plugins.length} plugins`
|
|
2648
2693
|
)} ${c.dim(`providers: ${providers2}`)}`
|
|
2649
2694
|
);
|
|
2695
|
+
for (const name of needsAuth) {
|
|
2696
|
+
lines.push(
|
|
2697
|
+
` ${c.yellow(`${name} requires OAuth`)} ${c.dim(
|
|
2698
|
+
`\u2014 run 'opencode mcp auth ${name}', then 'quiver-cli check'`
|
|
2699
|
+
)}`
|
|
2700
|
+
);
|
|
2701
|
+
}
|
|
2650
2702
|
if (missingTools) {
|
|
2651
|
-
lines.push(
|
|
2703
|
+
lines.push(
|
|
2704
|
+
` ${c.dim("run 'quiver-cli check' to populate tool counts and token estimates")}`
|
|
2705
|
+
);
|
|
2706
|
+
}
|
|
2707
|
+
lines.push("");
|
|
2708
|
+
block(lines);
|
|
2709
|
+
};
|
|
2710
|
+
}
|
|
2711
|
+
});
|
|
2712
|
+
|
|
2713
|
+
// src/commands/inspect.ts
|
|
2714
|
+
var inspect_exports = {};
|
|
2715
|
+
__export(inspect_exports, {
|
|
2716
|
+
inspect: () => inspect
|
|
2717
|
+
});
|
|
2718
|
+
var truncate2, padCell2, byCost, inspect;
|
|
2719
|
+
var init_inspect = __esm({
|
|
2720
|
+
"src/commands/inspect.ts"() {
|
|
2721
|
+
"use strict";
|
|
2722
|
+
init_repo();
|
|
2723
|
+
init_io();
|
|
2724
|
+
init_tokens();
|
|
2725
|
+
init_local_config();
|
|
2726
|
+
init_prompts();
|
|
2727
|
+
truncate2 = (s, max) => {
|
|
2728
|
+
const flat = s.replace(/\s+/g, " ").trim();
|
|
2729
|
+
if (max < 1) return "";
|
|
2730
|
+
return flat.length > max ? flat.slice(0, max - 1) + "\u2026" : flat;
|
|
2731
|
+
};
|
|
2732
|
+
padCell2 = (text, width, color) => color(text.padEnd(width));
|
|
2733
|
+
byCost = (a, b) => (b.tool.tokens ?? -1) - (a.tool.tokens ?? -1) || a.name.localeCompare(b.name);
|
|
2734
|
+
inspect = async (options) => {
|
|
2735
|
+
const arg = options.positionals[0];
|
|
2736
|
+
if (!arg) {
|
|
2737
|
+
if (options.json) {
|
|
2738
|
+
console.log(JSON.stringify({ ok: false, error: "missing-argument" }));
|
|
2739
|
+
} else {
|
|
2740
|
+
await error("Usage: quiver-cli inspect <mcp-name>");
|
|
2741
|
+
}
|
|
2742
|
+
process.exitCode = 1;
|
|
2743
|
+
return;
|
|
2744
|
+
}
|
|
2745
|
+
const name = arg.startsWith("mcp:") ? arg.slice("mcp:".length) : arg;
|
|
2746
|
+
const lock = readLockfile(options.targetRoot);
|
|
2747
|
+
if (!lock) {
|
|
2748
|
+
if (options.json) console.log(JSON.stringify({ ok: false, error: "no-lockfile" }));
|
|
2749
|
+
else await error("No quiver.lock found. Run `quiver-cli init` first.");
|
|
2750
|
+
process.exitCode = 1;
|
|
2751
|
+
return;
|
|
2752
|
+
}
|
|
2753
|
+
const entry = lock.entries[`mcp:${name}`];
|
|
2754
|
+
if (!entry || entry.type !== "mcp") {
|
|
2755
|
+
const available = Object.keys(lock.entries).filter((id) => id.startsWith("mcp:")).map((id) => id.slice("mcp:".length)).sort();
|
|
2756
|
+
if (options.json) {
|
|
2757
|
+
console.log(
|
|
2758
|
+
JSON.stringify({ ok: false, error: "unknown-server", available })
|
|
2759
|
+
);
|
|
2760
|
+
} else {
|
|
2761
|
+
await error(
|
|
2762
|
+
`Unknown MCP server "${name}".` + (available.length ? ` Available: ${available.join(", ")}` : "")
|
|
2763
|
+
);
|
|
2764
|
+
}
|
|
2765
|
+
process.exitCode = 1;
|
|
2766
|
+
return;
|
|
2767
|
+
}
|
|
2768
|
+
const mcpEntry = entry;
|
|
2769
|
+
let detail = null;
|
|
2770
|
+
if (repoCatalogExists(options.targetRoot)) {
|
|
2771
|
+
const { catalog } = loadRepoCatalog(options.targetRoot, lock.catalog.source);
|
|
2772
|
+
const cat = catalog.mcp.find((m) => m.name === name);
|
|
2773
|
+
if (cat) {
|
|
2774
|
+
detail = cat.server.transport === "http" ? cat.server.url : [cat.server.command, ...cat.server.args ?? []].join(" ");
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
const enabled = !disabledMcpServers(options.targetRoot).has(name);
|
|
2778
|
+
const tools = mcpEntry.tools ? Object.entries(mcpEntry.tools).map(([toolName, tool]) => ({ name: toolName, tool })).sort(byCost) : null;
|
|
2779
|
+
const total = mcpEntry.tools ? sumTokens(mcpEntry.tools) : null;
|
|
2780
|
+
if (options.json) {
|
|
2781
|
+
console.log(
|
|
2782
|
+
JSON.stringify(
|
|
2783
|
+
{
|
|
2784
|
+
ok: true,
|
|
2785
|
+
name,
|
|
2786
|
+
transport: mcpEntry.transport,
|
|
2787
|
+
detail,
|
|
2788
|
+
enabled,
|
|
2789
|
+
authRequired: mcpEntry.authRequired ?? false,
|
|
2790
|
+
toolsFetchedAt: mcpEntry.toolsFetchedAt,
|
|
2791
|
+
toolCount: tools ? tools.length : null,
|
|
2792
|
+
tokenEstimate: total,
|
|
2793
|
+
tools: tools ? tools.map(({ name: toolName, tool }) => ({
|
|
2794
|
+
name: toolName,
|
|
2795
|
+
description: tool.description,
|
|
2796
|
+
tokens: tool.tokens ?? null,
|
|
2797
|
+
inputSchemaHash: tool.inputSchemaHash
|
|
2798
|
+
})) : null
|
|
2799
|
+
},
|
|
2800
|
+
null,
|
|
2801
|
+
2
|
|
2802
|
+
)
|
|
2803
|
+
);
|
|
2804
|
+
return;
|
|
2805
|
+
}
|
|
2806
|
+
const c = palette();
|
|
2807
|
+
const term = process.stdout.columns ?? 80;
|
|
2808
|
+
const lines = [""];
|
|
2809
|
+
lines.push(
|
|
2810
|
+
` ${c.bold(name)} ${mcpEntry.transport}` + (detail ? ` ${c.dim(detail)}` : "") + (enabled ? "" : ` ${c.yellow("disabled")}`)
|
|
2811
|
+
);
|
|
2812
|
+
if (!tools) {
|
|
2813
|
+
lines.push("", ` ${c.dim("no tool snapshot recorded yet")}`);
|
|
2814
|
+
if (mcpEntry.authRequired) {
|
|
2815
|
+
lines.push(
|
|
2816
|
+
` ${c.yellow(`${name} requires OAuth`)} ${c.dim(
|
|
2817
|
+
`\u2014 run 'opencode mcp auth ${name}', then 'quiver-cli check'`
|
|
2818
|
+
)}`
|
|
2819
|
+
);
|
|
2820
|
+
} else {
|
|
2821
|
+
lines.push(` ${c.dim("run 'quiver-cli check' to introspect this server")}`);
|
|
2822
|
+
}
|
|
2823
|
+
lines.push("");
|
|
2824
|
+
block(lines);
|
|
2825
|
+
return;
|
|
2826
|
+
}
|
|
2827
|
+
const summary = `${tools.length} tools` + (total !== null ? ` \xB7 ${formatTokens(total)}` : "");
|
|
2828
|
+
lines.push(
|
|
2829
|
+
` ${c.bold(summary)}` + (mcpEntry.toolsFetchedAt ? ` ${c.dim(`snapshot from ${mcpEntry.toolsFetchedAt}`)}` : ""),
|
|
2830
|
+
""
|
|
2831
|
+
);
|
|
2832
|
+
const nameW = Math.max(...tools.map((t) => t.name.length));
|
|
2833
|
+
const tokW = Math.max(
|
|
2834
|
+
...tools.map(
|
|
2835
|
+
({ tool }) => (tool.tokens === void 0 ? "? tok" : formatTokens(tool.tokens)).length
|
|
2836
|
+
)
|
|
2837
|
+
);
|
|
2838
|
+
const descMax = term - (4 + nameW + 2 + tokW + 2) - 1;
|
|
2839
|
+
let missingTokens = false;
|
|
2840
|
+
for (const { name: toolName, tool } of tools) {
|
|
2841
|
+
if (tool.tokens === void 0) missingTokens = true;
|
|
2842
|
+
const tokens = padCell2(
|
|
2843
|
+
tool.tokens === void 0 ? "? tok" : formatTokens(tool.tokens),
|
|
2844
|
+
tokW,
|
|
2845
|
+
tool.tokens === void 0 ? c.dim : c.cyan
|
|
2846
|
+
);
|
|
2847
|
+
if (options.verbose) {
|
|
2848
|
+
lines.push(` ${c.bold(toolName.padEnd(nameW))} ${tokens}`);
|
|
2849
|
+
for (const descLine of tool.description.split("\n")) {
|
|
2850
|
+
lines.push(` ${c.dim(descLine)}`);
|
|
2851
|
+
}
|
|
2852
|
+
lines.push("");
|
|
2853
|
+
} else {
|
|
2854
|
+
lines.push(
|
|
2855
|
+
` ${toolName.padEnd(nameW)} ${tokens} ${c.dim(
|
|
2856
|
+
truncate2(tool.description, descMax)
|
|
2857
|
+
)}`.trimEnd()
|
|
2858
|
+
);
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2861
|
+
if (!options.verbose) lines.push("");
|
|
2862
|
+
lines.push(` ${c.dim("token counts are chars/4 estimates of name + description + input schema")}`);
|
|
2863
|
+
if (!options.verbose) {
|
|
2864
|
+
lines.push(` ${c.dim("use --verbose for full descriptions")}`);
|
|
2865
|
+
}
|
|
2866
|
+
if (missingTokens) {
|
|
2867
|
+
lines.push(
|
|
2868
|
+
` ${c.dim("run 'quiver-cli check' to populate missing token estimates")}`
|
|
2869
|
+
);
|
|
2652
2870
|
}
|
|
2653
2871
|
lines.push("");
|
|
2654
2872
|
block(lines);
|
|
@@ -2698,7 +2916,7 @@ var init_diff = __esm({
|
|
|
2698
2916
|
});
|
|
2699
2917
|
|
|
2700
2918
|
// src/mcp/introspect.ts
|
|
2701
|
-
var CONNECT_TIMEOUT_MS, withTimeout, introspect, errMsg;
|
|
2919
|
+
var CONNECT_TIMEOUT_MS, withTimeout, introspect, errMsg, isAuthError;
|
|
2702
2920
|
var init_introspect = __esm({
|
|
2703
2921
|
"src/mcp/introspect.ts"() {
|
|
2704
2922
|
"use strict";
|
|
@@ -2714,13 +2932,20 @@ var init_introspect = __esm({
|
|
|
2714
2932
|
clearTimeout(timer);
|
|
2715
2933
|
}
|
|
2716
2934
|
};
|
|
2717
|
-
introspect = async (server, { allowStdio }) => {
|
|
2935
|
+
introspect = async (server, { allowStdio, authToken }) => {
|
|
2718
2936
|
const { Client } = await import("@modelcontextprotocol/sdk/client/index.js");
|
|
2719
2937
|
let transport;
|
|
2720
2938
|
try {
|
|
2721
2939
|
if (server.transport === "http") {
|
|
2722
2940
|
const { StreamableHTTPClientTransport } = await import("@modelcontextprotocol/sdk/client/streamableHttp.js");
|
|
2723
|
-
const
|
|
2941
|
+
const headers2 = { ...server.headers ?? {} };
|
|
2942
|
+
const hasAuthHeader = Object.keys(headers2).some(
|
|
2943
|
+
(k) => k.toLowerCase() === "authorization"
|
|
2944
|
+
);
|
|
2945
|
+
if (authToken && !hasAuthHeader) {
|
|
2946
|
+
headers2["Authorization"] = `Bearer ${authToken}`;
|
|
2947
|
+
}
|
|
2948
|
+
const requestInit = Object.keys(headers2).length ? { headers: headers2 } : {};
|
|
2724
2949
|
transport = new StreamableHTTPClientTransport(new URL(server.url), {
|
|
2725
2950
|
requestInit
|
|
2726
2951
|
});
|
|
@@ -2755,6 +2980,9 @@ var init_introspect = __esm({
|
|
|
2755
2980
|
}));
|
|
2756
2981
|
return { ok: true, tools };
|
|
2757
2982
|
} catch (e) {
|
|
2983
|
+
if (await isAuthError(e)) {
|
|
2984
|
+
return { ok: false, reason: errMsg(e), authRequired: true };
|
|
2985
|
+
}
|
|
2758
2986
|
return { ok: false, reason: errMsg(e) };
|
|
2759
2987
|
} finally {
|
|
2760
2988
|
try {
|
|
@@ -2764,38 +2992,101 @@ var init_introspect = __esm({
|
|
|
2764
2992
|
}
|
|
2765
2993
|
};
|
|
2766
2994
|
errMsg = (e) => e instanceof Error ? e.message : String(e);
|
|
2995
|
+
isAuthError = async (e) => {
|
|
2996
|
+
try {
|
|
2997
|
+
const { UnauthorizedError } = await import("@modelcontextprotocol/sdk/client/auth.js");
|
|
2998
|
+
if (e instanceof UnauthorizedError) return true;
|
|
2999
|
+
} catch {
|
|
3000
|
+
}
|
|
3001
|
+
if (typeof e === "object" && e !== null && e.code === 401) {
|
|
3002
|
+
return true;
|
|
3003
|
+
}
|
|
3004
|
+
return /\b401\b|unauthorized|invalid_token/i.test(errMsg(e));
|
|
3005
|
+
};
|
|
3006
|
+
}
|
|
3007
|
+
});
|
|
3008
|
+
|
|
3009
|
+
// src/mcp/opencode-auth.ts
|
|
3010
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
3011
|
+
import { homedir as homedir2 } from "os";
|
|
3012
|
+
import { resolve as resolve18 } from "path";
|
|
3013
|
+
var EXPIRY_SKEW_MS, authFilePath, normalizeUrl, findOpencodeToken;
|
|
3014
|
+
var init_opencode_auth = __esm({
|
|
3015
|
+
"src/mcp/opencode-auth.ts"() {
|
|
3016
|
+
"use strict";
|
|
3017
|
+
EXPIRY_SKEW_MS = 3e4;
|
|
3018
|
+
authFilePath = () => {
|
|
3019
|
+
const base = process.env["XDG_DATA_HOME"] || resolve18(homedir2(), ".local", "share");
|
|
3020
|
+
return resolve18(base, "opencode", "mcp-auth.json");
|
|
3021
|
+
};
|
|
3022
|
+
normalizeUrl = (url) => url.trim().replace(/\/+$/, "").toLowerCase();
|
|
3023
|
+
findOpencodeToken = (name, url) => {
|
|
3024
|
+
let data;
|
|
3025
|
+
try {
|
|
3026
|
+
data = JSON.parse(readFileSync10(authFilePath(), "utf8"));
|
|
3027
|
+
} catch {
|
|
3028
|
+
return { status: "none" };
|
|
3029
|
+
}
|
|
3030
|
+
if (typeof data !== "object" || data === null) return { status: "none" };
|
|
3031
|
+
const entries = data;
|
|
3032
|
+
const target = normalizeUrl(url);
|
|
3033
|
+
const entry = Object.values(entries).find(
|
|
3034
|
+
(e) => e?.serverUrl && normalizeUrl(e.serverUrl) === target
|
|
3035
|
+
) ?? entries[name];
|
|
3036
|
+
const tokens = entry?.tokens;
|
|
3037
|
+
if (!tokens?.accessToken) return { status: "none" };
|
|
3038
|
+
if (typeof tokens.expiresAt === "number") {
|
|
3039
|
+
const expiresMs = tokens.expiresAt > 1e12 ? tokens.expiresAt : tokens.expiresAt * 1e3;
|
|
3040
|
+
if (expiresMs - EXPIRY_SKEW_MS <= Date.now()) return { status: "expired" };
|
|
3041
|
+
}
|
|
3042
|
+
return { status: "ok", accessToken: tokens.accessToken };
|
|
3043
|
+
};
|
|
2767
3044
|
}
|
|
2768
3045
|
});
|
|
2769
3046
|
|
|
2770
3047
|
// src/mcp/snapshot.ts
|
|
2771
|
-
var toSnapshot;
|
|
3048
|
+
var toSnapshot, backfillTokens;
|
|
2772
3049
|
var init_snapshot = __esm({
|
|
2773
3050
|
"src/mcp/snapshot.ts"() {
|
|
2774
3051
|
"use strict";
|
|
2775
3052
|
init_digest();
|
|
3053
|
+
init_tokens();
|
|
2776
3054
|
toSnapshot = (tools) => {
|
|
2777
3055
|
const snapshot = {};
|
|
2778
3056
|
for (const tool of tools) {
|
|
2779
3057
|
snapshot[tool.name] = {
|
|
2780
3058
|
description: tool.description,
|
|
2781
|
-
inputSchemaHash: jsonDigest(tool.inputSchema)
|
|
3059
|
+
inputSchemaHash: jsonDigest(tool.inputSchema),
|
|
3060
|
+
tokens: estimateTokens(tool)
|
|
2782
3061
|
};
|
|
2783
3062
|
}
|
|
2784
3063
|
return snapshot;
|
|
2785
3064
|
};
|
|
3065
|
+
backfillTokens = (stored, current) => {
|
|
3066
|
+
let changed = false;
|
|
3067
|
+
for (const [name, tool] of Object.entries(stored)) {
|
|
3068
|
+
const estimate = current[name]?.tokens;
|
|
3069
|
+
if (tool.tokens === void 0 && estimate !== void 0) {
|
|
3070
|
+
tool.tokens = estimate;
|
|
3071
|
+
changed = true;
|
|
3072
|
+
}
|
|
3073
|
+
}
|
|
3074
|
+
return changed;
|
|
3075
|
+
};
|
|
2786
3076
|
}
|
|
2787
3077
|
});
|
|
2788
3078
|
|
|
2789
3079
|
// src/commands/check.ts
|
|
2790
3080
|
var check_exports = {};
|
|
2791
3081
|
__export(check_exports, {
|
|
3082
|
+
authHint: () => authHint,
|
|
2792
3083
|
check: () => check,
|
|
2793
3084
|
hasCommand: () => hasCommand,
|
|
2794
3085
|
summarize: () => summarize
|
|
2795
3086
|
});
|
|
2796
3087
|
import { accessSync as accessSync2, constants as constants2 } from "fs";
|
|
2797
|
-
import { delimiter, resolve as
|
|
2798
|
-
var check, report2, driftLines, list2, recommend, summarize, hasCommand,
|
|
3088
|
+
import { delimiter, resolve as resolve19 } from "path";
|
|
3089
|
+
var check, report2, driftLines, list2, recommend, summarize, authHint, hasCommand, truncate3, fail;
|
|
2799
3090
|
var init_check = __esm({
|
|
2800
3091
|
"src/commands/check.ts"() {
|
|
2801
3092
|
"use strict";
|
|
@@ -2804,6 +3095,7 @@ var init_check = __esm({
|
|
|
2804
3095
|
init_schema();
|
|
2805
3096
|
init_diff();
|
|
2806
3097
|
init_introspect();
|
|
3098
|
+
init_opencode_auth();
|
|
2807
3099
|
init_snapshot();
|
|
2808
3100
|
init_local_config();
|
|
2809
3101
|
init_write();
|
|
@@ -2867,13 +3159,27 @@ var init_check = __esm({
|
|
|
2867
3159
|
}
|
|
2868
3160
|
checked.mcp += 1;
|
|
2869
3161
|
const server = interpolateEnvVars(catMcp.server);
|
|
2870
|
-
const
|
|
3162
|
+
const mcpEntry = entry;
|
|
3163
|
+
const cred = server.transport === "http" ? findOpencodeToken(p.name, server.url) : { status: "none" };
|
|
3164
|
+
const res = await introspect(server, {
|
|
3165
|
+
allowStdio: options.introspectStdio,
|
|
3166
|
+
authToken: cred.status === "ok" ? cred.accessToken : void 0
|
|
3167
|
+
});
|
|
2871
3168
|
if (!res.ok) {
|
|
2872
|
-
|
|
3169
|
+
if (res.authRequired && !mcpEntry.authRequired) {
|
|
3170
|
+
mcpEntry.authRequired = true;
|
|
3171
|
+
lockChanged = true;
|
|
3172
|
+
}
|
|
3173
|
+
const reason = res.authRequired ? authHint(cred.status, p.name) : res.reason;
|
|
3174
|
+
mcpReports.push({
|
|
3175
|
+
id,
|
|
3176
|
+
status: "skipped",
|
|
3177
|
+
reason,
|
|
3178
|
+
...res.authRequired ? { authRequired: true } : {}
|
|
3179
|
+
});
|
|
2873
3180
|
continue;
|
|
2874
3181
|
}
|
|
2875
3182
|
const current = toSnapshot(res.tools);
|
|
2876
|
-
const mcpEntry = entry;
|
|
2877
3183
|
if (!mcpEntry.tools) {
|
|
2878
3184
|
mcpEntry.tools = current;
|
|
2879
3185
|
mcpEntry.toolsFetchedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -2883,6 +3189,7 @@ var init_check = __esm({
|
|
|
2883
3189
|
}
|
|
2884
3190
|
const diff = diffSnapshots(mcpEntry.tools, current);
|
|
2885
3191
|
if (isEmptyDiff(diff)) {
|
|
3192
|
+
if (backfillTokens(mcpEntry.tools, current)) lockChanged = true;
|
|
2886
3193
|
mcpReports.push({ id, status: "ok" });
|
|
2887
3194
|
} else if (options.accept) {
|
|
2888
3195
|
mcpEntry.tools = current;
|
|
@@ -2942,7 +3249,15 @@ var init_check = __esm({
|
|
|
2942
3249
|
- ${shimProblems.join("\n - ")}`
|
|
2943
3250
|
);
|
|
2944
3251
|
}
|
|
2945
|
-
const
|
|
3252
|
+
const authSkipped = mcpReports.filter(
|
|
3253
|
+
(r) => r.status === "skipped" && r.authRequired
|
|
3254
|
+
);
|
|
3255
|
+
for (const r of authSkipped) {
|
|
3256
|
+
await warn(`${r.id}: ${r.reason}`);
|
|
3257
|
+
}
|
|
3258
|
+
const skipped = mcpReports.filter(
|
|
3259
|
+
(r) => r.status === "skipped" && !r.authRequired
|
|
3260
|
+
);
|
|
2946
3261
|
if (skipped.length) {
|
|
2947
3262
|
const names = skipped.map((r) => parseEntryId(r.id)?.name ?? r.id);
|
|
2948
3263
|
await info(
|
|
@@ -2991,8 +3306,8 @@ var init_check = __esm({
|
|
|
2991
3306
|
for (const d of dc) {
|
|
2992
3307
|
lines.push(
|
|
2993
3308
|
` "${d.name}":
|
|
2994
|
-
before: ${
|
|
2995
|
-
after: ${
|
|
3309
|
+
before: ${truncate3(d.before)}
|
|
3310
|
+
after: ${truncate3(d.after)}`
|
|
2996
3311
|
);
|
|
2997
3312
|
}
|
|
2998
3313
|
}
|
|
@@ -3036,13 +3351,19 @@ var init_check = __esm({
|
|
|
3036
3351
|
if (c.plugins) parts.push(plural(c.plugins, "plugin"));
|
|
3037
3352
|
return parts.length ? parts.join(", ") : "nothing";
|
|
3038
3353
|
};
|
|
3354
|
+
authHint = (cred, name) => {
|
|
3355
|
+
const reauth = `run 'opencode mcp auth ${name}', then 'quiver-cli check'`;
|
|
3356
|
+
if (cred === "expired") return `OAuth token expired \u2014 re-${reauth}`;
|
|
3357
|
+
if (cred === "ok") return `OAuth token rejected \u2014 re-${reauth}`;
|
|
3358
|
+
return `requires OAuth \u2014 ${reauth}`;
|
|
3359
|
+
};
|
|
3039
3360
|
hasCommand = (command) => {
|
|
3040
3361
|
if (!/^[A-Za-z0-9._-]+$/.test(command)) return false;
|
|
3041
3362
|
const extensions = process.platform === "win32" ? (process.env["PATHEXT"] ?? ".EXE;.CMD;.BAT;.COM").split(";") : [""];
|
|
3042
3363
|
for (const dir of (process.env["PATH"] ?? "").split(delimiter)) {
|
|
3043
3364
|
for (const extension of extensions) {
|
|
3044
3365
|
try {
|
|
3045
|
-
accessSync2(
|
|
3366
|
+
accessSync2(resolve19(dir, command + extension), constants2.X_OK);
|
|
3046
3367
|
return true;
|
|
3047
3368
|
} catch {
|
|
3048
3369
|
}
|
|
@@ -3050,7 +3371,7 @@ var init_check = __esm({
|
|
|
3050
3371
|
}
|
|
3051
3372
|
return false;
|
|
3052
3373
|
};
|
|
3053
|
-
|
|
3374
|
+
truncate3 = (s, max = 120) => s.length > max ? s.slice(0, max) + "\u2026" : s;
|
|
3054
3375
|
fail = async (options, code, message) => {
|
|
3055
3376
|
if (options.json) console.log(JSON.stringify({ ok: false, error: code }));
|
|
3056
3377
|
else await error(message);
|
|
@@ -3064,12 +3385,12 @@ import { execFileSync as execFileSync3 } from "child_process";
|
|
|
3064
3385
|
import {
|
|
3065
3386
|
existsSync as existsSync14,
|
|
3066
3387
|
mkdtempSync as mkdtempSync2,
|
|
3067
|
-
readFileSync as
|
|
3388
|
+
readFileSync as readFileSync11,
|
|
3068
3389
|
rmSync as rmSync7,
|
|
3069
3390
|
writeFileSync as writeFileSync8
|
|
3070
3391
|
} from "fs";
|
|
3071
3392
|
import { tmpdir } from "os";
|
|
3072
|
-
import { join, resolve as
|
|
3393
|
+
import { join, resolve as resolve20 } from "path";
|
|
3073
3394
|
var UPSTREAMS_FILE, upstreamsPath, loadUpstreams, writeUpstreams, fetchLatestCommit, fetchUpstreamDir, short, evaluateOrigin;
|
|
3074
3395
|
var init_upstreams = __esm({
|
|
3075
3396
|
"src/catalog/upstreams.ts"() {
|
|
@@ -3077,11 +3398,11 @@ var init_upstreams = __esm({
|
|
|
3077
3398
|
init_auth();
|
|
3078
3399
|
init_auth();
|
|
3079
3400
|
UPSTREAMS_FILE = "upstreams.json";
|
|
3080
|
-
upstreamsPath = (catalog) =>
|
|
3401
|
+
upstreamsPath = (catalog) => resolve20(catalog.root, UPSTREAMS_FILE);
|
|
3081
3402
|
loadUpstreams = (catalog) => {
|
|
3082
3403
|
const path = upstreamsPath(catalog);
|
|
3083
3404
|
if (!existsSync14(path)) return {};
|
|
3084
|
-
return JSON.parse(
|
|
3405
|
+
return JSON.parse(readFileSync11(path, "utf8"));
|
|
3085
3406
|
};
|
|
3086
3407
|
writeUpstreams = (catalog, map) => {
|
|
3087
3408
|
writeFileSync8(upstreamsPath(catalog), JSON.stringify(map, null, 2) + "\n");
|
|
@@ -3149,8 +3470,8 @@ var init_upstreams = __esm({
|
|
|
3149
3470
|
const msg = err instanceof Error && "stderr" in err ? String(err.stderr).trim().split("\n").pop() : err instanceof Error ? err.message : "git clone failed";
|
|
3150
3471
|
return { ok: false, reason: msg || "git clone failed" };
|
|
3151
3472
|
}
|
|
3152
|
-
const dir =
|
|
3153
|
-
if (!existsSync14(
|
|
3473
|
+
const dir = resolve20(tmp, origin.path);
|
|
3474
|
+
if (!existsSync14(resolve20(dir, "SKILL.md"))) {
|
|
3154
3475
|
cleanup();
|
|
3155
3476
|
return { ok: false, reason: `no SKILL.md at ${origin.path} in ${origin.repo}` };
|
|
3156
3477
|
}
|
|
@@ -3443,9 +3764,9 @@ __export(notifier_exports, {
|
|
|
3443
3764
|
installHint: () => installHint,
|
|
3444
3765
|
notifierSuppressed: () => notifierSuppressed
|
|
3445
3766
|
});
|
|
3446
|
-
import { existsSync as existsSync15, mkdirSync as mkdirSync6, readFileSync as
|
|
3447
|
-
import { homedir as
|
|
3448
|
-
import { dirname as dirname6, resolve as
|
|
3767
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync6, readFileSync as readFileSync12, writeFileSync as writeFileSync9 } from "fs";
|
|
3768
|
+
import { homedir as homedir3 } from "os";
|
|
3769
|
+
import { dirname as dirname6, resolve as resolve21 } from "path";
|
|
3449
3770
|
var REGISTRY_URL, CHECK_TTL_MS, FETCH_TIMEOUT_MS, INSTALL_HINT, cacheFilePath, installHint, getCurrentVersion, compareSemver, readCache, writeCache, fetchLatestVersion, checkForUpdate, notifierSuppressed;
|
|
3450
3771
|
var init_notifier = __esm({
|
|
3451
3772
|
"src/version/notifier.ts"() {
|
|
@@ -3456,14 +3777,14 @@ var init_notifier = __esm({
|
|
|
3456
3777
|
FETCH_TIMEOUT_MS = 2e3;
|
|
3457
3778
|
INSTALL_HINT = "pnpm add -g quiver-cli";
|
|
3458
3779
|
cacheFilePath = () => {
|
|
3459
|
-
const base = process.env["XDG_CACHE_HOME"] ||
|
|
3460
|
-
return
|
|
3780
|
+
const base = process.env["XDG_CACHE_HOME"] || resolve21(homedir3(), ".cache");
|
|
3781
|
+
return resolve21(base, "quiver", "update-check.json");
|
|
3461
3782
|
};
|
|
3462
3783
|
installHint = () => INSTALL_HINT;
|
|
3463
3784
|
getCurrentVersion = () => {
|
|
3464
3785
|
try {
|
|
3465
3786
|
const pkg = JSON.parse(
|
|
3466
|
-
|
|
3787
|
+
readFileSync12(resolve21(packageRoot, "package.json"), "utf8")
|
|
3467
3788
|
);
|
|
3468
3789
|
return pkg.version;
|
|
3469
3790
|
} catch {
|
|
@@ -3491,7 +3812,7 @@ var init_notifier = __esm({
|
|
|
3491
3812
|
const path = cacheFilePath();
|
|
3492
3813
|
if (!existsSync15(path)) return null;
|
|
3493
3814
|
try {
|
|
3494
|
-
return JSON.parse(
|
|
3815
|
+
return JSON.parse(readFileSync12(path, "utf8"));
|
|
3495
3816
|
} catch {
|
|
3496
3817
|
return null;
|
|
3497
3818
|
}
|
|
@@ -3560,6 +3881,7 @@ Commands:
|
|
|
3560
3881
|
providers [a,b] Change which tools get configs (claude, opencode, codex)
|
|
3561
3882
|
update [id] Pull newer catalog content into .agents/ (all or one entry)
|
|
3562
3883
|
list Show installed entries (skills, commands, plugins, MCP tool counts)
|
|
3884
|
+
inspect <name> Show an MCP server's tools with descriptions and token cost
|
|
3563
3885
|
check Detect drift: skill digests, provider shims, MCP tool
|
|
3564
3886
|
snapshots (--offline skips MCP re-introspection)
|
|
3565
3887
|
upstream Catalog maintenance: check source repos for skill updates
|
|
@@ -3571,8 +3893,9 @@ Commands:
|
|
|
3571
3893
|
Options:
|
|
3572
3894
|
-f, --force Overwrite existing files
|
|
3573
3895
|
--all, -y Keep everything without prompting (non-interactive)
|
|
3574
|
-
--json Machine-readable output (check/upstream/list)
|
|
3575
|
-
-V, --verbose Show full tool lists and description diffs (check)
|
|
3896
|
+
--json Machine-readable output (check/upstream/list/inspect)
|
|
3897
|
+
-V, --verbose Show full tool lists and description diffs (check);
|
|
3898
|
+
full tool descriptions (inspect)
|
|
3576
3899
|
--accept Record the current MCP tool snapshots as the new baseline (check)
|
|
3577
3900
|
--offline Skip MCP re-introspection; check digests + shims only (check)
|
|
3578
3901
|
--dry-run Report what would change without writing (update)
|
|
@@ -3696,6 +4019,11 @@ var run = async () => {
|
|
|
3696
4019
|
await list3(options);
|
|
3697
4020
|
break;
|
|
3698
4021
|
}
|
|
4022
|
+
case "inspect": {
|
|
4023
|
+
const { inspect: inspect2 } = await Promise.resolve().then(() => (init_inspect(), inspect_exports));
|
|
4024
|
+
await inspect2(options);
|
|
4025
|
+
break;
|
|
4026
|
+
}
|
|
3699
4027
|
case "check": {
|
|
3700
4028
|
const { check: check2 } = await Promise.resolve().then(() => (init_check(), check_exports));
|
|
3701
4029
|
await check2(options);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "quiver-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.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": {
|