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 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
- process.stdout.write(`OK
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
- var WHITELIST = /* @__PURE__ */ new Set(["WIKI_PATH", "WIKI_LANG"]);
484
- async function parseDotenvFile(path) {
485
- let text;
486
- try {
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 (!WHITELIST.has(key)) continue;
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 dirname2, resolve, join as join3 } from "path";
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(dirname2(resolve(input.file)));
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 = dirname2(cur);
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 mkdir2, rename, writeFile as writeFile2, stat as stat3 } from "fs/promises";
704
- import { dirname as dirname3 } from "path";
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 mkdir2(dirname3(dst), { recursive: true });
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 mkdir2(dirname3(path), { recursive: true });
783
+ await mkdir3(dirname4(path), { recursive: true });
725
784
  const enriched = { installed_at: (/* @__PURE__ */ new Date()).toISOString(), ...m };
726
- await writeFile2(path, JSON.stringify(enriched, null, 2));
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 mkdir3, readFile as readFile6, stat as stat5, writeFile as writeFile3 } from "fs/promises";
843
- import { join as join7, dirname as dirname4 } from "path";
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 mkdir3(target, { recursive: true });
961
+ await mkdir4(target, { recursive: true });
903
962
  for (const d of VAULT_DIRS) {
904
- await mkdir3(join7(target, d), { recursive: true });
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 writeFile3(join7(target, "SCHEMA.md"), schema, "utf8");
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 writeFile3(join7(target, "index.md"), idx, "utf8");
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 writeFile3(join7(target, "log.md"), log, "utf8");
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 mkdir3(dirname4(envPath), { recursive: true });
997
+ await mkdir4(dirname5(envPath), { recursive: true });
939
998
  const envBody = `WIKI_PATH=${target}
940
999
  WIKI_LANG=${canonicalLang}
941
1000
  `;
942
- await writeFile3(envPath, envBody, "utf8");
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 writeFile4, stat as stat6 } from "fs/promises";
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 writeFile4(logPath, fresh, "utf8");
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("0.2.0-beta.3");
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",
3
+ "version": "0.2.0-beta.6",
4
4
  "type": "module",
5
- "bin": { "skillwiki": "dist/cli.js" },
6
- "files": ["dist", "templates", "skills", "README.md"],
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": { "node": ">=20" }
33
+ "engines": {
34
+ "node": ">=20"
35
+ }
27
36
  }
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.1.0",
4
- "description": "Project-aware Karpathy-style knowledge base for Claude Code: 10 prompt-only skills (wiki-* + proj-*) backed by the deterministic `skillwiki` CLI (8 subcommands, JSON-by-default).",
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,16 @@
1
+ {
2
+ "hooks": {
3
+ "SessionStart": [
4
+ {
5
+ "matcher": "startup|clear|compact",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cmd\" session-start",
10
+ "async": false
11
+ }
12
+ ]
13
+ }
14
+ ]
15
+ }
16
+ }
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skillwiki/skills",
3
- "version": "0.1.0",
3
+ "version": "0.2.0-beta.6",
4
4
  "private": true,
5
- "files": ["wiki-*", "proj-*", ".claude-plugin", "README.md"]
5
+ "files": ["wiki-*", "proj-*", "using-skillwiki", ".claude-plugin", "hooks", "README.md"]
6
6
  }
@@ -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`.