@rayul/kordis 0.3.0 → 0.4.0

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.
package/dist/index.js CHANGED
@@ -175,6 +175,7 @@ var init_errors = __esm({
175
175
  error: z3.object({
176
176
  code: ErrorCodeSchema,
177
177
  message: z3.string(),
178
+ hint: z3.string().optional(),
178
179
  flags: z3.array(ReviewFlagSchema).optional(),
179
180
  retry_after_seconds: z3.number().int().nonnegative().optional(),
180
181
  request_id: z3.string().optional()
@@ -703,6 +704,109 @@ var init_api = __esm({
703
704
  }
704
705
  });
705
706
 
707
+ // ../../packages/contracts/src/context.ts
708
+ import { z as z10 } from "zod";
709
+ var KordisContextSchema, CONTEXT_HEADER_MAX_BYTES, CONTEXT_HEADER_NAME;
710
+ var init_context = __esm({
711
+ "../../packages/contracts/src/context.ts"() {
712
+ "use strict";
713
+ init_primitives();
714
+ KordisContextSchema = z10.object({
715
+ /**
716
+ * Which agentic tool configs exist locally. Presence only — no file
717
+ * contents inspected. Used to boost packages whose `tools_compatible`
718
+ * overlaps.
719
+ */
720
+ tools: z10.array(ToolSlugSchema).max(8).optional(),
721
+ /**
722
+ * Top-3 file extensions in CWD (excluding `.git`, `node_modules`,
723
+ * `dist`, `.next`, build artifacts). Each entry is a short lowercase
724
+ * tag like `ts`, `py`, `rs`, `go`.
725
+ */
726
+ languages: z10.array(z10.string().min(1).max(8)).max(3).optional(),
727
+ /**
728
+ * Frameworks detected from `package.json` deps, `pyproject.toml`,
729
+ * `Cargo.toml`, `go.mod`. Curated allowlist on the collector side
730
+ * (next, react, vue, svelte, fastapi, django, fastify, hono, ...).
731
+ */
732
+ frameworks: z10.array(z10.string().min(1).max(32)).max(8).optional(),
733
+ /**
734
+ * SHA-256 first-8-chars (lowercase hex) of each `full_slug` in the
735
+ * user's `~/.kordis/installed.json`. The server uses these to find
736
+ * co-pull patterns WITHOUT learning which slugs the user has installed
737
+ * (one-way hash, prefix-truncated, still anonymises across the small
738
+ * registry universe). Max 50 entries.
739
+ */
740
+ installed_slugs_hash: z10.array(z10.string().regex(/^[0-9a-f]{8}$/)).max(50).optional()
741
+ });
742
+ CONTEXT_HEADER_MAX_BYTES = 1024;
743
+ CONTEXT_HEADER_NAME = "X-Kordis-Context";
744
+ }
745
+ });
746
+
747
+ // ../../packages/contracts/src/recommend.ts
748
+ import { z as z11 } from "zod";
749
+ var RecommendQuerySchema, RecommendConfidenceSchema, RecommendScoreBreakdownSchema, RecommendMenuItemSchema, RecommendResponseSchema;
750
+ var init_recommend = __esm({
751
+ "../../packages/contracts/src/recommend.ts"() {
752
+ "use strict";
753
+ init_primitives();
754
+ init_enums();
755
+ RecommendQuerySchema = z11.object({
756
+ limit: z11.coerce.number().int().positive().max(50).default(10),
757
+ /**
758
+ * Include score_breakdown in the response. CLI sets when --verbose.
759
+ *
760
+ * NOT `z.coerce.boolean()` — that wraps `Boolean()` which treats the
761
+ * literal string `"false"` as truthy (non-empty string). Use explicit
762
+ * string-vs-true matching so URL query `?verbose=false` evaluates as
763
+ * `false`.
764
+ */
765
+ verbose: z11.union([z11.boolean(), z11.enum(["true", "false"])]).transform((v) => v === true || v === "true").optional().default(false)
766
+ });
767
+ RecommendConfidenceSchema = z11.enum(["low", "medium", "high"]);
768
+ RecommendScoreBreakdownSchema = z11.object({
769
+ content: z11.number(),
770
+ cf: z11.number(),
771
+ popularity: z11.number(),
772
+ final: z11.number()
773
+ });
774
+ RecommendMenuItemSchema = z11.object({
775
+ full_slug: FullSlugSchema,
776
+ title: z11.string(),
777
+ description: z11.string(),
778
+ type: HarnessTypeSchema,
779
+ tier: TierSchema,
780
+ current_version: SemverSchema,
781
+ stars: z11.number().int().nonnegative(),
782
+ downloads: z11.number().int().nonnegative(),
783
+ /** Ready-to-execute install command. Always populated. */
784
+ install_command: z11.string(),
785
+ /** Ready-to-execute JIT-read command. Always populated; agent decides which to use. */
786
+ jit_read_command: z11.string(),
787
+ /**
788
+ * One-line rationale derived from the strongest scoring signal. E.g.
789
+ * "You have FastAPI installed; this adds streaming middleware" or
790
+ * "Trending in your tool's ecosystem this week".
791
+ */
792
+ why_recommended: z11.string(),
793
+ confidence: RecommendConfidenceSchema,
794
+ /** Present only when the request specified `verbose=true`. */
795
+ score_breakdown: RecommendScoreBreakdownSchema.optional()
796
+ });
797
+ RecommendResponseSchema = z11.object({
798
+ items: z11.array(RecommendMenuItemSchema),
799
+ generated_at: IsoDateTimeSchema,
800
+ /**
801
+ * Whether personalization context arrived with the request. False means
802
+ * the server returned popularity-only results — useful for agents to
803
+ * disclose "context off → less personalised" to the user.
804
+ */
805
+ personalized: z11.boolean()
806
+ });
807
+ }
808
+ });
809
+
706
810
  // ../../packages/contracts/src/index.ts
707
811
  var init_src = __esm({
708
812
  "../../packages/contracts/src/index.ts"() {
@@ -716,6 +820,8 @@ var init_src = __esm({
716
820
  init_comment();
717
821
  init_auth();
718
822
  init_api();
823
+ init_context();
824
+ init_recommend();
719
825
  }
720
826
  });
721
827
 
@@ -728,6 +834,7 @@ var init_errors2 = __esm({
728
834
  KordisApiError = class extends Error {
729
835
  code;
730
836
  status;
837
+ hint;
731
838
  flags;
732
839
  retryAfter;
733
840
  requestId;
@@ -736,6 +843,7 @@ var init_errors2 = __esm({
736
843
  this.name = "KordisApiError";
737
844
  this.code = envelope.error.code;
738
845
  this.status = status;
846
+ this.hint = envelope.error.hint;
739
847
  this.flags = envelope.error.flags ?? [];
740
848
  this.retryAfter = envelope.error.retry_after_seconds;
741
849
  this.requestId = envelope.error.request_id ?? requestId;
@@ -794,6 +902,7 @@ var init_client = __esm({
794
902
  const headers = {
795
903
  accept: "application/json",
796
904
  ...await this.authHeaders(),
905
+ ...this.config.getDynamicHeaders ? await this.config.getDynamicHeaders() : {},
797
906
  ...opts.headers ?? {}
798
907
  };
799
908
  if (opts.body !== void 0) headers["content-type"] = "application/json";
@@ -933,6 +1042,20 @@ var init_client = __esm({
933
1042
  popular() {
934
1043
  return this.request("/api/packages/popular");
935
1044
  }
1045
+ /**
1046
+ * Hybrid recommendations (0.4.0+). The personalization signal rides on
1047
+ * the `X-Kordis-Context` header (injected by `getDynamicHeaders`), not in
1048
+ * the query string. Anonymous is fine — server falls back to
1049
+ * popularity-only with `confidence: 'low'`.
1050
+ */
1051
+ recommend(opts = {}) {
1052
+ return this.request("/api/packages/recommend", {
1053
+ query: {
1054
+ ...opts.limit !== void 0 ? { limit: opts.limit } : {},
1055
+ ...opts.verbose !== void 0 ? { verbose: opts.verbose } : {}
1056
+ }
1057
+ });
1058
+ }
936
1059
  star(username, slug) {
937
1060
  return this.request(`/api/packages/${username}/${slug}/star`, {
938
1061
  method: "POST"
@@ -1012,7 +1135,7 @@ var init_src2 = __esm({
1012
1135
 
1013
1136
  // src/index.ts
1014
1137
  init_src2();
1015
- import { Command as Command18 } from "commander";
1138
+ import { Command as Command19 } from "commander";
1016
1139
 
1017
1140
  // src/commands/login.ts
1018
1141
  import { Command } from "commander";
@@ -1020,6 +1143,7 @@ import open from "open";
1020
1143
 
1021
1144
  // src/client.ts
1022
1145
  init_src2();
1146
+ init_src();
1023
1147
 
1024
1148
  // src/config.ts
1025
1149
  import { homedir } from "os";
@@ -1039,7 +1163,8 @@ async function loadCliConfig() {
1039
1163
  return {
1040
1164
  apiUrl: apiUrlFromEnv ?? parsed.apiUrl ?? "https://api.kordis.store",
1041
1165
  ...parsed.username ? { username: parsed.username } : {},
1042
- ...parsed.sessionId ? { sessionId: parsed.sessionId } : {}
1166
+ ...parsed.sessionId ? { sessionId: parsed.sessionId } : {},
1167
+ personalization: parsed.personalization ?? "undecided"
1043
1168
  };
1044
1169
  } catch {
1045
1170
  return { apiUrl: apiUrlFromEnv ?? "https://api.kordis.store" };
@@ -1146,12 +1271,236 @@ async function readAll() {
1146
1271
  }
1147
1272
  }
1148
1273
 
1274
+ // src/context/collector.ts
1275
+ init_src();
1276
+ import { access as access2, readFile as readFile3, readdir, stat } from "fs/promises";
1277
+ import { homedir as homedir3 } from "os";
1278
+ import { join as join3, extname } from "path";
1279
+ import { createHash } from "crypto";
1280
+ async function pathExists2(p) {
1281
+ try {
1282
+ await access2(p);
1283
+ return true;
1284
+ } catch {
1285
+ return false;
1286
+ }
1287
+ }
1288
+ async function detectTools() {
1289
+ const checks = [
1290
+ { tool: "claude-code", paths: [join3(homedir3(), ".claude", "skills")] },
1291
+ { tool: "codex", paths: [join3(homedir3(), ".codex"), join3(homedir3(), ".codex", "skills")] },
1292
+ { tool: "cursor", paths: [join3(homedir3(), ".cursor")] },
1293
+ { tool: "gemini-cli", paths: [join3(homedir3(), ".gemini"), join3(homedir3(), ".gemini", "skills")] }
1294
+ ];
1295
+ const found = [];
1296
+ for (const { tool, paths } of checks) {
1297
+ for (const p of paths) {
1298
+ if (await pathExists2(p)) {
1299
+ found.push(tool);
1300
+ break;
1301
+ }
1302
+ }
1303
+ }
1304
+ return found;
1305
+ }
1306
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
1307
+ ".git",
1308
+ "node_modules",
1309
+ "dist",
1310
+ "build",
1311
+ ".next",
1312
+ ".nuxt",
1313
+ ".svelte-kit",
1314
+ "target",
1315
+ ".venv",
1316
+ "venv",
1317
+ "__pycache__",
1318
+ ".cache",
1319
+ ".turbo"
1320
+ ]);
1321
+ var EXT_TO_TAG = {
1322
+ ".ts": "ts",
1323
+ ".tsx": "ts",
1324
+ ".js": "js",
1325
+ ".jsx": "js",
1326
+ ".mjs": "js",
1327
+ ".cjs": "js",
1328
+ ".py": "py",
1329
+ ".rs": "rs",
1330
+ ".go": "go",
1331
+ ".rb": "rb",
1332
+ ".java": "java",
1333
+ ".kt": "kt",
1334
+ ".swift": "swift",
1335
+ ".c": "c",
1336
+ ".cpp": "cpp",
1337
+ ".cc": "cpp",
1338
+ ".h": "c",
1339
+ ".cs": "cs",
1340
+ ".php": "php",
1341
+ ".lua": "lua",
1342
+ ".r": "r",
1343
+ ".sh": "sh",
1344
+ ".zsh": "sh",
1345
+ ".bash": "sh",
1346
+ ".sql": "sql",
1347
+ ".html": "html",
1348
+ ".css": "css",
1349
+ ".scss": "scss"
1350
+ };
1351
+ async function detectLanguages(cwd = process.cwd()) {
1352
+ const counts = /* @__PURE__ */ new Map();
1353
+ await walk(cwd, 4, counts);
1354
+ return [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([tag]) => tag);
1355
+ }
1356
+ async function walk(dir, depth, counts) {
1357
+ if (depth < 0) return;
1358
+ let entries;
1359
+ try {
1360
+ entries = await readdir(dir, { withFileTypes: true });
1361
+ } catch {
1362
+ return;
1363
+ }
1364
+ for (const e of entries) {
1365
+ if (e.name.startsWith(".") && e.name !== "." && e.name !== "..") {
1366
+ if (SKIP_DIRS.has(e.name)) continue;
1367
+ }
1368
+ if (SKIP_DIRS.has(e.name)) continue;
1369
+ const full = join3(dir, e.name);
1370
+ if (e.isDirectory()) {
1371
+ await walk(full, depth - 1, counts);
1372
+ } else if (e.isFile()) {
1373
+ const tag = EXT_TO_TAG[extname(e.name).toLowerCase()];
1374
+ if (tag) counts.set(tag, (counts.get(tag) ?? 0) + 1);
1375
+ }
1376
+ }
1377
+ }
1378
+ var FRAMEWORK_ALLOWLIST = /* @__PURE__ */ new Set([
1379
+ "next",
1380
+ "react",
1381
+ "vue",
1382
+ "svelte",
1383
+ "angular",
1384
+ "solid",
1385
+ "fastapi",
1386
+ "django",
1387
+ "flask",
1388
+ "rails",
1389
+ "fastify",
1390
+ "hono",
1391
+ "express",
1392
+ "nest",
1393
+ "axum",
1394
+ "actix"
1395
+ ]);
1396
+ async function detectFrameworks(cwd = process.cwd()) {
1397
+ const found = /* @__PURE__ */ new Set();
1398
+ const pkgJsonPath = join3(cwd, "package.json");
1399
+ if (await pathExists2(pkgJsonPath)) {
1400
+ try {
1401
+ const pkg = JSON.parse(await readFile3(pkgJsonPath, "utf8"));
1402
+ const allDeps = Object.keys({
1403
+ ...pkg.dependencies,
1404
+ ...pkg.devDependencies,
1405
+ ...pkg.peerDependencies
1406
+ });
1407
+ for (const dep of allDeps) {
1408
+ const root = dep.split("/").pop()?.toLowerCase() ?? "";
1409
+ if (FRAMEWORK_ALLOWLIST.has(root)) found.add(root);
1410
+ if (root === "core" && dep.startsWith("@nestjs/")) found.add("nest");
1411
+ }
1412
+ } catch {
1413
+ }
1414
+ }
1415
+ const pyprojectPath = join3(cwd, "pyproject.toml");
1416
+ if (await pathExists2(pyprojectPath)) {
1417
+ try {
1418
+ const text = (await readFile3(pyprojectPath, "utf8")).toLowerCase();
1419
+ for (const fw of ["fastapi", "django", "flask"]) {
1420
+ if (text.includes(fw)) found.add(fw);
1421
+ }
1422
+ } catch {
1423
+ }
1424
+ }
1425
+ const cargoPath = join3(cwd, "Cargo.toml");
1426
+ if (await pathExists2(cargoPath)) {
1427
+ try {
1428
+ const text = (await readFile3(cargoPath, "utf8")).toLowerCase();
1429
+ for (const fw of ["axum", "actix"]) {
1430
+ if (text.includes(fw)) found.add(fw);
1431
+ }
1432
+ } catch {
1433
+ }
1434
+ }
1435
+ return [...found].slice(0, 8);
1436
+ }
1437
+ async function installedSlugsHash() {
1438
+ const lockPath = join3(process.env["KORDIS_HOME"] ?? join3(homedir3(), ".kordis"), "installed.json");
1439
+ if (!await pathExists2(lockPath)) return [];
1440
+ let parsed;
1441
+ try {
1442
+ parsed = JSON.parse(await readFile3(lockPath, "utf8"));
1443
+ } catch {
1444
+ return [];
1445
+ }
1446
+ const slugs = Object.keys(parsed.packages ?? {});
1447
+ return slugs.map((s) => createHash("sha256").update(s).digest("hex").slice(0, 8)).slice(0, 50);
1448
+ }
1449
+ async function collectContext(cwd = process.cwd()) {
1450
+ const [tools, languages, frameworks, installed_slugs_hash] = await Promise.all([
1451
+ detectTools(),
1452
+ detectLanguages(cwd),
1453
+ detectFrameworks(cwd),
1454
+ installedSlugsHash()
1455
+ ]);
1456
+ const ctx = {};
1457
+ if (tools.length > 0) ctx.tools = tools;
1458
+ if (languages.length > 0) ctx.languages = languages;
1459
+ if (frameworks.length > 0) ctx.frameworks = frameworks;
1460
+ if (installed_slugs_hash.length > 0) ctx.installed_slugs_hash = installed_slugs_hash;
1461
+ return ctx;
1462
+ }
1463
+ function buildContextHeader(ctx) {
1464
+ if ((!ctx.tools || ctx.tools.length === 0) && (!ctx.languages || ctx.languages.length === 0) && (!ctx.frameworks || ctx.frameworks.length === 0) && (!ctx.installed_slugs_hash || ctx.installed_slugs_hash.length === 0)) {
1465
+ return null;
1466
+ }
1467
+ const tryFields = (fields) => JSON.stringify(fields);
1468
+ let payload = { ...ctx };
1469
+ const dropOrder = [
1470
+ "frameworks",
1471
+ "languages",
1472
+ "installed_slugs_hash"
1473
+ ];
1474
+ let serialised = tryFields(payload);
1475
+ for (const drop of dropOrder) {
1476
+ if (Buffer.byteLength(serialised, "utf8") <= CONTEXT_HEADER_MAX_BYTES) break;
1477
+ payload = { ...payload };
1478
+ delete payload[drop];
1479
+ serialised = tryFields(payload);
1480
+ }
1481
+ if (Buffer.byteLength(serialised, "utf8") > CONTEXT_HEADER_MAX_BYTES) {
1482
+ return null;
1483
+ }
1484
+ return serialised;
1485
+ }
1486
+ var cachedHeader = null;
1487
+ var CACHE_TTL_MS = 5 * 60 * 1e3;
1488
+ async function getCachedContextHeader(cwd = process.cwd()) {
1489
+ if (cachedHeader && Date.now() - cachedHeader.ts < CACHE_TTL_MS) {
1490
+ return cachedHeader.value;
1491
+ }
1492
+ const ctx = await collectContext(cwd);
1493
+ const header = buildContextHeader(ctx);
1494
+ cachedHeader = { ts: Date.now(), value: header };
1495
+ return header;
1496
+ }
1497
+
1149
1498
  // src/version.ts
1150
1499
  import { readFileSync } from "fs";
1151
1500
  import { fileURLToPath } from "url";
1152
- import { dirname, join as join3 } from "path";
1501
+ import { dirname, join as join4 } from "path";
1153
1502
  var __dir = dirname(fileURLToPath(import.meta.url));
1154
- var pkgPath = join3(__dir, "..", "package.json");
1503
+ var pkgPath = join4(__dir, "..", "package.json");
1155
1504
  var VERSION = (() => {
1156
1505
  try {
1157
1506
  const raw = readFileSync(pkgPath, "utf8");
@@ -1168,10 +1517,17 @@ async function useClient() {
1168
1517
  const keychain = await createKeychain();
1169
1518
  const username = cfg.username ?? null;
1170
1519
  let currentTokens = username ? await keychain.load(username) : null;
1520
+ const personalizationEnabled = cfg.personalization === "on" && process.env["KORDIS_NO_CONTEXT"] !== "1" && !process.argv.includes("--no-context");
1171
1521
  const api = new KordisClient({
1172
1522
  baseUrl: cfg.apiUrl,
1173
1523
  userAgent: `kordis-cli/${VERSION} node/${process.version}`,
1174
- getAccessToken: async () => currentTokens?.access_token
1524
+ getAccessToken: async () => currentTokens?.access_token,
1525
+ ...personalizationEnabled ? {
1526
+ getDynamicHeaders: async () => {
1527
+ const value = await getCachedContextHeader();
1528
+ return value ? { [CONTEXT_HEADER_NAME]: value } : {};
1529
+ }
1530
+ } : {}
1175
1531
  });
1176
1532
  const authedRequest = async (fn) => {
1177
1533
  try {
@@ -1334,6 +1690,70 @@ function whoamiCommand() {
1334
1690
 
1335
1691
  // src/commands/search.ts
1336
1692
  import { Command as Command3 } from "commander";
1693
+
1694
+ // src/lib/recommendation-cache.ts
1695
+ import { readFile as readFile4, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
1696
+ import { homedir as homedir4 } from "os";
1697
+ import { join as join5 } from "path";
1698
+ function cacheDir() {
1699
+ return process.env["KORDIS_HOME"] ?? join5(homedir4(), ".kordis");
1700
+ }
1701
+ function cachePath() {
1702
+ return join5(cacheDir(), "recommendations.json");
1703
+ }
1704
+ async function readCache() {
1705
+ try {
1706
+ const raw = await readFile4(cachePath(), "utf8");
1707
+ const parsed = JSON.parse(raw);
1708
+ if (!parsed.generated_at || !Array.isArray(parsed.items)) return null;
1709
+ return parsed;
1710
+ } catch {
1711
+ return null;
1712
+ }
1713
+ }
1714
+ async function writeCache(items, generatedAt) {
1715
+ await mkdir3(cacheDir(), { recursive: true });
1716
+ const payload = {
1717
+ generated_at: generatedAt,
1718
+ items,
1719
+ seen_at: null
1720
+ };
1721
+ await writeFile3(cachePath(), JSON.stringify(payload, null, 2) + "\n", "utf8");
1722
+ }
1723
+ async function markSeen() {
1724
+ const existing = await readCache();
1725
+ if (!existing) return;
1726
+ await mkdir3(cacheDir(), { recursive: true });
1727
+ const updated = {
1728
+ ...existing,
1729
+ seen_at: (/* @__PURE__ */ new Date()).toISOString()
1730
+ };
1731
+ await writeFile3(cachePath(), JSON.stringify(updated, null, 2) + "\n", "utf8");
1732
+ }
1733
+
1734
+ // src/lib/recommendation-footer.ts
1735
+ var FRESHNESS_WINDOW_MS = 7 * 24 * 60 * 60 * 1e3;
1736
+ async function maybeRenderReminder() {
1737
+ try {
1738
+ const cfg = await loadCliConfig();
1739
+ if (cfg.personalization === "off") return null;
1740
+ const cache = await readCache();
1741
+ if (!cache || cache.items.length === 0) return null;
1742
+ const generated = new Date(cache.generated_at).getTime();
1743
+ if (!Number.isFinite(generated)) return null;
1744
+ if (Date.now() - generated > FRESHNESS_WINDOW_MS) return null;
1745
+ if (cache.seen_at) {
1746
+ const seen = new Date(cache.seen_at).getTime();
1747
+ if (Number.isFinite(seen) && seen >= generated) return null;
1748
+ }
1749
+ const n = cache.items.length;
1750
+ return `\u2192 ${n} new recommendation${n === 1 ? "" : "s"} match your stack: kordis recommend`;
1751
+ } catch {
1752
+ return null;
1753
+ }
1754
+ }
1755
+
1756
+ // src/commands/search.ts
1337
1757
  var SORT_MODES = [
1338
1758
  "relevance",
1339
1759
  "stars",
@@ -1435,8 +1855,8 @@ function searchCommand() {
1435
1855
 
1436
1856
  // src/commands/push.ts
1437
1857
  import { Command as Command4 } from "commander";
1438
- import { readFile as readFile3, stat, readdir } from "fs/promises";
1439
- import { resolve, join as join4, posix, sep } from "path";
1858
+ import { readFile as readFile5, stat as stat2, readdir as readdir2 } from "fs/promises";
1859
+ import { resolve, join as join6, posix, sep } from "path";
1440
1860
  import JSZip from "jszip";
1441
1861
 
1442
1862
  // ../../packages/harness-schema/src/parser.ts
@@ -1489,9 +1909,9 @@ var FrontmatterParseError = class extends Error {
1489
1909
 
1490
1910
  // ../../packages/harness-schema/src/deps.ts
1491
1911
  init_src();
1492
- import { z as z10 } from "zod";
1912
+ import { z as z12 } from "zod";
1493
1913
  var StringDepRe = /^([a-z0-9](-?[a-z0-9])*\/[a-z0-9](-?[a-z0-9])*)(?:@(.+))?$/;
1494
- var ObjectDepSchema = z10.object({
1914
+ var ObjectDepSchema = z12.object({
1495
1915
  full_slug: FullSlugSchema,
1496
1916
  version_range: SemverRangeSchema.default("*")
1497
1917
  });
@@ -1527,50 +1947,50 @@ init_src();
1527
1947
 
1528
1948
  // ../../packages/harness-schema/src/types/skill.ts
1529
1949
  init_src();
1530
- import { z as z11 } from "zod";
1531
- var SkillFrontmatterSchema = z11.object({
1532
- name: z11.string().min(1).max(64).regex(/^[a-z0-9]+(-[a-z0-9]+)*$/, "name must be lowercase-with-hyphens (Agent Skills spec)"),
1533
- description: z11.string().min(1).max(2048),
1950
+ import { z as z13 } from "zod";
1951
+ var SkillFrontmatterSchema = z13.object({
1952
+ name: z13.string().min(1).max(64).regex(/^[a-z0-9]+(-[a-z0-9]+)*$/, "name must be lowercase-with-hyphens (Agent Skills spec)"),
1953
+ description: z13.string().min(1).max(2048),
1534
1954
  version: SemverSchema.optional(),
1535
1955
  // Agent Skills optional metadata
1536
- license: z11.string().max(64).optional(),
1537
- homepage: z11.string().url().optional(),
1538
- model: z11.string().optional(),
1956
+ license: z13.string().max(64).optional(),
1957
+ homepage: z13.string().url().optional(),
1958
+ model: z13.string().optional(),
1539
1959
  // model override hint
1540
- "allowed-tools": z11.array(z11.string()).optional(),
1541
- "disable-model-invocation": z11.boolean().optional(),
1960
+ "allowed-tools": z13.array(z13.string()).optional(),
1961
+ "disable-model-invocation": z13.boolean().optional(),
1542
1962
  // Kordis-forwarded cross-type fields (kept minimal to not pollute the spec)
1543
- title: z11.string().min(1).max(200).optional(),
1544
- tags: z11.array(TagSchema).max(16).optional(),
1545
- tools: z11.array(ToolSlugSchema).optional()
1963
+ title: z13.string().min(1).max(200).optional(),
1964
+ tags: z13.array(TagSchema).max(16).optional(),
1965
+ tools: z13.array(ToolSlugSchema).optional()
1546
1966
  }).strict();
1547
1967
 
1548
1968
  // ../../packages/harness-schema/src/types/mcp-server.ts
1549
1969
  init_src();
1550
- import { z as z12 } from "zod";
1551
- var McpTransportSchema = z12.union([
1552
- z12.object({
1553
- type: z12.literal("stdio"),
1554
- command: z12.string().min(1),
1555
- args: z12.array(z12.string()).optional().default([]),
1556
- env: z12.record(z12.string()).optional().default({})
1970
+ import { z as z14 } from "zod";
1971
+ var McpTransportSchema = z14.union([
1972
+ z14.object({
1973
+ type: z14.literal("stdio"),
1974
+ command: z14.string().min(1),
1975
+ args: z14.array(z14.string()).optional().default([]),
1976
+ env: z14.record(z14.string()).optional().default({})
1557
1977
  }),
1558
- z12.object({
1559
- type: z12.enum(["http", "sse"]),
1560
- url: z12.string().url(),
1561
- headers: z12.record(z12.string()).optional().default({})
1978
+ z14.object({
1979
+ type: z14.enum(["http", "sse"]),
1980
+ url: z14.string().url(),
1981
+ headers: z14.record(z14.string()).optional().default({})
1562
1982
  })
1563
1983
  ]);
1564
- var McpCapabilitiesSchema = z12.object({
1565
- tools: z12.array(z12.string()).optional().default([]),
1566
- resources: z12.array(z12.string()).optional().default([]),
1567
- prompts: z12.array(z12.string()).optional().default([])
1984
+ var McpCapabilitiesSchema = z14.object({
1985
+ tools: z14.array(z14.string()).optional().default([]),
1986
+ resources: z14.array(z14.string()).optional().default([]),
1987
+ prompts: z14.array(z14.string()).optional().default([])
1568
1988
  });
1569
- var McpServerFrontmatterSchema = z12.object({
1570
- name: z12.string().min(1).max(64).regex(/^[a-z0-9]+(-[a-z0-9]+)*$/),
1571
- description: z12.string().min(1).max(2048),
1989
+ var McpServerFrontmatterSchema = z14.object({
1990
+ name: z14.string().min(1).max(64).regex(/^[a-z0-9]+(-[a-z0-9]+)*$/),
1991
+ description: z14.string().min(1).max(2048),
1572
1992
  version: SemverSchema,
1573
- mcp_spec_version: z12.string().default("2026-03"),
1993
+ mcp_spec_version: z14.string().default("2026-03"),
1574
1994
  transport: McpTransportSchema,
1575
1995
  capabilities: McpCapabilitiesSchema.optional().default({
1576
1996
  tools: [],
@@ -1578,35 +1998,35 @@ var McpServerFrontmatterSchema = z12.object({
1578
1998
  prompts: []
1579
1999
  }),
1580
2000
  /** Human-readable setup / auth instructions. Surfaced in `kordis info`. */
1581
- auth_notes: z12.string().optional(),
2001
+ auth_notes: z14.string().optional(),
1582
2002
  /** Required env vars with description + whether they must be secret. */
1583
- env_schema: z12.record(
1584
- z12.object({
1585
- description: z12.string(),
1586
- required: z12.boolean().default(true),
1587
- secret: z12.boolean().default(false)
2003
+ env_schema: z14.record(
2004
+ z14.object({
2005
+ description: z14.string(),
2006
+ required: z14.boolean().default(true),
2007
+ secret: z14.boolean().default(false)
1588
2008
  })
1589
2009
  ).optional(),
1590
- homepage: z12.string().url().optional(),
1591
- repository: z12.string().url().optional(),
1592
- license: z12.string().max(64).optional(),
1593
- title: z12.string().min(1).max(200).optional()
2010
+ homepage: z14.string().url().optional(),
2011
+ repository: z14.string().url().optional(),
2012
+ license: z14.string().max(64).optional(),
2013
+ title: z14.string().min(1).max(200).optional()
1594
2014
  }).strict();
1595
2015
 
1596
2016
  // ../../packages/harness-schema/src/types/project-rules.ts
1597
2017
  init_src();
1598
- import { z as z13 } from "zod";
1599
- var ProjectRulesFrontmatterSchema = z13.object({
1600
- title: z13.string().min(1).max(200),
1601
- description: z13.string().min(1).max(2048),
2018
+ import { z as z15 } from "zod";
2019
+ var ProjectRulesFrontmatterSchema = z15.object({
2020
+ title: z15.string().min(1).max(200),
2021
+ description: z15.string().min(1).max(2048),
1602
2022
  version: SemverSchema.optional(),
1603
- scope: z13.enum(["root", "nested", "globs"]).default("root"),
1604
- target_path: z13.string().optional(),
1605
- globs: z13.array(z13.string()).optional(),
1606
- target_tools: z13.array(ToolSlugSchema).optional().default([]),
2023
+ scope: z15.enum(["root", "nested", "globs"]).default("root"),
2024
+ target_path: z15.string().optional(),
2025
+ globs: z15.array(z15.string()).optional(),
2026
+ target_tools: z15.array(ToolSlugSchema).optional().default([]),
1607
2027
  /** When true, installation is idempotent via `<!-- kordis:... -->` markers. */
1608
- mergeable: z13.boolean().default(true),
1609
- license: z13.string().max(64).optional()
2028
+ mergeable: z15.boolean().default(true),
2029
+ license: z15.string().max(64).optional()
1610
2030
  }).strict().refine(
1611
2031
  (v) => v.scope !== "nested" || !!v.target_path,
1612
2032
  { message: 'scope="nested" requires target_path' }
@@ -1617,33 +2037,33 @@ var ProjectRulesFrontmatterSchema = z13.object({
1617
2037
 
1618
2038
  // ../../packages/harness-schema/src/types/custom-agent.ts
1619
2039
  init_src();
1620
- import { z as z14 } from "zod";
1621
- var CustomAgentFrontmatterSchema = z14.object({
1622
- name: z14.string().min(1).max(64).regex(/^[a-z0-9]+(-[a-z0-9]+)*$/),
1623
- description: z14.string().min(1).max(2048),
1624
- title: z14.string().min(1).max(200).optional(),
2040
+ import { z as z16 } from "zod";
2041
+ var CustomAgentFrontmatterSchema = z16.object({
2042
+ name: z16.string().min(1).max(64).regex(/^[a-z0-9]+(-[a-z0-9]+)*$/),
2043
+ description: z16.string().min(1).max(2048),
2044
+ title: z16.string().min(1).max(200).optional(),
1625
2045
  version: SemverSchema.optional(),
1626
2046
  /** System prompt that defines the agent's persona + rules. Required. */
1627
- system_prompt: z14.string().min(1).max(5e4),
2047
+ system_prompt: z16.string().min(1).max(5e4),
1628
2048
  /** Optional model override — string passed verbatim to the host. */
1629
- model: z14.string().optional(),
2049
+ model: z16.string().optional(),
1630
2050
  /** Tools this agent can call. Host-specific names (no uniform namespace). */
1631
- allowed_tools: z14.array(z14.string()).optional().default([]),
1632
- disallowed_tools: z14.array(z14.string()).optional().default([]),
2051
+ allowed_tools: z16.array(z16.string()).optional().default([]),
2052
+ disallowed_tools: z16.array(z16.string()).optional().default([]),
1633
2053
  /** Fork/context behaviour hint (Claude: `context: fork` etc.). */
1634
- context: z14.enum(["inherit", "fork", "isolated"]).optional(),
2054
+ context: z16.enum(["inherit", "fork", "isolated"]).optional(),
1635
2055
  /** Known-good tool runtimes. Empty = untested everywhere. */
1636
- tool_compat: z14.array(ToolSlugSchema).optional().default([]),
2056
+ tool_compat: z16.array(ToolSlugSchema).optional().default([]),
1637
2057
  /** Pre-load skills when this agent activates. */
1638
- preload_skills: z14.array(z14.string()).optional().default([]),
1639
- license: z14.string().max(64).optional(),
1640
- homepage: z14.string().url().optional()
2058
+ preload_skills: z16.array(z16.string()).optional().default([]),
2059
+ license: z16.string().max(64).optional(),
2060
+ homepage: z16.string().url().optional()
1641
2061
  }).strict();
1642
2062
 
1643
2063
  // ../../packages/harness-schema/src/types/hook.ts
1644
2064
  init_src();
1645
- import { z as z15 } from "zod";
1646
- var HookEventSchema = z15.enum([
2065
+ import { z as z17 } from "zod";
2066
+ var HookEventSchema = z17.enum([
1647
2067
  "PreToolUse",
1648
2068
  "PostToolUse",
1649
2069
  "UserPromptSubmit",
@@ -1655,37 +2075,37 @@ var HookEventSchema = z15.enum([
1655
2075
  "SubagentStop",
1656
2076
  "Background"
1657
2077
  ]);
1658
- var HookActionSchema = z15.union([
1659
- z15.object({
1660
- type: z15.literal("command"),
1661
- command: z15.string().min(1),
1662
- timeout_ms: z15.number().int().positive().max(6e5).optional(),
1663
- blocking: z15.boolean().default(false)
2078
+ var HookActionSchema = z17.union([
2079
+ z17.object({
2080
+ type: z17.literal("command"),
2081
+ command: z17.string().min(1),
2082
+ timeout_ms: z17.number().int().positive().max(6e5).optional(),
2083
+ blocking: z17.boolean().default(false)
1664
2084
  }),
1665
- z15.object({
1666
- type: z15.literal("http"),
1667
- url: z15.string().url(),
1668
- method: z15.enum(["GET", "POST", "PUT", "PATCH"]).default("POST"),
1669
- headers: z15.record(z15.string()).optional().default({}),
1670
- timeout_ms: z15.number().int().positive().max(6e5).optional(),
1671
- blocking: z15.boolean().default(false)
2085
+ z17.object({
2086
+ type: z17.literal("http"),
2087
+ url: z17.string().url(),
2088
+ method: z17.enum(["GET", "POST", "PUT", "PATCH"]).default("POST"),
2089
+ headers: z17.record(z17.string()).optional().default({}),
2090
+ timeout_ms: z17.number().int().positive().max(6e5).optional(),
2091
+ blocking: z17.boolean().default(false)
1672
2092
  })
1673
2093
  ]);
1674
- var HookBindingSchema = z15.object({
2094
+ var HookBindingSchema = z17.object({
1675
2095
  event: HookEventSchema,
1676
- matcher: z15.string().optional(),
2096
+ matcher: z17.string().optional(),
1677
2097
  action: HookActionSchema
1678
2098
  });
1679
- var HookFrontmatterSchema = z15.object({
1680
- name: z15.string().min(1).max(64).regex(/^[a-z0-9]+(-[a-z0-9]+)*$/),
1681
- description: z15.string().min(1).max(2048),
1682
- title: z15.string().min(1).max(200).optional(),
2099
+ var HookFrontmatterSchema = z17.object({
2100
+ name: z17.string().min(1).max(64).regex(/^[a-z0-9]+(-[a-z0-9]+)*$/),
2101
+ description: z17.string().min(1).max(2048),
2102
+ title: z17.string().min(1).max(200).optional(),
1683
2103
  version: SemverSchema.optional(),
1684
2104
  /** One or more hook bindings. */
1685
- hooks: z15.array(HookBindingSchema).min(1),
2105
+ hooks: z17.array(HookBindingSchema).min(1),
1686
2106
  /** Declared to surface in search filters — always "claude-code" for now. */
1687
- tool_compat: z15.array(z15.enum(["claude-code"])).default(["claude-code"]),
1688
- license: z15.string().max(64).optional()
2107
+ tool_compat: z17.array(z17.enum(["claude-code"])).default(["claude-code"]),
2108
+ license: z17.string().max(64).optional()
1689
2109
  }).strict();
1690
2110
 
1691
2111
  // ../../packages/harness-schema/src/validator.ts
@@ -1953,10 +2373,13 @@ function zodFail(issues) {
1953
2373
  hint = "description must be 1\u20132048 chars; current is too long. Move detail into the body.";
1954
2374
  } else if (field === "description" && msg.includes("small")) {
1955
2375
  hint = "description must be 1\u20132048 chars; provide a one-paragraph summary.";
2376
+ } else if (field) {
2377
+ hint = `Fix the value of frontmatter field "${field}" to match the schema. See the per-type spec under packages/harness-schema/src/types/ for the exact shape.`;
2378
+ } else {
2379
+ hint = "Fix the frontmatter to match the schema. See the per-type spec under packages/harness-schema/src/types/ for the exact shape.";
1956
2380
  }
1957
- const extra = {};
2381
+ const extra = { hint };
1958
2382
  if (field) extra.field = field;
1959
- if (hint) extra.hint = hint;
1960
2383
  return flag("invalid_frontmatter", i.message, extra);
1961
2384
  })
1962
2385
  };
@@ -2001,7 +2424,7 @@ function pushCommand() {
2001
2424
  async (path, opts) => {
2002
2425
  const bundle = await useClient();
2003
2426
  const abs = resolve(process.cwd(), path);
2004
- const s = await stat(abs);
2427
+ const s = await stat2(abs);
2005
2428
  let content;
2006
2429
  let assets = [];
2007
2430
  if (s.isDirectory()) {
@@ -2011,7 +2434,7 @@ function pushCommand() {
2011
2434
  ({ content, assets } = await bundleSkillZip(abs));
2012
2435
  content = ensureTypeField(content, "skill");
2013
2436
  } else {
2014
- content = await readFile3(abs, "utf8");
2437
+ content = await readFile5(abs, "utf8");
2015
2438
  }
2016
2439
  let nameFromFm = null;
2017
2440
  try {
@@ -2192,19 +2615,19 @@ function shouldExclude(relPath, isDir) {
2192
2615
  return false;
2193
2616
  }
2194
2617
  async function walkDir(base, relDir, collected) {
2195
- const entries = await readdir(join4(base, relDir), { withFileTypes: true });
2618
+ const entries = await readdir2(join6(base, relDir), { withFileTypes: true });
2196
2619
  for (const entry of entries) {
2197
2620
  const rel = relDir ? `${relDir}/${entry.name}` : entry.name;
2198
2621
  if (shouldExclude(rel, entry.isDirectory())) continue;
2199
2622
  if (entry.isDirectory()) {
2200
2623
  await walkDir(base, rel, collected);
2201
2624
  } else if (entry.isFile()) {
2202
- collected.push({ rel, abs: join4(base, rel) });
2625
+ collected.push({ rel, abs: join6(base, rel) });
2203
2626
  }
2204
2627
  }
2205
2628
  }
2206
2629
  async function readAsset(absPath) {
2207
- const buf = await readFile3(absPath);
2630
+ const buf = await readFile5(absPath);
2208
2631
  const text = buf.toString("utf8");
2209
2632
  const roundtrip = Buffer.from(text, "utf8");
2210
2633
  if (roundtrip.equals(buf)) {
@@ -2221,7 +2644,7 @@ async function bundleDirectory(dir) {
2221
2644
  if (!skillEntry) {
2222
2645
  throw new Error(`No SKILL.md found in directory ${dir}. Push a directory that has SKILL.md at its root.`);
2223
2646
  }
2224
- const content = await readFile3(skillEntry.abs, "utf8");
2647
+ const content = await readFile5(skillEntry.abs, "utf8");
2225
2648
  const assets = [];
2226
2649
  for (const f of all) {
2227
2650
  if (f.rel === "SKILL.md") continue;
@@ -2231,7 +2654,7 @@ async function bundleDirectory(dir) {
2231
2654
  return { content, assets };
2232
2655
  }
2233
2656
  async function bundleSkillZip(zipPath) {
2234
- const buf = await readFile3(zipPath);
2657
+ const buf = await readFile5(zipPath);
2235
2658
  const zip = await JSZip.loadAsync(buf);
2236
2659
  const entries = Object.keys(zip.files).filter((k) => !zip.files[k].dir);
2237
2660
  if (entries.length === 0) throw new Error(".skill file is empty");
@@ -2265,8 +2688,8 @@ async function bundleSkillZip(zipPath) {
2265
2688
 
2266
2689
  // src/commands/pull.ts
2267
2690
  import { Command as Command5 } from "commander";
2268
- import { mkdir as mkdir4, writeFile as writeFile4 } from "fs/promises";
2269
- import { dirname as dirname2, join as join6 } from "path";
2691
+ import { mkdir as mkdir5, writeFile as writeFile5 } from "fs/promises";
2692
+ import { dirname as dirname2, join as join8 } from "path";
2270
2693
 
2271
2694
  // ../../packages/translators/src/skill/identity.ts
2272
2695
  function buildSkillFile(input, outputPath) {
@@ -2708,21 +3131,21 @@ function getTranslator(type, format) {
2708
3131
  }
2709
3132
 
2710
3133
  // src/lockfile.ts
2711
- import { homedir as homedir3 } from "os";
2712
- import { join as join5 } from "path";
2713
- import { readFile as readFile4, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
2714
- var LOCKFILE_PATH = join5(homedir3(), ".kordis", "installed.json");
3134
+ import { homedir as homedir5 } from "os";
3135
+ import { join as join7 } from "path";
3136
+ import { readFile as readFile6, writeFile as writeFile4, mkdir as mkdir4 } from "fs/promises";
3137
+ var LOCKFILE_PATH = join7(homedir5(), ".kordis", "installed.json");
2715
3138
  async function readLockfile() {
2716
3139
  try {
2717
- const raw = await readFile4(LOCKFILE_PATH, "utf8");
3140
+ const raw = await readFile6(LOCKFILE_PATH, "utf8");
2718
3141
  return JSON.parse(raw);
2719
3142
  } catch {
2720
3143
  return { packages: {} };
2721
3144
  }
2722
3145
  }
2723
3146
  async function writeLockfile(lf) {
2724
- await mkdir3(join5(homedir3(), ".kordis"), { recursive: true });
2725
- await writeFile3(LOCKFILE_PATH, JSON.stringify(lf, null, 2) + "\n", "utf8");
3147
+ await mkdir4(join7(homedir5(), ".kordis"), { recursive: true });
3148
+ await writeFile4(LOCKFILE_PATH, JSON.stringify(lf, null, 2) + "\n", "utf8");
2726
3149
  }
2727
3150
  async function recordInstalled(fullSlug, entry) {
2728
3151
  const lf = await readLockfile();
@@ -2758,8 +3181,8 @@ async function doPullInstall(fullSlug, tool, installRoot, version) {
2758
3181
  assets: contentRes.assets
2759
3182
  });
2760
3183
  for (const f of result.files) {
2761
- const abs = join6(installRoot, f.path);
2762
- await mkdir4(dirname2(abs), { recursive: true });
3184
+ const abs = join8(installRoot, f.path);
3185
+ await mkdir5(dirname2(abs), { recursive: true });
2763
3186
  await writeEmittedFile(abs, f.content, f.encoding);
2764
3187
  written.push(abs);
2765
3188
  }
@@ -2778,8 +3201,8 @@ async function doPullInstall(fullSlug, tool, installRoot, version) {
2778
3201
  `Tool "${format}" is not supported for type "${parsed.type}"; installed canonical form instead.`
2779
3202
  );
2780
3203
  for (const f of result.files) {
2781
- const abs = join6(installRoot, f.path);
2782
- await mkdir4(dirname2(abs), { recursive: true });
3204
+ const abs = join8(installRoot, f.path);
3205
+ await mkdir5(dirname2(abs), { recursive: true });
2783
3206
  await writeEmittedFile(abs, f.content, f.encoding);
2784
3207
  written.push(abs);
2785
3208
  }
@@ -2836,11 +3259,13 @@ function pullCommand() {
2836
3259
  for (const n of result.notes) printHuman(c.yellow(` ! ${n}`));
2837
3260
  }
2838
3261
  });
3262
+ const reminder = await maybeRenderReminder();
3263
+ if (reminder) printHuman(c.gray(reminder));
2839
3264
  }
2840
3265
  );
2841
3266
  }
2842
3267
  async function detectTool(root) {
2843
- const { stat: stat2 } = await import("fs/promises");
3268
+ const { stat: stat3 } = await import("fs/promises");
2844
3269
  const checks = [
2845
3270
  [".claude", "claude-code"],
2846
3271
  [".cursor", "cursor"],
@@ -2856,7 +3281,7 @@ async function detectTool(root) {
2856
3281
  ];
2857
3282
  for (const [path, tool] of checks) {
2858
3283
  try {
2859
- await stat2(join6(root, path));
3284
+ await stat3(join8(root, path));
2860
3285
  return tool;
2861
3286
  } catch {
2862
3287
  }
@@ -2865,9 +3290,9 @@ async function detectTool(root) {
2865
3290
  }
2866
3291
  async function writeEmittedFile(abs, content, encoding) {
2867
3292
  if (encoding === "base64") {
2868
- await writeFile4(abs, Buffer.from(content, "base64"));
3293
+ await writeFile5(abs, Buffer.from(content, "base64"));
2869
3294
  } else {
2870
- await writeFile4(abs, content, "utf8");
3295
+ await writeFile5(abs, content, "utf8");
2871
3296
  }
2872
3297
  }
2873
3298
 
@@ -2988,11 +3413,12 @@ function logoutCommand() {
2988
3413
  }
2989
3414
 
2990
3415
  // src/commands/setup.ts
2991
- import { readFile as readFile5, writeFile as writeFile5, mkdir as mkdir5, access as access2 } from "fs/promises";
2992
- import { homedir as homedir4 } from "os";
2993
- import { join as join7, dirname as dirname3 } from "path";
3416
+ import { readFile as readFile7, writeFile as writeFile6, mkdir as mkdir6, access as access3 } from "fs/promises";
3417
+ import { homedir as homedir6 } from "os";
3418
+ import { join as join9, dirname as dirname3 } from "path";
2994
3419
  import { fileURLToPath as fileURLToPath2 } from "url";
2995
3420
  import { Command as Command9 } from "commander";
3421
+ import prompts from "prompts";
2996
3422
  var MARKER_START = "<!-- kordis-start -->";
2997
3423
  var MARKER_END = "<!-- kordis-end -->";
2998
3424
  var HUB_TARGETS = [
@@ -3004,7 +3430,8 @@ var SUB_SKILLS = [
3004
3430
  "kordis-search",
3005
3431
  "kordis-using",
3006
3432
  "kordis-publishing",
3007
- "kordis-social"
3433
+ "kordis-social",
3434
+ "kordis-recommend"
3008
3435
  ];
3009
3436
  var SKILL_TARGETS = [
3010
3437
  { tool: "Claude Code", dir: ".claude", skillsDir: ".claude/skills" },
@@ -3013,7 +3440,7 @@ var SKILL_TARGETS = [
3013
3440
  ];
3014
3441
  async function exists(path) {
3015
3442
  try {
3016
- await access2(path);
3443
+ await access3(path);
3017
3444
  return true;
3018
3445
  } catch {
3019
3446
  return false;
@@ -3023,18 +3450,18 @@ function assetRoots() {
3023
3450
  const __dir2 = dirname3(fileURLToPath2(import.meta.url));
3024
3451
  return {
3025
3452
  // Built binary: bin/kordis.js → assets/ is one level up
3026
- bundled: join7(__dir2, "..", "assets"),
3453
+ bundled: join9(__dir2, "..", "assets"),
3027
3454
  // Dev mode: src/commands/setup.ts → 4 levels up to repo root, then packages/skills/
3028
- source: join7(__dir2, "..", "..", "..", "..", "packages", "skills")
3455
+ source: join9(__dir2, "..", "..", "..", "..", "packages", "skills")
3029
3456
  };
3030
3457
  }
3031
3458
  async function pickAssetRoot() {
3032
3459
  const { bundled, source } = assetRoots();
3033
- return await exists(join7(bundled, "KORDIS.md")) ? bundled : source;
3460
+ return await exists(join9(bundled, "KORDIS.md")) ? bundled : source;
3034
3461
  }
3035
3462
  async function loadHubContent() {
3036
3463
  const root = await pickAssetRoot();
3037
- const raw = await readFile5(join7(root, "KORDIS.md"), "utf-8");
3464
+ const raw = await readFile7(join9(root, "KORDIS.md"), "utf-8");
3038
3465
  if (raw.startsWith("---")) {
3039
3466
  const end = raw.indexOf("\n---", 4);
3040
3467
  if (end !== -1) return raw.slice(end + 4).trimStart();
@@ -3043,7 +3470,7 @@ async function loadHubContent() {
3043
3470
  }
3044
3471
  async function loadSubSkillContent(name) {
3045
3472
  const root = await pickAssetRoot();
3046
- return readFile5(join7(root, name, "SKILL.md"), "utf-8");
3473
+ return readFile7(join9(root, name, "SKILL.md"), "utf-8");
3047
3474
  }
3048
3475
  async function injectBlock(filePath, content, dryRun) {
3049
3476
  const block = `${MARKER_START}
@@ -3052,14 +3479,14 @@ ${MARKER_END}`;
3052
3479
  let existing = "";
3053
3480
  let fileExists = true;
3054
3481
  try {
3055
- existing = await readFile5(filePath, "utf-8");
3482
+ existing = await readFile7(filePath, "utf-8");
3056
3483
  } catch {
3057
3484
  fileExists = false;
3058
3485
  }
3059
3486
  if (!fileExists) {
3060
3487
  if (!dryRun) {
3061
- await mkdir5(dirname3(filePath), { recursive: true });
3062
- await writeFile5(filePath, block + "\n", "utf-8");
3488
+ await mkdir6(dirname3(filePath), { recursive: true });
3489
+ await writeFile6(filePath, block + "\n", "utf-8");
3063
3490
  }
3064
3491
  return "injected";
3065
3492
  }
@@ -3070,31 +3497,31 @@ ${MARKER_END}`;
3070
3497
  if (current === block) return "unchanged";
3071
3498
  if (!dryRun) {
3072
3499
  const updated = existing.slice(0, startIdx) + block + existing.slice(endIdx + MARKER_END.length);
3073
- await writeFile5(filePath, updated, "utf-8");
3500
+ await writeFile6(filePath, updated, "utf-8");
3074
3501
  }
3075
3502
  return "updated";
3076
3503
  }
3077
3504
  if (!dryRun) {
3078
- await writeFile5(filePath, existing.trimEnd() + "\n\n" + block + "\n", "utf-8");
3505
+ await writeFile6(filePath, existing.trimEnd() + "\n\n" + block + "\n", "utf-8");
3079
3506
  }
3080
3507
  return "injected";
3081
3508
  }
3082
3509
  async function copyBundledSkill(filePath, content, dryRun) {
3083
3510
  let existing = null;
3084
3511
  try {
3085
- existing = await readFile5(filePath, "utf-8");
3512
+ existing = await readFile7(filePath, "utf-8");
3086
3513
  } catch {
3087
3514
  }
3088
3515
  if (existing === content) return "unchanged";
3089
3516
  if (!dryRun) {
3090
- await mkdir5(dirname3(filePath), { recursive: true });
3091
- await writeFile5(filePath, content, "utf-8");
3517
+ await mkdir6(dirname3(filePath), { recursive: true });
3518
+ await writeFile6(filePath, content, "utf-8");
3092
3519
  }
3093
3520
  return existing === null ? "injected" : "updated";
3094
3521
  }
3095
3522
  function setupCommand() {
3096
3523
  return new Command9("setup").description("inject kordis usage guide into global agent instruction files").option("--silent", "suppress errors \u2014 safe for postinstall").option("--dry-run", "detect installed tools without writing any files").action(async (opts) => {
3097
- const home = homedir4();
3524
+ const home = homedir6();
3098
3525
  const dryRun = opts.dryRun ?? false;
3099
3526
  let hubContent;
3100
3527
  let subSkillContents;
@@ -3111,8 +3538,8 @@ function setupCommand() {
3111
3538
  let anyFound = false;
3112
3539
  const changedTools = /* @__PURE__ */ new Set();
3113
3540
  for (const target of HUB_TARGETS) {
3114
- const dirPath = join7(home, target.dir);
3115
- const filePath = join7(home, target.file);
3541
+ const dirPath = join9(home, target.dir);
3542
+ const filePath = join9(home, target.file);
3116
3543
  if (!await exists(dirPath)) {
3117
3544
  if (!opts.silent) {
3118
3545
  printHuman(c.gray(` - ${target.tool.padEnd(12)} not installed, skipped`));
@@ -3139,10 +3566,10 @@ function setupCommand() {
3139
3566
  }
3140
3567
  }
3141
3568
  for (const target of SKILL_TARGETS) {
3142
- const dirPath = join7(home, target.dir);
3569
+ const dirPath = join9(home, target.dir);
3143
3570
  if (!await exists(dirPath)) continue;
3144
3571
  for (const subName of SUB_SKILLS) {
3145
- const filePath = join7(home, target.skillsDir, subName, "SKILL.md");
3572
+ const filePath = join9(home, target.skillsDir, subName, "SKILL.md");
3146
3573
  const dest = `~/${target.skillsDir}/${subName}/SKILL.md`;
3147
3574
  try {
3148
3575
  const result = await copyBundledSkill(filePath, subSkillContents[subName], dryRun);
@@ -3174,6 +3601,34 @@ function setupCommand() {
3174
3601
  printHuman(c.gray("No supported agent tools detected."));
3175
3602
  printHuman(c.gray("Run `kordis setup` again after installing Claude Code, Codex, or Gemini CLI."));
3176
3603
  }
3604
+ if (!opts.silent && !dryRun && process.stdin.isTTY && anyFound) {
3605
+ const cfg = await loadCliConfig();
3606
+ if (cfg.personalization === "undecided") {
3607
+ printHuman("");
3608
+ const { enable } = await prompts({
3609
+ type: "confirm",
3610
+ name: "enable",
3611
+ message: "Personalize recommendations using anonymous local signals (installed agent tools, language stats, frameworks)? Server never stores them \u2014 only re-ranks results in-flight.",
3612
+ initial: true
3613
+ });
3614
+ const newCfg = {
3615
+ ...cfg,
3616
+ personalization: enable ? "on" : "off"
3617
+ };
3618
+ await saveCliConfig(newCfg);
3619
+ if (enable) {
3620
+ printHuman(
3621
+ c.green("\u2713 personalization on") + c.gray(" \u2014 disable any time: `KORDIS_NO_CONTEXT=1` env or `--no-context` flag")
3622
+ );
3623
+ } else {
3624
+ printHuman(
3625
+ c.gray(
3626
+ "\u2192 recommendations will use popularity only. Re-enable later via `kordis recommend` if asked again."
3627
+ )
3628
+ );
3629
+ }
3630
+ }
3631
+ }
3177
3632
  });
3178
3633
  }
3179
3634
 
@@ -3274,7 +3729,7 @@ function unstarCommand() {
3274
3729
 
3275
3730
  // src/commands/delete.ts
3276
3731
  import { Command as Command12 } from "commander";
3277
- import prompts from "prompts";
3732
+ import prompts2 from "prompts";
3278
3733
  function deleteCommand() {
3279
3734
  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) => {
3280
3735
  const [username, slug] = fullSlug.split("/");
@@ -3289,7 +3744,7 @@ function deleteCommand() {
3289
3744
  printHuman(` "${detail.description?.slice(0, 80) ?? ""}"`);
3290
3745
  printHuman(c.yellow(` The slug cannot be reused after deletion. This cannot be undone via the CLI.`));
3291
3746
  printHuman("");
3292
- const { confirmed } = await prompts({
3747
+ const { confirmed } = await prompts2({
3293
3748
  type: "confirm",
3294
3749
  name: "confirmed",
3295
3750
  message: `Delete ${c.bold(fullSlug)}?`,
@@ -3703,9 +4158,99 @@ function diffCommand() {
3703
4158
  });
3704
4159
  }
3705
4160
 
4161
+ // src/commands/recommend.ts
4162
+ import { Command as Command18 } from "commander";
4163
+ import prompts3 from "prompts";
4164
+ function recommendCommand() {
4165
+ return new Command18("recommend").description("show personalized package recommendations").option("--limit <n>", "max items to return (1-50)", (v) => parseInt(v, 10), 10).option("--verbose", "include score_breakdown per item", false).option("--no-context", "skip the X-Kordis-Context header for this call").action(async (opts) => {
4166
+ const cfg = await loadCliConfig();
4167
+ if (cfg.personalization === "undecided" && process.stdin.isTTY && !isJsonMode() && opts.context !== false) {
4168
+ const { enable } = await prompts3({
4169
+ type: "confirm",
4170
+ name: "enable",
4171
+ message: "Personalize recommendations using anonymous local signals (installed agent tools, language stats, frameworks)? Server never stores them \u2014 only re-ranks results in-flight.",
4172
+ initial: true
4173
+ });
4174
+ const newCfg = {
4175
+ ...cfg,
4176
+ personalization: enable ? "on" : "off"
4177
+ };
4178
+ await saveCliConfig(newCfg);
4179
+ if (!enable) {
4180
+ printHuman(
4181
+ c.gray(
4182
+ "\u2192 recommendations will use popularity only. Re-enable later: `kordis context --enable`."
4183
+ )
4184
+ );
4185
+ }
4186
+ }
4187
+ const bundle = await useClient();
4188
+ const spin = spinner("Computing recommendations");
4189
+ const res = await bundle.api.recommend({
4190
+ limit: opts.limit,
4191
+ verbose: opts.verbose
4192
+ });
4193
+ spin.succeed();
4194
+ await writeCache(res.items, res.generated_at);
4195
+ await markSeen();
4196
+ printDual({
4197
+ json: res,
4198
+ human: () => {
4199
+ if (res.items.length === 0) {
4200
+ printHuman(c.yellow("(no recommendations available right now)"));
4201
+ printHuman(
4202
+ c.gray(
4203
+ " \u2192 registry may be sparse; try `kordis search --trending` for broader discovery"
4204
+ )
4205
+ );
4206
+ return;
4207
+ }
4208
+ printHuman(
4209
+ `${c.bold(`${res.items.length} pick${res.items.length === 1 ? "" : "s"}`)} ${c.gray(`(${res.personalized ? "personalized" : "popularity-only"} \xB7 generated ${res.generated_at.slice(0, 19)}Z)`)}`
4210
+ );
4211
+ printHuman("");
4212
+ res.items.forEach((it, idx) => renderItem(idx + 1, it, opts.verbose));
4213
+ printHuman("");
4214
+ printHuman(
4215
+ c.gray(
4216
+ "\u2192 install any with `kordis pull <full_slug>` or peek with `kordis read <full_slug>`"
4217
+ )
4218
+ );
4219
+ if (!res.personalized) {
4220
+ printHuman(
4221
+ c.gray(
4222
+ "\u2192 enable personalization for better matches: `kordis context --enable`"
4223
+ )
4224
+ );
4225
+ }
4226
+ }
4227
+ });
4228
+ });
4229
+ }
4230
+ function renderItem(idx, it, verbose) {
4231
+ const confidenceBadge = it.confidence === "high" ? c.green("high") : it.confidence === "medium" ? c.cyan("medium") : c.gray("low");
4232
+ printHuman(
4233
+ `${c.bold(`${idx}. ${it.full_slug}`)} ${c.gray(`v${it.current_version}`)} ${c.cyan(`[${it.type}]`)} ${c.gray(`tier ${it.tier}`)} ${c.gray("\xB7")} ${confidenceBadge} confidence`
4234
+ );
4235
+ printHuman(` ${it.description.length > 100 ? `${it.description.slice(0, 100)}\u2026` : it.description}`);
4236
+ printHuman(c.gray(` \u2605 ${it.stars} \u21E3 ${it.downloads}`));
4237
+ printHuman(c.gray(` \u2192 ${it.why_recommended}`));
4238
+ printHuman(c.gray(` install: ${c.cyan(it.install_command)}`));
4239
+ printHuman(c.gray(` read: ${c.cyan(it.jit_read_command)}`));
4240
+ if (verbose && it.score_breakdown) {
4241
+ const b = it.score_breakdown;
4242
+ printHuman(
4243
+ c.gray(
4244
+ ` score: content=${b.content} cf=${b.cf} popularity=${b.popularity} final=${b.final}`
4245
+ )
4246
+ );
4247
+ }
4248
+ printHuman("");
4249
+ }
4250
+
3706
4251
  // src/index.ts
3707
4252
  function buildProgram() {
3708
- const program = new Command18("kordis").version(VERSION).description("Kordis \u2014 registry for agentic-coding packages").option("--json", "emit JSON output to stdout (default: human)", false).hook("preAction", (thisCommand) => {
4253
+ const program = new Command19("kordis").version(VERSION).description("Kordis \u2014 registry for agentic-coding packages").option("--json", "emit JSON output to stdout (default: human)", false).hook("preAction", (thisCommand) => {
3709
4254
  const opts = thisCommand.optsWithGlobals();
3710
4255
  if (opts.json) setJsonMode(true);
3711
4256
  });
@@ -3726,6 +4271,7 @@ function buildProgram() {
3726
4271
  program.addCommand(outdatedCommand());
3727
4272
  program.addCommand(updateCommand());
3728
4273
  program.addCommand(diffCommand());
4274
+ program.addCommand(recommendCommand());
3729
4275
  program.addCommand(setupCommand());
3730
4276
  return program;
3731
4277
  }
@@ -3745,6 +4291,7 @@ function handleError(err) {
3745
4291
  error: {
3746
4292
  code: err.code,
3747
4293
  message: err.message,
4294
+ ...err.hint ? { hint: err.hint } : {},
3748
4295
  flags: err.flags,
3749
4296
  ...err.retryAfter !== void 0 ? { retry_after_seconds: err.retryAfter } : {},
3750
4297
  request_id: err.requestId
@@ -3752,6 +4299,11 @@ function handleError(err) {
3752
4299
  });
3753
4300
  } else {
3754
4301
  printHuman(c.red(`\u2717 ${err.code}: ${err.message}`));
4302
+ if (err.hint) {
4303
+ for (const line of err.hint.split("\n")) {
4304
+ printHuman(c.gray(` \u2192 ${line}`));
4305
+ }
4306
+ }
3755
4307
  for (const f of err.flags) {
3756
4308
  printHuman(c.red(` - ${f.code}${f.message ? `: ${f.message}` : ""}`));
3757
4309
  if (f.hint) {