forge-openclaw-plugin 0.2.60 → 0.2.65
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 +121 -51
- package/dist/assets/{board-B1V3M__K.js → board-DUwMfZvN.js} +1 -1
- package/dist/assets/index-B9VOpR7r.css +1 -0
- package/dist/assets/index-DoHjjze2.js +90 -0
- package/dist/assets/{motion-CltSTItx.js → motion-Crg3QyXD.js} +1 -1
- package/dist/assets/{table-B-VrSFx8.js → table-CTlDeYRs.js} +1 -1
- package/dist/assets/{ui-DUqM4jkt.js → ui-CJPaElbj.js} +1 -1
- package/dist/assets/{vendor-C0otBhgu.js → vendor-BdrT2htV.js} +217 -207
- package/dist/companion-iroh/darwin-arm64/forge-companion-iroh +0 -0
- package/dist/companion-iroh/darwin-x64/forge-companion-iroh +0 -0
- package/dist/companion-iroh/linux-x64/forge-companion-iroh +0 -0
- package/dist/companion-iroh-src/Cargo.lock +4559 -0
- package/dist/companion-iroh-src/Cargo.toml +37 -0
- package/dist/companion-iroh-src/src/lib.rs +279 -0
- package/dist/companion-iroh-src/src/main.rs +478 -0
- package/dist/companion-iroh-src/src/protocol.rs +129 -0
- package/dist/gamification-previews/dark-fantasy-item-trophy-tasks-anvil-marathon.webp +0 -0
- package/dist/gamification-previews/dark-fantasy-item-trophy-xp-levels-the-first-heat.webp +0 -0
- package/dist/gamification-previews/dark-fantasy-item-unlock-streaks-molten-crown-fire.webp +0 -0
- package/dist/gamification-previews/dark-fantasy-mascot.webp +0 -0
- package/dist/gamification-previews/dramatic-smithie-item-trophy-tasks-anvil-marathon.webp +0 -0
- package/dist/gamification-previews/dramatic-smithie-item-trophy-xp-levels-the-first-heat.webp +0 -0
- package/dist/gamification-previews/dramatic-smithie-item-unlock-streaks-molten-crown-fire.webp +0 -0
- package/dist/gamification-previews/dramatic-smithie-mascot.webp +0 -0
- package/dist/gamification-previews/mind-locksmith-item-trophy-tasks-anvil-marathon.webp +0 -0
- package/dist/gamification-previews/mind-locksmith-item-trophy-xp-levels-the-first-heat.webp +0 -0
- package/dist/gamification-previews/mind-locksmith-item-unlock-streaks-molten-crown-fire.webp +0 -0
- package/dist/gamification-previews/mind-locksmith-mascot.webp +0 -0
- package/dist/index.html +7 -7
- package/dist/openclaw/parity.js +27 -0
- package/dist/openclaw/plugin-entry-shared.js +2 -2
- package/dist/openclaw/plugin-sdk-types.d.ts +2 -1
- package/dist/openclaw/routes.d.ts +4 -0
- package/dist/openclaw/routes.js +112 -3
- package/dist/openclaw/tools.js +32 -4
- package/dist/server/server/migrations/059_data_backup_retention.sql +2 -0
- package/dist/server/server/src/app.js +288 -61
- package/dist/server/server/src/data-management-types.js +2 -0
- package/dist/server/server/src/discovery-advertiser.js +13 -0
- package/dist/server/server/src/health.js +58 -3
- package/dist/server/server/src/movement.js +16 -1
- package/dist/server/server/src/openapi.js +410 -9
- package/dist/server/server/src/repositories/rewards.js +60 -0
- package/dist/server/server/src/services/companion-iroh.js +425 -0
- package/dist/server/server/src/services/data-management.js +32 -2
- package/dist/server/server/src/services/doctor.js +762 -0
- package/dist/server/server/src/services/gamification.js +75 -3
- package/dist/server/server/src/services/life-force.js +166 -25
- package/dist/server/server/src/web.js +88 -12
- package/dist/server/src/lib/api.js +9 -0
- package/dist/server/src/lib/gamification-catalog.js +1 -1
- package/openclaw.plugin.json +85 -3
- package/package.json +10 -6
- package/server/migrations/059_data_backup_retention.sql +2 -0
- package/skills/forge-openclaw/SKILL.md +80 -19
- package/skills/forge-openclaw/entity_conversation_playbooks.md +283 -25
- package/skills/forge-openclaw/psyche_entity_playbooks.md +82 -0
- package/dist/assets/index-BwKAPo98.css +0 -1
- package/dist/assets/index-Dy7c-dRY.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
|
+
}
|
|
@@ -84,12 +84,13 @@ function ensureDataManagementSettingsRow() {
|
|
|
84
84
|
preferred_data_root,
|
|
85
85
|
backup_directory,
|
|
86
86
|
backup_frequency_hours,
|
|
87
|
+
backup_retention_days,
|
|
87
88
|
auto_repair_enabled,
|
|
88
89
|
last_auto_backup_at,
|
|
89
90
|
last_manual_backup_at,
|
|
90
91
|
created_at,
|
|
91
92
|
updated_at
|
|
92
|
-
) VALUES (1, ?, ?, NULL, 1, NULL, NULL, ?, ?)`)
|
|
93
|
+
) VALUES (1, ?, ?, NULL, 30, 1, NULL, NULL, ?, ?)`)
|
|
93
94
|
.run(dataRoot, backupDirectory, now, now);
|
|
94
95
|
}
|
|
95
96
|
function readDataManagementSettingsRow() {
|
|
@@ -99,6 +100,7 @@ function readDataManagementSettingsRow() {
|
|
|
99
100
|
preferred_data_root,
|
|
100
101
|
backup_directory,
|
|
101
102
|
backup_frequency_hours,
|
|
103
|
+
backup_retention_days,
|
|
102
104
|
auto_repair_enabled,
|
|
103
105
|
last_auto_backup_at,
|
|
104
106
|
last_manual_backup_at,
|
|
@@ -124,12 +126,13 @@ function writeDataManagementSettingsRow(patch) {
|
|
|
124
126
|
SET preferred_data_root = ?,
|
|
125
127
|
backup_directory = ?,
|
|
126
128
|
backup_frequency_hours = ?,
|
|
129
|
+
backup_retention_days = ?,
|
|
127
130
|
auto_repair_enabled = ?,
|
|
128
131
|
last_auto_backup_at = ?,
|
|
129
132
|
last_manual_backup_at = ?,
|
|
130
133
|
updated_at = ?
|
|
131
134
|
WHERE id = 1`)
|
|
132
|
-
.run(next.preferred_data_root, next.backup_directory, next.backup_frequency_hours, next.auto_repair_enabled, next.last_auto_backup_at, next.last_manual_backup_at, next.updated_at);
|
|
135
|
+
.run(next.preferred_data_root, next.backup_directory, next.backup_frequency_hours, next.backup_retention_days, next.auto_repair_enabled, next.last_auto_backup_at, next.last_manual_backup_at, next.updated_at);
|
|
133
136
|
}
|
|
134
137
|
function resolveCurrentDataManagementSettings() {
|
|
135
138
|
const row = readDataManagementSettingsRow();
|
|
@@ -139,6 +142,7 @@ function resolveCurrentDataManagementSettings() {
|
|
|
139
142
|
preferredDataRoot,
|
|
140
143
|
backupDirectory,
|
|
141
144
|
backupFrequencyHours: row.backup_frequency_hours,
|
|
145
|
+
backupRetentionDays: row.backup_retention_days,
|
|
142
146
|
autoRepairEnabled: row.auto_repair_enabled === 1,
|
|
143
147
|
lastAutoBackupAt: row.last_auto_backup_at,
|
|
144
148
|
lastManualBackupAt: row.last_manual_backup_at
|
|
@@ -461,6 +465,7 @@ export async function createDataBackup(input = { note: "" }, options = {}) {
|
|
|
461
465
|
}
|
|
462
466
|
if (mode === "automatic") {
|
|
463
467
|
writeDataManagementSettingsRow({ last_auto_backup_at: createdAt });
|
|
468
|
+
await pruneExpiredAutomaticBackups(settings.backupDirectory, settings.backupRetentionDays);
|
|
464
469
|
}
|
|
465
470
|
return backup;
|
|
466
471
|
}
|
|
@@ -468,6 +473,27 @@ export async function createDataBackup(input = { note: "" }, options = {}) {
|
|
|
468
473
|
await rm(sqliteSnapshot.tempDir, { recursive: true, force: true });
|
|
469
474
|
}
|
|
470
475
|
}
|
|
476
|
+
async function pruneExpiredAutomaticBackups(backupDirectory, retentionDays) {
|
|
477
|
+
if (!retentionDays) {
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
const cutoff = Date.now() - retentionDays * 24 * 60 * 60 * 1000;
|
|
481
|
+
const backups = await listDataBackups();
|
|
482
|
+
for (const backup of backups) {
|
|
483
|
+
if (backup.mode !== "automatic") {
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
if (path.resolve(backup.backupDirectory) !== path.resolve(backupDirectory)) {
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
const createdAtMs = new Date(backup.createdAt).getTime();
|
|
490
|
+
if (!Number.isFinite(createdAtMs) || createdAtMs >= cutoff) {
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
await rm(backup.archivePath, { force: true });
|
|
494
|
+
await rm(backup.manifestPath, { force: true });
|
|
495
|
+
}
|
|
496
|
+
}
|
|
471
497
|
async function openDatabaseSnapshot(databasePath) {
|
|
472
498
|
const database = new DatabaseSync(databasePath);
|
|
473
499
|
database.exec("PRAGMA busy_timeout = 250;");
|
|
@@ -643,6 +669,7 @@ export async function switchDataRoot(input, options = {}) {
|
|
|
643
669
|
preferred_data_root: targetDataRoot,
|
|
644
670
|
backup_directory: nextBackupDirectory,
|
|
645
671
|
backup_frequency_hours: previousSettings.backupFrequencyHours,
|
|
672
|
+
backup_retention_days: previousSettings.backupRetentionDays,
|
|
646
673
|
auto_repair_enabled: previousSettings.autoRepairEnabled ? 1 : 0
|
|
647
674
|
});
|
|
648
675
|
await (options.persistPreferredDataRoot ?? writeMonorepoPreferredDataRoot)(targetDataRoot);
|
|
@@ -698,6 +725,9 @@ export async function updateDataManagementSettings(input) {
|
|
|
698
725
|
backup_frequency_hours: parsed.backupFrequencyHours !== undefined
|
|
699
726
|
? parsed.backupFrequencyHours
|
|
700
727
|
: undefined,
|
|
728
|
+
backup_retention_days: parsed.backupRetentionDays !== undefined
|
|
729
|
+
? parsed.backupRetentionDays
|
|
730
|
+
: undefined,
|
|
701
731
|
auto_repair_enabled: parsed.autoRepairEnabled !== undefined
|
|
702
732
|
? parsed.autoRepairEnabled
|
|
703
733
|
? 1
|