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
package/src/cli/mesh.mjs CHANGED
@@ -41,12 +41,20 @@ function printHelp() {
41
41
  Usage: total-recall mesh <command> [options]
42
42
 
43
43
  status Control-server reachability and credential state
44
+ doctor Probe reachability & compute capabilities across all nodes
44
45
  nodes List mesh nodes and how to reach each one
45
46
  ssh <node> [cmd…] Open a session using the node's recorded access
47
+ exec <node> <cmd…> Execute a command on a node non-interactively (--json)
46
48
  access <node> Show resolved access for a node
47
49
  access <node> --user <u> [--port <n>] [--host <h>] [--identity <path>]
48
50
  Record how to reach a node
49
51
  access import Propose access from ~/.ssh/config (--apply to save)
52
+ ping [node] Measure round-trip latency to a node (or all nodes) (--json)
53
+ leader Show cluster leader election status and current leader (--json)
54
+ enroll [options] Enroll this node on the mesh control server
55
+ --status show current enrollment state
56
+ --server control-server URL
57
+ --force re-enroll even if already connected
50
58
  preauthkey Mint an enrollment key for 'tailscale up --authkey'
51
59
  --reusable usable by more than one node
52
60
  --ephemeral node is removed when it goes offline
@@ -124,6 +132,91 @@ export default async function meshCli(argv = []) {
124
132
  return;
125
133
  }
126
134
 
135
+ if (command === 'doctor' || command === 'diagnostics') {
136
+ console.log('\n🩺 Total Recall — Mesh Diagnostics & Compute Capability Audit\n');
137
+ const enriched = listEnrichedMeshNodes(vaultRoot);
138
+ const isJson = args.includes('--json');
139
+
140
+ const stripAnsi = (s) => String(s || '').replace(/\x1b\[[0-9;]*m/g, '');
141
+ const pad = (s, l) => String(s || '') + ' '.repeat(Math.max(0, l - stripAnsi(s).length));
142
+ const probeNodes = enriched.filter(n => n.ip);
143
+ const results = [];
144
+
145
+ for (const node of probeNodes) {
146
+ const resolved = resolveNodeAccess(node);
147
+ const nodeInfo = {
148
+ hostname: node.hostname,
149
+ ip: node.ip,
150
+ self: !!node.self,
151
+ online: !!node.online,
152
+ sshConfigured: resolved.complete,
153
+ sshTarget: resolved.target,
154
+ runtimes: [],
155
+ harnesses: [],
156
+ reachable: false,
157
+ error: null
158
+ };
159
+
160
+ if (node.self) {
161
+ try {
162
+ const { detectHarnesses } = await import('../core/meta-harness.mjs');
163
+ nodeInfo.harnesses = detectHarnesses().filter(h => h.available).map(h => h.id);
164
+ } catch {}
165
+ nodeInfo.runtimes = ['node', 'git'];
166
+ nodeInfo.reachable = true;
167
+ } else if (resolved.complete) {
168
+ try {
169
+ const probeCmd = 'echo "agy=$(which agy 2>/dev/null) claude=$(which claude 2>/dev/null) codex=$(which codex 2>/dev/null) gemini=$(which gemini 2>/dev/null) ollama=$(which ollama 2>/dev/null) docker=$(which docker 2>/dev/null) node=$(which node 2>/dev/null) git=$(which git 2>/dev/null)"';
170
+ const { execMeshCommand } = await import('../core/mesh.mjs');
171
+ const res = await execMeshCommand(node.hostname, probeCmd, { vaultRoot, timeoutMs: 4000 });
172
+ nodeInfo.reachable = res.success;
173
+ if (res.success) {
174
+ const parts = res.stdout.split(/\s+/);
175
+ for (const part of parts) {
176
+ const [k, v] = part.split('=');
177
+ if (v && v.trim()) {
178
+ if (['agy', 'claude', 'codex', 'gemini', 'ollama'].includes(k)) {
179
+ nodeInfo.harnesses.push(k);
180
+ } else {
181
+ nodeInfo.runtimes.push(k);
182
+ }
183
+ }
184
+ }
185
+ } else {
186
+ nodeInfo.error = res.stderr || 'Probe failed';
187
+ }
188
+ } catch (err) {
189
+ nodeInfo.reachable = false;
190
+ nodeInfo.error = err.message;
191
+ }
192
+ } else {
193
+ nodeInfo.reachable = false;
194
+ nodeInfo.error = 'No recorded SSH login';
195
+ }
196
+
197
+ results.push(nodeInfo);
198
+ }
199
+
200
+ if (isJson) {
201
+ console.log(JSON.stringify(results, null, 2));
202
+ return;
203
+ }
204
+
205
+ console.log('┌──────────────────────────────────────────┬────────────────┬──────────────────┬──────────────────┬──────────────────────────────────────┬──────────────────────────┐');
206
+ console.log(`│ ${pad('Node', 40)} │ ${pad('Mesh IP', 14)} │ ${pad('Role', 16)} │ ${pad('Mesh SSH', 16)} │ ${pad('Harnesses', 36)} │ ${pad('Runtimes', 24)} │`);
207
+ console.log('├──────────────────────────────────────────┼────────────────┼──────────────────┼──────────────────┼──────────────────────────────────────┼──────────────────────────┤');
208
+
209
+ for (const r of results) {
210
+ const roleStr = r.self ? '\x1b[36mLocal (Self)\x1b[0m' : (r.online ? '\x1b[32mOnline Peer\x1b[0m' : '\x1b[90mOffline\x1b[0m');
211
+ const sshStr = r.self ? '—' : (r.reachable ? '\x1b[32mReachable ✅\x1b[0m' : '\x1b[31mUnreachable ❌\x1b[0m');
212
+ const hStr = r.harnesses.length ? r.harnesses.join(', ') : '—';
213
+ const runStr = r.runtimes.length ? r.runtimes.join(', ') : '—';
214
+ console.log(`│ ${pad(r.hostname, 40)} │ ${pad(r.ip, 14)} │ ${pad(roleStr, 16)} │ ${pad(sshStr, 16)} │ ${pad(hStr, 36)} │ ${pad(runStr, 24)} │`);
215
+ }
216
+ console.log('└──────────────────────────────────────────┴────────────────┴──────────────────┴──────────────────┴──────────────────────────────────────┴──────────────────────────┘\n');
217
+ return;
218
+ }
219
+
127
220
  if (command === 'nodes') {
128
221
  // The control server is the authoritative registry, but local discovery
129
222
  // plus the vault can already answer "what is on my mesh and how do I
@@ -226,6 +319,33 @@ export default async function meshCli(argv = []) {
226
319
  return;
227
320
  }
228
321
 
322
+ if (command === 'exec') {
323
+ const target = args.shift();
324
+ if (!target || !args.length) {
325
+ fail('`exec` requires a node name and a command.', 'Example: total-recall mesh exec macmini uptime');
326
+ return;
327
+ }
328
+ const jsonMode = args.includes('--json');
329
+ const filteredArgs = args.filter((a) => a !== '--json');
330
+ const cmd = filteredArgs.join(' ');
331
+
332
+ try {
333
+ const { execMeshCommand } = await import('../core/mesh.mjs');
334
+ const res = await execMeshCommand(target, cmd, { vaultRoot });
335
+ if (jsonMode) {
336
+ console.log(JSON.stringify(res, null, 2));
337
+ } else {
338
+ if (res.stdout) console.log(res.stdout);
339
+ if (res.stderr) console.error(res.stderr);
340
+ }
341
+ process.exitCode = res.exitCode;
342
+ } catch (err) {
343
+ fail(`Execution failed: ${err.message}`);
344
+ }
345
+ return;
346
+ }
347
+
348
+
229
349
  if (command === 'access') {
230
350
  const target = args.shift();
231
351
  if (!target) {
@@ -329,6 +449,172 @@ export default async function meshCli(argv = []) {
329
449
  return;
330
450
  }
331
451
 
452
+ if (command === 'leader') {
453
+ const { getLeaderInfo, isLeader } = await import('../core/leader-election.mjs');
454
+ const leaderInfo = await getLeaderInfo();
455
+ const thisIsLeader = await isLeader();
456
+ if (args.includes('--json')) {
457
+ console.log(JSON.stringify({ leader: leaderInfo, is_current_node_leader: thisIsLeader }, null, 2));
458
+ return;
459
+ }
460
+ if (!leaderInfo) {
461
+ console.log('No leader elected (no mesh nodes online).');
462
+ return;
463
+ }
464
+ console.log(`👑 Current Cluster Leader: ${leaderInfo.hostname} (${leaderInfo.ip})`);
465
+ console.log(` Strategy: ${leaderInfo.strategy}`);
466
+ console.log(` Local node: ${thisIsLeader ? 'LEADER (this node runs cluster workloads)' : 'FOLLOWER'}`);
467
+ return;
468
+ }
469
+
470
+ if (command === 'ping' || command === 'latency') {
471
+ const target = args.find((a) => !a.startsWith('-'));
472
+ const isJson = args.includes('--json');
473
+ const { getMeshPeers, findMeshNode } = await import('../core/mesh.mjs');
474
+ const { throttledFetch } = await import('../core/throttled-fetch.mjs');
475
+
476
+ const port = Number(process.env.TR_SERVER_PORT || process.env.PORT || 3000);
477
+
478
+ if (target) {
479
+ const node = findMeshNode(target, vaultRoot);
480
+ if (!node) {
481
+ fail(`No mesh node matches "${target}".`);
482
+ return;
483
+ }
484
+ if (node.self) {
485
+ if (isJson) {
486
+ console.log(JSON.stringify({ node: node.hostname, ip: node.ip, latency_ms: 0, self: true, reachable: true }, null, 2));
487
+ } else {
488
+ console.log(`📍 ${node.hostname} (${node.ip}) is this local node (0 ms).`);
489
+ }
490
+ return;
491
+ }
492
+ if (!node.ip || !node.online) {
493
+ if (isJson) {
494
+ console.log(JSON.stringify({ node: node.hostname, ip: node.ip, latency_ms: null, reachable: false, status: 'offline' }, null, 2));
495
+ } else {
496
+ console.log(`⚠️ ${node.hostname} (${node.ip || 'no IP'}) is offline.`);
497
+ }
498
+ return;
499
+ }
500
+
501
+ const start = Date.now();
502
+ try {
503
+ const res = await throttledFetch(`http://${node.ip}:${port}/health`, {}, 5000);
504
+ const ms = Date.now() - start;
505
+ const ok = res.ok || res.status === 200;
506
+ if (isJson) {
507
+ console.log(JSON.stringify({ node: node.hostname, ip: node.ip, latency_ms: ok ? ms : null, reachable: ok, status: res.status }, null, 2));
508
+ } else {
509
+ console.log(`✅ ${node.hostname} (${node.ip}): ${ms} ms (status: ${res.status})`);
510
+ }
511
+ } catch (err) {
512
+ const { spawnSync } = await import('node:child_process');
513
+ const pingResult = spawnSync('ping', ['-c', '1', '-W', '2', node.ip], { encoding: 'utf8', timeout: 3000 });
514
+ const ms = Date.now() - start;
515
+ if (pingResult.status === 0) {
516
+ if (isJson) {
517
+ console.log(JSON.stringify({ node: node.hostname, ip: node.ip, latency_ms: ms, reachable: true, method: 'icmp' }, null, 2));
518
+ } else {
519
+ console.log(`✅ ${node.hostname} (${node.ip}): ${ms} ms (ICMP)`);
520
+ }
521
+ } else {
522
+ if (isJson) {
523
+ console.log(JSON.stringify({ node: node.hostname, ip: node.ip, latency_ms: null, reachable: false, error: err.message }, null, 2));
524
+ } else {
525
+ console.log(`❌ ${node.hostname} (${node.ip}): unreachable (${err.message})`);
526
+ }
527
+ }
528
+ }
529
+ return;
530
+ }
531
+
532
+ const peers = getMeshPeers({ includeSelf: true });
533
+ const results = [];
534
+ for (const peer of peers) {
535
+ if (peer.self) {
536
+ results.push({ hostname: peer.hostname, ip: peer.ip, latency_ms: 0, status: 'self' });
537
+ continue;
538
+ }
539
+ if (!peer.ip || !peer.online) {
540
+ results.push({ hostname: peer.hostname, ip: peer.ip, latency_ms: null, status: 'offline' });
541
+ continue;
542
+ }
543
+ const start = Date.now();
544
+ try {
545
+ const res = await throttledFetch(`http://${peer.ip}:${port}/health`, {}, 4000);
546
+ const ms = Date.now() - start;
547
+ const ok = res.ok || res.status === 200;
548
+ results.push({ hostname: peer.hostname, ip: peer.ip, latency_ms: ok ? ms : null, status: ok ? 'online' : 'error' });
549
+ } catch {
550
+ const { spawnSync } = await import('node:child_process');
551
+ const pingResult = spawnSync('ping', ['-c', '1', '-W', '2', peer.ip], { encoding: 'utf8', timeout: 2000 });
552
+ const ms = Date.now() - start;
553
+ results.push({ hostname: peer.hostname, ip: peer.ip, latency_ms: pingResult.status === 0 ? ms : null, status: pingResult.status === 0 ? 'icmp' : 'unreachable' });
554
+ }
555
+ }
556
+
557
+ if (isJson) {
558
+ console.log(JSON.stringify(results, null, 2));
559
+ return;
560
+ }
561
+
562
+ console.log('┌──────────────────────────────────────────┬────────────────┬──────────────┬──────────────┐');
563
+ console.log('│ Node │ Mesh IP │ Latency │ Status │');
564
+ console.log('├──────────────────────────────────────────┼────────────────┼──────────────┼──────────────┤');
565
+ for (const r of results) {
566
+ const lat = r.latency_ms != null ? `${r.latency_ms} ms` : '—';
567
+ console.log(`│ ${String(r.hostname || '').padEnd(40)} │ ${String(r.ip || '-').padEnd(14)} │ ${lat.padEnd(12)} │ ${r.status.padEnd(12)} │`);
568
+ }
569
+ console.log('└──────────────────────────────────────────┴────────────────┴──────────────┴──────────────┘');
570
+ return;
571
+ }
572
+
573
+ if (command === 'enroll') {
574
+ const { getEnrollmentStatus, enrollThisNode } = await import('../core/mesh-enroll.mjs');
575
+ if (args.includes('--status')) {
576
+ const status = await getEnrollmentStatus({ brainDir });
577
+ console.log(JSON.stringify(status, null, 2));
578
+ return;
579
+ }
580
+
581
+ const readOption = (flag) => {
582
+ const index = args.indexOf(flag);
583
+ return index === -1 ? null : args[index + 1] || null;
584
+ };
585
+ const server = readOption('--server');
586
+ const user = readOption('--user');
587
+ const force = args.includes('--force');
588
+
589
+ console.log('Initiating mesh enrollment...');
590
+ const result = await enrollThisNode({
591
+ brainDir,
592
+ loginServer: server,
593
+ user,
594
+ force,
595
+ });
596
+
597
+ if (args.includes('--json')) {
598
+ console.log(JSON.stringify(result, null, 2));
599
+ return;
600
+ }
601
+
602
+ if (result.ok) {
603
+ if (result.reason === 'already-enrolled') {
604
+ console.log(`✅ Already enrolled on mesh (${result.state}). Use --force to re-enroll.`);
605
+ } else {
606
+ console.log(`✅ Successfully enrolled on mesh (${result.state}).`);
607
+ }
608
+ } else if (result.interactiveUrl) {
609
+ console.log(`⚠️ Interactive authorization required.`);
610
+ console.log(` Open this URL in your browser to approve:`);
611
+ console.log(` ${result.interactiveUrl}`);
612
+ } else {
613
+ fail(result.reason || 'Enrollment failed', result.hint);
614
+ }
615
+ return;
616
+ }
617
+
332
618
  if (command !== 'policy') {
333
619
  fail(`Unknown mesh command: ${command}`, 'Run `total-recall mesh --help`.');
334
620
  return;
@@ -0,0 +1,186 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import os from "node:os";
4
+ import { validatePluginManifest } from "../../core/plugin-loader.mjs";
5
+
6
+ function toTitleCase(kebab) {
7
+ return kebab
8
+ .split("-")
9
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
10
+ .join(" ");
11
+ }
12
+
13
+ export async function createPlugin(args = []) {
14
+ const isGlobal = args.includes("--global") || args.includes("-g");
15
+ const withCli = args.includes("--with-cli");
16
+ const withGenerator = args.includes("--with-generator");
17
+
18
+ let name = null;
19
+ let description = null;
20
+ let category = null;
21
+
22
+ const cleanArgs = [];
23
+ for (let i = 0; i < args.length; i++) {
24
+ const arg = args[i];
25
+ if (arg === "--global" || arg === "-g" || arg === "--with-cli" || arg === "--with-generator") {
26
+ continue;
27
+ }
28
+ if (arg === "--name" && args[i + 1]) {
29
+ name = args[i + 1];
30
+ i++;
31
+ continue;
32
+ }
33
+ if (arg === "--description" && args[i + 1]) {
34
+ description = args[i + 1];
35
+ i++;
36
+ continue;
37
+ }
38
+ if (arg === "--category" && args[i + 1]) {
39
+ category = args[i + 1];
40
+ i++;
41
+ continue;
42
+ }
43
+ cleanArgs.push(arg);
44
+ }
45
+
46
+ const id = cleanArgs[0];
47
+ if (!id) {
48
+ console.error("❌ Error: Missing plugin id.");
49
+ console.error(" Usage: total-recall plugin create <id> [--name <name>] [--description <desc>] [--category <cat>] [--with-cli] [--with-generator] [--global]\n");
50
+ process.exit(1);
51
+ }
52
+
53
+ const ID_PATTERN = /^[a-z][a-z0-9-]{1,63}$/;
54
+ if (!ID_PATTERN.test(id)) {
55
+ console.error(`❌ Error: Invalid plugin id "${id}". Must be lowercase kebab-case (^[a-z][a-z0-9-]{1,63}$)`);
56
+ process.exit(1);
57
+ }
58
+
59
+ const pluginsBaseDir = isGlobal
60
+ ? path.join(os.homedir(), ".agent", "plugins")
61
+ : path.join(process.cwd(), ".agent", "plugins");
62
+
63
+ const pluginDir = path.join(pluginsBaseDir, id);
64
+ if (fs.existsSync(pluginDir)) {
65
+ console.error(`❌ Error: Plugin directory already exists at ${pluginDir}`);
66
+ process.exit(1);
67
+ }
68
+
69
+ fs.mkdirSync(pluginDir, { recursive: true });
70
+
71
+ const pluginName = name || toTitleCase(id);
72
+ const pluginDesc = description || `${pluginName} extension for Total Recall AI OS`;
73
+
74
+ const manifest = {
75
+ $schema: "https://github.com/total-recall/total-recall/blob/main/metadata.plugin.schema.json",
76
+ id,
77
+ name: pluginName,
78
+ version: "1.0.0",
79
+ description: pluginDesc,
80
+ author: "Total Recall Ecosystem",
81
+ license: "MIT"
82
+ };
83
+
84
+ if (category) {
85
+ manifest.ssss_schemas = {
86
+ categories: [
87
+ {
88
+ name: category,
89
+ description: `Custom ${category} memory nodes for ${pluginName}`,
90
+ node_type: "memory"
91
+ }
92
+ ]
93
+ };
94
+ }
95
+
96
+ if (withCli) {
97
+ manifest.cli = {
98
+ command: id,
99
+ handler: "./cli.mjs",
100
+ subcommands: [
101
+ { name: "status", description: `Show ${pluginName} status` }
102
+ ]
103
+ };
104
+
105
+ const cliContent = `#!/usr/bin/env node
106
+ export async function run(argv = []) {
107
+ const args = Array.isArray(argv) ? argv.slice(2) : [];
108
+ const sub = args[0] || "status";
109
+ console.log(\`⚡ \${sub.toUpperCase()} from plugin: ${id}\`);
110
+ console.log("Plugin is active and responding to CLI dispatch.");
111
+ }
112
+ export default run;
113
+ `;
114
+ fs.writeFileSync(path.join(pluginDir, "cli.mjs"), cliContent, "utf8");
115
+ }
116
+
117
+ if (withGenerator) {
118
+ manifest.compile = {
119
+ generator: "./generator.mjs",
120
+ auto: true
121
+ };
122
+
123
+ const generatorContent = `/**
124
+ * Context generator for ${pluginName}.
125
+ * Invoked during evolving context compilation.
126
+ */
127
+ export async function generateContext({ projectRoot, nodes = [] }) {
128
+ return \`#### ${pluginName} Context\n- Status: Operational\n- Active nodes: \${nodes.length}\n\`;
129
+ }
130
+ export default generateContext;
131
+ `;
132
+ fs.writeFileSync(path.join(pluginDir, "generator.mjs"), generatorContent, "utf8");
133
+ }
134
+
135
+ manifest.tasks = [
136
+ {
137
+ intent: `Periodic health verification for ${id}`,
138
+ schedule: "0 * * * *"
139
+ }
140
+ ];
141
+
142
+ const validation = validatePluginManifest(manifest);
143
+ if (!validation.valid) {
144
+ console.error("❌ Validation failed for generated manifest:", validation.errors);
145
+ fs.rmSync(pluginDir, { recursive: true, force: true });
146
+ process.exit(1);
147
+ }
148
+
149
+ fs.writeFileSync(
150
+ path.join(pluginDir, "plugin.json"),
151
+ JSON.stringify(manifest, null, 2) + "\n",
152
+ "utf8"
153
+ );
154
+
155
+ const lines = [
156
+ `# ${pluginName}`,
157
+ "",
158
+ `> ${pluginDesc}`,
159
+ "",
160
+ "## Installation",
161
+ "",
162
+ "```bash",
163
+ `npx total-recall plugin install ./.agent/plugins/${id} --link`,
164
+ "```",
165
+ "",
166
+ "## Features",
167
+ "",
168
+ `- **Identifier**: \`${id}\``,
169
+ "- **Version**: 1.0.0"
170
+ ];
171
+ if (category) lines.push(`- **SSSS Category**: \`${category}\``);
172
+ if (withCli) lines.push(`- **CLI Command**: \`npx total-recall ${id}\``);
173
+ lines.push("");
174
+
175
+ fs.writeFileSync(path.join(pluginDir, "README.md"), lines.join("\n"), "utf8");
176
+
177
+ console.log(`\n🎉 Successfully scaffolded plugin "${pluginName}"!`);
178
+ console.log(` Location: \x1b[36m${pluginDir}\x1b[0m`);
179
+ console.log(` Manifest: \x1b[33m${path.join(pluginDir, "plugin.json")}\x1b[0m`);
180
+ if (withCli) {
181
+ console.log(` CLI Handler: \x1b[32m${path.join(pluginDir, "cli.mjs")}\x1b[0m`);
182
+ }
183
+ console.log(`\nNext steps:`);
184
+ console.log(` Inspect: \x1b[32mnpx total-recall plugin info ${id}\x1b[0m`);
185
+ console.log(` List: \x1b[32mnpx total-recall plugin list\x1b[0m\n`);
186
+ }
@@ -0,0 +1,139 @@
1
+ import { listPlugins } from './list.mjs';
2
+ import { installPlugin } from './install.mjs';
3
+ import { removePlugin } from './remove.mjs';
4
+ import { searchCatalog } from './search.mjs';
5
+ import { createPlugin } from './create.mjs';
6
+ import { getPlugin } from '../../core/plugin-loader.mjs';
7
+
8
+ function printHelp() {
9
+ console.log(`
10
+ 🔌 Total Recall — Plugin Management System
11
+
12
+ Usage:
13
+ npx total-recall plugin <command> [options]
14
+
15
+ Commands:
16
+ search [query] Search plugin catalog (--json for pipelines)
17
+ catalog Browse the full catalog
18
+ list List all installed plugins (project & global)
19
+ create <id> Scaffold a new plugin manifest and directory
20
+ --name <name> Human-readable plugin name
21
+ --description <desc> Plugin purpose & capability
22
+ --category <cat> Declare an SSSS memory category
23
+ --with-cli Generate CLI command handler
24
+ --with-generator Generate context generator
25
+ --global, -g Create in ~/.agent/plugins/
26
+ install <path|git-url> Install or link a plugin
27
+ --link, -l Symlink local directory instead of copying
28
+ --global, -g Install to ~/.agent/plugins/
29
+ remove <id> Uninstall or unlink an installed plugin
30
+ info <id> Inspect detailed manifest for an installed plugin
31
+
32
+ Examples:
33
+ npx total-recall plugin list
34
+ npx total-recall plugin create system-monitor --with-cli --category system-metrics
35
+ npx total-recall plugin install ./path/to/my-plugin --link
36
+ npx total-recall plugin install https://github.com/org/scientific-frontiers.git
37
+ npx total-recall plugin info scientific-frontiers
38
+ npx total-recall plugin remove scientific-frontiers
39
+ `);
40
+ }
41
+
42
+ export async function run(argv = []) {
43
+ let args = argv;
44
+ if (Array.isArray(args) && args[0]?.endsWith('node')) {
45
+ args = args.slice(2);
46
+ }
47
+ if (args[0] === 'plugin' || args[0] === 'plugins') {
48
+ args = args.slice(1);
49
+ }
50
+
51
+ const command = args[0];
52
+ const rest = args.slice(1);
53
+
54
+ if (command === 'search' || command === 'catalog' || command === 'find') {
55
+ await searchCatalog(rest);
56
+ return;
57
+ }
58
+
59
+ if (command === 'create' || command === 'new' || command === 'init') {
60
+ await createPlugin(rest);
61
+ return;
62
+ }
63
+
64
+ if (!command || command === 'list') {
65
+ await listPlugins(rest);
66
+ return;
67
+ }
68
+
69
+ if (command === 'install' || command === 'add') {
70
+ await installPlugin(rest);
71
+ return;
72
+ }
73
+
74
+ if (command === 'remove' || command === 'rm' || command === 'uninstall') {
75
+ await removePlugin(rest);
76
+ return;
77
+ }
78
+
79
+ if (command === 'info' || command === 'show') {
80
+ const id = rest[0];
81
+ if (!id) {
82
+ console.error('❌ Error: Missing plugin ID. Usage: total-recall plugin info <id>');
83
+ process.exit(1);
84
+ }
85
+ const plugin = getPlugin(id, process.cwd());
86
+ if (!plugin) {
87
+ console.error(`❌ Plugin '${id}' not found.`);
88
+ process.exit(1);
89
+ }
90
+
91
+ console.log(`\n🔌 Plugin: ${plugin.manifest.name || plugin.id}`);
92
+ console.log(` ID: ${plugin.id}`);
93
+ console.log(` Version: v${plugin.manifest.version || '0.0.0'}`);
94
+ console.log(` Description: ${plugin.manifest.description || '—'}`);
95
+ console.log(` Directory: ${plugin.dir}`);
96
+ console.log(` Valid: ${plugin.valid ? '✅ Yes' : '❌ No'}`);
97
+
98
+ if (plugin.manifest.cli?.command) {
99
+ console.log(` CLI Command: total-recall ${plugin.manifest.cli.command}`);
100
+ }
101
+
102
+ const cats = plugin.manifest.ssss_schemas?.categories || [];
103
+ if (cats.length > 0) {
104
+ console.log(` SSSS Categories:`);
105
+ for (const c of cats) {
106
+ console.log(` - ${c.name} (${c.node_type || 'memory'}): ${c.description || ''}`);
107
+ }
108
+ }
109
+
110
+ const tasks = plugin.manifest.tasks || [];
111
+ if (tasks.length > 0) {
112
+ console.log(` Background Tasks:`);
113
+ for (const t of tasks) {
114
+ console.log(` - [${t.schedule}] ${t.intent}`);
115
+ }
116
+ }
117
+
118
+ const hooks = plugin.manifest.hooks || {};
119
+ if (Object.keys(hooks).length > 0) {
120
+ console.log(` Lifecycle Hooks:`);
121
+ for (const [h, script] of Object.entries(hooks)) {
122
+ console.log(` - ${h}: ${script}`);
123
+ }
124
+ }
125
+ console.log();
126
+ return;
127
+ }
128
+
129
+ if (command === '--help' || command === '-h' || command === 'help') {
130
+ printHelp();
131
+ return;
132
+ }
133
+
134
+ console.error(`❌ Unknown plugin command: '${command}'`);
135
+ printHelp();
136
+ process.exit(1);
137
+ }
138
+
139
+ export default run;