@rayul/kordis 0.10.3 → 0.11.1

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.
@@ -125,12 +125,19 @@ two parents requiring incompatible ranges = hard fail** (exit code
125
125
  can pin the offending one explicitly. Recommended deps are only
126
126
  walked at depth 0 when `--with-recommends` is passed.
127
127
 
128
+ **Per-version deps (0.11.0+)** — pinning an older version walks the
129
+ dependencies THAT version declared, not the package's current ones:
130
+ `kordis pull user/foo@0.1.0` resolves 0.1.0's graph even if newer
131
+ releases changed or dropped deps.
132
+
128
133
  `kordis read @range` resolves to a concrete version (the stderr footer
129
134
  shows it: `… @1.4.2 (resolved @^1 → 1.4.2)`) but does NOT walk
130
135
  transitive deps — read is a one-shot context inject, no install plan.
131
136
 
132
137
  `kordis info user/foo@^1` shows a `Requested: 1.4.2 (current: 2.0.0)`
133
- line above `Type` when the pin differs from the registry default.
138
+ line above `Type` when the pin differs from the registry default. Its
139
+ `Depends`/`Recommends` lists reflect the resolved version (0.11.0+), so
140
+ inspecting an old pin shows that version's declared deps.
134
141
 
135
142
  ## When a version is yanked (0.10.0+)
136
143
 
package/dist/index.js CHANGED
@@ -20,7 +20,7 @@ var init_primitives = __esm({
20
20
  UsernameSchema = z.string().min(3).max(32).regex(/^[a-z0-9]+(-[a-z0-9]+)*$/, "username must be lowercase alphanumeric with single hyphens");
21
21
  SlugSchema = z.string().min(1).max(64).regex(/^[a-z0-9]+(-[a-z0-9]+)*$/, "slug must be lowercase alphanumeric with single hyphens");
22
22
  FullSlugSchema = z.string().min(5).max(128).regex(/^[a-z0-9](-?[a-z0-9])*\/[a-z0-9](-?[a-z0-9])*$/, "full_slug must match {username}/{slug}");
23
- SemverSchema = z.string().regex(
23
+ SemverSchema = z.string().max(32).regex(
24
24
  // eslint-disable-next-line no-useless-escape
25
25
  /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/,
26
26
  "must be a valid semver version"
@@ -478,8 +478,8 @@ var init_auth = __esm({
478
478
  });
479
479
  DeviceCodeResponseSchema = z8.object({
480
480
  device_code: z8.string().min(16),
481
- user_code: z8.string().length(8),
482
- // shown to the user: e.g. "KORDIS-AB12"
481
+ user_code: z8.string().regex(/^KORDIS-[0-9A-Z]{4}-[0-9A-Z]{4}$/),
482
+ // shown to the user: e.g. "KORDIS-7QFX-2M9K"
483
483
  verification_url: z8.string().url(),
484
484
  expires_in: z8.number().int().positive(),
485
485
  // seconds
@@ -1153,7 +1153,7 @@ var init_client = __esm({
1153
1153
  `[kordis] api: ${method} ${url.pathname}${url.search} \u2192 fetch failed, retrying in 150ms (${err.message})`
1154
1154
  );
1155
1155
  }
1156
- await new Promise((resolve2) => setTimeout(resolve2, 150));
1156
+ await new Promise((resolve3) => setTimeout(resolve3, 150));
1157
1157
  continue;
1158
1158
  }
1159
1159
  }
@@ -1178,7 +1178,12 @@ var init_client = __esm({
1178
1178
  }
1179
1179
  if (res.status === 204) return void 0;
1180
1180
  const text = await res.text();
1181
- const parsed = text ? JSON.parse(text) : void 0;
1181
+ let parsed;
1182
+ try {
1183
+ parsed = text ? JSON.parse(text) : void 0;
1184
+ } catch {
1185
+ parsed = void 0;
1186
+ }
1182
1187
  if (!res.ok) {
1183
1188
  const envelope = normalizeErrorEnvelope(res.status, parsed, text);
1184
1189
  throw new KordisApiError(res.status, envelope, requestId);
@@ -1919,7 +1924,7 @@ async function reportError(kind, err, extras = {}) {
1919
1924
  });
1920
1925
  await Promise.race([
1921
1926
  client.recordTelemetryError(payload),
1922
- new Promise((resolve2) => setTimeout(resolve2, REPORT_TIMEOUT_MS))
1927
+ new Promise((resolve3) => setTimeout(resolve3, REPORT_TIMEOUT_MS))
1923
1928
  ]);
1924
1929
  } catch {
1925
1930
  }
@@ -2379,30 +2384,13 @@ function searchCommand() {
2379
2384
  });
2380
2385
  }
2381
2386
  const queryProvided = query.length > 0;
2387
+ const emptyHint = opts.type ? `no packages of type "${opts.type}" match. Try without --type, or list everything: kordis search --type ${opts.type} --sort recent --limit 25` : queryProvided ? "try `--type skill` (or mcp-server / project-rules / custom-agent / hook) to narrow, or broader keywords" : 'try keywords (e.g. `kordis search "topic"`) or `kordis search --trending`';
2382
2388
  printDual({
2383
- json: res,
2389
+ json: res.results.length === 0 ? { ...res, hint: emptyHint } : res,
2384
2390
  human: () => {
2385
2391
  if (res.results.length === 0) {
2386
2392
  printHuman(c.yellow("(no results)"));
2387
- if (opts.type) {
2388
- printHuman(
2389
- c.gray(
2390
- ` \u2192 no packages of type "${opts.type}" match. Try without --type, or list everything: kordis search --type ${opts.type} --sort recent --limit 25`
2391
- )
2392
- );
2393
- } else if (queryProvided) {
2394
- printHuman(
2395
- c.gray(
2396
- " \u2192 try `--type skill` (or mcp-server / project-rules / custom-agent / hook) to narrow, or broader keywords"
2397
- )
2398
- );
2399
- } else {
2400
- printHuman(
2401
- c.gray(
2402
- ' \u2192 try keywords (e.g. `kordis search "topic"`) or `kordis search --trending`'
2403
- )
2404
- );
2405
- }
2393
+ printHuman(c.gray(` \u2192 ${emptyHint}`));
2406
2394
  return;
2407
2395
  }
2408
2396
  if (typeof res.total === "number") {
@@ -2522,8 +2510,8 @@ function normaliseDep(entry, idx) {
2522
2510
  );
2523
2511
  }
2524
2512
  return {
2525
- full_slug: m[1],
2526
- version_range: (m[3] ?? "*").trim()
2513
+ full_slug: FullSlugSchema.parse(m[1]),
2514
+ version_range: SemverRangeSchema.parse((m[4] ?? "*").trim())
2527
2515
  };
2528
2516
  }
2529
2517
  if (typeof entry === "object" && entry !== null) {
@@ -2979,7 +2967,7 @@ var ASSET_MAX_COUNT = 50;
2979
2967
  var SAFE_PATH_RE = /^[a-zA-Z0-9][a-zA-Z0-9._/-]*$/;
2980
2968
  function isAssetPathSafe(p) {
2981
2969
  if (!SAFE_PATH_RE.test(p)) return false;
2982
- return !p.split("/").some((seg) => seg === "..");
2970
+ return p.split("/").every((seg) => seg.length > 0 && seg !== "." && seg !== "..");
2983
2971
  }
2984
2972
  function validateAssets(assets) {
2985
2973
  const flags = [];
@@ -3272,7 +3260,11 @@ async function bundleSkillZip(zipPath) {
3272
3260
  const buf = await readFile6(zipPath);
3273
3261
  const zip = await JSZip.loadAsync(buf);
3274
3262
  const entries = Object.keys(zip.files).filter((k) => !zip.files[k].dir);
3275
- if (entries.length === 0) throw new Error(".skill file is empty");
3263
+ if (entries.length === 0) {
3264
+ const e = new Error(".skill file is empty");
3265
+ e.hint = "The .skill archive contains no files. Re-export the .skill from skill-creator, or push the unpacked skill directory instead (the dir containing SKILL.md).";
3266
+ throw e;
3267
+ }
3276
3268
  const firstParts = entries[0].split("/");
3277
3269
  const prefix = firstParts.length > 1 ? firstParts[0] + "/" : "";
3278
3270
  const stripped = entries.map((k) => ({
@@ -3281,7 +3273,11 @@ async function bundleSkillZip(zipPath) {
3281
3273
  })).filter((f) => f.rel.length > 0 && !shouldExclude(f.rel, false));
3282
3274
  const skillEntry = stripped.find((f) => f.rel === "SKILL.md");
3283
3275
  if (!skillEntry) {
3284
- throw new Error(`.skill ZIP has no SKILL.md at its root (prefix="${prefix}")`);
3276
+ const e = new Error(
3277
+ `.skill ZIP has no SKILL.md at its root (prefix="${prefix}")`
3278
+ );
3279
+ e.hint = "SKILL.md must sit at <skill-name>/SKILL.md at the archive root. Re-package so the single top-level directory contains SKILL.md (skill-creator does this by default).";
3280
+ throw e;
3285
3281
  }
3286
3282
  const content = await zip.files[skillEntry.zipKey].async("string");
3287
3283
  const assets = [];
@@ -3304,7 +3300,7 @@ async function bundleSkillZip(zipPath) {
3304
3300
  // src/commands/pull.ts
3305
3301
  import { Command as Command5 } from "commander";
3306
3302
  import { mkdir as mkdir6, writeFile as writeFile6 } from "fs/promises";
3307
- import { dirname as dirname2, join as join9 } from "path";
3303
+ import { dirname as dirname2, join as join9, resolve as resolve2, sep as sep2 } from "path";
3308
3304
 
3309
3305
  // ../../packages/translators/src/skill/identity.ts
3310
3306
  function buildSkillFile(input, outputPath) {
@@ -3548,13 +3544,13 @@ var mcpToContinue = (input) => {
3548
3544
  }
3549
3545
  if (t.env && Object.keys(t.env).length > 0) {
3550
3546
  lines.push("env:");
3551
- for (const [k, v] of Object.entries(t.env)) lines.push(` ${k}: ${JSON.stringify(v)}`);
3547
+ for (const [k, v] of Object.entries(t.env)) lines.push(` ${JSON.stringify(k)}: ${JSON.stringify(v)}`);
3552
3548
  }
3553
3549
  } else {
3554
3550
  lines.push(`type: ${t.type}`, `url: ${JSON.stringify(t.url)}`);
3555
3551
  if (t.headers && Object.keys(t.headers).length > 0) {
3556
3552
  lines.push("headers:");
3557
- for (const [k, v] of Object.entries(t.headers)) lines.push(` ${k}: ${JSON.stringify(v)}`);
3553
+ for (const [k, v] of Object.entries(t.headers)) lines.push(` ${JSON.stringify(k)}: ${JSON.stringify(v)}`);
3558
3554
  }
3559
3555
  }
3560
3556
  return {
@@ -3781,16 +3777,23 @@ async function recordInstalled(fullSlug, entry) {
3781
3777
  // src/lib/parse-username.ts
3782
3778
  init_src();
3783
3779
  var SORT_KEYWORDS = ["latest", "popular", "recent", "updated", "stars", "downloads"];
3780
+ function usernameError(message, hint) {
3781
+ const e = new Error(message);
3782
+ e.hint = hint;
3783
+ return e;
3784
+ }
3784
3785
  function parseUsername(raw) {
3785
3786
  if (SORT_KEYWORDS.includes(raw.toLowerCase())) {
3786
- throw new Error(
3787
- `"${raw}" is a sort keyword \u2014 did you mean \`--sort ${raw}\`? Username arguments must follow the Kordis username shape (lowercase alphanumeric, hyphens allowed).`
3787
+ throw usernameError(
3788
+ `"${raw}" is a sort keyword \u2014 did you mean \`--sort ${raw}\`? Username arguments must follow the Kordis username shape (lowercase alphanumeric, hyphens allowed).`,
3789
+ `Pass it as the sort flag instead, e.g. \`--sort ${raw}\`, or supply an actual username argument.`
3788
3790
  );
3789
3791
  }
3790
3792
  const parsed = UsernameSchema.safeParse(raw);
3791
3793
  if (!parsed.success) {
3792
- throw new Error(
3793
- `invalid username "${raw}" \u2014 must be 3-32 chars, lowercase alphanumeric with single hyphens (e.g. "alice", "my-team")`
3794
+ throw usernameError(
3795
+ `invalid username "${raw}" \u2014 must be 3-32 chars, lowercase alphanumeric with single hyphens (e.g. "alice", "my-team")`,
3796
+ "Usernames are lowercase alphanumeric with single hyphens. Browse existing users with `kordis search` or look one up with `kordis profile <username>`."
3794
3797
  );
3795
3798
  }
3796
3799
  return parsed.data;
@@ -3798,28 +3801,44 @@ function parseUsername(raw) {
3798
3801
 
3799
3802
  // src/lib/parse-slug-version.ts
3800
3803
  var SUFFIX_CHARS = /[\^~*]/;
3804
+ function slugError(message, hint) {
3805
+ const e = new Error(message);
3806
+ e.hint = hint;
3807
+ return e;
3808
+ }
3801
3809
  function parseSlugAtVersion(raw) {
3802
3810
  if (!raw.includes("/")) {
3811
+ let username2;
3803
3812
  try {
3804
- const username2 = parseUsername(raw);
3805
- throw new Error(
3806
- `"${raw}" looks like a username, not a package \u2014 try \`kordis profile ${username2}\` for user details, or pass a full slug like "{username}/{slug}".`
3813
+ username2 = parseUsername(raw);
3814
+ } catch {
3815
+ username2 = void 0;
3816
+ }
3817
+ if (username2) {
3818
+ throw slugError(
3819
+ `"${raw}" looks like a username, not a package`,
3820
+ `Try \`kordis profile ${username2}\` for user details, or pass a full slug like "{username}/{slug}".`
3807
3821
  );
3808
- } catch (err) {
3809
- if (err instanceof Error) throw err;
3810
- throw new Error(`"${raw}" is missing the "{username}/{slug}" form`);
3811
3822
  }
3823
+ throw slugError(
3824
+ `"${raw}" is missing the "{username}/{slug}" form`,
3825
+ 'Use the form "{username}/{slug}", e.g. "alice/json-pretty".'
3826
+ );
3812
3827
  }
3813
3828
  const atIdx = raw.indexOf("@");
3814
3829
  const slugPart = atIdx === -1 ? raw : raw.slice(0, atIdx);
3815
3830
  const versionPart = atIdx === -1 ? "" : raw.slice(atIdx + 1);
3816
3831
  const [username, slug] = slugPart.split("/");
3817
3832
  if (!username || !slug) {
3818
- throw new Error('full_slug must be "{username}/{slug}"');
3833
+ throw slugError(
3834
+ 'full_slug must be "{username}/{slug}"',
3835
+ 'Pass both parts, e.g. "alice/json-pretty". Browse with `kordis search` if unsure.'
3836
+ );
3819
3837
  }
3820
3838
  if (atIdx !== -1 && versionPart.length === 0) {
3821
- throw new Error(
3822
- `"${raw}" has a trailing "@" with no version \u2014 drop the "@" or pass a version/range after it`
3839
+ throw slugError(
3840
+ `"${raw}" has a trailing "@" with no version`,
3841
+ `Drop the "@" or append a version/range, e.g. "${slugPart}@1.0.0" or "${slugPart}@^1".`
3823
3842
  );
3824
3843
  }
3825
3844
  const out = {
@@ -3840,7 +3859,11 @@ function parseSlugAtVersion(raw) {
3840
3859
  // src/commands/pull.ts
3841
3860
  async function doPullInstall(fullSlug, tool, installRoot, opts = {}) {
3842
3861
  const [username, slug] = fullSlug.split("/");
3843
- if (!username || !slug) throw new Error('full_slug must be "{username}/{slug}"');
3862
+ if (!username || !slug) {
3863
+ const e = new Error('full_slug must be "{username}/{slug}"');
3864
+ e.hint = 'Pass both parts, e.g. "alice/json-pretty". Browse with `kordis search` if unsure.';
3865
+ throw e;
3866
+ }
3844
3867
  const bundle = await useClient();
3845
3868
  const pullRes = await bundle.api.pull(username, slug, {
3846
3869
  ...opts.version ? { version: opts.version } : {},
@@ -3855,7 +3878,9 @@ async function doPullInstall(fullSlug, tool, installRoot, opts = {}) {
3855
3878
  });
3856
3879
  const parsed = validatePackageSource(contentRes.content);
3857
3880
  if (!parsed.ok || !parsed.type || !parsed.frontmatter) {
3858
- throw new Error("package stored content is not currently valid");
3881
+ const e = new Error("package stored content is not currently valid");
3882
+ e.hint = `This version's stored bytes no longer pass validation. Try another version via \`kordis versions ${fullSlug}\` (then pull a specific \`@<version>\`), or re-push a corrected version if you authored it.`;
3883
+ throw e;
3859
3884
  }
3860
3885
  const written = [];
3861
3886
  const notes = [];
@@ -3870,7 +3895,7 @@ async function doPullInstall(fullSlug, tool, installRoot, opts = {}) {
3870
3895
  assets: contentRes.assets
3871
3896
  });
3872
3897
  for (const f of result.files) {
3873
- const abs = join9(installRoot, f.path);
3898
+ const abs = safeInstallPath(installRoot, f.path);
3874
3899
  await mkdir6(dirname2(abs), { recursive: true });
3875
3900
  await writeEmittedFile(abs, f.content, f.encoding);
3876
3901
  written.push(abs);
@@ -3890,7 +3915,7 @@ async function doPullInstall(fullSlug, tool, installRoot, opts = {}) {
3890
3915
  `Tool "${format}" is not supported for type "${parsed.type}"; installed canonical form instead.`
3891
3916
  );
3892
3917
  for (const f of result.files) {
3893
- const abs = join9(installRoot, f.path);
3918
+ const abs = safeInstallPath(installRoot, f.path);
3894
3919
  await mkdir6(dirname2(abs), { recursive: true });
3895
3920
  await writeEmittedFile(abs, f.content, f.encoding);
3896
3921
  written.push(abs);
@@ -4053,6 +4078,26 @@ async function detectTool(root) {
4053
4078
  }
4054
4079
  return null;
4055
4080
  }
4081
+ function safeInstallPath(installRoot, relPath) {
4082
+ if (relPath.startsWith("/") || resolve2(relPath) === relPath) {
4083
+ const e = new Error(
4084
+ `refusing to write absolute path "${relPath}" outside the install root`
4085
+ );
4086
+ e.hint = "Package emitted an absolute file path. Re-push the package with relative paths only; report it to the author if you did not publish it.";
4087
+ throw e;
4088
+ }
4089
+ const abs = join9(installRoot, relPath);
4090
+ const resolvedRoot = resolve2(installRoot);
4091
+ const resolvedAbs = resolve2(abs);
4092
+ if (resolvedAbs !== resolvedRoot && !resolvedAbs.startsWith(resolvedRoot + sep2)) {
4093
+ const e = new Error(
4094
+ `refusing to write "${relPath}" \u2014 resolves outside the install root`
4095
+ );
4096
+ e.hint = "Package tried to write outside the install directory (path traversal). Re-push the package with relative in-tree paths only; report it to the author if you did not publish it.";
4097
+ throw e;
4098
+ }
4099
+ return abs;
4100
+ }
4056
4101
  async function writeEmittedFile(abs, content, encoding) {
4057
4102
  if (encoding === "base64") {
4058
4103
  await writeFile6(abs, Buffer.from(content, "base64"));
@@ -4550,7 +4595,9 @@ function commentsCommand() {
4550
4595
  async (rawSlug, opts) => {
4551
4596
  const [username, slug] = rawSlug.split("/");
4552
4597
  if (!username || !slug) {
4553
- throw new Error('full_slug must be "{username}/{slug}"');
4598
+ const e = new Error('full_slug must be "{username}/{slug}"');
4599
+ e.hint = 'Pass both parts, e.g. "alice/json-pretty". Browse with `kordis search` if unsure.';
4600
+ throw e;
4554
4601
  }
4555
4602
  if (opts.kind && !KINDS.includes(opts.kind)) {
4556
4603
  throw new Error(`--kind must be one of ${KINDS.join(", ")}`);
@@ -4565,14 +4612,13 @@ function commentsCommand() {
4565
4612
  ...opts.kind ? { kind: opts.kind } : {},
4566
4613
  ...opts.sort ? { sort: opts.sort } : {}
4567
4614
  });
4615
+ const emptyHint = "no comments yet \u2014 be the first: kordis comment " + rawSlug + ' --body "..."';
4568
4616
  printDual({
4569
- json: res,
4617
+ json: res.results.length === 0 ? { ...res, hint: emptyHint } : res,
4570
4618
  human: () => {
4571
4619
  if (res.results.length === 0) {
4572
4620
  printHuman(c.yellow("(no comments)"));
4573
- printHuman(
4574
- c.gray(" \u2192 be the first: kordis comment " + rawSlug + ' --body "..."')
4575
- );
4621
+ printHuman(c.gray(" \u2192 " + emptyHint));
4576
4622
  return;
4577
4623
  }
4578
4624
  printHuman(
@@ -4640,7 +4686,9 @@ function starCommand() {
4640
4686
  return new Command11("star").description("star a package").argument("<full_slug>", "package identifier {username}/{slug}").action(async (rawSlug) => {
4641
4687
  const [username, slug] = rawSlug.split("/");
4642
4688
  if (!username || !slug) {
4643
- throw new Error('full_slug must be "{username}/{slug}"');
4689
+ const e = new Error('full_slug must be "{username}/{slug}"');
4690
+ e.hint = 'Pass both parts, e.g. "alice/json-pretty". Browse with `kordis search` if unsure.';
4691
+ throw e;
4644
4692
  }
4645
4693
  const bundle = await useClient();
4646
4694
  requireAuth(bundle);
@@ -4658,7 +4706,9 @@ function unstarCommand() {
4658
4706
  return new Command11("unstar").description("remove your star from a package").argument("<full_slug>", "package identifier {username}/{slug}").action(async (rawSlug) => {
4659
4707
  const [username, slug] = rawSlug.split("/");
4660
4708
  if (!username || !slug) {
4661
- throw new Error('full_slug must be "{username}/{slug}"');
4709
+ const e = new Error('full_slug must be "{username}/{slug}"');
4710
+ e.hint = 'Pass both parts, e.g. "alice/json-pretty". Browse with `kordis search` if unsure.';
4711
+ throw e;
4662
4712
  }
4663
4713
  const bundle = await useClient();
4664
4714
  requireAuth(bundle);
@@ -4682,7 +4732,9 @@ function deleteCommand() {
4682
4732
  return new Command12("delete").description("permanently delete a package you own").argument("<full_slug>", "package identifier {username}/{slug}").option("--force", "skip the confirmation prompt").action(async (fullSlug, opts) => {
4683
4733
  const [username, slug] = fullSlug.split("/");
4684
4734
  if (!username || !slug) {
4685
- throw new Error('full_slug must be "{username}/{slug}"');
4735
+ const e = new Error('full_slug must be "{username}/{slug}"');
4736
+ e.hint = 'Pass both parts, e.g. "alice/json-pretty". Browse with `kordis search` if unsure.';
4737
+ throw e;
4686
4738
  }
4687
4739
  const bundle = await useClient();
4688
4740
  const detail = await bundle.api.getPackage(username, slug);
@@ -4890,11 +4942,13 @@ function listCommand() {
4890
4942
  sort: publicSort,
4891
4943
  ...opts.cursor ? { cursor: opts.cursor } : {}
4892
4944
  });
4945
+ const emptyHint = `no public packages for ${username} \u2014 verify the username via \`kordis profile ${username}\`, or browse the registry via \`kordis search\``;
4893
4946
  printDual({
4894
- json: res,
4947
+ json: res.results.length === 0 ? { ...res, hint: emptyHint } : res,
4895
4948
  human: () => {
4896
4949
  if (res.results.length === 0) {
4897
4950
  printHuman(c.yellow(`(no public packages for ${username})`));
4951
+ printHuman(c.gray(` \u2192 ${emptyHint}`));
4898
4952
  return;
4899
4953
  }
4900
4954
  for (const p of res.results) {
@@ -4920,7 +4974,9 @@ function versionsCommand() {
4920
4974
  return new Command16("versions").description("list all versions of a package").argument("<full_slug>", "package identifier {username}/{slug}").action(async (fullSlug) => {
4921
4975
  const [username, slug] = fullSlug.split("/");
4922
4976
  if (!username || !slug) {
4923
- throw new Error('full_slug must be "{username}/{slug}"');
4977
+ const e = new Error('full_slug must be "{username}/{slug}"');
4978
+ e.hint = 'Pass both parts, e.g. "alice/json-pretty". Browse with `kordis search` if unsure.';
4979
+ throw e;
4924
4980
  }
4925
4981
  const bundle = await useClient();
4926
4982
  const spin = spinner(`Fetching versions for ${fullSlug}`);
@@ -5234,7 +5290,9 @@ function diffCommand() {
5234
5290
  }
5235
5291
  const [username, slug] = fullSlug.split("/");
5236
5292
  if (!username || !slug) {
5237
- throw new Error('full_slug must be "{username}/{slug}"');
5293
+ const e = new Error('full_slug must be "{username}/{slug}"');
5294
+ e.hint = 'Pass both parts, e.g. "alice/json-pretty". Browse with `kordis search` if unsure.';
5295
+ throw e;
5238
5296
  }
5239
5297
  const bundle = await useClient();
5240
5298
  if (!from || !to) {
@@ -5327,8 +5385,9 @@ function recommendCommand() {
5327
5385
  spin.succeed();
5328
5386
  await writeCache(res.items, res.generated_at);
5329
5387
  await markSeen();
5388
+ const emptyHint = !res.personalized ? "personalization is OFF, so the server ranks on popularity alone \u2014 likely empty pool. Enable for stack-aware matches: `kordis setup --personalization on`, or broaden via `kordis search <topic>` / `kordis search --trending`." : "personalization is ON but no matches for your stack \u2014 registry may be sparse for this profile. Try `kordis search <topic>` or `kordis search --trending`.";
5330
5389
  printDual({
5331
- json: res,
5390
+ json: res.items.length === 0 ? { ...res, hint: emptyHint } : res,
5332
5391
  human: () => {
5333
5392
  if (res.items.length === 0) {
5334
5393
  printHuman(c.yellow("(no recommendations available right now)"));
@@ -5450,12 +5509,13 @@ function starsCommand() {
5450
5509
  limit: opts.limit,
5451
5510
  ...opts.cursor ? { cursor: opts.cursor } : {}
5452
5511
  });
5512
+ const emptyHint = `no public stars for ${username} \u2014 they may have none, or keep them private`;
5453
5513
  printDual({
5454
- json: res,
5514
+ json: res.results.length === 0 ? { ...res, hint: emptyHint } : res,
5455
5515
  human: () => {
5456
5516
  if (res.results.length === 0) {
5457
5517
  printHuman(c.yellow(`(no stars yet for ${username})`));
5458
- printHuman(c.gray(` \u2192 they may not have starred any public packages, or have set stars to private`));
5518
+ printHuman(c.gray(` \u2192 ${emptyHint}`));
5459
5519
  return;
5460
5520
  }
5461
5521
  for (const p of res.results) {
@@ -5520,12 +5580,14 @@ function makeListing(name) {
5520
5580
  limit: opts.limit,
5521
5581
  ...opts.cursor ? { cursor: opts.cursor } : {}
5522
5582
  });
5583
+ const emptyHint = name === "followers" ? `no followers yet for ${username} \u2014 verify the username via \`kordis profile ${username}\`, or this account simply has no followers` : `${username} isn't following anyone yet \u2014 verify the username via \`kordis profile ${username}\``;
5523
5584
  printDual({
5524
- json: res,
5585
+ json: res.results.length === 0 ? { ...res, hint: emptyHint } : res,
5525
5586
  human: () => {
5526
5587
  if (res.results.length === 0) {
5527
5588
  const noun = name === "followers" ? "followers yet" : "one yet";
5528
5589
  printHuman(c.yellow(`(no ${noun} for ${username})`));
5590
+ printHuman(c.gray(` \u2192 ${emptyHint}`));
5529
5591
  return;
5530
5592
  }
5531
5593
  for (const u of res.results) {
@@ -5605,10 +5667,10 @@ async function main(argv) {
5605
5667
  const cleaned = argv.filter((a, i) => !(a === "--" && i >= 2));
5606
5668
  await program.parseAsync(cleaned);
5607
5669
  } catch (err) {
5608
- handleError(err);
5670
+ await handleError(err);
5609
5671
  }
5610
5672
  }
5611
- function handleError(err) {
5673
+ async function handleError(err) {
5612
5674
  if (err instanceof KordisApiError) {
5613
5675
  if (isJsonMode()) {
5614
5676
  printJson({
@@ -5655,7 +5717,7 @@ function handleError(err) {
5655
5717
  printHuman(c.red(`\u2717 ${err.message}`));
5656
5718
  for (const line of hint.split("\n")) printHuman(c.gray(` \u2192 ${line}`));
5657
5719
  }
5658
- void reportError("network_unreachable", err);
5720
+ await reportError("network_unreachable", err);
5659
5721
  process.exit(err.exitCode());
5660
5722
  }
5661
5723
  if (err instanceof Error) {
@@ -5675,7 +5737,7 @@ function handleError(err) {
5675
5737
  for (const line of hintStr.split("\n")) printHuman(c.gray(` \u2192 ${line}`));
5676
5738
  }
5677
5739
  }
5678
- void reportError("unhandled_error", err);
5740
+ await reportError("unhandled_error", err);
5679
5741
  process.exit(1);
5680
5742
  }
5681
5743
  if (isJsonMode()) printJson({ error: { code: "generic_failure", message: String(err) } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rayul/kordis",
3
- "version": "0.10.3",
3
+ "version": "0.11.1",
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",
@@ -37,7 +37,7 @@
37
37
  "zod": "^3.23.8"
38
38
  },
39
39
  "optionalDependencies": {
40
- "keytar": "^7.9.0"
40
+ "keytar": "7.9.0"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@kordis/api-client": "workspace:*",