skillwiki 0.2.0 → 0.2.1-beta.10
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 +167 -63
- package/package.json +1 -1
- package/skills/.claude-plugin/plugin.json +2 -2
- package/skills/package.json +1 -1
- 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
|
}
|
|
@@ -1704,7 +1729,7 @@ function hasDuplicateFrontmatter(body) {
|
|
|
1704
1729
|
}
|
|
1705
1730
|
var ERROR_ORDER = ["broken_wikilinks", "invalid_frontmatter", "raw_dedup", "tag_not_in_taxonomy"];
|
|
1706
1731
|
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"];
|
|
1732
|
+
var INFO_ORDER = ["bridges", "page_structure", "topic_map_recommended", "frontmatter_wikilink", "wikilink_citation"];
|
|
1708
1733
|
async function runLint(input) {
|
|
1709
1734
|
const buckets = {};
|
|
1710
1735
|
const links = await runLinks({ vault: input.vault });
|
|
@@ -1757,6 +1782,7 @@ async function runLint(input) {
|
|
|
1757
1782
|
const dupFrontmatter = [];
|
|
1758
1783
|
const noOverview = [];
|
|
1759
1784
|
const fmWikilinkFlags = [];
|
|
1785
|
+
const wikilinkCitationFlags = [];
|
|
1760
1786
|
for (const page of scan.data.typedKnowledge) {
|
|
1761
1787
|
const text = await readPage(page);
|
|
1762
1788
|
const split = splitFrontmatter(text);
|
|
@@ -1766,6 +1792,7 @@ async function runLint(input) {
|
|
|
1766
1792
|
if (hasDuplicateFrontmatter(body)) dupFrontmatter.push(page.relPath);
|
|
1767
1793
|
if (isLegacyCitationStyle(body)) legacyPages.push(page.relPath);
|
|
1768
1794
|
if (hasOrphanedCitations(body)) orphanedPages.push(page.relPath);
|
|
1795
|
+
if (hasWikilinkCitations(body)) wikilinkCitationFlags.push(page.relPath);
|
|
1769
1796
|
const fmLinks = rawFm.match(/\[\[([^\[\]|]+)(?:\|[^\[\]]*)?\]\]/g) ?? [];
|
|
1770
1797
|
for (const link of fmLinks) {
|
|
1771
1798
|
const target = link.replace(/^\[\[/, "").replace(/(?:\|[^\[\]]*)?\]\]$/, "").trim();
|
|
@@ -1794,6 +1821,7 @@ async function runLint(input) {
|
|
|
1794
1821
|
if (dupFrontmatter.length > 0) buckets.duplicate_frontmatter = dupFrontmatter;
|
|
1795
1822
|
if (noOverview.length > 0) buckets.missing_overview = noOverview;
|
|
1796
1823
|
if (fmWikilinkFlags.length > 0) buckets.frontmatter_wikilink = fmWikilinkFlags;
|
|
1824
|
+
if (wikilinkCitationFlags.length > 0) buckets.wikilink_citation = wikilinkCitationFlags;
|
|
1797
1825
|
}
|
|
1798
1826
|
const errorOut = ERROR_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
|
|
1799
1827
|
const warningOut = WARNING_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
|
|
@@ -1888,8 +1916,8 @@ async function runConfigPath(input) {
|
|
|
1888
1916
|
}
|
|
1889
1917
|
|
|
1890
1918
|
// src/commands/doctor.ts
|
|
1891
|
-
import { existsSync as existsSync3, readdirSync,
|
|
1892
|
-
import { join as
|
|
1919
|
+
import { existsSync as existsSync3, readdirSync, statSync } from "fs";
|
|
1920
|
+
import { join as join17 } from "path";
|
|
1893
1921
|
import { execSync } from "child_process";
|
|
1894
1922
|
|
|
1895
1923
|
// src/utils/auto-update.ts
|
|
@@ -1953,6 +1981,27 @@ function triggerAutoUpdate(home, currentVersion) {
|
|
|
1953
1981
|
child.unref();
|
|
1954
1982
|
}
|
|
1955
1983
|
|
|
1984
|
+
// src/utils/plugin-registry.ts
|
|
1985
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
1986
|
+
import { join as join16 } from "path";
|
|
1987
|
+
var REGISTRY_PATH = join16(".claude", "plugins", "installed_plugins.json");
|
|
1988
|
+
var PLUGIN_KEY = "skillwiki@llm-wiki";
|
|
1989
|
+
function readInstalledPlugins(home) {
|
|
1990
|
+
try {
|
|
1991
|
+
const raw = readFileSync3(join16(home, REGISTRY_PATH), "utf8");
|
|
1992
|
+
return JSON.parse(raw);
|
|
1993
|
+
} catch {
|
|
1994
|
+
return null;
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
function findPlugin(home, key = PLUGIN_KEY) {
|
|
1998
|
+
const registry = readInstalledPlugins(home);
|
|
1999
|
+
if (!registry?.plugins) return null;
|
|
2000
|
+
const entries = registry.plugins[key];
|
|
2001
|
+
if (!entries || entries.length === 0) return null;
|
|
2002
|
+
return entries[0];
|
|
2003
|
+
}
|
|
2004
|
+
|
|
1956
2005
|
// src/commands/doctor.ts
|
|
1957
2006
|
function check(status, id, label, detail) {
|
|
1958
2007
|
return { id, label, status, detail };
|
|
@@ -2008,9 +2057,9 @@ function checkVaultStructure(resolvedPath) {
|
|
|
2008
2057
|
return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 vault directory does not exist");
|
|
2009
2058
|
}
|
|
2010
2059
|
const missing = [];
|
|
2011
|
-
if (!existsSync3(
|
|
2060
|
+
if (!existsSync3(join17(resolvedPath, "SCHEMA.md"))) missing.push("SCHEMA.md");
|
|
2012
2061
|
for (const dir of ["raw", "entities", "concepts", "meta"]) {
|
|
2013
|
-
if (!existsSync3(
|
|
2062
|
+
if (!existsSync3(join17(resolvedPath, dir))) missing.push(dir + "/");
|
|
2014
2063
|
}
|
|
2015
2064
|
if (missing.length === 0) {
|
|
2016
2065
|
return check("pass", "vault_structure", "Vault structure valid", "All required files and directories present");
|
|
@@ -2018,15 +2067,21 @@ function checkVaultStructure(resolvedPath) {
|
|
|
2018
2067
|
return check("warn", "vault_structure", "Vault structure valid", `Missing: ${missing.join(", ")} \u2014 run \`skillwiki init\` to add CodeWiki structure`);
|
|
2019
2068
|
}
|
|
2020
2069
|
function checkSkillsInstalled(home) {
|
|
2021
|
-
const
|
|
2022
|
-
if (
|
|
2023
|
-
|
|
2070
|
+
const plugin = findPlugin(home);
|
|
2071
|
+
if (plugin) {
|
|
2072
|
+
const found = findSkillMd(plugin.installPath);
|
|
2073
|
+
if (found.length > 0) {
|
|
2074
|
+
return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (plugin v${plugin.version})`);
|
|
2075
|
+
}
|
|
2024
2076
|
}
|
|
2025
|
-
const
|
|
2026
|
-
if (
|
|
2027
|
-
|
|
2077
|
+
const skillsDir = join17(home, ".claude", "skills");
|
|
2078
|
+
if (existsSync3(skillsDir)) {
|
|
2079
|
+
const found = findSkillMd(skillsDir);
|
|
2080
|
+
if (found.length > 0) {
|
|
2081
|
+
return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (CLI install)`);
|
|
2082
|
+
}
|
|
2028
2083
|
}
|
|
2029
|
-
return check("warn", "skills_installed", "Skills installed", "No SKILL.md files found
|
|
2084
|
+
return check("warn", "skills_installed", "Skills installed", "No SKILL.md files found");
|
|
2030
2085
|
}
|
|
2031
2086
|
function checkNpmUpdate(home, currentVersion) {
|
|
2032
2087
|
const { hasUpdate, latest } = latestFromCache(home, currentVersion);
|
|
@@ -2039,30 +2094,21 @@ function checkNpmUpdate(home, currentVersion) {
|
|
|
2039
2094
|
return check("pass", "npm_update", "npm CLI version", `v${currentVersion} (latest: v${latest})`);
|
|
2040
2095
|
}
|
|
2041
2096
|
function checkPluginVersionDrift(home, currentVersion) {
|
|
2042
|
-
const
|
|
2043
|
-
if (!
|
|
2044
|
-
return check("pass", "plugin_version_drift", "Plugin/CLI version", "Plugin
|
|
2097
|
+
const plugin = findPlugin(home);
|
|
2098
|
+
if (!plugin) {
|
|
2099
|
+
return check("pass", "plugin_version_drift", "Plugin/CLI version", "Plugin not installed \u2014 CLI only");
|
|
2045
2100
|
}
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
const pluginVersion = pluginData.version;
|
|
2050
|
-
if (!pluginVersion) {
|
|
2051
|
-
return check("pass", "plugin_version_drift", "Plugin/CLI version", "Plugin version not found in cache");
|
|
2052
|
-
}
|
|
2053
|
-
if (pluginVersion === currentVersion) {
|
|
2054
|
-
return check("pass", "plugin_version_drift", "Plugin/CLI version", `Both at v${currentVersion}`);
|
|
2055
|
-
}
|
|
2056
|
-
const updateCmd = semverGt(pluginVersion, currentVersion) ? "npm install -g skillwiki@beta" : "claude plugin update skillwiki@llm-wiki";
|
|
2057
|
-
return check(
|
|
2058
|
-
"warn",
|
|
2059
|
-
"plugin_version_drift",
|
|
2060
|
-
"Plugin/CLI version",
|
|
2061
|
-
`Plugin v${pluginVersion} \u2260 CLI v${currentVersion} \u2014 run \`${updateCmd}\``
|
|
2062
|
-
);
|
|
2063
|
-
} catch {
|
|
2064
|
-
return check("pass", "plugin_version_drift", "Plugin/CLI version", "Could not read plugin cache");
|
|
2101
|
+
const pluginVersion = plugin.version;
|
|
2102
|
+
if (pluginVersion === currentVersion) {
|
|
2103
|
+
return check("pass", "plugin_version_drift", "Plugin/CLI version", `Both at v${currentVersion}`);
|
|
2065
2104
|
}
|
|
2105
|
+
const updateCmd = semverGt(pluginVersion, currentVersion) ? "npm install -g skillwiki@beta" : "claude plugin update skillwiki@llm-wiki";
|
|
2106
|
+
return check(
|
|
2107
|
+
"warn",
|
|
2108
|
+
"plugin_version_drift",
|
|
2109
|
+
"Plugin/CLI version",
|
|
2110
|
+
`Plugin v${pluginVersion} \u2260 CLI v${currentVersion} \u2014 run \`${updateCmd}\``
|
|
2111
|
+
);
|
|
2066
2112
|
}
|
|
2067
2113
|
async function checkProfiles(home) {
|
|
2068
2114
|
const map = await parseDotenvFile(configPath(home));
|
|
@@ -2086,7 +2132,7 @@ async function checkProfiles(home) {
|
|
|
2086
2132
|
}
|
|
2087
2133
|
async function checkProjectLocalOverride(cwd) {
|
|
2088
2134
|
const dir = cwd ?? process.cwd();
|
|
2089
|
-
const envPath =
|
|
2135
|
+
const envPath = join17(dir, ".skillwiki", ".env");
|
|
2090
2136
|
if (existsSync3(envPath)) {
|
|
2091
2137
|
return check("pass", "project_local", "Project-local config", `Found: ${envPath}`);
|
|
2092
2138
|
}
|
|
@@ -2102,9 +2148,9 @@ function findSkillMd(dir) {
|
|
|
2102
2148
|
}
|
|
2103
2149
|
for (const entry of entries) {
|
|
2104
2150
|
if (entry.isFile() && entry.name === "SKILL.md") {
|
|
2105
|
-
results.push(
|
|
2151
|
+
results.push(join17(dir, entry.name));
|
|
2106
2152
|
} else if (entry.isDirectory()) {
|
|
2107
|
-
results.push(...findSkillMd(
|
|
2153
|
+
results.push(...findSkillMd(join17(dir, entry.name)));
|
|
2108
2154
|
}
|
|
2109
2155
|
}
|
|
2110
2156
|
return results;
|
|
@@ -2148,35 +2194,41 @@ async function runDoctor(input) {
|
|
|
2148
2194
|
|
|
2149
2195
|
// src/commands/archive.ts
|
|
2150
2196
|
import { rename as rename3, mkdir as mkdir5, readFile as readFile13, writeFile as writeFile6 } from "fs/promises";
|
|
2151
|
-
import { join as
|
|
2197
|
+
import { join as join18, dirname as dirname7 } from "path";
|
|
2152
2198
|
async function runArchive(input) {
|
|
2153
2199
|
const scan = await scanVault(input.vault);
|
|
2154
2200
|
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
}
|
|
2159
|
-
|
|
2201
|
+
const lookup = (pages) => {
|
|
2202
|
+
if (input.page.includes("/")) return pages.find((p) => p.relPath === input.page)?.relPath;
|
|
2203
|
+
return pages.find((p) => p.relPath.replace(/\.md$/, "").split("/").pop() === input.page)?.relPath;
|
|
2204
|
+
};
|
|
2205
|
+
let relPath = lookup(scan.data.typedKnowledge);
|
|
2206
|
+
let isRaw = false;
|
|
2207
|
+
if (!relPath) {
|
|
2208
|
+
relPath = lookup(scan.data.raw);
|
|
2209
|
+
isRaw = relPath != null;
|
|
2160
2210
|
}
|
|
2161
2211
|
if (!relPath) return { exitCode: ExitCode.ARCHIVE_TARGET_NOT_FOUND, result: err("ARCHIVE_TARGET_NOT_FOUND", { page: input.page }) };
|
|
2162
2212
|
if (relPath.startsWith("_archive/")) return { exitCode: ExitCode.ARCHIVE_ALREADY_ARCHIVED, result: err("ARCHIVE_ALREADY_ARCHIVED", { page: relPath }) };
|
|
2163
|
-
const archivePath =
|
|
2164
|
-
await mkdir5(dirname7(
|
|
2213
|
+
const archivePath = join18("_archive", relPath);
|
|
2214
|
+
await mkdir5(dirname7(join18(input.vault, archivePath)), { recursive: true });
|
|
2165
2215
|
let indexUpdated = false;
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2216
|
+
if (!isRaw) {
|
|
2217
|
+
const indexPath = join18(input.vault, "index.md");
|
|
2218
|
+
try {
|
|
2219
|
+
const idx = await readFile13(indexPath, "utf8");
|
|
2220
|
+
const slug = relPath.replace(/\.md$/, "").split("/").pop();
|
|
2221
|
+
const originalLines = idx.split("\n");
|
|
2222
|
+
const filtered = originalLines.filter((l) => !l.includes(`[[${slug}]]`));
|
|
2223
|
+
if (filtered.length !== originalLines.length) {
|
|
2224
|
+
await writeFile6(indexPath, filtered.join("\n"), "utf8");
|
|
2225
|
+
indexUpdated = true;
|
|
2226
|
+
}
|
|
2227
|
+
} catch (e) {
|
|
2228
|
+
if (e?.code !== "ENOENT") throw e;
|
|
2175
2229
|
}
|
|
2176
|
-
} catch (e) {
|
|
2177
|
-
if (e?.code !== "ENOENT") throw e;
|
|
2178
2230
|
}
|
|
2179
|
-
await rename3(
|
|
2231
|
+
await rename3(join18(input.vault, relPath), join18(input.vault, archivePath));
|
|
2180
2232
|
return { exitCode: ExitCode.OK, result: ok({ archived_from: relPath, archived_to: archivePath, index_updated: indexUpdated, humanHint: `${relPath} -> ${archivePath}${indexUpdated ? " (index updated)" : ""}` }) };
|
|
2181
2233
|
}
|
|
2182
2234
|
|
|
@@ -2223,16 +2275,31 @@ async function runDrift(input) {
|
|
|
2223
2275
|
const scan = await scanVault(input.vault);
|
|
2224
2276
|
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
2225
2277
|
const results = [];
|
|
2278
|
+
const newResults = [];
|
|
2226
2279
|
for (const raw of scan.data.raw) {
|
|
2227
2280
|
const text = await readPage(raw);
|
|
2228
2281
|
const split = splitFrontmatter(text);
|
|
2229
2282
|
if (!split.ok) continue;
|
|
2230
2283
|
const { rawFrontmatter, body } = split.data;
|
|
2284
|
+
const ingestedMatch = rawFrontmatter.match(/^ingested:\s*(.+)$/m);
|
|
2285
|
+
const ingestedRaw = ingestedMatch?.[1]?.trim() ?? "";
|
|
2286
|
+
const ingested = ingestedRaw.replace(/^["']|["']$/g, "");
|
|
2287
|
+
if (input.newSince && ingested && ingested >= input.newSince) {
|
|
2288
|
+
newResults.push({
|
|
2289
|
+
raw_path: raw.relPath,
|
|
2290
|
+
source_url: "",
|
|
2291
|
+
stored_sha256: "",
|
|
2292
|
+
current_sha256: null,
|
|
2293
|
+
status: "new",
|
|
2294
|
+
ingested
|
|
2295
|
+
});
|
|
2296
|
+
}
|
|
2231
2297
|
const sourceUrlMatch = rawFrontmatter.match(/^source_url:\s*(.+)$/m);
|
|
2232
2298
|
const storedHashMatch = rawFrontmatter.match(/^sha256:\s*([a-f0-9]+)$/m);
|
|
2233
2299
|
if (!sourceUrlMatch || !storedHashMatch) continue;
|
|
2234
2300
|
const sourceUrl = sourceUrlMatch[1].trim();
|
|
2235
2301
|
const storedHash = storedHashMatch[1];
|
|
2302
|
+
if (!sourceUrl.startsWith("http://") && !sourceUrl.startsWith("https://")) continue;
|
|
2236
2303
|
const resp = await doFetch(sourceUrl, FETCH_OPTS);
|
|
2237
2304
|
if (!resp.ok) {
|
|
2238
2305
|
results.push({
|
|
@@ -2277,12 +2344,13 @@ ${body}`;
|
|
|
2277
2344
|
const unchanged = results.filter((r) => r.status === "unchanged").length;
|
|
2278
2345
|
const exitCode = drifted.length > 0 ? ExitCode.DRIFT_DETECTED : ExitCode.OK;
|
|
2279
2346
|
const hintLines = [`scanned: ${results.length}, unchanged: ${unchanged}`];
|
|
2347
|
+
if (newResults.length > 0) hintLines.push(`new: ${newResults.length}`, ...newResults.map((n) => ` ${n.raw_path} (ingested: ${n.ingested})`));
|
|
2280
2348
|
if (drifted.length > 0) hintLines.push(`drifted: ${drifted.length}`, ...drifted.map((d) => ` ${d.raw_path}`));
|
|
2281
2349
|
if (fetchFailed.length > 0) hintLines.push(`fetch_failed: ${fetchFailed.length}`, ...fetchFailed.map((f) => ` ${f.raw_path}: ${f.fetch_error}`));
|
|
2282
2350
|
if (updated.length > 0) hintLines.push(`updated: ${updated.length}`, ...updated.map((u) => ` ${u.raw_path}`));
|
|
2283
2351
|
return {
|
|
2284
2352
|
exitCode,
|
|
2285
|
-
result: ok({ scanned: results.length, drifted, fetch_failed: fetchFailed, updated, unchanged, humanHint: hintLines.join("\n") })
|
|
2353
|
+
result: ok({ scanned: results.length, drifted, fetch_failed: fetchFailed, updated, newFiles: newResults, unchanged, humanHint: hintLines.join("\n") })
|
|
2286
2354
|
};
|
|
2287
2355
|
}
|
|
2288
2356
|
|
|
@@ -2557,6 +2625,37 @@ async function runUpdate(input) {
|
|
|
2557
2625
|
};
|
|
2558
2626
|
}
|
|
2559
2627
|
|
|
2628
|
+
// src/commands/transcripts.ts
|
|
2629
|
+
import { readdir as readdir4, stat as stat6, readFile as readFile14 } from "fs/promises";
|
|
2630
|
+
import { join as join19 } from "path";
|
|
2631
|
+
async function runTranscripts(input) {
|
|
2632
|
+
const dir = join19(input.vault, "raw", "transcripts");
|
|
2633
|
+
let entries;
|
|
2634
|
+
try {
|
|
2635
|
+
entries = await readdir4(dir, { withFileTypes: true });
|
|
2636
|
+
} catch {
|
|
2637
|
+
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: { ok: false, error: "VAULT_PATH_INVALID", detail: `raw/transcripts/ not found: ${dir}` } };
|
|
2638
|
+
}
|
|
2639
|
+
const transcripts = [];
|
|
2640
|
+
for (const entry of entries) {
|
|
2641
|
+
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
2642
|
+
const filePath = join19(dir, entry.name);
|
|
2643
|
+
const content = await readFile14(filePath, "utf8");
|
|
2644
|
+
const fm = extractFrontmatter(content);
|
|
2645
|
+
if (!fm.ok) continue;
|
|
2646
|
+
const ingested = typeof fm.data.ingested === "string" ? fm.data.ingested : "";
|
|
2647
|
+
if (input.since && ingested && ingested < input.since) continue;
|
|
2648
|
+
const s = await stat6(filePath);
|
|
2649
|
+
transcripts.push({
|
|
2650
|
+
file: `raw/transcripts/${entry.name}`,
|
|
2651
|
+
ingested,
|
|
2652
|
+
size: s.size
|
|
2653
|
+
});
|
|
2654
|
+
}
|
|
2655
|
+
const hint = transcripts.length > 0 ? transcripts.map((t) => `${t.file} (ingested: ${t.ingested || "unknown"}, ${t.size}B)`).join("\n") : "no transcript files found";
|
|
2656
|
+
return { exitCode: ExitCode.OK, result: ok({ transcripts, humanHint: hint }) };
|
|
2657
|
+
}
|
|
2658
|
+
|
|
2560
2659
|
// src/cli.ts
|
|
2561
2660
|
var pkg = JSON.parse(readFileSync5(new URL("../package.json", import.meta.url), "utf8"));
|
|
2562
2661
|
var program = new Command();
|
|
@@ -2692,10 +2791,10 @@ program.command("archive <page> [vault]").description("archive a typed-knowledge
|
|
|
2692
2791
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
2693
2792
|
else emit(await runArchive({ vault: v.vault, page }));
|
|
2694
2793
|
});
|
|
2695
|
-
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) => {
|
|
2794
|
+
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) => {
|
|
2696
2795
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
2697
2796
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
2698
|
-
else emit(await runDrift({ vault: v.vault, apply: opts.apply }));
|
|
2797
|
+
else emit(await runDrift({ vault: v.vault, apply: opts.apply, newSince: opts.new }));
|
|
2699
2798
|
});
|
|
2700
2799
|
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) => {
|
|
2701
2800
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
@@ -2716,6 +2815,11 @@ program.command("update").description("update skillwiki CLI to the latest versio
|
|
|
2716
2815
|
home: process.env.HOME ?? "",
|
|
2717
2816
|
distTag: opts.tag
|
|
2718
2817
|
})));
|
|
2818
|
+
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) => {
|
|
2819
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
2820
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
2821
|
+
else emit(await runTranscripts({ vault: v.vault, since: opts.since }));
|
|
2822
|
+
});
|
|
2719
2823
|
triggerAutoUpdate(process.env.HOME ?? "", pkg.version);
|
|
2720
2824
|
program.parseAsync(process.argv).catch((e) => {
|
|
2721
2825
|
process.stdout.write(JSON.stringify({ ok: false, error: "INTERNAL", detail: { message: String(e) } }) + "\n");
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skillwiki",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1-beta.10",
|
|
4
4
|
"skills": "./",
|
|
5
|
-
"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.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "karlorz",
|
|
8
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)
|