agentsmesh 0.36.0 → 0.37.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/CHANGELOG.md CHANGED
@@ -1,5 +1,43 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.37.0
4
+
5
+ ### Minor Changes
6
+
7
+ - f5ab3d1: **Changed — writes are atomic and stay inside the project.** Every generated file is written through an exclusively created temporary file and renamed into place, so a concurrent run or a crash never leaves a half-written file, and a symlink at the destination is replaced rather than followed. `generate` and `convert` reject an output directory that resolves outside the project (or the home directory with `--global`) through a symlink; symlinks that stay inside the boundary keep working. A stale `.generate.lock` left by a dead process is evicted, and one that cannot be removed fails fast with the error instead of retrying forever.
8
+
9
+ **Changed — Claude Code.** An explicitly empty canonical file now clears the entries agentsmesh generated: `allow: []`, `deny: []` and `ask: []` remove permission rules, `hooks.yaml` containing `{}` removes hooks, and `mcp.json` containing `{"mcpServers": {}}` removes servers, while every other key in `.claude/settings.json` and `.mcp.json` stays as it was. A missing canonical file still leaves the tool's settings alone.
10
+
11
+ **Fixed — MCP server and installs.** `add_mcp_server`, `update_mcp_server` and `remove_mcp_server` keep unrelated servers, unknown server fields and top-level keys of `.agentsmesh/mcp.json`, and refuse to overwrite a malformed file. `create_skill` and `update_skill` validate every supporting path and file size before writing anything, and `SKILL.md` can no longer be replaced through `supportingFiles`. Files written by the MCP server get the same line-ending normalization and executable bit as generated files. `install` stages a pack in a private transaction directory, so a sibling `<pack>.tmp` or `<pack>.old` directory is never touched and a failed swap restores the previous pack. `watch` ignores the new temporary files and no longer re-triggers on its own writes.
12
+
13
+ ### Patch Changes
14
+
15
+ - 87486d2: **Fixed — output boundary check runs before any write, and under `--dry-run`.** `generate` and `convert` now check every output path and every managed directory of the active targets against the project (or home) boundary before writing anything. Previously a managed directory that produced no output this run — `.cursor/agents` with agents disabled, say — was first checked during stale cleanup, after every file had been written and before the lock was saved, so a symlink escaping the boundary left a half-generated project with no lock and `check` reporting "not initialized" on every rerun. `--dry-run` now runs the same check, so a preview no longer reports OK for a layout the real run rejects. The error names the path, where it resolves to, and the boundary it escapes.
16
+ - f0668ac: Bumped the `smol-toml` production dependency to 1.8.0, clearing GHSA-7w5x-hrqm-74c2 (high) which affects every version up to 1.7.0. `smol-toml` parses `.codex/config.toml`, so the advisory shipped to anyone installing agentsmesh.
17
+ - ce9647d: **Fixed — a token in an install source is no longer written to files you commit.** Installing from `git+https://user:token@host/org/repo.git` recorded the URL verbatim in `installs.yaml`, `pack.yaml` and the install manifest, and used it to name a directory in the remote cache. The credential is now stripped from everything persisted and from cache keys; only the fetch itself sees it, and the same repository resolves to one cache entry whether or not a token was supplied. Authentication for a later `refresh` comes from git's own credential mechanism (a helper, or SSH) rather than from a secret committed alongside the manifest.
18
+
19
+ **Fixed — `agentsmesh.yaml` keeps its comments.** Adding an extends entry with `install --extends`, or removing one with `uninstall`, rewrote the file by re-serializing a parsed object, which silently discarded every comment and any key this version does not model. Both paths now edit the document in place.
20
+
21
+ **Fixed — `install --name` will not adopt an extends entry you wrote by hand.** A name already used by an entry pointing at a different source is refused with a message naming that source, because `uninstall` removes the row by name — so adopting it meant your entry disappeared along with the pack. Reusing the name of an earlier install of the same source still works.
22
+
23
+ **Changed — more skill file types are carried as bytes.** The binary allowlist added Office and design documents (`.xlsx`, `.docx`, `.pptx`, `.psd`, `.ai`, `.sketch`, `.fig`), uncompressed and modern archives (`.tar`, `.zst`, `.br`, `.lz4`), columnar data and model formats (`.parquet`, `.avro`, `.onnx`, `.safetensors`, `.gguf`), and further media and database extensions. A skill shipping a spreadsheet template is no longer corrupted on generate.
24
+
25
+ - 0ea6be3: **Fixed — `--json` output is complete when you pipe it.** Node writes to a pipe asynchronously, and `process.exit` discards whatever is still queued, so any envelope larger than the pipe buffer reached the consumer as an unparseable fragment: `agentsmesh diff --json | jq` received 64 KiB of a 7 MB document, while the same command redirected to a file was whole. Redirect and pipe now agree at any payload size. Human-readable output on a failing command is covered by the same change.
26
+
27
+ **Fixed — seeding the self-serve MCP entry no longer costs you the rest of `mcp.json`.** `init` and `import` add an `agentsmesh` server entry to `.agentsmesh/mcp.json`; that write went through the canonical parser, which models only the fields AgentsMesh itself uses. Every other top-level key and every per-server field it does not model — `cwd`, `disabled`, `timeout` — was dropped on the way back out, and a file that failed to parse was replaced by a document containing nothing but the new entry. The write now patches the raw document, so unknown keys survive verbatim. A file that cannot be rewritten safely (invalid JSON, or JSON with comments a rewrite would discard) is left untouched with a warning naming the reason, matching how the generated-output mergers already treat a file they cannot parse.
28
+
29
+ - 0b91ded: **Fixed — a rule scoped to one target is no longer dropped when targets share `AGENTS.md`.** Several tools read the same root instruction file, so two rules each scoped with `targets:` to a different one of them produced two different bodies for one path. The resolver kept whichever body was byte-longer and discarded the other silently, so one target's rule simply never appeared. Bodies that differ only inside the embedded-rules block — which is exactly what scoped rules produce — are now combined, keeping every rule and warning that the scoping was widened, because one file cannot differ per target. Where bodies differ outside that block, the run fails with the usual conflict error instead of guessing. The related Codex preference now compares substance rather than byte length: it applies only when one body carries every line of the other.
30
+
31
+ **Fixed — ordinary Markdown no longer fails generation.** Three shapes were read as broken local links and aborted the run: link syntax quoted inside inline code (`` `[label](path/to/file.md)` ``, which is how a rule documents the syntax), GFM footnote definitions (`[^1]: prose`), and — for URLs — a path containing parentheses. A Next.js route group such as `apps/(marketing)/package.json` was also truncated to `apps/(marketing)package.json` when the tail matched a real project file, because the protected span stopped at the first parenthesis. URLs now keep balanced parenthesised segments, and the broken-link check skips footnotes and inline code. A path written inside backticks is still rebased as before; only the check treats it as prose.
32
+
33
+ - 5374565: **Fixed — binary files a skill ships are no longer corrupted.** Supporting files under `.agentsmesh/skills/<name>/` were read as UTF-8 text, so an image, font, archive or PDF had every invalid byte replaced before any generator saw it, and the broken copy was written to every target. Binary payloads now travel as bytes through read, write and checksum, so a skill's diagram or icon arrives intact and `agentsmesh check` no longer reports drift on it the moment it is generated. Text files are unchanged: line-ending normalization and BOM handling still apply to them, and only a fixed list of binary extensions takes the byte path.
34
+
35
+ **Fixed — `agentsmesh init` no longer replaces canonical files.** When `agentsmesh.yaml` was missing but `.agentsmesh/` already held real content — deleted by hand, lost in a merge, or never committed — `init` overwrote `rules/_root.md`, `mcp.json`, `hooks.yaml`, `permissions.yaml` and `ignore` with templates and exited 0. The already-initialized error also told users to remove the config file and re-run, which was exactly the sequence that destroyed their work. The scaffold now only fills gaps, and the message explains that re-running `init` is not a reset.
36
+
37
+ **Fixed — the lessons merge driver no longer discards the other branch's lessons.** A failing driver does not make git re-merge the file with conflict markers: git leaves your side exactly as it was and marks the path unmerged, so `git add` silently threw away every rule the other branch captured. Three ordinary situations hit that path — both branches creating the graph for the first time (git supplies an empty ancestor), the same rule captured under two ids, and any validation error that already existed in the ancestor. The driver now treats an empty ancestor as an empty graph, ignores errors that predate the merge, and writes the union of both branches whenever it can build one, reserving a non-zero exit for "a human should look at this" instead of "throw work away".
38
+
39
+ **Fixed — `agentsmesh install --extends` no longer copies private extends into the shared config.** The entry was merged into the config as loaded, which already included the gitignored `agentsmesh.local.yaml`, so every local-only `extends` entry was written into the committed `agentsmesh.yaml`. The write is now scoped to what the shared file itself declares.
40
+
3
41
  ## 0.36.0
4
42
 
5
43
  ### Minor Changes
package/README.md CHANGED
@@ -142,6 +142,8 @@ Built to be depended on: 12,000+ tests on Linux, macOS, and Windows CI, JSON-Sch
142
142
  - **Lossless two-way sync.** When a tool has no native slot for a feature, it is embedded with round-trip metadata instead of dropped. [Managed embedding](https://samplexbro.github.io/agentsmesh/reference/managed-embedding/)
143
143
  - **Automatic link rebasing.** `.agentsmesh/skills/api-gen/template.hbs` becomes `.claude/skills/...` or `.cursor/skills/...` in each generated file. [Generation pipeline](https://samplexbro.github.io/agentsmesh/reference/generation-pipeline/)
144
144
  - **Safe adoption.** `import`, then `diff`, then `generate`, then `check`; nothing is overwritten blind. [Existing-project guide](https://samplexbro.github.io/agentsmesh/guides/existing-project/)
145
+ - **Output directory safety.** `generate` and `convert` reject output directories that resolve outside the project (or home directory with `--global`), checked before any write and under `--dry-run` too. Stale-file cleanup enforces the same boundary.
146
+ - **Safe updates.** Explicitly empty Claude permission, hook, and MCP configurations clear their generated entries. MCP edits preserve untouched server fields and reject malformed files; skill updates validate supporting files before writing.
145
147
  - **Migrate between tools.** `convert --from <a> --to <b>` rewrites one tool's config directly into another's. [convert](https://samplexbro.github.io/agentsmesh/cli/convert/)
146
148
  - **Global mode.** `~/.agentsmesh/` syncs personal config to `~/.claude/`, `~/.cursor/`, `~/.codex/`, and more; config commands accept `--global`. [Global paths](https://samplexbro.github.io/agentsmesh/reference/supported-tools/#global-mode)
147
149
  - **CI-ready.** `check` gates drift, `diff` previews, `merge` rebuilds the lock after a Git conflict, and `lint` warns about content a tool would mishandle. [check](https://samplexbro.github.io/agentsmesh/cli/check/) · [lint](https://samplexbro.github.io/agentsmesh/cli/lint/)
package/dist/canonical.js CHANGED
@@ -1,13 +1,13 @@
1
1
  import { join, basename, dirname, relative, extname, resolve, posix, win32 } from 'path';
2
- import { access, readdir, readFile, rm, mkdir, lstat, unlink, writeFile, rename, chmod, stat, realpath, mkdtemp, cp } from 'fs/promises';
2
+ import { access, readdir, readFile, rm, mkdir, lstat, open, stat, realpath, rename, writeFile, mkdtemp, cp } from 'fs/promises';
3
3
  import { setTimeout } from 'timers/promises';
4
+ import { randomUUID, createHash } from 'crypto';
4
5
  import { constants, existsSync, readFileSync, realpathSync, statSync } from 'fs';
5
6
  import { parse, stringify, parseDocument, isMap, Document, isSeq, isScalar, Scalar, Pair, YAMLSeq } from 'yaml';
6
7
  import { z } from 'zod';
7
8
  import { parse as parse$1, stringify as stringify$1 } from 'smol-toml';
8
9
  import { Buffer } from 'buffer';
9
10
  import { homedir, tmpdir } from 'os';
10
- import { createHash } from 'crypto';
11
11
  import { execFile } from 'child_process';
12
12
  import { URL as URL$1 } from 'url';
13
13
  import { promisify } from 'util';
@@ -92,13 +92,19 @@ function shouldNormalizeLineEndings(path) {
92
92
  const base = basename(path).toLowerCase();
93
93
  return TEXT_DOTFILES.has(base);
94
94
  }
95
+ function isBinaryPayloadPath(path) {
96
+ return BINARY_EXTENSIONS.has(extname(path).toLowerCase());
97
+ }
98
+ function payloadEncodingFor(path) {
99
+ return isBinaryPayloadPath(path) ? "latin1" : "utf-8";
100
+ }
95
101
  function normalizeLineEndings(content) {
96
102
  return content.replace(/\r\n?/g, "\n");
97
103
  }
98
104
  function executableModeFor(path) {
99
105
  return EXECUTABLE_SCRIPT_EXTENSIONS.has(extname(path).toLowerCase()) ? 493 : void 0;
100
106
  }
101
- var UTF8_BOM, TEXT_EXTENSIONS, TEXT_DOTFILES, EXECUTABLE_SCRIPT_EXTENSIONS;
107
+ var UTF8_BOM, TEXT_EXTENSIONS, TEXT_DOTFILES, BINARY_EXTENSIONS, EXECUTABLE_SCRIPT_EXTENSIONS;
102
108
  var init_fs_text_encoding = __esm({
103
109
  "src/utils/filesystem/fs-text-encoding.ts"() {
104
110
  UTF8_BOM = "\uFEFF";
@@ -143,9 +149,109 @@ var init_fs_text_encoding = __esm({
143
149
  ".rooignore",
144
150
  ".antigravityignore"
145
151
  ]);
152
+ BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
153
+ ".png",
154
+ ".jpg",
155
+ ".jpeg",
156
+ ".gif",
157
+ ".bmp",
158
+ ".ico",
159
+ ".webp",
160
+ ".avif",
161
+ ".tiff",
162
+ ".pdf",
163
+ ".zip",
164
+ ".gz",
165
+ ".tgz",
166
+ ".bz2",
167
+ ".xz",
168
+ ".7z",
169
+ ".rar",
170
+ ".jar",
171
+ ".woff",
172
+ ".woff2",
173
+ ".ttf",
174
+ ".otf",
175
+ ".eot",
176
+ ".mp3",
177
+ ".mp4",
178
+ ".wav",
179
+ ".ogg",
180
+ ".webm",
181
+ ".mov",
182
+ ".wasm",
183
+ ".bin",
184
+ ".dat",
185
+ ".db",
186
+ ".sqlite",
187
+ ".so",
188
+ ".dylib",
189
+ ".dll",
190
+ ".exe",
191
+ ".class",
192
+ ".pyc",
193
+ // Office and design documents are zip or proprietary containers.
194
+ ".xlsx",
195
+ ".xls",
196
+ ".docx",
197
+ ".doc",
198
+ ".pptx",
199
+ ".ppt",
200
+ ".odt",
201
+ ".ods",
202
+ ".psd",
203
+ ".ai",
204
+ ".sketch",
205
+ ".fig",
206
+ ".heic",
207
+ ".heif",
208
+ // Archives, columnar data, models and compressed payloads.
209
+ ".tar",
210
+ ".zst",
211
+ ".br",
212
+ ".lz4",
213
+ ".parquet",
214
+ ".avro",
215
+ ".orc",
216
+ ".pkl",
217
+ ".npy",
218
+ ".npz",
219
+ ".onnx",
220
+ ".pt",
221
+ ".safetensors",
222
+ ".gguf",
223
+ ".sqlite3",
224
+ ".avi",
225
+ ".mkv",
226
+ ".flac",
227
+ ".aac",
228
+ ".m4a",
229
+ ".ttc"
230
+ ]);
146
231
  EXECUTABLE_SCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([".sh", ".bash", ".zsh"]);
147
232
  }
148
233
  });
234
+ async function renameWithRetry(from, to, options = {}) {
235
+ const attempts = options.attempts ?? 5;
236
+ const delayMs = options.delayMs ?? 50;
237
+ for (let attempt = 0; ; attempt++) {
238
+ try {
239
+ await rename(from, to);
240
+ return;
241
+ } catch (err) {
242
+ const code = err.code;
243
+ const transient = code !== void 0 && TRANSIENT_RENAME_CODES.has(code);
244
+ if (!transient || attempt >= attempts - 1) throw err;
245
+ await setTimeout(delayMs * 2 ** attempt);
246
+ }
247
+ }
248
+ }
249
+ var TRANSIENT_RENAME_CODES;
250
+ var init_rename_retry = __esm({
251
+ "src/utils/filesystem/rename-retry.ts"() {
252
+ TRANSIENT_RENAME_CODES = /* @__PURE__ */ new Set(["EPERM", "EACCES", "EBUSY", "ENOTEMPTY", "EEXIST"]);
253
+ }
254
+ });
149
255
  function shouldSkipRecursiveBranch(segments) {
150
256
  if (segments.length > MAX_RECURSIVE_DEPTH) return true;
151
257
  const counts = /* @__PURE__ */ new Map();
@@ -236,30 +342,10 @@ var init_fs_traverse = __esm({
236
342
  MAX_SEGMENT_REPETITIONS = 3;
237
343
  }
238
344
  });
239
- async function renameWithRetry(from, to, options = {}) {
240
- const attempts = options.attempts ?? 5;
241
- const delayMs = options.delayMs ?? 50;
242
- for (let attempt = 0; ; attempt++) {
243
- try {
244
- await rename(from, to);
245
- return;
246
- } catch (err) {
247
- const code = err.code;
248
- const transient = code !== void 0 && TRANSIENT_RENAME_CODES.has(code);
249
- if (!transient || attempt >= attempts - 1) throw err;
250
- await setTimeout(delayMs * 2 ** attempt);
251
- }
252
- }
253
- }
254
- var TRANSIENT_RENAME_CODES;
255
- var init_rename_retry = __esm({
256
- "src/utils/filesystem/rename-retry.ts"() {
257
- TRANSIENT_RENAME_CODES = /* @__PURE__ */ new Set(["EPERM", "EACCES", "EBUSY", "ENOTEMPTY", "EEXIST"]);
258
- }
259
- });
260
345
  async function readFileSafe(path) {
261
346
  try {
262
- const data = await readFile(path, "utf-8");
347
+ const data = await readFile(path, payloadEncodingFor(path));
348
+ if (isBinaryPayloadPath(path)) return data;
263
349
  return data.startsWith(UTF8_BOM) ? data.slice(UTF8_BOM.length) : data;
264
350
  } catch (err) {
265
351
  const e = err;
@@ -283,40 +369,28 @@ async function writeFileAtomic(path, content, options) {
283
369
  { errnoCode: "EISDIR" }
284
370
  );
285
371
  }
286
- if (info.isSymbolicLink()) {
287
- await unlink(path).catch((e) => {
288
- if (e.code !== "ENOENT") throw e;
289
- });
290
- }
291
372
  } catch (err) {
292
373
  if (err instanceof FileSystemError) throw err;
293
374
  const e = err;
294
375
  if (e.code !== "ENOENT") throw err;
295
376
  }
296
- const tmpPath = `${path}.tmp`;
377
+ const tmpPath = `${path}.tmp-${randomUUID()}`;
297
378
  const payload = shouldNormalizeLineEndings(path) ? normalizeLineEndings(content) : content;
298
379
  const mode = executableModeFor(path);
380
+ let handle;
381
+ let ownsTemporaryFile = false;
299
382
  try {
300
- try {
301
- const tmpInfo = await lstat(tmpPath);
302
- if (tmpInfo.isSymbolicLink()) {
303
- await unlink(tmpPath);
304
- }
305
- } catch (tmpErr) {
306
- if (tmpErr.code !== "ENOENT") throw tmpErr;
307
- }
308
- const writeOpts = {
309
- encoding: "utf-8",
310
- flag: "w"
311
- };
312
- if (mode !== void 0) writeOpts.mode = mode;
313
- await writeFile(tmpPath, payload, writeOpts);
314
- await rename(tmpPath, path);
315
- if (mode !== void 0) {
316
- await chmod(path, mode);
317
- }
383
+ handle = await open(tmpPath, "wx", mode);
384
+ ownsTemporaryFile = true;
385
+ await handle.writeFile(payload, payloadEncodingFor(path));
386
+ if (mode !== void 0) await handle.chmod(mode);
387
+ await handle.close();
388
+ handle = void 0;
389
+ await renameWithRetry(tmpPath, path);
318
390
  } catch (err) {
319
- await rm(tmpPath, { force: true }).catch(() => {
391
+ await handle?.close().catch(() => {
392
+ });
393
+ if (ownsTemporaryFile) await rm(tmpPath, { force: true }).catch(() => {
320
394
  });
321
395
  const e = err;
322
396
  throw new FileSystemError(
@@ -341,6 +415,7 @@ var init_fs = __esm({
341
415
  "src/utils/filesystem/fs.ts"() {
342
416
  init_errors();
343
417
  init_fs_text_encoding();
418
+ init_rename_retry();
344
419
  init_fs_traverse();
345
420
  init_fs_text_encoding();
346
421
  init_rename_retry();
@@ -487,6 +562,60 @@ var init_boilerplate_filter = __esm({
487
562
  }
488
563
  });
489
564
 
565
+ // src/utils/text/json-comments.ts
566
+ function stripJsonComments(text) {
567
+ let result2 = "";
568
+ let i = 0;
569
+ const len = text.length;
570
+ while (i < len) {
571
+ const ch = text[i];
572
+ if (ch === '"') {
573
+ result2 += ch;
574
+ i++;
575
+ while (i < len) {
576
+ const sc = text[i];
577
+ result2 += sc;
578
+ if (sc === "\\") {
579
+ i++;
580
+ if (i < len) {
581
+ result2 += text[i];
582
+ }
583
+ } else if (sc === '"') {
584
+ break;
585
+ }
586
+ i++;
587
+ }
588
+ i++;
589
+ continue;
590
+ }
591
+ if (ch === "/" && text[i + 1] === "*") {
592
+ i += 2;
593
+ while (i < len) {
594
+ if (text[i] === "*" && text[i + 1] === "/") {
595
+ i += 2;
596
+ break;
597
+ }
598
+ i++;
599
+ }
600
+ continue;
601
+ }
602
+ if (ch === "/" && text[i + 1] === "/") {
603
+ i += 2;
604
+ while (i < len && text[i] !== "\n") {
605
+ i++;
606
+ }
607
+ continue;
608
+ }
609
+ result2 += ch;
610
+ i++;
611
+ }
612
+ return result2;
613
+ }
614
+ var init_json_comments = __esm({
615
+ "src/utils/text/json-comments.ts"() {
616
+ }
617
+ });
618
+
490
619
  // src/canonical/features/syntax-error.ts
491
620
  function failSyntax(filePath2, cause, onParseError) {
492
621
  const error = new CanonicalParseError(filePath2, cause);
@@ -536,54 +665,6 @@ function parseServer(raw) {
536
665
  env
537
666
  };
538
667
  }
539
- function stripJsonComments(text) {
540
- let result2 = "";
541
- let i = 0;
542
- const len = text.length;
543
- while (i < len) {
544
- const ch = text[i];
545
- if (ch === '"') {
546
- result2 += ch;
547
- i++;
548
- while (i < len) {
549
- const sc = text[i];
550
- result2 += sc;
551
- if (sc === "\\") {
552
- i++;
553
- if (i < len) {
554
- result2 += text[i];
555
- }
556
- } else if (sc === '"') {
557
- break;
558
- }
559
- i++;
560
- }
561
- i++;
562
- continue;
563
- }
564
- if (ch === "/" && text[i + 1] === "*") {
565
- i += 2;
566
- while (i < len) {
567
- if (text[i] === "*" && text[i + 1] === "/") {
568
- i += 2;
569
- break;
570
- }
571
- i++;
572
- }
573
- continue;
574
- }
575
- if (ch === "/" && text[i + 1] === "/") {
576
- i += 2;
577
- while (i < len && text[i] !== "\n") {
578
- i++;
579
- }
580
- continue;
581
- }
582
- result2 += ch;
583
- i++;
584
- }
585
- return result2;
586
- }
587
668
  async function parseMcp(mcpPath, onParseError) {
588
669
  const content = await readFileSafe(mcpPath);
589
670
  if (!content) return null;
@@ -606,6 +687,7 @@ async function parseMcp(mcpPath, onParseError) {
606
687
  }
607
688
  var init_mcp = __esm({
608
689
  "src/canonical/features/mcp.ts"() {
690
+ init_json_comments();
609
691
  init_syntax_error();
610
692
  init_fs();
611
693
  }
@@ -2310,43 +2392,72 @@ function isGlobAdjacent(content, start, end) {
2310
2392
  const next = end < content.length ? content.at(end) : "";
2311
2393
  return prev === "*" || next === "*";
2312
2394
  }
2395
+ var NON_REWRITABLE_BARE_FILES, PATH_TOKEN, LINE_NUMBER_SUFFIX;
2396
+ var init_link_rebaser_helpers = __esm({
2397
+ "src/core/reference/link-rebaser-helpers.ts"() {
2398
+ init_path_helpers();
2399
+ init_link_format_registry();
2400
+ NON_REWRITABLE_BARE_FILES = /* @__PURE__ */ new Set([
2401
+ "AGENTS.md",
2402
+ "CLAUDE.md",
2403
+ "GEMINI.md",
2404
+ "codex.md",
2405
+ ".windsurfrules",
2406
+ ".cursorrules"
2407
+ ]);
2408
+ PATH_TOKEN = /(?:\.\.[\\/]|\.\/|\.\\|\/[A-Za-z0-9._-]|[A-Za-z]:[\\/][A-Za-z0-9._-]|\.agentsmesh[\\/]|\.claude[\\/]|\.cursor[\\/]|\.github[\\/]|\.continue[\\/]|\.junie[\\/]|\.kiro[\\/]|\.gemini[\\/]|\.clinerules[\\/]|\.cline[\\/]|\.codex[\\/]|\.agents[\\/]|\.windsurf[\\/]|\.roo[\\/]|(?:[A-Za-z0-9._-]+[\\/])+|[A-Za-z0-9._-]+\.[A-Za-z0-9._-]+)[A-Za-z0-9._@%+~:\\/-]*/g;
2409
+ LINE_NUMBER_SUFFIX = /(?::(\d+)){1,2}$/;
2410
+ }
2411
+ });
2412
+
2413
+ // src/core/reference/protected-ranges.ts
2414
+ function extendAcrossBalancedParens(content, end) {
2415
+ let cursor = end;
2416
+ while (content[cursor] === "(") {
2417
+ let depth = 0;
2418
+ let scan = cursor;
2419
+ while (scan < content.length) {
2420
+ const ch = content[scan];
2421
+ if (ch === "(") depth += 1;
2422
+ else if (ch === ")") {
2423
+ depth -= 1;
2424
+ if (depth === 0) break;
2425
+ } else if (ch === void 0 || /\s/.test(ch)) return cursor;
2426
+ scan += 1;
2427
+ }
2428
+ if (depth !== 0) return cursor;
2429
+ cursor = scan + 1;
2430
+ while (cursor < content.length && !/[\s<>()\]]/.test(content.charAt(cursor))) cursor += 1;
2431
+ }
2432
+ return cursor;
2433
+ }
2313
2434
  function protectedRanges(content) {
2314
2435
  const ranges = [];
2315
2436
  for (const pattern of getLinkFormatRegistry().protectedSchemes) {
2316
2437
  const globalPattern = pattern.flags.includes("g") ? pattern : new RegExp(pattern.source, `${pattern.flags}g`);
2317
2438
  for (const match of content.matchAll(globalPattern)) {
2318
- ranges.push([match.index ?? 0, (match.index ?? 0) + match[0].length]);
2439
+ const start = match.index;
2440
+ ranges.push([start, extendAcrossBalancedParens(content, start + match[0].length)]);
2319
2441
  }
2320
2442
  }
2321
2443
  for (const match of content.matchAll(FENCED_CODE_BLOCK)) {
2322
- ranges.push([match.index ?? 0, (match.index ?? 0) + match[0].length]);
2444
+ ranges.push([match.index, match.index + match[0].length]);
2323
2445
  }
2324
2446
  for (const match of content.matchAll(ROOT_GENERATION_CONTRACT_BLOCK)) {
2325
- ranges.push([match.index ?? 0, (match.index ?? 0) + match[0].length]);
2447
+ ranges.push([match.index, match.index + match[0].length]);
2326
2448
  }
2327
2449
  for (const match of content.matchAll(EMBEDDED_RULES_BLOCK)) {
2328
- ranges.push([match.index ?? 0, (match.index ?? 0) + match[0].length]);
2450
+ ranges.push([match.index, match.index + match[0].length]);
2329
2451
  }
2330
2452
  return ranges;
2331
2453
  }
2332
- var NON_REWRITABLE_BARE_FILES, FENCED_CODE_BLOCK, ROOT_GENERATION_CONTRACT_BLOCK, EMBEDDED_RULES_BLOCK, PATH_TOKEN, LINE_NUMBER_SUFFIX;
2333
- var init_link_rebaser_helpers = __esm({
2334
- "src/core/reference/link-rebaser-helpers.ts"() {
2335
- init_path_helpers();
2454
+ var FENCED_CODE_BLOCK, ROOT_GENERATION_CONTRACT_BLOCK, EMBEDDED_RULES_BLOCK;
2455
+ var init_protected_ranges = __esm({
2456
+ "src/core/reference/protected-ranges.ts"() {
2336
2457
  init_link_format_registry();
2337
- NON_REWRITABLE_BARE_FILES = /* @__PURE__ */ new Set([
2338
- "AGENTS.md",
2339
- "CLAUDE.md",
2340
- "GEMINI.md",
2341
- "codex.md",
2342
- ".windsurfrules",
2343
- ".cursorrules"
2344
- ]);
2345
2458
  FENCED_CODE_BLOCK = /^(?:```|~~~)[^\n]*\n[\s\S]*?^(?:```|~~~)/gm;
2346
2459
  ROOT_GENERATION_CONTRACT_BLOCK = /<!-- agentsmesh:root-generation-contract:start -->[\s\S]*?<!-- agentsmesh:root-generation-contract:end -->/g;
2347
2460
  EMBEDDED_RULES_BLOCK = /<!-- agentsmesh:embedded-rules:start -->[\s\S]*?<!-- agentsmesh:embedded-rules:end -->/g;
2348
- PATH_TOKEN = /(?:\.\.[\\/]|\.\/|\.\\|\/[A-Za-z0-9._-]|[A-Za-z]:[\\/][A-Za-z0-9._-]|\.agentsmesh[\\/]|\.claude[\\/]|\.cursor[\\/]|\.github[\\/]|\.continue[\\/]|\.junie[\\/]|\.kiro[\\/]|\.gemini[\\/]|\.clinerules[\\/]|\.cline[\\/]|\.codex[\\/]|\.agents[\\/]|\.windsurf[\\/]|\.roo[\\/]|(?:[A-Za-z0-9._-]+[\\/])+|[A-Za-z0-9._-]+\.[A-Za-z0-9._-]+)[A-Za-z0-9._@%+~:\\/-]*/g;
2349
- LINE_NUMBER_SUFFIX = /(?::(\d+)){1,2}$/;
2350
2461
  }
2351
2462
  });
2352
2463
 
@@ -2839,6 +2950,7 @@ var init_link_rebaser = __esm({
2839
2950
  "src/core/reference/link-rebaser.ts"() {
2840
2951
  init_path_helpers();
2841
2952
  init_link_rebaser_helpers();
2953
+ init_protected_ranges();
2842
2954
  init_link_rebaser_output();
2843
2955
  init_link_rebaser_resolution();
2844
2956
  init_link_uri_encoding();
@@ -8509,7 +8621,7 @@ ${rawBody}`;
8509
8621
  });
8510
8622
  }
8511
8623
  function generateMcp3(canonical) {
8512
- if (!canonical.mcp || Object.keys(canonical.mcp.mcpServers).length === 0) return [];
8624
+ if (!canonical.mcp) return [];
8513
8625
  const content = JSON.stringify({ mcpServers: canonical.mcp.mcpServers }, null, 2);
8514
8626
  return [{ path: CLAUDE_MCP_JSON, content }];
8515
8627
  }
@@ -8544,14 +8656,12 @@ function generatePermissions4(canonical) {
8544
8656
  if (!canonical.permissions) return [];
8545
8657
  const { allow, deny } = canonical.permissions;
8546
8658
  const ask = canonical.permissions.ask ?? [];
8547
- if (allow.length === 0 && deny.length === 0 && ask.length === 0) return [];
8548
8659
  const content = JSON.stringify({ permissions: { allow, deny, ask } }, null, 2);
8549
8660
  return [{ path: CLAUDE_SETTINGS, content }];
8550
8661
  }
8551
8662
  function generateHooks3(canonical) {
8552
- if (!canonical.hooks || Object.keys(canonical.hooks).length === 0) return [];
8663
+ if (!canonical.hooks) return [];
8553
8664
  const claudeHooks = buildClaudeHooksObjectFromCanonical(canonical);
8554
- if (Object.keys(claudeHooks).length === 0) return [];
8555
8665
  const content = JSON.stringify({ hooks: claudeHooks }, null, 2);
8556
8666
  return [{ path: CLAUDE_SETTINGS, content }];
8557
8667
  }
@@ -20369,9 +20479,9 @@ var init_config_toml = __esm({
20369
20479
  function isValidKimiPermissionPattern(pattern) {
20370
20480
  const trimmed = pattern.trim();
20371
20481
  if (trimmed.length === 0) return false;
20372
- const open = trimmed.indexOf("(");
20373
- if (open === -1) return true;
20374
- return trimmed.endsWith(")") && open > 0;
20482
+ const open2 = trimmed.indexOf("(");
20483
+ if (open2 === -1) return true;
20484
+ return trimmed.endsWith(")") && open2 > 0;
20375
20485
  }
20376
20486
  function listOf(permissions, key) {
20377
20487
  return permissions[key] ?? [];
@@ -29130,6 +29240,14 @@ function redactUrlSecrets(message) {
29130
29240
  }
29131
29241
  );
29132
29242
  }
29243
+ function stripUrlCredentials(url) {
29244
+ return url.replace(
29245
+ URL_WITH_CREDENTIALS,
29246
+ (_full, scheme, _userinfo, rest) => {
29247
+ return `${scheme}${rest}`;
29248
+ }
29249
+ );
29250
+ }
29133
29251
  function gitProtocolOptIns() {
29134
29252
  const on = (v) => v === "1" || v === "true";
29135
29253
  return {
@@ -29528,6 +29646,7 @@ async function sweepStaleCache(cacheDir, maxAgeMs) {
29528
29646
  var MAX_CACHE_KEY_LENGTH = 80;
29529
29647
  var CACHE_KEY_HASH_LENGTH = 12;
29530
29648
  function buildCacheKey(provider, identifier, ref) {
29649
+ identifier = stripUrlCredentials(identifier);
29531
29650
  const safe = (value) => value.replace(/[^a-zA-Z0-9_.-]/g, "_").replace(/^\.+/, "_");
29532
29651
  const [org, repo] = provider === "github" ? identifier.split("/", 2) : [];
29533
29652
  const readable = org && repo ? `${safe(org)}--${safe(repo)}--${safe(ref)}` : `${safe(provider)}__${safe(identifier)}__${safe(ref)}`;