agentsmesh 0.18.1 → 0.19.1

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,408 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.19.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 041b9c5: fix(security): plug input/path/proto-pollution holes in plugin, install, MCP, and config
8
+
9
+ Closes a batch of security audit findings (2 HIGH + 5 MEDIUM):
10
+ - **Plugin source containment** (`src/plugins/load-plugin.ts`) — local plugin
11
+ sources are now resolved with `realpath` and rejected when they escape
12
+ `projectRoot`. A hostile `agentsmesh.yaml` with
13
+ `plugins[].source: "../../tmp/evil.js"` no longer reaches dynamic
14
+ `import()`. Bare npm specifiers continue to resolve through
15
+ `node_modules/<source>`. Both sides are canonicalized so macOS
16
+ `/tmp -> /private/tmp` (and other platform-level symlinks) do not
17
+ produce false positives.
18
+ - **Prototype pollution denylist** (`src/config/core/loader.ts`) —
19
+ `deepMergeObjects` over `agentsmesh.local.yaml` now skips `__proto__`,
20
+ `constructor`, and `prototype` keys. Defense-in-depth: the `yaml` v2
21
+ parser already strips `__proto__`, but this pins the invariant against
22
+ future parser swaps.
23
+ - **Install manifest name validation** (`src/install/core/install-manifest.ts`) —
24
+ `installManifestEntrySchema.name` now refuses path separators, NUL,
25
+ and `.`/`..` segments. A poisoned `installs.yaml` entry can no longer
26
+ drive `rm -rf` outside `.agentsmesh/packs/` at uninstall time.
27
+ - **`git+http://` allowlist** (`src/config/remote/remote-source.ts`) —
28
+ rejected by default; opt-in via `AGENTSMESH_ALLOW_INSECURE_GIT=1` for
29
+ closed-network development. `https://`, `ssh://`, and `file://` are
30
+ unchanged. Closes a MITM window before SHA pinning resolves.
31
+ - **MCP `cwd` / `description` refinement** (`src/mcp/schemas.ts`) — `cwd`
32
+ rejects `..` segments (POSIX + Windows separators), NUL, and newlines;
33
+ `description` rejects NUL and newlines. MCP clients can no longer
34
+ plant a structurally-escaping working directory that downstream agents
35
+ consume via `spawn(command, args, { cwd })`.
36
+ - **Global path redaction in MCP errors** (`src/mcp/errors.ts`) —
37
+ `redactAbsolutePaths` now strips paths anywhere in the string, catching
38
+ embedded paths in stack frames (`at Foo (/Users/...)`) and quoted
39
+ paths in Node errors (`ENOENT, open '/Users/...'`) the prior
40
+ whitespace-anchored regex missed.
41
+ - **`copyDir` symlink hardening** (`src/utils/filesystem/fs-traverse.ts`) —
42
+ `copyDir` now uses `lstat` and skips symlinks. A symlink in the source
43
+ tree pointing outside its root can no longer have its target's bytes
44
+ exfiltrated into the destination (and into any redistributed pack
45
+ built on top of it).
46
+
47
+ Behavioral changes that could affect existing consumers:
48
+ - `git+http://...` extends/installs require `AGENTSMESH_ALLOW_INSECURE_GIT=1`.
49
+ - MCP server entries with `cwd: "../foo"` no longer parse — rewrite as a
50
+ POSIX-relative path without `..` segments.
51
+ - Plugin `source:` entries pointing outside the project tree no longer
52
+ load. The standard `node_modules/<plugin>` and project-local layouts
53
+ are unaffected.
54
+ - A poisoned `installs.yaml` entry whose `name` contains separators or
55
+ `..` is now dropped at parse time (the rest of the manifest survives).
56
+ - A `agentsmesh.local.yaml` payload at `__proto__`, `constructor`, or
57
+ `prototype` keys is silently dropped instead of merged.
58
+
59
+ Branch coverage > 95% on every touched file; full unit/integration suite
60
+ (7596 tests) and plugin e2e suite (57 tests) green.
61
+
62
+ ## 0.19.0
63
+
64
+ ### Minor Changes
65
+
66
+ - 879eeed: refactor(install): every install-time command-directory read now delegates to per-target importer mappers
67
+
68
+ The previous skill-pack-aggregator refactor wired the target-mapper
69
+ delegation seam (`hasToolNativeCommandImporter` + `readToolNativeCommands`)
70
+ into exactly one call site: `mergeCommands`. The canonical / manual /
71
+ flat-collection install paths still routed through plain `parseCommands`
72
+ (`.md`-only), so a root-level `commands/*.toml` (Gemini CLI's native
73
+ format) on `JuliusBrussee/caveman` and similar repos was silently dropped
74
+ with a "Skipped N commands file(s) ... format: .toml" warning, even though
75
+ the gemini-cli descriptor already ships a TOML-aware mapper.
76
+
77
+ This change generalizes the seam into a single shared helper
78
+ (`readCommandsDirWithMappers`) used by every install-time read:
79
+ - **`src/install/importers/target-native-commands.ts`** gains
80
+ `readCommandsDirWithMappers(srcDir, { restrictToTarget?, parseOpts? })`.
81
+ When `restrictToTarget` is set (per-tool dir like `.gemini/commands/`),
82
+ only that target's mapper runs. When unset (canonical root `commands/`),
83
+ every registered target's non-`.md` mapper is tried; canonical `.md`
84
+ wins on basename collision so dedup-log readability is preserved.
85
+ - **`src/canonical/load/load-canonical-slice.ts`** now returns
86
+ `{ canonical, cleanup }` and takes an `enableTargetCommandMappers` flag.
87
+ Install-path callers (`discoverFromContentRoot`) set it; the extends
88
+ path leaves it off to preserve the historical `.md`-only behavior and
89
+ avoid the tmpdir staging lifecycle (extends would need cross-load
90
+ cleanup tracking that isn't worth the complexity for a rare edge case).
91
+ - **`src/sources/anthropic-skill-pack/merge-commands.ts`** drops its
92
+ bespoke per-spec loop and routes every spec — canonical root `commands/`
93
+ and per-tool dirs alike — through the shared helper.
94
+ - **`src/install/run/run-install-discovery.ts`** and
95
+ **`src/install/manual/manual-install-discovery.ts`** merge the slice's
96
+ staging cleanup into the existing `prep.cleanup` lifecycle.
97
+
98
+ Result on `JuliusBrussee/caveman`: install with no flags previously
99
+ produced `7 skills + 3 agents` and warned about 4 TOML commands; now
100
+ installs `7 skills + 4 commands + 3 agents`, no warning, no flag needed.
101
+ Verified end-to-end (`Installed 7 skills, 4 commands, 3 agents`).
102
+ `addyosmani/agent-skills` (the original skill-pack test) remains at
103
+ `23 skills, 8 commands, 3 agents` — no regression.
104
+
105
+ Architectural payoff: adding a future target whose commands use a
106
+ non-Markdown format is now a one-place change in that target's
107
+ descriptor. The aggregator and every install path automatically pick up
108
+ the new format via the shared seam.
109
+
110
+ - 3b6af70: feat(install): detect upstream SPDX license at pack-creation time and surface it in `agentsmesh installs list`
111
+
112
+ Every `agentsmesh install <source>` now probes the materialized pack root for a `LICENSE` / `COPYING` / `NOTICE` / `COPYRIGHT` file (across `.md` / `.txt` / `.rst` / no-extension variants) and runs a conservative SPDX detector against the bytes. The detector recognizes the dozen most common OSI/SPDX licenses by their canonical text fingerprints (MIT, Apache-2.0, BSD-2-Clause, BSD-3-Clause, GPL-2.0, GPL-3.0, LGPL-2.1, LGPL-3.0, AGPL-3.0, MPL-2.0, ISC, CC0-1.0, Unlicense) plus the explicit `SPDX-License-Identifier:` header. Unknown text resolves to `null` — better to say "unknown" than mislabel exotic terms.
113
+
114
+ The detected identifier is recorded in `pack.yaml#license`, propagated through `InstallsListEntry`, and rendered as a new `LICENSE` column in `agentsmesh installs list` (and JSON output). Lets you scan installed packs and spot proprietary or unknown-terms upstreams at a glance before relying on them.
115
+
116
+ Pack metadata schema is additive (`license` is optional) and prior `pack.yaml` files keep parsing — no migration needed.
117
+
118
+ - 975bdb5: Skill-pack-aware install pipeline, new `uninstall` and `installs` commands, descriptor-driven target dispatch refactors, and a senior-architect hardening pass. Roll-up of every change on `develop` since `v0.18.1`.
119
+
120
+ ## New commands
121
+
122
+ ### `agentsmesh install <url>` (extended)
123
+
124
+ Now auto-detects three source shapes via a multi-signal classifier (`src/install/classify/`) and dispatches accordingly:
125
+ - **`anthropic-skill-pack`** — imports root `skills/`, `agents/`, `references/`, merged per-target `.claude/commands/` + `.gemini/commands/`, and multi-tool rule files as one bulk set. A single command imports the full pack (e.g. all 23 skills + 3 agents + 7 commands of `addyosmani/agent-skills`); pre-change behaviour required 7+ invocations with `--as`.
126
+ - **`canonical-agentsmesh`** — unchanged.
127
+ - **`tool-native`** — unchanged. Five backcompat fixtures (`tests/integration/install-backcompat.integration.test.ts`) pin the discriminator threshold so legacy repos take their original path.
128
+ - **`unknown`** — unchanged canonical-slice fallback.
129
+
130
+ The discriminator threshold (sum of matched signal weights ≥ 1.4 with the primary `skills/<kebab>/SKILL.md` signal present) makes false positives essentially impossible on tool-native or canonical repos. `--target` and `--as` keep their explicit-override semantics and skip the classifier.
131
+
132
+ When classified as a skill pack and a TTY is attached, two interactive prompts surface:
133
+ - **Bulk select (three tiers)** — `[a]ll / [n]one / [s]elect per type` summary banner → per-type `[y/n/c]` → per-entity `[y/N/a=accept-all-remaining/q=skip-all-remaining]`. `--force`, `--json`, and non-TTY contexts accept everything.
134
+ - **Broken-link (three options)** — for body links that point outside the imported subtree, classify each as in-tree-included / resolvable-outside / unresolvable and cluster per entity. `[i]nclude resolvable as supporting files / [l]eave with warnings / [a]bort install`. `--force` defaults to `[l]eave-with-warnings`.
135
+
136
+ New flags:
137
+ - `--all` — install every sub-pack from a `.claude-plugin/marketplace.json` source.
138
+
139
+ Other install improvements:
140
+ - **Source-layout detection covers community-repo shapes**: root-level `SKILL.md` (`blader/humanizer`), root `.cursorrules` / `.windsurfrules` (`PatrickJS/awesome-cursorrules` style), nested marketplace plugin trees (`.claude-plugin/marketplace.json`), and arbitrary 2+ sub-pack directory layouts. The picker is descriptor-driven via `selectInstallCandidates`.
141
+ - **Concurrent-install lock** — `.agentsmesh/.install.lock` is acquired at the top of any `install` or `uninstall` run (and held across `--sync` replay). Concurrent invocations on the same project fail fast with `LockAcquisitionError` rather than racing on filesystem writes.
142
+ - **Pack writes are atomic** via staging-dir + rename. Each install now writes `.agentsmesh-install-manifest.json` next to the pack with the install-time `name`, `source`, `installed_at`, classifier verdict (`source_type`), and per-file `sha256:` map.
143
+ - **Pack-name preservation across URL variants** — `findExistingInstallName` (`src/install/core/install-name.ts`) keys reuse on canonical `github:<org>/<repo>` plus identity scope (`target + as + features`), so `https://`, `git@`, and `github:` spellings of the same source dedupe into one pack. The `git+` prefix is now stripped iteratively (no recursion, safe under malicious manifest input).
144
+ - **Lenient frontmatter parsing for all third-party imports** — `readSkillFrontmatterName` and `inferMdcTarget` skip files with invalid YAML and continue, rather than aborting the whole install on one bad scalar.
145
+ - **Flat-collection basename collisions** — when two `--as <kind>` flat-collection files share a basename, names are namespaced rather than dropped silently.
146
+ - **`--path`, `--target`, `--as` flags** — all trimmed symmetrically; empty/whitespace-only values now correctly normalise to "not provided" inside recursive auto-pick calls.
147
+
148
+ ### `agentsmesh uninstall <name>[,<name>...]` (NEW)
149
+
150
+ Removes one or more installed packs:
151
+ - `rm -rf .agentsmesh/packs/<name>/`.
152
+ - Drops the row from `installs.yaml`.
153
+ - Drops the matching `extends:` row from `agentsmesh.yaml` when present (`install --extends` is now uninstallable).
154
+ - Runs `generate` so `cleanupStaleGeneratedOutputs` evicts orphaned target files.
155
+
156
+ Flags: `--all`, `--keep-pack` (leave pack on disk; only drop yaml entries), `--keep-generated` (skip the final generate; warn about stale targets), `--global`, `--dry-run`, `--force`, `--json`. The `--keep-pack` flag also doubles as the apply-layer equivalent of the interactive `[k]eep-modified` action. `--force` is implied by `--json`.
157
+
158
+ Pre-uninstall **drift check** compares the current pack contents against `.agentsmesh-install-manifest.json`. When drift is detected, a modified-files prompt offers `[d]elete-anyway / [k]eep-modified / [a]bort`; `--force` defaults to `[d]`. Legacy packs (no manifest) auto-migrate at uninstall time — current contents become the baseline; a warning makes this explicit. Exit `130` on user-aborted prompt; `0` on success or `--dry-run`.
159
+
160
+ **Mid-batch failure isolation** — if `applyUninstall` throws for one pack, survivors continue. The failure lands in a new `data.failed[]` envelope; post-operation `generate` still runs over the packs that succeeded so the tool tree stays consistent with the (possibly partially mutated) `installs.yaml`. Exit code is `1` when any pack failed.
161
+
162
+ **`--dry-run uninstall` is a true no-op** — the legacy-manifest migration computes the baseline in memory but does NOT persist `.agentsmesh-install-manifest.json` to disk under `--dry-run`.
163
+
164
+ ### `agentsmesh installs list` (NEW)
165
+
166
+ Read-only inventory. Reads `installs.yaml`, hydrates `installed_at` + `source_type` from each pack's manifest, and emits either a space-padded `NAME / SOURCE / FEATURES / INSTALLED` table or a JSON envelope. Empty list exits 0. Forward-slash `pack_path`. `--global` reads from `~/.agentsmesh/installs.yaml`. The plural-vs-singular typo (`installs` vs `install`) surfaces a "did you mean `install`?" hint on unknown subcommands. Help banner comes from the central `help-data.ts` (single source of truth).
167
+
168
+ ## Reliability fixes (broken-link rewriter)
169
+
170
+ Two silent data-corruption bugs in the skill-pack broken-link `[i]nclude` flow are now fixed:
171
+ - **Verbatim destination matching** — body rewrites match `ScannedLink.raw` (the as-authored text), not a normalized form. Bodies authored with `{baseDir}/foo.md` or Windows-style `..\refs\x.md` previously copied the supporting file but silently skipped the body rewrite, leaving an orphan `references/<basename>` plus a still-broken link. Both forms now rewrite correctly.
172
+ - **Basename-collision disambiguation** — two distinct outside paths sharing a basename (`docs/A/README.md` + `docs/B/README.md` → `references/README.md`) previously dropped the second file's bytes and pointed both citations at the same target. Names are now slugged from the full `resolvedRelative` on collision (`references/docs-A-README.md`, `references/docs-B-README.md`).
173
+
174
+ ## Drift-detection robustness
175
+ - **CRLF / BOM normalization** — `hashFileForManifest` (`src/utils/crypto/hash.ts`) normalizes `\r\n?` → `\n` and strips a leading UTF-8 BOM for text-extension files (`.md`, `.json`, `.yaml`, etc.) before hashing. A Windows editor saving CRLF or a tool prepending a BOM no longer registers as drift. Binary files are still hashed as raw bytes.
176
+ - **Symlink-safe traversal** — install-time pack hashing and uninstall-time drift detection now use `readDirRecursiveNoSymlinks`. A symlink that used to be followed at install (silently absorbing external bytes into the hash) only to be unlinked-without-following at uninstall (`rm` removes the link, not the target) no longer produces a permanent drift-detection mismatch.
177
+
178
+ ## IDE auto-config via in-file schema directives (NEW)
179
+
180
+ Every YAML / JSON file the CLI writes is now stamped with an editor-recognizable schema directive so VSCode (Red Hat YAML extension), JetBrains IDEs, vim/neovim with `yaml-language-server` / `coc-json`, and the GitHub Actions YAML editor get autocomplete + validation immediately — **no IDE configuration required**.
181
+
182
+ YAML files get a top-of-file comment:
183
+
184
+ ```yaml
185
+ # yaml-language-server: $schema=https://unpkg.com/agentsmesh@<version>/schemas/<name>.json
186
+ version: 1
187
+ ...
188
+ ```
189
+
190
+ JSON files get a top-level `$schema` field. URLs are pinned to the running package version so the schema referenced always matches the format the file was written with; older files keep working pointed at their original schema until a writer touches them again.
191
+
192
+ Stamped files:
193
+ - `init` writes — `agentsmesh.yaml`, `agentsmesh.local.yaml`, `.agentsmesh/hooks.yaml`, `.agentsmesh/permissions.yaml`.
194
+ - `install` writes — `.agentsmesh/installs.yaml`, `.agentsmesh/packs/<name>/pack.yaml`, `.agentsmesh/packs/<name>/.agentsmesh-install-manifest.json`.
195
+ - `uninstall` refreshes — `.agentsmesh/installs.yaml` after row removal.
196
+
197
+ Implementation: a single helper module `src/utils/output/schema-directive.ts` exports `prependYamlSchemaDirective`, `stampJsonSchemaField`, `schemaUrl`, and `yamlSchemaDirective`. Each is idempotent — re-running a writer on a file that already carries the directive updates the URL in place rather than duplicating the line. New reference page at `reference/json-schemas.mdx` documents all four IDE mechanisms (in-file directive, `$schema` field, VSCode workspace settings, SchemaStore.org plans), CI validation examples, and troubleshooting. The getting-started installation guide expands the existing IDE-autocomplete section to mention the new `installs` and `install-manifest` schemas.
198
+
199
+ ## Published JSON Schemas — required-field correctness (FIX)
200
+
201
+ A long-standing bug in the published `schemas/*.json` files marked every field with a `.default(...)` in its Zod source as `required: true`. Editors then complained about valid minimal configs (e.g. an `agentsmesh.yaml` with just `version: 1` was flagged as "Missing required properties: overrides, pluginTargets, plugins"). The runtime parser substituted the documented default in every one of these cases — only the published schema disagreed.
202
+
203
+ Fixed by a post-processor in `src/schemas/schema-generator.ts::stripRequiredFromDefaults` that walks the emitted JSON Schema and strips defaultable fields from every `required` array. The Zod source schemas keep plain `.default(...)` (so the parsed TS type stays `T`, never `T | undefined`); the publishing layer alone reconciles "default present" with "user MUST provide". `buildAllSchemas()` calls the post-processor for all seven schemas. New regression test asserts the top-level `agentsmesh.json` required list collapses to `['version']`. Verified: `pnpm schemas:generate` produces correct `required` arrays:
204
+ - `agentsmesh.json`, `installs.json` — only `version`
205
+ - `permissions.json`, `hooks.json`, `mcp.json` — no required (everything has a default)
206
+ - `pack.json`, `install-manifest.json` — required = the documented structural fields, unchanged
207
+
208
+ ## Published JSON Schemas (NEW)
209
+
210
+ Two new entries in the published `schemas/` directory (already shipped via `package.json`'s `files` array) cover the two new file formats introduced in this release:
211
+
212
+ | File | Documents |
213
+ | ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
214
+ | `schemas/installs.json` | `.agentsmesh/installs.yaml` — the install manifest that records every materialized pack so `--sync` can replay them post-clone. |
215
+ | `schemas/install-manifest.json` | `.agentsmesh/packs/<name>/.agentsmesh-install-manifest.json` — per-pack integrity manifest (install-time provenance + per-file `sha256:` map used by `uninstall` to detect locally-modified files). |
216
+
217
+ Both schemas are generated from their respective Zod sources (`installManifestSchema` in `src/install/core/install-manifest.ts`; `installManifestFileSchema` in `src/install/manifest/install-manifest-hash.ts`) and verified by the existing schema-freshness test, which now covers all seven published schemas (was five).
218
+
219
+ Editors / GitHub schema validators that wire `$schema: https://unpkg.com/agentsmesh/schemas/installs.json` (or the corresponding install-manifest URL) get autocomplete and validation for these files; CI tools can use them to assert pack provenance without hand-coded JSON-shape checks. `buildAllSchemas()` and the `pnpm schemas:generate` script now produce **7 JSON schemas** instead of 5.
220
+
221
+ ## MCP server — pack lifecycle tools (NEW)
222
+
223
+ The built-in `agentsmesh mcp` server now exposes three additional tools so AI agents speaking MCP can install, uninstall, and inspect community packs without leaving the conversation:
224
+
225
+ | Tool | Purpose |
226
+ | --------------- | ------------------------------------------------------------------------------------------------------------------------------- |
227
+ | `install` | Install a community pack from a URL or local path. Auto-classifies the source layout; `target` / `as` overrides the classifier. |
228
+ | `uninstall` | Remove one or more installed packs. Mid-batch failures isolated; survivors continue. |
229
+ | `installs_list` | Read-only inventory of installed packs (also exposed as `agentsmesh://installs` resource). |
230
+
231
+ All three run with `force: true` internally — MCP has no stdin TTY, so the documented `--force` defaults are accepted for every interactive prompt the CLI would surface (bulk select → accept all, broken-link → leave-with-warnings, modified-files → delete-anyway). Input shapes mirror the CLI flags one-for-one; output envelopes match `InstallData` / `UninstallData` / `InstallsListData`.
232
+
233
+ Tool count moves from **41 → 44**; resource count moves from **16 → 17**. The MCP docs reference and the in-tree `register` / `tool-tables-sweep` contract tests are updated to match.
234
+
235
+ ## Programmatic / JSON-envelope additions (additive)
236
+
237
+ New required fields on public types. JSON readers are unaffected (additive); TypeScript code constructing these types directly needs to populate the new fields:
238
+
239
+ | Type | New field |
240
+ | ----------------------- | --------------------------------- |
241
+ | `UninstallData` | `failed: Array<{ name; reason }>` |
242
+ | `UninstallRemovedEntry` | `partial: boolean` |
243
+ | `AppliedRemoval` | `partial: boolean` |
244
+
245
+ `partial` lets JSON consumers distinguish a fully-clean removal from one where bytes were kept by design (`--keep-pack`, `[k]eep-modified`) or where a step silently no-op'd (no matching extends row, missing pack dir, etc.).
246
+
247
+ ## Descriptor-driven target dispatch (internal refactor)
248
+
249
+ Several install-layer behaviours that previously branched on hardcoded target IDs in shared code now read directly from the relevant target descriptor:
250
+ - **Native-format detection** — `src/config/resolve/native-format-detector.ts` walks `descriptor.detectionPaths` instead of a hand-maintained per-target table.
251
+ - **Native importer dispatch** — `src/install/native/native-importers.ts` looks up the importer via the descriptor registry.
252
+ - **Command directories** and **starter exclusions** — derived from each descriptor's `managedOutputs.dirs` and `excludeFromStarterInit` flag.
253
+ - **Conversion defaults** — `commands_to_skills` / `agents_to_skills` defaults now live on each descriptor's `conversionDefaults`, removing duplicated per-target enable lists.
254
+ - **Path-to-target hint map** — built at module load from descriptor `managedOutputs.dirs` patterns matching `^\.[^/]+\/(rules|commands|agents|skills)$`.
255
+
256
+ Plugin descriptors transparently inherit all of these capabilities.
257
+
258
+ ## Schema tightening (BREAKING for plugin authors)
259
+
260
+ **`validateDescriptor()` now requires `metadata`:**
261
+
262
+ ```ts
263
+ metadata: {
264
+ displayName: string;
265
+ category: 'cli' | 'ide' | 'agent-platform';
266
+ officialUrl: string;
267
+ shortDescription: string;
268
+ }
269
+ ```
270
+
271
+ Plugins built against earlier versions will fail to register at runtime until the descriptor adds the `metadata` block. All bundled plugin fixtures + the 30 built-in target descriptors already include it.
272
+
273
+ The Zod schema also now models `emitScopedSettings`, `mergeGeneratedOutputContent`, `postProcessHookOutputs`, and `preservesManualActivation` (previously laundered away by a `passthrough()` + cast).
274
+
275
+ ## Operational polish
276
+ - **`AGENTSMESH_MAX_TARBALL_MB`** env var caps GitHub tarball acceptance (default 500, range 1-4096). Set higher for large monorepo installs.
277
+ - **`AGENTSMESH_STRICT_PLUGINS=1`** turns plugin descriptor import failures from warning-and-skip into hard errors (CI gate).
278
+ - **Best-effort post-install / uninstall generate** — when the post-operation `generate` pass fails (e.g. lock contention), the surrounding `install` / `uninstall` exits cleanly with a warning rather than reverting the install/uninstall work.
279
+ - **First-time install on a fresh project** — `acquireInstallLock` now `mkdir`s the canonical dir before writing the lockfile, eliminating an ENOENT failure when no `.agentsmesh/` exists yet.
280
+ - **`agentsmesh install --json` and `agentsmesh uninstall --json`** — validation errors and per-pack failures no longer leak to stderr; they go only into the JSON envelope's `error` / `failed[]` fields. Wrappers that were grepping stderr for error text should read the envelope.
281
+ - **Forward-slash paths** — every CLI display string normalizes `\\` → `/`.
282
+
283
+ ## Internals
284
+
285
+ New files (selection): `src/install/classify/{types,signals,classify-source,layout-detect,layout-types,marketplace-manifest}.ts` and `src/install/classify/detectors/{fs-helpers,root-shape,collections}.ts`; `src/install/importers/{boilerplate-filter,entity-importers}.ts`; `src/install/lock/install-lock.ts`; `src/install/prompts/{prompt-io,prompt-types,bulk-prompt,broken-link-prompt,modified-files-prompt}.ts`; `src/install/links/{scan-relative-links,resolve-link}.ts`; `src/install/manifest/install-manifest-hash.ts`; `src/install/picker/select-candidates.ts`; `src/install/run/{single-pack-install,route-picker-result,run-install-marketplace,run-install-prompts,run-install-sync-locked,post-install-generate,install-abort-error}.ts`; `src/install/uninstall/{plan-uninstall,detect-modified,legacy-manifest-migration,uninstall-decisions,apply-uninstall,uninstall-result,run-uninstall}.ts`; `src/install/core/{install-target,install-report,pick-reuse-entry-name,remove-extend-entry}.ts`; `src/sources/anthropic-skill-pack/{index,aggregate,merge-commands,link-scan,apply-decisions}.ts`; `src/cli/commands/{uninstall,installs,installs-list}.ts`; `src/cli/renderers/{uninstall,installs}.ts`; `src/utils/filesystem/fs-traverse.ts::readDirRecursiveNoSymlinks`; `src/utils/crypto/hash.ts::hashFileForManifest`.
286
+
287
+ File-size discipline (per the project's 200-line cap): `layout-detect.ts` (244 → 116 lines) split into `detectors/` modules; `run-install-locked.ts` (295 → 180 lines) split into `single-pack-install.ts` + `route-picker-result.ts`.
288
+
289
+ Coverage: unit-test branch coverage held at 95% across the included set. New: 30+ unit suites and 17 integration tests covering anthropic-pack imports, broken-link / bulk prompt force paths, targeted overrides, backcompat across 5 tool-native fixtures, pack-name preservation, atomicity, every uninstall scenario, `installs list` round-trip, lock contention, marketplace recursion, failure-isolation, dry-run no-op, and CRLF/BOM/symlink hash invariants. Watch-test scheduler envelope hardened (chokidar polling forced in test harness; describe-level timeouts widened).
290
+
291
+ Docs: new `cli/uninstall.mdx`, `cli/installs.mdx`, `guides/installing-skill-packs.mdx`, and `docs/architecture/install.md`; expanded `cli/install.mdx`; README env-var table; lessons-file additions documenting the lenient-frontmatter contract, the circular-import trap in target descriptor evaluation, and the FSEvents flake fix.
292
+
293
+ ## Upgrade notes
294
+ 1. **Plugin authors must add a `metadata` block** to each `TargetDescriptor`. Validation rejects descriptors without it.
295
+ 2. **Packs installed before this version** may report some text files as `modified` on the first `uninstall` after upgrade if their content contains CRLF or a BOM. The standard `[d]elete-anyway / [k]eep-modified / [a]bort` prompt covers it (or use `--force` to accept the default). No data is lost. Installs created after upgrade use the new hashing algorithm consistently.
296
+ 3. **CI wrappers parsing `--json`** should read errors from the envelope's `error` field rather than scraping stderr.
297
+
298
+ ## Deferred to a follow-up
299
+
300
+ `src/install/native/native-path-pick-infer.ts` still hardcodes per-target dir prefixes for 8 targets (`gemini-cli`, `claude-code`, `cursor`, `copilot`, `windsurf`, `cline`, `continue`, `junie`, `codex-cli`). The descriptor-driven refactor touches all 27 builtin descriptors plus the three bespoke-layout targets and is tracked separately in `tasks/todo.md`. The file remains in the coverage exclude list with a `category 5` comment until refactored.
301
+
302
+ - 7cd2c3e: refactor(install): make the skill-pack aggregator delegate per-tool command reads to that target's importer mapper
303
+
304
+ `mergeCommands` (the Anthropic skill-pack aggregator's command merger) previously routed every per-tool directory through the canonical `.md`-only parser. That worked for `.claude/commands/` (Claude Code commands are Markdown) but silently dropped `.gemini/commands/*.toml` because Gemini CLI's slash-command format is TOML, not Markdown — even though the gemini-cli target descriptor already ships a TOML-aware mapper (`mapGeminiCommandFile` + `geminiCommandMapper`).
305
+
306
+ The aggregator now treats the `target` field on each `CommandMergeSpec` as load-bearing: when set and the target ships a directory-mode command importer with non-`.md` extensions, the aggregator delegates non-`.md` reads to that target's mapper (via the new `readToolNativeCommands`). Markdown files keep going through the canonical reader so dedup metadata keeps pointing at the upstream `.gemini/commands/foo.md`-style path.
307
+
308
+ Knock-on cleanups:
309
+ - New `src/install/importers/target-native-commands.ts` — single owner of "read a tool-native command directory through that tool's mapper". Both the descriptor-driven full install (`runDescriptorImport`) and the skill-pack aggregator now share this seam, so adding a new target whose commands aren't Markdown means writing one mapper in that target's descriptor — no aggregator change.
310
+ - `aggregateAnthropicSkillPack` now returns a `cleanup` callback that removes any temp staging directories created by per-target mappers. Wired through the existing `prep.cleanup` lifecycle so `runSinglePackInstall`'s `finally` block runs it after pack materialization.
311
+ - `parseCommands` learns a `handledByOtherReader` option so the canonical reader's "skipped N command file(s)" warning is suppressed for extensions another reader (the per-target mapper) actually consumes.
312
+
313
+ Side effect on `addyosmani/agent-skills` and similar multi-tool skill packs: the seven `.gemini/commands/*.toml` files now merge into the canonical command set alongside Claude's `.md` commands instead of triggering the "Skipped 7 commands file(s) … format: .toml" warning. Verified end-to-end: `installs list` shows `8 commands` (4 Claude + 7 Gemini deduped on basename collision), and the previously-dropped Gemini commands are present in `pack/commands/`.
314
+
315
+ ### Patch Changes
316
+
317
+ - 4c39bd4: fix(install): compound `.md` extensions (e.g. `.agent.md`) stay on the canonical reader
318
+
319
+ `hasNonMdEntityMapper` and friends in
320
+ `src/install/importers/target-native-commands.ts` previously asked
321
+ `ext !== '.md'` to decide whether a target's extension was
322
+ "non-Markdown". That treated Copilot's `.agent.md` (a Markdown
323
+ sub-extension Copilot uses to mark agent files) as non-Markdown and
324
+ routed those files through Copilot's importer mapper **in addition**
325
+ to the canonical reader. For any repo containing `foo.agent.md`, two
326
+ canonical agents were emitted: one slugged `foo.agent` (canonical
327
+ read) and one slugged `foo` (Copilot mapper). Surfaced during the
328
+ `VoltAgent/awesome-claude-code-subagents` compatibility sweep when
329
+ the same input started producing two extra agents per `.agent.md`
330
+ file.
331
+
332
+ Now uses `ext.toLowerCase().endsWith('.md')`, so any `.<sub>.md`
333
+ compound stays on the canonical reader and the seam only fires for
334
+ genuinely non-Markdown formats (`.toml`, `.mdc`, `.yaml`, `.json`).
335
+ Pinned by a regression test in
336
+ `tests/unit/install/importers/target-native-commands-plugin.test.ts`
337
+ ("compound .md extensions ... stay on the canonical reader — no
338
+ double-counting"), plus the existing per-kind plugin tests for
339
+ `.yaml`-extension plugins still pass unchanged.
340
+
341
+ Also includes the dedup-key change from the same commit: entities are
342
+ now deduped by source-file basename slug (matches the canonical
343
+ parser's `basename(path, '.md')` convention). Required because
344
+ `CanonicalRule` doesn't carry a `name` field — the prior
345
+ `entity.name`-keyed `Map` would collapse every rule into a single
346
+ entry. Fixes a separate latent issue on rule installs.
347
+
348
+ - a3e5686: fix(install): skip preserved boilerplate (README/LICENSE/NOTICE/COPYING/COPYRIGHT) in native descriptor import
349
+
350
+ Native-import directory mode (`runDirectory` in `descriptor-import-runner.ts`) previously materialized every `*.md` under `.claude/agents/`, `.claude/commands/`, and `.claude/rules/` as a canonical entity. Repos that ship folder-level documentation alongside content (e.g. `.claude/agents/README.md` and `.claude/agents/external/README.md` in `qdhenry/Claude-Command-Suite`) tripped the basename-slug collision check at parse time and hard-failed the install. The runner now consults `isPreservedBoilerplate` and silently drops those files — matching the existing filter applied via `entity-importers.ts` in the install-discovery path. Noise stems (`security`, `contributing`, …) are intentionally left through so user-authored rules like `.claude/rules/security.md` continue to import.
351
+
352
+ - fc3ec85: fix(install): surface recovery flags in every "no installable resources" error and document the auto-detect → flag fallback chain
353
+
354
+ `agentsmesh install <source>` runs the classifier first and falls back to user-supplied flags (`--path`, `--as`, `--target`, `--all`) when auto-detection refuses a source or can't disambiguate it. Three error paths used to dead-end without naming those flags, leaving the user stuck:
355
+ - `No installable files found under <path> for manual install` — now also says: _Try a different `--path`, or omit `--as` to let agentsmesh auto-detect the layout._
356
+ - `No installable native resources found under "<path>" for target "<id>"` (both call sites) — now also says: _Try `--path <dir>` without `--target` for auto-detection, or `--as <kind>` for a flat-collection override._
357
+ - `No installable resources after skipping invalid files (N): …` — now also says: _Fix the frontmatter in the source files (most often: unquoted scalars with embedded colons or square brackets), or narrow `--path` to a subdirectory that excludes them._
358
+
359
+ The `agentsmesh install --help` description now spells out the precedence — auto-classify first, then `--path` / `--as` / `--target` / `--all` to override — instead of just listing flags alphabetically.
360
+
361
+ Regression tests pin the flag names (not literal phrasing) so the contract stays visible even if future copy-edits rework the sentences.
362
+
363
+ - 4c39bd4: fix(reference): classify `(filename)` prose as bare-prose, not a Markdown link destination
364
+
365
+ `shouldRewritePathToken`'s `(`-branch in
366
+ `src/core/reference/link-token-context.ts` unconditionally treated any
367
+ token preceded by `(` as a Markdown link destination, regardless of
368
+ whether the `[label]` prefix was actually present. Prose mentions
369
+ like `Read the existing spec (SPEC.md or equivalent)` were routed to
370
+ the link-rewrite path; the rebaser then resolved `SPEC.md` against
371
+ the canonical pack's `commands/` dir (case-insensitive on macOS APFS
372
+ / Windows NTFS) and emitted `(../../.agentsmesh/.../SPEC.md or
373
+ equivalent)` into every generated `.claude/commands/`,
374
+ `.gemini/commands/`, `.cursor/commands/` artifact (etc.). The leaked
375
+ path was wrong even by intent — the author meant the filename as a
376
+ documentary mention, not a link target.
377
+
378
+ The matching guard in `getTokenContext` (same file, line 64) already
379
+ encodes the correct rule: a token is `markdown-link-dest` only when
380
+ `]` sits directly before the `(`. This change propagates that rule
381
+ into `shouldRewritePathToken`:
382
+ - With `]` immediately before `(` → real Markdown link, accept any
383
+ terminator (`)`, `#`, `?`, space, tab) — `[text](spec.md)`,
384
+ `[text](spec.md#anchor)`, etc. continue to rewrite cleanly.
385
+ - Without it → fall through to the bare-token path-shape checks, so
386
+ genuine paths inside parens (`(./commands/spec.md)`,
387
+ `(.claude/skills/foo.md)`) still rewrite via the slash /
388
+ root-relative branches, while bare filenames like
389
+ `(SPEC.md or equivalent)` stay verbatim.
390
+
391
+ Verified end-to-end by regenerating against the
392
+ `addyosmani-agent-skills` pack: `(SPEC.md or equivalent)` is now
393
+ preserved in `.gemini/commands/planning.toml`,
394
+ `.claude/commands/planning.md`, `.cursor/commands/planning.md` and the
395
+ 24 other targets. The same rule fires identically for `.md`, `.mdc`,
396
+ and `.toml` outputs — the engine is format-agnostic; only the
397
+ classifier's prose-vs-link distinction needed tightening.
398
+
399
+ Tests:
400
+ - New `tests/unit/core/link-token-classifier-prose-vs-md-link.test.ts`
401
+ (8 cases) pins the prose-vs-link distinction.
402
+ - `tests/unit/core/link-rebaser-deep-branches.test.ts:396` updated:
403
+ positive cases now require `](` prefix; a new sibling case pins the
404
+ negative behavior for prose forms.
405
+
3
406
  ## 0.18.1
4
407
 
5
408
  ### Patch Changes
package/README.md CHANGED
@@ -232,7 +232,9 @@ agentsmesh watch [--global] [--targets <csv>]
232
232
  agentsmesh check [--global]
233
233
  agentsmesh merge [--global]
234
234
  agentsmesh matrix [--global] [--targets <csv>] [--verbose]
235
- agentsmesh install <source> [--sync] [--path <dir>] [--target <id>] [--as <kind>] [--name <id>] [--extends] [--dry-run] [--global] [--force]
235
+ agentsmesh install <source> [--sync] [--path <dir>] [--target <id>] [--as <kind>] [--name <id>] [--extends] [--all] [--dry-run] [--global] [--force]
236
+ agentsmesh uninstall <name>[,<name>...] [--all] [--keep-pack] [--keep-generated] [--dry-run] [--global] [--force]
237
+ agentsmesh installs list [--global]
236
238
  agentsmesh plugin add|list|remove|info [--version <v>] [--id <id>]
237
239
  agentsmesh target scaffold <id> [--name <displayName>] [--force]
238
240
  ```
@@ -293,10 +295,24 @@ Install shared skills, rules, agents, and commands from any git repo:
293
295
  ```bash
294
296
  agentsmesh install github:org/shared-config@v1.0.0
295
297
  agentsmesh install --path rules --as rules github:team/standards
298
+ agentsmesh install github:team/prompts --path workflows --as commands --extends
296
299
  agentsmesh install --sync # restore all packs after clone
297
300
  ```
298
301
 
299
- Packs live in `.agentsmesh/packs/`, track in `installs.yaml`, and merge into canonical config on every `generate`.
302
+ Packs live in `.agentsmesh/packs/`, track in `installs.yaml`, and merge into canonical config on every `generate`. `install --extends` records a linked `extends:` entry instead of materializing a pack; when paired with `--as`, the forced kind is persisted as `extends[].as` so flat markdown directories continue to load as commands, agents, rules, or skills during later `generate` runs. Anthropic-style skill packs (root `skills/`, `agents/`, `references/`, `.claude/commands/`, …) are auto-detected by a multi-signal classifier and imported as a bulk set in a single command — no `--as` needed. The discriminator is strict enough that legacy tool-native and canonical-agentsmesh repos still take their original code paths (verified by 5 backcompat fixtures).
303
+
304
+ List and remove installed packs:
305
+
306
+ ```bash
307
+ agentsmesh installs list # NAME / SOURCE / FEATURES / INSTALLED table
308
+ agentsmesh uninstall <name> # rm pack dir, drop installs.yaml/extends entry, clean generated outputs
309
+ agentsmesh uninstall --all # sweep every install in this scope
310
+ agentsmesh uninstall <name> --keep-pack # only drop yaml entries; leave .agentsmesh/packs/<name>/ on disk
311
+ agentsmesh uninstall <name> --keep-generated # skip the final generate; emit a warning about stale target files
312
+ agentsmesh uninstall <name> --dry-run # preview; no writes
313
+ ```
314
+
315
+ Each install writes `.agentsmesh-install-manifest.json` next to the pack with per-file sha256 hashes; uninstall compares current contents against that manifest and prompts before deleting locally-modified files. `--force` accepts the documented defaults (bulk = accept all, broken-link = leave-with-warnings, modified = delete-anyway). `.agentsmesh/.install.lock` serialises install/uninstall so concurrent runs on the same project fail fast rather than racing on disk.
300
316
 
301
317
  ### What to commit and what to gitignore
302
318
 
@@ -330,6 +346,15 @@ Every config file ships with a generated JSON Schema, so VS Code, JetBrains, and
330
346
 
331
347
  `agentsmesh init` writes the appropriate `# yaml-language-server: $schema=...` directive (or `$schema` field for JSON) into each canonical file, so editors pick up validation immediately.
332
348
 
349
+ ### Environment variables
350
+
351
+ | Variable | Default | Description |
352
+ |---|---|---|
353
+ | `AGENTSMESH_GITHUB_TOKEN` | — | GitHub personal access token for private repo installs and `extends`. |
354
+ | `AGENTSMESH_CACHE` | `~/.agentsmeshcache` | Override the remote-extends / tarball cache directory. |
355
+ | `AGENTSMESH_MAX_TARBALL_MB` | `500` | Maximum GitHub tarball size in MiB the install command will accept. Allowed range: `1`–`4096`. Increase this when installing from large monorepos. |
356
+ | `AGENTSMESH_STRICT_PLUGINS` | `0` | When set to `1`, a failed plugin descriptor import fails the build instead of warning-and-skip. Useful in CI where a missing plugin target is a regression. |
357
+
333
358
  ---
334
359
 
335
360
  ## TypeScript / Programmatic API
@@ -432,7 +457,7 @@ See the [full feature matrix docs](https://samplexbro.github.io/agentsmesh/refer
432
457
 
433
458
  - **[Getting Started](https://samplexbro.github.io/agentsmesh/getting-started/installation/)** — installation, quick start
434
459
  - **[Canonical Config](https://samplexbro.github.io/agentsmesh/canonical-config/)** — rules, commands, agents, skills, MCP, hooks, ignore, permissions
435
- - **[CLI Reference](https://samplexbro.github.io/agentsmesh/cli/)** — `init`, `generate`, `import`, `convert`, `install`, `diff`, `lint`, `watch`, `check`, `merge`, `matrix`, `plugin`, `target`
460
+ - **[CLI Reference](https://samplexbro.github.io/agentsmesh/cli/)** — `init`, `generate`, `import`, `convert`, `install`, `uninstall`, `installs`, `diff`, `lint`, `watch`, `check`, `merge`, `matrix`, `plugin`, `target`
436
461
  - **[Configuration](https://samplexbro.github.io/agentsmesh/configuration/agentsmesh-yaml/)** — `agentsmesh.yaml`, local overrides, extends, collaboration, conversions
437
462
  - **[Guides](https://samplexbro.github.io/agentsmesh/guides/existing-project/)** — adopting in existing projects · multi-tool teams · sharing config · CI drift detection · community packs · **building plugins**
438
463
  - **[Reference](https://samplexbro.github.io/agentsmesh/reference/generation-pipeline/)** — supported tools matrix · generation pipeline · managed embedding
@@ -1,7 +1,33 @@
1
- import { b as CanonicalFiles, V as ValidatedConfig } from './schema-B4PYQrZH.js';
2
- export { C as CanonicalAgent, a as CanonicalCommand, c as CanonicalRule, d as CanonicalSkill, H as HookEntry, e as Hooks, I as IgnorePatterns, M as McpConfig, f as McpServer, P as Permissions, S as SkillSupportingFile, g as StdioMcpServer, U as UrlMcpServer } from './schema-B4PYQrZH.js';
1
+ import { b as CanonicalFiles, V as ValidatedConfig } from './schema-CLmR2JOb.js';
2
+ export { C as CanonicalAgent, a as CanonicalCommand, c as CanonicalRule, d as CanonicalSkill, H as HookEntry, e as Hooks, I as IgnorePatterns, M as McpConfig, f as McpServer, P as Permissions, S as SkillSupportingFile, g as StdioMcpServer, U as UrlMcpServer } from './schema-CLmR2JOb.js';
3
3
  import 'zod';
4
4
 
5
+ interface UnrecognizedFormatsWarningOptions {
6
+ /**
7
+ * Extensions to exclude from the warning because another reader handles
8
+ * them downstream (e.g. the gemini-cli command mapper parses `.toml` from
9
+ * `.gemini/commands/`). Without this, the canonical reader's warning would
10
+ * fire even when the alternate format is actually being installed.
11
+ *
12
+ * Lowercase, dot-prefixed (e.g. `'.toml'`).
13
+ */
14
+ readonly handledByOtherReader?: ReadonlySet<string>;
15
+ }
16
+
17
+ /**
18
+ * Parse .agentsmesh/rules/*.md into CanonicalRule objects.
19
+ */
20
+
21
+ interface ParseFrontmatterOptions extends UnrecognizedFormatsWarningOptions {
22
+ /**
23
+ * When supplied, frontmatter parse failures invoke the callback and the
24
+ * offending file is skipped instead of aborting the whole parse. Used by
25
+ * the install path to keep the run going through third-party content with
26
+ * malformed YAML. Strict callers (`generate`/`lint`/`check`) leave it unset.
27
+ */
28
+ onParseError?: (err: Error, filePath: string) => void;
29
+ }
30
+
5
31
  /**
6
32
  * Load all canonical files from a canonical directory into CanonicalFiles.
7
33
  */
@@ -11,9 +37,11 @@ import 'zod';
11
37
  * Missing directories/files yield empty arrays or null as per each parser.
12
38
  *
13
39
  * @param canonicalDirOrProjectRoot - Absolute path to canonical directory or project root
40
+ * @param opts - Optional `onParseError` callback for lenient install-time loads.
41
+ * Strict callers (`generate`/`lint`/`check`) leave it unset.
14
42
  * @returns CanonicalFiles with all parsed data
15
43
  */
16
- declare function loadCanonicalFiles(canonicalDirOrProjectRoot: string): Promise<CanonicalFiles>;
44
+ declare function loadCanonicalFiles(canonicalDirOrProjectRoot: string, opts?: ParseFrontmatterOptions): Promise<CanonicalFiles>;
17
45
 
18
46
  /**
19
47
  * Public API — canonical loading (package.json "exports"."./canonical").