skillwiki 0.2.0-beta.6 → 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
@@ -898,8 +898,35 @@ async function runLang(input) {
898
898
  }
899
899
 
900
900
  // src/commands/init.ts
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";
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
903
930
  var DEFAULT_TAXONOMY = [
904
931
  "research",
905
932
  "comparison",
@@ -924,25 +951,59 @@ var VAULT_DIRS = [
924
951
  "meta",
925
952
  "projects"
926
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
+ }
927
984
  async function runInit(input) {
928
985
  const pathRes = await resolveInitTimePath({ flag: input.flag, envValue: input.envValue, home: input.home });
929
986
  const target = pathRes.path;
930
987
  const langRes = await resolveLang({ flag: input.lang, envValue: void 0, home: input.home });
931
988
  const canonicalLang = langRes.canonical;
932
- let hasSchema = false;
989
+ let oldSchemaText;
933
990
  try {
934
- await stat5(join7(target, "SCHEMA.md"));
935
- hasSchema = true;
991
+ oldSchemaText = await readFile6(join7(target, "SCHEMA.md"), "utf8");
936
992
  } catch {
937
993
  }
938
- if (hasSchema && !input.force) {
994
+ if (oldSchemaText && !input.force) {
939
995
  return {
940
996
  exitCode: ExitCode.INIT_TARGET_NOT_EMPTY,
941
997
  result: err("INIT_TARGET_NOT_EMPTY", { target })
942
998
  };
943
999
  }
944
1000
  const envPath = join7(input.home, ".skillwiki", ".env");
945
- const existingEnv = await parseDotenvFile(envPath);
1001
+ let existingEnvRaw = "";
1002
+ try {
1003
+ existingEnvRaw = await readFile6(envPath, "utf8");
1004
+ } catch {
1005
+ }
1006
+ const existingEnv = parseDotenvText(existingEnvRaw);
946
1007
  const swDotenvHadPath = existingEnv.WIKI_PATH !== void 0;
947
1008
  if (existingEnv.WIKI_PATH !== void 0 && existingEnv.WIKI_PATH !== target && !input.force) {
948
1009
  return {
@@ -967,64 +1028,107 @@ async function runInit(input) {
967
1028
  return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { message: String(e) }) };
968
1029
  }
969
1030
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
970
- const taxonomy = input.taxonomy && input.taxonomy.length > 0 ? input.taxonomy : DEFAULT_TAXONOMY;
971
- 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");
972
1052
  try {
973
1053
  const schemaTpl = await readFile6(join7(input.templates, "SCHEMA.md"), "utf8");
974
- 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);
975
1055
  await writeFile4(join7(target, "SCHEMA.md"), schema, "utf8");
976
1056
  created.push("SCHEMA.md");
977
1057
  } catch (e) {
978
1058
  return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: "SCHEMA.md", message: String(e) }) };
979
1059
  }
980
- try {
981
- const idxTpl = await readFile6(join7(input.templates, "index.md"), "utf8");
982
- const idx = idxTpl.replace("{{INIT_DATE}}", today);
983
- await writeFile4(join7(target, "index.md"), idx, "utf8");
984
- created.push("index.md");
985
- } catch (e) {
986
- return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: "index.md", message: String(e) }) };
987
- }
988
- try {
989
- const logTpl = await readFile6(join7(input.templates, "log.md"), "utf8");
990
- const log = logTpl.replace(/\{\{INIT_DATE\}\}/g, today).replace("{{DOMAIN}}", input.domain).replace("{{WIKI_LANG}}", canonicalLang);
991
- await writeFile4(join7(target, "log.md"), log, "utf8");
992
- created.push("log.md");
993
- } catch (e) {
994
- 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
+ }
995
1077
  }
996
- try {
997
- await mkdir4(dirname5(envPath), { recursive: true });
998
- const envBody = `WIKI_PATH=${target}
999
- WIKI_LANG=${canonicalLang}
1000
- `;
1001
- await writeFile4(envPath, envBody, "utf8");
1002
- } catch (e) {
1003
- 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
+ }
1004
1098
  }
1005
1099
  const importedFromHermes = pathRes.source === "hermes-dotenv" && !swDotenvHadPath;
1006
1100
  return {
1007
1101
  exitCode: ExitCode.OK,
1008
1102
  result: ok({
1009
1103
  vault: target,
1010
- domain: input.domain,
1104
+ domain,
1011
1105
  taxonomy,
1012
1106
  lang: canonicalLang,
1013
1107
  created,
1014
- env_written: envPath,
1015
- imported_from_hermes: importedFromHermes
1108
+ preserved,
1109
+ env_written: envWritten,
1110
+ env_skipped: skipEnv,
1111
+ imported_from_hermes: importedFromHermes,
1112
+ discovered_tags
1016
1113
  })
1017
1114
  };
1018
1115
  }
1019
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
+
1020
1127
  // src/commands/links.ts
1021
1128
  async function runLinks(input) {
1022
1129
  const scan = await scanVault(input.vault);
1023
1130
  if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
1024
- const slugs = /* @__PURE__ */ new Set();
1025
- for (const p of scan.data.typedKnowledge) {
1026
- slugs.add(p.relPath.replace(/\.md$/, "").split("/").pop());
1027
- }
1131
+ const slugs = buildSlugMap(scan.data.typedKnowledge);
1028
1132
  const broken = [];
1029
1133
  for (const p of scan.data.typedKnowledge) {
1030
1134
  const text = await readPage(p);
@@ -1033,7 +1137,7 @@ async function runLinks(input) {
1033
1137
  const lines = body.split("\n");
1034
1138
  for (const slug of extractBodyWikilinks(body)) {
1035
1139
  const tail = slug.split("/").pop();
1036
- if (!slugs.has(tail)) {
1140
+ if (!slugs.has(tail.toLowerCase())) {
1037
1141
  const line = lines.findIndex((l) => l.includes(`[[${slug}`));
1038
1142
  broken.push({ page: p.relPath, slug, line: line >= 0 ? line + 1 : 0 });
1039
1143
  }
@@ -1048,33 +1152,6 @@ async function runLinks(input) {
1048
1152
  // src/commands/tag-audit.ts
1049
1153
  import { readFile as readFile7 } from "fs/promises";
1050
1154
  import { join as join8 } from "path";
1051
-
1052
- // src/parsers/taxonomy.ts
1053
- import yaml2 from "js-yaml";
1054
- var FENCE_RE = /^##\s+Tag Taxonomy\s*$[\s\S]*?```yaml\s*\n([\s\S]*?)\n```/m;
1055
- function extractTaxonomy(schemaText) {
1056
- const m = schemaText.match(FENCE_RE);
1057
- if (!m) return ok([]);
1058
- let parsed;
1059
- try {
1060
- parsed = yaml2.load(m[1], { schema: yaml2.JSON_SCHEMA });
1061
- } catch (e) {
1062
- return err("INVALID_FRONTMATTER", { message: e.message });
1063
- }
1064
- if (parsed === null || typeof parsed !== "object") {
1065
- return err("INVALID_FRONTMATTER", { message: "taxonomy block is not an object" });
1066
- }
1067
- const tax = parsed.taxonomy;
1068
- if (!Array.isArray(tax)) {
1069
- return err("INVALID_FRONTMATTER", { message: "taxonomy key missing or not an array" });
1070
- }
1071
- if (!tax.every((x) => typeof x === "string")) {
1072
- return err("INVALID_FRONTMATTER", { message: "taxonomy must be a list of strings" });
1073
- }
1074
- return ok(tax);
1075
- }
1076
-
1077
- // src/commands/tag-audit.ts
1078
1155
  async function runTagAudit(input) {
1079
1156
  const scan = await scanVault(input.vault);
1080
1157
  if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
@@ -1112,7 +1189,11 @@ async function runIndexCheck(input) {
1112
1189
  indexText = await readFile8(join9(input.vault, "index.md"), "utf8");
1113
1190
  } catch {
1114
1191
  }
1115
- 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
+ }
1116
1197
  const fileSlugs = /* @__PURE__ */ new Map();
1117
1198
  for (const p of scan.data.typedKnowledge) {
1118
1199
  const slug = p.relPath.replace(/\.md$/, "").split("/").pop();
@@ -1120,11 +1201,12 @@ async function runIndexCheck(input) {
1120
1201
  }
1121
1202
  const missing_from_index = [];
1122
1203
  for (const [slug, relPath] of fileSlugs.entries()) {
1123
- if (!indexSlugs.has(slug)) missing_from_index.push(relPath);
1204
+ if (!indexSlugsLower.has(slug.toLowerCase())) missing_from_index.push(relPath);
1124
1205
  }
1206
+ const fileSlugsLower = new Set([...fileSlugs.keys()].map((s) => s.toLowerCase()));
1125
1207
  const ghost_entries = [];
1126
- for (const slug of indexSlugs) {
1127
- if (!fileSlugs.has(slug)) ghost_entries.push(slug);
1208
+ for (const [lower, orig] of indexSlugsLower) {
1209
+ if (!fileSlugsLower.has(lower)) ghost_entries.push(orig);
1128
1210
  }
1129
1211
  if (missing_from_index.length > 0 || ghost_entries.length > 0) {
1130
1212
  return { exitCode: ExitCode.INDEX_INCOMPLETE, result: ok({ missing_from_index, ghost_entries }) };
@@ -1190,12 +1272,12 @@ async function runPagesize(input) {
1190
1272
  }
1191
1273
 
1192
1274
  // src/commands/log-rotate.ts
1193
- 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";
1194
1276
  import { join as join11 } from "path";
1195
1277
  var ENTRY_RE = /^## \[(\d{4})-\d{2}-\d{2}\]/gm;
1196
1278
  async function runLogRotate(input) {
1197
1279
  try {
1198
- await stat6(join11(input.vault, "SCHEMA.md"));
1280
+ await stat5(join11(input.vault, "SCHEMA.md"));
1199
1281
  } catch {
1200
1282
  return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
1201
1283
  }
@@ -1514,7 +1596,7 @@ program.command("lang").option("--lang <code>", "explicit language override").op
1514
1596
  explain: !!opts.explain
1515
1597
  }));
1516
1598
  });
1517
- 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) => {
1518
1600
  const templates = new URL("../templates/", import.meta.url).pathname;
1519
1601
  const taxonomy = typeof opts.taxonomy === "string" ? opts.taxonomy.split(",").map((s) => s.trim()).filter((s) => s.length > 0) : void 0;
1520
1602
  emit(await runInit({
@@ -1525,7 +1607,8 @@ program.command("init").option("--target <dir>", "explicit target directory").re
1525
1607
  domain: opts.domain,
1526
1608
  taxonomy,
1527
1609
  lang: opts.lang,
1528
- force: !!opts.force
1610
+ force: !!opts.force,
1611
+ noEnv: opts.env === false
1529
1612
  }));
1530
1613
  });
1531
1614
  async function resolveVaultArg(arg) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.2.0-beta.6",
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,6 +1,6 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.2.0-beta.6",
3
+ "version": "0.2.0-beta.7",
4
4
  "skills": "./",
5
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).",
6
6
  "author": {
@@ -1,6 +1,13 @@
1
1
  {
2
2
  "name": "@skillwiki/skills",
3
- "version": "0.2.0-beta.6",
3
+ "version": "0.2.0-beta.7",
4
4
  "private": true,
5
- "files": ["wiki-*", "proj-*", "using-skillwiki", ".claude-plugin", "hooks", "README.md"]
5
+ "files": [
6
+ "wiki-*",
7
+ "proj-*",
8
+ "using-skillwiki",
9
+ ".claude-plugin",
10
+ "hooks",
11
+ "README.md"
12
+ ]
6
13
  }