@rayul/kordis 0.2.1 → 0.2.2

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 (2) hide show
  1. package/dist/index.js +343 -204
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1342,11 +1342,31 @@ function searchCommand() {
1342
1342
  limit: opts.limit
1343
1343
  });
1344
1344
  }
1345
+ const queryProvided = query.length > 0;
1345
1346
  printDual({
1346
1347
  json: res,
1347
1348
  human: () => {
1348
1349
  if (res.results.length === 0) {
1349
1350
  printHuman(c.yellow("(no results)"));
1351
+ if (opts.type) {
1352
+ printHuman(
1353
+ c.gray(
1354
+ ` \u2192 no packages of type "${opts.type}" match. Try without --type, or list everything: kordis search --type ${opts.type} --sort recent --limit 25`
1355
+ )
1356
+ );
1357
+ } else if (queryProvided) {
1358
+ printHuman(
1359
+ c.gray(
1360
+ " \u2192 try `--type skill` (or mcp-server / project-rules / custom-agent / hook) to narrow, or broader keywords"
1361
+ )
1362
+ );
1363
+ } else {
1364
+ printHuman(
1365
+ c.gray(
1366
+ ' \u2192 try keywords (e.g. `kordis search "topic"`) or `kordis search --trending`'
1367
+ )
1368
+ );
1369
+ }
1350
1370
  return;
1351
1371
  }
1352
1372
  for (const p of res.results) {
@@ -1368,6 +1388,13 @@ function searchCommand() {
1368
1388
  if (res.next_cursor) {
1369
1389
  printHuman(c.gray(`(more available \u2014 pass --cursor ${res.next_cursor})`));
1370
1390
  }
1391
+ if (queryProvided && !opts.type && res.results.length > 3) {
1392
+ printHuman(
1393
+ c.gray(
1394
+ " \u2192 narrow further with `--type skill|mcp-server|project-rules|custom-agent|hook`"
1395
+ )
1396
+ );
1397
+ }
1371
1398
  }
1372
1399
  });
1373
1400
  });
@@ -1427,210 +1454,6 @@ var FrontmatterParseError = class extends Error {
1427
1454
  }
1428
1455
  };
1429
1456
 
1430
- // src/commands/push.ts
1431
- function pushCommand() {
1432
- return new Command4("push").description("publish a new package or a new version").argument("<path>", "SKILL.md file, .skill ZIP, or skill directory").option("--bump <strategy>", "patch|minor|major (update only)", "patch").option(
1433
- "--slug <slug>",
1434
- 'override slug; accepts "user/slug" to point at a specific package'
1435
- ).option("--type <type>", "override type inference").option("--changelog <text>", "optional changelog entry").option("--dry-run", "validate only; do not publish").option("--no-wait", "return immediately without waiting for review").action(
1436
- async (path, opts) => {
1437
- const bundle = await useClient();
1438
- const abs = resolve(process.cwd(), path);
1439
- const s = await stat(abs);
1440
- let content;
1441
- let assets = [];
1442
- if (s.isDirectory()) {
1443
- ({ content, assets } = await bundleDirectory(abs));
1444
- content = ensureTypeField(content, "skill");
1445
- } else if (abs.endsWith(".skill")) {
1446
- ({ content, assets } = await bundleSkillZip(abs));
1447
- content = ensureTypeField(content, "skill");
1448
- } else {
1449
- content = await readFile3(abs, "utf8");
1450
- }
1451
- let nameFromFm = null;
1452
- try {
1453
- const fm = parseFrontmatter(content).frontmatter ?? {};
1454
- const v = fm["name"];
1455
- if (typeof v === "string") nameFromFm = v;
1456
- } catch (err) {
1457
- throw err instanceof Error ? err : new Error(String(err));
1458
- }
1459
- let targetUser = null;
1460
- let targetSlug = null;
1461
- if (opts.slug && opts.slug.includes("/")) {
1462
- const [u, s2] = opts.slug.split("/", 2);
1463
- if (u && s2) {
1464
- targetUser = u;
1465
- targetSlug = s2;
1466
- }
1467
- } else {
1468
- if (bundle.username && (opts.slug || nameFromFm)) {
1469
- targetUser = bundle.username;
1470
- targetSlug = opts.slug ?? nameFromFm;
1471
- }
1472
- }
1473
- let targetFullSlug;
1474
- if (targetUser && targetSlug) {
1475
- try {
1476
- await bundle.api.getPackage(targetUser, targetSlug);
1477
- targetFullSlug = `${targetUser}/${targetSlug}`;
1478
- } catch (err) {
1479
- if (err instanceof KordisApiError && err.code === "not_found") {
1480
- targetFullSlug = void 0;
1481
- } else if (!(err instanceof KordisApiError)) {
1482
- throw err;
1483
- }
1484
- }
1485
- }
1486
- const spin = spinner(
1487
- opts.dryRun ? "Validating" : targetFullSlug ? `Publishing update to ${targetFullSlug}` : "Publishing"
1488
- );
1489
- const res = await bundle.authedRequest(
1490
- (api) => api.publish(
1491
- {
1492
- content,
1493
- assets: assets.length > 0 ? assets : void 0,
1494
- bump: opts.bump,
1495
- slug: opts.slug && !opts.slug.includes("/") ? opts.slug : void 0,
1496
- target_full_slug: targetFullSlug,
1497
- type: opts.type,
1498
- changelog: opts.changelog,
1499
- dry_run: !!opts.dryRun
1500
- },
1501
- {
1502
- waitForReviewMs: opts.wait === false ? 0 : 3e3
1503
- }
1504
- )
1505
- );
1506
- spin.succeed();
1507
- printDual({
1508
- json: res,
1509
- human: () => {
1510
- printHuman(
1511
- `${c.bold(res.full_slug)} ${c.gray(`v${res.version}`)} \u2192 ${res.status === "published" ? c.green("published") : res.status === "pending" ? c.yellow("pending review") : c.red("rejected")}`
1512
- );
1513
- if (assets.length > 0) {
1514
- printHuman(c.gray(` + ${assets.length} bundled file(s)`));
1515
- }
1516
- printHuman(` ${c.cyan(res.url)}`);
1517
- if (res.status === "pending") {
1518
- printHuman(
1519
- c.yellow(
1520
- ` ! AI review is still pending \u2014 re-check with \`kordis info ${res.full_slug} --json\` for the verdict.`
1521
- )
1522
- );
1523
- }
1524
- if (res.review.flags.length > 0) {
1525
- printHuman(c.yellow(` ${res.review.flags.length} review flag(s):`));
1526
- for (const f of res.review.flags) {
1527
- printHuman(c.yellow(` - ${f.code}${f.message ? `: ${f.message}` : ""}`));
1528
- }
1529
- }
1530
- }
1531
- });
1532
- }
1533
- );
1534
- }
1535
- function ensureTypeField(content, defaultType) {
1536
- const fmMatch = /^---\r?\n([\s\S]*?)\r?\n---/.exec(content);
1537
- if (!fmMatch) return content;
1538
- const fmBlock = fmMatch[1];
1539
- if (/^type\s*:/m.test(fmBlock)) return content;
1540
- const injected = fmMatch[0].replace(/^---\r?\n/, `---
1541
- type: ${defaultType}
1542
- `);
1543
- return content.replace(fmMatch[0], injected);
1544
- }
1545
- var EXCLUDE_NAMES = /* @__PURE__ */ new Set([".DS_Store", "Thumbs.db", ".gitkeep"]);
1546
- var EXCLUDE_DIRS = /* @__PURE__ */ new Set(["__pycache__", "node_modules", ".git", "evals", "dist", ".turbo"]);
1547
- var EXCLUDE_EXTS = /* @__PURE__ */ new Set([".pyc", ".pyo", ".rbc"]);
1548
- function shouldExclude(relPath, isDir) {
1549
- const parts = relPath.split(posix.sep);
1550
- const name = parts[parts.length - 1];
1551
- if (EXCLUDE_NAMES.has(name)) return true;
1552
- if (isDir && EXCLUDE_DIRS.has(name)) return true;
1553
- const ext = name.includes(".") ? "." + name.split(".").pop() : "";
1554
- if (EXCLUDE_EXTS.has(ext)) return true;
1555
- return false;
1556
- }
1557
- async function walkDir(base, relDir, collected) {
1558
- const entries = await readdir(join4(base, relDir), { withFileTypes: true });
1559
- for (const entry of entries) {
1560
- const rel = relDir ? `${relDir}/${entry.name}` : entry.name;
1561
- if (shouldExclude(rel, entry.isDirectory())) continue;
1562
- if (entry.isDirectory()) {
1563
- await walkDir(base, rel, collected);
1564
- } else if (entry.isFile()) {
1565
- collected.push({ rel, abs: join4(base, rel) });
1566
- }
1567
- }
1568
- }
1569
- async function readAsset(absPath) {
1570
- const buf = await readFile3(absPath);
1571
- const text = buf.toString("utf8");
1572
- const roundtrip = Buffer.from(text, "utf8");
1573
- if (roundtrip.equals(buf)) {
1574
- return { content: text, encoding: "utf8" };
1575
- }
1576
- const b64 = buf.toString("base64");
1577
- printHuman(c.gray(` \u2192 encoding ${absPath.split(sep).pop()} as base64`));
1578
- return { content: b64, encoding: "base64" };
1579
- }
1580
- async function bundleDirectory(dir) {
1581
- const all = [];
1582
- await walkDir(dir, "", all);
1583
- const skillEntry = all.find((f) => f.rel === "SKILL.md");
1584
- if (!skillEntry) {
1585
- throw new Error(`No SKILL.md found in directory ${dir}. Push a directory that has SKILL.md at its root.`);
1586
- }
1587
- const content = await readFile3(skillEntry.abs, "utf8");
1588
- const assets = [];
1589
- for (const f of all) {
1590
- if (f.rel === "SKILL.md") continue;
1591
- const { content: fc, encoding } = await readAsset(f.abs);
1592
- assets.push({ path: f.rel, content: fc, encoding });
1593
- }
1594
- return { content, assets };
1595
- }
1596
- async function bundleSkillZip(zipPath) {
1597
- const buf = await readFile3(zipPath);
1598
- const zip = await JSZip.loadAsync(buf);
1599
- const entries = Object.keys(zip.files).filter((k) => !zip.files[k].dir);
1600
- if (entries.length === 0) throw new Error(".skill file is empty");
1601
- const firstParts = entries[0].split("/");
1602
- const prefix = firstParts.length > 1 ? firstParts[0] + "/" : "";
1603
- const stripped = entries.map((k) => ({
1604
- zipKey: k,
1605
- rel: prefix ? k.slice(prefix.length) : k
1606
- })).filter((f) => f.rel.length > 0 && !shouldExclude(f.rel, false));
1607
- const skillEntry = stripped.find((f) => f.rel === "SKILL.md");
1608
- if (!skillEntry) {
1609
- throw new Error(`.skill ZIP has no SKILL.md at its root (prefix="${prefix}")`);
1610
- }
1611
- const content = await zip.files[skillEntry.zipKey].async("string");
1612
- const assets = [];
1613
- for (const f of stripped) {
1614
- if (f.rel === "SKILL.md") continue;
1615
- const rawBuf = await zip.files[f.zipKey].async("uint8array");
1616
- const buf2 = Buffer.from(rawBuf);
1617
- const text = buf2.toString("utf8");
1618
- const roundtrip = Buffer.from(text, "utf8");
1619
- if (roundtrip.equals(buf2)) {
1620
- assets.push({ path: f.rel, content: text, encoding: "utf8" });
1621
- } else {
1622
- printHuman(c.gray(` \u2192 encoding ${f.rel} as base64`));
1623
- assets.push({ path: f.rel, content: buf2.toString("base64"), encoding: "base64" });
1624
- }
1625
- }
1626
- return { content, assets };
1627
- }
1628
-
1629
- // src/commands/pull.ts
1630
- import { Command as Command5 } from "commander";
1631
- import { mkdir as mkdir4, writeFile as writeFile4 } from "fs/promises";
1632
- import { dirname as dirname2, join as join6 } from "path";
1633
-
1634
1457
  // ../../packages/harness-schema/src/deps.ts
1635
1458
  init_src();
1636
1459
  import { z as z10 } from "zod";
@@ -2105,6 +1928,312 @@ function zodFail(issues) {
2105
1928
  })
2106
1929
  };
2107
1930
  }
1931
+ var ASSET_MAX_COUNT = 50;
1932
+ var SAFE_PATH_RE = /^[a-zA-Z0-9][a-zA-Z0-9._/-]*$/;
1933
+ function isAssetPathSafe(p) {
1934
+ if (!SAFE_PATH_RE.test(p)) return false;
1935
+ return !p.split("/").some((seg) => seg === "..");
1936
+ }
1937
+ function validateAssets(assets) {
1938
+ const flags = [];
1939
+ if (assets.length > ASSET_MAX_COUNT) {
1940
+ flags.push(
1941
+ flag("too_many_assets", `bundle has ${assets.length} files (max ${ASSET_MAX_COUNT})`, {
1942
+ hint: `Bundle has more than ${ASSET_MAX_COUNT} files. Trim demos or move large reference material to an external Git repo.`
1943
+ })
1944
+ );
1945
+ return { ok: false, flags };
1946
+ }
1947
+ for (const asset of assets) {
1948
+ if (!isAssetPathSafe(asset.path)) {
1949
+ flags.push(
1950
+ flag("asset_path_unsafe", `unsafe asset path: "${asset.path}"`, {
1951
+ field: "assets",
1952
+ hint: 'Asset paths must be relative POSIX, no leading "/", no "..". Example: scripts/run.sh'
1953
+ })
1954
+ );
1955
+ }
1956
+ }
1957
+ return { ok: flags.length === 0, flags };
1958
+ }
1959
+
1960
+ // src/commands/push.ts
1961
+ var ASSET_MAX_BYTES = 2 * 1024 * 1024;
1962
+ var BUNDLE_MAX_BYTES = 5 * 1024 * 1024;
1963
+ function pushCommand() {
1964
+ return new Command4("push").description("publish a new package or a new version").argument("<path>", "SKILL.md file, .skill ZIP, or skill directory").option("--bump <strategy>", "patch|minor|major (update only)", "patch").option(
1965
+ "--slug <slug>",
1966
+ 'override slug; accepts "user/slug" to point at a specific package'
1967
+ ).option("--type <type>", "override type inference").option("--changelog <text>", "optional changelog entry").option("--dry-run", "validate only; do not publish").option("--no-wait", "return immediately without waiting for review").action(
1968
+ async (path, opts) => {
1969
+ const bundle = await useClient();
1970
+ const abs = resolve(process.cwd(), path);
1971
+ const s = await stat(abs);
1972
+ let content;
1973
+ let assets = [];
1974
+ if (s.isDirectory()) {
1975
+ ({ content, assets } = await bundleDirectory(abs));
1976
+ content = ensureTypeField(content, "skill");
1977
+ } else if (abs.endsWith(".skill")) {
1978
+ ({ content, assets } = await bundleSkillZip(abs));
1979
+ content = ensureTypeField(content, "skill");
1980
+ } else {
1981
+ content = await readFile3(abs, "utf8");
1982
+ }
1983
+ let nameFromFm = null;
1984
+ try {
1985
+ const fm = parseFrontmatter(content).frontmatter ?? {};
1986
+ const v = fm["name"];
1987
+ if (typeof v === "string") nameFromFm = v;
1988
+ } catch (err) {
1989
+ throw err instanceof Error ? err : new Error(String(err));
1990
+ }
1991
+ if (opts.dryRun) {
1992
+ await runDryRun({ content, assets });
1993
+ return;
1994
+ }
1995
+ let targetUser = null;
1996
+ let targetSlug = null;
1997
+ if (opts.slug && opts.slug.includes("/")) {
1998
+ const [u, s2] = opts.slug.split("/", 2);
1999
+ if (u && s2) {
2000
+ targetUser = u;
2001
+ targetSlug = s2;
2002
+ }
2003
+ } else {
2004
+ if (bundle.username && (opts.slug || nameFromFm)) {
2005
+ targetUser = bundle.username;
2006
+ targetSlug = opts.slug ?? nameFromFm;
2007
+ }
2008
+ }
2009
+ let targetFullSlug;
2010
+ if (targetUser && targetSlug) {
2011
+ try {
2012
+ await bundle.api.getPackage(targetUser, targetSlug);
2013
+ targetFullSlug = `${targetUser}/${targetSlug}`;
2014
+ } catch (err) {
2015
+ if (err instanceof KordisApiError && err.code === "not_found") {
2016
+ targetFullSlug = void 0;
2017
+ } else if (!(err instanceof KordisApiError)) {
2018
+ throw err;
2019
+ }
2020
+ }
2021
+ }
2022
+ const spin = spinner(
2023
+ opts.dryRun ? "Validating" : targetFullSlug ? `Publishing update to ${targetFullSlug}` : "Publishing"
2024
+ );
2025
+ const res = await bundle.authedRequest(
2026
+ (api) => api.publish(
2027
+ {
2028
+ content,
2029
+ assets: assets.length > 0 ? assets : void 0,
2030
+ bump: opts.bump,
2031
+ slug: opts.slug && !opts.slug.includes("/") ? opts.slug : void 0,
2032
+ target_full_slug: targetFullSlug,
2033
+ type: opts.type,
2034
+ changelog: opts.changelog,
2035
+ dry_run: !!opts.dryRun
2036
+ },
2037
+ {
2038
+ waitForReviewMs: opts.wait === false ? 0 : 3e3
2039
+ }
2040
+ )
2041
+ );
2042
+ spin.succeed();
2043
+ printDual({
2044
+ json: res,
2045
+ human: () => {
2046
+ printHuman(
2047
+ `${c.bold(res.full_slug)} ${c.gray(`v${res.version}`)} \u2192 ${res.status === "published" ? c.green("published") : res.status === "pending" ? c.yellow("pending review") : c.red("rejected")}`
2048
+ );
2049
+ if (assets.length > 0) {
2050
+ printHuman(c.gray(` + ${assets.length} bundled file(s)`));
2051
+ }
2052
+ printHuman(` ${c.cyan(res.url)}`);
2053
+ if (res.status === "pending") {
2054
+ printHuman(
2055
+ c.yellow(
2056
+ ` ! AI review is still pending \u2014 re-check with \`kordis info ${res.full_slug} --json\` for the verdict.`
2057
+ )
2058
+ );
2059
+ }
2060
+ if (res.review.flags.length > 0) {
2061
+ printHuman(c.yellow(` ${res.review.flags.length} review flag(s):`));
2062
+ for (const f of res.review.flags) {
2063
+ printHuman(c.yellow(` - ${f.code}${f.message ? `: ${f.message}` : ""}`));
2064
+ }
2065
+ }
2066
+ }
2067
+ });
2068
+ }
2069
+ );
2070
+ }
2071
+ function checkAssetSizesLocal(assets) {
2072
+ const flags = [];
2073
+ let totalBytes = 0;
2074
+ for (const asset of assets) {
2075
+ const byteLen = asset.encoding === "base64" ? Math.ceil(asset.content.length * 3 / 4) : Buffer.byteLength(asset.content, "utf8");
2076
+ if (byteLen > ASSET_MAX_BYTES) {
2077
+ flags.push({
2078
+ code: "asset_too_large",
2079
+ message: `asset "${asset.path}" is ${byteLen} bytes (max ${ASSET_MAX_BYTES})`,
2080
+ hint: `Single asset exceeds the ${ASSET_MAX_BYTES / 1024 / 1024} MB per-file cap. Trim or move large fixtures to an external Git repo.`
2081
+ });
2082
+ }
2083
+ totalBytes += byteLen;
2084
+ }
2085
+ if (totalBytes > BUNDLE_MAX_BYTES) {
2086
+ flags.push({
2087
+ code: "bundle_too_large",
2088
+ message: `bundle totals ${totalBytes} bytes (max ${BUNDLE_MAX_BYTES})`,
2089
+ hint: `Total bundle exceeds ${BUNDLE_MAX_BYTES / 1024 / 1024} MB. Remove generated artifacts; check .gitignore parity.`
2090
+ });
2091
+ }
2092
+ return flags;
2093
+ }
2094
+ async function runDryRun(args) {
2095
+ const { content, assets } = args;
2096
+ const v = validatePackageSource(content);
2097
+ const assetPathResult = assets.length > 0 ? validateAssets(assets) : { ok: true, flags: [] };
2098
+ const sizeFlags = checkAssetSizesLocal(assets);
2099
+ const allFlags = [...v.flags, ...assetPathResult.flags, ...sizeFlags];
2100
+ const ok = v.ok && assetPathResult.ok && sizeFlags.length === 0;
2101
+ printDual({
2102
+ json: {
2103
+ status: ok ? "dry_run_passed" : "dry_run_failed",
2104
+ type: v.type,
2105
+ tier: v.tier,
2106
+ capabilities: v.capabilities,
2107
+ asset_count: assets.length,
2108
+ flags: allFlags,
2109
+ note: "dry-run validates frontmatter + asset paths/sizes locally; AI review (real publish only) is not performed"
2110
+ },
2111
+ human: () => {
2112
+ if (ok) {
2113
+ printHuman(c.green("\u2713 dry-run validation passed"));
2114
+ if (v.type) printHuman(c.gray(` type: ${v.type} (tier ${v.tier})`));
2115
+ if (v.capabilities) {
2116
+ printHuman(
2117
+ c.gray(
2118
+ ` capabilities: jit_eligible=${v.capabilities.jit_eligible} executable=${v.capabilities.executable}`
2119
+ )
2120
+ );
2121
+ }
2122
+ if (assets.length > 0) printHuman(c.gray(` + ${assets.length} bundled file(s)`));
2123
+ printHuman(
2124
+ c.gray(" (AI review only runs on the real push; not performed here)")
2125
+ );
2126
+ return;
2127
+ }
2128
+ printHuman(c.red(`\u2717 dry-run validation failed (${allFlags.length} flag(s))`));
2129
+ for (const f of allFlags) {
2130
+ printHuman(c.red(` - ${f.code}${f.message ? `: ${f.message}` : ""}`));
2131
+ if (f.hint) {
2132
+ for (const line of f.hint.split("\n")) printHuman(c.gray(` \u2192 ${line}`));
2133
+ }
2134
+ }
2135
+ }
2136
+ });
2137
+ if (!ok) process.exit(30);
2138
+ }
2139
+ function ensureTypeField(content, defaultType) {
2140
+ const fmMatch = /^---\r?\n([\s\S]*?)\r?\n---/.exec(content);
2141
+ if (!fmMatch) return content;
2142
+ const fmBlock = fmMatch[1];
2143
+ if (/^type\s*:/m.test(fmBlock)) return content;
2144
+ const injected = fmMatch[0].replace(/^---\r?\n/, `---
2145
+ type: ${defaultType}
2146
+ `);
2147
+ return content.replace(fmMatch[0], injected);
2148
+ }
2149
+ var EXCLUDE_NAMES = /* @__PURE__ */ new Set([".DS_Store", "Thumbs.db", ".gitkeep"]);
2150
+ var EXCLUDE_DIRS = /* @__PURE__ */ new Set(["__pycache__", "node_modules", ".git", "evals", "dist", ".turbo"]);
2151
+ var EXCLUDE_EXTS = /* @__PURE__ */ new Set([".pyc", ".pyo", ".rbc"]);
2152
+ function shouldExclude(relPath, isDir) {
2153
+ const parts = relPath.split(posix.sep);
2154
+ const name = parts[parts.length - 1];
2155
+ if (EXCLUDE_NAMES.has(name)) return true;
2156
+ if (isDir && EXCLUDE_DIRS.has(name)) return true;
2157
+ const ext = name.includes(".") ? "." + name.split(".").pop() : "";
2158
+ if (EXCLUDE_EXTS.has(ext)) return true;
2159
+ return false;
2160
+ }
2161
+ async function walkDir(base, relDir, collected) {
2162
+ const entries = await readdir(join4(base, relDir), { withFileTypes: true });
2163
+ for (const entry of entries) {
2164
+ const rel = relDir ? `${relDir}/${entry.name}` : entry.name;
2165
+ if (shouldExclude(rel, entry.isDirectory())) continue;
2166
+ if (entry.isDirectory()) {
2167
+ await walkDir(base, rel, collected);
2168
+ } else if (entry.isFile()) {
2169
+ collected.push({ rel, abs: join4(base, rel) });
2170
+ }
2171
+ }
2172
+ }
2173
+ async function readAsset(absPath) {
2174
+ const buf = await readFile3(absPath);
2175
+ const text = buf.toString("utf8");
2176
+ const roundtrip = Buffer.from(text, "utf8");
2177
+ if (roundtrip.equals(buf)) {
2178
+ return { content: text, encoding: "utf8" };
2179
+ }
2180
+ const b64 = buf.toString("base64");
2181
+ printHuman(c.gray(` \u2192 encoding ${absPath.split(sep).pop()} as base64`));
2182
+ return { content: b64, encoding: "base64" };
2183
+ }
2184
+ async function bundleDirectory(dir) {
2185
+ const all = [];
2186
+ await walkDir(dir, "", all);
2187
+ const skillEntry = all.find((f) => f.rel === "SKILL.md");
2188
+ if (!skillEntry) {
2189
+ throw new Error(`No SKILL.md found in directory ${dir}. Push a directory that has SKILL.md at its root.`);
2190
+ }
2191
+ const content = await readFile3(skillEntry.abs, "utf8");
2192
+ const assets = [];
2193
+ for (const f of all) {
2194
+ if (f.rel === "SKILL.md") continue;
2195
+ const { content: fc, encoding } = await readAsset(f.abs);
2196
+ assets.push({ path: f.rel, content: fc, encoding });
2197
+ }
2198
+ return { content, assets };
2199
+ }
2200
+ async function bundleSkillZip(zipPath) {
2201
+ const buf = await readFile3(zipPath);
2202
+ const zip = await JSZip.loadAsync(buf);
2203
+ const entries = Object.keys(zip.files).filter((k) => !zip.files[k].dir);
2204
+ if (entries.length === 0) throw new Error(".skill file is empty");
2205
+ const firstParts = entries[0].split("/");
2206
+ const prefix = firstParts.length > 1 ? firstParts[0] + "/" : "";
2207
+ const stripped = entries.map((k) => ({
2208
+ zipKey: k,
2209
+ rel: prefix ? k.slice(prefix.length) : k
2210
+ })).filter((f) => f.rel.length > 0 && !shouldExclude(f.rel, false));
2211
+ const skillEntry = stripped.find((f) => f.rel === "SKILL.md");
2212
+ if (!skillEntry) {
2213
+ throw new Error(`.skill ZIP has no SKILL.md at its root (prefix="${prefix}")`);
2214
+ }
2215
+ const content = await zip.files[skillEntry.zipKey].async("string");
2216
+ const assets = [];
2217
+ for (const f of stripped) {
2218
+ if (f.rel === "SKILL.md") continue;
2219
+ const rawBuf = await zip.files[f.zipKey].async("uint8array");
2220
+ const buf2 = Buffer.from(rawBuf);
2221
+ const text = buf2.toString("utf8");
2222
+ const roundtrip = Buffer.from(text, "utf8");
2223
+ if (roundtrip.equals(buf2)) {
2224
+ assets.push({ path: f.rel, content: text, encoding: "utf8" });
2225
+ } else {
2226
+ printHuman(c.gray(` \u2192 encoding ${f.rel} as base64`));
2227
+ assets.push({ path: f.rel, content: buf2.toString("base64"), encoding: "base64" });
2228
+ }
2229
+ }
2230
+ return { content, assets };
2231
+ }
2232
+
2233
+ // src/commands/pull.ts
2234
+ import { Command as Command5 } from "commander";
2235
+ import { mkdir as mkdir4, writeFile as writeFile4 } from "fs/promises";
2236
+ import { dirname as dirname2, join as join6 } from "path";
2108
2237
 
2109
2238
  // ../../packages/translators/src/skill/identity.ts
2110
2239
  function buildSkillFile(input, outputPath) {
@@ -3290,6 +3419,11 @@ function outdatedCommand() {
3290
3419
  const outdatedCount = entries.filter((e) => e.outdated).length;
3291
3420
  if (outdatedCount === 0) {
3292
3421
  printHuman(c.green(`\u2713 All ${entries.length} package${entries.length !== 1 ? "s" : ""} are up to date`));
3422
+ printHuman(
3423
+ c.gray(
3424
+ " \u2192 bundled Kordis sub-skills update with the CLI: `npm i -g @rayul/kordis@latest`"
3425
+ )
3426
+ );
3293
3427
  return;
3294
3428
  }
3295
3429
  printHuman(
@@ -3305,6 +3439,11 @@ function outdatedCommand() {
3305
3439
  }
3306
3440
  printHuman("");
3307
3441
  printHuman(c.gray("Run `kordis update` to upgrade all outdated packages."));
3442
+ printHuman(
3443
+ c.gray(
3444
+ " \u2192 only `kordis pull`-installed packages are tracked here. Bundled sub-skills update via `npm i -g @rayul/kordis@latest`."
3445
+ )
3446
+ );
3308
3447
  }
3309
3448
  });
3310
3449
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rayul/kordis",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "private": false,
5
5
  "description": "Command-line client for the Kordis registry — discover, install, and publish agentic-coding packages.",
6
6
  "license": "MIT",