forge-openclaw-plugin 0.2.10 → 0.2.11
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
CHANGED
|
@@ -202,6 +202,14 @@ The skill is entity-format-driven. It teaches the agent how to:
|
|
|
202
202
|
|
|
203
203
|
For local use, set the plugin origin to `http://127.0.0.1` or `http://localhost` and the plugin will bring Forge up on the configured port automatically.
|
|
204
204
|
|
|
205
|
+
If you want to stop that plugin-managed local runtime cleanly, use:
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
forge stop
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
This only stops the runtime when it was auto-started by the OpenClaw plugin. If Forge was started manually some other way, `forge stop` tells you that instead of killing unrelated processes.
|
|
212
|
+
|
|
205
213
|
## Publishing and listing
|
|
206
214
|
|
|
207
215
|
The reliable publication path for the Forge plugin is:
|
|
@@ -1,3 +1,11 @@
|
|
|
1
1
|
import type { ForgePluginConfig } from "./api-client.js";
|
|
2
|
+
export type ForgeRuntimeStopResult = {
|
|
3
|
+
ok: boolean;
|
|
4
|
+
stopped: boolean;
|
|
5
|
+
managed: boolean;
|
|
6
|
+
message: string;
|
|
7
|
+
pid: number | null;
|
|
8
|
+
};
|
|
2
9
|
export declare function ensureForgeRuntimeReady(config: ForgePluginConfig): Promise<void>;
|
|
3
10
|
export declare function primeForgeRuntime(config: ForgePluginConfig): void;
|
|
11
|
+
export declare function stopForgeRuntime(config: ForgePluginConfig): Promise<ForgeRuntimeStopResult>;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
5
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
4
6
|
import { fileURLToPath } from "node:url";
|
|
5
7
|
const LOCAL_HOSTNAMES = new Set(["127.0.0.1", "localhost", "::1"]);
|
|
6
8
|
const STARTUP_TIMEOUT_MS = 15_000;
|
|
@@ -12,6 +14,63 @@ let startupPromise = null;
|
|
|
12
14
|
function runtimeKey(config) {
|
|
13
15
|
return `${config.origin}:${config.port}`;
|
|
14
16
|
}
|
|
17
|
+
function getRuntimeStatePath(config) {
|
|
18
|
+
const origin = new URL(config.origin).hostname.toLowerCase().replace(/[^a-z0-9._-]+/g, "-");
|
|
19
|
+
return path.join(homedir(), ".openclaw", "run", "forge-openclaw-plugin", `${origin}-${config.port}.json`);
|
|
20
|
+
}
|
|
21
|
+
async function writeRuntimeState(config, pid) {
|
|
22
|
+
const statePath = getRuntimeStatePath(config);
|
|
23
|
+
await mkdir(path.dirname(statePath), { recursive: true });
|
|
24
|
+
const payload = {
|
|
25
|
+
pid,
|
|
26
|
+
origin: config.origin,
|
|
27
|
+
port: config.port,
|
|
28
|
+
baseUrl: config.baseUrl,
|
|
29
|
+
startedAt: new Date().toISOString()
|
|
30
|
+
};
|
|
31
|
+
await writeFile(statePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
32
|
+
}
|
|
33
|
+
async function clearRuntimeState(config) {
|
|
34
|
+
await rm(getRuntimeStatePath(config), { force: true });
|
|
35
|
+
}
|
|
36
|
+
async function readRuntimeState(config) {
|
|
37
|
+
try {
|
|
38
|
+
const payload = await readFile(getRuntimeStatePath(config), "utf8");
|
|
39
|
+
const parsed = JSON.parse(payload);
|
|
40
|
+
if (typeof parsed.pid !== "number" || !Number.isFinite(parsed.pid)) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
pid: Math.trunc(parsed.pid),
|
|
45
|
+
origin: typeof parsed.origin === "string" ? parsed.origin : config.origin,
|
|
46
|
+
port: typeof parsed.port === "number" ? parsed.port : config.port,
|
|
47
|
+
baseUrl: typeof parsed.baseUrl === "string" ? parsed.baseUrl : config.baseUrl,
|
|
48
|
+
startedAt: typeof parsed.startedAt === "string" ? parsed.startedAt : new Date(0).toISOString()
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function processExists(pid) {
|
|
56
|
+
try {
|
|
57
|
+
process.kill(pid, 0);
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
return !(error instanceof Error) || !("code" in error) || error.code !== "ESRCH";
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async function waitForProcessExit(pid, timeoutMs) {
|
|
65
|
+
const deadline = Date.now() + timeoutMs;
|
|
66
|
+
while (Date.now() < deadline) {
|
|
67
|
+
if (!processExists(pid)) {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
await new Promise((resolve) => setTimeout(resolve, HEALTHCHECK_INTERVAL_MS));
|
|
71
|
+
}
|
|
72
|
+
return !processExists(pid);
|
|
73
|
+
}
|
|
15
74
|
function isLocalOrigin(origin) {
|
|
16
75
|
try {
|
|
17
76
|
return LOCAL_HOSTNAMES.has(new URL(origin).hostname.toLowerCase());
|
|
@@ -87,9 +146,13 @@ function spawnManagedRuntime(config, plan) {
|
|
|
87
146
|
managedRuntimeChild = null;
|
|
88
147
|
managedRuntimeKey = null;
|
|
89
148
|
}
|
|
149
|
+
void clearRuntimeState(config);
|
|
90
150
|
});
|
|
91
151
|
managedRuntimeChild = child;
|
|
92
152
|
managedRuntimeKey = runtimeKey(config);
|
|
153
|
+
void writeRuntimeState(config, child.pid).catch(() => {
|
|
154
|
+
// State tracking is best effort. Runtime health checks remain authoritative.
|
|
155
|
+
});
|
|
93
156
|
}
|
|
94
157
|
async function waitForRuntime(config, timeoutMs) {
|
|
95
158
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -134,3 +197,61 @@ export function primeForgeRuntime(config) {
|
|
|
134
197
|
// Keep plugin registration non-blocking. Failures surface on first real call.
|
|
135
198
|
});
|
|
136
199
|
}
|
|
200
|
+
export async function stopForgeRuntime(config) {
|
|
201
|
+
if (!isLocalOrigin(config.origin)) {
|
|
202
|
+
return {
|
|
203
|
+
ok: false,
|
|
204
|
+
stopped: false,
|
|
205
|
+
managed: false,
|
|
206
|
+
message: "Forge stop only supports local plugin-managed runtimes. Remote Forge targets must be stopped where they are hosted.",
|
|
207
|
+
pid: null
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
const state = await readRuntimeState(config);
|
|
211
|
+
if (!state) {
|
|
212
|
+
return {
|
|
213
|
+
ok: true,
|
|
214
|
+
stopped: false,
|
|
215
|
+
managed: false,
|
|
216
|
+
message: (await isForgeHealthy(config, HEALTHCHECK_TIMEOUT_MS))
|
|
217
|
+
? "Forge is running, but it does not look like a plugin-managed runtime. Stop it where it was started."
|
|
218
|
+
: "Forge is not running through the plugin-managed local runtime.",
|
|
219
|
+
pid: null
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
if (!processExists(state.pid)) {
|
|
223
|
+
await clearRuntimeState(config);
|
|
224
|
+
return {
|
|
225
|
+
ok: true,
|
|
226
|
+
stopped: false,
|
|
227
|
+
managed: true,
|
|
228
|
+
message: "The saved Forge runtime PID was stale. The plugin-managed runtime is already stopped.",
|
|
229
|
+
pid: state.pid
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
process.kill(state.pid, "SIGTERM");
|
|
233
|
+
if (!(await waitForProcessExit(state.pid, 5_000))) {
|
|
234
|
+
process.kill(state.pid, "SIGKILL");
|
|
235
|
+
if (!(await waitForProcessExit(state.pid, 2_000))) {
|
|
236
|
+
return {
|
|
237
|
+
ok: false,
|
|
238
|
+
stopped: false,
|
|
239
|
+
managed: true,
|
|
240
|
+
message: `Forge runtime pid ${state.pid} did not stop cleanly.`,
|
|
241
|
+
pid: state.pid
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (managedRuntimeChild?.pid === state.pid) {
|
|
246
|
+
managedRuntimeChild = null;
|
|
247
|
+
managedRuntimeKey = null;
|
|
248
|
+
}
|
|
249
|
+
await clearRuntimeState(config);
|
|
250
|
+
return {
|
|
251
|
+
ok: true,
|
|
252
|
+
stopped: true,
|
|
253
|
+
managed: true,
|
|
254
|
+
message: `Stopped the plugin-managed Forge runtime on ${config.baseUrl}.`,
|
|
255
|
+
pid: state.pid
|
|
256
|
+
};
|
|
257
|
+
}
|
package/dist/openclaw/routes.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { canBootstrapOperatorSession, callConfiguredForgeApi, expectForgeSuccess, readJsonRequestBody, readSingleHeaderValue, requireApiToken, writeForgeProxyResponse, writePluginError, writeRedirectResponse } from "./api-client.js";
|
|
2
|
+
import { stopForgeRuntime } from "./local-runtime.js";
|
|
2
3
|
import { collectSupportedPluginApiRouteKeys, makeApiRouteKey } from "./parity.js";
|
|
3
4
|
function passthroughSearch(path, url) {
|
|
4
5
|
return `${path}${url.search}`;
|
|
@@ -374,6 +375,9 @@ export function registerForgePluginCli(api, config) {
|
|
|
374
375
|
command.command("ui").description("Print the Forge UI entrypoint").action(async () => {
|
|
375
376
|
console.log(JSON.stringify({ webAppUrl: await resolveForgeUiUrl(config), pluginUiRoute: "/forge/v1/ui" }, null, 2));
|
|
376
377
|
});
|
|
378
|
+
command.command("stop").description("Stop the local Forge runtime when it was auto-started by the OpenClaw plugin").action(async () => {
|
|
379
|
+
console.log(JSON.stringify(await stopForgeRuntime(config), null, 2));
|
|
380
|
+
});
|
|
377
381
|
command.command("doctor").description("Run plugin connectivity and curated route diagnostics").action(async () => {
|
|
378
382
|
console.log(JSON.stringify(await runDoctor(config), null, 2));
|
|
379
383
|
});
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -16,6 +16,8 @@ Forge data location rule:
|
|
|
16
16
|
- on a linked repo-local install, this usually means `<repo>/openclaw-plugin/data/forge.sqlite`
|
|
17
17
|
- if the user wants the data somewhere else for persistence, backup, or manual control, tell them to set `plugins.entries["forge-openclaw-plugin"].config.dataRoot` and restart the OpenClaw gateway
|
|
18
18
|
- if the user asks where the data is stored or how to move it, explain the current default plainly and show the exact config field
|
|
19
|
+
- if the user wants to stop a plugin-managed local Forge runtime cleanly, tell them to run `forge stop`
|
|
20
|
+
- `forge stop` only shuts down a runtime that the OpenClaw plugin auto-started itself; if Forge was started manually elsewhere, it will say so instead of killing random local processes
|
|
19
21
|
|
|
20
22
|
Use these exact entity meanings when deciding what the user is describing.
|
|
21
23
|
|