openclaw-channel-dmwork 0.5.21 → 0.6.0-dev.cea9ee46
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/dist/cli/bind.d.ts +12 -0
- package/dist/cli/bind.js +104 -0
- package/dist/cli/bind.js.map +1 -0
- package/dist/cli/doctor.js +48 -30
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +53 -22
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/install.d.ts +17 -7
- package/dist/cli/install.js +216 -127
- package/dist/cli/install.js.map +1 -1
- package/dist/cli/openclaw-cli.d.ts +82 -0
- package/dist/cli/openclaw-cli.js +556 -33
- package/dist/cli/openclaw-cli.js.map +1 -1
- package/dist/cli/quickstart.d.ts +15 -0
- package/dist/cli/quickstart.js +241 -0
- package/dist/cli/quickstart.js.map +1 -0
- package/dist/cli/update.d.ts +3 -4
- package/dist/cli/update.js +7 -82
- package/dist/cli/update.js.map +1 -1
- package/dist/cli/utils.d.ts +3 -3
- package/dist/cli/utils.js +4 -5
- package/dist/cli/utils.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/package.json +4 -3
- package/dist/src/api-fetch.d.ts +3 -1
- package/dist/src/api-fetch.js +8 -2
- package/dist/src/api-fetch.js.map +1 -1
- package/dist/src/channel.js +20 -16
- package/dist/src/channel.js.map +1 -1
- package/dist/src/inbound.js +2 -64
- package/dist/src/inbound.js.map +1 -1
- package/dist/src/version.d.ts +1 -0
- package/dist/src/version.js +3 -0
- package/dist/src/version.js.map +1 -0
- package/openclaw.plugin.json +3 -0
- package/package.json +4 -3
- package/skills/dmwork-bot-api/SKILL.md +1004 -0
package/dist/cli/openclaw-cli.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* argument arrays to avoid shell-quoting issues.
|
|
5
5
|
*/
|
|
6
6
|
import { execFileSync, execSync } from "node:child_process";
|
|
7
|
-
import { readFileSync, writeFileSync, copyFileSync, existsSync } from "node:fs";
|
|
7
|
+
import { readFileSync, writeFileSync, copyFileSync, existsSync, rmSync, readdirSync, statSync, renameSync } from "node:fs";
|
|
8
8
|
import { homedir } from "node:os";
|
|
9
9
|
import { resolve } from "node:path";
|
|
10
10
|
/**
|
|
@@ -52,6 +52,10 @@ function findGlobalOpenclaw() {
|
|
|
52
52
|
return "openclaw";
|
|
53
53
|
}
|
|
54
54
|
const OPENCLAW = findGlobalOpenclaw();
|
|
55
|
+
/** Get the resolved openclaw binary path */
|
|
56
|
+
export function getOpenClawBin() {
|
|
57
|
+
return OPENCLAW;
|
|
58
|
+
}
|
|
55
59
|
/** Expand ~ to home directory */
|
|
56
60
|
function expandHome(p) {
|
|
57
61
|
if (p.startsWith("~/"))
|
|
@@ -62,14 +66,45 @@ function expandHome(p) {
|
|
|
62
66
|
// Config helpers
|
|
63
67
|
// ---------------------------------------------------------------------------
|
|
64
68
|
export function getConfigFilePath() {
|
|
65
|
-
|
|
69
|
+
const out = execFileSync(OPENCLAW, ["config", "file"], { encoding: "utf-8" });
|
|
70
|
+
// openclaw may prepend warnings/box-drawing to stdout; extract the actual path
|
|
71
|
+
// The path is typically the last non-empty line containing openclaw.json
|
|
72
|
+
const lines = out.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0);
|
|
73
|
+
const pathLine = lines.find((l) => l.endsWith("openclaw.json")) ?? lines[lines.length - 1];
|
|
74
|
+
return pathLine ?? out.trim();
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Strip OpenClaw stdout noise (banner, plugin log lines, timestamps).
|
|
78
|
+
* Old OpenClaw versions mix these into stdout alongside the actual value.
|
|
79
|
+
*/
|
|
80
|
+
function stripStdoutNoise(raw) {
|
|
81
|
+
return raw
|
|
82
|
+
.split("\n")
|
|
83
|
+
.filter((line) => {
|
|
84
|
+
const t = line.trim();
|
|
85
|
+
if (!t)
|
|
86
|
+
return false;
|
|
87
|
+
// Banner: 🦞 OpenClaw ...
|
|
88
|
+
if (/^[\u{1F980}\u{1F600}-\u{1FAFF}]/u.test(t))
|
|
89
|
+
return false;
|
|
90
|
+
// Plugin log: [plugins] ..., [dmwork] ...
|
|
91
|
+
if (/^\[[\w-]+\]/.test(t))
|
|
92
|
+
return false;
|
|
93
|
+
// Timestamped log: 17:37:26 [plugins] ...
|
|
94
|
+
if (/^\d{1,2}:\d{2}(:\d{2})?\s*\[/.test(t))
|
|
95
|
+
return false;
|
|
96
|
+
return true;
|
|
97
|
+
})
|
|
98
|
+
.join("\n")
|
|
99
|
+
.trim();
|
|
66
100
|
}
|
|
67
101
|
export function configGet(path) {
|
|
68
102
|
try {
|
|
69
|
-
const
|
|
103
|
+
const raw = execFileSync(OPENCLAW, ["config", "get", path], {
|
|
70
104
|
encoding: "utf-8",
|
|
71
105
|
stdio: ["pipe", "pipe", "pipe"],
|
|
72
|
-
})
|
|
106
|
+
});
|
|
107
|
+
const val = stripStdoutNoise(raw);
|
|
73
108
|
return val === "" ? null : val;
|
|
74
109
|
}
|
|
75
110
|
catch {
|
|
@@ -89,7 +124,25 @@ export function configGetJson(path) {
|
|
|
89
124
|
: Math.max(jsonStart, arrStart);
|
|
90
125
|
if (start < 0)
|
|
91
126
|
return null;
|
|
92
|
-
|
|
127
|
+
// Find matching end bracket to avoid trailing log noise breaking JSON.parse
|
|
128
|
+
const openChar = out[start];
|
|
129
|
+
const closeChar = openChar === "{" ? "}" : "]";
|
|
130
|
+
let depth = 0;
|
|
131
|
+
let end = -1;
|
|
132
|
+
for (let i = start; i < out.length; i++) {
|
|
133
|
+
if (out[i] === openChar)
|
|
134
|
+
depth++;
|
|
135
|
+
else if (out[i] === closeChar) {
|
|
136
|
+
depth--;
|
|
137
|
+
if (depth === 0) {
|
|
138
|
+
end = i;
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (end < 0)
|
|
144
|
+
return null;
|
|
145
|
+
return JSON.parse(out.slice(start, end + 1));
|
|
93
146
|
}
|
|
94
147
|
catch {
|
|
95
148
|
return null;
|
|
@@ -119,18 +172,81 @@ export function configUnset(path) {
|
|
|
119
172
|
// ---------------------------------------------------------------------------
|
|
120
173
|
// Plugin helpers
|
|
121
174
|
// ---------------------------------------------------------------------------
|
|
175
|
+
/**
|
|
176
|
+
* Check if an error indicates an unsupported CLI option.
|
|
177
|
+
* Checks stderr/stdout/message across different Node versions and shells.
|
|
178
|
+
*/
|
|
179
|
+
function isUnsupportedOptionError(err) {
|
|
180
|
+
const sources = [
|
|
181
|
+
err?.stderr?.toString?.(),
|
|
182
|
+
err?.stdout?.toString?.(),
|
|
183
|
+
err?.message,
|
|
184
|
+
String(err),
|
|
185
|
+
];
|
|
186
|
+
return sources.some((s) => s && (/unknown option|unrecognized option/i.test(s)));
|
|
187
|
+
}
|
|
188
|
+
function isPluginNotInstalledError(err) {
|
|
189
|
+
const sources = [
|
|
190
|
+
err?.stderr?.toString?.(),
|
|
191
|
+
err?.stdout?.toString?.(),
|
|
192
|
+
err?.message,
|
|
193
|
+
String(err),
|
|
194
|
+
];
|
|
195
|
+
return sources.some((s) => s && (/not installed|no such plugin|plugin not found/i.test(s)));
|
|
196
|
+
}
|
|
122
197
|
export function pluginsInstall(spec, quiet, force) {
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
198
|
+
const baseArgs = ["plugins", "install", spec];
|
|
199
|
+
// 3-layer degradation for old openclaw versions:
|
|
200
|
+
// 1. --force --dangerously-force-unsafe-install (newest openclaw)
|
|
201
|
+
// 2. --force (mid-age openclaw)
|
|
202
|
+
// 3. bare install (oldest openclaw)
|
|
203
|
+
const attempts = force
|
|
204
|
+
? [
|
|
205
|
+
[...baseArgs, "--force", "--dangerously-force-unsafe-install"],
|
|
206
|
+
[...baseArgs, "--force"],
|
|
207
|
+
baseArgs,
|
|
208
|
+
]
|
|
209
|
+
: [
|
|
210
|
+
[...baseArgs, "--dangerously-force-unsafe-install"],
|
|
211
|
+
baseArgs,
|
|
212
|
+
];
|
|
213
|
+
// Always pipe to capture stderr for degradation detection.
|
|
214
|
+
// stdio: "inherit" causes Node to omit stderr from the error object,
|
|
215
|
+
// making isUnsupportedOptionError() unable to detect "unknown option".
|
|
216
|
+
for (let i = 0; i < attempts.length; i++) {
|
|
217
|
+
try {
|
|
218
|
+
const result = execFileSync(OPENCLAW, attempts[i], {
|
|
219
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
220
|
+
encoding: "utf-8",
|
|
221
|
+
});
|
|
222
|
+
if (!quiet && result)
|
|
223
|
+
process.stdout.write(result);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
catch (err) {
|
|
227
|
+
if (isUnsupportedOptionError(err) && i < attempts.length - 1) {
|
|
228
|
+
continue; // try next degradation level
|
|
229
|
+
}
|
|
230
|
+
// Final attempt failed: replay captured output, then throw
|
|
231
|
+
if (!quiet) {
|
|
232
|
+
const stdout = err?.stdout?.toString?.();
|
|
233
|
+
const stderr = err?.stderr?.toString?.();
|
|
234
|
+
if (stdout)
|
|
235
|
+
process.stdout.write(stdout);
|
|
236
|
+
if (stderr)
|
|
237
|
+
process.stderr.write(stderr);
|
|
238
|
+
}
|
|
239
|
+
throw err;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
129
242
|
}
|
|
130
243
|
export function pluginsUpdate(id, quiet) {
|
|
131
|
-
execFileSync(OPENCLAW, ["plugins", "update", id], {
|
|
132
|
-
stdio:
|
|
244
|
+
const result = execFileSync(OPENCLAW, ["plugins", "update", id], {
|
|
245
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
246
|
+
encoding: "utf-8",
|
|
133
247
|
});
|
|
248
|
+
if (!quiet && result)
|
|
249
|
+
process.stdout.write(result);
|
|
134
250
|
}
|
|
135
251
|
export function pluginsUninstall(id, yes) {
|
|
136
252
|
const args = ["plugins", "uninstall", id];
|
|
@@ -138,21 +254,99 @@ export function pluginsUninstall(id, yes) {
|
|
|
138
254
|
args.push("--force");
|
|
139
255
|
execFileSync(OPENCLAW, args, { stdio: "inherit" });
|
|
140
256
|
}
|
|
141
|
-
|
|
257
|
+
/**
|
|
258
|
+
* Inspect a plugin. Returns structured outcome distinguishing:
|
|
259
|
+
* - ok + data: inspect succeeded
|
|
260
|
+
* - unsupported: old OpenClaw without `plugins inspect`
|
|
261
|
+
* - not_found: plugin genuinely not found
|
|
262
|
+
* - error: other failure (config corruption, plugin load crash, etc.)
|
|
263
|
+
*/
|
|
264
|
+
export function pluginsInspectDetailed(id) {
|
|
142
265
|
try {
|
|
143
266
|
const out = execFileSync(OPENCLAW, ["plugins", "inspect", id, "--json"], {
|
|
144
267
|
encoding: "utf-8",
|
|
145
268
|
stdio: ["pipe", "pipe", "pipe"],
|
|
146
269
|
});
|
|
147
|
-
// stdout may contain plugin log noise before JSON — find the JSON object
|
|
148
270
|
const jsonStart = out.indexOf("{");
|
|
149
271
|
if (jsonStart < 0)
|
|
150
|
-
return null;
|
|
151
|
-
|
|
272
|
+
return { ok: false, data: null, failReason: "error" };
|
|
273
|
+
const data = JSON.parse(out.slice(jsonStart));
|
|
274
|
+
return { ok: true, data, failReason: null };
|
|
152
275
|
}
|
|
153
|
-
catch {
|
|
154
|
-
|
|
276
|
+
catch (err) {
|
|
277
|
+
const sources = [
|
|
278
|
+
err?.stderr?.toString?.(),
|
|
279
|
+
err?.stdout?.toString?.(),
|
|
280
|
+
err?.message,
|
|
281
|
+
String(err),
|
|
282
|
+
];
|
|
283
|
+
const text = sources.filter(Boolean).join(" ");
|
|
284
|
+
if (/unknown command|unrecognized command/i.test(text)) {
|
|
285
|
+
return { ok: false, data: null, failReason: "unsupported" };
|
|
286
|
+
}
|
|
287
|
+
if (/not found|not installed|no such plugin/i.test(text)) {
|
|
288
|
+
return { ok: false, data: null, failReason: "not_found" };
|
|
289
|
+
}
|
|
290
|
+
return { ok: false, data: null, failReason: "error" };
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
/** Backward-compatible wrapper: returns data or null. */
|
|
294
|
+
export function pluginsInspect(id) {
|
|
295
|
+
const outcome = pluginsInspectDetailed(id);
|
|
296
|
+
return outcome.ok ? outcome.data : null;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Resolve plugin install state. Uses `plugins inspect` when available,
|
|
300
|
+
* falls back to config entries + directory + package.json for old OpenClaw
|
|
301
|
+
* versions that don't support `plugins inspect`.
|
|
302
|
+
*
|
|
303
|
+
* Fallback installed = all 3 artifacts present (entries + installs + dir),
|
|
304
|
+
* matching detectScenario()'s healthy definition. Partial presence is NOT
|
|
305
|
+
* considered installed — that's a broken state for doctor --fix to handle.
|
|
306
|
+
*/
|
|
307
|
+
export function resolvePluginState(id) {
|
|
308
|
+
// Try inspect first
|
|
309
|
+
const outcome = pluginsInspectDetailed(id);
|
|
310
|
+
if (outcome.ok && outcome.data?.plugin) {
|
|
311
|
+
return {
|
|
312
|
+
installed: true,
|
|
313
|
+
enabled: outcome.data.plugin.enabled,
|
|
314
|
+
version: outcome.data.plugin.version,
|
|
315
|
+
installPath: outcome.data.install?.installPath ?? null,
|
|
316
|
+
source: "inspect",
|
|
317
|
+
inspectFailReason: null,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
// Fallback: check config + filesystem
|
|
321
|
+
const cfg = readConfigFromFile();
|
|
322
|
+
const extDir = getConfigFilePathSafe().replace(/openclaw\.json$/, "extensions");
|
|
323
|
+
const pluginDir = resolve(extDir, id);
|
|
324
|
+
const hasDir = existsSync(pluginDir);
|
|
325
|
+
const entries = cfg?.plugins?.entries?.[id];
|
|
326
|
+
const installs = cfg?.plugins?.installs?.[id];
|
|
327
|
+
const hasEntry = Boolean(entries);
|
|
328
|
+
const hasInstall = Boolean(installs);
|
|
329
|
+
// Healthy install requires all 3 artifacts, same as detectScenario().
|
|
330
|
+
// Partial presence (e.g. dir exists but no entries/installs) is broken, not installed.
|
|
331
|
+
const installed = hasDir && hasEntry && hasInstall;
|
|
332
|
+
if (!installed) {
|
|
333
|
+
return {
|
|
334
|
+
installed: false, enabled: null, version: null, installPath: null,
|
|
335
|
+
source: "fallback", inspectFailReason: outcome.failReason,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
// Resolve version: installs record > package.json on disk
|
|
339
|
+
let version = installs?.version ?? null;
|
|
340
|
+
if (!version && hasDir) {
|
|
341
|
+
try {
|
|
342
|
+
const pkg = JSON.parse(readFileSync(resolve(pluginDir, "package.json"), "utf-8"));
|
|
343
|
+
version = pkg.version ?? null;
|
|
344
|
+
}
|
|
345
|
+
catch { /* no package.json */ }
|
|
155
346
|
}
|
|
347
|
+
const enabled = entries?.enabled ?? null;
|
|
348
|
+
const installPath = installs?.installPath ?? (hasDir ? `~/.openclaw/extensions/${id}` : null);
|
|
349
|
+
return { installed, enabled, version, installPath, source: "fallback", inspectFailReason: outcome.failReason };
|
|
156
350
|
}
|
|
157
351
|
// ---------------------------------------------------------------------------
|
|
158
352
|
// Gateway helpers
|
|
@@ -166,11 +360,28 @@ export function gatewayStatus() {
|
|
|
166
360
|
const jsonStart = out.indexOf("{");
|
|
167
361
|
if (jsonStart < 0)
|
|
168
362
|
return { running: false };
|
|
169
|
-
|
|
170
|
-
|
|
363
|
+
// Find matching } to avoid trailing log noise
|
|
364
|
+
let depth = 0;
|
|
365
|
+
let end = -1;
|
|
366
|
+
for (let i = jsonStart; i < out.length; i++) {
|
|
367
|
+
if (out[i] === "{")
|
|
368
|
+
depth++;
|
|
369
|
+
else if (out[i] === "}") {
|
|
370
|
+
depth--;
|
|
371
|
+
if (depth === 0) {
|
|
372
|
+
end = i;
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
if (end < 0)
|
|
378
|
+
return { running: false };
|
|
379
|
+
const data = JSON.parse(out.slice(jsonStart, end + 1));
|
|
171
380
|
const runtimeRunning = data.service?.runtime?.status === "running";
|
|
172
381
|
const healthy = data.health?.healthy === true;
|
|
173
|
-
|
|
382
|
+
// Fallback: port is busy with an openclaw-gateway process = gateway is running
|
|
383
|
+
const portBusy = data.port?.status === "busy";
|
|
384
|
+
return { running: runtimeRunning || healthy || portBusy };
|
|
174
385
|
}
|
|
175
386
|
catch {
|
|
176
387
|
return { running: false };
|
|
@@ -218,7 +429,7 @@ export function getOpenClawVersion() {
|
|
|
218
429
|
*/
|
|
219
430
|
export function saveChannelConfigFromFile() {
|
|
220
431
|
try {
|
|
221
|
-
const configPath =
|
|
432
|
+
const configPath = getConfigFilePathSafe();
|
|
222
433
|
const raw = readFileSync(configPath, "utf-8");
|
|
223
434
|
const cfg = JSON.parse(raw);
|
|
224
435
|
return cfg?.channels?.dmwork ?? null;
|
|
@@ -233,7 +444,7 @@ export function saveChannelConfigFromFile() {
|
|
|
233
444
|
* Creates a .bak backup before writing.
|
|
234
445
|
*/
|
|
235
446
|
export function restoreChannelConfigToFile(dmworkConfig) {
|
|
236
|
-
const configPath =
|
|
447
|
+
const configPath = getConfigFilePathSafe();
|
|
237
448
|
// Backup
|
|
238
449
|
copyFileSync(configPath, configPath + ".bak");
|
|
239
450
|
// Read, merge, write
|
|
@@ -242,7 +453,7 @@ export function restoreChannelConfigToFile(dmworkConfig) {
|
|
|
242
453
|
if (!cfg.channels)
|
|
243
454
|
cfg.channels = {};
|
|
244
455
|
cfg.channels.dmwork = dmworkConfig;
|
|
245
|
-
|
|
456
|
+
writeConfigAtomic(cfg);
|
|
246
457
|
}
|
|
247
458
|
/**
|
|
248
459
|
* Remove channels.dmwork directly from the JSON file.
|
|
@@ -254,7 +465,7 @@ export function restoreChannelConfigToFile(dmworkConfig) {
|
|
|
254
465
|
* Falls back to the standard default when CLI is unavailable
|
|
255
466
|
* (e.g. during uninstall when config validation fails).
|
|
256
467
|
*/
|
|
257
|
-
function getConfigFilePathSafe() {
|
|
468
|
+
export function getConfigFilePathSafe() {
|
|
258
469
|
try {
|
|
259
470
|
return expandHome(getConfigFilePath());
|
|
260
471
|
}
|
|
@@ -266,11 +477,10 @@ export function removeChannelConfigFromFile() {
|
|
|
266
477
|
try {
|
|
267
478
|
const configPath = getConfigFilePathSafe();
|
|
268
479
|
copyFileSync(configPath, configPath + ".bak");
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
if (cfg.channels?.dmwork) {
|
|
480
|
+
const cfg = readConfigFromFile();
|
|
481
|
+
if (cfg?.channels?.dmwork) {
|
|
272
482
|
delete cfg.channels.dmwork;
|
|
273
|
-
|
|
483
|
+
writeConfigAtomic(cfg);
|
|
274
484
|
}
|
|
275
485
|
}
|
|
276
486
|
catch {
|
|
@@ -311,7 +521,7 @@ export function removeOrphanedBindingsFromFile(channel, validAccountIds) {
|
|
|
311
521
|
// Keep only if accountId is in valid list (or no accountId specified)
|
|
312
522
|
return !b.match.accountId || validAccountIds.includes(b.match.accountId);
|
|
313
523
|
});
|
|
314
|
-
|
|
524
|
+
writeConfigAtomic(cfg);
|
|
315
525
|
}
|
|
316
526
|
catch {
|
|
317
527
|
// best effort
|
|
@@ -348,7 +558,6 @@ export function cleanupLegacyPlugin() {
|
|
|
348
558
|
}
|
|
349
559
|
// Remove legacy directory
|
|
350
560
|
try {
|
|
351
|
-
const { rmSync } = require("node:fs");
|
|
352
561
|
rmSync(legacyDir, { recursive: true, force: true });
|
|
353
562
|
actions.push(`Removed legacy directory: ${legacyDir}`);
|
|
354
563
|
}
|
|
@@ -370,7 +579,7 @@ export function cleanupLegacyPlugin() {
|
|
|
370
579
|
if (Array.isArray(cfg.plugins?.allow)) {
|
|
371
580
|
cfg.plugins.allow = cfg.plugins.allow.filter((id) => id !== LEGACY_PLUGIN_ID);
|
|
372
581
|
}
|
|
373
|
-
|
|
582
|
+
writeConfigAtomic(cfg);
|
|
374
583
|
actions.push(`Cleaned legacy entries from openclaw.json`);
|
|
375
584
|
}
|
|
376
585
|
}
|
|
@@ -379,4 +588,318 @@ export function cleanupLegacyPlugin() {
|
|
|
379
588
|
}
|
|
380
589
|
return actions;
|
|
381
590
|
}
|
|
591
|
+
/**
|
|
592
|
+
* Clean up stale openclaw-channel-dmwork directory that is not registered
|
|
593
|
+
* in plugins.installs (orphaned from a failed previous install).
|
|
594
|
+
*
|
|
595
|
+
* Only removes the directory if ALL of these are true:
|
|
596
|
+
* 1. The directory exists
|
|
597
|
+
* 2. pluginsInspect returns null (openclaw doesn't recognize it)
|
|
598
|
+
* 3. plugins.installs has no record for openclaw-channel-dmwork
|
|
599
|
+
*/
|
|
600
|
+
export function cleanupStalePluginDir() {
|
|
601
|
+
const actions = [];
|
|
602
|
+
const extensionsDir = getConfigFilePathSafe().replace(/openclaw\.json$/, "extensions");
|
|
603
|
+
const pluginDir = resolve(extensionsDir, "openclaw-channel-dmwork");
|
|
604
|
+
if (!existsSync(pluginDir))
|
|
605
|
+
return actions;
|
|
606
|
+
// Check if openclaw recognizes it
|
|
607
|
+
const inspect = pluginsInspect("openclaw-channel-dmwork");
|
|
608
|
+
if (inspect?.plugin)
|
|
609
|
+
return actions; // recognized, don't touch
|
|
610
|
+
// Check if it's in installs registry
|
|
611
|
+
try {
|
|
612
|
+
const cfg = readConfigFromFile();
|
|
613
|
+
if (cfg?.plugins?.installs?.["openclaw-channel-dmwork"]) {
|
|
614
|
+
return actions; // has install record, might just be inspect anomaly
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
catch { /* proceed with cleanup */ }
|
|
618
|
+
// All three conditions met: exists + not recognized + not in registry → stale
|
|
619
|
+
try {
|
|
620
|
+
rmSync(pluginDir, { recursive: true, force: true });
|
|
621
|
+
actions.push(`Removed stale plugin directory: ${pluginDir}`);
|
|
622
|
+
}
|
|
623
|
+
catch {
|
|
624
|
+
actions.push(`Warning: could not remove stale directory: ${pluginDir}`);
|
|
625
|
+
}
|
|
626
|
+
return actions;
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Clean up stale openclaw-install-stage directories that belong to DMWork.
|
|
630
|
+
* Only removes directories that:
|
|
631
|
+
* 1. Match .openclaw-install-stage-* pattern
|
|
632
|
+
* 2. Are older than 10 minutes (not a current installation)
|
|
633
|
+
* 3. Contain a package.json with name "openclaw-channel-dmwork"
|
|
634
|
+
*/
|
|
635
|
+
export function cleanupStaleStageDirectories() {
|
|
636
|
+
const actions = [];
|
|
637
|
+
const extensionsDir = getConfigFilePathSafe().replace(/openclaw\.json$/, "extensions");
|
|
638
|
+
try {
|
|
639
|
+
const entries = readdirSync(extensionsDir);
|
|
640
|
+
const now = Date.now();
|
|
641
|
+
const TEN_MINUTES = 10 * 60 * 1000;
|
|
642
|
+
for (const entry of entries) {
|
|
643
|
+
if (!entry.startsWith(".openclaw-install-stage-"))
|
|
644
|
+
continue;
|
|
645
|
+
const stagePath = resolve(extensionsDir, entry);
|
|
646
|
+
try {
|
|
647
|
+
const stat = statSync(stagePath);
|
|
648
|
+
if (!stat.isDirectory())
|
|
649
|
+
continue;
|
|
650
|
+
if (now - stat.mtimeMs < TEN_MINUTES)
|
|
651
|
+
continue; // too recent, skip
|
|
652
|
+
// Check if it's DMWork's stage directory
|
|
653
|
+
const pkgPath = resolve(stagePath, "package", "package.json");
|
|
654
|
+
const altPkgPath = resolve(stagePath, "package.json");
|
|
655
|
+
let isDmwork = false;
|
|
656
|
+
for (const p of [pkgPath, altPkgPath]) {
|
|
657
|
+
try {
|
|
658
|
+
const pkg = JSON.parse(readFileSync(p, "utf-8"));
|
|
659
|
+
if (pkg.name === "openclaw-channel-dmwork") {
|
|
660
|
+
isDmwork = true;
|
|
661
|
+
break;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
catch { /* try next */ }
|
|
665
|
+
}
|
|
666
|
+
if (!isDmwork)
|
|
667
|
+
continue; // not ours, don't touch
|
|
668
|
+
rmSync(stagePath, { recursive: true, force: true });
|
|
669
|
+
actions.push(`Removed stale stage directory: ${entry}`);
|
|
670
|
+
}
|
|
671
|
+
catch { /* skip this entry */ }
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
catch { /* best effort */ }
|
|
675
|
+
return actions;
|
|
676
|
+
}
|
|
677
|
+
// ---------------------------------------------------------------------------
|
|
678
|
+
// Atomic config write
|
|
679
|
+
// ---------------------------------------------------------------------------
|
|
680
|
+
/**
|
|
681
|
+
* Write openclaw.json atomically: write to .tmp then rename.
|
|
682
|
+
* Prevents gateway watcher from reading half-written/truncated JSON.
|
|
683
|
+
*/
|
|
684
|
+
export function writeConfigAtomic(cfg) {
|
|
685
|
+
const configPath = getConfigFilePathSafe();
|
|
686
|
+
const tmpPath = configPath + ".tmp";
|
|
687
|
+
writeFileSync(tmpPath, JSON.stringify(cfg, null, 2), "utf-8");
|
|
688
|
+
renameSync(tmpPath, configPath);
|
|
689
|
+
}
|
|
690
|
+
export function detectScenario() {
|
|
691
|
+
const cfg = readConfigFromFile();
|
|
692
|
+
const extDir = getConfigFilePathSafe().replace(/openclaw\.json$/, "extensions");
|
|
693
|
+
const hasLegacyDir = existsSync(resolve(extDir, "dmwork"));
|
|
694
|
+
const hasLegacyEntries = Boolean(cfg?.plugins?.entries?.["dmwork"]);
|
|
695
|
+
const hasLegacyInstalls = Boolean(cfg?.plugins?.installs?.["dmwork"]);
|
|
696
|
+
const hasLegacy = hasLegacyDir || hasLegacyEntries || hasLegacyInstalls;
|
|
697
|
+
const hasNewDir = existsSync(resolve(extDir, "openclaw-channel-dmwork"));
|
|
698
|
+
const hasNewEntries = Boolean(cfg?.plugins?.entries?.["openclaw-channel-dmwork"]);
|
|
699
|
+
const hasNewInstalls = Boolean(cfg?.plugins?.installs?.["openclaw-channel-dmwork"]);
|
|
700
|
+
const inspectOk = Boolean(pluginsInspect("openclaw-channel-dmwork")?.plugin);
|
|
701
|
+
const isHealthy = inspectOk || (hasNewDir && hasNewEntries && hasNewInstalls);
|
|
702
|
+
const hasNewPartial = (hasNewDir || hasNewEntries || hasNewInstalls) && !isHealthy;
|
|
703
|
+
const hasDmworkChannel = Boolean(cfg?.channels?.dmwork);
|
|
704
|
+
if (hasLegacy)
|
|
705
|
+
return "legacy";
|
|
706
|
+
if (isHealthy)
|
|
707
|
+
return "update";
|
|
708
|
+
if (hasNewPartial)
|
|
709
|
+
return "broken";
|
|
710
|
+
if (hasDmworkChannel)
|
|
711
|
+
return "deadlock";
|
|
712
|
+
return "fresh";
|
|
713
|
+
}
|
|
714
|
+
export function isHealthyInstall() {
|
|
715
|
+
const cfg = readConfigFromFile();
|
|
716
|
+
const extDir = getConfigFilePathSafe().replace(/openclaw\.json$/, "extensions");
|
|
717
|
+
const hasNewDir = existsSync(resolve(extDir, "openclaw-channel-dmwork"));
|
|
718
|
+
const hasNewEntries = Boolean(cfg?.plugins?.entries?.["openclaw-channel-dmwork"]);
|
|
719
|
+
const hasNewInstalls = Boolean(cfg?.plugins?.installs?.["openclaw-channel-dmwork"]);
|
|
720
|
+
const inspectOk = Boolean(pluginsInspect("openclaw-channel-dmwork")?.plugin);
|
|
721
|
+
return inspectOk || (hasNewDir && hasNewEntries && hasNewInstalls);
|
|
722
|
+
}
|
|
723
|
+
export function ensurePluginsAllow() {
|
|
724
|
+
try {
|
|
725
|
+
const cfg = readConfigFromFile();
|
|
726
|
+
if (!cfg?.plugins?.allow || !Array.isArray(cfg.plugins.allow))
|
|
727
|
+
return;
|
|
728
|
+
if (cfg.plugins.allow.includes("openclaw-channel-dmwork"))
|
|
729
|
+
return;
|
|
730
|
+
cfg.plugins.allow.push("openclaw-channel-dmwork");
|
|
731
|
+
writeConfigAtomic(cfg);
|
|
732
|
+
}
|
|
733
|
+
catch { /* best effort */ }
|
|
734
|
+
}
|
|
735
|
+
// ---------------------------------------------------------------------------
|
|
736
|
+
// pluginsUpdateCompat
|
|
737
|
+
// ---------------------------------------------------------------------------
|
|
738
|
+
export function pluginsUpdateCompat(id, tag, quiet) {
|
|
739
|
+
try {
|
|
740
|
+
const result = execFileSync(OPENCLAW, ["plugins", "update", id], {
|
|
741
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
742
|
+
encoding: "utf-8",
|
|
743
|
+
});
|
|
744
|
+
if (!quiet && result)
|
|
745
|
+
process.stdout.write(result);
|
|
746
|
+
}
|
|
747
|
+
catch (err) {
|
|
748
|
+
// Only fallback to install when update is unsupported or plugin not installed.
|
|
749
|
+
// Other errors (network, permissions, etc.) should propagate.
|
|
750
|
+
if (isUnsupportedOptionError(err) || isPluginNotInstalledError(err)) {
|
|
751
|
+
pluginsInstall(`${id}@${tag}`, quiet, true);
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
if (!quiet) {
|
|
755
|
+
const stdout = err?.stdout?.toString?.();
|
|
756
|
+
const stderr = err?.stderr?.toString?.();
|
|
757
|
+
if (stdout)
|
|
758
|
+
process.stdout.write(stdout);
|
|
759
|
+
if (stderr)
|
|
760
|
+
process.stderr.write(stderr);
|
|
761
|
+
}
|
|
762
|
+
throw err;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
// ---------------------------------------------------------------------------
|
|
766
|
+
// Legacy migration helpers
|
|
767
|
+
// ---------------------------------------------------------------------------
|
|
768
|
+
export function renameLegacyDir() {
|
|
769
|
+
const extDir = getConfigFilePathSafe().replace(/openclaw\.json$/, "extensions");
|
|
770
|
+
const legacyDir = resolve(extDir, "dmwork");
|
|
771
|
+
const backupDir = resolve(extDir, ".dmwork-backup");
|
|
772
|
+
if (!existsSync(legacyDir))
|
|
773
|
+
return false;
|
|
774
|
+
try {
|
|
775
|
+
if (existsSync(backupDir))
|
|
776
|
+
rmSync(backupDir, { recursive: true, force: true });
|
|
777
|
+
renameSync(legacyDir, backupDir);
|
|
778
|
+
return true;
|
|
779
|
+
}
|
|
780
|
+
catch {
|
|
781
|
+
return false;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
export function restoreLegacyDir() {
|
|
785
|
+
const extDir = getConfigFilePathSafe().replace(/openclaw\.json$/, "extensions");
|
|
786
|
+
const legacyDir = resolve(extDir, "dmwork");
|
|
787
|
+
const backupDir = resolve(extDir, ".dmwork-backup");
|
|
788
|
+
if (!existsSync(backupDir))
|
|
789
|
+
return;
|
|
790
|
+
try {
|
|
791
|
+
if (existsSync(legacyDir))
|
|
792
|
+
rmSync(legacyDir, { recursive: true, force: true });
|
|
793
|
+
renameSync(backupDir, legacyDir);
|
|
794
|
+
}
|
|
795
|
+
catch { /* best effort */ }
|
|
796
|
+
}
|
|
797
|
+
export function deleteLegacyBackup() {
|
|
798
|
+
const extDir = getConfigFilePathSafe().replace(/openclaw\.json$/, "extensions");
|
|
799
|
+
const backupDir = resolve(extDir, ".dmwork-backup");
|
|
800
|
+
if (existsSync(backupDir)) {
|
|
801
|
+
try {
|
|
802
|
+
rmSync(backupDir, { recursive: true, force: true });
|
|
803
|
+
}
|
|
804
|
+
catch { /* best effort */ }
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
export function removeLegacyFromConfig() {
|
|
808
|
+
try {
|
|
809
|
+
const cfg = readConfigFromFile();
|
|
810
|
+
if (!cfg)
|
|
811
|
+
return;
|
|
812
|
+
if (cfg.plugins?.entries?.["dmwork"])
|
|
813
|
+
delete cfg.plugins.entries["dmwork"];
|
|
814
|
+
if (cfg.plugins?.installs?.["dmwork"])
|
|
815
|
+
delete cfg.plugins.installs["dmwork"];
|
|
816
|
+
if (Array.isArray(cfg.plugins?.allow)) {
|
|
817
|
+
cfg.plugins.allow = cfg.plugins.allow.filter((id) => id !== "dmwork");
|
|
818
|
+
}
|
|
819
|
+
if (cfg.channels?.dmwork)
|
|
820
|
+
delete cfg.channels.dmwork;
|
|
821
|
+
writeConfigAtomic(cfg);
|
|
822
|
+
}
|
|
823
|
+
catch { /* best effort */ }
|
|
824
|
+
}
|
|
825
|
+
export function saveChannelConfigToDisk() {
|
|
826
|
+
try {
|
|
827
|
+
const backupPath = getConfigFilePathSafe().replace(/openclaw\.json$/, "channels-dmwork-backup.json");
|
|
828
|
+
const cfg = readConfigFromFile();
|
|
829
|
+
const dmwork = cfg?.channels?.dmwork;
|
|
830
|
+
if (dmwork) {
|
|
831
|
+
writeFileSync(backupPath, JSON.stringify(dmwork, null, 2), "utf-8");
|
|
832
|
+
}
|
|
833
|
+
else {
|
|
834
|
+
// No channels.dmwork — remove stale backup to prevent wrong restore
|
|
835
|
+
if (existsSync(backupPath))
|
|
836
|
+
rmSync(backupPath, { force: true });
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
catch { /* best effort */ }
|
|
840
|
+
}
|
|
841
|
+
export function restoreChannelConfigFromDisk() {
|
|
842
|
+
try {
|
|
843
|
+
const backupPath = getConfigFilePathSafe().replace(/openclaw\.json$/, "channels-dmwork-backup.json");
|
|
844
|
+
if (!existsSync(backupPath))
|
|
845
|
+
return;
|
|
846
|
+
let dmwork = JSON.parse(readFileSync(backupPath, "utf-8"));
|
|
847
|
+
// Migrate flat config → accounts.default
|
|
848
|
+
if (dmwork.botToken && !dmwork.accounts) {
|
|
849
|
+
dmwork = {
|
|
850
|
+
...dmwork,
|
|
851
|
+
accounts: { default: { botToken: dmwork.botToken, apiUrl: dmwork.apiUrl } },
|
|
852
|
+
};
|
|
853
|
+
delete dmwork.botToken;
|
|
854
|
+
}
|
|
855
|
+
const cfg = readConfigFromFile();
|
|
856
|
+
if (!cfg)
|
|
857
|
+
return;
|
|
858
|
+
if (!cfg.channels)
|
|
859
|
+
cfg.channels = {};
|
|
860
|
+
cfg.channels.dmwork = dmwork;
|
|
861
|
+
writeConfigAtomic(cfg);
|
|
862
|
+
rmSync(backupPath, { force: true });
|
|
863
|
+
}
|
|
864
|
+
catch { /* best effort */ }
|
|
865
|
+
}
|
|
866
|
+
export function cleanupBrokenInstall() {
|
|
867
|
+
const actions = [];
|
|
868
|
+
const cfg = readConfigFromFile();
|
|
869
|
+
const extDir = getConfigFilePathSafe().replace(/openclaw\.json$/, "extensions");
|
|
870
|
+
const pluginDir = resolve(extDir, "openclaw-channel-dmwork");
|
|
871
|
+
const hasDir = existsSync(pluginDir);
|
|
872
|
+
const hasEntries = Boolean(cfg?.plugins?.entries?.["openclaw-channel-dmwork"]);
|
|
873
|
+
const hasInstalls = Boolean(cfg?.plugins?.installs?.["openclaw-channel-dmwork"]);
|
|
874
|
+
// Use same healthy definition as detectScenario(): inspect OK OR all 3 artifacts present
|
|
875
|
+
const inspectOk = Boolean(pluginsInspect("openclaw-channel-dmwork")?.plugin);
|
|
876
|
+
const isHealthy = inspectOk || (hasDir && hasEntries && hasInstalls);
|
|
877
|
+
if (isHealthy)
|
|
878
|
+
return actions; // Actually healthy, nothing to clean
|
|
879
|
+
// Remove directory if it exists (orphan or partial)
|
|
880
|
+
if (hasDir) {
|
|
881
|
+
try {
|
|
882
|
+
rmSync(pluginDir, { recursive: true, force: true });
|
|
883
|
+
actions.push("Removed broken/orphan plugin directory");
|
|
884
|
+
}
|
|
885
|
+
catch { /* best effort */ }
|
|
886
|
+
}
|
|
887
|
+
// Remove stale config entries
|
|
888
|
+
if (cfg && (hasEntries || hasInstalls)) {
|
|
889
|
+
let changed = false;
|
|
890
|
+
if (hasEntries) {
|
|
891
|
+
delete cfg.plugins.entries["openclaw-channel-dmwork"];
|
|
892
|
+
changed = true;
|
|
893
|
+
}
|
|
894
|
+
if (hasInstalls) {
|
|
895
|
+
delete cfg.plugins.installs["openclaw-channel-dmwork"];
|
|
896
|
+
changed = true;
|
|
897
|
+
}
|
|
898
|
+
if (changed) {
|
|
899
|
+
writeConfigAtomic(cfg);
|
|
900
|
+
actions.push("Cleaned stale config entries");
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
return actions;
|
|
904
|
+
}
|
|
382
905
|
//# sourceMappingURL=openclaw-cli.js.map
|