@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
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Effect, FileSystem, Path, PlatformError } from "effect";
|
|
2
|
+
/** @experimental Loaded instruction-file content. */
|
|
3
|
+
export interface InstructionFile {
|
|
4
|
+
readonly path: string;
|
|
5
|
+
readonly content: string;
|
|
6
|
+
}
|
|
7
|
+
/** @experimental Instruction-file discovery options. */
|
|
8
|
+
export interface LoadInstructionFilesOptions {
|
|
9
|
+
readonly filenames?: ReadonlyArray<string>;
|
|
10
|
+
readonly cwd?: string;
|
|
11
|
+
readonly globalFiles?: ReadonlyArray<string>;
|
|
12
|
+
}
|
|
13
|
+
/** @experimental Load AGENTS.md / CLAUDE.md instruction files. */
|
|
14
|
+
export declare const loadInstructionFiles: (options?: LoadInstructionFilesOptions) => Effect.Effect<ReadonlyArray<InstructionFile>, PlatformError.PlatformError, FileSystem.FileSystem | Path.Path>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Effect, FileSystem, Path, PlatformError } from "effect";
|
|
2
|
+
const DEFAULT_FILENAMES = ["AGENTS.md", "CLAUDE.md"];
|
|
3
|
+
const readIfExists = (fs, file) => Effect.gen(function* () {
|
|
4
|
+
if (!(yield* fs.exists(file)))
|
|
5
|
+
return undefined;
|
|
6
|
+
const content = yield* fs.readFileString(file);
|
|
7
|
+
return { path: file, content };
|
|
8
|
+
});
|
|
9
|
+
const ancestors = (path, cwd) => {
|
|
10
|
+
const directories = [];
|
|
11
|
+
let cursor = path.resolve(cwd);
|
|
12
|
+
while (true) {
|
|
13
|
+
directories.push(cursor);
|
|
14
|
+
const parent = path.dirname(cursor);
|
|
15
|
+
if (parent === cursor)
|
|
16
|
+
break;
|
|
17
|
+
cursor = parent;
|
|
18
|
+
}
|
|
19
|
+
return directories.toReversed();
|
|
20
|
+
};
|
|
21
|
+
/** @experimental Load AGENTS.md / CLAUDE.md instruction files. */
|
|
22
|
+
export const loadInstructionFiles = (options = {}) => Effect.gen(function* () {
|
|
23
|
+
const fs = yield* FileSystem.FileSystem;
|
|
24
|
+
const path = yield* Path.Path;
|
|
25
|
+
const filenames = options.filenames ?? DEFAULT_FILENAMES;
|
|
26
|
+
const files = [];
|
|
27
|
+
for (const globalFile of options.globalFiles ?? []) {
|
|
28
|
+
const loaded = yield* readIfExists(fs, globalFile);
|
|
29
|
+
if (loaded !== undefined)
|
|
30
|
+
files.push(loaded);
|
|
31
|
+
}
|
|
32
|
+
for (const directory of ancestors(path, options.cwd ?? ".")) {
|
|
33
|
+
for (const filename of filenames) {
|
|
34
|
+
const loaded = yield* readIfExists(fs, path.join(directory, filename));
|
|
35
|
+
if (loaded !== undefined) {
|
|
36
|
+
files.push(loaded);
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return files;
|
|
42
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Crypto } from "effect";
|
|
2
|
+
import { HttpClient } from "effect/unstable/http";
|
|
3
|
+
import { SkillSource } from "@batonfx/core";
|
|
4
|
+
import { type Limits } from "./hosted-catalog.js";
|
|
5
|
+
/** @experimental Manifest-backed S3 catalog options. */
|
|
6
|
+
export interface Options extends Limits {
|
|
7
|
+
readonly bucket: string;
|
|
8
|
+
readonly region: string;
|
|
9
|
+
readonly prefix?: string;
|
|
10
|
+
readonly manifestName?: string;
|
|
11
|
+
readonly source?: string;
|
|
12
|
+
}
|
|
13
|
+
/** @experimental Build a manifest-backed S3 catalog source. */
|
|
14
|
+
export declare const make: (options: Options) => SkillSource.Source<HttpClient.HttpClient | Crypto.Crypto>;
|
|
15
|
+
/** @experimental Build a manifest-backed S3 catalog layer. */
|
|
16
|
+
export declare const layer: (options: Options) => import("effect/Layer").Layer<SkillSource.SkillSource, SkillSource.SkillSourceError, HttpClient.HttpClient | Crypto.Crypto>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Crypto, Effect } from "effect";
|
|
2
|
+
import { HttpClient } from "effect/unstable/http";
|
|
3
|
+
import { SkillSource } from "@batonfx/core";
|
|
4
|
+
import { make as makeHostedCatalog, resolveRelative, validateSkillPath } from "./hosted-catalog.js";
|
|
5
|
+
const segments = (value) => value
|
|
6
|
+
.split("/")
|
|
7
|
+
.filter((segment) => segment.length > 0)
|
|
8
|
+
.map(encodeURIComponent)
|
|
9
|
+
.join("/");
|
|
10
|
+
/** @experimental Build a manifest-backed S3 catalog source. */
|
|
11
|
+
export const make = (options) => {
|
|
12
|
+
const source = options.source ?? `s3://${options.bucket}/${options.prefix ?? ""}`;
|
|
13
|
+
if (!/^[a-z0-9](?:[a-z0-9-]{1,61})[a-z0-9]$/.test(options.bucket) ||
|
|
14
|
+
!/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])$/.test(options.region)) {
|
|
15
|
+
return Effect.fail(new SkillSource.SkillSourceError({ source, message: "Invalid S3 bucket or region for hosted skill catalog" }));
|
|
16
|
+
}
|
|
17
|
+
return Effect.gen(function* () {
|
|
18
|
+
if ((options.prefix?.length ?? 0) > 0)
|
|
19
|
+
yield* validateSkillPath(source, options.prefix ?? "");
|
|
20
|
+
yield* validateSkillPath(source, options.manifestName ?? "skills.json");
|
|
21
|
+
const prefix = segments(options.prefix ?? "");
|
|
22
|
+
const manifestName = segments(options.manifestName ?? "skills.json");
|
|
23
|
+
const manifestUrl = `https://${options.bucket}.s3.${options.region}.amazonaws.com/${prefix.length === 0 ? "" : `${prefix}/`}${manifestName}`;
|
|
24
|
+
return yield* makeHostedCatalog({
|
|
25
|
+
...options,
|
|
26
|
+
source,
|
|
27
|
+
manifestUrl,
|
|
28
|
+
resolveSkillUrl: (skillPath) => resolveRelative(source, manifestUrl, skillPath),
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
/** @experimental Build a manifest-backed S3 catalog layer. */
|
|
33
|
+
export const layer = (options) => SkillSource.layer([make(options)]);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { SkillSource } from "@batonfx/core";
|
|
3
|
+
export interface ParsedDocument {
|
|
4
|
+
readonly frontmatter: SkillSource.Frontmatter;
|
|
5
|
+
readonly body: string;
|
|
6
|
+
}
|
|
7
|
+
export declare const splitDocument: (source: string, content: string) => Effect.Effect<readonly [string, string], SkillSource.SkillSourceError>;
|
|
8
|
+
export declare const validateName: (source: string, name: string) => Effect.Effect<string, SkillSource.SkillSourceError>;
|
|
9
|
+
export declare const parseFrontmatter: (source: string, block: string, directoryName: string) => Effect.Effect<SkillSource.Frontmatter, SkillSource.SkillSourceError>;
|
|
10
|
+
export declare const parseDocument: (source: string, content: string, directoryName: string) => Effect.Effect<ParsedDocument, SkillSource.SkillSourceError>;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { SkillSource } from "@batonfx/core";
|
|
3
|
+
const sourceError = (source, message, cause) => new SkillSource.SkillSourceError({ source, message, ...(cause === undefined ? {} : { cause }) });
|
|
4
|
+
const normalizeKey = (key) => key.replace(/[-_]/g, "").toLowerCase();
|
|
5
|
+
const stripQuotes = (value) => {
|
|
6
|
+
const trimmed = value.trim();
|
|
7
|
+
return (trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith("'") && trimmed.endsWith("'"))
|
|
8
|
+
? trimmed.slice(1, -1)
|
|
9
|
+
: trimmed;
|
|
10
|
+
};
|
|
11
|
+
const parseInlineArray = (value) => {
|
|
12
|
+
const trimmed = value.trim();
|
|
13
|
+
if (!trimmed.startsWith("[") || !trimmed.endsWith("]"))
|
|
14
|
+
return [];
|
|
15
|
+
const inner = trimmed.slice(1, -1).trim();
|
|
16
|
+
return inner.length === 0 ? [] : inner.split(",").map((item) => stripQuotes(item).trim());
|
|
17
|
+
};
|
|
18
|
+
const parseBoolean = (value) => {
|
|
19
|
+
const lowered = value.trim().toLowerCase();
|
|
20
|
+
if (lowered === "true")
|
|
21
|
+
return true;
|
|
22
|
+
if (lowered === "false")
|
|
23
|
+
return false;
|
|
24
|
+
return undefined;
|
|
25
|
+
};
|
|
26
|
+
const setValue = (target, key, value) => {
|
|
27
|
+
switch (normalizeKey(key)) {
|
|
28
|
+
case "name":
|
|
29
|
+
if (typeof value === "string")
|
|
30
|
+
target.name = value;
|
|
31
|
+
break;
|
|
32
|
+
case "description":
|
|
33
|
+
if (typeof value === "string")
|
|
34
|
+
target.description = value;
|
|
35
|
+
break;
|
|
36
|
+
case "whentouse":
|
|
37
|
+
if (typeof value === "string")
|
|
38
|
+
target.whenToUse = value;
|
|
39
|
+
break;
|
|
40
|
+
case "allowedtools":
|
|
41
|
+
if (typeof value === "string")
|
|
42
|
+
target.allowedTools = value.split(/\s+/).filter((item) => item.length > 0);
|
|
43
|
+
else if (Array.isArray(value))
|
|
44
|
+
target.allowedTools = value;
|
|
45
|
+
break;
|
|
46
|
+
case "disablemodelinvocation":
|
|
47
|
+
if (typeof value === "boolean")
|
|
48
|
+
target.disableModelInvocation = value;
|
|
49
|
+
break;
|
|
50
|
+
case "userinvocable":
|
|
51
|
+
if (typeof value === "boolean")
|
|
52
|
+
target.userInvocable = value;
|
|
53
|
+
break;
|
|
54
|
+
case "contextfork":
|
|
55
|
+
if (typeof value === "boolean")
|
|
56
|
+
target.contextFork = value;
|
|
57
|
+
break;
|
|
58
|
+
case "agent":
|
|
59
|
+
if (typeof value === "string")
|
|
60
|
+
target.agent = value;
|
|
61
|
+
break;
|
|
62
|
+
case "model":
|
|
63
|
+
if (typeof value === "string")
|
|
64
|
+
target.model = value;
|
|
65
|
+
break;
|
|
66
|
+
case "paths":
|
|
67
|
+
if (Array.isArray(value))
|
|
68
|
+
target.paths = value;
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
const parseHeader = (source, block) => Effect.sync(() => {
|
|
73
|
+
const parsed = {};
|
|
74
|
+
const lines = block.split("\n");
|
|
75
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
76
|
+
const line = lines[index]?.trimEnd() ?? "";
|
|
77
|
+
if (line.trim().length === 0)
|
|
78
|
+
continue;
|
|
79
|
+
const separator = line.indexOf(":");
|
|
80
|
+
if (separator === -1)
|
|
81
|
+
continue;
|
|
82
|
+
const key = line.slice(0, separator).trim();
|
|
83
|
+
const raw = line.slice(separator + 1).trim();
|
|
84
|
+
if (raw.length === 0) {
|
|
85
|
+
const values = [];
|
|
86
|
+
while ((lines[index + 1]?.trimStart().startsWith("- ") ?? false) === true) {
|
|
87
|
+
index += 1;
|
|
88
|
+
values.push(stripQuotes((lines[index] ?? "").trimStart().slice(2)));
|
|
89
|
+
}
|
|
90
|
+
setValue(parsed, key, values);
|
|
91
|
+
}
|
|
92
|
+
else if (raw.startsWith("[") && raw.endsWith("]")) {
|
|
93
|
+
setValue(parsed, key, parseInlineArray(raw));
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
setValue(parsed, key, parseBoolean(raw) ?? stripQuotes(raw));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return parsed;
|
|
100
|
+
}).pipe(Effect.catchCause((cause) => Effect.fail(sourceError(source, "Invalid SKILL.md frontmatter", cause))));
|
|
101
|
+
export const splitDocument = (source, content) => Effect.gen(function* () {
|
|
102
|
+
const normalized = content.replace(/^\uFEFF/, "").replace(/\r\n/g, "\n");
|
|
103
|
+
const lines = normalized.split("\n");
|
|
104
|
+
if (lines[0] !== "---") {
|
|
105
|
+
return yield* Effect.fail(sourceError(source, "Invalid SKILL.md document: missing opening frontmatter fence"));
|
|
106
|
+
}
|
|
107
|
+
const close = lines.findIndex((line, index) => index > 0 && line === "---");
|
|
108
|
+
if (close === -1) {
|
|
109
|
+
return yield* Effect.fail(sourceError(source, "Invalid SKILL.md document: missing closing frontmatter fence"));
|
|
110
|
+
}
|
|
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("--")
|
|
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* () {
|
|
117
|
+
const parsed = yield* parseHeader(source, block);
|
|
118
|
+
if (parsed.name === undefined) {
|
|
119
|
+
return yield* Effect.fail(sourceError(source, "SKILL.md frontmatter requires name"));
|
|
120
|
+
}
|
|
121
|
+
yield* validateName(source, parsed.name);
|
|
122
|
+
if (parsed.name !== directoryName) {
|
|
123
|
+
return yield* Effect.fail(sourceError(source, `SKILL.md name must match directory ${directoryName}`));
|
|
124
|
+
}
|
|
125
|
+
if (parsed.description === undefined || parsed.description.length === 0 || parsed.description.length > 1_024) {
|
|
126
|
+
return yield* Effect.fail(sourceError(source, "SKILL.md description must contain 1-1024 characters"));
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
name: parsed.name,
|
|
130
|
+
description: parsed.description,
|
|
131
|
+
...(parsed.whenToUse === undefined ? {} : { whenToUse: parsed.whenToUse }),
|
|
132
|
+
...(parsed.allowedTools === undefined ? {} : { allowedTools: parsed.allowedTools }),
|
|
133
|
+
...(parsed.disableModelInvocation === undefined ? {} : { disableModelInvocation: parsed.disableModelInvocation }),
|
|
134
|
+
...(parsed.userInvocable === undefined ? {} : { userInvocable: parsed.userInvocable }),
|
|
135
|
+
...(parsed.contextFork === undefined ? {} : { contextFork: parsed.contextFork }),
|
|
136
|
+
...(parsed.agent === undefined ? {} : { agent: parsed.agent }),
|
|
137
|
+
...(parsed.model === undefined ? {} : { model: parsed.model }),
|
|
138
|
+
...(parsed.paths === undefined ? {} : { paths: parsed.paths }),
|
|
139
|
+
};
|
|
140
|
+
});
|
|
141
|
+
export const parseDocument = (source, content, directoryName) => Effect.gen(function* () {
|
|
142
|
+
const [header, body] = yield* splitDocument(source, content);
|
|
143
|
+
return { frontmatter: yield* parseFrontmatter(source, header, directoryName), body };
|
|
144
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { FileSystem, Layer, Path } from "effect";
|
|
2
|
+
import { SkillSource } from "@batonfx/core";
|
|
3
|
+
/** @experimental Filesystem skill-loader options. */
|
|
4
|
+
export interface LoadOptions {
|
|
5
|
+
readonly roots?: ReadonlyArray<string>;
|
|
6
|
+
readonly cwd?: string;
|
|
7
|
+
readonly descriptionCap?: number;
|
|
8
|
+
readonly frontmatterMaxBytes?: number;
|
|
9
|
+
}
|
|
10
|
+
/** @experimental Build a composable SkillSource from filesystem roots. */
|
|
11
|
+
export declare const make: (options?: LoadOptions) => SkillSource.Source<FileSystem.FileSystem | Path.Path>;
|
|
12
|
+
/** @experimental Build a SkillSource layer from filesystem roots. */
|
|
13
|
+
export declare const layer: (options?: LoadOptions) => Layer.Layer<SkillSource.SkillSource, SkillSource.SkillSourceError, FileSystem.FileSystem | Path.Path>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Effect, FileSystem, Layer, Path, PlatformError, Stream } from "effect";
|
|
2
|
+
import { SkillSource } from "@batonfx/core";
|
|
3
|
+
import { parseDocument, parseFrontmatter, splitDocument } from "./skill-document.js";
|
|
4
|
+
const DEFAULT_ROOTS = [".agents/skills", ".claude/skills", ".pi/skills"];
|
|
5
|
+
const decoder = new TextDecoder();
|
|
6
|
+
const sourceError = (source, message, cause) => new SkillSource.SkillSourceError({ source, message, ...(cause === undefined ? {} : { cause }) });
|
|
7
|
+
const mapPlatformError = (source, error) => sourceError(source, error.message, error);
|
|
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
|
+
const loadSkill = (fs, path, file, relativeFile, descriptionCap, frontmatterMaxBytes) => Effect.gen(function* () {
|
|
10
|
+
const header = yield* readHeader(fs, file, frontmatterMaxBytes);
|
|
11
|
+
const [headerBlock] = yield* splitDocument(file, header);
|
|
12
|
+
const directoryName = path.basename(path.dirname(relativeFile));
|
|
13
|
+
const frontmatter = yield* parseFrontmatter(file, headerBlock, directoryName);
|
|
14
|
+
return {
|
|
15
|
+
frontmatter,
|
|
16
|
+
listing: SkillSource.makeListing(frontmatter, descriptionCap),
|
|
17
|
+
body: fs.readFileString(file).pipe(Effect.mapError((error) => mapPlatformError(file, error)), Effect.flatMap((content) => parseDocument(file, content, directoryName).pipe(Effect.map((document) => document.body)))),
|
|
18
|
+
tools: [],
|
|
19
|
+
};
|
|
20
|
+
});
|
|
21
|
+
const discoverRoot = (fs, path, cwd, root, descriptionCap, frontmatterMaxBytes) => Effect.gen(function* () {
|
|
22
|
+
const rootPath = path.isAbsolute(root) ? path.normalize(root) : path.join(cwd, root);
|
|
23
|
+
const exists = yield* fs.exists(rootPath).pipe(Effect.mapError((error) => mapPlatformError(rootPath, error)));
|
|
24
|
+
if (!exists)
|
|
25
|
+
return [];
|
|
26
|
+
const entries = yield* fs
|
|
27
|
+
.readDirectory(rootPath, { recursive: true })
|
|
28
|
+
.pipe(Effect.mapError((error) => mapPlatformError(rootPath, error)));
|
|
29
|
+
const skills = [];
|
|
30
|
+
for (const skillFile of entries.filter((entry) => path.basename(entry) === "SKILL.md").toSorted()) {
|
|
31
|
+
skills.push(yield* loadSkill(fs, path, path.join(rootPath, skillFile), skillFile, descriptionCap, frontmatterMaxBytes));
|
|
32
|
+
}
|
|
33
|
+
return skills;
|
|
34
|
+
});
|
|
35
|
+
/** @experimental Build a composable SkillSource from filesystem roots. */
|
|
36
|
+
export const make = (options = {}) => Effect.gen(function* () {
|
|
37
|
+
const fs = yield* FileSystem.FileSystem;
|
|
38
|
+
const path = yield* Path.Path;
|
|
39
|
+
const cwd = options.cwd === undefined ? "." : path.resolve(options.cwd);
|
|
40
|
+
const roots = options.roots ?? DEFAULT_ROOTS;
|
|
41
|
+
const descriptionCap = options.descriptionCap ?? SkillSource.DESCRIPTION_CAP;
|
|
42
|
+
const frontmatterMaxBytes = options.frontmatterMaxBytes ?? 64 * 1024;
|
|
43
|
+
const byName = new Map();
|
|
44
|
+
for (const root of roots) {
|
|
45
|
+
for (const skill of yield* discoverRoot(fs, path, cwd, root, descriptionCap, frontmatterMaxBytes)) {
|
|
46
|
+
byName.set(skill.frontmatter.name, skill);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const skills = [...byName.values()];
|
|
50
|
+
return {
|
|
51
|
+
all: Effect.succeed(skills),
|
|
52
|
+
get: (name) => Effect.succeed(byName.get(name)),
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
/** @experimental Build a SkillSource layer from filesystem roots. */
|
|
56
|
+
export const layer = (options = {}) => Layer.effect(SkillSource.SkillSource, make(options).pipe(Effect.map(SkillSource.SkillSource.of)));
|
package/package.json
CHANGED
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "@batonfx/skills",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.2",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
7
7
|
"exports": {
|
|
8
|
-
".":
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js"
|
|
11
|
+
}
|
|
9
12
|
},
|
|
10
13
|
"scripts": {
|
|
11
|
-
"build": "
|
|
14
|
+
"build": "rm -rf dist && bun tsc -p tsconfig.build.json",
|
|
12
15
|
"lint": "bun --cwd ../.. oxlint packages/skills/src packages/skills/test",
|
|
13
16
|
"test": "bun --cwd ../.. vitest run packages/skills/test --passWithNoTests",
|
|
14
17
|
"typecheck": "bun tsc --noEmit"
|
|
15
18
|
},
|
|
16
19
|
"dependencies": {
|
|
17
|
-
"@batonfx/core": "0.4.
|
|
20
|
+
"@batonfx/core": "0.4.2",
|
|
18
21
|
"effect": "4.0.0-beta.93"
|
|
19
22
|
},
|
|
20
23
|
"devDependencies": {
|
|
@@ -22,5 +25,27 @@
|
|
|
22
25
|
"@types/bun": "1.3.13",
|
|
23
26
|
"typescript": "5.8.2",
|
|
24
27
|
"vitest": "4.0.16"
|
|
28
|
+
},
|
|
29
|
+
"description": "Filesystem and hosted skill sources for Baton agents",
|
|
30
|
+
"sideEffects": false,
|
|
31
|
+
"files": [
|
|
32
|
+
"dist",
|
|
33
|
+
"README.md"
|
|
34
|
+
],
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=22",
|
|
37
|
+
"bun": ">=1.3.0"
|
|
38
|
+
},
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "git+https://github.com/In-Time-Tec/batonfx.git",
|
|
42
|
+
"directory": "packages/skills"
|
|
43
|
+
},
|
|
44
|
+
"homepage": "https://github.com/In-Time-Tec/batonfx#readme",
|
|
45
|
+
"bugs": {
|
|
46
|
+
"url": "https://github.com/In-Time-Tec/batonfx/issues"
|
|
47
|
+
},
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public"
|
|
25
50
|
}
|
|
26
51
|
}
|
package/.turbo/turbo-build.log
DELETED
package/src/github-catalog.ts
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import { Crypto, Effect } from "effect"
|
|
2
|
-
import { HttpClient, Url } from "effect/unstable/http"
|
|
3
|
-
import { SkillSource } from "@batonfx/core"
|
|
4
|
-
import { type Limits, make as makeHostedCatalog, validateSkillPath } from "./hosted-catalog"
|
|
5
|
-
|
|
6
|
-
/** @experimental Manifest-backed GitHub catalog options. */
|
|
7
|
-
export interface Options extends Limits {
|
|
8
|
-
readonly owner: string
|
|
9
|
-
readonly repo: string
|
|
10
|
-
readonly ref: string
|
|
11
|
-
readonly root?: string
|
|
12
|
-
readonly manifestName?: string
|
|
13
|
-
readonly apiBaseUrl?: string
|
|
14
|
-
readonly source?: string
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const encodedPath = (value: string): string =>
|
|
18
|
-
value
|
|
19
|
-
.split("/")
|
|
20
|
-
.filter((segment) => segment.length > 0)
|
|
21
|
-
.map(encodeURIComponent)
|
|
22
|
-
.join("/")
|
|
23
|
-
|
|
24
|
-
/** @experimental Build a manifest-backed immutable GitHub catalog source. */
|
|
25
|
-
export const make = (options: Options): SkillSource.Source<HttpClient.HttpClient | Crypto.Crypto> => {
|
|
26
|
-
const source = options.source ?? `github:${options.owner}/${options.repo}@${options.ref}`
|
|
27
|
-
if (!/^[0-9a-fA-F]{40}$|^[0-9a-fA-F]{64}$/.test(options.ref)) {
|
|
28
|
-
return Effect.fail(
|
|
29
|
-
new SkillSource.SkillSourceError({ source, message: "GitHub skill catalog ref must be a commit id" }),
|
|
30
|
-
)
|
|
31
|
-
}
|
|
32
|
-
if (
|
|
33
|
-
!/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/.test(options.owner) ||
|
|
34
|
-
!/^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,98}[A-Za-z0-9])?$/.test(options.repo)
|
|
35
|
-
) {
|
|
36
|
-
return Effect.fail(new SkillSource.SkillSourceError({ source, message: "Invalid GitHub owner or repository" }))
|
|
37
|
-
}
|
|
38
|
-
return Effect.gen(function* () {
|
|
39
|
-
if ((options.root?.length ?? 0) > 0) yield* validateSkillPath(source, options.root ?? "")
|
|
40
|
-
yield* validateSkillPath(source, options.manifestName ?? "skills.json")
|
|
41
|
-
const apiBase = yield* Effect.fromResult(Url.fromString(options.apiBaseUrl ?? "https://api.github.com")).pipe(
|
|
42
|
-
Effect.mapError(
|
|
43
|
-
(cause) => new SkillSource.SkillSourceError({ source, message: "Invalid GitHub API base URL", cause }),
|
|
44
|
-
),
|
|
45
|
-
)
|
|
46
|
-
if (
|
|
47
|
-
apiBase.protocol !== "https:" ||
|
|
48
|
-
apiBase.username.length > 0 ||
|
|
49
|
-
apiBase.password.length > 0 ||
|
|
50
|
-
apiBase.search.length > 0 ||
|
|
51
|
-
apiBase.hash.length > 0
|
|
52
|
-
) {
|
|
53
|
-
return yield* Effect.fail(new SkillSource.SkillSourceError({ source, message: "Invalid GitHub API base URL" }))
|
|
54
|
-
}
|
|
55
|
-
const base = apiBase.toString().replace(/\/$/, "")
|
|
56
|
-
const root = encodedPath(options.root ?? "")
|
|
57
|
-
const manifestName = encodedPath(options.manifestName ?? "skills.json")
|
|
58
|
-
const repository = `${base}/repos/${encodeURIComponent(options.owner)}/${encodeURIComponent(options.repo)}/contents`
|
|
59
|
-
const rootUrl = `${repository}/${root.length === 0 ? "" : `${root}/`}`
|
|
60
|
-
const manifestUrl = `${rootUrl}${manifestName}?ref=${encodeURIComponent(options.ref)}`
|
|
61
|
-
const headers = {
|
|
62
|
-
accept: "application/vnd.github.raw+json",
|
|
63
|
-
"x-github-api-version": "2022-11-28",
|
|
64
|
-
}
|
|
65
|
-
return yield* makeHostedCatalog({
|
|
66
|
-
...options,
|
|
67
|
-
source,
|
|
68
|
-
manifestUrl,
|
|
69
|
-
manifestHeaders: headers,
|
|
70
|
-
bodyHeaders: headers,
|
|
71
|
-
resolveSkillUrl: (skillPath) =>
|
|
72
|
-
validateSkillPath(source, skillPath).pipe(
|
|
73
|
-
Effect.map((safePath) => `${rootUrl}${encodedPath(safePath)}?ref=${encodeURIComponent(options.ref)}`),
|
|
74
|
-
),
|
|
75
|
-
})
|
|
76
|
-
})
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** @experimental Build a manifest-backed immutable GitHub catalog layer. */
|
|
80
|
-
export const layer = (options: Options) => SkillSource.layer([make(options)])
|