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
package/src/hooks.js ADDED
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Hook utilities for oh-my-llmwikimode
3
+ *
4
+ * Safe message parsing and event handling for OpenCode hooks.
5
+ */
6
+
7
+ /**
8
+ * Extract text content from output.parts array.
9
+ *
10
+ * OpenCode UserMessage text is represented in parts, not in message.text.
11
+ * This utility safely extracts text from text parts only.
12
+ *
13
+ * @param {Array} parts - output.parts from chat.message hook
14
+ * @returns {string} - Joined text content
15
+ */
16
+ export function extractTextFromParts(parts) {
17
+ if (!Array.isArray(parts)) {
18
+ return "";
19
+ }
20
+
21
+ const texts = [];
22
+ for (const part of parts) {
23
+ if (part && typeof part.text === "string") {
24
+ texts.push(part.text);
25
+ }
26
+ }
27
+
28
+ return texts.join("");
29
+ }
30
+
31
+ /**
32
+ * Check if text contains problem-solving patterns.
33
+ *
34
+ * @param {string} text
35
+ * @returns {boolean}
36
+ */
37
+ export function hasProblemPattern(text) {
38
+ if (!text || typeof text !== "string") {
39
+ return false;
40
+ }
41
+
42
+ const problemKeywords = [
43
+ "bug", "error", "fail", "crash", "issue", "problem",
44
+ "exception", "broken", "not working", "fails",
45
+ "버그", "오류", "실패", "문제", "에러", "오작동",
46
+ ];
47
+
48
+ const lowerText = text.toLowerCase();
49
+ return problemKeywords.some((kw) => lowerText.includes(kw.toLowerCase()));
50
+ }
51
+
52
+ /**
53
+ * Check if text is an explicit request to save knowledge for later.
54
+ *
55
+ * These requests should be handled by the wiki_store tool so the stored entry
56
+ * is curated instead of auto-capturing the raw user prompt as a duplicate.
57
+ *
58
+ * @param {string} text
59
+ * @returns {boolean}
60
+ */
61
+ export function hasExplicitStorageRequest(text) {
62
+ if (!text || typeof text !== "string") {
63
+ return false;
64
+ }
65
+
66
+ const lowerText = text.toLowerCase();
67
+ const storagePatterns = [
68
+ "save this",
69
+ "store this",
70
+ "remember this",
71
+ "save this lesson",
72
+ "for next time",
73
+ "for later",
74
+ "note this",
75
+ "저장해줘",
76
+ "저장해 줘",
77
+ "저장해둬",
78
+ "저장해 둬",
79
+ "기억해줘",
80
+ "기억해 줘",
81
+ "기록해줘",
82
+ "기록해 줘",
83
+ "다음에 참고",
84
+ "나중에 참고",
85
+ ];
86
+
87
+ return storagePatterns.some((pattern) => lowerText.includes(pattern));
88
+ }
89
+
90
+ /**
91
+ * Check if text is an internal orchestration prompt rather than user knowledge.
92
+ *
93
+ * These prompts can contain words like "problem" or "debugging", but storing
94
+ * them pollutes the wiki with agent instructions instead of reusable lessons.
95
+ *
96
+ * @param {string} text
97
+ * @returns {boolean}
98
+ */
99
+ export function hasInternalInstructionPattern(text) {
100
+ if (!text || typeof text !== "string") {
101
+ return false;
102
+ }
103
+
104
+ const lowerText = text.toLowerCase();
105
+ const internalPatterns = [
106
+ "[analyze-mode]",
107
+ "[search-mode]",
108
+ "analysis mode. gather context before diving deep",
109
+ "maximize search effort",
110
+ "launch multiple background agents",
111
+ "mandatory delegate_task params",
112
+ "<!-- omo_internal_initiator -->",
113
+ ];
114
+
115
+ if (internalPatterns.some((pattern) => lowerText.includes(pattern))) {
116
+ return true;
117
+ }
118
+
119
+ return /1\.\s*task:/i.test(text)
120
+ && /2\.\s*expected outcome:/i.test(text)
121
+ && /3\.\s*required tools:/i.test(text)
122
+ && /4\.\s*must do:/i.test(text)
123
+ && /5\.\s*must not do:/i.test(text)
124
+ && /6\.\s*context:/i.test(text);
125
+ }
126
+
127
+ /**
128
+ * Check if text contains privacy opt-out phrases.
129
+ *
130
+ * @param {string} text
131
+ * @returns {boolean}
132
+ */
133
+ export function hasPrivacyOptOut(text) {
134
+ if (!text || typeof text !== "string") {
135
+ return false;
136
+ }
137
+
138
+ const lowerText = text.toLowerCase();
139
+ const optOutPatterns = [
140
+ "do not store",
141
+ "do not save",
142
+ "don't store",
143
+ "don't save",
144
+ "private",
145
+ "sensitive",
146
+ "confidential",
147
+ "personal",
148
+ "off the record",
149
+ "not for storage",
150
+ "no storage",
151
+ "저장하지 마",
152
+ "저장 말아",
153
+ "비밀",
154
+ "기밀",
155
+ "개인정보",
156
+ "민감정보",
157
+ ];
158
+
159
+ return optOutPatterns.some((pattern) => lowerText.includes(pattern.toLowerCase()));
160
+ }
161
+
162
+ /**
163
+ * Check if text exceeds safe size for auto-memory.
164
+ *
165
+ * @param {string} text
166
+ * @param {number} maxChars - default 5000
167
+ * @returns {boolean}
168
+ */
169
+ export function hasOversizedMessage(text, maxChars = 5000) {
170
+ if (!text || typeof text !== "string") {
171
+ return false;
172
+ }
173
+ return text.length > maxChars;
174
+ }
175
+
176
+ /**
177
+ * Safe event handler that only processes supported event types.
178
+ *
179
+ * @param {object} event
180
+ * @returns {boolean} - true if event was handled
181
+ */
182
+ export function handleSupportedEvent(event) {
183
+ if (!event || !event.type) {
184
+ return false;
185
+ }
186
+
187
+ // Only handle explicitly supported events.
188
+ // Do NOT assume "startup" or any other non-documented event type exists.
189
+ const supportedTypes = [
190
+ // Add supported event types here as they are discovered/documented
191
+ ];
192
+
193
+ if (supportedTypes.includes(event.type)) {
194
+ return true;
195
+ }
196
+
197
+ // Silently ignore unsupported events
198
+ return false;
199
+ }
@@ -0,0 +1,218 @@
1
+ import { redactSecrets } from "../security.js";
2
+
3
+ export const LIBRARIAN_REVIEW_STATUS = "review_required";
4
+ export const DEFAULT_BUILT_AT = "1970-01-01T00:00:00.000Z";
5
+
6
+ function isRecord(value) {
7
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8
+ }
9
+
10
+ function compareStrings(left, right) {
11
+ return String(left ?? "").localeCompare(String(right ?? ""));
12
+ }
13
+
14
+ function normalizeScalar(value, maxLength = 500) {
15
+ return redactSecrets(String(value ?? "")
16
+ .replace(/\r?\n/g, " ")
17
+ .replace(/\s+/g, " ")
18
+ .trim())
19
+ .slice(0, maxLength);
20
+ }
21
+
22
+ function normalizeTextBlock(value, maxLength = 20000) {
23
+ return redactSecrets(String(value ?? "")
24
+ .replace(/\r\n/g, "\n")
25
+ .trim())
26
+ .slice(0, maxLength);
27
+ }
28
+
29
+ function normalizeId(value, label = "id") {
30
+ const id = normalizeScalar(value, 120).toLowerCase();
31
+ if (!/^[a-z0-9][a-z0-9_-]{0,119}$/.test(id)) {
32
+ throw new Error(`Invalid ${label}: ${value}`);
33
+ }
34
+ return id;
35
+ }
36
+
37
+ function normalizeStringList(value, maxLength = 120) {
38
+ const values = Array.isArray(value) ? value : typeof value === "string" ? [value] : [];
39
+ return [...new Set(values.map((item) => normalizeScalar(item, maxLength)).filter(Boolean))]
40
+ .sort(compareStrings);
41
+ }
42
+
43
+ function normalizeBooleanFalse(value, label) {
44
+ if (value === undefined || value === null || value === false) return false;
45
+ throw new Error(`${label} must remain false`);
46
+ }
47
+
48
+ function normalizeReviewStatus(value) {
49
+ const status = normalizeScalar(value || LIBRARIAN_REVIEW_STATUS, 80).toLowerCase();
50
+ if (status !== LIBRARIAN_REVIEW_STATUS) {
51
+ throw new Error(`Librarian artifacts must stay ${LIBRARIAN_REVIEW_STATUS}`);
52
+ }
53
+ return LIBRARIAN_REVIEW_STATUS;
54
+ }
55
+
56
+ function normalizeScore(value, label) {
57
+ if (value === undefined || value === null || value === "") return undefined;
58
+ const number = Number(value);
59
+ if (!Number.isFinite(number)) throw new Error(`Invalid ${label} score`);
60
+ return Math.max(0, Math.min(1, Math.round(number * 1000) / 1000));
61
+ }
62
+
63
+ function validationResult(builder) {
64
+ try {
65
+ return { valid: true, value: builder(), errors: [] };
66
+ } catch (error) {
67
+ return { valid: false, value: null, errors: [error.message] };
68
+ }
69
+ }
70
+
71
+ export function normalizeLibrarianPath(value, label = "path", maxLength = 260) {
72
+ const normalized = normalizeScalar(value, maxLength).replace(/\\/g, "/");
73
+ if (!normalized) throw new Error(`Unsafe ${label}: path is required`);
74
+ if (/^[a-z]:\//i.test(normalized) || normalized.startsWith("/") || normalized.startsWith("//")) {
75
+ throw new Error(`Unsafe ${label}: absolute paths are not allowed`);
76
+ }
77
+ if (normalized.split("/").some((segment) => segment === "..")) {
78
+ throw new Error(`Unsafe ${label}: traversal is not allowed`);
79
+ }
80
+ return normalized;
81
+ }
82
+
83
+ function normalizeDigestEntry(input, label = "digest entry") {
84
+ if (!isRecord(input)) throw new Error(`${label} must be an object`);
85
+ const entry = {
86
+ path: normalizeLibrarianPath(input.path, `${label} path`),
87
+ title: normalizeScalar(input.title || input.path, 180),
88
+ status: normalizeScalar(input.status || "candidate", 80).toLowerCase(),
89
+ category: normalizeScalar(input.category || "", 80).toLowerCase(),
90
+ source: normalizeScalar(input.source || "", 120).toLowerCase(),
91
+ tags: normalizeStringList(input.tags, 80),
92
+ summary: normalizeScalar(input.summary, 240),
93
+ created_at: normalizeScalar(input.created_at || input.createdAt, 80),
94
+ updated_at: normalizeScalar(input.updated_at || input.updatedAt, 80),
95
+ };
96
+
97
+ const confidence = normalizeScore(input.confidence_score ?? input.confidenceScore, "confidence");
98
+ if (confidence !== undefined) entry.confidence_score = confidence;
99
+
100
+ const degree = Number(input.graph_degree ?? input.graphDegree);
101
+ if (Number.isFinite(degree)) entry.graph_degree = Math.max(0, Math.trunc(degree));
102
+
103
+ const daysSinceUpdate = Number(input.days_since_update ?? input.daysSinceUpdate);
104
+ if (Number.isFinite(daysSinceUpdate)) entry.days_since_update = Math.max(0, Math.trunc(daysSinceUpdate));
105
+
106
+ const reason = normalizeScalar(input.reason, 240);
107
+ if (reason) entry.reason = reason;
108
+
109
+ return entry;
110
+ }
111
+
112
+ function normalizeDigestEntries(value, label) {
113
+ const entries = Array.isArray(value) ? value : [];
114
+ return entries.map((entry) => normalizeDigestEntry(entry, label));
115
+ }
116
+
117
+ function normalizeDuplicateCandidate(input) {
118
+ if (!isRecord(input)) throw new Error("duplicate candidate must be an object");
119
+ const type = normalizeScalar(input.type || "unknown", 40).toLowerCase();
120
+ const duplicate = {
121
+ type,
122
+ entries: normalizeStringList(input.entries, 260).map((entryPath) => normalizeLibrarianPath(entryPath, "duplicate entry path")),
123
+ };
124
+ if (duplicate.entries.length < 2) throw new Error("duplicate candidate requires at least two entries");
125
+
126
+ const normalizedTitle = normalizeScalar(input.normalized_title || input.normalizedTitle, 160);
127
+ const hash = normalizeScalar(input.hash, 80);
128
+ if (normalizedTitle) duplicate.normalized_title = normalizedTitle;
129
+ if (hash) duplicate.hash = hash;
130
+ return duplicate;
131
+ }
132
+
133
+ function normalizeDuplicateCandidates(value) {
134
+ const candidates = Array.isArray(value) ? value : [];
135
+ return candidates.map(normalizeDuplicateCandidate);
136
+ }
137
+
138
+ function normalizeSections(value = {}) {
139
+ const sections = isRecord(value) ? value : {};
140
+ return {
141
+ new_entries: normalizeDigestEntries(sections.new_entries || sections.newEntries, "new entry"),
142
+ orphaned_entries: normalizeDigestEntries(sections.orphaned_entries || sections.orphanedEntries, "orphaned entry"),
143
+ duplicate_candidates: normalizeDuplicateCandidates(sections.duplicate_candidates || sections.duplicateCandidates),
144
+ stale_entries: normalizeDigestEntries(sections.stale_entries || sections.staleEntries, "stale entry"),
145
+ promotion_suggestions: normalizeDigestEntries(sections.promotion_suggestions || sections.promotionSuggestions, "promotion suggestion"),
146
+ };
147
+ }
148
+
149
+ function normalizeBoundaries(value = {}) {
150
+ const boundaries = isRecord(value) ? value : {};
151
+ return {
152
+ executed: normalizeBooleanFalse(boundaries.executed, "executed"),
153
+ mutates_sources: normalizeBooleanFalse(boundaries.mutates_sources || boundaries.mutatesSources, "mutates_sources"),
154
+ auto_promote: normalizeBooleanFalse(boundaries.auto_promote || boundaries.autoPromote, "auto_promote"),
155
+ auto_merge: normalizeBooleanFalse(boundaries.auto_merge || boundaries.autoMerge, "auto_merge"),
156
+ auto_delete: normalizeBooleanFalse(boundaries.auto_delete || boundaries.autoDelete, "auto_delete"),
157
+ };
158
+ }
159
+
160
+ function sourcePathsFromSections(sections) {
161
+ return normalizeStringList([
162
+ ...sections.new_entries.map((entry) => entry.path),
163
+ ...sections.orphaned_entries.map((entry) => entry.path),
164
+ ...sections.stale_entries.map((entry) => entry.path),
165
+ ...sections.promotion_suggestions.map((entry) => entry.path),
166
+ ...sections.duplicate_candidates.flatMap((duplicate) => duplicate.entries),
167
+ ], 260).map((entryPath) => normalizeLibrarianPath(entryPath, "source path"));
168
+ }
169
+
170
+ function normalizeSummaryCounts(input, sections) {
171
+ const counts = isRecord(input) ? input : {};
172
+ return {
173
+ total_entries: Number.isFinite(Number(counts.total_entries || counts.totalEntries)) ? Math.max(0, Math.trunc(Number(counts.total_entries || counts.totalEntries))) : 0,
174
+ new_entries: sections.new_entries.length,
175
+ orphaned_entries: sections.orphaned_entries.length,
176
+ duplicate_candidates: sections.duplicate_candidates.length,
177
+ stale_entries: sections.stale_entries.length,
178
+ promotion_suggestions: sections.promotion_suggestions.length,
179
+ };
180
+ }
181
+
182
+ function normalizePeriod(input = {}) {
183
+ const period = isRecord(input) ? input : {};
184
+ return {
185
+ start: normalizeScalar(period.start, 80),
186
+ end: normalizeScalar(period.end, 80),
187
+ days: Number.isFinite(Number(period.days)) ? Math.max(1, Math.trunc(Number(period.days))) : 7,
188
+ };
189
+ }
190
+
191
+ export function validateWeeklyDigestArtifact(input) {
192
+ return validationResult(() => {
193
+ if (!isRecord(input)) throw new Error("Weekly digest artifact must be an object");
194
+ const sections = normalizeSections(input.sections);
195
+ const title = normalizeScalar(input.title || "Weekly Digest", 180);
196
+ const reportMarkdown = normalizeTextBlock(input.report_markdown || input.reportMarkdown, 50000);
197
+ if (!reportMarkdown) throw new Error("Weekly digest report_markdown is required");
198
+
199
+ return {
200
+ schema_version: 1,
201
+ id: normalizeId(input.id, "weekly digest id"),
202
+ type: "weekly_digest",
203
+ title,
204
+ status: normalizeReviewStatus(input.status),
205
+ review_required: true,
206
+ review_only: true,
207
+ period: normalizePeriod(input.period),
208
+ source_paths: sourcePathsFromSections(sections),
209
+ summary_counts: normalizeSummaryCounts(input.summary_counts || input.summaryCounts, sections),
210
+ sections,
211
+ report_markdown: reportMarkdown,
212
+ boundaries: normalizeBoundaries(input.boundaries),
213
+ created_at: normalizeScalar(input.created_at || input.createdAt || DEFAULT_BUILT_AT, 80),
214
+ updated_at: normalizeScalar(input.updated_at || input.updatedAt || DEFAULT_BUILT_AT, 80),
215
+ actor: normalizeScalar(input.actor || "system", 120),
216
+ };
217
+ });
218
+ }