total-recall-brain 3.39.2 → 3.39.3
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/plugin/install.mjs +5 -0
- package/src/core/plugin-store.mjs +57 -0
- package/src/core/surface.mjs +36 -2
package/package.json
CHANGED
|
@@ -18,6 +18,11 @@ export async function installPlugin(args = []) {
|
|
|
18
18
|
console.log(`\n✅ Installed '${result.plugin.name}' (id: ${result.plugin.id}, v${result.plugin.version}) from ${from}`);
|
|
19
19
|
console.log(` Location: ${result.plugin.dir}`);
|
|
20
20
|
console.log(` sha256: ${result.sha256}`);
|
|
21
|
+
if (result.dependencies?.installed) {
|
|
22
|
+
console.log(` Dependencies: installed ${result.dependencies.packages.length} package(s)`);
|
|
23
|
+
} else if (result.dependencies?.reason && !['no package.json', 'no dependencies declared'].includes(result.dependencies.reason)) {
|
|
24
|
+
console.log(` Dependencies: NOT installed (${result.dependencies.reason})`);
|
|
25
|
+
}
|
|
21
26
|
const cmds = syncPluginCommands(result.plugin.dir, { global: isGlobal });
|
|
22
27
|
if (cmds.created.length) console.log(` Commands: ${cmds.created.map((c) => `total-recall ${c}`).join(', ')}`);
|
|
23
28
|
for (const c of cmds.skipped) console.log(` Skipped command '${c.name}': ${c.reason}`);
|
|
@@ -293,6 +293,50 @@ export async function recompileAfterPluginMutation(projectRoot = process.cwd(),
|
|
|
293
293
|
}
|
|
294
294
|
}
|
|
295
295
|
|
|
296
|
+
/**
|
|
297
|
+
* Install a copied plugin's declared runtime dependencies.
|
|
298
|
+
*
|
|
299
|
+
* A plugin must carry its dependencies before the loader imports it: a single
|
|
300
|
+
* unresolvable import from any installed plugin aborts every plugin command in
|
|
301
|
+
* that scope, not just the plugin that owns it. Linked plugins are developer
|
|
302
|
+
* checkouts that keep their own `node_modules`, so they are never touched.
|
|
303
|
+
*
|
|
304
|
+
* @returns {Promise<{ installed: boolean, packages?: string[], reason?: string }>}
|
|
305
|
+
*/
|
|
306
|
+
export async function installPluginDependencies(dest, id) {
|
|
307
|
+
const pkgPath = path.join(dest, 'package.json');
|
|
308
|
+
if (!fs.existsSync(pkgPath)) return { installed: false, reason: 'no package.json' };
|
|
309
|
+
|
|
310
|
+
let pkg;
|
|
311
|
+
try {
|
|
312
|
+
pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
313
|
+
} catch (err) {
|
|
314
|
+
throw new Error(`Plugin '${id}' was copied but its package.json is unreadable: ${err.message}`);
|
|
315
|
+
}
|
|
316
|
+
const packages = Object.keys(pkg.dependencies || {});
|
|
317
|
+
if (packages.length === 0) return { installed: false, reason: 'no dependencies declared' };
|
|
318
|
+
|
|
319
|
+
try {
|
|
320
|
+
await execFileAsync('npm', ['--version'], { timeout: 20_000 });
|
|
321
|
+
} catch {
|
|
322
|
+
console.warn(`⚠️ Plugin '${id}' declares ${packages.length} dependency(ies) but npm is unavailable. Run 'npm install --omit=dev' in ${dest}.`);
|
|
323
|
+
return { installed: false, reason: 'npm unavailable' };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
try {
|
|
327
|
+
await execFileAsync('npm', ['install', '--omit=dev', '--no-audit', '--no-fund', '--loglevel=error'], {
|
|
328
|
+
cwd: dest,
|
|
329
|
+
timeout: 600_000,
|
|
330
|
+
maxBuffer: 16 * 1024 * 1024
|
|
331
|
+
});
|
|
332
|
+
} catch (err) {
|
|
333
|
+
const detail = (err.stderr || err.stdout || err.message || '').toString().trim()
|
|
334
|
+
.split('\n').filter(Boolean).slice(-3).join(' ');
|
|
335
|
+
throw new Error(`Plugin '${id}' was copied but its dependencies (${packages.join(', ')}) failed to install: ${detail}`);
|
|
336
|
+
}
|
|
337
|
+
return { installed: true, packages };
|
|
338
|
+
}
|
|
339
|
+
|
|
296
340
|
/**
|
|
297
341
|
* Install a plugin.
|
|
298
342
|
* @param {string} source bundled id | public share URL | `peer:<host>/<id>` | git URL | local path
|
|
@@ -310,6 +354,7 @@ export async function installPlugin(source, options = {}) {
|
|
|
310
354
|
let dest;
|
|
311
355
|
let sourceRecord;
|
|
312
356
|
let verifiedSha = null;
|
|
357
|
+
let dependencies = null;
|
|
313
358
|
|
|
314
359
|
const resolved = resolveBundle ? await resolveBundle(source, options) : null;
|
|
315
360
|
const bundled = !resolved && ID_PATTERN.test(source) ? listBundledPlugins().find((p) => p.id === source) : null;
|
|
@@ -361,6 +406,17 @@ export async function installPlugin(source, options = {}) {
|
|
|
361
406
|
}
|
|
362
407
|
}
|
|
363
408
|
|
|
409
|
+
if (sourceRecord.kind !== 'link') {
|
|
410
|
+
try {
|
|
411
|
+
dependencies = await installPluginDependencies(dest, manifest.id);
|
|
412
|
+
} catch (err) {
|
|
413
|
+
// A plugin copied without its dependencies would break every plugin
|
|
414
|
+
// command in this scope, so undo the copy instead of leaving it behind.
|
|
415
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
416
|
+
throw err;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
364
420
|
const sha256 = verifiedSha || hashPluginDir(dest).sha256;
|
|
365
421
|
const vaultRoot = vaultForPluginsDir(pluginsDir);
|
|
366
422
|
try {
|
|
@@ -380,6 +436,7 @@ export async function installPlugin(source, options = {}) {
|
|
|
380
436
|
plugin: { id: manifest.id, name: manifest.name, version: manifest.version, dir: dest, scope },
|
|
381
437
|
source: sourceRecord,
|
|
382
438
|
sha256,
|
|
439
|
+
dependencies,
|
|
383
440
|
compilation
|
|
384
441
|
};
|
|
385
442
|
}
|
package/src/core/surface.mjs
CHANGED
|
@@ -343,6 +343,38 @@ export function readRuleBudgets(skillsDir) {
|
|
|
343
343
|
return out;
|
|
344
344
|
}
|
|
345
345
|
|
|
346
|
+
const DEFAULT_CONTEXT_BUDGET = { total_tokens: 4000, bootstrap_total_tokens: 2500 };
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Instruction-surface token budget from surface.yml files and the environment.
|
|
350
|
+
*
|
|
351
|
+
* The rule-section budgets above are characters, so a brain whose required
|
|
352
|
+
* rules are rich can need more text than the fixed token total allows and then
|
|
353
|
+
* write no surface at all. A project brain overrides the global brain;
|
|
354
|
+
* TR_CONTEXT_BUDGET_TOKENS overrides both, as JSON of the same keys.
|
|
355
|
+
*/
|
|
356
|
+
export function readContextBudget(skillsDir) {
|
|
357
|
+
const out = {};
|
|
358
|
+
const dirs = [skillsDir ? path.join(skillsDir, 'total-recall') : null, globalBrainDir].filter(Boolean);
|
|
359
|
+
for (const dir of dirs.reverse()) {
|
|
360
|
+
try {
|
|
361
|
+
const cfg = yaml.parse(fs.readFileSync(path.join(dir, 'config', 'surface.yml'), 'utf8')) || {};
|
|
362
|
+
for (const k of Object.keys(DEFAULT_CONTEXT_BUDGET)) {
|
|
363
|
+
const v = Number(cfg?.budget?.[k]);
|
|
364
|
+
if (Number.isFinite(v) && v > 0) out[k] = v;
|
|
365
|
+
}
|
|
366
|
+
} catch {
|
|
367
|
+
/* no surface.yml in this brain */
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
try {
|
|
371
|
+
Object.assign(out, JSON.parse(process.env.TR_CONTEXT_BUDGET_TOKENS || '{}'));
|
|
372
|
+
} catch {
|
|
373
|
+
/* ignore a malformed override */
|
|
374
|
+
}
|
|
375
|
+
return { ...DEFAULT_CONTEXT_BUDGET, ...out };
|
|
376
|
+
}
|
|
377
|
+
|
|
346
378
|
export function legacyRuleContributions(skillsDir) {
|
|
347
379
|
if (!skillsDir) return [];
|
|
348
380
|
return ['invariants', 'preferences', 'corrections'].map(name => ({
|
|
@@ -373,9 +405,11 @@ export function buildInstalledPluginsSummary(projectRoot = process.cwd()) {
|
|
|
373
405
|
}
|
|
374
406
|
|
|
375
407
|
export async function buildRulesBlock(skillsDir, nodes = [], {
|
|
376
|
-
consumer = 'ide', derivedDir, vaultDir, projectRoot, actions = [], bootstrap = false, total
|
|
408
|
+
consumer = 'ide', derivedDir, vaultDir, projectRoot, actions = [], bootstrap = false, total,
|
|
377
409
|
} = {}) {
|
|
378
410
|
const root = projectRoot || (skillsDir ? path.dirname(path.dirname(skillsDir)) : process.cwd());
|
|
411
|
+
const budget = readContextBudget(skillsDir);
|
|
412
|
+
const tokenBudget = total ?? (bootstrap ? budget.bootstrap_total_tokens : budget.total_tokens);
|
|
379
413
|
const rules = selectRules(curatedRules(nodes), { actions, projectRoot: root, bootstrap });
|
|
380
414
|
const contributions = [];
|
|
381
415
|
const counts = category => rules.filter(n => n.category === category).length;
|
|
@@ -412,7 +446,7 @@ DO NOT SKIP THIS. Run Total Recall CLI help and context first before doing anyth
|
|
|
412
446
|
const plugin = await assemblePluginContexts({ projectRoot: root, vaultDir, nodes, derivedDir });
|
|
413
447
|
contributions.push({ id: 'plugins', text: plugin });
|
|
414
448
|
}
|
|
415
|
-
const result = assembleContext(contributions, { total });
|
|
449
|
+
const result = assembleContext(contributions, { total: tokenBudget });
|
|
416
450
|
if (derivedDir) {
|
|
417
451
|
fs.mkdirSync(derivedDir, { recursive: true });
|
|
418
452
|
atomicWrite(path.join(derivedDir, bootstrap ? 'bootstrap-accounting.json' : 'context-accounting.json'), JSON.stringify({ ready: result.ready, ...result.stats }, null, 2));
|