total-recall-brain 3.39.6 → 3.40.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.
- package/package.json +2 -2
- package/src/cli/connect.mjs +34 -2
- package/src/cli/init.mjs +25 -1
- package/src/cli/plugin/create.mjs +18 -2
- package/src/cli/plugin/install.mjs +11 -1
- package/src/core/brain-updates.mjs +7 -1
- package/src/core/embeddings.mjs +33 -5
- package/src/core/frontmatter.mjs +23 -16
- package/src/core/plugin-state-store.mjs +16 -2
- package/src/core/surface.mjs +4 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "total-recall-brain",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.40.0",
|
|
4
4
|
"description": "Portable SSSS memory, local retrieval and task instructions with explicit plugin loading.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
"homepage": "https://github.com/gregiteen/total-recall#readme",
|
|
76
76
|
"dependencies": {
|
|
77
77
|
"@simplewebauthn/server": "^13.3.2",
|
|
78
|
-
"@ssss/cli": "npm:@gregiteen/ssss-cli@0.11.
|
|
78
|
+
"@ssss/cli": "npm:@gregiteen/ssss-cli@0.11.2",
|
|
79
79
|
"bcrypt": "^6.0.0",
|
|
80
80
|
"better-sqlite3": "^12.11.1",
|
|
81
81
|
"cookie-parser": "^1.4.7",
|
package/src/cli/connect.mjs
CHANGED
|
@@ -43,7 +43,15 @@ export const CLIENTS = {
|
|
|
43
43
|
globalMemoryPath: path.join(os.homedir(), '.dsh', 'memory', 'MEMORY.md'),
|
|
44
44
|
globalUserPath: path.join(os.homedir(), '.dsh', 'memory', 'user.md'),
|
|
45
45
|
localTarget: 'AGENTS.md',
|
|
46
|
-
skillsProjection: { scope: 'project', dir: path.join('.agents', 'skills') }
|
|
46
|
+
skillsProjection: { scope: 'project', dir: path.join('.agents', 'skills') },
|
|
47
|
+
// DSH's default agent preset mounts dsh-skill-filesystem with no config, so
|
|
48
|
+
// the roots it scans are its own defaults: <dshHome>/skills (user-dsh) plus
|
|
49
|
+
// <repo>/.agents/skills and <repo>/.dsh/skills. Its host-plane
|
|
50
|
+
// `skill-filesystem` row is disabled because presets own local discovery, so
|
|
51
|
+
// a profile patch cannot configure these roots. Projecting Total Recall's
|
|
52
|
+
// authored skills into <dshHome>/skills is therefore the supported way to
|
|
53
|
+
// make them reach the `/` slash menu, globally and for every repo.
|
|
54
|
+
globalSkillsProjection: { dir: path.join('.dsh', 'skills') }
|
|
47
55
|
},
|
|
48
56
|
'deepseek-harness': {
|
|
49
57
|
label: 'DeepSeek Harness',
|
|
@@ -51,7 +59,8 @@ export const CLIENTS = {
|
|
|
51
59
|
globalMemoryPath: path.join(os.homedir(), '.dsh', 'memory', 'MEMORY.md'),
|
|
52
60
|
globalUserPath: path.join(os.homedir(), '.dsh', 'memory', 'user.md'),
|
|
53
61
|
localTarget: 'AGENTS.md',
|
|
54
|
-
skillsProjection: { scope: 'project', dir: path.join('.agents', 'skills') }
|
|
62
|
+
skillsProjection: { scope: 'project', dir: path.join('.agents', 'skills') },
|
|
63
|
+
globalSkillsProjection: { dir: path.join('.dsh', 'skills') }
|
|
55
64
|
},
|
|
56
65
|
openclaw: {
|
|
57
66
|
label: 'OpenClaw',
|
|
@@ -1036,6 +1045,29 @@ export default async function connect(args) {
|
|
|
1036
1045
|
}
|
|
1037
1046
|
}
|
|
1038
1047
|
|
|
1048
|
+
// Project GLOBAL skills into the target IDE's home-scoped Agent Skills
|
|
1049
|
+
// directory. Some harnesses read only a home root for skills and never scan a
|
|
1050
|
+
// repo directory, so a client that declares `globalSkillsProjection` needs this
|
|
1051
|
+
// as well as the project projection above. `connect <ide>` must leave a client
|
|
1052
|
+
// usable for both scopes; `init` reaches the same destination through
|
|
1053
|
+
// projectSkillsForScope().
|
|
1054
|
+
if (preset.globalSkillsProjection) {
|
|
1055
|
+
const globalAgentDir = path.join(os.homedir(), '.agent');
|
|
1056
|
+
const skills = discoverRepoSkills(globalAgentDir);
|
|
1057
|
+
if (skills.length > 0) {
|
|
1058
|
+
const destDir = path.join(os.homedir(), preset.globalSkillsProjection.dir);
|
|
1059
|
+
const skillResults = projectSkillsAsCommands(destDir, skills, opts);
|
|
1060
|
+
result.global_skill_commands = skillResults;
|
|
1061
|
+
const fresh = skillResults.filter(r => r.action === 'linked' || r.action === 'copied');
|
|
1062
|
+
if (fresh.length > 0) {
|
|
1063
|
+
result.notes.push(
|
|
1064
|
+
` Global skills projected → ${destDir}/:\n` +
|
|
1065
|
+
fresh.map(r => ` ${r.name}`).join('\n')
|
|
1066
|
+
);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1039
1071
|
// Record this client in the registry so `status` can report freshness
|
|
1040
1072
|
registerClient(brainDir, opts.client, preset, result.projection?.targetPath || null);
|
|
1041
1073
|
|
package/src/cli/init.mjs
CHANGED
|
@@ -249,6 +249,9 @@ export default async function init(args) {
|
|
|
249
249
|
if (!fs.existsSync(path.join(authSource, 'plugin.json'))) throw new Error('Required auth scaffold is missing');
|
|
250
250
|
if (!fs.existsSync(authTarget) && !opts.dryRun) fs.cpSync(authSource, authTarget, { recursive: true, errorOnExist: true, force: false });
|
|
251
251
|
|
|
252
|
+
// Every scaffold plugin's node_modules are wired in Step 2.5, after the skill
|
|
253
|
+
// tree has been copied into the brain (see the note there).
|
|
254
|
+
|
|
252
255
|
// ── Step 2: Seed core skills ──
|
|
253
256
|
logStep('2/4', 'Installing core skills into .agent/skills/');
|
|
254
257
|
|
|
@@ -304,9 +307,30 @@ export default async function init(args) {
|
|
|
304
307
|
}
|
|
305
308
|
|
|
306
309
|
// ── Step 2.5: Create data directories inside the meta-skill (brain dir) ──
|
|
310
|
+
// Wire every scaffold plugin's dependencies AFTER the skill copy above: the
|
|
311
|
+
// plugin directories only exist once the skill tree has been copied, so an
|
|
312
|
+
// earlier pass silently skipped everything except auth (copied separately).
|
|
313
|
+
// A plugin shipped without its node_modules aborts plugin loading for the whole
|
|
314
|
+
// scope — the symptom was 'Cannot find package express' on an unrelated command.
|
|
307
315
|
if (!opts.dryRun) {
|
|
308
316
|
const { preparePluginDependencies } = await import('../core/plugin-dependencies.mjs');
|
|
309
|
-
|
|
317
|
+
const installedPluginsDir = path.join(brainDir, 'plugins');
|
|
318
|
+
for (const id of (fs.existsSync(installedPluginsDir) ? fs.readdirSync(installedPluginsDir) : [])) {
|
|
319
|
+
const target = path.join(installedPluginsDir, id);
|
|
320
|
+
if (!fs.existsSync(path.join(target, 'package.json'))) continue;
|
|
321
|
+
// One plugin whose declared dependency cannot be wired must not abort the
|
|
322
|
+
// whole run: the steps after this one — global skill projection into each
|
|
323
|
+
// IDE, then the vault compile — are independent of it. Before this guard a
|
|
324
|
+
// single unresolvable optional dependency (observed: '@gregiteen/ssss-cli'
|
|
325
|
+
// in a vendored plugin) killed `init` before skill projection, so no IDE
|
|
326
|
+
// ever received the global skills and their slash menus stayed empty with
|
|
327
|
+
// no error the user could connect to the cause.
|
|
328
|
+
try {
|
|
329
|
+
preparePluginDependencies(target);
|
|
330
|
+
} catch (err) {
|
|
331
|
+
logWarn(`Could not wire dependencies for plugin '${id}': ${err.message}`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
310
334
|
}
|
|
311
335
|
logStep('2.5/4', 'Creating user data directories inside meta-skill');
|
|
312
336
|
|
|
@@ -300,6 +300,22 @@ export async function run(argv = []) {
|
|
|
300
300
|
const args = Array.isArray(argv) ? argv.slice(3) : [];
|
|
301
301
|
const sub = args[0] || "status";
|
|
302
302
|
const json = args.includes("--json");
|
|
303
|
+
const wantsHelp = args.includes("--help") || args.includes("-h");
|
|
304
|
+
|
|
305
|
+
if (wantsHelp || sub === "help") {
|
|
306
|
+
const subs = ["status", ...Object.keys(IMPLEMENTED)];
|
|
307
|
+
console.log(\`${pluginName} v${manifest.version}
|
|
308
|
+
|
|
309
|
+
Usage: total-recall ${id} <subcommand> [options]
|
|
310
|
+
|
|
311
|
+
Subcommands:
|
|
312
|
+
\${subs.map((s) => \` \${s}\`).join("\\n")}
|
|
313
|
+
|
|
314
|
+
Options:
|
|
315
|
+
--json Machine-readable output
|
|
316
|
+
--help, -h Show this help\`);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
303
319
|
|
|
304
320
|
if (sub === "status") {
|
|
305
321
|
const status = { plugin: ${JSON.stringify(id)}, implemented: Object.keys(IMPLEMENTED) };
|
|
@@ -312,7 +328,7 @@ export async function run(argv = []) {
|
|
|
312
328
|
|
|
313
329
|
if (IMPLEMENTED[sub]) return IMPLEMENTED[sub](args.slice(1));
|
|
314
330
|
|
|
315
|
-
console.error(\`${pluginName}: "\${sub}" is not implemented.\`);
|
|
331
|
+
console.error(\`${pluginName}: "\${sub}" is not implemented. Run 'total-recall ${id} --help' for available subcommands.\`);
|
|
316
332
|
process.exitCode = 2;
|
|
317
333
|
}
|
|
318
334
|
export default run;
|
|
@@ -494,7 +510,7 @@ const manifest = JSON.parse(fs.readFileSync(path.join(root, 'plugin.json'), 'utf
|
|
|
494
510
|
|
|
495
511
|
test('manifest identifies the plugin', () => {
|
|
496
512
|
assert.equal(manifest.id, ${JSON.stringify(id)});
|
|
497
|
-
assert.match(manifest.version,
|
|
513
|
+
assert.match(manifest.version, /^\\d+\\.\\d+\\.\\d+/);
|
|
498
514
|
assert.equal(manifest.license, 'MIT');
|
|
499
515
|
});
|
|
500
516
|
|
|
@@ -33,7 +33,17 @@ export async function installPlugin(args = []) {
|
|
|
33
33
|
return;
|
|
34
34
|
}
|
|
35
35
|
const { autoCompileSurfaces } = await import('../../core/auto-compile.mjs');
|
|
36
|
-
|
|
36
|
+
// Compile the layer the plugin actually landed in. `scope` is not enough on
|
|
37
|
+
// its own: from a directory with no project brain, projectPluginsDir(cwd)
|
|
38
|
+
// resolves to the global plugins dir, so a plugin installed into the global
|
|
39
|
+
// brain still reports scope 'project'. Ask the resolver whether a project
|
|
40
|
+
// brain exists at all, and otherwise fall back to the global layer — the one
|
|
41
|
+
// that was really written. Assuming 'project' compiled nothing and printed
|
|
42
|
+
// "Auto-compile failed: No project brain found" after a successful install.
|
|
43
|
+
const { resolveBrainDir } = await import('../agent-dir.mjs');
|
|
44
|
+
let targetLayer = 'global';
|
|
45
|
+
try { resolveBrainDir('project'); targetLayer = 'project'; } catch { targetLayer = 'global'; }
|
|
46
|
+
if (!await autoCompileSurfaces({ layer: targetLayer, semantic: false, force: true })) process.exitCode = 1;
|
|
37
47
|
} catch (err) {
|
|
38
48
|
console.error(`❌ ${err.message}`);
|
|
39
49
|
process.exit(1);
|
|
@@ -48,7 +48,13 @@ export function discoverBrainConsumers(roots, { onError } = {}) {
|
|
|
48
48
|
if (deps.length) consumers.push({ root: real, file, pkg, deps });
|
|
49
49
|
}
|
|
50
50
|
for (const entry of fs.readdirSync(real, { withFileTypes: true })) {
|
|
51
|
-
if (entry.isDirectory()
|
|
51
|
+
if (!entry.isDirectory() || ignored.has(entry.name)) continue;
|
|
52
|
+
// Dot-directories are skipped to avoid descending into .git and similar,
|
|
53
|
+
// but .agent holds the brain and its plugin installs. A plugin that runs
|
|
54
|
+
// `npm install` keeps its own node_modules there, and skipping the whole
|
|
55
|
+
// dot tree left those copies stranded on an old version.
|
|
56
|
+
if (entry.name.startsWith('.') && entry.name !== '.agent') continue;
|
|
57
|
+
walk(path.join(real, entry.name));
|
|
52
58
|
}
|
|
53
59
|
}
|
|
54
60
|
for (const root of roots) walk(root);
|
package/src/core/embeddings.mjs
CHANGED
|
@@ -316,6 +316,23 @@ async function getOpenAIEmbedding(text, model = 'text-embedding-3-small') {
|
|
|
316
316
|
}
|
|
317
317
|
|
|
318
318
|
export async function getEmbedding(text, _unused, model = DEFAULT_EMBED_MODEL) {
|
|
319
|
+
const { embedding } = await getEmbeddingWithProvenance(text, model);
|
|
320
|
+
return embedding;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Resolve an embedding together with the provider and model that actually
|
|
325
|
+
* served it.
|
|
326
|
+
*
|
|
327
|
+
* The requested model name is not the serving model: Ollama is tried first and
|
|
328
|
+
* answers with a model discovered from the server, so a row labelled with the
|
|
329
|
+
* hosted default would misreport where the vector came from and whether the
|
|
330
|
+
* text ever left the machine. Persist the returned `provider`/`model`, never
|
|
331
|
+
* the requested default.
|
|
332
|
+
*
|
|
333
|
+
* @returns {Promise<{embedding: number[], provider: string, model: string}>}
|
|
334
|
+
*/
|
|
335
|
+
export async function getEmbeddingWithProvenance(text, model = DEFAULT_EMBED_MODEL) {
|
|
319
336
|
if (process.env.TR_EMBEDDINGS_DISABLED === '1') {
|
|
320
337
|
throw new Error('Semantic embeddings explicitly disabled; local recall remains available');
|
|
321
338
|
}
|
|
@@ -325,6 +342,8 @@ export async function getEmbedding(text, _unused, model = DEFAULT_EMBED_MODEL) {
|
|
|
325
342
|
const openaiKey = resolveOpenAiApiKey();
|
|
326
343
|
|
|
327
344
|
let embedding;
|
|
345
|
+
let provider = null;
|
|
346
|
+
let servedBy = null;
|
|
328
347
|
|
|
329
348
|
// Local/mesh Ollama first: no key, no spend, no rate limit, and unaffected by
|
|
330
349
|
// a hosted provider running out of credit. The model is discovered from the
|
|
@@ -342,6 +361,7 @@ export async function getEmbedding(text, _unused, model = DEFAULT_EMBED_MODEL) {
|
|
|
342
361
|
model: ollamaModel,
|
|
343
362
|
dims: EMBEDDING_DIMS,
|
|
344
363
|
});
|
|
364
|
+
if (embedding) { provider = 'ollama'; servedBy = ollamaModel; }
|
|
345
365
|
}
|
|
346
366
|
}
|
|
347
367
|
} catch (err) {
|
|
@@ -354,6 +374,7 @@ export async function getEmbedding(text, _unused, model = DEFAULT_EMBED_MODEL) {
|
|
|
354
374
|
if (!embedding && openRouterKey) {
|
|
355
375
|
try {
|
|
356
376
|
embedding = await getOpenRouterEmbedding(input);
|
|
377
|
+
if (embedding) { provider = 'openrouter'; servedBy = 'openai/text-embedding-3-small'; }
|
|
357
378
|
} catch (err) {
|
|
358
379
|
logger.debug('embeddings: OpenRouter failed, trying Google fallback', { err: err.message });
|
|
359
380
|
}
|
|
@@ -362,6 +383,9 @@ export async function getEmbedding(text, _unused, model = DEFAULT_EMBED_MODEL) {
|
|
|
362
383
|
if (!embedding && googleKey) {
|
|
363
384
|
try {
|
|
364
385
|
embedding = await getGoogleEmbedding(input, model);
|
|
386
|
+
// getGoogleEmbedding resolves the concrete model internally; record that,
|
|
387
|
+
// not the requested default, so provenance is exact.
|
|
388
|
+
if (embedding) { provider = 'google'; servedBy = await resolveEmbeddingModel(model); }
|
|
365
389
|
} catch (err) {
|
|
366
390
|
logger.debug('embeddings: Google API failed, trying OpenAI fallback', { err: err.message });
|
|
367
391
|
}
|
|
@@ -370,6 +394,7 @@ export async function getEmbedding(text, _unused, model = DEFAULT_EMBED_MODEL) {
|
|
|
370
394
|
if (!embedding && openaiKey) {
|
|
371
395
|
try {
|
|
372
396
|
embedding = await getOpenAIEmbedding(input);
|
|
397
|
+
if (embedding) { provider = 'openai'; servedBy = 'text-embedding-3-small'; }
|
|
373
398
|
} catch (err) {
|
|
374
399
|
logger.debug('embeddings: OpenAI fallback also failed', { err: err.message });
|
|
375
400
|
throw err;
|
|
@@ -383,7 +408,7 @@ export async function getEmbedding(text, _unused, model = DEFAULT_EMBED_MODEL) {
|
|
|
383
408
|
);
|
|
384
409
|
}
|
|
385
410
|
|
|
386
|
-
return embedding;
|
|
411
|
+
return { embedding, provider, model: servedBy };
|
|
387
412
|
}
|
|
388
413
|
|
|
389
414
|
export function cosineSimilarity(a, b) {
|
|
@@ -511,7 +536,10 @@ export async function buildEmbeddingsIndex(nodes, derivedDir, opts = {}) {
|
|
|
511
536
|
}
|
|
512
537
|
|
|
513
538
|
try {
|
|
514
|
-
|
|
539
|
+
// Record the provider/model that actually served this vector. The row used
|
|
540
|
+
// to be stamped with the requested default even when local Ollama answered,
|
|
541
|
+
// which made provenance — and "did this text leave the machine?" — unreadable.
|
|
542
|
+
const { embedding: parentEmbedding, model: servedModel } = await getEmbeddingWithProvenance(text, model);
|
|
515
543
|
const now = new Date().toISOString();
|
|
516
544
|
const embeddingJson = JSON.stringify(parentEmbedding);
|
|
517
545
|
|
|
@@ -524,7 +552,7 @@ export async function buildEmbeddingsIndex(nodes, derivedDir, opts = {}) {
|
|
|
524
552
|
generated_at=excluded.generated_at,
|
|
525
553
|
content_sha256=excluded.content_sha256,
|
|
526
554
|
embedding_json=excluded.embedding_json
|
|
527
|
-
`).run(node.slug, model, now, contentHash, embeddingJson);
|
|
555
|
+
`).run(node.slug, servedModel || model, now, contentHash, embeddingJson);
|
|
528
556
|
|
|
529
557
|
const rowId = db.prepare('SELECT rowid FROM vault_embeddings WHERE slug = ?').get(node.slug).rowid;
|
|
530
558
|
vssReplaceVault(db, rowId, embeddingJson);
|
|
@@ -658,7 +686,7 @@ export async function buildSessionEmbeddingsIndex(sessionsDir, derivedDir, opts
|
|
|
658
686
|
|
|
659
687
|
for (let i = 0; i < chunks.length; i++) {
|
|
660
688
|
const key = chunks.length === 1 ? sessionId : `${sessionId}:chunk-${i}`;
|
|
661
|
-
const embedding = await
|
|
689
|
+
const { embedding, model: servedModel } = await getEmbeddingWithProvenance(chunks[i], model);
|
|
662
690
|
const snippet = chunks[i].slice(0, 300);
|
|
663
691
|
|
|
664
692
|
db.transaction(() => {
|
|
@@ -670,7 +698,7 @@ export async function buildSessionEmbeddingsIndex(sessionsDir, derivedDir, opts
|
|
|
670
698
|
model=excluded.model,
|
|
671
699
|
generated_at=excluded.generated_at,
|
|
672
700
|
embedding_json=excluded.embedding_json
|
|
673
|
-
`).run(key, sessionId, i, chunks.length, snippet, model, new Date().toISOString(), JSON.stringify(embedding));
|
|
701
|
+
`).run(key, sessionId, i, chunks.length, snippet, servedModel || model, new Date().toISOString(), JSON.stringify(embedding));
|
|
674
702
|
|
|
675
703
|
const rowId = db.prepare('SELECT rowid FROM session_embeddings WHERE key = ?').get(key).rowid;
|
|
676
704
|
vssReplaceSession(db, rowId, JSON.stringify(embedding));
|
package/src/core/frontmatter.mjs
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Markdown metadata is data: YAML/JSON only, with no executable engines.
|
|
3
|
+
*
|
|
4
|
+
* YAML frontmatter is read and written by the SSSS parser (@ssss/cli/frontmatter),
|
|
5
|
+
* the single implementation shared with every SSSS host, so Total Recall and SSSS
|
|
6
|
+
* can never disagree about a vault file. This wrapper keeps the gray-matter-style
|
|
7
|
+
* API, the fence/language checks and the CRLF handling.
|
|
8
|
+
*/
|
|
9
|
+
import { parseFrontmatter, serializeDocument, repairEscapedNewlines } from '@ssss/cli/frontmatter';
|
|
10
|
+
|
|
11
|
+
// YAML tags (`!!js/function`, `!!python/object`, ...) can request code; SSSS reads
|
|
12
|
+
// them as plain text, so refuse them up front instead of storing them as data.
|
|
13
|
+
const YAML_TAG = /(?:^|[:,\[{-])[ \t]*!!\S/m;
|
|
11
14
|
|
|
12
15
|
export default function matter(input) {
|
|
13
16
|
const original = Buffer.isBuffer(input) ? input.toString('utf8') : typeof input === 'string' ? input : input?.content;
|
|
14
17
|
if (typeof original !== 'string') throw new TypeError('Markdown content must be a string');
|
|
15
|
-
const text = original.replace(
|
|
18
|
+
const text = repairEscapedNewlines(original.replace(/^/, ''));
|
|
16
19
|
const file = { data: {}, content: text, excerpt: '', orig: original };
|
|
17
20
|
const opening = /^---([^\r\n]*)\r?\n/.exec(text);
|
|
18
21
|
if (!opening || opening[1].startsWith('-')) return file;
|
|
@@ -22,7 +25,8 @@ export default function matter(input) {
|
|
|
22
25
|
const remainder = text.slice(opening[0].length);
|
|
23
26
|
const closing = /^---(?:\r?\n|$)/m.exec(remainder);
|
|
24
27
|
const metadata = closing ? remainder.slice(0, closing.index) : remainder;
|
|
25
|
-
|
|
28
|
+
if (language !== 'json' && YAML_TAG.test(metadata)) throw new Error('YAML tags are not allowed in frontmatter');
|
|
29
|
+
file.data = (language === 'json' ? JSON.parse(metadata || '{}') : parseFrontmatter(metadata.replace(/\r\n/g, '\n'), { strict: true })) || {};
|
|
26
30
|
file.content = closing ? remainder.slice(closing.index + closing[0].length) : '';
|
|
27
31
|
file.matter = metadata;
|
|
28
32
|
file.language = language;
|
|
@@ -30,11 +34,14 @@ export default function matter(input) {
|
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
const newline = value => value.endsWith('\n') ? value : value + '\n';
|
|
33
|
-
|
|
37
|
+
// Dates become ISO strings (SSSS keeps timestamps as strings); undefined is dropped.
|
|
38
|
+
const plain = value => JSON.parse(JSON.stringify(value, (_key, v) => (v instanceof Date ? v.toISOString() : v)));
|
|
39
|
+
|
|
40
|
+
matter.stringify = (input, data) => {
|
|
34
41
|
if (typeof input === 'string' && data == null) return input;
|
|
35
42
|
const file = typeof input === 'string' ? matter(input) : input;
|
|
36
43
|
if (!file || typeof file.content !== 'string') throw new TypeError('Markdown content must be a string');
|
|
37
|
-
const metadata = { ...file.data, ...(data ?? {}) };
|
|
38
|
-
const
|
|
39
|
-
return
|
|
44
|
+
const metadata = plain({ ...file.data, ...(data ?? {}) });
|
|
45
|
+
const head = Object.keys(metadata).length ? serializeDocument(metadata, '') : '';
|
|
46
|
+
return head + newline(file.content);
|
|
40
47
|
};
|
|
@@ -71,8 +71,22 @@ export async function executePluginState(request) {
|
|
|
71
71
|
}
|
|
72
72
|
// A guard serializes state read/compare/commit across native worker processes.
|
|
73
73
|
const lock = path.join(root, '.mutation-lock');
|
|
74
|
-
try {
|
|
75
|
-
|
|
74
|
+
try {
|
|
75
|
+
fs.mkdirSync(lock, { mode: 0o700 });
|
|
76
|
+
} catch (err) {
|
|
77
|
+
// Distinguish a genuine concurrent mutation from an environment failure so
|
|
78
|
+
// callers are not told the store is busy when it is actually inaccessible.
|
|
79
|
+
if (err && err.code === 'EEXIST') {
|
|
80
|
+
throw new Error('Plugin state mutation is busy; another mutation holds the guard');
|
|
81
|
+
}
|
|
82
|
+
if (err && (err.code === 'EACCES' || err.code === 'EPERM' || err.code === 'EROFS')) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
`Plugin state store is not writable (${err.code}): cannot create the mutation guard at ${lock}. ` +
|
|
85
|
+
'Grant write access to this brain root, or point TR_SECRETS_BRAIN at a store you can write to.'
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
throw new Error(`Plugin state mutation guard could not be created at ${lock}: ${err?.message || err}`);
|
|
89
|
+
}
|
|
76
90
|
try {
|
|
77
91
|
const durableKeys = new FileIdempotencyStore(path.join(root, '.idempotency'));
|
|
78
92
|
const intentHash = crypto.createHash('sha256').update(JSON.stringify({ path: relative, action, value, expectedVersion, format: request.format })).digest('hex');
|
package/src/core/surface.mjs
CHANGED
|
@@ -758,7 +758,10 @@ export async function compileSurface({ vaultDir, skillsDir, derivedDir, instruct
|
|
|
758
758
|
logger.info('surface', 'Semantic embeddings explicitly disabled; local indexes updated.', { derivedDir });
|
|
759
759
|
} else try {
|
|
760
760
|
const { buildEmbeddingsIndex } = await import('./embeddings.mjs');
|
|
761
|
-
|
|
761
|
+
// Forward `force`: the builder is content-hash incremental and skips a node
|
|
762
|
+
// whose text is unchanged, so without this `compile --force` rebuilt nothing
|
|
763
|
+
// and rows kept an outdated model label.
|
|
764
|
+
const built = await buildEmbeddingsIndex(nodes, derivedDir, { force });
|
|
762
765
|
semanticResult = {
|
|
763
766
|
indexed: built.built,
|
|
764
767
|
skipped: built.skipped,
|