total-recall-brain 3.18.3 → 3.19.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 (50) hide show
  1. package/frontend/dist/assets/{index-CGqvdwHT.js → index-B6AfuKv9.js} +115 -115
  2. package/frontend/dist/index.html +1 -1
  3. package/frontend/src/api/update.ts +29 -4
  4. package/frontend/src/pages/HealthPage.tsx +12 -16
  5. package/frontend/src/pages/SettingsPage.tsx +22 -1
  6. package/package.json +1 -1
  7. package/src/cli/agent-dir.mjs +15 -1
  8. package/src/cli/chat.mjs +1 -1
  9. package/src/cli/daemon.mjs +2 -1
  10. package/src/cli/dream.mjs +14 -10
  11. package/src/cli/forget.mjs +14 -2
  12. package/src/cli/friction.mjs +1 -1
  13. package/src/cli/import-rules.mjs +8 -3
  14. package/src/cli/ingest.mjs +3 -2
  15. package/src/cli/lint.mjs +9 -6
  16. package/src/cli/rebuild.mjs +9 -4
  17. package/src/cli/remember.mjs +21 -15
  18. package/src/cli/secret.mjs +4 -1
  19. package/src/cli/share.mjs +5 -3
  20. package/src/cli/skill-projection.mjs +21 -2
  21. package/src/cli/snapshot.mjs +10 -8
  22. package/src/cli/update.mjs +2 -1
  23. package/src/core/clarity-rewriter.mjs +31 -12
  24. package/src/core/config.mjs +10 -4
  25. package/src/core/daemon-loop.mjs +2 -1
  26. package/src/core/embeddings.mjs +51 -3
  27. package/src/core/memory-title.mjs +70 -0
  28. package/src/core/runtime.mjs +11 -1
  29. package/src/core/secrets-store.mjs +7 -1
  30. package/src/core/skills-registry.mjs +12 -0
  31. package/src/core/surface.mjs +4 -4
  32. package/src/core/throttled-fetch.mjs +5 -0
  33. package/src/core/vault.mjs +7 -1
  34. package/src/server/index.mjs +56 -0
  35. package/src/server/route-manifest.json +16 -0
  36. package/src/server/routes/_shared.mjs +33 -1
  37. package/src/server/routes/context.mjs +14 -9
  38. package/src/server/routes/dashboard.mjs +39 -18
  39. package/src/server/routes/docs.mjs +27 -13
  40. package/src/server/routes/dream.mjs +10 -11
  41. package/src/server/routes/export.mjs +43 -12
  42. package/src/server/routes/field.mjs +11 -9
  43. package/src/server/routes/graph.mjs +31 -13
  44. package/src/server/routes/import.mjs +35 -16
  45. package/src/server/routes/instructions.mjs +41 -5
  46. package/src/server/routes/memory.mjs +61 -21
  47. package/src/server/routes/share.mjs +2 -1
  48. package/src/server/routes/ssss.mjs +11 -3
  49. package/src/server/routes/update.mjs +22 -3
  50. package/src/server/routes/vault.mjs +53 -28
@@ -17,7 +17,7 @@
17
17
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
18
18
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
19
19
  <title>Total Recall — Portable Memory</title>
20
- <script type="module" crossorigin src="/assets/index-CGqvdwHT.js"></script>
20
+ <script type="module" crossorigin src="/assets/index-B6AfuKv9.js"></script>
21
21
  <link rel="stylesheet" crossorigin href="/assets/index-B4RASWbV.css">
22
22
  </head>
23
23
  <body>
@@ -25,8 +25,33 @@ export async function checkUpdate(): Promise<UpdateCheckResult> {
25
25
  }
26
26
  }
27
27
 
28
- export async function runUpdate(): Promise<{ success: boolean; message: string }> {
29
- const res = await apiFetch(`${API_BASE}/api/update/run`, { method: 'POST' })
30
- if (!res.ok) throw new Error(`Run update API error: ${res.status}`)
31
- return res.json()
28
+ export async function runUpdate(): Promise<{
29
+ success: boolean
30
+ message: string
31
+ summary?: {
32
+ latest?: string
33
+ updated?: number
34
+ failed?: number
35
+ up_to_date?: number
36
+ results?: { name: string; status: string; error?: string | null }[]
37
+ }
38
+ }> {
39
+ const res = await apiFetch(`${API_BASE}/api/update/run`, {
40
+ method: 'POST',
41
+ headers: { 'Content-Type': 'application/json' },
42
+ body: JSON.stringify({ force: true }),
43
+ })
44
+ const data = await res.json().catch(() => ({}))
45
+ if (!res.ok) {
46
+ throw new Error(data.error || data.message || `Run update API error: ${res.status}`)
47
+ }
48
+ // API historically omitted success — treat finished-with-0-failures as success
49
+ const failed = Number(data.summary?.failed ?? 0)
50
+ const success =
51
+ typeof data.success === 'boolean' ? data.success : failed === 0 && data.summary?.skipped !== true
52
+ return {
53
+ success,
54
+ message: data.message || (success ? 'Package auto-update finished' : 'Package auto-update failed'),
55
+ summary: data.summary,
56
+ }
32
57
  }
@@ -85,23 +85,19 @@ export default function HealthPage() {
85
85
  setUpdateError('')
86
86
 
87
87
  try {
88
- await runUpdate()
89
-
90
- // Keep polling health to check when it goes down and recovers
91
- let serverDown = false
92
- const pollInterval = setInterval(async () => {
93
- try {
94
- const res = await fetchHealth()
95
- if (serverDown && res.status === 'healthy') {
96
- clearInterval(pollInterval)
97
- window.location.reload()
98
- }
99
- } catch {
100
- // Connection failed = server is offline / restarting
101
- serverDown = true
102
- }
103
- }, 2000)
88
+ const result = await runUpdate()
89
+ if (!result.success) {
90
+ setUpdateError(result.message || 'Package auto-update reported failures')
91
+ setIsUpdating(false)
92
+ return
93
+ }
104
94
 
95
+ // Multi-repo npm update does not reboot this host — show success, optional reload
96
+ setIsUpdating(false)
97
+ setCheckMessage(
98
+ result.message ||
99
+ 'Registered projects updated. Restart local daemon/server if you need new code in this process.',
100
+ )
105
101
  } catch (e) {
106
102
  setUpdateError((e as Error).message)
107
103
  setIsUpdating(false)
@@ -117,7 +117,28 @@ export default function SettingsPage({ activeBrainId }: { activeBrainId?: string
117
117
  setUpdateMessage(null);
118
118
  try {
119
119
  const res = await runUpdate();
120
- setUpdateMessage(res.success ? 'Update complete. Restarting...' : 'Update failed: ' + res.message);
120
+ if (res.success) {
121
+ const s = res.summary;
122
+ const bits = [
123
+ s?.updated != null ? `${s.updated} updated` : null,
124
+ s?.up_to_date != null ? `${s.up_to_date} current` : null,
125
+ s?.latest ? `latest ${s.latest}` : null,
126
+ ].filter(Boolean);
127
+ setUpdateMessage(
128
+ bits.length
129
+ ? `Update complete — ${bits.join(' · ')}. Restart daemon/server if this host still runs old code.`
130
+ : res.message || 'Update complete.',
131
+ );
132
+ } else {
133
+ const failDetail = (res.summary?.results || [])
134
+ .filter((r) => r.status === 'failed')
135
+ .map((r) => `${r.name}: ${r.error || 'failed'}`)
136
+ .slice(0, 3)
137
+ .join('; ');
138
+ setUpdateMessage(
139
+ `Update failed: ${res.message}${failDetail ? ` (${failDetail})` : ''}`,
140
+ );
141
+ }
121
142
  const refreshed = await checkUpdate().catch(() => null);
122
143
  if (refreshed) {
123
144
  setUpdateInfo({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "total-recall-brain",
3
- "version": "3.18.3",
3
+ "version": "3.19.0",
4
4
  "description": "Personal AI system — database-free, SSSS-powered intelligence engine deployable to any POSIX host.",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -91,7 +91,21 @@ export function resolveAgentDir(layer = 'auto', targetPath = process.cwd()) {
91
91
  * @param {'global' | 'project' | 'auto'} [layer='auto']
92
92
  */
93
93
  export function resolveBrainDir(layer = 'auto', targetPath = process.cwd()) {
94
- return getGlobalBrainDir();
94
+ // Match resolveAgentDir: AGENT_DIR override pins the agent root → brain under it.
95
+ if (process.env.AGENT_DIR) {
96
+ return path.join(process.env.AGENT_DIR, 'skills', 'total-recall');
97
+ }
98
+ if (layer === 'global') return getGlobalBrainDir();
99
+ if (layer === 'project') {
100
+ const project = detectProjectBrain(targetPath);
101
+ if (!project) {
102
+ throw new Error('No project brain found. Run `npx total-recall init --project` to create one.');
103
+ }
104
+ return project.brainDir;
105
+ }
106
+ // Auto: project-local wins if a project brain exists
107
+ const project = detectProjectBrain(targetPath);
108
+ return project ? project.brainDir : getGlobalBrainDir();
95
109
  }
96
110
 
97
111
  /**
package/src/cli/chat.mjs CHANGED
@@ -10,7 +10,7 @@ import { callLocalRuntime, loadRuntimeConfig, checkRuntimeHealth } from '../core
10
10
  export default async function runChat(args) {
11
11
  console.log('🤖 Total Recall CLI Chat initialized. Type "exit" to quit.\n');
12
12
 
13
- const configDir = path.join(resolveBrainDir(), 'config');
13
+ const configDir = path.join(resolveBrainDir('global'), 'config');
14
14
 
15
15
  let config;
16
16
  let caller;
@@ -22,7 +22,8 @@ import { startDaemon, stopDaemon, readPid } from '../core/daemon-control.mjs';
22
22
 
23
23
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
24
24
  const ROOT = path.join(__dirname, '..', '..');
25
- const BRAIN_DIR = resolveBrainDir();
25
+ // System daemon always targets the global brain — never the cwd project vault.
26
+ const BRAIN_DIR = resolveBrainDir('global');
26
27
  const LOG_FILE = path.join(BRAIN_DIR, 'logs', 'daemon.log');
27
28
 
28
29
  function commandExists(cmd) {
package/src/cli/dream.mjs CHANGED
@@ -13,11 +13,9 @@
13
13
 
14
14
  import path from 'node:path';
15
15
  import { fileURLToPath } from 'node:url';
16
- import { resolveAgentDir, resolveBrainDir } from './agent-dir.mjs';
16
+ import { resolveAgentDir, resolveBrainDir, parseLayerFlag } from './agent-dir.mjs';
17
17
 
18
18
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
19
- const AGENT_DIR = resolveAgentDir();
20
- const BRAIN_DIR = resolveBrainDir();
21
19
 
22
20
  function parseArgs(args) {
23
21
  const opts = { vault: null, help: false };
@@ -42,20 +40,26 @@ function printHelp() {
42
40
  Usage: total-recall dream [options]
43
41
 
44
42
  Options:
45
- --vault <path> Override vault directory (default: ~/.agent/memory-vault)
43
+ --vault <path> Override vault directory
44
+ --global Dream the global brain (default)
45
+ --project Dream the project brain for the current repo
46
46
  --help, -h Show this help
47
47
  `);
48
48
  }
49
49
 
50
50
  export default async function dream(args) {
51
- const opts = parseArgs(args);
51
+ const { layer, remainingArgs } = parseLayerFlag(args);
52
+ const effectiveLayer = layer === 'auto' ? 'global' : layer;
53
+ const opts = parseArgs(remainingArgs);
52
54
  if (opts.help) { printHelp(); return; }
53
55
 
54
- const vaultDir = opts.vault || path.join(BRAIN_DIR, 'memory-vault');
55
- const skillsDir = path.join(AGENT_DIR, 'skills');
56
- const derivedDir = path.join(BRAIN_DIR, 'memory-derived');
57
- const conflictsDir = path.join(BRAIN_DIR, 'memory-inbox', 'conflicts');
58
- const instructionsFile = path.join(AGENT_DIR, 'INSTRUCTIONS.md');
56
+ const agentDir = resolveAgentDir(effectiveLayer);
57
+ const brainDir = resolveBrainDir(effectiveLayer);
58
+ const vaultDir = opts.vault || path.join(brainDir, 'memory-vault');
59
+ const skillsDir = path.join(agentDir, 'skills');
60
+ const derivedDir = path.join(brainDir, 'memory-derived');
61
+ const conflictsDir = path.join(brainDir, 'memory-inbox', 'conflicts');
62
+ const instructionsFile = path.join(agentDir, 'INSTRUCTIONS.md');
59
63
 
60
64
  console.error('\n 🌙 Starting dream cycle...\n');
61
65
  const startTime = Date.now();
@@ -3,6 +3,7 @@ import path from 'node:path';
3
3
  import { resolveAgentDir, resolveBrainDir, parseLayerFlag, getBothBrains } from './agent-dir.mjs';
4
4
  import { compileSurface } from '../core/surface.mjs';
5
5
  import { deleteNode } from '../core/vault.mjs';
6
+ import { invalidate } from '../core/vault-cache.mjs';
6
7
 
7
8
  function printHelp() {
8
9
  console.log(`
@@ -65,12 +66,16 @@ export default async function forget(args) {
65
66
  }
66
67
 
67
68
  console.log(` ✅ Soft-deleted ${count} nodes for project "${projectName}" (moved to .trash/)`);
69
+
70
+ // Files moved under .trash still invalidate the vault cache (watcher may lag)
71
+ invalidate(globalVaultDir);
68
72
 
69
73
  if (!remainingArgs.includes('--no-compile')) {
70
74
  console.log(' ⏳ Recompiling active memory surfaces and indexes in the background...');
71
75
  try {
72
76
  const { spawn } = await import('node:child_process');
73
- const child = spawn(process.argv[0], [process.argv[1], 'compile'], {
77
+ // Soft-delete from global vault tags — recompile global surfaces
78
+ const child = spawn(process.argv[0], [process.argv[1], 'compile', '--global'], {
74
79
  detached: true,
75
80
  stdio: 'ignore'
76
81
  });
@@ -130,6 +135,9 @@ export default async function forget(args) {
130
135
  process.exit(1);
131
136
  }
132
137
 
138
+ // Drop cached nodes immediately (fs.watch may lag or miss same-process deletes)
139
+ invalidate(vaultDir);
140
+
133
141
  console.log(` ✅ Deleted memory node "${slug}" from ${layerLabel} vault.`);
134
142
 
135
143
  // Recompile unless --no-compile
@@ -137,7 +145,11 @@ export default async function forget(args) {
137
145
  console.log(' ⏳ Recompiling active memory surfaces and indexes in the background...');
138
146
  try {
139
147
  const { spawn } = await import('node:child_process');
140
- const child = spawn(process.argv[0], [process.argv[1], 'compile'], {
148
+ // Pass layer so project forget recompiles the project brain, not auto/wrong vault
149
+ const compileArgs = [process.argv[1], 'compile'];
150
+ if (layer === 'project') compileArgs.push('--project');
151
+ else if (layer === 'global') compileArgs.push('--global');
152
+ const child = spawn(process.argv[0], compileArgs, {
141
153
  detached: true,
142
154
  stdio: 'ignore'
143
155
  });
@@ -9,7 +9,7 @@ export default async function runFriction(args) {
9
9
  console.log(`[Friction] 🔍 Analyzing logs for workflow bottlenecks...`);
10
10
 
11
11
  // Default path for watchdog logs
12
- const logPath = args[0] || path.join(resolveBrainDir(), 'logs', 'watchdog.jsonl');
12
+ const logPath = args[0] || path.join(resolveBrainDir('global'), 'logs', 'watchdog.jsonl');
13
13
 
14
14
  const result = await detectFriction(logPath);
15
15
  if (!result) {
@@ -8,10 +8,11 @@
8
8
 
9
9
  import path from 'node:path';
10
10
  import { detectAndImport } from '../core/import-rules.mjs';
11
- import { resolveAgentDir, resolveBrainDir } from './agent-dir.mjs';
11
+ import { resolveBrainDir, parseLayerFlag } from './agent-dir.mjs';
12
12
 
13
13
  export async function run(argv = []) {
14
- const args = argv.slice(3); // strip node, script, 'import'
14
+ const rawArgs = argv.slice(3); // strip node, script, 'import'
15
+ const { layer, remainingArgs: args } = parseLayerFlag(rawArgs);
15
16
 
16
17
  const dirs = [];
17
18
  let force = false;
@@ -33,7 +34,8 @@ export async function run(argv = []) {
33
34
  // Default: search cwd
34
35
  if (dirs.length === 0) dirs.push(process.cwd());
35
36
 
36
- const brainDir = resolveBrainDir();
37
+ // auto → project vault when present (import rules for this repo)
38
+ const brainDir = resolveBrainDir(layer);
37
39
  const vaultDir = path.join(brainDir, 'memory-vault');
38
40
 
39
41
  console.log(`\n🔍 Scanning for rule files in: ${dirs.join(', ')}\n`);
@@ -90,12 +92,15 @@ all future AI sessions.
90
92
 
91
93
  Options:
92
94
  --dir <path> Directory to search (default: current directory)
95
+ --global Import into the global vault
96
+ --project Import into the project vault (default: auto when present)
93
97
  --force Re-import files that were previously imported
94
98
  --dry-run Show what would be imported without writing anything
95
99
  --help Show this help
96
100
 
97
101
  Examples:
98
102
  npx total-recall import
103
+ npx total-recall import --project
99
104
  npx total-recall import --dir ~/my-project
100
105
  npx total-recall import --force --dry-run
101
106
  `);
@@ -16,8 +16,9 @@
16
16
  import path from 'node:path';
17
17
  import { resolveAgentDir, resolveBrainDir } from './agent-dir.mjs';
18
18
 
19
- const AGENT_DIR = resolveAgentDir();
20
- const BRAIN_DIR = resolveBrainDir();
19
+ // Session ingest lands in the global brain by default.
20
+ const AGENT_DIR = resolveAgentDir('global');
21
+ const BRAIN_DIR = resolveBrainDir('global');
21
22
 
22
23
  function parseArgs(args) {
23
24
  const opts = { sources: null, watch: false, help: false };
package/src/cli/lint.mjs CHANGED
@@ -17,11 +17,9 @@
17
17
  import fs from 'node:fs';
18
18
  import path from 'node:path';
19
19
  import { fileURLToPath } from 'node:url';
20
- import { resolveAgentDir, resolveBrainDir } from './agent-dir.mjs';
20
+ import { resolveBrainDir, parseLayerFlag } from './agent-dir.mjs';
21
21
 
22
22
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
23
- const AGENT_DIR = resolveAgentDir();
24
- const BRAIN_DIR = resolveBrainDir();
25
23
 
26
24
  function parseArgs(args) {
27
25
  const opts = { vault: null, strict: false, json: false, help: false, okf: false };
@@ -47,7 +45,9 @@ function printHelp() {
47
45
  Usage: total-recall lint [options]
48
46
 
49
47
  Options:
50
- --vault <path> Override vault directory (default: ~/.agent/memory-vault)
48
+ --vault <path> Override vault directory (default: global brain vault)
49
+ --global Lint the global vault (default)
50
+ --project Lint the project vault for the current repo
51
51
  --strict Treat warnings as errors (exit 1 on any issue)
52
52
  --okf Verify OKF v0.1 draft metadata compliance
53
53
  --json Output results as JSONL
@@ -60,10 +60,13 @@ function printHelp() {
60
60
  }
61
61
 
62
62
  export default async function lint(args) {
63
- const opts = parseArgs(args);
63
+ const { layer, remainingArgs } = parseLayerFlag(args);
64
+ // Default layer is global when no --project/--global (matches help text)
65
+ const effectiveLayer = layer === 'auto' ? 'global' : layer;
66
+ const opts = parseArgs(remainingArgs);
64
67
  if (opts.help) { printHelp(); return; }
65
68
 
66
- const vaultDir = opts.vault || path.join(BRAIN_DIR, 'memory-vault');
69
+ const vaultDir = opts.vault || path.join(resolveBrainDir(effectiveLayer), 'memory-vault');
67
70
 
68
71
  if (opts.okf) {
69
72
  const { lintOkfCompliance } = await import('../core/okf-adapter.mjs');
@@ -2,7 +2,7 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { compileSurface } from '../core/surface.mjs';
4
4
  import { detectIndexDrift } from '../core/drift-detector.mjs';
5
- import { resolveAgentDir, resolveBrainDir } from './agent-dir.mjs';
5
+ import { resolveAgentDir, resolveBrainDir, parseLayerFlag } from './agent-dir.mjs';
6
6
  import { logger } from '../core/logger.mjs';
7
7
 
8
8
  /**
@@ -14,8 +14,9 @@ import { logger } from '../core/logger.mjs';
14
14
  */
15
15
 
16
16
  export async function runRebuild(options = {}) {
17
- const agentDir = resolveAgentDir();
18
- const brainDir = resolveBrainDir();
17
+ const layer = options.layer || 'auto';
18
+ const agentDir = resolveAgentDir(layer);
19
+ const brainDir = resolveBrainDir(layer);
19
20
  const vaultDir = path.join(brainDir, 'memory-vault');
20
21
  const skillsDir = path.join(agentDir, 'skills');
21
22
  const derivedDir = path.join(brainDir, 'memory-derived');
@@ -82,7 +83,11 @@ export async function runRebuild(options = {}) {
82
83
  }
83
84
 
84
85
  export default async function cli(args) {
85
- const options = { check: args.includes('--check') };
86
+ const { layer, remainingArgs } = parseLayerFlag(args);
87
+ const options = {
88
+ check: remainingArgs.includes('--check'),
89
+ layer,
90
+ };
86
91
  const code = await runRebuild(options);
87
92
  process.exit(code);
88
93
  }
@@ -5,6 +5,7 @@ import matter from 'gray-matter';
5
5
  import { resolveAgentDir, resolveBrainDir, parseLayerFlag, getBothBrains, defaultLayerForCategory } from './agent-dir.mjs';
6
6
  import { compileSurface } from '../core/surface.mjs';
7
7
  import { writeNodeValidatedAsync } from '../core/validated-write.mjs';
8
+ import { defaultTitleFromBody } from '../core/memory-title.mjs';
8
9
 
9
10
  function printHelp() {
10
11
  console.log(`
@@ -243,8 +244,12 @@ export default async function remember(args) {
243
244
  // Create individual SSSS v2 node in the vault
244
245
  const contentHash = crypto.createHash('md5').update(bodyContent).digest('hex').slice(0, 8);
245
246
  const finalSlug = slug || `${category}-${contentHash}`;
246
- const truncatedContent = bodyContent.slice(0, 50) + (bodyContent.length > 50 ? '...' : '');
247
- const finalTitle = title || `Self-captured memory: ${truncatedContent.replace(/\n/g, ' ')}`;
247
+ // Provenance lives in source.type + tags — not a title prefix
248
+ const finalTitle = title || defaultTitleFromBody(bodyContent);
249
+ const finalTags = Array.isArray(tags) ? [...tags] : [];
250
+ if (!finalTags.includes('self-captured') && !title) {
251
+ finalTags.push('self-captured');
252
+ }
248
253
 
249
254
  const now = new Date().toISOString();
250
255
 
@@ -264,12 +269,13 @@ export default async function remember(args) {
264
269
  source: {
265
270
  type: 'remember-cli',
266
271
  session_id: process.env.TR_SESSION_ID || 'remember-session',
267
- evidence_count: 1
272
+ evidence_count: 1,
273
+ capture: 'self',
268
274
  },
269
275
  supersedes: [],
270
276
  superseded_by: null,
271
277
  contradicts: [],
272
- tags,
278
+ tags: finalTags,
273
279
  related,
274
280
  routes_to_skills: [],
275
281
  sentiment_polarity: type.toLowerCase() === 'preference' ? 'preference' : 'descriptive',
@@ -309,8 +315,9 @@ export default async function remember(args) {
309
315
  const oldTitle = (data.title || '').trim().toLowerCase();
310
316
  const newTitle = finalTitle.trim().toLowerCase();
311
317
 
312
- const words1 = new Set(bodyContent.toLowerCase().match(/\\b\\w+\\b/g) || []);
313
- const words2 = new Set((oldContent || '').toLowerCase().match(/\\b\\w+\\b/g) || []);
318
+ // Word tokens for Jaccard similarity (must be real \b/\w, not double-escaped literals)
319
+ const words1 = new Set(bodyContent.toLowerCase().match(/\b\w+\b/g) || []);
320
+ const words2 = new Set((oldContent || '').toLowerCase().match(/\b\w+\b/g) || []);
314
321
  const intersection = new Set([...words1].filter(x => words2.has(x)));
315
322
  const union = new Set([...words1, ...words2]);
316
323
  const similarity = union.size === 0 ? 0 : intersection.size / union.size;
@@ -325,14 +332,10 @@ export default async function remember(args) {
325
332
  data.superseded_by = finalSlug;
326
333
  data.updated = now;
327
334
  node.supersedes = [...(node.supersedes || []), data.slug];
328
-
329
- const lines = ['---'];
330
- for (const key of Object.keys(data)) {
331
- lines.push(`${key}: ${JSON.stringify(data[key])}`);
332
- }
333
- lines.push('---');
334
- lines.push(oldContent);
335
- fs.writeFileSync(p, lines.join('\n'));
335
+
336
+ // Use gray-matter so nested objects/arrays stay valid YAML
337
+ const bodyOut = (oldContent || '').trim();
338
+ fs.writeFileSync(p, matter.stringify(bodyOut ? `${bodyOut}\n` : '', data), 'utf8');
336
339
  }
337
340
  }
338
341
  }
@@ -360,7 +363,10 @@ export default async function remember(args) {
360
363
  console.log(' ⏳ Recompiling active memory surfaces and indexes in the background...');
361
364
  try {
362
365
  const { spawn } = await import('node:child_process');
363
- const child = spawn(process.argv[0], [process.argv[1], 'compile'], {
366
+ const compileArgs = [process.argv[1], 'compile'];
367
+ if (layer === 'project') compileArgs.push('--project');
368
+ else if (layer === 'global') compileArgs.push('--global');
369
+ const child = spawn(process.argv[0], compileArgs, {
364
370
  detached: true,
365
371
  stdio: 'ignore'
366
372
  });
@@ -985,9 +985,12 @@ export default async function secretCli(argv) {
985
985
 
986
986
  if (opts.command === 'check-surfaces') {
987
987
  const texts = [];
988
+ const agentRoot = path.dirname(path.dirname(brainDir)); // …/skills/total-recall → …/.agent (or similar)
988
989
  const candidates = [
989
- path.join(brainDir, 'INSTRUCTIONS.md'),
990
990
  path.join(process.cwd(), 'INSTRUCTIONS.md'),
991
+ path.join(process.cwd(), 'AGENTS.md'),
992
+ path.join(agentRoot, 'INSTRUCTIONS.md'),
993
+ path.join(brainDir, 'INSTRUCTIONS.md'), // legacy
991
994
  path.join(brainDir, 'openwiki'),
992
995
  ];
993
996
  for (const c of candidates) {
package/src/cli/share.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import crypto from 'node:crypto';
2
- import { parseLayerFlag, resolveBrainDir, getBothBrains, defaultLayerForCategory } from './agent-dir.mjs';
2
+ import { parseLayerFlag, resolveAgentDir, resolveBrainDir, getBothBrains, defaultLayerForCategory } from './agent-dir.mjs';
3
3
  import { createMemoryNode } from '../core/vault.mjs';
4
4
  import { writeNodeValidatedAsync } from '../core/validated-write.mjs';
5
5
  import { addToQueue } from '../core/research-queue.mjs';
@@ -128,10 +128,12 @@ export default async function share(args) {
128
128
  }
129
129
 
130
130
  const resolvedBrainDir = resolveBrainDir(layer);
131
+ const resolvedAgentDir = resolveAgentDir(layer);
131
132
  const vaultDir = path.join(resolvedBrainDir, 'memory-vault');
132
- const skillsDir = path.join(resolvedBrainDir, '..', '..');
133
+ // skillsDir must be <agent>/.agent/skills (or ~/.agent/skills), not the agent root
134
+ const skillsDir = path.join(resolvedAgentDir, 'skills');
133
135
  const derivedDir = path.join(resolvedBrainDir, 'memory-derived');
134
- const instructionsFile = path.join(skillsDir, '..', 'INSTRUCTIONS.md');
136
+ const instructionsFile = path.join(resolvedAgentDir, 'INSTRUCTIONS.md');
135
137
 
136
138
  const slugBase = category || 'facts';
137
139
  const slug = `${slugBase}-${crypto.randomBytes(4).toString('hex')}`;
@@ -70,6 +70,22 @@ function pathExists(filePath) {
70
70
  }
71
71
  }
72
72
 
73
+ // Marker written by tools like repo-expert-generate.mjs into any SKILL.md
74
+ // they produce. A real (non-symlink) directory carrying this marker is
75
+ // leftover tool output, not a user's hand-authored skill — it is always safe
76
+ // to heal back onto the canonical source, even without --force.
77
+ const AUTO_GENERATED_MARKER = '**Auto-generated** by `npx total-recall';
78
+
79
+ function isStaleGeneratedDir(entryPath) {
80
+ try {
81
+ if (!fs.statSync(entryPath).isDirectory()) return false;
82
+ const content = fs.readFileSync(path.join(entryPath, 'SKILL.md'), 'utf8');
83
+ return content.includes(AUTO_GENERATED_MARKER);
84
+ } catch {
85
+ return false;
86
+ }
87
+ }
88
+
73
89
  /**
74
90
  * Skill source roots, in priority order. The canonical Total Recall location
75
91
  * (.agent/skills, singular) wins, but we also scan the Antigravity-native
@@ -115,7 +131,10 @@ export function discoverRepoSkills(agentDir) {
115
131
  * Idempotent, self-healing, and strictly additive: a symlink that is broken or
116
132
  * stale is refreshed even without --force; a *real* (non-symlink) entry is
117
133
  * never clobbered unless --force (so a user's own skill always wins a name
118
- * collision); a skill whose source already *is* the destination is left alone.
134
+ * collision) — UNLESS that real entry is itself stale tool output (carries
135
+ * the auto-generated marker), in which case it is healed back onto the
136
+ * canonical source; a skill whose source already *is* the destination is
137
+ * left alone.
119
138
  */
120
139
  export function projectSkillsAsCommands(destDir, skills, opts = {}) {
121
140
  fs.mkdirSync(destDir, { recursive: true });
@@ -138,7 +157,7 @@ export function projectSkillsAsCommands(destDir, skills, opts = {}) {
138
157
  continue;
139
158
  }
140
159
  fs.unlinkSync(linkPath); // stale, broken, or --force → refresh
141
- } else if (!opts.force) {
160
+ } else if (!opts.force && !isStaleGeneratedDir(linkPath)) {
142
161
  written.push({ name: skill.name, action: 'skipped' }); // user's real file/dir wins
143
162
  continue;
144
163
  } else {
@@ -1,5 +1,5 @@
1
1
  import { createSnapshot, listSnapshots, rollbackVault } from '../core/snapshot.mjs';
2
- import { resolveAgentDir, resolveBrainDir } from './agent-dir.mjs';
2
+ import { resolveAgentDir, resolveBrainDir, parseLayerFlag } from './agent-dir.mjs';
3
3
  import { compileSurface } from '../core/surface.mjs';
4
4
  import path from 'path';
5
5
 
@@ -24,20 +24,22 @@ function printHelp() {
24
24
  rollback <id> Restore the vault to a specific snapshot ID
25
25
 
26
26
  Options:
27
- --help, -h Show this help
27
+ --global / --project Target brain layer (default: auto — project if present)
28
+ --help, -h Show this help
28
29
  `);
29
30
  }
30
31
 
31
32
  export default async function snapshotCli(args) {
32
- if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
33
+ const { layer, remainingArgs } = parseLayerFlag(args);
34
+ if (remainingArgs.length === 0 || remainingArgs.includes('--help') || remainingArgs.includes('-h')) {
33
35
  printHelp();
34
36
  return;
35
37
  }
36
38
 
37
- const command = args[0];
39
+ const command = remainingArgs[0];
38
40
 
39
41
  if (command === 'create') {
40
- const reason = args.slice(1).join(' ') || 'manual';
42
+ const reason = remainingArgs.slice(1).join(' ') || 'manual';
41
43
  console.log('📸 Creating snapshot...');
42
44
  const result = createSnapshot(reason);
43
45
  if (result.success) {
@@ -69,7 +71,7 @@ export default async function snapshotCli(args) {
69
71
  }
70
72
 
71
73
  else if (command === 'rollback') {
72
- const id = args[1];
74
+ const id = remainingArgs[1];
73
75
  if (!id) {
74
76
  console.error('❌ You must provide a snapshot ID to rollback to.');
75
77
  console.error('Usage: total-recall snapshot rollback <id>');
@@ -84,8 +86,8 @@ export default async function snapshotCli(args) {
84
86
  console.log(`✅ Rollback successful.`);
85
87
  console.log(`🔄 Recompiling derived indexes from restored vault...`);
86
88
 
87
- const agentDir = resolveAgentDir();
88
- const brainDir = resolveBrainDir();
89
+ const agentDir = resolveAgentDir(layer);
90
+ const brainDir = resolveBrainDir(layer);
89
91
  await compileSurface({
90
92
  vaultDir: path.join(brainDir, 'memory-vault'),
91
93
  skillsDir: path.join(agentDir, 'skills'),
@@ -16,7 +16,8 @@ import {
16
16
  } from '../core/package-auto-update.mjs';
17
17
  import { brainDir } from '../core/config.mjs';
18
18
 
19
- function parseArgs(args) {
19
+ /** @param {string[]} args */
20
+ export function parseArgs(args) {
20
21
  const opts = {
21
22
  apply: false,
22
23
  force: false,