kibi-cli 0.10.1 → 0.11.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/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +45 -0
- package/dist/commands/skills.d.ts +14 -0
- package/dist/commands/skills.d.ts.map +1 -0
- package/dist/commands/skills.js +86 -0
- package/dist/commands/usage-metrics.d.ts +8 -0
- package/dist/commands/usage-metrics.d.ts.map +1 -0
- package/dist/commands/usage-metrics.js +323 -0
- package/dist/public/skills/kibi-usage/SKILL.md +125 -0
- package/dist/public/skills/kibi-usage/resources/fact-lanes.md +45 -0
- package/dist/public/skills/kibi-usage/resources/relationship-directions.md +89 -0
- package/dist/public/skills/kibi-usage/resources/workflows.md +35 -0
- package/dist/public/skills.d.ts +42 -0
- package/dist/public/skills.d.ts.map +1 -0
- package/dist/public/skills.js +262 -0
- package/package.json +6 -2
- package/src/public/skills/kibi-usage/SKILL.md +125 -0
- package/src/public/skills/kibi-usage/resources/fact-lanes.md +45 -0
- package/src/public/skills/kibi-usage/resources/relationship-directions.md +89 -0
- package/src/public/skills/kibi-usage/resources/workflows.md +35 -0
- package/src/public/skills.ts +359 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Relationship Directions
|
|
2
|
+
|
|
3
|
+
## Direction Table
|
|
4
|
+
|
|
5
|
+
| Relationship | Source -> Target | Semantic Meaning |
|
|
6
|
+
|-------------|------------------|------------------|
|
|
7
|
+
| `implements` | symbol -> req | Production code symbol owns or implements requirement behavior |
|
|
8
|
+
| `specified_by` | req -> scenario | Requirement is specified by a BDD scenario |
|
|
9
|
+
| `verified_by` | req/scenario -> test | Requirement or scenario is verified by a test case |
|
|
10
|
+
| `validates` | test -> req/scenario | Test validates a requirement or scenario (inverse of verified_by) |
|
|
11
|
+
| `executable_for` | symbol -> test | Test symbol (code) is executable code for a test entity |
|
|
12
|
+
| `constrains` | req -> fact(subject) | Requirement constrains a strict-lane domain fact |
|
|
13
|
+
| `requires_property` | req -> fact(property_value) | Requirement requires a specific property value fact |
|
|
14
|
+
| `supersedes` | old-req -> new-req | Old requirement is formally replaced by a new requirement |
|
|
15
|
+
| `covered_by` | symbol -> test | Production symbol has test coverage evidence |
|
|
16
|
+
|
|
17
|
+
## Valid Payload Examples
|
|
18
|
+
|
|
19
|
+
### implements
|
|
20
|
+
```yaml
|
|
21
|
+
relationships:
|
|
22
|
+
- type: implements
|
|
23
|
+
from: SYM-001
|
|
24
|
+
to: REQ-001
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### specified_by
|
|
28
|
+
```yaml
|
|
29
|
+
relationships:
|
|
30
|
+
- type: specified_by
|
|
31
|
+
from: REQ-001
|
|
32
|
+
to: SCEN-001
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### verified_by
|
|
36
|
+
```yaml
|
|
37
|
+
relationships:
|
|
38
|
+
- type: verified_by
|
|
39
|
+
from: REQ-001
|
|
40
|
+
to: TEST-001
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### validates
|
|
44
|
+
```yaml
|
|
45
|
+
relationships:
|
|
46
|
+
- type: validates
|
|
47
|
+
from: TEST-001
|
|
48
|
+
to: SCEN-001
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### executable_for
|
|
52
|
+
```yaml
|
|
53
|
+
relationships:
|
|
54
|
+
- type: executable_for
|
|
55
|
+
from: SYM-test-login
|
|
56
|
+
to: TEST-001
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### constrains
|
|
60
|
+
```yaml
|
|
61
|
+
relationships:
|
|
62
|
+
- type: constrains
|
|
63
|
+
from: REQ-019
|
|
64
|
+
to: FACT-USER-ROLE
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### requires_property
|
|
68
|
+
```yaml
|
|
69
|
+
relationships:
|
|
70
|
+
- type: requires_property
|
|
71
|
+
from: REQ-019
|
|
72
|
+
to: FACT-LIMIT-3
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### supersedes
|
|
76
|
+
```yaml
|
|
77
|
+
relationships:
|
|
78
|
+
- type: supersedes
|
|
79
|
+
from: REQ-001
|
|
80
|
+
to: REQ-001-v2
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### covered_by
|
|
84
|
+
```yaml
|
|
85
|
+
relationships:
|
|
86
|
+
- type: covered_by
|
|
87
|
+
from: SYM-handler
|
|
88
|
+
to: TEST-005
|
|
89
|
+
```
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Workflows
|
|
2
|
+
|
|
3
|
+
## Discovery to Validation Sequence
|
|
4
|
+
|
|
5
|
+
The canonical workflow for any KB operation follows this pattern:
|
|
6
|
+
|
|
7
|
+
1. **Discover**: `kb_search` with focused probes
|
|
8
|
+
2. **Confirm**: `kb_query` for exact IDs and state
|
|
9
|
+
3. **Inspect**: `kb_status` when freshness matters
|
|
10
|
+
4. **Create endpoints**: `kb_upsert` for new entities (sequential)
|
|
11
|
+
5. **Link**: `kb_upsert` with relationship rows (sequential)
|
|
12
|
+
6. **Validate**: `kb_check` with targeted rules during work, full check at completion
|
|
13
|
+
|
|
14
|
+
## Creating a New Feature
|
|
15
|
+
```
|
|
16
|
+
1. kb_search to discover existing requirements and related knowledge
|
|
17
|
+
2. kb_query to confirm exact IDs and source-linked context
|
|
18
|
+
3. kb_upsert for new or updated requirements (include relationship rows)
|
|
19
|
+
4. kb_check with targeted rules
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Fixing a Traceability Gap
|
|
23
|
+
```
|
|
24
|
+
1. kb_query --sourceFile <code-file> to find linked entities
|
|
25
|
+
2. kb_find_gaps --type req --missing-rel specified_by to find orphan requirements
|
|
26
|
+
3. kb_upsert to add missing relationship rows (sequential)
|
|
27
|
+
4. kb_check with rules: ["no-dangling-refs", "symbol-traceability"]
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Before Risky Work
|
|
31
|
+
```
|
|
32
|
+
1. /brief-kibi or kb_briefing_generate for citation-backed briefing
|
|
33
|
+
2. Inspect briefingState; proceed only if ready
|
|
34
|
+
3. Use constraints, regressionRisks, and cited entities from the briefing
|
|
35
|
+
```
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
readFileSync,
|
|
4
|
+
readdirSync,
|
|
5
|
+
realpathSync,
|
|
6
|
+
statSync,
|
|
7
|
+
} from "node:fs";
|
|
8
|
+
import {
|
|
9
|
+
dirname,
|
|
10
|
+
isAbsolute,
|
|
11
|
+
join,
|
|
12
|
+
normalize,
|
|
13
|
+
relative,
|
|
14
|
+
resolve,
|
|
15
|
+
} from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
import matter from "gray-matter";
|
|
18
|
+
|
|
19
|
+
const SKILL_MARKDOWN_MAX_BYTES = 256 * 1024;
|
|
20
|
+
const RESOURCE_MAX_BYTES = 128 * 1024;
|
|
21
|
+
const SKILL_FILE_NAME = "SKILL.md";
|
|
22
|
+
|
|
23
|
+
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
24
|
+
let bundledSkillsDir = resolve(moduleDir, "skills");
|
|
25
|
+
const defaultBundledSkillsDir = bundledSkillsDir;
|
|
26
|
+
|
|
27
|
+
export function setBundledSkillsDir(dir: string): void {
|
|
28
|
+
bundledSkillsDir = dir;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function resetBundledSkillsDir(): void {
|
|
32
|
+
bundledSkillsDir = defaultBundledSkillsDir;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface SkillManifest {
|
|
36
|
+
id: string;
|
|
37
|
+
name: string;
|
|
38
|
+
description: string;
|
|
39
|
+
version: string;
|
|
40
|
+
kibiCompatibility: string;
|
|
41
|
+
tags?: string[];
|
|
42
|
+
resources?: string[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface SkillBundle {
|
|
46
|
+
manifest: SkillManifest;
|
|
47
|
+
body: string;
|
|
48
|
+
rootDir: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export class SkillNotFoundError extends Error {
|
|
52
|
+
constructor(id: string) {
|
|
53
|
+
super(`Skill not found: ${id}`);
|
|
54
|
+
this.name = "SkillNotFoundError";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export class SkillResourceNotFoundError extends Error {
|
|
59
|
+
constructor(id: string, resourcePath: string) {
|
|
60
|
+
super(`Skill resource not found: ${id}/${resourcePath}`);
|
|
61
|
+
this.name = "SkillResourceNotFoundError";
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export class SkillResourceOutOfBoundsError extends Error {
|
|
66
|
+
constructor(id: string, resourcePath: string) {
|
|
67
|
+
super(`Skill resource escapes bundle root: ${id}/${resourcePath}`);
|
|
68
|
+
this.name = "SkillResourceOutOfBoundsError";
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export class SkillValidationError extends Error {
|
|
73
|
+
readonly field: string;
|
|
74
|
+
|
|
75
|
+
constructor(field: string, message: string) {
|
|
76
|
+
super(message);
|
|
77
|
+
this.name = "SkillValidationError";
|
|
78
|
+
this.field = field;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export class SkillOversizeError extends Error {
|
|
83
|
+
readonly maxBytes: number;
|
|
84
|
+
readonly actualBytes: number;
|
|
85
|
+
|
|
86
|
+
constructor(pathLike: string, maxBytes: number, actualBytes: number) {
|
|
87
|
+
super(`Skill file exceeds ${maxBytes} bytes: ${pathLike} (${actualBytes} bytes)`);
|
|
88
|
+
this.name = "SkillOversizeError";
|
|
89
|
+
this.maxBytes = maxBytes;
|
|
90
|
+
this.actualBytes = actualBytes;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function listBundledSkills(): SkillManifest[] { // implements REQ-001
|
|
95
|
+
if (!existsSync(bundledSkillsDir)) {
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return readdirSync(bundledSkillsDir, { withFileTypes: true })
|
|
100
|
+
.filter((entry) => entry.isDirectory())
|
|
101
|
+
.map((entry) => join(bundledSkillsDir, entry.name))
|
|
102
|
+
.filter((rootDir) => existsSync(join(rootDir, SKILL_FILE_NAME)))
|
|
103
|
+
.map((rootDir) => parseSkillBundle(rootDir).manifest)
|
|
104
|
+
.sort((left, right) => left.id.localeCompare(right.id));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function loadBundledSkill(id: string): SkillBundle { // implements REQ-001
|
|
108
|
+
const rootDir = findBundledSkillRoot(id);
|
|
109
|
+
if (!rootDir) {
|
|
110
|
+
throw new SkillNotFoundError(id);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return parseSkillBundle(rootDir);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function readBundledSkillResource(
|
|
117
|
+
id: string,
|
|
118
|
+
resourcePath: string,
|
|
119
|
+
): string { // implements REQ-001
|
|
120
|
+
const bundle = loadBundledSkill(id);
|
|
121
|
+
const declaredResource = normalizeResourcePath(resourcePath);
|
|
122
|
+
|
|
123
|
+
if (!declaredResource || isPathOutOfBounds(resourcePath)) {
|
|
124
|
+
throw new SkillResourceOutOfBoundsError(id, resourcePath);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!isDeclaredResource(bundle.manifest, declaredResource)) {
|
|
128
|
+
throw new SkillResourceNotFoundError(id, resourcePath);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const candidatePath = resolve(bundle.rootDir, declaredResource);
|
|
132
|
+
let realResourcePath: string;
|
|
133
|
+
let realRootDir: string;
|
|
134
|
+
try {
|
|
135
|
+
realResourcePath = realpathSync(candidatePath);
|
|
136
|
+
realRootDir = realpathSync(bundle.rootDir);
|
|
137
|
+
} catch {
|
|
138
|
+
throw new SkillResourceNotFoundError(id, resourcePath);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (!isWithinRoot(realRootDir, realResourcePath)) {
|
|
142
|
+
throw new SkillResourceOutOfBoundsError(id, resourcePath);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
assertMaxBytes(candidatePath, RESOURCE_MAX_BYTES);
|
|
146
|
+
return readFileSync(candidatePath, "utf8");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function validateSkillBundle(
|
|
150
|
+
pathLike: string,
|
|
151
|
+
): { valid: boolean; errors: SkillValidationError[] } { // implements REQ-001
|
|
152
|
+
const skillFilePath = resolveSkillFilePath(pathLike);
|
|
153
|
+
const errors: SkillValidationError[] = [];
|
|
154
|
+
|
|
155
|
+
if (!existsSync(skillFilePath)) {
|
|
156
|
+
errors.push(new SkillValidationError("SKILL.md", `Missing ${SKILL_FILE_NAME}`));
|
|
157
|
+
return { valid: false, errors };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const parsed = matter(readFileSync(skillFilePath, "utf8"));
|
|
161
|
+
errors.push(...validateManifestData(parsed.data));
|
|
162
|
+
|
|
163
|
+
if (errors.length === 0) {
|
|
164
|
+
validateBundleContents(skillFilePath, coerceManifest(parsed.data), errors);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return { valid: errors.length === 0, errors };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function validateBundleContents(
|
|
171
|
+
skillFilePath: string,
|
|
172
|
+
manifest: SkillManifest,
|
|
173
|
+
errors: SkillValidationError[],
|
|
174
|
+
): void {
|
|
175
|
+
try {
|
|
176
|
+
assertMaxBytes(skillFilePath, SKILL_MARKDOWN_MAX_BYTES);
|
|
177
|
+
} catch (error) {
|
|
178
|
+
errors.push(new SkillValidationError("SKILL.md", error instanceof Error ? error.message : String(error)));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const rootDir = dirname(skillFilePath);
|
|
182
|
+
let realRootDir: string;
|
|
183
|
+
try {
|
|
184
|
+
realRootDir = realpathSync(rootDir);
|
|
185
|
+
} catch (error) {
|
|
186
|
+
errors.push(new SkillValidationError("SKILL.md", error instanceof Error ? error.message : String(error)));
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
for (const resource of manifest.resources ?? []) {
|
|
191
|
+
const resourcePath = resolve(rootDir, resource);
|
|
192
|
+
try {
|
|
193
|
+
if (!existsSync(resourcePath)) {
|
|
194
|
+
errors.push(new SkillValidationError("resources", `Missing skill resource: ${resource}`));
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const realResourcePath = realpathSync(resourcePath);
|
|
199
|
+
if (!isWithinRoot(realRootDir, realResourcePath)) {
|
|
200
|
+
errors.push(new SkillValidationError("resources", `Skill resource escapes bundle root: ${resource}`));
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
assertMaxBytes(resourcePath, RESOURCE_MAX_BYTES);
|
|
205
|
+
} catch (error) {
|
|
206
|
+
errors.push(new SkillValidationError("resources", error instanceof Error ? error.message : String(error)));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function findBundledSkillRoot(id: string): string | undefined {
|
|
212
|
+
if (!existsSync(bundledSkillsDir)) {
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
for (const entry of readdirSync(bundledSkillsDir, { withFileTypes: true })) {
|
|
217
|
+
if (!entry.isDirectory()) {
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const rootDir = join(bundledSkillsDir, entry.name);
|
|
222
|
+
const skillFilePath = join(rootDir, SKILL_FILE_NAME);
|
|
223
|
+
if (!existsSync(skillFilePath)) {
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const manifest = parseSkillBundle(rootDir).manifest;
|
|
228
|
+
if (manifest.id === id) {
|
|
229
|
+
return rootDir;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return undefined;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function parseSkillBundle(rootDir: string): SkillBundle {
|
|
237
|
+
const skillFilePath = join(rootDir, SKILL_FILE_NAME);
|
|
238
|
+
assertMaxBytes(skillFilePath, SKILL_MARKDOWN_MAX_BYTES);
|
|
239
|
+
|
|
240
|
+
const parsed = matter(readFileSync(skillFilePath, "utf8"));
|
|
241
|
+
const errors = validateManifestData(parsed.data);
|
|
242
|
+
if (errors.length > 0) {
|
|
243
|
+
throw errors[0] as SkillValidationError;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
manifest: coerceManifest(parsed.data),
|
|
248
|
+
body: parsed.content,
|
|
249
|
+
rootDir: resolve(rootDir),
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function validateManifestData(data: Record<string, unknown>): SkillValidationError[] {
|
|
254
|
+
const errors: SkillValidationError[] = [];
|
|
255
|
+
const requiredFields = [
|
|
256
|
+
"id",
|
|
257
|
+
"name",
|
|
258
|
+
"description",
|
|
259
|
+
"version",
|
|
260
|
+
"kibiCompatibility",
|
|
261
|
+
] as const;
|
|
262
|
+
|
|
263
|
+
for (const field of requiredFields) {
|
|
264
|
+
if (typeof data[field] !== "string" || data[field].trim() === "") {
|
|
265
|
+
errors.push(new SkillValidationError(field, `Missing required skill field: ${field}`));
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (typeof data.version === "string" && !/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(data.version)) {
|
|
270
|
+
errors.push(new SkillValidationError("version", `Invalid skill version: ${data.version}`));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (data.tags !== undefined && !isStringArray(data.tags)) {
|
|
274
|
+
errors.push(new SkillValidationError("tags", "Skill tags must be strings"));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (data.resources !== undefined) {
|
|
278
|
+
if (!isStringArray(data.resources)) {
|
|
279
|
+
errors.push(new SkillValidationError("resources", "Skill resources must be strings"));
|
|
280
|
+
} else {
|
|
281
|
+
for (const resource of data.resources) {
|
|
282
|
+
const normalized = normalizeResourcePath(resource);
|
|
283
|
+
if (!normalized || isPathOutOfBounds(resource)) {
|
|
284
|
+
errors.push(
|
|
285
|
+
new SkillValidationError("resources", `Invalid skill resource: ${resource}`),
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return errors;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function coerceManifest(data: Record<string, unknown>): SkillManifest {
|
|
296
|
+
const manifest: SkillManifest = {
|
|
297
|
+
id: String(data.id),
|
|
298
|
+
name: String(data.name),
|
|
299
|
+
description: String(data.description),
|
|
300
|
+
version: String(data.version),
|
|
301
|
+
kibiCompatibility: String(data.kibiCompatibility),
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
if (isStringArray(data.tags)) {
|
|
305
|
+
manifest.tags = data.tags;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (isStringArray(data.resources)) {
|
|
309
|
+
manifest.resources = data.resources.map((resource) => normalizeResourcePath(resource));
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return manifest;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function isStringArray(value: unknown): value is string[] {
|
|
316
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function normalizeResourcePath(pathLike: string): string {
|
|
320
|
+
return normalize(pathLike).replaceAll("\\", "/");
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function isPathOutOfBounds(pathLike: string): boolean {
|
|
324
|
+
const normalized = normalizeResourcePath(pathLike);
|
|
325
|
+
return (
|
|
326
|
+
isAbsolute(pathLike) ||
|
|
327
|
+
normalized === ".." ||
|
|
328
|
+
normalized.startsWith("../") ||
|
|
329
|
+
normalized.includes("/../")
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function isDeclaredResource(manifest: SkillManifest, resourcePath: string): boolean {
|
|
334
|
+
return (manifest.resources ?? []).some(
|
|
335
|
+
(declared) => normalizeResourcePath(declared) === resourcePath,
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function isWithinRoot(rootDir: string, candidatePath: string): boolean {
|
|
340
|
+
const relativePath = relative(rootDir, candidatePath);
|
|
341
|
+
return relativePath === "" || (!relativePath.startsWith("..") && !isAbsolute(relativePath));
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function assertMaxBytes(pathLike: string, maxBytes: number): void {
|
|
345
|
+
const size = statSync(pathLike).size;
|
|
346
|
+
if (size > maxBytes) {
|
|
347
|
+
throw new SkillOversizeError(pathLike, maxBytes, size);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function resolveSkillFilePath(pathLike: string): string {
|
|
352
|
+
const resolved = resolve(pathLike);
|
|
353
|
+
if (existsSync(resolved) && statSync(resolved).isDirectory()) {
|
|
354
|
+
return join(resolved, SKILL_FILE_NAME);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return resolved.endsWith(SKILL_FILE_NAME) ? resolved : join(resolved, SKILL_FILE_NAME);
|
|
358
|
+
}
|
|
359
|
+
|