@xandreed/plugin-workspace 0.0.0-stage → 0.15.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/LICENSE +21 -0
- package/README.md +4 -2
- package/dist/bash-tool.adapter.d.ts +15 -0
- package/dist/bash-tool.adapter.js +73 -0
- package/dist/bash.adapter.d.ts +81 -0
- package/dist/bash.adapter.js +112 -0
- package/dist/file-system.adapter.d.ts +24 -0
- package/dist/file-system.adapter.js +52 -0
- package/dist/image-resizer.adapter.d.ts +5 -0
- package/dist/image-resizer.adapter.js +68 -0
- package/dist/image-resizer.entity.d.ts +23 -0
- package/dist/image-resizer.entity.functions.d.ts +15 -0
- package/dist/image-resizer.entity.functions.js +4 -0
- package/dist/image-resizer.entity.js +7 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +38 -0
- package/dist/local-system.entity.d.ts +47 -0
- package/dist/local-system.entity.functions.d.ts +53 -0
- package/dist/local-system.entity.functions.js +46 -0
- package/dist/local-system.entity.js +12 -0
- package/dist/plugin.adapter.d.ts +183 -0
- package/dist/plugin.adapter.js +38 -0
- package/dist/read-tools.adapter.d.ts +39 -0
- package/dist/read-tools.adapter.js +109 -0
- package/dist/ripgrep.adapter.d.ts +74 -0
- package/dist/ripgrep.adapter.js +56 -0
- package/dist/ripgrep.entity.d.ts +63 -0
- package/dist/ripgrep.entity.functions.d.ts +87 -0
- package/dist/ripgrep.entity.functions.js +62 -0
- package/dist/ripgrep.entity.js +10 -0
- package/dist/shell.entity.functions.d.ts +2 -0
- package/dist/shell.entity.functions.js +1 -0
- package/dist/skills/skills-directory.adapter.d.ts +68 -0
- package/dist/skills/skills-directory.adapter.js +30 -0
- package/dist/skills/skills-directory.entity.d.ts +99 -0
- package/dist/skills/skills-directory.entity.functions.d.ts +88 -0
- package/dist/skills/skills-directory.entity.functions.js +45 -0
- package/dist/skills/skills-directory.entity.js +35 -0
- package/dist/text-edit.entity.d.ts +32 -0
- package/dist/text-edit.entity.functions.d.ts +32 -0
- package/dist/text-edit.entity.functions.js +68 -0
- package/dist/text-edit.entity.js +15 -0
- package/dist/text-search.entity.d.ts +23 -0
- package/dist/text-search.entity.functions.d.ts +58 -0
- package/dist/text-search.entity.functions.js +30 -0
- package/dist/text-search.entity.js +10 -0
- package/dist/text-window.entity.d.ts +46 -0
- package/dist/text-window.entity.functions.d.ts +54 -0
- package/dist/text-window.entity.functions.js +88 -0
- package/dist/text-window.entity.js +21 -0
- package/dist/workspace-path.entity.d.ts +43 -0
- package/dist/workspace-path.entity.functions.d.ts +90 -0
- package/dist/workspace-path.entity.functions.js +88 -0
- package/dist/workspace-path.entity.js +9 -0
- package/dist/workspace-tools.adapter.d.ts +66 -0
- package/dist/workspace-tools.adapter.js +60 -0
- package/dist/workspace-tools.entity.d.ts +552 -0
- package/dist/workspace-tools.entity.functions.d.ts +126 -0
- package/dist/workspace-tools.entity.functions.js +180 -0
- package/dist/workspace-tools.entity.js +154 -0
- package/dist/write-tools.adapter.d.ts +12 -0
- package/dist/write-tools.adapter.js +79 -0
- package/package.json +30 -4
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Effect, Option, Stream } from "effect";
|
|
2
|
+
import { Bash, RunContext } from "@xandreed/core";
|
|
3
|
+
import { rgReasonOf } from "./local-system.entity.functions.js";
|
|
4
|
+
import { decodeRgMessage, emptyRgSearch, foldRgMessage, rgLines, rgRecords, ripgrepCommand } from "./ripgrep.entity.functions.js";
|
|
5
|
+
import { clipHead } from "./text-window.entity.functions.js";
|
|
6
|
+
import { displayOf, excludesBelow, isWithin } from "./workspace-path.entity.functions.js";
|
|
7
|
+
import { bashEnvironment } from "./workspace-tools.entity.functions.js";
|
|
8
|
+
const empty = { search: emptyRgSearch, buffer: "", stderr: "", exit: Option.none(), invalid: !1 }, failure = (error, message) => ({ error, message });
|
|
9
|
+
export const searchRipgrep = Effect.fn("workspace.grep")(function* (access, target, directory, params, config) {
|
|
10
|
+
const bash = yield* Bash, run = yield* Effect.serviceOption(RunContext), cwd = directory ? target.path : access.path.dirname(target.path), input = directory ? "." : access.path.basename(target.path), exclude = excludesBelow(config.exclude, isWithin(access.root, cwd) ? Option.some(access.path.relative(access.root, cwd)) : Option.none()), limit = params.limit ?? config.grepMatches, perFile = directory ? Math.max(1, Math.ceil(limit / 4)) : limit, options = { limit, perFile, directory, lineChars: config.grepLineChars, display: (path) => displayOf(access, access.path.resolve(cwd, path)) }, fold = (state, event) => {
|
|
11
|
+
if (event._tag === "BashExit")
|
|
12
|
+
return { ...state, exit: Option.some(event) };
|
|
13
|
+
if (event.stream === "stderr")
|
|
14
|
+
return { ...state, stderr: clipHead(state.stderr + event.text, 16384) };
|
|
15
|
+
const complete = state.buffer + event.text;
|
|
16
|
+
if (complete.split(`
|
|
17
|
+
`).some((line) => line.length > config.maxFileBytes * 6 + 65536))
|
|
18
|
+
return { ...state, invalid: !0 };
|
|
19
|
+
const parsed = rgRecords("", complete);
|
|
20
|
+
return { ...state, buffer: parsed.buffer, search: parsed.records.reduce((search, message) => search.stopped ? search : foldRgMessage(search, message, options), state.search) };
|
|
21
|
+
}, output = yield* bash.stream(ripgrepCommand(config, params, input, perFile, exclude), {
|
|
22
|
+
cwd,
|
|
23
|
+
stdin: params.pattern.length === 0 ? `
|
|
24
|
+
` : params.pattern,
|
|
25
|
+
timeoutMs: config.grepTimeoutMs,
|
|
26
|
+
...bashEnvironment(config, Option.map(run, (current) => ({ sessionId: String(current.session.id), runId: current.runId, workspace: access.root })))
|
|
27
|
+
}).pipe(Stream.mapAccum(() => empty, (state, event) => {
|
|
28
|
+
const next = fold(state, event);
|
|
29
|
+
return [next, [next]];
|
|
30
|
+
}), Stream.takeUntil((state) => state.search.stopped || state.invalid), Stream.runFold(() => empty, (_previous, state) => state), Effect.mapError((error) => failure(error.reason, error.message))), search = Option.match(decodeRgMessage(output.buffer), {
|
|
31
|
+
onNone: () => output.search,
|
|
32
|
+
onSome: (message) => foldRgMessage(output.search, message, options)
|
|
33
|
+
});
|
|
34
|
+
if (output.invalid)
|
|
35
|
+
return yield* Effect.fail(failure("SearchFailed", "The search output was too large to read; narrow the path or the pattern and search again"));
|
|
36
|
+
const timedOut = Option.exists(output.exit, (exit) => exit.termination === "timeout"), unreadable = output.stderr.split(`
|
|
37
|
+
`).flatMap((line) => {
|
|
38
|
+
const found = /^rg: (.*): .*\(os error (\d+)\)$/.exec(line);
|
|
39
|
+
return found === null ? [] : [{ path: options.display(found[1] ?? ""), reason: rgReasonOf(Number(found[2])) }];
|
|
40
|
+
});
|
|
41
|
+
if (Option.exists(output.exit, (exit) => Option.contains(exit.exitCode, 2)) && search.messages === 0 && unreadable.length === 0)
|
|
42
|
+
return yield* Effect.fail(failure("InvalidPattern", `The search pattern or glob could not be parsed: ${output.stderr.trim()}. Pass literal: true to search for the pattern as text`));
|
|
43
|
+
if (Option.exists(output.exit, (exit) => exit.termination === "signal" || exit.termination === "exit" && !Option.exists(exit.exitCode, (code) => code === 0 || code === 1 || code === 2)))
|
|
44
|
+
return yield* Effect.fail(failure("SearchFailed", "The file search could not finish; try a narrower path or tell the user the search is unavailable"));
|
|
45
|
+
return {
|
|
46
|
+
pattern: params.pattern,
|
|
47
|
+
path: target.display,
|
|
48
|
+
lines: rgLines(search, params.context ?? 0),
|
|
49
|
+
matches: [...search.counts.values()].reduce((sum, count) => sum + count, 0),
|
|
50
|
+
files: search.counts.size,
|
|
51
|
+
skipped: search.skipped,
|
|
52
|
+
truncated: search.truncated,
|
|
53
|
+
unreadable,
|
|
54
|
+
timedOut
|
|
55
|
+
};
|
|
56
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
/** ripgrep represents non-UTF-8 paths and lines as base64 bytes. */
|
|
3
|
+
export declare const RgText: Schema.Union<readonly [Schema.Struct<{
|
|
4
|
+
readonly text: Schema.String;
|
|
5
|
+
}>, Schema.Struct<{
|
|
6
|
+
readonly bytes: Schema.StringFromBase64;
|
|
7
|
+
}>]>;
|
|
8
|
+
export type RgText = typeof RgText.Type;
|
|
9
|
+
/** Only the fields the workspace search consumes from ripgrep's JSON stream. */
|
|
10
|
+
export declare const RgMessage: Schema.toTaggedUnion<"type", readonly [Schema.Struct<{
|
|
11
|
+
readonly type: Schema.Literal<"begin">;
|
|
12
|
+
readonly data: Schema.Struct<{
|
|
13
|
+
readonly path: Schema.Union<readonly [Schema.Struct<{
|
|
14
|
+
readonly text: Schema.String;
|
|
15
|
+
}>, Schema.Struct<{
|
|
16
|
+
readonly bytes: Schema.StringFromBase64;
|
|
17
|
+
}>]>;
|
|
18
|
+
}>;
|
|
19
|
+
}>, Schema.Struct<{
|
|
20
|
+
readonly type: Schema.Literal<"match">;
|
|
21
|
+
readonly data: Schema.Struct<{
|
|
22
|
+
readonly path: Schema.Union<readonly [Schema.Struct<{
|
|
23
|
+
readonly text: Schema.String;
|
|
24
|
+
}>, Schema.Struct<{
|
|
25
|
+
readonly bytes: Schema.StringFromBase64;
|
|
26
|
+
}>]>;
|
|
27
|
+
readonly lines: Schema.Union<readonly [Schema.Struct<{
|
|
28
|
+
readonly text: Schema.String;
|
|
29
|
+
}>, Schema.Struct<{
|
|
30
|
+
readonly bytes: Schema.StringFromBase64;
|
|
31
|
+
}>]>;
|
|
32
|
+
readonly line_number: Schema.Int;
|
|
33
|
+
}>;
|
|
34
|
+
}>, Schema.Struct<{
|
|
35
|
+
readonly type: Schema.Literal<"context">;
|
|
36
|
+
readonly data: Schema.Struct<{
|
|
37
|
+
readonly path: Schema.Union<readonly [Schema.Struct<{
|
|
38
|
+
readonly text: Schema.String;
|
|
39
|
+
}>, Schema.Struct<{
|
|
40
|
+
readonly bytes: Schema.StringFromBase64;
|
|
41
|
+
}>]>;
|
|
42
|
+
readonly lines: Schema.Union<readonly [Schema.Struct<{
|
|
43
|
+
readonly text: Schema.String;
|
|
44
|
+
}>, Schema.Struct<{
|
|
45
|
+
readonly bytes: Schema.StringFromBase64;
|
|
46
|
+
}>]>;
|
|
47
|
+
readonly line_number: Schema.Int;
|
|
48
|
+
}>;
|
|
49
|
+
}>, Schema.Struct<{
|
|
50
|
+
readonly type: Schema.Literal<"end">;
|
|
51
|
+
readonly data: Schema.Struct<{
|
|
52
|
+
readonly path: Schema.Union<readonly [Schema.Struct<{
|
|
53
|
+
readonly text: Schema.String;
|
|
54
|
+
}>, Schema.Struct<{
|
|
55
|
+
readonly bytes: Schema.StringFromBase64;
|
|
56
|
+
}>]>;
|
|
57
|
+
readonly binary_offset: Schema.NullOr<Schema.Int>;
|
|
58
|
+
}>;
|
|
59
|
+
}>, Schema.Struct<{
|
|
60
|
+
readonly type: Schema.Literal<"summary">;
|
|
61
|
+
readonly data: Schema.Struct<{}>;
|
|
62
|
+
}>]>;
|
|
63
|
+
export type RgMessage = typeof RgMessage.Type;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Option } from "effect";
|
|
2
|
+
import { RgMessage } from "./ripgrep.entity.js";
|
|
3
|
+
import type { RgText } from "./ripgrep.entity.js";
|
|
4
|
+
import type { GrepLine, GrepParams, WorkspaceToolsConfig } from "./workspace-tools.entity.js";
|
|
5
|
+
export { shellQuote } from "./shell.entity.functions.js";
|
|
6
|
+
/** The pattern goes only to stdin, never into the shell command. */
|
|
7
|
+
export declare const ripgrepCommand: (config: WorkspaceToolsConfig, params: GrepParams, target: string, perFile: number, exclude: ReadonlyArray<string>) => string;
|
|
8
|
+
export declare const rgText: (value: RgText) => string;
|
|
9
|
+
export declare const decodeRgMessage: (input: unknown, options?: import("effect/SchemaAST").ParseOptions) => Option.Option<{
|
|
10
|
+
readonly type: "begin";
|
|
11
|
+
readonly data: {
|
|
12
|
+
readonly path: {
|
|
13
|
+
readonly text: string;
|
|
14
|
+
} | {
|
|
15
|
+
readonly bytes: string;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
} | {
|
|
19
|
+
readonly type: "match";
|
|
20
|
+
readonly data: {
|
|
21
|
+
readonly path: {
|
|
22
|
+
readonly text: string;
|
|
23
|
+
} | {
|
|
24
|
+
readonly bytes: string;
|
|
25
|
+
};
|
|
26
|
+
readonly lines: {
|
|
27
|
+
readonly text: string;
|
|
28
|
+
} | {
|
|
29
|
+
readonly bytes: string;
|
|
30
|
+
};
|
|
31
|
+
readonly line_number: number;
|
|
32
|
+
};
|
|
33
|
+
} | {
|
|
34
|
+
readonly type: "context";
|
|
35
|
+
readonly data: {
|
|
36
|
+
readonly path: {
|
|
37
|
+
readonly text: string;
|
|
38
|
+
} | {
|
|
39
|
+
readonly bytes: string;
|
|
40
|
+
};
|
|
41
|
+
readonly lines: {
|
|
42
|
+
readonly text: string;
|
|
43
|
+
} | {
|
|
44
|
+
readonly bytes: string;
|
|
45
|
+
};
|
|
46
|
+
readonly line_number: number;
|
|
47
|
+
};
|
|
48
|
+
} | {
|
|
49
|
+
readonly type: "end";
|
|
50
|
+
readonly data: {
|
|
51
|
+
readonly path: {
|
|
52
|
+
readonly text: string;
|
|
53
|
+
} | {
|
|
54
|
+
readonly bytes: string;
|
|
55
|
+
};
|
|
56
|
+
readonly binary_offset: number | null;
|
|
57
|
+
};
|
|
58
|
+
} | {
|
|
59
|
+
readonly type: "summary";
|
|
60
|
+
readonly data: {};
|
|
61
|
+
}>;
|
|
62
|
+
export interface RgSearch {
|
|
63
|
+
readonly lines: ReadonlyArray<GrepLine>;
|
|
64
|
+
readonly counts: ReadonlyMap<string, number>;
|
|
65
|
+
readonly skipped: number;
|
|
66
|
+
readonly truncated: boolean;
|
|
67
|
+
readonly stopAtEnd: boolean;
|
|
68
|
+
readonly capped: ReadonlySet<string>;
|
|
69
|
+
readonly stopped: boolean;
|
|
70
|
+
readonly messages: number;
|
|
71
|
+
}
|
|
72
|
+
export declare const emptyRgSearch: RgSearch;
|
|
73
|
+
/** Bound kept matches independently of rg's per-file cap (context can exceed it). */
|
|
74
|
+
export declare const foldRgMessage: (state: RgSearch, message: RgMessage, options: {
|
|
75
|
+
readonly limit: number;
|
|
76
|
+
readonly perFile: number;
|
|
77
|
+
readonly lineChars: number;
|
|
78
|
+
readonly directory?: boolean;
|
|
79
|
+
readonly display: (path: string) => string;
|
|
80
|
+
}) => RgSearch;
|
|
81
|
+
/** Keep only context surrounding selected matches; rg may send context for capped hits. */
|
|
82
|
+
export declare const rgLines: (state: RgSearch, context: number) => ReadonlyArray<GrepLine>;
|
|
83
|
+
/** Carry a partial JSON line between Bash chunks; decode only complete records. */
|
|
84
|
+
export declare const rgRecords: (buffer: string, chunk: string) => {
|
|
85
|
+
readonly buffer: string;
|
|
86
|
+
readonly records: ReadonlyArray<RgMessage>;
|
|
87
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Option, Schema } from "effect";
|
|
2
|
+
import { RgMessage } from "./ripgrep.entity.js";
|
|
3
|
+
import { clipChars } from "./text-window.entity.functions.js";
|
|
4
|
+
import { shellQuote } from "./shell.entity.functions.js";
|
|
5
|
+
export { shellQuote } from "./shell.entity.functions.js";
|
|
6
|
+
export const ripgrepCommand = (config, params, target, perFile, exclude) => [
|
|
7
|
+
"rg",
|
|
8
|
+
"--json",
|
|
9
|
+
"--no-config",
|
|
10
|
+
"--sort",
|
|
11
|
+
"path",
|
|
12
|
+
"--hidden",
|
|
13
|
+
"--no-follow",
|
|
14
|
+
...config.grepIgnoreFiles ? [] : ["--no-ignore"],
|
|
15
|
+
"--max-filesize",
|
|
16
|
+
String(config.maxFileBytes),
|
|
17
|
+
"--max-count",
|
|
18
|
+
String(perFile + 1),
|
|
19
|
+
...params.context === void 0 ? [] : ["-C", String(params.context)],
|
|
20
|
+
...params.ignoreCase ? ["-i"] : [],
|
|
21
|
+
...params.literal ? ["-F"] : [],
|
|
22
|
+
"-f",
|
|
23
|
+
"-",
|
|
24
|
+
...params.glob === void 0 ? [] : [`--glob=${params.glob}`],
|
|
25
|
+
...exclude.map((glob) => `--glob=!${glob.replace(/^\.\//, "/")}`),
|
|
26
|
+
"--",
|
|
27
|
+
target
|
|
28
|
+
].map(shellQuote).join(" "), rgText = (value) => ("text" in value) ? value.text : value.bytes, decodeRgMessage = Schema.decodeUnknownOption(Schema.fromJsonString(RgMessage)), emptyRgSearch = { lines: [], counts: new Map, skipped: 0, truncated: !1, stopped: !1, stopAtEnd: !1, capped: new Set, messages: 0 }, foldRgMessage = (state, message, options) => {
|
|
29
|
+
const next = { ...state, messages: state.messages + 1 }, add = (data, match) => {
|
|
30
|
+
const path = options.display(rgText(data.path)), count = state.counts.get(path) ?? 0, total = [...state.counts.values()].reduce((sum, found) => sum + found, 0), text = rgText(data.lines).replace(/\r?\n$/, ""), row = { path, line: data.line_number, text: clipChars(text, options.lineChars).text, match };
|
|
31
|
+
if (match && total >= options.limit)
|
|
32
|
+
return { ...next, lines: [...state.lines, { ...row, match: !1 }], truncated: !0, stopAtEnd: !0, capped: new Set([...state.capped, path]) };
|
|
33
|
+
if (match && count >= options.perFile)
|
|
34
|
+
return { ...next, lines: [...state.lines, { ...row, match: !1 }], truncated: !0, capped: new Set([...state.capped, path]) };
|
|
35
|
+
return {
|
|
36
|
+
...next,
|
|
37
|
+
lines: [...state.lines, row],
|
|
38
|
+
counts: match ? new Map([...state.counts, [path, count + 1]]) : state.counts
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
return RgMessage.match(message, {
|
|
42
|
+
begin: () => next,
|
|
43
|
+
match: ({ data }) => add(data, !0),
|
|
44
|
+
context: ({ data }) => add(data, !1),
|
|
45
|
+
end: ({ data }) => {
|
|
46
|
+
if (data.binary_offset === null) {
|
|
47
|
+
const atLimit = [...state.counts.values()].reduce((sum, count) => sum + count, 0) >= options.limit;
|
|
48
|
+
return { ...next, stopped: state.stopAtEnd || atLimit, truncated: state.truncated || options.directory === !0 && atLimit };
|
|
49
|
+
}
|
|
50
|
+
const path = options.display(rgText(data.path));
|
|
51
|
+
return { ...next, lines: state.lines.filter((line) => line.path !== path), counts: new Map([...state.counts].filter(([name]) => name !== path)), skipped: state.skipped + 1, stopped: !1, stopAtEnd: !1, capped: new Set([...state.capped].filter((name) => name !== path)), truncated: [...state.capped].some((name) => name !== path) };
|
|
52
|
+
},
|
|
53
|
+
summary: () => next
|
|
54
|
+
});
|
|
55
|
+
}, rgLines = (state, context) => {
|
|
56
|
+
const shown = new Set(state.lines.filter((line) => line.match).flatMap((hit) => Array.from({ length: context * 2 + 1 }, (_, offset) => `${hit.path}\x00${hit.line - context + offset}`)));
|
|
57
|
+
return state.lines.filter((line) => shown.has(`${line.path}\x00${line.line}`));
|
|
58
|
+
}, rgRecords = (buffer, chunk) => {
|
|
59
|
+
const lines = (buffer + chunk).split(`
|
|
60
|
+
`);
|
|
61
|
+
return { buffer: lines.at(-1) ?? "", records: lines.slice(0, -1).flatMap((line) => Option.toArray(decodeRgMessage(line))) };
|
|
62
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
export const RgText = Schema.Union([Schema.Struct({ text: Schema.String }), Schema.Struct({ bytes: Schema.StringFromBase64 })]);
|
|
3
|
+
const line = Schema.Struct({ path: RgText, lines: RgText, line_number: Schema.Int.check(Schema.isGreaterThan(0)) });
|
|
4
|
+
export const RgMessage = Schema.Union([
|
|
5
|
+
Schema.Struct({ type: Schema.Literal("begin"), data: Schema.Struct({ path: RgText }) }),
|
|
6
|
+
Schema.Struct({ type: Schema.Literal("match"), data: line }),
|
|
7
|
+
Schema.Struct({ type: Schema.Literal("context"), data: line }),
|
|
8
|
+
Schema.Struct({ type: Schema.Literal("end"), data: Schema.Struct({ path: RgText, binary_offset: Schema.NullOr(Schema.Int) }) }),
|
|
9
|
+
Schema.Struct({ type: Schema.Literal("summary"), data: Schema.Struct({}) })
|
|
10
|
+
]).pipe(Schema.toTaggedUnion("type"));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const shellQuote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Effect, Layer } from "effect";
|
|
2
|
+
import { Capabilities, FileSystem, HarnessError } from "@xandreed/core";
|
|
3
|
+
import type { SkillDefinition } from "@xandreed/core";
|
|
4
|
+
import { SkillsDirectoryConfig } from "./skills-directory.entity.js";
|
|
5
|
+
/**
|
|
6
|
+
* The skills in `config.roots`, each resolved against `workspace` (an
|
|
7
|
+
* absolute directory) unless absolute, read through the `FileSystem` port:
|
|
8
|
+
* the ids prefixed, the first root winning a clash, issues failing
|
|
9
|
+
* (`strict`) or logged, and the limits checked. What `skillsDirectoryPlugin`
|
|
10
|
+
* contributes; a host that composes capabilities itself can call it.
|
|
11
|
+
*
|
|
12
|
+
* @public
|
|
13
|
+
*/
|
|
14
|
+
export declare const loadSkillsDirectory: (config: SkillsDirectoryConfig, workspace: string) => Effect.Effect<ReadonlyArray<SkillDefinition>, HarnessError, FileSystem>;
|
|
15
|
+
/**
|
|
16
|
+
* Skills from directories of `SKILL.md` files (the Agent Skills layout,
|
|
17
|
+
* `.agents/skills/<name>/SKILL.md` by default), read once per agent through
|
|
18
|
+
* the `FileSystem` port and contributed as one capability. Tool discovery
|
|
19
|
+
* lists them in its catalogue, `load_skill` returns a skill's instructions
|
|
20
|
+
* and `read_skill_reference` the other files of its directory. A skill id
|
|
21
|
+
* the host or another plugin also defines fails `Agent.define` with
|
|
22
|
+
* `skills.duplicate`: set `idPrefix`. Relative roots resolve against the
|
|
23
|
+
* agent's workspace (`SessionEnvironment`), else the process's working
|
|
24
|
+
* directory.
|
|
25
|
+
*/
|
|
26
|
+
export declare const skillsDirectoryPlugin: import("@xandreed/core").TypedPlugin<{
|
|
27
|
+
readonly roots: readonly string[];
|
|
28
|
+
readonly required: boolean;
|
|
29
|
+
readonly strict: boolean;
|
|
30
|
+
readonly tools: "ignore" | "declared";
|
|
31
|
+
readonly idPrefix: string;
|
|
32
|
+
readonly maxReferenceBytes: number;
|
|
33
|
+
readonly maxSkills: number;
|
|
34
|
+
readonly maxFiles: number;
|
|
35
|
+
readonly maxBytes: number;
|
|
36
|
+
readonly alwaysSection: boolean;
|
|
37
|
+
readonly sectionOrder: number;
|
|
38
|
+
}, {
|
|
39
|
+
readonly roots: readonly string[];
|
|
40
|
+
readonly required: boolean;
|
|
41
|
+
readonly strict: boolean;
|
|
42
|
+
readonly tools: "ignore" | "declared";
|
|
43
|
+
readonly idPrefix: string;
|
|
44
|
+
readonly maxReferenceBytes: number;
|
|
45
|
+
readonly maxSkills: number;
|
|
46
|
+
readonly maxFiles: number;
|
|
47
|
+
readonly maxBytes: number;
|
|
48
|
+
readonly alwaysSection: boolean;
|
|
49
|
+
readonly sectionOrder: number;
|
|
50
|
+
}, Capabilities, HarnessError, FileSystem>;
|
|
51
|
+
/**
|
|
52
|
+
* The skills directory as a typed layer: contributes `Capabilities`; requires `FileSystem` (and reads `SessionEnvironment` when present).
|
|
53
|
+
*
|
|
54
|
+
* @public
|
|
55
|
+
*/
|
|
56
|
+
export declare const SkillsDirectoryPluginLive: (options?: {
|
|
57
|
+
readonly roots?: readonly string[];
|
|
58
|
+
readonly required?: boolean;
|
|
59
|
+
readonly strict?: boolean;
|
|
60
|
+
readonly tools?: "ignore" | "declared";
|
|
61
|
+
readonly idPrefix?: string;
|
|
62
|
+
readonly maxReferenceBytes?: number;
|
|
63
|
+
readonly maxSkills?: number;
|
|
64
|
+
readonly maxFiles?: number;
|
|
65
|
+
readonly maxBytes?: number;
|
|
66
|
+
readonly alwaysSection?: boolean;
|
|
67
|
+
readonly sectionOrder?: number;
|
|
68
|
+
} | undefined) => Layer.Layer<Capabilities, HarnessError, FileSystem>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { basename, resolve } from "node:path";
|
|
2
|
+
import { Effect, Layer, Option } from "effect";
|
|
3
|
+
import { Capabilities, collectSkillFiles, definePlugin, FileSystem, HarnessError, readSkillFiles, SessionEnvironment } from "@xandreed/core";
|
|
4
|
+
import { SkillsDirectoryConfig, skillsDirectoryDefaults } from "./skills-directory.entity.js";
|
|
5
|
+
import { mergeSkillRoots, settleSkills, skillsCapability } from "./skills-directory.entity.functions.js";
|
|
6
|
+
const rootFailure = (message) => new HarnessError({ code: "skills.root", message }), readRoot = (root, config) => Effect.gen(function* () {
|
|
7
|
+
if (!(yield* (yield* FileSystem).exists(root))) {
|
|
8
|
+
if (config.required)
|
|
9
|
+
return yield* Effect.fail(rootFailure(`the skills root ${root} does not exist (required)`));
|
|
10
|
+
yield* Effect.logDebug(`skills: no directory at ${root}; skipped`);
|
|
11
|
+
return Option.none();
|
|
12
|
+
}
|
|
13
|
+
const files = yield* collectSkillFiles(root, { maxFiles: config.maxFiles, maxBytes: config.maxBytes, maxFileBytes: config.maxReferenceBytes }), tree = readSkillFiles(files, { tools: config.tools, maxReferenceBytes: config.maxReferenceBytes, rootName: basename(root) });
|
|
14
|
+
return Option.some({ root, tree });
|
|
15
|
+
}).pipe(Effect.catchTag("FsError", (error) => Effect.fail(rootFailure(`cannot read the skills root ${root}: ${error.path}: ${error.message}`))));
|
|
16
|
+
export const loadSkillsDirectory = (config, workspace) => Effect.forEach(config.roots, (root) => readRoot(resolve(workspace, root), config), { concurrency: 1 }).pipe(Effect.map((roots) => mergeSkillRoots(roots.flatMap(Option.toArray), config.idPrefix)), Effect.flatMap((found) => settleSkills(found, config))), skillsDirectoryPlugin = definePlugin({
|
|
17
|
+
id: "@xandreed/plugin-workspace/skills",
|
|
18
|
+
version: "0.15.0",
|
|
19
|
+
scope: "runtime",
|
|
20
|
+
config: SkillsDirectoryConfig,
|
|
21
|
+
defaults: skillsDirectoryDefaults,
|
|
22
|
+
requires: [FileSystem],
|
|
23
|
+
optional: [SessionEnvironment],
|
|
24
|
+
provides: [],
|
|
25
|
+
contributes: [Capabilities],
|
|
26
|
+
layer: (config) => Layer.effect(Capabilities, Effect.gen(function* () {
|
|
27
|
+
const environment = yield* Effect.serviceOption(SessionEnvironment), workspace = resolve(Option.match(environment, { onNone: () => ".", onSome: (found) => found.workspace })), skills = yield* loadSkillsDirectory(config, workspace);
|
|
28
|
+
return [skillsCapability(skills, config)];
|
|
29
|
+
}))
|
|
30
|
+
}), SkillsDirectoryPluginLive = skillsDirectoryPlugin.live;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
/** One root as read: its absolute path and the tree of skills in it. */
|
|
3
|
+
export declare const SkillRoot: Schema.Struct<{
|
|
4
|
+
readonly root: Schema.String;
|
|
5
|
+
readonly tree: Schema.Struct<{
|
|
6
|
+
readonly skills: Schema.$Array<Schema.Struct<{
|
|
7
|
+
readonly directory: Schema.String;
|
|
8
|
+
readonly skill: Schema.Struct<{
|
|
9
|
+
readonly id: Schema.brand<Schema.Trimmed, "SkillId">;
|
|
10
|
+
readonly version: Schema.Trimmed;
|
|
11
|
+
readonly summary: Schema.Trimmed;
|
|
12
|
+
readonly instructions: Schema.String;
|
|
13
|
+
readonly tools: Schema.$Array<Schema.Trimmed>;
|
|
14
|
+
readonly always: Schema.Boolean;
|
|
15
|
+
readonly permissions: Schema.$Array<Schema.String>;
|
|
16
|
+
readonly references: Schema.$Array<Schema.Struct<{
|
|
17
|
+
readonly id: Schema.brand<Schema.Trimmed, "SkillReferenceId">;
|
|
18
|
+
readonly title: Schema.Trimmed;
|
|
19
|
+
readonly text: Schema.String;
|
|
20
|
+
}>>;
|
|
21
|
+
}>;
|
|
22
|
+
}>>;
|
|
23
|
+
readonly issues: Schema.$Array<Schema.Struct<{
|
|
24
|
+
readonly path: Schema.String;
|
|
25
|
+
readonly severity: Schema.Literals<readonly ["error", "warning"]>;
|
|
26
|
+
readonly message: Schema.String;
|
|
27
|
+
}>>;
|
|
28
|
+
}>;
|
|
29
|
+
}>;
|
|
30
|
+
export type SkillRoot = typeof SkillRoot.Type;
|
|
31
|
+
/** The skills of every root together, and every issue, its path absolute. */
|
|
32
|
+
export declare const SkillsFound: Schema.Struct<{
|
|
33
|
+
readonly skills: Schema.$Array<Schema.Struct<{
|
|
34
|
+
readonly id: Schema.brand<Schema.Trimmed, "SkillId">;
|
|
35
|
+
readonly version: Schema.Trimmed;
|
|
36
|
+
readonly summary: Schema.Trimmed;
|
|
37
|
+
readonly instructions: Schema.String;
|
|
38
|
+
readonly tools: Schema.$Array<Schema.Trimmed>;
|
|
39
|
+
readonly always: Schema.Boolean;
|
|
40
|
+
readonly permissions: Schema.$Array<Schema.String>;
|
|
41
|
+
readonly references: Schema.$Array<Schema.Struct<{
|
|
42
|
+
readonly id: Schema.brand<Schema.Trimmed, "SkillReferenceId">;
|
|
43
|
+
readonly title: Schema.Trimmed;
|
|
44
|
+
readonly text: Schema.String;
|
|
45
|
+
}>>;
|
|
46
|
+
}>>;
|
|
47
|
+
readonly issues: Schema.$Array<Schema.Struct<{
|
|
48
|
+
readonly path: Schema.String;
|
|
49
|
+
readonly severity: Schema.Literals<readonly ["error", "warning"]>;
|
|
50
|
+
readonly message: Schema.String;
|
|
51
|
+
}>>;
|
|
52
|
+
}>;
|
|
53
|
+
export type SkillsFound = typeof SkillsFound.Type;
|
|
54
|
+
/**
|
|
55
|
+
* How `skillsDirectoryPlugin` loads skills from directories of `SKILL.md`
|
|
56
|
+
* files. Flat.
|
|
57
|
+
*
|
|
58
|
+
* - `roots`: the directories, absolute or relative to the agent's workspace
|
|
59
|
+
* (`SessionEnvironment`, else the process's working directory). A skill
|
|
60
|
+
* whose id an earlier root already loaded is left out (an issue). One
|
|
61
|
+
* instance with several roots is how to read several directories: the
|
|
62
|
+
* capability and section ids are fixed, so two instances would share them.
|
|
63
|
+
* - `required`: a missing root fails `Agent.define` (`skills.root`); when
|
|
64
|
+
* false, a missing root is skipped.
|
|
65
|
+
* - `strict`: any issue (a `SKILL.md` without frontmatter or description, an
|
|
66
|
+
* id taken, a reference over `maxReferenceBytes`, an unreadable value)
|
|
67
|
+
* fails `Agent.define` with `skills.files`; when false, each is logged as a
|
|
68
|
+
* warning and what it names is left out.
|
|
69
|
+
* - `tools`: `"ignore"` drops the `tools` each `SKILL.md` declares, so a file
|
|
70
|
+
* skill is instructions and references only; `"declared"` keeps them, and
|
|
71
|
+
* a tool no capability registers fails `Agent.define` with `skills.tools`.
|
|
72
|
+
* - `idPrefix`: put before every skill id (`"project."`), for skills whose
|
|
73
|
+
* ids clash with the host's or another plugin's (`skills.duplicate`).
|
|
74
|
+
* - `maxReferenceBytes`: a reference larger than this is left out (it is
|
|
75
|
+
* not read, and does not count toward `maxBytes`).
|
|
76
|
+
* - `maxSkills` (all roots together), `maxFiles` and `maxBytes` (each root's
|
|
77
|
+
* files, and bytes of text): more fails `Agent.define` with `skills.limit`.
|
|
78
|
+
* - `alwaysSection`: render the instructions of the skills marked `always`
|
|
79
|
+
* in a static system-prompt section (tool discovery activates always-on
|
|
80
|
+
* skills but never shows their instructions), at `sectionOrder`.
|
|
81
|
+
*
|
|
82
|
+
* @public
|
|
83
|
+
*/
|
|
84
|
+
export declare const SkillsDirectoryConfig: Schema.Struct<{
|
|
85
|
+
readonly roots: Schema.$Array<Schema.NonEmptyString>;
|
|
86
|
+
readonly required: Schema.Boolean;
|
|
87
|
+
readonly strict: Schema.Boolean;
|
|
88
|
+
readonly tools: Schema.Literals<readonly ["declared", "ignore"]>;
|
|
89
|
+
readonly idPrefix: Schema.String;
|
|
90
|
+
readonly maxReferenceBytes: Schema.Int;
|
|
91
|
+
readonly maxSkills: Schema.Int;
|
|
92
|
+
readonly maxFiles: Schema.Int;
|
|
93
|
+
readonly maxBytes: Schema.Int;
|
|
94
|
+
readonly alwaysSection: Schema.Boolean;
|
|
95
|
+
readonly sectionOrder: Schema.Int;
|
|
96
|
+
}>;
|
|
97
|
+
export type SkillsDirectoryConfig = typeof SkillsDirectoryConfig.Type;
|
|
98
|
+
/** @public */
|
|
99
|
+
export declare const skillsDirectoryDefaults: SkillsDirectoryConfig;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { Effect, Option } from "effect";
|
|
2
|
+
import { HarnessError } from "@xandreed/core";
|
|
3
|
+
import type { Capability, SkillDefinition } from "@xandreed/core";
|
|
4
|
+
import type { SkillRoot, SkillsDirectoryConfig, SkillsFound } from "./skills-directory.entity.js";
|
|
5
|
+
/**
|
|
6
|
+
* The id of the capability the skills are contributed in. It is fixed, as
|
|
7
|
+
* is `ALWAYS_SECTION_ID`: read several directories with one plugin instance
|
|
8
|
+
* and several `roots`. Two instances would contribute two capabilities, and
|
|
9
|
+
* two sections, with the same id in `context.built` and turn records.
|
|
10
|
+
*
|
|
11
|
+
* @public
|
|
12
|
+
*/
|
|
13
|
+
export declare const SKILLS_CAPABILITY_ID = "@xandreed/plugin-workspace/skills";
|
|
14
|
+
/**
|
|
15
|
+
* The static section with the always-on skills' instructions.
|
|
16
|
+
*
|
|
17
|
+
* @public
|
|
18
|
+
*/
|
|
19
|
+
export declare const ALWAYS_SECTION_ID = "workspace.skills.always";
|
|
20
|
+
/**
|
|
21
|
+
* Every root's skills together, in root order: each id with `idPrefix`
|
|
22
|
+
* before it, a skill whose id an earlier root (or an earlier skill) already
|
|
23
|
+
* loaded left out with an error, and every issue's path made absolute.
|
|
24
|
+
*/
|
|
25
|
+
export declare const mergeSkillRoots: (roots: ReadonlyArray<SkillRoot>, idPrefix: string) => SkillsFound;
|
|
26
|
+
/**
|
|
27
|
+
* The skills to contribute: with `strict`, any issue fails with
|
|
28
|
+
* `skills.files` naming each one; otherwise each is logged as a warning.
|
|
29
|
+
* More than `maxSkills` fails with `skills.limit`.
|
|
30
|
+
*/
|
|
31
|
+
export declare const settleSkills: (found: {
|
|
32
|
+
readonly skills: readonly {
|
|
33
|
+
readonly id: string & import("effect/Brand").Brand<"SkillId">;
|
|
34
|
+
readonly version: string;
|
|
35
|
+
readonly summary: string;
|
|
36
|
+
readonly instructions: string;
|
|
37
|
+
readonly tools: readonly string[];
|
|
38
|
+
readonly always: boolean;
|
|
39
|
+
readonly permissions: readonly string[];
|
|
40
|
+
readonly references: readonly {
|
|
41
|
+
readonly id: string & import("effect/Brand").Brand<"SkillReferenceId">;
|
|
42
|
+
readonly title: string;
|
|
43
|
+
readonly text: string;
|
|
44
|
+
}[];
|
|
45
|
+
}[];
|
|
46
|
+
readonly issues: readonly {
|
|
47
|
+
readonly path: string;
|
|
48
|
+
readonly severity: "error" | "warning";
|
|
49
|
+
readonly message: string;
|
|
50
|
+
}[];
|
|
51
|
+
}, config: Pick<{
|
|
52
|
+
readonly roots: readonly string[];
|
|
53
|
+
readonly required: boolean;
|
|
54
|
+
readonly strict: boolean;
|
|
55
|
+
readonly tools: "ignore" | "declared";
|
|
56
|
+
readonly idPrefix: string;
|
|
57
|
+
readonly maxReferenceBytes: number;
|
|
58
|
+
readonly maxSkills: number;
|
|
59
|
+
readonly maxFiles: number;
|
|
60
|
+
readonly maxBytes: number;
|
|
61
|
+
readonly alwaysSection: boolean;
|
|
62
|
+
readonly sectionOrder: number;
|
|
63
|
+
}, "strict" | "maxSkills">) => Effect.Effect<readonly {
|
|
64
|
+
readonly id: string & import("effect/Brand").Brand<"SkillId">;
|
|
65
|
+
readonly version: string;
|
|
66
|
+
readonly summary: string;
|
|
67
|
+
readonly instructions: string;
|
|
68
|
+
readonly tools: readonly string[];
|
|
69
|
+
readonly always: boolean;
|
|
70
|
+
readonly permissions: readonly string[];
|
|
71
|
+
readonly references: readonly {
|
|
72
|
+
readonly id: string & import("effect/Brand").Brand<"SkillReferenceId">;
|
|
73
|
+
readonly title: string;
|
|
74
|
+
readonly text: string;
|
|
75
|
+
}[];
|
|
76
|
+
}[], HarnessError, never>;
|
|
77
|
+
/**
|
|
78
|
+
* The always-on skills' instructions as one system-prompt section: tool
|
|
79
|
+
* discovery activates them at every turn's start but lists only the skills
|
|
80
|
+
* that are loaded on demand.
|
|
81
|
+
*/
|
|
82
|
+
export declare const alwaysSkillsText: (skills: ReadonlyArray<SkillDefinition>) => Option.Option<string>;
|
|
83
|
+
/**
|
|
84
|
+
* The skills as one capability, with the always-on section when `alwaysSection` is set and some skill is always on.
|
|
85
|
+
*
|
|
86
|
+
* @public
|
|
87
|
+
*/
|
|
88
|
+
export declare const skillsCapability: (skills: ReadonlyArray<SkillDefinition>, config: Pick<SkillsDirectoryConfig, "alwaysSection" | "sectionOrder">) => Capability;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Effect, Option } from "effect";
|
|
2
|
+
import { canonicalJson, defineCapability, definePromptSection, fingerprintOf, HarnessError, SkillId, skillIssuesText } from "@xandreed/core";
|
|
3
|
+
export const SKILLS_CAPABILITY_ID = "@xandreed/plugin-workspace/skills", ALWAYS_SECTION_ID = "workspace.skills.always";
|
|
4
|
+
const inRoot = (root, path) => root.endsWith("/") ? `${root}${path}` : `${root}/${path}`, skillFileOf = (root, directory) => inRoot(root, directory === "" ? "SKILL.md" : `${directory}/SKILL.md`);
|
|
5
|
+
export const mergeSkillRoots = (roots, idPrefix) => {
|
|
6
|
+
const merged = roots.reduce((sofar, { root, tree }) => tree.skills.reduce((state, entry) => {
|
|
7
|
+
const skill = { ...entry.skill, id: SkillId.make(`${idPrefix}${entry.skill.id}`) }, from = skillFileOf(root, entry.directory), taken = state.skills.find((other) => other.skill.id === skill.id);
|
|
8
|
+
return taken === void 0 ? { ...state, skills: [...state.skills, { skill, from }] } : { ...state, issues: [...state.issues, { path: from, severity: "error", message: `the skill id ${skill.id} is already loaded from ${taken.from}` }] };
|
|
9
|
+
}, { ...sofar, issues: [...sofar.issues, ...tree.issues.map((found) => ({ ...found, path: inRoot(root, found.path) }))] }), { skills: [], issues: [] });
|
|
10
|
+
return { skills: merged.skills.map((item) => item.skill), issues: merged.issues };
|
|
11
|
+
}, settleSkills = Effect.fn("skills.settle")(function* (found, config) {
|
|
12
|
+
if (config.strict && found.issues.length > 0)
|
|
13
|
+
return yield* Effect.fail(new HarnessError({ code: "skills.files", message: skillIssuesText(found.issues) }));
|
|
14
|
+
yield* Effect.forEach(found.issues, (issue) => Effect.logWarning(`skills: ${issue.path}: ${issue.message} (${issue.severity === "error" ? "skill left out" : "read without it"})`), { concurrency: 1, discard: !0 });
|
|
15
|
+
if (found.skills.length > config.maxSkills)
|
|
16
|
+
return yield* Effect.fail(new HarnessError({ code: "skills.limit", message: `found ${found.skills.length} skills, more than ${config.maxSkills} (maxSkills)` }));
|
|
17
|
+
return found.skills;
|
|
18
|
+
}), alwaysSkillsText = (skills) => {
|
|
19
|
+
const always = skills.filter((skill) => skill.always);
|
|
20
|
+
return always.length === 0 ? Option.none() : Option.some([
|
|
21
|
+
"## Always-on skills",
|
|
22
|
+
...always.map((skill) => [
|
|
23
|
+
`### ${skill.id}`,
|
|
24
|
+
skill.instructions.length > 0 ? skill.instructions : skill.summary,
|
|
25
|
+
skill.references.length === 0 ? "" : `References (read_skill_reference): ${skill.references.map((reference) => `${reference.id} \u2014 ${reference.title}`).join("; ")}`
|
|
26
|
+
].filter((line) => line.length > 0).join(`
|
|
27
|
+
`))
|
|
28
|
+
].join(`
|
|
29
|
+
|
|
30
|
+
`));
|
|
31
|
+
}, skillsCapability = (skills, config) => {
|
|
32
|
+
const section = Option.filter(alwaysSkillsText(skills), () => config.alwaysSection).pipe(Option.map((text) => definePromptSection({
|
|
33
|
+
id: ALWAYS_SECTION_ID,
|
|
34
|
+
version: fingerprintOf(text),
|
|
35
|
+
tier: "static",
|
|
36
|
+
order: config.sectionOrder,
|
|
37
|
+
render: () => Effect.succeed(Option.some(text))
|
|
38
|
+
})));
|
|
39
|
+
return defineCapability({
|
|
40
|
+
id: SKILLS_CAPABILITY_ID,
|
|
41
|
+
version: fingerprintOf(canonicalJson(skills.map((skill) => [skill.id, skill.version]))),
|
|
42
|
+
skills,
|
|
43
|
+
promptSections: Option.toArray(section)
|
|
44
|
+
});
|
|
45
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import { SkillDefinition, SkillFileIssue, SkillTree } from "@xandreed/core";
|
|
3
|
+
export const SkillRoot = Schema.Struct({
|
|
4
|
+
root: Schema.String,
|
|
5
|
+
tree: SkillTree
|
|
6
|
+
}), SkillsFound = Schema.Struct({
|
|
7
|
+
skills: Schema.Array(SkillDefinition),
|
|
8
|
+
issues: Schema.Array(SkillFileIssue)
|
|
9
|
+
});
|
|
10
|
+
const count = (maximum) => Schema.Int.pipe(Schema.check(Schema.isBetween({ minimum: 1, maximum })));
|
|
11
|
+
export const SkillsDirectoryConfig = Schema.Struct({
|
|
12
|
+
roots: Schema.Array(Schema.NonEmptyString),
|
|
13
|
+
required: Schema.Boolean,
|
|
14
|
+
strict: Schema.Boolean,
|
|
15
|
+
tools: Schema.Literals(["declared", "ignore"]),
|
|
16
|
+
idPrefix: Schema.String.check(Schema.isPattern(/^\S*$/)).annotate({ description: "Put before every skill id; no whitespace." }),
|
|
17
|
+
maxReferenceBytes: count(16777216),
|
|
18
|
+
maxSkills: count(1e4),
|
|
19
|
+
maxFiles: count(1e6),
|
|
20
|
+
maxBytes: count(1073741824),
|
|
21
|
+
alwaysSection: Schema.Boolean,
|
|
22
|
+
sectionOrder: Schema.Int
|
|
23
|
+
}), skillsDirectoryDefaults = {
|
|
24
|
+
roots: [".agents/skills"],
|
|
25
|
+
required: !1,
|
|
26
|
+
strict: !1,
|
|
27
|
+
tools: "ignore",
|
|
28
|
+
idPrefix: "",
|
|
29
|
+
maxReferenceBytes: 65536,
|
|
30
|
+
maxSkills: 200,
|
|
31
|
+
maxFiles: 2000,
|
|
32
|
+
maxBytes: 8388608,
|
|
33
|
+
alwaysSection: !0,
|
|
34
|
+
sectionOrder: 850
|
|
35
|
+
};
|