total-recall-brain 3.28.1 → 3.28.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "total-recall-brain",
3
- "version": "3.28.1",
3
+ "version": "3.28.2",
4
4
  "description": "Personal AI system — database-free, SSSS-powered intelligence engine deployable to any POSIX host.",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -100,9 +100,8 @@ const CLIENTS = {
100
100
  label: 'Codex',
101
101
  mode: 'symlink',
102
102
  target: 'AGENTS.md',
103
- // Codex only discovers skills globally under $CODEX_HOME/skills (~/.codex/skills).
104
- // There is no project-local skills dir, so this projection is global by necessity.
105
- skillsProjection: { scope: 'home', dir: path.join('.codex', 'skills') }
103
+ // Codex discovers repository skills in <project>/.agents/skills.
104
+ skillsProjection: { scope: 'project', dir: path.join('.agents', 'skills') }
106
105
  },
107
106
  antigravity: {
108
107
  label: 'Antigravity',
@@ -961,8 +960,7 @@ export default async function connect(args) {
961
960
  }
962
961
  }
963
962
 
964
- // Project repo skills as native slash commands via the Agent Skills standard.
965
- // Each <agentDir>/skills/<name>/SKILL.md becomes /<name> in the target IDE.
963
+ // Project repo skills into the target IDE's Agent Skills directory.
966
964
  if (preset.skillsProjection) {
967
965
  const skills = discoverRepoSkills(agentDir);
968
966
  if (skills.length > 0) {
@@ -977,8 +975,8 @@ export default async function connect(args) {
977
975
  ? destDir
978
976
  : (path.relative(cwd, destDir) || destDir);
979
977
  result.notes.push(
980
- ` Repo skills projected as slash commands → ${destLabel}/:\n` +
981
- fresh.map(r => ` /${r.name}`).join('\n')
978
+ ` Repo skills projected → ${destLabel}/:\n` +
979
+ fresh.map(r => ` ${r.name}`).join('\n')
982
980
  );
983
981
  }
984
982
  }
package/src/cli/init.mjs CHANGED
@@ -600,13 +600,13 @@ export default async function init(args) {
600
600
  logStep('3.6/4', 'Ensuring openwiki is present');
601
601
  await ensureOpenWiki(brainDir, isProject, opts.dryRun);
602
602
 
603
- // ── Step 3.7: Project skills as slash commands into the IDEs in use ──
603
+ // ── Step 3.7: Project skills into the IDEs in use ──
604
604
  // Scope matches the brain: `init --project` projects PROJECT skills into the
605
605
  // repo's IDE skill dirs for IDEs actually used here (.claude/, .agents/, or
606
606
  // the current process); `init` projects GLOBAL skills into installed IDEs'
607
- // global skill dirs. Codex (global-only) is opt-in for project skills.
607
+ // global skill dirs. Codex discovers project skills in .agents/skills.
608
608
  const skillScope = isProject ? 'project' : 'global';
609
- logStep('3.7/4', `Projecting ${skillScope} skills as slash commands (IDEs in use)`);
609
+ logStep('3.7/4', `Projecting ${skillScope} skills (IDEs in use)`);
610
610
  if (opts.dryRun) {
611
611
  const targets = detectActiveSkillTargets({ scope: skillScope, cwd });
612
612
  for (const t of targets.filter(t => t.supported)) {
@@ -627,7 +627,7 @@ export default async function init(args) {
627
627
  const n = t.results.filter(r => ['linked', 'copied', 'exists', 'source'].includes(r.action)).length;
628
628
  logOk(`${t.label}: ${n} skill(s) → ${path.relative(cwd, t.destDir) || t.destDir}/`);
629
629
  }
630
- log(` Slash commands: ${skills.map(s => '/' + s.name).join(', ')}`);
630
+ log(` Discoverable skills: ${skills.map(s => s.name).join(', ')}`);
631
631
  if (available.length > 0) {
632
632
  log(` Also available (opt-in): ${available.map(t => t.clients.join('/')).join(', ')} — \`npx total-recall connect <ide>\``);
633
633
  }
@@ -2,8 +2,8 @@
2
2
  * Scope-aware skill → slash-command projection.
3
3
  *
4
4
  * A "repo skill" is any <skills-root>/<name>/SKILL.md directory (the Agent
5
- * Skills standard). Each IDE turns such a directory into a /<name> slash
6
- * command, but every IDE looks in a *different* place, and the right place
5
+ * Skills standard). Each IDE discovers skills in a different place, and that
6
+ * place
7
7
  * depends on the *scope* of the skill:
8
8
  *
9
9
  * PROJECT skills (authored in <repo>/.agent/skills or <repo>/.agents/skills)
@@ -16,9 +16,9 @@
16
16
  * machine. They are available in every repo.
17
17
  *
18
18
  * This mirrors the brain model: `init --project` ⇒ project skills; `init`
19
- * (global) ⇒ global skills. Codex has no project-local skills dir (it only
20
- * reads ~/.codex/skills globally), so it participates in GLOBAL projection only;
21
- * pushing project skills to Codex is opt-in via `connect codex`.
19
+ * (global) ⇒ global skills. Codex discovers project skills in
20
+ * <repo>/.agents/skills and global skills in ~/.codex/skills. In Codex,
21
+ * /skills opens the picker and $skill-name explicitly invokes a skill.
22
22
  */
23
23
  import fs from 'node:fs';
24
24
  import os from 'node:os';
@@ -54,9 +54,7 @@ export const IDE_SKILLS = [
54
54
  id: 'codex',
55
55
  label: 'Codex',
56
56
  clients: ['codex'],
57
- // Codex only discovers skills globally under ~/.codex/skills. There is no
58
- // project-local dir, so project skills are opt-in (namespaced) via connect.
59
- project: null,
57
+ project: { dir: ['.agents', 'skills'], markers: ['.agents', 'AGENTS.md'], env: ['CODEX_HOME'] },
60
58
  global: { dir: ['.codex', 'skills'], markers: ['.codex'] }
61
59
  },
62
60
  {
@@ -117,7 +115,7 @@ function isStaleGeneratedDir(entryPath) {
117
115
  }
118
116
 
119
117
  /**
120
- * Put one skill at linkPath so the IDE sees it as /<name>:
118
+ * Put one skill at linkPath so the IDE can discover it:
121
119
  * 1. a symlink RELATIVE to the destination dir — the projection then survives
122
120
  * `git clone`, a moved checkout, or a different home directory (an absolute
123
121
  * link only works on the machine that wrote it);
@@ -149,21 +147,23 @@ function placeSkill(skillDir, linkPath) {
149
147
  }
150
148
 
151
149
  /**
152
- * Skill source roots, in priority order. The canonical Total Recall location
153
- * (.agent/skills, singular) wins, but we also scan the Antigravity-native
154
- * .agents/skills (plural) so a skill authored in either place is discovered.
150
+ * Skill source roots, in priority order. The Total Recall location
151
+ * (.agent/skills, singular) wins, followed by repo-authored skills/ and
152
+ * already-projected .agents/skills. This avoids treating projected links as
153
+ * the source when the original is available.
155
154
  */
156
155
  export function skillSourceRoots(agentDir) {
157
156
  const roots = [path.join(agentDir, 'skills')];
158
157
  const parent = path.dirname(agentDir);
159
158
  if (path.basename(agentDir) === '.agent') {
159
+ roots.push(path.join(parent, 'skills'));
160
160
  roots.push(path.join(parent, '.agents', 'skills'));
161
161
  }
162
162
  return roots;
163
163
  }
164
164
 
165
165
  /**
166
- * Discover repo skills to expose as slash commands.
166
+ * Discover repo skills to expose to IDEs.
167
167
  * Every <root>/<name>/ directory containing a SKILL.md is a skill. When the
168
168
  * same name appears in more than one source root, the canonical root wins.
169
169
  */
@@ -20,13 +20,8 @@ import fs from 'node:fs';
20
20
  import crypto from 'node:crypto';
21
21
  import { loadQueue } from '../core/research-queue.mjs';
22
22
 
23
- import { agentDir, brainDir, totalRecallToken } from '../core/config.mjs';
24
-
25
- const AGENT_DIR = agentDir;
26
- const BRAIN_CONFIG = path.join(brainDir, 'config', 'brain.json');
27
- const SYNC_STATE = path.join(brainDir, 'config', 'sync-state.json');
28
- const CLIENTS_REGISTRY = path.join(brainDir, 'config', 'clients.json');
29
- const INSTRUCTIONS_FILE = path.join(AGENT_DIR, 'INSTRUCTIONS.md');
23
+ import { totalRecallToken } from '../core/config.mjs';
24
+ import { resolveBrainDir } from './agent-dir.mjs';
30
25
 
31
26
  function parseArgs(args) {
32
27
  const opts = { json: false, brain: null, token: null, help: false };
@@ -55,21 +50,21 @@ function printHelp() {
55
50
  `);
56
51
  }
57
52
 
58
- function loadBrainConfig() {
59
- if (!fs.existsSync(BRAIN_CONFIG)) return null;
60
- try { return JSON.parse(fs.readFileSync(BRAIN_CONFIG, 'utf8')); }
53
+ function loadBrainConfig(brainConfigPath) {
54
+ if (!fs.existsSync(brainConfigPath)) return null;
55
+ try { return JSON.parse(fs.readFileSync(brainConfigPath, 'utf8')); }
61
56
  catch { return null; }
62
57
  }
63
58
 
64
- function loadSyncState() {
65
- if (!fs.existsSync(SYNC_STATE)) return {};
66
- try { return JSON.parse(fs.readFileSync(SYNC_STATE, 'utf8')); }
59
+ function loadSyncState(syncStatePath) {
60
+ if (!fs.existsSync(syncStatePath)) return {};
61
+ try { return JSON.parse(fs.readFileSync(syncStatePath, 'utf8')); }
67
62
  catch { return {}; }
68
63
  }
69
64
 
70
- function loadClientsRegistry() {
71
- if (!fs.existsSync(CLIENTS_REGISTRY)) return {};
72
- try { return JSON.parse(fs.readFileSync(CLIENTS_REGISTRY, 'utf8'))?.clients || {}; }
65
+ function loadClientsRegistry(clientsRegistryPath) {
66
+ if (!fs.existsSync(clientsRegistryPath)) return {};
67
+ try { return JSON.parse(fs.readFileSync(clientsRegistryPath, 'utf8'))?.clients || {}; }
73
68
  catch { return {}; }
74
69
  }
75
70
 
@@ -90,21 +85,31 @@ function sha256(buf) {
90
85
  return crypto.createHash('sha256').update(buf).digest('hex');
91
86
  }
92
87
 
93
- async function probeBrain(brainUrl, token) {
88
+ async function probeBrain(brainUrl, token, brainId) {
94
89
  const headers = {};
95
90
  if (token) headers.Authorization = `Bearer ${token}`;
96
91
  const out = { reachable: false, instructions_sha256: null, instructions_bytes: null, error: null };
97
92
  try {
98
- const res = await fetch(`${brainUrl.replace(/\/$/, '')}/api/instructions`, { headers });
93
+ const url = new URL(`${brainUrl.replace(/\/$/, '')}/api/instructions`);
94
+ if (brainId) url.searchParams.set('brain', brainId);
95
+ const res = await fetch(url, { headers });
99
96
  if (!res.ok) {
100
97
  out.error = `HTTP ${res.status}`;
101
98
  return out;
102
99
  }
103
- const body = await res.json();
100
+ const contentType = res.headers?.get?.('content-type') || '';
101
+ const body = contentType.includes('application/json') ? await res.json() : null;
104
102
  out.reachable = true;
105
- out.instructions_sha256 = body.sha256 || null;
106
- out.instructions_bytes = body.bytes || null;
107
- out.modified = body.modified || null;
103
+ if (body) {
104
+ out.instructions_sha256 = body.sha256 || (body.content ? sha256(body.content) : null);
105
+ out.instructions_bytes = body.bytes || (body.content ? Buffer.byteLength(body.content) : null);
106
+ out.modified = body.modified || null;
107
+ } else {
108
+ const content = await res.text();
109
+ out.instructions_sha256 = sha256(content);
110
+ out.instructions_bytes = Buffer.byteLength(content);
111
+ out.modified = res.headers?.get?.('last-modified') || null;
112
+ }
108
113
  } catch (err) {
109
114
  out.error = err.message;
110
115
  }
@@ -115,9 +120,12 @@ export default async function statusCmd(args) {
115
120
  const opts = parseArgs(args);
116
121
  if (opts.help) { printHelp(); return; }
117
122
 
118
- const brainConfig = loadBrainConfig();
119
- const syncState = loadSyncState();
120
- const clientsRegistry = loadClientsRegistry();
123
+ const brainDir = resolveBrainDir();
124
+ const agentDir = path.dirname(path.dirname(brainDir));
125
+ const instructionsFile = path.join(path.dirname(agentDir), 'INSTRUCTIONS.md');
126
+ const brainConfig = loadBrainConfig(path.join(brainDir, 'config', 'brain.json'));
127
+ const syncState = loadSyncState(path.join(brainDir, 'config', 'sync-state.json'));
128
+ const clientsRegistry = loadClientsRegistry(path.join(brainDir, 'config', 'clients.json'));
121
129
  const brainUrl = opts.brain || brainConfig?.url || null;
122
130
  const token = opts.token || brainConfig?.token || totalRecallToken || null;
123
131
 
@@ -175,11 +183,11 @@ export default async function statusCmd(args) {
175
183
  } catch {}
176
184
 
177
185
  let localInstructions = null;
178
- if (fs.existsSync(INSTRUCTIONS_FILE)) {
179
- const buf = fs.readFileSync(INSTRUCTIONS_FILE);
180
- const stat = fs.statSync(INSTRUCTIONS_FILE);
186
+ if (fs.existsSync(instructionsFile)) {
187
+ const buf = fs.readFileSync(instructionsFile);
188
+ const stat = fs.statSync(instructionsFile);
181
189
  localInstructions = {
182
- path: INSTRUCTIONS_FILE,
190
+ path: instructionsFile,
183
191
  sha256: sha256(buf),
184
192
  bytes: stat.size,
185
193
  modified: stat.mtime.toISOString()
@@ -188,14 +196,17 @@ export default async function statusCmd(args) {
188
196
 
189
197
  let remote = null;
190
198
  if (brainUrl) {
191
- remote = await probeBrain(brainUrl, token);
199
+ const brainId = brainConfig?.layer === 'project' && brainConfig?.name
200
+ ? `project:${brainConfig.name}`
201
+ : null;
202
+ remote = await probeBrain(brainUrl, token, brainId);
192
203
  }
193
204
 
194
205
  const inSync = !!(localInstructions && remote?.instructions_sha256 &&
195
206
  remote.instructions_sha256 === localInstructions.sha256);
196
207
 
197
208
  const instructionsMtime = localInstructions
198
- ? fs.statSync(INSTRUCTIONS_FILE).mtimeMs
209
+ ? fs.statSync(instructionsFile).mtimeMs
199
210
  : null;
200
211
  const clientProjections = projectionsStatus(clientsRegistry, instructionsMtime);
201
212
 
@@ -61,15 +61,25 @@ let currentDbPath = null;
61
61
  const dbVssEnabled = new WeakMap();
62
62
 
63
63
  /**
64
- * better-sqlite3 on Linux appends ".so" when the path already ends in ".so",
65
- * producing "vss0.so.so". Strip known extensions before loadExtension.
64
+ * better-sqlite3 appends the platform suffix, so a path that already ends in
65
+ * ".so" reports "vss0.so.so: cannot open shared object file". Strip known
66
+ * extensions before loadExtension.
67
+ *
68
+ * When BOTH attempts fail, rethrow the FIRST error: the second is the artifact
69
+ * of the appended suffix, and logging it hid the real cause on a Linux host —
70
+ * "vss0.so.so: cannot open shared object file" while the actual reason was a
71
+ * missing libblas.so.3 that `ldd vss0.so` names immediately.
66
72
  */
67
73
  function loadExtensionBare(db, absolutePath) {
68
74
  const bare = String(absolutePath).replace(/\.(so|dylib|dll)$/i, '');
69
75
  try {
70
76
  db.loadExtension(bare);
71
- } catch {
72
- db.loadExtension(absolutePath);
77
+ } catch (bareErr) {
78
+ try {
79
+ db.loadExtension(absolutePath);
80
+ } catch {
81
+ throw bareErr;
82
+ }
73
83
  }
74
84
  }
75
85