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 +1 -1
- package/src/cli/connect.mjs +5 -7
- package/src/cli/init.mjs +4 -4
- package/src/cli/skill-projection.mjs +13 -13
- package/src/cli/status.mjs +42 -31
- package/src/core/embeddings.mjs +14 -4
package/package.json
CHANGED
package/src/cli/connect.mjs
CHANGED
|
@@ -100,9 +100,8 @@ const CLIENTS = {
|
|
|
100
100
|
label: 'Codex',
|
|
101
101
|
mode: 'symlink',
|
|
102
102
|
target: 'AGENTS.md',
|
|
103
|
-
// Codex
|
|
104
|
-
|
|
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
|
|
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
|
|
981
|
-
fresh.map(r => `
|
|
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
|
|
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
|
|
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
|
|
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(`
|
|
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
|
|
6
|
-
*
|
|
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
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
|
153
|
-
* (.agent/skills, singular) wins,
|
|
154
|
-
* .agents/skills
|
|
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
|
|
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
|
*/
|
package/src/cli/status.mjs
CHANGED
|
@@ -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 {
|
|
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(
|
|
60
|
-
try { return JSON.parse(fs.readFileSync(
|
|
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(
|
|
66
|
-
try { return JSON.parse(fs.readFileSync(
|
|
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(
|
|
72
|
-
try { return JSON.parse(fs.readFileSync(
|
|
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
|
|
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
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
|
119
|
-
const
|
|
120
|
-
const
|
|
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(
|
|
179
|
-
const buf = fs.readFileSync(
|
|
180
|
-
const stat = fs.statSync(
|
|
186
|
+
if (fs.existsSync(instructionsFile)) {
|
|
187
|
+
const buf = fs.readFileSync(instructionsFile);
|
|
188
|
+
const stat = fs.statSync(instructionsFile);
|
|
181
189
|
localInstructions = {
|
|
182
|
-
path:
|
|
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
|
-
|
|
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(
|
|
209
|
+
? fs.statSync(instructionsFile).mtimeMs
|
|
199
210
|
: null;
|
|
200
211
|
const clientProjections = projectionsStatus(clientsRegistry, instructionsMtime);
|
|
201
212
|
|
package/src/core/embeddings.mjs
CHANGED
|
@@ -61,15 +61,25 @@ let currentDbPath = null;
|
|
|
61
61
|
const dbVssEnabled = new WeakMap();
|
|
62
62
|
|
|
63
63
|
/**
|
|
64
|
-
* better-sqlite3
|
|
65
|
-
*
|
|
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
|
-
|
|
77
|
+
} catch (bareErr) {
|
|
78
|
+
try {
|
|
79
|
+
db.loadExtension(absolutePath);
|
|
80
|
+
} catch {
|
|
81
|
+
throw bareErr;
|
|
82
|
+
}
|
|
73
83
|
}
|
|
74
84
|
}
|
|
75
85
|
|