@skild/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Peiiii
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,6 @@
1
+ # @skild/core
2
+
3
+ Headless core for skild: install, validate, list, info, uninstall, update, and init Agent Skills locally.
4
+
5
+ This package is meant to be consumed by the `skild` CLI and future tooling.
6
+
@@ -0,0 +1,117 @@
1
+ type SkildErrorCode = 'INVALID_SOURCE' | 'NOT_A_DIRECTORY' | 'EMPTY_INSTALL_DIR' | 'ALREADY_INSTALLED' | 'SKILL_NOT_FOUND' | 'MISSING_METADATA' | 'INVALID_SKILL';
2
+ declare class SkildError extends Error {
3
+ readonly code: SkildErrorCode;
4
+ readonly details?: Record<string, unknown>;
5
+ constructor(code: SkildErrorCode, message: string, details?: Record<string, unknown>);
6
+ }
7
+
8
+ declare const PLATFORMS: readonly ["claude", "codex", "copilot"];
9
+ type Platform = (typeof PLATFORMS)[number];
10
+ type InstallScope = 'global' | 'project';
11
+ type SourceType = 'local' | 'github-url' | 'degit-shorthand';
12
+ interface InstallOptions {
13
+ platform?: Platform;
14
+ scope?: InstallScope;
15
+ force?: boolean;
16
+ }
17
+ interface ListOptions {
18
+ platform?: Platform;
19
+ scope?: InstallScope;
20
+ }
21
+ interface UpdateOptions {
22
+ platform?: Platform;
23
+ scope?: InstallScope;
24
+ force?: boolean;
25
+ }
26
+ interface SkillFrontmatter {
27
+ name: string;
28
+ description: string;
29
+ version?: string;
30
+ [key: string]: unknown;
31
+ }
32
+ interface SkillValidationIssue {
33
+ level: 'error' | 'warning';
34
+ message: string;
35
+ path?: string;
36
+ }
37
+ interface SkillValidationResult {
38
+ ok: boolean;
39
+ issues: SkillValidationIssue[];
40
+ frontmatter?: SkillFrontmatter;
41
+ }
42
+ interface InstallRecord {
43
+ schemaVersion: 1;
44
+ name: string;
45
+ platform: Platform;
46
+ scope: InstallScope;
47
+ source: string;
48
+ sourceType: SourceType;
49
+ installedAt: string;
50
+ updatedAt?: string;
51
+ installDir: string;
52
+ contentHash: string;
53
+ hasSkillMd: boolean;
54
+ skill?: {
55
+ frontmatter?: SkillFrontmatter;
56
+ validation?: SkillValidationResult;
57
+ };
58
+ }
59
+ interface LockEntry {
60
+ name: string;
61
+ platform: Platform;
62
+ scope: InstallScope;
63
+ source: string;
64
+ sourceType: SourceType;
65
+ installedAt: string;
66
+ updatedAt?: string;
67
+ installDir: string;
68
+ contentHash: string;
69
+ }
70
+ interface Lockfile {
71
+ schemaVersion: 1;
72
+ updatedAt: string;
73
+ entries: Record<string, LockEntry>;
74
+ }
75
+ interface GlobalConfig {
76
+ schemaVersion: 1;
77
+ defaultPlatform: Platform;
78
+ defaultScope: InstallScope;
79
+ }
80
+
81
+ declare function loadOrCreateGlobalConfig(): GlobalConfig;
82
+
83
+ declare function getSkillsDir(platform: Platform, scope: InstallScope): string;
84
+ declare function getSkillInstallDir(platform: Platform, scope: InstallScope, skillName: string): string;
85
+
86
+ declare function validateSkillDir(skillDir: string): SkillValidationResult;
87
+
88
+ interface InitOptions {
89
+ force?: boolean;
90
+ dir?: string;
91
+ description?: string;
92
+ }
93
+ declare function initSkill(name: string, options?: InitOptions): string;
94
+
95
+ interface InstallInput {
96
+ source: string;
97
+ nameOverride?: string;
98
+ }
99
+ declare function installSkill(input: InstallInput, options?: InstallOptions): Promise<InstallRecord>;
100
+ interface ListedSkill {
101
+ name: string;
102
+ installDir: string;
103
+ hasSkillMd: boolean;
104
+ record?: InstallRecord | null;
105
+ }
106
+ declare function listSkills(options?: ListOptions): ListedSkill[];
107
+ declare function getSkillInfo(name: string, options?: ListOptions): InstallRecord;
108
+ declare function validateSkill(nameOrPath: string, options?: {
109
+ platform?: Platform;
110
+ scope?: InstallScope;
111
+ }): SkillValidationResult;
112
+ declare function uninstallSkill(name: string, options?: InstallOptions & {
113
+ allowMissingMetadata?: boolean;
114
+ }): void;
115
+ declare function updateSkill(name?: string, options?: UpdateOptions): Promise<InstallRecord[]>;
116
+
117
+ export { type GlobalConfig, type InstallOptions, type InstallRecord, type InstallScope, type ListOptions, type Lockfile, PLATFORMS, type Platform, SkildError, type SkillFrontmatter, type SkillValidationIssue, type SkillValidationResult, type UpdateOptions, getSkillInfo, getSkillInstallDir, getSkillsDir, initSkill, installSkill, listSkills, loadOrCreateGlobalConfig, uninstallSkill, updateSkill, validateSkill, validateSkillDir };
package/dist/index.js ADDED
@@ -0,0 +1,504 @@
1
+ // src/errors.ts
2
+ var SkildError = class extends Error {
3
+ code;
4
+ details;
5
+ constructor(code, message, details) {
6
+ super(message);
7
+ this.code = code;
8
+ this.details = details;
9
+ }
10
+ };
11
+
12
+ // src/types.ts
13
+ var PLATFORMS = ["claude", "codex", "copilot"];
14
+
15
+ // src/storage.ts
16
+ import fs2 from "fs";
17
+ import path2 from "path";
18
+
19
+ // src/paths.ts
20
+ import os from "os";
21
+ import path from "path";
22
+ import fs from "fs";
23
+ function getHomeDir() {
24
+ return os.homedir();
25
+ }
26
+ function getProjectDir() {
27
+ return process.cwd();
28
+ }
29
+ function getSkildGlobalDir() {
30
+ return path.join(getHomeDir(), ".skild");
31
+ }
32
+ function ensureDir(dir) {
33
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
34
+ }
35
+ function getSkillsDir(platform, scope) {
36
+ const base = scope === "project" ? getProjectDir() : getHomeDir();
37
+ switch (platform) {
38
+ case "claude":
39
+ return path.join(base, ".claude", "skills");
40
+ case "codex":
41
+ return path.join(base, ".codex", "skills");
42
+ case "copilot":
43
+ return path.join(base, ".github", "skills");
44
+ }
45
+ }
46
+ function getProjectSkildDir() {
47
+ return path.join(getProjectDir(), ".skild");
48
+ }
49
+ function getProjectLockPath() {
50
+ return path.join(getProjectSkildDir(), "lock.json");
51
+ }
52
+ function getGlobalConfigPath() {
53
+ return path.join(getSkildGlobalDir(), "config.json");
54
+ }
55
+ function getGlobalLockPath() {
56
+ return path.join(getSkildGlobalDir(), "lock.json");
57
+ }
58
+ function getSkillInstallDir(platform, scope, skillName) {
59
+ return path.join(getSkillsDir(platform, scope), skillName);
60
+ }
61
+ function getSkillMetadataDir(skillInstallDir) {
62
+ return path.join(skillInstallDir, ".skild");
63
+ }
64
+ function getSkillInstallRecordPath(skillInstallDir) {
65
+ return path.join(getSkillMetadataDir(skillInstallDir), "install.json");
66
+ }
67
+
68
+ // src/storage.ts
69
+ function readJsonFile(filePath) {
70
+ if (!fs2.existsSync(filePath)) return null;
71
+ return JSON.parse(fs2.readFileSync(filePath, "utf8"));
72
+ }
73
+ function writeJsonFile(filePath, value) {
74
+ ensureDir(path2.dirname(filePath));
75
+ fs2.writeFileSync(filePath, JSON.stringify(value, null, 2) + "\n", "utf8");
76
+ }
77
+ function loadOrCreateGlobalConfig() {
78
+ const filePath = getGlobalConfigPath();
79
+ const existing = readJsonFile(filePath);
80
+ if (existing) return existing;
81
+ const created = {
82
+ schemaVersion: 1,
83
+ defaultPlatform: PLATFORMS[0],
84
+ defaultScope: "global"
85
+ };
86
+ writeJsonFile(filePath, created);
87
+ return created;
88
+ }
89
+ function loadLockfile(lockPath) {
90
+ return readJsonFile(lockPath);
91
+ }
92
+ function saveLockfile(lockPath, lockfile) {
93
+ writeJsonFile(lockPath, lockfile);
94
+ }
95
+ function getLockPath(scope) {
96
+ return scope === "project" ? getProjectLockPath() : getGlobalLockPath();
97
+ }
98
+ function loadOrCreateLockfile(scope) {
99
+ const lockPath = getLockPath(scope);
100
+ const existing = loadLockfile(lockPath);
101
+ if (existing) return existing;
102
+ const created = { schemaVersion: 1, updatedAt: (/* @__PURE__ */ new Date()).toISOString(), entries: {} };
103
+ saveLockfile(lockPath, created);
104
+ return created;
105
+ }
106
+ function upsertLockEntry(scope, entry) {
107
+ const lockfile = loadOrCreateLockfile(scope);
108
+ lockfile.entries[entry.name] = entry;
109
+ lockfile.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
110
+ saveLockfile(getLockPath(scope), lockfile);
111
+ }
112
+ function removeLockEntry(scope, name) {
113
+ const lockfile = loadOrCreateLockfile(scope);
114
+ delete lockfile.entries[name];
115
+ lockfile.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
116
+ saveLockfile(getLockPath(scope), lockfile);
117
+ }
118
+ function readInstallRecord(installDir) {
119
+ const filePath = getSkillInstallRecordPath(installDir);
120
+ return readJsonFile(filePath);
121
+ }
122
+ function writeInstallRecord(installDir, record) {
123
+ ensureDir(getSkillMetadataDir(installDir));
124
+ const filePath = getSkillInstallRecordPath(installDir);
125
+ writeJsonFile(filePath, record);
126
+ }
127
+
128
+ // src/skill.ts
129
+ import fs3 from "fs";
130
+ import path3 from "path";
131
+ import yaml from "js-yaml";
132
+ function readSkillMd(skillDir) {
133
+ const filePath = path3.join(skillDir, "SKILL.md");
134
+ if (!fs3.existsSync(filePath)) return null;
135
+ return fs3.readFileSync(filePath, "utf8");
136
+ }
137
+ function parseSkillFrontmatter(skillMdContent) {
138
+ const trimmed = skillMdContent.trimStart();
139
+ if (!trimmed.startsWith("---")) return null;
140
+ const end = trimmed.indexOf("\n---", 3);
141
+ if (end === -1) return null;
142
+ const yamlBlock = trimmed.slice(3, end).trim();
143
+ const parsed = yaml.load(yamlBlock);
144
+ if (!parsed || typeof parsed !== "object") return null;
145
+ return parsed;
146
+ }
147
+ function validateSkillDir(skillDir) {
148
+ const issues = [];
149
+ const skillMd = readSkillMd(skillDir);
150
+ if (!skillMd) {
151
+ issues.push({ level: "error", message: "Missing SKILL.md", path: path3.join(skillDir, "SKILL.md") });
152
+ return { ok: false, issues };
153
+ }
154
+ const frontmatter = parseSkillFrontmatter(skillMd);
155
+ if (!frontmatter) {
156
+ issues.push({ level: "error", message: "SKILL.md is missing valid YAML frontmatter (--- ... ---)" });
157
+ return { ok: false, issues };
158
+ }
159
+ if (!frontmatter.name || typeof frontmatter.name !== "string") {
160
+ issues.push({ level: "error", message: 'Frontmatter "name" is required and must be a string' });
161
+ }
162
+ if (!frontmatter.description || typeof frontmatter.description !== "string") {
163
+ issues.push({ level: "error", message: 'Frontmatter "description" is required and must be a string' });
164
+ }
165
+ return { ok: issues.every((i) => i.level !== "error"), issues, frontmatter };
166
+ }
167
+
168
+ // src/init.ts
169
+ import fs4 from "fs";
170
+ import path4 from "path";
171
+ function initSkill(name, options = {}) {
172
+ const targetDir = path4.resolve(options.dir || name);
173
+ if (fs4.existsSync(targetDir) && !options.force) {
174
+ throw new SkildError("ALREADY_INSTALLED", `Target directory already exists: ${targetDir}. Use --force to overwrite.`, { targetDir });
175
+ }
176
+ ensureDir(targetDir);
177
+ ensureDir(path4.join(targetDir, "scripts"));
178
+ ensureDir(path4.join(targetDir, "references"));
179
+ ensureDir(path4.join(targetDir, "assets"));
180
+ const description = options.description || "Describe what this Skill does.";
181
+ const skillMd = `---
182
+ name: ${name}
183
+ description: ${description}
184
+ ---
185
+
186
+ ## Instructions
187
+
188
+ - Add your workflow instructions here.
189
+
190
+ ## Files
191
+
192
+ - Put scripts in \`scripts/\`.
193
+ - Put docs in \`references/\`.
194
+ - Put static assets in \`assets/\`.
195
+ `;
196
+ fs4.writeFileSync(path4.join(targetDir, "SKILL.md"), skillMd, "utf8");
197
+ const runSh = `#!/usr/bin/env bash
198
+ set -euo pipefail
199
+
200
+ echo "${name}: run script placeholder"
201
+ `;
202
+ fs4.writeFileSync(path4.join(targetDir, "scripts", "run.sh"), runSh, "utf8");
203
+ try {
204
+ fs4.chmodSync(path4.join(targetDir, "scripts", "run.sh"), 493);
205
+ } catch {
206
+ }
207
+ return targetDir;
208
+ }
209
+
210
+ // src/lifecycle.ts
211
+ import path7 from "path";
212
+ import fs6 from "fs";
213
+ import degit from "degit";
214
+
215
+ // src/source.ts
216
+ import path6 from "path";
217
+
218
+ // src/fs.ts
219
+ import fs5 from "fs";
220
+ import path5 from "path";
221
+ import crypto from "crypto";
222
+ function pathExists(filePath) {
223
+ return fs5.existsSync(filePath);
224
+ }
225
+ function isDirectory(filePath) {
226
+ try {
227
+ return fs5.statSync(filePath).isDirectory();
228
+ } catch {
229
+ return false;
230
+ }
231
+ }
232
+ function isDirEmpty(dir) {
233
+ try {
234
+ const entries = fs5.readdirSync(dir);
235
+ return entries.length === 0;
236
+ } catch {
237
+ return true;
238
+ }
239
+ }
240
+ function copyDir(src, dest) {
241
+ fs5.cpSync(src, dest, { recursive: true });
242
+ }
243
+ function removeDir(dir) {
244
+ if (fs5.existsSync(dir)) fs5.rmSync(dir, { recursive: true, force: true });
245
+ }
246
+ function sanitizeForPathSegment(value) {
247
+ return value.replace(/[^a-zA-Z0-9._-]/g, "_");
248
+ }
249
+ function createTempDir(parentDir, prefix) {
250
+ ensureDir(parentDir);
251
+ const safePrefix = sanitizeForPathSegment(prefix || "tmp");
252
+ const template = path5.join(parentDir, `.skild-${safePrefix}-`);
253
+ return fs5.mkdtempSync(template);
254
+ }
255
+ function replaceDirAtomic(sourceDir, destDir) {
256
+ const backupDir = fs5.existsSync(destDir) ? `${destDir}.bak-${Date.now()}` : null;
257
+ try {
258
+ if (backupDir) fs5.renameSync(destDir, backupDir);
259
+ fs5.renameSync(sourceDir, destDir);
260
+ if (backupDir) removeDir(backupDir);
261
+ } catch (error) {
262
+ try {
263
+ if (!fs5.existsSync(destDir) && backupDir && fs5.existsSync(backupDir)) {
264
+ fs5.renameSync(backupDir, destDir);
265
+ }
266
+ } catch {
267
+ }
268
+ try {
269
+ if (fs5.existsSync(sourceDir)) removeDir(sourceDir);
270
+ } catch {
271
+ }
272
+ throw error;
273
+ }
274
+ }
275
+ function listFilesRecursive(rootDir) {
276
+ const results = [];
277
+ const stack = [rootDir];
278
+ while (stack.length) {
279
+ const current = stack.pop();
280
+ const entries = fs5.readdirSync(current, { withFileTypes: true });
281
+ for (const entry of entries) {
282
+ if (entry.name === ".skild") continue;
283
+ if (entry.name === ".git") continue;
284
+ const full = path5.join(current, entry.name);
285
+ if (entry.isDirectory()) stack.push(full);
286
+ else if (entry.isFile()) results.push(full);
287
+ }
288
+ }
289
+ results.sort();
290
+ return results;
291
+ }
292
+ function hashDirectoryContent(rootDir) {
293
+ const files = listFilesRecursive(rootDir);
294
+ const h = crypto.createHash("sha256");
295
+ for (const filePath of files) {
296
+ const rel = path5.relative(rootDir, filePath);
297
+ h.update(rel);
298
+ h.update("\0");
299
+ h.update(fs5.readFileSync(filePath));
300
+ h.update("\0");
301
+ }
302
+ return h.digest("hex");
303
+ }
304
+
305
+ // src/source.ts
306
+ function resolveLocalPath(source) {
307
+ const resolved = path6.resolve(source);
308
+ return pathExists(resolved) ? resolved : null;
309
+ }
310
+ function classifySource(source) {
311
+ const local = resolveLocalPath(source);
312
+ if (local) return "local";
313
+ if (/^https?:\/\//i.test(source) || source.includes("github.com")) return "github-url";
314
+ if (/^[^/]+\/[^/]+/.test(source)) return "degit-shorthand";
315
+ throw new SkildError(
316
+ "INVALID_SOURCE",
317
+ `Unsupported source "${source}". Use a Git URL (e.g. https://github.com/owner/repo/tree/<branch>/<subdir>) or degit shorthand (e.g. owner/repo[/subdir][#ref]) or a local directory.`
318
+ );
319
+ }
320
+ function extractSkillName(source) {
321
+ const local = resolveLocalPath(source);
322
+ if (local) return path6.basename(local) || "unknown-skill";
323
+ const cleaned = source.replace(/[#?].*$/, "");
324
+ const treeMatch = cleaned.match(/\/tree\/[^/]+\/(.+?)(?:\/)?$/);
325
+ if (treeMatch) return treeMatch[1].split("/").pop() || "unknown-skill";
326
+ const repoMatch = cleaned.match(/github\.com\/[^/]+\/([^/]+)/);
327
+ if (repoMatch) return repoMatch[1].replace(/\.git$/, "");
328
+ const parts = cleaned.split("/").filter(Boolean);
329
+ if (parts.length >= 2) return parts[parts.length - 1] || "unknown-skill";
330
+ return cleaned || "unknown-skill";
331
+ }
332
+ function toDegitPath(url) {
333
+ const treeMatch = url.match(/github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)\/(.+?)(?:\/)?$/);
334
+ if (treeMatch) {
335
+ const [, owner, repo, branch, subpath] = treeMatch;
336
+ return `${owner}/${repo}/${subpath}#${branch}`;
337
+ }
338
+ const repoMatch = url.match(/github\.com\/([^/]+\/[^/]+)/);
339
+ if (repoMatch) return repoMatch[1].replace(/\.git$/, "");
340
+ return url;
341
+ }
342
+
343
+ // src/lifecycle.ts
344
+ async function cloneRemote(degitSrc, targetPath) {
345
+ const emitter = degit(degitSrc, { force: true, verbose: false });
346
+ await emitter.clone(targetPath);
347
+ }
348
+ function ensureInstallableLocalDir(sourcePath) {
349
+ if (!isDirectory(sourcePath)) {
350
+ throw new SkildError("NOT_A_DIRECTORY", `Source path is not a directory: ${sourcePath}`, { sourcePath });
351
+ }
352
+ }
353
+ function assertNonEmptyInstall(stagingDir, source) {
354
+ if (isDirEmpty(stagingDir)) {
355
+ throw new SkildError(
356
+ "EMPTY_INSTALL_DIR",
357
+ `Installed directory is empty for source: ${source}
358
+ Source likely does not point to a valid subdirectory.
359
+ Try: https://github.com/<owner>/<repo>/tree/<branch>/skills/<skill-name>
360
+ Example: https://github.com/anthropics/skills/tree/main/skills/pdf`,
361
+ { source }
362
+ );
363
+ }
364
+ }
365
+ function resolvePlatformAndScope(options) {
366
+ const config = loadOrCreateGlobalConfig();
367
+ return {
368
+ platform: options.platform || config.defaultPlatform,
369
+ scope: options.scope || config.defaultScope
370
+ };
371
+ }
372
+ async function installSkill(input, options = {}) {
373
+ const { platform, scope } = resolvePlatformAndScope(options);
374
+ const source = input.source;
375
+ const sourceType = classifySource(source);
376
+ const skillsDir = getSkillsDir(platform, scope);
377
+ ensureDir(skillsDir);
378
+ const skillName = input.nameOverride || extractSkillName(source);
379
+ const installDir = getSkillInstallDir(platform, scope, skillName);
380
+ if (fs6.existsSync(installDir) && !options.force) {
381
+ throw new SkildError("ALREADY_INSTALLED", `Skill "${skillName}" is already installed at ${installDir}. Use --force, or uninstall first.`, {
382
+ skillName,
383
+ installDir
384
+ });
385
+ }
386
+ const tempRoot = createTempDir(skillsDir, skillName);
387
+ const stagingDir = path7.join(tempRoot, "staging");
388
+ try {
389
+ const localPath = resolveLocalPath(source);
390
+ if (localPath) {
391
+ ensureInstallableLocalDir(localPath);
392
+ copyDir(localPath, stagingDir);
393
+ } else {
394
+ const degitPath = toDegitPath(source);
395
+ await cloneRemote(degitPath, stagingDir);
396
+ }
397
+ assertNonEmptyInstall(stagingDir, source);
398
+ replaceDirAtomic(stagingDir, installDir);
399
+ const contentHash = hashDirectoryContent(installDir);
400
+ const validation = validateSkillDir(installDir);
401
+ const record = {
402
+ schemaVersion: 1,
403
+ name: skillName,
404
+ platform,
405
+ scope,
406
+ source,
407
+ sourceType,
408
+ installedAt: (/* @__PURE__ */ new Date()).toISOString(),
409
+ installDir,
410
+ contentHash,
411
+ hasSkillMd: fs6.existsSync(path7.join(installDir, "SKILL.md")),
412
+ skill: {
413
+ frontmatter: validation.frontmatter,
414
+ validation
415
+ }
416
+ };
417
+ writeInstallRecord(installDir, record);
418
+ const lockEntry = {
419
+ name: record.name,
420
+ platform: record.platform,
421
+ scope: record.scope,
422
+ source: record.source,
423
+ sourceType: record.sourceType,
424
+ installedAt: record.installedAt,
425
+ updatedAt: record.updatedAt,
426
+ installDir: record.installDir,
427
+ contentHash: record.contentHash
428
+ };
429
+ upsertLockEntry(scope, lockEntry);
430
+ return record;
431
+ } finally {
432
+ removeDir(tempRoot);
433
+ }
434
+ }
435
+ function listSkills(options = {}) {
436
+ const { platform, scope } = resolvePlatformAndScope(options);
437
+ const skillsDir = getSkillsDir(platform, scope);
438
+ if (!fs6.existsSync(skillsDir)) return [];
439
+ const entries = fs6.readdirSync(skillsDir, { withFileTypes: true }).filter((e) => e.isDirectory());
440
+ return entries.map((e) => {
441
+ const dir = path7.join(skillsDir, e.name);
442
+ const hasSkillMd = fs6.existsSync(path7.join(dir, "SKILL.md"));
443
+ const record = readInstallRecord(dir);
444
+ return { name: e.name, installDir: dir, hasSkillMd, record };
445
+ }).sort((a, b) => a.name.localeCompare(b.name));
446
+ }
447
+ function getSkillInfo(name, options = {}) {
448
+ const { platform, scope } = resolvePlatformAndScope(options);
449
+ const installDir = getSkillInstallDir(platform, scope, name);
450
+ if (!fs6.existsSync(installDir)) {
451
+ throw new SkildError("SKILL_NOT_FOUND", `Skill "${name}" not found in ${getSkillsDir(platform, scope)}`, { name, platform, scope });
452
+ }
453
+ const record = readInstallRecord(installDir);
454
+ if (!record) {
455
+ throw new SkildError("MISSING_METADATA", `Skill "${name}" is missing install metadata (.skild/install.json).`, { name, installDir });
456
+ }
457
+ return record;
458
+ }
459
+ function validateSkill(nameOrPath, options = {}) {
460
+ const localPath = resolveLocalPath(nameOrPath);
461
+ const dir = localPath || getSkillInstallDir(options.platform || loadOrCreateGlobalConfig().defaultPlatform, options.scope || loadOrCreateGlobalConfig().defaultScope, nameOrPath);
462
+ return validateSkillDir(dir);
463
+ }
464
+ function uninstallSkill(name, options = {}) {
465
+ const { platform, scope } = resolvePlatformAndScope(options);
466
+ const installDir = getSkillInstallDir(platform, scope, name);
467
+ if (!fs6.existsSync(installDir)) {
468
+ throw new SkildError("SKILL_NOT_FOUND", `Skill "${name}" not found in ${getSkillsDir(platform, scope)}`, { name, platform, scope });
469
+ }
470
+ const record = readInstallRecord(installDir);
471
+ if (!record && !options.allowMissingMetadata) {
472
+ throw new SkildError("MISSING_METADATA", `Skill "${name}" is missing install metadata. Use --force to uninstall anyway.`, { name, installDir });
473
+ }
474
+ removeDir(installDir);
475
+ removeLockEntry(scope, name);
476
+ }
477
+ async function updateSkill(name, options = {}) {
478
+ const { platform, scope } = resolvePlatformAndScope(options);
479
+ const targets = name ? [{ name }] : listSkills({ platform, scope }).map((s) => ({ name: s.name }));
480
+ const results = [];
481
+ for (const target of targets) {
482
+ const record = getSkillInfo(target.name, { platform, scope });
483
+ const updated = await installSkill({ source: record.source, nameOverride: record.name }, { platform, scope, force: true });
484
+ updated.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
485
+ writeInstallRecord(updated.installDir, updated);
486
+ results.push(updated);
487
+ }
488
+ return results;
489
+ }
490
+ export {
491
+ PLATFORMS,
492
+ SkildError,
493
+ getSkillInfo,
494
+ getSkillInstallDir,
495
+ getSkillsDir,
496
+ initSkill,
497
+ installSkill,
498
+ listSkills,
499
+ loadOrCreateGlobalConfig,
500
+ uninstallSkill,
501
+ updateSkill,
502
+ validateSkill,
503
+ validateSkillDir
504
+ };
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@skild/core",
3
+ "version": "0.1.0",
4
+ "description": "Skild core library (headless) for installing, validating, and managing Agent Skills locally.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "README.md",
17
+ "LICENSE"
18
+ ],
19
+ "dependencies": {
20
+ "degit": "^2.8.4",
21
+ "js-yaml": "^4.1.0"
22
+ },
23
+ "devDependencies": {
24
+ "@types/js-yaml": "^4.0.9",
25
+ "@types/node": "^20.10.0",
26
+ "tsup": "^8.0.0",
27
+ "typescript": "^5.3.0"
28
+ },
29
+ "engines": {
30
+ "node": ">=18"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public"
34
+ },
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/Peiiii/skild.git",
38
+ "directory": "packages/core"
39
+ },
40
+ "scripts": {
41
+ "build": "tsup src/index.ts --format esm --dts --clean",
42
+ "dev": "tsup src/index.ts --format esm --watch",
43
+ "typecheck": "tsc -p tsconfig.json --noEmit"
44
+ }
45
+ }