agent-skills-standard 2.7.2 → 2.7.3

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
@@ -40,20 +40,20 @@ js_yaml = __toESM(js_yaml);
40
40
  let zod = require("zod");
41
41
  let node_path = require("node:path");
42
42
  node_path = __toESM(node_path);
43
+ let child_process = require("child_process");
43
44
  let os = require("os");
44
45
  os = __toESM(os);
45
46
  let node_stream = require("node:stream");
46
47
  let node_child_process = require("node:child_process");
47
48
  let node_os = require("node:os");
48
49
  node_os = __toESM(node_os);
49
- let child_process = require("child_process");
50
50
  let fs = require("fs");
51
51
  fs = __toESM(fs);
52
52
  let crypto = require("crypto");
53
53
  crypto = __toESM(crypto);
54
54
  var package_default = {
55
55
  name: "agent-skills-standard",
56
- version: "2.7.2",
56
+ version: "2.7.3",
57
57
  description: "A CLI to manage and sync AI agent skills standard for Cursor, Claude, Copilot, Windsurf, and more.",
58
58
  main: "dist/index.js",
59
59
  bin: {
@@ -1436,6 +1436,11 @@ var McpConfigSchema = zod.z.object({
1436
1436
  scope: McpScopeSchema,
1437
1437
  prompted: zod.z.boolean(),
1438
1438
  version: zod.z.string().optional(),
1439
+ via: zod.z.enum([
1440
+ "auto",
1441
+ "npx",
1442
+ "global"
1443
+ ]).optional(),
1439
1444
  snippets: zod.z.boolean().optional()
1440
1445
  });
1441
1446
  var SkillConfigSchema = zod.z.object({
@@ -1479,6 +1484,7 @@ var MCP_FIELD_KEYS = [
1479
1484
  "scope",
1480
1485
  "prompted",
1481
1486
  "version",
1487
+ "via",
1482
1488
  "snippets"
1483
1489
  ];
1484
1490
  var DEFAULT_SDLC_SUPPORT_CATEGORIES = ["quality-engineering"];
@@ -3629,11 +3635,10 @@ var LockfileService = class {
3629
3635
  * agent-skills-standard-mcp`, which would silently pull whatever is latest
3630
3636
  * on npm at the moment the agent starts.
3631
3637
  *
3632
- * Bump this alongside mcp/package.json's version when releasing — there is
3633
- * no automated sync between the two (mirrors how skills/metadata.json
3634
- * category versions are maintained today).
3638
+ * Bumped automatically by `scripts/release-mcp.ts` when the MCP package is
3639
+ * released and gated in CI, so this pin always matches `mcp/package.json`.
3635
3640
  */
3636
- var MCP_COMPATIBLE_VERSION = "0.6.1";
3641
+ var MCP_COMPATIBLE_VERSION = "0.8.0";
3637
3642
  /**
3638
3643
  * Minimum `agent-skills-standard-mcp` version required for session retrospectives.
3639
3644
  */
@@ -3655,6 +3660,10 @@ function escapeTomlString(value) {
3655
3660
  }
3656
3661
  //#endregion
3657
3662
  //#region src/services/utils/codexTomlMcp.ts
3663
+ /** Structural equality for the server entry we generate and re-read. */
3664
+ function sameMcpEntry(a, b) {
3665
+ return a.command === b.command && a.args.length === b.args.length && a.args.every((arg, index) => arg === b.args[index]);
3666
+ }
3658
3667
  var HEADER_RE = /^\s*\[\[?\s*([^\]]+?)\s*\]\]?\s*(#.*)?$/;
3659
3668
  function normalizeKey(raw) {
3660
3669
  return raw.split(".").map((s) => s.trim().replace(/^"(.*)"$/, "$1").replace(/^'(.*)'$/, "$1")).join(".");
@@ -3692,6 +3701,61 @@ function renderTomlMcpServer(name, entry) {
3692
3701
  function hasTomlMcpServer(content, name) {
3693
3702
  return findOurRanges(content.split("\n"), name).length > 0;
3694
3703
  }
3704
+ function parseTomlStringLiteral(raw) {
3705
+ const value = raw.trim();
3706
+ if (value.length < 2) return null;
3707
+ if (value.startsWith("\"") && value.endsWith("\"")) return value.slice(1, -1).replace(/\\(["\\])/g, "$1");
3708
+ if (value.startsWith("'") && value.endsWith("'")) return value.slice(1, -1);
3709
+ return null;
3710
+ }
3711
+ var STRING_TOKEN_RE = /("(?:\\.|[^"\\])*"|'[^']*')/g;
3712
+ /**
3713
+ * Reads the `[mcp_servers.<name>]` table emitted by `renderTomlMcpServer`.
3714
+ * Returns null when the table is absent or either `command` / `args` is
3715
+ * missing or malformed. Only the exact table block is inspected, so
3716
+ * unrelated tables (and subtables) are ignored.
3717
+ */
3718
+ function readTomlMcpServer(content, name) {
3719
+ const lines = content.split("\n");
3720
+ const base = `mcp_servers.${name}`;
3721
+ let start = -1;
3722
+ for (let i = 0; i < lines.length; i += 1) {
3723
+ const m = HEADER_RE.exec(lines[i]);
3724
+ if (m && normalizeKey(m[1]) === base) {
3725
+ start = i;
3726
+ break;
3727
+ }
3728
+ }
3729
+ if (start === -1) return null;
3730
+ let command = null;
3731
+ let args = null;
3732
+ for (let i = start + 1; i < lines.length; i += 1) {
3733
+ const line = lines[i];
3734
+ if (HEADER_RE.test(line)) break;
3735
+ const commandMatch = /^\s*command\s*=\s*((?:"(?:\\.|[^"\\])*")|(?:'[^']*'))\s*(?:#.*)?$/.exec(line);
3736
+ if (commandMatch) {
3737
+ command = parseTomlStringLiteral(commandMatch[1]);
3738
+ continue;
3739
+ }
3740
+ const argsMatch = /^\s*args\s*=\s*\[(.*)\]\s*(?:#.*)?$/.exec(line);
3741
+ if (argsMatch) {
3742
+ const tokens = argsMatch[1].match(STRING_TOKEN_RE) ?? [];
3743
+ if (argsMatch[1].replace(STRING_TOKEN_RE, "").replace(/[\s,]/g, "").length > 0) return null;
3744
+ const parsed = [];
3745
+ for (const token of tokens) {
3746
+ const value = parseTomlStringLiteral(token);
3747
+ if (value === null) return null;
3748
+ parsed.push(value);
3749
+ }
3750
+ args = parsed;
3751
+ }
3752
+ }
3753
+ if (command === null || args === null) return null;
3754
+ return {
3755
+ command,
3756
+ args
3757
+ };
3758
+ }
3695
3759
  function removeTomlMcpServer(content, name) {
3696
3760
  const lines = content.split("\n");
3697
3761
  const ranges = findOurRanges(lines, name);
@@ -3727,6 +3791,10 @@ function upsertTomlMcpServer(content, name, entry) {
3727
3791
  //#region src/services/McpConfigService.ts
3728
3792
  var SERVER_NAME = "agent-skills-standard";
3729
3793
  var PACKAGE = "agent-skills-standard-mcp";
3794
+ /** Sentinel: the `ags-mcp` command is not on PATH (no manifest fallback allowed). */
3795
+ var GLOBAL_BINARY_MISSING = Symbol("ags-mcp-missing");
3796
+ var SEMVER_PREFIX = /^\d+\.\d+\.\d+/;
3797
+ var SEMVER_EXACT = /^\d+\.\d+\.\d+$/;
3730
3798
  var getTargets = (home = os.default.homedir(), platform = process.platform) => {
3731
3799
  const out = {};
3732
3800
  for (const agent of Object.values(Agent)) {
@@ -3761,18 +3829,111 @@ var McpConfigService = class {
3761
3829
  * <package>`, which would otherwise pull whatever is latest on npm the
3762
3830
  * moment the agent starts — an unpinned update-drift surface (AST07).
3763
3831
  * Pass an explicit `version` (from `mcp.version` in .skillsrc) to override.
3832
+ *
3833
+ * `via` selects the launch strategy (default `auto`). A global install
3834
+ * avoids the ~0.6s npx resolution overhead on every client spawn, but a
3835
+ * stale global must never be silently preferred over the pin.
3764
3836
  */
3765
- buildEntry(version) {
3837
+ buildEntry(opts = {}) {
3838
+ const version = opts.version || "0.8.0";
3839
+ const via = opts.via ?? "auto";
3840
+ const platform = this.testPlatform ?? process.platform;
3841
+ if (via === "global" || via === "auto" && platform !== "win32" && this.detectGlobalVersion() === version) return platform === "win32" ? {
3842
+ command: "cmd",
3843
+ args: ["/c", "ags-mcp"]
3844
+ } : {
3845
+ command: "ags-mcp",
3846
+ args: []
3847
+ };
3766
3848
  return {
3767
3849
  command: "npx",
3768
- args: ["-y", `${PACKAGE}@${version || "0.6.1"}`]
3850
+ args: [
3851
+ "-y",
3852
+ "--prefer-offline",
3853
+ `${PACKAGE}@${version}`
3854
+ ]
3769
3855
  };
3770
3856
  }
3857
+ /**
3858
+ * Version reported by a globally installed `ags-mcp`, or `null` when the
3859
+ * binary is missing/unusable.
3860
+ *
3861
+ * Two probes, in order:
3862
+ * 1. `ags-mcp --version` — authoritative and install-manager agnostic.
3863
+ * 2. the manifest under `npm root -g` — required for servers published
3864
+ * before `--version` existed, where probe 1 either prints nothing or
3865
+ * times out because the stdio server starts normally instead.
3866
+ * A missing binary (ENOENT) never falls through: an installed manifest
3867
+ * alone does not prove the command is on the client's PATH.
3868
+ */
3869
+ detectGlobalVersion() {
3870
+ const binaryVersion = this.probeGlobalBinaryVersion();
3871
+ if (binaryVersion === GLOBAL_BINARY_MISSING) return null;
3872
+ return binaryVersion ?? this.probeGlobalManifestVersion();
3873
+ }
3874
+ /** `ags-mcp --version`, or the sentinel when the command is not on PATH. */
3875
+ probeGlobalBinaryVersion() {
3876
+ try {
3877
+ if (this.testGlobalVersionProbe) return this.testGlobalVersionProbe();
3878
+ const output = (0, child_process.execFileSync)("ags-mcp", ["--version"], {
3879
+ encoding: "utf8",
3880
+ timeout: 2e3,
3881
+ stdio: [
3882
+ "ignore",
3883
+ "pipe",
3884
+ "ignore"
3885
+ ]
3886
+ });
3887
+ const firstLine = String(output).split("\n")[0].trim();
3888
+ return SEMVER_PREFIX.test(firstLine) ? firstLine : null;
3889
+ } catch (err) {
3890
+ return err.code === "ENOENT" ? GLOBAL_BINARY_MISSING : null;
3891
+ }
3892
+ }
3893
+ /** Version field of the globally installed manifest, purely from disk. */
3894
+ probeGlobalManifestVersion() {
3895
+ if (this.testGlobalManifestProbe) return this.testGlobalManifestProbe();
3896
+ const isWindows = (this.testPlatform ?? process.platform) === "win32";
3897
+ try {
3898
+ const globalRoot = String((0, child_process.execFileSync)(isWindows ? "npm.cmd" : "npm", ["root", "-g"], {
3899
+ encoding: "utf8",
3900
+ timeout: 5e3,
3901
+ stdio: [
3902
+ "ignore",
3903
+ "pipe",
3904
+ "ignore"
3905
+ ],
3906
+ ...isWindows ? { shell: true } : {}
3907
+ })).trim();
3908
+ if (!globalRoot) return null;
3909
+ const manifestPath = path.default.join(globalRoot, PACKAGE, "package.json");
3910
+ if (!fs_extra.default.pathExistsSync(manifestPath)) return null;
3911
+ const manifest = fs_extra.default.readJsonSync(manifestPath);
3912
+ return typeof manifest.version === "string" && SEMVER_EXACT.test(manifest.version) ? manifest.version : null;
3913
+ } catch {
3914
+ return null;
3915
+ }
3916
+ }
3771
3917
  /** For testing: allows overriding the home directory. */
3772
3918
  testHome = null;
3773
3919
  setHomeForTesting(home) {
3774
3920
  this.testHome = home;
3775
3921
  }
3922
+ /** For testing: allows overriding the detected platform. */
3923
+ testPlatform = null;
3924
+ setPlatformForTesting(platform) {
3925
+ this.testPlatform = platform;
3926
+ }
3927
+ /** For testing: allows overriding the global-version probe. */
3928
+ testGlobalVersionProbe = null;
3929
+ setGlobalVersionProbeForTesting(fn) {
3930
+ this.testGlobalVersionProbe = fn;
3931
+ }
3932
+ /** For testing: allows overriding the `npm root -g` manifest probe. */
3933
+ testGlobalManifestProbe = null;
3934
+ setGlobalManifestProbeForTesting(fn) {
3935
+ this.testGlobalManifestProbe = fn;
3936
+ }
3776
3937
  getTargets() {
3777
3938
  return getTargets(this.testHome ?? void 0);
3778
3939
  }
@@ -3791,7 +3952,10 @@ var McpConfigService = class {
3791
3952
  const { rootDir, agents, mcp } = opts;
3792
3953
  if (mcp.scope === "disabled" || !mcp.enabled) return report;
3793
3954
  const TARGETS = this.getTargets();
3794
- const entry = this.buildEntry(mcp.version);
3955
+ const entry = this.buildEntry({
3956
+ version: mcp.version,
3957
+ via: mcp.via
3958
+ });
3795
3959
  if (mcp.scope === "snippets-only" || mcp.snippets) await this.generateSnippets(rootDir, agents, entry, report);
3796
3960
  if (mcp.scope === "snippets-only") return report;
3797
3961
  for (const agent of agents) {
@@ -3868,12 +4032,45 @@ var McpConfigService = class {
3868
4032
  const target = TARGETS[agent];
3869
4033
  if (!target) continue;
3870
4034
  const row = { agent };
3871
- if (target.projectFile) row.project = await this.hasOurEntry(path.default.join(opts.rootDir, target.projectFile), target);
3872
- if (target.userFile) row.user = await this.hasOurEntry(target.userFile, target);
4035
+ if (target.projectFile) {
4036
+ const abs = path.default.join(opts.rootDir, target.projectFile);
4037
+ row.project = await this.hasOurEntry(abs, target);
4038
+ row.projectEntry = await this.readEntry(abs, target);
4039
+ }
4040
+ if (target.userFile) {
4041
+ row.user = await this.hasOurEntry(target.userFile, target);
4042
+ row.userEntry = await this.readEntry(target.userFile, target);
4043
+ }
3873
4044
  out.push(row);
3874
4045
  }
3875
4046
  return out;
3876
4047
  }
4048
+ /** Parse the entry this service wrote for `SERVER_NAME`, if present. */
4049
+ async readEntry(abs, target) {
4050
+ if (!await fs_extra.default.pathExists(abs)) return null;
4051
+ if (target.format === "toml") return readTomlMcpServer(await fs_extra.default.readFile(abs, "utf8"), SERVER_NAME);
4052
+ const data = await fs_extra.default.readJson(abs).catch(() => null);
4053
+ if (!data) return null;
4054
+ const container = this.getNestedValue(data, target.key);
4055
+ if (!container) return null;
4056
+ if (target.shape === "map") {
4057
+ if (typeof container !== "object" || Array.isArray(container)) return null;
4058
+ return this.normalizeEntry(container[SERVER_NAME]);
4059
+ }
4060
+ if (!Array.isArray(container)) return null;
4061
+ const item = container.find((entry) => entry.name === SERVER_NAME);
4062
+ return this.normalizeEntry(item?.transport);
4063
+ }
4064
+ normalizeEntry(value) {
4065
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
4066
+ const record = value;
4067
+ if (typeof record.command !== "string") return null;
4068
+ const args = Array.isArray(record.args) ? record.args.map((arg) => String(arg)) : [];
4069
+ return {
4070
+ command: record.command,
4071
+ args
4072
+ };
4073
+ }
3877
4074
  async generateSnippets(rootDir, agents, entry, report) {
3878
4075
  const dir = path.default.join(rootDir, "mcp-config-snippets");
3879
4076
  await fs_extra.default.ensureDir(dir);
@@ -3973,7 +4170,11 @@ var McpConfigService = class {
3973
4170
  if (!data) return false;
3974
4171
  const container = this.getNestedValue(data, target.key);
3975
4172
  if (!container) return false;
3976
- if (target.shape === "map") return Boolean(container[SERVER_NAME]);
4173
+ if (target.shape === "map") {
4174
+ if (typeof container !== "object" || Array.isArray(container)) return false;
4175
+ return Boolean(container[SERVER_NAME]);
4176
+ }
4177
+ if (!Array.isArray(container)) return false;
3977
4178
  return container.some((item) => item.name === SERVER_NAME);
3978
4179
  }
3979
4180
  buildFreshDoc(target, entry) {
@@ -4076,6 +4277,10 @@ function defaultDoctorDeps() {
4076
4277
  rootDir,
4077
4278
  agents
4078
4279
  }),
4280
+ buildMcpEntry: (cfg) => mcp.buildEntry({
4281
+ version: cfg.mcp?.version,
4282
+ via: cfg.mcp?.via
4283
+ }),
4079
4284
  hookStatus: (rootDir, agents) => hooks.status({
4080
4285
  rootDir,
4081
4286
  agents
@@ -4209,13 +4414,48 @@ var DoctorService = class {
4209
4414
  if (!config.mcp?.enabled || scope !== "project" && scope !== "user") return check("mcp", "skip", "MCP disabled in .skillsrc");
4210
4415
  const rows = await this.deps.mcpStatus(rootDir, agents);
4211
4416
  const missing = rows.filter((r) => scope === "project" ? !r.project : !(r.project || r.user)).map((r) => r.agent);
4212
- if (missing.length === 0) return check("mcp", "ok", `registered for ${rows.map((r) => r.agent).join(", ") || "no MCP-capable agents"}`);
4213
- return check("mcp", "fail", `MCP server not registered for: ${missing.join(", ")}`, {
4417
+ if (missing.length > 0) return check("mcp", "fail", `MCP server not registered for: ${missing.join(", ")}`, {
4214
4418
  description: `register MCP for ${missing.join(", ")}`,
4215
4419
  apply: async () => {
4216
4420
  for (const agent of missing) await this.deps.installMcp(rootDir, agent, config);
4217
4421
  }
4218
4422
  });
4423
+ const expected = this.deps.buildMcpEntry ? this.deps.buildMcpEntry(config) : new McpConfigService().buildEntry({
4424
+ version: config.mcp.version,
4425
+ via: config.mcp.via
4426
+ });
4427
+ const pin = config.mcp.version ?? "0.8.0";
4428
+ const drifted = [];
4429
+ for (const row of rows) {
4430
+ if (scope === "project") {
4431
+ if (row.project && row.projectEntry && !sameMcpEntry(row.projectEntry, expected)) drifted.push({
4432
+ agent: row.agent,
4433
+ scope: "project",
4434
+ entry: row.projectEntry
4435
+ });
4436
+ continue;
4437
+ }
4438
+ if (row.project && row.projectEntry && !sameMcpEntry(row.projectEntry, expected)) drifted.push({
4439
+ agent: row.agent,
4440
+ scope: "project",
4441
+ entry: row.projectEntry
4442
+ });
4443
+ if (row.user && row.userEntry && !sameMcpEntry(row.userEntry, expected)) drifted.push({
4444
+ agent: row.agent,
4445
+ scope: "user",
4446
+ entry: row.userEntry
4447
+ });
4448
+ }
4449
+ if (drifted.length > 0) {
4450
+ const driftedAgents = [...new Set(drifted.map((item) => item.agent))];
4451
+ return check("mcp", "warn", `MCP pin drift: ${drifted.map((item) => `${item.agent} ${item.scope} pins ${item.entry.command}${item.entry.args.length ? ` ${item.entry.args.join(" ")}` : ""} but this CLI expects agent-skills-standard-mcp@${pin}`).join("; ")}`, {
4452
+ description: `refresh MCP entry for ${driftedAgents.join(", ")}`,
4453
+ apply: async () => {
4454
+ for (const agent of driftedAgents) await this.deps.installMcp(rootDir, agent, config);
4455
+ }
4456
+ });
4457
+ }
4458
+ return check("mcp", "ok", `registered for ${rows.map((r) => r.agent).join(", ") || "no MCP-capable agents"}`);
4219
4459
  }
4220
4460
  async hooksCheck(rootDir, agents) {
4221
4461
  const rows = await this.deps.hookStatus(rootDir, agents);
@@ -10330,26 +10570,66 @@ var VALID_SCOPES = [
10330
10570
  "snippets-only",
10331
10571
  "disabled"
10332
10572
  ];
10573
+ var VALID_VIAS = [
10574
+ "auto",
10575
+ "npx",
10576
+ "global"
10577
+ ];
10578
+ function parseVia(value) {
10579
+ return typeof value === "string" && VALID_VIAS.includes(value) ? value : null;
10580
+ }
10581
+ function defaultGlobalInstaller(spec) {
10582
+ if (process.platform === "win32") {
10583
+ (0, child_process.execFileSync)("npm.cmd", [
10584
+ "i",
10585
+ "-g",
10586
+ spec
10587
+ ], {
10588
+ stdio: "inherit",
10589
+ shell: true
10590
+ });
10591
+ return;
10592
+ }
10593
+ (0, child_process.execFileSync)("npm", [
10594
+ "i",
10595
+ "-g",
10596
+ spec
10597
+ ], { stdio: "inherit" });
10598
+ }
10599
+ function formatEntry(entry) {
10600
+ if (!entry) return "(none)";
10601
+ return entry.args.length ? `${entry.command} ${entry.args.join(" ")}` : entry.command;
10602
+ }
10333
10603
  /**
10334
10604
  * `ags mcp <action>` — manage the optional MCP server integration without
10335
10605
  * editing `.skillsrc` by hand.
10336
10606
  *
10337
10607
  * Sub-actions:
10338
- * status Show current scope, enabled state, and which configs are wired
10339
- * enable Set mcp.enabled=true; install configs at the configured scope
10608
+ * status Show scope, launch mode, resolved command, and installed configs
10609
+ * enable [--via=...] Set mcp.enabled=true; optionally install configs at the configured scope
10340
10610
  * disable Set mcp.enabled=false; existing configs are NOT removed
10341
10611
  * scope <scope> Change scope: project | user | snippets-only | disabled
10342
- * install [--scope=...] One-shot install at the given (or configured) scope
10612
+ * via <auto|npx|global> Change launch mode (default: auto)
10613
+ * install [--scope=...] [--via=...]
10614
+ * One-shot install at the given (or configured) scope/mode
10343
10615
  * uninstall [--from=...] Remove our entry from configs (project | user | all)
10344
- * snippets Generate fresh snippet files in ./mcp-config-snippets/
10616
+ * snippets [--via=...] Generate fresh snippet files in ./mcp-config-snippets/
10617
+ *
10618
+ * `--install-global` (on enable) installs `agent-skills-standard-mcp@<pin>`
10619
+ * globally, then verifies the installed binary reports the pin.
10345
10620
  */
10346
10621
  var McpCommand = class {
10347
10622
  configService;
10348
10623
  mcpService;
10624
+ globalInstaller = defaultGlobalInstaller;
10349
10625
  constructor(configService, mcpService) {
10350
10626
  this.configService = configService ?? new ConfigService();
10351
10627
  this.mcpService = mcpService ?? new McpConfigService();
10352
10628
  }
10629
+ /** For testing: replace the global npm installer (never run real npm in unit tests). */
10630
+ setGlobalInstallerForTesting(fn) {
10631
+ this.globalInstaller = fn ?? defaultGlobalInstaller;
10632
+ }
10353
10633
  async run(action, options = {}) {
10354
10634
  const config = await this.configService.loadConfig();
10355
10635
  if (!config) {
@@ -10360,13 +10640,14 @@ var McpCommand = class {
10360
10640
  const agents = config.agents ?? [];
10361
10641
  switch (action) {
10362
10642
  case "status": return this.actionStatus(config, cwd);
10363
- case "enable": return this.actionEnable(config);
10643
+ case "enable": return this.actionEnable(config, agents, cwd, options);
10364
10644
  case "disable": return this.actionDisable(config);
10365
10645
  case "scope": return this.actionScope(config, options);
10646
+ case "via": return this.actionVia(config, options);
10366
10647
  case "install": return this.actionInstall(config, agents, cwd, options);
10367
10648
  case "uninstall": return this.actionUninstall(config, agents, cwd, options);
10368
- case "snippets": return this.actionSnippets(config, agents, cwd);
10369
- default: console.log(picocolors.default.red(`Unknown action: ${action}`), "\nValid actions: status, enable, disable, scope, install, uninstall, snippets");
10649
+ case "snippets": return this.actionSnippets(config, agents, cwd, options);
10650
+ default: console.log(picocolors.default.red(`Unknown action: ${action}`), "\nValid actions: status, enable, disable, scope, via, install, uninstall, snippets");
10370
10651
  }
10371
10652
  }
10372
10653
  async actionStatus(config, cwd) {
@@ -10379,20 +10660,50 @@ var McpCommand = class {
10379
10660
  console.log(picocolors.default.bold("\n🔌 MCP integration status\n"));
10380
10661
  console.log(` enabled : ${mcp.enabled ? picocolors.default.green("yes") : picocolors.default.gray("no")}`);
10381
10662
  console.log(` scope : ${picocolors.default.cyan(mcp.scope)}`);
10663
+ console.log(` via : ${picocolors.default.cyan(mcp.via ?? "auto")}`);
10382
10664
  console.log(` prompted: ${mcp.prompted ? "yes" : "no"}`);
10383
10665
  if (mcp.version) console.log(` version : ${mcp.version}`);
10666
+ const pin = mcp.version ?? "0.8.0";
10667
+ const entry = this.mcpService.buildEntry({
10668
+ version: mcp.version,
10669
+ via: mcp.via
10670
+ });
10671
+ const launch = entry.args.length ? `${entry.command} ${entry.args.join(" ")}` : entry.command;
10672
+ console.log(` launch : ${picocolors.default.gray(launch)}`);
10673
+ console.log(` pin : ${pin}`);
10674
+ const detected = this.mcpService.detectGlobalVersion();
10675
+ console.log(` global : ${detected ? picocolors.default.green(detected) : picocolors.default.gray("not found")}`);
10676
+ if (detected && detected !== pin) {
10677
+ console.log(picocolors.default.yellow(`\n⚠️ Global ags-mcp is ${detected} but the CLI pin is ${pin}; "auto" will use npx until they match.`));
10678
+ console.log(picocolors.default.gray(` Update with: npm i -g ${PACKAGE}@${pin}`));
10679
+ }
10384
10680
  if (rows.length === 0) {
10385
10681
  console.log(picocolors.default.gray("\n (no agents configured)"));
10386
10682
  return;
10387
10683
  }
10388
10684
  console.log("\n Per-agent installation state:");
10389
10685
  let installedAnywhere = false;
10686
+ const drift = [];
10390
10687
  for (const row of rows) {
10391
- const project = row.project === void 0 ? "—" : row.project ? picocolors.default.green("✓") : picocolors.default.gray("✗");
10392
- const user = row.user === void 0 ? "—" : row.user ? picocolors.default.green("✓") : picocolors.default.gray("✗");
10688
+ const project = row.project === void 0 ? "—" : `${row.project ? picocolors.default.green("✓") : picocolors.default.gray("✗")} ${formatEntry(row.projectEntry)}`;
10689
+ const user = row.user === void 0 ? "—" : `${row.user ? picocolors.default.green("✓") : picocolors.default.gray("✗")} ${formatEntry(row.userEntry)}`;
10393
10690
  if (row.project === true || row.user === true) installedAnywhere = true;
10691
+ if (row.project === true && row.projectEntry && !sameMcpEntry(row.projectEntry, entry)) drift.push({
10692
+ agent: row.agent,
10693
+ scope: "project",
10694
+ configured: row.projectEntry
10695
+ });
10696
+ if (row.user === true && row.userEntry && !sameMcpEntry(row.userEntry, entry)) drift.push({
10697
+ agent: row.agent,
10698
+ scope: "user",
10699
+ configured: row.userEntry
10700
+ });
10394
10701
  console.log(` ${row.agent.padEnd(12)} project: ${project} user: ${user}`);
10395
10702
  }
10703
+ if (drift.length > 0) {
10704
+ for (const item of drift) console.log(picocolors.default.yellow(`\n⚠️ MCP pin drift: ${item.agent} ${item.scope} pins ${formatEntry(item.configured)} but this CLI expects ${launch}.`));
10705
+ console.log(picocolors.default.gray(" Run `ags sync` (or `ags mcp install`) to refresh; existing agents pick it up on their next MCP start."));
10706
+ }
10396
10707
  if (!mcp.enabled && installedAnywhere) {
10397
10708
  console.log(picocolors.default.yellow("\n⚠️ Mismatch detected: .skillsrc says mcp.enabled=false, but the MCP entry IS present in at least one runtime config."));
10398
10709
  console.log(picocolors.default.gray(" The MCP will work, but `sync` will not keep its config in step. Consider one of:"));
@@ -10403,12 +10714,38 @@ var McpCommand = class {
10403
10714
  console.log(picocolors.default.gray(" Run `ags mcp install` to write the entries, or `ags mcp disable` to clear the consent flag."));
10404
10715
  }
10405
10716
  }
10406
- async actionEnable(config) {
10717
+ async actionEnable(config, agents, cwd, options) {
10407
10718
  const mcp = config.mcp ?? defaultMcpConfig();
10719
+ const viaOverrideRaw = options.via;
10720
+ const viaOverride = viaOverrideRaw ? parseVia(viaOverrideRaw) : void 0;
10721
+ if (viaOverrideRaw && !viaOverride) {
10722
+ console.log(picocolors.default.red(`Invalid --via value. Valid: ${VALID_VIAS.join(" | ")}`));
10723
+ return;
10724
+ }
10408
10725
  if (mcp.scope === "disabled") {
10409
10726
  console.log(picocolors.default.yellow("⚠️ scope is currently \"disabled\". Set a real scope first: `ags mcp scope project`"));
10410
10727
  return;
10411
10728
  }
10729
+ if (options.installGlobal) {
10730
+ const pin = mcp.version ?? "0.8.0";
10731
+ const spec = `${PACKAGE}@${pin}`;
10732
+ try {
10733
+ this.globalInstaller(spec);
10734
+ } catch {
10735
+ console.log(picocolors.default.red(`❌ Global install failed. Run manually:`));
10736
+ console.log(picocolors.default.gray(` npm i -g ${spec}`));
10737
+ process.exitCode = 1;
10738
+ return;
10739
+ }
10740
+ const detected = this.mcpService.detectGlobalVersion();
10741
+ if (detected !== pin) {
10742
+ console.log(picocolors.default.red(`❌ Global ags-mcp reports ${detected ?? "no version"} but the CLI pin is ${pin}.`));
10743
+ console.log(picocolors.default.gray(` Run manually: npm i -g ${spec}`));
10744
+ process.exitCode = 1;
10745
+ return;
10746
+ }
10747
+ console.log(picocolors.default.green(`✅ Global ags-mcp ${pin} installed and verified.`));
10748
+ }
10412
10749
  config.mcp = {
10413
10750
  ...mcp,
10414
10751
  enabled: true,
@@ -10416,8 +10753,37 @@ var McpCommand = class {
10416
10753
  };
10417
10754
  await this.configService.saveConfig(config);
10418
10755
  console.log(picocolors.default.green("✅ MCP enabled."));
10756
+ if (viaOverride) {
10757
+ const report = await this.mcpService.install({
10758
+ rootDir: cwd,
10759
+ agents,
10760
+ mcp: {
10761
+ ...config.mcp,
10762
+ via: viaOverride
10763
+ }
10764
+ });
10765
+ console.log(picocolors.default.gray(` Installed configs using launch mode "${viaOverride}".`));
10766
+ this.printReport(report, cwd);
10767
+ return;
10768
+ }
10419
10769
  console.log(picocolors.default.gray(" Run `ags mcp install` to write configs now, or it happens on next `sync`."));
10420
10770
  }
10771
+ async actionVia(config, options) {
10772
+ const via = options.via ?? options._?.[0];
10773
+ if (!parseVia(via)) {
10774
+ console.log(picocolors.default.red(`Invalid via. Valid: ${VALID_VIAS.join(" | ")}`));
10775
+ return;
10776
+ }
10777
+ config.mcp = {
10778
+ ...config.mcp ?? defaultMcpConfig(),
10779
+ via,
10780
+ prompted: true
10781
+ };
10782
+ await this.configService.saveConfig(config);
10783
+ console.log(picocolors.default.green(`✅ MCP launch mode set to "${via}".`));
10784
+ if (via === "global") console.log(picocolors.default.yellow("⚠️ \"global\" requires a matching global install: `ags mcp enable --install-global`."));
10785
+ else if (via === "auto") console.log(picocolors.default.gray(" \"auto\" uses a version-matched global ags-mcp when present, otherwise npx."));
10786
+ }
10421
10787
  async actionDisable(config) {
10422
10788
  config.mcp = {
10423
10789
  ...config.mcp ?? defaultMcpConfig(),
@@ -10451,6 +10817,12 @@ var McpCommand = class {
10451
10817
  console.log(picocolors.default.red(`Invalid --scope value. Valid: ${VALID_SCOPES.join(" | ")}`));
10452
10818
  return;
10453
10819
  }
10820
+ const viaOverrideRaw = options.via;
10821
+ const viaOverride = viaOverrideRaw ? parseVia(viaOverrideRaw) : void 0;
10822
+ if (viaOverrideRaw && !viaOverride) {
10823
+ console.log(picocolors.default.red(`Invalid --via value. Valid: ${VALID_VIAS.join(" | ")}`));
10824
+ return;
10825
+ }
10454
10826
  if (effectiveScope === "disabled") {
10455
10827
  console.log(picocolors.default.yellow("Scope is \"disabled\" — nothing to install."));
10456
10828
  return;
@@ -10470,7 +10842,8 @@ var McpCommand = class {
10470
10842
  mcp: {
10471
10843
  ...mcp,
10472
10844
  enabled: true,
10473
- scope: effectiveScope
10845
+ scope: effectiveScope,
10846
+ ...viaOverride ? { via: viaOverride } : {}
10474
10847
  },
10475
10848
  userScopePrompt: userPrompt
10476
10849
  });
@@ -10498,15 +10871,22 @@ var McpCommand = class {
10498
10871
  console.log(picocolors.default.green(`✅ Removed agent-skills MCP entry from:`));
10499
10872
  for (const r of removed) console.log(` - ${picocolors.default.cyan(r.agent)} → ${picocolors.default.gray(r.file)}`);
10500
10873
  }
10501
- async actionSnippets(config, agents, cwd) {
10874
+ async actionSnippets(config, agents, cwd, options) {
10502
10875
  const mcp = config.mcp ?? defaultMcpConfig();
10876
+ const viaOverrideRaw = options.via;
10877
+ const viaOverride = viaOverrideRaw ? parseVia(viaOverrideRaw) : void 0;
10878
+ if (viaOverrideRaw && !viaOverride) {
10879
+ console.log(picocolors.default.red(`Invalid --via value. Valid: ${VALID_VIAS.join(" | ")}`));
10880
+ return;
10881
+ }
10503
10882
  const report = await this.mcpService.install({
10504
10883
  rootDir: cwd,
10505
10884
  agents,
10506
10885
  mcp: {
10507
10886
  ...mcp,
10508
10887
  enabled: true,
10509
- scope: "snippets-only"
10888
+ scope: "snippets-only",
10889
+ ...viaOverride ? { via: viaOverride } : {}
10510
10890
  }
10511
10891
  });
10512
10892
  if (report.snippets.length === 0) {
@@ -10605,12 +10985,14 @@ var RetrospectiveCommand = class {
10605
10985
  env;
10606
10986
  customExit;
10607
10987
  log;
10988
+ mcpVersion;
10608
10989
  constructor(deps = {}) {
10609
10990
  this.spawnFn = deps.spawnFn ?? defaultSpawn;
10610
10991
  this.platform = deps.platform ?? process.platform;
10611
10992
  this.env = deps.env ?? process.env;
10612
10993
  this.customExit = deps.exit;
10613
10994
  this.log = deps.log ?? console.log;
10995
+ this.mcpVersion = deps.mcpVersion ?? "0.8.0";
10614
10996
  }
10615
10997
  exit(code) {
10616
10998
  process.exitCode = code;
@@ -10639,8 +11021,8 @@ var RetrospectiveCommand = class {
10639
11021
  }
10640
11022
  }
10641
11023
  const envSpec = this.env.AGS_MCP_PACKAGE_SPEC;
10642
- if (!(typeof envSpec === "string" && envSpec.trim() !== "") && !isVersionAtLeast("0.6.1", "0.8.0")) {
10643
- this.log(`Session retrospectives need agent-skills-standard-mcp ${RETROSPECTIVE_MIN_MCP_VERSION} or newer, but this ags is pinned to ${MCP_COMPATIBLE_VERSION}. Upgrade ags (ags upgrade), or set AGS_MCP_PACKAGE_SPEC to a newer package for testing.`);
11024
+ if (!(typeof envSpec === "string" && envSpec.trim() !== "") && !isVersionAtLeast(this.mcpVersion, "0.8.0")) {
11025
+ this.log(`Session retrospectives need agent-skills-standard-mcp ${RETROSPECTIVE_MIN_MCP_VERSION} or newer, but this ags is pinned to ${this.mcpVersion}. Upgrade ags (ags upgrade), or set AGS_MCP_PACKAGE_SPEC to a newer package for testing.`);
10644
11026
  this.exit(1);
10645
11027
  return;
10646
11028
  }
@@ -10659,7 +11041,8 @@ var RetrospectiveCommand = class {
10659
11041
  }
10660
11042
  const argsToSpawn = [
10661
11043
  "-y",
10662
- typeof envSpec === "string" && envSpec.trim() !== "" ? envSpec : `${PACKAGE}@${MCP_COMPATIBLE_VERSION}`,
11044
+ "--prefer-offline",
11045
+ typeof envSpec === "string" && envSpec.trim() !== "" ? envSpec : `${PACKAGE}@${this.mcpVersion}`,
10663
11046
  "retrospective",
10664
11047
  mappedAction,
10665
11048
  ...forwarded
@@ -11338,7 +11721,11 @@ var AuditCommand = class {
11338
11721
  * It checks the latest version on npm and performs the upgrade using the detected package manager.
11339
11722
  */
11340
11723
  var UpgradeCommand = class {
11724
+ deps;
11341
11725
  currentVersion = package_default.version;
11726
+ constructor(deps = {}) {
11727
+ this.deps = deps;
11728
+ }
11342
11729
  /**
11343
11730
  * Executes the upgrade flow.
11344
11731
  * @param options Command options, including `dryRun` to check for updates without installing.
@@ -11383,6 +11770,7 @@ var UpgradeCommand = class {
11383
11770
  const fallbackCmd = this.getUpgradeCommand(pm, "latest");
11384
11771
  (0, child_process.execSync)(fallbackCmd, { stdio: "inherit" });
11385
11772
  }
11773
+ await this.printMcpRefreshHint();
11386
11774
  if (pm === "pnpm") {
11387
11775
  const installedVersion = this.getInstalledVersionFromPnpm(latestVersion);
11388
11776
  if (installedVersion !== latestVersion) {
@@ -11434,6 +11822,12 @@ var UpgradeCommand = class {
11434
11822
  this.printManualInstructions(pm, latestVersion);
11435
11823
  }
11436
11824
  }
11825
+ async printMcpRefreshHint() {
11826
+ try {
11827
+ if (!(this.deps.loadMcpEnabled ? await this.deps.loadMcpEnabled() : (await new ConfigService().loadConfig())?.mcp?.enabled ?? false)) return;
11828
+ console.log(picocolors.default.gray(`MCP configs are not rewritten by upgrade — run \`ags sync\` (or \`ags mcp install\`) to move them to ${MCP_COMPATIBLE_VERSION}.`));
11829
+ } catch {}
11830
+ }
11437
11831
  detectPackageManager() {
11438
11832
  const execPath = process.argv[1] || "";
11439
11833
  if (execPath.includes("pnpm") || (process.env.npm_config_user_agent || "").includes("pnpm")) return "pnpm";
@@ -13410,9 +13804,12 @@ program.command("feedback").description("Report skill improvement opportunities
13410
13804
  program.command("upgrade").description("Upgrade the CLI to the latest version").option("--dry-run", "Check for updates without installing").action(async (options) => {
13411
13805
  await new UpgradeCommand().run(options);
13412
13806
  });
13413
- program.command("mcp <action> [scope]").description("Manage the optional MCP server integration. Actions: status | enable | disable | scope <project|user|snippets-only|disabled> | install | uninstall | snippets").option("--scope <scope>", "Override scope for install (project|user|snippets-only|disabled)").option("--from <from>", "For uninstall: project | user | all (default: project)").action(async (action, positional, opts) => {
13807
+ program.command("mcp <action> [scope]").description("Manage the optional MCP server integration. Actions: status | enable | disable | scope <project|user|snippets-only|disabled> | via <auto|npx|global> | install | uninstall | snippets").option("--scope <scope>", "Override scope for install (project|user|snippets-only|disabled)").option("--via <via>", "Override launch mode for this run (auto|npx|global)").option("--install-global", "On enable: install agent-skills-standard-mcp@<pin> globally, then verify its version").option("--from <from>", "For uninstall: project | user | all (default: project)").action(async (action, positional, opts) => {
13414
13808
  const merged = { ...opts };
13415
- if (positional) merged.scope = opts.scope ?? positional;
13809
+ if (positional) {
13810
+ if (action === "via") merged.via = opts.via ?? positional;
13811
+ else merged.scope = opts.scope ?? positional;
13812
+ }
13416
13813
  await new McpCommand().run(action, merged);
13417
13814
  });
13418
13815
  program.command("retrospective <action> [args...]").description("Enable, disable or inspect local session retrospectives (consent and the cross-session index). Runs the pinned agent-skills-standard-mcp package. Actions: enable | disable | status | index").allowUnknownOption(true).option("--store-root <path>", "Store root directory").option("--source-root <path>", "Additional source root for transcripts (repeatable)", (val, prev) => prev ? [...prev, val] : [val]).option("--since <YYYY-MM-DD>", "Index sessions since date").option("--until <YYYY-MM-DD>", "Index sessions until date").option("--folder <name>", "Index sessions from folder").action(async (action, args = [], options = {}) => {