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