@rayul/kordis 0.2.0 → 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 +351 -205
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1172,6 +1172,8 @@ import ora from "ora";
1172
1172
  var jsonMode = false;
1173
1173
  function setJsonMode(on) {
1174
1174
  jsonMode = on;
1175
+ if (on) process.env["KORDIS_JSON"] = "1";
1176
+ else delete process.env["KORDIS_JSON"];
1175
1177
  }
1176
1178
  function isJsonMode() {
1177
1179
  return jsonMode;
@@ -1340,11 +1342,31 @@ function searchCommand() {
1340
1342
  limit: opts.limit
1341
1343
  });
1342
1344
  }
1345
+ const queryProvided = query.length > 0;
1343
1346
  printDual({
1344
1347
  json: res,
1345
1348
  human: () => {
1346
1349
  if (res.results.length === 0) {
1347
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
+ }
1348
1370
  return;
1349
1371
  }
1350
1372
  for (const p of res.results) {
@@ -1366,6 +1388,13 @@ function searchCommand() {
1366
1388
  if (res.next_cursor) {
1367
1389
  printHuman(c.gray(`(more available \u2014 pass --cursor ${res.next_cursor})`));
1368
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
+ }
1369
1398
  }
1370
1399
  });
1371
1400
  });
@@ -1425,210 +1454,6 @@ var FrontmatterParseError = class extends Error {
1425
1454
  }
1426
1455
  };
1427
1456
 
1428
- // src/commands/push.ts
1429
- function pushCommand() {
1430
- 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(
1431
- "--slug <slug>",
1432
- 'override slug; accepts "user/slug" to point at a specific package'
1433
- ).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(
1434
- async (path, opts) => {
1435
- const bundle = await useClient();
1436
- const abs = resolve(process.cwd(), path);
1437
- const s = await stat(abs);
1438
- let content;
1439
- let assets = [];
1440
- if (s.isDirectory()) {
1441
- ({ content, assets } = await bundleDirectory(abs));
1442
- content = ensureTypeField(content, "skill");
1443
- } else if (abs.endsWith(".skill")) {
1444
- ({ content, assets } = await bundleSkillZip(abs));
1445
- content = ensureTypeField(content, "skill");
1446
- } else {
1447
- content = await readFile3(abs, "utf8");
1448
- }
1449
- let nameFromFm = null;
1450
- try {
1451
- const fm = parseFrontmatter(content).frontmatter ?? {};
1452
- const v = fm["name"];
1453
- if (typeof v === "string") nameFromFm = v;
1454
- } catch (err) {
1455
- throw err instanceof Error ? err : new Error(String(err));
1456
- }
1457
- let targetUser = null;
1458
- let targetSlug = null;
1459
- if (opts.slug && opts.slug.includes("/")) {
1460
- const [u, s2] = opts.slug.split("/", 2);
1461
- if (u && s2) {
1462
- targetUser = u;
1463
- targetSlug = s2;
1464
- }
1465
- } else {
1466
- if (bundle.username && (opts.slug || nameFromFm)) {
1467
- targetUser = bundle.username;
1468
- targetSlug = opts.slug ?? nameFromFm;
1469
- }
1470
- }
1471
- let targetFullSlug;
1472
- if (targetUser && targetSlug) {
1473
- try {
1474
- await bundle.api.getPackage(targetUser, targetSlug);
1475
- targetFullSlug = `${targetUser}/${targetSlug}`;
1476
- } catch (err) {
1477
- if (err instanceof KordisApiError && err.code === "not_found") {
1478
- targetFullSlug = void 0;
1479
- } else if (!(err instanceof KordisApiError)) {
1480
- throw err;
1481
- }
1482
- }
1483
- }
1484
- const spin = spinner(
1485
- opts.dryRun ? "Validating" : targetFullSlug ? `Publishing update to ${targetFullSlug}` : "Publishing"
1486
- );
1487
- const res = await bundle.authedRequest(
1488
- (api) => api.publish(
1489
- {
1490
- content,
1491
- assets: assets.length > 0 ? assets : void 0,
1492
- bump: opts.bump,
1493
- slug: opts.slug && !opts.slug.includes("/") ? opts.slug : void 0,
1494
- target_full_slug: targetFullSlug,
1495
- type: opts.type,
1496
- changelog: opts.changelog,
1497
- dry_run: !!opts.dryRun
1498
- },
1499
- {
1500
- waitForReviewMs: opts.wait === false ? 0 : 3e3
1501
- }
1502
- )
1503
- );
1504
- spin.succeed();
1505
- printDual({
1506
- json: res,
1507
- human: () => {
1508
- printHuman(
1509
- `${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")}`
1510
- );
1511
- if (assets.length > 0) {
1512
- printHuman(c.gray(` + ${assets.length} bundled file(s)`));
1513
- }
1514
- printHuman(` ${c.cyan(res.url)}`);
1515
- if (res.status === "pending") {
1516
- printHuman(
1517
- c.yellow(
1518
- ` ! AI review is still pending \u2014 re-check with \`kordis info ${res.full_slug} --json\` for the verdict.`
1519
- )
1520
- );
1521
- }
1522
- if (res.review.flags.length > 0) {
1523
- printHuman(c.yellow(` ${res.review.flags.length} review flag(s):`));
1524
- for (const f of res.review.flags) {
1525
- printHuman(c.yellow(` - ${f.code}${f.message ? `: ${f.message}` : ""}`));
1526
- }
1527
- }
1528
- }
1529
- });
1530
- }
1531
- );
1532
- }
1533
- function ensureTypeField(content, defaultType) {
1534
- const fmMatch = /^---\r?\n([\s\S]*?)\r?\n---/.exec(content);
1535
- if (!fmMatch) return content;
1536
- const fmBlock = fmMatch[1];
1537
- if (/^type\s*:/m.test(fmBlock)) return content;
1538
- const injected = fmMatch[0].replace(/^---\r?\n/, `---
1539
- type: ${defaultType}
1540
- `);
1541
- return content.replace(fmMatch[0], injected);
1542
- }
1543
- var EXCLUDE_NAMES = /* @__PURE__ */ new Set([".DS_Store", "Thumbs.db", ".gitkeep"]);
1544
- var EXCLUDE_DIRS = /* @__PURE__ */ new Set(["__pycache__", "node_modules", ".git", "evals", "dist", ".turbo"]);
1545
- var EXCLUDE_EXTS = /* @__PURE__ */ new Set([".pyc", ".pyo", ".rbc"]);
1546
- function shouldExclude(relPath, isDir) {
1547
- const parts = relPath.split(posix.sep);
1548
- const name = parts[parts.length - 1];
1549
- if (EXCLUDE_NAMES.has(name)) return true;
1550
- if (isDir && EXCLUDE_DIRS.has(name)) return true;
1551
- const ext = name.includes(".") ? "." + name.split(".").pop() : "";
1552
- if (EXCLUDE_EXTS.has(ext)) return true;
1553
- return false;
1554
- }
1555
- async function walkDir(base, relDir, collected) {
1556
- const entries = await readdir(join4(base, relDir), { withFileTypes: true });
1557
- for (const entry of entries) {
1558
- const rel = relDir ? `${relDir}/${entry.name}` : entry.name;
1559
- if (shouldExclude(rel, entry.isDirectory())) continue;
1560
- if (entry.isDirectory()) {
1561
- await walkDir(base, rel, collected);
1562
- } else if (entry.isFile()) {
1563
- collected.push({ rel, abs: join4(base, rel) });
1564
- }
1565
- }
1566
- }
1567
- async function readAsset(absPath) {
1568
- const buf = await readFile3(absPath);
1569
- const text = buf.toString("utf8");
1570
- const roundtrip = Buffer.from(text, "utf8");
1571
- if (roundtrip.equals(buf)) {
1572
- return { content: text, encoding: "utf8" };
1573
- }
1574
- const b64 = buf.toString("base64");
1575
- printHuman(c.gray(` \u2192 encoding ${absPath.split(sep).pop()} as base64`));
1576
- return { content: b64, encoding: "base64" };
1577
- }
1578
- async function bundleDirectory(dir) {
1579
- const all = [];
1580
- await walkDir(dir, "", all);
1581
- const skillEntry = all.find((f) => f.rel === "SKILL.md");
1582
- if (!skillEntry) {
1583
- throw new Error(`No SKILL.md found in directory ${dir}. Push a directory that has SKILL.md at its root.`);
1584
- }
1585
- const content = await readFile3(skillEntry.abs, "utf8");
1586
- const assets = [];
1587
- for (const f of all) {
1588
- if (f.rel === "SKILL.md") continue;
1589
- const { content: fc, encoding } = await readAsset(f.abs);
1590
- assets.push({ path: f.rel, content: fc, encoding });
1591
- }
1592
- return { content, assets };
1593
- }
1594
- async function bundleSkillZip(zipPath) {
1595
- const buf = await readFile3(zipPath);
1596
- const zip = await JSZip.loadAsync(buf);
1597
- const entries = Object.keys(zip.files).filter((k) => !zip.files[k].dir);
1598
- if (entries.length === 0) throw new Error(".skill file is empty");
1599
- const firstParts = entries[0].split("/");
1600
- const prefix = firstParts.length > 1 ? firstParts[0] + "/" : "";
1601
- const stripped = entries.map((k) => ({
1602
- zipKey: k,
1603
- rel: prefix ? k.slice(prefix.length) : k
1604
- })).filter((f) => f.rel.length > 0 && !shouldExclude(f.rel, false));
1605
- const skillEntry = stripped.find((f) => f.rel === "SKILL.md");
1606
- if (!skillEntry) {
1607
- throw new Error(`.skill ZIP has no SKILL.md at its root (prefix="${prefix}")`);
1608
- }
1609
- const content = await zip.files[skillEntry.zipKey].async("string");
1610
- const assets = [];
1611
- for (const f of stripped) {
1612
- if (f.rel === "SKILL.md") continue;
1613
- const rawBuf = await zip.files[f.zipKey].async("uint8array");
1614
- const buf2 = Buffer.from(rawBuf);
1615
- const text = buf2.toString("utf8");
1616
- const roundtrip = Buffer.from(text, "utf8");
1617
- if (roundtrip.equals(buf2)) {
1618
- assets.push({ path: f.rel, content: text, encoding: "utf8" });
1619
- } else {
1620
- printHuman(c.gray(` \u2192 encoding ${f.rel} as base64`));
1621
- assets.push({ path: f.rel, content: buf2.toString("base64"), encoding: "base64" });
1622
- }
1623
- }
1624
- return { content, assets };
1625
- }
1626
-
1627
- // src/commands/pull.ts
1628
- import { Command as Command5 } from "commander";
1629
- import { mkdir as mkdir4, writeFile as writeFile4 } from "fs/promises";
1630
- import { dirname as dirname2, join as join6 } from "path";
1631
-
1632
1457
  // ../../packages/harness-schema/src/deps.ts
1633
1458
  init_src();
1634
1459
  import { z as z10 } from "zod";
@@ -2103,11 +1928,317 @@ function zodFail(issues) {
2103
1928
  })
2104
1929
  };
2105
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";
2106
2237
 
2107
2238
  // ../../packages/translators/src/skill/identity.ts
2108
2239
  function buildSkillFile(input, outputPath) {
2109
2240
  const body = input.body.trimStart();
2110
- const fm = input.frontmatter;
2241
+ const fm = input.frontmatter["version"] === void 0 && input.version ? { ...input.frontmatter, version: input.version } : input.frontmatter;
2111
2242
  const name = typeof fm["name"] === "string" ? fm["name"] : input.fullSlug.split("/")[1];
2112
2243
  const frontmatter = ["---"];
2113
2244
  for (const [k, v] of Object.entries(fm)) {
@@ -3288,6 +3419,11 @@ function outdatedCommand() {
3288
3419
  const outdatedCount = entries.filter((e) => e.outdated).length;
3289
3420
  if (outdatedCount === 0) {
3290
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
+ );
3291
3427
  return;
3292
3428
  }
3293
3429
  printHuman(
@@ -3303,6 +3439,11 @@ function outdatedCommand() {
3303
3439
  }
3304
3440
  printHuman("");
3305
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
+ );
3306
3447
  }
3307
3448
  });
3308
3449
  });
@@ -3493,6 +3634,11 @@ function diffCommand() {
3493
3634
  printHuman("");
3494
3635
  if (!diff.unified_diff) {
3495
3636
  printHuman(c.dim("(no content changes between these versions)"));
3637
+ printHuman(
3638
+ c.dim(
3639
+ " \u2192 likely a metadata-only bump: description, deps, license, or version field"
3640
+ )
3641
+ );
3496
3642
  return;
3497
3643
  }
3498
3644
  for (const line of diff.unified_diff.split("\n")) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rayul/kordis",
3
- "version": "0.2.0",
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",