agentsmesh 0.14.0 → 0.16.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/engine.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { V as ValidatedConfig, b as CanonicalFiles } from './schema-o4oXUVBP.js';
2
- import { G as GenerateResult, h as TargetLayoutScope, L as LintDiagnostic, d as ImportResult } from './target-descriptor--Nw5i4v3.js';
1
+ import { V as ValidatedConfig, b as CanonicalFiles } from './schema-CD2qcmDL.js';
2
+ import { G as GenerateResult, h as TargetLayoutScope, L as LintDiagnostic, d as ImportResult } from './target-descriptor-CvB1qzPn.js';
3
3
  import 'zod';
4
4
 
5
5
  /**
package/dist/engine.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { z } from 'zod';
2
2
  import { stringify, parse } from 'yaml';
3
- import { readFile, rm, mkdir, readdir, stat, lstat, unlink, writeFile, rename, access, realpath } from 'fs/promises';
4
3
  import { join, basename, dirname, relative, win32, posix, resolve, extname } from 'path';
5
- import { constants, existsSync, realpathSync, statSync } from 'fs';
4
+ import { readFile, rm, mkdir, readdir, stat, lstat, unlink, writeFile, rename, chmod, access, realpath } from 'fs/promises';
5
+ import { constants, existsSync, realpathSync, statSync, readFileSync } from 'fs';
6
6
  import { parse as parse$1 } from 'smol-toml';
7
7
  import { Buffer } from 'buffer';
8
8
  import { homedir } from 'os';
@@ -619,6 +619,104 @@ function shouldNormalizeLineEndings(path) {
619
619
  function normalizeLineEndings(content) {
620
620
  return content.replace(/\r\n?/g, "\n");
621
621
  }
622
+ function executableModeFor(path) {
623
+ return EXECUTABLE_SCRIPT_EXTENSIONS.has(extname(path).toLowerCase()) ? 493 : void 0;
624
+ }
625
+ var UTF8_BOM, TEXT_EXTENSIONS, TEXT_DOTFILES, EXECUTABLE_SCRIPT_EXTENSIONS;
626
+ var init_fs_text_encoding = __esm({
627
+ "src/utils/filesystem/fs-text-encoding.ts"() {
628
+ UTF8_BOM = "\uFEFF";
629
+ TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
630
+ ".md",
631
+ ".mdc",
632
+ ".mdx",
633
+ ".markdown",
634
+ ".txt",
635
+ ".json",
636
+ ".jsonc",
637
+ ".yaml",
638
+ ".yml",
639
+ ".toml",
640
+ ".ini",
641
+ ".sh",
642
+ ".bash",
643
+ ".zsh",
644
+ ".ps1",
645
+ ".js",
646
+ ".mjs",
647
+ ".cjs",
648
+ ".ts",
649
+ ".tsx",
650
+ ".html",
651
+ ".css"
652
+ ]);
653
+ TEXT_DOTFILES = /* @__PURE__ */ new Set([
654
+ ".gitignore",
655
+ ".cursorignore",
656
+ ".cursorindexingignore",
657
+ ".aiignore",
658
+ ".agentignore",
659
+ ".clineignore",
660
+ ".geminiignore",
661
+ ".codeiumignore",
662
+ ".continueignore",
663
+ ".copilotignore",
664
+ ".windsurfignore",
665
+ ".junieignore",
666
+ ".kiroignore",
667
+ ".rooignore",
668
+ ".antigravityignore"
669
+ ]);
670
+ EXECUTABLE_SCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([".sh", ".bash", ".zsh"]);
671
+ }
672
+ });
673
+ async function readDirRecursive(dir, visited) {
674
+ let canonicalDir;
675
+ try {
676
+ canonicalDir = await realpath(dir);
677
+ } catch (err) {
678
+ const e = err;
679
+ if (e.code === "ENOENT" || e.code === "ENOTDIR" || e.code === "ELOOP") return [];
680
+ throw new FileSystemError(
681
+ dir,
682
+ `Failed to read directory ${dir}: ${e.message}. Check permissions.`,
683
+ { cause: err, errnoCode: e.code }
684
+ );
685
+ }
686
+ const seen = visited ?? /* @__PURE__ */ new Set();
687
+ if (seen.has(canonicalDir)) return [];
688
+ seen.add(canonicalDir);
689
+ try {
690
+ const entries = await readdir(dir, { withFileTypes: true });
691
+ const files = [];
692
+ for (const ent of entries) {
693
+ const full = join(dir, ent.name);
694
+ const walkChild = ent.isDirectory() || ent.isSymbolicLink() && await stat(full).then(
695
+ (s) => s.isDirectory(),
696
+ () => false
697
+ );
698
+ if (walkChild) {
699
+ files.push(...await readDirRecursive(full, seen));
700
+ } else {
701
+ files.push(full);
702
+ }
703
+ }
704
+ return files;
705
+ } catch (err) {
706
+ const e = err;
707
+ if (e.code === "ENOENT" || e.code === "ENOTDIR" || e.code === "EACCES") return [];
708
+ throw new FileSystemError(
709
+ dir,
710
+ `Failed to read directory ${dir}: ${e.message}. Check permissions.`,
711
+ { cause: err, errnoCode: e.code }
712
+ );
713
+ }
714
+ }
715
+ var init_fs_traverse = __esm({
716
+ "src/utils/filesystem/fs-traverse.ts"() {
717
+ init_errors();
718
+ }
719
+ });
622
720
  async function readFileSafe(path) {
623
721
  try {
624
722
  const data = await readFile(path, "utf-8");
@@ -633,7 +731,7 @@ async function readFileSafe(path) {
633
731
  );
634
732
  }
635
733
  }
636
- async function writeFileAtomic(path, content) {
734
+ async function writeFileAtomic(path, content, options) {
637
735
  const dir = dirname(path);
638
736
  await mkdir(dir, { recursive: true });
639
737
  try {
@@ -657,6 +755,7 @@ async function writeFileAtomic(path, content) {
657
755
  }
658
756
  const tmpPath = `${path}.tmp`;
659
757
  const payload = shouldNormalizeLineEndings(path) ? normalizeLineEndings(content) : content;
758
+ const mode = executableModeFor(path);
660
759
  try {
661
760
  try {
662
761
  const tmpInfo = await lstat(tmpPath);
@@ -666,8 +765,16 @@ async function writeFileAtomic(path, content) {
666
765
  } catch (tmpErr) {
667
766
  if (tmpErr.code !== "ENOENT") throw tmpErr;
668
767
  }
669
- await writeFile(tmpPath, payload, { encoding: "utf-8", flag: "w" });
768
+ const writeOpts = {
769
+ encoding: "utf-8",
770
+ flag: "w"
771
+ };
772
+ if (mode !== void 0) writeOpts.mode = mode;
773
+ await writeFile(tmpPath, payload, writeOpts);
670
774
  await rename(tmpPath, path);
775
+ if (mode !== void 0) {
776
+ await chmod(path, mode);
777
+ }
671
778
  } catch (err) {
672
779
  await rm(tmpPath, { force: true }).catch(() => {
673
780
  });
@@ -690,94 +797,12 @@ async function exists(path) {
690
797
  async function mkdirp(path) {
691
798
  await mkdir(path, { recursive: true });
692
799
  }
693
- async function readDirRecursive(dir, visited) {
694
- let canonicalDir;
695
- try {
696
- canonicalDir = await realpath(dir);
697
- } catch (err) {
698
- const e = err;
699
- if (e.code === "ENOENT" || e.code === "ENOTDIR" || e.code === "ELOOP") return [];
700
- throw new FileSystemError(
701
- dir,
702
- `Failed to read directory ${dir}: ${e.message}. Check permissions.`,
703
- { cause: err, errnoCode: e.code }
704
- );
705
- }
706
- const seen = visited ?? /* @__PURE__ */ new Set();
707
- if (seen.has(canonicalDir)) return [];
708
- seen.add(canonicalDir);
709
- try {
710
- const entries = await readdir(dir, { withFileTypes: true });
711
- const files = [];
712
- for (const ent of entries) {
713
- const full = join(dir, ent.name);
714
- const walkChild = ent.isDirectory() || ent.isSymbolicLink() && await stat(full).then(
715
- (s) => s.isDirectory(),
716
- () => false
717
- );
718
- if (walkChild) {
719
- files.push(...await readDirRecursive(full, seen));
720
- } else {
721
- files.push(full);
722
- }
723
- }
724
- return files;
725
- } catch (err) {
726
- const e = err;
727
- if (e.code === "ENOENT" || e.code === "ENOTDIR" || e.code === "EACCES") return [];
728
- throw new FileSystemError(
729
- dir,
730
- `Failed to read directory ${dir}: ${e.message}. Check permissions.`,
731
- { cause: err, errnoCode: e.code }
732
- );
733
- }
734
- }
735
- var UTF8_BOM, TEXT_EXTENSIONS, TEXT_DOTFILES;
736
800
  var init_fs = __esm({
737
801
  "src/utils/filesystem/fs.ts"() {
738
802
  init_errors();
739
- UTF8_BOM = "\uFEFF";
740
- TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
741
- ".md",
742
- ".mdc",
743
- ".mdx",
744
- ".markdown",
745
- ".txt",
746
- ".json",
747
- ".jsonc",
748
- ".yaml",
749
- ".yml",
750
- ".toml",
751
- ".ini",
752
- ".sh",
753
- ".bash",
754
- ".zsh",
755
- ".ps1",
756
- ".js",
757
- ".mjs",
758
- ".cjs",
759
- ".ts",
760
- ".tsx",
761
- ".html",
762
- ".css"
763
- ]);
764
- TEXT_DOTFILES = /* @__PURE__ */ new Set([
765
- ".gitignore",
766
- ".cursorignore",
767
- ".cursorindexingignore",
768
- ".aiignore",
769
- ".agentignore",
770
- ".clineignore",
771
- ".geminiignore",
772
- ".codeiumignore",
773
- ".continueignore",
774
- ".copilotignore",
775
- ".windsurfignore",
776
- ".junieignore",
777
- ".kiroignore",
778
- ".rooignore",
779
- ".antigravityignore"
780
- ]);
803
+ init_fs_text_encoding();
804
+ init_fs_traverse();
805
+ init_fs_text_encoding();
781
806
  }
782
807
  });
783
808
  function escapeRegExp(value) {
@@ -5173,13 +5198,16 @@ function generateAgents4(canonical) {
5173
5198
  function safeEventName(event) {
5174
5199
  return event.replace(/[^a-zA-Z0-9]/g, "-").toLowerCase();
5175
5200
  }
5201
+ function safeShellLine(value) {
5202
+ return value.replace(/[\r\n]+/g, " ");
5203
+ }
5176
5204
  function buildHookScript(event, command, matcher) {
5177
5205
  return [
5178
5206
  "#!/usr/bin/env bash",
5179
- `# agentsmesh-event: ${event}`,
5180
- `# agentsmesh-matcher: ${matcher}`,
5181
- `# agentsmesh-command: ${command}`,
5182
- "set -e",
5207
+ `# agentsmesh-event: ${safeShellLine(event)}`,
5208
+ `# agentsmesh-matcher: ${safeShellLine(matcher)}`,
5209
+ `# agentsmesh-command: ${safeShellLine(command)}`,
5210
+ "set -eu",
5183
5211
  command,
5184
5212
  ""
5185
5213
  ].join("\n");
@@ -7636,7 +7664,7 @@ function extractMatcher(comment) {
7636
7664
  function extractWrapperCommand(content) {
7637
7665
  const metadataMatch = content.match(/^# agentsmesh-command:\s*(.+)$/m);
7638
7666
  if (metadataMatch?.[1]) return metadataMatch[1].trim();
7639
- return content.replace(/^#!.*\n/, "").replace(/^#.*\n/gm, "").replace(/^HOOK_DIR=.*\n/gm, "").replace(/^set -e\n?/m, "").trim();
7667
+ return content.replace(/^#!.*\n/, "").replace(/^#.*\n/gm, "").replace(/^HOOK_DIR=.*\n/gm, "").replace(/^set -e[u]?\n?/m, "").trim();
7640
7668
  }
7641
7669
  async function importHooks(projectRoot, results) {
7642
7670
  const hooksDir = join(projectRoot, COPILOT_HOOKS_DIR);
@@ -7958,12 +7986,15 @@ async function buildAssetOutput(projectRoot, command) {
7958
7986
  function wrapperPath(event, index) {
7959
7987
  return `${COPILOT_HOOKS_DIR}/scripts/${safePhaseName(event)}-${index}.sh`;
7960
7988
  }
7989
+ function safeShellLine2(value) {
7990
+ return value.replace(/[\r\n]+/g, " ");
7991
+ }
7961
7992
  function buildWrapper(command, matcher) {
7962
7993
  return [
7963
7994
  "#!/usr/bin/env bash",
7964
- `# agentsmesh-matcher: ${matcher}`,
7965
- `# agentsmesh-command: ${command}`,
7966
- "set -e",
7995
+ `# agentsmesh-matcher: ${safeShellLine2(matcher)}`,
7996
+ `# agentsmesh-command: ${safeShellLine2(command)}`,
7997
+ "set -eu",
7967
7998
  command,
7968
7999
  ""
7969
8000
  ].join("\n");
@@ -7987,8 +8018,8 @@ async function addHookScriptAssets(projectRoot, canonical, outputs) {
7987
8018
  }
7988
8019
  }
7989
8020
  const wrapper = buildWrapper(command, entry.matcher).replace(
7990
- "set -e\n",
7991
- 'set -e\nHOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"\n'
8021
+ "set -eu\n",
8022
+ 'set -eu\nHOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"\n'
7992
8023
  );
7993
8024
  wrapperOutputs.push({ path: scriptPath, content: wrapper });
7994
8025
  index++;
@@ -15590,7 +15621,15 @@ var conversionsSchema = z.object({
15590
15621
  var pluginEntrySchema = z.object({
15591
15622
  id: z.string().regex(/^[a-z][a-z0-9-]*$/),
15592
15623
  source: z.string(),
15593
- version: z.string().optional()
15624
+ version: z.string().optional(),
15625
+ /**
15626
+ * When true, a failure to import or validate this plugin throws and
15627
+ * aborts the run. Default `false` keeps the lenient behavior of logging
15628
+ * a warning and continuing. Use strict mode in CI when missing
15629
+ * descriptors should fail the build instead of silently shrinking the
15630
+ * generation matrix.
15631
+ */
15632
+ strict: z.boolean().optional()
15594
15633
  }).strict();
15595
15634
  var configSchema = z.object({
15596
15635
  version: z.literal(1),
@@ -15758,6 +15797,13 @@ init_fs();
15758
15797
  init_fs();
15759
15798
  var execFileAsync = promisify(execFile);
15760
15799
  var REPO_DIRNAME = "repo";
15800
+ function ensureNotFlag(value, kind) {
15801
+ if (value.startsWith("-")) {
15802
+ throw new Error(
15803
+ `agentsmesh refuses ${kind} starting with "-" (option-injection guard): ${value}`
15804
+ );
15805
+ }
15806
+ }
15761
15807
  async function fetchGitRemoteExtend(parsed, extendName, options, cacheDir, buildCacheKey2) {
15762
15808
  const provider = "cloneUrl" in parsed ? "gitlab" : "git";
15763
15809
  const identifier = "cloneUrl" in parsed ? `${parsed.namespace}/${parsed.project}` : parsed.url;
@@ -15815,9 +15861,11 @@ function resolveCloneUrl(parsed) {
15815
15861
  return parsed.url;
15816
15862
  }
15817
15863
  async function cloneRepo(cloneUrl, repoDir) {
15864
+ ensureNotFlag(cloneUrl, "clone-url");
15818
15865
  await runGit(["clone", cloneUrl, repoDir]);
15819
15866
  }
15820
15867
  async function checkoutRef(repoDir, ref) {
15868
+ ensureNotFlag(ref, "ref");
15821
15869
  await runGit(["checkout", ref], repoDir);
15822
15870
  }
15823
15871
  async function getHeadSha(repoDir) {
@@ -15836,6 +15884,46 @@ async function runGit(args, cwd) {
15836
15884
 
15837
15885
  // src/config/remote/github-remote.ts
15838
15886
  init_fs();
15887
+ var MAX_TARBALL_BYTES = 500 * 1024 * 1024;
15888
+ async function readBoundedResponse(res, maxBytes) {
15889
+ const lenHeader = typeof res.headers?.get === "function" ? res.headers.get("content-length") : null;
15890
+ if (lenHeader !== null) {
15891
+ const declared = Number(lenHeader);
15892
+ if (Number.isFinite(declared) && declared > maxBytes) {
15893
+ throw new Error(`remote response declared ${declared} bytes; exceeds cap of ${maxBytes}`);
15894
+ }
15895
+ }
15896
+ const stream = res.body;
15897
+ if (!stream) {
15898
+ const buf = await res.arrayBuffer();
15899
+ if (buf.byteLength > maxBytes) {
15900
+ throw new Error(`remote response is ${buf.byteLength} bytes; exceeds cap of ${maxBytes}`);
15901
+ }
15902
+ return new Uint8Array(buf);
15903
+ }
15904
+ const reader = stream.getReader();
15905
+ const chunks = [];
15906
+ let total = 0;
15907
+ for (; ; ) {
15908
+ const { done, value } = await reader.read();
15909
+ if (done) break;
15910
+ if (!value) continue;
15911
+ total += value.byteLength;
15912
+ if (total > maxBytes) {
15913
+ await reader.cancel().catch(() => {
15914
+ });
15915
+ throw new Error(`remote response exceeded cap of ${maxBytes} bytes during streaming`);
15916
+ }
15917
+ chunks.push(value);
15918
+ }
15919
+ const out2 = new Uint8Array(total);
15920
+ let offset = 0;
15921
+ for (const c2 of chunks) {
15922
+ out2.set(c2, offset);
15923
+ offset += c2.byteLength;
15924
+ }
15925
+ return out2;
15926
+ }
15839
15927
  async function resolveLatestTag(org, repo, token) {
15840
15928
  const url = `https://api.github.com/repos/${org}/${repo}/releases/latest`;
15841
15929
  const headers = {
@@ -15875,11 +15963,11 @@ async function fetchGithubRemoteExtend(parsed, extendName, options, cacheDir, bu
15875
15963
  const tarballUrl = `https://github.com/${parsed.org}/${parsed.repo}/tarball/${tag}`;
15876
15964
  const headers = {};
15877
15965
  if (token) headers.Authorization = `Bearer ${token}`;
15878
- let tarballBuffer;
15966
+ let tarballBytes;
15879
15967
  try {
15880
15968
  const res = await globalThis.fetch(tarballUrl, { headers, redirect: "follow" });
15881
15969
  if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
15882
- tarballBuffer = await res.arrayBuffer();
15970
+ tarballBytes = await readBoundedResponse(res, MAX_TARBALL_BYTES);
15883
15971
  } catch (err) {
15884
15972
  const allowFallback = options.allowOfflineFallback !== false;
15885
15973
  if (allowFallback && await exists(extractDir)) {
@@ -15896,7 +15984,7 @@ async function fetchGithubRemoteExtend(parsed, extendName, options, cacheDir, bu
15896
15984
  await rm(extractDir, { recursive: true, force: true });
15897
15985
  await mkdir(extractDir, { recursive: true });
15898
15986
  const tarPath = join(extractDir, "archive.tar.gz");
15899
- await writeFile(tarPath, new Uint8Array(tarballBuffer));
15987
+ await writeFile(tarPath, tarballBytes);
15900
15988
  try {
15901
15989
  await tar.extract({
15902
15990
  file: tarPath,
@@ -16068,7 +16156,20 @@ function buildCacheKey(provider, identifier, ref) {
16068
16156
  }
16069
16157
  function getCacheDir() {
16070
16158
  const env = process.env.AGENTSMESH_CACHE;
16071
- if (env) return env;
16159
+ if (env) {
16160
+ const trimmed = env.trim();
16161
+ if (!trimmed) {
16162
+ return join(homedir(), ".agentsmesh", "cache");
16163
+ }
16164
+ const isAbs = /^([A-Za-z]:[\\/]|\/)/.test(trimmed);
16165
+ if (!isAbs) {
16166
+ throw new Error(`AGENTSMESH_CACHE must be an absolute path (got: "${trimmed}").`);
16167
+ }
16168
+ if (trimmed === "/" || /^[A-Za-z]:[\\/]?$/.test(trimmed)) {
16169
+ throw new Error(`AGENTSMESH_CACHE must not be the filesystem root (got: "${trimmed}").`);
16170
+ }
16171
+ return trimmed;
16172
+ }
16072
16173
  return join(homedir(), ".agentsmesh", "cache");
16073
16174
  }
16074
16175
  async function fetchRemoteExtend(source, extendName, options = {}) {
@@ -16160,6 +16261,98 @@ async function resolveExtendPaths(config, configDir, options = {}) {
16160
16261
  // src/canonical/features/rules.ts
16161
16262
  init_fs();
16162
16263
  init_markdown();
16264
+
16265
+ // src/utils/filesystem/windows-path-safety.ts
16266
+ var WINDOWS_RESERVED_NAMES = /* @__PURE__ */ new Set([
16267
+ "CON",
16268
+ "PRN",
16269
+ "AUX",
16270
+ "NUL",
16271
+ "COM1",
16272
+ "COM2",
16273
+ "COM3",
16274
+ "COM4",
16275
+ "COM5",
16276
+ "COM6",
16277
+ "COM7",
16278
+ "COM8",
16279
+ "COM9",
16280
+ "LPT1",
16281
+ "LPT2",
16282
+ "LPT3",
16283
+ "LPT4",
16284
+ "LPT5",
16285
+ "LPT6",
16286
+ "LPT7",
16287
+ "LPT8",
16288
+ "LPT9"
16289
+ ]);
16290
+ var WINDOWS_ILLEGAL_CHARS = new RegExp('[<>:"|?*\\u0000-\\u001F]');
16291
+ function segmentReservedName(segment) {
16292
+ const stem = segment.replace(/\.[^.]*$/, "").toUpperCase();
16293
+ return WINDOWS_RESERVED_NAMES.has(stem);
16294
+ }
16295
+ function findWindowsPathIssues(path) {
16296
+ const issues = [];
16297
+ const segments = path.split(/[\\/]/);
16298
+ for (const segment of segments) {
16299
+ if (segment === "" || segment === "." || segment === "..") continue;
16300
+ if (WINDOWS_ILLEGAL_CHARS.test(segment)) {
16301
+ issues.push({ segment, reason: "illegal-character" });
16302
+ continue;
16303
+ }
16304
+ if (/[. ]$/.test(segment)) {
16305
+ issues.push({ segment, reason: "trailing-dot-or-space" });
16306
+ continue;
16307
+ }
16308
+ if (segmentReservedName(segment)) {
16309
+ issues.push({ segment, reason: "reserved-name" });
16310
+ }
16311
+ }
16312
+ return issues;
16313
+ }
16314
+
16315
+ // src/canonical/features/validate-name.ts
16316
+ var CanonicalNameError = class extends Error {
16317
+ feature;
16318
+ name;
16319
+ constructor(feature, name, message) {
16320
+ super(message);
16321
+ this.feature = feature;
16322
+ this.name = name;
16323
+ }
16324
+ };
16325
+ function assertCanonicalName(feature, name) {
16326
+ const issues = findWindowsPathIssues(name);
16327
+ if (issues.length === 0) return;
16328
+ const reasons = issues.map((i) => `${i.segment} (${i.reason})`).join(", ");
16329
+ throw new CanonicalNameError(
16330
+ feature,
16331
+ name,
16332
+ `canonical ${feature} name "${name}" is not portable to Windows: ${reasons}. Rename the file.`
16333
+ );
16334
+ }
16335
+ function assertNoBasenameCollisions(feature, paths, stripExt) {
16336
+ const seen = /* @__PURE__ */ new Map();
16337
+ for (const p of paths) {
16338
+ const fwdIdx = p.lastIndexOf("/");
16339
+ const bckIdx = p.lastIndexOf("\\");
16340
+ const idx = Math.max(fwdIdx, bckIdx);
16341
+ const base = idx === -1 ? p : p.slice(idx + 1);
16342
+ const slug = base.endsWith(stripExt) ? base.slice(0, -stripExt.length) : base;
16343
+ const prior = seen.get(slug);
16344
+ if (prior !== void 0 && prior !== p) {
16345
+ throw new CanonicalNameError(
16346
+ feature,
16347
+ slug,
16348
+ `canonical ${feature} files collide on slug "${slug}": ${prior} vs ${p}. Rename one.`
16349
+ );
16350
+ }
16351
+ seen.set(slug, p);
16352
+ }
16353
+ }
16354
+
16355
+ // src/canonical/features/rules.ts
16163
16356
  var VALID_TRIGGERS = ["always_on", "model_decision", "glob", "manual"];
16164
16357
  function toStrArray(v) {
16165
16358
  if (Array.isArray(v)) return v.filter((x) => typeof x === "string");
@@ -16179,6 +16372,7 @@ async function parseRules(rulesDir) {
16179
16372
  if (!content) continue;
16180
16373
  const { frontmatter, body } = parseFrontmatter(content);
16181
16374
  const name = basename(path, ".md");
16375
+ assertCanonicalName("rule", name);
16182
16376
  const rootFromFilename = name === "_root";
16183
16377
  const rootFromFm = frontmatter.root === true;
16184
16378
  const triggerRaw = frontmatter.trigger;
@@ -16220,12 +16414,14 @@ function toToolsArray2(v) {
16220
16414
  async function parseCommands(commandsDir) {
16221
16415
  const files = await readDirRecursive(commandsDir);
16222
16416
  const mdFiles = files.filter((f) => f.endsWith(".md") && !basename(f).startsWith("_"));
16417
+ assertNoBasenameCollisions("command", mdFiles, ".md");
16223
16418
  const commands = [];
16224
16419
  for (const path of mdFiles) {
16225
16420
  const content = await readFileSafe(path);
16226
16421
  if (!content) continue;
16227
16422
  const { frontmatter, body } = parseFrontmatter(content);
16228
16423
  const name = basename(path, ".md");
16424
+ assertCanonicalName("command", name);
16229
16425
  const fromCamel = toToolsArray2(frontmatter.allowedTools);
16230
16426
  const fromKebab = toToolsArray2(frontmatter["allowed-tools"]);
16231
16427
  const allowedTools = fromCamel.length > 0 ? fromCamel : fromKebab;
@@ -16273,12 +16469,14 @@ function toHooks2(v) {
16273
16469
  async function parseAgents(agentsDir) {
16274
16470
  const files = await readDirRecursive(agentsDir);
16275
16471
  const mdFiles = files.filter((f) => f.endsWith(".md") && !basename(f).startsWith("_"));
16472
+ assertNoBasenameCollisions("agent", mdFiles, ".md");
16276
16473
  const agents = [];
16277
16474
  for (const path of mdFiles) {
16278
16475
  const content = await readFileSafe(path);
16279
16476
  if (!content) continue;
16280
16477
  const { frontmatter, body } = parseFrontmatter(content);
16281
16478
  const name = basename(path, ".md");
16479
+ assertCanonicalName("agent", name);
16282
16480
  const toolsCamel = toStrArray2(frontmatter.tools);
16283
16481
  const toolsKebab = toStrArray2(frontmatter["tools"]);
16284
16482
  const tools = toolsCamel.length > 0 ? toolsCamel : toolsKebab;
@@ -16346,9 +16544,11 @@ async function parseSkillDirectory(skillDir) {
16346
16544
  const { frontmatter, body } = parseFrontmatter(content);
16347
16545
  const supportingFiles = await listSupportingFiles(skillDir);
16348
16546
  const fmName = typeof frontmatter.name === "string" ? sanitizeSkillName(frontmatter.name) : "";
16547
+ const name = fmName || basename(skillDir);
16548
+ assertCanonicalName("skill", name);
16349
16549
  return {
16350
16550
  source: skillPath,
16351
- name: fmName || basename(skillDir),
16551
+ name,
16352
16552
  description: typeof frontmatter.description === "string" ? frontmatter.description : "",
16353
16553
  body,
16354
16554
  supportingFiles
@@ -16365,6 +16565,7 @@ async function parseSkills(skillsDir) {
16365
16565
  for (const ent of entries) {
16366
16566
  if (!ent.isDirectory()) continue;
16367
16567
  if (ent.name.startsWith("_")) continue;
16568
+ assertCanonicalName("skill", ent.name);
16368
16569
  const skillDir = join(skillsDir, ent.name);
16369
16570
  const skillPath = join(skillDir, SKILL_FILE);
16370
16571
  const content = await readFileSafe(skillPath);
@@ -17218,15 +17419,35 @@ async function loadCanonicalWithExtends(config, configDir, options = {}, canonic
17218
17419
  // src/plugins/load-plugin.ts
17219
17420
  init_target_descriptor_schema();
17220
17421
  init_registry();
17422
+ function resolveNpmSpecifier(source, projectRoot) {
17423
+ const pkgDir = join(projectRoot, "node_modules", source);
17424
+ const pkgJsonPath = join(pkgDir, "package.json");
17425
+ if (!existsSync(pkgJsonPath)) {
17426
+ throw new Error(`Cannot find package '${source}' in ${join(projectRoot, "node_modules")}`);
17427
+ }
17428
+ const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
17429
+ const entry = (typeof pkgJson.exports === "string" ? pkgJson.exports : null) ?? (typeof pkgJson.main === "string" ? pkgJson.main : null) ?? "index.js";
17430
+ const resolved = resolve(pkgDir, entry);
17431
+ if (!existsSync(resolved)) {
17432
+ throw new Error(`Package '${source}' entry '${entry}' does not exist at ${resolved}`);
17433
+ }
17434
+ return resolved;
17435
+ }
17436
+ function isLocalSource(source) {
17437
+ return source.startsWith("file:") || source.startsWith("./") || source.startsWith("../") || source.startsWith("/") || // Windows absolute paths: `D:\foo`, `C:/bar`, etc. `node:path`'s `resolve()` produces
17438
+ // these on win32, and they must not be misinterpreted as bare npm package names.
17439
+ /^[A-Za-z]:[/\\]/.test(source);
17440
+ }
17221
17441
  async function importPluginModule(entry, projectRoot) {
17222
17442
  const { source } = entry;
17223
17443
  let importTarget;
17224
- if (source.startsWith("file:") || source.startsWith("./") || source.startsWith("../") || source.startsWith("/")) {
17444
+ if (isLocalSource(source)) {
17225
17445
  const raw = source.startsWith("file:") ? fileURLToPath(source) : source;
17226
17446
  const resolved = resolve(projectRoot, raw);
17227
17447
  importTarget = pathToFileURL(resolved).href;
17228
17448
  } else {
17229
- importTarget = source;
17449
+ const resolved = resolveNpmSpecifier(source, projectRoot);
17450
+ importTarget = pathToFileURL(resolved).href;
17230
17451
  }
17231
17452
  const mod = await import(importTarget);
17232
17453
  return mod;
@@ -17265,18 +17486,29 @@ async function loadPlugin(entry, projectRoot) {
17265
17486
  }
17266
17487
  async function loadAllPlugins(entries, projectRoot) {
17267
17488
  const results = [];
17268
- await Promise.all(
17489
+ const envStrict = process.env.AGENTSMESH_STRICT_PLUGINS === "1" || process.env.AGENTSMESH_STRICT_PLUGINS === "true";
17490
+ const settled = await Promise.all(
17269
17491
  entries.map(async (entry) => {
17270
17492
  try {
17271
17493
  const loaded = await loadPlugin(entry, projectRoot);
17272
17494
  results.push(loaded);
17495
+ return null;
17273
17496
  } catch (err) {
17274
- logger.warn(
17275
- `Plugin '${entry.source}' failed to load: ${err instanceof Error ? err.message : String(err)}`
17276
- );
17497
+ const message = `Plugin '${entry.source}' failed to load: ${err instanceof Error ? err.message : String(err)}`;
17498
+ if (entry.strict === true || envStrict) {
17499
+ return new Error(message);
17500
+ }
17501
+ logger.warn(message);
17502
+ return null;
17277
17503
  }
17278
17504
  })
17279
17505
  );
17506
+ const fatal = settled.filter((e) => e !== null);
17507
+ if (fatal.length > 0) {
17508
+ const summary = fatal.map((e) => ` - ${e.message}`).join("\n");
17509
+ throw new Error(`agentsmesh: ${fatal.length} plugin(s) failed strict load:
17510
+ ${summary}`);
17511
+ }
17280
17512
  return results;
17281
17513
  }
17282
17514