quiver-cli 1.0.0 → 1.1.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 +17 -0
  2. package/dist/cli.js +224 -82
  3. 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
package/dist/cli.js CHANGED
@@ -1199,9 +1199,47 @@ var init_fsops = __esm({
1199
1199
  }
1200
1200
  });
1201
1201
 
1202
- // src/providers/opencode.ts
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 = resolve11(targetRoot, "opencode.json");
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 = resolve11(targetRoot, ".opencode/tui.json");
1260
- const tuiMarkerPath = resolve11(targetRoot, ".opencode/.quiver-tui");
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 (existsSync8(tuiMarkerPath)) {
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: resolve11(targetRoot, ".opencode/skills", s.name),
1314
+ path: resolve12(targetRoot, ".opencode/skills", s.name),
1277
1315
  target: s.absDir
1278
1316
  })),
1279
1317
  ...selected.commands.map((c) => ({
1280
- path: resolve11(targetRoot, ".opencode/commands", `${c.name}.md`),
1318
+ path: resolve12(targetRoot, ".opencode/commands", `${c.name}.md`),
1281
1319
  target: c.absPath
1282
1320
  })),
1283
1321
  ...selected.plugins.map((plugin) => ({
1284
- path: resolve11(
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: resolve11(targetRoot, ".opencode/skills"),
1332
+ path: resolve12(targetRoot, ".opencode/skills"),
1295
1333
  expected: new Set(selected.skills.map((s) => s.name))
1296
1334
  },
1297
1335
  {
1298
- path: resolve11(targetRoot, ".opencode/commands"),
1336
+ path: resolve12(targetRoot, ".opencode/commands"),
1299
1337
  expected: new Set(selected.commands.map((c) => `${c.name}.md`))
1300
1338
  },
1301
1339
  {
1302
- path: resolve11(targetRoot, ".opencode/plugins"),
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 existsSync9, lstatSync as lstatSync2 } from "fs";
1346
- import { basename, relative as relative5, resolve as resolve12 } from "path";
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 = resolve12(targetRoot, ".agents");
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 = resolve12(agentsRoot, "AGENTS.md");
1405
- if (existsSync9(agentsMd)) {
1406
- const rootAgents = resolve12(targetRoot, "AGENTS.md");
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: resolve12(targetRoot, "CLAUDE.md"), target: rootAgents }
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 existsSync10, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
1472
- import { resolve as resolve13 } from "path";
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 = resolve13(targetRoot, ".gitignore");
1489
- const current = existsSync10(path) ? readFileSync6(path, "utf8") : "";
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
- if (missingShims.length === 0 && missingSecrets.length === 0) return false;
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
- writeFileSync4(
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 existsSync11 } from "fs";
1751
- import { resolve as resolve14 } from "path";
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) => resolve14(targetRoot, ".agents");
1758
- repoCatalogExists = (targetRoot) => existsSync11(repoCatalogRoot(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 existsSync12, readdirSync as readdirSync4, readFileSync as readFileSync7, rmSync as rmSync4, writeFileSync as writeFileSync5 } from "fs";
1873
- import { resolve as resolve15 } from "path";
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 = resolve15(targetRoot, ".agents/config.json");
1924
- if (!existsSync12(configPath)) return;
1925
- const config = JSON.parse(readFileSync7(configPath, "utf8"));
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
- writeFileSync5(configPath, JSON.stringify(config, null, 2) + "\n");
1996
+ writeFileSync6(configPath, JSON.stringify(config, null, 2) + "\n");
1935
1997
  };
1936
1998
  cleanupEmptyGroups = (targetRoot) => {
1937
- const skillsRoot = resolve15(targetRoot, ".agents/skills");
1938
- if (!existsSync12(skillsRoot)) return;
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 = resolve15(skillsRoot, entry.name);
1942
- if (existsSync12(resolve15(dir, "SKILL.md"))) continue;
1943
- if (readdirSync4(dir).length === 0) rmSync4(dir, { recursive: true, force: true });
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 = resolve15(targetRoot, ".agents/config.json");
1948
- if (!existsSync12(configPath)) return;
1949
- const config = JSON.parse(readFileSync7(configPath, "utf8"));
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
- writeFileSync5(configPath, JSON.stringify(config, null, 2) + "\n");
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 readFileSync8, rmSync as rmSync5, writeFileSync as writeFileSync6 } from "fs";
2178
- import { dirname as dirname5, resolve as resolve16 } from "path";
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 = resolve16(targetRoot, ".agents", src2.sourcePath);
2266
- rmSync5(dest, { recursive: true, force: true });
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 = resolve16(targetRoot, ".agents", src2.sourcePath);
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 = resolve16(targetRoot, ".agents", src2.sourcePath);
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 = resolve16(targetRoot, ".agents/config.json");
2303
- const config2 = JSON.parse(readFileSync8(configPath2, "utf8"));
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
- writeFileSync6(configPath2, JSON.stringify(config2, null, 2) + "\n");
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 = resolve16(targetRoot, ".agents", "config.json");
2323
- const config = JSON.parse(readFileSync8(configPath, "utf8"));
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
- writeFileSync6(configPath, JSON.stringify(config, null, 2) + "\n");
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,6 +2567,7 @@ 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
2572
  toolCount: entry.tools ? Object.keys(entry.tools).length : null
2446
2573
  })),
@@ -2500,8 +2627,9 @@ var init_list = __esm({
2500
2627
  count === null ? c.dim : c.green
2501
2628
  );
2502
2629
  const detail = serverDetail2.get(name);
2630
+ const off = disabled.has(name) ? ` ${c.yellow("disabled")}` : "";
2503
2631
  lines.push(
2504
- ` ${name.padEnd(nameW)} ${entry.transport.padEnd(5)} ${tools}` + (detail ? ` ${c.dim(detail)}` : "")
2632
+ ` ${name.padEnd(nameW)} ${entry.transport.padEnd(5)} ${tools}` + (detail ? ` ${c.dim(detail)}` : "") + off
2505
2633
  );
2506
2634
  }
2507
2635
  }
@@ -2666,7 +2794,7 @@ __export(check_exports, {
2666
2794
  summarize: () => summarize
2667
2795
  });
2668
2796
  import { accessSync as accessSync2, constants as constants2 } from "fs";
2669
- import { delimiter, resolve as resolve17 } from "path";
2797
+ import { delimiter, resolve as resolve18 } from "path";
2670
2798
  var check, report2, driftLines, list2, recommend, summarize, hasCommand, truncate2, fail;
2671
2799
  var init_check = __esm({
2672
2800
  "src/commands/check.ts"() {
@@ -2677,6 +2805,7 @@ var init_check = __esm({
2677
2805
  init_diff();
2678
2806
  init_introspect();
2679
2807
  init_snapshot();
2808
+ init_local_config();
2680
2809
  init_write();
2681
2810
  init_interpolate();
2682
2811
  init_prompts();
@@ -2724,6 +2853,7 @@ var init_check = __esm({
2724
2853
  }
2725
2854
  const shimProblems = checkProviders(options.targetRoot, catalog, lock);
2726
2855
  const mcpReports = [];
2856
+ const disabled = disabledMcpServers(options.targetRoot);
2727
2857
  let lockChanged = false;
2728
2858
  for (const [id, entry] of Object.entries(lock.entries)) {
2729
2859
  if (options.offline) break;
@@ -2731,6 +2861,10 @@ var init_check = __esm({
2731
2861
  const p = parseEntryId(id);
2732
2862
  const catMcp = catalog.mcp.find((m) => m.name === p.name);
2733
2863
  if (!catMcp) continue;
2864
+ if (disabled.has(p.name)) {
2865
+ mcpReports.push({ id, status: "skipped", reason: "disabled locally" });
2866
+ continue;
2867
+ }
2734
2868
  checked.mcp += 1;
2735
2869
  const server = interpolateEnvVars(catMcp.server);
2736
2870
  const res = await introspect(server, { allowStdio: options.introspectStdio });
@@ -2908,7 +3042,7 @@ var init_check = __esm({
2908
3042
  for (const dir of (process.env["PATH"] ?? "").split(delimiter)) {
2909
3043
  for (const extension of extensions) {
2910
3044
  try {
2911
- accessSync2(resolve17(dir, command + extension), constants2.X_OK);
3045
+ accessSync2(resolve18(dir, command + extension), constants2.X_OK);
2912
3046
  return true;
2913
3047
  } catch {
2914
3048
  }
@@ -2928,14 +3062,14 @@ var init_check = __esm({
2928
3062
  // src/catalog/upstreams.ts
2929
3063
  import { execFileSync as execFileSync3 } from "child_process";
2930
3064
  import {
2931
- existsSync as existsSync13,
3065
+ existsSync as existsSync14,
2932
3066
  mkdtempSync as mkdtempSync2,
2933
- readFileSync as readFileSync9,
2934
- rmSync as rmSync6,
2935
- writeFileSync as writeFileSync7
3067
+ readFileSync as readFileSync10,
3068
+ rmSync as rmSync7,
3069
+ writeFileSync as writeFileSync8
2936
3070
  } from "fs";
2937
3071
  import { tmpdir } from "os";
2938
- import { join, resolve as resolve18 } from "path";
3072
+ import { join, resolve as resolve19 } from "path";
2939
3073
  var UPSTREAMS_FILE, upstreamsPath, loadUpstreams, writeUpstreams, fetchLatestCommit, fetchUpstreamDir, short, evaluateOrigin;
2940
3074
  var init_upstreams = __esm({
2941
3075
  "src/catalog/upstreams.ts"() {
@@ -2943,14 +3077,14 @@ var init_upstreams = __esm({
2943
3077
  init_auth();
2944
3078
  init_auth();
2945
3079
  UPSTREAMS_FILE = "upstreams.json";
2946
- upstreamsPath = (catalog) => resolve18(catalog.root, UPSTREAMS_FILE);
3080
+ upstreamsPath = (catalog) => resolve19(catalog.root, UPSTREAMS_FILE);
2947
3081
  loadUpstreams = (catalog) => {
2948
3082
  const path = upstreamsPath(catalog);
2949
- if (!existsSync13(path)) return {};
2950
- return JSON.parse(readFileSync9(path, "utf8"));
3083
+ if (!existsSync14(path)) return {};
3084
+ return JSON.parse(readFileSync10(path, "utf8"));
2951
3085
  };
2952
3086
  writeUpstreams = (catalog, map) => {
2953
- writeFileSync7(upstreamsPath(catalog), JSON.stringify(map, null, 2) + "\n");
3087
+ writeFileSync8(upstreamsPath(catalog), JSON.stringify(map, null, 2) + "\n");
2954
3088
  };
2955
3089
  fetchLatestCommit = async (origin) => {
2956
3090
  const url = `https://api.github.com/repos/${origin.repo}/commits?path=${encodeURIComponent(origin.path)}&sha=${encodeURIComponent(origin.ref)}&per_page=1`;
@@ -2989,7 +3123,7 @@ var init_upstreams = __esm({
2989
3123
  };
2990
3124
  fetchUpstreamDir = (origin) => {
2991
3125
  const tmp = mkdtempSync2(join(tmpdir(), "quiver-pull-"));
2992
- const cleanup = () => rmSync6(tmp, { recursive: true, force: true });
3126
+ const cleanup = () => rmSync7(tmp, { recursive: true, force: true });
2993
3127
  const git = (args, cwd) => {
2994
3128
  execFileSync3("git", args, {
2995
3129
  cwd,
@@ -3015,8 +3149,8 @@ var init_upstreams = __esm({
3015
3149
  const msg = err instanceof Error && "stderr" in err ? String(err.stderr).trim().split("\n").pop() : err instanceof Error ? err.message : "git clone failed";
3016
3150
  return { ok: false, reason: msg || "git clone failed" };
3017
3151
  }
3018
- const dir = resolve18(tmp, origin.path);
3019
- if (!existsSync13(resolve18(dir, "SKILL.md"))) {
3152
+ const dir = resolve19(tmp, origin.path);
3153
+ if (!existsSync14(resolve19(dir, "SKILL.md"))) {
3020
3154
  cleanup();
3021
3155
  return { ok: false, reason: `no SKILL.md at ${origin.path} in ${origin.repo}` };
3022
3156
  }
@@ -3064,7 +3198,7 @@ var upstream_exports = {};
3064
3198
  __export(upstream_exports, {
3065
3199
  upstream: () => upstream
3066
3200
  });
3067
- import { cpSync as cpSync3, rmSync as rmSync7 } from "fs";
3201
+ import { cpSync as cpSync3, rmSync as rmSync8 } from "fs";
3068
3202
  var upstream, guardWritableCatalog, STATUS_ORDER, pull, report3, countByStatus;
3069
3203
  var init_upstream = __esm({
3070
3204
  "src/commands/upstream.ts"() {
@@ -3174,7 +3308,7 @@ var init_upstream = __esm({
3174
3308
  continue;
3175
3309
  }
3176
3310
  try {
3177
- rmSync7(skill.absDir, { recursive: true, force: true });
3311
+ rmSync8(skill.absDir, { recursive: true, force: true });
3178
3312
  cpSync3(fetched.dir, skill.absDir, { recursive: true, dereference: true });
3179
3313
  } finally {
3180
3314
  fetched.cleanup();
@@ -3309,9 +3443,9 @@ __export(notifier_exports, {
3309
3443
  installHint: () => installHint,
3310
3444
  notifierSuppressed: () => notifierSuppressed
3311
3445
  });
3312
- import { existsSync as existsSync14, mkdirSync as mkdirSync6, readFileSync as readFileSync10, writeFileSync as writeFileSync8 } from "fs";
3446
+ import { existsSync as existsSync15, mkdirSync as mkdirSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync9 } from "fs";
3313
3447
  import { homedir as homedir2 } from "os";
3314
- import { dirname as dirname6, resolve as resolve19 } from "path";
3448
+ import { dirname as dirname6, resolve as resolve20 } from "path";
3315
3449
  var REGISTRY_URL, CHECK_TTL_MS, FETCH_TIMEOUT_MS, INSTALL_HINT, cacheFilePath, installHint, getCurrentVersion, compareSemver, readCache, writeCache, fetchLatestVersion, checkForUpdate, notifierSuppressed;
3316
3450
  var init_notifier = __esm({
3317
3451
  "src/version/notifier.ts"() {
@@ -3322,14 +3456,14 @@ var init_notifier = __esm({
3322
3456
  FETCH_TIMEOUT_MS = 2e3;
3323
3457
  INSTALL_HINT = "pnpm add -g quiver-cli";
3324
3458
  cacheFilePath = () => {
3325
- const base = process.env["XDG_CACHE_HOME"] || resolve19(homedir2(), ".cache");
3326
- return resolve19(base, "quiver", "update-check.json");
3459
+ const base = process.env["XDG_CACHE_HOME"] || resolve20(homedir2(), ".cache");
3460
+ return resolve20(base, "quiver", "update-check.json");
3327
3461
  };
3328
3462
  installHint = () => INSTALL_HINT;
3329
3463
  getCurrentVersion = () => {
3330
3464
  try {
3331
3465
  const pkg = JSON.parse(
3332
- readFileSync10(resolve19(packageRoot, "package.json"), "utf8")
3466
+ readFileSync11(resolve20(packageRoot, "package.json"), "utf8")
3333
3467
  );
3334
3468
  return pkg.version;
3335
3469
  } catch {
@@ -3355,9 +3489,9 @@ var init_notifier = __esm({
3355
3489
  };
3356
3490
  readCache = () => {
3357
3491
  const path = cacheFilePath();
3358
- if (!existsSync14(path)) return null;
3492
+ if (!existsSync15(path)) return null;
3359
3493
  try {
3360
- return JSON.parse(readFileSync10(path, "utf8"));
3494
+ return JSON.parse(readFileSync11(path, "utf8"));
3361
3495
  } catch {
3362
3496
  return null;
3363
3497
  }
@@ -3366,7 +3500,7 @@ var init_notifier = __esm({
3366
3500
  try {
3367
3501
  const path = cacheFilePath();
3368
3502
  mkdirSync6(dirname6(path), { recursive: true });
3369
- writeFileSync8(path, JSON.stringify(cache, null, 2) + "\n");
3503
+ writeFileSync9(path, JSON.stringify(cache, null, 2) + "\n");
3370
3504
  } catch {
3371
3505
  }
3372
3506
  };
@@ -3420,6 +3554,8 @@ Commands:
3420
3554
  init Interactive picker over the catalog; write native configs + quiver.lock
3421
3555
  add <id> Add an entry (skill:<name>, command:<name>, mcp:<name>, plugin:<name>)
3422
3556
  remove <id> Remove a single entry; keep lockfile + configs consistent
3557
+ disable <id> Turn an MCP server off locally (mcp:<name>, gitignored override)
3558
+ enable <id> Turn a locally disabled MCP server back on
3423
3559
  sync Regenerate provider configs from .agents/ (warns on drift)
3424
3560
  providers [a,b] Change which tools get configs (claude, opencode, codex)
3425
3561
  update [id] Pull newer catalog content into .agents/ (all or one entry)
@@ -3533,6 +3669,12 @@ var run = async () => {
3533
3669
  await remove2(options);
3534
3670
  break;
3535
3671
  }
3672
+ case "enable":
3673
+ case "disable": {
3674
+ const { toggle: toggle2 } = await Promise.resolve().then(() => (init_toggle(), toggle_exports));
3675
+ await toggle2(options, command === "enable");
3676
+ break;
3677
+ }
3536
3678
  case "sync": {
3537
3679
  const { sync: sync2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
3538
3680
  await sync2(options);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quiver-cli",
3
- "version": "1.0.0",
3
+ "version": "1.1.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": {