forge-openclaw-plugin 0.2.12 → 0.2.15

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
@@ -201,7 +201,7 @@ The skill is entity-format-driven. It teaches the agent how to:
201
201
  - ask only for missing fields
202
202
  - capture goals, projects, tasks, values, patterns, behaviors, beliefs, and trigger reports
203
203
 
204
- 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
+ For local use, set the plugin origin to `http://127.0.0.1` or `http://localhost` and the plugin will bring Forge up automatically. If you leave the default localhost setup alone and `4317` is already taken, Forge now moves to the next free local port and remembers that choice for future runs.
205
205
 
206
206
  If you want to manage that plugin-managed local runtime cleanly, use:
207
207
 
@@ -214,6 +214,16 @@ openclaw forge status
214
214
 
215
215
  These commands only manage the runtime when it was auto-started by the OpenClaw plugin. If Forge was started manually some other way, they tell you that instead of killing unrelated processes.
216
216
 
217
+ If the local runtime fails to come up, check the plugin-managed runtime log at:
218
+
219
+ ```bash
220
+ ~/.openclaw/logs/forge-openclaw-plugin/<host>-<port>.log
221
+ ```
222
+
223
+ On clean installs, the plugin now also repairs missing bundled runtime dependencies on first local start before it launches Forge.
224
+
225
+ The startup error now points at that log file when the child process exits before Forge becomes healthy.
226
+
217
227
  ## Publishing and listing
218
228
 
219
229
  The reliable publication path for the Forge plugin is:
@@ -5,6 +5,7 @@ export type ForgePluginConfig = {
5
5
  port: number;
6
6
  baseUrl: string;
7
7
  webAppUrl: string;
8
+ portSource: "configured" | "default" | "preferred";
8
9
  dataRoot: string;
9
10
  apiToken: string;
10
11
  actorLabel: string;
@@ -1,5 +1,6 @@
1
1
  import { spawn } from "node:child_process";
2
- import { existsSync } from "node:fs";
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,15 +9,79 @@ 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;
17
+ let managedRuntimeLogPath = null;
18
+ let lastRuntimeExitDetails = null;
13
19
  let startupPromise = null;
20
+ let startupRuntimeKey = null;
21
+ const dependencyInstallPromises = new Map();
14
22
  function runtimeKey(config) {
15
23
  return `${config.origin}:${config.port}`;
16
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
+ }
17
36
  function getRuntimeStatePath(config) {
18
37
  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`);
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
+ async function writePreferredPortState(config, port) {
51
+ const statePath = getPreferredPortStatePath(config.origin);
52
+ await mkdir(path.dirname(statePath), { recursive: true });
53
+ await writeFile(statePath, `${JSON.stringify({ origin: config.origin, port, updatedAt: new Date().toISOString() }, null, 2)}\n`, "utf8");
54
+ }
55
+ async function isPortAvailable(host, port) {
56
+ return await new Promise((resolve) => {
57
+ const server = net.createServer();
58
+ server.unref();
59
+ server.once("error", (error) => {
60
+ resolve(error.code !== "EADDRINUSE");
61
+ });
62
+ server.listen({ host, port, exclusive: true }, () => {
63
+ server.close(() => resolve(true));
64
+ });
65
+ });
66
+ }
67
+ async function findAvailableLocalPort(host, startPort) {
68
+ for (let candidate = Math.max(1, startPort), attempts = 0; candidate <= 65_535 && attempts < MAX_PORT_SCAN_ATTEMPTS; candidate += 1, attempts += 1) {
69
+ if (await isPortAvailable(host, candidate)) {
70
+ return candidate;
71
+ }
72
+ }
73
+ return null;
74
+ }
75
+ async function relocateLocalRuntimePort(config) {
76
+ if (config.portSource === "configured") {
77
+ 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.`);
78
+ }
79
+ const nextPort = await findAvailableLocalPort("127.0.0.1", config.port + 1);
80
+ if (nextPort === null) {
81
+ throw new Error(`Forge could not find a free localhost port after ${config.port}.`);
82
+ }
83
+ applyPortToConfig(config, nextPort, "preferred");
84
+ await writePreferredPortState(config, nextPort);
20
85
  }
21
86
  async function writeRuntimeState(config, pid) {
22
87
  const statePath = getRuntimeStatePath(config);
@@ -26,7 +91,8 @@ async function writeRuntimeState(config, pid) {
26
91
  origin: config.origin,
27
92
  port: config.port,
28
93
  baseUrl: config.baseUrl,
29
- startedAt: new Date().toISOString()
94
+ startedAt: new Date().toISOString(),
95
+ logPath: managedRuntimeLogPath
30
96
  };
31
97
  await writeFile(statePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
32
98
  }
@@ -45,7 +111,8 @@ async function readRuntimeState(config) {
45
111
  origin: typeof parsed.origin === "string" ? parsed.origin : config.origin,
46
112
  port: typeof parsed.port === "number" ? parsed.port : config.port,
47
113
  baseUrl: typeof parsed.baseUrl === "string" ? parsed.baseUrl : config.baseUrl,
48
- startedAt: typeof parsed.startedAt === "string" ? parsed.startedAt : new Date(0).toISOString()
114
+ startedAt: typeof parsed.startedAt === "string" ? parsed.startedAt : new Date(0).toISOString(),
115
+ logPath: typeof parsed.logPath === "string" ? parsed.logPath : null
49
116
  };
50
117
  }
51
118
  catch {
@@ -82,6 +149,86 @@ function isLocalOrigin(origin) {
82
149
  function getCurrentModuleRoot() {
83
150
  return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
84
151
  }
152
+ function getRuntimeLogPath(config) {
153
+ const origin = new URL(config.origin).hostname.toLowerCase().replace(/[^a-z0-9._-]+/g, "-");
154
+ return path.join(homedir(), ".openclaw", "logs", FORGE_PLUGIN_ID, `${origin}-${config.port}.log`);
155
+ }
156
+ function openRuntimeLogFile(logPath) {
157
+ mkdirSync(path.dirname(logPath), { recursive: true });
158
+ return openSync(logPath, "a");
159
+ }
160
+ function isPackagedServerPlan(plan) {
161
+ return plan.entryFile.endsWith(path.join("dist", "server", "index.js"));
162
+ }
163
+ function getNpmInvocation() {
164
+ const binDir = path.dirname(process.execPath);
165
+ const npmCli = process.platform === "win32" ? path.join(binDir, "npm.cmd") : path.join(binDir, "npm");
166
+ if (existsSync(npmCli)) {
167
+ return {
168
+ command: process.execPath,
169
+ args: [npmCli]
170
+ };
171
+ }
172
+ return {
173
+ command: "npm",
174
+ args: []
175
+ };
176
+ }
177
+ async function getMissingRuntimeDependencies(packageRoot) {
178
+ const packageJsonPath = path.join(packageRoot, "package.json");
179
+ const packageJson = JSON.parse(await readFile(packageJsonPath, "utf8"));
180
+ const dependencyNames = Object.keys(packageJson.dependencies ?? {});
181
+ return dependencyNames.filter((dependencyName) => !existsSync(path.join(packageRoot, "node_modules", dependencyName, "package.json")));
182
+ }
183
+ async function installMissingRuntimeDependencies(packageRoot, logPath) {
184
+ const { command, args } = getNpmInvocation();
185
+ const logFd = openRuntimeLogFile(logPath);
186
+ try {
187
+ await new Promise((resolve, reject) => {
188
+ const child = spawn(command, [...args, "install", "--omit=dev", "--silent", "--ignore-scripts"], {
189
+ cwd: packageRoot,
190
+ env: process.env,
191
+ stdio: ["ignore", logFd, logFd]
192
+ });
193
+ child.once("error", reject);
194
+ child.once("exit", (code, signal) => {
195
+ if (code === 0) {
196
+ resolve();
197
+ return;
198
+ }
199
+ reject(new Error(`npm dependency install exited with ${signal ? `signal ${signal}` : `code ${code ?? "unknown"}`}`));
200
+ });
201
+ });
202
+ }
203
+ finally {
204
+ closeSync(logFd);
205
+ }
206
+ }
207
+ async function ensurePackagedRuntimeDependencies(plan, config) {
208
+ if (!isPackagedServerPlan(plan)) {
209
+ return;
210
+ }
211
+ const missingDependencies = await getMissingRuntimeDependencies(plan.packageRoot);
212
+ if (missingDependencies.length === 0) {
213
+ return;
214
+ }
215
+ const logPath = getRuntimeLogPath(config);
216
+ managedRuntimeLogPath = logPath;
217
+ const installKey = plan.packageRoot;
218
+ const existingInstall = dependencyInstallPromises.get(installKey);
219
+ if (existingInstall) {
220
+ return existingInstall;
221
+ }
222
+ const installPromise = installMissingRuntimeDependencies(plan.packageRoot, logPath)
223
+ .catch((error) => {
224
+ throw new Error(`Forge runtime dependencies are missing (${missingDependencies.join(", ")}) and automatic install failed. Check logs at ${logPath}. Cause: ${error instanceof Error ? error.message : String(error)}`);
225
+ })
226
+ .finally(() => {
227
+ dependencyInstallPromises.delete(installKey);
228
+ });
229
+ dependencyInstallPromises.set(installKey, installPromise);
230
+ return installPromise;
231
+ }
85
232
  function resolveLaunchPlan() {
86
233
  const moduleRoot = getCurrentModuleRoot();
87
234
  // Published or linked plugin package runtime.
@@ -125,9 +272,11 @@ async function isForgeHealthy(config, timeoutMs) {
125
272
  clearTimeout(timeout);
126
273
  }
127
274
  }
128
- function spawnManagedRuntime(config, plan) {
129
- const isPackagedServer = plan.entryFile.endsWith(path.join("dist", "server", "index.js"));
275
+ async function spawnManagedRuntime(config, plan) {
276
+ const isPackagedServer = isPackagedServerPlan(plan);
130
277
  const args = isPackagedServer ? [plan.entryFile] : [plan.entryFile, path.join(plan.packageRoot, "server", "src", "index.ts")];
278
+ const logPath = getRuntimeLogPath(config);
279
+ const logFd = openRuntimeLogFile(logPath);
131
280
  const child = spawn(process.execPath, args, {
132
281
  cwd: plan.packageRoot,
133
282
  env: {
@@ -137,11 +286,20 @@ function spawnManagedRuntime(config, plan) {
137
286
  FORGE_BASE_PATH: "/forge/",
138
287
  ...(config.dataRoot ? { FORGE_DATA_ROOT: config.dataRoot } : {})
139
288
  },
140
- stdio: "ignore",
289
+ stdio: ["ignore", logFd, logFd],
141
290
  detached: true
142
291
  });
292
+ closeSync(logFd);
143
293
  child.unref();
144
- child.once("exit", () => {
294
+ managedRuntimeLogPath = logPath;
295
+ lastRuntimeExitDetails = null;
296
+ child.once("exit", (code, signal) => {
297
+ lastRuntimeExitDetails = {
298
+ pid: child.pid ?? -1,
299
+ code,
300
+ signal,
301
+ logPath
302
+ };
145
303
  if (managedRuntimeChild === child) {
146
304
  managedRuntimeChild = null;
147
305
  managedRuntimeKey = null;
@@ -150,19 +308,46 @@ function spawnManagedRuntime(config, plan) {
150
308
  });
151
309
  managedRuntimeChild = child;
152
310
  managedRuntimeKey = runtimeKey(config);
153
- void writeRuntimeState(config, child.pid).catch(() => {
154
- // State tracking is best effort. Runtime health checks remain authoritative.
155
- });
311
+ try {
312
+ await writeRuntimeState(config, child.pid);
313
+ }
314
+ catch (error) {
315
+ managedRuntimeChild = null;
316
+ managedRuntimeKey = null;
317
+ try {
318
+ process.kill(child.pid, "SIGTERM");
319
+ }
320
+ catch {
321
+ // If the child already exited we still want to surface the state-write failure.
322
+ }
323
+ 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)}`);
324
+ }
325
+ }
326
+ function formatRuntimeFailure(details, config) {
327
+ if (!details) {
328
+ return `Forge local runtime did not become healthy at ${config.baseUrl} within ${STARTUP_TIMEOUT_MS}ms`;
329
+ }
330
+ const suffix = details.logPath ? ` Check logs at ${details.logPath}.` : "";
331
+ if (details.signal) {
332
+ return `Forge local runtime exited before becoming healthy at ${config.baseUrl} (signal ${details.signal}).${suffix}`;
333
+ }
334
+ if (typeof details.code === "number") {
335
+ return `Forge local runtime exited before becoming healthy at ${config.baseUrl} (code ${details.code}).${suffix}`;
336
+ }
337
+ return `Forge local runtime exited before becoming healthy at ${config.baseUrl}.${suffix}`;
156
338
  }
157
- async function waitForRuntime(config, timeoutMs) {
339
+ async function waitForRuntime(config, timeoutMs, expectedPid) {
158
340
  const deadline = Date.now() + timeoutMs;
159
341
  while (Date.now() < deadline) {
160
342
  if (await isForgeHealthy(config, HEALTHCHECK_TIMEOUT_MS)) {
161
343
  return;
162
344
  }
345
+ if (expectedPid !== null && lastRuntimeExitDetails?.pid === expectedPid) {
346
+ throw new Error(formatRuntimeFailure(lastRuntimeExitDetails, config));
347
+ }
163
348
  await new Promise((resolve) => setTimeout(resolve, HEALTHCHECK_INTERVAL_MS));
164
349
  }
165
- throw new Error(`Forge local runtime did not become healthy at ${config.baseUrl} within ${timeoutMs}ms`);
350
+ throw new Error(formatRuntimeFailure(lastRuntimeExitDetails, config));
166
351
  }
167
352
  export async function ensureForgeRuntimeReady(config) {
168
353
  if (!isLocalOrigin(config.origin)) {
@@ -171,8 +356,21 @@ export async function ensureForgeRuntimeReady(config) {
171
356
  if (await isForgeHealthy(config, HEALTHCHECK_TIMEOUT_MS)) {
172
357
  return;
173
358
  }
359
+ const savedState = await readRuntimeState(config);
360
+ if (savedState && !processExists(savedState.pid)) {
361
+ await clearRuntimeState(config);
362
+ }
363
+ else if (savedState && processExists(savedState.pid)) {
364
+ try {
365
+ await waitForRuntime(config, EXISTING_RUNTIME_GRACE_MS, null);
366
+ return;
367
+ }
368
+ catch {
369
+ await stopForgeRuntime(config);
370
+ }
371
+ }
174
372
  const key = runtimeKey(config);
175
- if (startupPromise && managedRuntimeKey === key) {
373
+ if (startupPromise && (startupRuntimeKey === null || startupRuntimeKey === key)) {
176
374
  return startupPromise;
177
375
  }
178
376
  const plan = resolveLaunchPlan();
@@ -183,12 +381,22 @@ export async function ensureForgeRuntimeReady(config) {
183
381
  if (await isForgeHealthy(config, HEALTHCHECK_TIMEOUT_MS)) {
184
382
  return;
185
383
  }
384
+ startupRuntimeKey = runtimeKey(config);
385
+ if (!(await isPortAvailable("127.0.0.1", config.port))) {
386
+ await relocateLocalRuntimePort(config);
387
+ startupRuntimeKey = runtimeKey(config);
388
+ if (await isForgeHealthy(config, HEALTHCHECK_TIMEOUT_MS)) {
389
+ return;
390
+ }
391
+ }
392
+ await ensurePackagedRuntimeDependencies(plan, config);
186
393
  if (!managedRuntimeChild || managedRuntimeKey !== key || managedRuntimeChild.killed) {
187
- spawnManagedRuntime(config, plan);
394
+ await spawnManagedRuntime(config, plan);
188
395
  }
189
- await waitForRuntime(config, STARTUP_TIMEOUT_MS);
396
+ await waitForRuntime(config, STARTUP_TIMEOUT_MS, managedRuntimeChild?.pid ?? null);
190
397
  })().finally(() => {
191
398
  startupPromise = null;
399
+ startupRuntimeKey = null;
192
400
  });
193
401
  return startupPromise;
194
402
  }
@@ -204,6 +412,16 @@ export async function startForgeRuntime(config) {
204
412
  };
205
413
  }
206
414
  const existingState = await readRuntimeState(config);
415
+ if (!existingState && (await isForgeHealthy(config, HEALTHCHECK_TIMEOUT_MS))) {
416
+ return {
417
+ ok: true,
418
+ started: false,
419
+ managed: false,
420
+ message: `Forge is already running on ${config.baseUrl}, but it does not look like a plugin-managed runtime.`,
421
+ pid: null,
422
+ baseUrl: config.baseUrl
423
+ };
424
+ }
207
425
  if (existingState && processExists(existingState.pid) && (await isForgeHealthy(config, HEALTHCHECK_TIMEOUT_MS))) {
208
426
  return {
209
427
  ok: true,
@@ -216,6 +434,16 @@ export async function startForgeRuntime(config) {
216
434
  }
217
435
  await ensureForgeRuntimeReady(config);
218
436
  const state = await readRuntimeState(config);
437
+ if (!state && (await isForgeHealthy(config, HEALTHCHECK_TIMEOUT_MS))) {
438
+ return {
439
+ ok: true,
440
+ started: false,
441
+ managed: false,
442
+ message: `Forge is healthy on ${config.baseUrl}, but it does not look like a plugin-managed runtime.`,
443
+ pid: null,
444
+ baseUrl: config.baseUrl
445
+ };
446
+ }
219
447
  return {
220
448
  ok: true,
221
449
  started: 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,15 +40,46 @@ function normalizeTimeout(value, fallback) {
36
40
  }
37
41
  return Math.min(120_000, Math.max(1000, Math.round(value)));
38
42
  }
43
+ function isLocalOrigin(origin) {
44
+ try {
45
+ return LOCAL_HOSTNAMES.has(new URL(origin).hostname.toLowerCase());
46
+ }
47
+ catch {
48
+ return false;
49
+ }
50
+ }
51
+ function getPreferredLocalPortPath(origin) {
52
+ const hostname = new URL(origin).hostname.toLowerCase().replace(/[^a-z0-9._-]+/g, "-");
53
+ return path.join(homedir(), ".openclaw", "run", FORGE_PLUGIN_ID, `${hostname}-preferred-port.json`);
54
+ }
55
+ function readPreferredLocalPort(origin) {
56
+ if (!isLocalOrigin(origin)) {
57
+ return null;
58
+ }
59
+ try {
60
+ const preferredPortPath = getPreferredLocalPortPath(origin);
61
+ if (!existsSync(preferredPortPath)) {
62
+ return null;
63
+ }
64
+ const payload = JSON.parse(readFileSync(preferredPortPath, "utf8"));
65
+ return typeof payload.port === "number" && Number.isFinite(payload.port) ? payload.port : null;
66
+ }
67
+ catch {
68
+ return null;
69
+ }
70
+ }
39
71
  export function resolveForgePluginConfig(pluginConfig) {
40
72
  const raw = (pluginConfig ?? {});
41
73
  const origin = normalizeOrigin(raw.origin, DEFAULT_FORGE_ORIGIN);
42
- const port = normalizePort(raw.port, DEFAULT_FORGE_PORT);
74
+ const hasConfiguredPort = typeof raw.port === "number" && Number.isFinite(raw.port);
75
+ const preferredPort = hasConfiguredPort ? null : readPreferredLocalPort(origin);
76
+ const port = normalizePort(hasConfiguredPort ? raw.port : preferredPort ?? DEFAULT_FORGE_PORT, DEFAULT_FORGE_PORT);
43
77
  return {
44
78
  origin,
45
79
  port,
46
80
  baseUrl: buildForgeBaseUrl(origin, port),
47
81
  webAppUrl: buildForgeWebAppUrl(origin, port),
82
+ portSource: hasConfiguredPort ? "configured" : preferredPort !== null ? "preferred" : "default",
48
83
  dataRoot: typeof raw.dataRoot === "string" ? raw.dataRoot.trim() : "",
49
84
  apiToken: typeof raw.apiToken === "string" ? raw.apiToken.trim() : "",
50
85
  actorLabel: normalizeString(raw.actorLabel, "aurel"),
@@ -69,7 +104,7 @@ export const forgePluginConfigSchema = {
69
104
  default: DEFAULT_FORGE_PORT,
70
105
  minimum: 1,
71
106
  maximum: 65535,
72
- description: "Forge server port. Override this when your local machine uses a different port."
107
+ 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
108
  },
74
109
  dataRoot: {
75
110
  type: "string",
@@ -103,7 +138,7 @@ export const forgePluginConfigSchema = {
103
138
  },
104
139
  port: {
105
140
  label: "Forge Port",
106
- help: "Forge server port. Change this if your local machine uses another port.",
141
+ 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
142
  placeholder: "4317"
108
143
  },
109
144
  dataRoot: {
@@ -2,7 +2,7 @@
2
2
  "id": "forge-openclaw-plugin",
3
3
  "name": "Forge",
4
4
  "description": "Curated OpenClaw adapter for the Forge collaboration API, UI entrypoint, and localhost auto-start runtime.",
5
- "version": "0.2.12",
5
+ "version": "0.2.15",
6
6
  "skills": [
7
7
  "./skills"
8
8
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forge-openclaw-plugin",
3
- "version": "0.2.12",
3
+ "version": "0.2.15",
4
4
  "description": "Curated OpenClaw adapter for the Forge collaboration API, UI entrypoint, and localhost auto-start runtime.",
5
5
  "type": "module",
6
6
  "license": "MIT",