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