@rynx-ai/skills 0.1.10 → 0.1.11-beta.10
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/artifact.d.ts +2 -0
- package/dist/artifact.js +5 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/local-discovery-worker.d.ts +1 -0
- package/dist/local-discovery-worker.js +16 -0
- package/dist/local-discovery.d.ts +54 -0
- package/dist/local-discovery.js +417 -0
- package/package.json +2 -2
package/dist/artifact.d.ts
CHANGED
|
@@ -25,6 +25,8 @@ export interface SkillReceipt {
|
|
|
25
25
|
}
|
|
26
26
|
/** Receipt file written next to a skill at install time. */
|
|
27
27
|
export declare const SKILL_RECEIPT_FILE = ".rynx-skill.json";
|
|
28
|
+
/** A skill name is also used as a filesystem entry by provider adapters. */
|
|
29
|
+
export declare const SAFE_SKILL_NAME: RegExp;
|
|
28
30
|
/** The raw YAML frontmatter block of a `SKILL.md` (between the leading `---`s), or null. */
|
|
29
31
|
export declare function extractSkillFrontmatter(content: string): string | null;
|
|
30
32
|
/** Parse a `SKILL.md` body's leading YAML frontmatter for name + description. */
|
package/dist/artifact.js
CHANGED
|
@@ -11,6 +11,8 @@ import { readFile, readdir } from "node:fs/promises";
|
|
|
11
11
|
import path from "node:path";
|
|
12
12
|
/** Receipt file written next to a skill at install time. */
|
|
13
13
|
export const SKILL_RECEIPT_FILE = ".rynx-skill.json";
|
|
14
|
+
/** A skill name is also used as a filesystem entry by provider adapters. */
|
|
15
|
+
export const SAFE_SKILL_NAME = /^[a-z0-9][a-z0-9-]{0,127}$/;
|
|
14
16
|
/** The raw YAML frontmatter block of a `SKILL.md` (between the leading `---`s), or null. */
|
|
15
17
|
export function extractSkillFrontmatter(content) {
|
|
16
18
|
const match = content.match(/^---\s*\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n|$)/);
|
|
@@ -69,6 +71,8 @@ export async function readSkillMeta(skillDir) {
|
|
|
69
71
|
const fm = parseSkillFrontmatter(content);
|
|
70
72
|
if (!fm)
|
|
71
73
|
return null;
|
|
74
|
+
if (!SAFE_SKILL_NAME.test(fm.name) || fm.name !== path.basename(skillDir))
|
|
75
|
+
return null;
|
|
72
76
|
const ref = await readReceiptRef(skillDir);
|
|
73
77
|
return { ...fm, dir: skillDir, ...(ref ? { ref } : {}) };
|
|
74
78
|
}
|
|
@@ -104,7 +108,7 @@ export function parseSkillRefValue(value) {
|
|
|
104
108
|
if (typeof value !== "object" || value === null)
|
|
105
109
|
return null;
|
|
106
110
|
const v = value;
|
|
107
|
-
if (typeof v.name !== "string" || !v.name
|
|
111
|
+
if (typeof v.name !== "string" || !SAFE_SKILL_NAME.test(v.name))
|
|
108
112
|
return null;
|
|
109
113
|
const install = v.install;
|
|
110
114
|
if (typeof install !== "object" || install === null)
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { parentPort, workerData } from "node:worker_threads";
|
|
2
|
+
import { executeLocalSkillWorkerRequest } from "./local-discovery.js";
|
|
3
|
+
if (!parentPort)
|
|
4
|
+
throw new Error("Local Skill worker requires a parent port");
|
|
5
|
+
try {
|
|
6
|
+
parentPort.postMessage({
|
|
7
|
+
ok: true,
|
|
8
|
+
value: executeLocalSkillWorkerRequest(workerData, process.env),
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
parentPort.postMessage({
|
|
13
|
+
ok: false,
|
|
14
|
+
error: error instanceof Error ? error.message : String(error),
|
|
15
|
+
});
|
|
16
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export type LocalSkillRuntime = "codex" | "traex" | "claude";
|
|
2
|
+
export type LocalSkillRootKind = "provider" | "universal";
|
|
3
|
+
export interface LocalSkillCandidate {
|
|
4
|
+
key: string;
|
|
5
|
+
name: string;
|
|
6
|
+
description: string;
|
|
7
|
+
runtime: LocalSkillRuntime;
|
|
8
|
+
sourcePath: string;
|
|
9
|
+
root: LocalSkillRootKind;
|
|
10
|
+
fileCount: number;
|
|
11
|
+
sizeBytes: number;
|
|
12
|
+
}
|
|
13
|
+
export interface LocalSkillBundleFile {
|
|
14
|
+
path: string;
|
|
15
|
+
contentBase64: string;
|
|
16
|
+
sizeBytes: number;
|
|
17
|
+
sha256: string;
|
|
18
|
+
}
|
|
19
|
+
export interface LocalSkillBundle {
|
|
20
|
+
name: string;
|
|
21
|
+
description: string;
|
|
22
|
+
sourcePath: string;
|
|
23
|
+
contentHash: string;
|
|
24
|
+
files: LocalSkillBundleFile[];
|
|
25
|
+
}
|
|
26
|
+
export interface LocalSkillBundleFailure {
|
|
27
|
+
key: string;
|
|
28
|
+
name: string;
|
|
29
|
+
sourcePath?: string;
|
|
30
|
+
message: string;
|
|
31
|
+
}
|
|
32
|
+
export interface LocalSkillBundleCollection {
|
|
33
|
+
bundles: LocalSkillBundle[];
|
|
34
|
+
failures: LocalSkillBundleFailure[];
|
|
35
|
+
}
|
|
36
|
+
type WorkerRequest = {
|
|
37
|
+
kind: "scan";
|
|
38
|
+
runtime: LocalSkillRuntime;
|
|
39
|
+
} | {
|
|
40
|
+
kind: "collect";
|
|
41
|
+
runtime: LocalSkillRuntime;
|
|
42
|
+
keys: string[];
|
|
43
|
+
};
|
|
44
|
+
/** Scan provider-specific and universal Skill roots on this machine. */
|
|
45
|
+
export declare function scanLocalSkillCandidates(runtime: LocalSkillRuntime, env?: NodeJS.ProcessEnv): LocalSkillCandidate[];
|
|
46
|
+
/** Revalidate opaque scan keys and collect complete immutable payloads. */
|
|
47
|
+
export declare function collectLocalSkillBundles(runtime: LocalSkillRuntime, keys: string[], env?: NodeJS.ProcessEnv): LocalSkillBundleCollection;
|
|
48
|
+
/** Collect all valid Skill bundles below an already trusted local directory
|
|
49
|
+
* (for example, a safely extracted ZIP staging directory). */
|
|
50
|
+
export declare function collectSkillBundlesFromDirectory(root: string): LocalSkillBundleCollection;
|
|
51
|
+
export declare function scanLocalSkillCandidatesInWorker(runtime: LocalSkillRuntime, env?: NodeJS.ProcessEnv): Promise<LocalSkillCandidate[]>;
|
|
52
|
+
export declare function collectLocalSkillBundlesInWorker(runtime: LocalSkillRuntime, keys: string[], env?: NodeJS.ProcessEnv): Promise<LocalSkillBundleCollection>;
|
|
53
|
+
export declare function executeLocalSkillWorkerRequest(request: WorkerRequest, env: NodeJS.ProcessEnv): LocalSkillCandidate[] | LocalSkillBundleCollection;
|
|
54
|
+
export {};
|
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, readFileSync, realpathSync, readdirSync, statSync, } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { Worker } from "node:worker_threads";
|
|
7
|
+
import { parseSkillFrontmatter, SAFE_SKILL_NAME } from "./artifact.js";
|
|
8
|
+
const MAX_SKILL_FILE_BYTES = 512 * 1024;
|
|
9
|
+
const MAX_SKILL_BUNDLE_BYTES = 8 * 1024 * 1024;
|
|
10
|
+
const MAX_SKILL_IMPORT_BYTES = 24 * 1024 * 1024;
|
|
11
|
+
const MAX_SKILL_FILES = 300;
|
|
12
|
+
const MAX_DISCOVERY_DEPTH = 4;
|
|
13
|
+
const MAX_DISCOVERY_DIRS = 2_000;
|
|
14
|
+
const IGNORED_DIRS = new Set([
|
|
15
|
+
".git",
|
|
16
|
+
".hg",
|
|
17
|
+
".svn",
|
|
18
|
+
".cache",
|
|
19
|
+
".npm",
|
|
20
|
+
".pnpm-store",
|
|
21
|
+
"__pycache__",
|
|
22
|
+
"node_modules",
|
|
23
|
+
]);
|
|
24
|
+
/** Scan provider-specific and universal Skill roots on this machine. */
|
|
25
|
+
export function scanLocalSkillCandidates(runtime, env = process.env) {
|
|
26
|
+
const byName = new Map();
|
|
27
|
+
for (const root of localSkillRoots(runtime, env)) {
|
|
28
|
+
const resolved = readableDirectory(root.path);
|
|
29
|
+
if (!resolved)
|
|
30
|
+
continue;
|
|
31
|
+
for (const skillDir of discoverSkillDirs(resolved, 0)) {
|
|
32
|
+
const candidate = readCandidate(skillDir, runtime, root.root);
|
|
33
|
+
if (!candidate)
|
|
34
|
+
continue;
|
|
35
|
+
const current = byName.get(candidate.name);
|
|
36
|
+
if (!current || (current.root === "universal" && candidate.root === "provider")) {
|
|
37
|
+
byName.set(candidate.name, candidate);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return [...byName.values()].sort((left, right) => left.name.localeCompare(right.name));
|
|
42
|
+
}
|
|
43
|
+
/** Revalidate opaque scan keys and collect complete immutable payloads. */
|
|
44
|
+
export function collectLocalSkillBundles(runtime, keys, env = process.env) {
|
|
45
|
+
const bundles = [];
|
|
46
|
+
const failures = [];
|
|
47
|
+
let totalBytes = 0;
|
|
48
|
+
for (const key of keys) {
|
|
49
|
+
let payload;
|
|
50
|
+
try {
|
|
51
|
+
payload = decodeCandidateKey(key);
|
|
52
|
+
if (payload.runtime !== runtime)
|
|
53
|
+
throw new Error("runtime mismatch");
|
|
54
|
+
if (!isAllowedCandidatePath(payload, env)) {
|
|
55
|
+
throw new Error("skill path is outside runtime skill roots");
|
|
56
|
+
}
|
|
57
|
+
const bundle = collectBundle(payload.path);
|
|
58
|
+
if (!bundle)
|
|
59
|
+
throw new Error("SKILL.md not found or invalid");
|
|
60
|
+
const bytes = bundle.files.reduce((sum, file) => sum + file.sizeBytes, 0);
|
|
61
|
+
if (totalBytes + bytes > MAX_SKILL_IMPORT_BYTES) {
|
|
62
|
+
throw new Error("selected skills exceed the local import limit");
|
|
63
|
+
}
|
|
64
|
+
totalBytes += bytes;
|
|
65
|
+
bundles.push(bundle);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
failures.push({
|
|
69
|
+
key,
|
|
70
|
+
name: payload?.path ? path.basename(payload.path) : "unknown",
|
|
71
|
+
...(payload?.path ? { sourcePath: payload.path } : {}),
|
|
72
|
+
message: messageOf(error),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return { bundles, failures };
|
|
77
|
+
}
|
|
78
|
+
/** Collect all valid Skill bundles below an already trusted local directory
|
|
79
|
+
* (for example, a safely extracted ZIP staging directory). */
|
|
80
|
+
export function collectSkillBundlesFromDirectory(root) {
|
|
81
|
+
const resolved = readableDirectory(root);
|
|
82
|
+
if (!resolved)
|
|
83
|
+
return { bundles: [], failures: [] };
|
|
84
|
+
const bundles = [];
|
|
85
|
+
const failures = [];
|
|
86
|
+
for (const skillDir of discoverSkillDirs(resolved, 0)) {
|
|
87
|
+
try {
|
|
88
|
+
const bundle = collectBundle(skillDir);
|
|
89
|
+
if (!bundle)
|
|
90
|
+
throw new Error("SKILL.md not found or invalid");
|
|
91
|
+
bundles.push(bundle);
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
failures.push({
|
|
95
|
+
key: skillDir,
|
|
96
|
+
name: path.basename(skillDir),
|
|
97
|
+
sourcePath: skillDir,
|
|
98
|
+
message: messageOf(error),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return { bundles, failures };
|
|
103
|
+
}
|
|
104
|
+
export function scanLocalSkillCandidatesInWorker(runtime, env = process.env) {
|
|
105
|
+
return runWorker({ kind: "scan", runtime }, env);
|
|
106
|
+
}
|
|
107
|
+
export function collectLocalSkillBundlesInWorker(runtime, keys, env = process.env) {
|
|
108
|
+
return runWorker({ kind: "collect", runtime, keys }, env);
|
|
109
|
+
}
|
|
110
|
+
export function executeLocalSkillWorkerRequest(request, env) {
|
|
111
|
+
return request.kind === "scan"
|
|
112
|
+
? scanLocalSkillCandidates(request.runtime, env)
|
|
113
|
+
: collectLocalSkillBundles(request.runtime, request.keys, env);
|
|
114
|
+
}
|
|
115
|
+
function readCandidate(skillDir, runtime, root) {
|
|
116
|
+
const metadata = readMetadata(skillDir);
|
|
117
|
+
if (!metadata)
|
|
118
|
+
return undefined;
|
|
119
|
+
const summary = summarizeFiles(skillDir);
|
|
120
|
+
return {
|
|
121
|
+
key: encodeCandidateKey({ runtime, root, path: skillDir }),
|
|
122
|
+
...metadata,
|
|
123
|
+
runtime,
|
|
124
|
+
sourcePath: skillDir,
|
|
125
|
+
root,
|
|
126
|
+
...summary,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function readMetadata(skillDir) {
|
|
130
|
+
try {
|
|
131
|
+
const parsed = parseSkillFrontmatter(readFileSync(path.join(skillDir, "SKILL.md"), "utf8"));
|
|
132
|
+
if (!parsed || !SAFE_SKILL_NAME.test(parsed.name))
|
|
133
|
+
return undefined;
|
|
134
|
+
return parsed;
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function summarizeFiles(skillDir) {
|
|
141
|
+
let fileCount = 0;
|
|
142
|
+
let sizeBytes = 0;
|
|
143
|
+
walkFiles(skillDir, skillDir, (absolute) => {
|
|
144
|
+
if (fileCount >= MAX_SKILL_FILES)
|
|
145
|
+
return false;
|
|
146
|
+
try {
|
|
147
|
+
const info = statSync(absolute);
|
|
148
|
+
fileCount += 1;
|
|
149
|
+
sizeBytes += info.size;
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
// Descriptive scan metadata is best effort.
|
|
153
|
+
}
|
|
154
|
+
return fileCount < MAX_SKILL_FILES;
|
|
155
|
+
});
|
|
156
|
+
return { fileCount, sizeBytes };
|
|
157
|
+
}
|
|
158
|
+
function collectBundle(skillDir) {
|
|
159
|
+
const metadata = readMetadata(skillDir);
|
|
160
|
+
if (!metadata)
|
|
161
|
+
return undefined;
|
|
162
|
+
const files = [];
|
|
163
|
+
let storedBytes = 0;
|
|
164
|
+
walkFiles(skillDir, skillDir, (absolute, relative) => {
|
|
165
|
+
if (files.length >= MAX_SKILL_FILES)
|
|
166
|
+
return false;
|
|
167
|
+
const info = statSync(absolute);
|
|
168
|
+
if (info.size > MAX_SKILL_FILE_BYTES) {
|
|
169
|
+
throw new Error(`skill file is too large: ${relative}`);
|
|
170
|
+
}
|
|
171
|
+
if (storedBytes + info.size > MAX_SKILL_BUNDLE_BYTES) {
|
|
172
|
+
throw new Error("skill bundle is too large");
|
|
173
|
+
}
|
|
174
|
+
const buffer = readFileSync(absolute);
|
|
175
|
+
if (buffer.byteLength > MAX_SKILL_FILE_BYTES) {
|
|
176
|
+
throw new Error(`skill file is too large: ${relative}`);
|
|
177
|
+
}
|
|
178
|
+
if (storedBytes + buffer.byteLength > MAX_SKILL_BUNDLE_BYTES) {
|
|
179
|
+
throw new Error("skill bundle is too large");
|
|
180
|
+
}
|
|
181
|
+
storedBytes += buffer.byteLength;
|
|
182
|
+
files.push({
|
|
183
|
+
path: relative,
|
|
184
|
+
contentBase64: buffer.toString("base64"),
|
|
185
|
+
sizeBytes: buffer.byteLength,
|
|
186
|
+
sha256: createHash("sha256").update(buffer).digest("hex"),
|
|
187
|
+
});
|
|
188
|
+
return true;
|
|
189
|
+
});
|
|
190
|
+
if (!files.some((file) => file.path === "SKILL.md"))
|
|
191
|
+
return undefined;
|
|
192
|
+
files.sort((left, right) => left.path.localeCompare(right.path));
|
|
193
|
+
return {
|
|
194
|
+
...metadata,
|
|
195
|
+
sourcePath: skillDir,
|
|
196
|
+
contentHash: contentHash(files),
|
|
197
|
+
files,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function contentHash(files) {
|
|
201
|
+
const digest = createHash("sha256");
|
|
202
|
+
for (const file of files) {
|
|
203
|
+
digest.update(file.path);
|
|
204
|
+
digest.update("\0");
|
|
205
|
+
digest.update(file.sha256);
|
|
206
|
+
digest.update("\0");
|
|
207
|
+
digest.update(String(file.sizeBytes));
|
|
208
|
+
digest.update("\0");
|
|
209
|
+
}
|
|
210
|
+
return digest.digest("hex");
|
|
211
|
+
}
|
|
212
|
+
function localSkillRoots(runtime, env) {
|
|
213
|
+
const home = env.HOME?.trim() || homedir();
|
|
214
|
+
const universal = firstEnv(env, "AGENTS_SKILLS_DIR")
|
|
215
|
+
?? path.join(firstEnv(env, "AGENTS_HOME") ?? path.join(home, ".agents"), "skills");
|
|
216
|
+
const provider = runtime === "codex"
|
|
217
|
+
? [path.join(firstEnv(env, "CODEX_HOME") ?? path.join(home, ".codex"), "skills")]
|
|
218
|
+
: runtime === "claude"
|
|
219
|
+
? [path.join(firstEnv(env, "CLAUDE_CONFIG_DIR", "CLAUDE_HOME") ?? path.join(home, ".claude"), "skills")]
|
|
220
|
+
: [
|
|
221
|
+
path.join(firstEnv(env, "TRAECLI_HOME") ?? path.join(home, ".traecli"), "skills"),
|
|
222
|
+
path.join(firstEnv(env, "TRAE_HOME", "TRAEX_HOME") ?? path.join(home, ".trae"), "skills"),
|
|
223
|
+
];
|
|
224
|
+
return dedupeRoots([
|
|
225
|
+
...provider.map((rootPath) => ({ root: "provider", path: rootPath })),
|
|
226
|
+
{ root: "universal", path: universal },
|
|
227
|
+
]);
|
|
228
|
+
}
|
|
229
|
+
function dedupeRoots(roots) {
|
|
230
|
+
const seen = new Set();
|
|
231
|
+
return roots.filter((root) => {
|
|
232
|
+
const key = path.resolve(root.path);
|
|
233
|
+
if (seen.has(key))
|
|
234
|
+
return false;
|
|
235
|
+
seen.add(key);
|
|
236
|
+
return true;
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
function discoverSkillDirs(root, depth, visited = new Set(), state = { count: 0 }) {
|
|
240
|
+
if (depth > MAX_DISCOVERY_DEPTH || state.count >= MAX_DISCOVERY_DIRS)
|
|
241
|
+
return [];
|
|
242
|
+
let resolved;
|
|
243
|
+
try {
|
|
244
|
+
if (!statSync(root).isDirectory())
|
|
245
|
+
return [];
|
|
246
|
+
resolved = realpathSync(root);
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
return [];
|
|
250
|
+
}
|
|
251
|
+
if (visited.has(resolved))
|
|
252
|
+
return [];
|
|
253
|
+
visited.add(resolved);
|
|
254
|
+
state.count += 1;
|
|
255
|
+
if (existsSync(path.join(resolved, "SKILL.md")))
|
|
256
|
+
return [resolved];
|
|
257
|
+
if (depth >= MAX_DISCOVERY_DEPTH)
|
|
258
|
+
return [];
|
|
259
|
+
let entries;
|
|
260
|
+
try {
|
|
261
|
+
entries = readdirSync(resolved, { withFileTypes: true });
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
return [];
|
|
265
|
+
}
|
|
266
|
+
const found = [];
|
|
267
|
+
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
268
|
+
if (!entry.isDirectory() || entry.isSymbolicLink() || shouldSkip(entry.name))
|
|
269
|
+
continue;
|
|
270
|
+
found.push(...discoverSkillDirs(path.join(resolved, entry.name), depth + 1, visited, state));
|
|
271
|
+
if (state.count >= MAX_DISCOVERY_DIRS)
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
return found;
|
|
275
|
+
}
|
|
276
|
+
function walkFiles(root, current, visit) {
|
|
277
|
+
let realRoot = root;
|
|
278
|
+
let realCurrent = current;
|
|
279
|
+
if (root === current) {
|
|
280
|
+
try {
|
|
281
|
+
realRoot = realpathSync(root);
|
|
282
|
+
realCurrent = realRoot;
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
let entries;
|
|
289
|
+
try {
|
|
290
|
+
entries = readdirSync(realCurrent, { withFileTypes: true });
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
296
|
+
if (entry.isSymbolicLink() || shouldSkip(entry.name))
|
|
297
|
+
continue;
|
|
298
|
+
const absolute = path.join(realCurrent, entry.name);
|
|
299
|
+
if (entry.isDirectory()) {
|
|
300
|
+
if (!walkFiles(realRoot, absolute, visit))
|
|
301
|
+
return false;
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
if (!entry.isFile())
|
|
305
|
+
continue;
|
|
306
|
+
const relative = normalizeRelative(path.relative(realRoot, absolute));
|
|
307
|
+
if (relative && visit(absolute, relative) === false)
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
function shouldSkip(name) {
|
|
313
|
+
return !name || name.startsWith(".") || IGNORED_DIRS.has(name);
|
|
314
|
+
}
|
|
315
|
+
function normalizeRelative(value) {
|
|
316
|
+
const parts = value.replace(/\\/g, "/").split("/");
|
|
317
|
+
if (parts.some((part) => !part || part === "." || part === "..")) {
|
|
318
|
+
throw new Error("invalid skill file path");
|
|
319
|
+
}
|
|
320
|
+
return parts.join("/");
|
|
321
|
+
}
|
|
322
|
+
function encodeCandidateKey(payload) {
|
|
323
|
+
return Buffer.from(JSON.stringify(payload), "utf8").toString("base64url");
|
|
324
|
+
}
|
|
325
|
+
function decodeCandidateKey(key) {
|
|
326
|
+
const bytes = Buffer.from(key, "base64url");
|
|
327
|
+
if (bytes.byteLength > 16 * 1024)
|
|
328
|
+
throw new Error("candidate key is too large");
|
|
329
|
+
const value = JSON.parse(bytes.toString("utf8"));
|
|
330
|
+
if (!isRuntime(value.runtime))
|
|
331
|
+
throw new Error("unsupported runtime");
|
|
332
|
+
if (value.root !== "provider" && value.root !== "universal")
|
|
333
|
+
throw new Error("unsupported root");
|
|
334
|
+
if (typeof value.path !== "string" || !value.path.trim())
|
|
335
|
+
throw new Error("missing path");
|
|
336
|
+
return { runtime: value.runtime, root: value.root, path: path.resolve(value.path) };
|
|
337
|
+
}
|
|
338
|
+
function isAllowedCandidatePath(payload, env) {
|
|
339
|
+
return localSkillRoots(payload.runtime, env)
|
|
340
|
+
.filter((root) => root.root === payload.root)
|
|
341
|
+
.some((root) => {
|
|
342
|
+
const allowed = readableDirectory(root.path);
|
|
343
|
+
if (!allowed)
|
|
344
|
+
return false;
|
|
345
|
+
try {
|
|
346
|
+
const candidate = realpathSync(payload.path);
|
|
347
|
+
const relative = path.relative(allowed, candidate);
|
|
348
|
+
return relative === "" || (relative !== ".." &&
|
|
349
|
+
!relative.startsWith(`..${path.sep}`) &&
|
|
350
|
+
!path.isAbsolute(relative));
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
function readableDirectory(value) {
|
|
358
|
+
try {
|
|
359
|
+
const resolved = realpathSync(path.resolve(value));
|
|
360
|
+
return statSync(resolved).isDirectory() ? resolved : undefined;
|
|
361
|
+
}
|
|
362
|
+
catch {
|
|
363
|
+
return undefined;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
function firstEnv(env, ...keys) {
|
|
367
|
+
for (const key of keys) {
|
|
368
|
+
const value = env[key]?.trim();
|
|
369
|
+
if (value)
|
|
370
|
+
return value;
|
|
371
|
+
}
|
|
372
|
+
return undefined;
|
|
373
|
+
}
|
|
374
|
+
function isRuntime(value) {
|
|
375
|
+
return value === "codex" || value === "traex" || value === "claude";
|
|
376
|
+
}
|
|
377
|
+
function runWorker(request, env) {
|
|
378
|
+
const workerUrl = new URL("./local-discovery-worker.js", import.meta.url);
|
|
379
|
+
// Source-mode test runners resolve the TypeScript barrel directly and do not
|
|
380
|
+
// emit the sibling worker. Production package builds always take the Worker
|
|
381
|
+
// path; source-mode keeps identical validation semantics synchronously.
|
|
382
|
+
if (!existsSync(fileURLToPath(workerUrl))) {
|
|
383
|
+
return Promise.resolve(executeLocalSkillWorkerRequest(request, env));
|
|
384
|
+
}
|
|
385
|
+
return new Promise((resolve, reject) => {
|
|
386
|
+
const worker = new Worker(workerUrl, {
|
|
387
|
+
workerData: request,
|
|
388
|
+
env,
|
|
389
|
+
});
|
|
390
|
+
worker.unref();
|
|
391
|
+
let settled = false;
|
|
392
|
+
const finish = (error, value) => {
|
|
393
|
+
if (settled)
|
|
394
|
+
return;
|
|
395
|
+
settled = true;
|
|
396
|
+
void worker.terminate();
|
|
397
|
+
if (error)
|
|
398
|
+
reject(error);
|
|
399
|
+
else
|
|
400
|
+
resolve(value);
|
|
401
|
+
};
|
|
402
|
+
worker.once("message", (message) => {
|
|
403
|
+
if (!message?.ok)
|
|
404
|
+
finish(new Error(message?.error || "local Skill worker failed"));
|
|
405
|
+
else
|
|
406
|
+
finish(undefined, message.value);
|
|
407
|
+
});
|
|
408
|
+
worker.once("error", (error) => finish(error instanceof Error ? error : new Error(String(error))));
|
|
409
|
+
worker.once("exit", (code) => {
|
|
410
|
+
if (!settled)
|
|
411
|
+
finish(new Error(`local Skill worker exited without a result (${code})`));
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
function messageOf(error) {
|
|
416
|
+
return error instanceof Error ? error.message : String(error);
|
|
417
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rynx-ai/skills",
|
|
3
|
-
"version": "0.1.10",
|
|
3
|
+
"version": "0.1.11-beta.10",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/rynx-ai/rynx.git",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
}
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@rynx-ai/protocol": "0.1.10"
|
|
29
|
+
"@rynx-ai/protocol": "0.1.11-beta.10"
|
|
30
30
|
},
|
|
31
31
|
"scripts": {
|
|
32
32
|
"build": "rm -rf dist && tsc -p tsconfig.json"
|