opencode-skills-collection 1.0.195 → 2.0.0-beta.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/README.md +89 -41
- package/bundled-skills/.antigravity-install-manifest.json +1 -1
- package/dist/constants/constants.d.ts +14 -0
- package/dist/constants/constants.js +14 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +24 -60
- package/dist/skill-pointer/index.d.ts +23 -0
- package/dist/skill-pointer/index.js +28 -0
- package/dist/skill-pointer/pointer-generator.d.ts +10 -0
- package/dist/skill-pointer/pointer-generator.js +69 -0
- package/dist/skill-pointer/vault-installer.d.ts +17 -0
- package/dist/skill-pointer/vault-installer.js +86 -0
- package/dist/utils/fs.utils.d.ts +8 -0
- package/dist/utils/fs.utils.js +21 -0
- package/package.json +7 -6
package/README.md
CHANGED
|
@@ -14,42 +14,87 @@
|
|
|
14
14
|
|
|
15
15
|
# OpenCode Skills Collection
|
|
16
16
|
|
|
17
|
-
> An [OpenCode CLI](https://opencode.ai/) plugin that bundles and auto-syncs
|
|
17
|
+
> An [OpenCode CLI](https://opencode.ai/) plugin that bundles and auto-syncs a universal collection of AI skills —
|
|
18
|
+
> delivered instantly, with zero network latency at startup.
|
|
18
19
|
|
|
19
|
-
> ⚠️ **Previously published
|
|
20
|
+
> ⚠️ **Previously published
|
|
21
|
+
as [`opencode-skills-antigravity`](https://www.npmjs.com/package/opencode-skills-antigravity)** — that package is now
|
|
22
|
+
> deprecated and points to this one.
|
|
20
23
|
|
|
21
24
|
---
|
|
22
25
|
|
|
23
26
|
## Overview
|
|
24
27
|
|
|
25
|
-
**OpenCode Skills Collection**
|
|
28
|
+
**OpenCode Skills Collection** ships a pre-bundled snapshot of 800+ universal skills for the OpenCode CLI.
|
|
26
29
|
|
|
27
|
-
|
|
30
|
+
Instead of loading every skill into the AI context at startup — which would consume ~80k tokens and cause compaction
|
|
31
|
+
loops — the plugin uses a **SkillPointer** architecture: skills are organized into categories inside a hidden vault and
|
|
32
|
+
only loaded into context on demand.
|
|
28
33
|
|
|
29
34
|
---
|
|
30
35
|
|
|
31
36
|
## How It Works
|
|
32
37
|
|
|
33
|
-
The plugin operates in
|
|
38
|
+
The plugin operates in three phases:
|
|
34
39
|
|
|
35
|
-
**1.
|
|
40
|
+
**1. Local deployment (startup)**
|
|
36
41
|
|
|
37
|
-
|
|
42
|
+
When OpenCode starts, the plugin copies the pre-bundled skills from the npm package and runs the **SkillPointer pipeline
|
|
43
|
+
**:
|
|
38
44
|
|
|
39
|
-
|
|
40
|
-
-
|
|
41
|
-
|
|
42
|
-
|
|
45
|
+
```
|
|
46
|
+
bundled-skills/ (npm package)
|
|
47
|
+
│
|
|
48
|
+
▼
|
|
49
|
+
~/.config/opencode/skills/ ← OpenCode reads this
|
|
50
|
+
│
|
|
51
|
+
└── SkillPointer pipeline
|
|
52
|
+
│
|
|
53
|
+
├─ vault-manager → moves 800+ raw skills to the vault
|
|
54
|
+
└─ pointer-generator → writes ~35 lightweight pointer files
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**2. On-demand skill loading**
|
|
58
|
+
|
|
59
|
+
Each pointer file tells the AI: *"there are N skills for this category in the vault — use `list_dir` / `view_file` to
|
|
60
|
+
retrieve them when needed."*
|
|
61
|
+
The full skill content is only injected into context when the AI actually needs it.
|
|
43
62
|
|
|
44
|
-
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Disk Layout
|
|
45
66
|
|
|
46
|
-
|
|
67
|
+
After the first startup, your `~/.config/opencode/` directory looks like this:
|
|
47
68
|
|
|
48
69
|
```
|
|
49
|
-
~/.config/opencode/
|
|
70
|
+
~/.config/opencode/
|
|
71
|
+
├── opencode.json
|
|
72
|
+
├── skills/ ← ~35 pointer folders (active, read by OpenCode)
|
|
73
|
+
│ ├── backend-dev-category-pointer/
|
|
74
|
+
│ │ └── SKILL.md
|
|
75
|
+
│ ├── devops-category-pointer/
|
|
76
|
+
│ │ └── SKILL.md
|
|
77
|
+
│ └── ...
|
|
78
|
+
└── skill-libraries/ ← vault with all raw skills (hidden from startup context)
|
|
79
|
+
├── backend-dev/
|
|
80
|
+
│ ├── laravel-expert/
|
|
81
|
+
│ │ └── SKILL.md
|
|
82
|
+
│ └── wordpress-core/
|
|
83
|
+
│ └── SKILL.md
|
|
84
|
+
├── devops/
|
|
85
|
+
└── ...
|
|
50
86
|
```
|
|
51
87
|
|
|
52
|
-
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Context Usage
|
|
91
|
+
|
|
92
|
+
| | Without SkillPointer | With SkillPointer |
|
|
93
|
+
|----------------------|----------------------|---------------------|
|
|
94
|
+
| Folders in `skills/` | ~800 | ~35 |
|
|
95
|
+
| Tokens at startup | ~80,000 | ~255 |
|
|
96
|
+
| Skills available | All injected upfront | On-demand via vault |
|
|
97
|
+
| Compaction loops | ✗ frequent | ✓ none |
|
|
53
98
|
|
|
54
99
|
---
|
|
55
100
|
|
|
@@ -65,21 +110,24 @@ Add the plugin to your global OpenCode configuration file at `~/.config/opencode
|
|
|
65
110
|
}
|
|
66
111
|
```
|
|
67
112
|
|
|
68
|
-
That's it. OpenCode will automatically download the npm package on next startup via Bun — no manual `npm install`
|
|
113
|
+
That's it. OpenCode will automatically download the npm package on next startup via Bun — no manual `npm install`
|
|
114
|
+
needed.
|
|
69
115
|
|
|
70
116
|
---
|
|
71
117
|
|
|
72
118
|
## Usage
|
|
73
119
|
|
|
74
|
-
Once installed, all
|
|
120
|
+
Once installed, all skills are available in three ways:
|
|
75
121
|
|
|
76
122
|
**Explicit invocation via CLI:**
|
|
123
|
+
|
|
77
124
|
```bash
|
|
78
125
|
opencode run /brainstorming help me plan a new feature
|
|
79
126
|
opencode run /refactor clean up this function
|
|
80
127
|
```
|
|
81
128
|
|
|
82
129
|
**Slash commands in the OpenCode chat:**
|
|
130
|
+
|
|
83
131
|
```
|
|
84
132
|
/brainstorming
|
|
85
133
|
/refactor
|
|
@@ -87,6 +135,7 @@ opencode run /refactor clean up this function
|
|
|
87
135
|
```
|
|
88
136
|
|
|
89
137
|
**Natural language — OpenCode picks the right skill automatically:**
|
|
138
|
+
|
|
90
139
|
```
|
|
91
140
|
"Help me brainstorm ideas for a REST API design"
|
|
92
141
|
"Refactor this function to be more readable"
|
|
@@ -94,26 +143,6 @@ opencode run /refactor clean up this function
|
|
|
94
143
|
|
|
95
144
|
---
|
|
96
145
|
|
|
97
|
-
## Project Structure
|
|
98
|
-
|
|
99
|
-
```
|
|
100
|
-
opencode-skills-collection/
|
|
101
|
-
├── src/
|
|
102
|
-
│ └── index.ts # Plugin entry point — copies bundled skills on startup
|
|
103
|
-
├── bundled-skills/ # Pre-bundled skills snapshot (auto-updated by CI)
|
|
104
|
-
├── dist/ # Compiled TypeScript output
|
|
105
|
-
├── .github/
|
|
106
|
-
│ └── workflows/
|
|
107
|
-
│ ├── sync-skills.yml # Hourly skill sync + auto-publish
|
|
108
|
-
│ ├── release.yml # Manual version bump + GitHub Release
|
|
109
|
-
│ ├── publish.yml # npm publish on new release
|
|
110
|
-
│ └── merge-branch.yml # Keeps develop in sync with main
|
|
111
|
-
├── package.json
|
|
112
|
-
└── tsconfig.json
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
---
|
|
116
|
-
|
|
117
146
|
## Development
|
|
118
147
|
|
|
119
148
|
**Requirements:** Node.js ≥ 20, TypeScript ≥ 5
|
|
@@ -128,7 +157,8 @@ npm run build
|
|
|
128
157
|
# Output is in dist/
|
|
129
158
|
```
|
|
130
159
|
|
|
131
|
-
The plugin is written in TypeScript and compiled to ESNext with full type declarations. It targets ES2022 and uses ESM
|
|
160
|
+
The plugin is written in TypeScript and compiled to ESNext with full type declarations. It targets ES2022 and uses ESM
|
|
161
|
+
module resolution.
|
|
132
162
|
|
|
133
163
|
---
|
|
134
164
|
|
|
@@ -150,12 +180,30 @@ The old `opencode-skills-antigravity` package on npm is deprecated and re-export
|
|
|
150
180
|
|
|
151
181
|
## Contributing
|
|
152
182
|
|
|
153
|
-
Issues and pull requests are welcome
|
|
183
|
+
Issues and pull requests are welcome
|
|
184
|
+
at [github.com/FrancoStino/opencode-skills-collection](https://github.com/FrancoStino/opencode-skills-collection/issues).
|
|
154
185
|
|
|
155
|
-
If you'd like to contribute new skills to the
|
|
186
|
+
If you'd like to contribute new skills to the collection, open a PR adding a new folder inside `bundled-skills/` — it
|
|
187
|
+
will be automatically picked up on next sync.
|
|
156
188
|
|
|
157
189
|
---
|
|
158
190
|
|
|
191
|
+
## Beta Releases
|
|
192
|
+
|
|
193
|
+
Beta versions are published from the `develop` branch for testing before official releases.
|
|
194
|
+
|
|
195
|
+
### Installing Beta Versions
|
|
196
|
+
|
|
197
|
+
To use the latest beta version, update your `~/.config/opencode/opencode.json`:
|
|
198
|
+
|
|
199
|
+
```json
|
|
200
|
+
{
|
|
201
|
+
"plugin": [
|
|
202
|
+
"opencode-skills-collection@beta"
|
|
203
|
+
]
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
159
207
|
## License
|
|
160
208
|
|
|
161
|
-
MIT ©
|
|
209
|
+
[MIT ©](./LICENSE)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared constants used across the plugin.
|
|
3
|
+
*/
|
|
4
|
+
/** Suffix appended to every category pointer folder name. */
|
|
5
|
+
export declare const POINTER_SUFFIX: "-category-pointer";
|
|
6
|
+
/** Expected filename for every skill definition. */
|
|
7
|
+
export declare const SKILL_FILENAME: "SKILL.md";
|
|
8
|
+
/**
|
|
9
|
+
* Name of the vault directory that stores raw skills, co-located
|
|
10
|
+
* with the rest of OpenCode config under ~/.config/opencode/
|
|
11
|
+
*/
|
|
12
|
+
export declare const VAULT_DIR_NAME: "skill-libraries";
|
|
13
|
+
/** Fallback category slug for skills not present in skills_index.json. */
|
|
14
|
+
export declare const UNCATEGORIZED_CATEGORY: "uncategorized";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared constants used across the plugin.
|
|
3
|
+
*/
|
|
4
|
+
/** Suffix appended to every category pointer folder name. */
|
|
5
|
+
export const POINTER_SUFFIX = "-category-pointer";
|
|
6
|
+
/** Expected filename for every skill definition. */
|
|
7
|
+
export const SKILL_FILENAME = "SKILL.md";
|
|
8
|
+
/**
|
|
9
|
+
* Name of the vault directory that stores raw skills, co-located
|
|
10
|
+
* with the rest of OpenCode config under ~/.config/opencode/
|
|
11
|
+
*/
|
|
12
|
+
export const VAULT_DIR_NAME = "skill-libraries";
|
|
13
|
+
/** Fallback category slug for skills not present in skills_index.json. */
|
|
14
|
+
export const UNCATEGORIZED_CATEGORY = "uncategorized";
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
1
|
import type { Plugin } from "@opencode-ai/plugin";
|
|
2
|
+
/**
|
|
3
|
+
* OpenCode Skills Collection plugin.
|
|
4
|
+
*
|
|
5
|
+
* On every OpenCode startup:
|
|
6
|
+
* 1. Copies bundled skills directly into the vault
|
|
7
|
+
* (~/.config/opencode/skill-libraries/) organised by category.
|
|
8
|
+
* The active skills directory is never used as staging.
|
|
9
|
+
* 2. Generates lightweight category pointer SKILL.md files in
|
|
10
|
+
* ~/.config/opencode/skills/ without touching user custom skills.
|
|
11
|
+
*/
|
|
2
12
|
declare const OpenCodeSkillsCollection: Plugin;
|
|
3
13
|
export default OpenCodeSkillsCollection;
|
package/dist/index.js
CHANGED
|
@@ -1,71 +1,35 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import path from "path";
|
|
3
1
|
import os from "os";
|
|
2
|
+
import path from "path";
|
|
4
3
|
import { fileURLToPath } from "url";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
try {
|
|
9
|
-
fs.appendFileSync(LOG_FILE, line);
|
|
10
|
-
}
|
|
11
|
-
catch (_) { }
|
|
12
|
-
process.stderr.write(line);
|
|
13
|
-
}
|
|
14
|
-
function copyDirContents(srcDir, destDir) {
|
|
15
|
-
fs.mkdirSync(destDir, { recursive: true });
|
|
16
|
-
for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
|
|
17
|
-
const srcEntry = path.join(srcDir, entry.name);
|
|
18
|
-
const destEntry = path.join(destDir, entry.name);
|
|
19
|
-
if (entry.isDirectory()) {
|
|
20
|
-
copyDirContents(srcEntry, destEntry);
|
|
21
|
-
}
|
|
22
|
-
else {
|
|
23
|
-
fs.copyFileSync(srcEntry, destEntry);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
}
|
|
4
|
+
import { ensureDir } from "./utils/fs.utils.js";
|
|
5
|
+
import { runSkillPointer } from "./skill-pointer/index.js";
|
|
6
|
+
const ACTIVE_SKILLS_PATH_SEGMENTS = [".config", "opencode", "skills"];
|
|
27
7
|
function resolveBundledSkillsPath() {
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const candidate = path.join(dir, "bundled-skills");
|
|
34
|
-
if (fs.existsSync(candidate))
|
|
35
|
-
return candidate;
|
|
36
|
-
const parent = path.dirname(dir);
|
|
37
|
-
if (parent === dir)
|
|
38
|
-
break;
|
|
39
|
-
dir = parent;
|
|
40
|
-
}
|
|
41
|
-
throw new Error(`bundled-skills not found. import.meta.url=${import.meta.url}`);
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
return path.join(__dirname, "..", "bundled-skills");
|
|
10
|
+
}
|
|
11
|
+
function resolveActiveSkillsDir() {
|
|
12
|
+
return path.join(os.homedir(), ...ACTIVE_SKILLS_PATH_SEGMENTS);
|
|
42
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* OpenCode Skills Collection plugin.
|
|
16
|
+
*
|
|
17
|
+
* On every OpenCode startup:
|
|
18
|
+
* 1. Copies bundled skills directly into the vault
|
|
19
|
+
* (~/.config/opencode/skill-libraries/) organised by category.
|
|
20
|
+
* The active skills directory is never used as staging.
|
|
21
|
+
* 2. Generates lightweight category pointer SKILL.md files in
|
|
22
|
+
* ~/.config/opencode/skills/ without touching user custom skills.
|
|
23
|
+
*/
|
|
43
24
|
const OpenCodeSkillsCollection = async (_ctx) => {
|
|
44
|
-
log("plugin start");
|
|
45
25
|
try {
|
|
46
26
|
const bundledSkillsPath = resolveBundledSkillsPath();
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
fs.mkdirSync(skillsPath, { recursive: true });
|
|
51
|
-
const entries = fs.readdirSync(bundledSkillsPath, { withFileTypes: true });
|
|
52
|
-
log(`entries in bundled-skills: ${entries.length}`);
|
|
53
|
-
let installed = 0;
|
|
54
|
-
for (const entry of entries) {
|
|
55
|
-
if (!entry.isDirectory())
|
|
56
|
-
continue;
|
|
57
|
-
if (entry.name.startsWith("."))
|
|
58
|
-
continue;
|
|
59
|
-
const src = path.join(bundledSkillsPath, entry.name);
|
|
60
|
-
if (!fs.existsSync(path.join(src, "SKILL.md")))
|
|
61
|
-
continue;
|
|
62
|
-
copyDirContents(src, path.join(skillsPath, entry.name));
|
|
63
|
-
installed++;
|
|
64
|
-
}
|
|
65
|
-
log(`done — installed ${installed} skills`);
|
|
27
|
+
const activeSkillsDir = resolveActiveSkillsDir();
|
|
28
|
+
ensureDir(activeSkillsDir);
|
|
29
|
+
runSkillPointer({ bundledSkillsPath, activeSkillsDir });
|
|
66
30
|
}
|
|
67
|
-
catch
|
|
68
|
-
|
|
31
|
+
catch {
|
|
32
|
+
// Plugin errors must never crash OpenCode.
|
|
69
33
|
}
|
|
70
34
|
return {};
|
|
71
35
|
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface SkillPointerOptions {
|
|
2
|
+
/** Absolute path where OpenCode looks for active skills. */
|
|
3
|
+
activeSkillsDir: string;
|
|
4
|
+
/** Absolute path to the bundled-skills snapshot inside the npm package. */
|
|
5
|
+
bundledSkillsPath: string;
|
|
6
|
+
/**
|
|
7
|
+
* Absolute path of the hidden vault where raw skills are stored.
|
|
8
|
+
* Defaults to ~/.config/opencode/skill-libraries
|
|
9
|
+
*/
|
|
10
|
+
vaultDir?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Orchestrates the full SkillPointer pipeline:
|
|
14
|
+
*
|
|
15
|
+
* 1. Reads skills_index.json bundled alongside the skills snapshot.
|
|
16
|
+
* 2. Copies bundled skills directly into the vault, categorised by the index.
|
|
17
|
+
* 3. Generates pointer SKILL.md files in activeSkillsDir with full skill
|
|
18
|
+
* listings so keyword searches (e.g. "laravel") resolve out of the box.
|
|
19
|
+
*
|
|
20
|
+
* The activeSkillsDir is never used as a staging area — user custom
|
|
21
|
+
* skills already present there are never moved or overwritten.
|
|
22
|
+
*/
|
|
23
|
+
export declare function runSkillPointer(options: SkillPointerOptions): void;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import os from "os";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { VAULT_DIR_NAME } from "../constants/constants.js";
|
|
4
|
+
import { ensureDir } from "../utils/fs.utils.js";
|
|
5
|
+
import { generatePointers } from "./pointer-generator.js";
|
|
6
|
+
import { installSkillsToVault, loadSkillsIndex } from "./vault-installer.js";
|
|
7
|
+
function resolveDefaultVaultDir() {
|
|
8
|
+
return path.join(os.homedir(), ".config", "opencode", VAULT_DIR_NAME);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Orchestrates the full SkillPointer pipeline:
|
|
12
|
+
*
|
|
13
|
+
* 1. Reads skills_index.json bundled alongside the skills snapshot.
|
|
14
|
+
* 2. Copies bundled skills directly into the vault, categorised by the index.
|
|
15
|
+
* 3. Generates pointer SKILL.md files in activeSkillsDir with full skill
|
|
16
|
+
* listings so keyword searches (e.g. "laravel") resolve out of the box.
|
|
17
|
+
*
|
|
18
|
+
* The activeSkillsDir is never used as a staging area — user custom
|
|
19
|
+
* skills already present there are never moved or overwritten.
|
|
20
|
+
*/
|
|
21
|
+
export function runSkillPointer(options) {
|
|
22
|
+
const vaultDir = options.vaultDir ?? resolveDefaultVaultDir();
|
|
23
|
+
ensureDir(options.activeSkillsDir);
|
|
24
|
+
ensureDir(vaultDir);
|
|
25
|
+
const index = loadSkillsIndex(options.bundledSkillsPath);
|
|
26
|
+
installSkillsToVault(options.bundledSkillsPath, vaultDir, index);
|
|
27
|
+
generatePointers(options.activeSkillsDir, vaultDir, index);
|
|
28
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { SkillIndexEntry } from "./vault-installer.js";
|
|
2
|
+
/**
|
|
3
|
+
* Scans every category directory in the vault and writes
|
|
4
|
+
* a lightweight pointer SKILL.md into the active skills directory.
|
|
5
|
+
*
|
|
6
|
+
* Each pointer includes the full list of skill names + descriptions
|
|
7
|
+
* so keyword searches (e.g. "laravel", "wordpress") resolve correctly
|
|
8
|
+
* via get_available_skills without loading every SKILL.md.
|
|
9
|
+
*/
|
|
10
|
+
export declare function generatePointers(activeSkillsDir: string, vaultDir: string, index?: SkillIndexEntry[]): void;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { POINTER_SUFFIX, SKILL_FILENAME, UNCATEGORIZED_CATEGORY } from "../constants/constants.js";
|
|
4
|
+
import { ensureDir, listSubdirectories } from "../utils/fs.utils.js";
|
|
5
|
+
function buildPointerContent(category, skills, libraryPath) {
|
|
6
|
+
const title = category
|
|
7
|
+
.replace(/-/g, " ")
|
|
8
|
+
.replace(/\b\w/g, (char) => char.toUpperCase());
|
|
9
|
+
const normalizedPath = libraryPath.replace(/\\/g, "/");
|
|
10
|
+
const skillCount = skills.length;
|
|
11
|
+
const skillList = skills
|
|
12
|
+
.map((s) => `- **${s.id}** — ${s.description || s.name}`)
|
|
13
|
+
.join("\n");
|
|
14
|
+
return `---
|
|
15
|
+
name: ${category}${POINTER_SUFFIX}
|
|
16
|
+
description: "Pointer to a library of ${skillCount} specialized ${title} skills. Use when working on ${category}-related tasks."
|
|
17
|
+
risk: safe
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
# ${title} Capability Library 🎯
|
|
21
|
+
|
|
22
|
+
This is a **pointer skill**. The ${skillCount} specialized ${title} skills are stored in a hidden vault to keep your startup context minimal.
|
|
23
|
+
|
|
24
|
+
## Available skills in this category
|
|
25
|
+
|
|
26
|
+
${skillList}
|
|
27
|
+
|
|
28
|
+
## How to load a skill
|
|
29
|
+
|
|
30
|
+
1. Identify the skill name above matching your task.
|
|
31
|
+
2. Use \`view_file\` to read its \`SKILL.md\` from the vault:
|
|
32
|
+
\`${normalizedPath}/<skill-name>/SKILL.md\`
|
|
33
|
+
3. Follow those instructions to complete the request.
|
|
34
|
+
|
|
35
|
+
**Vault path:** \`${normalizedPath}\`
|
|
36
|
+
|
|
37
|
+
> Do not guess best practices — always read from the vault first.
|
|
38
|
+
`;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Scans every category directory in the vault and writes
|
|
42
|
+
* a lightweight pointer SKILL.md into the active skills directory.
|
|
43
|
+
*
|
|
44
|
+
* Each pointer includes the full list of skill names + descriptions
|
|
45
|
+
* so keyword searches (e.g. "laravel", "wordpress") resolve correctly
|
|
46
|
+
* via get_available_skills without loading every SKILL.md.
|
|
47
|
+
*/
|
|
48
|
+
export function generatePointers(activeSkillsDir, vaultDir, index = []) {
|
|
49
|
+
const categoryDirs = listSubdirectories(vaultDir);
|
|
50
|
+
const byCategory = new Map();
|
|
51
|
+
for (const entry of index) {
|
|
52
|
+
const cat = entry.category ?? UNCATEGORIZED_CATEGORY;
|
|
53
|
+
if (!byCategory.has(cat))
|
|
54
|
+
byCategory.set(cat, []);
|
|
55
|
+
byCategory.get(cat).push(entry);
|
|
56
|
+
}
|
|
57
|
+
for (const categoryName of categoryDirs) {
|
|
58
|
+
const categoryVaultPath = path.join(vaultDir, categoryName);
|
|
59
|
+
const skills = byCategory.get(categoryName) ?? [];
|
|
60
|
+
if (skills.length === 0) {
|
|
61
|
+
const subDirs = fs.readdirSync(categoryVaultPath).filter((e) => fs.statSync(path.join(categoryVaultPath, e)).isDirectory());
|
|
62
|
+
if (subDirs.length === 0)
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const pointerDir = path.join(activeSkillsDir, `${categoryName}${POINTER_SUFFIX}`);
|
|
66
|
+
ensureDir(pointerDir);
|
|
67
|
+
fs.writeFileSync(path.join(pointerDir, SKILL_FILENAME), buildPointerContent(categoryName, skills, categoryVaultPath), "utf-8");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface SkillIndexEntry {
|
|
2
|
+
id: string;
|
|
3
|
+
category: string;
|
|
4
|
+
name: string;
|
|
5
|
+
description: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Loads the pre-built skills_index.json from the project root.
|
|
9
|
+
* Falls back to a dynamically generated index from SKILL.md frontmatter
|
|
10
|
+
* when the file is missing, so the plugin always works correctly.
|
|
11
|
+
*/
|
|
12
|
+
export declare function loadSkillsIndex(bundledSkillsPath: string): SkillIndexEntry[];
|
|
13
|
+
/**
|
|
14
|
+
* Copies every skill folder from bundledSkillsPath directly into
|
|
15
|
+
* the vault under the appropriate category sub-directory.
|
|
16
|
+
*/
|
|
17
|
+
export declare function installSkillsToVault(bundledSkillsPath: string, vaultDir: string, index: SkillIndexEntry[]): void;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { ensureDir } from "../utils/fs.utils.js";
|
|
4
|
+
import { UNCATEGORIZED_CATEGORY } from "../constants/constants.js";
|
|
5
|
+
/**
|
|
6
|
+
* Extracts a frontmatter field value from a SKILL.md string.
|
|
7
|
+
* Handles both quoted and unquoted values.
|
|
8
|
+
*/
|
|
9
|
+
function parseFrontmatterField(content, field) {
|
|
10
|
+
const match = content.match(new RegExp(`^${field}:\\s*["']?([^"'\\n]+)["']?`, "m"));
|
|
11
|
+
return match ? match[1].trim() : "";
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Derives a category slug from a skill folder name by taking the
|
|
15
|
+
* first hyphen-separated segment (e.g. "laravel-expert" → "laravel",
|
|
16
|
+
* "wordpress-core" → "wordpress", "php-pro" → "php").
|
|
17
|
+
* Falls back to UNCATEGORIZED_CATEGORY for single-word names.
|
|
18
|
+
*/
|
|
19
|
+
function categoryFromFolderName(folderName) {
|
|
20
|
+
const parts = folderName.split("-");
|
|
21
|
+
return parts.length > 1 ? parts[0] : UNCATEGORIZED_CATEGORY;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Builds a SkillIndexEntry[] by scanning every skill folder in
|
|
25
|
+
* bundledSkillsPath and reading its SKILL.md frontmatter.
|
|
26
|
+
* This is used as fallback when skills_index.json is absent.
|
|
27
|
+
*/
|
|
28
|
+
function buildIndexFromBundledSkills(bundledSkillsPath) {
|
|
29
|
+
const index = [];
|
|
30
|
+
for (const entry of fs.readdirSync(bundledSkillsPath)) {
|
|
31
|
+
if (entry.startsWith(".") || entry === "skills_index.json" || entry === "README.md")
|
|
32
|
+
continue;
|
|
33
|
+
const skillDir = path.join(bundledSkillsPath, entry);
|
|
34
|
+
if (!fs.statSync(skillDir).isDirectory())
|
|
35
|
+
continue;
|
|
36
|
+
const skillMdPath = path.join(skillDir, "SKILL.md");
|
|
37
|
+
if (!fs.existsSync(skillMdPath))
|
|
38
|
+
continue;
|
|
39
|
+
const content = fs.readFileSync(skillMdPath, "utf-8");
|
|
40
|
+
const name = parseFrontmatterField(content, "name") || entry;
|
|
41
|
+
const description = parseFrontmatterField(content, "description") || name;
|
|
42
|
+
const category = parseFrontmatterField(content, "category") || categoryFromFolderName(entry);
|
|
43
|
+
index.push({ id: entry, category, name, description });
|
|
44
|
+
}
|
|
45
|
+
return index;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Loads the pre-built skills_index.json from the project root.
|
|
49
|
+
* Falls back to a dynamically generated index from SKILL.md frontmatter
|
|
50
|
+
* when the file is missing, so the plugin always works correctly.
|
|
51
|
+
*/
|
|
52
|
+
export function loadSkillsIndex(bundledSkillsPath) {
|
|
53
|
+
const indexPath = path.join(bundledSkillsPath, "..", "skills_index.json");
|
|
54
|
+
if (fs.existsSync(indexPath)) {
|
|
55
|
+
try {
|
|
56
|
+
const raw = fs.readFileSync(indexPath, "utf-8");
|
|
57
|
+
return JSON.parse(raw);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// fall through to dynamic generation
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return buildIndexFromBundledSkills(bundledSkillsPath);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Copies every skill folder from bundledSkillsPath directly into
|
|
67
|
+
* the vault under the appropriate category sub-directory.
|
|
68
|
+
*/
|
|
69
|
+
export function installSkillsToVault(bundledSkillsPath, vaultDir, index) {
|
|
70
|
+
if (!fs.existsSync(bundledSkillsPath))
|
|
71
|
+
return;
|
|
72
|
+
const categoryMap = new Map(index.map((e) => [e.id, e.category ?? UNCATEGORIZED_CATEGORY]));
|
|
73
|
+
for (const entry of fs.readdirSync(bundledSkillsPath)) {
|
|
74
|
+
if (entry.startsWith(".") ||
|
|
75
|
+
entry === "skills_index.json" ||
|
|
76
|
+
entry === "README.md")
|
|
77
|
+
continue;
|
|
78
|
+
const srcPath = path.join(bundledSkillsPath, entry);
|
|
79
|
+
if (!fs.statSync(srcPath).isDirectory())
|
|
80
|
+
continue;
|
|
81
|
+
const category = categoryMap.get(entry) ?? UNCATEGORIZED_CATEGORY;
|
|
82
|
+
const destPath = path.join(vaultDir, category, entry);
|
|
83
|
+
ensureDir(path.join(vaultDir, category));
|
|
84
|
+
fs.cpSync(srcPath, destPath, { recursive: true, force: true });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ensures a directory exists, creating it recursively if needed.
|
|
3
|
+
*/
|
|
4
|
+
export declare function ensureDir(dirPath: string): void;
|
|
5
|
+
/**
|
|
6
|
+
* Returns the names of all direct child directories inside a given path.
|
|
7
|
+
*/
|
|
8
|
+
export declare function listSubdirectories(dirPath: string): string[];
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
/**
|
|
4
|
+
* Ensures a directory exists, creating it recursively if needed.
|
|
5
|
+
*/
|
|
6
|
+
export function ensureDir(dirPath) {
|
|
7
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Returns the names of all direct child directories inside a given path.
|
|
11
|
+
*/
|
|
12
|
+
export function listSubdirectories(dirPath) {
|
|
13
|
+
if (!fs.existsSync(dirPath))
|
|
14
|
+
return [];
|
|
15
|
+
return fs
|
|
16
|
+
.readdirSync(dirPath)
|
|
17
|
+
.filter((entry) => {
|
|
18
|
+
const fullPath = path.join(dirPath, entry);
|
|
19
|
+
return fs.statSync(fullPath).isDirectory();
|
|
20
|
+
});
|
|
21
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-skills-collection",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0-beta.1",
|
|
4
4
|
"description": "OpenCode CLI plugin that automatically downloads and keeps skills up to date.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -23,20 +23,21 @@
|
|
|
23
23
|
"keywords": [
|
|
24
24
|
"opencode",
|
|
25
25
|
"opencode-plugin",
|
|
26
|
-
"antigravity",
|
|
27
26
|
"skills",
|
|
27
|
+
"skills-collection",
|
|
28
|
+
"skills-pointer",
|
|
28
29
|
"ai",
|
|
29
30
|
"agent"
|
|
30
31
|
],
|
|
31
|
-
"author": "Davide Ladisa <
|
|
32
|
+
"author": "Davide Ladisa <info@davideladisa.it>",
|
|
32
33
|
"license": "MIT",
|
|
33
34
|
"dependencies": {
|
|
34
|
-
"@opencode-ai/plugin": "^1.
|
|
35
|
+
"@opencode-ai/plugin": "^1.4.0"
|
|
35
36
|
},
|
|
36
37
|
"devDependencies": {
|
|
37
|
-
"@opencode-ai/sdk": "^1.
|
|
38
|
+
"@opencode-ai/sdk": "^1.4.0",
|
|
38
39
|
"@types/bun": "latest",
|
|
39
|
-
"@types/node": "^25.5.
|
|
40
|
+
"@types/node": "^25.5.2",
|
|
40
41
|
"typescript": "^6.0.2"
|
|
41
42
|
}
|
|
42
43
|
}
|