@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.
- package/README.md +5 -0
- package/dist/github-catalog.d.ts +18 -0
- package/dist/github-catalog.js +53 -0
- package/dist/hosted-catalog.d.ts +64 -0
- package/dist/hosted-catalog.js +147 -0
- package/dist/http-catalog.d.ts +13 -0
- package/dist/http-catalog.js +19 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -37363
- package/dist/instructions-files.d.ts +14 -0
- package/dist/instructions-files.js +42 -0
- package/dist/s3-catalog.d.ts +16 -0
- package/dist/s3-catalog.js +33 -0
- package/dist/skill-document.d.ts +10 -0
- package/dist/skill-document.js +144 -0
- package/dist/skill-loader.d.ts +13 -0
- package/dist/skill-loader.js +56 -0
- package/package.json +29 -4
- package/.turbo/turbo-build.log +0 -5
- package/src/github-catalog.ts +0 -80
- package/src/hosted-catalog.ts +0 -270
- package/src/http-catalog.ts +0 -29
- package/src/index.ts +0 -6
- package/src/instructions-files.ts +0 -63
- package/src/s3-catalog.ts +0 -49
- package/src/skill-document.ts +0 -174
- package/src/skill-loader.ts +0 -112
- package/test/hosted-catalog.test.ts +0 -398
- package/test/instructions-files.test.ts +0 -57
- package/test/skill-loader.test.ts +0 -248
- package/tsconfig.json +0 -7
|
@@ -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
|
-
})
|