@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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
`
|
|
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("<
|
|
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 (
|
|
1396
|
+
async (path, opts) => {
|
|
1374
1397
|
const bundle = await useClient();
|
|
1375
|
-
const abs = resolve(process.cwd(),
|
|
1376
|
-
const
|
|
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,
|
|
1381
|
-
if (u &&
|
|
1412
|
+
const [u, s2] = opts.slug.split("/", 2);
|
|
1413
|
+
if (u && s2) {
|
|
1382
1414
|
targetUser = u;
|
|
1383
|
-
targetSlug =
|
|
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
|
|
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:
|
|
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
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
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 =
|
|
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
|
|
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 =
|
|
2447
|
+
const abs = join5(installRoot, f.path);
|
|
2282
2448
|
await mkdir3(dirname2(abs), { recursive: true });
|
|
2283
|
-
await
|
|
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 =
|
|
2467
|
+
const abs = join5(installRoot, f.path);
|
|
2301
2468
|
await mkdir3(dirname2(abs), { recursive: true });
|
|
2302
|
-
await
|
|
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
|
|
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
|
|
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:
|
|
2659
|
+
bundled: join6(__dir2, "..", "assets"),
|
|
2486
2660
|
// Dev mode: src/commands/setup.ts → 4 levels up to repo root, then packages/skills/
|
|
2487
|
-
source:
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
2574
|
-
const filePath =
|
|
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 =
|
|
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 =
|
|
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.
|
|
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",
|