@rayul/kordis 0.3.1 → 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
@@ -704,6 +704,109 @@ 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
+ }
808
+ });
809
+
707
810
  // ../../packages/contracts/src/index.ts
708
811
  var init_src = __esm({
709
812
  "../../packages/contracts/src/index.ts"() {
@@ -717,6 +820,8 @@ var init_src = __esm({
717
820
  init_comment();
718
821
  init_auth();
719
822
  init_api();
823
+ init_context();
824
+ init_recommend();
720
825
  }
721
826
  });
722
827
 
@@ -797,6 +902,7 @@ var init_client = __esm({
797
902
  const headers = {
798
903
  accept: "application/json",
799
904
  ...await this.authHeaders(),
905
+ ...this.config.getDynamicHeaders ? await this.config.getDynamicHeaders() : {},
800
906
  ...opts.headers ?? {}
801
907
  };
802
908
  if (opts.body !== void 0) headers["content-type"] = "application/json";
@@ -936,6 +1042,20 @@ var init_client = __esm({
936
1042
  popular() {
937
1043
  return this.request("/api/packages/popular");
938
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
+ }
939
1059
  star(username, slug) {
940
1060
  return this.request(`/api/packages/${username}/${slug}/star`, {
941
1061
  method: "POST"
@@ -1015,7 +1135,7 @@ var init_src2 = __esm({
1015
1135
 
1016
1136
  // src/index.ts
1017
1137
  init_src2();
1018
- import { Command as Command18 } from "commander";
1138
+ import { Command as Command19 } from "commander";
1019
1139
 
1020
1140
  // src/commands/login.ts
1021
1141
  import { Command } from "commander";
@@ -1023,6 +1143,7 @@ import open from "open";
1023
1143
 
1024
1144
  // src/client.ts
1025
1145
  init_src2();
1146
+ init_src();
1026
1147
 
1027
1148
  // src/config.ts
1028
1149
  import { homedir } from "os";
@@ -1042,7 +1163,8 @@ async function loadCliConfig() {
1042
1163
  return {
1043
1164
  apiUrl: apiUrlFromEnv ?? parsed.apiUrl ?? "https://api.kordis.store",
1044
1165
  ...parsed.username ? { username: parsed.username } : {},
1045
- ...parsed.sessionId ? { sessionId: parsed.sessionId } : {}
1166
+ ...parsed.sessionId ? { sessionId: parsed.sessionId } : {},
1167
+ personalization: parsed.personalization ?? "undecided"
1046
1168
  };
1047
1169
  } catch {
1048
1170
  return { apiUrl: apiUrlFromEnv ?? "https://api.kordis.store" };
@@ -1149,12 +1271,236 @@ async function readAll() {
1149
1271
  }
1150
1272
  }
1151
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
+
1152
1498
  // src/version.ts
1153
1499
  import { readFileSync } from "fs";
1154
1500
  import { fileURLToPath } from "url";
1155
- import { dirname, join as join3 } from "path";
1501
+ import { dirname, join as join4 } from "path";
1156
1502
  var __dir = dirname(fileURLToPath(import.meta.url));
1157
- var pkgPath = join3(__dir, "..", "package.json");
1503
+ var pkgPath = join4(__dir, "..", "package.json");
1158
1504
  var VERSION = (() => {
1159
1505
  try {
1160
1506
  const raw = readFileSync(pkgPath, "utf8");
@@ -1171,10 +1517,17 @@ async function useClient() {
1171
1517
  const keychain = await createKeychain();
1172
1518
  const username = cfg.username ?? null;
1173
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");
1174
1521
  const api = new KordisClient({
1175
1522
  baseUrl: cfg.apiUrl,
1176
1523
  userAgent: `kordis-cli/${VERSION} node/${process.version}`,
1177
- 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
+ } : {}
1178
1531
  });
1179
1532
  const authedRequest = async (fn) => {
1180
1533
  try {
@@ -1337,6 +1690,70 @@ function whoamiCommand() {
1337
1690
 
1338
1691
  // src/commands/search.ts
1339
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
1340
1757
  var SORT_MODES = [
1341
1758
  "relevance",
1342
1759
  "stars",
@@ -1438,8 +1855,8 @@ function searchCommand() {
1438
1855
 
1439
1856
  // src/commands/push.ts
1440
1857
  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";
1858
+ import { readFile as readFile5, stat as stat2, readdir as readdir2 } from "fs/promises";
1859
+ import { resolve, join as join6, posix, sep } from "path";
1443
1860
  import JSZip from "jszip";
1444
1861
 
1445
1862
  // ../../packages/harness-schema/src/parser.ts
@@ -1492,9 +1909,9 @@ var FrontmatterParseError = class extends Error {
1492
1909
 
1493
1910
  // ../../packages/harness-schema/src/deps.ts
1494
1911
  init_src();
1495
- import { z as z10 } from "zod";
1912
+ import { z as z12 } from "zod";
1496
1913
  var StringDepRe = /^([a-z0-9](-?[a-z0-9])*\/[a-z0-9](-?[a-z0-9])*)(?:@(.+))?$/;
1497
- var ObjectDepSchema = z10.object({
1914
+ var ObjectDepSchema = z12.object({
1498
1915
  full_slug: FullSlugSchema,
1499
1916
  version_range: SemverRangeSchema.default("*")
1500
1917
  });
@@ -1530,50 +1947,50 @@ init_src();
1530
1947
 
1531
1948
  // ../../packages/harness-schema/src/types/skill.ts
1532
1949
  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),
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),
1537
1954
  version: SemverSchema.optional(),
1538
1955
  // Agent Skills optional metadata
1539
- license: z11.string().max(64).optional(),
1540
- homepage: z11.string().url().optional(),
1541
- model: z11.string().optional(),
1956
+ license: z13.string().max(64).optional(),
1957
+ homepage: z13.string().url().optional(),
1958
+ model: z13.string().optional(),
1542
1959
  // model override hint
1543
- "allowed-tools": z11.array(z11.string()).optional(),
1544
- "disable-model-invocation": z11.boolean().optional(),
1960
+ "allowed-tools": z13.array(z13.string()).optional(),
1961
+ "disable-model-invocation": z13.boolean().optional(),
1545
1962
  // 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()
1963
+ title: z13.string().min(1).max(200).optional(),
1964
+ tags: z13.array(TagSchema).max(16).optional(),
1965
+ tools: z13.array(ToolSlugSchema).optional()
1549
1966
  }).strict();
1550
1967
 
1551
1968
  // ../../packages/harness-schema/src/types/mcp-server.ts
1552
1969
  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({})
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({})
1560
1977
  }),
1561
- z12.object({
1562
- type: z12.enum(["http", "sse"]),
1563
- url: z12.string().url(),
1564
- 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({})
1565
1982
  })
1566
1983
  ]);
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([])
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([])
1571
1988
  });
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),
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),
1575
1992
  version: SemverSchema,
1576
- mcp_spec_version: z12.string().default("2026-03"),
1993
+ mcp_spec_version: z14.string().default("2026-03"),
1577
1994
  transport: McpTransportSchema,
1578
1995
  capabilities: McpCapabilitiesSchema.optional().default({
1579
1996
  tools: [],
@@ -1581,35 +1998,35 @@ var McpServerFrontmatterSchema = z12.object({
1581
1998
  prompts: []
1582
1999
  }),
1583
2000
  /** Human-readable setup / auth instructions. Surfaced in `kordis info`. */
1584
- auth_notes: z12.string().optional(),
2001
+ auth_notes: z14.string().optional(),
1585
2002
  /** 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)
2003
+ env_schema: z14.record(
2004
+ z14.object({
2005
+ description: z14.string(),
2006
+ required: z14.boolean().default(true),
2007
+ secret: z14.boolean().default(false)
1591
2008
  })
1592
2009
  ).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()
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()
1597
2014
  }).strict();
1598
2015
 
1599
2016
  // ../../packages/harness-schema/src/types/project-rules.ts
1600
2017
  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),
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),
1605
2022
  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([]),
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([]),
1610
2027
  /** When true, installation is idempotent via `<!-- kordis:... -->` markers. */
1611
- mergeable: z13.boolean().default(true),
1612
- license: z13.string().max(64).optional()
2028
+ mergeable: z15.boolean().default(true),
2029
+ license: z15.string().max(64).optional()
1613
2030
  }).strict().refine(
1614
2031
  (v) => v.scope !== "nested" || !!v.target_path,
1615
2032
  { message: 'scope="nested" requires target_path' }
@@ -1620,33 +2037,33 @@ var ProjectRulesFrontmatterSchema = z13.object({
1620
2037
 
1621
2038
  // ../../packages/harness-schema/src/types/custom-agent.ts
1622
2039
  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(),
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(),
1628
2045
  version: SemverSchema.optional(),
1629
2046
  /** System prompt that defines the agent's persona + rules. Required. */
1630
- system_prompt: z14.string().min(1).max(5e4),
2047
+ system_prompt: z16.string().min(1).max(5e4),
1631
2048
  /** Optional model override — string passed verbatim to the host. */
1632
- model: z14.string().optional(),
2049
+ model: z16.string().optional(),
1633
2050
  /** 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([]),
2051
+ allowed_tools: z16.array(z16.string()).optional().default([]),
2052
+ disallowed_tools: z16.array(z16.string()).optional().default([]),
1636
2053
  /** Fork/context behaviour hint (Claude: `context: fork` etc.). */
1637
- context: z14.enum(["inherit", "fork", "isolated"]).optional(),
2054
+ context: z16.enum(["inherit", "fork", "isolated"]).optional(),
1638
2055
  /** Known-good tool runtimes. Empty = untested everywhere. */
1639
- tool_compat: z14.array(ToolSlugSchema).optional().default([]),
2056
+ tool_compat: z16.array(ToolSlugSchema).optional().default([]),
1640
2057
  /** 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()
2058
+ preload_skills: z16.array(z16.string()).optional().default([]),
2059
+ license: z16.string().max(64).optional(),
2060
+ homepage: z16.string().url().optional()
1644
2061
  }).strict();
1645
2062
 
1646
2063
  // ../../packages/harness-schema/src/types/hook.ts
1647
2064
  init_src();
1648
- import { z as z15 } from "zod";
1649
- var HookEventSchema = z15.enum([
2065
+ import { z as z17 } from "zod";
2066
+ var HookEventSchema = z17.enum([
1650
2067
  "PreToolUse",
1651
2068
  "PostToolUse",
1652
2069
  "UserPromptSubmit",
@@ -1658,37 +2075,37 @@ var HookEventSchema = z15.enum([
1658
2075
  "SubagentStop",
1659
2076
  "Background"
1660
2077
  ]);
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)
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)
1667
2084
  }),
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)
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)
1675
2092
  })
1676
2093
  ]);
1677
- var HookBindingSchema = z15.object({
2094
+ var HookBindingSchema = z17.object({
1678
2095
  event: HookEventSchema,
1679
- matcher: z15.string().optional(),
2096
+ matcher: z17.string().optional(),
1680
2097
  action: HookActionSchema
1681
2098
  });
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(),
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(),
1686
2103
  version: SemverSchema.optional(),
1687
2104
  /** One or more hook bindings. */
1688
- hooks: z15.array(HookBindingSchema).min(1),
2105
+ hooks: z17.array(HookBindingSchema).min(1),
1689
2106
  /** 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()
2107
+ tool_compat: z17.array(z17.enum(["claude-code"])).default(["claude-code"]),
2108
+ license: z17.string().max(64).optional()
1692
2109
  }).strict();
1693
2110
 
1694
2111
  // ../../packages/harness-schema/src/validator.ts
@@ -2007,7 +2424,7 @@ function pushCommand() {
2007
2424
  async (path, opts) => {
2008
2425
  const bundle = await useClient();
2009
2426
  const abs = resolve(process.cwd(), path);
2010
- const s = await stat(abs);
2427
+ const s = await stat2(abs);
2011
2428
  let content;
2012
2429
  let assets = [];
2013
2430
  if (s.isDirectory()) {
@@ -2017,7 +2434,7 @@ function pushCommand() {
2017
2434
  ({ content, assets } = await bundleSkillZip(abs));
2018
2435
  content = ensureTypeField(content, "skill");
2019
2436
  } else {
2020
- content = await readFile3(abs, "utf8");
2437
+ content = await readFile5(abs, "utf8");
2021
2438
  }
2022
2439
  let nameFromFm = null;
2023
2440
  try {
@@ -2198,19 +2615,19 @@ function shouldExclude(relPath, isDir) {
2198
2615
  return false;
2199
2616
  }
2200
2617
  async function walkDir(base, relDir, collected) {
2201
- const entries = await readdir(join4(base, relDir), { withFileTypes: true });
2618
+ const entries = await readdir2(join6(base, relDir), { withFileTypes: true });
2202
2619
  for (const entry of entries) {
2203
2620
  const rel = relDir ? `${relDir}/${entry.name}` : entry.name;
2204
2621
  if (shouldExclude(rel, entry.isDirectory())) continue;
2205
2622
  if (entry.isDirectory()) {
2206
2623
  await walkDir(base, rel, collected);
2207
2624
  } else if (entry.isFile()) {
2208
- collected.push({ rel, abs: join4(base, rel) });
2625
+ collected.push({ rel, abs: join6(base, rel) });
2209
2626
  }
2210
2627
  }
2211
2628
  }
2212
2629
  async function readAsset(absPath) {
2213
- const buf = await readFile3(absPath);
2630
+ const buf = await readFile5(absPath);
2214
2631
  const text = buf.toString("utf8");
2215
2632
  const roundtrip = Buffer.from(text, "utf8");
2216
2633
  if (roundtrip.equals(buf)) {
@@ -2227,7 +2644,7 @@ async function bundleDirectory(dir) {
2227
2644
  if (!skillEntry) {
2228
2645
  throw new Error(`No SKILL.md found in directory ${dir}. Push a directory that has SKILL.md at its root.`);
2229
2646
  }
2230
- const content = await readFile3(skillEntry.abs, "utf8");
2647
+ const content = await readFile5(skillEntry.abs, "utf8");
2231
2648
  const assets = [];
2232
2649
  for (const f of all) {
2233
2650
  if (f.rel === "SKILL.md") continue;
@@ -2237,7 +2654,7 @@ async function bundleDirectory(dir) {
2237
2654
  return { content, assets };
2238
2655
  }
2239
2656
  async function bundleSkillZip(zipPath) {
2240
- const buf = await readFile3(zipPath);
2657
+ const buf = await readFile5(zipPath);
2241
2658
  const zip = await JSZip.loadAsync(buf);
2242
2659
  const entries = Object.keys(zip.files).filter((k) => !zip.files[k].dir);
2243
2660
  if (entries.length === 0) throw new Error(".skill file is empty");
@@ -2271,8 +2688,8 @@ async function bundleSkillZip(zipPath) {
2271
2688
 
2272
2689
  // src/commands/pull.ts
2273
2690
  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";
2691
+ import { mkdir as mkdir5, writeFile as writeFile5 } from "fs/promises";
2692
+ import { dirname as dirname2, join as join8 } from "path";
2276
2693
 
2277
2694
  // ../../packages/translators/src/skill/identity.ts
2278
2695
  function buildSkillFile(input, outputPath) {
@@ -2714,21 +3131,21 @@ function getTranslator(type, format) {
2714
3131
  }
2715
3132
 
2716
3133
  // 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");
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");
2721
3138
  async function readLockfile() {
2722
3139
  try {
2723
- const raw = await readFile4(LOCKFILE_PATH, "utf8");
3140
+ const raw = await readFile6(LOCKFILE_PATH, "utf8");
2724
3141
  return JSON.parse(raw);
2725
3142
  } catch {
2726
3143
  return { packages: {} };
2727
3144
  }
2728
3145
  }
2729
3146
  async function writeLockfile(lf) {
2730
- await mkdir3(join5(homedir3(), ".kordis"), { recursive: true });
2731
- 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");
2732
3149
  }
2733
3150
  async function recordInstalled(fullSlug, entry) {
2734
3151
  const lf = await readLockfile();
@@ -2764,8 +3181,8 @@ async function doPullInstall(fullSlug, tool, installRoot, version) {
2764
3181
  assets: contentRes.assets
2765
3182
  });
2766
3183
  for (const f of result.files) {
2767
- const abs = join6(installRoot, f.path);
2768
- await mkdir4(dirname2(abs), { recursive: true });
3184
+ const abs = join8(installRoot, f.path);
3185
+ await mkdir5(dirname2(abs), { recursive: true });
2769
3186
  await writeEmittedFile(abs, f.content, f.encoding);
2770
3187
  written.push(abs);
2771
3188
  }
@@ -2784,8 +3201,8 @@ async function doPullInstall(fullSlug, tool, installRoot, version) {
2784
3201
  `Tool "${format}" is not supported for type "${parsed.type}"; installed canonical form instead.`
2785
3202
  );
2786
3203
  for (const f of result.files) {
2787
- const abs = join6(installRoot, f.path);
2788
- await mkdir4(dirname2(abs), { recursive: true });
3204
+ const abs = join8(installRoot, f.path);
3205
+ await mkdir5(dirname2(abs), { recursive: true });
2789
3206
  await writeEmittedFile(abs, f.content, f.encoding);
2790
3207
  written.push(abs);
2791
3208
  }
@@ -2842,11 +3259,13 @@ function pullCommand() {
2842
3259
  for (const n of result.notes) printHuman(c.yellow(` ! ${n}`));
2843
3260
  }
2844
3261
  });
3262
+ const reminder = await maybeRenderReminder();
3263
+ if (reminder) printHuman(c.gray(reminder));
2845
3264
  }
2846
3265
  );
2847
3266
  }
2848
3267
  async function detectTool(root) {
2849
- const { stat: stat2 } = await import("fs/promises");
3268
+ const { stat: stat3 } = await import("fs/promises");
2850
3269
  const checks = [
2851
3270
  [".claude", "claude-code"],
2852
3271
  [".cursor", "cursor"],
@@ -2862,7 +3281,7 @@ async function detectTool(root) {
2862
3281
  ];
2863
3282
  for (const [path, tool] of checks) {
2864
3283
  try {
2865
- await stat2(join6(root, path));
3284
+ await stat3(join8(root, path));
2866
3285
  return tool;
2867
3286
  } catch {
2868
3287
  }
@@ -2871,9 +3290,9 @@ async function detectTool(root) {
2871
3290
  }
2872
3291
  async function writeEmittedFile(abs, content, encoding) {
2873
3292
  if (encoding === "base64") {
2874
- await writeFile4(abs, Buffer.from(content, "base64"));
3293
+ await writeFile5(abs, Buffer.from(content, "base64"));
2875
3294
  } else {
2876
- await writeFile4(abs, content, "utf8");
3295
+ await writeFile5(abs, content, "utf8");
2877
3296
  }
2878
3297
  }
2879
3298
 
@@ -2994,11 +3413,12 @@ function logoutCommand() {
2994
3413
  }
2995
3414
 
2996
3415
  // 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";
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";
3000
3419
  import { fileURLToPath as fileURLToPath2 } from "url";
3001
3420
  import { Command as Command9 } from "commander";
3421
+ import prompts from "prompts";
3002
3422
  var MARKER_START = "<!-- kordis-start -->";
3003
3423
  var MARKER_END = "<!-- kordis-end -->";
3004
3424
  var HUB_TARGETS = [
@@ -3010,7 +3430,8 @@ var SUB_SKILLS = [
3010
3430
  "kordis-search",
3011
3431
  "kordis-using",
3012
3432
  "kordis-publishing",
3013
- "kordis-social"
3433
+ "kordis-social",
3434
+ "kordis-recommend"
3014
3435
  ];
3015
3436
  var SKILL_TARGETS = [
3016
3437
  { tool: "Claude Code", dir: ".claude", skillsDir: ".claude/skills" },
@@ -3019,7 +3440,7 @@ var SKILL_TARGETS = [
3019
3440
  ];
3020
3441
  async function exists(path) {
3021
3442
  try {
3022
- await access2(path);
3443
+ await access3(path);
3023
3444
  return true;
3024
3445
  } catch {
3025
3446
  return false;
@@ -3029,18 +3450,18 @@ function assetRoots() {
3029
3450
  const __dir2 = dirname3(fileURLToPath2(import.meta.url));
3030
3451
  return {
3031
3452
  // Built binary: bin/kordis.js → assets/ is one level up
3032
- bundled: join7(__dir2, "..", "assets"),
3453
+ bundled: join9(__dir2, "..", "assets"),
3033
3454
  // Dev mode: src/commands/setup.ts → 4 levels up to repo root, then packages/skills/
3034
- source: join7(__dir2, "..", "..", "..", "..", "packages", "skills")
3455
+ source: join9(__dir2, "..", "..", "..", "..", "packages", "skills")
3035
3456
  };
3036
3457
  }
3037
3458
  async function pickAssetRoot() {
3038
3459
  const { bundled, source } = assetRoots();
3039
- return await exists(join7(bundled, "KORDIS.md")) ? bundled : source;
3460
+ return await exists(join9(bundled, "KORDIS.md")) ? bundled : source;
3040
3461
  }
3041
3462
  async function loadHubContent() {
3042
3463
  const root = await pickAssetRoot();
3043
- const raw = await readFile5(join7(root, "KORDIS.md"), "utf-8");
3464
+ const raw = await readFile7(join9(root, "KORDIS.md"), "utf-8");
3044
3465
  if (raw.startsWith("---")) {
3045
3466
  const end = raw.indexOf("\n---", 4);
3046
3467
  if (end !== -1) return raw.slice(end + 4).trimStart();
@@ -3049,7 +3470,7 @@ async function loadHubContent() {
3049
3470
  }
3050
3471
  async function loadSubSkillContent(name) {
3051
3472
  const root = await pickAssetRoot();
3052
- return readFile5(join7(root, name, "SKILL.md"), "utf-8");
3473
+ return readFile7(join9(root, name, "SKILL.md"), "utf-8");
3053
3474
  }
3054
3475
  async function injectBlock(filePath, content, dryRun) {
3055
3476
  const block = `${MARKER_START}
@@ -3058,14 +3479,14 @@ ${MARKER_END}`;
3058
3479
  let existing = "";
3059
3480
  let fileExists = true;
3060
3481
  try {
3061
- existing = await readFile5(filePath, "utf-8");
3482
+ existing = await readFile7(filePath, "utf-8");
3062
3483
  } catch {
3063
3484
  fileExists = false;
3064
3485
  }
3065
3486
  if (!fileExists) {
3066
3487
  if (!dryRun) {
3067
- await mkdir5(dirname3(filePath), { recursive: true });
3068
- await writeFile5(filePath, block + "\n", "utf-8");
3488
+ await mkdir6(dirname3(filePath), { recursive: true });
3489
+ await writeFile6(filePath, block + "\n", "utf-8");
3069
3490
  }
3070
3491
  return "injected";
3071
3492
  }
@@ -3076,31 +3497,31 @@ ${MARKER_END}`;
3076
3497
  if (current === block) return "unchanged";
3077
3498
  if (!dryRun) {
3078
3499
  const updated = existing.slice(0, startIdx) + block + existing.slice(endIdx + MARKER_END.length);
3079
- await writeFile5(filePath, updated, "utf-8");
3500
+ await writeFile6(filePath, updated, "utf-8");
3080
3501
  }
3081
3502
  return "updated";
3082
3503
  }
3083
3504
  if (!dryRun) {
3084
- await writeFile5(filePath, existing.trimEnd() + "\n\n" + block + "\n", "utf-8");
3505
+ await writeFile6(filePath, existing.trimEnd() + "\n\n" + block + "\n", "utf-8");
3085
3506
  }
3086
3507
  return "injected";
3087
3508
  }
3088
3509
  async function copyBundledSkill(filePath, content, dryRun) {
3089
3510
  let existing = null;
3090
3511
  try {
3091
- existing = await readFile5(filePath, "utf-8");
3512
+ existing = await readFile7(filePath, "utf-8");
3092
3513
  } catch {
3093
3514
  }
3094
3515
  if (existing === content) return "unchanged";
3095
3516
  if (!dryRun) {
3096
- await mkdir5(dirname3(filePath), { recursive: true });
3097
- await writeFile5(filePath, content, "utf-8");
3517
+ await mkdir6(dirname3(filePath), { recursive: true });
3518
+ await writeFile6(filePath, content, "utf-8");
3098
3519
  }
3099
3520
  return existing === null ? "injected" : "updated";
3100
3521
  }
3101
3522
  function setupCommand() {
3102
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) => {
3103
- const home = homedir4();
3524
+ const home = homedir6();
3104
3525
  const dryRun = opts.dryRun ?? false;
3105
3526
  let hubContent;
3106
3527
  let subSkillContents;
@@ -3117,8 +3538,8 @@ function setupCommand() {
3117
3538
  let anyFound = false;
3118
3539
  const changedTools = /* @__PURE__ */ new Set();
3119
3540
  for (const target of HUB_TARGETS) {
3120
- const dirPath = join7(home, target.dir);
3121
- const filePath = join7(home, target.file);
3541
+ const dirPath = join9(home, target.dir);
3542
+ const filePath = join9(home, target.file);
3122
3543
  if (!await exists(dirPath)) {
3123
3544
  if (!opts.silent) {
3124
3545
  printHuman(c.gray(` - ${target.tool.padEnd(12)} not installed, skipped`));
@@ -3145,10 +3566,10 @@ function setupCommand() {
3145
3566
  }
3146
3567
  }
3147
3568
  for (const target of SKILL_TARGETS) {
3148
- const dirPath = join7(home, target.dir);
3569
+ const dirPath = join9(home, target.dir);
3149
3570
  if (!await exists(dirPath)) continue;
3150
3571
  for (const subName of SUB_SKILLS) {
3151
- const filePath = join7(home, target.skillsDir, subName, "SKILL.md");
3572
+ const filePath = join9(home, target.skillsDir, subName, "SKILL.md");
3152
3573
  const dest = `~/${target.skillsDir}/${subName}/SKILL.md`;
3153
3574
  try {
3154
3575
  const result = await copyBundledSkill(filePath, subSkillContents[subName], dryRun);
@@ -3180,6 +3601,34 @@ function setupCommand() {
3180
3601
  printHuman(c.gray("No supported agent tools detected."));
3181
3602
  printHuman(c.gray("Run `kordis setup` again after installing Claude Code, Codex, or Gemini CLI."));
3182
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
+ }
3183
3632
  });
3184
3633
  }
3185
3634
 
@@ -3280,7 +3729,7 @@ function unstarCommand() {
3280
3729
 
3281
3730
  // src/commands/delete.ts
3282
3731
  import { Command as Command12 } from "commander";
3283
- import prompts from "prompts";
3732
+ import prompts2 from "prompts";
3284
3733
  function deleteCommand() {
3285
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) => {
3286
3735
  const [username, slug] = fullSlug.split("/");
@@ -3295,7 +3744,7 @@ function deleteCommand() {
3295
3744
  printHuman(` "${detail.description?.slice(0, 80) ?? ""}"`);
3296
3745
  printHuman(c.yellow(` The slug cannot be reused after deletion. This cannot be undone via the CLI.`));
3297
3746
  printHuman("");
3298
- const { confirmed } = await prompts({
3747
+ const { confirmed } = await prompts2({
3299
3748
  type: "confirm",
3300
3749
  name: "confirmed",
3301
3750
  message: `Delete ${c.bold(fullSlug)}?`,
@@ -3709,9 +4158,99 @@ function diffCommand() {
3709
4158
  });
3710
4159
  }
3711
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
+
3712
4251
  // src/index.ts
3713
4252
  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) => {
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) => {
3715
4254
  const opts = thisCommand.optsWithGlobals();
3716
4255
  if (opts.json) setJsonMode(true);
3717
4256
  });
@@ -3732,6 +4271,7 @@ function buildProgram() {
3732
4271
  program.addCommand(outdatedCommand());
3733
4272
  program.addCommand(updateCommand());
3734
4273
  program.addCommand(diffCommand());
4274
+ program.addCommand(recommendCommand());
3735
4275
  program.addCommand(setupCommand());
3736
4276
  return program;
3737
4277
  }