llm-wikis 0.1.0

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/.gitattributes ADDED
@@ -0,0 +1,3 @@
1
+ *.png filter=lfs diff=lfs merge=lfs -text
2
+ *.psd filter=lfs diff=lfs merge=lfs -text
3
+ *.jpg filter=lfs diff=lfs merge=lfs -text
package/package.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "llm-wikis",
3
+ "version": "0.1.0",
4
+ "description": "Multi-wiki sync observability for the Karpathy LLM Wiki pattern",
5
+ "license": "MIT",
6
+ "bin": {
7
+ "wiki-scan": "scripts/wiki-scan.js",
8
+ "wiki-ingest-record": "scripts/wiki-ingest-record.js",
9
+ "wiki-status-html": "scripts/wiki-status-html.js"
10
+ },
11
+ "scripts": {
12
+ "postinstall": "node postinstall.js"
13
+ },
14
+ "engines": {
15
+ "node": ">=18.0.0"
16
+ }
17
+ }
package/postinstall.js ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const os = require('os');
5
+
6
+ const src = path.join(__dirname, 'skill');
7
+ const dest = path.join(os.homedir(), '.claude', 'skills', 'wiki');
8
+
9
+ try {
10
+ fs.mkdirSync(dest, { recursive: true });
11
+ fs.cpSync(src, dest, { recursive: true, force: true });
12
+ console.log('[llm-wiki-sync] Skill installed to ~/.claude/skills/wiki/');
13
+ } catch (err) {
14
+ console.warn('[llm-wiki-sync] Could not install skill:', err.message);
15
+ console.warn('[llm-wiki-sync] Manually copy the skill/ directory to ~/.claude/skills/wiki/');
16
+ }
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const crypto = require('crypto');
7
+
8
+ function usage() {
9
+ console.error('Usage:');
10
+ console.error(' wiki-ingest-record <wiki-path> <raw-file-relpath> <wiki-page-relpath>');
11
+ console.error(' wiki-ingest-record <wiki-path> <raw-file-relpath> --update-hash-only');
12
+ console.error(' wiki-ingest-record <wiki-path> --defer <raw-file-relpath>');
13
+ process.exit(1);
14
+ }
15
+
16
+ function sha256(filePath) {
17
+ const hash = crypto.createHash('sha256');
18
+ hash.update(fs.readFileSync(filePath));
19
+ return hash.digest('hex');
20
+ }
21
+
22
+ function loadManifest(wikiPath) {
23
+ const manifestPath = path.join(wikiPath, '.wiki-manifest.json');
24
+ return JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
25
+ }
26
+
27
+ function saveManifest(wikiPath, manifest) {
28
+ const manifestPath = path.join(wikiPath, '.wiki-manifest.json');
29
+ const backupPath = path.join(wikiPath, '.wiki-manifest.backup.json');
30
+ const tmpPath = manifestPath + '.tmp';
31
+
32
+ if (fs.existsSync(manifestPath)) {
33
+ fs.copyFileSync(manifestPath, backupPath);
34
+ }
35
+
36
+ fs.writeFileSync(tmpPath, JSON.stringify(manifest, null, 2));
37
+ fs.renameSync(tmpPath, manifestPath);
38
+ }
39
+
40
+ const [,, wikiPath, arg2, arg3] = process.argv;
41
+
42
+ if (!wikiPath || !arg2) usage();
43
+
44
+ const absWikiPath = path.resolve(wikiPath);
45
+
46
+ // Mode: --defer <raw-file>
47
+ if (arg2 === '--defer') {
48
+ const rawRel = arg3;
49
+ if (!rawRel) usage();
50
+
51
+ const rawAbs = path.join(absWikiPath, rawRel);
52
+ if (!fs.existsSync(rawAbs)) {
53
+ console.error(`File not found: ${rawAbs}`);
54
+ process.exit(1);
55
+ }
56
+
57
+ const manifest = loadManifest(absWikiPath);
58
+ const hash = sha256(rawAbs);
59
+ const mtimeMs = fs.statSync(rawAbs).mtimeMs;
60
+
61
+ manifest.raw_to_wiki[rawRel] = {
62
+ hash,
63
+ modified: mtimeMs,
64
+ ingested: null,
65
+ deferred: true,
66
+ wiki_pages: manifest.raw_to_wiki[rawRel]?.wiki_pages ?? [],
67
+ };
68
+
69
+ saveManifest(absWikiPath, manifest);
70
+ console.log(`[wiki-ingest-record] Deferred: ${rawRel}`);
71
+ process.exit(0);
72
+ }
73
+
74
+ const rawRel = arg2;
75
+ const wikiPageArg = arg3;
76
+
77
+ if (!wikiPageArg) usage();
78
+
79
+ // Mode: --update-hash-only
80
+ if (wikiPageArg === '--update-hash-only') {
81
+ const manifest = loadManifest(absWikiPath);
82
+ const entry = manifest.raw_to_wiki[rawRel];
83
+
84
+ if (!entry || !entry.wiki_pages || entry.wiki_pages.length === 0) {
85
+ console.error(`No wiki pages found for ${rawRel} in manifest`);
86
+ process.exit(1);
87
+ }
88
+
89
+ for (const wikiPage of entry.wiki_pages) {
90
+ const wikiAbs = path.join(absWikiPath, wikiPage);
91
+ if (!fs.existsSync(wikiAbs)) continue;
92
+ const hash = sha256(wikiAbs);
93
+ const mtimeMs = fs.statSync(wikiAbs).mtimeMs;
94
+ if (manifest.wiki_to_raw[wikiPage]) {
95
+ manifest.wiki_to_raw[wikiPage].hash = hash;
96
+ manifest.wiki_to_raw[wikiPage].last_compiled = mtimeMs;
97
+ }
98
+ }
99
+
100
+ saveManifest(absWikiPath, manifest);
101
+ console.log(`[wiki-ingest-record] Updated hash only for: ${rawRel}`);
102
+ process.exit(0);
103
+ }
104
+
105
+ // Mode: record raw → wiki page
106
+ const wikiPageRel = wikiPageArg;
107
+ const rawAbs = path.join(absWikiPath, rawRel);
108
+ const wikiAbs = path.join(absWikiPath, wikiPageRel);
109
+
110
+ if (!fs.existsSync(rawAbs)) {
111
+ console.error(`Raw file not found: ${rawAbs}`);
112
+ process.exit(1);
113
+ }
114
+ if (!fs.existsSync(wikiAbs)) {
115
+ console.error(`Wiki page not found: ${wikiAbs}`);
116
+ process.exit(1);
117
+ }
118
+
119
+ const manifest = loadManifest(absWikiPath);
120
+
121
+ const rawHash = sha256(rawAbs);
122
+ const rawMtime = fs.statSync(rawAbs).mtimeMs;
123
+ const wikiHash = sha256(wikiAbs);
124
+ const wikiMtime = fs.statSync(wikiAbs).mtimeMs;
125
+ const now = Date.now();
126
+
127
+ // Update raw_to_wiki entry
128
+ const existingRaw = manifest.raw_to_wiki[rawRel] ?? { wiki_pages: [] };
129
+ const wikiPages = new Set(existingRaw.wiki_pages);
130
+ wikiPages.add(wikiPageRel);
131
+
132
+ manifest.raw_to_wiki[rawRel] = {
133
+ hash: rawHash,
134
+ modified: rawMtime,
135
+ ingested: now,
136
+ deferred: false,
137
+ wiki_pages: [...wikiPages],
138
+ };
139
+
140
+ // Update wiki_to_raw entry
141
+ const existingWiki = manifest.wiki_to_raw[wikiPageRel] ?? { sources: [] };
142
+ const sources = new Set(existingWiki.sources);
143
+ sources.add(rawRel);
144
+
145
+ manifest.wiki_to_raw[wikiPageRel] = {
146
+ hash: wikiHash,
147
+ last_compiled: wikiMtime,
148
+ sources: [...sources],
149
+ provenance_unknown: false,
150
+ };
151
+
152
+ manifest.last_ingestion = new Date(now).toISOString();
153
+
154
+ saveManifest(absWikiPath, manifest);
155
+ console.log(`[wiki-ingest-record] Recorded: ${rawRel} → ${wikiPageRel}`);
@@ -0,0 +1,241 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const crypto = require('crypto');
7
+
8
+ const VERSION = '0.1.0';
9
+
10
+ function usage() {
11
+ console.error('Usage:');
12
+ console.error(' wiki-scan --version');
13
+ console.error(' wiki-scan --list <project-root>');
14
+ console.error(' wiki-scan --status <wiki-path>');
15
+ process.exit(1);
16
+ }
17
+
18
+ function loadManifest(wikiPath) {
19
+ const manifestPath = path.join(wikiPath, '.wiki-manifest.json');
20
+ try {
21
+ return JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
22
+ } catch {
23
+ return null;
24
+ }
25
+ }
26
+
27
+ function saveManifest(wikiPath, manifest) {
28
+ const manifestPath = path.join(wikiPath, '.wiki-manifest.json');
29
+ const backupPath = path.join(wikiPath, '.wiki-manifest.backup.json');
30
+ const tmpPath = manifestPath + '.tmp';
31
+
32
+ // Backup current manifest before writing
33
+ if (fs.existsSync(manifestPath)) {
34
+ fs.copyFileSync(manifestPath, backupPath);
35
+ }
36
+
37
+ fs.writeFileSync(tmpPath, JSON.stringify(manifest, null, 2));
38
+ fs.renameSync(tmpPath, manifestPath);
39
+ }
40
+
41
+ function sha256(filePath) {
42
+ const hash = crypto.createHash('sha256');
43
+ hash.update(fs.readFileSync(filePath));
44
+ return hash.digest('hex');
45
+ }
46
+
47
+ function walkDir(dir, baseDir, results = []) {
48
+ if (!fs.existsSync(dir)) return results;
49
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
50
+ const full = path.join(dir, entry.name);
51
+ if (entry.isDirectory()) {
52
+ walkDir(full, baseDir, results);
53
+ } else {
54
+ const rel = path.relative(baseDir, full).replace(/\\/g, '/');
55
+ const stat = fs.statSync(full);
56
+ results.push({ rel, full, mtimeMs: stat.mtimeMs });
57
+ }
58
+ }
59
+ return results;
60
+ }
61
+
62
+ function listWikis(projectRoot) {
63
+ const wikisDir = path.join(projectRoot, 'wikis');
64
+
65
+ if (!fs.existsSync(wikisDir)) return [];
66
+
67
+ const entries = fs.readdirSync(wikisDir, { withFileTypes: true });
68
+ const wikis = [];
69
+
70
+ for (const entry of entries) {
71
+ if (!entry.isDirectory()) continue;
72
+ const wikiPath = path.join(wikisDir, entry.name);
73
+ const manifest = loadManifest(wikiPath);
74
+ if (!manifest) continue;
75
+
76
+ wikis.push({
77
+ name: entry.name,
78
+ path: path.relative(projectRoot, wikiPath),
79
+ manifest,
80
+ });
81
+ }
82
+
83
+ return wikis;
84
+ }
85
+
86
+ function scanStatus(wikiPath) {
87
+ const absWikiPath = path.resolve(wikiPath);
88
+ const manifest = loadManifest(absWikiPath);
89
+
90
+ if (!manifest) {
91
+ console.error(`No manifest found at ${absWikiPath}/.wiki-manifest.json`);
92
+ process.exit(1);
93
+ }
94
+
95
+ const rawDir = path.join(absWikiPath, 'raw');
96
+ const wikiDir = path.join(absWikiPath, 'wiki');
97
+
98
+ // Walk raw/ — paths relative to wiki root (e.g. "raw/notes.md")
99
+ const rawFiles = walkDir(rawDir, absWikiPath);
100
+
101
+ // Walk wiki/ — paths relative to wiki root (e.g. "wiki/notes.md")
102
+ const wikiFiles = walkDir(wikiDir, absWikiPath);
103
+
104
+ // Build lookup: relPath → { hash, mtimeMs } from disk
105
+ // Use mtime-gating: if mtime matches manifest entry, reuse stored hash
106
+ const rawByPath = {};
107
+ for (const file of rawFiles) {
108
+ const entry = manifest.raw_to_wiki[file.rel];
109
+ if (entry && entry.modified === file.mtimeMs) {
110
+ rawByPath[file.rel] = { hash: entry.hash, mtimeMs: file.mtimeMs };
111
+ } else {
112
+ rawByPath[file.rel] = { hash: sha256(file.full), mtimeMs: file.mtimeMs };
113
+ }
114
+ }
115
+
116
+ const wikiByPath = {};
117
+ for (const file of wikiFiles) {
118
+ const entry = manifest.wiki_to_raw[file.rel];
119
+ if (entry && entry.last_compiled === file.mtimeMs) {
120
+ wikiByPath[file.rel] = { hash: entry.hash, mtimeMs: file.mtimeMs };
121
+ } else {
122
+ wikiByPath[file.rel] = { hash: sha256(file.full), mtimeMs: file.mtimeMs };
123
+ }
124
+ }
125
+
126
+ // Diff raw files vs manifest
127
+ const manifestRawPaths = new Set(Object.keys(manifest.raw_to_wiki));
128
+ const diskRawPaths = new Set(Object.keys(rawByPath));
129
+
130
+ const added = [];
131
+ const modified = [];
132
+ const deleted = [];
133
+ const renamed = [];
134
+ const deferred = [];
135
+
136
+ // Files on disk not in manifest → added candidates
137
+ const addedCandidates = [...diskRawPaths].filter(p => !manifestRawPaths.has(p));
138
+
139
+ // Files in manifest not on disk → deleted candidates
140
+ const deletedCandidates = [...manifestRawPaths].filter(p => !diskRawPaths.has(p));
141
+
142
+ // Rename detection: match deleted candidates to added candidates by hash
143
+ const deletedByHash = {};
144
+ for (const p of deletedCandidates) {
145
+ const h = manifest.raw_to_wiki[p].hash;
146
+ deletedByHash[h] = p;
147
+ }
148
+
149
+ const addedByHash = {};
150
+ for (const p of addedCandidates) {
151
+ const h = rawByPath[p].hash;
152
+ addedByHash[h] = p;
153
+ }
154
+
155
+ const renamedFromPaths = new Set();
156
+ const renamedToPaths = new Set();
157
+
158
+ for (const [hash, fromPath] of Object.entries(deletedByHash)) {
159
+ if (addedByHash[hash]) {
160
+ const toPath = addedByHash[hash];
161
+ renamed.push({ from: fromPath, to: toPath });
162
+ renamedFromPaths.add(fromPath);
163
+ renamedToPaths.add(toPath);
164
+
165
+ // Update manifest in-place for renamed entry
166
+ const entry = manifest.raw_to_wiki[fromPath];
167
+ delete manifest.raw_to_wiki[fromPath];
168
+ manifest.raw_to_wiki[toPath] = { ...entry, hash: rawByPath[toPath].hash, modified: rawByPath[toPath].mtimeMs };
169
+ }
170
+ }
171
+
172
+ // Remaining added/deleted after rename resolution
173
+ for (const p of addedCandidates) {
174
+ if (!renamedToPaths.has(p)) added.push(p);
175
+ }
176
+ for (const p of deletedCandidates) {
177
+ if (!renamedFromPaths.has(p)) deleted.push(p);
178
+ }
179
+
180
+ // Modified: same path, hash changed
181
+ for (const p of diskRawPaths) {
182
+ if (!manifestRawPaths.has(p)) continue; // handled above
183
+ const diskHash = rawByPath[p].hash;
184
+ const manifestEntry = manifest.raw_to_wiki[p];
185
+ if (diskHash !== manifestEntry.hash) {
186
+ modified.push(p);
187
+ } else if (manifestEntry.deferred) {
188
+ deferred.push(p);
189
+ }
190
+ }
191
+
192
+ // Wiki integrity: hash mismatch on compiled wiki files
193
+ const wikiIntegrity = [];
194
+ for (const [relPath, diskInfo] of Object.entries(wikiByPath)) {
195
+ const entry = manifest.wiki_to_raw[relPath];
196
+ if (entry && !entry.provenance_unknown && diskInfo.hash !== entry.hash) {
197
+ wikiIntegrity.push(relPath);
198
+ }
199
+ }
200
+
201
+ // Persist manifest if renames were detected
202
+ if (renamed.length > 0) {
203
+ saveManifest(absWikiPath, manifest);
204
+ }
205
+
206
+ const result = {
207
+ wiki_name: manifest.wiki_name,
208
+ added,
209
+ modified,
210
+ deleted,
211
+ renamed,
212
+ deferred,
213
+ wiki_integrity: wikiIntegrity,
214
+ };
215
+
216
+ console.log(JSON.stringify(result, null, 2));
217
+ }
218
+
219
+ const [,, cmd, ...args] = process.argv;
220
+
221
+ if (cmd === '--version') {
222
+ console.log(VERSION);
223
+ process.exit(0);
224
+ }
225
+
226
+ if (cmd === '--list') {
227
+ const projectRoot = args[0];
228
+ if (!projectRoot) usage();
229
+ const wikis = listWikis(path.resolve(projectRoot));
230
+ console.log(JSON.stringify(wikis, null, 2));
231
+ process.exit(0);
232
+ }
233
+
234
+ if (cmd === '--status') {
235
+ const wikiPath = args[0];
236
+ if (!wikiPath) usage();
237
+ scanStatus(wikiPath);
238
+ process.exit(0);
239
+ }
240
+
241
+ usage();
@@ -0,0 +1,211 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ function usage() {
8
+ console.error('Usage:');
9
+ console.error(' wiki-status-html <wiki-path> <output-html-path> [--scan-json <scan-output.json>]');
10
+ process.exit(1);
11
+ }
12
+
13
+ function loadManifest(wikiPath) {
14
+ const manifestPath = path.join(wikiPath, '.wiki-manifest.json');
15
+ try {
16
+ return JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
17
+ } catch {
18
+ console.error(`Cannot read manifest at ${manifestPath}`);
19
+ process.exit(1);
20
+ }
21
+ }
22
+
23
+ function formatTs(ms) {
24
+ if (!ms) return '—';
25
+ return new Date(ms).toISOString().replace('T', ' ').replace(/\.\d+Z$/, ' UTC');
26
+ }
27
+
28
+ function shortHash(hash) {
29
+ return hash ? hash.slice(0, 8) : '—';
30
+ }
31
+
32
+ function getStatus(rawRel, entry, scanData) {
33
+ if (!scanData) {
34
+ if (!entry.ingested) return entry.deferred ? 'Deferred' : 'Pending';
35
+ return 'Synced';
36
+ }
37
+ if (scanData.added.includes(rawRel)) return 'Added';
38
+ if (scanData.modified.includes(rawRel)) return 'Modified';
39
+ if (scanData.deleted.includes(rawRel)) return 'Deleted';
40
+ if (scanData.renamed.some(r => r.from === rawRel || r.to === rawRel)) return 'Renamed';
41
+ if (entry && entry.deferred) return 'Deferred';
42
+ return 'Synced';
43
+ }
44
+
45
+ function statusBadge(status) {
46
+ const colors = {
47
+ Synced: '#22c55e',
48
+ Added: '#3b82f6',
49
+ Modified: '#f59e0b',
50
+ Deleted: '#ef4444',
51
+ Renamed: '#8b5cf6',
52
+ Deferred: '#94a3b8',
53
+ Pending: '#94a3b8',
54
+ };
55
+ const c = colors[status] || '#94a3b8';
56
+ return `<span class="badge" style="background:${c}">${status}</span>`;
57
+ }
58
+
59
+ function generateHtml(wikiPath, scanData) {
60
+ const absWikiPath = path.resolve(wikiPath);
61
+ const manifest = loadManifest(absWikiPath);
62
+ const generatedAt = new Date().toISOString().replace('T', ' ').replace(/\.\d+Z$/, ' UTC');
63
+
64
+ // Build rows: one per raw_to_wiki entry, plus scan-only added files not in manifest
65
+ const rows = [];
66
+ const manifestRawPaths = new Set(Object.keys(manifest.raw_to_wiki));
67
+
68
+ for (const [rawRel, entry] of Object.entries(manifest.raw_to_wiki)) {
69
+ const status = getStatus(rawRel, entry, scanData);
70
+ const wikiPages = entry.wiki_pages.join(', ') || '—';
71
+ const wikiHash = entry.wiki_pages.length > 0 && manifest.wiki_to_raw[entry.wiki_pages[0]]
72
+ ? shortHash(manifest.wiki_to_raw[entry.wiki_pages[0]].hash)
73
+ : '—';
74
+ rows.push({ rawRel, status, wikiPages, ingested: formatTs(entry.ingested), rawHash: shortHash(entry.hash), wikiHash });
75
+ }
76
+
77
+ // Add scan-only added files not yet in manifest
78
+ if (scanData) {
79
+ for (const rawRel of scanData.added) {
80
+ if (!manifestRawPaths.has(rawRel)) {
81
+ rows.push({ rawRel, status: 'Added', wikiPages: '—', ingested: '—', rawHash: '—', wikiHash: '—' });
82
+ }
83
+ }
84
+ }
85
+
86
+ // Summary counts
87
+ const counts = { Synced: 0, Added: 0, Modified: 0, Deleted: 0, Renamed: 0, Deferred: 0, Pending: 0 };
88
+ for (const r of rows) counts[r.status] = (counts[r.status] || 0) + 1;
89
+ const integrityCount = scanData ? scanData.wiki_integrity.length : 0;
90
+
91
+ const rowsHtml = rows.map(r => `
92
+ <tr data-status="${r.status}">
93
+ <td class="path">${r.rawRel}</td>
94
+ <td>${statusBadge(r.status)}</td>
95
+ <td class="path">${r.wikiPages}</td>
96
+ <td>${r.ingested}</td>
97
+ <td class="mono">${r.rawHash}</td>
98
+ <td class="mono">${r.wikiHash}</td>
99
+ </tr>`).join('\n');
100
+
101
+ const integrityRows = integrityCount > 0 ? `
102
+ <section class="integrity-section">
103
+ <h2>Wiki Integrity Warnings (${integrityCount})</h2>
104
+ <table>
105
+ <thead><tr><th>Wiki File</th><th>Note</th></tr></thead>
106
+ <tbody>
107
+ ${scanData.wiki_integrity.map(f => `<tr><td class="path">${f}</td><td>Hash mismatch — manually edited</td></tr>`).join('\n')}
108
+ </tbody>
109
+ </table>
110
+ </section>` : '';
111
+
112
+ return `<!DOCTYPE html>
113
+ <html lang="en">
114
+ <head>
115
+ <meta charset="UTF-8">
116
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
117
+ <title>${manifest.wiki_name} — Wiki Status</title>
118
+ <style>
119
+ * { box-sizing: border-box; margin: 0; padding: 0; }
120
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f172a; color: #e2e8f0; padding: 2rem; }
121
+ h1 { font-size: 1.5rem; margin-bottom: 0.25rem; }
122
+ .meta { color: #94a3b8; font-size: 0.85rem; margin-bottom: 1.5rem; }
123
+ .summary { display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: 1.5rem; }
124
+ .summary-item { background: #1e293b; border-radius: 6px; padding: 0.5rem 1rem; font-size: 0.875rem; }
125
+ .summary-item span { font-weight: 700; }
126
+ .filters { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 1rem; }
127
+ .filter-btn { background: #1e293b; border: 1px solid #334155; color: #e2e8f0; padding: 0.35rem 0.75rem; border-radius: 4px; cursor: pointer; font-size: 0.8rem; }
128
+ .filter-btn.active { border-color: #60a5fa; color: #60a5fa; }
129
+ table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
130
+ th { text-align: left; padding: 0.6rem 0.75rem; background: #1e293b; color: #94a3b8; font-weight: 600; font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; }
131
+ td { padding: 0.6rem 0.75rem; border-bottom: 1px solid #1e293b; }
132
+ tr:hover td { background: #1e293b; }
133
+ .path { font-family: monospace; font-size: 0.8rem; word-break: break-all; }
134
+ .mono { font-family: monospace; font-size: 0.8rem; color: #94a3b8; }
135
+ .badge { display: inline-block; padding: 0.2rem 0.5rem; border-radius: 3px; font-size: 0.75rem; font-weight: 600; color: #fff; }
136
+ .integrity-section { margin-top: 2rem; }
137
+ .integrity-section h2 { color: #fbbf24; font-size: 1rem; margin-bottom: 0.75rem; }
138
+ tr.hidden { display: none; }
139
+ </style>
140
+ </head>
141
+ <body>
142
+ <h1>${manifest.wiki_name}</h1>
143
+ <p class="meta">
144
+ Last ingestion: ${manifest.last_ingestion ? manifest.last_ingestion.replace('T', ' ').replace(/\.\d+Z$/, ' UTC') : '—'} &nbsp;·&nbsp;
145
+ Generated: ${generatedAt}
146
+ </p>
147
+
148
+ <div class="summary">
149
+ <div class="summary-item"><span>${rows.length}</span> total</div>
150
+ ${Object.entries(counts).filter(([,v]) => v > 0).map(([k, v]) => `<div class="summary-item"><span>${v}</span> ${k}</div>`).join('\n ')}
151
+ ${integrityCount > 0 ? `<div class="summary-item" style="border-left:3px solid #fbbf24"><span>${integrityCount}</span> integrity warnings</div>` : ''}
152
+ </div>
153
+
154
+ <div class="filters">
155
+ <button class="filter-btn active" onclick="filter('all')">All</button>
156
+ ${['Synced','Added','Modified','Deleted','Renamed','Deferred'].map(s => `<button class="filter-btn" onclick="filter('${s}')">${s}</button>`).join('\n ')}
157
+ </div>
158
+
159
+ <table id="main-table">
160
+ <thead>
161
+ <tr>
162
+ <th>Raw File</th>
163
+ <th>Status</th>
164
+ <th>Wiki Page(s)</th>
165
+ <th>Last Ingested</th>
166
+ <th>Raw SHA256</th>
167
+ <th>Wiki SHA256</th>
168
+ </tr>
169
+ </thead>
170
+ <tbody>
171
+ ${rowsHtml}
172
+ </tbody>
173
+ </table>
174
+
175
+ ${integrityRows}
176
+
177
+ <script>
178
+ let currentFilter = 'all';
179
+ function filter(status) {
180
+ currentFilter = status;
181
+ document.querySelectorAll('.filter-btn').forEach(b => b.classList.toggle('active', b.textContent === (status === 'all' ? 'All' : status)));
182
+ document.querySelectorAll('#main-table tbody tr').forEach(tr => {
183
+ tr.classList.toggle('hidden', status !== 'all' && tr.dataset.status !== status);
184
+ });
185
+ }
186
+ </script>
187
+ </body>
188
+ </html>`;
189
+ }
190
+
191
+ const args = process.argv.slice(2);
192
+
193
+ if (args.length < 2) usage();
194
+
195
+ const wikiPath = args[0];
196
+ const outputPath = args[1];
197
+
198
+ let scanData = null;
199
+ const scanJsonIdx = args.indexOf('--scan-json');
200
+ if (scanJsonIdx !== -1 && args[scanJsonIdx + 1]) {
201
+ try {
202
+ scanData = JSON.parse(fs.readFileSync(args[scanJsonIdx + 1], 'utf8'));
203
+ } catch (e) {
204
+ console.error('Cannot read scan JSON:', e.message);
205
+ process.exit(1);
206
+ }
207
+ }
208
+
209
+ const html = generateHtml(wikiPath, scanData);
210
+ fs.writeFileSync(outputPath, html);
211
+ console.log(`[wiki-status-html] Report written to ${outputPath}`);
@@ -0,0 +1,49 @@
1
+ # Manifest Format
2
+
3
+ Each wiki has one manifest at `wikis/<name>/.wiki-manifest.json`. A backup is written to `.wiki-manifest.backup.json` before every mutation.
4
+
5
+ ## Schema
6
+
7
+ ```json
8
+ {
9
+ "version": "1.0",
10
+ "wiki_name": "auditor",
11
+ "last_ingestion": null,
12
+ "raw_to_wiki": {
13
+ "raw/sub/paper.pdf": {
14
+ "hash": "sha256hex",
15
+ "modified": 1748600000000,
16
+ "ingested": 1748600000000,
17
+ "deferred": false,
18
+ "wiki_pages": ["wiki/concept-a.md"]
19
+ }
20
+ },
21
+ "wiki_to_raw": {
22
+ "wiki/concept-a.md": {
23
+ "hash": "sha256hex",
24
+ "last_compiled": 1748600000000,
25
+ "sources": ["raw/sub/paper.pdf"],
26
+ "provenance_unknown": false
27
+ }
28
+ }
29
+ }
30
+ ```
31
+
32
+ ## Field Notes
33
+
34
+ - `modified` / `last_compiled`: milliseconds since epoch (for fast mtime comparison)
35
+ - `deferred: true`: file scanned and hash stored, but no LLM ingestion run yet
36
+ - `provenance_unknown: true`: set by `rebuild-manifest` when wiki_to_raw entries are derived from disk scan without known source links
37
+ - Both `raw_to_wiki` and `wiki_to_raw` paths are relative to the wiki root (`wikis/<name>/`)
38
+
39
+ ## Empty Manifest (written by `init`)
40
+
41
+ ```json
42
+ {
43
+ "version": "1.0",
44
+ "wiki_name": "<name>",
45
+ "last_ingestion": null,
46
+ "raw_to_wiki": {},
47
+ "wiki_to_raw": {}
48
+ }
49
+ ```
package/skill/SKILL.md ADDED
@@ -0,0 +1,318 @@
1
+ ---
2
+ name: wiki
3
+ description: >
4
+ Multi-wiki sync layer for the Karpathy LLM Wiki pattern. Manages multiple independent
5
+ named wikis per project with sync observability, rename detection, and deferred ingestion.
6
+ Use when user types /wiki followed by a subcommand: init, use, list, status, ingest,
7
+ trace, report, recover, rebuild-manifest.
8
+ ---
9
+
10
+ # Wiki Skill
11
+
12
+ You are the `/wiki` command handler. Parse the first argument to determine the subcommand, then follow the instructions for that subcommand exactly.
13
+
14
+ **Active wiki**: read from `.wiki-context` in the project root (the current working directory). Commands that require an active wiki must fail with this message if `.wiki-context` is missing or the named wiki no longer exists:
15
+ > No active wiki. Run `/wiki use <name>` or `/wiki list` to see available wikis.
16
+
17
+ See [MANIFEST-FORMAT.md](MANIFEST-FORMAT.md) for the manifest schema.
18
+
19
+ ---
20
+
21
+ ## `/wiki init <name>`
22
+
23
+ 1. If `<name>` is not provided: print `"Usage: /wiki init <name>"` and stop.
24
+ 2. If `wikis/<name>/` already exists: print `"Wiki '<name>' already exists. Use /wiki use <name> to activate it."` and stop.
25
+ 3. Create directories: `wikis/<name>/raw/` and `wikis/<name>/wiki/`
26
+ 4. Write `wikis/<name>/WIKI.md` using the content from [WIKI-TEMPLATE.md](WIKI-TEMPLATE.md), replacing `{{WIKI_NAME}}` with the actual name.
27
+ 5. Write `wikis/<name>/.wiki-manifest.json`:
28
+ ```json
29
+ {
30
+ "version": "1.0",
31
+ "wiki_name": "<name>",
32
+ "last_ingestion": null,
33
+ "raw_to_wiki": {},
34
+ "wiki_to_raw": {}
35
+ }
36
+ ```
37
+ 6. Print:
38
+ ```
39
+ Wiki '<name>' created at wikis/<name>/
40
+
41
+ Next steps:
42
+ 1. Add source files to wikis/<name>/raw/
43
+ 2. Run /wiki use <name> to activate this wiki
44
+ 3. Run /wiki status to see what needs ingesting
45
+ 4. Run /wiki ingest to compile your first wiki pages
46
+
47
+ Tip: git add wikis/<name>/ && git commit -m "Add <name> wiki"
48
+ ```
49
+
50
+ ---
51
+
52
+ ## `/wiki list`
53
+
54
+ 1. Run: `wiki-scan --list .`
55
+ 2. Parse the JSON array output.
56
+ 3. If the array is empty: print `"No wikis found. Run /wiki use <name> to create one."` and stop.
57
+ 4. For each wiki entry, print one line:
58
+ ```
59
+ <name> (0 unsynced, 0 integrity warnings)
60
+ ```
61
+ Use the manifest's `raw_to_wiki` count for unsynced (files where `ingested` is null and `deferred` is false) and any wiki integrity warning data available.
62
+ 5. Mark the active wiki (from `.wiki-context`) with `[active]` suffix if set.
63
+
64
+ ---
65
+
66
+ ## `/wiki use <name>`
67
+
68
+ 1. If `<name>` is not provided: print `"Usage: /wiki use <name>"` and stop.
69
+ 2. Check that `wikis/<name>/.wiki-manifest.json` exists. If not: print `"Wiki '<name>' not found. Run /wiki list to see available wikis."` and stop.
70
+ 3. Write `<name>` to `.wiki-context` (overwrite if exists). The file should contain only the wiki name, no trailing whitespace.
71
+ 4. Print: `"Active wiki set to: <name>"`
72
+
73
+ ---
74
+
75
+ ## `/wiki status`
76
+
77
+ ### No active wiki (`.wiki-context` missing or empty)
78
+ Run `wiki-scan --list .` and print a project-wide summary:
79
+ ```
80
+ Wikis in this project:
81
+
82
+ auditor 0 unsynced
83
+ developer 2 unsynced
84
+
85
+ Run /wiki use <name> to activate a wiki and see detailed status.
86
+ ```
87
+ Count "unsynced" as entries in `raw_to_wiki` where `ingested` is null and `deferred` is false, plus any added/modified files visible in the manifest's stored state.
88
+
89
+ ### With active wiki
90
+ 1. Read `.wiki-context` to get `<name>`.
91
+ 2. Run: `wiki-scan --status wikis/<name>`
92
+ 3. Parse the JSON output. Fields: `wiki_name`, `added[]`, `modified[]`, `deleted[]`, `renamed[]` (objects with `from`/`to`), `deferred[]`, `wiki_integrity[]`.
93
+ 4. Format terminal report:
94
+ ```
95
+ Wiki: <name>
96
+
97
+ Raw changes
98
+ Added (N): raw/file1.md, raw/file2.md
99
+ Modified (N): —
100
+ Deleted (N): —
101
+ Renamed (N): raw/old.md → raw/new.md
102
+
103
+ Deferred (N): —
104
+
105
+ Wiki integrity warnings (N): —
106
+ ```
107
+ Use `—` for empty lists. For renamed, format each pair as `<from> → <to>`.
108
+ 5. If `added + modified + deleted + renamed + deferred + wiki_integrity` are all zero: print `"Everything is in sync."` instead of the table.
109
+ 6. **Always show wiki integrity warnings as a separate section**, even when other raw changes exist. Never suppress them.
110
+
111
+ ---
112
+
113
+ ## `/wiki ingest [--defer]`
114
+
115
+ Requires active wiki. Read `.wiki-context` for `<name>`.
116
+
117
+ ### With `--defer`
118
+
119
+ 1. Run: `wiki-scan --status wikis/<name>`
120
+ 2. Collect all files in `added` and `modified` from the scan output.
121
+ 3. If none: print `"Nothing to defer. Run /wiki status to check."` and stop.
122
+ 4. For each file, run: `wiki-ingest-record wikis/<name> --defer <file>`
123
+ 5. Print:
124
+ ```
125
+ Deferred N file(s). No tokens consumed.
126
+ Run /wiki ingest (without --defer) when ready to process them.
127
+ ```
128
+ 6. **Zero LLM calls** — this entire command is pure script execution. Do not read or process any raw files.
129
+
130
+ ### Without `--defer`
131
+
132
+ 1. Run: `wiki-scan --status wikis/<name>`
133
+ 2. Collect files to ingest: all in `added` + `modified` + any entries in the manifest where `deferred: true`.
134
+ 3. If `wiki_integrity` warnings exist, address them **first** (see Wiki Integrity Resolution below) before proceeding.
135
+ 4. If nothing to ingest after integrity resolution: print `"Nothing to ingest. Everything is in sync."` and stop.
136
+ 5. Present the file list:
137
+ ```
138
+ Ready to ingest N file(s) into wiki '<name>':
139
+ - raw/file1.md
140
+ - raw/file2.md
141
+ Proceed? (y/n)
142
+ ```
143
+ 6. On `n`: stop.
144
+ 7. For each file to ingest:
145
+ a. Read the file at `wikis/<name>/<raw-file>`
146
+ b. Read `wikis/<name>/WIKI.md` for domain conventions
147
+ c. Determine the wiki page path: `wiki/<basename-without-ext>.md` — if a page already exists for this raw file in the manifest (`wiki_pages`), use that path instead
148
+ d. Write or update the wiki page at `wikis/<name>/<wiki-page>` based on the raw file content and WIKI.md conventions. Include frontmatter:
149
+ ```yaml
150
+ ---
151
+ sources:
152
+ - <raw-file-relpath>
153
+ last_compiled: <ISO timestamp>
154
+ ---
155
+ ```
156
+ e. Run: `wiki-ingest-record wikis/<name> <raw-file> <wiki-page>`
157
+ f. Print: `✓ <raw-file> → <wiki-page>`
158
+ 8. Print: `"Ingested N file(s). Run /wiki status to verify."`
159
+
160
+ ### Wiki Integrity Resolution
161
+
162
+ When `wiki-scan --status` returns entries in `wiki_integrity`:
163
+ 1. Print: `"Wiki integrity warning: the following wiki files have been manually modified (hash mismatch):"`
164
+ 2. List the affected files.
165
+ 3. For each affected file, ask: `"Was this edit to <wiki-page> intentional? (y/n)"`
166
+ - `y` → run `wiki-ingest-record wikis/<name> <matching-raw-file> --update-hash-only` (keeps edit, updates stored hash)
167
+ - `n` → re-ingest the source raw file: read raw, rewrite wiki page, run `wiki-ingest-record` normally
168
+ 4. Never auto-resolve. Always ask.
169
+
170
+ ---
171
+
172
+ ## `/wiki trace <file>`
173
+
174
+ Requires active wiki.
175
+
176
+ 1. Read `.wiki-context` for `<name>`.
177
+ 2. Load `wikis/<name>/.wiki-manifest.json`.
178
+ 3. Normalize `<file>`: strip leading `wikis/<name>/` prefix if present. The remaining path should start with `raw/` or `wiki/`.
179
+ 4. Determine direction:
180
+ - If path starts with `raw/`: look up `manifest.raw_to_wiki[<file>]`
181
+ - If path starts with `wiki/`: look up `manifest.wiki_to_raw[<file>]`
182
+ - If neither matches: try both directions
183
+ 5. If not found in manifest: print `"<file> not found in manifest. Has it been ingested?"` and stop.
184
+ 6. Print:
185
+
186
+ **Raw → Wiki direction:**
187
+ ```
188
+ Tracing: raw/transformers.md
189
+
190
+ Status: Synced (or: Deferred / Pending)
191
+ Ingested: 2026-05-30 12:00:00 UTC (or: —)
192
+ Raw hash: a1b2c3d4
193
+ Wiki pages:
194
+ wiki/transformers.md
195
+ Hash: e5f6g7h8
196
+ Compiled: 2026-05-30 12:00:00 UTC
197
+ Sources: raw/transformers.md
198
+ ```
199
+
200
+ **Wiki → Raw direction:**
201
+ ```
202
+ Tracing: wiki/transformers.md
203
+
204
+ Hash: e5f6g7h8
205
+ Compiled: 2026-05-30 12:00:00 UTC
206
+ Sources:
207
+ raw/transformers.md
208
+ Hash: a1b2c3d4
209
+ Ingested: 2026-05-30 12:00:00 UTC
210
+ ```
211
+
212
+ Use `—` for null/missing values. Format timestamps as `YYYY-MM-DD HH:MM:SS UTC`.
213
+
214
+ ---
215
+
216
+ ## `/wiki report`
217
+
218
+ Requires active wiki.
219
+
220
+ 1. Read `.wiki-context` for `<name>`.
221
+ 2. Run: `wiki-scan --status wikis/<name>`
222
+ 3. Write the scan JSON output to a temp file at `/tmp/wiki-scan-<name>.json`
223
+ 4. Run: `wiki-status-html wikis/<name> /tmp/wiki-report-<name>.html --scan-json /tmp/wiki-scan-<name>.json`
224
+ 5. Open the HTML file in the default browser:
225
+ - Linux: `xdg-open /tmp/wiki-report-<name>.html`
226
+ - macOS: `open /tmp/wiki-report-<name>.html`
227
+ - If `open`/`xdg-open` fails: print the path instead: `"Report generated: /tmp/wiki-report-<name>.html"`
228
+ 6. Print: `"Report generated: /tmp/wiki-report-<name>.html"`
229
+
230
+ ---
231
+
232
+ ## `/wiki recover manifest`
233
+
234
+ Requires active wiki.
235
+
236
+ 1. Check `wikis/<name>/.wiki-manifest.backup.json` exists. If not: print `"No backup found at wikis/<name>/.wiki-manifest.backup.json. Cannot recover."` and stop.
237
+ 2. Parse the backup file and extract `last_ingestion` and entry counts.
238
+ 3. Print:
239
+ ```
240
+ Backup found:
241
+ Last ingestion: <timestamp or "—">
242
+ Raw entries: N
243
+ Wiki entries: M
244
+ ```
245
+ 4. Ask: `"Restore manifest from this backup? (y/n)"`
246
+ 5. On `n`: print `"Aborted."` and stop.
247
+ 6. On `y`: copy backup over manifest (`wikis/<name>/.wiki-manifest.backup.json` → `wikis/<name>/.wiki-manifest.json`).
248
+ 7. Print: `"Manifest restored from backup."`
249
+
250
+ ---
251
+
252
+ ## `/wiki rebuild-manifest`
253
+
254
+ Requires active wiki.
255
+
256
+ 1. Print:
257
+ ```
258
+ WARNING: This will re-derive the manifest from raw/ and wiki/ file hashes.
259
+ Wiki provenance (source→page links) will be lost and marked as unknown.
260
+ This cannot be undone unless you have a git backup.
261
+ ```
262
+ 2. Ask: `"Proceed with manifest rebuild? (y/n)"`
263
+ 3. On `n`: print `"Aborted."` and stop.
264
+ 4. On `y`:
265
+ a. Write backup of current manifest first (copy to `.wiki-manifest.backup.json`)
266
+ b. Walk `wikis/<name>/raw/` recursively, compute SHA-256 for every file
267
+ c. Walk `wikis/<name>/wiki/` recursively, compute SHA-256 for every file
268
+ d. Build new manifest:
269
+ - `raw_to_wiki`: one entry per raw file with `{hash, modified: <mtimeMs>, ingested: null, deferred: false, wiki_pages: []}`
270
+ - `wiki_to_raw`: one entry per wiki file with `{hash, last_compiled: null, sources: [], provenance_unknown: true}`
271
+ e. Write new manifest atomically (write to `.tmp`, rename over manifest)
272
+ 5. Print:
273
+ ```
274
+ Manifest rebuilt from disk.
275
+ N raw files, M wiki files found.
276
+ All wiki provenance marked as unknown.
277
+
278
+ Run /wiki ingest to re-establish source→page links.
279
+ ```
280
+
281
+ ---
282
+
283
+ ## `/wiki recover schema`
284
+
285
+ Requires active wiki.
286
+
287
+ 1. Print:
288
+ ```
289
+ WARNING: This will overwrite wikis/<name>/WIKI.md with the default template.
290
+ Any customizations you have made to this file will be lost.
291
+ ```
292
+ 2. Ask: `"Overwrite WIKI.md with the default template? (y/n)"`
293
+ 3. On `n`: print `"Aborted."` and stop.
294
+ 4. On `y`: write the content from [WIKI-TEMPLATE.md](WIKI-TEMPLATE.md) to `wikis/<name>/WIKI.md`, replacing `{{WIKI_NAME}}` with the actual wiki name.
295
+ 5. Print:
296
+ ```
297
+ WIKI.md reset to default template.
298
+ Customize it to describe your wiki's domain and conventions.
299
+ ```
300
+
301
+ ---
302
+
303
+ ## Unknown subcommand
304
+
305
+ Print:
306
+ ```
307
+ Unknown subcommand. Available commands:
308
+ /wiki init <name> Create a new wiki
309
+ /wiki use <name> Set the active wiki
310
+ /wiki list List all wikis
311
+ /wiki status Show sync status
312
+ /wiki ingest [--defer] Ingest unsynced files
313
+ /wiki trace <file> Show file provenance
314
+ /wiki report Generate HTML report
315
+ /wiki recover manifest Restore manifest from backup
316
+ /wiki rebuild-manifest Re-derive manifest from disk
317
+ /wiki recover schema Reset WIKI.md to default template
318
+ ```
@@ -0,0 +1,36 @@
1
+ # {{WIKI_NAME}} Wiki
2
+
3
+ This wiki covers: **[describe the knowledge domain here]**
4
+
5
+ ## How This Wiki Works
6
+
7
+ Source files live in `raw/`. When you run `/wiki ingest`, the AI reads each raw file and compiles it into a structured wiki page under `wiki/`. Every wiki page records its source files in frontmatter so provenance is always traceable.
8
+
9
+ ## Wiki Page Format
10
+
11
+ Each compiled wiki page should use this frontmatter:
12
+
13
+ ```yaml
14
+ ---
15
+ sources:
16
+ - raw/filename.md
17
+ last_compiled: YYYY-MM-DDTHH:MM:SSZ
18
+ ---
19
+ ```
20
+
21
+ ## Domain Conventions
22
+
23
+ [Describe what belongs in this wiki, how concepts should be structured, any domain-specific terminology, and what level of detail wiki pages should have.]
24
+
25
+ ## What to Include
26
+
27
+ - Key concepts and their definitions
28
+ - Relationships between concepts
29
+ - Important facts, decisions, or findings from source documents
30
+ - Cross-references between wiki pages where relevant
31
+
32
+ ## What to Exclude
33
+
34
+ - Verbatim copies of source documents
35
+ - Formatting noise or metadata from source files
36
+ - Duplicate information already covered in another wiki page