@yawlabs/mcp 0.64.2 → 0.66.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 +712 -59
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -373,6 +373,8 @@ Rotate a credential in one place (the dashboard), every machine picks up the new
373
373
  | `YAW_MCP_DISABLE_PERSISTENCE` | No | Set to `1` or `true` to keep learning + pack-history scoped to the current process -- nothing loaded at start, nothing written on shutdown. Intended for ephemeral / shared environments (CI, containers). Default: cross-session persistence enabled at `~/.yaw-mcp/state.json`. |
374
374
  | `YAW_MCP_AUTO_LOAD` | No | Set to `1` or `true` to pre-activate the top recurring pack (from persisted pack-history) on startup -- no LLM round-trip required. Skips silently when history is empty or no pack's namespaces are all installed. Default: off. Requires persistence to be enabled. |
375
375
  | `YAW_MCP_MIN_COMPLIANCE` | No | Minimum compliance grade (`A`, `B`, `C`, `D`, or `F`, case-insensitive) an installed server must report before `mcp_connect_activate` will load it. Ungraded servers always pass (don't punish unknown). `discover()` annotates below-grade servers in place and shows a "Compliance filter active" header when set. Invalid values log a warning and disable the filter. Default: unset (no filter). |
376
+ | `YAW_MCP_VAULT_PASSPHRASE` | No | Passphrase for the local secret vault (`~/.yaw-mcp/secrets.json`). Required for spawn-time `${secret:NAME}` substitution and to avoid the interactive prompt on `yaw-mcp secrets`. Stripped from every spawned upstream server's env. |
377
+ | `YAW_MCP_VAULT_PASSPHRASE_NEW` | No | The NEW passphrase for `yaw-mcp secrets rotate`. When unset, rotate prompts (confirm-twice) on a TTY instead. |
376
378
  | `MCP_CONNECT_TIMEOUT` | No | Connection timeout in ms for upstream servers (default: `15000`) |
377
379
  | `MCP_CONNECT_IDLE_THRESHOLD` | No | Baseline for idle auto-unload (default: `10`). The per-namespace adaptive cap is `[5, 50]` -- bursty namespaces extend past the baseline, long-idle ones unload at it. |
378
380
 
@@ -388,6 +390,104 @@ The popular Python-based MCP servers (`sqlite`, `time`, `sentry`, and other uvx-
388
390
 
389
391
  `uvx ARGS` is always rewritten to `uv tool run ARGS` at spawn time -- so only `uv` needs to be reachable, not `uvx` separately. Fixes Windows setups where one was on PATH and the other wasn't.
390
392
 
393
+ ## Local secret vault (no account required)
394
+
395
+ There are two ways a secret reaches a server yaw-mcp spawns, and they have
396
+ different threat models:
397
+
398
+ 1. **Backend-credential path (account required).** Paste a token into a
399
+ server's `env` block in the yaw.sh/mcp dashboard. The value is encrypted
400
+ at rest on the backend and injected at spawn time. It syncs across every
401
+ machine signed in to the same account, and revoking the account token cuts
402
+ off access everywhere on the next poll. This is the path the
403
+ "Cross-machine config sync" and "Trust & security" sections describe.
404
+
405
+ 2. **Local secret vault (no account required).** Keep the value in an
406
+ encrypted file on your own machine at `~/.yaw-mcp/secrets.json` and
407
+ reference it from any server's `env` with a `${secret:NAME}` placeholder.
408
+ yaw-mcp substitutes the decrypted value into the child env at spawn time.
409
+ The value never leaves your machine, never goes to the backend, and works
410
+ with no login. This is the right path when you want a credential that is
411
+ strictly local to one machine, or you don't have an account at all.
412
+
413
+ ### How `${secret:NAME}` references work
414
+
415
+ Put a placeholder in a server's `env` value -- it can stand alone or be
416
+ composed inline:
417
+
418
+ ```jsonc
419
+ {
420
+ "namespace": "gh",
421
+ "command": "npx",
422
+ "args": ["-y", "@modelcontextprotocol/server-github"],
423
+ "env": {
424
+ "GITHUB_PERSONAL_ACCESS_TOKEN": "${secret:gh}",
425
+ "AUTH_HEADER": "Bearer ${secret:gh}"
426
+ }
427
+ }
428
+ ```
429
+
430
+ At spawn time, if `YAW_MCP_VAULT_PASSPHRASE` is set in yaw-mcp's own
431
+ environment, yaw-mcp loads the vault, decrypts the referenced names, and
432
+ substitutes the values into the child's env. If the passphrase is absent, the
433
+ vault is missing, or a referenced name isn't stored, the literal
434
+ `${secret:NAME}` is passed through unchanged -- the child then surfaces its
435
+ own "missing/invalid credential" error, which is louder than silently passing
436
+ an empty string.
437
+
438
+ ### Managing the vault -- `yaw-mcp secrets`
439
+
440
+ ```bash
441
+ yaw-mcp secrets set <name> # store a value (stdin prompt, no echo; or --value/--stdin)
442
+ yaw-mcp secrets get <name> # decrypt and print one value
443
+ yaw-mcp secrets list # show entry NAMES (values stay encrypted)
444
+ yaw-mcp secrets remove <name> # delete an entry
445
+ yaw-mcp secrets lock # clear the in-process passphrase cache
446
+ yaw-mcp secrets rotate [--push] # re-encrypt the whole vault under a NEW passphrase
447
+ yaw-mcp secrets audit [--secret NAME] [--server NS] [--json] # who consumed which secret, when
448
+ yaw-mcp secrets push # upload the encrypted blob to your account (login required)
449
+ yaw-mcp secrets pull # download it back (login required)
450
+ ```
451
+
452
+ The passphrase derives the encryption key via scrypt and is cached in memory
453
+ for the lifetime of one yaw-mcp process; the on-disk file only ever holds
454
+ ciphertext (AES-256-GCM, per-entry IV + auth tag, one vault-level salt). Set
455
+ `YAW_MCP_VAULT_PASSPHRASE` in the env to avoid the prompt -- this is required
456
+ for spawn-time substitution, since a spawned MCP server runs non-interactively
457
+ and can't prompt on a TTY.
458
+
459
+ `rotate` re-encrypts every entry under a fresh salt + key derived from a NEW
460
+ passphrase (read from `YAW_MCP_VAULT_PASSPHRASE_NEW` or a confirm-twice
461
+ prompt). It decrypts every entry under the current passphrase FIRST and aborts
462
+ the whole operation -- leaving the on-disk vault untouched -- if any entry
463
+ fails to decrypt. **Rotate re-wraps the ENCRYPTION, not the underlying token
464
+ values:** a token that has leaked is still leaked after a rotate; rotate it at
465
+ its source. Rotate does NOT push to your account unless you pass `--push`.
466
+
467
+ `audit` reads an append-only NDJSON log at `~/.yaw-mcp/secrets-audit.log`
468
+ (mode `0600`, tail-capped) recording that a secret NAME was `injected` into
469
+ (or was `missing` for) a given server namespace, with a timestamp. **The log
470
+ never records a value** -- only names, namespaces, and times. Writes to it are
471
+ fail-open: a broken or unwritable log never blocks a server spawn.
472
+
473
+ The `mcp_connect_secrets` meta-tool gives the same picture to the model
474
+ without any decryption: per server it lists `injectedSecrets` (names the vault
475
+ has and the server references) and `missing` (referenced names the vault
476
+ lacks). It reads only the vault's key list and the servers' reference names --
477
+ it never decrypts or returns a value, and needs no passphrase.
478
+
479
+ ### Offline threat model
480
+
481
+ The vault protects the on-disk file against **offline brute-force after
482
+ exfiltration** -- a stolen laptop, a leaked backup, a synced dotfile repo. The
483
+ file is useless without the passphrase: scrypt key derivation makes guessing
484
+ expensive, and AES-256-GCM means a tampered ciphertext fails to decrypt rather
485
+ than yielding garbage plaintext. What the vault does **not** defend against: a
486
+ process running as you while the passphrase is cached in memory (it can ask
487
+ yaw-mcp to decrypt), a keylogger capturing the passphrase, or a value already
488
+ leaked at its source. For those, rotate the underlying token, not the vault
489
+ encryption.
490
+
391
491
  ## Trust & security
392
492
 
393
493
  MCP servers are third-party code that you choose to run, and yaw-mcp launches them on your machine or calls them over the network. We don't sandbox arbitrary code and we're not an antivirus -- that's your OS and network. What yaw-mcp gives you is **visibility and a gate**:
package/dist/index.js CHANGED
@@ -825,6 +825,7 @@ async function readConfigAt(path5, scope, warnings) {
825
825
  }
826
826
  const servers = Array.isArray(obj.servers) ? obj.servers.filter((v) => typeof v === "string") : void 0;
827
827
  const blocked = Array.isArray(obj.blocked) ? obj.blocked.filter((v) => typeof v === "string") : void 0;
828
+ const installNudge = typeof obj.installNudge === "boolean" ? obj.installNudge : void 0;
828
829
  if (token5) {
829
830
  if (scope === "project") {
830
831
  warnings.push(
@@ -833,7 +834,7 @@ async function readConfigAt(path5, scope, warnings) {
833
834
  }
834
835
  await checkPermissions(path5, warnings);
835
836
  }
836
- return { path: path5, scope, version, token: token5, apiBase, servers, blocked };
837
+ return { path: path5, scope, version, token: token5, apiBase, servers, blocked, installNudge };
837
838
  }
838
839
  async function checkPermissions(path5, warnings) {
839
840
  if (process.platform === "win32") return;
@@ -855,6 +856,13 @@ function pickServers(files) {
855
856
  if (project !== void 0) return project;
856
857
  return files.find((f) => f.scope === "global")?.servers;
857
858
  }
859
+ function pickInstallNudge(files) {
860
+ const local = files.find((f) => f.scope === "local")?.installNudge;
861
+ if (local !== void 0) return local;
862
+ const project = files.find((f) => f.scope === "project")?.installNudge;
863
+ if (project !== void 0) return project;
864
+ return files.find((f) => f.scope === "global")?.installNudge;
865
+ }
858
866
  function unionBlocked(files) {
859
867
  const set = /* @__PURE__ */ new Set();
860
868
  let touched = false;
@@ -934,6 +942,7 @@ async function loadYawMcpConfig(opts = {}) {
934
942
  apiBaseSource,
935
943
  servers: pickServers(loadedFiles),
936
944
  blocked: unionBlocked(loadedFiles),
945
+ installNudge: pickInstallNudge(loadedFiles),
937
946
  projectConfigDir,
938
947
  loadedFiles,
939
948
  warnings
@@ -963,10 +972,6 @@ function toProfile(config) {
963
972
  }
964
973
  return result;
965
974
  }
966
- async function loadEffectiveProfile(cwd, home) {
967
- const config = await loadYawMcpConfig({ cwd, home });
968
- return toProfile(config);
969
- }
970
975
  function isAllowed(rules, namespace) {
971
976
  if (!rules) return true;
972
977
  if (rules.blocked?.includes(namespace)) return false;
@@ -2060,6 +2065,18 @@ function cliToNamespaces() {
2060
2065
  reverseIndexCache = map;
2061
2066
  return map;
2062
2067
  }
2068
+ var SHADOW_INSTALL_TARGETS = {
2069
+ aws: { package: "@yawlabs/aws-mcp", namespace: "aws", name: "AWS" },
2070
+ caddy: { package: "@yawlabs/caddy-mcp", namespace: "caddy", name: "Caddy" },
2071
+ curl: { package: "@yawlabs/fetch-mcp", namespace: "fetch", name: "Fetch" },
2072
+ wget: { package: "@yawlabs/fetch-mcp", namespace: "fetch", name: "Fetch" },
2073
+ psql: { package: "@yawlabs/postgres-mcp", namespace: "postgres", name: "Postgres" },
2074
+ pg_dump: { package: "@yawlabs/postgres-mcp", namespace: "postgres", name: "Postgres" },
2075
+ tailscale: { package: "@yawlabs/tailscale-mcp", namespace: "tailscale", name: "Tailscale" }
2076
+ };
2077
+ function installTargetForCli(cli) {
2078
+ return SHADOW_INSTALL_TARGETS[cli];
2079
+ }
2063
2080
  function formatShadowLine(server) {
2064
2081
  const shadows = resolveShadowedClis(server);
2065
2082
  if (shadows.length === 0) return null;
@@ -3987,7 +4004,7 @@ async function runUpgrade(opts = {}) {
3987
4004
  return { exitCode: 3, lines };
3988
4005
  }
3989
4006
  function readCurrentVersion() {
3990
- return true ? "0.64.2" : "dev";
4007
+ return true ? "0.66.0" : "dev";
3991
4008
  }
3992
4009
 
3993
4010
  // src/usage-hints.ts
@@ -4049,7 +4066,7 @@ function selectFlakyNamespaces(entries, limit) {
4049
4066
  }
4050
4067
 
4051
4068
  // src/doctor-cmd.ts
4052
- var VERSION = true ? "0.64.2" : "dev";
4069
+ var VERSION = true ? "0.66.0" : "dev";
4053
4070
  function isPersistenceDisabled(env) {
4054
4071
  const raw = env.YAW_MCP_DISABLE_PERSISTENCE;
4055
4072
  return raw !== void 0 && raw !== "" && (raw === "1" || raw.toLowerCase() === "true");
@@ -5663,13 +5680,85 @@ function isFileNotFound2(err) {
5663
5680
  }
5664
5681
 
5665
5682
  // src/secrets-cmd.ts
5683
+ import { existsSync as existsSync6 } from "fs";
5684
+ import { homedir as homedir15 } from "os";
5685
+
5686
+ // src/secrets-audit.ts
5666
5687
  import { existsSync as existsSync5 } from "fs";
5667
- import { homedir as homedir14 } from "os";
5688
+ import { appendFile as appendFile2, chmod as chmod4, readFile as readFile9, writeFile } from "fs/promises";
5689
+ import { homedir as homedir13 } from "os";
5690
+ import { join as join10 } from "path";
5691
+ var SECRETS_AUDIT_FILENAME = "secrets-audit.log";
5692
+ var AUDIT_TAIL_CAP = 5e3;
5693
+ function auditLogPath(home = homedir13()) {
5694
+ return join10(home, CONFIG_DIRNAME, SECRETS_AUDIT_FILENAME);
5695
+ }
5696
+ async function appendAuditEvent(input, home = homedir13()) {
5697
+ try {
5698
+ const event = {
5699
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
5700
+ server: input.server,
5701
+ secret: input.secret,
5702
+ event: input.event
5703
+ };
5704
+ const path5 = auditLogPath(home);
5705
+ const line = `${JSON.stringify(event)}
5706
+ `;
5707
+ if (!existsSync5(path5)) {
5708
+ await atomicWriteFile(path5, line);
5709
+ } else {
5710
+ await appendFile2(path5, line, "utf8");
5711
+ }
5712
+ if (process.platform !== "win32") {
5713
+ await chmod4(path5, 384).catch(() => void 0);
5714
+ }
5715
+ await trimToTailCap(path5);
5716
+ } catch {
5717
+ }
5718
+ }
5719
+ async function trimToTailCap(path5) {
5720
+ const raw = await readFile9(path5, "utf8");
5721
+ const lines = raw.split("\n").filter((l) => l.length > 0);
5722
+ if (lines.length <= AUDIT_TAIL_CAP) return;
5723
+ const kept = lines.slice(lines.length - AUDIT_TAIL_CAP);
5724
+ await writeFile(path5, `${kept.join("\n")}
5725
+ `, "utf8");
5726
+ }
5727
+ async function readAuditLog(filter = {}, home = homedir13()) {
5728
+ const path5 = auditLogPath(home);
5729
+ if (!existsSync5(path5)) return [];
5730
+ let raw;
5731
+ try {
5732
+ raw = await readFile9(path5, "utf8");
5733
+ } catch {
5734
+ return [];
5735
+ }
5736
+ const out = [];
5737
+ for (const line of raw.split("\n")) {
5738
+ if (line.length === 0) continue;
5739
+ let parsed;
5740
+ try {
5741
+ parsed = JSON.parse(line);
5742
+ } catch {
5743
+ continue;
5744
+ }
5745
+ if (!isAuditEvent(parsed)) continue;
5746
+ if (filter.secret !== void 0 && parsed.secret !== filter.secret) continue;
5747
+ if (filter.server !== void 0 && parsed.server !== filter.server) continue;
5748
+ out.push(parsed);
5749
+ }
5750
+ return out;
5751
+ }
5752
+ function isAuditEvent(v) {
5753
+ if (!v || typeof v !== "object") return false;
5754
+ const e = v;
5755
+ return typeof e.ts === "string" && typeof e.server === "string" && typeof e.secret === "string" && (e.event === "injected" || e.event === "missing");
5756
+ }
5668
5757
 
5669
5758
  // src/secrets-vault.ts
5670
- import { chmod as chmod4, mkdir as mkdir4, readFile as readFile9 } from "fs/promises";
5671
- import { homedir as homedir13 } from "os";
5672
- import { dirname as dirname2, join as join10 } from "path";
5759
+ import { chmod as chmod5, mkdir as mkdir4, readFile as readFile10 } from "fs/promises";
5760
+ import { homedir as homedir14 } from "os";
5761
+ import { dirname as dirname2, join as join11 } from "path";
5673
5762
 
5674
5763
  // src/secrets-crypto.ts
5675
5764
  import { createCipheriv, createDecipheriv, randomBytes, scrypt as scryptCb } from "crypto";
@@ -5728,8 +5817,8 @@ function decryptEntry(entry, key) {
5728
5817
  var SECRETS_FILENAME = "secrets.json";
5729
5818
  var SECRETS_SCHEMA_VERSION = 1;
5730
5819
  var VAULT_CHECK_PLAINTEXT = "yaw-mcp-vault-v1";
5731
- function vaultPath(home = homedir13()) {
5732
- return join10(home, CONFIG_DIRNAME, SECRETS_FILENAME);
5820
+ function vaultPath(home = homedir14()) {
5821
+ return join11(home, CONFIG_DIRNAME, SECRETS_FILENAME);
5733
5822
  }
5734
5823
  function emptyVault() {
5735
5824
  return {
@@ -5741,7 +5830,7 @@ function emptyVault() {
5741
5830
  async function loadVault(path5) {
5742
5831
  let raw;
5743
5832
  try {
5744
- raw = await readFile9(path5, "utf8");
5833
+ raw = await readFile10(path5, "utf8");
5745
5834
  } catch (err) {
5746
5835
  const code = err.code;
5747
5836
  if (code === "ENOENT") return null;
@@ -5786,7 +5875,7 @@ async function saveVault(path5, vault) {
5786
5875
  await mkdir4(dir, { recursive: true });
5787
5876
  if (process.platform !== "win32") {
5788
5877
  try {
5789
- await chmod4(dir, 448);
5878
+ await chmod5(dir, 448);
5790
5879
  } catch {
5791
5880
  }
5792
5881
  }
@@ -5794,7 +5883,7 @@ async function saveVault(path5, vault) {
5794
5883
  `, "utf8", 384, 448);
5795
5884
  if (process.platform !== "win32") {
5796
5885
  try {
5797
- await chmod4(path5, 384);
5886
+ await chmod5(path5, 384);
5798
5887
  } catch {
5799
5888
  }
5800
5889
  }
@@ -5828,6 +5917,44 @@ function ensureCheck(vault, key) {
5828
5917
  if (vault.check) return vault;
5829
5918
  return { ...vault, check: encryptEntry(VAULT_CHECK_PLAINTEXT, key) };
5830
5919
  }
5920
+ async function rotateVault(vault, oldKey, newPassphrase) {
5921
+ if (vault.check) {
5922
+ try {
5923
+ const probe2 = decryptEntry(vault.check, oldKey);
5924
+ if (probe2 !== VAULT_CHECK_PLAINTEXT) {
5925
+ throw new Error("vault check marker did not match expected plaintext");
5926
+ }
5927
+ } catch {
5928
+ throw new Error("rotate aborted: current passphrase is wrong (vault check failed to decrypt)");
5929
+ }
5930
+ }
5931
+ const plaintext = /* @__PURE__ */ new Map();
5932
+ for (const [name, entry] of Object.entries(vault.entries)) {
5933
+ try {
5934
+ plaintext.set(name, decryptEntry(entry, oldKey));
5935
+ } catch {
5936
+ plaintext.clear();
5937
+ throw new Error(`rotate aborted: entry "${name}" failed to decrypt under the current passphrase`);
5938
+ }
5939
+ }
5940
+ const newSalt = generateSalt();
5941
+ const newKey = await deriveKey(newPassphrase, newSalt);
5942
+ try {
5943
+ const entries = {};
5944
+ for (const [name, value] of plaintext) {
5945
+ entries[name] = encryptEntry(value, newKey);
5946
+ }
5947
+ return {
5948
+ version: SECRETS_SCHEMA_VERSION,
5949
+ salt: newSalt.toString("base64"),
5950
+ entries,
5951
+ check: encryptEntry(VAULT_CHECK_PLAINTEXT, newKey)
5952
+ };
5953
+ } finally {
5954
+ plaintext.clear();
5955
+ newKey.fill(0);
5956
+ }
5957
+ }
5831
5958
  function listKeys(vault) {
5832
5959
  return Object.keys(vault.entries).sort();
5833
5960
  }
@@ -5919,6 +6046,19 @@ Actions:
5919
6046
  \`yaw-mcp login\` first. Refuses when the local
5920
6047
  vault has a different salt (different passphrase
5921
6048
  lineage) unless --force is passed.
6049
+ rotate Re-encrypt every entry under a NEW passphrase
6050
+ (fresh salt + derived key). Re-wraps the
6051
+ ENCRYPTION, NOT the underlying token values -- a
6052
+ leaked token is still leaked; rotate it at its
6053
+ source. Reads the current passphrase, then the
6054
+ new one (env YAW_MCP_VAULT_PASSPHRASE_NEW or a
6055
+ confirm-twice TTY prompt). Pass --push to also
6056
+ upload the re-encrypted blob to mcp_secrets.
6057
+ audit [--secret NAME] [--server NS]
6058
+ Show the local secret-resolution audit trail
6059
+ (~/.yaw-mcp/secrets-audit.log): which secret
6060
+ NAMES were injected into (or missing for) which
6061
+ server, and when. Never shows a value.
5922
6062
 
5923
6063
  Flags:
5924
6064
  --json Machine-readable output (where applicable).
@@ -5930,12 +6070,18 @@ Flags:
5930
6070
  --replace (push only) Overwrite even when the remote vault
5931
6071
  salt differs from the local (different passphrase
5932
6072
  lineage). Coordinate with your team first.
6073
+ --push (rotate only) After re-encrypting, push the new
6074
+ blob to mcp_secrets (requires a login session).
6075
+ --secret <name> (audit only) Filter to one secret name.
6076
+ --server <ns> (audit only) Filter to one server namespace.
5933
6077
 
5934
6078
  Passphrase:
5935
6079
  Set YAW_MCP_VAULT_PASSPHRASE in the env, or you will be prompted on
5936
6080
  the controlling TTY. The passphrase derives the encryption key via
5937
6081
  scrypt and is cached in memory for the lifetime of this yaw-mcp
5938
- process; the on-disk vault only ever holds ciphertext.`;
6082
+ process; the on-disk vault only ever holds ciphertext. For rotate, the
6083
+ NEW passphrase comes from YAW_MCP_VAULT_PASSPHRASE_NEW (or a TTY
6084
+ confirm-twice prompt).`;
5939
6085
  function parseSecretsArgs(argv) {
5940
6086
  const opts = {};
5941
6087
  for (let i = 0; i < argv.length; i++) {
@@ -5957,6 +6103,10 @@ function parseSecretsArgs(argv) {
5957
6103
  opts.replace = true;
5958
6104
  continue;
5959
6105
  }
6106
+ if (a === "--push") {
6107
+ opts.push = true;
6108
+ continue;
6109
+ }
5960
6110
  if (a === "--value") {
5961
6111
  const v = argv[++i];
5962
6112
  if (v === void 0 || v.startsWith("-")) {
@@ -5970,13 +6120,31 @@ ${SECRETS_USAGE}`
5970
6120
  opts.value = v;
5971
6121
  continue;
5972
6122
  }
6123
+ if (a === "--secret") {
6124
+ const v = argv[++i];
6125
+ if (v === void 0)
6126
+ return { ok: false, error: `yaw-mcp secrets: --secret requires a value
6127
+
6128
+ ${SECRETS_USAGE}` };
6129
+ opts.secretFilter = v;
6130
+ continue;
6131
+ }
6132
+ if (a === "--server") {
6133
+ const v = argv[++i];
6134
+ if (v === void 0)
6135
+ return { ok: false, error: `yaw-mcp secrets: --server requires a value
6136
+
6137
+ ${SECRETS_USAGE}` };
6138
+ opts.serverFilter = v;
6139
+ continue;
6140
+ }
5973
6141
  if (a.startsWith("-")) {
5974
6142
  return { ok: false, error: `yaw-mcp secrets: unknown flag "${a}"
5975
6143
 
5976
6144
  ${SECRETS_USAGE}` };
5977
6145
  }
5978
6146
  if (!opts.action) {
5979
- if (a !== "set" && a !== "get" && a !== "list" && a !== "remove" && a !== "lock" && a !== "push" && a !== "pull") {
6147
+ if (a !== "set" && a !== "get" && a !== "list" && a !== "remove" && a !== "lock" && a !== "push" && a !== "pull" && a !== "rotate" && a !== "audit") {
5980
6148
  return { ok: false, error: `yaw-mcp secrets: unknown action "${a}"
5981
6149
 
5982
6150
  ${SECRETS_USAGE}` };
@@ -6040,6 +6208,35 @@ async function resolvePassphrase(opts) {
6040
6208
  }
6041
6209
  return null;
6042
6210
  }
6211
+ async function resolveNewPassphrase(opts) {
6212
+ if (opts.newPassphrase !== void 0) return opts.newPassphrase.length > 0 ? opts.newPassphrase : null;
6213
+ const fromEnv = process.env.YAW_MCP_VAULT_PASSPHRASE_NEW;
6214
+ if (typeof fromEnv === "string" && fromEnv.length > 0) {
6215
+ if (fromEnv.length < MIN_PASSPHRASE_WARN_LEN) {
6216
+ const stderr = opts.io?.stderr ?? process.stderr;
6217
+ stderr.write(
6218
+ `yaw-mcp secrets: warning -- the new passphrase is shorter than ${MIN_PASSPHRASE_WARN_LEN} characters; consider a longer passphrase.
6219
+ `
6220
+ );
6221
+ }
6222
+ return fromEnv;
6223
+ }
6224
+ const stdin = opts.io?.stdin ?? process.stdin;
6225
+ const stdout = opts.io?.stdout ?? process.stdout;
6226
+ const isTTY = stdin.isTTY === true && stdout.isTTY === true;
6227
+ if (!isTTY) return null;
6228
+ for (let attempt = 0; attempt < MAX_PASSPHRASE_PROMPTS; attempt++) {
6229
+ const first = await readPassphraseFromTTY(stdin, stdout, "New vault passphrase: ");
6230
+ if (first.length === 0) {
6231
+ stdout.write("Passphrase cannot be empty.\n");
6232
+ continue;
6233
+ }
6234
+ const second = await readPassphraseFromTTY(stdin, stdout, "Confirm new passphrase: ");
6235
+ if (first === second) return first;
6236
+ stdout.write("Passphrases did not match. Try again.\n");
6237
+ }
6238
+ return null;
6239
+ }
6043
6240
  var MAX_PASSPHRASE_PROMPTS = 3;
6044
6241
  var MIN_PASSPHRASE_WARN_LEN = 12;
6045
6242
  function readPassphraseFromTTY(stdin, stdout, prompt = "Vault passphrase: ") {
@@ -6108,7 +6305,7 @@ async function runSecrets(opts, io = {
6108
6305
  out: (s) => process.stdout.write(s),
6109
6306
  err: (s) => process.stderr.write(s)
6110
6307
  }) {
6111
- const home = opts.home ?? homedir14();
6308
+ const home = opts.home ?? homedir15();
6112
6309
  const path5 = vaultPath(home);
6113
6310
  if (opts.action === "lock") {
6114
6311
  lock();
@@ -6123,12 +6320,18 @@ async function runSecrets(opts, io = {
6123
6320
  if (opts.action === "pull") {
6124
6321
  return await runSecretsPull(opts, io);
6125
6322
  }
6323
+ if (opts.action === "rotate") {
6324
+ return await runSecretsRotate(opts, io);
6325
+ }
6326
+ if (opts.action === "audit") {
6327
+ return await runSecretsAudit(opts, io);
6328
+ }
6126
6329
  if (opts.action === "list") {
6127
6330
  const loaded = await safeLoadVault(path5, io, opts.json, "list");
6128
6331
  if (!loaded.ok) return loaded.result;
6129
6332
  const vault2 = loaded.vault;
6130
6333
  const keys = vault2 ? listKeys(vault2) : [];
6131
- if (opts.json) io.out(`${JSON.stringify({ ok: true, vault: existsSync5(path5), keys }, null, 2)}
6334
+ if (opts.json) io.out(`${JSON.stringify({ ok: true, vault: existsSync6(path5), keys }, null, 2)}
6132
6335
  `);
6133
6336
  else if (!vault2) io.out(`No vault at ${path5}. Run \`yaw-mcp secrets set <name>\` to create one.
6134
6337
  `);
@@ -6159,7 +6362,7 @@ async function runSecrets(opts, io = {
6159
6362
  const loadedForMutate = await safeLoadVault(path5, io, opts.json, opts.action ?? "");
6160
6363
  if (!loadedForMutate.ok) return loadedForMutate.result;
6161
6364
  let vault = loadedForMutate.vault ?? newVault();
6162
- const isFresh = !existsSync5(path5);
6365
+ const isFresh = !existsSync6(path5);
6163
6366
  const passphrase = await resolvePassphrase(opts);
6164
6367
  if (passphrase === null) {
6165
6368
  const msg = "Passphrase required. Set YAW_MCP_VAULT_PASSPHRASE or run from a TTY so we can prompt.";
@@ -6261,7 +6464,7 @@ async function runSecrets(opts, io = {
6261
6464
  }
6262
6465
  var MCP_SECRETS_RESOURCE = "mcp_secrets";
6263
6466
  async function runSecretsPush(opts, io) {
6264
- const home = opts.home ?? homedir14();
6467
+ const home = opts.home ?? homedir15();
6265
6468
  const path5 = vaultPath(home);
6266
6469
  const session = await getSession({ home, baseUrl: opts.baseUrl });
6267
6470
  if (!session) {
@@ -6335,7 +6538,7 @@ async function runSecretsPush(opts, io) {
6335
6538
  }
6336
6539
  }
6337
6540
  async function runSecretsPull(opts, io) {
6338
- const home = opts.home ?? homedir14();
6541
+ const home = opts.home ?? homedir15();
6339
6542
  const path5 = vaultPath(home);
6340
6543
  const session = await getSession({ home, baseUrl: opts.baseUrl });
6341
6544
  if (!session) {
@@ -6403,10 +6606,159 @@ async function runSecretsPull(opts, io) {
6403
6606
  return { exitCode: 1 };
6404
6607
  }
6405
6608
  }
6609
+ async function runSecretsRotate(opts, io) {
6610
+ const home = opts.home ?? homedir15();
6611
+ const path5 = vaultPath(home);
6612
+ const vault = await loadVault(path5);
6613
+ if (!vault) {
6614
+ const msg = `No vault at ${path5} to rotate. Run \`yaw-mcp secrets set <name>\` first.`;
6615
+ if (opts.json) io.err(`${JSON.stringify({ ok: false, error: msg })}
6616
+ `);
6617
+ else io.err(`yaw-mcp secrets rotate: ${msg}
6618
+ `);
6619
+ return { exitCode: 1 };
6620
+ }
6621
+ const currentPassphrase = await resolvePassphrase(opts);
6622
+ if (currentPassphrase === null) {
6623
+ const msg = "Current passphrase required. Set YAW_MCP_VAULT_PASSPHRASE or run from a TTY so we can prompt.";
6624
+ if (opts.json) io.err(`${JSON.stringify({ ok: false, error: msg })}
6625
+ `);
6626
+ else io.err(`yaw-mcp secrets rotate: ${msg}
6627
+ `);
6628
+ return { exitCode: 1 };
6629
+ }
6630
+ let oldKey;
6631
+ try {
6632
+ oldKey = await unlock(vault, currentPassphrase);
6633
+ } catch (err) {
6634
+ const msg = err instanceof Error ? err.message : String(err);
6635
+ if (opts.json) io.err(`${JSON.stringify({ ok: false, error: msg })}
6636
+ `);
6637
+ else io.err(`yaw-mcp secrets rotate: ${msg}
6638
+ `);
6639
+ return { exitCode: 1 };
6640
+ }
6641
+ const newPassphrase = await resolveNewPassphrase(opts);
6642
+ if (newPassphrase === null) {
6643
+ const msg = "New passphrase required (and must be confirmed). Set YAW_MCP_VAULT_PASSPHRASE_NEW or run from a TTY so we can prompt.";
6644
+ if (opts.json) io.err(`${JSON.stringify({ ok: false, error: msg })}
6645
+ `);
6646
+ else io.err(`yaw-mcp secrets rotate: ${msg}
6647
+ `);
6648
+ return { exitCode: 1 };
6649
+ }
6650
+ let rotated;
6651
+ try {
6652
+ rotated = await rotateVault(vault, oldKey, newPassphrase);
6653
+ } catch (err) {
6654
+ const msg = err instanceof Error ? err.message : String(err);
6655
+ if (opts.json) io.err(`${JSON.stringify({ ok: false, error: msg })}
6656
+ `);
6657
+ else io.err(`yaw-mcp secrets rotate: ${msg}
6658
+ `);
6659
+ lock();
6660
+ return { exitCode: 1 };
6661
+ }
6662
+ await saveVault(path5, rotated);
6663
+ lock();
6664
+ const count = Object.keys(rotated.entries).length;
6665
+ let pushedVersion = null;
6666
+ if (opts.push) {
6667
+ const session = await getSession({ home, baseUrl: opts.baseUrl });
6668
+ if (!session) {
6669
+ const msg = "Rotated locally, but --push needs a session. Run `yaw-mcp login --key <license-key>` then push.";
6670
+ if (opts.json) io.err(`${JSON.stringify({ ok: true, rotated: true, pushed: false, note: msg })}
6671
+ `);
6672
+ else io.err(`yaw-mcp secrets rotate: ${msg}
6673
+ `);
6674
+ return { exitCode: 0 };
6675
+ }
6676
+ try {
6677
+ const remote = await getResource(MCP_SECRETS_RESOURCE, { home, baseUrl: opts.baseUrl });
6678
+ const result = await putResource(MCP_SECRETS_RESOURCE, remote.version, rotated, {
6679
+ home,
6680
+ baseUrl: opts.baseUrl
6681
+ });
6682
+ pushedVersion = result.version;
6683
+ } catch (err) {
6684
+ if (err instanceof TeamSyncStaleVersionError) {
6685
+ const hint = `Rotated locally. Push skipped -- remote is at v${err.currentVersion}; pull and reconcile, then push.`;
6686
+ if (opts.json) io.err(`${JSON.stringify({ ok: true, rotated: true, pushed: false, note: hint })}
6687
+ `);
6688
+ else io.err(`yaw-mcp secrets rotate: ${hint}
6689
+ `);
6690
+ return { exitCode: 0 };
6691
+ }
6692
+ if (err instanceof TeamSyncAuthError) {
6693
+ const hint = "Rotated locally. Push skipped -- session expired. Run `yaw-mcp login` again, then push.";
6694
+ if (opts.json) io.err(`${JSON.stringify({ ok: true, rotated: true, pushed: false, note: hint })}
6695
+ `);
6696
+ else io.err(`yaw-mcp secrets rotate: ${hint}
6697
+ `);
6698
+ return { exitCode: 0 };
6699
+ }
6700
+ const message = err instanceof Error ? err.message : String(err);
6701
+ if (opts.json) io.err(`${JSON.stringify({ ok: true, rotated: true, pushed: false, error: message })}
6702
+ `);
6703
+ else io.err(`yaw-mcp secrets rotate: rotated locally but push failed: ${message}
6704
+ `);
6705
+ return { exitCode: 0 };
6706
+ }
6707
+ }
6708
+ if (opts.json) {
6709
+ io.out(
6710
+ `${JSON.stringify({ ok: true, rotated: true, secret_count: count, pushed: pushedVersion !== null, ...pushedVersion !== null ? { new_version: pushedVersion } : {} })}
6711
+ `
6712
+ );
6713
+ } else {
6714
+ io.out(
6715
+ `Rotated ${count} secret${count === 1 ? "" : "s"} under a new passphrase (encryption re-wrapped, token values unchanged).
6716
+ `
6717
+ );
6718
+ if (pushedVersion !== null) io.out(`Pushed the re-encrypted vault -> mcp_secrets v${pushedVersion}.
6719
+ `);
6720
+ io.out("Vault locked -- the next secrets command will prompt for the new passphrase.\n");
6721
+ }
6722
+ return { exitCode: 0 };
6723
+ }
6724
+ async function runSecretsAudit(opts, io) {
6725
+ const home = opts.home ?? homedir15();
6726
+ let events;
6727
+ try {
6728
+ events = await readAuditLog(
6729
+ {
6730
+ ...opts.secretFilter !== void 0 ? { secret: opts.secretFilter } : {},
6731
+ ...opts.serverFilter !== void 0 ? { server: opts.serverFilter } : {}
6732
+ },
6733
+ home
6734
+ );
6735
+ } catch (err) {
6736
+ const msg = err instanceof Error ? err.message : String(err);
6737
+ if (opts.json) io.err(`${JSON.stringify({ ok: false, error: msg })}
6738
+ `);
6739
+ else io.err(`yaw-mcp secrets audit: ${msg}
6740
+ `);
6741
+ return { exitCode: 1 };
6742
+ }
6743
+ if (opts.json) {
6744
+ io.out(`${JSON.stringify({ ok: true, count: events.length, events }, null, 2)}
6745
+ `);
6746
+ return { exitCode: 0 };
6747
+ }
6748
+ if (events.length === 0) {
6749
+ io.out("No secret-resolution audit events recorded yet.\n");
6750
+ return { exitCode: 0 };
6751
+ }
6752
+ for (const e of events) {
6753
+ io.out(`${e.ts} ${e.event === "injected" ? "injected" : "missing "} ${e.server} ${e.secret}
6754
+ `);
6755
+ }
6756
+ return { exitCode: 0 };
6757
+ }
6406
6758
 
6407
6759
  // src/server.ts
6408
- import { readFile as readFile11 } from "fs/promises";
6409
- import { homedir as homedir15 } from "os";
6760
+ import { readFile as readFile12 } from "fs/promises";
6761
+ import { homedir as homedir16 } from "os";
6410
6762
  import { isAbsolute as isAbsolute2, relative, resolve as resolve6 } from "path";
6411
6763
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
6412
6764
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -6522,7 +6874,7 @@ function defaultSpawn2(cmd, args) {
6522
6874
  async function maybeAutoUpgrade(deps = {}) {
6523
6875
  const optOut = process.env.YAW_MCP_AUTO_UPGRADE;
6524
6876
  if (optOut === "0" || optOut?.toLowerCase() === "false") return;
6525
- const current = deps.currentVersion ?? (true ? "0.64.2" : "dev");
6877
+ const current = deps.currentVersion ?? (true ? "0.66.0" : "dev");
6526
6878
  if (current === "dev") return;
6527
6879
  const method = (deps.isSeaImpl ? await deps.isSeaImpl() : await detectSea()) ? "binary" : detectInstallMethod(deps.argvPath ?? process.argv[1]);
6528
6880
  const latest = await (deps.fetchLatestImpl ?? fetchLatestVersion2)();
@@ -7028,14 +7380,14 @@ function closestNames(query, candidates, limit) {
7028
7380
  }
7029
7381
 
7030
7382
  // src/guide.ts
7031
- import { readFile as readFile10 } from "fs/promises";
7383
+ import { readFile as readFile11 } from "fs/promises";
7032
7384
  var GUIDE_READ_TIMEOUT_MS = 1e3;
7033
7385
  async function readGuide(path5, scope) {
7034
7386
  let raw;
7035
7387
  const ac = new AbortController();
7036
7388
  const timer = setTimeout(() => ac.abort(new Error("guide read timeout")), GUIDE_READ_TIMEOUT_MS);
7037
7389
  try {
7038
- raw = await readFile10(path5, { encoding: "utf8", signal: ac.signal });
7390
+ raw = await readFile11(path5, { encoding: "utf8", signal: ac.signal });
7039
7391
  } catch (err) {
7040
7392
  const isTimeout = err instanceof Error && err.code === "ABORT_ERR";
7041
7393
  if (isTimeout) {
@@ -7255,6 +7607,63 @@ function pushToolCall(history, record, limit = HISTORY_LIMIT) {
7255
7607
  return history;
7256
7608
  }
7257
7609
 
7610
+ // src/install-nudge.ts
7611
+ import { existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
7612
+ import { dirname as dirname4, join as join12 } from "path";
7613
+ var INSTALL_NUDGE_MIN_COUNT = 5;
7614
+ var INSTALL_NUDGE_COOLDOWN_MS = 7 * 24 * 60 * 60 * 1e3;
7615
+ var INSTALL_NUDGE_STATE_FILENAME = "install-nudge-state.json";
7616
+ function installNudgeStatePath(home) {
7617
+ return join12(home, CONFIG_DIRNAME, INSTALL_NUDGE_STATE_FILENAME);
7618
+ }
7619
+ function installNudgeEnabled(env, config) {
7620
+ if (env.YAW_MCP_INSTALL_NUDGE === "1") return true;
7621
+ if (config?.installNudge === true) return true;
7622
+ return false;
7623
+ }
7624
+ function readState(home) {
7625
+ const path5 = installNudgeStatePath(home);
7626
+ if (!existsSync7(path5)) return { nudges: [] };
7627
+ try {
7628
+ const parsed = JSON.parse(readFileSync4(path5, "utf8"));
7629
+ if (!parsed || typeof parsed !== "object") return { nudges: [] };
7630
+ const raw = parsed.nudges;
7631
+ if (!Array.isArray(raw)) return { nudges: [] };
7632
+ const nudges = [];
7633
+ for (const entry of raw) {
7634
+ if (entry && typeof entry === "object" && typeof entry.cli === "string" && typeof entry.nudgedAt === "number") {
7635
+ nudges.push({ cli: entry.cli, nudgedAt: entry.nudgedAt });
7636
+ }
7637
+ }
7638
+ return { nudges };
7639
+ } catch {
7640
+ return { nudges: [] };
7641
+ }
7642
+ }
7643
+ function shouldNudge(cli, home, now = Date.now) {
7644
+ const state = readState(home);
7645
+ const rec = state.nudges.find((n) => n.cli === cli);
7646
+ if (!rec) return true;
7647
+ return now() - rec.nudgedAt >= INSTALL_NUDGE_COOLDOWN_MS;
7648
+ }
7649
+ function recordNudge(cli, home, now = Date.now) {
7650
+ try {
7651
+ const at = now();
7652
+ const state = readState(home);
7653
+ const kept = state.nudges.filter((n) => n.cli !== cli && at - n.nudgedAt < INSTALL_NUDGE_COOLDOWN_MS);
7654
+ kept.push({ cli, nudgedAt: at });
7655
+ const path5 = installNudgeStatePath(home);
7656
+ mkdirSync2(dirname4(path5), { recursive: true });
7657
+ writeFileSync2(path5, `${JSON.stringify({ nudges: kept }, null, 2)}
7658
+ `, "utf8");
7659
+ } catch (err) {
7660
+ log("debug", "install-nudge: failed to record nudge state", {
7661
+ cli,
7662
+ error: err instanceof Error ? err.message : String(err)
7663
+ });
7664
+ }
7665
+ }
7666
+
7258
7667
  // src/learning.ts
7259
7668
  var LEARNING_MIN_OBSERVATIONS = 3;
7260
7669
  var LEARNING_MAX_BOOST = 1.1;
@@ -7647,6 +8056,26 @@ var META_TOOLS = {
7647
8056
  openWorldHint: false
7648
8057
  }
7649
8058
  },
8059
+ secrets: {
8060
+ name: "mcp_connect_secrets",
8061
+ 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).",
8062
+ inputSchema: {
8063
+ type: "object",
8064
+ properties: {
8065
+ server: {
8066
+ type: "string",
8067
+ description: 'Optional: restrict the report to a single server namespace (e.g. "gh"). Omit to report every installed server that references a vault secret.'
8068
+ }
8069
+ }
8070
+ },
8071
+ annotations: {
8072
+ title: "Inspect Vault Secret Resolution",
8073
+ readOnlyHint: true,
8074
+ destructiveHint: false,
8075
+ idempotentHint: true,
8076
+ openWorldHint: false
8077
+ }
8078
+ },
7650
8079
  exec: {
7651
8080
  name: "mcp_connect_exec",
7652
8081
  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.",
@@ -7764,6 +8193,30 @@ function buildInstallPayload(args) {
7764
8193
  }
7765
8194
  return { ok: true, payload };
7766
8195
  }
8196
+ var SECRETS_REPORT_REF_RE = /\$\{secret:([a-zA-Z0-9_.-]+)\}/g;
8197
+ function computeSecretsReport(servers, vaultKeys) {
8198
+ const rows = [];
8199
+ for (const server of servers) {
8200
+ const referenced = /* @__PURE__ */ new Set();
8201
+ for (const v of Object.values(server.env ?? {})) {
8202
+ if (typeof v !== "string") continue;
8203
+ for (const m of v.matchAll(SECRETS_REPORT_REF_RE)) referenced.add(m[1]);
8204
+ }
8205
+ if (referenced.size === 0) continue;
8206
+ const injectedSecrets = [];
8207
+ const missing = [];
8208
+ for (const name of referenced) {
8209
+ if (vaultKeys.has(name)) injectedSecrets.push(name);
8210
+ else missing.push(name);
8211
+ }
8212
+ rows.push({
8213
+ server: server.namespace,
8214
+ injectedSecrets: injectedSecrets.sort(),
8215
+ missing: missing.sort()
8216
+ });
8217
+ }
8218
+ return rows;
8219
+ }
7767
8220
  var META_TOOL_NAMES = /* @__PURE__ */ new Set([
7768
8221
  META_TOOLS.discover.name,
7769
8222
  META_TOOLS.activate.name,
@@ -7775,7 +8228,8 @@ var META_TOOL_NAMES = /* @__PURE__ */ new Set([
7775
8228
  META_TOOLS.read_tool.name,
7776
8229
  META_TOOLS.suggest.name,
7777
8230
  META_TOOLS.exec.name,
7778
- META_TOOLS.bundles.name
8231
+ META_TOOLS.bundles.name,
8232
+ META_TOOLS.secrets.name
7779
8233
  ]);
7780
8234
 
7781
8235
  // src/pack-detect.ts
@@ -8973,6 +9427,62 @@ import {
8973
9427
  ToolListChangedNotificationSchema
8974
9428
  } from "@modelcontextprotocol/sdk/types.js";
8975
9429
 
9430
+ // src/oam-spawn.ts
9431
+ import { execFileSync } from "child_process";
9432
+ import { createRequire } from "module";
9433
+ var requireFrom = createRequire(import.meta.url);
9434
+ function packageName(spec) {
9435
+ const start = spec.startsWith("@") ? 1 : 0;
9436
+ const at = spec.indexOf("@", start);
9437
+ return at === -1 ? spec : spec.slice(0, at);
9438
+ }
9439
+ var oamBinCache;
9440
+ function oamBin() {
9441
+ if (oamBinCache !== void 0) return oamBinCache;
9442
+ const bin = process.env.OAM_BIN || (process.platform === "win32" ? "oam.exe" : "oam");
9443
+ try {
9444
+ execFileSync(bin, ["--version"], { stdio: "ignore" });
9445
+ oamBinCache = bin;
9446
+ } catch {
9447
+ oamBinCache = null;
9448
+ }
9449
+ return oamBinCache;
9450
+ }
9451
+ function rewriteForOam(command, args, deps) {
9452
+ const bin = deps.oamBin;
9453
+ if (!bin) return { command, args };
9454
+ const toOam = (entry, rest) => ({
9455
+ command: bin,
9456
+ args: rest.length > 0 ? ["run", entry, "--", ...rest] : ["run", entry]
9457
+ });
9458
+ if (command === "node") {
9459
+ const [entry, ...rest] = args;
9460
+ if (!entry) return { command, args };
9461
+ return toOam(entry, rest);
9462
+ }
9463
+ if (command === "npx") {
9464
+ const positional = args.filter((a) => a !== "-y" && a !== "--yes");
9465
+ const [spec, ...rest] = positional;
9466
+ if (!spec) return { command, args };
9467
+ const entry = deps.resolveEntry(packageName(spec));
9468
+ if (!entry) return { command, args };
9469
+ return toOam(entry, rest);
9470
+ }
9471
+ return { command, args };
9472
+ }
9473
+ function resolveOamSpawn(command, args) {
9474
+ return rewriteForOam(command, args, {
9475
+ oamBin: oamBin(),
9476
+ resolveEntry: (pkg) => {
9477
+ try {
9478
+ return requireFrom.resolve(pkg);
9479
+ } catch {
9480
+ return null;
9481
+ }
9482
+ }
9483
+ });
9484
+ }
9485
+
8976
9486
  // src/uv-bootstrap.ts
8977
9487
  import { spawn as spawn5 } from "child_process";
8978
9488
  import { createHash as createHash3 } from "crypto";
@@ -9213,7 +9723,7 @@ async function resolveUvSpawn(command, args) {
9213
9723
  }
9214
9724
 
9215
9725
  // src/upstream.ts
9216
- async function resolveServerEnv(env) {
9726
+ async function resolveServerEnv(env, namespace) {
9217
9727
  if (!hasSecretRefs(env)) return env;
9218
9728
  const refKeys = Object.entries(env).filter(([, v]) => typeof v === "string" && v.includes("${secret:")).map(([k]) => k);
9219
9729
  const passphrase = process.env.YAW_MCP_VAULT_PASSPHRASE;
@@ -9235,8 +9745,36 @@ async function resolveServerEnv(env) {
9235
9745
  if (missing.length > 0) {
9236
9746
  throw new Error(`vault: missing or undecryptable secret refs: ${missing.join(", ")}`);
9237
9747
  }
9748
+ try {
9749
+ await recordResolveAudit(namespace, env, missing);
9750
+ } catch (auditErr) {
9751
+ log("warn", "Failed to record secret-resolve audit (non-fatal)", {
9752
+ namespace,
9753
+ error: auditErr instanceof Error ? auditErr.message : String(auditErr)
9754
+ });
9755
+ }
9238
9756
  return resolved;
9239
9757
  }
9758
+ async function recordResolveAudit(namespace, env, missing) {
9759
+ const missingSet = new Set(missing);
9760
+ const referenced = collectSecretNames(env);
9761
+ for (const name of referenced) {
9762
+ if (missingSet.has(name)) continue;
9763
+ await appendAuditEvent({ server: namespace, secret: name, event: "injected" });
9764
+ }
9765
+ for (const name of missingSet) {
9766
+ await appendAuditEvent({ server: namespace, secret: name, event: "missing" });
9767
+ }
9768
+ }
9769
+ function collectSecretNames(env) {
9770
+ const names = /* @__PURE__ */ new Set();
9771
+ const re = /\$\{secret:([a-zA-Z0-9_.-]+)\}/g;
9772
+ for (const v of Object.values(env)) {
9773
+ if (typeof v !== "string") continue;
9774
+ for (const m of v.matchAll(re)) names.add(m[1]);
9775
+ }
9776
+ return [...names];
9777
+ }
9240
9778
  var DEFAULT_CONNECT_TIMEOUT = (() => {
9241
9779
  const env = process.env.MCP_CONNECT_TIMEOUT;
9242
9780
  if (!env) return 15e3;
@@ -9284,7 +9822,7 @@ function categorizeSpawnError(err) {
9284
9822
  }
9285
9823
  async function connectToUpstream(config, onDisconnect, onListChanged) {
9286
9824
  const client = new Client(
9287
- { name: "yaw-mcp", version: true ? "0.64.2" : "dev" },
9825
+ { name: "yaw-mcp", version: true ? "0.66.0" : "dev" },
9288
9826
  { capabilities: {} }
9289
9827
  );
9290
9828
  let transport;
@@ -9299,8 +9837,11 @@ async function connectToUpstream(config, onDisconnect, onListChanged) {
9299
9837
  YAW_MCP_VAULT_PASSPHRASE: _excludedVaultPassphrase,
9300
9838
  ...parentEnv
9301
9839
  } = process.env;
9302
- const resolved = await resolveUvSpawn(config.command, config.args ?? []);
9303
- const serverEnv = await resolveServerEnv(config.env ?? {});
9840
+ let resolved = await resolveUvSpawn(config.command, config.args ?? []);
9841
+ if (config.runtime === "oam") {
9842
+ resolved = resolveOamSpawn(resolved.command, resolved.args);
9843
+ }
9844
+ const serverEnv = await resolveServerEnv(config.env ?? {}, config.namespace);
9304
9845
  resolvedServerEnv = serverEnv;
9305
9846
  const stdioTransport = new StdioClientTransport({
9306
9847
  command: resolved.command,
@@ -9616,7 +10157,7 @@ var ConnectServer = class _ConnectServer {
9616
10157
  this.apiUrl = apiUrl5;
9617
10158
  this.token = token5;
9618
10159
  this.server = new Server(
9619
- { name: "yaw-mcp", version: true ? "0.64.2" : "dev" },
10160
+ { name: "yaw-mcp", version: true ? "0.66.0" : "dev" },
9620
10161
  {
9621
10162
  capabilities: {
9622
10163
  tools: { listChanged: true },
@@ -9662,6 +10203,19 @@ var ConnectServer = class _ConnectServer {
9662
10203
  // same namespace, on deactivate, and on config reconcile.
9663
10204
  toolFilters = /* @__PURE__ */ new Map();
9664
10205
  profile = null;
10206
+ // Shadow-driven install-nudge gate. Resolved once at start() from the
10207
+ // env override (YAW_MCP_INSTALL_NUDGE=1) OR config (installNudge: true);
10208
+ // off by default. When false, discover NEVER runs the shell-history scan
10209
+ // and its output is byte-identical to today (the load-bearing privacy
10210
+ // property). See install-nudge.ts. Stays false in unit tests that skip
10211
+ // start(), so the nudge block is opt-in there too.
10212
+ installNudge = false;
10213
+ // home / env used by the install-nudge shell-history scan. Default to the
10214
+ // real process values; overridable so tests can point the scan at a
10215
+ // synthetic home + stubbed env without touching the developer's real
10216
+ // shell history or ~/.yaw-mcp/ state file.
10217
+ nudgeHome = homedir16();
10218
+ nudgeEnv = process.env;
9665
10219
  // Loaded YAW-MCP.md guides (user-global + project-local). Null until
9666
10220
  // start() has run the loader; fail-open if either file is missing,
9667
10221
  // unreadable, or empty.
@@ -9886,7 +10440,8 @@ var ConnectServer = class _ConnectServer {
9886
10440
  }
9887
10441
  this.persistenceReady = true;
9888
10442
  }
9889
- this.profile = await loadEffectiveProfile(process.cwd()).catch(() => null);
10443
+ const resolvedConfig = await loadYawMcpConfig({ cwd: process.cwd() }).catch(() => null);
10444
+ this.profile = resolvedConfig ? toProfile(resolvedConfig) : null;
9890
10445
  if (this.profile) {
9891
10446
  log("info", "Loaded profile", {
9892
10447
  path: this.profile.path,
@@ -9895,6 +10450,10 @@ var ConnectServer = class _ConnectServer {
9895
10450
  block: this.profile.blocked
9896
10451
  });
9897
10452
  }
10453
+ this.installNudge = installNudgeEnabled(process.env, resolvedConfig);
10454
+ if (this.installNudge) {
10455
+ log("info", "Shadow-driven install nudge enabled");
10456
+ }
9898
10457
  this.guides = await loadGuides(process.cwd()).catch(() => ({ user: null, project: null }));
9899
10458
  if (this.guides.user || this.guides.project) {
9900
10459
  log("info", "Loaded YAW-MCP.md guide", {
@@ -10198,6 +10757,17 @@ var ConnectServer = class _ConnectServer {
10198
10757
  recordConnectEvent({ namespace: null, toolName: null, action: "bundles", latencyMs: null, success: true });
10199
10758
  return this.attachGuideNudge(this.handleBundles(action));
10200
10759
  }
10760
+ if (name === META_TOOLS.secrets.name) {
10761
+ const serverArg = typeof args.server === "string" ? args.server : void 0;
10762
+ recordConnectEvent({
10763
+ namespace: serverArg ?? null,
10764
+ toolName: null,
10765
+ action: "secrets",
10766
+ latencyMs: null,
10767
+ success: true
10768
+ });
10769
+ return this.attachGuideNudge(await this.handleSecretsReport(serverArg));
10770
+ }
10201
10771
  let routes = this.toolRoutes;
10202
10772
  let route = routes.get(name);
10203
10773
  if (route?.deferred) {
@@ -10678,6 +11248,7 @@ var ConnectServer = class _ConnectServer {
10678
11248
  lines.push(` ${server.namespace} \u2014 ${server.name} (disabled in dashboard)`);
10679
11249
  }
10680
11250
  }
11251
+ lines.push(...this.buildInstallCandidatesLines(activeServers));
10681
11252
  const activeCount = this.connections.size;
10682
11253
  const totalTools = Array.from(this.connections.values()).reduce((sum, c) => {
10683
11254
  const f = this.toolFilters.get(c.config.namespace);
@@ -10696,6 +11267,51 @@ ${activeCount} loaded in this session, ${totalTools} tools in context${tokenSumm
10696
11267
  }
10697
11268
  return { content: [{ type: "text", text: lines.join("\n") }] };
10698
11269
  }
11270
+ // Build the opt-in "Install candidates" block from the offline shell-
11271
+ // history shadow scan. Returns [] (no lines, byte-identical output) when
11272
+ // the gate is off — the load-bearing privacy property: with the gate
11273
+ // unset the scan never runs and nothing about shell history is read.
11274
+ //
11275
+ // When ON, for each heavily-used CLI the scan found:
11276
+ // - skip unless count >= INSTALL_NUDGE_MIN_COUNT (noise floor),
11277
+ // - skip unless a FIRST-PARTY install target exists (installTargetForCli;
11278
+ // a CLI like kubectl/npm/ssh with no target produces no nudge),
11279
+ // - skip if ANY namespace the CLI maps to is already installed (the user
11280
+ // already has a server that covers it — intersect the hit's namespaces
11281
+ // with the installed set),
11282
+ // - skip if the per-CLI cooldown hasn't elapsed (shouldNudge).
11283
+ // Surviving CLIs are recorded (recordNudge) so they stay suppressed for
11284
+ // the cooldown, and rendered as one line + an mcp_connect_install sketch.
11285
+ //
11286
+ // Privacy: the only data emitted is the aggregate integer count + the
11287
+ // first-party package / namespace / name. No raw history line, command
11288
+ // text, or argument ever reaches this output, and nothing here is sent to
11289
+ // analytics — scanShellHistoryForShadows is local-only and returns just
11290
+ // { cli, count, namespaces }.
11291
+ buildInstallCandidatesLines(activeServers) {
11292
+ if (!this.installNudge) return [];
11293
+ const installedNamespaces = new Set(activeServers.map((s) => s.namespace));
11294
+ const hits = scanShellHistoryForShadows({ home: this.nudgeHome, env: this.nudgeEnv });
11295
+ const candidates = [];
11296
+ for (const hit of hits) {
11297
+ if (hit.count < INSTALL_NUDGE_MIN_COUNT) continue;
11298
+ if (hit.namespaces.some((ns) => installedNamespaces.has(ns))) continue;
11299
+ const target = installTargetForCli(hit.cli);
11300
+ if (!target) continue;
11301
+ if (installedNamespaces.has(target.namespace)) continue;
11302
+ if (!shouldNudge(hit.cli, this.nudgeHome)) continue;
11303
+ candidates.push({ cli: hit.cli, count: hit.count, target });
11304
+ }
11305
+ if (candidates.length === 0) return [];
11306
+ const lines = ["\nInstall candidates (from your recent shell usage; history stays local):"];
11307
+ for (const { cli, count, target } of candidates) {
11308
+ lines.push(` ${cli.padEnd(10)} (ran ${count}x recently) -> install ${target.package}`);
11309
+ const sketch = `mcp_connect_install({ name: ${JSON.stringify(target.name)}, namespace: ${JSON.stringify(target.namespace)}, type: "local", command: "npx", args: ["-y", ${JSON.stringify(target.package)}] })`;
11310
+ lines.push(` ${sketch}`);
11311
+ recordNudge(cli, this.nudgeHome);
11312
+ }
11313
+ return lines;
11314
+ }
10699
11315
  // Activate a single server by namespace. Shared by handleActivate,
10700
11316
  // handleDispatch, and handleDiscoverWithAutoWarm so error handling,
10701
11317
  // retries, caching, and tool-report round-trips live in one place.
@@ -11293,7 +11909,7 @@ ${activeCount} loaded in this session, ${totalTools} tools in context${tokenSumm
11293
11909
  }
11294
11910
  const ALLOWED_FILENAMES = ["claude_desktop_config.json", "mcp.json", "settings.json", "mcp_config.json"];
11295
11911
  try {
11296
- const resolved = filepath.startsWith("~/") || filepath.startsWith("~\\") ? resolve6(homedir15(), filepath.slice(2)) : resolve6(filepath);
11912
+ const resolved = filepath.startsWith("~/") || filepath.startsWith("~\\") ? resolve6(homedir16(), filepath.slice(2)) : resolve6(filepath);
11297
11913
  const resolvedBasename = resolved.split(/[/\\]/).pop() || "";
11298
11914
  if (!ALLOWED_FILENAMES.includes(resolvedBasename)) {
11299
11915
  return {
@@ -11310,7 +11926,7 @@ ${activeCount} loaded in this session, ${totalTools} tools in context${tokenSumm
11310
11926
  const rel = relative(base, p);
11311
11927
  return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
11312
11928
  };
11313
- if (!isUnder(homedir15(), resolved) && !isUnder(process.cwd(), resolved)) {
11929
+ if (!isUnder(homedir16(), resolved) && !isUnder(process.cwd(), resolved)) {
11314
11930
  return {
11315
11931
  content: [
11316
11932
  { type: "text", text: "Import path must be under your home directory or the current working directory." }
@@ -11318,7 +11934,7 @@ ${activeCount} loaded in this session, ${totalTools} tools in context${tokenSumm
11318
11934
  isError: true
11319
11935
  };
11320
11936
  }
11321
- const raw = await readFile11(resolved, "utf-8");
11937
+ const raw = await readFile12(resolved, "utf-8");
11322
11938
  const parsed = JSON.parse(raw);
11323
11939
  if (!parsed.mcpServers || typeof parsed.mcpServers !== "object" || Array.isArray(parsed.mcpServers)) {
11324
11940
  return {
@@ -11610,6 +12226,43 @@ Use mcp_connect_discover to see imported servers.`
11610
12226
  );
11611
12227
  }
11612
12228
  }
12229
+ // Values-free preview of which local-vault secrets each installed
12230
+ // server's `${secret:NAME}` env refs resolve to. NAMES ONLY -- this
12231
+ // reads the vault's KEY LIST (listKeys, no unlock, no passphrase) and
12232
+ // the servers' env-reference names, and NEVER calls getSecret /
12233
+ // decryptEntry. Servers with no refs are omitted.
12234
+ async handleSecretsReport(serverArg) {
12235
+ const vault = await loadVault(vaultPath()).catch(() => null);
12236
+ const vaultKeys = new Set(vault ? listKeys(vault) : []);
12237
+ let servers = this.getProfiledActiveServers().map((s) => ({ namespace: s.namespace, env: s.env }));
12238
+ if (serverArg) servers = servers.filter((s) => s.namespace === serverArg);
12239
+ const rows = computeSecretsReport(servers, vaultKeys);
12240
+ if (serverArg && servers.length === 0) {
12241
+ return {
12242
+ content: [
12243
+ {
12244
+ type: "text",
12245
+ text: `No installed server with namespace "${serverArg}". Call mcp_connect_discover to list installed servers.`
12246
+ }
12247
+ ],
12248
+ isError: true
12249
+ };
12250
+ }
12251
+ if (rows.length === 0) {
12252
+ const scope = serverArg ? `Server "${serverArg}"` : "No installed server";
12253
+ return {
12254
+ content: [
12255
+ {
12256
+ type: "text",
12257
+ 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>\`.`
12258
+ }
12259
+ ]
12260
+ };
12261
+ }
12262
+ return {
12263
+ content: [{ type: "text", text: JSON.stringify(rows, null, 2) }]
12264
+ };
12265
+ }
11613
12266
  handleHealth() {
11614
12267
  const lines = [];
11615
12268
  if (this.profile) {
@@ -12122,21 +12775,21 @@ function truncateVersion(v) {
12122
12775
  }
12123
12776
 
12124
12777
  // src/set-active-cmd.ts
12125
- import { homedir as homedir16 } from "os";
12778
+ import { homedir as homedir17 } from "os";
12126
12779
 
12127
12780
  // src/sync-state.ts
12128
- import { existsSync as existsSync6 } from "fs";
12129
- import { mkdir as mkdir5, readFile as readFile12 } from "fs/promises";
12130
- import { dirname as dirname4, join as join11 } from "path";
12781
+ import { existsSync as existsSync8 } from "fs";
12782
+ import { mkdir as mkdir5, readFile as readFile13 } from "fs/promises";
12783
+ import { dirname as dirname5, join as join13 } from "path";
12131
12784
  var SYNC_STATE_FILENAME = "sync-state.json";
12132
12785
  function syncStatePath(home) {
12133
- return join11(home, CONFIG_DIRNAME, SYNC_STATE_FILENAME);
12786
+ return join13(home, CONFIG_DIRNAME, SYNC_STATE_FILENAME);
12134
12787
  }
12135
12788
  async function readSyncState(home) {
12136
12789
  const path5 = syncStatePath(home);
12137
- if (!existsSync6(path5)) return {};
12790
+ if (!existsSync8(path5)) return {};
12138
12791
  try {
12139
- const raw = await readFile12(path5, "utf8");
12792
+ const raw = await readFile13(path5, "utf8");
12140
12793
  const parsed = JSON.parse(raw);
12141
12794
  if (!parsed || typeof parsed !== "object") return {};
12142
12795
  return parsed;
@@ -12146,7 +12799,7 @@ async function readSyncState(home) {
12146
12799
  }
12147
12800
  async function writeSyncState(home, state) {
12148
12801
  const path5 = syncStatePath(home);
12149
- await mkdir5(dirname4(path5), { recursive: true });
12802
+ await mkdir5(dirname5(path5), { recursive: true });
12150
12803
  const existing = await readSyncState(home);
12151
12804
  const merged = { ...existing, ...state };
12152
12805
  await atomicWriteFile(path5, `${JSON.stringify(merged, null, 2)}
@@ -12240,7 +12893,7 @@ async function runSetActive(opts, io = { out: (s) => process.stdout.write(s), er
12240
12893
  base
12241
12894
  );
12242
12895
  if (typeof putRes.version === "number") {
12243
- await deps.writeSyncState(opts.home ?? homedir16(), {
12896
+ await deps.writeSyncState(opts.home ?? homedir17(), {
12244
12897
  mcp_bundles: { lastPulledVersion: putRes.version }
12245
12898
  }).catch(() => {
12246
12899
  });
@@ -12286,7 +12939,7 @@ function fail(io, json, message, code) {
12286
12939
  }
12287
12940
 
12288
12941
  // src/stats-cmd.ts
12289
- import { homedir as homedir17 } from "os";
12942
+ import { homedir as homedir18 } from "os";
12290
12943
  var STATS_USAGE = `Usage: yaw-mcp stats [--json] [--limit N] [--days N]
12291
12944
 
12292
12945
  Print a digest of recent AI tool calls recorded against your Yaw
@@ -12410,7 +13063,7 @@ async function runStats(opts, io = {
12410
13063
  out: (s) => process.stdout.write(s),
12411
13064
  err: (s) => process.stderr.write(s)
12412
13065
  }) {
12413
- const home = opts.home ?? homedir17();
13066
+ const home = opts.home ?? homedir18();
12414
13067
  const session = await getSession({ home, baseUrl: opts.baseUrl });
12415
13068
  if (!session) {
12416
13069
  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>";
@@ -12511,10 +13164,10 @@ function suggestFlag(input, limit = 2) {
12511
13164
  }
12512
13165
 
12513
13166
  // src/sync-cmd.ts
12514
- import { existsSync as existsSync7 } from "fs";
12515
- import { mkdir as mkdir6, readFile as readFile13 } from "fs/promises";
12516
- import { homedir as homedir18 } from "os";
12517
- import { dirname as dirname5, join as join12 } from "path";
13167
+ import { existsSync as existsSync9 } from "fs";
13168
+ import { mkdir as mkdir6, readFile as readFile14 } from "fs/promises";
13169
+ import { homedir as homedir19 } from "os";
13170
+ import { dirname as dirname6, join as join14 } from "path";
12518
13171
  var SYNC_USAGE = `Usage: yaw-mcp sync <push|pull|status> [--json]
12519
13172
 
12520
13173
  Replicate ~/.yaw-mcp/bundles.json across machines via your Yaw
@@ -12561,12 +13214,12 @@ ${SYNC_USAGE}` };
12561
13214
  return { ok: true, options: opts };
12562
13215
  }
12563
13216
  function bundlesPath(home) {
12564
- return join12(home, CONFIG_DIRNAME, BUNDLES_FILENAME2);
13217
+ return join14(home, CONFIG_DIRNAME, BUNDLES_FILENAME2);
12565
13218
  }
12566
13219
  async function readLocalBundles(home) {
12567
13220
  const path5 = bundlesPath(home);
12568
- if (!existsSync7(path5)) return { version: 1, servers: [] };
12569
- const raw = await readFile13(path5, "utf8");
13221
+ if (!existsSync9(path5)) return { version: 1, servers: [] };
13222
+ const raw = await readFile14(path5, "utf8");
12570
13223
  let parsed;
12571
13224
  try {
12572
13225
  parsed = JSON.parse(raw);
@@ -12581,7 +13234,7 @@ async function readLocalBundles(home) {
12581
13234
  }
12582
13235
  async function writeLocalBundles(home, file) {
12583
13236
  const path5 = bundlesPath(home);
12584
- await mkdir6(dirname5(path5), { recursive: true });
13237
+ await mkdir6(dirname6(path5), { recursive: true });
12585
13238
  await atomicWriteFile(path5, `${JSON.stringify(file, null, 2)}
12586
13239
  `);
12587
13240
  return path5;
@@ -12617,7 +13270,7 @@ async function runSync(opts, io = {
12617
13270
  out: (s) => process.stdout.write(s),
12618
13271
  err: (s) => process.stderr.write(s)
12619
13272
  }) {
12620
- const home = opts.home ?? homedir18();
13273
+ const home = opts.home ?? homedir19();
12621
13274
  const session = await getSession({ home, baseUrl: opts.baseUrl });
12622
13275
  if (!session) {
12623
13276
  const msg = "Not signed in. Run `yaw-mcp login --key <license-key>` first.";
@@ -13192,7 +13845,7 @@ if (subcommand === "compliance") {
13192
13845
  `);
13193
13846
  process.exit(0);
13194
13847
  } else if (subcommand === "--version" || subcommand === "-V") {
13195
- process.stdout.write(`yaw-mcp ${true ? "0.64.2" : "dev"}
13848
+ process.stdout.write(`yaw-mcp ${true ? "0.66.0" : "dev"}
13196
13849
  `);
13197
13850
  process.exit(0);
13198
13851
  } else if (subcommand && !subcommand.startsWith("-")) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yawlabs/mcp",
3
- "version": "0.64.2",
3
+ "version": "0.66.0",
4
4
  "mcpName": "io.github.YawLabs/mcp",
5
5
  "description": "Yaw MCP -- MCP servers, managed. Free to run locally; Yaw Team adds cross-machine sync.",
6
6
  "license": "UNLICENSED",