skillwiki 0.2.1-beta.1 → 0.2.1-beta.11
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 +220 -34
- package/package.json +1 -2
- package/skills/.claude-plugin/plugin.json +2 -3
- package/skills/package.json +1 -2
- package/skills/using-skillwiki/SKILL.md +18 -4
- package/skills/wiki-add-task/SKILL.md +104 -0
package/dist/cli.js
CHANGED
|
@@ -878,6 +878,10 @@ function hasOrphanedCitations(body) {
|
|
|
878
878
|
}
|
|
879
879
|
return false;
|
|
880
880
|
}
|
|
881
|
+
function hasWikilinkCitations(body) {
|
|
882
|
+
const stripped = stripFences(body);
|
|
883
|
+
return /\[\[raw\/[^\]]+\]\]/.test(stripped);
|
|
884
|
+
}
|
|
881
885
|
|
|
882
886
|
// src/commands/audit.ts
|
|
883
887
|
async function runAudit(input) {
|
|
@@ -1172,9 +1176,12 @@ var VAULT_DIRS = [
|
|
|
1172
1176
|
"queries",
|
|
1173
1177
|
"meta",
|
|
1174
1178
|
"projects",
|
|
1175
|
-
".obsidian"
|
|
1179
|
+
".obsidian",
|
|
1180
|
+
"_Templates"
|
|
1176
1181
|
];
|
|
1177
1182
|
var ATTACHMENT_FOLDER = "raw/assets";
|
|
1183
|
+
var NEW_FILE_FOLDER = "raw/transcripts";
|
|
1184
|
+
var TEMPLATE_FOLDER = "_Templates";
|
|
1178
1185
|
function extractDomainFromSchema(text) {
|
|
1179
1186
|
const m = text.match(/^##\s+Domain\s*\n([\s\S]*?)(?=\n\n|\n##|\s*$)/m);
|
|
1180
1187
|
if (!m) return "";
|
|
@@ -1305,9 +1312,26 @@ async function runInit(input) {
|
|
|
1305
1312
|
});
|
|
1306
1313
|
if (err1) return err1;
|
|
1307
1314
|
const errObsidian = await writeOrPreserve(".obsidian/app.json", async () => {
|
|
1308
|
-
return JSON.stringify({ attachmentFolderPath: ATTACHMENT_FOLDER }, null, 2) + "\n";
|
|
1315
|
+
return JSON.stringify({ attachmentFolderPath: ATTACHMENT_FOLDER, newFileLocation: "folder", newFileFolderPath: NEW_FILE_FOLDER }, null, 2) + "\n";
|
|
1309
1316
|
});
|
|
1310
1317
|
if (errObsidian) return errObsidian;
|
|
1318
|
+
const errTemplatesJson = await writeOrPreserve(".obsidian/templates.json", async () => {
|
|
1319
|
+
return JSON.stringify({ folder: TEMPLATE_FOLDER }, null, 2) + "\n";
|
|
1320
|
+
});
|
|
1321
|
+
if (errTemplatesJson) return errTemplatesJson;
|
|
1322
|
+
const errTemplate = await writeOrPreserve(`${TEMPLATE_FOLDER}/tpl-ad-hoc-capture.md`, async () => {
|
|
1323
|
+
return [
|
|
1324
|
+
"---",
|
|
1325
|
+
"project: ",
|
|
1326
|
+
"tags: []",
|
|
1327
|
+
"priority: ",
|
|
1328
|
+
"ingested: {{date:YYYY-MM-DD}}",
|
|
1329
|
+
"---",
|
|
1330
|
+
"",
|
|
1331
|
+
""
|
|
1332
|
+
].join("\n");
|
|
1333
|
+
});
|
|
1334
|
+
if (errTemplate) return errTemplate;
|
|
1311
1335
|
const err22 = await writeOrPreserve("log.md", async () => {
|
|
1312
1336
|
const tpl = await readFile6(join7(input.templates, "log.md"), "utf8");
|
|
1313
1337
|
return tpl.replace(/\{\{INIT_DATE\}\}/g, today).replace("{{DOMAIN}}", domain).replace("{{WIKI_LANG}}", canonicalLang);
|
|
@@ -1354,7 +1378,8 @@ async function runInit(input) {
|
|
|
1354
1378
|
env_skipped: skipEnv,
|
|
1355
1379
|
imported_from_hermes: importedFromHermes,
|
|
1356
1380
|
discovered_tags,
|
|
1357
|
-
humanHint
|
|
1381
|
+
humanHint,
|
|
1382
|
+
templates_created: created.includes(`${TEMPLATE_FOLDER}/tpl-ad-hoc-capture.md`)
|
|
1358
1383
|
})
|
|
1359
1384
|
};
|
|
1360
1385
|
}
|
|
@@ -1571,6 +1596,9 @@ Chronological action log. Newest entries last. Skill writes append entries; lint
|
|
|
1571
1596
|
return { exitCode: ExitCode.OK, result: ok({ entries, threshold: input.threshold, rotated: true, rotated_to: rotatedName, humanHint: `rotated ${entries} entries to ${rotatedName}` }) };
|
|
1572
1597
|
}
|
|
1573
1598
|
|
|
1599
|
+
// src/commands/lint.ts
|
|
1600
|
+
import { readFile as readFile12, writeFile as writeFile6 } from "fs/promises";
|
|
1601
|
+
|
|
1574
1602
|
// src/commands/topic-map-check.ts
|
|
1575
1603
|
var DEFAULT_THRESHOLD = 200;
|
|
1576
1604
|
async function runTopicMapCheck(input) {
|
|
@@ -1704,9 +1732,11 @@ function hasDuplicateFrontmatter(body) {
|
|
|
1704
1732
|
}
|
|
1705
1733
|
var ERROR_ORDER = ["broken_wikilinks", "invalid_frontmatter", "raw_dedup", "tag_not_in_taxonomy"];
|
|
1706
1734
|
var WARNING_ORDER = ["index_incomplete", "index_link_format", "stale_page", "page_too_large", "log_rotate_needed", "orphans", "legacy_citation_style", "orphaned_citations", "duplicate_frontmatter", "missing_overview"];
|
|
1707
|
-
var INFO_ORDER = ["bridges", "page_structure", "topic_map_recommended", "frontmatter_wikilink"];
|
|
1735
|
+
var INFO_ORDER = ["bridges", "page_structure", "topic_map_recommended", "frontmatter_wikilink", "wikilink_citation"];
|
|
1708
1736
|
async function runLint(input) {
|
|
1709
1737
|
const buckets = {};
|
|
1738
|
+
const fixed = [];
|
|
1739
|
+
const unresolved = [];
|
|
1710
1740
|
const links = await runLinks({ vault: input.vault });
|
|
1711
1741
|
if (links.result.ok && links.result.data.broken.length > 0) buckets.broken_wikilinks = links.result.data.broken;
|
|
1712
1742
|
if (!links.result.ok && links.result.error === "INVALID_FRONTMATTER") {
|
|
@@ -1757,6 +1787,7 @@ async function runLint(input) {
|
|
|
1757
1787
|
const dupFrontmatter = [];
|
|
1758
1788
|
const noOverview = [];
|
|
1759
1789
|
const fmWikilinkFlags = [];
|
|
1790
|
+
const wikilinkCitationFlags = [];
|
|
1760
1791
|
for (const page of scan.data.typedKnowledge) {
|
|
1761
1792
|
const text = await readPage(page);
|
|
1762
1793
|
const split = splitFrontmatter(text);
|
|
@@ -1766,6 +1797,7 @@ async function runLint(input) {
|
|
|
1766
1797
|
if (hasDuplicateFrontmatter(body)) dupFrontmatter.push(page.relPath);
|
|
1767
1798
|
if (isLegacyCitationStyle(body)) legacyPages.push(page.relPath);
|
|
1768
1799
|
if (hasOrphanedCitations(body)) orphanedPages.push(page.relPath);
|
|
1800
|
+
if (hasWikilinkCitations(body)) wikilinkCitationFlags.push(page.relPath);
|
|
1769
1801
|
const fmLinks = rawFm.match(/\[\[([^\[\]|]+)(?:\|[^\[\]]*)?\]\]/g) ?? [];
|
|
1770
1802
|
for (const link of fmLinks) {
|
|
1771
1803
|
const target = link.replace(/^\[\[/, "").replace(/(?:\|[^\[\]]*)?\]\]$/, "").trim();
|
|
@@ -1794,6 +1826,99 @@ async function runLint(input) {
|
|
|
1794
1826
|
if (dupFrontmatter.length > 0) buckets.duplicate_frontmatter = dupFrontmatter;
|
|
1795
1827
|
if (noOverview.length > 0) buckets.missing_overview = noOverview;
|
|
1796
1828
|
if (fmWikilinkFlags.length > 0) buckets.frontmatter_wikilink = fmWikilinkFlags;
|
|
1829
|
+
if (wikilinkCitationFlags.length > 0) buckets.wikilink_citation = wikilinkCitationFlags;
|
|
1830
|
+
if (input.fix && legacyPages.length > 0) {
|
|
1831
|
+
const FENCE_RE2 = /```[\s\S]*?```/g;
|
|
1832
|
+
const INLINE_MARKER = /\^\[raw\/[^\]]+\]/g;
|
|
1833
|
+
for (const relPath of legacyPages) {
|
|
1834
|
+
try {
|
|
1835
|
+
const absPath = `${input.vault}/${relPath}`;
|
|
1836
|
+
const raw = await readFile12(absPath, "utf8");
|
|
1837
|
+
const split = splitFrontmatter(raw);
|
|
1838
|
+
if (!split.ok) {
|
|
1839
|
+
unresolved.push(relPath);
|
|
1840
|
+
continue;
|
|
1841
|
+
}
|
|
1842
|
+
const body = split.data.body;
|
|
1843
|
+
const rawFm = split.data.rawFrontmatter;
|
|
1844
|
+
const stripped = body.replace(FENCE_RE2, "");
|
|
1845
|
+
const lines = stripped.split("\n");
|
|
1846
|
+
const inlineMarkers = [];
|
|
1847
|
+
let inSources = false;
|
|
1848
|
+
for (const line of lines) {
|
|
1849
|
+
if (/^## Sources\b/.test(line.trim())) {
|
|
1850
|
+
inSources = true;
|
|
1851
|
+
continue;
|
|
1852
|
+
}
|
|
1853
|
+
if (inSources) continue;
|
|
1854
|
+
for (const m of line.matchAll(INLINE_MARKER)) {
|
|
1855
|
+
inlineMarkers.push(m[0]);
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
if (inlineMarkers.length === 0) {
|
|
1859
|
+
unresolved.push(relPath);
|
|
1860
|
+
continue;
|
|
1861
|
+
}
|
|
1862
|
+
const bodyLines = body.split("\n");
|
|
1863
|
+
let inSrc = false;
|
|
1864
|
+
const newBodyLines = [];
|
|
1865
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1866
|
+
for (const line of bodyLines) {
|
|
1867
|
+
if (/^## Sources\b/.test(line.trim())) {
|
|
1868
|
+
inSrc = true;
|
|
1869
|
+
newBodyLines.push(line);
|
|
1870
|
+
continue;
|
|
1871
|
+
}
|
|
1872
|
+
if (inSrc) {
|
|
1873
|
+
newBodyLines.push(line);
|
|
1874
|
+
continue;
|
|
1875
|
+
}
|
|
1876
|
+
const lineWithoutMarkers = line.replace(INLINE_MARKER, "").trim();
|
|
1877
|
+
if (lineWithoutMarkers.length === 0 && INLINE_MARKER.test(line)) {
|
|
1878
|
+
continue;
|
|
1879
|
+
}
|
|
1880
|
+
let cleaned = line;
|
|
1881
|
+
for (const marker of inlineMarkers) {
|
|
1882
|
+
if (seen.has(marker)) continue;
|
|
1883
|
+
const escapedMarker = marker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1884
|
+
const trailingRe = new RegExp(`([.!?]\\s*)${escapedMarker}`);
|
|
1885
|
+
if (trailingRe.test(cleaned)) {
|
|
1886
|
+
cleaned = cleaned.replace(trailingRe, "$1");
|
|
1887
|
+
seen.add(marker);
|
|
1888
|
+
}
|
|
1889
|
+
const midRe = new RegExp(`${escapedMarker}\\s*`);
|
|
1890
|
+
if (!seen.has(marker) && midRe.test(cleaned)) {
|
|
1891
|
+
cleaned = cleaned.replace(midRe, "");
|
|
1892
|
+
seen.add(marker);
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
newBodyLines.push(cleaned);
|
|
1896
|
+
}
|
|
1897
|
+
let newBody = newBodyLines.join("\n");
|
|
1898
|
+
const dedupedMarkers = [...new Set(inlineMarkers)];
|
|
1899
|
+
const sourceLines = dedupedMarkers.map((m) => `- ${m}`);
|
|
1900
|
+
if (inSrc) {
|
|
1901
|
+
newBody = newBody.trimEnd() + "\n" + sourceLines.join("\n") + "\n";
|
|
1902
|
+
} else {
|
|
1903
|
+
newBody = newBody.trimEnd() + "\n\n## Sources\n\n" + sourceLines.join("\n") + "\n";
|
|
1904
|
+
}
|
|
1905
|
+
const newContent = `---
|
|
1906
|
+
${rawFm}
|
|
1907
|
+
---
|
|
1908
|
+
${newBody}`;
|
|
1909
|
+
await writeFile6(absPath, newContent, "utf8");
|
|
1910
|
+
fixed.push(relPath);
|
|
1911
|
+
} catch {
|
|
1912
|
+
unresolved.push(relPath);
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
if (fixed.length > 0) {
|
|
1916
|
+
const fixedSet = new Set(fixed);
|
|
1917
|
+
const remaining = legacyPages.filter((p) => !fixedSet.has(p));
|
|
1918
|
+
if (remaining.length > 0) buckets.legacy_citation_style = remaining;
|
|
1919
|
+
else delete buckets.legacy_citation_style;
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1797
1922
|
}
|
|
1798
1923
|
const errorOut = ERROR_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
|
|
1799
1924
|
const warningOut = WARNING_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
|
|
@@ -1821,13 +1946,15 @@ async function runLint(input) {
|
|
|
1821
1946
|
vault: { path: input.vault, source: input.source ?? "resolved" },
|
|
1822
1947
|
summary,
|
|
1823
1948
|
by_severity: { error: errorOut, warning: warningOut, info: infoOut },
|
|
1949
|
+
fixed,
|
|
1950
|
+
unresolved,
|
|
1824
1951
|
humanHint: hintLines.join("\n")
|
|
1825
1952
|
})
|
|
1826
1953
|
};
|
|
1827
1954
|
}
|
|
1828
1955
|
|
|
1829
1956
|
// src/commands/config.ts
|
|
1830
|
-
import { readFile as
|
|
1957
|
+
import { readFile as readFile13 } from "fs/promises";
|
|
1831
1958
|
import { existsSync } from "fs";
|
|
1832
1959
|
import { join as join14 } from "path";
|
|
1833
1960
|
function validateKey(key) {
|
|
@@ -1852,7 +1979,7 @@ async function runConfigSet(input) {
|
|
|
1852
1979
|
try {
|
|
1853
1980
|
let originalContent;
|
|
1854
1981
|
try {
|
|
1855
|
-
originalContent = await
|
|
1982
|
+
originalContent = await readFile13(filePath, "utf8");
|
|
1856
1983
|
} catch {
|
|
1857
1984
|
}
|
|
1858
1985
|
const existing = originalContent !== void 0 ? parseDotenvText(originalContent) : {};
|
|
@@ -2165,34 +2292,40 @@ async function runDoctor(input) {
|
|
|
2165
2292
|
}
|
|
2166
2293
|
|
|
2167
2294
|
// src/commands/archive.ts
|
|
2168
|
-
import { rename as rename3, mkdir as mkdir5, readFile as
|
|
2295
|
+
import { rename as rename3, mkdir as mkdir5, readFile as readFile14, writeFile as writeFile7 } from "fs/promises";
|
|
2169
2296
|
import { join as join18, dirname as dirname7 } from "path";
|
|
2170
2297
|
async function runArchive(input) {
|
|
2171
2298
|
const scan = await scanVault(input.vault);
|
|
2172
2299
|
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
}
|
|
2177
|
-
|
|
2300
|
+
const lookup = (pages) => {
|
|
2301
|
+
if (input.page.includes("/")) return pages.find((p) => p.relPath === input.page)?.relPath;
|
|
2302
|
+
return pages.find((p) => p.relPath.replace(/\.md$/, "").split("/").pop() === input.page)?.relPath;
|
|
2303
|
+
};
|
|
2304
|
+
let relPath = lookup(scan.data.typedKnowledge);
|
|
2305
|
+
let isRaw = false;
|
|
2306
|
+
if (!relPath) {
|
|
2307
|
+
relPath = lookup(scan.data.raw);
|
|
2308
|
+
isRaw = relPath != null;
|
|
2178
2309
|
}
|
|
2179
2310
|
if (!relPath) return { exitCode: ExitCode.ARCHIVE_TARGET_NOT_FOUND, result: err("ARCHIVE_TARGET_NOT_FOUND", { page: input.page }) };
|
|
2180
2311
|
if (relPath.startsWith("_archive/")) return { exitCode: ExitCode.ARCHIVE_ALREADY_ARCHIVED, result: err("ARCHIVE_ALREADY_ARCHIVED", { page: relPath }) };
|
|
2181
2312
|
const archivePath = join18("_archive", relPath);
|
|
2182
2313
|
await mkdir5(dirname7(join18(input.vault, archivePath)), { recursive: true });
|
|
2183
2314
|
let indexUpdated = false;
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2315
|
+
if (!isRaw) {
|
|
2316
|
+
const indexPath = join18(input.vault, "index.md");
|
|
2317
|
+
try {
|
|
2318
|
+
const idx = await readFile14(indexPath, "utf8");
|
|
2319
|
+
const slug = relPath.replace(/\.md$/, "").split("/").pop();
|
|
2320
|
+
const originalLines = idx.split("\n");
|
|
2321
|
+
const filtered = originalLines.filter((l) => !l.includes(`[[${slug}]]`));
|
|
2322
|
+
if (filtered.length !== originalLines.length) {
|
|
2323
|
+
await writeFile7(indexPath, filtered.join("\n"), "utf8");
|
|
2324
|
+
indexUpdated = true;
|
|
2325
|
+
}
|
|
2326
|
+
} catch (e) {
|
|
2327
|
+
if (e?.code !== "ENOENT") throw e;
|
|
2193
2328
|
}
|
|
2194
|
-
} catch (e) {
|
|
2195
|
-
if (e?.code !== "ENOENT") throw e;
|
|
2196
2329
|
}
|
|
2197
2330
|
await rename3(join18(input.vault, relPath), join18(input.vault, archivePath));
|
|
2198
2331
|
return { exitCode: ExitCode.OK, result: ok({ archived_from: relPath, archived_to: archivePath, index_updated: indexUpdated, humanHint: `${relPath} -> ${archivePath}${indexUpdated ? " (index updated)" : ""}` }) };
|
|
@@ -2200,7 +2333,7 @@ async function runArchive(input) {
|
|
|
2200
2333
|
|
|
2201
2334
|
// src/commands/drift.ts
|
|
2202
2335
|
import { createHash as createHash2 } from "crypto";
|
|
2203
|
-
import { writeFile as
|
|
2336
|
+
import { writeFile as writeFile8 } from "fs/promises";
|
|
2204
2337
|
|
|
2205
2338
|
// src/utils/fetch.ts
|
|
2206
2339
|
async function controlledFetch(url, opts) {
|
|
@@ -2241,16 +2374,31 @@ async function runDrift(input) {
|
|
|
2241
2374
|
const scan = await scanVault(input.vault);
|
|
2242
2375
|
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
2243
2376
|
const results = [];
|
|
2377
|
+
const newResults = [];
|
|
2244
2378
|
for (const raw of scan.data.raw) {
|
|
2245
2379
|
const text = await readPage(raw);
|
|
2246
2380
|
const split = splitFrontmatter(text);
|
|
2247
2381
|
if (!split.ok) continue;
|
|
2248
2382
|
const { rawFrontmatter, body } = split.data;
|
|
2383
|
+
const ingestedMatch = rawFrontmatter.match(/^ingested:\s*(.+)$/m);
|
|
2384
|
+
const ingestedRaw = ingestedMatch?.[1]?.trim() ?? "";
|
|
2385
|
+
const ingested = ingestedRaw.replace(/^["']|["']$/g, "");
|
|
2386
|
+
if (input.newSince && ingested && ingested >= input.newSince) {
|
|
2387
|
+
newResults.push({
|
|
2388
|
+
raw_path: raw.relPath,
|
|
2389
|
+
source_url: "",
|
|
2390
|
+
stored_sha256: "",
|
|
2391
|
+
current_sha256: null,
|
|
2392
|
+
status: "new",
|
|
2393
|
+
ingested
|
|
2394
|
+
});
|
|
2395
|
+
}
|
|
2249
2396
|
const sourceUrlMatch = rawFrontmatter.match(/^source_url:\s*(.+)$/m);
|
|
2250
2397
|
const storedHashMatch = rawFrontmatter.match(/^sha256:\s*([a-f0-9]+)$/m);
|
|
2251
2398
|
if (!sourceUrlMatch || !storedHashMatch) continue;
|
|
2252
2399
|
const sourceUrl = sourceUrlMatch[1].trim();
|
|
2253
2400
|
const storedHash = storedHashMatch[1];
|
|
2401
|
+
if (!sourceUrl.startsWith("http://") && !sourceUrl.startsWith("https://")) continue;
|
|
2254
2402
|
const resp = await doFetch(sourceUrl, FETCH_OPTS);
|
|
2255
2403
|
if (!resp.ok) {
|
|
2256
2404
|
results.push({
|
|
@@ -2271,7 +2419,7 @@ async function runDrift(input) {
|
|
|
2271
2419
|
${newFm}
|
|
2272
2420
|
---
|
|
2273
2421
|
${body}`;
|
|
2274
|
-
await
|
|
2422
|
+
await writeFile8(raw.absPath, newText, "utf8");
|
|
2275
2423
|
results.push({
|
|
2276
2424
|
raw_path: raw.relPath,
|
|
2277
2425
|
source_url: sourceUrl,
|
|
@@ -2295,17 +2443,18 @@ ${body}`;
|
|
|
2295
2443
|
const unchanged = results.filter((r) => r.status === "unchanged").length;
|
|
2296
2444
|
const exitCode = drifted.length > 0 ? ExitCode.DRIFT_DETECTED : ExitCode.OK;
|
|
2297
2445
|
const hintLines = [`scanned: ${results.length}, unchanged: ${unchanged}`];
|
|
2446
|
+
if (newResults.length > 0) hintLines.push(`new: ${newResults.length}`, ...newResults.map((n) => ` ${n.raw_path} (ingested: ${n.ingested})`));
|
|
2298
2447
|
if (drifted.length > 0) hintLines.push(`drifted: ${drifted.length}`, ...drifted.map((d) => ` ${d.raw_path}`));
|
|
2299
2448
|
if (fetchFailed.length > 0) hintLines.push(`fetch_failed: ${fetchFailed.length}`, ...fetchFailed.map((f) => ` ${f.raw_path}: ${f.fetch_error}`));
|
|
2300
2449
|
if (updated.length > 0) hintLines.push(`updated: ${updated.length}`, ...updated.map((u) => ` ${u.raw_path}`));
|
|
2301
2450
|
return {
|
|
2302
2451
|
exitCode,
|
|
2303
|
-
result: ok({ scanned: results.length, drifted, fetch_failed: fetchFailed, updated, unchanged, humanHint: hintLines.join("\n") })
|
|
2452
|
+
result: ok({ scanned: results.length, drifted, fetch_failed: fetchFailed, updated, newFiles: newResults, unchanged, humanHint: hintLines.join("\n") })
|
|
2304
2453
|
};
|
|
2305
2454
|
}
|
|
2306
2455
|
|
|
2307
2456
|
// src/commands/migrate-citations.ts
|
|
2308
|
-
import { writeFile as
|
|
2457
|
+
import { writeFile as writeFile9 } from "fs/promises";
|
|
2309
2458
|
var MARKER_RE2 = /\^\[(raw\/[^\]]+)\]/g;
|
|
2310
2459
|
function moveMarkersToParagraphEnd(body) {
|
|
2311
2460
|
const lines = body.split("\n");
|
|
@@ -2428,7 +2577,7 @@ ${migratedBody}${newFooter}`;
|
|
|
2428
2577
|
continue;
|
|
2429
2578
|
}
|
|
2430
2579
|
if (!input.dryRun) {
|
|
2431
|
-
await
|
|
2580
|
+
await writeFile9(page.absPath, newText, "utf8");
|
|
2432
2581
|
}
|
|
2433
2582
|
migrated.push(page.relPath);
|
|
2434
2583
|
}
|
|
@@ -2450,7 +2599,7 @@ ${migratedBody}${newFooter}`;
|
|
|
2450
2599
|
}
|
|
2451
2600
|
|
|
2452
2601
|
// src/commands/frontmatter-fix.ts
|
|
2453
|
-
import { writeFile as
|
|
2602
|
+
import { writeFile as writeFile10 } from "fs/promises";
|
|
2454
2603
|
function isoToday() {
|
|
2455
2604
|
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2456
2605
|
}
|
|
@@ -2492,7 +2641,7 @@ ${newBody}`;
|
|
|
2492
2641
|
continue;
|
|
2493
2642
|
}
|
|
2494
2643
|
if (!input.dryRun) {
|
|
2495
|
-
await
|
|
2644
|
+
await writeFile10(page.absPath, newText, "utf8");
|
|
2496
2645
|
}
|
|
2497
2646
|
fixed.push(page.relPath);
|
|
2498
2647
|
}
|
|
@@ -2575,6 +2724,37 @@ async function runUpdate(input) {
|
|
|
2575
2724
|
};
|
|
2576
2725
|
}
|
|
2577
2726
|
|
|
2727
|
+
// src/commands/transcripts.ts
|
|
2728
|
+
import { readdir as readdir4, stat as stat6, readFile as readFile15 } from "fs/promises";
|
|
2729
|
+
import { join as join19 } from "path";
|
|
2730
|
+
async function runTranscripts(input) {
|
|
2731
|
+
const dir = join19(input.vault, "raw", "transcripts");
|
|
2732
|
+
let entries;
|
|
2733
|
+
try {
|
|
2734
|
+
entries = await readdir4(dir, { withFileTypes: true });
|
|
2735
|
+
} catch {
|
|
2736
|
+
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: { ok: false, error: "VAULT_PATH_INVALID", detail: `raw/transcripts/ not found: ${dir}` } };
|
|
2737
|
+
}
|
|
2738
|
+
const transcripts = [];
|
|
2739
|
+
for (const entry of entries) {
|
|
2740
|
+
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
2741
|
+
const filePath = join19(dir, entry.name);
|
|
2742
|
+
const content = await readFile15(filePath, "utf8");
|
|
2743
|
+
const fm = extractFrontmatter(content);
|
|
2744
|
+
if (!fm.ok) continue;
|
|
2745
|
+
const ingested = typeof fm.data.ingested === "string" ? fm.data.ingested : "";
|
|
2746
|
+
if (input.since && ingested && ingested < input.since) continue;
|
|
2747
|
+
const s = await stat6(filePath);
|
|
2748
|
+
transcripts.push({
|
|
2749
|
+
file: `raw/transcripts/${entry.name}`,
|
|
2750
|
+
ingested,
|
|
2751
|
+
size: s.size
|
|
2752
|
+
});
|
|
2753
|
+
}
|
|
2754
|
+
const hint = transcripts.length > 0 ? transcripts.map((t) => `${t.file} (ingested: ${t.ingested || "unknown"}, ${t.size}B)`).join("\n") : "no transcript files found";
|
|
2755
|
+
return { exitCode: ExitCode.OK, result: ok({ transcripts, humanHint: hint }) };
|
|
2756
|
+
}
|
|
2757
|
+
|
|
2578
2758
|
// src/cli.ts
|
|
2579
2759
|
var pkg = JSON.parse(readFileSync5(new URL("../package.json", import.meta.url), "utf8"));
|
|
2580
2760
|
var program = new Command();
|
|
@@ -2682,7 +2862,7 @@ program.command("log-rotate [vault]").option("--threshold <n>", "entry count thr
|
|
|
2682
2862
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
2683
2863
|
else emit(await runLogRotate({ vault: v.vault, threshold: opts.threshold, apply: !!opts.apply }));
|
|
2684
2864
|
});
|
|
2685
|
-
program.command("lint [vault]").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("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
2865
|
+
program.command("lint [vault]").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) => {
|
|
2686
2866
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
2687
2867
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
2688
2868
|
else emit(await runLint({
|
|
@@ -2690,7 +2870,8 @@ program.command("lint [vault]").option("--days <n>", "stale threshold", (s) => p
|
|
|
2690
2870
|
source: vault ? "flag" : void 0,
|
|
2691
2871
|
days: opts.days,
|
|
2692
2872
|
lines: opts.lines,
|
|
2693
|
-
logThreshold: opts.logThreshold
|
|
2873
|
+
logThreshold: opts.logThreshold,
|
|
2874
|
+
fix: opts.fix ?? false
|
|
2694
2875
|
}));
|
|
2695
2876
|
});
|
|
2696
2877
|
var configCmd = program.command("config").description("manage skillwiki configuration");
|
|
@@ -2710,10 +2891,10 @@ program.command("archive <page> [vault]").description("archive a typed-knowledge
|
|
|
2710
2891
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
2711
2892
|
else emit(await runArchive({ vault: v.vault, page }));
|
|
2712
2893
|
});
|
|
2713
|
-
program.command("drift [vault]").description("detect content drift in raw sources").option("--apply", "update sha256 in drifted sources").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
2894
|
+
program.command("drift [vault]").description("detect content drift in raw sources").option("--apply", "update sha256 in drifted sources").option("--new <date>", "list raw files ingested on/after this date (YYYY-MM-DD)").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
2714
2895
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
2715
2896
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
2716
|
-
else emit(await runDrift({ vault: v.vault, apply: opts.apply }));
|
|
2897
|
+
else emit(await runDrift({ vault: v.vault, apply: opts.apply, newSince: opts.new }));
|
|
2717
2898
|
});
|
|
2718
2899
|
program.command("dedup [vault]").description("detect duplicate raw sources by sha256").option("--apply", "rewire citations and remove duplicate raw files", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
2719
2900
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
@@ -2734,6 +2915,11 @@ program.command("update").description("update skillwiki CLI to the latest versio
|
|
|
2734
2915
|
home: process.env.HOME ?? "",
|
|
2735
2916
|
distTag: opts.tag
|
|
2736
2917
|
})));
|
|
2918
|
+
program.command("transcripts [vault]").description("list transcript files in raw/transcripts/").option("--since <date>", "only files ingested on or after this date (YYYY-MM-DD)").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
2919
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
2920
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
2921
|
+
else emit(await runTranscripts({ vault: v.vault, since: opts.since }));
|
|
2922
|
+
});
|
|
2737
2923
|
triggerAutoUpdate(process.env.HOME ?? "", pkg.version);
|
|
2738
2924
|
program.parseAsync(process.argv).catch((e) => {
|
|
2739
2925
|
process.stdout.write(JSON.stringify({ ok: false, error: "INTERNAL", detail: { message: String(e) } }) + "\n");
|
package/package.json
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skillwiki",
|
|
3
|
-
"version": "0.2.1-beta.
|
|
4
|
-
"version": "0.2.1-beta.1",
|
|
3
|
+
"version": "0.2.1-beta.11",
|
|
5
4
|
"skills": "./",
|
|
6
|
-
"description": "Project-aware Karpathy-style knowledge base for Claude Code:
|
|
5
|
+
"description": "Project-aware Karpathy-style knowledge base for Claude Code: 15 prompt-only skills (wiki-*, proj-*, using-skillwiki) backed by the deterministic `skillwiki` CLI.",
|
|
7
6
|
"author": {
|
|
8
7
|
"name": "karlorz",
|
|
9
8
|
"url": "https://github.com/karlorz"
|
package/skills/package.json
CHANGED
|
@@ -20,6 +20,7 @@ Invoke a skillwiki skill when the user:
|
|
|
20
20
|
- Wants a health check or lint on their vault
|
|
21
21
|
- Mentions crystallizing a session into a note
|
|
22
22
|
- Talks about project workspaces, ADRs, or distillation
|
|
23
|
+
- Wants to quickly capture an idea, bug, task, or note without interrupting their workflow
|
|
23
24
|
- Wants to archive or clean up old vault pages
|
|
24
25
|
- Needs to detect source drift or re-ingest updated content
|
|
25
26
|
- Has a spec/plan in a non-skillwiki format (CodeStable, RFC, AIDE)
|
|
@@ -27,15 +28,15 @@ Invoke a skillwiki skill when the user:
|
|
|
27
28
|
|
|
28
29
|
## Vault Structure
|
|
29
30
|
|
|
30
|
-
A skillwiki vault has
|
|
31
|
+
A skillwiki vault has three layers. The canonical architecture lives in `SCHEMA.md` at the vault root — read it before creating any new directories.
|
|
31
32
|
|
|
32
|
-
**Layer 1 — Raw (`raw/`):** Immutable source material. Never modify after ingest.
|
|
33
|
+
**Layer 1 — Raw (`raw/`):** Immutable source material. Never modify after ingest. `raw/transcripts/` doubles as the ad-hoc capture point for meeting notes and unprocessed ideas.
|
|
33
34
|
|
|
34
35
|
```
|
|
35
36
|
raw/
|
|
36
37
|
├── articles/ # Web articles, clippings
|
|
37
38
|
├── papers/ # PDFs, arxiv papers
|
|
38
|
-
├── transcripts/ # Meeting notes, interviews
|
|
39
|
+
├── transcripts/ # Meeting notes, interviews, ad-hoc captures
|
|
39
40
|
└── assets/ # Images, diagrams referenced by sources
|
|
40
41
|
```
|
|
41
42
|
|
|
@@ -48,7 +49,19 @@ sha256: # computed by skillwiki hash over body bytes after closing ---
|
|
|
48
49
|
---
|
|
49
50
|
```
|
|
50
51
|
|
|
51
|
-
**Layer 2 —
|
|
52
|
+
**Layer 2 — Typed Knowledge:** `entities/`, `concepts/`, `comparisons/`, `queries/`, `meta/`. Agent-owned pages with `^[raw/...]` citation markers at paragraph-end. Global scope — project association via `provenance_projects:` frontmatter, not directory nesting.
|
|
53
|
+
|
|
54
|
+
**Layer 3 — Project Workspaces (`projects/{slug}/`):** Per-project lifecycle directories with `work/` (spec + plan + retro), `compound/` (distilled lessons/patterns), `architecture/` (ADRs), and `history/` (archived specs/plans).
|
|
55
|
+
|
|
56
|
+
**No `inbox/` directory.** Ad-hoc captures go to `raw/transcripts/` or directly into a project work item via `proj-work`. Do not invent new top-level directories — extend Layer 2 via SCHEMA.md tag taxonomy if needed.
|
|
57
|
+
|
|
58
|
+
### Ad-hoc capture: three entry points
|
|
59
|
+
|
|
60
|
+
| Entry | When | What happens |
|
|
61
|
+
|-------|------|-------------|
|
|
62
|
+
| `/wiki-add-task <text>` | You're in a Claude session | Appends entry to `raw/transcripts/YYYY-MM-DD-ad-hoc-captures.md` |
|
|
63
|
+
| Filesystem drop | You're NOT in a Claude session (Obsidian, editor, sync) | Create/edit any `.md` file in `raw/transcripts/` — dev-loop discovers it on next cycle |
|
|
64
|
+
| Dev-loop discovery | Automatic, next cycle | Scans `raw/transcripts/` for new files since last cycle, surfaces as claimable work |
|
|
52
65
|
|
|
53
66
|
## Skill Map
|
|
54
67
|
|
|
@@ -62,6 +75,7 @@ sha256: # computed by skillwiki hash over body bytes after closing ---
|
|
|
62
75
|
| `wiki-audit` | Verify raw provenance references and source frontmatter integrity |
|
|
63
76
|
| `wiki-archive` | Archive a typed-knowledge page — move to `_archive/`, remove from index |
|
|
64
77
|
| `wiki-reingest` | Detect drift in raw sources (sha256 comparison) and re-ingest updated content |
|
|
78
|
+
| `wiki-add-task` | Quick-capture ideas, bugs, tasks, notes into `raw/transcripts/` without leaving the current workflow |
|
|
65
79
|
| `wiki-adapter-prd` | Map foreign PRD formats (CodeStable, RFC, AIDE, Hermes) into vault pages |
|
|
66
80
|
| `proj-init` | Bootstrap a project workspace (README, requirements, architecture) |
|
|
67
81
|
| `proj-work` | Open or run a work item under a project's work/ directory |
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wiki-add-task
|
|
3
|
+
description: Capture ad-hoc ideas, bugs, tasks, or notes into the vault via /wiki-add-task or filesystem drop.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# wiki-add-task
|
|
7
|
+
|
|
8
|
+
Capture ad-hoc ideas, bugs, tasks, and notes into the vault. Three entry points depending on where you are:
|
|
9
|
+
|
|
10
|
+
| Entry | When | What happens |
|
|
11
|
+
|-------|------|-------------|
|
|
12
|
+
| `/wiki-add-task <text>` | You're in a Claude session | Appends entry to `raw/transcripts/YYYY-MM-DD-ad-hoc-captures.md` |
|
|
13
|
+
| Filesystem drop | You're NOT in a Claude session (Obsidian, editor, sync) | Create/edit any file in `raw/transcripts/` — dev-loop discovers it on next cycle |
|
|
14
|
+
| Dev-loop discovery | Automatic, next cycle | Scans `raw/transcripts/` for new files since last cycle, surfaces as claimable work |
|
|
15
|
+
|
|
16
|
+
## When This Skill Activates
|
|
17
|
+
|
|
18
|
+
- User invokes `/wiki-add-task` with a description.
|
|
19
|
+
- User says "add task", "capture this", "note this", "remember this", "log this idea", or similar.
|
|
20
|
+
- User provides a short text description and optionally a type tag.
|
|
21
|
+
|
|
22
|
+
## Output language
|
|
23
|
+
|
|
24
|
+
Run `skillwiki lang` at the start. Entry prose and `--human` summaries use the resolved language. Frontmatter keys, file names, and structural markers stay English.
|
|
25
|
+
|
|
26
|
+
## Steps
|
|
27
|
+
|
|
28
|
+
0. **Resolve vault and language.** Run `skillwiki path` (fail if NO_VAULT_CONFIGURED) and `skillwiki lang`.
|
|
29
|
+
1. **Parse arguments.** Extract from the user's message:
|
|
30
|
+
- `text` — the idea/bug/task/note content (required)
|
|
31
|
+
- `type` — one of: `idea`, `bug`, `task`, `note` (default: `idea`)
|
|
32
|
+
- `project` — optional project slug to cross-reference (e.g., `llm-wiki`)
|
|
33
|
+
2. **Determine target file.** The capture file is `raw/transcripts/YYYY-MM-DD-ad-hoc-captures.md` where YYYY-MM-DD is today's date. If the file exists, append; otherwise create it with standard raw frontmatter.
|
|
34
|
+
3. **Write the entry.** Append to the capture file:
|
|
35
|
+
```markdown
|
|
36
|
+
### HH:MM — [type]
|
|
37
|
+
|
|
38
|
+
[text]
|
|
39
|
+
|
|
40
|
+
<!---meta: {"captured_at": "YYYY-MM-DDTHH:MM:SS", "type": "[type]"}--->
|
|
41
|
+
```
|
|
42
|
+
- Use 24-hour time for HH:MM.
|
|
43
|
+
- Do not overwrite or modify existing entries.
|
|
44
|
+
4. **Cross-reference (optional).** If a `project` slug was provided:
|
|
45
|
+
- Check that `projects/{slug}/` exists in the vault.
|
|
46
|
+
- Append a one-line reference to the project's work log or compound notes:
|
|
47
|
+
`- [YYYY-MM-DD] capture: [text] → raw/transcripts/YYYY-MM-DD-ad-hoc-captures.md`
|
|
48
|
+
- Do NOT create a full work item (that's `proj-work`'s job).
|
|
49
|
+
5. **Update log.md.** Append: `## [YYYY-MM-DD] capture | [type]: [text (first 60 chars)]`
|
|
50
|
+
6. **Confirm to user.** Report what was captured and where. Suggest next steps:
|
|
51
|
+
- If `type: idea` → "Consider ingesting related sources to develop this idea."
|
|
52
|
+
- If `type: bug` → "Use proj-work to create a bug-fix work item."
|
|
53
|
+
- If `type: task` → "Use proj-work to track this task through the dev loop."
|
|
54
|
+
- If `type: note` → "Will be available for future wiki-query searches."
|
|
55
|
+
|
|
56
|
+
## Ad-hoc captures file format
|
|
57
|
+
|
|
58
|
+
The file `raw/transcripts/YYYY-MM-DD-ad-hoc-captures.md` is a standard raw source with frontmatter:
|
|
59
|
+
|
|
60
|
+
```yaml
|
|
61
|
+
---
|
|
62
|
+
source_url:
|
|
63
|
+
ingested: YYYY-MM-DD
|
|
64
|
+
sha256:
|
|
65
|
+
---
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The `sha256` is computed over the body after the closing `---`. On each append, recompute and update `sha256`. This keeps source-drift detection functional even though the file grows throughout the day.
|
|
69
|
+
|
|
70
|
+
## Stop conditions
|
|
71
|
+
|
|
72
|
+
- `skillwiki path` returns NO_VAULT_CONFIGURED.
|
|
73
|
+
- No `text` provided (prompt user once, then stop).
|
|
74
|
+
|
|
75
|
+
## Forbidden
|
|
76
|
+
|
|
77
|
+
- Creating an `inbox/` directory. All captures go to `raw/transcripts/`.
|
|
78
|
+
- Modifying existing entries in the captures file — only append.
|
|
79
|
+
- Creating a work item — this is capture-only. Use `proj-work` for full work items.
|
|
80
|
+
- Writing to any Layer 2 or Layer 3 location. Captures are Layer 1 (raw).
|
|
81
|
+
|
|
82
|
+
## Filesystem drop (offline capture)
|
|
83
|
+
|
|
84
|
+
When you're not in a Claude session, drop files directly into `raw/transcripts/`:
|
|
85
|
+
|
|
86
|
+
1. Create any `.md` file in `raw/transcripts/` — name it descriptively (e.g., `2026-05-07-idea-xyz.md`)
|
|
87
|
+
2. Add raw frontmatter at the top:
|
|
88
|
+
```yaml
|
|
89
|
+
---
|
|
90
|
+
source_url:
|
|
91
|
+
ingested: YYYY-MM-DD
|
|
92
|
+
sha256:
|
|
93
|
+
---
|
|
94
|
+
```
|
|
95
|
+
3. Write your idea/bug/task/note below the frontmatter
|
|
96
|
+
|
|
97
|
+
No special format required — the dev-loop QUERY step will discover new files on the next cycle and surface them as claimable work. Mark the type with a heading like `## idea`, `## bug`, `## task`, or just write freeform.
|
|
98
|
+
|
|
99
|
+
## Dev-loop discovery
|
|
100
|
+
|
|
101
|
+
When the dev-loop QUERY step runs, it should scan `raw/transcripts/` for files with `ingested:` date newer than the last cycle. New files are surfaced as claimable work items. The agent then decides whether to:
|
|
102
|
+
- Create a work item via `proj-work` (for tasks and bugs)
|
|
103
|
+
- Ingest as a knowledge page via `wiki-ingest` (for ideas with sources)
|
|
104
|
+
- Leave in place (for notes that don't need action yet)
|