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.
- package/bin/total-recall.mjs +31 -1
- package/docs/ARCHITECTURE.md +76 -9
- package/frontend/dist/assets/index-U7o-qU99.js +5742 -0
- package/frontend/dist/index.html +1 -1
- package/frontend/src/App.tsx +39 -1
- package/frontend/src/api/index.ts +1 -0
- package/frontend/src/api/plugins.ts +167 -0
- package/frontend/src/pages/ChatPage.tsx +44 -17
- package/frontend/src/pages/IntegrationsPage.tsx +7 -0
- package/frontend/src/pages/PluginsPage.spec.tsx +17 -0
- package/frontend/src/pages/PluginsPage.tsx +1331 -0
- package/package.json +1 -1
- package/scaffold/.agent/plugins/.gitkeep +0 -0
- package/scaffold/.agent/plugins/code-quality/README.md +16 -0
- package/scaffold/.agent/plugins/code-quality/cli.mjs +15 -0
- package/scaffold/.agent/plugins/code-quality/plugin.json +34 -0
- package/scaffold/.agent/plugins/git-sentinel/README.md +16 -0
- package/scaffold/.agent/plugins/git-sentinel/cli.mjs +86 -0
- package/scaffold/.agent/plugins/git-sentinel/plugin.json +34 -0
- package/scaffold/.agent/plugins/meta-harness/README.md +16 -0
- package/scaffold/.agent/plugins/meta-harness/cli.mjs +34 -0
- package/scaffold/.agent/plugins/meta-harness/plugin.json +34 -0
- package/scaffold/.agent/plugins/system-monitor/README.md +16 -0
- package/scaffold/.agent/plugins/system-monitor/cli.mjs +79 -0
- package/scaffold/.agent/plugins/system-monitor/generator.mjs +20 -0
- package/scaffold/.agent/plugins/system-monitor/plugin.json +38 -0
- package/scaffold/.agent/skills/meta-harness/SKILL.md +113 -0
- package/scaffold/.agent/skills/total-recall/references/cli-reference.md +23 -0
- package/src/cli/agent.mjs +173 -0
- package/src/cli/connect.mjs +89 -4
- package/src/cli/deploy-ui.mjs +6 -0
- package/src/cli/harness.mjs +135 -0
- package/src/cli/ingest-openwiki.mjs +71 -2
- package/src/cli/init.mjs +1 -0
- package/src/cli/lint.mjs +47 -3
- package/src/cli/mesh.mjs +286 -0
- package/src/cli/plugin/create.mjs +186 -0
- package/src/cli/plugin/index.mjs +139 -0
- package/src/cli/plugin/install.mjs +141 -0
- package/src/cli/plugin/list.mjs +83 -0
- package/src/cli/plugin/remove.mjs +51 -0
- package/src/cli/plugin/search.mjs +37 -0
- package/src/cli/skill-projection.mjs +14 -0
- package/src/cli/wizard.html +9 -0
- package/src/core/agent-manager.mjs +290 -0
- package/src/core/cache-prune.mjs +341 -0
- package/src/core/context-cache.mjs +130 -0
- package/src/core/daemon-loop.mjs +47 -1
- package/src/core/evolving-context.mjs +171 -0
- package/src/core/mesh-activity.mjs +140 -0
- package/src/core/mesh.mjs +174 -1
- package/src/core/meta-harness.mjs +251 -0
- package/src/core/plugin-context.mjs +206 -0
- package/src/core/plugin-loader.mjs +210 -0
- package/src/core/runtime.mjs +65 -13
- package/src/core/surface.mjs +32 -10
- package/src/server/api.mjs +18 -2
- package/src/server/index.mjs +8 -2
- package/src/server/rest.mjs +4 -0
- package/src/server/routes/integrations.mjs +5 -1
- package/src/server/routes/mesh-presence.mjs +143 -0
- package/src/server/routes/models.mjs +42 -30
- package/src/server/routes/plugins.mjs +517 -0
- package/src/server/tools.mjs +266 -0
- package/frontend/dist/assets/index-Bl3I11B0.js +0 -5742
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { validatePluginManifest } from '../../core/plugin-loader.mjs';
|
|
6
|
+
import { CURATED_CATALOG } from '../../server/routes/plugins.mjs';
|
|
7
|
+
|
|
8
|
+
export async function installPlugin(args = []) {
|
|
9
|
+
const isGlobal = args.includes('--global') || args.includes('-g');
|
|
10
|
+
const isLink = args.includes('--link') || args.includes('-l');
|
|
11
|
+
const cleanArgs = args.filter(a => a !== '--global' && a !== '-g' && a !== '--link' && a !== '-l');
|
|
12
|
+
|
|
13
|
+
let source = cleanArgs[0];
|
|
14
|
+
if (source) {
|
|
15
|
+
const matched = CURATED_CATALOG.find(p => p.id.toLowerCase() === source.toLowerCase());
|
|
16
|
+
if (matched) {
|
|
17
|
+
console.log(`📦 Resolving plugin '${matched.id}' from catalog: ${matched.sourceUrl}`);
|
|
18
|
+
source = matched.sourceUrl;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!source) {
|
|
23
|
+
console.error('❌ Error: Missing plugin source path or git URL.');
|
|
24
|
+
console.error(' Usage: total-recall plugin install <path|git-url> [--link] [--global]\n');
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const pluginsBaseDir = isGlobal
|
|
29
|
+
? path.join(os.homedir(), '.agent', 'plugins')
|
|
30
|
+
: path.join(process.cwd(), '.agent', 'plugins');
|
|
31
|
+
|
|
32
|
+
if (!fs.existsSync(pluginsBaseDir)) {
|
|
33
|
+
fs.mkdirSync(pluginsBaseDir, { recursive: true });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const isGitUrl = source.startsWith('http://') ||
|
|
37
|
+
source.startsWith('https://') ||
|
|
38
|
+
source.startsWith('git@') ||
|
|
39
|
+
source.endsWith('.git');
|
|
40
|
+
|
|
41
|
+
if (isGitUrl) {
|
|
42
|
+
console.log(`Cloning plugin from ${source}...`);
|
|
43
|
+
const tempDir = path.join(os.tmpdir(), `tr-plugin-${Date.now()}`);
|
|
44
|
+
const cloneRes = spawnSync('git', ['clone', '--depth', '1', source, tempDir], {
|
|
45
|
+
stdio: 'inherit',
|
|
46
|
+
encoding: 'utf8'
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
if (cloneRes.status !== 0) {
|
|
50
|
+
console.error(`❌ Failed to clone repository from ${source}`);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const manifestPath = path.join(tempDir, 'plugin.json');
|
|
55
|
+
if (!fs.existsSync(manifestPath)) {
|
|
56
|
+
console.error(`❌ Cloned repository does not contain a plugin.json manifest`);
|
|
57
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let manifest;
|
|
62
|
+
try {
|
|
63
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
64
|
+
} catch (err) {
|
|
65
|
+
console.error(`❌ Failed to parse plugin.json: ${err.message}`);
|
|
66
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const validation = validatePluginManifest(manifest);
|
|
71
|
+
if (!validation.valid) {
|
|
72
|
+
console.error(`❌ Invalid plugin manifest:`);
|
|
73
|
+
for (const e of validation.errors) {
|
|
74
|
+
console.error(` - ${e}`);
|
|
75
|
+
}
|
|
76
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const destDir = path.join(pluginsBaseDir, manifest.id);
|
|
81
|
+
if (fs.existsSync(destDir)) {
|
|
82
|
+
console.error(`❌ Plugin '${manifest.id}' already exists at ${destDir}. Remove it first.`);
|
|
83
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
fs.cpSync(tempDir, destDir, { recursive: true });
|
|
88
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
89
|
+
|
|
90
|
+
console.log(`\n✅ Successfully installed plugin '${manifest.name}' (id: ${manifest.id}, v${manifest.version})`);
|
|
91
|
+
console.log(` Location: ${destDir}\n`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Local directory install / link
|
|
96
|
+
const absSource = path.resolve(process.cwd(), source);
|
|
97
|
+
if (!fs.existsSync(absSource)) {
|
|
98
|
+
console.error(`❌ Source directory not found: ${absSource}`);
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const manifestPath = path.join(absSource, 'plugin.json');
|
|
103
|
+
if (!fs.existsSync(manifestPath)) {
|
|
104
|
+
console.error(`❌ Directory '${absSource}' does not contain a plugin.json manifest`);
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let manifest;
|
|
109
|
+
try {
|
|
110
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
111
|
+
} catch (err) {
|
|
112
|
+
console.error(`❌ Failed to parse plugin.json: ${err.message}`);
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const validation = validatePluginManifest(manifest);
|
|
117
|
+
if (!validation.valid) {
|
|
118
|
+
console.error(`❌ Invalid plugin manifest:`);
|
|
119
|
+
for (const e of validation.errors) {
|
|
120
|
+
console.error(` - ${e}`);
|
|
121
|
+
}
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const destDir = path.join(pluginsBaseDir, manifest.id);
|
|
126
|
+
|
|
127
|
+
if (fs.existsSync(destDir)) {
|
|
128
|
+
console.error(`❌ Plugin '${manifest.id}' is already installed at ${destDir}. Remove it first.`);
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (isLink) {
|
|
133
|
+
fs.symlinkSync(absSource, destDir, 'junction');
|
|
134
|
+
console.log(`\n✅ Linked plugin '${manifest.name}' (id: ${manifest.id}, v${manifest.version})`);
|
|
135
|
+
console.log(` Symlink: ${destDir} -> ${absSource}\n`);
|
|
136
|
+
} else {
|
|
137
|
+
fs.cpSync(absSource, destDir, { recursive: true });
|
|
138
|
+
console.log(`\n✅ Copied plugin '${manifest.name}' (id: ${manifest.id}, v${manifest.version})`);
|
|
139
|
+
console.log(` Location: ${destDir}\n`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { discoverPlugins } from '../../core/plugin-loader.mjs';
|
|
2
|
+
|
|
3
|
+
function stripAnsi(str) {
|
|
4
|
+
return String(str || '').replace(/\x1b\[[0-9;]*m/g, '');
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function padAnsi(str, targetLen) {
|
|
8
|
+
const visibleLen = stripAnsi(str).length;
|
|
9
|
+
const paddingNeeded = Math.max(0, targetLen - visibleLen);
|
|
10
|
+
return String(str || '') + ' '.repeat(paddingNeeded);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function listPlugins(args = []) {
|
|
14
|
+
const isJson = args.includes('--json');
|
|
15
|
+
const projectRoot = process.cwd();
|
|
16
|
+
const plugins = discoverPlugins(projectRoot);
|
|
17
|
+
|
|
18
|
+
if (isJson) {
|
|
19
|
+
console.log(JSON.stringify(plugins, null, 2));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
console.log(`\n🔌 Total Recall — Installed Plugins\n`);
|
|
24
|
+
|
|
25
|
+
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');
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
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)}┘`;
|
|
40
|
+
|
|
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);
|
|
50
|
+
|
|
51
|
+
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
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
console.log(botBorder);
|
|
69
|
+
|
|
70
|
+
// Show detailed validation errors if any
|
|
71
|
+
const invalid = plugins.filter(p => !p.valid);
|
|
72
|
+
if (invalid.length > 0) {
|
|
73
|
+
console.log(`\n⚠️ Configuration Warnings:`);
|
|
74
|
+
for (const inv of invalid) {
|
|
75
|
+
console.log(` Plugin '${inv.id}':`);
|
|
76
|
+
for (const err of inv.errors) {
|
|
77
|
+
console.log(` - ${err}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
console.log();
|
|
83
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
|
|
5
|
+
export async function removePlugin(args = []) {
|
|
6
|
+
const isGlobal = args.includes('--global') || args.includes('-g');
|
|
7
|
+
const cleanArgs = args.filter(a => a !== '--global' && a !== '-g');
|
|
8
|
+
|
|
9
|
+
const id = cleanArgs[0];
|
|
10
|
+
|
|
11
|
+
if (!id) {
|
|
12
|
+
console.error('❌ Error: Missing plugin ID to remove.');
|
|
13
|
+
console.error(' Usage: total-recall plugin remove <id> [--global]\n');
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
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
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!targetPath) {
|
|
34
|
+
console.error(`❌ Plugin '${id}' was not found in installed plugin directories.`);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
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`);
|
|
43
|
+
} else {
|
|
44
|
+
fs.rmSync(targetPath, { recursive: true, force: true });
|
|
45
|
+
console.log(`\n✅ Removed plugin '${id}' from ${targetPath}\n`);
|
|
46
|
+
}
|
|
47
|
+
} catch (err) {
|
|
48
|
+
console.error(`❌ Failed to remove plugin '${id}': ${err.message}`);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { CURATED_CATALOG } from "../../server/routes/plugins.mjs";
|
|
2
|
+
|
|
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
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (isJson) {
|
|
19
|
+
console.log(JSON.stringify(results, null, 2));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
console.log("\n🔌 Total Recall — Plugin Catalog & Discovery\n");
|
|
24
|
+
if (results.length === 0) {
|
|
25
|
+
console.log("No plugins found matching \"" + query + "\".\n");
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
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");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -58,6 +58,20 @@ export const IDE_SKILLS = [
|
|
|
58
58
|
// project-local dir, so project skills are opt-in (namespaced) via connect.
|
|
59
59
|
project: null,
|
|
60
60
|
global: { dir: ['.codex', 'skills'], markers: ['.codex'] }
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: 'hermes',
|
|
64
|
+
label: 'Hermes Agent',
|
|
65
|
+
clients: ['hermes', 'hermes-agent'],
|
|
66
|
+
project: { dir: ['.hermes', 'skills'], markers: ['.hermes'], env: ['HERMES_AGENT', 'HERMES_HOME'] },
|
|
67
|
+
global: { dir: ['.hermes', 'skills'], markers: ['.hermes'] }
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: 'dsh',
|
|
71
|
+
label: 'DeepSeek Harness',
|
|
72
|
+
clients: ['dsh', 'deepseek-harness', 'deepseek'],
|
|
73
|
+
project: { dir: ['.agents', 'skills'], markers: ['.dsh', '.agents'], env: ['DSH_AGENT', 'DEEPSEEK_HARNESS'] },
|
|
74
|
+
global: { dir: ['.dsh', 'skills'], markers: ['.dsh'] }
|
|
61
75
|
}
|
|
62
76
|
];
|
|
63
77
|
|
package/src/cli/wizard.html
CHANGED
|
@@ -1188,6 +1188,15 @@
|
|
|
1188
1188
|
</div>
|
|
1189
1189
|
<span class="ide-check">✓</span>
|
|
1190
1190
|
</label>
|
|
1191
|
+
<label class="ide-card" id="ide-dsh">
|
|
1192
|
+
<input type="checkbox" value="dsh" onchange="toggleIde(this)">
|
|
1193
|
+
<span class="ide-icon">⚡</span>
|
|
1194
|
+
<div>
|
|
1195
|
+
<div class="ide-name">DeepSeek Harness (dsh)</div>
|
|
1196
|
+
<div class="ide-desc">DeepSeek AI — AGENTS.md + ~/.dsh/memory/MEMORY.md</div>
|
|
1197
|
+
</div>
|
|
1198
|
+
<span class="ide-check">✓</span>
|
|
1199
|
+
</label>
|
|
1191
1200
|
<label class="ide-card" id="ide-openclaw">
|
|
1192
1201
|
<input type="checkbox" value="openclaw" onchange="toggleIde(this)">
|
|
1193
1202
|
<span class="ide-icon">🦀</span>
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import { HARNESS_SPECS, detectHarnesses } from './meta-harness.mjs';
|
|
6
|
+
import { findBinaryInPath } from './runtime.mjs';
|
|
7
|
+
import { logger } from './logger.mjs';
|
|
8
|
+
|
|
9
|
+
function resolveStateDir() {
|
|
10
|
+
const stateDir = path.join(os.homedir(), '.agent', 'state');
|
|
11
|
+
if (!fs.existsSync(stateDir)) {
|
|
12
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
13
|
+
}
|
|
14
|
+
return stateDir;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function resolveLogDir() {
|
|
18
|
+
const logDir = path.join(os.homedir(), '.agent', 'logs', 'agents');
|
|
19
|
+
if (!fs.existsSync(logDir)) {
|
|
20
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
return logDir;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getStateFilePath() {
|
|
26
|
+
return path.join(resolveStateDir(), 'active-agents.json');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function loadAgentState() {
|
|
30
|
+
const stateFile = getStateFilePath();
|
|
31
|
+
if (!fs.existsSync(stateFile)) return [];
|
|
32
|
+
try {
|
|
33
|
+
const raw = fs.readFileSync(stateFile, 'utf8');
|
|
34
|
+
const list = JSON.parse(raw);
|
|
35
|
+
return Array.isArray(list) ? list : [];
|
|
36
|
+
} catch {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function saveAgentState(list) {
|
|
42
|
+
const stateFile = getStateFilePath();
|
|
43
|
+
try {
|
|
44
|
+
fs.writeFileSync(stateFile, JSON.stringify(list, null, 2), 'utf8');
|
|
45
|
+
} catch (err) {
|
|
46
|
+
logger.error('Failed to save agent state:', err.message);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function isProcessRunning(pid) {
|
|
51
|
+
if (!pid) return false;
|
|
52
|
+
try {
|
|
53
|
+
process.kill(pid, 0);
|
|
54
|
+
return true;
|
|
55
|
+
} catch (e) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* List all active/recent spawned agents, pruning defunct processes.
|
|
62
|
+
*/
|
|
63
|
+
export function listAgents() {
|
|
64
|
+
const agents = loadAgentState();
|
|
65
|
+
const updated = [];
|
|
66
|
+
|
|
67
|
+
for (const a of agents) {
|
|
68
|
+
if (a.remote) {
|
|
69
|
+
updated.push(a);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const alive = isProcessRunning(a.pid);
|
|
73
|
+
if (alive) {
|
|
74
|
+
updated.push({ ...a, status: 'running' });
|
|
75
|
+
} else {
|
|
76
|
+
updated.push({ ...a, status: a.status === 'running' ? 'stopped' : a.status });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
saveAgentState(updated);
|
|
81
|
+
return updated;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Spawn an agent harness headlessly or in background detach mode.
|
|
86
|
+
*/
|
|
87
|
+
export async function spawnAgent(harnessId, taskPrompt, options = {}) {
|
|
88
|
+
const spec = HARNESS_SPECS[harnessId];
|
|
89
|
+
if (!spec) {
|
|
90
|
+
throw new Error(`Unknown harness ID "${harnessId}". Available: ${Object.keys(HARNESS_SPECS).join(', ')}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Remote mesh execution branch
|
|
94
|
+
if (options.node) {
|
|
95
|
+
const { findMeshNode, getMeshSelf, execMeshCommand } = await import('./mesh.mjs');
|
|
96
|
+
const targetNode = findMeshNode(options.node, options.vaultRoot);
|
|
97
|
+
if (!targetNode) {
|
|
98
|
+
throw new Error(`Target mesh node "${options.node}" not found in mesh topology.`);
|
|
99
|
+
}
|
|
100
|
+
const selfNode = getMeshSelf();
|
|
101
|
+
const isSelf = targetNode.self || (selfNode && targetNode.ip === selfNode.ip);
|
|
102
|
+
|
|
103
|
+
if (!isSelf) {
|
|
104
|
+
const id = `agent-${harnessId}-${Date.now().toString(36)}`;
|
|
105
|
+
const remoteCmd = `total-recall agent spawn ${harnessId} ${JSON.stringify(taskPrompt)} ${options.name ? `--name ${JSON.stringify(options.name)}` : ''} --json`;
|
|
106
|
+
const execResult = await execMeshCommand(options.node, remoteCmd, {
|
|
107
|
+
vaultRoot: options.vaultRoot,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
let remoteRecord = null;
|
|
111
|
+
try {
|
|
112
|
+
const jsonMatch = execResult.stdout.match(/\{[\s\S]*\}/);
|
|
113
|
+
if (jsonMatch) remoteRecord = JSON.parse(jsonMatch[0]);
|
|
114
|
+
} catch {}
|
|
115
|
+
|
|
116
|
+
const record = {
|
|
117
|
+
...(remoteRecord || {}),
|
|
118
|
+
id: remoteRecord?.id || id,
|
|
119
|
+
node: options.node,
|
|
120
|
+
remote: true,
|
|
121
|
+
status: execResult.success ? 'running' : 'failed',
|
|
122
|
+
startedAt: remoteRecord?.startedAt || new Date().toISOString(),
|
|
123
|
+
task: taskPrompt,
|
|
124
|
+
harness: harnessId,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const current = loadAgentState();
|
|
128
|
+
current.unshift(record);
|
|
129
|
+
saveAgentState(current);
|
|
130
|
+
return record;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const binPath = findBinaryInPath(spec.binary);
|
|
135
|
+
if (!binPath) {
|
|
136
|
+
throw new Error(`Binary "${spec.binary}" for harness "${spec.name}" not found on PATH.`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const id = `agent-${harnessId}-${Date.now().toString(36)}`;
|
|
140
|
+
const logDir = resolveLogDir();
|
|
141
|
+
const logFile = path.join(logDir, `${id}.log`);
|
|
142
|
+
const logFd = fs.openSync(logFile, 'a');
|
|
143
|
+
|
|
144
|
+
const args = [...spec.defaultFlags, taskPrompt];
|
|
145
|
+
const cwd = options.cwd || process.cwd();
|
|
146
|
+
|
|
147
|
+
const child = spawn(binPath, args, {
|
|
148
|
+
cwd,
|
|
149
|
+
detached: options.detach !== false,
|
|
150
|
+
stdio: options.detach !== false ? ['ignore', logFd, logFd] : 'pipe',
|
|
151
|
+
env: {
|
|
152
|
+
...process.env,
|
|
153
|
+
TR_AGENT_ID: id,
|
|
154
|
+
TR_HARNESS: harnessId
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const record = {
|
|
159
|
+
id,
|
|
160
|
+
name: options.name || `${spec.name} Worker`,
|
|
161
|
+
harness: harnessId,
|
|
162
|
+
binary: binPath,
|
|
163
|
+
pid: child.pid,
|
|
164
|
+
task: taskPrompt,
|
|
165
|
+
cwd,
|
|
166
|
+
logFile,
|
|
167
|
+
startedAt: new Date().toISOString(),
|
|
168
|
+
status: 'running',
|
|
169
|
+
detached: options.detach !== false
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const current = loadAgentState();
|
|
173
|
+
current.unshift(record);
|
|
174
|
+
saveAgentState(current);
|
|
175
|
+
|
|
176
|
+
if (options.detach !== false) {
|
|
177
|
+
child.unref();
|
|
178
|
+
return record;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Foreground execution
|
|
182
|
+
return new Promise((resolve, reject) => {
|
|
183
|
+
let output = '';
|
|
184
|
+
let errOutput = '';
|
|
185
|
+
|
|
186
|
+
child.stdout.on('data', (chunk) => {
|
|
187
|
+
output += chunk.toString();
|
|
188
|
+
fs.writeSync(logFd, chunk);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
child.stderr.on('data', (chunk) => {
|
|
192
|
+
errOutput += chunk.toString();
|
|
193
|
+
fs.writeSync(logFd, chunk);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
child.on('close', (code) => {
|
|
197
|
+
fs.closeSync(logFd);
|
|
198
|
+
const list = loadAgentState();
|
|
199
|
+
const item = list.find(x => x.id === id);
|
|
200
|
+
if (item) {
|
|
201
|
+
item.status = code === 0 ? 'completed' : 'failed';
|
|
202
|
+
item.exitCode = code;
|
|
203
|
+
item.endedAt = new Date().toISOString();
|
|
204
|
+
saveAgentState(list);
|
|
205
|
+
}
|
|
206
|
+
resolve({
|
|
207
|
+
...record,
|
|
208
|
+
status: code === 0 ? 'completed' : 'failed',
|
|
209
|
+
exitCode: code,
|
|
210
|
+
output: output.trim(),
|
|
211
|
+
stderr: errOutput.trim()
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
child.on('error', (err) => {
|
|
216
|
+
try { fs.closeSync(logFd); } catch {}
|
|
217
|
+
reject(err);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Kill a running agent by ID or PID.
|
|
224
|
+
*/
|
|
225
|
+
export async function killAgent(idOrPid) {
|
|
226
|
+
const list = loadAgentState();
|
|
227
|
+
const target = list.find(a => a.id === idOrPid || String(a.pid) === String(idOrPid));
|
|
228
|
+
|
|
229
|
+
if (!target) {
|
|
230
|
+
throw new Error(`Agent "${idOrPid}" not found in active agent registry.`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (target.remote && target.node) {
|
|
234
|
+
const { execMeshCommand } = await import('./mesh.mjs');
|
|
235
|
+
const remoteCmd = `total-recall agent kill ${target.id}`;
|
|
236
|
+
await execMeshCommand(target.node, remoteCmd);
|
|
237
|
+
target.status = 'killed';
|
|
238
|
+
target.endedAt = new Date().toISOString();
|
|
239
|
+
saveAgentState(list);
|
|
240
|
+
return { success: true, message: `Terminated remote agent ${target.id} on mesh node "${target.node}"` };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (isProcessRunning(target.pid)) {
|
|
244
|
+
try {
|
|
245
|
+
process.kill(target.pid, 'SIGTERM');
|
|
246
|
+
target.status = 'killed';
|
|
247
|
+
target.endedAt = new Date().toISOString();
|
|
248
|
+
saveAgentState(list);
|
|
249
|
+
return { success: true, message: `Terminated agent ${target.id} (PID ${target.pid})` };
|
|
250
|
+
} catch (err) {
|
|
251
|
+
throw new Error(`Failed to kill process ${target.pid}: ${err.message}`);
|
|
252
|
+
}
|
|
253
|
+
} else {
|
|
254
|
+
target.status = 'stopped';
|
|
255
|
+
saveAgentState(list);
|
|
256
|
+
return { success: true, message: `Agent ${target.id} was already stopped.` };
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Get recent log output for an agent.
|
|
262
|
+
*/
|
|
263
|
+
export async function getAgentLogs(idOrPid, tailLines = 50) {
|
|
264
|
+
const list = loadAgentState();
|
|
265
|
+
const target = list.find(a => a.id === idOrPid || String(a.pid) === String(idOrPid));
|
|
266
|
+
|
|
267
|
+
if (!target) {
|
|
268
|
+
throw new Error(`Agent "${idOrPid}" not found.`);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (target.remote && target.node) {
|
|
272
|
+
const { execMeshCommand } = await import('./mesh.mjs');
|
|
273
|
+
const remoteCmd = `total-recall agent logs ${target.id} --tail ${tailLines}`;
|
|
274
|
+
const res = await execMeshCommand(target.node, remoteCmd);
|
|
275
|
+
return res.stdout || res.stderr || '(No logs returned from remote node)';
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (!fs.existsSync(target.logFile)) {
|
|
279
|
+
return '(No log file found for this agent)';
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
try {
|
|
283
|
+
const raw = fs.readFileSync(target.logFile, 'utf8');
|
|
284
|
+
const lines = raw.split('\n');
|
|
285
|
+
return lines.slice(-tailLines).join('\n');
|
|
286
|
+
} catch (err) {
|
|
287
|
+
return `Error reading logs: ${err.message}`;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|