@yawlabs/mcp 0.64.1 → 0.65.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 +100 -0
  2. package/dist/index.js +654 -106
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1148,9 +1148,15 @@ async function runBundlesCommand(opts = {}) {
1148
1148
  env: opts.env
1149
1149
  });
1150
1150
  if (!config.token) {
1151
- printErr(
1152
- "yaw-mcp bundles match: no token resolved. Run `yaw-mcp install <client> --token mcp_pat_\u2026` or set YAW_MCP_TOKEN."
1153
- );
1151
+ const msg = "no token resolved. Run `yaw-mcp install <client> --token mcp_pat_\u2026` or set YAW_MCP_TOKEN.";
1152
+ if (opts.json) {
1153
+ const jsonErr = JSON.stringify({ ok: false, error: msg });
1154
+ lines.push(jsonErr);
1155
+ writeErr(`${jsonErr}
1156
+ `);
1157
+ } else {
1158
+ printErr(`yaw-mcp bundles match: ${msg}`);
1159
+ }
1154
1160
  return { exitCode: 1, lines };
1155
1161
  }
1156
1162
  const fetcher = opts.fetcher ?? fetchConfig;
@@ -1159,11 +1165,26 @@ async function runBundlesCommand(opts = {}) {
1159
1165
  backend = await fetcher(config.apiBase, config.token);
1160
1166
  } catch (err) {
1161
1167
  const msg = err instanceof ConfigError || err instanceof Error ? err.message : String(err);
1162
- printErr(`yaw-mcp bundles match: ${msg}`);
1168
+ if (opts.json) {
1169
+ const jsonErr = JSON.stringify({ ok: false, error: msg });
1170
+ lines.push(jsonErr);
1171
+ writeErr(`${jsonErr}
1172
+ `);
1173
+ } else {
1174
+ printErr(`yaw-mcp bundles match: ${msg}`);
1175
+ }
1163
1176
  return { exitCode: 2, lines };
1164
1177
  }
1165
1178
  if (!backend) {
1166
- printErr("yaw-mcp bundles match: backend returned 304 without a conditional request.");
1179
+ const msg = "backend returned 304 without a conditional request.";
1180
+ if (opts.json) {
1181
+ const jsonErr = JSON.stringify({ ok: false, error: msg });
1182
+ lines.push(jsonErr);
1183
+ writeErr(`${jsonErr}
1184
+ `);
1185
+ } else {
1186
+ printErr(`yaw-mcp bundles match: ${msg}`);
1187
+ }
1167
1188
  return { exitCode: 2, lines };
1168
1189
  }
1169
1190
  const installed = backend.servers.filter((s) => s.isActive).map((s) => s.namespace);
@@ -1380,18 +1401,27 @@ function renderScript(shell) {
1380
1401
  return renderPowershell();
1381
1402
  }
1382
1403
  }
1404
+ function isPlaceholder(s) {
1405
+ return s.startsWith("<") && s.endsWith(">");
1406
+ }
1383
1407
  function renderBash() {
1384
1408
  const subcommandList = SUBCOMMAND_SPEC.map((s) => s.name).join(" ");
1385
1409
  const topLevelFlags = "--help -h --version -V";
1386
1410
  const cases = SUBCOMMAND_SPEC.map((spec) => {
1387
- const posClause = spec.positional ? ` if [[ $cword -eq 2 ]]; then
1388
- COMPREPLY=( $(compgen -W "${spec.positional.join(" ")} ${spec.flags.join(" ")}" -- "$cur") )
1411
+ const indexedPositionals = (spec.positional ?? []).map((p, i) => ({ value: p, index: i })).filter(({ value }) => !isPlaceholder(value));
1412
+ const posClauses = indexedPositionals.map(
1413
+ ({ value, index }) => ` if [[ $cword -eq $((${index} + 2)) ]]; then
1414
+ COMPREPLY=( $(compgen -W "${value}" -- "$cur") )
1389
1415
  return 0
1390
- fi` : "";
1416
+ fi`
1417
+ );
1418
+ const parts = [
1419
+ ...posClauses,
1420
+ ` COMPREPLY=( $(compgen -W "${spec.flags.join(" ")}" -- "$cur") )`,
1421
+ " return 0"
1422
+ ].filter((p) => p !== "");
1391
1423
  return ` ${spec.name})
1392
- ${posClause}
1393
- COMPREPLY=( $(compgen -W "${spec.flags.join(" ")}" -- "$cur") )
1394
- return 0
1424
+ ${parts.join("\n")}
1395
1425
  ;;`;
1396
1426
  }).join("\n");
1397
1427
  return `# bash completion for yaw-mcp \u2014 generated by \`yaw-mcp completion bash\`
@@ -1418,8 +1448,10 @@ function renderZsh() {
1418
1448
  const subcommandList = SUBCOMMAND_SPEC.map((s) => ` '${s.name}:${s.description}'`).join("\n");
1419
1449
  const argsCases = SUBCOMMAND_SPEC.map((spec) => {
1420
1450
  const lines = [` ${spec.name})`];
1421
- if (spec.positional) {
1422
- lines.push(` _arguments '1: :(${spec.positional.join(" ")})' '*: :(${spec.flags.join(" ")})'`);
1451
+ const indexedPositionals = (spec.positional ?? []).map((p, i) => ({ value: p, index: i })).filter(({ value }) => !isPlaceholder(value));
1452
+ if (indexedPositionals.length > 0) {
1453
+ const posArgs = indexedPositionals.map(({ value, index }) => `'${index + 1}: :(${value})'`).join(" ");
1454
+ lines.push(` _arguments ${posArgs} '*: :(${spec.flags.join(" ")})'`);
1423
1455
  } else {
1424
1456
  lines.push(` _arguments '*: :(${spec.flags.join(" ")})'`);
1425
1457
  }
@@ -1463,9 +1495,13 @@ complete -c yaw-mcp -f`;
1463
1495
  const flagLines = [];
1464
1496
  for (const spec of SUBCOMMAND_SPEC) {
1465
1497
  if (spec.positional) {
1466
- for (const p of spec.positional) {
1467
- positionalLines.push(`complete -c yaw-mcp -n "__fish_seen_subcommand_from ${spec.name}" -a ${p}`);
1468
- }
1498
+ spec.positional.forEach((p, i) => {
1499
+ if (isPlaceholder(p)) return;
1500
+ const expectedCount = i + 2;
1501
+ positionalLines.push(
1502
+ `complete -c yaw-mcp -n "__fish_seen_subcommand_from ${spec.name}; and test (count (commandline -opc)) -eq ${expectedCount}" -a ${p}`
1503
+ );
1504
+ });
1469
1505
  }
1470
1506
  for (const f of spec.flags) {
1471
1507
  if (!f.startsWith("--")) continue;
@@ -1478,12 +1514,13 @@ complete -c yaw-mcp -f`;
1478
1514
  function renderPowershell() {
1479
1515
  const subcommandNames = SUBCOMMAND_SPEC.map((s) => `'${s.name}'`).join(", ");
1480
1516
  const caseBranches = SUBCOMMAND_SPEC.map((spec) => {
1481
- const positional = spec.positional ? spec.positional.map((p) => `'${p}'`).join(", ") : "";
1517
+ const indexedPositionals = (spec.positional ?? []).map((p, i) => ({ value: p, index: i })).filter(({ value }) => !isPlaceholder(value));
1482
1518
  const flags = spec.flags.map((f) => `'${f}'`).join(", ");
1483
- const positionalLine = positional ? ` $completions += @(${positional})
1519
+ const positionalLines = indexedPositionals.map(({ value, index }) => ` if ($tokens.Count -eq ${index + 2}) { $completions += @('${value}') }`).join("\n");
1520
+ const positionalBlock = positionalLines ? `${positionalLines}
1484
1521
  ` : "";
1485
1522
  return ` '${spec.name}' {
1486
- ${positionalLine} $completions += @(${flags})
1523
+ ${positionalBlock} $completions += @(${flags})
1487
1524
  }`;
1488
1525
  }).join("\n");
1489
1526
  return `# PowerShell completion for yaw-mcp \u2014 generated by \`yaw-mcp completion powershell\`
@@ -1694,6 +1731,19 @@ var token = "";
1694
1731
  var lastFailure = null;
1695
1732
  var lastLoggedConnectStatus = null;
1696
1733
  var lastLoggedDispatchStatus = null;
1734
+ var warnedInsecureBearerSkipConnect = false;
1735
+ var warnedInsecureBearerSkipDispatch = false;
1736
+ function shouldSendBearer(targetUrl) {
1737
+ let parsed;
1738
+ try {
1739
+ parsed = new URL(targetUrl);
1740
+ } catch {
1741
+ return false;
1742
+ }
1743
+ if (parsed.protocol === "https:") return true;
1744
+ if (parsed.protocol === "http:" && isLoopbackHost(parsed.hostname)) return true;
1745
+ return false;
1746
+ }
1697
1747
  function getLastAnalyticsFailure() {
1698
1748
  return lastFailure;
1699
1749
  }
@@ -1746,12 +1796,20 @@ async function flush() {
1746
1796
  const events = buffer.splice(0, FLUSH_SIZE);
1747
1797
  const url = `${apiUrl.replace(/\/$/, "")}/api/connect/analytics`;
1748
1798
  try {
1799
+ const headers = { "Content-Type": "application/json" };
1800
+ if (shouldSendBearer(url)) {
1801
+ headers.Authorization = `Bearer ${token}`;
1802
+ } else if (!warnedInsecureBearerSkipConnect) {
1803
+ log(
1804
+ "warn",
1805
+ "Analytics URL is not https and not loopback; sending without Authorization header to avoid leaking the bearer token",
1806
+ { url }
1807
+ );
1808
+ warnedInsecureBearerSkipConnect = true;
1809
+ }
1749
1810
  const res = await request3(url, {
1750
1811
  method: "POST",
1751
- headers: {
1752
- Authorization: `Bearer ${token}`,
1753
- "Content-Type": "application/json"
1754
- },
1812
+ headers,
1755
1813
  body: JSON.stringify({ events }),
1756
1814
  headersTimeout: 1e4,
1757
1815
  bodyTimeout: 1e4
@@ -1788,17 +1846,25 @@ async function flushDispatch() {
1788
1846
  const events = dispatchBuffer.splice(0, FLUSH_SIZE);
1789
1847
  const url = `${apiUrl.replace(/\/$/, "")}/api/connect/dispatch-events`;
1790
1848
  try {
1849
+ const headers = { "Content-Type": "application/json" };
1850
+ if (shouldSendBearer(url)) {
1851
+ headers.Authorization = `Bearer ${token}`;
1852
+ } else if (!warnedInsecureBearerSkipDispatch) {
1853
+ log(
1854
+ "warn",
1855
+ "Analytics URL is not https and not loopback; sending without Authorization header to avoid leaking the bearer token",
1856
+ { url }
1857
+ );
1858
+ warnedInsecureBearerSkipDispatch = true;
1859
+ }
1791
1860
  const res = await request3(url, {
1792
1861
  method: "POST",
1793
- headers: {
1794
- Authorization: `Bearer ${token}`,
1795
- "Content-Type": "application/json"
1796
- },
1862
+ headers,
1797
1863
  body: JSON.stringify({ events }),
1798
1864
  headersTimeout: 1e4,
1799
1865
  bodyTimeout: 1e4
1800
1866
  });
1801
- if (res.statusCode >= 400 && res.statusCode !== 204) {
1867
+ if (res.statusCode >= 400) {
1802
1868
  const retryable = res.statusCode >= 500 || res.statusCode === 408 || res.statusCode === 429;
1803
1869
  if (retryable) {
1804
1870
  const room = MAX_BUFFER - dispatchBuffer.length;
@@ -1812,7 +1878,7 @@ async function flushDispatch() {
1812
1878
  lastLoggedDispatchStatus = res.statusCode;
1813
1879
  }
1814
1880
  lastFailure = { statusCode: res.statusCode, url, at: Date.now() };
1815
- } else if (res.statusCode < 400) {
1881
+ } else {
1816
1882
  lastFailure = null;
1817
1883
  lastLoggedDispatchStatus = null;
1818
1884
  }
@@ -1830,6 +1896,8 @@ function initAnalytics(url, tok) {
1830
1896
  token = tok;
1831
1897
  lastLoggedConnectStatus = null;
1832
1898
  lastLoggedDispatchStatus = null;
1899
+ warnedInsecureBearerSkipConnect = false;
1900
+ warnedInsecureBearerSkipDispatch = false;
1833
1901
  teamAnalyticsDisabled = false;
1834
1902
  flushTimer = setInterval(() => {
1835
1903
  flush().catch(() => {
@@ -2296,9 +2364,12 @@ function errorMessage(err) {
2296
2364
  import { request as request4 } from "undici";
2297
2365
  var apiUrl2 = "";
2298
2366
  var token2 = "";
2299
- var lastFailure2 = null;
2300
- function getLastReportFailure() {
2301
- return lastFailure2;
2367
+ var lastFailureByServer = /* @__PURE__ */ new Map();
2368
+ function getLastReportFailure(serverId) {
2369
+ if (serverId !== void 0) return lastFailureByServer.get(serverId) ?? null;
2370
+ const entries = [...lastFailureByServer.values()];
2371
+ if (entries.length === 0) return null;
2372
+ return entries.reduce((a, b) => a.at >= b.at ? a : b);
2302
2373
  }
2303
2374
  function initToolReport(url, tok) {
2304
2375
  apiUrl2 = url;
@@ -2306,7 +2377,7 @@ function initToolReport(url, tok) {
2306
2377
  }
2307
2378
  async function reportTools(serverId, tools) {
2308
2379
  if (!apiUrl2 || !token2 || !serverId) return;
2309
- const url = `${apiUrl2.replace(/\/$/, "")}/api/connect/servers/${serverId}/tools`;
2380
+ const url = `${apiUrl2.replace(/\/$/, "")}/api/connect/servers/${encodeURIComponent(serverId)}/tools`;
2310
2381
  try {
2311
2382
  const res = await request4(url, {
2312
2383
  method: "POST",
@@ -2322,12 +2393,13 @@ async function reportTools(serverId, tools) {
2322
2393
  });
2323
2394
  if (res.statusCode >= 400 && res.statusCode !== 404) {
2324
2395
  log("warn", "Tool report failed", { serverId, status: res.statusCode });
2325
- lastFailure2 = { statusCode: res.statusCode, url, at: Date.now() };
2326
- } else if (res.statusCode < 400) {
2327
- lastFailure2 = null;
2396
+ lastFailureByServer.set(serverId, { statusCode: res.statusCode, url, at: Date.now() });
2397
+ } else {
2398
+ lastFailureByServer.delete(serverId);
2328
2399
  }
2329
2400
  } catch (err) {
2330
2401
  log("warn", "Tool report error", { serverId, error: err?.message });
2402
+ lastFailureByServer.set(serverId, { statusCode: 0, url, at: Date.now() });
2331
2403
  }
2332
2404
  }
2333
2405
 
@@ -2367,6 +2439,9 @@ function tokenizeCommand(cmd) {
2367
2439
  has = true;
2368
2440
  }
2369
2441
  }
2442
+ if (quote !== null) {
2443
+ throw new Error(`Unbalanced quote in command: ${cmd}`);
2444
+ }
2370
2445
  if (has) out.push(cur);
2371
2446
  return out;
2372
2447
  }
@@ -3912,7 +3987,7 @@ async function runUpgrade(opts = {}) {
3912
3987
  return { exitCode: 3, lines };
3913
3988
  }
3914
3989
  function readCurrentVersion() {
3915
- return true ? "0.64.1" : "dev";
3990
+ return true ? "0.65.0" : "dev";
3916
3991
  }
3917
3992
 
3918
3993
  // src/usage-hints.ts
@@ -3974,7 +4049,7 @@ function selectFlakyNamespaces(entries, limit) {
3974
4049
  }
3975
4050
 
3976
4051
  // src/doctor-cmd.ts
3977
- var VERSION = true ? "0.64.1" : "dev";
4052
+ var VERSION = true ? "0.65.0" : "dev";
3978
4053
  function isPersistenceDisabled(env) {
3979
4054
  const raw = env.YAW_MCP_DISABLE_PERSISTENCE;
3980
4055
  return raw !== void 0 && raw !== "" && (raw === "1" || raw.toLowerCase() === "true");
@@ -5588,13 +5663,85 @@ function isFileNotFound2(err) {
5588
5663
  }
5589
5664
 
5590
5665
  // src/secrets-cmd.ts
5666
+ import { existsSync as existsSync6 } from "fs";
5667
+ import { homedir as homedir15 } from "os";
5668
+
5669
+ // src/secrets-audit.ts
5591
5670
  import { existsSync as existsSync5 } from "fs";
5592
- import { homedir as homedir14 } from "os";
5671
+ import { appendFile as appendFile2, chmod as chmod4, readFile as readFile9, writeFile } from "fs/promises";
5672
+ import { homedir as homedir13 } from "os";
5673
+ import { join as join10 } from "path";
5674
+ var SECRETS_AUDIT_FILENAME = "secrets-audit.log";
5675
+ var AUDIT_TAIL_CAP = 5e3;
5676
+ function auditLogPath(home = homedir13()) {
5677
+ return join10(home, CONFIG_DIRNAME, SECRETS_AUDIT_FILENAME);
5678
+ }
5679
+ async function appendAuditEvent(input, home = homedir13()) {
5680
+ try {
5681
+ const event = {
5682
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
5683
+ server: input.server,
5684
+ secret: input.secret,
5685
+ event: input.event
5686
+ };
5687
+ const path5 = auditLogPath(home);
5688
+ const line = `${JSON.stringify(event)}
5689
+ `;
5690
+ if (!existsSync5(path5)) {
5691
+ await atomicWriteFile(path5, line);
5692
+ } else {
5693
+ await appendFile2(path5, line, "utf8");
5694
+ }
5695
+ if (process.platform !== "win32") {
5696
+ await chmod4(path5, 384).catch(() => void 0);
5697
+ }
5698
+ await trimToTailCap(path5);
5699
+ } catch {
5700
+ }
5701
+ }
5702
+ async function trimToTailCap(path5) {
5703
+ const raw = await readFile9(path5, "utf8");
5704
+ const lines = raw.split("\n").filter((l) => l.length > 0);
5705
+ if (lines.length <= AUDIT_TAIL_CAP) return;
5706
+ const kept = lines.slice(lines.length - AUDIT_TAIL_CAP);
5707
+ await writeFile(path5, `${kept.join("\n")}
5708
+ `, "utf8");
5709
+ }
5710
+ async function readAuditLog(filter = {}, home = homedir13()) {
5711
+ const path5 = auditLogPath(home);
5712
+ if (!existsSync5(path5)) return [];
5713
+ let raw;
5714
+ try {
5715
+ raw = await readFile9(path5, "utf8");
5716
+ } catch {
5717
+ return [];
5718
+ }
5719
+ const out = [];
5720
+ for (const line of raw.split("\n")) {
5721
+ if (line.length === 0) continue;
5722
+ let parsed;
5723
+ try {
5724
+ parsed = JSON.parse(line);
5725
+ } catch {
5726
+ continue;
5727
+ }
5728
+ if (!isAuditEvent(parsed)) continue;
5729
+ if (filter.secret !== void 0 && parsed.secret !== filter.secret) continue;
5730
+ if (filter.server !== void 0 && parsed.server !== filter.server) continue;
5731
+ out.push(parsed);
5732
+ }
5733
+ return out;
5734
+ }
5735
+ function isAuditEvent(v) {
5736
+ if (!v || typeof v !== "object") return false;
5737
+ const e = v;
5738
+ return typeof e.ts === "string" && typeof e.server === "string" && typeof e.secret === "string" && (e.event === "injected" || e.event === "missing");
5739
+ }
5593
5740
 
5594
5741
  // src/secrets-vault.ts
5595
- import { chmod as chmod4, mkdir as mkdir4, readFile as readFile9 } from "fs/promises";
5596
- import { homedir as homedir13 } from "os";
5597
- import { dirname as dirname2, join as join10 } from "path";
5742
+ import { chmod as chmod5, mkdir as mkdir4, readFile as readFile10 } from "fs/promises";
5743
+ import { homedir as homedir14 } from "os";
5744
+ import { dirname as dirname2, join as join11 } from "path";
5598
5745
 
5599
5746
  // src/secrets-crypto.ts
5600
5747
  import { createCipheriv, createDecipheriv, randomBytes, scrypt as scryptCb } from "crypto";
@@ -5653,8 +5800,8 @@ function decryptEntry(entry, key) {
5653
5800
  var SECRETS_FILENAME = "secrets.json";
5654
5801
  var SECRETS_SCHEMA_VERSION = 1;
5655
5802
  var VAULT_CHECK_PLAINTEXT = "yaw-mcp-vault-v1";
5656
- function vaultPath(home = homedir13()) {
5657
- return join10(home, CONFIG_DIRNAME, SECRETS_FILENAME);
5803
+ function vaultPath(home = homedir14()) {
5804
+ return join11(home, CONFIG_DIRNAME, SECRETS_FILENAME);
5658
5805
  }
5659
5806
  function emptyVault() {
5660
5807
  return {
@@ -5666,7 +5813,7 @@ function emptyVault() {
5666
5813
  async function loadVault(path5) {
5667
5814
  let raw;
5668
5815
  try {
5669
- raw = await readFile9(path5, "utf8");
5816
+ raw = await readFile10(path5, "utf8");
5670
5817
  } catch (err) {
5671
5818
  const code = err.code;
5672
5819
  if (code === "ENOENT") return null;
@@ -5711,7 +5858,7 @@ async function saveVault(path5, vault) {
5711
5858
  await mkdir4(dir, { recursive: true });
5712
5859
  if (process.platform !== "win32") {
5713
5860
  try {
5714
- await chmod4(dir, 448);
5861
+ await chmod5(dir, 448);
5715
5862
  } catch {
5716
5863
  }
5717
5864
  }
@@ -5719,7 +5866,7 @@ async function saveVault(path5, vault) {
5719
5866
  `, "utf8", 384, 448);
5720
5867
  if (process.platform !== "win32") {
5721
5868
  try {
5722
- await chmod4(path5, 384);
5869
+ await chmod5(path5, 384);
5723
5870
  } catch {
5724
5871
  }
5725
5872
  }
@@ -5753,6 +5900,44 @@ function ensureCheck(vault, key) {
5753
5900
  if (vault.check) return vault;
5754
5901
  return { ...vault, check: encryptEntry(VAULT_CHECK_PLAINTEXT, key) };
5755
5902
  }
5903
+ async function rotateVault(vault, oldKey, newPassphrase) {
5904
+ if (vault.check) {
5905
+ try {
5906
+ const probe2 = decryptEntry(vault.check, oldKey);
5907
+ if (probe2 !== VAULT_CHECK_PLAINTEXT) {
5908
+ throw new Error("vault check marker did not match expected plaintext");
5909
+ }
5910
+ } catch {
5911
+ throw new Error("rotate aborted: current passphrase is wrong (vault check failed to decrypt)");
5912
+ }
5913
+ }
5914
+ const plaintext = /* @__PURE__ */ new Map();
5915
+ for (const [name, entry] of Object.entries(vault.entries)) {
5916
+ try {
5917
+ plaintext.set(name, decryptEntry(entry, oldKey));
5918
+ } catch {
5919
+ plaintext.clear();
5920
+ throw new Error(`rotate aborted: entry "${name}" failed to decrypt under the current passphrase`);
5921
+ }
5922
+ }
5923
+ const newSalt = generateSalt();
5924
+ const newKey = await deriveKey(newPassphrase, newSalt);
5925
+ try {
5926
+ const entries = {};
5927
+ for (const [name, value] of plaintext) {
5928
+ entries[name] = encryptEntry(value, newKey);
5929
+ }
5930
+ return {
5931
+ version: SECRETS_SCHEMA_VERSION,
5932
+ salt: newSalt.toString("base64"),
5933
+ entries,
5934
+ check: encryptEntry(VAULT_CHECK_PLAINTEXT, newKey)
5935
+ };
5936
+ } finally {
5937
+ plaintext.clear();
5938
+ newKey.fill(0);
5939
+ }
5940
+ }
5756
5941
  function listKeys(vault) {
5757
5942
  return Object.keys(vault.entries).sort();
5758
5943
  }
@@ -5844,6 +6029,19 @@ Actions:
5844
6029
  \`yaw-mcp login\` first. Refuses when the local
5845
6030
  vault has a different salt (different passphrase
5846
6031
  lineage) unless --force is passed.
6032
+ rotate Re-encrypt every entry under a NEW passphrase
6033
+ (fresh salt + derived key). Re-wraps the
6034
+ ENCRYPTION, NOT the underlying token values -- a
6035
+ leaked token is still leaked; rotate it at its
6036
+ source. Reads the current passphrase, then the
6037
+ new one (env YAW_MCP_VAULT_PASSPHRASE_NEW or a
6038
+ confirm-twice TTY prompt). Pass --push to also
6039
+ upload the re-encrypted blob to mcp_secrets.
6040
+ audit [--secret NAME] [--server NS]
6041
+ Show the local secret-resolution audit trail
6042
+ (~/.yaw-mcp/secrets-audit.log): which secret
6043
+ NAMES were injected into (or missing for) which
6044
+ server, and when. Never shows a value.
5847
6045
 
5848
6046
  Flags:
5849
6047
  --json Machine-readable output (where applicable).
@@ -5855,12 +6053,18 @@ Flags:
5855
6053
  --replace (push only) Overwrite even when the remote vault
5856
6054
  salt differs from the local (different passphrase
5857
6055
  lineage). Coordinate with your team first.
6056
+ --push (rotate only) After re-encrypting, push the new
6057
+ blob to mcp_secrets (requires a login session).
6058
+ --secret <name> (audit only) Filter to one secret name.
6059
+ --server <ns> (audit only) Filter to one server namespace.
5858
6060
 
5859
6061
  Passphrase:
5860
6062
  Set YAW_MCP_VAULT_PASSPHRASE in the env, or you will be prompted on
5861
6063
  the controlling TTY. The passphrase derives the encryption key via
5862
6064
  scrypt and is cached in memory for the lifetime of this yaw-mcp
5863
- process; the on-disk vault only ever holds ciphertext.`;
6065
+ process; the on-disk vault only ever holds ciphertext. For rotate, the
6066
+ NEW passphrase comes from YAW_MCP_VAULT_PASSPHRASE_NEW (or a TTY
6067
+ confirm-twice prompt).`;
5864
6068
  function parseSecretsArgs(argv) {
5865
6069
  const opts = {};
5866
6070
  for (let i = 0; i < argv.length; i++) {
@@ -5882,6 +6086,10 @@ function parseSecretsArgs(argv) {
5882
6086
  opts.replace = true;
5883
6087
  continue;
5884
6088
  }
6089
+ if (a === "--push") {
6090
+ opts.push = true;
6091
+ continue;
6092
+ }
5885
6093
  if (a === "--value") {
5886
6094
  const v = argv[++i];
5887
6095
  if (v === void 0 || v.startsWith("-")) {
@@ -5895,13 +6103,31 @@ ${SECRETS_USAGE}`
5895
6103
  opts.value = v;
5896
6104
  continue;
5897
6105
  }
6106
+ if (a === "--secret") {
6107
+ const v = argv[++i];
6108
+ if (v === void 0)
6109
+ return { ok: false, error: `yaw-mcp secrets: --secret requires a value
6110
+
6111
+ ${SECRETS_USAGE}` };
6112
+ opts.secretFilter = v;
6113
+ continue;
6114
+ }
6115
+ if (a === "--server") {
6116
+ const v = argv[++i];
6117
+ if (v === void 0)
6118
+ return { ok: false, error: `yaw-mcp secrets: --server requires a value
6119
+
6120
+ ${SECRETS_USAGE}` };
6121
+ opts.serverFilter = v;
6122
+ continue;
6123
+ }
5898
6124
  if (a.startsWith("-")) {
5899
6125
  return { ok: false, error: `yaw-mcp secrets: unknown flag "${a}"
5900
6126
 
5901
6127
  ${SECRETS_USAGE}` };
5902
6128
  }
5903
6129
  if (!opts.action) {
5904
- if (a !== "set" && a !== "get" && a !== "list" && a !== "remove" && a !== "lock" && a !== "push" && a !== "pull") {
6130
+ if (a !== "set" && a !== "get" && a !== "list" && a !== "remove" && a !== "lock" && a !== "push" && a !== "pull" && a !== "rotate" && a !== "audit") {
5905
6131
  return { ok: false, error: `yaw-mcp secrets: unknown action "${a}"
5906
6132
 
5907
6133
  ${SECRETS_USAGE}` };
@@ -5965,6 +6191,35 @@ async function resolvePassphrase(opts) {
5965
6191
  }
5966
6192
  return null;
5967
6193
  }
6194
+ async function resolveNewPassphrase(opts) {
6195
+ if (opts.newPassphrase !== void 0) return opts.newPassphrase.length > 0 ? opts.newPassphrase : null;
6196
+ const fromEnv = process.env.YAW_MCP_VAULT_PASSPHRASE_NEW;
6197
+ if (typeof fromEnv === "string" && fromEnv.length > 0) {
6198
+ if (fromEnv.length < MIN_PASSPHRASE_WARN_LEN) {
6199
+ const stderr = opts.io?.stderr ?? process.stderr;
6200
+ stderr.write(
6201
+ `yaw-mcp secrets: warning -- the new passphrase is shorter than ${MIN_PASSPHRASE_WARN_LEN} characters; consider a longer passphrase.
6202
+ `
6203
+ );
6204
+ }
6205
+ return fromEnv;
6206
+ }
6207
+ const stdin = opts.io?.stdin ?? process.stdin;
6208
+ const stdout = opts.io?.stdout ?? process.stdout;
6209
+ const isTTY = stdin.isTTY === true && stdout.isTTY === true;
6210
+ if (!isTTY) return null;
6211
+ for (let attempt = 0; attempt < MAX_PASSPHRASE_PROMPTS; attempt++) {
6212
+ const first = await readPassphraseFromTTY(stdin, stdout, "New vault passphrase: ");
6213
+ if (first.length === 0) {
6214
+ stdout.write("Passphrase cannot be empty.\n");
6215
+ continue;
6216
+ }
6217
+ const second = await readPassphraseFromTTY(stdin, stdout, "Confirm new passphrase: ");
6218
+ if (first === second) return first;
6219
+ stdout.write("Passphrases did not match. Try again.\n");
6220
+ }
6221
+ return null;
6222
+ }
5968
6223
  var MAX_PASSPHRASE_PROMPTS = 3;
5969
6224
  var MIN_PASSPHRASE_WARN_LEN = 12;
5970
6225
  function readPassphraseFromTTY(stdin, stdout, prompt = "Vault passphrase: ") {
@@ -6033,7 +6288,7 @@ async function runSecrets(opts, io = {
6033
6288
  out: (s) => process.stdout.write(s),
6034
6289
  err: (s) => process.stderr.write(s)
6035
6290
  }) {
6036
- const home = opts.home ?? homedir14();
6291
+ const home = opts.home ?? homedir15();
6037
6292
  const path5 = vaultPath(home);
6038
6293
  if (opts.action === "lock") {
6039
6294
  lock();
@@ -6048,12 +6303,18 @@ async function runSecrets(opts, io = {
6048
6303
  if (opts.action === "pull") {
6049
6304
  return await runSecretsPull(opts, io);
6050
6305
  }
6306
+ if (opts.action === "rotate") {
6307
+ return await runSecretsRotate(opts, io);
6308
+ }
6309
+ if (opts.action === "audit") {
6310
+ return await runSecretsAudit(opts, io);
6311
+ }
6051
6312
  if (opts.action === "list") {
6052
6313
  const loaded = await safeLoadVault(path5, io, opts.json, "list");
6053
6314
  if (!loaded.ok) return loaded.result;
6054
6315
  const vault2 = loaded.vault;
6055
6316
  const keys = vault2 ? listKeys(vault2) : [];
6056
- if (opts.json) io.out(`${JSON.stringify({ ok: true, vault: existsSync5(path5), keys }, null, 2)}
6317
+ if (opts.json) io.out(`${JSON.stringify({ ok: true, vault: existsSync6(path5), keys }, null, 2)}
6057
6318
  `);
6058
6319
  else if (!vault2) io.out(`No vault at ${path5}. Run \`yaw-mcp secrets set <name>\` to create one.
6059
6320
  `);
@@ -6084,7 +6345,7 @@ async function runSecrets(opts, io = {
6084
6345
  const loadedForMutate = await safeLoadVault(path5, io, opts.json, opts.action ?? "");
6085
6346
  if (!loadedForMutate.ok) return loadedForMutate.result;
6086
6347
  let vault = loadedForMutate.vault ?? newVault();
6087
- const isFresh = !existsSync5(path5);
6348
+ const isFresh = !existsSync6(path5);
6088
6349
  const passphrase = await resolvePassphrase(opts);
6089
6350
  if (passphrase === null) {
6090
6351
  const msg = "Passphrase required. Set YAW_MCP_VAULT_PASSPHRASE or run from a TTY so we can prompt.";
@@ -6186,7 +6447,7 @@ async function runSecrets(opts, io = {
6186
6447
  }
6187
6448
  var MCP_SECRETS_RESOURCE = "mcp_secrets";
6188
6449
  async function runSecretsPush(opts, io) {
6189
- const home = opts.home ?? homedir14();
6450
+ const home = opts.home ?? homedir15();
6190
6451
  const path5 = vaultPath(home);
6191
6452
  const session = await getSession({ home, baseUrl: opts.baseUrl });
6192
6453
  if (!session) {
@@ -6260,7 +6521,7 @@ async function runSecretsPush(opts, io) {
6260
6521
  }
6261
6522
  }
6262
6523
  async function runSecretsPull(opts, io) {
6263
- const home = opts.home ?? homedir14();
6524
+ const home = opts.home ?? homedir15();
6264
6525
  const path5 = vaultPath(home);
6265
6526
  const session = await getSession({ home, baseUrl: opts.baseUrl });
6266
6527
  if (!session) {
@@ -6328,10 +6589,159 @@ async function runSecretsPull(opts, io) {
6328
6589
  return { exitCode: 1 };
6329
6590
  }
6330
6591
  }
6592
+ async function runSecretsRotate(opts, io) {
6593
+ const home = opts.home ?? homedir15();
6594
+ const path5 = vaultPath(home);
6595
+ const vault = await loadVault(path5);
6596
+ if (!vault) {
6597
+ const msg = `No vault at ${path5} to rotate. Run \`yaw-mcp secrets set <name>\` first.`;
6598
+ if (opts.json) io.err(`${JSON.stringify({ ok: false, error: msg })}
6599
+ `);
6600
+ else io.err(`yaw-mcp secrets rotate: ${msg}
6601
+ `);
6602
+ return { exitCode: 1 };
6603
+ }
6604
+ const currentPassphrase = await resolvePassphrase(opts);
6605
+ if (currentPassphrase === null) {
6606
+ const msg = "Current passphrase required. Set YAW_MCP_VAULT_PASSPHRASE or run from a TTY so we can prompt.";
6607
+ if (opts.json) io.err(`${JSON.stringify({ ok: false, error: msg })}
6608
+ `);
6609
+ else io.err(`yaw-mcp secrets rotate: ${msg}
6610
+ `);
6611
+ return { exitCode: 1 };
6612
+ }
6613
+ let oldKey;
6614
+ try {
6615
+ oldKey = await unlock(vault, currentPassphrase);
6616
+ } catch (err) {
6617
+ const msg = err instanceof Error ? err.message : String(err);
6618
+ if (opts.json) io.err(`${JSON.stringify({ ok: false, error: msg })}
6619
+ `);
6620
+ else io.err(`yaw-mcp secrets rotate: ${msg}
6621
+ `);
6622
+ return { exitCode: 1 };
6623
+ }
6624
+ const newPassphrase = await resolveNewPassphrase(opts);
6625
+ if (newPassphrase === null) {
6626
+ const msg = "New passphrase required (and must be confirmed). Set YAW_MCP_VAULT_PASSPHRASE_NEW or run from a TTY so we can prompt.";
6627
+ if (opts.json) io.err(`${JSON.stringify({ ok: false, error: msg })}
6628
+ `);
6629
+ else io.err(`yaw-mcp secrets rotate: ${msg}
6630
+ `);
6631
+ return { exitCode: 1 };
6632
+ }
6633
+ let rotated;
6634
+ try {
6635
+ rotated = await rotateVault(vault, oldKey, newPassphrase);
6636
+ } catch (err) {
6637
+ const msg = err instanceof Error ? err.message : String(err);
6638
+ if (opts.json) io.err(`${JSON.stringify({ ok: false, error: msg })}
6639
+ `);
6640
+ else io.err(`yaw-mcp secrets rotate: ${msg}
6641
+ `);
6642
+ lock();
6643
+ return { exitCode: 1 };
6644
+ }
6645
+ await saveVault(path5, rotated);
6646
+ lock();
6647
+ const count = Object.keys(rotated.entries).length;
6648
+ let pushedVersion = null;
6649
+ if (opts.push) {
6650
+ const session = await getSession({ home, baseUrl: opts.baseUrl });
6651
+ if (!session) {
6652
+ const msg = "Rotated locally, but --push needs a session. Run `yaw-mcp login --key <license-key>` then push.";
6653
+ if (opts.json) io.err(`${JSON.stringify({ ok: true, rotated: true, pushed: false, note: msg })}
6654
+ `);
6655
+ else io.err(`yaw-mcp secrets rotate: ${msg}
6656
+ `);
6657
+ return { exitCode: 0 };
6658
+ }
6659
+ try {
6660
+ const remote = await getResource(MCP_SECRETS_RESOURCE, { home, baseUrl: opts.baseUrl });
6661
+ const result = await putResource(MCP_SECRETS_RESOURCE, remote.version, rotated, {
6662
+ home,
6663
+ baseUrl: opts.baseUrl
6664
+ });
6665
+ pushedVersion = result.version;
6666
+ } catch (err) {
6667
+ if (err instanceof TeamSyncStaleVersionError) {
6668
+ const hint = `Rotated locally. Push skipped -- remote is at v${err.currentVersion}; pull and reconcile, then push.`;
6669
+ if (opts.json) io.err(`${JSON.stringify({ ok: true, rotated: true, pushed: false, note: hint })}
6670
+ `);
6671
+ else io.err(`yaw-mcp secrets rotate: ${hint}
6672
+ `);
6673
+ return { exitCode: 0 };
6674
+ }
6675
+ if (err instanceof TeamSyncAuthError) {
6676
+ const hint = "Rotated locally. Push skipped -- session expired. Run `yaw-mcp login` again, then push.";
6677
+ if (opts.json) io.err(`${JSON.stringify({ ok: true, rotated: true, pushed: false, note: hint })}
6678
+ `);
6679
+ else io.err(`yaw-mcp secrets rotate: ${hint}
6680
+ `);
6681
+ return { exitCode: 0 };
6682
+ }
6683
+ const message = err instanceof Error ? err.message : String(err);
6684
+ if (opts.json) io.err(`${JSON.stringify({ ok: true, rotated: true, pushed: false, error: message })}
6685
+ `);
6686
+ else io.err(`yaw-mcp secrets rotate: rotated locally but push failed: ${message}
6687
+ `);
6688
+ return { exitCode: 0 };
6689
+ }
6690
+ }
6691
+ if (opts.json) {
6692
+ io.out(
6693
+ `${JSON.stringify({ ok: true, rotated: true, secret_count: count, pushed: pushedVersion !== null, ...pushedVersion !== null ? { new_version: pushedVersion } : {} })}
6694
+ `
6695
+ );
6696
+ } else {
6697
+ io.out(
6698
+ `Rotated ${count} secret${count === 1 ? "" : "s"} under a new passphrase (encryption re-wrapped, token values unchanged).
6699
+ `
6700
+ );
6701
+ if (pushedVersion !== null) io.out(`Pushed the re-encrypted vault -> mcp_secrets v${pushedVersion}.
6702
+ `);
6703
+ io.out("Vault locked -- the next secrets command will prompt for the new passphrase.\n");
6704
+ }
6705
+ return { exitCode: 0 };
6706
+ }
6707
+ async function runSecretsAudit(opts, io) {
6708
+ const home = opts.home ?? homedir15();
6709
+ let events;
6710
+ try {
6711
+ events = await readAuditLog(
6712
+ {
6713
+ ...opts.secretFilter !== void 0 ? { secret: opts.secretFilter } : {},
6714
+ ...opts.serverFilter !== void 0 ? { server: opts.serverFilter } : {}
6715
+ },
6716
+ home
6717
+ );
6718
+ } catch (err) {
6719
+ const msg = err instanceof Error ? err.message : String(err);
6720
+ if (opts.json) io.err(`${JSON.stringify({ ok: false, error: msg })}
6721
+ `);
6722
+ else io.err(`yaw-mcp secrets audit: ${msg}
6723
+ `);
6724
+ return { exitCode: 1 };
6725
+ }
6726
+ if (opts.json) {
6727
+ io.out(`${JSON.stringify({ ok: true, count: events.length, events }, null, 2)}
6728
+ `);
6729
+ return { exitCode: 0 };
6730
+ }
6731
+ if (events.length === 0) {
6732
+ io.out("No secret-resolution audit events recorded yet.\n");
6733
+ return { exitCode: 0 };
6734
+ }
6735
+ for (const e of events) {
6736
+ io.out(`${e.ts} ${e.event === "injected" ? "injected" : "missing "} ${e.server} ${e.secret}
6737
+ `);
6738
+ }
6739
+ return { exitCode: 0 };
6740
+ }
6331
6741
 
6332
6742
  // src/server.ts
6333
- import { readFile as readFile11 } from "fs/promises";
6334
- import { homedir as homedir15 } from "os";
6743
+ import { readFile as readFile12 } from "fs/promises";
6744
+ import { homedir as homedir16 } from "os";
6335
6745
  import { isAbsolute as isAbsolute2, relative, resolve as resolve6 } from "path";
6336
6746
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
6337
6747
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -6447,7 +6857,7 @@ function defaultSpawn2(cmd, args) {
6447
6857
  async function maybeAutoUpgrade(deps = {}) {
6448
6858
  const optOut = process.env.YAW_MCP_AUTO_UPGRADE;
6449
6859
  if (optOut === "0" || optOut?.toLowerCase() === "false") return;
6450
- const current = deps.currentVersion ?? (true ? "0.64.1" : "dev");
6860
+ const current = deps.currentVersion ?? (true ? "0.65.0" : "dev");
6451
6861
  if (current === "dev") return;
6452
6862
  const method = (deps.isSeaImpl ? await deps.isSeaImpl() : await detectSea()) ? "binary" : detectInstallMethod(deps.argvPath ?? process.argv[1]);
6453
6863
  const latest = await (deps.fetchLatestImpl ?? fetchLatestVersion2)();
@@ -6953,22 +7363,22 @@ function closestNames(query, candidates, limit) {
6953
7363
  }
6954
7364
 
6955
7365
  // src/guide.ts
6956
- import { readFile as readFile10 } from "fs/promises";
7366
+ import { readFile as readFile11 } from "fs/promises";
6957
7367
  var GUIDE_READ_TIMEOUT_MS = 1e3;
6958
7368
  async function readGuide(path5, scope) {
6959
7369
  let raw;
7370
+ const ac = new AbortController();
7371
+ const timer = setTimeout(() => ac.abort(new Error("guide read timeout")), GUIDE_READ_TIMEOUT_MS);
6960
7372
  try {
6961
- raw = await Promise.race([
6962
- readFile10(path5, "utf8"),
6963
- new Promise(
6964
- (_, reject) => setTimeout(() => reject(new Error("guide read timeout")), GUIDE_READ_TIMEOUT_MS)
6965
- )
6966
- ]);
7373
+ raw = await readFile11(path5, { encoding: "utf8", signal: ac.signal });
6967
7374
  } catch (err) {
6968
- if (err instanceof Error && err.message === "guide read timeout") {
7375
+ const isTimeout = err instanceof Error && err.code === "ABORT_ERR";
7376
+ if (isTimeout) {
6969
7377
  log("warn", "Guide read timed out", { path: path5 });
6970
7378
  }
6971
7379
  return null;
7380
+ } finally {
7381
+ clearTimeout(timer);
6972
7382
  }
6973
7383
  const content = raw.trim();
6974
7384
  if (content.length === 0) {
@@ -7080,7 +7490,7 @@ function initHeartbeat(url, tok) {
7080
7490
  lastLoggedErrorMessage = null;
7081
7491
  warnedInsecureBearerSkip = false;
7082
7492
  }
7083
- function shouldSendBearer(targetUrl) {
7493
+ function shouldSendBearer2(targetUrl) {
7084
7494
  let parsed;
7085
7495
  try {
7086
7496
  parsed = new URL(targetUrl);
@@ -7106,7 +7516,7 @@ async function reportHeartbeat(clientName, clientVersion, isRefresh = false) {
7106
7516
  const headers = {
7107
7517
  "Content-Type": "application/json"
7108
7518
  };
7109
- if (shouldSendBearer(fullUrl)) {
7519
+ if (shouldSendBearer2(fullUrl)) {
7110
7520
  headers.Authorization = `Bearer ${token3}`;
7111
7521
  }
7112
7522
  const res = await request6(fullUrl, {
@@ -7572,6 +7982,26 @@ var META_TOOLS = {
7572
7982
  openWorldHint: false
7573
7983
  }
7574
7984
  },
7985
+ secrets: {
7986
+ name: "mcp_connect_secrets",
7987
+ description: "List, per installed server, which local-vault secrets its `${secret:NAME}` env references resolve to -- by NAME only, never a value. Use this to confirm a server will get the credentials it needs before activating it, or to spot a typo'd / un-set secret reference. `injectedSecrets` are the names the local vault HAS and the server references; `missing` are names the server references but the vault LACKS (set them via `yaw-mcp secrets set <name>`). This is a values-free preview: it reads the vault's KEY LIST and the server's env-reference NAMES, and never decrypts or returns any secret value. Servers with no `${secret:...}` references are omitted. Requires no passphrase (no decryption happens).",
7988
+ inputSchema: {
7989
+ type: "object",
7990
+ properties: {
7991
+ server: {
7992
+ type: "string",
7993
+ description: 'Optional: restrict the report to a single server namespace (e.g. "gh"). Omit to report every installed server that references a vault secret.'
7994
+ }
7995
+ }
7996
+ },
7997
+ annotations: {
7998
+ title: "Inspect Vault Secret Resolution",
7999
+ readOnlyHint: true,
8000
+ destructiveHint: false,
8001
+ idempotentHint: true,
8002
+ openWorldHint: false
8003
+ }
8004
+ },
7575
8005
  exec: {
7576
8006
  name: "mcp_connect_exec",
7577
8007
  description: "Run a short DECLARATIVE pipeline of upstream tool calls in a single round-trip. Use this when you already know the exact 2-4 tool calls to make and one call's output feeds another's args \u2014 e.g. `a = gh_list_prs(); b = gh_get_pr(a[0].number); return b`. NOT a code sandbox: there is no expression language, no loops, no branching, no arithmetic. The only control flow is sequential step execution; the only data-flow primitive is `{\"$ref\": \"<stepId>[.path.to.value]\"}` which substitutes a prior step's output (or a nested field of it) into the next step's args. Paths support dot keys and `[N]` / `.N` array indexing. Each step's `tool` must be a namespaced, already-loaded tool name (the exec does not auto-activate \u2014 call `mcp_connect_activate` first). Max 16 steps per exec. If any step fails, the whole pipeline fails and returns `{ ok: false, failedStep, error, partial: { ...completed outputs } }`. On success returns `{ ok: true, result: <return-step output>, steps: { ...all outputs } }`. Prefer this over back-to-back tool calls when the chain is deterministic \u2014 it saves prompt-token replay and client round-trips.",
@@ -7689,6 +8119,30 @@ function buildInstallPayload(args) {
7689
8119
  }
7690
8120
  return { ok: true, payload };
7691
8121
  }
8122
+ var SECRETS_REPORT_REF_RE = /\$\{secret:([a-zA-Z0-9_.-]+)\}/g;
8123
+ function computeSecretsReport(servers, vaultKeys) {
8124
+ const rows = [];
8125
+ for (const server of servers) {
8126
+ const referenced = /* @__PURE__ */ new Set();
8127
+ for (const v of Object.values(server.env ?? {})) {
8128
+ if (typeof v !== "string") continue;
8129
+ for (const m of v.matchAll(SECRETS_REPORT_REF_RE)) referenced.add(m[1]);
8130
+ }
8131
+ if (referenced.size === 0) continue;
8132
+ const injectedSecrets = [];
8133
+ const missing = [];
8134
+ for (const name of referenced) {
8135
+ if (vaultKeys.has(name)) injectedSecrets.push(name);
8136
+ else missing.push(name);
8137
+ }
8138
+ rows.push({
8139
+ server: server.namespace,
8140
+ injectedSecrets: injectedSecrets.sort(),
8141
+ missing: missing.sort()
8142
+ });
8143
+ }
8144
+ return rows;
8145
+ }
7692
8146
  var META_TOOL_NAMES = /* @__PURE__ */ new Set([
7693
8147
  META_TOOLS.discover.name,
7694
8148
  META_TOOLS.activate.name,
@@ -7700,7 +8154,8 @@ var META_TOOL_NAMES = /* @__PURE__ */ new Set([
7700
8154
  META_TOOLS.read_tool.name,
7701
8155
  META_TOOLS.suggest.name,
7702
8156
  META_TOOLS.exec.name,
7703
- META_TOOLS.bundles.name
8157
+ META_TOOLS.bundles.name,
8158
+ META_TOOLS.secrets.name
7704
8159
  ]);
7705
8160
 
7706
8161
  // src/pack-detect.ts
@@ -8139,7 +8594,8 @@ function pruneJson(value) {
8139
8594
  }
8140
8595
 
8141
8596
  // src/read-tool.ts
8142
- function normalizeToolName(namespace, raw) {
8597
+ function normalizeToolName(namespace, raw, tools) {
8598
+ if (tools?.some((t) => t.name === raw)) return raw;
8143
8599
  const prefix = `${namespace}_`;
8144
8600
  if (raw.startsWith(prefix) && raw.length > prefix.length) return raw.slice(prefix.length);
8145
8601
  return raw;
@@ -9137,7 +9593,7 @@ async function resolveUvSpawn(command, args) {
9137
9593
  }
9138
9594
 
9139
9595
  // src/upstream.ts
9140
- async function resolveServerEnv(env) {
9596
+ async function resolveServerEnv(env, namespace) {
9141
9597
  if (!hasSecretRefs(env)) return env;
9142
9598
  const refKeys = Object.entries(env).filter(([, v]) => typeof v === "string" && v.includes("${secret:")).map(([k]) => k);
9143
9599
  const passphrase = process.env.YAW_MCP_VAULT_PASSPHRASE;
@@ -9159,8 +9615,36 @@ async function resolveServerEnv(env) {
9159
9615
  if (missing.length > 0) {
9160
9616
  throw new Error(`vault: missing or undecryptable secret refs: ${missing.join(", ")}`);
9161
9617
  }
9618
+ try {
9619
+ await recordResolveAudit(namespace, env, missing);
9620
+ } catch (auditErr) {
9621
+ log("warn", "Failed to record secret-resolve audit (non-fatal)", {
9622
+ namespace,
9623
+ error: auditErr instanceof Error ? auditErr.message : String(auditErr)
9624
+ });
9625
+ }
9162
9626
  return resolved;
9163
9627
  }
9628
+ async function recordResolveAudit(namespace, env, missing) {
9629
+ const missingSet = new Set(missing);
9630
+ const referenced = collectSecretNames(env);
9631
+ for (const name of referenced) {
9632
+ if (missingSet.has(name)) continue;
9633
+ await appendAuditEvent({ server: namespace, secret: name, event: "injected" });
9634
+ }
9635
+ for (const name of missingSet) {
9636
+ await appendAuditEvent({ server: namespace, secret: name, event: "missing" });
9637
+ }
9638
+ }
9639
+ function collectSecretNames(env) {
9640
+ const names = /* @__PURE__ */ new Set();
9641
+ const re = /\$\{secret:([a-zA-Z0-9_.-]+)\}/g;
9642
+ for (const v of Object.values(env)) {
9643
+ if (typeof v !== "string") continue;
9644
+ for (const m of v.matchAll(re)) names.add(m[1]);
9645
+ }
9646
+ return [...names];
9647
+ }
9164
9648
  var DEFAULT_CONNECT_TIMEOUT = (() => {
9165
9649
  const env = process.env.MCP_CONNECT_TIMEOUT;
9166
9650
  if (!env) return 15e3;
@@ -9208,7 +9692,7 @@ function categorizeSpawnError(err) {
9208
9692
  }
9209
9693
  async function connectToUpstream(config, onDisconnect, onListChanged) {
9210
9694
  const client = new Client(
9211
- { name: "yaw-mcp", version: true ? "0.64.1" : "dev" },
9695
+ { name: "yaw-mcp", version: true ? "0.65.0" : "dev" },
9212
9696
  { capabilities: {} }
9213
9697
  );
9214
9698
  let transport;
@@ -9224,7 +9708,7 @@ async function connectToUpstream(config, onDisconnect, onListChanged) {
9224
9708
  ...parentEnv
9225
9709
  } = process.env;
9226
9710
  const resolved = await resolveUvSpawn(config.command, config.args ?? []);
9227
- const serverEnv = await resolveServerEnv(config.env ?? {});
9711
+ const serverEnv = await resolveServerEnv(config.env ?? {}, config.namespace);
9228
9712
  resolvedServerEnv = serverEnv;
9229
9713
  const stdioTransport = new StdioClientTransport({
9230
9714
  command: resolved.command,
@@ -9279,7 +9763,7 @@ async function connectToUpstream(config, onDisconnect, onListChanged) {
9279
9763
  message = `Server "${config.namespace}" started but didn't complete the MCP handshake within ${connectTimeoutMs / 1e3}s.${trimmedStderr ? ` stderr tail: ${redactSecretsInOutput(trimmedStderr, resolvedServerEnv).slice(-500)}` : ""}`;
9280
9764
  } else if (trimmedStderr.length > 0) {
9281
9765
  category = "install_failure";
9282
- const safe = redactSecretsInOutput(trimmedStderr, config.env ?? {});
9766
+ const safe = redactSecretsInOutput(trimmedStderr, resolvedServerEnv);
9283
9767
  message = `Server "${config.namespace}" failed to start. stderr: ${safe.slice(-500)}`;
9284
9768
  } else {
9285
9769
  category = categorizeSpawnError(err);
@@ -9292,7 +9776,7 @@ async function connectToUpstream(config, onDisconnect, onListChanged) {
9292
9776
  if (config.id) {
9293
9777
  message = `${message} \u2192 Edit at https://yaw.sh/mcp/dashboard/connect#server-${config.id}`;
9294
9778
  }
9295
- const redactedTail = trimmedStderr ? redactSecretsInOutput(trimmedStderr, config.env ?? {}) : void 0;
9779
+ const redactedTail = trimmedStderr ? redactSecretsInOutput(trimmedStderr, resolvedServerEnv) : void 0;
9296
9780
  throw new ActivationError(message, category, redactedTail, err);
9297
9781
  }
9298
9782
  log("info", "Connected to upstream", { name: config.name, namespace: config.namespace, type: config.type });
@@ -9540,7 +10024,7 @@ var ConnectServer = class _ConnectServer {
9540
10024
  this.apiUrl = apiUrl5;
9541
10025
  this.token = token5;
9542
10026
  this.server = new Server(
9543
- { name: "yaw-mcp", version: true ? "0.64.1" : "dev" },
10027
+ { name: "yaw-mcp", version: true ? "0.65.0" : "dev" },
9544
10028
  {
9545
10029
  capabilities: {
9546
10030
  tools: { listChanged: true },
@@ -10122,6 +10606,17 @@ var ConnectServer = class _ConnectServer {
10122
10606
  recordConnectEvent({ namespace: null, toolName: null, action: "bundles", latencyMs: null, success: true });
10123
10607
  return this.attachGuideNudge(this.handleBundles(action));
10124
10608
  }
10609
+ if (name === META_TOOLS.secrets.name) {
10610
+ const serverArg = typeof args.server === "string" ? args.server : void 0;
10611
+ recordConnectEvent({
10612
+ namespace: serverArg ?? null,
10613
+ toolName: null,
10614
+ action: "secrets",
10615
+ latencyMs: null,
10616
+ success: true
10617
+ });
10618
+ return this.attachGuideNudge(await this.handleSecretsReport(serverArg));
10619
+ }
10125
10620
  let routes = this.toolRoutes;
10126
10621
  let route = routes.get(name);
10127
10622
  if (route?.deferred) {
@@ -11217,7 +11712,7 @@ ${activeCount} loaded in this session, ${totalTools} tools in context${tokenSumm
11217
11712
  }
11218
11713
  const ALLOWED_FILENAMES = ["claude_desktop_config.json", "mcp.json", "settings.json", "mcp_config.json"];
11219
11714
  try {
11220
- const resolved = filepath.startsWith("~/") || filepath.startsWith("~\\") ? resolve6(homedir15(), filepath.slice(2)) : resolve6(filepath);
11715
+ const resolved = filepath.startsWith("~/") || filepath.startsWith("~\\") ? resolve6(homedir16(), filepath.slice(2)) : resolve6(filepath);
11221
11716
  const resolvedBasename = resolved.split(/[/\\]/).pop() || "";
11222
11717
  if (!ALLOWED_FILENAMES.includes(resolvedBasename)) {
11223
11718
  return {
@@ -11234,7 +11729,7 @@ ${activeCount} loaded in this session, ${totalTools} tools in context${tokenSumm
11234
11729
  const rel = relative(base, p);
11235
11730
  return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
11236
11731
  };
11237
- if (!isUnder(homedir15(), resolved) && !isUnder(process.cwd(), resolved)) {
11732
+ if (!isUnder(homedir16(), resolved) && !isUnder(process.cwd(), resolved)) {
11238
11733
  return {
11239
11734
  content: [
11240
11735
  { type: "text", text: "Import path must be under your home directory or the current working directory." }
@@ -11242,7 +11737,7 @@ ${activeCount} loaded in this session, ${totalTools} tools in context${tokenSumm
11242
11737
  isError: true
11243
11738
  };
11244
11739
  }
11245
- const raw = await readFile11(resolved, "utf-8");
11740
+ const raw = await readFile12(resolved, "utf-8");
11246
11741
  const parsed = JSON.parse(raw);
11247
11742
  if (!parsed.mcpServers || typeof parsed.mcpServers !== "object" || Array.isArray(parsed.mcpServers)) {
11248
11743
  return {
@@ -11471,9 +11966,9 @@ Use mcp_connect_discover to see imported servers.`
11471
11966
  isError: true
11472
11967
  };
11473
11968
  }
11474
- const toolName = normalizeToolName(serverArg, toolArg);
11475
11969
  const existing = this.connections.get(serverArg);
11476
11970
  if (existing && existing.status === "connected") {
11971
+ const toolName = normalizeToolName(serverArg, toolArg, existing.tools);
11477
11972
  const tool = findTool(existing.tools, toolName);
11478
11973
  if (!tool) {
11479
11974
  return {
@@ -11509,6 +12004,7 @@ Use mcp_connect_discover to see imported servers.`
11509
12004
  };
11510
12005
  }
11511
12006
  try {
12007
+ const toolName = normalizeToolName(serverArg, toolArg, transient.tools);
11512
12008
  const tool = findTool(transient.tools, toolName);
11513
12009
  if (!tool) {
11514
12010
  return {
@@ -11533,6 +12029,43 @@ Use mcp_connect_discover to see imported servers.`
11533
12029
  );
11534
12030
  }
11535
12031
  }
12032
+ // Values-free preview of which local-vault secrets each installed
12033
+ // server's `${secret:NAME}` env refs resolve to. NAMES ONLY -- this
12034
+ // reads the vault's KEY LIST (listKeys, no unlock, no passphrase) and
12035
+ // the servers' env-reference names, and NEVER calls getSecret /
12036
+ // decryptEntry. Servers with no refs are omitted.
12037
+ async handleSecretsReport(serverArg) {
12038
+ const vault = await loadVault(vaultPath()).catch(() => null);
12039
+ const vaultKeys = new Set(vault ? listKeys(vault) : []);
12040
+ let servers = this.getProfiledActiveServers().map((s) => ({ namespace: s.namespace, env: s.env }));
12041
+ if (serverArg) servers = servers.filter((s) => s.namespace === serverArg);
12042
+ const rows = computeSecretsReport(servers, vaultKeys);
12043
+ if (serverArg && servers.length === 0) {
12044
+ return {
12045
+ content: [
12046
+ {
12047
+ type: "text",
12048
+ text: `No installed server with namespace "${serverArg}". Call mcp_connect_discover to list installed servers.`
12049
+ }
12050
+ ],
12051
+ isError: true
12052
+ };
12053
+ }
12054
+ if (rows.length === 0) {
12055
+ const scope = serverArg ? `Server "${serverArg}"` : "No installed server";
12056
+ return {
12057
+ content: [
12058
+ {
12059
+ type: "text",
12060
+ text: `${scope} references any \${secret:NAME} vault values. Add a reference in a server's env (e.g. GITHUB_TOKEN=\${secret:gh}) and store the value with \`yaw-mcp secrets set <name>\`.`
12061
+ }
12062
+ ]
12063
+ };
12064
+ }
12065
+ return {
12066
+ content: [{ type: "text", text: JSON.stringify(rows, null, 2) }]
12067
+ };
12068
+ }
11536
12069
  handleHealth() {
11537
12070
  const lines = [];
11538
12071
  if (this.profile) {
@@ -11974,9 +12507,10 @@ async function runServersCommand(opts = {}) {
11974
12507
  printErr("yaw-mcp servers: backend returned no data (unexpected 304).");
11975
12508
  return { exitCode: 2, lines };
11976
12509
  }
11977
- const filtered = opts.filter ? {
12510
+ const filterStr = opts.filter;
12511
+ const filtered = filterStr !== void 0 ? {
11978
12512
  ...backend,
11979
- servers: backend.servers.filter((s) => s.namespace.toLowerCase().includes(opts.filter.toLowerCase()))
12513
+ servers: backend.servers.filter((s) => s.namespace.toLowerCase().includes(filterStr.toLowerCase()))
11980
12514
  } : backend;
11981
12515
  const gradesReader = opts.gradesReader ?? readGradesCache;
11982
12516
  const grades = await gradesReader(opts.home).catch(() => ({}));
@@ -11991,12 +12525,12 @@ async function runServersCommand(opts = {}) {
11991
12525
  const payload = {
11992
12526
  ...merged,
11993
12527
  filter: opts.filter ?? null,
11994
- filterMatched: opts.filter ? merged.servers.length > 0 : null
12528
+ filterMatched: opts.filter !== void 0 ? merged.servers.length > 0 : null
11995
12529
  };
11996
12530
  print(JSON.stringify(payload, null, 2));
11997
12531
  return { exitCode: 0, lines };
11998
12532
  }
11999
- if (opts.filter && filtered.servers.length === 0) {
12533
+ if (opts.filter !== void 0 && filtered.servers.length === 0) {
12000
12534
  print(`No servers match "${opts.filter}". Run \`yaw-mcp servers\` to see the full list.`);
12001
12535
  return { exitCode: 0, lines };
12002
12536
  }
@@ -12044,21 +12578,21 @@ function truncateVersion(v) {
12044
12578
  }
12045
12579
 
12046
12580
  // src/set-active-cmd.ts
12047
- import { homedir as homedir16 } from "os";
12581
+ import { homedir as homedir17 } from "os";
12048
12582
 
12049
12583
  // src/sync-state.ts
12050
- import { existsSync as existsSync6 } from "fs";
12051
- import { mkdir as mkdir5, readFile as readFile12 } from "fs/promises";
12052
- import { dirname as dirname4, join as join11 } from "path";
12584
+ import { existsSync as existsSync7 } from "fs";
12585
+ import { mkdir as mkdir5, readFile as readFile13 } from "fs/promises";
12586
+ import { dirname as dirname4, join as join12 } from "path";
12053
12587
  var SYNC_STATE_FILENAME = "sync-state.json";
12054
12588
  function syncStatePath(home) {
12055
- return join11(home, CONFIG_DIRNAME, SYNC_STATE_FILENAME);
12589
+ return join12(home, CONFIG_DIRNAME, SYNC_STATE_FILENAME);
12056
12590
  }
12057
12591
  async function readSyncState(home) {
12058
12592
  const path5 = syncStatePath(home);
12059
- if (!existsSync6(path5)) return {};
12593
+ if (!existsSync7(path5)) return {};
12060
12594
  try {
12061
- const raw = await readFile12(path5, "utf8");
12595
+ const raw = await readFile13(path5, "utf8");
12062
12596
  const parsed = JSON.parse(raw);
12063
12597
  if (!parsed || typeof parsed !== "object") return {};
12064
12598
  return parsed;
@@ -12162,10 +12696,15 @@ async function runSetActive(opts, io = { out: (s) => process.stdout.write(s), er
12162
12696
  base
12163
12697
  );
12164
12698
  if (typeof putRes.version === "number") {
12165
- await deps.writeSyncState(opts.home ?? homedir16(), {
12699
+ await deps.writeSyncState(opts.home ?? homedir17(), {
12166
12700
  mcp_bundles: { lastPulledVersion: putRes.version }
12167
12701
  }).catch(() => {
12168
12702
  });
12703
+ } else {
12704
+ io.err(
12705
+ `yaw-mcp set-active: putRes.version was not a number (got ${JSON.stringify(putRes.version)}); local sync-state not updated
12706
+ `
12707
+ );
12169
12708
  }
12170
12709
  return done(io, opts.json, namespace, active, true);
12171
12710
  } catch (e) {
@@ -12203,7 +12742,7 @@ function fail(io, json, message, code) {
12203
12742
  }
12204
12743
 
12205
12744
  // src/stats-cmd.ts
12206
- import { homedir as homedir17 } from "os";
12745
+ import { homedir as homedir18 } from "os";
12207
12746
  var STATS_USAGE = `Usage: yaw-mcp stats [--json] [--limit N] [--days N]
12208
12747
 
12209
12748
  Print a digest of recent AI tool calls recorded against your Yaw
@@ -12327,7 +12866,7 @@ async function runStats(opts, io = {
12327
12866
  out: (s) => process.stdout.write(s),
12328
12867
  err: (s) => process.stderr.write(s)
12329
12868
  }) {
12330
- const home = opts.home ?? homedir17();
12869
+ const home = opts.home ?? homedir18();
12331
12870
  const session = await getSession({ home, baseUrl: opts.baseUrl });
12332
12871
  if (!session) {
12333
12872
  const msg = "Not signed in. Yaw MCP analytics requires a Yaw Team account.\n - Yaw Team: $15/seat/mo or $150/seat/yr -- https://yaw.sh/mcp\nSign in with: yaw-mcp login --key <license-key>";
@@ -12416,14 +12955,22 @@ function suggestSubcommand(input, limit = 3) {
12416
12955
  }
12417
12956
  function suggestFlag(input, limit = 2) {
12418
12957
  if (input.length <= 2) return [];
12419
- return closestNames(input, FLAG_ALIASES, limit);
12958
+ const q = input.toLowerCase();
12959
+ const hits = [];
12960
+ for (const alias of FLAG_ALIASES) {
12961
+ if (alias.toLowerCase() === q) continue;
12962
+ const d = levenshtein(q, alias.toLowerCase());
12963
+ if (d <= 2) hits.push({ name: alias, d });
12964
+ }
12965
+ hits.sort((a, b) => a.d - b.d || a.name.localeCompare(b.name));
12966
+ return hits.slice(0, limit).map((h) => h.name);
12420
12967
  }
12421
12968
 
12422
12969
  // src/sync-cmd.ts
12423
- import { existsSync as existsSync7 } from "fs";
12424
- import { mkdir as mkdir6, readFile as readFile13 } from "fs/promises";
12425
- import { homedir as homedir18 } from "os";
12426
- import { dirname as dirname5, join as join12 } from "path";
12970
+ import { existsSync as existsSync8 } from "fs";
12971
+ import { mkdir as mkdir6, readFile as readFile14 } from "fs/promises";
12972
+ import { homedir as homedir19 } from "os";
12973
+ import { dirname as dirname5, join as join13 } from "path";
12427
12974
  var SYNC_USAGE = `Usage: yaw-mcp sync <push|pull|status> [--json]
12428
12975
 
12429
12976
  Replicate ~/.yaw-mcp/bundles.json across machines via your Yaw
@@ -12470,12 +13017,12 @@ ${SYNC_USAGE}` };
12470
13017
  return { ok: true, options: opts };
12471
13018
  }
12472
13019
  function bundlesPath(home) {
12473
- return join12(home, CONFIG_DIRNAME, BUNDLES_FILENAME2);
13020
+ return join13(home, CONFIG_DIRNAME, BUNDLES_FILENAME2);
12474
13021
  }
12475
13022
  async function readLocalBundles(home) {
12476
13023
  const path5 = bundlesPath(home);
12477
- if (!existsSync7(path5)) return { version: 1, servers: [] };
12478
- const raw = await readFile13(path5, "utf8");
13024
+ if (!existsSync8(path5)) return { version: 1, servers: [] };
13025
+ const raw = await readFile14(path5, "utf8");
12479
13026
  let parsed;
12480
13027
  try {
12481
13028
  parsed = JSON.parse(raw);
@@ -12526,7 +13073,7 @@ async function runSync(opts, io = {
12526
13073
  out: (s) => process.stdout.write(s),
12527
13074
  err: (s) => process.stderr.write(s)
12528
13075
  }) {
12529
- const home = opts.home ?? homedir18();
13076
+ const home = opts.home ?? homedir19();
12530
13077
  const session = await getSession({ home, baseUrl: opts.baseUrl });
12531
13078
  if (!session) {
12532
13079
  const msg = "Not signed in. Run `yaw-mcp login --key <license-key>` first.";
@@ -12685,10 +13232,11 @@ function handleSyncError(err, opts, io) {
12685
13232
  return { exitCode: 1 };
12686
13233
  }
12687
13234
  if (err instanceof TeamSyncAuthError) {
12688
- if (opts.json)
12689
- io.err(`${JSON.stringify({ ok: false, error: "Session expired or revoked. Run `yaw-mcp login` again." })}
13235
+ const authMsg = "Session expired or revoked. Run `yaw-mcp login --key <license-key>` again.";
13236
+ if (opts.json) io.err(`${JSON.stringify({ ok: false, error: authMsg })}
13237
+ `);
13238
+ else io.err(`yaw-mcp sync: ${authMsg}
12690
13239
  `);
12691
- else io.err("yaw-mcp sync: session expired or revoked. Run `yaw-mcp login --key <license-key>` again.\n");
12692
13240
  return { exitCode: 1 };
12693
13241
  }
12694
13242
  if (err instanceof TeamSyncForbiddenError) {
@@ -13100,7 +13648,7 @@ if (subcommand === "compliance") {
13100
13648
  `);
13101
13649
  process.exit(0);
13102
13650
  } else if (subcommand === "--version" || subcommand === "-V") {
13103
- process.stdout.write(`yaw-mcp ${true ? "0.64.1" : "dev"}
13651
+ process.stdout.write(`yaw-mcp ${true ? "0.65.0" : "dev"}
13104
13652
  `);
13105
13653
  process.exit(0);
13106
13654
  } else if (subcommand && !subcommand.startsWith("-")) {