@batonfx/skills 0.4.0 → 0.4.2

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.
@@ -1,248 +0,0 @@
1
- import { describe, expect, it } from "@effect/vitest"
2
- import { Effect, FileSystem, Layer, Path, PlatformError, Stream } from "effect"
3
- import { SkillSource } from "@batonfx/core"
4
- import { SkillLoader } from "../src/index"
5
-
6
- const encoder = new TextEncoder()
7
-
8
- interface ReadCounts {
9
- readonly full: Record<string, number>
10
- readonly streamed: Record<string, number>
11
- }
12
-
13
- const notFound = (method: string, path: string) =>
14
- PlatformError.systemError({
15
- _tag: "NotFound",
16
- module: "SkillLoaderTest",
17
- method,
18
- description: "not found",
19
- pathOrDescriptor: path,
20
- })
21
-
22
- const testFsLayer = (
23
- files: Readonly<Record<string, string>>,
24
- directories: Readonly<Record<string, ReadonlyArray<string>>>,
25
- reads: ReadCounts = { full: {}, streamed: {} },
26
- ) =>
27
- FileSystem.layerNoop({
28
- exists: (path) => Effect.succeed(path in files || path in directories),
29
- readDirectory: (path) => {
30
- const entries = directories[path]
31
- return entries === undefined ? Effect.fail(notFound("readDirectory", path)) : Effect.succeed([...entries])
32
- },
33
- readFileString: (path) => {
34
- const content = files[path]
35
- return content === undefined
36
- ? Effect.fail(notFound("readFileString", path))
37
- : Effect.sync(() => {
38
- reads.full[path] = (reads.full[path] ?? 0) + 1
39
- return content
40
- })
41
- },
42
- stream: (path, options) => {
43
- const content = files[path]
44
- return content === undefined
45
- ? Stream.fail(notFound("stream", path))
46
- : Stream.sync(() => {
47
- reads.streamed[path] = (reads.streamed[path] ?? 0) + 1
48
- const bytesToRead = Number(options?.bytesToRead ?? content.length)
49
- return encoder.encode(content.slice(0, bytesToRead))
50
- })
51
- },
52
- })
53
-
54
- const loaderTestLayer = (
55
- options: Parameters<typeof SkillLoader.layer>[0],
56
- files: Readonly<Record<string, string>>,
57
- directories: Readonly<Record<string, ReadonlyArray<string>>>,
58
- reads?: ReadCounts,
59
- ) => SkillLoader.layer(options).pipe(Layer.provide(Layer.mergeAll(testFsLayer(files, directories, reads), Path.layer)))
60
-
61
- describe("SkillLoader", () => {
62
- it.effect("parses frontmatter and leaves body lazy", () => {
63
- const reads: ReadCounts = { full: {}, streamed: {} }
64
- const path = "/repo/.agents/skills/review/SKILL.md"
65
- const files = {
66
- [path]: `---
67
- name: review
68
- description: Review code carefully
69
- when-to-use: before merging
70
- allowed-tools: read grep
71
- disableModelInvocation: false
72
- userInvocable: true
73
- contextFork: true
74
- agent: reviewer
75
- model: fast
76
- paths: ["packages/core/**", "docs/spec/**"]
77
- ---
78
- # Review body
79
- Use the checklist.
80
- `,
81
- }
82
- const directories = { "/repo/.agents/skills": ["review/SKILL.md"] }
83
- return Effect.gen(function* () {
84
- const source = yield* SkillSource.SkillSource
85
- const all = yield* source.all
86
- const found = yield* source.get("review")
87
-
88
- expect(all).toHaveLength(1)
89
- expect(found).toBe(all[0])
90
- expect(all[0]?.frontmatter).toMatchObject({
91
- name: "review",
92
- description: "Review code carefully",
93
- whenToUse: "before merging",
94
- allowedTools: ["read", "grep"],
95
- disableModelInvocation: false,
96
- userInvocable: true,
97
- contextFork: true,
98
- agent: "reviewer",
99
- model: "fast",
100
- paths: ["packages/core/**", "docs/spec/**"],
101
- })
102
- expect(all[0]?.listing).toBe("- review: Review code carefully")
103
- expect(reads.streamed[path]).toBe(1)
104
- expect(reads.full[path]).toBeUndefined()
105
-
106
- const body = yield* all[0]!.body
107
-
108
- expect(body).toContain("# Review body")
109
- expect(reads.full[path]).toBe(1)
110
- }).pipe(Effect.provide(loaderTestLayer({ cwd: "/repo", roots: [".agents/skills"] }, files, directories, reads)))
111
- })
112
-
113
- it.effect("uses standard names for nested skills and lets later roots win collisions", () => {
114
- const files = {
115
- "/repo/a/frontend/lint/SKILL.md": `---
116
- name: lint
117
- description: Lint frontend
118
- ---
119
- body a`,
120
- "/repo/a/dup/SKILL.md": `---
121
- name: dup
122
- description: First duplicate
123
- ---
124
- body first`,
125
- "/repo/b/dup/SKILL.md": `---
126
- name: dup
127
- description: Second duplicate
128
- ---
129
- body second`,
130
- }
131
- const directories = {
132
- "/repo/a": ["frontend/lint/SKILL.md", "dup/SKILL.md"],
133
- "/repo/b": ["dup/SKILL.md"],
134
- }
135
- return Effect.gen(function* () {
136
- const source = yield* SkillSource.SkillSource
137
- const all = yield* source.all
138
- const nested = yield* source.get("lint")
139
- const duplicate = yield* source.get("dup")
140
-
141
- expect(all.map((skill) => skill.frontmatter.name)).toEqual(["dup", "lint"])
142
- expect(nested?.frontmatter.description).toBe("Lint frontend")
143
- expect(duplicate?.frontmatter.description).toBe("Second duplicate")
144
- }).pipe(Effect.provide(loaderTestLayer({ cwd: "/repo", roots: ["a", "b"] }, files, directories)))
145
- })
146
-
147
- it.effect("fails typed for invalid frontmatter and keeps user-only skills addressable", () => {
148
- const files = {
149
- "/repo/skills/user-only/SKILL.md": `---
150
- name: user-only
151
- description: User only
152
- disableModelInvocation: true
153
- ---
154
- body`,
155
- "/repo/skills/bad/SKILL.md": `---
156
- name: bad
157
- ---
158
- body`,
159
- }
160
- const directories = { "/repo/skills": ["user-only/SKILL.md", "bad/SKILL.md"] }
161
- return Effect.gen(function* () {
162
- const failure = yield* Effect.flip(
163
- SkillSource.SkillSource.pipe(
164
- Effect.provide(loaderTestLayer({ cwd: "/repo", roots: ["skills"] }, files, directories)),
165
- ),
166
- )
167
-
168
- expect(failure._tag).toBe("@batonfx/core/SkillSourceError")
169
-
170
- const goodSource = yield* SkillSource.SkillSource.pipe(
171
- Effect.provide(
172
- loaderTestLayer(
173
- { cwd: "/repo", roots: ["skills"] },
174
- {
175
- "/repo/skills/user-only/SKILL.md": files["/repo/skills/user-only/SKILL.md"] ?? "",
176
- },
177
- { "/repo/skills": ["user-only/SKILL.md"] },
178
- ),
179
- ),
180
- )
181
- const userOnly = yield* goodSource.get("user-only")
182
-
183
- expect(userOnly).toBeDefined()
184
- expect(SkillSource.selectListings(userOnly === undefined ? [] : [userOnly], 1_000, [])).toEqual([])
185
- })
186
- })
187
-
188
- it.effect("enforces required standard name, directory equality, and description length", () => {
189
- const cases = [
190
- {
191
- file: "/repo/skills/missing/SKILL.md",
192
- content: `---
193
- description: Missing name
194
- ---
195
- body`,
196
- },
197
- {
198
- file: "/repo/skills/directory/SKILL.md",
199
- content: `---
200
- name: different
201
- description: Mismatched directory
202
- ---
203
- body`,
204
- },
205
- {
206
- file: "/repo/skills/invalid/SKILL.md",
207
- content: `---
208
- name: Invalid_Name
209
- description: Invalid name
210
- ---
211
- body`,
212
- },
213
- {
214
- file: "/repo/skills/double--dash/SKILL.md",
215
- content: `---
216
- name: double--dash
217
- description: Consecutive hyphens are invalid
218
- ---
219
- body`,
220
- },
221
- {
222
- file: "/repo/skills/long/SKILL.md",
223
- content: `---
224
- name: long
225
- description: ${"x".repeat(1025)}
226
- ---
227
- body`,
228
- },
229
- ]
230
- return Effect.gen(function* () {
231
- for (const testCase of cases) {
232
- const relative = testCase.file.slice("/repo/skills/".length)
233
- const failure = yield* Effect.flip(
234
- SkillSource.SkillSource.pipe(
235
- Effect.provide(
236
- loaderTestLayer(
237
- { cwd: "/repo", roots: ["skills"] },
238
- { [testCase.file]: testCase.content },
239
- { "/repo/skills": [relative] },
240
- ),
241
- ),
242
- ),
243
- )
244
- expect(failure._tag).toBe("@batonfx/core/SkillSourceError")
245
- }
246
- })
247
- })
248
- })
package/tsconfig.json DELETED
@@ -1,7 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.json",
3
- "compilerOptions": {
4
- "rootDir": "."
5
- },
6
- "include": ["src/**/*.ts", "test/**/*.ts"]
7
- }