@lark-apaas/openclaw-scripts-diagnose-cli 0.1.6 → 0.1.7-alpha.1

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 +533 -334
  2. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -50,7 +50,7 @@ node_assert = __toESM(node_assert);
50
50
  * it terse and parseable.
51
51
  */
52
52
  function getVersion() {
53
- return "0.1.6";
53
+ return "0.1.7-alpha.1";
54
54
  }
55
55
  //#endregion
56
56
  //#region src/rule-engine/base.ts
@@ -4134,7 +4134,7 @@ async function installExtension(tag, ossFileMap, opts = {}) {
4134
4134
  };
4135
4135
  }));
4136
4136
  for (const { pkg, tarball } of tarballs) {
4137
- installOne(pkg, tarball, homeBase);
4137
+ installOne$1(pkg, tarball, homeBase);
4138
4138
  console.error(`[install-extension] ${pkg.name}: installed`);
4139
4139
  }
4140
4140
  if (!opts.skipConfigUpdate) updatePluginInstalls(opts.configPath ?? node_path.default.join(homeBase, "workspace/agent/openclaw.json"), targets);
@@ -4187,7 +4187,7 @@ function updatePluginInstalls(configPath, installedPkgs) {
4187
4187
  moveSafe(tmpPath, configPath);
4188
4188
  console.error(`[install-extension] plugins.installs updated: ${updated} entry(ies) in ${configPath}` + (skipped > 0 ? ` (${skipped} package(s) without installMetadata skipped)` : ""));
4189
4189
  }
4190
- function installOne(pkg, tarball, homeBase) {
4190
+ function installOne$1(pkg, tarball, homeBase) {
4191
4191
  const destDir = node_path.default.join(homeBase, pkg.installPath);
4192
4192
  const stagingDir = destDir + ".new";
4193
4193
  const oldDir = destDir + ".old";
@@ -4729,6 +4729,356 @@ function sleepSync(ms) {
4729
4729
  Atomics.wait(arr, 0, 0, ms);
4730
4730
  }
4731
4731
  //#endregion
4732
+ //#region src/lark-cli-init.ts
4733
+ const LARK_PLUGIN_NAMES = ["openclaw-lark", "feishu-openclaw-plugin"];
4734
+ const PE_XML_TAG = "lark-cli-pe";
4735
+ const PE_PLACEHOLDER = `
4736
+ <${PE_XML_TAG}>
4737
+ **【强制要求 - 无例外】飞书所有操作必须先读 feishu-lark-cli skill。未读skill不得调用任何飞书工具,违者视为操作失误。**
4738
+ </${PE_XML_TAG}>
4739
+ `;
4740
+ function isLarkPluginInstalled(configPath) {
4741
+ const extDir = getExtensionsDir(configPath);
4742
+ return LARK_PLUGIN_NAMES.some((name) => {
4743
+ try {
4744
+ return node_fs.default.existsSync(node_path.default.join(extDir, name, "package.json"));
4745
+ } catch {
4746
+ return false;
4747
+ }
4748
+ });
4749
+ }
4750
+ function isLarkCliAvailable() {
4751
+ try {
4752
+ return (0, node_child_process.spawnSync)("lark-cli", ["--version"], {
4753
+ encoding: "utf-8",
4754
+ timeout: 5e3,
4755
+ stdio: [
4756
+ "ignore",
4757
+ "pipe",
4758
+ "ignore"
4759
+ ]
4760
+ }).status === 0;
4761
+ } catch {
4762
+ return false;
4763
+ }
4764
+ }
4765
+ function readConfig(configPath) {
4766
+ try {
4767
+ const raw = node_fs.default.readFileSync(configPath, "utf-8");
4768
+ const parsed = loadJSON5().parse(raw);
4769
+ return parsed != null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
4770
+ } catch {
4771
+ return null;
4772
+ }
4773
+ }
4774
+ /**
4775
+ * Resolve the feishu app secret for the given appId.
4776
+ *
4777
+ * Lookup order:
4778
+ * 1. channels.feishu.appSecret (single-agent: feishu.appId === appId)
4779
+ * 2. channels.feishu.accounts[key].appSecret (multi-agent: account.appId === appId)
4780
+ *
4781
+ * Value interpretation:
4782
+ * - string → use directly
4783
+ * - object → secret is managed by a provider; use `feishuAppSecret` param instead
4784
+ *
4785
+ * Returns null when the secret cannot be determined.
4786
+ */
4787
+ function resolveAppSecret(appId, config, feishuAppSecret) {
4788
+ const feishu = getNestedMap(config, "channels", "feishu");
4789
+ if (!feishu) return null;
4790
+ let rawSecret;
4791
+ if (typeof feishu.appId === "string" && feishu.appId === appId) rawSecret = feishu.appSecret;
4792
+ else {
4793
+ const accounts = asRecord(feishu.accounts);
4794
+ if (accounts) for (const [, val] of Object.entries(accounts)) {
4795
+ const account = asRecord(val);
4796
+ if (account?.appId === appId) {
4797
+ rawSecret = account.appSecret ?? feishu.appSecret;
4798
+ break;
4799
+ }
4800
+ }
4801
+ }
4802
+ if (typeof rawSecret === "string" && rawSecret) return rawSecret;
4803
+ if (rawSecret != null && typeof rawSecret === "object") return feishuAppSecret ?? null;
4804
+ return null;
4805
+ }
4806
+ /**
4807
+ * Resolve the agents.md path for the given appId from the openclaw config.
4808
+ *
4809
+ * Case 1: appId matches channels.feishu.appId (single-agent path)
4810
+ * → WORKSPACE_DIR/AGENTS.md
4811
+ *
4812
+ * Case 2: appId found in channels.feishu.accounts (multi-agent path)
4813
+ * → find account key where account.appId === appId
4814
+ * → find binding where match.channel=feishu && match.accountId=that key
4815
+ * → if agentId === 'main' → WORKSPACE_DIR/agents.md
4816
+ * → else find agent in agents.list by id → agent.workspace/agents.md
4817
+ *
4818
+ * Returns null when the path cannot be determined.
4819
+ */
4820
+ function resolveAgentsMdPath(appId, config) {
4821
+ const feishu = getNestedMap(config, "channels", "feishu");
4822
+ if (!feishu) {
4823
+ console.error("resolveAgentsMdPath: channels.feishu not found");
4824
+ return null;
4825
+ }
4826
+ if (typeof feishu.appId === "string" && feishu.appId === appId) {
4827
+ console.error(`resolveAgentsMdPath: case=single-agent feishu.appId=${feishu.appId}`);
4828
+ return node_path.default.join(WORKSPACE_DIR, "workspace", "AGENTS.md");
4829
+ }
4830
+ const accounts = asRecord(feishu.accounts);
4831
+ if (!accounts) {
4832
+ console.error("resolveAgentsMdPath: feishu.accounts not found");
4833
+ return null;
4834
+ }
4835
+ let accountId;
4836
+ for (const [key, val] of Object.entries(accounts)) if (asRecord(val)?.appId === appId) {
4837
+ accountId = key;
4838
+ break;
4839
+ }
4840
+ if (!accountId) {
4841
+ console.error(`resolveAgentsMdPath: no account found with appId=${appId} in feishu.accounts keys=[${Object.keys(accounts).join(",")}]`);
4842
+ return null;
4843
+ }
4844
+ console.error(`resolveAgentsMdPath: found accountId=${accountId}`);
4845
+ const bindings = Array.isArray(config.bindings) ? config.bindings : [];
4846
+ let agentId;
4847
+ for (const b of bindings) {
4848
+ const binding = asRecord(b);
4849
+ if (!binding) continue;
4850
+ const match = asRecord(binding.match);
4851
+ if (match?.channel === "feishu" && match?.accountId === accountId) {
4852
+ if (typeof binding.agentId === "string") {
4853
+ agentId = binding.agentId;
4854
+ break;
4855
+ }
4856
+ }
4857
+ }
4858
+ if (!agentId) {
4859
+ console.error(`resolveAgentsMdPath: no binding found for accountId=${accountId} in bindings(count=${bindings.length})`);
4860
+ return null;
4861
+ }
4862
+ console.error(`resolveAgentsMdPath: found agentId=${agentId}`);
4863
+ if (agentId === "main") {
4864
+ console.error("resolveAgentsMdPath: case=multi-agent-main");
4865
+ return node_path.default.join(WORKSPACE_DIR, "workspace", "AGENTS.md");
4866
+ }
4867
+ const agentsObj = asRecord(config.agents);
4868
+ const list = Array.isArray(agentsObj?.list) ? agentsObj.list : [];
4869
+ for (const a of list) {
4870
+ const agent = asRecord(a);
4871
+ if (agent?.id === agentId) {
4872
+ const ws = typeof agent.workspace === "string" ? agent.workspace : node_path.default.join(WORKSPACE_DIR, "workspace");
4873
+ console.error(`resolveAgentsMdPath: case=multi-agent-custom agentId=${agentId} workspace=${ws}`);
4874
+ return node_path.default.join(ws, "AGENTS.md");
4875
+ }
4876
+ }
4877
+ console.error(`resolveAgentsMdPath: agentId=${agentId} not found in agents.list(count=${list.length})`);
4878
+ return null;
4879
+ }
4880
+ function appendPeToAgentsMd(agentsMdPath) {
4881
+ const dir = node_path.default.dirname(agentsMdPath);
4882
+ if (!node_fs.default.existsSync(dir)) node_fs.default.mkdirSync(dir, { recursive: true });
4883
+ const existing = node_fs.default.existsSync(agentsMdPath) ? node_fs.default.readFileSync(agentsMdPath, "utf-8") : "";
4884
+ if (existing.includes(`<${PE_XML_TAG}>`)) {
4885
+ console.error(`lark-cli-init: <${PE_XML_TAG}> already present in ${agentsMdPath}, skipping`);
4886
+ return;
4887
+ }
4888
+ const sep = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
4889
+ node_fs.default.appendFileSync(agentsMdPath, `${sep}${PE_PLACEHOLDER}`, "utf-8");
4890
+ console.error(`lark-cli-init: appended PE placeholder to ${agentsMdPath}`);
4891
+ }
4892
+ /**
4893
+ * Collect every Feishu bot appId declared in the openclaw config.
4894
+ * Covers both single-agent (channels.feishu.appId) and multi-agent
4895
+ * (channels.feishu.accounts[*].appId) layouts. Returns a deduplicated list.
4896
+ */
4897
+ function collectFeishuAppIds(configPath) {
4898
+ const config = readConfig(configPath ?? CONFIG_PATH);
4899
+ if (!config) return [];
4900
+ const feishu = getNestedMap(config, "channels", "feishu");
4901
+ if (!feishu) return [];
4902
+ const appIds = /* @__PURE__ */ new Set();
4903
+ if (typeof feishu.appId === "string" && feishu.appId) appIds.add(feishu.appId);
4904
+ const accounts = asRecord(feishu.accounts);
4905
+ if (accounts) for (const val of Object.values(accounts)) {
4906
+ const account = asRecord(val);
4907
+ if (typeof account?.appId === "string" && account.appId) appIds.add(account.appId);
4908
+ }
4909
+ return [...appIds];
4910
+ }
4911
+ function runLarkCliInit(opts) {
4912
+ const configPath = opts.configPath ?? CONFIG_PATH;
4913
+ if (!isLarkPluginInstalled(configPath)) {
4914
+ console.error("lark-cli-init: skipping — openclaw-lark plugin not installed");
4915
+ return {
4916
+ ok: true,
4917
+ skipped: true,
4918
+ skipReason: "openclaw-lark plugin not installed"
4919
+ };
4920
+ }
4921
+ if (!isLarkCliAvailable()) {
4922
+ console.error("lark-cli-init: skipping — lark-cli command not found");
4923
+ return {
4924
+ ok: true,
4925
+ skipped: true,
4926
+ skipReason: "lark-cli command not found"
4927
+ };
4928
+ }
4929
+ const config = readConfig(configPath);
4930
+ if (!config) return {
4931
+ ok: false,
4932
+ error: `could not read config at ${configPath}`
4933
+ };
4934
+ const agentsMdPath = resolveAgentsMdPath(opts.appId, config);
4935
+ console.error(`lark-cli-init: resolved agents.md path=${agentsMdPath ?? "(null)"}`);
4936
+ if (!agentsMdPath) return {
4937
+ ok: false,
4938
+ error: `could not resolve agents.md path for appId=${opts.appId}`
4939
+ };
4940
+ const appSecret = resolveAppSecret(opts.appId, config, opts.feishuAppSecret);
4941
+ if (!appSecret) return {
4942
+ ok: false,
4943
+ error: `could not resolve appSecret for appId=${opts.appId}`
4944
+ };
4945
+ console.error(`lark-cli-init: running lark-cli config init --name ${opts.appId} --app-id ${opts.appId} --brand feishu --app-secret-stdin --force-init`);
4946
+ const initRes = (0, node_child_process.spawnSync)("lark-cli", [
4947
+ "config",
4948
+ "init",
4949
+ "--name",
4950
+ opts.appId,
4951
+ "--app-id",
4952
+ opts.appId,
4953
+ "--brand",
4954
+ "feishu",
4955
+ "--app-secret-stdin",
4956
+ "--force-init"
4957
+ ], {
4958
+ stdio: [
4959
+ "pipe",
4960
+ "pipe",
4961
+ "pipe"
4962
+ ],
4963
+ encoding: "utf-8",
4964
+ input: appSecret
4965
+ });
4966
+ const configInitStdout = initRes.stdout?.trim() || void 0;
4967
+ const configInitStderr = initRes.stderr?.trim() || void 0;
4968
+ if (configInitStdout) console.error(`lark-cli config init stdout: ${configInitStdout}`);
4969
+ if (configInitStderr) console.error(`lark-cli config init stderr: ${configInitStderr}`);
4970
+ if (initRes.error) return {
4971
+ ok: false,
4972
+ configInitStdout,
4973
+ configInitStderr,
4974
+ error: `lark-cli config init spawn error: ${initRes.error.message}`
4975
+ };
4976
+ if (initRes.status !== 0) return {
4977
+ ok: false,
4978
+ configInitExitCode: initRes.status ?? void 0,
4979
+ configInitStdout,
4980
+ configInitStderr,
4981
+ error: `lark-cli config init exited with code ${initRes.status}`
4982
+ };
4983
+ appendPeToAgentsMd(agentsMdPath);
4984
+ return {
4985
+ ok: true,
4986
+ configInitExitCode: 0,
4987
+ agentsMdPath
4988
+ };
4989
+ }
4990
+ const LARK_CLI_NAME = "lark-cli";
4991
+ async function installClis(tag, ossFileMap, opts) {
4992
+ const homeBase = opts.homeBase ?? "/home/gem";
4993
+ if (opts.names.length === 0) throw new Error("install-clis: must provide at least one --cli=<name>");
4994
+ const manifest = await fetchManifest(ossFileMap, tag);
4995
+ console.error(`[install-clis] manifest=${JSON.stringify(manifest)}`);
4996
+ const allClis = manifest.packages.filter((p) => p.role === "cli" && p.name !== "openclaw");
4997
+ const wanted = new Set(opts.names);
4998
+ const targets = allClis.filter((p) => wanted.has(p.name) || p.packageName != null && wanted.has(p.packageName));
4999
+ const foundKeys = /* @__PURE__ */ new Set();
5000
+ for (const t of targets) {
5001
+ foundKeys.add(t.name);
5002
+ if (t.packageName) foundKeys.add(t.packageName);
5003
+ }
5004
+ const missing = opts.names.filter((n) => !foundKeys.has(n));
5005
+ if (missing.length > 0) throw new Error(`install-clis: not found in manifest: ${missing.join(", ")}`);
5006
+ console.error(`[install-clis] tag=${tag} targets=${targets.length}`);
5007
+ const t0 = Date.now();
5008
+ const tarballs = await Promise.all(targets.map(async (p) => {
5009
+ const tb = await downloadWithCache(p, ossFileMap, opts);
5010
+ console.error(`[install-clis] ${p.name}: downloaded`);
5011
+ return {
5012
+ pkg: p,
5013
+ tarball: tb
5014
+ };
5015
+ }));
5016
+ for (const { pkg, tarball } of tarballs) {
5017
+ installOne(pkg, tarball, homeBase, opts.tmpRoot);
5018
+ console.error(`[install-clis] ${pkg.name}: installed`);
5019
+ }
5020
+ if (targets.some((p) => p.name === LARK_CLI_NAME)) {
5021
+ const appIds = collectFeishuAppIds();
5022
+ console.error(`[install-clis] lark-cli installed — running lark-cli-init for ${appIds.length} appId(s): [${appIds.join(", ")}]`);
5023
+ for (const appId of appIds) {
5024
+ console.error(`[install-clis] lark-cli-init: appId=${appId}`);
5025
+ const result = runLarkCliInit({
5026
+ appId,
5027
+ feishuAppSecret: opts.feishuAppSecret
5028
+ });
5029
+ console.error(`[install-clis] lark-cli-init: appId=${appId} ok=${result.ok}` + (result.skipped ? ` skipped=true reason=${result.skipReason}` : "") + (result.error ? ` error=${result.error}` : ""));
5030
+ }
5031
+ }
5032
+ console.error(`[install-clis] done ${targets.length}/${targets.length} in ${Date.now() - t0}ms`);
5033
+ }
5034
+ function installOne(pkg, tarball, homeBase, tmpRoot) {
5035
+ const targetDir = node_path.default.join(homeBase, pkg.installPath);
5036
+ const bakDir = targetDir + ".bak";
5037
+ const newDir = targetDir + ".new";
5038
+ node_fs.default.mkdirSync(node_path.default.dirname(targetDir), { recursive: true });
5039
+ if (node_fs.default.existsSync(newDir)) node_fs.default.rmSync(newDir, {
5040
+ recursive: true,
5041
+ force: true
5042
+ });
5043
+ if (node_fs.default.existsSync(bakDir)) node_fs.default.rmSync(bakDir, {
5044
+ recursive: true,
5045
+ force: true
5046
+ });
5047
+ const tmpStage = node_fs.default.mkdtempSync(node_path.default.join(tmpRoot ?? node_os.default.tmpdir(), "cli-install-"));
5048
+ try {
5049
+ extractTarballTolerant(tarball, tmpStage, { stripComponents: 1 });
5050
+ if (!node_fs.default.existsSync(node_path.default.join(tmpStage, "package.json"))) throw new Error(`cli tarball missing package.json: ${pkg.name}`);
5051
+ moveSafe(tmpStage, newDir);
5052
+ const hadExisting = node_fs.default.existsSync(targetDir);
5053
+ try {
5054
+ if (hadExisting) moveSafe(targetDir, bakDir);
5055
+ moveSafe(newDir, targetDir);
5056
+ } catch (e) {
5057
+ if (hadExisting && !node_fs.default.existsSync(targetDir) && node_fs.default.existsSync(bakDir)) try {
5058
+ moveSafe(bakDir, targetDir);
5059
+ } catch {}
5060
+ try {
5061
+ node_fs.default.rmSync(newDir, {
5062
+ recursive: true,
5063
+ force: true
5064
+ });
5065
+ } catch {}
5066
+ throw e;
5067
+ }
5068
+ if (hadExisting && node_fs.default.existsSync(bakDir)) node_fs.default.rmSync(bakDir, {
5069
+ recursive: true,
5070
+ force: true
5071
+ });
5072
+ } finally {
5073
+ if (node_fs.default.existsSync(tmpStage)) try {
5074
+ node_fs.default.rmSync(tmpStage, {
5075
+ recursive: true,
5076
+ force: true
5077
+ });
5078
+ } catch {}
5079
+ }
5080
+ }
5081
+ //#endregion
4732
5082
  //#region src/oss/resolveOssFileMap.ts
4733
5083
  /**
4734
5084
  * Pick an OssFileMap in the order of decreasing specificity:
@@ -7731,342 +8081,102 @@ async function runDoctor(rawCtx, opts) {
7731
8081
  aborted = true;
7732
8082
  break outer;
7733
8083
  }
7734
- if (v2.pass) {
7735
- console.error(`rule ${key}: revalidate -> pass (fixed) durationMs=${Date.now() - tRevalidate}`);
7736
- results.push({
7737
- rule: key,
7738
- status: "fixed",
7739
- before: v1.message
7740
- });
7741
- } else {
7742
- console.error(`rule ${key}: revalidate -> still-broken durationMs=${Date.now() - tRevalidate} after=${v2.message ?? ""}`);
7743
- results.push({
7744
- rule: key,
7745
- status: "still-broken",
7746
- before: v1.message,
7747
- after: v2.message
7748
- });
7749
- failedKeys.add(key);
7750
- }
7751
- }
7752
- let backupPath = null;
7753
- if (configDirty && !aborted) {
7754
- backupPath = backupConfigSync(ctx.configPath);
7755
- const serialized = JSON.stringify(ctx.config, null, 2);
7756
- try {
7757
- writeFile(ctx.configPath, serialized);
7758
- console.error(`runDoctor: writeback ok path=${ctx.configPath} bytes=${serialized.length}`);
7759
- } catch (e) {
7760
- const msg = e.message;
7761
- console.error(`runDoctor: writeback failed path=${ctx.configPath} message=${msg}`);
7762
- results.push({
7763
- rule: "*config-writeback*",
7764
- status: "error",
7765
- message: "config write failed: " + msg
7766
- });
7767
- aborted = true;
7768
- }
7769
- } else if (configDirty && aborted) console.error("runDoctor: writeback skipped (aborted, leaving on-disk config untouched)");
7770
- else console.error("runDoctor: writeback skipped (no rule transitioned to fixed)");
7771
- console.error(`runDoctor: end aborted=${aborted} results=${results.length}`);
7772
- if (originalConfig !== null) {
7773
- const d = (0, import_lib.diffString)(originalConfig, ctx.config, { color: false });
7774
- console.error(`runDoctor: diff backupPath=${backupPath ?? "(none)"} changed=${!!d}`);
7775
- if (d) {
7776
- process.stdout.write(`original: ${backupPath ?? "(none)"}\n`);
7777
- process.stdout.write(`after fixed: ${ctx.configPath}\n`);
7778
- process.stdout.write("diff:\n");
7779
- process.stdout.write(d);
7780
- if (!d.endsWith("\n")) process.stdout.write("\n");
7781
- } else process.stdout.write("(no openclaw.json changes)\n");
7782
- }
7783
- return finalize(results, aborted);
7784
- }
7785
- /** Deep-clone a JSON-shaped value. Uses structuredClone where available
7786
- * (Node 17+); falls back to JSON round-trip otherwise. The config we
7787
- * clone here is JSON5-parsed so it's strictly tree-shaped — no
7788
- * references, no functions — making both paths equivalent. */
7789
- function deepClone(v) {
7790
- if (typeof structuredClone === "function") return structuredClone(v);
7791
- return JSON.parse(JSON.stringify(v));
7792
- }
7793
- function finalize(results, aborted) {
7794
- const summary = {
7795
- pass: 0,
7796
- failed: 0,
7797
- fixed: 0,
7798
- stillBroken: 0,
7799
- skipped: 0,
7800
- error: 0,
7801
- unknown: 0
7802
- };
7803
- for (const r of results) switch (r.status) {
7804
- case "pass":
7805
- summary.pass++;
7806
- break;
7807
- case "failed":
7808
- summary.failed++;
7809
- break;
7810
- case "fixed":
7811
- summary.fixed++;
7812
- break;
7813
- case "still-broken":
7814
- summary.stillBroken++;
7815
- break;
7816
- case "skipped":
7817
- summary.skipped++;
7818
- break;
7819
- case "error":
7820
- summary.error++;
7821
- break;
7822
- case "unknown":
7823
- summary.unknown++;
7824
- break;
7825
- }
7826
- return {
7827
- results,
7828
- summary,
7829
- aborted
7830
- };
7831
- }
7832
- //#endregion
7833
- //#region src/lark-cli-init.ts
7834
- const LARK_PLUGIN_NAMES = ["openclaw-lark", "feishu-openclaw-plugin"];
7835
- const PE_XML_TAG = "lark-cli-pe";
7836
- const PE_PLACEHOLDER = `
7837
- <${PE_XML_TAG}>
7838
- **【强制要求 - 无例外】飞书所有操作必须先读 feishu-lark-cli skill。未读skill不得调用任何飞书工具,违者视为操作失误。**
7839
- </${PE_XML_TAG}>
7840
- `;
7841
- function isLarkPluginInstalled(configPath) {
7842
- const extDir = getExtensionsDir(configPath);
7843
- return LARK_PLUGIN_NAMES.some((name) => {
7844
- try {
7845
- return node_fs.default.existsSync(node_path.default.join(extDir, name, "package.json"));
7846
- } catch {
7847
- return false;
7848
- }
7849
- });
7850
- }
7851
- function isLarkCliAvailable() {
7852
- try {
7853
- return (0, node_child_process.spawnSync)("lark-cli", ["--version"], {
7854
- encoding: "utf-8",
7855
- timeout: 5e3,
7856
- stdio: [
7857
- "ignore",
7858
- "pipe",
7859
- "ignore"
7860
- ]
7861
- }).status === 0;
7862
- } catch {
7863
- return false;
7864
- }
7865
- }
7866
- function readConfig(configPath) {
7867
- try {
7868
- const raw = node_fs.default.readFileSync(configPath, "utf-8");
7869
- const parsed = loadJSON5().parse(raw);
7870
- return parsed != null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
7871
- } catch {
7872
- return null;
7873
- }
7874
- }
7875
- /**
7876
- * Resolve the feishu app secret for the given appId.
7877
- *
7878
- * Lookup order:
7879
- * 1. channels.feishu.appSecret (single-agent: feishu.appId === appId)
7880
- * 2. channels.feishu.accounts[key].appSecret (multi-agent: account.appId === appId)
7881
- *
7882
- * Value interpretation:
7883
- * - string → use directly
7884
- * - object → secret is managed by a provider; use `feishuAppSecret` param instead
7885
- *
7886
- * Returns null when the secret cannot be determined.
7887
- */
7888
- function resolveAppSecret(appId, config, feishuAppSecret) {
7889
- const feishu = getNestedMap(config, "channels", "feishu");
7890
- if (!feishu) return null;
7891
- let rawSecret;
7892
- if (typeof feishu.appId === "string" && feishu.appId === appId) rawSecret = feishu.appSecret;
7893
- else {
7894
- const accounts = asRecord(feishu.accounts);
7895
- if (accounts) for (const [, val] of Object.entries(accounts)) {
7896
- const account = asRecord(val);
7897
- if (account?.appId === appId) {
7898
- rawSecret = account.appSecret ?? feishu.appSecret;
7899
- break;
7900
- }
7901
- }
7902
- }
7903
- if (typeof rawSecret === "string" && rawSecret) return rawSecret;
7904
- if (rawSecret != null && typeof rawSecret === "object") return feishuAppSecret ?? null;
7905
- return null;
7906
- }
7907
- /**
7908
- * Resolve the agents.md path for the given appId from the openclaw config.
7909
- *
7910
- * Case 1: appId matches channels.feishu.appId (single-agent path)
7911
- * → WORKSPACE_DIR/AGENTS.md
7912
- *
7913
- * Case 2: appId found in channels.feishu.accounts (multi-agent path)
7914
- * → find account key where account.appId === appId
7915
- * → find binding where match.channel=feishu && match.accountId=that key
7916
- * → if agentId === 'main' → WORKSPACE_DIR/agents.md
7917
- * → else find agent in agents.list by id → agent.workspace/agents.md
7918
- *
7919
- * Returns null when the path cannot be determined.
7920
- */
7921
- function resolveAgentsMdPath(appId, config) {
7922
- const feishu = getNestedMap(config, "channels", "feishu");
7923
- if (!feishu) {
7924
- console.error("resolveAgentsMdPath: channels.feishu not found");
7925
- return null;
7926
- }
7927
- if (typeof feishu.appId === "string" && feishu.appId === appId) {
7928
- console.error(`resolveAgentsMdPath: case=single-agent feishu.appId=${feishu.appId}`);
7929
- return node_path.default.join(WORKSPACE_DIR, "workspace", "AGENTS.md");
7930
- }
7931
- const accounts = asRecord(feishu.accounts);
7932
- if (!accounts) {
7933
- console.error("resolveAgentsMdPath: feishu.accounts not found");
7934
- return null;
7935
- }
7936
- let accountId;
7937
- for (const [key, val] of Object.entries(accounts)) if (asRecord(val)?.appId === appId) {
7938
- accountId = key;
7939
- break;
7940
- }
7941
- if (!accountId) {
7942
- console.error(`resolveAgentsMdPath: no account found with appId=${appId} in feishu.accounts keys=[${Object.keys(accounts).join(",")}]`);
7943
- return null;
7944
- }
7945
- console.error(`resolveAgentsMdPath: found accountId=${accountId}`);
7946
- const bindings = Array.isArray(config.bindings) ? config.bindings : [];
7947
- let agentId;
7948
- for (const b of bindings) {
7949
- const binding = asRecord(b);
7950
- if (!binding) continue;
7951
- const match = asRecord(binding.match);
7952
- if (match?.channel === "feishu" && match?.accountId === accountId) {
7953
- if (typeof binding.agentId === "string") {
7954
- agentId = binding.agentId;
7955
- break;
7956
- }
7957
- }
7958
- }
7959
- if (!agentId) {
7960
- console.error(`resolveAgentsMdPath: no binding found for accountId=${accountId} in bindings(count=${bindings.length})`);
7961
- return null;
7962
- }
7963
- console.error(`resolveAgentsMdPath: found agentId=${agentId}`);
7964
- if (agentId === "main") {
7965
- console.error("resolveAgentsMdPath: case=multi-agent-main");
7966
- return node_path.default.join(WORKSPACE_DIR, "workspace", "AGENTS.md");
8084
+ if (v2.pass) {
8085
+ console.error(`rule ${key}: revalidate -> pass (fixed) durationMs=${Date.now() - tRevalidate}`);
8086
+ results.push({
8087
+ rule: key,
8088
+ status: "fixed",
8089
+ before: v1.message
8090
+ });
8091
+ } else {
8092
+ console.error(`rule ${key}: revalidate -> still-broken durationMs=${Date.now() - tRevalidate} after=${v2.message ?? ""}`);
8093
+ results.push({
8094
+ rule: key,
8095
+ status: "still-broken",
8096
+ before: v1.message,
8097
+ after: v2.message
8098
+ });
8099
+ failedKeys.add(key);
8100
+ }
7967
8101
  }
7968
- const agentsObj = asRecord(config.agents);
7969
- const list = Array.isArray(agentsObj?.list) ? agentsObj.list : [];
7970
- for (const a of list) {
7971
- const agent = asRecord(a);
7972
- if (agent?.id === agentId) {
7973
- const ws = typeof agent.workspace === "string" ? agent.workspace : node_path.default.join(WORKSPACE_DIR, "workspace");
7974
- console.error(`resolveAgentsMdPath: case=multi-agent-custom agentId=${agentId} workspace=${ws}`);
7975
- return node_path.default.join(ws, "AGENTS.md");
8102
+ let backupPath = null;
8103
+ if (configDirty && !aborted) {
8104
+ backupPath = backupConfigSync(ctx.configPath);
8105
+ const serialized = JSON.stringify(ctx.config, null, 2);
8106
+ try {
8107
+ writeFile(ctx.configPath, serialized);
8108
+ console.error(`runDoctor: writeback ok path=${ctx.configPath} bytes=${serialized.length}`);
8109
+ } catch (e) {
8110
+ const msg = e.message;
8111
+ console.error(`runDoctor: writeback failed path=${ctx.configPath} message=${msg}`);
8112
+ results.push({
8113
+ rule: "*config-writeback*",
8114
+ status: "error",
8115
+ message: "config write failed: " + msg
8116
+ });
8117
+ aborted = true;
7976
8118
  }
8119
+ } else if (configDirty && aborted) console.error("runDoctor: writeback skipped (aborted, leaving on-disk config untouched)");
8120
+ else console.error("runDoctor: writeback skipped (no rule transitioned to fixed)");
8121
+ console.error(`runDoctor: end aborted=${aborted} results=${results.length}`);
8122
+ if (originalConfig !== null) {
8123
+ const d = (0, import_lib.diffString)(originalConfig, ctx.config, { color: false });
8124
+ console.error(`runDoctor: diff backupPath=${backupPath ?? "(none)"} changed=${!!d}`);
8125
+ if (d) {
8126
+ process.stdout.write(`original: ${backupPath ?? "(none)"}\n`);
8127
+ process.stdout.write(`after fixed: ${ctx.configPath}\n`);
8128
+ process.stdout.write("diff:\n");
8129
+ process.stdout.write(d);
8130
+ if (!d.endsWith("\n")) process.stdout.write("\n");
8131
+ } else process.stdout.write("(no openclaw.json changes)\n");
7977
8132
  }
7978
- console.error(`resolveAgentsMdPath: agentId=${agentId} not found in agents.list(count=${list.length})`);
7979
- return null;
8133
+ return finalize(results, aborted);
7980
8134
  }
7981
- function appendPeToAgentsMd(agentsMdPath) {
7982
- const dir = node_path.default.dirname(agentsMdPath);
7983
- if (!node_fs.default.existsSync(dir)) node_fs.default.mkdirSync(dir, { recursive: true });
7984
- const existing = node_fs.default.existsSync(agentsMdPath) ? node_fs.default.readFileSync(agentsMdPath, "utf-8") : "";
7985
- if (existing.includes(`<${PE_XML_TAG}>`)) {
7986
- console.error(`lark-cli-init: <${PE_XML_TAG}> already present in ${agentsMdPath}, skipping`);
7987
- return;
7988
- }
7989
- const sep = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
7990
- node_fs.default.appendFileSync(agentsMdPath, `${sep}${PE_PLACEHOLDER}`, "utf-8");
7991
- console.error(`lark-cli-init: appended PE placeholder to ${agentsMdPath}`);
8135
+ /** Deep-clone a JSON-shaped value. Uses structuredClone where available
8136
+ * (Node 17+); falls back to JSON round-trip otherwise. The config we
8137
+ * clone here is JSON5-parsed so it's strictly tree-shaped — no
8138
+ * references, no functions making both paths equivalent. */
8139
+ function deepClone(v) {
8140
+ if (typeof structuredClone === "function") return structuredClone(v);
8141
+ return JSON.parse(JSON.stringify(v));
7992
8142
  }
7993
- function runLarkCliInit(opts) {
7994
- const configPath = opts.configPath ?? CONFIG_PATH;
7995
- if (!isLarkPluginInstalled(configPath)) {
7996
- console.error("lark-cli-init: skipping — openclaw-lark plugin not installed");
7997
- return {
7998
- ok: true,
7999
- skipped: true,
8000
- skipReason: "openclaw-lark plugin not installed"
8001
- };
8002
- }
8003
- if (!isLarkCliAvailable()) {
8004
- console.error("lark-cli-init: skipping — lark-cli command not found");
8005
- return {
8006
- ok: true,
8007
- skipped: true,
8008
- skipReason: "lark-cli command not found"
8009
- };
8010
- }
8011
- const config = readConfig(configPath);
8012
- if (!config) return {
8013
- ok: false,
8014
- error: `could not read config at ${configPath}`
8015
- };
8016
- const agentsMdPath = resolveAgentsMdPath(opts.appId, config);
8017
- console.error(`lark-cli-init: resolved agents.md path=${agentsMdPath ?? "(null)"}`);
8018
- if (!agentsMdPath) return {
8019
- ok: false,
8020
- error: `could not resolve agents.md path for appId=${opts.appId}`
8021
- };
8022
- const appSecret = resolveAppSecret(opts.appId, config, opts.feishuAppSecret);
8023
- if (!appSecret) return {
8024
- ok: false,
8025
- error: `could not resolve appSecret for appId=${opts.appId}`
8026
- };
8027
- console.error(`lark-cli-init: running lark-cli config init --name ${opts.appId} --app-id ${opts.appId} --brand feishu --app-secret-stdin --force-init`);
8028
- const initRes = (0, node_child_process.spawnSync)("lark-cli", [
8029
- "config",
8030
- "init",
8031
- "--name",
8032
- opts.appId,
8033
- "--app-id",
8034
- opts.appId,
8035
- "--brand",
8036
- "feishu",
8037
- "--app-secret-stdin",
8038
- "--force-init"
8039
- ], {
8040
- stdio: [
8041
- "pipe",
8042
- "pipe",
8043
- "pipe"
8044
- ],
8045
- encoding: "utf-8",
8046
- input: appSecret
8047
- });
8048
- const configInitStdout = initRes.stdout?.trim() || void 0;
8049
- const configInitStderr = initRes.stderr?.trim() || void 0;
8050
- if (configInitStdout) console.error(`lark-cli config init stdout: ${configInitStdout}`);
8051
- if (configInitStderr) console.error(`lark-cli config init stderr: ${configInitStderr}`);
8052
- if (initRes.error) return {
8053
- ok: false,
8054
- configInitStdout,
8055
- configInitStderr,
8056
- error: `lark-cli config init spawn error: ${initRes.error.message}`
8057
- };
8058
- if (initRes.status !== 0) return {
8059
- ok: false,
8060
- configInitExitCode: initRes.status ?? void 0,
8061
- configInitStdout,
8062
- configInitStderr,
8063
- error: `lark-cli config init exited with code ${initRes.status}`
8143
+ function finalize(results, aborted) {
8144
+ const summary = {
8145
+ pass: 0,
8146
+ failed: 0,
8147
+ fixed: 0,
8148
+ stillBroken: 0,
8149
+ skipped: 0,
8150
+ error: 0,
8151
+ unknown: 0
8064
8152
  };
8065
- appendPeToAgentsMd(agentsMdPath);
8153
+ for (const r of results) switch (r.status) {
8154
+ case "pass":
8155
+ summary.pass++;
8156
+ break;
8157
+ case "failed":
8158
+ summary.failed++;
8159
+ break;
8160
+ case "fixed":
8161
+ summary.fixed++;
8162
+ break;
8163
+ case "still-broken":
8164
+ summary.stillBroken++;
8165
+ break;
8166
+ case "skipped":
8167
+ summary.skipped++;
8168
+ break;
8169
+ case "error":
8170
+ summary.error++;
8171
+ break;
8172
+ case "unknown":
8173
+ summary.unknown++;
8174
+ break;
8175
+ }
8066
8176
  return {
8067
- ok: true,
8068
- configInitExitCode: 0,
8069
- agentsMdPath
8177
+ results,
8178
+ summary,
8179
+ aborted
8070
8180
  };
8071
8181
  }
8072
8182
  //#endregion
@@ -8149,7 +8259,7 @@ async function reportCliRun(opts) {
8149
8259
  //#region src/help.ts
8150
8260
  const BIN = "mclaw-diagnose";
8151
8261
  function versionBanner() {
8152
- return `v0.1.6`;
8262
+ return `v0.1.7-alpha.1`;
8153
8263
  }
8154
8264
  const COMMANDS = [
8155
8265
  {
@@ -8367,6 +8477,37 @@ OPTIONS
8367
8477
  --skip-config-update Leave plugins.installs in openclaw.json untouched.
8368
8478
  --ctx=<base64> Opaque ctx; see install-openclaw for semantics.
8369
8479
  --oss_file_map=... Pre-built OSS URL map (base64 JSON).
8480
+ `
8481
+ },
8482
+ {
8483
+ name: "install-clis",
8484
+ hidden: true,
8485
+ summary: "Install CLI package(s) (role=cli) from the manifest",
8486
+ help: `USAGE
8487
+ ${BIN} install-clis <tag> --cli=<name>... [options]
8488
+
8489
+ DESCRIPTION
8490
+ Downloads + installs one or more CLI tarballs (role=cli in the manifest)
8491
+ into <home_base>/<pkg.installPath>. Currently the only CLI in this
8492
+ category is lark-cli (@larksuite/cli); openclaw itself is handled by the
8493
+ dedicated install-openclaw command.
8494
+
8495
+ Each package is first extracted into a temporary directory (os.tmpdir(),
8496
+ typically tmpfs) to avoid overlayfs race conditions, then ferried to a
8497
+ sibling .new directory and atomically swapped into the final target path.
8498
+
8499
+ ARGUMENTS
8500
+ <tag> Openclaw version tag, e.g. 2026.4.11.
8501
+
8502
+ OPTIONS
8503
+ --cli=<name> CLI package to install by short name or scoped
8504
+ packageName (repeatable, at least one required).
8505
+ --home_base=<dir> Override the /home/gem base (tests).
8506
+ --ctx=<base64> Opaque ctx; ossFileMap is extracted from it.
8507
+ --oss_file_map=... Pre-built OSS URL map (base64 JSON); skips innerapi.
8508
+
8509
+ EXAMPLES
8510
+ ${BIN} install-clis 2026.4.11 --cli=lark-cli
8370
8511
  `
8371
8512
  },
8372
8513
  {
@@ -9862,7 +10003,7 @@ function parseCtxFlag(args) {
9862
10003
  * (The mode itself is args[0] in the filtered set, so we skip index 0.)
9863
10004
  */
9864
10005
  function getPositionalTag(args, modeName) {
9865
- return args.find((a, i) => i > 0 && !a.startsWith("--") && a !== modeName);
10006
+ return args.find((a, i) => i > 0 && !a.startsWith("-") && a !== modeName);
9866
10007
  }
9867
10008
  function getFlag(args, name) {
9868
10009
  const prefix = `--${name}=`;
@@ -10203,6 +10344,64 @@ async function main() {
10203
10344
  if (error) throw error;
10204
10345
  break;
10205
10346
  }
10347
+ case "install-clis": {
10348
+ const tag = getPositionalTag(args, "install-clis");
10349
+ if (!tag) {
10350
+ console.error("Usage: install-clis <tag> --cli=<name>... [--home_base=<dir>] [--ctx=<base64> | --oss_file_map=<base64>]");
10351
+ node_process.default.exit(1);
10352
+ }
10353
+ const names = getMultiFlag(args, "cli");
10354
+ const homeBase = getFlag(args, "home_base");
10355
+ const ossFileMapFlag = getFlag(args, "oss_file_map");
10356
+ let installOssFileMap;
10357
+ let rawForTelemetry;
10358
+ if (!ossFileMapFlag) {
10359
+ rawForTelemetry = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
10360
+ populate: planCtxPopulate({ command: "install" }),
10361
+ caller,
10362
+ traceId
10363
+ });
10364
+ installOssFileMap = normalizeCtx(rawForTelemetry).install.ossFileMap;
10365
+ }
10366
+ const ossFileMap = resolveOssFileMap({
10367
+ ossFileMapFlag,
10368
+ installOssFileMap
10369
+ });
10370
+ console.error(`[install-clis] ossFileMap=${JSON.stringify(ossFileMap)}`);
10371
+ let feishuAppSecret;
10372
+ if (names.includes("lark-cli")) feishuAppSecret = normalizeCtx(await fetchCtxViaInnerApi({
10373
+ populate: { app: ["feishuAppSecret"] },
10374
+ caller,
10375
+ traceId
10376
+ })).app.feishuAppSecret || void 0;
10377
+ let success = true;
10378
+ let error;
10379
+ try {
10380
+ await installClis(tag, ossFileMap, {
10381
+ names,
10382
+ homeBase,
10383
+ feishuAppSecret
10384
+ });
10385
+ } catch (e) {
10386
+ success = false;
10387
+ error = e;
10388
+ }
10389
+ if (success) console.log(JSON.stringify({ ok: true }));
10390
+ await reportRun("install-clis", rc, rawForTelemetry, args.join(" "), Date.now() - t0, {
10391
+ success,
10392
+ result: {
10393
+ tag,
10394
+ names
10395
+ },
10396
+ error
10397
+ }, {
10398
+ scene,
10399
+ profile,
10400
+ fix: false
10401
+ });
10402
+ if (error) throw error;
10403
+ break;
10404
+ }
10206
10405
  case "download-resource": {
10207
10406
  const tag = getPositionalTag(args, "download-resource");
10208
10407
  if (!tag) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lark-apaas/openclaw-scripts-diagnose-cli",
3
- "version": "0.1.6",
3
+ "version": "0.1.7-alpha.1",
4
4
  "description": "CLI for OpenClaw config diagnose and repair with JSON5 support",
5
5
  "main": "dist/index.cjs",
6
6
  "bin": {