total-recall-brain 3.18.3 → 3.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/frontend/dist/assets/{index-CGqvdwHT.js → index-D0XeeXhG.js} +119 -119
  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/frontend/src/pages/SkillsPage.tsx +40 -13
  7. package/package.json +1 -1
  8. package/src/cli/agent-dir.mjs +15 -1
  9. package/src/cli/chat.mjs +1 -1
  10. package/src/cli/daemon.mjs +2 -1
  11. package/src/cli/dream.mjs +14 -10
  12. package/src/cli/forget.mjs +14 -2
  13. package/src/cli/friction.mjs +1 -1
  14. package/src/cli/import-rules.mjs +8 -3
  15. package/src/cli/ingest.mjs +3 -2
  16. package/src/cli/lint.mjs +9 -6
  17. package/src/cli/rebuild.mjs +9 -4
  18. package/src/cli/remember.mjs +21 -15
  19. package/src/cli/secret.mjs +4 -1
  20. package/src/cli/share.mjs +5 -3
  21. package/src/cli/skill-projection.mjs +21 -2
  22. package/src/cli/snapshot.mjs +10 -8
  23. package/src/cli/update.mjs +2 -1
  24. package/src/core/clarity-rewriter.mjs +31 -12
  25. package/src/core/config.mjs +10 -4
  26. package/src/core/daemon-loop.mjs +2 -1
  27. package/src/core/embeddings.mjs +52 -7
  28. package/src/core/memory-title.mjs +70 -0
  29. package/src/core/runtime.mjs +11 -1
  30. package/src/core/secrets-store.mjs +7 -1
  31. package/src/core/skills-registry.mjs +12 -0
  32. package/src/core/source-adapters.mjs +1 -1
  33. package/src/core/surface.mjs +4 -4
  34. package/src/core/throttled-fetch.mjs +59 -24
  35. package/src/core/vault.mjs +7 -1
  36. package/src/server/index.mjs +56 -0
  37. package/src/server/route-manifest.json +16 -0
  38. package/src/server/routes/_shared.mjs +33 -1
  39. package/src/server/routes/context.mjs +14 -9
  40. package/src/server/routes/dashboard.mjs +39 -18
  41. package/src/server/routes/docs.mjs +27 -13
  42. package/src/server/routes/dream.mjs +10 -11
  43. package/src/server/routes/export.mjs +43 -12
  44. package/src/server/routes/field.mjs +11 -9
  45. package/src/server/routes/graph.mjs +31 -13
  46. package/src/server/routes/import.mjs +35 -16
  47. package/src/server/routes/instructions.mjs +41 -5
  48. package/src/server/routes/memory.mjs +61 -21
  49. package/src/server/routes/rules.mjs +11 -27
  50. package/src/server/routes/share.mjs +2 -1
  51. package/src/server/routes/skills.mjs +3 -0
  52. package/src/server/routes/ssss.mjs +11 -3
  53. package/src/server/routes/update.mjs +22 -3
  54. 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-D0XeeXhG.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({
@@ -189,6 +189,22 @@ export default function SkillsPage({ activeBrainId }: { activeBrainId?: string }
189
189
  const [installingPkg, setInstallingPkg] = useState<string | null>(null);
190
190
  const [quarantineAlert, setQuarantineAlert] = useState<{ pkg: string; reason: string } | null>(null);
191
191
 
192
+ // ─── Active brain → single deployable repo target ──────────────────────────
193
+ // activeBrainId is 'global', 'project:<name>', or a comma-joined multi-select
194
+ // (see BrainSelector). Only a single, non-global selection maps to one concrete
195
+ // project repo we can deploy a global skill into.
196
+ const activeRepoName = useMemo(() => {
197
+ if (!activeBrainId) return null;
198
+ const ids = activeBrainId.split(',').map((s) => s.trim()).filter(Boolean);
199
+ if (ids.length !== 1 || ids[0] === 'global') return null;
200
+ const id = ids[0];
201
+ return id.startsWith('project:') ? id.slice('project:'.length) : id;
202
+ }, [activeBrainId]);
203
+ const activeRepoGroup = useMemo(
204
+ () => (activeRepoName ? repoGroups.find((g) => g.repo === activeRepoName) || null : null),
205
+ [activeRepoName, repoGroups],
206
+ );
207
+
192
208
  // ─── Lifecycle tab state ────────────────────────────────────────────────────
193
209
  const lifecycleVersion = useMemo(() => selectedSkill && skillContent ? extractVersion(skillContent) : '0.0.0', [selectedSkill, skillContent]);
194
210
  const changelogEntries = useMemo(() => selectedSkill && skillContent ? parseChangelog(skillContent) : [], [selectedSkill, skillContent]);
@@ -269,14 +285,23 @@ export default function SkillsPage({ activeBrainId }: { activeBrainId?: string }
269
285
  groups = (data.repos || []) as RepoSkillsGroup[];
270
286
  }
271
287
 
272
- if (activeBrainId && activeBrainId !== 'global') {
273
- const targetRepoId = activeBrainId.startsWith('project:') ? activeBrainId.replace('project:', '') : activeBrainId;
288
+ // activeBrainId may be a comma-joined multi-select (see BrainSelector) — split it
289
+ // rather than treating the whole string as a single repo id, else selecting more
290
+ // than one brain collapsed the list down to only the "Global" group.
291
+ const selectedBrainIds = (activeBrainId || '').split(',').map((s) => s.trim()).filter(Boolean);
292
+ if (selectedBrainIds.length > 0 && !selectedBrainIds.includes('global')) {
293
+ const targetRepoIds = selectedBrainIds.map((id) =>
294
+ id.startsWith('project:') ? id.slice('project:'.length) : id,
295
+ );
274
296
  groups = groups.filter(
275
- g => g.repo === 'Global' ||
276
- g.repo === targetRepoId ||
277
- targetRepoId.endsWith(`/${g.repo}`) ||
278
- g.repo.endsWith(`/${targetRepoId}`) ||
279
- activeBrainId === `project:${g.repo}`
297
+ g => g.repo === 'Global' ||
298
+ targetRepoIds.some(
299
+ (targetRepoId) =>
300
+ g.repo === targetRepoId ||
301
+ targetRepoId.endsWith(`/${g.repo}`) ||
302
+ g.repo.endsWith(`/${targetRepoId}`),
303
+ ) ||
304
+ selectedBrainIds.includes(`project:${g.repo}`)
280
305
  );
281
306
  list = groups.flatMap(g => g.skills || []);
282
307
  }
@@ -713,24 +738,26 @@ Configure triggers, options, and prompts inside this rules sheet to hot-recompil
713
738
  {skill.name}
714
739
  </span>
715
740
  <div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
716
- {activeBrainId && activeBrainId !== 'Global' && group.repo === 'Global' && (
741
+ {activeRepoGroup?.path && group.repo === 'Global' && (
717
742
  <input
718
743
  type="checkbox"
719
- checked={!!repoGroups.find(g => g.repo === activeBrainId)?.skills?.some(s => s.name === skill.name)}
744
+ checked={!!activeRepoGroup?.skills?.some(s => s.name === skill.name)}
720
745
  onClick={(e) => e.stopPropagation()}
721
746
  onChange={async (e) => {
722
747
  e.stopPropagation();
723
748
  const checked = e.target.checked;
749
+ const targetRepoPath = activeRepoGroup?.path;
750
+ if (!targetRepoPath) return;
724
751
  if (checked) {
725
752
  // Open preview modal instead of instantly deploying
726
753
  setPreviewModalOpen(true);
727
754
  setPreviewLoading(true);
728
755
  setPreviewData(null);
729
756
  try {
730
- const res = await previewSkillRepo(skill.name, activeBrainId);
757
+ const res = await previewSkillRepo(skill.name, targetRepoPath);
731
758
  setPreviewData({
732
759
  skillName: skill.name,
733
- targetRepo: activeBrainId,
760
+ targetRepo: targetRepoPath,
734
761
  original: res.original,
735
762
  preview: res.preview,
736
763
  signals: res.signals
@@ -744,7 +771,7 @@ Configure triggers, options, and prompts inside this rules sheet to hot-recompil
744
771
  } else {
745
772
  // Uncheck (disable) happens immediately
746
773
  try {
747
- await toggleSkillRepo(skill.name, activeBrainId, false);
774
+ await toggleSkillRepo(skill.name, targetRepoPath, false);
748
775
  void fetchSkillsList();
749
776
  } catch (err) {
750
777
  setError((err as Error).message);
@@ -831,7 +858,7 @@ Configure triggers, options, and prompts inside this rules sheet to hot-recompil
831
858
  <h2 style={{ marginTop: 0, marginBottom: 12 }}>Skill Adaptation Preview</h2>
832
859
  {previewLoading ? (
833
860
  <div style={{ flex: 1, display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
834
- <p>Generating adaptive preview for {activeBrainId}...</p>
861
+ <p>Generating adaptive preview for {activeRepoName || activeBrainId}...</p>
835
862
  </div>
836
863
  ) : previewData ? (
837
864
  <>
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.1",
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')}`;