skillwiki 0.5.1 → 0.5.3
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
|
@@ -59,7 +59,8 @@ var ExitCode = {
|
|
|
59
59
|
SYNC_PUSH_FAILED: 42,
|
|
60
60
|
SYNC_PULL_FAILED: 43,
|
|
61
61
|
BACKUP_SYNC_FAILED: 44,
|
|
62
|
-
BACKUP_RESTORE_CONFLICTS: 45
|
|
62
|
+
BACKUP_RESTORE_CONFLICTS: 45,
|
|
63
|
+
USAGE: 46
|
|
63
64
|
};
|
|
64
65
|
|
|
65
66
|
// ../shared/src/json-output.ts
|
|
@@ -91,7 +92,8 @@ var TypedKnowledgeSchema = z.object({
|
|
|
91
92
|
contradictions: z.array(z.string()).optional(),
|
|
92
93
|
provenance: z.enum(["research", "project", "mixed"]).optional(),
|
|
93
94
|
provenance_projects: z.array(wikilink).optional(),
|
|
94
|
-
work_items: z.array(wikilink).optional()
|
|
95
|
+
work_items: z.array(wikilink).optional(),
|
|
96
|
+
stale_ttl: z.number().int().positive().optional()
|
|
95
97
|
}).superRefine((v, ctx) => {
|
|
96
98
|
if (v.provenance && v.provenance !== "research" && (!v.provenance_projects || v.provenance_projects.length === 0)) {
|
|
97
99
|
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["provenance_projects"], message: "required when provenance != research" });
|
|
@@ -1004,8 +1006,10 @@ function isLegacyCitationStyle(body) {
|
|
|
1004
1006
|
return false;
|
|
1005
1007
|
}
|
|
1006
1008
|
function hasOrphanedCitations(body) {
|
|
1007
|
-
const
|
|
1009
|
+
const noFm = body.replace(FRONTMATTER, "");
|
|
1010
|
+
const stripped = stripFences(noFm);
|
|
1008
1011
|
const lines = stripped.split("\n");
|
|
1012
|
+
const rawLines = noFm.split("\n");
|
|
1009
1013
|
let inSources = false;
|
|
1010
1014
|
let sourcesEnded = false;
|
|
1011
1015
|
let sourcesStartLine = -1;
|
|
@@ -1025,10 +1029,13 @@ function hasOrphanedCitations(body) {
|
|
|
1025
1029
|
}
|
|
1026
1030
|
continue;
|
|
1027
1031
|
}
|
|
1028
|
-
const isListItem = /^\s*[-*]\s+/.test(line);
|
|
1032
|
+
const isListItem = /^\s*(?:[-*]|\d+\.)\s+/.test(line);
|
|
1029
1033
|
const hasMarker = /\^\[raw\//.test(line);
|
|
1034
|
+
const hasBacktickRawPath = /`raw\/[^`]+`/.test(rawLines[i]);
|
|
1030
1035
|
if (isListItem && hasMarker) {
|
|
1031
1036
|
lastNonBlankInSources = i;
|
|
1037
|
+
} else if (isListItem && hasBacktickRawPath) {
|
|
1038
|
+
lastNonBlankInSources = i;
|
|
1032
1039
|
} else if (hasMarker && !isListItem) {
|
|
1033
1040
|
return true;
|
|
1034
1041
|
} else {
|
|
@@ -1835,6 +1842,42 @@ async function runIndexCheck(input) {
|
|
|
1835
1842
|
// src/commands/stale.ts
|
|
1836
1843
|
import { readdir as readdir4, rename as rename2, mkdir as mkdir6, readFile as readFile10 } from "fs/promises";
|
|
1837
1844
|
import { join as join13 } from "path";
|
|
1845
|
+
|
|
1846
|
+
// src/parsers/expiry-annotations.ts
|
|
1847
|
+
var HEADING_RE = /^#{1,6}\s+(.+)$/;
|
|
1848
|
+
var ANNOTATION_RE = /^<!--\s*expires:\s*(\d{4}-\d{2}-\d{2})(?:\s+refresh:\s*(weekly|monthly|quarterly))?(?:\s+source:\s*(\S+))?\s*-->$/;
|
|
1849
|
+
var VALID_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
1850
|
+
function isValidDate(s) {
|
|
1851
|
+
if (!VALID_DATE_RE.test(s)) return false;
|
|
1852
|
+
const d = /* @__PURE__ */ new Date(s + "T00:00:00Z");
|
|
1853
|
+
return !Number.isNaN(d.getTime()) && s === d.toISOString().slice(0, 10);
|
|
1854
|
+
}
|
|
1855
|
+
function parseExpiryAnnotations(content, pagePath) {
|
|
1856
|
+
const lines = content.split("\n");
|
|
1857
|
+
const annotations = [];
|
|
1858
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1859
|
+
const match = lines[i].match(ANNOTATION_RE);
|
|
1860
|
+
if (!match) continue;
|
|
1861
|
+
const expires = match[1];
|
|
1862
|
+
if (!isValidDate(expires)) continue;
|
|
1863
|
+
if (i === 0) continue;
|
|
1864
|
+
const prevLine = lines[i - 1];
|
|
1865
|
+
const headingMatch = prevLine.match(HEADING_RE);
|
|
1866
|
+
if (!headingMatch) continue;
|
|
1867
|
+
annotations.push({
|
|
1868
|
+
page: pagePath,
|
|
1869
|
+
heading: headingMatch[1].trim(),
|
|
1870
|
+
line: i + 1,
|
|
1871
|
+
// 1-indexed
|
|
1872
|
+
expires,
|
|
1873
|
+
refresh: match[2],
|
|
1874
|
+
source: match[3]
|
|
1875
|
+
});
|
|
1876
|
+
}
|
|
1877
|
+
return annotations;
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
// src/commands/stale.ts
|
|
1838
1881
|
function daysSince(isoDate2) {
|
|
1839
1882
|
return Math.floor((Date.now() - Date.parse(isoDate2)) / 864e5);
|
|
1840
1883
|
}
|
|
@@ -1852,6 +1895,12 @@ async function runStale(input) {
|
|
|
1852
1895
|
projectSlugs = (await readdir4(projectsDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
1853
1896
|
} catch {
|
|
1854
1897
|
}
|
|
1898
|
+
if (input.project) {
|
|
1899
|
+
if (!projectSlugs.includes(input.project)) {
|
|
1900
|
+
return { exitCode: ExitCode.USAGE, result: { ok: false, error: "UNKNOWN_PROJECT", detail: `Project "${input.project}" not found. Available: ${projectSlugs.join(", ") || "(none)"}` } };
|
|
1901
|
+
}
|
|
1902
|
+
projectSlugs = [input.project];
|
|
1903
|
+
}
|
|
1855
1904
|
for (const slug of projectSlugs) {
|
|
1856
1905
|
const workPath = join13(projectsDir, slug, "work");
|
|
1857
1906
|
let entries;
|
|
@@ -1893,9 +1942,10 @@ async function runStale(input) {
|
|
|
1893
1942
|
function extractSlug(projectField) {
|
|
1894
1943
|
return projectField.replace(/^\[\[/, "").replace(/\]\]$/, "").replace(/^"|"$/g, "");
|
|
1895
1944
|
}
|
|
1945
|
+
const TERMINAL_STATUSES = /* @__PURE__ */ new Set(["completed", "abandoned", "done", "invalid"]);
|
|
1896
1946
|
const KIND_FROM_FILENAME = /^(?:\d{4}-\d{2}-\d{2})-(task|bug|idea|note|observation)-.+\.md$/;
|
|
1897
1947
|
const LOOP_CYCLE_PATTERN = /loop-cycle-/;
|
|
1898
|
-
|
|
1948
|
+
let transcripts = scan.data.raw.filter((p) => p.relPath.startsWith("raw/transcripts/") && p.relPath.endsWith(".md"));
|
|
1899
1949
|
const claimedPaths = /* @__PURE__ */ new Set();
|
|
1900
1950
|
const transcriptMeta = /* @__PURE__ */ new Map();
|
|
1901
1951
|
for (const t of transcripts) {
|
|
@@ -1904,6 +1954,7 @@ async function runStale(input) {
|
|
|
1904
1954
|
const fm = extractFrontmatter(content);
|
|
1905
1955
|
let kind = fm.ok && typeof fm.data.kind === "string" ? fm.data.kind : "";
|
|
1906
1956
|
let project = fm.ok && typeof fm.data.project === "string" ? fm.data.project : "";
|
|
1957
|
+
if (input.project && !project.includes(input.project)) continue;
|
|
1907
1958
|
let inferred = false;
|
|
1908
1959
|
if (input.forceScan && !kind) {
|
|
1909
1960
|
const basename = t.relPath.split("/").pop();
|
|
@@ -1948,7 +1999,7 @@ async function runStale(input) {
|
|
|
1948
1999
|
const overlap = wWords.filter((w) => tWords.has(w)).length;
|
|
1949
2000
|
if (dirName.includes(tSlug) || tSlug.includes(wSlug) || overlap >= 1) {
|
|
1950
2001
|
claimedPaths.add(t.relPath);
|
|
1951
|
-
if (status
|
|
2002
|
+
if (TERMINAL_STATUSES.has(status)) {
|
|
1952
2003
|
staleTranscripts.push({ path: t.relPath, reason: `work item projects/${slug}/work/${dirName} is ${status}` });
|
|
1953
2004
|
}
|
|
1954
2005
|
break;
|
|
@@ -1958,7 +2009,7 @@ async function runStale(input) {
|
|
|
1958
2009
|
for (const [dir, status] of workDirs) {
|
|
1959
2010
|
if (dir.split("/").pop().startsWith(datePrefix)) {
|
|
1960
2011
|
claimedPaths.add(t.relPath);
|
|
1961
|
-
if (status
|
|
2012
|
+
if (TERMINAL_STATUSES.has(status)) {
|
|
1962
2013
|
staleTranscripts.push({ path: t.relPath, reason: `work item ${dir} is ${status}` });
|
|
1963
2014
|
}
|
|
1964
2015
|
break;
|
|
@@ -2003,10 +2054,8 @@ async function runStale(input) {
|
|
|
2003
2054
|
continue;
|
|
2004
2055
|
}
|
|
2005
2056
|
const hasSpec = files.includes("spec.md"), hasPlan = files.includes("plan.md"), hasWI = files.includes("work-item.md");
|
|
2006
|
-
if (status
|
|
2007
|
-
doneWorkItems.push({ path: relDir, reason: "completed \u2014 should be archived
|
|
2008
|
-
} else if (status === "invalid") {
|
|
2009
|
-
doneWorkItems.push({ path: relDir, reason: "invalid \u2014 should be archived" });
|
|
2057
|
+
if (TERMINAL_STATUSES.has(status)) {
|
|
2058
|
+
doneWorkItems.push({ path: relDir, reason: `${status || "completed"} \u2014 should be archived` });
|
|
2010
2059
|
} else if (hasSpec && !hasPlan) {
|
|
2011
2060
|
incompleteWorkItems.push({ path: relDir, reason: "has spec but no plan" });
|
|
2012
2061
|
} else if (hasWI && !hasSpec && !hasPlan) {
|
|
@@ -2019,16 +2068,53 @@ async function runStale(input) {
|
|
|
2019
2068
|
const text = await readFile10(join13(input.vault, page.relPath), "utf8");
|
|
2020
2069
|
const fm = extractFrontmatter(text);
|
|
2021
2070
|
if (fm.ok && typeof fm.data.updated === "string") {
|
|
2071
|
+
if (input.project) {
|
|
2072
|
+
const pp = fm.data.provenance_projects;
|
|
2073
|
+
const linked = Array.isArray(pp) && pp.some((p) => String(p).includes(input.project));
|
|
2074
|
+
if (!linked) continue;
|
|
2075
|
+
}
|
|
2022
2076
|
const age = daysSince(fm.data.updated);
|
|
2023
|
-
|
|
2024
|
-
|
|
2077
|
+
const threshold = typeof fm.data.stale_ttl === "number" && fm.data.stale_ttl > 0 ? fm.data.stale_ttl : input.days;
|
|
2078
|
+
if (age >= threshold) {
|
|
2079
|
+
stale.push({ page: page.relPath, reason: `updated ${age} days ago (threshold: ${threshold})` });
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
} catch {
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
const staleSections = [];
|
|
2086
|
+
for (const page of scan.data.typedKnowledge) {
|
|
2087
|
+
try {
|
|
2088
|
+
const text = await readFile10(join13(input.vault, page.relPath), "utf8");
|
|
2089
|
+
const projectFilter = input.project;
|
|
2090
|
+
if (projectFilter) {
|
|
2091
|
+
const fm = extractFrontmatter(text);
|
|
2092
|
+
if (fm.ok) {
|
|
2093
|
+
const pp = fm.data.provenance_projects;
|
|
2094
|
+
const linked = Array.isArray(pp) && pp.some((p) => String(p).includes(projectFilter));
|
|
2095
|
+
if (!linked) continue;
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
const annotations = parseExpiryAnnotations(text, page.relPath);
|
|
2099
|
+
for (const ann of annotations) {
|
|
2100
|
+
if (daysSince(ann.expires) >= 0) {
|
|
2101
|
+
staleSections.push({
|
|
2102
|
+
page: ann.page,
|
|
2103
|
+
heading: ann.heading,
|
|
2104
|
+
line: ann.line,
|
|
2105
|
+
expires: ann.expires,
|
|
2106
|
+
refresh: ann.refresh,
|
|
2107
|
+
source: ann.source,
|
|
2108
|
+
reason: `section "${ann.heading}" expired on ${ann.expires}`
|
|
2109
|
+
});
|
|
2025
2110
|
}
|
|
2026
2111
|
}
|
|
2027
2112
|
} catch {
|
|
2028
2113
|
}
|
|
2029
2114
|
}
|
|
2115
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2030
2116
|
if (input.archive) {
|
|
2031
|
-
const archiveDir = join13(input.vault, "_archive",
|
|
2117
|
+
const archiveDir = join13(input.vault, "_archive", today);
|
|
2032
2118
|
await mkdir6(archiveDir, { recursive: true });
|
|
2033
2119
|
const citedRawPaths = /* @__PURE__ */ new Set();
|
|
2034
2120
|
for (const page of scan.data.typedKnowledge) {
|
|
@@ -2082,7 +2168,7 @@ async function runStale(input) {
|
|
|
2082
2168
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2083
2169
|
});
|
|
2084
2170
|
}
|
|
2085
|
-
const total = stale.length + staleTranscripts.length + unclaimedTranscripts.length + incompleteWorkItems.length + doneWorkItems.length;
|
|
2171
|
+
const total = stale.length + staleTranscripts.length + unclaimedTranscripts.length + incompleteWorkItems.length + doneWorkItems.length + staleSections.length;
|
|
2086
2172
|
const hintLines = [];
|
|
2087
2173
|
if (stale.length > 0) hintLines.push(`stale_pages: ${stale.length}`, ...stale.map((p) => ` ${p.page}: ${p.reason}`));
|
|
2088
2174
|
if (staleTranscripts.length > 0) hintLines.push(`stale_transcripts: ${staleTranscripts.length}`, ...staleTranscripts.map((t) => ` ${t.path}: ${t.reason}`));
|
|
@@ -2090,6 +2176,7 @@ async function runStale(input) {
|
|
|
2090
2176
|
hint: ${t.hint}` : ""}`));
|
|
2091
2177
|
if (incompleteWorkItems.length > 0) hintLines.push(`incomplete_work_items: ${incompleteWorkItems.length}`, ...incompleteWorkItems.map((w) => ` ${w.path}: ${w.reason}`));
|
|
2092
2178
|
if (doneWorkItems.length > 0) hintLines.push(`done_work_items: ${doneWorkItems.length}`, ...doneWorkItems.map((w) => ` ${w.path}: ${w.reason}`));
|
|
2179
|
+
if (staleSections.length > 0) hintLines.push(`stale_sections: ${staleSections.length}`, ...staleSections.map((s) => ` ${s.page}#${s.heading}: ${s.reason}`));
|
|
2093
2180
|
if (archived.length > 0) hintLines.push(`archived: ${archived.length}`, ...archived.map((a) => ` ${a}`));
|
|
2094
2181
|
if (hintLines.length === 0) hintLines.push("no stale transcripts or incomplete work items");
|
|
2095
2182
|
return { exitCode: total > 0 ? ExitCode.STALE_PAGE : ExitCode.OK, result: ok({
|
|
@@ -2098,6 +2185,7 @@ async function runStale(input) {
|
|
|
2098
2185
|
unclaimed_transcripts: unclaimedTranscripts,
|
|
2099
2186
|
incomplete_work_items: incompleteWorkItems,
|
|
2100
2187
|
done_work_items: doneWorkItems,
|
|
2188
|
+
stale_sections: staleSections,
|
|
2101
2189
|
archived,
|
|
2102
2190
|
humanHint: hintLines.join("\n")
|
|
2103
2191
|
}) };
|
|
@@ -2466,11 +2554,11 @@ function buildCliSurface() {
|
|
|
2466
2554
|
program2.command("index-check").option("--wiki <name>");
|
|
2467
2555
|
program2.command("index-link-format").option("--wiki <name>");
|
|
2468
2556
|
program2.command("topic-map-check").option("--threshold <n>").option("--wiki <name>");
|
|
2469
|
-
program2.command("stale").option("--archive").option("--days <n>").option("--force-scan").option("--wiki <name>");
|
|
2557
|
+
program2.command("stale").option("--archive").option("--days <n>").option("--force-scan").option("--project <slug>").option("--refresh").option("--wiki <name>");
|
|
2470
2558
|
program2.command("claim").option("--project <slug>").option("--slug <slug>").option("--wiki <name>");
|
|
2471
2559
|
program2.command("pagesize").option("--lines <n>").option("--wiki <name>");
|
|
2472
2560
|
program2.command("log-rotate").option("--threshold <n>").option("--apply").option("--wiki <name>");
|
|
2473
|
-
program2.command("lint").option("--days <n>").option("--lines <n>").option("--log-threshold <n>").option("--fix").option("--wiki <name>");
|
|
2561
|
+
program2.command("lint").option("--days <n>").option("--lines <n>").option("--log-threshold <n>").option("--fix").option("--only <bucket>").option("--wiki <name>");
|
|
2474
2562
|
program2.command("config");
|
|
2475
2563
|
program2.command("doctor");
|
|
2476
2564
|
program2.command("status").option("--wiki <name>");
|
|
@@ -2615,8 +2703,8 @@ function extractSourceEntries(rawFm) {
|
|
|
2615
2703
|
return entries;
|
|
2616
2704
|
}
|
|
2617
2705
|
var ERROR_ORDER = ["broken_wikilinks", "invalid_frontmatter", "raw_dedup", "broken_sources", "tag_not_in_taxonomy"];
|
|
2618
|
-
var WARNING_ORDER = ["raw_body_duplicate", "raw_subdirectory_duplicate", "index_incomplete", "index_link_format", "stale_page", "page_too_large", "log_rotate_needed", "orphans", "compound_refs", "legacy_citation_style", "orphaned_citations", "duplicate_frontmatter", "work_item_health", "orphaned_project_pages", "missing_overview"];
|
|
2619
|
-
var INFO_ORDER = ["bridges", "page_structure", "topic_map_recommended", "frontmatter_wikilink", "wikilink_citation", "missing_tldr", "
|
|
2706
|
+
var WARNING_ORDER = ["raw_body_duplicate", "raw_subdirectory_duplicate", "index_incomplete", "index_link_format", "stale_page", "page_too_large", "log_rotate_needed", "orphans", "compound_refs", "legacy_citation_style", "orphaned_citations", "duplicate_frontmatter", "work_item_health", "orphaned_project_pages", "missing_overview", "missing_diagram"];
|
|
2707
|
+
var INFO_ORDER = ["bridges", "page_structure", "topic_map_recommended", "frontmatter_wikilink", "wikilink_citation", "missing_tldr", "stale_sections", "cli_refs"];
|
|
2620
2708
|
async function runLint(input) {
|
|
2621
2709
|
const buckets = {};
|
|
2622
2710
|
const fixed = [];
|
|
@@ -2785,7 +2873,7 @@ async function runLint(input) {
|
|
|
2785
2873
|
const text = await readPage(specPage);
|
|
2786
2874
|
const fm = extractFrontmatter(text);
|
|
2787
2875
|
if (fm.ok) {
|
|
2788
|
-
specStatus = fm.data.status;
|
|
2876
|
+
specStatus = typeof fm.data.status === "string" ? fm.data.status : void 0;
|
|
2789
2877
|
specStarted = fm.data.started;
|
|
2790
2878
|
}
|
|
2791
2879
|
}
|
|
@@ -2840,6 +2928,27 @@ async function runLint(input) {
|
|
|
2840
2928
|
}
|
|
2841
2929
|
}
|
|
2842
2930
|
if (cliRefFlags.length > 0) buckets.cli_refs = cliRefFlags;
|
|
2931
|
+
const staleSectionFlags = [];
|
|
2932
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2933
|
+
const approachingThreshold = 7;
|
|
2934
|
+
for (const page of scan.data.typedKnowledge) {
|
|
2935
|
+
try {
|
|
2936
|
+
const text = await readPage(page);
|
|
2937
|
+
const annotations = parseExpiryAnnotations(text, page.relPath);
|
|
2938
|
+
for (const ann of annotations) {
|
|
2939
|
+
if (ann.expires < today) {
|
|
2940
|
+
staleSectionFlags.push(`${page.relPath}: section "${ann.heading}" expired on ${ann.expires}`);
|
|
2941
|
+
} else {
|
|
2942
|
+
const daysUntilExpiry = Math.floor((Date.parse(ann.expires) - Date.now()) / 864e5);
|
|
2943
|
+
if (daysUntilExpiry <= approachingThreshold) {
|
|
2944
|
+
staleSectionFlags.push(`${page.relPath}: section "${ann.heading}" expires in ${daysUntilExpiry} day(s) (${ann.expires})`);
|
|
2945
|
+
}
|
|
2946
|
+
}
|
|
2947
|
+
}
|
|
2948
|
+
} catch {
|
|
2949
|
+
}
|
|
2950
|
+
}
|
|
2951
|
+
if (staleSectionFlags.length > 0) buckets.stale_sections = staleSectionFlags;
|
|
2843
2952
|
if (input.fix && legacyPages.length > 0) {
|
|
2844
2953
|
const FENCE_RE2 = /```[\s\S]*?```/g;
|
|
2845
2954
|
const INLINE_MARKER = /\^\[raw\/[^\]]+\]/g;
|
|
@@ -3106,6 +3215,38 @@ ${newBody}`;
|
|
|
3106
3215
|
const errorOut = ERROR_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
|
|
3107
3216
|
const warningOut = WARNING_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
|
|
3108
3217
|
const infoOut = INFO_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
|
|
3218
|
+
if (input.only) {
|
|
3219
|
+
const allKnown = [...ERROR_ORDER, ...WARNING_ORDER, ...INFO_ORDER];
|
|
3220
|
+
if (!allKnown.includes(input.only)) {
|
|
3221
|
+
return {
|
|
3222
|
+
exitCode: ExitCode.USAGE,
|
|
3223
|
+
result: { ok: false, error: "UNKNOWN_BUCKET", detail: `Unknown bucket "${input.only}". Valid: ${allKnown.join(", ")}` }
|
|
3224
|
+
};
|
|
3225
|
+
}
|
|
3226
|
+
const match = [...errorOut, ...warningOut, ...infoOut].filter((b) => b.kind === input.only);
|
|
3227
|
+
const severity = ERROR_ORDER.includes(input.only) ? "error" : WARNING_ORDER.includes(input.only) ? "warning" : "info";
|
|
3228
|
+
const filtered = severity === "error" ? { error: match, warning: [], info: [] } : severity === "warning" ? { error: [], warning: match, info: [] } : { error: [], warning: [], info: match };
|
|
3229
|
+
const fSummary = {
|
|
3230
|
+
errors: filtered.error.reduce((n, b) => n + b.items.length, 0),
|
|
3231
|
+
warnings: filtered.warning.reduce((n, b) => n + b.items.length, 0),
|
|
3232
|
+
info: filtered.info.reduce((n, b) => n + b.items.length, 0)
|
|
3233
|
+
};
|
|
3234
|
+
let fExit = ExitCode.OK;
|
|
3235
|
+
if (fSummary.errors > 0) fExit = ExitCode.LINT_HAS_ERRORS;
|
|
3236
|
+
else if (fSummary.warnings > 0 || fSummary.info > 0) fExit = ExitCode.LINT_HAS_WARNINGS;
|
|
3237
|
+
return {
|
|
3238
|
+
exitCode: fExit,
|
|
3239
|
+
result: ok({
|
|
3240
|
+
vault: { path: input.vault, source: input.source ?? "resolved" },
|
|
3241
|
+
summary: fSummary,
|
|
3242
|
+
by_severity: filtered,
|
|
3243
|
+
fixed,
|
|
3244
|
+
unresolved,
|
|
3245
|
+
humanHint: `--only ${input.only}
|
|
3246
|
+
${match.length === 0 ? "0 violations" : match.map((b) => ` ${b.kind}: ${b.items.length}`).join("\n")}`
|
|
3247
|
+
})
|
|
3248
|
+
};
|
|
3249
|
+
}
|
|
3109
3250
|
const summary = {
|
|
3110
3251
|
errors: errorOut.reduce((n, b) => n + b.items.length, 0),
|
|
3111
3252
|
warnings: warningOut.reduce((n, b) => n + b.items.length, 0),
|
|
@@ -3213,7 +3354,7 @@ import { platform } from "os";
|
|
|
3213
3354
|
|
|
3214
3355
|
// src/utils/auto-update.ts
|
|
3215
3356
|
import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync5, mkdirSync as mkdirSync2 } from "fs";
|
|
3216
|
-
import { join as join20, dirname as
|
|
3357
|
+
import { join as join20, dirname as dirname7 } from "path";
|
|
3217
3358
|
import { spawn } from "child_process";
|
|
3218
3359
|
|
|
3219
3360
|
// src/utils/update-consts.ts
|
|
@@ -3243,7 +3384,7 @@ function readCache(home) {
|
|
|
3243
3384
|
}
|
|
3244
3385
|
function writeCache(home, cache) {
|
|
3245
3386
|
const p = cachePath(home);
|
|
3246
|
-
mkdirSync2(
|
|
3387
|
+
mkdirSync2(dirname7(p), { recursive: true });
|
|
3247
3388
|
writeFileSync3(p, JSON.stringify(cache, null, 2));
|
|
3248
3389
|
}
|
|
3249
3390
|
function latestFromCache(home, currentVersion) {
|
|
@@ -3792,7 +3933,7 @@ async function runDoctor(input) {
|
|
|
3792
3933
|
|
|
3793
3934
|
// src/commands/archive.ts
|
|
3794
3935
|
import { rename as rename4, mkdir as mkdir8, readFile as readFile16, writeFile as writeFile9 } from "fs/promises";
|
|
3795
|
-
import { join as join23, dirname as
|
|
3936
|
+
import { join as join23, dirname as dirname8 } from "path";
|
|
3796
3937
|
async function runArchive(input) {
|
|
3797
3938
|
const scan = await scanVault(input.vault);
|
|
3798
3939
|
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
@@ -3809,7 +3950,7 @@ async function runArchive(input) {
|
|
|
3809
3950
|
if (!relPath) return { exitCode: ExitCode.ARCHIVE_TARGET_NOT_FOUND, result: err("ARCHIVE_TARGET_NOT_FOUND", { page: input.page }) };
|
|
3810
3951
|
if (relPath.startsWith("_archive/")) return { exitCode: ExitCode.ARCHIVE_ALREADY_ARCHIVED, result: err("ARCHIVE_ALREADY_ARCHIVED", { page: relPath }) };
|
|
3811
3952
|
const archivePath = join23("_archive", relPath).replace(/\\/g, "/");
|
|
3812
|
-
await mkdir8(
|
|
3953
|
+
await mkdir8(dirname8(join23(input.vault, archivePath)), { recursive: true });
|
|
3813
3954
|
let indexUpdated = false;
|
|
3814
3955
|
if (!isRaw) {
|
|
3815
3956
|
const indexPath = join23(input.vault, "index.md");
|
|
@@ -4471,7 +4612,7 @@ async function runTranscripts(input) {
|
|
|
4471
4612
|
|
|
4472
4613
|
// src/commands/project-index.ts
|
|
4473
4614
|
import { readdir as readdir6, readFile as readFile18, writeFile as writeFile13, mkdir as mkdir9 } from "fs/promises";
|
|
4474
|
-
import { join as join27, dirname as
|
|
4615
|
+
import { join as join27, dirname as dirname9 } from "path";
|
|
4475
4616
|
var LAYER2_DIRS = ["entities", "concepts", "comparisons", "queries", "meta"];
|
|
4476
4617
|
async function runProjectIndex(input) {
|
|
4477
4618
|
const slug = input.slug;
|
|
@@ -4585,7 +4726,7 @@ Autogenerated by \`skillwiki project-index\` on ${today}.
|
|
|
4585
4726
|
}
|
|
4586
4727
|
if (input.apply) {
|
|
4587
4728
|
try {
|
|
4588
|
-
await mkdir9(
|
|
4729
|
+
await mkdir9(dirname9(indexPath), { recursive: true });
|
|
4589
4730
|
await writeFile13(indexPath, body, "utf8");
|
|
4590
4731
|
} catch (e) {
|
|
4591
4732
|
return {
|
|
@@ -5640,7 +5781,7 @@ async function runSyncPull(input) {
|
|
|
5640
5781
|
|
|
5641
5782
|
// src/commands/backup.ts
|
|
5642
5783
|
import { statSync as statSync4, readdirSync as readdirSync2, readFileSync as readFileSync9, mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
5643
|
-
import { join as join32, relative as relative3, dirname as
|
|
5784
|
+
import { join as join32, relative as relative3, dirname as dirname10 } from "path";
|
|
5644
5785
|
import { PutObjectCommand, HeadObjectCommand, ListObjectsV2Command, GetObjectCommand, DeleteObjectsCommand } from "@aws-sdk/client-s3";
|
|
5645
5786
|
|
|
5646
5787
|
// src/utils/s3-client.ts
|
|
@@ -5776,7 +5917,7 @@ async function runBackupRestore(input) {
|
|
|
5776
5917
|
const resp = await client.send(new GetObjectCommand({ Bucket: input.bucket, Key: obj.Key }));
|
|
5777
5918
|
const body = await resp.Body?.transformToByteArray();
|
|
5778
5919
|
if (body) {
|
|
5779
|
-
mkdirSync3(
|
|
5920
|
+
mkdirSync3(dirname10(localPath), { recursive: true });
|
|
5780
5921
|
writeFileSync4(localPath, Buffer.from(body));
|
|
5781
5922
|
downloaded++;
|
|
5782
5923
|
}
|
|
@@ -6461,10 +6602,10 @@ program.command("topic-map-check [vault]").description("check whether a topic ma
|
|
|
6461
6602
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
6462
6603
|
else emit(await runTopicMapCheck({ vault: v.vault, threshold: opts.threshold }), v.vault);
|
|
6463
6604
|
});
|
|
6464
|
-
program.command("stale [vault]").description("identify stale transcripts and incomplete work items").option("--archive", "move stale items to _archive/", false).option("--days <n>", "staleness threshold in days", (s) => parseInt(s, 10), 3).option("--force-scan", "infer kind/project from filename and content when frontmatter is missing", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
6605
|
+
program.command("stale [vault]").description("identify stale transcripts and incomplete work items").option("--archive", "move stale items to _archive/", false).option("--days <n>", "staleness threshold in days", (s) => parseInt(s, 10), 3).option("--force-scan", "infer kind/project from filename and content when frontmatter is missing", false).option("--project <slug>", "scope to a single project").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
6465
6606
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
6466
6607
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
6467
|
-
else emit(await runStale({ vault: v.vault, days: opts.days, archive: !!opts.archive, forceScan: !!opts.forceScan }), v.vault);
|
|
6608
|
+
else emit(await runStale({ vault: v.vault, days: opts.days, archive: !!opts.archive, forceScan: !!opts.forceScan, project: opts.project }), v.vault);
|
|
6468
6609
|
});
|
|
6469
6610
|
program.command("claim <transcript> [vault]").description("claim an unclaimed transcript by creating a work item with source: link").option("--project <slug>", "project slug (overrides transcript frontmatter)").option("--slug <slug>", "work-item slug (defaults to transcript filename without date/kind prefix)").option("--wiki <name>", "wiki profile name").action(async (transcript, vault, opts) => {
|
|
6470
6611
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
@@ -6481,7 +6622,7 @@ program.command("log-rotate [vault]").description("rotate or trim the vault log
|
|
|
6481
6622
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
6482
6623
|
else emit(await runLogRotate({ vault: v.vault, threshold: opts.threshold, apply: !!opts.apply }), v.vault);
|
|
6483
6624
|
});
|
|
6484
|
-
program.command("lint [vault]").description("run all vault health checks").option("--days <n>", "stale threshold", (s) => parseInt(s, 10), 90).option("--lines <n>", "pagesize threshold", (s) => parseInt(s, 10), 200).option("--log-threshold <n>", "log rotation threshold", (s) => parseInt(s, 10), 500).option("--fix", "auto-fix legacy_citation_style violations").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
6625
|
+
program.command("lint [vault]").description("run all vault health checks").option("--days <n>", "stale threshold", (s) => parseInt(s, 10), 90).option("--lines <n>", "pagesize threshold", (s) => parseInt(s, 10), 200).option("--log-threshold <n>", "log rotation threshold", (s) => parseInt(s, 10), 500).option("--fix", "auto-fix legacy_citation_style violations").option("--only <bucket>", "run only the specified lint bucket").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
6485
6626
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
6486
6627
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
6487
6628
|
else emit(await runLint({
|
|
@@ -6490,7 +6631,8 @@ program.command("lint [vault]").description("run all vault health checks").optio
|
|
|
6490
6631
|
days: opts.days,
|
|
6491
6632
|
lines: opts.lines,
|
|
6492
6633
|
logThreshold: opts.logThreshold,
|
|
6493
|
-
fix: opts.fix ?? false
|
|
6634
|
+
fix: opts.fix ?? false,
|
|
6635
|
+
only: opts.only
|
|
6494
6636
|
}), v.vault);
|
|
6495
6637
|
});
|
|
6496
6638
|
var configCmd = program.command("config").description("manage skillwiki configuration");
|
|
@@ -6597,7 +6739,10 @@ syncCmd.command("pull [vault]").description("pull remote vault changes and lint"
|
|
|
6597
6739
|
var backupCmd = program.command("backup").description("manage S3-compatible remote backup");
|
|
6598
6740
|
backupCmd.command("sync [vault]").description("sync vault to S3-compatible remote backup").option("--dry-run", "list actions without executing").option("--bucket <name>", "S3 bucket name").option("--endpoint <url>", "S3 endpoint URL").option("--region <region>", "S3 region", "us-east-1").option("--prune", "delete orphaned S3 objects not in vault", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
6599
6741
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
6600
|
-
if (!v.ok)
|
|
6742
|
+
if (!v.ok) {
|
|
6743
|
+
emit({ exitCode: v.exitCode, result: v.payload });
|
|
6744
|
+
return;
|
|
6745
|
+
}
|
|
6601
6746
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
|
|
6602
6747
|
const dotenv = await parseDotenvFile(configPath(home));
|
|
6603
6748
|
emit(await runBackupSync({
|
|
@@ -6613,7 +6758,10 @@ backupCmd.command("sync [vault]").description("sync vault to S3-compatible remot
|
|
|
6613
6758
|
});
|
|
6614
6759
|
backupCmd.command("restore [vault]").description("restore vault from S3-compatible remote backup").option("--bucket <name>", "S3 bucket name").option("--endpoint <url>", "S3 endpoint URL").option("--region <region>", "S3 region", "us-east-1").option("--target <dir>", "restore target directory (defaults to vault)").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
6615
6760
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
6616
|
-
if (!v.ok)
|
|
6761
|
+
if (!v.ok) {
|
|
6762
|
+
emit({ exitCode: v.exitCode, result: v.payload });
|
|
6763
|
+
return;
|
|
6764
|
+
}
|
|
6617
6765
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
|
|
6618
6766
|
const dotenv = await parseDotenvFile(configPath(home));
|
|
6619
6767
|
emit(await runBackupRestore({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skillwiki",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
4
4
|
"skills": "./",
|
|
5
5
|
"description": "Project-aware Karpathy-style knowledge base for Claude Code: 18 prompt-only skills (wiki-*, proj-*, using-skillwiki) backed by the deterministic `skillwiki` CLI.",
|
|
6
6
|
"author": {
|
package/skills/package.json
CHANGED
|
@@ -71,7 +71,7 @@ sha256: # computed by skillwiki hash over body bytes after closing ---
|
|
|
71
71
|
| `wiki-canvas` | Generate Obsidian Canvas visualization from vault graph data |
|
|
72
72
|
| `proj-decide` | Write an Architectural Decision Record (ADR) |
|
|
73
73
|
| `wiki-gate-plan-mode` | Toggle EnterPlanMode gating — force superpowers planning instead of built-in plan mode |
|
|
74
|
-
| `dev-loop
|
|
74
|
+
| `dev-loop:research` | Research agent for dev-loop IDLE — scans repo + vault health, outputs prioritized work-item recommendations (formerly `/dev-loop-research`) |
|
|
75
75
|
## CLI Backbone
|
|
76
76
|
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.
|
|
77
77
|
Key CLI subcommands: `init`, `lint`, `config`, `doctor`, `path`, `lang`, `install`, `graph build`, `archive`, `drift`, `compound`, `tag-sync`, `sync status`, `seed`, `stale`, `observe`, `canvas generate`.
|