@rayul/kordis 0.1.9 → 0.1.11

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.
@@ -14,10 +14,18 @@ Publish a valid, AI-review-passing version. ALWAYS dry-run first.
14
14
  - `name` — must equal slug, lowercase-with-hyphens.
15
15
  - `description` — 1–2 sentences, slightly **pushy** ("Use whenever the
16
16
  user…"). Claude under-triggers without pushiness.
17
- - `version` — semver.
17
+ - `version` — semver. **Behaviour differs by path** (see below).
18
18
  - `license` — SPDX (e.g. `MIT`, `Apache-2.0`, `CC-BY-4.0`).
19
19
  - Optional: `homepage`, `tags`, `type`.
20
20
 
21
+ **`version` field handling:**
22
+ - **First publish** — used as-is (else defaults to `0.1.0`).
23
+ - **Update** — the registry computes the next version from
24
+ `current_version + --bump <strategy>` (default `patch`). If the file's
25
+ `version` differs from that result, the API rejects with `usage`. Either
26
+ drop the `version` field on updates, set `--bump` to match, or align
27
+ them.
28
+
21
29
  If the body contains third-party content, MUST preserve the original copyright
22
30
  and license text inside the body and link to source via `homepage`.
23
31
 
@@ -40,7 +48,14 @@ and license text inside the body and link to source via `homepage`.
40
48
 
41
49
  ### 1. Dry-run
42
50
  `kordis push <file> --dry-run --json`. Returns the same validation, conflict,
43
- and AI-review verdicts a real push would produce.
51
+ and version-resolution verdicts a real push would produce.
52
+
53
+ CAVEAT: dry-run does NOT exercise the AI reviewer — it short-circuits with
54
+ a synthetic `review.result: "passed"` (`prompt_version: "stub-v0"`). Treat
55
+ dry-run as syntactic + structural validation only. A real push routes
56
+ through the AI review worker, which can land terminal `passed` /
57
+ `rejected`, OR leave the publish in `status: "pending"` if the worker
58
+ hasn't decided within the wait window.
44
59
 
45
60
  **Success criteria**: dry-run returns a `status: "published"`-shaped envelope
46
61
  with no error flags.
@@ -51,7 +66,8 @@ Possible verdicts:
51
66
  - `conflict` (exit 40) — slug taken; suggest a different slug (or pass
52
67
  `--slug` if first publish).
53
68
  - `ai_review_rejected` (exit 32) — EXPLAIN the rejection to the user. Do NOT
54
- silently retry.
69
+ silently retry. Includes deterministic Stage-1 hits like
70
+ `curl_pipe_shell`, `aws_access_key`, `private_key_block`, etc.
55
71
  - `rate_limited` (exit 31) — wait `retry_after_seconds`.
56
72
  - `not_authenticated` (exit 10) — `kordis login`, then retry.
57
73
 
@@ -61,8 +77,12 @@ Possible verdicts:
61
77
  `kordis push <file> --json`. Add `--bump minor` (or `major`) when shipping a
62
78
  non-patch update.
63
79
 
64
- **Success criteria**: returns `{full_slug, version, url}`. Show all three to
65
- the user.
80
+ **Success criteria**: returns `{full_slug, version, url, status}`. Show all
81
+ four to the user. If `status: "pending"`, the AI reviewer hasn't decided
82
+ yet — re-check with `kordis info <full_slug> --json` (or wait and re-run
83
+ `kordis info`); the `pending` page is not yet served on the public URL.
84
+ If `status: "rejected"`, surface `review.flags` to the user and DO NOT
85
+ silently retry — explain why.
66
86
 
67
87
  ## Examples
68
88
 
@@ -76,7 +96,10 @@ User: "publish my new skill"
76
96
 
77
97
  <example>
78
98
  User: "release v0.2.0 of <full_slug>"
79
- 1. update `version: 0.2.0` in the file's frontmatter
99
+ - previous version was 0.1.x
100
+ 1. drop or align the frontmatter `version` field — `--bump minor` from
101
+ 0.1.x will yield 0.2.0; if the file says `0.2.0` that matches and is OK,
102
+ if it says anything else (e.g. `0.1.0`) the API will reject
80
103
  2. kordis push <file> --bump minor --dry-run --json
81
104
  3. clean → kordis push <file> --bump minor --json
82
105
  </example>
@@ -86,3 +109,25 @@ User: "release with a note that breaking-changes the API"
86
109
  1. kordis push <file> --bump major --changelog "BREAKING: <summary>" --dry-run --json
87
110
  2. clean → drop --dry-run for the real push
88
111
  </example>
112
+
113
+ ## Forking another Kordis package
114
+
115
+ When the user asks to fork / adapt / republish someone else's package
116
+ under their own slug, do the following:
117
+
118
+ 1. **License first.** Read the upstream `license` field (`kordis info
119
+ <upstream> --json`). MIT / Apache-2.0 / BSD all permit derivatives;
120
+ CC-BY-SA forces ShareAlike (your fork must use the same license).
121
+ GPL-flavoured / proprietary → STOP and tell the user.
122
+ 2. **Carry the legal minimum.** Preserve the upstream copyright and
123
+ permission notices verbatim inside the body. Do NOT strip them.
124
+ 3. **Credit upstream.** In the new package's body, add a `Derived from
125
+ <upstream-full-slug>` line near the top, and point `homepage` at the
126
+ upstream source (the original repo, not the Kordis page — `homepage`
127
+ is for the canonical authoring source).
128
+ 4. **Pick a fresh slug.** A fork is a *new* package; do not reuse the
129
+ upstream slug. Start at `0.1.0` (or whatever the file's `version` says
130
+ on first publish).
131
+ 5. **Tag derivation in `description`.** A short " — fork of
132
+ <upstream-full-slug>" suffix is enough; users searching the fork need
133
+ to know.
@@ -59,7 +59,15 @@ array.
59
59
  - For trending results, the `trending_score` field is populated — surface it
60
60
  when comparing.
61
61
  - IMPORTANT: if results are 0, RELAX one filter at a time before giving up.
62
- Do NOT invent a slug to fill the gap.
62
+ **Drop `--tool` first** — many published packages have an empty
63
+ `tools_compatible` field, so `--tool <name>` undercounts hard. Then drop
64
+ `--type`. Then shorten the query. Do NOT invent a slug to fill the gap.
65
+ - Most packages on the registry are very young — `stars` and `downloads` are
66
+ often `0`. Lean on `review_status: passed` and the description text match,
67
+ not on social signals alone.
68
+ - For richer triage data, prefer `kordis search --json` over
69
+ `kordis info --json` — search returns `description`, `tier`,
70
+ `review_status`, `tools_compatible`, etc. per result; `info` is sparser.
63
71
  - Surface the top 3–5 with slug, type, tier, stars, one-line description.
64
72
 
65
73
  **Success criteria**: user picked one, or you reported "no fit" honestly.
@@ -34,15 +34,30 @@ Post a top-level comment on a package. Auth required.
34
34
  | `--kind <k>` | `comment` (default), `tip`, `report`, `praise`, `question` |
35
35
  | `--title <t>` | 1–140 chars; honoured for top-level posts only |
36
36
 
37
- Replies, edits, status flips (open/resolved on a `report`), and listing
38
- existing comments are NOT yet exposed in the CLI. Direct the user to
39
- `https://kordis.store/p/<full_slug>` for those interactions until verbs land.
37
+ **Pick the right `--kind`:**
38
+ - `comment` — generic remark, default when nothing more specific fits.
39
+ - `tip` — actionable advice for OTHER users ("works great if you also
40
+ pull <other-slug>"). Subject of the post is reader, not author.
41
+ - `report` — bug / problem report. Has an open/resolved status (managed on
42
+ the web today).
43
+ - `praise` — short thank-you / compliment to the author. Body is enough;
44
+ `--title` is optional.
45
+ - `question` — explicit question expecting a reply (typically from the
46
+ author). Use when the user wants a response, not just to share thought.
47
+
48
+ Replies, edits, status flips (open/resolved on a `report`), listing
49
+ existing comments, and reading back your own comments are NOT yet exposed
50
+ in the CLI. Direct the user to `https://kordis.store/p/<full_slug>` for
51
+ those interactions until verbs land.
40
52
 
41
53
  ## Star commands
42
54
 
43
55
  ### `kordis star <full_slug> [--json]`
44
56
  Mark a package as a favourite. Idempotent — re-running on an already-starred
45
- package returns the current count without a duplicate. Auth required.
57
+ package returns the current count without a duplicate. Auth required. The
58
+ response shape is the same whether the call newly starred or no-op'd; if the
59
+ user wants to verify a NEW star landed, compare `stars` from `kordis info
60
+ <full_slug> --json` before and after.
46
61
 
47
62
  ### `kordis unstar <full_slug> [--json]`
48
63
  Remove your star. Idempotent — silent no-op when not previously starred.
@@ -43,6 +43,15 @@ Writes files into the current project.
43
43
  | `--target <path>` | install into a sub-directory (default: cwd) |
44
44
  | `--with-recommends` | also install recommended dependencies |
45
45
 
46
+ IMPORTANT: `pull` **fails fast** when the target tool can't be inferred from
47
+ the cwd (no `.claude/`, `.codex/`, `.cursor/`, `AGENTS.md`, `CLAUDE.md`,
48
+ etc.). Pass `--tool <name>` explicitly when running in a project that has
49
+ not been initialised for any specific tool yet. Don't rely on the old "drop
50
+ a canonical SKILL.md at cwd" fallback — Claude Code never picked it up.
51
+
52
+ Valid `--tool` values: `claude-code`, `codex`, `cursor`, `continue`,
53
+ `windsurf`, `antigravity`, `copilot`.
54
+
46
55
  ### `kordis info <full_slug> --json`
47
56
  Inspect metadata (description, tier, deps, license, review status, …) before
48
57
  reading or pulling. ALWAYS run this for `mcp-server` / `hook` types, where the
@@ -91,6 +100,17 @@ auto-resolve silently.
91
100
  `"following <full_slug>@<version>"`). RECOMMENDED: end your reply with that
92
101
  exact line. Skip only when it would clutter a casual non-technical reply.
93
102
 
103
+ ## Bundled assets
104
+ Skills published as a `.skill` bundle or directory include `assets/`,
105
+ `references/`, `scripts/`, and `agents/` subdirectories alongside
106
+ `SKILL.md`. After `kordis pull`, these files land next to `SKILL.md` in
107
+ the tool's skill directory (e.g. `.claude/skills/<name>/assets/foo.html`)
108
+ and are directly readable by the agent.
109
+
110
+ If a skill body references a helper file and the file isn't present after
111
+ pull, the package was published as a single-file skill — follow its
112
+ `homepage` to the upstream source.
113
+
94
114
  ## Examples
95
115
 
96
116
  <example>
package/dist/index.js CHANGED
@@ -459,7 +459,7 @@ var init_auth = __esm({
459
459
 
460
460
  // ../../packages/contracts/src/api.ts
461
461
  import { z as z9 } from "zod";
462
- var SearchQuerySchema, SearchResponseSchema, TrendingQuerySchema, RisingQuerySchema, StarResponseSchema, PublishPackageRequestSchema, PublishWaitQuerySchema, PublishPackageResponseSchema, PackagePathParamsSchema, ContentQuerySchema, PackageContentResponseSchema, JitReadRequestSchema, JitReadResponseSchema, PullPackageResponseSchema, ListVersionsResponseSchema, VersionDiffQuerySchema, ListCommentsQuerySchema, ListCommentsResponseSchema, UserProfileResponseSchema, UserPackagesQuerySchema;
462
+ var SearchQuerySchema, SearchResponseSchema, TrendingQuerySchema, RisingQuerySchema, StarResponseSchema, AssetSchema, PublishPackageRequestSchema, PublishWaitQuerySchema, PublishPackageResponseSchema, PackagePathParamsSchema, ContentQuerySchema, PackageContentResponseSchema, JitReadRequestSchema, JitReadResponseSchema, PullPackageResponseSchema, ListVersionsResponseSchema, VersionDiffQuerySchema, ListCommentsQuerySchema, ListCommentsResponseSchema, UserProfileResponseSchema, UserPackagesQuerySchema;
463
463
  var init_api = __esm({
464
464
  "../../packages/contracts/src/api.ts"() {
465
465
  "use strict";
@@ -498,6 +498,11 @@ var init_api = __esm({
498
498
  starred: z9.boolean(),
499
499
  star_count: z9.number().int().nonnegative()
500
500
  });
501
+ AssetSchema = z9.object({
502
+ path: z9.string().min(1).max(255),
503
+ content: z9.string(),
504
+ encoding: z9.enum(["utf8", "base64"]).default("utf8")
505
+ });
501
506
  PublishPackageRequestSchema = z9.object({
502
507
  /**
503
508
  * Raw source content — YAML frontmatter + markdown body. Server parses and
@@ -528,7 +533,12 @@ var init_api = __esm({
528
533
  /**
529
534
  * Skip draft validation (preview mode) — only allowed in dev builds.
530
535
  */
531
- dry_run: z9.boolean().optional()
536
+ dry_run: z9.boolean().optional(),
537
+ /**
538
+ * Bundled files beyond SKILL.md: assets, references, scripts, agents, etc.
539
+ * Max 50 entries; max 2 MB per file; max 5 MB total bundle.
540
+ */
541
+ assets: z9.array(AssetSchema).max(50).optional()
532
542
  });
533
543
  PublishWaitQuerySchema = z9.object({
534
544
  /**
@@ -574,8 +584,9 @@ var init_api = __esm({
574
584
  version: SemverSchema,
575
585
  format: z9.string(),
576
586
  content: z9.string(),
577
- notes: z9.array(z9.string()).default([])
587
+ notes: z9.array(z9.string()).default([]),
578
588
  // translator warnings, e.g. ".mdc → Claude: @filepath unsupported"
589
+ assets: z9.array(AssetSchema).default([])
579
590
  });
580
591
  JitReadRequestSchema = z9.object({
581
592
  /**
@@ -1004,6 +1015,7 @@ import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2, unlink
1004
1015
  import { homedir as homedir2 } from "os";
1005
1016
  import { join as join2 } from "path";
1006
1017
  var SERVICE = "kordis";
1018
+ var plaintextWarningEmitted = false;
1007
1019
  async function loadKeytar() {
1008
1020
  try {
1009
1021
  const mod = await import("keytar");
@@ -1031,6 +1043,16 @@ async function createKeychain() {
1031
1043
  }
1032
1044
  };
1033
1045
  }
1046
+ if (!plaintextWarningEmitted && process.env["KORDIS_JSON"] !== "1") {
1047
+ plaintextWarningEmitted = true;
1048
+ const yellow = "\x1B[33m";
1049
+ const reset = "\x1B[0m";
1050
+ process.stderr.write(
1051
+ `${yellow}[kordis] WARNING: OS keychain unavailable; falling back to plaintext token storage at ~/.kordis/.tokens.json (mode 0600).
1052
+ [kordis] On macOS install Xcode Command Line Tools, on Linux install libsecret-1-dev to enable keytar.${reset}
1053
+ `
1054
+ );
1055
+ }
1034
1056
  const store = await readAll();
1035
1057
  return {
1036
1058
  async save(username, tokens) {
@@ -1321,8 +1343,9 @@ function searchCommand() {
1321
1343
 
1322
1344
  // src/commands/push.ts
1323
1345
  import { Command as Command4 } from "commander";
1324
- import { readFile as readFile3 } from "fs/promises";
1325
- import { resolve } from "path";
1346
+ import { readFile as readFile3, stat, readdir } from "fs/promises";
1347
+ import { resolve, join as join4, posix, sep } from "path";
1348
+ import JSZip from "jszip";
1326
1349
 
1327
1350
  // ../../packages/harness-schema/src/parser.ts
1328
1351
  import { parse as parseYaml } from "yaml";
@@ -1366,21 +1389,30 @@ var FrontmatterParseError = class extends Error {
1366
1389
 
1367
1390
  // src/commands/push.ts
1368
1391
  function pushCommand() {
1369
- return new Command4("push").description("publish a new package or a new version").argument("<file>", "path to the canonical package source").option("--bump <strategy>", "patch|minor|major (update only)", "patch").option(
1392
+ 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(
1370
1393
  "--slug <slug>",
1371
1394
  'override slug; accepts "user/slug" to point at a specific package'
1372
1395
  ).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(
1373
- async (file, opts) => {
1396
+ async (path, opts) => {
1374
1397
  const bundle = await useClient();
1375
- const abs = resolve(process.cwd(), file);
1376
- const content = await readFile3(abs, "utf8");
1398
+ const abs = resolve(process.cwd(), path);
1399
+ const s = await stat(abs);
1400
+ let content;
1401
+ let assets = [];
1402
+ if (s.isDirectory()) {
1403
+ ({ content, assets } = await bundleDirectory(abs));
1404
+ } else if (abs.endsWith(".skill")) {
1405
+ ({ content, assets } = await bundleSkillZip(abs));
1406
+ } else {
1407
+ content = await readFile3(abs, "utf8");
1408
+ }
1377
1409
  let targetUser = null;
1378
1410
  let targetSlug = null;
1379
1411
  if (opts.slug && opts.slug.includes("/")) {
1380
- const [u, s] = opts.slug.split("/", 2);
1381
- if (u && s) {
1412
+ const [u, s2] = opts.slug.split("/", 2);
1413
+ if (u && s2) {
1382
1414
  targetUser = u;
1383
- targetSlug = s;
1415
+ targetSlug = s2;
1384
1416
  }
1385
1417
  } else {
1386
1418
  let nameFromFm = null;
@@ -1415,6 +1447,7 @@ function pushCommand() {
1415
1447
  (api) => api.publish(
1416
1448
  {
1417
1449
  content,
1450
+ assets: assets.length > 0 ? assets : void 0,
1418
1451
  bump: opts.bump,
1419
1452
  slug: opts.slug && !opts.slug.includes("/") ? opts.slug : void 0,
1420
1453
  target_full_slug: targetFullSlug,
@@ -1434,7 +1467,17 @@ function pushCommand() {
1434
1467
  printHuman(
1435
1468
  `${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")}`
1436
1469
  );
1470
+ if (assets.length > 0) {
1471
+ printHuman(c.gray(` + ${assets.length} bundled file(s)`));
1472
+ }
1437
1473
  printHuman(` ${c.cyan(res.url)}`);
1474
+ if (res.status === "pending") {
1475
+ printHuman(
1476
+ c.yellow(
1477
+ ` ! AI review is still pending \u2014 re-check with \`kordis info ${res.full_slug} --json\` for the verdict.`
1478
+ )
1479
+ );
1480
+ }
1438
1481
  if (res.review.flags.length > 0) {
1439
1482
  printHuman(c.yellow(` ${res.review.flags.length} review flag(s):`));
1440
1483
  for (const f of res.review.flags) {
@@ -1446,11 +1489,94 @@ function pushCommand() {
1446
1489
  }
1447
1490
  );
1448
1491
  }
1492
+ var EXCLUDE_NAMES = /* @__PURE__ */ new Set([".DS_Store", "Thumbs.db", ".gitkeep"]);
1493
+ var EXCLUDE_DIRS = /* @__PURE__ */ new Set(["__pycache__", "node_modules", ".git", "evals", "dist", ".turbo"]);
1494
+ var EXCLUDE_EXTS = /* @__PURE__ */ new Set([".pyc", ".pyo", ".rbc"]);
1495
+ function shouldExclude(relPath, isDir) {
1496
+ const parts = relPath.split(posix.sep);
1497
+ const name = parts[parts.length - 1];
1498
+ if (EXCLUDE_NAMES.has(name)) return true;
1499
+ if (isDir && EXCLUDE_DIRS.has(name)) return true;
1500
+ const ext = name.includes(".") ? "." + name.split(".").pop() : "";
1501
+ if (EXCLUDE_EXTS.has(ext)) return true;
1502
+ return false;
1503
+ }
1504
+ async function walkDir(base, relDir, collected) {
1505
+ const entries = await readdir(join4(base, relDir), { withFileTypes: true });
1506
+ for (const entry of entries) {
1507
+ const rel = relDir ? `${relDir}/${entry.name}` : entry.name;
1508
+ if (shouldExclude(rel, entry.isDirectory())) continue;
1509
+ if (entry.isDirectory()) {
1510
+ await walkDir(base, rel, collected);
1511
+ } else if (entry.isFile()) {
1512
+ collected.push({ rel, abs: join4(base, rel) });
1513
+ }
1514
+ }
1515
+ }
1516
+ async function readAsset(absPath) {
1517
+ const buf = await readFile3(absPath);
1518
+ const text = buf.toString("utf8");
1519
+ const roundtrip = Buffer.from(text, "utf8");
1520
+ if (roundtrip.equals(buf)) {
1521
+ return { content: text, encoding: "utf8" };
1522
+ }
1523
+ const b64 = buf.toString("base64");
1524
+ printHuman(c.gray(` \u2192 encoding ${absPath.split(sep).pop()} as base64`));
1525
+ return { content: b64, encoding: "base64" };
1526
+ }
1527
+ async function bundleDirectory(dir) {
1528
+ const all = [];
1529
+ await walkDir(dir, "", all);
1530
+ const skillEntry = all.find((f) => f.rel === "SKILL.md");
1531
+ if (!skillEntry) {
1532
+ throw new Error(`No SKILL.md found in directory ${dir}. Push a directory that has SKILL.md at its root.`);
1533
+ }
1534
+ const content = await readFile3(skillEntry.abs, "utf8");
1535
+ const assets = [];
1536
+ for (const f of all) {
1537
+ if (f.rel === "SKILL.md") continue;
1538
+ const { content: fc, encoding } = await readAsset(f.abs);
1539
+ assets.push({ path: f.rel, content: fc, encoding });
1540
+ }
1541
+ return { content, assets };
1542
+ }
1543
+ async function bundleSkillZip(zipPath) {
1544
+ const buf = await readFile3(zipPath);
1545
+ const zip = await JSZip.loadAsync(buf);
1546
+ const entries = Object.keys(zip.files).filter((k) => !zip.files[k].dir);
1547
+ if (entries.length === 0) throw new Error(".skill file is empty");
1548
+ const firstParts = entries[0].split("/");
1549
+ const prefix = firstParts.length > 1 ? firstParts[0] + "/" : "";
1550
+ const stripped = entries.map((k) => ({
1551
+ zipKey: k,
1552
+ rel: prefix ? k.slice(prefix.length) : k
1553
+ })).filter((f) => f.rel.length > 0 && !shouldExclude(f.rel, false));
1554
+ const skillEntry = stripped.find((f) => f.rel === "SKILL.md");
1555
+ if (!skillEntry) {
1556
+ throw new Error(`.skill ZIP has no SKILL.md at its root (prefix="${prefix}")`);
1557
+ }
1558
+ const content = await zip.files[skillEntry.zipKey].async("string");
1559
+ const assets = [];
1560
+ for (const f of stripped) {
1561
+ if (f.rel === "SKILL.md") continue;
1562
+ const rawBuf = await zip.files[f.zipKey].async("uint8array");
1563
+ const buf2 = Buffer.from(rawBuf);
1564
+ const text = buf2.toString("utf8");
1565
+ const roundtrip = Buffer.from(text, "utf8");
1566
+ if (roundtrip.equals(buf2)) {
1567
+ assets.push({ path: f.rel, content: text, encoding: "utf8" });
1568
+ } else {
1569
+ printHuman(c.gray(` \u2192 encoding ${f.rel} as base64`));
1570
+ assets.push({ path: f.rel, content: buf2.toString("base64"), encoding: "base64" });
1571
+ }
1572
+ }
1573
+ return { content, assets };
1574
+ }
1449
1575
 
1450
1576
  // src/commands/pull.ts
1451
1577
  import { Command as Command5 } from "commander";
1452
1578
  import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
1453
- import { dirname as dirname2, join as join4 } from "path";
1579
+ import { dirname as dirname2, join as join5 } from "path";
1454
1580
 
1455
1581
  // ../../packages/harness-schema/src/deps.ts
1456
1582
  init_src();
@@ -1770,7 +1896,12 @@ var BLOCKING_CODES = /* @__PURE__ */ new Set([
1770
1896
  "missing_description",
1771
1897
  "invalid_frontmatter",
1772
1898
  "content_too_short",
1773
- "content_too_long"
1899
+ "content_too_long",
1900
+ // asset-bundle codes
1901
+ "asset_path_unsafe",
1902
+ "asset_too_large",
1903
+ "bundle_too_large",
1904
+ "too_many_assets"
1774
1905
  ]);
1775
1906
  function earlyExit(firstFlag, override = {}) {
1776
1907
  return {
@@ -1853,10 +1984,11 @@ function buildSkillFile(input, outputPath) {
1853
1984
  }
1854
1985
  frontmatter.push("---");
1855
1986
  const content = frontmatter.join("\n") + "\n\n" + body;
1987
+ const resolvedPath = outputPath.replace("{name}", String(name));
1856
1988
  return {
1857
1989
  files: [
1858
1990
  {
1859
- path: outputPath.replace("{name}", String(name)),
1991
+ path: resolvedPath,
1860
1992
  content,
1861
1993
  mergeable: false,
1862
1994
  format: "markdown"
@@ -1866,17 +1998,45 @@ function buildSkillFile(input, outputPath) {
1866
1998
  format: "agent-skills"
1867
1999
  };
1868
2000
  }
1869
- var skillToClaude = (input) => buildSkillFile(input, ".claude/skills/{name}/SKILL.md");
1870
- var skillToCodex = (input) => buildSkillFile(input, ".codex/skills/{name}.md");
1871
- var skillToCursor = (input) => buildSkillFile(input, ".cursor/skills/{name}.md");
1872
- var skillToWindsurf = (input) => buildSkillFile(input, ".windsurf/skills/{name}.md");
1873
- var skillToContinue = (input) => buildSkillFile(input, ".continue/skills/{name}.md");
1874
- var skillToCopilot = (input) => buildSkillFile(input, ".github/skills/{name}.md");
1875
- var skillToGeminiCli = (input) => buildSkillFile(input, ".gemini/skills/{name}.md");
1876
- var skillToOpenCode = (input) => buildSkillFile(input, ".opencode/skills/{name}.md");
1877
- var skillToAntigravity = (input) => buildSkillFile(input, ".antigravity/skills/{name}.md");
2001
+ function buildSkillDir(input, outputPath) {
2002
+ const result = buildSkillFile(input, outputPath);
2003
+ const skillMdPath = result.files[0].path;
2004
+ const skillDir = skillMdPath.slice(0, skillMdPath.lastIndexOf("/"));
2005
+ const assetFiles = (input.assets ?? []).map((asset) => ({
2006
+ path: `${skillDir}/${asset.path}`,
2007
+ content: asset.content,
2008
+ mergeable: false,
2009
+ format: "plain",
2010
+ encoding: asset.encoding ?? "utf8"
2011
+ }));
2012
+ return {
2013
+ files: [...result.files, ...assetFiles],
2014
+ notes: result.notes,
2015
+ format: result.format
2016
+ };
2017
+ }
2018
+ function buildSkillSingleFile(input, outputPath) {
2019
+ const result = buildSkillFile(input, outputPath);
2020
+ const notes = [...result.notes];
2021
+ if (input.assets && input.assets.length > 0) {
2022
+ const tool = outputPath.split("/")[0]?.replace(".", "") ?? "this tool";
2023
+ notes.push(
2024
+ `${input.assets.length} bundled asset(s) not installed \u2014 ${tool} uses single-file skill convention. Pull with --tool claude-code to install the full bundle.`
2025
+ );
2026
+ }
2027
+ return { ...result, notes };
2028
+ }
2029
+ var skillToClaude = (input) => buildSkillDir(input, ".claude/skills/{name}/SKILL.md");
2030
+ var skillToCodex = (input) => buildSkillDir(input, ".codex/skills/{name}/SKILL.md");
2031
+ var skillToCursor = (input) => buildSkillSingleFile(input, ".cursor/skills/{name}.md");
2032
+ var skillToWindsurf = (input) => buildSkillSingleFile(input, ".windsurf/skills/{name}.md");
2033
+ var skillToContinue = (input) => buildSkillSingleFile(input, ".continue/skills/{name}.md");
2034
+ var skillToCopilot = (input) => buildSkillSingleFile(input, ".github/skills/{name}.md");
2035
+ var skillToGeminiCli = (input) => buildSkillDir(input, ".gemini/skills/{name}/SKILL.md");
2036
+ var skillToOpenCode = (input) => buildSkillSingleFile(input, ".opencode/skills/{name}.md");
2037
+ var skillToAntigravity = (input) => buildSkillSingleFile(input, ".antigravity/skills/{name}.md");
1878
2038
  var skillCanonical = (input) => {
1879
- const res = buildSkillFile(input, "SKILL.md");
2039
+ const res = buildSkillDir(input, "SKILL.md");
1880
2040
  res.format = "agent-skills-canonical";
1881
2041
  return res;
1882
2042
  };
@@ -2255,6 +2415,11 @@ function pullCommand() {
2255
2415
  const userCwd = process.env["INIT_CWD"] ?? process.cwd();
2256
2416
  const installRoot = opts.target ?? userCwd;
2257
2417
  const detected = opts.tool ?? await detectTool(installRoot);
2418
+ if (!detected) {
2419
+ throw new Error(
2420
+ "Cannot detect target tool from the current directory.\n Pass --tool <name> explicitly. Valid: claude-code, codex, cursor,\n continue, windsurf, antigravity, copilot.\n Or run `kordis pull` inside a project that already has one of:\n .claude/, .codex/, .cursor/, AGENTS.md, CLAUDE.md, .cursorrules,\n .continue/, .windsurf/, .antigravity/, .github/copilot-instructions.md"
2421
+ );
2422
+ }
2258
2423
  const spin = spinner(`Pulling ${fullSlug}`);
2259
2424
  const detail = await bundle.api.getPackage(username, slug);
2260
2425
  await bundle.api.pull(username, slug);
@@ -2268,19 +2433,20 @@ function pullCommand() {
2268
2433
  }
2269
2434
  const written = [];
2270
2435
  const notes = [];
2271
- const format = detected ?? "canonical";
2436
+ const format = detected;
2272
2437
  try {
2273
2438
  const translator = getTranslator(parsed.type, format);
2274
2439
  const result = translator({
2275
2440
  fullSlug,
2276
2441
  frontmatter: parsed.frontmatter.data,
2277
2442
  body: parsed.body,
2278
- version: contentRes.version
2443
+ version: contentRes.version,
2444
+ assets: contentRes.assets
2279
2445
  });
2280
2446
  for (const f of result.files) {
2281
- const abs = join4(installRoot, f.path);
2447
+ const abs = join5(installRoot, f.path);
2282
2448
  await mkdir3(dirname2(abs), { recursive: true });
2283
- await writeFile3(abs, f.content, "utf8");
2449
+ await writeEmittedFile(abs, f.content, f.encoding);
2284
2450
  written.push(abs);
2285
2451
  }
2286
2452
  notes.push(...result.notes);
@@ -2291,15 +2457,16 @@ function pullCommand() {
2291
2457
  fullSlug,
2292
2458
  frontmatter: parsed.frontmatter.data,
2293
2459
  body: parsed.body,
2294
- version: contentRes.version
2460
+ version: contentRes.version,
2461
+ assets: contentRes.assets
2295
2462
  });
2296
2463
  notes.push(
2297
2464
  `Tool "${format}" is not supported for type "${parsed.type}"; installed canonical form instead.`
2298
2465
  );
2299
2466
  for (const f of result.files) {
2300
- const abs = join4(installRoot, f.path);
2467
+ const abs = join5(installRoot, f.path);
2301
2468
  await mkdir3(dirname2(abs), { recursive: true });
2302
- await writeFile3(abs, f.content, "utf8");
2469
+ await writeEmittedFile(abs, f.content, f.encoding);
2303
2470
  written.push(abs);
2304
2471
  }
2305
2472
  } else {
@@ -2327,7 +2494,7 @@ function pullCommand() {
2327
2494
  );
2328
2495
  }
2329
2496
  async function detectTool(root) {
2330
- const { stat } = await import("fs/promises");
2497
+ const { stat: stat2 } = await import("fs/promises");
2331
2498
  const checks = [
2332
2499
  [".claude", "claude-code"],
2333
2500
  [".cursor", "cursor"],
@@ -2343,13 +2510,20 @@ async function detectTool(root) {
2343
2510
  ];
2344
2511
  for (const [path, tool] of checks) {
2345
2512
  try {
2346
- await stat(join4(root, path));
2513
+ await stat2(join5(root, path));
2347
2514
  return tool;
2348
2515
  } catch {
2349
2516
  }
2350
2517
  }
2351
2518
  return null;
2352
2519
  }
2520
+ async function writeEmittedFile(abs, content, encoding) {
2521
+ if (encoding === "base64") {
2522
+ await writeFile3(abs, Buffer.from(content, "base64"));
2523
+ } else {
2524
+ await writeFile3(abs, content, "utf8");
2525
+ }
2526
+ }
2353
2527
 
2354
2528
  // src/commands/read.ts
2355
2529
  import { Command as Command6 } from "commander";
@@ -2449,7 +2623,7 @@ function logoutCommand() {
2449
2623
  // src/commands/setup.ts
2450
2624
  import { readFile as readFile4, writeFile as writeFile4, mkdir as mkdir4, access } from "fs/promises";
2451
2625
  import { homedir as homedir3 } from "os";
2452
- import { join as join5, dirname as dirname3 } from "path";
2626
+ import { join as join6, dirname as dirname3 } from "path";
2453
2627
  import { fileURLToPath as fileURLToPath2 } from "url";
2454
2628
  import { Command as Command9 } from "commander";
2455
2629
  var MARKER_START = "<!-- kordis-start -->";
@@ -2482,18 +2656,18 @@ function assetRoots() {
2482
2656
  const __dir2 = dirname3(fileURLToPath2(import.meta.url));
2483
2657
  return {
2484
2658
  // Built binary: bin/kordis.js → assets/ is one level up
2485
- bundled: join5(__dir2, "..", "assets"),
2659
+ bundled: join6(__dir2, "..", "assets"),
2486
2660
  // Dev mode: src/commands/setup.ts → 4 levels up to repo root, then packages/skills/
2487
- source: join5(__dir2, "..", "..", "..", "..", "packages", "skills")
2661
+ source: join6(__dir2, "..", "..", "..", "..", "packages", "skills")
2488
2662
  };
2489
2663
  }
2490
2664
  async function pickAssetRoot() {
2491
2665
  const { bundled, source } = assetRoots();
2492
- return await exists(join5(bundled, "KORDIS.md")) ? bundled : source;
2666
+ return await exists(join6(bundled, "KORDIS.md")) ? bundled : source;
2493
2667
  }
2494
2668
  async function loadHubContent() {
2495
2669
  const root = await pickAssetRoot();
2496
- const raw = await readFile4(join5(root, "KORDIS.md"), "utf-8");
2670
+ const raw = await readFile4(join6(root, "KORDIS.md"), "utf-8");
2497
2671
  if (raw.startsWith("---")) {
2498
2672
  const end = raw.indexOf("\n---", 4);
2499
2673
  if (end !== -1) return raw.slice(end + 4).trimStart();
@@ -2502,7 +2676,7 @@ async function loadHubContent() {
2502
2676
  }
2503
2677
  async function loadSubSkillContent(name) {
2504
2678
  const root = await pickAssetRoot();
2505
- return readFile4(join5(root, name, "SKILL.md"), "utf-8");
2679
+ return readFile4(join6(root, name, "SKILL.md"), "utf-8");
2506
2680
  }
2507
2681
  async function injectBlock(filePath, content, dryRun) {
2508
2682
  const block = `${MARKER_START}
@@ -2570,8 +2744,8 @@ function setupCommand() {
2570
2744
  let anyFound = false;
2571
2745
  const changedTools = /* @__PURE__ */ new Set();
2572
2746
  for (const target of HUB_TARGETS) {
2573
- const dirPath = join5(home, target.dir);
2574
- const filePath = join5(home, target.file);
2747
+ const dirPath = join6(home, target.dir);
2748
+ const filePath = join6(home, target.file);
2575
2749
  if (!await exists(dirPath)) {
2576
2750
  if (!opts.silent) {
2577
2751
  printHuman(c.gray(` - ${target.tool.padEnd(12)} not installed, skipped`));
@@ -2598,10 +2772,10 @@ function setupCommand() {
2598
2772
  }
2599
2773
  }
2600
2774
  for (const target of SKILL_TARGETS) {
2601
- const dirPath = join5(home, target.dir);
2775
+ const dirPath = join6(home, target.dir);
2602
2776
  if (!await exists(dirPath)) continue;
2603
2777
  for (const subName of SUB_SKILLS) {
2604
- const filePath = join5(home, target.skillsDir, subName, "SKILL.md");
2778
+ const filePath = join6(home, target.skillsDir, subName, "SKILL.md");
2605
2779
  const dest = `~/${target.skillsDir}/${subName}/SKILL.md`;
2606
2780
  try {
2607
2781
  const result = await copyBundledSkill(filePath, subSkillContents[subName], dryRun);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rayul/kordis",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
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",
@@ -26,6 +26,7 @@
26
26
  "dependencies": {
27
27
  "@iarna/toml": "^2.2.5",
28
28
  "commander": "^12.1.0",
29
+ "jszip": "^3.10.1",
29
30
  "kleur": "^4.1.5",
30
31
  "open": "^10.1.0",
31
32
  "ora": "^8.1.1",
@@ -42,6 +43,7 @@
42
43
  "@kordis/contracts": "workspace:*",
43
44
  "@kordis/harness-schema": "workspace:*",
44
45
  "@kordis/translators": "workspace:*",
46
+ "@types/jszip": "^3.4.1",
45
47
  "@types/node": "^22.9.0",
46
48
  "@types/prompts": "^2.4.9",
47
49
  "tsup": "^8.3.5",