@lark-apaas/openclaw-scripts-diagnose-cli 0.1.14-alpha.11 → 0.1.14-alpha.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.cjs +902 -987
  2. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -52,7 +52,7 @@ node_assert = __toESM(node_assert);
52
52
  * it terse and parseable.
53
53
  */
54
54
  function getVersion() {
55
- return "0.1.14-alpha.11";
55
+ return "0.1.14-alpha.13";
56
56
  }
57
57
  //#endregion
58
58
  //#region src/rule-engine/base.ts
@@ -2520,255 +2520,586 @@ function upsertResourceConstrainedToolsBlock(content) {
2520
2520
  return `${content}${content.length > 0 && !content.endsWith("\n") ? "\n\n" : "\n"}${RESOURCE_CONSTRAINED_TOOLS_BLOCK}\n`;
2521
2521
  }
2522
2522
  //#endregion
2523
- //#region src/rules/miaoda-official-plugins-install-spec-unlock.ts
2523
+ //#region src/paths.ts
2524
2524
  /**
2525
- * Official miaoda-side plugins that must track manifest version-locked specs
2526
- * here block upgrades. Third-party / user-installed plugins are intentionally
2527
- * out of scope (users may pin them deliberately).
2525
+ * Central directory for all ephemeral diagnose/reset artifacts: task status
2526
+ * files (`reset-<taskId>.json`) and human-readable step logs
2527
+ * (`reset-<taskId>.log`). Having everything under one dir makes debugging a
2528
+ * stuck reset much easier — `ls /tmp/openclaw-diagnose/` shows every recent
2529
+ * run, and each run's log is right next to its state.
2528
2530
  */
2529
- const OFFICIAL_PLUGIN_NAMES = new Set([
2530
- "openclaw-extension-miaoda",
2531
- "openclaw-extension-miaoda-coding",
2532
- "openclaw-guardian-plugin",
2533
- "openclaw-mem0-plugin",
2534
- "openclaw-lark"
2535
- ]);
2536
- const LOCKED_NPM_SPEC = /^(@[a-z0-9][\w.-]*\/)?[a-z0-9][\w.-]*@[^@/:#\s]+$/i;
2537
- function isLockedNpmSpec(spec) {
2538
- return typeof spec === "string" && LOCKED_NPM_SPEC.test(spec);
2539
- }
2540
- function unlockSpec(spec) {
2541
- const slash = spec.indexOf("/");
2542
- const cut = slash === -1 ? spec.indexOf("@") : spec.indexOf("@", slash + 1);
2543
- return spec.slice(0, cut);
2531
+ const DIAGNOSE_DIR = "/tmp/openclaw-diagnose";
2532
+ function resetResultFile(taskId) {
2533
+ return `${DIAGNOSE_DIR}/reset-${taskId}.json`;
2544
2534
  }
2545
- /** Yield `[key, lockedSpec]` for every official-plugin install whose `spec` is locked. */
2546
- function* iterLockedOfficialInstalls(config) {
2547
- const installs = getNestedMap(config, "plugins", "installs");
2548
- if (!installs) return;
2549
- for (const [key, entry] of Object.entries(installs)) {
2550
- if (!OFFICIAL_PLUGIN_NAMES.has(key)) continue;
2551
- const spec = asRecord(entry)?.spec;
2552
- if (isLockedNpmSpec(spec)) yield [key, spec];
2553
- }
2535
+ function resetLogFile(taskId) {
2536
+ return `${DIAGNOSE_DIR}/reset-${taskId}.log`;
2554
2537
  }
2555
- let MiaodaOfficialPluginsInstallSpecUnlockRule = class MiaodaOfficialPluginsInstallSpecUnlockRule extends DiagnoseRule {
2556
- validate(ctx) {
2557
- const locked = [...iterLockedOfficialInstalls(ctx.config)].map(([k]) => k);
2558
- if (locked.length === 0) return { pass: true };
2559
- return {
2560
- pass: false,
2561
- message: "plugins.installs 中官方插件存在锁版本的 spec: " + locked.sort().join(",")
2562
- };
2563
- }
2564
- repair(ctx) {
2565
- for (const [key, spec] of iterLockedOfficialInstalls(ctx.config)) setNestedValue(ctx.config, [
2566
- "plugins",
2567
- "installs",
2568
- key,
2569
- "spec"
2570
- ], unlockSpec(spec));
2571
- }
2572
- };
2573
- MiaodaOfficialPluginsInstallSpecUnlockRule = __decorate([Rule({
2574
- key: "miaoda_official_plugins_install_spec_unlock",
2575
- description: "移除官方妙搭插件安装条目中的锁版本 npm spec,使其跟随最新 manifest 版本",
2576
- dependsOn: ["config_syntax_check"],
2577
- repairMode: "standard",
2578
- level: "silent"
2579
- })], MiaodaOfficialPluginsInstallSpecUnlockRule);
2538
+ /** Sandbox workspace root where openclaw config + agent state lives. */
2539
+ const WORKSPACE_DIR = "/home/gem/workspace/agent";
2540
+ /** File containing the provider key used by the openclaw miaoda provider. */
2541
+ const PROVIDER_FILE_PATH = "/home/gem/workspace/.force/openclaw/miaoda-provider-key";
2542
+ /** File containing the miaoda openclaw secrets JSON. */
2543
+ const SECRETS_FILE_PATH = "/home/gem/workspace/.force/openclaw/miaoda-openclaw-secrets.json";
2544
+ /** Absolute path to the openclaw config JSON. */
2545
+ const CONFIG_PATH = `${WORKSPACE_DIR}/openclaw.json`;
2580
2546
  //#endregion
2581
- //#region src/rules/miaoda-plugin-allow.ts
2582
- const MIAODA_PLUGIN = "openclaw-extension-miaoda";
2583
- let MiaodaPluginAllowRule = class MiaodaPluginAllowRule extends DiagnoseRule {
2584
- validate(ctx) {
2585
- if (!isPluginInstalledOnDisk(getExtensionsDir(ctx.configPath), MIAODA_PLUGIN)) return { pass: true };
2586
- if (getAllow$1(ctx.config).includes(MIAODA_PLUGIN)) return { pass: true };
2587
- return {
2588
- pass: false,
2589
- message: `plugins.allow 缺少 ${MIAODA_PLUGIN}(已在 extensions/ 下装但未启用)`
2590
- };
2591
- }
2592
- repair(ctx) {
2593
- if (!isPluginInstalledOnDisk(getExtensionsDir(ctx.configPath), MIAODA_PLUGIN)) return;
2594
- const plugins = ctx.config.plugins;
2595
- if (plugins == null || typeof plugins !== "object" || Array.isArray(plugins)) {
2596
- ctx.config.plugins = { allow: [MIAODA_PLUGIN] };
2597
- return;
2547
+ //#region src/lark-cli-init.ts
2548
+ const LARK_PLUGIN_NAMES$1 = ["openclaw-lark", "feishu-openclaw-plugin"];
2549
+ const PE_XML_TAG = "lark-cli-pe";
2550
+ const PE_PLACEHOLDER = `
2551
+ <${PE_XML_TAG}>
2552
+ **【强制要求 - 无例外】飞书所有操作必须先读 feishu-lark-cli skill。未读skill不得调用任何飞书工具,违者视为操作失误。**
2553
+ </${PE_XML_TAG}>
2554
+ `;
2555
+ function isLarkPluginInstalled(configPath) {
2556
+ const extDir = getExtensionsDir(configPath);
2557
+ return LARK_PLUGIN_NAMES$1.some((name) => {
2558
+ try {
2559
+ return node_fs.default.existsSync(node_path.default.join(extDir, name, "package.json"));
2560
+ } catch {
2561
+ return false;
2598
2562
  }
2599
- const pluginsMap = plugins;
2600
- const rawAllow = pluginsMap.allow;
2601
- const allow = Array.isArray(rawAllow) ? rawAllow : [];
2602
- if (allow.includes(MIAODA_PLUGIN)) return;
2603
- allow.push(MIAODA_PLUGIN);
2604
- pluginsMap.allow = allow;
2605
- }
2606
- };
2607
- MiaodaPluginAllowRule = __decorate([Rule({
2608
- key: "miaoda_plugin_allow",
2609
- description: "当 openclaw-extension-miaoda 已在磁盘安装但未在 allow 列表中时,将其添加到 plugins.allow(实验性)",
2610
- dependsOn: ["config_syntax_check"],
2611
- repairMode: "standard",
2612
- level: "critical",
2613
- profile: "standard"
2614
- })], MiaodaPluginAllowRule);
2615
- function getAllow$1(config) {
2616
- const plugins = config.plugins;
2617
- if (plugins == null || typeof plugins !== "object" || Array.isArray(plugins)) return [];
2618
- const allow = plugins.allow;
2619
- if (!Array.isArray(allow)) return [];
2620
- return allow.filter((e) => typeof e === "string");
2563
+ });
2621
2564
  }
2622
- //#endregion
2623
- //#region src/rules/lark-plugin-allow.ts
2624
- const LARK_PLUGIN = "openclaw-lark";
2625
- const LEGACY_LARK_PLUGIN = "feishu-openclaw-plugin";
2626
- const LARK_PLUGIN_NAMES$1 = [LARK_PLUGIN, LEGACY_LARK_PLUGIN];
2627
- let LarkPluginAllowRule = class LarkPluginAllowRule extends DiagnoseRule {
2628
- validate(ctx) {
2629
- const allow = getAllow(ctx.config);
2630
- if (LARK_PLUGIN_NAMES$1.some((name) => allow.includes(name))) return { pass: true };
2631
- const installed = detectInstalledLarkPlugin(getExtensionsDir(ctx.configPath));
2632
- if (installed == null) return { pass: true };
2633
- return {
2634
- pass: false,
2635
- message: `plugins.allow 缺少飞书插件 ${installed}(已在 extensions/ 下装但未启用)`
2636
- };
2565
+ function isLarkCliAvailable$2() {
2566
+ try {
2567
+ return (0, node_child_process.spawnSync)("lark-cli", ["--version"], {
2568
+ encoding: "utf-8",
2569
+ timeout: 5e3,
2570
+ stdio: [
2571
+ "ignore",
2572
+ "pipe",
2573
+ "ignore"
2574
+ ]
2575
+ }).status === 0;
2576
+ } catch {
2577
+ return false;
2637
2578
  }
2638
- repair(ctx) {
2639
- const installed = detectInstalledLarkPlugin(getExtensionsDir(ctx.configPath));
2640
- if (installed == null) return;
2641
- if (ctx.config.plugins == null || typeof ctx.config.plugins !== "object" || Array.isArray(ctx.config.plugins)) {
2642
- ctx.config.plugins = { allow: [installed] };
2643
- return;
2644
- }
2645
- const pluginsMap = ctx.config.plugins;
2646
- const rawAllow = pluginsMap.allow;
2647
- const original = Array.isArray(rawAllow) ? rawAllow : [];
2648
- const stringAllow = original.filter((e) => typeof e === "string");
2649
- if (LARK_PLUGIN_NAMES$1.some((name) => stringAllow.includes(name))) return;
2650
- original.push(installed);
2651
- pluginsMap.allow = original;
2579
+ }
2580
+ function readConfig(configPath) {
2581
+ try {
2582
+ const raw = node_fs.default.readFileSync(configPath, "utf-8");
2583
+ const parsed = loadJSON5().parse(raw);
2584
+ return parsed != null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
2585
+ } catch {
2586
+ return null;
2652
2587
  }
2653
- };
2654
- LarkPluginAllowRule = __decorate([Rule({
2655
- key: "lark_plugin_allow",
2656
- description: "当飞书插件(openclaw-lark 或旧版名)已在磁盘安装但未加入 plugins.allow 时,自动添加",
2657
- dependsOn: ["config_syntax_check"],
2658
- repairMode: "standard",
2659
- level: "critical"
2660
- })], LarkPluginAllowRule);
2661
- function getAllow(config) {
2662
- const plugins = config.plugins;
2663
- if (plugins == null || typeof plugins !== "object" || Array.isArray(plugins)) return [];
2664
- const allow = plugins.allow;
2665
- if (!Array.isArray(allow)) return [];
2666
- return allow.filter((e) => typeof e === "string");
2667
2588
  }
2668
2589
  /**
2669
- * fs-only 检测:`<extDir>/<name>/package.json` 存在即视为已装。
2670
- * 优先级 openclaw-lark(新版)> feishu-openclaw-plugin(legacy)。
2671
- * 不读 package.json 内容,只判存在性,避开 JSON 损坏。
2590
+ * Resolve the feishu app secret for the given appId.
2591
+ *
2592
+ * Lookup order:
2593
+ * 1. channels.feishu.appSecret (single-agent: feishu.appId === appId)
2594
+ * 2. channels.feishu.accounts[key].appSecret (multi-agent: account.appId === appId)
2595
+ *
2596
+ * Value interpretation:
2597
+ * - string → use directly
2598
+ * - object → secret is managed by a provider; use `feishuAppSecret` param instead
2599
+ *
2600
+ * Returns null when the secret cannot be determined.
2672
2601
  */
2673
- function detectInstalledLarkPlugin(extDir) {
2674
- for (const name of [LARK_PLUGIN, LEGACY_LARK_PLUGIN]) if (pluginPackageJsonExists(extDir, name)) return name;
2602
+ function resolveAppSecret(appId, config, feishuAppSecret) {
2603
+ const feishu = getNestedMap(config, "channels", "feishu");
2604
+ if (!feishu) return null;
2605
+ let rawSecret;
2606
+ if (typeof feishu.appId === "string" && feishu.appId === appId) rawSecret = feishu.appSecret;
2607
+ else {
2608
+ const accounts = asRecord(feishu.accounts);
2609
+ if (accounts) for (const [, val] of Object.entries(accounts)) {
2610
+ const account = asRecord(val);
2611
+ if (account?.appId === appId) {
2612
+ rawSecret = account.appSecret ?? feishu.appSecret;
2613
+ break;
2614
+ }
2615
+ }
2616
+ }
2617
+ if (typeof rawSecret === "string" && rawSecret) return rawSecret;
2618
+ if (rawSecret != null && typeof rawSecret === "object") return feishuAppSecret ?? null;
2675
2619
  return null;
2676
2620
  }
2677
- function pluginPackageJsonExists(extDir, pluginDir) {
2678
- try {
2679
- return node_fs.default.existsSync(node_path.default.join(extDir, pluginDir, "package.json"));
2680
- } catch {
2681
- return false;
2621
+ /**
2622
+ * Resolve the agents.md path for the given appId from the openclaw config.
2623
+ *
2624
+ * Case 1: appId matches channels.feishu.appId (single-agent path)
2625
+ * → WORKSPACE_DIR/AGENTS.md
2626
+ *
2627
+ * Case 2: appId found in channels.feishu.accounts (multi-agent path)
2628
+ * → find account key where account.appId === appId
2629
+ * → find binding where match.channel=feishu && match.accountId=that key
2630
+ * → if agentId === 'main' → WORKSPACE_DIR/agents.md
2631
+ * → else find agent in agents.list by id → agent.workspace/agents.md
2632
+ *
2633
+ * Returns null when the path cannot be determined.
2634
+ */
2635
+ function resolveAgentsMdPath(appId, config) {
2636
+ const feishu = getNestedMap(config, "channels", "feishu");
2637
+ if (!feishu) {
2638
+ console.error("resolveAgentsMdPath: channels.feishu not found");
2639
+ return null;
2682
2640
  }
2641
+ if (typeof feishu.appId === "string" && feishu.appId === appId) {
2642
+ console.error(`resolveAgentsMdPath: case=single-agent feishu.appId=${feishu.appId}`);
2643
+ return node_path.default.join(WORKSPACE_DIR, "workspace", "AGENTS.md");
2644
+ }
2645
+ const accounts = asRecord(feishu.accounts);
2646
+ if (!accounts) {
2647
+ console.error("resolveAgentsMdPath: feishu.accounts not found");
2648
+ return null;
2649
+ }
2650
+ let accountId;
2651
+ for (const [key, val] of Object.entries(accounts)) if (asRecord(val)?.appId === appId) {
2652
+ accountId = key;
2653
+ break;
2654
+ }
2655
+ if (!accountId) {
2656
+ console.error(`resolveAgentsMdPath: no account found with appId=${appId} in feishu.accounts keys=[${Object.keys(accounts).join(",")}]`);
2657
+ return null;
2658
+ }
2659
+ console.error(`resolveAgentsMdPath: found accountId=${accountId}`);
2660
+ const bindings = Array.isArray(config.bindings) ? config.bindings : [];
2661
+ let agentId;
2662
+ for (const b of bindings) {
2663
+ const binding = asRecord(b);
2664
+ if (!binding) continue;
2665
+ const match = asRecord(binding.match);
2666
+ if (match?.channel === "feishu" && match?.accountId === accountId) {
2667
+ if (typeof binding.agentId === "string") {
2668
+ agentId = binding.agentId;
2669
+ break;
2670
+ }
2671
+ }
2672
+ }
2673
+ if (!agentId) {
2674
+ console.error(`resolveAgentsMdPath: no binding found for accountId=${accountId} in bindings(count=${bindings.length})`);
2675
+ return null;
2676
+ }
2677
+ console.error(`resolveAgentsMdPath: found agentId=${agentId}`);
2678
+ if (agentId === "main") {
2679
+ console.error("resolveAgentsMdPath: case=multi-agent-main");
2680
+ return node_path.default.join(WORKSPACE_DIR, "workspace", "AGENTS.md");
2681
+ }
2682
+ const agentsObj = asRecord(config.agents);
2683
+ const list = Array.isArray(agentsObj?.list) ? agentsObj.list : [];
2684
+ for (const a of list) {
2685
+ const agent = asRecord(a);
2686
+ if (agent?.id === agentId) {
2687
+ const ws = typeof agent.workspace === "string" ? agent.workspace : node_path.default.join(WORKSPACE_DIR, "workspace");
2688
+ console.error(`resolveAgentsMdPath: case=multi-agent-custom agentId=${agentId} workspace=${ws}`);
2689
+ return node_path.default.join(ws, "AGENTS.md");
2690
+ }
2691
+ }
2692
+ console.error(`resolveAgentsMdPath: agentId=${agentId} not found in agents.list(count=${list.length})`);
2693
+ return null;
2683
2694
  }
2684
- //#endregion
2685
- //#region src/rules/old-miaoda-plugins-cleanup.ts
2686
- const NEW_MIAODA = "openclaw-extension-miaoda";
2687
- const OLD_PLUGIN_NAMES = Object.freeze([
2688
- "openclaw-feishu-greeting",
2689
- "openclaw-miaoda-keepalive",
2690
- "feishu-greeting",
2691
- "miaoda-keepalive"
2692
- ]);
2693
- function getPluginMaps(config) {
2694
- const rawAllow = asRecord(config.plugins)?.allow;
2695
- return {
2696
- entries: getNestedMap(config, "plugins", "entries"),
2697
- installs: getNestedMap(config, "plugins", "installs"),
2698
- allow: Array.isArray(rawAllow) ? rawAllow : void 0
2699
- };
2700
- }
2701
- function hasNewMiaoda({ entries, installs, allow }) {
2702
- return asRecord(entries?.[NEW_MIAODA]) != null || asRecord(installs?.[NEW_MIAODA]) != null || (allow?.includes(NEW_MIAODA) ?? false);
2695
+ function appendPeToAgentsMd(agentsMdPath) {
2696
+ const dir = node_path.default.dirname(agentsMdPath);
2697
+ if (!node_fs.default.existsSync(dir)) node_fs.default.mkdirSync(dir, { recursive: true });
2698
+ const existing = node_fs.default.existsSync(agentsMdPath) ? node_fs.default.readFileSync(agentsMdPath, "utf-8") : "";
2699
+ if (existing.includes(`<lark-cli-pe>`)) {
2700
+ console.error(`lark-cli-init: <${PE_XML_TAG}> already present in ${agentsMdPath}, skipping`);
2701
+ return;
2702
+ }
2703
+ const sep = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
2704
+ node_fs.default.appendFileSync(agentsMdPath, `${sep}${PE_PLACEHOLDER}`, "utf-8");
2705
+ console.error(`lark-cli-init: appended PE placeholder to ${agentsMdPath}`);
2703
2706
  }
2704
- function findResiduals({ entries, installs, allow }, extensionsDir) {
2705
- return OLD_PLUGIN_NAMES.filter((name) => entries?.[name] != null || installs?.[name] != null || (allow?.includes(name) ?? false) || node_fs.default.existsSync(node_path.default.join(extensionsDir, name)));
2707
+ /**
2708
+ * Collect every Feishu bot appId declared in the openclaw config.
2709
+ * Covers both single-agent (channels.feishu.appId) and multi-agent
2710
+ * (channels.feishu.accounts[*].appId) layouts. Returns a deduplicated list.
2711
+ */
2712
+ function collectFeishuAppIds(configPath) {
2713
+ const config = readConfig(configPath ?? CONFIG_PATH);
2714
+ if (!config) return [];
2715
+ const feishu = getNestedMap(config, "channels", "feishu");
2716
+ if (!feishu) return [];
2717
+ const appIds = /* @__PURE__ */ new Set();
2718
+ const topAppId = feishu.appId;
2719
+ if (typeof topAppId === "string" && topAppId.trim()) appIds.add(topAppId.trim());
2720
+ const accounts = asRecord(feishu.accounts);
2721
+ if (accounts) for (const val of Object.values(accounts)) {
2722
+ const appId = asRecord(val)?.appId;
2723
+ if (typeof appId === "string" && appId.trim()) appIds.add(appId.trim());
2724
+ }
2725
+ return [...appIds];
2706
2726
  }
2707
- let OldMiaodaPluginsCleanupRule = class OldMiaodaPluginsCleanupRule extends DiagnoseRule {
2727
+ function runLarkCliInit(opts) {
2728
+ const configPath = opts.configPath ?? CONFIG_PATH;
2729
+ if (!isLarkPluginInstalled(configPath)) {
2730
+ console.error("lark-cli-init: skipping — openclaw-lark plugin not installed");
2731
+ return {
2732
+ ok: true,
2733
+ skipped: true,
2734
+ skipReason: "openclaw-lark plugin not installed"
2735
+ };
2736
+ }
2737
+ if (!isLarkCliAvailable$2()) {
2738
+ console.error("lark-cli-init: skipping — lark-cli command not found");
2739
+ return {
2740
+ ok: true,
2741
+ skipped: true,
2742
+ skipReason: "lark-cli command not found"
2743
+ };
2744
+ }
2745
+ const config = readConfig(configPath);
2746
+ if (!config) return {
2747
+ ok: false,
2748
+ error: `could not read config at ${configPath}`
2749
+ };
2750
+ const agentsMdPath = resolveAgentsMdPath(opts.appId, config);
2751
+ console.error(`lark-cli-init: resolved agents.md path=${agentsMdPath ?? "(null)"}`);
2752
+ if (!agentsMdPath) return {
2753
+ ok: false,
2754
+ error: `could not resolve agents.md path for appId=${opts.appId}`
2755
+ };
2756
+ const appSecret = resolveAppSecret(opts.appId, config, opts.feishuAppSecret);
2757
+ if (!appSecret) return {
2758
+ ok: false,
2759
+ error: `could not resolve appSecret for appId=${opts.appId}`
2760
+ };
2761
+ console.error(`lark-cli-init: running lark-cli config init --name ${opts.appId} --app-id ${opts.appId} --brand feishu --app-secret-stdin --force-init`);
2762
+ const initRes = (0, node_child_process.spawnSync)("lark-cli", [
2763
+ "config",
2764
+ "init",
2765
+ "--name",
2766
+ opts.appId,
2767
+ "--app-id",
2768
+ opts.appId,
2769
+ "--brand",
2770
+ "feishu",
2771
+ "--app-secret-stdin",
2772
+ "--force-init"
2773
+ ], {
2774
+ stdio: [
2775
+ "pipe",
2776
+ "pipe",
2777
+ "pipe"
2778
+ ],
2779
+ encoding: "utf-8",
2780
+ input: appSecret
2781
+ });
2782
+ const configInitStdout = initRes.stdout?.trim() || void 0;
2783
+ const configInitStderr = initRes.stderr?.trim() || void 0;
2784
+ if (configInitStdout) console.error(`lark-cli config init stdout: ${configInitStdout}`);
2785
+ if (configInitStderr) console.error(`lark-cli config init stderr: ${configInitStderr}`);
2786
+ if (initRes.error) return {
2787
+ ok: false,
2788
+ configInitStdout,
2789
+ configInitStderr,
2790
+ error: `lark-cli config init spawn error: ${initRes.error.message}`
2791
+ };
2792
+ if (initRes.status !== 0) return {
2793
+ ok: false,
2794
+ configInitExitCode: initRes.status ?? void 0,
2795
+ configInitStdout,
2796
+ configInitStderr,
2797
+ error: `lark-cli config init exited with code ${initRes.status}`
2798
+ };
2799
+ appendPeToAgentsMd(agentsMdPath);
2800
+ return {
2801
+ ok: true,
2802
+ configInitExitCode: 0,
2803
+ agentsMdPath
2804
+ };
2805
+ }
2806
+ //#endregion
2807
+ //#region src/rules/agents-md-lark-cli-pe.ts
2808
+ function isLarkCliAvailable$1() {
2809
+ try {
2810
+ return (0, node_child_process.spawnSync)("lark-cli", ["--version"], {
2811
+ encoding: "utf-8",
2812
+ timeout: 5e3,
2813
+ stdio: [
2814
+ "ignore",
2815
+ "pipe",
2816
+ "ignore"
2817
+ ]
2818
+ }).status === 0;
2819
+ } catch {
2820
+ return false;
2821
+ }
2822
+ }
2823
+ let AgentsMdLarkCliPeRule = class AgentsMdLarkCliPeRule extends DiagnoseRule {
2708
2824
  validate(ctx) {
2709
- const maps = getPluginMaps(ctx.config);
2710
- if (!hasNewMiaoda(maps)) return { pass: true };
2711
- const residuals = findResiduals(maps, getExtensionsDir(ctx.configPath));
2712
- if (residuals.length === 0) return { pass: true };
2825
+ if (!isLarkCliAvailable$1()) return { pass: true };
2826
+ const missingPath = collectExistingAgentsMdPaths(ctx).find((filePath) => {
2827
+ return !node_fs.default.readFileSync(filePath, "utf-8").includes(`<${PE_XML_TAG}>`);
2828
+ });
2829
+ if (!missingPath) return { pass: true };
2713
2830
  return {
2714
2831
  pass: false,
2715
- message: "旧 miaoda 插件残留: " + residuals.sort().join(",")
2832
+ message: `${missingPath} 中缺少 lark-cli-pe PE 内容,需要追加`
2716
2833
  };
2717
2834
  }
2718
2835
  repair(ctx) {
2719
- const maps = getPluginMaps(ctx.config);
2720
- if (!hasNewMiaoda(maps)) return;
2721
- const extensionsDir = getExtensionsDir(ctx.configPath);
2722
- const { entries, installs, allow } = maps;
2723
- const oldSet = new Set(OLD_PLUGIN_NAMES);
2724
- if (allow) for (let i = allow.length - 1; i >= 0; i--) {
2725
- const v = allow[i];
2726
- if (typeof v === "string" && oldSet.has(v)) allow.splice(i, 1);
2727
- }
2728
- for (const name of OLD_PLUGIN_NAMES) {
2729
- if (entries && name in entries) delete entries[name];
2730
- if (installs && name in installs) delete installs[name];
2731
- const target = node_path.default.join(extensionsDir, name);
2732
- const rel = node_path.default.relative(extensionsDir, target);
2733
- if (!rel || rel.startsWith("..") || node_path.default.isAbsolute(rel)) continue;
2734
- try {
2735
- node_fs.default.rmSync(target, {
2736
- recursive: true,
2737
- force: true
2738
- });
2739
- } catch (e) {
2740
- console.error(`[old_miaoda_plugins_cleanup] rmSync ${target} failed: ${e.message}`);
2741
- }
2836
+ if (!isLarkCliAvailable$1()) return;
2837
+ for (const filePath of collectExistingAgentsMdPaths(ctx)) {
2838
+ const content = node_fs.default.readFileSync(filePath, "utf-8");
2839
+ if (content.includes(`<lark-cli-pe>`)) continue;
2840
+ const sep = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
2841
+ node_fs.default.appendFileSync(filePath, `${sep}${PE_PLACEHOLDER}`, "utf-8");
2842
+ console.error(`agents-md-lark-cli-pe: appended PE to ${filePath}`);
2742
2843
  }
2743
2844
  }
2744
2845
  };
2745
- OldMiaodaPluginsCleanupRule = __decorate([Rule({
2746
- key: "old_miaoda_plugins_cleanup",
2747
- description: "当新版 openclaw-extension-miaoda 已存在时,清理过时插件引用(openclaw-feishu-greeting、openclaw-miaoda-keepalive 等)",
2846
+ AgentsMdLarkCliPeRule = __decorate([Rule({
2847
+ key: "agents_md_lark_cli_pe",
2848
+ description: "检测各智能体 AGENTS.md 中是否缺失 lark-cli-pe PE 内容,lark-cli 存在时自动追加",
2748
2849
  dependsOn: ["config_syntax_check"],
2749
2850
  repairMode: "standard",
2750
2851
  level: "silent"
2751
- })], OldMiaodaPluginsCleanupRule);
2852
+ })], AgentsMdLarkCliPeRule);
2752
2853
  //#endregion
2753
- //#region src/rules/builtin-plugin-missing.ts
2854
+ //#region src/rules/miaoda-official-plugins-install-spec-unlock.ts
2754
2855
  /**
2755
- * install-extension --all 安装的内置扩展插件列表(manifest role=extension)。
2756
- * miaoda-official-plugins-install-spec-unlock.ts 中的 OFFICIAL_PLUGIN_NAMES 保持一致。
2856
+ * Official miaoda-side plugins that must track manifest — version-locked specs
2857
+ * here block upgrades. Third-party / user-installed plugins are intentionally
2858
+ * out of scope (users may pin them deliberately).
2757
2859
  */
2758
- const BUILTIN_EXTENSION_PLUGINS = new Set([
2759
- "openclaw-lark",
2860
+ const OFFICIAL_PLUGIN_NAMES = new Set([
2760
2861
  "openclaw-extension-miaoda",
2761
2862
  "openclaw-extension-miaoda-coding",
2762
2863
  "openclaw-guardian-plugin",
2763
- "openclaw-mem0-plugin"
2864
+ "openclaw-mem0-plugin",
2865
+ "openclaw-lark"
2764
2866
  ]);
2765
- function findMissingBuiltinPlugins(ctx) {
2766
- const extDir = getExtensionsDir(ctx.configPath);
2767
- return [...BUILTIN_EXTENSION_PLUGINS].filter((name) => !isPluginInstalledOnDisk(extDir, name)).sort();
2867
+ const LOCKED_NPM_SPEC = /^(@[a-z0-9][\w.-]*\/)?[a-z0-9][\w.-]*@[^@/:#\s]+$/i;
2868
+ function isLockedNpmSpec(spec) {
2869
+ return typeof spec === "string" && LOCKED_NPM_SPEC.test(spec);
2768
2870
  }
2769
- let BuiltinPluginMissingRule = class BuiltinPluginMissingRule extends DiagnoseRule {
2871
+ function unlockSpec(spec) {
2872
+ const slash = spec.indexOf("/");
2873
+ const cut = slash === -1 ? spec.indexOf("@") : spec.indexOf("@", slash + 1);
2874
+ return spec.slice(0, cut);
2875
+ }
2876
+ /** Yield `[key, lockedSpec]` for every official-plugin install whose `spec` is locked. */
2877
+ function* iterLockedOfficialInstalls(config) {
2878
+ const installs = getNestedMap(config, "plugins", "installs");
2879
+ if (!installs) return;
2880
+ for (const [key, entry] of Object.entries(installs)) {
2881
+ if (!OFFICIAL_PLUGIN_NAMES.has(key)) continue;
2882
+ const spec = asRecord(entry)?.spec;
2883
+ if (isLockedNpmSpec(spec)) yield [key, spec];
2884
+ }
2885
+ }
2886
+ let MiaodaOfficialPluginsInstallSpecUnlockRule = class MiaodaOfficialPluginsInstallSpecUnlockRule extends DiagnoseRule {
2770
2887
  validate(ctx) {
2771
- const missing = findMissingBuiltinPlugins(ctx);
2888
+ const locked = [...iterLockedOfficialInstalls(ctx.config)].map(([k]) => k);
2889
+ if (locked.length === 0) return { pass: true };
2890
+ return {
2891
+ pass: false,
2892
+ message: "plugins.installs 中官方插件存在锁版本的 spec: " + locked.sort().join(",")
2893
+ };
2894
+ }
2895
+ repair(ctx) {
2896
+ for (const [key, spec] of iterLockedOfficialInstalls(ctx.config)) setNestedValue(ctx.config, [
2897
+ "plugins",
2898
+ "installs",
2899
+ key,
2900
+ "spec"
2901
+ ], unlockSpec(spec));
2902
+ }
2903
+ };
2904
+ MiaodaOfficialPluginsInstallSpecUnlockRule = __decorate([Rule({
2905
+ key: "miaoda_official_plugins_install_spec_unlock",
2906
+ description: "移除官方妙搭插件安装条目中的锁版本 npm spec,使其跟随最新 manifest 版本",
2907
+ dependsOn: ["config_syntax_check"],
2908
+ repairMode: "standard",
2909
+ level: "silent"
2910
+ })], MiaodaOfficialPluginsInstallSpecUnlockRule);
2911
+ //#endregion
2912
+ //#region src/rules/miaoda-plugin-allow.ts
2913
+ const MIAODA_PLUGIN = "openclaw-extension-miaoda";
2914
+ let MiaodaPluginAllowRule = class MiaodaPluginAllowRule extends DiagnoseRule {
2915
+ validate(ctx) {
2916
+ if (!isPluginInstalledOnDisk(getExtensionsDir(ctx.configPath), MIAODA_PLUGIN)) return { pass: true };
2917
+ if (getAllow$1(ctx.config).includes(MIAODA_PLUGIN)) return { pass: true };
2918
+ return {
2919
+ pass: false,
2920
+ message: `plugins.allow 缺少 ${MIAODA_PLUGIN}(已在 extensions/ 下装但未启用)`
2921
+ };
2922
+ }
2923
+ repair(ctx) {
2924
+ if (!isPluginInstalledOnDisk(getExtensionsDir(ctx.configPath), MIAODA_PLUGIN)) return;
2925
+ const plugins = ctx.config.plugins;
2926
+ if (plugins == null || typeof plugins !== "object" || Array.isArray(plugins)) {
2927
+ ctx.config.plugins = { allow: [MIAODA_PLUGIN] };
2928
+ return;
2929
+ }
2930
+ const pluginsMap = plugins;
2931
+ const rawAllow = pluginsMap.allow;
2932
+ const allow = Array.isArray(rawAllow) ? rawAllow : [];
2933
+ if (allow.includes(MIAODA_PLUGIN)) return;
2934
+ allow.push(MIAODA_PLUGIN);
2935
+ pluginsMap.allow = allow;
2936
+ }
2937
+ };
2938
+ MiaodaPluginAllowRule = __decorate([Rule({
2939
+ key: "miaoda_plugin_allow",
2940
+ description: "当 openclaw-extension-miaoda 已在磁盘安装但未在 allow 列表中时,将其添加到 plugins.allow(实验性)",
2941
+ dependsOn: ["config_syntax_check"],
2942
+ repairMode: "standard",
2943
+ level: "critical",
2944
+ profile: "standard"
2945
+ })], MiaodaPluginAllowRule);
2946
+ function getAllow$1(config) {
2947
+ const plugins = config.plugins;
2948
+ if (plugins == null || typeof plugins !== "object" || Array.isArray(plugins)) return [];
2949
+ const allow = plugins.allow;
2950
+ if (!Array.isArray(allow)) return [];
2951
+ return allow.filter((e) => typeof e === "string");
2952
+ }
2953
+ //#endregion
2954
+ //#region src/rules/lark-plugin-allow.ts
2955
+ const LARK_PLUGIN = "openclaw-lark";
2956
+ const LEGACY_LARK_PLUGIN = "feishu-openclaw-plugin";
2957
+ const LARK_PLUGIN_NAMES = [LARK_PLUGIN, LEGACY_LARK_PLUGIN];
2958
+ let LarkPluginAllowRule = class LarkPluginAllowRule extends DiagnoseRule {
2959
+ validate(ctx) {
2960
+ const allow = getAllow(ctx.config);
2961
+ if (LARK_PLUGIN_NAMES.some((name) => allow.includes(name))) return { pass: true };
2962
+ const installed = detectInstalledLarkPlugin(getExtensionsDir(ctx.configPath));
2963
+ if (installed == null) return { pass: true };
2964
+ return {
2965
+ pass: false,
2966
+ message: `plugins.allow 缺少飞书插件 ${installed}(已在 extensions/ 下装但未启用)`
2967
+ };
2968
+ }
2969
+ repair(ctx) {
2970
+ const installed = detectInstalledLarkPlugin(getExtensionsDir(ctx.configPath));
2971
+ if (installed == null) return;
2972
+ if (ctx.config.plugins == null || typeof ctx.config.plugins !== "object" || Array.isArray(ctx.config.plugins)) {
2973
+ ctx.config.plugins = { allow: [installed] };
2974
+ return;
2975
+ }
2976
+ const pluginsMap = ctx.config.plugins;
2977
+ const rawAllow = pluginsMap.allow;
2978
+ const original = Array.isArray(rawAllow) ? rawAllow : [];
2979
+ const stringAllow = original.filter((e) => typeof e === "string");
2980
+ if (LARK_PLUGIN_NAMES.some((name) => stringAllow.includes(name))) return;
2981
+ original.push(installed);
2982
+ pluginsMap.allow = original;
2983
+ }
2984
+ };
2985
+ LarkPluginAllowRule = __decorate([Rule({
2986
+ key: "lark_plugin_allow",
2987
+ description: "当飞书插件(openclaw-lark 或旧版名)已在磁盘安装但未加入 plugins.allow 时,自动添加",
2988
+ dependsOn: ["config_syntax_check"],
2989
+ repairMode: "standard",
2990
+ level: "critical"
2991
+ })], LarkPluginAllowRule);
2992
+ function getAllow(config) {
2993
+ const plugins = config.plugins;
2994
+ if (plugins == null || typeof plugins !== "object" || Array.isArray(plugins)) return [];
2995
+ const allow = plugins.allow;
2996
+ if (!Array.isArray(allow)) return [];
2997
+ return allow.filter((e) => typeof e === "string");
2998
+ }
2999
+ /**
3000
+ * fs-only 检测:`<extDir>/<name>/package.json` 存在即视为已装。
3001
+ * 优先级 openclaw-lark(新版)> feishu-openclaw-plugin(legacy)。
3002
+ * 不读 package.json 内容,只判存在性,避开 JSON 损坏。
3003
+ */
3004
+ function detectInstalledLarkPlugin(extDir) {
3005
+ for (const name of [LARK_PLUGIN, LEGACY_LARK_PLUGIN]) if (pluginPackageJsonExists(extDir, name)) return name;
3006
+ return null;
3007
+ }
3008
+ function pluginPackageJsonExists(extDir, pluginDir) {
3009
+ try {
3010
+ return node_fs.default.existsSync(node_path.default.join(extDir, pluginDir, "package.json"));
3011
+ } catch {
3012
+ return false;
3013
+ }
3014
+ }
3015
+ //#endregion
3016
+ //#region src/rules/old-miaoda-plugins-cleanup.ts
3017
+ const NEW_MIAODA = "openclaw-extension-miaoda";
3018
+ const OLD_PLUGIN_NAMES = Object.freeze([
3019
+ "openclaw-feishu-greeting",
3020
+ "openclaw-miaoda-keepalive",
3021
+ "feishu-greeting",
3022
+ "miaoda-keepalive"
3023
+ ]);
3024
+ function getPluginMaps(config) {
3025
+ const rawAllow = asRecord(config.plugins)?.allow;
3026
+ return {
3027
+ entries: getNestedMap(config, "plugins", "entries"),
3028
+ installs: getNestedMap(config, "plugins", "installs"),
3029
+ allow: Array.isArray(rawAllow) ? rawAllow : void 0
3030
+ };
3031
+ }
3032
+ function hasNewMiaoda({ entries, installs, allow }) {
3033
+ return asRecord(entries?.[NEW_MIAODA]) != null || asRecord(installs?.[NEW_MIAODA]) != null || (allow?.includes(NEW_MIAODA) ?? false);
3034
+ }
3035
+ function findResiduals({ entries, installs, allow }, extensionsDir) {
3036
+ return OLD_PLUGIN_NAMES.filter((name) => entries?.[name] != null || installs?.[name] != null || (allow?.includes(name) ?? false) || node_fs.default.existsSync(node_path.default.join(extensionsDir, name)));
3037
+ }
3038
+ let OldMiaodaPluginsCleanupRule = class OldMiaodaPluginsCleanupRule extends DiagnoseRule {
3039
+ validate(ctx) {
3040
+ const maps = getPluginMaps(ctx.config);
3041
+ if (!hasNewMiaoda(maps)) return { pass: true };
3042
+ const residuals = findResiduals(maps, getExtensionsDir(ctx.configPath));
3043
+ if (residuals.length === 0) return { pass: true };
3044
+ return {
3045
+ pass: false,
3046
+ message: "旧 miaoda 插件残留: " + residuals.sort().join(",")
3047
+ };
3048
+ }
3049
+ repair(ctx) {
3050
+ const maps = getPluginMaps(ctx.config);
3051
+ if (!hasNewMiaoda(maps)) return;
3052
+ const extensionsDir = getExtensionsDir(ctx.configPath);
3053
+ const { entries, installs, allow } = maps;
3054
+ const oldSet = new Set(OLD_PLUGIN_NAMES);
3055
+ if (allow) for (let i = allow.length - 1; i >= 0; i--) {
3056
+ const v = allow[i];
3057
+ if (typeof v === "string" && oldSet.has(v)) allow.splice(i, 1);
3058
+ }
3059
+ for (const name of OLD_PLUGIN_NAMES) {
3060
+ if (entries && name in entries) delete entries[name];
3061
+ if (installs && name in installs) delete installs[name];
3062
+ const target = node_path.default.join(extensionsDir, name);
3063
+ const rel = node_path.default.relative(extensionsDir, target);
3064
+ if (!rel || rel.startsWith("..") || node_path.default.isAbsolute(rel)) continue;
3065
+ try {
3066
+ node_fs.default.rmSync(target, {
3067
+ recursive: true,
3068
+ force: true
3069
+ });
3070
+ } catch (e) {
3071
+ console.error(`[old_miaoda_plugins_cleanup] rmSync ${target} failed: ${e.message}`);
3072
+ }
3073
+ }
3074
+ }
3075
+ };
3076
+ OldMiaodaPluginsCleanupRule = __decorate([Rule({
3077
+ key: "old_miaoda_plugins_cleanup",
3078
+ description: "当新版 openclaw-extension-miaoda 已存在时,清理过时插件引用(openclaw-feishu-greeting、openclaw-miaoda-keepalive 等)",
3079
+ dependsOn: ["config_syntax_check"],
3080
+ repairMode: "standard",
3081
+ level: "silent"
3082
+ })], OldMiaodaPluginsCleanupRule);
3083
+ //#endregion
3084
+ //#region src/rules/builtin-plugin-missing.ts
3085
+ /**
3086
+ * install-extension --all 安装的内置扩展插件列表(manifest role=extension)。
3087
+ * 与 miaoda-official-plugins-install-spec-unlock.ts 中的 OFFICIAL_PLUGIN_NAMES 保持一致。
3088
+ */
3089
+ const BUILTIN_EXTENSION_PLUGINS = new Set([
3090
+ "openclaw-lark",
3091
+ "openclaw-extension-miaoda",
3092
+ "openclaw-extension-miaoda-coding",
3093
+ "openclaw-guardian-plugin",
3094
+ "openclaw-mem0-plugin"
3095
+ ]);
3096
+ function findMissingBuiltinPlugins(ctx) {
3097
+ const extDir = getExtensionsDir(ctx.configPath);
3098
+ return [...BUILTIN_EXTENSION_PLUGINS].filter((name) => !isPluginInstalledOnDisk(extDir, name)).sort();
3099
+ }
3100
+ let BuiltinPluginMissingRule = class BuiltinPluginMissingRule extends DiagnoseRule {
3101
+ validate(ctx) {
3102
+ const missing = findMissingBuiltinPlugins(ctx);
2772
3103
  if (missing.length === 0) return { pass: true };
2773
3104
  return {
2774
3105
  pass: false,
@@ -3588,7 +3919,7 @@ function extractScopedNameFromSpec(spec) {
3588
3919
  const at = spec.indexOf("@", 1);
3589
3920
  return at === -1 ? spec : spec.slice(0, at);
3590
3921
  }
3591
- function isLarkCliAvailable$1() {
3922
+ function isLarkCliAvailable() {
3592
3923
  try {
3593
3924
  return (0, node_child_process.spawnSync)(LARK_CLI_NAME$1, ["--version"], {
3594
3925
  encoding: "utf-8",
@@ -3629,7 +3960,7 @@ function installLarkCliOnce(tag) {
3629
3960
  let LarkCliMissingForInstalledLarkPluginRule = class LarkCliMissingForInstalledLarkPluginRule extends DiagnoseRule {
3630
3961
  validate(ctx) {
3631
3962
  if (!isTargetForkPlugin(readInstalledLarkPlugin(ctx))) return { pass: true };
3632
- if (isLarkCliAvailable$1()) return { pass: true };
3963
+ if (isLarkCliAvailable()) return { pass: true };
3633
3964
  return {
3634
3965
  pass: false,
3635
3966
  message: `${FORK_PACKAGE_NAME}@${TARGET_VERSION} 已安装,但 lark-cli 不可用;将执行一次 lark-cli 安装`
@@ -3637,7 +3968,7 @@ let LarkCliMissingForInstalledLarkPluginRule = class LarkCliMissingForInstalledL
3637
3968
  }
3638
3969
  repair(ctx) {
3639
3970
  if (!isTargetForkPlugin(readInstalledLarkPlugin(ctx))) return;
3640
- if (isLarkCliAvailable$1()) return;
3971
+ if (isLarkCliAvailable()) return;
3641
3972
  installLarkCliOnce(ctx.vars.recommendedOpenclawTag ?? TARGET_VERSION);
3642
3973
  }
3643
3974
  };
@@ -3650,117 +3981,6 @@ LarkCliMissingForInstalledLarkPluginRule = __decorate([Rule({
3650
3981
  usesVars: ["recommendedOpenclawTag"]
3651
3982
  })], LarkCliMissingForInstalledLarkPluginRule);
3652
3983
  //#endregion
3653
- //#region src/rules/feishu-bot-channel-config.ts
3654
- /**
3655
- * Ensures each bot account's channel config is correct:
3656
- * 1. `allowFrom` contains its own `creatorOpenID` from larkApps
3657
- * 2. `appSecret` is either the canonical provider-ref or matches larkApps plaintext
3658
- *
3659
- * Covers both multi-account (channels.feishu.accounts) and single-account
3660
- * (channels.feishu.appId + allowFrom at top level) layouts.
3661
- */
3662
- let FeishuBotChannelConfigRule = class FeishuBotChannelConfigRule extends DiagnoseRule {
3663
- validate(ctx) {
3664
- const larkApps = ctx.vars.larkApps;
3665
- if (!larkApps || larkApps.length === 0) return { pass: true };
3666
- const feishu = asRecord(getNestedMap(ctx.config, "channels", "feishu"));
3667
- if (!feishu) return { pass: true };
3668
- const issues = [];
3669
- const accounts = asRecord(feishu.accounts);
3670
- if (accounts) for (const [accountId, account] of Object.entries(accounts)) {
3671
- const bot = asRecord(account);
3672
- if (!bot) continue;
3673
- const appId = bot.appId;
3674
- if (typeof appId !== "string" || !appId.startsWith("cli_")) continue;
3675
- const larkApp = larkApps.find((e) => e.larkAppID === appId);
3676
- if (!larkApp) continue;
3677
- this.checkBot(accountId, bot, larkApp, issues);
3678
- }
3679
- const singleAppId = feishu.appId;
3680
- if (typeof singleAppId === "string" && singleAppId.startsWith("cli_") && !accounts) {
3681
- const larkApp = larkApps.find((e) => e.larkAppID === singleAppId);
3682
- if (larkApp) this.checkBot("feishu", feishu, larkApp, issues);
3683
- }
3684
- if (issues.length === 0) return { pass: true };
3685
- return {
3686
- pass: false,
3687
- message: issues.join("; ")
3688
- };
3689
- }
3690
- /** Check a single bot entry (either an account object or the feishu channel itself).
3691
- * appSecret is validated based on its current type:
3692
- * - object → must match canonical provider-ref
3693
- * - string → must match larkApps plaintext
3694
- */
3695
- checkBot(label, bot, larkApp, issues) {
3696
- const creatorOpenID = larkApp.creatorOpenID;
3697
- const allowFrom = Array.isArray(bot.allowFrom) ? bot.allowFrom : [];
3698
- if (typeof creatorOpenID === "string" && creatorOpenID !== "") {
3699
- if (!allowFrom.includes(creatorOpenID)) issues.push(`${label} allowFrom missing creatorOpenID ${creatorOpenID.length > 8 ? creatorOpenID.slice(0, 4) + "***" + creatorOpenID.slice(-4) : "***"}`);
3700
- } else if (allowFrom.length === 0) issues.push(`${label} allowFrom is empty (creatorOpenID unavailable, cannot auto-fix)`);
3701
- const secret = bot.appSecret;
3702
- if (typeof secret === "object" && secret !== null && !Array.isArray(secret)) {
3703
- if (!matchMap(secret, DEFAULT_FEISHU_APP_SECRET)) issues.push(`${label} appSecret is a provider-ref but not the canonical one`);
3704
- } else if (typeof secret === "string") {
3705
- if (secret !== larkApp.appSecret) issues.push(`${label} appSecret plaintext mismatch`);
3706
- } else issues.push(`${label} appSecret has unexpected type ${typeof secret}`);
3707
- }
3708
- repair(ctx) {
3709
- const larkApps = ctx.vars.larkApps;
3710
- if (!larkApps || larkApps.length === 0) return;
3711
- const feishu = asRecord(getNestedMap(ctx.config, "channels", "feishu"));
3712
- if (!feishu) return;
3713
- const accounts = asRecord(feishu.accounts);
3714
- if (accounts) for (const [, account] of Object.entries(accounts)) {
3715
- const bot = asRecord(account);
3716
- if (!bot) continue;
3717
- const appId = bot.appId;
3718
- if (typeof appId !== "string" || !appId.startsWith("cli_")) continue;
3719
- const larkApp = larkApps.find((e) => e.larkAppID === appId);
3720
- if (!larkApp) continue;
3721
- this.fixBot(bot, larkApp);
3722
- }
3723
- const singleAppId = feishu.appId;
3724
- if (typeof singleAppId === "string" && singleAppId.startsWith("cli_") && !accounts) {
3725
- const larkApp = larkApps.find((e) => e.larkAppID === singleAppId);
3726
- if (larkApp) this.fixBot(feishu, larkApp);
3727
- }
3728
- }
3729
- /** Fix a single bot entry in-place.
3730
- * appSecret is repaired based on its current type:
3731
- * - object → fix to canonical provider-ref
3732
- * - string → fix to larkApps plaintext
3733
- */
3734
- fixBot(bot, larkApp) {
3735
- const creatorOpenID = larkApp.creatorOpenID;
3736
- if (typeof creatorOpenID === "string" && creatorOpenID !== "") {
3737
- const allowFrom = Array.isArray(bot.allowFrom) ? [...bot.allowFrom] : [];
3738
- if (!allowFrom.includes(creatorOpenID)) {
3739
- allowFrom.push(creatorOpenID);
3740
- bot.allowFrom = allowFrom;
3741
- }
3742
- }
3743
- const secret = bot.appSecret;
3744
- if (typeof secret === "object" && secret !== null && !Array.isArray(secret)) {
3745
- if (!matchMap(secret, DEFAULT_FEISHU_APP_SECRET)) bot.appSecret = { ...DEFAULT_FEISHU_APP_SECRET };
3746
- } else if (typeof secret === "string") {
3747
- if (secret !== larkApp.appSecret) bot.appSecret = larkApp.appSecret;
3748
- }
3749
- }
3750
- };
3751
- FeishuBotChannelConfigRule = __decorate([Rule({
3752
- key: "feishu_bot_channel_config",
3753
- description: "确保飞书配置中 bot 账号的 allowFrom 包含其创建者 openID 且 appSecret 值正确",
3754
- dependsOn: [
3755
- "config_syntax_check",
3756
- "feishu_default_account",
3757
- "feishu_bot_id"
3758
- ],
3759
- repairMode: "standard",
3760
- usesVars: ["larkApps"],
3761
- level: "critical"
3762
- })], FeishuBotChannelConfigRule);
3763
- //#endregion
3764
3984
  //#region src/check.ts
3765
3985
  /** Telemetry-aware entry: returns both the legacy CheckResult (for stdout)
3766
3986
  * AND a DoctorReport-shape payload (for `openclaw.report_cli_run`). The
@@ -4201,32 +4421,8 @@ function finalize$1(results, aborted) {
4201
4421
  };
4202
4422
  }
4203
4423
  //#endregion
4204
- //#region src/paths.ts
4205
- /**
4206
- * Central directory for all ephemeral diagnose/reset artifacts: task status
4207
- * files (`reset-<taskId>.json`) and human-readable step logs
4208
- * (`reset-<taskId>.log`). Having everything under one dir makes debugging a
4209
- * stuck reset much easier — `ls /tmp/openclaw-diagnose/` shows every recent
4210
- * run, and each run's log is right next to its state.
4211
- */
4212
- const DIAGNOSE_DIR = "/tmp/openclaw-diagnose";
4213
- function resetResultFile(taskId) {
4214
- return `${DIAGNOSE_DIR}/reset-${taskId}.json`;
4215
- }
4216
- function resetLogFile(taskId) {
4217
- return `${DIAGNOSE_DIR}/reset-${taskId}.log`;
4218
- }
4219
- /** Sandbox workspace root where openclaw config + agent state lives. */
4220
- const WORKSPACE_DIR = "/home/gem/workspace/agent";
4221
- /** File containing the provider key used by the openclaw miaoda provider. */
4222
- const PROVIDER_FILE_PATH = "/home/gem/workspace/.force/openclaw/miaoda-provider-key";
4223
- /** File containing the miaoda openclaw secrets JSON. */
4224
- const SECRETS_FILE_PATH = "/home/gem/workspace/.force/openclaw/miaoda-openclaw-secrets.json";
4225
- /** Absolute path to the openclaw config JSON. */
4226
- const CONFIG_PATH = `${WORKSPACE_DIR}/openclaw.json`;
4227
- //#endregion
4228
- //#region src/run-log.ts
4229
- let currentRunContext;
4424
+ //#region src/run-log.ts
4425
+ let currentRunContext;
4230
4426
  /**
4231
4427
  * Install the run context for this CLI execution. Must be called once at
4232
4428
  * the top of `main()` before any subcommand work; idempotent (last call
@@ -4360,10 +4556,9 @@ function makeLogger(logFile) {
4360
4556
  /**
4361
4557
  * Start an async reset task: spawn a detached child process and return the taskId.
4362
4558
  *
4363
- * The child process runs: node cli.js reset --worker --task-id=xxx
4364
- * The worker fetches ctx from innerApi itself — no --ctx passthrough.
4559
+ * The child process runs: node cli.js reset --worker --task-id=xxx --ctx=base64
4365
4560
  */
4366
- function startAsyncReset() {
4561
+ function startAsyncReset(ctxBase64) {
4367
4562
  const taskId = (0, node_crypto.randomUUID)();
4368
4563
  const resultFile = resetResultFile(taskId);
4369
4564
  const log = makeLogger(resetLogFile(taskId));
@@ -4387,7 +4582,8 @@ function startAsyncReset() {
4387
4582
  process.argv[1],
4388
4583
  "reset",
4389
4584
  "--worker",
4390
- `--task-id=${taskId}`
4585
+ `--task-id=${taskId}`,
4586
+ `--ctx=${ctxBase64}`
4391
4587
  ], {
4392
4588
  detached: true,
4393
4589
  stdio: "ignore",
@@ -4505,557 +4701,297 @@ function describeCause(c, depth = 0) {
4505
4701
  const head = code ? `${code} ${c.message}` : c.message;
4506
4702
  const inner = describeCause(c.cause, depth + 1);
4507
4703
  return inner ? `${head} <- ${inner}` : head;
4508
- }
4509
- return String(c);
4510
- }
4511
- /** Strip query string — OSS signed URLs put the auth token there. Keeps
4512
- * protocol/host/path so the operator can tell *which* OSS bucket / object
4513
- * was being fetched. */
4514
- function redactUrl(raw) {
4515
- try {
4516
- const u = new URL(raw);
4517
- const tail = u.search ? "?<redacted>" : "";
4518
- return `${u.protocol}//${u.host}${u.pathname}${tail}`;
4519
- } catch {
4520
- return "<invalid-url>";
4521
- }
4522
- }
4523
- function originOf(raw) {
4524
- try {
4525
- return new URL(raw).host;
4526
- } catch {
4527
- return "unknown-host";
4528
- }
4529
- }
4530
- //#endregion
4531
- //#region src/oss/fetchManifest.ts
4532
- const MANIFEST_PREFIX = "builtin/manifests/openclaw/recommended/";
4533
- const MANIFEST_SUFFIX = ".json";
4534
- const manifestCache = /* @__PURE__ */ new Map();
4535
- async function fetchManifest(ossFileMap, tag) {
4536
- const key = `${MANIFEST_PREFIX}${tag}${MANIFEST_SUFFIX}`;
4537
- const url = ossFileMap[key];
4538
- if (!url) {
4539
- const available = Object.keys(ossFileMap).filter((k) => k.startsWith(MANIFEST_PREFIX) && k.endsWith(MANIFEST_SUFFIX)).map((k) => k.slice(39, -5));
4540
- const availStr = available.length ? available.join(", ") : "(none)";
4541
- throw new Error(`manifest signed URL missing for tag "${tag}" (key ${key}). Available tags in ossFileMap: ${availStr}. Either pass an available tag or update the studio_server TCC openclaw_upgrade_config supported_versions.`);
4542
- }
4543
- const cached = manifestCache.get(url);
4544
- if (cached) return cached;
4545
- const label = `openclaw manifest tag=${tag}`;
4546
- const res = await fetchWithDiag(url, { label });
4547
- if (!res.ok) {
4548
- const body = await readPreview(res);
4549
- throw new Error(`${label}: HTTP ${res.status} ${res.statusText}` + (body ? ` body=${body}` : ""));
4550
- }
4551
- const manifest = await res.json();
4552
- manifestCache.set(url, manifest);
4553
- return manifest;
4554
- }
4555
- /** Best-effort body preview for HTTP-error diagnostics. OSS errors are
4556
- * XML; first 256 chars usually contain the <Code> + <Message> we care
4557
- * about. Failures here are swallowed (logging-only, not load-bearing). */
4558
- async function readPreview(res) {
4559
- try {
4560
- return (await res.text()).slice(0, 256);
4561
- } catch {
4562
- return "";
4563
- }
4564
- }
4565
- async function downloadWithCache(pkg, ossFileMap, opts = {}) {
4566
- const cacheRoot = opts.cacheRoot ?? "/tmp/openclaw-diagnose/resources";
4567
- const shortHash = pkg.shasum.slice(0, 16);
4568
- const destDir = node_path.default.join(cacheRoot, shortHash);
4569
- const destFile = node_path.default.join(destDir, node_path.default.posix.basename(pkg.ossKey));
4570
- node_fs.default.mkdirSync(destDir, { recursive: true });
4571
- if (node_fs.default.existsSync(destFile)) return destFile;
4572
- const url = ossFileMap[pkg.ossKey];
4573
- if (!url) throw new Error(`signed URL missing for ${pkg.ossKey}`);
4574
- if (!pkg.integrity.startsWith("sha512-")) throw new Error(`unsupported integrity format: ${pkg.integrity}`);
4575
- const expected = pkg.integrity.slice(7);
4576
- const tmpFile = node_path.default.join(destDir, `.tmp.${process.pid}.${node_crypto.default.randomBytes(4).toString("hex")}`);
4577
- try {
4578
- const label = `download ${pkg.ossKey}`;
4579
- const res = await fetchWithDiag(url, {
4580
- label,
4581
- timeoutMs: 6e4
4582
- });
4583
- if (!res.ok) {
4584
- let preview = "";
4585
- try {
4586
- preview = (await res.text()).slice(0, 256);
4587
- } catch {}
4588
- throw new Error(`${label}: HTTP ${res.status} ${res.statusText}` + (preview ? ` body=${preview}` : ""));
4589
- }
4590
- if (!res.body) throw new Error(`${label}: empty body`);
4591
- const hasher = node_crypto.default.createHash("sha512");
4592
- const source = node_stream.Readable.fromWeb(res.body);
4593
- async function* teeAndHash(src) {
4594
- for await (const chunk of src) {
4595
- hasher.update(chunk);
4596
- yield chunk;
4597
- }
4598
- }
4599
- await (0, node_stream_promises.pipeline)(source, teeAndHash, node_fs.default.createWriteStream(tmpFile));
4600
- const actual = hasher.digest("base64");
4601
- if (actual !== expected) {
4602
- const envBypass = process.env.OPENCLAW_DEBUG_SKIP_INTEGRITY === "1";
4603
- if (opts.skipIntegrity || envBypass) {
4604
- const sourceLabel = opts.skipIntegrity ? "skipIntegrity=true" : "OPENCLAW_DEBUG_SKIP_INTEGRITY=1";
4605
- console.error(`⚠ [downloadWithCache] INTEGRITY BYPASS for ${pkg.ossKey}: expected ${expected.slice(0, 12)}… got ${actual.slice(0, 12)}… — ${sourceLabel}. DO NOT use this flag in production.`);
4606
- } else throw new Error(`integrity mismatch for ${pkg.ossKey}: expected ${expected} got ${actual}`);
4607
- }
4608
- moveSafe(tmpFile, destFile);
4609
- return destFile;
4610
- } catch (e) {
4611
- try {
4612
- node_fs.default.unlinkSync(tmpFile);
4613
- } catch {}
4614
- throw e;
4615
- }
4616
- }
4617
- //#endregion
4618
- //#region src/install-openclaw.ts
4619
- async function installOpenclaw(openclawTag, ossFileMap, opts = {}) {
4620
- const homeBase = resolveHomeBase(opts.homeBase);
4621
- const t0 = Date.now();
4622
- const pkg = (await fetchManifest(ossFileMap, openclawTag)).packages.find((p) => p.role === "cli" && p.name === "openclaw");
4623
- if (!pkg) throw new Error("install-openclaw: role=cli,name=openclaw not found in manifest");
4624
- const targetDir = opts.targetDir ?? node_path.default.join(homeBase, pkg.installPath);
4625
- const bakDir = targetDir + ".bak";
4626
- const newDir = targetDir + ".new";
4627
- const tarball = await downloadWithCache(pkg, ossFileMap, opts);
4628
- console.error(`[install-openclaw] tag=${openclawTag} shasum=${pkg.shasum.slice(0, 12)}...`);
4629
- if (node_fs.default.existsSync(newDir)) node_fs.default.rmSync(newDir, {
4630
- recursive: true,
4631
- force: true
4632
- });
4633
- if (node_fs.default.existsSync(bakDir)) node_fs.default.rmSync(bakDir, {
4634
- recursive: true,
4635
- force: true
4636
- });
4637
- node_fs.default.mkdirSync(node_path.default.dirname(targetDir), { recursive: true });
4638
- const tmpStage = node_fs.default.mkdtempSync(node_path.default.join(opts.tmpRoot ?? node_os.default.tmpdir(), "openclaw-install-"));
4639
- try {
4640
- extractTarballTolerant(tarball, tmpStage, { stripComponents: 1 });
4641
- if (!node_fs.default.existsSync(node_path.default.join(tmpStage, "package.json"))) throw new Error("extracted tarball missing package.json");
4642
- moveSafe(tmpStage, newDir);
4643
- const hadExisting = node_fs.default.existsSync(targetDir);
4644
- try {
4645
- if (hadExisting) moveSafe(targetDir, bakDir);
4646
- moveSafe(newDir, targetDir);
4647
- } catch (e) {
4648
- if (hadExisting && !node_fs.default.existsSync(targetDir) && node_fs.default.existsSync(bakDir)) try {
4649
- moveSafe(bakDir, targetDir);
4650
- } catch {}
4651
- try {
4652
- node_fs.default.rmSync(newDir, {
4653
- recursive: true,
4654
- force: true
4655
- });
4656
- } catch {}
4657
- throw e;
4658
- }
4659
- if (hadExisting && node_fs.default.existsSync(bakDir)) node_fs.default.rmSync(bakDir, {
4660
- recursive: true,
4661
- force: true
4662
- });
4663
- } finally {
4664
- if (node_fs.default.existsSync(tmpStage)) try {
4665
- node_fs.default.rmSync(tmpStage, {
4666
- recursive: true,
4667
- force: true
4668
- });
4669
- } catch {}
4670
- }
4671
- console.error(`[install-openclaw] done in ${Date.now() - t0}ms`);
4672
- }
4673
- async function installExtension(tag, ossFileMap, opts = {}) {
4674
- const homeBase = resolveHomeBase(opts.homeBase);
4675
- const hasAll = !!opts.all;
4676
- const hasNames = (opts.names?.length ?? 0) > 0;
4677
- if (hasAll && hasNames) throw new Error("install-extension: --all and --extension are mutually exclusive");
4678
- if (!hasAll && !hasNames) throw new Error("install-extension: must provide --all or --extension=<name>");
4679
- const allExts = (await fetchManifest(ossFileMap, tag)).packages.filter((p) => p.role === "extension");
4680
- let targets;
4681
- if (hasAll) targets = allExts;
4682
- else {
4683
- const wanted = new Set(opts.names);
4684
- targets = allExts.filter((p) => wanted.has(p.name) || p.packageName != null && wanted.has(p.packageName));
4685
- const foundKeys = /* @__PURE__ */ new Set();
4686
- for (const t of targets) {
4687
- foundKeys.add(t.name);
4688
- if (t.packageName) foundKeys.add(t.packageName);
4689
- }
4690
- const missing = opts.names.filter((n) => !foundKeys.has(n));
4691
- if (missing.length > 0) throw new Error(`install-extension: not found in manifest: ${missing.join(", ")}`);
4692
- }
4693
- console.error(`[install-extension] tag=${tag} targets=${targets.length}`);
4694
- const t0 = Date.now();
4695
- const tarballs = await Promise.all(targets.map(async (p) => {
4696
- const tb = await downloadWithCache(p, ossFileMap, opts);
4697
- console.error(`[install-extension] ${p.name}: downloaded`);
4698
- return {
4699
- pkg: p,
4700
- tarball: tb
4701
- };
4702
- }));
4703
- for (const { pkg, tarball } of tarballs) {
4704
- installOne$1(pkg, tarball, homeBase);
4705
- console.error(`[install-extension] ${pkg.name}: installed`);
4706
- }
4707
- if (!opts.skipConfigUpdate) updatePluginInstalls(opts.configPath ?? node_path.default.join(homeBase, "workspace/agent/openclaw.json"), targets);
4708
- else console.error(`[install-extension] skipConfigUpdate=true — not touching openclaw.json`);
4709
- console.error(`[install-extension] done ${targets.length}/${targets.length} in ${Date.now() - t0}ms`);
4710
- }
4711
- const MEM0_PLUGIN_NAME = "openclaw-mem0-plugin";
4712
- const PLUGINS_TO_AUTO_ENABLE = ["openclaw-lark", "openclaw-extension-miaoda"];
4713
- /**
4714
- * Merge each installed extension's installMetadata into openclaw.json's
4715
- * plugins.installs[<pkg.name>]. Atomic write via tmp + rename.
4716
- *
4717
- * - No openclaw.json → log + return (not an error; some install contexts don't have it yet)
4718
- * - Extension without installMetadata in manifest → skip that entry (log)
4719
- * - Existing plugins.installs entries for other extensions left untouched
4720
- *
4721
- * Special-case for mem0: when openclaw-mem0-plugin is among the installed
4722
- * targets, also append it to plugins.allow (idempotent) and seed
4723
- * plugins.entries.openclaw-mem0-plugin = { enabled: false } when the key
4724
- * is absent. Existing values are preserved (user toggles survive).
4725
- */
4726
- function updatePluginInstalls(configPath, installedPkgs) {
4727
- if (!node_fs.default.existsSync(configPath)) {
4728
- console.error(`[install-extension] no config at ${configPath} — skip plugins.installs update`);
4729
- return;
4730
- }
4731
- const JSON5 = loadJSON5();
4732
- const raw = node_fs.default.readFileSync(configPath, "utf-8");
4733
- const config = JSON5.parse(raw);
4734
- if (!config.plugins || typeof config.plugins !== "object") config.plugins = {};
4735
- const plugins = config.plugins;
4736
- if (!plugins.installs || typeof plugins.installs !== "object") plugins.installs = {};
4737
- const installs = plugins.installs;
4738
- let updated = 0;
4739
- let skipped = 0;
4740
- for (const pkg of installedPkgs) if (pkg.installMetadata) {
4741
- installs[pkg.name] = pkg.installMetadata;
4742
- updated++;
4743
- } else skipped++;
4744
- if (installedPkgs.some((p) => p.name === MEM0_PLUGIN_NAME)) {
4745
- const allowRaw = plugins.allow;
4746
- const allow = Array.isArray(allowRaw) ? allowRaw : [];
4747
- if (!allow.filter((e) => typeof e === "string").includes(MEM0_PLUGIN_NAME)) allow.push(MEM0_PLUGIN_NAME);
4748
- plugins.allow = allow;
4749
- if (!plugins.entries || typeof plugins.entries !== "object" || Array.isArray(plugins.entries)) plugins.entries = {};
4750
- const entries = plugins.entries;
4751
- if (!(MEM0_PLUGIN_NAME in entries)) entries[MEM0_PLUGIN_NAME] = { enabled: false };
4752
- }
4753
- for (const pkg of installedPkgs) {
4754
- if (!PLUGINS_TO_AUTO_ENABLE.includes(pkg.name)) continue;
4755
- if (!Array.isArray(plugins.allow)) plugins.allow = [];
4756
- const allow = plugins.allow;
4757
- if (!allow.includes(pkg.name)) allow.push(pkg.name);
4758
- if (!plugins.entries || typeof plugins.entries !== "object" || Array.isArray(plugins.entries)) plugins.entries = {};
4759
- const entries = plugins.entries;
4760
- entries[pkg.name] = {
4761
- ...asRecord(entries[pkg.name]) ?? {},
4762
- enabled: true
4763
- };
4764
- }
4765
- const tmpPath = configPath + ".installs-tmp";
4766
- node_fs.default.writeFileSync(tmpPath, JSON.stringify(config, null, 2), "utf-8");
4767
- moveSafe(tmpPath, configPath);
4768
- console.error(`[install-extension] plugins.installs updated: ${updated} entry(ies) in ${configPath}` + (skipped > 0 ? ` (${skipped} package(s) without installMetadata skipped)` : ""));
4769
- }
4770
- function installOne$1(pkg, tarball, homeBase) {
4771
- const destDir = node_path.default.join(homeBase, pkg.installPath);
4772
- const stagingDir = destDir + ".new";
4773
- const oldDir = destDir + ".old";
4774
- node_fs.default.mkdirSync(node_path.default.dirname(destDir), { recursive: true });
4775
- if (node_fs.default.existsSync(stagingDir)) node_fs.default.rmSync(stagingDir, {
4776
- recursive: true,
4777
- force: true
4778
- });
4779
- node_fs.default.mkdirSync(stagingDir);
4780
- try {
4781
- extractTarballTolerant(tarball, stagingDir, { stripComponents: 1 });
4782
- if (!node_fs.default.existsSync(node_path.default.join(stagingDir, "package.json"))) throw new Error(`extension tarball missing package.json: ${pkg.name}`);
4783
- } catch (e) {
4784
- try {
4785
- node_fs.default.rmSync(stagingDir, {
4786
- recursive: true,
4787
- force: true
4788
- });
4789
- } catch {}
4790
- throw e;
4791
- }
4792
- const hadOld = node_fs.default.existsSync(destDir);
4793
- if (hadOld) moveSafe(destDir, oldDir);
4794
- moveSafe(stagingDir, destDir);
4795
- if (hadOld && node_fs.default.existsSync(oldDir)) node_fs.default.rmSync(oldDir, {
4796
- recursive: true,
4797
- force: true
4798
- });
4799
- }
4800
- //#endregion
4801
- //#region src/lark-cli-init.ts
4802
- const LARK_PLUGIN_NAMES = ["openclaw-lark", "feishu-openclaw-plugin"];
4803
- const PE_XML_TAG = "lark-cli-pe";
4804
- const PE_PLACEHOLDER = `
4805
- <${PE_XML_TAG}>
4806
- **【强制要求 - 无例外】飞书所有操作必须先读 feishu-lark-cli skill。未读skill不得调用任何飞书工具,违者视为操作失误。**
4807
- </${PE_XML_TAG}>
4808
- `;
4809
- function isLarkPluginInstalled(configPath) {
4810
- const extDir = getExtensionsDir(configPath);
4811
- return LARK_PLUGIN_NAMES.some((name) => {
4812
- try {
4813
- return node_fs.default.existsSync(node_path.default.join(extDir, name, "package.json"));
4814
- } catch {
4815
- return false;
4816
- }
4817
- });
4704
+ }
4705
+ return String(c);
4818
4706
  }
4819
- function isLarkCliAvailable() {
4707
+ /** Strip query string — OSS signed URLs put the auth token there. Keeps
4708
+ * protocol/host/path so the operator can tell *which* OSS bucket / object
4709
+ * was being fetched. */
4710
+ function redactUrl(raw) {
4820
4711
  try {
4821
- return (0, node_child_process.spawnSync)("lark-cli", ["--version"], {
4822
- encoding: "utf-8",
4823
- timeout: 5e3,
4824
- stdio: [
4825
- "ignore",
4826
- "pipe",
4827
- "ignore"
4828
- ]
4829
- }).status === 0;
4712
+ const u = new URL(raw);
4713
+ const tail = u.search ? "?<redacted>" : "";
4714
+ return `${u.protocol}//${u.host}${u.pathname}${tail}`;
4830
4715
  } catch {
4831
- return false;
4716
+ return "<invalid-url>";
4832
4717
  }
4833
4718
  }
4834
- function readConfig(configPath) {
4719
+ function originOf(raw) {
4835
4720
  try {
4836
- const raw = node_fs.default.readFileSync(configPath, "utf-8");
4837
- const parsed = loadJSON5().parse(raw);
4838
- return parsed != null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
4721
+ return new URL(raw).host;
4839
4722
  } catch {
4840
- return null;
4841
- }
4842
- }
4843
- /**
4844
- * Resolve the feishu app secret for the given appId.
4845
- *
4846
- * Lookup order:
4847
- * 1. channels.feishu.appSecret (single-agent: feishu.appId === appId)
4848
- * 2. channels.feishu.accounts[key].appSecret (multi-agent: account.appId === appId)
4849
- *
4850
- * Value interpretation:
4851
- * - string → use directly
4852
- * - object → secret is managed by a provider; use `feishuAppSecret` param instead
4853
- *
4854
- * Returns null when the secret cannot be determined.
4855
- */
4856
- function resolveAppSecret(appId, config, feishuAppSecret) {
4857
- const feishu = getNestedMap(config, "channels", "feishu");
4858
- if (!feishu) return null;
4859
- let rawSecret;
4860
- if (typeof feishu.appId === "string" && feishu.appId === appId) rawSecret = feishu.appSecret;
4861
- else {
4862
- const accounts = asRecord(feishu.accounts);
4863
- if (accounts) for (const [, val] of Object.entries(accounts)) {
4864
- const account = asRecord(val);
4865
- if (account?.appId === appId) {
4866
- rawSecret = account.appSecret ?? feishu.appSecret;
4867
- break;
4868
- }
4869
- }
4723
+ return "unknown-host";
4870
4724
  }
4871
- if (typeof rawSecret === "string" && rawSecret) return rawSecret;
4872
- if (rawSecret != null && typeof rawSecret === "object") return feishuAppSecret ?? null;
4873
- return null;
4874
4725
  }
4875
- /**
4876
- * Resolve the agents.md path for the given appId from the openclaw config.
4877
- *
4878
- * Case 1: appId matches channels.feishu.appId (single-agent path)
4879
- * → WORKSPACE_DIR/AGENTS.md
4880
- *
4881
- * Case 2: appId found in channels.feishu.accounts (multi-agent path)
4882
- * → find account key where account.appId === appId
4883
- * → find binding where match.channel=feishu && match.accountId=that key
4884
- * → if agentId === 'main' → WORKSPACE_DIR/agents.md
4885
- * → else find agent in agents.list by id → agent.workspace/agents.md
4886
- *
4887
- * Returns null when the path cannot be determined.
4888
- */
4889
- function resolveAgentsMdPath(appId, config) {
4890
- const feishu = getNestedMap(config, "channels", "feishu");
4891
- if (!feishu) {
4892
- console.error("resolveAgentsMdPath: channels.feishu not found");
4893
- return null;
4894
- }
4895
- if (typeof feishu.appId === "string" && feishu.appId === appId) {
4896
- console.error(`resolveAgentsMdPath: case=single-agent feishu.appId=${feishu.appId}`);
4897
- return node_path.default.join(WORKSPACE_DIR, "workspace", "AGENTS.md");
4898
- }
4899
- const accounts = asRecord(feishu.accounts);
4900
- if (!accounts) {
4901
- console.error("resolveAgentsMdPath: feishu.accounts not found");
4902
- return null;
4726
+ //#endregion
4727
+ //#region src/oss/fetchManifest.ts
4728
+ const MANIFEST_PREFIX = "builtin/manifests/openclaw/recommended/";
4729
+ const MANIFEST_SUFFIX = ".json";
4730
+ const manifestCache = /* @__PURE__ */ new Map();
4731
+ async function fetchManifest(ossFileMap, tag) {
4732
+ const key = `${MANIFEST_PREFIX}${tag}${MANIFEST_SUFFIX}`;
4733
+ const url = ossFileMap[key];
4734
+ if (!url) {
4735
+ const available = Object.keys(ossFileMap).filter((k) => k.startsWith(MANIFEST_PREFIX) && k.endsWith(MANIFEST_SUFFIX)).map((k) => k.slice(39, -5));
4736
+ const availStr = available.length ? available.join(", ") : "(none)";
4737
+ throw new Error(`manifest signed URL missing for tag "${tag}" (key ${key}). Available tags in ossFileMap: ${availStr}. Either pass an available tag or update the studio_server TCC openclaw_upgrade_config supported_versions.`);
4903
4738
  }
4904
- let accountId;
4905
- for (const [key, val] of Object.entries(accounts)) if (asRecord(val)?.appId === appId) {
4906
- accountId = key;
4907
- break;
4739
+ const cached = manifestCache.get(url);
4740
+ if (cached) return cached;
4741
+ const label = `openclaw manifest tag=${tag}`;
4742
+ const res = await fetchWithDiag(url, { label });
4743
+ if (!res.ok) {
4744
+ const body = await readPreview(res);
4745
+ throw new Error(`${label}: HTTP ${res.status} ${res.statusText}` + (body ? ` body=${body}` : ""));
4908
4746
  }
4909
- if (!accountId) {
4910
- console.error(`resolveAgentsMdPath: no account found with appId=${appId} in feishu.accounts keys=[${Object.keys(accounts).join(",")}]`);
4911
- return null;
4747
+ const manifest = await res.json();
4748
+ manifestCache.set(url, manifest);
4749
+ return manifest;
4750
+ }
4751
+ /** Best-effort body preview for HTTP-error diagnostics. OSS errors are
4752
+ * XML; first 256 chars usually contain the <Code> + <Message> we care
4753
+ * about. Failures here are swallowed (logging-only, not load-bearing). */
4754
+ async function readPreview(res) {
4755
+ try {
4756
+ return (await res.text()).slice(0, 256);
4757
+ } catch {
4758
+ return "";
4912
4759
  }
4913
- console.error(`resolveAgentsMdPath: found accountId=${accountId}`);
4914
- const bindings = Array.isArray(config.bindings) ? config.bindings : [];
4915
- let agentId;
4916
- for (const b of bindings) {
4917
- const binding = asRecord(b);
4918
- if (!binding) continue;
4919
- const match = asRecord(binding.match);
4920
- if (match?.channel === "feishu" && match?.accountId === accountId) {
4921
- if (typeof binding.agentId === "string") {
4922
- agentId = binding.agentId;
4923
- break;
4760
+ }
4761
+ async function downloadWithCache(pkg, ossFileMap, opts = {}) {
4762
+ const cacheRoot = opts.cacheRoot ?? "/tmp/openclaw-diagnose/resources";
4763
+ const shortHash = pkg.shasum.slice(0, 16);
4764
+ const destDir = node_path.default.join(cacheRoot, shortHash);
4765
+ const destFile = node_path.default.join(destDir, node_path.default.posix.basename(pkg.ossKey));
4766
+ node_fs.default.mkdirSync(destDir, { recursive: true });
4767
+ if (node_fs.default.existsSync(destFile)) return destFile;
4768
+ const url = ossFileMap[pkg.ossKey];
4769
+ if (!url) throw new Error(`signed URL missing for ${pkg.ossKey}`);
4770
+ if (!pkg.integrity.startsWith("sha512-")) throw new Error(`unsupported integrity format: ${pkg.integrity}`);
4771
+ const expected = pkg.integrity.slice(7);
4772
+ const tmpFile = node_path.default.join(destDir, `.tmp.${process.pid}.${node_crypto.default.randomBytes(4).toString("hex")}`);
4773
+ try {
4774
+ const label = `download ${pkg.ossKey}`;
4775
+ const res = await fetchWithDiag(url, {
4776
+ label,
4777
+ timeoutMs: 6e4
4778
+ });
4779
+ if (!res.ok) {
4780
+ let preview = "";
4781
+ try {
4782
+ preview = (await res.text()).slice(0, 256);
4783
+ } catch {}
4784
+ throw new Error(`${label}: HTTP ${res.status} ${res.statusText}` + (preview ? ` body=${preview}` : ""));
4785
+ }
4786
+ if (!res.body) throw new Error(`${label}: empty body`);
4787
+ const hasher = node_crypto.default.createHash("sha512");
4788
+ const source = node_stream.Readable.fromWeb(res.body);
4789
+ async function* teeAndHash(src) {
4790
+ for await (const chunk of src) {
4791
+ hasher.update(chunk);
4792
+ yield chunk;
4924
4793
  }
4925
4794
  }
4795
+ await (0, node_stream_promises.pipeline)(source, teeAndHash, node_fs.default.createWriteStream(tmpFile));
4796
+ const actual = hasher.digest("base64");
4797
+ if (actual !== expected) {
4798
+ const envBypass = process.env.OPENCLAW_DEBUG_SKIP_INTEGRITY === "1";
4799
+ if (opts.skipIntegrity || envBypass) {
4800
+ const sourceLabel = opts.skipIntegrity ? "skipIntegrity=true" : "OPENCLAW_DEBUG_SKIP_INTEGRITY=1";
4801
+ console.error(`⚠ [downloadWithCache] INTEGRITY BYPASS for ${pkg.ossKey}: expected ${expected.slice(0, 12)}… got ${actual.slice(0, 12)}… — ${sourceLabel}. DO NOT use this flag in production.`);
4802
+ } else throw new Error(`integrity mismatch for ${pkg.ossKey}: expected ${expected} got ${actual}`);
4803
+ }
4804
+ moveSafe(tmpFile, destFile);
4805
+ return destFile;
4806
+ } catch (e) {
4807
+ try {
4808
+ node_fs.default.unlinkSync(tmpFile);
4809
+ } catch {}
4810
+ throw e;
4926
4811
  }
4927
- if (!agentId) {
4928
- console.error(`resolveAgentsMdPath: no binding found for accountId=${accountId} in bindings(count=${bindings.length})`);
4929
- return null;
4930
- }
4931
- console.error(`resolveAgentsMdPath: found agentId=${agentId}`);
4932
- if (agentId === "main") {
4933
- console.error("resolveAgentsMdPath: case=multi-agent-main");
4934
- return node_path.default.join(WORKSPACE_DIR, "workspace", "AGENTS.md");
4935
- }
4936
- const agentsObj = asRecord(config.agents);
4937
- const list = Array.isArray(agentsObj?.list) ? agentsObj.list : [];
4938
- for (const a of list) {
4939
- const agent = asRecord(a);
4940
- if (agent?.id === agentId) {
4941
- const ws = typeof agent.workspace === "string" ? agent.workspace : node_path.default.join(WORKSPACE_DIR, "workspace");
4942
- console.error(`resolveAgentsMdPath: case=multi-agent-custom agentId=${agentId} workspace=${ws}`);
4943
- return node_path.default.join(ws, "AGENTS.md");
4812
+ }
4813
+ //#endregion
4814
+ //#region src/install-openclaw.ts
4815
+ async function installOpenclaw(openclawTag, ossFileMap, opts = {}) {
4816
+ const homeBase = resolveHomeBase(opts.homeBase);
4817
+ const t0 = Date.now();
4818
+ const pkg = (await fetchManifest(ossFileMap, openclawTag)).packages.find((p) => p.role === "cli" && p.name === "openclaw");
4819
+ if (!pkg) throw new Error("install-openclaw: role=cli,name=openclaw not found in manifest");
4820
+ const targetDir = opts.targetDir ?? node_path.default.join(homeBase, pkg.installPath);
4821
+ const bakDir = targetDir + ".bak";
4822
+ const newDir = targetDir + ".new";
4823
+ const tarball = await downloadWithCache(pkg, ossFileMap, opts);
4824
+ console.error(`[install-openclaw] tag=${openclawTag} shasum=${pkg.shasum.slice(0, 12)}...`);
4825
+ if (node_fs.default.existsSync(newDir)) node_fs.default.rmSync(newDir, {
4826
+ recursive: true,
4827
+ force: true
4828
+ });
4829
+ if (node_fs.default.existsSync(bakDir)) node_fs.default.rmSync(bakDir, {
4830
+ recursive: true,
4831
+ force: true
4832
+ });
4833
+ node_fs.default.mkdirSync(node_path.default.dirname(targetDir), { recursive: true });
4834
+ const tmpStage = node_fs.default.mkdtempSync(node_path.default.join(opts.tmpRoot ?? node_os.default.tmpdir(), "openclaw-install-"));
4835
+ try {
4836
+ extractTarballTolerant(tarball, tmpStage, { stripComponents: 1 });
4837
+ if (!node_fs.default.existsSync(node_path.default.join(tmpStage, "package.json"))) throw new Error("extracted tarball missing package.json");
4838
+ moveSafe(tmpStage, newDir);
4839
+ const hadExisting = node_fs.default.existsSync(targetDir);
4840
+ try {
4841
+ if (hadExisting) moveSafe(targetDir, bakDir);
4842
+ moveSafe(newDir, targetDir);
4843
+ } catch (e) {
4844
+ if (hadExisting && !node_fs.default.existsSync(targetDir) && node_fs.default.existsSync(bakDir)) try {
4845
+ moveSafe(bakDir, targetDir);
4846
+ } catch {}
4847
+ try {
4848
+ node_fs.default.rmSync(newDir, {
4849
+ recursive: true,
4850
+ force: true
4851
+ });
4852
+ } catch {}
4853
+ throw e;
4944
4854
  }
4855
+ if (hadExisting && node_fs.default.existsSync(bakDir)) node_fs.default.rmSync(bakDir, {
4856
+ recursive: true,
4857
+ force: true
4858
+ });
4859
+ } finally {
4860
+ if (node_fs.default.existsSync(tmpStage)) try {
4861
+ node_fs.default.rmSync(tmpStage, {
4862
+ recursive: true,
4863
+ force: true
4864
+ });
4865
+ } catch {}
4945
4866
  }
4946
- console.error(`resolveAgentsMdPath: agentId=${agentId} not found in agents.list(count=${list.length})`);
4947
- return null;
4867
+ console.error(`[install-openclaw] done in ${Date.now() - t0}ms`);
4948
4868
  }
4949
- function appendPeToAgentsMd(agentsMdPath) {
4950
- const dir = node_path.default.dirname(agentsMdPath);
4951
- if (!node_fs.default.existsSync(dir)) node_fs.default.mkdirSync(dir, { recursive: true });
4952
- const existing = node_fs.default.existsSync(agentsMdPath) ? node_fs.default.readFileSync(agentsMdPath, "utf-8") : "";
4953
- if (existing.includes(`<${PE_XML_TAG}>`)) {
4954
- console.error(`lark-cli-init: <${PE_XML_TAG}> already present in ${agentsMdPath}, skipping`);
4955
- return;
4869
+ async function installExtension(tag, ossFileMap, opts = {}) {
4870
+ const homeBase = resolveHomeBase(opts.homeBase);
4871
+ const hasAll = !!opts.all;
4872
+ const hasNames = (opts.names?.length ?? 0) > 0;
4873
+ if (hasAll && hasNames) throw new Error("install-extension: --all and --extension are mutually exclusive");
4874
+ if (!hasAll && !hasNames) throw new Error("install-extension: must provide --all or --extension=<name>");
4875
+ const allExts = (await fetchManifest(ossFileMap, tag)).packages.filter((p) => p.role === "extension");
4876
+ let targets;
4877
+ if (hasAll) targets = allExts;
4878
+ else {
4879
+ const wanted = new Set(opts.names);
4880
+ targets = allExts.filter((p) => wanted.has(p.name) || p.packageName != null && wanted.has(p.packageName));
4881
+ const foundKeys = /* @__PURE__ */ new Set();
4882
+ for (const t of targets) {
4883
+ foundKeys.add(t.name);
4884
+ if (t.packageName) foundKeys.add(t.packageName);
4885
+ }
4886
+ const missing = opts.names.filter((n) => !foundKeys.has(n));
4887
+ if (missing.length > 0) throw new Error(`install-extension: not found in manifest: ${missing.join(", ")}`);
4956
4888
  }
4957
- const sep = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
4958
- node_fs.default.appendFileSync(agentsMdPath, `${sep}${PE_PLACEHOLDER}`, "utf-8");
4959
- console.error(`lark-cli-init: appended PE placeholder to ${agentsMdPath}`);
4889
+ console.error(`[install-extension] tag=${tag} targets=${targets.length}`);
4890
+ const t0 = Date.now();
4891
+ const tarballs = await Promise.all(targets.map(async (p) => {
4892
+ const tb = await downloadWithCache(p, ossFileMap, opts);
4893
+ console.error(`[install-extension] ${p.name}: downloaded`);
4894
+ return {
4895
+ pkg: p,
4896
+ tarball: tb
4897
+ };
4898
+ }));
4899
+ for (const { pkg, tarball } of tarballs) {
4900
+ installOne$1(pkg, tarball, homeBase);
4901
+ console.error(`[install-extension] ${pkg.name}: installed`);
4902
+ }
4903
+ if (!opts.skipConfigUpdate) updatePluginInstalls(opts.configPath ?? node_path.default.join(homeBase, "workspace/agent/openclaw.json"), targets);
4904
+ else console.error(`[install-extension] skipConfigUpdate=true — not touching openclaw.json`);
4905
+ console.error(`[install-extension] done ${targets.length}/${targets.length} in ${Date.now() - t0}ms`);
4960
4906
  }
4907
+ const MEM0_PLUGIN_NAME = "openclaw-mem0-plugin";
4908
+ const PLUGINS_TO_AUTO_ENABLE = ["openclaw-lark", "openclaw-extension-miaoda"];
4961
4909
  /**
4962
- * Collect every Feishu bot appId declared in the openclaw config.
4963
- * Covers both single-agent (channels.feishu.appId) and multi-agent
4964
- * (channels.feishu.accounts[*].appId) layouts. Returns a deduplicated list.
4910
+ * Merge each installed extension's installMetadata into openclaw.json's
4911
+ * plugins.installs[<pkg.name>]. Atomic write via tmp + rename.
4912
+ *
4913
+ * - No openclaw.json → log + return (not an error; some install contexts don't have it yet)
4914
+ * - Extension without installMetadata in manifest → skip that entry (log)
4915
+ * - Existing plugins.installs entries for other extensions left untouched
4916
+ *
4917
+ * Special-case for mem0: when openclaw-mem0-plugin is among the installed
4918
+ * targets, also append it to plugins.allow (idempotent) and seed
4919
+ * plugins.entries.openclaw-mem0-plugin = { enabled: false } when the key
4920
+ * is absent. Existing values are preserved (user toggles survive).
4965
4921
  */
4966
- function collectFeishuAppIds(configPath) {
4967
- const config = readConfig(configPath ?? CONFIG_PATH);
4968
- if (!config) return [];
4969
- const feishu = getNestedMap(config, "channels", "feishu");
4970
- if (!feishu) return [];
4971
- const appIds = /* @__PURE__ */ new Set();
4972
- const topAppId = feishu.appId;
4973
- if (typeof topAppId === "string" && topAppId.trim()) appIds.add(topAppId.trim());
4974
- const accounts = asRecord(feishu.accounts);
4975
- if (accounts) for (const val of Object.values(accounts)) {
4976
- const appId = asRecord(val)?.appId;
4977
- if (typeof appId === "string" && appId.trim()) appIds.add(appId.trim());
4922
+ function updatePluginInstalls(configPath, installedPkgs) {
4923
+ if (!node_fs.default.existsSync(configPath)) {
4924
+ console.error(`[install-extension] no config at ${configPath} — skip plugins.installs update`);
4925
+ return;
4978
4926
  }
4979
- return [...appIds];
4980
- }
4981
- function runLarkCliInit(opts) {
4982
- const configPath = opts.configPath ?? CONFIG_PATH;
4983
- if (!isLarkPluginInstalled(configPath)) {
4984
- console.error("lark-cli-init: skipping openclaw-lark plugin not installed");
4985
- return {
4986
- ok: true,
4987
- skipped: true,
4988
- skipReason: "openclaw-lark plugin not installed"
4989
- };
4927
+ const JSON5 = loadJSON5();
4928
+ const raw = node_fs.default.readFileSync(configPath, "utf-8");
4929
+ const config = JSON5.parse(raw);
4930
+ if (!config.plugins || typeof config.plugins !== "object") config.plugins = {};
4931
+ const plugins = config.plugins;
4932
+ if (!plugins.installs || typeof plugins.installs !== "object") plugins.installs = {};
4933
+ const installs = plugins.installs;
4934
+ let updated = 0;
4935
+ let skipped = 0;
4936
+ for (const pkg of installedPkgs) if (pkg.installMetadata) {
4937
+ installs[pkg.name] = pkg.installMetadata;
4938
+ updated++;
4939
+ } else skipped++;
4940
+ if (installedPkgs.some((p) => p.name === MEM0_PLUGIN_NAME)) {
4941
+ const allowRaw = plugins.allow;
4942
+ const allow = Array.isArray(allowRaw) ? allowRaw : [];
4943
+ if (!allow.filter((e) => typeof e === "string").includes(MEM0_PLUGIN_NAME)) allow.push(MEM0_PLUGIN_NAME);
4944
+ plugins.allow = allow;
4945
+ if (!plugins.entries || typeof plugins.entries !== "object" || Array.isArray(plugins.entries)) plugins.entries = {};
4946
+ const entries = plugins.entries;
4947
+ if (!(MEM0_PLUGIN_NAME in entries)) entries[MEM0_PLUGIN_NAME] = { enabled: false };
4990
4948
  }
4991
- if (!isLarkCliAvailable()) {
4992
- console.error("lark-cli-init: skipping — lark-cli command not found");
4993
- return {
4994
- ok: true,
4995
- skipped: true,
4996
- skipReason: "lark-cli command not found"
4949
+ for (const pkg of installedPkgs) {
4950
+ if (!PLUGINS_TO_AUTO_ENABLE.includes(pkg.name)) continue;
4951
+ if (!Array.isArray(plugins.allow)) plugins.allow = [];
4952
+ const allow = plugins.allow;
4953
+ if (!allow.includes(pkg.name)) allow.push(pkg.name);
4954
+ if (!plugins.entries || typeof plugins.entries !== "object" || Array.isArray(plugins.entries)) plugins.entries = {};
4955
+ const entries = plugins.entries;
4956
+ entries[pkg.name] = {
4957
+ ...asRecord(entries[pkg.name]) ?? {},
4958
+ enabled: true
4997
4959
  };
4998
4960
  }
4999
- const config = readConfig(configPath);
5000
- if (!config) return {
5001
- ok: false,
5002
- error: `could not read config at ${configPath}`
5003
- };
5004
- const agentsMdPath = resolveAgentsMdPath(opts.appId, config);
5005
- console.error(`lark-cli-init: resolved agents.md path=${agentsMdPath ?? "(null)"}`);
5006
- if (!agentsMdPath) return {
5007
- ok: false,
5008
- error: `could not resolve agents.md path for appId=${opts.appId}`
5009
- };
5010
- const appSecret = resolveAppSecret(opts.appId, config, opts.feishuAppSecret);
5011
- if (!appSecret) return {
5012
- ok: false,
5013
- error: `could not resolve appSecret for appId=${opts.appId}`
5014
- };
5015
- console.error(`lark-cli-init: running lark-cli config init --name ${opts.appId} --app-id ${opts.appId} --brand feishu --app-secret-stdin --force-init`);
5016
- const initRes = (0, node_child_process.spawnSync)("lark-cli", [
5017
- "config",
5018
- "init",
5019
- "--name",
5020
- opts.appId,
5021
- "--app-id",
5022
- opts.appId,
5023
- "--brand",
5024
- "feishu",
5025
- "--app-secret-stdin",
5026
- "--force-init"
5027
- ], {
5028
- stdio: [
5029
- "pipe",
5030
- "pipe",
5031
- "pipe"
5032
- ],
5033
- encoding: "utf-8",
5034
- input: appSecret
4961
+ const tmpPath = configPath + ".installs-tmp";
4962
+ node_fs.default.writeFileSync(tmpPath, JSON.stringify(config, null, 2), "utf-8");
4963
+ moveSafe(tmpPath, configPath);
4964
+ console.error(`[install-extension] plugins.installs updated: ${updated} entry(ies) in ${configPath}` + (skipped > 0 ? ` (${skipped} package(s) without installMetadata skipped)` : ""));
4965
+ }
4966
+ function installOne$1(pkg, tarball, homeBase) {
4967
+ const destDir = node_path.default.join(homeBase, pkg.installPath);
4968
+ const stagingDir = destDir + ".new";
4969
+ const oldDir = destDir + ".old";
4970
+ node_fs.default.mkdirSync(node_path.default.dirname(destDir), { recursive: true });
4971
+ if (node_fs.default.existsSync(stagingDir)) node_fs.default.rmSync(stagingDir, {
4972
+ recursive: true,
4973
+ force: true
4974
+ });
4975
+ node_fs.default.mkdirSync(stagingDir);
4976
+ try {
4977
+ extractTarballTolerant(tarball, stagingDir, { stripComponents: 1 });
4978
+ if (!node_fs.default.existsSync(node_path.default.join(stagingDir, "package.json"))) throw new Error(`extension tarball missing package.json: ${pkg.name}`);
4979
+ } catch (e) {
4980
+ try {
4981
+ node_fs.default.rmSync(stagingDir, {
4982
+ recursive: true,
4983
+ force: true
4984
+ });
4985
+ } catch {}
4986
+ throw e;
4987
+ }
4988
+ const hadOld = node_fs.default.existsSync(destDir);
4989
+ if (hadOld) moveSafe(destDir, oldDir);
4990
+ moveSafe(stagingDir, destDir);
4991
+ if (hadOld && node_fs.default.existsSync(oldDir)) node_fs.default.rmSync(oldDir, {
4992
+ recursive: true,
4993
+ force: true
5035
4994
  });
5036
- const configInitStdout = initRes.stdout?.trim() || void 0;
5037
- const configInitStderr = initRes.stderr?.trim() || void 0;
5038
- if (configInitStdout) console.error(`lark-cli config init stdout: ${configInitStdout}`);
5039
- if (configInitStderr) console.error(`lark-cli config init stderr: ${configInitStderr}`);
5040
- if (initRes.error) return {
5041
- ok: false,
5042
- configInitStdout,
5043
- configInitStderr,
5044
- error: `lark-cli config init spawn error: ${initRes.error.message}`
5045
- };
5046
- if (initRes.status !== 0) return {
5047
- ok: false,
5048
- configInitExitCode: initRes.status ?? void 0,
5049
- configInitStdout,
5050
- configInitStderr,
5051
- error: `lark-cli config init exited with code ${initRes.status}`
5052
- };
5053
- appendPeToAgentsMd(agentsMdPath);
5054
- return {
5055
- ok: true,
5056
- configInitExitCode: 0,
5057
- agentsMdPath
5058
- };
5059
4995
  }
5060
4996
  //#endregion
5061
4997
  //#region ../../openclaw-slardar/lib/client.js
@@ -6901,60 +6837,6 @@ function mergeCoreBackupAndOrigins(configPath, vars, resetData, log) {
6901
6837
  log(`allowedOrigins: added ${added.length} (${JSON.stringify(added)}), total now ${mergedOrigins.length}`);
6902
6838
  }
6903
6839
  /**
6904
- * Fix bot account allowFrom and appSecret using larkApps from innerApi.
6905
- *
6906
- * For each bot account (key starts with `bot-cli_`):
6907
- * - allowFrom must contain the bot's own creatorOpenID from larkApps
6908
- * - appSecret must be either the canonical provider-ref or match larkApps plaintext
6909
- *
6910
- * Runs after mergeCoreBackupAndOrigins so it operates on the final config state.
6911
- */
6912
- function fixBotChannelConfig(configPath, larkApps, log) {
6913
- if (!larkApps || larkApps.length === 0) {
6914
- log("no larkApps data, skip bot channel config fix");
6915
- return;
6916
- }
6917
- const config = loadJSON5().parse(node_fs.default.readFileSync(configPath, "utf-8"));
6918
- const accounts = asRecord(getNestedMap(config, "channels", "feishu")?.accounts);
6919
- if (!accounts) {
6920
- log("no feishu accounts in config, skip bot channel config fix");
6921
- return;
6922
- }
6923
- let fixCount = 0;
6924
- for (const [, account] of Object.entries(accounts)) {
6925
- const bot = asRecord(account);
6926
- if (!bot) continue;
6927
- const appId = bot.appId;
6928
- if (typeof appId !== "string" || !appId.startsWith("cli_")) continue;
6929
- const larkApp = larkApps.find((e) => e.larkAppID === appId);
6930
- if (!larkApp) continue;
6931
- const creatorOpenID = larkApp.creatorOpenID;
6932
- if (typeof creatorOpenID === "string" && creatorOpenID !== "") {
6933
- const allowFrom = Array.isArray(bot.allowFrom) ? [...bot.allowFrom] : [];
6934
- if (!allowFrom.includes(creatorOpenID)) {
6935
- allowFrom.push(creatorOpenID);
6936
- bot.allowFrom = allowFrom;
6937
- fixCount++;
6938
- }
6939
- }
6940
- const secret = bot.appSecret;
6941
- let needsFix = false;
6942
- if (typeof secret === "object" && secret !== null && !Array.isArray(secret)) {
6943
- if (!matchMap(secret, DEFAULT_FEISHU_APP_SECRET)) needsFix = true;
6944
- } else if (typeof secret === "string") {
6945
- if (secret !== larkApp.appSecret) needsFix = true;
6946
- } else needsFix = true;
6947
- if (needsFix) {
6948
- bot.appSecret = { ...DEFAULT_FEISHU_APP_SECRET };
6949
- fixCount++;
6950
- }
6951
- }
6952
- if (fixCount > 0) {
6953
- node_fs.default.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
6954
- log(`fixed ${fixCount} bot channel config issue(s) (allowFrom/appSecret)`);
6955
- } else log("bot channel config ok, no fixes needed");
6956
- }
6957
- /**
6958
6840
  * Step 7: Verify startup scripts landed in configDir/scripts/.
6959
6841
  *
6960
6842
  * Scripts are extracted directly to configDir/scripts/ during stageTemplate —
@@ -7099,7 +6981,6 @@ async function runReset(input, taskId, resultFile) {
7099
6981
  await step5InstallOpenclaw(openclawTag, ossFileMap, log);
7100
6982
  step(6);
7101
6983
  mergeCoreBackupAndOrigins(configPath, vars, resetData, log);
7102
- fixBotChannelConfig(configPath, vars.larkApps, log);
7103
6984
  step(7);
7104
6985
  verifyStartupScripts(configDir, log);
7105
6986
  step(8);
@@ -7898,8 +7779,7 @@ function normalizeCtx(raw) {
7898
7779
  reset: {
7899
7780
  templateVars: r.reset.templateVars ?? {},
7900
7781
  coreBackup: r.reset.coreBackup
7901
- },
7902
- larkApps: Array.isArray(r.larkApps) ? r.larkApps : []
7782
+ }
7903
7783
  };
7904
7784
  }
7905
7785
  const vars = r.vars ?? {};
@@ -7924,8 +7804,7 @@ function normalizeCtx(raw) {
7924
7804
  reset: {
7925
7805
  templateVars: resetData.templateVars ?? {},
7926
7806
  coreBackup: resetData.coreBackup
7927
- },
7928
- larkApps: Array.isArray(r.larkApps) ? r.larkApps : []
7807
+ }
7929
7808
  };
7930
7809
  }
7931
7810
  function fillApp(src) {
@@ -7990,8 +7869,7 @@ function buildCheckInput(raw, configPathOverride) {
7990
7869
  providerFilePath: PROVIDER_FILE_PATH,
7991
7870
  secretsFilePath: SECRETS_FILE_PATH,
7992
7871
  templateVars: ctx.app.templateVars,
7993
- recommendedOpenclawTag: ctx.app.recommendedOpenclawTag,
7994
- larkApps: ctx.larkApps
7872
+ recommendedOpenclawTag: ctx.app.recommendedOpenclawTag
7995
7873
  },
7996
7874
  templateVars: ctx.app.templateVars
7997
7875
  };
@@ -8023,8 +7901,7 @@ function buildRepairInput(raw, configPathOverride) {
8023
7901
  providerFilePath: PROVIDER_FILE_PATH,
8024
7902
  secretsFilePath: SECRETS_FILE_PATH,
8025
7903
  templateVars: ctx.app.templateVars,
8026
- recommendedOpenclawTag: ctx.app.recommendedOpenclawTag,
8027
- larkApps: ctx.larkApps
7904
+ recommendedOpenclawTag: ctx.app.recommendedOpenclawTag
8028
7905
  },
8029
7906
  repairData: {
8030
7907
  secretsContent: ctx.secrets.secretsContent,
@@ -8060,8 +7937,7 @@ function buildResetInput(raw, configPathOverride) {
8060
7937
  providerFilePath: PROVIDER_FILE_PATH,
8061
7938
  secretsFilePath: SECRETS_FILE_PATH,
8062
7939
  templateVars: ctx.app.templateVars,
8063
- recommendedOpenclawTag: ctx.app.recommendedOpenclawTag,
8064
- larkApps: ctx.larkApps
7940
+ recommendedOpenclawTag: ctx.app.recommendedOpenclawTag
8065
7941
  },
8066
7942
  resetData: {
8067
7943
  templateVars: ctx.reset.templateVars,
@@ -10371,7 +10247,7 @@ async function reportCliRun(opts) {
10371
10247
  //#region src/help.ts
10372
10248
  const BIN = "mclaw-diagnose";
10373
10249
  function versionBanner() {
10374
- return `v0.1.14-alpha.11`;
10250
+ return `v0.1.14-alpha.13`;
10375
10251
  }
10376
10252
  const COMMANDS = [
10377
10253
  {
@@ -10475,12 +10351,16 @@ EXIT CODES
10475
10351
  hidden: true,
10476
10352
  summary: "Run rule-engine check only",
10477
10353
  help: `USAGE
10478
- ${BIN} check
10354
+ ${BIN} check [--ctx=<base64>]
10479
10355
 
10480
10356
  DESCRIPTION
10481
10357
  Runs the rule engine against the sandbox's current openclaw config and
10482
- returns { failedRules }. Ctx is fetched from innerapi automatically.
10483
- End-users should prefer \`doctor\`.
10358
+ returns { failedRules }. Used by sandbox_console's push-style callers
10359
+ that already own the ctx — end-users should prefer \`doctor\`.
10360
+
10361
+ OPTIONS
10362
+ --ctx=<base64> Opaque ctx JSON (base64). When absent, fetched from
10363
+ innerapi (same path as doctor).
10484
10364
  `
10485
10365
  },
10486
10366
  {
@@ -10488,11 +10368,16 @@ DESCRIPTION
10488
10368
  hidden: true,
10489
10369
  summary: "Apply standard-mode repairs",
10490
10370
  help: `USAGE
10491
- ${BIN} repair
10371
+ ${BIN} repair [--ctx=<base64>]
10492
10372
 
10493
10373
  DESCRIPTION
10494
- Runs repair for the failing rules. Ctx is fetched from innerapi
10495
- automatically. End-users should use \`doctor --fix\` instead.
10374
+ Runs repair for the failing rules listed inside the ctx's repairData.
10375
+ Intended for sandbox_console's push path — end-users should use
10376
+ \`doctor --fix\` instead.
10377
+
10378
+ OPTIONS
10379
+ --ctx=<base64> Opaque ctx JSON (base64). When absent, fetched from
10380
+ innerapi.
10496
10381
  `
10497
10382
  },
10498
10383
  {
@@ -10500,15 +10385,14 @@ DESCRIPTION
10500
10385
  hidden: true,
10501
10386
  summary: "Re-initialize sandbox via the 9-step reset pipeline",
10502
10387
  help: `USAGE
10503
- ${BIN} reset --async
10504
- ${BIN} reset --worker --task-id=<id>
10388
+ ${BIN} reset --async [--ctx=<base64>]
10389
+ ${BIN} reset --worker --task-id=<id> [--ctx=<base64>]
10505
10390
 
10506
10391
  DESCRIPTION
10507
10392
  Two-phase pipeline driven asynchronously: the --async invocation spawns
10508
10393
  a detached worker and returns { taskId } immediately; the --worker
10509
10394
  invocation (spawned by --async) runs the actual 9 steps and writes
10510
10395
  progress to /tmp/openclaw-diagnose/reset-<taskId>.json.
10511
- Ctx is fetched from innerapi automatically.
10512
10396
 
10513
10397
  Poll progress with \`${BIN} get_reset_task --task-id=<id>\`.
10514
10398
 
@@ -10516,6 +10400,7 @@ OPTIONS
10516
10400
  --async Start a detached worker and return taskId on stdout.
10517
10401
  --worker Internal — run the 9-step pipeline (launched by --async).
10518
10402
  --task-id=<id> Required with --worker; identifies the progress file.
10403
+ --ctx=<base64> Opaque ctx JSON; fetched from innerapi when absent.
10519
10404
  `
10520
10405
  },
10521
10406
  {
@@ -10538,7 +10423,7 @@ OPTIONS
10538
10423
  hidden: true,
10539
10424
  summary: "Download + install the openclaw tarball",
10540
10425
  help: `USAGE
10541
- ${BIN} install-openclaw <tag> [--oss_file_map=<base64>]
10426
+ ${BIN} install-openclaw <tag> [--ctx=<base64> | --oss_file_map=<base64>]
10542
10427
 
10543
10428
  DESCRIPTION
10544
10429
  Downloads the openclaw@<tag> tgz via the signed OSS URL found in the
@@ -10550,9 +10435,9 @@ ARGUMENTS
10550
10435
  <tag> Openclaw version tag, e.g. 2026.4.11.
10551
10436
 
10552
10437
  OPTIONS
10438
+ --ctx=<base64> Opaque ctx; ossFileMap is extracted from it.
10553
10439
  --oss_file_map=... Pre-built OSS URL map (base64 JSON); skips innerapi
10554
- entirely. When absent, ossFileMap is fetched from
10555
- innerapi automatically.
10440
+ entirely. Wins over --ctx when both provided.
10556
10441
  `
10557
10442
  },
10558
10443
  {
@@ -10578,7 +10463,8 @@ OPTIONS
10578
10463
  --home_base=<dir> Override the /home/gem base (tests).
10579
10464
  --config_path=<p> Override the openclaw.json path (tests).
10580
10465
  --skip-config-update Leave plugins.installs in openclaw.json untouched.
10581
- --oss_file_map=... Pre-built OSS URL map (base64 JSON); skips innerapi.
10466
+ --ctx=<base64> Opaque ctx; see install-openclaw for semantics.
10467
+ --oss_file_map=... Pre-built OSS URL map (base64 JSON).
10582
10468
  `
10583
10469
  },
10584
10470
  {
@@ -10605,6 +10491,7 @@ OPTIONS
10605
10491
  --cli=<name> CLI package to install by short name or scoped
10606
10492
  packageName (repeatable, at least one required).
10607
10493
  --home_base=<dir> Override the /home/gem base (tests).
10494
+ --ctx=<base64> Opaque ctx; ossFileMap is extracted from it.
10608
10495
  --oss_file_map=... Pre-built OSS URL map (base64 JSON); skips innerapi.
10609
10496
 
10610
10497
  EXAMPLES
@@ -10711,7 +10598,8 @@ OPTIONS
10711
10598
  --role=<role> Package role (e.g. template, config).
10712
10599
  --name=<name> Package name within the role.
10713
10600
  --dir=<dir> Target dir (defaults to dirname(pkg.installPath)).
10714
- --oss_file_map=... Pre-built OSS URL map (base64 JSON); skips innerapi.
10601
+ --ctx=<base64> Opaque ctx; ossFileMap is extracted from it.
10602
+ --oss_file_map=... Pre-built OSS URL map (base64 JSON).
10715
10603
  `
10716
10604
  }
10717
10605
  ];
@@ -10787,31 +10675,31 @@ function planVarsFields(opts = {}) {
10787
10675
  *
10788
10676
  * Per-command group needs:
10789
10677
  *
10790
- * doctor / check app + larkApps
10791
- * repair app + secrets + larkApps
10792
- * reset app + secrets + install + reset + larkApps
10678
+ * doctor / check app (rule-driven)
10679
+ * repair app + secrets (writes secretsContent / providerKeyContent)
10680
+ * reset app + secrets + install + reset (the works)
10793
10681
  * install-* install only
10794
10682
  *
10795
10683
  * Empty result (`{}`) means "no group needed" — the CLI can skip the
10796
10684
  * `fetchCtxViaInnerApi` call entirely and run with a synthetic empty ctx.
10685
+ * Happens e.g. when the user pinned `--rule=<key>` to a vars-free rule on
10686
+ * `doctor`.
10797
10687
  */
10798
10688
  function planCtxPopulate(opts) {
10799
10689
  if (opts.command === "install") return { install: true };
10800
10690
  const populate = {};
10801
- if (planVarsFields({
10691
+ const appFields = planVarsFields({
10802
10692
  disabled: opts.disabled,
10803
10693
  onlyRules: opts.onlyRules,
10804
10694
  profile: opts.profile
10805
- }).length > 0) populate.app = true;
10806
- if (opts.command === "repair") {
10807
- populate.secrets = true;
10808
- populate.larkApps = true;
10809
- } else if (opts.command === "reset") {
10695
+ });
10696
+ if (appFields.length > 0) populate.app = appFields;
10697
+ if (opts.command === "repair") populate.secrets = true;
10698
+ else if (opts.command === "reset") {
10810
10699
  populate.secrets = true;
10811
10700
  populate.install = true;
10812
10701
  populate.reset = true;
10813
- populate.larkApps = true;
10814
- } else if (opts.command === "doctor" || opts.command === "check") populate.larkApps = true;
10702
+ }
10815
10703
  return populate;
10816
10704
  }
10817
10705
  //#endregion
@@ -10870,6 +10758,21 @@ function reportDoctorRunToSlardar(opts) {
10870
10758
  const args = node_process.default.argv.slice(2);
10871
10759
  const mode = args.find((a) => !a.startsWith("-"));
10872
10760
  /**
10761
+ * Decode `--ctx=<base64>` into an opaque JSON object. Returns undefined when
10762
+ * the flag isn't present — the caller decides whether to fall back to the
10763
+ * innerapi or to error out.
10764
+ *
10765
+ * The object's shape is not enforced here; downstream code consumes it via
10766
+ * either `normalizeCtx()` (new path) or direct field access for the legacy
10767
+ * check/repair/reset contract still used by sandbox_console push.
10768
+ */
10769
+ function parseCtxFlag(args) {
10770
+ const ctxArg = args.find((a) => a.startsWith("--ctx="));
10771
+ if (!ctxArg) return void 0;
10772
+ const b64 = ctxArg.slice(6);
10773
+ return JSON.parse(Buffer.from(b64, "base64").toString("utf-8"));
10774
+ }
10775
+ /**
10873
10776
  * Pull the first non-flag positional after the mode name.
10874
10777
  * (The mode itself is args[0] in the filtered set, so we skip index 0.)
10875
10778
  */
@@ -10897,8 +10800,8 @@ function getMultiFlag(args, name) {
10897
10800
  * case but is no longer consulted.
10898
10801
  */
10899
10802
  async function reportRun(command, rc, _raw, invocation, durationMs, outcome, slardar = {
10900
- scene: void 0,
10901
- profile: "standard",
10803
+ scene,
10804
+ profile,
10902
10805
  fix: false
10903
10806
  }) {
10904
10807
  console.error(`${command}: telemetry calling report_cli_run`);
@@ -10962,7 +10865,7 @@ async function main() {
10962
10865
  console.error(`${mode}: begin argv=[${args.join(" ")}] version=${getVersion()} traceId=${traceId ?? "-"} caller=${caller ?? "-"} runIdGenerated=${rc.generated}`);
10963
10866
  switch (mode) {
10964
10867
  case "check": {
10965
- const raw = await fetchCtxViaInnerApi({
10868
+ const raw = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
10966
10869
  populate: planCtxPopulate({
10967
10870
  command: "check",
10968
10871
  profile
@@ -10987,7 +10890,7 @@ async function main() {
10987
10890
  break;
10988
10891
  }
10989
10892
  case "repair": {
10990
- const raw = await fetchCtxViaInnerApi({
10893
+ const raw = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
10991
10894
  populate: planCtxPopulate({
10992
10895
  command: "repair",
10993
10896
  profile
@@ -11058,15 +10961,27 @@ async function main() {
11058
10961
  break;
11059
10962
  }
11060
10963
  case "reset":
11061
- if (args.includes("--async")) console.log(JSON.stringify(startAsyncReset()));
11062
- else if (args.includes("--worker")) {
10964
+ if (args.includes("--async")) {
10965
+ const ctxArg = args.find((a) => a.startsWith("--ctx="));
10966
+ let ctxBase64;
10967
+ if (ctxArg) ctxBase64 = ctxArg.slice(6);
10968
+ else {
10969
+ const fetched = await fetchCtxViaInnerApi({
10970
+ populate: planCtxPopulate({ command: "reset" }),
10971
+ caller,
10972
+ traceId
10973
+ });
10974
+ ctxBase64 = Buffer.from(JSON.stringify(fetched), "utf-8").toString("base64");
10975
+ }
10976
+ console.log(JSON.stringify(startAsyncReset(ctxBase64)));
10977
+ } else if (args.includes("--worker")) {
11063
10978
  const taskId = args.find((a) => a.startsWith("--task-id="))?.slice(10);
11064
10979
  if (!taskId) {
11065
10980
  console.error("Error: --task-id=<id> is required for worker");
11066
10981
  node_process.default.exit(1);
11067
10982
  }
11068
10983
  const resultFile = resetResultFile(taskId);
11069
- const raw = await fetchCtxViaInnerApi({
10984
+ const raw = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
11070
10985
  populate: planCtxPopulate({ command: "reset" }),
11071
10986
  caller,
11072
10987
  traceId
@@ -11090,7 +11005,7 @@ async function main() {
11090
11005
  return;
11091
11006
  }
11092
11007
  } else {
11093
- console.error("Usage: reset --async | reset --worker --task-id=<id>");
11008
+ console.error("Usage: reset --async [--ctx=<base64>] | reset --worker --task-id=<id> [--ctx=<base64>]");
11094
11009
  node_process.default.exit(1);
11095
11010
  }
11096
11011
  break;
@@ -11106,14 +11021,14 @@ async function main() {
11106
11021
  case "install-openclaw": {
11107
11022
  const tag = getPositionalTag(args, "install-openclaw");
11108
11023
  if (!tag) {
11109
- console.error("Usage: install-openclaw <tag> [--oss_file_map=<base64>]");
11024
+ console.error("Usage: install-openclaw <tag> [--ctx=<base64> | --oss_file_map=<base64>]");
11110
11025
  node_process.default.exit(1);
11111
11026
  }
11112
11027
  const ossFileMapFlag = getFlag(args, "oss_file_map");
11113
11028
  let installOssFileMap;
11114
11029
  let rawForTelemetry;
11115
11030
  if (!ossFileMapFlag) {
11116
- rawForTelemetry = await fetchCtxViaInnerApi({
11031
+ rawForTelemetry = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
11117
11032
  populate: planCtxPopulate({ command: "install" }),
11118
11033
  caller,
11119
11034
  traceId
@@ -11148,7 +11063,7 @@ async function main() {
11148
11063
  case "install-extension": {
11149
11064
  const tag = getPositionalTag(args, "install-extension");
11150
11065
  if (!tag) {
11151
- console.error("Usage: install-extension <tag> (--all | --extension=<name>...) [--home_base=<dir>] [--config_path=<path>] [--skip-config-update] [--oss_file_map=<base64>]");
11066
+ console.error("Usage: install-extension <tag> (--all | --extension=<name>...) [--home_base=<dir>] [--config_path=<path>] [--skip-config-update] [--ctx=<base64> | --oss_file_map=<base64>]");
11152
11067
  node_process.default.exit(1);
11153
11068
  }
11154
11069
  const all = args.includes("--all");
@@ -11160,7 +11075,7 @@ async function main() {
11160
11075
  let installOssFileMap;
11161
11076
  let rawForTelemetry;
11162
11077
  if (!ossFileMapFlag) {
11163
- rawForTelemetry = await fetchCtxViaInnerApi({
11078
+ rawForTelemetry = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
11164
11079
  populate: planCtxPopulate({ command: "install" }),
11165
11080
  caller,
11166
11081
  traceId
@@ -11206,12 +11121,12 @@ async function main() {
11206
11121
  case "install-cli": {
11207
11122
  const tag = getPositionalTag(args, "install-cli");
11208
11123
  if (!tag) {
11209
- console.error("Usage: install-cli <tag> --cli=<name>... [--home_base=<dir>] [--oss_file_map=<base64>]");
11124
+ console.error("Usage: install-cli <tag> --cli=<name>... [--home_base=<dir>] [--ctx=<base64> | --oss_file_map=<base64>]");
11210
11125
  node_process.default.exit(1);
11211
11126
  }
11212
11127
  const names = getMultiFlag(args, "cli");
11213
11128
  if (names.length === 0) {
11214
- console.error("Usage: install-cli <tag> --cli=<name>... [--home_base=<dir>] [--oss_file_map=<base64>]");
11129
+ console.error("Usage: install-cli <tag> --cli=<name>... [--home_base=<dir>] [--ctx=<base64> | --oss_file_map=<base64>]");
11215
11130
  node_process.default.exit(1);
11216
11131
  }
11217
11132
  const homeBase = getFlag(args, "home_base");
@@ -11219,7 +11134,7 @@ async function main() {
11219
11134
  let installOssFileMap;
11220
11135
  let rawForTelemetry;
11221
11136
  if (!ossFileMapFlag) {
11222
- rawForTelemetry = await fetchCtxViaInnerApi({
11137
+ rawForTelemetry = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
11223
11138
  populate: planCtxPopulate({ command: "install" }),
11224
11139
  caller,
11225
11140
  traceId
@@ -11267,7 +11182,7 @@ async function main() {
11267
11182
  case "download-resource": {
11268
11183
  const tag = getPositionalTag(args, "download-resource");
11269
11184
  if (!tag) {
11270
- console.error("Usage: download-resource <tag> --role=<role> --name=<name> [--dir=<dir>] [--oss_file_map=<base64>]");
11185
+ console.error("Usage: download-resource <tag> --role=<role> --name=<name> [--dir=<dir>] [--ctx=<base64> | --oss_file_map=<base64>]");
11271
11186
  node_process.default.exit(1);
11272
11187
  }
11273
11188
  const role = getFlag(args, "role");
@@ -11281,7 +11196,7 @@ async function main() {
11281
11196
  let installOssFileMap;
11282
11197
  let rawForTelemetry;
11283
11198
  if (!ossFileMapFlag) {
11284
- rawForTelemetry = await fetchCtxViaInnerApi({
11199
+ rawForTelemetry = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
11285
11200
  populate: planCtxPopulate({ command: "install" }),
11286
11201
  caller,
11287
11202
  traceId