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.
Files changed (3) hide show
  1. package/README.md +9 -3
  2. package/dist/cli.js +362 -34
  3. 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. Servers that fail introspection (e.g. requiring interactive
164
- OAuth) are reported as skipped. stdio servers run foreign code and are only
165
- introspected with `--introspect-stdio`.
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
- if (count === null) missingTools = true;
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(` ${c.dim("run 'quiver-cli check' to populate tool counts")}`);
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 requestInit = server.headers ? { headers: server.headers } : {};
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 resolve18 } from "path";
2798
- var check, report2, driftLines, list2, recommend, summarize, hasCommand, truncate2, fail;
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 res = await introspect(server, { allowStdio: options.introspectStdio });
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
- mcpReports.push({ id, status: "skipped", reason: res.reason });
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 skipped = mcpReports.filter((r) => r.status === "skipped");
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: ${truncate2(d.before)}
2995
- after: ${truncate2(d.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(resolve18(dir, command + extension), constants2.X_OK);
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
- truncate2 = (s, max = 120) => s.length > max ? s.slice(0, max) + "\u2026" : s;
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 readFileSync10,
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 resolve19 } from "path";
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) => resolve19(catalog.root, UPSTREAMS_FILE);
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(readFileSync10(path, "utf8"));
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 = resolve19(tmp, origin.path);
3153
- if (!existsSync14(resolve19(dir, "SKILL.md"))) {
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 readFileSync11, writeFileSync as writeFileSync9 } from "fs";
3447
- import { homedir as homedir2 } from "os";
3448
- import { dirname as dirname6, resolve as resolve20 } from "path";
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"] || resolve20(homedir2(), ".cache");
3460
- return resolve20(base, "quiver", "update-check.json");
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
- readFileSync11(resolve20(packageRoot, "package.json"), "utf8")
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(readFileSync11(path, "utf8"));
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.1.0",
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": {