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
|
@@ -1,85 +1,158 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* Validated write adapter — routes memory mutations through the SSSS §6
|
|
5
|
-
* Operation Contract (processOperation) instead of bypassing it via raw
|
|
6
|
-
* writeNode(). This ensures every write gets:
|
|
7
|
-
* - Envelope validation (Stage 1)
|
|
8
|
-
* - Idempotency deduplication (Stage 2)
|
|
9
|
-
* - Authorization checks (Stage 3)
|
|
10
|
-
* - Lease protection (Stage 4)
|
|
11
|
-
* - Schema validation (Stage 5)
|
|
12
|
-
* - Atomic commit (Stage 6)
|
|
13
|
-
* - Audit trail (Stage 7)
|
|
2
|
+
* Validated write adapter — all vault node mutations go through the SSSS 0.9
|
|
3
|
+
* package kernel (processOperationAsync).
|
|
14
4
|
*
|
|
15
5
|
* Usage:
|
|
16
|
-
*
|
|
17
|
-
* const result = writeNodeValidated(node, vaultDir);
|
|
6
|
+
* const result = await writeNodeValidatedAsync(node, vaultDir);
|
|
18
7
|
* if (!result.success) throw new Error(result.validation.errors.join('; '));
|
|
19
8
|
*/
|
|
20
9
|
|
|
21
10
|
import crypto from 'node:crypto';
|
|
22
|
-
import
|
|
23
|
-
import matter from 'gray-matter';
|
|
24
|
-
import { processOperation } from './operation-validator.mjs';
|
|
11
|
+
import { processOperationAsync } from './operation-validator.mjs';
|
|
25
12
|
import { safeStringify } from './vault.mjs';
|
|
26
13
|
import { invalidate } from './vault-cache.mjs';
|
|
27
14
|
|
|
15
|
+
const SAFE_NAME = /^[a-zA-Z0-9_-]+$/;
|
|
16
|
+
|
|
28
17
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* @param {object} node - The memory node object (slug, category, title, body, etc.)
|
|
32
|
-
* @param {string} vaultDir - Absolute path to the vault directory
|
|
33
|
-
* @param {object} [options]
|
|
34
|
-
* @param {string} [options.agentRole='admin'] - Agent authorization role
|
|
35
|
-
* @param {string} [options.workspaceId='default'] - Workspace scope
|
|
36
|
-
* @param {string} [options.idempotencyKey] - Idempotency key (auto-generated if omitted)
|
|
37
|
-
* @param {string} [options.leaseStore] - Path to lease store directory
|
|
38
|
-
* @param {string} [options.eventLogDir] - Path to event log directory
|
|
39
|
-
* @param {boolean} [options.dryRun=false] - Validate without committing
|
|
40
|
-
* @returns {object} Operation response (§6.4)
|
|
18
|
+
* Normalize a vault node so package universal frontmatter + TR memory v2 pass.
|
|
41
19
|
*/
|
|
42
|
-
export function
|
|
20
|
+
export function prepareNodeForContract(node = {}) {
|
|
21
|
+
const prepared = { ...node };
|
|
22
|
+
if (!prepared.type) prepared.type = 'memory';
|
|
23
|
+
|
|
24
|
+
// Identity fallbacks
|
|
25
|
+
if (!prepared.slug) {
|
|
26
|
+
prepared.slug = prepared.proposal_id || prepared.migration_id || prepared.release_id || 'unnamed';
|
|
27
|
+
}
|
|
28
|
+
// Proposal "category" is a topic label in TR; vault folder is always proposals/.
|
|
29
|
+
if (prepared.type === 'proposal') {
|
|
30
|
+
if (prepared.category && prepared.category !== 'proposals') {
|
|
31
|
+
prepared.proposal_topic = prepared.proposal_topic || prepared.category;
|
|
32
|
+
}
|
|
33
|
+
prepared.category = 'proposals';
|
|
34
|
+
} else if (!prepared.category) {
|
|
35
|
+
if (prepared.type === 'migration') prepared.category = 'migrations';
|
|
36
|
+
else if (prepared.type === 'schema-proposal') prepared.category = 'schema-proposals';
|
|
37
|
+
else prepared.category = 'uncategorized';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Package 0.9 universal fields
|
|
41
|
+
if (!prepared.title) {
|
|
42
|
+
prepared.title = prepared.name || prepared.summary || prepared.slug || 'Untitled';
|
|
43
|
+
}
|
|
44
|
+
if (!prepared.description) {
|
|
45
|
+
prepared.description = prepared.rationale || prepared.summary || prepared.title;
|
|
46
|
+
}
|
|
47
|
+
if (!prepared.timestamp) {
|
|
48
|
+
prepared.timestamp = prepared.updated || prepared.created || prepared.proposed_at
|
|
49
|
+
|| prepared.applied_at || new Date().toISOString();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (prepared.type === 'memory') {
|
|
53
|
+
if (!prepared.schema_version) prepared.schema_version = 2;
|
|
54
|
+
if (!prepared.status) prepared.status = 'active';
|
|
55
|
+
if (!prepared.updated) prepared.updated = new Date().toISOString();
|
|
56
|
+
if (!prepared.last_accessed) prepared.last_accessed = new Date().toISOString();
|
|
57
|
+
if (!prepared.created) prepared.created = prepared.updated;
|
|
58
|
+
// Package core enum for memory.category is closed; remap host-only folders.
|
|
59
|
+
const MEMORY_CATEGORIES = new Set([
|
|
60
|
+
'invariants', 'patterns', 'anti-patterns', 'preferences',
|
|
61
|
+
'decisions', 'concepts', 'facts', 'lore',
|
|
62
|
+
]);
|
|
63
|
+
if (prepared.category && !MEMORY_CATEGORIES.has(prepared.category)) {
|
|
64
|
+
const original = prepared.category;
|
|
65
|
+
prepared.tags = Array.isArray(prepared.tags) ? [...prepared.tags] : [];
|
|
66
|
+
if (!prepared.tags.includes(`folder:${original}`)) prepared.tags.push(`folder:${original}`);
|
|
67
|
+
prepared.category = original === 'instructions' ? 'preferences' : 'facts';
|
|
68
|
+
}
|
|
69
|
+
// Fill schema v2 required fields so partial updates (graph linking) still validate.
|
|
70
|
+
if (prepared.schema_version === 2) {
|
|
71
|
+
if (prepared.confidence === undefined || prepared.confidence === null) prepared.confidence = 0.5;
|
|
72
|
+
if (prepared.importance === undefined || prepared.importance === null) prepared.importance = 3;
|
|
73
|
+
if (!prepared.modality) prepared.modality = 'descriptive';
|
|
74
|
+
if (!prepared.subject) prepared.subject = 'agent';
|
|
75
|
+
if (!prepared.predicate) prepared.predicate = 'know';
|
|
76
|
+
if (!prepared.object) prepared.object = prepared.slug || 'fact';
|
|
77
|
+
if (!prepared.sentiment_polarity) prepared.sentiment_polarity = 'descriptive';
|
|
78
|
+
if (!prepared.sentiment_target) prepared.sentiment_target = prepared.slug || 'memory';
|
|
79
|
+
}
|
|
80
|
+
const now = new Date().toISOString();
|
|
81
|
+
if (!prepared.x_temporal_context) {
|
|
82
|
+
prepared.x_temporal_context = prepared.updated || prepared.created || now;
|
|
83
|
+
}
|
|
84
|
+
if (!prepared.x_citations) {
|
|
85
|
+
prepared.x_citations = [{
|
|
86
|
+
source: prepared.source?.type || 'unknown',
|
|
87
|
+
title: prepared.title || 'Untitled Memory',
|
|
88
|
+
url: prepared.source?.session_id ? `session://${prepared.source.session_id}` : 'unknown',
|
|
89
|
+
published: prepared.x_temporal_context,
|
|
90
|
+
relevance: 1.0,
|
|
91
|
+
accessed: now,
|
|
92
|
+
}];
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return prepared;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function buildEnvelope(node, options = {}) {
|
|
43
100
|
const {
|
|
44
|
-
agentRole = 'admin',
|
|
45
101
|
workspaceId = 'default',
|
|
46
102
|
idempotencyKey,
|
|
47
|
-
leaseStore,
|
|
48
|
-
eventLogDir,
|
|
49
103
|
dryRun = false,
|
|
104
|
+
path: pathOverride,
|
|
50
105
|
} = options;
|
|
51
106
|
|
|
52
|
-
|
|
53
|
-
const { body, _filePath, ...frontmatter } =
|
|
54
|
-
if (!frontmatter.type) frontmatter.type = 'memory';
|
|
55
|
-
if (!frontmatter.schema_version) frontmatter.schema_version = 2;
|
|
56
|
-
if (!frontmatter.updated) frontmatter.updated = new Date().toISOString();
|
|
57
|
-
if (!frontmatter.last_accessed) frontmatter.last_accessed = new Date().toISOString();
|
|
107
|
+
const prepared = prepareNodeForContract(node);
|
|
108
|
+
const { body, _filePath, _layer, _filepath, ...frontmatter } = prepared;
|
|
58
109
|
|
|
59
|
-
|
|
110
|
+
if (!SAFE_NAME.test(String(frontmatter.slug))) {
|
|
111
|
+
throw new Error(`Invalid slug: ${frontmatter.slug}`);
|
|
112
|
+
}
|
|
113
|
+
if (!SAFE_NAME.test(String(frontmatter.category))) {
|
|
114
|
+
throw new Error(`Invalid category: ${frontmatter.category}`);
|
|
115
|
+
}
|
|
60
116
|
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
const slug = frontmatter.slug || 'unnamed';
|
|
64
|
-
const vfsPath = `${category}/${slug}.md`;
|
|
117
|
+
const content = safeStringify(body || '', frontmatter);
|
|
118
|
+
const vfsPath = pathOverride || `${frontmatter.category}/${frontmatter.slug}.md`;
|
|
65
119
|
|
|
66
|
-
|
|
120
|
+
return {
|
|
67
121
|
type: 'operation',
|
|
68
122
|
idempotency_key: idempotencyKey || crypto.randomUUID(),
|
|
69
123
|
path: vfsPath,
|
|
70
124
|
workspace_id: workspaceId,
|
|
71
125
|
content,
|
|
72
|
-
dry_run: dryRun,
|
|
126
|
+
dry_run: !!dryRun,
|
|
127
|
+
actor: { role: options.agentRole || 'system' },
|
|
73
128
|
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* @deprecated Use {@link writeNodeValidatedAsync}.
|
|
133
|
+
*/
|
|
134
|
+
export function writeNodeValidated(node, vaultDir, options = {}) {
|
|
135
|
+
return writeNodeValidatedAsync(node, vaultDir, options);
|
|
136
|
+
}
|
|
74
137
|
|
|
75
|
-
|
|
76
|
-
|
|
138
|
+
/**
|
|
139
|
+
* Write a node through the SSSS 0.9 package kernel.
|
|
140
|
+
*/
|
|
141
|
+
export async function writeNodeValidatedAsync(node, vaultDir, options = {}) {
|
|
142
|
+
const {
|
|
143
|
+
agentRole = 'system',
|
|
144
|
+
leaseStore,
|
|
145
|
+
eventLogDir,
|
|
146
|
+
dryRun = false,
|
|
147
|
+
} = options;
|
|
148
|
+
|
|
149
|
+
const envelope = buildEnvelope(node, { ...options, agentRole });
|
|
150
|
+
const result = await processOperationAsync(envelope, vaultDir, {
|
|
77
151
|
agentRole,
|
|
78
152
|
leaseStore,
|
|
79
153
|
eventLogDir,
|
|
80
154
|
});
|
|
81
155
|
|
|
82
|
-
// Invalidate vault cache on successful commit
|
|
83
156
|
if (result.success && !dryRun) {
|
|
84
157
|
invalidate(vaultDir);
|
|
85
158
|
}
|
|
@@ -88,13 +161,8 @@ export function writeNodeValidated(node, vaultDir, options = {}) {
|
|
|
88
161
|
}
|
|
89
162
|
|
|
90
163
|
/**
|
|
91
|
-
* Validate a node
|
|
92
|
-
* Useful for pre-flight checks on user input.
|
|
93
|
-
*
|
|
94
|
-
* @param {object} node - The memory node object
|
|
95
|
-
* @param {string} vaultDir - Absolute path to the vault directory
|
|
96
|
-
* @returns {object} Validation result with errors/warnings
|
|
164
|
+
* Validate a node without committing.
|
|
97
165
|
*/
|
|
98
166
|
export function validateNode(node, vaultDir) {
|
|
99
|
-
return
|
|
167
|
+
return writeNodeValidatedAsync(node, vaultDir, { dryRun: true });
|
|
100
168
|
}
|
package/src/core/vault.mjs
CHANGED
|
@@ -3,6 +3,8 @@ import path from 'path';
|
|
|
3
3
|
import matter from 'gray-matter';
|
|
4
4
|
import { logger } from './logger.mjs';
|
|
5
5
|
|
|
6
|
+
const SAFE_NAME = /^[a-zA-Z0-9_-]+$/;
|
|
7
|
+
|
|
6
8
|
/**
|
|
7
9
|
* Atomic file write using write-then-rename to prevent partial corruption.
|
|
8
10
|
*/
|
|
@@ -113,42 +115,27 @@ export function loadMergedNodes(globalVaultDir, projectVaultDir) {
|
|
|
113
115
|
|
|
114
116
|
|
|
115
117
|
/**
|
|
116
|
-
* Write a
|
|
118
|
+
* Write a vault node through the SSSS 0.9 Operation Contract (package kernel).
|
|
119
|
+
* Always async — callers must await.
|
|
120
|
+
*
|
|
121
|
+
* @param {object} node
|
|
122
|
+
* @param {string} vaultDir - Vault root (or scoped root such as proposals/)
|
|
123
|
+
* @param {object} [options]
|
|
124
|
+
* @returns {Promise<object>} Operation response
|
|
117
125
|
*/
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
126
|
+
export async function writeNode(node, vaultDir, options = {}) {
|
|
127
|
+
// Dynamic import avoids circular dependency with validated-write → vault.safeStringify
|
|
128
|
+
const { writeNodeValidatedAsync } = await import('./validated-write.mjs');
|
|
129
|
+
const result = await writeNodeValidatedAsync(node, vaultDir, {
|
|
130
|
+
agentRole: options.agentRole || 'system',
|
|
131
|
+
workspaceId: options.workspaceId || 'default',
|
|
132
|
+
...options,
|
|
133
|
+
});
|
|
134
|
+
if (!result.success) {
|
|
135
|
+
const errors = result.validation?.errors || [result.error || 'unknown validation failure'];
|
|
136
|
+
throw new Error(`writeNode contract failure: ${errors.join('; ')}`);
|
|
129
137
|
}
|
|
130
|
-
|
|
131
|
-
const { body, ...frontmatter } = node;
|
|
132
|
-
|
|
133
|
-
if (frontmatter.type === 'memory') {
|
|
134
|
-
const now = new Date().toISOString();
|
|
135
|
-
if (!frontmatter.x_temporal_context) {
|
|
136
|
-
frontmatter.x_temporal_context = frontmatter.updated || frontmatter.created || now;
|
|
137
|
-
}
|
|
138
|
-
if (!frontmatter.x_citations) {
|
|
139
|
-
frontmatter.x_citations = [{
|
|
140
|
-
source: frontmatter.source?.type || 'unknown',
|
|
141
|
-
title: frontmatter.title || 'Untitled Memory',
|
|
142
|
-
url: frontmatter.source?.session_id ? `session://${frontmatter.source.session_id}` : 'unknown',
|
|
143
|
-
published: frontmatter.x_temporal_context,
|
|
144
|
-
relevance: 1.0,
|
|
145
|
-
accessed: now
|
|
146
|
-
}];
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const raw = safeStringify(body || '', frontmatter);
|
|
151
|
-
atomicWrite(filePath, raw);
|
|
138
|
+
return result;
|
|
152
139
|
}
|
|
153
140
|
|
|
154
141
|
/**
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebAuthn / passkey credential store for Total Recall dashboard.
|
|
3
|
+
*
|
|
4
|
+
* Credentials live under <brain>/config/webauthn.json (mode 0600).
|
|
5
|
+
* Challenges are in-memory only (short TTL).
|
|
6
|
+
* Never store secret values here — only authenticator public keys.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import crypto from 'node:crypto';
|
|
12
|
+
import {
|
|
13
|
+
generateRegistrationOptions,
|
|
14
|
+
verifyRegistrationResponse,
|
|
15
|
+
generateAuthenticationOptions,
|
|
16
|
+
verifyAuthenticationResponse,
|
|
17
|
+
} from '@simplewebauthn/server';
|
|
18
|
+
|
|
19
|
+
const CHALLENGE_TTL_MS = 5 * 60 * 1000;
|
|
20
|
+
/** @type {Map<string, { challenge: string, type: string, exp: number }>} */
|
|
21
|
+
const challenges = new Map();
|
|
22
|
+
|
|
23
|
+
const DASHBOARD_USER = {
|
|
24
|
+
id: Buffer.from('total-recall-dashboard-owner'),
|
|
25
|
+
name: 'dashboard',
|
|
26
|
+
displayName: 'Total Recall Owner',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export function resolveWebAuthnPath(brainDir) {
|
|
30
|
+
return path.join(brainDir, 'config', 'webauthn.json');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function loadStore(brainDir) {
|
|
34
|
+
const filePath = resolveWebAuthnPath(brainDir);
|
|
35
|
+
if (!fs.existsSync(filePath)) {
|
|
36
|
+
return { version: 1, credentials: [] };
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
40
|
+
if (!data || !Array.isArray(data.credentials)) {
|
|
41
|
+
return { version: 1, credentials: [] };
|
|
42
|
+
}
|
|
43
|
+
return data;
|
|
44
|
+
} catch {
|
|
45
|
+
return { version: 1, credentials: [] };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function saveStore(brainDir, store) {
|
|
50
|
+
const filePath = resolveWebAuthnPath(brainDir);
|
|
51
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
52
|
+
fs.writeFileSync(filePath, JSON.stringify(store, null, 2) + '\n', {
|
|
53
|
+
encoding: 'utf8',
|
|
54
|
+
mode: 0o600,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function listPasskeys(brainDir) {
|
|
59
|
+
const store = loadStore(brainDir);
|
|
60
|
+
return store.credentials.map((c) => ({
|
|
61
|
+
id: c.id,
|
|
62
|
+
created_at: c.created_at || null,
|
|
63
|
+
transports: c.transports || [],
|
|
64
|
+
deviceType: c.deviceType || null,
|
|
65
|
+
backedUp: !!c.backedUp,
|
|
66
|
+
label: c.label || 'Passkey',
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function hasPasskeys(brainDir) {
|
|
71
|
+
return loadStore(brainDir).credentials.length > 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function putChallenge(key, challenge, type) {
|
|
75
|
+
challenges.set(key, { challenge, type, exp: Date.now() + CHALLENGE_TTL_MS });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function takeChallenge(key, type) {
|
|
79
|
+
const row = challenges.get(key);
|
|
80
|
+
challenges.delete(key);
|
|
81
|
+
if (!row) return null;
|
|
82
|
+
if (row.exp < Date.now()) return null;
|
|
83
|
+
if (type && row.type !== type) return null;
|
|
84
|
+
return row.challenge;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Derive rpID + origin from the request (localhost-safe).
|
|
89
|
+
*/
|
|
90
|
+
export function resolveRpFromRequest(req) {
|
|
91
|
+
const rawHost = String(req.get('x-forwarded-host') || req.get('host') || 'localhost:3000');
|
|
92
|
+
const host = rawHost.split(',')[0].trim();
|
|
93
|
+
let hostname = host.split(':')[0] || 'localhost';
|
|
94
|
+
// WebAuthn: 127.0.0.1 is awkward; prefer localhost
|
|
95
|
+
if (hostname === '127.0.0.1') hostname = 'localhost';
|
|
96
|
+
|
|
97
|
+
const protoHeader = req.get('x-forwarded-proto');
|
|
98
|
+
const proto =
|
|
99
|
+
protoHeader?.split(',')[0]?.trim() ||
|
|
100
|
+
(req.secure || process.env.NODE_ENV === 'production' ? 'https' : 'http');
|
|
101
|
+
|
|
102
|
+
// Rebuild origin; map 127.0.0.1 → localhost for WebAuthn origin match
|
|
103
|
+
let originHost = host.replace(/^127\.0\.0\.1/, 'localhost');
|
|
104
|
+
const origin = `${proto}://${originHost}`;
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
rpID: hostname,
|
|
108
|
+
rpName: 'Total Recall',
|
|
109
|
+
origin,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function challengeKey(req, kind) {
|
|
114
|
+
const sid = req.auth?.type || 'anon';
|
|
115
|
+
return `${kind}:${sid}:${req.ip || 'local'}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* @param {string} brainDir
|
|
120
|
+
* @param {import('express').Request} req
|
|
121
|
+
*/
|
|
122
|
+
export async function beginRegistration(brainDir, req) {
|
|
123
|
+
const { rpID, rpName } = resolveRpFromRequest(req);
|
|
124
|
+
const store = loadStore(brainDir);
|
|
125
|
+
const options = await generateRegistrationOptions({
|
|
126
|
+
rpName,
|
|
127
|
+
rpID,
|
|
128
|
+
userName: DASHBOARD_USER.name,
|
|
129
|
+
userDisplayName: DASHBOARD_USER.displayName,
|
|
130
|
+
userID: DASHBOARD_USER.id,
|
|
131
|
+
attestationType: 'none',
|
|
132
|
+
excludeCredentials: store.credentials.map((c) => ({
|
|
133
|
+
id: c.id,
|
|
134
|
+
transports: c.transports,
|
|
135
|
+
})),
|
|
136
|
+
authenticatorSelection: {
|
|
137
|
+
residentKey: 'preferred',
|
|
138
|
+
userVerification: 'preferred',
|
|
139
|
+
authenticatorAttachment: 'platform',
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
putChallenge(challengeKey(req, 'reg'), options.challenge, 'reg');
|
|
143
|
+
return options;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* @param {string} brainDir
|
|
148
|
+
* @param {import('express').Request} req
|
|
149
|
+
* @param {object} response - browser registration response JSON
|
|
150
|
+
* @param {{ label?: string }} opts
|
|
151
|
+
*/
|
|
152
|
+
export async function finishRegistration(brainDir, req, response, opts = {}) {
|
|
153
|
+
const expectedChallenge = takeChallenge(challengeKey(req, 'reg'), 'reg');
|
|
154
|
+
if (!expectedChallenge) {
|
|
155
|
+
throw new Error('Registration challenge expired or missing. Try again.');
|
|
156
|
+
}
|
|
157
|
+
const { rpID, origin } = resolveRpFromRequest(req);
|
|
158
|
+
const verification = await verifyRegistrationResponse({
|
|
159
|
+
response,
|
|
160
|
+
expectedChallenge,
|
|
161
|
+
expectedOrigin: origin,
|
|
162
|
+
expectedRPID: rpID,
|
|
163
|
+
requireUserVerification: false,
|
|
164
|
+
});
|
|
165
|
+
if (!verification.verified || !verification.registrationInfo) {
|
|
166
|
+
throw new Error('Passkey registration failed verification');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const { credential, credentialDeviceType, credentialBackedUp } = verification.registrationInfo;
|
|
170
|
+
const store = loadStore(brainDir);
|
|
171
|
+
const id = credential.id;
|
|
172
|
+
// Replace if same id re-registered
|
|
173
|
+
store.credentials = store.credentials.filter((c) => c.id !== id);
|
|
174
|
+
store.credentials.push({
|
|
175
|
+
id,
|
|
176
|
+
publicKey: Buffer.from(credential.publicKey).toString('base64url'),
|
|
177
|
+
counter: credential.counter ?? 0,
|
|
178
|
+
transports: response.response?.transports || credential.transports || [],
|
|
179
|
+
deviceType: credentialDeviceType,
|
|
180
|
+
backedUp: !!credentialBackedUp,
|
|
181
|
+
label: opts.label || 'Passkey',
|
|
182
|
+
created_at: new Date().toISOString(),
|
|
183
|
+
});
|
|
184
|
+
saveStore(brainDir, store);
|
|
185
|
+
return { id, verified: true };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* @param {string} brainDir
|
|
190
|
+
* @param {import('express').Request} req
|
|
191
|
+
*/
|
|
192
|
+
export async function beginAuthentication(brainDir, req) {
|
|
193
|
+
const store = loadStore(brainDir);
|
|
194
|
+
if (!store.credentials.length) {
|
|
195
|
+
throw new Error('No passkeys registered. Register a passkey first.');
|
|
196
|
+
}
|
|
197
|
+
const { rpID } = resolveRpFromRequest(req);
|
|
198
|
+
const options = await generateAuthenticationOptions({
|
|
199
|
+
rpID,
|
|
200
|
+
userVerification: 'preferred',
|
|
201
|
+
allowCredentials: store.credentials.map((c) => ({
|
|
202
|
+
id: c.id,
|
|
203
|
+
transports: c.transports,
|
|
204
|
+
})),
|
|
205
|
+
});
|
|
206
|
+
putChallenge(challengeKey(req, 'auth'), options.challenge, 'auth');
|
|
207
|
+
return options;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* @param {string} brainDir
|
|
212
|
+
* @param {import('express').Request} req
|
|
213
|
+
* @param {object} response
|
|
214
|
+
*/
|
|
215
|
+
export async function finishAuthentication(brainDir, req, response) {
|
|
216
|
+
const expectedChallenge = takeChallenge(challengeKey(req, 'auth'), 'auth');
|
|
217
|
+
if (!expectedChallenge) {
|
|
218
|
+
throw new Error('Authentication challenge expired or missing. Try again.');
|
|
219
|
+
}
|
|
220
|
+
const store = loadStore(brainDir);
|
|
221
|
+
const credId = response?.id;
|
|
222
|
+
const stored = store.credentials.find((c) => c.id === credId);
|
|
223
|
+
if (!stored) {
|
|
224
|
+
throw new Error('Unknown passkey credential');
|
|
225
|
+
}
|
|
226
|
+
const { rpID, origin } = resolveRpFromRequest(req);
|
|
227
|
+
|
|
228
|
+
const verification = await verifyAuthenticationResponse({
|
|
229
|
+
response,
|
|
230
|
+
expectedChallenge,
|
|
231
|
+
expectedOrigin: origin,
|
|
232
|
+
expectedRPID: rpID,
|
|
233
|
+
credential: {
|
|
234
|
+
id: stored.id,
|
|
235
|
+
publicKey: Buffer.from(stored.publicKey, 'base64url'),
|
|
236
|
+
counter: stored.counter || 0,
|
|
237
|
+
transports: stored.transports,
|
|
238
|
+
},
|
|
239
|
+
requireUserVerification: false,
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
if (!verification.verified) {
|
|
243
|
+
throw new Error('Passkey authentication failed');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const newCounter = verification.authenticationInfo?.newCounter;
|
|
247
|
+
if (typeof newCounter === 'number') {
|
|
248
|
+
stored.counter = newCounter;
|
|
249
|
+
saveStore(brainDir, store);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
verified: true,
|
|
254
|
+
credentialId: stored.id,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function deletePasskey(brainDir, credentialId) {
|
|
259
|
+
const store = loadStore(brainDir);
|
|
260
|
+
const before = store.credentials.length;
|
|
261
|
+
store.credentials = store.credentials.filter((c) => c.id !== credentialId);
|
|
262
|
+
if (store.credentials.length === before) {
|
|
263
|
+
return { found: false };
|
|
264
|
+
}
|
|
265
|
+
saveStore(brainDir, store);
|
|
266
|
+
return { found: true, deleted: true };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** Test helper */
|
|
270
|
+
export function _clearChallengesForTests() {
|
|
271
|
+
challenges.clear();
|
|
272
|
+
}
|
package/src/server/api.mjs
CHANGED
|
@@ -176,7 +176,12 @@ function resolveRequestedModel(requestedModel, runtimeConfig) {
|
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
function ssssReferenceDir() {
|
|
179
|
-
|
|
179
|
+
const candidates = [
|
|
180
|
+
path.join(SKILLS_DIR, 'total-recall', 'references'),
|
|
181
|
+
path.join(SKILLS_DIR, 'total-recall', 'modules', 'ssss', 'references'),
|
|
182
|
+
path.join(SKILLS_DIR, 'total-recall', 'skills', 'tr-ssss', 'references'),
|
|
183
|
+
];
|
|
184
|
+
return candidates.find((p) => fs.existsSync(p)) || candidates[0];
|
|
180
185
|
}
|
|
181
186
|
|
|
182
187
|
function listSsssReferences(req) {
|
|
@@ -401,7 +406,7 @@ apiRouter.post('/v1/chat/completions', requireScope('chat:write'), async (req, r
|
|
|
401
406
|
|
|
402
407
|
|
|
403
408
|
const dateStr = new Date().toLocaleString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', timeZoneName: 'short' });
|
|
404
|
-
let baseSystemPrompt = `You are Total Recall
|
|
409
|
+
let baseSystemPrompt = `You are Total Recall — portable personal memory for any IDE. Your brain is a database-free, Markdown-first Structured Semantic Syntax System (SSSS) vault on disk. Core loop: write (remember/session) → dream (consolidate) → read (recall + compiled surfaces) → optional daemon tasks. The current date and time is ${dateStr}.
|
|
405
410
|
CRITICAL RULES — you have these tools and MUST use them:
|
|
406
411
|
|
|
407
412
|
BROWSER TOOLS (headless Chromium — use for web tasks):
|
|
@@ -510,10 +515,14 @@ You have a REAL browser AND full desktop control. Use them. Navigate, click, typ
|
|
|
510
515
|
baseSystemPrompt += `\n\n=== TIER 1 HOT MEMORY INSTRUCTIONS ===\n${compressInstructions(instructions)}`;
|
|
511
516
|
}
|
|
512
517
|
|
|
513
|
-
const
|
|
514
|
-
|
|
518
|
+
const ssssCandidates = [
|
|
519
|
+
path.join(AGENT_DIR, 'skills', 'total-recall', 'references', 'ssss-reference.md'),
|
|
520
|
+
path.join(AGENT_DIR, 'skills', 'total-recall', 'SKILL.md'),
|
|
521
|
+
];
|
|
522
|
+
const ssssPath = ssssCandidates.find((p) => fs.existsSync(p));
|
|
523
|
+
if (ssssPath) {
|
|
515
524
|
const ssssContent = fs.readFileSync(ssssPath, 'utf8');
|
|
516
|
-
baseSystemPrompt += `\n\n=== STRUCTURED SEMANTIC SYNTAX SYSTEM (SSSS) DOCUMENTATION ===\
|
|
525
|
+
baseSystemPrompt += `\n\n=== STRUCTURED SEMANTIC SYNTAX SYSTEM (SSSS) DOCUMENTATION ===\nPrefer @ssss/cli for mutations. Compact reference:\n${compressSsssSkill(ssssContent)}`;
|
|
517
526
|
}
|
|
518
527
|
|
|
519
528
|
// Load user profile — drives all personalisation and idle work
|
|
@@ -580,13 +589,14 @@ Use execute_code to call these endpoints with node's built-in fetch() — you ar
|
|
|
580
589
|
// No profile yet — enter interview mode
|
|
581
590
|
const interviewTask = fs.readFileSync(interviewTaskPath, 'utf8');
|
|
582
591
|
baseSystemPrompt += `\n\n=== ONBOARDING INTERVIEW MODE ===
|
|
583
|
-
This user has
|
|
584
|
-
|
|
585
|
-
Ask questions one at a time, warmly and conversationally.
|
|
586
|
-
After the interview, write
|
|
592
|
+
This user has no user-profile yet. Conduct the portable-memory onboarding interview before unrelated work.
|
|
593
|
+
Total Recall is personal memory for any IDE (write → dream → read → optional tasks) — not a vendor OS takeover.
|
|
594
|
+
Ask questions one at a time, warmly and conversationally. Reflect each answer before the next.
|
|
595
|
+
After the interview, write answers to:
|
|
587
596
|
- ${BRAIN_DIR}/memory-vault/preferences/user-profile.md
|
|
588
597
|
- ${BRAIN_DIR}/memory-vault/preferences/user-priorities.md
|
|
589
598
|
- ${BRAIN_DIR}/memory-vault/preferences/user-interests.md
|
|
599
|
+
Prefer CLI when available: npx total-recall remember preference "..." --tags user,profile,onboarding
|
|
590
600
|
Then mark ${interviewTaskPath} as status: done.
|
|
591
601
|
|
|
592
602
|
INTERVIEW TASK:
|
|
@@ -643,7 +653,7 @@ ${interviewTask}`;
|
|
|
643
653
|
// Overwrite system prompt to protect the CPU/memory footprint of the fallback model
|
|
644
654
|
const systemMsgIdx = currentMessages.findIndex(m => m.role === 'system');
|
|
645
655
|
if (systemMsgIdx !== -1) {
|
|
646
|
-
currentMessages[systemMsgIdx].content = `You are Total Recall
|
|
656
|
+
currentMessages[systemMsgIdx].content = `You are Total Recall — portable personal memory (Markdown vault on disk). The current date and time is ${dateStr}.
|
|
647
657
|
You are operating in a lightweight text-only mode. Answer the user's questions directly, concisely, and helpfully without using tools. Keep your responses short and complete.`;
|
|
648
658
|
}
|
|
649
659
|
|
|
@@ -913,20 +923,20 @@ apiRouter.get('/v1/chat/suggestions', requireAuth, requireScope('chat:read'), (r
|
|
|
913
923
|
const standardSuggestions = [
|
|
914
924
|
{
|
|
915
925
|
type: 'fact',
|
|
916
|
-
title: '🧠
|
|
917
|
-
text: '
|
|
926
|
+
title: '🧠 Memory loop',
|
|
927
|
+
text: 'Walk the portable memory loop: remember → dream → recall, and connect an IDE so surfaces stay in sync.',
|
|
918
928
|
nodes: []
|
|
919
929
|
},
|
|
920
930
|
{
|
|
921
931
|
type: 'concept',
|
|
922
|
-
title: '💡
|
|
923
|
-
text: 'What
|
|
932
|
+
title: '💡 Skills across any repo',
|
|
933
|
+
text: 'What skills should we track or deploy next? Hosts are equal — skill track any path via the registry.',
|
|
924
934
|
nodes: []
|
|
925
935
|
},
|
|
926
936
|
{
|
|
927
937
|
type: 'question',
|
|
928
|
-
title: '❓
|
|
929
|
-
text: '
|
|
938
|
+
title: '❓ Vault health',
|
|
939
|
+
text: 'Want a vault compile / dream pass so search indexes and instruction surfaces stay synchronized?',
|
|
930
940
|
nodes: []
|
|
931
941
|
}
|
|
932
942
|
];
|