@hasna/skills 0.5.10 → 0.6.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/README.md +95 -53
- package/bin/index.js +3467 -1460
- package/bin/mcp.js +2638 -1713
- package/bin/migrate.js +110 -7
- package/bin/server.js +5487 -216
- package/bin/worker.js +372 -17
- package/dist/admin-contract.d.ts +13 -13
- package/dist/admin-contract.js +13 -13
- package/dist/cli/commands/agent-integration.d.ts +2 -0
- package/dist/cli/commands/context.d.ts +19 -0
- package/dist/cli/commands/install.d.ts +1 -0
- package/dist/cli/commands/profile-sync.d.ts +10 -0
- package/dist/cli/commands/profiles.d.ts +2 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +1273 -331
- package/dist/lib/agent-integration.d.ts +74 -0
- package/dist/lib/cloud-executions.d.ts +39 -0
- package/dist/lib/managed-policy.d.ts +6 -0
- package/dist/lib/profile-admin.d.ts +4 -0
- package/dist/lib/profile-client.d.ts +19 -0
- package/dist/lib/read-access.d.ts +3 -23
- package/dist/lib/selected-document.d.ts +6 -0
- package/dist/lib/selected-run.d.ts +31 -0
- package/dist/lib/selection-cache.d.ts +40 -0
- package/dist/lib/selection-resolver.d.ts +75 -0
- package/dist/lib/skill-context.d.ts +43 -0
- package/dist/lib/skillinfo.d.ts +2 -0
- package/dist/sdk/execution/dispatchers/ecs.d.ts +8 -0
- package/dist/sdk/index.d.ts +2 -0
- package/dist/sdk/index.js +9791 -8152
- package/dist/server/app.d.ts +7 -1
- package/dist/server/artifact-storage.d.ts +7 -10
- package/dist/server/auth.d.ts +2 -0
- package/dist/server/config.d.ts +2 -0
- package/dist/server/profile-api.d.ts +3 -0
- package/dist/server/runtime-api.d.ts +31 -0
- package/dist/server/runtime-policy.d.ts +25 -0
- package/dist/server/runtime-store.d.ts +73 -0
- package/dist/server/runtime-worker.d.ts +19 -0
- package/dist/server/selection-store.d.ts +39 -0
- package/dist/server/sqlite-store.d.ts +2 -0
- package/dist/server/store.d.ts +3 -0
- package/dist/server/types.d.ts +3 -0
- package/dist/storage.js +3 -0
- package/dist/types/skill-selection.d.ts +41 -0
- package/migrations/postgres/0007_skill_profiles.sql +19 -0
- package/migrations/postgres/0008_skill_runtime.sql +14 -0
- package/migrations/sqlite/0007_skill_profiles.sql +19 -0
- package/migrations/sqlite/0008_skill_runtime.sql +14 -0
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -256,6 +256,9 @@ function getDataDir() {
|
|
|
256
256
|
}
|
|
257
257
|
return root;
|
|
258
258
|
}
|
|
259
|
+
function getDataDirReadOnly() {
|
|
260
|
+
return getDataRoot();
|
|
261
|
+
}
|
|
259
262
|
function getConfigPath(scope) {
|
|
260
263
|
if (scope === "global") {
|
|
261
264
|
return join2(getDataDir(), "config.json");
|
|
@@ -350,12 +353,13 @@ import {
|
|
|
350
353
|
mkdirSync as mkdirSync3,
|
|
351
354
|
mkdtempSync,
|
|
352
355
|
readdirSync as readdirSync5,
|
|
356
|
+
realpathSync as realpathSync2,
|
|
353
357
|
renameSync,
|
|
354
358
|
rmSync,
|
|
355
359
|
statSync as statSync6,
|
|
356
360
|
writeFileSync as writeFileSync3
|
|
357
361
|
} from "fs";
|
|
358
|
-
import { basename as basename2, dirname as dirname3, isAbsolute as isAbsolute2, join as join7, normalize as normalize2 } from "path";
|
|
362
|
+
import { basename as basename2, dirname as dirname3, isAbsolute as isAbsolute2, join as join7, normalize as normalize2, relative as relative2, resolve as resolve2, sep as sep2 } from "path";
|
|
359
363
|
|
|
360
364
|
// src/lib/registry-data/development-tools.ts
|
|
361
365
|
var DEVELOPMENT_TOOLS_SKILLS = [
|
|
@@ -2650,14 +2654,18 @@ function escapeJsString(value) {
|
|
|
2650
2654
|
// src/lib/portable-skills.ts
|
|
2651
2655
|
var LEGACY_CUSTOM_DIRNAME = "custom";
|
|
2652
2656
|
function getPortableSkillsRoot(options = {}) {
|
|
2657
|
+
return resolvePortableSkillsRoot(options, true);
|
|
2658
|
+
}
|
|
2659
|
+
function resolvePortableSkillsRoot(options, migrate) {
|
|
2653
2660
|
if (options.rootDir)
|
|
2654
2661
|
return options.rootDir;
|
|
2655
|
-
const appDir = options.homeDir ? join7(options.homeDir, ".hasna", "skills") : getDataDir();
|
|
2662
|
+
const appDir = options.homeDir ? join7(options.homeDir, ".hasna", "skills") : migrate ? getDataDir() : getDataDirReadOnly();
|
|
2656
2663
|
const cache = join7(appDir, SKILLS_CACHE_DIRNAME);
|
|
2657
2664
|
if (isOwnerLayoutMigrated(appDir) && safeIsDirectory(cache))
|
|
2658
2665
|
return cache;
|
|
2659
2666
|
const installed = join7(appDir, INSTALLED_SKILLS_DIRNAME);
|
|
2660
|
-
|
|
2667
|
+
if (migrate)
|
|
2668
|
+
migrateLegacySkillLayout(appDir, installed);
|
|
2661
2669
|
return installed;
|
|
2662
2670
|
}
|
|
2663
2671
|
function looksLikeSkillDirectory(path) {
|
|
@@ -2834,6 +2842,26 @@ function portPortableSkillDirectory(sourceDir, options = {}) {
|
|
|
2834
2842
|
function isSkillCandidate(dir) {
|
|
2835
2843
|
return existsSync7(join7(dir, "SKILL.md")) || existsSync7(join7(dir, "skill.json")) || existsSync7(join7(dir, "package.json"));
|
|
2836
2844
|
}
|
|
2845
|
+
function physicalImportPath(path) {
|
|
2846
|
+
let ancestor = resolve2(path);
|
|
2847
|
+
const suffix = [];
|
|
2848
|
+
while (!existsSync7(ancestor)) {
|
|
2849
|
+
suffix.unshift(basename2(ancestor));
|
|
2850
|
+
ancestor = dirname3(ancestor);
|
|
2851
|
+
}
|
|
2852
|
+
return join7(realpathSync2(ancestor), ...suffix);
|
|
2853
|
+
}
|
|
2854
|
+
function assertDisjointImportPaths(source, destination) {
|
|
2855
|
+
const sourcePath = realpathSync2(source);
|
|
2856
|
+
const destinationPath = physicalImportPath(destination);
|
|
2857
|
+
const contains = (parent, child) => {
|
|
2858
|
+
const path = relative2(parent, child);
|
|
2859
|
+
return path === "" || !isAbsolute2(path) && path !== ".." && !path.startsWith(`..${sep2}`);
|
|
2860
|
+
};
|
|
2861
|
+
if (contains(sourcePath, destinationPath) || contains(destinationPath, sourcePath)) {
|
|
2862
|
+
throw new Error("Source and destination skill directories must not overlap");
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2837
2865
|
function portPortableSkill(sourcePath, options = {}) {
|
|
2838
2866
|
const absoluteSource = normalize2(sourcePath);
|
|
2839
2867
|
if (!existsSync7(absoluteSource) || !statSync6(absoluteSource).isDirectory()) {
|
|
@@ -2847,8 +2875,10 @@ function portPortableSkill(sourcePath, options = {}) {
|
|
|
2847
2875
|
const via = explicitName ? `Name '${skillName}' matches a bundled official skill.` : `Inferred name '${skillName}'${sourceSlug && sourceSlug !== skillName ? ` (from source folder '${basename2(absoluteSource)}')` : ""} matches a bundled official skill.`;
|
|
2848
2876
|
throw new Error(`${via} Importing it would shadow the official '${skillName}'. ` + `Pass --name to choose a different name, or --allow-shadow to override deliberately.`);
|
|
2849
2877
|
}
|
|
2878
|
+
assertDisjointImportPaths(absoluteSource, join7(resolvePortableSkillsRoot(options, false), skillName));
|
|
2850
2879
|
const root = getPortableSkillsRoot(options);
|
|
2851
2880
|
const destination = join7(root, skillName);
|
|
2881
|
+
assertDisjointImportPaths(absoluteSource, destination);
|
|
2852
2882
|
if (existsSync7(destination)) {
|
|
2853
2883
|
if (!options.overwrite)
|
|
2854
2884
|
throw new Error(`Skill '${skillName}' already exists at ${destination}`);
|
|
@@ -3447,27 +3477,369 @@ function getAllTags() {
|
|
|
3447
3477
|
return Array.from(tagSet).sort();
|
|
3448
3478
|
}
|
|
3449
3479
|
// src/lib/installer.ts
|
|
3450
|
-
import { existsSync as
|
|
3451
|
-
import { dirname as
|
|
3452
|
-
import { homedir as
|
|
3480
|
+
import { existsSync as existsSync12, readFileSync as readFileSync10, rmSync as rmSync3 } from "fs";
|
|
3481
|
+
import { dirname as dirname6, join as join12 } from "path";
|
|
3482
|
+
import { homedir as homedir4 } from "os";
|
|
3453
3483
|
import { fileURLToPath } from "url";
|
|
3454
3484
|
|
|
3455
3485
|
// src/lib/agent-sync.ts
|
|
3456
3486
|
import {
|
|
3457
3487
|
cpSync as cpSync3,
|
|
3458
|
-
existsSync as
|
|
3459
|
-
lstatSync as
|
|
3460
|
-
mkdirSync as
|
|
3488
|
+
existsSync as existsSync10,
|
|
3489
|
+
lstatSync as lstatSync4,
|
|
3490
|
+
mkdirSync as mkdirSync5,
|
|
3461
3491
|
mkdtempSync as mkdtempSync2,
|
|
3462
|
-
readFileSync as
|
|
3463
|
-
readdirSync as
|
|
3464
|
-
renameSync as
|
|
3492
|
+
readFileSync as readFileSync8,
|
|
3493
|
+
readdirSync as readdirSync8,
|
|
3494
|
+
renameSync as renameSync3,
|
|
3465
3495
|
rmSync as rmSync2,
|
|
3466
3496
|
statSync as statSync7,
|
|
3467
|
-
writeFileSync as
|
|
3497
|
+
writeFileSync as writeFileSync5
|
|
3468
3498
|
} from "fs";
|
|
3499
|
+
import { homedir as homedir3 } from "os";
|
|
3500
|
+
import { basename as basename3, dirname as dirname5, join as join10 } from "path";
|
|
3501
|
+
|
|
3502
|
+
// src/lib/agent-integration.ts
|
|
3503
|
+
import { createHash as createHash2, randomUUID } from "crypto";
|
|
3504
|
+
import { existsSync as existsSync9, lstatSync as lstatSync3, mkdirSync as mkdirSync4, readFileSync as readFileSync7, readdirSync as readdirSync7, readlinkSync, realpathSync as realpathSync3, renameSync as renameSync2, writeFileSync as writeFileSync4, unlinkSync, chmodSync } from "fs";
|
|
3505
|
+
import { dirname as dirname4, join as join9, resolve as resolve3, relative as relative3, isAbsolute as isAbsolute3, sep as sep3 } from "path";
|
|
3469
3506
|
import { homedir as homedir2 } from "os";
|
|
3470
|
-
|
|
3507
|
+
var sha = (data) => createHash2("sha256").update(data).digest("hex");
|
|
3508
|
+
var HOOK_EVENTS = ["UserPromptSubmit", "SessionStart", "SubagentStart"];
|
|
3509
|
+
var ROOTS = [
|
|
3510
|
+
["claude", ".claude/skills"],
|
|
3511
|
+
["codex", ".codex/skills"],
|
|
3512
|
+
["codex", ".agents/skills"],
|
|
3513
|
+
["gemini", ".gemini/skills"],
|
|
3514
|
+
["codewith", ".codewith/skills"],
|
|
3515
|
+
["opencode", ".config/opencode/skills"],
|
|
3516
|
+
["cursor", ".cursor/skills"]
|
|
3517
|
+
];
|
|
3518
|
+
function assertSafePath(path) {
|
|
3519
|
+
let cursor = resolve3(path);
|
|
3520
|
+
while (true) {
|
|
3521
|
+
if (lstatSync3(cursor, { throwIfNoEntry: false })?.isSymbolicLink())
|
|
3522
|
+
throw new Error(`Refusing symlink path: ${cursor}`);
|
|
3523
|
+
const parent = dirname4(cursor);
|
|
3524
|
+
if (parent === cursor)
|
|
3525
|
+
return;
|
|
3526
|
+
cursor = parent;
|
|
3527
|
+
}
|
|
3528
|
+
}
|
|
3529
|
+
function readRootAlias(home, agent) {
|
|
3530
|
+
assertSafePath(home);
|
|
3531
|
+
const realHome = realpathSync3(home), alias = join9(realHome, `.${agent}`);
|
|
3532
|
+
const stat = lstatSync3(alias, { throwIfNoEntry: false, bigint: true });
|
|
3533
|
+
if (!stat?.isSymbolicLink())
|
|
3534
|
+
throw new Error(`Agent root alias changed after planning: ${alias}`);
|
|
3535
|
+
const link = readlinkSync(alias), targetPath = resolve3(dirname4(alias), link);
|
|
3536
|
+
assertSafePath(targetPath);
|
|
3537
|
+
const targetStat = lstatSync3(targetPath, { throwIfNoEntry: false });
|
|
3538
|
+
if (!targetStat?.isDirectory())
|
|
3539
|
+
throw new Error(`Agent root alias must target an existing directory: ${alias}`);
|
|
3540
|
+
const target = realpathSync3(targetPath), inside = relative3(realHome, target);
|
|
3541
|
+
if (!inside || inside === ".." || inside.startsWith(`..${sep3}`) || isAbsolute3(inside))
|
|
3542
|
+
throw new Error(`Agent root alias target must be inside the real home: ${alias}`);
|
|
3543
|
+
return { agent, home: realHome, alias, target, link, aliasIdentity: `${stat.dev}:${stat.ino}:${stat.ctimeNs}`, targetIdentity: `${targetStat.dev}:${targetStat.ino}` };
|
|
3544
|
+
}
|
|
3545
|
+
function rootAliases(home, allowed = false) {
|
|
3546
|
+
if (!allowed)
|
|
3547
|
+
return [];
|
|
3548
|
+
const result = [];
|
|
3549
|
+
for (const agent of ["claude", "codex"])
|
|
3550
|
+
if (lstatSync3(join9(home, `.${agent}`), { throwIfNoEntry: false })?.isSymbolicLink())
|
|
3551
|
+
result.push(readRootAlias(home, agent));
|
|
3552
|
+
if (result.some((item, index) => result.slice(index + 1).some((other) => item.target === other.target || item.target.startsWith(other.target + sep3) || other.target.startsWith(item.target + sep3))))
|
|
3553
|
+
throw new Error("Agent root alias targets must not overlap");
|
|
3554
|
+
return result;
|
|
3555
|
+
}
|
|
3556
|
+
function canonicalAgentPath(path, aliases) {
|
|
3557
|
+
const absolute = resolve3(path), binding = aliases.find((item) => absolute === item.alias || absolute.startsWith(item.alias + sep3));
|
|
3558
|
+
return binding ? join9(binding.target, relative3(binding.alias, absolute)) : absolute;
|
|
3559
|
+
}
|
|
3560
|
+
function recheckRootAliases(aliases) {
|
|
3561
|
+
for (const binding of aliases) {
|
|
3562
|
+
if (!["claude", "codex"].includes(binding.agent) || binding.alias !== join9(binding.home, `.${binding.agent}`))
|
|
3563
|
+
throw new Error("Invalid agent root alias binding");
|
|
3564
|
+
const current = readRootAlias(binding.home, binding.agent);
|
|
3565
|
+
if (JSON.stringify(current) !== JSON.stringify(binding))
|
|
3566
|
+
throw new Error(`Agent root alias changed after planning: ${binding.alias}`);
|
|
3567
|
+
}
|
|
3568
|
+
}
|
|
3569
|
+
function treeHash(root) {
|
|
3570
|
+
const hash = createHash2("sha256");
|
|
3571
|
+
let bytes = 0, files = 0;
|
|
3572
|
+
function visit(path) {
|
|
3573
|
+
const stat = lstatSync3(path);
|
|
3574
|
+
if (stat.isSymbolicLink())
|
|
3575
|
+
throw new Error(`Refusing symlink in skill: ${path}`);
|
|
3576
|
+
const rel = relative3(root, path);
|
|
3577
|
+
if (stat.isDirectory()) {
|
|
3578
|
+
hash.update(`d\x00${rel}\x00`);
|
|
3579
|
+
for (const name of readdirSync7(path).sort())
|
|
3580
|
+
visit(join9(path, name));
|
|
3581
|
+
return;
|
|
3582
|
+
}
|
|
3583
|
+
if (!stat.isFile())
|
|
3584
|
+
throw new Error(`Unsupported skill file: ${path}`);
|
|
3585
|
+
bytes += stat.size;
|
|
3586
|
+
files++;
|
|
3587
|
+
if (bytes > 64 * 1024 * 1024 || files > 1e4)
|
|
3588
|
+
throw new Error("Native skill exceeds migration size limits");
|
|
3589
|
+
hash.update(`f\x00${rel}\x00${stat.size}\x00`);
|
|
3590
|
+
hash.update(readFileSync7(path));
|
|
3591
|
+
}
|
|
3592
|
+
assertSafePath(root);
|
|
3593
|
+
visit(root);
|
|
3594
|
+
return hash.digest("hex");
|
|
3595
|
+
}
|
|
3596
|
+
function inventoryNativeSkills(home = homedir2(), options = {}) {
|
|
3597
|
+
const aliases = rootAliases(home, options.allowRootAliases);
|
|
3598
|
+
const roots = ROOTS.map(([agent, path]) => [agent, canonicalAgentPath(join9(home, path), aliases)]);
|
|
3599
|
+
if (options.projectDir)
|
|
3600
|
+
for (const [agent, path] of ROOTS)
|
|
3601
|
+
roots.push([agent, join9(resolve3(options.projectDir), path)]);
|
|
3602
|
+
const entries = [], seen = new Set;
|
|
3603
|
+
function visit(agent, path, vendor = false, depth = 0) {
|
|
3604
|
+
assertSafePath(path);
|
|
3605
|
+
if (!existsSync9(path))
|
|
3606
|
+
return;
|
|
3607
|
+
if (!lstatSync3(path).isDirectory())
|
|
3608
|
+
return;
|
|
3609
|
+
if (existsSync9(join9(path, "SKILL.md"))) {
|
|
3610
|
+
if (seen.has(path))
|
|
3611
|
+
return;
|
|
3612
|
+
seen.add(path);
|
|
3613
|
+
let managed = false;
|
|
3614
|
+
const marker = join9(path, ".hasna-skills.json");
|
|
3615
|
+
if (existsSync9(marker)) {
|
|
3616
|
+
try {
|
|
3617
|
+
managed = JSON.parse(readFileSync7(marker, "utf8")).managedBy === "@hasna/skills";
|
|
3618
|
+
} catch {}
|
|
3619
|
+
}
|
|
3620
|
+
const rootAlias = aliases.find((item) => path === item.target || path.startsWith(item.target + sep3));
|
|
3621
|
+
entries.push({ agent, path, hash: treeHash(path), managed, vendor, ...rootAlias ? { rootAlias } : {} });
|
|
3622
|
+
return;
|
|
3623
|
+
}
|
|
3624
|
+
if (depth > (vendor ? 8 : 3))
|
|
3625
|
+
return;
|
|
3626
|
+
for (const name of readdirSync7(path).sort()) {
|
|
3627
|
+
if (name === "node_modules")
|
|
3628
|
+
continue;
|
|
3629
|
+
if (name.startsWith(".") && name !== ".system")
|
|
3630
|
+
continue;
|
|
3631
|
+
const isVendor = vendor || name === ".system";
|
|
3632
|
+
if (isVendor && !options.includeVendor)
|
|
3633
|
+
continue;
|
|
3634
|
+
visit(agent, join9(path, name), isVendor, depth + 1);
|
|
3635
|
+
}
|
|
3636
|
+
}
|
|
3637
|
+
for (const [agent, path] of roots)
|
|
3638
|
+
visit(agent, path);
|
|
3639
|
+
if (options.includeVendor) {
|
|
3640
|
+
for (const agent of ["codex", "claude"])
|
|
3641
|
+
visit(agent, canonicalAgentPath(join9(home, `.${agent}`, "plugins", "cache"), aliases), true);
|
|
3642
|
+
}
|
|
3643
|
+
recheckRootAliases(aliases);
|
|
3644
|
+
return entries;
|
|
3645
|
+
}
|
|
3646
|
+
function readOptional(path) {
|
|
3647
|
+
assertSafePath(path);
|
|
3648
|
+
return existsSync9(path) ? readFileSync7(path, "utf8") : null;
|
|
3649
|
+
}
|
|
3650
|
+
function jsonObject(text, path) {
|
|
3651
|
+
if (text === null)
|
|
3652
|
+
return {};
|
|
3653
|
+
const value = JSON.parse(text);
|
|
3654
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
3655
|
+
throw new Error(`Expected configuration object: ${path}`);
|
|
3656
|
+
return value;
|
|
3657
|
+
}
|
|
3658
|
+
function shellQuote(value) {
|
|
3659
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
3660
|
+
}
|
|
3661
|
+
function configureHooks(config, agent, command, profileId) {
|
|
3662
|
+
config.hooks ??= {};
|
|
3663
|
+
if (typeof config.hooks !== "object" || Array.isArray(config.hooks))
|
|
3664
|
+
throw new Error("Expected hooks configuration object");
|
|
3665
|
+
for (const event of HOOK_EVENTS) {
|
|
3666
|
+
const existing = config.hooks[event] ?? [];
|
|
3667
|
+
if (!Array.isArray(existing))
|
|
3668
|
+
throw new Error(`Expected ${event} hooks array`);
|
|
3669
|
+
const hookCommand = `${shellQuote(command)} hook user-prompt --agent ${agent} --selection-profile ${profileId}`;
|
|
3670
|
+
const retained = existing.flatMap((entry) => {
|
|
3671
|
+
if (!entry || !Array.isArray(entry.hooks))
|
|
3672
|
+
throw new Error(`Malformed ${event} hook entry`);
|
|
3673
|
+
const hooks = entry.hooks.filter((hook) => !(hook?.type === "command" && typeof hook.command === "string" && /(?:^|\s)hook user-prompt --agent (?:claude|codex)(?: --selection-profile [A-Za-z0-9._-]+)?$/.test(hook.command)));
|
|
3674
|
+
return hooks.length ? [{ ...entry, hooks }] : [];
|
|
3675
|
+
});
|
|
3676
|
+
retained.push({ hooks: [{ type: "command", command: hookCommand, timeout: 15 }] });
|
|
3677
|
+
config.hooks[event] = retained;
|
|
3678
|
+
}
|
|
3679
|
+
if (agent === "claude") {
|
|
3680
|
+
config.permissions ??= {};
|
|
3681
|
+
if (typeof config.permissions !== "object" || Array.isArray(config.permissions))
|
|
3682
|
+
throw new Error("Expected permissions configuration object");
|
|
3683
|
+
const denied = config.permissions.deny ?? [];
|
|
3684
|
+
if (!Array.isArray(denied))
|
|
3685
|
+
throw new Error("Expected permission deny array");
|
|
3686
|
+
config.permissions.deny = [...new Set([...denied, "Skill"])];
|
|
3687
|
+
}
|
|
3688
|
+
}
|
|
3689
|
+
function disableCodexSkills(text, skills, aliases) {
|
|
3690
|
+
Bun.TOML.parse(text);
|
|
3691
|
+
let result = text;
|
|
3692
|
+
for (const skill of skills.filter((entry) => entry.agent === "codex")) {
|
|
3693
|
+
const path = join9(skill.path, "SKILL.md");
|
|
3694
|
+
let found = false;
|
|
3695
|
+
result = result.replace(/^\[\[skills\.config\]\][^\n]*(?:\n(?!\s*\[)[^\n]*)*/gm, (section) => {
|
|
3696
|
+
const parsed = Bun.TOML.parse(section);
|
|
3697
|
+
const declared = parsed.skills?.config?.[0]?.path;
|
|
3698
|
+
if (!declared || ![resolve3(path), resolve3(skill.path)].includes(canonicalAgentPath(declared, aliases)))
|
|
3699
|
+
return section;
|
|
3700
|
+
found = true;
|
|
3701
|
+
return /^\s*enabled\s*=/m.test(section) ? section.replace(/^\s*enabled\s*=.*$/m, "enabled = false") : `${section.trimEnd()}
|
|
3702
|
+
enabled = false
|
|
3703
|
+
`;
|
|
3704
|
+
});
|
|
3705
|
+
if (!found)
|
|
3706
|
+
result = `${result.trimEnd()}
|
|
3707
|
+
|
|
3708
|
+
[[skills.config]]
|
|
3709
|
+
path = ${JSON.stringify(path)}
|
|
3710
|
+
enabled = false
|
|
3711
|
+
`;
|
|
3712
|
+
}
|
|
3713
|
+
Bun.TOML.parse(result);
|
|
3714
|
+
return result;
|
|
3715
|
+
}
|
|
3716
|
+
function planAgentIntegration(options) {
|
|
3717
|
+
const home = options.home ?? homedir2(), dataDir2 = options.dataDir ?? getDataDir();
|
|
3718
|
+
const profileId = options.profileId ?? "default";
|
|
3719
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(profileId) || profileId.includes(".."))
|
|
3720
|
+
throw new Error("Invalid selection profile id");
|
|
3721
|
+
const aliases = rootAliases(home, options.allowRootAliases);
|
|
3722
|
+
const nativeSkills = inventoryNativeSkills(home, { includeVendor: options.includeVendor, projectDir: options.projectDir, allowRootAliases: options.allowRootAliases });
|
|
3723
|
+
const changes = [];
|
|
3724
|
+
for (const agent of [...new Set(options.agents)]) {
|
|
3725
|
+
if (agent !== "claude" && agent !== "codex")
|
|
3726
|
+
throw new Error(`Unsupported agent: ${agent}`);
|
|
3727
|
+
const path = canonicalAgentPath(join9(home, `.${agent}`, agent === "claude" ? "settings.json" : "hooks.json"), aliases);
|
|
3728
|
+
const before = readOptional(path), config = jsonObject(before, path);
|
|
3729
|
+
configureHooks(config, agent, options.command ?? "skills", profileId);
|
|
3730
|
+
const after = `${JSON.stringify(config, null, 2)}
|
|
3731
|
+
`;
|
|
3732
|
+
if (before !== after)
|
|
3733
|
+
changes.push({ path, before, after });
|
|
3734
|
+
if (agent === "codex") {
|
|
3735
|
+
const configPath = canonicalAgentPath(join9(home, ".codex", "config.toml"), aliases), previous = readOptional(configPath);
|
|
3736
|
+
const next = disableCodexSkills(previous ?? "", nativeSkills, aliases);
|
|
3737
|
+
if (next !== (previous ?? ""))
|
|
3738
|
+
changes.push({ path: configPath, before: previous, after: next });
|
|
3739
|
+
}
|
|
3740
|
+
}
|
|
3741
|
+
recheckRootAliases(aliases);
|
|
3742
|
+
return { dataDir: dataDir2, profileId, changes, nativeSkills, ...aliases.length ? { rootAliases: aliases } : {} };
|
|
3743
|
+
}
|
|
3744
|
+
function atomicWrite(path, content) {
|
|
3745
|
+
assertSafePath(path);
|
|
3746
|
+
mkdirSync4(dirname4(path), { recursive: true, mode: 448 });
|
|
3747
|
+
const temporary = `${path}.skills-${randomUUID()}`;
|
|
3748
|
+
try {
|
|
3749
|
+
writeFileSync4(temporary, content, { mode: 384, flag: "wx" });
|
|
3750
|
+
renameSync2(temporary, path);
|
|
3751
|
+
} finally {
|
|
3752
|
+
if (existsSync9(temporary))
|
|
3753
|
+
unlinkSync(temporary);
|
|
3754
|
+
}
|
|
3755
|
+
}
|
|
3756
|
+
function applyAgentIntegration(plan) {
|
|
3757
|
+
const aliases = plan.rootAliases ?? [];
|
|
3758
|
+
recheckRootAliases(aliases);
|
|
3759
|
+
for (const change of plan.changes)
|
|
3760
|
+
if (readOptional(change.path) !== change.before)
|
|
3761
|
+
throw new Error(`Configuration changed after planning: ${change.path}`);
|
|
3762
|
+
const backupRoot = join9(plan.dataDir, "migration", randomUUID()), backups = [], written = [];
|
|
3763
|
+
if (!plan.changes.length) {
|
|
3764
|
+
const policyPath = join9(plan.dataDir, "agent-policy.json");
|
|
3765
|
+
const policy = jsonObject(readOptional(policyPath), policyPath);
|
|
3766
|
+
recheckRootAliases(aliases);
|
|
3767
|
+
if (policy.loading !== "cli" || policy.profileId !== plan.profileId)
|
|
3768
|
+
atomicWrite(policyPath, JSON.stringify({ version: 1, loading: "cli", profileId: plan.profileId, installedAt: new Date().toISOString() }) + `
|
|
3769
|
+
`);
|
|
3770
|
+
return { changed: [], backups, ...aliases.length ? { rootAliases: aliases } : {} };
|
|
3771
|
+
}
|
|
3772
|
+
assertSafePath(backupRoot);
|
|
3773
|
+
mkdirSync4(backupRoot, { recursive: true, mode: 448 });
|
|
3774
|
+
chmodSync(backupRoot, 448);
|
|
3775
|
+
for (const [index, change] of plan.changes.entries())
|
|
3776
|
+
if (change.before !== null) {
|
|
3777
|
+
const backup = join9(backupRoot, `${index}-${sha(change.path).slice(0, 12)}.backup`);
|
|
3778
|
+
writeFileSync4(backup, change.before, { mode: 384, flag: "wx" });
|
|
3779
|
+
backups.push(backup);
|
|
3780
|
+
}
|
|
3781
|
+
try {
|
|
3782
|
+
for (const change of plan.changes) {
|
|
3783
|
+
recheckRootAliases(aliases);
|
|
3784
|
+
atomicWrite(change.path, change.after);
|
|
3785
|
+
written.push(change);
|
|
3786
|
+
}
|
|
3787
|
+
recheckRootAliases(aliases);
|
|
3788
|
+
atomicWrite(join9(plan.dataDir, "agent-policy.json"), JSON.stringify({ version: 1, loading: "cli", profileId: plan.profileId, installedAt: new Date().toISOString() }) + `
|
|
3789
|
+
`);
|
|
3790
|
+
} catch (error) {
|
|
3791
|
+
for (const change of written.reverse()) {
|
|
3792
|
+
if (change.before === null)
|
|
3793
|
+
unlinkSync(change.path);
|
|
3794
|
+
else
|
|
3795
|
+
atomicWrite(change.path, change.before);
|
|
3796
|
+
}
|
|
3797
|
+
throw error;
|
|
3798
|
+
}
|
|
3799
|
+
atomicWrite(join9(backupRoot, "receipt.json"), JSON.stringify({ version: 1, changes: plan.changes.map((change) => ({ path: change.path, beforeHash: change.before === null ? null : sha(change.before), afterHash: sha(change.after) })), backups, ...aliases.length ? { rootAliases: aliases } : {} }) + `
|
|
3800
|
+
`);
|
|
3801
|
+
return { changed: plan.changes.map((change) => change.path), backups, ...aliases.length ? { rootAliases: aliases } : {} };
|
|
3802
|
+
}
|
|
3803
|
+
function archiveNativeSkills(inventory, options) {
|
|
3804
|
+
const aliases = [...new Map(inventory.filter((entry) => entry.rootAlias).map((entry) => [entry.rootAlias.alias, entry.rootAlias])).values()];
|
|
3805
|
+
if (aliases.length && !options.allowRootAliases)
|
|
3806
|
+
throw new Error("Native migration requires explicit allowRootAliases for agent root aliases");
|
|
3807
|
+
recheckRootAliases(aliases);
|
|
3808
|
+
const selected = inventory.filter((entry) => !entry.vendor && (entry.managed || options.includeUnmanaged));
|
|
3809
|
+
for (const entry of selected)
|
|
3810
|
+
if (treeHash(entry.path) !== entry.hash)
|
|
3811
|
+
throw new Error(`Native skill changed after planning: ${entry.path}`);
|
|
3812
|
+
if (!selected.length)
|
|
3813
|
+
return { entries: [], ...aliases.length ? { rootAliases: aliases } : {} };
|
|
3814
|
+
const archiveRoot = join9(options.dataDir ?? getDataDir(), "migration", randomUUID(), "native");
|
|
3815
|
+
assertSafePath(archiveRoot);
|
|
3816
|
+
mkdirSync4(archiveRoot, { recursive: true, mode: 448 });
|
|
3817
|
+
const entries = [];
|
|
3818
|
+
try {
|
|
3819
|
+
for (const [index, entry] of selected.entries()) {
|
|
3820
|
+
recheckRootAliases(aliases);
|
|
3821
|
+
const archive = join9(archiveRoot, `${index}-${sha(entry.path).slice(0, 12)}`);
|
|
3822
|
+
renameSync2(entry.path, archive);
|
|
3823
|
+
entries.push({ source: entry.path, archive, hash: entry.hash });
|
|
3824
|
+
}
|
|
3825
|
+
recheckRootAliases(aliases);
|
|
3826
|
+
atomicWrite(join9(archiveRoot, "receipt.json"), JSON.stringify({ version: 1, entries, ...aliases.length ? { rootAliases: aliases } : {} }) + `
|
|
3827
|
+
`);
|
|
3828
|
+
} catch (error) {
|
|
3829
|
+
for (const entry of entries.reverse())
|
|
3830
|
+
renameSync2(entry.archive, entry.source);
|
|
3831
|
+
throw error;
|
|
3832
|
+
}
|
|
3833
|
+
return { entries, ...aliases.length ? { rootAliases: aliases } : {} };
|
|
3834
|
+
}
|
|
3835
|
+
function assertNativeExportAllowed(dataDir2 = getDataDir()) {
|
|
3836
|
+
const path = join9(dataDir2, "agent-policy.json");
|
|
3837
|
+
if (!existsSync9(path))
|
|
3838
|
+
return;
|
|
3839
|
+
const policy = jsonObject(readOptional(path), path);
|
|
3840
|
+
if (policy.loading === "cli")
|
|
3841
|
+
throw new Error("NATIVE_SKILL_EXPORT_DISABLED: this station loads skills through the Skills CLI; use skills sync --selection-profile <id>");
|
|
3842
|
+
}
|
|
3471
3843
|
// src/lib/home-migration.ts
|
|
3472
3844
|
function resolveCorpusRoot(options = {}) {
|
|
3473
3845
|
return getPortableSkillsRoot(options);
|
|
@@ -3482,11 +3854,11 @@ function isSkillsOwnershipMarker(marker) {
|
|
|
3482
3854
|
return typeof marker === "object" && marker !== null && Object.hasOwn(marker, "managedBy") && marker.managedBy === SYNC_MARKER_MANAGED_BY;
|
|
3483
3855
|
}
|
|
3484
3856
|
function hasSkillsOwnershipMarker(dir) {
|
|
3485
|
-
const path =
|
|
3857
|
+
const path = join10(dir, SYNC_MARKER_FILE);
|
|
3486
3858
|
try {
|
|
3487
|
-
if (!
|
|
3859
|
+
if (!lstatSync4(path).isFile())
|
|
3488
3860
|
return false;
|
|
3489
|
-
const marker = JSON.parse(
|
|
3861
|
+
const marker = JSON.parse(readFileSync8(path, "utf8"));
|
|
3490
3862
|
return isSkillsOwnershipMarker(marker);
|
|
3491
3863
|
} catch {
|
|
3492
3864
|
return false;
|
|
@@ -3503,12 +3875,12 @@ function resolveSyncAgents(arg) {
|
|
|
3503
3875
|
}
|
|
3504
3876
|
return [arg];
|
|
3505
3877
|
}
|
|
3506
|
-
function agentGlobalSkillsDir(agent, homeDir =
|
|
3878
|
+
function agentGlobalSkillsDir(agent, homeDir = homedir3()) {
|
|
3507
3879
|
switch (agent) {
|
|
3508
3880
|
case "opencode":
|
|
3509
|
-
return
|
|
3881
|
+
return join10(homeDir, ".config", "opencode", "skills");
|
|
3510
3882
|
default:
|
|
3511
|
-
return
|
|
3883
|
+
return join10(homeDir, `.${agent}`, "skills");
|
|
3512
3884
|
}
|
|
3513
3885
|
}
|
|
3514
3886
|
function adaptSkillMdForAgent(skillMd, agent) {
|
|
@@ -3567,8 +3939,8 @@ function resolveSyncCorpus(options = {}) {
|
|
|
3567
3939
|
function packageSourceRoots(source) {
|
|
3568
3940
|
const roots = [];
|
|
3569
3941
|
for (const sub of ["skills"]) {
|
|
3570
|
-
const candidate =
|
|
3571
|
-
if (
|
|
3942
|
+
const candidate = join10(source, sub);
|
|
3943
|
+
if (existsSync10(candidate) && isDirectory(candidate))
|
|
3572
3944
|
roots.push(candidate);
|
|
3573
3945
|
}
|
|
3574
3946
|
if (roots.length > 0)
|
|
@@ -3578,15 +3950,15 @@ function packageSourceRoots(source) {
|
|
|
3578
3950
|
function containsSkillDirectories(path) {
|
|
3579
3951
|
let entries;
|
|
3580
3952
|
try {
|
|
3581
|
-
entries =
|
|
3953
|
+
entries = readdirSync8(path);
|
|
3582
3954
|
} catch {
|
|
3583
3955
|
return false;
|
|
3584
3956
|
}
|
|
3585
3957
|
return entries.some((entry) => {
|
|
3586
|
-
const candidate =
|
|
3958
|
+
const candidate = join10(path, entry);
|
|
3587
3959
|
if (!isDirectory(candidate))
|
|
3588
3960
|
return false;
|
|
3589
|
-
return
|
|
3961
|
+
return existsSync10(join10(candidate, "SKILL.md")) || existsSync10(join10(candidate, "skill.json")) || existsSync10(join10(candidate, "package.json"));
|
|
3590
3962
|
});
|
|
3591
3963
|
}
|
|
3592
3964
|
function isDirectory(path) {
|
|
@@ -3597,9 +3969,10 @@ function isDirectory(path) {
|
|
|
3597
3969
|
}
|
|
3598
3970
|
}
|
|
3599
3971
|
function syncSkillsToAgents(options = {}) {
|
|
3972
|
+
assertNativeExportAllowed();
|
|
3600
3973
|
const requested = normalizeRequested(options.names);
|
|
3601
3974
|
const agents = options.agents?.length ? options.agents : [...SYNC_AGENTS];
|
|
3602
|
-
const homeDir = options.homeDir ??
|
|
3975
|
+
const homeDir = options.homeDir ?? homedir3();
|
|
3603
3976
|
const { roots, source } = resolveSyncCorpus(options);
|
|
3604
3977
|
const corpus = listPortableSkillsAcrossRoots(roots);
|
|
3605
3978
|
const byName = new Map(corpus.map((skill) => [skill.name, skill]));
|
|
@@ -3625,7 +3998,7 @@ function syncSkillsToAgents(options = {}) {
|
|
|
3625
3998
|
actions.push({
|
|
3626
3999
|
skill: name,
|
|
3627
4000
|
agent,
|
|
3628
|
-
path:
|
|
4001
|
+
path: join10(agentGlobalSkillsDir(agent, homeDir), name, "SKILL.md"),
|
|
3629
4002
|
action: "skip",
|
|
3630
4003
|
reason: "not found in this machine's corpus"
|
|
3631
4004
|
});
|
|
@@ -3654,8 +4027,8 @@ function syncSkillsToAgents(options = {}) {
|
|
|
3654
4027
|
return { actions };
|
|
3655
4028
|
}
|
|
3656
4029
|
function writeManagedAgentSkill(params) {
|
|
3657
|
-
const homeDir = params.homeDir ??
|
|
3658
|
-
const dir =
|
|
4030
|
+
const homeDir = params.homeDir ?? homedir3();
|
|
4031
|
+
const dir = join10(agentGlobalSkillsDir(params.agent, homeDir), params.skill);
|
|
3659
4032
|
const result = writeManagedSkillDir(dir, params.skillMd, {
|
|
3660
4033
|
skill: params.skill,
|
|
3661
4034
|
source: params.source,
|
|
@@ -3672,11 +4045,11 @@ function writeManagedAgentSkill(params) {
|
|
|
3672
4045
|
};
|
|
3673
4046
|
}
|
|
3674
4047
|
function writeManagedSkillDir(dir, skillMd, options) {
|
|
3675
|
-
const skillMdPath =
|
|
3676
|
-
const markerPath =
|
|
3677
|
-
const dirExists =
|
|
4048
|
+
const skillMdPath = join10(dir, "SKILL.md");
|
|
4049
|
+
const markerPath = join10(dir, SYNC_MARKER_FILE);
|
|
4050
|
+
const dirExists = existsSync10(dir);
|
|
3678
4051
|
const managed = hasSkillsOwnershipMarker(dir);
|
|
3679
|
-
const hasSkillMd =
|
|
4052
|
+
const hasSkillMd = existsSync10(skillMdPath);
|
|
3680
4053
|
if (dirExists && !managed && !hasSkillMd) {
|
|
3681
4054
|
return {
|
|
3682
4055
|
action: "skip",
|
|
@@ -3688,13 +4061,13 @@ function writeManagedSkillDir(dir, skillMd, options) {
|
|
|
3688
4061
|
return {
|
|
3689
4062
|
action: "skip",
|
|
3690
4063
|
path: skillMdPath,
|
|
3691
|
-
reason:
|
|
4064
|
+
reason: existsSync10(markerPath) ? "an unmanaged SKILL.md already exists here (invalid or foreign ownership marker); pass --force to overwrite" : "an unmanaged SKILL.md already exists here (hand-authored); pass --force to overwrite"
|
|
3692
4065
|
};
|
|
3693
4066
|
}
|
|
3694
4067
|
if (dirExists && managed && hasSkillMd && !options.force && isPointerSkillMd(skillMd)) {
|
|
3695
4068
|
let existingIsStub = false;
|
|
3696
4069
|
try {
|
|
3697
|
-
existingIsStub = isPointerSkillMd(
|
|
4070
|
+
existingIsStub = isPointerSkillMd(readFileSync8(skillMdPath, "utf-8"));
|
|
3698
4071
|
} catch {
|
|
3699
4072
|
existingIsStub = false;
|
|
3700
4073
|
}
|
|
@@ -3709,32 +4082,32 @@ function writeManagedSkillDir(dir, skillMd, options) {
|
|
|
3709
4082
|
const action = dirExists ? "update" : "create";
|
|
3710
4083
|
if (options.dryRun)
|
|
3711
4084
|
return { action, path: skillMdPath };
|
|
3712
|
-
const parentDir =
|
|
3713
|
-
|
|
3714
|
-
const transactionDir = mkdtempSync2(
|
|
3715
|
-
const candidateDir =
|
|
3716
|
-
const backupDir =
|
|
3717
|
-
const candidateSkillMdPath =
|
|
3718
|
-
const candidateMarkerPath =
|
|
4085
|
+
const parentDir = dirname5(dir);
|
|
4086
|
+
mkdirSync5(parentDir, { recursive: true });
|
|
4087
|
+
const transactionDir = mkdtempSync2(join10(parentDir, `.hasna-skills-write-${basename3(dir)}-`));
|
|
4088
|
+
const candidateDir = join10(transactionDir, "candidate");
|
|
4089
|
+
const backupDir = join10(transactionDir, "backup");
|
|
4090
|
+
const candidateSkillMdPath = join10(candidateDir, "SKILL.md");
|
|
4091
|
+
const candidateMarkerPath = join10(candidateDir, SYNC_MARKER_FILE);
|
|
3719
4092
|
const marker = {
|
|
3720
4093
|
managedBy: SYNC_MARKER_MANAGED_BY,
|
|
3721
4094
|
skill: options.skill,
|
|
3722
4095
|
source: options.source ?? "corpus",
|
|
3723
4096
|
syncedAt: new Date().toISOString()
|
|
3724
4097
|
};
|
|
3725
|
-
const renameDirectory = options.renameDirectory ??
|
|
4098
|
+
const renameDirectory = options.renameDirectory ?? renameSync3;
|
|
3726
4099
|
let originalMoved = false;
|
|
3727
4100
|
let preserveTransaction = false;
|
|
3728
4101
|
try {
|
|
3729
4102
|
if (options.resourceDir) {
|
|
3730
4103
|
cpSync3(options.resourceDir, candidateDir, { recursive: true, force: true });
|
|
3731
4104
|
} else {
|
|
3732
|
-
|
|
4105
|
+
mkdirSync5(candidateDir, { recursive: true });
|
|
3733
4106
|
}
|
|
3734
|
-
|
|
4107
|
+
writeFileSync5(candidateSkillMdPath, skillMd.endsWith(`
|
|
3735
4108
|
`) ? skillMd : `${skillMd}
|
|
3736
4109
|
`);
|
|
3737
|
-
|
|
4110
|
+
writeFileSync5(candidateMarkerPath, `${JSON.stringify(marker, null, 2)}
|
|
3738
4111
|
`);
|
|
3739
4112
|
if (dirExists) {
|
|
3740
4113
|
originalMoved = true;
|
|
@@ -3742,9 +4115,9 @@ function writeManagedSkillDir(dir, skillMd, options) {
|
|
|
3742
4115
|
}
|
|
3743
4116
|
renameDirectory(candidateDir, dir);
|
|
3744
4117
|
} catch (error) {
|
|
3745
|
-
if (originalMoved &&
|
|
4118
|
+
if (originalMoved && existsSync10(backupDir)) {
|
|
3746
4119
|
try {
|
|
3747
|
-
if (
|
|
4120
|
+
if (existsSync10(dir))
|
|
3748
4121
|
rmSync2(dir, { recursive: true, force: true });
|
|
3749
4122
|
renameDirectory(backupDir, dir);
|
|
3750
4123
|
originalMoved = false;
|
|
@@ -3763,8 +4136,8 @@ function writeManagedSkillDir(dir, skillMd, options) {
|
|
|
3763
4136
|
}
|
|
3764
4137
|
return { action, path: skillMdPath };
|
|
3765
4138
|
}
|
|
3766
|
-
function removeManagedAgentSkill(skill, agent, homeDir =
|
|
3767
|
-
const dir =
|
|
4139
|
+
function removeManagedAgentSkill(skill, agent, homeDir = homedir3()) {
|
|
4140
|
+
const dir = join10(agentGlobalSkillsDir(agent, homeDir), skill);
|
|
3768
4141
|
if (!hasSkillsOwnershipMarker(dir))
|
|
3769
4142
|
return false;
|
|
3770
4143
|
rmSync2(dir, { recursive: true, force: true });
|
|
@@ -3772,9 +4145,9 @@ function removeManagedAgentSkill(skill, agent, homeDir = homedir2()) {
|
|
|
3772
4145
|
}
|
|
3773
4146
|
function sourceSkillMd(skillPath, name, description, kind, preferBundledDocs = false) {
|
|
3774
4147
|
if (kind === undefined || kind === "instruction" || preferBundledDocs) {
|
|
3775
|
-
const skillMdPath =
|
|
3776
|
-
if (
|
|
3777
|
-
return
|
|
4148
|
+
const skillMdPath = join10(skillPath, "SKILL.md");
|
|
4149
|
+
if (existsSync10(skillMdPath))
|
|
4150
|
+
return readFileSync8(skillMdPath, "utf-8");
|
|
3778
4151
|
}
|
|
3779
4152
|
return pointerSkillMd(name, description);
|
|
3780
4153
|
}
|
|
@@ -3804,8 +4177,8 @@ function normalizeSkillName(name) {
|
|
|
3804
4177
|
}
|
|
3805
4178
|
|
|
3806
4179
|
// src/lib/project-state.ts
|
|
3807
|
-
import { existsSync as
|
|
3808
|
-
import { join as
|
|
4180
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync6, readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "fs";
|
|
4181
|
+
import { join as join11 } from "path";
|
|
3809
4182
|
var VALID_PIN_SOURCES = [
|
|
3810
4183
|
"official",
|
|
3811
4184
|
"custom",
|
|
@@ -3820,17 +4193,17 @@ var SKILLS_PROJECT_DIR = ".skills";
|
|
|
3820
4193
|
var PROJECT_CONFIG_FILE = "project.json";
|
|
3821
4194
|
var DEFAULT_EXPORT_DIR = ".skills/exports";
|
|
3822
4195
|
function getProjectStateDir(targetDir = process.cwd()) {
|
|
3823
|
-
return
|
|
4196
|
+
return join11(targetDir, SKILLS_PROJECT_DIR);
|
|
3824
4197
|
}
|
|
3825
4198
|
function getProjectConfigPath(targetDir = process.cwd()) {
|
|
3826
|
-
return
|
|
4199
|
+
return join11(getProjectStateDir(targetDir), PROJECT_CONFIG_FILE);
|
|
3827
4200
|
}
|
|
3828
4201
|
function loadProjectConfig(targetDir = process.cwd()) {
|
|
3829
4202
|
const path = getProjectConfigPath(targetDir);
|
|
3830
|
-
if (!
|
|
4203
|
+
if (!existsSync11(path))
|
|
3831
4204
|
return null;
|
|
3832
4205
|
try {
|
|
3833
|
-
return normalizeProjectConfig(JSON.parse(
|
|
4206
|
+
return normalizeProjectConfig(JSON.parse(readFileSync9(path, "utf-8")));
|
|
3834
4207
|
} catch {
|
|
3835
4208
|
return null;
|
|
3836
4209
|
}
|
|
@@ -3851,9 +4224,9 @@ function ensureProjectConfig(targetDir = process.cwd()) {
|
|
|
3851
4224
|
}
|
|
3852
4225
|
function saveProjectConfig(config, targetDir = process.cwd()) {
|
|
3853
4226
|
const dir = getProjectStateDir(targetDir);
|
|
3854
|
-
|
|
4227
|
+
mkdirSync6(dir, { recursive: true });
|
|
3855
4228
|
const normalized = normalizeProjectConfig({ ...config, updatedAt: new Date().toISOString() });
|
|
3856
|
-
|
|
4229
|
+
writeFileSync6(getProjectConfigPath(targetDir), JSON.stringify(normalized, null, 2) + `
|
|
3857
4230
|
`);
|
|
3858
4231
|
}
|
|
3859
4232
|
function pinProjectSkill(name, details = {}, targetDir = process.cwd()) {
|
|
@@ -3944,16 +4317,16 @@ function isPinSource(value) {
|
|
|
3944
4317
|
}
|
|
3945
4318
|
|
|
3946
4319
|
// src/lib/installer.ts
|
|
3947
|
-
var __dirname2 =
|
|
4320
|
+
var __dirname2 = dirname6(fileURLToPath(import.meta.url));
|
|
3948
4321
|
function findSkillsDir() {
|
|
3949
4322
|
let dir = __dirname2;
|
|
3950
4323
|
for (let i = 0;i < 5; i++) {
|
|
3951
|
-
const candidate =
|
|
3952
|
-
if (
|
|
4324
|
+
const candidate = join12(dir, "skills");
|
|
4325
|
+
if (existsSync12(candidate) && !dir.includes(".skills"))
|
|
3953
4326
|
return candidate;
|
|
3954
|
-
dir =
|
|
4327
|
+
dir = dirname6(dir);
|
|
3955
4328
|
}
|
|
3956
|
-
return
|
|
4329
|
+
return join12(__dirname2, "..", "skills");
|
|
3957
4330
|
}
|
|
3958
4331
|
var SKILLS_DIR = findSkillsDir();
|
|
3959
4332
|
function getSkillPath(name) {
|
|
@@ -3961,25 +4334,25 @@ function getSkillPath(name) {
|
|
|
3961
4334
|
const portable = findPortableSkill(skillName);
|
|
3962
4335
|
if (portable)
|
|
3963
4336
|
return portable.path;
|
|
3964
|
-
const legacyCustomPath =
|
|
3965
|
-
if (
|
|
4337
|
+
const legacyCustomPath = join12(getDataDir(), "custom", skillName);
|
|
4338
|
+
if (existsSync12(legacyCustomPath))
|
|
3966
4339
|
return legacyCustomPath;
|
|
3967
4340
|
const extensionPath = findExtensionSkillPath(skillName);
|
|
3968
4341
|
if (extensionPath)
|
|
3969
4342
|
return extensionPath;
|
|
3970
|
-
return
|
|
4343
|
+
return join12(SKILLS_DIR, skillName);
|
|
3971
4344
|
}
|
|
3972
4345
|
function getCanonicalSkillName(name) {
|
|
3973
4346
|
return getSkill(name)?.name ?? resolveSkillAlias(normalizeSkillSlug(name));
|
|
3974
4347
|
}
|
|
3975
4348
|
function skillExists(name) {
|
|
3976
|
-
return
|
|
4349
|
+
return existsSync12(getSkillPath(name));
|
|
3977
4350
|
}
|
|
3978
4351
|
function installSkill(name, options = {}) {
|
|
3979
4352
|
const { targetDir = process.cwd(), overwrite = false } = options;
|
|
3980
4353
|
const canonicalName = getCanonicalSkillName(name);
|
|
3981
4354
|
const skillName = normalizeSkillName(canonicalName);
|
|
3982
|
-
if (!
|
|
4355
|
+
if (!existsSync12(getSkillPath(name))) {
|
|
3983
4356
|
const knownOfficial = Boolean(getSkill(name));
|
|
3984
4357
|
return {
|
|
3985
4358
|
skill: canonicalName,
|
|
@@ -4006,7 +4379,7 @@ function installSkill(name, options = {}) {
|
|
|
4006
4379
|
}
|
|
4007
4380
|
function installSkillSource(name, _options = {}) {
|
|
4008
4381
|
const canonicalName = getCanonicalSkillName(name);
|
|
4009
|
-
if (!
|
|
4382
|
+
if (!existsSync12(getSkillPath(name))) {
|
|
4010
4383
|
return { skill: canonicalName, success: false, error: `Skill '${name}' not found`, mode: "source" };
|
|
4011
4384
|
}
|
|
4012
4385
|
return {
|
|
@@ -4027,12 +4400,12 @@ function installSkillManifest(manifest, _options = {}) {
|
|
|
4027
4400
|
}
|
|
4028
4401
|
function createLocalSkillManifest(name, generateSkillMd) {
|
|
4029
4402
|
const sourcePath = getSkillPath(name);
|
|
4030
|
-
if (!
|
|
4403
|
+
if (!existsSync12(sourcePath))
|
|
4031
4404
|
return null;
|
|
4032
4405
|
let skillMd = "";
|
|
4033
|
-
const skillMdPath =
|
|
4034
|
-
if (
|
|
4035
|
-
skillMd =
|
|
4406
|
+
const skillMdPath = join12(sourcePath, "SKILL.md");
|
|
4407
|
+
if (existsSync12(skillMdPath)) {
|
|
4408
|
+
skillMd = readFileSync10(skillMdPath, "utf-8");
|
|
4036
4409
|
} else if (generateSkillMd) {
|
|
4037
4410
|
skillMd = generateSkillMd(name) ?? "";
|
|
4038
4411
|
} else {
|
|
@@ -4104,20 +4477,21 @@ function getAgentSkillsDir(agent, scope = "global", projectDir) {
|
|
|
4104
4477
|
const base = projectDir || process.cwd();
|
|
4105
4478
|
switch (agent) {
|
|
4106
4479
|
case "pi":
|
|
4107
|
-
return scope === "project" ?
|
|
4480
|
+
return scope === "project" ? join12(base, ".pi", "skills") : join12(homedir4(), ".pi", "agent", "skills");
|
|
4108
4481
|
case "opencode":
|
|
4109
|
-
return scope === "project" ?
|
|
4482
|
+
return scope === "project" ? join12(base, ".opencode", "skills") : join12(homedir4(), ".config", "opencode", "skills");
|
|
4110
4483
|
default:
|
|
4111
|
-
return scope === "project" ?
|
|
4484
|
+
return scope === "project" ? join12(base, `.${agent}`, "skills") : join12(homedir4(), `.${agent}`, "skills");
|
|
4112
4485
|
}
|
|
4113
4486
|
}
|
|
4114
4487
|
function getAgentSkillPath(name, agent, scope = "global", projectDir) {
|
|
4115
4488
|
const skillName = normalizeSkillName(getCanonicalSkillName(name));
|
|
4116
|
-
return
|
|
4489
|
+
return join12(getAgentSkillsDir(agent, scope, projectDir), skillName);
|
|
4117
4490
|
}
|
|
4118
4491
|
function installSkillForAgent(name, options, generateSkillMd) {
|
|
4492
|
+
assertNativeExportAllowed();
|
|
4119
4493
|
const canonicalName = getCanonicalSkillName(name);
|
|
4120
|
-
if (!
|
|
4494
|
+
if (!existsSync12(getSkillPath(name))) {
|
|
4121
4495
|
return { skill: canonicalName, success: false, error: `Skill '${name}' not found` };
|
|
4122
4496
|
}
|
|
4123
4497
|
const scope = options.scope ?? "global";
|
|
@@ -4148,9 +4522,9 @@ function removeSkillForAgent(name, options) {
|
|
|
4148
4522
|
}
|
|
4149
4523
|
function resolveAgentSkillMd(name, generateSkillMd) {
|
|
4150
4524
|
const sourcePath = getSkillPath(name);
|
|
4151
|
-
const skillMdPath =
|
|
4152
|
-
if (
|
|
4153
|
-
return
|
|
4525
|
+
const skillMdPath = join12(sourcePath, "SKILL.md");
|
|
4526
|
+
if (existsSync12(skillMdPath))
|
|
4527
|
+
return readFileSync10(skillMdPath, "utf-8");
|
|
4154
4528
|
if (generateSkillMd)
|
|
4155
4529
|
return generateSkillMd(name);
|
|
4156
4530
|
return generateMinimalSkillMd(name);
|
|
@@ -4168,7 +4542,7 @@ function warnMissingDependencies(name, targetDir) {
|
|
|
4168
4542
|
}
|
|
4169
4543
|
function generateMinimalSkillMd(name) {
|
|
4170
4544
|
const sourcePath = getSkillPath(name);
|
|
4171
|
-
if (!
|
|
4545
|
+
if (!existsSync12(sourcePath))
|
|
4172
4546
|
return null;
|
|
4173
4547
|
const canonicalName = getCanonicalSkillName(name);
|
|
4174
4548
|
const meta = getSkill(canonicalName);
|
|
@@ -4183,7 +4557,7 @@ function generateMinimalSkillMd(name) {
|
|
|
4183
4557
|
"---",
|
|
4184
4558
|
""
|
|
4185
4559
|
].filter(Boolean);
|
|
4186
|
-
const fallbackDoc = readFileIfExists(
|
|
4560
|
+
const fallbackDoc = readFileIfExists(join12(sourcePath, "README.md")) || readFileIfExists(join12(sourcePath, "CLAUDE.md"));
|
|
4187
4561
|
if (fallbackDoc)
|
|
4188
4562
|
return `${frontmatter.join(`
|
|
4189
4563
|
`)}${fallbackDoc.trim()}
|
|
@@ -4202,38 +4576,38 @@ skills run ${canonicalName}
|
|
|
4202
4576
|
`;
|
|
4203
4577
|
}
|
|
4204
4578
|
function readBundledSkillVersion(name) {
|
|
4205
|
-
const pkgPath =
|
|
4206
|
-
if (!
|
|
4579
|
+
const pkgPath = join12(getSkillPath(name), "package.json");
|
|
4580
|
+
if (!existsSync12(pkgPath))
|
|
4207
4581
|
return "unknown";
|
|
4208
4582
|
try {
|
|
4209
|
-
const pkg = JSON.parse(
|
|
4583
|
+
const pkg = JSON.parse(readFileSync10(pkgPath, "utf-8"));
|
|
4210
4584
|
return pkg.version || "unknown";
|
|
4211
4585
|
} catch {
|
|
4212
4586
|
return "unknown";
|
|
4213
4587
|
}
|
|
4214
4588
|
}
|
|
4215
4589
|
function readFileIfExists(path) {
|
|
4216
|
-
return
|
|
4590
|
+
return existsSync12(path) ? readFileSync10(path, "utf-8") : null;
|
|
4217
4591
|
}
|
|
4218
4592
|
function loadProjectConfigCompat(targetDir) {
|
|
4219
4593
|
return loadProjectConfig(targetDir);
|
|
4220
4594
|
}
|
|
4221
4595
|
// src/lib/run-state.ts
|
|
4222
|
-
import { createHash as
|
|
4223
|
-
import { existsSync as
|
|
4224
|
-
import { extname, join as
|
|
4596
|
+
import { createHash as createHash3, randomBytes } from "crypto";
|
|
4597
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync7, readFileSync as readFileSync11, readdirSync as readdirSync9, statSync as statSync8, writeFileSync as writeFileSync7 } from "fs";
|
|
4598
|
+
import { extname, join as join13, relative as relative4 } from "path";
|
|
4225
4599
|
function createSkillRun(params, targetDir = process.cwd()) {
|
|
4226
4600
|
const now = new Date;
|
|
4227
4601
|
const id = createRunId(now);
|
|
4228
4602
|
const day = now.toISOString().slice(0, 10);
|
|
4229
4603
|
const skillName = normalizeSkillName(params.skill);
|
|
4230
4604
|
const root = getProjectStateDir(targetDir);
|
|
4231
|
-
const runDir =
|
|
4232
|
-
const logsDir =
|
|
4233
|
-
const exportDir =
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4605
|
+
const runDir = join13(root, "runs", day, id);
|
|
4606
|
+
const logsDir = join13(runDir, "logs");
|
|
4607
|
+
const exportDir = join13(root, "exports", skillName, id);
|
|
4608
|
+
mkdirSync7(logsDir, { recursive: true });
|
|
4609
|
+
mkdirSync7(exportDir, { recursive: true });
|
|
4610
|
+
mkdirSync7(join13(root, "tmp"), { recursive: true });
|
|
4237
4611
|
const record = {
|
|
4238
4612
|
id,
|
|
4239
4613
|
skill: skillName,
|
|
@@ -4285,27 +4659,27 @@ function updateSkillRun(context, patch) {
|
|
|
4285
4659
|
return context.record;
|
|
4286
4660
|
}
|
|
4287
4661
|
function writeRunLogs(context, stdout = "", stderr = "") {
|
|
4288
|
-
|
|
4289
|
-
|
|
4662
|
+
writeFileSync7(join13(context.logsDir, "stdout.log"), stdout);
|
|
4663
|
+
writeFileSync7(join13(context.logsDir, "stderr.log"), stderr);
|
|
4290
4664
|
}
|
|
4291
4665
|
function appendRunEvent(context, event, data = {}) {
|
|
4292
4666
|
const line = JSON.stringify({ ts: new Date().toISOString(), event, ...data }) + `
|
|
4293
4667
|
`;
|
|
4294
|
-
const path =
|
|
4295
|
-
const previous =
|
|
4296
|
-
|
|
4668
|
+
const path = join13(context.runDir, "events.ndjson");
|
|
4669
|
+
const previous = existsSync13(path) ? readFileSync11(path, "utf-8") : "";
|
|
4670
|
+
writeFileSync7(path, previous + line);
|
|
4297
4671
|
}
|
|
4298
4672
|
function listSkillRuns(targetDir = process.cwd(), limit = 50) {
|
|
4299
|
-
const runsRoot =
|
|
4300
|
-
if (!
|
|
4673
|
+
const runsRoot = join13(getProjectStateDir(targetDir), "runs");
|
|
4674
|
+
if (!existsSync13(runsRoot))
|
|
4301
4675
|
return [];
|
|
4302
4676
|
const records = [];
|
|
4303
|
-
for (const day of
|
|
4304
|
-
const dayDir =
|
|
4677
|
+
for (const day of readdirSync9(runsRoot).sort().reverse()) {
|
|
4678
|
+
const dayDir = join13(runsRoot, day);
|
|
4305
4679
|
if (!statSync8(dayDir).isDirectory())
|
|
4306
4680
|
continue;
|
|
4307
|
-
for (const runId of
|
|
4308
|
-
const record = readRunRecord(
|
|
4681
|
+
for (const runId of readdirSync9(dayDir).sort().reverse()) {
|
|
4682
|
+
const record = readRunRecord(join13(dayDir, runId));
|
|
4309
4683
|
if (record)
|
|
4310
4684
|
records.push(record);
|
|
4311
4685
|
if (records.length >= limit)
|
|
@@ -4315,57 +4689,57 @@ function listSkillRuns(targetDir = process.cwd(), limit = 50) {
|
|
|
4315
4689
|
return records;
|
|
4316
4690
|
}
|
|
4317
4691
|
function findSkillRun(runId, targetDir = process.cwd()) {
|
|
4318
|
-
const runsRoot =
|
|
4319
|
-
if (!
|
|
4692
|
+
const runsRoot = join13(getProjectStateDir(targetDir), "runs");
|
|
4693
|
+
if (!existsSync13(runsRoot))
|
|
4320
4694
|
return null;
|
|
4321
|
-
for (const day of
|
|
4322
|
-
const record = readRunRecord(
|
|
4695
|
+
for (const day of readdirSync9(runsRoot)) {
|
|
4696
|
+
const record = readRunRecord(join13(runsRoot, day, runId));
|
|
4323
4697
|
if (record)
|
|
4324
4698
|
return record;
|
|
4325
4699
|
}
|
|
4326
4700
|
return null;
|
|
4327
4701
|
}
|
|
4328
4702
|
function getRunExportDir(runId, skill, targetDir = process.cwd()) {
|
|
4329
|
-
return
|
|
4703
|
+
return join13(getProjectStateDir(targetDir), "exports", normalizeSkillName(skill), runId);
|
|
4330
4704
|
}
|
|
4331
4705
|
function writeRunRecord(context) {
|
|
4332
|
-
|
|
4706
|
+
writeFileSync7(join13(context.runDir, "run.json"), JSON.stringify(context.record, null, 2) + `
|
|
4333
4707
|
`);
|
|
4334
4708
|
}
|
|
4335
4709
|
function writeArtifactsManifest(context, artifacts) {
|
|
4336
|
-
|
|
4710
|
+
writeFileSync7(join13(context.runDir, "artifacts.json"), JSON.stringify({ runId: context.record.id, artifacts }, null, 2) + `
|
|
4337
4711
|
`);
|
|
4338
4712
|
}
|
|
4339
4713
|
function collectRunArtifacts(context) {
|
|
4340
|
-
if (!
|
|
4714
|
+
if (!existsSync13(context.exportDir))
|
|
4341
4715
|
return [];
|
|
4342
4716
|
const artifacts = [];
|
|
4343
4717
|
for (const path of walkFiles(context.exportDir)) {
|
|
4344
4718
|
const stat = statSync8(path);
|
|
4345
|
-
const bytes =
|
|
4719
|
+
const bytes = readFileSync11(path);
|
|
4346
4720
|
artifacts.push({
|
|
4347
4721
|
path: toProjectRelative(context.targetDir, path),
|
|
4348
4722
|
mime: mimeForPath(path),
|
|
4349
|
-
sha256:
|
|
4723
|
+
sha256: createHash3("sha256").update(bytes).digest("hex"),
|
|
4350
4724
|
sizeBytes: stat.size
|
|
4351
4725
|
});
|
|
4352
4726
|
}
|
|
4353
4727
|
return artifacts.sort((a, b) => a.path.localeCompare(b.path));
|
|
4354
4728
|
}
|
|
4355
4729
|
function readRunRecord(runDir) {
|
|
4356
|
-
const path =
|
|
4357
|
-
if (!
|
|
4730
|
+
const path = join13(runDir, "run.json");
|
|
4731
|
+
if (!existsSync13(path))
|
|
4358
4732
|
return null;
|
|
4359
4733
|
try {
|
|
4360
|
-
return JSON.parse(
|
|
4734
|
+
return JSON.parse(readFileSync11(path, "utf-8"));
|
|
4361
4735
|
} catch {
|
|
4362
4736
|
return null;
|
|
4363
4737
|
}
|
|
4364
4738
|
}
|
|
4365
4739
|
function walkFiles(dir) {
|
|
4366
4740
|
const files = [];
|
|
4367
|
-
for (const entry of
|
|
4368
|
-
const full =
|
|
4741
|
+
for (const entry of readdirSync9(dir)) {
|
|
4742
|
+
const full = join13(dir, entry);
|
|
4369
4743
|
if (statSync8(full).isDirectory())
|
|
4370
4744
|
files.push(...walkFiles(full));
|
|
4371
4745
|
else
|
|
@@ -4377,7 +4751,7 @@ function createRunId(now) {
|
|
|
4377
4751
|
return `run_${now.getTime().toString(36)}_${randomBytes(4).toString("hex")}`;
|
|
4378
4752
|
}
|
|
4379
4753
|
function toProjectRelative(targetDir, path) {
|
|
4380
|
-
const rel =
|
|
4754
|
+
const rel = relative4(targetDir, path).split(/[\\/]/).join("/");
|
|
4381
4755
|
return rel.startsWith(".") ? rel : `./${rel}`;
|
|
4382
4756
|
}
|
|
4383
4757
|
function mimeForPath(path) {
|
|
@@ -4411,16 +4785,16 @@ function mimeForPath(path) {
|
|
|
4411
4785
|
}
|
|
4412
4786
|
}
|
|
4413
4787
|
// src/lib/skillinfo.ts
|
|
4414
|
-
import { existsSync as
|
|
4415
|
-
import { join as
|
|
4788
|
+
import { existsSync as existsSync14, readFileSync as readFileSync12 } from "fs";
|
|
4789
|
+
import { join as join14 } from "path";
|
|
4416
4790
|
function isInstructionSkillDir(skillPath, meta) {
|
|
4417
4791
|
if (meta?.kind === "instruction")
|
|
4418
4792
|
return true;
|
|
4419
|
-
const skillMdPath =
|
|
4420
|
-
if (!
|
|
4793
|
+
const skillMdPath = join14(skillPath, "SKILL.md");
|
|
4794
|
+
if (!existsSync14(skillMdPath))
|
|
4421
4795
|
return false;
|
|
4422
4796
|
try {
|
|
4423
|
-
return parseSkillFrontmatter(
|
|
4797
|
+
return parseSkillFrontmatter(readFileSync12(skillMdPath, "utf-8"))?.kind === "instruction";
|
|
4424
4798
|
} catch {
|
|
4425
4799
|
return false;
|
|
4426
4800
|
}
|
|
@@ -4443,12 +4817,12 @@ var HOSTED_PROVIDER_ENV_PREFIXES = [
|
|
|
4443
4817
|
];
|
|
4444
4818
|
function getSkillDocs(name) {
|
|
4445
4819
|
const skillPath = getSkillPath(name);
|
|
4446
|
-
if (!
|
|
4820
|
+
if (!existsSync14(skillPath))
|
|
4447
4821
|
return null;
|
|
4448
4822
|
return {
|
|
4449
|
-
skillMd: readIfExists(
|
|
4450
|
-
readme: readIfExists(
|
|
4451
|
-
claudeMd: readIfExists(
|
|
4823
|
+
skillMd: readIfExists(join14(skillPath, "SKILL.md")),
|
|
4824
|
+
readme: readIfExists(join14(skillPath, "README.md")),
|
|
4825
|
+
claudeMd: readIfExists(join14(skillPath, "CLAUDE.md"))
|
|
4452
4826
|
};
|
|
4453
4827
|
}
|
|
4454
4828
|
function getSkillBestDoc(name) {
|
|
@@ -4459,17 +4833,24 @@ function getSkillBestDoc(name) {
|
|
|
4459
4833
|
}
|
|
4460
4834
|
function getSkillRequirements(name) {
|
|
4461
4835
|
const skillPath = getSkillPath(name);
|
|
4462
|
-
if (!
|
|
4836
|
+
if (!existsSync14(skillPath))
|
|
4463
4837
|
return null;
|
|
4464
4838
|
const texts = [];
|
|
4465
4839
|
for (const file of ["SKILL.md", "README.md", "CLAUDE.md", ".env.example", ".env.local.example"]) {
|
|
4466
|
-
const content = readIfExists(
|
|
4840
|
+
const content = readIfExists(join14(skillPath, file));
|
|
4467
4841
|
if (content)
|
|
4468
4842
|
texts.push(content);
|
|
4469
4843
|
}
|
|
4844
|
+
const meta = getSkill(name);
|
|
4845
|
+
let dependencies = {};
|
|
4846
|
+
try {
|
|
4847
|
+
dependencies = JSON.parse(readFileSync12(join14(skillPath, "package.json"), "utf8")).dependencies || {};
|
|
4848
|
+
} catch {}
|
|
4849
|
+
return getSkillRequirementsFromContent(name, texts, dependencies, meta);
|
|
4850
|
+
}
|
|
4851
|
+
function getSkillRequirementsFromContent(name, texts, dependencies = {}, meta) {
|
|
4470
4852
|
const allText = texts.join(`
|
|
4471
4853
|
`);
|
|
4472
|
-
const meta = getSkill(name);
|
|
4473
4854
|
const canonicalName = meta?.name ?? normalizeSkillName(name);
|
|
4474
4855
|
const envVars = extractEnvVars(allText);
|
|
4475
4856
|
if (isHostedPremiumSkill(canonicalName, meta)) {
|
|
@@ -4502,14 +4883,6 @@ function getSkillRequirements(name) {
|
|
|
4502
4883
|
}
|
|
4503
4884
|
const skillName = normalizeSkillName(name);
|
|
4504
4885
|
let cliCommand = `skills run ${skillName}`;
|
|
4505
|
-
let dependencies = {};
|
|
4506
|
-
const pkgPath = join13(skillPath, "package.json");
|
|
4507
|
-
if (existsSync13(pkgPath)) {
|
|
4508
|
-
try {
|
|
4509
|
-
const pkg = JSON.parse(readFileSync11(pkgPath, "utf-8"));
|
|
4510
|
-
dependencies = pkg.dependencies || {};
|
|
4511
|
-
} catch {}
|
|
4512
|
-
}
|
|
4513
4886
|
return {
|
|
4514
4887
|
envVars: Array.from(envVars).sort(),
|
|
4515
4888
|
systemDeps: Array.from(systemDeps).sort(),
|
|
@@ -4524,7 +4897,7 @@ async function runSkill(name, args, options = {}) {
|
|
|
4524
4897
|
const meta = getSkill(name);
|
|
4525
4898
|
const canonicalName = meta?.name ?? name;
|
|
4526
4899
|
const skillPath = getSkillPath(canonicalName);
|
|
4527
|
-
if (!
|
|
4900
|
+
if (!existsSync14(skillPath)) {
|
|
4528
4901
|
return { exitCode: 1, error: `Skill '${name}' not found` };
|
|
4529
4902
|
}
|
|
4530
4903
|
if (isInstructionSkillDir(skillPath, meta)) {
|
|
@@ -4533,13 +4906,13 @@ async function runSkill(name, args, options = {}) {
|
|
|
4533
4906
|
error: `Skill '${name}' is an instruction skill (kind: instruction) and is not runnable. Instruction skills are consumed by coding agents via SKILL.md, not executed with 'skills run'.`
|
|
4534
4907
|
};
|
|
4535
4908
|
}
|
|
4536
|
-
const pkgPath =
|
|
4537
|
-
if (!
|
|
4909
|
+
const pkgPath = join14(skillPath, "package.json");
|
|
4910
|
+
if (!existsSync14(pkgPath)) {
|
|
4538
4911
|
return { exitCode: 1, error: `No package.json in skill '${name}'` };
|
|
4539
4912
|
}
|
|
4540
4913
|
let entryPoint;
|
|
4541
4914
|
try {
|
|
4542
|
-
const pkg = JSON.parse(
|
|
4915
|
+
const pkg = JSON.parse(readFileSync12(pkgPath, "utf-8"));
|
|
4543
4916
|
if (pkg.bin) {
|
|
4544
4917
|
const binValues = Object.values(pkg.bin);
|
|
4545
4918
|
entryPoint = binValues[0];
|
|
@@ -4553,12 +4926,12 @@ async function runSkill(name, args, options = {}) {
|
|
|
4553
4926
|
} catch {
|
|
4554
4927
|
return { exitCode: 1, error: `Failed to parse package.json for skill '${name}'` };
|
|
4555
4928
|
}
|
|
4556
|
-
const entryPath =
|
|
4557
|
-
if (!
|
|
4929
|
+
const entryPath = join14(skillPath, entryPoint);
|
|
4930
|
+
if (!existsSync14(entryPath)) {
|
|
4558
4931
|
return { exitCode: 1, error: `Entry point '${entryPoint}' not found in skill '${name}'` };
|
|
4559
4932
|
}
|
|
4560
|
-
const nodeModules =
|
|
4561
|
-
if (!
|
|
4933
|
+
const nodeModules = join14(skillPath, "node_modules");
|
|
4934
|
+
if (!existsSync14(nodeModules)) {
|
|
4562
4935
|
const install = Bun.spawn(["bun", "install", "--no-save"], {
|
|
4563
4936
|
cwd: skillPath,
|
|
4564
4937
|
stdout: "pipe",
|
|
@@ -4630,7 +5003,7 @@ function generateSkillMd(name) {
|
|
|
4630
5003
|
if (!meta)
|
|
4631
5004
|
return null;
|
|
4632
5005
|
const skillPath = getSkillPath(name);
|
|
4633
|
-
if (!
|
|
5006
|
+
if (!existsSync14(skillPath))
|
|
4634
5007
|
return null;
|
|
4635
5008
|
const frontmatter = [
|
|
4636
5009
|
"---",
|
|
@@ -4639,13 +5012,13 @@ function generateSkillMd(name) {
|
|
|
4639
5012
|
"---"
|
|
4640
5013
|
].join(`
|
|
4641
5014
|
`);
|
|
4642
|
-
const readme = readIfExists(
|
|
4643
|
-
const claudeMd = readIfExists(
|
|
5015
|
+
const readme = readIfExists(join14(skillPath, "README.md"));
|
|
5016
|
+
const claudeMd = readIfExists(join14(skillPath, "CLAUDE.md"));
|
|
4644
5017
|
let cliCommand = null;
|
|
4645
|
-
const pkgPath =
|
|
4646
|
-
if (
|
|
5018
|
+
const pkgPath = join14(skillPath, "package.json");
|
|
5019
|
+
if (existsSync14(pkgPath)) {
|
|
4647
5020
|
try {
|
|
4648
|
-
const pkg = JSON.parse(
|
|
5021
|
+
const pkg = JSON.parse(readFileSync12(pkgPath, "utf-8"));
|
|
4649
5022
|
if (pkg.bin) {
|
|
4650
5023
|
const binKeys = Object.keys(pkg.bin);
|
|
4651
5024
|
if (binKeys.length > 0)
|
|
@@ -4720,8 +5093,8 @@ function extractEnvVars(text) {
|
|
|
4720
5093
|
}
|
|
4721
5094
|
function readIfExists(path) {
|
|
4722
5095
|
try {
|
|
4723
|
-
if (
|
|
4724
|
-
return
|
|
5096
|
+
if (existsSync14(path)) {
|
|
5097
|
+
return readFileSync12(path, "utf-8");
|
|
4725
5098
|
}
|
|
4726
5099
|
} catch {}
|
|
4727
5100
|
return null;
|
|
@@ -8702,11 +9075,11 @@ var NEVER = INVALID;
|
|
|
8702
9075
|
// ../contracts/dist/client/transport.js
|
|
8703
9076
|
import { isIP } from "net";
|
|
8704
9077
|
import { spawnSync } from "child_process";
|
|
8705
|
-
import { closeSync, fstatSync, openSync, readFileSync as
|
|
9078
|
+
import { closeSync, fstatSync, openSync, readFileSync as readFileSync13 } from "fs";
|
|
8706
9079
|
import { O_NOFOLLOW, O_NONBLOCK, O_RDONLY } from "constants";
|
|
8707
9080
|
import { createRequire } from "module";
|
|
8708
9081
|
import { hostname as osHostname } from "os";
|
|
8709
|
-
import { isAbsolute as
|
|
9082
|
+
import { isAbsolute as isAbsolute4, join as join15 } from "path";
|
|
8710
9083
|
function envToken(name) {
|
|
8711
9084
|
return name.toUpperCase().replace(/-/g, "_");
|
|
8712
9085
|
}
|
|
@@ -8765,21 +9138,21 @@ function homeDir(env) {
|
|
|
8765
9138
|
}
|
|
8766
9139
|
function absoluteOverride(env, key) {
|
|
8767
9140
|
const value = env[key]?.trim();
|
|
8768
|
-
return value &&
|
|
9141
|
+
return value && isAbsolute4(value) ? value : null;
|
|
8769
9142
|
}
|
|
8770
9143
|
function hasnaHomeDir(env) {
|
|
8771
9144
|
const override = absoluteOverride(env, HASNA_HOME_ENV_KEY);
|
|
8772
9145
|
if (override)
|
|
8773
9146
|
return override;
|
|
8774
9147
|
const home = homeDir(env);
|
|
8775
|
-
return home ?
|
|
9148
|
+
return home ? join15(home, HASNA_HOME_DIR) : null;
|
|
8776
9149
|
}
|
|
8777
9150
|
function appConfigDir(name, env) {
|
|
8778
9151
|
const configRoot = absoluteOverride(env, HASNA_CONFIG_HOME_ENV_KEY);
|
|
8779
9152
|
if (configRoot)
|
|
8780
|
-
return
|
|
9153
|
+
return join15(configRoot, name);
|
|
8781
9154
|
const root = hasnaHomeDir(env);
|
|
8782
|
-
return root ?
|
|
9155
|
+
return root ? join15(root, name, CONFIG_SUBDIR) : null;
|
|
8783
9156
|
}
|
|
8784
9157
|
function credentialDiskSourceList(name, env, profile = null) {
|
|
8785
9158
|
if (!SAFE_APP_SLUG.test(name))
|
|
@@ -8788,7 +9161,7 @@ function credentialDiskSourceList(name, env, profile = null) {
|
|
|
8788
9161
|
if (!directory)
|
|
8789
9162
|
return [];
|
|
8790
9163
|
const file = profile ? `${CREDENTIALS_FILE}-${profile}` : CREDENTIALS_FILE;
|
|
8791
|
-
return [{ path:
|
|
9164
|
+
return [{ path: join15(directory, file), tier: "disk" }];
|
|
8792
9165
|
}
|
|
8793
9166
|
function credentialDiskSources(name, env) {
|
|
8794
9167
|
return credentialDiskSourceList(name, env, null).map((s) => s.path);
|
|
@@ -8863,7 +9236,7 @@ function readAppConfigFile(path) {
|
|
|
8863
9236
|
unsafe("the file is not owned by the current user");
|
|
8864
9237
|
if (before.size > MAX_CREDENTIAL_FILE_BYTES)
|
|
8865
9238
|
unsafe("the file exceeds the size limit");
|
|
8866
|
-
const bytes =
|
|
9239
|
+
const bytes = readFileSync13(fd);
|
|
8867
9240
|
const after = fstatSync(fd);
|
|
8868
9241
|
if (!configFileReadsCoherent(before, after)) {
|
|
8869
9242
|
unsafe("the file changed while being read");
|
|
@@ -9386,7 +9759,7 @@ var AUTHORITY_OVERRIDE_HEADERS = new Set([
|
|
|
9386
9759
|
]);
|
|
9387
9760
|
|
|
9388
9761
|
// src/lib/instance-credentials.ts
|
|
9389
|
-
import { closeSync as closeSync2, constants, fstatSync as fstatSync2, lstatSync as
|
|
9762
|
+
import { closeSync as closeSync2, constants, fstatSync as fstatSync2, lstatSync as lstatSync5, openSync as openSync2, readSync } from "fs";
|
|
9390
9763
|
var SKILLS_BOUND_API_URL = "HASNA_SKILLS_BOUND_API_URL";
|
|
9391
9764
|
function selectedSkillsProfile(env, explicit) {
|
|
9392
9765
|
const selected = explicit ?? env.HASNA_PROFILE;
|
|
@@ -9402,7 +9775,7 @@ function skillsProfileCredentialFiles(env, explicit) {
|
|
|
9402
9775
|
}
|
|
9403
9776
|
function fileIdentity(file) {
|
|
9404
9777
|
try {
|
|
9405
|
-
return
|
|
9778
|
+
return lstatSync5(file);
|
|
9406
9779
|
} catch (error) {
|
|
9407
9780
|
if (["ENOENT", "ENOTDIR"].includes(error.code ?? ""))
|
|
9408
9781
|
return null;
|
|
@@ -11082,7 +11455,7 @@ function object(value) {
|
|
|
11082
11455
|
}
|
|
11083
11456
|
|
|
11084
11457
|
// src/lib/remote-files.ts
|
|
11085
|
-
import { createHash as
|
|
11458
|
+
import { createHash as createHash4 } from "crypto";
|
|
11086
11459
|
var MAX_REMOTE_FILE_BYTES = 64 * 1024 * 1024;
|
|
11087
11460
|
function describeRemoteFiles(files) {
|
|
11088
11461
|
if (files.length > 10)
|
|
@@ -11100,7 +11473,7 @@ function describeRemoteFiles(files) {
|
|
|
11100
11473
|
});
|
|
11101
11474
|
}
|
|
11102
11475
|
function sha256(bytes) {
|
|
11103
|
-
return
|
|
11476
|
+
return createHash4("sha256").update(bytes).digest("hex");
|
|
11104
11477
|
}
|
|
11105
11478
|
async function readBoundedResponse(response, maximum) {
|
|
11106
11479
|
if (!Number.isSafeInteger(maximum) || maximum < 0 || maximum > MAX_REMOTE_FILE_BYTES)
|
|
@@ -11193,10 +11566,10 @@ async function readQuoteUnavailableCode(response) {
|
|
|
11193
11566
|
let timer;
|
|
11194
11567
|
let deadlineExceeded = false;
|
|
11195
11568
|
const expired = Symbol("quote body deadline");
|
|
11196
|
-
const deadline = new Promise((
|
|
11569
|
+
const deadline = new Promise((resolve4) => {
|
|
11197
11570
|
timer = setTimeout(() => {
|
|
11198
11571
|
deadlineExceeded = true;
|
|
11199
|
-
|
|
11572
|
+
resolve4(expired);
|
|
11200
11573
|
reader.cancel().catch(() => {});
|
|
11201
11574
|
}, 1500);
|
|
11202
11575
|
});
|
|
@@ -11315,6 +11688,7 @@ class RemoteSkillsClient {
|
|
|
11315
11688
|
credentials: "omit",
|
|
11316
11689
|
signal: options?.signal ?? AbortSignal.timeout(15000),
|
|
11317
11690
|
headers: {
|
|
11691
|
+
"User-Agent": "hasna-skills",
|
|
11318
11692
|
Authorization: `Bearer ${this.apiKey}`,
|
|
11319
11693
|
"Content-Type": "application/json",
|
|
11320
11694
|
...options?.headers
|
|
@@ -12009,26 +12383,26 @@ async function createRemoteSkillsClient(env = process.env) {
|
|
|
12009
12383
|
return connection ? new RemoteSkillsClient(connection.apiKey, connection.apiOrigin) : null;
|
|
12010
12384
|
}
|
|
12011
12385
|
// src/lib/scheduler.ts
|
|
12012
|
-
import { existsSync as
|
|
12013
|
-
import { join as
|
|
12386
|
+
import { existsSync as existsSync15, readFileSync as readFileSync14, writeFileSync as writeFileSync8, mkdirSync as mkdirSync8 } from "fs";
|
|
12387
|
+
import { join as join16 } from "path";
|
|
12014
12388
|
function getSchedulesPath(targetDir = process.cwd()) {
|
|
12015
|
-
return
|
|
12389
|
+
return join16(targetDir, ".skills", "schedules.json");
|
|
12016
12390
|
}
|
|
12017
12391
|
function loadSchedules(targetDir = process.cwd()) {
|
|
12018
12392
|
const path = getSchedulesPath(targetDir);
|
|
12019
|
-
if (
|
|
12393
|
+
if (existsSync15(path)) {
|
|
12020
12394
|
try {
|
|
12021
|
-
return JSON.parse(
|
|
12395
|
+
return JSON.parse(readFileSync14(path, "utf-8"));
|
|
12022
12396
|
} catch {}
|
|
12023
12397
|
}
|
|
12024
12398
|
return { version: 1, schedules: [] };
|
|
12025
12399
|
}
|
|
12026
12400
|
function saveSchedules(data, targetDir = process.cwd()) {
|
|
12027
12401
|
const path = getSchedulesPath(targetDir);
|
|
12028
|
-
const dir =
|
|
12029
|
-
if (!
|
|
12030
|
-
|
|
12031
|
-
|
|
12402
|
+
const dir = join16(targetDir, ".skills");
|
|
12403
|
+
if (!existsSync15(dir))
|
|
12404
|
+
mkdirSync8(dir, { recursive: true });
|
|
12405
|
+
writeFileSync8(path, JSON.stringify(data, null, 2));
|
|
12032
12406
|
}
|
|
12033
12407
|
function validateCronField(expr, min, max, label) {
|
|
12034
12408
|
for (const part of expr.split(",")) {
|
|
@@ -12213,7 +12587,7 @@ function recordScheduleRun(id, status, targetDir) {
|
|
|
12213
12587
|
saveSchedules(data, targetDir);
|
|
12214
12588
|
}
|
|
12215
12589
|
// src/lib/revision.ts
|
|
12216
|
-
import { createHash as
|
|
12590
|
+
import { createHash as createHash5 } from "crypto";
|
|
12217
12591
|
var REVISION_ID_PATTERN = /^[0-9a-f]{64}$/;
|
|
12218
12592
|
function revisionIdOf(content) {
|
|
12219
12593
|
const canonical = JSON.stringify({
|
|
@@ -12229,19 +12603,19 @@ function revisionIdOf(content) {
|
|
|
12229
12603
|
bundleSha256: content.bundleSha256 ?? null,
|
|
12230
12604
|
bundleByteSize: content.bundleByteSize ?? null
|
|
12231
12605
|
});
|
|
12232
|
-
return
|
|
12606
|
+
return createHash5("sha256").update(canonical).digest("hex");
|
|
12233
12607
|
}
|
|
12234
12608
|
function revisionIdOfRecord(record4) {
|
|
12235
12609
|
return revisionIdOf(record4);
|
|
12236
12610
|
}
|
|
12237
12611
|
// src/lib/pull.ts
|
|
12238
|
-
import { existsSync as
|
|
12239
|
-
import { dirname as
|
|
12612
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync9, mkdtempSync as mkdtempSync3, readFileSync as readFileSync16, renameSync as renameSync4, rmSync as rmSync4, writeFileSync as writeFileSync9 } from "fs";
|
|
12613
|
+
import { dirname as dirname7, join as join18 } from "path";
|
|
12240
12614
|
|
|
12241
12615
|
// src/lib/skill-bundle.ts
|
|
12242
|
-
import { createHash as
|
|
12243
|
-
import { readFileSync as
|
|
12244
|
-
import { join as
|
|
12616
|
+
import { createHash as createHash6 } from "crypto";
|
|
12617
|
+
import { readFileSync as readFileSync15, readdirSync as readdirSync10, statSync as statSync9 } from "fs";
|
|
12618
|
+
import { join as join17, relative as relative5 } from "path";
|
|
12245
12619
|
import { createGunzip } from "zlib";
|
|
12246
12620
|
var BLOCK = 512;
|
|
12247
12621
|
var ANY_SEGMENT_EXCLUDES = new Set([
|
|
@@ -12394,7 +12768,7 @@ function ownBytes(view) {
|
|
|
12394
12768
|
return out;
|
|
12395
12769
|
}
|
|
12396
12770
|
function sha256Hex(bytes) {
|
|
12397
|
-
return
|
|
12771
|
+
return createHash6("sha256").update(bytes).digest("hex");
|
|
12398
12772
|
}
|
|
12399
12773
|
function collectSkillBundleEntries(dir) {
|
|
12400
12774
|
const entries = [];
|
|
@@ -12402,9 +12776,9 @@ function collectSkillBundleEntries(dir) {
|
|
|
12402
12776
|
return entries.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
12403
12777
|
}
|
|
12404
12778
|
function walk(root, current, out) {
|
|
12405
|
-
for (const entry of
|
|
12406
|
-
const absolute =
|
|
12407
|
-
const rel =
|
|
12779
|
+
for (const entry of readdirSync10(current, { withFileTypes: true })) {
|
|
12780
|
+
const absolute = join17(current, entry.name);
|
|
12781
|
+
const rel = relative5(root, absolute).split("\\").join("/");
|
|
12408
12782
|
const isRootLevel = !rel.includes("/");
|
|
12409
12783
|
if (ANY_SEGMENT_EXCLUDES.has(entry.name.toLowerCase()))
|
|
12410
12784
|
continue;
|
|
@@ -12427,7 +12801,7 @@ function walk(root, current, out) {
|
|
|
12427
12801
|
const stats = statSync9(absolute);
|
|
12428
12802
|
out.push({
|
|
12429
12803
|
path: rel,
|
|
12430
|
-
bytes: ownBytes(
|
|
12804
|
+
bytes: ownBytes(readFileSync15(absolute)),
|
|
12431
12805
|
mode: stats.mode & 64 ? 493 : 420
|
|
12432
12806
|
});
|
|
12433
12807
|
}
|
|
@@ -12640,7 +13014,7 @@ async function inspectSkillBundle(bundle, options = {}) {
|
|
|
12640
13014
|
parser.push(chunk);
|
|
12641
13015
|
bytesSinceYield += chunk.byteLength;
|
|
12642
13016
|
if (bytesSinceYield >= 256 * 1024) {
|
|
12643
|
-
await new Promise((
|
|
13017
|
+
await new Promise((resolve4) => setTimeout(resolve4, 0));
|
|
12644
13018
|
bytesSinceYield = 0;
|
|
12645
13019
|
check();
|
|
12646
13020
|
}
|
|
@@ -12886,7 +13260,7 @@ async function pullOne(client, rawName, corpusOptions, verify) {
|
|
|
12886
13260
|
return reconcileTombstone(slug, corpusOptions);
|
|
12887
13261
|
}
|
|
12888
13262
|
if (bundleResponse.status === 404) {
|
|
12889
|
-
const marker = readPullMarker(
|
|
13263
|
+
const marker = readPullMarker(join18(getPortableSkillsRoot(corpusOptions), slug));
|
|
12890
13264
|
if (isPublishedInstallMarker(marker)) {
|
|
12891
13265
|
return { name: slug, success: true, purged: true, removed: false };
|
|
12892
13266
|
}
|
|
@@ -12925,7 +13299,7 @@ async function pullOne(client, rawName, corpusOptions, verify) {
|
|
|
12925
13299
|
return { name: slug, success: false, error: `Skill '${slug}' was not found on the configured Skills instance.` };
|
|
12926
13300
|
}
|
|
12927
13301
|
if (!meta?.revisionId) {
|
|
12928
|
-
const marker = readPullMarker(
|
|
13302
|
+
const marker = readPullMarker(join18(getPortableSkillsRoot(corpusOptions), slug));
|
|
12929
13303
|
if (isPublishedInstallMarker(marker)) {
|
|
12930
13304
|
return { name: slug, success: true, purged: true, removed: false };
|
|
12931
13305
|
}
|
|
@@ -12982,7 +13356,7 @@ function provenRevision(meta, slug, bundle) {
|
|
|
12982
13356
|
return declared;
|
|
12983
13357
|
}
|
|
12984
13358
|
function reconcileTombstone(slug, corpusOptions) {
|
|
12985
|
-
const target =
|
|
13359
|
+
const target = join18(getPortableSkillsRoot(corpusOptions), slug);
|
|
12986
13360
|
if (!hasSkillsOwnershipMarker(target)) {
|
|
12987
13361
|
return { name: slug, success: true, tombstoned: true, removed: false, leftInPlace: true };
|
|
12988
13362
|
}
|
|
@@ -12991,7 +13365,7 @@ function reconcileTombstone(slug, corpusOptions) {
|
|
|
12991
13365
|
}
|
|
12992
13366
|
function readPullMarker(dir) {
|
|
12993
13367
|
try {
|
|
12994
|
-
return JSON.parse(
|
|
13368
|
+
return JSON.parse(readFileSync16(join18(dir, PULL_MARKER_FILE), "utf-8"));
|
|
12995
13369
|
} catch {
|
|
12996
13370
|
return null;
|
|
12997
13371
|
}
|
|
@@ -13125,17 +13499,17 @@ function verifyBundleResponseBytes(buffer, response, verify = {}) {
|
|
|
13125
13499
|
}
|
|
13126
13500
|
function installBundleAtomically(name, entries, options = {}, marker = {}) {
|
|
13127
13501
|
const root = getPortableSkillsRoot(options);
|
|
13128
|
-
|
|
13129
|
-
const target =
|
|
13130
|
-
const created = !
|
|
13131
|
-
const staging = mkdtempSync3(
|
|
13502
|
+
mkdirSync9(root, { recursive: true });
|
|
13503
|
+
const target = join18(root, name);
|
|
13504
|
+
const created = !existsSync16(target);
|
|
13505
|
+
const staging = mkdtempSync3(join18(root, `.pull-${name}-`));
|
|
13132
13506
|
let moved = false;
|
|
13133
13507
|
let backup = null;
|
|
13134
13508
|
try {
|
|
13135
13509
|
for (const entry of entries) {
|
|
13136
|
-
const destination =
|
|
13137
|
-
|
|
13138
|
-
|
|
13510
|
+
const destination = join18(staging, entry.path);
|
|
13511
|
+
mkdirSync9(dirname7(destination), { recursive: true });
|
|
13512
|
+
writeFileSync9(destination, entry.bytes, { mode: entry.mode });
|
|
13139
13513
|
}
|
|
13140
13514
|
writePullMarker(staging, {
|
|
13141
13515
|
skill: name,
|
|
@@ -13145,19 +13519,19 @@ function installBundleAtomically(name, entries, options = {}, marker = {}) {
|
|
|
13145
13519
|
...marker.signature ? { signature: marker.signature } : {},
|
|
13146
13520
|
...marker.revisionId ? { revisionId: marker.revisionId } : {}
|
|
13147
13521
|
});
|
|
13148
|
-
if (
|
|
13149
|
-
backup = mkdtempSync3(
|
|
13150
|
-
|
|
13522
|
+
if (existsSync16(target)) {
|
|
13523
|
+
backup = mkdtempSync3(join18(root, `.pull-backup-${name}-`));
|
|
13524
|
+
renameSync4(target, join18(backup, name));
|
|
13151
13525
|
moved = true;
|
|
13152
13526
|
}
|
|
13153
|
-
|
|
13527
|
+
renameSync4(staging, target);
|
|
13154
13528
|
if (moved && backup)
|
|
13155
13529
|
rmSync4(backup, { recursive: true, force: true });
|
|
13156
13530
|
} catch (error) {
|
|
13157
13531
|
rmSync4(staging, { recursive: true, force: true });
|
|
13158
|
-
if (moved && backup &&
|
|
13532
|
+
if (moved && backup && existsSync16(join18(backup, name))) {
|
|
13159
13533
|
try {
|
|
13160
|
-
|
|
13534
|
+
renameSync4(join18(backup, name), target);
|
|
13161
13535
|
} catch {}
|
|
13162
13536
|
}
|
|
13163
13537
|
throw error;
|
|
@@ -13176,7 +13550,7 @@ function writePullMarker(dir, record4) {
|
|
|
13176
13550
|
...record4.revisionId ? { revisionId: record4.revisionId } : {},
|
|
13177
13551
|
syncedAt: new Date().toISOString()
|
|
13178
13552
|
};
|
|
13179
|
-
|
|
13553
|
+
writeFileSync9(join18(dir, PULL_MARKER_FILE), `${JSON.stringify(marker, null, 2)}
|
|
13180
13554
|
`);
|
|
13181
13555
|
}
|
|
13182
13556
|
async function safeMeta(client, slug) {
|
|
@@ -13331,12 +13705,12 @@ function findSkillsParityForMcpTool(tool) {
|
|
|
13331
13705
|
return SKILLS_CLI_MCP_PARITY.find((entry) => entry.mcpTools.includes(tool));
|
|
13332
13706
|
}
|
|
13333
13707
|
// src/lib/registry-sync.ts
|
|
13334
|
-
import { mkdirSync as
|
|
13335
|
-
import { dirname as
|
|
13708
|
+
import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync10 } from "fs";
|
|
13709
|
+
import { dirname as dirname8, relative as relative6 } from "path";
|
|
13336
13710
|
// package.json
|
|
13337
13711
|
var package_default = {
|
|
13338
13712
|
name: "@hasna/skills",
|
|
13339
|
-
version: "0.
|
|
13713
|
+
version: "0.6.0",
|
|
13340
13714
|
description: "Skills library for AI coding agents",
|
|
13341
13715
|
type: "module",
|
|
13342
13716
|
bin: {
|
|
@@ -13452,7 +13826,8 @@ var package_default = {
|
|
|
13452
13826
|
},
|
|
13453
13827
|
repository: {
|
|
13454
13828
|
type: "git",
|
|
13455
|
-
url: "
|
|
13829
|
+
url: "https://github.com/hasna/apps.git",
|
|
13830
|
+
directory: "apps/skills"
|
|
13456
13831
|
},
|
|
13457
13832
|
homepage: "https://github.com/hasna/skills",
|
|
13458
13833
|
bugs: {
|
|
@@ -13472,7 +13847,7 @@ function createRegistrySyncArtifact(options = {}) {
|
|
|
13472
13847
|
const registry = [...loadRegistryProfile(profile)].sort((a, b) => a.name.localeCompare(b.name));
|
|
13473
13848
|
const skills = registry.map((skill) => {
|
|
13474
13849
|
const skillPath = getSkillPath(skill.name);
|
|
13475
|
-
const directory =
|
|
13850
|
+
const directory = relative6(process.cwd(), skillPath) || skillPath;
|
|
13476
13851
|
const validation = includeValidation ? validateSkillDirectory(skill.name, skillPath, skill) : undefined;
|
|
13477
13852
|
const docs = includeDocs ? buildDocs(skill.name) : undefined;
|
|
13478
13853
|
return {
|
|
@@ -13517,8 +13892,8 @@ function createRegistrySyncArtifact(options = {}) {
|
|
|
13517
13892
|
};
|
|
13518
13893
|
}
|
|
13519
13894
|
function writeRegistrySyncArtifact(path, artifact) {
|
|
13520
|
-
|
|
13521
|
-
|
|
13895
|
+
mkdirSync10(dirname8(path), { recursive: true });
|
|
13896
|
+
writeFileSync10(path, `${JSON.stringify(artifact, null, 2)}
|
|
13522
13897
|
`);
|
|
13523
13898
|
}
|
|
13524
13899
|
function buildDocs(name) {
|
|
@@ -14590,17 +14965,17 @@ function clone2(value) {
|
|
|
14590
14965
|
return JSON.parse(JSON.stringify(value));
|
|
14591
14966
|
}
|
|
14592
14967
|
// src/lib/feedback.ts
|
|
14593
|
-
import { appendFileSync, existsSync as
|
|
14594
|
-
import { dirname as
|
|
14968
|
+
import { appendFileSync, existsSync as existsSync17, mkdirSync as mkdirSync11 } from "fs";
|
|
14969
|
+
import { dirname as dirname9, join as join19 } from "path";
|
|
14595
14970
|
import { Database } from "bun:sqlite";
|
|
14596
14971
|
function getFeedbackDbPath() {
|
|
14597
|
-
return
|
|
14972
|
+
return join19(getDataDir(), "skills.db");
|
|
14598
14973
|
}
|
|
14599
14974
|
function getFeedbackDb() {
|
|
14600
14975
|
const dbPath = getFeedbackDbPath();
|
|
14601
|
-
const dir =
|
|
14602
|
-
if (!
|
|
14603
|
-
|
|
14976
|
+
const dir = dirname9(dbPath);
|
|
14977
|
+
if (!existsSync17(dir))
|
|
14978
|
+
mkdirSync11(dir, { recursive: true });
|
|
14604
14979
|
const db = new Database(dbPath);
|
|
14605
14980
|
db.exec("PRAGMA journal_mode = WAL");
|
|
14606
14981
|
db.exec([
|
|
@@ -14626,10 +15001,10 @@ function saveFeedback(input) {
|
|
|
14626
15001
|
throw new Error("Feedback message is required");
|
|
14627
15002
|
const category = input.category ?? "general";
|
|
14628
15003
|
if (isApiMode()) {
|
|
14629
|
-
const path =
|
|
14630
|
-
const dir =
|
|
14631
|
-
if (!
|
|
14632
|
-
|
|
15004
|
+
const path = join19(getDataDir(), "feedback.jsonl");
|
|
15005
|
+
const dir = dirname9(path);
|
|
15006
|
+
if (!existsSync17(dir))
|
|
15007
|
+
mkdirSync11(dir, { recursive: true });
|
|
14633
15008
|
appendFileSync(path, JSON.stringify({ message, category, email: input.email ?? null, agent: input.agent ?? null, version: input.version ?? null, createdAt: new Date().toISOString() }) + `
|
|
14634
15009
|
`);
|
|
14635
15010
|
return { saved: true, category, path };
|
|
@@ -14650,16 +15025,16 @@ function isApiMode(env = process.env) {
|
|
|
14650
15025
|
}
|
|
14651
15026
|
}
|
|
14652
15027
|
// src/lib/native-storage.ts
|
|
14653
|
-
import { createHash as
|
|
15028
|
+
import { createHash as createHash7, createHmac as createHmac2 } from "crypto";
|
|
14654
15029
|
import {
|
|
14655
|
-
existsSync as
|
|
14656
|
-
mkdirSync as
|
|
14657
|
-
readFileSync as
|
|
14658
|
-
readdirSync as
|
|
15030
|
+
existsSync as existsSync18,
|
|
15031
|
+
mkdirSync as mkdirSync12,
|
|
15032
|
+
readFileSync as readFileSync17,
|
|
15033
|
+
readdirSync as readdirSync11,
|
|
14659
15034
|
statSync as statSync10,
|
|
14660
|
-
writeFileSync as
|
|
15035
|
+
writeFileSync as writeFileSync11
|
|
14661
15036
|
} from "fs";
|
|
14662
|
-
import { dirname as
|
|
15037
|
+
import { dirname as dirname10, join as join20, normalize as normalize3, relative as relative7, sep as sep4 } from "path";
|
|
14663
15038
|
var SKILLS_STORAGE_TABLES = [
|
|
14664
15039
|
"skills_sync_records",
|
|
14665
15040
|
"skills_sync_cursors"
|
|
@@ -14796,7 +15171,7 @@ function getSkillsNativeStorageStatus(options = {}) {
|
|
|
14796
15171
|
local: {
|
|
14797
15172
|
dataDir: getDataDir(),
|
|
14798
15173
|
projectStateDir: getProjectStateDir(targetDir),
|
|
14799
|
-
feedbackDbPath:
|
|
15174
|
+
feedbackDbPath: join20(getDataDir(), "skills.db")
|
|
14800
15175
|
},
|
|
14801
15176
|
remote: {
|
|
14802
15177
|
databaseConfigured: Boolean(config.databaseUrl),
|
|
@@ -14819,14 +15194,14 @@ function getStorageStatus(options = {}) {
|
|
|
14819
15194
|
function exportSkillsLocalSnapshot(targetDir = process.cwd(), options = {}) {
|
|
14820
15195
|
const projectStateDir = getProjectStateDir(targetDir);
|
|
14821
15196
|
const files = [];
|
|
14822
|
-
if (
|
|
15197
|
+
if (existsSync18(projectStateDir)) {
|
|
14823
15198
|
for (const filePath of walkFiles2(projectStateDir)) {
|
|
14824
|
-
const bytes =
|
|
14825
|
-
const relativePath = toPosix(
|
|
15199
|
+
const bytes = readFileSync17(filePath);
|
|
15200
|
+
const relativePath = toPosix(relative7(targetDir, filePath));
|
|
14826
15201
|
files.push({
|
|
14827
15202
|
path: relativePath,
|
|
14828
15203
|
sizeBytes: bytes.byteLength,
|
|
14829
|
-
sha256:
|
|
15204
|
+
sha256: createHash7("sha256").update(bytes).digest("hex"),
|
|
14830
15205
|
...options.includeFileContents ? { contentBase64: Buffer.from(bytes).toString("base64") } : {}
|
|
14831
15206
|
});
|
|
14832
15207
|
}
|
|
@@ -14846,17 +15221,17 @@ function importSkillsLocalSnapshot(snapshot, targetDir = process.cwd(), options
|
|
|
14846
15221
|
continue;
|
|
14847
15222
|
}
|
|
14848
15223
|
const absolutePath = resolveSnapshotPath(targetDir, file.path);
|
|
14849
|
-
if (
|
|
15224
|
+
if (existsSync18(absolutePath) && !options.overwrite) {
|
|
14850
15225
|
skipped += 1;
|
|
14851
15226
|
continue;
|
|
14852
15227
|
}
|
|
14853
15228
|
const bytes = Buffer.from(file.contentBase64, "base64");
|
|
14854
|
-
const hash =
|
|
15229
|
+
const hash = createHash7("sha256").update(bytes).digest("hex");
|
|
14855
15230
|
if (hash !== file.sha256) {
|
|
14856
15231
|
throw new Error(`Snapshot file checksum mismatch: ${file.path}`);
|
|
14857
15232
|
}
|
|
14858
|
-
|
|
14859
|
-
|
|
15233
|
+
mkdirSync12(dirname10(absolutePath), { recursive: true });
|
|
15234
|
+
writeFileSync11(absolutePath, bytes);
|
|
14860
15235
|
written += 1;
|
|
14861
15236
|
}
|
|
14862
15237
|
return { written, skipped };
|
|
@@ -15152,8 +15527,8 @@ function parsePositiveInteger(value) {
|
|
|
15152
15527
|
}
|
|
15153
15528
|
function walkFiles2(dir) {
|
|
15154
15529
|
const files = [];
|
|
15155
|
-
for (const entry of
|
|
15156
|
-
const full =
|
|
15530
|
+
for (const entry of readdirSync11(dir)) {
|
|
15531
|
+
const full = join20(dir, entry);
|
|
15157
15532
|
const stats = statSync10(full);
|
|
15158
15533
|
if (stats.isDirectory())
|
|
15159
15534
|
files.push(...walkFiles2(full));
|
|
@@ -15164,13 +15539,13 @@ function walkFiles2(dir) {
|
|
|
15164
15539
|
}
|
|
15165
15540
|
function resolveSnapshotPath(targetDir, snapshotPath) {
|
|
15166
15541
|
const normalizedPath = normalize3(snapshotPath);
|
|
15167
|
-
if (normalizedPath.startsWith("..") || normalizedPath.includes(`${
|
|
15542
|
+
if (normalizedPath.startsWith("..") || normalizedPath.includes(`${sep4}..${sep4}`) || normalizedPath.startsWith(sep4)) {
|
|
15168
15543
|
throw new Error(`Unsafe snapshot path: ${snapshotPath}`);
|
|
15169
15544
|
}
|
|
15170
15545
|
if (!toPosix(normalizedPath).startsWith(".skills/")) {
|
|
15171
15546
|
throw new Error(`Snapshot path must stay inside .skills: ${snapshotPath}`);
|
|
15172
15547
|
}
|
|
15173
|
-
return
|
|
15548
|
+
return join20(targetDir, normalizedPath);
|
|
15174
15549
|
}
|
|
15175
15550
|
function normalizeS3Prefix(prefix) {
|
|
15176
15551
|
return (prefix ?? "").trim().replace(/^\/+|\/+$/g, "");
|
|
@@ -15183,7 +15558,7 @@ function toIsoString(value) {
|
|
|
15183
15558
|
return Number.isNaN(date.getTime()) ? value : date.toISOString();
|
|
15184
15559
|
}
|
|
15185
15560
|
function sha256Hex2(value) {
|
|
15186
|
-
return
|
|
15561
|
+
return createHash7("sha256").update(value).digest("hex");
|
|
15187
15562
|
}
|
|
15188
15563
|
function normalizeHeaders(headers) {
|
|
15189
15564
|
const result = {};
|
|
@@ -15235,20 +15610,20 @@ function toArrayBuffer(bytes) {
|
|
|
15235
15610
|
return buffer;
|
|
15236
15611
|
}
|
|
15237
15612
|
// src/lib/station-snapshot.ts
|
|
15238
|
-
import { createHash as
|
|
15613
|
+
import { createHash as createHash8 } from "crypto";
|
|
15239
15614
|
import {
|
|
15240
15615
|
copyFileSync as copyFileSync2,
|
|
15241
|
-
mkdirSync as
|
|
15242
|
-
readFileSync as
|
|
15616
|
+
mkdirSync as mkdirSync13,
|
|
15617
|
+
readFileSync as readFileSync18,
|
|
15243
15618
|
statSync as statSync12,
|
|
15244
|
-
writeFileSync as
|
|
15619
|
+
writeFileSync as writeFileSync12
|
|
15245
15620
|
} from "fs";
|
|
15246
|
-
import { dirname as
|
|
15621
|
+
import { dirname as dirname11, isAbsolute as isAbsolute5, relative as relative8, resolve as resolve4, sep as sep6 } from "path";
|
|
15247
15622
|
|
|
15248
15623
|
// src/lib/portable-snapshot-filter.ts
|
|
15249
|
-
import { readdirSync as
|
|
15250
|
-
import { homedir as
|
|
15251
|
-
import { join as
|
|
15624
|
+
import { readdirSync as readdirSync12, statSync as statSync11 } from "fs";
|
|
15625
|
+
import { homedir as homedir5 } from "os";
|
|
15626
|
+
import { join as join21, sep as sep5 } from "path";
|
|
15252
15627
|
var SYNC_HOMES = [
|
|
15253
15628
|
{ name: "skills", subClass: "skills", agent: null },
|
|
15254
15629
|
{ name: "custom", subClass: "custom", agent: null },
|
|
@@ -15339,29 +15714,29 @@ function isPortableWithinSkill(relativeParts) {
|
|
|
15339
15714
|
return PORTABLE_SUBDIRS.has(second);
|
|
15340
15715
|
}
|
|
15341
15716
|
function homePathFor(definition, homesRoot) {
|
|
15342
|
-
const home = homesRoot ??
|
|
15717
|
+
const home = homesRoot ?? homedir5();
|
|
15343
15718
|
if (definition.subClass === "skills" || definition.subClass === "custom") {
|
|
15344
|
-
return
|
|
15719
|
+
return join21(skillsDataRootForHome(home), definition.name);
|
|
15345
15720
|
}
|
|
15346
15721
|
if (definition.agent === "opencode") {
|
|
15347
|
-
return
|
|
15722
|
+
return join21(home, ".config", "opencode", "skills");
|
|
15348
15723
|
}
|
|
15349
|
-
return
|
|
15724
|
+
return join21(home, `.${definition.agent}`, "skills");
|
|
15350
15725
|
}
|
|
15351
15726
|
function destinationFor(definition, stationId, relativePath) {
|
|
15352
|
-
const category = definition.subClass === "agent-homes" ?
|
|
15353
|
-
return
|
|
15727
|
+
const category = definition.subClass === "agent-homes" ? join21("agent-homes", definition.agent ?? "") : definition.name;
|
|
15728
|
+
return join21("resources", stationId, "skills", category, ...relativePath.split(sep5));
|
|
15354
15729
|
}
|
|
15355
15730
|
function walkEntries(absoluteRoot) {
|
|
15356
15731
|
let entries;
|
|
15357
15732
|
try {
|
|
15358
|
-
entries =
|
|
15733
|
+
entries = readdirSync12(absoluteRoot, { withFileTypes: true });
|
|
15359
15734
|
} catch {
|
|
15360
15735
|
return [];
|
|
15361
15736
|
}
|
|
15362
15737
|
const output = [];
|
|
15363
15738
|
for (const entry of entries) {
|
|
15364
|
-
const childFull =
|
|
15739
|
+
const childFull = join21(absoluteRoot, entry.name);
|
|
15365
15740
|
if (entry.isSymbolicLink()) {
|
|
15366
15741
|
output.push({ kind: "symlink", relativePath: entry.name, fullPath: childFull });
|
|
15367
15742
|
continue;
|
|
@@ -15372,7 +15747,7 @@ function walkEntries(absoluteRoot) {
|
|
|
15372
15747
|
}
|
|
15373
15748
|
const nested = walkEntries(childFull);
|
|
15374
15749
|
for (const item of nested) {
|
|
15375
|
-
output.push({ ...item, relativePath:
|
|
15750
|
+
output.push({ ...item, relativePath: join21(entry.name, item.relativePath) });
|
|
15376
15751
|
}
|
|
15377
15752
|
continue;
|
|
15378
15753
|
}
|
|
@@ -15410,7 +15785,7 @@ function validateStationId(stationId) {
|
|
|
15410
15785
|
}
|
|
15411
15786
|
}
|
|
15412
15787
|
function sha256File(filePath) {
|
|
15413
|
-
return
|
|
15788
|
+
return createHash8("sha256").update(readFileSync18(filePath)).digest("hex");
|
|
15414
15789
|
}
|
|
15415
15790
|
function scanHome(definition, homesRoot) {
|
|
15416
15791
|
const homePath = homePathFor(definition, homesRoot);
|
|
@@ -15418,7 +15793,7 @@ function scanHome(definition, homesRoot) {
|
|
|
15418
15793
|
const portable = [];
|
|
15419
15794
|
const skipped = [];
|
|
15420
15795
|
for (const entry of entries) {
|
|
15421
|
-
const relativeParts = entry.relativePath.split(
|
|
15796
|
+
const relativeParts = entry.relativePath.split(sep6);
|
|
15422
15797
|
const fileName = relativeParts[relativeParts.length - 1];
|
|
15423
15798
|
if (entry.kind === "symlink") {
|
|
15424
15799
|
skipped.push({ relativePath: entry.relativePath, reason: "symlink" });
|
|
@@ -15481,7 +15856,7 @@ function humanHomes(scanned) {
|
|
|
15481
15856
|
}));
|
|
15482
15857
|
}
|
|
15483
15858
|
function writeStationSnapshot(options) {
|
|
15484
|
-
const repoRoot =
|
|
15859
|
+
const repoRoot = resolve4(options.repoRoot ?? process.cwd());
|
|
15485
15860
|
const { scanned, plans, totalBytes } = planStationSnapshot(options);
|
|
15486
15861
|
const manifestFiles = plans.map((plan) => ({
|
|
15487
15862
|
relativePath: plan.source.relativePath,
|
|
@@ -15507,9 +15882,9 @@ function writeStationSnapshot(options) {
|
|
|
15507
15882
|
const conflicts = [];
|
|
15508
15883
|
const untouched = [];
|
|
15509
15884
|
for (const plan of plans) {
|
|
15510
|
-
const destination =
|
|
15511
|
-
const destinationRelative =
|
|
15512
|
-
if (destinationRelative.startsWith("..") || destinationRelative.startsWith(
|
|
15885
|
+
const destination = resolve4(repoRoot, plan.destination);
|
|
15886
|
+
const destinationRelative = relative8(repoRoot, destination);
|
|
15887
|
+
if (destinationRelative.startsWith("..") || destinationRelative.startsWith(sep6) || isAbsolute5(destinationRelative)) {
|
|
15513
15888
|
throw new StationSnapshotError("DESTINATION_ESCAPE", `destination escapes repo root: ${plan.destination}`);
|
|
15514
15889
|
}
|
|
15515
15890
|
let existingDigest = null;
|
|
@@ -15530,8 +15905,8 @@ function writeStationSnapshot(options) {
|
|
|
15530
15905
|
}
|
|
15531
15906
|
let written = 0;
|
|
15532
15907
|
for (const plan of untouched) {
|
|
15533
|
-
const destination =
|
|
15534
|
-
|
|
15908
|
+
const destination = resolve4(repoRoot, plan.destination);
|
|
15909
|
+
mkdirSync13(dirname11(destination), { recursive: true });
|
|
15535
15910
|
copyFileSync2(plan.source.fullPath, destination);
|
|
15536
15911
|
written += 1;
|
|
15537
15912
|
}
|
|
@@ -15549,9 +15924,9 @@ function writeStationSnapshot(options) {
|
|
|
15549
15924
|
},
|
|
15550
15925
|
files: manifestFiles
|
|
15551
15926
|
};
|
|
15552
|
-
const manifestPath =
|
|
15553
|
-
|
|
15554
|
-
|
|
15927
|
+
const manifestPath = resolve4(repoRoot, "resources", options.stationId, "skills", "sync-manifest.json");
|
|
15928
|
+
mkdirSync13(dirname11(manifestPath), { recursive: true });
|
|
15929
|
+
writeFileSync12(manifestPath, `${JSON.stringify(manifest, null, 2)}
|
|
15555
15930
|
`);
|
|
15556
15931
|
return {
|
|
15557
15932
|
...base,
|
|
@@ -15561,16 +15936,16 @@ function writeStationSnapshot(options) {
|
|
|
15561
15936
|
};
|
|
15562
15937
|
}
|
|
15563
15938
|
// src/lib/station-hydrate.ts
|
|
15564
|
-
import { createHash as
|
|
15939
|
+
import { createHash as createHash9 } from "crypto";
|
|
15565
15940
|
import {
|
|
15566
15941
|
copyFileSync as copyFileSync3,
|
|
15567
|
-
mkdirSync as
|
|
15568
|
-
readdirSync as
|
|
15569
|
-
readFileSync as
|
|
15942
|
+
mkdirSync as mkdirSync14,
|
|
15943
|
+
readdirSync as readdirSync13,
|
|
15944
|
+
readFileSync as readFileSync19,
|
|
15570
15945
|
statSync as statSync13,
|
|
15571
|
-
writeFileSync as
|
|
15946
|
+
writeFileSync as writeFileSync13
|
|
15572
15947
|
} from "fs";
|
|
15573
|
-
import { dirname as
|
|
15948
|
+
import { dirname as dirname12, join as join22, resolve as resolve5, sep as sep7 } from "path";
|
|
15574
15949
|
var STATION_HYDRATION_MANIFEST_SCHEMA = "hasna.fleet-resources.skills-hydration-manifest/v1";
|
|
15575
15950
|
var STATION_HYDRATION_PRODUCER = { name: "@hasna/skills", version: package_default.version };
|
|
15576
15951
|
var MANIFEST_HASH_KEY_SEP = String.fromCharCode(0);
|
|
@@ -15578,14 +15953,14 @@ function fail(code, message, detail = []) {
|
|
|
15578
15953
|
throw new StationSnapshotError(code, message, detail);
|
|
15579
15954
|
}
|
|
15580
15955
|
function snapshotRootFor(repoRoot, stationId) {
|
|
15581
|
-
return
|
|
15956
|
+
return join22(repoRoot, "resources", stationId, "skills");
|
|
15582
15957
|
}
|
|
15583
15958
|
function readSnapshotManifest(repoRoot, stationId) {
|
|
15584
15959
|
const snapshotRoot = snapshotRootFor(repoRoot, stationId);
|
|
15585
|
-
const manifestPath =
|
|
15960
|
+
const manifestPath = join22(snapshotRoot, "sync-manifest.json");
|
|
15586
15961
|
let manifest;
|
|
15587
15962
|
try {
|
|
15588
|
-
manifest = JSON.parse(
|
|
15963
|
+
manifest = JSON.parse(readFileSync19(manifestPath, "utf8"));
|
|
15589
15964
|
} catch (error) {
|
|
15590
15965
|
fail("MANIFEST_UNREADABLE", `cannot read snapshot manifest: ${manifestPath}: ${error.message}`);
|
|
15591
15966
|
}
|
|
@@ -15609,10 +15984,10 @@ function planStationHydration(stationId, repoRoot) {
|
|
|
15609
15984
|
const hashMismatches = [];
|
|
15610
15985
|
const skippedByRule = [];
|
|
15611
15986
|
for (const agent of SYNC_AGENTS) {
|
|
15612
|
-
const agentRoot =
|
|
15987
|
+
const agentRoot = join22(snapshotRoot, "agent-homes", agent);
|
|
15613
15988
|
let identEntries;
|
|
15614
15989
|
try {
|
|
15615
|
-
identEntries =
|
|
15990
|
+
identEntries = readdirSync13(agentRoot, { withFileTypes: true });
|
|
15616
15991
|
} catch {
|
|
15617
15992
|
continue;
|
|
15618
15993
|
}
|
|
@@ -15620,10 +15995,10 @@ function planStationHydration(stationId, repoRoot) {
|
|
|
15620
15995
|
if (!identEntry.isDirectory() || identEntry.name.startsWith(".")) {
|
|
15621
15996
|
continue;
|
|
15622
15997
|
}
|
|
15623
|
-
const identRoot =
|
|
15998
|
+
const identRoot = join22(agentRoot, identEntry.name);
|
|
15624
15999
|
const entries = walkEntries(identRoot);
|
|
15625
16000
|
for (const entry of entries) {
|
|
15626
|
-
const relativeParts = [identEntry.name, ...entry.relativePath.split(
|
|
16001
|
+
const relativeParts = [identEntry.name, ...entry.relativePath.split(sep7)];
|
|
15627
16002
|
if (entry.kind === "symlink") {
|
|
15628
16003
|
symlinks.push({ ident: identEntry.name, agent, relativePath: entry.relativePath });
|
|
15629
16004
|
continue;
|
|
@@ -15647,8 +16022,8 @@ function planStationHydration(stationId, repoRoot) {
|
|
|
15647
16022
|
});
|
|
15648
16023
|
continue;
|
|
15649
16024
|
}
|
|
15650
|
-
const withinIdent = relativeParts.slice(1).join(
|
|
15651
|
-
const homeRelative = relativeParts.join(
|
|
16025
|
+
const withinIdent = relativeParts.slice(1).join(sep7);
|
|
16026
|
+
const homeRelative = relativeParts.join(sep7);
|
|
15652
16027
|
if (REFUSED_SCANNER_FLAGGED.has(homeRelative)) {
|
|
15653
16028
|
skippedByRule.push({
|
|
15654
16029
|
ident: identEntry.name,
|
|
@@ -15721,7 +16096,7 @@ function planStationHydration(stationId, repoRoot) {
|
|
|
15721
16096
|
for (const copy of copies) {
|
|
15722
16097
|
let isStub = false;
|
|
15723
16098
|
try {
|
|
15724
|
-
isStub = isPointerSkillMd(
|
|
16099
|
+
isStub = isPointerSkillMd(readFileSync19(copy.fullPath, "utf8"));
|
|
15725
16100
|
} catch {
|
|
15726
16101
|
isStub = false;
|
|
15727
16102
|
}
|
|
@@ -15767,12 +16142,12 @@ function skillSha256(skill) {
|
|
|
15767
16142
|
return sha256File(skill.files[0].winner.fullPath);
|
|
15768
16143
|
}
|
|
15769
16144
|
const joined = skill.files.map((file) => sha256File(file.winner.fullPath));
|
|
15770
|
-
return
|
|
16145
|
+
return createHash9("sha256").update(joined.sort().join(`
|
|
15771
16146
|
`)).digest("hex");
|
|
15772
16147
|
}
|
|
15773
16148
|
function writeStationHydration(options) {
|
|
15774
|
-
const repoRoot =
|
|
15775
|
-
const cacheRoot =
|
|
16149
|
+
const repoRoot = resolve5(options.repoRoot ?? process.cwd());
|
|
16150
|
+
const cacheRoot = resolve5(options.cacheRoot ?? resolveCorpusRoot());
|
|
15776
16151
|
const plan = planStationHydration(options.stationId, repoRoot);
|
|
15777
16152
|
const resultSkills = plan.winners.map((skill) => ({
|
|
15778
16153
|
ident: skill.ident,
|
|
@@ -15805,7 +16180,7 @@ function writeStationHydration(options) {
|
|
|
15805
16180
|
const toWrite = [];
|
|
15806
16181
|
for (const skill of plan.winners) {
|
|
15807
16182
|
for (const file of skill.files) {
|
|
15808
|
-
const destination =
|
|
16183
|
+
const destination = join22(cacheRoot, skill.ident, file.withinIdent);
|
|
15809
16184
|
const digest = sha256File(file.winner.fullPath);
|
|
15810
16185
|
let existingDigest = null;
|
|
15811
16186
|
try {
|
|
@@ -15826,7 +16201,7 @@ function writeStationHydration(options) {
|
|
|
15826
16201
|
}
|
|
15827
16202
|
let written = 0;
|
|
15828
16203
|
for (const entry of toWrite) {
|
|
15829
|
-
|
|
16204
|
+
mkdirSync14(dirname12(entry.destination), { recursive: true });
|
|
15830
16205
|
copyFileSync3(entry.fullPath, entry.destination);
|
|
15831
16206
|
written += 1;
|
|
15832
16207
|
}
|
|
@@ -15847,9 +16222,9 @@ function writeStationHydration(options) {
|
|
|
15847
16222
|
},
|
|
15848
16223
|
skills: resultSkills
|
|
15849
16224
|
};
|
|
15850
|
-
const hydrationManifestPath =
|
|
15851
|
-
|
|
15852
|
-
|
|
16225
|
+
const hydrationManifestPath = join22(dirname12(cacheRoot), `hydration-${options.stationId}.json`);
|
|
16226
|
+
mkdirSync14(dirname12(hydrationManifestPath), { recursive: true });
|
|
16227
|
+
writeFileSync13(hydrationManifestPath, `${JSON.stringify(hydration, null, 2)}
|
|
15853
16228
|
`);
|
|
15854
16229
|
return {
|
|
15855
16230
|
...base,
|
|
@@ -15954,7 +16329,7 @@ async function requestInvitationEmail(origin, action, input) {
|
|
|
15954
16329
|
}
|
|
15955
16330
|
|
|
15956
16331
|
// src/lib/remote-private-publications.ts
|
|
15957
|
-
import { createHash as
|
|
16332
|
+
import { createHash as createHash10 } from "crypto";
|
|
15958
16333
|
var PRIVATE_PUBLICATION_MAX_BYTES = 16 * 1024 * 1024;
|
|
15959
16334
|
var failures = {
|
|
15960
16335
|
INVALID_REQUEST: [400, "The publication request is invalid."],
|
|
@@ -16001,7 +16376,7 @@ var hash = (v) => typeof v === "string" && /^[a-f0-9]{64}$/.test(v);
|
|
|
16001
16376
|
var record5 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
16002
16377
|
var exact = (v, keys) => record5(v) && Object.keys(v).sort().join(",") === keys.sort().join(",");
|
|
16003
16378
|
var date = (v) => typeof v === "string" && /^\d{4}-\d\d-\d\dT[0-9:.]+(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/.test(v) && Number.isFinite(Date.parse(v));
|
|
16004
|
-
var publicationSha256 = (bytes) =>
|
|
16379
|
+
var publicationSha256 = (bytes) => createHash10("sha256").update(bytes).digest("hex");
|
|
16005
16380
|
function checkedPublicationDeclaration(value) {
|
|
16006
16381
|
if (!exact(value, ["idempotencyKey", "version", "expectedCurrentVersionId", "manifestText", "archiveSha256", "archiveByteSize"]) || !publicationUuid(value.idempotencyKey) || !(value.expectedCurrentVersionId === null || publicationUuid(value.expectedCurrentVersionId)) || typeof value.version !== "string" || !value.version || value.version.length > 128 || /[\p{Cc}\p{Cs}]/u.test(value.version) || typeof value.manifestText !== "string" || Buffer.byteLength(value.manifestText) > 16384 || !hash(value.archiveSha256) || !Number.isSafeInteger(value.archiveByteSize) || Number(value.archiveByteSize) < 1 || Number(value.archiveByteSize) > PRIVATE_PUBLICATION_MAX_BYTES)
|
|
16007
16382
|
return bad();
|
|
@@ -16246,12 +16621,12 @@ class RemotePrivatePublicationsClient {
|
|
|
16246
16621
|
previous = view;
|
|
16247
16622
|
if (!["queued", "verifying"].includes(view.state) || Date.now() >= until)
|
|
16248
16623
|
return view;
|
|
16249
|
-
await new Promise((
|
|
16624
|
+
await new Promise((resolve6) => {
|
|
16250
16625
|
const timer = setTimeout(done, Math.min(1000, until - Date.now()));
|
|
16251
16626
|
function done() {
|
|
16252
16627
|
clearTimeout(timer);
|
|
16253
16628
|
options.signal?.removeEventListener("abort", done);
|
|
16254
|
-
|
|
16629
|
+
resolve6();
|
|
16255
16630
|
}
|
|
16256
16631
|
options.signal?.addEventListener("abort", done, { once: true });
|
|
16257
16632
|
});
|
|
@@ -16483,23 +16858,23 @@ class RemoteSkillsAuthClient {
|
|
|
16483
16858
|
}
|
|
16484
16859
|
}
|
|
16485
16860
|
// src/lib/private-publication-recovery.ts
|
|
16486
|
-
import { constants as constants2, closeSync as closeSync3, fsyncSync, fstatSync as fstatSync3, lstatSync as
|
|
16487
|
-
import { dirname as
|
|
16488
|
-
import { randomUUID } from "crypto";
|
|
16861
|
+
import { constants as constants2, closeSync as closeSync3, fsyncSync, fstatSync as fstatSync3, lstatSync as lstatSync6, mkdirSync as mkdirSync15, openSync as openSync3, readSync as readSync2, realpathSync as realpathSync4, renameSync as renameSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync14 } from "fs";
|
|
16862
|
+
import { dirname as dirname13, isAbsolute as isAbsolute6, join as join23, resolve as resolve6 } from "path";
|
|
16863
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
16489
16864
|
var fail2 = () => {
|
|
16490
16865
|
throw new PrivatePublicationError("PUBLICATION_RECOVERY_INVALID", "The recovery directory is invalid or changed. Preserve it and inspect the existing intent; do not start a replacement automatically.");
|
|
16491
16866
|
};
|
|
16492
16867
|
function safeDirectory(directory) {
|
|
16493
|
-
if (!
|
|
16868
|
+
if (!isAbsolute6(directory) || directory !== resolve6(directory) || realpathSync4(directory) !== directory)
|
|
16494
16869
|
return fail2();
|
|
16495
|
-
for (let path = directory;; path =
|
|
16496
|
-
const stat =
|
|
16870
|
+
for (let path = directory;; path = dirname13(path)) {
|
|
16871
|
+
const stat = lstatSync6(path);
|
|
16497
16872
|
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
16498
16873
|
return fail2();
|
|
16499
|
-
if (path ===
|
|
16874
|
+
if (path === dirname13(path))
|
|
16500
16875
|
break;
|
|
16501
16876
|
}
|
|
16502
|
-
const own =
|
|
16877
|
+
const own = lstatSync6(directory);
|
|
16503
16878
|
if ((own.mode & 63) !== 0 || process.getuid && own.uid !== process.getuid())
|
|
16504
16879
|
return fail2();
|
|
16505
16880
|
return { dev: own.dev, ino: own.ino };
|
|
@@ -16510,7 +16885,7 @@ function unchangedDirectory(directory, identity) {
|
|
|
16510
16885
|
return fail2();
|
|
16511
16886
|
}
|
|
16512
16887
|
function readOwned(directory, name, max) {
|
|
16513
|
-
const identity = safeDirectory(directory), file =
|
|
16888
|
+
const identity = safeDirectory(directory), file = join23(directory, name);
|
|
16514
16889
|
const fd = openSync3(file, constants2.O_RDONLY | constants2.O_NOFOLLOW);
|
|
16515
16890
|
try {
|
|
16516
16891
|
const stat = fstatSync3(fd);
|
|
@@ -16534,20 +16909,20 @@ function readOwned(directory, name, max) {
|
|
|
16534
16909
|
}
|
|
16535
16910
|
}
|
|
16536
16911
|
function writeOwned(directory, name, bytes) {
|
|
16537
|
-
const fd = openSync3(
|
|
16912
|
+
const fd = openSync3(join23(directory, name), constants2.O_WRONLY | constants2.O_CREAT | constants2.O_EXCL | constants2.O_NOFOLLOW, 384);
|
|
16538
16913
|
try {
|
|
16539
|
-
|
|
16914
|
+
writeFileSync14(fd, bytes);
|
|
16540
16915
|
fsyncSync(fd);
|
|
16541
16916
|
} finally {
|
|
16542
16917
|
closeSync3(fd);
|
|
16543
16918
|
}
|
|
16544
16919
|
}
|
|
16545
16920
|
function save(directory, value) {
|
|
16546
|
-
const identity = safeDirectory(directory), temporary = `.receipt-${
|
|
16921
|
+
const identity = safeDirectory(directory), temporary = `.receipt-${randomUUID2()}.json`;
|
|
16547
16922
|
writeOwned(directory, temporary, JSON.stringify(value) + `
|
|
16548
16923
|
`);
|
|
16549
16924
|
unchangedDirectory(directory, identity);
|
|
16550
|
-
|
|
16925
|
+
renameSync5(join23(directory, temporary), join23(directory, "receipt.json"));
|
|
16551
16926
|
const fd = openSync3(directory, constants2.O_RDONLY | constants2.O_NOFOLLOW);
|
|
16552
16927
|
try {
|
|
16553
16928
|
fsyncSync(fd);
|
|
@@ -16560,7 +16935,7 @@ function bind(client, receipt) {
|
|
|
16560
16935
|
throw new PrivatePublicationError("PUBLICATION_IDENTITY_CHANGED", "The fresh session does not match the recovery directory's server, account and membership.");
|
|
16561
16936
|
}
|
|
16562
16937
|
async function locked(directory, action) {
|
|
16563
|
-
const identity = safeDirectory(directory), file =
|
|
16938
|
+
const identity = safeDirectory(directory), file = join23(directory, "operation.lock");
|
|
16564
16939
|
let fd;
|
|
16565
16940
|
try {
|
|
16566
16941
|
fd = openSync3(file, constants2.O_WRONLY | constants2.O_CREAT | constants2.O_EXCL | constants2.O_NOFOLLOW, 384);
|
|
@@ -16569,17 +16944,17 @@ async function locked(directory, action) {
|
|
|
16569
16944
|
}
|
|
16570
16945
|
const lock = fstatSync3(fd);
|
|
16571
16946
|
try {
|
|
16572
|
-
|
|
16947
|
+
writeFileSync14(fd, JSON.stringify({ pid: process.pid }) + `
|
|
16573
16948
|
`);
|
|
16574
16949
|
fsyncSync(fd);
|
|
16575
16950
|
return await action();
|
|
16576
16951
|
} finally {
|
|
16577
16952
|
closeSync3(fd);
|
|
16578
16953
|
unchangedDirectory(directory, identity);
|
|
16579
|
-
const now =
|
|
16954
|
+
const now = lstatSync6(file);
|
|
16580
16955
|
if (now.dev !== lock.dev || now.ino !== lock.ino || !now.isFile() || now.isSymbolicLink())
|
|
16581
16956
|
fail2();
|
|
16582
|
-
|
|
16957
|
+
unlinkSync2(file);
|
|
16583
16958
|
}
|
|
16584
16959
|
}
|
|
16585
16960
|
function readPrivatePublicationRecovery(directory) {
|
|
@@ -16610,7 +16985,7 @@ async function preparePrivatePublication(client, sourceDirectory, recoveryDirect
|
|
|
16610
16985
|
throw new PrivatePublicationError("PUBLICATION_BUNDLE_INVALID", "The skill must have a valid skill.json with its current content hash. Validate the skill before publishing.");
|
|
16611
16986
|
const manifestText = new TextDecoder("utf-8", { fatal: true }).decode(manifest.bytes);
|
|
16612
16987
|
const declaration = checkedPublicationDeclaration({
|
|
16613
|
-
idempotencyKey: input.idempotencyKey ??
|
|
16988
|
+
idempotencyKey: input.idempotencyKey ?? randomUUID2(),
|
|
16614
16989
|
version: JSON.parse(manifestText).version,
|
|
16615
16990
|
expectedCurrentVersionId: input.expectedCurrentVersionId,
|
|
16616
16991
|
manifestText,
|
|
@@ -16628,11 +17003,11 @@ async function preparePrivatePublication(client, sourceDirectory, recoveryDirect
|
|
|
16628
17003
|
phase: "prepared",
|
|
16629
17004
|
intent: null
|
|
16630
17005
|
};
|
|
16631
|
-
if (!
|
|
17006
|
+
if (!isAbsolute6(recoveryDirectory) || resolve6(recoveryDirectory) !== recoveryDirectory || realpathSync4(dirname13(recoveryDirectory)) !== dirname13(recoveryDirectory))
|
|
16632
17007
|
return fail2();
|
|
16633
|
-
|
|
17008
|
+
mkdirSync15(recoveryDirectory, { mode: 448 });
|
|
16634
17009
|
safeDirectory(recoveryDirectory);
|
|
16635
|
-
const parent = openSync3(
|
|
17010
|
+
const parent = openSync3(dirname13(recoveryDirectory), constants2.O_RDONLY | constants2.O_NOFOLLOW);
|
|
16636
17011
|
try {
|
|
16637
17012
|
fsyncSync(parent);
|
|
16638
17013
|
} finally {
|
|
@@ -16718,6 +17093,565 @@ async function inspectLocked(client, directory, cancel) {
|
|
|
16718
17093
|
throw new PrivatePublicationError("PUBLICATION_INTENT_UNKNOWN", "Reconcile the saved begin request with publication resume before cancelling its intent.");
|
|
16719
17094
|
return privatePublicationResult(directory, receipt);
|
|
16720
17095
|
}
|
|
17096
|
+
// src/lib/profile-client.ts
|
|
17097
|
+
function object2(value) {
|
|
17098
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
17099
|
+
}
|
|
17100
|
+
function identifier(value) {
|
|
17101
|
+
return typeof value === "string" && /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/.test(value) && !value.includes("..");
|
|
17102
|
+
}
|
|
17103
|
+
function invalid4() {
|
|
17104
|
+
throw new Error("The Skills API returned an invalid profile response");
|
|
17105
|
+
}
|
|
17106
|
+
function validateSelection(value) {
|
|
17107
|
+
if (!object2(value) || !identifier(value.slug) || typeof value.version !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._+-]{0,127}$/.test(value.version) || value.version.includes("..") || typeof value.bundleDigest !== "string" || !/^sha256:[a-f0-9]{64}$/.test(value.bundleDigest))
|
|
17108
|
+
invalid4();
|
|
17109
|
+
if (value.triggers !== undefined) {
|
|
17110
|
+
if (!object2(value.triggers) || Object.keys(value.triggers).some((key) => !["keywords", "paths", "always"].includes(key)))
|
|
17111
|
+
invalid4();
|
|
17112
|
+
if (value.triggers.always !== undefined && typeof value.triggers.always !== "boolean")
|
|
17113
|
+
invalid4();
|
|
17114
|
+
for (const key of ["keywords", "paths"]) {
|
|
17115
|
+
const values = value.triggers[key];
|
|
17116
|
+
if (values !== undefined && (!Array.isArray(values) || values.length > 32 || values.some((term) => typeof term !== "string" || !term.trim() || term.length > 256)))
|
|
17117
|
+
invalid4();
|
|
17118
|
+
}
|
|
17119
|
+
}
|
|
17120
|
+
}
|
|
17121
|
+
|
|
17122
|
+
class HttpProfileClient {
|
|
17123
|
+
key;
|
|
17124
|
+
authority;
|
|
17125
|
+
origin;
|
|
17126
|
+
constructor(key, origin) {
|
|
17127
|
+
this.key = key;
|
|
17128
|
+
if (!key.trim())
|
|
17129
|
+
throw new Error("A Skills credential is required");
|
|
17130
|
+
this.origin = normalizeSkillsApiOrigin(origin);
|
|
17131
|
+
this.authority = skillsApiRequestUrl(this.origin, "/api/v1/").replace(/\/+$/, "");
|
|
17132
|
+
}
|
|
17133
|
+
async request(path, init = {}) {
|
|
17134
|
+
let response;
|
|
17135
|
+
try {
|
|
17136
|
+
response = await fetch(skillsApiRequestUrl(this.origin, `/api/v1${path}`), {
|
|
17137
|
+
...init,
|
|
17138
|
+
redirect: "error",
|
|
17139
|
+
credentials: "omit",
|
|
17140
|
+
signal: AbortSignal.timeout(15000),
|
|
17141
|
+
headers: {
|
|
17142
|
+
"Content-Type": "application/json",
|
|
17143
|
+
"User-Agent": `hasna-skills/${package_default.version}`,
|
|
17144
|
+
Authorization: `Bearer ${this.key}`,
|
|
17145
|
+
...init.headers
|
|
17146
|
+
}
|
|
17147
|
+
});
|
|
17148
|
+
} catch {
|
|
17149
|
+
throw new Error("Unable to reach the configured Skills API");
|
|
17150
|
+
}
|
|
17151
|
+
if (!response.ok) {
|
|
17152
|
+
response.body?.cancel().catch(() => {});
|
|
17153
|
+
throw new Error(`Skills API request failed (HTTP ${response.status})`);
|
|
17154
|
+
}
|
|
17155
|
+
return response;
|
|
17156
|
+
}
|
|
17157
|
+
async read(response) {
|
|
17158
|
+
try {
|
|
17159
|
+
return JSON.parse(new TextDecoder().decode(await readBoundedResponse(response, 1e6)));
|
|
17160
|
+
} catch {
|
|
17161
|
+
invalid4();
|
|
17162
|
+
}
|
|
17163
|
+
}
|
|
17164
|
+
async resolveProfile(id) {
|
|
17165
|
+
if (!identifier(id))
|
|
17166
|
+
throw new Error("Invalid selection profile id");
|
|
17167
|
+
const result = await this.read(await this.request(`/profiles/${encodeURIComponent(id)}/resolve`));
|
|
17168
|
+
if (!object2(result) || result.profileId !== id || result.authority !== this.authority || !identifier(result.workspaceId) || !identifier(result.profileRevision) || !Array.isArray(result.selections) || result.selections.length > 256)
|
|
17169
|
+
invalid4();
|
|
17170
|
+
const seen = new Set;
|
|
17171
|
+
for (const entry of result.selections) {
|
|
17172
|
+
validateSelection(entry);
|
|
17173
|
+
const item = entry;
|
|
17174
|
+
if (seen.has(item.slug) || item.authority !== this.authority || item.workspaceId !== result.workspaceId || item.profileRevision !== result.profileRevision)
|
|
17175
|
+
invalid4();
|
|
17176
|
+
seen.add(item.slug);
|
|
17177
|
+
}
|
|
17178
|
+
return result;
|
|
17179
|
+
}
|
|
17180
|
+
async recordStation(id, input) {
|
|
17181
|
+
if (!identifier(id) || !identifier(input.profileId) || !identifier(input.profileRevision) || !Array.isArray(input.selections) || input.selections.length > 256)
|
|
17182
|
+
throw new Error("Invalid station state");
|
|
17183
|
+
input.selections.forEach(validateSelection);
|
|
17184
|
+
const result = await this.read(await this.request(`/stations/${encodeURIComponent(id)}/state`, {
|
|
17185
|
+
method: "PUT",
|
|
17186
|
+
body: JSON.stringify(input)
|
|
17187
|
+
}));
|
|
17188
|
+
if (!object2(result) || result.stationId !== id || result.profileId !== input.profileId || result.profileRevision !== input.profileRevision || !identifier(result.workspaceId) || !identifier(result.actorId) || typeof result.appliedAt !== "string" || !Array.isArray(result.selections))
|
|
17189
|
+
invalid4();
|
|
17190
|
+
result.selections.forEach(validateSelection);
|
|
17191
|
+
return result;
|
|
17192
|
+
}
|
|
17193
|
+
async getBundle(slug, version) {
|
|
17194
|
+
if (!identifier(slug) || !/^[A-Za-z0-9][A-Za-z0-9._+-]{0,127}$/.test(version) || version.includes(".."))
|
|
17195
|
+
throw new Error("Invalid immutable skill reference");
|
|
17196
|
+
return this.request(`/skills/${encodeURIComponent(slug)}/versions/${encodeURIComponent(version)}/bundle`);
|
|
17197
|
+
}
|
|
17198
|
+
}
|
|
17199
|
+
async function createProfileClient() {
|
|
17200
|
+
const connection = await resolveSkillsConnection();
|
|
17201
|
+
if (!connection)
|
|
17202
|
+
throw new Error("Selection profiles require a configured Skills API");
|
|
17203
|
+
return new HttpProfileClient(connection.apiKey, connection.apiOrigin);
|
|
17204
|
+
}
|
|
17205
|
+
// src/lib/skill-context.ts
|
|
17206
|
+
import { createHash as createHash12 } from "crypto";
|
|
17207
|
+
import { resolve as resolve8 } from "path";
|
|
17208
|
+
|
|
17209
|
+
// src/lib/selection-cache.ts
|
|
17210
|
+
import { createHash as createHash11, randomUUID as randomUUID3 } from "crypto";
|
|
17211
|
+
import { closeSync as closeSync4, constants as constants3, fstatSync as fstatSync4, lstatSync as lstatSync7, mkdirSync as mkdirSync16, openSync as openSync4, readFileSync as readFileSync20, renameSync as renameSync6, unlinkSync as unlinkSync3, writeFileSync as writeFileSync15 } from "fs";
|
|
17212
|
+
import { dirname as dirname14, join as join24, parse, resolve as resolve7 } from "path";
|
|
17213
|
+
class SkillSelectionError extends Error {
|
|
17214
|
+
code;
|
|
17215
|
+
constructor(code, message) {
|
|
17216
|
+
super(message);
|
|
17217
|
+
this.code = code;
|
|
17218
|
+
this.name = "SkillSelectionError";
|
|
17219
|
+
}
|
|
17220
|
+
}
|
|
17221
|
+
var MAX_CACHED_PROFILE_AGE_MS = 24 * 60 * 60 * 1000;
|
|
17222
|
+
var SELECTION_LOCK_FILE = "selection.lock.json";
|
|
17223
|
+
var MAX_RECEIPT_BYTES = 1024 * 1024;
|
|
17224
|
+
function selectionCacheRoot(options = {}) {
|
|
17225
|
+
return resolve7(options.cacheDir ?? join24(getDataDirReadOnly(), "selection-cache"));
|
|
17226
|
+
}
|
|
17227
|
+
function selectionKey(selection) {
|
|
17228
|
+
validateSelection2(selection);
|
|
17229
|
+
return hash2([selection.authority, selection.workspaceId, selection.slug, selection.version, selection.bundleDigest]);
|
|
17230
|
+
}
|
|
17231
|
+
function hash2(value) {
|
|
17232
|
+
return createHash11("sha256").update(JSON.stringify(value)).digest("hex");
|
|
17233
|
+
}
|
|
17234
|
+
function validateSelection2(selection) {
|
|
17235
|
+
if (!selection || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(selection.slug) || !isValidSkillVersion(selection.version) || !/^sha256:[a-f0-9]{64}$/.test(selection.bundleDigest) || typeof selection.workspaceId !== "string" || !selection.workspaceId.trim() || typeof selection.profileRevision !== "string" || !selection.profileRevision.trim()) {
|
|
17236
|
+
throw new SkillSelectionError("INVALID_SELECTION", "A skill selection must carry an exact version, digest, workspace and profile revision.");
|
|
17237
|
+
}
|
|
17238
|
+
try {
|
|
17239
|
+
const url = new URL(selection.authority);
|
|
17240
|
+
if (url.username || url.password || url.search || url.hash || !["https:", "http:"].includes(url.protocol) || url.protocol === "http:" && !["localhost", "127.0.0.1", "[::1]"].includes(url.hostname))
|
|
17241
|
+
throw new Error;
|
|
17242
|
+
} catch {
|
|
17243
|
+
throw new SkillSelectionError("INVALID_SELECTION", "The skill selection authority is invalid.");
|
|
17244
|
+
}
|
|
17245
|
+
}
|
|
17246
|
+
function validateResolvedProfile(profile, authority) {
|
|
17247
|
+
if (!profile || typeof profile.profileId !== "string" || !profile.profileId.trim() || !Array.isArray(profile.selections) || profile.selections.length > 1000 || typeof profile.workspaceId !== "string" || !profile.workspaceId.trim() || typeof profile.profileRevision !== "string" || !profile.profileRevision.trim() || typeof profile.authority !== "string" || authority !== undefined && profile.authority !== authority) {
|
|
17248
|
+
throw new SkillSelectionError("PROFILE_IDENTITY_MISMATCH", "The resolved profile does not match the configured Skills authority.");
|
|
17249
|
+
}
|
|
17250
|
+
const slugs = new Set;
|
|
17251
|
+
for (const selection of profile.selections) {
|
|
17252
|
+
validateSelection2(selection);
|
|
17253
|
+
if (selection.authority !== profile.authority || selection.workspaceId !== profile.workspaceId || selection.profileRevision !== profile.profileRevision || slugs.has(selection.slug)) {
|
|
17254
|
+
throw new SkillSelectionError("PROFILE_IDENTITY_MISMATCH", "The resolved profile contains conflicting selection identities.");
|
|
17255
|
+
}
|
|
17256
|
+
slugs.add(selection.slug);
|
|
17257
|
+
}
|
|
17258
|
+
}
|
|
17259
|
+
function selectionBundlePath(selection, options = {}) {
|
|
17260
|
+
validateSelection2(selection);
|
|
17261
|
+
return join24(selectionCacheRoot(options), "objects", hash2(selection.authority), hash2(selection.workspaceId), `${selection.bundleDigest.slice(7)}.tar.gz`);
|
|
17262
|
+
}
|
|
17263
|
+
function assertRegularPath(path, createParents = false) {
|
|
17264
|
+
const absolute = resolve7(path);
|
|
17265
|
+
const root = parse(absolute).root;
|
|
17266
|
+
let cursor2 = root;
|
|
17267
|
+
const segments = absolute.slice(root.length).split("/");
|
|
17268
|
+
for (let index = 0;index < segments.length; index++) {
|
|
17269
|
+
cursor2 = join24(cursor2, segments[index]);
|
|
17270
|
+
try {
|
|
17271
|
+
const stat = lstatSync7(cursor2);
|
|
17272
|
+
if (stat.isSymbolicLink() || index < segments.length - 1 && !stat.isDirectory()) {
|
|
17273
|
+
throw new SkillSelectionError("UNSAFE_CACHE_PATH", "Skills cache paths must not traverse symlinks or non-directories.");
|
|
17274
|
+
}
|
|
17275
|
+
} catch (error) {
|
|
17276
|
+
if (error.code !== "ENOENT")
|
|
17277
|
+
throw error;
|
|
17278
|
+
if (createParents && index < segments.length - 1)
|
|
17279
|
+
mkdirSync16(cursor2, { mode: 448 });
|
|
17280
|
+
}
|
|
17281
|
+
}
|
|
17282
|
+
}
|
|
17283
|
+
function readRegularFile(path, limit) {
|
|
17284
|
+
assertRegularPath(path);
|
|
17285
|
+
let fd;
|
|
17286
|
+
try {
|
|
17287
|
+
fd = openSync4(path, constants3.O_RDONLY | constants3.O_NOFOLLOW);
|
|
17288
|
+
} catch (error) {
|
|
17289
|
+
if (error.code === "ENOENT")
|
|
17290
|
+
return null;
|
|
17291
|
+
throw error;
|
|
17292
|
+
}
|
|
17293
|
+
try {
|
|
17294
|
+
const stat = fstatSync4(fd);
|
|
17295
|
+
if (!stat.isFile() || stat.size > limit)
|
|
17296
|
+
throw new SkillSelectionError("INVALID_CACHE_FILE", "The Skills cache contains an invalid or oversized file.");
|
|
17297
|
+
return readFileSync20(fd);
|
|
17298
|
+
} finally {
|
|
17299
|
+
closeSync4(fd);
|
|
17300
|
+
}
|
|
17301
|
+
}
|
|
17302
|
+
function writeSelectionJson(path, value) {
|
|
17303
|
+
const bytes = Buffer.from(`${JSON.stringify(value, null, 2)}
|
|
17304
|
+
`);
|
|
17305
|
+
if (bytes.length > MAX_RECEIPT_BYTES)
|
|
17306
|
+
throw new SkillSelectionError("RECEIPT_TOO_LARGE", "The Skills selection receipt exceeds its size limit.");
|
|
17307
|
+
atomicWrite2(path, bytes, 384);
|
|
17308
|
+
}
|
|
17309
|
+
function atomicWrite2(path, bytes, mode) {
|
|
17310
|
+
assertRegularPath(path, true);
|
|
17311
|
+
const temporary = join24(dirname14(path), `.selection-${randomUUID3()}.tmp`);
|
|
17312
|
+
try {
|
|
17313
|
+
writeFileSync15(temporary, bytes, { flag: "wx", mode });
|
|
17314
|
+
renameSync6(temporary, path);
|
|
17315
|
+
} finally {
|
|
17316
|
+
try {
|
|
17317
|
+
unlinkSync3(temporary);
|
|
17318
|
+
} catch (error) {
|
|
17319
|
+
if (error.code !== "ENOENT")
|
|
17320
|
+
throw error;
|
|
17321
|
+
}
|
|
17322
|
+
}
|
|
17323
|
+
}
|
|
17324
|
+
function readSelectionJson(path) {
|
|
17325
|
+
const bytes = readRegularFile(path, MAX_RECEIPT_BYTES);
|
|
17326
|
+
if (!bytes)
|
|
17327
|
+
return null;
|
|
17328
|
+
try {
|
|
17329
|
+
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
17330
|
+
} catch {
|
|
17331
|
+
throw new SkillSelectionError("INVALID_RECEIPT", "The Skills selection receipt is unreadable; sync the profile again.");
|
|
17332
|
+
}
|
|
17333
|
+
}
|
|
17334
|
+
async function verifiedEntries(selection, bytes) {
|
|
17335
|
+
if (`sha256:${sha256Hex(bytes)}` !== selection.bundleDigest)
|
|
17336
|
+
throw new SkillSelectionError("BUNDLE_DIGEST_MISMATCH", "The skill bundle does not match its selected digest.");
|
|
17337
|
+
const bundle = await inspectSkillBundle(bytes);
|
|
17338
|
+
return bundle.entries;
|
|
17339
|
+
}
|
|
17340
|
+
async function readCachedSelection(selection, options = {}) {
|
|
17341
|
+
const bytes = readRegularFile(selectionBundlePath(selection, options), SKILL_BUNDLE_INSPECTION_LIMITS.compressedBytes);
|
|
17342
|
+
return bytes ? verifiedEntries(selection, bytes) : null;
|
|
17343
|
+
}
|
|
17344
|
+
async function cacheSelectionBundle(selection, response, options = {}) {
|
|
17345
|
+
validateSelection2(selection);
|
|
17346
|
+
if (!response?.ok)
|
|
17347
|
+
throw new SkillSelectionError("BUNDLE_UNAVAILABLE", `The selected skill bundle is unavailable${response ? ` (HTTP ${response.status})` : ""}.`);
|
|
17348
|
+
const declared = response.headers.get("X-Skill-Bundle-Sha256");
|
|
17349
|
+
const version = response.headers.get("X-Skill-Version");
|
|
17350
|
+
if (declared && `sha256:${declared}` !== selection.bundleDigest || version && version !== selection.version) {
|
|
17351
|
+
await response.body?.cancel();
|
|
17352
|
+
throw new SkillSelectionError("BUNDLE_IDENTITY_MISMATCH", "The returned bundle does not match the selected version and digest.");
|
|
17353
|
+
}
|
|
17354
|
+
const maximum = SKILL_BUNDLE_INSPECTION_LIMITS.compressedBytes;
|
|
17355
|
+
const reader = response.body?.getReader();
|
|
17356
|
+
if (!reader)
|
|
17357
|
+
throw new SkillSelectionError("BUNDLE_UNAVAILABLE", "The selected bundle response has no body.");
|
|
17358
|
+
const chunks = [];
|
|
17359
|
+
let length = 0;
|
|
17360
|
+
try {
|
|
17361
|
+
while (true) {
|
|
17362
|
+
const next = await reader.read();
|
|
17363
|
+
if (next.done)
|
|
17364
|
+
break;
|
|
17365
|
+
length += next.value.byteLength;
|
|
17366
|
+
if (length > maximum)
|
|
17367
|
+
throw new SkillSelectionError("BUNDLE_TOO_LARGE", "The selected bundle exceeds the cache size limit.");
|
|
17368
|
+
chunks.push(next.value);
|
|
17369
|
+
}
|
|
17370
|
+
} catch (error) {
|
|
17371
|
+
await reader.cancel().catch(() => {});
|
|
17372
|
+
throw error;
|
|
17373
|
+
} finally {
|
|
17374
|
+
reader.releaseLock();
|
|
17375
|
+
}
|
|
17376
|
+
const bytes = new Uint8Array(length);
|
|
17377
|
+
let offset = 0;
|
|
17378
|
+
for (const chunk of chunks) {
|
|
17379
|
+
bytes.set(chunk, offset);
|
|
17380
|
+
offset += chunk.byteLength;
|
|
17381
|
+
}
|
|
17382
|
+
const entries = await verifiedEntries(selection, bytes);
|
|
17383
|
+
const path = selectionBundlePath(selection, options);
|
|
17384
|
+
const existing = readRegularFile(path, maximum);
|
|
17385
|
+
if (existing)
|
|
17386
|
+
await verifiedEntries(selection, existing);
|
|
17387
|
+
else
|
|
17388
|
+
atomicWrite2(path, bytes, 256);
|
|
17389
|
+
return entries;
|
|
17390
|
+
}
|
|
17391
|
+
function profilePath(profileId, options) {
|
|
17392
|
+
return join24(selectionCacheRoot(options), "profiles", `${hash2(profileId)}.json`);
|
|
17393
|
+
}
|
|
17394
|
+
function activateSelectionProfile(profile, options = {}) {
|
|
17395
|
+
validateResolvedProfile(profile);
|
|
17396
|
+
const receipt = { schemaVersion: 1, verifiedAt: new Date((options.now ?? Date.now)()).toISOString(), profile };
|
|
17397
|
+
writeSelectionJson(profilePath(profile.profileId, options), receipt);
|
|
17398
|
+
return receipt;
|
|
17399
|
+
}
|
|
17400
|
+
function readSelectionProfile(profileId, options = {}) {
|
|
17401
|
+
const receipt = readSelectionJson(profilePath(profileId, options));
|
|
17402
|
+
if (receipt)
|
|
17403
|
+
validateSelectionReceipt(receipt);
|
|
17404
|
+
return receipt;
|
|
17405
|
+
}
|
|
17406
|
+
function validateSelectionReceipt(receipt) {
|
|
17407
|
+
if (!receipt || receipt.schemaVersion !== 1 || !Number.isFinite(Date.parse(receipt.verifiedAt)))
|
|
17408
|
+
throw new SkillSelectionError("INVALID_RECEIPT", "The Skills selection receipt is invalid.");
|
|
17409
|
+
validateResolvedProfile(receipt.profile);
|
|
17410
|
+
}
|
|
17411
|
+
function assertFreshCachedProfile(receipt, options = {}) {
|
|
17412
|
+
validateSelectionReceipt(receipt);
|
|
17413
|
+
const age = (options.now ?? Date.now)() - Date.parse(receipt.verifiedAt);
|
|
17414
|
+
const maximum = options.maxAgeMs ?? MAX_CACHED_PROFILE_AGE_MS;
|
|
17415
|
+
if (!Number.isFinite(maximum) || maximum <= 0 || maximum > MAX_CACHED_PROFILE_AGE_MS || age < 0 || age > maximum) {
|
|
17416
|
+
throw new SkillSelectionError("CACHED_PROFILE_EXPIRED", "The cached Skills profile has expired; authenticate and sync it again.");
|
|
17417
|
+
}
|
|
17418
|
+
}
|
|
17419
|
+
function projectSelectionLockPath(projectDir) {
|
|
17420
|
+
return join24(resolve7(projectDir), ".skills", SELECTION_LOCK_FILE);
|
|
17421
|
+
}
|
|
17422
|
+
function readProjectSelection(projectDir) {
|
|
17423
|
+
const receipt = readSelectionJson(projectSelectionLockPath(projectDir));
|
|
17424
|
+
if (receipt)
|
|
17425
|
+
validateSelectionReceipt(receipt);
|
|
17426
|
+
return receipt;
|
|
17427
|
+
}
|
|
17428
|
+
function sessionReceiptPath(sessionId, options = {}) {
|
|
17429
|
+
if (!sessionId.trim() || sessionId.length > 256)
|
|
17430
|
+
throw new SkillSelectionError("INVALID_SESSION", "A Skills session id must contain 1\u2013256 characters.");
|
|
17431
|
+
return join24(selectionCacheRoot(options), "sessions", `${hash2(sessionId)}.json`);
|
|
17432
|
+
}
|
|
17433
|
+
function readSkillSession(sessionId, options = {}) {
|
|
17434
|
+
const receipt = readSelectionJson(sessionReceiptPath(sessionId, options));
|
|
17435
|
+
if (receipt) {
|
|
17436
|
+
validateSelectionReceipt(receipt);
|
|
17437
|
+
if (receipt.sessionId !== sessionId || !Array.isArray(receipt.loaded) || !receipt.loaded.every((key) => /^[a-f0-9]{64}$/.test(key))) {
|
|
17438
|
+
throw new SkillSelectionError("INVALID_RECEIPT", "The Skills session receipt is invalid.");
|
|
17439
|
+
}
|
|
17440
|
+
}
|
|
17441
|
+
return receipt;
|
|
17442
|
+
}
|
|
17443
|
+
|
|
17444
|
+
// src/lib/selected-document.ts
|
|
17445
|
+
var DOCUMENT_PATHS = ["SKILL.md", "README.md", "CLAUDE.md"];
|
|
17446
|
+
function readSelectedDocument(entries, file) {
|
|
17447
|
+
const entry = file !== undefined ? entries.find((candidate) => candidate.path === file) : DOCUMENT_PATHS.map((path) => entries.find((candidate) => candidate.path === path && candidate.bytes.length > 0)).find(Boolean);
|
|
17448
|
+
if (!entry) {
|
|
17449
|
+
if (file !== undefined)
|
|
17450
|
+
throw new SkillSelectionError("SKILL_FILE_MISSING", "The selected skill bundle does not contain that file.");
|
|
17451
|
+
throw new SkillSelectionError("SKILL_DOCS_MISSING", "The selected bundle has no nonempty SKILL.md, README.md, or CLAUDE.md. Use skills load <slug>@<version> --file <path> to read an exact bundle file; loading context never runs the skill.");
|
|
17452
|
+
}
|
|
17453
|
+
try {
|
|
17454
|
+
return { file: entry.path, content: new TextDecoder("utf-8", { fatal: true }).decode(entry.bytes) };
|
|
17455
|
+
} catch {
|
|
17456
|
+
throw new SkillSelectionError("SKILL_FILE_NOT_TEXT", "The selected bundle file is not valid UTF-8 text; choose a documentation file with --file.");
|
|
17457
|
+
}
|
|
17458
|
+
}
|
|
17459
|
+
|
|
17460
|
+
// src/lib/selection-resolver.ts
|
|
17461
|
+
async function syncSelectionProfile(profileId, options = {}) {
|
|
17462
|
+
const client = options.client ?? await createProfileClient();
|
|
17463
|
+
const profile = await client.resolveProfile(profileId);
|
|
17464
|
+
validateResolvedProfile(profile, client.authority);
|
|
17465
|
+
if (profile.profileId !== profileId)
|
|
17466
|
+
throw new SkillSelectionError("PROFILE_IDENTITY_MISMATCH", "The API returned a different selection profile.");
|
|
17467
|
+
const previous = readSelectionProfile(profileId, options);
|
|
17468
|
+
const changed = !previous || JSON.stringify(previous.profile) !== JSON.stringify(profile);
|
|
17469
|
+
let downloaded = 0;
|
|
17470
|
+
for (const selection of profile.selections) {
|
|
17471
|
+
const entries = await readCachedSelection(selection, options);
|
|
17472
|
+
if (entries)
|
|
17473
|
+
continue;
|
|
17474
|
+
if (options.check) {
|
|
17475
|
+
downloaded++;
|
|
17476
|
+
continue;
|
|
17477
|
+
}
|
|
17478
|
+
await cacheSelectionBundle(selection, await client.getBundle(selection.slug, selection.version), options);
|
|
17479
|
+
downloaded++;
|
|
17480
|
+
}
|
|
17481
|
+
if (options.check)
|
|
17482
|
+
return { profile, receipt: previous, changed: changed || downloaded > 0, downloaded, stationReported: false };
|
|
17483
|
+
const receipt = activateSelectionProfile(profile, options);
|
|
17484
|
+
if (options.projectDir)
|
|
17485
|
+
writeSelectionJson(projectSelectionLockPath(options.projectDir), receipt);
|
|
17486
|
+
let stationReported = false;
|
|
17487
|
+
if (options.stationId) {
|
|
17488
|
+
try {
|
|
17489
|
+
await client.recordStation(options.stationId, {
|
|
17490
|
+
profileId: profile.profileId,
|
|
17491
|
+
profileRevision: profile.profileRevision,
|
|
17492
|
+
selections: profile.selections
|
|
17493
|
+
});
|
|
17494
|
+
} catch {
|
|
17495
|
+
throw new SkillSelectionError("PROFILE_APPLIED_REPORT_FAILED", "The verified Skills profile is active, but the station receipt could not be reported. Retry sync to confirm the station state.");
|
|
17496
|
+
}
|
|
17497
|
+
stationReported = true;
|
|
17498
|
+
}
|
|
17499
|
+
return { profile, receipt, changed, downloaded, stationReported };
|
|
17500
|
+
}
|
|
17501
|
+
async function resolveSelectionContext(profileId, options = {}) {
|
|
17502
|
+
const session = options.sessionId ? readSkillSession(options.sessionId, options) : null;
|
|
17503
|
+
const parentSession = !session && options.parentSessionId ? readSkillSession(options.parentSessionId, options) : null;
|
|
17504
|
+
const project = options.projectDir ? readProjectSelection(options.projectDir) : null;
|
|
17505
|
+
if (session && session.profile.profileId !== profileId || parentSession && parentSession.profile.profileId !== profileId || project && project.profile.profileId !== profileId) {
|
|
17506
|
+
throw new SkillSelectionError("PROFILE_LOCK_MISMATCH", "The session or project is pinned to a different Skills profile. Select that profile or explicitly sync the new selection.");
|
|
17507
|
+
}
|
|
17508
|
+
if (options.cached) {
|
|
17509
|
+
const receipt2 = session ?? parentSession ?? project ?? readSelectionProfile(profileId, options);
|
|
17510
|
+
if (!receipt2)
|
|
17511
|
+
throw new SkillSelectionError("CACHED_PROFILE_MISSING", "No verified Skills profile is cached; authenticate and sync it first.");
|
|
17512
|
+
assertFreshCachedProfile(receipt2, options);
|
|
17513
|
+
assertBinding(receipt2.profile, options);
|
|
17514
|
+
return { receipt: receipt2, session, inheritedLoaded: parentSession?.loaded };
|
|
17515
|
+
}
|
|
17516
|
+
const client = options.client ?? await createProfileClient();
|
|
17517
|
+
const current = await client.resolveProfile(profileId);
|
|
17518
|
+
validateResolvedProfile(current, client.authority);
|
|
17519
|
+
if (current.profileId !== profileId)
|
|
17520
|
+
throw new SkillSelectionError("PROFILE_IDENTITY_MISMATCH", "The API returned a different selection profile.");
|
|
17521
|
+
assertBinding(current, options);
|
|
17522
|
+
const locked2 = session ?? parentSession ?? project;
|
|
17523
|
+
if (locked2 && (locked2.profile.authority !== current.authority || locked2.profile.workspaceId !== current.workspaceId)) {
|
|
17524
|
+
throw new SkillSelectionError("PROFILE_IDENTITY_MISMATCH", "The pinned selection belongs to another Skills authority or workspace.");
|
|
17525
|
+
}
|
|
17526
|
+
const receipt = locked2 ?? { schemaVersion: 1, verifiedAt: new Date((options.now ?? Date.now)()).toISOString(), profile: current };
|
|
17527
|
+
return { receipt, session, client, inheritedLoaded: parentSession?.loaded };
|
|
17528
|
+
}
|
|
17529
|
+
function assertBinding(profile, options) {
|
|
17530
|
+
if (options.authority !== undefined && profile.authority !== options.authority || options.workspaceId !== undefined && profile.workspaceId !== options.workspaceId) {
|
|
17531
|
+
throw new SkillSelectionError("PROFILE_IDENTITY_MISMATCH", "The cached selection belongs to another Skills authority or workspace.");
|
|
17532
|
+
}
|
|
17533
|
+
}
|
|
17534
|
+
async function readSelectedEntries(selection, context, options = {}) {
|
|
17535
|
+
if (options.cached) {
|
|
17536
|
+
const entries = await readCachedSelection(selection, options);
|
|
17537
|
+
if (!entries)
|
|
17538
|
+
throw new SkillSelectionError("CACHED_BUNDLE_MISSING", "The exact selected bundle is not cached. Cached mode cannot fetch or use an authoring draft.");
|
|
17539
|
+
return entries;
|
|
17540
|
+
}
|
|
17541
|
+
const client = context.client ?? options.client ?? await createProfileClient();
|
|
17542
|
+
return cacheSelectionBundle(selection, await client.getBundle(selection.slug, selection.version), options);
|
|
17543
|
+
}
|
|
17544
|
+
function exactProfileSelection(spec, profile) {
|
|
17545
|
+
const at = spec.lastIndexOf("@");
|
|
17546
|
+
const slug = at > 0 ? spec.slice(0, at) : spec;
|
|
17547
|
+
const version = at > 0 ? spec.slice(at + 1) : undefined;
|
|
17548
|
+
const selection = profile.selections.find((entry) => entry.slug === slug && (version === undefined || version === entry.version));
|
|
17549
|
+
if (!selection || version === "")
|
|
17550
|
+
throw new SkillSelectionError("SKILL_NOT_SELECTED", "The requested exact skill version is not selected by this profile or project lock.");
|
|
17551
|
+
return selection;
|
|
17552
|
+
}
|
|
17553
|
+
|
|
17554
|
+
// src/lib/skill-context.ts
|
|
17555
|
+
function words(value) {
|
|
17556
|
+
return new Set(value.toLowerCase().match(/[a-z0-9]+/g) ?? []);
|
|
17557
|
+
}
|
|
17558
|
+
function pathMatch(pattern, path) {
|
|
17559
|
+
if (!pattern || pattern.length > 512)
|
|
17560
|
+
return false;
|
|
17561
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "\x00").replace(/\*/g, "[^/]*").replace(/\u0000/g, ".*");
|
|
17562
|
+
return new RegExp(`^(?:${escaped})$`).test(path.replace(/\\/g, "/"));
|
|
17563
|
+
}
|
|
17564
|
+
async function buildSkillContext(input, options = {}) {
|
|
17565
|
+
const prompt = input.prompt ?? "";
|
|
17566
|
+
if (prompt.length > 128 * 1024 || (input.paths?.length ?? 0) > 100 || (input.skills?.length ?? 0) > 100) {
|
|
17567
|
+
throw new SkillSelectionError("CONTEXT_INPUT_TOO_LARGE", "The Skills context input exceeds its size limit.");
|
|
17568
|
+
}
|
|
17569
|
+
const maxChars = options.maxChars ?? 8000;
|
|
17570
|
+
const maxSkills = options.maxSkills ?? 3;
|
|
17571
|
+
if (!Number.isInteger(maxChars) || maxChars < 512 || maxChars > 64000 || !Number.isInteger(maxSkills) || maxSkills < 1 || maxSkills > 20) {
|
|
17572
|
+
throw new SkillSelectionError("INVALID_CONTEXT_BUDGET", "Context limits must be 512\u201364000 characters and 1\u201320 skills.");
|
|
17573
|
+
}
|
|
17574
|
+
const profileId = input.profileId ?? "default";
|
|
17575
|
+
const sessionId = input.sessionId ? input.agentId ? `${input.sessionId}:${input.agentId}` : input.sessionId : undefined;
|
|
17576
|
+
const resolverOptions = { ...options, projectDir: input.cwd ?? options.projectDir, sessionId, parentSessionId: input.agentId ? input.sessionId : undefined };
|
|
17577
|
+
const resolved = await resolveSelectionContext(profileId, resolverOptions);
|
|
17578
|
+
const profile = resolved.receipt.profile;
|
|
17579
|
+
const explicit = new Set((input.skills ?? []).map((spec) => selectionKey(exactProfileSelection(spec, profile))));
|
|
17580
|
+
for (const match of prompt.matchAll(/\$([a-z0-9]+(?:-[a-z0-9]+)*(?:@[a-zA-Z0-9._-]+)?)/g)) {
|
|
17581
|
+
const spec = match[1];
|
|
17582
|
+
const slug = spec.split("@")[0];
|
|
17583
|
+
if (profile.selections.some((entry) => entry.slug === slug))
|
|
17584
|
+
explicit.add(selectionKey(exactProfileSelection(spec, profile)));
|
|
17585
|
+
}
|
|
17586
|
+
const promptWords = words(prompt);
|
|
17587
|
+
const loaded = new Set(resolved.session?.loaded ?? []);
|
|
17588
|
+
const paths = [...input.paths ?? [], ...input.cwd ? [input.cwd, resolve8(input.cwd)] : []];
|
|
17589
|
+
const candidates = profile.selections.map((selection) => {
|
|
17590
|
+
const key = selectionKey(selection);
|
|
17591
|
+
if (input.restore && loaded.has(key))
|
|
17592
|
+
return { selection, reason: "session-restore", score: 2000 };
|
|
17593
|
+
if (resolved.inheritedLoaded?.includes(key))
|
|
17594
|
+
return { selection, reason: "subagent-inherit", score: 2000 };
|
|
17595
|
+
if (explicit.has(key))
|
|
17596
|
+
return { selection, reason: "explicit", score: 1000 };
|
|
17597
|
+
if (selection.triggers?.always)
|
|
17598
|
+
return { selection, reason: "profile-required", score: 900 };
|
|
17599
|
+
const matchedPath = selection.triggers?.paths?.some((pattern) => paths.some((path) => pathMatch(pattern, path)));
|
|
17600
|
+
if (matchedPath)
|
|
17601
|
+
return { selection, reason: "path", score: 500 };
|
|
17602
|
+
const keywords = selection.triggers?.keywords ?? selection.slug.split("-").filter((word) => word.length >= 4);
|
|
17603
|
+
const matches = keywords.filter((keyword) => {
|
|
17604
|
+
const tokens = [...words(keyword)];
|
|
17605
|
+
return tokens.length > 0 && tokens.every((token) => promptWords.has(token));
|
|
17606
|
+
}).length;
|
|
17607
|
+
return { selection, reason: "prompt", score: matches * 100 };
|
|
17608
|
+
}).filter((candidate) => candidate.score > 0).sort((a, b) => b.score - a.score || a.selection.slug.localeCompare(b.selection.slug));
|
|
17609
|
+
const sections = [];
|
|
17610
|
+
const selections = [];
|
|
17611
|
+
const omitted = [];
|
|
17612
|
+
let chars = 0;
|
|
17613
|
+
for (const candidate of candidates) {
|
|
17614
|
+
const { selection, reason } = candidate;
|
|
17615
|
+
if (!input.restore && !explicit.has(selectionKey(selection)) && loaded.has(selectionKey(selection)))
|
|
17616
|
+
continue;
|
|
17617
|
+
const loadCommand = `skills load ${selection.slug}@${selection.version} --selection-profile ${profile.profileId}`;
|
|
17618
|
+
if (selections.length >= maxSkills) {
|
|
17619
|
+
omitted.push({ slug: selection.slug, version: selection.version, reason: "skill-limit", loadCommand });
|
|
17620
|
+
continue;
|
|
17621
|
+
}
|
|
17622
|
+
const entries = await readSelectedEntries(selection, resolved, resolverOptions);
|
|
17623
|
+
const { content } = readSelectedDocument(entries);
|
|
17624
|
+
const section = `Skill ${selection.slug}@${selection.version} (${selection.bundleDigest}; ${reason})
|
|
17625
|
+
${content}`;
|
|
17626
|
+
if (chars + section.length + (sections.length ? 2 : 0) > maxChars) {
|
|
17627
|
+
omitted.push({ slug: selection.slug, version: selection.version, reason: "context-budget", loadCommand });
|
|
17628
|
+
continue;
|
|
17629
|
+
}
|
|
17630
|
+
chars += section.length + (sections.length ? 2 : 0);
|
|
17631
|
+
sections.push(section);
|
|
17632
|
+
selections.push({ ...selection, reason });
|
|
17633
|
+
loaded.add(selectionKey(selection));
|
|
17634
|
+
}
|
|
17635
|
+
const context = sections.join(`
|
|
17636
|
+
|
|
17637
|
+
`);
|
|
17638
|
+
const receipt = {
|
|
17639
|
+
schemaVersion: 1,
|
|
17640
|
+
id: createHash12("sha256").update(JSON.stringify({ profile: profile.profileRevision, sessionId, selections, omitted })).digest("hex"),
|
|
17641
|
+
profileId: profile.profileId,
|
|
17642
|
+
profileRevision: profile.profileRevision,
|
|
17643
|
+
authority: profile.authority,
|
|
17644
|
+
workspaceId: profile.workspaceId,
|
|
17645
|
+
source: options.cached ? "verified-cache" : "api",
|
|
17646
|
+
...sessionId ? { sessionId } : {},
|
|
17647
|
+
restored: Boolean(input.restore),
|
|
17648
|
+
chars: context.length,
|
|
17649
|
+
selections
|
|
17650
|
+
};
|
|
17651
|
+
if (sessionId)
|
|
17652
|
+
writeSelectionJson(sessionReceiptPath(sessionId, options), { ...resolved.receipt, sessionId, loaded: [...loaded] });
|
|
17653
|
+
return { context, selections, omitted, receipt };
|
|
17654
|
+
}
|
|
16721
17655
|
export {
|
|
16722
17656
|
writeStationSnapshot,
|
|
16723
17657
|
writeStationHydration,
|
|
@@ -16744,6 +17678,7 @@ export {
|
|
|
16744
17678
|
unpinSkill,
|
|
16745
17679
|
unpinProjectSkill,
|
|
16746
17680
|
syncSkillsToAgents,
|
|
17681
|
+
syncSelectionProfile,
|
|
16747
17682
|
summarizeMcpToolContract,
|
|
16748
17683
|
storageCapabilities,
|
|
16749
17684
|
skillsPostgresSyncSchemaSql,
|
|
@@ -16793,6 +17728,7 @@ export {
|
|
|
16793
17728
|
planStationSnapshot,
|
|
16794
17729
|
planStationHydration,
|
|
16795
17730
|
planSkillsS3SnapshotUpload,
|
|
17731
|
+
planAgentIntegration,
|
|
16796
17732
|
pinSkill,
|
|
16797
17733
|
pinProjectSkill,
|
|
16798
17734
|
parseSkillFrontmatter,
|
|
@@ -16825,6 +17761,7 @@ export {
|
|
|
16825
17761
|
isGatewayBackedSkill,
|
|
16826
17762
|
isExcludedSkillFileName,
|
|
16827
17763
|
isBasicSkillName,
|
|
17764
|
+
inventoryNativeSkills,
|
|
16828
17765
|
installSkills,
|
|
16829
17766
|
installSkillSource,
|
|
16830
17767
|
installSkillManifest,
|
|
@@ -16892,6 +17829,7 @@ export {
|
|
|
16892
17829
|
createSkillMcpMetadata,
|
|
16893
17830
|
createRemoteSkillsClient,
|
|
16894
17831
|
createRegistrySyncArtifact,
|
|
17832
|
+
createProfileClient,
|
|
16895
17833
|
createMcpContractManifest,
|
|
16896
17834
|
createLocalSkillManifest,
|
|
16897
17835
|
continuePrivatePublication,
|
|
@@ -16903,6 +17841,9 @@ export {
|
|
|
16903
17841
|
canonicalizeManifest,
|
|
16904
17842
|
buildSkillsS3ObjectUrl,
|
|
16905
17843
|
buildSkillsApiUrl,
|
|
17844
|
+
buildSkillContext,
|
|
17845
|
+
archiveNativeSkills,
|
|
17846
|
+
applyAgentIntegration,
|
|
16906
17847
|
appendRunEvent,
|
|
16907
17848
|
agentGlobalSkillsDir,
|
|
16908
17849
|
addSchedule,
|
|
@@ -16971,6 +17912,7 @@ export {
|
|
|
16971
17912
|
MissingSkillsFleetError,
|
|
16972
17913
|
MCP_CONTRACT_SCHEMA_VERSION,
|
|
16973
17914
|
InvitationEmailInputError,
|
|
17915
|
+
HttpProfileClient,
|
|
16974
17916
|
HostedApiError,
|
|
16975
17917
|
DEFAULT_EXPORT_DIR,
|
|
16976
17918
|
ContentHashInputError,
|