skillwiki 0.2.0-beta.3 → 0.2.0-beta.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +277 -32
- package/package.json +13 -4
- package/skills/.claude-plugin/plugin.json +3 -2
- package/skills/hooks/hooks.json +16 -0
- package/skills/hooks/run-hook.cmd +43 -0
- package/skills/hooks/session-start +29 -0
- package/skills/package.json +2 -2
- package/skills/using-skillwiki/SKILL.md +57 -0
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
+
import { readFileSync } from "fs";
|
|
4
5
|
import { Command } from "commander";
|
|
5
6
|
|
|
6
7
|
// src/utils/output.ts
|
|
@@ -9,9 +10,14 @@ function printJson(r) {
|
|
|
9
10
|
}
|
|
10
11
|
function printHuman(r) {
|
|
11
12
|
if (r.ok) {
|
|
12
|
-
|
|
13
|
+
if (typeof r.data === "object" && r.data !== null && "humanHint" in r.data) {
|
|
14
|
+
process.stdout.write(`${r.data.humanHint}
|
|
15
|
+
`);
|
|
16
|
+
} else {
|
|
17
|
+
process.stdout.write(`OK
|
|
13
18
|
${formatData(r.data)}
|
|
14
19
|
`);
|
|
20
|
+
}
|
|
15
21
|
} else {
|
|
16
22
|
process.stdout.write(`ERR ${r.error}
|
|
17
23
|
${r.detail !== void 0 ? formatData(r.detail) + "\n" : ""}`);
|
|
@@ -53,7 +59,11 @@ var ExitCode = {
|
|
|
53
59
|
LINT_HAS_WARNINGS: 22,
|
|
54
60
|
LINT_HAS_ERRORS: 23,
|
|
55
61
|
ENV_WRITE_CONFLICT: 24,
|
|
56
|
-
NO_VAULT_CONFIGURED: 25
|
|
62
|
+
NO_VAULT_CONFIGURED: 25,
|
|
63
|
+
INVALID_CONFIG_KEY: 26,
|
|
64
|
+
CONFIG_WRITE_FAILED: 27,
|
|
65
|
+
DOCTOR_HAS_WARNINGS: 28,
|
|
66
|
+
DOCTOR_HAS_ERRORS: 29
|
|
57
67
|
};
|
|
58
68
|
|
|
59
69
|
// ../shared/src/json-output.ts
|
|
@@ -479,15 +489,11 @@ async function runOverlap(input) {
|
|
|
479
489
|
import { join as join2 } from "path";
|
|
480
490
|
|
|
481
491
|
// src/utils/dotenv.ts
|
|
482
|
-
import { readFile as readFile4 } from "fs/promises";
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
text = await readFile4(path, "utf8");
|
|
488
|
-
} catch {
|
|
489
|
-
return {};
|
|
490
|
-
}
|
|
492
|
+
import { readFile as readFile4, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
493
|
+
import { dirname as dirname2 } from "path";
|
|
494
|
+
var CONFIG_KEYS = ["WIKI_PATH", "WIKI_LANG"];
|
|
495
|
+
var _whitelist = new Set(CONFIG_KEYS);
|
|
496
|
+
function parseDotenvText(text) {
|
|
491
497
|
const out = {};
|
|
492
498
|
for (const rawLine of text.split(/\r?\n/)) {
|
|
493
499
|
const line = rawLine.trim();
|
|
@@ -496,12 +502,65 @@ async function parseDotenvFile(path) {
|
|
|
496
502
|
if (eq <= 0) continue;
|
|
497
503
|
const key = line.slice(0, eq).trim();
|
|
498
504
|
const value = line.slice(eq + 1).trim();
|
|
499
|
-
if (!
|
|
505
|
+
if (!_whitelist.has(key)) continue;
|
|
500
506
|
if (value.length === 0) continue;
|
|
501
507
|
out[key] = value;
|
|
502
508
|
}
|
|
503
509
|
return out;
|
|
504
510
|
}
|
|
511
|
+
async function parseDotenvFile(path) {
|
|
512
|
+
let text;
|
|
513
|
+
try {
|
|
514
|
+
text = await readFile4(path, "utf8");
|
|
515
|
+
} catch {
|
|
516
|
+
return {};
|
|
517
|
+
}
|
|
518
|
+
return parseDotenvText(text);
|
|
519
|
+
}
|
|
520
|
+
async function writeDotenv(filePath, entries, originalContent) {
|
|
521
|
+
const lines = originalContent !== void 0 ? updateLines(originalContent, entries) : freshLines(entries);
|
|
522
|
+
await mkdir2(dirname2(filePath), { recursive: true });
|
|
523
|
+
await writeFile2(filePath, lines.join("\n") + "\n", "utf8");
|
|
524
|
+
}
|
|
525
|
+
function freshLines(entries) {
|
|
526
|
+
const out = [];
|
|
527
|
+
for (const [key, value] of Object.entries(entries)) {
|
|
528
|
+
if (value !== void 0) out.push(`${key}=${value}`);
|
|
529
|
+
}
|
|
530
|
+
return out;
|
|
531
|
+
}
|
|
532
|
+
function updateLines(originalContent, entries) {
|
|
533
|
+
let rawLines = originalContent.split(/\r?\n/);
|
|
534
|
+
if (rawLines.length > 0 && rawLines[rawLines.length - 1] === "") {
|
|
535
|
+
rawLines = rawLines.slice(0, -1);
|
|
536
|
+
}
|
|
537
|
+
const keysToWrite = new Set(Object.keys(entries));
|
|
538
|
+
const out = [];
|
|
539
|
+
for (const line of rawLines) {
|
|
540
|
+
const trimmed = line.trim();
|
|
541
|
+
if (trimmed.length === 0 || trimmed.startsWith("#")) {
|
|
542
|
+
out.push(line);
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
const eq = trimmed.indexOf("=");
|
|
546
|
+
if (eq <= 0) {
|
|
547
|
+
out.push(line);
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
const key = trimmed.slice(0, eq).trim();
|
|
551
|
+
if (keysToWrite.has(key)) {
|
|
552
|
+
out.push(`${key}=${entries[key]}`);
|
|
553
|
+
keysToWrite.delete(key);
|
|
554
|
+
} else {
|
|
555
|
+
out.push(line);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
for (const key of keysToWrite) {
|
|
559
|
+
const value = entries[key];
|
|
560
|
+
if (value !== void 0) out.push(`${key}=${value}`);
|
|
561
|
+
}
|
|
562
|
+
return out;
|
|
563
|
+
}
|
|
505
564
|
|
|
506
565
|
// src/utils/wiki-path.ts
|
|
507
566
|
async function resolveInitTimePath(input) {
|
|
@@ -630,7 +689,7 @@ function simulateRemoval(adj, removed) {
|
|
|
630
689
|
|
|
631
690
|
// src/commands/audit.ts
|
|
632
691
|
import { readFile as readFile5, stat as stat2 } from "fs/promises";
|
|
633
|
-
import { dirname as
|
|
692
|
+
import { dirname as dirname3, resolve, join as join3 } from "path";
|
|
634
693
|
|
|
635
694
|
// src/parsers/citations.ts
|
|
636
695
|
var FENCE2 = /```[\s\S]*?```/g;
|
|
@@ -657,7 +716,7 @@ async function runAudit(input) {
|
|
|
657
716
|
if (!fm.ok) return { exitCode: ExitCode.INVALID_FRONTMATTER, result: fm };
|
|
658
717
|
const split = splitFrontmatter(text);
|
|
659
718
|
const body = split.ok ? split.data.body : text;
|
|
660
|
-
const vault = await findVaultRoot(
|
|
719
|
+
const vault = await findVaultRoot(dirname3(resolve(input.file)));
|
|
661
720
|
if (!vault) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID") };
|
|
662
721
|
const markers = extractCitationMarkers(body);
|
|
663
722
|
const resolved = await Promise.all(markers.map(async (m) => {
|
|
@@ -688,7 +747,7 @@ async function findVaultRoot(start) {
|
|
|
688
747
|
return cur;
|
|
689
748
|
} catch {
|
|
690
749
|
}
|
|
691
|
-
const parent =
|
|
750
|
+
const parent = dirname3(cur);
|
|
692
751
|
if (parent === cur) return null;
|
|
693
752
|
cur = parent;
|
|
694
753
|
}
|
|
@@ -700,10 +759,10 @@ import { readdir as readdir2, stat as stat4 } from "fs/promises";
|
|
|
700
759
|
import { join as join4 } from "path";
|
|
701
760
|
|
|
702
761
|
// src/utils/install-fs.ts
|
|
703
|
-
import { copyFile, mkdir as
|
|
704
|
-
import { dirname as
|
|
762
|
+
import { copyFile, mkdir as mkdir3, rename, writeFile as writeFile3, stat as stat3 } from "fs/promises";
|
|
763
|
+
import { dirname as dirname4 } from "path";
|
|
705
764
|
async function atomicCopyWithBackup(src, dst) {
|
|
706
|
-
await
|
|
765
|
+
await mkdir3(dirname4(dst), { recursive: true });
|
|
707
766
|
let backupPath = null;
|
|
708
767
|
try {
|
|
709
768
|
await stat3(dst);
|
|
@@ -721,9 +780,9 @@ async function atomicCopyWithBackup(src, dst) {
|
|
|
721
780
|
return ok({ copied: true, backupPath });
|
|
722
781
|
}
|
|
723
782
|
async function writeManifest(path, m) {
|
|
724
|
-
await
|
|
783
|
+
await mkdir3(dirname4(path), { recursive: true });
|
|
725
784
|
const enriched = { installed_at: (/* @__PURE__ */ new Date()).toISOString(), ...m };
|
|
726
|
-
await
|
|
785
|
+
await writeFile3(path, JSON.stringify(enriched, null, 2));
|
|
727
786
|
}
|
|
728
787
|
|
|
729
788
|
// src/commands/install.ts
|
|
@@ -839,8 +898,8 @@ async function runLang(input) {
|
|
|
839
898
|
}
|
|
840
899
|
|
|
841
900
|
// src/commands/init.ts
|
|
842
|
-
import { mkdir as
|
|
843
|
-
import { join as join7, dirname as
|
|
901
|
+
import { mkdir as mkdir4, readFile as readFile6, stat as stat5, writeFile as writeFile4 } from "fs/promises";
|
|
902
|
+
import { join as join7, dirname as dirname5 } from "path";
|
|
844
903
|
var DEFAULT_TAXONOMY = [
|
|
845
904
|
"research",
|
|
846
905
|
"comparison",
|
|
@@ -899,9 +958,9 @@ async function runInit(input) {
|
|
|
899
958
|
}
|
|
900
959
|
const created = [];
|
|
901
960
|
try {
|
|
902
|
-
await
|
|
961
|
+
await mkdir4(target, { recursive: true });
|
|
903
962
|
for (const d of VAULT_DIRS) {
|
|
904
|
-
await
|
|
963
|
+
await mkdir4(join7(target, d), { recursive: true });
|
|
905
964
|
created.push(d + "/");
|
|
906
965
|
}
|
|
907
966
|
} catch (e) {
|
|
@@ -913,7 +972,7 @@ async function runInit(input) {
|
|
|
913
972
|
try {
|
|
914
973
|
const schemaTpl = await readFile6(join7(input.templates, "SCHEMA.md"), "utf8");
|
|
915
974
|
const schema = schemaTpl.replace("{{DOMAIN}}", input.domain).replace("{{WIKI_LANG}}", canonicalLang).replace("{{TAXONOMY_YAML}}", taxonomyYaml);
|
|
916
|
-
await
|
|
975
|
+
await writeFile4(join7(target, "SCHEMA.md"), schema, "utf8");
|
|
917
976
|
created.push("SCHEMA.md");
|
|
918
977
|
} catch (e) {
|
|
919
978
|
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: "SCHEMA.md", message: String(e) }) };
|
|
@@ -921,7 +980,7 @@ async function runInit(input) {
|
|
|
921
980
|
try {
|
|
922
981
|
const idxTpl = await readFile6(join7(input.templates, "index.md"), "utf8");
|
|
923
982
|
const idx = idxTpl.replace("{{INIT_DATE}}", today);
|
|
924
|
-
await
|
|
983
|
+
await writeFile4(join7(target, "index.md"), idx, "utf8");
|
|
925
984
|
created.push("index.md");
|
|
926
985
|
} catch (e) {
|
|
927
986
|
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: "index.md", message: String(e) }) };
|
|
@@ -929,17 +988,17 @@ async function runInit(input) {
|
|
|
929
988
|
try {
|
|
930
989
|
const logTpl = await readFile6(join7(input.templates, "log.md"), "utf8");
|
|
931
990
|
const log = logTpl.replace(/\{\{INIT_DATE\}\}/g, today).replace("{{DOMAIN}}", input.domain).replace("{{WIKI_LANG}}", canonicalLang);
|
|
932
|
-
await
|
|
991
|
+
await writeFile4(join7(target, "log.md"), log, "utf8");
|
|
933
992
|
created.push("log.md");
|
|
934
993
|
} catch (e) {
|
|
935
994
|
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: "log.md", message: String(e) }) };
|
|
936
995
|
}
|
|
937
996
|
try {
|
|
938
|
-
await
|
|
997
|
+
await mkdir4(dirname5(envPath), { recursive: true });
|
|
939
998
|
const envBody = `WIKI_PATH=${target}
|
|
940
999
|
WIKI_LANG=${canonicalLang}
|
|
941
1000
|
`;
|
|
942
|
-
await
|
|
1001
|
+
await writeFile4(envPath, envBody, "utf8");
|
|
943
1002
|
} catch (e) {
|
|
944
1003
|
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: envPath, message: String(e) }) };
|
|
945
1004
|
}
|
|
@@ -1131,7 +1190,7 @@ async function runPagesize(input) {
|
|
|
1131
1190
|
}
|
|
1132
1191
|
|
|
1133
1192
|
// src/commands/log-rotate.ts
|
|
1134
|
-
import { readFile as readFile10, rename as rename2, writeFile as
|
|
1193
|
+
import { readFile as readFile10, rename as rename2, writeFile as writeFile5, stat as stat6 } from "fs/promises";
|
|
1135
1194
|
import { join as join11 } from "path";
|
|
1136
1195
|
var ENTRY_RE = /^## \[(\d{4})-\d{2}-\d{2}\]/gm;
|
|
1137
1196
|
async function runLogRotate(input) {
|
|
@@ -1172,7 +1231,7 @@ Chronological action log. Newest entries last. Skill writes append entries; lint
|
|
|
1172
1231
|
|
|
1173
1232
|
- Previous log moved to ${rotatedName}
|
|
1174
1233
|
`;
|
|
1175
|
-
await
|
|
1234
|
+
await writeFile5(logPath, fresh, "utf8");
|
|
1176
1235
|
} catch (e) {
|
|
1177
1236
|
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { message: String(e) }) };
|
|
1178
1237
|
}
|
|
@@ -1236,9 +1295,185 @@ async function runLint(input) {
|
|
|
1236
1295
|
};
|
|
1237
1296
|
}
|
|
1238
1297
|
|
|
1298
|
+
// src/commands/config.ts
|
|
1299
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
1300
|
+
import { existsSync } from "fs";
|
|
1301
|
+
import { join as join12 } from "path";
|
|
1302
|
+
function validateKey(key) {
|
|
1303
|
+
return CONFIG_KEYS.includes(key);
|
|
1304
|
+
}
|
|
1305
|
+
function configPath(home) {
|
|
1306
|
+
return join12(home, ".skillwiki", ".env");
|
|
1307
|
+
}
|
|
1308
|
+
async function runConfigGet(input) {
|
|
1309
|
+
if (!validateKey(input.key)) {
|
|
1310
|
+
return { exitCode: ExitCode.INVALID_CONFIG_KEY, result: err("INVALID_CONFIG_KEY", { key: input.key }) };
|
|
1311
|
+
}
|
|
1312
|
+
const map = await parseDotenvFile(configPath(input.home));
|
|
1313
|
+
const value = map[input.key] ?? "";
|
|
1314
|
+
return { exitCode: ExitCode.OK, result: ok({ key: input.key, value, humanHint: value }) };
|
|
1315
|
+
}
|
|
1316
|
+
async function runConfigSet(input) {
|
|
1317
|
+
if (!validateKey(input.key)) {
|
|
1318
|
+
return { exitCode: ExitCode.INVALID_CONFIG_KEY, result: err("INVALID_CONFIG_KEY", { key: input.key }) };
|
|
1319
|
+
}
|
|
1320
|
+
const filePath = configPath(input.home);
|
|
1321
|
+
try {
|
|
1322
|
+
let originalContent;
|
|
1323
|
+
try {
|
|
1324
|
+
originalContent = await readFile11(filePath, "utf8");
|
|
1325
|
+
} catch {
|
|
1326
|
+
}
|
|
1327
|
+
const existing = originalContent !== void 0 ? parseDotenvText(originalContent) : {};
|
|
1328
|
+
const merged = { ...existing, [input.key]: input.value };
|
|
1329
|
+
await writeDotenv(filePath, merged, originalContent);
|
|
1330
|
+
return { exitCode: ExitCode.OK, result: ok({ key: input.key, value: input.value, written: true, humanHint: `${input.key}=${input.value}` }) };
|
|
1331
|
+
} catch (e) {
|
|
1332
|
+
return { exitCode: ExitCode.CONFIG_WRITE_FAILED, result: err("CONFIG_WRITE_FAILED", { key: input.key, error: String(e) }) };
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
async function runConfigList(input) {
|
|
1336
|
+
const map = await parseDotenvFile(configPath(input.home));
|
|
1337
|
+
const entries = Object.entries(map).map(([key, value]) => ({ key, value: value ?? "" }));
|
|
1338
|
+
return { exitCode: ExitCode.OK, result: ok({ entries, humanHint: entries.map((e) => `${e.key}=${e.value}`).join("\n") }) };
|
|
1339
|
+
}
|
|
1340
|
+
async function runConfigPath(input) {
|
|
1341
|
+
const filePath = configPath(input.home);
|
|
1342
|
+
return { exitCode: ExitCode.OK, result: ok({ path: filePath, exists: existsSync(filePath), humanHint: filePath }) };
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
// src/commands/doctor.ts
|
|
1346
|
+
import { existsSync as existsSync2, readdirSync, statSync } from "fs";
|
|
1347
|
+
import { join as join13 } from "path";
|
|
1348
|
+
import { execSync } from "child_process";
|
|
1349
|
+
function check(status, id, label, detail) {
|
|
1350
|
+
return { id, label, status, detail };
|
|
1351
|
+
}
|
|
1352
|
+
function checkNodeVersion() {
|
|
1353
|
+
const major = parseInt(process.version.slice(1).split(".")[0], 10);
|
|
1354
|
+
if (major >= 20) {
|
|
1355
|
+
return check("pass", "node_version", "Node.js version", `v${major} >= 20`);
|
|
1356
|
+
}
|
|
1357
|
+
return check("error", "node_version", "Node.js version", `Node.js v${major} is below minimum v20`);
|
|
1358
|
+
}
|
|
1359
|
+
function checkCliOnPath(argv) {
|
|
1360
|
+
if (argv.length >= 2 && argv[1].endsWith("cli.js")) {
|
|
1361
|
+
return check("warn", "cli_on_path", "skillwiki on PATH", "Running via node cli.js (dev mode) \u2014 PATH check skipped");
|
|
1362
|
+
}
|
|
1363
|
+
if (argv.length >= 2 && argv[1] === "skillwiki") {
|
|
1364
|
+
return check("pass", "cli_on_path", "skillwiki on PATH", "Running as skillwiki \u2014 already on PATH");
|
|
1365
|
+
}
|
|
1366
|
+
try {
|
|
1367
|
+
execSync("which skillwiki 2>/dev/null", { encoding: "utf8" }).trim();
|
|
1368
|
+
return check("pass", "cli_on_path", "skillwiki on PATH", "skillwiki found on PATH");
|
|
1369
|
+
} catch {
|
|
1370
|
+
return check("warn", "cli_on_path", "skillwiki on PATH", "skillwiki not found on PATH");
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
async function checkConfigFile(home) {
|
|
1374
|
+
const cfgPath = configPath(home);
|
|
1375
|
+
if (!existsSync2(cfgPath)) {
|
|
1376
|
+
return check("warn", "config_file", "Config file exists", `${cfgPath} not found`);
|
|
1377
|
+
}
|
|
1378
|
+
try {
|
|
1379
|
+
const map = await parseDotenvFile(cfgPath);
|
|
1380
|
+
const keys = Object.keys(map);
|
|
1381
|
+
return check("pass", "config_file", "Config file exists", `Found with keys: ${keys.length > 0 ? keys.join(", ") : "(none set)"}`);
|
|
1382
|
+
} catch (e) {
|
|
1383
|
+
return check("warn", "config_file", "Config file exists", `Failed to parse ${cfgPath}: ${String(e)}`);
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
function checkWikiPathExists(resolvedPath) {
|
|
1387
|
+
if (resolvedPath === void 0) {
|
|
1388
|
+
return check("error", "wiki_path_exists", "Vault directory exists", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
1389
|
+
}
|
|
1390
|
+
if (existsSync2(resolvedPath) && statSync(resolvedPath).isDirectory()) {
|
|
1391
|
+
return check("pass", "wiki_path_exists", "Vault directory exists", resolvedPath);
|
|
1392
|
+
}
|
|
1393
|
+
return check("error", "wiki_path_exists", "Vault directory exists", `${resolvedPath} does not exist or is not a directory`);
|
|
1394
|
+
}
|
|
1395
|
+
function checkVaultStructure(resolvedPath) {
|
|
1396
|
+
if (resolvedPath === void 0) {
|
|
1397
|
+
return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
1398
|
+
}
|
|
1399
|
+
if (!existsSync2(resolvedPath)) {
|
|
1400
|
+
return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 vault directory does not exist");
|
|
1401
|
+
}
|
|
1402
|
+
const missing = [];
|
|
1403
|
+
if (!existsSync2(join13(resolvedPath, "SCHEMA.md"))) missing.push("SCHEMA.md");
|
|
1404
|
+
for (const dir of ["raw", "entities", "concepts", "meta"]) {
|
|
1405
|
+
if (!existsSync2(join13(resolvedPath, dir))) missing.push(dir + "/");
|
|
1406
|
+
}
|
|
1407
|
+
if (missing.length === 0) {
|
|
1408
|
+
return check("pass", "vault_structure", "Vault structure valid", "All required files and directories present");
|
|
1409
|
+
}
|
|
1410
|
+
return check("error", "vault_structure", "Vault structure valid", `Missing: ${missing.join(", ")}`);
|
|
1411
|
+
}
|
|
1412
|
+
function checkSkillsInstalled(home) {
|
|
1413
|
+
const skillsDir = join13(home, ".claude", "skills");
|
|
1414
|
+
if (!existsSync2(skillsDir)) {
|
|
1415
|
+
return check("warn", "skills_installed", "Skills installed", `${skillsDir} not found`);
|
|
1416
|
+
}
|
|
1417
|
+
const found = findSkillMd(skillsDir);
|
|
1418
|
+
if (found.length > 0) {
|
|
1419
|
+
return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found`);
|
|
1420
|
+
}
|
|
1421
|
+
return check("warn", "skills_installed", "Skills installed", "No SKILL.md files found in ~/.claude/skills/");
|
|
1422
|
+
}
|
|
1423
|
+
function findSkillMd(dir) {
|
|
1424
|
+
const results = [];
|
|
1425
|
+
let entries;
|
|
1426
|
+
try {
|
|
1427
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
1428
|
+
} catch {
|
|
1429
|
+
return results;
|
|
1430
|
+
}
|
|
1431
|
+
for (const entry of entries) {
|
|
1432
|
+
if (entry.isFile() && entry.name === "SKILL.md") {
|
|
1433
|
+
results.push(join13(dir, entry.name));
|
|
1434
|
+
} else if (entry.isDirectory()) {
|
|
1435
|
+
results.push(...findSkillMd(join13(dir, entry.name)));
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
return results;
|
|
1439
|
+
}
|
|
1440
|
+
async function runDoctor(input) {
|
|
1441
|
+
const checks = [];
|
|
1442
|
+
checks.push(checkNodeVersion());
|
|
1443
|
+
checks.push(checkCliOnPath(input.argv));
|
|
1444
|
+
checks.push(await checkConfigFile(input.home));
|
|
1445
|
+
const resolved = await resolveRuntimePath({ flag: void 0, envValue: input.envValue, home: input.home });
|
|
1446
|
+
if (resolved.ok) {
|
|
1447
|
+
checks.push(check("pass", "wiki_path_set", "WIKI_PATH configured", `Resolved via ${resolved.data.source}: ${resolved.data.path}`));
|
|
1448
|
+
} else {
|
|
1449
|
+
checks.push(check("error", "wiki_path_set", "WIKI_PATH configured", "No vault configured. Run `skillwiki init` or pass --vault."));
|
|
1450
|
+
}
|
|
1451
|
+
const resolvedPath = resolved.ok ? resolved.data.path : void 0;
|
|
1452
|
+
checks.push(checkWikiPathExists(resolvedPath));
|
|
1453
|
+
checks.push(checkVaultStructure(resolvedPath));
|
|
1454
|
+
checks.push(checkSkillsInstalled(input.home));
|
|
1455
|
+
const summary = {
|
|
1456
|
+
pass: checks.filter((c) => c.status === "pass").length,
|
|
1457
|
+
warn: checks.filter((c) => c.status === "warn").length,
|
|
1458
|
+
error: checks.filter((c) => c.status === "error").length
|
|
1459
|
+
};
|
|
1460
|
+
const exitCode = summary.error > 0 ? ExitCode.DOCTOR_HAS_ERRORS : summary.warn > 0 ? ExitCode.DOCTOR_HAS_WARNINGS : ExitCode.OK;
|
|
1461
|
+
const statusIcon = { pass: "\u2713", warn: "\u26A0", error: "\u2717" };
|
|
1462
|
+
const lines = checks.map((c) => {
|
|
1463
|
+
const icon = statusIcon[c.status];
|
|
1464
|
+
const padded = c.label.padEnd(24);
|
|
1465
|
+
return ` ${icon} ${padded} ${c.detail}`;
|
|
1466
|
+
});
|
|
1467
|
+
lines.push("");
|
|
1468
|
+
lines.push(`${summary.pass} pass \xB7 ${summary.warn} warn \xB7 ${summary.error} error`);
|
|
1469
|
+
const humanHint = lines.join("\n");
|
|
1470
|
+
return { exitCode, result: ok({ checks, summary, humanHint }) };
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1239
1473
|
// src/cli.ts
|
|
1474
|
+
var pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
1240
1475
|
var program = new Command();
|
|
1241
|
-
program.name("skillwiki").description("Deterministic helpers for CodeWiki skills").version(
|
|
1476
|
+
program.name("skillwiki").description("Deterministic helpers for CodeWiki skills").version(pkg.version);
|
|
1242
1477
|
program.option("--human", "render terminal-readable output instead of JSON");
|
|
1243
1478
|
function emit(r) {
|
|
1244
1479
|
if (program.opts().human) printHuman(r.result);
|
|
@@ -1344,6 +1579,16 @@ program.command("lint [vault]").option("--days <n>", "stale threshold", (s) => p
|
|
|
1344
1579
|
logThreshold: opts.logThreshold
|
|
1345
1580
|
}));
|
|
1346
1581
|
});
|
|
1582
|
+
var configCmd = program.command("config").description("manage skillwiki configuration");
|
|
1583
|
+
configCmd.command("get <key>").description("print the value of a config key").action(async (key) => emit(await runConfigGet({ key, home: process.env.HOME ?? "" })));
|
|
1584
|
+
configCmd.command("set <key> <value>").description("set a config key value").action(async (key, value) => emit(await runConfigSet({ key, value, home: process.env.HOME ?? "" })));
|
|
1585
|
+
configCmd.command("list").description("list all config key=value pairs").action(async () => emit(await runConfigList({ home: process.env.HOME ?? "" })));
|
|
1586
|
+
configCmd.command("path").description("print the config file path").action(async () => emit(await runConfigPath({ home: process.env.HOME ?? "" })));
|
|
1587
|
+
program.command("doctor").description("diagnose skillwiki setup issues").action(async () => emit(await runDoctor({
|
|
1588
|
+
home: process.env.HOME ?? "",
|
|
1589
|
+
envValue: process.env.WIKI_PATH,
|
|
1590
|
+
argv: process.argv
|
|
1591
|
+
})));
|
|
1347
1592
|
program.parseAsync(process.argv).catch((e) => {
|
|
1348
1593
|
process.stdout.write(JSON.stringify({ ok: false, error: "INTERNAL", detail: { message: String(e) } }) + "\n");
|
|
1349
1594
|
process.exit(1);
|
package/package.json
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skillwiki",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.6",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"bin": {
|
|
6
|
-
|
|
5
|
+
"bin": {
|
|
6
|
+
"skillwiki": "dist/cli.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"templates",
|
|
11
|
+
"skills",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
7
14
|
"scripts": {
|
|
8
15
|
"build": "tsup && rm -rf ./skills && cp -r ../skills ./skills",
|
|
9
16
|
"test": "vitest run",
|
|
@@ -23,5 +30,7 @@
|
|
|
23
30
|
"typescript": "^5.7.0",
|
|
24
31
|
"vitest": "^2.1.0"
|
|
25
32
|
},
|
|
26
|
-
"engines": {
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=20"
|
|
35
|
+
}
|
|
27
36
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skillwiki",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
3
|
+
"version": "0.2.0-beta.6",
|
|
4
|
+
"skills": "./",
|
|
5
|
+
"description": "Project-aware Karpathy-style knowledge base for Claude Code: 11 prompt-only skills (wiki-*, proj-*, using-skillwiki) backed by the deterministic `skillwiki` CLI (8 subcommands, JSON-by-default).",
|
|
5
6
|
"author": {
|
|
6
7
|
"name": "karlorz",
|
|
7
8
|
"url": "https://github.com/karlorz"
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
: << 'CMDBLOCK'
|
|
2
|
+
@echo off
|
|
3
|
+
REM Cross-platform polyglot wrapper for hook scripts.
|
|
4
|
+
REM On Windows: cmd.exe runs the batch portion, which finds and calls bash.
|
|
5
|
+
REM On Unix: the shell interprets this as a script (: is a no-op in bash).
|
|
6
|
+
REM
|
|
7
|
+
REM Hook scripts use extensionless filenames (e.g. "session-start" not
|
|
8
|
+
REM "session-start.sh") so Claude Code's Windows auto-detection -- which
|
|
9
|
+
REM prepends "bash" to any command containing .sh -- doesn't interfere.
|
|
10
|
+
|
|
11
|
+
if "%~1"=="" (
|
|
12
|
+
echo run-hook.cmd: missing script name >&2
|
|
13
|
+
exit /b 1
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
set "HOOK_DIR=%~dp0"
|
|
17
|
+
|
|
18
|
+
REM Try Git for Windows bash in standard locations
|
|
19
|
+
if exist "C:\Program Files\Git\bin\bash.exe" (
|
|
20
|
+
"C:\Program Files\Git\bin\bash.exe" "%HOOK_DIR%%~1" %2 %3 %4 %5 %6 %7 %8 %9
|
|
21
|
+
exit /b %ERRORLEVEL%
|
|
22
|
+
)
|
|
23
|
+
if exist "C:\Program Files (x86)\Git\bin\bash.exe" (
|
|
24
|
+
"C:\Program Files (x86)\Git\bin\bash.exe" "%HOOK_DIR%%~1" %2 %3 %4 %5 %6 %7 %8 %9
|
|
25
|
+
exit /b %ERRORLEVEL%
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
REM Try bash on PATH
|
|
29
|
+
where bash >nul 2>nul
|
|
30
|
+
if %ERRORLEVEL% equ 0 (
|
|
31
|
+
bash "%HOOK_DIR%%~1" %2 %3 %4 %5 %6 %7 %8 %9
|
|
32
|
+
exit /b %ERRORLEVEL%
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
REM No bash found - exit silently
|
|
36
|
+
exit /b 0
|
|
37
|
+
CMDBLOCK
|
|
38
|
+
|
|
39
|
+
# Unix: run the named script directly
|
|
40
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
41
|
+
SCRIPT_NAME="$1"
|
|
42
|
+
shift
|
|
43
|
+
exec bash "${SCRIPT_DIR}/${SCRIPT_NAME}" "$@"
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# SessionStart hook for skillwiki plugin
|
|
3
|
+
# Injects using-skillwiki SKILL.md content into every conversation.
|
|
4
|
+
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
|
|
8
|
+
|
|
9
|
+
skill_content=$(cat "${PLUGIN_ROOT}/using-skillwiki/SKILL.md" 2>/dev/null || echo "Error reading using-skillwiki skill")
|
|
10
|
+
|
|
11
|
+
# Escape string for JSON embedding using bash parameter substitution.
|
|
12
|
+
# Each ${s//old/new} is a single C-level pass.
|
|
13
|
+
escape_for_json() {
|
|
14
|
+
local s="$1"
|
|
15
|
+
s="${s//\\/\\\\}"
|
|
16
|
+
s="${s//\"/\\\"}"
|
|
17
|
+
s="${s//$'\n'/\\n}"
|
|
18
|
+
s="${s//$'\r'/\\r}"
|
|
19
|
+
s="${s//$'\t'/\\t}"
|
|
20
|
+
printf '%s' "$s"
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
skill_escaped=$(escape_for_json "$skill_content")
|
|
24
|
+
session_context="<EXTREMELY_IMPORTANT>\nYou have skillwiki.\n\n**Below is the full content of your 'skillwiki:using-skillwiki' skill - your introduction to the skillwiki skills. For all other skills, use the 'Skill' tool:**\n\n${skill_escaped}\n</EXTREMELY_IMPORTANT>"
|
|
25
|
+
|
|
26
|
+
# Uses printf instead of heredoc to work around bash 5.3+ heredoc hang.
|
|
27
|
+
printf '{\n "hookSpecificOutput": {\n "hookEventName": "SessionStart",\n "additionalContext": "%s"\n }\n}\n' "$session_context"
|
|
28
|
+
|
|
29
|
+
exit 0
|
package/skills/package.json
CHANGED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: using-skillwiki
|
|
3
|
+
description: Invoke at session start or when knowledge-base tasks arise — maps all wiki-*/proj-* skills and teaches the skillwiki CLI workflow
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<SUBAGENT-STOP>
|
|
7
|
+
If you were dispatched as a subagent to execute a specific task, skip this skill.
|
|
8
|
+
</SUBAGENT-STOP>
|
|
9
|
+
|
|
10
|
+
# using-skillwiki
|
|
11
|
+
|
|
12
|
+
You have skillwiki — a project-aware Karpathy-style knowledge base for Claude Code.
|
|
13
|
+
|
|
14
|
+
## When to Use These Skills
|
|
15
|
+
|
|
16
|
+
Invoke a skillwiki skill when the user:
|
|
17
|
+
- Wants to create, build, or start a vault/wiki/knowledge base
|
|
18
|
+
- Mentions ingesting sources, reading URLs into notes, converting content
|
|
19
|
+
- Asks to search, query, or find information in their vault
|
|
20
|
+
- Wants a health check or lint on their vault
|
|
21
|
+
- Mentions crystallizing a session into a note
|
|
22
|
+
- Talks about project workspaces, ADRs, or distillation
|
|
23
|
+
- Asks about their skillwiki configuration or setup health
|
|
24
|
+
|
|
25
|
+
## Skill Map
|
|
26
|
+
|
|
27
|
+
| Skill | When to Invoke |
|
|
28
|
+
|-------|----------------|
|
|
29
|
+
| `wiki-init` | Bootstrap a new vault — SCHEMA.md, index.md, log.md, ~/.skillwiki/.env |
|
|
30
|
+
| `wiki-ingest` | Convert URLs, files, or pasted text into typed-knowledge pages |
|
|
31
|
+
| `wiki-query` | Search the vault and synthesize an answer with ranked results |
|
|
32
|
+
| `wiki-lint` | Vault health check (stale pages, oversized pages, log rotation) |
|
|
33
|
+
| `wiki-crystallize` | Distill the current working session into a typed-knowledge page |
|
|
34
|
+
| `wiki-audit` | Verify raw provenance references and source frontmatter integrity |
|
|
35
|
+
| `proj-init` | Bootstrap a project workspace (README, requirements, architecture) |
|
|
36
|
+
| `proj-work` | Open or run a work item under a project's work/ directory |
|
|
37
|
+
| `proj-distill` | Distill project compound entries into vault concept pages |
|
|
38
|
+
| `proj-decide` | Write an Architectural Decision Record (ADR) |
|
|
39
|
+
|
|
40
|
+
## CLI Backbone
|
|
41
|
+
|
|
42
|
+
All skills are backed by the `skillwiki` CLI — a deterministic tool with no LLM calls. It handles path resolution, config management, validation, and linting. Skills invoke it via Bash for the mechanical parts and use Claude for the creative parts.
|
|
43
|
+
|
|
44
|
+
Key CLI subcommands: `init`, `lint`, `config`, `doctor`, `path`, `lang`, `install`, `graph build`.
|
|
45
|
+
|
|
46
|
+
Run `skillwiki doctor` to diagnose setup issues. Run `skillwiki config list` to see current configuration.
|
|
47
|
+
|
|
48
|
+
## Typical Workflow
|
|
49
|
+
|
|
50
|
+
1. **Init** (`wiki-init`) — create vault, set domain and taxonomy
|
|
51
|
+
2. **Ingest** (`wiki-ingest`) — add sources, build pages
|
|
52
|
+
3. **Query** (`wiki-query`) — search and synthesize answers
|
|
53
|
+
4. **Lint** (`wiki-lint`) — periodic health checks
|
|
54
|
+
5. **Crystallize** (`wiki-crystallize`) — save session insights as pages
|
|
55
|
+
6. **Audit** (`wiki-audit`) — verify source integrity
|
|
56
|
+
|
|
57
|
+
For longer-running project work, use `proj-init` → `proj-work` → `proj-distill` / `proj-decide`.
|