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.
- package/bin/total-recall.mjs +24 -1
- package/docs/ARCHITECTURE.md +26 -18
- package/extension/background.js +259 -84
- package/extension/content-script.css +7 -13
- package/extension/content-script.js +252 -477
- package/extension/icons/icon-128.png +0 -0
- package/extension/icons/icon-16.png +0 -0
- package/extension/icons/icon-32.png +0 -0
- package/extension/icons/icon-48.png +0 -0
- package/extension/icons/mark.svg +6 -0
- package/extension/lib/brain-client.js +160 -73
- package/extension/lib/tokens.css +187 -0
- package/extension/lib/ui-helpers.js +229 -0
- package/extension/lib/ui-helpers.spec.mjs +94 -0
- package/extension/manifest.json +20 -5
- package/extension/options/options.css +64 -218
- package/extension/options/options.html +91 -43
- package/extension/options/options.js +208 -85
- package/extension/sidepanel/sidepanel.css +268 -970
- package/extension/sidepanel/sidepanel.html +200 -166
- package/extension/sidepanel/sidepanel.js +788 -942
- package/frontend/dist/assets/index-DGMNZi_1.js +5742 -0
- package/frontend/dist/index.html +1 -1
- package/frontend/src/api/plugins.ts +124 -92
- package/frontend/src/components/ResearchAgendaTab.spec.tsx +11 -0
- package/frontend/src/components/ResearchAgendaTab.tsx +19 -14
- package/frontend/src/pages/PluginsPage.spec.tsx +87 -16
- package/frontend/src/pages/PluginsPage.tsx +454 -1218
- package/package.json +2 -1
- package/plugins/code-quality/README.md +25 -0
- package/plugins/code-quality/cli.mjs +25 -0
- package/plugins/code-quality/plugin.json +17 -0
- package/plugins/git-sentinel/README.md +27 -0
- package/{scaffold/.agent/plugins → plugins}/git-sentinel/cli.mjs +8 -5
- package/plugins/git-sentinel/plugin.json +18 -0
- package/plugins/system-monitor/README.md +26 -0
- package/{scaffold/.agent/plugins → plugins}/system-monitor/cli.mjs +5 -4
- package/{scaffold/.agent/plugins → plugins}/system-monitor/generator.mjs +4 -5
- package/plugins/system-monitor/plugin.json +29 -0
- package/scaffold/.agent/skills/meta-harness/SKILL.md +4 -4
- package/scaffold/.agent/skills/plugins/SKILL.md +53 -0
- package/scaffold/.agent/skills/total-recall/scripts/brain-state.json +1 -0
- package/src/cli/agent.mjs +1 -1
- package/src/cli/app/index.mjs +493 -0
- package/src/cli/brain.mjs +33 -3
- package/src/cli/command.mjs +80 -41
- package/src/cli/harness.mjs +1 -1
- package/src/cli/init.mjs +3 -1
- package/src/cli/mesh.mjs +118 -12
- package/src/cli/plugin/create.mjs +144 -25
- package/src/cli/plugin/index.mjs +83 -53
- package/src/cli/plugin/install.mjs +10 -127
- package/src/cli/plugin/list.mjs +44 -44
- package/src/cli/plugin/remove.mjs +23 -30
- package/src/cli/plugin/scaffold-cmd.mjs +30 -0
- package/src/cli/plugin/search.mjs +108 -23
- package/src/cli/rebuild.mjs +37 -1
- package/src/cli/recall.mjs +17 -2
- package/src/cli/research.mjs +4 -6
- package/src/cli/share.mjs +2 -0
- package/src/core/app-deploy/apply.mjs +386 -0
- package/src/core/app-deploy/design-tokens.mjs +217 -0
- package/src/core/app-deploy/plan.mjs +249 -0
- package/src/core/app-deploy/resolve.mjs +295 -0
- package/src/core/app-deploy/source.mjs +337 -0
- package/src/core/app-deploy/standalone.mjs +248 -0
- package/src/core/app-deploy/upgrade.mjs +256 -0
- package/src/core/app-deploy/verify.mjs +213 -0
- package/src/core/bound-hosts.mjs +1 -1
- package/src/core/brain-registry.mjs +146 -0
- package/src/core/brain-state.json +1 -0
- package/src/core/daemon-loop.mjs +30 -26
- package/src/core/dream.mjs +5 -17
- package/src/core/fact-seeker.mjs +53 -368
- package/src/core/lan-discovery.mjs +3 -3
- package/src/core/mesh-access-sync.mjs +134 -0
- package/src/core/mesh-access.mjs +18 -3
- package/src/core/mesh-discover.mjs +173 -0
- package/src/core/mesh.mjs +131 -32
- package/src/core/optimizer.mjs +4 -41
- package/src/core/plugin-bundle.mjs +167 -0
- package/src/core/plugin-context.mjs +8 -13
- package/src/core/plugin-loader.mjs +272 -68
- package/src/core/plugin-peers.mjs +163 -0
- package/src/core/plugin-public.mjs +85 -0
- package/src/core/plugin-runner-child.mjs +24 -0
- package/src/core/plugin-runner.mjs +102 -0
- package/src/core/plugin-store.mjs +384 -0
- package/src/core/plugin-tasks.mjs +212 -0
- package/src/core/post-mortem.mjs +40 -9
- package/src/core/research-gate.mjs +182 -0
- package/src/core/research-queue.mjs +47 -7
- package/src/core/research-surface.mjs +211 -0
- package/src/core/research.mjs +25 -32
- package/src/core/scheduler.mjs +67 -153
- package/src/core/schema.mjs +129 -0
- package/src/core/search.mjs +12 -1
- package/src/core/secret-integration-research.mjs +12 -8
- package/src/core/secrets-store.mjs +32 -2
- package/src/core/session-watcher.mjs +6 -0
- package/src/core/surface.mjs +64 -3
- package/src/core/task-executors.mjs +8 -45
- package/src/core/vault-cache.mjs +70 -4
- package/src/server/api.mjs +21 -0
- package/src/server/rest.mjs +2 -0
- package/src/server/route-manifest.json +21 -5
- package/src/server/routes/collab.mjs +38 -11
- package/src/server/routes/extension.mjs +3 -2
- package/src/server/routes/memory.mjs +24 -2
- package/src/server/routes/mesh.mjs +7 -7
- package/src/server/routes/plugins-mesh.mjs +49 -0
- package/src/server/routes/plugins.mjs +91 -426
- package/src/server/routes/research.mjs +5 -2
- package/src/server/routes/share.mjs +2 -0
- package/src/server/tools.mjs +29 -2
- package/extension/icons/README.md +0 -27
- package/extension/lib/preconfigured.js +0 -5
- package/extension/popup/popup.css +0 -227
- package/extension/popup/popup.html +0 -50
- package/extension/popup/popup.js +0 -175
- package/frontend/dist/assets/index-U7o-qU99.js +0 -5742
- package/scaffold/.agent/plugins/.gitkeep +0 -0
- package/scaffold/.agent/plugins/code-quality/README.md +0 -16
- package/scaffold/.agent/plugins/code-quality/cli.mjs +0 -15
- package/scaffold/.agent/plugins/code-quality/plugin.json +0 -34
- package/scaffold/.agent/plugins/git-sentinel/README.md +0 -16
- package/scaffold/.agent/plugins/git-sentinel/plugin.json +0 -34
- package/scaffold/.agent/plugins/meta-harness/README.md +0 -16
- package/scaffold/.agent/plugins/meta-harness/cli.mjs +0 -34
- package/scaffold/.agent/plugins/meta-harness/plugin.json +0 -34
- package/scaffold/.agent/plugins/system-monitor/README.md +0 -16
- package/scaffold/.agent/plugins/system-monitor/plugin.json +0 -38
package/src/cli/command.mjs
CHANGED
|
@@ -4,25 +4,57 @@ import { resolveBrainLayer } from '../core/config.mjs';
|
|
|
4
4
|
|
|
5
5
|
function printHelp() {
|
|
6
6
|
console.log(`
|
|
7
|
-
total-recall command — Manage
|
|
7
|
+
total-recall command — Manage custom composable CLI commands
|
|
8
8
|
|
|
9
9
|
Usage:
|
|
10
|
-
total-recall command create <name> "<code>" Create a new custom CLI command
|
|
11
|
-
total-recall command read <name> Read the code of a custom CLI command
|
|
12
|
-
total-recall command update <name> "<code>" Update an existing custom CLI command
|
|
13
|
-
total-recall command remove <name> Remove a custom CLI command
|
|
14
|
-
total-recall command list List
|
|
10
|
+
total-recall command create <name> "<code>" [--global] Create a new custom CLI command
|
|
11
|
+
total-recall command read <name> [--global] Read the code of a custom CLI command
|
|
12
|
+
total-recall command update <name> "<code>" [--global] Update an existing custom CLI command
|
|
13
|
+
total-recall command remove <name> [--global] Remove a custom CLI command
|
|
14
|
+
total-recall command list [--global] List custom CLI commands
|
|
15
15
|
|
|
16
16
|
Examples:
|
|
17
17
|
npx total-recall command create hello "console.log('Hello from the brain!');"
|
|
18
|
+
npx total-recall command create my-tool "console.log(args);" --global
|
|
18
19
|
npx total-recall command read hello
|
|
19
20
|
npx total-recall command list
|
|
20
21
|
`);
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
function resolveTargetDir(isGlobal) {
|
|
25
|
+
if (isGlobal) {
|
|
26
|
+
try {
|
|
27
|
+
const gBrain = resolveBrainLayer('global');
|
|
28
|
+
return path.join(gBrain.agentDir, 'commands');
|
|
29
|
+
} catch {
|
|
30
|
+
return path.join(process.env.HOME || '/root', '.agent', 'commands');
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const pBrain = resolveBrainLayer('project');
|
|
36
|
+
return path.join(pBrain.agentDir, 'commands');
|
|
37
|
+
} catch {
|
|
38
|
+
try {
|
|
39
|
+
const gBrain = resolveBrainLayer('global');
|
|
40
|
+
return path.join(gBrain.agentDir, 'commands');
|
|
41
|
+
} catch {
|
|
42
|
+
return path.join(process.env.HOME || '/root', '.agent', 'commands');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function run(argv = []) {
|
|
48
|
+
const args = Array.isArray(argv) ? argv.slice(3) : [];
|
|
49
|
+
return commandCmd(args);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export default async function commandCmd(rawArgs = []) {
|
|
53
|
+
const isGlobal = rawArgs.includes('--global') || rawArgs.includes('-g');
|
|
54
|
+
const cleanArgs = rawArgs.filter(a => a !== '--global' && a !== '-g');
|
|
55
|
+
|
|
56
|
+
const action = cleanArgs[0];
|
|
57
|
+
const name = cleanArgs[1];
|
|
26
58
|
|
|
27
59
|
if (!action || action === '--help' || action === '-h') {
|
|
28
60
|
printHelp();
|
|
@@ -31,27 +63,17 @@ export default async function commandCmd(args) {
|
|
|
31
63
|
|
|
32
64
|
if (!name && action !== 'list') {
|
|
33
65
|
console.error(`Error: Missing command name.`);
|
|
34
|
-
console.error(`Usage: total-recall command ${action} <name
|
|
66
|
+
console.error(`Usage: total-recall command ${action} <name> [--global]`);
|
|
35
67
|
process.exit(1);
|
|
36
68
|
}
|
|
37
69
|
|
|
38
|
-
|
|
39
|
-
let projectBrain;
|
|
40
|
-
try {
|
|
41
|
-
projectBrain = resolveBrainLayer('project');
|
|
42
|
-
} catch (err) {
|
|
43
|
-
console.error(`Error: You must be inside a Total Recall project to manage custom commands.`);
|
|
44
|
-
console.error(err.message);
|
|
45
|
-
process.exit(1);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const commandsDir = path.join(projectBrain.agentDir, 'commands');
|
|
70
|
+
const commandsDir = resolveTargetDir(isGlobal);
|
|
49
71
|
|
|
50
72
|
if (action === 'create') {
|
|
51
|
-
const code =
|
|
73
|
+
const code = cleanArgs[2];
|
|
52
74
|
if (!code) {
|
|
53
75
|
console.error(`Error: Missing code snippet for the command.`);
|
|
54
|
-
console.error(`Usage: total-recall command create <name> "<code>"`);
|
|
76
|
+
console.error(`Usage: total-recall command create <name> "<code>" [--global]`);
|
|
55
77
|
process.exit(1);
|
|
56
78
|
}
|
|
57
79
|
|
|
@@ -61,9 +83,13 @@ export default async function commandCmd(args) {
|
|
|
61
83
|
|
|
62
84
|
const commandPath = path.join(commandsDir, `${name}.mjs`);
|
|
63
85
|
|
|
64
|
-
// Wrap code in
|
|
86
|
+
// Wrap code in an exported run/default function for composable CLI execution
|
|
65
87
|
const fileContent = `// Auto-generated custom CLI command: ${name}
|
|
66
|
-
export
|
|
88
|
+
export async function run(argv = []) {
|
|
89
|
+
const args = Array.isArray(argv) ? argv.slice(3) : [];
|
|
90
|
+
${code}
|
|
91
|
+
}
|
|
92
|
+
export default async function (args = []) {
|
|
67
93
|
${code}
|
|
68
94
|
}
|
|
69
95
|
`;
|
|
@@ -76,26 +102,29 @@ export default async function run(args) {
|
|
|
76
102
|
if (fs.existsSync(commandPath)) {
|
|
77
103
|
console.log(fs.readFileSync(commandPath, 'utf8'));
|
|
78
104
|
} else {
|
|
79
|
-
console.error(`Error: Custom command '${name}' does not exist.`);
|
|
105
|
+
console.error(`Error: Custom command '${name}' does not exist in ${commandsDir}.`);
|
|
80
106
|
process.exit(1);
|
|
81
107
|
}
|
|
82
108
|
} else if (action === 'update') {
|
|
83
|
-
const code =
|
|
109
|
+
const code = cleanArgs[2];
|
|
84
110
|
if (!code) {
|
|
85
111
|
console.error(`Error: Missing code snippet for the command.`);
|
|
86
|
-
console.error(`Usage: total-recall command update <name> "<code>"`);
|
|
112
|
+
console.error(`Usage: total-recall command update <name> "<code>" [--global]`);
|
|
87
113
|
process.exit(1);
|
|
88
114
|
}
|
|
89
115
|
|
|
90
116
|
const commandPath = path.join(commandsDir, `${name}.mjs`);
|
|
91
117
|
if (!fs.existsSync(commandPath)) {
|
|
92
|
-
console.error(`Error: Custom command '${name}' does not exist.`);
|
|
118
|
+
console.error(`Error: Custom command '${name}' does not exist in ${commandsDir}.`);
|
|
93
119
|
process.exit(1);
|
|
94
120
|
}
|
|
95
121
|
|
|
96
|
-
// Wrap code in a default exported function for CLI execution
|
|
97
122
|
const fileContent = `// Auto-generated custom CLI command: ${name}
|
|
98
|
-
export
|
|
123
|
+
export async function run(argv = []) {
|
|
124
|
+
const args = Array.isArray(argv) ? argv.slice(3) : [];
|
|
125
|
+
${code}
|
|
126
|
+
}
|
|
127
|
+
export default async function (args = []) {
|
|
99
128
|
${code}
|
|
100
129
|
}
|
|
101
130
|
`;
|
|
@@ -104,18 +133,28 @@ export default async function run(args) {
|
|
|
104
133
|
console.log(`\x1b[32m✔ Successfully updated custom command: \x1b[1m${name}\x1b[0m`);
|
|
105
134
|
console.log(` Saved to: ${commandPath}`);
|
|
106
135
|
} else if (action === 'list') {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
136
|
+
const targets = isGlobal
|
|
137
|
+
? [{ label: 'Global', dir: resolveTargetDir(true) }]
|
|
138
|
+
: [
|
|
139
|
+
{ label: 'Project', dir: resolveTargetDir(false) },
|
|
140
|
+
{ label: 'Global', dir: resolveTargetDir(true) }
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
let foundAny = false;
|
|
144
|
+
for (const t of targets) {
|
|
145
|
+
if (fs.existsSync(t.dir)) {
|
|
146
|
+
const files = fs.readdirSync(t.dir).filter(f => f.endsWith('.mjs'));
|
|
147
|
+
if (files.length > 0) {
|
|
148
|
+
foundAny = true;
|
|
149
|
+
console.log(`\x1b[1m${t.label} CLI commands (${t.dir}):\x1b[0m`);
|
|
150
|
+
files.forEach(f => {
|
|
151
|
+
console.log(` - \x1b[36m${f.replace('.mjs', '')}\x1b[0m (npx total-recall ${f.replace('.mjs', '')})`);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
110
155
|
}
|
|
111
|
-
|
|
112
|
-
if (files.length === 0) {
|
|
156
|
+
if (!foundAny) {
|
|
113
157
|
console.log('No custom commands found.');
|
|
114
|
-
} else {
|
|
115
|
-
console.log(`\x1b[1mCustom CLI commands in ${commandsDir}:\x1b[0m`);
|
|
116
|
-
files.forEach(f => {
|
|
117
|
-
console.log(` - ${f.replace('.mjs', '')}`);
|
|
118
|
-
});
|
|
119
158
|
}
|
|
120
159
|
} else if (action === 'remove') {
|
|
121
160
|
const commandPath = path.join(commandsDir, `${name}.mjs`);
|
package/src/cli/harness.mjs
CHANGED
|
@@ -25,7 +25,7 @@ Available Harness IDs:
|
|
|
25
25
|
Examples:
|
|
26
26
|
npx total-recall harness list
|
|
27
27
|
npx total-recall harness dispatch claude "Review and run code quality checks on src/core/"
|
|
28
|
-
npx total-recall harness dispatch ollama --node
|
|
28
|
+
npx total-recall harness dispatch ollama --node build-box "What is test-time compute scaling?"
|
|
29
29
|
npx total-recall harness council "Propose architecture for decentralized research mesh"
|
|
30
30
|
`);
|
|
31
31
|
}
|
package/src/cli/init.mjs
CHANGED
|
@@ -223,7 +223,7 @@ export default async function init(args) {
|
|
|
223
223
|
|
|
224
224
|
const dirs = [
|
|
225
225
|
path.join(agentDir, 'skills'),
|
|
226
|
-
path.join(agentDir, 'plugins'),
|
|
226
|
+
path.join(agentDir, 'skills', 'total-recall', 'plugins'),
|
|
227
227
|
];
|
|
228
228
|
|
|
229
229
|
let created = 0;
|
|
@@ -917,4 +917,6 @@ export default async function init(args) {
|
|
|
917
917
|
} else if (opts.yes) {
|
|
918
918
|
log(`Dashboard (when server running): ${dashboardUrl}`);
|
|
919
919
|
}
|
|
920
|
+
|
|
921
|
+
process.exit(0);
|
|
920
922
|
}
|
package/src/cli/mesh.mjs
CHANGED
|
@@ -25,7 +25,12 @@ import {
|
|
|
25
25
|
setHeadscalePolicy,
|
|
26
26
|
buildMeshSshPolicy,
|
|
27
27
|
} from '../core/headscale-client.mjs';
|
|
28
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
findMeshNode,
|
|
30
|
+
listEnrichedMeshNodes,
|
|
31
|
+
meshVaultRoot,
|
|
32
|
+
setMeshNodeAccess,
|
|
33
|
+
} from '../core/mesh.mjs';
|
|
29
34
|
import {
|
|
30
35
|
buildSshArgs,
|
|
31
36
|
formatAccessTarget,
|
|
@@ -49,6 +54,8 @@ function printHelp() {
|
|
|
49
54
|
access <node> --user <u> [--port <n>] [--host <h>] [--identity <path>]
|
|
50
55
|
Record how to reach a node
|
|
51
56
|
access import Propose access from ~/.ssh/config (--apply to save)
|
|
57
|
+
access sync Learn login accounts from every peer already reachable
|
|
58
|
+
access discover [node] Find a working login with this machine's keys and record it
|
|
52
59
|
ping [node] Measure round-trip latency to a node (or all nodes) (--json)
|
|
53
60
|
leader Show cluster leader election status and current leader (--json)
|
|
54
61
|
enroll [options] Enroll this node on the mesh control server
|
|
@@ -76,8 +83,8 @@ function printHelp() {
|
|
|
76
83
|
Examples:
|
|
77
84
|
npx total-recall mesh nodes
|
|
78
85
|
npx total-recall mesh access import
|
|
79
|
-
npx total-recall mesh ssh
|
|
80
|
-
npx total-recall mesh ssh
|
|
86
|
+
npx total-recall mesh ssh build-box
|
|
87
|
+
npx total-recall mesh ssh build-box 'uptime'
|
|
81
88
|
npx total-recall mesh policy init-ssh
|
|
82
89
|
`);
|
|
83
90
|
}
|
|
@@ -88,6 +95,42 @@ function fail(message, hint) {
|
|
|
88
95
|
process.exitCode = 1;
|
|
89
96
|
}
|
|
90
97
|
|
|
98
|
+
function describeDiscoveryFailure(result, node) {
|
|
99
|
+
const name = String(node.hostname).split('.')[0];
|
|
100
|
+
switch (result.reason) {
|
|
101
|
+
case 'host-key':
|
|
102
|
+
return `host key not trusted yet — run 'total-recall mesh ssh ${name}' once to confirm it`;
|
|
103
|
+
case 'unreachable':
|
|
104
|
+
return 'not answering on ssh (asleep, offline, or no ssh server)';
|
|
105
|
+
case 'no-address':
|
|
106
|
+
return 'no address known';
|
|
107
|
+
case 'no-candidate-user':
|
|
108
|
+
return `no account to try — 'total-recall mesh access ${name} --user <login>'`;
|
|
109
|
+
default:
|
|
110
|
+
return `none of ${result.attempts} account/key pairs was accepted — authorise this machine's key on ${name}`;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Say what actually blocks a node, so an untrusted host key or a machine with
|
|
115
|
+
// no ssh server is not reported the same way as a missing account.
|
|
116
|
+
function explainUnreachable(result) {
|
|
117
|
+
const name = String(result.hostname).split('.')[0];
|
|
118
|
+
const text = String(result.error || '');
|
|
119
|
+
if (!result.sshConfigured) {
|
|
120
|
+
return `no login recorded — try 'total-recall mesh access sync' or 'total-recall mesh access discover ${name}'`;
|
|
121
|
+
}
|
|
122
|
+
if (/Host key verification failed|REMOTE HOST IDENTIFICATION HAS CHANGED/i.test(text)) {
|
|
123
|
+
return `host key not trusted yet — run 'total-recall mesh ssh ${name}' once to confirm it`;
|
|
124
|
+
}
|
|
125
|
+
if (/Permission denied/i.test(text)) {
|
|
126
|
+
return `${result.sshTarget} refused this machine's key — authorise it on ${name}`;
|
|
127
|
+
}
|
|
128
|
+
if (/timed out|No route to host|Connection refused/i.test(text)) {
|
|
129
|
+
return 'not answering on ssh (asleep, offline, or no ssh server)';
|
|
130
|
+
}
|
|
131
|
+
return text.split('\n')[0] || 'probe failed';
|
|
132
|
+
}
|
|
133
|
+
|
|
91
134
|
// A file-mode control server cannot accept an API-written policy. That is a
|
|
92
135
|
// server configuration problem, not a user error, so say what to change.
|
|
93
136
|
function reportPolicyModeError(err) {
|
|
@@ -102,18 +145,20 @@ function reportPolicyModeError(err) {
|
|
|
102
145
|
}
|
|
103
146
|
|
|
104
147
|
export default async function meshCli(argv = []) {
|
|
105
|
-
|
|
106
|
-
if (!args.length || args[0] === '--help' || args[0] === '-h') {
|
|
148
|
+
if (!argv.length || argv[0] === '--help' || argv[0] === '-h') {
|
|
107
149
|
printHelp();
|
|
108
150
|
return;
|
|
109
151
|
}
|
|
110
152
|
|
|
111
|
-
|
|
153
|
+
// parseLayerFlag returns { layer, remainingArgs }. Passing the object on
|
|
154
|
+
// meant `--global`/`--project` were silently ignored and left in the args.
|
|
155
|
+
const { layer, remainingArgs } = parseLayerFlag(argv);
|
|
156
|
+
const args = [...remainingArgs];
|
|
112
157
|
const brainDir = resolveBrainDir(layer);
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
const vaultRoot = path.join(brainDir, 'memory-vault');
|
|
158
|
+
// How to reach a machine is a fact about the machine, so node entities live
|
|
159
|
+
// in its global brain by default (see meshVaultRoot); an explicit layer flag
|
|
160
|
+
// still selects a brain for anyone who wants per-project records.
|
|
161
|
+
const vaultRoot = layer === 'auto' ? meshVaultRoot() : path.join(brainDir, 'memory-vault');
|
|
117
162
|
const command = args.shift();
|
|
118
163
|
|
|
119
164
|
// `--help` after a subcommand must never EXECUTE that subcommand. Only the
|
|
@@ -168,8 +213,14 @@ export default async function meshCli(argv = []) {
|
|
|
168
213
|
try {
|
|
169
214
|
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
215
|
const { execMeshCommand } = await import('../core/mesh.mjs');
|
|
171
|
-
|
|
216
|
+
// Cloud peers routinely need several seconds; a 4 s budget reported
|
|
217
|
+
// reachable machines as down.
|
|
218
|
+
const res = await execMeshCommand(node.hostname, probeCmd, { vaultRoot, timeoutMs: 15000 });
|
|
172
219
|
nodeInfo.reachable = res.success;
|
|
220
|
+
if (res.success && !node.access?.verified_at) {
|
|
221
|
+
// A login that just worked is the only kind worth sharing with peers.
|
|
222
|
+
await setMeshNodeAccess(node.hostname, { verified_at: new Date().toISOString() }, { vaultRoot });
|
|
223
|
+
}
|
|
173
224
|
if (res.success) {
|
|
174
225
|
const parts = res.stdout.split(/\s+/);
|
|
175
226
|
for (const part of parts) {
|
|
@@ -214,6 +265,10 @@ export default async function meshCli(argv = []) {
|
|
|
214
265
|
console.log(`│ ${pad(r.hostname, 40)} │ ${pad(r.ip, 14)} │ ${pad(roleStr, 16)} │ ${pad(sshStr, 16)} │ ${pad(hStr, 36)} │ ${pad(runStr, 24)} │`);
|
|
215
266
|
}
|
|
216
267
|
console.log('└──────────────────────────────────────────┴────────────────┴──────────────────┴──────────────────┴──────────────────────────────────────┴──────────────────────────┘\n');
|
|
268
|
+
for (const r of results.filter((x) => !x.self && !x.reachable)) {
|
|
269
|
+
console.log(` ${String(r.hostname).split('.')[0].padEnd(20)} ${explainUnreachable(r)}`);
|
|
270
|
+
}
|
|
271
|
+
if (results.some((x) => !x.self && !x.reachable)) console.log('');
|
|
217
272
|
return;
|
|
218
273
|
}
|
|
219
274
|
|
|
@@ -322,7 +377,7 @@ export default async function meshCli(argv = []) {
|
|
|
322
377
|
if (command === 'exec') {
|
|
323
378
|
const target = args.shift();
|
|
324
379
|
if (!target || !args.length) {
|
|
325
|
-
fail('`exec` requires a node name and a command.', 'Example: total-recall mesh exec
|
|
380
|
+
fail('`exec` requires a node name and a command.', 'Example: total-recall mesh exec build-box uptime');
|
|
326
381
|
return;
|
|
327
382
|
}
|
|
328
383
|
const jsonMode = args.includes('--json');
|
|
@@ -353,6 +408,57 @@ export default async function meshCli(argv = []) {
|
|
|
353
408
|
return;
|
|
354
409
|
}
|
|
355
410
|
|
|
411
|
+
if (target === 'discover') {
|
|
412
|
+
const { discoverNodeAccess } = await import('../core/mesh-discover.mjs');
|
|
413
|
+
const wanted = args.find((a) => !a.startsWith('-'));
|
|
414
|
+
const targets = wanted
|
|
415
|
+
? [findMeshNode(wanted, vaultRoot)].filter(Boolean)
|
|
416
|
+
: listEnrichedMeshNodes(vaultRoot).filter(
|
|
417
|
+
(n) => !n.self && n.ip && n.online !== false && !n.access?.verified_at,
|
|
418
|
+
);
|
|
419
|
+
if (!targets.length) {
|
|
420
|
+
console.log(wanted ? `No mesh node matches "${wanted}".` : 'Every online node already has a verified login.');
|
|
421
|
+
if (wanted) process.exitCode = 1;
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
let missing = 0;
|
|
425
|
+
for (const node of targets) {
|
|
426
|
+
const result = await discoverNodeAccess(node, { vaultRoot });
|
|
427
|
+
const name = String(node.hostname).padEnd(38);
|
|
428
|
+
if (result.found) {
|
|
429
|
+
const key = result.access.identity_file ? ` (key ${result.access.identity_file})` : '';
|
|
430
|
+
console.log(` ✅ ${name} ${result.access.ssh_user}${key}`);
|
|
431
|
+
} else {
|
|
432
|
+
missing += 1;
|
|
433
|
+
console.log(` ❌ ${name} ${describeDiscoveryFailure(result, node)}`);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
if (missing) process.exitCode = 1;
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (target === 'sync') {
|
|
441
|
+
const { syncAccessFromPeers } = await import('../core/mesh-access-sync.mjs');
|
|
442
|
+
const { peers, learned } = await syncAccessFromPeers({ vaultRoot });
|
|
443
|
+
for (const peer of peers) {
|
|
444
|
+
const state = peer.read ? `read ${peer.documents} node record(s)` : `skipped: ${peer.reason}`;
|
|
445
|
+
console.log(` ${String(peer.hostname).padEnd(38)} ${state}`);
|
|
446
|
+
}
|
|
447
|
+
console.log('');
|
|
448
|
+
if (!learned.length) {
|
|
449
|
+
console.log('Nothing new: every login a reachable peer knows is already recorded here.');
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
for (const item of learned) {
|
|
453
|
+
console.log(
|
|
454
|
+
` ${item.written ? '✅' : '⚠️ '} ${String(item.hostname).padEnd(38)} ${item.access.ssh_user}` +
|
|
455
|
+
` (from ${String(item.from).split('.')[0]})`,
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
if (learned.some((item) => !item.written)) process.exitCode = 1;
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
|
|
356
462
|
if (target === 'import') {
|
|
357
463
|
const nodes = listEnrichedMeshNodes(vaultRoot);
|
|
358
464
|
const proposals = proposeAccessFromSshConfig(nodes, readSshConfig());
|