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.
Files changed (152) hide show
  1. package/README.md +91 -108
  2. package/bin/antigravity.mjs +81 -52
  3. package/bin/total-recall.mjs +32 -57
  4. package/extension/background.js +1 -1
  5. package/extension/options/options.js +9 -3
  6. package/extension/sidepanel/sidepanel.js +0 -2
  7. package/frontend/index.html +4 -3
  8. package/frontend/package.json +1 -0
  9. package/frontend/src/App.tsx +109 -152
  10. package/frontend/src/api.ts +605 -23
  11. package/frontend/src/components/BrainSelector.tsx +211 -199
  12. package/frontend/src/components/DocumentEditorModal.tsx +57 -0
  13. package/frontend/src/components/DocumentTable.tsx +47 -0
  14. package/frontend/src/components/Graph3D.tsx +120 -71
  15. package/frontend/src/components/UsageChart.tsx +3 -3
  16. package/frontend/src/components/brand/BrandMark.tsx +95 -0
  17. package/frontend/src/index.css +286 -86
  18. package/frontend/src/pages/ApiKeysPage.tsx +1878 -215
  19. package/frontend/src/pages/ChatPage.tsx +103 -45
  20. package/frontend/src/pages/DesignDocsPage.tsx +5 -5
  21. package/frontend/src/pages/FilesPage.tsx +10 -6
  22. package/frontend/src/pages/GraphPage.tsx +5 -3
  23. package/frontend/src/pages/HelpPage.tsx +2 -2
  24. package/frontend/src/pages/InboxPage.tsx +82 -0
  25. package/frontend/src/pages/InstructionsPage.tsx +1 -1
  26. package/frontend/src/pages/IntegrationsPage.tsx +21 -10
  27. package/frontend/src/pages/LoginPage.tsx +95 -77
  28. package/frontend/src/pages/MemoryPage.tsx +31 -8
  29. package/frontend/src/pages/OnboardingPage.tsx +690 -0
  30. package/frontend/src/pages/OpenWikiPage.tsx +412 -323
  31. package/frontend/src/pages/SettingsPage.tsx +825 -922
  32. package/frontend/src/pages/SkillsPage.tsx +272 -147
  33. package/frontend/src/pages/UsagePage.tsx +23 -31
  34. package/frontend/src/pages/VaultPage.tsx +223 -0
  35. package/frontend/src/types.ts +13 -0
  36. package/package.json +4 -4
  37. package/scaffold/.agent/skills/total-recall/SKILL.md +43 -4
  38. package/scaffold/.agent/skills/total-recall/modules/README.md +10 -0
  39. package/scaffold/.agent/skills/total-recall/modules/agents/README.md +5 -0
  40. package/scaffold/.agent/skills/total-recall/{skills/tr-cli-agents → modules/agents}/agents.yml +8 -0
  41. package/scaffold/.agent/skills/total-recall/modules/skill-deploy/README.md +9 -0
  42. package/scaffold/.agent/skills/total-recall/openwiki/README.md +10 -0
  43. package/scaffold/.agent/skills/total-recall/openwiki/architecture.md +5 -0
  44. package/scaffold/.agent/skills/total-recall/openwiki/ide.md +10 -0
  45. package/scaffold/.agent/skills/total-recall/openwiki/memory.md +11 -0
  46. package/scaffold/.agent/skills/total-recall/openwiki/secrets.md +7 -0
  47. package/scaffold/.agent/skills/total-recall/openwiki/skills.md +7 -0
  48. package/src/cli/brain.mjs +63 -5
  49. package/src/cli/connect.mjs +11 -19
  50. package/src/cli/daemon.mjs +5 -4
  51. package/src/cli/deploy-ui.mjs +8 -3
  52. package/src/cli/deploy.mjs +84 -27
  53. package/src/cli/ingest/index.mjs +1 -1
  54. package/src/cli/init.mjs +63 -34
  55. package/src/cli/remember.mjs +3 -3
  56. package/src/cli/secret.mjs +812 -0
  57. package/src/cli/setup.mjs +34 -7
  58. package/src/cli/share.mjs +2 -2
  59. package/src/cli/skill.mjs +396 -35
  60. package/src/cli/sync.mjs +4 -4
  61. package/src/cli/task.mjs +287 -0
  62. package/src/core/clarity-rewriter.mjs +5 -5
  63. package/src/core/config.mjs +44 -2
  64. package/src/core/conflict-detector.mjs +5 -5
  65. package/src/core/crons.mjs +54 -0
  66. package/src/core/daemon-loop.mjs +87 -235
  67. package/src/core/dream.mjs +170 -10
  68. package/src/core/env-import.mjs +456 -0
  69. package/src/core/fact-seeker.mjs +14 -14
  70. package/src/core/import-rules.mjs +20 -7
  71. package/src/core/inference-engine.mjs +3 -3
  72. package/src/core/migrate.mjs +14 -8
  73. package/src/core/okf-adapter.mjs +2 -2
  74. package/src/core/operation-validator.mjs +39 -331
  75. package/src/core/optimizer.mjs +7 -0
  76. package/src/core/post-mortem.mjs +2 -2
  77. package/src/core/project-brain.mjs +349 -0
  78. package/src/core/provider-catalog.mjs +296 -0
  79. package/src/core/quick-capture.mjs +1 -1
  80. package/src/core/remote-vault-sync.mjs +194 -0
  81. package/src/core/research-queue.mjs +7 -11
  82. package/src/core/runtime.mjs +33 -9
  83. package/src/core/scheduler.mjs +59 -5
  84. package/src/core/schema.mjs +14 -0
  85. package/src/core/search.mjs +1 -1
  86. package/src/core/secrets-env-export.mjs +323 -0
  87. package/src/core/secrets-rotate.mjs +245 -0
  88. package/src/core/secrets-store.mjs +645 -0
  89. package/src/core/semantic-index.mjs +3 -3
  90. package/src/core/skills-registry.mjs +1062 -0
  91. package/src/core/source-adapters.mjs +4 -1
  92. package/src/core/ssss-host-extension.mjs +94 -0
  93. package/src/core/ssss-kernel-bridge.mjs +530 -0
  94. package/src/core/surface.mjs +14 -0
  95. package/src/core/task-envelope.mjs +298 -0
  96. package/src/core/task-executors.mjs +600 -0
  97. package/src/core/validated-write.mjs +124 -56
  98. package/src/core/vault.mjs +21 -34
  99. package/src/core/webauthn-store.mjs +272 -0
  100. package/src/server/api.mjs +26 -16
  101. package/src/server/auth.mjs +51 -0
  102. package/src/server/index.mjs +3 -3
  103. package/src/server/keys.mjs +30 -4
  104. package/src/server/rest.mjs +198 -25
  105. package/src/server/route-manifest.json +156 -0
  106. package/src/server/routes/_shared.mjs +40 -1
  107. package/src/server/routes/docs.mjs +260 -0
  108. package/src/server/routes/memory.mjs +145 -32
  109. package/src/server/routes/research.mjs +30 -0
  110. package/src/server/routes/secrets.mjs +470 -0
  111. package/src/server/routes/share.mjs +2 -2
  112. package/src/server/routes/skills.mjs +452 -54
  113. package/src/server/routes/sync.mjs +48 -0
  114. package/src/server/routes/webauthn.mjs +184 -0
  115. package/src/server/update-route-manifest.mjs +39 -0
  116. package/templates/onboarding-interview.md +32 -22
  117. package/templates/openwiki/README.md +10 -0
  118. package/templates/openwiki/architecture.md +5 -0
  119. package/templates/openwiki/ide.md +10 -0
  120. package/templates/openwiki/memory.md +11 -0
  121. package/templates/openwiki/secrets.md +7 -0
  122. package/templates/openwiki/skills.md +7 -0
  123. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/SKILL.md +0 -121
  124. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/references/antigravity.txt +0 -20
  125. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/references/claude.txt +0 -58
  126. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/references/codex.txt +0 -81
  127. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/references/gemini.txt +0 -44
  128. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/scripts/.gitkeep +0 -0
  129. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/subagents/.gitkeep +0 -0
  130. package/scaffold/.agent/skills/total-recall/skills/tr-research/SKILL.md +0 -338
  131. package/scaffold/.agent/skills/total-recall/skills/tr-skill/SKILL.md +0 -282
  132. package/scaffold/.agent/skills/total-recall/skills/tr-skill/assets/skill-template/SKILL.md +0 -20
  133. package/scaffold/.agent/skills/total-recall/skills/tr-skill/evals/evals.json +0 -20
  134. package/scaffold/.agent/skills/total-recall/skills/tr-skill/hooks/on-skill-add.sh +0 -14
  135. package/scaffold/.agent/skills/total-recall/skills/tr-skill/hooks/on-skill-delete.sh +0 -14
  136. package/scaffold/.agent/skills/total-recall/skills/tr-skill/learnings/.gitkeep +0 -0
  137. package/scaffold/.agent/skills/total-recall/skills/tr-skill/references/.gitkeep +0 -0
  138. package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/.gitkeep +0 -0
  139. package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/create-skill.sh +0 -65
  140. package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/enforce-skill-optimization.mjs +0 -135
  141. package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/skill-watcher.mjs +0 -64
  142. package/scaffold/.agent/skills/total-recall/skills/tr-skill/subagents/audit-all-skills.md +0 -62
  143. package/scaffold/.agent/skills/total-recall/skills/tr-ssss/SKILL.md +0 -298
  144. package/scaffold/.agent/skills/total-recall/skills/tr-ssss/evals/evals.json +0 -17
  145. package/scaffold/.agent/skills/total-recall/skills/tr-ssss/references/CHANGELOG.md +0 -124
  146. package/scaffold/.agent/skills/total-recall/skills/tr-ssss/references/ssss-spec.md +0 -1325
  147. package/scaffold/.agent/skills/total-recall/skills/tr-ssss/scripts/validate-schema.mjs +0 -232
  148. package/scaffold/.agent/skills/total-recall/skills/tr-ssss/subagents/audit-ssss-node.md +0 -9
  149. /package/scaffold/.agent/skills/total-recall/{skills/tr-cli-agents/evals → modules/skill-deploy/scripts}/.gitkeep +0 -0
  150. /package/scaffold/.agent/skills/total-recall/{skills/tr-skill → modules/skill-deploy}/scripts/find-skills.mjs +0 -0
  151. /package/scaffold/.agent/skills/total-recall/{skills/tr-skill → modules/skill-deploy}/scripts/install-skill.mjs +0 -0
  152. /package/scaffold/.agent/skills/total-recall/{skills/tr-skill → modules/skill-deploy}/scripts/scan-skill.mjs +0 -0
@@ -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 res = await fetch(url, { credentials: 'include', ...options })
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 res = await apiFetch(`${API_BASE}/api/memory/${slug}`)
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 async function listSkills(): Promise<import('./types').FileNode[]> {
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(name: string): Promise<{ name: string; content: string }> {
320
- const res = await apiFetch(`${API_BASE}/api/skills/${name}`)
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 saveSkill(name: string, content: string): Promise<void> {
326
- const res = await apiFetch(`${API_BASE}/api/skills/${name}`, {
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 res = await apiFetch(`${API_BASE}/api/skills/${name}`, {
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>): Promise<MemoryNode> {
572
- const res = await apiFetch(`${API_BASE}/api/memory/${slug}`, {
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 res = await apiFetch(`${API_BASE}/api/memory`, {
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 res = await apiFetch(`${API_BASE}/api/memory/${slug}`, {
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
- export async function fetchOpenWikiNodes(): Promise<import('./types').MemoryNode[]> {
802
- const res = await apiFetch(`${API_BASE}/api/memory?tags=openwiki`)
803
- if (!res.ok) throw new Error(`OpenWiki nodes error: ${res.status}`)
804
- const data = await res.json()
805
- return Array.isArray(data) ? data : (data.nodes ?? [])
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
+ }