@yawlabs/mcp 0.60.6 → 0.62.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -17,7 +17,7 @@ import {
17
17
  signIn,
18
18
  signOut,
19
19
  userConfigDir
20
- } from "./chunk-I5625HJE.js";
20
+ } from "./chunk-WORQOSXT.js";
21
21
 
22
22
  // src/audit-cmd.ts
23
23
  import { homedir as homedir3 } from "os";
@@ -73,9 +73,49 @@ function stripJsoncComments(src) {
73
73
  }
74
74
  return out;
75
75
  }
76
+ function stripTrailingCommas(src) {
77
+ let out = "";
78
+ let i = 0;
79
+ const len = src.length;
80
+ let inString = false;
81
+ let stringChar = "";
82
+ while (i < len) {
83
+ const c = src[i];
84
+ if (inString) {
85
+ out += c;
86
+ if (c === "\\" && i + 1 < len) {
87
+ out += src[i + 1];
88
+ i += 2;
89
+ continue;
90
+ }
91
+ if (c === stringChar) inString = false;
92
+ i++;
93
+ continue;
94
+ }
95
+ if (c === '"' || c === "'") {
96
+ inString = true;
97
+ stringChar = c;
98
+ out += c;
99
+ i++;
100
+ continue;
101
+ }
102
+ if (c === ",") {
103
+ let j = i + 1;
104
+ while (j < len && (src[j] === " " || src[j] === " " || src[j] === "\r" || src[j] === "\n")) j++;
105
+ if (j < len && (src[j] === "]" || src[j] === "}")) {
106
+ out += " ";
107
+ i++;
108
+ continue;
109
+ }
110
+ }
111
+ out += c;
112
+ i++;
113
+ }
114
+ return out;
115
+ }
76
116
  function parseJsonc(src) {
77
117
  const debommed = src.charCodeAt(0) === 65279 ? src.slice(1) : src;
78
- const stripped = stripJsoncComments(debommed);
118
+ const stripped = stripTrailingCommas(stripJsoncComments(debommed));
79
119
  return JSON.parse(stripped);
80
120
  }
81
121
 
@@ -97,10 +137,10 @@ function validateEntry(entry) {
97
137
  return { grade, score, gradedAt };
98
138
  }
99
139
  async function readGradesCache(home = homedir()) {
100
- const path3 = gradesCachePath(home);
140
+ const path5 = gradesCachePath(home);
101
141
  let raw;
102
142
  try {
103
- raw = await readFile(path3, "utf8");
143
+ raw = await readFile(path5, "utf8");
104
144
  } catch {
105
145
  return {};
106
146
  }
@@ -109,7 +149,7 @@ async function readGradesCache(home = homedir()) {
109
149
  parsed = parseJsonc(raw);
110
150
  } catch (err) {
111
151
  log("warn", "grades.json is not valid JSON; ignoring", {
112
- path: path3,
152
+ path: path5,
113
153
  error: err instanceof Error ? err.message : String(err)
114
154
  });
115
155
  return {};
@@ -123,12 +163,12 @@ async function readGradesCache(home = homedir()) {
123
163
  return out;
124
164
  }
125
165
  async function writeGrade(namespace, grade, home = homedir()) {
126
- const path3 = gradesCachePath(home);
166
+ const path5 = gradesCachePath(home);
127
167
  const cache = await readGradesCache(home);
128
168
  cache[namespace] = grade;
129
- await atomicWriteFile(path3, `${JSON.stringify(cache, null, 2)}
169
+ await atomicWriteFile(path5, `${JSON.stringify(cache, null, 2)}
130
170
  `);
131
- return path3;
171
+ return path5;
132
172
  }
133
173
 
134
174
  // src/local-bundles.ts
@@ -180,36 +220,43 @@ function validateEntry2(entry, warnings) {
180
220
  description
181
221
  };
182
222
  }
183
- async function readBundlesAt(path3, warnings) {
223
+ async function readBundlesAt(path5, warnings) {
184
224
  let raw;
185
225
  try {
186
- raw = await readFile2(path3, "utf8");
187
- } catch {
188
- return { exists: false, file: null };
226
+ raw = await readFile2(path5, "utf8");
227
+ } catch (err) {
228
+ const code = err.code;
229
+ if (code === "ENOENT" || code === "EISDIR") {
230
+ return { exists: false, file: null };
231
+ }
232
+ const msg = err instanceof Error ? err.message : String(err);
233
+ warnings.push(`${path5}: could not read file (${msg}) -- skipping`);
234
+ log("warn", "Could not read bundles.json", { path: path5, error: msg, code });
235
+ return { exists: true, file: null };
189
236
  }
190
237
  let parsed;
191
238
  try {
192
239
  parsed = parseJsonc(raw);
193
240
  } catch (err) {
194
241
  const msg = err instanceof Error ? err.message : String(err);
195
- warnings.push(`${path3}: invalid JSON (${msg}) -- file ignored`);
196
- log("warn", "bundles.json is not valid JSON; ignoring", { path: path3, error: msg });
242
+ warnings.push(`${path5}: invalid JSON (${msg}) -- file ignored`);
243
+ log("warn", "bundles.json is not valid JSON; ignoring", { path: path5, error: msg });
197
244
  return { exists: true, file: null };
198
245
  }
199
246
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
200
- warnings.push(`${path3}: root must be a JSON object -- file ignored`);
247
+ warnings.push(`${path5}: root must be a JSON object -- file ignored`);
201
248
  return { exists: true, file: null };
202
249
  }
203
250
  const obj = parsed;
204
251
  const version = typeof obj.version === "number" ? obj.version : void 0;
205
252
  if (version !== void 0 && version > CURRENT_BUNDLES_SCHEMA_VERSION) {
206
253
  warnings.push(
207
- `${path3}: schema version ${version} is newer than this yaw-mcp (${CURRENT_BUNDLES_SCHEMA_VERSION}); upgrade with \`npm i -g @yawlabs/mcp@latest\`. Loading best-effort.`
254
+ `${path5}: schema version ${version} is newer than this yaw-mcp (${CURRENT_BUNDLES_SCHEMA_VERSION}); upgrade with \`npm i -g @yawlabs/mcp@latest\`. Loading best-effort.`
208
255
  );
209
256
  }
210
257
  const rawServers = obj.servers;
211
258
  if (!Array.isArray(rawServers)) {
212
- warnings.push(`${path3}: 'servers' must be an array -- file ignored`);
259
+ warnings.push(`${path5}: 'servers' must be an array -- file ignored`);
213
260
  return { exists: true, file: null };
214
261
  }
215
262
  return {
@@ -257,6 +304,7 @@ async function loadLocalBundles(opts = {}) {
257
304
  warnings
258
305
  };
259
306
  }
307
+ var bundleWriteChain = Promise.resolve();
260
308
  function deriveNamespace(name) {
261
309
  let ns = name.toLowerCase().replace(/[^a-z0-9]+/g, "");
262
310
  if (ns.length === 0) return "server";
@@ -265,21 +313,34 @@ function deriveNamespace(name) {
265
313
  return ns;
266
314
  }
267
315
  async function readRawUserBundles(home) {
268
- const path3 = localBundlesPath(userConfigDir(home));
269
- if (!existsSync(path3)) {
316
+ const path5 = localBundlesPath(userConfigDir(home));
317
+ if (!existsSync(path5)) {
270
318
  return { version: CURRENT_BUNDLES_SCHEMA_VERSION, servers: [] };
271
319
  }
272
320
  const warnings = [];
273
- const r = await readBundlesAt(path3, warnings);
321
+ const r = await readBundlesAt(path5, warnings);
274
322
  if (!r.file) {
275
- const detail = warnings.length > 0 ? ` (${warnings.join("; ")})` : "";
276
- throw new Error(`${path3} is malformed${detail}; fix it by hand before adding servers.`);
323
+ const warningText = warnings.join("; ");
324
+ const isReadError = /EPERM|EACCES|could not read/i.test(warningText);
325
+ if (isReadError) {
326
+ throw new Error(`${path5} could not be read (${warningText}) -- check file permissions before adding servers.`);
327
+ }
328
+ const detail = warnings.length > 0 ? ` (${warningText})` : "";
329
+ throw new Error(`${path5} could not be parsed -- fix the JSON${detail} before adding servers.`);
277
330
  }
278
331
  return { version: r.file.version ?? CURRENT_BUNDLES_SCHEMA_VERSION, servers: r.file.servers };
279
332
  }
280
- async function upsertUserBundle(entry, opts = {}) {
333
+ function upsertUserBundle(entry, opts = {}) {
334
+ const result = bundleWriteChain.then(() => doUpsertUserBundle(entry, opts));
335
+ bundleWriteChain = result.then(
336
+ () => void 0,
337
+ () => void 0
338
+ );
339
+ return result;
340
+ }
341
+ async function doUpsertUserBundle(entry, opts) {
281
342
  const home = opts.home ?? homedir2();
282
- const path3 = localBundlesPath(userConfigDir(home));
343
+ const path5 = localBundlesPath(userConfigDir(home));
283
344
  const file = await readRawUserBundles(home);
284
345
  const idx = file.servers.findIndex(
285
346
  (s) => s?.namespace === entry.namespace || entry.name != null && s?.name === entry.name
@@ -288,22 +349,30 @@ async function upsertUserBundle(entry, opts = {}) {
288
349
  if (replaced) file.servers[idx] = entry;
289
350
  else file.servers.push(entry);
290
351
  file.version = file.version ?? CURRENT_BUNDLES_SCHEMA_VERSION;
291
- await atomicWriteFile(path3, `${JSON.stringify(file, null, 2)}
352
+ await atomicWriteFile(path5, `${JSON.stringify(file, null, 2)}
292
353
  `);
293
- return { path: path3, replaced };
354
+ return { path: path5, replaced };
355
+ }
356
+ function removeUserBundle(namespace, opts = {}) {
357
+ const result = bundleWriteChain.then(() => doRemoveUserBundle(namespace, opts));
358
+ bundleWriteChain = result.then(
359
+ () => void 0,
360
+ () => void 0
361
+ );
362
+ return result;
294
363
  }
295
- async function removeUserBundle(namespace, opts = {}) {
364
+ async function doRemoveUserBundle(namespace, opts) {
296
365
  const home = opts.home ?? homedir2();
297
- const path3 = localBundlesPath(userConfigDir(home));
298
- if (!existsSync(path3)) return { path: path3, removed: false };
366
+ const path5 = localBundlesPath(userConfigDir(home));
367
+ if (!existsSync(path5)) return { path: path5, removed: false };
299
368
  const file = await readRawUserBundles(home);
300
369
  const before = file.servers.length;
301
370
  file.servers = file.servers.filter((s) => s?.namespace !== namespace);
302
- if (file.servers.length === before) return { path: path3, removed: false };
371
+ if (file.servers.length === before) return { path: path5, removed: false };
303
372
  file.version = file.version ?? CURRENT_BUNDLES_SCHEMA_VERSION;
304
- await atomicWriteFile(path3, `${JSON.stringify(file, null, 2)}
373
+ await atomicWriteFile(path5, `${JSON.stringify(file, null, 2)}
305
374
  `);
306
- return { path: path3, removed: true };
375
+ return { path: path5, removed: true };
307
376
  }
308
377
  async function findShadowingProjectBundles(cwd, home = homedir2()) {
309
378
  const projectDir = await findProjectConfigDir(cwd, home).catch(() => null);
@@ -385,11 +454,11 @@ async function runAudit(opts = {}) {
385
454
  return { exitCode: 1, lines };
386
455
  }
387
456
  const home = opts.home ?? homedir3();
388
- const { config, path: path3 } = await loadLocalBundles({ cwd: opts.cwd, home });
457
+ const { config, path: path5 } = await loadLocalBundles({ cwd: opts.cwd, home });
389
458
  const servers = config?.servers ?? [];
390
459
  const server = findServer(servers, namespace);
391
460
  if (!server) {
392
- const where = path3 ? ` (${path3})` : "";
461
+ const where = path5 ? ` (${path5})` : "";
393
462
  printErr(
394
463
  `yaw-mcp audit: no server named "${namespace}" in bundles.json${where}. Run \`yaw-mcp list\` to see configured servers.`
395
464
  );
@@ -495,7 +564,7 @@ function matchBundles(installedNamespaces) {
495
564
  return { ready, partial };
496
565
  }
497
566
  function bundleActivateHint(bundle) {
498
- return `mcp_connect_activate({ namespaces: ${JSON.stringify(bundle.namespaces)} })`;
567
+ return `mcp_connect_activate({ servers: ${JSON.stringify(bundle.namespaces)} })`;
499
568
  }
500
569
  function topPartialBundles(installedNamespaces, limit) {
501
570
  if (limit <= 0) return [];
@@ -510,19 +579,19 @@ function topPartialBundles(installedNamespaces, limit) {
510
579
  // src/config-loader.ts
511
580
  import { readFile as readFile3, stat as stat2 } from "fs/promises";
512
581
  import { homedir as homedir4 } from "os";
513
- import { join as join4, resolve } from "path";
582
+ import { join as join4, resolve as resolve2 } from "path";
514
583
 
515
584
  // src/migrate.ts
516
585
  import { mkdir, rename, stat } from "fs/promises";
517
- import { dirname, join as join3 } from "path";
586
+ import { dirname, join as join3, resolve } from "path";
518
587
  var LEGACY_GLOBAL_FILENAME = ".yaw-mcp.json";
519
588
  var LEGACY_PROJECT_FILENAME = ".yaw-mcp.json";
520
589
  var LEGACY_LOCAL_FILENAME = ".yaw-mcp.local.json";
521
590
  var NEW_CONFIG_FILENAME = "config.json";
522
591
  var NEW_LOCAL_FILENAME = "config.local.json";
523
- async function exists(path3) {
592
+ async function exists(path5) {
524
593
  try {
525
- await stat(path3);
594
+ await stat(path5);
526
595
  return true;
527
596
  } catch {
528
597
  return false;
@@ -573,9 +642,8 @@ async function migrateLegacyConfigPaths(opts) {
573
642
  }
574
643
  }
575
644
  async function findLegacyProjectRoot(cwd, home) {
576
- const { resolve: resolve5, dirname: dirname5 } = await import("path");
577
- const homeResolved = resolve5(home);
578
- let dir = resolve5(cwd);
645
+ const homeResolved = resolve(home);
646
+ let dir = resolve(cwd);
579
647
  let prev = "";
580
648
  while (dir !== prev) {
581
649
  if (dir === homeResolved) return null;
@@ -583,7 +651,7 @@ async function findLegacyProjectRoot(cwd, home) {
583
651
  const legacyLocal = join3(dir, LEGACY_LOCAL_FILENAME);
584
652
  if (await exists(legacyProject) || await exists(legacyLocal)) return dir;
585
653
  prev = dir;
586
- dir = dirname5(dir);
654
+ dir = dirname(dir);
587
655
  }
588
656
  return null;
589
657
  }
@@ -593,10 +661,10 @@ var CONFIG_FILENAME = "config.json";
593
661
  var LOCAL_CONFIG_FILENAME = "config.local.json";
594
662
  var CURRENT_SCHEMA_VERSION = 1;
595
663
  var DEFAULT_API_BASE = "https://yaw.sh/mcp";
596
- async function readConfigAt(path3, scope, warnings) {
664
+ async function readConfigAt(path5, scope, warnings) {
597
665
  let raw;
598
666
  try {
599
- raw = await readFile3(path3, "utf8");
667
+ raw = await readFile3(path5, "utf8");
600
668
  } catch {
601
669
  return null;
602
670
  }
@@ -605,19 +673,19 @@ async function readConfigAt(path3, scope, warnings) {
605
673
  parsed = parseJsonc(raw);
606
674
  } catch (err) {
607
675
  const msg = err instanceof Error ? err.message : String(err);
608
- warnings.push(`${path3}: invalid JSON (${msg}) \u2014 file ignored`);
609
- log("warn", "Config file is not valid JSON; ignoring", { path: path3, error: msg });
676
+ warnings.push(`${path5}: invalid JSON (${msg}) \u2014 file ignored`);
677
+ log("warn", "Config file is not valid JSON; ignoring", { path: path5, error: msg });
610
678
  return null;
611
679
  }
612
680
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
613
- warnings.push(`${path3}: root must be a JSON object \u2014 file ignored`);
681
+ warnings.push(`${path5}: root must be a JSON object \u2014 file ignored`);
614
682
  return null;
615
683
  }
616
684
  const obj = parsed;
617
685
  const version = typeof obj.version === "number" ? obj.version : void 0;
618
686
  if (version !== void 0 && version > CURRENT_SCHEMA_VERSION) {
619
687
  warnings.push(
620
- `${path3}: schema version ${version} is newer than this yaw-mcp (${CURRENT_SCHEMA_VERSION}); upgrade with \`npm i -g @yawlabs/mcp@latest\`. Loading best-effort.`
688
+ `${path5}: schema version ${version} is newer than this yaw-mcp (${CURRENT_SCHEMA_VERSION}); upgrade with \`npm i -g @yawlabs/mcp@latest\`. Loading best-effort.`
621
689
  );
622
690
  }
623
691
  const token5 = typeof obj.token === "string" && obj.token.length > 0 ? obj.token : void 0;
@@ -627,21 +695,21 @@ async function readConfigAt(path3, scope, warnings) {
627
695
  if (token5) {
628
696
  if (scope === "project") {
629
697
  warnings.push(
630
- `${path3}: 'token' should not appear in a project-shared file. Move it to ${CONFIG_DIRNAME}/${LOCAL_CONFIG_FILENAME} (gitignored) or ~/${CONFIG_DIRNAME}/${CONFIG_FILENAME}.`
698
+ `${path5}: 'token' found in a project-shared config file is IGNORED -- yaw-mcp never reads a token from this scope to avoid committing credentials. Move it to ${CONFIG_DIRNAME}/${LOCAL_CONFIG_FILENAME} (machine-local, gitignore by convention) or ~/${CONFIG_DIRNAME}/${CONFIG_FILENAME} (user-global).`
631
699
  );
632
700
  }
633
- await checkPermissions(path3, warnings);
701
+ await checkPermissions(path5, warnings);
634
702
  }
635
- return { path: path3, scope, version, token: token5, apiBase, servers, blocked };
703
+ return { path: path5, scope, version, token: token5, apiBase, servers, blocked };
636
704
  }
637
- async function checkPermissions(path3, warnings) {
705
+ async function checkPermissions(path5, warnings) {
638
706
  if (process.platform === "win32") return;
639
707
  try {
640
- const st = await stat2(path3);
708
+ const st = await stat2(path5);
641
709
  const mode = st.mode & 511;
642
710
  if ((mode & 63) !== 0) {
643
711
  warnings.push(
644
- `${path3}: contains a token but is readable by group/other (mode ${mode.toString(8)}). Run \`chmod 600 ${path3}\` to restrict.`
712
+ `${path5}: contains a token but is readable by group/other (mode ${mode.toString(8)}). Run \`chmod 600 ${path5}\` to restrict.`
645
713
  );
646
714
  }
647
715
  } catch {
@@ -666,8 +734,8 @@ function unionBlocked(files) {
666
734
  return touched ? [...set] : void 0;
667
735
  }
668
736
  async function loadYawMcpConfig(opts = {}) {
669
- const cwd = resolve(opts.cwd ?? process.cwd());
670
- const home = resolve(opts.home ?? homedir4());
737
+ const cwd = resolve2(opts.cwd ?? process.cwd());
738
+ const home = resolve2(opts.home ?? homedir4());
671
739
  const env = opts.env ?? process.env;
672
740
  const warnings = [];
673
741
  const loadedFiles = [];
@@ -684,7 +752,11 @@ async function loadYawMcpConfig(opts = {}) {
684
752
  const globalPath = join4(globalDir, CONFIG_FILENAME);
685
753
  const local = localPath ? await readConfigAt(localPath, "local", warnings) : null;
686
754
  if (local) loadedFiles.push(local);
687
- const projectIsGlobal = projectConfigDir !== null && projectConfigDir === globalDir;
755
+ const normalizeDir = (d) => {
756
+ const r = resolve2(d);
757
+ return process.platform === "win32" ? r.toLowerCase() : r;
758
+ };
759
+ const projectIsGlobal = projectConfigDir !== null && normalizeDir(projectConfigDir) === normalizeDir(globalDir);
688
760
  const project = projectIsGlobal || !projectPath ? null : await readConfigAt(projectPath, "project", warnings);
689
761
  if (project) loadedFiles.push(project);
690
762
  const global = await readConfigAt(globalPath, "global", warnings);
@@ -804,9 +876,9 @@ async function fetchConfig(apiUrl5, token5, currentVersion) {
804
876
  await res.body.text().catch(() => {
805
877
  });
806
878
  throw new ConfigError(
807
- `Access denied (HTTP 403) \u2014 the token ${tokenFingerprint(token5)} was accepted but lacks permission to read this account's servers.
808
- The account may be suspended or the token scope reduced \u2014 check
809
- https://yaw.sh/mcp/dashboard/settings/tokens, or reach support@mcp.hosting.`,
879
+ `Access denied (HTTP 403) -- the token ${tokenFingerprint(token5)} was accepted but lacks permission to read this account's servers.
880
+ The account may be suspended or the token scope reduced -- check
881
+ https://yaw.sh/mcp/dashboard/settings/tokens, or reach support@yaw.sh.`,
810
882
  true
811
883
  );
812
884
  }
@@ -825,9 +897,8 @@ async function fetchConfig(apiUrl5, token5, currentVersion) {
825
897
  }
826
898
  return true;
827
899
  });
828
- const NAMESPACE_RE3 = /^[a-z][a-z0-9_]{0,29}$/;
829
900
  data.servers = data.servers.filter((s) => {
830
- if (!s.namespace || !NAMESPACE_RE3.test(s.namespace)) {
901
+ if (!s.namespace || !NAMESPACE_RE.test(s.namespace)) {
831
902
  log("warn", "Skipping server with invalid namespace", { namespace: s.namespace, name: s.name });
832
903
  return false;
833
904
  }
@@ -862,7 +933,7 @@ function parseBundlesArgs(argv) {
862
933
  if (a === "--json") {
863
934
  json = true;
864
935
  } else if (a === "--help" || a === "-h") {
865
- return { ok: false, error: BUNDLES_USAGE };
936
+ return { ok: false, error: BUNDLES_USAGE, help: true };
866
937
  } else if (a === "list" || a === "match") {
867
938
  if (actionSet) {
868
939
  return {
@@ -926,7 +997,7 @@ async function runBundlesCommand(opts = {}) {
926
997
  return { exitCode: 2, lines };
927
998
  }
928
999
  if (!backend) {
929
- printErr("yaw-mcp bundles match: backend returned no data (unexpected 304).");
1000
+ printErr("yaw-mcp bundles match: backend returned 304 without a conditional request.");
930
1001
  return { exitCode: 2, lines };
931
1002
  }
932
1003
  const installed = backend.servers.filter((s) => s.isActive).map((s) => s.namespace);
@@ -1065,20 +1136,27 @@ var SUBCOMMAND_SPEC = [
1065
1136
  positional: ["push", "pull", "status"],
1066
1137
  flags: ["--key", "--json", "--help"]
1067
1138
  },
1068
- { name: "stats", description: "Show usage statistics", flags: ["--key", "--limit", "--days", "--json", "--help"] },
1139
+ { name: "stats", description: "Show usage statistics", flags: ["--limit", "--days", "--json", "--help"] },
1069
1140
  {
1070
1141
  name: "secrets",
1071
1142
  description: "Manage stored secrets",
1072
1143
  positional: ["set", "get", "list", "remove", "lock", "push", "pull"],
1073
1144
  flags: ["--key", "--value", "--stdin", "--json", "--help"]
1074
1145
  },
1146
+ {
1147
+ name: "set-active",
1148
+ description: "Enable/disable a team server (authoritative)",
1149
+ positional: ["<namespace>", "on", "off"],
1150
+ flags: ["--json", "--help"]
1151
+ },
1075
1152
  // Other.
1153
+ { name: "audit", description: "Run a full-pass audit of loaded servers", flags: ["--json", "--help"] },
1076
1154
  { name: "compliance", description: "Run the compliance suite against a server", flags: ["--publish", "--help"] },
1077
1155
  { name: "help", description: "Show usage", flags: [] }
1078
1156
  ];
1079
1157
  function parseCompletionArgs(argv) {
1080
1158
  if (argv.includes("--help") || argv.includes("-h")) {
1081
- return { ok: false, error: COMPLETION_USAGE };
1159
+ return { ok: false, error: COMPLETION_USAGE, help: true };
1082
1160
  }
1083
1161
  const positional = argv.filter((a) => !a.startsWith("-"));
1084
1162
  if (positional.length === 0) {
@@ -1275,10 +1353,16 @@ async function runComplianceCommand(argv) {
1275
1353
  if (publish) {
1276
1354
  const result = await publishReport(apiUrl5, report);
1277
1355
  if (!result) return 1;
1356
+ const reportUrl = typeof result.reportUrl === "string" ? result.reportUrl.trim() : "";
1357
+ const badgeUrl = typeof result.badgeUrl === "string" ? result.badgeUrl.trim() : "";
1358
+ if (!reportUrl || !badgeUrl) {
1359
+ process.stderr.write("\nPublish failed: server returned 200 but no report/badge URL.\n");
1360
+ return 1;
1361
+ }
1278
1362
  process.stdout.write(`
1279
- Published: ${result.reportUrl}
1363
+ Published: ${reportUrl}
1280
1364
  `);
1281
- process.stdout.write(`Badge: ${result.badgeUrl}
1365
+ process.stdout.write(`Badge: ${badgeUrl}
1282
1366
  `);
1283
1367
  if (result.deleteToken) {
1284
1368
  process.stdout.write(`
@@ -1289,7 +1373,7 @@ Delete token (save this): ${result.deleteToken}
1289
1373
  return 0;
1290
1374
  }
1291
1375
  function runTest(args) {
1292
- return new Promise((resolve5) => {
1376
+ return new Promise((resolve7) => {
1293
1377
  const child = spawn("npx", ["-y", "@yawlabs/mcp-compliance", "test", "--format", "json", ...args], {
1294
1378
  stdio: ["ignore", "pipe", "inherit"],
1295
1379
  shell: process.platform === "win32"
@@ -1302,7 +1386,7 @@ function runTest(args) {
1302
1386
  process.stderr.write(`
1303
1387
  Failed to launch mcp-compliance: ${err.message}
1304
1388
  `);
1305
- resolve5(null);
1389
+ resolve7(null);
1306
1390
  });
1307
1391
  child.on("close", (code) => {
1308
1392
  try {
@@ -1311,15 +1395,15 @@ Failed to launch mcp-compliance: ${err.message}
1311
1395
  process.stderr.write(`
1312
1396
  mcp-compliance returned unexpected JSON (exit ${code}).
1313
1397
  `);
1314
- resolve5(null);
1398
+ resolve7(null);
1315
1399
  return;
1316
1400
  }
1317
- resolve5(parsed);
1401
+ resolve7(parsed);
1318
1402
  } catch {
1319
1403
  process.stderr.write(`
1320
1404
  mcp-compliance exited ${code} without valid JSON output.
1321
1405
  `);
1322
- resolve5(null);
1406
+ resolve7(null);
1323
1407
  }
1324
1408
  });
1325
1409
  });
@@ -1328,7 +1412,7 @@ function printSummary(report) {
1328
1412
  const { grade, score, summary, url } = report;
1329
1413
  process.stdout.write(
1330
1414
  `
1331
- Compliance: ${grade} (${score.toFixed(1)}%) \u2014 ${summary.passed}/${summary.total} passed, ${summary.requiredPassed}/${summary.required} required
1415
+ Compliance: ${grade} (${score.toFixed(1)}%) -- ${summary.passed}/${summary.total} passed, ${summary.requiredPassed}/${summary.required} required
1332
1416
  Target: ${url}
1333
1417
  `
1334
1418
  );
@@ -1343,15 +1427,16 @@ async function publishReport(apiUrl5, report) {
1343
1427
  if (res.statusCode !== 200) {
1344
1428
  const body = await res.body.text().catch(() => "");
1345
1429
  process.stderr.write(`
1346
- Publish failed: HTTP ${res.statusCode}${body ? ` \u2014 ${body}` : ""}
1430
+ Publish failed: HTTP ${res.statusCode}${body ? ` -- ${body}` : ""}
1347
1431
  `);
1348
1432
  return null;
1349
1433
  }
1350
1434
  const parsed = await res.body.json();
1351
1435
  return parsed;
1352
1436
  } catch (err) {
1437
+ const message = err instanceof Error ? err.message : String(err);
1353
1438
  process.stderr.write(`
1354
- Publish failed: ${err?.message ?? String(err)}
1439
+ Publish failed: ${message}
1355
1440
  `);
1356
1441
  return null;
1357
1442
  }
@@ -1359,7 +1444,7 @@ Publish failed: ${err?.message ?? String(err)}
1359
1444
 
1360
1445
  // src/doctor-cmd.ts
1361
1446
  import { existsSync as existsSync4, readFileSync, statSync } from "fs";
1362
- import { readFile as readFile7 } from "fs/promises";
1447
+ import { readFile as readFile7, stat as stat3 } from "fs/promises";
1363
1448
  import { homedir as homedir8 } from "os";
1364
1449
  import { join as join8 } from "path";
1365
1450
 
@@ -1494,6 +1579,7 @@ function initAnalytics(url, tok) {
1494
1579
  token = tok;
1495
1580
  lastLoggedConnectStatus = null;
1496
1581
  lastLoggedDispatchStatus = null;
1582
+ teamAnalyticsDisabled = false;
1497
1583
  flushTimer = setInterval(() => {
1498
1584
  flush().catch(() => {
1499
1585
  });
@@ -1669,7 +1755,7 @@ function formatShadowLine(server) {
1669
1755
 
1670
1756
  // src/install-targets.ts
1671
1757
  import { homedir as homedir5 } from "os";
1672
- import { join as join5 } from "path";
1758
+ import { isAbsolute, join as join5, resolve as resolve3 } from "path";
1673
1759
  var CURRENT_OS = process.platform === "darwin" ? "macos" : process.platform === "win32" ? "windows" : "linux";
1674
1760
  var INSTALL_TARGETS = [
1675
1761
  {
@@ -1763,10 +1849,11 @@ function resolveInstallPath(opts) {
1763
1849
  if (scopeSpec.requiresProjectDir && !projectDir) {
1764
1850
  throw new Error(`Scope ${scope} for ${clientId} requires a project directory`);
1765
1851
  }
1852
+ const absoluteProjectDir = projectDir && !isAbsolute(projectDir) ? resolve3(projectDir) : projectDir;
1766
1853
  const p = pathFor(clientId, scope, os, {
1767
1854
  home,
1768
1855
  appData,
1769
- projectDir: projectDir ?? "",
1856
+ projectDir: absoluteProjectDir ?? "",
1770
1857
  claudeConfigDir: claudeConfigDir && claudeConfigDir.length > 0 ? claudeConfigDir : void 0
1771
1858
  });
1772
1859
  return p;
@@ -1929,7 +2016,8 @@ function sanitizeLearning(input) {
1929
2016
  if (typeof u.dispatched !== "number" || !Number.isFinite(u.dispatched) || u.dispatched < 0) continue;
1930
2017
  if (typeof u.succeeded !== "number" || !Number.isFinite(u.succeeded) || u.succeeded < 0) continue;
1931
2018
  if (typeof u.lastUsedAt !== "number" || !Number.isFinite(u.lastUsedAt) || u.lastUsedAt < 0) continue;
1932
- out[k] = { dispatched: u.dispatched, succeeded: u.succeeded, lastUsedAt: u.lastUsedAt };
2019
+ const succeeded = Math.min(u.succeeded, u.dispatched);
2020
+ out[k] = { dispatched: u.dispatched, succeeded, lastUsedAt: u.lastUsedAt };
1933
2021
  }
1934
2022
  return out;
1935
2023
  }
@@ -1997,7 +2085,7 @@ import { createHash as createHash2 } from "crypto";
1997
2085
  import { existsSync as existsSync3 } from "fs";
1998
2086
  import { chmod as chmod2, mkdir as mkdir2, readFile as readFile6, readdir, unlink } from "fs/promises";
1999
2087
  import { homedir as homedir7, hostname, userInfo } from "os";
2000
- import { join as join7, resolve as resolve3 } from "path";
2088
+ import { join as join7, resolve as resolve5 } from "path";
2001
2089
  import { request as request5 } from "undici";
2002
2090
 
2003
2091
  // src/catalog.ts
@@ -2099,7 +2187,7 @@ async function resolveCatalogSlug(slug, opts = {}) {
2099
2187
  import { existsSync as existsSync2 } from "fs";
2100
2188
  import { chmod, readFile as readFile5 } from "fs/promises";
2101
2189
  import { homedir as homedir6 } from "os";
2102
- import { join as join6, resolve as resolve2 } from "path";
2190
+ import { join as join6, resolve as resolve4 } from "path";
2103
2191
  import { createInterface } from "readline/promises";
2104
2192
  var USAGE = "Usage: yaw-mcp install <claude-code|claude-desktop|cursor|vscode> [--scope user|project|local]\n [--token <mcp_pat_\u2026>] [--project-dir <path>] [--os macos|linux|windows]\n [--force | --skip] [--dry-run] [--no-yaw-mcp-config]\n yaw-mcp install --list (detect clients; no writes)\n yaw-mcp install --all [--token <mcp_pat_\u2026>] (install into every detected client)";
2105
2193
  async function runInstall(opts) {
@@ -2152,7 +2240,7 @@ ${USAGE}`);
2152
2240
  );
2153
2241
  return { written: [], wouldWrite: [], messages, exitCode: 2 };
2154
2242
  }
2155
- const projectDir = scopeSpec.requiresProjectDir ? resolve2(opts.projectDir ?? process.cwd()) : void 0;
2243
+ const projectDir = scopeSpec.requiresProjectDir ? resolve4(opts.projectDir ?? process.cwd()) : void 0;
2156
2244
  let resolved;
2157
2245
  try {
2158
2246
  resolved = resolveInstallPath({
@@ -2221,7 +2309,7 @@ ${USAGE}`);
2221
2309
  if (opts.force) decision = "overwrite";
2222
2310
  else if (opts.skip) decision = "skip";
2223
2311
  else if (opts.promptAnswer) decision = opts.promptAnswer;
2224
- else if (opts.io?.isTTY ?? process.stdout.isTTY) {
2312
+ else if (opts.io?.isTTY ?? (Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY))) {
2225
2313
  decision = await promptCollision(resolved.absolute, opts.io);
2226
2314
  } else {
2227
2315
  err(
@@ -2323,38 +2411,38 @@ ${settingsPatch.nextJson}`);
2323
2411
  );
2324
2412
  }
2325
2413
  log2(`
2326
- \u2713 ${target.label} is configured. Restart it to pick up the new MCP server.`);
2414
+ Done: ${target.label} is configured. Restart it to pick up the new MCP server.`);
2327
2415
  return { written, wouldWrite: [], messages, exitCode: 0 };
2328
2416
  }
2329
2417
  async function prepareClaudeCodeSettingsPatch(opts) {
2330
- const path3 = resolveClaudeCodeSettingsPath(opts.scope, {
2418
+ const path5 = resolveClaudeCodeSettingsPath(opts.scope, {
2331
2419
  home: opts.home,
2332
2420
  projectDir: opts.projectDir,
2333
2421
  os: opts.os,
2334
2422
  claudeConfigDir: opts.claudeConfigDir
2335
2423
  });
2336
- if (!path3) return null;
2424
+ if (!path5) return null;
2337
2425
  let existing = {};
2338
- if (existsSync2(path3)) {
2426
+ if (existsSync2(path5)) {
2339
2427
  try {
2340
- const raw = await readFile5(path3, "utf8");
2428
+ const raw = await readFile5(path5, "utf8");
2341
2429
  if (raw.trim().length > 0) {
2342
2430
  const parsed = parseJsonc(raw);
2343
2431
  if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
2344
2432
  existing = parsed;
2345
2433
  } else {
2346
- return { path: path3, nextJson: "", changed: false };
2434
+ return { path: path5, nextJson: "", changed: false };
2347
2435
  }
2348
2436
  }
2349
2437
  } catch {
2350
- return { path: path3, nextJson: "", changed: false };
2438
+ return { path: path5, nextJson: "", changed: false };
2351
2439
  }
2352
2440
  }
2353
2441
  const merged = mergePermissionsAllow(existing, [CLAUDE_CODE_ALLOW_PATTERN]);
2354
2442
  const before = JSON.stringify(existing);
2355
2443
  const after = JSON.stringify(merged);
2356
- if (before === after) return { path: path3, nextJson: "", changed: false };
2357
- return { path: path3, nextJson: `${JSON.stringify(merged, null, 2)}
2444
+ if (before === after) return { path: path5, nextJson: "", changed: false };
2445
+ return { path: path5, nextJson: `${JSON.stringify(merged, null, 2)}
2358
2446
  `, changed: true };
2359
2447
  }
2360
2448
  var LEGACY_CLAUDE_CODE_ALLOW_PATTERNS = ["mcp__mcp_hosting__*", "mcp__yaw_mcp__*"];
@@ -2373,13 +2461,13 @@ function mergePermissionsAllow(existing, patterns) {
2373
2461
  out.permissions = perms;
2374
2462
  return out;
2375
2463
  }
2376
- async function promptCollision(path3, io) {
2464
+ async function promptCollision(path5, io) {
2377
2465
  const stdin = io?.stdin ?? process.stdin;
2378
2466
  const stdout = io?.stdout ?? process.stdout;
2379
2467
  const rl = createInterface({ input: stdin, output: stdout });
2380
2468
  try {
2381
2469
  const answer = (await rl.question(
2382
- `${path3} already has an "${ENTRY_NAME}" entry.
2470
+ `${path5} already has an "${ENTRY_NAME}" entry.
2383
2471
  [o]verwrite, [s]kip, or [a]bort? (default: skip) `
2384
2472
  )).trim().toLowerCase();
2385
2473
  if (answer.startsWith("o")) return "overwrite";
@@ -2439,13 +2527,13 @@ function removeFromClientConfig(existing, containerPath, entryName) {
2439
2527
  parent[leafKey] = container;
2440
2528
  return out;
2441
2529
  }
2442
- async function composeYawMcpConfig(path3, token5) {
2530
+ async function composeYawMcpConfig(path5, token5) {
2443
2531
  let existing = {};
2444
2532
  let backupPath;
2445
- if (existsSync2(path3)) {
2533
+ if (existsSync2(path5)) {
2446
2534
  let raw = "";
2447
2535
  try {
2448
- raw = await readFile5(path3, "utf8");
2536
+ raw = await readFile5(path5, "utf8");
2449
2537
  } catch {
2450
2538
  raw = "";
2451
2539
  }
@@ -2454,9 +2542,16 @@ async function composeYawMcpConfig(path3, token5) {
2454
2542
  const parsed = parseJsonc(raw);
2455
2543
  if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
2456
2544
  existing = parsed;
2545
+ } else {
2546
+ const candidate = `${path5}.bak-${Date.now()}`;
2547
+ try {
2548
+ await atomicWriteFile(candidate, raw);
2549
+ backupPath = candidate;
2550
+ } catch {
2551
+ }
2457
2552
  }
2458
2553
  } catch {
2459
- const candidate = `${path3}.bak-${Date.now()}`;
2554
+ const candidate = `${path5}.bak-${Date.now()}`;
2460
2555
  try {
2461
2556
  await atomicWriteFile(candidate, raw);
2462
2557
  backupPath = candidate;
@@ -2525,7 +2620,7 @@ function parseInstallArgs(argv) {
2525
2620
  break;
2526
2621
  case "-h":
2527
2622
  case "--help":
2528
- return { ok: false, error: USAGE };
2623
+ return { ok: false, error: USAGE, help: true };
2529
2624
  default:
2530
2625
  if (a.startsWith("--")) return { ok: false, error: `Unknown flag: ${a}
2531
2626
  ${USAGE}` };
@@ -2660,7 +2755,7 @@ async function runInstallAll(opts, log2, err) {
2660
2755
  }
2661
2756
  const totalPlanned = plans.length;
2662
2757
  if (failed === 0) {
2663
- log2(`\u2713 ${succeeded}/${totalPlanned} clients installed successfully.`);
2758
+ log2(`Done: ${succeeded}/${totalPlanned} clients installed successfully.`);
2664
2759
  return {
2665
2760
  written: aggregateWritten,
2666
2761
  wouldWrite: aggregateWouldWrite,
@@ -2755,7 +2850,7 @@ function parseTryArgs(argv) {
2755
2850
  }
2756
2851
  case "-h":
2757
2852
  case "--help":
2758
- return { ok: false, error: TRY_USAGE };
2853
+ return { ok: false, error: TRY_USAGE, help: true };
2759
2854
  default:
2760
2855
  if (a.startsWith("--")) return { ok: false, error: `Unknown flag: ${a}
2761
2856
  ${TRY_USAGE}` };
@@ -2776,7 +2871,7 @@ function parseTryCleanupArgs(argv) {
2776
2871
  const positional = [];
2777
2872
  for (let i = 0; i < argv.length; i++) {
2778
2873
  const a = argv[i];
2779
- if (a === "-h" || a === "--help") return { ok: false, error: TRY_CLEANUP_USAGE };
2874
+ if (a === "-h" || a === "--help") return { ok: false, error: TRY_CLEANUP_USAGE, help: true };
2780
2875
  if (a === "--base") {
2781
2876
  const v = argv[++i];
2782
2877
  if (!v) return { ok: false, error: "--base requires a URL" };
@@ -2822,10 +2917,10 @@ function computeAnonId() {
2822
2917
  return h.digest("hex").slice(0, 16);
2823
2918
  }
2824
2919
  async function loadOrCreateAnonId(home = homedir7()) {
2825
- const path3 = anonIdPath(home);
2826
- if (existsSync3(path3)) {
2920
+ const path5 = anonIdPath(home);
2921
+ if (existsSync3(path5)) {
2827
2922
  try {
2828
- const raw = (await readFile6(path3, "utf8")).trim();
2923
+ const raw = (await readFile6(path5, "utf8")).trim();
2829
2924
  if (/^[0-9a-f]{16}$/.test(raw)) return raw;
2830
2925
  } catch {
2831
2926
  }
@@ -2833,11 +2928,11 @@ async function loadOrCreateAnonId(home = homedir7()) {
2833
2928
  const id = computeAnonId();
2834
2929
  try {
2835
2930
  await mkdir2(trialsDir(home), { recursive: true });
2836
- await atomicWriteFile(path3, `${id}
2931
+ await atomicWriteFile(path5, `${id}
2837
2932
  `);
2838
2933
  if (process.platform !== "win32") {
2839
2934
  try {
2840
- await chmod2(path3, 384);
2935
+ await chmod2(path5, 384);
2841
2936
  } catch {
2842
2937
  }
2843
2938
  }
@@ -2922,7 +3017,7 @@ async function runTry(opts) {
2922
3017
  }
2923
3018
  const clientId = opts.clientId ?? await autoDetectClient({ home, os, cwd, claudeConfigDir });
2924
3019
  const scope = clientId === "vscode" ? "project" : "user";
2925
- const projectDir = scope === "project" ? resolve3(cwd) : void 0;
3020
+ const projectDir = scope === "project" ? resolve5(cwd) : void 0;
2926
3021
  let resolved;
2927
3022
  try {
2928
3023
  resolved = resolveInstallPath({ clientId, scope, os, home, projectDir, claudeConfigDir });
@@ -2931,7 +3026,7 @@ async function runTry(opts) {
2931
3026
  return { exitCode: 1, written: [] };
2932
3027
  }
2933
3028
  const supplied = { ...env, ...opts.envOverrides ?? {} };
2934
- const missing = (server.requiredEnvVars ?? []).filter((k) => !supplied[k] || supplied[k] === "");
3029
+ const missing = (server.requiredEnvVars ?? []).filter((k) => (supplied[k] ?? "").trim() === "");
2935
3030
  if (missing.length > 0) {
2936
3031
  printErr(`yaw-mcp try: ${server.name} needs the following env var(s) before it can run:`);
2937
3032
  for (const k of missing) printErr(` - ${k}`);
@@ -2944,7 +3039,7 @@ async function runTry(opts) {
2944
3039
  }
2945
3040
  const trialEnv = {};
2946
3041
  for (const k of server.requiredEnvVars ?? []) {
2947
- const v = supplied[k];
3042
+ const v = (supplied[k] ?? "").trim();
2948
3043
  if (v) trialEnv[k] = v;
2949
3044
  }
2950
3045
  for (const [k, v] of Object.entries(opts.envOverrides ?? {})) {
@@ -3065,6 +3160,7 @@ async function runTryCleanup(opts) {
3065
3160
  printErr(`yaw-mcp try-cleanup: marker at ${markerPath} is unreadable (${e.message}).`);
3066
3161
  return { exitCode: 1, written: [] };
3067
3162
  }
3163
+ const written = [];
3068
3164
  if (existsSync3(marker.clientPath)) {
3069
3165
  try {
3070
3166
  const raw = await readFile6(marker.clientPath, "utf8");
@@ -3079,13 +3175,14 @@ async function runTryCleanup(opts) {
3079
3175
  if (stripped !== parsed) {
3080
3176
  await atomicWriteFile(marker.clientPath, `${JSON.stringify(stripped, null, 2)}
3081
3177
  `);
3178
+ written.push(marker.clientPath);
3082
3179
  print(`Removed ${marker.entryName} from ${marker.clientPath}`);
3083
3180
  }
3084
3181
  }
3085
3182
  }
3086
3183
  } catch (e) {
3087
3184
  printErr(
3088
- `yaw-mcp try-cleanup: warning \u2014 couldn't strip ${marker.entryName} from ${marker.clientPath} (${e.message}).`
3185
+ `yaw-mcp try-cleanup: warning -- couldn't strip ${marker.entryName} from ${marker.clientPath} (${e.message}).`
3089
3186
  );
3090
3187
  }
3091
3188
  }
@@ -3099,7 +3196,7 @@ async function runTryCleanup(opts) {
3099
3196
  const postEvent = opts.postEvent ?? defaultPostEvent;
3100
3197
  postEvent(baseUrl, { slug, action: "cleanup", anonId }).catch(() => void 0);
3101
3198
  print(`Trial for "${slug}" cleaned up.`);
3102
- return { exitCode: 0, written: [marker.clientPath] };
3199
+ return { exitCode: 0, written };
3103
3200
  }
3104
3201
  function formatTtl(ms) {
3105
3202
  const clamped = Math.max(0, ms);
@@ -3122,12 +3219,12 @@ async function scanTrials(opts = {}) {
3122
3219
  }
3123
3220
  for (const filename of entries) {
3124
3221
  if (!filename.endsWith(".json")) continue;
3125
- const path3 = join7(dir, filename);
3222
+ const path5 = join7(dir, filename);
3126
3223
  try {
3127
- const raw = await readFile6(path3, "utf8");
3224
+ const raw = await readFile6(path5, "utf8");
3128
3225
  const parsed = JSON.parse(raw);
3129
3226
  if (!parsed || typeof parsed !== "object" || typeof parsed.slug !== "string" || typeof parsed.expiresAt !== "number" || typeof parsed.clientPath !== "string" || !Array.isArray(parsed.containerPath) || typeof parsed.entryName !== "string") {
3130
- result.malformed.push(path3);
3227
+ result.malformed.push(path5);
3131
3228
  continue;
3132
3229
  }
3133
3230
  const msUntilExpiry = parsed.expiresAt - now;
@@ -3136,7 +3233,7 @@ async function scanTrials(opts = {}) {
3136
3233
  if (expired) result.expired.push(entry);
3137
3234
  else result.live.push(entry);
3138
3235
  } catch {
3139
- result.malformed.push(path3);
3236
+ result.malformed.push(path5);
3140
3237
  }
3141
3238
  }
3142
3239
  return result;
@@ -3189,16 +3286,19 @@ var UPGRADE_USAGE = `Usage: yaw-mcp upgrade [--run] [--json]
3189
3286
 
3190
3287
  Show (or execute) the command to upgrade @yawlabs/mcp to the latest version.
3191
3288
 
3192
- --run Run the upgrade in place (global and local npm installs).
3193
- No-op for npx installs \u2014 they always fetch the latest.
3289
+ --run Run the upgrade in place (global npm, pnpm, bun, and local npm
3290
+ installs). No-op for npx installs -- they always fetch the latest.
3194
3291
  --json Emit a machine-readable snapshot ({ current, latest, stale,
3195
- method, command }) instead of prose.`;
3292
+ method, command }) instead of prose.
3293
+ NOTE: --json is a report-only snapshot; it never spawns an upgrade
3294
+ even when combined with --run. Use --run without --json to
3295
+ actually perform the upgrade.`;
3196
3296
  function parseUpgradeArgs(argv) {
3197
3297
  const opts = {};
3198
3298
  for (const a of argv) {
3199
3299
  if (a === "--run") opts.run = true;
3200
3300
  else if (a === "--json") opts.json = true;
3201
- else if (a === "--help" || a === "-h") return { ok: false, error: UPGRADE_USAGE };
3301
+ else if (a === "--help" || a === "-h") return { ok: false, error: UPGRADE_USAGE, help: true };
3202
3302
  else return { ok: false, error: `yaw-mcp upgrade: unknown argument "${a}"
3203
3303
 
3204
3304
  ${UPGRADE_USAGE}` };
@@ -3227,7 +3327,7 @@ function localInstallRoot(argvPath) {
3227
3327
  }
3228
3328
  async function defaultNpmPrefix() {
3229
3329
  if (process.env.VITEST) return null;
3230
- return new Promise((resolve5) => {
3330
+ return new Promise((resolve7) => {
3231
3331
  const child = spawn2("npm", ["prefix", "-g"], {
3232
3332
  shell: process.platform === "win32",
3233
3333
  stdio: ["ignore", "pipe", "ignore"]
@@ -3235,18 +3335,18 @@ async function defaultNpmPrefix() {
3235
3335
  let out = "";
3236
3336
  const timer = setTimeout(() => {
3237
3337
  child.kill();
3238
- resolve5(null);
3338
+ resolve7(null);
3239
3339
  }, 3e3);
3240
3340
  child.stdout?.on("data", (d) => {
3241
3341
  out += String(d);
3242
3342
  });
3243
3343
  child.on("close", (code) => {
3244
3344
  clearTimeout(timer);
3245
- resolve5(code === 0 && out.trim() ? out.trim() : null);
3345
+ resolve7(code === 0 && out.trim() ? out.trim() : null);
3246
3346
  });
3247
3347
  child.on("error", () => {
3248
3348
  clearTimeout(timer);
3249
- resolve5(null);
3349
+ resolve7(null);
3250
3350
  });
3251
3351
  });
3252
3352
  }
@@ -3339,10 +3439,10 @@ async function defaultFetchLatest() {
3339
3439
  }
3340
3440
  }
3341
3441
  async function defaultSpawn(cmd, args, cwd) {
3342
- return new Promise((resolve5) => {
3442
+ return new Promise((resolve7) => {
3343
3443
  const child = spawn2(cmd, args, { stdio: "inherit", shell: process.platform === "win32", cwd });
3344
- child.on("close", (code) => resolve5(typeof code === "number" ? code : 1));
3345
- child.on("error", () => resolve5(1));
3444
+ child.on("close", (code) => resolve7(typeof code === "number" ? code : 1));
3445
+ child.on("error", () => resolve7(1));
3346
3446
  });
3347
3447
  }
3348
3448
  async function detectSea() {
@@ -3407,7 +3507,7 @@ async function runUpgrade(opts = {}) {
3407
3507
  print(`Install: ${method}`);
3408
3508
  if (!plan.stale) {
3409
3509
  print("");
3410
- print("\u2713 You're on the latest version \u2014 nothing to do.");
3510
+ print("OK: You're on the latest version -- nothing to do.");
3411
3511
  return { exitCode: 0, lines };
3412
3512
  }
3413
3513
  print("");
@@ -3440,6 +3540,9 @@ async function runUpgrade(opts = {}) {
3440
3540
  print("Run it yourself (--run can't safely automate this install method):");
3441
3541
  }
3442
3542
  print("");
3543
+ if (installRoot) {
3544
+ print(`in ${installRoot}:`);
3545
+ }
3443
3546
  print(` ${plan.command}`);
3444
3547
  return { exitCode: 1, lines };
3445
3548
  }
@@ -3460,7 +3563,7 @@ async function runUpgrade(opts = {}) {
3460
3563
  const code = await runner(runSpec.cmd, runSpec.args, runSpec.cwd);
3461
3564
  if (code === 0) {
3462
3565
  print("");
3463
- print(`\u2713 Upgraded @yawlabs/mcp to ${latest}`);
3566
+ print(`OK: Upgraded @yawlabs/mcp to ${latest}`);
3464
3567
  return { exitCode: 0, lines };
3465
3568
  }
3466
3569
  printErr(`yaw-mcp upgrade: ${runSpec.cmd} exited ${code}. Try running the command yourself:`);
@@ -3469,7 +3572,7 @@ async function runUpgrade(opts = {}) {
3469
3572
  return { exitCode: 3, lines };
3470
3573
  }
3471
3574
  function readCurrentVersion() {
3472
- return true ? "0.60.6" : "dev";
3575
+ return true ? "0.62.0" : "dev";
3473
3576
  }
3474
3577
 
3475
3578
  // src/usage-hints.ts
@@ -3497,7 +3600,7 @@ function buildCoUsageMap(packs) {
3497
3600
  function formatUsageHint(usage, coUsedWith) {
3498
3601
  const parts = [];
3499
3602
  if (usage && usage.succeeded >= MIN_SUCCESS_TO_SHOW) {
3500
- parts.push(`used ${usage.succeeded}x`);
3603
+ parts.push(`used ${Math.round(usage.succeeded)}x`);
3501
3604
  }
3502
3605
  if (coUsedWith.length > 0) {
3503
3606
  const shown = coUsedWith.slice(0, MAX_PEERS);
@@ -3531,7 +3634,11 @@ function selectFlakyNamespaces(entries, limit) {
3531
3634
  }
3532
3635
 
3533
3636
  // src/doctor-cmd.ts
3534
- var VERSION = true ? "0.60.6" : "dev";
3637
+ var VERSION = true ? "0.62.0" : "dev";
3638
+ function isPersistenceDisabled(env) {
3639
+ const raw = env.YAW_MCP_DISABLE_PERSISTENCE;
3640
+ return raw !== void 0 && raw !== "" && (raw === "1" || raw.toLowerCase() === "true");
3641
+ }
3535
3642
  async function runDoctor(opts = {}) {
3536
3643
  if (opts.json) return runDoctorJson(opts);
3537
3644
  const lines = [];
@@ -3568,8 +3675,16 @@ async function runDoctor(opts = {}) {
3568
3675
  print(` source: ${config.apiBaseSource}`);
3569
3676
  print("");
3570
3677
  renderEnvSection({ env, print });
3571
- await renderStateSection({ home, env, print });
3572
- await renderReliabilitySection({ home, env, print });
3678
+ const persistenceDisabled = isPersistenceDisabled(env);
3679
+ const stateFilePath = join8(userConfigDir(home), STATE_FILENAME);
3680
+ const persistedState = persistenceDisabled ? null : await loadState(stateFilePath);
3681
+ await renderStateSection({
3682
+ filePath: stateFilePath,
3683
+ disabled: persistenceDisabled,
3684
+ persisted: persistedState,
3685
+ print
3686
+ });
3687
+ renderReliabilitySection({ disabled: persistenceDisabled, persisted: persistedState, print });
3573
3688
  await renderTrialsSection({ home, env, print, postEvent: opts.postTryEvent, now: opts.now });
3574
3689
  renderBackgroundPostersSection({ print });
3575
3690
  const claudeConfigDir = env.CLAUDE_CONFIG_DIR && env.CLAUDE_CONFIG_DIR.length > 0 ? env.CLAUDE_CONFIG_DIR : void 0;
@@ -3599,29 +3714,31 @@ async function runDoctor(opts = {}) {
3599
3714
  }
3600
3715
  print("");
3601
3716
  }
3602
- const skipCheck = opts.skipRegistryCheck === true || Boolean(process.env.VITEST);
3717
+ const skipCheck = (opts.skipRegistryCheck === true || Boolean(process.env.VITEST)) && !opts.registryFetch;
3603
3718
  const latest = skipCheck ? null : await fetchLatestVersion(opts.registryFetch);
3604
- const staleHint = latest && VERSION !== "dev" && compareSemver(VERSION, latest) < 0 ? latest : null;
3719
+ const effectiveVersion = opts.currentVersion ?? VERSION;
3720
+ const staleHint = latest && effectiveVersion !== "dev" && compareSemver(effectiveVersion, latest) < 0 ? latest : null;
3605
3721
  if (staleHint) {
3606
- const method = await detectSea() ? "binary" : await refineInstallMethod(detectInstallMethod(process.argv[1]), process.argv[1]);
3722
+ const effectiveArgvPath = opts.argvPath ?? process.argv[1];
3723
+ const method = await detectSea() ? "binary" : await refineInstallMethod(detectInstallMethod(effectiveArgvPath), effectiveArgvPath);
3607
3724
  print("UPGRADE AVAILABLE");
3608
3725
  if (method === "bundled-app") {
3609
- print(` Running ${VERSION}; npm latest is ${staleHint}. This copy ships inside`);
3726
+ print(` Running ${effectiveVersion}; npm latest is ${staleHint}. This copy ships inside`);
3610
3727
  print(" Yaw Terminal and updates with the app \u2014 update Yaw Terminal to get it.");
3611
3728
  } else if (method === "npx") {
3612
- print(` Running ${VERSION}; npm latest is ${staleHint}. npx fetches the latest`);
3729
+ print(` Running ${effectiveVersion}; npm latest is ${staleHint}. npx fetches the latest`);
3613
3730
  print(" on each spawn \u2014 restart your MCP client to pick it up.");
3614
3731
  } else if (method === "binary") {
3615
- print(` Running ${VERSION}; npm latest is ${staleHint}. This is a standalone`);
3732
+ print(` Running ${effectiveVersion}; npm latest is ${staleHint}. This is a standalone`);
3616
3733
  print(" binary \u2014 download the latest build and replace the executable:");
3617
3734
  print(` ${BINARY_DOWNLOAD_URL}`);
3618
3735
  } else if (method === "global-npm" || method === "pnpm-global" || method === "bun-global" || method === "local-node-modules") {
3619
- print(` Running ${VERSION}; npm latest is ${staleHint}. To upgrade in place:`);
3736
+ print(` Running ${effectiveVersion}; npm latest is ${staleHint}. To upgrade in place:`);
3620
3737
  print("");
3621
3738
  print(" yaw-mcp upgrade --run");
3622
3739
  } else {
3623
- const plan = buildUpgradePlan({ current: VERSION, latest: staleHint, method });
3624
- print(` Running ${VERSION}; npm latest is ${staleHint}. To upgrade:`);
3740
+ const plan = buildUpgradePlan({ current: effectiveVersion, latest: staleHint, method });
3741
+ print(` Running ${effectiveVersion}; npm latest is ${staleHint}. To upgrade:`);
3625
3742
  print("");
3626
3743
  print(` ${plan.command ?? "npm install -g @yawlabs/mcp@latest"}`);
3627
3744
  }
@@ -3668,24 +3785,21 @@ async function runDoctorJson(opts) {
3668
3785
  const raw = env[name];
3669
3786
  envOverrides[name] = raw === void 0 || raw === "" ? null : raw;
3670
3787
  }
3671
- const persistRaw = env.YAW_MCP_DISABLE_PERSISTENCE;
3672
- const persistDisabled = persistRaw !== void 0 && persistRaw !== "" && (persistRaw === "1" || persistRaw.toLowerCase() === "true");
3673
- const state = persistDisabled ? { disabled: true, path: null, savedAt: null, learningEntries: null, packHistoryEntries: null } : await (async () => {
3674
- const filePath = join8(userConfigDir(home), STATE_FILENAME);
3675
- const persisted = await loadState(filePath);
3788
+ const persistDisabled = isPersistenceDisabled(env);
3789
+ const stateFilePath = join8(userConfigDir(home), STATE_FILENAME);
3790
+ const persisted = persistDisabled ? null : await loadState(stateFilePath);
3791
+ const state = persistDisabled || !persisted ? { disabled: true, path: null, savedAt: null, learningEntries: null, packHistoryEntries: null } : (() => {
3676
3792
  const fresh = persisted.savedAt === 0;
3677
3793
  return {
3678
3794
  disabled: false,
3679
- path: filePath,
3795
+ path: stateFilePath,
3680
3796
  savedAt: fresh ? null : new Date(persisted.savedAt).toISOString(),
3681
3797
  learningEntries: fresh ? 0 : Object.keys(persisted.learning).length,
3682
3798
  packHistoryEntries: fresh ? 0 : persisted.packHistory.length
3683
3799
  };
3684
3800
  })();
3685
3801
  const reliability = [];
3686
- if (!persistDisabled) {
3687
- const filePath = join8(userConfigDir(home), STATE_FILENAME);
3688
- const persisted = await loadState(filePath);
3802
+ if (!persistDisabled && persisted) {
3689
3803
  if (persisted.savedAt !== 0) {
3690
3804
  const entries = Object.entries(persisted.learning).map(([namespace, usage]) => ({ namespace, usage }));
3691
3805
  for (const { namespace, usage } of selectFlakyNamespaces(entries, 5)) {
@@ -3700,9 +3814,10 @@ async function runDoctorJson(opts) {
3700
3814
  }
3701
3815
  }
3702
3816
  const shellShadows = scanShellHistoryForShadows({ home, env });
3703
- const skipCheck = opts.skipRegistryCheck === true || Boolean(process.env.VITEST);
3817
+ const skipCheck = (opts.skipRegistryCheck === true || Boolean(process.env.VITEST)) && !opts.registryFetch;
3704
3818
  const latest = skipCheck ? null : await fetchLatestVersion(opts.registryFetch);
3705
- const stale = latest !== null && VERSION !== "dev" && compareSemver(VERSION, latest) < 0;
3819
+ const effectiveVersion = opts.currentVersion ?? VERSION;
3820
+ const stale = latest !== null && effectiveVersion !== "dev" && compareSemver(effectiveVersion, latest) < 0;
3706
3821
  let exitCode = 0;
3707
3822
  let summary;
3708
3823
  if (config.token === null) {
@@ -3731,7 +3846,7 @@ async function runDoctorJson(opts) {
3731
3846
  reliability,
3732
3847
  clients,
3733
3848
  shellShadows,
3734
- upgrade: { current: VERSION, latest, stale },
3849
+ upgrade: { current: effectiveVersion, latest, stale },
3735
3850
  diagnosis: { exitCode, summary }
3736
3851
  };
3737
3852
  const blob = JSON.stringify(snapshotJson, null, 2);
@@ -3760,16 +3875,13 @@ function renderEnvSection(opts) {
3760
3875
  print("");
3761
3876
  }
3762
3877
  async function renderStateSection(opts) {
3763
- const { home, env, print } = opts;
3764
- const raw = env.YAW_MCP_DISABLE_PERSISTENCE;
3765
- const disabled = raw !== void 0 && raw !== "" && (raw === "1" || raw.toLowerCase() === "true");
3878
+ const { filePath, disabled, persisted, print } = opts;
3766
3879
  print("STATE");
3767
3880
  if (disabled) {
3768
3881
  print(" status: disabled via YAW_MCP_DISABLE_PERSISTENCE");
3769
3882
  print("");
3770
3883
  return;
3771
3884
  }
3772
- const filePath = join8(userConfigDir(home), STATE_FILENAME);
3773
3885
  print(` path: ${filePath}`);
3774
3886
  const peek = await peekStateFile(filePath);
3775
3887
  if (peek.kind === "malformed") {
@@ -3790,8 +3902,7 @@ async function renderStateSection(opts) {
3790
3902
  print("");
3791
3903
  return;
3792
3904
  }
3793
- const persisted = await loadState(filePath);
3794
- if (persisted.savedAt === 0) {
3905
+ if (!persisted || persisted.savedAt === 0) {
3795
3906
  print(" (no persisted state yet \u2014 will be created on the first tool call)");
3796
3907
  } else {
3797
3908
  print(` last saved: ${formatRelativeAge(Date.now() - persisted.savedAt)} ago`);
@@ -3823,13 +3934,9 @@ async function peekStateFile(filePath) {
3823
3934
  }
3824
3935
  return { kind: "ok" };
3825
3936
  }
3826
- async function renderReliabilitySection(opts) {
3827
- const { home, env, print } = opts;
3828
- const raw = env.YAW_MCP_DISABLE_PERSISTENCE;
3829
- const disabled = raw !== void 0 && raw !== "" && (raw === "1" || raw.toLowerCase() === "true");
3830
- if (disabled) return;
3831
- const filePath = join8(userConfigDir(home), STATE_FILENAME);
3832
- const persisted = await loadState(filePath);
3937
+ function renderReliabilitySection(opts) {
3938
+ const { disabled, persisted, print } = opts;
3939
+ if (disabled || !persisted) return;
3833
3940
  if (persisted.savedAt === 0) return;
3834
3941
  const entries = Object.entries(persisted.learning).map(([namespace, usage]) => ({ namespace, usage }));
3835
3942
  const flaky = selectFlakyNamespaces(entries, 5);
@@ -3855,8 +3962,8 @@ async function renderTrialsSection(opts) {
3855
3962
  for (const { marker, msUntilExpiry } of scan.live) {
3856
3963
  print(` ${marker.slug} -> ${marker.clientName} (${marker.clientPath}) \u2014 expires in ${formatTtl(msUntilExpiry)}`);
3857
3964
  }
3858
- for (const path3 of scan.malformed) {
3859
- print(` ! malformed marker at ${path3} (delete by hand)`);
3965
+ for (const path5 of scan.malformed) {
3966
+ print(` ! malformed marker at ${path5} (delete by hand)`);
3860
3967
  }
3861
3968
  print("");
3862
3969
  }
@@ -3975,9 +4082,9 @@ function probeClients(opts) {
3975
4082
  }
3976
4083
  return out;
3977
4084
  }
3978
- function walkContainer(root, path3) {
4085
+ function walkContainer(root, path5) {
3979
4086
  let cur = root;
3980
- for (const key of path3) {
4087
+ for (const key of path5) {
3981
4088
  if (typeof cur !== "object" || cur === null || Array.isArray(cur)) return null;
3982
4089
  cur = cur[key];
3983
4090
  }
@@ -4018,6 +4125,7 @@ async function probeClientsAsync(opts) {
4018
4125
  let malformed = false;
4019
4126
  if (exists3) {
4020
4127
  try {
4128
+ await stat3(resolved.absolute);
4021
4129
  const raw = await readFile7(resolved.absolute, "utf8");
4022
4130
  if (raw.trim().length > 0) {
4023
4131
  const parsed = parseJsonc(raw);
@@ -4124,9 +4232,9 @@ function shellHistorySources(opts) {
4124
4232
  }
4125
4233
  return sources;
4126
4234
  }
4127
- function readTailLines(path3, n) {
4235
+ function readTailLines(path5, n) {
4128
4236
  try {
4129
- const raw = readFileSync(path3, "utf8");
4237
+ const raw = readFileSync(path5, "utf8");
4130
4238
  const all = raw.split(/\r?\n/);
4131
4239
  return all.length <= n ? all : all.slice(all.length - n);
4132
4240
  } catch {
@@ -4171,97 +4279,494 @@ function compareSemver(a, b) {
4171
4279
  return 0;
4172
4280
  }
4173
4281
 
4174
- // src/fuzzy.ts
4175
- function levenshtein(a, b) {
4176
- if (a === b) return 0;
4177
- const aLen = a.length;
4178
- const bLen = b.length;
4179
- if (aLen === 0) return bLen;
4180
- if (bLen === 0) return aLen;
4181
- let prev = new Array(bLen + 1);
4182
- let curr = new Array(bLen + 1);
4183
- for (let j = 0; j <= bLen; j++) prev[j] = j;
4184
- for (let i = 1; i <= aLen; i++) {
4185
- curr[0] = i;
4186
- for (let j = 1; j <= bLen; j++) {
4187
- const cost = a.charCodeAt(i - 1) === b.charCodeAt(j - 1) ? 0 : 1;
4188
- curr[j] = Math.min(
4189
- curr[j - 1] + 1,
4190
- // insertion
4191
- prev[j] + 1,
4192
- // deletion
4193
- prev[j - 1] + cost
4194
- // substitution
4195
- );
4282
+ // src/foundry-cmd.ts
4283
+ import { mkdirSync, readFileSync as readFileSync3, writeFileSync } from "fs";
4284
+ import { homedir as homedir10 } from "os";
4285
+ import path3 from "path";
4286
+
4287
+ // src/foundry-corpus.ts
4288
+ import { readFileSync as readFileSync2 } from "fs";
4289
+
4290
+ // src/relevance.ts
4291
+ var K1 = 1.2;
4292
+ var B = 0.75;
4293
+ var FIELD_WEIGHTS = {
4294
+ name: 3,
4295
+ namespace: 2,
4296
+ description: 1.5,
4297
+ toolName: 2,
4298
+ toolDescription: 1
4299
+ };
4300
+ var MIN_TOKEN_LEN = 3;
4301
+ function tokenize(text) {
4302
+ if (!text) return [];
4303
+ return text.toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length >= MIN_TOKEN_LEN);
4304
+ }
4305
+ function buildDocFields(server) {
4306
+ const toolNameTokens = [];
4307
+ const toolDescriptionTokens = [];
4308
+ for (const tool of server.tools) {
4309
+ toolNameTokens.push(...tokenize(tool.name));
4310
+ toolDescriptionTokens.push(...tokenize(tool.description));
4311
+ }
4312
+ return {
4313
+ namespace: tokenize(server.namespace),
4314
+ name: tokenize(server.name),
4315
+ description: tokenize(server.description),
4316
+ toolName: toolNameTokens,
4317
+ toolDescription: toolDescriptionTokens
4318
+ };
4319
+ }
4320
+ function termFreq(tokens, term) {
4321
+ let count = 0;
4322
+ for (const t of tokens) {
4323
+ if (t === term) count++;
4324
+ }
4325
+ return count;
4326
+ }
4327
+ function bm25Score(queryTerms, fields, avgFieldLen, idfValues) {
4328
+ let score = 0;
4329
+ const seen = /* @__PURE__ */ new Set();
4330
+ for (const term of queryTerms) {
4331
+ if (seen.has(term)) continue;
4332
+ seen.add(term);
4333
+ const termIdf = idfValues.get(term);
4334
+ if (termIdf === void 0 || termIdf <= 0) continue;
4335
+ for (const [fieldName, weight] of Object.entries(FIELD_WEIGHTS)) {
4336
+ const fieldTokens = fields[fieldName];
4337
+ if (fieldTokens.length === 0) continue;
4338
+ const tf = termFreq(fieldTokens, term);
4339
+ if (tf === 0) continue;
4340
+ const avg = avgFieldLen[fieldName] || 1;
4341
+ const normLen = 1 - B + B * (fieldTokens.length / avg);
4342
+ const numerator = tf * (K1 + 1);
4343
+ const denominator = tf + K1 * normLen;
4344
+ score += weight * termIdf * (numerator / denominator);
4196
4345
  }
4197
- [prev, curr] = [curr, prev];
4198
4346
  }
4199
- return prev[bLen];
4347
+ return score;
4200
4348
  }
4201
- function closestNames(query, candidates, limit) {
4202
- if (limit <= 0) return [];
4203
- const q = query.toLowerCase();
4204
- const scored = [];
4205
- for (const c of candidates) {
4206
- if (c === query) continue;
4207
- const lc = c.toLowerCase();
4208
- let score = null;
4209
- if (lc === q) {
4210
- score = 0;
4211
- } else if (lc.startsWith(q) || q.startsWith(lc)) {
4212
- score = 1;
4213
- } else if (lc.includes(q) || q.includes(lc)) {
4214
- score = 2;
4215
- } else {
4216
- const d = levenshtein(q, lc);
4217
- if (d <= 2) score = 2 + d;
4349
+ function rankServers(context, servers) {
4350
+ const queryTerms = tokenize(context);
4351
+ if (queryTerms.length === 0 || servers.length === 0) return [];
4352
+ const docsWithFields = servers.map((s) => ({ server: s, fields: buildDocFields(s) }));
4353
+ const N = docsWithFields.length;
4354
+ const df = /* @__PURE__ */ new Map();
4355
+ for (const { fields } of docsWithFields) {
4356
+ const bag = /* @__PURE__ */ new Set([
4357
+ ...fields.namespace,
4358
+ ...fields.name,
4359
+ ...fields.description,
4360
+ ...fields.toolName,
4361
+ ...fields.toolDescription
4362
+ ]);
4363
+ for (const term of bag) {
4364
+ df.set(term, (df.get(term) ?? 0) + 1);
4218
4365
  }
4219
- if (score !== null) scored.push({ name: c, score });
4220
4366
  }
4221
- scored.sort((a, b) => {
4222
- if (a.score !== b.score) return a.score - b.score;
4223
- return a.name.localeCompare(b.name);
4367
+ const idfValues = /* @__PURE__ */ new Map();
4368
+ for (const [term, d] of df) {
4369
+ idfValues.set(term, Math.log((N - d + 0.5) / (d + 0.5) + 1));
4370
+ }
4371
+ const totalLen = {
4372
+ namespace: 0,
4373
+ name: 0,
4374
+ description: 0,
4375
+ toolName: 0,
4376
+ toolDescription: 0
4377
+ };
4378
+ for (const { fields } of docsWithFields) {
4379
+ totalLen.namespace += fields.namespace.length;
4380
+ totalLen.name += fields.name.length;
4381
+ totalLen.description += fields.description.length;
4382
+ totalLen.toolName += fields.toolName.length;
4383
+ totalLen.toolDescription += fields.toolDescription.length;
4384
+ }
4385
+ const denom = Math.max(N, 1);
4386
+ const avgFieldLen = {
4387
+ namespace: totalLen.namespace / denom,
4388
+ name: totalLen.name / denom,
4389
+ description: totalLen.description / denom,
4390
+ toolName: totalLen.toolName / denom,
4391
+ toolDescription: totalLen.toolDescription / denom
4392
+ };
4393
+ const results = [];
4394
+ for (const { server, fields } of docsWithFields) {
4395
+ const score = bm25Score(queryTerms, fields, avgFieldLen, idfValues);
4396
+ if (score > 0) {
4397
+ results.push({ namespace: server.namespace, score });
4398
+ }
4399
+ }
4400
+ results.sort((a, b) => {
4401
+ if (b.score !== a.score) return b.score - a.score;
4402
+ return a.namespace < b.namespace ? -1 : 1;
4224
4403
  });
4225
- return scored.slice(0, limit).map((s) => s.name);
4404
+ return results;
4226
4405
  }
4227
4406
 
4228
- // src/local-add-cmd.ts
4229
- import { homedir as homedir9 } from "os";
4230
- var SLUG_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
4231
- var ADD_USAGE = `Usage: yaw-mcp add <slug> [flags]
4232
-
4233
- Resolve <slug> from the yaw.sh/mcp catalog and add it to your local
4234
- ~/.yaw-mcp/bundles.json so yaw-mcp loads it (no account needed).
4235
-
4236
- This is NOT the same as \`yaw-mcp install\` -- install wires the yaw-mcp
4237
- aggregator into an AI client; add adds an MCP server to yaw-mcp itself.
4238
-
4239
- --env KEY=value Provide a required env var's value. Repeatable. Required
4240
- vars not given here AND not in your shell block the add.
4241
- --dry-run Print what would be written without writing.
4242
- --json Emit the written entry as JSON (implies success on stdout).
4243
- --catalog <url> Override the catalog URL (default the public catalog).`;
4244
- function parseEnvFlag(v, bag) {
4245
- if (!v || !v.includes("=")) return "--env requires KEY=value";
4246
- const eq = v.indexOf("=");
4247
- const key = v.slice(0, eq);
4248
- if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) return `--env: invalid KEY "${key}"`;
4249
- bag[key] = v.slice(eq + 1);
4250
- return null;
4251
- }
4252
- function parseAddArgs(argv) {
4253
- if (argv.length === 0) return { ok: false, error: ADD_USAGE };
4254
- const positional = [];
4255
- const opts = {};
4256
- const env = {};
4257
- for (let i = 0; i < argv.length; i++) {
4258
- const a = argv[i];
4259
- const next = () => argv[++i];
4260
- switch (a) {
4261
- case "--env": {
4262
- const e = parseEnvFlag(next(), env);
4263
- if (e) return { ok: false, error: e };
4264
- break;
4407
+ // src/foundry-corpus.ts
4408
+ var FOUNDRY_CORPUS_VERSION = 1;
4409
+ var DEFAULT_CORPUS_CAP = 500;
4410
+ function parseTraceLines(text) {
4411
+ const out = [];
4412
+ for (const line of text.split("\n")) {
4413
+ const trimmed = line.trim();
4414
+ if (!trimmed) continue;
4415
+ try {
4416
+ const obj = JSON.parse(trimmed);
4417
+ if (obj && Array.isArray(obj.tokens) && typeof obj.chosen === "string") {
4418
+ out.push(obj);
4419
+ }
4420
+ } catch {
4421
+ }
4422
+ }
4423
+ return out;
4424
+ }
4425
+ function entryKey(tokens, chosen) {
4426
+ return `${tokens.join(" ")}::${chosen}`;
4427
+ }
4428
+ function capStratified(entries, cap) {
4429
+ if (entries.length <= cap) return entries;
4430
+ const byChosen = /* @__PURE__ */ new Map();
4431
+ for (const e of entries) {
4432
+ const g = byChosen.get(e.chosen);
4433
+ if (g) g.push(e);
4434
+ else byChosen.set(e.chosen, [e]);
4435
+ }
4436
+ const groups = [...byChosen.entries()].sort((a, b) => a[0] < b[0] ? -1 : 1).map(([, g]) => g.sort((x, y) => y.weight - x.weight));
4437
+ const out = [];
4438
+ let i = 0;
4439
+ while (out.length < cap) {
4440
+ let took = false;
4441
+ for (const g of groups) {
4442
+ if (i < g.length) {
4443
+ out.push(g[i]);
4444
+ took = true;
4445
+ if (out.length >= cap) break;
4446
+ }
4447
+ }
4448
+ if (!took) break;
4449
+ i++;
4450
+ }
4451
+ return out;
4452
+ }
4453
+ function buildCorpusFromTraces(traces, servers, opts = {}) {
4454
+ const known = new Set(servers.map((s) => s.namespace));
4455
+ const byKey = /* @__PURE__ */ new Map();
4456
+ for (const t of traces) {
4457
+ if (!t || typeof t.chosen !== "string" || !known.has(t.chosen)) continue;
4458
+ if (!Array.isArray(t.tokens) || t.tokens.length === 0) continue;
4459
+ const tokens = [...t.tokens].filter((x) => typeof x === "string").sort();
4460
+ if (tokens.length === 0) continue;
4461
+ const key = entryKey(tokens, t.chosen);
4462
+ const prev = byKey.get(key);
4463
+ if (prev) prev.weight += 1;
4464
+ else byKey.set(key, { tokens, chosen: t.chosen, weight: 1 });
4465
+ }
4466
+ const entries = capStratified([...byKey.values()], opts.cap ?? DEFAULT_CORPUS_CAP);
4467
+ return { version: FOUNDRY_CORPUS_VERSION, servers, entries };
4468
+ }
4469
+ function scoreCorpus(corpus) {
4470
+ let totalWeight = 0;
4471
+ let top1Weight = 0;
4472
+ let top3Weight = 0;
4473
+ for (const e of corpus.entries) {
4474
+ totalWeight += e.weight;
4475
+ const top3 = rankServers(e.tokens.join(" "), corpus.servers).slice(0, 3).map((r) => r.namespace);
4476
+ if (top3[0] === e.chosen) top1Weight += e.weight;
4477
+ if (top3.includes(e.chosen)) top3Weight += e.weight;
4478
+ }
4479
+ return {
4480
+ totalWeight,
4481
+ top1Weight,
4482
+ top3Weight,
4483
+ top1: totalWeight > 0 ? top1Weight / totalWeight : 0,
4484
+ top3: totalWeight > 0 ? top3Weight / totalWeight : 0
4485
+ };
4486
+ }
4487
+
4488
+ // src/foundry.ts
4489
+ import { appendFile, mkdir as mkdir3, stat as stat4 } from "fs/promises";
4490
+ import { homedir as homedir9 } from "os";
4491
+ import path2 from "path";
4492
+ var SECRET_PREFIXES = ["sk_", "sk-", "tok_", "ghp_", "gho_", "xox", "pk_", "akia"];
4493
+ function looksSensitive(token5) {
4494
+ for (const prefix of SECRET_PREFIXES) {
4495
+ if (token5.startsWith(prefix)) return true;
4496
+ }
4497
+ if (token5.length >= 16 && /^[0-9a-f]+$/.test(token5)) return true;
4498
+ if (token5.length >= 12 && /[a-z]/.test(token5) && /[0-9]/.test(token5)) return true;
4499
+ if (token5.length >= 16 && /^[a-z]+$/.test(token5)) return true;
4500
+ return false;
4501
+ }
4502
+ function redactIntent(intent) {
4503
+ const all = tokenize(intent);
4504
+ const tokens = [];
4505
+ let redactedCount = 0;
4506
+ for (const token5 of all) {
4507
+ if (looksSensitive(token5)) {
4508
+ redactedCount++;
4509
+ } else {
4510
+ tokens.push(token5);
4511
+ }
4512
+ }
4513
+ tokens.sort();
4514
+ return { tokens, redactedCount };
4515
+ }
4516
+ function isFoundryEnabled() {
4517
+ const raw = process.env.YAW_MCP_FOUNDRY;
4518
+ if (!raw) return false;
4519
+ const v = raw.trim().toLowerCase();
4520
+ return v === "1" || v === "true";
4521
+ }
4522
+ var MAX_FOUNDRY_BYTES = 5 * 1024 * 1024;
4523
+ var FOUNDRY_FILENAME = "foundry.jsonl";
4524
+ async function appendFoundryTrace(trace, home = homedir9()) {
4525
+ try {
4526
+ if (!isFoundryEnabled()) return;
4527
+ const dir = userConfigDir(home);
4528
+ const file = path2.join(dir, FOUNDRY_FILENAME);
4529
+ try {
4530
+ const info = await stat4(file);
4531
+ if (info.size >= MAX_FOUNDRY_BYTES) return;
4532
+ } catch {
4533
+ }
4534
+ const line = `${JSON.stringify({
4535
+ tokens: trace.tokens,
4536
+ candidates: trace.candidates,
4537
+ chosen: trace.chosen,
4538
+ redactedCount: trace.redactedCount
4539
+ })}
4540
+ `;
4541
+ await mkdir3(dir, { recursive: true });
4542
+ await appendFile(file, line, "utf8");
4543
+ } catch {
4544
+ }
4545
+ }
4546
+
4547
+ // src/foundry-cmd.ts
4548
+ var DEFAULT_OUT = path3.join("src", "tests", "fixtures", "foundry-corpus.json");
4549
+ var FOUNDRY_USAGE = `Usage: yaw-mcp foundry export [--out <path>] [--cap <n>] [--json]
4550
+
4551
+ Fold the opt-in dispatch harvest (~/.yaw-mcp/foundry.jsonl) into a routing
4552
+ regression corpus consumed by the foundry-routing test gate. Maintainer
4553
+ command: requires a local bundles.json for the server-catalog snapshot.
4554
+
4555
+ --out <path> Where to write the corpus (default: ${DEFAULT_OUT}).
4556
+ --cap <n> Max entries, stratified by chosen server (default: ${DEFAULT_CORPUS_CAP}).
4557
+ --json Emit a machine-readable summary instead of text.`;
4558
+ function parseFoundryArgs(argv) {
4559
+ let action;
4560
+ let out = DEFAULT_OUT;
4561
+ let cap = DEFAULT_CORPUS_CAP;
4562
+ let json = false;
4563
+ for (let i = 0; i < argv.length; i++) {
4564
+ const a = argv[i];
4565
+ if (a === "--help" || a === "-h") return { ok: false, error: FOUNDRY_USAGE };
4566
+ if (a === "--json") {
4567
+ json = true;
4568
+ } else if (a === "--out") {
4569
+ const v = argv[++i];
4570
+ if (!v) return { ok: false, error: `yaw-mcp foundry: --out needs a path
4571
+
4572
+ ${FOUNDRY_USAGE}` };
4573
+ out = v;
4574
+ } else if (a === "--cap") {
4575
+ const v = Number(argv[++i]);
4576
+ if (!Number.isFinite(v) || v <= 0)
4577
+ return { ok: false, error: `yaw-mcp foundry: --cap needs a positive number
4578
+
4579
+ ${FOUNDRY_USAGE}` };
4580
+ cap = Math.floor(v);
4581
+ } else if (a.startsWith("-")) {
4582
+ return { ok: false, error: `yaw-mcp foundry: unknown argument "${a}"
4583
+
4584
+ ${FOUNDRY_USAGE}` };
4585
+ } else if (action === void 0) {
4586
+ if (a !== "export")
4587
+ return { ok: false, error: `yaw-mcp foundry: unknown action "${a}" (only "export")
4588
+
4589
+ ${FOUNDRY_USAGE}` };
4590
+ action = a;
4591
+ } else {
4592
+ return { ok: false, error: `yaw-mcp foundry: unexpected extra argument "${a}"
4593
+
4594
+ ${FOUNDRY_USAGE}` };
4595
+ }
4596
+ }
4597
+ if (action === void 0) return { ok: false, error: `yaw-mcp foundry: missing action.
4598
+
4599
+ ${FOUNDRY_USAGE}` };
4600
+ return { ok: true, options: { action, out, cap, json } };
4601
+ }
4602
+ async function defaultLoadServers(cwd, home) {
4603
+ const { config } = await loadLocalBundles({ cwd, home });
4604
+ return (config?.servers ?? []).map((s) => ({
4605
+ namespace: s.namespace,
4606
+ name: s.name,
4607
+ description: s.description,
4608
+ tools: s.toolCache ?? []
4609
+ }));
4610
+ }
4611
+ async function runFoundryExport(opts) {
4612
+ const write = opts.write ?? ((s) => process.stdout.write(s));
4613
+ const writeErr = opts.writeErr ?? ((s) => process.stderr.write(s));
4614
+ const lines = [];
4615
+ const print = (s = "") => {
4616
+ lines.push(s);
4617
+ write(`${s}
4618
+ `);
4619
+ };
4620
+ const printErr = (s) => {
4621
+ lines.push(s);
4622
+ writeErr(`${s}
4623
+ `);
4624
+ };
4625
+ const home = opts.home ?? homedir10();
4626
+ const harvestPath = path3.join(userConfigDir(home), FOUNDRY_FILENAME);
4627
+ const blob = opts.readTraces ? opts.readTraces() : (() => {
4628
+ try {
4629
+ return readFileSync3(harvestPath, "utf8");
4630
+ } catch {
4631
+ return null;
4632
+ }
4633
+ })();
4634
+ if (blob === null) {
4635
+ printErr(`yaw-mcp foundry: no harvest at ${harvestPath}. Set YAW_MCP_FOUNDRY=1 and dispatch first.`);
4636
+ return { exitCode: 1, lines };
4637
+ }
4638
+ const traces = parseTraceLines(blob);
4639
+ if (traces.length === 0) {
4640
+ printErr(`yaw-mcp foundry: ${harvestPath} has no parseable traces.`);
4641
+ return { exitCode: 1, lines };
4642
+ }
4643
+ const servers = opts.loadServers ? await opts.loadServers() : await defaultLoadServers(opts.cwd, home);
4644
+ const corpus = buildCorpusFromTraces(traces, servers, { cap: opts.cap });
4645
+ if (corpus.entries.length === 0) {
4646
+ printErr(
4647
+ `yaw-mcp foundry: ${traces.length} traces but 0 usable entries -- none of the chosen servers are in the local catalog (${servers.length} servers).`
4648
+ );
4649
+ return { exitCode: 2, lines };
4650
+ }
4651
+ mkdirSync(path3.dirname(path3.resolve(opts.out)), { recursive: true });
4652
+ writeFileSync(opts.out, `${JSON.stringify(corpus, null, 2)}
4653
+ `, "utf8");
4654
+ const score = scoreCorpus(corpus);
4655
+ if (opts.json) {
4656
+ print(
4657
+ JSON.stringify(
4658
+ {
4659
+ out: opts.out,
4660
+ entries: corpus.entries.length,
4661
+ servers: corpus.servers.length,
4662
+ fromTraces: traces.length,
4663
+ top1: score.top1,
4664
+ top3: score.top3
4665
+ },
4666
+ null,
4667
+ 2
4668
+ )
4669
+ );
4670
+ return { exitCode: 0, lines };
4671
+ }
4672
+ print(`Wrote ${corpus.entries.length} entries (from ${traces.length} traces) to ${opts.out}`);
4673
+ print(
4674
+ `BM25-floor accuracy on this corpus: top-1 ${(score.top1 * 100).toFixed(1)}%, top-3 ${(score.top3 * 100).toFixed(1)}%`
4675
+ );
4676
+ return { exitCode: 0, lines };
4677
+ }
4678
+
4679
+ // src/fuzzy.ts
4680
+ function levenshtein(a, b) {
4681
+ if (a === b) return 0;
4682
+ const aLen = a.length;
4683
+ const bLen = b.length;
4684
+ if (aLen === 0) return bLen;
4685
+ if (bLen === 0) return aLen;
4686
+ let prev = new Array(bLen + 1);
4687
+ let curr = new Array(bLen + 1);
4688
+ for (let j = 0; j <= bLen; j++) prev[j] = j;
4689
+ for (let i = 1; i <= aLen; i++) {
4690
+ curr[0] = i;
4691
+ for (let j = 1; j <= bLen; j++) {
4692
+ const cost = a.charCodeAt(i - 1) === b.charCodeAt(j - 1) ? 0 : 1;
4693
+ curr[j] = Math.min(
4694
+ curr[j - 1] + 1,
4695
+ // insertion
4696
+ prev[j] + 1,
4697
+ // deletion
4698
+ prev[j - 1] + cost
4699
+ // substitution
4700
+ );
4701
+ }
4702
+ [prev, curr] = [curr, prev];
4703
+ }
4704
+ return prev[bLen];
4705
+ }
4706
+ function closestNames(query, candidates, limit) {
4707
+ if (limit <= 0) return [];
4708
+ const q = query.toLowerCase();
4709
+ const scored = [];
4710
+ for (const c of candidates) {
4711
+ if (c === query) continue;
4712
+ const lc = c.toLowerCase();
4713
+ let score = null;
4714
+ if (lc === q) {
4715
+ score = 0;
4716
+ } else if (lc.startsWith(q) || q.startsWith(lc)) {
4717
+ score = 1;
4718
+ } else if (lc.includes(q) || q.includes(lc)) {
4719
+ score = 2;
4720
+ } else {
4721
+ const d = levenshtein(q, lc);
4722
+ if (d <= 2) score = 2 + d;
4723
+ }
4724
+ if (score !== null) scored.push({ name: c, score });
4725
+ }
4726
+ scored.sort((a, b) => {
4727
+ if (a.score !== b.score) return a.score - b.score;
4728
+ return a.name.localeCompare(b.name);
4729
+ });
4730
+ return scored.slice(0, limit).map((s) => s.name);
4731
+ }
4732
+
4733
+ // src/local-add-cmd.ts
4734
+ import { homedir as homedir11 } from "os";
4735
+ var SLUG_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
4736
+ var ADD_USAGE = `Usage: yaw-mcp add <slug> [flags]
4737
+
4738
+ Resolve <slug> from the yaw.sh/mcp catalog and add it to your local
4739
+ ~/.yaw-mcp/bundles.json so yaw-mcp loads it (no account needed).
4740
+
4741
+ This is NOT the same as \`yaw-mcp install\` -- install wires the yaw-mcp
4742
+ aggregator into an AI client; add adds an MCP server to yaw-mcp itself.
4743
+
4744
+ --env KEY=value Provide a required env var's value. Repeatable. Required
4745
+ vars not given here AND not in your shell block the add.
4746
+ --dry-run Print what would be written without writing.
4747
+ --json Emit the written entry as JSON (implies success on stdout).
4748
+ --catalog <url> Override the catalog URL (default the public catalog).`;
4749
+ function parseEnvFlag(v, bag) {
4750
+ if (!v || !v.includes("=")) return "--env requires KEY=value";
4751
+ const eq = v.indexOf("=");
4752
+ const key = v.slice(0, eq);
4753
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) return `--env: invalid KEY "${key}"`;
4754
+ bag[key] = v.slice(eq + 1);
4755
+ return null;
4756
+ }
4757
+ function parseAddArgs(argv) {
4758
+ if (argv.length === 0) return { ok: false, error: ADD_USAGE };
4759
+ const positional = [];
4760
+ const opts = {};
4761
+ const env = {};
4762
+ for (let i = 0; i < argv.length; i++) {
4763
+ const a = argv[i];
4764
+ const next = () => argv[++i];
4765
+ switch (a) {
4766
+ case "--env": {
4767
+ const e = parseEnvFlag(next(), env);
4768
+ if (e) return { ok: false, error: e };
4769
+ break;
4265
4770
  }
4266
4771
  case "--dry-run":
4267
4772
  opts.dryRun = true;
@@ -4277,7 +4782,7 @@ function parseAddArgs(argv) {
4277
4782
  }
4278
4783
  case "-h":
4279
4784
  case "--help":
4280
- return { ok: false, error: ADD_USAGE };
4785
+ return { ok: false, error: ADD_USAGE, help: true };
4281
4786
  default:
4282
4787
  if (a.startsWith("--")) return { ok: false, error: `Unknown flag: ${a}
4283
4788
  ${ADD_USAGE}` };
@@ -4309,7 +4814,7 @@ async function runAdd(opts) {
4309
4814
  return { exitCode: 2, written: [] };
4310
4815
  }
4311
4816
  const env = opts.env ?? process.env;
4312
- const home = opts.home ?? homedir9();
4817
+ const home = opts.home ?? homedir11();
4313
4818
  const cwd = opts.cwd ?? process.cwd();
4314
4819
  let server;
4315
4820
  try {
@@ -4336,6 +4841,10 @@ async function runAdd(opts) {
4336
4841
  const entryEnv = {};
4337
4842
  for (const k of server.requiredEnvKeys) entryEnv[k] = "";
4338
4843
  for (const [k, v] of Object.entries(opts.envOverrides ?? {})) entryEnv[k] = v;
4844
+ const overrides = opts.envOverrides ?? {};
4845
+ const ambientOnlyRequired = server.requiredEnvKeys.filter(
4846
+ (k) => (!overrides[k] || overrides[k] === "") && env[k] != null && env[k] !== ""
4847
+ );
4339
4848
  const entry = {
4340
4849
  id: `local-${namespace}`,
4341
4850
  name: server.name,
@@ -4371,6 +4880,11 @@ async function runAdd(opts) {
4371
4880
  print(`${res.replaced ? "Updated" : "Added"} ${server.name} (namespace "${namespace}") in ${res.path}`);
4372
4881
  print("Restart your MCP client (or yaw-mcp) to pick it up.");
4373
4882
  }
4883
+ if (ambientOnlyRequired.length > 0) {
4884
+ printErr(
4885
+ `Note: ${ambientOnlyRequired.join(", ")} ${ambientOnlyRequired.length === 1 ? "was" : "were"} read from your shell env and NOT persisted; the server depends on ${ambientOnlyRequired.length === 1 ? "that var" : "those vars"} being present wherever yaw-mcp launches. Pass --env ${ambientOnlyRequired[0]}=... to persist a value.`
4886
+ );
4887
+ }
4374
4888
  const shadow = await findShadowingProjectBundles(cwd, home).catch(() => null);
4375
4889
  if (shadow) {
4376
4890
  printErr(
@@ -4389,7 +4903,7 @@ function parseRemoveArgs(argv) {
4389
4903
  if (argv.length === 0) return { ok: false, error: REMOVE_USAGE };
4390
4904
  const positional = [];
4391
4905
  for (const a of argv) {
4392
- if (a === "-h" || a === "--help") return { ok: false, error: REMOVE_USAGE };
4906
+ if (a === "-h" || a === "--help") return { ok: false, error: REMOVE_USAGE, help: true };
4393
4907
  if (a.startsWith("--")) return { ok: false, error: `Unknown flag: ${a}
4394
4908
  ${REMOVE_USAGE}` };
4395
4909
  positional.push(a);
@@ -4415,7 +4929,7 @@ async function runRemove(opts) {
4415
4929
  printErr(`yaw-mcp remove: "${opts.target}" isn't a valid slug or namespace.`);
4416
4930
  return { exitCode: 2, written: [] };
4417
4931
  }
4418
- const home = opts.home ?? homedir9();
4932
+ const home = opts.home ?? homedir11();
4419
4933
  const cwd = opts.cwd ?? process.cwd();
4420
4934
  const derived = deriveNamespace(opts.target);
4421
4935
  const candidates = derived === opts.target ? [opts.target] : [opts.target, derived];
@@ -4459,7 +4973,7 @@ var LIST_USAGE = `Usage: yaw-mcp list [--json]
4459
4973
  function parseListArgs(argv) {
4460
4974
  const opts = {};
4461
4975
  for (const a of argv) {
4462
- if (a === "-h" || a === "--help") return { ok: false, error: LIST_USAGE };
4976
+ if (a === "-h" || a === "--help") return { ok: false, error: LIST_USAGE, help: true };
4463
4977
  if (a === "--json") {
4464
4978
  opts.json = true;
4465
4979
  continue;
@@ -4471,14 +4985,18 @@ ${LIST_USAGE}` };
4471
4985
  }
4472
4986
  async function runList(opts) {
4473
4987
  const out = opts.out ?? ((s) => process.stdout.write(s));
4988
+ const err = opts.err ?? ((s) => process.stderr.write(s));
4474
4989
  const print = (s = "") => out(`${s}
4475
4990
  `);
4476
- const home = opts.home ?? homedir9();
4991
+ const printErr = (s) => err(`${s}
4992
+ `);
4993
+ const home = opts.home ?? homedir11();
4477
4994
  const cwd = opts.cwd ?? process.cwd();
4478
4995
  const loaded = await loadLocalBundles({ home, cwd });
4479
4996
  const servers = loaded.config?.servers ?? [];
4997
+ for (const w of loaded.warnings) printErr(`warning: ${w}`);
4480
4998
  if (opts.json) {
4481
- print(JSON.stringify({ path: loaded.path, servers }, null, 2));
4999
+ print(JSON.stringify({ path: loaded.path, servers, warnings: loaded.warnings }, null, 2));
4482
5000
  return { exitCode: 0, written: [] };
4483
5001
  }
4484
5002
  if (servers.length === 0) {
@@ -4519,12 +5037,14 @@ function parseLoginArgs(argv) {
4519
5037
  const a = argv[i];
4520
5038
  if (a === "--key") {
4521
5039
  const v = argv[++i];
4522
- if (!v) return { ok: false, error: "yaw-mcp login: --key requires a value\n\n" + LOGIN_USAGE };
5040
+ if (!v) return { ok: false, error: `yaw-mcp login: --key requires a value
5041
+
5042
+ ${LOGIN_USAGE}` };
4523
5043
  opts.key = v;
4524
5044
  } else if (a === "--json") {
4525
5045
  opts.json = true;
4526
5046
  } else if (a === "--help" || a === "-h") {
4527
- return { ok: false, error: LOGIN_USAGE };
5047
+ return { ok: false, error: LOGIN_USAGE, help: true };
4528
5048
  } else {
4529
5049
  return { ok: false, error: `yaw-mcp login: unknown argument "${a}"
4530
5050
 
@@ -4532,7 +5052,9 @@ ${LOGIN_USAGE}` };
4532
5052
  }
4533
5053
  }
4534
5054
  if (!opts.key) {
4535
- return { ok: false, error: "yaw-mcp login: --key is required\n\n" + LOGIN_USAGE };
5055
+ return { ok: false, error: `yaw-mcp login: --key is required
5056
+
5057
+ ${LOGIN_USAGE}` };
4536
5058
  }
4537
5059
  return { ok: true, options: opts };
4538
5060
  }
@@ -4574,7 +5096,7 @@ async function runLogin(opts, io = {
4574
5096
  io.err(`yaw-mcp login: ${message}
4575
5097
  `);
4576
5098
  }
4577
- return { exitCode: err instanceof TeamSyncAuthError ? 1 : 1 };
5099
+ return { exitCode: err instanceof TeamSyncAuthError ? 1 : 2 };
4578
5100
  }
4579
5101
  }
4580
5102
 
@@ -4590,7 +5112,7 @@ function parseLogoutArgs(argv) {
4590
5112
  const opts = {};
4591
5113
  for (const a of argv) {
4592
5114
  if (a === "--json") opts.json = true;
4593
- else if (a === "--help" || a === "-h") return { ok: false, error: LOGOUT_USAGE };
5115
+ else if (a === "--help" || a === "-h") return { ok: false, error: LOGOUT_USAGE, help: true };
4594
5116
  else return { ok: false, error: `yaw-mcp logout: unknown argument "${a}"
4595
5117
 
4596
5118
  ${LOGOUT_USAGE}` };
@@ -4621,7 +5143,7 @@ async function runLogout(opts = {}, io = {
4621
5143
 
4622
5144
  // src/reset-learning-cmd.ts
4623
5145
  import { unlink as unlink2 } from "fs/promises";
4624
- import { homedir as homedir10 } from "os";
5146
+ import { homedir as homedir12 } from "os";
4625
5147
  import { join as join9 } from "path";
4626
5148
  var RESET_LEARNING_USAGE = `Usage: yaw-mcp reset-learning
4627
5149
 
@@ -4644,7 +5166,7 @@ ${RESET_LEARNING_USAGE}`
4644
5166
  return { kind: "ok", options: {} };
4645
5167
  }
4646
5168
  async function runResetLearning(opts = {}) {
4647
- const home = opts.home ?? homedir10();
5169
+ const home = opts.home ?? homedir12();
4648
5170
  const env = opts.env ?? process.env;
4649
5171
  const write = opts.out ?? ((s) => process.stdout.write(s));
4650
5172
  const writeErr = opts.err ?? ((s) => process.stderr.write(s));
@@ -4693,15 +5215,13 @@ function isFileNotFound2(err) {
4693
5215
 
4694
5216
  // src/secrets-cmd.ts
4695
5217
  import { existsSync as existsSync6 } from "fs";
4696
- import { mkdir as mkdir3 } from "fs/promises";
4697
- import { homedir as homedir12 } from "os";
4698
- import { dirname as dirname3 } from "path";
5218
+ import { homedir as homedir14 } from "os";
4699
5219
 
4700
5220
  // src/secrets-vault.ts
4701
5221
  import { existsSync as existsSync5 } from "fs";
4702
5222
  import { chmod as chmod3, readFile as readFile8 } from "fs/promises";
4703
- import { homedir as homedir11 } from "os";
4704
- import { dirname as dirname2, join as join10 } from "path";
5223
+ import { homedir as homedir13 } from "os";
5224
+ import { join as join10 } from "path";
4705
5225
 
4706
5226
  // src/secrets-crypto.ts
4707
5227
  import { createCipheriv, createDecipheriv, randomBytes, scrypt as scryptCb } from "crypto";
@@ -4721,13 +5241,13 @@ async function deriveKey(passphrase, salt) {
4721
5241
  return scryptCallWithMaxmem(passphrase, salt, KEY_LEN);
4722
5242
  }
4723
5243
  async function scryptCallWithMaxmem(password, salt, keylen) {
4724
- return new Promise((resolve5, reject) => {
5244
+ return new Promise((resolve7, reject) => {
4725
5245
  scryptCb(
4726
5246
  password,
4727
5247
  salt,
4728
5248
  keylen,
4729
5249
  { N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P, maxmem: SCRYPT_MAXMEM },
4730
- (err, key) => err ? reject(err) : resolve5(key)
5250
+ (err, key) => err ? reject(err) : resolve7(key)
4731
5251
  );
4732
5252
  });
4733
5253
  }
@@ -4759,7 +5279,8 @@ function decryptEntry(entry, key) {
4759
5279
  // src/secrets-vault.ts
4760
5280
  var SECRETS_FILENAME = "secrets.json";
4761
5281
  var SECRETS_SCHEMA_VERSION = 1;
4762
- function vaultPath(home = homedir11()) {
5282
+ var VAULT_CHECK_PLAINTEXT = "yaw-mcp-vault-v1";
5283
+ function vaultPath(home = homedir13()) {
4763
5284
  return join10(home, CONFIG_DIRNAME, SECRETS_FILENAME);
4764
5285
  }
4765
5286
  function emptyVault() {
@@ -4769,39 +5290,50 @@ function emptyVault() {
4769
5290
  entries: {}
4770
5291
  };
4771
5292
  }
4772
- async function loadVault(path3) {
4773
- if (!existsSync5(path3)) return null;
5293
+ async function loadVault(path5) {
5294
+ if (!existsSync5(path5)) return null;
4774
5295
  let raw;
4775
5296
  try {
4776
- raw = await readFile8(path3, "utf8");
5297
+ raw = await readFile8(path5, "utf8");
4777
5298
  } catch (err) {
4778
- log("warn", "Failed to read vault", { path: path3, error: err instanceof Error ? err.message : String(err) });
5299
+ log("warn", "Failed to read vault", { path: path5, error: err instanceof Error ? err.message : String(err) });
4779
5300
  return null;
4780
5301
  }
4781
5302
  let parsed;
4782
5303
  try {
4783
5304
  parsed = JSON.parse(raw);
4784
5305
  } catch (err) {
4785
- log("warn", "Vault file is not valid JSON", { path: path3, error: err instanceof Error ? err.message : String(err) });
5306
+ log("warn", "Vault file is not valid JSON", { path: path5, error: err instanceof Error ? err.message : String(err) });
4786
5307
  return null;
4787
5308
  }
4788
5309
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
4789
5310
  const obj = parsed;
4790
5311
  if (typeof obj.salt !== "string" || !obj.entries || typeof obj.entries !== "object") return null;
5312
+ const entries = obj.entries;
5313
+ for (const [name, entry] of Object.entries(entries)) {
5314
+ if (!isEncryptedEntry(entry)) {
5315
+ throw new Error(`vault corrupt at entry ${name}`);
5316
+ }
5317
+ }
5318
+ const check = isEncryptedEntry(obj.check) ? obj.check : void 0;
4791
5319
  return {
4792
5320
  version: typeof obj.version === "number" ? obj.version : SECRETS_SCHEMA_VERSION,
4793
5321
  salt: obj.salt,
4794
- entries: obj.entries
5322
+ entries: obj.entries,
5323
+ ...check ? { check } : {}
4795
5324
  };
4796
5325
  }
4797
- async function saveVault(path3, vault) {
4798
- const tmpDir = dirname2(path3);
4799
- void tmpDir;
4800
- await atomicWriteFile(path3, `${JSON.stringify(vault, null, 2)}
5326
+ function isEncryptedEntry(v) {
5327
+ if (!v || typeof v !== "object") return false;
5328
+ const e = v;
5329
+ return typeof e.iv === "string" && typeof e.ciphertext === "string" && typeof e.authTag === "string";
5330
+ }
5331
+ async function saveVault(path5, vault) {
5332
+ await atomicWriteFile(path5, `${JSON.stringify(vault, null, 2)}
4801
5333
  `);
4802
5334
  if (process.platform !== "win32") {
4803
5335
  try {
4804
- await chmod3(path3, 384);
5336
+ await chmod3(path5, 384);
4805
5337
  } catch {
4806
5338
  }
4807
5339
  }
@@ -4817,22 +5349,39 @@ async function unlock(vault, passphrase) {
4817
5349
  if (cachedKey && cachedSalt === vault.salt) return cachedKey;
4818
5350
  const salt = Buffer.from(vault.salt, "base64");
4819
5351
  const key = await deriveKey(passphrase, salt);
5352
+ verifyKey(vault, key);
4820
5353
  cachedKey = key;
4821
5354
  cachedSalt = vault.salt;
4822
5355
  return key;
4823
5356
  }
5357
+ function verifyKey(vault, key) {
5358
+ const canary = vault.check ?? Object.values(vault.entries)[0];
5359
+ if (!canary) return;
5360
+ try {
5361
+ decryptEntry(canary, key);
5362
+ } catch {
5363
+ throw new Error("wrong passphrase for this vault (decryption failed)");
5364
+ }
5365
+ }
5366
+ function ensureCheck(vault, key) {
5367
+ if (vault.check) return vault;
5368
+ return { ...vault, check: encryptEntry(VAULT_CHECK_PLAINTEXT, key) };
5369
+ }
4824
5370
  function listKeys(vault) {
4825
5371
  return Object.keys(vault.entries).sort();
4826
5372
  }
4827
5373
  function setSecret(vault, key, name, value) {
4828
5374
  if (!name) throw new Error("secret name is required");
4829
- return {
4830
- ...vault,
4831
- entries: {
4832
- ...vault.entries,
4833
- [name]: encryptEntry(value, key)
4834
- }
4835
- };
5375
+ return ensureCheck(
5376
+ {
5377
+ ...vault,
5378
+ entries: {
5379
+ ...vault.entries,
5380
+ [name]: encryptEntry(value, key)
5381
+ }
5382
+ },
5383
+ key
5384
+ );
4836
5385
  }
4837
5386
  function removeSecret(vault, name) {
4838
5387
  if (!(name in vault.entries)) return vault;
@@ -4902,13 +5451,17 @@ Actions:
4902
5451
  Requires \`yaw-mcp login\` first.
4903
5452
  pull Download the vault from mcp_secrets and write
4904
5453
  it locally. Overwrites local vault. Requires
4905
- \`yaw-mcp login\` first.
5454
+ \`yaw-mcp login\` first. Refuses when the local
5455
+ vault has a different salt (different passphrase
5456
+ lineage) unless --force is passed.
4906
5457
 
4907
5458
  Flags:
4908
5459
  --json Machine-readable output (where applicable).
4909
5460
  --value <v> Inline secret value (set only). Beware shell
4910
5461
  history -- prefer the default stdin prompt.
4911
5462
  --stdin Read the secret from raw stdin (set only).
5463
+ --force (pull only) Overwrite even when the local vault
5464
+ salt differs from the remote. Back up first.
4912
5465
 
4913
5466
  Passphrase:
4914
5467
  Set YAW_MCP_VAULT_PASSPHRASE in the env, or you will be prompted on
@@ -4919,7 +5472,7 @@ function parseSecretsArgs(argv) {
4919
5472
  const opts = {};
4920
5473
  for (let i = 0; i < argv.length; i++) {
4921
5474
  const a = argv[i];
4922
- if (a === "--help" || a === "-h") return { ok: false, error: SECRETS_USAGE };
5475
+ if (a === "--help" || a === "-h") return { ok: false, error: SECRETS_USAGE, help: true };
4923
5476
  if (a === "--json") {
4924
5477
  opts.json = true;
4925
5478
  continue;
@@ -4928,9 +5481,15 @@ function parseSecretsArgs(argv) {
4928
5481
  opts.fromStdin = true;
4929
5482
  continue;
4930
5483
  }
5484
+ if (a === "--force") {
5485
+ opts.force = true;
5486
+ continue;
5487
+ }
4931
5488
  if (a === "--value") {
4932
5489
  const v = argv[++i];
4933
- if (v === void 0) return { ok: false, error: "yaw-mcp secrets: --value requires a value\n\n" + SECRETS_USAGE };
5490
+ if (v === void 0) return { ok: false, error: `yaw-mcp secrets: --value requires a value
5491
+
5492
+ ${SECRETS_USAGE}` };
4934
5493
  opts.value = v;
4935
5494
  continue;
4936
5495
  }
@@ -4956,7 +5515,9 @@ ${SECRETS_USAGE}` };
4956
5515
 
4957
5516
  ${SECRETS_USAGE}` };
4958
5517
  }
4959
- if (!opts.action) return { ok: false, error: "yaw-mcp secrets: missing action\n\n" + SECRETS_USAGE };
5518
+ if (!opts.action) return { ok: false, error: `yaw-mcp secrets: missing action
5519
+
5520
+ ${SECRETS_USAGE}` };
4960
5521
  if ((opts.action === "set" || opts.action === "get" || opts.action === "remove") && !opts.name) {
4961
5522
  return { ok: false, error: `yaw-mcp secrets ${opts.action}: <name> is required
4962
5523
 
@@ -4965,18 +5526,34 @@ ${SECRETS_USAGE}` };
4965
5526
  return { ok: true, options: opts };
4966
5527
  }
4967
5528
  async function resolvePassphrase(opts) {
4968
- if (opts.passphrase !== void 0) return opts.passphrase;
5529
+ if (opts.passphrase !== void 0) return opts.passphrase.length > 0 ? opts.passphrase : null;
4969
5530
  const fromEnv = process.env.YAW_MCP_VAULT_PASSPHRASE;
4970
- if (typeof fromEnv === "string" && fromEnv.length > 0) return fromEnv;
5531
+ if (typeof fromEnv === "string" && fromEnv.length > 0) {
5532
+ if (fromEnv.length < MIN_PASSPHRASE_WARN_LEN) {
5533
+ const stderr = opts.io?.stderr ?? process.stderr;
5534
+ stderr.write(
5535
+ `yaw-mcp secrets: warning -- YAW_MCP_VAULT_PASSPHRASE is shorter than ${MIN_PASSPHRASE_WARN_LEN} characters; consider a longer passphrase.
5536
+ `
5537
+ );
5538
+ }
5539
+ return fromEnv;
5540
+ }
4971
5541
  const stdin = opts.io?.stdin ?? process.stdin;
4972
5542
  const stdout = opts.io?.stdout ?? process.stdout;
4973
5543
  const isTTY = stdin.isTTY === true && stdout.isTTY === true;
4974
5544
  if (!isTTY) return null;
4975
- return readPassphraseFromTTY(stdin, stdout);
5545
+ for (let attempt = 0; attempt < MAX_PASSPHRASE_PROMPTS; attempt++) {
5546
+ const entered = await readPassphraseFromTTY(stdin, stdout);
5547
+ if (entered.length > 0) return entered;
5548
+ stdout.write("Passphrase cannot be empty.\n");
5549
+ }
5550
+ return null;
4976
5551
  }
4977
- function readPassphraseFromTTY(stdin, stdout) {
4978
- stdout.write("Vault passphrase: ");
4979
- return new Promise((resolve5) => {
5552
+ var MAX_PASSPHRASE_PROMPTS = 3;
5553
+ var MIN_PASSPHRASE_WARN_LEN = 12;
5554
+ function readPassphraseFromTTY(stdin, stdout, prompt = "Vault passphrase: ") {
5555
+ stdout.write(prompt);
5556
+ return new Promise((resolve7) => {
4980
5557
  const chunks = [];
4981
5558
  const wasRaw = stdin.isRaw === true;
4982
5559
  try {
@@ -4995,7 +5572,7 @@ function readPassphraseFromTTY(stdin, stdout) {
4995
5572
  } catch {
4996
5573
  }
4997
5574
  stdin.pause();
4998
- resolve5(chunks.join(""));
5575
+ resolve7(chunks.join(""));
4999
5576
  return;
5000
5577
  }
5001
5578
  if (ch === "") {
@@ -5025,16 +5602,12 @@ async function readStdinValue(io) {
5025
5602
  for await (const chunk of stdin) chunks.push(chunk);
5026
5603
  return chunks.join("").replace(/\r?\n$/, "");
5027
5604
  }
5028
- async function ensureVaultDir(path3) {
5029
- const dir = dirname3(path3);
5030
- if (!existsSync6(dir)) await mkdir3(dir, { recursive: true });
5031
- }
5032
5605
  async function runSecrets(opts, io = {
5033
5606
  out: (s) => process.stdout.write(s),
5034
5607
  err: (s) => process.stderr.write(s)
5035
5608
  }) {
5036
- const home = opts.home ?? homedir12();
5037
- const path3 = vaultPath(home);
5609
+ const home = opts.home ?? homedir14();
5610
+ const path5 = vaultPath(home);
5038
5611
  if (opts.action === "lock") {
5039
5612
  lock();
5040
5613
  if (opts.json) io.out(`${JSON.stringify({ ok: true, locked: true })}
@@ -5049,24 +5622,24 @@ async function runSecrets(opts, io = {
5049
5622
  return await runSecretsPull(opts, io);
5050
5623
  }
5051
5624
  if (opts.action === "list") {
5052
- const vault2 = await loadVault(path3);
5625
+ const vault2 = await loadVault(path5);
5053
5626
  const keys = vault2 ? listKeys(vault2) : [];
5054
- if (opts.json) io.out(`${JSON.stringify({ ok: true, vault: existsSync6(path3), keys }, null, 2)}
5627
+ if (opts.json) io.out(`${JSON.stringify({ ok: true, vault: existsSync6(path5), keys }, null, 2)}
5055
5628
  `);
5056
- else if (!vault2) io.out(`No vault at ${path3}. Run \`yaw-mcp secrets set <name>\` to create one.
5629
+ else if (!vault2) io.out(`No vault at ${path5}. Run \`yaw-mcp secrets set <name>\` to create one.
5057
5630
  `);
5058
- else if (keys.length === 0) io.out(`Vault at ${path3} is empty.
5631
+ else if (keys.length === 0) io.out(`Vault at ${path5} is empty.
5059
5632
  `);
5060
5633
  else {
5061
- io.out(`Vault at ${path3}
5634
+ io.out(`Vault at ${path5}
5062
5635
  `);
5063
5636
  for (const k of keys) io.out(` ${k}
5064
5637
  `);
5065
5638
  }
5066
5639
  return { exitCode: 0 };
5067
5640
  }
5068
- let vault = await loadVault(path3) ?? newVault();
5069
- const isFresh = !existsSync6(path3);
5641
+ let vault = await loadVault(path5) ?? newVault();
5642
+ const isFresh = !existsSync6(path5);
5070
5643
  const passphrase = await resolvePassphrase(opts);
5071
5644
  if (passphrase === null) {
5072
5645
  const msg = "Passphrase required. Set YAW_MCP_VAULT_PASSPHRASE or run from a TTY so we can prompt.";
@@ -5101,8 +5674,7 @@ async function runSecrets(opts, io = {
5101
5674
  return { exitCode: 1 };
5102
5675
  }
5103
5676
  vault = setSecret(vault, key, name, value);
5104
- await ensureVaultDir(path3);
5105
- await saveVault(path3, vault);
5677
+ await saveVault(path5, vault);
5106
5678
  if (opts.json) io.out(`${JSON.stringify({ ok: true, name, fresh_vault: isFresh })}
5107
5679
  `);
5108
5680
  else io.out(`${isFresh ? "Created vault and " : ""}Stored secret "${name}".
@@ -5148,7 +5720,7 @@ async function runSecrets(opts, io = {
5148
5720
  return { exitCode: 1 };
5149
5721
  }
5150
5722
  vault = removeSecret(vault, name);
5151
- await saveVault(path3, vault);
5723
+ await saveVault(path5, vault);
5152
5724
  if (opts.json) io.out(`${JSON.stringify({ ok: true, removed: name })}
5153
5725
  `);
5154
5726
  else io.out(`Removed "${name}".
@@ -5161,8 +5733,8 @@ async function runSecrets(opts, io = {
5161
5733
  }
5162
5734
  var MCP_SECRETS_RESOURCE = "mcp_secrets";
5163
5735
  async function runSecretsPush(opts, io) {
5164
- const home = opts.home ?? homedir12();
5165
- const path3 = vaultPath(home);
5736
+ const home = opts.home ?? homedir14();
5737
+ const path5 = vaultPath(home);
5166
5738
  const session = await getSession({ home, baseUrl: opts.baseUrl });
5167
5739
  if (!session) {
5168
5740
  const msg = "Not signed in. Run `yaw-mcp login --key <license-key>` first.";
@@ -5172,9 +5744,9 @@ async function runSecretsPush(opts, io) {
5172
5744
  `);
5173
5745
  return { exitCode: 1 };
5174
5746
  }
5175
- const vault = await loadVault(path3);
5747
+ const vault = await loadVault(path5);
5176
5748
  if (!vault) {
5177
- const msg = `No local vault at ${path3} to push. Run \`yaw-mcp secrets set <name>\` first.`;
5749
+ const msg = `No local vault at ${path5} to push. Run \`yaw-mcp secrets set <name>\` first.`;
5178
5750
  if (opts.json) io.err(`${JSON.stringify({ ok: false, error: msg })}
5179
5751
  `);
5180
5752
  else io.err(`yaw-mcp secrets push: ${msg}
@@ -5224,8 +5796,8 @@ async function runSecretsPush(opts, io) {
5224
5796
  }
5225
5797
  }
5226
5798
  async function runSecretsPull(opts, io) {
5227
- const home = opts.home ?? homedir12();
5228
- const path3 = vaultPath(home);
5799
+ const home = opts.home ?? homedir14();
5800
+ const path5 = vaultPath(home);
5229
5801
  const session = await getSession({ home, baseUrl: opts.baseUrl });
5230
5802
  if (!session) {
5231
5803
  const msg = "Not signed in. Run `yaw-mcp login --key <license-key>` first.";
@@ -5237,7 +5809,9 @@ async function runSecretsPull(opts, io) {
5237
5809
  }
5238
5810
  try {
5239
5811
  const remote = await getResource(MCP_SECRETS_RESOURCE, { home, baseUrl: opts.baseUrl });
5240
- if (!remote.data || !remote.data.salt || !remote.data.entries) {
5812
+ const remoteEntries = remote.data?.entries;
5813
+ const remoteHasEntries = remoteEntries !== void 0 && remoteEntries !== null && typeof remoteEntries === "object" && Object.keys(remoteEntries).length > 0;
5814
+ if (!remote.data || !remote.data.salt || !remoteHasEntries) {
5241
5815
  const msg = "Remote mcp_secrets is empty. Push from this machine to seed it.";
5242
5816
  if (opts.json) io.out(`${JSON.stringify({ ok: true, empty: true })}
5243
5817
  `);
@@ -5245,18 +5819,29 @@ async function runSecretsPull(opts, io) {
5245
5819
  `);
5246
5820
  return { exitCode: 0 };
5247
5821
  }
5248
- await ensureVaultDir(path3);
5249
- await saveVault(path3, remote.data);
5822
+ const localVault = await loadVault(path5);
5823
+ const localHasEntries = localVault !== null && Object.keys(localVault.entries).length > 0;
5824
+ if (localHasEntries && localVault.salt !== remote.data.salt && !opts.force) {
5825
+ const msg = `Local vault at ${path5} has a different salt than the remote (different passphrase lineage). Back up ${path5} first, then re-run with --force to overwrite.`;
5826
+ if (opts.json) io.err(`${JSON.stringify({ ok: false, error: msg })}
5827
+ `);
5828
+ else io.err(`yaw-mcp secrets pull: ${msg}
5829
+ `);
5830
+ return { exitCode: 1 };
5831
+ }
5832
+ await saveVault(path5, remote.data);
5250
5833
  lock();
5251
5834
  const count = Object.keys(remote.data.entries).length;
5252
5835
  if (opts.json) {
5253
5836
  io.out(
5254
- `${JSON.stringify({ ok: true, secret_count: count, remote_version: remote.version, written: path3 }, null, 2)}
5837
+ `${JSON.stringify({ ok: true, secret_count: count, remote_version: remote.version, written: path5 }, null, 2)}
5255
5838
  `
5256
5839
  );
5257
5840
  } else {
5258
- io.out(`Pulled ${count} secret${count === 1 ? "" : "s"} (encrypted) -> ${path3}
5259
- `);
5841
+ io.out(
5842
+ `Local vault replaced with remote copy: ${count} secret${count === 1 ? "" : "s"} (encrypted) -> ${path5}
5843
+ `
5844
+ );
5260
5845
  io.out("Vault locked -- next secrets command will prompt for the passphrase.\n");
5261
5846
  }
5262
5847
  return { exitCode: 0 };
@@ -5280,8 +5865,8 @@ async function runSecretsPull(opts, io) {
5280
5865
 
5281
5866
  // src/server.ts
5282
5867
  import { readFile as readFile10 } from "fs/promises";
5283
- import { homedir as homedir13 } from "os";
5284
- import { isAbsolute, relative, resolve as resolve4 } from "path";
5868
+ import { homedir as homedir15 } from "os";
5869
+ import { isAbsolute as isAbsolute2, relative, resolve as resolve6 } from "path";
5285
5870
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
5286
5871
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5287
5872
  import {
@@ -5314,33 +5899,38 @@ async function fetchLatestVersion2() {
5314
5899
  }
5315
5900
  }
5316
5901
  function defaultSpawn2(cmd, args) {
5902
+ let errorFired = false;
5903
+ const correctiveCmd = cmd === "npm" ? "npm install -g @yawlabs/mcp@latest" : cmd === "pnpm" ? "pnpm add -g @yawlabs/mcp@latest" : "bun add -g @yawlabs/mcp@latest";
5317
5904
  const child = spawn3(cmd, args, {
5318
5905
  stdio: "ignore",
5319
5906
  // Stay a child of this process (not detached) so it dies with yaw-mcp
5320
- // if yaw-mcp exits mid-install -- a half-finished `npm i -g` is fine
5321
- // (npm is atomic per package) and a re-run next startup completes it.
5907
+ // if yaw-mcp exits mid-install -- a half-finished install is fine
5908
+ // (npm/pnpm/bun are atomic per package) and a re-run next startup completes it.
5322
5909
  detached: false,
5323
5910
  shell: process.platform === "win32"
5324
5911
  });
5325
5912
  child.on("close", (code) => {
5913
+ if (errorFired) return;
5326
5914
  if (code === 0) {
5327
5915
  log("info", "yaw-mcp self-upgrade complete; the next client restart will run the new version");
5328
5916
  } else {
5917
+ const hint = cmd === "npm" ? " (often EACCES on a sudo-installed global -- run with the right permissions)" : "";
5329
5918
  log(
5330
5919
  "warn",
5331
- "yaw-mcp self-upgrade: npm exited non-zero (often EACCES on a sudo-installed global). Run `npm install -g @yawlabs/mcp@latest` with the right permissions, or set YAW_MCP_AUTO_UPGRADE=0 to silence this.",
5920
+ `yaw-mcp self-upgrade: ${cmd} exited non-zero${hint}. Run \`${correctiveCmd}\` manually, or set YAW_MCP_AUTO_UPGRADE=0 to silence this.`,
5332
5921
  { code }
5333
5922
  );
5334
5923
  }
5335
5924
  });
5336
5925
  child.on("error", (err) => {
5337
- log("warn", "yaw-mcp self-upgrade: npm spawn failed", { error: err?.message });
5926
+ errorFired = true;
5927
+ log("warn", `yaw-mcp self-upgrade: ${cmd} spawn failed`, { error: err?.message });
5338
5928
  });
5339
5929
  }
5340
5930
  async function maybeAutoUpgrade(deps = {}) {
5341
5931
  const optOut = process.env.YAW_MCP_AUTO_UPGRADE;
5342
5932
  if (optOut === "0" || optOut?.toLowerCase() === "false") return;
5343
- const current = deps.currentVersion ?? (true ? "0.60.6" : "dev");
5933
+ const current = deps.currentVersion ?? (true ? "0.62.0" : "dev");
5344
5934
  if (current === "dev") return;
5345
5935
  const method = (deps.isSeaImpl ? await deps.isSeaImpl() : await detectSea()) ? "binary" : detectInstallMethod(deps.argvPath ?? process.argv[1]);
5346
5936
  const latest = await (deps.fetchLatestImpl ?? fetchLatestVersion2)();
@@ -5365,11 +5955,23 @@ async function maybeAutoUpgrade(deps = {}) {
5365
5955
  log("info", "yaw-mcp (standalone binary) is behind npm; download the latest build to update", { current, latest });
5366
5956
  return;
5367
5957
  }
5368
- log("info", "yaw-mcp is out of date; restart your MCP client to pick up the latest version", {
5369
- current,
5370
- latest,
5371
- method
5372
- });
5958
+ if (method === "npx") {
5959
+ log("info", "yaw-mcp is out of date; restart your MCP client to pick up the latest version", {
5960
+ current,
5961
+ latest,
5962
+ method
5963
+ });
5964
+ } else {
5965
+ log(
5966
+ "info",
5967
+ "yaw-mcp is out of date; run `yaw-mcp upgrade --run` to update this install (a restart won't refresh a stale global)",
5968
+ {
5969
+ current,
5970
+ latest,
5971
+ method
5972
+ }
5973
+ );
5974
+ }
5373
5975
  }
5374
5976
 
5375
5977
  // src/compliance.ts
@@ -5671,6 +6273,28 @@ function resolveArgs(args, bindings) {
5671
6273
  }
5672
6274
  return args;
5673
6275
  }
6276
+ function collectRefDeps(args) {
6277
+ const deps = /* @__PURE__ */ new Set();
6278
+ const walk = (node) => {
6279
+ if (isRefNode(node)) {
6280
+ const tokens = parseRefPath(node.$ref);
6281
+ if (tokens && tokens.length > 0 && typeof tokens[0] === "string") {
6282
+ deps.add(tokens[0]);
6283
+ }
6284
+ return;
6285
+ }
6286
+ if (Array.isArray(node)) {
6287
+ for (const v of node) walk(v);
6288
+ return;
6289
+ }
6290
+ if (node !== null && typeof node === "object") {
6291
+ for (const v of Object.values(node)) walk(v);
6292
+ return;
6293
+ }
6294
+ };
6295
+ walk(args);
6296
+ return Array.from(deps);
6297
+ }
5674
6298
  var MAX_EXEC_STEPS = 16;
5675
6299
  function validateExecRequest(req) {
5676
6300
  if (req === null || typeof req !== "object" || Array.isArray(req)) {
@@ -5687,6 +6311,7 @@ function validateExecRequest(req) {
5687
6311
  return { ok: false, message: `too many steps (${steps.length}); max is ${MAX_EXEC_STEPS}` };
5688
6312
  }
5689
6313
  const seenIds = /* @__PURE__ */ new Set();
6314
+ const positionalSlots = /* @__PURE__ */ new Set();
5690
6315
  for (let i = 0; i < steps.length; i++) {
5691
6316
  const step = steps[i];
5692
6317
  if (step === null || typeof step !== "object" || Array.isArray(step)) {
@@ -5704,16 +6329,33 @@ function validateExecRequest(req) {
5704
6329
  return { ok: false, message: `step ${i}: duplicate id "${s.id}"` };
5705
6330
  }
5706
6331
  seenIds.add(s.id);
6332
+ } else {
6333
+ positionalSlots.add(String(i));
5707
6334
  }
5708
6335
  if (s.args !== void 0 && (s.args === null || typeof s.args !== "object" || Array.isArray(s.args))) {
5709
6336
  return { ok: false, message: `step ${i}: \`args\` must be an object if provided` };
5710
6337
  }
5711
6338
  }
6339
+ for (const id of seenIds) {
6340
+ if (positionalSlots.has(id)) {
6341
+ return {
6342
+ ok: false,
6343
+ message: `step id "${id}" collides with the positional binding key of an unnamed step; rename it`
6344
+ };
6345
+ }
6346
+ }
5712
6347
  if (ret !== void 0) {
5713
6348
  if (typeof ret !== "string" || ret.length === 0) {
5714
6349
  return { ok: false, message: "`return` must be a non-empty step id string" };
5715
6350
  }
5716
- if (!seenIds.has(ret)) {
6351
+ const allBindingKeys = new Set(seenIds);
6352
+ for (let i = 0; i < steps.length; i++) {
6353
+ const s = steps[i];
6354
+ if (typeof s.id !== "string" || s.id.length === 0) {
6355
+ allBindingKeys.add(String(i));
6356
+ }
6357
+ }
6358
+ if (!allBindingKeys.has(ret)) {
5717
6359
  return { ok: false, message: `\`return\` references unknown step id "${ret}"` };
5718
6360
  }
5719
6361
  }
@@ -5726,18 +6368,18 @@ function stepBindingKey(step, index) {
5726
6368
  // src/guide.ts
5727
6369
  import { readFile as readFile9 } from "fs/promises";
5728
6370
  var GUIDE_READ_TIMEOUT_MS = 1e3;
5729
- async function readGuide(path3, scope) {
6371
+ async function readGuide(path5, scope) {
5730
6372
  let raw;
5731
6373
  try {
5732
6374
  raw = await Promise.race([
5733
- readFile9(path3, "utf8"),
6375
+ readFile9(path5, "utf8"),
5734
6376
  new Promise(
5735
6377
  (_, reject) => setTimeout(() => reject(new Error("guide read timeout")), GUIDE_READ_TIMEOUT_MS)
5736
6378
  )
5737
6379
  ]);
5738
6380
  } catch (err) {
5739
6381
  if (err instanceof Error && err.message === "guide read timeout") {
5740
- log("warn", "Guide read timed out", { path: path3 });
6382
+ log("warn", "Guide read timed out", { path: path5 });
5741
6383
  }
5742
6384
  return null;
5743
6385
  }
@@ -5745,7 +6387,7 @@ async function readGuide(path3, scope) {
5745
6387
  if (content.length === 0) {
5746
6388
  return null;
5747
6389
  }
5748
- return { scope, path: path3, content };
6390
+ return { scope, path: path5, content };
5749
6391
  }
5750
6392
  async function loadUserGuide(home) {
5751
6393
  const p = guidePath(userConfigDir(home));
@@ -5933,6 +6575,11 @@ var LEARNING_MIN_BOOST = 0.9;
5933
6575
  var PENALTY_RATE_THRESHOLD = 0.8;
5934
6576
  var SATURATION_AT = 10;
5935
6577
  var LearningStore = class {
6578
+ // Growth bound: the map keys on namespace, and recordDispatch is only
6579
+ // reached with validated/bounded namespaces from the dispatch path, so
6580
+ // cardinality is capped by the number of distinct configured namespaces.
6581
+ // No explicit cap is needed unless untrusted strings can reach
6582
+ // recordDispatch (they cannot today).
5936
6583
  usage = /* @__PURE__ */ new Map();
5937
6584
  recordDispatch(namespace) {
5938
6585
  const prev = this.usage.get(namespace);
@@ -5950,6 +6597,50 @@ var LearningStore = class {
5950
6597
  lastUsedAt: Date.now()
5951
6598
  });
5952
6599
  }
6600
+ // Graded outcome in [0,1] for a single dispatched proxy call. This is
6601
+ // the SOUND replacement for the binary recordDispatch + recordSuccess
6602
+ // pair on the proxy path: a 200/empty/error-shaped reply no longer banks
6603
+ // full credit (the reward is computed by reward.ts/computeOutcomeReward).
6604
+ // One call records BOTH the dispatch (denominator) and the graded credit
6605
+ // (numerator), so callers no longer pair recordDispatch with a
6606
+ // conditional recordSuccess. reward is clamped to [0,1].
6607
+ recordOutcome(namespace, reward) {
6608
+ const r = Number.isFinite(reward) ? Math.min(1, Math.max(0, reward)) : 0;
6609
+ const prev = this.usage.get(namespace);
6610
+ this.usage.set(namespace, {
6611
+ dispatched: (prev?.dispatched ?? 0) + 1,
6612
+ succeeded: (prev?.succeeded ?? 0) + r,
6613
+ lastUsedAt: Date.now()
6614
+ });
6615
+ }
6616
+ // Synthetic failed observation with NO success credit. Used by the
6617
+ // re-dispatch routing-miss signal (redispatch.ts): when the model
6618
+ // abandons server A and re-routes a similar intent to B, A's earlier
6619
+ // "clean" reply was useless in hindsight, so we depress A's success rate
6620
+ // by one failed observation. Denominator-only — same shape as a
6621
+ // recordDispatch that never sees a matching success.
6622
+ recordMiss(namespace) {
6623
+ const prev = this.usage.get(namespace);
6624
+ this.usage.set(namespace, {
6625
+ dispatched: (prev?.dispatched ?? 0) + 1,
6626
+ succeeded: prev?.succeeded ?? 0,
6627
+ lastUsedAt: Date.now()
6628
+ });
6629
+ }
6630
+ // Apply a DELTA to a namespace's success credit WITHOUT touching the
6631
+ // dispatched count. Used by the optional LLM reward grader (reward-grader.ts)
6632
+ // to revise a previously-recorded heuristic reward in the background: the
6633
+ // caller records the heuristic via recordOutcome immediately, then later
6634
+ // adjusts by (graded - heuristic) once the LLM verdict lands. Adding a delta
6635
+ // (rather than setting an absolute) stays correct under concurrent
6636
+ // recordOutcome calls on the same namespace. No-op for an unknown namespace
6637
+ // (nothing to revise); succeeded is clamped to [0, dispatched].
6638
+ adjustSucceeded(namespace, delta) {
6639
+ const u = this.usage.get(namespace);
6640
+ if (!u || !Number.isFinite(delta)) return;
6641
+ u.succeeded = Math.min(u.dispatched, Math.max(0, u.succeeded + delta));
6642
+ u.lastUsedAt = Date.now();
6643
+ }
5953
6644
  get(namespace) {
5954
6645
  return this.usage.get(namespace);
5955
6646
  }
@@ -5962,8 +6653,9 @@ var LearningStore = class {
5962
6653
  boostFactor(namespace) {
5963
6654
  const u = this.usage.get(namespace);
5964
6655
  if (!u) return 1;
5965
- if (u.dispatched >= LEARNING_MIN_OBSERVATIONS) {
5966
- const rate = u.succeeded / u.dispatched;
6656
+ const dispatched = Math.max(u.dispatched, u.succeeded);
6657
+ if (dispatched >= LEARNING_MIN_OBSERVATIONS) {
6658
+ const rate = u.succeeded / dispatched;
5967
6659
  if (rate < PENALTY_RATE_THRESHOLD) {
5968
6660
  const distance = Math.min(1, (PENALTY_RATE_THRESHOLD - rate) / PENALTY_RATE_THRESHOLD);
5969
6661
  return 1 - distance * (1 - LEARNING_MIN_BOOST);
@@ -6007,7 +6699,11 @@ var LearningStore = class {
6007
6699
  loadSnapshot(snapshot) {
6008
6700
  this.usage.clear();
6009
6701
  for (const [ns, usage] of Object.entries(snapshot)) {
6010
- this.usage.set(ns, { dispatched: usage.dispatched, succeeded: usage.succeeded, lastUsedAt: usage.lastUsedAt });
6702
+ const dispatched = Number.isFinite(usage.dispatched) ? Math.max(0, usage.dispatched) : 0;
6703
+ const succeededRaw = Number.isFinite(usage.succeeded) ? Math.max(0, usage.succeeded) : 0;
6704
+ const succeeded = Math.min(succeededRaw, dispatched);
6705
+ const lastUsedAt = Number.isFinite(usage.lastUsedAt) ? usage.lastUsedAt : 0;
6706
+ this.usage.set(ns, { dispatched, succeeded, lastUsedAt });
6011
6707
  }
6012
6708
  }
6013
6709
  };
@@ -6137,6 +6833,7 @@ var META_TOOLS = {
6137
6833
  },
6138
6834
  budget: {
6139
6835
  type: "number",
6836
+ default: 1,
6140
6837
  description: "How many top-ranked servers to load into the session. Defaults to 1. Cap is 10. Raise only when one task genuinely spans multiple servers."
6141
6838
  }
6142
6839
  },
@@ -6684,8 +7381,9 @@ async function routeResourceRead(uri, resourceRoutes, activeConnections, builtin
6684
7381
  const result = await connection.client.readResource({ uri: route.originalUri });
6685
7382
  return result;
6686
7383
  } catch (err) {
6687
- log("error", "Resource read failed", { uri, namespace: route.namespace, error: err.message });
6688
- return { contents: [{ uri, text: `Error: ${err.message}` }] };
7384
+ const message = err instanceof Error ? err.message : String(err);
7385
+ log("error", "Resource read failed", { uri, namespace: route.namespace, error: message });
7386
+ return { contents: [{ uri, text: `Error: ${message}` }] };
6689
7387
  }
6690
7388
  }
6691
7389
  async function routePromptGet(name, args, promptRoutes, activeConnections) {
@@ -6703,8 +7401,9 @@ async function routePromptGet(name, args, promptRoutes, activeConnections) {
6703
7401
  const result = await connection.client.getPrompt({ name: route.originalName, arguments: args });
6704
7402
  return result;
6705
7403
  } catch (err) {
6706
- log("error", "Prompt get failed", { name, namespace: route.namespace, error: err.message });
6707
- return { messages: [{ role: "user", content: { type: "text", text: `Error: ${err.message}` } }] };
7404
+ const message = err instanceof Error ? err.message : String(err);
7405
+ log("error", "Prompt get failed", { name, namespace: route.namespace, error: message });
7406
+ return { messages: [{ role: "user", content: { type: "text", text: `Error: ${message}` } }] };
6708
7407
  }
6709
7408
  }
6710
7409
  async function routeToolCall(toolName, args, toolRoutes, activeConnections) {
@@ -6791,12 +7490,14 @@ function pruneWhitespace(text) {
6791
7490
  function pruneJson(value) {
6792
7491
  if (value === null || value === void 0) return void 0;
6793
7492
  if (Array.isArray(value)) {
6794
- const cleaned = [];
6795
- for (const el of value) {
7493
+ if (value.length === 0) return void 0;
7494
+ const cleaned = value.map((el) => {
6796
7495
  const pv = pruneJson(el);
6797
- if (pv !== void 0) cleaned.push(pv);
6798
- }
6799
- return cleaned.length === 0 ? void 0 : cleaned;
7496
+ if (pv !== void 0) return pv;
7497
+ if (el !== null && typeof el === "object" && !Array.isArray(el)) return {};
7498
+ return null;
7499
+ });
7500
+ return cleaned;
6800
7501
  }
6801
7502
  if (typeof value === "object") {
6802
7503
  const out = {};
@@ -6849,122 +7550,87 @@ function formatToolNotFound(server, toolName, availableTools) {
6849
7550
  return `"${toolName}" not found on "${server.namespace}". Available tools: ${names}`;
6850
7551
  }
6851
7552
 
6852
- // src/relevance.ts
6853
- var K1 = 1.2;
6854
- var B = 0.75;
6855
- var FIELD_WEIGHTS = {
6856
- name: 3,
6857
- namespace: 2,
6858
- description: 1.5,
6859
- toolName: 2,
6860
- toolDescription: 1
6861
- };
6862
- var MIN_TOKEN_LEN = 3;
6863
- function tokenize(text) {
6864
- if (!text) return [];
6865
- return text.toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length >= MIN_TOKEN_LEN);
6866
- }
6867
- function buildDocFields(server) {
6868
- const toolNameTokens = [];
6869
- const toolDescriptionTokens = [];
6870
- for (const tool of server.tools) {
6871
- toolNameTokens.push(...tokenize(tool.name));
6872
- toolDescriptionTokens.push(...tokenize(tool.description));
6873
- }
6874
- return {
6875
- namespace: tokenize(server.namespace),
6876
- name: tokenize(server.name),
6877
- description: tokenize(server.description),
6878
- toolName: toolNameTokens,
6879
- toolDescription: toolDescriptionTokens
6880
- };
6881
- }
6882
- function termFreq(tokens, term) {
6883
- let count = 0;
6884
- for (const t of tokens) {
6885
- if (t === term) count++;
6886
- }
6887
- return count;
6888
- }
6889
- function bm25Score(queryTerms, fields, idf, avgFieldLen, idfValues) {
6890
- let score = 0;
6891
- const seen = /* @__PURE__ */ new Set();
6892
- for (const term of queryTerms) {
6893
- if (seen.has(term)) continue;
6894
- seen.add(term);
6895
- const termIdf = idfValues.get(term);
6896
- if (termIdf === void 0 || termIdf <= 0) continue;
6897
- for (const [fieldName, weight] of Object.entries(FIELD_WEIGHTS)) {
6898
- const fieldTokens = fields[fieldName];
6899
- if (fieldTokens.length === 0) continue;
6900
- const tf = termFreq(fieldTokens, term);
6901
- if (tf === 0) continue;
6902
- const avg = avgFieldLen[fieldName] || 1;
6903
- const normLen = 1 - B + B * (fieldTokens.length / avg);
6904
- const numerator = tf * (K1 + 1);
6905
- const denominator = tf + K1 * normLen;
6906
- score += weight * termIdf * (numerator / denominator);
7553
+ // src/redispatch.ts
7554
+ var RING_CAP = 8;
7555
+ var WINDOW_MS = 12e4;
7556
+ var JACCARD_THRESHOLD = 0.4;
7557
+ var MIN_SHARED_TOKENS = 3;
7558
+ function jaccard(a, b) {
7559
+ if (a.size === 0 || b.size === 0) return { score: 0, shared: 0 };
7560
+ let shared = 0;
7561
+ for (const t of a) {
7562
+ if (b.has(t)) shared++;
7563
+ }
7564
+ const union = a.size + b.size - shared;
7565
+ return { score: union === 0 ? 0 : shared / union, shared };
7566
+ }
7567
+ var RedispatchTracker = class {
7568
+ // Newest record is at the END of the array. Capped at RING_CAP by shifting
7569
+ // the oldest off the front.
7570
+ ring = [];
7571
+ // Record a new dispatch decision. intentTokens = tokenize(intent).
7572
+ push(namespace, intentTokens, now) {
7573
+ for (const rec of this.ring) {
7574
+ if (rec.namespace === namespace && !rec.consumed) rec.furtherUse = true;
7575
+ }
7576
+ this.ring.push({
7577
+ namespace,
7578
+ tokens: new Set(intentTokens),
7579
+ time: now,
7580
+ replied: false,
7581
+ cleanReply: false,
7582
+ furtherUse: false,
7583
+ consumed: false
7584
+ });
7585
+ while (this.ring.length > RING_CAP) {
7586
+ this.ring.shift();
6907
7587
  }
6908
7588
  }
6909
- void idf;
6910
- return score;
6911
- }
6912
- function rankServers(context, servers) {
6913
- const queryTerms = tokenize(context);
6914
- if (queryTerms.length === 0 || servers.length === 0) return [];
6915
- const docsWithFields = servers.map((s) => ({ server: s, fields: buildDocFields(s) }));
6916
- const N = docsWithFields.length;
6917
- const df = /* @__PURE__ */ new Map();
6918
- for (const { fields } of docsWithFields) {
6919
- const bag = /* @__PURE__ */ new Set([
6920
- ...fields.namespace,
6921
- ...fields.name,
6922
- ...fields.description,
6923
- ...fields.toolName,
6924
- ...fields.toolDescription
6925
- ]);
6926
- for (const term of bag) {
6927
- df.set(term, (df.get(term) ?? 0) + 1);
7589
+ // Called from the proxy path when the dispatched server replied. We only
7590
+ // care about the MOST-RECENT record for `namespace` (that's the dispatch
7591
+ // the reply belongs to).
7592
+ //
7593
+ // First clean reply for that record sets cleanReply=true. Any SUBSEQUENT
7594
+ // call for that same record sets furtherUse=true (the server kept getting
7595
+ // used -> NOT abandoned, so it can never be a loser).
7596
+ markReply(namespace, clean) {
7597
+ for (let i = this.ring.length - 1; i >= 0; i--) {
7598
+ const rec = this.ring[i];
7599
+ if (rec.namespace !== namespace) continue;
7600
+ if (!rec.replied) {
7601
+ rec.replied = true;
7602
+ if (clean) rec.cleanReply = true;
7603
+ } else {
7604
+ rec.furtherUse = true;
7605
+ }
7606
+ return;
6928
7607
  }
6929
7608
  }
6930
- const idfValues = /* @__PURE__ */ new Map();
6931
- for (const [term, d] of df) {
6932
- idfValues.set(term, Math.log((N - d + 0.5) / (d + 0.5) + 1));
6933
- }
6934
- const totalLen = {
6935
- namespace: 0,
6936
- name: 0,
6937
- description: 0,
6938
- toolName: 0,
6939
- toolDescription: 0
6940
- };
6941
- for (const { fields } of docsWithFields) {
6942
- totalLen.namespace += fields.namespace.length;
6943
- totalLen.name += fields.name.length;
6944
- totalLen.description += fields.description.length;
6945
- totalLen.toolName += fields.toolName.length;
6946
- totalLen.toolDescription += fields.toolDescription.length;
6947
- }
6948
- const avgFieldLen = {
6949
- namespace: totalLen.namespace / N,
6950
- name: totalLen.name / N,
6951
- description: totalLen.description / N,
6952
- toolName: totalLen.toolName / N,
6953
- toolDescription: totalLen.toolDescription / N
6954
- };
6955
- const results = [];
6956
- for (const { server, fields } of docsWithFields) {
6957
- const score = bm25Score(queryTerms, fields, /* @__PURE__ */ new Map(), avgFieldLen, idfValues);
6958
- if (score > 0) {
6959
- results.push({ namespace: server.namespace, score });
7609
+ // When a new dispatch (newNamespace) lands, look back over the recent ring
7610
+ // for an ABANDONED record (cleanReply && !furtherUse) on a DIFFERENT
7611
+ // namespace whose intent is SIMILAR to newTokens and within the time
7612
+ // window. If found, that earlier server was the wrong route -> return
7613
+ // { loser }. isExcluded(a, b) returns true when a->b is a known legitimate
7614
+ // multi-server chain (curated bundle / detected pack) and must NOT be
7615
+ // treated as a miss. Returns null when no miss.
7616
+ detectMiss(newNamespace, newTokens, now, isExcluded) {
7617
+ const newSet = new Set(newTokens);
7618
+ for (let i = this.ring.length - 1; i >= 0; i--) {
7619
+ const rec = this.ring[i];
7620
+ if (rec.consumed) continue;
7621
+ if (rec.namespace === newNamespace) continue;
7622
+ if (!rec.cleanReply || rec.furtherUse) continue;
7623
+ if (now - rec.time > WINDOW_MS) continue;
7624
+ if (isExcluded(rec.namespace, newNamespace)) continue;
7625
+ const { score, shared } = jaccard(rec.tokens, newSet);
7626
+ if (shared < MIN_SHARED_TOKENS) continue;
7627
+ if (score < JACCARD_THRESHOLD) continue;
7628
+ rec.consumed = true;
7629
+ return { loser: rec.namespace };
6960
7630
  }
7631
+ return null;
6961
7632
  }
6962
- results.sort((a, b) => {
6963
- if (b.score !== a.score) return b.score - a.score;
6964
- return a.namespace < b.namespace ? -1 : 1;
6965
- });
6966
- return results;
6967
- }
7633
+ };
6968
7634
 
6969
7635
  // src/rerank.ts
6970
7636
  import { request as request7 } from "undici";
@@ -7064,18 +7730,150 @@ async function callLegacyRerank(payload) {
7064
7730
  return null;
7065
7731
  }
7066
7732
  }
7067
- async function readTeamCookie() {
7068
- const teamSync = await import("./team-sync-5356FJP6.js");
7069
- const session = await teamSync.getSession();
7070
- if (!session) return null;
7071
- const { readFile: readFile12 } = await import("fs/promises");
7072
- try {
7073
- const raw = await readFile12(teamSync.sessionStatePath(), "utf8");
7074
- const parsed = JSON.parse(raw);
7075
- return typeof parsed.cookie === "string" && parsed.cookie ? parsed.cookie : null;
7076
- } catch {
7077
- return null;
7733
+ async function readTeamCookie() {
7734
+ const teamSync = await import("./team-sync-B4R6FLKR.js");
7735
+ return teamSync.getCachedCookie();
7736
+ }
7737
+
7738
+ // src/reward-grader.ts
7739
+ function isRewardGraderEnabled() {
7740
+ const raw = process.env.YAW_MCP_REWARD_GRADER;
7741
+ if (!raw) return false;
7742
+ const v = raw.trim().toLowerCase();
7743
+ return v === "1" || v === "true";
7744
+ }
7745
+ function isUncertainReward(heuristic) {
7746
+ return heuristic >= 0.2 && heuristic <= 0.3;
7747
+ }
7748
+ var GRADER_MAX_TOKENS = 8;
7749
+ var GRADER_TIMEOUT_MS = 4e3;
7750
+ var RESULT_SNIPPET_LEN = 600;
7751
+ function firstResultText(result) {
7752
+ const content = result.content;
7753
+ if (Array.isArray(content)) {
7754
+ for (const block of content) {
7755
+ if (typeof block.text === "string" && block.text.trim().length > 0) {
7756
+ const t = block.text.trim();
7757
+ return t.length > RESULT_SNIPPET_LEN ? `${t.slice(0, RESULT_SNIPPET_LEN)}...` : t;
7758
+ }
7759
+ }
7760
+ }
7761
+ return "(empty result)";
7762
+ }
7763
+ function buildGraderPrompt(ctx) {
7764
+ const lines = ["You are grading whether an MCP tool call accomplished its goal."];
7765
+ if (ctx.intent && ctx.intent.trim().length > 0) {
7766
+ lines.push("", `Goal: ${ctx.intent.trim()}`);
7767
+ }
7768
+ lines.push(
7769
+ "",
7770
+ `Tool called: ${ctx.toolName}`,
7771
+ `Result (truncated): ${ctx.resultText}`,
7772
+ "",
7773
+ "Did the tool call accomplish the goal / return a useful, on-task result?",
7774
+ "Reply with ONLY one word: YES, PARTIAL, or NO."
7775
+ );
7776
+ return lines.join("\n");
7777
+ }
7778
+ function parseGrade(text) {
7779
+ const m = /\b(yes|partial|no)\b/i.exec(text);
7780
+ if (!m) return null;
7781
+ switch (m[1].toLowerCase()) {
7782
+ case "yes":
7783
+ return 1;
7784
+ case "partial":
7785
+ return 0.5;
7786
+ default:
7787
+ return 0;
7788
+ }
7789
+ }
7790
+ async function gradeOutcomeViaSampling(server, ctx) {
7791
+ const caps = server.getClientCapabilities();
7792
+ if (!caps?.sampling) return null;
7793
+ const prompt = buildGraderPrompt(ctx);
7794
+ try {
7795
+ const result = await withTimeout(
7796
+ server.createMessage({
7797
+ messages: [{ role: "user", content: { type: "text", text: prompt } }],
7798
+ maxTokens: GRADER_MAX_TOKENS,
7799
+ includeContext: "none"
7800
+ }),
7801
+ GRADER_TIMEOUT_MS
7802
+ );
7803
+ if (!result || typeof result !== "object" || !("content" in result) || !result.content) return null;
7804
+ const text = extractText(result.content);
7805
+ if (!text) return null;
7806
+ return parseGrade(text);
7807
+ } catch (err) {
7808
+ log("warn", "Reward grader sampling failed", { error: err instanceof Error ? err.message : String(err) });
7809
+ return null;
7810
+ }
7811
+ }
7812
+ function withTimeout(p, ms) {
7813
+ return new Promise((resolve7) => {
7814
+ const timer = setTimeout(() => resolve7(null), ms);
7815
+ if (typeof timer === "object" && timer && "unref" in timer) timer.unref();
7816
+ p.then(
7817
+ (v) => {
7818
+ clearTimeout(timer);
7819
+ resolve7(v);
7820
+ },
7821
+ () => {
7822
+ clearTimeout(timer);
7823
+ resolve7(null);
7824
+ }
7825
+ );
7826
+ });
7827
+ }
7828
+ function extractText(content) {
7829
+ if (Array.isArray(content)) {
7830
+ return content.map((c) => c && typeof c === "object" && "type" in c && c.type === "text" && "text" in c ? String(c.text) : "").filter(Boolean).join("\n");
7831
+ }
7832
+ if (content && typeof content === "object" && "type" in content) {
7833
+ const block = content;
7834
+ if (block.type === "text" && typeof block.text === "string") return block.text;
7835
+ }
7836
+ return "";
7837
+ }
7838
+
7839
+ // src/reward.ts
7840
+ var ERROR_SHAPED_CATEGORIES = /* @__PURE__ */ new Set([
7841
+ "validation_error",
7842
+ "timeout",
7843
+ "unauthorized",
7844
+ "unknown_tool",
7845
+ "connection_lost",
7846
+ "rate_limited",
7847
+ "not_found"
7848
+ ]);
7849
+ function firstTextBlock(result) {
7850
+ const content = result.content;
7851
+ if (!content || content.length === 0) return void 0;
7852
+ for (const block of content) {
7853
+ if (typeof block.text === "string" && block.text.trim().length > 0) return block.text;
7854
+ }
7855
+ return void 0;
7856
+ }
7857
+ function isEmptyBody(result) {
7858
+ const content = result.content;
7859
+ if (!content || content.length === 0) return true;
7860
+ for (const block of content) {
7861
+ if (typeof block.text === "string" && block.text.trim().length > 0) {
7862
+ return false;
7863
+ }
7864
+ }
7865
+ return true;
7866
+ }
7867
+ function computeOutcomeReward(result) {
7868
+ if (result.isError === true) return 0;
7869
+ const text = firstTextBlock(result);
7870
+ if (text !== void 0) {
7871
+ if (ERROR_SHAPED_CATEGORIES.has(classifyError(text))) {
7872
+ return 0.2;
7873
+ }
7078
7874
  }
7875
+ if (isEmptyBody(result)) return 0.3;
7876
+ return 1;
7079
7877
  }
7080
7878
 
7081
7879
  // src/runtime-detect.ts
@@ -7089,55 +7887,58 @@ function initRuntimeDetect(url, tok) {
7089
7887
  apiUrl4 = url;
7090
7888
  token4 = tok;
7091
7889
  }
7890
+ var PYTHON_CANDIDATES = process.platform === "win32" ? [
7891
+ { bin: "py", args: ["-3", "--version"] },
7892
+ { bin: "python", args: ["--version"] },
7893
+ { bin: "python3", args: ["--version"] }
7894
+ ] : [
7895
+ { bin: "python3", args: ["--version"] },
7896
+ { bin: "python", args: ["--version"] }
7897
+ ];
7092
7898
  var PROBES = {
7093
7899
  node: {
7094
- bin: "node",
7095
- args: ["--version"],
7900
+ candidates: [{ bin: "node", args: ["--version"] }],
7096
7901
  parse: (out) => out.trim().replace(/^v/, "") || true
7097
7902
  },
7098
7903
  npx: {
7099
- bin: "npx",
7100
- args: ["--version"],
7904
+ candidates: [{ bin: "npx", args: ["--version"] }],
7101
7905
  parse: (out) => out.trim() || true
7102
7906
  },
7103
7907
  python: {
7104
- bin: process.platform === "win32" ? "python" : "python3",
7105
- args: ["--version"],
7908
+ candidates: PYTHON_CANDIDATES,
7106
7909
  parse: (out) => {
7107
7910
  const m = out.match(/Python\s+(\d+\.\d+\.\d+)/);
7108
7911
  return m ? m[1] : true;
7109
7912
  }
7110
7913
  },
7111
7914
  uvx: {
7112
- bin: "uvx",
7113
- args: ["--version"],
7915
+ candidates: [{ bin: "uvx", args: ["--version"] }],
7114
7916
  parse: (out) => {
7115
7917
  const m = out.match(/(\d+\.\d+\.\d+)/);
7116
7918
  return m ? m[1] : true;
7117
7919
  }
7118
7920
  },
7119
7921
  docker: {
7120
- bin: "docker",
7121
- args: ["--version"],
7922
+ candidates: [{ bin: "docker", args: ["--version"] }],
7122
7923
  parse: (out) => {
7123
7924
  const m = out.match(/Docker version (\d+\.\d+\.\d+)/);
7124
7925
  return m ? m[1] : true;
7125
7926
  }
7126
7927
  }
7127
7928
  };
7128
- async function probe(name, p) {
7129
- return new Promise((resolve5) => {
7929
+ async function probeCandidate(c, parse) {
7930
+ return new Promise((resolve7) => {
7130
7931
  let settled = false;
7131
7932
  const settle = (v) => {
7132
7933
  if (settled) return;
7133
7934
  settled = true;
7134
- resolve5(v);
7935
+ resolve7(v);
7135
7936
  };
7136
7937
  let stdout = "";
7137
7938
  let stderr = "";
7138
7939
  let child;
7139
7940
  try {
7140
- child = spawn4(p.bin, p.args, {
7941
+ child = spawn4(c.bin, c.args, {
7141
7942
  stdio: ["ignore", "pipe", "pipe"],
7142
7943
  // Windows needs a shell for PATH lookup of .cmd/.bat shims —
7143
7944
  // node/npx/uvx arrive as `npx.cmd` in PATH, and native spawn
@@ -7180,16 +7981,22 @@ async function probe(name, p) {
7180
7981
  return;
7181
7982
  }
7182
7983
  const text = stdout || stderr;
7183
- if (p.parse) {
7184
- const parsed = p.parse(text);
7984
+ if (parse) {
7985
+ const parsed = parse(text);
7185
7986
  settle(parsed);
7186
7987
  } else {
7187
7988
  settle(true);
7188
7989
  }
7189
7990
  });
7190
- void name;
7191
7991
  });
7192
7992
  }
7993
+ async function probe(_name, p) {
7994
+ for (const c of p.candidates) {
7995
+ const result = await probeCandidate(c, p.parse);
7996
+ if (result !== false) return result;
7997
+ }
7998
+ return false;
7999
+ }
7193
8000
  async function detectRuntimes() {
7194
8001
  const entries = await Promise.all(
7195
8002
  Object.entries(PROBES).map(async ([name, p]) => [name, await probe(name, p)])
@@ -7233,6 +8040,9 @@ async function reportRuntimes() {
7233
8040
  // src/sampling-rank.ts
7234
8041
  var SAMPLING_TIEBREAK_RATIO = 0.9;
7235
8042
  var SAMPLING_MAX_TOKENS = 120;
8043
+ var MAX_SAMPLES = 5;
8044
+ var SAMPLING_TIMEOUT_MS = 2e3;
8045
+ var AGGRESSIVE_AMBIGUITY_THRESHOLD = 0.6;
7236
8046
  function shouldTiebreak(ranked, ratio = SAMPLING_TIEBREAK_RATIO) {
7237
8047
  if (ranked.length < 2) return false;
7238
8048
  const [top, second] = ranked;
@@ -7242,7 +8052,7 @@ function shouldTiebreak(ranked, ratio = SAMPLING_TIEBREAK_RATIO) {
7242
8052
  function buildTiebreakPrompt(intent, candidates) {
7243
8053
  const blocks = candidates.map((c, i) => {
7244
8054
  const toolLine = c.tools.length > 0 ? c.tools.slice(0, 8).map((t) => t.name).join(", ") : "(no tool metadata yet)";
7245
- return `${i + 1}. ${c.namespace}${c.description ? ` \u2014 ${c.description}` : ""}
8055
+ return `${i + 1}. ${c.namespace}${c.description ? ` -- ${c.description}` : ""}
7246
8056
  tools: ${toolLine}`;
7247
8057
  });
7248
8058
  return [
@@ -7279,27 +8089,7 @@ function parseTiebreakResponse(response, candidates) {
7279
8089
  function escapeRegex(s) {
7280
8090
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7281
8091
  }
7282
- async function tiebreakViaSampling(server, intent, candidates) {
7283
- const caps = server.getClientCapabilities();
7284
- if (!caps?.sampling) return null;
7285
- if (candidates.length < 2) return null;
7286
- const prompt = buildTiebreakPrompt(intent, candidates);
7287
- try {
7288
- const result = await server.createMessage({
7289
- messages: [{ role: "user", content: { type: "text", text: prompt } }],
7290
- maxTokens: SAMPLING_MAX_TOKENS,
7291
- // Hint that we want a cheap, fast response.
7292
- includeContext: "none"
7293
- });
7294
- const text = result && typeof result === "object" && "content" in result && result.content ? extractText(result.content) : "";
7295
- if (!text) return null;
7296
- return parseTiebreakResponse(text, candidates);
7297
- } catch (err) {
7298
- log("warn", "Sampling tiebreak failed", { error: err instanceof Error ? err.message : String(err) });
7299
- return null;
7300
- }
7301
- }
7302
- function extractText(content) {
8092
+ function extractText2(content) {
7303
8093
  if (!content) return "";
7304
8094
  if (Array.isArray(content)) {
7305
8095
  return content.map((c) => c && typeof c === "object" && "type" in c && c.type === "text" && "text" in c ? String(c.text) : "").filter(Boolean).join("\n");
@@ -7325,6 +8115,109 @@ function buildCandidates(topRanked, serversByNamespace, toolsByNamespace) {
7325
8115
  }
7326
8116
  return out;
7327
8117
  }
8118
+ function parseRouteEffort(raw) {
8119
+ if (raw === void 0) return "auto";
8120
+ switch (raw.trim().toLowerCase()) {
8121
+ case "off":
8122
+ return "off";
8123
+ case "aggressive":
8124
+ return "aggressive";
8125
+ default:
8126
+ return "auto";
8127
+ }
8128
+ }
8129
+ function computeAmbiguity(ranked, k = 3) {
8130
+ if (ranked.length < 2) return 0;
8131
+ const top = ranked[0];
8132
+ if (!top || top.score <= 0) return 0;
8133
+ const topK = ranked.slice(0, Math.max(2, k));
8134
+ const second = topK[1];
8135
+ const secondScore = second ? second.score : 0;
8136
+ const inverseMargin = Math.min(1, Math.max(0, secondScore / top.score));
8137
+ const weights = topK.map((c) => Math.max(0, c.score));
8138
+ const total = weights.reduce((a, b) => a + b, 0);
8139
+ let entropy = 0;
8140
+ if (total > 0 && topK.length >= 2) {
8141
+ let h = 0;
8142
+ for (const w of weights) {
8143
+ if (w <= 0) continue;
8144
+ const p = w / total;
8145
+ h -= p * Math.log(p);
8146
+ }
8147
+ const maxH = Math.log(topK.length);
8148
+ entropy = maxH > 0 ? h / maxH : 0;
8149
+ }
8150
+ return Math.max(inverseMargin, entropy);
8151
+ }
8152
+ function shouldSample(ranked, effort) {
8153
+ if (effort === "off") return false;
8154
+ if (effort === "auto") return shouldTiebreak(ranked);
8155
+ return computeAmbiguity(ranked) >= AGGRESSIVE_AMBIGUITY_THRESHOLD;
8156
+ }
8157
+ function sampleCountForEffort(effort) {
8158
+ switch (effort) {
8159
+ case "off":
8160
+ return 0;
8161
+ case "aggressive":
8162
+ return 3;
8163
+ default:
8164
+ return 1;
8165
+ }
8166
+ }
8167
+ async function bestOfNViaSampling(server, intent, candidates, n) {
8168
+ const caps = server.getClientCapabilities();
8169
+ if (!caps?.sampling) return null;
8170
+ if (candidates.length < 2) return null;
8171
+ const samples = Math.min(MAX_SAMPLES, Math.max(1, Math.floor(n)));
8172
+ const prompt = buildTiebreakPrompt(intent, candidates);
8173
+ const sampleOnce = async () => {
8174
+ try {
8175
+ const result = await server.createMessage({
8176
+ messages: [{ role: "user", content: { type: "text", text: prompt } }],
8177
+ maxTokens: SAMPLING_MAX_TOKENS,
8178
+ includeContext: "none"
8179
+ });
8180
+ const text = result && typeof result === "object" && "content" in result && result.content ? extractText2(result.content) : "";
8181
+ if (!text) return null;
8182
+ return parseTiebreakResponse(text, candidates);
8183
+ } catch (err) {
8184
+ log("warn", "Best-of-N sample failed", { error: err instanceof Error ? err.message : String(err) });
8185
+ return null;
8186
+ }
8187
+ };
8188
+ const aggregate2 = (async () => {
8189
+ const results = await Promise.all(Array.from({ length: samples }, () => sampleOnce()));
8190
+ const votes = /* @__PURE__ */ new Map();
8191
+ for (const ns of results) {
8192
+ if (!ns) continue;
8193
+ votes.set(ns, (votes.get(ns) ?? 0) + 1);
8194
+ }
8195
+ if (votes.size === 0) return null;
8196
+ const order = /* @__PURE__ */ new Map();
8197
+ candidates.forEach((c, i) => order.set(c.namespace, i));
8198
+ let winner = null;
8199
+ let bestVotes = -1;
8200
+ let bestRank = Number.POSITIVE_INFINITY;
8201
+ for (const [ns, count] of votes) {
8202
+ const rank = order.get(ns) ?? Number.POSITIVE_INFINITY;
8203
+ if (count > bestVotes || count === bestVotes && rank < bestRank) {
8204
+ winner = ns;
8205
+ bestVotes = count;
8206
+ bestRank = rank;
8207
+ }
8208
+ }
8209
+ return winner;
8210
+ })();
8211
+ let timer;
8212
+ const timeout = new Promise((resolve7) => {
8213
+ timer = setTimeout(() => resolve7(null), SAMPLING_TIMEOUT_MS);
8214
+ });
8215
+ try {
8216
+ return await Promise.race([aggregate2, timeout]);
8217
+ } finally {
8218
+ if (timer) clearTimeout(timer);
8219
+ }
8220
+ }
7328
8221
 
7329
8222
  // src/server-cap.ts
7330
8223
  var DEFAULT_SERVER_CAP = 6;
@@ -7366,7 +8259,7 @@ import { spawn as spawn5 } from "child_process";
7366
8259
  import { createHash as createHash3 } from "crypto";
7367
8260
  import { createWriteStream } from "fs";
7368
8261
  import fs from "fs/promises";
7369
- import path2 from "path";
8262
+ import path4 from "path";
7370
8263
  import { pipeline } from "stream/promises";
7371
8264
  import { request as request9 } from "undici";
7372
8265
  var UV_VERSION = "0.11.7";
@@ -7406,18 +8299,21 @@ async function exists2(p) {
7406
8299
  }
7407
8300
  }
7408
8301
  async function onPath(cmd) {
7409
- return new Promise((resolve5) => {
8302
+ return new Promise((resolve7) => {
7410
8303
  let settled = false;
7411
8304
  const settle = (v) => {
7412
8305
  if (settled) return;
7413
8306
  settled = true;
7414
- resolve5(v);
8307
+ resolve7(v);
7415
8308
  };
7416
8309
  let child;
7417
8310
  try {
7418
8311
  child = spawn5(cmd, ["--version"], {
7419
8312
  stdio: "ignore",
7420
- shell: false,
8313
+ // Windows needs a shell for PATHEXT shim resolution (.cmd/.exe)
8314
+ // so `uv --version` finds uv.cmd without an ENOENT false-negative.
8315
+ // Mirrors the PROBES spawn options in runtime-detect.ts.
8316
+ shell: process.platform === "win32",
7421
8317
  windowsHide: process.platform === "win32"
7422
8318
  });
7423
8319
  } catch {
@@ -7442,10 +8338,24 @@ async function onPath(cmd) {
7442
8338
  });
7443
8339
  });
7444
8340
  }
8341
+ var UV_FETCH_TIMEOUT_MS = 3e4;
7445
8342
  async function fetchWithRedirects(url, maxHops = 5) {
7446
8343
  let current = url;
7447
8344
  for (let i = 0; i < maxHops; i++) {
7448
- const res = await request9(current, { method: "GET" });
8345
+ let res;
8346
+ try {
8347
+ res = await request9(current, {
8348
+ method: "GET",
8349
+ headersTimeout: UV_FETCH_TIMEOUT_MS,
8350
+ bodyTimeout: UV_FETCH_TIMEOUT_MS
8351
+ });
8352
+ } catch (e) {
8353
+ const code = e.code;
8354
+ if (code === "UND_ERR_HEADERS_TIMEOUT" || code === "UND_ERR_BODY_TIMEOUT") {
8355
+ throw new Error(`uv download timed out after ${UV_FETCH_TIMEOUT_MS}ms (${current})`);
8356
+ }
8357
+ throw e;
8358
+ }
7449
8359
  if (res.statusCode >= 300 && res.statusCode < 400) {
7450
8360
  const loc = res.headers.location;
7451
8361
  if (!loc) throw new Error(`Redirect without Location header from ${current}`);
@@ -7457,13 +8367,29 @@ async function fetchWithRedirects(url, maxHops = 5) {
7457
8367
  await res.body.dump();
7458
8368
  throw new Error(`GET ${current} failed: HTTP ${res.statusCode}`);
7459
8369
  }
7460
- return Buffer.from(await res.body.arrayBuffer());
8370
+ try {
8371
+ return Buffer.from(await res.body.arrayBuffer());
8372
+ } catch (e) {
8373
+ const code = e.code;
8374
+ if (code === "UND_ERR_HEADERS_TIMEOUT" || code === "UND_ERR_BODY_TIMEOUT") {
8375
+ throw new Error(`uv download timed out after ${UV_FETCH_TIMEOUT_MS}ms (${current})`);
8376
+ }
8377
+ throw e;
8378
+ }
7461
8379
  }
7462
8380
  throw new Error(`Too many redirects starting at ${url}`);
7463
8381
  }
7464
8382
  async function extractArchive(archivePath, destDir) {
7465
8383
  await fs.mkdir(destDir, { recursive: true });
7466
8384
  if (process.platform === "win32") {
8385
+ for (const [label, p] of [
8386
+ ["archivePath", archivePath],
8387
+ ["destDir", destDir]
8388
+ ]) {
8389
+ if (/['\r\n]/.test(p)) {
8390
+ throw new Error(`Refusing to extract uv archive: ${label} contains a quote or newline (${JSON.stringify(p)})`);
8391
+ }
8392
+ }
7467
8393
  await runCommand("powershell.exe", [
7468
8394
  "-NoProfile",
7469
8395
  "-NonInteractive",
@@ -7475,7 +8401,7 @@ async function extractArchive(archivePath, destDir) {
7475
8401
  }
7476
8402
  }
7477
8403
  function runCommand(cmd, args) {
7478
- return new Promise((resolve5, reject) => {
8404
+ return new Promise((resolve7, reject) => {
7479
8405
  const child = spawn5(cmd, args, {
7480
8406
  stdio: ["ignore", "pipe", "pipe"],
7481
8407
  shell: false,
@@ -7487,7 +8413,7 @@ function runCommand(cmd, args) {
7487
8413
  });
7488
8414
  child.on("error", reject);
7489
8415
  child.on("close", (code) => {
7490
- if (code === 0) resolve5();
8416
+ if (code === 0) resolve7();
7491
8417
  else reject(new Error(`${cmd} exited ${code}: ${stderr.trim()}`));
7492
8418
  });
7493
8419
  });
@@ -7495,7 +8421,7 @@ function runCommand(cmd, args) {
7495
8421
  async function findBinary(root, name) {
7496
8422
  const entries = await fs.readdir(root, { withFileTypes: true });
7497
8423
  for (const e of entries) {
7498
- const full = path2.join(root, e.name);
8424
+ const full = path4.join(root, e.name);
7499
8425
  if (e.isFile() && e.name === name) return full;
7500
8426
  if (e.isDirectory()) {
7501
8427
  const found = await findBinary(full, name);
@@ -7506,7 +8432,12 @@ async function findBinary(root, name) {
7506
8432
  }
7507
8433
  var pending = null;
7508
8434
  function ensureUv() {
7509
- pending ??= resolveUv();
8435
+ if (!pending) {
8436
+ pending = resolveUv().catch((err) => {
8437
+ pending = null;
8438
+ return Promise.reject(err);
8439
+ });
8440
+ }
7510
8441
  return pending;
7511
8442
  }
7512
8443
  async function resolveUv() {
@@ -7517,8 +8448,8 @@ async function resolveUv() {
7517
8448
  `No prebuilt uv binary for ${process.platform}/${process.arch}. Install uv manually: https://docs.astral.sh/uv/`
7518
8449
  );
7519
8450
  }
7520
- const installDir = path2.join(cacheDir(), "uv", UV_VERSION);
7521
- const finalBin = path2.join(installDir, binName());
8451
+ const installDir = path4.join(cacheDir(), "uv", UV_VERSION);
8452
+ const finalBin = path4.join(installDir, binName());
7522
8453
  if (await exists2(finalBin)) return finalBin;
7523
8454
  await fs.mkdir(installDir, { recursive: true });
7524
8455
  log("info", "Bootstrapping uv", { version: UV_VERSION, target, cache: installDir });
@@ -7531,20 +8462,24 @@ async function resolveUv() {
7531
8462
  if (!expected || expected.toLowerCase() !== actual.toLowerCase()) {
7532
8463
  throw new Error(`uv archive checksum mismatch (expected ${expected}, got ${actual})`);
7533
8464
  }
7534
- const archivePath = path2.join(installDir, archiveName);
8465
+ const archivePath = path4.join(installDir, archiveName);
7535
8466
  await pipeline(async function* () {
7536
8467
  yield archiveBuf;
7537
8468
  }, createWriteStream(archivePath));
7538
- const extractDir = path2.join(installDir, "extract");
7539
- await fs.rm(extractDir, { recursive: true, force: true });
7540
- await extractArchive(archivePath, extractDir);
7541
- const extracted = await findBinary(extractDir, binName());
7542
- if (!extracted) throw new Error(`uv binary not found inside ${archiveName}`);
7543
- await fs.rename(extracted, finalBin);
7544
- await fs.chmod(finalBin, 493).catch(() => {
7545
- });
7546
- await fs.rm(extractDir, { recursive: true, force: true });
7547
- await fs.rm(archivePath, { force: true });
8469
+ const extractDir = path4.join(installDir, `extract-${process.pid}-${Date.now()}`);
8470
+ try {
8471
+ await extractArchive(archivePath, extractDir);
8472
+ const extracted = await findBinary(extractDir, binName());
8473
+ if (!extracted) throw new Error(`uv binary not found inside ${archiveName}`);
8474
+ await fs.rename(extracted, finalBin);
8475
+ await fs.chmod(finalBin, 493).catch(() => {
8476
+ });
8477
+ } finally {
8478
+ await fs.rm(extractDir, { recursive: true, force: true }).catch(() => {
8479
+ });
8480
+ await fs.rm(archivePath, { force: true }).catch(() => {
8481
+ });
8482
+ }
7548
8483
  log("info", "uv bootstrap complete", { bin: finalBin });
7549
8484
  return finalBin;
7550
8485
  }
@@ -7622,7 +8557,7 @@ function categorizeSpawnError(err) {
7622
8557
  }
7623
8558
  async function connectToUpstream(config, onDisconnect, onListChanged) {
7624
8559
  const client = new Client(
7625
- { name: "yaw-mcp", version: true ? "0.60.6" : "dev" },
8560
+ { name: "yaw-mcp", version: true ? "0.62.0" : "dev" },
7626
8561
  { capabilities: {} }
7627
8562
  );
7628
8563
  let transport;
@@ -7631,7 +8566,11 @@ async function connectToUpstream(config, onDisconnect, onListChanged) {
7631
8566
  if (!config.command) {
7632
8567
  throw new Error("command is required for local servers");
7633
8568
  }
7634
- const { YAW_MCP_TOKEN: _excluded, ...parentEnv } = process.env;
8569
+ const {
8570
+ YAW_MCP_TOKEN: _excludedToken,
8571
+ YAW_MCP_VAULT_PASSPHRASE: _excludedVaultPassphrase,
8572
+ ...parentEnv
8573
+ } = process.env;
7635
8574
  const resolved = await resolveUvSpawn(config.command, config.args ?? []);
7636
8575
  const serverEnv = await resolveServerEnv(config.env ?? {});
7637
8576
  const stdioTransport = new StdioClientTransport({
@@ -7664,7 +8603,10 @@ async function connectToUpstream(config, onDisconnect, onListChanged) {
7664
8603
  }, CONNECT_TIMEOUT);
7665
8604
  });
7666
8605
  try {
7667
- await Promise.race([client.connect(transport), timeoutPromise]);
8606
+ const connectP = client.connect(transport);
8607
+ connectP.catch(() => {
8608
+ });
8609
+ await Promise.race([connectP, timeoutPromise]);
7668
8610
  clearTimeout(timer);
7669
8611
  } catch (err) {
7670
8612
  clearTimeout(timer);
@@ -7830,7 +8772,18 @@ async function fetchPromptsFromUpstream(client, namespace) {
7830
8772
  }
7831
8773
  }
7832
8774
  async function fetchToolsFromUpstream(client, namespace) {
7833
- const result = await client.listTools({}, { timeout: LIST_TIMEOUT });
8775
+ let result;
8776
+ try {
8777
+ result = await client.listTools({}, { timeout: LIST_TIMEOUT });
8778
+ } catch (err) {
8779
+ const message = err instanceof Error ? err.message : String(err);
8780
+ throw new ActivationError(
8781
+ `"${namespace}" returned an error on tools/list: ${message}`,
8782
+ "protocol_error",
8783
+ void 0,
8784
+ err
8785
+ );
8786
+ }
7834
8787
  const raw = result.tools ?? [];
7835
8788
  if (raw.length > MAX_TOOLS_PER_SERVER) {
7836
8789
  log("warn", "Upstream returned more tools than cap; truncating", {
@@ -7860,7 +8813,7 @@ function resolvePollIntervalMs() {
7860
8813
  }
7861
8814
  return seconds * 1e3;
7862
8815
  }
7863
- function isPersistenceDisabled() {
8816
+ function isPersistenceDisabled2() {
7864
8817
  const raw = process.env.YAW_MCP_DISABLE_PERSISTENCE;
7865
8818
  if (raw === void 0 || raw === "") return false;
7866
8819
  return raw === "1" || raw.toLowerCase() === "true";
@@ -7931,7 +8884,7 @@ var ConnectServer = class _ConnectServer {
7931
8884
  this.apiUrl = apiUrl5;
7932
8885
  this.token = token5;
7933
8886
  this.server = new Server(
7934
- { name: "yaw-mcp", version: true ? "0.60.6" : "dev" },
8887
+ { name: "yaw-mcp", version: true ? "0.62.0" : "dev" },
7935
8888
  {
7936
8889
  capabilities: {
7937
8890
  tools: { listChanged: true },
@@ -8006,6 +8959,17 @@ var ConnectServer = class _ConnectServer {
8006
8959
  // same promise as the first; the entry is cleared when the promise
8007
8960
  // settles (success or failure).
8008
8961
  activationInflight = /* @__PURE__ */ new Map();
8962
+ // Tracks namespaces whose current activationInflight was initiated by
8963
+ // prewarmDormantServers. An explicit mcp_connect_activate clears the
8964
+ // namespace from this set, which prevents prewarm from disconnecting a
8965
+ // connection the user just claimed. Without this, the prewarm race is:
8966
+ // 1. prewarm activateOne("foo") -> inflight P1
8967
+ // 2. user activateOne("foo") -> joins P1 (same promise)
8968
+ // 3. P1 resolves ok=true for both callers
8969
+ // 4. prewarm disconnects "foo" — user's next tool call fails
8970
+ // With this set: prewarm only disconnects when the namespace was NOT
8971
+ // claimed by an explicit activate while P1 was in flight.
8972
+ prewarmNamespaces = /* @__PURE__ */ new Set();
8009
8973
  // Usage learning — nudges dispatch toward namespaces that have been
8010
8974
  // genuinely useful. Counts persist across yaw-mcp restarts via state.json
8011
8975
  // (see persistence.ts). YAW_MCP_DISABLE_PERSISTENCE=1 makes it session
@@ -8016,6 +8980,16 @@ var ConnectServer = class _ConnectServer {
8016
8980
  // "packs". Observation-only; never activates anything. Meta-tool calls
8017
8981
  // are deliberately excluded because they aren't user workflow.
8018
8982
  packDetector = new PackDetector();
8983
+ // Session-scoped re-dispatch tracking — watches for the model abandoning
8984
+ // one server and re-routing a similar intent to another, which is
8985
+ // evidence the first route was wrong. Feeds a negative learning signal
8986
+ // (LearningStore.recordMiss). Not persisted: a re-dispatch window that
8987
+ // spans a restart is meaningless. See redispatch.ts.
8988
+ redispatch = new RedispatchTracker();
8989
+ // Last dispatch intent per namespace (session-scoped, not persisted). Lets
8990
+ // the optional reward grader (reward-grader.ts) judge a tool call against the
8991
+ // goal the server was routed for. Bounded by the number of namespaces.
8992
+ lastIntentByNamespace = /* @__PURE__ */ new Map();
8019
8993
  // Short-TTL dedup cache for discover output. Agents often call
8020
8994
  // discover twice in quick succession (e.g. once to list, again after
8021
8995
  // a failed activate) — the second call returns the same text if
@@ -8166,7 +9140,7 @@ var ConnectServer = class _ConnectServer {
8166
9140
  });
8167
9141
  }
8168
9142
  async start() {
8169
- if (isPersistenceDisabled()) {
9143
+ if (isPersistenceDisabled2()) {
8170
9144
  log("info", "Cross-session persistence disabled via YAW_MCP_DISABLE_PERSISTENCE");
8171
9145
  } else {
8172
9146
  const persisted = await loadState();
@@ -8203,6 +9177,15 @@ var ConnectServer = class _ConnectServer {
8203
9177
  });
8204
9178
  for (const w of result.warnings) log("warn", "bundles.json warning", { warning: w });
8205
9179
  this.config = result.config ?? { servers: [], configVersion: "" };
9180
+ const seenNs = /* @__PURE__ */ new Set();
9181
+ this.config.servers = this.config.servers.filter((s) => {
9182
+ if (seenNs.has(s.namespace)) {
9183
+ log("warn", "Duplicate namespace in bundles.json, skipping", { namespace: s.namespace });
9184
+ return false;
9185
+ }
9186
+ seenNs.add(s.namespace);
9187
+ return true;
9188
+ });
8206
9189
  this.configVersion = this.config.configVersion;
8207
9190
  log("info", "Local mode: loaded bundles", {
8208
9191
  path: result.path,
@@ -8314,8 +9297,21 @@ var ConnectServer = class _ConnectServer {
8314
9297
  await Promise.all(
8315
9298
  batch.map(async (server) => {
8316
9299
  try {
8317
- const result = await this.activateOne(server.namespace);
9300
+ const result = await this.activateOne(
9301
+ server.namespace,
9302
+ void 0,
9303
+ /* fromPrewarm */
9304
+ true
9305
+ );
8318
9306
  if (!result.ok) return;
9307
+ if (!this.prewarmNamespaces.has(server.namespace)) {
9308
+ log("info", "Pre-warm skipping disconnect \u2014 namespace claimed by explicit activate", {
9309
+ namespace: server.namespace
9310
+ });
9311
+ anyPopulated = true;
9312
+ return;
9313
+ }
9314
+ this.prewarmNamespaces.delete(server.namespace);
8319
9315
  const conn = this.connections.get(server.namespace);
8320
9316
  if (conn) {
8321
9317
  await disconnectFromUpstream(conn).catch(() => {
@@ -8361,7 +9357,7 @@ var ConnectServer = class _ConnectServer {
8361
9357
  }
8362
9358
  return result;
8363
9359
  }
8364
- async handleToolCall(name, args, extra) {
9360
+ async handleToolCall(name, args, extra, opts) {
8365
9361
  const progress = createProgressReporter(extra);
8366
9362
  if (name === META_TOOLS.discover.name) {
8367
9363
  recordConnectEvent({ namespace: null, toolName: null, action: "discover", latencyMs: null, success: true });
@@ -8370,8 +9366,9 @@ var ConnectServer = class _ConnectServer {
8370
9366
  if (name === META_TOOLS.dispatch.name) {
8371
9367
  const intent = typeof args.intent === "string" ? args.intent : "";
8372
9368
  const budget = typeof args.budget === "number" && Number.isFinite(args.budget) ? args.budget : 1;
9369
+ const routeEffort = typeof args.routeEffort === "string" ? args.routeEffort : void 0;
8373
9370
  recordConnectEvent({ namespace: null, toolName: null, action: "activate", latencyMs: null, success: true });
8374
- return this.attachGuideNudge(await this.handleDispatch(intent, budget, progress));
9371
+ return this.attachGuideNudge(await this.handleDispatch(intent, budget, progress, routeEffort));
8375
9372
  }
8376
9373
  if (name === META_TOOLS.activate.name) {
8377
9374
  const namespaces = resolveNamespaces(args);
@@ -8496,7 +9493,7 @@ var ConnectServer = class _ConnectServer {
8496
9493
  content: [
8497
9494
  {
8498
9495
  type: "text",
8499
- text: `Tool "${name}" is no longer available after loading "${activation.serverId ? activation.serverId : name}" \u2014 the upstream's tool set changed. Call mcp_connect_discover to see current tools.`
9496
+ text: `Tool "${name}" is no longer available after loading "${activation.serverId ? activation.serverId : name}" \u2014 the upstream's tool set changed. Call mcp_connect_discover to list the current tools for that namespace.`
8500
9497
  }
8501
9498
  ],
8502
9499
  isError: true
@@ -8504,9 +9501,10 @@ var ConnectServer = class _ConnectServer {
8504
9501
  }
8505
9502
  }
8506
9503
  if (route) {
8507
- const conn = this.connections.get(route.namespace);
9504
+ const ns = route.namespace;
9505
+ const conn = this.connections.get(ns);
8508
9506
  if (conn && conn.status === "error") {
8509
- const serverConfig = this.config?.servers.find((s) => s.namespace === route.namespace);
9507
+ const serverConfig = this.config?.servers.find((s) => s.namespace === ns);
8510
9508
  if (serverConfig) {
8511
9509
  let reconnected = false;
8512
9510
  let lastErr;
@@ -8521,17 +9519,30 @@ var ConnectServer = class _ConnectServer {
8521
9519
  this.onUpstreamDisconnect,
8522
9520
  this.onUpstreamListChanged
8523
9521
  );
8524
- this.connections.set(route.namespace, newConn);
9522
+ this.connections.set(ns, newConn);
8525
9523
  this.rebuildRoutes();
8526
9524
  await this.notifyAllListsChanged();
8527
- log("info", "Auto-reconnected to upstream", { namespace: route.namespace });
9525
+ log("info", "Auto-reconnected to upstream", { namespace: ns });
8528
9526
  reconnected = true;
9527
+ routes = this.toolRoutes;
9528
+ route = routes.get(name);
9529
+ if (!route || route.deferred) {
9530
+ return {
9531
+ content: [
9532
+ {
9533
+ type: "text",
9534
+ text: `Tool "${name}" is no longer available after reconnecting "${serverConfig.namespace}" \u2014 the upstream's tool set changed. Call mcp_connect_discover to list the current tools for that namespace.`
9535
+ }
9536
+ ],
9537
+ isError: true
9538
+ };
9539
+ }
8529
9540
  break;
8530
9541
  } catch (err) {
8531
9542
  lastErr = err;
8532
9543
  if (attempt < RECONNECT_ATTEMPTS - 1) {
8533
9544
  log("warn", "Auto-reconnect attempt failed, retrying", {
8534
- namespace: route.namespace,
9545
+ namespace: ns,
8535
9546
  error: err instanceof Error ? err.message : String(err)
8536
9547
  });
8537
9548
  }
@@ -8540,12 +9551,12 @@ var ConnectServer = class _ConnectServer {
8540
9551
  if (!reconnected) {
8541
9552
  conn.status = "error";
8542
9553
  const lastErrMsg = lastErr instanceof Error ? lastErr.message : String(lastErr);
8543
- log("error", "Auto-reconnect failed", { namespace: route.namespace, error: lastErrMsg });
9554
+ log("error", "Auto-reconnect failed", { namespace: ns, error: lastErrMsg });
8544
9555
  return {
8545
9556
  content: [
8546
9557
  {
8547
9558
  type: "text",
8548
- text: `Server "${route.namespace}" disconnected and auto-reconnect failed: ${lastErrMsg}. Use mcp_connect_activate with server "${route.namespace}" to reload it manually.`
9559
+ text: `Server "${ns}" disconnected and auto-reconnect failed: ${lastErrMsg}. Use mcp_connect_activate with server "${ns}" to reload it manually.`
8549
9560
  }
8550
9561
  ],
8551
9562
  isError: true
@@ -8621,9 +9632,19 @@ var ConnectServer = class _ConnectServer {
8621
9632
  } catch {
8622
9633
  }
8623
9634
  }
8624
- this.learning.recordDispatch(route.namespace);
8625
- if (!result.isError) this.learning.recordSuccess(route.namespace);
8626
- this.scheduleStateSave();
9635
+ if (!opts?.deferLearning) {
9636
+ const reward = computeOutcomeReward(result);
9637
+ this.learning.recordOutcome(route.namespace, reward);
9638
+ this.scheduleStateSave();
9639
+ if (isRewardGraderEnabled() && isUncertainReward(reward)) {
9640
+ void this.refineRewardInBackground(route.namespace, reward, {
9641
+ intent: this.lastIntentByNamespace.get(route.namespace),
9642
+ toolName: route.originalName,
9643
+ resultText: firstResultText(result)
9644
+ });
9645
+ }
9646
+ this.redispatch.markReply(route.namespace, !result.isError);
9647
+ }
8627
9648
  if (!result.isError) {
8628
9649
  this.packDetector.recordCall(route.namespace, route.originalName, Date.now());
8629
9650
  }
@@ -8952,15 +9973,28 @@ ${activeCount} loaded in this session, ${totalTools} tools in context${tokenSumm
8952
9973
  // child process; the second set() would win and the first would leak
8953
9974
  // until its transport noticed. See activationInflight.
8954
9975
  //
9976
+ // `fromPrewarm` marks the inflight as prewarm-initiated so that
9977
+ // prewarmDormantServers can safely disconnect when it is the sole
9978
+ // caller, but skip the disconnect when an explicit activate has also
9979
+ // joined the inflight promise. An explicit call (fromPrewarm=false)
9980
+ // removes the namespace from prewarmNamespaces so prewarm's teardown
9981
+ // code sees it as "claimed" and leaves the connection alive.
9982
+ //
8955
9983
  // Returns:
8956
9984
  // { ok: true, message } — already connected or newly connected
8957
9985
  // { ok: false, message, isChanged: false } — failed or not in config
8958
- activateOne(namespace, progress) {
9986
+ activateOne(namespace, progress, fromPrewarm = false) {
9987
+ if (!fromPrewarm) {
9988
+ this.prewarmNamespaces.delete(namespace);
9989
+ }
8959
9990
  const inflight = this.activationInflight.get(namespace);
8960
9991
  if (inflight) {
8961
9992
  progress?.(`"${namespace}" load already in flight \u2014 awaiting existing attempt`);
8962
9993
  return inflight;
8963
9994
  }
9995
+ if (fromPrewarm) {
9996
+ this.prewarmNamespaces.add(namespace);
9997
+ }
8964
9998
  const promise = this.runActivateOne(namespace, progress).finally(() => {
8965
9999
  if (this.activationInflight.get(namespace) === promise) {
8966
10000
  this.activationInflight.delete(namespace);
@@ -9010,7 +10044,12 @@ ${activeCount} loaded in this session, ${totalTools} tools in context${tokenSumm
9010
10044
  }
9011
10045
  const capDecision = evaluateServerCap(namespace, loadedSlots, this.serverCap);
9012
10046
  if (!capDecision.allow) {
9013
- return { ok: false, isChanged: false, message: capDecision.message ?? "Concurrent server cap reached." };
10047
+ return {
10048
+ ok: false,
10049
+ isChanged: false,
10050
+ capped: true,
10051
+ message: capDecision.message ?? "Concurrent server cap reached."
10052
+ };
9014
10053
  }
9015
10054
  const elicited = this.elicitedEnv.get(namespace);
9016
10055
  const effectiveConfig = elicited ? { ...serverConfig, env: { ...serverConfig.env, ...elicited } } : serverConfig;
@@ -9160,6 +10199,7 @@ ${activeCount} loaded in this session, ${totalTools} tools in context${tokenSumm
9160
10199
  const results = [];
9161
10200
  let anyChanged = false;
9162
10201
  let anyError = false;
10202
+ let anyCapped = false;
9163
10203
  const minCompliance = resolveMinCompliance();
9164
10204
  const total = namespaces.length;
9165
10205
  let i = 0;
@@ -9179,7 +10219,10 @@ ${activeCount} loaded in this session, ${totalTools} tools in context${tokenSumm
9179
10219
  const r = await this.activateOne(namespace, progress);
9180
10220
  results.push(r.message);
9181
10221
  if (r.isChanged) anyChanged = true;
9182
- if (!r.ok) anyError = true;
10222
+ if (!r.ok) {
10223
+ if (r.capped) anyCapped = true;
10224
+ else anyError = true;
10225
+ }
9183
10226
  }
9184
10227
  if (anyChanged) {
9185
10228
  this.rebuildRoutes();
@@ -9189,7 +10232,7 @@ ${activeCount} loaded in this session, ${totalTools} tools in context${tokenSumm
9189
10232
  }
9190
10233
  return {
9191
10234
  content: [{ type: "text", text: results.join("\n") }],
9192
- isError: anyError && !anyChanged ? true : void 0
10235
+ isError: anyError || anyCapped && !anyChanged ? true : void 0
9193
10236
  };
9194
10237
  }
9195
10238
  // Smart-routing meta-tool. The LLM describes the task in plain English
@@ -9197,7 +10240,33 @@ ${activeCount} loaded in this session, ${totalTools} tools in context${tokenSumm
9197
10240
  // with BM25 and activates the top N, then lets the LLM call the now-
9198
10241
  // exposed tools normally. Default budget is 1 because over-activating
9199
10242
  // pollutes the tool list in the LLM's context with noise.
9200
- async handleDispatch(intent, budget, progress) {
10243
+ // Is A -> B a designed multi-server flow rather than a routing miss? True
10244
+ // when both namespaces co-occur in a curated bundle or a detected usage
10245
+ // pack — those A-then-B sequences are intentional, so re-dispatch from A
10246
+ // to B must NOT penalize A. Used as detectMiss's exclusion predicate.
10247
+ // Background refinement of a just-recorded heuristic reward via the optional
10248
+ // LLM grader. Fire-and-forget: the tool result has already returned. If the
10249
+ // grader returns a verdict different from the heuristic, revise the credit by
10250
+ // the delta (recordOutcome already counted the dispatch). Never throws.
10251
+ async refineRewardInBackground(namespace, heuristic, ctx) {
10252
+ try {
10253
+ const graded = await gradeOutcomeViaSampling(this.server, ctx);
10254
+ if (graded === null || graded === heuristic) return;
10255
+ this.learning.adjustSucceeded(namespace, graded - heuristic);
10256
+ this.scheduleStateSave();
10257
+ } catch {
10258
+ }
10259
+ }
10260
+ isLegitChain(a, b) {
10261
+ for (const bundle of CURATED_BUNDLES) {
10262
+ if (bundle.namespaces.includes(a) && bundle.namespaces.includes(b)) return true;
10263
+ }
10264
+ for (const pack of this.packDetector.detectChains()) {
10265
+ if (pack.namespaces.includes(a) && pack.namespaces.includes(b)) return true;
10266
+ }
10267
+ return false;
10268
+ }
10269
+ async handleDispatch(intent, budget, progress, routeEffortOverride) {
9201
10270
  const trimmed = intent?.trim?.() ?? "";
9202
10271
  if (trimmed.length === 0) {
9203
10272
  return {
@@ -9241,11 +10310,13 @@ ${activeCount} loaded in this session, ${totalTools} tools in context${tokenSumm
9241
10310
  isError: true
9242
10311
  };
9243
10312
  }
9244
- if (budget === 1 && shouldTiebreak(ranked)) {
10313
+ const effort = parseRouteEffort(routeEffortOverride ?? process.env.YAW_MCP_ROUTE_EFFORT);
10314
+ if (budget === 1 && shouldSample(ranked, effort)) {
9245
10315
  progress?.("Top candidates close \u2014 asking LLM to pick\u2026");
9246
10316
  const serversByNamespace = new Map(activeServers.map((s) => [s.namespace, s]));
9247
10317
  const candidates = buildCandidates(ranked.slice(0, 3), serversByNamespace, this.toolCache);
9248
- const picked = await tiebreakViaSampling(this.server, trimmed, candidates);
10318
+ const samples = sampleCountForEffort(effort);
10319
+ const picked = await bestOfNViaSampling(this.server, trimmed, candidates, samples);
9249
10320
  if (picked) {
9250
10321
  const winner = ranked.find((r) => r.namespace === picked);
9251
10322
  if (winner) {
@@ -9258,9 +10329,31 @@ ${activeCount} loaded in this session, ${totalTools} tools in context${tokenSumm
9258
10329
  }
9259
10330
  const safeBudget = Math.max(1, Math.min(10, Math.floor(budget)));
9260
10331
  const winners = ranked.slice(0, safeBudget);
10332
+ const primary = winners[0]?.namespace;
10333
+ if (primary) {
10334
+ for (const w of winners) this.lastIntentByNamespace.set(w.namespace, trimmed);
10335
+ const intentTokens = tokenize(trimmed);
10336
+ const now = Date.now();
10337
+ const miss = this.redispatch.detectMiss(primary, intentTokens, now, (a, b) => this.isLegitChain(a, b));
10338
+ if (miss) {
10339
+ this.learning.recordMiss(miss.loser);
10340
+ this.scheduleStateSave();
10341
+ }
10342
+ this.redispatch.push(primary, intentTokens, now);
10343
+ if (isFoundryEnabled()) {
10344
+ const redacted = redactIntent(trimmed);
10345
+ void appendFoundryTrace({
10346
+ tokens: redacted.tokens,
10347
+ redactedCount: redacted.redactedCount,
10348
+ candidates: ranked.slice(0, 5).map((r) => ({ ns: r.namespace, score: r.score })),
10349
+ chosen: primary
10350
+ });
10351
+ }
10352
+ }
9261
10353
  const results = [];
9262
10354
  let anyChanged = false;
9263
10355
  let anyError = false;
10356
+ let anyCapped = false;
9264
10357
  let i = 0;
9265
10358
  for (const winner of winners) {
9266
10359
  i += 1;
@@ -9268,7 +10361,10 @@ ${activeCount} loaded in this session, ${totalTools} tools in context${tokenSumm
9268
10361
  const r = await this.activateOne(winner.namespace, progress);
9269
10362
  results.push(`${winner.namespace} (score ${winner.score.toFixed(2)}): ${r.message}`);
9270
10363
  if (r.isChanged) anyChanged = true;
9271
- if (!r.ok) anyError = true;
10364
+ if (!r.ok) {
10365
+ if (r.capped) anyCapped = true;
10366
+ else anyError = true;
10367
+ }
9272
10368
  }
9273
10369
  if (anyChanged) {
9274
10370
  this.rebuildRoutes();
@@ -9278,7 +10374,7 @@ ${activeCount} loaded in this session, ${totalTools} tools in context${tokenSumm
9278
10374
  `;
9279
10375
  return {
9280
10376
  content: [{ type: "text", text: header + results.join("\n") }],
9281
- isError: anyError && !anyChanged ? true : void 0
10377
+ isError: anyError || anyCapped && !anyChanged ? true : void 0
9282
10378
  };
9283
10379
  }
9284
10380
  async handleDeactivate(namespaces) {
@@ -9309,8 +10405,7 @@ ${activeCount} loaded in this session, ${totalTools} tools in context${tokenSumm
9309
10405
  await this.notifyAllListsChanged();
9310
10406
  }
9311
10407
  return {
9312
- content: [{ type: "text", text: results.join("\n") }],
9313
- isError: !anyChanged ? true : void 0
10408
+ content: [{ type: "text", text: results.join("\n") }]
9314
10409
  };
9315
10410
  }
9316
10411
  async trackUsageAndAutoDeactivate(calledNamespace) {
@@ -9400,7 +10495,7 @@ ${activeCount} loaded in this session, ${totalTools} tools in context${tokenSumm
9400
10495
  continue;
9401
10496
  }
9402
10497
  const oldConfig = connection.config;
9403
- if (oldConfig.command !== newServerConfig.command || !argsEqual(oldConfig.args, newServerConfig.args) || oldConfig.url !== newServerConfig.url || !envEqual(oldConfig.env, newServerConfig.env)) {
10498
+ if (oldConfig.command !== newServerConfig.command || !argsEqual(oldConfig.args, newServerConfig.args) || oldConfig.url !== newServerConfig.url || !envEqual(oldConfig.env, newServerConfig.env) || oldConfig.type !== newServerConfig.type || oldConfig.connectTimeoutMs !== newServerConfig.connectTimeoutMs) {
9404
10499
  log("info", "Server config changed, deactivating stale connection", { namespace });
9405
10500
  await disconnectFromUpstream(connection);
9406
10501
  this.connections.delete(namespace);
@@ -9462,7 +10557,7 @@ ${activeCount} loaded in this session, ${totalTools} tools in context${tokenSumm
9462
10557
  }
9463
10558
  const ALLOWED_FILENAMES = ["claude_desktop_config.json", "mcp.json", "settings.json", "mcp_config.json"];
9464
10559
  try {
9465
- const resolved = filepath.startsWith("~/") || filepath.startsWith("~\\") ? resolve4(homedir13(), filepath.slice(2)) : resolve4(filepath);
10560
+ const resolved = filepath.startsWith("~/") || filepath.startsWith("~\\") ? resolve6(homedir15(), filepath.slice(2)) : resolve6(filepath);
9466
10561
  const resolvedBasename = resolved.split(/[/\\]/).pop() || "";
9467
10562
  if (!ALLOWED_FILENAMES.includes(resolvedBasename)) {
9468
10563
  return {
@@ -9477,9 +10572,9 @@ ${activeCount} loaded in this session, ${totalTools} tools in context${tokenSumm
9477
10572
  }
9478
10573
  const isUnder = (base, p) => {
9479
10574
  const rel = relative(base, p);
9480
- return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
10575
+ return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
9481
10576
  };
9482
- if (!isUnder(homedir13(), resolved) && !isUnder(process.cwd(), resolved)) {
10577
+ if (!isUnder(homedir15(), resolved) && !isUnder(process.cwd(), resolved)) {
9483
10578
  return {
9484
10579
  content: [
9485
10580
  { type: "text", text: "Import path must be under your home directory or the current working directory." }
@@ -9566,7 +10661,12 @@ Use mcp_connect_discover to see imported servers.`
9566
10661
  ]
9567
10662
  };
9568
10663
  } catch (err) {
9569
- return { content: [{ type: "text", text: `Import error: ${err.message}` }], isError: true };
10664
+ const { code, text } = this.mapNetworkError(err, "Import");
10665
+ log("warn", "handleImport error", {
10666
+ error: err instanceof Error ? err.message : String(err),
10667
+ code
10668
+ });
10669
+ return { content: [{ type: "text", text }], isError: true };
9570
10670
  }
9571
10671
  }
9572
10672
  // Install a new MCP server on the user's Yaw MCP account. Validates
@@ -9657,15 +10757,7 @@ Use mcp_connect_discover to see imported servers.`
9657
10757
  ]
9658
10758
  };
9659
10759
  } catch (err) {
9660
- const code = typeof err === "object" && err !== null ? err.code || err.cause?.code : void 0;
9661
- let text;
9662
- if (code === "UND_ERR_HEADERS_TIMEOUT" || code === "UND_ERR_BODY_TIMEOUT" || code === "UND_ERR_CONNECT_TIMEOUT") {
9663
- text = "Install timed out talking to yaw.sh/mcp. Retry in a moment.";
9664
- } else if (code === "ECONNREFUSED" || code === "ENOTFOUND" || code === "EAI_AGAIN" || code === "UND_ERR_SOCKET") {
9665
- text = "Couldn't reach yaw.sh/mcp (network unreachable or DNS failure). Check your connection and retry.";
9666
- } else {
9667
- text = "Install failed unexpectedly. Check yaw-mcp logs on this machine for the underlying error.";
9668
- }
10760
+ const { code, text } = this.mapNetworkError(err, "Install");
9669
10761
  log("warn", "handleInstall error", {
9670
10762
  error: err instanceof Error ? err.message : String(err),
9671
10763
  code
@@ -9673,6 +10765,23 @@ Use mcp_connect_discover to see imported servers.`
9673
10765
  return { content: [{ type: "text", text }], isError: true };
9674
10766
  }
9675
10767
  }
10768
+ // Map a raw undici/network error to a user-facing string instead of
10769
+ // leaking `err.message` (e.g. "getaddrinfo ENOTFOUND yaw.sh") verbatim to
10770
+ // the model. Returns the extracted node error code (for ops logging) and a
10771
+ // clean message keyed by the failing operation verb ("Install"/"Import").
10772
+ // Callers keep the raw error in the log; the LLM/user only sees `text`.
10773
+ mapNetworkError(err, op) {
10774
+ const code = typeof err === "object" && err !== null ? err.code || err.cause?.code : void 0;
10775
+ let text;
10776
+ if (code === "UND_ERR_HEADERS_TIMEOUT" || code === "UND_ERR_BODY_TIMEOUT" || code === "UND_ERR_CONNECT_TIMEOUT") {
10777
+ text = `${op} timed out talking to yaw.sh/mcp. Retry in a moment.`;
10778
+ } else if (code === "ECONNREFUSED" || code === "ENOTFOUND" || code === "EAI_AGAIN" || code === "UND_ERR_SOCKET") {
10779
+ text = `Couldn't reach yaw.sh/mcp (network unreachable or DNS failure). Check your connection and retry.`;
10780
+ } else {
10781
+ text = `${op} failed unexpectedly. Check yaw-mcp logs on this machine for the underlying error.`;
10782
+ }
10783
+ return { code, text };
10784
+ }
9676
10785
  // Signature-on-demand: return one tool's full input schema without
9677
10786
  // persistently activating its server. When the server is already
9678
10787
  // loaded we read from the in-memory connection. When it isn't, we
@@ -9861,7 +10970,7 @@ To load the top pack in one step, call \`mcp_connect_activate\` with namespaces=
9861
10970
  `];
9862
10971
  for (const bundle of CURATED_BUNDLES) {
9863
10972
  lines2.push(` ${bundle.id} \u2014 ${bundle.description}`);
9864
- lines2.push(` namespaces: ${JSON.stringify(bundle.namespaces)}`);
10973
+ lines2.push(` servers: ${JSON.stringify(bundle.namespaces)}`);
9865
10974
  lines2.push(` activate: ${bundleActivateHint(bundle)}`);
9866
10975
  }
9867
10976
  lines2.push("");
@@ -9887,7 +10996,7 @@ To load the top pack in one step, call \`mcp_connect_activate\` with namespaces=
9887
10996
  lines.push("Bundles ready to activate now:");
9888
10997
  for (const bundle of ready) {
9889
10998
  lines.push(` ${bundle.id} \u2014 ${bundle.description}`);
9890
- lines.push(` namespaces: ${JSON.stringify(bundle.namespaces)}`);
10999
+ lines.push(` servers: ${JSON.stringify(bundle.namespaces)}`);
9891
11000
  lines.push(` activate: ${bundleActivateHint(bundle)}`);
9892
11001
  }
9893
11002
  }
@@ -9903,6 +11012,30 @@ To load the top pack in one step, call \`mcp_connect_activate\` with namespaces=
9903
11012
  }
9904
11013
  return { content: [{ type: "text", text: lines.join("\n") }] };
9905
11014
  }
11015
+ // Extract the semantic payload from a successful MCP tool result for use
11016
+ // as a step binding in exec pipelines. The MCP wire format wraps every
11017
+ // result in `{ content: [{type, text}, ...], isError? }`, but the exec
11018
+ // tool description promises `$ref` targets that behave like the tool's
11019
+ // actual output -- e.g. `a = gh_list_prs(); b = gh_get_pr(a[0].number)`.
11020
+ //
11021
+ // Rules, in order:
11022
+ // 1. Single text item whose text is valid JSON -> the parsed JSON value.
11023
+ // 2. Single text item (non-JSON) -> the raw text string.
11024
+ // 3. Everything else (multi-item, non-text, empty) -> the content array.
11025
+ //
11026
+ // This is intentionally simple and loss-free: callers can still reach
11027
+ // the full wire payload via the `partial` / `steps` objects if needed.
11028
+ static parseStepPayload(result) {
11029
+ const content = result.content;
11030
+ if (!Array.isArray(content) || content.length !== 1) return content ?? [];
11031
+ const item = content[0];
11032
+ if (item.type !== "text" || typeof item.text !== "string") return content;
11033
+ try {
11034
+ return JSON.parse(item.text);
11035
+ } catch {
11036
+ return item.text;
11037
+ }
11038
+ }
9906
11039
  // Declarative pipeline executor. Runs N tool calls in order, binding
9907
11040
  // each output under the step's id (or positional index), and lets
9908
11041
  // later steps splice those outputs into their args via
@@ -9934,6 +11067,7 @@ To load the top pack in one step, call \`mcp_connect_activate\` with namespaces=
9934
11067
  const explicitReturn = typeof args.return === "string" ? args.return : void 0;
9935
11068
  const bindings = {};
9936
11069
  const stepKeys = [];
11070
+ const stepNamespaces = /* @__PURE__ */ new Map();
9937
11071
  for (let i = 0; i < steps.length; i++) {
9938
11072
  const step = steps[i];
9939
11073
  const key = stepBindingKey(step, i);
@@ -10003,9 +11137,26 @@ To load the top pack in one step, call \`mcp_connect_activate\` with namespaces=
10003
11137
  isError: true
10004
11138
  };
10005
11139
  }
10006
- const stepResult = await this.handleToolCall(step.tool, resolvedArgs);
11140
+ const stepNs = this.toolRoutes.get(step.tool)?.namespace;
11141
+ if (stepNs) stepNamespaces.set(key, stepNs);
11142
+ const stepResult = await this.handleToolCall(step.tool, resolvedArgs, void 0, { deferLearning: true });
10007
11143
  if (stepResult.isError) {
10008
11144
  const errText = stepResult.content?.[0]?.text ?? "unknown error";
11145
+ const routingFault = errText.includes("no longer available") || errText.includes("no longer connected") || errText.includes("auto-reconnect failed") || errText.includes("Unknown tool:");
11146
+ if (stepNs && !routingFault) {
11147
+ const inputShaped = errText.includes("[code=-32602]") || classifyError(errText) === "validation_error";
11148
+ const deps = collectRefDeps(step.args);
11149
+ if (inputShaped && deps.length > 0) {
11150
+ this.learning.recordOutcome(stepNs, 0.5);
11151
+ for (const dep of deps) {
11152
+ const depNs = stepNamespaces.get(dep);
11153
+ if (depNs) this.learning.recordOutcome(depNs, 0.5);
11154
+ }
11155
+ } else {
11156
+ this.learning.recordOutcome(stepNs, 0);
11157
+ }
11158
+ this.scheduleStateSave();
11159
+ }
10009
11160
  return {
10010
11161
  content: [
10011
11162
  {
@@ -10025,7 +11176,11 @@ To load the top pack in one step, call \`mcp_connect_activate\` with namespaces=
10025
11176
  isError: true
10026
11177
  };
10027
11178
  }
10028
- bindings[key] = stepResult;
11179
+ if (stepNs) {
11180
+ this.learning.recordOutcome(stepNs, computeOutcomeReward(stepResult));
11181
+ this.scheduleStateSave();
11182
+ }
11183
+ bindings[key] = _ConnectServer.parseStepPayload(stepResult);
10029
11184
  }
10030
11185
  const returnKey = explicitReturn ?? stepKeys[stepKeys.length - 1];
10031
11186
  const finalResult = bindings[returnKey];
@@ -10097,7 +11252,7 @@ function parseServersArgs(argv) {
10097
11252
  if (a === "--json") {
10098
11253
  json = true;
10099
11254
  } else if (a === "--help" || a === "-h") {
10100
- return { ok: false, error: SERVERS_USAGE };
11255
+ return { ok: false, error: SERVERS_USAGE, help: true };
10101
11256
  } else if (a.startsWith("-")) {
10102
11257
  return { ok: false, error: `yaw-mcp servers: unknown argument "${a}"
10103
11258
 
@@ -10171,7 +11326,12 @@ async function runServersCommand(opts = {}) {
10171
11326
  })
10172
11327
  };
10173
11328
  if (opts.json) {
10174
- print(JSON.stringify(merged, null, 2));
11329
+ const payload = {
11330
+ ...merged,
11331
+ filter: opts.filter ?? null,
11332
+ filterMatched: opts.filter ? merged.servers.length > 0 : null
11333
+ };
11334
+ print(JSON.stringify(payload, null, 2));
10175
11335
  return { exitCode: 0, lines };
10176
11336
  }
10177
11337
  if (opts.filter && filtered.servers.length === 0) {
@@ -10221,8 +11381,165 @@ function truncateVersion(v) {
10221
11381
  return v.length > 8 ? v.slice(0, 8) : v;
10222
11382
  }
10223
11383
 
11384
+ // src/set-active-cmd.ts
11385
+ import { homedir as homedir16 } from "os";
11386
+
11387
+ // src/sync-state.ts
11388
+ import { existsSync as existsSync7 } from "fs";
11389
+ import { mkdir as mkdir4, readFile as readFile11 } from "fs/promises";
11390
+ import { dirname as dirname2, join as join11 } from "path";
11391
+ var SYNC_STATE_FILENAME = "sync-state.json";
11392
+ function syncStatePath(home) {
11393
+ return join11(home, CONFIG_DIRNAME, SYNC_STATE_FILENAME);
11394
+ }
11395
+ async function readSyncState(home) {
11396
+ const path5 = syncStatePath(home);
11397
+ if (!existsSync7(path5)) return {};
11398
+ try {
11399
+ const raw = await readFile11(path5, "utf8");
11400
+ const parsed = JSON.parse(raw);
11401
+ if (!parsed || typeof parsed !== "object") return {};
11402
+ return parsed;
11403
+ } catch {
11404
+ return {};
11405
+ }
11406
+ }
11407
+ async function writeSyncState(home, state) {
11408
+ const path5 = syncStatePath(home);
11409
+ await mkdir4(dirname2(path5), { recursive: true });
11410
+ await atomicWriteFile(path5, `${JSON.stringify(state, null, 2)}
11411
+ `);
11412
+ }
11413
+
11414
+ // src/set-active-cmd.ts
11415
+ var SET_ACTIVE_RESOURCE = "mcp_bundles";
11416
+ var SET_ACTIVE_USAGE = `Usage: yaw-mcp set-active <namespace> <on|off>
11417
+
11418
+ Enable or disable a server in your shared Yaw Team config. The change is
11419
+ authoritative -- it applies to every member's config on their next
11420
+ sync/connect.
11421
+
11422
+ <namespace> The server's namespace (see \`yaw-mcp sync status\` or the dashboard).
11423
+ on|off Whether the server should be active.
11424
+ --json Emit machine-readable JSON instead of prose.
11425
+
11426
+ Sign in first with \`yaw-mcp login --key <license-key>\`. To hide a server only
11427
+ on THIS machine, edit the \`blocked\` list in ~/.yaw-mcp/config.json instead.`;
11428
+ function parseState(s) {
11429
+ const v = s.toLowerCase();
11430
+ if (v === "on" || v === "true" || v === "enable" || v === "enabled") return true;
11431
+ if (v === "off" || v === "false" || v === "disable" || v === "disabled") return false;
11432
+ return null;
11433
+ }
11434
+ function parseSetActiveArgs(argv) {
11435
+ const opts = {};
11436
+ const positionals = [];
11437
+ for (const a of argv) {
11438
+ if (a === "--json") opts.json = true;
11439
+ else if (a === "--help" || a === "-h") return { ok: false, error: SET_ACTIVE_USAGE, help: true };
11440
+ else if (a.startsWith("-"))
11441
+ return { ok: false, error: `yaw-mcp set-active: unknown flag "${a}"
11442
+
11443
+ ${SET_ACTIVE_USAGE}` };
11444
+ else positionals.push(a);
11445
+ }
11446
+ if (positionals.length > 2)
11447
+ return { ok: false, error: `yaw-mcp set-active: too many arguments
11448
+
11449
+ ${SET_ACTIVE_USAGE}` };
11450
+ const [ns, state] = positionals;
11451
+ if (!ns || !state)
11452
+ return { ok: false, error: `yaw-mcp set-active: <namespace> and <on|off> are required
11453
+
11454
+ ${SET_ACTIVE_USAGE}` };
11455
+ if (!NAMESPACE_RE.test(ns))
11456
+ return { ok: false, error: `yaw-mcp set-active: invalid namespace "${ns}"
11457
+
11458
+ ${SET_ACTIVE_USAGE}` };
11459
+ const active = parseState(state);
11460
+ if (active === null)
11461
+ return { ok: false, error: `yaw-mcp set-active: state must be on|off (got "${state}")
11462
+
11463
+ ${SET_ACTIVE_USAGE}` };
11464
+ opts.namespace = ns;
11465
+ opts.active = active;
11466
+ return { ok: true, options: opts };
11467
+ }
11468
+ async function runSetActive(opts, io = { out: (s) => process.stdout.write(s), err: (s) => process.stderr.write(s) }, deps = { getResource, putResource, writeSyncState }) {
11469
+ const { namespace, active } = opts;
11470
+ if (!namespace || active === void 0) {
11471
+ io.err("yaw-mcp set-active: <namespace> and <on|off> are required\n");
11472
+ return { exitCode: 2 };
11473
+ }
11474
+ const base = { home: opts.home, baseUrl: opts.baseUrl };
11475
+ try {
11476
+ for (let attempt = 0; ; attempt++) {
11477
+ const res = await deps.getResource(SET_ACTIVE_RESOURCE, base);
11478
+ const data = res.data ?? { servers: [] };
11479
+ const servers = Array.isArray(data.servers) ? data.servers : [];
11480
+ const idx = servers.findIndex((s) => s?.namespace === namespace);
11481
+ if (idx < 0) {
11482
+ return fail(
11483
+ io,
11484
+ opts.json,
11485
+ `No team server with namespace "${namespace}". Run \`yaw-mcp sync status\` to list them.`,
11486
+ 1
11487
+ );
11488
+ }
11489
+ if (servers[idx].isActive !== false === active) {
11490
+ return done(io, opts.json, namespace, active, false);
11491
+ }
11492
+ const nextServers = servers.map((s, i) => i === idx ? { ...s, isActive: active } : s);
11493
+ try {
11494
+ const putRes = await deps.putResource(
11495
+ SET_ACTIVE_RESOURCE,
11496
+ res.version,
11497
+ { ...data, version: 1, servers: nextServers },
11498
+ base
11499
+ );
11500
+ if (typeof putRes.version === "number") {
11501
+ await deps.writeSyncState(opts.home ?? homedir16(), {
11502
+ mcp_bundles: { lastPulledVersion: putRes.version }
11503
+ }).catch(() => {
11504
+ });
11505
+ }
11506
+ return done(io, opts.json, namespace, active, true);
11507
+ } catch (e) {
11508
+ if (e instanceof TeamSyncStaleVersionError && attempt < 1) continue;
11509
+ throw e;
11510
+ }
11511
+ }
11512
+ } catch (e) {
11513
+ if (e instanceof TeamSyncAuthError)
11514
+ return fail(io, opts.json, "Not signed in. Run `yaw-mcp login --key <license-key>` first.", 1);
11515
+ if (e instanceof TeamSyncForbiddenError)
11516
+ return fail(io, opts.json, "You do not have permission to edit the team's servers.", 1);
11517
+ return fail(io, opts.json, e instanceof Error ? e.message : String(e), 1);
11518
+ }
11519
+ }
11520
+ function done(io, json, namespace, active, changed) {
11521
+ if (json) {
11522
+ io.out(`${JSON.stringify({ ok: true, namespace, isActive: active, changed })}
11523
+ `);
11524
+ } else if (changed) {
11525
+ io.out(`${namespace} is now ${active ? "active" : "inactive"} for the team.
11526
+ `);
11527
+ } else {
11528
+ io.out(`${namespace} is already ${active ? "active" : "inactive"}.
11529
+ `);
11530
+ }
11531
+ return { exitCode: 0 };
11532
+ }
11533
+ function fail(io, json, message, code) {
11534
+ if (json) io.err(`${JSON.stringify({ ok: false, error: message })}
11535
+ `);
11536
+ else io.err(`yaw-mcp set-active: ${message}
11537
+ `);
11538
+ return { exitCode: code };
11539
+ }
11540
+
10224
11541
  // src/stats-cmd.ts
10225
- import { homedir as homedir14 } from "os";
11542
+ import { homedir as homedir17 } from "os";
10226
11543
  var STATS_USAGE = `Usage: yaw-mcp stats [--json] [--limit N] [--days N]
10227
11544
 
10228
11545
  Print a digest of recent AI tool calls recorded against your Yaw
@@ -10242,18 +11559,22 @@ function parseStatsArgs(argv) {
10242
11559
  const v = argv[++i];
10243
11560
  const n = Number.parseInt(v ?? "", 10);
10244
11561
  if (!Number.isFinite(n) || n <= 0 || n > 1e3)
10245
- return { ok: false, error: "yaw-mcp stats: --limit must be a positive integer up to 1000\n\n" + STATS_USAGE };
11562
+ return { ok: false, error: `yaw-mcp stats: --limit must be a positive integer up to 1000
11563
+
11564
+ ${STATS_USAGE}` };
10246
11565
  opts.limit = n;
10247
11566
  } else if (a === "--days") {
10248
11567
  const v = argv[++i];
10249
11568
  const n = Number.parseInt(v ?? "", 10);
10250
11569
  if (!Number.isFinite(n) || n <= 0 || n > 365)
10251
- return { ok: false, error: "yaw-mcp stats: --days must be a positive integer up to 365\n\n" + STATS_USAGE };
11570
+ return { ok: false, error: `yaw-mcp stats: --days must be a positive integer up to 365
11571
+
11572
+ ${STATS_USAGE}` };
10252
11573
  opts.days = n;
10253
11574
  } else if (a === "--json") {
10254
11575
  opts.json = true;
10255
11576
  } else if (a === "--help" || a === "-h") {
10256
- return { ok: false, error: STATS_USAGE };
11577
+ return { ok: false, error: STATS_USAGE, help: true };
10257
11578
  } else {
10258
11579
  return { ok: false, error: `yaw-mcp stats: unknown argument "${a}"
10259
11580
 
@@ -10294,8 +11615,9 @@ function aggregate(events) {
10294
11615
  function formatPlain(events, opts, orderId, total) {
10295
11616
  const lines = [];
10296
11617
  lines.push(`Signed in to order ${orderId}.`);
11618
+ const renderedCount = Math.min(events.length, opts.limit ?? 50);
10297
11619
  lines.push(
10298
- `Showing ${events.length} of ${total} event${total === 1 ? "" : "s"} from the last ${opts.days ?? 7} day${opts.days === 1 ? "" : "s"}.`
11620
+ `Showing ${renderedCount} of ${total} event${total === 1 ? "" : "s"} from the last ${opts.days ?? 7} day${opts.days === 1 ? "" : "s"}.`
10299
11621
  );
10300
11622
  if (events.length === 0) {
10301
11623
  lines.push("");
@@ -10306,7 +11628,7 @@ function formatPlain(events, opts, orderId, total) {
10306
11628
  }
10307
11629
  const agg = aggregate(events);
10308
11630
  lines.push("");
10309
- lines.push("By server:");
11631
+ lines.push("By server (full window):");
10310
11632
  const nsCol = Math.max(...agg.byNamespace.map((n) => n.namespace.length), 6);
10311
11633
  for (const n of agg.byNamespace) {
10312
11634
  const success = n.success.toString().padStart(5);
@@ -10337,7 +11659,7 @@ async function runStats(opts, io = {
10337
11659
  out: (s) => process.stdout.write(s),
10338
11660
  err: (s) => process.stderr.write(s)
10339
11661
  }) {
10340
- const home = opts.home ?? homedir14();
11662
+ const home = opts.home ?? homedir17();
10341
11663
  const session = await getSession({ home, baseUrl: opts.baseUrl });
10342
11664
  if (!session) {
10343
11665
  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>";
@@ -10351,7 +11673,7 @@ async function runStats(opts, io = {
10351
11673
  const result = await listAnalyticsEvents({ home, baseUrl: opts.baseUrl });
10352
11674
  const days = opts.days ?? 7;
10353
11675
  const cutoff = Date.now() - days * 24 * 60 * 60 * 1e3;
10354
- const filtered = result.events.filter((e) => e.ts >= cutoff);
11676
+ const filtered = result.events.filter((e) => e.ts >= cutoff).sort((a, b) => a.ts - b.ts);
10355
11677
  if (opts.json) {
10356
11678
  io.out(
10357
11679
  `${JSON.stringify(
@@ -10394,10 +11716,10 @@ async function runStats(opts, io = {
10394
11716
  }
10395
11717
 
10396
11718
  // src/sync-cmd.ts
10397
- import { existsSync as existsSync7 } from "fs";
10398
- import { mkdir as mkdir4, readFile as readFile11 } from "fs/promises";
10399
- import { homedir as homedir15 } from "os";
10400
- import { dirname as dirname4, join as join11 } from "path";
11719
+ import { existsSync as existsSync8 } from "fs";
11720
+ import { mkdir as mkdir5, readFile as readFile12 } from "fs/promises";
11721
+ import { homedir as homedir18 } from "os";
11722
+ import { dirname as dirname3, join as join12 } from "path";
10401
11723
  var SYNC_USAGE = `Usage: yaw-mcp sync <push|pull|status> [--json]
10402
11724
 
10403
11725
  Replicate ~/.yaw-mcp/bundles.json across machines via your Yaw
@@ -10410,7 +11732,9 @@ var SYNC_USAGE = `Usage: yaw-mcp sync <push|pull|status> [--json]
10410
11732
  status Show sign-in state, last-pulled version, and a coarse
10411
11733
  local-vs-remote diff.
10412
11734
 
10413
- --json Emit machine-readable JSON.
11735
+ --json Emit machine-readable JSON.
11736
+ --dry-run (pull only) Preview the merge without writing the local file
11737
+ or advancing the sync version.
10414
11738
 
10415
11739
  Sign in first with \`yaw-mcp login --key <license-key>\`.`;
10416
11740
  var BUNDLES_FILENAME2 = "bundles.json";
@@ -10426,8 +11750,10 @@ ${SYNC_USAGE}` };
10426
11750
  opts.action = a;
10427
11751
  } else if (a === "--json") {
10428
11752
  opts.json = true;
11753
+ } else if (a === "--dry-run") {
11754
+ opts.dryRun = true;
10429
11755
  } else if (a === "--help" || a === "-h") {
10430
- return { ok: false, error: SYNC_USAGE };
11756
+ return { ok: false, error: SYNC_USAGE, help: true };
10431
11757
  } else {
10432
11758
  return { ok: false, error: `yaw-mcp sync: unknown argument "${a}"
10433
11759
 
@@ -10440,24 +11766,30 @@ ${SYNC_USAGE}` };
10440
11766
  return { ok: true, options: opts };
10441
11767
  }
10442
11768
  function bundlesPath(home) {
10443
- return join11(home, CONFIG_DIRNAME, BUNDLES_FILENAME2);
11769
+ return join12(home, CONFIG_DIRNAME, BUNDLES_FILENAME2);
10444
11770
  }
10445
11771
  async function readLocalBundles(home) {
10446
- const path3 = bundlesPath(home);
10447
- if (!existsSync7(path3)) return { version: 1, servers: [] };
10448
- const raw = await readFile11(path3, "utf8");
10449
- const parsed = JSON.parse(raw);
11772
+ const path5 = bundlesPath(home);
11773
+ if (!existsSync8(path5)) return { version: 1, servers: [] };
11774
+ const raw = await readFile12(path5, "utf8");
11775
+ let parsed;
11776
+ try {
11777
+ parsed = JSON.parse(raw);
11778
+ } catch (err) {
11779
+ const msg = err instanceof Error ? err.message : String(err);
11780
+ throw new Error(`${path5}: invalid JSON -- ${msg}. Fix the file before running sync.`);
11781
+ }
10450
11782
  if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.servers)) {
10451
- throw new Error(`${path3}: malformed -- expected { servers: [...] }`);
11783
+ throw new Error(`${path5}: malformed -- expected { servers: [...] }`);
10452
11784
  }
10453
11785
  return parsed;
10454
11786
  }
10455
11787
  async function writeLocalBundles(home, file) {
10456
- const path3 = bundlesPath(home);
10457
- await mkdir4(dirname4(path3), { recursive: true });
10458
- await atomicWriteFile(path3, `${JSON.stringify(file, null, 2)}
11788
+ const path5 = bundlesPath(home);
11789
+ await mkdir5(dirname3(path5), { recursive: true });
11790
+ await atomicWriteFile(path5, `${JSON.stringify(file, null, 2)}
10459
11791
  `);
10460
- return path3;
11792
+ return path5;
10461
11793
  }
10462
11794
  function stripEnvValues(server) {
10463
11795
  if (!server.env || typeof server.env !== "object") return server;
@@ -10478,11 +11810,19 @@ function mergeLocalEnv(incoming, local) {
10478
11810
  return { ...srv, env: merged };
10479
11811
  });
10480
11812
  }
11813
+ function mergeRemoteActive(localStripped, remoteServers) {
11814
+ const remoteByNs = new Map(remoteServers.filter((s) => s.namespace).map((s) => [s.namespace, s]));
11815
+ return localStripped.map((srv) => {
11816
+ const matched = srv.namespace ? remoteByNs.get(srv.namespace) : void 0;
11817
+ if (!matched || matched.isActive === void 0) return srv;
11818
+ return { ...srv, isActive: matched.isActive };
11819
+ });
11820
+ }
10481
11821
  async function runSync(opts, io = {
10482
11822
  out: (s) => process.stdout.write(s),
10483
11823
  err: (s) => process.stderr.write(s)
10484
11824
  }) {
10485
- const home = opts.home ?? homedir15();
11825
+ const home = opts.home ?? homedir18();
10486
11826
  const session = await getSession({ home, baseUrl: opts.baseUrl });
10487
11827
  if (!session) {
10488
11828
  const msg = "Not signed in. Run `yaw-mcp login --key <license-key>` first.";
@@ -10508,7 +11848,9 @@ async function syncStatus(session, opts, io, home) {
10508
11848
  home: opts.home,
10509
11849
  baseUrl: opts.baseUrl
10510
11850
  });
10511
- const local = await readLocalBundles(home).catch(() => ({ servers: [] }));
11851
+ const local = await readLocalBundles(home);
11852
+ const syncState = await readSyncState(home);
11853
+ const lastPulledVersion = syncState.mcp_bundles?.lastPulledVersion ?? null;
10512
11854
  const localNs = new Set(local.servers.map((s) => s.namespace).filter((n) => typeof n === "string"));
10513
11855
  const remoteNs = new Set(
10514
11856
  (remote.data?.servers ?? []).map((s) => s.namespace).filter((n) => typeof n === "string")
@@ -10523,6 +11865,7 @@ async function syncStatus(session, opts, io, home) {
10523
11865
  signedInAs: session.email,
10524
11866
  role: session.role,
10525
11867
  remoteVersion: remote.version,
11868
+ lastPulledVersion,
10526
11869
  localOnly,
10527
11870
  remoteOnly,
10528
11871
  updatedAt: remote.updated_at,
@@ -10539,6 +11882,8 @@ async function syncStatus(session, opts, io, home) {
10539
11882
  io.out(`Remote mcp_bundles: version ${remote.version}`);
10540
11883
  if (remote.updated_at) io.out(`, updated ${remote.updated_at} by ${remote.updated_by ?? "unknown"}`);
10541
11884
  io.out("\n");
11885
+ io.out(lastPulledVersion === null ? "Last pulled: never pulled.\n" : `Last pulled: v${lastPulledVersion}.
11886
+ `);
10542
11887
  if (localOnly.length > 0) io.out(`Local-only servers: ${localOnly.join(", ")}
10543
11888
  `);
10544
11889
  if (remoteOnly.length > 0) io.out(`Remote-only servers: ${remoteOnly.join(", ")}
@@ -10553,9 +11898,26 @@ async function syncPull(opts, io, home) {
10553
11898
  baseUrl: opts.baseUrl
10554
11899
  });
10555
11900
  const remoteServers = remote.data?.servers ?? [];
10556
- const local = await readLocalBundles(home).catch(() => ({ servers: [] }));
11901
+ const local = await readLocalBundles(home);
10557
11902
  const merged = mergeLocalEnv(remoteServers, local.servers);
11903
+ if (opts.dryRun) {
11904
+ const target = bundlesPath(home);
11905
+ if (opts.json) {
11906
+ io.out(
11907
+ `${JSON.stringify({ ok: true, dryRun: true, wouldWrite: target, serverCount: merged.length, remoteVersion: remote.version }, null, 2)}
11908
+ `
11909
+ );
11910
+ } else {
11911
+ io.out(
11912
+ `[dry-run] would pull ${merged.length} server${merged.length === 1 ? "" : "s"} -> ${target} (remote v${remote.version}); nothing written.
11913
+ `
11914
+ );
11915
+ if (remote.version === 0) io.out("Remote mcp_bundles is empty (version 0). Push from this machine to seed it.\n");
11916
+ }
11917
+ return { exitCode: 0 };
11918
+ }
10558
11919
  const written = await writeLocalBundles(home, { version: 1, servers: merged });
11920
+ await writeSyncState(home, { mcp_bundles: { lastPulledVersion: remote.version } });
10559
11921
  if (opts.json) {
10560
11922
  io.out(
10561
11923
  `${JSON.stringify({ ok: true, written, serverCount: merged.length, remoteVersion: remote.version }, null, 2)}
@@ -10574,19 +11936,24 @@ async function syncPush(opts, io, home) {
10574
11936
  home: opts.home,
10575
11937
  baseUrl: opts.baseUrl
10576
11938
  });
10577
- const stripped = local.servers.map(stripEnvValues);
11939
+ const remoteServers = remote.data?.servers ?? [];
11940
+ const stripped = mergeRemoteActive(local.servers.map(stripEnvValues), remoteServers);
10578
11941
  const payload = { version: 1, servers: stripped };
10579
- const res = await putResource(MCP_BUNDLES_RESOURCE, remote.version, payload, {
11942
+ const syncState = await readSyncState(home);
11943
+ const lastPulled = syncState.mcp_bundles?.lastPulledVersion;
11944
+ const pushVersion = lastPulled ?? remote.version;
11945
+ const res = await putResource(MCP_BUNDLES_RESOURCE, pushVersion, payload, {
10580
11946
  home: opts.home,
10581
11947
  baseUrl: opts.baseUrl
10582
11948
  });
11949
+ await writeSyncState(home, { mcp_bundles: { lastPulledVersion: res.version } });
10583
11950
  if (opts.json) {
10584
11951
  io.out(`${JSON.stringify({ ok: true, serverCount: stripped.length, newVersion: res.version }, null, 2)}
10585
11952
  `);
10586
11953
  } else {
10587
11954
  io.out(`Pushed ${stripped.length} server${stripped.length === 1 ? "" : "s"} -> mcp_bundles v${res.version}.
10588
11955
  `);
10589
- io.out("Env values stripped before upload; secrets stay machine-local until Phase 6b ships the encrypted vault.\n");
11956
+ io.out("Env values stripped before upload; use `yaw-mcp secrets push` to sync secrets across machines.\n");
10590
11957
  }
10591
11958
  return { exitCode: 0 };
10592
11959
  }
@@ -10625,6 +11992,7 @@ function handleSyncError(err, opts, io) {
10625
11992
  var KNOWN_SUBCOMMANDS = [
10626
11993
  "compliance",
10627
11994
  "audit",
11995
+ "foundry",
10628
11996
  "install",
10629
11997
  "add",
10630
11998
  "remove",
@@ -10642,6 +12010,7 @@ var KNOWN_SUBCOMMANDS = [
10642
12010
  "sync",
10643
12011
  "stats",
10644
12012
  "secrets",
12013
+ "set-active",
10645
12014
  "help",
10646
12015
  "--help",
10647
12016
  "-h",
@@ -10659,9 +12028,23 @@ if (subcommand === "compliance") {
10659
12028
  process.exit(2);
10660
12029
  }
10661
12030
  runAudit(parsed.options).then((r) => process.exit(r.exitCode));
12031
+ } else if (subcommand === "foundry") {
12032
+ const parsed = parseFoundryArgs(process.argv.slice(3));
12033
+ if (!parsed.ok) {
12034
+ const isHelp = parsed.error === FOUNDRY_USAGE;
12035
+ (isHelp ? process.stdout : process.stderr).write(`${parsed.error}
12036
+ `);
12037
+ process.exit(isHelp ? 0 : 2);
12038
+ }
12039
+ runFoundryExport(parsed.options).then((r) => process.exit(r.exitCode));
10662
12040
  } else if (subcommand === "install") {
10663
12041
  const parsed = parseInstallArgs(process.argv.slice(3));
10664
12042
  if (!parsed.ok) {
12043
+ if (parsed.help) {
12044
+ process.stdout.write(`${parsed.error}
12045
+ `);
12046
+ process.exit(0);
12047
+ }
10665
12048
  process.stderr.write(`${parsed.error}
10666
12049
  `);
10667
12050
  process.exit(2);
@@ -10700,6 +12083,11 @@ if (subcommand === "compliance") {
10700
12083
  } else if (subcommand === "servers") {
10701
12084
  const parsed = parseServersArgs(process.argv.slice(3));
10702
12085
  if (!parsed.ok) {
12086
+ if (parsed.help) {
12087
+ process.stdout.write(`${parsed.error}
12088
+ `);
12089
+ process.exit(0);
12090
+ }
10703
12091
  process.stderr.write(`${parsed.error}
10704
12092
  `);
10705
12093
  process.exit(2);
@@ -10708,6 +12096,11 @@ if (subcommand === "compliance") {
10708
12096
  } else if (subcommand === "bundles") {
10709
12097
  const parsed = parseBundlesArgs(process.argv.slice(3));
10710
12098
  if (!parsed.ok) {
12099
+ if (parsed.help) {
12100
+ process.stdout.write(`${parsed.error}
12101
+ `);
12102
+ process.exit(0);
12103
+ }
10711
12104
  process.stderr.write(`${parsed.error}
10712
12105
  `);
10713
12106
  process.exit(2);
@@ -10716,6 +12109,11 @@ if (subcommand === "compliance") {
10716
12109
  } else if (subcommand === "completion") {
10717
12110
  const parsed = parseCompletionArgs(process.argv.slice(3));
10718
12111
  if (!parsed.ok) {
12112
+ if (parsed.help) {
12113
+ process.stdout.write(`${parsed.error}
12114
+ `);
12115
+ process.exit(0);
12116
+ }
10719
12117
  process.stderr.write(`${parsed.error}
10720
12118
  `);
10721
12119
  process.exit(2);
@@ -10724,6 +12122,11 @@ if (subcommand === "compliance") {
10724
12122
  } else if (subcommand === "upgrade") {
10725
12123
  const parsed = parseUpgradeArgs(process.argv.slice(3));
10726
12124
  if (!parsed.ok) {
12125
+ if (parsed.help) {
12126
+ process.stdout.write(`${parsed.error}
12127
+ `);
12128
+ process.exit(0);
12129
+ }
10727
12130
  process.stderr.write(`${parsed.error}
10728
12131
  `);
10729
12132
  process.exit(2);
@@ -10732,6 +12135,11 @@ if (subcommand === "compliance") {
10732
12135
  } else if (subcommand === "try") {
10733
12136
  const parsed = parseTryArgs(process.argv.slice(3));
10734
12137
  if (!parsed.ok) {
12138
+ if (parsed.help) {
12139
+ process.stdout.write(`${parsed.error}
12140
+ `);
12141
+ process.exit(0);
12142
+ }
10735
12143
  process.stderr.write(`${parsed.error}
10736
12144
  `);
10737
12145
  process.exit(2);
@@ -10740,6 +12148,11 @@ if (subcommand === "compliance") {
10740
12148
  } else if (subcommand === "try-cleanup") {
10741
12149
  const parsed = parseTryCleanupArgs(process.argv.slice(3));
10742
12150
  if (!parsed.ok) {
12151
+ if (parsed.help) {
12152
+ process.stdout.write(`${parsed.error}
12153
+ `);
12154
+ process.exit(0);
12155
+ }
10743
12156
  process.stderr.write(`${parsed.error}
10744
12157
  `);
10745
12158
  process.exit(2);
@@ -10748,6 +12161,11 @@ if (subcommand === "compliance") {
10748
12161
  } else if (subcommand === "add") {
10749
12162
  const parsed = parseAddArgs(process.argv.slice(3));
10750
12163
  if (!parsed.ok) {
12164
+ if (parsed.help) {
12165
+ process.stdout.write(`${parsed.error}
12166
+ `);
12167
+ process.exit(0);
12168
+ }
10751
12169
  process.stderr.write(`${parsed.error}
10752
12170
  `);
10753
12171
  process.exit(2);
@@ -10756,6 +12174,11 @@ if (subcommand === "compliance") {
10756
12174
  } else if (subcommand === "remove") {
10757
12175
  const parsed = parseRemoveArgs(process.argv.slice(3));
10758
12176
  if (!parsed.ok) {
12177
+ if (parsed.help) {
12178
+ process.stdout.write(`${parsed.error}
12179
+ `);
12180
+ process.exit(0);
12181
+ }
10759
12182
  process.stderr.write(`${parsed.error}
10760
12183
  `);
10761
12184
  process.exit(2);
@@ -10764,6 +12187,11 @@ if (subcommand === "compliance") {
10764
12187
  } else if (subcommand === "list") {
10765
12188
  const parsed = parseListArgs(process.argv.slice(3));
10766
12189
  if (!parsed.ok) {
12190
+ if (parsed.help) {
12191
+ process.stdout.write(`${parsed.error}
12192
+ `);
12193
+ process.exit(0);
12194
+ }
10767
12195
  process.stderr.write(`${parsed.error}
10768
12196
  `);
10769
12197
  process.exit(2);
@@ -10772,6 +12200,11 @@ if (subcommand === "compliance") {
10772
12200
  } else if (subcommand === "login") {
10773
12201
  const parsed = parseLoginArgs(process.argv.slice(3));
10774
12202
  if (!parsed.ok) {
12203
+ if (parsed.help) {
12204
+ process.stdout.write(`${parsed.error}
12205
+ `);
12206
+ process.exit(0);
12207
+ }
10775
12208
  process.stderr.write(`${parsed.error}
10776
12209
  `);
10777
12210
  process.exit(2);
@@ -10780,6 +12213,11 @@ if (subcommand === "compliance") {
10780
12213
  } else if (subcommand === "logout") {
10781
12214
  const parsed = parseLogoutArgs(process.argv.slice(3));
10782
12215
  if (!parsed.ok) {
12216
+ if (parsed.help) {
12217
+ process.stdout.write(`${parsed.error}
12218
+ `);
12219
+ process.exit(0);
12220
+ }
10783
12221
  process.stderr.write(`${parsed.error}
10784
12222
  `);
10785
12223
  process.exit(2);
@@ -10788,6 +12226,11 @@ if (subcommand === "compliance") {
10788
12226
  } else if (subcommand === "sync") {
10789
12227
  const parsed = parseSyncArgs(process.argv.slice(3));
10790
12228
  if (!parsed.ok) {
12229
+ if (parsed.help) {
12230
+ process.stdout.write(`${parsed.error}
12231
+ `);
12232
+ process.exit(0);
12233
+ }
10791
12234
  process.stderr.write(`${parsed.error}
10792
12235
  `);
10793
12236
  process.exit(2);
@@ -10796,6 +12239,11 @@ if (subcommand === "compliance") {
10796
12239
  } else if (subcommand === "stats") {
10797
12240
  const parsed = parseStatsArgs(process.argv.slice(3));
10798
12241
  if (!parsed.ok) {
12242
+ if (parsed.help) {
12243
+ process.stdout.write(`${parsed.error}
12244
+ `);
12245
+ process.exit(0);
12246
+ }
10799
12247
  process.stderr.write(`${parsed.error}
10800
12248
  `);
10801
12249
  process.exit(2);
@@ -10804,19 +12252,38 @@ if (subcommand === "compliance") {
10804
12252
  } else if (subcommand === "secrets") {
10805
12253
  const parsed = parseSecretsArgs(process.argv.slice(3));
10806
12254
  if (!parsed.ok) {
12255
+ if (parsed.help) {
12256
+ process.stdout.write(`${parsed.error}
12257
+ `);
12258
+ process.exit(0);
12259
+ }
10807
12260
  process.stderr.write(`${parsed.error}
10808
12261
  `);
10809
12262
  process.exit(2);
10810
12263
  }
10811
12264
  runSecrets(parsed.options).then((r) => process.exit(r.exitCode));
12265
+ } else if (subcommand === "set-active") {
12266
+ const parsed = parseSetActiveArgs(process.argv.slice(3));
12267
+ if (!parsed.ok) {
12268
+ if (parsed.help) {
12269
+ process.stdout.write(`${parsed.error}
12270
+ `);
12271
+ process.exit(0);
12272
+ }
12273
+ process.stderr.write(`${parsed.error}
12274
+ `);
12275
+ process.exit(2);
12276
+ }
12277
+ runSetActive(parsed.options).then((r) => process.exit(r.exitCode));
10812
12278
  } else if (subcommand === "--help" || subcommand === "-h" || subcommand === "help") {
10813
12279
  process.stdout.write(`
10814
12280
  yaw-mcp \u2014 one install, every MCP server, managed from the cloud.
10815
12281
 
10816
12282
  Quickstart:
10817
- 1. Get a token https://yaw.sh/mcp/dashboard/settings/tokens
10818
- 2. Install yaw-mcp yaw-mcp install claude-code --token mcp_pat_...
10819
- 3. Verify setup yaw-mcp doctor
12283
+ 1. Install yaw-mcp yaw-mcp install claude-code
12284
+ 2. Verify setup yaw-mcp doctor
12285
+ 3. Yaw Team (optional) yaw-mcp login --key <license-key>
12286
+ https://yaw.sh/mcp/dashboard/settings/tokens
10820
12287
 
10821
12288
  Setup (connect a client to yaw-mcp):
10822
12289
  install <client> Connect one MCP client to yaw-mcp. This wires the
@@ -10919,12 +12386,12 @@ if (subcommand === "compliance") {
10919
12386
  (or kill yaw-mcp; the client will respawn it) after editing any config.
10920
12387
 
10921
12388
  Docs: https://yaw.sh/mcp
10922
- Source: https://github.com/YawLabs/yaw-mcp
12389
+ Source: https://github.com/YawLabs/mcp
10923
12390
 
10924
12391
  `);
10925
12392
  process.exit(0);
10926
12393
  } else if (subcommand === "--version" || subcommand === "-V") {
10927
- process.stdout.write(`yaw-mcp ${true ? "0.60.6" : "dev"}
12394
+ process.stdout.write(`yaw-mcp ${true ? "0.62.0" : "dev"}
10928
12395
  `);
10929
12396
  process.exit(0);
10930
12397
  } else if (subcommand && !subcommand.startsWith("-")) {
@@ -10966,6 +12433,8 @@ async function runServer() {
10966
12433
  };
10967
12434
  process.on("SIGTERM", shutdown);
10968
12435
  process.on("SIGINT", shutdown);
12436
+ process.on("unhandledRejection", (e) => log("error", "unhandledRejection", { error: String(e) }));
12437
+ process.on("uncaughtException", (e) => log("error", "uncaughtException", { error: String(e) }));
10969
12438
  server.start().catch((err) => {
10970
12439
  if (err instanceof ConfigError && err.fatal) {
10971
12440
  const msg2 = err instanceof Error ? err.message : String(err);