quiver-cli 1.0.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 +26 -3
- package/dist/cli.js +336 -93
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -50,6 +50,8 @@ quiver-cli check # detect drift (CI-friendly: --json, exit 1)
|
|
|
50
50
|
| `quiver-cli init` | Interactive picker; write native configs + `quiver.lock` |
|
|
51
51
|
| `quiver-cli add <id>` | Add one entry (`skill:`, `command:`, `mcp:`, `plugin:`) |
|
|
52
52
|
| `quiver-cli remove <id>` | Remove one entry; keep lockfile + configs consistent (alias: `rm`)|
|
|
53
|
+
| `quiver-cli disable <id>` | Turn an MCP server off locally (`mcp:<name>`, gitignored override)|
|
|
54
|
+
| `quiver-cli enable <id>` | Turn a locally disabled MCP server back on |
|
|
53
55
|
| `quiver-cli update [id]` | Pull newer catalog content into `.agents/` (all or one entry) |
|
|
54
56
|
| `quiver-cli sync` | Regenerate provider configs from `.agents/`; warn on drift |
|
|
55
57
|
| `quiver-cli providers` | Change which tools get configs (`opencode`, `claude`, `codex`) |
|
|
@@ -120,6 +122,21 @@ quiver-cli add plugin:rtk
|
|
|
120
122
|
quiver-cli check # also verifies that the rtk binary is on PATH
|
|
121
123
|
```
|
|
122
124
|
|
|
125
|
+
## Toggling MCP servers locally
|
|
126
|
+
|
|
127
|
+
Some MCP servers are only worth their token cost for specific tasks. Instead
|
|
128
|
+
of removing them, switch them off locally:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
quiver-cli disable mcp:posthog # drop it from the generated provider configs
|
|
132
|
+
quiver-cli enable mcp:posthog # bring it back - instant, no re-introspection
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
The state lives in `.agents/config.local.json` (gitignored), so toggling never
|
|
136
|
+
touches the committed `.agents/config.json`, `quiver.lock` or your teammates'
|
|
137
|
+
setups. Disabled servers keep their lockfile entry and tool snapshot; `list`
|
|
138
|
+
marks them as `disabled`, and `check` skips their re-introspection.
|
|
139
|
+
|
|
123
140
|
## The lockfile
|
|
124
141
|
|
|
125
142
|
`quiver.lock` records, per entry: source path, content digest, and — for MCP
|
|
@@ -143,9 +160,15 @@ This is the basis for `sync` and `check`.
|
|
|
143
160
|
poisoning), shown as a readable before/after.
|
|
144
161
|
|
|
145
162
|
The first successful introspection records a baseline; subsequent `check` runs
|
|
146
|
-
diff against it.
|
|
147
|
-
|
|
148
|
-
|
|
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.
|
|
149
172
|
|
|
150
173
|
Pass `--offline` to skip MCP re-introspection entirely and check only digests
|
|
151
174
|
and provider shims — no network, no foreign code, useful for a fast local
|
package/dist/cli.js
CHANGED
|
@@ -1199,9 +1199,47 @@ var init_fsops = __esm({
|
|
|
1199
1199
|
}
|
|
1200
1200
|
});
|
|
1201
1201
|
|
|
1202
|
-
// src/providers/
|
|
1203
|
-
import { existsSync as existsSync8 } from "fs";
|
|
1202
|
+
// src/providers/local-config.ts
|
|
1203
|
+
import { existsSync as existsSync8, readFileSync as readFileSync6, rmSync as rmSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
1204
1204
|
import { resolve as resolve11 } from "path";
|
|
1205
|
+
var localConfigPath, readLocalConfig, disabledMcpServers, setMcpEnabled;
|
|
1206
|
+
var init_local_config = __esm({
|
|
1207
|
+
"src/providers/local-config.ts"() {
|
|
1208
|
+
"use strict";
|
|
1209
|
+
localConfigPath = (targetRoot) => resolve11(targetRoot, ".agents", "config.local.json");
|
|
1210
|
+
readLocalConfig = (targetRoot) => {
|
|
1211
|
+
const path = localConfigPath(targetRoot);
|
|
1212
|
+
if (!existsSync8(path)) return {};
|
|
1213
|
+
return JSON.parse(readFileSync6(path, "utf8"));
|
|
1214
|
+
};
|
|
1215
|
+
disabledMcpServers = (targetRoot) => {
|
|
1216
|
+
const disabled = /* @__PURE__ */ new Set();
|
|
1217
|
+
const servers = readLocalConfig(targetRoot).mcpServers ?? {};
|
|
1218
|
+
for (const [name, override] of Object.entries(servers)) {
|
|
1219
|
+
if (override.enabled === false) disabled.add(name);
|
|
1220
|
+
}
|
|
1221
|
+
return disabled;
|
|
1222
|
+
};
|
|
1223
|
+
setMcpEnabled = (targetRoot, name, enabled) => {
|
|
1224
|
+
const config = readLocalConfig(targetRoot);
|
|
1225
|
+
const servers = { ...config.mcpServers ?? {} };
|
|
1226
|
+
if (enabled) delete servers[name];
|
|
1227
|
+
else servers[name] = { enabled: false };
|
|
1228
|
+
if (Object.keys(servers).length) config.mcpServers = servers;
|
|
1229
|
+
else delete config.mcpServers;
|
|
1230
|
+
const path = localConfigPath(targetRoot);
|
|
1231
|
+
if (Object.keys(config).length === 0) {
|
|
1232
|
+
rmSync4(path, { force: true });
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
writeFileSync4(path, JSON.stringify(config, null, 2) + "\n");
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
});
|
|
1239
|
+
|
|
1240
|
+
// src/providers/opencode.ts
|
|
1241
|
+
import { existsSync as existsSync9 } from "fs";
|
|
1242
|
+
import { resolve as resolve12 } from "path";
|
|
1205
1243
|
var formatOpenCodeJson, preserveEnvReference, preserveEnvReferences, planOpenCode;
|
|
1206
1244
|
var init_opencode = __esm({
|
|
1207
1245
|
"src/providers/opencode.ts"() {
|
|
@@ -1253,11 +1291,11 @@ var init_opencode = __esm({
|
|
|
1253
1291
|
const files = [];
|
|
1254
1292
|
const removeFiles = [];
|
|
1255
1293
|
const json = formatOpenCodeJson(rawMcpServers, opencodeConfig);
|
|
1256
|
-
const jsonPath =
|
|
1294
|
+
const jsonPath = resolve12(targetRoot, "opencode.json");
|
|
1257
1295
|
if (json) files.push({ path: jsonPath, content: json });
|
|
1258
1296
|
else removeFiles.push(jsonPath);
|
|
1259
|
-
const tuiPath =
|
|
1260
|
-
const tuiMarkerPath =
|
|
1297
|
+
const tuiPath = resolve12(targetRoot, ".opencode/tui.json");
|
|
1298
|
+
const tuiMarkerPath = resolve12(targetRoot, ".opencode/.quiver-tui");
|
|
1261
1299
|
if (tuiConfig) {
|
|
1262
1300
|
files.push({
|
|
1263
1301
|
path: tuiPath,
|
|
@@ -1268,20 +1306,20 @@ var init_opencode = __esm({
|
|
|
1268
1306
|
) + "\n"
|
|
1269
1307
|
});
|
|
1270
1308
|
files.push({ path: tuiMarkerPath, content: "managed by quiver\n" });
|
|
1271
|
-
} else if (
|
|
1309
|
+
} else if (existsSync9(tuiMarkerPath)) {
|
|
1272
1310
|
removeFiles.push(tuiPath, tuiMarkerPath);
|
|
1273
1311
|
}
|
|
1274
1312
|
const symlinks = [
|
|
1275
1313
|
...selected.skills.map((s) => ({
|
|
1276
|
-
path:
|
|
1314
|
+
path: resolve12(targetRoot, ".opencode/skills", s.name),
|
|
1277
1315
|
target: s.absDir
|
|
1278
1316
|
})),
|
|
1279
1317
|
...selected.commands.map((c) => ({
|
|
1280
|
-
path:
|
|
1318
|
+
path: resolve12(targetRoot, ".opencode/commands", `${c.name}.md`),
|
|
1281
1319
|
target: c.absPath
|
|
1282
1320
|
})),
|
|
1283
1321
|
...selected.plugins.map((plugin) => ({
|
|
1284
|
-
path:
|
|
1322
|
+
path: resolve12(
|
|
1285
1323
|
targetRoot,
|
|
1286
1324
|
".opencode/plugins",
|
|
1287
1325
|
`${plugin.name}${plugin.absPath.endsWith(".js") ? ".js" : ".ts"}`
|
|
@@ -1291,15 +1329,15 @@ var init_opencode = __esm({
|
|
|
1291
1329
|
];
|
|
1292
1330
|
const managedDirs = [
|
|
1293
1331
|
{
|
|
1294
|
-
path:
|
|
1332
|
+
path: resolve12(targetRoot, ".opencode/skills"),
|
|
1295
1333
|
expected: new Set(selected.skills.map((s) => s.name))
|
|
1296
1334
|
},
|
|
1297
1335
|
{
|
|
1298
|
-
path:
|
|
1336
|
+
path: resolve12(targetRoot, ".opencode/commands"),
|
|
1299
1337
|
expected: new Set(selected.commands.map((c) => `${c.name}.md`))
|
|
1300
1338
|
},
|
|
1301
1339
|
{
|
|
1302
|
-
path:
|
|
1340
|
+
path: resolve12(targetRoot, ".opencode/plugins"),
|
|
1303
1341
|
expected: new Set(
|
|
1304
1342
|
selected.plugins.map(
|
|
1305
1343
|
(plugin) => `${plugin.name}${plugin.absPath.endsWith(".js") ? ".js" : ".ts"}`
|
|
@@ -1342,8 +1380,8 @@ var init_selection = __esm({
|
|
|
1342
1380
|
});
|
|
1343
1381
|
|
|
1344
1382
|
// src/providers/write.ts
|
|
1345
|
-
import { existsSync as
|
|
1346
|
-
import { basename, relative as relative5, resolve as
|
|
1383
|
+
import { existsSync as existsSync10, lstatSync as lstatSync2 } from "fs";
|
|
1384
|
+
import { basename, relative as relative5, resolve as resolve13 } from "path";
|
|
1347
1385
|
var isLinkable, buildPlan, writeProviders, formatWriteResult, checkProviders;
|
|
1348
1386
|
var init_write = __esm({
|
|
1349
1387
|
"src/providers/write.ts"() {
|
|
@@ -1353,6 +1391,7 @@ var init_write = __esm({
|
|
|
1353
1391
|
init_claude();
|
|
1354
1392
|
init_codex();
|
|
1355
1393
|
init_fsops();
|
|
1394
|
+
init_local_config();
|
|
1356
1395
|
init_opencode();
|
|
1357
1396
|
init_selection();
|
|
1358
1397
|
isLinkable = (path) => {
|
|
@@ -1365,10 +1404,14 @@ var init_write = __esm({
|
|
|
1365
1404
|
buildPlan = (targetRoot, catalog, lock, onMissingEnv, onSkippedRootFile) => {
|
|
1366
1405
|
loadEnvLocal(targetRoot);
|
|
1367
1406
|
const selected = resolveSelection(catalog, lock);
|
|
1407
|
+
const disabled = disabledMcpServers(targetRoot);
|
|
1408
|
+
if (disabled.size) {
|
|
1409
|
+
selected.mcp = selected.mcp.filter((m) => !disabled.has(m.name));
|
|
1410
|
+
}
|
|
1368
1411
|
const rawMcpServers = {};
|
|
1369
1412
|
for (const mcp of selected.mcp) rawMcpServers[mcp.name] = mcp.server;
|
|
1370
1413
|
const mcpServers = interpolateEnvVars(rawMcpServers, onMissingEnv);
|
|
1371
|
-
const agentsRoot =
|
|
1414
|
+
const agentsRoot = resolve13(targetRoot, ".agents");
|
|
1372
1415
|
const inputs = {
|
|
1373
1416
|
targetRoot,
|
|
1374
1417
|
agentsRoot,
|
|
@@ -1401,12 +1444,12 @@ var init_write = __esm({
|
|
|
1401
1444
|
}
|
|
1402
1445
|
}
|
|
1403
1446
|
const rootSymlinks = [];
|
|
1404
|
-
const agentsMd =
|
|
1405
|
-
if (
|
|
1406
|
-
const rootAgents =
|
|
1447
|
+
const agentsMd = resolve13(agentsRoot, "AGENTS.md");
|
|
1448
|
+
if (existsSync10(agentsMd)) {
|
|
1449
|
+
const rootAgents = resolve13(targetRoot, "AGENTS.md");
|
|
1407
1450
|
for (const link of [
|
|
1408
1451
|
{ path: rootAgents, target: agentsMd },
|
|
1409
|
-
{ path:
|
|
1452
|
+
{ path: resolve13(targetRoot, "CLAUDE.md"), target: rootAgents }
|
|
1410
1453
|
]) {
|
|
1411
1454
|
if (isLinkable(link.path)) rootSymlinks.push(link);
|
|
1412
1455
|
else onSkippedRootFile?.(basename(link.path));
|
|
@@ -1468,15 +1511,16 @@ var init_write = __esm({
|
|
|
1468
1511
|
|
|
1469
1512
|
// src/commands/gitignore.ts
|
|
1470
1513
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
1471
|
-
import { existsSync as
|
|
1472
|
-
import { resolve as
|
|
1473
|
-
var HEADER, SECRETS_COMMENT, PROVIDER_ENTRIES, SHARED_ENTRIES, SECRET_ENTRIES, patchGitignore, ignoredSourcePaths;
|
|
1514
|
+
import { existsSync as existsSync11, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
|
|
1515
|
+
import { resolve as resolve14 } from "path";
|
|
1516
|
+
var HEADER, SECRETS_COMMENT, LOCAL_COMMENT, PROVIDER_ENTRIES, SHARED_ENTRIES, SECRET_ENTRIES, LOCAL_ENTRIES, patchGitignore, ensureLocalOverrideIgnored, ignoredSourcePaths;
|
|
1474
1517
|
var init_gitignore = __esm({
|
|
1475
1518
|
"src/commands/gitignore.ts"() {
|
|
1476
1519
|
"use strict";
|
|
1477
1520
|
init_schema();
|
|
1478
1521
|
HEADER = "# Generated by quiver (source of truth: .agents/ + quiver.lock)";
|
|
1479
1522
|
SECRETS_COMMENT = "# Local secrets for MCP env-var interpolation";
|
|
1523
|
+
LOCAL_COMMENT = "# Local overrides (quiver-cli enable/disable)";
|
|
1480
1524
|
PROVIDER_ENTRIES = {
|
|
1481
1525
|
claude: [".claude/", ".mcp.json"],
|
|
1482
1526
|
opencode: [".opencode/", "opencode.json"],
|
|
@@ -1484,9 +1528,10 @@ var init_gitignore = __esm({
|
|
|
1484
1528
|
};
|
|
1485
1529
|
SHARED_ENTRIES = ["AGENTS.md", "CLAUDE.md"];
|
|
1486
1530
|
SECRET_ENTRIES = [".env.local"];
|
|
1531
|
+
LOCAL_ENTRIES = [".agents/config.local.json"];
|
|
1487
1532
|
patchGitignore = (targetRoot, providers2) => {
|
|
1488
|
-
const path =
|
|
1489
|
-
const current =
|
|
1533
|
+
const path = resolve14(targetRoot, ".gitignore");
|
|
1534
|
+
const current = existsSync11(path) ? readFileSync7(path, "utf8") : "";
|
|
1490
1535
|
const lines = new Set(current.split("\n").map((l) => l.trim()));
|
|
1491
1536
|
const active = providers2?.length ? providers2 : [...PROVIDERS];
|
|
1492
1537
|
const shimEntries = [
|
|
@@ -1495,17 +1540,34 @@ var init_gitignore = __esm({
|
|
|
1495
1540
|
];
|
|
1496
1541
|
const missingShims = shimEntries.filter((e) => !lines.has(e));
|
|
1497
1542
|
const missingSecrets = SECRET_ENTRIES.filter((e) => !lines.has(e));
|
|
1498
|
-
|
|
1543
|
+
const missingLocal = LOCAL_ENTRIES.filter((e) => !lines.has(e));
|
|
1544
|
+
if (missingShims.length === 0 && missingSecrets.length === 0 && missingLocal.length === 0) {
|
|
1545
|
+
return false;
|
|
1546
|
+
}
|
|
1499
1547
|
const block2 = [];
|
|
1500
1548
|
if (missingShims.length) block2.push(HEADER, ...missingShims);
|
|
1501
1549
|
if (missingSecrets.length) block2.push(SECRETS_COMMENT, ...missingSecrets);
|
|
1550
|
+
if (missingLocal.length) block2.push(LOCAL_COMMENT, ...missingLocal);
|
|
1502
1551
|
const prefix = current && !current.endsWith("\n") ? "\n" : "";
|
|
1503
|
-
|
|
1552
|
+
writeFileSync5(
|
|
1504
1553
|
path,
|
|
1505
1554
|
current + prefix + (current ? "\n" : "") + block2.join("\n") + "\n"
|
|
1506
1555
|
);
|
|
1507
1556
|
return true;
|
|
1508
1557
|
};
|
|
1558
|
+
ensureLocalOverrideIgnored = (targetRoot) => {
|
|
1559
|
+
const path = resolve14(targetRoot, ".gitignore");
|
|
1560
|
+
const current = existsSync11(path) ? readFileSync7(path, "utf8") : "";
|
|
1561
|
+
const lines = new Set(current.split("\n").map((l) => l.trim()));
|
|
1562
|
+
const missing = LOCAL_ENTRIES.filter((e) => !lines.has(e));
|
|
1563
|
+
if (!missing.length) return false;
|
|
1564
|
+
const prefix = current && !current.endsWith("\n") ? "\n" : "";
|
|
1565
|
+
writeFileSync5(
|
|
1566
|
+
path,
|
|
1567
|
+
current + prefix + (current ? "\n" : "") + [LOCAL_COMMENT, ...missing].join("\n") + "\n"
|
|
1568
|
+
);
|
|
1569
|
+
return true;
|
|
1570
|
+
};
|
|
1509
1571
|
ignoredSourcePaths = (targetRoot) => {
|
|
1510
1572
|
try {
|
|
1511
1573
|
const out = execFileSync2(
|
|
@@ -1747,15 +1809,15 @@ var init_init = __esm({
|
|
|
1747
1809
|
});
|
|
1748
1810
|
|
|
1749
1811
|
// src/catalog/repo.ts
|
|
1750
|
-
import { existsSync as
|
|
1751
|
-
import { resolve as
|
|
1812
|
+
import { existsSync as existsSync12 } from "fs";
|
|
1813
|
+
import { resolve as resolve15 } from "path";
|
|
1752
1814
|
var repoCatalogRoot, repoCatalogExists, loadRepoCatalog;
|
|
1753
1815
|
var init_repo = __esm({
|
|
1754
1816
|
"src/catalog/repo.ts"() {
|
|
1755
1817
|
"use strict";
|
|
1756
1818
|
init_discover();
|
|
1757
|
-
repoCatalogRoot = (targetRoot) =>
|
|
1758
|
-
repoCatalogExists = (targetRoot) =>
|
|
1819
|
+
repoCatalogRoot = (targetRoot) => resolve15(targetRoot, ".agents");
|
|
1820
|
+
repoCatalogExists = (targetRoot) => existsSync12(repoCatalogRoot(targetRoot));
|
|
1759
1821
|
loadRepoCatalog = (targetRoot, source) => {
|
|
1760
1822
|
const resolved = {
|
|
1761
1823
|
source,
|
|
@@ -1869,8 +1931,8 @@ var remove_exports = {};
|
|
|
1869
1931
|
__export(remove_exports, {
|
|
1870
1932
|
remove: () => remove
|
|
1871
1933
|
});
|
|
1872
|
-
import { existsSync as
|
|
1873
|
-
import { resolve as
|
|
1934
|
+
import { existsSync as existsSync13, readdirSync as readdirSync4, readFileSync as readFileSync8, rmSync as rmSync5, writeFileSync as writeFileSync6 } from "fs";
|
|
1935
|
+
import { resolve as resolve16 } from "path";
|
|
1874
1936
|
var remove, rewriteRepoPlugins, cleanupEmptyGroups, rewriteRepoMcp;
|
|
1875
1937
|
var init_remove = __esm({
|
|
1876
1938
|
"src/commands/remove.ts"() {
|
|
@@ -1920,9 +1982,9 @@ var init_remove = __esm({
|
|
|
1920
1982
|
await success(`Removed ${id}.`);
|
|
1921
1983
|
};
|
|
1922
1984
|
rewriteRepoPlugins = (targetRoot, lock) => {
|
|
1923
|
-
const configPath =
|
|
1924
|
-
if (!
|
|
1925
|
-
const config = JSON.parse(
|
|
1985
|
+
const configPath = resolve16(targetRoot, ".agents/config.json");
|
|
1986
|
+
if (!existsSync13(configPath)) return;
|
|
1987
|
+
const config = JSON.parse(readFileSync8(configPath, "utf8"));
|
|
1926
1988
|
if (!config.plugins) return;
|
|
1927
1989
|
const keep = new Set(
|
|
1928
1990
|
Object.keys(lock.entries).map(parseEntryId).filter((p) => p?.type === "plugin").map((p) => p.name)
|
|
@@ -1931,22 +1993,22 @@ var init_remove = __esm({
|
|
|
1931
1993
|
Object.entries(config.plugins).filter(([name]) => keep.has(name))
|
|
1932
1994
|
);
|
|
1933
1995
|
if (!Object.keys(config.plugins).length) delete config.plugins;
|
|
1934
|
-
|
|
1996
|
+
writeFileSync6(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
1935
1997
|
};
|
|
1936
1998
|
cleanupEmptyGroups = (targetRoot) => {
|
|
1937
|
-
const skillsRoot =
|
|
1938
|
-
if (!
|
|
1999
|
+
const skillsRoot = resolve16(targetRoot, ".agents/skills");
|
|
2000
|
+
if (!existsSync13(skillsRoot)) return;
|
|
1939
2001
|
for (const entry of readdirSync4(skillsRoot, { withFileTypes: true })) {
|
|
1940
2002
|
if (!entry.isDirectory()) continue;
|
|
1941
|
-
const dir =
|
|
1942
|
-
if (
|
|
1943
|
-
if (readdirSync4(dir).length === 0)
|
|
2003
|
+
const dir = resolve16(skillsRoot, entry.name);
|
|
2004
|
+
if (existsSync13(resolve16(dir, "SKILL.md"))) continue;
|
|
2005
|
+
if (readdirSync4(dir).length === 0) rmSync5(dir, { recursive: true, force: true });
|
|
1944
2006
|
}
|
|
1945
2007
|
};
|
|
1946
2008
|
rewriteRepoMcp = (targetRoot, lock) => {
|
|
1947
|
-
const configPath =
|
|
1948
|
-
if (!
|
|
1949
|
-
const config = JSON.parse(
|
|
2009
|
+
const configPath = resolve16(targetRoot, ".agents/config.json");
|
|
2010
|
+
if (!existsSync13(configPath)) return;
|
|
2011
|
+
const config = JSON.parse(readFileSync8(configPath, "utf8"));
|
|
1950
2012
|
if (!config.mcpServers) return;
|
|
1951
2013
|
const keep = new Set(
|
|
1952
2014
|
Object.keys(lock.entries).map(parseEntryId).filter((p) => p?.type === "mcp").map((p) => p.name)
|
|
@@ -1957,7 +2019,69 @@ var init_remove = __esm({
|
|
|
1957
2019
|
}
|
|
1958
2020
|
if (Object.keys(kept).length) config.mcpServers = kept;
|
|
1959
2021
|
else delete config.mcpServers;
|
|
1960
|
-
|
|
2022
|
+
writeFileSync6(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
2023
|
+
};
|
|
2024
|
+
}
|
|
2025
|
+
});
|
|
2026
|
+
|
|
2027
|
+
// src/commands/toggle.ts
|
|
2028
|
+
var toggle_exports = {};
|
|
2029
|
+
__export(toggle_exports, {
|
|
2030
|
+
toggle: () => toggle
|
|
2031
|
+
});
|
|
2032
|
+
var toggle;
|
|
2033
|
+
var init_toggle = __esm({
|
|
2034
|
+
"src/commands/toggle.ts"() {
|
|
2035
|
+
"use strict";
|
|
2036
|
+
init_repo();
|
|
2037
|
+
init_io();
|
|
2038
|
+
init_schema();
|
|
2039
|
+
init_local_config();
|
|
2040
|
+
init_write();
|
|
2041
|
+
init_prompts();
|
|
2042
|
+
init_gitignore();
|
|
2043
|
+
toggle = async (options, enabled) => {
|
|
2044
|
+
const verb = enabled ? "enable" : "disable";
|
|
2045
|
+
const id = options.positionals[0];
|
|
2046
|
+
const parsed = id ? parseEntryId(id) : null;
|
|
2047
|
+
if (!parsed || parsed.type !== "mcp") {
|
|
2048
|
+
await error(`Usage: quiver-cli ${verb} mcp:<name>`);
|
|
2049
|
+
process.exitCode = 1;
|
|
2050
|
+
return;
|
|
2051
|
+
}
|
|
2052
|
+
const lock = readLockfile(options.targetRoot);
|
|
2053
|
+
if (!lock) {
|
|
2054
|
+
await error("No quiver.lock found. Run `quiver-cli init` first.");
|
|
2055
|
+
process.exitCode = 1;
|
|
2056
|
+
return;
|
|
2057
|
+
}
|
|
2058
|
+
if (!repoCatalogExists(options.targetRoot)) {
|
|
2059
|
+
await error("No .agents/ directory found. Run `quiver-cli init` first.");
|
|
2060
|
+
process.exitCode = 1;
|
|
2061
|
+
return;
|
|
2062
|
+
}
|
|
2063
|
+
if (!lock.entries[id]) {
|
|
2064
|
+
const installed = Object.keys(lock.entries).map(parseEntryId).filter((p) => p?.type === "mcp").map((p) => p.name).sort((a, b) => a.localeCompare(b));
|
|
2065
|
+
await error(
|
|
2066
|
+
`${id} is not installed.` + (installed.length ? ` Installed MCP servers: ${installed.join(", ")}.` : " No MCP servers installed.")
|
|
2067
|
+
);
|
|
2068
|
+
process.exitCode = 1;
|
|
2069
|
+
return;
|
|
2070
|
+
}
|
|
2071
|
+
const disabled = disabledMcpServers(options.targetRoot);
|
|
2072
|
+
if (enabled !== disabled.has(parsed.name)) {
|
|
2073
|
+
await info(`${id} is already ${enabled ? "enabled" : "disabled"}.`);
|
|
2074
|
+
return;
|
|
2075
|
+
}
|
|
2076
|
+
setMcpEnabled(options.targetRoot, parsed.name, enabled);
|
|
2077
|
+
if (!enabled && ensureLocalOverrideIgnored(options.targetRoot)) {
|
|
2078
|
+
await step("Added .agents/config.local.json to .gitignore");
|
|
2079
|
+
}
|
|
2080
|
+
const { catalog } = loadRepoCatalog(options.targetRoot, lock.catalog.source);
|
|
2081
|
+
writeProviders(options.targetRoot, catalog, lock);
|
|
2082
|
+
await success(
|
|
2083
|
+
enabled ? `Enabled ${id}.` : `Disabled ${id} locally (.agents/config.local.json). Re-enable with \`quiver-cli enable ${id}\`.`
|
|
2084
|
+
);
|
|
1961
2085
|
};
|
|
1962
2086
|
}
|
|
1963
2087
|
});
|
|
@@ -2174,8 +2298,8 @@ var update_exports = {};
|
|
|
2174
2298
|
__export(update_exports, {
|
|
2175
2299
|
update: () => update
|
|
2176
2300
|
});
|
|
2177
|
-
import { cpSync as cpSync2, mkdirSync as mkdirSync5, readFileSync as
|
|
2178
|
-
import { dirname as dirname5, resolve as
|
|
2301
|
+
import { cpSync as cpSync2, mkdirSync as mkdirSync5, readFileSync as readFileSync9, rmSync as rmSync6, writeFileSync as writeFileSync7 } from "fs";
|
|
2302
|
+
import { dirname as dirname5, resolve as resolve17 } from "path";
|
|
2179
2303
|
var update, applyUpdate, report;
|
|
2180
2304
|
var init_update = __esm({
|
|
2181
2305
|
"src/commands/update.ts"() {
|
|
@@ -2262,8 +2386,8 @@ var init_update = __esm({
|
|
|
2262
2386
|
return { id, status: "local-changes" };
|
|
2263
2387
|
}
|
|
2264
2388
|
if (dryRun) return { id, status: "updated" };
|
|
2265
|
-
const dest =
|
|
2266
|
-
|
|
2389
|
+
const dest = resolve17(targetRoot, ".agents", src2.sourcePath);
|
|
2390
|
+
rmSync6(dest, { recursive: true, force: true });
|
|
2267
2391
|
mkdirSync5(dirname5(dest), { recursive: true });
|
|
2268
2392
|
cpSync2(src2.absDir, dest, { recursive: true });
|
|
2269
2393
|
entry.digest = src2.digest;
|
|
@@ -2280,7 +2404,7 @@ var init_update = __esm({
|
|
|
2280
2404
|
return { id, status: "local-changes" };
|
|
2281
2405
|
}
|
|
2282
2406
|
if (dryRun) return { id, status: "updated" };
|
|
2283
|
-
const dest =
|
|
2407
|
+
const dest = resolve17(targetRoot, ".agents", src2.sourcePath);
|
|
2284
2408
|
mkdirSync5(dirname5(dest), { recursive: true });
|
|
2285
2409
|
cpSync2(src2.absPath, dest, { force: true });
|
|
2286
2410
|
entry.digest = src2.digest;
|
|
@@ -2296,16 +2420,16 @@ var init_update = __esm({
|
|
|
2296
2420
|
return { id, status: "local-changes" };
|
|
2297
2421
|
}
|
|
2298
2422
|
if (dryRun) return { id, status: "updated" };
|
|
2299
|
-
const dest =
|
|
2423
|
+
const dest = resolve17(targetRoot, ".agents", src2.sourcePath);
|
|
2300
2424
|
mkdirSync5(dirname5(dest), { recursive: true });
|
|
2301
2425
|
cpSync2(src2.absPath, dest, { force: true });
|
|
2302
|
-
const configPath2 =
|
|
2303
|
-
const config2 = JSON.parse(
|
|
2426
|
+
const configPath2 = resolve17(targetRoot, ".agents/config.json");
|
|
2427
|
+
const config2 = JSON.parse(readFileSync9(configPath2, "utf8"));
|
|
2304
2428
|
config2.plugins = {
|
|
2305
2429
|
...config2.plugins,
|
|
2306
2430
|
[parsed.name]: sourceCatalog.config.plugins?.[parsed.name]
|
|
2307
2431
|
};
|
|
2308
|
-
|
|
2432
|
+
writeFileSync7(configPath2, JSON.stringify(config2, null, 2) + "\n");
|
|
2309
2433
|
entry.digest = src2.digest;
|
|
2310
2434
|
entry.sourcePath = src2.sourcePath;
|
|
2311
2435
|
entry.requires = src2.requires;
|
|
@@ -2319,10 +2443,10 @@ var init_update = __esm({
|
|
|
2319
2443
|
return { id, status: "local-changes" };
|
|
2320
2444
|
}
|
|
2321
2445
|
if (dryRun) return { id, status: "updated" };
|
|
2322
|
-
const configPath =
|
|
2323
|
-
const config = JSON.parse(
|
|
2446
|
+
const configPath = resolve17(targetRoot, ".agents", "config.json");
|
|
2447
|
+
const config = JSON.parse(readFileSync9(configPath, "utf8"));
|
|
2324
2448
|
config.mcpServers = { ...config.mcpServers, [parsed.name]: src.server };
|
|
2325
|
-
|
|
2449
|
+
writeFileSync7(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
2326
2450
|
entry.configDigest = src.configDigest;
|
|
2327
2451
|
entry.transport = src.server.transport;
|
|
2328
2452
|
entry.tools = null;
|
|
@@ -2387,6 +2511,7 @@ var init_list = __esm({
|
|
|
2387
2511
|
init_repo();
|
|
2388
2512
|
init_io();
|
|
2389
2513
|
init_schema();
|
|
2514
|
+
init_local_config();
|
|
2390
2515
|
init_prompts();
|
|
2391
2516
|
truncate = (s, max) => {
|
|
2392
2517
|
const flat = s.replace(/\s+/g, " ").trim();
|
|
@@ -2427,6 +2552,7 @@ var init_list = __esm({
|
|
|
2427
2552
|
for (const group of [skills, commands, mcp, plugins]) {
|
|
2428
2553
|
group.sort((a, b) => a.name.localeCompare(b.name));
|
|
2429
2554
|
}
|
|
2555
|
+
const disabled = disabledMcpServers(options.targetRoot);
|
|
2430
2556
|
if (options.json) {
|
|
2431
2557
|
console.log(
|
|
2432
2558
|
JSON.stringify(
|
|
@@ -2441,8 +2567,10 @@ var init_list = __esm({
|
|
|
2441
2567
|
mcp: mcp.map(({ name, entry }) => ({
|
|
2442
2568
|
name,
|
|
2443
2569
|
transport: entry.transport,
|
|
2570
|
+
enabled: !disabled.has(name),
|
|
2444
2571
|
detail: serverDetail2.get(name) ?? null,
|
|
2445
|
-
toolCount: entry.tools ? Object.keys(entry.tools).length : null
|
|
2572
|
+
toolCount: entry.tools ? Object.keys(entry.tools).length : null,
|
|
2573
|
+
authRequired: entry.authRequired ?? false
|
|
2446
2574
|
})),
|
|
2447
2575
|
plugins: plugins.map(({ name, entry }) => ({
|
|
2448
2576
|
name,
|
|
@@ -2482,6 +2610,7 @@ var init_list = __esm({
|
|
|
2482
2610
|
}
|
|
2483
2611
|
}
|
|
2484
2612
|
let missingTools = false;
|
|
2613
|
+
const needsAuth = [];
|
|
2485
2614
|
if (mcp.length) {
|
|
2486
2615
|
const nameW = Math.max(...mcp.map((e) => e.name.length));
|
|
2487
2616
|
const toolW = Math.max(
|
|
@@ -2493,15 +2622,19 @@ var init_list = __esm({
|
|
|
2493
2622
|
lines.push("", ` ${c.bold("mcp servers")}`);
|
|
2494
2623
|
for (const { name, entry } of mcp) {
|
|
2495
2624
|
const count = entry.tools ? Object.keys(entry.tools).length : null;
|
|
2496
|
-
if (count === null)
|
|
2625
|
+
if (count === null) {
|
|
2626
|
+
if (entry.authRequired) needsAuth.push(name);
|
|
2627
|
+
else missingTools = true;
|
|
2628
|
+
}
|
|
2497
2629
|
const tools = padCell(
|
|
2498
2630
|
`${count ?? "?"} tools`,
|
|
2499
2631
|
toolW,
|
|
2500
2632
|
count === null ? c.dim : c.green
|
|
2501
2633
|
);
|
|
2502
2634
|
const detail = serverDetail2.get(name);
|
|
2635
|
+
const off = disabled.has(name) ? ` ${c.yellow("disabled")}` : "";
|
|
2503
2636
|
lines.push(
|
|
2504
|
-
` ${name.padEnd(nameW)} ${entry.transport.padEnd(5)} ${tools}` + (detail ? ` ${c.dim(detail)}` : "")
|
|
2637
|
+
` ${name.padEnd(nameW)} ${entry.transport.padEnd(5)} ${tools}` + (detail ? ` ${c.dim(detail)}` : "") + off
|
|
2505
2638
|
);
|
|
2506
2639
|
}
|
|
2507
2640
|
}
|
|
@@ -2519,6 +2652,13 @@ var init_list = __esm({
|
|
|
2519
2652
|
`${skills.length} skills \xB7 ${commands.length} commands \xB7 ${mcp.length} MCP servers \xB7 ${plugins.length} plugins`
|
|
2520
2653
|
)} ${c.dim(`providers: ${providers2}`)}`
|
|
2521
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
|
+
}
|
|
2522
2662
|
if (missingTools) {
|
|
2523
2663
|
lines.push(` ${c.dim("run 'quiver-cli check' to populate tool counts")}`);
|
|
2524
2664
|
}
|
|
@@ -2570,7 +2710,7 @@ var init_diff = __esm({
|
|
|
2570
2710
|
});
|
|
2571
2711
|
|
|
2572
2712
|
// src/mcp/introspect.ts
|
|
2573
|
-
var CONNECT_TIMEOUT_MS, withTimeout, introspect, errMsg;
|
|
2713
|
+
var CONNECT_TIMEOUT_MS, withTimeout, introspect, errMsg, isAuthError;
|
|
2574
2714
|
var init_introspect = __esm({
|
|
2575
2715
|
"src/mcp/introspect.ts"() {
|
|
2576
2716
|
"use strict";
|
|
@@ -2586,13 +2726,20 @@ var init_introspect = __esm({
|
|
|
2586
2726
|
clearTimeout(timer);
|
|
2587
2727
|
}
|
|
2588
2728
|
};
|
|
2589
|
-
introspect = async (server, { allowStdio }) => {
|
|
2729
|
+
introspect = async (server, { allowStdio, authToken }) => {
|
|
2590
2730
|
const { Client } = await import("@modelcontextprotocol/sdk/client/index.js");
|
|
2591
2731
|
let transport;
|
|
2592
2732
|
try {
|
|
2593
2733
|
if (server.transport === "http") {
|
|
2594
2734
|
const { StreamableHTTPClientTransport } = await import("@modelcontextprotocol/sdk/client/streamableHttp.js");
|
|
2595
|
-
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 } : {};
|
|
2596
2743
|
transport = new StreamableHTTPClientTransport(new URL(server.url), {
|
|
2597
2744
|
requestInit
|
|
2598
2745
|
});
|
|
@@ -2627,6 +2774,9 @@ var init_introspect = __esm({
|
|
|
2627
2774
|
}));
|
|
2628
2775
|
return { ok: true, tools };
|
|
2629
2776
|
} catch (e) {
|
|
2777
|
+
if (await isAuthError(e)) {
|
|
2778
|
+
return { ok: false, reason: errMsg(e), authRequired: true };
|
|
2779
|
+
}
|
|
2630
2780
|
return { ok: false, reason: errMsg(e) };
|
|
2631
2781
|
} finally {
|
|
2632
2782
|
try {
|
|
@@ -2636,6 +2786,55 @@ var init_introspect = __esm({
|
|
|
2636
2786
|
}
|
|
2637
2787
|
};
|
|
2638
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
|
+
};
|
|
2639
2838
|
}
|
|
2640
2839
|
});
|
|
2641
2840
|
|
|
@@ -2661,13 +2860,14 @@ var init_snapshot = __esm({
|
|
|
2661
2860
|
// src/commands/check.ts
|
|
2662
2861
|
var check_exports = {};
|
|
2663
2862
|
__export(check_exports, {
|
|
2863
|
+
authHint: () => authHint,
|
|
2664
2864
|
check: () => check,
|
|
2665
2865
|
hasCommand: () => hasCommand,
|
|
2666
2866
|
summarize: () => summarize
|
|
2667
2867
|
});
|
|
2668
2868
|
import { accessSync as accessSync2, constants as constants2 } from "fs";
|
|
2669
|
-
import { delimiter, resolve as
|
|
2670
|
-
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;
|
|
2671
2871
|
var init_check = __esm({
|
|
2672
2872
|
"src/commands/check.ts"() {
|
|
2673
2873
|
"use strict";
|
|
@@ -2676,7 +2876,9 @@ var init_check = __esm({
|
|
|
2676
2876
|
init_schema();
|
|
2677
2877
|
init_diff();
|
|
2678
2878
|
init_introspect();
|
|
2879
|
+
init_opencode_auth();
|
|
2679
2880
|
init_snapshot();
|
|
2881
|
+
init_local_config();
|
|
2680
2882
|
init_write();
|
|
2681
2883
|
init_interpolate();
|
|
2682
2884
|
init_prompts();
|
|
@@ -2724,6 +2926,7 @@ var init_check = __esm({
|
|
|
2724
2926
|
}
|
|
2725
2927
|
const shimProblems = checkProviders(options.targetRoot, catalog, lock);
|
|
2726
2928
|
const mcpReports = [];
|
|
2929
|
+
const disabled = disabledMcpServers(options.targetRoot);
|
|
2727
2930
|
let lockChanged = false;
|
|
2728
2931
|
for (const [id, entry] of Object.entries(lock.entries)) {
|
|
2729
2932
|
if (options.offline) break;
|
|
@@ -2731,15 +2934,33 @@ var init_check = __esm({
|
|
|
2731
2934
|
const p = parseEntryId(id);
|
|
2732
2935
|
const catMcp = catalog.mcp.find((m) => m.name === p.name);
|
|
2733
2936
|
if (!catMcp) continue;
|
|
2937
|
+
if (disabled.has(p.name)) {
|
|
2938
|
+
mcpReports.push({ id, status: "skipped", reason: "disabled locally" });
|
|
2939
|
+
continue;
|
|
2940
|
+
}
|
|
2734
2941
|
checked.mcp += 1;
|
|
2735
2942
|
const server = interpolateEnvVars(catMcp.server);
|
|
2736
|
-
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
|
+
});
|
|
2737
2949
|
if (!res.ok) {
|
|
2738
|
-
|
|
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
|
+
});
|
|
2739
2961
|
continue;
|
|
2740
2962
|
}
|
|
2741
2963
|
const current = toSnapshot(res.tools);
|
|
2742
|
-
const mcpEntry = entry;
|
|
2743
2964
|
if (!mcpEntry.tools) {
|
|
2744
2965
|
mcpEntry.tools = current;
|
|
2745
2966
|
mcpEntry.toolsFetchedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -2808,7 +3029,15 @@ var init_check = __esm({
|
|
|
2808
3029
|
- ${shimProblems.join("\n - ")}`
|
|
2809
3030
|
);
|
|
2810
3031
|
}
|
|
2811
|
-
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
|
+
);
|
|
2812
3041
|
if (skipped.length) {
|
|
2813
3042
|
const names = skipped.map((r) => parseEntryId(r.id)?.name ?? r.id);
|
|
2814
3043
|
await info(
|
|
@@ -2902,13 +3131,19 @@ var init_check = __esm({
|
|
|
2902
3131
|
if (c.plugins) parts.push(plural(c.plugins, "plugin"));
|
|
2903
3132
|
return parts.length ? parts.join(", ") : "nothing";
|
|
2904
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
|
+
};
|
|
2905
3140
|
hasCommand = (command) => {
|
|
2906
3141
|
if (!/^[A-Za-z0-9._-]+$/.test(command)) return false;
|
|
2907
3142
|
const extensions = process.platform === "win32" ? (process.env["PATHEXT"] ?? ".EXE;.CMD;.BAT;.COM").split(";") : [""];
|
|
2908
3143
|
for (const dir of (process.env["PATH"] ?? "").split(delimiter)) {
|
|
2909
3144
|
for (const extension of extensions) {
|
|
2910
3145
|
try {
|
|
2911
|
-
accessSync2(
|
|
3146
|
+
accessSync2(resolve19(dir, command + extension), constants2.X_OK);
|
|
2912
3147
|
return true;
|
|
2913
3148
|
} catch {
|
|
2914
3149
|
}
|
|
@@ -2928,14 +3163,14 @@ var init_check = __esm({
|
|
|
2928
3163
|
// src/catalog/upstreams.ts
|
|
2929
3164
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
2930
3165
|
import {
|
|
2931
|
-
existsSync as
|
|
3166
|
+
existsSync as existsSync14,
|
|
2932
3167
|
mkdtempSync as mkdtempSync2,
|
|
2933
|
-
readFileSync as
|
|
2934
|
-
rmSync as
|
|
2935
|
-
writeFileSync as
|
|
3168
|
+
readFileSync as readFileSync11,
|
|
3169
|
+
rmSync as rmSync7,
|
|
3170
|
+
writeFileSync as writeFileSync8
|
|
2936
3171
|
} from "fs";
|
|
2937
3172
|
import { tmpdir } from "os";
|
|
2938
|
-
import { join, resolve as
|
|
3173
|
+
import { join, resolve as resolve20 } from "path";
|
|
2939
3174
|
var UPSTREAMS_FILE, upstreamsPath, loadUpstreams, writeUpstreams, fetchLatestCommit, fetchUpstreamDir, short, evaluateOrigin;
|
|
2940
3175
|
var init_upstreams = __esm({
|
|
2941
3176
|
"src/catalog/upstreams.ts"() {
|
|
@@ -2943,14 +3178,14 @@ var init_upstreams = __esm({
|
|
|
2943
3178
|
init_auth();
|
|
2944
3179
|
init_auth();
|
|
2945
3180
|
UPSTREAMS_FILE = "upstreams.json";
|
|
2946
|
-
upstreamsPath = (catalog) =>
|
|
3181
|
+
upstreamsPath = (catalog) => resolve20(catalog.root, UPSTREAMS_FILE);
|
|
2947
3182
|
loadUpstreams = (catalog) => {
|
|
2948
3183
|
const path = upstreamsPath(catalog);
|
|
2949
|
-
if (!
|
|
2950
|
-
return JSON.parse(
|
|
3184
|
+
if (!existsSync14(path)) return {};
|
|
3185
|
+
return JSON.parse(readFileSync11(path, "utf8"));
|
|
2951
3186
|
};
|
|
2952
3187
|
writeUpstreams = (catalog, map) => {
|
|
2953
|
-
|
|
3188
|
+
writeFileSync8(upstreamsPath(catalog), JSON.stringify(map, null, 2) + "\n");
|
|
2954
3189
|
};
|
|
2955
3190
|
fetchLatestCommit = async (origin) => {
|
|
2956
3191
|
const url = `https://api.github.com/repos/${origin.repo}/commits?path=${encodeURIComponent(origin.path)}&sha=${encodeURIComponent(origin.ref)}&per_page=1`;
|
|
@@ -2989,7 +3224,7 @@ var init_upstreams = __esm({
|
|
|
2989
3224
|
};
|
|
2990
3225
|
fetchUpstreamDir = (origin) => {
|
|
2991
3226
|
const tmp = mkdtempSync2(join(tmpdir(), "quiver-pull-"));
|
|
2992
|
-
const cleanup = () =>
|
|
3227
|
+
const cleanup = () => rmSync7(tmp, { recursive: true, force: true });
|
|
2993
3228
|
const git = (args, cwd) => {
|
|
2994
3229
|
execFileSync3("git", args, {
|
|
2995
3230
|
cwd,
|
|
@@ -3015,8 +3250,8 @@ var init_upstreams = __esm({
|
|
|
3015
3250
|
const msg = err instanceof Error && "stderr" in err ? String(err.stderr).trim().split("\n").pop() : err instanceof Error ? err.message : "git clone failed";
|
|
3016
3251
|
return { ok: false, reason: msg || "git clone failed" };
|
|
3017
3252
|
}
|
|
3018
|
-
const dir =
|
|
3019
|
-
if (!
|
|
3253
|
+
const dir = resolve20(tmp, origin.path);
|
|
3254
|
+
if (!existsSync14(resolve20(dir, "SKILL.md"))) {
|
|
3020
3255
|
cleanup();
|
|
3021
3256
|
return { ok: false, reason: `no SKILL.md at ${origin.path} in ${origin.repo}` };
|
|
3022
3257
|
}
|
|
@@ -3064,7 +3299,7 @@ var upstream_exports = {};
|
|
|
3064
3299
|
__export(upstream_exports, {
|
|
3065
3300
|
upstream: () => upstream
|
|
3066
3301
|
});
|
|
3067
|
-
import { cpSync as cpSync3, rmSync as
|
|
3302
|
+
import { cpSync as cpSync3, rmSync as rmSync8 } from "fs";
|
|
3068
3303
|
var upstream, guardWritableCatalog, STATUS_ORDER, pull, report3, countByStatus;
|
|
3069
3304
|
var init_upstream = __esm({
|
|
3070
3305
|
"src/commands/upstream.ts"() {
|
|
@@ -3174,7 +3409,7 @@ var init_upstream = __esm({
|
|
|
3174
3409
|
continue;
|
|
3175
3410
|
}
|
|
3176
3411
|
try {
|
|
3177
|
-
|
|
3412
|
+
rmSync8(skill.absDir, { recursive: true, force: true });
|
|
3178
3413
|
cpSync3(fetched.dir, skill.absDir, { recursive: true, dereference: true });
|
|
3179
3414
|
} finally {
|
|
3180
3415
|
fetched.cleanup();
|
|
@@ -3309,9 +3544,9 @@ __export(notifier_exports, {
|
|
|
3309
3544
|
installHint: () => installHint,
|
|
3310
3545
|
notifierSuppressed: () => notifierSuppressed
|
|
3311
3546
|
});
|
|
3312
|
-
import { existsSync as
|
|
3313
|
-
import { homedir as
|
|
3314
|
-
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";
|
|
3315
3550
|
var REGISTRY_URL, CHECK_TTL_MS, FETCH_TIMEOUT_MS, INSTALL_HINT, cacheFilePath, installHint, getCurrentVersion, compareSemver, readCache, writeCache, fetchLatestVersion, checkForUpdate, notifierSuppressed;
|
|
3316
3551
|
var init_notifier = __esm({
|
|
3317
3552
|
"src/version/notifier.ts"() {
|
|
@@ -3322,14 +3557,14 @@ var init_notifier = __esm({
|
|
|
3322
3557
|
FETCH_TIMEOUT_MS = 2e3;
|
|
3323
3558
|
INSTALL_HINT = "pnpm add -g quiver-cli";
|
|
3324
3559
|
cacheFilePath = () => {
|
|
3325
|
-
const base = process.env["XDG_CACHE_HOME"] ||
|
|
3326
|
-
return
|
|
3560
|
+
const base = process.env["XDG_CACHE_HOME"] || resolve21(homedir3(), ".cache");
|
|
3561
|
+
return resolve21(base, "quiver", "update-check.json");
|
|
3327
3562
|
};
|
|
3328
3563
|
installHint = () => INSTALL_HINT;
|
|
3329
3564
|
getCurrentVersion = () => {
|
|
3330
3565
|
try {
|
|
3331
3566
|
const pkg = JSON.parse(
|
|
3332
|
-
|
|
3567
|
+
readFileSync12(resolve21(packageRoot, "package.json"), "utf8")
|
|
3333
3568
|
);
|
|
3334
3569
|
return pkg.version;
|
|
3335
3570
|
} catch {
|
|
@@ -3355,9 +3590,9 @@ var init_notifier = __esm({
|
|
|
3355
3590
|
};
|
|
3356
3591
|
readCache = () => {
|
|
3357
3592
|
const path = cacheFilePath();
|
|
3358
|
-
if (!
|
|
3593
|
+
if (!existsSync15(path)) return null;
|
|
3359
3594
|
try {
|
|
3360
|
-
return JSON.parse(
|
|
3595
|
+
return JSON.parse(readFileSync12(path, "utf8"));
|
|
3361
3596
|
} catch {
|
|
3362
3597
|
return null;
|
|
3363
3598
|
}
|
|
@@ -3366,7 +3601,7 @@ var init_notifier = __esm({
|
|
|
3366
3601
|
try {
|
|
3367
3602
|
const path = cacheFilePath();
|
|
3368
3603
|
mkdirSync6(dirname6(path), { recursive: true });
|
|
3369
|
-
|
|
3604
|
+
writeFileSync9(path, JSON.stringify(cache, null, 2) + "\n");
|
|
3370
3605
|
} catch {
|
|
3371
3606
|
}
|
|
3372
3607
|
};
|
|
@@ -3420,6 +3655,8 @@ Commands:
|
|
|
3420
3655
|
init Interactive picker over the catalog; write native configs + quiver.lock
|
|
3421
3656
|
add <id> Add an entry (skill:<name>, command:<name>, mcp:<name>, plugin:<name>)
|
|
3422
3657
|
remove <id> Remove a single entry; keep lockfile + configs consistent
|
|
3658
|
+
disable <id> Turn an MCP server off locally (mcp:<name>, gitignored override)
|
|
3659
|
+
enable <id> Turn a locally disabled MCP server back on
|
|
3423
3660
|
sync Regenerate provider configs from .agents/ (warns on drift)
|
|
3424
3661
|
providers [a,b] Change which tools get configs (claude, opencode, codex)
|
|
3425
3662
|
update [id] Pull newer catalog content into .agents/ (all or one entry)
|
|
@@ -3533,6 +3770,12 @@ var run = async () => {
|
|
|
3533
3770
|
await remove2(options);
|
|
3534
3771
|
break;
|
|
3535
3772
|
}
|
|
3773
|
+
case "enable":
|
|
3774
|
+
case "disable": {
|
|
3775
|
+
const { toggle: toggle2 } = await Promise.resolve().then(() => (init_toggle(), toggle_exports));
|
|
3776
|
+
await toggle2(options, command === "enable");
|
|
3777
|
+
break;
|
|
3778
|
+
}
|
|
3536
3779
|
case "sync": {
|
|
3537
3780
|
const { sync: sync2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
|
|
3538
3781
|
await sync2(options);
|
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": {
|