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,517 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
import { requireAuth, requireScope } from "../auth.mjs";
|
|
7
|
+
import { discoverPlugins, getPluginById, validatePluginManifest } from "../../core/plugin-loader.mjs";
|
|
8
|
+
import { serverError, badRequest } from "./_shared.mjs";
|
|
9
|
+
|
|
10
|
+
const router = Router();
|
|
11
|
+
|
|
12
|
+
function getRatingsFilePath(projectRoot) {
|
|
13
|
+
const root = projectRoot || process.cwd();
|
|
14
|
+
return path.join(root, ".agent", "config", "plugin-ratings.json");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function loadRatings(projectRoot) {
|
|
18
|
+
const filePath = getRatingsFilePath(projectRoot);
|
|
19
|
+
if (!fs.existsSync(filePath)) return {};
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
22
|
+
} catch {
|
|
23
|
+
return {};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function saveRatings(projectRoot, ratings) {
|
|
28
|
+
const filePath = getRatingsFilePath(projectRoot);
|
|
29
|
+
const dir = path.dirname(filePath);
|
|
30
|
+
if (!fs.existsSync(dir)) {
|
|
31
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
32
|
+
}
|
|
33
|
+
fs.writeFileSync(filePath, JSON.stringify(ratings, null, 2), "utf8");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const CURATED_CATALOG = [
|
|
37
|
+
{
|
|
38
|
+
id: "scientific-frontiers",
|
|
39
|
+
name: "Scientific Frontiers Engine",
|
|
40
|
+
version: "1.0.0",
|
|
41
|
+
description: "Continuous scientific capability intelligence, curiosity-driven research graph weaver, and benchmark ledger.",
|
|
42
|
+
tags: ["science", "benchmarks", "curiosity", "graph"],
|
|
43
|
+
author: "Total Recall Ecosystem",
|
|
44
|
+
sourceUrl: "https://github.com/gregiteen/scientific-frontiers-engine.git",
|
|
45
|
+
rating: 4.9,
|
|
46
|
+
reviewCount: 142,
|
|
47
|
+
installCount: "3.2k",
|
|
48
|
+
verified: true,
|
|
49
|
+
isInstalled: false
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: "meta-harness",
|
|
53
|
+
name: "Meta Harness & Agent Orchestrator",
|
|
54
|
+
version: "1.0.0",
|
|
55
|
+
description: "Orchestrates and delegates tasks across connected AI harnesses (Antigravity, Claude Code, Codex, Ollama).",
|
|
56
|
+
tags: ["agents", "multi-harness", "mesh", "delegation"],
|
|
57
|
+
author: "Total Recall Ecosystem",
|
|
58
|
+
sourceUrl: "./.agent/plugins/meta-harness",
|
|
59
|
+
rating: 4.8,
|
|
60
|
+
reviewCount: 98,
|
|
61
|
+
installCount: "2.4k",
|
|
62
|
+
verified: true,
|
|
63
|
+
isInstalled: false
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: "code-quality",
|
|
67
|
+
name: "Code Quality & SSSS Conformance",
|
|
68
|
+
version: "1.2.0",
|
|
69
|
+
description: "Local-first code quality gates, SSSS schema verification, and invariant enforcement.",
|
|
70
|
+
tags: ["quality", "conformance", "gates", "testing"],
|
|
71
|
+
author: "Total Recall Ecosystem",
|
|
72
|
+
sourceUrl: "./.agent/plugins/code-quality",
|
|
73
|
+
rating: 5.0,
|
|
74
|
+
reviewCount: 310,
|
|
75
|
+
installCount: "8.2k",
|
|
76
|
+
verified: true,
|
|
77
|
+
isInstalled: false
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: "system-monitor",
|
|
81
|
+
name: "System Monitor & Telemetry",
|
|
82
|
+
version: "1.0.0",
|
|
83
|
+
description: "Real-time host resource metrics, memory watchdog, and dynamic system telemetry context generator.",
|
|
84
|
+
tags: ["telemetry", "metrics", "monitor", "watchdog"],
|
|
85
|
+
author: "Total Recall Ecosystem",
|
|
86
|
+
sourceUrl: "./.agent/plugins/system-monitor",
|
|
87
|
+
rating: 4.9,
|
|
88
|
+
reviewCount: 56,
|
|
89
|
+
installCount: "1.5k",
|
|
90
|
+
verified: true,
|
|
91
|
+
isInstalled: false
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: "git-sentinel",
|
|
95
|
+
name: "Git Sentinel & Repo Watchdog",
|
|
96
|
+
version: "1.0.0",
|
|
97
|
+
description: "Continuous repository auditor checking dirty worktrees, unpushed commits, and stale branches.",
|
|
98
|
+
tags: ["git", "sentinel", "audit", "vcs"],
|
|
99
|
+
author: "Total Recall Ecosystem",
|
|
100
|
+
sourceUrl: "./.agent/plugins/git-sentinel",
|
|
101
|
+
rating: 4.8,
|
|
102
|
+
reviewCount: 73,
|
|
103
|
+
installCount: "2.1k",
|
|
104
|
+
verified: true,
|
|
105
|
+
isInstalled: false
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
id: "chrome-devtools",
|
|
109
|
+
name: "Chrome DevTools Automation",
|
|
110
|
+
version: "1.0.0",
|
|
111
|
+
description: "Browser performance auditing, Core Web Vitals profiling, and accessibility testing tools.",
|
|
112
|
+
tags: ["browser", "devtools", "testing", "mcp"],
|
|
113
|
+
author: "Modern Web Guidance",
|
|
114
|
+
sourceUrl: "https://github.com/total-recall-plugins/chrome-devtools.git",
|
|
115
|
+
rating: 4.7,
|
|
116
|
+
reviewCount: 84,
|
|
117
|
+
installCount: "1.8k",
|
|
118
|
+
verified: true,
|
|
119
|
+
isInstalled: false
|
|
120
|
+
}
|
|
121
|
+
];
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* GET /api/plugins
|
|
125
|
+
* Returns list of installed plugins with their manifests, validation status, ratings, and capabilities.
|
|
126
|
+
*/
|
|
127
|
+
router.get("/api/plugins", requireAuth, requireScope("config:read"), (req, res) => {
|
|
128
|
+
try {
|
|
129
|
+
const projectRoot = req.query?.root || process.cwd();
|
|
130
|
+
const plugins = discoverPlugins(projectRoot);
|
|
131
|
+
const userRatings = loadRatings(projectRoot);
|
|
132
|
+
|
|
133
|
+
res.json({
|
|
134
|
+
success: true,
|
|
135
|
+
count: plugins.length,
|
|
136
|
+
plugins: plugins.map((p) => {
|
|
137
|
+
const userReview = userRatings[p.id] || null;
|
|
138
|
+
const catalogMatch = CURATED_CATALOG.find((c) => c.id === p.id);
|
|
139
|
+
const baseRating = catalogMatch?.rating || 4.8;
|
|
140
|
+
const reviewCount = catalogMatch?.reviewCount || 12;
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
id: p.id,
|
|
144
|
+
name: p.manifest?.name || p.id,
|
|
145
|
+
version: p.manifest?.version || "0.0.0",
|
|
146
|
+
description: p.manifest?.description || "",
|
|
147
|
+
valid: p.valid,
|
|
148
|
+
errors: p.errors,
|
|
149
|
+
dir: p.dir,
|
|
150
|
+
rating: baseRating,
|
|
151
|
+
reviewCount: userReview ? reviewCount + 1 : reviewCount,
|
|
152
|
+
installCount: catalogMatch?.installCount || "1.0k",
|
|
153
|
+
userRating: userReview?.rating || null,
|
|
154
|
+
userReview: userReview?.review || null,
|
|
155
|
+
categories: p.manifest?.ssss_schemas?.categories || [],
|
|
156
|
+
tasks: p.manifest?.tasks || [],
|
|
157
|
+
openwiki_hubs: p.manifest?.openwiki_hubs || [],
|
|
158
|
+
tools: p.manifest?.tools || [],
|
|
159
|
+
cli: p.manifest?.cli || null
|
|
160
|
+
};
|
|
161
|
+
})
|
|
162
|
+
});
|
|
163
|
+
} catch (err) {
|
|
164
|
+
serverError(res, err);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* GET /api/plugins/catalog
|
|
170
|
+
* Returns curated discoverable plugins with ratings.
|
|
171
|
+
*/
|
|
172
|
+
router.get("/api/plugins/catalog", requireAuth, requireScope("config:read"), (req, res) => {
|
|
173
|
+
try {
|
|
174
|
+
const projectRoot = req.query?.root || process.cwd();
|
|
175
|
+
const installed = new Set(discoverPlugins(projectRoot).map((p) => p.id));
|
|
176
|
+
const userRatings = loadRatings(projectRoot);
|
|
177
|
+
|
|
178
|
+
const catalogWithStatus = CURATED_CATALOG.map((item) => ({
|
|
179
|
+
...item,
|
|
180
|
+
isInstalled: installed.has(item.id),
|
|
181
|
+
userRating: userRatings[item.id]?.rating || null
|
|
182
|
+
}));
|
|
183
|
+
res.json({
|
|
184
|
+
success: true,
|
|
185
|
+
catalog: catalogWithStatus
|
|
186
|
+
});
|
|
187
|
+
} catch (err) {
|
|
188
|
+
serverError(res, err);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* POST /api/plugins/:id/rate
|
|
194
|
+
* Submit or update a rating and review for an installed plugin.
|
|
195
|
+
*/
|
|
196
|
+
router.post("/api/plugins/:id/rate", requireAuth, requireScope("config:write"), (req, res) => {
|
|
197
|
+
try {
|
|
198
|
+
const id = req.params.id;
|
|
199
|
+
const { rating, review = "" } = req.body || {};
|
|
200
|
+
const numRating = Number(rating);
|
|
201
|
+
|
|
202
|
+
if (isNaN(numRating) || numRating < 1 || numRating > 5) {
|
|
203
|
+
return badRequest(res, "Rating must be a number between 1 and 5");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const projectRoot = req.body?.projectRoot || process.cwd();
|
|
207
|
+
const ratings = loadRatings(projectRoot);
|
|
208
|
+
|
|
209
|
+
ratings[id] = {
|
|
210
|
+
rating: Math.round(numRating * 10) / 10,
|
|
211
|
+
review: typeof review === "string" ? review.trim() : "",
|
|
212
|
+
updatedAt: new Date().toISOString()
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
saveRatings(projectRoot, ratings);
|
|
216
|
+
|
|
217
|
+
res.json({
|
|
218
|
+
success: true,
|
|
219
|
+
message: "Rating saved for " + id,
|
|
220
|
+
pluginId: id,
|
|
221
|
+
userRating: ratings[id]
|
|
222
|
+
});
|
|
223
|
+
} catch (err) {
|
|
224
|
+
serverError(res, err);
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* POST /api/plugins/install
|
|
230
|
+
* Installs or links a plugin from a local directory or git repository.
|
|
231
|
+
*/
|
|
232
|
+
router.post("/api/plugins/install", requireAuth, requireScope("config:write"), (req, res) => {
|
|
233
|
+
try {
|
|
234
|
+
let { source, link = false, global: isGlobal = false, projectRoot } = req.body || {};
|
|
235
|
+
if (!source || typeof source !== "string") {
|
|
236
|
+
return badRequest(res, "Missing source path or git URL");
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const matchedCatalog = CURATED_CATALOG.find((p) => p.id.toLowerCase() === source.toLowerCase());
|
|
240
|
+
if (matchedCatalog) {
|
|
241
|
+
source = matchedCatalog.sourceUrl;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const root = projectRoot || process.cwd();
|
|
245
|
+
const pluginsBaseDir = isGlobal
|
|
246
|
+
? path.join(os.homedir(), ".agent", "plugins")
|
|
247
|
+
: path.join(root, ".agent", "plugins");
|
|
248
|
+
|
|
249
|
+
if (!fs.existsSync(pluginsBaseDir)) {
|
|
250
|
+
fs.mkdirSync(pluginsBaseDir, { recursive: true });
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const isGitUrl = source.startsWith("http://") ||
|
|
254
|
+
source.startsWith("https://") ||
|
|
255
|
+
source.startsWith("git@") ||
|
|
256
|
+
source.endsWith(".git");
|
|
257
|
+
|
|
258
|
+
if (isGitUrl) {
|
|
259
|
+
const tempDir = path.join(os.tmpdir(), "tr-plugin-" + Date.now());
|
|
260
|
+
const cloneRes = spawnSync("git", ["clone", "--depth", "1", source, tempDir], {
|
|
261
|
+
encoding: "utf8"
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
if (cloneRes.status !== 0) {
|
|
265
|
+
return badRequest(res, "Failed to clone repository: " + (cloneRes.stderr || "git clone failed"));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const manifestPath = path.join(tempDir, "plugin.json");
|
|
269
|
+
if (!fs.existsSync(manifestPath)) {
|
|
270
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
271
|
+
return badRequest(res, "Cloned repository does not contain a plugin.json manifest");
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
let manifest;
|
|
275
|
+
try {
|
|
276
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
277
|
+
} catch (err) {
|
|
278
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
279
|
+
return badRequest(res, "Invalid plugin.json JSON: " + err.message);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const validation = validatePluginManifest(manifest);
|
|
283
|
+
if (!validation.valid) {
|
|
284
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
285
|
+
return badRequest(res, "Invalid plugin manifest: " + validation.errors.join(", "));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const destDir = path.join(pluginsBaseDir, manifest.id);
|
|
289
|
+
if (fs.existsSync(destDir)) {
|
|
290
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
291
|
+
return badRequest(res, "Plugin " + manifest.id + " is already installed at " + destDir);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
fs.cpSync(tempDir, destDir, { recursive: true });
|
|
295
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
296
|
+
|
|
297
|
+
return res.json({
|
|
298
|
+
success: true,
|
|
299
|
+
message: "Successfully installed plugin " + manifest.name,
|
|
300
|
+
plugin: { id: manifest.id, name: manifest.name, version: manifest.version, dir: destDir }
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Local directory install / link
|
|
305
|
+
const absSource = path.isAbsolute(source) ? source : path.resolve(root, source);
|
|
306
|
+
if (!fs.existsSync(absSource)) {
|
|
307
|
+
return badRequest(res, "Source directory not found: " + absSource);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const manifestPath = path.join(absSource, "plugin.json");
|
|
311
|
+
if (!fs.existsSync(manifestPath)) {
|
|
312
|
+
return badRequest(res, "Directory " + absSource + " does not contain a plugin.json manifest");
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
let manifest;
|
|
316
|
+
try {
|
|
317
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
318
|
+
} catch (err) {
|
|
319
|
+
return badRequest(res, "Invalid plugin.json JSON: " + err.message);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const validation = validatePluginManifest(manifest);
|
|
323
|
+
if (!validation.valid) {
|
|
324
|
+
return badRequest(res, "Invalid plugin manifest: " + validation.errors.join(", "));
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const destDir = path.join(pluginsBaseDir, manifest.id);
|
|
328
|
+
if (fs.existsSync(destDir)) {
|
|
329
|
+
return badRequest(res, "Plugin " + manifest.id + " is already installed");
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (link) {
|
|
333
|
+
fs.symlinkSync(absSource, destDir, "junction");
|
|
334
|
+
} else {
|
|
335
|
+
fs.cpSync(absSource, destDir, { recursive: true });
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return res.json({
|
|
339
|
+
success: true,
|
|
340
|
+
message: "Successfully " + (link ? "linked" : "installed") + " plugin " + manifest.name,
|
|
341
|
+
plugin: { id: manifest.id, name: manifest.name, version: manifest.version, dir: destDir }
|
|
342
|
+
});
|
|
343
|
+
} catch (err) {
|
|
344
|
+
serverError(res, err);
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* DELETE /api/plugins/:id
|
|
350
|
+
* Removes or unlinks an installed plugin.
|
|
351
|
+
*/
|
|
352
|
+
router.delete("/api/plugins/:id", requireAuth, requireScope("config:write"), (req, res) => {
|
|
353
|
+
try {
|
|
354
|
+
const id = req.params.id;
|
|
355
|
+
const isGlobal = req.query?.global === "true";
|
|
356
|
+
const projectRoot = req.query?.root || process.cwd();
|
|
357
|
+
|
|
358
|
+
const searchDirs = isGlobal
|
|
359
|
+
? [path.join(os.homedir(), ".agent", "plugins")]
|
|
360
|
+
: [
|
|
361
|
+
path.join(projectRoot, ".agent", "plugins"),
|
|
362
|
+
path.join(os.homedir(), ".agent", "plugins")
|
|
363
|
+
];
|
|
364
|
+
|
|
365
|
+
let targetPath = null;
|
|
366
|
+
for (const dir of searchDirs) {
|
|
367
|
+
const candidate = path.join(dir, id);
|
|
368
|
+
if (fs.existsSync(candidate)) {
|
|
369
|
+
targetPath = candidate;
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (!targetPath) {
|
|
375
|
+
return badRequest(res, "Plugin " + id + " not found");
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const stat = fs.lstatSync(targetPath);
|
|
379
|
+
if (stat.isSymbolicLink()) {
|
|
380
|
+
fs.unlinkSync(targetPath);
|
|
381
|
+
} else {
|
|
382
|
+
fs.rmSync(targetPath, { recursive: true, force: true });
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
res.json({
|
|
386
|
+
success: true,
|
|
387
|
+
message: "Plugin " + id + " uninstalled successfully",
|
|
388
|
+
id
|
|
389
|
+
});
|
|
390
|
+
} catch (err) {
|
|
391
|
+
serverError(res, err);
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* GET /api/plugins/:id
|
|
397
|
+
* Retrieve details of a specific plugin.
|
|
398
|
+
*/
|
|
399
|
+
router.get("/api/plugins/:id", requireAuth, requireScope("config:read"), (req, res) => {
|
|
400
|
+
try {
|
|
401
|
+
const projectRoot = req.query?.root || process.cwd();
|
|
402
|
+
const plugin = getPluginById(req.params.id, projectRoot);
|
|
403
|
+
if (!plugin) {
|
|
404
|
+
return badRequest(res, "Plugin " + req.params.id + " not found");
|
|
405
|
+
}
|
|
406
|
+
const userRatings = loadRatings(projectRoot);
|
|
407
|
+
res.json({
|
|
408
|
+
success: true,
|
|
409
|
+
plugin: {
|
|
410
|
+
id: plugin.id,
|
|
411
|
+
name: plugin.manifest?.name || plugin.id,
|
|
412
|
+
version: plugin.manifest?.version || "0.0.0",
|
|
413
|
+
description: plugin.manifest?.description || "",
|
|
414
|
+
valid: plugin.valid,
|
|
415
|
+
errors: plugin.errors,
|
|
416
|
+
dir: plugin.dir,
|
|
417
|
+
manifest: plugin.manifest,
|
|
418
|
+
userRating: userRatings[plugin.id]?.rating || null,
|
|
419
|
+
userReview: userRatings[plugin.id]?.review || null
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
} catch (err) {
|
|
423
|
+
serverError(res, err);
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* POST /api/plugins/:id/run
|
|
429
|
+
* Executes a plugin CLI handler and returns captured stdout/stderr.
|
|
430
|
+
*/
|
|
431
|
+
router.post("/api/plugins/:id/run", requireAuth, requireScope("config:read"), async (req, res) => {
|
|
432
|
+
try {
|
|
433
|
+
const { id } = req.params;
|
|
434
|
+
const { subcommand = "", args = [] } = req.body || {};
|
|
435
|
+
const projectRoot = req.body?.projectRoot || process.cwd();
|
|
436
|
+
const plugin = getPluginById(id, projectRoot);
|
|
437
|
+
|
|
438
|
+
if (!plugin) {
|
|
439
|
+
return badRequest(res, "Plugin " + id + " not found");
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (!plugin.manifest?.cli?.handler) {
|
|
443
|
+
return badRequest(res, "Plugin " + id + " does not declare a CLI handler");
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const absHandler = path.resolve(plugin.dir, plugin.manifest.cli.handler);
|
|
447
|
+
if (!fs.existsSync(absHandler)) {
|
|
448
|
+
return badRequest(res, "Plugin CLI handler file not found: " + absHandler);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const logs = [];
|
|
452
|
+
const origLog = console.log;
|
|
453
|
+
const origError = console.error;
|
|
454
|
+
console.log = (...a) => logs.push(a.map(x => typeof x === "object" ? JSON.stringify(x, null, 2) : String(x)).join(" "));
|
|
455
|
+
console.error = (...a) => logs.push("[error] " + a.map(x => typeof x === "object" ? JSON.stringify(x, null, 2) : String(x)).join(" "));
|
|
456
|
+
|
|
457
|
+
try {
|
|
458
|
+
const handlerMod = await import(absHandler + "?t=" + Date.now());
|
|
459
|
+
const argv = ["node", "total-recall", id, ...(subcommand ? [subcommand] : []), ...(Array.isArray(args) ? args : [])];
|
|
460
|
+
if (handlerMod.run) {
|
|
461
|
+
await handlerMod.run(argv);
|
|
462
|
+
} else if (handlerMod.default) {
|
|
463
|
+
await handlerMod.default(argv.slice(3));
|
|
464
|
+
}
|
|
465
|
+
} finally {
|
|
466
|
+
console.log = origLog;
|
|
467
|
+
console.error = origError;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
res.json({
|
|
471
|
+
success: true,
|
|
472
|
+
pluginId: id,
|
|
473
|
+
output: logs.join("\n")
|
|
474
|
+
});
|
|
475
|
+
} catch (err) {
|
|
476
|
+
serverError(res, err);
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* GET /api/plugins/:id/readme
|
|
482
|
+
* Returns the README markdown content for the plugin.
|
|
483
|
+
*/
|
|
484
|
+
router.get("/api/plugins/:id/readme", requireAuth, requireScope("config:read"), (req, res) => {
|
|
485
|
+
try {
|
|
486
|
+
const { id } = req.params;
|
|
487
|
+
const projectRoot = req.query?.root || process.cwd();
|
|
488
|
+
const plugin = getPluginById(id, projectRoot);
|
|
489
|
+
|
|
490
|
+
if (!plugin) {
|
|
491
|
+
return badRequest(res, "Plugin " + id + " not found");
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const candidates = [
|
|
495
|
+
path.join(plugin.dir, "README.md"),
|
|
496
|
+
path.join(plugin.dir, "readme.md")
|
|
497
|
+
];
|
|
498
|
+
|
|
499
|
+
let content = "# " + (plugin.manifest?.name || id) + "\n\n" + (plugin.manifest?.description || "");
|
|
500
|
+
for (const cand of candidates) {
|
|
501
|
+
if (fs.existsSync(cand)) {
|
|
502
|
+
content = fs.readFileSync(cand, "utf8");
|
|
503
|
+
break;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
res.json({
|
|
508
|
+
success: true,
|
|
509
|
+
pluginId: id,
|
|
510
|
+
readme: content
|
|
511
|
+
});
|
|
512
|
+
} catch (err) {
|
|
513
|
+
serverError(res, err);
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
export default router;
|