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,229 @@
1
+ param(
2
+ [string]$WikiRoot = "~/Documents/llm-wiki",
3
+ [string]$ConfigPath = ""
4
+ )
5
+
6
+ $ErrorActionPreference = "Stop"
7
+
8
+ $PluginName = "oh-my-llmwikimode"
9
+
10
+ function Get-HomePath {
11
+ if (-not [string]::IsNullOrWhiteSpace($env:USERPROFILE)) {
12
+ return $env:USERPROFILE
13
+ }
14
+
15
+ return $HOME
16
+ }
17
+
18
+ function Expand-UserPath {
19
+ param([string]$InputPath)
20
+
21
+ if ([string]::IsNullOrWhiteSpace($InputPath)) {
22
+ return $InputPath
23
+ }
24
+
25
+ $homePath = Get-HomePath
26
+ if ($InputPath -eq "~") {
27
+ return $homePath
28
+ }
29
+
30
+ if ($InputPath.StartsWith("~/") -or $InputPath.StartsWith("~\")) {
31
+ return Join-Path $homePath $InputPath.Substring(2)
32
+ }
33
+
34
+ return [System.IO.Path]::GetFullPath($InputPath)
35
+ }
36
+
37
+ function Get-DefaultOpenCodeConfigDir {
38
+ return Join-Path (Get-HomePath) ".config\opencode"
39
+ }
40
+
41
+ function ConvertTo-PlainObject {
42
+ param($InputObject)
43
+
44
+ if ($null -eq $InputObject) {
45
+ return $null
46
+ }
47
+
48
+ if ($InputObject -is [System.Management.Automation.PSCustomObject]) {
49
+ $result = [ordered]@{}
50
+ foreach ($property in $InputObject.PSObject.Properties) {
51
+ $result[$property.Name] = ConvertTo-PlainObject $property.Value
52
+ }
53
+ return $result
54
+ }
55
+
56
+ if (($InputObject -is [System.Collections.IEnumerable]) -and -not ($InputObject -is [string])) {
57
+ $items = @()
58
+ foreach ($item in $InputObject) {
59
+ $items += ,(ConvertTo-PlainObject $item)
60
+ }
61
+ return ,$items
62
+ }
63
+
64
+ return $InputObject
65
+ }
66
+
67
+ function Read-OpenCodeConfig {
68
+ param([string]$TargetConfigPath)
69
+
70
+ if (-not (Test-Path -LiteralPath $TargetConfigPath)) {
71
+ return [ordered]@{}
72
+ }
73
+
74
+ $rawJson = Get-Content -LiteralPath $TargetConfigPath -Raw -Encoding UTF8
75
+ if ([string]::IsNullOrWhiteSpace($rawJson)) {
76
+ return [ordered]@{}
77
+ }
78
+
79
+ try {
80
+ $parsed = $rawJson | ConvertFrom-Json
81
+ $plain = ConvertTo-PlainObject $parsed
82
+ if (-not ($plain -is [System.Collections.IDictionary])) {
83
+ throw "opencode.json root must be a JSON object."
84
+ }
85
+ return $plain
86
+ } catch {
87
+ throw "Could not parse $TargetConfigPath. Fix the JSON first; no changes were written. $($_.Exception.Message)"
88
+ }
89
+ }
90
+
91
+ function New-LlmWikiPluginEntry {
92
+ param([string]$ConfiguredWikiRoot)
93
+
94
+ return ,@(
95
+ $PluginName,
96
+ [ordered]@{
97
+ wikiRoot = $ConfiguredWikiRoot
98
+ autoMemoryEnabled = $true
99
+ autoSearchEnabled = $true
100
+ }
101
+ )
102
+ }
103
+
104
+ function Test-LlmWikiPluginEntry {
105
+ param($Entry)
106
+
107
+ $items = @($Entry)
108
+ return ($items.Count -ge 1 -and [string]$items[0] -eq $PluginName)
109
+ }
110
+
111
+ function Add-OrUpdatePluginEntry {
112
+ param(
113
+ [System.Collections.IDictionary]$Config,
114
+ [string]$ConfiguredWikiRoot
115
+ )
116
+
117
+ $pluginEntry = New-LlmWikiPluginEntry $ConfiguredWikiRoot
118
+ $existingPlugins = @()
119
+ if ($Config.Contains("plugin") -and $null -ne $Config["plugin"]) {
120
+ foreach ($entry in @($Config["plugin"])) {
121
+ $existingPlugins += ,$entry
122
+ }
123
+ }
124
+
125
+ $updatedPlugins = @()
126
+ $found = $false
127
+ foreach ($entry in $existingPlugins) {
128
+ if (Test-LlmWikiPluginEntry $entry) {
129
+ $updatedPlugins += ,$pluginEntry
130
+ $found = $true
131
+ } else {
132
+ $updatedPlugins += ,$entry
133
+ }
134
+ }
135
+
136
+ if (-not $found) {
137
+ $updatedPlugins += ,$pluginEntry
138
+ }
139
+
140
+ $Config["plugin"] = @($updatedPlugins)
141
+ return $Config
142
+ }
143
+
144
+ function Get-PluginSnippetJson {
145
+ param([string]$ConfiguredWikiRoot)
146
+
147
+ $snippet = [ordered]@{
148
+ plugin = @(
149
+ (New-LlmWikiPluginEntry $ConfiguredWikiRoot)
150
+ )
151
+ }
152
+ return ($snippet | ConvertTo-Json -Depth 20)
153
+ }
154
+
155
+ function Ensure-WikiDirectories {
156
+ param([string]$ResolvedWikiRoot)
157
+
158
+ $directories = @(
159
+ $ResolvedWikiRoot,
160
+ (Join-Path $ResolvedWikiRoot "inbox"),
161
+ (Join-Path $ResolvedWikiRoot "problems"),
162
+ (Join-Path (Join-Path $ResolvedWikiRoot "editorial") "lessons"),
163
+ (Join-Path $ResolvedWikiRoot "raw"),
164
+ (Join-Path $ResolvedWikiRoot ".system")
165
+ )
166
+
167
+ foreach ($directory in $directories) {
168
+ if (-not (Test-Path -LiteralPath $directory)) {
169
+ New-Item -ItemType Directory -Path $directory -Force | Out-Null
170
+ }
171
+ }
172
+ }
173
+
174
+ function Write-NextSteps {
175
+ param(
176
+ [string]$ResolvedWikiRoot,
177
+ [string]$TargetConfigPath
178
+ )
179
+
180
+ Write-Host ""
181
+ Write-Host "LLM Wiki installation complete." -ForegroundColor Green
182
+ Write-Host "Wiki root: $ResolvedWikiRoot"
183
+ Write-Host "OpenCode config: $TargetConfigPath"
184
+ Write-Host ""
185
+ Write-Host "Next steps:"
186
+ Write-Host "1. Restart OpenCode."
187
+ Write-Host "2. Ask OpenCode to check your wiki status."
188
+ Write-Host ('3. Optional CLI check: npx llmwiki lint --wiki-root "{0}"' -f $WikiRoot)
189
+ }
190
+
191
+ $resolvedWikiRoot = Expand-UserPath $WikiRoot
192
+ $configDir = Get-DefaultOpenCodeConfigDir
193
+ if (-not [string]::IsNullOrWhiteSpace($ConfigPath)) {
194
+ $resolvedConfigPath = Expand-UserPath $ConfigPath
195
+ $configDir = Split-Path -Parent $resolvedConfigPath
196
+ } else {
197
+ $resolvedConfigPath = Join-Path $configDir "opencode.json"
198
+ }
199
+
200
+ Ensure-WikiDirectories $resolvedWikiRoot
201
+
202
+ if (-not (Test-Path -LiteralPath $configDir)) {
203
+ New-Item -ItemType Directory -Path $configDir -Force | Out-Null
204
+ }
205
+
206
+ $configExists = Test-Path -LiteralPath $resolvedConfigPath
207
+ if ($configExists) {
208
+ Write-Host "Existing OpenCode config found: $resolvedConfigPath"
209
+ Write-Host "A timestamped backup will be created before any change."
210
+ $answer = Read-Host "Continue updating opencode.json? [y/N]"
211
+ if ($answer -notmatch "(?i)^(y|yes)$") {
212
+ Write-Host "Config not modified. Add this snippet manually if you want to finish setup:"
213
+ Write-Host (Get-PluginSnippetJson $WikiRoot)
214
+ exit 0
215
+ }
216
+
217
+ $timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
218
+ $backupPath = "$resolvedConfigPath.bak.$timestamp"
219
+ Copy-Item -LiteralPath $resolvedConfigPath -Destination $backupPath -Force
220
+ Write-Host "Backup written: $backupPath"
221
+ }
222
+
223
+ $config = Read-OpenCodeConfig $resolvedConfigPath
224
+ $config = Add-OrUpdatePluginEntry $config $WikiRoot
225
+ $configJson = $config | ConvertTo-Json -Depth 20
226
+ $utf8NoBom = New-Object System.Text.UTF8Encoding($false)
227
+ [System.IO.File]::WriteAllText($resolvedConfigPath, $configJson + [Environment]::NewLine, $utf8NoBom)
228
+
229
+ Write-NextSteps -ResolvedWikiRoot $resolvedWikiRoot -TargetConfigPath $resolvedConfigPath
package/src/config.js ADDED
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Configuration and path resolution for oh-my-llmwikimode
3
+ */
4
+
5
+ import os from "node:os";
6
+ import path from "node:path";
7
+
8
+ /**
9
+ * Expand `~` in a path to the user's home directory.
10
+ *
11
+ * @param {string} inputPath
12
+ * @returns {string}
13
+ */
14
+ export function expandHome(inputPath) {
15
+ if (!inputPath || typeof inputPath !== "string") {
16
+ return inputPath;
17
+ }
18
+
19
+ if (inputPath.startsWith("~/")) {
20
+ return path.join(os.homedir(), inputPath.slice(2));
21
+ }
22
+
23
+ if (inputPath === "~") {
24
+ return os.homedir();
25
+ }
26
+
27
+ return inputPath;
28
+ }
29
+
30
+ /**
31
+ * Resolve wiki root path with precedence:
32
+ * 1. Explicit plugin option `wikiRoot`
33
+ * 2. Environment variable `LLMWIKIMODE_WIKI_ROOT`
34
+ * 3. Default `~/Documents/llm-wiki`
35
+ *
36
+ * @param {object} options - Plugin options from opencode.json
37
+ * @returns {string} - Resolved absolute path
38
+ */
39
+ export function resolveWikiRoot(options = {}) {
40
+ const fromOption = options.wikiRoot;
41
+ const fromEnv = process.env.LLMWIKIMODE_WIKI_ROOT;
42
+ const defaultPath = "~/Documents/llm-wiki";
43
+
44
+ const rawPath = fromOption || fromEnv || defaultPath;
45
+ return path.resolve(expandHome(rawPath));
46
+ }
47
+
48
+ /**
49
+ * Resolve state directory path.
50
+ * Default: `~/.config/opencode/.llmwikimode/`
51
+ *
52
+ * @returns {string} - Resolved absolute path
53
+ */
54
+ export function resolveStateDir() {
55
+ return path.resolve(expandHome("~/.config/opencode/.llmwikimode"));
56
+ }
57
+
58
+ /**
59
+ * Get full configuration object.
60
+ *
61
+ * @param {object} options - Plugin options from opencode.json
62
+ * @returns {object}
63
+ */
64
+ export function getConfig(options = {}) {
65
+ return {
66
+ wikiRoot: resolveWikiRoot(options),
67
+ stateDir: resolveStateDir(),
68
+ autoMemoryEnabled: options.autoMemoryEnabled !== false,
69
+ verboseAutoMemory: options.verboseAutoMemory === true,
70
+ autoSearchEnabled: options.autoSearchEnabled !== false,
71
+ contextBudget: options.contextBudget || 2500,
72
+ maxEntries: options.maxEntries || 3,
73
+ };
74
+ }
@@ -0,0 +1,134 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { getCuratorPaths, readCuratorQueue } from "./queue.js";
4
+ import { validateConsolidationProposal, validateLessonCandidateArtifact } from "./schema.js";
5
+
6
+ function compareStrings(left, right) {
7
+ return String(left ?? "").localeCompare(String(right ?? ""));
8
+ }
9
+
10
+ function summarizeCard(card) {
11
+ return {
12
+ id: card.id,
13
+ kind: card.kind,
14
+ title: card.title,
15
+ status: card.status,
16
+ review_required: card.review_required,
17
+ artifact_path: card.artifact_path,
18
+ source_paths: card.source_paths,
19
+ scoring_hints: card.scoring_hints,
20
+ created_at: card.created_at,
21
+ updated_at: card.updated_at,
22
+ };
23
+ }
24
+
25
+ function summarizeCandidate(candidate, artifactPath) {
26
+ return {
27
+ id: candidate.id,
28
+ title: candidate.title,
29
+ status: candidate.status,
30
+ review_required: candidate.review_required,
31
+ artifact_path: artifactPath,
32
+ source_paths: candidate.source_refs.map((ref) => ref.path),
33
+ related_paths: candidate.related_refs.map((ref) => ref.path),
34
+ proposed_title: candidate.proposed_lesson.title,
35
+ proposed_tags: candidate.proposed_lesson.tags,
36
+ scoring_hints: candidate.scoring_hints,
37
+ created_at: candidate.created_at,
38
+ updated_at: candidate.updated_at,
39
+ };
40
+ }
41
+
42
+ function summarizeProposal(proposal, artifactPath) {
43
+ return {
44
+ id: proposal.id,
45
+ title: proposal.title,
46
+ status: proposal.status,
47
+ review_required: proposal.review_required,
48
+ artifact_path: artifactPath,
49
+ entry_paths: proposal.entry_refs.map((ref) => ref.path),
50
+ rationale: proposal.rationale,
51
+ proposed_actions: proposal.proposed_actions,
52
+ scoring_hints: proposal.scoring_hints,
53
+ created_at: proposal.created_at,
54
+ updated_at: proposal.updated_at,
55
+ };
56
+ }
57
+
58
+ function readJsonArtifacts(directory, relativePrefix, validator, summarizer) {
59
+ if (!fs.existsSync(directory)) return [];
60
+ return fs.readdirSync(directory)
61
+ .filter((fileName) => fileName.endsWith(".json"))
62
+ .sort(compareStrings)
63
+ .flatMap((fileName) => {
64
+ const filePath = path.join(directory, fileName);
65
+ const artifactPath = `${relativePrefix}/${fileName}`;
66
+ try {
67
+ const validation = validator(JSON.parse(fs.readFileSync(filePath, "utf-8")));
68
+ if (!validation.valid) return [];
69
+ return [summarizer(validation.value, artifactPath)];
70
+ } catch {
71
+ return [];
72
+ }
73
+ });
74
+ }
75
+
76
+ function readAuditSummary(wikiRoot) {
77
+ const paths = getCuratorPaths(wikiRoot);
78
+ const actionCounts = {};
79
+ let auditCount = 0;
80
+ let lastRecord = null;
81
+
82
+ if (fs.existsSync(paths.auditFile)) {
83
+ for (const line of fs.readFileSync(paths.auditFile, "utf-8").split(/\r?\n/).filter(Boolean)) {
84
+ try {
85
+ const record = JSON.parse(line);
86
+ const action = String(record.action || "unknown");
87
+ actionCounts[action] = (actionCounts[action] || 0) + 1;
88
+ auditCount += 1;
89
+ lastRecord = record;
90
+ } catch {
91
+ actionCounts.malformed = (actionCounts.malformed || 0) + 1;
92
+ auditCount += 1;
93
+ }
94
+ }
95
+ }
96
+
97
+ return {
98
+ audit_count: auditCount,
99
+ action_counts: Object.fromEntries(Object.entries(actionCounts).sort(([left], [right]) => compareStrings(left, right))),
100
+ last_action: lastRecord?.action || "",
101
+ last_actor: lastRecord?.actor || "",
102
+ last_timestamp: lastRecord?.timestamp || "",
103
+ };
104
+ }
105
+
106
+ export function buildCuratorBrowserData(wikiRoot) {
107
+ const paths = getCuratorPaths(wikiRoot);
108
+ const queue = readCuratorQueue(wikiRoot);
109
+ const queueCards = queue.cards.map(summarizeCard);
110
+ const lessonCandidates = readJsonArtifacts(
111
+ paths.candidatesDir,
112
+ ".system/curator/lesson-candidates",
113
+ validateLessonCandidateArtifact,
114
+ summarizeCandidate
115
+ );
116
+ const consolidationProposals = readJsonArtifacts(
117
+ paths.proposalsDir,
118
+ ".system/curator/consolidation-proposals",
119
+ validateConsolidationProposal,
120
+ summarizeProposal
121
+ );
122
+
123
+ return {
124
+ queue_cards: queueCards,
125
+ lesson_candidates: lessonCandidates,
126
+ consolidation_proposals: consolidationProposals,
127
+ audit_summary: {
128
+ ...readAuditSummary(wikiRoot),
129
+ card_count: queueCards.length,
130
+ lesson_candidate_count: lessonCandidates.length,
131
+ consolidation_proposal_count: consolidationProposals.length,
132
+ },
133
+ };
134
+ }