quiver-cli 1.2.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 (2) hide show
  1. package/dist/cli.js +237 -10
  2. package/package.json +1 -1
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) => {
@@ -2570,6 +2596,7 @@ var init_list = __esm({
2570
2596
  enabled: !disabled.has(name),
2571
2597
  detail: serverDetail2.get(name) ?? null,
2572
2598
  toolCount: entry.tools ? Object.keys(entry.tools).length : null,
2599
+ tokenEstimate: entry.tools ? sumTokens(entry.tools) : null,
2573
2600
  authRequired: entry.authRequired ?? false
2574
2601
  })),
2575
2602
  plugins: plugins.map(({ name, entry }) => ({
@@ -2619,22 +2646,35 @@ var init_list = __esm({
2619
2646
  return `${n ?? "?"} tools`.length;
2620
2647
  })
2621
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));
2622
2654
  lines.push("", ` ${c.bold("mcp servers")}`);
2623
2655
  for (const { name, entry } of mcp) {
2624
2656
  const count = entry.tools ? Object.keys(entry.tools).length : null;
2657
+ const tokenTotal = entry.tools ? sumTokens(entry.tools) : null;
2625
2658
  if (count === null) {
2626
2659
  if (entry.authRequired) needsAuth.push(name);
2627
2660
  else missingTools = true;
2661
+ } else if (tokenTotal === null) {
2662
+ missingTools = true;
2628
2663
  }
2629
2664
  const tools = padCell(
2630
2665
  `${count ?? "?"} tools`,
2631
2666
  toolW,
2632
2667
  count === null ? c.dim : c.green
2633
2668
  );
2669
+ const tokens = padCell(
2670
+ tokenCell(entry),
2671
+ tokW,
2672
+ tokenTotal === null ? c.dim : c.cyan
2673
+ );
2634
2674
  const detail = serverDetail2.get(name);
2635
2675
  const off = disabled.has(name) ? ` ${c.yellow("disabled")}` : "";
2636
2676
  lines.push(
2637
- ` ${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
2638
2678
  );
2639
2679
  }
2640
2680
  }
@@ -2660,7 +2700,173 @@ var init_list = __esm({
2660
2700
  );
2661
2701
  }
2662
2702
  if (missingTools) {
2663
- 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
+ );
2664
2870
  }
2665
2871
  lines.push("");
2666
2872
  block(lines);
@@ -2839,21 +3045,34 @@ var init_opencode_auth = __esm({
2839
3045
  });
2840
3046
 
2841
3047
  // src/mcp/snapshot.ts
2842
- var toSnapshot;
3048
+ var toSnapshot, backfillTokens;
2843
3049
  var init_snapshot = __esm({
2844
3050
  "src/mcp/snapshot.ts"() {
2845
3051
  "use strict";
2846
3052
  init_digest();
3053
+ init_tokens();
2847
3054
  toSnapshot = (tools) => {
2848
3055
  const snapshot = {};
2849
3056
  for (const tool of tools) {
2850
3057
  snapshot[tool.name] = {
2851
3058
  description: tool.description,
2852
- inputSchemaHash: jsonDigest(tool.inputSchema)
3059
+ inputSchemaHash: jsonDigest(tool.inputSchema),
3060
+ tokens: estimateTokens(tool)
2853
3061
  };
2854
3062
  }
2855
3063
  return snapshot;
2856
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
+ };
2857
3076
  }
2858
3077
  });
2859
3078
 
@@ -2867,7 +3086,7 @@ __export(check_exports, {
2867
3086
  });
2868
3087
  import { accessSync as accessSync2, constants as constants2 } from "fs";
2869
3088
  import { delimiter, resolve as resolve19 } from "path";
2870
- var check, report2, driftLines, list2, recommend, summarize, authHint, hasCommand, truncate2, fail;
3089
+ var check, report2, driftLines, list2, recommend, summarize, authHint, hasCommand, truncate3, fail;
2871
3090
  var init_check = __esm({
2872
3091
  "src/commands/check.ts"() {
2873
3092
  "use strict";
@@ -2970,6 +3189,7 @@ var init_check = __esm({
2970
3189
  }
2971
3190
  const diff = diffSnapshots(mcpEntry.tools, current);
2972
3191
  if (isEmptyDiff(diff)) {
3192
+ if (backfillTokens(mcpEntry.tools, current)) lockChanged = true;
2973
3193
  mcpReports.push({ id, status: "ok" });
2974
3194
  } else if (options.accept) {
2975
3195
  mcpEntry.tools = current;
@@ -3086,8 +3306,8 @@ var init_check = __esm({
3086
3306
  for (const d of dc) {
3087
3307
  lines.push(
3088
3308
  ` "${d.name}":
3089
- before: ${truncate2(d.before)}
3090
- after: ${truncate2(d.after)}`
3309
+ before: ${truncate3(d.before)}
3310
+ after: ${truncate3(d.after)}`
3091
3311
  );
3092
3312
  }
3093
3313
  }
@@ -3151,7 +3371,7 @@ var init_check = __esm({
3151
3371
  }
3152
3372
  return false;
3153
3373
  };
3154
- 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;
3155
3375
  fail = async (options, code, message) => {
3156
3376
  if (options.json) console.log(JSON.stringify({ ok: false, error: code }));
3157
3377
  else await error(message);
@@ -3661,6 +3881,7 @@ Commands:
3661
3881
  providers [a,b] Change which tools get configs (claude, opencode, codex)
3662
3882
  update [id] Pull newer catalog content into .agents/ (all or one entry)
3663
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
3664
3885
  check Detect drift: skill digests, provider shims, MCP tool
3665
3886
  snapshots (--offline skips MCP re-introspection)
3666
3887
  upstream Catalog maintenance: check source repos for skill updates
@@ -3672,8 +3893,9 @@ Commands:
3672
3893
  Options:
3673
3894
  -f, --force Overwrite existing files
3674
3895
  --all, -y Keep everything without prompting (non-interactive)
3675
- --json Machine-readable output (check/upstream/list)
3676
- -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)
3677
3899
  --accept Record the current MCP tool snapshots as the new baseline (check)
3678
3900
  --offline Skip MCP re-introspection; check digests + shims only (check)
3679
3901
  --dry-run Report what would change without writing (update)
@@ -3797,6 +4019,11 @@ var run = async () => {
3797
4019
  await list3(options);
3798
4020
  break;
3799
4021
  }
4022
+ case "inspect": {
4023
+ const { inspect: inspect2 } = await Promise.resolve().then(() => (init_inspect(), inspect_exports));
4024
+ await inspect2(options);
4025
+ break;
4026
+ }
3800
4027
  case "check": {
3801
4028
  const { check: check2 } = await Promise.resolve().then(() => (init_check(), check_exports));
3802
4029
  await check2(options);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quiver-cli",
3
- "version": "1.2.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": {