@hasna/skills 0.5.3 → 0.5.4
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/bin/index.js +525 -585
- package/bin/mcp.js +376 -323
- package/bin/migrate.js +1 -164
- package/bin/server.js +58 -220
- package/bin/worker.js +97 -258
- package/dist/admin-contract.js +168 -166
- package/dist/index.d.ts +1 -1
- package/dist/index.js +448 -365
- package/dist/lib/remote-client.d.ts +7 -0
- package/dist/lib/remote-quote-errors.d.ts +12 -0
- package/dist/sdk/index.d.ts +2 -1
- package/dist/sdk/index.js +401 -301
- package/dist/storage.js +35 -33
- package/package.json +1 -1
package/bin/migrate.js
CHANGED
|
@@ -7,7 +7,7 @@ import { join as join6 } from "path";
|
|
|
7
7
|
// package.json
|
|
8
8
|
var package_default = {
|
|
9
9
|
name: "@hasna/skills",
|
|
10
|
-
version: "0.5.
|
|
10
|
+
version: "0.5.4",
|
|
11
11
|
description: "Skills library for AI coding agents",
|
|
12
12
|
type: "module",
|
|
13
13
|
bin: {
|
|
@@ -461,40 +461,7 @@ function publicPrincipal(partial = {}) {
|
|
|
461
461
|
// src/server/rows.ts
|
|
462
462
|
import { randomUUID } from "crypto";
|
|
463
463
|
|
|
464
|
-
// src/lib/skill-entry-path.ts
|
|
465
|
-
class SkillEntryPaths {
|
|
466
|
-
files = new Set;
|
|
467
|
-
directories = new Set;
|
|
468
|
-
add(path, maxBytes, invalid, limit) {
|
|
469
|
-
if (path.length > maxBytes)
|
|
470
|
-
limit();
|
|
471
|
-
const encoded = new TextEncoder().encode(path);
|
|
472
|
-
if (encoded.byteLength > maxBytes)
|
|
473
|
-
limit();
|
|
474
|
-
if (new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(encoded) !== path)
|
|
475
|
-
invalid("Invalid UTF-8 entry path");
|
|
476
|
-
if (!path || /[\\:\x00-\x1f\x7f]/u.test(path))
|
|
477
|
-
invalid("Unsafe entry path");
|
|
478
|
-
if (path.split("/").some((segment) => !segment || segment === "." || segment === ".."))
|
|
479
|
-
invalid("Unsafe entry path segment");
|
|
480
|
-
const key = path.normalize("NFC").toLowerCase().normalize("NFC");
|
|
481
|
-
if (this.files.has(key) || this.directories.has(key))
|
|
482
|
-
invalid("Duplicate or conflicting entry path");
|
|
483
|
-
const parents = key.split("/");
|
|
484
|
-
parents.pop();
|
|
485
|
-
while (parents.length) {
|
|
486
|
-
const parent = parents.join("/");
|
|
487
|
-
if (this.files.has(parent))
|
|
488
|
-
invalid("Conflicting entry file ancestor");
|
|
489
|
-
this.directories.add(parent);
|
|
490
|
-
parents.pop();
|
|
491
|
-
}
|
|
492
|
-
this.files.add(key);
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
|
|
496
464
|
// src/lib/skill-bundle.ts
|
|
497
|
-
var BLOCK = 512;
|
|
498
465
|
var ANY_SEGMENT_EXCLUDES = new Set([
|
|
499
466
|
".git",
|
|
500
467
|
".ds_store",
|
|
@@ -631,136 +598,6 @@ var SKILL_BUNDLE_INSPECTION_LIMITS = Object.freeze({
|
|
|
631
598
|
timeoutMs: 5000
|
|
632
599
|
});
|
|
633
600
|
|
|
634
|
-
class SkillBundleInspectionError extends Error {
|
|
635
|
-
code;
|
|
636
|
-
constructor(code, message) {
|
|
637
|
-
super(message);
|
|
638
|
-
this.code = code;
|
|
639
|
-
this.name = "SkillBundleInspectionError";
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
function invalidBundle(message) {
|
|
643
|
-
throw new SkillBundleInspectionError("BUNDLE_INVALID", message);
|
|
644
|
-
}
|
|
645
|
-
class BoundedTarReader {
|
|
646
|
-
limits;
|
|
647
|
-
check;
|
|
648
|
-
header = new Uint8Array(BLOCK);
|
|
649
|
-
headerOffset = 0;
|
|
650
|
-
pending;
|
|
651
|
-
bodyOffset = 0;
|
|
652
|
-
padding = 0;
|
|
653
|
-
zeroBlocks = 0;
|
|
654
|
-
entries = [];
|
|
655
|
-
paths = new SkillEntryPaths;
|
|
656
|
-
fileBytes = 0;
|
|
657
|
-
constructor(limits, check) {
|
|
658
|
-
this.limits = limits;
|
|
659
|
-
this.check = check;
|
|
660
|
-
}
|
|
661
|
-
push(chunk) {
|
|
662
|
-
let offset = 0;
|
|
663
|
-
while (offset < chunk.byteLength) {
|
|
664
|
-
this.check();
|
|
665
|
-
if (this.pending) {
|
|
666
|
-
const count = Math.min(this.pending.bytes.byteLength - this.bodyOffset, chunk.byteLength - offset);
|
|
667
|
-
this.pending.bytes.set(chunk.subarray(offset, offset + count), this.bodyOffset);
|
|
668
|
-
offset += count;
|
|
669
|
-
this.bodyOffset += count;
|
|
670
|
-
if (this.bodyOffset === this.pending.bytes.byteLength) {
|
|
671
|
-
this.entries.push(this.pending);
|
|
672
|
-
this.pending = undefined;
|
|
673
|
-
}
|
|
674
|
-
} else if (this.padding) {
|
|
675
|
-
const count = Math.min(this.padding, chunk.byteLength - offset);
|
|
676
|
-
if (chunk.subarray(offset, offset + count).some((byte) => byte !== 0))
|
|
677
|
-
invalidBundle("Nonzero tar body padding");
|
|
678
|
-
offset += count;
|
|
679
|
-
this.padding -= count;
|
|
680
|
-
} else {
|
|
681
|
-
const count = Math.min(BLOCK - this.headerOffset, chunk.byteLength - offset);
|
|
682
|
-
this.header.set(chunk.subarray(offset, offset + count), this.headerOffset);
|
|
683
|
-
offset += count;
|
|
684
|
-
this.headerOffset += count;
|
|
685
|
-
if (this.headerOffset === BLOCK) {
|
|
686
|
-
this.readHeader();
|
|
687
|
-
this.headerOffset = 0;
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
finish() {
|
|
693
|
-
this.check();
|
|
694
|
-
if (this.pending || this.padding || this.headerOffset || this.zeroBlocks < 2)
|
|
695
|
-
invalidBundle("Truncated tar bundle");
|
|
696
|
-
return this.entries;
|
|
697
|
-
}
|
|
698
|
-
readHeader() {
|
|
699
|
-
this.check();
|
|
700
|
-
const h = this.header;
|
|
701
|
-
if (h.every((byte) => byte === 0)) {
|
|
702
|
-
this.zeroBlocks++;
|
|
703
|
-
return;
|
|
704
|
-
}
|
|
705
|
-
if (this.zeroBlocks)
|
|
706
|
-
invalidBundle("Nonzero tar data after terminator");
|
|
707
|
-
if (this.entries.length >= this.limits.entries)
|
|
708
|
-
throw new SkillBundleInspectionError("BUNDLE_LIMIT", "Bundle entry limit exceeded");
|
|
709
|
-
let checksum = 0;
|
|
710
|
-
for (let i = 0;i < BLOCK; i++)
|
|
711
|
-
checksum += i >= 148 && i < 156 ? 32 : h[i];
|
|
712
|
-
if (tarOctal(h.subarray(148, 156)) !== checksum)
|
|
713
|
-
invalidBundle("Invalid tar header checksum");
|
|
714
|
-
if (new TextDecoder().decode(h.subarray(257, 265)) !== "ustar\x00" + "00")
|
|
715
|
-
invalidBundle("Unsupported tar format");
|
|
716
|
-
if (h[156] !== 48 && h[156] !== 0 || h.subarray(157, 257).some((b) => b !== 0) || h.subarray(345).some((b) => b !== 0))
|
|
717
|
-
invalidBundle("Unsupported tar entry or path prefix");
|
|
718
|
-
const mode = tarOctal(h.subarray(100, 108));
|
|
719
|
-
if (mode > 511)
|
|
720
|
-
invalidBundle("Unsupported tar permission bits");
|
|
721
|
-
tarOctal(h.subarray(108, 116));
|
|
722
|
-
tarOctal(h.subarray(116, 124));
|
|
723
|
-
tarOctal(h.subarray(136, 148));
|
|
724
|
-
const size = tarOctal(h.subarray(124, 136));
|
|
725
|
-
if (size > this.limits.fileBytes || this.fileBytes + size > this.limits.decompressedBytes) {
|
|
726
|
-
throw new SkillBundleInspectionError("BUNDLE_LIMIT", "Bundle file byte limit exceeded");
|
|
727
|
-
}
|
|
728
|
-
const name = h.subarray(0, 100);
|
|
729
|
-
const end = name.indexOf(0);
|
|
730
|
-
if (end !== -1 && name.subarray(end).some((b) => b !== 0))
|
|
731
|
-
invalidBundle("Invalid tar path padding");
|
|
732
|
-
const raw = end === -1 ? name : name.subarray(0, end);
|
|
733
|
-
if (raw.byteLength > this.limits.pathBytes)
|
|
734
|
-
throw new SkillBundleInspectionError("BUNDLE_LIMIT", "Bundle path byte limit exceeded");
|
|
735
|
-
let path;
|
|
736
|
-
try {
|
|
737
|
-
path = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(raw);
|
|
738
|
-
} catch {
|
|
739
|
-
return invalidBundle("Invalid UTF-8 bundle path");
|
|
740
|
-
}
|
|
741
|
-
this.paths.add(path, this.limits.pathBytes, invalidBundle, () => {
|
|
742
|
-
throw new SkillBundleInspectionError("BUNDLE_LIMIT", "Bundle path byte limit exceeded");
|
|
743
|
-
});
|
|
744
|
-
this.fileBytes += size;
|
|
745
|
-
this.pending = { path, mode, bytes: new Uint8Array(new ArrayBuffer(size)) };
|
|
746
|
-
this.bodyOffset = 0;
|
|
747
|
-
this.padding = (BLOCK - size % BLOCK) % BLOCK;
|
|
748
|
-
if (!size) {
|
|
749
|
-
this.entries.push(this.pending);
|
|
750
|
-
this.pending = undefined;
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
function tarOctal(field) {
|
|
755
|
-
const text = new TextDecoder().decode(field);
|
|
756
|
-
if (!/^[0-7]+[\0 ]*$/.test(text))
|
|
757
|
-
invalidBundle("Invalid tar octal field");
|
|
758
|
-
const value = Number.parseInt(text, 8);
|
|
759
|
-
if (!Number.isSafeInteger(value))
|
|
760
|
-
invalidBundle("Tar integer is out of range");
|
|
761
|
-
return value;
|
|
762
|
-
}
|
|
763
|
-
|
|
764
601
|
// src/server/rows.ts
|
|
765
602
|
function nowIso() {
|
|
766
603
|
return new Date().toISOString();
|