total-recall-brain 3.28.2 → 3.30.1
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/SKILL.md +22 -6
- 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/connect.mjs +10 -1
- 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 +76 -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
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Peer-facing plugin routes — how one Total Recall node shares plugins with
|
|
3
|
+
* another. Only reachable from the mesh (100.64.0.0/10) or loopback with the
|
|
4
|
+
* shared TR_MESH_SYNC_TOKEN, and only ever read-only: a peer can see and fetch
|
|
5
|
+
* what this node's owner marked shared, nothing more.
|
|
6
|
+
*/
|
|
7
|
+
import { Router } from "express";
|
|
8
|
+
import { requireMeshSyncAuth } from "../../core/mesh-auth.mjs";
|
|
9
|
+
import { getMeshHostname } from "../../core/mesh.mjs";
|
|
10
|
+
import { listSharedPlugins, packSharedPlugin, packPublicPlugin } from "../../core/plugin-store.mjs";
|
|
11
|
+
import { serverError } from "./_shared.mjs";
|
|
12
|
+
|
|
13
|
+
const router = Router();
|
|
14
|
+
const ID_PATTERN = /^[a-z][a-z0-9-]{1,63}$/;
|
|
15
|
+
|
|
16
|
+
router.get("/api/mesh/plugins", requireMeshSyncAuth, (_req, res) => {
|
|
17
|
+
try {
|
|
18
|
+
const plugins = listSharedPlugins(process.cwd()).map(({ _plugin, ...rest }) => rest);
|
|
19
|
+
res.json({ node: { hostname: getMeshHostname() }, plugins });
|
|
20
|
+
} catch (err) {
|
|
21
|
+
serverError(res, err);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
router.get("/api/mesh/plugins/:id/bundle", requireMeshSyncAuth, (req, res) => {
|
|
26
|
+
try {
|
|
27
|
+
if (!ID_PATTERN.test(req.params.id)) return res.status(400).json({ error: "Invalid plugin id" });
|
|
28
|
+
const bundle = packSharedPlugin(req.params.id, process.cwd());
|
|
29
|
+
if (!bundle) return res.status(404).json({ error: `Plugin ${req.params.id} is not shared by this node` });
|
|
30
|
+
res.json(bundle);
|
|
31
|
+
} catch (err) {
|
|
32
|
+
serverError(res, err);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Shared bundles are public by owner choice. The recipient's URL carries a
|
|
37
|
+
// content hash pin; this route never lists plugins or executes their code.
|
|
38
|
+
router.get("/api/public/plugins/:id/bundle", (req, res) => {
|
|
39
|
+
try {
|
|
40
|
+
if (!ID_PATTERN.test(req.params.id)) return res.status(400).json({ error: "Invalid plugin id" });
|
|
41
|
+
const bundle = packPublicPlugin(req.params.id, process.cwd());
|
|
42
|
+
if (!bundle) return res.status(404).json({ error: "Plugin is not shared" });
|
|
43
|
+
res.set("Cache-Control", "no-store").json(bundle);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
serverError(res, err);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
export default router;
|
|
@@ -1,225 +1,70 @@
|
|
|
1
1
|
import { Router } from "express";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import os from "node:os";
|
|
5
|
-
import { spawnSync } from "node:child_process";
|
|
6
4
|
import { requireAuth, requireScope } from "../auth.mjs";
|
|
7
|
-
import {
|
|
5
|
+
import { getPluginById } from "../../core/plugin-loader.mjs";
|
|
6
|
+
import {
|
|
7
|
+
listInstalledPlugins,
|
|
8
|
+
listAvailableBundled,
|
|
9
|
+
describePlugin,
|
|
10
|
+
installPlugin,
|
|
11
|
+
uninstallPlugin,
|
|
12
|
+
setPluginShared
|
|
13
|
+
} from "../../core/plugin-store.mjs";
|
|
14
|
+
import { listPeerPlugins } from "../../core/plugin-peers.mjs";
|
|
15
|
+
import { runPluginCommand } from "../../core/plugin-runner.mjs";
|
|
8
16
|
import { serverError, badRequest } from "./_shared.mjs";
|
|
9
17
|
|
|
10
18
|
const router = Router();
|
|
11
19
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
];
|
|
20
|
+
// Plugins always resolve against the server's own project. Routes used to take
|
|
21
|
+
// a caller-supplied `root`, which let a request point discovery — and the
|
|
22
|
+
// runner — at any directory on disk.
|
|
23
|
+
const projectRoot = () => process.cwd();
|
|
122
24
|
|
|
123
25
|
/**
|
|
124
26
|
* GET /api/plugins
|
|
125
|
-
*
|
|
27
|
+
* Installed plugins (project + global): manifest facts, provenance, content
|
|
28
|
+
* hash, sharing state and scheduled-task history. Nothing else.
|
|
126
29
|
*/
|
|
127
|
-
router.get("/api/plugins", requireAuth, requireScope("config:read"), (
|
|
30
|
+
router.get("/api/plugins", requireAuth, requireScope("config:read"), (_req, res) => {
|
|
128
31
|
try {
|
|
129
|
-
const
|
|
130
|
-
|
|
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
|
-
});
|
|
32
|
+
const plugins = listInstalledPlugins(projectRoot());
|
|
33
|
+
res.json({ success: true, count: plugins.length, plugins });
|
|
163
34
|
} catch (err) {
|
|
164
35
|
serverError(res, err);
|
|
165
36
|
}
|
|
166
37
|
});
|
|
167
38
|
|
|
168
39
|
/**
|
|
169
|
-
* GET /api/plugins/
|
|
170
|
-
*
|
|
40
|
+
* GET /api/plugins/available
|
|
41
|
+
* Plugins bundled with this Total Recall package, installable on any node.
|
|
171
42
|
*/
|
|
172
|
-
router.get("/api/plugins/
|
|
43
|
+
router.get("/api/plugins/available", requireAuth, requireScope("config:read"), (_req, res) => {
|
|
173
44
|
try {
|
|
174
|
-
|
|
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
|
-
});
|
|
45
|
+
res.json({ success: true, plugins: listAvailableBundled(projectRoot()) });
|
|
187
46
|
} catch (err) {
|
|
188
47
|
serverError(res, err);
|
|
189
48
|
}
|
|
190
49
|
});
|
|
191
50
|
|
|
192
51
|
/**
|
|
193
|
-
*
|
|
194
|
-
*
|
|
52
|
+
* GET /api/plugins/peers
|
|
53
|
+
* Plugins shared by each mesh peer, queried live. Per-peer status is reported
|
|
54
|
+
* exactly as observed (ok, offline, unreachable, not_configured, unsupported, error).
|
|
195
55
|
*/
|
|
196
|
-
router.
|
|
56
|
+
router.get("/api/plugins/peers", requireAuth, requireScope("config:read"), async (_req, res) => {
|
|
197
57
|
try {
|
|
198
|
-
const
|
|
199
|
-
const
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
58
|
+
const result = await listPeerPlugins();
|
|
59
|
+
const installed = new Map(listInstalledPlugins(projectRoot()).map((p) => [p.id, p]));
|
|
60
|
+
for (const peer of result.peers) {
|
|
61
|
+
for (const p of peer.plugins) {
|
|
62
|
+
const local = installed.get(p.id);
|
|
63
|
+
p.installed = !!local;
|
|
64
|
+
p.same_as_installed = !!local && local.sha256 === p.sha256;
|
|
65
|
+
}
|
|
204
66
|
}
|
|
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
|
-
});
|
|
67
|
+
res.json({ success: true, ...result });
|
|
223
68
|
} catch (err) {
|
|
224
69
|
serverError(res, err);
|
|
225
70
|
}
|
|
@@ -227,288 +72,108 @@ router.post("/api/plugins/:id/rate", requireAuth, requireScope("config:write"),
|
|
|
227
72
|
|
|
228
73
|
/**
|
|
229
74
|
* POST /api/plugins/install
|
|
230
|
-
*
|
|
75
|
+
* body: { source: "<bundled id>" | "<share link>" | "peer:<host>/<id>" | "<git url>" | "<path>", link?, global? }
|
|
231
76
|
*/
|
|
232
|
-
router.post("/api/plugins/install", requireAuth, requireScope("config:write"), (req, res) => {
|
|
77
|
+
router.post("/api/plugins/install", requireAuth, requireScope("config:write"), async (req, res) => {
|
|
233
78
|
try {
|
|
234
|
-
|
|
79
|
+
const { source, link = false, global: isGlobal = false } = req.body || {};
|
|
235
80
|
if (!source || typeof source !== "string") {
|
|
236
|
-
return badRequest(res, "Missing source
|
|
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(", "));
|
|
81
|
+
return badRequest(res, "Missing plugin source (bundled id, share link, peer:<host>/<id>, git URL, or path)");
|
|
325
82
|
}
|
|
326
|
-
|
|
327
|
-
|
|
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({
|
|
83
|
+
const result = await installPlugin(source, { projectRoot: projectRoot(), link: !!link, global: !!isGlobal });
|
|
84
|
+
res.json({
|
|
339
85
|
success: true,
|
|
340
|
-
message:
|
|
341
|
-
|
|
86
|
+
message: `Installed ${result.plugin.name} ${result.plugin.version} (${result.source.kind})`,
|
|
87
|
+
...result
|
|
342
88
|
});
|
|
343
89
|
} catch (err) {
|
|
344
|
-
|
|
90
|
+
badRequest(res, err.message);
|
|
345
91
|
}
|
|
346
92
|
});
|
|
347
93
|
|
|
348
94
|
/**
|
|
349
|
-
*
|
|
350
|
-
*
|
|
95
|
+
* POST /api/plugins/:id/share body: { shared: boolean }
|
|
96
|
+
* Offer (or stop offering) an installed plugin by direct link and mesh.
|
|
351
97
|
*/
|
|
352
|
-
router.
|
|
98
|
+
router.post("/api/plugins/:id/share", requireAuth, requireScope("config:write"), async (req, res) => {
|
|
353
99
|
try {
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
-
}
|
|
100
|
+
const shared = req.body?.shared;
|
|
101
|
+
if (typeof shared !== "boolean") return badRequest(res, "Body must include shared: true|false");
|
|
102
|
+
const plugin = await setPluginShared(req.params.id, shared, { projectRoot: projectRoot() });
|
|
103
|
+
res.json({ success: true, plugin });
|
|
104
|
+
} catch (err) {
|
|
105
|
+
badRequest(res, err.message);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
384
108
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
109
|
+
/**
|
|
110
|
+
* DELETE /api/plugins/:id[?global=true]
|
|
111
|
+
*/
|
|
112
|
+
router.delete("/api/plugins/:id", requireAuth, requireScope("config:write"), async (req, res) => {
|
|
113
|
+
try {
|
|
114
|
+
const result = await uninstallPlugin(req.params.id, {
|
|
115
|
+
projectRoot: projectRoot(),
|
|
116
|
+
global: req.query?.global === "true" ? true : undefined
|
|
389
117
|
});
|
|
118
|
+
res.json({ success: true, message: `Plugin ${result.id} removed`, ...result });
|
|
390
119
|
} catch (err) {
|
|
391
|
-
|
|
120
|
+
badRequest(res, err.message);
|
|
392
121
|
}
|
|
393
122
|
});
|
|
394
123
|
|
|
395
124
|
/**
|
|
396
125
|
* GET /api/plugins/:id
|
|
397
|
-
* Retrieve details of a specific plugin.
|
|
398
126
|
*/
|
|
399
127
|
router.get("/api/plugins/:id", requireAuth, requireScope("config:read"), (req, res) => {
|
|
400
128
|
try {
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
|
|
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
|
-
});
|
|
129
|
+
const plugin = getPluginById(req.params.id, projectRoot());
|
|
130
|
+
if (!plugin) return res.status(404).json({ success: false, error: `Plugin ${req.params.id} not found` });
|
|
131
|
+
res.json({ success: true, plugin: { ...describePlugin(plugin), manifest: plugin.manifest } });
|
|
422
132
|
} catch (err) {
|
|
423
133
|
serverError(res, err);
|
|
424
134
|
}
|
|
425
135
|
});
|
|
426
136
|
|
|
427
137
|
/**
|
|
428
|
-
* POST /api/plugins/:id/run
|
|
429
|
-
*
|
|
138
|
+
* POST /api/plugins/:id/run body: { subcommand?, args? }
|
|
139
|
+
* Runs the plugin's CLI handler in a child process. Executing plugin code is a
|
|
140
|
+
* write-level action.
|
|
430
141
|
*/
|
|
431
|
-
router.post("/api/plugins/:id/run", requireAuth, requireScope("config:
|
|
142
|
+
router.post("/api/plugins/:id/run", requireAuth, requireScope("config:write"), async (req, res) => {
|
|
432
143
|
try {
|
|
433
|
-
const { id } = req.params;
|
|
434
144
|
const { subcommand = "", args = [] } = req.body || {};
|
|
435
|
-
|
|
436
|
-
|
|
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;
|
|
145
|
+
if (typeof subcommand !== "string" || !Array.isArray(args) || args.some((a) => typeof a !== "string")) {
|
|
146
|
+
return badRequest(res, "subcommand must be a string and args an array of strings");
|
|
468
147
|
}
|
|
148
|
+
const plugin = getPluginById(req.params.id, projectRoot());
|
|
149
|
+
if (!plugin) return res.status(404).json({ success: false, error: `Plugin ${req.params.id} not found` });
|
|
150
|
+
if (!plugin.valid) return badRequest(res, `Plugin ${plugin.id} has an invalid manifest: ${plugin.errors.join("; ")}`);
|
|
151
|
+
if (!plugin.manifest?.cli?.handler) return badRequest(res, `Plugin ${plugin.id} does not declare a CLI handler`);
|
|
469
152
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
pluginId: id,
|
|
473
|
-
output: logs.join("\n")
|
|
474
|
-
});
|
|
153
|
+
const result = await runPluginCommand(plugin, { subcommand, args, cwd: projectRoot() });
|
|
154
|
+
res.json({ success: result.ok, pluginId: plugin.id, ...result });
|
|
475
155
|
} catch (err) {
|
|
476
|
-
|
|
156
|
+
badRequest(res, err.message);
|
|
477
157
|
}
|
|
478
158
|
});
|
|
479
159
|
|
|
480
160
|
/**
|
|
481
161
|
* GET /api/plugins/:id/readme
|
|
482
|
-
* Returns the README markdown content for the plugin.
|
|
483
162
|
*/
|
|
484
163
|
router.get("/api/plugins/:id/readme", requireAuth, requireScope("config:read"), (req, res) => {
|
|
485
164
|
try {
|
|
486
|
-
const
|
|
487
|
-
|
|
488
|
-
const plugin = getPluginById(id, projectRoot);
|
|
489
|
-
|
|
490
|
-
if (!plugin) {
|
|
491
|
-
return badRequest(res, "Plugin " + id + " not found");
|
|
492
|
-
}
|
|
165
|
+
const plugin = getPluginById(req.params.id, projectRoot());
|
|
166
|
+
if (!plugin) return res.status(404).json({ success: false, error: `Plugin ${req.params.id} not found` });
|
|
493
167
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
path.join(plugin.dir,
|
|
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");
|
|
168
|
+
let readme = `# ${plugin.manifest?.name || plugin.id}\n\n${plugin.manifest?.description || ""}`;
|
|
169
|
+
for (const name of ["README.md", "readme.md"]) {
|
|
170
|
+
const candidate = path.join(plugin.dir, name);
|
|
171
|
+
if (fs.existsSync(candidate)) {
|
|
172
|
+
readme = fs.readFileSync(candidate, "utf8");
|
|
503
173
|
break;
|
|
504
174
|
}
|
|
505
175
|
}
|
|
506
|
-
|
|
507
|
-
res.json({
|
|
508
|
-
success: true,
|
|
509
|
-
pluginId: id,
|
|
510
|
-
readme: content
|
|
511
|
-
});
|
|
176
|
+
res.json({ success: true, pluginId: plugin.id, readme });
|
|
512
177
|
} catch (err) {
|
|
513
178
|
serverError(res, err);
|
|
514
179
|
}
|
|
@@ -30,9 +30,12 @@ researchRouter.get('/api/research', requireAuth, requireScope('memory:read'), (r
|
|
|
30
30
|
|
|
31
31
|
researchRouter.post('/api/research', requireAuth, requireScope('memory:write'), (req, res) => {
|
|
32
32
|
try {
|
|
33
|
-
const { topic, priority, notes } = req.body || {};
|
|
33
|
+
const { topic, priority, notes, via } = req.body || {};
|
|
34
34
|
if (!topic) return badRequest(res, 'topic is required');
|
|
35
|
-
|
|
35
|
+
// Every REST request is a human asking (dashboard, extension, API client).
|
|
36
|
+
// `via` only labels where from; autonomous research never enters here.
|
|
37
|
+
const requested_via = ['dashboard', 'extension', 'chat', 'api'].includes(via) ? via : 'api';
|
|
38
|
+
res.status(201).json(addToQueue({ topic, priority, notes, origin: 'user', requested_via }));
|
|
36
39
|
} catch (err) { serverError(res, err); }
|
|
37
40
|
});
|
|
38
41
|
|