Skip to content

feat: mirror opencode skills into .cursor/skills/ for Cursor agent discovery - #90

Merged
justin-carper merged 6 commits into
mainfrom
feat/skills-bridge
Aug 3, 2026
Merged

feat: mirror opencode skills into .cursor/skills/ for Cursor agent discovery#90
justin-carper merged 6 commits into
mainfrom
feat/skills-bridge

Conversation

@justin-carper

Copy link
Copy Markdown
Collaborator

Supersedes #78. Wayne's original commit (cee7d16) is carried over unmodified,
with authorship preserved; the three commits on top resolve the review feedback
left on that PR.

Summary

Mirrors opencode's resolved skills into <cwd>/.cursor/skills/ so the Cursor
agent (and Cursor sub-agents) discover and load them natively via the project
settings layer. Skills are discovered from the standard opencode locations plus
config.skills.paths, filtered through the live permission config, and
materialised as a git-ignored mirror stamped with a generated: opencode-cursor
sentinel. Only sentinel-bearing files are ever overwritten or deleted, so
user-owned .cursor/skills/<id>/ directories are safe. An <available_skills>
catalogue is appended to the generated system rule, and the mirror re-syncs each
turn behind a hash gate. Full detail in #78.

Review fixes on top

de89036fix(skills): follow symlinked skill dirs and files

Dirent.isDirectory() returns false for a symlink pointing at a directory, so
a skill linked in from a shared checkout was dropped from the mirror with no
warning. Symlinked supporting files were lost the same way in collectFiles and
copyTree. Entries are now classified through the link target via a shared
entryKind helper, with a resolved-path visited set guarding the cycles that
following links admits. Broken symlinks and links to non-regular files are still
rejected. Six tests added, three of which were confirmed red before the fix.

053e4ebdocs(skills): correct deny scope, note plugin-skill gap

skills.include is checked before permissions resolve, so it re-admits a denied
skill by design. SECURITY.md read as though deny were absolute — corrected,
since that is the file someone reads when deciding whether deny is a hard
guarantee.

Skills bundled inside opencode plugin packages land in the package cache
(~/.cache/opencode/packages/<pkg>/node_modules/<pkg>/skills/), which is not a
scanned location, and @opencode-ai/sdk exposes no skills API to resolve them.
Now documented in the README limitations list with the config.skills.paths
workaround, so a user does not quietly receive a subset of their skills.

fb44fb6style(plugin): normalize indentation in skills bridge blocks

index.ts block re-tabbed to the surrounding level, de-dented closing braces
corrected, and tabs in the otherwise space-indented cursor-tools.ts delegate
call converted to spaces.

Verification

npm run typecheck && npm test && npm run build

typecheck: clean
Test Files  32 passed (32)
     Tests  409 passed (409)
build:     ESM + DTS success

Limitations

  • config.skills.urls (HTTP catalogs) are not mirrored.
  • Skills bundled inside opencode plugin packages are not discovered.
  • cursor_cloud_agent targets a remote repo and does not inherit skills.
  • cursor_delegate with a non-session cwd does not mirror into that cwd, but
    passes settingSources: ["project"] so a pre-existing mirror there loads.

WayneSimpson and others added 6 commits July 26, 2026 14:22
…scovery

Discover opencode's resolved skills (project + global + config.skills.paths),
filter through permission config, and materialise them as a git-ignored mirror
in <cwd>/.cursor/skills/ with a 'generated: opencode-cursor' sentinel. An
<available_skills> catalogue is appended to the generated system rule so the
Cursor agent can discover and load skills on demand.

- New: src/plugin/skill-discovery.ts — filesystem walk, frontmatter parsing,
  permission filtering (map-form + rule-array), extraPaths, path expansion,
  skillSetHash including all files (mtime + size).
- New: src/provider/skill-mirror.ts — materialisation with sentinel, git-ignore,
  idempotent writes, stale-dir pruning, user-owned protection, per-file 1MB
  skip, total 10MB cap, removeSkillMirror for dispose, buildSkillsCatalogue.
- Updated: src/provider/system-rule.ts — writeSystemRule and
  resolveSystemDelivery accept optional skillsCatalogue appended to rule body.
- Updated: src/plugin/index.ts — forwardSkills option (default true),
  skills.include/exclude override, materialisation in config hook, live
  re-sync in chat.params hook (hash-gated), currentSkillsCatalogue always
  forwarded per turn, removeSkillMirror in dispose.
- Updated: src/provider/language-model.ts — dynamic + static catalogue
  resolution (no self-provisioning — respects forwardSkills:false).
- Updated: src/provider/delegate.ts + src/plugin/cursor-tools.ts —
  settingSources: ['project'] passed to delegate's acquireAgent.
- Updated: src/provider/index.ts — skillsCatalogue in provider options.
- Tests: 403 passing (25 skill-discovery, 14 skill-mirror, 10 plugin-skill-
  mirror, 6 catalogue in language-model-system, plus existing).
- Docs: README.md (Skills section), CHANGELOG.md ([Unreleased]),
  SECURITY.md (skills mirror threat model).
- .gitignore: exclude .cursor/ (plugin-generated runtime artifacts).
Dirent.isDirectory() is false for a symlink pointing at a directory, so
a skill linked in from a shared checkout was dropped from the mirror
with no warning. Symlinked supporting files were lost the same way.

Entries are now classified through the link target, with a resolved-path
visited set guarding the cycles that following links admits.
skills.include is checked before permissions resolve, so it re-admits a
denied skill by design — SECURITY.md read as though deny were absolute.

Skills bundled in opencode plugin packages live in the package cache,
which is not a scanned location, and the SDK exposes no skills API to
resolve them. Documented so the omission is visible rather than silent.
Two conflicts, both resolved to keep each side's intent:

- language-model.ts: #85/#86 moved the resolveSystemDelivery call inside
  the withSessionLock callback. The skills-catalogue lookup was re-placed
  into that relocated call rather than restored to its old position.
- CHANGELOG.md: the skills-bridge entry stays under [Unreleased]; main's
  0.6.2 and 0.7.0 release sections are kept in full.
0.7.0 (#85) moved the plugin's own diagnostics off console.* and onto
opencode's structured logging API. The skills bridge predated that and
still wrote to the terminal, leaving the only console.* calls in src/.

Withheld skills and the mirror-failure path now pass structured extra
fields instead of pre-formatted strings, so the reason codes are
queryable rather than embedded in a message.

Also corrects the plugin-cache path in the README limitation: it is
platform-dependent, as PLUGIN_CACHE_PATH shows, and was documented as
POSIX-only.
@justin-carper
justin-carper merged commit 49ed903 into main Aug 3, 2026
6 checks passed
@justin-carper
justin-carper deleted the feat/skills-bridge branch August 3, 2026 17:03
justin-carper added a commit that referenced this pull request Aug 3, 2026
The 0.7.1-next.0 section shipped three problems.

- The skills bridge (#90) named no contributor. Wayne Simpson authored the
  original implementation commit; the release notes credited only the PR
  opener, and the Co-authored-by trailer in the squashed commit was the
  sole record.
- The #89 entry said src/model-limits.ts is "regenerated on schedule". The
  weekly model-data-drift job runs --check and fails on staleness; it never
  writes. Regenerating is manual. It also named a "models generator CLI"
  that does not exist -- the script is npm run sync:model-limits -- and
  carried CI-internal detail (import-purity, stdout capture) that is
  invisible to users.
- #88's dev-dependency bumps had no entry, breaking the convention every
  prior release follows.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants