@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.
Files changed (3) hide show
  1. package/README.md +35 -0
  2. package/dist/index.js +153 -0
  3. 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
+ }