total-recall-brain 3.26.0 → 3.28.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 (65) hide show
  1. package/bin/total-recall.mjs +31 -1
  2. package/docs/ARCHITECTURE.md +76 -9
  3. package/frontend/dist/assets/index-U7o-qU99.js +5742 -0
  4. package/frontend/dist/index.html +1 -1
  5. package/frontend/src/App.tsx +39 -1
  6. package/frontend/src/api/index.ts +1 -0
  7. package/frontend/src/api/plugins.ts +167 -0
  8. package/frontend/src/pages/ChatPage.tsx +44 -17
  9. package/frontend/src/pages/IntegrationsPage.tsx +7 -0
  10. package/frontend/src/pages/PluginsPage.spec.tsx +17 -0
  11. package/frontend/src/pages/PluginsPage.tsx +1331 -0
  12. package/package.json +1 -1
  13. package/scaffold/.agent/plugins/.gitkeep +0 -0
  14. package/scaffold/.agent/plugins/code-quality/README.md +16 -0
  15. package/scaffold/.agent/plugins/code-quality/cli.mjs +15 -0
  16. package/scaffold/.agent/plugins/code-quality/plugin.json +34 -0
  17. package/scaffold/.agent/plugins/git-sentinel/README.md +16 -0
  18. package/scaffold/.agent/plugins/git-sentinel/cli.mjs +86 -0
  19. package/scaffold/.agent/plugins/git-sentinel/plugin.json +34 -0
  20. package/scaffold/.agent/plugins/meta-harness/README.md +16 -0
  21. package/scaffold/.agent/plugins/meta-harness/cli.mjs +34 -0
  22. package/scaffold/.agent/plugins/meta-harness/plugin.json +34 -0
  23. package/scaffold/.agent/plugins/system-monitor/README.md +16 -0
  24. package/scaffold/.agent/plugins/system-monitor/cli.mjs +79 -0
  25. package/scaffold/.agent/plugins/system-monitor/generator.mjs +20 -0
  26. package/scaffold/.agent/plugins/system-monitor/plugin.json +38 -0
  27. package/scaffold/.agent/skills/meta-harness/SKILL.md +113 -0
  28. package/scaffold/.agent/skills/total-recall/references/cli-reference.md +23 -0
  29. package/src/cli/agent.mjs +173 -0
  30. package/src/cli/connect.mjs +89 -4
  31. package/src/cli/deploy-ui.mjs +6 -0
  32. package/src/cli/harness.mjs +135 -0
  33. package/src/cli/ingest-openwiki.mjs +71 -2
  34. package/src/cli/init.mjs +1 -0
  35. package/src/cli/lint.mjs +47 -3
  36. package/src/cli/mesh.mjs +286 -0
  37. package/src/cli/plugin/create.mjs +186 -0
  38. package/src/cli/plugin/index.mjs +139 -0
  39. package/src/cli/plugin/install.mjs +141 -0
  40. package/src/cli/plugin/list.mjs +83 -0
  41. package/src/cli/plugin/remove.mjs +51 -0
  42. package/src/cli/plugin/search.mjs +37 -0
  43. package/src/cli/skill-projection.mjs +14 -0
  44. package/src/cli/wizard.html +9 -0
  45. package/src/core/agent-manager.mjs +290 -0
  46. package/src/core/cache-prune.mjs +341 -0
  47. package/src/core/context-cache.mjs +130 -0
  48. package/src/core/daemon-loop.mjs +47 -1
  49. package/src/core/evolving-context.mjs +171 -0
  50. package/src/core/mesh-activity.mjs +140 -0
  51. package/src/core/mesh.mjs +174 -1
  52. package/src/core/meta-harness.mjs +251 -0
  53. package/src/core/plugin-context.mjs +206 -0
  54. package/src/core/plugin-loader.mjs +210 -0
  55. package/src/core/runtime.mjs +65 -13
  56. package/src/core/surface.mjs +32 -10
  57. package/src/server/api.mjs +18 -2
  58. package/src/server/index.mjs +8 -2
  59. package/src/server/rest.mjs +4 -0
  60. package/src/server/routes/integrations.mjs +5 -1
  61. package/src/server/routes/mesh-presence.mjs +143 -0
  62. package/src/server/routes/models.mjs +42 -30
  63. package/src/server/routes/plugins.mjs +517 -0
  64. package/src/server/tools.mjs +266 -0
  65. package/frontend/dist/assets/index-Bl3I11B0.js +0 -5742
@@ -0,0 +1,173 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { listAgents, spawnAgent, killAgent, getAgentLogs } from '../core/agent-manager.mjs';
4
+ import { detectHarnesses } from '../core/meta-harness.mjs';
5
+
6
+ function printHelp() {
7
+ console.log(`
8
+ 🤖 Total Recall — CLI Agent Management & Process Controller
9
+
10
+ Usage:
11
+ npx total-recall agent <command> [options]
12
+
13
+ Commands:
14
+ list List all running and recent agent processes
15
+ spawn <harness> "<task>" Spawn a background or foreground agent task
16
+ status [id] Show detailed status of an agent
17
+ logs <id> [--tail 50] Inspect streaming logs of a spawned agent
18
+ kill <id|pid> Terminate an active agent process
19
+
20
+ Options:
21
+ --node <name> Target a remote mesh node (macmini, cloud, etc.)
22
+ --json Output machine-readable JSON for Unix pipelines
23
+ --detach Spawn process detached in background (default: true)
24
+ --name <label> Assign a human-readable name to the agent session
25
+ --tail <lines> Number of log lines to inspect (default: 50)
26
+
27
+ Available Harnesses:
28
+ agy, claude, codex, gemini
29
+
30
+ Examples:
31
+ npx total-recall agent list
32
+ npx total-recall agent spawn claude "Refactor src/cli/harness.mjs error handling"
33
+ npx total-recall agent spawn agy "Crawl recent preprints on quantum shuttling" --name "Quantum Scout"
34
+ npx total-recall agent logs agent-claude-xyz --tail 100
35
+ npx total-recall agent kill agent-claude-xyz
36
+ `);
37
+ }
38
+
39
+ export async function run(argv) {
40
+ let args = argv;
41
+ if (Array.isArray(args) && args[0]?.endsWith('node')) {
42
+ args = args.slice(2);
43
+ }
44
+ if (args[0] === 'agent' || args[0] === 'agents') {
45
+ args = args.slice(1);
46
+ }
47
+
48
+ const command = args[0];
49
+ const rest = args.slice(1);
50
+
51
+ if (!command || command === 'list') {
52
+ const isJson = rest.includes('--json');
53
+ const agents = listAgents();
54
+
55
+ if (isJson) {
56
+ console.log(JSON.stringify(agents, null, 2));
57
+ return;
58
+ }
59
+
60
+ console.log(`\n🤖 Total Recall — Active Agent Process Registry\n`);
61
+ if (agents.length === 0) {
62
+ console.log('No agents currently active or tracked.');
63
+ console.log('Spawn an agent with: npx total-recall agent spawn <harness> "<task>"\n');
64
+ return;
65
+ }
66
+
67
+ const pad = (s, l) => String(s || '').padEnd(l).slice(0, l);
68
+ console.log('┌────────────────────────┬──────────────┬────────┬────────────┬────────────────────────────┬─────────────────────────────┐');
69
+ console.log(`│ ${pad('Agent ID', 22)} │ ${pad('Harness', 12)} │ ${pad('PID', 6)} │ ${pad('Status', 10)} │ ${pad('Started At', 26)} │ ${pad('Task Intent', 27)} │`);
70
+ console.log('├────────────────────────┼──────────────┼────────┼────────────┼────────────────────────────┼─────────────────────────────┤');
71
+
72
+ for (const a of agents) {
73
+ const statusBadge = a.status === 'running' ? '\x1b[32mrunning\x1b[0m ' : (a.status === 'completed' ? '\x1b[36mcompleted\x1b[0m ' : '\x1b[90mstopped\x1b[0m ');
74
+ const time = (a.startedAt || '').replace('T', ' ').slice(0, 19);
75
+ console.log(`│ \x1b[1m${pad(a.id, 22)}\x1b[0m │ ${pad(a.harness, 12)} │ ${pad(a.pid, 6)} │ ${statusBadge} │ ${pad(time, 26)} │ ${pad(a.task, 27)} │`);
76
+ }
77
+ console.log('└────────────────────────┴──────────────┴────────┴────────────┴────────────────────────────┴─────────────────────────────┘\n');
78
+ return;
79
+ }
80
+
81
+ if (command === 'spawn') {
82
+ let targetNode = null;
83
+ const filteredRest = [];
84
+ for (let i = 0; i < rest.length; i++) {
85
+ if (rest[i] === '--node' || rest[i] === '-n') {
86
+ targetNode = rest[i + 1];
87
+ i++;
88
+ } else {
89
+ filteredRest.push(rest[i]);
90
+ }
91
+ }
92
+
93
+ const harness = filteredRest[0];
94
+ const task = filteredRest.filter(r => !r.startsWith('--')).slice(1).join(' ').trim();
95
+ const detach = !filteredRest.includes('--no-detach');
96
+ const isJson = filteredRest.includes('--json');
97
+ const nameIdx = filteredRest.indexOf('--name');
98
+ const name = nameIdx !== -1 ? filteredRest[nameIdx + 1] : null;
99
+
100
+ if (!harness || !task) {
101
+ console.error('Error: Please specify harness ID and task prompt.');
102
+ console.error('Usage: npx total-recall agent spawn <harness> "<task>" [--name "..."] [--node <node>]');
103
+ process.exit(1);
104
+ }
105
+
106
+ const nodeStr = targetNode ? ` on mesh node \x1b[1;33m${targetNode}\x1b[0m` : '';
107
+ if (!isJson) console.log(`\n🚀 Spawning agent on harness: \x1b[1;36m${harness}\x1b[0m${nodeStr}...`);
108
+ try {
109
+ const record = await spawnAgent(harness, task, { detach, name, node: targetNode });
110
+ if (isJson) {
111
+ console.log(JSON.stringify(record, null, 2));
112
+ return;
113
+ }
114
+ console.log(`✅ Agent session spawned successfully!`);
115
+ console.log(` Session ID: \x1b[1m${record.id}\x1b[0m (PID: ${record.pid || 'remote'})`);
116
+ console.log(` Status: \x1b[32m${record.status}\x1b[0m`);
117
+ if (record.node) console.log(` Mesh Node: ${record.node}`);
118
+ if (record.logFile) console.log(` Log File: ${record.logFile}`);
119
+ console.log(`\nMonitor logs with: \x1b[36mnpx total-recall agent logs ${record.id}\x1b[0m\n`);
120
+ } catch (err) {
121
+ console.error(`\n❌ Failed to spawn agent: ${err.message}\n`);
122
+ process.exit(1);
123
+ }
124
+ return;
125
+ }
126
+
127
+ if (command === 'logs') {
128
+ const id = rest.find(r => !r.startsWith('--'));
129
+ if (!id) {
130
+ console.error('Error: Please specify agent ID or PID.');
131
+ console.error('Usage: npx total-recall agent logs <id> [--tail 50]');
132
+ process.exit(1);
133
+ }
134
+
135
+ const tailIdx = rest.indexOf('--tail');
136
+ const tailLines = tailIdx !== -1 ? parseInt(rest[tailIdx + 1], 10) : 50;
137
+
138
+ try {
139
+ const logs = await getAgentLogs(id, tailLines);
140
+ console.log(`\n📜 Logs for Agent [${id}]:\n`);
141
+ console.log(logs || '(Log is empty)');
142
+ console.log('');
143
+ } catch (err) {
144
+ console.error(`\n❌ ${err.message}\n`);
145
+ process.exit(1);
146
+ }
147
+ return;
148
+ }
149
+
150
+ if (command === 'kill') {
151
+ const id = rest.find(r => !r.startsWith('--'));
152
+ if (!id) {
153
+ console.error('Error: Please specify agent ID or PID to terminate.');
154
+ console.error('Usage: npx total-recall agent kill <id|pid>');
155
+ process.exit(1);
156
+ }
157
+
158
+ try {
159
+ const res = await killAgent(id);
160
+ console.log(`\n✅ ${res.message}\n`);
161
+ } catch (err) {
162
+ console.error(`\n❌ ${err.message}\n`);
163
+ process.exit(1);
164
+ }
165
+ return;
166
+ }
167
+
168
+ printHelp();
169
+ }
170
+
171
+ export default async function (args) {
172
+ await run(args);
173
+ }
@@ -28,6 +28,30 @@ const CLIENTS = {
28
28
  mode: 'hermes',
29
29
  memoryPath: path.join(os.homedir(), '.hermes', 'memories', 'MEMORY.md'),
30
30
  userPath: path.join(os.homedir(), '.hermes', 'memories', 'USER.md'),
31
+ skillsProjection: { scope: 'project', dir: path.join('.hermes', 'skills') }
32
+ },
33
+ 'hermes-agent': {
34
+ label: 'Hermes Agent',
35
+ mode: 'hermes',
36
+ memoryPath: path.join(os.homedir(), '.hermes', 'memories', 'MEMORY.md'),
37
+ userPath: path.join(os.homedir(), '.hermes', 'memories', 'USER.md'),
38
+ skillsProjection: { scope: 'project', dir: path.join('.hermes', 'skills') }
39
+ },
40
+ dsh: {
41
+ label: 'DeepSeek Harness (dsh)',
42
+ mode: 'dsh',
43
+ globalMemoryPath: path.join(os.homedir(), '.dsh', 'memory', 'MEMORY.md'),
44
+ globalUserPath: path.join(os.homedir(), '.dsh', 'memory', 'user.md'),
45
+ localTarget: 'AGENTS.md',
46
+ skillsProjection: { scope: 'project', dir: path.join('.agents', 'skills') }
47
+ },
48
+ 'deepseek-harness': {
49
+ label: 'DeepSeek Harness',
50
+ mode: 'dsh',
51
+ globalMemoryPath: path.join(os.homedir(), '.dsh', 'memory', 'MEMORY.md'),
52
+ globalUserPath: path.join(os.homedir(), '.dsh', 'memory', 'user.md'),
53
+ localTarget: 'AGENTS.md',
54
+ skillsProjection: { scope: 'project', dir: path.join('.agents', 'skills') }
31
55
  },
32
56
  openclaw: {
33
57
  label: 'OpenClaw',
@@ -163,6 +187,10 @@ function printHelp() {
163
187
  antigravity
164
188
  gemini
165
189
  aider
190
+ pi
191
+ hermes
192
+ dsh
193
+ openclaw
166
194
  http-api
167
195
  obsidian
168
196
  generic
@@ -797,14 +825,71 @@ export default async function connect(args) {
797
825
  result.notes.push(` ~/.pi/agent/AGENTS.md → ${instructionsPath}`);
798
826
  } else if (preset.mode === 'hermes') {
799
827
  // Hermes: write compiled MEMORY.md to ~/.hermes/memories/MEMORY.md (max 2200 chars)
828
+ // and project-local .hermes/memories/MEMORY.md if inside a project workspace
800
829
  const instructions = readInstructions(cwd, agentDir);
801
830
  if (!instructions) throw new Error('No INSTRUCTIONS.md found. Run npx total-recall init or compile first.');
802
- fs.mkdirSync(path.dirname(preset.memoryPath), { recursive: true });
831
+ const homeDir = process.env.HOME || os.homedir();
832
+ const globalMemPath = path.join(homeDir, '.hermes', 'memories', 'MEMORY.md');
803
833
  const capped = instructions.slice(0, 2200);
804
- fs.writeFileSync(preset.memoryPath, capped, 'utf8');
805
- result.projection = { targetPath: preset.memoryPath, action: 'written' };
806
- result.notes.push(` Hermes MEMORY.md (${capped.length} chars) → ${preset.memoryPath}`);
834
+
835
+ let wroteGlobal = false;
836
+ try {
837
+ fs.mkdirSync(path.dirname(globalMemPath), { recursive: true });
838
+ fs.writeFileSync(globalMemPath, capped, 'utf8');
839
+ wroteGlobal = true;
840
+ } catch {
841
+ // Gracefully continue if global directory is unwritable in sandbox
842
+ }
843
+
844
+ let localMemPath = null;
845
+ if (!isHome) {
846
+ localMemPath = path.join(cwd, '.hermes', 'memories', 'MEMORY.md');
847
+ fs.mkdirSync(path.dirname(localMemPath), { recursive: true });
848
+ fs.writeFileSync(localMemPath, capped, 'utf8');
849
+ }
850
+
851
+ result.projection = { targetPath: localMemPath || globalMemPath, action: 'written' };
852
+ if (wroteGlobal) {
853
+ result.notes.push(` Hermes MEMORY.md (${capped.length} chars) → ${globalMemPath}`);
854
+ }
855
+ if (localMemPath) {
856
+ result.notes.push(` Hermes local MEMORY.md → ${localMemPath}`);
857
+ }
807
858
  result.notes.push(' Note: Hermes has a 2,200-char limit on MEMORY.md. Total Recall auto-caps this.');
859
+ } else if (preset.mode === 'dsh') {
860
+ // DeepSeek Harness: write global ~/.dsh/memory/MEMORY.md and symlink project AGENTS.md
861
+ const instructions = readInstructions(cwd, agentDir);
862
+ if (!instructions) throw new Error('No INSTRUCTIONS.md found. Run npx total-recall init or compile first.');
863
+ const homeDir = process.env.HOME || os.homedir();
864
+ const globalMemPath = path.join(homeDir, '.dsh', 'memory', 'MEMORY.md');
865
+
866
+ let wroteGlobal = false;
867
+ try {
868
+ fs.mkdirSync(path.dirname(globalMemPath), { recursive: true });
869
+ fs.writeFileSync(globalMemPath, instructions, 'utf8');
870
+ wroteGlobal = true;
871
+ } catch {
872
+ // Gracefully continue if global directory is unwritable in sandbox
873
+ }
874
+
875
+ const agentsPath = path.join(cwd, preset.localTarget);
876
+ if (pathExists(agentsPath)) {
877
+ if (opts.force) {
878
+ try {
879
+ fs.unlinkSync(agentsPath);
880
+ } catch {
881
+ fs.rmSync(agentsPath, { force: true, recursive: true });
882
+ }
883
+ fs.symlinkSync('INSTRUCTIONS.md', agentsPath);
884
+ }
885
+ } else {
886
+ fs.symlinkSync('INSTRUCTIONS.md', agentsPath);
887
+ }
888
+ result.projection = { targetPath: agentsPath, action: 'symlinked' };
889
+ if (wroteGlobal) {
890
+ result.notes.push(` DeepSeek Harness MEMORY.md → ${globalMemPath}`);
891
+ }
892
+ result.notes.push(` DeepSeek Harness AGENTS.md → INSTRUCTIONS.md`);
808
893
  } else if (preset.mode === 'openclaw') {
809
894
  // OpenClaw: write MEMORY.md + symlink AGENTS.md in cwd
810
895
  const instructions = readInstructions(cwd, agentDir);
@@ -692,6 +692,9 @@ export function startDeployUI(port = 3001) {
692
692
  'gemini': [HOME + '/.gemini'],
693
693
  'pi': [HOME + '/.pi/agent'],
694
694
  'hermes': [HOME + '/.hermes'],
695
+ 'hermes-agent':[HOME + '/.hermes'],
696
+ 'dsh': [HOME + '/.dsh'],
697
+ 'deepseek-harness': [HOME + '/.dsh'],
695
698
  'openclaw': [HOME + '/.openclaw'],
696
699
  'obsidian': [HOME + '/Library/Application Support/obsidian', HOME + '/.config/obsidian'],
697
700
  };
@@ -725,6 +728,9 @@ export function startDeployUI(port = 3001) {
725
728
  'gemini': { client: 'gemini', label: 'Gemini CLI' },
726
729
  'pi': { client: 'pi', label: 'Pi Coding Agent' },
727
730
  'hermes': { client: 'hermes', label: 'Hermes Agent' },
731
+ 'hermes-agent':{ client: 'hermes-agent', label: 'Hermes Agent' },
732
+ 'dsh': { client: 'dsh', label: 'DeepSeek Harness (dsh)' },
733
+ 'deepseek-harness': { client: 'deepseek-harness', label: 'DeepSeek Harness' },
728
734
  'openclaw': { client: 'openclaw', label: 'OpenClaw' },
729
735
  };
730
736
 
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { detectHarnesses, dispatchTask, runCouncil, HARNESS_SPECS } from '../core/meta-harness.mjs';
4
+
5
+ function printHelp() {
6
+ console.log(`
7
+ 🎮 Total Recall — Meta Harness & Multi-Agent Management Layer
8
+
9
+ Usage:
10
+ npx total-recall harness <command> [options]
11
+
12
+ Commands:
13
+ list Inspect all external IDE and CLI developer harnesses
14
+ dispatch <id> "<task>" Headlessly invoke a specific agent harness
15
+ Options: --node <name> to execute remotely on a mesh node
16
+ council "<task>" Run concurrent multi-harness consensus deliberation
17
+
18
+ Available Harness IDs:
19
+ agy Google Antigravity CLI (Frontier reasoning, AI Ultra)
20
+ claude Claude Code CLI (Deep codebase refactoring & Unix execution)
21
+ codex OpenAI Codex CLI (Program synthesis & sandbox execution)
22
+ gemini Google Gemini CLI (Fast utilities & completion chaining)
23
+ ollama Ollama Local LLM (Local neural reasoning, zero API cost)
24
+
25
+ Examples:
26
+ npx total-recall harness list
27
+ npx total-recall harness dispatch claude "Review and run code quality checks on src/core/"
28
+ npx total-recall harness dispatch ollama --node macmini "What is test-time compute scaling?"
29
+ npx total-recall harness council "Propose architecture for decentralized research mesh"
30
+ `);
31
+ }
32
+
33
+ export async function run(argv) {
34
+ let args = argv;
35
+ if (Array.isArray(args) && args[0]?.endsWith('node')) {
36
+ args = args.slice(2);
37
+ }
38
+ if (args[0] === 'harness') {
39
+ args = args.slice(1);
40
+ }
41
+
42
+ const command = args[0];
43
+ const rest = args.slice(1);
44
+
45
+ if (!command || command === 'list' || command === 'status') {
46
+ const harnesses = detectHarnesses();
47
+ console.log(`\n🎮 Total Recall — Connected Agent Harnesses & Runtimes\n`);
48
+
49
+ const pad = (s, l) => String(s || '').padEnd(l).slice(0, l);
50
+ console.log('┌──────────────┬──────────────────────────────┬──────────────┬────────────────────────────────────────────────────────┐');
51
+ console.log(`│ ${pad('Harness ID', 12)} │ ${pad('Name', 28)} │ ${pad('Status', 12)} │ ${pad('Binary Path', 54)} │`);
52
+ console.log('├──────────────┼──────────────────────────────┼──────────────┼────────────────────────────────────────────────────────┤');
53
+
54
+ for (const h of harnesses) {
55
+ const status = h.available ? '\x1b[32mActive ✅\x1b[0m ' : '\x1b[31mNot Found ❌\x1b[0m';
56
+ const p = h.binaryPath || '—';
57
+ console.log(`│ \x1b[1m${pad(h.id, 12)}\x1b[0m │ ${pad(h.name, 28)} │ ${status} │ ${pad(p, 54)} │`);
58
+ }
59
+ console.log('└──────────────┴──────────────────────────────┴──────────────┴────────────────────────────────────────────────────────┘\n');
60
+ return;
61
+ }
62
+
63
+ if (command === 'dispatch') {
64
+ let targetNode = null;
65
+ const filteredRest = [];
66
+ for (let i = 0; i < rest.length; i++) {
67
+ if (rest[i] === '--node' || rest[i] === '-n') {
68
+ targetNode = rest[i + 1];
69
+ i++;
70
+ } else {
71
+ filteredRest.push(rest[i]);
72
+ }
73
+ }
74
+
75
+ const harnessId = filteredRest[0];
76
+ const task = filteredRest.slice(1).join(' ').trim();
77
+ if (!harnessId || !task) {
78
+ console.error('Error: Please specify harness ID and task prompt.');
79
+ console.error('Usage: npx total-recall harness dispatch <id> [--node <node>] "<task>"');
80
+ process.exit(1);
81
+ }
82
+
83
+ const nodeStr = targetNode ? ` on mesh node \x1b[1;33m${targetNode}\x1b[0m` : '';
84
+ console.log(`\n🚀 Dispatching task to \x1b[1;36m${harnessId}\x1b[0m${nodeStr}...`);
85
+ try {
86
+ const res = await dispatchTask(harnessId, task, { node: targetNode });
87
+ console.log(`\n--- [Response from ${res.harnessName}${targetNode ? ' (' + targetNode + ')' : ''}] ---\n`);
88
+ console.log(res.response);
89
+ console.log(`\n✅ Exit code: ${res.exitCode}`);
90
+ } catch (err) {
91
+ console.error(`\n❌ Dispatch failed: ${err.message}`);
92
+ process.exit(1);
93
+ }
94
+ return;
95
+ }
96
+
97
+ if (command === 'council') {
98
+ const task = rest.join(' ').trim();
99
+ if (!task) {
100
+ console.error('Error: Please specify task prompt for council deliberation.');
101
+ console.error('Usage: npx total-recall harness council "<task>"');
102
+ process.exit(1);
103
+ }
104
+
105
+ console.log(`\n🏛️ Convening Multi-Agent Council across all available harnesses...`);
106
+ console.log(` Task: "${task}"\n`);
107
+
108
+ try {
109
+ const councilRes = await runCouncil(task);
110
+ console.log(`Participating Harnesses: ${councilRes.participants.join(', ')}\n`);
111
+
112
+ for (const r of councilRes.results) {
113
+ console.log(`================================================================`);
114
+ console.log(`● Harness: \x1b[1;36m${r.harnessName}\x1b[0m (${r.harnessId})`);
115
+ console.log(`================================================================`);
116
+ if (r.success) {
117
+ console.log(r.response);
118
+ } else {
119
+ console.log(`\x1b[31mError: ${r.error || r.stderr || 'Execution failed'}\x1b[0m`);
120
+ }
121
+ console.log('');
122
+ }
123
+ } catch (err) {
124
+ console.error(`\n❌ Council deliberation failed: ${err.message}`);
125
+ process.exit(1);
126
+ }
127
+ return;
128
+ }
129
+
130
+ printHelp();
131
+ }
132
+
133
+ export default async function (args) {
134
+ await run(args);
135
+ }
@@ -11,6 +11,7 @@ function printHelp() {
11
11
  --dry-run Preview import without writing
12
12
  --global Target the global brain
13
13
  --project Target the project brain (default)
14
+ --plugins Discover and ingest OpenWiki hubs from installed plugins
14
15
  --help, -h Show this help
15
16
  `);
16
17
  }
@@ -18,6 +19,7 @@ function printHelp() {
18
19
  export async function runOpenWikiIngest(args) {
19
20
  let openwikiPath = null;
20
21
  let dryRun = false;
22
+ let includePlugins = false;
21
23
  let layerFlag = '--project';
22
24
  let help = false;
23
25
 
@@ -27,6 +29,7 @@ export async function runOpenWikiIngest(args) {
27
29
  case '--dry-run': dryRun = true; break;
28
30
  case '--global': layerFlag = '--global'; break;
29
31
  case '--project': layerFlag = '--project'; break;
32
+ case '--plugins': includePlugins = true; break;
30
33
  case '--help': case '-h': help = true; break;
31
34
  default:
32
35
  if (!arg.startsWith('-') && !openwikiPath) {
@@ -35,12 +38,12 @@ export async function runOpenWikiIngest(args) {
35
38
  }
36
39
  }
37
40
 
38
- if (help || !openwikiPath) {
41
+ if (help || (!openwikiPath && !includePlugins)) {
39
42
  printHelp();
40
43
  return;
41
44
  }
42
45
 
43
- if (!fs.existsSync(openwikiPath)) {
46
+ if (openwikiPath && !fs.existsSync(openwikiPath)) {
44
47
  console.error(`❌ Error: OpenWiki directory not found at ${openwikiPath}`);
45
48
  process.exit(1);
46
49
  }
@@ -116,6 +119,20 @@ export async function runOpenWikiIngest(args) {
116
119
  }
117
120
  }
118
121
 
122
+ if (includePlugins) {
123
+ console.log(`\n 🔌 Ingesting OpenWiki hubs from installed plugins...`);
124
+ const pluginResults = await syncPluginOpenWikiHubs({ dryRun, layerFlag });
125
+ for (const r of pluginResults) {
126
+ if (r.status === 'imported' || r.status === 'dry_run') {
127
+ console.log(` 🟢 [Plugin Hub] ${r.slug} ("${r.title}")`);
128
+ imported++;
129
+ } else {
130
+ console.error(` 🔴 [Plugin Hub] Error importing ${r.slug}: ${r.error}`);
131
+ errors++;
132
+ }
133
+ }
134
+ }
135
+
119
136
  console.log(`\n Import Summary:`);
120
137
  console.log(` 🟢 Imported: ${imported} files`);
121
138
  console.log(` 🔴 Errors: ${errors} files`);
@@ -124,3 +141,55 @@ export async function runOpenWikiIngest(args) {
124
141
  console.log(`\n ✅ OpenWiki successfully integrated into Total Recall.`);
125
142
  }
126
143
  }
144
+
145
+ /**
146
+ * Discover and ingest OpenWiki hubs declared by installed plugins.
147
+ */
148
+ export async function syncPluginOpenWikiHubs(options = {}) {
149
+ const { projectRoot = process.cwd(), dryRun = false, layerFlag = '--project' } = options;
150
+ const { discoverPlugins } = await import('../core/plugin-loader.mjs');
151
+ const plugins = discoverPlugins(projectRoot);
152
+ const results = [];
153
+
154
+ for (const plugin of plugins) {
155
+ const hubs = plugin.manifest?.openwiki_hubs || [];
156
+ for (const hub of hubs) {
157
+ if (!hub.path) continue;
158
+ const hubFilePath = path.isAbsolute(hub.path) ? hub.path : path.join(plugin.dir, hub.path);
159
+ if (!fs.existsSync(hubFilePath)) continue;
160
+
161
+ try {
162
+ const content = fs.readFileSync(hubFilePath, 'utf8');
163
+ const title = hub.title || path.basename(hub.path, '.md').replace(/[-_]/g, ' ');
164
+ const slug = `openwiki-plugin-${plugin.id}-${path.basename(hub.path, '.md')}`.toLowerCase();
165
+
166
+ if (dryRun) {
167
+ results.push({ slug, title, status: 'dry_run' });
168
+ continue;
169
+ }
170
+
171
+ const cliArgs = [
172
+ process.argv[1],
173
+ 'remember', 'fact',
174
+ content,
175
+ '--title', title,
176
+ '--slug', slug,
177
+ '--tags', `openwiki,plugin,plugin-${plugin.id}`,
178
+ layerFlag
179
+ ];
180
+
181
+ const res = spawnSync(process.argv[0], cliArgs, { encoding: 'utf8' });
182
+ results.push({
183
+ slug,
184
+ title,
185
+ status: res.status === 0 ? 'imported' : 'error',
186
+ error: res.status === 0 ? null : res.stderr
187
+ });
188
+ } catch (err) {
189
+ results.push({ slug: hub.path, title: hub.title, status: 'error', error: err.message });
190
+ }
191
+ }
192
+ }
193
+
194
+ return results;
195
+ }
package/src/cli/init.mjs CHANGED
@@ -199,6 +199,7 @@ export default async function init(args) {
199
199
 
200
200
  const dirs = [
201
201
  path.join(agentDir, 'skills'),
202
+ path.join(agentDir, 'plugins'),
202
203
  ];
203
204
 
204
205
  let created = 0;
package/src/cli/lint.mjs CHANGED
@@ -22,13 +22,14 @@ import { resolveBrainDir, parseLayerFlag } from './agent-dir.mjs';
22
22
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
23
23
 
24
24
  function parseArgs(args) {
25
- const opts = { vault: null, strict: false, json: false, help: false, okf: false };
25
+ const opts = { vault: null, strict: false, json: false, help: false, okf: false, plugins: false };
26
26
  for (let i = 0; i < args.length; i++) {
27
27
  switch (args[i]) {
28
28
  case '--vault': opts.vault = args[++i]; break;
29
29
  case '--strict': opts.strict = true; break;
30
30
  case '--json': opts.json = true; break;
31
31
  case '--okf': opts.okf = true; break;
32
+ case '--plugins': opts.plugins = true; break;
32
33
  case '--help': case '-h': opts.help = true; break;
33
34
  }
34
35
  }
@@ -37,10 +38,11 @@ function parseArgs(args) {
37
38
 
38
39
  function printHelp() {
39
40
  console.log(`
40
- total-recall lint — Validate vault nodes against SSSS schema v2 or OKF spec
41
+ total-recall lint — Validate vault nodes against SSSS schema v2, OKF spec, or plugins
41
42
 
42
43
  Scans all .md files in the memory vault and validates their YAML
43
- frontmatter against the Zod schemas or checks for OKF compliance.
44
+ frontmatter against the Zod schemas, checks OKF compliance, or validates
45
+ installed plugin.json manifests.
44
46
 
45
47
  Usage: total-recall lint [options]
46
48
 
@@ -50,6 +52,7 @@ function printHelp() {
50
52
  --project Lint the project vault for the current repo
51
53
  --strict Treat warnings as errors (exit 1 on any issue)
52
54
  --okf Verify OKF v0.1 draft metadata compliance
55
+ --plugins Validate all installed plugin manifests (plugin.json)
53
56
  --json Output results as JSONL
54
57
  --help, -h Show this help
55
58
 
@@ -102,6 +105,47 @@ export default async function lint(args) {
102
105
  return;
103
106
  }
104
107
 
108
+ if (opts.plugins) {
109
+ const { discoverPlugins } = await import('../core/plugin-loader.mjs');
110
+ const plugins = discoverPlugins(process.cwd());
111
+ let pluginErrors = 0;
112
+
113
+ if (!opts.json) {
114
+ console.error(`\n 🔌 Linting installed plugins:\n`);
115
+ }
116
+
117
+ for (const p of plugins) {
118
+ if (!p.valid) {
119
+ pluginErrors += p.errors.length;
120
+ if (opts.json) {
121
+ console.log(JSON.stringify({ plugin: p.id, manifestPath: p.manifestPath, level: 'error', errors: p.errors }));
122
+ } else {
123
+ console.error(` ❌ Plugin '${p.id}' (${p.manifestPath}):`);
124
+ for (const err of p.errors) {
125
+ console.error(` ${err}`);
126
+ }
127
+ }
128
+ } else {
129
+ if (opts.json) {
130
+ console.log(JSON.stringify({ plugin: p.id, manifestPath: p.manifestPath, level: 'info', valid: true }));
131
+ } else {
132
+ console.error(` ✅ Plugin '${p.id}' (v${p.manifest.version || '0.0.0'}): valid`);
133
+ }
134
+ }
135
+ }
136
+
137
+ if (!opts.json) {
138
+ console.error(`\n ── Plugin Results ──`);
139
+ console.error(` Total plugins scanned: ${plugins.length}`);
140
+ console.error(` Errors: ${pluginErrors}\n`);
141
+ }
142
+
143
+ if (pluginErrors > 0) {
144
+ process.exit(1);
145
+ }
146
+ return;
147
+ }
148
+
105
149
  if (!opts.json) {
106
150
  console.error(`\n 🔍 Linting vault: ${vaultDir}\n`);
107
151
  }