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 +203 -127
- package/package.json +5 -1
- 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 +9 -2
- package/skills/using-skillwiki/SKILL.md +57 -0
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(
|
|
74
|
-
return detail === void 0 ? { ok: false, error
|
|
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
|
|
495
|
-
|
|
496
|
-
|
|
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 (!
|
|
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,
|
|
898
|
-
import { join as join7
|
|
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
|
|
989
|
+
let oldSchemaText;
|
|
929
990
|
try {
|
|
930
|
-
await
|
|
931
|
-
hasSchema = true;
|
|
991
|
+
oldSchemaText = await readFile6(join7(target, "SCHEMA.md"), "utf8");
|
|
932
992
|
} catch {
|
|
933
993
|
}
|
|
934
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
967
|
-
|
|
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}}",
|
|
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
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
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
|
-
|
|
993
|
-
await
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
return {
|
|
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
|
|
1104
|
+
domain,
|
|
1007
1105
|
taxonomy,
|
|
1008
1106
|
lang: canonicalLang,
|
|
1009
1107
|
created,
|
|
1010
|
-
|
|
1011
|
-
|
|
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 =
|
|
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
|
|
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 (!
|
|
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
|
|
1123
|
-
if (!
|
|
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
|
|
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
|
|
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
|
|
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 ?
|
|
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
|
|
1346
|
-
return { id, label, status
|
|
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
|
|
1437
|
+
return check("pass", "node_version", "Node.js version", `v${major} >= 20`);
|
|
1358
1438
|
}
|
|
1359
|
-
return
|
|
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
|
|
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
|
|
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
|
|
1450
|
+
return check("pass", "cli_on_path", "skillwiki on PATH", "skillwiki found on PATH");
|
|
1371
1451
|
} catch {
|
|
1372
|
-
return
|
|
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 =
|
|
1456
|
+
const cfgPath = configPath(home);
|
|
1377
1457
|
if (!existsSync2(cfgPath)) {
|
|
1378
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1473
|
+
return check("pass", "wiki_path_exists", "Vault directory exists", resolvedPath);
|
|
1401
1474
|
}
|
|
1402
|
-
return
|
|
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
|
|
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
|
|
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
|
|
1490
|
+
return check("pass", "vault_structure", "Vault structure valid", "All required files and directories present");
|
|
1418
1491
|
}
|
|
1419
|
-
return
|
|
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
|
|
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
|
|
1501
|
+
return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found`);
|
|
1429
1502
|
}
|
|
1430
|
-
return
|
|
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.
|
|
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.
|
|
4
|
-
"
|
|
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,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
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skillwiki/skills",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0-beta.7",
|
|
4
4
|
"private": true,
|
|
5
|
-
"files": [
|
|
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`.
|