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.
- package/bin/total-recall.mjs +10 -6
- package/docs/reference/cli-reference.md +18 -0
- package/frontend/dist/assets/index-4tGQfTa2.js +5605 -0
- package/frontend/dist/assets/index-DtwsDyFM.css +1 -0
- package/frontend/dist/index.html +2 -2
- package/frontend/package.json +11 -0
- package/frontend/src/App.tsx +47 -122
- package/frontend/src/components/plugins/plugin-frame.ts +1 -0
- package/frontend/src/index.css +152 -86
- package/frontend/src/pages/HelpPage.tsx +13 -10
- package/frontend/src/pages/MemoryPage.tsx +28 -30
- package/frontend/src/pages/NetworkPage.tsx +3 -3
- package/frontend/src/pages/PluginsPage.tsx +6 -5
- package/frontend/src/pages/RulesPage.tsx +16 -12
- package/package.json +4 -3
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +3114 -36
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package.json +4 -1
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +3114 -36
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package.json +4 -1
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +3121 -33
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package.json +4 -1
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package-lock.json +3122 -34
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package.json +4 -1
- package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +3127 -49
- package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +5 -2
- package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/mesh.mjs +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/capability/index.mjs +136 -7
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/cli.mjs +135 -26
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package-lock.json +3121 -33
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package.json +5 -2
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/plugin.json +228 -9
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/skills/meta-harness/SKILL.md +24 -30
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/alerts.mjs +29 -16
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/cascade.mjs +73 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/coordinator.mjs +182 -365
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/dispatch.mjs +33 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/documentation.mjs +40 -14
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/executor.mjs +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/agy.mjs +187 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/claude.mjs +275 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/cline.mjs +132 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/codex.mjs +187 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/cursor.mjs +158 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/devin.mjs +157 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/dsh.mjs +183 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/grok.mjs +132 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/hermes.mjs +173 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/index.mjs +87 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/openclaw.mjs +173 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/pi.mjs +129 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate.mjs +12 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/json-reply.mjs +22 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/learner.mjs +72 -70
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/model-catalog.mjs +195 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/paths.mjs +11 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/pool.mjs +36 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/review.mjs +169 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/router.mjs +136 -346
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/runtime.mjs +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/ssss-store.mjs +20 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/telemetry.mjs +43 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/threads.mjs +156 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worker.mjs +117 -35
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worktree.mjs +51 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/card.js +398 -57
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/cli.mjs +304 -46
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +367 -41
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +5 -2
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +33 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/config.mjs +2 -2
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/generate.mjs +159 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/graph.mjs +197 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/lint.mjs +153 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/mesh-vaults.mjs +261 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/refresh.mjs +151 -4
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/search.mjs +114 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/token.mjs +96 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/card.js +899 -662
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/vendor/openwiki.mjs +8987 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/vendor/skills/mermaid-diagrams/SKILL.md +44 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/vendor/skills/write-connector/SKILL.md +56 -0
- package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +3097 -81
- package/scaffold/.agent/skills/total-recall/plugins/security/package.json +4 -1
- package/src/cli/agent.mjs +12 -1
- package/src/cli/compact.mjs +108 -0
- package/src/cli/connect.mjs +56 -1
- package/src/cli/context.mjs +55 -23
- package/src/cli/doctor.mjs +5 -2
- package/src/cli/forget.mjs +2 -0
- package/src/cli/harness.mjs +62 -50
- package/src/cli/memory-entry.mjs +2 -2
- package/src/cli/preflight.mjs +152 -16
- package/src/cli/release.mjs +386 -0
- package/src/cli/remember.mjs +19 -1
- package/src/cli/rules.mjs +54 -12
- package/src/cli/startup.mjs +1 -1
- package/src/cli/status.mjs +12 -2
- package/src/core/claude-hooks.mjs +41 -3
- package/src/core/cli-commands.mjs +2 -0
- package/src/core/cli-semantic-index.mjs +3 -2
- package/src/core/conflict-detector.mjs +23 -12
- package/src/core/context-compactor.mjs +145 -0
- package/src/core/context-policy.mjs +20 -0
- package/src/core/duplicate-guard.mjs +350 -0
- package/src/core/meta-harness.mjs +31 -11
- package/src/core/preflight-gate.mjs +2 -2
- package/src/core/preflight-receipt.mjs +29 -1
- package/src/core/scaffold-plugins.mjs +7 -0
- package/src/core/stdio-flush.mjs +23 -0
- package/src/core/surface.mjs +33 -10
- package/src/server/invoke-durability.mjs +39 -0
- package/src/server/invoke-write-child.mjs +25 -0
- package/src/server/routes/invoke.mjs +7 -0
- package/src/server/routes/plugins.mjs +2 -2
- package/frontend/dist/assets/index-BIQmqDKP.css +0 -1
- package/frontend/dist/assets/index-CRzjQLu7.js +0 -5631
- package/frontend/src/components/DaemonLogsTab.tsx +0 -54
- package/frontend/src/components/ResearchAgendaTab.tsx +0 -712
- package/frontend/src/components/TaskQueueTab.tsx +0 -193
- package/frontend/src/components/VoiceInput.tsx +0 -112
- package/frontend/src/pages/ChatPage.tsx +0 -1059
- package/frontend/src/pages/CollabPage.tsx +0 -470
- package/frontend/src/pages/DesignDocsPage.tsx +0 -621
- package/frontend/src/pages/FilesPage.tsx +0 -393
- package/frontend/src/pages/HealthPage.tsx +0 -359
- package/frontend/src/pages/InboxPage.tsx +0 -83
- package/frontend/src/pages/InstructionsPage.tsx +0 -583
- package/frontend/src/pages/IntegrationsPage.tsx +0 -761
- package/frontend/src/pages/MeshPage.css +0 -58
- package/frontend/src/pages/MeshPage.tsx +0 -1219
- package/frontend/src/pages/OkfPage.tsx +0 -535
- package/frontend/src/pages/SandboxPage.tsx +0 -113
- package/frontend/src/pages/SkillsPage.tsx +0 -1994
- package/frontend/src/pages/TasksPage.tsx +0 -427
- package/frontend/src/pages/UsagePage.tsx +0 -702
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/README.md +0 -65
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/cli.mjs +0 -76
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/package.json +0 -19
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/plugin.json +0 -266
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/adapters/dsh.mjs +0 -446
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/documentation.mjs +0 -152
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/executor.mjs +0 -71
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/runtime.mjs +0 -36
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/setup.mjs +0 -35
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/worker.mjs +0 -175
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/ui/DESIGN.md +0 -32
- 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
|
-
*
|
|
8
|
-
*
|
|
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-
|
|
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
|
+
}
|