agent-skills-standard 2.7.2 → 2.7.4

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.4",
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.1";
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)) {
@@ -3749,6 +3817,27 @@ var getTargets = (home = os.default.homedir(), platform = process.platform) => {
3749
3817
  return out;
3750
3818
  };
3751
3819
  /**
3820
+ * Command used to run `ags-mcp --version`.
3821
+ *
3822
+ * On Windows npm exposes the binary as a `.cmd` shim, which `execFileSync`
3823
+ * cannot spawn directly (ENOENT), so the probe goes through `cmd /c` — the
3824
+ * same shape the generated global launch entry uses. POSIX keeps the bare
3825
+ * binary with no shell.
3826
+ */
3827
+ function globalVersionProbeCommand(platform = process.platform) {
3828
+ return platform === "win32" ? {
3829
+ command: "cmd",
3830
+ args: [
3831
+ "/c",
3832
+ "ags-mcp",
3833
+ "--version"
3834
+ ]
3835
+ } : {
3836
+ command: "ags-mcp",
3837
+ args: ["--version"]
3838
+ };
3839
+ }
3840
+ /**
3752
3841
  * Owns all MCP-config writes. Pure logic — never prompts the user directly.
3753
3842
  * The caller (sync command, mcp subcommand) supplies a `userScopePrompt`
3754
3843
  * callback that is invoked ONLY when the user has chosen `scope: 'user'` AND
@@ -3761,18 +3850,143 @@ var McpConfigService = class {
3761
3850
  * <package>`, which would otherwise pull whatever is latest on npm the
3762
3851
  * moment the agent starts — an unpinned update-drift surface (AST07).
3763
3852
  * Pass an explicit `version` (from `mcp.version` in .skillsrc) to override.
3853
+ *
3854
+ * `via` selects the launch strategy (default `auto`). A global install
3855
+ * avoids the ~0.6s npx resolution overhead on every client spawn, but a
3856
+ * stale global must never be silently preferred over the pin.
3764
3857
  */
3765
- buildEntry(version) {
3858
+ buildEntry(opts = {}) {
3859
+ const version = opts.version || "0.8.1";
3860
+ const via = opts.via ?? "auto";
3861
+ const platform = this.testPlatform ?? process.platform;
3862
+ if (via === "global" || via === "auto" && platform !== "win32" && this.detectGlobalVersion() === version) return platform === "win32" ? {
3863
+ command: "cmd",
3864
+ args: ["/c", "ags-mcp"]
3865
+ } : {
3866
+ command: "ags-mcp",
3867
+ args: []
3868
+ };
3766
3869
  return {
3767
3870
  command: "npx",
3768
- args: ["-y", `${PACKAGE}@${version || "0.6.1"}`]
3871
+ args: [
3872
+ "-y",
3873
+ "--prefer-offline",
3874
+ `${PACKAGE}@${version}`
3875
+ ]
3769
3876
  };
3770
3877
  }
3878
+ /**
3879
+ * Version reported by a globally installed `ags-mcp`, or `null` when the
3880
+ * binary is missing/unusable.
3881
+ *
3882
+ * Two probes, in order:
3883
+ * 1. `ags-mcp --version` — authoritative and install-manager agnostic.
3884
+ * 2. the manifest under `npm root -g` — required for servers published
3885
+ * before `--version` existed, where probe 1 either prints nothing or
3886
+ * times out because the stdio server starts normally instead.
3887
+ * A missing binary never falls through: an installed manifest alone does
3888
+ * not prove the command is on the client's PATH. POSIX surfaces that as
3889
+ * ENOENT; win32 (which probes through `cmd /c`) proves it with
3890
+ * `where.exe ags-mcp` before allowing the legacy manifest fallback.
3891
+ */
3892
+ detectGlobalVersion() {
3893
+ const binaryVersion = this.probeGlobalBinaryVersion();
3894
+ if (binaryVersion === GLOBAL_BINARY_MISSING) return null;
3895
+ if (binaryVersion) return binaryVersion;
3896
+ if (!this.probeGlobalBinaryOnPath()) return null;
3897
+ return this.probeGlobalManifestVersion();
3898
+ }
3899
+ /**
3900
+ * Whether `ags-mcp` resolves on PATH. POSIX misses already surface as
3901
+ * ENOENT in the version probe, so only win32 needs the explicit check.
3902
+ */
3903
+ probeGlobalBinaryOnPath() {
3904
+ if ((this.testPlatform ?? process.platform) !== "win32") return true;
3905
+ if (this.testGlobalPathProbe) return this.testGlobalPathProbe();
3906
+ try {
3907
+ const output = (0, child_process.execFileSync)("where.exe", ["ags-mcp"], {
3908
+ encoding: "utf8",
3909
+ timeout: 2e3,
3910
+ stdio: [
3911
+ "ignore",
3912
+ "pipe",
3913
+ "ignore"
3914
+ ]
3915
+ });
3916
+ return String(output).trim().length > 0;
3917
+ } catch {
3918
+ return false;
3919
+ }
3920
+ }
3921
+ /** `ags-mcp --version`, or the sentinel when the command is not on PATH. */
3922
+ probeGlobalBinaryVersion() {
3923
+ try {
3924
+ if (this.testGlobalVersionProbe) return this.testGlobalVersionProbe();
3925
+ const { command, args } = globalVersionProbeCommand(this.testPlatform ?? process.platform);
3926
+ const output = (0, child_process.execFileSync)(command, args, {
3927
+ encoding: "utf8",
3928
+ timeout: 2e3,
3929
+ stdio: [
3930
+ "ignore",
3931
+ "pipe",
3932
+ "ignore"
3933
+ ]
3934
+ });
3935
+ const firstLine = String(output).split("\n")[0].trim();
3936
+ return SEMVER_PREFIX.test(firstLine) ? firstLine : null;
3937
+ } catch (err) {
3938
+ return err.code === "ENOENT" ? GLOBAL_BINARY_MISSING : null;
3939
+ }
3940
+ }
3941
+ /** Version field of the globally installed manifest, purely from disk. */
3942
+ probeGlobalManifestVersion() {
3943
+ if (this.testGlobalManifestProbe) return this.testGlobalManifestProbe();
3944
+ const isWindows = (this.testPlatform ?? process.platform) === "win32";
3945
+ try {
3946
+ const globalRoot = String((0, child_process.execFileSync)(isWindows ? "npm.cmd" : "npm", ["root", "-g"], {
3947
+ encoding: "utf8",
3948
+ timeout: 5e3,
3949
+ stdio: [
3950
+ "ignore",
3951
+ "pipe",
3952
+ "ignore"
3953
+ ],
3954
+ ...isWindows ? { shell: true } : {}
3955
+ })).trim();
3956
+ if (!globalRoot) return null;
3957
+ const manifestPath = path.default.join(globalRoot, PACKAGE, "package.json");
3958
+ if (!fs_extra.default.pathExistsSync(manifestPath)) return null;
3959
+ const manifest = fs_extra.default.readJsonSync(manifestPath);
3960
+ return typeof manifest.version === "string" && SEMVER_EXACT.test(manifest.version) ? manifest.version : null;
3961
+ } catch {
3962
+ return null;
3963
+ }
3964
+ }
3771
3965
  /** For testing: allows overriding the home directory. */
3772
3966
  testHome = null;
3773
3967
  setHomeForTesting(home) {
3774
3968
  this.testHome = home;
3775
3969
  }
3970
+ /** For testing: allows overriding the detected platform. */
3971
+ testPlatform = null;
3972
+ setPlatformForTesting(platform) {
3973
+ this.testPlatform = platform;
3974
+ }
3975
+ /** For testing: allows overriding the global-version probe. */
3976
+ testGlobalVersionProbe = null;
3977
+ setGlobalVersionProbeForTesting(fn) {
3978
+ this.testGlobalVersionProbe = fn;
3979
+ }
3980
+ /** For testing: allows overriding the `npm root -g` manifest probe. */
3981
+ testGlobalManifestProbe = null;
3982
+ setGlobalManifestProbeForTesting(fn) {
3983
+ this.testGlobalManifestProbe = fn;
3984
+ }
3985
+ /** For testing: allows overriding the win32 PATH-resolution probe. */
3986
+ testGlobalPathProbe = null;
3987
+ setGlobalPathProbeForTesting(fn) {
3988
+ this.testGlobalPathProbe = fn;
3989
+ }
3776
3990
  getTargets() {
3777
3991
  return getTargets(this.testHome ?? void 0);
3778
3992
  }
@@ -3791,7 +4005,10 @@ var McpConfigService = class {
3791
4005
  const { rootDir, agents, mcp } = opts;
3792
4006
  if (mcp.scope === "disabled" || !mcp.enabled) return report;
3793
4007
  const TARGETS = this.getTargets();
3794
- const entry = this.buildEntry(mcp.version);
4008
+ const entry = this.buildEntry({
4009
+ version: mcp.version,
4010
+ via: mcp.via
4011
+ });
3795
4012
  if (mcp.scope === "snippets-only" || mcp.snippets) await this.generateSnippets(rootDir, agents, entry, report);
3796
4013
  if (mcp.scope === "snippets-only") return report;
3797
4014
  for (const agent of agents) {
@@ -3868,12 +4085,45 @@ var McpConfigService = class {
3868
4085
  const target = TARGETS[agent];
3869
4086
  if (!target) continue;
3870
4087
  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);
4088
+ if (target.projectFile) {
4089
+ const abs = path.default.join(opts.rootDir, target.projectFile);
4090
+ row.project = await this.hasOurEntry(abs, target);
4091
+ row.projectEntry = await this.readEntry(abs, target);
4092
+ }
4093
+ if (target.userFile) {
4094
+ row.user = await this.hasOurEntry(target.userFile, target);
4095
+ row.userEntry = await this.readEntry(target.userFile, target);
4096
+ }
3873
4097
  out.push(row);
3874
4098
  }
3875
4099
  return out;
3876
4100
  }
4101
+ /** Parse the entry this service wrote for `SERVER_NAME`, if present. */
4102
+ async readEntry(abs, target) {
4103
+ if (!await fs_extra.default.pathExists(abs)) return null;
4104
+ if (target.format === "toml") return readTomlMcpServer(await fs_extra.default.readFile(abs, "utf8"), SERVER_NAME);
4105
+ const data = await fs_extra.default.readJson(abs).catch(() => null);
4106
+ if (!data) return null;
4107
+ const container = this.getNestedValue(data, target.key);
4108
+ if (!container) return null;
4109
+ if (target.shape === "map") {
4110
+ if (typeof container !== "object" || Array.isArray(container)) return null;
4111
+ return this.normalizeEntry(container[SERVER_NAME]);
4112
+ }
4113
+ if (!Array.isArray(container)) return null;
4114
+ const item = container.find((entry) => entry.name === SERVER_NAME);
4115
+ return this.normalizeEntry(item?.transport);
4116
+ }
4117
+ normalizeEntry(value) {
4118
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
4119
+ const record = value;
4120
+ if (typeof record.command !== "string") return null;
4121
+ const args = Array.isArray(record.args) ? record.args.map((arg) => String(arg)) : [];
4122
+ return {
4123
+ command: record.command,
4124
+ args
4125
+ };
4126
+ }
3877
4127
  async generateSnippets(rootDir, agents, entry, report) {
3878
4128
  const dir = path.default.join(rootDir, "mcp-config-snippets");
3879
4129
  await fs_extra.default.ensureDir(dir);
@@ -3973,7 +4223,11 @@ var McpConfigService = class {
3973
4223
  if (!data) return false;
3974
4224
  const container = this.getNestedValue(data, target.key);
3975
4225
  if (!container) return false;
3976
- if (target.shape === "map") return Boolean(container[SERVER_NAME]);
4226
+ if (target.shape === "map") {
4227
+ if (typeof container !== "object" || Array.isArray(container)) return false;
4228
+ return Boolean(container[SERVER_NAME]);
4229
+ }
4230
+ if (!Array.isArray(container)) return false;
3977
4231
  return container.some((item) => item.name === SERVER_NAME);
3978
4232
  }
3979
4233
  buildFreshDoc(target, entry) {
@@ -4076,6 +4330,10 @@ function defaultDoctorDeps() {
4076
4330
  rootDir,
4077
4331
  agents
4078
4332
  }),
4333
+ buildMcpEntry: (cfg) => mcp.buildEntry({
4334
+ version: cfg.mcp?.version,
4335
+ via: cfg.mcp?.via
4336
+ }),
4079
4337
  hookStatus: (rootDir, agents) => hooks.status({
4080
4338
  rootDir,
4081
4339
  agents
@@ -4209,13 +4467,48 @@ var DoctorService = class {
4209
4467
  if (!config.mcp?.enabled || scope !== "project" && scope !== "user") return check("mcp", "skip", "MCP disabled in .skillsrc");
4210
4468
  const rows = await this.deps.mcpStatus(rootDir, agents);
4211
4469
  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(", ")}`, {
4470
+ if (missing.length > 0) return check("mcp", "fail", `MCP server not registered for: ${missing.join(", ")}`, {
4214
4471
  description: `register MCP for ${missing.join(", ")}`,
4215
4472
  apply: async () => {
4216
4473
  for (const agent of missing) await this.deps.installMcp(rootDir, agent, config);
4217
4474
  }
4218
4475
  });
4476
+ const expected = this.deps.buildMcpEntry ? this.deps.buildMcpEntry(config) : new McpConfigService().buildEntry({
4477
+ version: config.mcp.version,
4478
+ via: config.mcp.via
4479
+ });
4480
+ const pin = config.mcp.version ?? "0.8.1";
4481
+ const drifted = [];
4482
+ for (const row of rows) {
4483
+ if (scope === "project") {
4484
+ if (row.project && row.projectEntry && !sameMcpEntry(row.projectEntry, expected)) drifted.push({
4485
+ agent: row.agent,
4486
+ scope: "project",
4487
+ entry: row.projectEntry
4488
+ });
4489
+ continue;
4490
+ }
4491
+ if (row.project && row.projectEntry && !sameMcpEntry(row.projectEntry, expected)) drifted.push({
4492
+ agent: row.agent,
4493
+ scope: "project",
4494
+ entry: row.projectEntry
4495
+ });
4496
+ if (row.user && row.userEntry && !sameMcpEntry(row.userEntry, expected)) drifted.push({
4497
+ agent: row.agent,
4498
+ scope: "user",
4499
+ entry: row.userEntry
4500
+ });
4501
+ }
4502
+ if (drifted.length > 0) {
4503
+ const driftedAgents = [...new Set(drifted.map((item) => item.agent))];
4504
+ 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("; ")}`, {
4505
+ description: `refresh MCP entry for ${driftedAgents.join(", ")}`,
4506
+ apply: async () => {
4507
+ for (const agent of driftedAgents) await this.deps.installMcp(rootDir, agent, config);
4508
+ }
4509
+ });
4510
+ }
4511
+ return check("mcp", "ok", `registered for ${rows.map((r) => r.agent).join(", ") || "no MCP-capable agents"}`);
4219
4512
  }
4220
4513
  async hooksCheck(rootDir, agents) {
4221
4514
  const rows = await this.deps.hookStatus(rootDir, agents);
@@ -10330,26 +10623,94 @@ var VALID_SCOPES = [
10330
10623
  "snippets-only",
10331
10624
  "disabled"
10332
10625
  ];
10626
+ var VALID_VIAS = [
10627
+ "auto",
10628
+ "npx",
10629
+ "global"
10630
+ ];
10631
+ function parseVia(value) {
10632
+ return typeof value === "string" && VALID_VIAS.includes(value) ? value : null;
10633
+ }
10634
+ function defaultGlobalInstaller(spec) {
10635
+ if (process.platform === "win32") {
10636
+ (0, child_process.execFileSync)("npm.cmd", [
10637
+ "i",
10638
+ "-g",
10639
+ spec
10640
+ ], {
10641
+ stdio: "inherit",
10642
+ shell: true
10643
+ });
10644
+ return;
10645
+ }
10646
+ (0, child_process.execFileSync)("npm", [
10647
+ "i",
10648
+ "-g",
10649
+ spec
10650
+ ], { stdio: "inherit" });
10651
+ }
10652
+ function formatEntry(entry) {
10653
+ if (!entry) return "(none)";
10654
+ return entry.args.length ? `${entry.command} ${entry.args.join(" ")}` : entry.command;
10655
+ }
10333
10656
  /**
10334
10657
  * `ags mcp <action>` — manage the optional MCP server integration without
10335
10658
  * editing `.skillsrc` by hand.
10336
10659
  *
10337
10660
  * 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
10661
+ * status Show scope, launch mode, resolved command, and installed configs
10662
+ * enable [--via=...] Set mcp.enabled=true; optionally install configs at the configured scope
10340
10663
  * disable Set mcp.enabled=false; existing configs are NOT removed
10341
10664
  * scope <scope> Change scope: project | user | snippets-only | disabled
10342
- * install [--scope=...] One-shot install at the given (or configured) scope
10665
+ * via <auto|npx|global> Change launch mode (default: auto)
10666
+ * install [--scope=...] [--via=...]
10667
+ * One-shot install at the given (or configured) scope/mode
10343
10668
  * uninstall [--from=...] Remove our entry from configs (project | user | all)
10344
- * snippets Generate fresh snippet files in ./mcp-config-snippets/
10669
+ * snippets [--via=...] Generate fresh snippet files in ./mcp-config-snippets/
10670
+ *
10671
+ * `enable` also installs `agent-skills-standard-mcp@<pin>` globally and
10672
+ * verifies the reported version whenever the effective launch mode is
10673
+ * `global` (saved mode or `--via global`) — no extra flag required.
10674
+ * `--install-global` remains as an explicit trigger for the same install.
10345
10675
  */
10346
10676
  var McpCommand = class {
10347
10677
  configService;
10348
10678
  mcpService;
10679
+ globalInstaller = defaultGlobalInstaller;
10349
10680
  constructor(configService, mcpService) {
10350
10681
  this.configService = configService ?? new ConfigService();
10351
10682
  this.mcpService = mcpService ?? new McpConfigService();
10352
10683
  }
10684
+ /** For testing: replace the global npm installer (never run real npm in unit tests). */
10685
+ setGlobalInstallerForTesting(fn) {
10686
+ this.globalInstaller = fn ?? defaultGlobalInstaller;
10687
+ }
10688
+ /**
10689
+ * Install the pinned package globally and prove the installed binary
10690
+ * reports that exact pin. Returns false (with exit code 1) when either
10691
+ * step fails so callers can abort before persisting or writing configs.
10692
+ */
10693
+ ensureGlobalPin(pin) {
10694
+ const spec = `${PACKAGE}@${pin}`;
10695
+ try {
10696
+ this.globalInstaller(spec);
10697
+ } catch {
10698
+ console.log(picocolors.default.red(`❌ Global install failed. Run manually:`));
10699
+ console.log(picocolors.default.gray(` npm i -g ${spec}`));
10700
+ process.exitCode = 1;
10701
+ return false;
10702
+ }
10703
+ const detected = this.mcpService.detectGlobalVersion();
10704
+ if (detected !== pin) {
10705
+ console.log(picocolors.default.red(`❌ Global ags-mcp reports ${detected ?? "no version"} but the CLI pin is ${pin}.`));
10706
+ if (!detected) console.log(picocolors.default.gray(" Probe found no usable version: check that npm's global bin directory is on PATH and that `ags-mcp --version` prints the expected pin."));
10707
+ console.log(picocolors.default.gray(` Run manually: npm i -g ${spec}`));
10708
+ process.exitCode = 1;
10709
+ return false;
10710
+ }
10711
+ console.log(picocolors.default.green(`✅ Global ags-mcp ${pin} installed and verified.`));
10712
+ return true;
10713
+ }
10353
10714
  async run(action, options = {}) {
10354
10715
  const config = await this.configService.loadConfig();
10355
10716
  if (!config) {
@@ -10360,13 +10721,14 @@ var McpCommand = class {
10360
10721
  const agents = config.agents ?? [];
10361
10722
  switch (action) {
10362
10723
  case "status": return this.actionStatus(config, cwd);
10363
- case "enable": return this.actionEnable(config);
10724
+ case "enable": return this.actionEnable(config, agents, cwd, options);
10364
10725
  case "disable": return this.actionDisable(config);
10365
10726
  case "scope": return this.actionScope(config, options);
10727
+ case "via": return this.actionVia(config, options);
10366
10728
  case "install": return this.actionInstall(config, agents, cwd, options);
10367
10729
  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");
10730
+ case "snippets": return this.actionSnippets(config, agents, cwd, options);
10731
+ default: console.log(picocolors.default.red(`Unknown action: ${action}`), "\nValid actions: status, enable, disable, scope, via, install, uninstall, snippets");
10370
10732
  }
10371
10733
  }
10372
10734
  async actionStatus(config, cwd) {
@@ -10379,20 +10741,50 @@ var McpCommand = class {
10379
10741
  console.log(picocolors.default.bold("\n🔌 MCP integration status\n"));
10380
10742
  console.log(` enabled : ${mcp.enabled ? picocolors.default.green("yes") : picocolors.default.gray("no")}`);
10381
10743
  console.log(` scope : ${picocolors.default.cyan(mcp.scope)}`);
10744
+ console.log(` via : ${picocolors.default.cyan(mcp.via ?? "auto")}`);
10382
10745
  console.log(` prompted: ${mcp.prompted ? "yes" : "no"}`);
10383
10746
  if (mcp.version) console.log(` version : ${mcp.version}`);
10747
+ const pin = mcp.version ?? "0.8.1";
10748
+ const entry = this.mcpService.buildEntry({
10749
+ version: mcp.version,
10750
+ via: mcp.via
10751
+ });
10752
+ const launch = entry.args.length ? `${entry.command} ${entry.args.join(" ")}` : entry.command;
10753
+ console.log(` launch : ${picocolors.default.gray(launch)}`);
10754
+ console.log(` pin : ${pin}`);
10755
+ const detected = this.mcpService.detectGlobalVersion();
10756
+ console.log(` global : ${detected ? picocolors.default.green(detected) : picocolors.default.gray("not found")}`);
10757
+ if (detected && detected !== pin) {
10758
+ console.log(picocolors.default.yellow(`\n⚠️ Global ags-mcp is ${detected} but the CLI pin is ${pin}; "auto" will use npx until they match.`));
10759
+ console.log(picocolors.default.gray(` Update with: npm i -g ${PACKAGE}@${pin}`));
10760
+ }
10384
10761
  if (rows.length === 0) {
10385
10762
  console.log(picocolors.default.gray("\n (no agents configured)"));
10386
10763
  return;
10387
10764
  }
10388
10765
  console.log("\n Per-agent installation state:");
10389
10766
  let installedAnywhere = false;
10767
+ const drift = [];
10390
10768
  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("✗");
10769
+ const project = row.project === void 0 ? "—" : `${row.project ? picocolors.default.green("✓") : picocolors.default.gray("✗")} ${formatEntry(row.projectEntry)}`;
10770
+ const user = row.user === void 0 ? "—" : `${row.user ? picocolors.default.green("✓") : picocolors.default.gray("✗")} ${formatEntry(row.userEntry)}`;
10393
10771
  if (row.project === true || row.user === true) installedAnywhere = true;
10772
+ if (row.project === true && row.projectEntry && !sameMcpEntry(row.projectEntry, entry)) drift.push({
10773
+ agent: row.agent,
10774
+ scope: "project",
10775
+ configured: row.projectEntry
10776
+ });
10777
+ if (row.user === true && row.userEntry && !sameMcpEntry(row.userEntry, entry)) drift.push({
10778
+ agent: row.agent,
10779
+ scope: "user",
10780
+ configured: row.userEntry
10781
+ });
10394
10782
  console.log(` ${row.agent.padEnd(12)} project: ${project} user: ${user}`);
10395
10783
  }
10784
+ if (drift.length > 0) {
10785
+ 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}.`));
10786
+ console.log(picocolors.default.gray(" Run `ags sync` (or `ags mcp install`) to refresh; existing agents pick it up on their next MCP start."));
10787
+ }
10396
10788
  if (!mcp.enabled && installedAnywhere) {
10397
10789
  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
10790
  console.log(picocolors.default.gray(" The MCP will work, but `sync` will not keep its config in step. Consider one of:"));
@@ -10403,12 +10795,36 @@ var McpCommand = class {
10403
10795
  console.log(picocolors.default.gray(" Run `ags mcp install` to write the entries, or `ags mcp disable` to clear the consent flag."));
10404
10796
  }
10405
10797
  }
10406
- async actionEnable(config) {
10798
+ /**
10799
+ * Per-file consent for user-home MCP configs. Shared by every install path
10800
+ * so `enable` (including global mode) cannot drift from `actionInstall`.
10801
+ */
10802
+ async promptUserScopeWrite(agent, file) {
10803
+ const { ok } = await inquirer.default.prompt([{
10804
+ type: "confirm",
10805
+ name: "ok",
10806
+ message: `Write user-scope MCP config for ${picocolors.default.cyan(agent)} at ${picocolors.default.gray(file)}?`,
10807
+ default: false
10808
+ }]);
10809
+ return ok;
10810
+ }
10811
+ async actionEnable(config, agents, cwd, options) {
10407
10812
  const mcp = config.mcp ?? defaultMcpConfig();
10813
+ const viaOverrideRaw = options.via;
10814
+ const viaOverride = viaOverrideRaw ? parseVia(viaOverrideRaw) : void 0;
10815
+ if (viaOverrideRaw && !viaOverride) {
10816
+ console.log(picocolors.default.red(`Invalid --via value. Valid: ${VALID_VIAS.join(" | ")}`));
10817
+ return;
10818
+ }
10408
10819
  if (mcp.scope === "disabled") {
10409
10820
  console.log(picocolors.default.yellow("⚠️ scope is currently \"disabled\". Set a real scope first: `ags mcp scope project`"));
10410
10821
  return;
10411
10822
  }
10823
+ const effectiveVia = viaOverride ?? mcp.via ?? "auto";
10824
+ if (Boolean(options.installGlobal) || effectiveVia === "global") {
10825
+ const pin = mcp.version ?? "0.8.1";
10826
+ if (!this.ensureGlobalPin(pin)) return;
10827
+ }
10412
10828
  config.mcp = {
10413
10829
  ...mcp,
10414
10830
  enabled: true,
@@ -10416,8 +10832,39 @@ var McpCommand = class {
10416
10832
  };
10417
10833
  await this.configService.saveConfig(config);
10418
10834
  console.log(picocolors.default.green("✅ MCP enabled."));
10835
+ if (viaOverride || effectiveVia === "global") {
10836
+ const installVia = viaOverride ?? "global";
10837
+ const report = await this.mcpService.install({
10838
+ rootDir: cwd,
10839
+ agents,
10840
+ mcp: {
10841
+ ...config.mcp,
10842
+ via: installVia
10843
+ },
10844
+ userScopePrompt: (agent, file) => this.promptUserScopeWrite(agent, file)
10845
+ });
10846
+ console.log(picocolors.default.gray(` Installed configs using launch mode "${installVia}".`));
10847
+ this.printReport(report, cwd);
10848
+ return;
10849
+ }
10419
10850
  console.log(picocolors.default.gray(" Run `ags mcp install` to write configs now, or it happens on next `sync`."));
10420
10851
  }
10852
+ async actionVia(config, options) {
10853
+ const via = options.via ?? options._?.[0];
10854
+ if (!parseVia(via)) {
10855
+ console.log(picocolors.default.red(`Invalid via. Valid: ${VALID_VIAS.join(" | ")}`));
10856
+ return;
10857
+ }
10858
+ config.mcp = {
10859
+ ...config.mcp ?? defaultMcpConfig(),
10860
+ via,
10861
+ prompted: true
10862
+ };
10863
+ await this.configService.saveConfig(config);
10864
+ console.log(picocolors.default.green(`✅ MCP launch mode set to "${via}".`));
10865
+ if (via === "global") console.log(picocolors.default.yellow("⚠️ \"global\" requires a matching global install: `ags mcp enable` installs and verifies it automatically."));
10866
+ else if (via === "auto") console.log(picocolors.default.gray(" \"auto\" uses a version-matched global ags-mcp when present, otherwise npx."));
10867
+ }
10421
10868
  async actionDisable(config) {
10422
10869
  config.mcp = {
10423
10870
  ...config.mcp ?? defaultMcpConfig(),
@@ -10451,28 +10898,26 @@ var McpCommand = class {
10451
10898
  console.log(picocolors.default.red(`Invalid --scope value. Valid: ${VALID_SCOPES.join(" | ")}`));
10452
10899
  return;
10453
10900
  }
10901
+ const viaOverrideRaw = options.via;
10902
+ const viaOverride = viaOverrideRaw ? parseVia(viaOverrideRaw) : void 0;
10903
+ if (viaOverrideRaw && !viaOverride) {
10904
+ console.log(picocolors.default.red(`Invalid --via value. Valid: ${VALID_VIAS.join(" | ")}`));
10905
+ return;
10906
+ }
10454
10907
  if (effectiveScope === "disabled") {
10455
10908
  console.log(picocolors.default.yellow("Scope is \"disabled\" — nothing to install."));
10456
10909
  return;
10457
10910
  }
10458
- const userPrompt = async (agent, file) => {
10459
- const { ok } = await inquirer.default.prompt([{
10460
- type: "confirm",
10461
- name: "ok",
10462
- message: `Write user-scope MCP config for ${picocolors.default.cyan(agent)} at ${picocolors.default.gray(file)}?`,
10463
- default: false
10464
- }]);
10465
- return ok;
10466
- };
10467
10911
  const report = await this.mcpService.install({
10468
10912
  rootDir: cwd,
10469
10913
  agents,
10470
10914
  mcp: {
10471
10915
  ...mcp,
10472
10916
  enabled: true,
10473
- scope: effectiveScope
10917
+ scope: effectiveScope,
10918
+ ...viaOverride ? { via: viaOverride } : {}
10474
10919
  },
10475
- userScopePrompt: userPrompt
10920
+ userScopePrompt: (agent, file) => this.promptUserScopeWrite(agent, file)
10476
10921
  });
10477
10922
  this.printReport(report, cwd);
10478
10923
  }
@@ -10498,15 +10943,22 @@ var McpCommand = class {
10498
10943
  console.log(picocolors.default.green(`✅ Removed agent-skills MCP entry from:`));
10499
10944
  for (const r of removed) console.log(` - ${picocolors.default.cyan(r.agent)} → ${picocolors.default.gray(r.file)}`);
10500
10945
  }
10501
- async actionSnippets(config, agents, cwd) {
10946
+ async actionSnippets(config, agents, cwd, options) {
10502
10947
  const mcp = config.mcp ?? defaultMcpConfig();
10948
+ const viaOverrideRaw = options.via;
10949
+ const viaOverride = viaOverrideRaw ? parseVia(viaOverrideRaw) : void 0;
10950
+ if (viaOverrideRaw && !viaOverride) {
10951
+ console.log(picocolors.default.red(`Invalid --via value. Valid: ${VALID_VIAS.join(" | ")}`));
10952
+ return;
10953
+ }
10503
10954
  const report = await this.mcpService.install({
10504
10955
  rootDir: cwd,
10505
10956
  agents,
10506
10957
  mcp: {
10507
10958
  ...mcp,
10508
10959
  enabled: true,
10509
- scope: "snippets-only"
10960
+ scope: "snippets-only",
10961
+ ...viaOverride ? { via: viaOverride } : {}
10510
10962
  }
10511
10963
  });
10512
10964
  if (report.snippets.length === 0) {
@@ -10605,12 +11057,14 @@ var RetrospectiveCommand = class {
10605
11057
  env;
10606
11058
  customExit;
10607
11059
  log;
11060
+ mcpVersion;
10608
11061
  constructor(deps = {}) {
10609
11062
  this.spawnFn = deps.spawnFn ?? defaultSpawn;
10610
11063
  this.platform = deps.platform ?? process.platform;
10611
11064
  this.env = deps.env ?? process.env;
10612
11065
  this.customExit = deps.exit;
10613
11066
  this.log = deps.log ?? console.log;
11067
+ this.mcpVersion = deps.mcpVersion ?? "0.8.1";
10614
11068
  }
10615
11069
  exit(code) {
10616
11070
  process.exitCode = code;
@@ -10639,8 +11093,8 @@ var RetrospectiveCommand = class {
10639
11093
  }
10640
11094
  }
10641
11095
  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.`);
11096
+ if (!(typeof envSpec === "string" && envSpec.trim() !== "") && !isVersionAtLeast(this.mcpVersion, "0.8.0")) {
11097
+ 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
11098
  this.exit(1);
10645
11099
  return;
10646
11100
  }
@@ -10659,7 +11113,8 @@ var RetrospectiveCommand = class {
10659
11113
  }
10660
11114
  const argsToSpawn = [
10661
11115
  "-y",
10662
- typeof envSpec === "string" && envSpec.trim() !== "" ? envSpec : `${PACKAGE}@${MCP_COMPATIBLE_VERSION}`,
11116
+ "--prefer-offline",
11117
+ typeof envSpec === "string" && envSpec.trim() !== "" ? envSpec : `${PACKAGE}@${this.mcpVersion}`,
10663
11118
  "retrospective",
10664
11119
  mappedAction,
10665
11120
  ...forwarded
@@ -11338,7 +11793,11 @@ var AuditCommand = class {
11338
11793
  * It checks the latest version on npm and performs the upgrade using the detected package manager.
11339
11794
  */
11340
11795
  var UpgradeCommand = class {
11796
+ deps;
11341
11797
  currentVersion = package_default.version;
11798
+ constructor(deps = {}) {
11799
+ this.deps = deps;
11800
+ }
11342
11801
  /**
11343
11802
  * Executes the upgrade flow.
11344
11803
  * @param options Command options, including `dryRun` to check for updates without installing.
@@ -11383,6 +11842,7 @@ var UpgradeCommand = class {
11383
11842
  const fallbackCmd = this.getUpgradeCommand(pm, "latest");
11384
11843
  (0, child_process.execSync)(fallbackCmd, { stdio: "inherit" });
11385
11844
  }
11845
+ await this.printMcpRefreshHint();
11386
11846
  if (pm === "pnpm") {
11387
11847
  const installedVersion = this.getInstalledVersionFromPnpm(latestVersion);
11388
11848
  if (installedVersion !== latestVersion) {
@@ -11434,6 +11894,12 @@ var UpgradeCommand = class {
11434
11894
  this.printManualInstructions(pm, latestVersion);
11435
11895
  }
11436
11896
  }
11897
+ async printMcpRefreshHint() {
11898
+ try {
11899
+ if (!(this.deps.loadMcpEnabled ? await this.deps.loadMcpEnabled() : (await new ConfigService().loadConfig())?.mcp?.enabled ?? false)) return;
11900
+ 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}.`));
11901
+ } catch {}
11902
+ }
11437
11903
  detectPackageManager() {
11438
11904
  const execPath = process.argv[1] || "";
11439
11905
  if (execPath.includes("pnpm") || (process.env.npm_config_user_agent || "").includes("pnpm")) return "pnpm";
@@ -13410,9 +13876,12 @@ program.command("feedback").description("Report skill improvement opportunities
13410
13876
  program.command("upgrade").description("Upgrade the CLI to the latest version").option("--dry-run", "Check for updates without installing").action(async (options) => {
13411
13877
  await new UpgradeCommand().run(options);
13412
13878
  });
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) => {
13879
+ 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 (automatic when launch mode is global)").option("--from <from>", "For uninstall: project | user | all (default: project)").action(async (action, positional, opts) => {
13414
13880
  const merged = { ...opts };
13415
- if (positional) merged.scope = opts.scope ?? positional;
13881
+ if (positional) {
13882
+ if (action === "via") merged.via = opts.via ?? positional;
13883
+ else merged.scope = opts.scope ?? positional;
13884
+ }
13416
13885
  await new McpCommand().run(action, merged);
13417
13886
  });
13418
13887
  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 = {}) => {