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.
- package/frontend/dist/assets/index-D9-jAOr9.js +502 -0
- package/frontend/dist/assets/index-L0eEIoBf.css +1 -0
- package/frontend/dist/brand/total-recall-badge.jpg +0 -0
- package/frontend/dist/brand/total-recall-icon.svg +6 -0
- package/frontend/dist/brand/total-recall-lockup.png +0 -0
- package/frontend/dist/brand/total-recall-mark.svg +9 -0
- package/frontend/dist/favicon.svg +6 -0
- package/frontend/dist/icons.svg +24 -0
- package/frontend/dist/index.html +27 -0
- package/frontend/dist/manifest.json +25 -0
- package/frontend/dist/sw.js +272 -0
- package/frontend/index.html +7 -2
- package/frontend/package.json +2 -0
- package/frontend/src/App.spec.tsx +82 -0
- package/frontend/src/api/_base.ts +35 -0
- package/frontend/src/api/api.spec.ts +297 -0
- package/frontend/src/api/auth.ts +73 -0
- package/frontend/src/api/chat.ts +93 -0
- package/frontend/src/api/config.ts +35 -0
- package/frontend/src/api/docs.ts +146 -0
- package/frontend/src/api/extension.ts +9 -0
- package/frontend/src/api/index.ts +20 -0
- package/frontend/src/api/integrations.ts +22 -0
- package/frontend/src/api/keys.ts +384 -0
- package/frontend/src/api/memory.ts +234 -0
- package/frontend/src/api/models.ts +36 -0
- package/frontend/src/api/research.ts +43 -0
- package/frontend/src/api/sandbox.ts +19 -0
- package/frontend/src/api/sessions.ts +27 -0
- package/frontend/src/api/skills.ts +127 -0
- package/frontend/src/api/system.ts +165 -0
- package/frontend/src/api/update.ts +21 -0
- package/frontend/src/components/BrainSelector.spec.tsx +72 -0
- package/frontend/src/components/DaemonLogsTab.spec.tsx +21 -0
- package/frontend/src/components/DocumentEditorModal.spec.tsx +80 -0
- package/frontend/src/components/DocumentTable.spec.tsx +45 -0
- package/frontend/src/components/Graph3D.spec.tsx +75 -0
- package/frontend/src/components/MarkdownUtils.spec.tsx +82 -0
- package/frontend/src/components/MobilePairing.spec.tsx +24 -0
- package/frontend/src/components/MobilePairing.tsx +37 -0
- package/frontend/src/components/ResearchAgendaTab.spec.tsx +127 -0
- package/frontend/src/components/TaskQueueTab.spec.tsx +70 -0
- package/frontend/src/components/TaskQueueTab.tsx +9 -1
- package/frontend/src/components/UsageChart.spec.tsx +89 -0
- package/frontend/src/components/VoiceInput.spec.tsx +38 -0
- package/frontend/src/components/VoiceInput.tsx +68 -0
- package/frontend/src/index.css +30 -0
- package/frontend/src/main.tsx +13 -0
- package/frontend/src/pages/ApiKeysPage.spec.tsx +32 -0
- package/frontend/src/pages/AutomationsPage.spec.tsx +60 -0
- package/frontend/src/pages/ChatPage.spec.tsx +37 -0
- package/frontend/src/pages/ChatPage.tsx +14 -0
- package/frontend/src/pages/CollabPage.spec.tsx +64 -0
- package/frontend/src/pages/DesignDocsPage.spec.tsx +43 -0
- package/frontend/src/pages/FilesPage.spec.tsx +46 -0
- package/frontend/src/pages/GraphPage.spec.tsx +24 -0
- package/frontend/src/pages/HealthPage.spec.tsx +31 -0
- package/frontend/src/pages/HelpPage.spec.tsx +63 -0
- package/frontend/src/pages/InboxPage.spec.tsx +55 -0
- package/frontend/src/pages/InstructionsPage.spec.tsx +52 -0
- package/frontend/src/pages/InstructionsPage.tsx +1 -1
- package/frontend/src/pages/IntegrationsPage.spec.tsx +55 -0
- package/frontend/src/pages/LoginPage.spec.tsx +73 -0
- package/frontend/src/pages/LoginPage.tsx +20 -0
- package/frontend/src/pages/MemoryPage.spec.tsx +51 -0
- package/frontend/src/pages/ModelsPage.spec.tsx +64 -0
- package/frontend/src/pages/ModelsPage.tsx +18 -11
- package/frontend/src/pages/OkfPage.spec.tsx +46 -0
- package/frontend/src/pages/OnboardingPage.spec.tsx +58 -0
- package/frontend/src/pages/OpenWikiPage.spec.tsx +32 -0
- package/frontend/src/pages/SandboxPage.spec.tsx +33 -0
- package/frontend/src/pages/SettingsPage.spec.tsx +33 -0
- package/frontend/src/pages/SettingsPage.tsx +6 -1
- package/frontend/src/pages/SkillsPage.spec.tsx +20 -0
- package/frontend/src/pages/TasksPage.spec.tsx +17 -0
- package/frontend/src/pages/UsagePage.spec.tsx +35 -0
- package/frontend/src/pages/VaultPage.spec.tsx +26 -0
- package/frontend/src/setupTests.ts +23 -0
- package/frontend/vite.config.ts +6 -0
- package/package.json +16 -4
- package/scaffold/.agent/skills/total-recall/SKILL.md +6 -2
- package/scaffold/.agent/skills/total-recall/skills-registry/index.yaml +2575 -0
- package/src/cli/connect.mjs +7 -1
- package/src/cli/deploy.mjs +8 -8
- package/src/cli/init.mjs +14 -4
- package/src/cli/recall.mjs +3 -1
- package/src/cli/remember.mjs +2 -1
- package/src/cli/restore.mjs +2 -2
- package/src/cli/upgrade.mjs +2 -2
- package/src/core/clarity-rewriter.mjs +2 -2
- package/src/core/crons.mjs +23 -55
- package/src/core/crypto.mjs +4 -8
- package/src/core/daemon-control.mjs +5 -0
- package/src/core/embeddings.mjs +207 -374
- package/src/core/fact-seeker.mjs +114 -0
- package/src/core/github-sync.mjs +319 -0
- package/src/core/obsidian-sync.mjs +129 -0
- package/src/core/project-brain.mjs +2 -1
- package/src/core/remote-vault-sync.mjs +6 -6
- package/src/core/runtime.mjs +1 -1
- package/src/core/skills-registry.mjs +11 -0
- package/src/core/snapshot.mjs +1 -1
- package/src/core/vault-watcher.mjs +6 -3
- package/src/server/api.mjs +1 -1
- package/src/server/rest.mjs +50 -2163
- package/src/server/route-manifest.json +57 -5
- package/src/server/routes/brains.mjs +212 -0
- package/src/server/routes/capture.mjs +33 -0
- package/src/server/routes/collab.mjs +1 -1
- package/src/server/routes/config.mjs +206 -0
- package/src/server/routes/context.mjs +68 -0
- package/src/server/routes/dashboard.mjs +53 -0
- package/src/server/routes/dream.mjs +30 -0
- package/src/server/routes/export.mjs +40 -0
- package/src/server/routes/extension.mjs +83 -0
- package/src/server/routes/field.mjs +48 -0
- package/src/server/routes/files.mjs +34 -0
- package/src/server/routes/graph.mjs +323 -0
- package/src/server/routes/help.mjs +184 -0
- package/src/server/routes/import.mjs +39 -0
- package/src/server/routes/instructions.mjs +29 -0
- package/src/server/routes/integrations.mjs +123 -0
- package/src/server/routes/memory.mjs +19 -1
- package/src/server/routes/models.mjs +388 -0
- package/src/server/routes/scripts.mjs +110 -0
- package/src/server/routes/sessions.mjs +1 -1
- package/src/server/routes/ssss.mjs +173 -0
- package/src/server/routes/system.mjs +167 -0
- package/src/server/routes/tasks.mjs +96 -0
- package/src/server/routes/tts.mjs +39 -0
- package/src/server/routes/update.mjs +42 -0
- package/src/server/routes/vault.mjs +99 -0
- package/frontend/src/api.ts +0 -1441
- package/scaffold/.agent/skills/ssss/SKILL.md +0 -235
- package/scaffold/.agent/skills/ssss/evals/evals.json +0 -37
- package/scaffold/.agent/skills/ssss/references/contract-reference.md +0 -95
- package/scaffold/.agent/skills/ssss/scripts/validate-schema.mjs +0 -215
- package/scaffold/.agent/skills/ssss/subagents/audit-ssss-contract.md +0 -22
- package/src/core/pattern_detector.mjs +0 -113
- package/src/core/promotion-pipeline.mjs +0 -54
- package/src/core/semantic-index.mjs +0 -276
- 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
|
+
}
|