@vincemakes/kiso-skills-ext 0.44.0 → 0.45.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 CHANGED
@@ -17,6 +17,17 @@ Skill directories: `~/.kiso/skills/<name>/SKILL.md` (or the project-level
17
17
  `.kiso/skills/` after the trust gate). No configuration file — the
18
18
  extension scans the skills dir at startup.
19
19
 
20
+ A name found twice resolves to its first occurrence (root order, then
21
+ directory-name order within each root); the later one is reported with
22
+ the broken skills, never listed.
23
+
24
+ A host passes options instead: `createSkillsExtension({ roots, include })`.
25
+ `roots` replaces the default scan with the host's directories, in order.
26
+ `include(entry)` decides which skills are active, and the model's index,
27
+ `read_skill`, the catalog and the count are all built from that one
28
+ filtered list, so a skill the host turned off cannot be loaded on
29
+ request either.
30
+
20
31
  ## Invoking a skill yourself
21
32
 
22
33
  `/skill <name> [args]` sends a skill as your turn: its SKILL.md body, then
@@ -37,9 +37,19 @@ function kisoHome() {
37
37
  return process.env.KISO_HOME ?? join(homedir(), ".kiso");
38
38
  }
39
39
 
40
- export default async function createSkillsExtension() {
41
- const skillsDir = process.env.KISO_SKILLS_DIR ?? join(kisoHome(), "skills");
42
- const { index, broken } = loadIndex(skillsDir);
40
+ /**
41
+ * `options` are for a host; the CLI passes none.
42
+ * - `roots`: directories to scan, in order. Given, they replace the
43
+ * default (KISO_SKILLS_DIR, else $KISO_HOME/skills); the env var is not read.
44
+ * - `include(entry)`: false drops the skill from the ONE active index that
45
+ * the prompt, `read_skill`, the catalog and the count are all built
46
+ * from, so no surface can offer a skill another one hides. `broken`
47
+ * (installation diagnostics) is not filtered.
48
+ */
49
+ export default async function createSkillsExtension(options = {}) {
50
+ const roots = options.roots ?? [process.env.KISO_SKILLS_DIR ?? join(kisoHome(), "skills")];
51
+ const { index: loaded, broken } = firstNameWins(roots.map((root) => loadIndex(root)));
52
+ const index = options.include === undefined ? loaded : loaded.filter((s) => options.include(entryOf(s)));
43
53
  // finding #8: no persistent resources — SKILL.md files are read per call;
44
54
  // nothing is spawned or connected — no dispose is needed, explicitly.
45
55
  const catalog = skillsCatalog(index, broken);
@@ -137,13 +147,43 @@ function loadIndex(skillsDir) {
137
147
  return { index, broken };
138
148
  }
139
149
 
150
+ /** A name found twice resolves to its FIRST occurrence — root order, then
151
+ * the directory sort within a root — and every later one is reported in
152
+ * `broken`, never listed. `read_skill` always served the first; the index
153
+ * used to list both, offering a skill nobody could load. Resolved before
154
+ * a host's `include`, so a filter cannot change which one wins. */
155
+ function firstNameWins(scans) {
156
+ const index = [];
157
+ const broken = [];
158
+ const winners = new Map();
159
+ scans.forEach((scan, root) => {
160
+ broken.push(...scan.broken);
161
+ for (const skill of scan.index) {
162
+ const first = winners.get(skill.name);
163
+ if (first === undefined) {
164
+ winners.set(skill.name, { skill, root });
165
+ index.push(skill);
166
+ continue;
167
+ }
168
+ const where = first.root === root ? first.skill.dir : `${first.skill.dir} in an earlier root`;
169
+ broken.push({ dir: skill.dir, reason: `duplicate name "${skill.name}" — already provided by ${where}` });
170
+ }
171
+ });
172
+ return { index, broken };
173
+ }
174
+
175
+ /** The published entry shape — the catalog's, and what `include` is shown. */
176
+ function entryOf({ name, description, dir, path, userInvocable }) {
177
+ return { name, description, dir, path, userInvocable };
178
+ }
179
+
140
180
  /** 0.40.0 — what the CLI reads to let a PERSON invoke a skill. `body` reads
141
181
  * the file at call time (finding #8: nothing is held), strips the
142
182
  * frontmatter, and refuses — never truncates — a body over the cap: a
143
183
  * skill cut in half is a different instruction than the one written. */
144
184
  function skillsCatalog(index, broken) {
145
185
  return {
146
- entries: index.map(({ name, description, dir, path, userInvocable }) => ({ name, description, dir, path, userInvocable })),
186
+ entries: index.map(entryOf),
147
187
  broken: broken.map(({ dir, reason }) => ({ dir, reason })),
148
188
  body(name) {
149
189
  const skill = index.find((s) => s.name === name);
package/index.d.ts CHANGED
@@ -16,20 +16,35 @@ type SkillsExtension = KisoExtension & { readonly skills?: number; readonly cata
16
16
  /** 0.40.0: the same scan, for the CLI's `/skill` and `/skills`. `body` reads
17
17
  * the file at call time and returns the SKILL.md body without its
18
18
  * frontmatter, or the reason it cannot (over the cap, unreadable). */
19
+ export interface SkillsCatalogEntry {
20
+ readonly name: string;
21
+ readonly description: string;
22
+ /** the directory under the skills root the skill was found in */
23
+ readonly dir: string;
24
+ readonly path: string;
25
+ /** false only for `user-invocable: false` — the model may still load it */
26
+ readonly userInvocable: boolean;
27
+ }
28
+
19
29
  export interface SkillsCatalog {
20
- readonly entries: readonly {
21
- readonly name: string;
22
- readonly description: string;
23
- /** the directory under the skills root the skill was found in */
24
- readonly dir: string;
25
- readonly path: string;
26
- /** false only for `user-invocable: false` — the model may still load it */
27
- readonly userInvocable: boolean;
28
- }[];
30
+ readonly entries: readonly SkillsCatalogEntry[];
29
31
  /** the loader's own reason per skipped entry — the words the model's warning line uses */
30
32
  readonly broken: readonly { readonly dir: string; readonly reason: string }[];
31
33
  body(name: string): { readonly body: string } | { readonly error: string };
32
34
  }
33
35
 
34
- declare const createSkillsExtension: () => SkillsExtension | Promise<SkillsExtension>;
36
+ /** For a host; the CLI passes none, and omitting both keeps the default scan. */
37
+ export interface SkillsExtensionOptions {
38
+ /** Directories to scan, in order. Given → replaces the default
39
+ * (KISO_SKILLS_DIR, else $KISO_HOME/skills); the env var is not read.
40
+ * A name found twice resolves to the first occurrence (root order,
41
+ * then directory order); later ones are reported in `broken`. */
42
+ readonly roots?: readonly string[];
43
+ /** false → the skill is absent from the model's index, from `read_skill`,
44
+ * from the catalog and from the count, which are all built from one
45
+ * filtered list. Runs after duplicates resolve; `broken` is not filtered. */
46
+ readonly include?: (entry: SkillsCatalogEntry) => boolean;
47
+ }
48
+
49
+ declare const createSkillsExtension: (options?: SkillsExtensionOptions) => SkillsExtension | Promise<SkillsExtension>;
35
50
  export default createSkillsExtension;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-skills-ext",
3
- "version": "0.44.0",
3
+ "version": "0.45.0",
4
4
  "description": "kiso official skills extension \u2014 two-tier progressive skills, kernel untouched",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -23,7 +23,7 @@
23
23
  "test": "vitest run"
24
24
  },
25
25
  "devDependencies": {
26
- "@vincemakes/kiso-core": "0.44.0",
26
+ "@vincemakes/kiso-core": "0.45.0",
27
27
  "@types/node": "^26.1.2",
28
28
  "typescript": "^5.7.2",
29
29
  "vitest": "^3.0.0"