total-recall-brain 3.5.0 → 3.6.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.
- package/bin/total-recall.mjs +3 -0
- package/extension/content-script.js +354 -11
- package/extension/popup/popup.html +5 -0
- package/extension/popup/popup.js +25 -0
- package/frontend/src/App.tsx +29 -0
- package/frontend/src/api.ts +20 -0
- package/frontend/src/pages/ChatPage.tsx +101 -8
- package/frontend/src/pages/CollabPage.tsx +470 -0
- package/frontend/src/pages/GraphPage.tsx +62 -0
- package/frontend/src/pages/HealthPage.tsx +6 -1
- package/frontend/src/pages/HelpPage.tsx +141 -0
- package/package.json +3 -2
- package/src/cli/collab.mjs +49 -0
- package/src/cli/help.mjs +1 -1
- package/src/core/dream.mjs +41 -0
- package/src/core/source-adapters.mjs +14 -6
- package/src/server/index.mjs +21 -1
- package/src/server/rest.mjs +49 -1
- package/src/server/route-manifest.json +4 -0
- package/src/server/routes/collab.mjs +286 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react'
|
|
2
|
+
import { listMemory, listResearch, fetchChatThreads } from '../api'
|
|
3
|
+
import type { MemoryNode, ResearchItem } from '../types'
|
|
4
|
+
import type { ChatThread } from '../api'
|
|
5
|
+
import Graph3D from '../components/Graph3D'
|
|
6
|
+
|
|
7
|
+
export default function GraphPage({ activeBrainId }: { activeBrainId?: string }) {
|
|
8
|
+
const [memoryNodes, setMemoryNodes] = useState<MemoryNode[]>([])
|
|
9
|
+
const [researchItems, setResearchItems] = useState<ResearchItem[]>([])
|
|
10
|
+
const [threads, setThreads] = useState<ChatThread[]>([])
|
|
11
|
+
const [loading, setLoading] = useState(true)
|
|
12
|
+
const [selectedGroundingNodes, setSelectedGroundingNodes] = useState<string[]>([])
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
async function loadData() {
|
|
16
|
+
setLoading(true)
|
|
17
|
+
try {
|
|
18
|
+
const [nodes, researchRes, threadsRes] = await Promise.all([
|
|
19
|
+
listMemory(activeBrainId),
|
|
20
|
+
listResearch(activeBrainId).catch(() => ({ items: [] })),
|
|
21
|
+
fetchChatThreads().catch(() => [])
|
|
22
|
+
])
|
|
23
|
+
setMemoryNodes(nodes)
|
|
24
|
+
setResearchItems(researchRes.items || [])
|
|
25
|
+
setThreads(threadsRes)
|
|
26
|
+
} catch (err) {
|
|
27
|
+
console.error('Failed to load graph data', err)
|
|
28
|
+
} finally {
|
|
29
|
+
setLoading(false)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
loadData()
|
|
33
|
+
}, [activeBrainId])
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div className="page" style={{ padding: 0, height: 'calc(100vh - 1px)', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
|
37
|
+
<div style={{ padding: '16px 24px', background: 'rgba(18, 18, 26, 0.45)', backdropFilter: 'blur(16px)', borderBottom: '1px solid var(--border)', flexShrink: 0 }}>
|
|
38
|
+
<h1 style={{ fontSize: '18px', fontWeight: 700 }}>Sovereign Memory Network</h1>
|
|
39
|
+
<p style={{ fontSize: '12px', color: 'var(--text-secondary)', marginTop: '2px' }}>Interactive 3D constellation layout of your knowledge vault, sessions, and active thoughts.</p>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<div style={{ flex: 1, position: 'relative', overflow: 'hidden' }}>
|
|
43
|
+
{loading ? (
|
|
44
|
+
<div className="skeleton" style={{ width: '100%', height: '100%' }} />
|
|
45
|
+
) : (
|
|
46
|
+
<Graph3D
|
|
47
|
+
threads={threads}
|
|
48
|
+
memoryNodes={memoryNodes}
|
|
49
|
+
researchItems={researchItems}
|
|
50
|
+
onOpenThread={() => {}}
|
|
51
|
+
onGroundMemoryNode={(slug) => {
|
|
52
|
+
setSelectedGroundingNodes(prev =>
|
|
53
|
+
prev.includes(slug) ? prev.filter(s => s !== slug) : [...prev, slug]
|
|
54
|
+
)
|
|
55
|
+
}}
|
|
56
|
+
selectedGroundingNodes={selectedGroundingNodes}
|
|
57
|
+
/>
|
|
58
|
+
)}
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
@@ -50,8 +50,13 @@ export default function HealthPage() {
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
checkVersion()
|
|
53
|
+
const updateInterval = setInterval(checkVersion, 3600000)
|
|
53
54
|
|
|
54
|
-
return () => {
|
|
55
|
+
return () => {
|
|
56
|
+
active = false;
|
|
57
|
+
clearInterval(interval);
|
|
58
|
+
clearInterval(updateInterval);
|
|
59
|
+
}
|
|
55
60
|
}, [])
|
|
56
61
|
|
|
57
62
|
const handleStartUpdate = async () => {
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react'
|
|
2
|
+
import { fetchHelpTopics, fetchHelpContent } from '../api'
|
|
3
|
+
import type { HelpTopic } from '../api'
|
|
4
|
+
|
|
5
|
+
function renderMarkdown(md: string) {
|
|
6
|
+
if (!md) return ''
|
|
7
|
+
let html = md
|
|
8
|
+
.replace(/&/g, '&')
|
|
9
|
+
.replace(/</g, '<')
|
|
10
|
+
.replace(/>/g, '>')
|
|
11
|
+
|
|
12
|
+
// Headings
|
|
13
|
+
html = html.replace(/^### (.*$)/gim, '<h3>$1</h3>')
|
|
14
|
+
html = html.replace(/^## (.*$)/gim, '<h2>$1</h2>')
|
|
15
|
+
html = html.replace(/^# (.*$)/gim, '<h1>$1</h1>')
|
|
16
|
+
|
|
17
|
+
// Bold & Italic
|
|
18
|
+
html = html.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
|
19
|
+
html = html.replace(/\*(.*?)\*/g, '<em>$1</em>')
|
|
20
|
+
|
|
21
|
+
// Code blocks
|
|
22
|
+
html = html.replace(
|
|
23
|
+
/```([\s\S]*?)```/g,
|
|
24
|
+
'<pre style="background:rgba(0,0,0,0.3);padding:12px;border-radius:6px;border:1px solid rgba(255,255,255,0.06);font-family:monospace;overflow-x:auto;margin:12px 0;"><code>$1</code></pre>'
|
|
25
|
+
)
|
|
26
|
+
html = html.replace(
|
|
27
|
+
/`(.*?)`/g,
|
|
28
|
+
'<code style="background:rgba(255,255,255,0.06);padding:2px 6px;border-radius:4px;font-family:monospace;">$1</code>'
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
// Blockquotes
|
|
32
|
+
html = html.replace(
|
|
33
|
+
/^> (.*$)/gim,
|
|
34
|
+
'<blockquote style="border-left:4px solid var(--purple-primary);padding-left:14px;color:var(--text-secondary);margin:16px 0;font-style:italic;">$1</blockquote>'
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
// Lists
|
|
38
|
+
html = html.replace(/^- (.*$)/gim, '<li style="margin-left:20px;list-style-type:disc;">$1</li>')
|
|
39
|
+
|
|
40
|
+
// Linebreaks
|
|
41
|
+
html = html.replace(/\n/g, '<br />')
|
|
42
|
+
|
|
43
|
+
return <div dangerouslySetInnerHTML={{ __html: html }} className="markdown-preview-block" />
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export default function HelpPage() {
|
|
47
|
+
const [topics, setTopics] = useState<HelpTopic[]>([])
|
|
48
|
+
const [selectedTopic, setSelectedTopic] = useState<string | null>(null)
|
|
49
|
+
const [content, setContent] = useState<string>('')
|
|
50
|
+
const [loading, setLoading] = useState<boolean>(false)
|
|
51
|
+
const [error, setError] = useState<string>('')
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
async function loadTopics() {
|
|
55
|
+
try {
|
|
56
|
+
const data = await fetchHelpTopics()
|
|
57
|
+
setTopics(data.topics)
|
|
58
|
+
if (data.topics.length > 0) {
|
|
59
|
+
setSelectedTopic(data.topics[0].id)
|
|
60
|
+
}
|
|
61
|
+
} catch (err: unknown) {
|
|
62
|
+
setError((err as Error).message || 'Failed to load help topics')
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
loadTopics()
|
|
66
|
+
}, [])
|
|
67
|
+
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (!selectedTopic) return
|
|
70
|
+
async function loadContent() {
|
|
71
|
+
setLoading(true)
|
|
72
|
+
try {
|
|
73
|
+
const data = await fetchHelpContent(selectedTopic!)
|
|
74
|
+
setContent(data.content)
|
|
75
|
+
} catch (err: unknown) {
|
|
76
|
+
setError((err as Error).message || 'Failed to load help details')
|
|
77
|
+
} finally {
|
|
78
|
+
setLoading(false)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
loadContent()
|
|
82
|
+
}, [selectedTopic])
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<div className="page" style={{ padding: '24px', maxWidth: '1100px', margin: '0 auto' }}>
|
|
86
|
+
<div className="page-header" style={{ marginBottom: '24px' }}>
|
|
87
|
+
<h1>Sovereign Help System</h1>
|
|
88
|
+
<p>Offline interactive documentation, CLI command references, and SSSS VFS specifications.</p>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
{error && <div className="badge badge-error" style={{ marginBottom: '16px' }}>⚠️ {error}</div>}
|
|
92
|
+
|
|
93
|
+
<div style={{ display: 'grid', gridTemplateColumns: '260px 1fr', gap: '30px', minHeight: '500px' }}>
|
|
94
|
+
{/* Sidebar */}
|
|
95
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
|
|
96
|
+
{topics.map((t) => (
|
|
97
|
+
<button
|
|
98
|
+
key={t.id}
|
|
99
|
+
onClick={() => setSelectedTopic(t.id)}
|
|
100
|
+
style={{
|
|
101
|
+
textAlign: 'left',
|
|
102
|
+
padding: '12px 16px',
|
|
103
|
+
borderRadius: '8px',
|
|
104
|
+
background: selectedTopic === t.id ? 'var(--purple-primary)' : 'rgba(255, 255, 255, 0.03)',
|
|
105
|
+
color: '#fff',
|
|
106
|
+
border: selectedTopic === t.id ? '1px solid var(--purple-border)' : '1px solid transparent',
|
|
107
|
+
cursor: 'pointer',
|
|
108
|
+
transition: 'all 0.2s',
|
|
109
|
+
}}
|
|
110
|
+
>
|
|
111
|
+
<div style={{ fontWeight: 'bold', fontSize: '14px' }}>{t.title}</div>
|
|
112
|
+
<div style={{ fontSize: '11px', color: selectedTopic === t.id ? 'rgba(255, 255, 255, 0.8)' : 'var(--text-secondary)', marginTop: '4px' }}>
|
|
113
|
+
{t.description}
|
|
114
|
+
</div>
|
|
115
|
+
</button>
|
|
116
|
+
))}
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
{/* Content Pane */}
|
|
120
|
+
<div
|
|
121
|
+
style={{
|
|
122
|
+
background: 'var(--surface-bg)',
|
|
123
|
+
border: '1px solid var(--border)',
|
|
124
|
+
borderRadius: '12px',
|
|
125
|
+
padding: '30px',
|
|
126
|
+
overflowY: 'auto',
|
|
127
|
+
maxHeight: '75vh',
|
|
128
|
+
}}
|
|
129
|
+
>
|
|
130
|
+
{loading ? (
|
|
131
|
+
<div className="skeleton" style={{ height: '400px' }} />
|
|
132
|
+
) : (
|
|
133
|
+
<div className="markdown-body" style={{ color: 'var(--text-primary)', lineHeight: '1.7', fontSize: '14px' }}>
|
|
134
|
+
{renderMarkdown(content)}
|
|
135
|
+
</div>
|
|
136
|
+
)}
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
)
|
|
141
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "total-recall-brain",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.6.0",
|
|
4
4
|
"description": "Sovereign AI system — database-free, SSSS-powered personal intelligence engine deployable to any POSIX host.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -65,7 +65,8 @@
|
|
|
65
65
|
"jsonwebtoken": "^9.0.3",
|
|
66
66
|
"natural": "^8.1.1",
|
|
67
67
|
"yaml": "^2.9.0",
|
|
68
|
-
"zod": "^4.4.3"
|
|
68
|
+
"zod": "^4.4.3",
|
|
69
|
+
"ws": "^8.18.0"
|
|
69
70
|
},
|
|
70
71
|
"devDependencies": {
|
|
71
72
|
"skills": "^1.5.7",
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* total-recall collab
|
|
3
|
+
*
|
|
4
|
+
* Start the collaboration backend server and frontend development portal.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* npx total-recall collab
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { spawn } from 'node:child_process';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
|
|
14
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const COLLAB_DIR = path.join(__dirname, '..', '..', 'collab');
|
|
16
|
+
|
|
17
|
+
export default function collab(args) {
|
|
18
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
19
|
+
console.log(`
|
|
20
|
+
total-recall collab — Start collaboration workspace
|
|
21
|
+
|
|
22
|
+
Usage: total-recall collab [options]
|
|
23
|
+
|
|
24
|
+
Options:
|
|
25
|
+
--help Show this help
|
|
26
|
+
|
|
27
|
+
Starts the Express/WebSockets backend server and Vite frontend portal.
|
|
28
|
+
`);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
console.log(` ⚡ Starting Total Recall collaboration workspace...`);
|
|
33
|
+
console.log(` Press Ctrl+C to stop.\n`);
|
|
34
|
+
|
|
35
|
+
const child = spawn('npm', ['run', 'dev'], {
|
|
36
|
+
cwd: COLLAB_DIR,
|
|
37
|
+
stdio: 'inherit',
|
|
38
|
+
shell: true,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
process.on('SIGINT', () => {
|
|
42
|
+
child.kill('SIGINT');
|
|
43
|
+
process.exit(0);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
child.on('exit', (code) => {
|
|
47
|
+
process.exit(code ?? 0);
|
|
48
|
+
});
|
|
49
|
+
}
|
package/src/cli/help.mjs
CHANGED
|
@@ -148,6 +148,6 @@ function printGeneralHelp() {
|
|
|
148
148
|
\x1b[1mAll Commands Available:\x1b[0m
|
|
149
149
|
\x1b[33minit\x1b[0m, \x1b[33msetup\x1b[0m, \x1b[33mstart\x1b[0m, \x1b[33mdeploy\x1b[0m, \x1b[33mdoctor\x1b[0m, \x1b[33mdream\x1b[0m, \x1b[33mlint\x1b[0m, \x1b[33mdaemon\x1b[0m, \x1b[33mbackup\x1b[0m, \x1b[33mrestore\x1b[0m, \x1b[33msync\x1b[0m, \x1b[33mstatus\x1b[0m,
|
|
150
150
|
\x1b[33mgenerate-pat\x1b[0m, \x1b[33mhash-password\x1b[0m, \x1b[33mreset-password\x1b[0m, \x1b[33mcompile\x1b[0m, \x1b[33mrebuild\x1b[0m, \x1b[33msnapshot\x1b[0m, \x1b[33mmigrate\x1b[0m,
|
|
151
|
-
\x1b[33mimport\x1b[0m, \x1b[33mingest\x1b[0m, \x1b[33mconnect\x1b[0m, \x1b[33mrelay\x1b[0m, \x1b[33muninstall\x1b[0m, \x1b[33mchat\x1b[0m, \x1b[33mremember\x1b[0m, \x1b[33mrecall\x1b[0m, \x1b[33mresearch\x1b[0m, \x1b[33mmap\x1b[0m, \x1b[33mbrain\x1b[0m, \x1b[33mskill\x1b[0m
|
|
151
|
+
\x1b[33mimport\x1b[0m, \x1b[33mingest\x1b[0m, \x1b[33mconnect\x1b[0m, \x1b[33mrelay\x1b[0m, \x1b[33muninstall\x1b[0m, \x1b[33mchat\x1b[0m, \x1b[33mremember\x1b[0m, \x1b[33mrecall\x1b[0m, \x1b[33mresearch\x1b[0m, \x1b[33mmap\x1b[0m, \x1b[33mbrain\x1b[0m, \x1b[33mskill\x1b[0m, \x1b[33mcollab\x1b[0m
|
|
152
152
|
`);
|
|
153
153
|
}
|
package/src/core/dream.mjs
CHANGED
|
@@ -137,6 +137,47 @@ export function autoPruneStorage(brainDir, vaultDir, conflictsDir) {
|
|
|
137
137
|
pruneDir(inboxDir, draftMaxAgeMs); // Keeps the local dev inbox fully clean of automatic research noise
|
|
138
138
|
if (conflictsDir) pruneDir(conflictsDir, draftMaxAgeMs);
|
|
139
139
|
|
|
140
|
+
// Prune scheduler tasks queue: move completed/failed ones to archive, delete archives older than logMaxAgeMs
|
|
141
|
+
const tasksQueueDir = path.join(brainDir, 'scheduler', 'queue');
|
|
142
|
+
const tasksArchiveDir = path.join(tasksQueueDir, 'archive');
|
|
143
|
+
if (fs.existsSync(tasksQueueDir)) {
|
|
144
|
+
try {
|
|
145
|
+
if (!fs.existsSync(tasksArchiveDir)) {
|
|
146
|
+
fs.mkdirSync(tasksArchiveDir, { recursive: true });
|
|
147
|
+
}
|
|
148
|
+
const files = fs.readdirSync(tasksQueueDir);
|
|
149
|
+
for (const file of files) {
|
|
150
|
+
if (!file.endsWith('.md')) continue;
|
|
151
|
+
const fullPath = path.join(tasksQueueDir, file);
|
|
152
|
+
try {
|
|
153
|
+
const raw = fs.readFileSync(fullPath, 'utf8');
|
|
154
|
+
const { data } = matter(raw);
|
|
155
|
+
if (data.status === 'completed' || data.status === 'failed') {
|
|
156
|
+
fs.renameSync(fullPath, path.join(tasksArchiveDir, file));
|
|
157
|
+
}
|
|
158
|
+
} catch (e) {
|
|
159
|
+
// ignore parsing/permission errors for individual files
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const archiveFiles = fs.readdirSync(tasksArchiveDir);
|
|
164
|
+
for (const file of archiveFiles) {
|
|
165
|
+
if (!file.endsWith('.md')) continue;
|
|
166
|
+
const fullPath = path.join(tasksArchiveDir, file);
|
|
167
|
+
try {
|
|
168
|
+
const stat = fs.statSync(fullPath);
|
|
169
|
+
if (now - stat.mtimeMs > logMaxAgeMs) {
|
|
170
|
+
fs.unlinkSync(fullPath);
|
|
171
|
+
}
|
|
172
|
+
} catch (e) {
|
|
173
|
+
// ignore
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
} catch (err) {
|
|
177
|
+
logger.warn('dream', `Failed to prune scheduler tasks: ${err.message}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
140
181
|
// Automatically prune transient planning files in every active/historical conversation folder,
|
|
141
182
|
// while permanently preserving the conversations/threads themselves (.system_generated)
|
|
142
183
|
try {
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import yaml from 'yaml';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
brainDir,
|
|
6
|
+
braveApiKey as defaultBraveKey,
|
|
7
|
+
braveSearchApiKey as defaultBraveSearchKey,
|
|
8
|
+
exaApiKey as defaultExaKey,
|
|
9
|
+
githubToken as defaultGithubToken,
|
|
10
|
+
serperApiKey as defaultSerperKey,
|
|
11
|
+
tavilyApiKey as defaultTavilyKey
|
|
12
|
+
} from './config.mjs';
|
|
5
13
|
|
|
6
14
|
/**
|
|
7
15
|
* Research Source Adapters — Real Service Integrations
|
|
@@ -42,12 +50,12 @@ export function loadResearchConfig(configPath = DEFAULT_CONFIG_PATH) {
|
|
|
42
50
|
|
|
43
51
|
return {
|
|
44
52
|
// ── Paid search providers (fallback chain: Brave → Tavily → Exa → Serper) ──
|
|
45
|
-
braveApiKey: process.env.BRAVE_SEARCH_API_KEY || process.env.BRAVE_API_KEY || secrets.brave_api_key || fileConfig.brave_api_key || null,
|
|
46
|
-
tavilyApiKey: process.env.TAVILY_API_KEY || secrets.tavily_api_key || fileConfig.tavily_api_key || null,
|
|
47
|
-
exaApiKey: process.env.EXA_API_KEY || secrets.exa_api_key || fileConfig.exa_api_key || null,
|
|
48
|
-
serperApiKey: process.env.SERPER_API_KEY || secrets.serper_api_key || fileConfig.serper_api_key || null,
|
|
53
|
+
braveApiKey: process.env.BRAVE_SEARCH_API_KEY || process.env.BRAVE_API_KEY || secrets.brave_api_key || fileConfig.brave_api_key || defaultBraveKey || defaultBraveSearchKey || null,
|
|
54
|
+
tavilyApiKey: process.env.TAVILY_API_KEY || secrets.tavily_api_key || fileConfig.tavily_api_key || defaultTavilyKey || null,
|
|
55
|
+
exaApiKey: process.env.EXA_API_KEY || secrets.exa_api_key || fileConfig.exa_api_key || defaultExaKey || null,
|
|
56
|
+
serperApiKey: process.env.SERPER_API_KEY || secrets.serper_api_key || fileConfig.serper_api_key || defaultSerperKey || null,
|
|
49
57
|
// GitHub: personal access token for higher rate limits (60→5000 req/hr)
|
|
50
|
-
githubToken: process.env.GITHUB_TOKEN || secrets.github_token || fileConfig.github_token || null,
|
|
58
|
+
githubToken: process.env.GITHUB_TOKEN || secrets.github_token || fileConfig.github_token || defaultGithubToken || null,
|
|
51
59
|
fetchTimeoutMs: fileConfig.fetch_timeout_ms || 10000,
|
|
52
60
|
maxResultsPerSource: fileConfig.max_results_per_source || 5,
|
|
53
61
|
userAgent: fileConfig.user_agent || 'TotalRecall/1.0 (knowledge-acquisition; +https://github.com/gregiteen/total-recall)',
|
package/src/server/index.mjs
CHANGED
|
@@ -198,8 +198,12 @@ try {
|
|
|
198
198
|
app.use('/api', apiRateLimiter());
|
|
199
199
|
app.use(restRouter);
|
|
200
200
|
logger.info('server', 'REST API mounted (/api/*, /v1/models, /.well-known/total-recall.json)');
|
|
201
|
+
|
|
202
|
+
const { collabRouter } = await import('./routes/collab.mjs');
|
|
203
|
+
app.use(collabRouter);
|
|
204
|
+
logger.info('server', 'Collaboration API routes mounted (/api/collab/*)');
|
|
201
205
|
} catch (err) {
|
|
202
|
-
logger.error('server', `REST API failed to load: ${err.message}`);
|
|
206
|
+
logger.error('server', `REST API or Collab routes failed to load: ${err.message}`);
|
|
203
207
|
}
|
|
204
208
|
|
|
205
209
|
// ─── API Routes (/v1/chat/completions) ──────────────────────────────────────────
|
|
@@ -573,7 +577,23 @@ const HOST = nodeEnv === 'production' && publicBindRequested && serverSecurityCo
|
|
|
573
577
|
? '127.0.0.1'
|
|
574
578
|
: configuredHost;
|
|
575
579
|
|
|
580
|
+
import { WebSocketServer } from 'ws';
|
|
581
|
+
const collabWss = new WebSocketServer({ noServer: true });
|
|
582
|
+
|
|
576
583
|
const server = app.listen(PORT, HOST, () => {
|
|
584
|
+
server.on('upgrade', async (request, socket, head) => {
|
|
585
|
+
const url = new URL(request.url, `http://${request.headers.host || 'localhost'}`);
|
|
586
|
+
if (url.pathname === '/collab-ws') {
|
|
587
|
+
try {
|
|
588
|
+
const { handleCollabUpgrade } = await import('./routes/collab.mjs');
|
|
589
|
+
handleCollabUpgrade(request, socket, head, collabWss);
|
|
590
|
+
} catch (err) {
|
|
591
|
+
socket.write('HTTP/1.1 500 Internal Server Error\r\n\r\n');
|
|
592
|
+
socket.destroy();
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
|
|
577
597
|
if (HOST !== configuredHost) {
|
|
578
598
|
logger.error("server", `Refusing public bind '${configuredHost}' in production. Bound to ${HOST}.`);
|
|
579
599
|
}
|
package/src/server/rest.mjs
CHANGED
|
@@ -944,12 +944,19 @@ router.get('/api/extension/status', requireAuth, requireScope('config:read'), as
|
|
|
944
944
|
try {
|
|
945
945
|
const extDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../extension');
|
|
946
946
|
const available = fs.existsSync(path.join(extDir, 'manifest.json'));
|
|
947
|
+
let version = '0.0.0';
|
|
948
|
+
if (available) {
|
|
949
|
+
try {
|
|
950
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(extDir, 'manifest.json'), 'utf8'));
|
|
951
|
+
version = manifest.version || '0.0.0';
|
|
952
|
+
} catch {}
|
|
953
|
+
}
|
|
947
954
|
|
|
948
955
|
// Check if extension has ever connected by looking for the marker file
|
|
949
956
|
const markerPath = path.join(BRAIN_DIR, 'config', '.extension-connected');
|
|
950
957
|
const connected = fs.existsSync(markerPath);
|
|
951
958
|
|
|
952
|
-
res.json({ available, connected });
|
|
959
|
+
res.json({ available, connected, version });
|
|
953
960
|
} catch (err) { serverError(res, err); }
|
|
954
961
|
});
|
|
955
962
|
|
|
@@ -2067,4 +2074,45 @@ router.post('/api/update/run', requireAuth, async (req, res) => {
|
|
|
2067
2074
|
}
|
|
2068
2075
|
});
|
|
2069
2076
|
|
|
2077
|
+
router.get('/api/help', requireAuth, (req, res) => {
|
|
2078
|
+
const { topic } = req.query;
|
|
2079
|
+
const docsDir = path.join(ROOT, 'docs');
|
|
2080
|
+
const referenceDir = path.join(docsDir, 'reference');
|
|
2081
|
+
|
|
2082
|
+
if (!topic) {
|
|
2083
|
+
return res.json({
|
|
2084
|
+
topics: [
|
|
2085
|
+
{ id: 'cli-reference', title: 'CLI Reference Guide', description: 'npx total-recall command catalog and flags' },
|
|
2086
|
+
{ id: 'ssss', title: 'SSSS Specifications', description: 'Structured Semantic Syntax System guide' },
|
|
2087
|
+
{ id: 'architecture', title: 'System Architecture', description: 'System topology and VFS structures' },
|
|
2088
|
+
{ id: 'collab', title: 'Collaboration Guide', description: 'Collaborative workspaces and team annotations' }
|
|
2089
|
+
]
|
|
2090
|
+
});
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
try {
|
|
2094
|
+
let filePath = '';
|
|
2095
|
+
if (topic === 'cli-reference') {
|
|
2096
|
+
filePath = path.join(referenceDir, 'cli-reference.md');
|
|
2097
|
+
} else if (topic === 'ssss') {
|
|
2098
|
+
filePath = path.join(docsDir, 'SSSS.md');
|
|
2099
|
+
} else if (topic === 'architecture') {
|
|
2100
|
+
filePath = path.join(docsDir, 'ARCHITECTURE.md');
|
|
2101
|
+
} else if (topic === 'collab') {
|
|
2102
|
+
filePath = path.join(referenceDir, 'collab.md');
|
|
2103
|
+
} else {
|
|
2104
|
+
return res.status(404).json({ error: 'Help topic not found' });
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
if (!fs.existsSync(filePath)) {
|
|
2108
|
+
return res.status(404).json({ error: 'Help topic document not found on server' });
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
2112
|
+
res.json({ topic, content });
|
|
2113
|
+
} catch (err) {
|
|
2114
|
+
serverError(res, err);
|
|
2115
|
+
}
|
|
2116
|
+
});
|
|
2117
|
+
|
|
2070
2118
|
export { router as restRouter };
|