@xandreed/plugin-workspace 0.14.0 → 0.15.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 +1 -1
- package/dist/bash-tool.adapter.js +10 -9
- package/dist/bash.adapter.d.ts +60 -8
- package/dist/bash.adapter.js +56 -13
- package/dist/file-system.adapter.d.ts +5 -1
- package/dist/file-system.adapter.js +1 -1
- 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 +4 -2
- package/dist/index.js +4 -1
- package/dist/local-system.entity.d.ts +10 -3
- package/dist/local-system.entity.functions.d.ts +3 -2
- package/dist/local-system.entity.functions.js +16 -2
- package/dist/local-system.entity.js +3 -2
- package/dist/plugin.adapter.d.ts +103 -40
- package/dist/plugin.adapter.js +16 -5
- package/dist/read-tools.adapter.d.ts +10 -4
- package/dist/read-tools.adapter.js +40 -31
- 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 +20 -14
- package/dist/skills/skills-directory.adapter.js +2 -2
- package/dist/skills/skills-directory.entity.d.ts +8 -5
- package/dist/skills/skills-directory.entity.functions.d.ts +58 -3
- package/dist/skills/skills-directory.entity.functions.js +5 -5
- package/dist/text-edit.entity.functions.js +3 -3
- package/dist/text-search.entity.functions.d.ts +7 -0
- package/dist/text-search.entity.functions.js +2 -2
- package/dist/workspace-path.entity.d.ts +21 -0
- package/dist/workspace-path.entity.functions.d.ts +39 -9
- package/dist/workspace-path.entity.functions.js +43 -18
- package/dist/workspace-path.entity.js +3 -2
- package/dist/workspace-tools.adapter.d.ts +48 -3
- package/dist/workspace-tools.adapter.js +23 -9
- package/dist/workspace-tools.entity.d.ts +136 -17
- package/dist/workspace-tools.entity.functions.d.ts +80 -5
- package/dist/workspace-tools.entity.functions.js +87 -31
- package/dist/workspace-tools.entity.js +26 -5
- package/dist/write-tools.adapter.js +10 -8
- package/package.json +6 -5
- package/dist/bash.adapter.test.d.ts +0 -1
- package/dist/file-system.adapter.test.d.ts +0 -1
- package/dist/local-system.entity.functions.test.d.ts +0 -1
- package/dist/plugin.adapter.test.d.ts +0 -1
- package/dist/skills/skills-directory.adapter.test.d.ts +0 -1
- package/dist/skills/skills-directory.entity.functions.test.d.ts +0 -1
- package/dist/text-edit.entity.functions.test.d.ts +0 -1
- package/dist/text-search.entity.functions.test.d.ts +0 -1
- package/dist/text-window.entity.functions.test.d.ts +0 -1
- package/dist/workspace-path.entity.functions.test.d.ts +0 -1
- package/dist/workspace-tools.adapter.test.d.ts +0 -1
- package/dist/workspace-tools.entity.functions.test.d.ts +0 -1
- package/dist/workspace-tools.journey.test.d.ts +0 -1
|
@@ -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("'", "'\\''")}'`;
|
|
@@ -8,6 +8,8 @@ import { SkillsDirectoryConfig } from "./skills-directory.entity.js";
|
|
|
8
8
|
* the ids prefixed, the first root winning a clash, issues failing
|
|
9
9
|
* (`strict`) or logged, and the limits checked. What `skillsDirectoryPlugin`
|
|
10
10
|
* contributes; a host that composes capabilities itself can call it.
|
|
11
|
+
*
|
|
12
|
+
* @public
|
|
11
13
|
*/
|
|
12
14
|
export declare const loadSkillsDirectory: (config: SkillsDirectoryConfig, workspace: string) => Effect.Effect<ReadonlyArray<SkillDefinition>, HarnessError, FileSystem>;
|
|
13
15
|
/**
|
|
@@ -46,17 +48,21 @@ export declare const skillsDirectoryPlugin: import("@xandreed/core").TypedPlugin
|
|
|
46
48
|
readonly alwaysSection: boolean;
|
|
47
49
|
readonly sectionOrder: number;
|
|
48
50
|
}, Capabilities, HarnessError, FileSystem>;
|
|
49
|
-
/**
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
readonly
|
|
56
|
-
readonly
|
|
57
|
-
readonly
|
|
58
|
-
readonly
|
|
59
|
-
readonly
|
|
60
|
-
readonly
|
|
61
|
-
readonly
|
|
62
|
-
|
|
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>;
|
|
@@ -13,9 +13,9 @@ const rootFailure = (message) => new HarnessError({ code: "skills.root", message
|
|
|
13
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
14
|
return Option.some({ root, tree });
|
|
15
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)).pipe(Effect.map((roots) => mergeSkillRoots(roots.flatMap(Option.toArray), config.idPrefix)), Effect.flatMap((found) => settleSkills(found, config))), skillsDirectoryPlugin = definePlugin({
|
|
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
17
|
id: "@xandreed/plugin-workspace/skills",
|
|
18
|
-
version: "0.
|
|
18
|
+
version: "0.15.1",
|
|
19
19
|
scope: "runtime",
|
|
20
20
|
config: SkillsDirectoryConfig,
|
|
21
21
|
defaults: skillsDirectoryDefaults,
|
|
@@ -6,7 +6,7 @@ export declare const SkillRoot: Schema.Struct<{
|
|
|
6
6
|
readonly skills: Schema.$Array<Schema.Struct<{
|
|
7
7
|
readonly directory: Schema.String;
|
|
8
8
|
readonly skill: Schema.Struct<{
|
|
9
|
-
readonly id: Schema.Trimmed
|
|
9
|
+
readonly id: Schema.brand<Schema.Trimmed, "SkillId">;
|
|
10
10
|
readonly version: Schema.Trimmed;
|
|
11
11
|
readonly summary: Schema.Trimmed;
|
|
12
12
|
readonly instructions: Schema.String;
|
|
@@ -14,7 +14,7 @@ export declare const SkillRoot: Schema.Struct<{
|
|
|
14
14
|
readonly always: Schema.Boolean;
|
|
15
15
|
readonly permissions: Schema.$Array<Schema.String>;
|
|
16
16
|
readonly references: Schema.$Array<Schema.Struct<{
|
|
17
|
-
readonly id: Schema.Trimmed
|
|
17
|
+
readonly id: Schema.brand<Schema.Trimmed, "SkillReferenceId">;
|
|
18
18
|
readonly title: Schema.Trimmed;
|
|
19
19
|
readonly text: Schema.String;
|
|
20
20
|
}>>;
|
|
@@ -31,7 +31,7 @@ export type SkillRoot = typeof SkillRoot.Type;
|
|
|
31
31
|
/** The skills of every root together, and every issue, its path absolute. */
|
|
32
32
|
export declare const SkillsFound: Schema.Struct<{
|
|
33
33
|
readonly skills: Schema.$Array<Schema.Struct<{
|
|
34
|
-
readonly id: Schema.Trimmed
|
|
34
|
+
readonly id: Schema.brand<Schema.Trimmed, "SkillId">;
|
|
35
35
|
readonly version: Schema.Trimmed;
|
|
36
36
|
readonly summary: Schema.Trimmed;
|
|
37
37
|
readonly instructions: Schema.String;
|
|
@@ -39,7 +39,7 @@ export declare const SkillsFound: Schema.Struct<{
|
|
|
39
39
|
readonly always: Schema.Boolean;
|
|
40
40
|
readonly permissions: Schema.$Array<Schema.String>;
|
|
41
41
|
readonly references: Schema.$Array<Schema.Struct<{
|
|
42
|
-
readonly id: Schema.Trimmed
|
|
42
|
+
readonly id: Schema.brand<Schema.Trimmed, "SkillReferenceId">;
|
|
43
43
|
readonly title: Schema.Trimmed;
|
|
44
44
|
readonly text: Schema.String;
|
|
45
45
|
}>>;
|
|
@@ -53,7 +53,7 @@ export declare const SkillsFound: Schema.Struct<{
|
|
|
53
53
|
export type SkillsFound = typeof SkillsFound.Type;
|
|
54
54
|
/**
|
|
55
55
|
* How `skillsDirectoryPlugin` loads skills from directories of `SKILL.md`
|
|
56
|
-
* files. Flat
|
|
56
|
+
* files. Flat.
|
|
57
57
|
*
|
|
58
58
|
* - `roots`: the directories, absolute or relative to the agent's workspace
|
|
59
59
|
* (`SessionEnvironment`, else the process's working directory). A skill
|
|
@@ -78,6 +78,8 @@ export type SkillsFound = typeof SkillsFound.Type;
|
|
|
78
78
|
* - `alwaysSection`: render the instructions of the skills marked `always`
|
|
79
79
|
* in a static system-prompt section (tool discovery activates always-on
|
|
80
80
|
* skills but never shows their instructions), at `sectionOrder`.
|
|
81
|
+
*
|
|
82
|
+
* @public
|
|
81
83
|
*/
|
|
82
84
|
export declare const SkillsDirectoryConfig: Schema.Struct<{
|
|
83
85
|
readonly roots: Schema.$Array<Schema.NonEmptyString>;
|
|
@@ -93,4 +95,5 @@ export declare const SkillsDirectoryConfig: Schema.Struct<{
|
|
|
93
95
|
readonly sectionOrder: Schema.Int;
|
|
94
96
|
}>;
|
|
95
97
|
export type SkillsDirectoryConfig = typeof SkillsDirectoryConfig.Type;
|
|
98
|
+
/** @public */
|
|
96
99
|
export declare const skillsDirectoryDefaults: SkillsDirectoryConfig;
|
|
@@ -7,9 +7,15 @@ import type { SkillRoot, SkillsDirectoryConfig, SkillsFound } from "./skills-dir
|
|
|
7
7
|
* is `ALWAYS_SECTION_ID`: read several directories with one plugin instance
|
|
8
8
|
* and several `roots`. Two instances would contribute two capabilities, and
|
|
9
9
|
* two sections, with the same id in `context.built` and turn records.
|
|
10
|
+
*
|
|
11
|
+
* @public
|
|
10
12
|
*/
|
|
11
13
|
export declare const SKILLS_CAPABILITY_ID = "@xandreed/plugin-workspace/skills";
|
|
12
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* The static section with the always-on skills' instructions.
|
|
16
|
+
*
|
|
17
|
+
* @public
|
|
18
|
+
*/
|
|
13
19
|
export declare const ALWAYS_SECTION_ID = "workspace.skills.always";
|
|
14
20
|
/**
|
|
15
21
|
* Every root's skills together, in root order: each id with `idPrefix`
|
|
@@ -22,12 +28,61 @@ export declare const mergeSkillRoots: (roots: ReadonlyArray<SkillRoot>, idPrefix
|
|
|
22
28
|
* `skills.files` naming each one; otherwise each is logged as a warning.
|
|
23
29
|
* More than `maxSkills` fails with `skills.limit`.
|
|
24
30
|
*/
|
|
25
|
-
export declare const settleSkills: (found:
|
|
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>;
|
|
26
77
|
/**
|
|
27
78
|
* The always-on skills' instructions as one system-prompt section: tool
|
|
28
79
|
* discovery activates them at every turn's start but lists only the skills
|
|
29
80
|
* that are loaded on demand.
|
|
30
81
|
*/
|
|
31
82
|
export declare const alwaysSkillsText: (skills: ReadonlyArray<SkillDefinition>) => Option.Option<string>;
|
|
32
|
-
/**
|
|
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
|
+
*/
|
|
33
88
|
export declare const skillsCapability: (skills: ReadonlyArray<SkillDefinition>, config: Pick<SkillsDirectoryConfig, "alwaysSection" | "sectionOrder">) => Capability;
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { Effect, Option } from "effect";
|
|
2
|
-
import { canonicalJson, defineCapability, fingerprintOf, HarnessError, skillIssuesText } from "@xandreed/core";
|
|
2
|
+
import { canonicalJson, defineCapability, definePromptSection, fingerprintOf, HarnessError, SkillId, skillIssuesText } from "@xandreed/core";
|
|
3
3
|
export const SKILLS_CAPABILITY_ID = "@xandreed/plugin-workspace/skills", ALWAYS_SECTION_ID = "workspace.skills.always";
|
|
4
4
|
const inRoot = (root, path) => root.endsWith("/") ? `${root}${path}` : `${root}/${path}`, skillFileOf = (root, directory) => inRoot(root, directory === "" ? "SKILL.md" : `${directory}/SKILL.md`);
|
|
5
5
|
export const mergeSkillRoots = (roots, idPrefix) => {
|
|
6
6
|
const merged = roots.reduce((sofar, { root, tree }) => tree.skills.reduce((state, entry) => {
|
|
7
|
-
const skill = { ...entry.skill, id: `${idPrefix}${entry.skill.id}` }, from = skillFileOf(root, entry.directory), taken = state.skills.find((other) => other.skill.id === skill.id);
|
|
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
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
9
|
}, { ...sofar, issues: [...sofar.issues, ...tree.issues.map((found) => ({ ...found, path: inRoot(root, found.path) }))] }), { skills: [], issues: [] });
|
|
10
10
|
return { skills: merged.skills.map((item) => item.skill), issues: merged.issues };
|
|
11
|
-
}, settleSkills =
|
|
11
|
+
}, settleSkills = Effect.fn("skills.settle")(function* (found, config) {
|
|
12
12
|
if (config.strict && found.issues.length > 0)
|
|
13
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"})`), { discard: !0 });
|
|
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
15
|
if (found.skills.length > config.maxSkills)
|
|
16
16
|
return yield* Effect.fail(new HarnessError({ code: "skills.limit", message: `found ${found.skills.length} skills, more than ${config.maxSkills} (maxSkills)` }));
|
|
17
17
|
return found.skills;
|
|
@@ -29,7 +29,7 @@ export const mergeSkillRoots = (roots, idPrefix) => {
|
|
|
29
29
|
|
|
30
30
|
`));
|
|
31
31
|
}, skillsCapability = (skills, config) => {
|
|
32
|
-
const section = Option.filter(alwaysSkillsText(skills), () => config.alwaysSection).pipe(Option.map((text) => ({
|
|
32
|
+
const section = Option.filter(alwaysSkillsText(skills), () => config.alwaysSection).pipe(Option.map((text) => definePromptSection({
|
|
33
33
|
id: ALWAYS_SECTION_ID,
|
|
34
34
|
version: fingerprintOf(text),
|
|
35
35
|
tier: "static",
|
|
@@ -7,7 +7,7 @@ const label = (index, count) => count === 1 ? "oldText" : `edit ${index + 1}'s o
|
|
|
7
7
|
if (first < 0)
|
|
8
8
|
return Result.succeed(Option.none());
|
|
9
9
|
if (base.indexOf(edit.oldText, first + 1) >= 0)
|
|
10
|
-
return Result.fail(failure("
|
|
10
|
+
return Result.fail(failure("AmbiguousMatch", `${label(index, count)} appears ${Math.max(2, base.split(edit.oldText).length - 1)} times; add surrounding lines so it appears exactly once`));
|
|
11
11
|
return Result.succeed(Option.some({ index, start: first, end: first + edit.oldText.length, newText: edit.newText, normalized: !1 }));
|
|
12
12
|
}, normalizedSpan = (base, edit, index, count) => {
|
|
13
13
|
const withBreak = edit.oldText.endsWith(`
|
|
@@ -20,7 +20,7 @@ const label = (index, count) => count === 1 ? "oldText" : `edit ${index + 1}'s o
|
|
|
20
20
|
if (start === void 0)
|
|
21
21
|
return Result.succeed(Option.none());
|
|
22
22
|
if (others.length > 0)
|
|
23
|
-
return Result.fail(failure("
|
|
23
|
+
return Result.fail(failure("AmbiguousMatch", `${label(index, count)} matches ${starts.length} places once whitespace and quotes are normalised; add surrounding lines so it matches exactly once`));
|
|
24
24
|
const lineStart = (line) => lines.slice(0, line).reduce((total, text) => total + text.length + 1, 0), after = start + wanted.length, lastLine = lines[after - 1] ?? "", end = withBreak ? Math.min(base.length, lineStart(after)) : lineStart(after) - 1 - (lastLine.endsWith("\r") ? 1 : 0);
|
|
25
25
|
return Result.succeed(Option.some({ index, start: lineStart(start), end, newText: edit.newText, normalized: !0 }));
|
|
26
26
|
}, spanOf = (base, edit, index, count, fuzzy) => {
|
|
@@ -42,7 +42,7 @@ export const applyTextEdits = (original, edits, options) => {
|
|
|
42
42
|
const ordered = [...spans].sort((left, right) => left.start - right.start), clash = ordered.findIndex((span, at) => at > 0 && span.start < (ordered[at - 1]?.end ?? 0));
|
|
43
43
|
if (clash > 0) {
|
|
44
44
|
const [first, second] = [ordered[clash - 1]?.index ?? 0, ordered[clash]?.index ?? 0].sort((left, right) => left - right);
|
|
45
|
-
return Result.fail(failure("
|
|
45
|
+
return Result.fail(failure("OverlappingEdits", `edits ${(first ?? 0) + 1} and ${(second ?? 0) + 1} change overlapping text; merge them into one edit`));
|
|
46
46
|
}
|
|
47
47
|
const built = ordered.reduce((state, span) => ({ text: state.text + base.slice(state.at, span.start) + span.newText, at: span.end }), { text: "", at: 0 }), after = built.text + base.slice(built.at);
|
|
48
48
|
if (after === base)
|
|
@@ -9,6 +9,13 @@ import type { FileSearch } from "./text-search.entity.js";
|
|
|
9
9
|
* number of times (`(\d+\.){3}`) is not counted.
|
|
10
10
|
*/
|
|
11
11
|
export declare const repeatsRepetition: (source: string) => boolean;
|
|
12
|
+
/**
|
|
13
|
+
* The pattern, refused (`InvalidPattern`) when it holds a line break: `grep`
|
|
14
|
+
* matches one line at a time, so the builtin engine would find nothing and
|
|
15
|
+
* ripgrep, which reads its patterns one per line from stdin, would search
|
|
16
|
+
* for each line of it on its own. Both engines refuse it alike.
|
|
17
|
+
*/
|
|
18
|
+
export declare const singleLinePattern: (pattern: string) => Result.Result<string, Failure>;
|
|
12
19
|
/**
|
|
13
20
|
* The expression `grep` matches with: the pattern as a JavaScript regular
|
|
14
21
|
* expression, or, with `literal`, as plain text; `ignoreCase` ignores
|
|
@@ -11,10 +11,10 @@ export const repeatsRepetition = (source) => (source.match(tokenPattern) ?? []).
|
|
|
11
11
|
if (isQuantifier(token) && unbounded(token))
|
|
12
12
|
return { groups: [...scan.groups.slice(0, -1), !0], closedRepeating: !1, nested: scan.nested || scan.closedRepeating };
|
|
13
13
|
return { ...scan, closedRepeating: !1 };
|
|
14
|
-
}, { groups: [!1], closedRepeating: !1, nested: !1 }).nested, compilePattern = (pattern, options) => {
|
|
14
|
+
}, { groups: [!1], closedRepeating: !1, nested: !1 }).nested, singleLinePattern = (pattern) => /[\r\n]/.test(pattern) ? Result.fail(failure("InvalidPattern", "the pattern contains a line break, but grep matches one line at a time; search for a single line of the text instead")) : Result.succeed(pattern), compilePattern = (pattern, options) => {
|
|
15
15
|
const source = options.literal ? pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") : pattern, compiled = Result.try({
|
|
16
16
|
try: () => new RegExp(source, options.ignoreCase ? "i" : ""),
|
|
17
|
-
catch: (error) => failure("InvalidPattern", `${pattern} is not a valid regular expression
|
|
17
|
+
catch: (error) => failure("InvalidPattern", `${pattern} is not a valid regular expression; the engine says: ${error instanceof Error ? error.message : String(error)}. Pass literal: true to search for it as text`)
|
|
18
18
|
});
|
|
19
19
|
return Result.flatMap(compiled, (regex) => !options.literal && repeatsRepetition(source) ? Result.fail(failure("InvalidPattern", `${pattern} repeats a group that itself repeats (as in (a+)+), which can take exponential time on some lines; rewrite it without the nested repetition, or search with bash (grep -E, rg)`)) : Result.succeed(regex));
|
|
20
20
|
}, searchText = (text, pattern, options) => {
|
|
@@ -1,4 +1,20 @@
|
|
|
1
1
|
import { Schema } from "effect";
|
|
2
|
+
/** Whether the file tools may only read a root, or may also change it. */
|
|
3
|
+
export declare const WorkspaceAccessMode: Schema.Literals<readonly ["read", "write"]>;
|
|
4
|
+
export type WorkspaceAccessMode = typeof WorkspaceAccessMode.Type;
|
|
5
|
+
/** An extra file-tool root, absolute or relative to the session's workspace. */
|
|
6
|
+
export declare const WorkspaceAllowedRoot: Schema.Struct<{
|
|
7
|
+
readonly path: Schema.NonEmptyString;
|
|
8
|
+
readonly access: Schema.Literals<readonly ["read", "write"]>;
|
|
9
|
+
}>;
|
|
10
|
+
export type WorkspaceAllowedRoot = typeof WorkspaceAllowedRoot.Type;
|
|
11
|
+
/** An allowed root, resolved on the same file system as the workspace. */
|
|
12
|
+
export declare const AllowedRoot: Schema.Struct<{
|
|
13
|
+
readonly root: Schema.String;
|
|
14
|
+
readonly lexicalRoot: Schema.String;
|
|
15
|
+
readonly access: Schema.Literals<readonly ["read", "write"]>;
|
|
16
|
+
}>;
|
|
17
|
+
export type AllowedRoot = typeof AllowedRoot.Type;
|
|
2
18
|
/**
|
|
3
19
|
* A path a tool was given, resolved in the workspace: `path` is where the
|
|
4
20
|
* tool acts (absolute, every link on the way resolved, through the nearest
|
|
@@ -18,5 +34,10 @@ export type WorkspaceTarget = typeof WorkspaceTarget.Type;
|
|
|
18
34
|
export declare const WorkspaceRoot: Schema.Struct<{
|
|
19
35
|
readonly root: Schema.String;
|
|
20
36
|
readonly lexicalRoot: Schema.String;
|
|
37
|
+
readonly roots: Schema.$Array<Schema.Struct<{
|
|
38
|
+
readonly root: Schema.String;
|
|
39
|
+
readonly lexicalRoot: Schema.String;
|
|
40
|
+
readonly access: Schema.Literals<readonly ["read", "write"]>;
|
|
41
|
+
}>>;
|
|
21
42
|
}>;
|
|
22
43
|
export type WorkspaceRoot = typeof WorkspaceRoot.Type;
|
|
@@ -1,43 +1,73 @@
|
|
|
1
1
|
import { Effect, Option, Result } from "effect";
|
|
2
2
|
import type { Context, Path } from "effect";
|
|
3
3
|
import { FileSystem, FsError, SessionEnvironment } from "@xandreed/core";
|
|
4
|
-
import type { Failure } from "@xandreed/core";
|
|
5
|
-
import type { WorkspaceRoot, WorkspaceTarget } from "./workspace-path.entity.js";
|
|
4
|
+
import type { Failure, FsReason } from "@xandreed/core";
|
|
5
|
+
import type { WorkspaceAccessMode, WorkspaceAllowedRoot, WorkspaceRoot, WorkspaceTarget } from "./workspace-path.entity.js";
|
|
6
6
|
type Fs = Context.Service.Shape<typeof FileSystem>;
|
|
7
7
|
/** The workspace a call works in (`WorkspaceRoot`), with the file system and path functions it uses. */
|
|
8
8
|
export type WorkspaceAccess = WorkspaceRoot & {
|
|
9
9
|
readonly fs: Fs;
|
|
10
10
|
readonly path: Path.Path;
|
|
11
11
|
};
|
|
12
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* A file-system failure as the model reads it: what went wrong with the path
|
|
14
|
+
* it gave, in words, by the failure's reason. Never the backend's message,
|
|
15
|
+
* which names host paths the model never gave.
|
|
16
|
+
*/
|
|
13
17
|
export declare const fsFailure: (display: string) => (error: FsError) => Failure;
|
|
18
|
+
/** The workspace directory cannot be opened: why, in words, never the reason's name or the backend's message (it names the host's path). */
|
|
19
|
+
export declare const workspaceMissing: (reason: FsReason) => Failure;
|
|
14
20
|
/** Whether `path` is `root` or below it. */
|
|
15
21
|
export declare const isWithin: (root: string, path: string) => boolean;
|
|
16
22
|
/**
|
|
17
23
|
* Open the workspace of the call: `SessionEnvironment.workspace` made
|
|
18
24
|
* absolute (against the process's directory) and resolved by the file
|
|
19
25
|
* system; one that cannot be resolved fails with `WorkspaceMissing`.
|
|
26
|
+
* Extra roots are resolved against it for this call. An extra root that
|
|
27
|
+
* cannot be opened as a directory is logged and left out.
|
|
20
28
|
*/
|
|
21
|
-
export declare const openWorkspace: (path: Path.Path
|
|
29
|
+
export declare const openWorkspace: (path: Path.Path, options?: {
|
|
30
|
+
readonly roots: ReadonlyArray<WorkspaceAllowedRoot>;
|
|
31
|
+
readonly workspaceAccess: WorkspaceAccessMode;
|
|
32
|
+
} | undefined) => Effect.Effect<WorkspaceAccess, {
|
|
33
|
+
readonly error: string;
|
|
34
|
+
readonly message: string;
|
|
35
|
+
}, FileSystem | SessionEnvironment>;
|
|
36
|
+
/** Whether a real path is inside any root the file tools may read. */
|
|
37
|
+
export declare const withinAnyRoot: (access: WorkspaceAccess, path: string) => boolean;
|
|
22
38
|
/** How a path appears to the model: relative to the root (`.` for the root itself), or absolute outside it. */
|
|
23
39
|
export declare const displayOf: (access: WorkspaceAccess, absolute: string) => string;
|
|
24
40
|
/**
|
|
25
41
|
* Resolve a path a tool was given: relative to the workspace root, or
|
|
26
|
-
* absolute. With `confine`, a path written outside
|
|
42
|
+
* absolute. With `confine`, a path written outside every allowed root is refused
|
|
27
43
|
* before anything is read, and so is one whose real location (links
|
|
28
|
-
* resolved, through the nearest existing ancestor) is outside
|
|
29
|
-
* (`OutsideWorkspace`).
|
|
44
|
+
* resolved, through the nearest existing ancestor) is outside all roots
|
|
45
|
+
* (`OutsideWorkspace`). A write into a read root fails with `ReadOnly`;
|
|
46
|
+
* the most specific real root decides access. With confinement off these
|
|
47
|
+
* restrictions are disabled. This is a guardrail against mistakes, not a
|
|
30
48
|
* sandbox: what a path names can change between this check and its use.
|
|
31
49
|
*/
|
|
32
50
|
export declare const resolveTarget: (access: WorkspaceAccess, input: string, options: {
|
|
33
51
|
readonly confine: boolean;
|
|
34
|
-
|
|
52
|
+
readonly intent?: WorkspaceAccessMode;
|
|
53
|
+
}) => Effect.Effect<{
|
|
54
|
+
readonly path: string;
|
|
55
|
+
readonly display: string;
|
|
56
|
+
}, {
|
|
57
|
+
readonly error: string;
|
|
58
|
+
readonly message: string;
|
|
59
|
+
}, never>;
|
|
35
60
|
/** A compiled `protect` glob: the pattern, and whether a path relative to the root matches it. */
|
|
36
61
|
export interface ProtectRule {
|
|
37
62
|
readonly glob: string;
|
|
38
63
|
readonly matches: (relative: string) => boolean;
|
|
39
64
|
}
|
|
40
|
-
/**
|
|
65
|
+
/**
|
|
66
|
+
* Compile `protect` globs (dot entries included); fails with the first
|
|
67
|
+
* pattern that does not compile. A rule also matches a path that differs
|
|
68
|
+
* from it only in letter case, so a case-insensitive file system cannot
|
|
69
|
+
* reach a protected file (`.GIT/config`) through another spelling.
|
|
70
|
+
*/
|
|
41
71
|
export declare const compileProtect: (globs: ReadonlyArray<string>) => Result.Result<ReadonlyArray<ProtectRule>, string>;
|
|
42
72
|
/**
|
|
43
73
|
* The `exclude` globs as a walk from a directory must be given them, the
|