@hasna/skills 0.10.31 → 0.10.34
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 +38 -0
- package/bin/index.js +150 -37
- package/bin/mcp.js +80 -22
- package/bin/migrate.js +1 -1
- package/bin/server.js +1 -1
- package/bin/worker.js +1 -1
- package/dist/cli/commands/runtime-copyfile.d.ts +2 -0
- package/dist/index.js +83 -25
- package/dist/sdk/index.js +83 -25
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -623,6 +623,18 @@ on this same host. The exact lock bytes and a metadata record remain under
|
|
|
623
623
|
by recovery. A live, recent, malformed or different-host lock still blocks the
|
|
624
624
|
write. A recovery guard serializes competing recoverers.
|
|
625
625
|
|
|
626
|
+
Writers gather identity before publishing the lock. A private, fully written and
|
|
627
|
+
synced staging inode is linked into the live path atomically without replacing
|
|
628
|
+
an existing lock; the staging name is then removed before the protected write.
|
|
629
|
+
The writer keeps its descriptor open through lock release. An interruption at
|
|
630
|
+
the brief two-link publication boundary leaves a complete marker and its exact
|
|
631
|
+
operation-bound staging witness. Recovery accepts that pair only when both names
|
|
632
|
+
identify the same private inode and the existing stale-owner checks pass.
|
|
633
|
+
Readers from 0.10.31 and earlier refuse this interrupted two-link state; inspect
|
|
634
|
+
and recover it with an updated installation. Ordinary held locks have one link.
|
|
635
|
+
This prevents new empty locks; it does not recover historical empty or malformed
|
|
636
|
+
locks, whose ownership remains unknown and which must stay protected.
|
|
637
|
+
|
|
626
638
|
Older PID-only locks require a local review because they do not identify their
|
|
627
639
|
host. On the host that created the lock, inspect the receipt and lock, verify
|
|
628
640
|
the recorded PID belongs to no live writer, then apply the exact reviewed lock:
|
|
@@ -1136,6 +1148,32 @@ semantic version. If discovery or verification fails, it exits nonzero and
|
|
|
1136
1148
|
explains that installation may already have completed. It does not automatically
|
|
1137
1149
|
reinstall or change your PATH.
|
|
1138
1150
|
|
|
1151
|
+
### Exact-version runtime updates
|
|
1152
|
+
|
|
1153
|
+
`skills self-update --version 0.10.34` installs a verified package into a retained
|
|
1154
|
+
copyfile runtime, preserves configuration and launcher preimages, and keeps the
|
|
1155
|
+
previous runtime available for receipt-bound rollback.
|
|
1156
|
+
|
|
1157
|
+
To require a minimum dependency age, pass a positive integer number of days and
|
|
1158
|
+
repeat the exclusion option for package names or package globs:
|
|
1159
|
+
|
|
1160
|
+
```bash
|
|
1161
|
+
skills self-update --version 0.10.34 --min-release-age 7 \
|
|
1162
|
+
--min-release-age-exclude '@hasna/*' \
|
|
1163
|
+
--min-release-age-exclude '@hasna-internal/*' \
|
|
1164
|
+
--min-release-age-exclude '@openai/*' \
|
|
1165
|
+
--min-release-age-exclude '@anthropic-ai/*' \
|
|
1166
|
+
--min-release-age-exclude openai
|
|
1167
|
+
```
|
|
1168
|
+
|
|
1169
|
+
The policy reaches both dependency lock resolution and `npm ci` through private
|
|
1170
|
+
npm configuration. Exemptions match each dependency's own package identity;
|
|
1171
|
+
unrelated transitive dependencies retain the minimum age. These options require
|
|
1172
|
+
an exact version and npm 11.19.0 or newer with both age capabilities. They are
|
|
1173
|
+
refused for rollback, alias adoption or the no-argument updater. Existing ambient
|
|
1174
|
+
npm settings and credentials are not forwarded. Omitting the policy preserves
|
|
1175
|
+
existing exact-version update behavior.
|
|
1176
|
+
|
|
1139
1177
|
### Local environment assignments
|
|
1140
1178
|
|
|
1141
1179
|
`skills env-check --set 'KEY=value'` writes one literal value to the current
|
package/bin/index.js
CHANGED
|
@@ -36868,7 +36868,7 @@ var package_default;
|
|
|
36868
36868
|
var init_package = __esm(() => {
|
|
36869
36869
|
package_default = {
|
|
36870
36870
|
name: "@hasna/skills",
|
|
36871
|
-
version: "0.10.
|
|
36871
|
+
version: "0.10.34",
|
|
36872
36872
|
description: "Skills library for AI coding agents",
|
|
36873
36873
|
type: "module",
|
|
36874
36874
|
bin: {
|
|
@@ -48139,7 +48139,7 @@ var init_profile_limits = __esm(() => {
|
|
|
48139
48139
|
// src/lib/selection-cache.ts
|
|
48140
48140
|
import { createHash as createHash6, randomUUID as randomUUID3 } from "crypto";
|
|
48141
48141
|
import { spawnSync } from "child_process";
|
|
48142
|
-
import { closeSync as closeSync2, constants as constants3, fstatSync as fstatSync2, fsyncSync, lstatSync as lstatSync7, mkdirSync as mkdirSync3, openSync as openSync2, readFileSync as readFileSync9, readlinkSync as readlinkSync2, readSync as readSync2, renameSync, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
|
|
48142
|
+
import { closeSync as closeSync2, constants as constants3, fstatSync as fstatSync2, fsyncSync, linkSync, lstatSync as lstatSync7, mkdirSync as mkdirSync3, openSync as openSync2, readFileSync as readFileSync9, readlinkSync as readlinkSync2, readSync as readSync2, renameSync, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
|
|
48143
48143
|
import { hostname } from "os";
|
|
48144
48144
|
import { dirname as dirname7, join as join15, parse as parse2, resolve as resolve8 } from "path";
|
|
48145
48145
|
import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
|
|
@@ -48487,7 +48487,7 @@ function readSessionLock(sessionId, options) {
|
|
|
48487
48487
|
const before2 = lstatSync7(path, { throwIfNoEntry: false });
|
|
48488
48488
|
if (!before2)
|
|
48489
48489
|
return null;
|
|
48490
|
-
if (!before2.isFile() || before2.nlink
|
|
48490
|
+
if (!before2.isFile() || ![1, 2].includes(before2.nlink) || (before2.mode & 63) !== 0 || process.getuid && before2.uid !== process.getuid()) {
|
|
48491
48491
|
throw new SkillSelectionError("SESSION_WRITE_LOCKED", "The session lock is not a private regular file; recovery requires review.");
|
|
48492
48492
|
}
|
|
48493
48493
|
const bytes = readRegularFile(path, SESSION_LOCK_MAX_BYTES);
|
|
@@ -48501,10 +48501,54 @@ function readSessionLock(sessionId, options) {
|
|
|
48501
48501
|
} catch {
|
|
48502
48502
|
throw new SkillSelectionError("SESSION_WRITE_LOCKED", "The session lock is unreadable; recovery requires review.");
|
|
48503
48503
|
}
|
|
48504
|
-
if (!marker || ![1, 2].includes(marker.schemaVersion) || marker.sessionId !== sessionId || !/^[a-f0-9-]{36}$/i.test(marker.operationId) || !Number.isSafeInteger(marker.pid) || marker.pid <= 0 || marker.schemaVersion === 2 && (!marker.host || !marker.bootId || !marker.processStart || process.platform === "linux" && !marker.pidNamespace)) {
|
|
48504
|
+
if (!marker || ![1, 2].includes(marker.schemaVersion) || marker.sessionId !== sessionId || !/^[a-f0-9-]{36}$/i.test(marker.operationId) || !Number.isSafeInteger(marker.pid) || marker.pid <= 0 || marker.publication !== undefined && marker.publication !== "hard-link-v1" || marker.schemaVersion === 2 && (!marker.host || !marker.bootId || !marker.processStart || process.platform === "linux" && !marker.pidNamespace)) {
|
|
48505
48505
|
throw new SkillSelectionError("SESSION_WRITE_LOCKED", "The session lock has invalid ownership metadata; recovery requires review.");
|
|
48506
48506
|
}
|
|
48507
|
-
|
|
48507
|
+
let stagingPath;
|
|
48508
|
+
if (after2.nlink === 2) {
|
|
48509
|
+
stagingPath = `${path}.${marker.operationId}.prepared`;
|
|
48510
|
+
assertRegularPath(stagingPath);
|
|
48511
|
+
const staged = lstatSync7(stagingPath, { throwIfNoEntry: false });
|
|
48512
|
+
if (marker.publication !== "hard-link-v1" || !staged?.isFile() || staged.dev !== after2.dev || staged.ino !== after2.ino || staged.nlink !== 2 || staged.mode !== after2.mode || staged.uid !== after2.uid || staged.gid !== after2.gid) {
|
|
48513
|
+
throw new SkillSelectionError("SESSION_WRITE_LOCKED", "The session lock has an unbound publication link; recovery requires review.");
|
|
48514
|
+
}
|
|
48515
|
+
}
|
|
48516
|
+
return { path, bytes, marker, sha256: sha256Hex(bytes), dev: after2.dev, ino: after2.ino, mtimeMs: after2.mtimeMs, stagingPath };
|
|
48517
|
+
}
|
|
48518
|
+
function publishSessionLock(path, marker) {
|
|
48519
|
+
const stagingPath = `${path}.${marker.operationId}.prepared`;
|
|
48520
|
+
const bytes = Buffer.from(`${JSON.stringify({ ...marker, publication: "hard-link-v1" })}
|
|
48521
|
+
`);
|
|
48522
|
+
const fd = openSync2(stagingPath, constants3.O_WRONLY | constants3.O_CREAT | constants3.O_EXCL | constants3.O_NOFOLLOW, 384);
|
|
48523
|
+
const stat = fstatSync2(fd);
|
|
48524
|
+
let published = false, handedOff = false;
|
|
48525
|
+
try {
|
|
48526
|
+
writeFileSync3(fd, bytes);
|
|
48527
|
+
fsyncSync(fd);
|
|
48528
|
+
linkSync(stagingPath, path);
|
|
48529
|
+
published = true;
|
|
48530
|
+
syncDirectory(dirname7(path));
|
|
48531
|
+
const staged = lstatSync7(stagingPath);
|
|
48532
|
+
if (!staged.isFile() || staged.dev !== stat.dev || staged.ino !== stat.ino) {
|
|
48533
|
+
throw new SkillSelectionError("SESSION_WRITE_LOCK_CHANGED", "The initialized lock staging inode changed before publication completed.");
|
|
48534
|
+
}
|
|
48535
|
+
unlinkSync(stagingPath);
|
|
48536
|
+
syncDirectory(dirname7(path));
|
|
48537
|
+
handedOff = true;
|
|
48538
|
+
return { path, stagingPath, fd, dev: stat.dev, ino: stat.ino };
|
|
48539
|
+
} finally {
|
|
48540
|
+
if (!handedOff) {
|
|
48541
|
+
try {
|
|
48542
|
+
for (const target of [...published ? [path] : [], stagingPath]) {
|
|
48543
|
+
const current = lstatSync7(target, { throwIfNoEntry: false });
|
|
48544
|
+
if (current?.isFile() && current.dev === stat.dev && current.ino === stat.ino)
|
|
48545
|
+
unlinkSync(target);
|
|
48546
|
+
}
|
|
48547
|
+
} finally {
|
|
48548
|
+
closeSync2(fd);
|
|
48549
|
+
}
|
|
48550
|
+
}
|
|
48551
|
+
}
|
|
48508
48552
|
}
|
|
48509
48553
|
function writerIsGone(lock, explicitLocalHost) {
|
|
48510
48554
|
if (Date.now() - lock.mtimeMs < SESSION_LOCK_MIN_AGE_MS)
|
|
@@ -48593,10 +48637,21 @@ function archiveSessionLock(lock, receipt, explicitLocalHost) {
|
|
|
48593
48637
|
try {
|
|
48594
48638
|
renameSync(lock.path, archivePath);
|
|
48595
48639
|
moved = true;
|
|
48640
|
+
const archivedPaths = [archivePath];
|
|
48641
|
+
if (finalLock.stagingPath) {
|
|
48642
|
+
const publicationPath = join15(directory, "publication.write-lock");
|
|
48643
|
+
renameSync(finalLock.stagingPath, publicationPath);
|
|
48644
|
+
archivedPaths.push(publicationPath);
|
|
48645
|
+
}
|
|
48596
48646
|
syncDirectory(directory);
|
|
48597
48647
|
syncDirectory(dirname7(lock.path));
|
|
48598
|
-
|
|
48599
|
-
|
|
48648
|
+
for (const path of archivedPaths) {
|
|
48649
|
+
const before2 = lstatSync7(path, { throwIfNoEntry: false });
|
|
48650
|
+
const bytes = readRegularFile(path, SESSION_LOCK_MAX_BYTES);
|
|
48651
|
+
const after2 = lstatSync7(path, { throwIfNoEntry: false });
|
|
48652
|
+
if (!before2?.isFile() || before2.dev !== lock.dev || before2.ino !== lock.ino || before2.nlink !== archivedPaths.length || before2.mtimeMs !== lock.mtimeMs || (before2.mode & 63) !== 0 || process.getuid && before2.uid !== process.getuid() || !bytes || sha256Hex(bytes) !== lock.sha256 || !after2?.isFile() || before2.dev !== after2.dev || before2.ino !== after2.ino || before2.mtimeMs !== after2.mtimeMs || before2.mode !== after2.mode || before2.uid !== after2.uid || before2.gid !== after2.gid || before2.nlink !== after2.nlink) {
|
|
48653
|
+
throw new Error("Archived lock ownership or bytes changed");
|
|
48654
|
+
}
|
|
48600
48655
|
}
|
|
48601
48656
|
} catch (error) {
|
|
48602
48657
|
if (moved)
|
|
@@ -48623,6 +48678,7 @@ function recoverSessionWriteLock(sessionId, reviewDigest, options = {}) {
|
|
|
48623
48678
|
}
|
|
48624
48679
|
function withSessionWriteLocks(sessionIds, options, action) {
|
|
48625
48680
|
const operationId = randomUUID3();
|
|
48681
|
+
const identity4 = localLockIdentity();
|
|
48626
48682
|
const locks = [...new Set(sessionIds)].map((sessionId) => ({ sessionId, path: `${sessionReceiptPath(sessionId, options)}.write-lock` })).sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
48627
48683
|
const owned = [];
|
|
48628
48684
|
const assertOwned = () => {
|
|
@@ -48636,9 +48692,10 @@ function withSessionWriteLocks(sessionIds, options, action) {
|
|
|
48636
48692
|
try {
|
|
48637
48693
|
for (const lock of locks) {
|
|
48638
48694
|
assertRegularPath(lock.path, true);
|
|
48639
|
-
|
|
48695
|
+
const marker = { schemaVersion: identity4 ? 2 : 1, operationId, sessionId: lock.sessionId, pid: process.pid, ...identity4 };
|
|
48696
|
+
let acquired;
|
|
48640
48697
|
try {
|
|
48641
|
-
|
|
48698
|
+
acquired = publishSessionLock(lock.path, marker);
|
|
48642
48699
|
} catch (error) {
|
|
48643
48700
|
if (error.code !== "EEXIST")
|
|
48644
48701
|
throw error;
|
|
@@ -48653,7 +48710,7 @@ function withSessionWriteLocks(sessionIds, options, action) {
|
|
|
48653
48710
|
throw recoveryError;
|
|
48654
48711
|
}
|
|
48655
48712
|
try {
|
|
48656
|
-
|
|
48713
|
+
acquired = publishSessionLock(lock.path, marker);
|
|
48657
48714
|
} catch (retryError) {
|
|
48658
48715
|
if (retryError.code === "EEXIST") {
|
|
48659
48716
|
if (!readSessionLock(lock.sessionId, options))
|
|
@@ -48663,19 +48720,20 @@ function withSessionWriteLocks(sessionIds, options, action) {
|
|
|
48663
48720
|
throw retryError;
|
|
48664
48721
|
}
|
|
48665
48722
|
}
|
|
48666
|
-
|
|
48667
|
-
owned.push({ path: lock.path, fd, dev: stat.dev, ino: stat.ino });
|
|
48668
|
-
const identity4 = localLockIdentity();
|
|
48669
|
-
writeFileSync3(fd, `${JSON.stringify({ schemaVersion: identity4 ? 2 : 1, operationId, sessionId: lock.sessionId, pid: process.pid, ...identity4 })}
|
|
48670
|
-
`);
|
|
48723
|
+
owned.push(acquired);
|
|
48671
48724
|
}
|
|
48672
48725
|
return action(assertOwned);
|
|
48673
48726
|
} finally {
|
|
48674
48727
|
for (const lock of owned.reverse()) {
|
|
48675
|
-
|
|
48676
|
-
|
|
48677
|
-
|
|
48678
|
-
|
|
48728
|
+
try {
|
|
48729
|
+
for (const path of [lock.path, lock.stagingPath]) {
|
|
48730
|
+
const current = lstatSync7(path, { throwIfNoEntry: false });
|
|
48731
|
+
if (current?.isFile() && current.dev === lock.dev && current.ino === lock.ino)
|
|
48732
|
+
unlinkSync(path);
|
|
48733
|
+
}
|
|
48734
|
+
} finally {
|
|
48735
|
+
closeSync2(lock.fd);
|
|
48736
|
+
}
|
|
48679
48737
|
}
|
|
48680
48738
|
}
|
|
48681
48739
|
}
|
|
@@ -52538,7 +52596,7 @@ var init_agent_sumi = __esm(() => {
|
|
|
52538
52596
|
// src/lib/agent-integration.ts
|
|
52539
52597
|
import { createHash as createHash12, randomUUID as randomUUID5 } from "crypto";
|
|
52540
52598
|
import { isDeepStrictEqual as isDeepStrictEqual5 } from "util";
|
|
52541
|
-
import { existsSync as existsSync13, lstatSync as lstatSync16, statSync as statSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync13, readdirSync as readdirSync8, opendirSync as opendirSync3, readlinkSync as readlinkSync4, realpathSync as realpathSync7, renameSync as renameSync2, rmdirSync as rmdirSync2, writeFileSync as writeFileSync4, unlinkSync as unlinkSync3, chmodSync, openSync as openSync8, closeSync as closeSync8, fsyncSync as fsyncSync2, fstatSync as fstatSync8, readSync as readSync6, constants as constants8, linkSync } from "fs";
|
|
52599
|
+
import { existsSync as existsSync13, lstatSync as lstatSync16, statSync as statSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync13, readdirSync as readdirSync8, opendirSync as opendirSync3, readlinkSync as readlinkSync4, realpathSync as realpathSync7, renameSync as renameSync2, rmdirSync as rmdirSync2, writeFileSync as writeFileSync4, unlinkSync as unlinkSync3, chmodSync, openSync as openSync8, closeSync as closeSync8, fsyncSync as fsyncSync2, fstatSync as fstatSync8, readSync as readSync6, constants as constants8, linkSync as linkSync2 } from "fs";
|
|
52542
52600
|
import { dirname as dirname11, join as join23, resolve as resolve12, relative as relative5, isAbsolute as isAbsolute11, sep as sep5 } from "path";
|
|
52543
52601
|
import { homedir as homedir3 } from "os";
|
|
52544
52602
|
function hasNoDarwinAcl(fd) {
|
|
@@ -53592,7 +53650,7 @@ function archiveNativeSkills(inventory, options) {
|
|
|
53592
53650
|
continue;
|
|
53593
53651
|
}
|
|
53594
53652
|
if (entry.discoveryOnly) {
|
|
53595
|
-
|
|
53653
|
+
linkSync2(entry.archive, entry.source);
|
|
53596
53654
|
syncArchiveDirectory(dirname11(entry.source));
|
|
53597
53655
|
unlinkSync3(entry.archive);
|
|
53598
53656
|
} else {
|
|
@@ -55575,7 +55633,7 @@ var init_plugin_projection = __esm(() => {
|
|
|
55575
55633
|
|
|
55576
55634
|
// src/lib/plugin-projection-store.ts
|
|
55577
55635
|
import { createHash as createHash14, randomUUID as randomUUID6 } from "crypto";
|
|
55578
|
-
import { constants as constants9, closeSync as closeSync9, fchmodSync, fstatSync as fstatSync9, fsyncSync as fsyncSync3, linkSync as
|
|
55636
|
+
import { constants as constants9, closeSync as closeSync9, fchmodSync, fstatSync as fstatSync9, fsyncSync as fsyncSync3, linkSync as linkSync3, lstatSync as lstatSync18, mkdirSync as mkdirSync9, openSync as openSync9, opendirSync as opendirSync4, readSync as readSync7, renameSync as renameSync5, rmdirSync as rmdirSync3, rmSync as rmSync4, unlinkSync as unlinkSync4, writeSync as writeSync2 } from "fs";
|
|
55579
55637
|
import { dirname as dirname13, isAbsolute as isAbsolute12, join as join30, parse as parse5, resolve as resolve13 } from "path";
|
|
55580
55638
|
function stat(path) {
|
|
55581
55639
|
try {
|
|
@@ -55887,7 +55945,7 @@ function writePluginJsonImmutable(path, value) {
|
|
|
55887
55945
|
written = stat(temporary);
|
|
55888
55946
|
verifyAncestors(chain, true);
|
|
55889
55947
|
try {
|
|
55890
|
-
|
|
55948
|
+
linkSync3(temporary, path);
|
|
55891
55949
|
} catch (error) {
|
|
55892
55950
|
if (!(error && typeof error === "object" && ("code" in error) && error.code === "EEXIST"))
|
|
55893
55951
|
throw error;
|
|
@@ -57265,13 +57323,13 @@ function resolveAgentDiscovery(options) {
|
|
|
57265
57323
|
unresolved("JSONC configuration requires a supported format adapter");
|
|
57266
57324
|
for (const root2 of [directory, join33(home, ".claude"), join33(home, ".agents")])
|
|
57267
57325
|
for (const name of ["skill", "skills"])
|
|
57268
|
-
roots.add(join33(root2, name));
|
|
57326
|
+
roots.add(canonical6(join33(root2, name)));
|
|
57269
57327
|
if (config.skills !== undefined && (!Array.isArray(config.skills) || config.skills.some((path) => typeof path !== "string")))
|
|
57270
57328
|
unresolved("skills must be a string array");
|
|
57271
57329
|
for (const value of config.skills ?? []) {
|
|
57272
57330
|
if (typeof value !== "string" || /[\0$]/.test(value) || !value || !(isAbsolute17(value) || value.startsWith("~/")))
|
|
57273
57331
|
unresolved("remote or relative skill sources need a dedicated discovery adapter");
|
|
57274
|
-
roots.add(value.startsWith("~/") ? join33(home, value.slice(2)) : value);
|
|
57332
|
+
roots.add(canonical6(value.startsWith("~/") ? join33(home, value.slice(2)) : value));
|
|
57275
57333
|
}
|
|
57276
57334
|
for (const key of ["plugin", "plugins"])
|
|
57277
57335
|
if (config[key] !== undefined && !Array.isArray(config[key]))
|
|
@@ -109695,16 +109753,47 @@ function assertConfigUnchanged(configs) {
|
|
|
109695
109753
|
}
|
|
109696
109754
|
}
|
|
109697
109755
|
}
|
|
109698
|
-
|
|
109756
|
+
function releaseAgePolicy(options) {
|
|
109757
|
+
if (options.minReleaseAgeExclude !== undefined && !Array.isArray(options.minReleaseAgeExclude))
|
|
109758
|
+
throw new Error("MIN_RELEASE_AGE_EXCLUDE_INVALID");
|
|
109759
|
+
if (options.minReleaseAge === undefined) {
|
|
109760
|
+
if (options.minReleaseAgeExclude?.length)
|
|
109761
|
+
throw new Error("MIN_RELEASE_AGE_REQUIRED_FOR_EXCLUSIONS");
|
|
109762
|
+
return;
|
|
109763
|
+
}
|
|
109764
|
+
if (!Number.isSafeInteger(options.minReleaseAge) || options.minReleaseAge < 1)
|
|
109765
|
+
throw new Error("MIN_RELEASE_AGE_INVALID");
|
|
109766
|
+
const exclusions = options.minReleaseAgeExclude ?? [];
|
|
109767
|
+
if (!Array.isArray(exclusions) || exclusions.some((pattern) => typeof pattern !== "string" || pattern.length > 214 || !/^(?:@[a-z0-9*?][a-z0-9._*?-]*\/)?[a-z0-9*?][a-z0-9._*?-]*$/.test(pattern))) {
|
|
109768
|
+
throw new Error("MIN_RELEASE_AGE_EXCLUDE_INVALID");
|
|
109769
|
+
}
|
|
109770
|
+
return { minReleaseAge: options.minReleaseAge, minReleaseAgeExclude: [...new Set(exclusions)] };
|
|
109771
|
+
}
|
|
109772
|
+
async function npmReleaseAgeCapability(cwd2, env3) {
|
|
109773
|
+
try {
|
|
109774
|
+
const versionProbe = Bun.spawn(["npm", "--version"], { cwd: cwd2, env: env3, stdin: "ignore", stdout: "pipe", stderr: "ignore" });
|
|
109775
|
+
const [versionText, versionStatus] = await Promise.all([new Response(versionProbe.stdout).text(), versionProbe.exited]);
|
|
109776
|
+
const version2 = versionText.trim();
|
|
109777
|
+
const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version2);
|
|
109778
|
+
if (versionStatus !== 0 || !match || Number(match[1]) < 11 || Number(match[1]) === 11 && Number(match[2]) < 19)
|
|
109779
|
+
throw new Error("NPM_RELEASE_AGE_UNSUPPORTED");
|
|
109780
|
+
const configProbe = Bun.spawn(["npm", "config", "list", "--json"], { cwd: cwd2, env: env3, stdin: "ignore", stdout: "pipe", stderr: "ignore" });
|
|
109781
|
+
const [configText, configStatus] = await Promise.all([new Response(configProbe.stdout).text(), configProbe.exited]);
|
|
109782
|
+
const config2 = object9(JSON.parse(configText), "NPM_RELEASE_AGE_UNSUPPORTED");
|
|
109783
|
+
if (configStatus !== 0 || !Object.hasOwn(config2, "min-release-age") || !Object.hasOwn(config2, "min-release-age-exclude") || !Array.isArray(config2["min-release-age-exclude"]))
|
|
109784
|
+
throw new Error("NPM_RELEASE_AGE_UNSUPPORTED");
|
|
109785
|
+
return version2;
|
|
109786
|
+
} catch {
|
|
109787
|
+
throw new Error("NPM_RELEASE_AGE_UNSUPPORTED");
|
|
109788
|
+
}
|
|
109789
|
+
}
|
|
109790
|
+
async function runNpm(args2, cwd2, home, staging, policy) {
|
|
109699
109791
|
const userNpmrc = join56(staging, "npm-user.npmrc"), globalNpmrc = join56(staging, "npm-global.npmrc");
|
|
109700
|
-
|
|
109701
|
-
writeFileSync23(userNpmrc, `registry=${REGISTRY_ORIGIN}/
|
|
109792
|
+
const baseConfig = `registry=${REGISTRY_ORIGIN}/
|
|
109702
109793
|
ignore-scripts=true
|
|
109703
|
-
|
|
109794
|
+
`;
|
|
109704
109795
|
if (!existsSync34(globalNpmrc))
|
|
109705
|
-
writeFileSync23(globalNpmrc,
|
|
109706
|
-
ignore-scripts=true
|
|
109707
|
-
`, { mode: 384, flag: "wx" });
|
|
109796
|
+
writeFileSync23(globalNpmrc, baseConfig, { mode: 384, flag: "wx" });
|
|
109708
109797
|
const env3 = {
|
|
109709
109798
|
PATH: process.env.PATH ?? "",
|
|
109710
109799
|
HOME: home,
|
|
@@ -109718,6 +109807,19 @@ ignore-scripts=true
|
|
|
109718
109807
|
NPM_CONFIG_REGISTRY: `${REGISTRY_ORIGIN}/`,
|
|
109719
109808
|
NPM_CONFIG_IGNORE_SCRIPTS: "true"
|
|
109720
109809
|
};
|
|
109810
|
+
const capabilityNpmrc = join56(staging, "npm-capability.npmrc");
|
|
109811
|
+
if (policy && !existsSync34(capabilityNpmrc))
|
|
109812
|
+
writeFileSync23(capabilityNpmrc, baseConfig, { mode: 384, flag: "wx" });
|
|
109813
|
+
if (policy && readFileSync37(capabilityNpmrc, "utf8") !== baseConfig)
|
|
109814
|
+
throw new Error("NPM_ISOLATED_CONFIG_DRIFT");
|
|
109815
|
+
const npmVersion = policy ? await npmReleaseAgeCapability(cwd2, { ...env3, NPM_CONFIG_USERCONFIG: capabilityNpmrc }) : undefined;
|
|
109816
|
+
const config2 = baseConfig + (policy ? `min-release-age=${policy.minReleaseAge}
|
|
109817
|
+
${policy.minReleaseAgeExclude.map((pattern) => `min-release-age-exclude[]=${pattern}
|
|
109818
|
+
`).join("")}` : "");
|
|
109819
|
+
if (!existsSync34(userNpmrc))
|
|
109820
|
+
writeFileSync23(userNpmrc, config2, { mode: 384, flag: "wx" });
|
|
109821
|
+
if (readFileSync37(userNpmrc, "utf8") !== config2 || readFileSync37(globalNpmrc, "utf8") !== baseConfig)
|
|
109822
|
+
throw new Error("NPM_ISOLATED_CONFIG_DRIFT");
|
|
109721
109823
|
const proc = Bun.spawn(["npm", ...args2, "--registry", `${REGISTRY_ORIGIN}/`, "--ignore-scripts", "--no-audit", "--no-fund"], {
|
|
109722
109824
|
cwd: cwd2,
|
|
109723
109825
|
env: env3,
|
|
@@ -109728,6 +109830,7 @@ ignore-scripts=true
|
|
|
109728
109830
|
const exitCode = await proc.exited;
|
|
109729
109831
|
if (exitCode !== 0)
|
|
109730
109832
|
throw new Error("DEPENDENCY_GRAPH_INSTALL_FAILED");
|
|
109833
|
+
return npmVersion;
|
|
109731
109834
|
}
|
|
109732
109835
|
function comparePackageTrees(expectedRoot, installedRoot) {
|
|
109733
109836
|
const expected = readTree(expectedRoot, false);
|
|
@@ -110106,6 +110209,7 @@ async function updateCopyfileRuntime(version2, options = {}) {
|
|
|
110106
110209
|
throw new Error("EXACT_VERSION_REQUIRED");
|
|
110107
110210
|
if (!STABLE_SEMVER_PATTERN.test(version2))
|
|
110108
110211
|
throw new Error("EXACT_STABLE_VERSION_REQUIRED");
|
|
110212
|
+
const policy = releaseAgePolicy(options);
|
|
110109
110213
|
const home = resolve24(options.homeDir ?? process.env.HOME ?? "");
|
|
110110
110214
|
if (!home || !existsSync34(home))
|
|
110111
110215
|
throw new Error("HOME_NOT_FOUND");
|
|
@@ -110143,8 +110247,10 @@ async function updateCopyfileRuntime(version2, options = {}) {
|
|
|
110143
110247
|
mkdirSync24(stagePackage, { mode: 448 });
|
|
110144
110248
|
copyFileSync(artifact.tarballPath, join56(stagePackage, "verified.tgz"));
|
|
110145
110249
|
writeFileSync23(join56(stagePackage, "package.json"), JSON.stringify({ private: true, dependencies: { [PACKAGE_NAME]: "file:./verified.tgz" } }, null, 2), { mode: 384, flag: "wx" });
|
|
110146
|
-
await runNpm(["install", "--package-lock-only", "--prefix", stagePackage], stagePackage, home, stagePath);
|
|
110147
|
-
await runNpm(["ci", "--prefix", stagePackage], stagePackage, home, stagePath);
|
|
110250
|
+
const npmVersion = await runNpm(["install", "--package-lock-only", "--prefix", stagePackage], stagePackage, home, stagePath, policy);
|
|
110251
|
+
const ciNpmVersion = await runNpm(["ci", "--prefix", stagePackage], stagePackage, home, stagePath, policy);
|
|
110252
|
+
if (npmVersion !== ciNpmVersion)
|
|
110253
|
+
throw new Error("NPM_VERSION_DRIFT_DURING_UPDATE");
|
|
110148
110254
|
const lockBytes = readFileSync37(join56(stagePackage, "package-lock.json"));
|
|
110149
110255
|
const installedPackage = join56(stagePackage, "node_modules", "@hasna", "skills");
|
|
110150
110256
|
const installed = readPackage(join56(installedPackage, "package.json"));
|
|
@@ -110185,6 +110291,7 @@ async function updateCopyfileRuntime(version2, options = {}) {
|
|
|
110185
110291
|
targetPackageRoot,
|
|
110186
110292
|
configs: configPreimages,
|
|
110187
110293
|
preimageSha256,
|
|
110294
|
+
...policy ? { dependencyPolicy: { ...policy, npmVersion } } : {},
|
|
110188
110295
|
launchers,
|
|
110189
110296
|
switchedLaunchers: [],
|
|
110190
110297
|
rollbackCompletedLaunchers: []
|
|
@@ -110223,7 +110330,7 @@ async function updateCopyfileRuntime(version2, options = {}) {
|
|
|
110223
110330
|
}
|
|
110224
110331
|
receipt.state = "switched";
|
|
110225
110332
|
persistReceipt(join56(finalPath, "rollout-receipt.json"), receipt);
|
|
110226
|
-
return { updated: true, version: version2, currentVersion: layout.currentVersion, receiptId: id2, runtimeRoot: finalPath, tarballSha256: artifact.sha256, tarballIntegrity: artifact.integrity, runtimeTreeSha256: stagedTree.digest, launcherCount: launchers.length, configCount: configPreimages.length };
|
|
110333
|
+
return { updated: true, version: version2, currentVersion: layout.currentVersion, receiptId: id2, runtimeRoot: finalPath, tarballSha256: artifact.sha256, tarballIntegrity: artifact.integrity, runtimeTreeSha256: stagedTree.digest, launcherCount: launchers.length, configCount: configPreimages.length, ...policy ? { dependencyPolicy: { ...policy, npmVersion } } : {} };
|
|
110227
110334
|
} catch (error61) {
|
|
110228
110335
|
if (switched.length > 0 || moved) {
|
|
110229
110336
|
let rollbackFailed = false;
|
|
@@ -110447,7 +110554,12 @@ function registerRuntime(parent) {
|
|
|
110447
110554
|
});
|
|
110448
110555
|
const setup = parent.command("setup").description("Point this CLI at a Skills API server, or register agent integrations").option("--api-url <url>", "Skills API origin to send remote work to").option("--global", "Accepted for compatibility; the API origin is always per-user", false).option("--json", "Output setup result as JSON", false).action(async (options) => handleSetup(options));
|
|
110449
110556
|
setup.command("agents").option("--json", "Output registration result as JSON", false).description("Register the Skills MCP server with all supported agents").action(async (options) => handleMcp({ register: "all", json: options.json }));
|
|
110450
|
-
parent.command("self-update").description("Update @hasna/skills to the latest version").option("--json", "Output result as JSON", false).option("--version <version>", "Install this exact @hasna/skills version as a staged copyfile runtime").option("--rollback <receipt-id>", "Restore launcher links from an exact copyfile rollout receipt").option("--adopt-aliases", "Switch verified legacy @hasna/skills aliases to the active copyfile runtime").option("--rollback-aliases <receipt-id>", "Restore aliases from an exact adoption receipt").action(async (options) => {
|
|
110557
|
+
parent.command("self-update").description("Update @hasna/skills to the latest version").option("--json", "Output result as JSON", false).option("--version <version>", "Install this exact @hasna/skills version as a staged copyfile runtime").option("--min-release-age <days>", "Require a positive integer dependency release age in days for an exact-version update").option("--min-release-age-exclude <pattern>", "Exclude matching package identities from the release-age policy (repeatable)", (pattern, prior) => [...prior, pattern], []).option("--rollback <receipt-id>", "Restore launcher links from an exact copyfile rollout receipt").option("--adopt-aliases", "Switch verified legacy @hasna/skills aliases to the active copyfile runtime").option("--rollback-aliases <receipt-id>", "Restore aliases from an exact adoption receipt").action(async (options) => {
|
|
110558
|
+
if ((options.minReleaseAge !== undefined || options.minReleaseAgeExclude.length > 0) && (!options.version || options.rollback || options.adoptAliases || options.rollbackAliases)) {
|
|
110559
|
+
console.error(JSON.stringify({ error: "RELEASE_AGE_POLICY_REQUIRES_EXACT_VERSION" }));
|
|
110560
|
+
process.exitCode = 1;
|
|
110561
|
+
return;
|
|
110562
|
+
}
|
|
110451
110563
|
if ([options.version, options.rollback, options.adoptAliases, options.rollbackAliases].filter(Boolean).length > 1) {
|
|
110452
110564
|
console.error(JSON.stringify({ error: "SELF_UPDATE_OPERATIONS_ARE_MUTUALLY_EXCLUSIVE" }));
|
|
110453
110565
|
process.exitCode = 1;
|
|
@@ -110483,7 +110595,8 @@ function registerRuntime(parent) {
|
|
|
110483
110595
|
}
|
|
110484
110596
|
if (options.version) {
|
|
110485
110597
|
try {
|
|
110486
|
-
const
|
|
110598
|
+
const minReleaseAge = options.minReleaseAge === undefined ? undefined : /^\d+$/.test(options.minReleaseAge) ? Number(options.minReleaseAge) : NaN;
|
|
110599
|
+
const result2 = await updateCopyfileRuntime(options.version, { minReleaseAge, minReleaseAgeExclude: options.minReleaseAgeExclude });
|
|
110487
110600
|
if (options.json)
|
|
110488
110601
|
console.log(JSON.stringify(result2));
|
|
110489
110602
|
else
|
package/bin/mcp.js
CHANGED
|
@@ -20019,7 +20019,7 @@ var package_default;
|
|
|
20019
20019
|
var init_package = __esm(() => {
|
|
20020
20020
|
package_default = {
|
|
20021
20021
|
name: "@hasna/skills",
|
|
20022
|
-
version: "0.10.
|
|
20022
|
+
version: "0.10.34",
|
|
20023
20023
|
description: "Skills library for AI coding agents",
|
|
20024
20024
|
type: "module",
|
|
20025
20025
|
bin: {
|
|
@@ -46227,7 +46227,7 @@ var init_profile_limits = __esm(() => {
|
|
|
46227
46227
|
// src/lib/selection-cache.ts
|
|
46228
46228
|
import { createHash as createHash6, randomUUID } from "crypto";
|
|
46229
46229
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
46230
|
-
import { closeSync as closeSync6, constants as constants3, fstatSync as fstatSync6, fsyncSync, lstatSync as lstatSync8, mkdirSync as mkdirSync3, openSync as openSync6, readFileSync as readFileSync10, readlinkSync as readlinkSync2, readSync as readSync3, renameSync, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
|
|
46230
|
+
import { closeSync as closeSync6, constants as constants3, fstatSync as fstatSync6, fsyncSync, linkSync, lstatSync as lstatSync8, mkdirSync as mkdirSync3, openSync as openSync6, readFileSync as readFileSync10, readlinkSync as readlinkSync2, readSync as readSync3, renameSync, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
|
|
46231
46231
|
import { hostname as hostname3 } from "os";
|
|
46232
46232
|
import { dirname as dirname8, join as join14, parse as parse7, resolve as resolve7 } from "path";
|
|
46233
46233
|
import { isDeepStrictEqual } from "util";
|
|
@@ -46563,7 +46563,7 @@ function readSessionLock(sessionId, options) {
|
|
|
46563
46563
|
const before = lstatSync8(path, { throwIfNoEntry: false });
|
|
46564
46564
|
if (!before)
|
|
46565
46565
|
return null;
|
|
46566
|
-
if (!before.isFile() || before.nlink
|
|
46566
|
+
if (!before.isFile() || ![1, 2].includes(before.nlink) || (before.mode & 63) !== 0 || process.getuid && before.uid !== process.getuid()) {
|
|
46567
46567
|
throw new SkillSelectionError("SESSION_WRITE_LOCKED", "The session lock is not a private regular file; recovery requires review.");
|
|
46568
46568
|
}
|
|
46569
46569
|
const bytes = readRegularFile(path, SESSION_LOCK_MAX_BYTES);
|
|
@@ -46577,10 +46577,54 @@ function readSessionLock(sessionId, options) {
|
|
|
46577
46577
|
} catch {
|
|
46578
46578
|
throw new SkillSelectionError("SESSION_WRITE_LOCKED", "The session lock is unreadable; recovery requires review.");
|
|
46579
46579
|
}
|
|
46580
|
-
if (!marker || ![1, 2].includes(marker.schemaVersion) || marker.sessionId !== sessionId || !/^[a-f0-9-]{36}$/i.test(marker.operationId) || !Number.isSafeInteger(marker.pid) || marker.pid <= 0 || marker.schemaVersion === 2 && (!marker.host || !marker.bootId || !marker.processStart || process.platform === "linux" && !marker.pidNamespace)) {
|
|
46580
|
+
if (!marker || ![1, 2].includes(marker.schemaVersion) || marker.sessionId !== sessionId || !/^[a-f0-9-]{36}$/i.test(marker.operationId) || !Number.isSafeInteger(marker.pid) || marker.pid <= 0 || marker.publication !== undefined && marker.publication !== "hard-link-v1" || marker.schemaVersion === 2 && (!marker.host || !marker.bootId || !marker.processStart || process.platform === "linux" && !marker.pidNamespace)) {
|
|
46581
46581
|
throw new SkillSelectionError("SESSION_WRITE_LOCKED", "The session lock has invalid ownership metadata; recovery requires review.");
|
|
46582
46582
|
}
|
|
46583
|
-
|
|
46583
|
+
let stagingPath;
|
|
46584
|
+
if (after.nlink === 2) {
|
|
46585
|
+
stagingPath = `${path}.${marker.operationId}.prepared`;
|
|
46586
|
+
assertRegularPath(stagingPath);
|
|
46587
|
+
const staged = lstatSync8(stagingPath, { throwIfNoEntry: false });
|
|
46588
|
+
if (marker.publication !== "hard-link-v1" || !staged?.isFile() || staged.dev !== after.dev || staged.ino !== after.ino || staged.nlink !== 2 || staged.mode !== after.mode || staged.uid !== after.uid || staged.gid !== after.gid) {
|
|
46589
|
+
throw new SkillSelectionError("SESSION_WRITE_LOCKED", "The session lock has an unbound publication link; recovery requires review.");
|
|
46590
|
+
}
|
|
46591
|
+
}
|
|
46592
|
+
return { path, bytes, marker, sha256: sha256Hex(bytes), dev: after.dev, ino: after.ino, mtimeMs: after.mtimeMs, stagingPath };
|
|
46593
|
+
}
|
|
46594
|
+
function publishSessionLock(path, marker) {
|
|
46595
|
+
const stagingPath = `${path}.${marker.operationId}.prepared`;
|
|
46596
|
+
const bytes = Buffer.from(`${JSON.stringify({ ...marker, publication: "hard-link-v1" })}
|
|
46597
|
+
`);
|
|
46598
|
+
const fd = openSync6(stagingPath, constants3.O_WRONLY | constants3.O_CREAT | constants3.O_EXCL | constants3.O_NOFOLLOW, 384);
|
|
46599
|
+
const stat = fstatSync6(fd);
|
|
46600
|
+
let published = false, handedOff = false;
|
|
46601
|
+
try {
|
|
46602
|
+
writeFileSync3(fd, bytes);
|
|
46603
|
+
fsyncSync(fd);
|
|
46604
|
+
linkSync(stagingPath, path);
|
|
46605
|
+
published = true;
|
|
46606
|
+
syncDirectory(dirname8(path));
|
|
46607
|
+
const staged = lstatSync8(stagingPath);
|
|
46608
|
+
if (!staged.isFile() || staged.dev !== stat.dev || staged.ino !== stat.ino) {
|
|
46609
|
+
throw new SkillSelectionError("SESSION_WRITE_LOCK_CHANGED", "The initialized lock staging inode changed before publication completed.");
|
|
46610
|
+
}
|
|
46611
|
+
unlinkSync(stagingPath);
|
|
46612
|
+
syncDirectory(dirname8(path));
|
|
46613
|
+
handedOff = true;
|
|
46614
|
+
return { path, stagingPath, fd, dev: stat.dev, ino: stat.ino };
|
|
46615
|
+
} finally {
|
|
46616
|
+
if (!handedOff) {
|
|
46617
|
+
try {
|
|
46618
|
+
for (const target of [...published ? [path] : [], stagingPath]) {
|
|
46619
|
+
const current = lstatSync8(target, { throwIfNoEntry: false });
|
|
46620
|
+
if (current?.isFile() && current.dev === stat.dev && current.ino === stat.ino)
|
|
46621
|
+
unlinkSync(target);
|
|
46622
|
+
}
|
|
46623
|
+
} finally {
|
|
46624
|
+
closeSync6(fd);
|
|
46625
|
+
}
|
|
46626
|
+
}
|
|
46627
|
+
}
|
|
46584
46628
|
}
|
|
46585
46629
|
function writerIsGone(lock, explicitLocalHost) {
|
|
46586
46630
|
if (Date.now() - lock.mtimeMs < SESSION_LOCK_MIN_AGE_MS)
|
|
@@ -46642,10 +46686,21 @@ function archiveSessionLock(lock, receipt, explicitLocalHost) {
|
|
|
46642
46686
|
try {
|
|
46643
46687
|
renameSync(lock.path, archivePath);
|
|
46644
46688
|
moved = true;
|
|
46689
|
+
const archivedPaths = [archivePath];
|
|
46690
|
+
if (finalLock.stagingPath) {
|
|
46691
|
+
const publicationPath = join14(directory, "publication.write-lock");
|
|
46692
|
+
renameSync(finalLock.stagingPath, publicationPath);
|
|
46693
|
+
archivedPaths.push(publicationPath);
|
|
46694
|
+
}
|
|
46645
46695
|
syncDirectory(directory);
|
|
46646
46696
|
syncDirectory(dirname8(lock.path));
|
|
46647
|
-
|
|
46648
|
-
|
|
46697
|
+
for (const path of archivedPaths) {
|
|
46698
|
+
const before = lstatSync8(path, { throwIfNoEntry: false });
|
|
46699
|
+
const bytes = readRegularFile(path, SESSION_LOCK_MAX_BYTES);
|
|
46700
|
+
const after = lstatSync8(path, { throwIfNoEntry: false });
|
|
46701
|
+
if (!before?.isFile() || before.dev !== lock.dev || before.ino !== lock.ino || before.nlink !== archivedPaths.length || before.mtimeMs !== lock.mtimeMs || (before.mode & 63) !== 0 || process.getuid && before.uid !== process.getuid() || !bytes || sha256Hex(bytes) !== lock.sha256 || !after?.isFile() || before.dev !== after.dev || before.ino !== after.ino || before.mtimeMs !== after.mtimeMs || before.mode !== after.mode || before.uid !== after.uid || before.gid !== after.gid || before.nlink !== after.nlink) {
|
|
46702
|
+
throw new Error("Archived lock ownership or bytes changed");
|
|
46703
|
+
}
|
|
46649
46704
|
}
|
|
46650
46705
|
} catch (error61) {
|
|
46651
46706
|
if (moved)
|
|
@@ -46662,6 +46717,7 @@ function archiveSessionLock(lock, receipt, explicitLocalHost) {
|
|
|
46662
46717
|
}
|
|
46663
46718
|
function withSessionWriteLocks(sessionIds, options, action) {
|
|
46664
46719
|
const operationId = randomUUID();
|
|
46720
|
+
const identity3 = localLockIdentity();
|
|
46665
46721
|
const locks = [...new Set(sessionIds)].map((sessionId) => ({ sessionId, path: `${sessionReceiptPath(sessionId, options)}.write-lock` })).sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
46666
46722
|
const owned = [];
|
|
46667
46723
|
const assertOwned = () => {
|
|
@@ -46675,9 +46731,10 @@ function withSessionWriteLocks(sessionIds, options, action) {
|
|
|
46675
46731
|
try {
|
|
46676
46732
|
for (const lock of locks) {
|
|
46677
46733
|
assertRegularPath(lock.path, true);
|
|
46678
|
-
|
|
46734
|
+
const marker = { schemaVersion: identity3 ? 2 : 1, operationId, sessionId: lock.sessionId, pid: process.pid, ...identity3 };
|
|
46735
|
+
let acquired;
|
|
46679
46736
|
try {
|
|
46680
|
-
|
|
46737
|
+
acquired = publishSessionLock(lock.path, marker);
|
|
46681
46738
|
} catch (error61) {
|
|
46682
46739
|
if (error61.code !== "EEXIST")
|
|
46683
46740
|
throw error61;
|
|
@@ -46692,7 +46749,7 @@ function withSessionWriteLocks(sessionIds, options, action) {
|
|
|
46692
46749
|
throw recoveryError;
|
|
46693
46750
|
}
|
|
46694
46751
|
try {
|
|
46695
|
-
|
|
46752
|
+
acquired = publishSessionLock(lock.path, marker);
|
|
46696
46753
|
} catch (retryError) {
|
|
46697
46754
|
if (retryError.code === "EEXIST") {
|
|
46698
46755
|
if (!readSessionLock(lock.sessionId, options))
|
|
@@ -46702,19 +46759,20 @@ function withSessionWriteLocks(sessionIds, options, action) {
|
|
|
46702
46759
|
throw retryError;
|
|
46703
46760
|
}
|
|
46704
46761
|
}
|
|
46705
|
-
|
|
46706
|
-
owned.push({ path: lock.path, fd, dev: stat.dev, ino: stat.ino });
|
|
46707
|
-
const identity3 = localLockIdentity();
|
|
46708
|
-
writeFileSync3(fd, `${JSON.stringify({ schemaVersion: identity3 ? 2 : 1, operationId, sessionId: lock.sessionId, pid: process.pid, ...identity3 })}
|
|
46709
|
-
`);
|
|
46762
|
+
owned.push(acquired);
|
|
46710
46763
|
}
|
|
46711
46764
|
return action(assertOwned);
|
|
46712
46765
|
} finally {
|
|
46713
46766
|
for (const lock of owned.reverse()) {
|
|
46714
|
-
|
|
46715
|
-
|
|
46716
|
-
|
|
46717
|
-
|
|
46767
|
+
try {
|
|
46768
|
+
for (const path of [lock.path, lock.stagingPath]) {
|
|
46769
|
+
const current = lstatSync8(path, { throwIfNoEntry: false });
|
|
46770
|
+
if (current?.isFile() && current.dev === lock.dev && current.ino === lock.ino)
|
|
46771
|
+
unlinkSync(path);
|
|
46772
|
+
}
|
|
46773
|
+
} finally {
|
|
46774
|
+
closeSync6(lock.fd);
|
|
46775
|
+
}
|
|
46718
46776
|
}
|
|
46719
46777
|
}
|
|
46720
46778
|
}
|
|
@@ -48897,7 +48955,7 @@ var init_plugin_projection = __esm(() => {
|
|
|
48897
48955
|
});
|
|
48898
48956
|
|
|
48899
48957
|
// src/lib/plugin-projection-store.ts
|
|
48900
|
-
import { constants as constants7, closeSync as closeSync10, fchmodSync, fstatSync as fstatSync10, fsyncSync as fsyncSync2, linkSync, lstatSync as lstatSync14, mkdirSync as mkdirSync6, openSync as openSync10, opendirSync as opendirSync2, readSync as readSync6, renameSync as renameSync2, rmdirSync as rmdirSync2, rmSync as rmSync2, unlinkSync as unlinkSync3, writeSync as writeSync2 } from "fs";
|
|
48958
|
+
import { constants as constants7, closeSync as closeSync10, fchmodSync, fstatSync as fstatSync10, fsyncSync as fsyncSync2, linkSync as linkSync2, lstatSync as lstatSync14, mkdirSync as mkdirSync6, openSync as openSync10, opendirSync as opendirSync2, readSync as readSync6, renameSync as renameSync2, rmdirSync as rmdirSync2, rmSync as rmSync2, unlinkSync as unlinkSync3, writeSync as writeSync2 } from "fs";
|
|
48901
48959
|
import { dirname as dirname11, isAbsolute as isAbsolute9, join as join22, parse as parse9, resolve as resolve11 } from "path";
|
|
48902
48960
|
function stat(path) {
|
|
48903
48961
|
try {
|
|
@@ -49956,13 +50014,13 @@ function resolveAgentDiscovery(options) {
|
|
|
49956
50014
|
unresolved("JSONC configuration requires a supported format adapter");
|
|
49957
50015
|
for (const root of [directory, join25(home, ".claude"), join25(home, ".agents")])
|
|
49958
50016
|
for (const name of ["skill", "skills"])
|
|
49959
|
-
roots.add(join25(root, name));
|
|
50017
|
+
roots.add(canonical6(join25(root, name)));
|
|
49960
50018
|
if (config2.skills !== undefined && (!Array.isArray(config2.skills) || config2.skills.some((path) => typeof path !== "string")))
|
|
49961
50019
|
unresolved("skills must be a string array");
|
|
49962
50020
|
for (const value of config2.skills ?? []) {
|
|
49963
50021
|
if (typeof value !== "string" || /[\0$]/.test(value) || !value || !(isAbsolute13(value) || value.startsWith("~/")))
|
|
49964
50022
|
unresolved("remote or relative skill sources need a dedicated discovery adapter");
|
|
49965
|
-
roots.add(value.startsWith("~/") ? join25(home, value.slice(2)) : value);
|
|
50023
|
+
roots.add(canonical6(value.startsWith("~/") ? join25(home, value.slice(2)) : value));
|
|
49966
50024
|
}
|
|
49967
50025
|
for (const key of ["plugin", "plugins"])
|
|
49968
50026
|
if (config2[key] !== undefined && !Array.isArray(config2[key]))
|
package/bin/migrate.js
CHANGED
package/bin/server.js
CHANGED
|
@@ -24122,7 +24122,7 @@ var init_dist_es9 = __esm(() => {
|
|
|
24122
24122
|
// package.json
|
|
24123
24123
|
var package_default = {
|
|
24124
24124
|
name: "@hasna/skills",
|
|
24125
|
-
version: "0.10.
|
|
24125
|
+
version: "0.10.34",
|
|
24126
24126
|
description: "Skills library for AI coding agents",
|
|
24127
24127
|
type: "module",
|
|
24128
24128
|
bin: {
|
package/bin/worker.js
CHANGED
|
@@ -24125,7 +24125,7 @@ import { randomUUID as randomUUID6 } from "crypto";
|
|
|
24125
24125
|
// package.json
|
|
24126
24126
|
var package_default = {
|
|
24127
24127
|
name: "@hasna/skills",
|
|
24128
|
-
version: "0.10.
|
|
24128
|
+
version: "0.10.34",
|
|
24129
24129
|
description: "Skills library for AI coding agents",
|
|
24130
24130
|
type: "module",
|
|
24131
24131
|
bin: {
|
|
@@ -26,6 +26,8 @@ export declare function updateCopyfileRuntime(version: string, options?: {
|
|
|
26
26
|
pathValue?: string;
|
|
27
27
|
registryOrigin?: string;
|
|
28
28
|
fetcher?: typeof fetch;
|
|
29
|
+
minReleaseAge?: number;
|
|
30
|
+
minReleaseAgeExclude?: string[];
|
|
29
31
|
onLauncherSwitched?: (path: string) => void;
|
|
30
32
|
}): Promise<JsonObject>;
|
|
31
33
|
export declare function rollbackCopyfileRuntime(receiptId: string, options?: {
|
package/dist/index.js
CHANGED
|
@@ -9665,7 +9665,7 @@ import { isAbsolute as isAbsolute13, join as join26, resolve as resolve13 } from
|
|
|
9665
9665
|
// src/lib/selection-cache.ts
|
|
9666
9666
|
import { createHash as createHash6, randomUUID } from "crypto";
|
|
9667
9667
|
import { spawnSync } from "child_process";
|
|
9668
|
-
import { closeSync as closeSync2, constants as constants2, fstatSync as fstatSync2, fsyncSync, lstatSync as lstatSync7, mkdirSync as mkdirSync3, openSync as openSync2, readFileSync as readFileSync8, readlinkSync as readlinkSync2, readSync as readSync2, renameSync, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
|
|
9668
|
+
import { closeSync as closeSync2, constants as constants2, fstatSync as fstatSync2, fsyncSync, linkSync, lstatSync as lstatSync7, mkdirSync as mkdirSync3, openSync as openSync2, readFileSync as readFileSync8, readlinkSync as readlinkSync2, readSync as readSync2, renameSync, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
|
|
9669
9669
|
import { hostname } from "os";
|
|
9670
9670
|
import { dirname as dirname7, join as join11, parse as parse2, resolve as resolve7 } from "path";
|
|
9671
9671
|
import { isDeepStrictEqual } from "util";
|
|
@@ -10655,7 +10655,7 @@ function readSessionLock(sessionId, options) {
|
|
|
10655
10655
|
const before = lstatSync7(path, { throwIfNoEntry: false });
|
|
10656
10656
|
if (!before)
|
|
10657
10657
|
return null;
|
|
10658
|
-
if (!before.isFile() || before.nlink
|
|
10658
|
+
if (!before.isFile() || ![1, 2].includes(before.nlink) || (before.mode & 63) !== 0 || process.getuid && before.uid !== process.getuid()) {
|
|
10659
10659
|
throw new SkillSelectionError("SESSION_WRITE_LOCKED", "The session lock is not a private regular file; recovery requires review.");
|
|
10660
10660
|
}
|
|
10661
10661
|
const bytes = readRegularFile(path, SESSION_LOCK_MAX_BYTES);
|
|
@@ -10669,10 +10669,54 @@ function readSessionLock(sessionId, options) {
|
|
|
10669
10669
|
} catch {
|
|
10670
10670
|
throw new SkillSelectionError("SESSION_WRITE_LOCKED", "The session lock is unreadable; recovery requires review.");
|
|
10671
10671
|
}
|
|
10672
|
-
if (!marker || ![1, 2].includes(marker.schemaVersion) || marker.sessionId !== sessionId || !/^[a-f0-9-]{36}$/i.test(marker.operationId) || !Number.isSafeInteger(marker.pid) || marker.pid <= 0 || marker.schemaVersion === 2 && (!marker.host || !marker.bootId || !marker.processStart || process.platform === "linux" && !marker.pidNamespace)) {
|
|
10672
|
+
if (!marker || ![1, 2].includes(marker.schemaVersion) || marker.sessionId !== sessionId || !/^[a-f0-9-]{36}$/i.test(marker.operationId) || !Number.isSafeInteger(marker.pid) || marker.pid <= 0 || marker.publication !== undefined && marker.publication !== "hard-link-v1" || marker.schemaVersion === 2 && (!marker.host || !marker.bootId || !marker.processStart || process.platform === "linux" && !marker.pidNamespace)) {
|
|
10673
10673
|
throw new SkillSelectionError("SESSION_WRITE_LOCKED", "The session lock has invalid ownership metadata; recovery requires review.");
|
|
10674
10674
|
}
|
|
10675
|
-
|
|
10675
|
+
let stagingPath;
|
|
10676
|
+
if (after.nlink === 2) {
|
|
10677
|
+
stagingPath = `${path}.${marker.operationId}.prepared`;
|
|
10678
|
+
assertRegularPath(stagingPath);
|
|
10679
|
+
const staged = lstatSync7(stagingPath, { throwIfNoEntry: false });
|
|
10680
|
+
if (marker.publication !== "hard-link-v1" || !staged?.isFile() || staged.dev !== after.dev || staged.ino !== after.ino || staged.nlink !== 2 || staged.mode !== after.mode || staged.uid !== after.uid || staged.gid !== after.gid) {
|
|
10681
|
+
throw new SkillSelectionError("SESSION_WRITE_LOCKED", "The session lock has an unbound publication link; recovery requires review.");
|
|
10682
|
+
}
|
|
10683
|
+
}
|
|
10684
|
+
return { path, bytes, marker, sha256: sha256Hex(bytes), dev: after.dev, ino: after.ino, mtimeMs: after.mtimeMs, stagingPath };
|
|
10685
|
+
}
|
|
10686
|
+
function publishSessionLock(path, marker) {
|
|
10687
|
+
const stagingPath = `${path}.${marker.operationId}.prepared`;
|
|
10688
|
+
const bytes = Buffer.from(`${JSON.stringify({ ...marker, publication: "hard-link-v1" })}
|
|
10689
|
+
`);
|
|
10690
|
+
const fd = openSync2(stagingPath, constants2.O_WRONLY | constants2.O_CREAT | constants2.O_EXCL | constants2.O_NOFOLLOW, 384);
|
|
10691
|
+
const stat = fstatSync2(fd);
|
|
10692
|
+
let published = false, handedOff = false;
|
|
10693
|
+
try {
|
|
10694
|
+
writeFileSync3(fd, bytes);
|
|
10695
|
+
fsyncSync(fd);
|
|
10696
|
+
linkSync(stagingPath, path);
|
|
10697
|
+
published = true;
|
|
10698
|
+
syncDirectory(dirname7(path));
|
|
10699
|
+
const staged = lstatSync7(stagingPath);
|
|
10700
|
+
if (!staged.isFile() || staged.dev !== stat.dev || staged.ino !== stat.ino) {
|
|
10701
|
+
throw new SkillSelectionError("SESSION_WRITE_LOCK_CHANGED", "The initialized lock staging inode changed before publication completed.");
|
|
10702
|
+
}
|
|
10703
|
+
unlinkSync(stagingPath);
|
|
10704
|
+
syncDirectory(dirname7(path));
|
|
10705
|
+
handedOff = true;
|
|
10706
|
+
return { path, stagingPath, fd, dev: stat.dev, ino: stat.ino };
|
|
10707
|
+
} finally {
|
|
10708
|
+
if (!handedOff) {
|
|
10709
|
+
try {
|
|
10710
|
+
for (const target of [...published ? [path] : [], stagingPath]) {
|
|
10711
|
+
const current = lstatSync7(target, { throwIfNoEntry: false });
|
|
10712
|
+
if (current?.isFile() && current.dev === stat.dev && current.ino === stat.ino)
|
|
10713
|
+
unlinkSync(target);
|
|
10714
|
+
}
|
|
10715
|
+
} finally {
|
|
10716
|
+
closeSync2(fd);
|
|
10717
|
+
}
|
|
10718
|
+
}
|
|
10719
|
+
}
|
|
10676
10720
|
}
|
|
10677
10721
|
function writerIsGone(lock, explicitLocalHost) {
|
|
10678
10722
|
if (Date.now() - lock.mtimeMs < SESSION_LOCK_MIN_AGE_MS)
|
|
@@ -10734,10 +10778,21 @@ function archiveSessionLock(lock, receipt, explicitLocalHost) {
|
|
|
10734
10778
|
try {
|
|
10735
10779
|
renameSync(lock.path, archivePath);
|
|
10736
10780
|
moved = true;
|
|
10781
|
+
const archivedPaths = [archivePath];
|
|
10782
|
+
if (finalLock.stagingPath) {
|
|
10783
|
+
const publicationPath = join11(directory, "publication.write-lock");
|
|
10784
|
+
renameSync(finalLock.stagingPath, publicationPath);
|
|
10785
|
+
archivedPaths.push(publicationPath);
|
|
10786
|
+
}
|
|
10737
10787
|
syncDirectory(directory);
|
|
10738
10788
|
syncDirectory(dirname7(lock.path));
|
|
10739
|
-
|
|
10740
|
-
|
|
10789
|
+
for (const path of archivedPaths) {
|
|
10790
|
+
const before = lstatSync7(path, { throwIfNoEntry: false });
|
|
10791
|
+
const bytes = readRegularFile(path, SESSION_LOCK_MAX_BYTES);
|
|
10792
|
+
const after = lstatSync7(path, { throwIfNoEntry: false });
|
|
10793
|
+
if (!before?.isFile() || before.dev !== lock.dev || before.ino !== lock.ino || before.nlink !== archivedPaths.length || before.mtimeMs !== lock.mtimeMs || (before.mode & 63) !== 0 || process.getuid && before.uid !== process.getuid() || !bytes || sha256Hex(bytes) !== lock.sha256 || !after?.isFile() || before.dev !== after.dev || before.ino !== after.ino || before.mtimeMs !== after.mtimeMs || before.mode !== after.mode || before.uid !== after.uid || before.gid !== after.gid || before.nlink !== after.nlink) {
|
|
10794
|
+
throw new Error("Archived lock ownership or bytes changed");
|
|
10795
|
+
}
|
|
10741
10796
|
}
|
|
10742
10797
|
} catch (error) {
|
|
10743
10798
|
if (moved)
|
|
@@ -10754,6 +10809,7 @@ function archiveSessionLock(lock, receipt, explicitLocalHost) {
|
|
|
10754
10809
|
}
|
|
10755
10810
|
function withSessionWriteLocks(sessionIds, options, action) {
|
|
10756
10811
|
const operationId = randomUUID();
|
|
10812
|
+
const identity3 = localLockIdentity();
|
|
10757
10813
|
const locks = [...new Set(sessionIds)].map((sessionId) => ({ sessionId, path: `${sessionReceiptPath(sessionId, options)}.write-lock` })).sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
10758
10814
|
const owned = [];
|
|
10759
10815
|
const assertOwned = () => {
|
|
@@ -10767,9 +10823,10 @@ function withSessionWriteLocks(sessionIds, options, action) {
|
|
|
10767
10823
|
try {
|
|
10768
10824
|
for (const lock of locks) {
|
|
10769
10825
|
assertRegularPath(lock.path, true);
|
|
10770
|
-
|
|
10826
|
+
const marker = { schemaVersion: identity3 ? 2 : 1, operationId, sessionId: lock.sessionId, pid: process.pid, ...identity3 };
|
|
10827
|
+
let acquired;
|
|
10771
10828
|
try {
|
|
10772
|
-
|
|
10829
|
+
acquired = publishSessionLock(lock.path, marker);
|
|
10773
10830
|
} catch (error) {
|
|
10774
10831
|
if (error.code !== "EEXIST")
|
|
10775
10832
|
throw error;
|
|
@@ -10784,7 +10841,7 @@ function withSessionWriteLocks(sessionIds, options, action) {
|
|
|
10784
10841
|
throw recoveryError;
|
|
10785
10842
|
}
|
|
10786
10843
|
try {
|
|
10787
|
-
|
|
10844
|
+
acquired = publishSessionLock(lock.path, marker);
|
|
10788
10845
|
} catch (retryError) {
|
|
10789
10846
|
if (retryError.code === "EEXIST") {
|
|
10790
10847
|
if (!readSessionLock(lock.sessionId, options))
|
|
@@ -10794,19 +10851,20 @@ function withSessionWriteLocks(sessionIds, options, action) {
|
|
|
10794
10851
|
throw retryError;
|
|
10795
10852
|
}
|
|
10796
10853
|
}
|
|
10797
|
-
|
|
10798
|
-
owned.push({ path: lock.path, fd, dev: stat.dev, ino: stat.ino });
|
|
10799
|
-
const identity3 = localLockIdentity();
|
|
10800
|
-
writeFileSync3(fd, `${JSON.stringify({ schemaVersion: identity3 ? 2 : 1, operationId, sessionId: lock.sessionId, pid: process.pid, ...identity3 })}
|
|
10801
|
-
`);
|
|
10854
|
+
owned.push(acquired);
|
|
10802
10855
|
}
|
|
10803
10856
|
return action(assertOwned);
|
|
10804
10857
|
} finally {
|
|
10805
10858
|
for (const lock of owned.reverse()) {
|
|
10806
|
-
|
|
10807
|
-
|
|
10808
|
-
|
|
10809
|
-
|
|
10859
|
+
try {
|
|
10860
|
+
for (const path of [lock.path, lock.stagingPath]) {
|
|
10861
|
+
const current = lstatSync7(path, { throwIfNoEntry: false });
|
|
10862
|
+
if (current?.isFile() && current.dev === lock.dev && current.ino === lock.ino)
|
|
10863
|
+
unlinkSync(path);
|
|
10864
|
+
}
|
|
10865
|
+
} finally {
|
|
10866
|
+
closeSync2(lock.fd);
|
|
10867
|
+
}
|
|
10810
10868
|
}
|
|
10811
10869
|
}
|
|
10812
10870
|
}
|
|
@@ -10917,7 +10975,7 @@ function replaceSkillSession(expected, replacement, plan, options = {}, beforeCo
|
|
|
10917
10975
|
// package.json
|
|
10918
10976
|
var package_default = {
|
|
10919
10977
|
name: "@hasna/skills",
|
|
10920
|
-
version: "0.10.
|
|
10978
|
+
version: "0.10.34",
|
|
10921
10979
|
description: "Skills library for AI coding agents",
|
|
10922
10980
|
type: "module",
|
|
10923
10981
|
bin: {
|
|
@@ -14152,7 +14210,7 @@ function upgradeCodexSettingsWitnessV3(previous, reviewedSettingsPath) {
|
|
|
14152
14210
|
// src/lib/agent-integration.ts
|
|
14153
14211
|
import { createHash as createHash12, randomUUID as randomUUID2 } from "crypto";
|
|
14154
14212
|
import { isDeepStrictEqual as isDeepStrictEqual4 } from "util";
|
|
14155
|
-
import { existsSync as existsSync9, lstatSync as lstatSync16, statSync as statSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync12, readdirSync as readdirSync8, opendirSync as opendirSync3, readlinkSync as readlinkSync4, realpathSync as realpathSync7, renameSync as renameSync2, rmdirSync as rmdirSync2, writeFileSync as writeFileSync4, unlinkSync as unlinkSync3, chmodSync, openSync as openSync8, closeSync as closeSync8, fsyncSync as fsyncSync2, fstatSync as fstatSync8, readSync as readSync6, constants as constants7, linkSync } from "fs";
|
|
14213
|
+
import { existsSync as existsSync9, lstatSync as lstatSync16, statSync as statSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync12, readdirSync as readdirSync8, opendirSync as opendirSync3, readlinkSync as readlinkSync4, realpathSync as realpathSync7, renameSync as renameSync2, rmdirSync as rmdirSync2, writeFileSync as writeFileSync4, unlinkSync as unlinkSync3, chmodSync, openSync as openSync8, closeSync as closeSync8, fsyncSync as fsyncSync2, fstatSync as fstatSync8, readSync as readSync6, constants as constants7, linkSync as linkSync2 } from "fs";
|
|
14156
14214
|
import { dirname as dirname11, join as join18, resolve as resolve11, relative as relative5, isAbsolute as isAbsolute11, sep as sep5 } from "path";
|
|
14157
14215
|
import { homedir as homedir2 } from "os";
|
|
14158
14216
|
|
|
@@ -15741,7 +15799,7 @@ function archiveNativeSkills(inventory, options) {
|
|
|
15741
15799
|
continue;
|
|
15742
15800
|
}
|
|
15743
15801
|
if (entry.discoveryOnly) {
|
|
15744
|
-
|
|
15802
|
+
linkSync2(entry.archive, entry.source);
|
|
15745
15803
|
syncArchiveDirectory(dirname11(entry.source));
|
|
15746
15804
|
unlinkSync3(entry.archive);
|
|
15747
15805
|
} else {
|
|
@@ -17321,7 +17379,7 @@ function buildPluginProjection(entries) {
|
|
|
17321
17379
|
|
|
17322
17380
|
// src/lib/plugin-projection-store.ts
|
|
17323
17381
|
import { createHash as createHash14, randomUUID as randomUUID3 } from "crypto";
|
|
17324
|
-
import { constants as constants8, closeSync as closeSync9, fchmodSync, fstatSync as fstatSync9, fsyncSync as fsyncSync3, linkSync as
|
|
17382
|
+
import { constants as constants8, closeSync as closeSync9, fchmodSync, fstatSync as fstatSync9, fsyncSync as fsyncSync3, linkSync as linkSync3, lstatSync as lstatSync18, mkdirSync as mkdirSync8, openSync as openSync9, opendirSync as opendirSync4, readSync as readSync7, renameSync as renameSync4, rmdirSync as rmdirSync3, rmSync as rmSync3, unlinkSync as unlinkSync4, writeSync as writeSync2 } from "fs";
|
|
17325
17383
|
import { dirname as dirname13, isAbsolute as isAbsolute12, join as join25, parse as parse5, resolve as resolve12 } from "path";
|
|
17326
17384
|
var READ_FLAGS = constants8.O_RDONLY | constants8.O_NOFOLLOW | constants8.O_NONBLOCK;
|
|
17327
17385
|
var WRITE_FLAGS = constants8.O_WRONLY | constants8.O_CREAT | constants8.O_EXCL | constants8.O_NOFOLLOW | constants8.O_NONBLOCK;
|
|
@@ -17637,7 +17695,7 @@ function writePluginJsonImmutable(path, value) {
|
|
|
17637
17695
|
written = stat(temporary);
|
|
17638
17696
|
verifyAncestors(chain, true);
|
|
17639
17697
|
try {
|
|
17640
|
-
|
|
17698
|
+
linkSync3(temporary, path);
|
|
17641
17699
|
} catch (error) {
|
|
17642
17700
|
if (!(error && typeof error === "object" && ("code" in error) && error.code === "EEXIST"))
|
|
17643
17701
|
throw error;
|
|
@@ -18737,13 +18795,13 @@ function resolveAgentDiscovery(options) {
|
|
|
18737
18795
|
unresolved("JSONC configuration requires a supported format adapter");
|
|
18738
18796
|
for (const root2 of [directory, join28(home, ".claude"), join28(home, ".agents")])
|
|
18739
18797
|
for (const name of ["skill", "skills"])
|
|
18740
|
-
roots.add(join28(root2, name));
|
|
18798
|
+
roots.add(canonical6(join28(root2, name)));
|
|
18741
18799
|
if (config.skills !== undefined && (!Array.isArray(config.skills) || config.skills.some((path) => typeof path !== "string")))
|
|
18742
18800
|
unresolved("skills must be a string array");
|
|
18743
18801
|
for (const value of config.skills ?? []) {
|
|
18744
18802
|
if (typeof value !== "string" || /[\0$]/.test(value) || !value || !(isAbsolute16(value) || value.startsWith("~/")))
|
|
18745
18803
|
unresolved("remote or relative skill sources need a dedicated discovery adapter");
|
|
18746
|
-
roots.add(value.startsWith("~/") ? join28(home, value.slice(2)) : value);
|
|
18804
|
+
roots.add(canonical6(value.startsWith("~/") ? join28(home, value.slice(2)) : value));
|
|
18747
18805
|
}
|
|
18748
18806
|
for (const key of ["plugin", "plugins"])
|
|
18749
18807
|
if (config[key] !== undefined && !Array.isArray(config[key]))
|
package/dist/sdk/index.js
CHANGED
|
@@ -32373,7 +32373,7 @@ var init_event_sink = __esm(() => {
|
|
|
32373
32373
|
// package.json
|
|
32374
32374
|
var package_default = {
|
|
32375
32375
|
name: "@hasna/skills",
|
|
32376
|
-
version: "0.10.
|
|
32376
|
+
version: "0.10.34",
|
|
32377
32377
|
description: "Skills library for AI coding agents",
|
|
32378
32378
|
type: "module",
|
|
32379
32379
|
bin: {
|
|
@@ -33976,7 +33976,7 @@ import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
|
|
|
33976
33976
|
// src/lib/selection-cache.ts
|
|
33977
33977
|
import { createHash as createHash3, randomUUID } from "crypto";
|
|
33978
33978
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
33979
|
-
import { closeSync as closeSync5, constants as constants2, fstatSync as fstatSync5, fsyncSync, lstatSync as lstatSync3, mkdirSync as mkdirSync2, openSync as openSync5, readFileSync as readFileSync6, readlinkSync, readSync as readSync2, renameSync, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
|
|
33979
|
+
import { closeSync as closeSync5, constants as constants2, fstatSync as fstatSync5, fsyncSync, linkSync, lstatSync as lstatSync3, mkdirSync as mkdirSync2, openSync as openSync5, readFileSync as readFileSync6, readlinkSync, readSync as readSync2, renameSync, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
|
|
33980
33980
|
import { hostname } from "os";
|
|
33981
33981
|
import { dirname as dirname3, join as join7, parse, resolve as resolve2 } from "path";
|
|
33982
33982
|
import { isDeepStrictEqual } from "util";
|
|
@@ -35259,7 +35259,7 @@ function readSessionLock(sessionId, options) {
|
|
|
35259
35259
|
const before = lstatSync3(path, { throwIfNoEntry: false });
|
|
35260
35260
|
if (!before)
|
|
35261
35261
|
return null;
|
|
35262
|
-
if (!before.isFile() || before.nlink
|
|
35262
|
+
if (!before.isFile() || ![1, 2].includes(before.nlink) || (before.mode & 63) !== 0 || process.getuid && before.uid !== process.getuid()) {
|
|
35263
35263
|
throw new SkillSelectionError("SESSION_WRITE_LOCKED", "The session lock is not a private regular file; recovery requires review.");
|
|
35264
35264
|
}
|
|
35265
35265
|
const bytes = readRegularFile(path, SESSION_LOCK_MAX_BYTES);
|
|
@@ -35273,10 +35273,54 @@ function readSessionLock(sessionId, options) {
|
|
|
35273
35273
|
} catch {
|
|
35274
35274
|
throw new SkillSelectionError("SESSION_WRITE_LOCKED", "The session lock is unreadable; recovery requires review.");
|
|
35275
35275
|
}
|
|
35276
|
-
if (!marker || ![1, 2].includes(marker.schemaVersion) || marker.sessionId !== sessionId || !/^[a-f0-9-]{36}$/i.test(marker.operationId) || !Number.isSafeInteger(marker.pid) || marker.pid <= 0 || marker.schemaVersion === 2 && (!marker.host || !marker.bootId || !marker.processStart || process.platform === "linux" && !marker.pidNamespace)) {
|
|
35276
|
+
if (!marker || ![1, 2].includes(marker.schemaVersion) || marker.sessionId !== sessionId || !/^[a-f0-9-]{36}$/i.test(marker.operationId) || !Number.isSafeInteger(marker.pid) || marker.pid <= 0 || marker.publication !== undefined && marker.publication !== "hard-link-v1" || marker.schemaVersion === 2 && (!marker.host || !marker.bootId || !marker.processStart || process.platform === "linux" && !marker.pidNamespace)) {
|
|
35277
35277
|
throw new SkillSelectionError("SESSION_WRITE_LOCKED", "The session lock has invalid ownership metadata; recovery requires review.");
|
|
35278
35278
|
}
|
|
35279
|
-
|
|
35279
|
+
let stagingPath;
|
|
35280
|
+
if (after.nlink === 2) {
|
|
35281
|
+
stagingPath = `${path}.${marker.operationId}.prepared`;
|
|
35282
|
+
assertRegularPath(stagingPath);
|
|
35283
|
+
const staged = lstatSync3(stagingPath, { throwIfNoEntry: false });
|
|
35284
|
+
if (marker.publication !== "hard-link-v1" || !staged?.isFile() || staged.dev !== after.dev || staged.ino !== after.ino || staged.nlink !== 2 || staged.mode !== after.mode || staged.uid !== after.uid || staged.gid !== after.gid) {
|
|
35285
|
+
throw new SkillSelectionError("SESSION_WRITE_LOCKED", "The session lock has an unbound publication link; recovery requires review.");
|
|
35286
|
+
}
|
|
35287
|
+
}
|
|
35288
|
+
return { path, bytes, marker, sha256: sha256Hex(bytes), dev: after.dev, ino: after.ino, mtimeMs: after.mtimeMs, stagingPath };
|
|
35289
|
+
}
|
|
35290
|
+
function publishSessionLock(path, marker) {
|
|
35291
|
+
const stagingPath = `${path}.${marker.operationId}.prepared`;
|
|
35292
|
+
const bytes = Buffer.from(`${JSON.stringify({ ...marker, publication: "hard-link-v1" })}
|
|
35293
|
+
`);
|
|
35294
|
+
const fd = openSync5(stagingPath, constants2.O_WRONLY | constants2.O_CREAT | constants2.O_EXCL | constants2.O_NOFOLLOW, 384);
|
|
35295
|
+
const stat = fstatSync5(fd);
|
|
35296
|
+
let published = false, handedOff = false;
|
|
35297
|
+
try {
|
|
35298
|
+
writeFileSync2(fd, bytes);
|
|
35299
|
+
fsyncSync(fd);
|
|
35300
|
+
linkSync(stagingPath, path);
|
|
35301
|
+
published = true;
|
|
35302
|
+
syncDirectory(dirname3(path));
|
|
35303
|
+
const staged = lstatSync3(stagingPath);
|
|
35304
|
+
if (!staged.isFile() || staged.dev !== stat.dev || staged.ino !== stat.ino) {
|
|
35305
|
+
throw new SkillSelectionError("SESSION_WRITE_LOCK_CHANGED", "The initialized lock staging inode changed before publication completed.");
|
|
35306
|
+
}
|
|
35307
|
+
unlinkSync(stagingPath);
|
|
35308
|
+
syncDirectory(dirname3(path));
|
|
35309
|
+
handedOff = true;
|
|
35310
|
+
return { path, stagingPath, fd, dev: stat.dev, ino: stat.ino };
|
|
35311
|
+
} finally {
|
|
35312
|
+
if (!handedOff) {
|
|
35313
|
+
try {
|
|
35314
|
+
for (const target of [...published ? [path] : [], stagingPath]) {
|
|
35315
|
+
const current = lstatSync3(target, { throwIfNoEntry: false });
|
|
35316
|
+
if (current?.isFile() && current.dev === stat.dev && current.ino === stat.ino)
|
|
35317
|
+
unlinkSync(target);
|
|
35318
|
+
}
|
|
35319
|
+
} finally {
|
|
35320
|
+
closeSync5(fd);
|
|
35321
|
+
}
|
|
35322
|
+
}
|
|
35323
|
+
}
|
|
35280
35324
|
}
|
|
35281
35325
|
function writerIsGone(lock, explicitLocalHost) {
|
|
35282
35326
|
if (Date.now() - lock.mtimeMs < SESSION_LOCK_MIN_AGE_MS)
|
|
@@ -35338,10 +35382,21 @@ function archiveSessionLock(lock, receipt, explicitLocalHost) {
|
|
|
35338
35382
|
try {
|
|
35339
35383
|
renameSync(lock.path, archivePath);
|
|
35340
35384
|
moved = true;
|
|
35385
|
+
const archivedPaths = [archivePath];
|
|
35386
|
+
if (finalLock.stagingPath) {
|
|
35387
|
+
const publicationPath = join7(directory, "publication.write-lock");
|
|
35388
|
+
renameSync(finalLock.stagingPath, publicationPath);
|
|
35389
|
+
archivedPaths.push(publicationPath);
|
|
35390
|
+
}
|
|
35341
35391
|
syncDirectory(directory);
|
|
35342
35392
|
syncDirectory(dirname3(lock.path));
|
|
35343
|
-
|
|
35344
|
-
|
|
35393
|
+
for (const path of archivedPaths) {
|
|
35394
|
+
const before = lstatSync3(path, { throwIfNoEntry: false });
|
|
35395
|
+
const bytes = readRegularFile(path, SESSION_LOCK_MAX_BYTES);
|
|
35396
|
+
const after = lstatSync3(path, { throwIfNoEntry: false });
|
|
35397
|
+
if (!before?.isFile() || before.dev !== lock.dev || before.ino !== lock.ino || before.nlink !== archivedPaths.length || before.mtimeMs !== lock.mtimeMs || (before.mode & 63) !== 0 || process.getuid && before.uid !== process.getuid() || !bytes || sha256Hex(bytes) !== lock.sha256 || !after?.isFile() || before.dev !== after.dev || before.ino !== after.ino || before.mtimeMs !== after.mtimeMs || before.mode !== after.mode || before.uid !== after.uid || before.gid !== after.gid || before.nlink !== after.nlink) {
|
|
35398
|
+
throw new Error("Archived lock ownership or bytes changed");
|
|
35399
|
+
}
|
|
35345
35400
|
}
|
|
35346
35401
|
} catch (error) {
|
|
35347
35402
|
if (moved)
|
|
@@ -35358,6 +35413,7 @@ function archiveSessionLock(lock, receipt, explicitLocalHost) {
|
|
|
35358
35413
|
}
|
|
35359
35414
|
function withSessionWriteLocks(sessionIds, options, action) {
|
|
35360
35415
|
const operationId = randomUUID();
|
|
35416
|
+
const identity = localLockIdentity();
|
|
35361
35417
|
const locks = [...new Set(sessionIds)].map((sessionId) => ({ sessionId, path: `${sessionReceiptPath(sessionId, options)}.write-lock` })).sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
35362
35418
|
const owned = [];
|
|
35363
35419
|
const assertOwned = () => {
|
|
@@ -35371,9 +35427,10 @@ function withSessionWriteLocks(sessionIds, options, action) {
|
|
|
35371
35427
|
try {
|
|
35372
35428
|
for (const lock of locks) {
|
|
35373
35429
|
assertRegularPath(lock.path, true);
|
|
35374
|
-
|
|
35430
|
+
const marker = { schemaVersion: identity ? 2 : 1, operationId, sessionId: lock.sessionId, pid: process.pid, ...identity };
|
|
35431
|
+
let acquired;
|
|
35375
35432
|
try {
|
|
35376
|
-
|
|
35433
|
+
acquired = publishSessionLock(lock.path, marker);
|
|
35377
35434
|
} catch (error) {
|
|
35378
35435
|
if (error.code !== "EEXIST")
|
|
35379
35436
|
throw error;
|
|
@@ -35388,7 +35445,7 @@ function withSessionWriteLocks(sessionIds, options, action) {
|
|
|
35388
35445
|
throw recoveryError;
|
|
35389
35446
|
}
|
|
35390
35447
|
try {
|
|
35391
|
-
|
|
35448
|
+
acquired = publishSessionLock(lock.path, marker);
|
|
35392
35449
|
} catch (retryError) {
|
|
35393
35450
|
if (retryError.code === "EEXIST") {
|
|
35394
35451
|
if (!readSessionLock(lock.sessionId, options))
|
|
@@ -35398,19 +35455,20 @@ function withSessionWriteLocks(sessionIds, options, action) {
|
|
|
35398
35455
|
throw retryError;
|
|
35399
35456
|
}
|
|
35400
35457
|
}
|
|
35401
|
-
|
|
35402
|
-
owned.push({ path: lock.path, fd, dev: stat.dev, ino: stat.ino });
|
|
35403
|
-
const identity = localLockIdentity();
|
|
35404
|
-
writeFileSync2(fd, `${JSON.stringify({ schemaVersion: identity ? 2 : 1, operationId, sessionId: lock.sessionId, pid: process.pid, ...identity })}
|
|
35405
|
-
`);
|
|
35458
|
+
owned.push(acquired);
|
|
35406
35459
|
}
|
|
35407
35460
|
return action(assertOwned);
|
|
35408
35461
|
} finally {
|
|
35409
35462
|
for (const lock of owned.reverse()) {
|
|
35410
|
-
|
|
35411
|
-
|
|
35412
|
-
|
|
35413
|
-
|
|
35463
|
+
try {
|
|
35464
|
+
for (const path of [lock.path, lock.stagingPath]) {
|
|
35465
|
+
const current = lstatSync3(path, { throwIfNoEntry: false });
|
|
35466
|
+
if (current?.isFile() && current.dev === lock.dev && current.ino === lock.ino)
|
|
35467
|
+
unlinkSync(path);
|
|
35468
|
+
}
|
|
35469
|
+
} finally {
|
|
35470
|
+
closeSync5(lock.fd);
|
|
35471
|
+
}
|
|
35414
35472
|
}
|
|
35415
35473
|
}
|
|
35416
35474
|
}
|
|
@@ -39115,7 +39173,7 @@ var NATIVE_SKILL_ROOTS = [
|
|
|
39115
39173
|
// src/lib/agent-integration.ts
|
|
39116
39174
|
import { createHash as createHash18, randomUUID as randomUUID3 } from "crypto";
|
|
39117
39175
|
import { isDeepStrictEqual as isDeepStrictEqual6 } from "util";
|
|
39118
|
-
import { existsSync as existsSync7, lstatSync as lstatSync20, statSync as statSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync14, readdirSync as readdirSync8, opendirSync as opendirSync5, readlinkSync as readlinkSync4, realpathSync as realpathSync8, renameSync as renameSync3, rmdirSync as rmdirSync3, writeFileSync as writeFileSync4, unlinkSync as unlinkSync4, chmodSync, openSync as openSync16, closeSync as closeSync16, fsyncSync as fsyncSync4, fstatSync as fstatSync16, readSync as readSync11, constants as constants12, linkSync as
|
|
39176
|
+
import { existsSync as existsSync7, lstatSync as lstatSync20, statSync as statSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync14, readdirSync as readdirSync8, opendirSync as opendirSync5, readlinkSync as readlinkSync4, realpathSync as realpathSync8, renameSync as renameSync3, rmdirSync as rmdirSync3, writeFileSync as writeFileSync4, unlinkSync as unlinkSync4, chmodSync, openSync as openSync16, closeSync as closeSync16, fsyncSync as fsyncSync4, fstatSync as fstatSync16, readSync as readSync11, constants as constants12, linkSync as linkSync3 } from "fs";
|
|
39119
39177
|
import { dirname as dirname15, join as join23, resolve as resolve16, relative as relative4, isAbsolute as isAbsolute17, sep as sep5 } from "path";
|
|
39120
39178
|
import { homedir as homedir3 } from "os";
|
|
39121
39179
|
|
|
@@ -40116,7 +40174,7 @@ function buildPluginProjection(entries) {
|
|
|
40116
40174
|
|
|
40117
40175
|
// src/lib/plugin-projection-store.ts
|
|
40118
40176
|
import { createHash as createHash13, randomUUID as randomUUID2 } from "crypto";
|
|
40119
|
-
import { constants as constants8, closeSync as closeSync12, fchmodSync, fstatSync as fstatSync12, fsyncSync as fsyncSync2, linkSync, lstatSync as lstatSync13, mkdirSync as mkdirSync4, openSync as openSync12, opendirSync as opendirSync3, readSync as readSync7, renameSync as renameSync2, rmdirSync as rmdirSync2, rmSync, unlinkSync as unlinkSync3, writeSync as writeSync2 } from "fs";
|
|
40177
|
+
import { constants as constants8, closeSync as closeSync12, fchmodSync, fstatSync as fstatSync12, fsyncSync as fsyncSync2, linkSync as linkSync2, lstatSync as lstatSync13, mkdirSync as mkdirSync4, openSync as openSync12, opendirSync as opendirSync3, readSync as readSync7, renameSync as renameSync2, rmdirSync as rmdirSync2, rmSync, unlinkSync as unlinkSync3, writeSync as writeSync2 } from "fs";
|
|
40120
40178
|
import { dirname as dirname10, isAbsolute as isAbsolute11, join as join16, parse as parse5, resolve as resolve10 } from "path";
|
|
40121
40179
|
var READ_FLAGS = constants8.O_RDONLY | constants8.O_NOFOLLOW | constants8.O_NONBLOCK;
|
|
40122
40180
|
var WRITE_FLAGS = constants8.O_WRONLY | constants8.O_CREAT | constants8.O_EXCL | constants8.O_NOFOLLOW | constants8.O_NONBLOCK;
|
|
@@ -40432,7 +40490,7 @@ function writePluginJsonImmutable(path, value) {
|
|
|
40432
40490
|
written = stat(temporary);
|
|
40433
40491
|
verifyAncestors(chain, true);
|
|
40434
40492
|
try {
|
|
40435
|
-
|
|
40493
|
+
linkSync2(temporary, path);
|
|
40436
40494
|
} catch (error) {
|
|
40437
40495
|
if (!(error && typeof error === "object" && ("code" in error) && error.code === "EEXIST"))
|
|
40438
40496
|
throw error;
|
|
@@ -42076,13 +42134,13 @@ function resolveAgentDiscovery(options) {
|
|
|
42076
42134
|
unresolved("JSONC configuration requires a supported format adapter");
|
|
42077
42135
|
for (const root2 of [directory, join22(home, ".claude"), join22(home, ".agents")])
|
|
42078
42136
|
for (const name of ["skill", "skills"])
|
|
42079
|
-
roots.add(join22(root2, name));
|
|
42137
|
+
roots.add(canonical6(join22(root2, name)));
|
|
42080
42138
|
if (config.skills !== undefined && (!Array.isArray(config.skills) || config.skills.some((path) => typeof path !== "string")))
|
|
42081
42139
|
unresolved("skills must be a string array");
|
|
42082
42140
|
for (const value of config.skills ?? []) {
|
|
42083
42141
|
if (typeof value !== "string" || /[\0$]/.test(value) || !value || !(isAbsolute16(value) || value.startsWith("~/")))
|
|
42084
42142
|
unresolved("remote or relative skill sources need a dedicated discovery adapter");
|
|
42085
|
-
roots.add(value.startsWith("~/") ? join22(home, value.slice(2)) : value);
|
|
42143
|
+
roots.add(canonical6(value.startsWith("~/") ? join22(home, value.slice(2)) : value));
|
|
42086
42144
|
}
|
|
42087
42145
|
for (const key of ["plugin", "plugins"])
|
|
42088
42146
|
if (config[key] !== undefined && !Array.isArray(config[key]))
|
|
@@ -43720,7 +43778,7 @@ function archiveNativeSkills(inventory, options) {
|
|
|
43720
43778
|
continue;
|
|
43721
43779
|
}
|
|
43722
43780
|
if (entry.discoveryOnly) {
|
|
43723
|
-
|
|
43781
|
+
linkSync3(entry.archive, entry.source);
|
|
43724
43782
|
syncArchiveDirectory(dirname15(entry.source));
|
|
43725
43783
|
unlinkSync4(entry.archive);
|
|
43726
43784
|
} else {
|