@batonfx/skills 0.4.2 → 0.4.4

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.
@@ -15,4 +15,4 @@ export interface Options extends Limits {
15
15
  /** @experimental Build a manifest-backed immutable GitHub catalog source. */
16
16
  export declare const make: (options: Options) => SkillSource.Source<HttpClient.HttpClient | Crypto.Crypto>;
17
17
  /** @experimental Build a manifest-backed immutable GitHub catalog layer. */
18
- export declare const layer: (options: Options) => import("effect/Layer").Layer<SkillSource.SkillSource, SkillSource.SkillSourceError, HttpClient.HttpClient | Crypto.Crypto>;
18
+ export declare const layer: (options: Options) => import("effect/Layer").Layer<SkillSource.SkillSource, SkillSource.SkillSourceError, Crypto.Crypto | HttpClient.HttpClient>;
@@ -11,23 +11,23 @@ const encodedPath = (value) => value
11
11
  export const make = (options) => {
12
12
  const source = options.source ?? `github:${options.owner}/${options.repo}@${options.ref}`;
13
13
  if (!/^[0-9a-fA-F]{40}$|^[0-9a-fA-F]{64}$/.test(options.ref)) {
14
- return Effect.fail(new SkillSource.SkillSourceError({ source, message: "GitHub skill catalog ref must be a commit id" }));
14
+ return Effect.fail(SkillSource.SkillSourceError.make({ source, message: "GitHub skill catalog ref must be a commit id" }));
15
15
  }
16
16
  if (!/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/.test(options.owner) ||
17
17
  !/^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,98}[A-Za-z0-9])?$/.test(options.repo)) {
18
- return Effect.fail(new SkillSource.SkillSourceError({ source, message: "Invalid GitHub owner or repository" }));
18
+ return Effect.fail(SkillSource.SkillSourceError.make({ source, message: "Invalid GitHub owner or repository" }));
19
19
  }
20
20
  return Effect.gen(function* () {
21
21
  if ((options.root?.length ?? 0) > 0)
22
22
  yield* validateSkillPath(source, options.root ?? "");
23
23
  yield* validateSkillPath(source, options.manifestName ?? "skills.json");
24
- const apiBase = yield* Effect.fromResult(Url.fromString(options.apiBaseUrl ?? "https://api.github.com")).pipe(Effect.mapError((cause) => new SkillSource.SkillSourceError({ source, message: "Invalid GitHub API base URL", cause })));
24
+ const apiBase = yield* Effect.fromResult(Url.fromString(options.apiBaseUrl ?? "https://api.github.com")).pipe(Effect.mapError((cause) => SkillSource.SkillSourceError.make({ source, message: "Invalid GitHub API base URL", cause })));
25
25
  if (apiBase.protocol !== "https:" ||
26
26
  apiBase.username.length > 0 ||
27
27
  apiBase.password.length > 0 ||
28
28
  apiBase.search.length > 0 ||
29
29
  apiBase.hash.length > 0) {
30
- return yield* Effect.fail(new SkillSource.SkillSourceError({ source, message: "Invalid GitHub API base URL" }));
30
+ return yield* SkillSource.SkillSourceError.make({ source, message: "Invalid GitHub API base URL" });
31
31
  }
32
32
  const base = apiBase.toString().replace(/\/$/, "");
33
33
  const root = encodedPath(options.root ?? "");
@@ -55,9 +55,15 @@ export interface MakeOptions extends Limits {
55
55
  readonly bodyHeaders?: Readonly<Record<string, string>>;
56
56
  }
57
57
  /** @experimental Validate one safe relative SKILL.md path. */
58
- export declare const validateSkillPath: (source: string, skillPath: string) => Effect.Effect<string, SkillSource.SkillSourceError>;
58
+ export declare const validateSkillPath: {
59
+ (skillPath: string): (source: string) => Effect.Effect<string, SkillSource.SkillSourceError>;
60
+ (source: string, skillPath: string): Effect.Effect<string, SkillSource.SkillSourceError>;
61
+ };
59
62
  /** @experimental Resolve a same-origin path beneath a manifest directory. */
60
- export declare const resolveRelative: (source: string, manifestUrl: string, skillPath: string) => Effect.Effect<string, SkillSource.SkillSourceError>;
63
+ export declare const resolveRelative: {
64
+ (manifestUrl: string, skillPath: string): (source: string) => Effect.Effect<string, SkillSource.SkillSourceError>;
65
+ (source: string, manifestUrl: string, skillPath: string): Effect.Effect<string, SkillSource.SkillSourceError>;
66
+ };
61
67
  /** @experimental Build a hosted manifest source over Effect HTTP and Crypto services. */
62
68
  export declare const make: (options: MakeOptions) => SkillSource.Source<HttpClient.HttpClient | Crypto.Crypto>;
63
69
  /** @experimental One-source hosted catalog layer. */
@@ -1,4 +1,4 @@
1
- import { Crypto, Effect, Encoding, Layer, Schema, Stream } from "effect";
1
+ import { Crypto, Effect, Encoding, Function, Layer, 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";
@@ -23,7 +23,7 @@ export const Manifest = Schema.Struct({
23
23
  skills: Schema.Array(ManifestSkill),
24
24
  });
25
25
  const decoder = new TextDecoder("utf-8", { fatal: true });
26
- const sourceError = (source, message, cause) => new SkillSource.SkillSourceError({ source, message, ...(cause === undefined ? {} : { cause }) });
26
+ const sourceError = (source, message, cause) => SkillSource.SkillSourceError.make({ source, message, ...(cause === undefined ? {} : { cause }) });
27
27
  const safeInteger = (source, name, value, minimum) => Number.isSafeInteger(value) && value >= minimum
28
28
  ? Effect.succeed(value)
29
29
  : Effect.fail(sourceError(source, `${name} must be a safe integer >= ${minimum}`));
@@ -67,7 +67,7 @@ const frontmatter = (entry) => ({
67
67
  });
68
68
  const sameFrontmatter = (left, right) => JSON.stringify(left) === JSON.stringify(right);
69
69
  /** @experimental Validate one safe relative SKILL.md path. */
70
- export const validateSkillPath = (source, skillPath) => {
70
+ export const validateSkillPath = Function.dual(2, (source, skillPath) => {
71
71
  const segments = skillPath.split("/");
72
72
  return skillPath.length > 0 &&
73
73
  !skillPath.startsWith("/") &&
@@ -78,18 +78,18 @@ export const validateSkillPath = (source, skillPath) => {
78
78
  segments.every((segment) => segment.length > 0 && segment !== "." && segment !== "..")
79
79
  ? Effect.succeed(skillPath)
80
80
  : Effect.fail(sourceError(source, `Unsafe hosted skill path: ${skillPath}`));
81
- };
81
+ });
82
82
  /** @experimental Resolve a same-origin path beneath a manifest directory. */
83
- export const resolveRelative = (source, manifestUrl, skillPath) => Effect.gen(function* () {
83
+ export const resolveRelative = Function.dual(3, (source, manifestUrl, skillPath) => Effect.gen(function* () {
84
84
  yield* validateSkillPath(source, skillPath);
85
85
  const manifest = yield* Effect.fromResult(Url.fromString(manifestUrl)).pipe(Effect.mapError((error) => sourceError(source, "Invalid manifest URL", error)));
86
86
  const directory = yield* Effect.fromResult(Url.fromString(".", manifest)).pipe(Effect.mapError((error) => sourceError(source, "Invalid manifest directory URL", error)));
87
87
  const resolved = yield* Effect.fromResult(Url.fromString(skillPath, directory)).pipe(Effect.mapError((error) => sourceError(source, "Invalid hosted skill URL", error)));
88
88
  if (resolved.origin !== manifest.origin || !resolved.pathname.startsWith(directory.pathname)) {
89
- return yield* Effect.fail(sourceError(source, `Hosted skill path escapes manifest directory: ${skillPath}`));
89
+ return yield* sourceError(source, `Hosted skill path escapes manifest directory: ${skillPath}`);
90
90
  }
91
91
  return resolved.toString();
92
- });
92
+ }));
93
93
  /** @experimental Build a hosted manifest source over Effect HTTP and Crypto services. */
94
94
  export const make = (options) => Effect.gen(function* () {
95
95
  const client = yield* HttpClient.HttpClient;
@@ -102,23 +102,23 @@ export const make = (options) => Effect.gen(function* () {
102
102
  const manifestText = yield* decodeText(options.source, manifestBytes);
103
103
  const manifest = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(Manifest))(manifestText).pipe(Effect.mapError((error) => sourceError(options.source, "Invalid hosted skill manifest", error)));
104
104
  if (manifest.skills.length > maxSkills) {
105
- return yield* Effect.fail(sourceError(options.source, `Hosted skill manifest exceeds ${maxSkills} skills`));
105
+ return yield* sourceError(options.source, `Hosted skill manifest exceeds ${maxSkills} skills`);
106
106
  }
107
107
  const byName = new Map();
108
108
  for (const entry of manifest.skills) {
109
109
  yield* validateName(options.source, entry.name);
110
110
  if (entry.description.length === 0 || entry.description.length > 1_024) {
111
- return yield* Effect.fail(sourceError(options.source, "Hosted skill description must contain 1-1024 characters"));
111
+ return yield* sourceError(options.source, "Hosted skill description must contain 1-1024 characters");
112
112
  }
113
113
  if (!/^[0-9a-f]{64}$/.test(entry.sha256)) {
114
- return yield* Effect.fail(sourceError(options.source, `Invalid SHA-256 for hosted skill ${entry.name}`));
114
+ return yield* sourceError(options.source, `Invalid SHA-256 for hosted skill ${entry.name}`);
115
115
  }
116
116
  if (byName.has(entry.name)) {
117
- return yield* Effect.fail(sourceError(options.source, `Duplicate hosted skill name: ${entry.name}`));
117
+ return yield* sourceError(options.source, `Duplicate hosted skill name: ${entry.name}`);
118
118
  }
119
119
  const pathSegments = entry.skillPath.split("/");
120
120
  if (pathSegments.at(-1) !== "SKILL.md" || pathSegments.at(-2) !== entry.name) {
121
- return yield* Effect.fail(sourceError(options.source, `Hosted skill path directory must match skill name: ${entry.name}`));
121
+ return yield* sourceError(options.source, `Hosted skill path directory must match skill name: ${entry.name}`);
122
122
  }
123
123
  const skillUrl = yield* options.resolveSkillUrl(entry.skillPath);
124
124
  const metadata = frontmatter(entry);
@@ -10,4 +10,4 @@ export interface Options extends Limits {
10
10
  /** @experimental Build a generic HTTP catalog source. */
11
11
  export declare const make: (options: Options) => SkillSource.Source<HttpClient.HttpClient | Crypto.Crypto>;
12
12
  /** @experimental Build a generic HTTP catalog layer. */
13
- export declare const layer: (options: Options) => import("effect/Layer").Layer<SkillSource.SkillSource, SkillSource.SkillSourceError, HttpClient.HttpClient | Crypto.Crypto>;
13
+ export declare const layer: (options: Options) => import("effect/Layer").Layer<SkillSource.SkillSource, SkillSource.SkillSourceError, Crypto.Crypto | HttpClient.HttpClient>;
@@ -2,18 +2,16 @@ 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 = (cause) => new SkillSource.SkillSourceError({ source: "http-skill-catalog", message: "Invalid manifest URL", cause });
5
+ const invalidUrl = (cause) => SkillSource.SkillSourceError.make({ source: "http-skill-catalog", message: "Invalid manifest URL", cause });
6
6
  /** @experimental Build a generic HTTP catalog source. */
7
- export const make = (options) => {
8
- return Effect.gen(function* () {
9
- const parsed = yield* Effect.fromResult(Url.fromString(options.manifestUrl)).pipe(Effect.mapError(invalidUrl));
10
- const source = options.source ?? `${parsed.origin}${parsed.pathname}`;
11
- return yield* makeHostedCatalog({
12
- ...options,
13
- source,
14
- resolveSkillUrl: (skillPath) => resolveRelative(source, options.manifestUrl, skillPath),
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 = options.source ?? `${parsed.origin}${parsed.pathname}`;
10
+ return yield* makeHostedCatalog({
11
+ ...options,
12
+ source,
13
+ resolveSkillUrl: (skillPath) => resolveRelative(source, options.manifestUrl, skillPath),
16
14
  });
17
- };
15
+ });
18
16
  /** @experimental Build a generic HTTP catalog layer. */
19
17
  export const layer = (options) => SkillSource.layer([make(options)]);
@@ -13,4 +13,4 @@ export interface Options extends Limits {
13
13
  /** @experimental Build a manifest-backed S3 catalog source. */
14
14
  export declare const make: (options: Options) => SkillSource.Source<HttpClient.HttpClient | Crypto.Crypto>;
15
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>;
16
+ export declare const layer: (options: Options) => import("effect/Layer").Layer<SkillSource.SkillSource, SkillSource.SkillSourceError, Crypto.Crypto | HttpClient.HttpClient>;
@@ -12,7 +12,7 @@ export const make = (options) => {
12
12
  const source = options.source ?? `s3://${options.bucket}/${options.prefix ?? ""}`;
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(new SkillSource.SkillSourceError({ source, message: "Invalid S3 bucket or region for hosted skill catalog" }));
15
+ return Effect.fail(SkillSource.SkillSourceError.make({ source, message: "Invalid S3 bucket or region for hosted skill catalog" }));
16
16
  }
17
17
  return Effect.gen(function* () {
18
18
  if ((options.prefix?.length ?? 0) > 0)
@@ -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: (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>;
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
+ };
@@ -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) => new SkillSource.SkillSourceError({ source, message, ...(cause === undefined ? {} : { 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* Effect.fail(sourceError(source, "Invalid SKILL.md document: missing opening frontmatter fence"));
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* Effect.fail(sourceError(source, "Invalid SKILL.md document: missing closing frontmatter fence"));
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* Effect.fail(sourceError(source, "SKILL.md frontmatter requires name"));
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* Effect.fail(sourceError(source, `SKILL.md name must match directory ${directoryName}`));
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* Effect.fail(sourceError(source, "SKILL.md description must contain 1-1024 characters"));
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
+ }));
@@ -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) => new SkillSource.SkillSourceError({ source, message, ...(cause === undefined ? {} : { 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.2",
4
+ "version": "0.4.4",
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.4.2",
20
+ "@batonfx/core": "0.4.4",
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": "5.8.2",
26
+ "typescript": "7.0.2",
27
27
  "vitest": "4.0.16"
28
28
  },
29
29
  "description": "Filesystem and hosted skill sources for Baton agents",