total-recall-brain 3.12.2 → 3.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +3 -1
- package/frontend/src/App.tsx +112 -111
- package/frontend/src/api.ts +700 -18
- 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 +78 -0
- 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 +614 -0
- 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 +583 -0
- 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/ModelsPage.tsx +280 -23
- package/frontend/src/pages/OkfPage.tsx +535 -0
- package/frontend/src/pages/OnboardingPage.tsx +690 -0
- package/frontend/src/pages/OpenWikiPage.tsx +561 -0
- package/frontend/src/pages/SettingsPage.tsx +826 -740
- package/frontend/src/pages/SkillsPage.tsx +1259 -117
- package/frontend/src/pages/UsagePage.tsx +256 -11
- package/frontend/src/pages/VaultPage.tsx +223 -0
- package/frontend/src/types.ts +173 -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 +19 -34
- package/src/core/webauthn-store.mjs +272 -0
- package/src/server/api.mjs +28 -16
- package/src/server/auth.mjs +53 -2
- package/src/server/index.mjs +3 -3
- package/src/server/keys.mjs +30 -4
- package/src/server/rest.mjs +405 -68
- package/src/server/route-manifest.json +168 -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}`)
|
|
@@ -667,12 +1067,33 @@ export interface GeminiModelInfo {
|
|
|
667
1067
|
}
|
|
668
1068
|
|
|
669
1069
|
export async function fetchGeminiModels(): Promise<GeminiModelInfo[]> {
|
|
670
|
-
const res = await apiFetch(`${API_BASE}/api/gemini-models`)
|
|
1070
|
+
const res = await apiFetch(`${API_BASE}/api/gemini-models?t=${Date.now()}`)
|
|
671
1071
|
if (!res.ok) throw new Error(`Gemini models API error: ${res.status}`)
|
|
672
1072
|
const data = await res.json()
|
|
673
1073
|
return data.models ?? []
|
|
674
1074
|
}
|
|
675
1075
|
|
|
1076
|
+
export async function fetchClaudeModels(): Promise<GeminiModelInfo[]> {
|
|
1077
|
+
const res = await apiFetch(`${API_BASE}/api/claude-models?t=${Date.now()}`)
|
|
1078
|
+
if (!res.ok) throw new Error(`Claude models API error: ${res.status}`)
|
|
1079
|
+
const data = await res.json()
|
|
1080
|
+
return data.models ?? []
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
export async function fetchOpenaiModels(): Promise<GeminiModelInfo[]> {
|
|
1084
|
+
const res = await apiFetch(`${API_BASE}/api/openai-models?t=${Date.now()}`)
|
|
1085
|
+
if (!res.ok) throw new Error(`OpenAI models API error: ${res.status}`)
|
|
1086
|
+
const data = await res.json()
|
|
1087
|
+
return data.models ?? []
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
export async function fetchOpenRouterModels(): Promise<GeminiModelInfo[]> {
|
|
1091
|
+
const res = await apiFetch(`${API_BASE}/api/openrouter-models?t=${Date.now()}`)
|
|
1092
|
+
if (!res.ok) throw new Error(`OpenRouter models API error: ${res.status}`)
|
|
1093
|
+
const data = await res.json()
|
|
1094
|
+
return data.models ?? []
|
|
1095
|
+
}
|
|
1096
|
+
|
|
676
1097
|
// ─── Share API ──────────────────────────────────────────────────────────────────
|
|
677
1098
|
|
|
678
1099
|
export async function shareToApi(payload: { url: string; action: string; title?: string; tags?: string[] }): Promise<{ success: boolean; message?: string }> {
|
|
@@ -732,3 +1153,264 @@ export async function fetchHelpContent(topicId: string): Promise<{ topic: string
|
|
|
732
1153
|
if (!res.ok) throw new Error('Failed to load help content')
|
|
733
1154
|
return res.json()
|
|
734
1155
|
}
|
|
1156
|
+
|
|
1157
|
+
// ─── Instructions APIs ─────────────────────────────────────────────────────────
|
|
1158
|
+
|
|
1159
|
+
export async function fetchInstructions(): Promise<{ surfaces: Array<{ name: string; filename: string; size: number; lastCompiled: string; active: boolean }>; lastCompileTimestamp: string; totalNodes: number }> {
|
|
1160
|
+
const res = await apiFetch(`${API_BASE}/api/dashboard/instructions`)
|
|
1161
|
+
if (!res.ok) throw new Error(`Instructions API error: ${res.status}`)
|
|
1162
|
+
return res.json()
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
export async function fetchInstructionContent(name: string): Promise<{ name: string; content: string }> {
|
|
1166
|
+
const res = await apiFetch(`${API_BASE}/api/ssss/instructions?surface=${encodeURIComponent(name)}`)
|
|
1167
|
+
if (!res.ok) throw new Error(`Instruction content error: ${res.status}`)
|
|
1168
|
+
return res.json()
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
// ─── OKF APIs ──────────────────────────────────────────────────────────────────
|
|
1172
|
+
|
|
1173
|
+
export async function runOkfLint(): Promise<{ results: Array<{ slug: string; field: string; severity: string; message: string }>; summary: { total: number; errors: number; warnings: number } }> {
|
|
1174
|
+
const res = await apiFetch(`${API_BASE}/api/sandbox`, {
|
|
1175
|
+
method: 'POST',
|
|
1176
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1177
|
+
body: JSON.stringify({ code: 'const { execSync } = require("child_process"); console.log(JSON.stringify({ output: execSync("npx total-recall lint --okf 2>&1 || true", { encoding: "utf-8" }) }));', timeout: 30000 }),
|
|
1178
|
+
})
|
|
1179
|
+
if (!res.ok) throw new Error(`OKF lint error: ${res.status}`)
|
|
1180
|
+
return res.json()
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
export async function triggerOkfExport(path: string, options: { stripSsss?: boolean; format?: string; scope?: string }): Promise<{ success: boolean; message: string; path?: string }> {
|
|
1184
|
+
const args = [`export ${path} --okf`]
|
|
1185
|
+
if (options.stripSsss) args.push('--strip-ssss')
|
|
1186
|
+
if (options.format) args.push(`--format ${options.format}`)
|
|
1187
|
+
if (options.scope === 'global') args.push('--global')
|
|
1188
|
+
if (options.scope === 'project') args.push('--project')
|
|
1189
|
+
|
|
1190
|
+
const res = await apiFetch(`${API_BASE}/api/sandbox`, {
|
|
1191
|
+
method: 'POST',
|
|
1192
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1193
|
+
body: JSON.stringify({ code: `const { execSync } = require("child_process"); try { const out = execSync("npx total-recall ${args.join(' ')} 2>&1", { encoding: "utf-8" }); console.log(JSON.stringify({ success: true, message: out })); } catch(e) { console.log(JSON.stringify({ success: false, message: e.stdout || e.message })); }`, timeout: 60000 }),
|
|
1194
|
+
})
|
|
1195
|
+
if (!res.ok) throw new Error(`OKF export error: ${res.status}`)
|
|
1196
|
+
return res.json()
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
// ─── OpenWiki APIs ─────────────────────────────────────────────────────────────
|
|
1200
|
+
|
|
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
|
+
})
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
// UCW export/import/validate handled via runSandbox + @ssss/cli (SSSS §16)
|
|
1285
|
+
|
|
1286
|
+
export async function fetchVaultStatus(): Promise<{ totalNodes: number; categories: Record<string, number>; embeddings: number; lastCompiled: string }> {
|
|
1287
|
+
const res = await apiFetch(`${API_BASE}/api/vault/status`)
|
|
1288
|
+
if (!res.ok) throw new Error(`Vault status error: ${res.status}`)
|
|
1289
|
+
return res.json()
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
export async function fetchMemoryStats(): Promise<{ total: number; byCategory: Record<string, number>; byPriority: Record<string, number>; byStatus: Record<string, number> }> {
|
|
1293
|
+
const res = await apiFetch(`${API_BASE}/api/memory/stats`)
|
|
1294
|
+
if (!res.ok) throw new Error(`Memory stats error: ${res.status}`)
|
|
1295
|
+
return res.json()
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
// ─── Design Docs APIs ──────────────────────────────────────────────────────────
|
|
1299
|
+
|
|
1300
|
+
export async function fetchDesignDocs(): Promise<Array<{ name: string; path: string; size: number; modified: string; category: string }>> {
|
|
1301
|
+
const res = await apiFetch(`${API_BASE}/api/files?path=docs`)
|
|
1302
|
+
if (!res.ok) throw new Error(`Design docs error: ${res.status}`)
|
|
1303
|
+
return res.json()
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
export async function fetchDesignDocContent(path: string): Promise<{ content: string }> {
|
|
1307
|
+
const res = await apiFetch(`${API_BASE}/api/files?path=${encodeURIComponent(path)}&content=true`)
|
|
1308
|
+
if (!res.ok) throw new Error(`Design doc content error: ${res.status}`)
|
|
1309
|
+
return res.json()
|
|
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
|
+
}
|