@ycodium-ai/plugin-skill-discovery 0.2.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.
- package/README.md +35 -0
- package/dist/index.js +153 -0
- package/package.json +47 -0
package/README.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# @ycodium-ai/plugin-skill-discovery
|
|
2
|
+
|
|
3
|
+
Lists skill packages that other agent tools have already installed on this
|
|
4
|
+
machine. The listing is read-only: those directories belong to the other
|
|
5
|
+
tools.
|
|
6
|
+
|
|
7
|
+
## What it does
|
|
8
|
+
|
|
9
|
+
The `list` command scans Claude, Codex, Cursor, and the shared
|
|
10
|
+
`~/.agents/skills` tree. Provider homes come from the host's current instance
|
|
11
|
+
list, so a moved Claude or Codex home is still found. Skills with the same
|
|
12
|
+
name are merged; identical copies are marked as such.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
ycodium plugin add @ycodium-ai/plugin-skill-discovery
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The plugin asks to read provider home directories and `~/.agents/skills`. It
|
|
21
|
+
does not write or take over those files.
|
|
22
|
+
|
|
23
|
+
Ycodium ships it enabled as `core.skill-discovery`. Disabling it stops the
|
|
24
|
+
listing command. Uninstalling it writes a removal to the home patch so a
|
|
25
|
+
restart does not bring it back.
|
|
26
|
+
|
|
27
|
+
## Developing
|
|
28
|
+
|
|
29
|
+
Built from the Ycodium repository. The Node entry is bundled, but
|
|
30
|
+
`@ycodium-ai/plugin-api` stays external and is provided by the Ycodium host at
|
|
31
|
+
load time.
|
|
32
|
+
|
|
33
|
+
## License
|
|
34
|
+
|
|
35
|
+
UNLICENSED.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import * as NodeCrypto from "node:crypto";
|
|
3
|
+
import * as NodeOS from "node:os";
|
|
4
|
+
import * as NodePath from "node:path";
|
|
5
|
+
import { definePlugin } from "@ycodium-ai/plugin-api";
|
|
6
|
+
|
|
7
|
+
// src/discovery.ts
|
|
8
|
+
var SKILLS_SUBDIRECTORY = {
|
|
9
|
+
claude: "skills",
|
|
10
|
+
codex: "skills",
|
|
11
|
+
// 不是 `skills`。抄错这一个字符的后果是「Cursor 的技能一个都读不到」,而且不会报错。
|
|
12
|
+
cursor: "skills-cursor"
|
|
13
|
+
};
|
|
14
|
+
var DEFAULT_HOME_DIRECTORY = {
|
|
15
|
+
claude: ".claude",
|
|
16
|
+
codex: ".codex",
|
|
17
|
+
cursor: ".cursor"
|
|
18
|
+
};
|
|
19
|
+
var SHARED_HOME_DIRECTORY = ".agents";
|
|
20
|
+
var SHARED_SKILLS_SUBDIRECTORY = "skills";
|
|
21
|
+
var skillSourceRoots = (input) => {
|
|
22
|
+
const seen = /* @__PURE__ */ new Set();
|
|
23
|
+
const roots = [];
|
|
24
|
+
const push = (tool, path) => {
|
|
25
|
+
if (seen.has(path)) return;
|
|
26
|
+
seen.add(path);
|
|
27
|
+
roots.push({ tool, path });
|
|
28
|
+
};
|
|
29
|
+
push("shared", input.join(input.homedir, SHARED_HOME_DIRECTORY, SHARED_SKILLS_SUBDIRECTORY));
|
|
30
|
+
for (const tool of ["claude", "codex", "cursor"]) {
|
|
31
|
+
const subdirectory = SKILLS_SUBDIRECTORY[tool];
|
|
32
|
+
if (subdirectory === null) continue;
|
|
33
|
+
const homes = input.resolvedHomes[tool] ?? [];
|
|
34
|
+
if (homes.length === 0) {
|
|
35
|
+
push(tool, input.join(input.homedir, DEFAULT_HOME_DIRECTORY[tool], subdirectory));
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
for (const home of homes) push(tool, input.join(home, subdirectory));
|
|
39
|
+
}
|
|
40
|
+
return roots;
|
|
41
|
+
};
|
|
42
|
+
var mergeDiscoveredSkills = (discovered) => {
|
|
43
|
+
const byName = /* @__PURE__ */ new Map();
|
|
44
|
+
for (const skill of discovered) {
|
|
45
|
+
const existing = byName.get(skill.name);
|
|
46
|
+
if (existing === void 0) byName.set(skill.name, [skill]);
|
|
47
|
+
else existing.push(skill);
|
|
48
|
+
}
|
|
49
|
+
return [...byName.entries()].map(([name, sources]) => ({
|
|
50
|
+
name,
|
|
51
|
+
sources,
|
|
52
|
+
identical: sources.every((source) => source.digest === sources[0]?.digest)
|
|
53
|
+
})).sort((left, right) => left.name.localeCompare(right.name));
|
|
54
|
+
};
|
|
55
|
+
var parseSkillFrontMatter = (content) => {
|
|
56
|
+
const lines = content.split(/\r?\n/);
|
|
57
|
+
if (lines[0]?.trim() !== "---") return { description: null, version: null };
|
|
58
|
+
let description = null;
|
|
59
|
+
let version = null;
|
|
60
|
+
for (const line of lines.slice(1)) {
|
|
61
|
+
if (line.trim() === "---") break;
|
|
62
|
+
const match = /^(description|version):\s*(.+)$/.exec(line);
|
|
63
|
+
if (match === null) continue;
|
|
64
|
+
const value = (match[2] ?? "").trim().replace(/^["']|["']$/g, "");
|
|
65
|
+
if (value.length === 0) continue;
|
|
66
|
+
if (match[1] === "description" && description === null) description = value;
|
|
67
|
+
if (match[1] === "version" && version === null) version = value;
|
|
68
|
+
}
|
|
69
|
+
return { description, version };
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// src/index.ts
|
|
73
|
+
var SKILL_FILE = "SKILL.md";
|
|
74
|
+
var LIST_PAYLOAD_SCHEMA = {
|
|
75
|
+
type: "object",
|
|
76
|
+
properties: {},
|
|
77
|
+
additionalProperties: false
|
|
78
|
+
};
|
|
79
|
+
var toolForDriver = (driver) => driver === "claudeAgent" ? "claude" : driver === "codex" ? "codex" : driver === "cursor" ? "cursor" : null;
|
|
80
|
+
var digestOf = (content) => NodeCrypto.createHash("sha256").update(content, "utf8").digest("hex");
|
|
81
|
+
var scanRoot = async (ctx, root) => {
|
|
82
|
+
let entries;
|
|
83
|
+
try {
|
|
84
|
+
entries = await ctx.fs.readDir(root.path);
|
|
85
|
+
} catch {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
const found = [];
|
|
89
|
+
for (const entry of entries) {
|
|
90
|
+
if (entry.type !== "directory") continue;
|
|
91
|
+
const skillPath = NodePath.join(root.path, entry.name);
|
|
92
|
+
let content;
|
|
93
|
+
try {
|
|
94
|
+
content = await ctx.fs.readFile(NodePath.join(skillPath, SKILL_FILE));
|
|
95
|
+
} catch {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const { description, version } = parseSkillFrontMatter(content);
|
|
99
|
+
found.push({
|
|
100
|
+
tool: root.tool,
|
|
101
|
+
// 名字取目录名而不是 front matter 里的 `name`:调用它的是目录名,两者不一致时
|
|
102
|
+
// 显示 front matter 那个会让人按显示的名字去调,然后调不到。
|
|
103
|
+
name: entry.name,
|
|
104
|
+
path: skillPath,
|
|
105
|
+
digest: digestOf(content),
|
|
106
|
+
description,
|
|
107
|
+
version
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
return found;
|
|
111
|
+
};
|
|
112
|
+
var rootsFor = async (ctx) => {
|
|
113
|
+
const homes = {};
|
|
114
|
+
try {
|
|
115
|
+
for (const instance of await ctx.providers.listInstances()) {
|
|
116
|
+
const tool = toolForDriver(instance.driver);
|
|
117
|
+
if (tool === null || instance.homePath === null) continue;
|
|
118
|
+
(homes[tool] ??= []).push(instance.homePath);
|
|
119
|
+
}
|
|
120
|
+
} catch (error) {
|
|
121
|
+
ctx.log.warn("could not list provider instances; scanning the default locations only", {
|
|
122
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
return skillSourceRoots({
|
|
126
|
+
homedir: NodeOS.homedir(),
|
|
127
|
+
resolvedHomes: homes,
|
|
128
|
+
join: (...segments) => NodePath.join(...segments)
|
|
129
|
+
});
|
|
130
|
+
};
|
|
131
|
+
var list = async (ctx) => {
|
|
132
|
+
const roots = await rootsFor(ctx);
|
|
133
|
+
const discovered = [];
|
|
134
|
+
for (const root of roots) discovered.push(...await scanRoot(ctx, root));
|
|
135
|
+
return { skills: mergeDiscoveredSkills(discovered), scannedRoots: roots.length };
|
|
136
|
+
};
|
|
137
|
+
var index_default = definePlugin({
|
|
138
|
+
id: "skill-discovery",
|
|
139
|
+
apiVersion: 2,
|
|
140
|
+
activate: (ctx) => {
|
|
141
|
+
ctx.command({
|
|
142
|
+
name: "list",
|
|
143
|
+
description: "Lists the skill packages other agent tools have installed on this machine.",
|
|
144
|
+
// 纯读盘,没有任何写操作。
|
|
145
|
+
scope: "read",
|
|
146
|
+
payloadSchema: LIST_PAYLOAD_SCHEMA,
|
|
147
|
+
handle: () => list(ctx)
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
export {
|
|
152
|
+
index_default as default
|
|
153
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ycodium-ai/plugin-skill-discovery",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Lists skill packages other agent tools have installed on this machine.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"skills",
|
|
7
|
+
"ycodium",
|
|
8
|
+
"ycodium-plugin"
|
|
9
|
+
],
|
|
10
|
+
"license": "UNLICENSED",
|
|
11
|
+
"files": [
|
|
12
|
+
"dist"
|
|
13
|
+
],
|
|
14
|
+
"type": "module",
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@effect/vitest": "4.0.0-beta.103",
|
|
20
|
+
"@types/node": "24.12.4",
|
|
21
|
+
"esbuild": "0.28.1",
|
|
22
|
+
"vite-plus": "0.2.2",
|
|
23
|
+
"@ycodium-ai/plugin-api": "0.1.1"
|
|
24
|
+
},
|
|
25
|
+
"ycodium": {
|
|
26
|
+
"plugin": "./dist/index.js",
|
|
27
|
+
"displayName": "Skill discovery",
|
|
28
|
+
"description": "Lists skill packages other agent tools have installed on this machine.",
|
|
29
|
+
"icon": "blocks",
|
|
30
|
+
"permissions": [
|
|
31
|
+
{
|
|
32
|
+
"kind": "providers.homes"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"kind": "fs.read",
|
|
36
|
+
"paths": [
|
|
37
|
+
"~/.agents/skills"
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "node build.mjs",
|
|
44
|
+
"typecheck": "tsgo --noEmit",
|
|
45
|
+
"test": "vp test run"
|
|
46
|
+
}
|
|
47
|
+
}
|