total-recall-brain 3.1.0

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 (170) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +173 -0
  3. package/bin/antigravity.mjs +212 -0
  4. package/bin/init.mjs +36 -0
  5. package/bin/oci-sniper.sh +69 -0
  6. package/bin/total-recall.mjs +229 -0
  7. package/docs/ARCHITECTURE.md +251 -0
  8. package/docs/IDE_INTEGRATION.md +112 -0
  9. package/package.json +71 -0
  10. package/scaffold/.agent/skills/total-recall/SKILL.md +195 -0
  11. package/scaffold/.agent/skills/total-recall/evals/evals.json +20 -0
  12. package/scaffold/.agent/skills/total-recall/memory-vault/concepts/cli-help-reference.md +145 -0
  13. package/scaffold/.agent/skills/total-recall/memory-vault/invariants/operating-instructions.md +75 -0
  14. package/scaffold/.agent/skills/total-recall/memory-vault/preferences/always-websearch-gap.md +16 -0
  15. package/scaffold/.agent/skills/total-recall/memory-vault/preferences/topic-research-sop.md +17 -0
  16. package/scaffold/.agent/skills/total-recall/references/api-reference.md +187 -0
  17. package/scaffold/.agent/skills/total-recall/references/architecture-reference.md +81 -0
  18. package/scaffold/.agent/skills/total-recall/references/cli-reference.md +103 -0
  19. package/scaffold/.agent/skills/total-recall/references/ssss-reference.md +88 -0
  20. package/scaffold/.agent/skills/total-recall/references/troubleshooting.md +68 -0
  21. package/scaffold/.agent/skills/total-recall/scripts/sync-repo.mjs +83 -0
  22. package/scaffold/.agent/skills/total-recall/skills/cli-agents/SKILL.md +122 -0
  23. package/scaffold/.agent/skills/total-recall/skills/cli-agents/agents.yml +27 -0
  24. package/scaffold/.agent/skills/total-recall/skills/cli-agents/references/antigravity.txt +20 -0
  25. package/scaffold/.agent/skills/total-recall/skills/cli-agents/references/claude.txt +58 -0
  26. package/scaffold/.agent/skills/total-recall/skills/cli-agents/references/codex.txt +81 -0
  27. package/scaffold/.agent/skills/total-recall/skills/cli-agents/references/gemini.txt +44 -0
  28. package/scaffold/.agent/skills/total-recall/skills/research/SKILL.md +337 -0
  29. package/scaffold/.agent/skills/total-recall/skills/skill/SKILL.md +281 -0
  30. package/scaffold/.agent/skills/total-recall/skills/skill/assets/skill-template/SKILL.md +20 -0
  31. package/scaffold/.agent/skills/total-recall/skills/skill/evals/evals.json +20 -0
  32. package/scaffold/.agent/skills/total-recall/skills/skill/hooks/on-skill-add.sh +14 -0
  33. package/scaffold/.agent/skills/total-recall/skills/skill/hooks/on-skill-delete.sh +14 -0
  34. package/scaffold/.agent/skills/total-recall/skills/skill/learnings/.gitkeep +0 -0
  35. package/scaffold/.agent/skills/total-recall/skills/skill/references/.gitkeep +0 -0
  36. package/scaffold/.agent/skills/total-recall/skills/skill/scripts/.gitkeep +0 -0
  37. package/scaffold/.agent/skills/total-recall/skills/skill/scripts/create-skill.sh +65 -0
  38. package/scaffold/.agent/skills/total-recall/skills/skill/scripts/enforce-skill-optimization.mjs +135 -0
  39. package/scaffold/.agent/skills/total-recall/skills/skill/scripts/find-skills.mjs +124 -0
  40. package/scaffold/.agent/skills/total-recall/skills/skill/scripts/scan-skill.mjs +207 -0
  41. package/scaffold/.agent/skills/total-recall/skills/skill/scripts/skill-watcher.mjs +64 -0
  42. package/scaffold/.agent/skills/total-recall/skills/skill/subagents/audit-all-skills.md +62 -0
  43. package/scaffold/.agent/skills/total-recall/skills/ssss/SKILL.md +27 -0
  44. package/scaffold/.agent/skills/total-recall/skills/ssss/evals/evals.json +17 -0
  45. package/scaffold/.agent/skills/total-recall/skills/ssss/references/ssss-spec.md +117 -0
  46. package/scaffold/.agent/skills/total-recall/skills/ssss/scripts/validate-schema.mjs +54 -0
  47. package/scaffold/.agent/skills/total-recall/skills/ssss/subagents/audit-ssss-node.md +9 -0
  48. package/scaffold/.agent/skills/total-recall/subagents/total-recall-diagnostician.md +13 -0
  49. package/src/cli/agent-dir.mjs +105 -0
  50. package/src/cli/backup.mjs +313 -0
  51. package/src/cli/brain.mjs +224 -0
  52. package/src/cli/chat.mjs +74 -0
  53. package/src/cli/config.mjs +247 -0
  54. package/src/cli/connect.mjs +872 -0
  55. package/src/cli/daemon.mjs +228 -0
  56. package/src/cli/deploy-ui.mjs +1793 -0
  57. package/src/cli/deploy.mjs +1726 -0
  58. package/src/cli/doctor.mjs +254 -0
  59. package/src/cli/dream.mjs +80 -0
  60. package/src/cli/forget.mjs +92 -0
  61. package/src/cli/friction.mjs +21 -0
  62. package/src/cli/generate-pat.mjs +65 -0
  63. package/src/cli/hash-password.mjs +22 -0
  64. package/src/cli/help.mjs +153 -0
  65. package/src/cli/import-rules.mjs +102 -0
  66. package/src/cli/ingest/index.mjs +318 -0
  67. package/src/cli/ingest/parsers/chrome-bookmarks.mjs +238 -0
  68. package/src/cli/ingest/parsers/google-keep.mjs +147 -0
  69. package/src/cli/ingest/parsers/search-history.mjs +142 -0
  70. package/src/cli/ingest/parsers/youtube-history.mjs +181 -0
  71. package/src/cli/ingest/utils/dedup.mjs +69 -0
  72. package/src/cli/ingest/utils/takeout-walker.mjs +101 -0
  73. package/src/cli/ingest.mjs +102 -0
  74. package/src/cli/init.mjs +736 -0
  75. package/src/cli/integration-dispatcher.mjs +128 -0
  76. package/src/cli/lint.mjs +166 -0
  77. package/src/cli/map.mjs +194 -0
  78. package/src/cli/migrate.mjs +87 -0
  79. package/src/cli/rebuild.mjs +88 -0
  80. package/src/cli/recall.mjs +219 -0
  81. package/src/cli/relay.mjs +518 -0
  82. package/src/cli/remember.mjs +306 -0
  83. package/src/cli/research.mjs +381 -0
  84. package/src/cli/reset-password.mjs +75 -0
  85. package/src/cli/restore.mjs +135 -0
  86. package/src/cli/setup.mjs +616 -0
  87. package/src/cli/share.mjs +180 -0
  88. package/src/cli/skill.mjs +313 -0
  89. package/src/cli/snapshot.mjs +108 -0
  90. package/src/cli/start.mjs +63 -0
  91. package/src/cli/status.mjs +296 -0
  92. package/src/cli/sync.mjs +383 -0
  93. package/src/cli/uninstall.mjs +613 -0
  94. package/src/cli/upgrade.mjs +141 -0
  95. package/src/cli/wizard.html +3682 -0
  96. package/src/core/blackboard.mjs +54 -0
  97. package/src/core/clarity-rewriter.mjs +770 -0
  98. package/src/core/conclusion-writer.mjs +259 -0
  99. package/src/core/config.mjs +277 -0
  100. package/src/core/conflict-detector.mjs +536 -0
  101. package/src/core/crypto.mjs +107 -0
  102. package/src/core/daemon-control.mjs +158 -0
  103. package/src/core/daemon-loop.mjs +364 -0
  104. package/src/core/dream.mjs +347 -0
  105. package/src/core/drift-detector.mjs +143 -0
  106. package/src/core/embeddings.mjs +632 -0
  107. package/src/core/emergency-alerts.mjs +137 -0
  108. package/src/core/evolution.mjs +107 -0
  109. package/src/core/fact-seeker.mjs +1602 -0
  110. package/src/core/friction.mjs +95 -0
  111. package/src/core/import-rules.mjs +308 -0
  112. package/src/core/inference-engine.mjs +367 -0
  113. package/src/core/logger.mjs +75 -0
  114. package/src/core/memory-layers.mjs +96 -0
  115. package/src/core/migrate.mjs +116 -0
  116. package/src/core/notifications.mjs +77 -0
  117. package/src/core/operation-validator.mjs +272 -0
  118. package/src/core/optimizer.mjs +330 -0
  119. package/src/core/pattern_detector.mjs +113 -0
  120. package/src/core/post-mortem.mjs +413 -0
  121. package/src/core/protect-instructions.mjs +148 -0
  122. package/src/core/quick-capture.mjs +145 -0
  123. package/src/core/research-queue.mjs +431 -0
  124. package/src/core/research.mjs +461 -0
  125. package/src/core/runtime.mjs +515 -0
  126. package/src/core/sandbox.mjs +163 -0
  127. package/src/core/scheduler.mjs +569 -0
  128. package/src/core/schema.mjs +333 -0
  129. package/src/core/search.mjs +219 -0
  130. package/src/core/semantic-index.mjs +276 -0
  131. package/src/core/session-watcher.mjs +850 -0
  132. package/src/core/snapshot.mjs +133 -0
  133. package/src/core/source-adapters.mjs +1038 -0
  134. package/src/core/steering.mjs +137 -0
  135. package/src/core/surface.mjs +444 -0
  136. package/src/core/task_runner.mjs +125 -0
  137. package/src/core/tts.mjs +142 -0
  138. package/src/core/usage-tracker.mjs +349 -0
  139. package/src/core/vault-cache.mjs +76 -0
  140. package/src/core/vault.mjs +273 -0
  141. package/src/core/watchdog.mjs +236 -0
  142. package/src/server/api.mjs +944 -0
  143. package/src/server/auth.mjs +410 -0
  144. package/src/server/index.mjs +868 -0
  145. package/src/server/keys.mjs +181 -0
  146. package/src/server/rest.mjs +1652 -0
  147. package/src/server/route-manifest.json +77 -0
  148. package/src/server/routes/_shared.mjs +93 -0
  149. package/src/server/routes/keys.mjs +55 -0
  150. package/src/server/routes/memory.mjs +249 -0
  151. package/src/server/routes/sessions.mjs +336 -0
  152. package/src/server/routes/share.mjs +138 -0
  153. package/src/server/tools.mjs +618 -0
  154. package/templates/Caddyfile +37 -0
  155. package/templates/com.totalrecall.backup.plist +42 -0
  156. package/templates/com.totalrecall.daemon.plist +40 -0
  157. package/templates/com.totalrecall.duckdns.plist +23 -0
  158. package/templates/com.totalrecall.server.plist +46 -0
  159. package/templates/default-config/budget.yml +6 -0
  160. package/templates/default-config/runtime.yml +11 -0
  161. package/templates/default-config/security.yml +53 -0
  162. package/templates/default-config/voice.yml +34 -0
  163. package/templates/obsidian-queries/conflicts.md +26 -0
  164. package/templates/obsidian-queries/daily.md +17 -0
  165. package/templates/obsidian-queries/index.md +38 -0
  166. package/templates/obsidian-queries/skills.md +14 -0
  167. package/templates/onboarding-interview.md +72 -0
  168. package/templates/skill_engineering.md +40 -0
  169. package/templates/total-recall-daemon.service +21 -0
  170. package/templates/total-recall-server.service +20 -0
@@ -0,0 +1,259 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import matter from 'gray-matter';
4
+ import { callLocalRuntime } from './runtime.mjs';
5
+ import { atomicWrite, safeStringify } from './vault.mjs';
6
+ import { logger } from './logger.mjs';
7
+
8
+ /**
9
+ * Total Recall Conclusion Writer — Research → Active Validation Gate
10
+ *
11
+ * All research-derived facts arrive as status: draft in .agent/memory-inbox/pending/.
12
+ * Before promoting any draft to status: active, the Conclusion Writer asks the local LLM:
13
+ * 1. Is this fact self-consistent?
14
+ * 2. Does it contradict any existing active node?
15
+ * 3. Is the confidence appropriate given the source?
16
+ *
17
+ * If it passes, the node is promoted (status: active, moved to vault).
18
+ * If it fails, it gets quarantined with a reason record.
19
+ *
20
+ * This prevents research hallucinations from polluting working memory.
21
+ */
22
+
23
+ const VALIDATION_SYSTEM = `You are a Fact Validation Gate for a sovereign AI memory system called Total Recall.
24
+
25
+ You will be given a draft memory node and a list of related active nodes. Determine if the draft should be promoted to active memory.
26
+
27
+ Output valid JSON:
28
+ {
29
+ "verdict": "APPROVE" | "REJECT" | "NEEDS_REVISION",
30
+ "confidence_adjustment": -0.3 to +0.1,
31
+ "reasoning": "string",
32
+ "revision_instructions": "string (only if NEEDS_REVISION)"
33
+ }
34
+
35
+ Criteria for REJECT:
36
+ - Factual claim that contradicts an active node with higher confidence
37
+ - Circular or empty content (no actionable information)
38
+ - Obvious hallucination (specific version numbers without any evidence)
39
+ - Duplicate of an existing node
40
+
41
+ Criteria for NEEDS_REVISION:
42
+ - Directionally correct but confidence is too high for the evidence
43
+ - Title is misleading
44
+ - Body needs tightening
45
+
46
+ Criteria for APPROVE:
47
+ - Adds genuine new knowledge
48
+ - Does not contradict existing high-confidence nodes
49
+ - Confidence is appropriate for the evidence
50
+
51
+ Output ONLY valid JSON.`;
52
+
53
+ function buildValidationPrompt(draftNode, relatedActiveNodes) {
54
+ const draftText = [
55
+ `## Draft Node: ${draftNode.slug}`,
56
+ `Title: "${draftNode.title}"`,
57
+ `Category: ${draftNode.category} | Confidence: ${draftNode.confidence}`,
58
+ `Source: ${draftNode.source?.type || 'unknown'} / ${draftNode.source?.agent || 'unknown'}`,
59
+ '',
60
+ '## Body',
61
+ (draftNode.body || '').slice(0, 1200),
62
+ ].join('\n');
63
+
64
+ const relatedText = relatedActiveNodes.length > 0
65
+ ? '\n\n## Related Active Nodes\n' + relatedActiveNodes.map(n => [
66
+ `### [${n.slug}] ${n.title} (confidence: ${n.confidence})`,
67
+ (n.body || '').slice(0, 400),
68
+ ].join('\n')).join('\n\n')
69
+ : '\n\n## Related Active Nodes\n(none)';
70
+
71
+ return draftText + relatedText + '\n\nShould this draft be promoted to active memory?';
72
+ }
73
+
74
+ /**
75
+ * Find active nodes related to a draft (same category or shared tags).
76
+ */
77
+ function findRelatedNodes(draftNode, allActiveNodes) {
78
+ return allActiveNodes
79
+ .filter(n => {
80
+ if (n.category !== draftNode.category) return false;
81
+ const draftTags = new Set(draftNode.tags || []);
82
+ const sharedTags = (n.tags || []).filter(t => draftTags.has(t));
83
+ return sharedTags.length > 0;
84
+ })
85
+ .slice(0, 5); // Cap at 5 to keep prompt size manageable
86
+ }
87
+
88
+ /**
89
+ * Run the validation gate on a single draft node.
90
+ *
91
+ * @param {object} draftNode Parsed draft node object (includes _filepath)
92
+ * @param {object[]} allActiveNodes All currently active nodes from vault
93
+ * @param {object} opts
94
+ * @param {string} opts.vaultDir Path to memory vault
95
+ * @param {string} opts.quarantineDir Path to quarantine directory
96
+ * @param {object} opts.runtimeConfig Local LLM config
97
+ * @returns {{ verdict: string, promoted: boolean, error?: string }}
98
+ */
99
+ export async function validateDraftNode(draftNode, allActiveNodes, { vaultDir, quarantineDir, runtimeConfig }) {
100
+ const relatedNodes = findRelatedNodes(draftNode, allActiveNodes);
101
+ const prompt = buildValidationPrompt(draftNode, relatedNodes);
102
+
103
+ let result;
104
+ try {
105
+ const rawResponse = await callLocalRuntime(prompt, VALIDATION_SYSTEM, runtimeConfig);
106
+ const jsonMatch = rawResponse.match(/\{[\s\S]*\}/);
107
+ if (!jsonMatch) throw new Error('No JSON in response');
108
+ result = JSON.parse(jsonMatch[0]);
109
+ } catch (err) {
110
+ logger.info({
111
+ subsystem: 'conclusion-writer',
112
+ message: `Validation failed for ${draftNode.slug}: ${err.message}`,
113
+ });
114
+ return { verdict: 'SKIP', promoted: false, error: err.message };
115
+ }
116
+
117
+ const verdict = result.verdict || 'REJECT';
118
+ const confidenceAdj = Number(result.confidence_adjustment) || 0;
119
+
120
+ if (verdict === 'APPROVE') {
121
+ await promoteToVault(draftNode, confidenceAdj, vaultDir);
122
+ logger.info({
123
+ subsystem: 'conclusion-writer',
124
+ message: `APPROVED: ${draftNode.slug} → ${draftNode.category}/`,
125
+ });
126
+ return { verdict: 'APPROVE', promoted: true };
127
+ }
128
+
129
+ if (verdict === 'NEEDS_REVISION') {
130
+ await applyRevision(draftNode, confidenceAdj, result.revision_instructions);
131
+ logger.info({
132
+ subsystem: 'conclusion-writer',
133
+ message: `NEEDS_REVISION: ${draftNode.slug} — revised in-place`,
134
+ });
135
+ return { verdict: 'NEEDS_REVISION', promoted: false };
136
+ }
137
+
138
+ // REJECT → quarantine
139
+ await quarantineDraft(draftNode, result.reasoning || 'Rejected by validation gate', quarantineDir);
140
+ logger.info({
141
+ subsystem: 'conclusion-writer',
142
+ message: `REJECTED: ${draftNode.slug} — ${result.reasoning}`,
143
+ });
144
+ return { verdict: 'REJECT', promoted: false };
145
+ }
146
+
147
+ async function promoteToVault(draftNode, confidenceAdj, vaultDir) {
148
+ if (!draftNode._filepath) return;
149
+
150
+ const raw = fs.readFileSync(draftNode._filepath, 'utf8');
151
+ const { data, content } = matter(raw);
152
+
153
+ // Promote
154
+ data.status = 'active';
155
+ data.confidence = Math.min(1.0, Math.max(0.1, (data.confidence || 0.5) + confidenceAdj));
156
+ data.updated = new Date().toISOString();
157
+ data.x_promoted_at = new Date().toISOString();
158
+
159
+ // Compute target path in vault
160
+ const targetDir = path.join(vaultDir, data.category || 'concepts');
161
+ if (!fs.existsSync(targetDir)) {
162
+ fs.mkdirSync(targetDir, { recursive: true });
163
+ }
164
+
165
+ const targetPath = path.join(targetDir, `${data.slug}.md`);
166
+ atomicWrite(targetPath, safeStringify(content, data));
167
+
168
+ // Remove from inbox
169
+ try { fs.unlinkSync(draftNode._filepath); } catch { /* ignore */ }
170
+ }
171
+
172
+ async function applyRevision(draftNode, confidenceAdj, revisionInstructions) {
173
+ if (!draftNode._filepath || !revisionInstructions) return;
174
+
175
+ const raw = fs.readFileSync(draftNode._filepath, 'utf8');
176
+ const { data, content } = matter(raw);
177
+
178
+ data.confidence = Math.min(1.0, Math.max(0.1, (data.confidence || 0.5) + confidenceAdj));
179
+ data.x_revision_requested = revisionInstructions;
180
+ data.updated = new Date().toISOString();
181
+
182
+ atomicWrite(draftNode._filepath, safeStringify(content, data));
183
+ }
184
+
185
+ async function quarantineDraft(draftNode, reason, quarantineDir) {
186
+ if (!fs.existsSync(quarantineDir)) {
187
+ fs.mkdirSync(quarantineDir, { recursive: true });
188
+ }
189
+
190
+ if (!draftNode._filepath) return;
191
+
192
+ const raw = fs.readFileSync(draftNode._filepath, 'utf8');
193
+ const { data, content } = matter(raw);
194
+ data.status = 'quarantined';
195
+ data.x_quarantine_reason = reason;
196
+ data.x_quarantined_at = new Date().toISOString();
197
+
198
+ const targetPath = path.join(quarantineDir, `${data.slug}.md`);
199
+ atomicWrite(targetPath, safeStringify(content, data));
200
+
201
+ try { fs.unlinkSync(draftNode._filepath); } catch { /* ignore */ }
202
+ }
203
+
204
+ /**
205
+ * Run the validation gate over all draft nodes in the inbox.
206
+ *
207
+ * @param {object} opts
208
+ * @param {string} opts.inboxDir Path to pending inbox
209
+ * @param {string} opts.vaultDir Path to memory vault
210
+ * @param {string} opts.quarantineDir Path to quarantine
211
+ * @param {object} opts.runtimeConfig
212
+ * @returns {{ processed: number, approved: number, rejected: number }}
213
+ */
214
+ export async function runConclusionWriter({ inboxDir, vaultDir, quarantineDir, runtimeConfig }) {
215
+ if (!fs.existsSync(inboxDir)) {
216
+ return { processed: 0, approved: 0, rejected: 0, skipped: 'no-inbox' };
217
+ }
218
+
219
+ const files = fs.readdirSync(inboxDir).filter(f => f.endsWith('.md'));
220
+ if (files.length === 0) {
221
+ return { processed: 0, approved: 0, rejected: 0, skipped: 'empty-inbox' };
222
+ }
223
+
224
+ // Load all active vault nodes for comparison
225
+ const { loadNodes } = await import('./vault.mjs');
226
+ const allActiveNodes = loadNodes(vaultDir).filter(n => n.status === 'active');
227
+
228
+ let approved = 0;
229
+ let rejected = 0;
230
+
231
+ // Process up to 10 drafts per run to avoid excessive LLM calls
232
+ const batch = files.slice(0, 10);
233
+
234
+ for (const file of batch) {
235
+ const filepath = path.join(inboxDir, file);
236
+ try {
237
+ const raw = fs.readFileSync(filepath, 'utf8');
238
+ const { data, content } = matter(raw);
239
+ if (!data.slug || data.status !== 'draft') continue;
240
+
241
+ const draftNode = { ...data, body: content.trim(), _filepath: filepath };
242
+ const result = await validateDraftNode(draftNode, allActiveNodes, {
243
+ vaultDir,
244
+ quarantineDir,
245
+ runtimeConfig,
246
+ });
247
+
248
+ if (result.promoted) approved++;
249
+ else if (result.verdict === 'REJECT') rejected++;
250
+ } catch (err) {
251
+ logger.info({
252
+ subsystem: 'conclusion-writer',
253
+ message: `Error processing ${file}: ${err.message}`,
254
+ });
255
+ }
256
+ }
257
+
258
+ return { processed: batch.length, approved, rejected };
259
+ }
@@ -0,0 +1,277 @@
1
+ import { z } from 'zod';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import fs from 'fs';
5
+
6
+ // ─── Robust PATH-Expansion for LaunchAgents & background processes ─────────────
7
+ try {
8
+ const HOME = os.homedir();
9
+ const commonPaths = [
10
+ '/opt/homebrew/bin',
11
+ '/usr/local/bin',
12
+ path.join(HOME, '.local/bin'),
13
+ path.join(HOME, '.nvm/versions/node/v24.12.0/bin'),
14
+ path.join(HOME, '.npm-global/bin'),
15
+ path.join(HOME, 'bin'),
16
+ '/usr/bin',
17
+ '/bin',
18
+ '/usr/sbin',
19
+ '/sbin'
20
+ ];
21
+
22
+ const nvmVersionsDir = path.join(HOME, '.nvm', 'versions', 'node');
23
+ if (fs.existsSync(nvmVersionsDir)) {
24
+ const versions = fs.readdirSync(nvmVersionsDir);
25
+ for (const v of versions) {
26
+ commonPaths.unshift(path.join(nvmVersionsDir, v, 'bin'));
27
+ }
28
+ }
29
+
30
+ const activePaths = process.env.PATH ? process.env.PATH.split(path.delimiter) : [];
31
+ const uniquePaths = Array.from(new Set([...activePaths, ...commonPaths])).filter(p => fs.existsSync(p));
32
+ process.env.PATH = uniquePaths.join(path.delimiter);
33
+ } catch (e) {
34
+ // Silent fallback
35
+ }
36
+
37
+
38
+ /**
39
+ * Central Configuration Schema
40
+ * Uses Zod to validate and parse environment variables into a strongly typed system.
41
+ */
42
+ const configSchema = z.object({
43
+ agentDir: z.preprocess(
44
+ (val) => val || process.env._TR_TEST_AGENT_DIR || path.join(os.homedir(), '.agent'),
45
+ z.string()
46
+ ),
47
+ cliAgent: z.string().optional(),
48
+ cliModel: z.string().optional(),
49
+ cliTimeout: z.preprocess((val) => val === undefined || val === '' ? 300 : parseInt(String(val), 10), z.number().int().default(300)),
50
+ googleApiKey: z.string().optional(),
51
+ embedModel: z.string().default('gemini-embedding-2'),
52
+ searxngBaseUrl: z.string().optional(),
53
+ braveApiKey: z.string().optional(),
54
+ braveSearchApiKey: z.string().optional(),
55
+ exaApiKey: z.string().optional(),
56
+ githubToken: z.string().optional(),
57
+ serperApiKey: z.string().optional(),
58
+ tavilyApiKey: z.string().optional(),
59
+ dailySearchLimit: z.preprocess((val) => val === undefined || val === '' ? 50 : parseInt(String(val), 10), z.number().int().default(50)),
60
+ researchCooldownMs: z.preprocess((val) => val === undefined || val === '' ? 3600000 : parseInt(String(val), 10), z.number().int().default(3600000)),
61
+ sessionSecret: z.string().optional(),
62
+ nodeEnv: z.string().default('production'),
63
+ port: z.preprocess((val) => val === undefined || val === '' ? 3000 : parseInt(String(val), 10), z.number().int().default(3000)),
64
+ host: z.string().default('127.0.0.1'),
65
+ display: z.string().optional(),
66
+ totalRecallToken: z.string().optional(),
67
+ trBrain: z.string().optional(),
68
+ trPat: z.string().optional(),
69
+ xdgConfigHome: z.preprocess(
70
+ (val) => val || path.join(os.homedir(), '.config'),
71
+ z.string()
72
+ )
73
+ });
74
+
75
+ // Resolve brainDir paths
76
+ const resolvedAgentDir = process.env.AGENT_DIR || process.env._TR_TEST_AGENT_DIR || path.join(os.homedir(), '.agent');
77
+ const tempGlobalBrainDir = path.join(os.homedir(), '.agent', 'skills', 'total-recall');
78
+
79
+ // Auto-detect project brain if any
80
+ let projectBrainDir = null;
81
+ if (!process.env._TR_TEST_AGENT_DIR) {
82
+ let dir = process.cwd();
83
+ const homeDir = os.homedir();
84
+ while (dir !== path.dirname(dir)) {
85
+ if (dir === homeDir) break;
86
+ const candidate = path.join(dir, '.agent', 'skills', 'total-recall');
87
+ if (fs.existsSync(path.join(candidate, 'SKILL.md'))) {
88
+ projectBrainDir = candidate;
89
+ break;
90
+ }
91
+ dir = path.dirname(dir);
92
+ }
93
+ }
94
+
95
+ // Load secrets from project or global
96
+ let secrets = {};
97
+ try {
98
+ const pathsToCheck = [];
99
+ if (projectBrainDir) {
100
+ pathsToCheck.push(path.join(projectBrainDir, 'config', 'secrets.enc'));
101
+ }
102
+ pathsToCheck.push(path.join(tempGlobalBrainDir, 'config', 'secrets.enc'));
103
+ pathsToCheck.push(path.join(resolvedAgentDir, 'skills', 'total-recall', 'config', 'secrets.enc'));
104
+
105
+ for (const p of pathsToCheck) {
106
+ if (fs.existsSync(p)) {
107
+ try {
108
+ const parsed = JSON.parse(fs.readFileSync(p, 'utf8') || '{}');
109
+ if (Object.keys(parsed).length > 0) {
110
+ secrets = parsed;
111
+ break; // Found valid secrets!
112
+ }
113
+ } catch {}
114
+ }
115
+ }
116
+ } catch (e) {
117
+ // Ignore
118
+ }
119
+
120
+ // Capture raw configuration values from process.env and secrets.enc
121
+ const rawConfig = {
122
+ agentDir: process.env.AGENT_DIR,
123
+ cliAgent: process.env.TR_CLI_AGENT,
124
+ cliModel: process.env.TR_CLI_MODEL,
125
+ cliTimeout: process.env.TR_CLI_TIMEOUT,
126
+ googleApiKey: process.env.GOOGLE_API_KEY || secrets.google_api_key,
127
+ embedModel: process.env.TR_EMBED_MODEL,
128
+ searxngBaseUrl: process.env.SEARXNG_BASE_URL,
129
+ braveApiKey: process.env.BRAVE_SEARCH_API_KEY || process.env.BRAVE_API_KEY || secrets.brave_api_key,
130
+ braveSearchApiKey: process.env.BRAVE_SEARCH_API_KEY || secrets.brave_api_key,
131
+ exaApiKey: process.env.EXA_API_KEY || secrets.exa_api_key,
132
+ githubToken: process.env.GITHUB_TOKEN || secrets.github_token,
133
+ serperApiKey: process.env.SERPER_API_KEY || secrets.serper_api_key,
134
+ tavilyApiKey: process.env.TAVILY_API_KEY || secrets.tavily_api_key,
135
+ dailySearchLimit: process.env.TR_DAILY_SEARCH_LIMIT,
136
+ researchCooldownMs: process.env.RESEARCH_COOLDOWN_MS,
137
+ sessionSecret: process.env.SESSION_SECRET,
138
+ nodeEnv: process.env.NODE_ENV,
139
+ port: process.env.PORT,
140
+ host: process.env.HOST,
141
+ display: process.env.DISPLAY,
142
+ totalRecallToken: process.env.TOTAL_RECALL_TOKEN,
143
+ trBrain: process.env.TR_BRAIN,
144
+ trPat: process.env.TR_PAT,
145
+ xdgConfigHome: process.env.XDG_CONFIG_HOME
146
+ };
147
+
148
+ let validated;
149
+ try {
150
+ validated = configSchema.parse(rawConfig);
151
+ } catch (err) {
152
+ const issues = err.issues.map(i => `${i.path.join('.')}: ${i.message}`).join(', ');
153
+ throw new Error(`Configuration validation failed: ${issues}`);
154
+ }
155
+
156
+ const config = Object.freeze(validated);
157
+
158
+ export default config;
159
+
160
+ /**
161
+ * brainDir — The Total Recall meta-skill directory.
162
+ * This IS the user's brain. ALL user data (memory-vault, config, sessions,
163
+ * scheduler, memory-derived, memory-inbox, logs, .backups) lives inside here.
164
+ * Backup backs up this one directory and gets everything.
165
+ */
166
+ export const brainDir = path.join(config.agentDir, 'skills', 'total-recall');
167
+
168
+ export const {
169
+ agentDir,
170
+ cliAgent,
171
+ cliModel,
172
+ cliTimeout,
173
+ googleApiKey,
174
+ embedModel,
175
+ searxngBaseUrl,
176
+ braveApiKey,
177
+ braveSearchApiKey,
178
+ exaApiKey,
179
+ githubToken,
180
+ serperApiKey,
181
+ tavilyApiKey,
182
+ dailySearchLimit,
183
+ researchCooldownMs,
184
+ sessionSecret,
185
+ nodeEnv,
186
+ port,
187
+ host,
188
+ display,
189
+ totalRecallToken,
190
+ trBrain,
191
+ trPat,
192
+ xdgConfigHome
193
+ } = config;
194
+
195
+ export function getEnvVar(name) {
196
+ return process.env[name];
197
+ }
198
+
199
+ // ─── Layered Brain Resolution ───────────────────────────────────────────────
200
+
201
+ /**
202
+ * Global brain — always at ~/.agent/skills/total-recall/
203
+ * Holds identity: universal preferences, invariants, corrections, coding principles.
204
+ */
205
+ export const globalAgentDir = path.join(os.homedir(), '.agent');
206
+ export const globalBrainDir = path.join(globalAgentDir, 'skills', 'total-recall');
207
+
208
+ /**
209
+ * Detect a project-level brain by walking up from startDir looking for
210
+ * .agent/skills/total-recall/. Skips the home directory (that's the global brain).
211
+ *
212
+ * @param {string} [startDir=process.cwd()] - Directory to start searching from
213
+ * @returns {{ agentDir: string, brainDir: string, projectRoot: string } | null}
214
+ */
215
+ export function detectProjectBrain(startDir = process.cwd()) {
216
+ // In test mode, skip project detection
217
+ if (process.env._TR_TEST_AGENT_DIR) return null;
218
+
219
+ let dir = startDir;
220
+ const homeDir = os.homedir();
221
+ while (dir !== path.dirname(dir)) {
222
+ if (dir === homeDir) break; // don't detect global as project
223
+ const candidate = path.join(dir, '.agent', 'skills', 'total-recall');
224
+ if (fs.existsSync(path.join(candidate, 'SKILL.md'))) {
225
+ return {
226
+ agentDir: path.join(dir, '.agent'),
227
+ brainDir: candidate,
228
+ projectRoot: dir,
229
+ };
230
+ }
231
+ dir = path.dirname(dir);
232
+ }
233
+ return null;
234
+ }
235
+
236
+ /**
237
+ * Get both brain layers for the current context.
238
+ *
239
+ * @returns {{ global: { agentDir: string, brainDir: string, layer: 'global' },
240
+ * project: { agentDir: string, brainDir: string, projectRoot: string, layer: 'project' } | null }}
241
+ */
242
+ export function getActiveBrains() {
243
+ const global = { agentDir: globalAgentDir, brainDir: globalBrainDir, layer: 'global' };
244
+ const project = detectProjectBrain();
245
+ return {
246
+ global,
247
+ project: project ? { ...project, layer: 'project' } : null,
248
+ };
249
+ }
250
+
251
+ /**
252
+ * Resolve which brain to use for a given operation.
253
+ *
254
+ * @param {'global' | 'project' | 'auto'} layer - Explicit layer choice or auto-detect
255
+ * @param {string} [category] - Memory category (used for auto-detect heuristic)
256
+ * @returns {{ agentDir: string, brainDir: string, layer: 'global' | 'project' }}
257
+ */
258
+ export function resolveBrainLayer(layer = 'auto', category) {
259
+ const { global, project } = getActiveBrains();
260
+
261
+ if (layer === 'global') return global;
262
+ if (layer === 'project') {
263
+ if (!project) {
264
+ throw new Error('No project brain found. Run `npx total-recall init --project` to create one.');
265
+ }
266
+ return project;
267
+ }
268
+
269
+ // Auto-detect by category heuristic
270
+ if (project && category) {
271
+ const projectCategories = new Set(['fact', 'concept', 'pattern', 'anti-pattern', 'decision']);
272
+ if (projectCategories.has(category)) return project;
273
+ }
274
+
275
+ // Default: project if exists, else global
276
+ return project || global;
277
+ }