@batonfx/skills 0.4.3 → 0.6.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 +52 -2
- package/dist/github-catalog.d.ts +1 -2
- package/dist/github-catalog.js +10 -6
- package/dist/hosted-catalog.d.ts +12 -44
- package/dist/hosted-catalog.js +25 -29
- package/dist/http-catalog.d.ts +1 -2
- package/dist/http-catalog.js +10 -11
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/s3-catalog.d.ts +1 -2
- package/dist/s3-catalog.js +8 -4
- package/dist/skill-document.d.ts +16 -4
- package/dist/skill-document.js +15 -15
- package/dist/skill-loader.js +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,5 +1,55 @@
|
|
|
1
1
|
# `@batonfx/skills`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Focused composition guide for filesystem and hosted skill sources.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
bun add effect @effect/platform-bun @batonfx/core @batonfx/skills
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Imports
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { SkillSource } from "@batonfx/core"
|
|
15
|
+
import { SkillLoader } from "@batonfx/skills"
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Layer graph
|
|
19
|
+
|
|
20
|
+
```text
|
|
21
|
+
BunServices.layer
|
|
22
|
+
└─ provides FileSystem + Path
|
|
23
|
+
└─ SkillLoader.layer({ roots: [] })
|
|
24
|
+
└─ provides SkillSource.SkillSource
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Runnable program
|
|
28
|
+
|
|
29
|
+
Checked source: [`../../examples/package-composition-guides/src/skills.ts`](../../examples/package-composition-guides/src/skills.ts)
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { Console, Effect, Layer } from "effect"
|
|
33
|
+
import { BunServices } from "@effect/platform-bun"
|
|
34
|
+
import { SkillSource } from "@batonfx/core"
|
|
35
|
+
import { SkillLoader } from "@batonfx/skills"
|
|
36
|
+
|
|
37
|
+
const skillLayer = SkillLoader.layer({ roots: [] }).pipe(Layer.provide(BunServices.layer))
|
|
38
|
+
|
|
39
|
+
const program = SkillSource.SkillSource.use((source) =>
|
|
40
|
+
source.all.pipe(Effect.flatMap((skills) => Console.log(`discovered ${skills.length} skills`))),
|
|
41
|
+
).pipe(Effect.provide(skillLayer))
|
|
42
|
+
|
|
43
|
+
await Effect.runPromise(program)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Run `bun examples/package-composition-guides/src/skills.ts`.
|
|
47
|
+
|
|
48
|
+
## Errors, requirements, and resources
|
|
49
|
+
|
|
50
|
+
`BunServices.layer` provides the `FileSystem` and `Path` requirements used by `SkillLoader.layer`; the composed program has `R = never`, succeeds with `void`, and retains schema-backed `SkillSourceError` for filesystem, parsing, path, and validation failures. The layer owns platform services; this empty-root run performs no concurrent work and uses no timers, detached fibers, or unbounded buffers.
|
|
51
|
+
|
|
52
|
+
## More
|
|
53
|
+
|
|
54
|
+
- Current behavior: [Instructions and skills](../../docs/features/instructions-and-skills.md)
|
|
55
|
+
- Deeper example: [capstone local assistant](../../examples/capstone-local-assistant/)
|
package/dist/github-catalog.d.ts
CHANGED
|
@@ -10,9 +10,8 @@ export interface Options extends Limits {
|
|
|
10
10
|
readonly root?: string;
|
|
11
11
|
readonly manifestName?: string;
|
|
12
12
|
readonly apiBaseUrl?: string;
|
|
13
|
-
readonly source?: string;
|
|
14
13
|
}
|
|
15
14
|
/** @experimental Build a manifest-backed immutable GitHub catalog source. */
|
|
16
15
|
export declare const make: (options: Options) => SkillSource.Source<HttpClient.HttpClient | Crypto.Crypto>;
|
|
17
16
|
/** @experimental Build a manifest-backed immutable GitHub catalog layer. */
|
|
18
|
-
export declare const layer: (options: Options) => import("effect/Layer").Layer<SkillSource.SkillSource, SkillSource.SkillSourceError,
|
|
17
|
+
export declare const layer: (options: Options) => import("effect/Layer").Layer<SkillSource.SkillSource, SkillSource.SkillSourceError, Crypto.Crypto | HttpClient.HttpClient>;
|
package/dist/github-catalog.js
CHANGED
|
@@ -9,25 +9,29 @@ const encodedPath = (value) => value
|
|
|
9
9
|
.join("/");
|
|
10
10
|
/** @experimental Build a manifest-backed immutable GitHub catalog source. */
|
|
11
11
|
export const make = (options) => {
|
|
12
|
-
const
|
|
12
|
+
const validationSource = "github-skill-catalog";
|
|
13
13
|
if (!/^[0-9a-fA-F]{40}$|^[0-9a-fA-F]{64}$/.test(options.ref)) {
|
|
14
|
-
return Effect.fail(
|
|
14
|
+
return Effect.fail(SkillSource.SkillSourceError.make({
|
|
15
|
+
source: validationSource,
|
|
16
|
+
message: "GitHub skill catalog ref must be a commit id",
|
|
17
|
+
}));
|
|
15
18
|
}
|
|
16
19
|
if (!/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/.test(options.owner) ||
|
|
17
20
|
!/^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,98}[A-Za-z0-9])?$/.test(options.repo)) {
|
|
18
|
-
return Effect.fail(
|
|
21
|
+
return Effect.fail(SkillSource.SkillSourceError.make({ source: validationSource, message: "Invalid GitHub owner or repository" }));
|
|
19
22
|
}
|
|
23
|
+
const source = `github:${options.owner}/${options.repo}@${options.ref}`;
|
|
20
24
|
return Effect.gen(function* () {
|
|
21
25
|
if ((options.root?.length ?? 0) > 0)
|
|
22
26
|
yield* validateSkillPath(source, options.root ?? "");
|
|
23
27
|
yield* validateSkillPath(source, options.manifestName ?? "skills.json");
|
|
24
|
-
const apiBase = yield* Effect.fromResult(Url.fromString(options.apiBaseUrl ?? "https://api.github.com")).pipe(Effect.mapError((
|
|
28
|
+
const apiBase = yield* Effect.fromResult(Url.fromString(options.apiBaseUrl ?? "https://api.github.com")).pipe(Effect.mapError(() => SkillSource.SkillSourceError.make({ source, message: "Invalid GitHub API base URL" })));
|
|
25
29
|
if (apiBase.protocol !== "https:" ||
|
|
26
30
|
apiBase.username.length > 0 ||
|
|
27
31
|
apiBase.password.length > 0 ||
|
|
28
32
|
apiBase.search.length > 0 ||
|
|
29
33
|
apiBase.hash.length > 0) {
|
|
30
|
-
return yield*
|
|
34
|
+
return yield* SkillSource.SkillSourceError.make({ source, message: "Invalid GitHub API base URL" });
|
|
31
35
|
}
|
|
32
36
|
const base = apiBase.toString().replace(/\/$/, "");
|
|
33
37
|
const root = encodedPath(options.root ?? "");
|
|
@@ -40,7 +44,7 @@ export const make = (options) => {
|
|
|
40
44
|
"x-github-api-version": "2022-11-28",
|
|
41
45
|
};
|
|
42
46
|
return yield* makeHostedCatalog({
|
|
43
|
-
|
|
47
|
+
limits: options,
|
|
44
48
|
source,
|
|
45
49
|
manifestUrl,
|
|
46
50
|
manifestHeaders: headers,
|
package/dist/hosted-catalog.d.ts
CHANGED
|
@@ -1,43 +1,6 @@
|
|
|
1
|
-
import { Crypto, Effect
|
|
1
|
+
import { Crypto, Effect } from "effect";
|
|
2
2
|
import { HttpClient } from "effect/unstable/http";
|
|
3
3
|
import { SkillSource } from "@batonfx/core";
|
|
4
|
-
/** @experimental Baton hosted skill manifest entry. */
|
|
5
|
-
export declare const ManifestSkill: Schema.Struct<{
|
|
6
|
-
readonly name: Schema.String;
|
|
7
|
-
readonly description: Schema.String;
|
|
8
|
-
readonly skillPath: Schema.String;
|
|
9
|
-
readonly sha256: Schema.String;
|
|
10
|
-
readonly whenToUse: Schema.optionalKey<Schema.String>;
|
|
11
|
-
readonly allowedTools: Schema.optionalKey<Schema.$Array<Schema.String>>;
|
|
12
|
-
readonly disableModelInvocation: Schema.optionalKey<Schema.Boolean>;
|
|
13
|
-
readonly userInvocable: Schema.optionalKey<Schema.Boolean>;
|
|
14
|
-
readonly contextFork: Schema.optionalKey<Schema.Boolean>;
|
|
15
|
-
readonly agent: Schema.optionalKey<Schema.String>;
|
|
16
|
-
readonly model: Schema.optionalKey<Schema.String>;
|
|
17
|
-
readonly paths: Schema.optionalKey<Schema.$Array<Schema.String>>;
|
|
18
|
-
}>;
|
|
19
|
-
/** @experimental */
|
|
20
|
-
export type ManifestSkill = typeof ManifestSkill.Type;
|
|
21
|
-
/** @experimental Baton hosted skill manifest. */
|
|
22
|
-
export declare const Manifest: Schema.Struct<{
|
|
23
|
-
readonly version: Schema.Literal<1>;
|
|
24
|
-
readonly skills: Schema.$Array<Schema.Struct<{
|
|
25
|
-
readonly name: Schema.String;
|
|
26
|
-
readonly description: Schema.String;
|
|
27
|
-
readonly skillPath: Schema.String;
|
|
28
|
-
readonly sha256: Schema.String;
|
|
29
|
-
readonly whenToUse: Schema.optionalKey<Schema.String>;
|
|
30
|
-
readonly allowedTools: Schema.optionalKey<Schema.$Array<Schema.String>>;
|
|
31
|
-
readonly disableModelInvocation: Schema.optionalKey<Schema.Boolean>;
|
|
32
|
-
readonly userInvocable: Schema.optionalKey<Schema.Boolean>;
|
|
33
|
-
readonly contextFork: Schema.optionalKey<Schema.Boolean>;
|
|
34
|
-
readonly agent: Schema.optionalKey<Schema.String>;
|
|
35
|
-
readonly model: Schema.optionalKey<Schema.String>;
|
|
36
|
-
readonly paths: Schema.optionalKey<Schema.$Array<Schema.String>>;
|
|
37
|
-
}>>;
|
|
38
|
-
}>;
|
|
39
|
-
/** @experimental */
|
|
40
|
-
export type Manifest = typeof Manifest.Type;
|
|
41
4
|
/** @experimental Shared hosted-catalog limits and trusted tools. */
|
|
42
5
|
export interface Limits {
|
|
43
6
|
readonly manifestMaxBytes?: number;
|
|
@@ -46,8 +9,8 @@ export interface Limits {
|
|
|
46
9
|
readonly descriptionCap?: number;
|
|
47
10
|
readonly toolsBySkill?: Readonly<Record<string, ReadonlyArray<SkillSource.Skill["tools"][number]>>>;
|
|
48
11
|
}
|
|
49
|
-
|
|
50
|
-
|
|
12
|
+
interface MakeOptions {
|
|
13
|
+
readonly limits: Limits;
|
|
51
14
|
readonly source: string;
|
|
52
15
|
readonly manifestUrl: string;
|
|
53
16
|
readonly resolveSkillUrl: (skillPath: string) => Effect.Effect<string, SkillSource.SkillSourceError>;
|
|
@@ -55,10 +18,15 @@ export interface MakeOptions extends Limits {
|
|
|
55
18
|
readonly bodyHeaders?: Readonly<Record<string, string>>;
|
|
56
19
|
}
|
|
57
20
|
/** @experimental Validate one safe relative SKILL.md path. */
|
|
58
|
-
export declare const validateSkillPath:
|
|
21
|
+
export declare const validateSkillPath: {
|
|
22
|
+
(skillPath: string): (source: string) => Effect.Effect<string, SkillSource.SkillSourceError>;
|
|
23
|
+
(source: string, skillPath: string): Effect.Effect<string, SkillSource.SkillSourceError>;
|
|
24
|
+
};
|
|
59
25
|
/** @experimental Resolve a same-origin path beneath a manifest directory. */
|
|
60
|
-
export declare const resolveRelative:
|
|
26
|
+
export declare const resolveRelative: {
|
|
27
|
+
(manifestUrl: string, skillPath: string): (source: string) => Effect.Effect<string, SkillSource.SkillSourceError>;
|
|
28
|
+
(source: string, manifestUrl: string, skillPath: string): Effect.Effect<string, SkillSource.SkillSourceError>;
|
|
29
|
+
};
|
|
61
30
|
/** @experimental Build a hosted manifest source over Effect HTTP and Crypto services. */
|
|
62
31
|
export declare const make: (options: MakeOptions) => SkillSource.Source<HttpClient.HttpClient | Crypto.Crypto>;
|
|
63
|
-
|
|
64
|
-
export declare const layer: (options: MakeOptions) => Layer.Layer<SkillSource.SkillSource, SkillSource.SkillSourceError, HttpClient.HttpClient | Crypto.Crypto>;
|
|
32
|
+
export {};
|
package/dist/hosted-catalog.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { Crypto, Effect, Encoding,
|
|
1
|
+
import { Crypto, Effect, Encoding, Function, Schema, Stream } from "effect";
|
|
2
2
|
import { HttpClient, HttpClientRequest, HttpClientResponse, Url } from "effect/unstable/http";
|
|
3
3
|
import { SkillSource } from "@batonfx/core";
|
|
4
4
|
import { parseDocument, validateName } from "./skill-document.js";
|
|
5
|
-
|
|
6
|
-
export const ManifestSkill = Schema.Struct({
|
|
5
|
+
const ManifestSkill = Schema.Struct({
|
|
7
6
|
name: Schema.String,
|
|
8
7
|
description: Schema.String,
|
|
9
8
|
skillPath: Schema.String,
|
|
@@ -17,13 +16,12 @@ export const ManifestSkill = Schema.Struct({
|
|
|
17
16
|
model: Schema.optionalKey(Schema.String),
|
|
18
17
|
paths: Schema.optionalKey(Schema.Array(Schema.String)),
|
|
19
18
|
});
|
|
20
|
-
|
|
21
|
-
export const Manifest = Schema.Struct({
|
|
19
|
+
const Manifest = Schema.Struct({
|
|
22
20
|
version: Schema.Literal(1),
|
|
23
21
|
skills: Schema.Array(ManifestSkill),
|
|
24
22
|
});
|
|
25
23
|
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
26
|
-
const sourceError = (source, message, cause) =>
|
|
24
|
+
const sourceError = (source, message, cause) => SkillSource.SkillSourceError.make({ source, message, ...(cause === undefined ? {} : { cause }) });
|
|
27
25
|
const safeInteger = (source, name, value, minimum) => Number.isSafeInteger(value) && value >= minimum
|
|
28
26
|
? Effect.succeed(value)
|
|
29
27
|
: Effect.fail(sourceError(source, `${name} must be a safe integer >= ${minimum}`));
|
|
@@ -33,7 +31,7 @@ const request = (url, headers) => {
|
|
|
33
31
|
value = HttpClientRequest.setHeader(value, name, header);
|
|
34
32
|
return value;
|
|
35
33
|
};
|
|
36
|
-
const fetchBytes = (client, source, url, headers, maxBytes) => client.execute(request(url, headers)).pipe(Effect.flatMap(HttpClientResponse.filterStatusOk), Effect.mapError((
|
|
34
|
+
const fetchBytes = (client, source, url, headers, maxBytes) => client.execute(request(url, headers)).pipe(Effect.flatMap(HttpClientResponse.filterStatusOk), Effect.mapError(() => sourceError(source, "Hosted skill request failed")), Effect.flatMap((response) => response.stream.pipe(Stream.mapError(() => sourceError(source, "Hosted skill request failed")), Stream.runFoldEffect(() => ({ size: 0, chunks: [] }), (state, chunk) => {
|
|
37
35
|
const size = state.size + chunk.byteLength;
|
|
38
36
|
if (size > maxBytes) {
|
|
39
37
|
return Effect.fail(sourceError(source, `Hosted skill response exceeds ${maxBytes} bytes`));
|
|
@@ -67,7 +65,7 @@ const frontmatter = (entry) => ({
|
|
|
67
65
|
});
|
|
68
66
|
const sameFrontmatter = (left, right) => JSON.stringify(left) === JSON.stringify(right);
|
|
69
67
|
/** @experimental Validate one safe relative SKILL.md path. */
|
|
70
|
-
export const validateSkillPath = (source, skillPath) => {
|
|
68
|
+
export const validateSkillPath = Function.dual(2, (source, skillPath) => {
|
|
71
69
|
const segments = skillPath.split("/");
|
|
72
70
|
return skillPath.length > 0 &&
|
|
73
71
|
!skillPath.startsWith("/") &&
|
|
@@ -77,48 +75,48 @@ export const validateSkillPath = (source, skillPath) => {
|
|
|
77
75
|
!skillPath.includes("#") &&
|
|
78
76
|
segments.every((segment) => segment.length > 0 && segment !== "." && segment !== "..")
|
|
79
77
|
? Effect.succeed(skillPath)
|
|
80
|
-
: Effect.fail(sourceError(source,
|
|
81
|
-
};
|
|
78
|
+
: Effect.fail(sourceError(source, "Unsafe hosted skill path"));
|
|
79
|
+
});
|
|
82
80
|
/** @experimental Resolve a same-origin path beneath a manifest directory. */
|
|
83
|
-
export const resolveRelative = (source, manifestUrl, skillPath) => Effect.gen(function* () {
|
|
81
|
+
export const resolveRelative = Function.dual(3, (source, manifestUrl, skillPath) => Effect.gen(function* () {
|
|
84
82
|
yield* validateSkillPath(source, skillPath);
|
|
85
|
-
const manifest = yield* Effect.fromResult(Url.fromString(manifestUrl)).pipe(Effect.mapError((
|
|
86
|
-
const directory = yield* Effect.fromResult(Url.fromString(".", manifest)).pipe(Effect.mapError((
|
|
87
|
-
const resolved = yield* Effect.fromResult(Url.fromString(skillPath, directory)).pipe(Effect.mapError((
|
|
83
|
+
const manifest = yield* Effect.fromResult(Url.fromString(manifestUrl)).pipe(Effect.mapError(() => sourceError(source, "Invalid manifest URL")));
|
|
84
|
+
const directory = yield* Effect.fromResult(Url.fromString(".", manifest)).pipe(Effect.mapError(() => sourceError(source, "Invalid manifest directory URL")));
|
|
85
|
+
const resolved = yield* Effect.fromResult(Url.fromString(skillPath, directory)).pipe(Effect.mapError(() => sourceError(source, "Invalid hosted skill URL")));
|
|
88
86
|
if (resolved.origin !== manifest.origin || !resolved.pathname.startsWith(directory.pathname)) {
|
|
89
|
-
return yield*
|
|
87
|
+
return yield* sourceError(source, "Hosted skill path escapes manifest directory");
|
|
90
88
|
}
|
|
91
89
|
return resolved.toString();
|
|
92
|
-
});
|
|
90
|
+
}));
|
|
93
91
|
/** @experimental Build a hosted manifest source over Effect HTTP and Crypto services. */
|
|
94
92
|
export const make = (options) => Effect.gen(function* () {
|
|
95
93
|
const client = yield* HttpClient.HttpClient;
|
|
96
94
|
const crypto = yield* Crypto.Crypto;
|
|
97
|
-
const manifestMaxBytes = yield* safeInteger(options.source, "manifestMaxBytes", options.manifestMaxBytes ?? 1024 * 1024, 1);
|
|
98
|
-
const bodyMaxBytes = yield* safeInteger(options.source, "bodyMaxBytes", options.bodyMaxBytes ?? 1024 * 1024, 1);
|
|
99
|
-
const maxSkills = yield* safeInteger(options.source, "maxSkills", options.maxSkills ?? 1_000, 1);
|
|
100
|
-
const descriptionCap = yield* safeInteger(options.source, "descriptionCap", options.descriptionCap ?? SkillSource.DESCRIPTION_CAP, 0);
|
|
95
|
+
const manifestMaxBytes = yield* safeInteger(options.source, "manifestMaxBytes", options.limits.manifestMaxBytes ?? 1024 * 1024, 1);
|
|
96
|
+
const bodyMaxBytes = yield* safeInteger(options.source, "bodyMaxBytes", options.limits.bodyMaxBytes ?? 1024 * 1024, 1);
|
|
97
|
+
const maxSkills = yield* safeInteger(options.source, "maxSkills", options.limits.maxSkills ?? 1_000, 1);
|
|
98
|
+
const descriptionCap = yield* safeInteger(options.source, "descriptionCap", options.limits.descriptionCap ?? SkillSource.DESCRIPTION_CAP, 0);
|
|
101
99
|
const manifestBytes = yield* fetchBytes(client, options.source, options.manifestUrl, options.manifestHeaders, manifestMaxBytes);
|
|
102
100
|
const manifestText = yield* decodeText(options.source, manifestBytes);
|
|
103
101
|
const manifest = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(Manifest))(manifestText).pipe(Effect.mapError((error) => sourceError(options.source, "Invalid hosted skill manifest", error)));
|
|
104
102
|
if (manifest.skills.length > maxSkills) {
|
|
105
|
-
return yield*
|
|
103
|
+
return yield* sourceError(options.source, `Hosted skill manifest exceeds ${maxSkills} skills`);
|
|
106
104
|
}
|
|
107
105
|
const byName = new Map();
|
|
108
106
|
for (const entry of manifest.skills) {
|
|
109
107
|
yield* validateName(options.source, entry.name);
|
|
110
108
|
if (entry.description.length === 0 || entry.description.length > 1_024) {
|
|
111
|
-
return yield*
|
|
109
|
+
return yield* sourceError(options.source, "Hosted skill description must contain 1-1024 characters");
|
|
112
110
|
}
|
|
113
111
|
if (!/^[0-9a-f]{64}$/.test(entry.sha256)) {
|
|
114
|
-
return yield*
|
|
112
|
+
return yield* sourceError(options.source, `Invalid SHA-256 for hosted skill ${entry.name}`);
|
|
115
113
|
}
|
|
116
114
|
if (byName.has(entry.name)) {
|
|
117
|
-
return yield*
|
|
115
|
+
return yield* sourceError(options.source, `Duplicate hosted skill name: ${entry.name}`);
|
|
118
116
|
}
|
|
119
117
|
const pathSegments = entry.skillPath.split("/");
|
|
120
118
|
if (pathSegments.at(-1) !== "SKILL.md" || pathSegments.at(-2) !== entry.name) {
|
|
121
|
-
return yield*
|
|
119
|
+
return yield* sourceError(options.source, `Hosted skill path directory must match skill name: ${entry.name}`);
|
|
122
120
|
}
|
|
123
121
|
const skillUrl = yield* options.resolveSkillUrl(entry.skillPath);
|
|
124
122
|
const metadata = frontmatter(entry);
|
|
@@ -127,8 +125,8 @@ export const make = (options) => Effect.gen(function* () {
|
|
|
127
125
|
: Effect.fail(sourceError(options.source, `SHA-256 mismatch for hosted skill ${entry.name}`))))), Effect.flatMap((content) => parseDocument(options.source, content, entry.name).pipe(Effect.flatMap((document) => sameFrontmatter(document.frontmatter, metadata)
|
|
128
126
|
? Effect.succeed(document.body)
|
|
129
127
|
: Effect.fail(sourceError(options.source, `Frontmatter mismatch for hosted skill ${entry.name}`))))));
|
|
130
|
-
const tools = options.toolsBySkill !== undefined && Object.hasOwn(options.toolsBySkill, entry.name)
|
|
131
|
-
? (options.toolsBySkill[entry.name] ?? [])
|
|
128
|
+
const tools = options.limits.toolsBySkill !== undefined && Object.hasOwn(options.limits.toolsBySkill, entry.name)
|
|
129
|
+
? (options.limits.toolsBySkill[entry.name] ?? [])
|
|
132
130
|
: [];
|
|
133
131
|
byName.set(entry.name, {
|
|
134
132
|
frontmatter: metadata,
|
|
@@ -143,5 +141,3 @@ export const make = (options) => Effect.gen(function* () {
|
|
|
143
141
|
get: (name) => Effect.succeed(byName.get(name)),
|
|
144
142
|
};
|
|
145
143
|
});
|
|
146
|
-
/** @experimental One-source hosted catalog layer. */
|
|
147
|
-
export const layer = (options) => Layer.effect(SkillSource.SkillSource, make(options).pipe(Effect.map(SkillSource.SkillSource.of)));
|
package/dist/http-catalog.d.ts
CHANGED
|
@@ -5,9 +5,8 @@ import { type Limits } from "./hosted-catalog.js";
|
|
|
5
5
|
/** @experimental Generic HTTP skill catalog options. */
|
|
6
6
|
export interface Options extends Limits {
|
|
7
7
|
readonly manifestUrl: string;
|
|
8
|
-
readonly source?: string;
|
|
9
8
|
}
|
|
10
9
|
/** @experimental Build a generic HTTP catalog source. */
|
|
11
10
|
export declare const make: (options: Options) => SkillSource.Source<HttpClient.HttpClient | Crypto.Crypto>;
|
|
12
11
|
/** @experimental Build a generic HTTP catalog layer. */
|
|
13
|
-
export declare const layer: (options: Options) => import("effect/Layer").Layer<SkillSource.SkillSource, SkillSource.SkillSourceError,
|
|
12
|
+
export declare const layer: (options: Options) => import("effect/Layer").Layer<SkillSource.SkillSource, SkillSource.SkillSourceError, Crypto.Crypto | HttpClient.HttpClient>;
|
package/dist/http-catalog.js
CHANGED
|
@@ -2,18 +2,17 @@ import { Crypto, Effect } from "effect";
|
|
|
2
2
|
import { HttpClient, Url } from "effect/unstable/http";
|
|
3
3
|
import { SkillSource } from "@batonfx/core";
|
|
4
4
|
import { make as makeHostedCatalog, resolveRelative } from "./hosted-catalog.js";
|
|
5
|
-
const invalidUrl = (
|
|
5
|
+
const invalidUrl = () => SkillSource.SkillSourceError.make({ source: "http-skill-catalog", message: "Invalid manifest URL" });
|
|
6
6
|
/** @experimental Build a generic HTTP catalog source. */
|
|
7
|
-
export const make = (options) => {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
});
|
|
7
|
+
export const make = (options) => Effect.gen(function* () {
|
|
8
|
+
const parsed = yield* Effect.fromResult(Url.fromString(options.manifestUrl)).pipe(Effect.mapError(invalidUrl));
|
|
9
|
+
const source = `${parsed.origin}${parsed.pathname}`;
|
|
10
|
+
return yield* makeHostedCatalog({
|
|
11
|
+
limits: options,
|
|
12
|
+
source,
|
|
13
|
+
manifestUrl: options.manifestUrl,
|
|
14
|
+
resolveSkillUrl: (skillPath) => resolveRelative(source, options.manifestUrl, skillPath),
|
|
16
15
|
});
|
|
17
|
-
};
|
|
16
|
+
});
|
|
18
17
|
/** @experimental Build a generic HTTP catalog layer. */
|
|
19
18
|
export const layer = (options) => SkillSource.layer([make(options)]);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
export * as GitHubCatalog from "./github-catalog.js";
|
|
2
|
-
export * as HostedCatalog from "./hosted-catalog.js";
|
|
3
2
|
export * as HttpCatalog from "./http-catalog.js";
|
|
4
3
|
export * as InstructionFiles from "./instructions-files.js";
|
|
5
4
|
export * as S3Catalog from "./s3-catalog.js";
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
export * as GitHubCatalog from "./github-catalog.js";
|
|
2
|
-
export * as HostedCatalog from "./hosted-catalog.js";
|
|
3
2
|
export * as HttpCatalog from "./http-catalog.js";
|
|
4
3
|
export * as InstructionFiles from "./instructions-files.js";
|
|
5
4
|
export * as S3Catalog from "./s3-catalog.js";
|
package/dist/s3-catalog.d.ts
CHANGED
|
@@ -8,9 +8,8 @@ export interface Options extends Limits {
|
|
|
8
8
|
readonly region: string;
|
|
9
9
|
readonly prefix?: string;
|
|
10
10
|
readonly manifestName?: string;
|
|
11
|
-
readonly source?: string;
|
|
12
11
|
}
|
|
13
12
|
/** @experimental Build a manifest-backed S3 catalog source. */
|
|
14
13
|
export declare const make: (options: Options) => SkillSource.Source<HttpClient.HttpClient | Crypto.Crypto>;
|
|
15
14
|
/** @experimental Build a manifest-backed S3 catalog layer. */
|
|
16
|
-
export declare const layer: (options: Options) => import("effect/Layer").Layer<SkillSource.SkillSource, SkillSource.SkillSourceError,
|
|
15
|
+
export declare const layer: (options: Options) => import("effect/Layer").Layer<SkillSource.SkillSource, SkillSource.SkillSourceError, Crypto.Crypto | HttpClient.HttpClient>;
|
package/dist/s3-catalog.js
CHANGED
|
@@ -9,20 +9,24 @@ const segments = (value) => value
|
|
|
9
9
|
.join("/");
|
|
10
10
|
/** @experimental Build a manifest-backed S3 catalog source. */
|
|
11
11
|
export const make = (options) => {
|
|
12
|
-
const
|
|
12
|
+
const validationSource = "s3-skill-catalog";
|
|
13
13
|
if (!/^[a-z0-9](?:[a-z0-9-]{1,61})[a-z0-9]$/.test(options.bucket) ||
|
|
14
14
|
!/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])$/.test(options.region)) {
|
|
15
|
-
return Effect.fail(
|
|
15
|
+
return Effect.fail(SkillSource.SkillSourceError.make({
|
|
16
|
+
source: validationSource,
|
|
17
|
+
message: "Invalid S3 bucket or region for hosted skill catalog",
|
|
18
|
+
}));
|
|
16
19
|
}
|
|
17
20
|
return Effect.gen(function* () {
|
|
18
21
|
if ((options.prefix?.length ?? 0) > 0)
|
|
19
|
-
yield* validateSkillPath(
|
|
22
|
+
yield* validateSkillPath(validationSource, options.prefix ?? "");
|
|
23
|
+
const source = `s3://${options.bucket}/${options.prefix ?? ""}`;
|
|
20
24
|
yield* validateSkillPath(source, options.manifestName ?? "skills.json");
|
|
21
25
|
const prefix = segments(options.prefix ?? "");
|
|
22
26
|
const manifestName = segments(options.manifestName ?? "skills.json");
|
|
23
27
|
const manifestUrl = `https://${options.bucket}.s3.${options.region}.amazonaws.com/${prefix.length === 0 ? "" : `${prefix}/`}${manifestName}`;
|
|
24
28
|
return yield* makeHostedCatalog({
|
|
25
|
-
|
|
29
|
+
limits: options,
|
|
26
30
|
source,
|
|
27
31
|
manifestUrl,
|
|
28
32
|
resolveSkillUrl: (skillPath) => resolveRelative(source, manifestUrl, skillPath),
|
package/dist/skill-document.d.ts
CHANGED
|
@@ -4,7 +4,19 @@ export interface ParsedDocument {
|
|
|
4
4
|
readonly frontmatter: SkillSource.Frontmatter;
|
|
5
5
|
readonly body: string;
|
|
6
6
|
}
|
|
7
|
-
export declare const splitDocument:
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
export declare const splitDocument: {
|
|
8
|
+
(content: string): (source: string) => Effect.Effect<readonly [string, string], SkillSource.SkillSourceError>;
|
|
9
|
+
(source: string, content: string): Effect.Effect<readonly [string, string], SkillSource.SkillSourceError>;
|
|
10
|
+
};
|
|
11
|
+
export declare const validateName: {
|
|
12
|
+
(name: string): (source: string) => Effect.Effect<string, SkillSource.SkillSourceError>;
|
|
13
|
+
(source: string, name: string): Effect.Effect<string, SkillSource.SkillSourceError>;
|
|
14
|
+
};
|
|
15
|
+
export declare const parseFrontmatter: {
|
|
16
|
+
(block: string, directoryName: string): (source: string) => Effect.Effect<SkillSource.Frontmatter, SkillSource.SkillSourceError>;
|
|
17
|
+
(source: string, block: string, directoryName: string): Effect.Effect<SkillSource.Frontmatter, SkillSource.SkillSourceError>;
|
|
18
|
+
};
|
|
19
|
+
export declare const parseDocument: {
|
|
20
|
+
(content: string, directoryName: string): (source: string) => Effect.Effect<ParsedDocument, SkillSource.SkillSourceError>;
|
|
21
|
+
(source: string, content: string, directoryName: string): Effect.Effect<ParsedDocument, SkillSource.SkillSourceError>;
|
|
22
|
+
};
|
package/dist/skill-document.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Effect } from "effect";
|
|
1
|
+
import { Effect, Function } from "effect";
|
|
2
2
|
import { SkillSource } from "@batonfx/core";
|
|
3
|
-
const sourceError = (source, message, cause) =>
|
|
3
|
+
const sourceError = (source, message, cause) => SkillSource.SkillSourceError.make({ source, message, ...(cause === undefined ? {} : { cause }) });
|
|
4
4
|
const normalizeKey = (key) => key.replace(/[-_]/g, "").toLowerCase();
|
|
5
5
|
const stripQuotes = (value) => {
|
|
6
6
|
const trimmed = value.trim();
|
|
@@ -98,32 +98,32 @@ const parseHeader = (source, block) => Effect.sync(() => {
|
|
|
98
98
|
}
|
|
99
99
|
return parsed;
|
|
100
100
|
}).pipe(Effect.catchCause((cause) => Effect.fail(sourceError(source, "Invalid SKILL.md frontmatter", cause))));
|
|
101
|
-
export const splitDocument = (source, content) => Effect.gen(function* () {
|
|
101
|
+
export const splitDocument = Function.dual(2, (source, content) => Effect.gen(function* () {
|
|
102
102
|
const normalized = content.replace(/^\uFEFF/, "").replace(/\r\n/g, "\n");
|
|
103
103
|
const lines = normalized.split("\n");
|
|
104
104
|
if (lines[0] !== "---") {
|
|
105
|
-
return yield*
|
|
105
|
+
return yield* sourceError(source, "Invalid SKILL.md document: missing opening frontmatter fence");
|
|
106
106
|
}
|
|
107
107
|
const close = lines.findIndex((line, index) => index > 0 && line === "---");
|
|
108
108
|
if (close === -1) {
|
|
109
|
-
return yield*
|
|
109
|
+
return yield* sourceError(source, "Invalid SKILL.md document: missing closing frontmatter fence");
|
|
110
110
|
}
|
|
111
111
|
return [lines.slice(1, close).join("\n"), lines.slice(close + 1).join("\n")];
|
|
112
|
-
});
|
|
113
|
-
export const validateName = (source, name) => /^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/.test(name) && !name.includes("--")
|
|
112
|
+
}));
|
|
113
|
+
export const validateName = Function.dual(2, (source, name) => /^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/.test(name) && !name.includes("--")
|
|
114
114
|
? Effect.succeed(name)
|
|
115
|
-
: Effect.fail(sourceError(source, "SKILL.md name must be 1-64 lowercase alphanumeric or single-hyphen-separated characters"));
|
|
116
|
-
export const parseFrontmatter = (source, block, directoryName) => Effect.gen(function* () {
|
|
115
|
+
: Effect.fail(sourceError(source, "SKILL.md name must be 1-64 lowercase alphanumeric or single-hyphen-separated characters")));
|
|
116
|
+
export const parseFrontmatter = Function.dual(3, (source, block, directoryName) => Effect.gen(function* () {
|
|
117
117
|
const parsed = yield* parseHeader(source, block);
|
|
118
118
|
if (parsed.name === undefined) {
|
|
119
|
-
return yield*
|
|
119
|
+
return yield* sourceError(source, "SKILL.md frontmatter requires name");
|
|
120
120
|
}
|
|
121
121
|
yield* validateName(source, parsed.name);
|
|
122
122
|
if (parsed.name !== directoryName) {
|
|
123
|
-
return yield*
|
|
123
|
+
return yield* sourceError(source, `SKILL.md name must match directory ${directoryName}`);
|
|
124
124
|
}
|
|
125
125
|
if (parsed.description === undefined || parsed.description.length === 0 || parsed.description.length > 1_024) {
|
|
126
|
-
return yield*
|
|
126
|
+
return yield* sourceError(source, "SKILL.md description must contain 1-1024 characters");
|
|
127
127
|
}
|
|
128
128
|
return {
|
|
129
129
|
name: parsed.name,
|
|
@@ -137,8 +137,8 @@ export const parseFrontmatter = (source, block, directoryName) => Effect.gen(fun
|
|
|
137
137
|
...(parsed.model === undefined ? {} : { model: parsed.model }),
|
|
138
138
|
...(parsed.paths === undefined ? {} : { paths: parsed.paths }),
|
|
139
139
|
};
|
|
140
|
-
});
|
|
141
|
-
export const parseDocument = (source, content, directoryName) => Effect.gen(function* () {
|
|
140
|
+
}));
|
|
141
|
+
export const parseDocument = Function.dual(3, (source, content, directoryName) => Effect.gen(function* () {
|
|
142
142
|
const [header, body] = yield* splitDocument(source, content);
|
|
143
143
|
return { frontmatter: yield* parseFrontmatter(source, header, directoryName), body };
|
|
144
|
-
});
|
|
144
|
+
}));
|
package/dist/skill-loader.js
CHANGED
|
@@ -3,7 +3,7 @@ import { SkillSource } from "@batonfx/core";
|
|
|
3
3
|
import { parseDocument, parseFrontmatter, splitDocument } from "./skill-document.js";
|
|
4
4
|
const DEFAULT_ROOTS = [".agents/skills", ".claude/skills", ".pi/skills"];
|
|
5
5
|
const decoder = new TextDecoder();
|
|
6
|
-
const sourceError = (source, message, cause) =>
|
|
6
|
+
const sourceError = (source, message, cause) => SkillSource.SkillSourceError.make({ source, message, ...(cause === undefined ? {} : { cause }) });
|
|
7
7
|
const mapPlatformError = (source, error) => sourceError(source, error.message, error);
|
|
8
8
|
const readHeader = (fs, source, bytes) => fs.stream(source, { bytesToRead: bytes, chunkSize: bytes }).pipe(Stream.runFold(() => "", (content, chunk) => `${content}${decoder.decode(chunk)}`), Effect.mapError((error) => mapPlatformError(source, error)));
|
|
9
9
|
const loadSkill = (fs, path, file, relativeFile, descriptionCap, frontmatterMaxBytes) => Effect.gen(function* () {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "@batonfx/skills",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.6.0",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
7
7
|
"exports": {
|
|
@@ -17,13 +17,13 @@
|
|
|
17
17
|
"typecheck": "bun tsc --noEmit"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@batonfx/core": "0.
|
|
20
|
+
"@batonfx/core": "0.6.0",
|
|
21
21
|
"effect": "4.0.0-beta.93"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@effect/vitest": "4.0.0-beta.93",
|
|
25
25
|
"@types/bun": "1.3.13",
|
|
26
|
-
"typescript": "
|
|
26
|
+
"typescript": "7.0.2",
|
|
27
27
|
"vitest": "4.0.16"
|
|
28
28
|
},
|
|
29
29
|
"description": "Filesystem and hosted skill sources for Baton agents",
|