opencode-skills-collection 2.0.282 → 3.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 +36 -4
- package/dist/skill-pointer/config-loader.d.ts +10 -0
- package/dist/skill-pointer/config-loader.js +41 -0
- package/dist/skill-pointer/index.d.ts +4 -0
- package/dist/skill-pointer/index.js +7 -2
- package/dist/skill-pointer/pointer-generator.js +2 -15
- package/dist/skill-pointer/risk-level.d.ts +1 -0
- package/dist/skill-pointer/risk-level.js +1 -0
- package/dist/skill-pointer/skill-risk-filter.d.ts +5 -0
- package/dist/skill-pointer/skill-risk-filter.js +15 -0
- package/dist/skill-pointer/vault-installer.d.ts +2 -0
- package/dist/skill-pointer/vault-installer.js +15 -11
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
|
|
3
|
-
<img src="
|
|
3
|
+
<img src="docs/assets/logo.svg" alt="OpenCode Skills Collection"/>
|
|
4
4
|
|
|
5
5
|
<br/>
|
|
6
6
|
<br/>
|
|
@@ -36,8 +36,7 @@ The plugin operates in two phases:
|
|
|
36
36
|
|
|
37
37
|
**1. Local deployment (startup)**
|
|
38
38
|
|
|
39
|
-
When OpenCode starts, the plugin copies the pre-bundled skills from the npm package and runs the
|
|
40
|
-
**:
|
|
39
|
+
When OpenCode starts, the plugin copies the pre-bundled skills from the npm package and runs the SkillPointer pipeline:
|
|
41
40
|
|
|
42
41
|
```
|
|
43
42
|
bundled-skills/ (npm package)
|
|
@@ -88,7 +87,7 @@ After the first startup, your `~/.config/opencode/` directory looks like this:
|
|
|
88
87
|
|
|
89
88
|
| | Without SkillPointer | With SkillPointer |
|
|
90
89
|
|----------------------|----------------------|---------------------|
|
|
91
|
-
| Folders in `skills/` | ~1000
|
|
90
|
+
| Folders in `skills/` | ~1000 | ~35 |
|
|
92
91
|
| Tokens at startup | ~80,000 | ~255 |
|
|
93
92
|
| Skills available | All injected upfront | On-demand via vault |
|
|
94
93
|
| Compaction loops | ✗ frequent | ✓ none |
|
|
@@ -140,6 +139,39 @@ opencode run /refactor clean up this function
|
|
|
140
139
|
|
|
141
140
|
---
|
|
142
141
|
|
|
142
|
+
## Skill Risk Filter
|
|
143
|
+
|
|
144
|
+
The plugin supports configurable risk-based filtering of skills. By default, **all skills are loaded** — filtering is
|
|
145
|
+
opt-in.
|
|
146
|
+
|
|
147
|
+
Each skill in the index has a `risk` field with one of these levels:
|
|
148
|
+
|
|
149
|
+
| Level | Description |
|
|
150
|
+
|-------------|--------------------------------------------------------------------|
|
|
151
|
+
| `none` | No risk assessment |
|
|
152
|
+
| `safe` | Verified safe |
|
|
153
|
+
| `critical` | Contains sensitive operations |
|
|
154
|
+
| `offensive` | Contains offensive security tools (exploits, reverse shells, etc.) |
|
|
155
|
+
| `unknown` | Not yet classified |
|
|
156
|
+
|
|
157
|
+
### Configuration
|
|
158
|
+
|
|
159
|
+
Create a `~/.config/opencode/skill-filter.jsonc` file:
|
|
160
|
+
|
|
161
|
+
```jsonc
|
|
162
|
+
{
|
|
163
|
+
"excludedRiskLevels": ["offensive"],
|
|
164
|
+
"excludedSkills": ["windows-privilege-escalation"]
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
- **`excludedRiskLevels`**: Array of risk levels to block entirely
|
|
169
|
+
- **`excludedSkills`**: Array of specific skill IDs to block
|
|
170
|
+
|
|
171
|
+
Blocked skills are excluded from both the vault and the generated pointers — they are never loaded into context.
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
143
175
|
## Development
|
|
144
176
|
|
|
145
177
|
**Requirements:** Node.js ≥ 20, TypeScript ≥ 5
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { RiskLevel } from "./risk-level.js";
|
|
2
|
+
export interface SkillRiskFilterConfig {
|
|
3
|
+
excludedRiskLevels?: RiskLevel[];
|
|
4
|
+
excludedSkills?: string[];
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Loads filter config from skill-filter.jsonc. Missing file or section returns defaults.
|
|
8
|
+
* @param configPath Optional override (for testing).
|
|
9
|
+
*/
|
|
10
|
+
export declare function loadFilterConfig(configPath?: string): SkillRiskFilterConfig;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import os from "os";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import stripJsonComments from "strip-json-comments";
|
|
5
|
+
const DEFAULT_CONFIG = {
|
|
6
|
+
excludedRiskLevels: [],
|
|
7
|
+
excludedSkills: [],
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Strips JSONC comments so content can be parsed as plain JSON.
|
|
11
|
+
* Uses strip-json-comments library to properly handle strings.
|
|
12
|
+
*/
|
|
13
|
+
function stripJsoncComments(content) {
|
|
14
|
+
return stripJsonComments(content);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Loads filter config from skill-filter.jsonc. Missing file or section returns defaults.
|
|
18
|
+
* @param configPath Optional override (for testing).
|
|
19
|
+
*/
|
|
20
|
+
export function loadFilterConfig(configPath) {
|
|
21
|
+
const resolvedPath = configPath ?? path.join(os.homedir(), ".config", "opencode", "skill-filter.jsonc");
|
|
22
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
23
|
+
return { ...DEFAULT_CONFIG };
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
const raw = fs.readFileSync(resolvedPath, "utf-8");
|
|
27
|
+
const stripped = stripJsoncComments(raw);
|
|
28
|
+
const parsed = JSON.parse(stripped);
|
|
29
|
+
return {
|
|
30
|
+
excludedRiskLevels: Array.isArray(parsed.excludedRiskLevels)
|
|
31
|
+
? parsed.excludedRiskLevels
|
|
32
|
+
: DEFAULT_CONFIG.excludedRiskLevels,
|
|
33
|
+
excludedSkills: Array.isArray(parsed.excludedSkills)
|
|
34
|
+
? parsed.excludedSkills
|
|
35
|
+
: DEFAULT_CONFIG.excludedSkills,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return { ...DEFAULT_CONFIG };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -8,6 +8,10 @@ export interface SkillPointerOptions {
|
|
|
8
8
|
* Defaults to ~/.config/opencode/skill-libraries
|
|
9
9
|
*/
|
|
10
10
|
vaultDir?: string;
|
|
11
|
+
/**
|
|
12
|
+
* Optional path to the risk filter configuration file.
|
|
13
|
+
*/
|
|
14
|
+
configPath?: string;
|
|
11
15
|
}
|
|
12
16
|
/**
|
|
13
17
|
* Orchestrates the full SkillPointer pipeline:
|
|
@@ -4,6 +4,8 @@ import { VAULT_DIR_NAME } from "../constants/constants.js";
|
|
|
4
4
|
import { ensureDir } from "../utils/fs.utils.js";
|
|
5
5
|
import { generatePointers } from "./pointer-generator.js";
|
|
6
6
|
import { installSkillsToVault, loadSkillsIndex } from "./vault-installer.js";
|
|
7
|
+
import { filterIndex } from "./skill-risk-filter.js";
|
|
8
|
+
import { loadFilterConfig } from "./config-loader.js";
|
|
7
9
|
function resolveDefaultVaultDir() {
|
|
8
10
|
return path.join(os.homedir(), ".config", "opencode", VAULT_DIR_NAME);
|
|
9
11
|
}
|
|
@@ -23,6 +25,9 @@ export function runSkillPointer(options) {
|
|
|
23
25
|
ensureDir(options.activeSkillsDir);
|
|
24
26
|
ensureDir(vaultDir);
|
|
25
27
|
const index = loadSkillsIndex(options.bundledSkillsPath);
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
const configPath = options.configPath || path.join(os.homedir(), ".config", "opencode", "skill-filter.jsonc");
|
|
29
|
+
const config = loadFilterConfig(configPath);
|
|
30
|
+
const filteredIndex = filterIndex(index, config);
|
|
31
|
+
installSkillsToVault(options.bundledSkillsPath, vaultDir, filteredIndex);
|
|
32
|
+
generatePointers(options.activeSkillsDir, vaultDir, filteredIndex);
|
|
28
33
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { POINTER_SUFFIX, SKILL_FILENAME, UNCATEGORIZED_CATEGORY } from "../constants/constants.js";
|
|
4
|
-
import { ensureDir
|
|
4
|
+
import { ensureDir } from "../utils/fs.utils.js";
|
|
5
5
|
function buildPointerContent(category, skills, libraryPath) {
|
|
6
6
|
const title = category
|
|
7
7
|
.replace(/-/g, " ")
|
|
@@ -46,7 +46,6 @@ ${skillList}
|
|
|
46
46
|
* via get_available_skills without loading every SKILL.md.
|
|
47
47
|
*/
|
|
48
48
|
export function generatePointers(activeSkillsDir, vaultDir, index = []) {
|
|
49
|
-
const categoryDirs = listSubdirectories(vaultDir);
|
|
50
49
|
const byCategory = new Map();
|
|
51
50
|
for (const entry of index) {
|
|
52
51
|
const cat = entry.category ?? UNCATEGORIZED_CATEGORY;
|
|
@@ -54,20 +53,8 @@ export function generatePointers(activeSkillsDir, vaultDir, index = []) {
|
|
|
54
53
|
byCategory.set(cat, []);
|
|
55
54
|
byCategory.get(cat).push(entry);
|
|
56
55
|
}
|
|
57
|
-
for (const categoryName of
|
|
56
|
+
for (const [categoryName, skills] of byCategory.entries()) {
|
|
58
57
|
const categoryVaultPath = path.join(vaultDir, categoryName);
|
|
59
|
-
let 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
|
-
skills = subDirs.map((dir) => ({
|
|
65
|
-
id: dir,
|
|
66
|
-
category: categoryName,
|
|
67
|
-
name: dir.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()),
|
|
68
|
-
description: dir.replace(/-/g, " "),
|
|
69
|
-
}));
|
|
70
|
-
}
|
|
71
58
|
const pointerDir = path.join(activeSkillsDir, `${categoryName}${POINTER_SUFFIX}`);
|
|
72
59
|
ensureDir(pointerDir);
|
|
73
60
|
fs.writeFileSync(path.join(pointerDir, SKILL_FILENAME), buildPointerContent(categoryName, skills, categoryVaultPath), "utf-8");
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type RiskLevel = "none" | "safe" | "critical" | "offensive" | "unknown";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { RiskLevel } from "./risk-level.js";
|
|
2
|
+
import type { SkillIndexEntry } from "./vault-installer.js";
|
|
3
|
+
import type { SkillRiskFilterConfig } from "./config-loader.js";
|
|
4
|
+
export declare function shouldLoad(skillId: string, risk: RiskLevel | undefined, config: SkillRiskFilterConfig): boolean;
|
|
5
|
+
export declare function filterIndex(index: SkillIndexEntry[], config: SkillRiskFilterConfig): SkillIndexEntry[];
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function shouldLoad(skillId, risk, config) {
|
|
2
|
+
const effectiveRisk = risk ?? "unknown";
|
|
3
|
+
if ((config.excludedRiskLevels ?? []).includes(effectiveRisk)) {
|
|
4
|
+
return false;
|
|
5
|
+
}
|
|
6
|
+
if ((config.excludedSkills ?? []).includes(skillId)) {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
export function filterIndex(index, config) {
|
|
12
|
+
return index.filter((entry) => {
|
|
13
|
+
return shouldLoad(entry.id, entry.risk, config);
|
|
14
|
+
});
|
|
15
|
+
}
|
|
@@ -10,6 +10,13 @@ function parseFrontmatterField(content, field) {
|
|
|
10
10
|
const match = content.match(new RegExp(`^${field}:\\s*["']?([^"'\\n]+)["']?`, "m"));
|
|
11
11
|
return match ? match[1].trim() : "";
|
|
12
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* Normalizes a parsed risk value to a valid RiskLevel, or "unknown" if invalid.
|
|
15
|
+
*/
|
|
16
|
+
function normalizeRisk(parsed) {
|
|
17
|
+
const valid = ["none", "safe", "critical", "offensive", "unknown"];
|
|
18
|
+
return valid.includes(parsed) ? parsed : "unknown";
|
|
19
|
+
}
|
|
13
20
|
/**
|
|
14
21
|
* Derives a category slug from a skill folder name by taking the
|
|
15
22
|
* first hyphen-separated segment (e.g. "laravel-expert" → "laravel",
|
|
@@ -40,7 +47,9 @@ function buildIndexFromBundledSkills(bundledSkillsPath) {
|
|
|
40
47
|
const name = parseFrontmatterField(content, "name") || entry;
|
|
41
48
|
const description = parseFrontmatterField(content, "description") || name;
|
|
42
49
|
const category = parseFrontmatterField(content, "category") || categoryFromFolderName(entry);
|
|
43
|
-
|
|
50
|
+
const riskRaw = parseFrontmatterField(content, "risk");
|
|
51
|
+
const risk = normalizeRisk(riskRaw);
|
|
52
|
+
index.push({ id: entry, category, name, description, risk });
|
|
44
53
|
}
|
|
45
54
|
return index;
|
|
46
55
|
}
|
|
@@ -69,17 +78,12 @@ export function loadSkillsIndex(bundledSkillsPath) {
|
|
|
69
78
|
export function installSkillsToVault(bundledSkillsPath, vaultDir, index) {
|
|
70
79
|
if (!fs.existsSync(bundledSkillsPath))
|
|
71
80
|
return;
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
if (
|
|
75
|
-
entry === "skills_index.json" ||
|
|
76
|
-
entry === "README.md")
|
|
77
|
-
continue;
|
|
78
|
-
const srcPath = path.join(bundledSkillsPath, entry);
|
|
79
|
-
if (!fs.statSync(srcPath).isDirectory())
|
|
81
|
+
for (const entry of index) {
|
|
82
|
+
const srcPath = path.join(bundledSkillsPath, entry.id);
|
|
83
|
+
if (!fs.existsSync(srcPath) || !fs.statSync(srcPath).isDirectory())
|
|
80
84
|
continue;
|
|
81
|
-
const category =
|
|
82
|
-
const destPath = path.join(vaultDir, category, entry);
|
|
85
|
+
const category = entry.category ?? UNCATEGORIZED_CATEGORY;
|
|
86
|
+
const destPath = path.join(vaultDir, category, entry.id);
|
|
83
87
|
ensureDir(path.join(vaultDir, category));
|
|
84
88
|
fs.cpSync(srcPath, destPath, { recursive: true, force: true });
|
|
85
89
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-skills-collection",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.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",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
},
|
|
20
20
|
"scripts": {
|
|
21
21
|
"build": "tsc",
|
|
22
|
+
"test": "bun test",
|
|
22
23
|
"prepublishOnly": "npm run build"
|
|
23
24
|
},
|
|
24
25
|
"keywords": [
|
|
@@ -33,12 +34,14 @@
|
|
|
33
34
|
"author": "Davide Ladisa <info@davideladisa.it>",
|
|
34
35
|
"license": "MIT",
|
|
35
36
|
"dependencies": {
|
|
36
|
-
"@opencode-ai/plugin": "^1.4.0"
|
|
37
|
+
"@opencode-ai/plugin": "^1.4.0",
|
|
38
|
+
"strip-json-comments": "^5.0.3"
|
|
37
39
|
},
|
|
38
40
|
"devDependencies": {
|
|
39
41
|
"@opencode-ai/sdk": "^1.4.0",
|
|
40
42
|
"@types/bun": "latest",
|
|
41
43
|
"@types/node": "^25.5.2",
|
|
44
|
+
"@types/strip-json-comments": "^0.0.30",
|
|
42
45
|
"typescript": "^6.0.2"
|
|
43
46
|
}
|
|
44
47
|
}
|