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.
- package/dist/cli.js +237 -10
- 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(
|
|
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,
|
|
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: ${
|
|
3090
|
-
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
|
-
|
|
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.
|
|
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": {
|