forge-openclaw-plugin 0.2.61 → 0.2.66

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.
Files changed (31) hide show
  1. package/README.md +28 -5
  2. package/dist/assets/{board-DThHV1D8.js → board-DFNV9VAZ.js} +1 -1
  3. package/dist/assets/index-CFZxwOFB.js +90 -0
  4. package/dist/assets/{index-7gvVCqnV.css → index-lFN9z5op.css} +1 -1
  5. package/dist/assets/{motion-BtTJtHCw.js → motion-CXdn34ih.js} +1 -1
  6. package/dist/assets/{table-Bnw6pcwN.js → table-CEq3bTDv.js} +1 -1
  7. package/dist/assets/{ui-CnVxFkj0.js → ui-g7FaEglG.js} +1 -1
  8. package/dist/assets/{vendor-BgZ3YrRd.js → vendor-BcOHGipZ.js} +236 -216
  9. package/dist/companion-iroh/darwin-arm64/forge-companion-iroh +0 -0
  10. package/dist/companion-iroh/darwin-x64/forge-companion-iroh +0 -0
  11. package/dist/companion-iroh/linux-x64/forge-companion-iroh +0 -0
  12. package/dist/companion-iroh-src/Cargo.lock +4559 -0
  13. package/dist/companion-iroh-src/Cargo.toml +37 -0
  14. package/dist/companion-iroh-src/src/lib.rs +279 -0
  15. package/dist/companion-iroh-src/src/main.rs +478 -0
  16. package/dist/companion-iroh-src/src/protocol.rs +129 -0
  17. package/dist/index.html +7 -7
  18. package/dist/server/server/src/app.js +163 -18
  19. package/dist/server/server/src/discovery-advertiser.js +13 -0
  20. package/dist/server/server/src/health.js +18 -3
  21. package/dist/server/server/src/movement.js +16 -1
  22. package/dist/server/server/src/openapi.js +12 -2
  23. package/dist/server/server/src/services/companion-iroh.js +425 -0
  24. package/dist/server/server/src/services/life-force.js +166 -25
  25. package/dist/server/server/src/web.js +88 -12
  26. package/openclaw.plugin.json +1 -1
  27. package/package.json +3 -3
  28. package/skills/forge-openclaw/SKILL.md +44 -2
  29. package/skills/forge-openclaw/entity_conversation_playbooks.md +217 -17
  30. package/skills/forge-openclaw/psyche_entity_playbooks.md +59 -0
  31. package/dist/assets/index-_Cn6Prym.js +0 -90
@@ -0,0 +1,425 @@
1
+ import { spawn, spawnSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { mkdir } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { logForgeDebug } from "../debug.js";
7
+ import { getEffectiveDataRoot } from "../db.js";
8
+ const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
9
+ const companionIrohManifestPath = path.join(projectRoot, "companion-iroh", "Cargo.toml");
10
+ const DEFAULT_IROH_START_TIMEOUT_MS = 25_000;
11
+ const COMPANION_IROH_ALPN = "forge-companion/1";
12
+ const FORGE_IROH_AGENT = "forge";
13
+ let irohHostState = emptyIrohHostState();
14
+ export async function buildCompanionPairingTransport(input) {
15
+ const requestApiBaseUrl = normalizeApiBaseUrl(input.requestApiBaseUrl);
16
+ const requestUiBaseUrl = normalizeUiBaseUrl(input.requestUiBaseUrl) ??
17
+ deriveUiBaseUrlFromApiBaseUrl(requestApiBaseUrl);
18
+ if (input.requestedMode === "manual-http") {
19
+ return manualHttpTransport(requestApiBaseUrl, requestUiBaseUrl, [
20
+ "Manual HTTP/TCP pairing was explicitly requested."
21
+ ]);
22
+ }
23
+ if (!shouldAutoStartIrohHost()) {
24
+ return manualHttpTransport(requestApiBaseUrl, requestUiBaseUrl, [
25
+ "Forge Iroh companion transport is unavailable in this runtime, so Forge fell back to direct HTTP."
26
+ ]);
27
+ }
28
+ const snapshot = await ensureCompanionIrohHost(localForgeBaseUrl());
29
+ if (snapshot.status === "ready" && snapshot.pairPayload) {
30
+ return irohTransport({
31
+ pairPayload: snapshot.pairPayload,
32
+ alpn: snapshot.alpn ?? COMPANION_IROH_ALPN,
33
+ localBaseUrl: snapshot.localBaseUrl,
34
+ recreateCommand: snapshot.recreateCommand ?? undefined,
35
+ startedAt: snapshot.startedAt ?? undefined,
36
+ notes: [
37
+ "Default pairing uses Forge's Rust Iroh transport over QUIC.",
38
+ "The QR payload carries the Iroh node id, host token, optional relay, and ALPN forge-companion/1.",
39
+ "Manual HTTP/TCP pairing remains available with --manual-http for advanced local setups."
40
+ ]
41
+ });
42
+ }
43
+ return manualHttpTransport(requestApiBaseUrl, requestUiBaseUrl, [
44
+ snapshot.lastError ??
45
+ "No Forge Iroh companion host could be started, so Forge fell back to direct HTTP."
46
+ ]);
47
+ }
48
+ export function getCompanionIrohStatus() {
49
+ return snapshotFor(irohHostState.pairPayload && irohHostState.child ? "ready" : "unavailable");
50
+ }
51
+ export async function stopCompanionIroh() {
52
+ if (irohHostState.child && !irohHostState.child.killed) {
53
+ irohHostState.child.kill("SIGTERM");
54
+ }
55
+ irohHostState = emptyIrohHostState();
56
+ }
57
+ export function companionIrohApiBaseUrlFromNodeId(nodeId) {
58
+ return `forge-iroh://${nodeId.trim()}/api/v1`;
59
+ }
60
+ export function companionIrohUiBaseUrlFromNodeId(nodeId) {
61
+ return `forge-iroh://${nodeId.trim()}/forge/`;
62
+ }
63
+ async function ensureCompanionIrohHost(localBaseUrl) {
64
+ if (irohHostState.child &&
65
+ !irohHostState.child.killed &&
66
+ irohHostState.pairPayload &&
67
+ irohHostState.localBaseUrl === localBaseUrl) {
68
+ return snapshotFor("ready", localBaseUrl);
69
+ }
70
+ if (irohHostState.starting) {
71
+ return irohHostState.starting;
72
+ }
73
+ irohHostState.starting = startCompanionIrohHost(localBaseUrl).finally(() => {
74
+ irohHostState.starting = null;
75
+ });
76
+ return irohHostState.starting;
77
+ }
78
+ async function startCompanionIrohHost(localBaseUrl) {
79
+ const stateDir = path.join(getEffectiveDataRoot(), "companion-iroh");
80
+ await mkdir(stateDir, { recursive: true });
81
+ const hostCommand = resolveIrohHostCommand({
82
+ stateDir,
83
+ localBaseUrl
84
+ });
85
+ if (!hostCommand) {
86
+ irohHostState.lastError =
87
+ "Forge companion Iroh host is unavailable. Build companion-iroh, install cargo, or set FORGE_COMPANION_IROH_BIN.";
88
+ return snapshotFor("unavailable", localBaseUrl, stateDir);
89
+ }
90
+ if (irohHostState.child && !irohHostState.child.killed) {
91
+ irohHostState.child.kill("SIGTERM");
92
+ }
93
+ const child = spawn(hostCommand.command, hostCommand.args, {
94
+ env: process.env,
95
+ stdio: ["ignore", "pipe", "pipe"]
96
+ });
97
+ irohHostState.child = child;
98
+ irohHostState.pairPayload = null;
99
+ irohHostState.alpn = null;
100
+ irohHostState.localBaseUrl = localBaseUrl;
101
+ irohHostState.stateDir = stateDir;
102
+ irohHostState.recreateCommand = hostCommand.displayCommand;
103
+ irohHostState.startedAt = new Date().toISOString();
104
+ irohHostState.lastError = null;
105
+ const seenLogs = [];
106
+ let stdoutBuffer = "";
107
+ const rememberLog = (chunk) => {
108
+ const text = chunk.toString("utf8");
109
+ seenLogs.push(text);
110
+ if (seenLogs.length > 20) {
111
+ seenLogs.shift();
112
+ }
113
+ };
114
+ const parseReadyLines = (chunk) => {
115
+ stdoutBuffer += chunk.toString("utf8");
116
+ let lineEnd = stdoutBuffer.indexOf("\n");
117
+ while (lineEnd >= 0) {
118
+ const line = stdoutBuffer.slice(0, lineEnd).trim();
119
+ stdoutBuffer = stdoutBuffer.slice(lineEnd + 1);
120
+ applyIrohHostReadyLine(line);
121
+ lineEnd = stdoutBuffer.indexOf("\n");
122
+ }
123
+ };
124
+ child.stdout?.on("data", (chunk) => {
125
+ rememberLog(chunk);
126
+ parseReadyLines(chunk);
127
+ });
128
+ child.stderr?.on("data", rememberLog);
129
+ child.once("error", (error) => {
130
+ irohHostState.lastError = error.message;
131
+ });
132
+ child.once("exit", (code, signal) => {
133
+ if (irohHostState.child === child) {
134
+ irohHostState.child = null;
135
+ irohHostState.pairPayload = null;
136
+ irohHostState.alpn = null;
137
+ irohHostState.lastError =
138
+ code === 0
139
+ ? "Forge companion Iroh host stopped."
140
+ : `Forge companion Iroh host exited with ${signal ?? `code ${code}`}.`;
141
+ }
142
+ });
143
+ const deadline = Date.now() + readIrohStartTimeoutMs();
144
+ while (Date.now() < deadline) {
145
+ if (irohHostState.pairPayload) {
146
+ return snapshotFor("ready", localBaseUrl, stateDir);
147
+ }
148
+ if (!irohHostState.child) {
149
+ break;
150
+ }
151
+ await delay(200);
152
+ }
153
+ if (!irohHostState.pairPayload) {
154
+ irohHostState.lastError =
155
+ irohHostState.lastError ??
156
+ `Forge companion Iroh host did not report a ready pair payload. Recent output: ${seenLogs
157
+ .join("")
158
+ .trim()
159
+ .slice(-800)}`;
160
+ if (irohHostState.child && !irohHostState.child.killed) {
161
+ irohHostState.child.kill("SIGTERM");
162
+ }
163
+ return snapshotFor("error", localBaseUrl, stateDir);
164
+ }
165
+ return snapshotFor("ready", localBaseUrl, stateDir);
166
+ }
167
+ function applyIrohHostReadyLine(line) {
168
+ if (!line) {
169
+ return;
170
+ }
171
+ try {
172
+ const parsed = JSON.parse(line);
173
+ if (parsed.event !== "ready" || !isIrohPairPayload(parsed.pairPayload)) {
174
+ return;
175
+ }
176
+ if (parsed.alpn !== undefined && parsed.alpn !== COMPANION_IROH_ALPN) {
177
+ irohHostState.lastError = `Unsupported companion Iroh ALPN: ${String(parsed.alpn)}`;
178
+ return;
179
+ }
180
+ irohHostState.pairPayload = parsed.pairPayload;
181
+ irohHostState.alpn = COMPANION_IROH_ALPN;
182
+ }
183
+ catch {
184
+ // Non-JSON stdout is treated as diagnostic output from cargo or the host.
185
+ }
186
+ }
187
+ function isIrohPairPayload(value) {
188
+ if (!value || typeof value !== "object") {
189
+ return false;
190
+ }
191
+ const payload = value;
192
+ return (payload.v === 1 &&
193
+ typeof payload.node_id === "string" &&
194
+ payload.node_id.trim().length > 0 &&
195
+ typeof payload.token === "string" &&
196
+ payload.token.trim().length > 0 &&
197
+ (payload.host_name === undefined || typeof payload.host_name === "string") &&
198
+ (payload.relay === undefined || typeof payload.relay === "string"));
199
+ }
200
+ function irohTransport(input) {
201
+ const nodeId = input.pairPayload.node_id;
202
+ return {
203
+ transportMode: "iroh",
204
+ apiBaseUrl: companionIrohApiBaseUrlFromNodeId(nodeId),
205
+ uiBaseUrl: companionIrohUiBaseUrlFromNodeId(nodeId),
206
+ transport: {
207
+ protocol: "iroh",
208
+ provider: "forge-companion-iroh",
209
+ status: "ready",
210
+ localBaseUrl: input.localBaseUrl,
211
+ nodeId,
212
+ relay: input.pairPayload.relay,
213
+ alpn: input.alpn,
214
+ agent: FORGE_IROH_AGENT,
215
+ pairPayload: input.pairPayload,
216
+ recreateCommand: input.recreateCommand,
217
+ startedAt: input.startedAt,
218
+ notes: input.notes
219
+ }
220
+ };
221
+ }
222
+ function manualHttpTransport(apiBaseUrl, uiBaseUrl, notes) {
223
+ return {
224
+ transportMode: "manual-http",
225
+ apiBaseUrl,
226
+ uiBaseUrl,
227
+ transport: {
228
+ protocol: "http",
229
+ provider: "manual-http",
230
+ status: "ready",
231
+ localBaseUrl: apiBaseUrl.replace(/\/api\/v1\/?$/u, ""),
232
+ notes
233
+ }
234
+ };
235
+ }
236
+ function snapshotFor(status, localBaseUrl = localForgeBaseUrl(), stateDir = irohHostState.stateDir) {
237
+ return {
238
+ status,
239
+ pairPayload: irohHostState.pairPayload,
240
+ alpn: irohHostState.alpn,
241
+ localBaseUrl,
242
+ stateDir,
243
+ recreateCommand: irohHostState.recreateCommand,
244
+ startedAt: irohHostState.startedAt,
245
+ lastError: irohHostState.lastError
246
+ };
247
+ }
248
+ function resolveIrohHostCommand(input) {
249
+ const hostArgs = [
250
+ "host",
251
+ "--state-dir",
252
+ input.stateDir,
253
+ "--local-base-url",
254
+ input.localBaseUrl
255
+ ];
256
+ const explicitBin = process.env.FORGE_COMPANION_IROH_BIN?.trim();
257
+ if (explicitBin) {
258
+ const command = path.isAbsolute(explicitBin)
259
+ ? explicitBin
260
+ : path.resolve(projectRoot, explicitBin);
261
+ return {
262
+ command,
263
+ args: hostArgs,
264
+ displayCommand: `${shellQuote(command)} ${hostArgs.map(shellQuote).join(" ")}`
265
+ };
266
+ }
267
+ for (const candidate of candidateIrohBinaries()) {
268
+ if (existsSync(candidate)) {
269
+ return {
270
+ command: candidate,
271
+ args: hostArgs,
272
+ displayCommand: `${shellQuote(candidate)} ${hostArgs.map(shellQuote).join(" ")}`
273
+ };
274
+ }
275
+ }
276
+ const cargoPath = resolveCommand("cargo");
277
+ const manifestPath = resolveCompanionIrohManifestPath();
278
+ if (!cargoPath || !manifestPath) {
279
+ return null;
280
+ }
281
+ const args = [
282
+ "run",
283
+ "--manifest-path",
284
+ manifestPath,
285
+ "--quiet",
286
+ "--",
287
+ ...hostArgs
288
+ ];
289
+ return {
290
+ command: cargoPath,
291
+ args,
292
+ displayCommand: `${shellQuote(cargoPath)} ${args.map(shellQuote).join(" ")}`
293
+ };
294
+ }
295
+ function candidateIrohBinaries() {
296
+ const binaryName = process.platform === "win32" ? "forge-companion-iroh.exe" : "forge-companion-iroh";
297
+ const platformKey = `${process.platform}-${process.arch}`;
298
+ return [
299
+ path.join(projectRoot, "companion-iroh", "target", "release", binaryName),
300
+ path.join(projectRoot, "companion-iroh", "target", "debug", binaryName),
301
+ path.join(projectRoot, "openclaw-plugin", "dist", "companion-iroh", platformKey, binaryName),
302
+ path.join(projectRoot, "companion-iroh", platformKey, binaryName),
303
+ path.join(projectRoot, "companion-iroh", binaryName)
304
+ ];
305
+ }
306
+ function resolveCompanionIrohManifestPath() {
307
+ const candidates = [
308
+ companionIrohManifestPath,
309
+ path.join(projectRoot, "companion-iroh-src", "Cargo.toml")
310
+ ];
311
+ return candidates.find((candidate) => existsSync(candidate)) ?? null;
312
+ }
313
+ function shouldAutoStartIrohHost() {
314
+ if (process.env.FORGE_COMPANION_IROH_DISABLED === "1") {
315
+ return false;
316
+ }
317
+ if (process.env.FORGE_COMPANION_IROH_AUTOSTART === "0") {
318
+ return false;
319
+ }
320
+ if (isTestRuntime() && !process.env.FORGE_COMPANION_IROH_BIN?.trim()) {
321
+ return false;
322
+ }
323
+ return true;
324
+ }
325
+ function isTestRuntime() {
326
+ return (process.env.NODE_ENV === "test" ||
327
+ process.env.VITEST === "true" ||
328
+ process.argv.some((arg) => arg === "--test" || arg.includes("vitest")));
329
+ }
330
+ function localForgeBaseUrl() {
331
+ const configured = process.env.FORGE_COMPANION_IROH_LOCAL_BASE_URL?.trim();
332
+ if (configured) {
333
+ return configured.replace(/\/+$/u, "");
334
+ }
335
+ const port = Number(process.env.PORT ?? 4317);
336
+ const safePort = Number.isInteger(port) && port > 0 ? port : 4317;
337
+ return `http://127.0.0.1:${safePort}`;
338
+ }
339
+ function normalizeApiBaseUrl(value) {
340
+ const trimmed = value.trim();
341
+ try {
342
+ const url = new URL(trimmed);
343
+ url.pathname = url.pathname.replace(/\/+$/u, "");
344
+ if (!url.pathname.endsWith("/api/v1")) {
345
+ url.pathname = `${url.pathname}/api/v1`.replace(/\/{2,}/gu, "/");
346
+ }
347
+ url.search = "";
348
+ url.hash = "";
349
+ return url.toString().replace(/\/$/u, "");
350
+ }
351
+ catch {
352
+ return trimmed;
353
+ }
354
+ }
355
+ function normalizeUiBaseUrl(value) {
356
+ if (!value?.trim()) {
357
+ return null;
358
+ }
359
+ try {
360
+ const url = new URL(value);
361
+ url.pathname = "/forge/";
362
+ url.search = "";
363
+ url.hash = "";
364
+ return url.toString();
365
+ }
366
+ catch {
367
+ return null;
368
+ }
369
+ }
370
+ function deriveUiBaseUrlFromApiBaseUrl(apiBaseUrl) {
371
+ try {
372
+ const url = new URL(apiBaseUrl);
373
+ url.pathname = "/forge/";
374
+ url.search = "";
375
+ url.hash = "";
376
+ return url.toString();
377
+ }
378
+ catch {
379
+ return apiBaseUrl;
380
+ }
381
+ }
382
+ function readIrohStartTimeoutMs() {
383
+ const parsed = Number(process.env.FORGE_COMPANION_IROH_START_TIMEOUT_MS);
384
+ if (Number.isFinite(parsed) && parsed > 0) {
385
+ return Math.round(parsed);
386
+ }
387
+ return DEFAULT_IROH_START_TIMEOUT_MS;
388
+ }
389
+ function emptyIrohHostState() {
390
+ return {
391
+ child: null,
392
+ pairPayload: null,
393
+ alpn: null,
394
+ localBaseUrl: null,
395
+ stateDir: null,
396
+ recreateCommand: null,
397
+ startedAt: null,
398
+ lastError: null,
399
+ starting: null
400
+ };
401
+ }
402
+ function resolveCommand(command) {
403
+ const result = spawnSync(process.platform === "win32" ? "where" : "command", process.platform === "win32" ? [command] : ["-v", command], {
404
+ encoding: "utf8",
405
+ shell: process.platform !== "win32"
406
+ });
407
+ if (result.status !== 0) {
408
+ return null;
409
+ }
410
+ const resolved = result.stdout.split(/\r?\n/u)[0]?.trim();
411
+ if (!resolved) {
412
+ return null;
413
+ }
414
+ logForgeDebug(`[companion-iroh] resolved ${command} at ${resolved}`);
415
+ return resolved;
416
+ }
417
+ function shellQuote(value) {
418
+ if (/^[a-zA-Z0-9_./:=+-]+$/u.test(value)) {
419
+ return value;
420
+ }
421
+ return `'${value.replaceAll("'", "'\\''")}'`;
422
+ }
423
+ function delay(ms) {
424
+ return new Promise((resolve) => setTimeout(resolve, ms));
425
+ }