yeka-skills 0.1.1 → 0.2.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.
Files changed (3) hide show
  1. package/README.md +27 -2
  2. package/dist/cli.js +888 -61
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # yeka-skills
2
2
 
3
- Install and update private Gemography agent skills from Yeka Skills.
3
+ Install, update, and remove private Gemography agent skills from Yeka Skills.
4
4
 
5
5
  ```bash
6
6
  npx yeka-skills add ste100-session
@@ -20,13 +20,38 @@ Commands:
20
20
  ```text
21
21
  yeka-skills add <skill>
22
22
  yeka-skills list
23
+ yeka-skills share <skill>
23
24
  yeka-skills status
24
25
  yeka-skills update [skill]
26
+ yeka-skills remove <skill>
25
27
  yeka-skills login
26
28
  yeka-skills logout
27
29
  ```
28
30
 
29
- Use `--dry-run` with `add` or `update` to verify and report changes without installation.
31
+ ## Share a local skill with the team
32
+
33
+ `yeka-skills share <skill>` packages one of your local skill folders and sends it to the private
34
+ submission inbox, where a review bot turns it into a team-repo pull request. Nothing is published
35
+ by a share — a person reviews every submission first.
36
+
37
+ - The CLI looks for the folder in the same local roots the installer knows. If a skill exists in
38
+ several of them, it picks the one it would install into first (the current Codex folder, then
39
+ the Claude folder, then the legacy Codex folder) and tells you which it chose.
40
+ - `yeka-skills share <skill> --check` looks the folder over without network access or a session:
41
+ SKILL.md present with a matching `name`, folder under the 5 MB share limit, and plain-language
42
+ warnings about anything reviewers will look at (web addresses, credentials paths, `curl | sh`,
43
+ long base64 tokens). Warnings never block a share — the team's server-side checks decide.
44
+ - `--notes-file <path>` attaches a small UTF-8 text file (up to 8 KB) with your interview answers.
45
+ The file rides inside the archive as `_yeka/notes.txt`, always the first entry, namespaced so
46
+ the review bot can find it, read it, and strip it before the skill content is unpacked. Do not
47
+ create a `_yeka` folder inside your skill yourself.
48
+ - A successful share answers with one sentence: you'll hear back in #yeka-skills, and updates
49
+ usually go live within the hour.
50
+
51
+ Use `--dry-run` with `add`, `update`, or `remove` to verify and report changes without mutating the
52
+ managed installation. Removal is fully offline and uses local receipts. It refuses locally edited
53
+ copies unless `--force` is present; forced removal moves edited content into a private backup and
54
+ prints its path.
30
55
 
31
56
  Managed installation receipts and backups stay in `~/Library/Application Support/Yeka Skills/`.
32
57
 
package/dist/cli.js CHANGED
@@ -4,9 +4,9 @@
4
4
  import { Command } from "commander";
5
5
 
6
6
  // src/commands.ts
7
- import { chmod as chmod2, lstat as lstat6, mkdir as mkdir4, mkdtemp as mkdtemp2, rm as rm3 } from "fs/promises";
8
- import os6 from "os";
9
- import path7 from "path";
7
+ import { chmod as chmod3, lstat as lstat9, mkdir as mkdir6, mkdtemp as mkdtemp3, readFile as readFile4, rm as rm5 } from "fs/promises";
8
+ import os7 from "os";
9
+ import path9 from "path";
10
10
 
11
11
  // ../contracts/dist/index.js
12
12
  import { z } from "zod";
@@ -116,7 +116,7 @@ import { createHash as createHash3, randomUUID as randomUUID2 } from "crypto";
116
116
  import {
117
117
  chmod,
118
118
  cp,
119
- lstat as lstat3,
119
+ lstat as lstat4,
120
120
  mkdir as mkdir3,
121
121
  mkdtemp,
122
122
  rename as rename2,
@@ -412,11 +412,45 @@ var extractVerifiedArchive = async (archivePath, destination, expectedContentHas
412
412
 
413
413
  // src/receipts.ts
414
414
  import { createHash as createHash2, randomUUID } from "crypto";
415
- import { mkdir as mkdir2, readFile, readdir as readdir2, rename, unlink, writeFile } from "fs/promises";
415
+ import { lstat as lstat3, mkdir as mkdir2, readFile, readdir as readdir2, rename, unlink, writeFile } from "fs/promises";
416
416
  import os2 from "os";
417
417
  import path4 from "path";
418
418
  var rootIdentifier = (targetRoot) => createHash2("sha256").update(targetRoot).digest("hex").slice(0, 16);
419
419
  var receiptPath = (targetRoot, skillName, home = os2.homedir()) => path4.join(localPaths(home).receipts, rootIdentifier(targetRoot), `${skillName}.json`);
420
+ var receiptStoreExists = async (home, root) => {
421
+ const resolvedHome = path4.resolve(home);
422
+ const relativeRoot = path4.relative(resolvedHome, root);
423
+ if (relativeRoot.startsWith("..") || path4.isAbsolute(relativeRoot)) {
424
+ throw new CliError("unsafe_receipts", "The Yeka Skills receipt location is unsafe.");
425
+ }
426
+ let current = resolvedHome;
427
+ const homeEntry = await lstat3(current);
428
+ if (!homeEntry.isDirectory() || homeEntry.isSymbolicLink()) {
429
+ throw new CliError(
430
+ "unsafe_receipts",
431
+ `The Yeka Skills receipt location is not a real directory: ${current}.`
432
+ );
433
+ }
434
+ for (const segment of relativeRoot.split(path4.sep)) {
435
+ current = path4.join(current, segment);
436
+ let entry;
437
+ try {
438
+ entry = await lstat3(current);
439
+ } catch (error) {
440
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
441
+ return false;
442
+ }
443
+ throw error;
444
+ }
445
+ if (!entry.isDirectory() || entry.isSymbolicLink()) {
446
+ throw new CliError(
447
+ "unsafe_receipts",
448
+ `The Yeka Skills receipt location is not a real directory: ${current}.`
449
+ );
450
+ }
451
+ }
452
+ return true;
453
+ };
420
454
  var writeReceipt = async (receipt, home = os2.homedir()) => {
421
455
  const validated = InstallReceiptSchema.parse(receipt);
422
456
  const destination = receiptPath(receipt.targetRoot, receipt.skillName, home);
@@ -457,50 +491,76 @@ var readReceipt = async (targetRoot, skillName, home = os2.homedir()) => {
457
491
  });
458
492
  }
459
493
  };
460
- var listReceipts = async (home = os2.homedir()) => {
494
+ var scanReceiptRecords = async (home = os2.homedir()) => {
461
495
  const root = localPaths(home).receipts;
496
+ if (!await receiptStoreExists(home, root)) {
497
+ return { records: [], invalidFiles: [] };
498
+ }
462
499
  let rootEntries;
463
500
  try {
464
501
  rootEntries = await readdir2(root, { withFileTypes: true });
465
502
  } catch (error) {
466
503
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
467
- return [];
504
+ return { records: [], invalidFiles: [] };
468
505
  }
469
506
  throw error;
470
507
  }
471
- const receipts = [];
508
+ const records = [];
509
+ const invalidFiles = [];
472
510
  for (const rootEntry of rootEntries) {
473
511
  if (!rootEntry.isDirectory()) {
512
+ if (/^[0-9a-f]{16}$/.test(rootEntry.name)) {
513
+ invalidFiles.push(path4.join(root, rootEntry.name));
514
+ }
474
515
  continue;
475
516
  }
476
517
  const files = await readdir2(path4.join(root, rootEntry.name), { withFileTypes: true });
477
518
  for (const file of files) {
478
- if (!file.isFile() || !file.name.endsWith(".json")) {
519
+ if (!file.name.endsWith(".json")) {
520
+ continue;
521
+ }
522
+ const filePath = path4.join(root, rootEntry.name, file.name);
523
+ if (!file.isFile()) {
524
+ invalidFiles.push(filePath);
479
525
  continue;
480
526
  }
481
527
  try {
482
- receipts.push(
483
- InstallReceiptSchema.parse(
484
- JSON.parse(await readFile(path4.join(root, rootEntry.name, file.name), "utf8"))
485
- )
486
- );
487
- } catch (error) {
488
- throw new CliError("invalid_receipt", `The local receipt ${file.name} is invalid.`, {
489
- cause: error
528
+ const serialized = await readFile(filePath, "utf8");
529
+ records.push({
530
+ filePath,
531
+ contentHash: createHash2("sha256").update(serialized).digest("hex"),
532
+ receipt: InstallReceiptSchema.parse(JSON.parse(serialized))
490
533
  });
534
+ } catch {
535
+ invalidFiles.push(filePath);
491
536
  }
492
537
  }
493
538
  }
494
- return receipts.sort(
495
- (left, right) => `${left.skillName}:${left.runtime}`.localeCompare(`${right.skillName}:${right.runtime}`, "en")
539
+ records.sort(
540
+ (left, right) => `${left.receipt.skillName}:${left.receipt.runtime}:${left.filePath}`.localeCompare(
541
+ `${right.receipt.skillName}:${right.receipt.runtime}:${right.filePath}`,
542
+ "en"
543
+ )
496
544
  );
545
+ invalidFiles.sort((left, right) => left.localeCompare(right, "en"));
546
+ return { records, invalidFiles };
547
+ };
548
+ var listReceipts = async (home = os2.homedir()) => {
549
+ const scan = await scanReceiptRecords(home);
550
+ if (scan.invalidFiles.length > 0) {
551
+ throw new CliError(
552
+ "invalid_receipt",
553
+ `The local receipt file is invalid: ${scan.invalidFiles[0]}.`
554
+ );
555
+ }
556
+ return scan.records.map((record) => record.receipt);
497
557
  };
498
558
 
499
559
  // src/installer.ts
500
560
  var isNotFound = (error) => error instanceof Error && "code" in error && error.code === "ENOENT";
501
561
  var fingerprint = async (entryPath) => {
502
562
  try {
503
- const entry = await lstat3(entryPath, { bigint: true });
563
+ const entry = await lstat4(entryPath, { bigint: true });
504
564
  return {
505
565
  dev: entry.dev,
506
566
  ino: entry.ino,
@@ -525,7 +585,7 @@ var assertSafeTarget = async (target, skillName, home) => {
525
585
  const parent = path5.dirname(resolvedRoot);
526
586
  let parentStat;
527
587
  try {
528
- parentStat = await lstat3(parent);
588
+ parentStat = await lstat4(parent);
529
589
  } catch (error) {
530
590
  if (isNotFound(error)) {
531
591
  throw new CliError("runtime_not_found", `The ${target.runtime} configuration directory is missing.`);
@@ -537,7 +597,7 @@ var assertSafeTarget = async (target, skillName, home) => {
537
597
  }
538
598
  const rootFingerprint = await fingerprint(resolvedRoot);
539
599
  if (rootFingerprint !== null) {
540
- const rootStat = await lstat3(resolvedRoot);
600
+ const rootStat = await lstat4(resolvedRoot);
541
601
  if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) {
542
602
  throw new CliError("unsafe_target", `The ${target.runtime} skills directory is not a real directory.`);
543
603
  }
@@ -580,8 +640,8 @@ var preflightTarget = async (target, skill, home) => {
580
640
  if (oldReceipt !== null && (oldReceipt.runtime !== target.runtime || path5.resolve(oldReceipt.targetRoot) !== targetRoot || path5.resolve(oldReceipt.installedPath) !== installedPath)) {
581
641
  throw new CliError("invalid_receipt_target", `The local receipt for ${skill.name} has an invalid target.`);
582
642
  }
583
- const entryFingerprint = await fingerprint(installedPath);
584
- if (entryFingerprint === null) {
643
+ const entryFingerprint2 = await fingerprint(installedPath);
644
+ if (entryFingerprint2 === null) {
585
645
  return {
586
646
  action: "install",
587
647
  runtime: target.runtime,
@@ -598,11 +658,11 @@ var preflightTarget = async (target, skill, home) => {
598
658
  targetRoot,
599
659
  installedPath,
600
660
  existed: true,
601
- fingerprint: entryFingerprint,
661
+ fingerprint: entryFingerprint2,
602
662
  oldReceipt
603
663
  };
604
664
  }
605
- const installedStat = await lstat3(installedPath);
665
+ const installedStat = await lstat4(installedPath);
606
666
  if (!installedStat.isDirectory() || installedStat.isSymbolicLink()) {
607
667
  throw new CliError(
608
668
  "local_changes",
@@ -632,7 +692,7 @@ var preflightTarget = async (target, skill, home) => {
632
692
  targetRoot,
633
693
  installedPath,
634
694
  existed: true,
635
- fingerprint: entryFingerprint,
695
+ fingerprint: entryFingerprint2,
636
696
  oldReceipt
637
697
  };
638
698
  };
@@ -644,7 +704,7 @@ var ensureTargetRoot = async (targetRoot) => {
644
704
  throw error;
645
705
  }
646
706
  }
647
- const rootStat = await lstat3(targetRoot);
707
+ const rootStat = await lstat4(targetRoot);
648
708
  if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) {
649
709
  throw new CliError("unsafe_target", `The skills target is not a real directory: ${targetRoot}`);
650
710
  }
@@ -737,7 +797,7 @@ var installSkillFromArchive = async (options) => {
737
797
  }
738
798
  const operationCache = localPaths(home).cache;
739
799
  await mkdir3(operationCache, { recursive: true, mode: 448 });
740
- const cacheStat = await lstat3(operationCache);
800
+ const cacheStat = await lstat4(operationCache);
741
801
  if (!cacheStat.isDirectory() || cacheStat.isSymbolicLink()) {
742
802
  throw new CliError("unsafe_cache", "The Yeka Skills cache is not a real directory.");
743
803
  }
@@ -1038,14 +1098,14 @@ var deleteSession = async () => {
1038
1098
  };
1039
1099
 
1040
1100
  // src/local-status.ts
1041
- import { lstat as lstat4 } from "fs/promises";
1101
+ import { lstat as lstat5 } from "fs/promises";
1042
1102
  import os4 from "os";
1043
1103
  var isNotFound2 = (error) => error instanceof Error && "code" in error && error.code === "ENOENT";
1044
1104
  var inspectLocalStatus = async (receipt, remote, home = os4.homedir()) => {
1045
1105
  targetsFromReceipts([receipt], receipt.skillName, home);
1046
1106
  let installedStat;
1047
1107
  try {
1048
- installedStat = await lstat4(receipt.installedPath);
1108
+ installedStat = await lstat5(receipt.installedPath);
1049
1109
  } catch (error) {
1050
1110
  if (isNotFound2(error)) {
1051
1111
  return { kind: "missing", receipt, ...remote === void 0 ? {} : { remote } };
@@ -1249,8 +1309,346 @@ var login = async () => {
1249
1309
  }
1250
1310
  };
1251
1311
 
1312
+ // src/remover.ts
1313
+ import { createHash as createHash6, randomUUID as randomUUID3 } from "crypto";
1314
+ import { chmod as chmod2, lstat as lstat6, mkdir as mkdir4, readFile as readFile2, rename as rename3, rm as rm3 } from "fs/promises";
1315
+ import os5 from "os";
1316
+ import path6 from "path";
1317
+ var isNotFound3 = (error) => error instanceof Error && "code" in error && error.code === "ENOENT";
1318
+ var entryFingerprint = async (entryPath) => {
1319
+ try {
1320
+ const entry = await lstat6(entryPath, { bigint: true });
1321
+ return {
1322
+ dev: entry.dev,
1323
+ ino: entry.ino,
1324
+ mode: Number(entry.mode),
1325
+ mtimeNs: entry.mtimeNs,
1326
+ size: entry.size
1327
+ };
1328
+ } catch (error) {
1329
+ if (isNotFound3(error)) {
1330
+ return null;
1331
+ }
1332
+ throw error;
1333
+ }
1334
+ };
1335
+ var fingerprintsMatch2 = (left, right) => left !== null && right !== void 0 && left.dev === right.dev && left.ino === right.ino && left.mode === right.mode && left.mtimeNs === right.mtimeNs && left.size === right.size;
1336
+ var agentLabel = (runtime) => runtime === "codex" ? "Codex" : "Claude";
1337
+ var targetIdentifier = (targetRoot) => createHash6("sha256").update(targetRoot).digest("hex").slice(0, 16);
1338
+ var receiptIssue = (record, message) => `${record.filePath}: ${message}`;
1339
+ var assertRealDirectory = async (directory) => {
1340
+ const fingerprint2 = await entryFingerprint(directory);
1341
+ if (fingerprint2 === null) {
1342
+ return false;
1343
+ }
1344
+ const entry = await lstat6(directory);
1345
+ return entry.isDirectory() && !entry.isSymbolicLink();
1346
+ };
1347
+ var validateReceiptTarget = (record, skillName, home) => {
1348
+ const { receipt } = record;
1349
+ try {
1350
+ targetsFromReceipts([receipt], skillName, home);
1351
+ } catch {
1352
+ return receiptIssue(record, "the target is outside the supported local skill folders");
1353
+ }
1354
+ const canonicalRoot = path6.resolve(receipt.targetRoot);
1355
+ const canonicalInstalledPath = path6.join(canonicalRoot, skillName);
1356
+ if (receipt.targetRoot !== canonicalRoot || receipt.installedPath !== canonicalInstalledPath || path6.dirname(receipt.installedPath) !== canonicalRoot) {
1357
+ return receiptIssue(record, "the installed path is not the canonical path inside its target root");
1358
+ }
1359
+ if (receiptPath(receipt.targetRoot, receipt.skillName, home) !== record.filePath) {
1360
+ return receiptIssue(record, "the file is stored under the wrong target identifier");
1361
+ }
1362
+ return null;
1363
+ };
1364
+ var preflightRecord = async (record, skillName, home, force, batchRoot) => {
1365
+ const issues = [];
1366
+ const targetIssue = validateReceiptTarget(record, skillName, home);
1367
+ if (targetIssue !== null) {
1368
+ return { issues: [targetIssue] };
1369
+ }
1370
+ const receiptFingerprint = await entryFingerprint(record.filePath);
1371
+ if (receiptFingerprint === null) {
1372
+ return { issues: [receiptIssue(record, "the receipt disappeared during preflight")] };
1373
+ }
1374
+ const receiptStat = await lstat6(record.filePath);
1375
+ if (!receiptStat.isFile() || receiptStat.isSymbolicLink()) {
1376
+ return { issues: [receiptIssue(record, "the receipt is not a real file")] };
1377
+ }
1378
+ const { receipt } = record;
1379
+ const installedFingerprint = await entryFingerprint(receipt.installedPath);
1380
+ if (installedFingerprint === null) {
1381
+ return {
1382
+ target: {
1383
+ action: "clean-receipt",
1384
+ runtime: receipt.runtime,
1385
+ targetRoot: receipt.targetRoot,
1386
+ installedPath: receipt.installedPath,
1387
+ record,
1388
+ receiptFingerprint
1389
+ },
1390
+ issues
1391
+ };
1392
+ }
1393
+ if (!await assertRealDirectory(path6.dirname(receipt.targetRoot))) {
1394
+ issues.push(receiptIssue(record, "the agent configuration directory is not a real directory"));
1395
+ }
1396
+ if (!await assertRealDirectory(receipt.targetRoot)) {
1397
+ issues.push(receiptIssue(record, "the target root is not a real directory"));
1398
+ }
1399
+ const installedStat = await lstat6(receipt.installedPath);
1400
+ if (!installedStat.isDirectory() || installedStat.isSymbolicLink()) {
1401
+ issues.push(
1402
+ `${agentLabel(receipt.runtime)} copy at ${receipt.installedPath} is not a real directory.`
1403
+ );
1404
+ return { issues };
1405
+ }
1406
+ let installedHash;
1407
+ try {
1408
+ installedHash = (await hashDirectory(receipt.installedPath)).contentHash;
1409
+ } catch {
1410
+ issues.push(
1411
+ `${agentLabel(receipt.runtime)} copy at ${receipt.installedPath} contains unsupported local changes.`
1412
+ );
1413
+ return { issues };
1414
+ }
1415
+ const modified = installedHash !== receipt.installedHash;
1416
+ if (modified && !force) {
1417
+ issues.push(
1418
+ `${agentLabel(receipt.runtime)} copy at ${receipt.installedPath} was edited after installation; re-run with --force to preserve it in a private backup.`
1419
+ );
1420
+ }
1421
+ const backupPath = path6.join(
1422
+ batchRoot,
1423
+ "content",
1424
+ receipt.runtime,
1425
+ targetIdentifier(receipt.targetRoot),
1426
+ skillName
1427
+ );
1428
+ return {
1429
+ target: {
1430
+ action: modified ? "backup-modified" : "remove",
1431
+ runtime: receipt.runtime,
1432
+ targetRoot: receipt.targetRoot,
1433
+ installedPath: receipt.installedPath,
1434
+ backupPath,
1435
+ record,
1436
+ receiptFingerprint,
1437
+ installedFingerprint,
1438
+ installedHash
1439
+ },
1440
+ issues
1441
+ };
1442
+ };
1443
+ var assertInstalledUnchanged = async (target) => {
1444
+ const currentFingerprint = await entryFingerprint(target.installedPath);
1445
+ if (target.action === "clean-receipt") {
1446
+ if (currentFingerprint !== null) {
1447
+ throw new CliError(
1448
+ "target_changed",
1449
+ `The ${target.runtime} installation appeared during removal preflight.`
1450
+ );
1451
+ }
1452
+ return;
1453
+ }
1454
+ if (!fingerprintsMatch2(currentFingerprint, target.installedFingerprint)) {
1455
+ throw new CliError(
1456
+ "target_changed",
1457
+ `The ${target.runtime} installation changed during removal preflight.`
1458
+ );
1459
+ }
1460
+ const currentHash = (await hashDirectory(target.installedPath)).contentHash;
1461
+ if (currentHash !== target.installedHash) {
1462
+ throw new CliError(
1463
+ "target_changed",
1464
+ `The ${target.runtime} installation changed during removal preflight.`
1465
+ );
1466
+ }
1467
+ };
1468
+ var assertReceiptUnchanged = async (target) => {
1469
+ const currentFingerprint = await entryFingerprint(target.record.filePath);
1470
+ if (!fingerprintsMatch2(currentFingerprint, target.receiptFingerprint)) {
1471
+ throw new CliError(
1472
+ "receipt_changed",
1473
+ `The receipt changed during removal: ${target.record.filePath}.`
1474
+ );
1475
+ }
1476
+ const serialized = await readFile2(target.record.filePath, "utf8");
1477
+ const contentHash = createHash6("sha256").update(serialized).digest("hex");
1478
+ if (contentHash !== target.record.contentHash) {
1479
+ throw new CliError(
1480
+ "receipt_changed",
1481
+ `The receipt changed during removal: ${target.record.filePath}.`
1482
+ );
1483
+ }
1484
+ };
1485
+ var createPrivateBatchRoot = async (batchRoot, home) => {
1486
+ const backups = localPaths(home).backups;
1487
+ await mkdir4(backups, { recursive: true, mode: 448 });
1488
+ const backupsStat = await lstat6(backups);
1489
+ if (!backupsStat.isDirectory() || backupsStat.isSymbolicLink()) {
1490
+ throw new CliError("unsafe_backup", "The Yeka Skills backup location is not a real directory.");
1491
+ }
1492
+ await mkdir4(batchRoot, { mode: 448 });
1493
+ await chmod2(batchRoot, 448);
1494
+ };
1495
+ var moveToBatch = async (source, destination) => {
1496
+ await mkdir4(path6.dirname(destination), { recursive: true, mode: 448 });
1497
+ await rename3(source, destination);
1498
+ return { source, destination };
1499
+ };
1500
+ var rollbackMoves = async (moves) => {
1501
+ const failures = [];
1502
+ for (const move of [...moves].reverse()) {
1503
+ try {
1504
+ await rename3(move.destination, move.source);
1505
+ } catch (error) {
1506
+ failures.push(error instanceof Error ? error : new Error(String(error)));
1507
+ }
1508
+ }
1509
+ return failures;
1510
+ };
1511
+ var restoreReceipts2 = async (targets, home) => {
1512
+ const failures = [];
1513
+ for (const target of [...targets].reverse()) {
1514
+ try {
1515
+ await writeReceipt(target.record.receipt, home);
1516
+ } catch (error) {
1517
+ failures.push(error instanceof Error ? error : new Error(String(error)));
1518
+ }
1519
+ }
1520
+ return failures;
1521
+ };
1522
+ var publicActions = (targets) => targets.map(({ action, runtime, targetRoot, installedPath, backupPath }) => ({
1523
+ action,
1524
+ runtime,
1525
+ targetRoot,
1526
+ installedPath,
1527
+ ...backupPath === void 0 ? {} : { backupPath }
1528
+ }));
1529
+ var removableSkillNames = async (home = os5.homedir()) => {
1530
+ const scan = await scanReceiptRecords(home);
1531
+ return [...new Set(scan.records.map((record) => record.receipt.skillName))].sort(
1532
+ (left, right) => left.localeCompare(right, "en")
1533
+ );
1534
+ };
1535
+ var removeSkill = async (skillName, options = {}) => {
1536
+ const home = path6.resolve(options.home ?? os5.homedir());
1537
+ const now = options.now ?? /* @__PURE__ */ new Date();
1538
+ const timestamp = now.toISOString().replace(/[:.]/g, "-");
1539
+ const batchRoot = path6.join(
1540
+ localPaths(home).backups,
1541
+ `${timestamp}-remove-${randomUUID3()}`
1542
+ );
1543
+ const scan = await scanReceiptRecords(home);
1544
+ const matchingRecords = scan.records.filter((record) => record.receipt.skillName === skillName);
1545
+ if (matchingRecords.length === 0 && scan.invalidFiles.length === 0) {
1546
+ throw new CliError("not_installed", `${skillName} is not a managed installation.`);
1547
+ }
1548
+ const issues = scan.invalidFiles.map((filePath) => `${filePath}: the receipt is invalid or corrupt`);
1549
+ const targets = [];
1550
+ for (const record of matchingRecords) {
1551
+ try {
1552
+ const result = await preflightRecord(
1553
+ record,
1554
+ skillName,
1555
+ home,
1556
+ options.force === true,
1557
+ batchRoot
1558
+ );
1559
+ issues.push(...result.issues);
1560
+ if (result.target !== void 0) {
1561
+ targets.push(result.target);
1562
+ }
1563
+ } catch (error) {
1564
+ issues.push(
1565
+ receiptIssue(
1566
+ record,
1567
+ error instanceof Error ? error.message : "preflight could not inspect this target"
1568
+ )
1569
+ );
1570
+ }
1571
+ }
1572
+ const liveInstallsByRuntime = /* @__PURE__ */ new Map();
1573
+ for (const target of targets) {
1574
+ if (target.backupPath !== void 0) {
1575
+ const live = liveInstallsByRuntime.get(target.runtime) ?? [];
1576
+ live.push(target);
1577
+ liveInstallsByRuntime.set(target.runtime, live);
1578
+ }
1579
+ }
1580
+ for (const [runtime, liveTargets] of liveInstallsByRuntime) {
1581
+ if (liveTargets.length > 1) {
1582
+ for (const target of liveTargets) {
1583
+ issues.push(
1584
+ receiptIssue(
1585
+ target.record,
1586
+ `there is more than one ${runtime} receipt with an existing installation`
1587
+ )
1588
+ );
1589
+ }
1590
+ }
1591
+ }
1592
+ if (issues.length > 0) {
1593
+ throw new CliError(
1594
+ "remove_preflight_failed",
1595
+ `Nothing was removed because preflight found a problem:
1596
+ ${issues.map((issue) => `- ${issue}`).join("\n")}`
1597
+ );
1598
+ }
1599
+ if (targets.length === 0) {
1600
+ throw new CliError("not_installed", `${skillName} is not a managed installation.`);
1601
+ }
1602
+ const actions = publicActions(targets);
1603
+ if (options.dryRun === true) {
1604
+ return actions;
1605
+ }
1606
+ const hasDirectoryMoves = targets.some((target) => target.backupPath !== void 0);
1607
+ if (hasDirectoryMoves) {
1608
+ await createPrivateBatchRoot(batchRoot, home);
1609
+ }
1610
+ const directoryMoves = [];
1611
+ const deletedReceipts = [];
1612
+ try {
1613
+ for (const target of targets) {
1614
+ await assertInstalledUnchanged(target);
1615
+ if (target.backupPath !== void 0) {
1616
+ directoryMoves.push(await moveToBatch(target.installedPath, target.backupPath));
1617
+ }
1618
+ }
1619
+ for (const target of targets) {
1620
+ await assertReceiptUnchanged(target);
1621
+ await deleteReceipt(target.targetRoot, skillName, home);
1622
+ deletedReceipts.push(target);
1623
+ }
1624
+ } catch (error) {
1625
+ const receiptFailures = await restoreReceipts2(deletedReceipts, home);
1626
+ const directoryFailures = await rollbackMoves(directoryMoves);
1627
+ if (receiptFailures.length > 0 || directoryFailures.length > 0) {
1628
+ throw new CliError(
1629
+ "rollback_failed",
1630
+ hasDirectoryMoves ? `Removal failed and automatic rollback was incomplete. Recovery data remains at ${batchRoot}.` : "Removal failed and automatic rollback was incomplete.",
1631
+ { cause: error }
1632
+ );
1633
+ }
1634
+ if (hasDirectoryMoves) {
1635
+ await rm3(batchRoot, { recursive: true, force: true });
1636
+ }
1637
+ throw new CliError(
1638
+ "remove_failed",
1639
+ `Nothing was removed. ${error instanceof Error ? error.message : "The removal batch failed."}`,
1640
+ { cause: error }
1641
+ );
1642
+ }
1643
+ const retainsModifiedCopy = targets.some((target) => target.action === "backup-modified");
1644
+ if (hasDirectoryMoves && !retainsModifiedCopy) {
1645
+ await rm3(batchRoot, { recursive: true, force: true });
1646
+ }
1647
+ return actions;
1648
+ };
1649
+
1252
1650
  // src/registry-client.ts
1253
- import { createHash as createHash6 } from "crypto";
1651
+ import { createHash as createHash7 } from "crypto";
1254
1652
  import { open, unlink as unlink2 } from "fs/promises";
1255
1653
  var MAX_MANIFEST_BYTES = 1024 * 1024;
1256
1654
  var parseContentLength = (value) => {
@@ -1355,7 +1753,7 @@ var RegistryClient = class {
1355
1753
  throw new CliError("artifact_size_mismatch", "The skill artifact size does not match its manifest.");
1356
1754
  }
1357
1755
  const file = await open(destination, "wx", 384);
1358
- const hash = createHash6("sha256");
1756
+ const hash = createHash7("sha256");
1359
1757
  const reader = response.body.getReader();
1360
1758
  let bytesWritten = 0;
1361
1759
  try {
@@ -1403,6 +1801,60 @@ var RegistryClient = class {
1403
1801
  );
1404
1802
  }
1405
1803
  }
1804
+ async uploadSubmission(skillName, archiveSha256, body, token) {
1805
+ const response = await fetch(new URL("/v1/submissions", this.origin), {
1806
+ method: "POST",
1807
+ headers: {
1808
+ Authorization: `Bearer ${token}`,
1809
+ "X-Yeka-Skill-Name": skillName,
1810
+ "X-Yeka-Archive-Sha256": archiveSha256,
1811
+ "Content-Type": "application/gzip"
1812
+ },
1813
+ body,
1814
+ redirect: "error",
1815
+ signal: AbortSignal.timeout(2 * 60 * 1e3)
1816
+ });
1817
+ if (response.status === 201) {
1818
+ const payload = await response.json();
1819
+ if (typeof payload.submissionId !== "string" || payload.submissionId.length === 0) {
1820
+ throw new CliError("invalid_submission_response", "The registry response was incomplete.");
1821
+ }
1822
+ return payload.submissionId;
1823
+ }
1824
+ if (response.status === 401 || response.status === 403) {
1825
+ await response.body?.cancel("authentication failed");
1826
+ throw new AuthenticationError();
1827
+ }
1828
+ await response.body?.cancel("submission rejected");
1829
+ if (response.status === 415) {
1830
+ throw new CliError(
1831
+ "submission_not_gzip",
1832
+ "The registry did not accept the archive as gzip. Run `yeka-skills share " + skillName + " --check` and share the error with the team channel if it looks fine."
1833
+ );
1834
+ }
1835
+ if (response.status === 413) {
1836
+ throw new CliError(
1837
+ "submission_too_large",
1838
+ "The skill is over the 5 MB share limit. Move anything heavy out of the folder, then run `yeka-skills share " + skillName + "` again."
1839
+ );
1840
+ }
1841
+ if (response.status === 429) {
1842
+ throw new CliError(
1843
+ "submission_rate_limited",
1844
+ "You have shared the daily maximum of 20 skills. Try again tomorrow."
1845
+ );
1846
+ }
1847
+ if (response.status === 400) {
1848
+ throw new CliError(
1849
+ "submission_rejected",
1850
+ "The registry could not accept this share. Run `yeka-skills share " + skillName + " --check` to look the folder over, then try again."
1851
+ );
1852
+ }
1853
+ throw new CliError(
1854
+ "registry_request_failed",
1855
+ `The registry could not accept the share right now (status ${response.status}). Try again in a moment.`
1856
+ );
1857
+ }
1406
1858
  };
1407
1859
 
1408
1860
  // src/session.ts
@@ -1424,13 +1876,253 @@ var withRegistrySession = async (operation, dependencies = defaultDependencies)
1424
1876
  }
1425
1877
  };
1426
1878
 
1879
+ // src/share.ts
1880
+ import { createHash as createHash8 } from "crypto";
1881
+ import { lstat as lstat7, mkdir as mkdir5, mkdtemp as mkdtemp2, readdir as readdir3, readFile as readFile3, realpath, rm as rm4, stat as stat2, writeFile as writeFile2 } from "fs/promises";
1882
+ import { tmpdir } from "os";
1883
+ import path7 from "path";
1884
+ import * as tar2 from "tar";
1885
+ import { gzipSync } from "zlib";
1886
+ var MAX_PACKAGED_BYTES = 5 * 1024 * 1024;
1887
+ var MAX_NOTES_BYTES = 8 * 1024;
1888
+ var RESERVED_ENTRY = "_yeka";
1889
+ var NOTES_ARCHIVE_PATH = "_yeka/notes.txt";
1890
+ var SKILL_MANIFEST_PATH = "SKILL.md";
1891
+ var isSkillFolder = async (candidate) => {
1892
+ try {
1893
+ const entry = await lstat7(candidate);
1894
+ if (entry.isSymbolicLink()) {
1895
+ const resolved = await stat2(await realpath(candidate));
1896
+ return resolved.isDirectory();
1897
+ }
1898
+ return entry.isDirectory();
1899
+ } catch (error) {
1900
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
1901
+ return false;
1902
+ }
1903
+ throw error;
1904
+ }
1905
+ };
1906
+ var candidateRoots = (home) => [
1907
+ { runtime: "codex", targetRoot: path7.join(home, ".agents", "skills") },
1908
+ { runtime: "claude", targetRoot: path7.join(home, ".claude", "skills") },
1909
+ { runtime: "codex", targetRoot: path7.join(home, ".codex", "skills") }
1910
+ ];
1911
+ var resolveSkillFolder = async (skillName, home) => {
1912
+ const parsedName = SkillNameSchema.safeParse(skillName);
1913
+ if (!parsedName.success) {
1914
+ throw new CliError("invalid_skill_name", "Use the exact lowercase skill name from the catalog.");
1915
+ }
1916
+ const found = [];
1917
+ for (const candidate of candidateRoots(home)) {
1918
+ const requestedPath = path7.join(candidate.targetRoot, skillName);
1919
+ if (await isSkillFolder(requestedPath)) {
1920
+ found.push({
1921
+ ...candidate,
1922
+ requestedPath,
1923
+ folderPath: await realpath(requestedPath)
1924
+ });
1925
+ }
1926
+ }
1927
+ if (found.length === 0) {
1928
+ throw new CliError(
1929
+ "skill_folder_not_found",
1930
+ `No local skill folder named ${skillName} was found in your Codex or Claude skills folders.`
1931
+ );
1932
+ }
1933
+ return found[0];
1934
+ };
1935
+ var enumerateSkillFiles = async (folderPath) => {
1936
+ const entries = [];
1937
+ const walk = async (relativePrefix) => {
1938
+ const absoluteDir = relativePrefix === "" ? folderPath : path7.join(folderPath, relativePrefix);
1939
+ for (const entry of await readdir3(absoluteDir, { withFileTypes: true })) {
1940
+ const relativePath = relativePrefix === "" ? entry.name : `${relativePrefix}/${entry.name}`;
1941
+ if (relativePrefix === "" && entry.name === RESERVED_ENTRY) {
1942
+ throw new CliError(
1943
+ "reserved_folder_name",
1944
+ "This skill contains a reserved `_yeka` folder. Rename it \u2014 Yeka Skills uses `_yeka` inside the archive for its own metadata."
1945
+ );
1946
+ }
1947
+ if (entry.isSymbolicLink()) {
1948
+ throw new CliError(
1949
+ "unsafe_skill_folder",
1950
+ `${relativePath} is a symlink. Replace it with a real file or folder before sharing.`
1951
+ );
1952
+ }
1953
+ if (entry.isDirectory()) {
1954
+ await walk(relativePath);
1955
+ } else if (entry.isFile()) {
1956
+ entries.push({ relativePath, sizeBytes: (await lstat7(path7.join(absoluteDir, entry.name))).size });
1957
+ } else {
1958
+ throw new CliError(
1959
+ "unsafe_skill_folder",
1960
+ `${relativePath} is not a regular file. Remove it before sharing.`
1961
+ );
1962
+ }
1963
+ }
1964
+ };
1965
+ await walk("");
1966
+ entries.sort((left, right) => left.relativePath.localeCompare(right.relativePath, "en"));
1967
+ if (!entries.some((entry) => entry.relativePath === SKILL_MANIFEST_PATH)) {
1968
+ throw new CliError(
1969
+ "skill_md_missing",
1970
+ `The folder has no SKILL.md file. Add one at ${path7.join(folderPath, SKILL_MANIFEST_PATH)} so reviewers know what the skill does.`
1971
+ );
1972
+ }
1973
+ return {
1974
+ entries,
1975
+ totalBytes: entries.reduce((total, entry) => total + entry.sizeBytes, 0)
1976
+ };
1977
+ };
1978
+ var CREDENTIAL_PATH_PATTERN = /(\.ssh[/\\]|\.aws[/\\]|\.netrc|\.git-credentials|id_rsa|\.env\b|credentials\.json)/i;
1979
+ var CURL_PIPE_SHELL_PATTERN = /(?:curl|wget)\s[^|]*\|\s*(?:ba|z)?sh\b/i;
1980
+ var LONG_BASE64_PATTERN = /[A-Za-z0-9+/]{80,}={0,2}/;
1981
+ var URL_PATTERN = /https?:\/\/[^\s"'`>)\]]+/g;
1982
+ var lintWarningsFor = async (folderPath, entries) => {
1983
+ const warnings = [];
1984
+ const urls = /* @__PURE__ */ new Set();
1985
+ for (const { relativePath } of entries) {
1986
+ const text = (await readFile3(path7.join(folderPath, relativePath))).toString("utf8");
1987
+ for (const match of text.matchAll(URL_PATTERN)) {
1988
+ urls.add(match[0]);
1989
+ }
1990
+ if (CREDENTIAL_PATH_PATTERN.test(text)) {
1991
+ warnings.push(
1992
+ `${relativePath} mentions a credentials path (like .ssh, .aws, or a .env file). Check that it only points at them and never contains the secrets themselves.`
1993
+ );
1994
+ }
1995
+ if (CURL_PIPE_SHELL_PATTERN.test(text)) {
1996
+ warnings.push(
1997
+ `${relativePath} pipes curl or wget straight into a shell. Reviewers will look at this closely, so make sure the command is safe.`
1998
+ );
1999
+ }
2000
+ if (LONG_BASE64_PATTERN.test(text)) {
2001
+ warnings.push(
2002
+ `${relativePath} contains a long base64 token. Make sure it is not a real secret before you share.`
2003
+ );
2004
+ }
2005
+ }
2006
+ if (urls.size > 0) {
2007
+ warnings.push(
2008
+ `The skill mentions these web addresses, just so you know: ${[...urls].slice(0, 5).join(", ")}.`
2009
+ );
2010
+ }
2011
+ return warnings;
2012
+ };
2013
+ var packageSkillFolder = async (folderPath, notes, maxBytes = MAX_PACKAGED_BYTES) => {
2014
+ if (notes !== void 0 && Buffer.byteLength(notes.text, "utf8") > MAX_NOTES_BYTES) {
2015
+ throw new CliError(
2016
+ "notes_too_large",
2017
+ "The notes file is over 8 KB. Trim it down and try again."
2018
+ );
2019
+ }
2020
+ const skillFiles = await enumerateSkillFiles(folderPath);
2021
+ const staging = await mkdtemp2(path7.join(tmpdir(), "yeka-share-"));
2022
+ let archiveBytes;
2023
+ try {
2024
+ const tarPath = path7.join(staging, "skill.tar");
2025
+ if (notes === void 0) {
2026
+ await tar2.c(
2027
+ { cwd: folderPath, file: tarPath, portable: true, noMtime: true, strict: true },
2028
+ skillFiles.entries.map((entry) => entry.relativePath)
2029
+ );
2030
+ } else {
2031
+ const notesStagingPath = path7.join(staging, NOTES_ARCHIVE_PATH);
2032
+ await mkdir5(path7.dirname(notesStagingPath), { recursive: true });
2033
+ await writeFile2(notesStagingPath, notes.text, { encoding: "utf8", mode: 384 });
2034
+ await tar2.c(
2035
+ { cwd: staging, file: tarPath, portable: true, noMtime: true, strict: true },
2036
+ [NOTES_ARCHIVE_PATH]
2037
+ );
2038
+ await tar2.r(
2039
+ { cwd: folderPath, file: tarPath, portable: true, noMtime: true, strict: true },
2040
+ skillFiles.entries.map((entry) => entry.relativePath)
2041
+ );
2042
+ }
2043
+ const archivePaths = [];
2044
+ await tar2.t({
2045
+ file: tarPath,
2046
+ strict: true,
2047
+ onReadEntry: (entry) => {
2048
+ const entryPath = entry.path.replace(/\/+$/, "");
2049
+ if (archivePaths.includes(entryPath)) {
2050
+ throw new CliError(
2051
+ "reserved_folder_name",
2052
+ `The archive repeats ${entryPath}. Remove the duplicate before sharing.`
2053
+ );
2054
+ }
2055
+ archivePaths.push(entryPath);
2056
+ }
2057
+ });
2058
+ const expectedPaths = new Set(skillFiles.entries.map((entry) => entry.relativePath));
2059
+ if (notes !== void 0) {
2060
+ expectedPaths.add(NOTES_ARCHIVE_PATH);
2061
+ }
2062
+ if (archivePaths.length !== expectedPaths.size || archivePaths.some((p) => !expectedPaths.has(p))) {
2063
+ throw new CliError(
2064
+ "unsafe_skill_folder",
2065
+ "The folder changed while the archive was being built. Nothing was shared; run the share again."
2066
+ );
2067
+ }
2068
+ const recheck = await enumerateSkillFiles(folderPath);
2069
+ if (recheck.entries.length !== skillFiles.entries.length || recheck.entries.some(
2070
+ (entry, index) => entry.relativePath !== skillFiles.entries[index].relativePath || entry.sizeBytes !== skillFiles.entries[index].sizeBytes
2071
+ )) {
2072
+ throw new CliError(
2073
+ "skill_folder_changed",
2074
+ "The folder changed while it was being packaged. Nothing was shared; run the share again."
2075
+ );
2076
+ }
2077
+ const tarBytes = new Uint8Array(await readFile3(tarPath));
2078
+ const bytes = new Uint8Array(gzipSync(tarBytes));
2079
+ archiveBytes = bytes.byteLength;
2080
+ if (archiveBytes > maxBytes) {
2081
+ throw new CliError(
2082
+ "skill_too_large",
2083
+ `The packaged skill is ${(archiveBytes / (1024 * 1024)).toFixed(1)} MB, over the 5 MB share limit. Move anything heavy out of the folder and try again.`
2084
+ );
2085
+ }
2086
+ const sha256 = createHash8("sha256").update(bytes).digest("hex");
2087
+ return {
2088
+ bytes,
2089
+ sha256,
2090
+ fileCount: skillFiles.entries.length,
2091
+ packagedBytes: skillFiles.totalBytes,
2092
+ archiveBytes
2093
+ };
2094
+ } finally {
2095
+ await rm4(staging, { recursive: true, force: true });
2096
+ }
2097
+ };
2098
+ var checkSkillFolder = async (folderPath, skillName, maxBytes = MAX_PACKAGED_BYTES, notes) => {
2099
+ const skillFiles = await enumerateSkillFiles(folderPath);
2100
+ const skillMd = await readFile3(path7.join(folderPath, SKILL_MANIFEST_PATH), "utf8");
2101
+ const frontmatterMatch = skillMd.match(/^---\r?\n([\s\S]*?)\r?\n---/);
2102
+ const frontmatter = frontmatterMatch?.[1] ?? "";
2103
+ const declaredName = frontmatter.match(/^name:\s*(.+)\s*$/m)?.[1]?.trim() ?? "";
2104
+ if (declaredName !== skillName || !SkillNameSchema.safeParse(declaredName).success) {
2105
+ throw new CliError(
2106
+ "skill_name_mismatch",
2107
+ declaredName.length === 0 ? `The SKILL.md frontmatter has no name line. Add \`name: ${skillName}\` at the top.` : `The SKILL.md frontmatter says name: ${declaredName}, but the folder is ${skillName}. Make them match, using only lowercase letters, numbers, and dashes.`
2108
+ );
2109
+ }
2110
+ const packaged = await packageSkillFolder(folderPath, notes, maxBytes);
2111
+ return {
2112
+ fileCount: packaged.fileCount,
2113
+ rawBytes: skillFiles.totalBytes,
2114
+ archiveBytes: packaged.archiveBytes,
2115
+ warnings: await lintWarningsFor(folderPath, skillFiles.entries)
2116
+ };
2117
+ };
2118
+
1427
2119
  // src/targets.ts
1428
- import { lstat as lstat5 } from "fs/promises";
1429
- import os5 from "os";
1430
- import path6 from "path";
2120
+ import { lstat as lstat8 } from "fs/promises";
2121
+ import os6 from "os";
2122
+ import path8 from "path";
1431
2123
  var isRealDirectory = async (candidate) => {
1432
2124
  try {
1433
- const entry = await lstat5(candidate);
2125
+ const entry = await lstat8(candidate);
1434
2126
  return entry.isDirectory() && !entry.isSymbolicLink();
1435
2127
  } catch (error) {
1436
2128
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
@@ -1441,7 +2133,7 @@ var isRealDirectory = async (candidate) => {
1441
2133
  };
1442
2134
  var pathExists = async (candidate) => {
1443
2135
  try {
1444
- await lstat5(candidate);
2136
+ await lstat8(candidate);
1445
2137
  return true;
1446
2138
  } catch (error) {
1447
2139
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
@@ -1451,17 +2143,17 @@ var pathExists = async (candidate) => {
1451
2143
  }
1452
2144
  };
1453
2145
  var resolveCodexTarget = async (home, skillName) => {
1454
- const currentRoot = path6.join(home, ".agents", "skills");
1455
- const currentParent = path6.join(home, ".agents");
1456
- const codexConfig = path6.join(home, ".codex");
1457
- const legacyRoot = path6.join(codexConfig, "skills");
2146
+ const currentRoot = path8.join(home, ".agents", "skills");
2147
+ const currentParent = path8.join(home, ".agents");
2148
+ const codexConfig = path8.join(home, ".codex");
2149
+ const legacyRoot = path8.join(codexConfig, "skills");
1458
2150
  const legacyActive = await isRealDirectory(legacyRoot);
1459
2151
  const currentActive = await isRealDirectory(currentRoot) || await isRealDirectory(currentParent) || await isRealDirectory(codexConfig) && !legacyActive;
1460
2152
  if (currentActive) {
1461
- if (legacyActive && await pathExists(path6.join(legacyRoot, skillName))) {
2153
+ if (legacyActive && await pathExists(path8.join(legacyRoot, skillName))) {
1462
2154
  throw new CliError(
1463
2155
  "legacy_codex_conflict",
1464
- `Codex can see a legacy copy at ${path6.join(legacyRoot, skillName)}. Remove or archive that copy before installation.`
2156
+ `Codex can see a legacy copy at ${path8.join(legacyRoot, skillName)}. Remove or archive that copy before installation.`
1465
2157
  );
1466
2158
  }
1467
2159
  return { runtime: "codex", targetRoot: currentRoot, legacy: false };
@@ -1472,17 +2164,17 @@ var resolveCodexTarget = async (home, skillName) => {
1472
2164
  return null;
1473
2165
  };
1474
2166
  var resolveClaudeTarget = async (home) => {
1475
- const claudeConfig = path6.join(home, ".claude");
2167
+ const claudeConfig = path8.join(home, ".claude");
1476
2168
  if (!await isRealDirectory(claudeConfig)) {
1477
2169
  return null;
1478
2170
  }
1479
2171
  return {
1480
2172
  runtime: "claude",
1481
- targetRoot: path6.join(claudeConfig, "skills"),
2173
+ targetRoot: path8.join(claudeConfig, "skills"),
1482
2174
  legacy: false
1483
2175
  };
1484
2176
  };
1485
- var resolveInstallTargets = async (skill, home = os5.homedir()) => {
2177
+ var resolveInstallTargets = async (skill, home = os6.homedir()) => {
1486
2178
  const targets = [];
1487
2179
  if (skill.runtimes.includes("codex")) {
1488
2180
  const codex = await resolveCodexTarget(home, skill.name);
@@ -1526,8 +2218,8 @@ var findSkill = (manifest, name) => {
1526
2218
  };
1527
2219
  var ensurePrivateCache = async (home) => {
1528
2220
  const cache = localPaths(home).cache;
1529
- await mkdir4(cache, { recursive: true, mode: 448 });
1530
- const cacheStat = await lstat6(cache);
2221
+ await mkdir6(cache, { recursive: true, mode: 448 });
2222
+ const cacheStat = await lstat9(cache);
1531
2223
  if (!cacheStat.isDirectory() || cacheStat.isSymbolicLink()) {
1532
2224
  throw new CliError("unsafe_cache", "The Yeka Skills cache is not a real directory.");
1533
2225
  }
@@ -1535,9 +2227,9 @@ var ensurePrivateCache = async (home) => {
1535
2227
  };
1536
2228
  var downloadAndInstall = async (client, token, skill, targets, options, home) => {
1537
2229
  const cache = await ensurePrivateCache(home);
1538
- const downloadRoot = await mkdtemp2(path7.join(cache, "download-"));
1539
- await chmod2(downloadRoot, 448);
1540
- const archivePath = path7.join(downloadRoot, `${skill.name}.tgz`);
2230
+ const downloadRoot = await mkdtemp3(path9.join(cache, "download-"));
2231
+ await chmod3(downloadRoot, 448);
2232
+ const archivePath = path9.join(downloadRoot, `${skill.name}.tgz`);
1541
2233
  try {
1542
2234
  await client.downloadArtifact(skill, token, archivePath);
1543
2235
  return await installSkillFromArchive({
@@ -1548,7 +2240,7 @@ var downloadAndInstall = async (client, token, skill, targets, options, home) =>
1548
2240
  home
1549
2241
  });
1550
2242
  } finally {
1551
- await rm3(downloadRoot, { recursive: true, force: true });
2243
+ await rm5(downloadRoot, { recursive: true, force: true });
1552
2244
  }
1553
2245
  };
1554
2246
  var actionMessage = (skillName, action, dryRun) => {
@@ -1571,17 +2263,52 @@ var printActions = (context, skillName, actions, dryRun) => {
1571
2263
  }
1572
2264
  }
1573
2265
  };
2266
+ var COMPANION_SKILL_NAME = "yeka-skills";
1574
2267
  var addCommand = async (skillNameInput, options, context = {}) => {
1575
2268
  const skillName = validatedSkillName(skillNameInput);
1576
- const home = path7.resolve(context.home ?? os6.homedir());
2269
+ const home = path9.resolve(context.home ?? os7.homedir());
1577
2270
  const client = context.client ?? new RegistryClient();
1578
- const actions = await withRegistrySession(async (token) => {
2271
+ const { actions, companionInstalled } = await withRegistrySession(async (token) => {
1579
2272
  const manifest = await client.getManifest(token);
1580
2273
  const skill = findSkill(manifest, skillName);
1581
2274
  const targets = await resolveInstallTargets(skill, home);
1582
- return downloadAndInstall(client, token, skill, targets, options, home);
1583
- });
2275
+ const primaryActions = await downloadAndInstall(client, token, skill, targets, options, home);
2276
+ let companion = "skipped";
2277
+ if (options.dryRun !== true && skillName !== COMPANION_SKILL_NAME) {
2278
+ let hasCompanionReceipt = false;
2279
+ try {
2280
+ const receipts = await listReceipts(home);
2281
+ hasCompanionReceipt = receipts.some((r2) => r2.skillName === COMPANION_SKILL_NAME);
2282
+ } catch {
2283
+ hasCompanionReceipt = false;
2284
+ }
2285
+ if (hasCompanionReceipt) {
2286
+ companion = "present";
2287
+ } else {
2288
+ try {
2289
+ const companionSkill = findSkill(manifest, COMPANION_SKILL_NAME);
2290
+ const companionTargets = await resolveInstallTargets(companionSkill, home);
2291
+ await downloadAndInstall(client, token, companionSkill, companionTargets, options, home);
2292
+ companion = "installed";
2293
+ } catch {
2294
+ companion = "failed";
2295
+ }
2296
+ }
2297
+ }
2298
+ return { actions: primaryActions, companionInstalled: companion };
2299
+ }, context.session);
1584
2300
  printActions(context, skillName, actions, options.dryRun === true);
2301
+ if (companionInstalled === "installed") {
2302
+ output(
2303
+ context,
2304
+ "Also installed the yeka-skills assistant so your agent knows how to update and share skills."
2305
+ );
2306
+ } else if (companionInstalled === "failed") {
2307
+ output(
2308
+ context,
2309
+ "(could not auto-install the assistant; run `npx yeka-skills add yeka-skills` yourself)"
2310
+ );
2311
+ }
1585
2312
  };
1586
2313
  var receiptGroups = (receipts) => {
1587
2314
  const groups = /* @__PURE__ */ new Map();
@@ -1607,7 +2334,7 @@ var statusLabel = (status) => {
1607
2334
  }
1608
2335
  };
1609
2336
  var listCommand = async (context = {}) => {
1610
- const home = path7.resolve(context.home ?? os6.homedir());
2337
+ const home = path9.resolve(context.home ?? os7.homedir());
1611
2338
  const receipts = await listReceipts(home);
1612
2339
  if (receipts.length === 0) {
1613
2340
  output(context, "No managed Yeka skills are installed.");
@@ -1618,7 +2345,7 @@ var listCommand = async (context = {}) => {
1618
2345
  }
1619
2346
  };
1620
2347
  var statusCommand = async (context = {}) => {
1621
- const home = path7.resolve(context.home ?? os6.homedir());
2348
+ const home = path9.resolve(context.home ?? os7.homedir());
1622
2349
  const receipts = await listReceipts(home);
1623
2350
  if (receipts.length === 0) {
1624
2351
  output(context, "No managed Yeka skills are installed.");
@@ -1653,7 +2380,7 @@ var assertUpdatableStatuses = (skillName, statuses) => {
1653
2380
  };
1654
2381
  var updateCommand = async (skillNameInput, options, context = {}) => {
1655
2382
  const selectedName = skillNameInput === void 0 ? void 0 : validatedSkillName(skillNameInput);
1656
- const home = path7.resolve(context.home ?? os6.homedir());
2383
+ const home = path9.resolve(context.home ?? os7.homedir());
1657
2384
  const allReceipts = await listReceipts(home);
1658
2385
  const groups = receiptGroups(allReceipts);
1659
2386
  if (selectedName !== void 0 && !groups.has(selectedName)) {
@@ -1706,6 +2433,100 @@ var updateCommand = async (skillNameInput, options, context = {}) => {
1706
2433
  printActions(context, result.name, result.actions, options.dryRun === true);
1707
2434
  }
1708
2435
  };
2436
+ var removeActionMessage = (skillName, action, dryRun) => {
2437
+ const agent = action.runtime === "codex" ? "Codex" : "Claude";
2438
+ switch (action.action) {
2439
+ case "remove":
2440
+ return dryRun ? `Would remove the ${agent} copy of ${skillName} at ${action.installedPath}.` : `Removed the ${agent} copy of ${skillName} at ${action.installedPath}.`;
2441
+ case "clean-receipt":
2442
+ return dryRun ? `The ${agent} copy of ${skillName} is already gone; would clean its receipt.` : `The ${agent} copy of ${skillName} was already gone; receipt cleaned.`;
2443
+ case "backup-modified":
2444
+ return dryRun ? `Would remove the edited ${agent} copy of ${skillName} and preserve it in a private backup folder.` : `Removed the edited ${agent} copy of ${skillName}; backup: ${action.backupPath}.`;
2445
+ }
2446
+ };
2447
+ var removeCommand = async (skillNameInput, options, context = {}) => {
2448
+ const home = path9.resolve(context.home ?? os7.homedir());
2449
+ if (skillNameInput === void 0) {
2450
+ const names = await removableSkillNames(home);
2451
+ throw new CliError(
2452
+ "missing_skill_name",
2453
+ names.length === 0 ? "Usage: yeka-skills remove <skill>. No managed Yeka skills are removable." : `Usage: yeka-skills remove <skill>. Removable skills: ${names.join(", ")}.`
2454
+ );
2455
+ }
2456
+ const skillName = validatedSkillName(skillNameInput);
2457
+ const actions = await removeSkill(skillName, {
2458
+ home,
2459
+ ...options.dryRun === void 0 ? {} : { dryRun: options.dryRun },
2460
+ ...options.force === void 0 ? {} : { force: options.force }
2461
+ });
2462
+ for (const action of actions) {
2463
+ output(context, removeActionMessage(skillName, action, options.dryRun === true));
2464
+ }
2465
+ };
2466
+ var readNotesFile = async (notesPath) => {
2467
+ let text;
2468
+ try {
2469
+ text = await readFile4(notesPath, "utf8");
2470
+ } catch {
2471
+ throw new CliError(
2472
+ "notes_file_unreadable",
2473
+ `The notes file could not be read: ${notesPath}. Check the path and try again.`
2474
+ );
2475
+ }
2476
+ if (Buffer.byteLength(text, "utf8") > 8 * 1024) {
2477
+ throw new CliError(
2478
+ "notes_too_large",
2479
+ "The notes file is over 8 KB. Trim it down and try again."
2480
+ );
2481
+ }
2482
+ return { text };
2483
+ };
2484
+ var agentLabel2 = (runtime) => runtime === "codex" ? "Codex" : "Claude";
2485
+ var shareCommand = async (skillNameInput, options, context = {}) => {
2486
+ const home = path9.resolve(context.home ?? os7.homedir());
2487
+ if (skillNameInput === void 0) {
2488
+ throw new CliError("missing_skill_name", "Usage: yeka-skills share <skill-name>.");
2489
+ }
2490
+ const skillName = validatedSkillName(skillNameInput);
2491
+ const folder = await resolveSkillFolder(skillName, home);
2492
+ output(
2493
+ context,
2494
+ folder.requestedPath === folder.folderPath ? `Found ${skillName} in your ${agentLabel2(folder.runtime)} skills folder: ${folder.folderPath}` : `Found ${skillName} in your ${agentLabel2(folder.runtime)} skills folder: ${folder.requestedPath} (packaging the folder it points at: ${folder.folderPath})`
2495
+ );
2496
+ const notes = options.notesFile === void 0 ? void 0 : await readNotesFile(options.notesFile);
2497
+ const check = await checkSkillFolder(folder.folderPath, skillName, void 0, notes);
2498
+ if (options.check === true) {
2499
+ if (check.warnings.length === 0) {
2500
+ output(context, "Nothing looked risky.");
2501
+ } else {
2502
+ for (const warning of check.warnings) {
2503
+ output(context, `Heads up: ${warning}`);
2504
+ }
2505
+ output(context, "These are warnings only \u2014 they never block a share. The team's checks decide.");
2506
+ }
2507
+ output(
2508
+ context,
2509
+ `${skillName} is ready to share (${check.fileCount} files, ${(check.archiveBytes / (1024 * 1024)).toFixed(1)} MB packaged). Run \`yeka-skills share ${skillName}\` when you're happy with it.`
2510
+ );
2511
+ return;
2512
+ }
2513
+ const packaged = await packageSkillFolder(folder.folderPath, notes);
2514
+ const client = context.client ?? new RegistryClient();
2515
+ try {
2516
+ await withRegistrySession(
2517
+ (token) => client.uploadSubmission(skillName, packaged.sha256, packaged.bytes, token)
2518
+ );
2519
+ } catch (error) {
2520
+ if (error instanceof CliError && error.code.startsWith("login_")) {
2521
+ throw new CliError(
2522
+ error.code,
2523
+ `${error.message} You can also run \`yeka-skills login\` first, then try the share again.`
2524
+ );
2525
+ }
2526
+ throw error;
2527
+ }
2528
+ output(context, "Shared. You'll hear back in #yeka-skills \u2014 updates usually go live within the hour.");
2529
+ };
1709
2530
  var loginCommand = async (context = {}) => {
1710
2531
  await deleteSession();
1711
2532
  await login();
@@ -1718,13 +2539,19 @@ var logoutCommand = async (context = {}) => {
1718
2539
 
1719
2540
  // src/cli.ts
1720
2541
  var program = new Command();
1721
- program.name("yeka-skills").description("Install and update private Gemography agent skills").version("0.1.0").showHelpAfterError();
2542
+ program.name("yeka-skills").description("Install, update, and remove private Gemography agent skills").version("0.1.0").showHelpAfterError();
1722
2543
  program.command("add").description("Install one private skill").argument("<skill>", "exact skill name").option("--dry-run", "verify and show changes without installation").action(async (skill, options) => addCommand(skill, options));
1723
2544
  program.command("list").description("List managed local skills without network access").action(async () => listCommand());
1724
2545
  program.command("status").description("Check local skills and available updates").action(async () => statusCommand());
1725
2546
  program.command("update").description("Update one managed skill or all managed skills").argument("[skill]", "exact skill name").option("--dry-run", "verify and show changes without installation").action(
1726
2547
  async (skill, options) => updateCommand(skill, options)
1727
2548
  );
2549
+ program.command("remove").description("Remove one managed skill without registry access").argument("[skill]", "exact skill name").option("--force", "preserve and remove a locally edited copy").option("--dry-run", "verify and show changes without removal").action(
2550
+ async (skill, options) => removeCommand(skill, options)
2551
+ );
2552
+ program.command("share").description("Share one local skill with the team for review").argument("<skill>", "exact skill name").option("--check", "look the skill over without sharing").option("--notes-file <path>", "text file with your interview answers, shared with the reviewers").action(
2553
+ async (skill, options) => shareCommand(skill, options)
2554
+ );
1728
2555
  program.command("login").description("Start a new private registry session").action(async () => loginCommand());
1729
2556
  program.command("logout").description("Remove the saved private registry session").action(async () => logoutCommand());
1730
2557
  var main = async () => {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "yeka-skills",
3
- "version": "0.1.1",
4
- "description": "Install and update private Gemography agent skills",
3
+ "version": "0.2.0",
4
+ "description": "Install, update, and remove private Gemography agent skills",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
7
7
  "repository": {