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.
Files changed (3) hide show
  1. package/README.md +26 -3
  2. package/dist/cli.js +336 -93
  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
@@ -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. Servers that fail introspection (e.g. requiring interactive
147
- OAuth) are reported as skipped. stdio servers run foreign code and are only
148
- introspected with `--introspect-stdio`.
163
+ diff against it. stdio servers run foreign code and are only introspected with
164
+ `--introspect-stdio`.
165
+
166
+ **OAuth-protected servers** (e.g. Linear): `check` reuses opencode's MCP
167
+ credentials (`~/.local/share/opencode/mcp-auth.json`, read-only — quiver never
168
+ refreshes or rewrites them). Authenticate once with
169
+ `opencode mcp auth <name>`, then re-run `quiver-cli check` to record the tool
170
+ snapshot. Without a valid token the server is skipped with an actionable hint,
171
+ and `quiver-cli list` shows why the tool count is missing.
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/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,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) missingTools = true;
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 requestInit = server.headers ? { headers: server.headers } : {};
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 resolve17 } from "path";
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 res = await introspect(server, { allowStdio: options.introspectStdio });
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
- mcpReports.push({ id, status: "skipped", reason: res.reason });
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 skipped = mcpReports.filter((r) => r.status === "skipped");
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(resolve17(dir, command + extension), constants2.X_OK);
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 existsSync13,
3166
+ existsSync as existsSync14,
2932
3167
  mkdtempSync as mkdtempSync2,
2933
- readFileSync as readFileSync9,
2934
- rmSync as rmSync6,
2935
- writeFileSync as writeFileSync7
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 resolve18 } from "path";
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) => resolve18(catalog.root, UPSTREAMS_FILE);
3181
+ upstreamsPath = (catalog) => resolve20(catalog.root, UPSTREAMS_FILE);
2947
3182
  loadUpstreams = (catalog) => {
2948
3183
  const path = upstreamsPath(catalog);
2949
- if (!existsSync13(path)) return {};
2950
- return JSON.parse(readFileSync9(path, "utf8"));
3184
+ if (!existsSync14(path)) return {};
3185
+ return JSON.parse(readFileSync11(path, "utf8"));
2951
3186
  };
2952
3187
  writeUpstreams = (catalog, map) => {
2953
- writeFileSync7(upstreamsPath(catalog), JSON.stringify(map, null, 2) + "\n");
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 = () => rmSync6(tmp, { recursive: true, force: true });
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 = resolve18(tmp, origin.path);
3019
- if (!existsSync13(resolve18(dir, "SKILL.md"))) {
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 rmSync7 } from "fs";
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
- rmSync7(skill.absDir, { recursive: true, force: true });
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 existsSync14, mkdirSync as mkdirSync6, readFileSync as readFileSync10, writeFileSync as writeFileSync8 } from "fs";
3313
- import { homedir as homedir2 } from "os";
3314
- import { dirname as dirname6, resolve as resolve19 } from "path";
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"] || resolve19(homedir2(), ".cache");
3326
- return resolve19(base, "quiver", "update-check.json");
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
- readFileSync10(resolve19(packageRoot, "package.json"), "utf8")
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 (!existsSync14(path)) return null;
3593
+ if (!existsSync15(path)) return null;
3359
3594
  try {
3360
- return JSON.parse(readFileSync10(path, "utf8"));
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
- writeFileSync8(path, JSON.stringify(cache, null, 2) + "\n");
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.0.0",
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": {