forge-openclaw-plugin 0.2.13 → 0.2.18
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/README.md +8 -5
- package/dist/assets/{board-C_m78kvK.js → board-2KevHCI0.js} +2 -2
- package/dist/assets/{board-C_m78kvK.js.map → board-2KevHCI0.js.map} +1 -1
- package/dist/assets/index-CDYW4WDH.js +36 -0
- package/dist/assets/index-CDYW4WDH.js.map +1 -0
- package/dist/assets/index-yroQr6YZ.css +1 -0
- package/dist/assets/{motion-CpZvZumD.js → motion-q19HPmWs.js} +2 -2
- package/dist/assets/{motion-CpZvZumD.js.map → motion-q19HPmWs.js.map} +1 -1
- package/dist/assets/{table-DtyXTw03.js → table-BDMHBY4a.js} +2 -2
- package/dist/assets/{table-DtyXTw03.js.map → table-BDMHBY4a.js.map} +1 -1
- package/dist/assets/{ui-BXbpiKyS.js → ui-CQ_AsFs8.js} +2 -2
- package/dist/assets/{ui-BXbpiKyS.js.map → ui-CQ_AsFs8.js.map} +1 -1
- package/dist/assets/{vendor-QBH6qVEe.js → vendor-5HifrnRK.js} +90 -75
- package/dist/assets/{vendor-QBH6qVEe.js.map → vendor-5HifrnRK.js.map} +1 -1
- package/dist/assets/{viz-w-IMeueL.js → viz-CQzkRnTu.js} +2 -2
- package/dist/assets/{viz-w-IMeueL.js.map → viz-CQzkRnTu.js.map} +1 -1
- package/dist/index.html +8 -8
- package/dist/openclaw/api-client.d.ts +1 -0
- package/dist/openclaw/local-runtime.js +243 -15
- package/dist/openclaw/plugin-entry-shared.js +45 -4
- package/dist/openclaw/tools.js +15 -0
- package/dist/server/app.js +129 -11
- package/dist/server/openapi.js +181 -4
- package/dist/server/repositories/habits.js +358 -0
- package/dist/server/repositories/rewards.js +62 -0
- package/dist/server/services/context.js +16 -6
- package/dist/server/services/dashboard.js +6 -3
- package/dist/server/services/entity-crud.js +23 -1
- package/dist/server/services/gamification.js +66 -18
- package/dist/server/services/insights.js +2 -1
- package/dist/server/services/reviews.js +2 -1
- package/dist/server/types.js +140 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/server/migrations/003_habits.sql +30 -0
- package/server/migrations/004_habit_links.sql +8 -0
- package/server/migrations/005_habit_psyche_links.sql +24 -0
- package/skills/forge-openclaw/SKILL.md +16 -2
- package/skills/forge-openclaw/cron_jobs.md +395 -0
- package/dist/assets/index-BWtLtXwb.js +0 -36
- package/dist/assets/index-BWtLtXwb.js.map +0 -1
- package/dist/assets/index-Dp5GXY_z.css +0 -1
package/dist/index.html
CHANGED
|
@@ -13,15 +13,15 @@
|
|
|
13
13
|
/>
|
|
14
14
|
<link rel="icon" type="image/png" href="/forge/assets/favicon-BCHm9dUV.ico" />
|
|
15
15
|
<link rel="alternate icon" href="/forge/assets/favicon-BCHm9dUV.ico" />
|
|
16
|
-
<script type="module" crossorigin src="/forge/assets/index-
|
|
17
|
-
<link rel="modulepreload" crossorigin href="/forge/assets/vendor-
|
|
18
|
-
<link rel="modulepreload" crossorigin href="/forge/assets/motion-
|
|
19
|
-
<link rel="modulepreload" crossorigin href="/forge/assets/ui-
|
|
20
|
-
<link rel="modulepreload" crossorigin href="/forge/assets/table-
|
|
21
|
-
<link rel="modulepreload" crossorigin href="/forge/assets/viz-
|
|
22
|
-
<link rel="modulepreload" crossorigin href="/forge/assets/board-
|
|
16
|
+
<script type="module" crossorigin src="/forge/assets/index-CDYW4WDH.js"></script>
|
|
17
|
+
<link rel="modulepreload" crossorigin href="/forge/assets/vendor-5HifrnRK.js">
|
|
18
|
+
<link rel="modulepreload" crossorigin href="/forge/assets/motion-q19HPmWs.js">
|
|
19
|
+
<link rel="modulepreload" crossorigin href="/forge/assets/ui-CQ_AsFs8.js">
|
|
20
|
+
<link rel="modulepreload" crossorigin href="/forge/assets/table-BDMHBY4a.js">
|
|
21
|
+
<link rel="modulepreload" crossorigin href="/forge/assets/viz-CQzkRnTu.js">
|
|
22
|
+
<link rel="modulepreload" crossorigin href="/forge/assets/board-2KevHCI0.js">
|
|
23
23
|
<link rel="stylesheet" crossorigin href="/forge/assets/vendor-CRS-psbw.css">
|
|
24
|
-
<link rel="stylesheet" crossorigin href="/forge/assets/index-
|
|
24
|
+
<link rel="stylesheet" crossorigin href="/forge/assets/index-yroQr6YZ.css">
|
|
25
25
|
</head>
|
|
26
26
|
<body class="bg-canvas text-ink antialiased">
|
|
27
27
|
<div id="root"></div>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { closeSync, existsSync, mkdirSync, openSync } from "node:fs";
|
|
3
|
+
import net from "node:net";
|
|
3
4
|
import { homedir } from "node:os";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
@@ -8,18 +9,98 @@ const LOCAL_HOSTNAMES = new Set(["127.0.0.1", "localhost", "::1"]);
|
|
|
8
9
|
const STARTUP_TIMEOUT_MS = 15_000;
|
|
9
10
|
const HEALTHCHECK_TIMEOUT_MS = 1_500;
|
|
10
11
|
const HEALTHCHECK_INTERVAL_MS = 250;
|
|
12
|
+
const EXISTING_RUNTIME_GRACE_MS = 3_000;
|
|
13
|
+
const MAX_PORT_SCAN_ATTEMPTS = 20;
|
|
14
|
+
const FORGE_PLUGIN_ID = "forge-openclaw-plugin";
|
|
11
15
|
let managedRuntimeChild = null;
|
|
12
16
|
let managedRuntimeKey = null;
|
|
13
17
|
let managedRuntimeLogPath = null;
|
|
14
18
|
let lastRuntimeExitDetails = null;
|
|
15
19
|
let startupPromise = null;
|
|
20
|
+
let startupRuntimeKey = null;
|
|
16
21
|
const dependencyInstallPromises = new Map();
|
|
17
22
|
function runtimeKey(config) {
|
|
18
23
|
return `${config.origin}:${config.port}`;
|
|
19
24
|
}
|
|
25
|
+
function buildForgeBaseUrl(origin, port) {
|
|
26
|
+
const url = new URL(origin.endsWith("/") ? origin : `${origin}/`);
|
|
27
|
+
url.port = String(port);
|
|
28
|
+
url.pathname = "/";
|
|
29
|
+
url.search = "";
|
|
30
|
+
url.hash = "";
|
|
31
|
+
return url.origin;
|
|
32
|
+
}
|
|
33
|
+
function buildForgeWebAppUrl(origin, port) {
|
|
34
|
+
return `${buildForgeBaseUrl(origin, port)}/forge/`;
|
|
35
|
+
}
|
|
20
36
|
function getRuntimeStatePath(config) {
|
|
21
37
|
const origin = new URL(config.origin).hostname.toLowerCase().replace(/[^a-z0-9._-]+/g, "-");
|
|
22
|
-
return path.join(homedir(), ".openclaw", "run",
|
|
38
|
+
return path.join(homedir(), ".openclaw", "run", FORGE_PLUGIN_ID, `${origin}-${config.port}.json`);
|
|
39
|
+
}
|
|
40
|
+
function getPreferredPortStatePath(origin) {
|
|
41
|
+
const hostname = new URL(origin).hostname.toLowerCase().replace(/[^a-z0-9._-]+/g, "-");
|
|
42
|
+
return path.join(homedir(), ".openclaw", "run", FORGE_PLUGIN_ID, `${hostname}-preferred-port.json`);
|
|
43
|
+
}
|
|
44
|
+
function applyPortToConfig(config, port, portSource) {
|
|
45
|
+
config.port = port;
|
|
46
|
+
config.baseUrl = buildForgeBaseUrl(config.origin, port);
|
|
47
|
+
config.webAppUrl = buildForgeWebAppUrl(config.origin, port);
|
|
48
|
+
config.portSource = portSource;
|
|
49
|
+
}
|
|
50
|
+
function getExpectedDataRoot(config) {
|
|
51
|
+
return config.dataRoot.trim().length > 0 ? path.resolve(config.dataRoot) : null;
|
|
52
|
+
}
|
|
53
|
+
function isExpectedDataRoot(expectedDataRoot, actualDataRoot) {
|
|
54
|
+
if (!expectedDataRoot) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
if (!actualDataRoot) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
return path.resolve(actualDataRoot) === expectedDataRoot;
|
|
61
|
+
}
|
|
62
|
+
function formatRuntimeDataRootMismatch(config, expectedDataRoot, actualDataRoot) {
|
|
63
|
+
return [
|
|
64
|
+
`Forge is already responding on ${config.baseUrl}, but it is using storage root ${actualDataRoot ?? "(unknown)"}.`,
|
|
65
|
+
`The OpenClaw plugin is configured to use ${expectedDataRoot}.`,
|
|
66
|
+
"Restart the plugin-managed runtime or stop the conflicting Forge server so the configured dataRoot can take over."
|
|
67
|
+
].join(" ");
|
|
68
|
+
}
|
|
69
|
+
async function writePreferredPortState(config, port) {
|
|
70
|
+
const statePath = getPreferredPortStatePath(config.origin);
|
|
71
|
+
await mkdir(path.dirname(statePath), { recursive: true });
|
|
72
|
+
await writeFile(statePath, `${JSON.stringify({ origin: config.origin, port, updatedAt: new Date().toISOString() }, null, 2)}\n`, "utf8");
|
|
73
|
+
}
|
|
74
|
+
async function isPortAvailable(host, port) {
|
|
75
|
+
return await new Promise((resolve) => {
|
|
76
|
+
const server = net.createServer();
|
|
77
|
+
server.unref();
|
|
78
|
+
server.once("error", (error) => {
|
|
79
|
+
resolve(error.code !== "EADDRINUSE");
|
|
80
|
+
});
|
|
81
|
+
server.listen({ host, port, exclusive: true }, () => {
|
|
82
|
+
server.close(() => resolve(true));
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
async function findAvailableLocalPort(host, startPort) {
|
|
87
|
+
for (let candidate = Math.max(1, startPort), attempts = 0; candidate <= 65_535 && attempts < MAX_PORT_SCAN_ATTEMPTS; candidate += 1, attempts += 1) {
|
|
88
|
+
if (await isPortAvailable(host, candidate)) {
|
|
89
|
+
return candidate;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
async function relocateLocalRuntimePort(config) {
|
|
95
|
+
if (config.portSource === "configured") {
|
|
96
|
+
throw new Error(`Configured Forge port ${config.port} is already in use on ${new URL(config.origin).hostname}. Set a different plugin port or stop the process using it.`);
|
|
97
|
+
}
|
|
98
|
+
const nextPort = await findAvailableLocalPort("127.0.0.1", config.port + 1);
|
|
99
|
+
if (nextPort === null) {
|
|
100
|
+
throw new Error(`Forge could not find a free localhost port after ${config.port}.`);
|
|
101
|
+
}
|
|
102
|
+
applyPortToConfig(config, nextPort, "preferred");
|
|
103
|
+
await writePreferredPortState(config, nextPort);
|
|
23
104
|
}
|
|
24
105
|
async function writeRuntimeState(config, pid) {
|
|
25
106
|
const statePath = getRuntimeStatePath(config);
|
|
@@ -89,7 +170,7 @@ function getCurrentModuleRoot() {
|
|
|
89
170
|
}
|
|
90
171
|
function getRuntimeLogPath(config) {
|
|
91
172
|
const origin = new URL(config.origin).hostname.toLowerCase().replace(/[^a-z0-9._-]+/g, "-");
|
|
92
|
-
return path.join(homedir(), ".openclaw", "logs",
|
|
173
|
+
return path.join(homedir(), ".openclaw", "logs", FORGE_PLUGIN_ID, `${origin}-${config.port}.log`);
|
|
93
174
|
}
|
|
94
175
|
function openRuntimeLogFile(logPath) {
|
|
95
176
|
mkdirSync(path.dirname(logPath), { recursive: true });
|
|
@@ -210,7 +291,44 @@ async function isForgeHealthy(config, timeoutMs) {
|
|
|
210
291
|
clearTimeout(timeout);
|
|
211
292
|
}
|
|
212
293
|
}
|
|
213
|
-
function
|
|
294
|
+
async function probeForgeRuntime(config, timeoutMs) {
|
|
295
|
+
const controller = new AbortController();
|
|
296
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
297
|
+
try {
|
|
298
|
+
const response = await fetch(new URL("/api/v1/health", config.baseUrl), {
|
|
299
|
+
method: "GET",
|
|
300
|
+
headers: {
|
|
301
|
+
accept: "application/json",
|
|
302
|
+
"x-forge-runtime-probe": "1"
|
|
303
|
+
},
|
|
304
|
+
signal: controller.signal
|
|
305
|
+
});
|
|
306
|
+
if (!response.ok) {
|
|
307
|
+
return { healthy: false, pid: null, storageRoot: null, basePath: null };
|
|
308
|
+
}
|
|
309
|
+
const payload = (await response.json());
|
|
310
|
+
return {
|
|
311
|
+
healthy: true,
|
|
312
|
+
pid: typeof payload.runtime?.pid === "number" && Number.isFinite(payload.runtime.pid) ? Math.trunc(payload.runtime.pid) : null,
|
|
313
|
+
storageRoot: typeof payload.runtime?.storageRoot === "string" ? path.resolve(payload.runtime.storageRoot) : null,
|
|
314
|
+
basePath: typeof payload.runtime?.basePath === "string" ? payload.runtime.basePath : null
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
return { healthy: false, pid: null, storageRoot: null, basePath: null };
|
|
319
|
+
}
|
|
320
|
+
finally {
|
|
321
|
+
clearTimeout(timeout);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
async function adoptManagedRuntimeState(config, probe) {
|
|
325
|
+
if (probe.pid === null || !processExists(probe.pid)) {
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
await writeRuntimeState(config, probe.pid);
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
331
|
+
async function spawnManagedRuntime(config, plan) {
|
|
214
332
|
const isPackagedServer = isPackagedServerPlan(plan);
|
|
215
333
|
const args = isPackagedServer ? [plan.entryFile] : [plan.entryFile, path.join(plan.packageRoot, "server", "src", "index.ts")];
|
|
216
334
|
const logPath = getRuntimeLogPath(config);
|
|
@@ -246,9 +364,20 @@ function spawnManagedRuntime(config, plan) {
|
|
|
246
364
|
});
|
|
247
365
|
managedRuntimeChild = child;
|
|
248
366
|
managedRuntimeKey = runtimeKey(config);
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
}
|
|
367
|
+
try {
|
|
368
|
+
await writeRuntimeState(config, child.pid);
|
|
369
|
+
}
|
|
370
|
+
catch (error) {
|
|
371
|
+
managedRuntimeChild = null;
|
|
372
|
+
managedRuntimeKey = null;
|
|
373
|
+
try {
|
|
374
|
+
process.kill(child.pid, "SIGTERM");
|
|
375
|
+
}
|
|
376
|
+
catch {
|
|
377
|
+
// If the child already exited we still want to surface the state-write failure.
|
|
378
|
+
}
|
|
379
|
+
throw new Error(`Forge local runtime started on ${config.baseUrl}, but the plugin could not persist its state. ${error instanceof Error ? error.message : String(error)}`);
|
|
380
|
+
}
|
|
252
381
|
}
|
|
253
382
|
function formatRuntimeFailure(details, config) {
|
|
254
383
|
if (!details) {
|
|
@@ -280,11 +409,47 @@ export async function ensureForgeRuntimeReady(config) {
|
|
|
280
409
|
if (!isLocalOrigin(config.origin)) {
|
|
281
410
|
return;
|
|
282
411
|
}
|
|
283
|
-
|
|
412
|
+
const expectedDataRoot = getExpectedDataRoot(config);
|
|
413
|
+
const initialProbe = await probeForgeRuntime(config, HEALTHCHECK_TIMEOUT_MS);
|
|
414
|
+
if (initialProbe.healthy && isExpectedDataRoot(expectedDataRoot, initialProbe.storageRoot)) {
|
|
415
|
+
const existingState = await readRuntimeState(config);
|
|
416
|
+
if (!existingState) {
|
|
417
|
+
await adoptManagedRuntimeState(config, initialProbe);
|
|
418
|
+
}
|
|
284
419
|
return;
|
|
285
420
|
}
|
|
421
|
+
const savedState = await readRuntimeState(config);
|
|
422
|
+
if (savedState && !processExists(savedState.pid)) {
|
|
423
|
+
await clearRuntimeState(config);
|
|
424
|
+
}
|
|
425
|
+
else if (savedState && processExists(savedState.pid)) {
|
|
426
|
+
if (initialProbe.healthy && !isExpectedDataRoot(expectedDataRoot, initialProbe.storageRoot)) {
|
|
427
|
+
await stopForgeRuntime(config);
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
try {
|
|
431
|
+
await waitForRuntime(config, EXISTING_RUNTIME_GRACE_MS, null);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
catch {
|
|
435
|
+
await stopForgeRuntime(config);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
else if (initialProbe.healthy) {
|
|
440
|
+
if (!isExpectedDataRoot(expectedDataRoot, initialProbe.storageRoot)) {
|
|
441
|
+
throw new Error(formatRuntimeDataRootMismatch(config, expectedDataRoot, initialProbe.storageRoot));
|
|
442
|
+
}
|
|
443
|
+
try {
|
|
444
|
+
await waitForRuntime(config, EXISTING_RUNTIME_GRACE_MS, null);
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
catch {
|
|
448
|
+
// There is no plugin-managed pid to stop here; fall through into normal startup handling.
|
|
449
|
+
}
|
|
450
|
+
}
|
|
286
451
|
const key = runtimeKey(config);
|
|
287
|
-
if (startupPromise &&
|
|
452
|
+
if (startupPromise && (startupRuntimeKey === null || startupRuntimeKey === key)) {
|
|
288
453
|
return startupPromise;
|
|
289
454
|
}
|
|
290
455
|
const plan = resolveLaunchPlan();
|
|
@@ -292,16 +457,31 @@ export async function ensureForgeRuntimeReady(config) {
|
|
|
292
457
|
return;
|
|
293
458
|
}
|
|
294
459
|
startupPromise = (async () => {
|
|
295
|
-
|
|
460
|
+
const probeBeforeStart = await probeForgeRuntime(config, HEALTHCHECK_TIMEOUT_MS);
|
|
461
|
+
if (probeBeforeStart.healthy && isExpectedDataRoot(expectedDataRoot, probeBeforeStart.storageRoot)) {
|
|
296
462
|
return;
|
|
297
463
|
}
|
|
464
|
+
startupRuntimeKey = runtimeKey(config);
|
|
465
|
+
if (!(await isPortAvailable("127.0.0.1", config.port))) {
|
|
466
|
+
await relocateLocalRuntimePort(config);
|
|
467
|
+
startupRuntimeKey = runtimeKey(config);
|
|
468
|
+
const probeAfterRelocation = await probeForgeRuntime(config, HEALTHCHECK_TIMEOUT_MS);
|
|
469
|
+
if (probeAfterRelocation.healthy && isExpectedDataRoot(expectedDataRoot, probeAfterRelocation.storageRoot)) {
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
298
473
|
await ensurePackagedRuntimeDependencies(plan, config);
|
|
299
474
|
if (!managedRuntimeChild || managedRuntimeKey !== key || managedRuntimeChild.killed) {
|
|
300
|
-
spawnManagedRuntime(config, plan);
|
|
475
|
+
await spawnManagedRuntime(config, plan);
|
|
301
476
|
}
|
|
302
477
|
await waitForRuntime(config, STARTUP_TIMEOUT_MS, managedRuntimeChild?.pid ?? null);
|
|
478
|
+
const probeAfterStart = await probeForgeRuntime(config, HEALTHCHECK_TIMEOUT_MS);
|
|
479
|
+
if (!probeAfterStart.healthy || !isExpectedDataRoot(expectedDataRoot, probeAfterStart.storageRoot)) {
|
|
480
|
+
throw new Error(formatRuntimeDataRootMismatch(config, expectedDataRoot, probeAfterStart.storageRoot));
|
|
481
|
+
}
|
|
303
482
|
})().finally(() => {
|
|
304
483
|
startupPromise = null;
|
|
484
|
+
startupRuntimeKey = null;
|
|
305
485
|
});
|
|
306
486
|
return startupPromise;
|
|
307
487
|
}
|
|
@@ -316,8 +496,26 @@ export async function startForgeRuntime(config) {
|
|
|
316
496
|
baseUrl: config.baseUrl
|
|
317
497
|
};
|
|
318
498
|
}
|
|
319
|
-
const
|
|
320
|
-
|
|
499
|
+
const expectedDataRoot = getExpectedDataRoot(config);
|
|
500
|
+
const probe = await probeForgeRuntime(config, HEALTHCHECK_TIMEOUT_MS);
|
|
501
|
+
let existingState = await readRuntimeState(config);
|
|
502
|
+
if (!existingState && probe.healthy && isExpectedDataRoot(expectedDataRoot, probe.storageRoot)) {
|
|
503
|
+
const adopted = await adoptManagedRuntimeState(config, probe);
|
|
504
|
+
if (adopted) {
|
|
505
|
+
existingState = await readRuntimeState(config);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
if (probe.healthy && !isExpectedDataRoot(expectedDataRoot, probe.storageRoot)) {
|
|
509
|
+
return {
|
|
510
|
+
ok: false,
|
|
511
|
+
started: false,
|
|
512
|
+
managed: Boolean(existingState),
|
|
513
|
+
message: formatRuntimeDataRootMismatch(config, expectedDataRoot, probe.storageRoot),
|
|
514
|
+
pid: existingState?.pid ?? null,
|
|
515
|
+
baseUrl: config.baseUrl
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
if (!existingState && probe.healthy) {
|
|
321
519
|
return {
|
|
322
520
|
ok: true,
|
|
323
521
|
started: false,
|
|
@@ -327,7 +525,7 @@ export async function startForgeRuntime(config) {
|
|
|
327
525
|
baseUrl: config.baseUrl
|
|
328
526
|
};
|
|
329
527
|
}
|
|
330
|
-
if (existingState && processExists(existingState.pid) &&
|
|
528
|
+
if (existingState && processExists(existingState.pid) && probe.healthy) {
|
|
331
529
|
return {
|
|
332
530
|
ok: true,
|
|
333
531
|
started: false,
|
|
@@ -422,8 +620,16 @@ export async function stopForgeRuntime(config) {
|
|
|
422
620
|
};
|
|
423
621
|
}
|
|
424
622
|
export async function getForgeRuntimeStatus(config) {
|
|
425
|
-
const
|
|
426
|
-
const
|
|
623
|
+
const expectedDataRoot = getExpectedDataRoot(config);
|
|
624
|
+
const probe = await probeForgeRuntime(config, HEALTHCHECK_TIMEOUT_MS);
|
|
625
|
+
const healthy = probe.healthy;
|
|
626
|
+
let state = await readRuntimeState(config);
|
|
627
|
+
if (!state && healthy && isExpectedDataRoot(expectedDataRoot, probe.storageRoot)) {
|
|
628
|
+
const adopted = await adoptManagedRuntimeState(config, probe);
|
|
629
|
+
if (adopted) {
|
|
630
|
+
state = await readRuntimeState(config);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
427
633
|
const pid = state?.pid ?? null;
|
|
428
634
|
const managed = Boolean(state);
|
|
429
635
|
const running = healthy || (pid !== null && processExists(pid));
|
|
@@ -453,6 +659,17 @@ export async function getForgeRuntimeStatus(config) {
|
|
|
453
659
|
};
|
|
454
660
|
}
|
|
455
661
|
if (healthy && managed) {
|
|
662
|
+
if (!isExpectedDataRoot(expectedDataRoot, probe.storageRoot)) {
|
|
663
|
+
return {
|
|
664
|
+
ok: false,
|
|
665
|
+
running: true,
|
|
666
|
+
healthy: true,
|
|
667
|
+
managed: true,
|
|
668
|
+
message: formatRuntimeDataRootMismatch(config, expectedDataRoot, probe.storageRoot),
|
|
669
|
+
pid,
|
|
670
|
+
baseUrl: config.baseUrl
|
|
671
|
+
};
|
|
672
|
+
}
|
|
456
673
|
return {
|
|
457
674
|
ok: true,
|
|
458
675
|
running: true,
|
|
@@ -464,6 +681,17 @@ export async function getForgeRuntimeStatus(config) {
|
|
|
464
681
|
};
|
|
465
682
|
}
|
|
466
683
|
if (healthy) {
|
|
684
|
+
if (!isExpectedDataRoot(expectedDataRoot, probe.storageRoot)) {
|
|
685
|
+
return {
|
|
686
|
+
ok: false,
|
|
687
|
+
running: true,
|
|
688
|
+
healthy: true,
|
|
689
|
+
managed: false,
|
|
690
|
+
message: formatRuntimeDataRootMismatch(config, expectedDataRoot, probe.storageRoot),
|
|
691
|
+
pid: null,
|
|
692
|
+
baseUrl: config.baseUrl
|
|
693
|
+
};
|
|
694
|
+
}
|
|
467
695
|
return {
|
|
468
696
|
ok: true,
|
|
469
697
|
running: true,
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
1
4
|
import { buildForgeBaseUrl, buildForgeWebAppUrl } from "./api-client.js";
|
|
2
5
|
import { primeForgeRuntime } from "./local-runtime.js";
|
|
3
6
|
import { registerForgePluginCli, registerForgePluginRoutes } from "./routes.js";
|
|
@@ -7,6 +10,7 @@ export const FORGE_PLUGIN_NAME = "Forge";
|
|
|
7
10
|
export const FORGE_PLUGIN_DESCRIPTION = "Curated OpenClaw adapter for the Forge collaboration API, UI entrypoint, and localhost auto-start runtime.";
|
|
8
11
|
export const DEFAULT_FORGE_ORIGIN = "http://127.0.0.1";
|
|
9
12
|
export const DEFAULT_FORGE_PORT = 4317;
|
|
13
|
+
const LOCAL_HOSTNAMES = new Set(["127.0.0.1", "localhost", "::1"]);
|
|
10
14
|
function normalizeString(value, fallback) {
|
|
11
15
|
return typeof value === "string" && value.trim().length > 0 ? value.trim() : fallback;
|
|
12
16
|
}
|
|
@@ -36,16 +40,53 @@ function normalizeTimeout(value, fallback) {
|
|
|
36
40
|
}
|
|
37
41
|
return Math.min(120_000, Math.max(1000, Math.round(value)));
|
|
38
42
|
}
|
|
43
|
+
function normalizeDataRoot(value) {
|
|
44
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
45
|
+
return "";
|
|
46
|
+
}
|
|
47
|
+
return path.resolve(value.trim());
|
|
48
|
+
}
|
|
49
|
+
function isLocalOrigin(origin) {
|
|
50
|
+
try {
|
|
51
|
+
return LOCAL_HOSTNAMES.has(new URL(origin).hostname.toLowerCase());
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function getPreferredLocalPortPath(origin) {
|
|
58
|
+
const hostname = new URL(origin).hostname.toLowerCase().replace(/[^a-z0-9._-]+/g, "-");
|
|
59
|
+
return path.join(homedir(), ".openclaw", "run", FORGE_PLUGIN_ID, `${hostname}-preferred-port.json`);
|
|
60
|
+
}
|
|
61
|
+
function readPreferredLocalPort(origin) {
|
|
62
|
+
if (!isLocalOrigin(origin)) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
const preferredPortPath = getPreferredLocalPortPath(origin);
|
|
67
|
+
if (!existsSync(preferredPortPath)) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
const payload = JSON.parse(readFileSync(preferredPortPath, "utf8"));
|
|
71
|
+
return typeof payload.port === "number" && Number.isFinite(payload.port) ? payload.port : null;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
39
77
|
export function resolveForgePluginConfig(pluginConfig) {
|
|
40
78
|
const raw = (pluginConfig ?? {});
|
|
41
79
|
const origin = normalizeOrigin(raw.origin, DEFAULT_FORGE_ORIGIN);
|
|
42
|
-
const
|
|
80
|
+
const hasConfiguredPort = typeof raw.port === "number" && Number.isFinite(raw.port);
|
|
81
|
+
const preferredPort = hasConfiguredPort ? null : readPreferredLocalPort(origin);
|
|
82
|
+
const port = normalizePort(hasConfiguredPort ? raw.port : preferredPort ?? DEFAULT_FORGE_PORT, DEFAULT_FORGE_PORT);
|
|
43
83
|
return {
|
|
44
84
|
origin,
|
|
45
85
|
port,
|
|
46
86
|
baseUrl: buildForgeBaseUrl(origin, port),
|
|
47
87
|
webAppUrl: buildForgeWebAppUrl(origin, port),
|
|
48
|
-
|
|
88
|
+
portSource: hasConfiguredPort ? "configured" : preferredPort !== null ? "preferred" : "default",
|
|
89
|
+
dataRoot: normalizeDataRoot(raw.dataRoot),
|
|
49
90
|
apiToken: typeof raw.apiToken === "string" ? raw.apiToken.trim() : "",
|
|
50
91
|
actorLabel: normalizeString(raw.actorLabel, "aurel"),
|
|
51
92
|
timeoutMs: normalizeTimeout(raw.timeoutMs, 15_000)
|
|
@@ -69,7 +110,7 @@ export const forgePluginConfigSchema = {
|
|
|
69
110
|
default: DEFAULT_FORGE_PORT,
|
|
70
111
|
minimum: 1,
|
|
71
112
|
maximum: 65535,
|
|
72
|
-
description: "Forge server port. Override this when
|
|
113
|
+
description: "Forge server port. Override this only when you want to pin a specific port. Default localhost installs can move to the next free port automatically if 4317 is already taken."
|
|
73
114
|
},
|
|
74
115
|
dataRoot: {
|
|
75
116
|
type: "string",
|
|
@@ -103,7 +144,7 @@ export const forgePluginConfigSchema = {
|
|
|
103
144
|
},
|
|
104
145
|
port: {
|
|
105
146
|
label: "Forge Port",
|
|
106
|
-
help: "Forge server port. Change this
|
|
147
|
+
help: "Forge server port. Change this only when you want to pin a specific port. Default localhost installs can move to the next free port automatically if 4317 is busy.",
|
|
107
148
|
placeholder: "4317"
|
|
108
149
|
},
|
|
109
150
|
dataRoot: {
|
package/dist/openclaw/tools.js
CHANGED
|
@@ -238,6 +238,21 @@ export function registerForgePluginTools(api, config) {
|
|
|
238
238
|
method: "POST",
|
|
239
239
|
path: "/api/v1/entities/restore"
|
|
240
240
|
});
|
|
241
|
+
registerWriteTool(api, config, {
|
|
242
|
+
name: "forge_grant_reward_bonus",
|
|
243
|
+
label: "Forge Grant Reward Bonus",
|
|
244
|
+
description: "Grant an explicit manual XP bonus or penalty with provenance. Use only for auditable operator judgement beyond the normal task-run and habit reward flows.",
|
|
245
|
+
parameters: Type.Object({
|
|
246
|
+
entityType: Type.String({ minLength: 1 }),
|
|
247
|
+
entityId: Type.String({ minLength: 1 }),
|
|
248
|
+
deltaXp: Type.Number(),
|
|
249
|
+
reasonTitle: Type.String({ minLength: 1 }),
|
|
250
|
+
reasonSummary: optionalString(),
|
|
251
|
+
metadata: Type.Optional(Type.Record(Type.String(), Type.Any()))
|
|
252
|
+
}),
|
|
253
|
+
method: "POST",
|
|
254
|
+
path: "/api/v1/rewards/bonus"
|
|
255
|
+
});
|
|
241
256
|
registerWriteTool(api, config, {
|
|
242
257
|
name: "forge_post_insight",
|
|
243
258
|
label: "Forge Post Insight",
|