total-recall-brain 3.13.0 → 3.13.2
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/README.md +91 -108
- package/bin/antigravity.mjs +81 -52
- package/bin/total-recall.mjs +32 -57
- package/extension/background.js +1 -1
- package/extension/options/options.js +9 -3
- package/extension/sidepanel/sidepanel.js +0 -2
- package/frontend/index.html +4 -3
- package/frontend/package.json +1 -0
- package/frontend/src/App.tsx +109 -152
- package/frontend/src/api.ts +605 -23
- package/frontend/src/components/BrainSelector.tsx +211 -199
- package/frontend/src/components/DocumentEditorModal.tsx +57 -0
- package/frontend/src/components/DocumentTable.tsx +47 -0
- package/frontend/src/components/Graph3D.tsx +120 -71
- package/frontend/src/components/UsageChart.tsx +3 -3
- package/frontend/src/components/brand/BrandMark.tsx +95 -0
- package/frontend/src/index.css +286 -86
- package/frontend/src/pages/ApiKeysPage.tsx +1878 -215
- package/frontend/src/pages/ChatPage.tsx +103 -45
- package/frontend/src/pages/DesignDocsPage.tsx +5 -5
- package/frontend/src/pages/FilesPage.tsx +10 -6
- package/frontend/src/pages/GraphPage.tsx +5 -3
- package/frontend/src/pages/HelpPage.tsx +2 -2
- package/frontend/src/pages/InboxPage.tsx +82 -0
- package/frontend/src/pages/InstructionsPage.tsx +1 -1
- package/frontend/src/pages/IntegrationsPage.tsx +21 -10
- package/frontend/src/pages/LoginPage.tsx +95 -77
- package/frontend/src/pages/MemoryPage.tsx +31 -8
- package/frontend/src/pages/OnboardingPage.tsx +690 -0
- package/frontend/src/pages/OpenWikiPage.tsx +412 -323
- package/frontend/src/pages/SettingsPage.tsx +825 -922
- package/frontend/src/pages/SkillsPage.tsx +272 -147
- package/frontend/src/pages/UsagePage.tsx +23 -31
- package/frontend/src/pages/VaultPage.tsx +223 -0
- package/frontend/src/types.ts +13 -0
- package/package.json +4 -4
- package/scaffold/.agent/skills/total-recall/SKILL.md +43 -4
- package/scaffold/.agent/skills/total-recall/modules/README.md +10 -0
- package/scaffold/.agent/skills/total-recall/modules/agents/README.md +5 -0
- package/scaffold/.agent/skills/total-recall/{skills/tr-cli-agents → modules/agents}/agents.yml +8 -0
- package/scaffold/.agent/skills/total-recall/modules/skill-deploy/README.md +9 -0
- package/scaffold/.agent/skills/total-recall/openwiki/README.md +10 -0
- package/scaffold/.agent/skills/total-recall/openwiki/architecture.md +5 -0
- package/scaffold/.agent/skills/total-recall/openwiki/ide.md +10 -0
- package/scaffold/.agent/skills/total-recall/openwiki/memory.md +11 -0
- package/scaffold/.agent/skills/total-recall/openwiki/secrets.md +7 -0
- package/scaffold/.agent/skills/total-recall/openwiki/skills.md +7 -0
- package/src/cli/brain.mjs +63 -5
- package/src/cli/connect.mjs +11 -19
- package/src/cli/daemon.mjs +5 -4
- package/src/cli/deploy-ui.mjs +8 -3
- package/src/cli/deploy.mjs +84 -27
- package/src/cli/ingest/index.mjs +1 -1
- package/src/cli/init.mjs +63 -34
- package/src/cli/remember.mjs +3 -3
- package/src/cli/secret.mjs +812 -0
- package/src/cli/setup.mjs +34 -7
- package/src/cli/share.mjs +2 -2
- package/src/cli/skill.mjs +396 -35
- package/src/cli/sync.mjs +4 -4
- package/src/cli/task.mjs +287 -0
- package/src/core/clarity-rewriter.mjs +5 -5
- package/src/core/config.mjs +44 -2
- package/src/core/conflict-detector.mjs +5 -5
- package/src/core/crons.mjs +54 -0
- package/src/core/daemon-loop.mjs +87 -235
- package/src/core/dream.mjs +170 -10
- package/src/core/env-import.mjs +456 -0
- package/src/core/fact-seeker.mjs +14 -14
- package/src/core/import-rules.mjs +20 -7
- package/src/core/inference-engine.mjs +3 -3
- package/src/core/migrate.mjs +14 -8
- package/src/core/okf-adapter.mjs +2 -2
- package/src/core/operation-validator.mjs +39 -331
- package/src/core/optimizer.mjs +7 -0
- package/src/core/post-mortem.mjs +2 -2
- package/src/core/project-brain.mjs +349 -0
- package/src/core/provider-catalog.mjs +296 -0
- package/src/core/quick-capture.mjs +1 -1
- package/src/core/remote-vault-sync.mjs +194 -0
- package/src/core/research-queue.mjs +7 -11
- package/src/core/runtime.mjs +33 -9
- package/src/core/scheduler.mjs +59 -5
- package/src/core/schema.mjs +14 -0
- package/src/core/search.mjs +1 -1
- package/src/core/secrets-env-export.mjs +323 -0
- package/src/core/secrets-rotate.mjs +245 -0
- package/src/core/secrets-store.mjs +645 -0
- package/src/core/semantic-index.mjs +3 -3
- package/src/core/skills-registry.mjs +1062 -0
- package/src/core/source-adapters.mjs +4 -1
- package/src/core/ssss-host-extension.mjs +94 -0
- package/src/core/ssss-kernel-bridge.mjs +530 -0
- package/src/core/surface.mjs +14 -0
- package/src/core/task-envelope.mjs +298 -0
- package/src/core/task-executors.mjs +600 -0
- package/src/core/validated-write.mjs +124 -56
- package/src/core/vault.mjs +21 -34
- package/src/core/webauthn-store.mjs +272 -0
- package/src/server/api.mjs +26 -16
- package/src/server/auth.mjs +51 -0
- package/src/server/index.mjs +3 -3
- package/src/server/keys.mjs +30 -4
- package/src/server/rest.mjs +198 -25
- package/src/server/route-manifest.json +156 -0
- package/src/server/routes/_shared.mjs +40 -1
- package/src/server/routes/docs.mjs +260 -0
- package/src/server/routes/memory.mjs +145 -32
- package/src/server/routes/research.mjs +30 -0
- package/src/server/routes/secrets.mjs +470 -0
- package/src/server/routes/share.mjs +2 -2
- package/src/server/routes/skills.mjs +452 -54
- package/src/server/routes/sync.mjs +48 -0
- package/src/server/routes/webauthn.mjs +184 -0
- package/src/server/update-route-manifest.mjs +39 -0
- package/templates/onboarding-interview.md +32 -22
- package/templates/openwiki/README.md +10 -0
- package/templates/openwiki/architecture.md +5 -0
- package/templates/openwiki/ide.md +10 -0
- package/templates/openwiki/memory.md +11 -0
- package/templates/openwiki/secrets.md +7 -0
- package/templates/openwiki/skills.md +7 -0
- package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/SKILL.md +0 -121
- package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/references/antigravity.txt +0 -20
- package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/references/claude.txt +0 -58
- package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/references/codex.txt +0 -81
- package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/references/gemini.txt +0 -44
- package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/scripts/.gitkeep +0 -0
- package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/subagents/.gitkeep +0 -0
- package/scaffold/.agent/skills/total-recall/skills/tr-research/SKILL.md +0 -338
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/SKILL.md +0 -282
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/assets/skill-template/SKILL.md +0 -20
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/evals/evals.json +0 -20
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/hooks/on-skill-add.sh +0 -14
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/hooks/on-skill-delete.sh +0 -14
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/learnings/.gitkeep +0 -0
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/references/.gitkeep +0 -0
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/.gitkeep +0 -0
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/create-skill.sh +0 -65
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/enforce-skill-optimization.mjs +0 -135
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/skill-watcher.mjs +0 -64
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/subagents/audit-all-skills.md +0 -62
- package/scaffold/.agent/skills/total-recall/skills/tr-ssss/SKILL.md +0 -298
- package/scaffold/.agent/skills/total-recall/skills/tr-ssss/evals/evals.json +0 -17
- package/scaffold/.agent/skills/total-recall/skills/tr-ssss/references/CHANGELOG.md +0 -124
- package/scaffold/.agent/skills/total-recall/skills/tr-ssss/references/ssss-spec.md +0 -1325
- package/scaffold/.agent/skills/total-recall/skills/tr-ssss/scripts/validate-schema.mjs +0 -232
- package/scaffold/.agent/skills/total-recall/skills/tr-ssss/subagents/audit-ssss-node.md +0 -9
- /package/scaffold/.agent/skills/total-recall/{skills/tr-cli-agents/evals → modules/skill-deploy/scripts}/.gitkeep +0 -0
- /package/scaffold/.agent/skills/total-recall/{skills/tr-skill → modules/skill-deploy}/scripts/find-skills.mjs +0 -0
- /package/scaffold/.agent/skills/total-recall/{skills/tr-skill → modules/skill-deploy}/scripts/install-skill.mjs +0 -0
- /package/scaffold/.agent/skills/total-recall/{skills/tr-skill → modules/skill-deploy}/scripts/scan-skill.mjs +0 -0
package/frontend/src/api.ts
CHANGED
|
@@ -20,7 +20,13 @@ export function registerUnauthedCallback(cb: UnauthedCallback) { onUnauthed = cb
|
|
|
20
20
|
export function clearUnauthedCallback() { onUnauthed = null }
|
|
21
21
|
|
|
22
22
|
async function apiFetch(url: string, options: RequestInit = {}): Promise<Response> {
|
|
23
|
-
const
|
|
23
|
+
const activeBrain = localStorage.getItem('total-recall-active-brain');
|
|
24
|
+
const headers = new Headers(options.headers || {});
|
|
25
|
+
if (activeBrain) {
|
|
26
|
+
headers.set('x-total-recall-brain', activeBrain);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const res = await fetch(url, { credentials: 'include', ...options, headers })
|
|
24
30
|
if (res.status === 401) {
|
|
25
31
|
onUnauthed?.()
|
|
26
32
|
}
|
|
@@ -261,8 +267,11 @@ export async function searchMemory(query: string, category?: string): Promise<Me
|
|
|
261
267
|
return Array.isArray(data) ? data : (data.nodes || [])
|
|
262
268
|
}
|
|
263
269
|
|
|
264
|
-
export async function readMemory(slug: string): Promise<MemoryNode | null> {
|
|
265
|
-
const
|
|
270
|
+
export async function readMemory(slug: string, brainId?: string): Promise<MemoryNode | null> {
|
|
271
|
+
const params = new URLSearchParams()
|
|
272
|
+
if (brainId) params.set('brain', brainId)
|
|
273
|
+
const qs = params.toString()
|
|
274
|
+
const res = await apiFetch(`${API_BASE}/api/memory/${encodeURIComponent(slug)}${qs ? `?${qs}` : ''}`)
|
|
266
275
|
if (res.status === 404) return null
|
|
267
276
|
if (!res.ok) throw new Error(`Memory API error: ${res.status}`)
|
|
268
277
|
return res.json()
|
|
@@ -310,29 +319,75 @@ export async function listFiles(): Promise<import('./types').FileNode[]> {
|
|
|
310
319
|
return res.json()
|
|
311
320
|
}
|
|
312
321
|
|
|
313
|
-
export
|
|
322
|
+
export interface ProjectSkill {
|
|
323
|
+
id: string
|
|
324
|
+
name: string
|
|
325
|
+
repo: string
|
|
326
|
+
project_path?: string
|
|
327
|
+
source?: string
|
|
328
|
+
skill_dir?: string
|
|
329
|
+
has_skill_md?: boolean
|
|
330
|
+
size?: number
|
|
331
|
+
modified?: string
|
|
332
|
+
isDirectory?: boolean
|
|
333
|
+
subSkills?: string[]
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export interface SkillsCatalog {
|
|
337
|
+
repos: {
|
|
338
|
+
repo: string
|
|
339
|
+
path: string | null
|
|
340
|
+
skills: ProjectSkill[]
|
|
341
|
+
ok?: boolean
|
|
342
|
+
count?: number
|
|
343
|
+
error?: string
|
|
344
|
+
}[]
|
|
345
|
+
skills: ProjectSkill[]
|
|
346
|
+
total: number
|
|
347
|
+
excluded_note?: string
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export async function listSkills(): Promise<SkillsCatalog | ProjectSkill[]> {
|
|
314
351
|
const res = await apiFetch(API_BASE + '/api/skills')
|
|
315
352
|
if (!res.ok) throw new Error(`Skills API error: ${res.status}`)
|
|
316
353
|
return res.json()
|
|
317
354
|
}
|
|
318
355
|
|
|
319
|
-
export async function fetchSkill(
|
|
320
|
-
|
|
356
|
+
export async function fetchSkill(
|
|
357
|
+
name: string,
|
|
358
|
+
repo?: string,
|
|
359
|
+
): Promise<{ name: string; content: string; repo?: string }> {
|
|
360
|
+
const q = repo ? `?repo=${encodeURIComponent(repo)}` : ''
|
|
361
|
+
const res = await apiFetch(`${API_BASE}/api/skills/${encodeURIComponent(name)}${q}`)
|
|
321
362
|
if (!res.ok) throw new Error(`Skill fetch API error: ${res.status}`)
|
|
322
363
|
return res.json()
|
|
323
364
|
}
|
|
324
365
|
|
|
325
|
-
export async function
|
|
326
|
-
|
|
366
|
+
export async function fetchSkillFiles(
|
|
367
|
+
name: string,
|
|
368
|
+
dir: string,
|
|
369
|
+
repo?: string,
|
|
370
|
+
): Promise<{ name: string; size: string; isDirectory: boolean }[]> {
|
|
371
|
+
const params = new URLSearchParams({ dir })
|
|
372
|
+
if (repo) params.set('repo', repo)
|
|
373
|
+
const res = await apiFetch(`${API_BASE}/api/skills/${encodeURIComponent(name)}/files?${params}`)
|
|
374
|
+
if (!res.ok) throw new Error(`Skill files fetch API error: ${res.status}`)
|
|
375
|
+
return res.json()
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export async function saveSkill(name: string, content: string, repo?: string): Promise<void> {
|
|
379
|
+
const q = repo ? `?repo=${encodeURIComponent(repo)}` : ''
|
|
380
|
+
const res = await apiFetch(`${API_BASE}/api/skills/${encodeURIComponent(name)}${q}`, {
|
|
327
381
|
method: 'PUT',
|
|
328
382
|
headers: { 'Content-Type': 'application/json' },
|
|
329
|
-
body: JSON.stringify({ content }),
|
|
383
|
+
body: JSON.stringify({ content, repo }),
|
|
330
384
|
})
|
|
331
385
|
if (!res.ok) throw new Error(`Skill save API error: ${res.status}`)
|
|
332
386
|
}
|
|
333
387
|
|
|
334
|
-
export async function deleteSkill(name: string): Promise<void> {
|
|
335
|
-
const
|
|
388
|
+
export async function deleteSkill(name: string, repo?: string): Promise<void> {
|
|
389
|
+
const q = repo ? `?repo=${encodeURIComponent(repo)}` : ''
|
|
390
|
+
const res = await apiFetch(`${API_BASE}/api/skills/${encodeURIComponent(name)}${q}`, {
|
|
336
391
|
method: 'DELETE',
|
|
337
392
|
})
|
|
338
393
|
if (!res.ok) throw new Error(`Skill delete API error: ${res.status}`)
|
|
@@ -452,6 +507,342 @@ export async function revokeApiKey(id: string): Promise<void> {
|
|
|
452
507
|
if (!res.ok) throw new Error(`Keys API error: ${res.status}`)
|
|
453
508
|
}
|
|
454
509
|
|
|
510
|
+
// ─── Secrets store (API keys / env import — not PATs) ─────────────────────────
|
|
511
|
+
|
|
512
|
+
export interface EnvSecretCandidate {
|
|
513
|
+
key: string
|
|
514
|
+
source: string
|
|
515
|
+
source_label: string
|
|
516
|
+
provider: string | null
|
|
517
|
+
masked: string
|
|
518
|
+
length: number
|
|
519
|
+
known: boolean
|
|
520
|
+
already_set?: boolean
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
export async function scanEnvSecrets(): Promise<{
|
|
524
|
+
candidates: EnvSecretCandidate[]
|
|
525
|
+
count: number
|
|
526
|
+
sources_scanned: string[]
|
|
527
|
+
}> {
|
|
528
|
+
const res = await apiFetch(`${API_BASE}/api/secrets/scan-env`)
|
|
529
|
+
if (!res.ok) throw new Error(`Env scan failed: ${res.status}`)
|
|
530
|
+
return res.json()
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
export async function parseEnvPaste(text: string): Promise<{
|
|
534
|
+
candidates: EnvSecretCandidate[]
|
|
535
|
+
count: number
|
|
536
|
+
}> {
|
|
537
|
+
const res = await apiFetch(`${API_BASE}/api/secrets/parse-env`, {
|
|
538
|
+
method: 'POST',
|
|
539
|
+
headers: { 'Content-Type': 'application/json' },
|
|
540
|
+
body: JSON.stringify({ text }),
|
|
541
|
+
})
|
|
542
|
+
if (!res.ok) {
|
|
543
|
+
const err = await res.json().catch(() => ({}))
|
|
544
|
+
throw new Error(err.error || `Parse failed: ${res.status}`)
|
|
545
|
+
}
|
|
546
|
+
return res.json()
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
export async function importEnvSecrets(opts: {
|
|
550
|
+
keys?: string[]
|
|
551
|
+
all?: boolean
|
|
552
|
+
pairs?: Record<string, string>
|
|
553
|
+
overwrite?: boolean
|
|
554
|
+
}): Promise<{
|
|
555
|
+
imported: { key: string; provider: string | null }[]
|
|
556
|
+
skipped: { key: string; reason: string }[]
|
|
557
|
+
errors: { key: string; error: string }[]
|
|
558
|
+
imported_count: number
|
|
559
|
+
skipped_count: number
|
|
560
|
+
}> {
|
|
561
|
+
const res = await apiFetch(`${API_BASE}/api/secrets/import-env`, {
|
|
562
|
+
method: 'POST',
|
|
563
|
+
headers: { 'Content-Type': 'application/json' },
|
|
564
|
+
body: JSON.stringify(opts),
|
|
565
|
+
})
|
|
566
|
+
if (!res.ok) {
|
|
567
|
+
const err = await res.json().catch(() => ({}))
|
|
568
|
+
throw new Error(err.error || `Import failed: ${res.status}`)
|
|
569
|
+
}
|
|
570
|
+
return res.json()
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
export interface SecretCatalogKey {
|
|
574
|
+
key: string
|
|
575
|
+
set: boolean
|
|
576
|
+
length: number
|
|
577
|
+
fingerprint: string | null
|
|
578
|
+
masked: string | null
|
|
579
|
+
scope: string
|
|
580
|
+
provider: string | null
|
|
581
|
+
provider_name?: string | null
|
|
582
|
+
label: string | null
|
|
583
|
+
repos: string[]
|
|
584
|
+
/** Single product repo, or null for Developer secrets */
|
|
585
|
+
repo?: string | null
|
|
586
|
+
multi_repo_error?: boolean
|
|
587
|
+
binding_error?: string | null
|
|
588
|
+
project_path: string | null
|
|
589
|
+
subscription_tier: string | null
|
|
590
|
+
monthly_cost_usd: number | null
|
|
591
|
+
monthly_cap_usd: number | null
|
|
592
|
+
api_docs_url: string | null
|
|
593
|
+
console_url: string | null
|
|
594
|
+
pricing_url: string | null
|
|
595
|
+
schema: { auth: string; header?: string; env_keys?: string[]; notes?: string } | null
|
|
596
|
+
schema_notes: string | null
|
|
597
|
+
auth_scheme: string | null
|
|
598
|
+
rotate_every_days: number | null
|
|
599
|
+
auto_rotate: boolean
|
|
600
|
+
next_rotate_due: string | null
|
|
601
|
+
rotation_overdue: boolean
|
|
602
|
+
notes: string | null
|
|
603
|
+
created_at: string | null
|
|
604
|
+
updated_at: string | null
|
|
605
|
+
rotated_at: string | null
|
|
606
|
+
usage_30d: { events: number; cost_usd: number; input_tokens: number; output_tokens: number }
|
|
607
|
+
tiers: { id: string; label: string; monthly_usd?: number | null }[]
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
export interface SecretCatalog {
|
|
611
|
+
keys: SecretCatalogKey[]
|
|
612
|
+
providers: unknown[]
|
|
613
|
+
summary: {
|
|
614
|
+
total_keys: number
|
|
615
|
+
providers_active: number
|
|
616
|
+
monthly_subscription_usd: number
|
|
617
|
+
multi_repo_violations?: number
|
|
618
|
+
developer_keys?: number
|
|
619
|
+
product_keys?: number
|
|
620
|
+
usage_7d: { events: number; cost_usd: number }
|
|
621
|
+
usage_30d: { events: number; cost_usd: number }
|
|
622
|
+
rotation_overdue: number
|
|
623
|
+
budget: Record<string, unknown>
|
|
624
|
+
}
|
|
625
|
+
by_provider: { provider: string; keys: number; cost_30d: number; monthly_cost: number }[]
|
|
626
|
+
store: string
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
export async function fetchSecretsCatalog(): Promise<SecretCatalog> {
|
|
630
|
+
const res = await apiFetch(`${API_BASE}/api/secrets`)
|
|
631
|
+
if (!res.ok) throw new Error(`Secrets catalog error: ${res.status}`)
|
|
632
|
+
return res.json()
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
export async function updateSecretMeta(key: string, patch: Record<string, unknown>): Promise<SecretCatalogKey> {
|
|
636
|
+
const res = await apiFetch(`${API_BASE}/api/secrets/${encodeURIComponent(key)}`, {
|
|
637
|
+
method: 'PATCH',
|
|
638
|
+
headers: { 'Content-Type': 'application/json' },
|
|
639
|
+
body: JSON.stringify(patch),
|
|
640
|
+
})
|
|
641
|
+
if (!res.ok) {
|
|
642
|
+
const err = await res.json().catch(() => ({}))
|
|
643
|
+
throw new Error(err.error || `Meta update failed: ${res.status}`)
|
|
644
|
+
}
|
|
645
|
+
return res.json()
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
export async function rotateSecretValue(
|
|
649
|
+
key: string,
|
|
650
|
+
value: string,
|
|
651
|
+
opts: { export_env?: boolean; export_all?: boolean } = {},
|
|
652
|
+
): Promise<{
|
|
653
|
+
rotated?: boolean
|
|
654
|
+
next_rotate_due?: string | null
|
|
655
|
+
exports?: { ok: boolean; envPath?: string; count?: number; error?: string }[]
|
|
656
|
+
secret?: SecretCatalogKey
|
|
657
|
+
}> {
|
|
658
|
+
const res = await apiFetch(`${API_BASE}/api/secrets/${encodeURIComponent(key)}/rotate`, {
|
|
659
|
+
method: 'POST',
|
|
660
|
+
headers: { 'Content-Type': 'application/json' },
|
|
661
|
+
body: JSON.stringify({
|
|
662
|
+
value,
|
|
663
|
+
export_env: opts.export_env ?? true,
|
|
664
|
+
export_all: opts.export_all ?? true,
|
|
665
|
+
}),
|
|
666
|
+
})
|
|
667
|
+
if (!res.ok) {
|
|
668
|
+
const err = await res.json().catch(() => ({}))
|
|
669
|
+
throw new Error(err.error || `Rotate failed: ${res.status}`)
|
|
670
|
+
}
|
|
671
|
+
return res.json()
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
export async function deleteProviderSecret(key: string): Promise<void> {
|
|
675
|
+
const res = await apiFetch(`${API_BASE}/api/secrets/${encodeURIComponent(key)}`, { method: 'DELETE' })
|
|
676
|
+
if (!res.ok) throw new Error(`Delete failed: ${res.status}`)
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
export async function recordSecretUsage(body: {
|
|
680
|
+
key_ref?: string
|
|
681
|
+
key?: string
|
|
682
|
+
provider?: string
|
|
683
|
+
model?: string
|
|
684
|
+
cost_usd?: number
|
|
685
|
+
input_tokens?: number
|
|
686
|
+
output_tokens?: number
|
|
687
|
+
source?: string
|
|
688
|
+
}): Promise<unknown> {
|
|
689
|
+
const res = await apiFetch(`${API_BASE}/api/secrets/usage`, {
|
|
690
|
+
method: 'POST',
|
|
691
|
+
headers: { 'Content-Type': 'application/json' },
|
|
692
|
+
body: JSON.stringify(body),
|
|
693
|
+
})
|
|
694
|
+
if (!res.ok) throw new Error(`Usage record failed: ${res.status}`)
|
|
695
|
+
return res.json()
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
export async function exportEnvFromSecrets(opts: {
|
|
699
|
+
path?: string
|
|
700
|
+
all_projects?: boolean
|
|
701
|
+
dry_run?: boolean
|
|
702
|
+
include_global?: boolean
|
|
703
|
+
keys?: string[]
|
|
704
|
+
}): Promise<{
|
|
705
|
+
count?: number
|
|
706
|
+
keys?: string[]
|
|
707
|
+
envPath?: string
|
|
708
|
+
results?: { ok: boolean; name?: string; envPath?: string; count?: number; error?: string }[]
|
|
709
|
+
store?: string
|
|
710
|
+
}> {
|
|
711
|
+
const res = await apiFetch(`${API_BASE}/api/secrets/export-env`, {
|
|
712
|
+
method: 'POST',
|
|
713
|
+
headers: { 'Content-Type': 'application/json' },
|
|
714
|
+
body: JSON.stringify(opts),
|
|
715
|
+
})
|
|
716
|
+
if (!res.ok) {
|
|
717
|
+
const err = await res.json().catch(() => ({}))
|
|
718
|
+
throw new Error(err.error || `export-env failed: ${res.status}`)
|
|
719
|
+
}
|
|
720
|
+
return res.json()
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
export async function fetchRotationDue(): Promise<{ due: SecretCatalogKey[]; count: number }> {
|
|
724
|
+
const res = await apiFetch(`${API_BASE}/api/secrets/rotation-due`)
|
|
725
|
+
if (!res.ok) throw new Error(`rotation-due failed: ${res.status}`)
|
|
726
|
+
return res.json()
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
export async function enqueueRotationDue(): Promise<unknown> {
|
|
730
|
+
const res = await apiFetch(`${API_BASE}/api/secrets/rotation-due/enqueue`, { method: 'POST' })
|
|
731
|
+
if (!res.ok) {
|
|
732
|
+
// fallback: client can use CLI; route may not exist yet
|
|
733
|
+
const err = await res.json().catch(() => ({}))
|
|
734
|
+
throw new Error(err.error || `enqueue failed: ${res.status}`)
|
|
735
|
+
}
|
|
736
|
+
return res.json()
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// ─── WebAuthn / passkey step-up (secret reveal) ───────────────────────────────
|
|
740
|
+
|
|
741
|
+
export interface WebAuthnStatus {
|
|
742
|
+
enabled: boolean
|
|
743
|
+
has_passkeys: boolean
|
|
744
|
+
count: number
|
|
745
|
+
passkeys: { id: string; created_at: string | null; label?: string }[]
|
|
746
|
+
password_step_up_allowed?: boolean
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
export async function fetchWebAuthnStatus(): Promise<WebAuthnStatus> {
|
|
750
|
+
const res = await apiFetch(`${API_BASE}/api/webauthn/status`)
|
|
751
|
+
if (!res.ok) throw new Error(`WebAuthn status failed: ${res.status}`)
|
|
752
|
+
return res.json()
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
export async function webauthnRegisterOptions(): Promise<PublicKeyCredentialCreationOptionsJSON> {
|
|
756
|
+
const res = await apiFetch(`${API_BASE}/api/webauthn/register/options`, { method: 'POST' })
|
|
757
|
+
if (!res.ok) {
|
|
758
|
+
const err = await res.json().catch(() => ({}))
|
|
759
|
+
throw new Error(err.error || `register options failed: ${res.status}`)
|
|
760
|
+
}
|
|
761
|
+
return res.json()
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
export async function webauthnRegisterVerify(
|
|
765
|
+
response: unknown,
|
|
766
|
+
label?: string,
|
|
767
|
+
): Promise<{ verified: boolean; passkeys?: WebAuthnStatus['passkeys'] }> {
|
|
768
|
+
const res = await apiFetch(`${API_BASE}/api/webauthn/register/verify`, {
|
|
769
|
+
method: 'POST',
|
|
770
|
+
headers: { 'Content-Type': 'application/json' },
|
|
771
|
+
body: JSON.stringify({ response, label }),
|
|
772
|
+
})
|
|
773
|
+
if (!res.ok) {
|
|
774
|
+
const err = await res.json().catch(() => ({}))
|
|
775
|
+
throw new Error(err.error || `register verify failed: ${res.status}`)
|
|
776
|
+
}
|
|
777
|
+
return res.json()
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
export async function webauthnAssertOptions(): Promise<PublicKeyCredentialRequestOptionsJSON> {
|
|
781
|
+
const res = await apiFetch(`${API_BASE}/api/webauthn/assert/options`, { method: 'POST' })
|
|
782
|
+
if (!res.ok) {
|
|
783
|
+
const err = await res.json().catch(() => ({}))
|
|
784
|
+
throw new Error(err.error || `assert options failed: ${res.status}`)
|
|
785
|
+
}
|
|
786
|
+
return res.json()
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
export async function webauthnAssertVerify(
|
|
790
|
+
response: unknown,
|
|
791
|
+
): Promise<{ verified: boolean; step_up_token: string; expires_in: number }> {
|
|
792
|
+
const res = await apiFetch(`${API_BASE}/api/webauthn/assert/verify`, {
|
|
793
|
+
method: 'POST',
|
|
794
|
+
headers: { 'Content-Type': 'application/json' },
|
|
795
|
+
body: JSON.stringify({ response, purpose: 'secrets:reveal', ttl_seconds: 60 }),
|
|
796
|
+
})
|
|
797
|
+
if (!res.ok) {
|
|
798
|
+
const err = await res.json().catch(() => ({}))
|
|
799
|
+
throw new Error(err.error || `assert verify failed: ${res.status}`)
|
|
800
|
+
}
|
|
801
|
+
return res.json()
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
export async function webauthnPasswordStepUp(
|
|
805
|
+
password: string,
|
|
806
|
+
): Promise<{ verified: boolean; step_up_token: string; expires_in: number }> {
|
|
807
|
+
const res = await apiFetch(`${API_BASE}/api/webauthn/step-up/password`, {
|
|
808
|
+
method: 'POST',
|
|
809
|
+
headers: { 'Content-Type': 'application/json' },
|
|
810
|
+
body: JSON.stringify({ password, purpose: 'secrets:reveal', ttl_seconds: 60 }),
|
|
811
|
+
})
|
|
812
|
+
if (!res.ok) {
|
|
813
|
+
const err = await res.json().catch(() => ({}))
|
|
814
|
+
throw new Error(err.error || `password step-up failed: ${res.status}`)
|
|
815
|
+
}
|
|
816
|
+
return res.json()
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
export async function deletePasskey(id: string): Promise<void> {
|
|
820
|
+
const res = await apiFetch(`${API_BASE}/api/webauthn/credentials/${encodeURIComponent(id)}`, {
|
|
821
|
+
method: 'DELETE',
|
|
822
|
+
})
|
|
823
|
+
if (!res.ok) throw new Error(`Delete passkey failed: ${res.status}`)
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
export async function revealSecretValue(
|
|
827
|
+
key: string,
|
|
828
|
+
stepUpToken: string,
|
|
829
|
+
): Promise<{ key: string; value: string; revealed_at: string }> {
|
|
830
|
+
const res = await apiFetch(`${API_BASE}/api/secrets/${encodeURIComponent(key)}/reveal`, {
|
|
831
|
+
method: 'POST',
|
|
832
|
+
headers: { 'Content-Type': 'application/json' },
|
|
833
|
+
body: JSON.stringify({ step_up_token: stepUpToken }),
|
|
834
|
+
})
|
|
835
|
+
if (!res.ok) {
|
|
836
|
+
const err = await res.json().catch(() => ({}))
|
|
837
|
+
throw new Error(err.error || `Reveal failed: ${res.status}`)
|
|
838
|
+
}
|
|
839
|
+
return res.json()
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/** Minimal JSON option types (compatible with @simplewebauthn/browser) */
|
|
843
|
+
export type PublicKeyCredentialCreationOptionsJSON = Record<string, unknown>
|
|
844
|
+
export type PublicKeyCredentialRequestOptionsJSON = Record<string, unknown>
|
|
845
|
+
|
|
455
846
|
export async function connectClient(client: string, baseUrl: string): Promise<{ success: boolean; message: string }> {
|
|
456
847
|
const res = await apiFetch(`${API_BASE}/api/integrations/connect`, {
|
|
457
848
|
method: 'POST',
|
|
@@ -568,8 +959,11 @@ export async function resolveConflict(
|
|
|
568
959
|
|
|
569
960
|
// ─── Memory Editor APIs ────────────────────────────────────────────────────────
|
|
570
961
|
|
|
571
|
-
export async function saveMemory(slug: string, node: Partial<MemoryNode
|
|
572
|
-
const
|
|
962
|
+
export async function saveMemory(slug: string, node: Partial<MemoryNode>, brainId?: string): Promise<MemoryNode> {
|
|
963
|
+
const params = new URLSearchParams()
|
|
964
|
+
if (brainId) params.set('brain', brainId)
|
|
965
|
+
const qs = params.toString()
|
|
966
|
+
const res = await apiFetch(`${API_BASE}/api/memory/${encodeURIComponent(slug)}${qs ? `?${qs}` : ''}`, {
|
|
573
967
|
method: "PATCH",
|
|
574
968
|
headers: { "Content-Type": "application/json" },
|
|
575
969
|
body: JSON.stringify(node),
|
|
@@ -578,8 +972,11 @@ export async function saveMemory(slug: string, node: Partial<MemoryNode>): Promi
|
|
|
578
972
|
return res.json()
|
|
579
973
|
}
|
|
580
974
|
|
|
581
|
-
export async function createMemory(node: Partial<MemoryNode> & { slug: string }): Promise<MemoryNode> {
|
|
582
|
-
const
|
|
975
|
+
export async function createMemory(node: Partial<MemoryNode> & { slug: string }, brainId?: string): Promise<MemoryNode> {
|
|
976
|
+
const params = new URLSearchParams()
|
|
977
|
+
if (brainId) params.set('brain', brainId)
|
|
978
|
+
const qs = params.toString()
|
|
979
|
+
const res = await apiFetch(`${API_BASE}/api/memory${qs ? `?${qs}` : ''}`, {
|
|
583
980
|
method: "POST",
|
|
584
981
|
headers: { "Content-Type": "application/json" },
|
|
585
982
|
body: JSON.stringify(node),
|
|
@@ -588,8 +985,11 @@ export async function createMemory(node: Partial<MemoryNode> & { slug: string })
|
|
|
588
985
|
return res.json()
|
|
589
986
|
}
|
|
590
987
|
|
|
591
|
-
export async function deleteMemory(slug: string): Promise<void> {
|
|
592
|
-
const
|
|
988
|
+
export async function deleteMemory(slug: string, brainId?: string): Promise<void> {
|
|
989
|
+
const params = new URLSearchParams()
|
|
990
|
+
if (brainId) params.set('brain', brainId)
|
|
991
|
+
const qs = params.toString()
|
|
992
|
+
const res = await apiFetch(`${API_BASE}/api/memory/${encodeURIComponent(slug)}${qs ? `?${qs}` : ''}`, {
|
|
593
993
|
method: "DELETE",
|
|
594
994
|
})
|
|
595
995
|
if (!res.ok) throw new Error(`Failed to delete memory: ${res.status}`)
|
|
@@ -757,7 +1157,7 @@ export async function fetchHelpContent(topicId: string): Promise<{ topic: string
|
|
|
757
1157
|
// ─── Instructions APIs ─────────────────────────────────────────────────────────
|
|
758
1158
|
|
|
759
1159
|
export async function fetchInstructions(): Promise<{ surfaces: Array<{ name: string; filename: string; size: number; lastCompiled: string; active: boolean }>; lastCompileTimestamp: string; totalNodes: number }> {
|
|
760
|
-
const res = await apiFetch(`${API_BASE}/api/instructions`)
|
|
1160
|
+
const res = await apiFetch(`${API_BASE}/api/dashboard/instructions`)
|
|
761
1161
|
if (!res.ok) throw new Error(`Instructions API error: ${res.status}`)
|
|
762
1162
|
return res.json()
|
|
763
1163
|
}
|
|
@@ -798,11 +1198,87 @@ export async function triggerOkfExport(path: string, options: { stripSsss?: bool
|
|
|
798
1198
|
|
|
799
1199
|
// ─── OpenWiki APIs ─────────────────────────────────────────────────────────────
|
|
800
1200
|
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
1201
|
+
/**
|
|
1202
|
+
* OpenWiki nodes for the selected brain(s) only.
|
|
1203
|
+
* Scoped by x-total-recall-brain header (apiFetch) and explicit brain query.
|
|
1204
|
+
*/
|
|
1205
|
+
export async function fetchOpenWikiNodes(
|
|
1206
|
+
brainId?: string | null,
|
|
1207
|
+
): Promise<import('./types').MemoryNode[]> {
|
|
1208
|
+
const active =
|
|
1209
|
+
brainId ||
|
|
1210
|
+
(typeof localStorage !== 'undefined' ? localStorage.getItem('total-recall-active-brain') : null) ||
|
|
1211
|
+
'global'
|
|
1212
|
+
|
|
1213
|
+
// Reuse listMemory brain routing (global vs project:name), then keep openwiki-tagged only.
|
|
1214
|
+
// Server also filters tag=openwiki when we hit /api/memory?tag=openwiki.
|
|
1215
|
+
const params = new URLSearchParams({ tag: 'openwiki', limit: '500' })
|
|
1216
|
+
const ids = active.split(',').map((s) => s.trim()).filter(Boolean)
|
|
1217
|
+
|
|
1218
|
+
if (ids.length === 0 || (ids.length === 1 && ids[0] === 'global')) {
|
|
1219
|
+
const res = await apiFetch(`${API_BASE}/api/memory?${params}`)
|
|
1220
|
+
if (!res.ok) throw new Error(`OpenWiki nodes error: ${res.status}`)
|
|
1221
|
+
const data = await res.json()
|
|
1222
|
+
const nodes = Array.isArray(data) ? data : (data.nodes ?? [])
|
|
1223
|
+
return filterOpenWikiNodes(nodes)
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
// Multi / project brains — fetch each scope separately (same merge rules as Memory)
|
|
1227
|
+
const fetchPromises = ids.map(async (id) => {
|
|
1228
|
+
try {
|
|
1229
|
+
if (id === 'global') {
|
|
1230
|
+
const res = await apiFetch(`${API_BASE}/api/memory?${params}`)
|
|
1231
|
+
if (!res.ok) return []
|
|
1232
|
+
const data = await res.json()
|
|
1233
|
+
return Array.isArray(data) ? data : (data.nodes ?? [])
|
|
1234
|
+
}
|
|
1235
|
+
// Project brain: list all nodes then filter openwiki (brains/:id/nodes may not support tag)
|
|
1236
|
+
const res = await apiFetch(`${API_BASE}/api/brains/${encodeURIComponent(id)}/nodes`)
|
|
1237
|
+
if (!res.ok) {
|
|
1238
|
+
// Fallback: memory list with brain header already set by apiFetch
|
|
1239
|
+
const res2 = await apiFetch(`${API_BASE}/api/memory?${params}&brain=${encodeURIComponent(id)}`)
|
|
1240
|
+
if (!res2.ok) return []
|
|
1241
|
+
const data2 = await res2.json()
|
|
1242
|
+
return Array.isArray(data2) ? data2 : (data2.nodes ?? [])
|
|
1243
|
+
}
|
|
1244
|
+
const data = await res.json()
|
|
1245
|
+
const nodes = Array.isArray(data) ? data : (data.nodes ?? [])
|
|
1246
|
+
return filterOpenWikiNodes(nodes)
|
|
1247
|
+
} catch {
|
|
1248
|
+
return []
|
|
1249
|
+
}
|
|
1250
|
+
})
|
|
1251
|
+
|
|
1252
|
+
const results = await Promise.all(fetchPromises)
|
|
1253
|
+
const merged: import('./types').MemoryNode[] = []
|
|
1254
|
+
const seen = new Set<string>()
|
|
1255
|
+
// Project overrides global on slug collision
|
|
1256
|
+
const ordered = results
|
|
1257
|
+
.map((nodes, i) => ({ nodes, id: ids[i] }))
|
|
1258
|
+
.sort((a, b) => {
|
|
1259
|
+
if (a.id === 'global') return 1
|
|
1260
|
+
if (b.id === 'global') return -1
|
|
1261
|
+
return 0
|
|
1262
|
+
})
|
|
1263
|
+
for (const { nodes } of ordered) {
|
|
1264
|
+
for (const n of filterOpenWikiNodes(nodes)) {
|
|
1265
|
+
if (!seen.has(n.slug)) {
|
|
1266
|
+
seen.add(n.slug)
|
|
1267
|
+
merged.push(n)
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
return merged
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
function filterOpenWikiNodes(
|
|
1275
|
+
nodes: import('./types').MemoryNode[],
|
|
1276
|
+
): import('./types').MemoryNode[] {
|
|
1277
|
+
return nodes.filter((n) => {
|
|
1278
|
+
const tags = (n.tags || []).map((t) => String(t).toLowerCase())
|
|
1279
|
+
const slug = String(n.slug || '').toLowerCase()
|
|
1280
|
+
return tags.includes('openwiki') || slug.startsWith('openwiki-')
|
|
1281
|
+
})
|
|
806
1282
|
}
|
|
807
1283
|
|
|
808
1284
|
// UCW export/import/validate handled via runSandbox + @ssss/cli (SSSS §16)
|
|
@@ -832,3 +1308,109 @@ export async function fetchDesignDocContent(path: string): Promise<{ content: st
|
|
|
832
1308
|
if (!res.ok) throw new Error(`Design doc content error: ${res.status}`)
|
|
833
1309
|
return res.json()
|
|
834
1310
|
}
|
|
1311
|
+
|
|
1312
|
+
export interface VaultDocument extends Record<string, unknown> {
|
|
1313
|
+
id?: string
|
|
1314
|
+
type?: string
|
|
1315
|
+
name?: string
|
|
1316
|
+
path: string
|
|
1317
|
+
status?: string
|
|
1318
|
+
portability?: string
|
|
1319
|
+
updatedAt?: string | number
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
export interface DocsResponse {
|
|
1323
|
+
docs: VaultDocument[]
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
export interface DocumentResponse {
|
|
1327
|
+
raw: string
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
export interface SavedView {
|
|
1331
|
+
id: string
|
|
1332
|
+
name: string
|
|
1333
|
+
filters: Record<string, string>
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
export async function fetchDocs(brain?: string, params?: Record<string, string>): Promise<DocsResponse> {
|
|
1337
|
+
const p = new URLSearchParams()
|
|
1338
|
+
if (brain) p.set('brain', brain)
|
|
1339
|
+
if (params) {
|
|
1340
|
+
for (const [k, v] of Object.entries(params)) if (v) p.set(k, v)
|
|
1341
|
+
}
|
|
1342
|
+
const res = await apiFetch(`/api/docs?${p.toString()}`)
|
|
1343
|
+
if (!res.ok) throw new Error('Failed to fetch docs')
|
|
1344
|
+
return res.json()
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
export async function readDoc(path: string, brain?: string): Promise<DocumentResponse> {
|
|
1348
|
+
const p = new URLSearchParams({ path })
|
|
1349
|
+
if (brain) p.set('brain', brain)
|
|
1350
|
+
const res = await apiFetch(`/api/docs/read?${p.toString()}`)
|
|
1351
|
+
if (!res.ok) throw new Error('Failed to read doc')
|
|
1352
|
+
return res.json()
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
export async function createDoc(path: string, content: string, brain?: string) {
|
|
1356
|
+
const res = await apiFetch('/api/docs', {
|
|
1357
|
+
method: 'POST',
|
|
1358
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1359
|
+
body: JSON.stringify({ path, content, brainId: brain })
|
|
1360
|
+
})
|
|
1361
|
+
if (!res.ok) throw new Error('Failed to create doc')
|
|
1362
|
+
return res.json()
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
export async function updateDoc(path: string, content: string, brain?: string) {
|
|
1366
|
+
const res = await apiFetch('/api/docs', {
|
|
1367
|
+
method: 'PUT',
|
|
1368
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1369
|
+
body: JSON.stringify({ path, content, brainId: brain })
|
|
1370
|
+
})
|
|
1371
|
+
if (!res.ok) throw new Error('Failed to update doc')
|
|
1372
|
+
return res.json()
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
export async function deleteDoc(path: string, brain?: string) {
|
|
1376
|
+
const p = new URLSearchParams({ path })
|
|
1377
|
+
if (brain) p.set('brain', brain)
|
|
1378
|
+
const res = await apiFetch(`/api/docs?${p.toString()}`, { method: 'DELETE' })
|
|
1379
|
+
if (!res.ok) throw new Error('Failed to delete doc')
|
|
1380
|
+
return res.json()
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
export async function fetchViews(): Promise<SavedView[]> {
|
|
1384
|
+
const res = await apiFetch('/api/views')
|
|
1385
|
+
if (!res.ok) throw new Error('Failed to fetch views')
|
|
1386
|
+
return res.json()
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
export async function createView(name: string, filters: Record<string, string>) {
|
|
1390
|
+
const res = await apiFetch('/api/views', {
|
|
1391
|
+
method: 'POST',
|
|
1392
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1393
|
+
body: JSON.stringify({ name, filters })
|
|
1394
|
+
})
|
|
1395
|
+
if (!res.ok) throw new Error('Failed to create view')
|
|
1396
|
+
return res.json()
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
export async function deleteView(id: string) {
|
|
1400
|
+
const res = await apiFetch(`/api/views/${id}`, { method: 'DELETE' })
|
|
1401
|
+
if (!res.ok) throw new Error('Failed to delete view')
|
|
1402
|
+
return res.json()
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
export async function postDecision(id: string, action: string, notes?: string): Promise<{ success: boolean; droplet_response?: unknown }> {
|
|
1406
|
+
const res = await apiFetch(`${API_BASE}/api/sync/remote-vault/proposals/${id}/decision`, {
|
|
1407
|
+
method: 'POST',
|
|
1408
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1409
|
+
body: JSON.stringify({ action, notes }),
|
|
1410
|
+
})
|
|
1411
|
+
if (!res.ok) {
|
|
1412
|
+
const errorData = await res.json().catch(() => ({}))
|
|
1413
|
+
throw new Error(errorData.error || `Decision API error: ${res.status}`)
|
|
1414
|
+
}
|
|
1415
|
+
return res.json()
|
|
1416
|
+
}
|