skillwiki 0.2.0-beta.4 → 0.2.0-beta.7

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
@@ -70,8 +70,8 @@ var ExitCode = {
70
70
  function ok(data) {
71
71
  return { ok: true, data };
72
72
  }
73
- function err(error2, detail) {
74
- return detail === void 0 ? { ok: false, error: error2 } : { ok: false, error: error2, detail };
73
+ function err(error, detail) {
74
+ return detail === void 0 ? { ok: false, error } : { ok: false, error, detail };
75
75
  }
76
76
 
77
77
  // ../shared/src/schemas.ts
@@ -491,14 +491,9 @@ import { join as join2 } from "path";
491
491
  // src/utils/dotenv.ts
492
492
  import { readFile as readFile4, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
493
493
  import { dirname as dirname2 } from "path";
494
- var WHITELIST = /* @__PURE__ */ new Set(["WIKI_PATH", "WIKI_LANG"]);
495
- async function parseDotenvFile(path) {
496
- let text;
497
- try {
498
- text = await readFile4(path, "utf8");
499
- } catch {
500
- return {};
501
- }
494
+ var CONFIG_KEYS = ["WIKI_PATH", "WIKI_LANG"];
495
+ var _whitelist = new Set(CONFIG_KEYS);
496
+ function parseDotenvText(text) {
502
497
  const out = {};
503
498
  for (const rawLine of text.split(/\r?\n/)) {
504
499
  const line = rawLine.trim();
@@ -507,12 +502,21 @@ async function parseDotenvFile(path) {
507
502
  if (eq <= 0) continue;
508
503
  const key = line.slice(0, eq).trim();
509
504
  const value = line.slice(eq + 1).trim();
510
- if (!WHITELIST.has(key)) continue;
505
+ if (!_whitelist.has(key)) continue;
511
506
  if (value.length === 0) continue;
512
507
  out[key] = value;
513
508
  }
514
509
  return out;
515
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
+ }
516
520
  async function writeDotenv(filePath, entries, originalContent) {
517
521
  const lines = originalContent !== void 0 ? updateLines(originalContent, entries) : freshLines(entries);
518
522
  await mkdir2(dirname2(filePath), { recursive: true });
@@ -894,8 +898,35 @@ async function runLang(input) {
894
898
  }
895
899
 
896
900
  // src/commands/init.ts
897
- import { mkdir as mkdir4, readFile as readFile6, stat as stat5, writeFile as writeFile4 } from "fs/promises";
898
- import { join as join7, dirname as dirname5 } from "path";
901
+ import { mkdir as mkdir4, readFile as readFile6, readdir as readdir3, writeFile as writeFile4 } from "fs/promises";
902
+ import { join as join7 } from "path";
903
+
904
+ // src/parsers/taxonomy.ts
905
+ import yaml2 from "js-yaml";
906
+ var FENCE_RE = /^##\s+Tag Taxonomy\s*$[\s\S]*?```yaml\s*\n([\s\S]*?)\n```/m;
907
+ function extractTaxonomy(schemaText) {
908
+ const m = schemaText.match(FENCE_RE);
909
+ if (!m) return err("NO_TAXONOMY_BLOCK", { message: "No fenced YAML taxonomy block found in SCHEMA.md" });
910
+ let parsed;
911
+ try {
912
+ parsed = yaml2.load(m[1], { schema: yaml2.JSON_SCHEMA });
913
+ } catch (e) {
914
+ return err("INVALID_FRONTMATTER", { message: e.message });
915
+ }
916
+ if (parsed === null || typeof parsed !== "object") {
917
+ return err("INVALID_FRONTMATTER", { message: "taxonomy block is not an object" });
918
+ }
919
+ const tax = parsed.taxonomy;
920
+ if (!Array.isArray(tax)) {
921
+ return err("INVALID_FRONTMATTER", { message: "taxonomy key missing or not an array" });
922
+ }
923
+ if (!tax.every((x) => typeof x === "string")) {
924
+ return err("INVALID_FRONTMATTER", { message: "taxonomy must be a list of strings" });
925
+ }
926
+ return ok(tax);
927
+ }
928
+
929
+ // src/commands/init.ts
899
930
  var DEFAULT_TAXONOMY = [
900
931
  "research",
901
932
  "comparison",
@@ -920,25 +951,59 @@ var VAULT_DIRS = [
920
951
  "meta",
921
952
  "projects"
922
953
  ];
954
+ function extractDomainFromSchema(text) {
955
+ const m = text.match(/^##\s+Domain\s*\n([\s\S]*?)(?=\n\n|\n##|\s*$)/m);
956
+ if (!m) return "";
957
+ const d = m[1].trim();
958
+ return d.startsWith("##") ? "" : d;
959
+ }
960
+ async function discoverTagsFromPages(target, knownSlugs) {
961
+ const knownSet = new Set(knownSlugs);
962
+ const discovered = /* @__PURE__ */ new Set();
963
+ for (const dir of ["entities", "concepts", "comparisons", "queries"]) {
964
+ let entries;
965
+ try {
966
+ entries = (await readdir3(join7(target, dir), { withFileTypes: true })).filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => e.name);
967
+ } catch {
968
+ continue;
969
+ }
970
+ for (const file of entries) {
971
+ try {
972
+ const text = await readFile6(join7(target, dir, file), "utf8");
973
+ const fm = extractFrontmatter(text);
974
+ if (!fm.ok || !fm.data.tags || !Array.isArray(fm.data.tags)) continue;
975
+ for (const t of fm.data.tags) {
976
+ if (typeof t === "string" && !knownSet.has(t)) discovered.add(t);
977
+ }
978
+ } catch {
979
+ }
980
+ }
981
+ }
982
+ return [...discovered].sort();
983
+ }
923
984
  async function runInit(input) {
924
985
  const pathRes = await resolveInitTimePath({ flag: input.flag, envValue: input.envValue, home: input.home });
925
986
  const target = pathRes.path;
926
987
  const langRes = await resolveLang({ flag: input.lang, envValue: void 0, home: input.home });
927
988
  const canonicalLang = langRes.canonical;
928
- let hasSchema = false;
989
+ let oldSchemaText;
929
990
  try {
930
- await stat5(join7(target, "SCHEMA.md"));
931
- hasSchema = true;
991
+ oldSchemaText = await readFile6(join7(target, "SCHEMA.md"), "utf8");
932
992
  } catch {
933
993
  }
934
- if (hasSchema && !input.force) {
994
+ if (oldSchemaText && !input.force) {
935
995
  return {
936
996
  exitCode: ExitCode.INIT_TARGET_NOT_EMPTY,
937
997
  result: err("INIT_TARGET_NOT_EMPTY", { target })
938
998
  };
939
999
  }
940
1000
  const envPath = join7(input.home, ".skillwiki", ".env");
941
- const existingEnv = await parseDotenvFile(envPath);
1001
+ let existingEnvRaw = "";
1002
+ try {
1003
+ existingEnvRaw = await readFile6(envPath, "utf8");
1004
+ } catch {
1005
+ }
1006
+ const existingEnv = parseDotenvText(existingEnvRaw);
942
1007
  const swDotenvHadPath = existingEnv.WIKI_PATH !== void 0;
943
1008
  if (existingEnv.WIKI_PATH !== void 0 && existingEnv.WIKI_PATH !== target && !input.force) {
944
1009
  return {
@@ -963,64 +1028,107 @@ async function runInit(input) {
963
1028
  return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { message: String(e) }) };
964
1029
  }
965
1030
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
966
- const taxonomy = input.taxonomy && input.taxonomy.length > 0 ? input.taxonomy : DEFAULT_TAXONOMY;
967
- const taxonomyYaml = taxonomy.map((t) => ` - ${t}`).join("\n");
1031
+ let taxonomy = input.taxonomy && input.taxonomy.length > 0 ? input.taxonomy : DEFAULT_TAXONOMY;
1032
+ let domain = input.domain;
1033
+ let oldTaxonomy = [];
1034
+ if (oldSchemaText) {
1035
+ if (!domain) {
1036
+ const oldDomain = extractDomainFromSchema(oldSchemaText);
1037
+ if (oldDomain) domain = oldDomain;
1038
+ }
1039
+ const oldTax = extractTaxonomy(oldSchemaText);
1040
+ if (oldTax.ok) oldTaxonomy = oldTax.data;
1041
+ }
1042
+ const taxonomySet = new Set(taxonomy);
1043
+ for (const t of oldTaxonomy) {
1044
+ if (!taxonomySet.has(t)) {
1045
+ taxonomy.push(t);
1046
+ taxonomySet.add(t);
1047
+ }
1048
+ }
1049
+ const discovered = await discoverTagsFromPages(target, taxonomy);
1050
+ const discovered_tags = discovered.length;
1051
+ const fullTaxonomyYaml = discovered.length > 0 ? taxonomy.map((t) => ` - ${t}`).join("\n") + "\n # --- Discovered from existing pages ---\n" + discovered.map((t) => ` - ${t}`).join("\n") : taxonomy.map((t) => ` - ${t}`).join("\n");
968
1052
  try {
969
1053
  const schemaTpl = await readFile6(join7(input.templates, "SCHEMA.md"), "utf8");
970
- const schema = schemaTpl.replace("{{DOMAIN}}", input.domain).replace("{{WIKI_LANG}}", canonicalLang).replace("{{TAXONOMY_YAML}}", taxonomyYaml);
1054
+ const schema = schemaTpl.replace("{{DOMAIN}}", domain).replace("{{WIKI_LANG}}", canonicalLang).replace("{{TAXONOMY_YAML}}", fullTaxonomyYaml);
971
1055
  await writeFile4(join7(target, "SCHEMA.md"), schema, "utf8");
972
1056
  created.push("SCHEMA.md");
973
1057
  } catch (e) {
974
1058
  return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: "SCHEMA.md", message: String(e) }) };
975
1059
  }
976
- try {
977
- const idxTpl = await readFile6(join7(input.templates, "index.md"), "utf8");
978
- const idx = idxTpl.replace("{{INIT_DATE}}", today);
979
- await writeFile4(join7(target, "index.md"), idx, "utf8");
980
- created.push("index.md");
981
- } catch (e) {
982
- return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: "index.md", message: String(e) }) };
983
- }
984
- try {
985
- const logTpl = await readFile6(join7(input.templates, "log.md"), "utf8");
986
- const log = logTpl.replace(/\{\{INIT_DATE\}\}/g, today).replace("{{DOMAIN}}", input.domain).replace("{{WIKI_LANG}}", canonicalLang);
987
- await writeFile4(join7(target, "log.md"), log, "utf8");
988
- created.push("log.md");
989
- } catch (e) {
990
- return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: "log.md", message: String(e) }) };
1060
+ const preserved = [];
1061
+ async function writeOrPreserve(fileName, render) {
1062
+ try {
1063
+ const existing = await readFile6(join7(target, fileName), "utf8");
1064
+ if (existing.split("\n").length > 10) {
1065
+ preserved.push(fileName);
1066
+ return void 0;
1067
+ }
1068
+ } catch {
1069
+ }
1070
+ try {
1071
+ await writeFile4(join7(target, fileName), await render(), "utf8");
1072
+ created.push(fileName);
1073
+ return void 0;
1074
+ } catch (e) {
1075
+ return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: fileName, message: String(e) }) };
1076
+ }
991
1077
  }
992
- try {
993
- await mkdir4(dirname5(envPath), { recursive: true });
994
- const envBody = `WIKI_PATH=${target}
995
- WIKI_LANG=${canonicalLang}
996
- `;
997
- await writeFile4(envPath, envBody, "utf8");
998
- } catch (e) {
999
- return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: envPath, message: String(e) }) };
1078
+ const err1 = await writeOrPreserve("index.md", async () => {
1079
+ const tpl = await readFile6(join7(input.templates, "index.md"), "utf8");
1080
+ return tpl.replace("{{INIT_DATE}}", today);
1081
+ });
1082
+ if (err1) return err1;
1083
+ const err2 = await writeOrPreserve("log.md", async () => {
1084
+ const tpl = await readFile6(join7(input.templates, "log.md"), "utf8");
1085
+ return tpl.replace(/\{\{INIT_DATE\}\}/g, today).replace("{{DOMAIN}}", domain).replace("{{WIKI_LANG}}", canonicalLang);
1086
+ });
1087
+ if (err2) return err2;
1088
+ const isTempPath = target.startsWith("/tmp/") || target === "/tmp" || target.startsWith("/var/tmp/") || target === "/var/tmp" || target.startsWith("/private/tmp/");
1089
+ const skipEnv = !!input.noEnv || isTempPath;
1090
+ let envWritten = "";
1091
+ if (!skipEnv) {
1092
+ try {
1093
+ await writeDotenv(envPath, { WIKI_PATH: target, WIKI_LANG: canonicalLang }, existingEnvRaw);
1094
+ envWritten = envPath;
1095
+ } catch (e) {
1096
+ return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: envPath, message: String(e) }) };
1097
+ }
1000
1098
  }
1001
1099
  const importedFromHermes = pathRes.source === "hermes-dotenv" && !swDotenvHadPath;
1002
1100
  return {
1003
1101
  exitCode: ExitCode.OK,
1004
1102
  result: ok({
1005
1103
  vault: target,
1006
- domain: input.domain,
1104
+ domain,
1007
1105
  taxonomy,
1008
1106
  lang: canonicalLang,
1009
1107
  created,
1010
- env_written: envPath,
1011
- imported_from_hermes: importedFromHermes
1108
+ preserved,
1109
+ env_written: envWritten,
1110
+ env_skipped: skipEnv,
1111
+ imported_from_hermes: importedFromHermes,
1112
+ discovered_tags
1012
1113
  })
1013
1114
  };
1014
1115
  }
1015
1116
 
1117
+ // src/utils/slug.ts
1118
+ function buildSlugMap(pages) {
1119
+ const map = /* @__PURE__ */ new Map();
1120
+ for (const p of pages) {
1121
+ const slug = p.relPath.replace(/\.md$/, "").split("/").pop();
1122
+ map.set(slug.toLowerCase(), slug);
1123
+ }
1124
+ return map;
1125
+ }
1126
+
1016
1127
  // src/commands/links.ts
1017
1128
  async function runLinks(input) {
1018
1129
  const scan = await scanVault(input.vault);
1019
1130
  if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
1020
- const slugs = /* @__PURE__ */ new Set();
1021
- for (const p of scan.data.typedKnowledge) {
1022
- slugs.add(p.relPath.replace(/\.md$/, "").split("/").pop());
1023
- }
1131
+ const slugs = buildSlugMap(scan.data.typedKnowledge);
1024
1132
  const broken = [];
1025
1133
  for (const p of scan.data.typedKnowledge) {
1026
1134
  const text = await readPage(p);
@@ -1029,7 +1137,7 @@ async function runLinks(input) {
1029
1137
  const lines = body.split("\n");
1030
1138
  for (const slug of extractBodyWikilinks(body)) {
1031
1139
  const tail = slug.split("/").pop();
1032
- if (!slugs.has(tail)) {
1140
+ if (!slugs.has(tail.toLowerCase())) {
1033
1141
  const line = lines.findIndex((l) => l.includes(`[[${slug}`));
1034
1142
  broken.push({ page: p.relPath, slug, line: line >= 0 ? line + 1 : 0 });
1035
1143
  }
@@ -1044,33 +1152,6 @@ async function runLinks(input) {
1044
1152
  // src/commands/tag-audit.ts
1045
1153
  import { readFile as readFile7 } from "fs/promises";
1046
1154
  import { join as join8 } from "path";
1047
-
1048
- // src/parsers/taxonomy.ts
1049
- import yaml2 from "js-yaml";
1050
- var FENCE_RE = /^##\s+Tag Taxonomy\s*$[\s\S]*?```yaml\s*\n([\s\S]*?)\n```/m;
1051
- function extractTaxonomy(schemaText) {
1052
- const m = schemaText.match(FENCE_RE);
1053
- if (!m) return ok([]);
1054
- let parsed;
1055
- try {
1056
- parsed = yaml2.load(m[1], { schema: yaml2.JSON_SCHEMA });
1057
- } catch (e) {
1058
- return err("INVALID_FRONTMATTER", { message: e.message });
1059
- }
1060
- if (parsed === null || typeof parsed !== "object") {
1061
- return err("INVALID_FRONTMATTER", { message: "taxonomy block is not an object" });
1062
- }
1063
- const tax = parsed.taxonomy;
1064
- if (!Array.isArray(tax)) {
1065
- return err("INVALID_FRONTMATTER", { message: "taxonomy key missing or not an array" });
1066
- }
1067
- if (!tax.every((x) => typeof x === "string")) {
1068
- return err("INVALID_FRONTMATTER", { message: "taxonomy must be a list of strings" });
1069
- }
1070
- return ok(tax);
1071
- }
1072
-
1073
- // src/commands/tag-audit.ts
1074
1155
  async function runTagAudit(input) {
1075
1156
  const scan = await scanVault(input.vault);
1076
1157
  if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
@@ -1108,7 +1189,11 @@ async function runIndexCheck(input) {
1108
1189
  indexText = await readFile8(join9(input.vault, "index.md"), "utf8");
1109
1190
  } catch {
1110
1191
  }
1111
- const indexSlugs = new Set(extractBodyWikilinks(indexText).map((s) => s.split("/").pop()));
1192
+ const indexSlugsLower = /* @__PURE__ */ new Map();
1193
+ for (const s of extractBodyWikilinks(indexText)) {
1194
+ const tail = s.split("/").pop();
1195
+ indexSlugsLower.set(tail.toLowerCase(), tail);
1196
+ }
1112
1197
  const fileSlugs = /* @__PURE__ */ new Map();
1113
1198
  for (const p of scan.data.typedKnowledge) {
1114
1199
  const slug = p.relPath.replace(/\.md$/, "").split("/").pop();
@@ -1116,11 +1201,12 @@ async function runIndexCheck(input) {
1116
1201
  }
1117
1202
  const missing_from_index = [];
1118
1203
  for (const [slug, relPath] of fileSlugs.entries()) {
1119
- if (!indexSlugs.has(slug)) missing_from_index.push(relPath);
1204
+ if (!indexSlugsLower.has(slug.toLowerCase())) missing_from_index.push(relPath);
1120
1205
  }
1206
+ const fileSlugsLower = new Set([...fileSlugs.keys()].map((s) => s.toLowerCase()));
1121
1207
  const ghost_entries = [];
1122
- for (const slug of indexSlugs) {
1123
- if (!fileSlugs.has(slug)) ghost_entries.push(slug);
1208
+ for (const [lower, orig] of indexSlugsLower) {
1209
+ if (!fileSlugsLower.has(lower)) ghost_entries.push(orig);
1124
1210
  }
1125
1211
  if (missing_from_index.length > 0 || ghost_entries.length > 0) {
1126
1212
  return { exitCode: ExitCode.INDEX_INCOMPLETE, result: ok({ missing_from_index, ghost_entries }) };
@@ -1186,12 +1272,12 @@ async function runPagesize(input) {
1186
1272
  }
1187
1273
 
1188
1274
  // src/commands/log-rotate.ts
1189
- import { readFile as readFile10, rename as rename2, writeFile as writeFile5, stat as stat6 } from "fs/promises";
1275
+ import { readFile as readFile10, rename as rename2, writeFile as writeFile5, stat as stat5 } from "fs/promises";
1190
1276
  import { join as join11 } from "path";
1191
1277
  var ENTRY_RE = /^## \[(\d{4})-\d{2}-\d{2}\]/gm;
1192
1278
  async function runLogRotate(input) {
1193
1279
  try {
1194
- await stat6(join11(input.vault, "SCHEMA.md"));
1280
+ await stat5(join11(input.vault, "SCHEMA.md"));
1195
1281
  } catch {
1196
1282
  return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
1197
1283
  }
@@ -1296,7 +1382,7 @@ import { readFile as readFile11 } from "fs/promises";
1296
1382
  import { existsSync } from "fs";
1297
1383
  import { join as join12 } from "path";
1298
1384
  function validateKey(key) {
1299
- return key === "WIKI_PATH" || key === "WIKI_LANG";
1385
+ return CONFIG_KEYS.includes(key);
1300
1386
  }
1301
1387
  function configPath(home) {
1302
1388
  return join12(home, ".skillwiki", ".env");
@@ -1320,7 +1406,7 @@ async function runConfigSet(input) {
1320
1406
  originalContent = await readFile11(filePath, "utf8");
1321
1407
  } catch {
1322
1408
  }
1323
- const existing = originalContent !== void 0 ? await parseDotenvFile(filePath) : {};
1409
+ const existing = originalContent !== void 0 ? parseDotenvText(originalContent) : {};
1324
1410
  const merged = { ...existing, [input.key]: input.value };
1325
1411
  await writeDotenv(filePath, merged, originalContent);
1326
1412
  return { exitCode: ExitCode.OK, result: ok({ key: input.key, value: input.value, written: true, humanHint: `${input.key}=${input.value}` }) };
@@ -1342,71 +1428,58 @@ async function runConfigPath(input) {
1342
1428
  import { existsSync as existsSync2, readdirSync, statSync } from "fs";
1343
1429
  import { join as join13 } from "path";
1344
1430
  import { execSync } from "child_process";
1345
- function pass(id, label, detail) {
1346
- return { id, label, status: "pass", detail };
1347
- }
1348
- function warn(id, label, detail) {
1349
- return { id, label, status: "warn", detail };
1350
- }
1351
- function error(id, label, detail) {
1352
- return { id, label, status: "error", detail };
1431
+ function check(status, id, label, detail) {
1432
+ return { id, label, status, detail };
1353
1433
  }
1354
1434
  function checkNodeVersion() {
1355
1435
  const major = parseInt(process.version.slice(1).split(".")[0], 10);
1356
1436
  if (major >= 20) {
1357
- return pass("node_version", "Node.js version", `v${major} >= 20`);
1437
+ return check("pass", "node_version", "Node.js version", `v${major} >= 20`);
1358
1438
  }
1359
- return error("node_version", "Node.js version", `Node.js v${major} is below minimum v20`);
1439
+ return check("error", "node_version", "Node.js version", `Node.js v${major} is below minimum v20`);
1360
1440
  }
1361
1441
  function checkCliOnPath(argv) {
1362
1442
  if (argv.length >= 2 && argv[1].endsWith("cli.js")) {
1363
- return warn("cli_on_path", "skillwiki on PATH", "Running via node cli.js (dev mode) \u2014 PATH check skipped");
1443
+ return check("warn", "cli_on_path", "skillwiki on PATH", "Running via node cli.js (dev mode) \u2014 PATH check skipped");
1364
1444
  }
1365
1445
  if (argv.length >= 2 && argv[1] === "skillwiki") {
1366
- return pass("cli_on_path", "skillwiki on PATH", "Running as skillwiki \u2014 already on PATH");
1446
+ return check("pass", "cli_on_path", "skillwiki on PATH", "Running as skillwiki \u2014 already on PATH");
1367
1447
  }
1368
1448
  try {
1369
1449
  execSync("which skillwiki 2>/dev/null", { encoding: "utf8" }).trim();
1370
- return pass("cli_on_path", "skillwiki on PATH", "skillwiki found on PATH");
1450
+ return check("pass", "cli_on_path", "skillwiki on PATH", "skillwiki found on PATH");
1371
1451
  } catch {
1372
- return warn("cli_on_path", "skillwiki on PATH", "skillwiki not found on PATH");
1452
+ return check("warn", "cli_on_path", "skillwiki on PATH", "skillwiki not found on PATH");
1373
1453
  }
1374
1454
  }
1375
1455
  async function checkConfigFile(home) {
1376
- const cfgPath = join13(home, ".skillwiki", ".env");
1456
+ const cfgPath = configPath(home);
1377
1457
  if (!existsSync2(cfgPath)) {
1378
- return warn("config_file", "Config file exists", `${cfgPath} not found`);
1458
+ return check("warn", "config_file", "Config file exists", `${cfgPath} not found`);
1379
1459
  }
1380
1460
  try {
1381
1461
  const map = await parseDotenvFile(cfgPath);
1382
1462
  const keys = Object.keys(map);
1383
- return pass("config_file", "Config file exists", `Found with keys: ${keys.length > 0 ? keys.join(", ") : "(none set)"}`);
1463
+ return check("pass", "config_file", "Config file exists", `Found with keys: ${keys.length > 0 ? keys.join(", ") : "(none set)"}`);
1384
1464
  } catch (e) {
1385
- return warn("config_file", "Config file exists", `Failed to parse ${cfgPath}: ${String(e)}`);
1465
+ return check("warn", "config_file", "Config file exists", `Failed to parse ${cfgPath}: ${String(e)}`);
1386
1466
  }
1387
1467
  }
1388
- async function checkWikiPathSet(input) {
1389
- const r = await resolveRuntimePath({ flag: void 0, envValue: input.envValue, home: input.home });
1390
- if (r.ok) {
1391
- return pass("wiki_path_set", "WIKI_PATH configured", `Resolved via ${r.data.source}: ${r.data.path}`);
1392
- }
1393
- return error("wiki_path_set", "WIKI_PATH configured", "No vault configured. Run `skillwiki init` or pass --vault.");
1394
- }
1395
1468
  function checkWikiPathExists(resolvedPath) {
1396
1469
  if (resolvedPath === void 0) {
1397
- return error("wiki_path_exists", "Vault directory exists", "Cannot check \u2014 WIKI_PATH not resolved");
1470
+ return check("error", "wiki_path_exists", "Vault directory exists", "Cannot check \u2014 WIKI_PATH not resolved");
1398
1471
  }
1399
1472
  if (existsSync2(resolvedPath) && statSync(resolvedPath).isDirectory()) {
1400
- return pass("wiki_path_exists", "Vault directory exists", resolvedPath);
1473
+ return check("pass", "wiki_path_exists", "Vault directory exists", resolvedPath);
1401
1474
  }
1402
- return error("wiki_path_exists", "Vault directory exists", `${resolvedPath} does not exist or is not a directory`);
1475
+ return check("error", "wiki_path_exists", "Vault directory exists", `${resolvedPath} does not exist or is not a directory`);
1403
1476
  }
1404
1477
  function checkVaultStructure(resolvedPath) {
1405
1478
  if (resolvedPath === void 0) {
1406
- return error("vault_structure", "Vault structure valid", "Cannot check \u2014 WIKI_PATH not resolved");
1479
+ return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 WIKI_PATH not resolved");
1407
1480
  }
1408
1481
  if (!existsSync2(resolvedPath)) {
1409
- return error("vault_structure", "Vault structure valid", "Cannot check \u2014 vault directory does not exist");
1482
+ return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 vault directory does not exist");
1410
1483
  }
1411
1484
  const missing = [];
1412
1485
  if (!existsSync2(join13(resolvedPath, "SCHEMA.md"))) missing.push("SCHEMA.md");
@@ -1414,20 +1487,20 @@ function checkVaultStructure(resolvedPath) {
1414
1487
  if (!existsSync2(join13(resolvedPath, dir))) missing.push(dir + "/");
1415
1488
  }
1416
1489
  if (missing.length === 0) {
1417
- return pass("vault_structure", "Vault structure valid", "All required files and directories present");
1490
+ return check("pass", "vault_structure", "Vault structure valid", "All required files and directories present");
1418
1491
  }
1419
- return error("vault_structure", "Vault structure valid", `Missing: ${missing.join(", ")}`);
1492
+ return check("error", "vault_structure", "Vault structure valid", `Missing: ${missing.join(", ")}`);
1420
1493
  }
1421
1494
  function checkSkillsInstalled(home) {
1422
1495
  const skillsDir = join13(home, ".claude", "skills");
1423
1496
  if (!existsSync2(skillsDir)) {
1424
- return warn("skills_installed", "Skills installed", `${skillsDir} not found`);
1497
+ return check("warn", "skills_installed", "Skills installed", `${skillsDir} not found`);
1425
1498
  }
1426
1499
  const found = findSkillMd(skillsDir);
1427
1500
  if (found.length > 0) {
1428
- return pass("skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found`);
1501
+ return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found`);
1429
1502
  }
1430
- return warn("skills_installed", "Skills installed", "No SKILL.md files found in ~/.claude/skills/");
1503
+ return check("warn", "skills_installed", "Skills installed", "No SKILL.md files found in ~/.claude/skills/");
1431
1504
  }
1432
1505
  function findSkillMd(dir) {
1433
1506
  const results = [];
@@ -1451,9 +1524,12 @@ async function runDoctor(input) {
1451
1524
  checks.push(checkNodeVersion());
1452
1525
  checks.push(checkCliOnPath(input.argv));
1453
1526
  checks.push(await checkConfigFile(input.home));
1454
- const wikiPathCheck = await checkWikiPathSet(input);
1455
- checks.push(wikiPathCheck);
1456
1527
  const resolved = await resolveRuntimePath({ flag: void 0, envValue: input.envValue, home: input.home });
1528
+ if (resolved.ok) {
1529
+ checks.push(check("pass", "wiki_path_set", "WIKI_PATH configured", `Resolved via ${resolved.data.source}: ${resolved.data.path}`));
1530
+ } else {
1531
+ checks.push(check("error", "wiki_path_set", "WIKI_PATH configured", "No vault configured. Run `skillwiki init` or pass --vault."));
1532
+ }
1457
1533
  const resolvedPath = resolved.ok ? resolved.data.path : void 0;
1458
1534
  checks.push(checkWikiPathExists(resolvedPath));
1459
1535
  checks.push(checkVaultStructure(resolvedPath));
@@ -1520,7 +1596,7 @@ program.command("lang").option("--lang <code>", "explicit language override").op
1520
1596
  explain: !!opts.explain
1521
1597
  }));
1522
1598
  });
1523
- program.command("init").option("--target <dir>", "explicit target directory").requiredOption("--domain <text>", "knowledge domain seed").option("--taxonomy <csv>", "comma-separated tag list").option("--lang <code>", "output language (BCP 47 or alias)").option("--force", "override existing target / env conflict", false).action(async (opts) => {
1599
+ program.command("init").option("--target <dir>", "explicit target directory").requiredOption("--domain <text>", "knowledge domain seed").option("--taxonomy <csv>", "comma-separated tag list").option("--lang <code>", "output language (BCP 47 or alias)").option("--force", "override existing target / env conflict", false).option("--no-env", "skip writing ~/.skillwiki/.env").action(async (opts) => {
1524
1600
  const templates = new URL("../templates/", import.meta.url).pathname;
1525
1601
  const taxonomy = typeof opts.taxonomy === "string" ? opts.taxonomy.split(",").map((s) => s.trim()).filter((s) => s.length > 0) : void 0;
1526
1602
  emit(await runInit({
@@ -1531,7 +1607,8 @@ program.command("init").option("--target <dir>", "explicit target directory").re
1531
1607
  domain: opts.domain,
1532
1608
  taxonomy,
1533
1609
  lang: opts.lang,
1534
- force: !!opts.force
1610
+ force: !!opts.force,
1611
+ noEnv: opts.env === false
1535
1612
  }));
1536
1613
  });
1537
1614
  async function resolveVaultArg(arg) {
@@ -1593,7 +1670,6 @@ configCmd.command("path").description("print the config file path").action(async
1593
1670
  program.command("doctor").description("diagnose skillwiki setup issues").action(async () => emit(await runDoctor({
1594
1671
  home: process.env.HOME ?? "",
1595
1672
  envValue: process.env.WIKI_PATH,
1596
- envLang: process.env.WIKI_LANG,
1597
1673
  argv: process.argv
1598
1674
  })));
1599
1675
  program.parseAsync(process.argv).catch((e) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.2.0-beta.4",
3
+ "version": "0.2.0-beta.7",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "skillwiki": "dist/cli.js"
@@ -30,6 +30,10 @@
30
30
  "typescript": "^5.7.0",
31
31
  "vitest": "^2.1.0"
32
32
  },
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "https://github.com/karlorz/llm-wiki.git"
36
+ },
33
37
  "engines": {
34
38
  "node": ">=20"
35
39
  }
@@ -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.7",
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,13 @@
1
1
  {
2
2
  "name": "@skillwiki/skills",
3
- "version": "0.1.0",
3
+ "version": "0.2.0-beta.7",
4
4
  "private": true,
5
- "files": ["wiki-*", "proj-*", ".claude-plugin", "README.md"]
5
+ "files": [
6
+ "wiki-*",
7
+ "proj-*",
8
+ "using-skillwiki",
9
+ ".claude-plugin",
10
+ "hooks",
11
+ "README.md"
12
+ ]
6
13
  }
@@ -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`.