total-recall-brain 3.28.2 → 3.30.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.
Files changed (132) hide show
  1. package/bin/total-recall.mjs +24 -1
  2. package/docs/ARCHITECTURE.md +26 -18
  3. package/extension/background.js +259 -84
  4. package/extension/content-script.css +7 -13
  5. package/extension/content-script.js +252 -477
  6. package/extension/icons/icon-128.png +0 -0
  7. package/extension/icons/icon-16.png +0 -0
  8. package/extension/icons/icon-32.png +0 -0
  9. package/extension/icons/icon-48.png +0 -0
  10. package/extension/icons/mark.svg +6 -0
  11. package/extension/lib/brain-client.js +160 -73
  12. package/extension/lib/tokens.css +187 -0
  13. package/extension/lib/ui-helpers.js +229 -0
  14. package/extension/lib/ui-helpers.spec.mjs +94 -0
  15. package/extension/manifest.json +20 -5
  16. package/extension/options/options.css +64 -218
  17. package/extension/options/options.html +91 -43
  18. package/extension/options/options.js +208 -85
  19. package/extension/sidepanel/sidepanel.css +268 -970
  20. package/extension/sidepanel/sidepanel.html +200 -166
  21. package/extension/sidepanel/sidepanel.js +788 -942
  22. package/frontend/dist/assets/index-DGMNZi_1.js +5742 -0
  23. package/frontend/dist/index.html +1 -1
  24. package/frontend/src/api/plugins.ts +124 -92
  25. package/frontend/src/components/ResearchAgendaTab.spec.tsx +11 -0
  26. package/frontend/src/components/ResearchAgendaTab.tsx +19 -14
  27. package/frontend/src/pages/PluginsPage.spec.tsx +87 -16
  28. package/frontend/src/pages/PluginsPage.tsx +454 -1218
  29. package/package.json +2 -1
  30. package/plugins/code-quality/README.md +25 -0
  31. package/plugins/code-quality/cli.mjs +25 -0
  32. package/plugins/code-quality/plugin.json +17 -0
  33. package/plugins/git-sentinel/README.md +27 -0
  34. package/{scaffold/.agent/plugins → plugins}/git-sentinel/cli.mjs +8 -5
  35. package/plugins/git-sentinel/plugin.json +18 -0
  36. package/plugins/system-monitor/README.md +26 -0
  37. package/{scaffold/.agent/plugins → plugins}/system-monitor/cli.mjs +5 -4
  38. package/{scaffold/.agent/plugins → plugins}/system-monitor/generator.mjs +4 -5
  39. package/plugins/system-monitor/plugin.json +29 -0
  40. package/scaffold/.agent/skills/meta-harness/SKILL.md +4 -4
  41. package/scaffold/.agent/skills/plugins/SKILL.md +53 -0
  42. package/scaffold/.agent/skills/total-recall/scripts/brain-state.json +1 -0
  43. package/src/cli/agent.mjs +1 -1
  44. package/src/cli/app/index.mjs +493 -0
  45. package/src/cli/brain.mjs +33 -3
  46. package/src/cli/command.mjs +80 -41
  47. package/src/cli/harness.mjs +1 -1
  48. package/src/cli/init.mjs +3 -1
  49. package/src/cli/mesh.mjs +118 -12
  50. package/src/cli/plugin/create.mjs +144 -25
  51. package/src/cli/plugin/index.mjs +83 -53
  52. package/src/cli/plugin/install.mjs +10 -127
  53. package/src/cli/plugin/list.mjs +44 -44
  54. package/src/cli/plugin/remove.mjs +23 -30
  55. package/src/cli/plugin/scaffold-cmd.mjs +30 -0
  56. package/src/cli/plugin/search.mjs +108 -23
  57. package/src/cli/rebuild.mjs +37 -1
  58. package/src/cli/recall.mjs +17 -2
  59. package/src/cli/research.mjs +4 -6
  60. package/src/cli/share.mjs +2 -0
  61. package/src/core/app-deploy/apply.mjs +386 -0
  62. package/src/core/app-deploy/design-tokens.mjs +217 -0
  63. package/src/core/app-deploy/plan.mjs +249 -0
  64. package/src/core/app-deploy/resolve.mjs +295 -0
  65. package/src/core/app-deploy/source.mjs +337 -0
  66. package/src/core/app-deploy/standalone.mjs +248 -0
  67. package/src/core/app-deploy/upgrade.mjs +256 -0
  68. package/src/core/app-deploy/verify.mjs +213 -0
  69. package/src/core/bound-hosts.mjs +1 -1
  70. package/src/core/brain-registry.mjs +146 -0
  71. package/src/core/brain-state.json +1 -0
  72. package/src/core/daemon-loop.mjs +30 -26
  73. package/src/core/dream.mjs +5 -17
  74. package/src/core/fact-seeker.mjs +53 -368
  75. package/src/core/lan-discovery.mjs +3 -3
  76. package/src/core/mesh-access-sync.mjs +134 -0
  77. package/src/core/mesh-access.mjs +18 -3
  78. package/src/core/mesh-discover.mjs +173 -0
  79. package/src/core/mesh.mjs +131 -32
  80. package/src/core/optimizer.mjs +4 -41
  81. package/src/core/plugin-bundle.mjs +167 -0
  82. package/src/core/plugin-context.mjs +8 -13
  83. package/src/core/plugin-loader.mjs +272 -68
  84. package/src/core/plugin-peers.mjs +163 -0
  85. package/src/core/plugin-public.mjs +85 -0
  86. package/src/core/plugin-runner-child.mjs +24 -0
  87. package/src/core/plugin-runner.mjs +102 -0
  88. package/src/core/plugin-store.mjs +384 -0
  89. package/src/core/plugin-tasks.mjs +212 -0
  90. package/src/core/post-mortem.mjs +40 -9
  91. package/src/core/research-gate.mjs +182 -0
  92. package/src/core/research-queue.mjs +47 -7
  93. package/src/core/research-surface.mjs +211 -0
  94. package/src/core/research.mjs +25 -32
  95. package/src/core/scheduler.mjs +67 -153
  96. package/src/core/schema.mjs +129 -0
  97. package/src/core/search.mjs +12 -1
  98. package/src/core/secret-integration-research.mjs +12 -8
  99. package/src/core/secrets-store.mjs +32 -2
  100. package/src/core/session-watcher.mjs +6 -0
  101. package/src/core/surface.mjs +64 -3
  102. package/src/core/task-executors.mjs +8 -45
  103. package/src/core/vault-cache.mjs +70 -4
  104. package/src/server/api.mjs +21 -0
  105. package/src/server/rest.mjs +2 -0
  106. package/src/server/route-manifest.json +21 -5
  107. package/src/server/routes/collab.mjs +38 -11
  108. package/src/server/routes/extension.mjs +3 -2
  109. package/src/server/routes/memory.mjs +24 -2
  110. package/src/server/routes/mesh.mjs +7 -7
  111. package/src/server/routes/plugins-mesh.mjs +49 -0
  112. package/src/server/routes/plugins.mjs +91 -426
  113. package/src/server/routes/research.mjs +5 -2
  114. package/src/server/routes/share.mjs +2 -0
  115. package/src/server/tools.mjs +29 -2
  116. package/extension/icons/README.md +0 -27
  117. package/extension/lib/preconfigured.js +0 -5
  118. package/extension/popup/popup.css +0 -227
  119. package/extension/popup/popup.html +0 -50
  120. package/extension/popup/popup.js +0 -175
  121. package/frontend/dist/assets/index-U7o-qU99.js +0 -5742
  122. package/scaffold/.agent/plugins/.gitkeep +0 -0
  123. package/scaffold/.agent/plugins/code-quality/README.md +0 -16
  124. package/scaffold/.agent/plugins/code-quality/cli.mjs +0 -15
  125. package/scaffold/.agent/plugins/code-quality/plugin.json +0 -34
  126. package/scaffold/.agent/plugins/git-sentinel/README.md +0 -16
  127. package/scaffold/.agent/plugins/git-sentinel/plugin.json +0 -34
  128. package/scaffold/.agent/plugins/meta-harness/README.md +0 -16
  129. package/scaffold/.agent/plugins/meta-harness/cli.mjs +0 -34
  130. package/scaffold/.agent/plugins/meta-harness/plugin.json +0 -34
  131. package/scaffold/.agent/plugins/system-monitor/README.md +0 -16
  132. package/scaffold/.agent/plugins/system-monitor/plugin.json +0 -38
@@ -1,4 +1,5 @@
1
- import { discoverPlugins } from '../../core/plugin-loader.mjs';
1
+ import { listInstalledPlugins } from '../../core/plugin-store.mjs';
2
+ import { projectPluginsDir, globalPluginsDir } from '../../core/plugin-loader.mjs';
2
3
 
3
4
  function stripAnsi(str) {
4
5
  return String(str || '').replace(/\x1b\[[0-9;]*m/g, '');
@@ -10,10 +11,14 @@ function padAnsi(str, targetLen) {
10
11
  return String(str || '') + ' '.repeat(paddingNeeded);
11
12
  }
12
13
 
14
+ function clip(str, len) {
15
+ const s = String(str || '');
16
+ return s.length > len ? s.slice(0, len - 1) + '…' : s;
17
+ }
18
+
13
19
  export async function listPlugins(args = []) {
14
20
  const isJson = args.includes('--json');
15
- const projectRoot = process.cwd();
16
- const plugins = discoverPlugins(projectRoot);
21
+ const plugins = listInstalledPlugins(process.cwd());
17
22
 
18
23
  if (isJson) {
19
24
  console.log(JSON.stringify(plugins, null, 2));
@@ -23,61 +28,56 @@ export async function listPlugins(args = []) {
23
28
  console.log(`\n🔌 Total Recall — Installed Plugins\n`);
24
29
 
25
30
  if (plugins.length === 0) {
26
- console.log(' No plugins installed in this project or globally (~/.agent/plugins/).');
27
- console.log(' Install one with: npx total-recall plugin install <path|git-url>\n');
31
+ console.log(` No plugins installed in ${projectPluginsDir(process.cwd())}`);
32
+ console.log(` or globally in ${globalPluginsDir()}.`);
33
+ console.log(' See what you can install: npx total-recall plugin available\n');
28
34
  return;
29
35
  }
30
36
 
31
- const colId = 20;
32
- const colName = 28;
33
- const colVer = 10;
34
- const colStatus = 12;
35
- const colCats = 24;
36
-
37
- const topBorder = `┌${'─'.repeat(colId + 2)}┬${'─'.repeat(colName + 2)}┬${'─'.repeat(colVer + 2)}┬${'─'.repeat(colStatus + 2)}┬${'─'.repeat(colCats + 2)}┐`;
38
- const midBorder = `├${'─'.repeat(colId + 2)}┼${'─'.repeat(colName + 2)}┼${'─'.repeat(colVer + 2)}┼${'─'.repeat(colStatus + 2)}┼${'─'.repeat(colCats + 2)}┤`;
39
- const botBorder = `└${'─'.repeat(colId + 2)}┴${'─'.repeat(colName + 2)}┴${'─'.repeat(colVer + 2)}┴${'─'.repeat(colStatus + 2)}┴${'─'.repeat(colCats + 2)}┘`;
37
+ const cols = [
38
+ ['Plugin ID', 22],
39
+ ['Version', 9],
40
+ ['Scope', 8],
41
+ ['Source', 22],
42
+ ['Shared', 7],
43
+ ['Status', 10]
44
+ ];
45
+ const line = (l, m, r) => l + cols.map(([, w]) => '─'.repeat(w + 2)).join(m) + r;
46
+ const row = (cells) => '│ ' + cells.map((c, i) => padAnsi(c, cols[i][1])).join(' │ ') + ' │';
40
47
 
41
- console.log(topBorder);
42
- console.log(
43
- `│ ${padAnsi('Plugin ID', colId)} │ ` +
44
- `${padAnsi('Name', colName)} │ ` +
45
- `${padAnsi('Version', colVer)} │ ` +
46
- `${padAnsi('Status', colStatus)} │ ` +
47
- `${padAnsi('SSSS Categories', colCats)} │`
48
- );
49
- console.log(midBorder);
48
+ console.log(line('┌', '┬', '┐'));
49
+ console.log(row(cols.map(([h]) => h)));
50
+ console.log(line('├', '┼', '┤'));
50
51
 
51
52
  for (const p of plugins) {
52
- const id = p.id;
53
- const name = (p.manifest.name || id).slice(0, colName);
54
- const ver = p.manifest.version ? `v${p.manifest.version}` : '—';
55
- const status = p.valid ? '\x1b[32mActive ✅\x1b[0m' : '\x1b[31mInvalid ❌\x1b[0m';
56
- const cats = (p.manifest.ssss_schemas?.categories || []).map(c => c.name).join(', ') || '—';
57
- const catStr = cats.length > colCats ? cats.slice(0, colCats - 3) + '...' : cats;
58
-
59
- console.log(
60
- `│ \x1b[1m${padAnsi(id, colId)}\x1b[0m │ ` +
61
- `${padAnsi(name, colName)} │ ` +
62
- `${padAnsi(ver, colVer)} │ ` +
63
- `${padAnsi(status, colStatus)} │ ` +
64
- `${padAnsi(catStr, colCats)} │`
65
- );
53
+ const source = p.source.kind === 'peer' ? `peer ${p.source.peer_hostname}` : p.source.kind;
54
+ const status = !p.valid
55
+ ? '\x1b[31minvalid\x1b[0m'
56
+ : p.modified_since_install ? '\x1b[33mmodified\x1b[0m' : '\x1b[32mok\x1b[0m';
57
+ console.log(row([
58
+ `\x1b[1m${clip(p.id, 22)}\x1b[0m`,
59
+ `v${p.version}`,
60
+ p.scope,
61
+ clip(source, 22),
62
+ p.shared ? 'yes' : 'no',
63
+ status
64
+ ]));
66
65
  }
67
66
 
68
- console.log(botBorder);
67
+ console.log(line('└', '┴', '┘'));
69
68
 
70
- // Show detailed validation errors if any
71
69
  const invalid = plugins.filter(p => !p.valid);
72
70
  if (invalid.length > 0) {
73
- console.log(`\n⚠️ Configuration Warnings:`);
71
+ console.log(`\n⚠️ Manifest problems:`);
74
72
  for (const inv of invalid) {
75
- console.log(` Plugin '${inv.id}':`);
76
- for (const err of inv.errors) {
77
- console.log(` - ${err}`);
78
- }
73
+ console.log(` ${inv.id}:`);
74
+ for (const err of inv.errors) console.log(` - ${err}`);
79
75
  }
80
76
  }
77
+ const modified = plugins.filter(p => p.modified_since_install);
78
+ if (modified.length > 0) {
79
+ console.log(`\nℹ️ Changed on disk since install (content hash differs): ${modified.map(p => p.id).join(', ')}`);
80
+ }
81
81
 
82
82
  console.log();
83
83
  }
@@ -1,12 +1,9 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import os from 'node:os';
1
+ import { uninstallPlugin, setPluginShared } from '../../core/plugin-store.mjs';
2
+ import { publicPluginShareUrl } from '../../core/plugin-public.mjs';
4
3
 
5
4
  export async function removePlugin(args = []) {
6
5
  const isGlobal = args.includes('--global') || args.includes('-g');
7
- const cleanArgs = args.filter(a => a !== '--global' && a !== '-g');
8
-
9
- const id = cleanArgs[0];
6
+ const id = args.find(a => !a.startsWith('-'));
10
7
 
11
8
  if (!id) {
12
9
  console.error('❌ Error: Missing plugin ID to remove.');
@@ -14,38 +11,34 @@ export async function removePlugin(args = []) {
14
11
  process.exit(1);
15
12
  }
16
13
 
17
- const searchDirs = isGlobal
18
- ? [path.join(os.homedir(), '.agent', 'plugins')]
19
- : [
20
- path.join(process.cwd(), '.agent', 'plugins'),
21
- path.join(os.homedir(), '.agent', 'plugins')
22
- ];
23
-
24
- let targetPath = null;
25
- for (const dir of searchDirs) {
26
- const candidate = path.join(dir, id);
27
- if (fs.existsSync(candidate)) {
28
- targetPath = candidate;
29
- break;
30
- }
14
+ try {
15
+ const result = await uninstallPlugin(id, { projectRoot: process.cwd(), global: isGlobal ? true : undefined });
16
+ console.log(`\n✅ Removed plugin '${id}' (${result.scope}) from ${result.dir}\n`);
17
+ } catch (err) {
18
+ console.error(`❌ ${err.message}`);
19
+ process.exit(1);
31
20
  }
21
+ }
32
22
 
33
- if (!targetPath) {
34
- console.error(`❌ Plugin '${id}' was not found in installed plugin directories.`);
23
+ export async function sharePlugin(args = [], shared = true) {
24
+ const id = args.find(a => !a.startsWith('-'));
25
+ if (!id) {
26
+ console.error(`❌ Error: Missing plugin ID. Usage: total-recall plugin ${shared ? 'share' : 'unshare'} <id>`);
35
27
  process.exit(1);
36
28
  }
37
-
38
29
  try {
39
- const stat = fs.lstatSync(targetPath);
40
- if (stat.isSymbolicLink()) {
41
- fs.unlinkSync(targetPath);
42
- console.log(`\n✅ Unlinked plugin '${id}' from ${targetPath}\n`);
30
+ const plugin = await setPluginShared(id, shared, { projectRoot: process.cwd() });
31
+ if (shared) {
32
+ console.log(`\n✅ '${id}' is now shared (sha256 ${plugin.sha256?.slice(0, 16)}…).`);
33
+ const url = publicPluginShareUrl(id, plugin.sha256);
34
+ if (url) console.log(` Send this link to another Total Recall user: ${url}`);
35
+ else console.log(' Set TR_PUBLIC_BASE_URL to your public HTTPS origin to print a share link.');
36
+ console.log(' Mesh peers can also install it with: npx total-recall plugin install peer:<this-node>/' + id + '\n');
43
37
  } else {
44
- fs.rmSync(targetPath, { recursive: true, force: true });
45
- console.log(`\n✅ Removed plugin '${id}' from ${targetPath}\n`);
38
+ console.log(`\n✅ '${id}' is no longer shared.\n`);
46
39
  }
47
40
  } catch (err) {
48
- console.error(`❌ Failed to remove plugin '${id}': ${err.message}`);
41
+ console.error(`❌ ${err.message}`);
49
42
  process.exit(1);
50
43
  }
51
44
  }
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Scaffold CLI command handler
4
+ * Handles:
5
+ * npx total-recall scaffold-plugin <id> [options]
6
+ * npx total-recall scaffold plugin <id> [options]
7
+ * npx total-recall scaffold repo <path> [options]
8
+ */
9
+
10
+ import { createPlugin } from './create.mjs';
11
+
12
+ export async function run(argv = []) {
13
+ let args = Array.isArray(argv) ? argv.slice(2) : [];
14
+ if (args[0] === 'scaffold' || args[0] === 'scaffold-plugin') {
15
+ args = args.slice(1);
16
+ }
17
+ if (args[0] === 'plugin') {
18
+ args = args.slice(1);
19
+ }
20
+
21
+ await createPlugin(args);
22
+ }
23
+
24
+ export default async function (args = []) {
25
+ let cleanArgs = Array.isArray(args) ? args : [];
26
+ if (cleanArgs[0] === 'plugin') {
27
+ cleanArgs = cleanArgs.slice(1);
28
+ }
29
+ await createPlugin(cleanArgs);
30
+ }
@@ -1,37 +1,122 @@
1
- import { CURATED_CATALOG } from "../../server/routes/plugins.mjs";
1
+ import { listAvailableBundled } from '../../core/plugin-store.mjs';
2
+ import { listPeerPlugins } from '../../core/plugin-peers.mjs';
2
3
 
3
- export async function searchCatalog(args = []) {
4
- const isJson = args.includes("--json");
5
- const cleanArgs = args.filter(a => a !== "--json");
6
- const query = cleanArgs.join(" ").toLowerCase().trim();
7
-
8
- let results = CURATED_CATALOG;
9
- if (query) {
10
- results = CURATED_CATALOG.filter(p =>
11
- p.id.toLowerCase().includes(query) ||
12
- p.name.toLowerCase().includes(query) ||
13
- p.description.toLowerCase().includes(query) ||
14
- p.tags.some(t => t.toLowerCase().includes(query))
15
- );
4
+ function parseArgs(args) {
5
+ const isJson = args.includes('--json');
6
+ let useCase = null;
7
+ const words = [];
8
+ for (let i = 0; i < args.length; i++) {
9
+ if (args[i] === '--json') continue;
10
+ if (args[i] === '--use-case' && args[i + 1]) {
11
+ useCase = args[++i];
12
+ continue;
13
+ }
14
+ words.push(args[i]);
16
15
  }
16
+ return { isJson, useCase, query: words.join(' ').toLowerCase().trim() };
17
+ }
18
+
19
+ function matches(p, query, useCase) {
20
+ if (useCase && !(p.use_cases || []).includes(useCase)) return false;
21
+ if (!query) return true;
22
+ return [p.id, p.name, p.description, ...(p.use_cases || [])]
23
+ .some((v) => String(v || '').toLowerCase().includes(query));
24
+ }
25
+
26
+ function printPlugin(p, installHint) {
27
+ const installed = p.installed ? ' \x1b[32m(installed)\x1b[0m' : '';
28
+ console.log(`\x1b[1;36m● ${p.name}\x1b[0m (id: \x1b[33m${p.id}\x1b[0m, v${p.version})${installed}`);
29
+ console.log(` ${p.description}`);
30
+ if (p.use_cases?.length) console.log(` Use cases: ${p.use_cases.join(', ')}`);
31
+ if (p.sha256) console.log(` sha256: ${p.sha256.slice(0, 16)}… (${p.file_count} files, ${p.size_bytes} bytes)`);
32
+ if (!p.installed) console.log(` Install: \x1b[32mnpx total-recall plugin install ${installHint}\x1b[0m`);
33
+ console.log();
34
+ }
35
+
36
+ /** `plugin available` — plugins bundled with this Total Recall package. */
37
+ export async function listAvailable(args = []) {
38
+ const { isJson, useCase, query } = parseArgs(args);
39
+ const results = listAvailableBundled(process.cwd()).filter((p) => matches(p, query, useCase));
17
40
 
18
41
  if (isJson) {
19
42
  console.log(JSON.stringify(results, null, 2));
20
43
  return;
21
44
  }
22
45
 
23
- console.log("\n🔌 Total Recall — Plugin Catalog & Discovery\n");
46
+ console.log('\n📦 Total Recall — Bundled Plugins\n');
24
47
  if (results.length === 0) {
25
- console.log("No plugins found matching \"" + query + "\".\n");
48
+ console.log(`No bundled plugins match${query ? ` "${query}"` : ''}${useCase ? ` for use case "${useCase}"` : ''}.\n`);
49
+ return;
50
+ }
51
+ for (const p of results) printPlugin(p, p.id);
52
+ }
53
+
54
+ /** `plugin peers` — plugins shared by mesh peers, asked live. */
55
+ export async function listPeers(args = []) {
56
+ const { isJson, useCase, query } = parseArgs(args);
57
+ const result = await listPeerPlugins();
58
+ for (const peer of result.peers) {
59
+ peer.plugins = peer.plugins.filter((p) => matches(p, query, useCase));
60
+ }
61
+
62
+ if (isJson) {
63
+ console.log(JSON.stringify(result, null, 2));
64
+ return;
65
+ }
66
+
67
+ console.log('\n🛰️ Total Recall — Plugins Shared on the Mesh\n');
68
+ if (!result.mesh.available) {
69
+ console.log(' This node is not connected to a mesh. Set one up with: npx total-recall mesh enroll (see: npx total-recall mesh --help)\n');
70
+ return;
71
+ }
72
+ if (result.peers.length === 0) {
73
+ console.log(' No other nodes on the mesh.\n');
26
74
  return;
27
75
  }
76
+ for (const peer of result.peers) {
77
+ const label = peer.status === 'ok'
78
+ ? `\x1b[32m${peer.plugins.length} shared\x1b[0m`
79
+ : `\x1b[33m${peer.status}\x1b[0m${peer.error ? ` — ${peer.error}` : ''}`;
80
+ console.log(`\x1b[1m${peer.hostname}\x1b[0m (${peer.ip}) ${label}`);
81
+ for (const p of peer.plugins) {
82
+ console.log(` ● ${p.name} (id: \x1b[33m${p.id}\x1b[0m, v${p.version})`);
83
+ console.log(` ${p.description}`);
84
+ if (p.use_cases.length) console.log(` Use cases: ${p.use_cases.join(', ')}`);
85
+ console.log(` sha256 ${p.sha256.slice(0, 16)}… · ${p.file_count} files · ${p.size_bytes} bytes`);
86
+ console.log(` Install: \x1b[32mnpx total-recall plugin install peer:${peer.hostname}/${p.id}\x1b[0m`);
87
+ }
88
+ console.log();
89
+ }
90
+ }
91
+
92
+ /** `plugin search <query>` — bundled plugins plus whatever mesh peers share. */
93
+ export async function searchPlugins(args = []) {
94
+ const { isJson, useCase, query } = parseArgs(args);
95
+ const bundled = listAvailableBundled(process.cwd()).filter((p) => matches(p, query, useCase));
96
+ const peers = await listPeerPlugins();
97
+ const shared = [];
98
+ for (const peer of peers.peers) {
99
+ for (const p of peer.plugins) {
100
+ if (matches(p, query, useCase)) shared.push({ ...p, peer: peer.hostname, source: `peer:${peer.hostname}/${p.id}` });
101
+ }
102
+ }
28
103
 
29
- for (const p of results) {
30
- const verifiedBadge = p.verified ? " [Verified]" : "";
31
- console.log("\x1b[1;36m● " + p.name + "\x1b[0m (id: \x1b[33m" + p.id + "\x1b[0m, v" + p.version + ")" + verifiedBadge);
32
- console.log(" Rating: ★ " + p.rating + " (" + p.reviewCount + " reviews) | Installs: " + p.installCount);
33
- console.log(" Description: " + p.description);
34
- console.log(" Tags: " + p.tags.join(", "));
35
- console.log(" Install: \x1b[32mnpx total-recall plugin install " + p.sourceUrl + "\x1b[0m\n");
104
+ if (isJson) {
105
+ console.log(JSON.stringify({ bundled, peers: shared }, null, 2));
106
+ return;
107
+ }
108
+
109
+ console.log(`\n🔎 Plugins matching "${query || '*'}"${useCase ? ` (use case: ${useCase})` : ''}\n`);
110
+ if (bundled.length === 0 && shared.length === 0) {
111
+ console.log('No plugins found.\n');
112
+ return;
113
+ }
114
+ if (bundled.length) {
115
+ console.log('Bundled:\n');
116
+ for (const p of bundled) printPlugin(p, p.id);
117
+ }
118
+ if (shared.length) {
119
+ console.log('Shared on the mesh:\n');
120
+ for (const p of shared) printPlugin({ ...p, name: `${p.name} — from ${p.peer}` }, p.source);
36
121
  }
37
122
  }
@@ -4,6 +4,7 @@ import { compileSurface } from '../core/surface.mjs';
4
4
  import { detectIndexDrift } from '../core/drift-detector.mjs';
5
5
  import { resolveAgentDir, resolveBrainDir, parseLayerFlag } from './agent-dir.mjs';
6
6
  import { logger } from '../core/logger.mjs';
7
+ import { listLocalBrains } from '../core/brain-registry.mjs';
7
8
 
8
9
  /**
9
10
  * SSSS Projection Rebuild Command
@@ -107,12 +108,47 @@ export async function runRebuild(options = {}) {
107
108
  }
108
109
  }
109
110
 
111
+ /**
112
+ * Recompile every registered project brain's surfaces. Each project's rules
113
+ * are the global rules plus its own (see mergeGlobalRuleNodes), so a global
114
+ * change is not done until every project has been rebuilt. Incremental: a
115
+ * project whose vault and the global rules are unchanged is skipped.
116
+ */
117
+ export async function compileRegisteredProjects({ brains = listLocalBrains(), compile = compileSurface } = {}) {
118
+ let failed = 0;
119
+ for (const brain of brains) {
120
+ if (brain.kind !== 'project' || !brain.exists) continue;
121
+ const agentDir = path.dirname(path.dirname(brain.brainDir));
122
+ try {
123
+ const stats = await compile({
124
+ vaultDir: path.join(brain.brainDir, 'memory-vault'),
125
+ skillsDir: path.join(agentDir, 'skills'),
126
+ derivedDir: path.join(brain.brainDir, 'memory-derived'),
127
+ instructionsFile: path.join(agentDir, 'INSTRUCTIONS.md'),
128
+ });
129
+ console.log(`✅ ${brain.name}: ${stats.skipped ? 'unchanged' : `rules recompiled (${stats.skillsInjected} surfaces)`}`);
130
+ } catch (err) {
131
+ failed += 1;
132
+ console.error(`❌ ${brain.name}: ${err.message}`);
133
+ logger.error('rebuild', `Project compile failed for ${brain.brainDir}: ${err.message}`);
134
+ }
135
+ }
136
+ return failed;
137
+ }
138
+
110
139
  export default async function cli(args) {
111
140
  const { layer, remainingArgs } = parseLayerFlag(args);
112
141
  const options = {
113
142
  check: remainingArgs.includes('--check'),
114
143
  layer,
115
144
  };
116
- const code = await runRebuild(options);
145
+ let code = await runRebuild(options);
146
+ // Global rules land in every project's surfaces, so a global compile (what
147
+ // `remember --global` runs) or an explicit --all rebuilds each project too.
148
+ if (code === 0 && !options.check && (layer === 'global' || remainingArgs.includes('--all'))) {
149
+ console.log('--------------------------');
150
+ console.log('🔁 Recompiling registered project brains...');
151
+ if (await compileRegisteredProjects()) code = 1;
152
+ }
117
153
  process.exit(code);
118
154
  }
@@ -106,7 +106,7 @@ export default async function recall(args) {
106
106
 
107
107
  // Determine which brains to search
108
108
  const brains = getBothBrains();
109
- const searchTargets = [];
109
+ let searchTargets = [];
110
110
 
111
111
  if (layer === 'global' || layer === 'auto') {
112
112
  if (brains.global) {
@@ -119,8 +119,23 @@ export default async function recall(args) {
119
119
  }
120
120
  }
121
121
 
122
+ // Brains switched off with `brain off` are not read. Filtered here, after
123
+ // layer selection, so an explicit --layer on an off brain says why it came
124
+ // back empty instead of silently searching nothing.
125
+ const { isBrainEnabled, readBrainToggles } = await import('../core/brain-registry.mjs');
126
+ const toggles = readBrainToggles();
127
+ const skipped = searchTargets.filter((t) => !isBrainEnabled(t.brainDir, { toggles }));
128
+ if (skipped.length) {
129
+ searchTargets = searchTargets.filter((t) => !skipped.includes(t));
130
+ for (const t of skipped) {
131
+ console.error(` ⏸ ${t.label} brain is switched off (npx total-recall brain on ${t.label === 'global' ? 'global' : t.brainDir})`);
132
+ }
133
+ }
134
+
122
135
  if (searchTargets.length === 0) {
123
- console.error('No brain found. Run `npx total-recall init` to create one.');
136
+ console.error(skipped.length
137
+ ? 'Every brain in scope is switched off. Run `npx total-recall brain list` to see them.'
138
+ : 'No brain found. Run `npx total-recall init` to create one.');
124
139
  process.exit(1);
125
140
  }
126
141
 
@@ -51,11 +51,9 @@ function getStatusBadge(status) {
51
51
 
52
52
  function getPhaseLabel(phase) {
53
53
  switch (phase) {
54
- case 'acquisition': return 'Acquisition (1/5)';
55
- case 'deliberation': return 'Deliberation (2/5)';
56
- case 'improvement': return 'Refinement & Clarity (3/5)';
57
- case 'monitoring': return 'Sources & Feeds (4/5)';
58
- case 'expansion': return 'Tangent Expansion (5/5)';
54
+ case 'acquisition': return 'Acquisition (1/3)';
55
+ case 'deliberation': return 'Deliberation (2/3)';
56
+ case 'improvement': return 'Refinement & Clarity (3/3)';
59
57
  default: return phase || 'unknown';
60
58
  }
61
59
  }
@@ -174,7 +172,7 @@ function handleAdd(args, resolvedBrainDir) {
174
172
  }
175
173
 
176
174
  try {
177
- const item = addToQueue({ topic, priority, notes, brainDir: resolvedBrainDir });
175
+ const item = addToQueue({ topic, priority, notes, brainDir: resolvedBrainDir, origin: 'user', requested_via: 'cli' });
178
176
  console.log();
179
177
  console.log(` ${green('✅ Successfully enqueued research project:')}`);
180
178
  console.log(` ${bold('Topic:')} "${item.topic}"`);
package/src/cli/share.mjs CHANGED
@@ -112,6 +112,8 @@ export default async function share(args) {
112
112
  topic,
113
113
  priority: 'medium',
114
114
  notes: text || '',
115
+ origin: 'user',
116
+ requested_via: 'cli',
115
117
  });
116
118
  console.log(` ✅ Queued research: "${topic}" (id: ${item.id})`);
117
119
  return;