total-recall-brain 3.14.4 → 3.14.7

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 (142) hide show
  1. package/frontend/dist/assets/index-D9-jAOr9.js +502 -0
  2. package/frontend/dist/assets/index-L0eEIoBf.css +1 -0
  3. package/frontend/dist/brand/total-recall-badge.jpg +0 -0
  4. package/frontend/dist/brand/total-recall-icon.svg +6 -0
  5. package/frontend/dist/brand/total-recall-lockup.png +0 -0
  6. package/frontend/dist/brand/total-recall-mark.svg +9 -0
  7. package/frontend/dist/favicon.svg +6 -0
  8. package/frontend/dist/icons.svg +24 -0
  9. package/frontend/dist/index.html +27 -0
  10. package/frontend/dist/manifest.json +25 -0
  11. package/frontend/dist/sw.js +272 -0
  12. package/frontend/index.html +7 -2
  13. package/frontend/package.json +2 -0
  14. package/frontend/src/App.spec.tsx +82 -0
  15. package/frontend/src/api/_base.ts +35 -0
  16. package/frontend/src/api/api.spec.ts +297 -0
  17. package/frontend/src/api/auth.ts +73 -0
  18. package/frontend/src/api/chat.ts +93 -0
  19. package/frontend/src/api/config.ts +35 -0
  20. package/frontend/src/api/docs.ts +146 -0
  21. package/frontend/src/api/extension.ts +9 -0
  22. package/frontend/src/api/index.ts +20 -0
  23. package/frontend/src/api/integrations.ts +22 -0
  24. package/frontend/src/api/keys.ts +384 -0
  25. package/frontend/src/api/memory.ts +234 -0
  26. package/frontend/src/api/models.ts +36 -0
  27. package/frontend/src/api/research.ts +43 -0
  28. package/frontend/src/api/sandbox.ts +19 -0
  29. package/frontend/src/api/sessions.ts +27 -0
  30. package/frontend/src/api/skills.ts +127 -0
  31. package/frontend/src/api/system.ts +165 -0
  32. package/frontend/src/api/update.ts +21 -0
  33. package/frontend/src/components/BrainSelector.spec.tsx +72 -0
  34. package/frontend/src/components/DaemonLogsTab.spec.tsx +21 -0
  35. package/frontend/src/components/DocumentEditorModal.spec.tsx +80 -0
  36. package/frontend/src/components/DocumentTable.spec.tsx +45 -0
  37. package/frontend/src/components/Graph3D.spec.tsx +75 -0
  38. package/frontend/src/components/MarkdownUtils.spec.tsx +82 -0
  39. package/frontend/src/components/MobilePairing.spec.tsx +24 -0
  40. package/frontend/src/components/MobilePairing.tsx +37 -0
  41. package/frontend/src/components/ResearchAgendaTab.spec.tsx +127 -0
  42. package/frontend/src/components/TaskQueueTab.spec.tsx +70 -0
  43. package/frontend/src/components/TaskQueueTab.tsx +9 -1
  44. package/frontend/src/components/UsageChart.spec.tsx +89 -0
  45. package/frontend/src/components/VoiceInput.spec.tsx +38 -0
  46. package/frontend/src/components/VoiceInput.tsx +68 -0
  47. package/frontend/src/index.css +30 -0
  48. package/frontend/src/main.tsx +13 -0
  49. package/frontend/src/pages/ApiKeysPage.spec.tsx +32 -0
  50. package/frontend/src/pages/AutomationsPage.spec.tsx +60 -0
  51. package/frontend/src/pages/ChatPage.spec.tsx +37 -0
  52. package/frontend/src/pages/ChatPage.tsx +14 -0
  53. package/frontend/src/pages/CollabPage.spec.tsx +64 -0
  54. package/frontend/src/pages/DesignDocsPage.spec.tsx +43 -0
  55. package/frontend/src/pages/FilesPage.spec.tsx +46 -0
  56. package/frontend/src/pages/GraphPage.spec.tsx +24 -0
  57. package/frontend/src/pages/HealthPage.spec.tsx +31 -0
  58. package/frontend/src/pages/HelpPage.spec.tsx +63 -0
  59. package/frontend/src/pages/InboxPage.spec.tsx +55 -0
  60. package/frontend/src/pages/InstructionsPage.spec.tsx +52 -0
  61. package/frontend/src/pages/InstructionsPage.tsx +1 -1
  62. package/frontend/src/pages/IntegrationsPage.spec.tsx +55 -0
  63. package/frontend/src/pages/LoginPage.spec.tsx +73 -0
  64. package/frontend/src/pages/LoginPage.tsx +20 -0
  65. package/frontend/src/pages/MemoryPage.spec.tsx +51 -0
  66. package/frontend/src/pages/ModelsPage.spec.tsx +64 -0
  67. package/frontend/src/pages/ModelsPage.tsx +18 -11
  68. package/frontend/src/pages/OkfPage.spec.tsx +46 -0
  69. package/frontend/src/pages/OnboardingPage.spec.tsx +58 -0
  70. package/frontend/src/pages/OpenWikiPage.spec.tsx +32 -0
  71. package/frontend/src/pages/SandboxPage.spec.tsx +33 -0
  72. package/frontend/src/pages/SettingsPage.spec.tsx +33 -0
  73. package/frontend/src/pages/SettingsPage.tsx +6 -1
  74. package/frontend/src/pages/SkillsPage.spec.tsx +20 -0
  75. package/frontend/src/pages/TasksPage.spec.tsx +17 -0
  76. package/frontend/src/pages/UsagePage.spec.tsx +35 -0
  77. package/frontend/src/pages/VaultPage.spec.tsx +26 -0
  78. package/frontend/src/setupTests.ts +23 -0
  79. package/frontend/vite.config.ts +6 -0
  80. package/package.json +16 -4
  81. package/scaffold/.agent/skills/total-recall/SKILL.md +6 -2
  82. package/scaffold/.agent/skills/total-recall/skills-registry/index.yaml +2575 -0
  83. package/src/cli/connect.mjs +7 -1
  84. package/src/cli/deploy.mjs +8 -8
  85. package/src/cli/init.mjs +14 -4
  86. package/src/cli/recall.mjs +3 -1
  87. package/src/cli/remember.mjs +2 -1
  88. package/src/cli/restore.mjs +2 -2
  89. package/src/cli/upgrade.mjs +2 -2
  90. package/src/core/clarity-rewriter.mjs +2 -2
  91. package/src/core/crons.mjs +23 -55
  92. package/src/core/crypto.mjs +4 -8
  93. package/src/core/daemon-control.mjs +5 -0
  94. package/src/core/embeddings.mjs +207 -374
  95. package/src/core/fact-seeker.mjs +114 -0
  96. package/src/core/github-sync.mjs +319 -0
  97. package/src/core/obsidian-sync.mjs +129 -0
  98. package/src/core/project-brain.mjs +2 -1
  99. package/src/core/remote-vault-sync.mjs +6 -6
  100. package/src/core/runtime.mjs +1 -1
  101. package/src/core/skills-registry.mjs +11 -0
  102. package/src/core/snapshot.mjs +1 -1
  103. package/src/core/vault-watcher.mjs +6 -3
  104. package/src/server/api.mjs +1 -1
  105. package/src/server/rest.mjs +50 -2163
  106. package/src/server/route-manifest.json +57 -5
  107. package/src/server/routes/brains.mjs +212 -0
  108. package/src/server/routes/capture.mjs +33 -0
  109. package/src/server/routes/collab.mjs +1 -1
  110. package/src/server/routes/config.mjs +206 -0
  111. package/src/server/routes/context.mjs +68 -0
  112. package/src/server/routes/dashboard.mjs +53 -0
  113. package/src/server/routes/dream.mjs +30 -0
  114. package/src/server/routes/export.mjs +40 -0
  115. package/src/server/routes/extension.mjs +83 -0
  116. package/src/server/routes/field.mjs +48 -0
  117. package/src/server/routes/files.mjs +34 -0
  118. package/src/server/routes/graph.mjs +323 -0
  119. package/src/server/routes/help.mjs +184 -0
  120. package/src/server/routes/import.mjs +39 -0
  121. package/src/server/routes/instructions.mjs +29 -0
  122. package/src/server/routes/integrations.mjs +123 -0
  123. package/src/server/routes/memory.mjs +19 -1
  124. package/src/server/routes/models.mjs +388 -0
  125. package/src/server/routes/scripts.mjs +110 -0
  126. package/src/server/routes/sessions.mjs +1 -1
  127. package/src/server/routes/ssss.mjs +173 -0
  128. package/src/server/routes/system.mjs +167 -0
  129. package/src/server/routes/tasks.mjs +96 -0
  130. package/src/server/routes/tts.mjs +39 -0
  131. package/src/server/routes/update.mjs +42 -0
  132. package/src/server/routes/vault.mjs +99 -0
  133. package/frontend/src/api.ts +0 -1441
  134. package/scaffold/.agent/skills/ssss/SKILL.md +0 -235
  135. package/scaffold/.agent/skills/ssss/evals/evals.json +0 -37
  136. package/scaffold/.agent/skills/ssss/references/contract-reference.md +0 -95
  137. package/scaffold/.agent/skills/ssss/scripts/validate-schema.mjs +0 -215
  138. package/scaffold/.agent/skills/ssss/subagents/audit-ssss-contract.md +0 -22
  139. package/src/core/pattern_detector.mjs +0 -113
  140. package/src/core/promotion-pipeline.mjs +0 -54
  141. package/src/core/semantic-index.mjs +0 -276
  142. package/src/core/task_runner.mjs +0 -131
@@ -0,0 +1,22 @@
1
+ // ─── Integrations domain ──────────────────────────────────────────────────────
2
+
3
+ import { apiFetch, API_BASE } from './_base'
4
+
5
+ export async function connectClient(client: string, baseUrl: string): Promise<{ success: boolean; message: string }> {
6
+ const res = await apiFetch(`${API_BASE}/api/integrations/connect`, {
7
+ method: 'POST',
8
+ headers: { 'Content-Type': 'application/json' },
9
+ body: JSON.stringify({ client, baseUrl }),
10
+ })
11
+ if (!res.ok) {
12
+ const errorData = await res.json().catch(() => ({}))
13
+ throw new Error(errorData.error || `Connection error: ${res.status}`)
14
+ }
15
+ return res.json()
16
+ }
17
+
18
+ export async function fetchActiveIntegrations(): Promise<{ success: boolean; active: string[] }> {
19
+ const res = await apiFetch(`${API_BASE}/api/integrations/active`, { method: 'GET' })
20
+ if (!res.ok) throw new Error(`Integrations API error: ${res.status}`)
21
+ return res.json()
22
+ }
@@ -0,0 +1,384 @@
1
+ // ─── Keys & Secrets domain ────────────────────────────────────────────────────
2
+
3
+ import { apiFetch, API_BASE } from './_base'
4
+
5
+ // ─── API Key Lifecycle ────────────────────────────────────────────────────────
6
+
7
+ export interface ApiKey {
8
+ id: string
9
+ name: string
10
+ token_preview: string
11
+ scopes: string[]
12
+ expires_at: string | null
13
+ created_at: string
14
+ last_used_at: string | null
15
+ hit_count: number
16
+ revoked: boolean
17
+ }
18
+
19
+ export interface IssuedApiKey extends ApiKey {
20
+ token: string // full token — only returned on creation
21
+ }
22
+
23
+ export interface ApiKeyListResponse {
24
+ keys: ApiKey[]
25
+ available_scopes: string[]
26
+ }
27
+
28
+ export async function listApiKeys(): Promise<ApiKeyListResponse> {
29
+ const res = await apiFetch(`${API_BASE}/api/keys`)
30
+ if (!res.ok) throw new Error(`Keys API error: ${res.status}`)
31
+ const data = await res.json()
32
+ return Array.isArray(data) ? { keys: data, available_scopes: ['*'] } : data
33
+ }
34
+
35
+ export async function issueApiKey(name: string, scopes?: string[], expiresAt?: string | null): Promise<IssuedApiKey> {
36
+ const res = await apiFetch(`${API_BASE}/api/keys`, {
37
+ method: 'POST',
38
+ headers: { 'Content-Type': 'application/json' },
39
+ body: JSON.stringify({ name, scopes, expires_at: expiresAt || null }),
40
+ })
41
+ if (!res.ok) throw new Error(`Keys API error: ${res.status}`)
42
+ return res.json()
43
+ }
44
+
45
+ export async function revokeApiKey(id: string): Promise<void> {
46
+ const res = await apiFetch(`${API_BASE}/api/keys/${id}`, { method: 'DELETE' })
47
+ if (!res.ok) throw new Error(`Keys API error: ${res.status}`)
48
+ }
49
+
50
+ // ─── Secrets store (API keys / env import — not PATs) ─────────────────────────
51
+
52
+ export interface EnvSecretCandidate {
53
+ key: string
54
+ source: string
55
+ source_label: string
56
+ provider: string | null
57
+ masked: string
58
+ length: number
59
+ known: boolean
60
+ already_set?: boolean
61
+ }
62
+
63
+ export async function scanEnvSecrets(): Promise<{
64
+ candidates: EnvSecretCandidate[]
65
+ count: number
66
+ sources_scanned: string[]
67
+ }> {
68
+ const res = await apiFetch(`${API_BASE}/api/secrets/scan-env`)
69
+ if (!res.ok) throw new Error(`Env scan failed: ${res.status}`)
70
+ return res.json()
71
+ }
72
+
73
+ export async function parseEnvPaste(text: string): Promise<{
74
+ candidates: EnvSecretCandidate[]
75
+ count: number
76
+ }> {
77
+ const res = await apiFetch(`${API_BASE}/api/secrets/parse-env`, {
78
+ method: 'POST',
79
+ headers: { 'Content-Type': 'application/json' },
80
+ body: JSON.stringify({ text }),
81
+ })
82
+ if (!res.ok) {
83
+ const err = await res.json().catch(() => ({}))
84
+ throw new Error(err.error || `Parse failed: ${res.status}`)
85
+ }
86
+ return res.json()
87
+ }
88
+
89
+ export async function importEnvSecrets(opts: {
90
+ keys?: string[]
91
+ all?: boolean
92
+ pairs?: Record<string, string>
93
+ overwrite?: boolean
94
+ }): Promise<{
95
+ imported: { key: string; provider: string | null }[]
96
+ skipped: { key: string; reason: string }[]
97
+ errors: { key: string; error: string }[]
98
+ imported_count: number
99
+ skipped_count: number
100
+ }> {
101
+ const res = await apiFetch(`${API_BASE}/api/secrets/import-env`, {
102
+ method: 'POST',
103
+ headers: { 'Content-Type': 'application/json' },
104
+ body: JSON.stringify(opts),
105
+ })
106
+ if (!res.ok) {
107
+ const err = await res.json().catch(() => ({}))
108
+ throw new Error(err.error || `Import failed: ${res.status}`)
109
+ }
110
+ return res.json()
111
+ }
112
+
113
+ export interface SecretCatalogKey {
114
+ key: string
115
+ set: boolean
116
+ length: number
117
+ fingerprint: string | null
118
+ masked: string | null
119
+ scope: string
120
+ provider: string | null
121
+ provider_name?: string | null
122
+ label: string | null
123
+ repos: string[]
124
+ /** Single product repo, or null for Developer secrets */
125
+ repo?: string | null
126
+ multi_repo_error?: boolean
127
+ binding_error?: string | null
128
+ project_path: string | null
129
+ subscription_tier: string | null
130
+ monthly_cost_usd: number | null
131
+ monthly_cap_usd: number | null
132
+ api_docs_url: string | null
133
+ console_url: string | null
134
+ pricing_url: string | null
135
+ schema: { auth: string; header?: string; env_keys?: string[]; notes?: string } | null
136
+ schema_notes: string | null
137
+ auth_scheme: string | null
138
+ rotate_every_days: number | null
139
+ auto_rotate: boolean
140
+ next_rotate_due: string | null
141
+ rotation_overdue: boolean
142
+ notes: string | null
143
+ created_at: string | null
144
+ updated_at: string | null
145
+ rotated_at: string | null
146
+ usage_30d: { events: number; cost_usd: number; input_tokens: number; output_tokens: number }
147
+ tiers: { id: string; label: string; monthly_usd?: number | null }[]
148
+ }
149
+
150
+ export interface SecretCatalog {
151
+ keys: SecretCatalogKey[]
152
+ providers: unknown[]
153
+ summary: {
154
+ total_keys: number
155
+ providers_active: number
156
+ monthly_subscription_usd: number
157
+ multi_repo_violations?: number
158
+ developer_keys?: number
159
+ product_keys?: number
160
+ usage_7d: { events: number; cost_usd: number }
161
+ usage_30d: { events: number; cost_usd: number }
162
+ rotation_overdue: number
163
+ budget: Record<string, unknown>
164
+ }
165
+ by_provider: { provider: string; keys: number; cost_30d: number; monthly_cost: number }[]
166
+ store: string
167
+ }
168
+
169
+ export async function fetchSecretsCatalog(): Promise<SecretCatalog> {
170
+ const res = await apiFetch(`${API_BASE}/api/secrets`)
171
+ if (!res.ok) throw new Error(`Secrets catalog error: ${res.status}`)
172
+ return res.json()
173
+ }
174
+
175
+ export async function updateSecretMeta(key: string, patch: Record<string, unknown>): Promise<SecretCatalogKey> {
176
+ const res = await apiFetch(`${API_BASE}/api/secrets/${encodeURIComponent(key)}`, {
177
+ method: 'PATCH',
178
+ headers: { 'Content-Type': 'application/json' },
179
+ body: JSON.stringify(patch),
180
+ })
181
+ if (!res.ok) {
182
+ const err = await res.json().catch(() => ({}))
183
+ throw new Error(err.error || `Meta update failed: ${res.status}`)
184
+ }
185
+ return res.json()
186
+ }
187
+
188
+ export async function rotateSecretValue(
189
+ key: string,
190
+ value: string,
191
+ opts: { export_env?: boolean; export_all?: boolean } = {},
192
+ ): Promise<{
193
+ rotated?: boolean
194
+ next_rotate_due?: string | null
195
+ exports?: { ok: boolean; envPath?: string; count?: number; error?: string }[]
196
+ secret?: SecretCatalogKey
197
+ }> {
198
+ const res = await apiFetch(`${API_BASE}/api/secrets/${encodeURIComponent(key)}/rotate`, {
199
+ method: 'POST',
200
+ headers: { 'Content-Type': 'application/json' },
201
+ body: JSON.stringify({
202
+ value,
203
+ export_env: opts.export_env ?? true,
204
+ export_all: opts.export_all ?? true,
205
+ }),
206
+ })
207
+ if (!res.ok) {
208
+ const err = await res.json().catch(() => ({}))
209
+ throw new Error(err.error || `Rotate failed: ${res.status}`)
210
+ }
211
+ return res.json()
212
+ }
213
+
214
+ export async function deleteProviderSecret(key: string): Promise<void> {
215
+ const res = await apiFetch(`${API_BASE}/api/secrets/${encodeURIComponent(key)}`, { method: 'DELETE' })
216
+ if (!res.ok) throw new Error(`Delete failed: ${res.status}`)
217
+ }
218
+
219
+ export async function recordSecretUsage(body: {
220
+ key_ref?: string
221
+ key?: string
222
+ provider?: string
223
+ model?: string
224
+ cost_usd?: number
225
+ input_tokens?: number
226
+ output_tokens?: number
227
+ source?: string
228
+ }): Promise<unknown> {
229
+ const res = await apiFetch(`${API_BASE}/api/secrets/usage`, {
230
+ method: 'POST',
231
+ headers: { 'Content-Type': 'application/json' },
232
+ body: JSON.stringify(body),
233
+ })
234
+ if (!res.ok) throw new Error(`Usage record failed: ${res.status}`)
235
+ return res.json()
236
+ }
237
+
238
+ export async function exportEnvFromSecrets(opts: {
239
+ path?: string
240
+ all_projects?: boolean
241
+ dry_run?: boolean
242
+ include_global?: boolean
243
+ keys?: string[]
244
+ }): Promise<{
245
+ count?: number
246
+ keys?: string[]
247
+ envPath?: string
248
+ results?: { ok: boolean; name?: string; envPath?: string; count?: number; error?: string }[]
249
+ store?: string
250
+ }> {
251
+ const res = await apiFetch(`${API_BASE}/api/secrets/export-env`, {
252
+ method: 'POST',
253
+ headers: { 'Content-Type': 'application/json' },
254
+ body: JSON.stringify(opts),
255
+ })
256
+ if (!res.ok) {
257
+ const err = await res.json().catch(() => ({}))
258
+ throw new Error(err.error || `export-env failed: ${res.status}`)
259
+ }
260
+ return res.json()
261
+ }
262
+
263
+ export async function fetchRotationDue(): Promise<{ due: SecretCatalogKey[]; count: number }> {
264
+ const res = await apiFetch(`${API_BASE}/api/secrets/rotation-due`)
265
+ if (!res.ok) throw new Error(`rotation-due failed: ${res.status}`)
266
+ return res.json()
267
+ }
268
+
269
+ export async function enqueueRotationDue(): Promise<unknown> {
270
+ const res = await apiFetch(`${API_BASE}/api/secrets/rotation-due/enqueue`, { method: 'POST' })
271
+ if (!res.ok) {
272
+ // fallback: client can use CLI; route may not exist yet
273
+ const err = await res.json().catch(() => ({}))
274
+ throw new Error(err.error || `enqueue failed: ${res.status}`)
275
+ }
276
+ return res.json()
277
+ }
278
+
279
+ export async function revealSecretValue(
280
+ key: string,
281
+ stepUpToken: string,
282
+ ): Promise<{ key: string; value: string; revealed_at: string }> {
283
+ const res = await apiFetch(`${API_BASE}/api/secrets/${encodeURIComponent(key)}/reveal`, {
284
+ method: 'POST',
285
+ headers: { 'Content-Type': 'application/json' },
286
+ body: JSON.stringify({ step_up_token: stepUpToken }),
287
+ })
288
+ if (!res.ok) {
289
+ const err = await res.json().catch(() => ({}))
290
+ throw new Error(err.error || `Reveal failed: ${res.status}`)
291
+ }
292
+ return res.json()
293
+ }
294
+
295
+ // ─── WebAuthn / passkey step-up (secret reveal) ───────────────────────────────
296
+
297
+ export interface WebAuthnStatus {
298
+ enabled: boolean
299
+ has_passkeys: boolean
300
+ count: number
301
+ passkeys: { id: string; created_at: string | null; label?: string }[]
302
+ password_step_up_allowed?: boolean
303
+ }
304
+
305
+ /** Minimal JSON option types (compatible with @simplewebauthn/browser) */
306
+ export type PublicKeyCredentialCreationOptionsJSON = Record<string, unknown>
307
+ export type PublicKeyCredentialRequestOptionsJSON = Record<string, unknown>
308
+
309
+ export async function fetchWebAuthnStatus(): Promise<WebAuthnStatus> {
310
+ const res = await apiFetch(`${API_BASE}/api/webauthn/status`)
311
+ if (!res.ok) throw new Error(`WebAuthn status failed: ${res.status}`)
312
+ return res.json()
313
+ }
314
+
315
+ export async function webauthnRegisterOptions(): Promise<PublicKeyCredentialCreationOptionsJSON> {
316
+ const res = await apiFetch(`${API_BASE}/api/webauthn/register/options`, { method: 'POST' })
317
+ if (!res.ok) {
318
+ const err = await res.json().catch(() => ({}))
319
+ throw new Error(err.error || `register options failed: ${res.status}`)
320
+ }
321
+ return res.json()
322
+ }
323
+
324
+ export async function webauthnRegisterVerify(
325
+ response: unknown,
326
+ label?: string,
327
+ ): Promise<{ verified: boolean; passkeys?: WebAuthnStatus['passkeys'] }> {
328
+ const res = await apiFetch(`${API_BASE}/api/webauthn/register/verify`, {
329
+ method: 'POST',
330
+ headers: { 'Content-Type': 'application/json' },
331
+ body: JSON.stringify({ response, label }),
332
+ })
333
+ if (!res.ok) {
334
+ const err = await res.json().catch(() => ({}))
335
+ throw new Error(err.error || `register verify failed: ${res.status}`)
336
+ }
337
+ return res.json()
338
+ }
339
+
340
+ export async function webauthnAssertOptions(): Promise<PublicKeyCredentialRequestOptionsJSON> {
341
+ const res = await apiFetch(`${API_BASE}/api/webauthn/assert/options`, { method: 'POST' })
342
+ if (!res.ok) {
343
+ const err = await res.json().catch(() => ({}))
344
+ throw new Error(err.error || `assert options failed: ${res.status}`)
345
+ }
346
+ return res.json()
347
+ }
348
+
349
+ export async function webauthnAssertVerify(
350
+ response: unknown,
351
+ ): Promise<{ verified: boolean; step_up_token: string; expires_in: number }> {
352
+ const res = await apiFetch(`${API_BASE}/api/webauthn/assert/verify`, {
353
+ method: 'POST',
354
+ headers: { 'Content-Type': 'application/json' },
355
+ body: JSON.stringify({ response, purpose: 'secrets:reveal', ttl_seconds: 60 }),
356
+ })
357
+ if (!res.ok) {
358
+ const err = await res.json().catch(() => ({}))
359
+ throw new Error(err.error || `assert verify failed: ${res.status}`)
360
+ }
361
+ return res.json()
362
+ }
363
+
364
+ export async function webauthnPasswordStepUp(
365
+ password: string,
366
+ ): Promise<{ verified: boolean; step_up_token: string; expires_in: number }> {
367
+ const res = await apiFetch(`${API_BASE}/api/webauthn/step-up/password`, {
368
+ method: 'POST',
369
+ headers: { 'Content-Type': 'application/json' },
370
+ body: JSON.stringify({ password, purpose: 'secrets:reveal', ttl_seconds: 60 }),
371
+ })
372
+ if (!res.ok) {
373
+ const err = await res.json().catch(() => ({}))
374
+ throw new Error(err.error || `password step-up failed: ${res.status}`)
375
+ }
376
+ return res.json()
377
+ }
378
+
379
+ export async function deletePasskey(id: string): Promise<void> {
380
+ const res = await apiFetch(`${API_BASE}/api/webauthn/credentials/${encodeURIComponent(id)}`, {
381
+ method: 'DELETE',
382
+ })
383
+ if (!res.ok) throw new Error(`Delete passkey failed: ${res.status}`)
384
+ }
@@ -0,0 +1,234 @@
1
+ // ─── Memory domain ────────────────────────────────────────────────────────────
2
+
3
+ import { apiFetch, API_BASE } from './_base'
4
+ import type { MemoryNode, Conflict } from './_base'
5
+
6
+ export async function listMemory(brainId?: string, category?: string, status?: string): Promise<MemoryNode[]> {
7
+ const params = new URLSearchParams()
8
+ if (category) params.set('category', category)
9
+ if (status) params.set('status', status)
10
+
11
+ if (!brainId) {
12
+ const res = await apiFetch(`${API_BASE}/api/memory?${params}`)
13
+ if (!res.ok) throw new Error(`Memory API error: ${res.status}`)
14
+ const data = await res.json()
15
+ return Array.isArray(data) ? data : (data.nodes || [])
16
+ }
17
+
18
+ const ids = brainId.split(',')
19
+ const fetchPromises = ids.map(async (id) => {
20
+ const url = id === 'global'
21
+ ? `${API_BASE}/api/memory?${params}`
22
+ : `${API_BASE}/api/brains/${id}/nodes?${params}`
23
+ try {
24
+ const res = await apiFetch(url)
25
+ if (!res.ok) return []
26
+ const data = await res.json()
27
+ return Array.isArray(data) ? data : (data.nodes || [])
28
+ } catch {
29
+ return []
30
+ }
31
+ })
32
+
33
+ const results = await Promise.all(fetchPromises)
34
+ const merged: MemoryNode[] = []
35
+ const seenSlugs = new Set<string>()
36
+
37
+ // Project brains override Global brain nodes on slug collisions
38
+ const sortedResults = results.map((nodes, index) => ({ nodes, id: ids[index] }))
39
+ .sort((a, b) => {
40
+ if (a.id === 'global') return 1
41
+ if (b.id === 'global') return -1
42
+ return 0
43
+ })
44
+
45
+ for (const { nodes } of sortedResults) {
46
+ for (const node of nodes) {
47
+ if (!seenSlugs.has(node.slug)) {
48
+ seenSlugs.add(node.slug)
49
+ merged.push(node)
50
+ }
51
+ }
52
+ }
53
+
54
+ return merged
55
+ }
56
+
57
+ export async function searchMemory(query: string, category?: string): Promise<MemoryNode[]> {
58
+ const params = new URLSearchParams({ q: query })
59
+ if (category) params.set('category', category)
60
+ const res = await apiFetch(`${API_BASE}/api/memory?${params}`)
61
+ if (!res.ok) throw new Error(`Memory API error: ${res.status}`)
62
+ const data = await res.json()
63
+ return Array.isArray(data) ? data : (data.nodes || [])
64
+ }
65
+
66
+ export async function readMemory(slug: string, brainId?: string): Promise<MemoryNode | null> {
67
+ const params = new URLSearchParams()
68
+ if (brainId) params.set('brain', brainId)
69
+ const qs = params.toString()
70
+ const res = await apiFetch(`${API_BASE}/api/memory/${encodeURIComponent(slug)}${qs ? `?${qs}` : ''}`)
71
+ if (res.status === 404) return null
72
+ if (!res.ok) throw new Error(`Memory API error: ${res.status}`)
73
+ return res.json()
74
+ }
75
+
76
+ export async function saveMemory(slug: string, node: Partial<MemoryNode>, brainId?: string): Promise<MemoryNode> {
77
+ const params = new URLSearchParams()
78
+ if (brainId) params.set('brain', brainId)
79
+ const qs = params.toString()
80
+ const res = await apiFetch(`${API_BASE}/api/memory/${encodeURIComponent(slug)}${qs ? `?${qs}` : ''}`, {
81
+ method: "PATCH",
82
+ headers: { "Content-Type": "application/json" },
83
+ body: JSON.stringify(node),
84
+ })
85
+ if (!res.ok) throw new Error(`Failed to save memory: ${res.status}`)
86
+ return res.json()
87
+ }
88
+
89
+ export async function createMemory(node: Partial<MemoryNode> & { slug: string }, brainId?: string): Promise<MemoryNode> {
90
+ const params = new URLSearchParams()
91
+ if (brainId) params.set('brain', brainId)
92
+ const qs = params.toString()
93
+ const res = await apiFetch(`${API_BASE}/api/memory${qs ? `?${qs}` : ''}`, {
94
+ method: "POST",
95
+ headers: { "Content-Type": "application/json" },
96
+ body: JSON.stringify(node),
97
+ })
98
+ if (!res.ok) throw new Error(`Failed to create memory: ${res.status}`)
99
+ return res.json()
100
+ }
101
+
102
+ export async function deleteMemory(slug: string, brainId?: string): Promise<void> {
103
+ const params = new URLSearchParams()
104
+ if (brainId) params.set('brain', brainId)
105
+ const qs = params.toString()
106
+ const res = await apiFetch(`${API_BASE}/api/memory/${encodeURIComponent(slug)}${qs ? `?${qs}` : ''}`, {
107
+ method: "DELETE",
108
+ })
109
+ if (!res.ok) throw new Error(`Failed to delete memory: ${res.status}`)
110
+ }
111
+
112
+ export async function fetchMemoryStats(): Promise<{ total: number; byCategory: Record<string, number>; byPriority: Record<string, number>; byStatus: Record<string, number> }> {
113
+ const res = await apiFetch(`${API_BASE}/api/memory/stats`)
114
+ if (!res.ok) throw new Error(`Memory stats error: ${res.status}`)
115
+ return res.json()
116
+ }
117
+
118
+ export async function fetchGraph(): Promise<{ nodes: MemoryNode[]; routes: unknown[] }> {
119
+ const res = await apiFetch(`${API_BASE}/api/graph`)
120
+ if (!res.ok) throw new Error(`Graph API error: ${res.status}`)
121
+ return res.json()
122
+ }
123
+
124
+ export async function fetchConflicts(): Promise<{ conflicts: Conflict[] }> {
125
+ const res = await apiFetch(`${API_BASE}/api/conflicts`)
126
+ if (!res.ok) throw new Error(`Conflicts API error: ${res.status}`)
127
+ return res.json()
128
+ }
129
+
130
+ export async function resolveConflict(
131
+ conflictId: string,
132
+ action: "keep" | "supersede",
133
+ winnerSlug: string
134
+ ): Promise<{ success: boolean; conflict_id: string }> {
135
+ const res = await apiFetch(`${API_BASE}/api/conflicts/resolve`, {
136
+ method: "POST",
137
+ headers: { "Content-Type": "application/json" },
138
+ body: JSON.stringify({ conflict_id: conflictId, action, winner_slug: winnerSlug }),
139
+ })
140
+ if (!res.ok) {
141
+ const errorData = await res.json().catch(() => ({}))
142
+ throw new Error(errorData.error || `Conflict resolution API error: ${res.status}`)
143
+ }
144
+ return res.json()
145
+ }
146
+
147
+ export async function fetchVaultStatus(): Promise<{ totalNodes: number; categories: Record<string, number>; embeddings: number; lastCompiled: string }> {
148
+ const res = await apiFetch(`${API_BASE}/api/vault/status`)
149
+ if (!res.ok) throw new Error(`Vault status error: ${res.status}`)
150
+ return res.json()
151
+ }
152
+
153
+ // ─── OpenWiki ─────────────────────────────────────────────────────────────────
154
+
155
+ /**
156
+ * OpenWiki nodes for the selected brain(s) only.
157
+ * Scoped by x-total-recall-brain header (apiFetch) and explicit brain query.
158
+ */
159
+ export async function fetchOpenWikiNodes(
160
+ brainId?: string | null,
161
+ ): Promise<MemoryNode[]> {
162
+ const active =
163
+ brainId ||
164
+ (typeof localStorage !== 'undefined' ? localStorage.getItem('total-recall-active-brain') : null) ||
165
+ 'global'
166
+
167
+ // Reuse listMemory brain routing (global vs project:name), then keep openwiki-tagged only.
168
+ // Server also filters tag=openwiki when we hit /api/memory?tag=openwiki.
169
+ const params = new URLSearchParams({ tag: 'openwiki', limit: '500' })
170
+ const ids = active.split(',').map((s) => s.trim()).filter(Boolean)
171
+
172
+ if (ids.length === 0 || (ids.length === 1 && ids[0] === 'global')) {
173
+ const res = await apiFetch(`${API_BASE}/api/memory?${params}`)
174
+ if (!res.ok) throw new Error(`OpenWiki nodes error: ${res.status}`)
175
+ const data = await res.json()
176
+ const nodes = Array.isArray(data) ? data : (data.nodes ?? [])
177
+ return filterOpenWikiNodes(nodes)
178
+ }
179
+
180
+ // Multi / project brains — fetch each scope separately (same merge rules as Memory)
181
+ const fetchPromises = ids.map(async (id) => {
182
+ try {
183
+ if (id === 'global') {
184
+ const res = await apiFetch(`${API_BASE}/api/memory?${params}`)
185
+ if (!res.ok) return []
186
+ const data = await res.json()
187
+ return Array.isArray(data) ? data : (data.nodes ?? [])
188
+ }
189
+ // Project brain: list all nodes then filter openwiki (brains/:id/nodes may not support tag)
190
+ const res = await apiFetch(`${API_BASE}/api/brains/${encodeURIComponent(id)}/nodes`)
191
+ if (!res.ok) {
192
+ // Fallback: memory list with brain header already set by apiFetch
193
+ const res2 = await apiFetch(`${API_BASE}/api/memory?${params}&brain=${encodeURIComponent(id)}`)
194
+ if (!res2.ok) return []
195
+ const data2 = await res2.json()
196
+ return Array.isArray(data2) ? data2 : (data2.nodes ?? [])
197
+ }
198
+ const data = await res.json()
199
+ const nodes = Array.isArray(data) ? data : (data.nodes ?? [])
200
+ return filterOpenWikiNodes(nodes)
201
+ } catch {
202
+ return []
203
+ }
204
+ })
205
+
206
+ const results = await Promise.all(fetchPromises)
207
+ const merged: MemoryNode[] = []
208
+ const seen = new Set<string>()
209
+ // Project overrides global on slug collision
210
+ const ordered = results
211
+ .map((nodes, i) => ({ nodes, id: ids[i] }))
212
+ .sort((a, b) => {
213
+ if (a.id === 'global') return 1
214
+ if (b.id === 'global') return -1
215
+ return 0
216
+ })
217
+ for (const { nodes } of ordered) {
218
+ for (const n of filterOpenWikiNodes(nodes)) {
219
+ if (!seen.has(n.slug)) {
220
+ seen.add(n.slug)
221
+ merged.push(n)
222
+ }
223
+ }
224
+ }
225
+ return merged
226
+ }
227
+
228
+ function filterOpenWikiNodes(nodes: MemoryNode[]): MemoryNode[] {
229
+ return nodes.filter((n) => {
230
+ const tags = (n.tags || []).map((t) => String(t).toLowerCase())
231
+ const slug = String(n.slug || '').toLowerCase()
232
+ return tags.includes('openwiki') || slug.startsWith('openwiki-')
233
+ })
234
+ }
@@ -0,0 +1,36 @@
1
+ // ─── Models domain ────────────────────────────────────────────────────────────
2
+
3
+ import { apiFetch, API_BASE } from './_base'
4
+
5
+ export interface GeminiModelInfo {
6
+ id: string
7
+ displayName: string
8
+ }
9
+
10
+ export async function fetchGeminiModels(): Promise<GeminiModelInfo[]> {
11
+ const res = await apiFetch(`${API_BASE}/api/gemini-models?t=${Date.now()}`)
12
+ if (!res.ok) throw new Error(`Gemini models API error: ${res.status}`)
13
+ const data = await res.json()
14
+ return data.models ?? []
15
+ }
16
+
17
+ export async function fetchClaudeModels(): Promise<GeminiModelInfo[]> {
18
+ const res = await apiFetch(`${API_BASE}/api/claude-models?t=${Date.now()}`)
19
+ if (!res.ok) throw new Error(`Claude models API error: ${res.status}`)
20
+ const data = await res.json()
21
+ return data.models ?? []
22
+ }
23
+
24
+ export async function fetchOpenaiModels(): Promise<GeminiModelInfo[]> {
25
+ const res = await apiFetch(`${API_BASE}/api/openai-models?t=${Date.now()}`)
26
+ if (!res.ok) throw new Error(`OpenAI models API error: ${res.status}`)
27
+ const data = await res.json()
28
+ return data.models ?? []
29
+ }
30
+
31
+ export async function fetchOpenRouterModels(): Promise<GeminiModelInfo[]> {
32
+ const res = await apiFetch(`${API_BASE}/api/openrouter-models?t=${Date.now()}`)
33
+ if (!res.ok) throw new Error(`OpenRouter models API error: ${res.status}`)
34
+ const data = await res.json()
35
+ return data.models ?? []
36
+ }