opencode-agent-skills-md 1.0.0 → 1.1.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.
Files changed (129) hide show
  1. package/dist/cli.mjs +770 -0
  2. package/dist/plugin.mjs +1138 -0
  3. package/dist/src/cli/config.d.ts +144 -0
  4. package/dist/src/cli/install.d.ts +33 -0
  5. package/dist/src/cli/main.d.ts +11 -0
  6. package/dist/src/cli/real-fs.d.ts +6 -0
  7. package/dist/src/cli/status.d.ts +34 -0
  8. package/dist/src/cli/uninstall.d.ts +22 -0
  9. package/dist/src/host.d.ts +51 -0
  10. package/dist/src/index.d.ts +17 -0
  11. package/dist/src/plugin.d.ts +35 -0
  12. package/dist/src/sdk.d.ts +51 -0
  13. package/dist/src/tools.d.ts +86 -0
  14. package/package.json +48 -18
  15. package/{packages/opencode-agent-skills-md/src → src}/cli/main.ts +20 -4
  16. package/.beads/.local_version +0 -1
  17. package/.beads/README.md +0 -81
  18. package/.beads/config.yaml +0 -61
  19. package/.beads/deletions.jsonl +0 -1
  20. package/.beads/issues.jsonl +0 -64
  21. package/.beads/metadata.json +0 -4
  22. package/.gitattributes +0 -3
  23. package/.github/CODEOWNERS +0 -1
  24. package/.github/copilot-instructions.md +0 -78
  25. package/.github/dependabot.yml +0 -13
  26. package/.github/workflows/release.yml +0 -51
  27. package/.opencode/command/test-compaction.md +0 -9
  28. package/.opencode/command/test-find-skills.md +0 -7
  29. package/.opencode/command/test-read-skill-file.md +0 -14
  30. package/.opencode/command/test-run-skill-script.md +0 -13
  31. package/.opencode/command/test-skills.md +0 -14
  32. package/.opencode/command/test-use-skill.md +0 -10
  33. package/.opencode/skills/git-helper/SKILL.md +0 -65
  34. package/.opencode/skills/test-skill/SKILL.md +0 -43
  35. package/.opencode/skills/test-skill/example-config.json +0 -16
  36. package/.opencode/skills/test-skill/helper-docs.md +0 -29
  37. package/.opencode/skills/test-skill/scripts/echo-args +0 -14
  38. package/.opencode/skills/test-skill/scripts/greet +0 -6
  39. package/AGENTS.md +0 -43
  40. package/CHANGELOG.md +0 -178
  41. package/Justfile +0 -39
  42. package/README.md +0 -189
  43. package/openspec/changes/archive/2026-06-14-skills-core-decouple/specs/core-decoupling/spec.md +0 -74
  44. package/openspec/changes/archive/2026-06-14-skills-core-decouple/tasks.md +0 -64
  45. package/openspec/changes/archive/2026-06-14-skills-core-decouple/verify-report.md +0 -75
  46. package/openspec/changes/archive/2026-06-17-fix-skill-loading-regression/apply-progress.md +0 -136
  47. package/openspec/changes/archive/2026-06-17-fix-skill-loading-regression/archive-report.md +0 -77
  48. package/openspec/changes/archive/2026-06-17-fix-skill-loading-regression/design.md +0 -89
  49. package/openspec/changes/archive/2026-06-17-fix-skill-loading-regression/proposal.md +0 -65
  50. package/openspec/changes/archive/2026-06-17-fix-skill-loading-regression/specs/core-decoupling/spec.md +0 -77
  51. package/openspec/changes/archive/2026-06-17-fix-skill-loading-regression/tasks.md +0 -65
  52. package/openspec/changes/archive/2026-06-17-fix-skill-loading-regression/verify-report.md +0 -165
  53. package/openspec/specs/core-decoupling/spec.md +0 -110
  54. package/packages/core/package.json +0 -30
  55. package/packages/core/src/content.d.ts +0 -16
  56. package/packages/core/src/content.ts +0 -30
  57. package/packages/core/src/debug.ts +0 -16
  58. package/packages/core/src/discovery.d.ts +0 -86
  59. package/packages/core/src/discovery.ts +0 -257
  60. package/packages/core/src/index.d.ts +0 -20
  61. package/packages/core/src/index.ts +0 -55
  62. package/packages/core/src/match.d.ts +0 -19
  63. package/packages/core/src/match.ts +0 -75
  64. package/packages/core/src/parse.d.ts +0 -26
  65. package/packages/core/src/parse.ts +0 -141
  66. package/packages/core/src/scripts.d.ts +0 -17
  67. package/packages/core/src/scripts.ts +0 -79
  68. package/packages/core/src/search.d.ts +0 -83
  69. package/packages/core/src/search.ts +0 -188
  70. package/packages/core/src/types.d.ts +0 -82
  71. package/packages/core/src/types.ts +0 -131
  72. package/packages/core/src/walk.ts +0 -109
  73. package/packages/core/tests/agnostic.test.ts +0 -346
  74. package/packages/core/tests/content.test.ts +0 -65
  75. package/packages/core/tests/discovery.test.ts +0 -370
  76. package/packages/core/tests/package-boundary.test.ts +0 -310
  77. package/packages/core/tests/parse-trigger.test.ts +0 -282
  78. package/packages/core/tests/search.test.ts +0 -374
  79. package/packages/core/tests/subpath.test.ts +0 -87
  80. package/packages/core/tsconfig.json +0 -10
  81. package/packages/opencode-agent-skills-md/package.json +0 -42
  82. package/packages/opencode-agent-skills-md/rolldown.config.js +0 -48
  83. package/packages/opencode-agent-skills-md/tests/cli-commands.test.ts +0 -1423
  84. package/packages/opencode-agent-skills-md/tests/e2e/startup-smoke.test.ts +0 -66
  85. package/packages/opencode-agent-skills-md/tests/fixtures/skills/home/.claude/skills/claude-user-only-skill/SKILL.md +0 -8
  86. package/packages/opencode-agent-skills-md/tests/fixtures/skills/home/.config/opencode/skills/shared-skill/SKILL.md +0 -8
  87. package/packages/opencode-agent-skills-md/tests/fixtures/skills/home/.config/opencode/skills/user-only-skill/SKILL.md +0 -8
  88. package/packages/opencode-agent-skills-md/tests/fixtures/skills/project/.claude/skills/claude-project-only-skill/SKILL.md +0 -8
  89. package/packages/opencode-agent-skills-md/tests/fixtures/skills/project/.opencode/skills/go-tester/SKILL.md +0 -12
  90. package/packages/opencode-agent-skills-md/tests/fixtures/skills/project/.opencode/skills/nested/team/nested-skill/SKILL.md +0 -8
  91. package/packages/opencode-agent-skills-md/tests/fixtures/skills/project/.opencode/skills/rust-tester/SKILL.md +0 -11
  92. package/packages/opencode-agent-skills-md/tests/fixtures/skills/project/.opencode/skills/scripted-skill/SKILL.md +0 -8
  93. package/packages/opencode-agent-skills-md/tests/fixtures/skills/project/.opencode/skills/scripted-skill/bin/echo.sh +0 -2
  94. package/packages/opencode-agent-skills-md/tests/fixtures/skills/project/.opencode/skills/scripted-skill/docs/reference.md +0 -1
  95. package/packages/opencode-agent-skills-md/tests/fixtures/skills/project/.opencode/skills/shared-skill/SKILL.md +0 -8
  96. package/packages/opencode-agent-skills-md/tests/fixtures/skills/project/.opencode/skills/using-superpowers/SKILL.md +0 -8
  97. package/packages/opencode-agent-skills-md/tests/integration/helpers/mock-opencode.ts +0 -114
  98. package/packages/opencode-agent-skills-md/tests/integration/plugin.test.ts +0 -316
  99. package/packages/opencode-agent-skills-md/tests/integration/skill-discovery.test.ts +0 -315
  100. package/packages/opencode-agent-skills-md/tests/opencode/host.test.ts +0 -179
  101. package/packages/opencode-agent-skills-md/tests/opencode/plugin.test.ts +0 -551
  102. package/packages/opencode-agent-skills-md/tests/opencode/subpath.test.ts +0 -66
  103. package/packages/opencode-agent-skills-md/tests/opencode/tools.test.ts +0 -213
  104. package/packages/opencode-agent-skills-md/tests/package-boundary.test.ts +0 -346
  105. package/packages/opencode-agent-skills-md/tests/tools-security.test.ts +0 -72
  106. package/packages/opencode-agent-skills-md/tsconfig.build.json +0 -11
  107. package/packages/opencode-agent-skills-md/tsconfig.json +0 -10
  108. package/plans/001-ci-gate.md +0 -177
  109. package/plans/002-is-path-safe.md +0 -243
  110. package/plans/003-escape-prompts.md +0 -310
  111. package/plans/004-test-security-paths.md +0 -228
  112. package/plans/005-stop-swallowing-errors.md +0 -246
  113. package/plans/006-preserve-jsonc-commas.md +0 -144
  114. package/plans/007-write-before-purge.md +0 -144
  115. package/plans/008-reuse-walkdir-for-list-skill-files.md +0 -164
  116. package/plans/README.md +0 -43
  117. package/pnpm-workspace.yaml +0 -6
  118. package/tests/workspace.test.ts +0 -367
  119. package/tsconfig.json +0 -15
  120. /package/{packages/opencode-agent-skills-md/src → src}/cli/config.ts +0 -0
  121. /package/{packages/opencode-agent-skills-md/src → src}/cli/install.ts +0 -0
  122. /package/{packages/opencode-agent-skills-md/src → src}/cli/real-fs.ts +0 -0
  123. /package/{packages/opencode-agent-skills-md/src → src}/cli/status.ts +0 -0
  124. /package/{packages/opencode-agent-skills-md/src → src}/cli/uninstall.ts +0 -0
  125. /package/{packages/opencode-agent-skills-md/src → src}/host.ts +0 -0
  126. /package/{packages/opencode-agent-skills-md/src → src}/index.ts +0 -0
  127. /package/{packages/opencode-agent-skills-md/src → src}/plugin.ts +0 -0
  128. /package/{packages/opencode-agent-skills-md/src → src}/sdk.ts +0 -0
  129. /package/{packages/opencode-agent-skills-md/src → src}/tools.ts +0 -0
@@ -0,0 +1,144 @@
1
+ /** npm package name for this plugin. */
2
+ export declare const PLUGIN_NAME = "opencode-agent-skills-md";
3
+ /** Maximum number of CLI-created backups retained in the config directory. */
4
+ export declare const BACKUP_LIMIT = 3;
5
+ /** Filename used for the OpenCode global config (preferred). */
6
+ export declare const CONFIG_FILE_BASENAME = "opencode";
7
+ /** Subdirectory under the user config root that holds `opencode.json`. */
8
+ export declare const OPENCODE_CONFIG_SUBDIR = "opencode";
9
+ /**
10
+ * Minimal synchronous filesystem surface that the `oas` CLI commands rely
11
+ * on. The CLI never imports `node:fs` directly — every disk operation
12
+ * goes through this boundary so tests can run fully in memory.
13
+ *
14
+ * The methods intentionally mirror only the operations the CLI needs:
15
+ * helpers that demand more (e.g. `rmSync` for recursive directory
16
+ * removal during uninstall purge) live outside this interface and call
17
+ * `node:fs` directly at the call site, where they belong.
18
+ */
19
+ export interface CliFs {
20
+ readFileSync(path: string): string;
21
+ writeFileSync(path: string, content: string): void;
22
+ renameSync(from: string, to: string): void;
23
+ copyFileSync(from: string, to: string): void;
24
+ unlinkSync(path: string): void;
25
+ mkdirSync(path: string, opts?: {
26
+ recursive?: boolean;
27
+ }): void;
28
+ readdirSync(path: string): string[];
29
+ existsSync(path: string): boolean;
30
+ }
31
+ export interface ResolvedConfigPath {
32
+ /** Absolute path to use for reads/writes. `.json` by default. */
33
+ path: string;
34
+ /** "json" when the resolved file ends in `.json`, "jsonc" otherwise. */
35
+ format: "json" | "jsonc";
36
+ /** True when `path` already existed on disk before resolution. */
37
+ existed: boolean;
38
+ }
39
+ /**
40
+ * Resolve the parent directory that holds the global OpenCode config.
41
+ * `$OPENCODE_CONFIG_DIR` wins; otherwise we fall back to
42
+ * `$HOME/.config/opencode` (or `os.homedir()` as a last-resort).
43
+ *
44
+ * Exposed separately so tests and the rotation helper can reuse it
45
+ * without re-deriving the precedence rules.
46
+ */
47
+ export declare const resolveConfigDir: (env?: NodeJS.ProcessEnv) => string;
48
+ /**
49
+ * Resolve the global OpenCode config file path.
50
+ *
51
+ * Precedence:
52
+ * 1. If `$OPENCODE_CONFIG_DIR/opencode.json` exists, use it.
53
+ * 2. Else if `$OPENCODE_CONFIG_DIR/opencode.jsonc` exists, use it.
54
+ * 3. Else fall back to `$HOME/.config/opencode/opencode.json`, then `.jsonc`.
55
+ * 4. If nothing exists, return the preferred target `.json` in the
56
+ * resolved directory so `install` knows where to create the file.
57
+ *
58
+ * `.json` always wins over `.jsonc` when both exist.
59
+ */
60
+ export declare const resolveGlobalConfigPath: (fs: CliFs, env?: NodeJS.ProcessEnv) => ResolvedConfigPath;
61
+ /**
62
+ * Strip JSONC-style comments and trailing commas, then parse with `JSON.parse`.
63
+ *
64
+ * Returns `{}` for empty input or whitespace-only input.
65
+ * **Throws** on malformed JSON — callers must handle the error to avoid
66
+ * silently overwriting a corrupt config with an empty one.
67
+ */
68
+ export declare const parseJsonc: (text: string) => Record<string, unknown>;
69
+ /**
70
+ * True when `entry` is a string that resolves to this plugin by base name.
71
+ * Matches `opencode-agent-skills-md` and any `opencode-agent-skills-md@<spec>`
72
+ * variant. Non-string entries (legacy object-form leftover) return false.
73
+ */
74
+ export declare const matchesPlugin: (entry: unknown) => boolean;
75
+ /**
76
+ * Coerce the raw value of `config.plugin` into a clean string array.
77
+ *
78
+ * Handles:
79
+ * - `undefined` / `null` → `[]`
80
+ * - array of strings (or mixed) → only the string entries
81
+ * - the broken object form `{ "<name>": ... }` → the keys (in declaration order)
82
+ * - any other non-object, non-array shape → `[]` (doctor surfaces it)
83
+ */
84
+ export declare const normalizePlugin: (raw: unknown) => string[];
85
+ /**
86
+ * Dedupe the plugin list by base name (the part before the first `@`),
87
+ * keeping the LAST occurrence of each base. Any `opencode-agent-skills-md`
88
+ * entries are removed entirely so the install flow can append one fresh
89
+ * entry at the end without leaving stale versions behind.
90
+ *
91
+ * Order is preserved for the surviving entries (last-wins per base).
92
+ */
93
+ export declare const dedupePlugins: (entries: readonly string[]) => string[];
94
+ /**
95
+ * Build the npm specifier we will write into `plugin[]`:
96
+ * `"opencode-agent-skills-md"` when no version is supplied, otherwise
97
+ * `"opencode-agent-skills-md@<version>"`. Empty / whitespace-only versions
98
+ * are treated as "no version".
99
+ */
100
+ export declare const buildSpecifier: (version?: string) => string;
101
+ /**
102
+ * If `configPath` already exists, copy it next to itself as a timestamped
103
+ * sibling and prune older CLI-created backups so at most `BACKUP_LIMIT`
104
+ * survive (newest first). Returns the backup path, or `null` when no
105
+ * backup was needed (file missing or not writable).
106
+ */
107
+ export declare const backupIfWritable: (configPath: string, fs: CliFs) => string | null;
108
+ /**
109
+ * Prune CLI-created backups of `configPath`, keeping only the newest
110
+ * `limit` siblings (lexical order on the timestamp suffix is fine because
111
+ * the stamp is fixed-width and ISO-8601-derived).
112
+ */
113
+ export declare const rotateBackups: (configPath: string, limit: number, fs: CliFs) => void;
114
+ /**
115
+ * Write `content` to `targetPath` via a temp sibling + rename. The
116
+ * rename is atomic on POSIX (and best-effort on Windows), so a crashed
117
+ * CLI never leaves a half-written config behind. Any parent directories
118
+ * are created with `{ recursive: true }` so first-run installs Just Work.
119
+ */
120
+ export declare const writeAtomically: (targetPath: string, content: string, fs: CliFs) => void;
121
+ export interface LoadedConfig {
122
+ /** Absolute path the loader used (existing or newly-targeted). */
123
+ path: string;
124
+ /** Parsed config object — `{}` if the file was absent or unreadable. */
125
+ config: Record<string, unknown>;
126
+ /** Whether the file existed on disk before loading. */
127
+ existed: boolean;
128
+ /**
129
+ * If the existing config file is malformed JSON, this contains the
130
+ * error message. Commands must check this and abort rather than
131
+ * silently overwriting the corrupt file with an empty config.
132
+ */
133
+ parseError?: string;
134
+ }
135
+ /**
136
+ * Resolve the global config path, read it (if it exists), and parse it.
137
+ * Missing files yield `config = {}` and `existed = false` so the install
138
+ * flow can treat "fresh install" and "already installed" the same way.
139
+ *
140
+ * Malformed JSONC is surfaced via `LoadedConfig.parseError`. Commands MUST
141
+ * check this field and abort instead of silently overwriting the user's
142
+ * corrupt config with an empty one.
143
+ */
144
+ export declare const loadGlobalConfig: (fs: CliFs, env?: NodeJS.ProcessEnv) => LoadedConfig;
@@ -0,0 +1,33 @@
1
+ import { type CliFs } from "./config";
2
+ export interface InstallOptions {
3
+ /** Optional version pin (e.g. `"1.2.3"`, `"latest"`). Omit for bare specifier. */
4
+ version?: string;
5
+ /** Plan the change and print it without writing. */
6
+ dryRun?: boolean;
7
+ /** Reserved for future confirmation prompts; accepted but unused for now. */
8
+ yes?: boolean;
9
+ }
10
+ export interface InstallResult {
11
+ /** Outcome of the command. */
12
+ status: "wrote" | "planned" | "noop";
13
+ /** Resolved config path (existing or newly-targeted). */
14
+ path: string;
15
+ /** Specifier that was added (or would be added under `--dry-run`). */
16
+ specifier: string;
17
+ /** Backup path created before the write, or `null` when no backup was needed. */
18
+ backup: string | null;
19
+ }
20
+ /**
21
+ * Run `oas install` against the global OpenCode config.
22
+ *
23
+ * Steps: load → normalize → drop existing oas variants → dedupe surviving
24
+ * entries → append one requested specifier → backup → atomic write. The
25
+ * backup is a timestamped sibling of the config file; rotation to
26
+ * `BACKUP_LIMIT` is handled inside `backupIfWritable`.
27
+ *
28
+ * Idempotency: re-running with the same specifier resolves to a `noop`
29
+ * result without touching disk. A malformed config triggers an error so
30
+ * the user can fix the JSONC instead of silently losing it to an empty
31
+ * overwrite.
32
+ */
33
+ export declare const runInstall: (opts?: InstallOptions, fs?: CliFs) => InstallResult;
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ export interface MainResult {
3
+ command: string | null;
4
+ exitCode: 0 | 1 | 2;
5
+ }
6
+ /**
7
+ * Pure(ish) dispatcher: takes argv, runs the matching command, sets
8
+ * `process.exitCode`, and returns a structured result so tests can assert
9
+ * without reading the exit code.
10
+ */
11
+ export declare const runMain: (argv?: readonly string[]) => MainResult;
@@ -0,0 +1,6 @@
1
+ import type { CliFs } from "./config";
2
+ /**
3
+ * Build a `CliFs` that delegates to `node:fs`. All methods are sync; the
4
+ * CLI is short-lived and never benefits from async I/O.
5
+ */
6
+ export declare const createRealFs: () => CliFs;
@@ -0,0 +1,34 @@
1
+ import { type CliFs } from "./config";
2
+ export interface StatusResult {
3
+ /** Whether an `opencode-agent-skills-md` entry is present in `plugin`. */
4
+ installed: boolean;
5
+ /** Resolved config path the loader used. */
6
+ path: string;
7
+ /** Detected on-disk format. */
8
+ format: "json" | "jsonc";
9
+ /** The active specifier, or `null` when not installed. */
10
+ specifier: string | null;
11
+ /** Other plugin entries preserved alongside the oas one. */
12
+ extras: string[];
13
+ }
14
+ export interface DoctorResult {
15
+ /** True when there are zero blocking issues. */
16
+ ok: boolean;
17
+ /** Blocking problems — the install flow will not work until they are fixed. */
18
+ issues: string[];
19
+ /** Non-blocking advisories — install may still work. */
20
+ warnings: string[];
21
+ /** Informational notes about what was checked. */
22
+ info: string[];
23
+ }
24
+ /**
25
+ * Read-only status probe. Prints a human-readable report to stdout and
26
+ * returns the same data as a structured result so callers (including
27
+ * `main.ts` and tests) can consume it without parsing the message.
28
+ */
29
+ export declare const runStatus: (fs?: CliFs) => StatusResult;
30
+ /**
31
+ * Health checks. The function does not exit on its own — it returns a
32
+ * `DoctorResult` and `main.ts` maps `ok === false` to exit code 1.
33
+ */
34
+ export declare const runDoctor: (fs?: CliFs, env?: NodeJS.ProcessEnv) => DoctorResult;
@@ -0,0 +1,22 @@
1
+ import { type CliFs } from "./config";
2
+ export interface UninstallOptions {
3
+ /** Also remove the runtime cache and the plugin's own config dir. */
4
+ purge?: boolean;
5
+ /** Plan the change and print it without writing. */
6
+ dryRun?: boolean;
7
+ /** Reserved for future confirmation prompts; accepted but unused for now. */
8
+ yes?: boolean;
9
+ }
10
+ export interface UninstallResult {
11
+ status: "wrote" | "planned" | "noop";
12
+ path: string;
13
+ /** Plugin entries that were (or would be) removed from the config. */
14
+ removed: string[];
15
+ /** Cache / config dirs removed under `--purge`. Empty when `--purge` was not set. */
16
+ purged: string[];
17
+ }
18
+ /** Bun/npm-style cache path where the plugin gets installed at runtime. */
19
+ export declare const cachePath: (env?: NodeJS.ProcessEnv) => string;
20
+ /** Plugin's own XDG config dir (separate from the OpenCode config it edits). */
21
+ export declare const pluginConfigPath: (env?: NodeJS.ProcessEnv) => string;
22
+ export declare const runUninstall: (opts?: UninstallOptions, fs?: CliFs) => UninstallResult;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * OpenCode host adapter.
3
+ *
4
+ * Wraps the OpenCode SDK client (`PluginInput["client"]`) and provides
5
+ * a bounded surface for content injection, session context, and filesystem
6
+ * access consumed by the plugin and skill tools.
7
+ *
8
+ * The boundary contracts (`SkillHostClient`, `SkillHostSession`,
9
+ * `SkillHostContext`) are declared in the `opencode-agent-skills-md-core`
10
+ * package per spec R2; this module IMPLEMENTS them over the OpenCode SDK
11
+ * client plus `node:fs/promises`. No other package may declare a concrete
12
+ * implementation — the plugin package owns exactly one.
13
+ */
14
+ import type { PluginInput } from "@opencode-ai/plugin";
15
+ import type { SkillHostClient, SkillHostSession } from "opencode-agent-skills-md-core";
16
+ /** Concrete OpenCode client (the SDK's generated client type). */
17
+ export type OpencodeClient = PluginInput["client"];
18
+ /**
19
+ * File access surface exposed alongside the host client. Tools
20
+ * consume these via the host instead of importing `node:fs/promises` so the
21
+ * boundary stays explicit and easy to stub in tests.
22
+ */
23
+ export interface OpencodeHostFileAccess {
24
+ readFile(path: string): Promise<string>;
25
+ readdir(path: string): Promise<string[]>;
26
+ }
27
+ /**
28
+ * Concrete OpenCode client surface.
29
+ *
30
+ * Structurally identical to the core boundary contract `SkillHostClient`
31
+ * (it implements all four methods). The alias is preserved for backward
32
+ * compatibility with prior plugin-package consumers and to make the
33
+ * OpenCode-specific implementation obvious at use sites.
34
+ */
35
+ export type OpencodeSkillHostClient = SkillHostClient;
36
+ /**
37
+ * The full host surface: a bounded client plus a session factory. Each call
38
+ * to `session(id)` returns a `SkillHostSession` carrying only the id the core
39
+ * needs to thread through host calls.
40
+ */
41
+ export interface OpencodeSkillHost {
42
+ client: OpencodeSkillHostClient;
43
+ session: (id: string) => SkillHostSession;
44
+ }
45
+ /**
46
+ * Build an `OpencodeSkillHost` over the supplied OpenCode SDK client.
47
+ *
48
+ * The host is the only place in the codebase that touches the SDK's
49
+ * `client.session.prompt` and `client.session.messages` methods.
50
+ */
51
+ export declare const createOpencodeSkillHost: (client: OpencodeClient) => OpencodeSkillHost;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * OpenCode host adapter — root entrypoint.
3
+ *
4
+ * Re-exports the plugin factory as the package's default export so the
5
+ * `rolldown` build can target this file directly. The root `src/plugin.ts`
6
+ * shim forwards to this module to preserve the legacy import path while
7
+ * `package.json` still resolves `dist/plugin.mjs` to the package main.
8
+ *
9
+ * Public surface:
10
+ * - default export: SkillsPlugin (the @opencode-ai/plugin Plugin factory)
11
+ * - named exports: SkillsPlugin, createOpencodeSkillHost
12
+ */
13
+ import { SkillsPlugin } from "./plugin";
14
+ export { SkillsPlugin };
15
+ export { createOpencodeSkillHost } from "./host";
16
+ export type { OpencodeClient, OpencodeSkillHost, OpencodeSkillHostClient, OpencodeHostFileAccess, } from "./host";
17
+ export default SkillsPlugin;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * OpenCode Agent Skills Plugin (host adapter).
3
+ *
4
+ * The plugin factory builds the host over the OpenCode SDK client, composes
5
+ * the four skill tools, and wires the chat.message and event hooks. The
6
+ * keyword matcher and session/loaded-skill bookkeeping are the only
7
+ * adapter-specific logic; everything else delegates to the portable core
8
+ * or the host.
9
+ */
10
+ import type { Plugin } from "@opencode-ai/plugin";
11
+ import { type SkillSummary } from "opencode-agent-skills-md-core";
12
+ /**
13
+ * Render the matched-skill synthetic injection that asks the model to
14
+ * evaluate which of the matched skills (if any) it should activate.
15
+ *
16
+ * Each skill line carries a sub-line `trigger: <text>` whenever the
17
+ * skill has a non-empty `trigger`, so the model knows which user
18
+ * phrases should activate it. Skills with no trigger render exactly as
19
+ * before (`- name: description`).
20
+ */
21
+ export declare const formatMatchedSkillsInjection: (matchedSkills: SkillSummary[]) => string;
22
+ /**
23
+ * Lightweight keyword matching to replace ML embeddings.
24
+ *
25
+ * Per-token contribution:
26
+ * - name hit = 2x
27
+ * - trigger hit = 1.5x
28
+ * - desc hit = 1x
29
+ *
30
+ * The trigger tier (1.5x) sits between name (2x) and description (1x)
31
+ * so a trigger-matched skill outranks a description-matched skill at
32
+ * the same query, but a name-matched skill still wins overall.
33
+ */
34
+ export declare const matchSkillsByKeyword: (userMessage: string, availableSkills: SkillSummary[]) => SkillSummary[];
35
+ export declare const SkillsPlugin: Plugin;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Local interfaces for the OpenCode hook payload shapes this plugin
3
+ * actually consumes.
4
+ *
5
+ * These intentionally mirror only the narrow slice of the SDK types
6
+ * the plugin reads — defining them locally (rather than importing the
7
+ * SDK's broad `UserMessage` / `Part` / `Event` types) keeps the
8
+ * adapter resilient to upstream shape changes and lets us narrow
9
+ * untyped runtime payloads safely.
10
+ *
11
+ * Internal only: this module is not re-exported from `src/index.ts`.
12
+ */
13
+ /** A text-bearing chat part. `text` is optional because some parts carry metadata only. */
14
+ export interface ChatTextPart {
15
+ type: "text";
16
+ text?: string;
17
+ synthetic?: boolean;
18
+ }
19
+ /** Minimal shape of the `chat.message` output payload the plugin reads. */
20
+ export interface ChatMessageOutput {
21
+ message: {
22
+ sessionID: string;
23
+ model?: string;
24
+ agent?: string;
25
+ };
26
+ parts: unknown[];
27
+ }
28
+ /** `session.compacted` event payload. */
29
+ export interface SessionCompactedEvent {
30
+ type: "session.compacted";
31
+ properties: {
32
+ sessionID: string;
33
+ };
34
+ }
35
+ /** `session.deleted` event payload. */
36
+ export interface SessionDeletedEvent {
37
+ type: "session.deleted";
38
+ properties: {
39
+ info: {
40
+ id: string;
41
+ };
42
+ };
43
+ }
44
+ /** Discriminated union of the session lifecycle events this plugin handles. */
45
+ export type SessionEvent = SessionCompactedEvent | SessionDeletedEvent;
46
+ /** Type guard: narrows `unknown` to `ChatTextPart` when `part.type === "text"`. */
47
+ export declare const isChatTextPart: (part: unknown) => part is ChatTextPart;
48
+ /** Type guard: narrows `unknown` to `SessionCompactedEvent`. */
49
+ export declare const isSessionCompactedEvent: (event: unknown) => event is SessionCompactedEvent;
50
+ /** Type guard: narrows `unknown` to `SessionDeletedEvent`. */
51
+ export declare const isSessionDeletedEvent: (event: unknown) => event is SessionDeletedEvent;
@@ -0,0 +1,86 @@
1
+ /**
2
+ * OpenCode tool factories.
3
+ *
4
+ * The four skill tools (get_available_skills, read_skill_file, run_skill_script,
5
+ * use_skill) compose the portable core engine with the OpenCode host. Tools
6
+ * consume the host's bounded client surface; they never reference the
7
+ * OpenCode SDK client or the `node:fs` module directly.
8
+ *
9
+ * `createSkillTools(host, $, directory)` returns the four tool factories
10
+ * pre-bound to the host, the shell runner, and the project directory. The
11
+ * plugin instantiates them at registration time.
12
+ */
13
+ import type { PluginInput } from "@opencode-ai/plugin";
14
+ import { tool } from "@opencode-ai/plugin";
15
+ import type { OpencodeSkillHost } from "./host";
16
+ /** @internal - exported for testing */
17
+ export declare const _escapeXml: (s: string) => string;
18
+ /** @internal - exported for testing */
19
+ export declare const _escapeShellArg: (arg: string) => string;
20
+ /**
21
+ * Portable return type for tool factory consts.
22
+ *
23
+ * The `tool()` helper captures the Zod shape of its `args` parameter in the
24
+ * generic parameter, so the inferred return type of each factory leaks Zod
25
+ * types. When TypeScript emits `.d.ts` files for the package, that generic
26
+ * instantiation cannot be named portably across zod versions. Annotating
27
+ * the return type as `ReturnType<typeof tool>` erases the per-call Zod
28
+ * shape and leaves a stable, portable declaration for downstream consumers.
29
+ */
30
+ type SkillTool = ReturnType<typeof tool>;
31
+ /**
32
+ * Tool translation guide for skills written for Claude Code.
33
+ * Injected into skill content to help the AI use OpenCode equivalents.
34
+ */
35
+ export declare const toolTranslation = "<tool-translation>\nThis skill may reference Claude Code tools. Use OpenCode equivalents:\n- TodoWrite/TodoRead -> todowrite/todoread\n- Task (subagents) -> task tool with subagent_type parameter\n- Skill tool -> use_skill tool\n- Read/Write/Edit/Bash/Glob/Grep/WebFetch -> lowercase (read/write/edit/bash/glob/grep/webfetch)\n</tool-translation>";
36
+ export interface SkillTools {
37
+ GetAvailableSkills: ReturnType<typeof GetAvailableSkills>;
38
+ ReadSkillFile: ReturnType<typeof ReadSkillFile>;
39
+ RunSkillScript: ReturnType<typeof RunSkillScript>;
40
+ UseSkill: ReturnType<typeof UseSkill>;
41
+ }
42
+ /**
43
+ * Callback fired by `UseSkill` after a successful load so the host can
44
+ * update its session-level bookkeeping (loaded-skill set, TUI icon, etc.).
45
+ * The core never assumes a callback is registered; missing it must not
46
+ * break the load.
47
+ */
48
+ export type OnSkillLoaded = (sessionID: string, skillName: string) => void;
49
+ /**
50
+ * Build the four skill tool factories bound to the host, shell, and
51
+ * project directory. The returned object is what the plugin registers
52
+ * under its `tool` hook.
53
+ *
54
+ * The optional `onSkillLoaded` callback is threaded through to `UseSkill`
55
+ * so a successful load can update host session state (e.g., the loaded-
56
+ * skill set used to suppress duplicate match injection in `chat.message`).
57
+ */
58
+ export declare const createSkillTools: (host: OpencodeSkillHost, $: PluginInput["$"], directory: string, onSkillLoaded?: OnSkillLoaded) => SkillTools;
59
+ /**
60
+ * Resolve a skill by name, or return a "not found" message with a
61
+ * close-match suggestion.
62
+ *
63
+ * Centralizes the duplicated resolve-then-suggest pattern that
64
+ * `use_skill`, `read_skill_file`, and `run_skill_script` all need.
65
+ * Returning a single `string` keeps the call site trivial:
66
+ *
67
+ * - skill found → returns `skill.name`
68
+ * - skill missing, suggestion → `Skill "<name>" not found. Did you mean "<suggestion>"?`
69
+ * - skill missing, no hint → `Skill "<name>" not found. Use get_available_skills to list available skills.`
70
+ *
71
+ * Not-found messages always start with the literal `Skill "` so callers
72
+ * can detect them with `result.startsWith('Skill "')`. The skill-name
73
+ * regex (`/^[\p{Ll}\p{N}-]+$/u`) forbids uppercase initial characters,
74
+ * so a legitimate skill name can never collide with that prefix.
75
+ *
76
+ * The helper does its own discovery; callers that need the `Skill` object
77
+ * (rather than just its name) re-resolve via a second `discoverAllSkills`
78
+ * call. Discovery is cheap (file-listing only) and the OS-level metadata
79
+ * cache absorbs most of the cost.
80
+ */
81
+ export declare const resolveSkillOrSuggest: (directory: string, skillName: string) => Promise<string>;
82
+ declare const GetAvailableSkills: (directory: string) => SkillTool;
83
+ declare const ReadSkillFile: (directory: string, host: OpencodeSkillHost) => SkillTool;
84
+ declare const RunSkillScript: (directory: string, $: PluginInput["$"]) => SkillTool;
85
+ declare const UseSkill: (directory: string, host: OpencodeSkillHost, onSkillLoaded?: (sessionID: string, skillName: string) => void) => SkillTool;
86
+ export {};
package/package.json CHANGED
@@ -1,14 +1,54 @@
1
1
  {
2
2
  "name": "opencode-agent-skills-md",
3
- "version": "1.0.0",
4
- "private": false,
5
- "description": "pnpm workspace root for opencode-agent-skills-md. The user-facing OpenCode plugin lives at packages/opencode-agent-skills-md and the portable engine at packages/core. This manifest exists only to wire the workspace — it has no exports of its own; consumers install one of the workspace packages directly.",
6
- "author": "Josh Thomas <josh@joshthomas.dev>",
3
+ "version": "1.1.0",
4
+ "description": "OpenCode adapter for the opencode-agent-skills-md workspace: plugin factory, host, and the four skill tools.",
5
+ "type": "module",
6
+ "main": "src/index.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./src/index.ts",
10
+ "import": "./src/index.ts",
11
+ "default": "./src/index.ts"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "src",
17
+ "README.md",
18
+ "LICENSE"
19
+ ],
20
+ "bin": {
21
+ "oas": "./dist/cli.mjs"
22
+ },
23
+ "dependencies": {
24
+ "yaml": "^2.9.0"
25
+ },
26
+ "peerDependencies": {
27
+ "@opencode-ai/plugin": ">=1.0.0"
28
+ },
29
+ "devDependencies": {
30
+ "@types/node": "^25.9.1",
31
+ "rolldown": "^1.0.3",
32
+ "tsx": "^4.22.3",
33
+ "typescript": "^6.0.3",
34
+ "opencode-agent-skills-md-core": "0.0.0"
35
+ },
36
+ "engines": {
37
+ "node": ">=18.0.0"
38
+ },
7
39
  "license": "MIT",
8
40
  "repository": {
9
41
  "type": "git",
10
42
  "url": "https://github.com/MetalbolicX/opencode-agent-skills-md.git"
11
43
  },
44
+ "homepage": "https://github.com/MetalbolicX/opencode-agent-skills-md",
45
+ "bugs": {
46
+ "url": "https://github.com/MetalbolicX/opencode-agent-skills-md/issues"
47
+ },
48
+ "author": {
49
+ "name": "Josh Thomas",
50
+ "url": "https://github.com/joshjoshthomas"
51
+ },
12
52
  "keywords": [
13
53
  "opencode",
14
54
  "plugin",
@@ -16,20 +56,10 @@
16
56
  "agent",
17
57
  "ai"
18
58
  ],
19
- "engines": {
20
- "node": ">=18.0.0"
21
- },
22
- "devDependencies": {
23
- "@types/node": "^25.9.1",
24
- "tsx": "^4.22.3",
25
- "typescript": "^6.0.3",
26
- "opencode-agent-skills-md": "0.7.0",
27
- "opencode-agent-skills-md-core": "0.0.0"
28
- },
29
59
  "scripts": {
30
- "build": "pnpm -r --workspace-concurrency=1 run build",
31
- "test": "pnpm -r --no-bail --workspace-concurrency=1 test && node --import tsx --test tests/workspace.test.ts",
32
- "test:workspace": "node --import tsx --test tests/workspace.test.ts",
33
- "typecheck": "pnpm -r --workspace-concurrency=1 run typecheck"
60
+ "build": "rm -rf dist && rolldown -c && tsc -p tsconfig.build.json",
61
+ "pretest": "npm run build",
62
+ "test": "node --import tsx --test \"tests/**/*.test.ts\"",
63
+ "typecheck": "tsc --noEmit"
34
64
  }
35
65
  }
@@ -16,6 +16,7 @@
16
16
  // ---------------------------------------------------------------------------
17
17
 
18
18
  import { pathToFileURL } from "node:url";
19
+ import { realpathSync } from "node:fs";
19
20
  import { parseArgs } from "node:util";
20
21
  import { runInstall } from "./install";
21
22
  import { runDoctor, runStatus } from "./status";
@@ -186,15 +187,30 @@ export const runMain = (argv: readonly string[] = process.argv): MainResult => {
186
187
  * cli.mjs`), `false` when it was imported from a test harness. We avoid
187
188
  * `import.meta.main` because the package floor is Node 18 and that field
188
189
  * only landed in Node 22.
190
+ *
191
+ * Symlink-aware: when the script is invoked through a symlink (e.g. the
192
+ * pnpm global store, where `node_modules/<pkg>` is a symlink into a
193
+ * content-addressable store), `process.argv[1]` carries the symlink path
194
+ * while `import.meta.url` carries the real path after symlink resolution.
195
+ * Comparing the two verbatim would always be `false` under pnpm, causing
196
+ * the CLI to silently exit. We resolve both sides through `realpathSync`
197
+ * before comparing.
189
198
  */
190
- const invokedAsMain = ((): boolean => {
199
+ const checkInvokedAsMain = (): boolean => {
191
200
  if (!process.argv[1]) return false;
192
201
  try {
193
- return import.meta.url === pathToFileURL(process.argv[1]).href;
202
+ const realArgv = pathToFileURL(realpathSync(process.argv[1])).href;
203
+ return import.meta.url === realArgv;
194
204
  } catch {
195
- return false;
205
+ try {
206
+ return import.meta.url === pathToFileURL(process.argv[1]).href;
207
+ } catch {
208
+ return false;
209
+ }
196
210
  }
197
- })();
211
+ };
212
+
213
+ const invokedAsMain = checkInvokedAsMain();
198
214
 
199
215
  if (invokedAsMain) {
200
216
  runMain(process.argv);
@@ -1 +0,0 @@
1
- 0.44.0