total-recall-brain 3.1.2 → 3.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/extension/background.js +99 -0
  2. package/extension/content-script.css +16 -0
  3. package/extension/content-script.js +134 -0
  4. package/extension/icons/README.md +27 -0
  5. package/extension/icons/icon-128.png +0 -0
  6. package/extension/icons/icon-16.png +0 -0
  7. package/extension/icons/icon-48.png +0 -0
  8. package/extension/lib/brain-client.js +47 -0
  9. package/extension/manifest.json +39 -0
  10. package/extension/options/options.css +218 -0
  11. package/extension/options/options.html +72 -0
  12. package/extension/options/options.js +99 -0
  13. package/extension/popup/popup.css +221 -0
  14. package/extension/popup/popup.html +44 -0
  15. package/extension/popup/popup.js +107 -0
  16. package/extension/sidepanel/sidepanel.css +340 -0
  17. package/extension/sidepanel/sidepanel.html +38 -0
  18. package/extension/sidepanel/sidepanel.js +229 -0
  19. package/frontend/README.md +73 -0
  20. package/frontend/index.html +17 -0
  21. package/frontend/package.json +31 -0
  22. package/frontend/src/App.css +2 -0
  23. package/frontend/src/App.tsx +390 -0
  24. package/frontend/src/api.ts +688 -0
  25. package/frontend/src/assets/hero.png +0 -0
  26. package/frontend/src/assets/react.svg +1 -0
  27. package/frontend/src/assets/vite.svg +1 -0
  28. package/frontend/src/components/BrainSelector.tsx +272 -0
  29. package/frontend/src/components/DaemonLogsTab.tsx +54 -0
  30. package/frontend/src/components/Graph3D.tsx +850 -0
  31. package/frontend/src/components/MarkdownUtils.tsx +99 -0
  32. package/frontend/src/components/ResearchAgendaTab.tsx +707 -0
  33. package/frontend/src/components/TaskQueueTab.tsx +185 -0
  34. package/frontend/src/index.css +1589 -0
  35. package/frontend/src/main.tsx +10 -0
  36. package/frontend/src/pages/ApiKeysPage.tsx +288 -0
  37. package/frontend/src/pages/AutomationsPage.tsx +354 -0
  38. package/frontend/src/pages/ChatPage.tsx +806 -0
  39. package/frontend/src/pages/DeploymentsPage.tsx +275 -0
  40. package/frontend/src/pages/FilesPage.tsx +384 -0
  41. package/frontend/src/pages/HealthPage.tsx +91 -0
  42. package/frontend/src/pages/IntegrationsPage.tsx +382 -0
  43. package/frontend/src/pages/LoginPage.tsx +237 -0
  44. package/frontend/src/pages/MemoryPage.tsx +1192 -0
  45. package/frontend/src/pages/SandboxPage.tsx +113 -0
  46. package/frontend/src/pages/SettingsPage.tsx +662 -0
  47. package/frontend/src/pages/SkillsPage.tsx +618 -0
  48. package/frontend/src/pages/TasksPage.tsx +392 -0
  49. package/frontend/src/pages/UsagePage.tsx +260 -0
  50. package/frontend/src/types.ts +163 -0
  51. package/frontend/tsconfig.app.json +25 -0
  52. package/frontend/tsconfig.json +7 -0
  53. package/frontend/tsconfig.node.json +24 -0
  54. package/frontend/vite.config.ts +24 -0
  55. package/package.json +8 -1
  56. package/scaffold/.agent/skills/total-recall/memory-vault/concepts/cli-help-reference.md +35 -8
  57. package/scaffold/.agent/skills/total-recall/memory-vault/invariants/operating-instructions.md +13 -8
  58. package/scaffold/.agent/skills/total-recall/memory-vault/preferences/always-websearch-gap.md +28 -1
  59. package/scaffold/.agent/skills/total-recall/memory-vault/preferences/topic-research-sop.md +28 -1
  60. package/scaffold/.agent/skills/total-recall/skills/ssss/references/ssss-spec.md +830 -81
@@ -0,0 +1,688 @@
1
+ // ─── API helpers for Total Recall backend ────────────────────────────────────────
2
+
3
+ import type { HealthData, MemoryNode, SandboxResult, ResearchItem, Conflict, UsageData, ConfigJson } from './types'
4
+
5
+ let API_BASE = localStorage.getItem('TOTAL_RECALL_API_BASE') || ''
6
+
7
+ export function getApiBase() { return API_BASE }
8
+ export function setApiBase(url: string) {
9
+ API_BASE = url
10
+ localStorage.setItem('TOTAL_RECALL_API_BASE', url)
11
+ window.location.reload()
12
+ }
13
+
14
+ // ─── Global 401 handler ────────────────────────────────────────────────────────
15
+ // Components can register a callback to be notified when any request gets 401.
16
+ // App.tsx registers this on mount to flip auth state → shows login screen.
17
+ type UnauthedCallback = () => void
18
+ let onUnauthed: UnauthedCallback | null = null
19
+ export function registerUnauthedCallback(cb: UnauthedCallback) { onUnauthed = cb }
20
+ export function clearUnauthedCallback() { onUnauthed = null }
21
+
22
+ async function apiFetch(url: string, options: RequestInit = {}): Promise<Response> {
23
+ const res = await fetch(url, { credentials: 'include', ...options })
24
+ if (res.status === 401) {
25
+ onUnauthed?.()
26
+ }
27
+ return res
28
+ }
29
+
30
+ // ─── Auth ──────────────────────────────────────────────────────────────────────
31
+
32
+ export async function checkSession(): Promise<boolean> {
33
+ try {
34
+ const res = await fetch(API_BASE + '/auth/me', { credentials: 'include' })
35
+ return res.ok
36
+ } catch {
37
+ return false
38
+ }
39
+ }
40
+
41
+ export async function login(password: string): Promise<{ ok: boolean; requiresPasswordReset?: boolean; error?: string }> {
42
+ try {
43
+ const res = await fetch(API_BASE + '/auth/login', {
44
+ method: 'POST',
45
+ headers: { 'Content-Type': 'application/json' },
46
+ credentials: 'include',
47
+ body: JSON.stringify({ password }),
48
+ })
49
+ const data = await res.json().catch(() => ({}))
50
+ if (res.ok) return { ok: true, requiresPasswordReset: data.requiresPasswordReset }
51
+ return { ok: false, error: data.error || 'Invalid password' }
52
+ } catch {
53
+ return { ok: false, error: 'Network error — is the server running?' }
54
+ }
55
+ }
56
+
57
+ export async function changePassword(newPassword: string): Promise<{ ok: boolean; error?: string }> {
58
+ try {
59
+ const res = await fetch(API_BASE + '/auth/change-password', {
60
+ method: 'POST',
61
+ headers: { 'Content-Type': 'application/json' },
62
+ credentials: 'include',
63
+ body: JSON.stringify({ newPassword }),
64
+ })
65
+ if (res.ok) return { ok: true }
66
+ const data = await res.json().catch(() => ({}))
67
+ return { ok: false, error: data.error || 'Failed to change password' }
68
+ } catch {
69
+ return { ok: false, error: 'Network error' }
70
+ }
71
+ }
72
+
73
+ export async function getAuthStatus(): Promise<{ configured: boolean }> {
74
+ try {
75
+ const res = await fetch(API_BASE + "/auth/status")
76
+ if (res.ok) return await res.json()
77
+ return { configured: true }
78
+ } catch {
79
+ return { configured: true }
80
+ }
81
+ }
82
+
83
+ export async function setupPassword(newPassword: string): Promise<{ ok: boolean; error?: string }> {
84
+ try {
85
+ const res = await fetch(API_BASE + "/auth/setup", {
86
+ method: "POST",
87
+ headers: { "Content-Type": "application/json" },
88
+ body: JSON.stringify({ newPassword }),
89
+ })
90
+ const data = await res.json().catch(() => ({}))
91
+ if (res.ok) return { ok: true }
92
+ return { ok: false, error: data.error || "Failed to setup password" }
93
+ } catch {
94
+ return { ok: false, error: "Network error — is the server running?" }
95
+ }
96
+ }
97
+
98
+ export async function logout(): Promise<void> {
99
+ await fetch(API_BASE + '/auth/logout', { method: 'POST', credentials: 'include' }).catch(() => {})
100
+ }
101
+
102
+ // ─── Chat ──────────────────────────────────────────────────────────────────────
103
+
104
+ export interface ChatThread {
105
+ id: string
106
+ title: string
107
+ turns: number
108
+ lastUpdated: number
109
+ brainId: string
110
+ }
111
+
112
+ export interface ChatSuggestion {
113
+ type: string
114
+ title: string
115
+ text: string
116
+ nodes: string[]
117
+ }
118
+
119
+ export async function sendChat(
120
+ messages: { role: string; content: string }[],
121
+ signal?: AbortSignal,
122
+ sessionId?: string,
123
+ groundingNodes?: string[],
124
+ model?: string,
125
+ brainId?: string
126
+ ): Promise<string> {
127
+ const headers: Record<string, string> = { 'Content-Type': 'application/json' }
128
+ if (sessionId) {
129
+ headers['x-session-id'] = sessionId
130
+ }
131
+ const res = await apiFetch(API_BASE + '/v1/chat/completions', {
132
+ method: 'POST',
133
+ signal,
134
+ headers,
135
+ body: JSON.stringify({ messages, groundingNodes, model, brainId }),
136
+ })
137
+ if (!res.ok) throw new Error(`Chat API error: ${res.status}`)
138
+ const data = await res.json()
139
+ return data.choices?.[0]?.message?.content ?? '(empty response)'
140
+ }
141
+
142
+ export async function fetchChatHistory(sessionId?: string): Promise<{ role: string; content: string }[]> {
143
+ const headers: Record<string, string> = {}
144
+ if (sessionId) {
145
+ headers['x-session-id'] = sessionId
146
+ }
147
+ const res = await apiFetch(API_BASE + '/v1/chat/history', { headers })
148
+ if (!res.ok) throw new Error(`Chat history error: ${res.status}`)
149
+ const data = await res.json()
150
+ return data.messages ?? []
151
+ }
152
+
153
+ export async function fetchChatThreads(): Promise<ChatThread[]> {
154
+ const res = await apiFetch(API_BASE + '/v1/chat/threads')
155
+ if (!res.ok) throw new Error(`Chat threads list error: ${res.status}`)
156
+ return res.json()
157
+ }
158
+
159
+ export async function deleteChatThread(id: string): Promise<{ deleted: boolean; id: string }> {
160
+ const res = await apiFetch(API_BASE + `/v1/chat/threads/${id}`, { method: 'DELETE' })
161
+ if (!res.ok) throw new Error(`Delete chat thread error: ${res.status}`)
162
+ return res.json()
163
+ }
164
+
165
+ export async function fetchChatSuggestions(): Promise<ChatSuggestion[]> {
166
+ const res = await apiFetch(API_BASE + '/v1/chat/suggestions')
167
+ if (!res.ok) throw new Error(`Chat suggestions list error: ${res.status}`)
168
+ return res.json()
169
+ }
170
+
171
+
172
+
173
+ // ─── TTS (Kokoro-82M) ──────────────────────────────────────────────────────────
174
+
175
+ export async function fetchTtsStatus(): Promise<{ enabled: boolean }> {
176
+ try {
177
+ const res = await apiFetch(API_BASE + '/api/tts/status')
178
+ if (!res.ok) return { enabled: false }
179
+ return res.json()
180
+ } catch {
181
+ return { enabled: false }
182
+ }
183
+ }
184
+
185
+ export async function fetchTtsAudio(text: string): Promise<Blob | null> {
186
+ const res = await apiFetch(API_BASE + '/api/tts', {
187
+ method: 'POST',
188
+ headers: { 'Content-Type': 'application/json' },
189
+ body: JSON.stringify({ text }),
190
+ })
191
+ if (res.status === 503) return null // Kokoro not configured — caller should fall back.
192
+ if (!res.ok) throw new Error(`TTS error: ${res.status}`)
193
+ return res.blob()
194
+ }
195
+
196
+ // ─── Health ────────────────────────────────────────────────────────────────────
197
+
198
+ export async function fetchHealth(): Promise<HealthData> {
199
+ const res = await apiFetch(API_BASE + '/health')
200
+ if (!res.ok) throw new Error(`Health API error: ${res.status}`)
201
+ return res.json()
202
+ }
203
+
204
+ export async function listMemory(brainId?: string, category?: string, status?: string): Promise<MemoryNode[]> {
205
+ const params = new URLSearchParams()
206
+ if (category) params.set('category', category)
207
+ if (status) params.set('status', status)
208
+
209
+ if (!brainId) {
210
+ const res = await apiFetch(`${API_BASE}/api/memory?${params}`)
211
+ if (!res.ok) throw new Error(`Memory API error: ${res.status}`)
212
+ const data = await res.json()
213
+ return Array.isArray(data) ? data : (data.nodes || [])
214
+ }
215
+
216
+ const ids = brainId.split(',')
217
+ const fetchPromises = ids.map(async (id) => {
218
+ const url = id === 'global'
219
+ ? `${API_BASE}/api/memory?${params}`
220
+ : `${API_BASE}/api/brains/${id}/nodes?${params}`
221
+ try {
222
+ const res = await apiFetch(url)
223
+ if (!res.ok) return []
224
+ const data = await res.json()
225
+ return Array.isArray(data) ? data : (data.nodes || [])
226
+ } catch {
227
+ return []
228
+ }
229
+ })
230
+
231
+ const results = await Promise.all(fetchPromises)
232
+ const merged: MemoryNode[] = []
233
+ const seenSlugs = new Set<string>()
234
+
235
+ // Project brains override Global brain nodes on slug collisions
236
+ const sortedResults = results.map((nodes, index) => ({ nodes, id: ids[index] }))
237
+ .sort((a, b) => {
238
+ if (a.id === 'global') return 1
239
+ if (b.id === 'global') return -1
240
+ return 0
241
+ })
242
+
243
+ for (const { nodes } of sortedResults) {
244
+ for (const node of nodes) {
245
+ if (!seenSlugs.has(node.slug)) {
246
+ seenSlugs.add(node.slug)
247
+ merged.push(node)
248
+ }
249
+ }
250
+ }
251
+
252
+ return merged
253
+ }
254
+
255
+ export async function searchMemory(query: string, category?: string): Promise<MemoryNode[]> {
256
+ const params = new URLSearchParams({ q: query })
257
+ if (category) params.set('category', category)
258
+ const res = await apiFetch(`${API_BASE}/api/memory?${params}`)
259
+ if (!res.ok) throw new Error(`Memory API error: ${res.status}`)
260
+ const data = await res.json()
261
+ return Array.isArray(data) ? data : (data.nodes || [])
262
+ }
263
+
264
+ export async function readMemory(slug: string): Promise<MemoryNode | null> {
265
+ const res = await apiFetch(`${API_BASE}/api/memory/${slug}`)
266
+ if (res.status === 404) return null
267
+ if (!res.ok) throw new Error(`Memory API error: ${res.status}`)
268
+ return res.json()
269
+ }
270
+
271
+ export async function runSandbox(code: string, timeoutMs = 5000): Promise<SandboxResult> {
272
+ try {
273
+ const res = await apiFetch(`${API_BASE}/api/sandbox`, {
274
+ method: 'POST',
275
+ headers: { 'Content-Type': 'application/json' },
276
+ body: JSON.stringify({ code, timeout_ms: timeoutMs }),
277
+ })
278
+ if (!res.ok) throw new Error(`Sandbox API error: ${res.status}`)
279
+ const data = await res.json()
280
+ return { success: data.success, output: data.output || '(no output)', isError: !data.success }
281
+ } catch (e) {
282
+ return { success: false, output: (e as Error).message, isError: true }
283
+ }
284
+ }
285
+
286
+
287
+ // ─── Tasks ─────────────────────────────────────────────────────────────────────
288
+
289
+ export async function listTasks(): Promise<import('./types').Task[]> {
290
+ const res = await apiFetch(API_BASE + '/api/tasks')
291
+ if (!res.ok) throw new Error(`Tasks API error: ${res.status}`)
292
+ return res.json()
293
+ }
294
+
295
+ export async function createTask(category: string, target: string, body: string, priority?: number): Promise<{ slug: string }> {
296
+ const res = await apiFetch(API_BASE + '/api/tasks', {
297
+ method: 'POST',
298
+ headers: { 'Content-Type': 'application/json' },
299
+ body: JSON.stringify({ category, target, body, priority }),
300
+ })
301
+ if (!res.ok) throw new Error(`Tasks API error: ${res.status}`)
302
+ return res.json()
303
+ }
304
+
305
+ // ─── Files ─────────────────────────────────────────────────────────────────────
306
+
307
+ export async function listFiles(): Promise<import('./types').FileNode[]> {
308
+ const res = await apiFetch(API_BASE + '/api/files')
309
+ if (!res.ok) throw new Error(`Files API error: ${res.status}`)
310
+ return res.json()
311
+ }
312
+
313
+ export async function listSkills(): Promise<import('./types').FileNode[]> {
314
+ const res = await apiFetch(API_BASE + '/api/skills')
315
+ if (!res.ok) throw new Error(`Skills API error: ${res.status}`)
316
+ return res.json()
317
+ }
318
+
319
+ export async function fetchSkill(name: string): Promise<{ name: string; content: string }> {
320
+ const res = await apiFetch(`${API_BASE}/api/skills/${name}`)
321
+ if (!res.ok) throw new Error(`Skill fetch API error: ${res.status}`)
322
+ return res.json()
323
+ }
324
+
325
+ export async function saveSkill(name: string, content: string): Promise<void> {
326
+ const res = await apiFetch(`${API_BASE}/api/skills/${name}`, {
327
+ method: 'PUT',
328
+ headers: { 'Content-Type': 'application/json' },
329
+ body: JSON.stringify({ content }),
330
+ })
331
+ if (!res.ok) throw new Error(`Skill save API error: ${res.status}`)
332
+ }
333
+
334
+ export async function deleteSkill(name: string): Promise<void> {
335
+ const res = await apiFetch(`${API_BASE}/api/skills/${name}`, {
336
+ method: 'DELETE',
337
+ })
338
+ if (!res.ok) throw new Error(`Skill delete API error: ${res.status}`)
339
+ }
340
+
341
+ /**
342
+ * Queries the skills.sh registry sorted by absolute installs rating.
343
+ *
344
+ * @param query - The keyword or package name search term to locate in the registry.
345
+ * @returns A promise resolving to an array of matching registry skill records.
346
+ */
347
+ export async function searchSkillsRegistry(query: string): Promise<unknown[]> {
348
+ const params = new URLSearchParams({ q: query })
349
+ const res = await apiFetch(`${API_BASE}/api/skills/search?${params}`)
350
+ if (!res.ok) throw new Error(`Registry search API error: ${res.status}`)
351
+ return res.json()
352
+ }
353
+
354
+ /**
355
+ * Automates downloading a package from skills.sh, executing static security checks,
356
+ * quarantining vulnerable files, and hot-recompiling the active brain shims.
357
+ *
358
+ * @param pkg - The package name format (e.g. "owner/repo@skill" or "owner/repo").
359
+ * @returns A promise resolving to the status result containing success indicators and logs.
360
+ */
361
+ export async function installRegistrySkill(pkg: string): Promise<{ success: boolean; reason?: string; skillName?: string; path?: string }> {
362
+ const res = await apiFetch(`${API_BASE}/api/skills/install`, {
363
+ method: 'POST',
364
+ headers: { 'Content-Type': 'application/json' },
365
+ body: JSON.stringify({ pkg }),
366
+ })
367
+ if (!res.ok) throw new Error(`Registry installation API error: ${res.status}`)
368
+ return res.json()
369
+ }
370
+
371
+ export async function fetchUsageStats(): Promise<UsageData> {
372
+ const res = await apiFetch(`${API_BASE}/api/usage`)
373
+ if (!res.ok) throw new Error(`Usage API error: ${res.status}`)
374
+ return res.json()
375
+ }
376
+
377
+ // ─── Config ────────────────────────────────────────────────────────────────────
378
+
379
+ export async function fetchConfig(name: string): Promise<string> {
380
+ const res = await apiFetch(`${API_BASE}/api/config/${name}`)
381
+ if (!res.ok) throw new Error(`Config API error: ${res.status}`)
382
+ const data = await res.json()
383
+ return data.content
384
+ }
385
+
386
+ export async function saveConfig(name: string, content: string): Promise<void> {
387
+ const res = await apiFetch(`${API_BASE}/api/config/${name}`, {
388
+ method: 'PUT',
389
+ headers: { 'Content-Type': 'application/json' },
390
+ body: JSON.stringify({ content }),
391
+ })
392
+ if (!res.ok) throw new Error(`Config API error: ${res.status}`)
393
+ }
394
+
395
+ export async function fetchConfigJson(): Promise<ConfigJson> {
396
+ const res = await apiFetch(`${API_BASE}/api/config-json`)
397
+ if (!res.ok) throw new Error(`Config JSON API error: ${res.status}`)
398
+ return res.json()
399
+ }
400
+
401
+ export async function saveConfigJson(config: ConfigJson): Promise<void> {
402
+ const res = await apiFetch(`${API_BASE}/api/config-json`, {
403
+ method: 'POST',
404
+ headers: { 'Content-Type': 'application/json' },
405
+ body: JSON.stringify(config),
406
+ })
407
+ if (!res.ok) throw new Error(`Config JSON API error: ${res.status}`)
408
+ }
409
+
410
+ // ─── API Key Lifecycle ──────────────────────────────────────────────────────────
411
+
412
+ export interface ApiKey {
413
+ id: string
414
+ name: string
415
+ token_preview: string
416
+ scopes: string[]
417
+ expires_at: string | null
418
+ created_at: string
419
+ last_used_at: string | null
420
+ hit_count: number
421
+ revoked: boolean
422
+ }
423
+
424
+ export interface IssuedApiKey extends ApiKey {
425
+ token: string // full token — only returned on creation
426
+ }
427
+
428
+ export interface ApiKeyListResponse {
429
+ keys: ApiKey[]
430
+ available_scopes: string[]
431
+ }
432
+
433
+ export async function listApiKeys(): Promise<ApiKeyListResponse> {
434
+ const res = await apiFetch(`${API_BASE}/api/keys`)
435
+ if (!res.ok) throw new Error(`Keys API error: ${res.status}`)
436
+ const data = await res.json()
437
+ return Array.isArray(data) ? { keys: data, available_scopes: ['*'] } : data
438
+ }
439
+
440
+ export async function issueApiKey(name: string, scopes?: string[], expiresAt?: string | null): Promise<IssuedApiKey> {
441
+ const res = await apiFetch(`${API_BASE}/api/keys`, {
442
+ method: 'POST',
443
+ headers: { 'Content-Type': 'application/json' },
444
+ body: JSON.stringify({ name, scopes, expires_at: expiresAt || null }),
445
+ })
446
+ if (!res.ok) throw new Error(`Keys API error: ${res.status}`)
447
+ return res.json()
448
+ }
449
+
450
+ export async function revokeApiKey(id: string): Promise<void> {
451
+ const res = await apiFetch(`${API_BASE}/api/keys/${id}`, { method: 'DELETE' })
452
+ if (!res.ok) throw new Error(`Keys API error: ${res.status}`)
453
+ }
454
+
455
+ export async function connectClient(client: string, baseUrl: string): Promise<{ success: boolean; message: string }> {
456
+ const res = await apiFetch(`${API_BASE}/api/integrations/connect`, {
457
+ method: 'POST',
458
+ headers: { 'Content-Type': 'application/json' },
459
+ body: JSON.stringify({ client, baseUrl }),
460
+ })
461
+ if (!res.ok) {
462
+ const errorData = await res.json().catch(() => ({}))
463
+ throw new Error(errorData.error || `Connection error: ${res.status}`)
464
+ }
465
+ return res.json()
466
+ }
467
+
468
+ export async function fetchActiveIntegrations(): Promise<{ success: boolean; active: string[] }> {
469
+ const res = await apiFetch(`${API_BASE}/api/integrations/active`, { method: 'GET' })
470
+ if (!res.ok) throw new Error(`Integrations API error: ${res.status}`)
471
+ return res.json()
472
+ }
473
+
474
+ // ─── Research & Logs ───────────────────────────────────────────────────────────
475
+
476
+ export interface ResearchQueueResponse {
477
+ counts: { pending: number; in_progress: number; done: number; failed: number }
478
+ total: number
479
+ items: ResearchItem[]
480
+ }
481
+
482
+ export async function listResearch(status?: string): Promise<ResearchQueueResponse> {
483
+ const params = new URLSearchParams()
484
+ if (status) params.set('status', status)
485
+ const res = await apiFetch(`${API_BASE}/api/research?${params}`)
486
+ if (!res.ok) throw new Error(`Research API error: ${res.status}`)
487
+ return res.json()
488
+ }
489
+
490
+ export async function createResearch(topic: string, priority?: string, notes?: string): Promise<ResearchItem> {
491
+ const res = await apiFetch(`${API_BASE}/api/research`, {
492
+ method: 'POST',
493
+ headers: { 'Content-Type': 'application/json' },
494
+ body: JSON.stringify({ topic, priority, notes }),
495
+ })
496
+ if (!res.ok) throw new Error(`Research API error: ${res.status}`)
497
+ return res.json()
498
+ }
499
+
500
+ export async function patchResearch(id: string, updates: Record<string, unknown>): Promise<unknown> {
501
+ const res = await apiFetch(`${API_BASE}/api/research/${id}`, {
502
+ method: 'PATCH',
503
+ headers: { 'Content-Type': 'application/json' },
504
+ body: JSON.stringify(updates),
505
+ })
506
+ if (!res.ok) throw new Error(`Research PATCH error: ${res.status}`)
507
+ return res.json()
508
+ }
509
+
510
+ export async function deleteResearch(id: string): Promise<void> {
511
+ const res = await apiFetch(`${API_BASE}/api/research/${id}`, { method: 'DELETE' })
512
+ if (!res.ok) throw new Error(`Research DELETE error: ${res.status}`)
513
+ }
514
+
515
+ export async function fetchLogs(type: 'server' | 'daemon'): Promise<{ content: string }> {
516
+ const res = await apiFetch(`${API_BASE}/api/logs/${type}`)
517
+ if (!res.ok) throw new Error(`Logs API error: ${res.status}`)
518
+ return res.json()
519
+ }
520
+
521
+ export async function triggerRecompile(): Promise<{ success: boolean; message: string }> {
522
+ const res = await apiFetch(`${API_BASE}/api/vault/compile`, { method: 'POST' })
523
+ if (!res.ok) throw new Error(`Recompile API error: ${res.status}`)
524
+ return res.json()
525
+ }
526
+
527
+ export async function triggerDream(): Promise<{ success: boolean; status: string }> {
528
+ const res = await apiFetch(`${API_BASE}/api/dream`, { method: 'POST' })
529
+ if (!res.ok) throw new Error(`Dream Cycle API error: ${res.status}`)
530
+ return res.json()
531
+ }
532
+
533
+ export async function fetchGraph(): Promise<{ nodes: MemoryNode[]; routes: unknown[] }> {
534
+ const res = await apiFetch(`${API_BASE}/api/graph`)
535
+ if (!res.ok) throw new Error(`Graph API error: ${res.status}`)
536
+ return res.json()
537
+ }
538
+
539
+ export async function fetchConflicts(): Promise<{ conflicts: Conflict[] }> {
540
+ const res = await apiFetch(`${API_BASE}/api/conflicts`)
541
+ if (!res.ok) throw new Error(`Conflicts API error: ${res.status}`)
542
+ return res.json()
543
+ }
544
+
545
+ export async function resolveConflict(
546
+ conflictId: string,
547
+ action: "keep" | "supersede",
548
+ winnerSlug: string
549
+ ): Promise<{ success: boolean; conflict_id: string }> {
550
+ const res = await apiFetch(`${API_BASE}/api/conflicts/resolve`, {
551
+ method: "POST",
552
+ headers: { "Content-Type": "application/json" },
553
+ body: JSON.stringify({ conflict_id: conflictId, action, winner_slug: winnerSlug }),
554
+ })
555
+ if (!res.ok) {
556
+ const errorData = await res.json().catch(() => ({}))
557
+ throw new Error(errorData.error || `Conflict resolution API error: ${res.status}`)
558
+ }
559
+ return res.json()
560
+ }
561
+
562
+ // ─── Memory Editor APIs ────────────────────────────────────────────────────────
563
+
564
+ export async function saveMemory(slug: string, node: Partial<MemoryNode>): Promise<MemoryNode> {
565
+ const res = await apiFetch(`${API_BASE}/api/memory/${slug}`, {
566
+ method: "PUT",
567
+ headers: { "Content-Type": "application/json" },
568
+ body: JSON.stringify(node),
569
+ })
570
+ if (!res.ok) throw new Error(`Failed to save memory: ${res.status}`)
571
+ return res.json()
572
+ }
573
+
574
+ export async function createMemory(node: Partial<MemoryNode> & { slug: string }): Promise<MemoryNode> {
575
+ const res = await apiFetch(`${API_BASE}/api/memory`, {
576
+ method: "POST",
577
+ headers: { "Content-Type": "application/json" },
578
+ body: JSON.stringify(node),
579
+ })
580
+ if (!res.ok) throw new Error(`Failed to create memory: ${res.status}`)
581
+ return res.json()
582
+ }
583
+
584
+ export async function deleteMemory(slug: string): Promise<void> {
585
+ const res = await apiFetch(`${API_BASE}/api/memory/${slug}`, {
586
+ method: "DELETE",
587
+ })
588
+ if (!res.ok) throw new Error(`Failed to delete memory: ${res.status}`)
589
+ }
590
+
591
+ // ─── Scripts APIs ──────────────────────────────────────────────────────────────
592
+
593
+ export interface ScriptFile {
594
+ name: string
595
+ size: number
596
+ modified: string
597
+ }
598
+
599
+ export async function listScripts(): Promise<ScriptFile[]> {
600
+ const res = await apiFetch(`${API_BASE}/api/scripts`)
601
+ if (!res.ok) throw new Error(`Failed to list scripts: ${res.status}`)
602
+ return res.json()
603
+ }
604
+
605
+ export async function readScript(name: string): Promise<{ name: string; content: string }> {
606
+ const res = await apiFetch(`${API_BASE}/api/scripts/${name}`)
607
+ if (!res.ok) throw new Error(`Failed to read script: ${res.status}`)
608
+ return res.json()
609
+ }
610
+
611
+ export async function saveScript(name: string, content: string): Promise<{ success: boolean; message: string }> {
612
+ const res = await apiFetch(`${API_BASE}/api/scripts/${name}`, {
613
+ method: "PUT",
614
+ headers: { "Content-Type": "application/json" },
615
+ body: JSON.stringify({ content }),
616
+ })
617
+ if (!res.ok) throw new Error(`Failed to save script: ${res.status}`)
618
+ return res.json()
619
+ }
620
+
621
+ export async function runScript(name: string): Promise<{ success: boolean; output: string; exitCode: number }> {
622
+ const res = await apiFetch(`${API_BASE}/api/scripts/${name}/run`, {
623
+ method: "POST"
624
+ })
625
+ if (!res.ok) throw new Error(`Failed to run script: ${res.status}`)
626
+ return res.json()
627
+ }
628
+
629
+ // ─── Sessions APIs ─────────────────────────────────────────────────────────────
630
+
631
+ export interface SessionSummary {
632
+ id: string
633
+ filename: string
634
+ title: string
635
+ date: string
636
+ source: string | null
637
+ project: string | null
638
+ count: number
639
+ modified: string
640
+ size: number
641
+ }
642
+
643
+ export async function fetchSessions(limit = 50, offset = 0): Promise<{ total: number; sessions: SessionSummary[] }> {
644
+ const params = new URLSearchParams({ limit: String(limit), offset: String(offset) })
645
+ const res = await apiFetch(`${API_BASE}/api/sessions?${params}`)
646
+ if (!res.ok) throw new Error(`Sessions API error: ${res.status}`)
647
+ return res.json()
648
+ }
649
+
650
+ export async function deleteSession(id: string): Promise<void> {
651
+ const res = await apiFetch(`${API_BASE}/api/sessions/${id}`, { method: 'DELETE' })
652
+ if (!res.ok) throw new Error(`Failed to delete session: ${res.status}`)
653
+ }
654
+
655
+ // ─── Gemini Models APIs ─────────────────────────────────────────────────────────
656
+
657
+ export interface GeminiModelInfo {
658
+ id: string
659
+ displayName: string
660
+ }
661
+
662
+ export async function fetchGeminiModels(): Promise<GeminiModelInfo[]> {
663
+ const res = await apiFetch(`${API_BASE}/api/gemini-models`)
664
+ if (!res.ok) throw new Error(`Gemini models API error: ${res.status}`)
665
+ const data = await res.json()
666
+ return data.models ?? []
667
+ }
668
+
669
+ // ─── Share API ──────────────────────────────────────────────────────────────────
670
+
671
+ export async function shareToApi(payload: { url: string; action: string; title?: string; tags?: string[] }): Promise<{ success: boolean; message?: string }> {
672
+ const res = await apiFetch(`${API_BASE}/api/share`, {
673
+ method: 'POST',
674
+ headers: { 'Content-Type': 'application/json' },
675
+ body: JSON.stringify(payload),
676
+ })
677
+ if (!res.ok) throw new Error(`Share API error: ${res.status}`)
678
+ return res.json()
679
+ }
680
+
681
+ // ─── Extension Status ──────────────────────────────────────────────────────────
682
+
683
+ export async function fetchExtensionStatus(): Promise<{ available: boolean; connected: boolean }> {
684
+ const res = await apiFetch(`${API_BASE}/api/extension/status`)
685
+ if (!res.ok) return { available: false, connected: false }
686
+ return res.json()
687
+ }
688
+
Binary file