@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/assets/KORDIS.md +2 -1
- package/assets/kordis-recommend/SKILL.md +148 -0
- package/dist/index.js +719 -150
- package/package.json +1 -1
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
|
|
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
|
|
1515
|
+
import { dirname, join as join4 } from "path";
|
|
1156
1516
|
var __dir = dirname(fileURLToPath(import.meta.url));
|
|
1157
|
-
var pkgPath =
|
|
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
|
|
1442
|
-
import { resolve, join as
|
|
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
|
|
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 =
|
|
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
|
|
1534
|
-
var SkillFrontmatterSchema =
|
|
1535
|
-
name:
|
|
1536
|
-
description:
|
|
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:
|
|
1540
|
-
homepage:
|
|
1541
|
-
model:
|
|
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":
|
|
1544
|
-
"disable-model-invocation":
|
|
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:
|
|
1547
|
-
tags:
|
|
1548
|
-
tools:
|
|
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
|
|
1554
|
-
var McpTransportSchema =
|
|
1555
|
-
|
|
1556
|
-
type:
|
|
1557
|
-
command:
|
|
1558
|
-
args:
|
|
1559
|
-
env:
|
|
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
|
-
|
|
1562
|
-
type:
|
|
1563
|
-
url:
|
|
1564
|
-
headers:
|
|
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 =
|
|
1568
|
-
tools:
|
|
1569
|
-
resources:
|
|
1570
|
-
prompts:
|
|
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 =
|
|
1573
|
-
name:
|
|
1574
|
-
description:
|
|
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:
|
|
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:
|
|
2015
|
+
auth_notes: z14.string().optional(),
|
|
1585
2016
|
/** Required env vars with description + whether they must be secret. */
|
|
1586
|
-
env_schema:
|
|
1587
|
-
|
|
1588
|
-
description:
|
|
1589
|
-
required:
|
|
1590
|
-
secret:
|
|
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:
|
|
1594
|
-
repository:
|
|
1595
|
-
license:
|
|
1596
|
-
title:
|
|
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
|
|
1602
|
-
var ProjectRulesFrontmatterSchema =
|
|
1603
|
-
title:
|
|
1604
|
-
description:
|
|
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:
|
|
1607
|
-
target_path:
|
|
1608
|
-
globs:
|
|
1609
|
-
target_tools:
|
|
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:
|
|
1612
|
-
license:
|
|
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
|
|
1624
|
-
var CustomAgentFrontmatterSchema =
|
|
1625
|
-
name:
|
|
1626
|
-
description:
|
|
1627
|
-
title:
|
|
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:
|
|
2061
|
+
system_prompt: z16.string().min(1).max(5e4),
|
|
1631
2062
|
/** Optional model override — string passed verbatim to the host. */
|
|
1632
|
-
model:
|
|
2063
|
+
model: z16.string().optional(),
|
|
1633
2064
|
/** Tools this agent can call. Host-specific names (no uniform namespace). */
|
|
1634
|
-
allowed_tools:
|
|
1635
|
-
disallowed_tools:
|
|
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:
|
|
2068
|
+
context: z16.enum(["inherit", "fork", "isolated"]).optional(),
|
|
1638
2069
|
/** Known-good tool runtimes. Empty = untested everywhere. */
|
|
1639
|
-
tool_compat:
|
|
2070
|
+
tool_compat: z16.array(ToolSlugSchema).optional().default([]),
|
|
1640
2071
|
/** Pre-load skills when this agent activates. */
|
|
1641
|
-
preload_skills:
|
|
1642
|
-
license:
|
|
1643
|
-
homepage:
|
|
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
|
|
1649
|
-
var HookEventSchema =
|
|
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 =
|
|
1662
|
-
|
|
1663
|
-
type:
|
|
1664
|
-
command:
|
|
1665
|
-
timeout_ms:
|
|
1666
|
-
blocking:
|
|
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
|
-
|
|
1669
|
-
type:
|
|
1670
|
-
url:
|
|
1671
|
-
method:
|
|
1672
|
-
headers:
|
|
1673
|
-
timeout_ms:
|
|
1674
|
-
blocking:
|
|
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 =
|
|
2108
|
+
var HookBindingSchema = z17.object({
|
|
1678
2109
|
event: HookEventSchema,
|
|
1679
|
-
matcher:
|
|
2110
|
+
matcher: z17.string().optional(),
|
|
1680
2111
|
action: HookActionSchema
|
|
1681
2112
|
});
|
|
1682
|
-
var HookFrontmatterSchema =
|
|
1683
|
-
name:
|
|
1684
|
-
description:
|
|
1685
|
-
title:
|
|
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:
|
|
2119
|
+
hooks: z17.array(HookBindingSchema).min(1),
|
|
1689
2120
|
/** Declared to surface in search filters — always "claude-code" for now. */
|
|
1690
|
-
tool_compat:
|
|
1691
|
-
license:
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
2639
|
+
collected.push({ rel, abs: join6(base, rel) });
|
|
2209
2640
|
}
|
|
2210
2641
|
}
|
|
2211
2642
|
}
|
|
2212
2643
|
async function readAsset(absPath) {
|
|
2213
|
-
const buf = await
|
|
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
|
|
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
|
|
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
|
|
2275
|
-
import { dirname as dirname2, join as
|
|
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
|
|
2718
|
-
import { join as
|
|
2719
|
-
import { readFile as
|
|
2720
|
-
var LOCKFILE_PATH =
|
|
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
|
|
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
|
|
2731
|
-
await
|
|
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 =
|
|
2768
|
-
await
|
|
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 =
|
|
2788
|
-
await
|
|
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:
|
|
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
|
|
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
|
|
3307
|
+
await writeFile5(abs, Buffer.from(content, "base64"));
|
|
2875
3308
|
} else {
|
|
2876
|
-
await
|
|
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
|
|
2998
|
-
import { homedir as
|
|
2999
|
-
import { join as
|
|
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
|
|
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:
|
|
3467
|
+
bundled: join9(__dir2, "..", "assets"),
|
|
3033
3468
|
// Dev mode: src/commands/setup.ts → 4 levels up to repo root, then packages/skills/
|
|
3034
|
-
source:
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
3068
|
-
await
|
|
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
|
|
3514
|
+
await writeFile6(filePath, updated, "utf-8");
|
|
3080
3515
|
}
|
|
3081
3516
|
return "updated";
|
|
3082
3517
|
}
|
|
3083
3518
|
if (!dryRun) {
|
|
3084
|
-
await
|
|
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
|
|
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
|
|
3097
|
-
await
|
|
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 =
|
|
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 =
|
|
3121
|
-
const filePath =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
3786
|
-
|
|
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) {
|