tinker-agent 1.0.65 → 1.2.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/CHANGELOG.md +36 -0
- package/LICENSE +201 -0
- package/NOTICE +6 -0
- package/README.md +90 -9
- package/bin/tinker.js +39 -0
- package/node_modules/ansi-regex/index.d.ts +33 -0
- package/node_modules/ansi-regex/index.js +14 -0
- package/node_modules/ansi-regex/license +9 -0
- package/node_modules/ansi-regex/package.json +61 -0
- package/node_modules/ansi-regex/readme.md +66 -0
- package/node_modules/ansi-styles/index.d.ts +236 -0
- package/node_modules/ansi-styles/index.js +223 -0
- package/node_modules/ansi-styles/license +9 -0
- package/node_modules/ansi-styles/package.json +54 -0
- package/node_modules/ansi-styles/readme.md +173 -0
- package/node_modules/chalk/license +9 -0
- package/node_modules/chalk/package.json +83 -0
- package/node_modules/chalk/readme.md +297 -0
- package/node_modules/chalk/source/index.d.ts +325 -0
- package/node_modules/chalk/source/index.js +225 -0
- package/node_modules/chalk/source/utilities.js +33 -0
- package/node_modules/chalk/source/vendor/ansi-styles/index.d.ts +236 -0
- package/node_modules/chalk/source/vendor/ansi-styles/index.js +223 -0
- package/node_modules/chalk/source/vendor/supports-color/browser.d.ts +1 -0
- package/node_modules/chalk/source/vendor/supports-color/browser.js +34 -0
- package/node_modules/chalk/source/vendor/supports-color/index.d.ts +55 -0
- package/node_modules/chalk/source/vendor/supports-color/index.js +190 -0
- package/node_modules/character-entities/index.d.ts +6 -0
- package/node_modules/character-entities/index.js +2132 -0
- package/node_modules/character-entities/license +22 -0
- package/node_modules/character-entities/package.json +78 -0
- package/node_modules/character-entities/readme.md +152 -0
- package/node_modules/decode-named-character-reference/index.d.ts +13 -0
- package/node_modules/decode-named-character-reference/index.d.ts.map +1 -0
- package/node_modules/decode-named-character-reference/index.dom.d.ts +6 -0
- package/node_modules/decode-named-character-reference/index.dom.d.ts.map +1 -0
- package/node_modules/decode-named-character-reference/index.dom.js +32 -0
- package/node_modules/decode-named-character-reference/index.js +19 -0
- package/node_modules/decode-named-character-reference/license +22 -0
- package/node_modules/decode-named-character-reference/package.json +90 -0
- package/node_modules/decode-named-character-reference/readme.md +136 -0
- package/node_modules/get-east-asian-width/index.d.ts +60 -0
- package/node_modules/get-east-asian-width/index.js +30 -0
- package/node_modules/get-east-asian-width/license +9 -0
- package/node_modules/get-east-asian-width/lookup-data.js +21 -0
- package/node_modules/get-east-asian-width/lookup.js +138 -0
- package/node_modules/get-east-asian-width/package.json +71 -0
- package/node_modules/get-east-asian-width/readme.md +65 -0
- package/node_modules/get-east-asian-width/utilities.js +24 -0
- package/node_modules/has-flag/index.d.ts +39 -0
- package/node_modules/has-flag/index.js +8 -0
- package/node_modules/has-flag/license +9 -0
- package/node_modules/has-flag/package.json +49 -0
- package/node_modules/has-flag/readme.md +74 -0
- package/node_modules/is-fullwidth-code-point/index.d.ts +17 -0
- package/node_modules/is-fullwidth-code-point/index.js +12 -0
- package/node_modules/is-fullwidth-code-point/license +9 -0
- package/node_modules/is-fullwidth-code-point/package.json +53 -0
- package/node_modules/is-fullwidth-code-point/readme.md +31 -0
- package/node_modules/markdansi/LICENSE +21 -0
- package/node_modules/markdansi/README.md +167 -0
- package/node_modules/markdansi/dist/ast.d.ts +99 -0
- package/node_modules/markdansi/dist/ast.js +1 -0
- package/node_modules/markdansi/dist/cli.d.ts +17 -0
- package/node_modules/markdansi/dist/cli.js +208 -0
- package/node_modules/markdansi/dist/hyperlink.d.ts +9 -0
- package/node_modules/markdansi/dist/hyperlink.js +26 -0
- package/node_modules/markdansi/dist/index.d.ts +10 -0
- package/node_modules/markdansi/dist/index.js +16 -0
- package/node_modules/markdansi/dist/parser.d.ts +2 -0
- package/node_modules/markdansi/dist/parser.js +205 -0
- package/node_modules/markdansi/dist/render.d.ts +9 -0
- package/node_modules/markdansi/dist/render.js +680 -0
- package/node_modules/markdansi/dist/stream.d.ts +39 -0
- package/node_modules/markdansi/dist/stream.js +192 -0
- package/node_modules/markdansi/dist/theme.d.ts +18 -0
- package/node_modules/markdansi/dist/theme.js +105 -0
- package/node_modules/markdansi/dist/types.d.ts +58 -0
- package/node_modules/markdansi/dist/types.js +1 -0
- package/node_modules/markdansi/dist/wrap.d.ts +10 -0
- package/node_modules/markdansi/dist/wrap.js +73 -0
- package/node_modules/markdansi/docs/spec.md +110 -0
- package/node_modules/markdansi/package.json +78 -0
- package/node_modules/markdansi/tsconfig.json +18 -0
- package/node_modules/marked/LICENSE +44 -0
- package/node_modules/marked/README.md +115 -0
- package/node_modules/marked/bin/main.js +284 -0
- package/node_modules/marked/bin/marked.js +16 -0
- package/node_modules/marked/lib/marked.d.ts +760 -0
- package/node_modules/marked/lib/marked.esm.js +77 -0
- package/node_modules/marked/lib/marked.esm.js.map +7 -0
- package/node_modules/marked/lib/marked.umd.js +79 -0
- package/node_modules/marked/lib/marked.umd.js.map +7 -0
- package/node_modules/marked/man/marked.1 +113 -0
- package/node_modules/marked/man/marked.1.md +93 -0
- package/node_modules/marked/package.json +103 -0
- package/node_modules/slice-ansi/index.d.ts +19 -0
- package/node_modules/slice-ansi/index.js +317 -0
- package/node_modules/slice-ansi/license +10 -0
- package/node_modules/slice-ansi/package.json +59 -0
- package/node_modules/slice-ansi/readme.md +55 -0
- package/node_modules/slice-ansi/tokenize-ansi.js +752 -0
- package/node_modules/string-width/index.d.ts +39 -0
- package/node_modules/string-width/index.js +203 -0
- package/node_modules/string-width/license +9 -0
- package/node_modules/string-width/package.json +65 -0
- package/node_modules/string-width/readme.md +66 -0
- package/node_modules/strip-ansi/index.d.ts +15 -0
- package/node_modules/strip-ansi/index.js +19 -0
- package/node_modules/strip-ansi/license +9 -0
- package/node_modules/strip-ansi/package.json +59 -0
- package/node_modules/strip-ansi/readme.md +37 -0
- package/node_modules/supports-color/browser.d.ts +1 -0
- package/node_modules/supports-color/browser.js +35 -0
- package/node_modules/supports-color/index.d.ts +55 -0
- package/node_modules/supports-color/index.js +202 -0
- package/node_modules/supports-color/license +9 -0
- package/node_modules/supports-color/package.json +64 -0
- package/node_modules/supports-color/readme.md +75 -0
- package/node_modules/supports-hyperlinks/browser.js +10 -0
- package/node_modules/supports-hyperlinks/index.d.ts +25 -0
- package/node_modules/supports-hyperlinks/index.js +163 -0
- package/node_modules/supports-hyperlinks/license +10 -0
- package/node_modules/supports-hyperlinks/package.json +47 -0
- package/node_modules/supports-hyperlinks/readme.md +45 -0
- package/package.json +41 -10
- package/patches/markdansi@0.3.2.patch +22 -2
- package/src/agent/context-builder.ts +24 -10
- package/src/agent/context-meter.ts +89 -5
- package/src/agent/loop.ts +107 -10
- package/src/agent/runtime-session.ts +1437 -82
- package/src/agent/session-ledger.ts +169 -29
- package/src/agent/types.ts +12 -1
- package/src/agent/user-prompt-projection.ts +62 -0
- package/src/cli/config.ts +57 -14
- package/src/cli/model-profiles.ts +124 -0
- package/src/cli/run-runner.ts +4 -1
- package/src/cli/tui-runner.tsx +68 -30
- package/src/context/compiled-context-hash.ts +1 -1
- package/src/context/compiled-context-validator.ts +92 -15
- package/src/context/context-automation-policy.ts +121 -0
- package/src/context/context-manager.ts +390 -11
- package/src/context/context-policy.ts +8 -0
- package/src/context/context-protocol-validator.ts +11 -1
- package/src/context/context-revision-compiler.ts +190 -27
- package/src/context/context-revision.ts +105 -19
- package/src/context/context-surface.ts +329 -0
- package/src/context/prefix-retirement-planner.ts +570 -0
- package/src/context/protocol-frame.ts +30 -3
- package/src/context/recall-retirement-contract.ts +27 -0
- package/src/context/swap-planner.ts +34 -10
- package/src/events/observation-text-log.ts +3 -3
- package/src/events/stdout-event-printer.ts +39 -3
- package/src/events/types.ts +172 -35
- package/src/ids/runtime-id.ts +6 -0
- package/src/ids/uuid-v7.ts +7 -0
- package/src/image/image-asset-store.ts +319 -0
- package/src/image/image-input-policy.ts +14 -0
- package/src/image/image-probe.ts +233 -0
- package/src/image/image-types.ts +262 -0
- package/src/instructions/project-instructions.ts +4 -1
- package/src/mcp/mcp-manager.ts +83 -19
- package/src/model/fake-model-client.ts +9 -1
- package/src/model/input-token-estimator.ts +25 -0
- package/src/model/model-client.ts +60 -0
- package/src/model/model-request-preflight.ts +1 -0
- package/src/model/moonshot-input-token-estimator.ts +107 -0
- package/src/model/openai-chat-mapping.ts +77 -1
- package/src/model/openai-chat-model-client.ts +389 -59
- package/src/model/openai-chat-stream.ts +272 -0
- package/src/model/prompt-prefix-hash.ts +2 -4
- package/src/model/token-estimator.ts +4 -0
- package/src/observation/observation-builder.ts +29 -0
- package/src/session/resume-projection.ts +94 -34
- package/src/session/session-catalog.ts +4 -4
- package/src/session/session-errors.ts +1 -1
- package/src/session/session-history-reader.ts +151 -4
- package/src/session/session-last-response-reader.ts +189 -0
- package/src/session/session-schema.ts +349 -35
- package/src/session/session-store.ts +3266 -255
- package/src/session/sqlite-session-ledger.ts +9 -4
- package/src/skills/skill-catalog.ts +182 -0
- package/src/skills/skill-context.ts +364 -0
- package/src/skills/skill-loader.ts +779 -0
- package/src/skills/skill-tool.ts +297 -0
- package/src/tools/edit.ts +51 -64
- package/src/tools/ensure-parent-directory.ts +6 -0
- package/src/tools/read.ts +2 -2
- package/src/tools/recall.ts +43 -40
- package/src/tools/registry.ts +24 -3
- package/src/tools/ripgrep.ts +3 -2
- package/src/tools/types.ts +43 -4
- package/src/tools/write.ts +20 -31
- package/src/tui/app.tsx +333 -96
- package/src/tui/clipboard.ts +5 -0
- package/src/tui/components/mcp-panel.tsx +28 -0
- package/src/tui/components/prompt-input.tsx +790 -257
- package/src/tui/components/skills-panel.tsx +31 -0
- package/src/tui/components/timeline.tsx +30 -1
- package/src/tui/event-store.ts +85 -2
- package/src/tui/file-mention.ts +234 -0
- package/src/tui/line-editor.ts +1 -0
- package/src/tui/project-slash-commands.ts +229 -0
- package/src/tui/prompt-draft.ts +510 -0
- package/src/tui/prompt-history.ts +294 -22
- package/src/tui/slash-commands.ts +46 -2
- package/src/tui/tui-session-controller.ts +110 -48
- package/src/tui/workspace-file-search.ts +94 -0
|
@@ -0,0 +1,779 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { lstat, open, readdir, realpath, stat } from "node:fs/promises";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { isAlias, isMap, isScalar, parseDocument, visit } from "yaml";
|
|
7
|
+
import { immutableRecord } from "../context/protocol-frame";
|
|
8
|
+
import { createSkillCatalogSnapshot } from "./skill-catalog";
|
|
9
|
+
|
|
10
|
+
export const SKILL_FILE_MAX_BYTES = 64 * 1024;
|
|
11
|
+
export const SKILL_COUNT_MAX = 128;
|
|
12
|
+
export const SKILL_CATALOG_MAX_BYTES = 64 * 1024;
|
|
13
|
+
export const SKILL_RESOURCE_MAX_DEPTH = 4;
|
|
14
|
+
export const SKILL_RESOURCE_MAX_ENTRIES = 200;
|
|
15
|
+
export const ACTIVE_SKILL_COUNT_MAX = 16;
|
|
16
|
+
export const ACTIVE_SKILL_TOTAL_BYTES_MAX = 128 * 1024;
|
|
17
|
+
|
|
18
|
+
export type SkillScope = "project" | "user";
|
|
19
|
+
|
|
20
|
+
export type LoadedSkill = {
|
|
21
|
+
readonly name: string;
|
|
22
|
+
readonly description: string;
|
|
23
|
+
readonly scope: SkillScope;
|
|
24
|
+
readonly directory: string;
|
|
25
|
+
readonly skillFilePath: string;
|
|
26
|
+
readonly content: string;
|
|
27
|
+
readonly byteLength: number;
|
|
28
|
+
readonly sha256: string;
|
|
29
|
+
readonly frontmatter: {
|
|
30
|
+
readonly license?: string;
|
|
31
|
+
readonly compatibility?: string;
|
|
32
|
+
readonly metadata?: Readonly<Record<string, string>>;
|
|
33
|
+
readonly allowedTools?: string;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type SkillShadow = {
|
|
38
|
+
readonly name: string;
|
|
39
|
+
readonly winner: SkillScope;
|
|
40
|
+
readonly loser: SkillScope;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type SkillCatalogSnapshot = {
|
|
44
|
+
readonly workspaceRoot: string;
|
|
45
|
+
readonly homeRoot: string;
|
|
46
|
+
readonly skills: ReadonlyMap<string, LoadedSkill>;
|
|
47
|
+
readonly shadowed: readonly SkillShadow[];
|
|
48
|
+
readonly manifestSha256: string;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type SkillErrorCode =
|
|
52
|
+
| "SKILL_ROOT_INVALID"
|
|
53
|
+
| "SKILL_PATH_OUTSIDE_SCOPE"
|
|
54
|
+
| "SKILL_FILE_NOT_REGULAR"
|
|
55
|
+
| "SKILL_FILE_TOO_LARGE"
|
|
56
|
+
| "SKILL_FILE_NOT_UTF8"
|
|
57
|
+
| "SKILL_FRONTMATTER_INVALID"
|
|
58
|
+
| "SKILL_FIELD_INVALID"
|
|
59
|
+
| "SKILL_NAME_MISMATCH"
|
|
60
|
+
| "SKILL_BODY_EMPTY"
|
|
61
|
+
| "SKILL_COUNT_EXCEEDED"
|
|
62
|
+
| "SKILL_CATALOG_TOO_LARGE"
|
|
63
|
+
| "SKILL_RESOURCE_INVALID"
|
|
64
|
+
| "SKILL_NOT_FOUND"
|
|
65
|
+
| "SKILL_ACTIVE_LIMIT_EXCEEDED"
|
|
66
|
+
| "SKILL_ACTIVATION_STATE_INVALID"
|
|
67
|
+
| "SKILL_DISPATCH_COMMIT_FAILED"
|
|
68
|
+
| "SKILLS_UPDATE_STALE"
|
|
69
|
+
| "SKILLS_UPDATE_VALIDATION_FAILED";
|
|
70
|
+
|
|
71
|
+
export class SkillError extends Error {
|
|
72
|
+
constructor(
|
|
73
|
+
readonly code: SkillErrorCode,
|
|
74
|
+
message: string,
|
|
75
|
+
options?: ErrorOptions,
|
|
76
|
+
) {
|
|
77
|
+
super(message, options);
|
|
78
|
+
this.name = "SkillError";
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
type ScopeRoot = {
|
|
83
|
+
scope: SkillScope;
|
|
84
|
+
trustRoot: string;
|
|
85
|
+
configuredRoot: string;
|
|
86
|
+
canonicalRoot: string;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const SKILL_FIELDS = Object.freeze([
|
|
90
|
+
"name",
|
|
91
|
+
"description",
|
|
92
|
+
"license",
|
|
93
|
+
"compatibility",
|
|
94
|
+
"metadata",
|
|
95
|
+
"allowed-tools",
|
|
96
|
+
] as const);
|
|
97
|
+
|
|
98
|
+
const SKILL_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
99
|
+
|
|
100
|
+
export async function loadSkillCatalog(input: {
|
|
101
|
+
workspaceRoot: string;
|
|
102
|
+
homeRoot?: string;
|
|
103
|
+
}): Promise<SkillCatalogSnapshot> {
|
|
104
|
+
const workspaceRoot = await canonicalDirectory(input.workspaceRoot, "workspace");
|
|
105
|
+
const homeRoot = await canonicalDirectory(input.homeRoot ?? os.homedir(), "home");
|
|
106
|
+
const configured = [
|
|
107
|
+
{
|
|
108
|
+
scope: "project" as const,
|
|
109
|
+
trustRoot: workspaceRoot,
|
|
110
|
+
root: path.join(workspaceRoot, ".agents", "skills"),
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
scope: "user" as const,
|
|
114
|
+
trustRoot: homeRoot,
|
|
115
|
+
root: path.join(homeRoot, ".agents", "skills"),
|
|
116
|
+
},
|
|
117
|
+
];
|
|
118
|
+
const roots: ScopeRoot[] = [];
|
|
119
|
+
for (const entry of configured) {
|
|
120
|
+
const root = await resolveScopeRoot(entry.scope, entry.trustRoot, entry.root);
|
|
121
|
+
if (root === undefined) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const existing = roots.find(
|
|
125
|
+
(candidate) => candidate.canonicalRoot === root.canonicalRoot,
|
|
126
|
+
);
|
|
127
|
+
if (existing !== undefined) {
|
|
128
|
+
if (entry.scope === "project") {
|
|
129
|
+
existing.scope = "project";
|
|
130
|
+
existing.trustRoot = workspaceRoot;
|
|
131
|
+
existing.configuredRoot = entry.root;
|
|
132
|
+
}
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
roots.push(root);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const candidates: LoadedSkill[] = [];
|
|
139
|
+
for (const root of roots.sort(
|
|
140
|
+
(left, right) => scopeOrder(left.scope) - scopeOrder(right.scope),
|
|
141
|
+
)) {
|
|
142
|
+
candidates.push(...(await loadScopeCandidates(root, candidates.length)));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const byName = new Map<string, LoadedSkill>();
|
|
146
|
+
const shadowed: SkillShadow[] = [];
|
|
147
|
+
for (const skill of candidates
|
|
148
|
+
.filter((candidate) => candidate.scope === "user")
|
|
149
|
+
.sort(compareSkills)) {
|
|
150
|
+
if (byName.has(skill.name)) {
|
|
151
|
+
throw new SkillError(
|
|
152
|
+
"SKILL_ACTIVATION_STATE_INVALID",
|
|
153
|
+
`Loader invariant failed: duplicate ${skill.scope} skill ${skill.name}.`,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
byName.set(skill.name, skill);
|
|
157
|
+
}
|
|
158
|
+
for (const skill of candidates
|
|
159
|
+
.filter((candidate) => candidate.scope === "project")
|
|
160
|
+
.sort(compareSkills)) {
|
|
161
|
+
const previous = byName.get(skill.name);
|
|
162
|
+
if (previous?.scope === "project") {
|
|
163
|
+
throw new SkillError(
|
|
164
|
+
"SKILL_ACTIVATION_STATE_INVALID",
|
|
165
|
+
`Loader invariant failed: duplicate project skill ${skill.name}.`,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
if (previous !== undefined) {
|
|
169
|
+
shadowed.push(
|
|
170
|
+
immutableRecord({
|
|
171
|
+
name: skill.name,
|
|
172
|
+
winner: "project" as const,
|
|
173
|
+
loser: "user" as const,
|
|
174
|
+
}),
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
byName.set(skill.name, skill);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return createSkillCatalogSnapshot({
|
|
181
|
+
workspaceRoot,
|
|
182
|
+
homeRoot,
|
|
183
|
+
skills: [...byName.values()].sort(compareSkills),
|
|
184
|
+
shadowed: shadowed.sort((left, right) => compareText(left.name, right.name)),
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function canonicalDirectory(value: string, label: string): Promise<string> {
|
|
189
|
+
if (!path.isAbsolute(value)) {
|
|
190
|
+
throw new SkillError(
|
|
191
|
+
"SKILL_ROOT_INVALID",
|
|
192
|
+
`Agent Skills ${label} root must be an absolute path: ${value}.`,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
try {
|
|
196
|
+
const canonical = await realpath(value);
|
|
197
|
+
const stats = await stat(canonical);
|
|
198
|
+
if (!stats.isDirectory()) {
|
|
199
|
+
throw new Error("not a directory");
|
|
200
|
+
}
|
|
201
|
+
return canonical;
|
|
202
|
+
} catch (error) {
|
|
203
|
+
throw new SkillError(
|
|
204
|
+
"SKILL_ROOT_INVALID",
|
|
205
|
+
`Agent Skills ${label} root is invalid: ${value}.`,
|
|
206
|
+
{ cause: error },
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function resolveScopeRoot(
|
|
212
|
+
scope: SkillScope,
|
|
213
|
+
trustRoot: string,
|
|
214
|
+
configuredRoot: string,
|
|
215
|
+
): Promise<ScopeRoot | undefined> {
|
|
216
|
+
let configuredStats;
|
|
217
|
+
try {
|
|
218
|
+
configuredStats = await lstat(configuredRoot);
|
|
219
|
+
} catch (error) {
|
|
220
|
+
if (isErrno(error, "ENOENT")) {
|
|
221
|
+
return undefined;
|
|
222
|
+
}
|
|
223
|
+
throw rootError(scope, configuredRoot, error);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
const canonicalRoot = await realpath(configuredRoot);
|
|
228
|
+
if (!isWithin(trustRoot, canonicalRoot)) {
|
|
229
|
+
throw new SkillError(
|
|
230
|
+
"SKILL_PATH_OUTSIDE_SCOPE",
|
|
231
|
+
`Agent Skills ${scope} root resolves outside its trust boundary: ${configuredRoot}.`,
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
const rootStats = await stat(canonicalRoot);
|
|
235
|
+
if (!rootStats.isDirectory()) {
|
|
236
|
+
throw new SkillError(
|
|
237
|
+
"SKILL_ROOT_INVALID",
|
|
238
|
+
`Agent Skills ${scope} root must be a directory: ${configuredRoot}.`,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
if (!configuredStats.isDirectory() && !configuredStats.isSymbolicLink()) {
|
|
242
|
+
throw new SkillError(
|
|
243
|
+
"SKILL_ROOT_INVALID",
|
|
244
|
+
`Agent Skills ${scope} root must be a directory or directory symlink: ${configuredRoot}.`,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
return { scope, trustRoot, configuredRoot, canonicalRoot };
|
|
248
|
+
} catch (error) {
|
|
249
|
+
if (error instanceof SkillError) {
|
|
250
|
+
throw error;
|
|
251
|
+
}
|
|
252
|
+
throw rootError(scope, configuredRoot, error);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function loadScopeCandidates(
|
|
257
|
+
root: ScopeRoot,
|
|
258
|
+
existingCount: number,
|
|
259
|
+
): Promise<LoadedSkill[]> {
|
|
260
|
+
let entries;
|
|
261
|
+
try {
|
|
262
|
+
entries = await readdir(root.canonicalRoot, { withFileTypes: true });
|
|
263
|
+
} catch (error) {
|
|
264
|
+
throw rootError(root.scope, root.configuredRoot, error);
|
|
265
|
+
}
|
|
266
|
+
entries.sort((left, right) => compareText(left.name, right.name));
|
|
267
|
+
|
|
268
|
+
const skills: LoadedSkill[] = [];
|
|
269
|
+
for (const entry of entries) {
|
|
270
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) {
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
const logicalDirectory = path.join(root.canonicalRoot, entry.name);
|
|
274
|
+
const canonicalSkillDirectory = await resolveSkillDirectory(
|
|
275
|
+
root,
|
|
276
|
+
logicalDirectory,
|
|
277
|
+
entry.name,
|
|
278
|
+
);
|
|
279
|
+
const logicalSkillFile = path.join(logicalDirectory, "SKILL.md");
|
|
280
|
+
let skillFileStats;
|
|
281
|
+
try {
|
|
282
|
+
skillFileStats = await lstat(logicalSkillFile);
|
|
283
|
+
} catch (error) {
|
|
284
|
+
if (isErrno(error, "ENOENT")) {
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
throw skillLoadError(root.scope, entry.name, error);
|
|
288
|
+
}
|
|
289
|
+
void skillFileStats;
|
|
290
|
+
const discoveredCount = existingCount + skills.length + 1;
|
|
291
|
+
if (discoveredCount > SKILL_COUNT_MAX) {
|
|
292
|
+
throw new SkillError(
|
|
293
|
+
"SKILL_COUNT_EXCEEDED",
|
|
294
|
+
`Discovered ${discoveredCount} Agent Skills; the limit is ${SKILL_COUNT_MAX}.`,
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
skills.push(
|
|
298
|
+
await loadSkillFile({
|
|
299
|
+
scope: root.scope,
|
|
300
|
+
directoryName: entry.name,
|
|
301
|
+
canonicalDirectory: canonicalSkillDirectory,
|
|
302
|
+
logicalSkillFile,
|
|
303
|
+
}),
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
return skills;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async function resolveSkillDirectory(
|
|
310
|
+
root: ScopeRoot,
|
|
311
|
+
logicalDirectory: string,
|
|
312
|
+
directoryName: string,
|
|
313
|
+
): Promise<string> {
|
|
314
|
+
try {
|
|
315
|
+
const canonical = await realpath(logicalDirectory);
|
|
316
|
+
if (!isWithin(root.trustRoot, canonical)) {
|
|
317
|
+
throw new SkillError(
|
|
318
|
+
"SKILL_PATH_OUTSIDE_SCOPE",
|
|
319
|
+
`Agent Skill ${root.scope}/${directoryName} resolves outside its trust boundary.`,
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
const stats = await stat(canonical);
|
|
323
|
+
if (!stats.isDirectory()) {
|
|
324
|
+
throw new SkillError(
|
|
325
|
+
"SKILL_ROOT_INVALID",
|
|
326
|
+
`Agent Skill candidate ${root.scope}/${directoryName} is not a directory.`,
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
return canonical;
|
|
330
|
+
} catch (error) {
|
|
331
|
+
if (error instanceof SkillError) {
|
|
332
|
+
throw error;
|
|
333
|
+
}
|
|
334
|
+
throw skillLoadError(root.scope, directoryName, error);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function loadSkillFile(input: {
|
|
339
|
+
scope: SkillScope;
|
|
340
|
+
directoryName: string;
|
|
341
|
+
canonicalDirectory: string;
|
|
342
|
+
logicalSkillFile: string;
|
|
343
|
+
}): Promise<LoadedSkill> {
|
|
344
|
+
let handle;
|
|
345
|
+
try {
|
|
346
|
+
handle = await open(
|
|
347
|
+
input.logicalSkillFile,
|
|
348
|
+
constants.O_RDONLY | constants.O_NONBLOCK,
|
|
349
|
+
);
|
|
350
|
+
} catch (error) {
|
|
351
|
+
throw skillLoadError(input.scope, input.directoryName, error);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
try {
|
|
355
|
+
const canonicalFile = await realpath(input.logicalSkillFile);
|
|
356
|
+
if (!isWithin(input.canonicalDirectory, canonicalFile)) {
|
|
357
|
+
throw new SkillError(
|
|
358
|
+
"SKILL_PATH_OUTSIDE_SCOPE",
|
|
359
|
+
`Agent Skill ${input.scope}/${input.directoryName}/SKILL.md resolves outside its skill directory.`,
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
const openedStats = await handle.stat();
|
|
363
|
+
if (!openedStats.isFile()) {
|
|
364
|
+
throw new SkillError(
|
|
365
|
+
"SKILL_FILE_NOT_REGULAR",
|
|
366
|
+
`Agent Skill ${input.scope}/${input.directoryName}/SKILL.md must be a regular file.`,
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
const targetStats = await stat(canonicalFile);
|
|
370
|
+
if (targetStats.dev !== openedStats.dev || targetStats.ino !== openedStats.ino) {
|
|
371
|
+
throw new SkillError(
|
|
372
|
+
"SKILL_FILE_NOT_REGULAR",
|
|
373
|
+
`Agent Skill ${input.scope}/${input.directoryName}/SKILL.md changed while being opened.`,
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
if (openedStats.size > SKILL_FILE_MAX_BYTES) {
|
|
377
|
+
throw tooLargeError(input.scope, input.directoryName, openedStats.size);
|
|
378
|
+
}
|
|
379
|
+
const bytes = await readBounded(handle, SKILL_FILE_MAX_BYTES);
|
|
380
|
+
if (bytes.byteLength > SKILL_FILE_MAX_BYTES) {
|
|
381
|
+
throw tooLargeError(input.scope, input.directoryName, bytes.byteLength);
|
|
382
|
+
}
|
|
383
|
+
const canonicalFileAfterRead = await realpath(input.logicalSkillFile);
|
|
384
|
+
const openedStatsAfterRead = await handle.stat();
|
|
385
|
+
const targetStatsAfterRead = await stat(canonicalFileAfterRead);
|
|
386
|
+
if (
|
|
387
|
+
canonicalFileAfterRead !== canonicalFile ||
|
|
388
|
+
openedStatsAfterRead.dev !== openedStats.dev ||
|
|
389
|
+
openedStatsAfterRead.ino !== openedStats.ino ||
|
|
390
|
+
openedStatsAfterRead.size !== openedStats.size ||
|
|
391
|
+
openedStatsAfterRead.mtimeMs !== openedStats.mtimeMs ||
|
|
392
|
+
openedStatsAfterRead.ctimeMs !== openedStats.ctimeMs ||
|
|
393
|
+
targetStatsAfterRead.dev !== openedStatsAfterRead.dev ||
|
|
394
|
+
targetStatsAfterRead.ino !== openedStatsAfterRead.ino
|
|
395
|
+
) {
|
|
396
|
+
throw new SkillError(
|
|
397
|
+
"SKILL_FILE_NOT_REGULAR",
|
|
398
|
+
`Agent Skill ${input.scope}/${input.directoryName}/SKILL.md changed while being read.`,
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
if (bytes.includes(0)) {
|
|
402
|
+
throw new SkillError(
|
|
403
|
+
"SKILL_FIELD_INVALID",
|
|
404
|
+
`Agent Skill ${input.scope}/${input.directoryName}/SKILL.md contains a NUL byte.`,
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
let content: string;
|
|
409
|
+
try {
|
|
410
|
+
content = new TextDecoder("utf-8", {
|
|
411
|
+
fatal: true,
|
|
412
|
+
ignoreBOM: true,
|
|
413
|
+
}).decode(bytes);
|
|
414
|
+
} catch (error) {
|
|
415
|
+
throw new SkillError(
|
|
416
|
+
"SKILL_FILE_NOT_UTF8",
|
|
417
|
+
`Agent Skill ${input.scope}/${input.directoryName}/SKILL.md is not valid UTF-8.`,
|
|
418
|
+
{ cause: error },
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
const parsed = parseSkillDocument(content, input.scope, input.directoryName);
|
|
422
|
+
return immutableRecord({
|
|
423
|
+
name: parsed.name,
|
|
424
|
+
description: parsed.description,
|
|
425
|
+
scope: input.scope,
|
|
426
|
+
directory: input.canonicalDirectory,
|
|
427
|
+
skillFilePath: canonicalFile,
|
|
428
|
+
content,
|
|
429
|
+
byteLength: bytes.byteLength,
|
|
430
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
431
|
+
frontmatter: parsed.frontmatter,
|
|
432
|
+
});
|
|
433
|
+
} catch (error) {
|
|
434
|
+
if (error instanceof SkillError) {
|
|
435
|
+
throw error;
|
|
436
|
+
}
|
|
437
|
+
throw skillLoadError(input.scope, input.directoryName, error);
|
|
438
|
+
} finally {
|
|
439
|
+
await handle.close().catch((error: unknown) => {
|
|
440
|
+
throw skillLoadError(input.scope, input.directoryName, error);
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function parseSkillDocument(
|
|
446
|
+
content: string,
|
|
447
|
+
scope: SkillScope,
|
|
448
|
+
directoryName: string,
|
|
449
|
+
): Pick<LoadedSkill, "name" | "description" | "frontmatter"> {
|
|
450
|
+
const lines = content.split(/\r?\n/);
|
|
451
|
+
if (lines[0] !== "---") {
|
|
452
|
+
throw frontmatterError(scope, directoryName, "must start with an exact --- line");
|
|
453
|
+
}
|
|
454
|
+
const closingIndex = lines.indexOf("---", 1);
|
|
455
|
+
if (closingIndex < 0) {
|
|
456
|
+
throw frontmatterError(scope, directoryName, "has no closing --- line");
|
|
457
|
+
}
|
|
458
|
+
const frontmatterText = lines.slice(1, closingIndex).join("\n");
|
|
459
|
+
const body = lines.slice(closingIndex + 1).join("\n");
|
|
460
|
+
if (body.trim() === "") {
|
|
461
|
+
throw new SkillError(
|
|
462
|
+
"SKILL_BODY_EMPTY",
|
|
463
|
+
`Agent Skill ${scope}/${directoryName}/SKILL.md must have a non-empty Markdown body.`,
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
let document: ReturnType<typeof parseDocument>;
|
|
468
|
+
try {
|
|
469
|
+
document = parseDocument(frontmatterText, {
|
|
470
|
+
schema: "core",
|
|
471
|
+
customTags: [],
|
|
472
|
+
uniqueKeys: true,
|
|
473
|
+
prettyErrors: false,
|
|
474
|
+
strict: true,
|
|
475
|
+
});
|
|
476
|
+
} catch (error) {
|
|
477
|
+
throw frontmatterError(scope, directoryName, errorMessage(error));
|
|
478
|
+
}
|
|
479
|
+
if (document.errors.length > 0 || document.warnings.length > 0) {
|
|
480
|
+
const issue = document.errors[0] ?? document.warnings[0];
|
|
481
|
+
throw frontmatterError(
|
|
482
|
+
scope,
|
|
483
|
+
directoryName,
|
|
484
|
+
issue?.message ?? "could not be parsed",
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
if (!isMap(document.contents)) {
|
|
488
|
+
throw frontmatterError(scope, directoryName, "root must be a mapping");
|
|
489
|
+
}
|
|
490
|
+
let prohibitedNode = false;
|
|
491
|
+
visit(document, {
|
|
492
|
+
Node(_key, node) {
|
|
493
|
+
const candidate = node;
|
|
494
|
+
if (
|
|
495
|
+
isAlias(node) ||
|
|
496
|
+
candidate.anchor !== undefined ||
|
|
497
|
+
(candidate.tag !== undefined && !candidate.tag.startsWith("tag:yaml.org,2002:"))
|
|
498
|
+
) {
|
|
499
|
+
prohibitedNode = true;
|
|
500
|
+
return visit.BREAK;
|
|
501
|
+
}
|
|
502
|
+
return undefined;
|
|
503
|
+
},
|
|
504
|
+
});
|
|
505
|
+
if (prohibitedNode) {
|
|
506
|
+
throw frontmatterError(
|
|
507
|
+
scope,
|
|
508
|
+
directoryName,
|
|
509
|
+
"aliases, anchors, and custom tags are not supported",
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
let value: unknown;
|
|
514
|
+
try {
|
|
515
|
+
value = document.toJS({ maxAliasCount: 0 });
|
|
516
|
+
} catch (error) {
|
|
517
|
+
throw frontmatterError(scope, directoryName, errorMessage(error));
|
|
518
|
+
}
|
|
519
|
+
if (!isPlainRecord(value)) {
|
|
520
|
+
throw frontmatterError(scope, directoryName, "root must be a plain mapping");
|
|
521
|
+
}
|
|
522
|
+
const unknown = Object.keys(value).filter(
|
|
523
|
+
(key) => !(SKILL_FIELDS as readonly string[]).includes(key),
|
|
524
|
+
);
|
|
525
|
+
if (unknown.length > 0) {
|
|
526
|
+
throw fieldError(
|
|
527
|
+
scope,
|
|
528
|
+
directoryName,
|
|
529
|
+
`contains unknown field ${JSON.stringify(unknown.sort(compareText)[0])}`,
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const name = requireStringField(value, "name", scope, directoryName);
|
|
534
|
+
const description = requireStringField(value, "description", scope, directoryName);
|
|
535
|
+
if (
|
|
536
|
+
codePointLength(name) < 1 ||
|
|
537
|
+
codePointLength(name) > 64 ||
|
|
538
|
+
!SKILL_NAME_PATTERN.test(name)
|
|
539
|
+
) {
|
|
540
|
+
throw fieldError(
|
|
541
|
+
scope,
|
|
542
|
+
directoryName,
|
|
543
|
+
"name must be 1-64 lowercase ASCII letters, digits, or single hyphens",
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
if (name !== directoryName) {
|
|
547
|
+
throw new SkillError(
|
|
548
|
+
"SKILL_NAME_MISMATCH",
|
|
549
|
+
`Agent Skill ${scope}/${directoryName} declares name ${JSON.stringify(name)}; it must match the directory name.`,
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
if (codePointLength(description) > 1_024) {
|
|
553
|
+
throw fieldError(scope, directoryName, "description exceeds 1024 characters");
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const license = optionalStringField(value, "license", scope, directoryName);
|
|
557
|
+
const compatibility = optionalStringField(
|
|
558
|
+
value,
|
|
559
|
+
"compatibility",
|
|
560
|
+
scope,
|
|
561
|
+
directoryName,
|
|
562
|
+
);
|
|
563
|
+
if (
|
|
564
|
+
compatibility !== undefined &&
|
|
565
|
+
(codePointLength(compatibility) < 1 || codePointLength(compatibility) > 500)
|
|
566
|
+
) {
|
|
567
|
+
throw fieldError(
|
|
568
|
+
scope,
|
|
569
|
+
directoryName,
|
|
570
|
+
"compatibility must contain 1-500 characters",
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
const allowedTools = optionalStringField(
|
|
574
|
+
value,
|
|
575
|
+
"allowed-tools",
|
|
576
|
+
scope,
|
|
577
|
+
directoryName,
|
|
578
|
+
);
|
|
579
|
+
const metadata = parseMetadata(
|
|
580
|
+
value.metadata,
|
|
581
|
+
document.get("metadata", true),
|
|
582
|
+
scope,
|
|
583
|
+
directoryName,
|
|
584
|
+
);
|
|
585
|
+
|
|
586
|
+
return {
|
|
587
|
+
name,
|
|
588
|
+
description,
|
|
589
|
+
frontmatter: immutableRecord({
|
|
590
|
+
...(license === undefined ? {} : { license }),
|
|
591
|
+
...(compatibility === undefined ? {} : { compatibility }),
|
|
592
|
+
...(metadata === undefined ? {} : { metadata }),
|
|
593
|
+
...(allowedTools === undefined ? {} : { allowedTools }),
|
|
594
|
+
}),
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function parseMetadata(
|
|
599
|
+
value: unknown,
|
|
600
|
+
node: unknown,
|
|
601
|
+
scope: SkillScope,
|
|
602
|
+
directoryName: string,
|
|
603
|
+
): Readonly<Record<string, string>> | undefined {
|
|
604
|
+
if (value === undefined) {
|
|
605
|
+
return undefined;
|
|
606
|
+
}
|
|
607
|
+
if (!isPlainRecord(value) || !isMap(node)) {
|
|
608
|
+
throw fieldError(scope, directoryName, "metadata must be a plain mapping");
|
|
609
|
+
}
|
|
610
|
+
for (const pair of node.items) {
|
|
611
|
+
if (!isScalar(pair.key) || typeof pair.key.value !== "string") {
|
|
612
|
+
throw fieldError(scope, directoryName, "metadata keys must be strings");
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
const entries: Array<readonly [string, string]> = [];
|
|
616
|
+
for (const key of Object.keys(value).sort(compareText)) {
|
|
617
|
+
const child = value[key];
|
|
618
|
+
if (typeof child !== "string") {
|
|
619
|
+
throw fieldError(
|
|
620
|
+
scope,
|
|
621
|
+
directoryName,
|
|
622
|
+
`metadata value for ${JSON.stringify(key)} must be a string`,
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
entries.push([key, child]);
|
|
626
|
+
}
|
|
627
|
+
return Object.freeze(Object.fromEntries(entries));
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function requireStringField(
|
|
631
|
+
value: Record<string, unknown>,
|
|
632
|
+
field: string,
|
|
633
|
+
scope: SkillScope,
|
|
634
|
+
directoryName: string,
|
|
635
|
+
): string {
|
|
636
|
+
const result = optionalStringField(value, field, scope, directoryName);
|
|
637
|
+
if (result === undefined) {
|
|
638
|
+
throw fieldError(scope, directoryName, `${field} is required`);
|
|
639
|
+
}
|
|
640
|
+
if (result.trim() === "") {
|
|
641
|
+
throw fieldError(scope, directoryName, `${field} must be a non-empty string`);
|
|
642
|
+
}
|
|
643
|
+
return result;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function optionalStringField(
|
|
647
|
+
value: Record<string, unknown>,
|
|
648
|
+
field: string,
|
|
649
|
+
scope: SkillScope,
|
|
650
|
+
directoryName: string,
|
|
651
|
+
): string | undefined {
|
|
652
|
+
const result = value[field];
|
|
653
|
+
if (result === undefined) {
|
|
654
|
+
return undefined;
|
|
655
|
+
}
|
|
656
|
+
if (typeof result !== "string") {
|
|
657
|
+
throw fieldError(scope, directoryName, `${field} must be a string`);
|
|
658
|
+
}
|
|
659
|
+
return result;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
async function readBounded(
|
|
663
|
+
handle: Awaited<ReturnType<typeof open>>,
|
|
664
|
+
maxBytes: number,
|
|
665
|
+
): Promise<Buffer> {
|
|
666
|
+
const chunks: Buffer[] = [];
|
|
667
|
+
let total = 0;
|
|
668
|
+
while (total <= maxBytes) {
|
|
669
|
+
const chunk = Buffer.allocUnsafe(Math.min(8192, maxBytes + 1 - total));
|
|
670
|
+
const { bytesRead } = await handle.read(chunk, 0, chunk.byteLength, null);
|
|
671
|
+
if (bytesRead === 0) {
|
|
672
|
+
break;
|
|
673
|
+
}
|
|
674
|
+
chunks.push(chunk.subarray(0, bytesRead));
|
|
675
|
+
total += bytesRead;
|
|
676
|
+
}
|
|
677
|
+
return Buffer.concat(chunks, total);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
681
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
682
|
+
return false;
|
|
683
|
+
}
|
|
684
|
+
return (
|
|
685
|
+
Object.getPrototypeOf(value) === Object.prototype ||
|
|
686
|
+
Object.getPrototypeOf(value) === null
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function codePointLength(value: string): number {
|
|
691
|
+
return [...value].length;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function isWithin(root: string, candidate: string): boolean {
|
|
695
|
+
const relative = path.relative(root, candidate);
|
|
696
|
+
return (
|
|
697
|
+
relative !== ".." &&
|
|
698
|
+
!relative.startsWith(`..${path.sep}`) &&
|
|
699
|
+
!path.isAbsolute(relative)
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function compareSkills(left: LoadedSkill, right: LoadedSkill): number {
|
|
704
|
+
return compareText(left.name, right.name);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function compareText(left: string, right: string): number {
|
|
708
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function scopeOrder(scope: SkillScope): number {
|
|
712
|
+
return scope === "user" ? 0 : 1;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function tooLargeError(
|
|
716
|
+
scope: SkillScope,
|
|
717
|
+
directoryName: string,
|
|
718
|
+
actual: number,
|
|
719
|
+
): SkillError {
|
|
720
|
+
return new SkillError(
|
|
721
|
+
"SKILL_FILE_TOO_LARGE",
|
|
722
|
+
`Agent Skill ${scope}/${directoryName}/SKILL.md is ${actual} bytes; the limit is ${SKILL_FILE_MAX_BYTES} bytes.`,
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function frontmatterError(
|
|
727
|
+
scope: SkillScope,
|
|
728
|
+
directoryName: string,
|
|
729
|
+
detail: string,
|
|
730
|
+
): SkillError {
|
|
731
|
+
return new SkillError(
|
|
732
|
+
"SKILL_FRONTMATTER_INVALID",
|
|
733
|
+
`Agent Skill ${scope}/${directoryName}/SKILL.md frontmatter ${detail}.`,
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function fieldError(
|
|
738
|
+
scope: SkillScope,
|
|
739
|
+
directoryName: string,
|
|
740
|
+
detail: string,
|
|
741
|
+
): SkillError {
|
|
742
|
+
return new SkillError(
|
|
743
|
+
"SKILL_FIELD_INVALID",
|
|
744
|
+
`Agent Skill ${scope}/${directoryName}/SKILL.md ${detail}.`,
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function rootError(scope: SkillScope, root: string, cause: unknown): SkillError {
|
|
749
|
+
return new SkillError(
|
|
750
|
+
"SKILL_ROOT_INVALID",
|
|
751
|
+
`Failed to load Agent Skills ${scope} root ${root}: ${errorMessage(cause)}.`,
|
|
752
|
+
{ cause },
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function skillLoadError(
|
|
757
|
+
scope: SkillScope,
|
|
758
|
+
directoryName: string,
|
|
759
|
+
cause: unknown,
|
|
760
|
+
): SkillError {
|
|
761
|
+
return new SkillError(
|
|
762
|
+
"SKILL_FILE_NOT_REGULAR",
|
|
763
|
+
`Failed to load Agent Skill ${scope}/${directoryName}: ${errorMessage(cause)}.`,
|
|
764
|
+
{ cause },
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function isErrno(error: unknown, code: string): boolean {
|
|
769
|
+
return (
|
|
770
|
+
typeof error === "object" &&
|
|
771
|
+
error !== null &&
|
|
772
|
+
"code" in error &&
|
|
773
|
+
error.code === code
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
function errorMessage(error: unknown): string {
|
|
778
|
+
return error instanceof Error ? error.message : String(error);
|
|
779
|
+
}
|