@chrysb/alphaclaw 0.7.2-beta.1 → 0.7.2-beta.2
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/lib/public/css/theme.css +12 -1
- package/lib/public/js/app.js +10 -2
- package/lib/public/js/components/cron-tab/cron-job-detail.js +18 -2
- package/lib/public/js/components/cron-tab/cron-job-list.js +43 -0
- package/lib/public/js/components/cron-tab/cron-job-trends-panel.js +319 -0
- package/lib/public/js/components/cron-tab/cron-job-usage.js +22 -8
- package/lib/public/js/components/cron-tab/cron-overview.js +17 -13
- package/lib/public/js/components/cron-tab/cron-prompt-editor.js +1 -1
- package/lib/public/js/components/cron-tab/cron-run-history-panel.js +29 -12
- package/lib/public/js/components/cron-tab/cron-runs-trend-card.js +109 -53
- package/lib/public/js/components/cron-tab/index.js +6 -0
- package/lib/public/js/components/cron-tab/use-cron-tab.js +51 -0
- package/lib/public/js/components/nodes-tab/connected-nodes/index.js +85 -0
- package/lib/public/js/components/nodes-tab/connected-nodes/user-connected-nodes.js +25 -0
- package/lib/public/js/components/nodes-tab/exec-allowlist/index.js +89 -0
- package/lib/public/js/components/nodes-tab/exec-allowlist/use-exec-allowlist.js +78 -0
- package/lib/public/js/components/nodes-tab/exec-config/index.js +118 -0
- package/lib/public/js/components/nodes-tab/exec-config/use-exec-config.js +79 -0
- package/lib/public/js/components/nodes-tab/index.js +46 -0
- package/lib/public/js/components/nodes-tab/setup-wizard/index.js +243 -0
- package/lib/public/js/components/nodes-tab/setup-wizard/use-setup-wizard.js +159 -0
- package/lib/public/js/components/nodes-tab/use-nodes-tab.js +22 -0
- package/lib/public/js/components/routes/index.js +1 -0
- package/lib/public/js/components/routes/nodes-route.js +11 -0
- package/lib/public/js/components/usage-tab/use-usage-tab.js +11 -3
- package/lib/public/js/lib/api.js +61 -0
- package/lib/public/js/lib/app-navigation.js +2 -0
- package/lib/public/js/lib/format.js +50 -0
- package/lib/server/constants.js +1 -0
- package/lib/server/cron-service.js +230 -1
- package/lib/server/init/register-server-routes.js +8 -0
- package/lib/server/routes/cron.js +11 -0
- package/lib/server/routes/nodes.js +286 -0
- package/package.json +2 -2
|
@@ -136,6 +136,17 @@ const registerCronRoutes = ({
|
|
|
136
136
|
res.status(400).json({ ok: false, error: error.message });
|
|
137
137
|
}
|
|
138
138
|
});
|
|
139
|
+
app.get("/api/cron/jobs/:id/trends", requireAuth, (req, res) => {
|
|
140
|
+
try {
|
|
141
|
+
const trends = cronService.getJobRunTrends({
|
|
142
|
+
jobId: req.params.id,
|
|
143
|
+
range: String(req.query.range || "7d"),
|
|
144
|
+
});
|
|
145
|
+
res.json({ ok: true, trends });
|
|
146
|
+
} catch (error) {
|
|
147
|
+
res.status(400).json({ ok: false, error: error.message });
|
|
148
|
+
}
|
|
149
|
+
});
|
|
139
150
|
|
|
140
151
|
app.get("/api/cron/usage/bulk", requireAuth, (req, res) => {
|
|
141
152
|
try {
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
const crypto = require("crypto");
|
|
3
|
+
const { parseJsonObjectFromNoisyOutput } = require("../utils/json");
|
|
4
|
+
const { quoteShellArg } = require("../utils/shell");
|
|
5
|
+
|
|
6
|
+
const kAllowedExecHosts = new Set(["gateway", "node"]);
|
|
7
|
+
const kAllowedExecSecurity = new Set(["deny", "allowlist", "full"]);
|
|
8
|
+
const kAllowedExecAsk = new Set(["off", "on-miss", "always"]);
|
|
9
|
+
const kSafeNodeIdPattern = /^[\w\-:.]+$/;
|
|
10
|
+
|
|
11
|
+
const quoteCliArg = (value) => quoteShellArg(value, { strategy: "single" });
|
|
12
|
+
|
|
13
|
+
const normalizeExecAsk = (value) => {
|
|
14
|
+
const normalized = String(value || "").trim().toLowerCase();
|
|
15
|
+
if (normalized === "on") return "on-miss";
|
|
16
|
+
return normalized;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const buildDefaultExecConfig = () => ({
|
|
20
|
+
host: "gateway",
|
|
21
|
+
security: "allowlist",
|
|
22
|
+
ask: "on-miss",
|
|
23
|
+
node: "",
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const parseNodesStatus = (stdout) => {
|
|
27
|
+
const parsed = parseJsonObjectFromNoisyOutput(stdout) || {};
|
|
28
|
+
const nodes = Array.isArray(parsed.nodes) ? parsed.nodes : [];
|
|
29
|
+
const pending = Array.isArray(parsed.pending)
|
|
30
|
+
? parsed.pending
|
|
31
|
+
: nodes.filter((entry) => entry && entry.paired === false);
|
|
32
|
+
return { nodes, pending };
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const readExecApprovalsFile = ({ fsModule, openclawDir }) => {
|
|
36
|
+
const filePath = path.join(openclawDir, "exec-approvals.json");
|
|
37
|
+
try {
|
|
38
|
+
const raw = fsModule.readFileSync(filePath, "utf8");
|
|
39
|
+
const parsed = JSON.parse(raw);
|
|
40
|
+
return parsed && typeof parsed === "object" ? parsed : { version: 1 };
|
|
41
|
+
} catch {
|
|
42
|
+
return { version: 1 };
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const writeExecApprovalsFile = ({ fsModule, openclawDir, file }) => {
|
|
47
|
+
const filePath = path.join(openclawDir, "exec-approvals.json");
|
|
48
|
+
fsModule.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
49
|
+
fsModule.writeFileSync(filePath, JSON.stringify(file, null, 2) + "\n", "utf8");
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const ensureWildcardAgent = (file) => {
|
|
53
|
+
const agents = file.agents && typeof file.agents === "object" ? file.agents : {};
|
|
54
|
+
const wildcard =
|
|
55
|
+
agents["*"] && typeof agents["*"] === "object" ? agents["*"] : {};
|
|
56
|
+
const allowlist = Array.isArray(wildcard.allowlist) ? wildcard.allowlist : [];
|
|
57
|
+
agents["*"] = { ...wildcard, allowlist };
|
|
58
|
+
return { ...file, version: 1, agents };
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const resolveSetupUiBaseUrl = (req) => {
|
|
62
|
+
const explicit = String(
|
|
63
|
+
process.env.ALPHACLAW_SETUP_URL ||
|
|
64
|
+
process.env.ALPHACLAW_BASE_URL ||
|
|
65
|
+
process.env.RENDER_EXTERNAL_URL ||
|
|
66
|
+
process.env.URL ||
|
|
67
|
+
"",
|
|
68
|
+
)
|
|
69
|
+
.trim()
|
|
70
|
+
.replace(/\/+$/, "");
|
|
71
|
+
if (explicit) return explicit;
|
|
72
|
+
|
|
73
|
+
const railwayPublicDomain = String(process.env.RAILWAY_PUBLIC_DOMAIN || "").trim();
|
|
74
|
+
if (railwayPublicDomain) {
|
|
75
|
+
return `https://${railwayPublicDomain}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const railwayStaticUrl = String(process.env.RAILWAY_STATIC_URL || "")
|
|
79
|
+
.trim()
|
|
80
|
+
.replace(/\/+$/, "");
|
|
81
|
+
if (railwayStaticUrl) return railwayStaticUrl;
|
|
82
|
+
|
|
83
|
+
const forwardedProto = String(req.headers["x-forwarded-proto"] || "").trim();
|
|
84
|
+
const forwardedHost = String(req.headers["x-forwarded-host"] || "").trim();
|
|
85
|
+
if (forwardedProto && forwardedHost) {
|
|
86
|
+
return `${forwardedProto}://${forwardedHost}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const reqProtocol = req.protocol || "http";
|
|
90
|
+
const reqHost = req.get("host");
|
|
91
|
+
if (reqHost) {
|
|
92
|
+
return `${reqProtocol}://${reqHost}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return "http://localhost:3000";
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const parseBaseUrlParts = (baseUrl) => {
|
|
99
|
+
try {
|
|
100
|
+
const parsed = new URL(baseUrl);
|
|
101
|
+
const tls = parsed.protocol === "https:";
|
|
102
|
+
const port =
|
|
103
|
+
Number(parsed.port) || (tls ? 443 : 80);
|
|
104
|
+
return {
|
|
105
|
+
baseUrl: parsed.origin,
|
|
106
|
+
host: parsed.hostname,
|
|
107
|
+
port,
|
|
108
|
+
tls,
|
|
109
|
+
};
|
|
110
|
+
} catch {
|
|
111
|
+
return {
|
|
112
|
+
baseUrl: "http://localhost:3000",
|
|
113
|
+
host: "localhost",
|
|
114
|
+
port: 3000,
|
|
115
|
+
tls: false,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const registerNodeRoutes = ({
|
|
121
|
+
app,
|
|
122
|
+
clawCmd,
|
|
123
|
+
openclawDir,
|
|
124
|
+
gatewayToken = "",
|
|
125
|
+
fsModule,
|
|
126
|
+
}) => {
|
|
127
|
+
app.get("/api/nodes", async (_req, res) => {
|
|
128
|
+
const result = await clawCmd("nodes status --json", { quiet: true });
|
|
129
|
+
if (!result.ok) {
|
|
130
|
+
return res.status(500).json({
|
|
131
|
+
ok: false,
|
|
132
|
+
error: result.stderr || "Could not load nodes status",
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
const status = parseNodesStatus(result.stdout);
|
|
136
|
+
return res.json({ ok: true, ...status });
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
app.post("/api/nodes/:id/approve", async (req, res) => {
|
|
140
|
+
const nodeId = String(req.params.id || "").trim();
|
|
141
|
+
if (!nodeId || !kSafeNodeIdPattern.test(nodeId)) {
|
|
142
|
+
return res.status(400).json({ ok: false, error: "Invalid node id" });
|
|
143
|
+
}
|
|
144
|
+
const result = await clawCmd(`nodes approve ${quoteCliArg(nodeId)}`);
|
|
145
|
+
if (!result.ok) {
|
|
146
|
+
return res.status(500).json({
|
|
147
|
+
ok: false,
|
|
148
|
+
error: result.stderr || "Could not approve node",
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
return res.json({ ok: true });
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
app.get("/api/nodes/connect-info", async (req, res) => {
|
|
155
|
+
const baseUrl = resolveSetupUiBaseUrl(req);
|
|
156
|
+
const parsed = parseBaseUrlParts(baseUrl);
|
|
157
|
+
return res.json({
|
|
158
|
+
ok: true,
|
|
159
|
+
baseUrl: parsed.baseUrl,
|
|
160
|
+
gatewayHost: parsed.host,
|
|
161
|
+
gatewayPort: parsed.port,
|
|
162
|
+
gatewayToken: String(gatewayToken || ""),
|
|
163
|
+
tls: parsed.tls,
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
app.get("/api/nodes/exec-config", async (_req, res) => {
|
|
168
|
+
const result = await clawCmd("config get tools.exec --json", { quiet: true });
|
|
169
|
+
if (!result.ok) {
|
|
170
|
+
return res.json({ ok: true, config: buildDefaultExecConfig() });
|
|
171
|
+
}
|
|
172
|
+
const parsed = parseJsonObjectFromNoisyOutput(result.stdout) || {};
|
|
173
|
+
const config = buildDefaultExecConfig();
|
|
174
|
+
const host = String(parsed.host || "").trim().toLowerCase();
|
|
175
|
+
const security = String(parsed.security || "").trim().toLowerCase();
|
|
176
|
+
const ask = normalizeExecAsk(parsed.ask);
|
|
177
|
+
const node = String(parsed.node || "").trim();
|
|
178
|
+
if (kAllowedExecHosts.has(host)) config.host = host;
|
|
179
|
+
if (kAllowedExecSecurity.has(security)) config.security = security;
|
|
180
|
+
if (kAllowedExecAsk.has(ask)) config.ask = ask;
|
|
181
|
+
if (node) config.node = node;
|
|
182
|
+
return res.json({ ok: true, config });
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
app.post("/api/nodes/exec-config", async (req, res) => {
|
|
186
|
+
const body = req.body || {};
|
|
187
|
+
const host = String(body.host || "").trim().toLowerCase();
|
|
188
|
+
const security = String(body.security || "").trim().toLowerCase();
|
|
189
|
+
const ask = normalizeExecAsk(body.ask);
|
|
190
|
+
const node = String(body.node || "").trim();
|
|
191
|
+
if (!kAllowedExecHosts.has(host)) {
|
|
192
|
+
return res.status(400).json({ ok: false, error: "Invalid exec host" });
|
|
193
|
+
}
|
|
194
|
+
if (!kAllowedExecSecurity.has(security)) {
|
|
195
|
+
return res.status(400).json({ ok: false, error: "Invalid exec security" });
|
|
196
|
+
}
|
|
197
|
+
if (!kAllowedExecAsk.has(ask)) {
|
|
198
|
+
return res.status(400).json({ ok: false, error: "Invalid exec ask mode" });
|
|
199
|
+
}
|
|
200
|
+
if (host === "node" && !node) {
|
|
201
|
+
return res
|
|
202
|
+
.status(400)
|
|
203
|
+
.json({ ok: false, error: "Node target is required when host is node" });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const commands = [
|
|
207
|
+
`config set tools.exec.host ${quoteCliArg(host)}`,
|
|
208
|
+
`config set tools.exec.security ${quoteCliArg(security)}`,
|
|
209
|
+
`config set tools.exec.ask ${quoteCliArg(ask)}`,
|
|
210
|
+
host === "node"
|
|
211
|
+
? `config set tools.exec.node ${quoteCliArg(node)}`
|
|
212
|
+
: "config set tools.exec.node ''",
|
|
213
|
+
];
|
|
214
|
+
|
|
215
|
+
for (const command of commands) {
|
|
216
|
+
const result = await clawCmd(command);
|
|
217
|
+
if (!result.ok) {
|
|
218
|
+
return res.status(500).json({
|
|
219
|
+
ok: false,
|
|
220
|
+
error: result.stderr || `Could not apply exec config (${command})`,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return res.json({ ok: true, restartRequired: true });
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
app.get("/api/nodes/exec-approvals", (_req, res) => {
|
|
229
|
+
const approvals = ensureWildcardAgent(
|
|
230
|
+
readExecApprovalsFile({ fsModule, openclawDir }),
|
|
231
|
+
);
|
|
232
|
+
const allowlist = approvals?.agents?.["*"]?.allowlist || [];
|
|
233
|
+
return res.json({
|
|
234
|
+
ok: true,
|
|
235
|
+
file: approvals,
|
|
236
|
+
allowlist,
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
app.post("/api/nodes/exec-approvals/allowlist", (req, res) => {
|
|
241
|
+
const pattern = String(req.body?.pattern || "").trim();
|
|
242
|
+
if (!pattern) {
|
|
243
|
+
return res.status(400).json({ ok: false, error: "pattern is required" });
|
|
244
|
+
}
|
|
245
|
+
const approvals = ensureWildcardAgent(
|
|
246
|
+
readExecApprovalsFile({ fsModule, openclawDir }),
|
|
247
|
+
);
|
|
248
|
+
const allowlist = approvals.agents["*"].allowlist;
|
|
249
|
+
const existing = allowlist.find(
|
|
250
|
+
(entry) => String(entry?.pattern || "").trim() === pattern,
|
|
251
|
+
);
|
|
252
|
+
if (existing) {
|
|
253
|
+
return res.json({ ok: true, entry: existing, unchanged: true });
|
|
254
|
+
}
|
|
255
|
+
const entry = {
|
|
256
|
+
pattern,
|
|
257
|
+
id: crypto.randomUUID(),
|
|
258
|
+
lastUsedAt: Date.now(),
|
|
259
|
+
};
|
|
260
|
+
approvals.agents["*"].allowlist = [...allowlist, entry];
|
|
261
|
+
writeExecApprovalsFile({ fsModule, openclawDir, file: approvals });
|
|
262
|
+
return res.json({ ok: true, entry });
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
app.delete("/api/nodes/exec-approvals/allowlist/:id", (req, res) => {
|
|
266
|
+
const id = String(req.params.id || "").trim();
|
|
267
|
+
if (!id) {
|
|
268
|
+
return res.status(400).json({ ok: false, error: "id is required" });
|
|
269
|
+
}
|
|
270
|
+
const approvals = ensureWildcardAgent(
|
|
271
|
+
readExecApprovalsFile({ fsModule, openclawDir }),
|
|
272
|
+
);
|
|
273
|
+
const allowlist = approvals.agents["*"].allowlist;
|
|
274
|
+
const nextAllowlist = allowlist.filter((entry) => String(entry?.id || "") !== id);
|
|
275
|
+
if (nextAllowlist.length === allowlist.length) {
|
|
276
|
+
return res.status(404).json({ ok: false, error: "Allowlist entry not found" });
|
|
277
|
+
}
|
|
278
|
+
approvals.agents["*"].allowlist = nextAllowlist;
|
|
279
|
+
writeExecApprovalsFile({ fsModule, openclawDir, file: approvals });
|
|
280
|
+
return res.json({ ok: true });
|
|
281
|
+
});
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
module.exports = {
|
|
285
|
+
registerNodeRoutes,
|
|
286
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chrysb/alphaclaw",
|
|
3
|
-
"version": "0.7.2-beta.
|
|
3
|
+
"version": "0.7.2-beta.2",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"express": "^4.21.0",
|
|
33
33
|
"http-proxy": "^1.18.1",
|
|
34
|
-
"openclaw": "2026.3.
|
|
34
|
+
"openclaw": "2026.3.12",
|
|
35
35
|
"ws": "^8.19.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|