@lark-apaas/openclaw-scripts-diagnose-cli 0.1.1-alpha.8 → 0.1.1-beta.0

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/index.cjs CHANGED
@@ -192,14 +192,14 @@ function fileExists(filePath) {
192
192
  return node_fs.default.existsSync(filePath);
193
193
  }
194
194
  /** Execute a shell command, return stdout. Throws on failure. */
195
- function shell(cmd, timeoutMs = 1e4) {
195
+ function shell(cmd, timeoutMs = 6e4) {
196
196
  return (0, node_child_process.execSync)(cmd, {
197
197
  encoding: "utf-8",
198
198
  timeout: timeoutMs
199
199
  }).trim();
200
200
  }
201
201
  //#endregion
202
- //#region \0@oxc-project+runtime@0.121.0/helpers/decorate.js
202
+ //#region \0@oxc-project+runtime@0.115.0/helpers/decorate.js
203
203
  function __decorate(decorators, target, key, desc) {
204
204
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
205
205
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -245,11 +245,15 @@ function findBackupFiles(configPath) {
245
245
  }
246
246
  /**
247
247
  * Among backup files, find the one with the highest numeric suffix.
248
- * `.bak` (no number) is treated as 0, `.bak1` as 1, `.bak2` as 2, etc.
248
+ * Supports all three naming styles used by the current backup code and its
249
+ * older variants:
250
+ * `.bak` → n = 0 (legacy single-slot backup)
251
+ * `.bakN` → n = N (older style, dot-less)
252
+ * `.bak.N` → n = N (current style written by reset Step 1)
249
253
  */
250
254
  function findHighestBackup(backupFiles) {
251
255
  if (backupFiles.length === 0) return null;
252
- const bakRegex = /\.bak(\d*)$/;
256
+ const bakRegex = /\.bak\.?(\d*)$/;
253
257
  let best = null;
254
258
  for (const f of backupFiles) {
255
259
  const match = bakRegex.exec(f);
@@ -686,7 +690,9 @@ let AllowedOriginsRule = class AllowedOriginsRule extends DiagnoseRule {
686
690
  validate(ctx) {
687
691
  const expected = getExpectedOrigins(ctx.vars);
688
692
  if (expected.length === 0) return { pass: true };
689
- const missing = findMissing(getCurrentOrigins(ctx.config), expected);
693
+ const current = getCurrentOrigins(ctx.config);
694
+ if (hasWildcard(current)) return { pass: true };
695
+ const missing = findMissing(current, expected);
690
696
  if (missing.length === 0) return { pass: true };
691
697
  return {
692
698
  pass: false,
@@ -696,6 +702,7 @@ let AllowedOriginsRule = class AllowedOriginsRule extends DiagnoseRule {
696
702
  repair(ctx) {
697
703
  const expected = getExpectedOrigins(ctx.vars);
698
704
  const current = getCurrentOrigins(ctx.config);
705
+ if (hasWildcard(current)) return;
699
706
  const missing = findMissing(current, expected);
700
707
  if (missing.length > 0) {
701
708
  const seen = /* @__PURE__ */ new Set();
@@ -735,6 +742,10 @@ function findMissing(current, expected) {
735
742
  const set = new Set(current);
736
743
  return expected.filter((o) => !set.has(o));
737
744
  }
745
+ /** Exact "*" entry means allow-all; pattern globs like "https://*.example.com" don't count. */
746
+ function hasWildcard(origins) {
747
+ return origins.includes("*");
748
+ }
738
749
  //#endregion
739
750
  //#region src/rules/jwt-token.ts
740
751
  let JwtTokenRule = class JwtTokenRule extends DiagnoseRule {
@@ -985,45 +996,34 @@ function runRepair(input) {
985
996
  }
986
997
  }
987
998
  //#endregion
988
- //#region src/backup.ts
989
- const BACKUP_PATH = "/home/gem/workspace/.force/openclaw/core-backup.json";
990
- function runBackup(input) {
999
+ //#region src/paths.ts
1000
+ /**
1001
+ * Central directory for all ephemeral diagnose/reset artifacts: task status
1002
+ * files (`reset-<taskId>.json`) and human-readable step logs
1003
+ * (`reset-<taskId>.log`). Having everything under one dir makes debugging a
1004
+ * stuck reset much easier — `ls /tmp/openclaw-diagnose/` shows every recent
1005
+ * run, and each run's log is right next to its state.
1006
+ */
1007
+ const DIAGNOSE_DIR = "/tmp/openclaw-diagnose";
1008
+ function resetResultFile(taskId) {
1009
+ return `${DIAGNOSE_DIR}/reset-${taskId}.json`;
1010
+ }
1011
+ function resetLogFile(taskId) {
1012
+ return `${DIAGNOSE_DIR}/reset-${taskId}.log`;
1013
+ }
1014
+ //#endregion
1015
+ //#region src/logger.ts
1016
+ function makeLogger(logFile) {
991
1017
  try {
992
- const { configPath } = input;
1018
+ const dir = node_path.default.dirname(logFile);
1019
+ if (!node_fs.default.existsSync(dir)) node_fs.default.mkdirSync(dir, { recursive: true });
1020
+ } catch {}
1021
+ return (msg) => {
1022
+ const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}\n`;
993
1023
  try {
994
- const validateOutput = shell("openclaw config validate --json");
995
- if (!JSON.parse(validateOutput).valid) return {
996
- success: false,
997
- error: "config validation failed"
998
- };
999
- } catch (e) {
1000
- return {
1001
- success: false,
1002
- error: "config validate command failed: " + e.message
1003
- };
1004
- }
1005
- if (!fileExists(configPath)) return {
1006
- success: false,
1007
- error: "config file not found: " + configPath
1008
- };
1009
- const config = loadJSON5().parse(readFile(configPath));
1010
- const backup = { _backup_meta: { created_at: (/* @__PURE__ */ new Date()).toISOString() } };
1011
- if (config.agents) backup.agents = config.agents;
1012
- if (config.bindings) backup.bindings = config.bindings;
1013
- const feishu = config.channels?.feishu;
1014
- if (feishu?.accounts) backup.channels = { feishu: { accounts: feishu.accounts } };
1015
- const backupDir = node_path.default.dirname(BACKUP_PATH);
1016
- if (!node_fs.default.existsSync(backupDir)) node_fs.default.mkdirSync(backupDir, { recursive: true });
1017
- const tmpPath = BACKUP_PATH + ".tmp";
1018
- node_fs.default.writeFileSync(tmpPath, JSON.stringify(backup, null, 2), "utf-8");
1019
- node_fs.default.renameSync(tmpPath, BACKUP_PATH);
1020
- return { success: true };
1021
- } catch (e) {
1022
- return {
1023
- success: false,
1024
- error: "backup failed: " + e.message
1025
- };
1026
- }
1024
+ node_fs.default.appendFileSync(logFile, line);
1025
+ } catch {}
1026
+ };
1027
1027
  }
1028
1028
  //#endregion
1029
1029
  //#region src/reset-async.ts
@@ -1034,7 +1034,9 @@ function runBackup(input) {
1034
1034
  */
1035
1035
  function startAsyncReset(ctxBase64) {
1036
1036
  const taskId = (0, node_crypto.randomUUID)();
1037
- const resultFile = `/tmp/openclaw-reset-${taskId}.json`;
1037
+ const resultFile = resetResultFile(taskId);
1038
+ const log = makeLogger(resetLogFile(taskId));
1039
+ log(`=== startAsyncReset spawning worker for taskId=${taskId} ===`);
1038
1040
  const initial = {
1039
1041
  status: "running",
1040
1042
  step: 0,
@@ -1058,6 +1060,7 @@ function startAsyncReset(ctxBase64) {
1058
1060
  stdio: "ignore"
1059
1061
  });
1060
1062
  child.on("error", (err) => {
1063
+ log(`FATAL worker failed to start: ${err.message}`);
1061
1064
  const failResult = {
1062
1065
  status: "failed",
1063
1066
  step: 0,
@@ -1072,6 +1075,7 @@ function startAsyncReset(ctxBase64) {
1072
1075
  node_fs.default.renameSync(errTmpPath, resultFile);
1073
1076
  });
1074
1077
  child.unref();
1078
+ log(`spawned worker pid=${child.pid}`);
1075
1079
  return { taskId };
1076
1080
  }
1077
1081
  //#endregion
@@ -1080,14 +1084,16 @@ const STEPS = [
1080
1084
  "备份当前配置",
1081
1085
  "生成默认配置",
1082
1086
  "杀掉 openclaw 进程",
1083
- "重装 openclaw",
1087
+ "等待沙箱初始化完成",
1088
+ "确认 openclaw 版本",
1084
1089
  "合并核心备份配置",
1085
1090
  "复制启动脚本",
1086
- "重装内置插件",
1091
+ "安装扩展",
1087
1092
  "启动并验证"
1088
1093
  ];
1089
1094
  const TOTAL_STEPS = STEPS.length;
1090
- const CORE_BACKUP_PATH = "/home/gem/workspace/.force/openclaw/core-backup.json";
1095
+ /** Pre-packed extensions archive on OSS. Update this URL when releasing a new version. */
1096
+ const EXTENSIONS_OSS_URL = "https://miaoda-template-online.oss-cn-beijing.aliyuncs.com/builtin/tool/pkg/openclaw-extensions-2026.4.9.tar.gz";
1091
1097
  /**
1092
1098
  * Directory holding the bundled openclaw template (openclaw.json + scripts/).
1093
1099
  * Synced from git@code.byted.org:apaas/miaoda-openclaw-template.git via
@@ -1134,8 +1140,11 @@ function markFailed(resultFile, step, error, startedAt) {
1134
1140
  });
1135
1141
  }
1136
1142
  /** Step 1: Backup current config as openclaw.json.bak.N */
1137
- function backupCurrentConfig(configPath) {
1138
- if (!fileExists(configPath)) return;
1143
+ function backupCurrentConfig(configPath, log) {
1144
+ if (!fileExists(configPath)) {
1145
+ log("no existing config, skip backup");
1146
+ return;
1147
+ }
1139
1148
  const dir = node_path.default.dirname(configPath);
1140
1149
  let maxN = 0;
1141
1150
  try {
@@ -1147,90 +1156,321 @@ function backupCurrentConfig(configPath) {
1147
1156
  }
1148
1157
  }
1149
1158
  } catch {}
1150
- node_fs.default.copyFileSync(configPath, configPath + ".bak." + (maxN + 1));
1159
+ const bakPath = configPath + ".bak." + (maxN + 1);
1160
+ node_fs.default.copyFileSync(configPath, bakPath);
1161
+ log(`backed up to ${bakPath}`);
1151
1162
  }
1152
1163
  /** Step 2: Replace $$__XXX__ placeholders and write default config. */
1153
- function generateDefaultConfig(srcDir, configPath, templateVars) {
1164
+ function generateDefaultConfig(srcDir, configPath, templateVars, log) {
1154
1165
  const srcConfigPath = node_path.default.join(srcDir, "openclaw.json");
1155
1166
  if (!fileExists(srcConfigPath)) throw new Error("template openclaw.json not found at " + srcConfigPath);
1156
1167
  let content = node_fs.default.readFileSync(srcConfigPath, "utf-8");
1157
- for (const [placeholder, value] of Object.entries(templateVars)) content = content.split(placeholder).join(value);
1168
+ let replaced = 0;
1169
+ for (const [placeholder, value] of Object.entries(templateVars)) {
1170
+ const parts = content.split(placeholder);
1171
+ if (parts.length > 1) replaced += parts.length - 1;
1172
+ content = parts.join(value);
1173
+ }
1158
1174
  node_fs.default.writeFileSync(configPath, content, "utf-8");
1175
+ log(`wrote ${configPath} (${replaced} placeholder(s) replaced, ${Object.keys(templateVars).length} provided)`);
1159
1176
  }
1160
1177
  /** Step 3: Kill all openclaw processes. */
1161
- function killOpenclawProcesses() {
1178
+ function killOpenclawProcesses(log) {
1162
1179
  try {
1163
1180
  shell("pkill -f openclaw-gateway || true", 5e3);
1164
1181
  } catch {}
1165
1182
  shell("sleep 2", 5e3);
1183
+ log("killed openclaw-gateway processes");
1184
+ }
1185
+ /**
1186
+ * Step 4: Wait for the sandbox's own init (init_sandbox.sh / concurrent npm
1187
+ * install) to finish before we start our own npm i -g. Two npm processes
1188
+ * sharing ~/.npm cache + competing for disk/network just makes everything
1189
+ * crawl; letting init finish first is the cleanest way to get exclusive
1190
+ * access. Polls every 10s up to `maxWaitMs`. If the deadline is hit we fall
1191
+ * through anyway — better to try than to fail the reset outright.
1192
+ */
1193
+ function waitForInitNpm(maxWaitMs, log) {
1194
+ const deadline = Date.now() + maxWaitMs;
1195
+ const ownPid = String(process.pid);
1196
+ let polls = 0;
1197
+ while (Date.now() < deadline) {
1198
+ polls++;
1199
+ let running = 0;
1200
+ try {
1201
+ const out = shell(`pgrep -af "init_sandbox.sh|npm install|npm i " | grep -v -- "${ownPid}" | wc -l`, 1e4);
1202
+ running = parseInt(out.trim(), 10) || 0;
1203
+ } catch {
1204
+ log(`poll ${polls}: no concurrent npm, proceeding`);
1205
+ return;
1206
+ }
1207
+ if (running === 0) {
1208
+ log(`poll ${polls}: no concurrent npm, proceeding`);
1209
+ return;
1210
+ }
1211
+ log(`poll ${polls}: ${running} concurrent npm/init process(es) still running, waiting 10s`);
1212
+ try {
1213
+ shell("sleep 10", 12e3);
1214
+ } catch {}
1215
+ }
1216
+ log(`deadline (${maxWaitMs}ms) hit after ${polls} poll(s), proceeding anyway`);
1217
+ }
1218
+ /**
1219
+ * Step 5: Ensure openclaw binary is at the template's recommended version.
1220
+ *
1221
+ * Only checks/installs the binary — does NOT run `doctor --fix` any more.
1222
+ * Extensions are installed separately via OSS tar.gz (Step 8), which means
1223
+ * the openclaw-lark extension schema is already in place when openclaw
1224
+ * starts, avoiding the schema-priority mismatch that caused doctor to
1225
+ * reject valid config fields like threadSession / footer.
1226
+ */
1227
+ function ensureOpenclawBinary(srcDir, configPath, log) {
1228
+ const targetVersion = loadJSON5().parse(node_fs.default.readFileSync(node_path.default.join(srcDir, "openclaw.json"), "utf-8")).meta?.lastTouchedVersion;
1229
+ log(`target openclaw version: ${targetVersion ?? "<unset>"}`);
1230
+ if (targetVersion && isOpenclawAtVersion(targetVersion)) {
1231
+ log("openclaw already at target version, nothing to do");
1232
+ return;
1233
+ }
1234
+ if (isOpenclawInstalled()) {
1235
+ const updateCmd = `openclaw update${targetVersion ? " --tag " + targetVersion : ""} --yes --no-restart`;
1236
+ log(`openclaw installed but version mismatched, running: ${updateCmd}`);
1237
+ const timeout = 12 * 6e4;
1238
+ const t = Date.now();
1239
+ const tmpConfig = configPath + ".update-tmp";
1240
+ const hadConfig = node_fs.default.existsSync(configPath);
1241
+ if (hadConfig) {
1242
+ node_fs.default.renameSync(configPath, tmpConfig);
1243
+ log("temporarily hid config to bypass config guard");
1244
+ }
1245
+ try {
1246
+ shell(updateCmd, timeout);
1247
+ log(`openclaw update done in ${Date.now() - t}ms`);
1248
+ } catch (e) {
1249
+ const elapsed = Date.now() - t;
1250
+ if (elapsed >= timeout - 1e3) log(`openclaw update timed out after ${elapsed}ms (non-fatal, continuing)`);
1251
+ else throw e;
1252
+ } finally {
1253
+ if (hadConfig && node_fs.default.existsSync(tmpConfig)) {
1254
+ node_fs.default.renameSync(tmpConfig, configPath);
1255
+ log("restored config after update");
1256
+ }
1257
+ }
1258
+ } else {
1259
+ log("openclaw binary not found, running full reinstall");
1260
+ fullReinstall(targetVersion, log);
1261
+ }
1166
1262
  }
1167
- /** Step 4: Reinstall openclaw to the version specified in template. */
1168
- function reinstallOpenclaw(srcDir) {
1169
- const version = loadJSON5().parse(node_fs.default.readFileSync(node_path.default.join(srcDir, "openclaw.json"), "utf-8")).meta?.lastTouchedVersion;
1263
+ /** Check if openclaw command exists (regardless of version). */
1264
+ function isOpenclawInstalled() {
1170
1265
  try {
1171
- shell("npm uninstall -g openclaw 2>/dev/null || true", 3e4);
1266
+ shell("which openclaw 2>/dev/null", 5e3);
1267
+ return true;
1268
+ } catch {
1269
+ return false;
1270
+ }
1271
+ }
1272
+ /** Full uninstall + reinstall from npm (slow path, triggers postinstall). */
1273
+ function fullReinstall(targetVersion, log) {
1274
+ try {
1275
+ shell("npm uninstall -g openclaw 2>/dev/null || true", 6e4);
1276
+ } catch {}
1277
+ try {
1278
+ shell("rm -rf /home/gem/.npm-global/lib/node_modules/openclaw /home/gem/.npm-global/bin/openclaw 2>/dev/null || true", 1e4);
1279
+ log("force-cleaned residual openclaw global dir");
1172
1280
  } catch {}
1173
- shell(`npm i -g openclaw@${version || "latest"}`, 6e5);
1174
- shell("openclaw doctor --fix", 12e4);
1281
+ const installCmd = `npm i -g openclaw@${targetVersion || "latest"}`;
1282
+ log(`running: ${installCmd}`);
1283
+ const t = Date.now();
1284
+ shell(installCmd, 30 * 6e4);
1285
+ log(`npm install done in ${Date.now() - t}ms`);
1175
1286
  }
1176
- /** Step 5: Merge core-backup.json into config + ensure allowedOrigins. */
1177
- function mergeCoreBackupAndOrigins(configPath, vars) {
1287
+ /** Return true if `openclaw --version` output contains `targetVersion`. */
1288
+ function isOpenclawAtVersion(targetVersion) {
1289
+ try {
1290
+ return shell("openclaw --version 2>&1 || true", 1e4).includes(targetVersion);
1291
+ } catch {
1292
+ return false;
1293
+ }
1294
+ }
1295
+ /** Step 6: Merge coreBackup from resetData + ensure allowedOrigins. */
1296
+ function mergeCoreBackupAndOrigins(configPath, vars, resetData, log) {
1178
1297
  const JSON5 = loadJSON5();
1179
- if (fileExists(CORE_BACKUP_PATH)) {
1180
- const backup = JSON.parse(node_fs.default.readFileSync(CORE_BACKUP_PATH, "utf-8"));
1298
+ const backup = resetData.coreBackup;
1299
+ if (backup) {
1181
1300
  const config = JSON5.parse(node_fs.default.readFileSync(configPath, "utf-8"));
1182
- if (backup.agents) config.agents = backup.agents;
1183
- if (backup.bindings) config.bindings = backup.bindings;
1184
- const backupAccounts = backup.channels?.feishu;
1185
- if (backupAccounts?.accounts) {
1301
+ const merged = [];
1302
+ if (backup.agents && backup.agents.length > 0) {
1303
+ if (!config.agents) config.agents = {};
1304
+ const agents = config.agents;
1305
+ if (!Array.isArray(agents.list)) agents.list = [];
1306
+ const configDir = node_path.default.dirname(configPath);
1307
+ for (const agent of backup.agents) {
1308
+ const enriched = {
1309
+ id: agent.id,
1310
+ name: agent.id,
1311
+ workspace: agent.workspace,
1312
+ agentDir: configDir + "/agents/" + agent.id + "/agent"
1313
+ };
1314
+ agents.list.push(enriched);
1315
+ }
1316
+ merged.push(`agents(+${backup.agents.length})`);
1317
+ const list = agents.list;
1318
+ let mainIdx = list.findIndex((a) => a.id === "main");
1319
+ if (mainIdx < 0) {
1320
+ list.unshift({ id: "main" });
1321
+ mainIdx = 0;
1322
+ }
1323
+ list[mainIdx].subagents = { allowAgents: ["*"] };
1324
+ list[mainIdx].default = true;
1325
+ merged.push("main-team-mode");
1326
+ const feishu = config.channels?.feishu;
1327
+ if (feishu) {
1328
+ if (!feishu.accounts) feishu.accounts = {};
1329
+ const accounts = feishu.accounts;
1330
+ const defaultAccount = {};
1331
+ for (const key of [
1332
+ "dmPolicy",
1333
+ "allowFrom",
1334
+ "groupPolicy",
1335
+ "groupAllowFrom"
1336
+ ]) if (feishu[key] !== void 0) defaultAccount[key] = feishu[key];
1337
+ if (Object.keys(defaultAccount).length > 0) {
1338
+ accounts.default = defaultAccount;
1339
+ merged.push("accounts.default");
1340
+ }
1341
+ }
1342
+ }
1343
+ if (backup.bindings && backup.bindings.length > 0) {
1344
+ config.bindings = backup.bindings;
1345
+ merged.push("bindings");
1346
+ }
1347
+ const backupAccounts = backup.channels?.feishu?.accounts;
1348
+ if (backupAccounts && Object.keys(backupAccounts).length > 0) {
1186
1349
  if (!config.channels) config.channels = {};
1187
1350
  const ch = config.channels;
1188
1351
  if (!ch.feishu) ch.feishu = {};
1189
- ch.feishu.accounts = backupAccounts.accounts;
1352
+ const feishu = ch.feishu;
1353
+ if (!feishu.accounts) feishu.accounts = {};
1354
+ Object.assign(feishu.accounts, backupAccounts);
1355
+ merged.push("channels.feishu.accounts");
1190
1356
  }
1191
1357
  node_fs.default.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
1192
- }
1358
+ log(`merged from coreBackup: [${merged.join(", ") || "nothing"}]`);
1359
+ } else log("no coreBackup in resetData, skip multi-agent merge");
1193
1360
  const expectedOrigins = Array.isArray(vars.expectedOrigins) ? vars.expectedOrigins : [];
1194
- if (expectedOrigins.length > 0) {
1195
- const config = JSON5.parse(node_fs.default.readFileSync(configPath, "utf-8"));
1196
- if (!config.gateway) config.gateway = {};
1197
- const gw = config.gateway;
1198
- if (!gw.controlUi) gw.controlUi = {};
1199
- const cui = gw.controlUi;
1200
- const current = Array.isArray(cui.allowedOrigins) ? cui.allowedOrigins.filter((o) => typeof o === "string") : [];
1201
- const seen = new Set(current);
1202
- const merged = [...current];
1203
- for (const o of expectedOrigins) if (!seen.has(o)) {
1204
- merged.push(o);
1205
- seen.add(o);
1206
- }
1207
- cui.allowedOrigins = merged;
1208
- node_fs.default.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
1361
+ if (expectedOrigins.length === 0) {
1362
+ log("no expectedOrigins provided");
1363
+ return;
1364
+ }
1365
+ const config = JSON5.parse(node_fs.default.readFileSync(configPath, "utf-8"));
1366
+ if (!config.gateway) config.gateway = {};
1367
+ const gw = config.gateway;
1368
+ if (!gw.controlUi) gw.controlUi = {};
1369
+ const cui = gw.controlUi;
1370
+ const current = Array.isArray(cui.allowedOrigins) ? cui.allowedOrigins.filter((o) => typeof o === "string") : [];
1371
+ if (current.includes("*")) {
1372
+ log("allowedOrigins already contains \"*\", skip origin merge");
1373
+ return;
1374
+ }
1375
+ const seen = new Set(current);
1376
+ const added = [];
1377
+ const mergedOrigins = [...current];
1378
+ for (const o of expectedOrigins) if (!seen.has(o)) {
1379
+ mergedOrigins.push(o);
1380
+ seen.add(o);
1381
+ added.push(o);
1209
1382
  }
1383
+ cui.allowedOrigins = mergedOrigins;
1384
+ node_fs.default.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
1385
+ log(`allowedOrigins: added ${added.length} (${JSON.stringify(added)}), total now ${mergedOrigins.length}`);
1210
1386
  }
1211
- /** Step 6: Copy startup scripts from template to agent dir. */
1212
- function copyStartupScripts(srcDir, configDir) {
1387
+ /** Step 7: Copy startup scripts from template to agent dir. */
1388
+ function copyStartupScripts(srcDir, configDir, log) {
1213
1389
  const srcScriptsDir = node_path.default.join(srcDir, "scripts");
1214
1390
  const targetScriptsDir = node_path.default.join(configDir, "scripts");
1215
- if (!node_fs.default.existsSync(srcScriptsDir)) return;
1391
+ if (!node_fs.default.existsSync(srcScriptsDir)) {
1392
+ log(`no scripts/ in template, skip`);
1393
+ return;
1394
+ }
1216
1395
  if (!node_fs.default.existsSync(targetScriptsDir)) node_fs.default.mkdirSync(targetScriptsDir, { recursive: true });
1217
1396
  shell(`cp -r '${srcScriptsDir}'/* '${targetScriptsDir}/'`, 1e4);
1397
+ log(`copied scripts/* -> ${targetScriptsDir}`);
1218
1398
  }
1219
- /** Step 7: Reinstall all plugins via openclaw CLI. */
1220
- function reinstallPlugins() {
1399
+ /**
1400
+ * Step 8: Install extensions from OSS tar.gz or fall back to `openclaw plugins update`.
1401
+ *
1402
+ * 1. Derive the OSS URL from the openclaw version in the template config
1403
+ * 2. Download tar.gz → extract to a staging dir
1404
+ * 3. For each included extension, backup (mv) the user's existing copy
1405
+ * to a temp dir — extensions NOT in the archive are left untouched
1406
+ * 4. Move the fresh extensions from staging to the target dir
1407
+ *
1408
+ * This bypasses npm entirely and avoids the schema-priority mismatch where
1409
+ * `openclaw doctor --fix` would validate config against the bundled feishu
1410
+ * plugin's strict schema before openclaw-lark is installed.
1411
+ *
1412
+ * Falls back to `openclaw plugins update --all` if the OSS download fails.
1413
+ */
1414
+ function installExtensions(configDir, log) {
1415
+ const ossUrl = EXTENSIONS_OSS_URL;
1416
+ const targetExtDir = node_path.default.join(configDir, "extensions");
1417
+ const tmpDir = node_path.default.join(configDir, `.ext-reset-tmp-${Date.now()}`);
1418
+ const tarPath = node_path.default.join(tmpDir, "extensions.tar.gz");
1419
+ node_fs.default.mkdirSync(tmpDir, { recursive: true });
1221
1420
  try {
1222
- shell("openclaw plugins update --all", 3e5);
1223
- } catch {}
1421
+ log(`downloading extensions from ${ossUrl}`);
1422
+ const dlStart = Date.now();
1423
+ shell(`curl -fsSL '${ossUrl}' -o '${tarPath}'`, 5 * 6e4);
1424
+ const size = node_fs.default.statSync(tarPath).size;
1425
+ log(`download done in ${Date.now() - dlStart}ms (${(size / 1024 / 1024).toFixed(1)}MB)`);
1426
+ shell(`tar -xzf '${tarPath}' -C '${tmpDir}'`, 6e4);
1427
+ const stagedExtDir = node_path.default.join(tmpDir, "extensions");
1428
+ if (!node_fs.default.existsSync(stagedExtDir)) throw new Error("tar.gz does not contain an extensions/ directory");
1429
+ const extNames = node_fs.default.readdirSync(stagedExtDir).filter((name) => node_fs.default.statSync(node_path.default.join(stagedExtDir, name)).isDirectory());
1430
+ log(`archive contains ${extNames.length} extension(s): ${extNames.join(", ")}`);
1431
+ for (const name of extNames) {
1432
+ const target = node_path.default.join(targetExtDir, name);
1433
+ if (node_fs.default.existsSync(target)) node_fs.default.rmSync(target, {
1434
+ recursive: true,
1435
+ force: true
1436
+ });
1437
+ node_fs.default.renameSync(node_path.default.join(stagedExtDir, name), target);
1438
+ log(` ${name}: installed`);
1439
+ }
1440
+ const packinfo = node_path.default.join(stagedExtDir, ".packinfo.json");
1441
+ if (node_fs.default.existsSync(packinfo)) {
1442
+ node_fs.default.copyFileSync(packinfo, node_path.default.join(targetExtDir, ".packinfo.json"));
1443
+ log(`packinfo: ${node_fs.default.readFileSync(packinfo, "utf-8").trim()}`);
1444
+ }
1445
+ log("extensions installed from OSS successfully");
1446
+ } finally {
1447
+ try {
1448
+ node_fs.default.rmSync(tmpDir, {
1449
+ recursive: true,
1450
+ force: true
1451
+ });
1452
+ } catch {}
1453
+ }
1224
1454
  }
1225
- /** Step 8: Write secrets/provider key files and restart openclaw. */
1226
- function writeSecretsAndRestart(vars, resetData, configDir) {
1227
- if (resetData.secretsContent && vars.secretsFilePath) writeFile(vars.secretsFilePath, resetData.secretsContent);
1228
- if (resetData.providerKeyContent && vars.providerFilePath) writeFile(vars.providerFilePath, resetData.providerKeyContent);
1455
+ /** Step 9: Write secrets/provider key files and restart openclaw. */
1456
+ function writeSecretsAndRestart(vars, resetData, configDir, log) {
1457
+ if (resetData.secretsContent && vars.secretsFilePath) {
1458
+ writeFile(vars.secretsFilePath, resetData.secretsContent);
1459
+ log(`wrote secrets to ${vars.secretsFilePath}`);
1460
+ }
1461
+ if (resetData.providerKeyContent && vars.providerFilePath) {
1462
+ writeFile(vars.providerFilePath, resetData.providerKeyContent);
1463
+ log(`wrote provider key to ${vars.providerFilePath}`);
1464
+ }
1229
1465
  const restartScript = node_path.default.join(configDir, "scripts", "restart.sh");
1230
- if (fileExists(restartScript)) shell(`bash '${restartScript}'`, 3e4);
1466
+ if (fileExists(restartScript)) {
1467
+ const t = Date.now();
1468
+ shell(`bash '${restartScript}'`, 3e4);
1469
+ log(`restart.sh done in ${Date.now() - t}ms`);
1470
+ } else log(`no restart.sh at ${restartScript}, skip`);
1231
1471
  }
1232
1472
  /**
1233
- * Run the 8-step reset process. Called from the worker entry point.
1473
+ * Run the 9-step reset process. Called from the worker entry point.
1234
1474
  *
1235
1475
  * Each step is an independent function. The orchestrator handles progress
1236
1476
  * reporting, error handling, and process-level exception guards.
@@ -1245,43 +1485,60 @@ function runReset(input, taskId, resultFile) {
1245
1485
  const configDir = node_path.default.dirname(configPath);
1246
1486
  const srcDir = TEMPLATE_DIR;
1247
1487
  let currentStep = 0;
1488
+ let stepStartedAt = Date.now();
1489
+ const log = makeLogger(resetLogFile(taskId));
1490
+ log(`=== reset started, taskId=${taskId}, pid=${process.pid} ===`);
1491
+ log(`configPath=${configPath}, configDir=${configDir}, templateDir=${srcDir}`);
1248
1492
  if (!node_fs.default.existsSync(node_path.default.join(srcDir, "openclaw.json"))) {
1249
- markFailed(resultFile, 0, `bundled template not found at ${srcDir}`, startedAt);
1493
+ const err = `bundled template not found at ${srcDir}`;
1494
+ log(`ERROR: ${err}`);
1495
+ markFailed(resultFile, 0, err, startedAt);
1250
1496
  process.exit(1);
1251
1497
  }
1252
1498
  process.on("uncaughtException", (err) => {
1499
+ log(`FATAL uncaughtException: ${err.message}\n${err.stack ?? ""}`);
1253
1500
  markFailed(resultFile, currentStep, `uncaught exception: ${err.message}`, startedAt);
1254
1501
  process.exit(1);
1255
1502
  });
1256
1503
  process.on("unhandledRejection", (reason) => {
1504
+ log(`FATAL unhandledRejection: ${String(reason)}`);
1257
1505
  markFailed(resultFile, currentStep, `unhandled rejection: ${reason}`, startedAt);
1258
1506
  process.exit(1);
1259
1507
  });
1260
- /** Advance to the next step, updating the progress file. */
1508
+ /** Advance to the next step, updating the progress file and logging a boundary. */
1261
1509
  const step = (n) => {
1510
+ if (currentStep > 0) log(`step ${currentStep} "${STEPS[currentStep - 1]}" done in ${Date.now() - stepStartedAt}ms`);
1262
1511
  currentStep = n;
1512
+ stepStartedAt = Date.now();
1513
+ log(`--- step ${n}/${TOTAL_STEPS}: ${STEPS[n - 1]} ---`);
1263
1514
  updateProgress(resultFile, n, startedAt);
1264
1515
  };
1265
1516
  try {
1266
1517
  step(1);
1267
- backupCurrentConfig(configPath);
1518
+ backupCurrentConfig(configPath, log);
1268
1519
  step(2);
1269
- generateDefaultConfig(srcDir, configPath, resetData.templateVars);
1520
+ generateDefaultConfig(srcDir, configPath, resetData.templateVars, log);
1270
1521
  step(3);
1271
- killOpenclawProcesses();
1522
+ killOpenclawProcesses(log);
1272
1523
  step(4);
1273
- reinstallOpenclaw(srcDir);
1524
+ waitForInitNpm(10 * 6e4, log);
1274
1525
  step(5);
1275
- mergeCoreBackupAndOrigins(configPath, vars);
1526
+ ensureOpenclawBinary(srcDir, configPath, log);
1276
1527
  step(6);
1277
- copyStartupScripts(srcDir, configDir);
1528
+ mergeCoreBackupAndOrigins(configPath, vars, resetData, log);
1278
1529
  step(7);
1279
- reinstallPlugins();
1530
+ copyStartupScripts(srcDir, configDir, log);
1280
1531
  step(8);
1281
- writeSecretsAndRestart(vars, resetData, configDir);
1532
+ installExtensions(configDir, log);
1533
+ step(9);
1534
+ writeSecretsAndRestart(vars, resetData, configDir, log);
1535
+ log(`step 9 "${STEPS[8]}" done in ${Date.now() - stepStartedAt}ms`);
1536
+ log("=== reset completed successfully ===");
1282
1537
  markDone(resultFile, startedAt);
1283
1538
  } catch (e) {
1284
- markFailed(resultFile, currentStep, e.message, startedAt);
1539
+ const err = e.message;
1540
+ log(`ERROR in step ${currentStep} "${STEPS[currentStep - 1] ?? "init"}" after ${Date.now() - stepStartedAt}ms: ${err}\n${e.stack ?? ""}`);
1541
+ markFailed(resultFile, currentStep, err, startedAt);
1285
1542
  process.exit(1);
1286
1543
  }
1287
1544
  }
@@ -1293,7 +1550,7 @@ function runReset(input, taskId, resultFile) {
1293
1550
  * Returns immediately on terminal states (done/failed).
1294
1551
  */
1295
1552
  function getResetTask(taskId) {
1296
- const resultFile = `/tmp/openclaw-reset-${taskId}.json`;
1553
+ const resultFile = resetResultFile(taskId);
1297
1554
  const deadline = Date.now() + 3e4;
1298
1555
  while (Date.now() < deadline) {
1299
1556
  if (!node_fs.default.existsSync(resultFile)) {
@@ -1340,16 +1597,6 @@ switch (mode) {
1340
1597
  else console.log(JSON.stringify(runRepair(input)));
1341
1598
  break;
1342
1599
  }
1343
- case "backup": {
1344
- const ctx = args.find((a) => a.startsWith("--ctx="))?.slice(6);
1345
- if (!ctx) {
1346
- console.error("Error: --ctx=<base64> is required");
1347
- node_process.default.exit(1);
1348
- }
1349
- const input = JSON.parse(Buffer.from(ctx, "base64").toString("utf-8"));
1350
- console.log(JSON.stringify(runBackup(input)));
1351
- break;
1352
- }
1353
1600
  case "reset":
1354
1601
  if (args.includes("--async")) {
1355
1602
  const ctx = args.find((a) => a.startsWith("--ctx="))?.slice(6);
@@ -1365,7 +1612,7 @@ switch (mode) {
1365
1612
  console.error("Error: --ctx=<base64> and --task-id=<id> are required for worker");
1366
1613
  node_process.default.exit(1);
1367
1614
  }
1368
- const resultFile = `/tmp/openclaw-reset-${taskId}.json`;
1615
+ const resultFile = resetResultFile(taskId);
1369
1616
  runReset(JSON.parse(Buffer.from(ctx, "base64").toString("utf-8")), taskId, resultFile);
1370
1617
  } else {
1371
1618
  console.error("Usage: reset --async --ctx=<base64> | reset --worker --task-id=<id> --ctx=<base64>");
@@ -1382,7 +1629,7 @@ switch (mode) {
1382
1629
  break;
1383
1630
  }
1384
1631
  default:
1385
- console.error("Usage: mclaw-diagnose <check|repair|backup|reset|get_reset_task> [options]");
1632
+ console.error("Usage: mclaw-diagnose <check|repair|reset|get_reset_task> [options]");
1386
1633
  node_process.default.exit(1);
1387
1634
  }
1388
1635
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lark-apaas/openclaw-scripts-diagnose-cli",
3
- "version": "0.1.1-alpha.8",
3
+ "version": "0.1.1-beta.0",
4
4
  "description": "CLI for OpenClaw config diagnose and repair with JSON5 support",
5
5
  "main": "dist/index.cjs",
6
6
  "bin": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "meta": {
3
3
  "lastTouchedVersion": "2026.4.9",
4
- "lastTouchedAt": "2026-04-11T09:30:21.703Z"
4
+ "lastTouchedAt": "2026-04-13T03:14:33.386Z"
5
5
  },
6
6
  "wizard": {
7
7
  "lastRunAt": "2026-03-30T14:54:56.160Z",
@@ -448,14 +448,27 @@
448
448
  "plugins": {
449
449
  "allow": [
450
450
  "openclaw-lark",
451
- "openclaw-extension-miaoda",
452
451
  "openclaw-extension-miaoda-coding",
453
- "browser"
452
+ "browser",
453
+ "openclaw-mem0-plugin",
454
+ "openclaw-extension-miaoda"
454
455
  ],
455
456
  "entries": {
456
457
  "feishu": {
457
458
  "enabled": false
458
459
  },
460
+ "openclaw-lark": {
461
+ "enabled": true
462
+ },
463
+ "openclaw-extension-miaoda-coding": {
464
+ "enabled": true
465
+ },
466
+ "browser": {
467
+ "enabled": true
468
+ },
469
+ "openclaw-mem0-plugin": {
470
+ "enabled": false
471
+ },
459
472
  "openclaw-extension-miaoda": {
460
473
  "enabled": true,
461
474
  "config": {
@@ -466,15 +479,6 @@
466
479
  ]
467
480
  }
468
481
  }
469
- },
470
- "openclaw-lark": {
471
- "enabled": true
472
- },
473
- "openclaw-extension-miaoda-coding": {
474
- "enabled": true
475
- },
476
- "browser": {
477
- "enabled": true
478
482
  }
479
483
  },
480
484
  "installs": {
@@ -491,31 +495,44 @@
491
495
  "resolvedAt": "2026-04-03T06:27:44.706Z",
492
496
  "installedAt": "2026-04-03T06:28:10.547Z"
493
497
  },
494
- "openclaw-extension-miaoda": {
495
- "source": "npm",
496
- "installPath": "./extensions/openclaw-extension-miaoda",
497
- "version": "1.0.9",
498
- "installedAt": "2026-04-11T09:27:06.639Z",
499
- "spec": "@lark-apaas/openclaw-extension-miaoda@1.0.9",
500
- "resolvedVersion": "1.0.9",
501
- "resolvedName": "@lark-apaas/openclaw-extension-miaoda",
502
- "resolvedSpec": "@lark-apaas/openclaw-extension-miaoda@1.0.9",
503
- "resolvedAt": "2026-04-11T09:27:06.639Z",
504
- "integrity": "sha512-McNeuPAUDLrMhT3yZuwk9A7pI262r2CK1N1KNQP6VuzymkDUjx2sTcJPCEBB3bFkdXd0yUU983/OhJiaJo+JWg==",
505
- "shasum": "a3c75886e40b63f39a33a2660932f8afdae6a514"
506
- },
507
498
  "openclaw-extension-miaoda-coding": {
508
499
  "source": "npm",
509
500
  "spec": "@lark-apaas/openclaw-extension-miaoda-coding",
510
501
  "installPath": "./extensions/openclaw-extension-miaoda-coding",
511
- "version": "1.0.8",
502
+ "version": "1.0.11",
512
503
  "resolvedName": "@lark-apaas/openclaw-extension-miaoda-coding",
513
- "resolvedVersion": "1.0.8",
514
- "resolvedSpec": "@lark-apaas/openclaw-extension-miaoda-coding@1.0.8",
515
- "integrity": "sha512-uxlLtgH2CTwz56UTaZD+n/x1p2a3Q01o3Og7oLUJCm6izWHXFEI1SQhNnCPggrfSam49KFir8xB64tY4T9dt2Q==",
516
- "shasum": "058eadf5bc71ae87f79b5096b9d96f4afb89a9db",
517
- "resolvedAt": "2026-04-09T11:33:36.208Z",
518
- "installedAt": "2026-04-09T11:33:37.171Z"
504
+ "resolvedVersion": "1.0.11",
505
+ "resolvedSpec": "@lark-apaas/openclaw-extension-miaoda-coding@1.0.11",
506
+ "integrity": "sha512-B2i3n367cR37UVJrDCTZSBljxUYzDZ4gWf3dw79XBdaZcq0G88XnkgyFmJoZijQd/HQP13xNBlfcLk4Lz2c6Bg==",
507
+ "shasum": "a6bf5efcec70bbd139ee7a75ea183e4f9751fd39",
508
+ "resolvedAt": "2026-04-13T14:40:18.926Z",
509
+ "installedAt": "2026-04-13T14:40:18.926Z"
510
+ },
511
+ "openclaw-mem0-plugin": {
512
+ "source": "npm",
513
+ "spec": "@shareclz/openclaw-mem0-plugin@1.1.2",
514
+ "installPath": "./extensions/openclaw-mem0-plugin",
515
+ "installedAt": "2026-04-13T03:13:10.564Z",
516
+ "version": "1.1.2",
517
+ "resolvedVersion": "1.1.2",
518
+ "resolvedName": "@shareclz/openclaw-mem0-plugin",
519
+ "resolvedSpec": "@shareclz/openclaw-mem0-plugin@1.1.2",
520
+ "resolvedAt": "2026-04-13T03:13:10.564Z",
521
+ "integrity": "sha512-j6m8oN6ykVc3a1/Bw0o7LmXmNApiGk6wxafrT0zW9m4UiqZe8LM4Y2LzvGEAnKwFdkUIkaSzpl/8Qp7MPy/zcQ==",
522
+ "shasum": "b1bdaa44917ce9535ba1acddb145ea0ed5d26bae"
523
+ },
524
+ "openclaw-extension-miaoda": {
525
+ "source": "npm",
526
+ "installPath": "./extensions/openclaw-extension-miaoda",
527
+ "version": "1.0.10",
528
+ "installedAt": "2026-04-14T12:21:23.090Z",
529
+ "spec": "@lark-apaas/openclaw-extension-miaoda@1.0.10",
530
+ "resolvedVersion": "1.0.10",
531
+ "resolvedName": "@lark-apaas/openclaw-extension-miaoda",
532
+ "resolvedSpec": "@lark-apaas/openclaw-extension-miaoda@1.0.10",
533
+ "resolvedAt": "2026-04-14T12:21:23.090Z",
534
+ "integrity": "sha512-aWXIBdzcjUQAdthiUjLhgGp8G0TMVB3wpAzR3wjscDRX7XAo1zhpmcWkBM0JNdrle1i3e5eB4hxVcLfn1yooNw==",
535
+ "shasum": "5ef5f6ef972d90b43091c79e8a1038a3052cc1f5"
519
536
  }
520
537
  }
521
538
  }