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,306 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import crypto from 'node:crypto';
4
+ import { resolveAgentDir, resolveBrainDir, parseLayerFlag, getBothBrains, defaultLayerForCategory } from './agent-dir.mjs';
5
+ import { compileSurface } from '../core/surface.mjs';
6
+ import { writeNode } from '../core/vault.mjs';
7
+
8
+ function printHelp() {
9
+ console.log(`
10
+ total-recall remember — Remember rules, preferences, corrections, and facts in real time
11
+
12
+ Usage: total-recall remember <category> "<content>" [options]
13
+
14
+ Categories:
15
+ invariant - Mandatory behavioral directive (appends to rules/invariants.md & writes to vault)
16
+ preference - User style preference (appends to rules/preferences.md & writes to vault)
17
+ correction - Learned correction (appends to rules/corrections.md & writes to vault)
18
+ fact - Factual memory node
19
+ concept - Domain concept node
20
+ pattern - Positive design/behavior pattern
21
+ anti-pattern - Negative design/behavior anti-pattern
22
+ decision - Architectural decision
23
+ lore - Backstory and system context
24
+
25
+ Options (SSSS v2 Frontmatter):
26
+ --tags, -t <list> Comma-separated list of tags (e.g. "config,server")
27
+ --importance, -i <1-5> Numerical importance level (default: 3)
28
+ --priority, -p <level> absolute | high | normal | low (default: normal)
29
+ --modality, -m <type> must | must_not | should | should_not | descriptive | preference
30
+ --confidence, -c <0-1> Confidence level (default: 1.0)
31
+ --slug <custom-slug> Define custom kebab-case slug
32
+ --title <custom-title> Define custom human-readable title
33
+ --status <state> active | draft | archived (default: active)
34
+ --subject <string> SPO Subject (default: system)
35
+ --predicate <string> SPO Predicate (default: remembers_fact)
36
+ --object <string> SPO Object (default: brain)
37
+ --related <list> Comma-separated list of related slugs
38
+ --expires <duration> TTL for temporary rules (e.g. "7d", "2w", "30d", "6h", "3m")
39
+
40
+ Examples:
41
+ npx total-recall remember invariant "Never run tsc directly." --importance 5 --priority absolute
42
+ npx total-recall remember preference "Always use single quotes." --tags "style,js" --modality preference
43
+ npx total-recall remember fact "The server runs on port 3000." --tags "config,port" --importance 4
44
+ npx total-recall remember fact "Uses Drizzle ORM" --project # Saves to project brain
45
+ npx total-recall remember invariant "No var" --global # Saves to global brain
46
+ `);
47
+ }
48
+
49
+ /**
50
+ * Parse a human-friendly duration string and return a Date in the future.
51
+ * Supported units: h (hours), d (days), w (weeks), m (months).
52
+ * E.g. "7d" → 7 days from now, "2w" → 14 days from now.
53
+ */
54
+ function parseDuration(str) {
55
+ if (!str || typeof str !== 'string') {
56
+ throw new Error(`Invalid duration: "${str}". Expected format like "7d", "2w", "30d", "6h", "3m".`);
57
+ }
58
+ const match = str.trim().match(/^(\d+)([hdwm])$/i);
59
+ if (!match) {
60
+ throw new Error(`Invalid duration: "${str}". Expected format like "7d", "2w", "30d", "6h", "3m".`);
61
+ }
62
+ const amount = parseInt(match[1], 10);
63
+ const unit = match[2].toLowerCase();
64
+ const date = new Date();
65
+ switch (unit) {
66
+ case 'h': date.setHours(date.getHours() + amount); break;
67
+ case 'd': date.setDate(date.getDate() + amount); break;
68
+ case 'w': date.setDate(date.getDate() + amount * 7); break;
69
+ case 'm': date.setMonth(date.getMonth() + amount); break;
70
+ default: throw new Error(`Unknown duration unit: "${unit}".`);
71
+ }
72
+ return date;
73
+ }
74
+
75
+ export default async function remember(args) {
76
+ // Parse layer flag first
77
+ const { layer: explicitLayer, remainingArgs: layerArgs } = parseLayerFlag(args);
78
+ const type = layerArgs[0];
79
+ const bodyContent = layerArgs[1];
80
+
81
+ if (!type || !bodyContent || type === '--help' || type === '-h') {
82
+ printHelp();
83
+ return;
84
+ }
85
+
86
+ const categoryMap = {
87
+ invariant: 'invariants',
88
+ preference: 'preferences',
89
+ correction: 'anti-patterns',
90
+ 'anti-pattern': 'anti-patterns',
91
+ pattern: 'patterns',
92
+ decision: 'decisions',
93
+ concept: 'concepts',
94
+ fact: 'facts',
95
+ lore: 'lore'
96
+ };
97
+
98
+ const category = categoryMap[type.toLowerCase()];
99
+
100
+ if (!category) {
101
+ console.error(`Invalid memory category: "${type}". Run total-recall remember --help for details.`);
102
+ process.exit(1);
103
+ }
104
+
105
+ // Resolve layer: explicit flag > category heuristic > auto-detect
106
+ let layer = explicitLayer;
107
+ if (layer === 'auto') {
108
+ layer = defaultLayerForCategory(category);
109
+ // Fall back to global if no project brain exists
110
+ const project = getBothBrains().project;
111
+ if (layer === 'project' && !project) {
112
+ layer = 'global';
113
+ }
114
+ }
115
+
116
+ // Parse Options
117
+ let tags = [];
118
+ let importance = 3;
119
+ let priority = 'normal';
120
+ let modality = 'descriptive';
121
+ let confidence = 1.0;
122
+ let slug = null;
123
+ let title = null;
124
+ let status = 'active';
125
+ let subject = 'system';
126
+ let predicate = 'remembers_fact';
127
+ let object = 'brain';
128
+ let related = [];
129
+ let expiresAt = null;
130
+
131
+ for (let i = 2; i < layerArgs.length; i++) {
132
+ const arg = layerArgs[i];
133
+ const val = layerArgs[i + 1];
134
+
135
+ if (arg === '--tags' || arg === '-t') {
136
+ if (val) {
137
+ tags = val.split(',').map(t => t.trim());
138
+ i++;
139
+ }
140
+ } else if (arg === '--importance' || arg === '-i') {
141
+ if (val) {
142
+ const num = parseInt(val, 10);
143
+ if (!isNaN(num)) importance = num;
144
+ i++;
145
+ }
146
+ } else if (arg === '--priority' || arg === '-p') {
147
+ if (val) {
148
+ priority = val.toLowerCase();
149
+ i++;
150
+ }
151
+ } else if (arg === '--modality' || arg === '-m') {
152
+ if (val) {
153
+ modality = val.toLowerCase();
154
+ i++;
155
+ }
156
+ } else if (arg === '--confidence' || arg === '-c') {
157
+ if (val) {
158
+ const num = parseFloat(val);
159
+ if (!isNaN(num)) confidence = num;
160
+ i++;
161
+ }
162
+ } else if (arg === '--slug') {
163
+ if (val) {
164
+ slug = val.trim();
165
+ i++;
166
+ }
167
+ } else if (arg === '--title') {
168
+ if (val) {
169
+ title = val.trim();
170
+ i++;
171
+ }
172
+ } else if (arg === '--status') {
173
+ if (val) {
174
+ status = val.toLowerCase();
175
+ i++;
176
+ }
177
+ } else if (arg === '--subject') {
178
+ if (val) {
179
+ subject = val.trim();
180
+ i++;
181
+ }
182
+ } else if (arg === '--predicate') {
183
+ if (val) {
184
+ predicate = val.trim();
185
+ i++;
186
+ }
187
+ } else if (arg === '--object') {
188
+ if (val) {
189
+ object = val.trim();
190
+ i++;
191
+ }
192
+ } else if (arg === '--related') {
193
+ if (val) {
194
+ related = val.split(',').map(r => r.trim());
195
+ i++;
196
+ }
197
+ } else if (arg === '--expires') {
198
+ if (val) {
199
+ try {
200
+ expiresAt = parseDuration(val).toISOString();
201
+ } catch (err) {
202
+ console.error(`❌ ${err.message}`);
203
+ process.exit(1);
204
+ }
205
+ i++;
206
+ }
207
+ }
208
+ }
209
+
210
+ const resolvedAgentDir = resolveAgentDir(layer);
211
+ const resolvedBrainDir = resolveBrainDir(layer);
212
+ const skillsDir = path.join(resolvedAgentDir, 'skills');
213
+ const vaultDir = path.join(resolvedBrainDir, 'memory-vault');
214
+ const derivedDir = path.join(resolvedBrainDir, 'memory-derived');
215
+ const instructionsFile = path.join(resolvedAgentDir, 'INSTRUCTIONS.md');
216
+ const layerLabel = layer === 'project' ? '[project]' : '[global]';
217
+
218
+ // Rule sheet mapping (for push rules projection)
219
+ const ruleFiles = {
220
+ invariant: { file: 'invariants.md' },
221
+ preference: { file: 'preferences.md' },
222
+ correction: { file: 'corrections.md' }
223
+ };
224
+
225
+ const ruleConfig = ruleFiles[type.toLowerCase()];
226
+
227
+ if (ruleConfig) {
228
+ const rulePath = path.join(skillsDir, 'total-recall', 'rules', ruleConfig.file);
229
+ if (fs.existsSync(rulePath)) {
230
+ let fileContent = fs.readFileSync(rulePath, 'utf8').trimEnd();
231
+ const normalizedBody = bodyContent.startsWith('-') ? bodyContent : `- ${bodyContent}`;
232
+ fileContent += `\n${normalizedBody}\n`;
233
+ fs.writeFileSync(rulePath, fileContent, 'utf8');
234
+ console.log(` ✅ Rule successfully appended to rules sheet: ${ruleConfig.file}`);
235
+ }
236
+ }
237
+
238
+ // Create individual SSSS v2 node in the vault
239
+ const finalSlug = slug || `${category}-${crypto.randomBytes(4).toString('hex')}`;
240
+ const truncatedContent = bodyContent.slice(0, 50) + (bodyContent.length > 50 ? '...' : '');
241
+ const finalTitle = title || `Self-captured memory: ${truncatedContent.replace(/\n/g, ' ')}`;
242
+
243
+ const now = new Date().toISOString();
244
+
245
+ // Construct schema-compliant SSSS node
246
+ const node = {
247
+ type: 'memory',
248
+ slug: finalSlug,
249
+ category,
250
+ title: finalTitle,
251
+ status,
252
+ confidence,
253
+ importance,
254
+ created: now,
255
+ updated: now,
256
+ last_accessed: now,
257
+ source: {
258
+ type: 'remember-cli',
259
+ session_id: process.env.TR_SESSION_ID || 'remember-session',
260
+ evidence_count: 1
261
+ },
262
+ supersedes: [],
263
+ superseded_by: null,
264
+ contradicts: [],
265
+ tags,
266
+ related,
267
+ routes_to_skills: [],
268
+ sentiment_polarity: type.toLowerCase() === 'preference' ? 'preference' : 'descriptive',
269
+ sentiment_target: subject,
270
+ modality,
271
+ subject,
272
+ predicate,
273
+ object,
274
+ decay: {
275
+ half_life_days: 180,
276
+ access_count: 1
277
+ },
278
+ schema_version: 2,
279
+ ...(expiresAt ? { expires_at: expiresAt } : {}),
280
+ x_temporal_context: now,
281
+ body: bodyContent
282
+ };
283
+
284
+ // Add absolute invariant properties if priority is absolute
285
+ if (priority === 'absolute') {
286
+ node.priority = 'absolute';
287
+ node.immutable = true;
288
+ }
289
+
290
+ writeNode(node, vaultDir);
291
+ console.log(` ✅ Permanent SSSS memory node created ${layerLabel} in vault: memory-vault/${category}/${finalSlug}.md`);
292
+
293
+ // Instantly recompile instructions and vector indexes in the exact same turn!
294
+ console.log(' ⏳ Recompiling active memory surfaces and indexes...');
295
+ try {
296
+ const compileResult = await compileSurface({
297
+ vaultDir,
298
+ skillsDir,
299
+ derivedDir,
300
+ instructionsFile
301
+ });
302
+ console.log(` ✅ Recompilation successful! Processed ${compileResult.nodesProcessed} SSSS nodes.`);
303
+ } catch (err) {
304
+ console.warn(` ⚠️ Memory saved, but surface recompilation failed: ${err.message}`);
305
+ }
306
+ }
@@ -0,0 +1,381 @@
1
+ /**
2
+ * src/cli/research.mjs
3
+ *
4
+ * Dedicated CLI subcommand for managing ongoing research projects.
5
+ * Usage:
6
+ * npx total-recall research list
7
+ * npx total-recall research add "<topic>" [--priority high|medium|low] [--notes "..."]
8
+ * npx total-recall research status
9
+ * npx total-recall research show <id-or-topic>
10
+ * npx total-recall research report <id-or-topic>
11
+ * npx total-recall research cancel <id>
12
+ */
13
+
14
+ import fs from 'node:fs';
15
+ import path from 'node:path';
16
+ import os from 'node:os';
17
+ import { resolveBrainDir, parseLayerFlag } from './agent-dir.mjs';
18
+ import {
19
+ listQueue,
20
+ addToQueue,
21
+ removeFromQueue,
22
+ loadQueue
23
+ } from '../core/research-queue.mjs';
24
+
25
+ // ANSI escape helpers
26
+ const bold = (s) => `\x1b[1m${s}\x1b[0m`;
27
+ const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
28
+ const green = (s) => `\x1b[32m${s}\x1b[0m`;
29
+ const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
30
+ const red = (s) => `\x1b[31m${s}\x1b[0m`;
31
+ const grey = (s) => `\x1b[90m${s}\x1b[0m`;
32
+ const boldCyan = (s) => `\x1b[1m\x1b[36m${s}\x1b[0m`;
33
+ const boldGreen = (s) => `\x1b[1m\x1b[32m${s}\x1b[0m`;
34
+ const boldYellow = (s) => `\x1b[1m\x1b[33m${s}\x1b[0m`;
35
+ const boldRed = (s) => `\x1b[1m\x1b[31m${s}\x1b[0m`;
36
+
37
+ function getStatusBadge(status) {
38
+ switch (status) {
39
+ case 'in_progress':
40
+ return boldCyan('[IN PROGRESS 🧪]');
41
+ case 'pending':
42
+ return boldYellow('[PENDING ⏳]');
43
+ case 'done':
44
+ return boldGreen('[COMPLETED ✅]');
45
+ case 'failed':
46
+ return boldRed('[FAILED ❌]');
47
+ default:
48
+ return bold(`[${status.toUpperCase()}]`);
49
+ }
50
+ }
51
+
52
+ function getPhaseLabel(phase) {
53
+ switch (phase) {
54
+ case 'acquisition': return 'Acquisition (1/5)';
55
+ case 'deliberation': return 'Deliberation (2/5)';
56
+ case 'improvement': return 'Refinement & Clarity (3/5)';
57
+ case 'monitoring': return 'Sources & Feeds (4/5)';
58
+ case 'expansion': return 'Tangent Expansion (5/5)';
59
+ default: return phase || 'unknown';
60
+ }
61
+ }
62
+
63
+ function timeSince(dateString) {
64
+ if (!dateString) return 'unknown';
65
+ const seconds = Math.floor((new Date() - new Date(dateString)) / 1000);
66
+ if (seconds < 60) return 'just now';
67
+ const minutes = Math.floor(seconds / 60);
68
+ if (minutes < 60) return `${minutes}m ago`;
69
+ const hours = Math.floor(minutes / 60);
70
+ if (hours < 24) return `${hours}h ago`;
71
+ const days = Math.floor(hours / 24);
72
+ return `${days}d ago`;
73
+ }
74
+
75
+ function printHeader() {
76
+ console.log(grey('┌─────────────────────────────────────────────────────────────┐'));
77
+ console.log(grey('│ ') + boldCyan('Total Recall — Active Autonomous Research Engine') + grey(' │'));
78
+ console.log(grey('└─────────────────────────────────────────────────────────────┘'));
79
+ }
80
+
81
+ function printUsage() {
82
+ console.log(`
83
+ Usage: total-recall research <command> [options]
84
+
85
+ Commands:
86
+ list [--status <pending|in_progress|done|failed>] [--query <text>]
87
+ List all research projects with status & phase (default)
88
+ add "<topic>" [--priority low|medium|high] [--notes "<text>"]
89
+ Queue a new research topic
90
+ status Summary counts of all research projects
91
+ show <id-or-topic> Show the current summary, conclusions, & directions
92
+ report <id-or-topic> Read the full research report from the vault
93
+ cancel <id> Cancel and remove a research project
94
+
95
+ Options:
96
+ --help, -h Show this help manual
97
+ `);
98
+ }
99
+
100
+ function findProject(idOrTopic, resolvedBrainDir) {
101
+ const queue = loadQueue(resolvedBrainDir);
102
+ // Try ID prefix match first
103
+ let match = queue.find(p => p.id.startsWith(idOrTopic));
104
+ if (!match) {
105
+ // Try case-insensitive topic match
106
+ const q = idOrTopic.toLowerCase();
107
+ match = queue.find(p => p.topic.toLowerCase().includes(q));
108
+ }
109
+ return match;
110
+ }
111
+
112
+ function getReportContent(nodeSlug, resolvedBrainDir) {
113
+ if (!nodeSlug) return null;
114
+ const vaultFile = path.join(resolvedBrainDir, 'memory-vault', 'facts', `${nodeSlug}.md`);
115
+ const inboxFile = path.join(resolvedBrainDir, 'memory-inbox', 'pending', `${nodeSlug}.md`);
116
+
117
+ if (fs.existsSync(vaultFile)) {
118
+ return { path: vaultFile, content: fs.readFileSync(vaultFile, 'utf8'), promoted: true };
119
+ } else if (fs.existsSync(inboxFile)) {
120
+ return { path: inboxFile, content: fs.readFileSync(inboxFile, 'utf8'), promoted: false };
121
+ }
122
+ return null;
123
+ }
124
+
125
+ // ─── Command Handlers ────────────────────────────────────────────────────────
126
+
127
+ function handleList(args, resolvedBrainDir) {
128
+ let status = 'all';
129
+ let query = '';
130
+
131
+ for (let i = 0; i < args.length; i++) {
132
+ if (args[i] === '--status') status = args[++i];
133
+ else if (args[i] === '--query') query = args[++i];
134
+ }
135
+
136
+ const { items, total } = listQueue({ status, query, brainDir: resolvedBrainDir });
137
+
138
+ printHeader();
139
+ if (items.length === 0) {
140
+ console.log(` No research projects found matching status: ${bold(status)}`);
141
+ console.log();
142
+ return;
143
+ }
144
+
145
+ console.log(` Displaying ${bold(items.length)} of ${bold(total)} research projects:`);
146
+ console.log();
147
+
148
+ for (const item of items) {
149
+ const badge = getStatusBadge(item.status);
150
+ const phase = getPhaseLabel(item.research_phase);
151
+ console.log(` ${badge} ${bold(item.topic)}`);
152
+ console.log(` ${grey('ID:')} ${item.id.slice(0, 8)}... | ${grey('Phase:')} ${cyan(phase)} | ${grey('Updated:')} ${timeSince(item.updated_at)}`);
153
+ if (item.summary?.conclusions && item.summary.conclusions.length > 0 && item.summary.conclusions[0] !== 'Initial facts gathered and indexed in the knowledge base.') {
154
+ console.log(` ${grey('↳')} ${item.summary.conclusions[0].slice(0, 100)}...`);
155
+ }
156
+ console.log();
157
+ }
158
+ }
159
+
160
+ function handleAdd(args, resolvedBrainDir) {
161
+ const topic = args[0];
162
+ if (!topic || topic.startsWith('--')) {
163
+ console.error(red(' Error: topic string is required.'));
164
+ printUsage();
165
+ process.exit(1);
166
+ }
167
+
168
+ let priority = 'medium';
169
+ let notes = '';
170
+
171
+ for (let i = 1; i < args.length; i++) {
172
+ if (args[i] === '--priority') priority = args[++i];
173
+ else if (args[i] === '--notes') notes = args[++i];
174
+ }
175
+
176
+ try {
177
+ const item = addToQueue({ topic, priority, notes, brainDir: resolvedBrainDir });
178
+ console.log();
179
+ console.log(` ${green('✅ Successfully enqueued research project:')}`);
180
+ console.log(` ${bold('Topic:')} "${item.topic}"`);
181
+ console.log(` ${bold('ID:')} ${item.id}`);
182
+ console.log(` ${bold('Priority:')} ${item.priority}`);
183
+ console.log();
184
+ } catch (err) {
185
+ console.error(red(` Error: ${err.message}`));
186
+ process.exit(1);
187
+ }
188
+ }
189
+
190
+ function handleStatus(resolvedBrainDir) {
191
+ const queue = loadQueue(resolvedBrainDir);
192
+ const counts = { pending: 0, in_progress: 0, done: 0, failed: 0 };
193
+ const phaseCounts = { acquisition: 0, deliberation: 0, improvement: 0, monitoring: 0, expansion: 0 };
194
+
195
+ for (const item of queue) {
196
+ if (counts[item.status] !== undefined) counts[item.status]++;
197
+ const phase = item.research_phase || 'acquisition';
198
+ if (phaseCounts[phase] !== undefined) phaseCounts[phase]++;
199
+ }
200
+
201
+ printHeader();
202
+ console.log(` ${bold('Overall Project Statistics:')}`);
203
+ console.log(` 🧪 ${boldCyan('In Progress:')} ${counts.in_progress}`);
204
+ console.log(` ⏳ ${boldYellow('Pending:')} ${counts.pending}`);
205
+ console.log(` ✅ ${boldGreen('Completed:')} ${counts.done}`);
206
+ console.log(` ❌ ${boldRed('Failed:')} ${counts.failed}`);
207
+ console.log();
208
+ console.log(` ${bold('Active Phase Breakdown:')}`);
209
+ console.log(` 1. ${grey('Acquisition:')} ${phaseCounts.acquisition} enqueued`);
210
+ console.log(` 2. ${grey('Deliberation:')} ${phaseCounts.deliberation} enqueued`);
211
+ console.log(` 3. ${grey('Refinement & Clarity:')} ${phaseCounts.improvement} enqueued`);
212
+ console.log(` 4. ${grey('Sources & Feeds:')} ${phaseCounts.monitoring} enqueued`);
213
+ console.log(` 5. ${grey('Tangent Expansion:')} ${phaseCounts.expansion} enqueued`);
214
+ console.log();
215
+ }
216
+
217
+ function handleShow(args, resolvedBrainDir) {
218
+ const query = args[0];
219
+ if (!query) {
220
+ console.error(red(' Error: project ID or topic string required.'));
221
+ printUsage();
222
+ process.exit(1);
223
+ }
224
+
225
+ const project = findProject(query, resolvedBrainDir);
226
+ if (!project) {
227
+ console.error(red(` Error: no research project found for query: "${query}"`));
228
+ process.exit(1);
229
+ }
230
+
231
+ printHeader();
232
+ console.log(` ${bold('Research Topic:')} ${boldCyan(`"${project.topic}"`)}`);
233
+ console.log(` ${grey('ID:')} ${project.id}`);
234
+ console.log(` ${grey('Status:')} ${getStatusBadge(project.status)}`);
235
+ console.log(` ${grey('Active Phase:')} ${cyan(getPhaseLabel(project.research_phase))}`);
236
+ console.log(` ${grey('Last Updated:')} ${timeSince(project.updated_at)} (${new Date(project.updated_at).toLocaleString()})`);
237
+ console.log();
238
+
239
+ const summary = project.summary;
240
+ if (!summary) {
241
+ console.log(` ${boldYellow('⚠️ No summary available yet for this project.')}`);
242
+ console.log();
243
+ return;
244
+ }
245
+
246
+ console.log(grey(' ───────────────────────────────────────────────────────────'));
247
+ console.log(` ${bold('🔑 Key Conclusions & Insights')}`);
248
+ if (summary.conclusions && summary.conclusions.length > 0) {
249
+ for (const c of summary.conclusions) {
250
+ console.log(` ✨ ${c}`);
251
+ }
252
+ } else {
253
+ console.log(` ${grey('(Pending phase completion to extract insights)')}`);
254
+ }
255
+ console.log();
256
+
257
+ console.log(` ${bold('🚀 Ongoing Directions & Gaps')}`);
258
+ if (summary.ongoing_directions && summary.ongoing_directions.length > 0) {
259
+ for (const d of summary.ongoing_directions) {
260
+ console.log(` ↳ ${d}`);
261
+ }
262
+ } else {
263
+ console.log(` ${grey('(Pending phase completion to extract future directions)')}`);
264
+ }
265
+ console.log();
266
+
267
+ if (project.node_slug) {
268
+ console.log(grey(' ───────────────────────────────────────────────────────────'));
269
+ console.log(` ${bold('Full Report Details:')}`);
270
+ console.log(` ${grey('Vault Node Slug:')} [[${project.node_slug}]]`);
271
+ console.log(` ${grey('Status in Vault:')} ${project.summary?.node_status === 'active' ? green('Promoted (Active Fact)') : yellow('Staged (Pending Validation)')}`);
272
+ console.log(` ${grey('Sources Consulted:')} ${summary.sources_count || 0} unique references`);
273
+ console.log();
274
+ console.log(` ${grey('To read the full raw markdown report, run:')}`);
275
+ console.log(` ${boldCyan(`npx total-recall research report ${project.id.slice(0, 8)}`)}`);
276
+ }
277
+ console.log();
278
+ }
279
+
280
+ function handleReport(args, resolvedBrainDir) {
281
+ const query = args[0];
282
+ if (!query) {
283
+ console.error(red(' Error: project ID or topic string required.'));
284
+ printUsage();
285
+ process.exit(1);
286
+ }
287
+
288
+ const project = findProject(query, resolvedBrainDir);
289
+ if (!project) {
290
+ console.error(red(` Error: no research project found for query: "${query}"`));
291
+ process.exit(1);
292
+ }
293
+
294
+ if (!project.node_slug) {
295
+ console.error(red(` Error: research project "${project.topic}" has not produced a report slug yet. Let the daemon run its acquisition cycle.`));
296
+ process.exit(1);
297
+ }
298
+
299
+ const report = getReportContent(project.node_slug, resolvedBrainDir);
300
+ if (!report) {
301
+ console.error(red(` Error: report file not found for slug [[${project.node_slug}]].`));
302
+ process.exit(1);
303
+ }
304
+
305
+ console.log(grey('┌─────────────────────────────────────────────────────────────┐'));
306
+ console.log(grey('│ ') + boldCyan(`Report: [[${project.node_slug}]]`) + grey(' '.repeat(Math.max(0, 43 - project.node_slug.length))) + grey('│'));
307
+ console.log(grey('│ ') + grey(`Location: ${report.path}`) + grey(' '.repeat(Math.max(0, 48 - report.path.length))) + grey('│'));
308
+ console.log(grey('│ ') + (report.promoted ? boldGreen('PROMOTED ACTIVE FACT') : boldYellow('PENDING VALIDATION DRAFT')) + grey(' '.repeat(report.promoted ? 38 : 34)) + grey('│'));
309
+ console.log(grey('└─────────────────────────────────────────────────────────────┘'));
310
+ console.log();
311
+ console.log(report.content);
312
+ console.log();
313
+ }
314
+
315
+ function handleCancel(args, resolvedBrainDir) {
316
+ const id = args[0];
317
+ if (!id) {
318
+ console.error(red(' Error: project ID is required.'));
319
+ printUsage();
320
+ process.exit(1);
321
+ }
322
+
323
+ try {
324
+ const result = removeFromQueue(id, resolvedBrainDir);
325
+ console.log();
326
+ console.log(` ${green('✅ Successfully cancelled and removed research project:')}`);
327
+ console.log(` ${bold('Topic:')} "${result.topic}"`);
328
+ console.log(` ${bold('ID:')} ${result.id}`);
329
+ console.log();
330
+ } catch (err) {
331
+ console.error(red(` Error: ${err.message}`));
332
+ process.exit(1);
333
+ }
334
+ }
335
+
336
+ // ─── CLI Entrypoint ──────────────────────────────────────────────────────────
337
+
338
+ export default async function research(args) {
339
+ // Parse layer flag from args
340
+ const { layer, remainingArgs } = parseLayerFlag(args);
341
+ const resolvedBrainDir = resolveBrainDir(layer);
342
+ const cmd = remainingArgs[0] || 'list';
343
+
344
+ if (cmd === '--help' || cmd === '-h' || cmd === 'help') {
345
+ printHeader();
346
+ printUsage();
347
+ return;
348
+ }
349
+
350
+ const subArgs = remainingArgs.slice(1);
351
+
352
+ switch (cmd) {
353
+ case 'list':
354
+ handleList(subArgs, resolvedBrainDir);
355
+ break;
356
+ case 'add':
357
+ handleAdd(subArgs, resolvedBrainDir);
358
+ break;
359
+ case 'status':
360
+ handleStatus(resolvedBrainDir);
361
+ break;
362
+ case 'show':
363
+ handleShow(subArgs, resolvedBrainDir);
364
+ break;
365
+ case 'report':
366
+ handleReport(subArgs, resolvedBrainDir);
367
+ break;
368
+ case 'cancel':
369
+ handleCancel(subArgs, resolvedBrainDir);
370
+ break;
371
+ default:
372
+ const matched = findProject(cmd, resolvedBrainDir);
373
+ if (matched) {
374
+ handleShow([cmd, ...subArgs], resolvedBrainDir);
375
+ } else {
376
+ console.error(red(` Error: unknown research subcommand: "${cmd}"`));
377
+ printUsage();
378
+ process.exit(1);
379
+ }
380
+ }
381
+ }