@rayul/kordis 0.3.1 → 0.5.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
@@ -704,6 +704,123 @@ var init_api = __esm({
704
704
  }
705
705
  });
706
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
+ * 0.5.0 — number of additional candidates the server had before applying
808
+ * the per-author cap. When `capped_by_author > 0`, the agent can explain
809
+ * "menu is shorter than --limit because diversity cap fired" instead of
810
+ * the user inferring the registry is sparse. Absent on responses from
811
+ * pre-0.5.0 servers.
812
+ */
813
+ capped_by_author: z11.number().int().nonnegative().optional(),
814
+ /**
815
+ * 0.5.0 — total number of candidates the server scored before any
816
+ * trimming/capping. Helps the agent surface "we picked N of M" honesty
817
+ * when the menu is short. Absent on pre-0.5.0 servers.
818
+ */
819
+ total_candidates: z11.number().int().nonnegative().optional()
820
+ });
821
+ }
822
+ });
823
+
707
824
  // ../../packages/contracts/src/index.ts
708
825
  var init_src = __esm({
709
826
  "../../packages/contracts/src/index.ts"() {
@@ -717,6 +834,8 @@ var init_src = __esm({
717
834
  init_comment();
718
835
  init_auth();
719
836
  init_api();
837
+ init_context();
838
+ init_recommend();
720
839
  }
721
840
  });
722
841
 
@@ -797,6 +916,7 @@ var init_client = __esm({
797
916
  const headers = {
798
917
  accept: "application/json",
799
918
  ...await this.authHeaders(),
919
+ ...this.config.getDynamicHeaders ? await this.config.getDynamicHeaders() : {},
800
920
  ...opts.headers ?? {}
801
921
  };
802
922
  if (opts.body !== void 0) headers["content-type"] = "application/json";
@@ -936,6 +1056,20 @@ var init_client = __esm({
936
1056
  popular() {
937
1057
  return this.request("/api/packages/popular");
938
1058
  }
1059
+ /**
1060
+ * Hybrid recommendations (0.4.0+). The personalization signal rides on
1061
+ * the `X-Kordis-Context` header (injected by `getDynamicHeaders`), not in
1062
+ * the query string. Anonymous is fine — server falls back to
1063
+ * popularity-only with `confidence: 'low'`.
1064
+ */
1065
+ recommend(opts = {}) {
1066
+ return this.request("/api/packages/recommend", {
1067
+ query: {
1068
+ ...opts.limit !== void 0 ? { limit: opts.limit } : {},
1069
+ ...opts.verbose !== void 0 ? { verbose: opts.verbose } : {}
1070
+ }
1071
+ });
1072
+ }
939
1073
  star(username, slug) {
940
1074
  return this.request(`/api/packages/${username}/${slug}/star`, {
941
1075
  method: "POST"
@@ -1015,7 +1149,7 @@ var init_src2 = __esm({
1015
1149
 
1016
1150
  // src/index.ts
1017
1151
  init_src2();
1018
- import { Command as Command18 } from "commander";
1152
+ import { Command as Command19 } from "commander";
1019
1153
 
1020
1154
  // src/commands/login.ts
1021
1155
  import { Command } from "commander";
@@ -1023,6 +1157,7 @@ import open from "open";
1023
1157
 
1024
1158
  // src/client.ts
1025
1159
  init_src2();
1160
+ init_src();
1026
1161
 
1027
1162
  // src/config.ts
1028
1163
  import { homedir } from "os";
@@ -1042,7 +1177,8 @@ async function loadCliConfig() {
1042
1177
  return {
1043
1178
  apiUrl: apiUrlFromEnv ?? parsed.apiUrl ?? "https://api.kordis.store",
1044
1179
  ...parsed.username ? { username: parsed.username } : {},
1045
- ...parsed.sessionId ? { sessionId: parsed.sessionId } : {}
1180
+ ...parsed.sessionId ? { sessionId: parsed.sessionId } : {},
1181
+ personalization: parsed.personalization ?? "undecided"
1046
1182
  };
1047
1183
  } catch {
1048
1184
  return { apiUrl: apiUrlFromEnv ?? "https://api.kordis.store" };
@@ -1149,12 +1285,236 @@ async function readAll() {
1149
1285
  }
1150
1286
  }
1151
1287
 
1288
+ // src/context/collector.ts
1289
+ init_src();
1290
+ import { access as access2, readFile as readFile3, readdir, stat } from "fs/promises";
1291
+ import { homedir as homedir3 } from "os";
1292
+ import { join as join3, extname } from "path";
1293
+ import { createHash } from "crypto";
1294
+ async function pathExists2(p) {
1295
+ try {
1296
+ await access2(p);
1297
+ return true;
1298
+ } catch {
1299
+ return false;
1300
+ }
1301
+ }
1302
+ async function detectTools() {
1303
+ const checks = [
1304
+ { tool: "claude-code", paths: [join3(homedir3(), ".claude", "skills")] },
1305
+ { tool: "codex", paths: [join3(homedir3(), ".codex"), join3(homedir3(), ".codex", "skills")] },
1306
+ { tool: "cursor", paths: [join3(homedir3(), ".cursor")] },
1307
+ { tool: "gemini-cli", paths: [join3(homedir3(), ".gemini"), join3(homedir3(), ".gemini", "skills")] }
1308
+ ];
1309
+ const found = [];
1310
+ for (const { tool, paths } of checks) {
1311
+ for (const p of paths) {
1312
+ if (await pathExists2(p)) {
1313
+ found.push(tool);
1314
+ break;
1315
+ }
1316
+ }
1317
+ }
1318
+ return found;
1319
+ }
1320
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
1321
+ ".git",
1322
+ "node_modules",
1323
+ "dist",
1324
+ "build",
1325
+ ".next",
1326
+ ".nuxt",
1327
+ ".svelte-kit",
1328
+ "target",
1329
+ ".venv",
1330
+ "venv",
1331
+ "__pycache__",
1332
+ ".cache",
1333
+ ".turbo"
1334
+ ]);
1335
+ var EXT_TO_TAG = {
1336
+ ".ts": "ts",
1337
+ ".tsx": "ts",
1338
+ ".js": "js",
1339
+ ".jsx": "js",
1340
+ ".mjs": "js",
1341
+ ".cjs": "js",
1342
+ ".py": "py",
1343
+ ".rs": "rs",
1344
+ ".go": "go",
1345
+ ".rb": "rb",
1346
+ ".java": "java",
1347
+ ".kt": "kt",
1348
+ ".swift": "swift",
1349
+ ".c": "c",
1350
+ ".cpp": "cpp",
1351
+ ".cc": "cpp",
1352
+ ".h": "c",
1353
+ ".cs": "cs",
1354
+ ".php": "php",
1355
+ ".lua": "lua",
1356
+ ".r": "r",
1357
+ ".sh": "sh",
1358
+ ".zsh": "sh",
1359
+ ".bash": "sh",
1360
+ ".sql": "sql",
1361
+ ".html": "html",
1362
+ ".css": "css",
1363
+ ".scss": "scss"
1364
+ };
1365
+ async function detectLanguages(cwd = process.cwd()) {
1366
+ const counts = /* @__PURE__ */ new Map();
1367
+ await walk(cwd, 4, counts);
1368
+ return [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([tag]) => tag);
1369
+ }
1370
+ async function walk(dir, depth, counts) {
1371
+ if (depth < 0) return;
1372
+ let entries;
1373
+ try {
1374
+ entries = await readdir(dir, { withFileTypes: true });
1375
+ } catch {
1376
+ return;
1377
+ }
1378
+ for (const e of entries) {
1379
+ if (e.name.startsWith(".") && e.name !== "." && e.name !== "..") {
1380
+ if (SKIP_DIRS.has(e.name)) continue;
1381
+ }
1382
+ if (SKIP_DIRS.has(e.name)) continue;
1383
+ const full = join3(dir, e.name);
1384
+ if (e.isDirectory()) {
1385
+ await walk(full, depth - 1, counts);
1386
+ } else if (e.isFile()) {
1387
+ const tag = EXT_TO_TAG[extname(e.name).toLowerCase()];
1388
+ if (tag) counts.set(tag, (counts.get(tag) ?? 0) + 1);
1389
+ }
1390
+ }
1391
+ }
1392
+ var FRAMEWORK_ALLOWLIST = /* @__PURE__ */ new Set([
1393
+ "next",
1394
+ "react",
1395
+ "vue",
1396
+ "svelte",
1397
+ "angular",
1398
+ "solid",
1399
+ "fastapi",
1400
+ "django",
1401
+ "flask",
1402
+ "rails",
1403
+ "fastify",
1404
+ "hono",
1405
+ "express",
1406
+ "nest",
1407
+ "axum",
1408
+ "actix"
1409
+ ]);
1410
+ async function detectFrameworks(cwd = process.cwd()) {
1411
+ const found = /* @__PURE__ */ new Set();
1412
+ const pkgJsonPath = join3(cwd, "package.json");
1413
+ if (await pathExists2(pkgJsonPath)) {
1414
+ try {
1415
+ const pkg = JSON.parse(await readFile3(pkgJsonPath, "utf8"));
1416
+ const allDeps = Object.keys({
1417
+ ...pkg.dependencies,
1418
+ ...pkg.devDependencies,
1419
+ ...pkg.peerDependencies
1420
+ });
1421
+ for (const dep of allDeps) {
1422
+ const root = dep.split("/").pop()?.toLowerCase() ?? "";
1423
+ if (FRAMEWORK_ALLOWLIST.has(root)) found.add(root);
1424
+ if (root === "core" && dep.startsWith("@nestjs/")) found.add("nest");
1425
+ }
1426
+ } catch {
1427
+ }
1428
+ }
1429
+ const pyprojectPath = join3(cwd, "pyproject.toml");
1430
+ if (await pathExists2(pyprojectPath)) {
1431
+ try {
1432
+ const text = (await readFile3(pyprojectPath, "utf8")).toLowerCase();
1433
+ for (const fw of ["fastapi", "django", "flask"]) {
1434
+ if (text.includes(fw)) found.add(fw);
1435
+ }
1436
+ } catch {
1437
+ }
1438
+ }
1439
+ const cargoPath = join3(cwd, "Cargo.toml");
1440
+ if (await pathExists2(cargoPath)) {
1441
+ try {
1442
+ const text = (await readFile3(cargoPath, "utf8")).toLowerCase();
1443
+ for (const fw of ["axum", "actix"]) {
1444
+ if (text.includes(fw)) found.add(fw);
1445
+ }
1446
+ } catch {
1447
+ }
1448
+ }
1449
+ return [...found].slice(0, 8);
1450
+ }
1451
+ async function installedSlugsHash() {
1452
+ const lockPath = join3(process.env["KORDIS_HOME"] ?? join3(homedir3(), ".kordis"), "installed.json");
1453
+ if (!await pathExists2(lockPath)) return [];
1454
+ let parsed;
1455
+ try {
1456
+ parsed = JSON.parse(await readFile3(lockPath, "utf8"));
1457
+ } catch {
1458
+ return [];
1459
+ }
1460
+ const slugs = Object.keys(parsed.packages ?? {});
1461
+ return slugs.map((s) => createHash("sha256").update(s).digest("hex").slice(0, 8)).slice(0, 50);
1462
+ }
1463
+ async function collectContext(cwd = process.cwd()) {
1464
+ const [tools, languages, frameworks, installed_slugs_hash] = await Promise.all([
1465
+ detectTools(),
1466
+ detectLanguages(cwd),
1467
+ detectFrameworks(cwd),
1468
+ installedSlugsHash()
1469
+ ]);
1470
+ const ctx = {};
1471
+ if (tools.length > 0) ctx.tools = tools;
1472
+ if (languages.length > 0) ctx.languages = languages;
1473
+ if (frameworks.length > 0) ctx.frameworks = frameworks;
1474
+ if (installed_slugs_hash.length > 0) ctx.installed_slugs_hash = installed_slugs_hash;
1475
+ return ctx;
1476
+ }
1477
+ function buildContextHeader(ctx) {
1478
+ 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)) {
1479
+ return null;
1480
+ }
1481
+ const tryFields = (fields) => JSON.stringify(fields);
1482
+ let payload = { ...ctx };
1483
+ const dropOrder = [
1484
+ "frameworks",
1485
+ "languages",
1486
+ "installed_slugs_hash"
1487
+ ];
1488
+ let serialised = tryFields(payload);
1489
+ for (const drop of dropOrder) {
1490
+ if (Buffer.byteLength(serialised, "utf8") <= CONTEXT_HEADER_MAX_BYTES) break;
1491
+ payload = { ...payload };
1492
+ delete payload[drop];
1493
+ serialised = tryFields(payload);
1494
+ }
1495
+ if (Buffer.byteLength(serialised, "utf8") > CONTEXT_HEADER_MAX_BYTES) {
1496
+ return null;
1497
+ }
1498
+ return serialised;
1499
+ }
1500
+ var cachedHeader = null;
1501
+ var CACHE_TTL_MS = 5 * 60 * 1e3;
1502
+ async function getCachedContextHeader(cwd = process.cwd()) {
1503
+ if (cachedHeader && Date.now() - cachedHeader.ts < CACHE_TTL_MS) {
1504
+ return cachedHeader.value;
1505
+ }
1506
+ const ctx = await collectContext(cwd);
1507
+ const header = buildContextHeader(ctx);
1508
+ cachedHeader = { ts: Date.now(), value: header };
1509
+ return header;
1510
+ }
1511
+
1152
1512
  // src/version.ts
1153
1513
  import { readFileSync } from "fs";
1154
1514
  import { fileURLToPath } from "url";
1155
- import { dirname, join as join3 } from "path";
1515
+ import { dirname, join as join4 } from "path";
1156
1516
  var __dir = dirname(fileURLToPath(import.meta.url));
1157
- var pkgPath = join3(__dir, "..", "package.json");
1517
+ var pkgPath = join4(__dir, "..", "package.json");
1158
1518
  var VERSION = (() => {
1159
1519
  try {
1160
1520
  const raw = readFileSync(pkgPath, "utf8");
@@ -1171,10 +1531,17 @@ async function useClient() {
1171
1531
  const keychain = await createKeychain();
1172
1532
  const username = cfg.username ?? null;
1173
1533
  let currentTokens = username ? await keychain.load(username) : null;
1534
+ const personalizationEnabled = cfg.personalization === "on" && process.env["KORDIS_NO_CONTEXT"] !== "1" && !process.argv.includes("--no-context");
1174
1535
  const api = new KordisClient({
1175
1536
  baseUrl: cfg.apiUrl,
1176
1537
  userAgent: `kordis-cli/${VERSION} node/${process.version}`,
1177
- getAccessToken: async () => currentTokens?.access_token
1538
+ getAccessToken: async () => currentTokens?.access_token,
1539
+ ...personalizationEnabled ? {
1540
+ getDynamicHeaders: async () => {
1541
+ const value = await getCachedContextHeader();
1542
+ return value ? { [CONTEXT_HEADER_NAME]: value } : {};
1543
+ }
1544
+ } : {}
1178
1545
  });
1179
1546
  const authedRequest = async (fn) => {
1180
1547
  try {
@@ -1337,6 +1704,70 @@ function whoamiCommand() {
1337
1704
 
1338
1705
  // src/commands/search.ts
1339
1706
  import { Command as Command3 } from "commander";
1707
+
1708
+ // src/lib/recommendation-cache.ts
1709
+ import { readFile as readFile4, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
1710
+ import { homedir as homedir4 } from "os";
1711
+ import { join as join5 } from "path";
1712
+ function cacheDir() {
1713
+ return process.env["KORDIS_HOME"] ?? join5(homedir4(), ".kordis");
1714
+ }
1715
+ function cachePath() {
1716
+ return join5(cacheDir(), "recommendations.json");
1717
+ }
1718
+ async function readCache() {
1719
+ try {
1720
+ const raw = await readFile4(cachePath(), "utf8");
1721
+ const parsed = JSON.parse(raw);
1722
+ if (!parsed.generated_at || !Array.isArray(parsed.items)) return null;
1723
+ return parsed;
1724
+ } catch {
1725
+ return null;
1726
+ }
1727
+ }
1728
+ async function writeCache(items, generatedAt) {
1729
+ await mkdir3(cacheDir(), { recursive: true });
1730
+ const payload = {
1731
+ generated_at: generatedAt,
1732
+ items,
1733
+ seen_at: null
1734
+ };
1735
+ await writeFile3(cachePath(), JSON.stringify(payload, null, 2) + "\n", "utf8");
1736
+ }
1737
+ async function markSeen() {
1738
+ const existing = await readCache();
1739
+ if (!existing) return;
1740
+ await mkdir3(cacheDir(), { recursive: true });
1741
+ const updated = {
1742
+ ...existing,
1743
+ seen_at: (/* @__PURE__ */ new Date()).toISOString()
1744
+ };
1745
+ await writeFile3(cachePath(), JSON.stringify(updated, null, 2) + "\n", "utf8");
1746
+ }
1747
+
1748
+ // src/lib/recommendation-footer.ts
1749
+ var FRESHNESS_WINDOW_MS = 7 * 24 * 60 * 60 * 1e3;
1750
+ async function maybeRenderReminder() {
1751
+ try {
1752
+ const cfg = await loadCliConfig();
1753
+ if (cfg.personalization === "off") return null;
1754
+ const cache = await readCache();
1755
+ if (!cache || cache.items.length === 0) return null;
1756
+ const generated = new Date(cache.generated_at).getTime();
1757
+ if (!Number.isFinite(generated)) return null;
1758
+ if (Date.now() - generated > FRESHNESS_WINDOW_MS) return null;
1759
+ if (cache.seen_at) {
1760
+ const seen = new Date(cache.seen_at).getTime();
1761
+ if (Number.isFinite(seen) && seen >= generated) return null;
1762
+ }
1763
+ const n = cache.items.length;
1764
+ return `\u2192 ${n} new recommendation${n === 1 ? "" : "s"} match your stack: kordis recommend`;
1765
+ } catch {
1766
+ return null;
1767
+ }
1768
+ }
1769
+
1770
+ // src/commands/search.ts
1340
1771
  var SORT_MODES = [
1341
1772
  "relevance",
1342
1773
  "stars",
@@ -1438,8 +1869,8 @@ function searchCommand() {
1438
1869
 
1439
1870
  // src/commands/push.ts
1440
1871
  import { Command as Command4 } from "commander";
1441
- import { readFile as readFile3, stat, readdir } from "fs/promises";
1442
- import { resolve, join as join4, posix, sep } from "path";
1872
+ import { readFile as readFile5, stat as stat2, readdir as readdir2 } from "fs/promises";
1873
+ import { resolve, join as join6, posix, sep } from "path";
1443
1874
  import JSZip from "jszip";
1444
1875
 
1445
1876
  // ../../packages/harness-schema/src/parser.ts
@@ -1492,9 +1923,9 @@ var FrontmatterParseError = class extends Error {
1492
1923
 
1493
1924
  // ../../packages/harness-schema/src/deps.ts
1494
1925
  init_src();
1495
- import { z as z10 } from "zod";
1926
+ import { z as z12 } from "zod";
1496
1927
  var StringDepRe = /^([a-z0-9](-?[a-z0-9])*\/[a-z0-9](-?[a-z0-9])*)(?:@(.+))?$/;
1497
- var ObjectDepSchema = z10.object({
1928
+ var ObjectDepSchema = z12.object({
1498
1929
  full_slug: FullSlugSchema,
1499
1930
  version_range: SemverRangeSchema.default("*")
1500
1931
  });
@@ -1530,50 +1961,50 @@ init_src();
1530
1961
 
1531
1962
  // ../../packages/harness-schema/src/types/skill.ts
1532
1963
  init_src();
1533
- import { z as z11 } from "zod";
1534
- var SkillFrontmatterSchema = z11.object({
1535
- name: z11.string().min(1).max(64).regex(/^[a-z0-9]+(-[a-z0-9]+)*$/, "name must be lowercase-with-hyphens (Agent Skills spec)"),
1536
- description: z11.string().min(1).max(2048),
1964
+ import { z as z13 } from "zod";
1965
+ var SkillFrontmatterSchema = z13.object({
1966
+ name: z13.string().min(1).max(64).regex(/^[a-z0-9]+(-[a-z0-9]+)*$/, "name must be lowercase-with-hyphens (Agent Skills spec)"),
1967
+ description: z13.string().min(1).max(2048),
1537
1968
  version: SemverSchema.optional(),
1538
1969
  // Agent Skills optional metadata
1539
- license: z11.string().max(64).optional(),
1540
- homepage: z11.string().url().optional(),
1541
- model: z11.string().optional(),
1970
+ license: z13.string().max(64).optional(),
1971
+ homepage: z13.string().url().optional(),
1972
+ model: z13.string().optional(),
1542
1973
  // model override hint
1543
- "allowed-tools": z11.array(z11.string()).optional(),
1544
- "disable-model-invocation": z11.boolean().optional(),
1974
+ "allowed-tools": z13.array(z13.string()).optional(),
1975
+ "disable-model-invocation": z13.boolean().optional(),
1545
1976
  // Kordis-forwarded cross-type fields (kept minimal to not pollute the spec)
1546
- title: z11.string().min(1).max(200).optional(),
1547
- tags: z11.array(TagSchema).max(16).optional(),
1548
- tools: z11.array(ToolSlugSchema).optional()
1977
+ title: z13.string().min(1).max(200).optional(),
1978
+ tags: z13.array(TagSchema).max(16).optional(),
1979
+ tools: z13.array(ToolSlugSchema).optional()
1549
1980
  }).strict();
1550
1981
 
1551
1982
  // ../../packages/harness-schema/src/types/mcp-server.ts
1552
1983
  init_src();
1553
- import { z as z12 } from "zod";
1554
- var McpTransportSchema = z12.union([
1555
- z12.object({
1556
- type: z12.literal("stdio"),
1557
- command: z12.string().min(1),
1558
- args: z12.array(z12.string()).optional().default([]),
1559
- env: z12.record(z12.string()).optional().default({})
1984
+ import { z as z14 } from "zod";
1985
+ var McpTransportSchema = z14.union([
1986
+ z14.object({
1987
+ type: z14.literal("stdio"),
1988
+ command: z14.string().min(1),
1989
+ args: z14.array(z14.string()).optional().default([]),
1990
+ env: z14.record(z14.string()).optional().default({})
1560
1991
  }),
1561
- z12.object({
1562
- type: z12.enum(["http", "sse"]),
1563
- url: z12.string().url(),
1564
- headers: z12.record(z12.string()).optional().default({})
1992
+ z14.object({
1993
+ type: z14.enum(["http", "sse"]),
1994
+ url: z14.string().url(),
1995
+ headers: z14.record(z14.string()).optional().default({})
1565
1996
  })
1566
1997
  ]);
1567
- var McpCapabilitiesSchema = z12.object({
1568
- tools: z12.array(z12.string()).optional().default([]),
1569
- resources: z12.array(z12.string()).optional().default([]),
1570
- prompts: z12.array(z12.string()).optional().default([])
1998
+ var McpCapabilitiesSchema = z14.object({
1999
+ tools: z14.array(z14.string()).optional().default([]),
2000
+ resources: z14.array(z14.string()).optional().default([]),
2001
+ prompts: z14.array(z14.string()).optional().default([])
1571
2002
  });
1572
- var McpServerFrontmatterSchema = z12.object({
1573
- name: z12.string().min(1).max(64).regex(/^[a-z0-9]+(-[a-z0-9]+)*$/),
1574
- description: z12.string().min(1).max(2048),
2003
+ var McpServerFrontmatterSchema = z14.object({
2004
+ name: z14.string().min(1).max(64).regex(/^[a-z0-9]+(-[a-z0-9]+)*$/),
2005
+ description: z14.string().min(1).max(2048),
1575
2006
  version: SemverSchema,
1576
- mcp_spec_version: z12.string().default("2026-03"),
2007
+ mcp_spec_version: z14.string().default("2026-03"),
1577
2008
  transport: McpTransportSchema,
1578
2009
  capabilities: McpCapabilitiesSchema.optional().default({
1579
2010
  tools: [],
@@ -1581,35 +2012,35 @@ var McpServerFrontmatterSchema = z12.object({
1581
2012
  prompts: []
1582
2013
  }),
1583
2014
  /** Human-readable setup / auth instructions. Surfaced in `kordis info`. */
1584
- auth_notes: z12.string().optional(),
2015
+ auth_notes: z14.string().optional(),
1585
2016
  /** Required env vars with description + whether they must be secret. */
1586
- env_schema: z12.record(
1587
- z12.object({
1588
- description: z12.string(),
1589
- required: z12.boolean().default(true),
1590
- secret: z12.boolean().default(false)
2017
+ env_schema: z14.record(
2018
+ z14.object({
2019
+ description: z14.string(),
2020
+ required: z14.boolean().default(true),
2021
+ secret: z14.boolean().default(false)
1591
2022
  })
1592
2023
  ).optional(),
1593
- homepage: z12.string().url().optional(),
1594
- repository: z12.string().url().optional(),
1595
- license: z12.string().max(64).optional(),
1596
- title: z12.string().min(1).max(200).optional()
2024
+ homepage: z14.string().url().optional(),
2025
+ repository: z14.string().url().optional(),
2026
+ license: z14.string().max(64).optional(),
2027
+ title: z14.string().min(1).max(200).optional()
1597
2028
  }).strict();
1598
2029
 
1599
2030
  // ../../packages/harness-schema/src/types/project-rules.ts
1600
2031
  init_src();
1601
- import { z as z13 } from "zod";
1602
- var ProjectRulesFrontmatterSchema = z13.object({
1603
- title: z13.string().min(1).max(200),
1604
- description: z13.string().min(1).max(2048),
2032
+ import { z as z15 } from "zod";
2033
+ var ProjectRulesFrontmatterSchema = z15.object({
2034
+ title: z15.string().min(1).max(200),
2035
+ description: z15.string().min(1).max(2048),
1605
2036
  version: SemverSchema.optional(),
1606
- scope: z13.enum(["root", "nested", "globs"]).default("root"),
1607
- target_path: z13.string().optional(),
1608
- globs: z13.array(z13.string()).optional(),
1609
- target_tools: z13.array(ToolSlugSchema).optional().default([]),
2037
+ scope: z15.enum(["root", "nested", "globs"]).default("root"),
2038
+ target_path: z15.string().optional(),
2039
+ globs: z15.array(z15.string()).optional(),
2040
+ target_tools: z15.array(ToolSlugSchema).optional().default([]),
1610
2041
  /** When true, installation is idempotent via `<!-- kordis:... -->` markers. */
1611
- mergeable: z13.boolean().default(true),
1612
- license: z13.string().max(64).optional()
2042
+ mergeable: z15.boolean().default(true),
2043
+ license: z15.string().max(64).optional()
1613
2044
  }).strict().refine(
1614
2045
  (v) => v.scope !== "nested" || !!v.target_path,
1615
2046
  { message: 'scope="nested" requires target_path' }
@@ -1620,33 +2051,33 @@ var ProjectRulesFrontmatterSchema = z13.object({
1620
2051
 
1621
2052
  // ../../packages/harness-schema/src/types/custom-agent.ts
1622
2053
  init_src();
1623
- import { z as z14 } from "zod";
1624
- var CustomAgentFrontmatterSchema = z14.object({
1625
- name: z14.string().min(1).max(64).regex(/^[a-z0-9]+(-[a-z0-9]+)*$/),
1626
- description: z14.string().min(1).max(2048),
1627
- title: z14.string().min(1).max(200).optional(),
2054
+ import { z as z16 } from "zod";
2055
+ var CustomAgentFrontmatterSchema = z16.object({
2056
+ name: z16.string().min(1).max(64).regex(/^[a-z0-9]+(-[a-z0-9]+)*$/),
2057
+ description: z16.string().min(1).max(2048),
2058
+ title: z16.string().min(1).max(200).optional(),
1628
2059
  version: SemverSchema.optional(),
1629
2060
  /** System prompt that defines the agent's persona + rules. Required. */
1630
- system_prompt: z14.string().min(1).max(5e4),
2061
+ system_prompt: z16.string().min(1).max(5e4),
1631
2062
  /** Optional model override — string passed verbatim to the host. */
1632
- model: z14.string().optional(),
2063
+ model: z16.string().optional(),
1633
2064
  /** Tools this agent can call. Host-specific names (no uniform namespace). */
1634
- allowed_tools: z14.array(z14.string()).optional().default([]),
1635
- disallowed_tools: z14.array(z14.string()).optional().default([]),
2065
+ allowed_tools: z16.array(z16.string()).optional().default([]),
2066
+ disallowed_tools: z16.array(z16.string()).optional().default([]),
1636
2067
  /** Fork/context behaviour hint (Claude: `context: fork` etc.). */
1637
- context: z14.enum(["inherit", "fork", "isolated"]).optional(),
2068
+ context: z16.enum(["inherit", "fork", "isolated"]).optional(),
1638
2069
  /** Known-good tool runtimes. Empty = untested everywhere. */
1639
- tool_compat: z14.array(ToolSlugSchema).optional().default([]),
2070
+ tool_compat: z16.array(ToolSlugSchema).optional().default([]),
1640
2071
  /** Pre-load skills when this agent activates. */
1641
- preload_skills: z14.array(z14.string()).optional().default([]),
1642
- license: z14.string().max(64).optional(),
1643
- homepage: z14.string().url().optional()
2072
+ preload_skills: z16.array(z16.string()).optional().default([]),
2073
+ license: z16.string().max(64).optional(),
2074
+ homepage: z16.string().url().optional()
1644
2075
  }).strict();
1645
2076
 
1646
2077
  // ../../packages/harness-schema/src/types/hook.ts
1647
2078
  init_src();
1648
- import { z as z15 } from "zod";
1649
- var HookEventSchema = z15.enum([
2079
+ import { z as z17 } from "zod";
2080
+ var HookEventSchema = z17.enum([
1650
2081
  "PreToolUse",
1651
2082
  "PostToolUse",
1652
2083
  "UserPromptSubmit",
@@ -1658,37 +2089,37 @@ var HookEventSchema = z15.enum([
1658
2089
  "SubagentStop",
1659
2090
  "Background"
1660
2091
  ]);
1661
- var HookActionSchema = z15.union([
1662
- z15.object({
1663
- type: z15.literal("command"),
1664
- command: z15.string().min(1),
1665
- timeout_ms: z15.number().int().positive().max(6e5).optional(),
1666
- blocking: z15.boolean().default(false)
2092
+ var HookActionSchema = z17.union([
2093
+ z17.object({
2094
+ type: z17.literal("command"),
2095
+ command: z17.string().min(1),
2096
+ timeout_ms: z17.number().int().positive().max(6e5).optional(),
2097
+ blocking: z17.boolean().default(false)
1667
2098
  }),
1668
- z15.object({
1669
- type: z15.literal("http"),
1670
- url: z15.string().url(),
1671
- method: z15.enum(["GET", "POST", "PUT", "PATCH"]).default("POST"),
1672
- headers: z15.record(z15.string()).optional().default({}),
1673
- timeout_ms: z15.number().int().positive().max(6e5).optional(),
1674
- blocking: z15.boolean().default(false)
2099
+ z17.object({
2100
+ type: z17.literal("http"),
2101
+ url: z17.string().url(),
2102
+ method: z17.enum(["GET", "POST", "PUT", "PATCH"]).default("POST"),
2103
+ headers: z17.record(z17.string()).optional().default({}),
2104
+ timeout_ms: z17.number().int().positive().max(6e5).optional(),
2105
+ blocking: z17.boolean().default(false)
1675
2106
  })
1676
2107
  ]);
1677
- var HookBindingSchema = z15.object({
2108
+ var HookBindingSchema = z17.object({
1678
2109
  event: HookEventSchema,
1679
- matcher: z15.string().optional(),
2110
+ matcher: z17.string().optional(),
1680
2111
  action: HookActionSchema
1681
2112
  });
1682
- var HookFrontmatterSchema = z15.object({
1683
- name: z15.string().min(1).max(64).regex(/^[a-z0-9]+(-[a-z0-9]+)*$/),
1684
- description: z15.string().min(1).max(2048),
1685
- title: z15.string().min(1).max(200).optional(),
2113
+ var HookFrontmatterSchema = z17.object({
2114
+ name: z17.string().min(1).max(64).regex(/^[a-z0-9]+(-[a-z0-9]+)*$/),
2115
+ description: z17.string().min(1).max(2048),
2116
+ title: z17.string().min(1).max(200).optional(),
1686
2117
  version: SemverSchema.optional(),
1687
2118
  /** One or more hook bindings. */
1688
- hooks: z15.array(HookBindingSchema).min(1),
2119
+ hooks: z17.array(HookBindingSchema).min(1),
1689
2120
  /** Declared to surface in search filters — always "claude-code" for now. */
1690
- tool_compat: z15.array(z15.enum(["claude-code"])).default(["claude-code"]),
1691
- license: z15.string().max(64).optional()
2121
+ tool_compat: z17.array(z17.enum(["claude-code"])).default(["claude-code"]),
2122
+ license: z17.string().max(64).optional()
1692
2123
  }).strict();
1693
2124
 
1694
2125
  // ../../packages/harness-schema/src/validator.ts
@@ -2007,7 +2438,7 @@ function pushCommand() {
2007
2438
  async (path, opts) => {
2008
2439
  const bundle = await useClient();
2009
2440
  const abs = resolve(process.cwd(), path);
2010
- const s = await stat(abs);
2441
+ const s = await stat2(abs);
2011
2442
  let content;
2012
2443
  let assets = [];
2013
2444
  if (s.isDirectory()) {
@@ -2017,7 +2448,7 @@ function pushCommand() {
2017
2448
  ({ content, assets } = await bundleSkillZip(abs));
2018
2449
  content = ensureTypeField(content, "skill");
2019
2450
  } else {
2020
- content = await readFile3(abs, "utf8");
2451
+ content = await readFile5(abs, "utf8");
2021
2452
  }
2022
2453
  let nameFromFm = null;
2023
2454
  try {
@@ -2198,19 +2629,19 @@ function shouldExclude(relPath, isDir) {
2198
2629
  return false;
2199
2630
  }
2200
2631
  async function walkDir(base, relDir, collected) {
2201
- const entries = await readdir(join4(base, relDir), { withFileTypes: true });
2632
+ const entries = await readdir2(join6(base, relDir), { withFileTypes: true });
2202
2633
  for (const entry of entries) {
2203
2634
  const rel = relDir ? `${relDir}/${entry.name}` : entry.name;
2204
2635
  if (shouldExclude(rel, entry.isDirectory())) continue;
2205
2636
  if (entry.isDirectory()) {
2206
2637
  await walkDir(base, rel, collected);
2207
2638
  } else if (entry.isFile()) {
2208
- collected.push({ rel, abs: join4(base, rel) });
2639
+ collected.push({ rel, abs: join6(base, rel) });
2209
2640
  }
2210
2641
  }
2211
2642
  }
2212
2643
  async function readAsset(absPath) {
2213
- const buf = await readFile3(absPath);
2644
+ const buf = await readFile5(absPath);
2214
2645
  const text = buf.toString("utf8");
2215
2646
  const roundtrip = Buffer.from(text, "utf8");
2216
2647
  if (roundtrip.equals(buf)) {
@@ -2227,7 +2658,7 @@ async function bundleDirectory(dir) {
2227
2658
  if (!skillEntry) {
2228
2659
  throw new Error(`No SKILL.md found in directory ${dir}. Push a directory that has SKILL.md at its root.`);
2229
2660
  }
2230
- const content = await readFile3(skillEntry.abs, "utf8");
2661
+ const content = await readFile5(skillEntry.abs, "utf8");
2231
2662
  const assets = [];
2232
2663
  for (const f of all) {
2233
2664
  if (f.rel === "SKILL.md") continue;
@@ -2237,7 +2668,7 @@ async function bundleDirectory(dir) {
2237
2668
  return { content, assets };
2238
2669
  }
2239
2670
  async function bundleSkillZip(zipPath) {
2240
- const buf = await readFile3(zipPath);
2671
+ const buf = await readFile5(zipPath);
2241
2672
  const zip = await JSZip.loadAsync(buf);
2242
2673
  const entries = Object.keys(zip.files).filter((k) => !zip.files[k].dir);
2243
2674
  if (entries.length === 0) throw new Error(".skill file is empty");
@@ -2271,8 +2702,8 @@ async function bundleSkillZip(zipPath) {
2271
2702
 
2272
2703
  // src/commands/pull.ts
2273
2704
  import { Command as Command5 } from "commander";
2274
- import { mkdir as mkdir4, writeFile as writeFile4 } from "fs/promises";
2275
- import { dirname as dirname2, join as join6 } from "path";
2705
+ import { mkdir as mkdir5, writeFile as writeFile5 } from "fs/promises";
2706
+ import { dirname as dirname2, join as join8 } from "path";
2276
2707
 
2277
2708
  // ../../packages/translators/src/skill/identity.ts
2278
2709
  function buildSkillFile(input, outputPath) {
@@ -2714,21 +3145,21 @@ function getTranslator(type, format) {
2714
3145
  }
2715
3146
 
2716
3147
  // src/lockfile.ts
2717
- import { homedir as homedir3 } from "os";
2718
- import { join as join5 } from "path";
2719
- import { readFile as readFile4, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
2720
- var LOCKFILE_PATH = join5(homedir3(), ".kordis", "installed.json");
3148
+ import { homedir as homedir5 } from "os";
3149
+ import { join as join7 } from "path";
3150
+ import { readFile as readFile6, writeFile as writeFile4, mkdir as mkdir4 } from "fs/promises";
3151
+ var LOCKFILE_PATH = join7(homedir5(), ".kordis", "installed.json");
2721
3152
  async function readLockfile() {
2722
3153
  try {
2723
- const raw = await readFile4(LOCKFILE_PATH, "utf8");
3154
+ const raw = await readFile6(LOCKFILE_PATH, "utf8");
2724
3155
  return JSON.parse(raw);
2725
3156
  } catch {
2726
3157
  return { packages: {} };
2727
3158
  }
2728
3159
  }
2729
3160
  async function writeLockfile(lf) {
2730
- await mkdir3(join5(homedir3(), ".kordis"), { recursive: true });
2731
- await writeFile3(LOCKFILE_PATH, JSON.stringify(lf, null, 2) + "\n", "utf8");
3161
+ await mkdir4(join7(homedir5(), ".kordis"), { recursive: true });
3162
+ await writeFile4(LOCKFILE_PATH, JSON.stringify(lf, null, 2) + "\n", "utf8");
2732
3163
  }
2733
3164
  async function recordInstalled(fullSlug, entry) {
2734
3165
  const lf = await readLockfile();
@@ -2764,8 +3195,8 @@ async function doPullInstall(fullSlug, tool, installRoot, version) {
2764
3195
  assets: contentRes.assets
2765
3196
  });
2766
3197
  for (const f of result.files) {
2767
- const abs = join6(installRoot, f.path);
2768
- await mkdir4(dirname2(abs), { recursive: true });
3198
+ const abs = join8(installRoot, f.path);
3199
+ await mkdir5(dirname2(abs), { recursive: true });
2769
3200
  await writeEmittedFile(abs, f.content, f.encoding);
2770
3201
  written.push(abs);
2771
3202
  }
@@ -2784,8 +3215,8 @@ async function doPullInstall(fullSlug, tool, installRoot, version) {
2784
3215
  `Tool "${format}" is not supported for type "${parsed.type}"; installed canonical form instead.`
2785
3216
  );
2786
3217
  for (const f of result.files) {
2787
- const abs = join6(installRoot, f.path);
2788
- await mkdir4(dirname2(abs), { recursive: true });
3218
+ const abs = join8(installRoot, f.path);
3219
+ await mkdir5(dirname2(abs), { recursive: true });
2789
3220
  await writeEmittedFile(abs, f.content, f.encoding);
2790
3221
  written.push(abs);
2791
3222
  }
@@ -2842,11 +3273,13 @@ function pullCommand() {
2842
3273
  for (const n of result.notes) printHuman(c.yellow(` ! ${n}`));
2843
3274
  }
2844
3275
  });
3276
+ const reminder = await maybeRenderReminder();
3277
+ if (reminder) printHuman(c.gray(reminder));
2845
3278
  }
2846
3279
  );
2847
3280
  }
2848
3281
  async function detectTool(root) {
2849
- const { stat: stat2 } = await import("fs/promises");
3282
+ const { stat: stat3 } = await import("fs/promises");
2850
3283
  const checks = [
2851
3284
  [".claude", "claude-code"],
2852
3285
  [".cursor", "cursor"],
@@ -2862,7 +3295,7 @@ async function detectTool(root) {
2862
3295
  ];
2863
3296
  for (const [path, tool] of checks) {
2864
3297
  try {
2865
- await stat2(join6(root, path));
3298
+ await stat3(join8(root, path));
2866
3299
  return tool;
2867
3300
  } catch {
2868
3301
  }
@@ -2871,9 +3304,9 @@ async function detectTool(root) {
2871
3304
  }
2872
3305
  async function writeEmittedFile(abs, content, encoding) {
2873
3306
  if (encoding === "base64") {
2874
- await writeFile4(abs, Buffer.from(content, "base64"));
3307
+ await writeFile5(abs, Buffer.from(content, "base64"));
2875
3308
  } else {
2876
- await writeFile4(abs, content, "utf8");
3309
+ await writeFile5(abs, content, "utf8");
2877
3310
  }
2878
3311
  }
2879
3312
 
@@ -2994,11 +3427,12 @@ function logoutCommand() {
2994
3427
  }
2995
3428
 
2996
3429
  // src/commands/setup.ts
2997
- import { readFile as readFile5, writeFile as writeFile5, mkdir as mkdir5, access as access2 } from "fs/promises";
2998
- import { homedir as homedir4 } from "os";
2999
- import { join as join7, dirname as dirname3 } from "path";
3430
+ import { readFile as readFile7, writeFile as writeFile6, mkdir as mkdir6, access as access3 } from "fs/promises";
3431
+ import { homedir as homedir6 } from "os";
3432
+ import { join as join9, dirname as dirname3 } from "path";
3000
3433
  import { fileURLToPath as fileURLToPath2 } from "url";
3001
3434
  import { Command as Command9 } from "commander";
3435
+ import prompts from "prompts";
3002
3436
  var MARKER_START = "<!-- kordis-start -->";
3003
3437
  var MARKER_END = "<!-- kordis-end -->";
3004
3438
  var HUB_TARGETS = [
@@ -3010,7 +3444,8 @@ var SUB_SKILLS = [
3010
3444
  "kordis-search",
3011
3445
  "kordis-using",
3012
3446
  "kordis-publishing",
3013
- "kordis-social"
3447
+ "kordis-social",
3448
+ "kordis-recommend"
3014
3449
  ];
3015
3450
  var SKILL_TARGETS = [
3016
3451
  { tool: "Claude Code", dir: ".claude", skillsDir: ".claude/skills" },
@@ -3019,7 +3454,7 @@ var SKILL_TARGETS = [
3019
3454
  ];
3020
3455
  async function exists(path) {
3021
3456
  try {
3022
- await access2(path);
3457
+ await access3(path);
3023
3458
  return true;
3024
3459
  } catch {
3025
3460
  return false;
@@ -3029,18 +3464,18 @@ function assetRoots() {
3029
3464
  const __dir2 = dirname3(fileURLToPath2(import.meta.url));
3030
3465
  return {
3031
3466
  // Built binary: bin/kordis.js → assets/ is one level up
3032
- bundled: join7(__dir2, "..", "assets"),
3467
+ bundled: join9(__dir2, "..", "assets"),
3033
3468
  // Dev mode: src/commands/setup.ts → 4 levels up to repo root, then packages/skills/
3034
- source: join7(__dir2, "..", "..", "..", "..", "packages", "skills")
3469
+ source: join9(__dir2, "..", "..", "..", "..", "packages", "skills")
3035
3470
  };
3036
3471
  }
3037
3472
  async function pickAssetRoot() {
3038
3473
  const { bundled, source } = assetRoots();
3039
- return await exists(join7(bundled, "KORDIS.md")) ? bundled : source;
3474
+ return await exists(join9(bundled, "KORDIS.md")) ? bundled : source;
3040
3475
  }
3041
3476
  async function loadHubContent() {
3042
3477
  const root = await pickAssetRoot();
3043
- const raw = await readFile5(join7(root, "KORDIS.md"), "utf-8");
3478
+ const raw = await readFile7(join9(root, "KORDIS.md"), "utf-8");
3044
3479
  if (raw.startsWith("---")) {
3045
3480
  const end = raw.indexOf("\n---", 4);
3046
3481
  if (end !== -1) return raw.slice(end + 4).trimStart();
@@ -3049,7 +3484,7 @@ async function loadHubContent() {
3049
3484
  }
3050
3485
  async function loadSubSkillContent(name) {
3051
3486
  const root = await pickAssetRoot();
3052
- return readFile5(join7(root, name, "SKILL.md"), "utf-8");
3487
+ return readFile7(join9(root, name, "SKILL.md"), "utf-8");
3053
3488
  }
3054
3489
  async function injectBlock(filePath, content, dryRun) {
3055
3490
  const block = `${MARKER_START}
@@ -3058,14 +3493,14 @@ ${MARKER_END}`;
3058
3493
  let existing = "";
3059
3494
  let fileExists = true;
3060
3495
  try {
3061
- existing = await readFile5(filePath, "utf-8");
3496
+ existing = await readFile7(filePath, "utf-8");
3062
3497
  } catch {
3063
3498
  fileExists = false;
3064
3499
  }
3065
3500
  if (!fileExists) {
3066
3501
  if (!dryRun) {
3067
- await mkdir5(dirname3(filePath), { recursive: true });
3068
- await writeFile5(filePath, block + "\n", "utf-8");
3502
+ await mkdir6(dirname3(filePath), { recursive: true });
3503
+ await writeFile6(filePath, block + "\n", "utf-8");
3069
3504
  }
3070
3505
  return "injected";
3071
3506
  }
@@ -3076,31 +3511,31 @@ ${MARKER_END}`;
3076
3511
  if (current === block) return "unchanged";
3077
3512
  if (!dryRun) {
3078
3513
  const updated = existing.slice(0, startIdx) + block + existing.slice(endIdx + MARKER_END.length);
3079
- await writeFile5(filePath, updated, "utf-8");
3514
+ await writeFile6(filePath, updated, "utf-8");
3080
3515
  }
3081
3516
  return "updated";
3082
3517
  }
3083
3518
  if (!dryRun) {
3084
- await writeFile5(filePath, existing.trimEnd() + "\n\n" + block + "\n", "utf-8");
3519
+ await writeFile6(filePath, existing.trimEnd() + "\n\n" + block + "\n", "utf-8");
3085
3520
  }
3086
3521
  return "injected";
3087
3522
  }
3088
3523
  async function copyBundledSkill(filePath, content, dryRun) {
3089
3524
  let existing = null;
3090
3525
  try {
3091
- existing = await readFile5(filePath, "utf-8");
3526
+ existing = await readFile7(filePath, "utf-8");
3092
3527
  } catch {
3093
3528
  }
3094
3529
  if (existing === content) return "unchanged";
3095
3530
  if (!dryRun) {
3096
- await mkdir5(dirname3(filePath), { recursive: true });
3097
- await writeFile5(filePath, content, "utf-8");
3531
+ await mkdir6(dirname3(filePath), { recursive: true });
3532
+ await writeFile6(filePath, content, "utf-8");
3098
3533
  }
3099
3534
  return existing === null ? "injected" : "updated";
3100
3535
  }
3101
3536
  function setupCommand() {
3102
3537
  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) => {
3103
- const home = homedir4();
3538
+ const home = homedir6();
3104
3539
  const dryRun = opts.dryRun ?? false;
3105
3540
  let hubContent;
3106
3541
  let subSkillContents;
@@ -3117,8 +3552,8 @@ function setupCommand() {
3117
3552
  let anyFound = false;
3118
3553
  const changedTools = /* @__PURE__ */ new Set();
3119
3554
  for (const target of HUB_TARGETS) {
3120
- const dirPath = join7(home, target.dir);
3121
- const filePath = join7(home, target.file);
3555
+ const dirPath = join9(home, target.dir);
3556
+ const filePath = join9(home, target.file);
3122
3557
  if (!await exists(dirPath)) {
3123
3558
  if (!opts.silent) {
3124
3559
  printHuman(c.gray(` - ${target.tool.padEnd(12)} not installed, skipped`));
@@ -3145,10 +3580,10 @@ function setupCommand() {
3145
3580
  }
3146
3581
  }
3147
3582
  for (const target of SKILL_TARGETS) {
3148
- const dirPath = join7(home, target.dir);
3583
+ const dirPath = join9(home, target.dir);
3149
3584
  if (!await exists(dirPath)) continue;
3150
3585
  for (const subName of SUB_SKILLS) {
3151
- const filePath = join7(home, target.skillsDir, subName, "SKILL.md");
3586
+ const filePath = join9(home, target.skillsDir, subName, "SKILL.md");
3152
3587
  const dest = `~/${target.skillsDir}/${subName}/SKILL.md`;
3153
3588
  try {
3154
3589
  const result = await copyBundledSkill(filePath, subSkillContents[subName], dryRun);
@@ -3180,6 +3615,34 @@ function setupCommand() {
3180
3615
  printHuman(c.gray("No supported agent tools detected."));
3181
3616
  printHuman(c.gray("Run `kordis setup` again after installing Claude Code, Codex, or Gemini CLI."));
3182
3617
  }
3618
+ if (!opts.silent && !dryRun && process.stdin.isTTY && anyFound) {
3619
+ const cfg = await loadCliConfig();
3620
+ if (cfg.personalization === "undecided") {
3621
+ printHuman("");
3622
+ const { enable } = await prompts({
3623
+ type: "confirm",
3624
+ name: "enable",
3625
+ message: "Personalize recommendations using anonymous local signals (installed agent tools, language stats, frameworks)? Server never stores them \u2014 only re-ranks results in-flight.",
3626
+ initial: true
3627
+ });
3628
+ const newCfg = {
3629
+ ...cfg,
3630
+ personalization: enable ? "on" : "off"
3631
+ };
3632
+ await saveCliConfig(newCfg);
3633
+ if (enable) {
3634
+ printHuman(
3635
+ c.green("\u2713 personalization on") + c.gray(" \u2014 disable any time: `KORDIS_NO_CONTEXT=1` env or `--no-context` flag")
3636
+ );
3637
+ } else {
3638
+ printHuman(
3639
+ c.gray(
3640
+ "\u2192 recommendations will use popularity only. Re-enable later via `kordis recommend` if asked again."
3641
+ )
3642
+ );
3643
+ }
3644
+ }
3645
+ }
3183
3646
  });
3184
3647
  }
3185
3648
 
@@ -3280,7 +3743,7 @@ function unstarCommand() {
3280
3743
 
3281
3744
  // src/commands/delete.ts
3282
3745
  import { Command as Command12 } from "commander";
3283
- import prompts from "prompts";
3746
+ import prompts2 from "prompts";
3284
3747
  function deleteCommand() {
3285
3748
  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) => {
3286
3749
  const [username, slug] = fullSlug.split("/");
@@ -3295,7 +3758,7 @@ function deleteCommand() {
3295
3758
  printHuman(` "${detail.description?.slice(0, 80) ?? ""}"`);
3296
3759
  printHuman(c.yellow(` The slug cannot be reused after deletion. This cannot be undone via the CLI.`));
3297
3760
  printHuman("");
3298
- const { confirmed } = await prompts({
3761
+ const { confirmed } = await prompts2({
3299
3762
  type: "confirm",
3300
3763
  name: "confirmed",
3301
3764
  message: `Delete ${c.bold(fullSlug)}?`,
@@ -3709,9 +4172,107 @@ function diffCommand() {
3709
4172
  });
3710
4173
  }
3711
4174
 
4175
+ // src/commands/recommend.ts
4176
+ import { Command as Command18 } from "commander";
4177
+ import prompts3 from "prompts";
4178
+ function recommendCommand() {
4179
+ 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) => {
4180
+ const cfg = await loadCliConfig();
4181
+ if (cfg.personalization === "undecided" && process.stdin.isTTY && !isJsonMode() && opts.context !== false) {
4182
+ const { enable } = await prompts3({
4183
+ type: "confirm",
4184
+ name: "enable",
4185
+ message: "Personalize recommendations using anonymous local signals (installed agent tools, language stats, frameworks)? Server never stores them \u2014 only re-ranks results in-flight.",
4186
+ initial: true
4187
+ });
4188
+ const newCfg = {
4189
+ ...cfg,
4190
+ personalization: enable ? "on" : "off"
4191
+ };
4192
+ await saveCliConfig(newCfg);
4193
+ if (!enable) {
4194
+ printHuman(
4195
+ c.gray(
4196
+ "\u2192 recommendations will use popularity only. Re-enable later: `kordis setup` (re-asks the opt-in)."
4197
+ )
4198
+ );
4199
+ }
4200
+ }
4201
+ const bundle = await useClient();
4202
+ const spin = spinner("Computing recommendations");
4203
+ const res = await bundle.api.recommend({
4204
+ limit: opts.limit,
4205
+ verbose: opts.verbose
4206
+ });
4207
+ spin.succeed();
4208
+ await writeCache(res.items, res.generated_at);
4209
+ await markSeen();
4210
+ printDual({
4211
+ json: res,
4212
+ human: () => {
4213
+ if (res.items.length === 0) {
4214
+ printHuman(c.yellow("(no recommendations available right now)"));
4215
+ printHuman(
4216
+ c.gray(
4217
+ " \u2192 registry may be sparse; try `kordis search --trending` for broader discovery"
4218
+ )
4219
+ );
4220
+ return;
4221
+ }
4222
+ printHuman(
4223
+ `${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)`)}`
4224
+ );
4225
+ printHuman("");
4226
+ res.items.forEach((it, idx) => renderItem(idx + 1, it, opts.verbose));
4227
+ printHuman("");
4228
+ printHuman(
4229
+ c.gray(
4230
+ "\u2192 install any with `kordis pull <full_slug>` or peek with `kordis read <full_slug>`"
4231
+ )
4232
+ );
4233
+ if (typeof res.capped_by_author === "number" && res.capped_by_author > 0) {
4234
+ const extras = res.capped_by_author;
4235
+ printHuman(
4236
+ c.gray(
4237
+ `\u2192 ${extras} more candidate${extras === 1 ? "" : "s"} from the same publisher${extras === 1 ? " was" : "s were"} skipped by the diversity cap`
4238
+ )
4239
+ );
4240
+ }
4241
+ if (!res.personalized) {
4242
+ printHuman(
4243
+ c.gray(
4244
+ "\u2192 enable personalization for tailored picks: `kordis setup` (re-asks the opt-in)"
4245
+ )
4246
+ );
4247
+ }
4248
+ }
4249
+ });
4250
+ });
4251
+ }
4252
+ function renderItem(idx, it, verbose) {
4253
+ const confidenceBadge = it.confidence === "high" ? c.green("high") : it.confidence === "medium" ? c.cyan("medium") : c.gray("low");
4254
+ printHuman(
4255
+ `${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`
4256
+ );
4257
+ printHuman(` ${it.description.length > 100 ? `${it.description.slice(0, 100)}\u2026` : it.description}`);
4258
+ printHuman(c.gray(` \u2605 ${it.stars} \u21E3 ${it.downloads}`));
4259
+ printHuman(c.gray(` \u2192 ${it.why_recommended}`));
4260
+ printHuman(c.gray(` install: ${c.cyan(it.install_command)}`));
4261
+ printHuman(c.gray(` read: ${c.cyan(it.jit_read_command)}`));
4262
+ if (verbose && it.score_breakdown) {
4263
+ const b = it.score_breakdown;
4264
+ printHuman(
4265
+ c.gray(
4266
+ ` score: content=${b.content} cf=${b.cf} popularity=${b.popularity} final=${b.final}`
4267
+ )
4268
+ );
4269
+ }
4270
+ printHuman("");
4271
+ }
4272
+
3712
4273
  // src/index.ts
3713
4274
  function buildProgram() {
3714
- 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) => {
4275
+ 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) => {
3715
4276
  const opts = thisCommand.optsWithGlobals();
3716
4277
  if (opts.json) setJsonMode(true);
3717
4278
  });
@@ -3732,6 +4293,7 @@ function buildProgram() {
3732
4293
  program.addCommand(outdatedCommand());
3733
4294
  program.addCommand(updateCommand());
3734
4295
  program.addCommand(diffCommand());
4296
+ program.addCommand(recommendCommand());
3735
4297
  program.addCommand(setupCommand());
3736
4298
  return program;
3737
4299
  }
@@ -3782,8 +4344,15 @@ function handleError(err) {
3782
4344
  process.exit(err.exitCode());
3783
4345
  }
3784
4346
  if (err instanceof KordisNetworkError) {
3785
- if (isJsonMode()) printJson({ error: { code: "network_unreachable", message: err.message } });
3786
- else printHuman(c.red(`\u2717 ${err.message}`));
4347
+ const hint = "Check your network connection, or override the API endpoint with `KORDIS_API_URL=https://api.kordis.store` (or your self-hosted instance). If kordis.store itself is down, see https://kordis.store for status.";
4348
+ if (isJsonMode()) {
4349
+ printJson({
4350
+ error: { code: "network_unreachable", message: err.message, hint }
4351
+ });
4352
+ } else {
4353
+ printHuman(c.red(`\u2717 ${err.message}`));
4354
+ for (const line of hint.split("\n")) printHuman(c.gray(` \u2192 ${line}`));
4355
+ }
3787
4356
  process.exit(err.exitCode());
3788
4357
  }
3789
4358
  if (err instanceof Error) {