@batonfx/skills 0.4.4 → 0.6.1

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 CHANGED
@@ -1,5 +1,55 @@
1
1
  # `@batonfx/skills`
2
2
 
3
- Filesystem and hosted skill sources for Baton agents.
3
+ Focused composition guide for filesystem and hosted skill sources.
4
4
 
5
- See the [Baton documentation](https://github.com/In-Time-Tec/batonfx#readme) for installation, examples, and API guidance.
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/)
@@ -10,7 +10,6 @@ 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>;
@@ -9,19 +9,23 @@ 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 source = options.source ?? `github:${options.owner}/${options.repo}@${options.ref}`;
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(SkillSource.SkillSourceError.make({ source, message: "GitHub skill catalog ref must be a commit id" }));
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(SkillSource.SkillSourceError.make({ source, message: "Invalid GitHub owner or repository" }));
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((cause) => SkillSource.SkillSourceError.make({ source, message: "Invalid GitHub API base URL", cause })));
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 ||
@@ -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
- ...options,
47
+ limits: options,
44
48
  source,
45
49
  manifestUrl,
46
50
  manifestHeaders: headers,
@@ -1,43 +1,6 @@
1
- import { Crypto, Effect, Layer, Schema } from "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
- /** @experimental Internal hosted-catalog construction options. */
50
- export interface MakeOptions extends Limits {
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>;
@@ -66,5 +29,4 @@ export declare const resolveRelative: {
66
29
  };
67
30
  /** @experimental Build a hosted manifest source over Effect HTTP and Crypto services. */
68
31
  export declare const make: (options: MakeOptions) => SkillSource.Source<HttpClient.HttpClient | Crypto.Crypto>;
69
- /** @experimental One-source hosted catalog layer. */
70
- export declare const layer: (options: MakeOptions) => Layer.Layer<SkillSource.SkillSource, SkillSource.SkillSourceError, HttpClient.HttpClient | Crypto.Crypto>;
32
+ export {};
@@ -1,9 +1,8 @@
1
- import { Crypto, Effect, Encoding, Function, Layer, Schema, Stream } from "effect";
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
- /** @experimental Baton hosted skill manifest entry. */
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,8 +16,7 @@ export const ManifestSkill = Schema.Struct({
17
16
  model: Schema.optionalKey(Schema.String),
18
17
  paths: Schema.optionalKey(Schema.Array(Schema.String)),
19
18
  });
20
- /** @experimental Baton hosted skill manifest. */
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
  });
@@ -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((error) => sourceError(source, "Hosted skill request failed", error)), Effect.flatMap((response) => response.stream.pipe(Stream.mapError((error) => sourceError(source, "Hosted skill request failed", error)), Stream.runFoldEffect(() => ({ size: 0, chunks: [] }), (state, chunk) => {
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`));
@@ -77,16 +75,16 @@ export const validateSkillPath = Function.dual(2, (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, `Unsafe hosted skill path: ${skillPath}`));
78
+ : Effect.fail(sourceError(source, "Unsafe hosted skill path"));
81
79
  });
82
80
  /** @experimental Resolve a same-origin path beneath a manifest directory. */
83
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((error) => sourceError(source, "Invalid manifest URL", error)));
86
- const directory = yield* Effect.fromResult(Url.fromString(".", manifest)).pipe(Effect.mapError((error) => sourceError(source, "Invalid manifest directory URL", error)));
87
- const resolved = yield* Effect.fromResult(Url.fromString(skillPath, directory)).pipe(Effect.mapError((error) => sourceError(source, "Invalid hosted skill URL", error)));
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* sourceError(source, `Hosted skill path escapes manifest directory: ${skillPath}`);
87
+ return yield* sourceError(source, "Hosted skill path escapes manifest directory");
90
88
  }
91
89
  return resolved.toString();
92
90
  }));
@@ -94,10 +92,10 @@ export const resolveRelative = Function.dual(3, (source, manifestUrl, skillPath)
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)));
@@ -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)));
@@ -5,7 +5,6 @@ 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>;
@@ -2,14 +2,15 @@ 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) => SkillSource.SkillSourceError.make({ source: "http-skill-catalog", message: "Invalid manifest URL", cause });
5
+ const invalidUrl = () => SkillSource.SkillSourceError.make({ source: "http-skill-catalog", message: "Invalid manifest URL" });
6
6
  /** @experimental Build a generic HTTP catalog source. */
7
7
  export const make = (options) => Effect.gen(function* () {
8
8
  const parsed = yield* Effect.fromResult(Url.fromString(options.manifestUrl)).pipe(Effect.mapError(invalidUrl));
9
- const source = options.source ?? `${parsed.origin}${parsed.pathname}`;
9
+ const source = `${parsed.origin}${parsed.pathname}`;
10
10
  return yield* makeHostedCatalog({
11
- ...options,
11
+ limits: options,
12
12
  source,
13
+ manifestUrl: options.manifestUrl,
13
14
  resolveSkillUrl: (skillPath) => resolveRelative(source, options.manifestUrl, skillPath),
14
15
  });
15
16
  });
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";
@@ -8,7 +8,6 @@ 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>;
@@ -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 source = options.source ?? `s3://${options.bucket}/${options.prefix ?? ""}`;
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(SkillSource.SkillSourceError.make({ source, message: "Invalid S3 bucket or region for hosted skill catalog" }));
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(source, options.prefix ?? "");
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
- ...options,
29
+ limits: options,
26
30
  source,
27
31
  manifestUrl,
28
32
  resolveSkillUrl: (skillPath) => resolveRelative(source, manifestUrl, skillPath),
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.4",
4
+ "version": "0.6.1",
5
5
  "private": false,
6
6
  "type": "module",
7
7
  "exports": {
@@ -17,11 +17,11 @@
17
17
  "typecheck": "bun tsc --noEmit"
18
18
  },
19
19
  "dependencies": {
20
- "@batonfx/core": "0.4.4",
21
- "effect": "4.0.0-beta.93"
20
+ "@batonfx/core": "0.6.1",
21
+ "effect": "4.0.0-beta.98"
22
22
  },
23
23
  "devDependencies": {
24
- "@effect/vitest": "4.0.0-beta.93",
24
+ "@effect/vitest": "4.0.0-beta.98",
25
25
  "@types/bun": "1.3.13",
26
26
  "typescript": "7.0.2",
27
27
  "vitest": "4.0.16"