oh-my-llmwikimode 1.0.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.
Files changed (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +494 -0
  3. package/bin/llmwiki.js +1493 -0
  4. package/docs/INSTALLATION.md +228 -0
  5. package/docs/SCOPE_LOCK.md +79 -0
  6. package/docs/STAGE1_GUIDE.md +265 -0
  7. package/docs/STAGE2_AGENT_TEAM_GUIDE.md +141 -0
  8. package/docs/STAGE3_CONVERSATIONAL_GROWTH_GUIDE.md +50 -0
  9. package/docs/TEST_WORKSHEET.md +120 -0
  10. package/docs/github-private-bootstrap.md +53 -0
  11. package/docs/release.md +79 -0
  12. package/docs/stage4-slice1-manual-test.md +259 -0
  13. package/docs/stage4-slice1-user-guide.md +269 -0
  14. package/docs/user-guide-ko.md +452 -0
  15. package/package.json +76 -0
  16. package/scripts/install-llmwiki.ps1 +229 -0
  17. package/src/config.js +74 -0
  18. package/src/curator/browser-data.js +134 -0
  19. package/src/curator/queue.js +324 -0
  20. package/src/curator/schema.js +237 -0
  21. package/src/curator/scoring.js +83 -0
  22. package/src/hooks.js +199 -0
  23. package/src/librarian/schema.js +218 -0
  24. package/src/librarian/weekly-digest.js +478 -0
  25. package/src/security.js +127 -0
  26. package/src/server.js +860 -0
  27. package/src/stage4/graph-reasoning/analyzer.js +255 -0
  28. package/src/stage4/graph-reasoning/browser-data.js +130 -0
  29. package/src/stage4/graph-reasoning/index.js +35 -0
  30. package/src/stage4/graph-reasoning/loader.js +122 -0
  31. package/src/stage4/graph-reasoning/queue.js +154 -0
  32. package/src/stage4/graph-reasoning/schema.js +190 -0
  33. package/src/team/browser-data.js +142 -0
  34. package/src/team/capabilities.js +79 -0
  35. package/src/team/dispatch.js +108 -0
  36. package/src/team/queue.js +290 -0
  37. package/src/team/schema.js +225 -0
  38. package/src/team/shared-memory.js +183 -0
  39. package/src/todo/browser-data.js +71 -0
  40. package/src/todo/queue.js +159 -0
  41. package/src/todo/schema.js +90 -0
  42. package/src/utils/embedding-model.js +111 -0
  43. package/src/wiki/alias-suggestions.js +180 -0
  44. package/src/wiki/browser-data.js +284 -0
  45. package/src/wiki/doctor.js +218 -0
  46. package/src/wiki/entry-normalizer.js +139 -0
  47. package/src/wiki/ingest.js +443 -0
  48. package/src/wiki/lesson-proposal-analyzer.js +463 -0
  49. package/src/wiki/lesson-proposal-manager.js +331 -0
  50. package/src/wiki/lesson-template.js +182 -0
  51. package/src/wiki/lint.js +294 -0
  52. package/src/wiki/notebooklm-adapter.js +264 -0
  53. package/src/wiki/query.js +304 -0
  54. package/src/wiki/raw-manager.js +400 -0
  55. package/src/wiki/search-feedback.js +211 -0
  56. package/src/wiki/semantic-index.js +333 -0
  57. package/src/wiki/semantic-search.js +170 -0
  58. package/src/wiki/source-ledger.js +370 -0
  59. package/src/wiki/store.js +1329 -0
  60. package/src/wiki/usage-events.js +144 -0
@@ -0,0 +1,370 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { redactSecrets } from "../security.js";
5
+
6
+ const DEFAULT_IMPORTED_AT = "1970-01-01T00:00:00.000Z";
7
+ const LEDGER_RELATIVE_PATH = "raw/sources/ledger.jsonl";
8
+ const SOURCE_FILES_RELATIVE_ROOT = "raw/sources/files";
9
+ const AUDIT_RELATIVE_PATH = ".system/source-ledger/audit.jsonl";
10
+
11
+ function result(success, payload = {}) {
12
+ return { success, ...payload };
13
+ }
14
+
15
+ function isRecord(value) {
16
+ return typeof value === "object" && value !== null && !Array.isArray(value);
17
+ }
18
+
19
+ function nowIso(options = {}) {
20
+ return options.now || new Date().toISOString();
21
+ }
22
+
23
+ function compareStrings(left, right) {
24
+ return String(left ?? "").localeCompare(String(right ?? ""));
25
+ }
26
+
27
+ function normalizeScalar(value, maxLength = 500) {
28
+ return redactSecrets(String(value ?? "")
29
+ .replace(/\r?\n/g, " ")
30
+ .replace(/\s+/g, " ")
31
+ .trim())
32
+ .slice(0, maxLength);
33
+ }
34
+
35
+ function normalizeId(value, label = "id") {
36
+ const id = normalizeScalar(value, 120).toLowerCase();
37
+ if (!/^[a-z0-9][a-z0-9_-]{0,119}$/.test(id)) {
38
+ throw new Error(`Invalid ${label}: ${value}`);
39
+ }
40
+ return id;
41
+ }
42
+
43
+ function normalizeToken(value, fallback, label) {
44
+ const token = normalizeScalar(value || fallback, 120).toLowerCase();
45
+ if (!token) throw new Error(`${label} is required`);
46
+ return token;
47
+ }
48
+
49
+ function normalizeContentHash(value) {
50
+ const hash = normalizeScalar(value, 80).toLowerCase();
51
+ if (!/^[a-f0-9]{64}$/.test(hash)) {
52
+ throw new Error("content_sha256 is required");
53
+ }
54
+ return hash;
55
+ }
56
+
57
+ function normalizeStringList(value, maxLength = 260) {
58
+ const values = Array.isArray(value) ? value : typeof value === "string" ? [value] : [];
59
+ return [...new Set(values.map((item) => normalizeScalar(item, maxLength)).filter(Boolean))]
60
+ .sort(compareStrings);
61
+ }
62
+
63
+ function normalizeBooleanFalse(value, label) {
64
+ if (value === undefined || value === null || value === false) return false;
65
+ throw new Error(`${label} must remain false`);
66
+ }
67
+
68
+ function isAbsolutePath(value) {
69
+ return path.isAbsolute(value) || path.win32.isAbsolute(value) || path.posix.isAbsolute(value);
70
+ }
71
+
72
+ function isInsideOrEqual(parentDirectory, candidatePath) {
73
+ const relativePath = path.relative(parentDirectory, candidatePath);
74
+ return relativePath === "" || (!relativePath.startsWith("..") && !path.isAbsolute(relativePath));
75
+ }
76
+
77
+ function toSlashPath(value) {
78
+ return value.replace(/\\/g, "/");
79
+ }
80
+
81
+ function normalizeWikiRelativePath(wikiRoot, value, label = "path", maxLength = 500) {
82
+ const rawPath = normalizeScalar(value, maxLength);
83
+ if (!rawPath) throw new Error(`Unsafe ${label}: path is required`);
84
+
85
+ if (/^[a-z]:($|[^/\\])/i.test(rawPath)) {
86
+ throw new Error(`Unsafe ${label}: drive-qualified paths are not allowed`);
87
+ }
88
+
89
+ if (isAbsolutePath(rawPath)) {
90
+ const absolutePath = path.resolve(rawPath);
91
+ const absoluteRoot = path.resolve(wikiRoot);
92
+ if (!isInsideOrEqual(absoluteRoot, absolutePath)) {
93
+ throw new Error(`Unsafe ${label}: absolute path is outside wiki root`);
94
+ }
95
+ const relativePath = toSlashPath(path.relative(absoluteRoot, absolutePath));
96
+ if (!relativePath) throw new Error(`Unsafe ${label}: path is required`);
97
+ if (relativePath.split("/").some((segment) => segment === "..")) {
98
+ throw new Error(`Unsafe ${label}: traversal is not allowed`);
99
+ }
100
+ return relativePath;
101
+ }
102
+
103
+ const normalized = toSlashPath(rawPath);
104
+ if (normalized.startsWith("/") || normalized.startsWith("//")) {
105
+ throw new Error(`Unsafe ${label}: absolute path is outside wiki root`);
106
+ }
107
+ if (normalized.split("/").some((segment) => segment === "..")) {
108
+ throw new Error(`Unsafe ${label}: traversal is not allowed`);
109
+ }
110
+
111
+ const resolved = path.resolve(wikiRoot, normalized);
112
+ if (!isInsideOrEqual(path.resolve(wikiRoot), resolved)) {
113
+ throw new Error(`Unsafe ${label}: path is outside wiki root`);
114
+ }
115
+ return normalized;
116
+ }
117
+
118
+ function normalizeStoredPath(wikiRoot, sourceId, value) {
119
+ const storedPath = normalizeWikiRelativePath(wikiRoot, value, "stored path");
120
+ const requiredPrefix = `${SOURCE_FILES_RELATIVE_ROOT}/${sourceId}/`;
121
+ if (!storedPath.startsWith(requiredPrefix)) {
122
+ throw new Error(`Unsafe stored path: must stay under ${requiredPrefix}`);
123
+ }
124
+ return storedPath;
125
+ }
126
+
127
+ function validatedJsonLine(value) {
128
+ const line = JSON.stringify(value);
129
+ JSON.parse(line);
130
+ return `${line}\n`;
131
+ }
132
+
133
+ function validationResult(builder) {
134
+ try {
135
+ return { valid: true, value: builder(), errors: [] };
136
+ } catch (error) {
137
+ return { valid: false, value: null, errors: [error.message] };
138
+ }
139
+ }
140
+
141
+ function validateSourceLedgerAuditRecord(wikiRoot, input) {
142
+ return validationResult(() => {
143
+ if (!isRecord(input)) throw new Error("Source ledger audit record must be an object");
144
+ const action = normalizeToken(input.action, "", "Source ledger audit action");
145
+ return {
146
+ timestamp: normalizeScalar(input.timestamp || DEFAULT_IMPORTED_AT, 80),
147
+ action,
148
+ actor: normalizeScalar(input.actor || "system", 120),
149
+ subject_id: normalizeId(input.subject_id || input.subjectId, "audit subject id"),
150
+ artifact_path: normalizeWikiRelativePath(
151
+ wikiRoot,
152
+ input.artifact_path || input.artifactPath || LEDGER_RELATIVE_PATH,
153
+ "artifact path"
154
+ ),
155
+ source_paths: normalizeStringList(input.source_paths || input.sourcePaths, 500)
156
+ .map((item) => normalizeWikiRelativePath(wikiRoot, item, "source path")),
157
+ executed: normalizeBooleanFalse(input.executed, "executed"),
158
+ };
159
+ });
160
+ }
161
+
162
+ export function getSourceLedgerPaths(wikiRoot, sourceId = "") {
163
+ const rawSourcesDir = path.join(wikiRoot, "raw", "sources");
164
+ const filesRoot = path.join(rawSourcesDir, "files");
165
+ const safeSourceId = sourceId ? normalizeId(sourceId, "source id") : "";
166
+ return {
167
+ root: wikiRoot,
168
+ rawSourcesDir,
169
+ ledgerFile: path.join(wikiRoot, ...LEDGER_RELATIVE_PATH.split("/")),
170
+ filesRoot,
171
+ sourceFilesDir: safeSourceId ? path.join(filesRoot, safeSourceId) : filesRoot,
172
+ systemDir: path.join(wikiRoot, ".system"),
173
+ auditDir: path.join(wikiRoot, ".system", "source-ledger"),
174
+ auditFile: path.join(wikiRoot, ...AUDIT_RELATIVE_PATH.split("/")),
175
+ };
176
+ }
177
+
178
+ export function normalizeSourceRecord(wikiRoot, input, options = {}) {
179
+ if (!isRecord(input)) throw new Error("Source ledger record must be an object");
180
+ const sourceId = normalizeId(input.source_id || input.sourceId, "source id");
181
+ const sourcePath = normalizeWikiRelativePath(wikiRoot, input.source_path || input.sourcePath, "source path");
182
+ const title = normalizeScalar(input.title || sourcePath, 180);
183
+ if (!title) throw new Error("Source ledger title is required");
184
+
185
+ return {
186
+ version: 1,
187
+ source_id: sourceId,
188
+ kind: normalizeToken(input.kind, "unknown", "kind"),
189
+ title,
190
+ source_path: sourcePath,
191
+ stored_path: normalizeStoredPath(wikiRoot, sourceId, input.stored_path || input.storedPath),
192
+ content_sha256: normalizeContentHash(input.content_sha256 || input.contentSha256),
193
+ imported_at: normalizeScalar(input.imported_at || input.importedAt || nowIso(options), 80),
194
+ visibility: normalizeToken(input.visibility, "local", "visibility"),
195
+ redaction: normalizeToken(input.redaction, "metadata_only", "redaction"),
196
+ creates_wiki_entry: false,
197
+ };
198
+ }
199
+
200
+ export function appendSourceRecord(wikiRoot, input, options = {}) {
201
+ try {
202
+ const record = normalizeSourceRecord(wikiRoot, input, options);
203
+ const paths = getSourceLedgerPaths(wikiRoot, record.source_id);
204
+ fs.mkdirSync(path.dirname(paths.ledgerFile), { recursive: true });
205
+ fs.mkdirSync(paths.sourceFilesDir, { recursive: true });
206
+ fs.appendFileSync(paths.ledgerFile, validatedJsonLine(record), { encoding: "utf-8", flag: "a" });
207
+ return result(true, {
208
+ record,
209
+ path: LEDGER_RELATIVE_PATH,
210
+ source_files_dir: `${SOURCE_FILES_RELATIVE_ROOT}/${record.source_id}`
211
+ });
212
+ } catch (error) {
213
+ return result(false, { error: error.message });
214
+ }
215
+ }
216
+
217
+ export function readSourceRecords(wikiRoot) {
218
+ try {
219
+ const paths = getSourceLedgerPaths(wikiRoot);
220
+ if (!fs.existsSync(paths.ledgerFile)) return result(true, { records: [] });
221
+ const lines = fs.readFileSync(paths.ledgerFile, "utf-8")
222
+ .split(/\r?\n/)
223
+ .filter((line) => line.trim());
224
+ const records = lines.map((line) => normalizeSourceRecord(wikiRoot, JSON.parse(line)));
225
+ return result(true, { records });
226
+ } catch (error) {
227
+ return result(false, { error: error.message, records: [] });
228
+ }
229
+ }
230
+
231
+ export function appendSourceLedgerAudit(wikiRoot, record, options = {}) {
232
+ const validation = validateSourceLedgerAuditRecord(wikiRoot, {
233
+ timestamp: nowIso(options),
234
+ actor: options.actor || "system",
235
+ executed: false,
236
+ ...record,
237
+ });
238
+ if (!validation.valid) return result(false, { error: validation.errors.join("; ") });
239
+ const paths = getSourceLedgerPaths(wikiRoot);
240
+ fs.mkdirSync(paths.auditDir, { recursive: true });
241
+ fs.appendFileSync(paths.auditFile, validatedJsonLine(validation.value), { encoding: "utf-8", flag: "a" });
242
+ return result(true, { audit: validation.value });
243
+ }
244
+
245
+ const TEXT_EXTENSIONS = new Set([
246
+ ".txt", ".md", ".markdown", ".json", ".js", ".ts", ".jsx", ".tsx",
247
+ ".css", ".scss", ".less", ".html", ".htm", ".xml", ".yaml", ".yml",
248
+ ".csv", ".log", ".ini", ".conf", ".cfg", ".sh", ".bash", ".ps1",
249
+ ".py", ".rb", ".go", ".rs", ".java", ".c", ".cpp", ".h", ".hpp",
250
+ ".php", ".sql", ".r", ".swift", ".kt", ".scala", ".clj", ".ex",
251
+ ".exs", ".elm", ".erl", ".hrl", ".fs", ".fsx", ".gd", ".gdscript",
252
+ ".nim", ".nims", ".ml", ".mli", ".opam", ".re", ".rei", ".v",
253
+ ".vhd", ".vhdl", ".zig", ".wasm", ".wat", ".coffee", ".litcoffee",
254
+ ".dart", ".flutter", ".groovy", ".gvy", ".gy", ".gsh", ".julia",
255
+ ".jl", ".lua", ".moon", ".nuspec", ".purs", ".purescript", ".rkt",
256
+ ".scheme", ".scm", ".ss", ".sls", ".sld", ".sps", ".st", ".smalltalk",
257
+ ".tcl", ".tk", ".wasm", ".wat", ".wren", ".xquery", ".xq", ".xsl",
258
+ ".xslt", ".axd", ".coffee", ".litcoffee", ".iced", ".cjsx",
259
+ ]);
260
+
261
+ function isTextFile(filePath) {
262
+ const ext = path.extname(filePath).toLowerCase();
263
+ if (ext) return TEXT_EXTENSIONS.has(ext);
264
+ // If no extension, try to read a small sample
265
+ try {
266
+ const sample = fs.readFileSync(filePath, "utf-8").slice(0, 1024);
267
+ // If it contains null bytes, it's likely binary
268
+ return !sample.includes("\0");
269
+ } catch {
270
+ return false;
271
+ }
272
+ }
273
+
274
+ export function importSourceFile(wikiRoot, filePath, options = {}) {
275
+ try {
276
+ const absPath = path.resolve(String(filePath || "").trim());
277
+ if (!absPath || !fs.existsSync(absPath)) {
278
+ return result(false, { error: `Source file not found: ${filePath}` });
279
+ }
280
+
281
+ const filename = path.basename(absPath);
282
+ const isText = isTextFile(absPath);
283
+ const importedAt = nowIso(options);
284
+ const sourceIdInput = options.source_id || options.sourceId || filename.replace(/\.[^.]+$/, "").replace(/[^a-zA-Z0-9_-]/g, "_").toLowerCase();
285
+ const sourceId = normalizeId(sourceIdInput, "source id");
286
+ const paths = getSourceLedgerPaths(wikiRoot, sourceId);
287
+ const title = options.title || filename;
288
+
289
+ let contentSha256;
290
+ let storedRel;
291
+ let redactionType;
292
+ let hasContentFile = false;
293
+
294
+ if (isText) {
295
+ const content = fs.readFileSync(absPath, "utf-8");
296
+ contentSha256 = crypto.createHash("sha256").update(content, "utf-8").digest("hex");
297
+ const redactedContent = redactSecrets(content);
298
+ redactionType = content !== redactedContent ? "content_redacted" : "content_clean";
299
+ storedRel = ["raw", "sources", "files", sourceId, "content.txt"].join("/");
300
+ const storedAbs = path.join(wikiRoot, storedRel);
301
+ fs.mkdirSync(path.dirname(storedAbs), { recursive: true });
302
+ fs.writeFileSync(storedAbs, redactedContent, "utf-8");
303
+ hasContentFile = true;
304
+ } else {
305
+ // Non-text: metadata only, compute hash from binary
306
+ const buffer = fs.readFileSync(absPath);
307
+ contentSha256 = crypto.createHash("sha256").update(buffer).digest("hex");
308
+ redactionType = "metadata_only";
309
+ storedRel = ["raw", "sources", "files", sourceId, "manifest.json"].join("/");
310
+ }
311
+
312
+ const manifest = {
313
+ source_id: sourceId,
314
+ original_filename: filename,
315
+ stored_path: storedRel,
316
+ content_sha256: contentSha256,
317
+ imported_at: importedAt,
318
+ kind: options.kind || (isText ? "document" : "binary"),
319
+ title: title,
320
+ size_bytes: fs.statSync(absPath).size,
321
+ is_text: isText,
322
+ redaction: redactionType,
323
+ };
324
+
325
+ const manifestPath = path.join(paths.sourceFilesDir, "manifest.json");
326
+ fs.mkdirSync(paths.sourceFilesDir, { recursive: true });
327
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
328
+
329
+ const ledgerResult = appendSourceRecord(wikiRoot, {
330
+ source_id: sourceId,
331
+ kind: manifest.kind,
332
+ title: title,
333
+ source_path: storedRel,
334
+ stored_path: storedRel,
335
+ content_sha256: contentSha256,
336
+ imported_at: importedAt,
337
+ visibility: options.visibility || "local",
338
+ redaction: redactionType,
339
+ }, { now: importedAt });
340
+
341
+ if (!ledgerResult.success) {
342
+ return result(false, { error: "Ledger append failed: " + ledgerResult.error });
343
+ }
344
+
345
+ const auditResult = appendSourceLedgerAudit(wikiRoot, {
346
+ action: "import_source_file",
347
+ subject_id: sourceId,
348
+ artifact_path: storedRel,
349
+ source_paths: [storedRel],
350
+ }, { actor: options.actor || "cli", now: importedAt });
351
+
352
+ if (!auditResult.success) {
353
+ return result(false, { error: "Audit append failed: " + auditResult.error });
354
+ }
355
+
356
+ return result(true, {
357
+ source_id: sourceId,
358
+ filename: filename,
359
+ stored_path: storedRel,
360
+ manifest_path: ["raw", "sources", "files", sourceId, "manifest.json"].join("/"),
361
+ ledger_path: LEDGER_RELATIVE_PATH,
362
+ audit_path: AUDIT_RELATIVE_PATH,
363
+ content_sha256: contentSha256,
364
+ is_text: isText,
365
+ redacted: redactionType === "content_redacted",
366
+ });
367
+ } catch (error) {
368
+ return result(false, { error: error.message });
369
+ }
370
+ }