total-recall-brain 3.41.4 → 3.41.6

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 (147) hide show
  1. package/bin/total-recall.mjs +10 -6
  2. package/docs/reference/cli-reference.md +18 -0
  3. package/frontend/dist/assets/index-4tGQfTa2.js +5605 -0
  4. package/frontend/dist/assets/index-DtwsDyFM.css +1 -0
  5. package/frontend/dist/index.html +2 -2
  6. package/frontend/package.json +11 -0
  7. package/frontend/src/App.tsx +47 -122
  8. package/frontend/src/components/plugins/plugin-frame.ts +1 -0
  9. package/frontend/src/index.css +152 -86
  10. package/frontend/src/pages/HelpPage.tsx +13 -10
  11. package/frontend/src/pages/MemoryPage.tsx +28 -30
  12. package/frontend/src/pages/NetworkPage.tsx +3 -3
  13. package/frontend/src/pages/PluginsPage.tsx +6 -5
  14. package/frontend/src/pages/RulesPage.tsx +16 -12
  15. package/package.json +4 -3
  16. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +3114 -36
  17. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package.json +4 -1
  18. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +3114 -36
  19. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package.json +4 -1
  20. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +3121 -33
  21. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package.json +4 -1
  22. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package-lock.json +3122 -34
  23. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package.json +4 -1
  24. package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +3127 -49
  25. package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +5 -2
  26. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/mesh.mjs +1 -1
  27. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/capability/index.mjs +136 -7
  28. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/cli.mjs +135 -26
  29. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package-lock.json +3121 -33
  30. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package.json +5 -2
  31. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/plugin.json +228 -9
  32. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/skills/meta-harness/SKILL.md +24 -30
  33. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/alerts.mjs +29 -16
  34. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/cascade.mjs +73 -0
  35. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/coordinator.mjs +182 -365
  36. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/dispatch.mjs +33 -0
  37. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/documentation.mjs +40 -14
  38. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/executor.mjs +4 -4
  39. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/agy.mjs +187 -0
  40. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/claude.mjs +275 -0
  41. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/cline.mjs +132 -0
  42. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/codex.mjs +187 -0
  43. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/cursor.mjs +158 -0
  44. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/devin.mjs +157 -0
  45. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/dsh.mjs +183 -0
  46. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/grok.mjs +132 -0
  47. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/hermes.mjs +173 -0
  48. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/index.mjs +87 -0
  49. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/openclaw.mjs +173 -0
  50. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/pi.mjs +129 -0
  51. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate.mjs +12 -0
  52. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/json-reply.mjs +22 -0
  53. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/learner.mjs +72 -70
  54. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/model-catalog.mjs +195 -0
  55. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/paths.mjs +11 -0
  56. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/pool.mjs +36 -0
  57. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/review.mjs +169 -0
  58. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/router.mjs +136 -346
  59. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/runtime.mjs +1 -1
  60. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/ssss-store.mjs +20 -0
  61. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/telemetry.mjs +43 -0
  62. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/threads.mjs +156 -0
  63. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worker.mjs +117 -35
  64. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worktree.mjs +51 -0
  65. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/card.js +398 -57
  66. package/scaffold/.agent/skills/total-recall/plugins/openwiki/cli.mjs +304 -46
  67. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +367 -41
  68. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +5 -2
  69. package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +33 -0
  70. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/config.mjs +2 -2
  71. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/generate.mjs +159 -0
  72. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/graph.mjs +197 -0
  73. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/lint.mjs +153 -0
  74. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/mesh-vaults.mjs +261 -0
  75. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/refresh.mjs +151 -4
  76. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/search.mjs +114 -0
  77. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/token.mjs +96 -0
  78. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/card.js +899 -662
  79. package/scaffold/.agent/skills/total-recall/plugins/openwiki/vendor/openwiki.mjs +8987 -0
  80. package/scaffold/.agent/skills/total-recall/plugins/openwiki/vendor/skills/mermaid-diagrams/SKILL.md +44 -0
  81. package/scaffold/.agent/skills/total-recall/plugins/openwiki/vendor/skills/write-connector/SKILL.md +56 -0
  82. package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +3097 -81
  83. package/scaffold/.agent/skills/total-recall/plugins/security/package.json +4 -1
  84. package/src/cli/agent.mjs +12 -1
  85. package/src/cli/compact.mjs +108 -0
  86. package/src/cli/connect.mjs +56 -1
  87. package/src/cli/context.mjs +55 -23
  88. package/src/cli/doctor.mjs +5 -2
  89. package/src/cli/forget.mjs +2 -0
  90. package/src/cli/harness.mjs +62 -50
  91. package/src/cli/memory-entry.mjs +2 -2
  92. package/src/cli/preflight.mjs +152 -16
  93. package/src/cli/release.mjs +386 -0
  94. package/src/cli/remember.mjs +19 -1
  95. package/src/cli/rules.mjs +54 -12
  96. package/src/cli/startup.mjs +1 -1
  97. package/src/cli/status.mjs +12 -2
  98. package/src/core/claude-hooks.mjs +41 -3
  99. package/src/core/cli-commands.mjs +2 -0
  100. package/src/core/cli-semantic-index.mjs +3 -2
  101. package/src/core/conflict-detector.mjs +23 -12
  102. package/src/core/context-compactor.mjs +145 -0
  103. package/src/core/context-policy.mjs +20 -0
  104. package/src/core/duplicate-guard.mjs +350 -0
  105. package/src/core/meta-harness.mjs +31 -11
  106. package/src/core/preflight-gate.mjs +2 -2
  107. package/src/core/preflight-receipt.mjs +29 -1
  108. package/src/core/scaffold-plugins.mjs +7 -0
  109. package/src/core/stdio-flush.mjs +23 -0
  110. package/src/core/surface.mjs +33 -10
  111. package/src/server/invoke-durability.mjs +39 -0
  112. package/src/server/invoke-write-child.mjs +25 -0
  113. package/src/server/routes/invoke.mjs +7 -0
  114. package/src/server/routes/plugins.mjs +2 -2
  115. package/frontend/dist/assets/index-BIQmqDKP.css +0 -1
  116. package/frontend/dist/assets/index-CRzjQLu7.js +0 -5631
  117. package/frontend/src/components/DaemonLogsTab.tsx +0 -54
  118. package/frontend/src/components/ResearchAgendaTab.tsx +0 -712
  119. package/frontend/src/components/TaskQueueTab.tsx +0 -193
  120. package/frontend/src/components/VoiceInput.tsx +0 -112
  121. package/frontend/src/pages/ChatPage.tsx +0 -1059
  122. package/frontend/src/pages/CollabPage.tsx +0 -470
  123. package/frontend/src/pages/DesignDocsPage.tsx +0 -621
  124. package/frontend/src/pages/FilesPage.tsx +0 -393
  125. package/frontend/src/pages/HealthPage.tsx +0 -359
  126. package/frontend/src/pages/InboxPage.tsx +0 -83
  127. package/frontend/src/pages/InstructionsPage.tsx +0 -583
  128. package/frontend/src/pages/IntegrationsPage.tsx +0 -761
  129. package/frontend/src/pages/MeshPage.css +0 -58
  130. package/frontend/src/pages/MeshPage.tsx +0 -1219
  131. package/frontend/src/pages/OkfPage.tsx +0 -535
  132. package/frontend/src/pages/SandboxPage.tsx +0 -113
  133. package/frontend/src/pages/SkillsPage.tsx +0 -1994
  134. package/frontend/src/pages/TasksPage.tsx +0 -427
  135. package/frontend/src/pages/UsagePage.tsx +0 -702
  136. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/README.md +0 -65
  137. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/cli.mjs +0 -76
  138. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/package.json +0 -19
  139. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/plugin.json +0 -266
  140. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/adapters/dsh.mjs +0 -446
  141. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/documentation.mjs +0 -152
  142. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/executor.mjs +0 -71
  143. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/runtime.mjs +0 -36
  144. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/setup.mjs +0 -35
  145. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/worker.mjs +0 -175
  146. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/ui/DESIGN.md +0 -32
  147. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/ui/card.js +0 -575
@@ -0,0 +1,261 @@
1
+ /**
2
+ * Multi-Vault and Mesh Discovery Engine for OpenWiki.
3
+ *
4
+ * Uses the Total Recall Mesh plugin dynamically (zero hardcoded hosts/IPs/devices).
5
+ * Discovers local vaults, registered project vaults, and live online mesh peers.
6
+ */
7
+ import fs from 'node:fs';
8
+ import os from 'node:os';
9
+ import path from 'node:path';
10
+ import { execFileSync, spawnSync } from 'node:child_process';
11
+ import { readProjects } from './refresh.mjs';
12
+
13
+ /**
14
+ * Returns the path to the global vault directory.
15
+ */
16
+ export function getGlobalVaultPath(home = os.homedir()) {
17
+ return path.join(home, '.agent', 'skills', 'total-recall', 'memory-vault');
18
+ }
19
+
20
+ /**
21
+ * Discovers all active vault roots on the current machine dynamically:
22
+ * - Global memory vault
23
+ * - Every registered project repository's memory vault
24
+ * - Every registered project repository's openwiki projection directory
25
+ */
26
+ export function discoverLocalVaults({ home = os.homedir() } = {}) {
27
+ const vaults = [];
28
+
29
+ // 1. Global Vault
30
+ const globalVault = getGlobalVaultPath(home);
31
+ if (fs.existsSync(globalVault)) {
32
+ vaults.push({
33
+ type: 'global',
34
+ name: 'global-vault',
35
+ root: globalVault,
36
+ isVault: true,
37
+ hasOpenWikiDir: fs.existsSync(path.join(globalVault, 'openwiki-articles')) ||
38
+ fs.existsSync(path.join(globalVault, 'openwiki-hubs')),
39
+ });
40
+ }
41
+
42
+ // 2. Project Vaults & Repositories from project-registry
43
+ let projects = [];
44
+ try {
45
+ projects = readProjects();
46
+ } catch {
47
+ projects = [];
48
+ }
49
+
50
+ for (const proj of projects) {
51
+ const projectVault = path.join(proj.root, '.agent', 'skills', 'total-recall', 'memory-vault');
52
+ const projectOpenWiki = path.join(proj.root, 'openwiki');
53
+ vaults.push({
54
+ type: 'project',
55
+ name: proj.name,
56
+ root: proj.root,
57
+ vaultPath: fs.existsSync(projectVault) ? projectVault : null,
58
+ openwikiPath: fs.existsSync(projectOpenWiki) ? projectOpenWiki : null,
59
+ hasOpenWikiDir: fs.existsSync(projectOpenWiki),
60
+ isVault: fs.existsSync(projectVault),
61
+ });
62
+ }
63
+
64
+ return vaults;
65
+ }
66
+
67
+ /**
68
+ * Dynamically queries the mesh plugin for live mesh peers.
69
+ * Zero hardcoded hostnames, IPs, or personal user configurations.
70
+ */
71
+ export function getMeshNodes() {
72
+ try {
73
+ const stdout = execFileSync('total-recall', ['mesh', 'status'], {
74
+ encoding: 'utf8',
75
+ timeout: 6000,
76
+ stdio: ['ignore', 'pipe', 'ignore'],
77
+ });
78
+
79
+ const lines = stdout.split('\n');
80
+ const jsonLine = lines.find((l) => l.trim().startsWith('{') && l.includes('"nodes"'));
81
+ if (!jsonLine) return [];
82
+
83
+ const parsed = JSON.parse(jsonLine);
84
+ if (!Array.isArray(parsed.nodes)) return [];
85
+
86
+ return parsed.nodes
87
+ .filter((n) => n && n.ip && typeof n.ip === 'string')
88
+ .map((n) => ({
89
+ hostname: n.os_hostname || n.hostname || n.title || n.ip,
90
+ ip: n.ip,
91
+ ips: Array.isArray(n.ips) ? n.ips : [n.ip],
92
+ self: Boolean(n.self),
93
+ online: Boolean(n.online),
94
+ os: n.os || 'unknown',
95
+ }));
96
+ } catch {
97
+ return [];
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Execute a command on a remote mesh node via Total Recall mesh plugin.
103
+ * Guarded against self-execution (never ssh a machine into itself).
104
+ */
105
+ export function execOnMeshNode(nodeTarget, commandStr, { timeoutMs = 25000 } = {}) {
106
+ if (!nodeTarget || typeof nodeTarget !== 'string') {
107
+ throw new Error('A valid mesh node identifier or IP is required');
108
+ }
109
+
110
+ // Verify against dynamic mesh self status
111
+ const nodes = getMeshNodes();
112
+ const matched = nodes.find(
113
+ (n) => n.ip === nodeTarget || n.hostname.toLowerCase() === nodeTarget.toLowerCase()
114
+ );
115
+
116
+ if (matched?.self) {
117
+ throw new Error(`Target node '${nodeTarget}' is the local host. Do not mesh exec into self.`);
118
+ }
119
+
120
+ const result = spawnSync('total-recall', ['mesh', 'exec', nodeTarget, commandStr], {
121
+ encoding: 'utf8',
122
+ timeout: timeoutMs,
123
+ });
124
+
125
+ return {
126
+ ok: result.status === 0,
127
+ status: result.status,
128
+ stdout: result.stdout || '',
129
+ stderr: result.stderr || '',
130
+ error: result.error ? result.error.message : null,
131
+ };
132
+ }
133
+
134
+ /**
135
+ * Scans a directory for OpenWiki markdown hub files.
136
+ */
137
+ export function scanDirectoryHubs(targetDir, { label = '', repoName = '' } = {}) {
138
+ const hubs = [];
139
+ if (!fs.existsSync(targetDir)) return hubs;
140
+
141
+ try {
142
+ const entries = fs.readdirSync(targetDir, { withFileTypes: true });
143
+ for (const ent of entries) {
144
+ if (ent.isFile() && ent.name.endsWith('.md')) {
145
+ const fullPath = path.join(targetDir, ent.name);
146
+ try {
147
+ const content = fs.readFileSync(fullPath, 'utf8');
148
+ const titleMatch = content.match(/^#\s+(.+)$/m);
149
+ const title = titleMatch ? titleMatch[1].trim() : ent.name.replace(/\.md$/, '');
150
+ const lines = content.split('\n').length;
151
+ hubs.push({
152
+ title,
153
+ fileName: ent.name,
154
+ path: fullPath,
155
+ repoName: repoName || path.basename(path.dirname(targetDir)),
156
+ sourceType: label || 'openwiki',
157
+ lines,
158
+ size_bytes: Buffer.byteLength(content, 'utf8'),
159
+ });
160
+ } catch {
161
+ // ignore unreadable files
162
+ }
163
+ }
164
+ }
165
+ } catch {
166
+ // directory read error
167
+ }
168
+
169
+ return hubs;
170
+ }
171
+
172
+ /**
173
+ * Universal Hubs Discovery across:
174
+ * - Current cwd `./openwiki`
175
+ * - All registered project vaults (`--all-vaults`)
176
+ * - Global memory vault (`openwiki-*` categories)
177
+ * - Remote mesh node (`--node <target>`)
178
+ */
179
+ export async function discoverAllHubs({
180
+ allVaults = false,
181
+ node = null,
182
+ cwd = process.cwd(),
183
+ } = {}) {
184
+ // If target is a remote mesh node, delegate through mesh
185
+ if (node && node !== 'local' && node !== 'self') {
186
+ const nodes = getMeshNodes();
187
+ const selfNode = nodes.find((n) => n.self);
188
+ const isSelf = selfNode && (selfNode.ip === node || selfNode.hostname.toLowerCase() === node.toLowerCase());
189
+
190
+ if (!isSelf) {
191
+ const meshRes = execOnMeshNode(node, `total-recall openwiki hubs ${allVaults ? '--all-vaults' : ''} --json`);
192
+ if (meshRes.ok) {
193
+ try {
194
+ const parsed = JSON.parse(meshRes.stdout);
195
+ return {
196
+ node,
197
+ isRemote: true,
198
+ hubs: parsed.hubs || [],
199
+ count: parsed.count || (parsed.hubs ? parsed.hubs.length : 0),
200
+ };
201
+ } catch {
202
+ throw new Error(`Invalid JSON response from mesh node ${node}: ${meshRes.stdout}`);
203
+ }
204
+ } else {
205
+ throw new Error(`Mesh exec failed on node ${node}: ${meshRes.stderr || meshRes.error}`);
206
+ }
207
+ }
208
+ }
209
+
210
+ const allHubs = [];
211
+
212
+ // 1. Current working directory openwiki
213
+ const localOpenWiki = path.resolve(cwd, 'openwiki');
214
+ const localHubs = scanDirectoryHubs(localOpenWiki, {
215
+ label: 'current-workspace',
216
+ repoName: path.basename(cwd),
217
+ });
218
+ allHubs.push(...localHubs);
219
+
220
+ // If not all vaults requested, return local hubs
221
+ if (!allVaults) {
222
+ return {
223
+ node: 'local',
224
+ isRemote: false,
225
+ hubs: allHubs,
226
+ count: allHubs.length,
227
+ scope: 'workspace',
228
+ };
229
+ }
230
+
231
+ // 2. Scan all project vaults across the machine
232
+ const localVaults = discoverLocalVaults();
233
+ for (const vault of localVaults) {
234
+ if (vault.type === 'project' && vault.openwikiPath) {
235
+ if (path.resolve(vault.openwikiPath) === path.resolve(localOpenWiki)) continue;
236
+ const projHubs = scanDirectoryHubs(vault.openwikiPath, {
237
+ label: 'project-openwiki',
238
+ repoName: vault.name,
239
+ });
240
+ allHubs.push(...projHubs);
241
+ } else if (vault.type === 'global') {
242
+ const gHubsDir = path.join(vault.root, 'openwiki-hubs');
243
+ const gArticlesDir = path.join(vault.root, 'openwiki-articles');
244
+ if (fs.existsSync(gHubsDir)) {
245
+ allHubs.push(...scanDirectoryHubs(gHubsDir, { label: 'global-vault-hubs', repoName: 'global-vault' }));
246
+ }
247
+ if (fs.existsSync(gArticlesDir)) {
248
+ allHubs.push(...scanDirectoryHubs(gArticlesDir, { label: 'global-vault-articles', repoName: 'global-vault' }));
249
+ }
250
+ }
251
+ }
252
+
253
+ return {
254
+ node: 'local',
255
+ isRemote: false,
256
+ hubs: allHubs,
257
+ count: allHubs.length,
258
+ scope: 'all-vaults',
259
+ vaultsScanned: localVaults.length,
260
+ };
261
+ }
@@ -4,8 +4,8 @@
4
4
  * The generated `openwiki/` pages are a per-repository projection: they are
5
5
  * written into <repo>/openwiki/ and describe that repository's code. They rot
6
6
  * silently, because nothing in the repository is obliged to regenerate them —
7
- * festech-modular, festech.live and ultrachat-ai-powered each carried pages
8
- * three months and 78-149 commits out of date while still looking authoritative.
7
+ * repositories have carried pages three months and 78-149 commits out of date
8
+ * while still looking authoritative.
9
9
  *
10
10
  * The driver belongs here rather than in every repository: the plugin is one of
11
11
  * the bundled defaults served from the shared brain, so one implementation
@@ -21,7 +21,7 @@ import fs from 'node:fs';
21
21
  import os from 'node:os';
22
22
  import path from 'node:path';
23
23
  import { fileURLToPath } from 'node:url';
24
- import { spawnSync } from 'node:child_process';
24
+ import { spawn, spawnSync } from 'node:child_process';
25
25
  import { loadOpenWikiConfig } from './config.mjs';
26
26
 
27
27
  const PLUGIN_ROOT = fileURLToPath(new URL('..', import.meta.url));
@@ -82,7 +82,16 @@ export function hasWiki(root) {
82
82
  * since the recorded generation, which is the same measure the per-repo driver
83
83
  * used. Uncommitted work is not counted, so an open edit does not read as drift.
84
84
  */
85
+ export function isGitRepo(root, { git = spawnSync } = {}) {
86
+ if (fs.existsSync(path.join(root, '.git'))) return true;
87
+ const res = git('git', ['-C', root, 'rev-parse', '--is-inside-work-tree'], { encoding: 'utf8' });
88
+ return !res.error && res.status === 0;
89
+ }
90
+
85
91
  export function staleness(root, { git = spawnSync } = {}) {
92
+ if (!isGitRepo(root, { git })) {
93
+ return { root, head: '', generatedAt: null, generatedStatus: null, files: null, lastCodeChange: null, commitsSince: 0, stale: false };
94
+ }
86
95
  const marker = path.join(root, 'openwiki', '.last-update.json');
87
96
  let head = '';
88
97
  const headRun = git('git', ['-C', root, 'rev-parse', 'HEAD'], { encoding: 'utf8' });
@@ -171,11 +180,19 @@ export function resolveKey({ provider = null, keys = null, run = spawnSync } = {
171
180
  export function resolveGenerator({ pluginRoot = null, run = spawnSync, env = process.env, home = os.homedir(), targetVersion = null } = {}) {
172
181
  const root = pluginRoot !== null ? pluginRoot : PLUGIN_ROOT;
173
182
  if (root) {
183
+ const bundled = path.join(root, 'vendor', 'openwiki.mjs');
184
+ if (fs.existsSync(bundled)) {
185
+ return { command: process.execPath, args: [bundled], via: 'bundled plugin generator' };
186
+ }
174
187
  const candidate = path.join(root, 'node_modules', '.bin', 'openwiki');
175
188
  if (fs.existsSync(candidate)) return { command: candidate, args: [], via: 'plugin local node_modules/.bin' };
176
189
  }
177
190
 
178
191
  if (pluginRoot === null) {
192
+ const localBundled = path.join(process.cwd(), 'vendor', 'openwiki.mjs');
193
+ if (fs.existsSync(localBundled)) {
194
+ return { command: process.execPath, args: [localBundled], via: 'bundled plugin generator' };
195
+ }
179
196
  const local = path.join(process.cwd(), 'node_modules', '.bin', 'openwiki');
180
197
  if (fs.existsSync(local)) return { command: local, args: [], via: 'local node_modules/.bin' };
181
198
  }
@@ -235,6 +252,9 @@ export function stillGenerating(root) {
235
252
  export function refreshProject(project, {
236
253
  key, model = null, provider = null, binary = null, binaryArgs = [], timeoutMs = 30 * 60 * 1000, run = spawnSync, now = Date.now(), targetVersion = null, pluginRoot = PLUGIN_ROOT,
237
254
  } = {}) {
255
+ if (!isGitRepo(project.root, { git: run })) {
256
+ return { ...project, action: 'skipped', reason: 'not a git repository' };
257
+ }
238
258
  const before = staleness(project.root, { git: run });
239
259
  const uninitialized = !hasWiki(project.root);
240
260
 
@@ -247,7 +267,7 @@ export function refreshProject(project, {
247
267
  ({ command, args, via } = resolved);
248
268
  }
249
269
 
250
- const config = typeof loadOpenWikiConfig === 'function' ? loadOpenWikiConfig() : { model: 'deepseek/deepseek-v4.1-flash', provider: 'openrouter' };
270
+ const config = typeof loadOpenWikiConfig === 'function' ? loadOpenWikiConfig() : { model: 'deepseek/deepseek-chat', provider: 'openrouter' };
251
271
  const selectedModel = model || config.model;
252
272
  const selectedProvider = provider || config.provider;
253
273
 
@@ -336,8 +356,135 @@ export function refreshProject(project, {
336
356
  return { ...project, action: uninitialized ? 'initialized' : 'refreshed', from: before.generatedAt, to: after.generatedAt, durationMs: Date.now() - startedAt, via, ...after };
337
357
  }
338
358
 
359
+ export async function refreshProjectAsync(project, {
360
+ key, model = null, provider = null, binary = null, binaryArgs = [], timeoutMs = 30 * 60 * 1000, now = Date.now(), targetVersion = null, pluginRoot = PLUGIN_ROOT,
361
+ } = {}) {
362
+ if (!isGitRepo(project.root, { git: spawnSync })) {
363
+ return { ...project, action: 'skipped', reason: 'not a git repository' };
364
+ }
365
+ const before = staleness(project.root, { git: spawnSync });
366
+ const uninitialized = !hasWiki(project.root);
367
+ let command = binary, args = binaryArgs, via = binary ? 'explicit' : null;
368
+ if (!command) {
369
+ const resolved = resolveGenerator({ run: spawnSync, targetVersion, pluginRoot });
370
+ if (!resolved.command) {
371
+ return { ...project, action: 'failed', reason: 'no openwiki generator available (no local .bin, no PATH entry, no npx)', ...before };
372
+ }
373
+ ({ command, args, via } = resolved);
374
+ }
375
+
376
+ const config = typeof loadOpenWikiConfig === 'function' ? loadOpenWikiConfig() : { model: 'deepseek/deepseek-chat', provider: 'openrouter' };
377
+ const selectedModel = model || config.model;
378
+ const selectedProvider = provider || config.provider;
379
+
380
+ const modeFlag = uninitialized ? '--init' : '--update';
381
+ const cmdArgs = [...args, modeFlag, '-p'];
382
+ if (selectedModel) {
383
+ cmdArgs.push('--modelId', selectedModel);
384
+ }
385
+
386
+ if (uninitialized) {
387
+ const runFile = path.join(project.root, 'openwiki', '.run.json');
388
+ if (fs.existsSync(runFile)) {
389
+ try { fs.unlinkSync(runFile); } catch {}
390
+ }
391
+ }
392
+
393
+ const childEnv = { ...process.env };
394
+ if (selectedProvider === 'google') {
395
+ childEnv.GOOGLE_API_KEY = key;
396
+ childEnv.GEMINI_API_KEY = key;
397
+ childEnv.OPENWIKI_PROVIDER = 'gemini';
398
+ delete childEnv.OPENAI_API_KEY;
399
+ delete childEnv.OPENROUTER_API_KEY;
400
+ } else {
401
+ childEnv.OPENROUTER_API_KEY = key;
402
+ childEnv.OPENWIKI_PROVIDER = 'openrouter';
403
+ if (config.max_tokens) {
404
+ childEnv.OPENWIKI_OPENROUTER_MAX_TOKENS = String(config.max_tokens);
405
+ childEnv.OPENWIKI_MAX_OUTPUT_TOKENS = String(config.max_tokens);
406
+ }
407
+ delete childEnv.OPENAI_API_KEY;
408
+ }
409
+
410
+ const startedAt = now;
411
+ const result = await new Promise((resolve) => {
412
+ const proc = spawn(command, cmdArgs, {
413
+ cwd: project.root,
414
+ env: childEnv,
415
+ stdio: ['ignore', 'pipe', 'pipe'],
416
+ timeout: timeoutMs,
417
+ });
418
+ let stdout = '', stderr = '';
419
+ proc.stdout?.on('data', (d) => { stdout += d; });
420
+ proc.stderr?.on('data', (d) => { stderr += d; });
421
+ proc.on('error', (err) => resolve({ error: err, status: 1, stdout, stderr }));
422
+ proc.on('close', (status) => resolve({ status: status ?? 0, stdout, stderr }));
423
+ });
424
+
425
+ // OpenWiki generator may drop GitHub workflow files; our system strictly forbids GitHub Actions.
426
+ const workflowDir = path.join(project.root, '.github', 'workflows');
427
+ const workflowFile = path.join(workflowDir, 'openwiki-update.yml');
428
+ if (fs.existsSync(workflowFile)) {
429
+ try { fs.unlinkSync(workflowFile); } catch {}
430
+ }
431
+ if (fs.existsSync(workflowDir)) {
432
+ try {
433
+ const remaining = fs.readdirSync(workflowDir);
434
+ if (remaining.length === 0) fs.rmdirSync(workflowDir);
435
+ } catch {}
436
+ }
437
+
438
+ const after = staleness(project.root, { git: spawnSync });
439
+ const exitCode = result.error ? (result.error.code || 1) : result.status;
440
+ const advanced = after.generatedAt !== before.generatedAt || (uninitialized && hasWiki(project.root));
441
+ const wrote = pagesWritten(before, after) || (uninitialized && hasWiki(project.root) ? 1 : 0);
442
+
443
+ const outcome = after.generatedStatus === 'interrupted' || stillGenerating(project.root)
444
+ ? 'partial'
445
+ : (advanced ? (uninitialized ? 'initialized' : 'refreshed') : 'unchanged');
446
+
447
+ const extractReason = (rawErr, rawOut) => {
448
+ if (result.error?.message) return result.error.message;
449
+ const combined = `${rawErr || ''}\n${rawOut || ''}`.trim();
450
+ if (!combined) return `exit ${exitCode}`;
451
+ const lines = combined.split('\n').map((l) => l.trim()).filter(Boolean);
452
+ const diag = lines.find((l) => l.includes('Error Diagnostics') || l.includes('exited without') || l.includes('Could not restore') || l.includes('Provider returned error') || l.includes('fetch failed'));
453
+ if (diag) return diag.replace(/^\[ERR\]\s*/, '');
454
+ const last = lines[lines.length - 1];
455
+ return last ? last.replace(/^\[ERR\]\s*/, '') : `exit ${exitCode}`;
456
+ };
457
+ const failReason = extractReason(result.stderr, result.stdout);
458
+
459
+ if (exitCode !== 0 && !wrote) {
460
+ return { ...project, action: 'failed', reason: failReason, exitCode, ...after };
461
+ }
462
+ if (!advanced && !wrote) {
463
+ return { ...project, action: 'failed', reason: failReason || `command exited ${exitCode} and no page changed`, exitCode, ...after };
464
+ }
465
+ if (outcome === 'partial') {
466
+ return {
467
+ ...project,
468
+ action: 'partial',
469
+ reason: `${wrote} page(s) rewritten, but the run did not finish (the generator was cut off); it resumes on the next refresh`,
470
+ exitCode,
471
+ from: before.generatedAt,
472
+ to: after.generatedAt,
473
+ via,
474
+ ...after,
475
+ };
476
+ }
477
+ if (exitCode !== 0) {
478
+ return { ...project, action: 'failed', reason: failReason, exitCode, ...after };
479
+ }
480
+ return { ...project, action: uninitialized ? 'initialized' : 'refreshed', from: before.generatedAt, to: after.generatedAt, durationMs: Date.now() - startedAt, via, ...after };
481
+ }
482
+
339
483
  /** Is a refresh due for this project under the configured interval? */
340
484
  export function isDue(project, { lockMs = DEFAULT_LOCK_MS, now = Date.now(), git = spawnSync } = {}) {
485
+ if (!isGitRepo(project.root, { git })) {
486
+ return { due: false, skipped: true, reason: 'not a git repository' };
487
+ }
341
488
  const state = staleness(project.root, { git });
342
489
  if (!hasWiki(project.root)) {
343
490
  return { due: true, uninitialized: true, reason: 'uninitialized (missing openwiki/ pages)', ...state };
@@ -0,0 +1,114 @@
1
+ /**
2
+ * OpenWiki Evidence Search & Query Engine.
3
+ *
4
+ * Searches knowledge hubs across local workspace, all registered project vaults,
5
+ * or remote mesh nodes. Extracts evidence passages and linked concepts.
6
+ */
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+ import { discoverAllHubs, execOnMeshNode } from './mesh-vaults.mjs';
10
+
11
+ /**
12
+ * Searches for query terms across markdown files.
13
+ */
14
+ export async function searchWikiHubs(query, {
15
+ allVaults = false,
16
+ node = null,
17
+ limit = 20,
18
+ cwd = process.cwd(),
19
+ } = {}) {
20
+ if (!query || typeof query !== 'string' || !query.trim()) {
21
+ throw new Error('Search query must be a non-empty string');
22
+ }
23
+
24
+ // If remote mesh node specified, dispatch via mesh
25
+ if (node && node !== 'local' && node !== 'self') {
26
+ const escapedQuery = query.replace(/"/g, '\\"');
27
+ const meshRes = execOnMeshNode(
28
+ node,
29
+ `total-recall openwiki search "${escapedQuery}" ${allVaults ? '--all-vaults' : ''} --json`
30
+ );
31
+ if (meshRes.ok) {
32
+ try {
33
+ return JSON.parse(meshRes.stdout);
34
+ } catch {
35
+ throw new Error(`Invalid JSON from mesh node ${node}: ${meshRes.stdout}`);
36
+ }
37
+ } else {
38
+ throw new Error(`Mesh search failed on node ${node}: ${meshRes.stderr || meshRes.error}`);
39
+ }
40
+ }
41
+
42
+ const hubsResult = await discoverAllHubs({ allVaults, cwd });
43
+ const rawTerms = query.toLowerCase().split(/\s+/).filter(Boolean);
44
+ const terms = rawTerms.map((t) => t.replace(/[^\w-]/g, ''));
45
+
46
+ const matches = [];
47
+
48
+ for (const hub of hubsResult.hubs) {
49
+ let content;
50
+ try {
51
+ content = fs.readFileSync(hub.path, 'utf8');
52
+ } catch {
53
+ continue;
54
+ }
55
+
56
+ const lines = content.split('\n');
57
+ let currentHeading = hub.title;
58
+
59
+ for (let i = 0; i < lines.length; i++) {
60
+ const line = lines[i];
61
+ const headingMatch = line.match(/^#{1,6}\s+(.+)$/);
62
+ if (headingMatch) {
63
+ currentHeading = headingMatch[1].trim();
64
+ }
65
+
66
+ const lowerLine = line.toLowerCase();
67
+ let termHits = 0;
68
+ for (const term of terms) {
69
+ if (lowerLine.includes(term)) termHits++;
70
+ }
71
+
72
+ if (termHits > 0) {
73
+ // Build surrounding snippet (1 line before, matched line, 1 line after)
74
+ const start = Math.max(0, i - 1);
75
+ const end = Math.min(lines.length, i + 2);
76
+ const snippetLines = lines.slice(start, end).map((l) => l.trim()).filter(Boolean);
77
+ const snippet = snippetLines.join(' ');
78
+
79
+ // Score: exact full phrase match bonus, heading bonus, multiple term bonus
80
+ let score = termHits * 10;
81
+ if (lowerLine.includes(query.toLowerCase())) score += 30;
82
+ if (headingMatch) score += 20;
83
+
84
+ matches.push({
85
+ title: hub.title,
86
+ section: currentHeading,
87
+ fileName: hub.fileName,
88
+ repoName: hub.repoName,
89
+ path: hub.path,
90
+ line: i + 1,
91
+ snippet: snippet.length > 280 ? snippet.slice(0, 277) + '...' : snippet,
92
+ score,
93
+ });
94
+
95
+ // Skip forward so overlapping lines don't spam identical snippets
96
+ i += 1;
97
+ }
98
+ }
99
+ }
100
+
101
+ // Sort descending by relevance score
102
+ matches.sort((a, b) => b.score - a.score);
103
+ const truncated = matches.slice(0, limit);
104
+
105
+ return {
106
+ ok: true,
107
+ query,
108
+ scope: hubsResult.scope,
109
+ totalHubsScanned: hubsResult.count,
110
+ count: truncated.length,
111
+ totalHits: matches.length,
112
+ results: truncated,
113
+ };
114
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * OpenWiki Token & Secret Management.
3
+ *
4
+ * Implements [invariants-473efe83]:
5
+ * Directly checks and sets credentials in Total Recall auth secrets store.
6
+ * Never echoes raw secrets to stdout/stderr.
7
+ */
8
+ import { spawnSync } from 'node:child_process';
9
+
10
+ const PROVIDER_KEY_MAP = {
11
+ openrouter: 'OPENROUTER_API_KEY',
12
+ google: 'GOOGLE_API_KEY',
13
+ };
14
+
15
+ /**
16
+ * Checks token readiness across supported providers.
17
+ */
18
+ export function getTokenStatus() {
19
+ const result = spawnSync('total-recall', ['auth', 'secret', 'list', '--json'], {
20
+ encoding: 'utf8',
21
+ timeout: 5000,
22
+ });
23
+
24
+ const configured = {
25
+ openrouter: false,
26
+ google: false,
27
+ };
28
+
29
+ if (result.status === 0 && result.stdout) {
30
+ try {
31
+ const parsed = JSON.parse(result.stdout);
32
+ const keys = Array.isArray(parsed)
33
+ ? parsed.map((k) => (typeof k === 'string' ? k : k.key || k.name))
34
+ : (parsed.keys || []);
35
+
36
+ configured.openrouter = keys.includes('OPENROUTER_API_KEY') || Boolean(process.env.OPENROUTER_API_KEY);
37
+ configured.google = keys.includes('GOOGLE_API_KEY') || Boolean(process.env.GOOGLE_API_KEY);
38
+ } catch {
39
+ configured.openrouter = Boolean(process.env.OPENROUTER_API_KEY);
40
+ configured.google = Boolean(process.env.GOOGLE_API_KEY);
41
+ }
42
+ } else {
43
+ configured.openrouter = Boolean(process.env.OPENROUTER_API_KEY);
44
+ configured.google = Boolean(process.env.GOOGLE_API_KEY);
45
+ }
46
+
47
+ return {
48
+ ok: true,
49
+ tokens: {
50
+ openrouter: {
51
+ key: 'OPENROUTER_API_KEY',
52
+ configured: configured.openrouter,
53
+ status: configured.openrouter ? 'configured' : 'missing',
54
+ },
55
+ google: {
56
+ key: 'GOOGLE_API_KEY',
57
+ configured: configured.google,
58
+ status: configured.google ? 'configured' : 'missing',
59
+ },
60
+ },
61
+ };
62
+ }
63
+
64
+ /**
65
+ * Securely writes a token into Total Recall auth secrets store using --stdin.
66
+ */
67
+ export function setTokenValue(provider, secretValue) {
68
+ const keyName = PROVIDER_KEY_MAP[provider?.toLowerCase()] || provider;
69
+ if (!keyName) {
70
+ throw new Error(`Unsupported token provider '${provider}'. Allowed: openrouter, google`);
71
+ }
72
+
73
+ if (!secretValue || typeof secretValue !== 'string' || !secretValue.trim()) {
74
+ throw new Error(`Token value for ${keyName} must be a non-empty string`);
75
+ }
76
+
77
+ const trimmed = secretValue.trim();
78
+
79
+ // Pipe into total-recall auth secret set <KEY> --stdin
80
+ const child = spawnSync('total-recall', ['auth', 'secret', 'set', keyName, '--stdin'], {
81
+ input: trimmed,
82
+ encoding: 'utf8',
83
+ timeout: 5000,
84
+ });
85
+
86
+ if (child.status !== 0) {
87
+ throw new Error(child.stderr || child.error?.message || `Failed to set ${keyName} in secrets store`);
88
+ }
89
+
90
+ return {
91
+ ok: true,
92
+ provider,
93
+ key: keyName,
94
+ message: `Secret ${keyName} securely recorded in Total Recall secrets store.`,
95
+ };
96
+ }