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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/LICENSE +13 -0
  2. package/README.md +121 -0
  3. package/dist/index.cjs +1008 -0
  4. package/package.json +40 -0
package/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Lark Technologies Pte. Ltd. and/or its affiliates
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted,provided that the above copyright notice and this permission notice appear in all copies.
6
+
7
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
8
+ IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE
9
+ INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO
10
+ EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
11
+ CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,
12
+ DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
13
+ ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,121 @@
1
+ # @lark-apaas/openclaw-scripts-diagnose-cli
2
+
3
+ OpenClaw 配置诊断与修复 CLI,支持 JSON5 格式。
4
+
5
+ ## 使用方式
6
+
7
+ ```bash
8
+ # 检查模式:校验 openclaw 配置
9
+ npx -y @lark-apaas/openclaw-scripts-diagnose-cli check --ctx='<base64编码的JSON>'
10
+
11
+ # 修复模式:修复失败的规则
12
+ npx -y @lark-apaas/openclaw-scripts-diagnose-cli repair --ctx='<base64编码的JSON>'
13
+ ```
14
+
15
+ ## 架构
16
+
17
+ ```
18
+ @Rule() 装饰器注册
19
+
20
+ 依赖图拓扑排序
21
+
22
+ 按依赖顺序执行 validate / repair
23
+
24
+ 输出 JSON 结果到 stdout
25
+ ```
26
+
27
+ ### 规则引擎
28
+
29
+ - **`DiagnoseRule`** 抽象基类:每个规则实现 `validate()` 和可选的 `repair()`
30
+ - **`@Rule(meta)`** 装饰器:声明规则元信息(key、dependsOn、repairMode、skipWhen),自动注册到全局注册表
31
+ - **依赖图**:框架通过 `dependsOn` 拓扑排序,依赖失败则跳过当前规则
32
+
33
+ ### 修复模式(RepairMode)
34
+
35
+ | 模式 | 说明 | 脚本行为 |
36
+ |------|------|---------|
37
+ | `standard` | 可自动修复 | 调用 `repair()` 方法 |
38
+ | `ai` | 需 AI/人工介入 | 仅返回 `{ rule_name, detail }` |
39
+ | `reset` | 需外部重置 | 仅返回 `{ rule_name, detail }` |
40
+
41
+ ## 规则列表
42
+
43
+ ### 依赖关系图
44
+
45
+ ```
46
+ multi_process_detect config_file_recover
47
+ (无依赖) (无依赖)
48
+
49
+ config_file_missing
50
+
51
+ config_syntax_check
52
+ ↙ ↙ ↓ ↘ ↘
53
+ model_ secret_ feishu gateway allowed_
54
+ provider provider channel origins
55
+ ↓ ↓
56
+ jwt_token secrets_file
57
+ ```
58
+
59
+ ### 规则详情
60
+
61
+ | 规则 Key | RepairMode | dependsOn | skipWhen | 说明 |
62
+ |----------|-----------|-----------|----------|------|
63
+ | `multi_process_detect` | standard | — | — | 检测多个 openclaw-gateway 进程 |
64
+ | `config_file_recover` | standard | — | — | 配置文件缺失但有 `.bak` 备份,从最高编号备份恢复 |
65
+ | `config_file_missing` | reset | `config_file_recover` | — | 配置文件缺失且无备份 |
66
+ | `config_syntax_check` | ai | `config_file_recover`, `config_file_missing` | — | JSON5 语法校验 |
67
+ | `model_provider` | standard | `config_syntax_check` | 未使用 miaoda 模型 | 模型 Provider 配置 |
68
+ | `secret_provider` | standard | `config_syntax_check` | 未使用 miaoda 或无 provider 引用 | 密钥 Provider 配置 |
69
+ | `feishu_channel` | standard | `config_syntax_check` | — | 飞书渠道配置 |
70
+ | `gateway` | standard | `config_syntax_check` | — | 网关配置(port、auth、controlUi) |
71
+ | `allowed_origins` | standard | `config_syntax_check` | — | CORS 来源配置 |
72
+ | `jwt_token` | standard | `config_syntax_check` | apiKey 未引用 miaoda-provider | JWT Token 有效期 |
73
+ | `secrets_file` | standard | `config_syntax_check` | 无字段引用 miaoda-secret-provider | 密钥文件内容校验 |
74
+
75
+ ## Check 输出格式
76
+
77
+ ```json
78
+ {
79
+ "failedRules": {
80
+ "standard": ["gateway", "feishu_channel"],
81
+ "ai": [{ "rule_name": "config_syntax_check", "detail": "JSON5 语法错误..." }],
82
+ "reset": []
83
+ }
84
+ }
85
+ ```
86
+
87
+ ## 扩展新规则
88
+
89
+ 1. 在 `src/rules/` 下新建文件
90
+ 2. 实现 `@Rule()` 装饰的类:
91
+
92
+ ```typescript
93
+ @Rule({
94
+ key: 'my_new_rule',
95
+ dependsOn: ['config_syntax_check'],
96
+ repairMode: 'standard',
97
+ })
98
+ class MyNewRule extends DiagnoseRule {
99
+ validate(ctx: RuleContext): RuleResult {
100
+ // 检查逻辑
101
+ }
102
+ repair(ctx: RuleContext): void {
103
+ // 修复逻辑(仅 standard 模式需要)
104
+ }
105
+ }
106
+ ```
107
+
108
+ 3. 在 `src/rules/index.ts` 加一行 `import './my-new-rule.js'`
109
+
110
+ ## 开发
111
+
112
+ ```bash
113
+ # 构建
114
+ npm run build
115
+
116
+ # 单元测试
117
+ npm test
118
+
119
+ # 集成测试(需要 Docker)
120
+ npm run test:integration
121
+ ```
package/dist/index.cjs ADDED
@@ -0,0 +1,1008 @@
1
+ #!/usr/bin/env node
2
+ //#region \0rolldown/runtime.js
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
+ key = keys[i];
12
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
13
+ get: ((k) => from[k]).bind(null, key),
14
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
15
+ });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
20
+ value: mod,
21
+ enumerable: true
22
+ }) : target, mod));
23
+ //#endregion
24
+ let node_process = require("node:process");
25
+ node_process = __toESM(node_process);
26
+ let json5 = require("json5");
27
+ json5 = __toESM(json5);
28
+ let node_fs = require("node:fs");
29
+ node_fs = __toESM(node_fs);
30
+ let node_path = require("node:path");
31
+ node_path = __toESM(node_path);
32
+ let node_child_process = require("node:child_process");
33
+ //#region src/rule-engine/base.ts
34
+ /** Abstract base class for all diagnose rules */
35
+ var DiagnoseRule = class {
36
+ /** Metadata injected by @Rule() decorator */
37
+ get meta() {
38
+ return this._meta;
39
+ }
40
+ /** Fix the issue. Mutates ctx.config in-place. Default: no-op */
41
+ repair(_ctx) {}
42
+ };
43
+ const ruleRegistry = [];
44
+ /** Class decorator: register a rule with metadata */
45
+ function Rule(meta) {
46
+ return function(target) {
47
+ target.prototype._meta = meta;
48
+ ruleRegistry.push(target);
49
+ return target;
50
+ };
51
+ }
52
+ /** Get all registered rule instances, topologically sorted by dependsOn */
53
+ function getAllRules() {
54
+ return topoSort(ruleRegistry.map((Ctor) => new Ctor()));
55
+ }
56
+ function topoSort(rules) {
57
+ const keyMap = /* @__PURE__ */ new Map();
58
+ const inDegree = /* @__PURE__ */ new Map();
59
+ const adj = /* @__PURE__ */ new Map();
60
+ for (const rule of rules) {
61
+ keyMap.set(rule.meta.key, rule);
62
+ inDegree.set(rule.meta.key, (rule.meta.dependsOn || []).length);
63
+ for (const dep of rule.meta.dependsOn || []) {
64
+ if (!adj.has(dep)) adj.set(dep, []);
65
+ adj.get(dep).push(rule.meta.key);
66
+ }
67
+ }
68
+ const queue = [...keyMap.keys()].filter((k) => inDegree.get(k) === 0);
69
+ const sorted = [];
70
+ while (queue.length > 0) {
71
+ const key = queue.shift();
72
+ sorted.push(keyMap.get(key));
73
+ for (const dependent of adj.get(key) || []) {
74
+ inDegree.set(dependent, inDegree.get(dependent) - 1);
75
+ if (inDegree.get(dependent) === 0) queue.push(dependent);
76
+ }
77
+ }
78
+ return sorted;
79
+ }
80
+ //#endregion
81
+ //#region src/utils.ts
82
+ /**
83
+ * Navigate nested object by keys, returning the value if it's a non-array object,
84
+ * or undefined otherwise.
85
+ */
86
+ function getNestedMap(obj, ...keys) {
87
+ let current = obj;
88
+ for (const key of keys) {
89
+ if (current == null || typeof current !== "object") return void 0;
90
+ current = current[key];
91
+ }
92
+ return current != null && typeof current === "object" && !Array.isArray(current) ? current : void 0;
93
+ }
94
+ /**
95
+ * Recursively check that all entries in `expected` match those in `actual`.
96
+ * Values are compared via String() coercion; nested objects recurse.
97
+ */
98
+ function matchMap(actual, expected) {
99
+ if (actual == null || typeof actual !== "object") return false;
100
+ const actualObj = actual;
101
+ for (const [k, v] of Object.entries(expected)) {
102
+ const av = actualObj[k];
103
+ if (av === void 0) return false;
104
+ if (v != null && typeof v === "object" && !Array.isArray(v)) {
105
+ if (!matchMap(av, v)) return false;
106
+ continue;
107
+ }
108
+ if (String(av) !== String(v)) return false;
109
+ }
110
+ return true;
111
+ }
112
+ /** Check if a value is a provider reference pointing to `providerName`. */
113
+ function isProviderRef(val, providerName) {
114
+ return val != null && typeof val === "object" && !Array.isArray(val) && val.provider === providerName;
115
+ }
116
+ /** Validate that a string is a non-expired JWT (3-part, base64url payload with numeric exp). */
117
+ function isValidJWT(token) {
118
+ if (typeof token !== "string") return false;
119
+ const parts = token.split(".");
120
+ if (parts.length !== 3) return false;
121
+ try {
122
+ const exp = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf-8")).exp;
123
+ if (typeof exp !== "number") return false;
124
+ return Date.now() < exp * 1e3;
125
+ } catch {
126
+ return false;
127
+ }
128
+ }
129
+ /** Set a deeply nested value, creating intermediate objects as needed. */
130
+ function setNestedValue(obj, keys, value) {
131
+ let current = obj;
132
+ for (let i = 0; i < keys.length - 1; i++) {
133
+ const k = keys[i];
134
+ if (current[k] == null || typeof current[k] !== "object") current[k] = {};
135
+ current = current[k];
136
+ }
137
+ current[keys[keys.length - 1]] = value;
138
+ }
139
+ /** Analyze which miaoda providers the config references. */
140
+ function analyzeProviderDeps(config) {
141
+ const deps = {
142
+ usesMiaodaProvider: false,
143
+ usesMiaodaSecretProvider: false
144
+ };
145
+ if (!config) return deps;
146
+ const miaoda = getNestedMap(config, "models", "providers", "miaoda");
147
+ if (miaoda) {
148
+ if (isProviderRef(miaoda.apiKey, "miaoda-provider")) deps.usesMiaodaProvider = true;
149
+ if (isProviderRef((typeof miaoda.headers === "object" && miaoda.headers || {})["x-api-key"], "miaoda-secret-provider")) deps.usesMiaodaSecretProvider = true;
150
+ }
151
+ const feishu = getNestedMap(config, "channels", "feishu");
152
+ if (feishu && isProviderRef(feishu.appSecret, "miaoda-secret-provider")) deps.usesMiaodaSecretProvider = true;
153
+ const auth = getNestedMap(config, "gateway", "auth");
154
+ if (auth && isProviderRef(auth.token, "miaoda-secret-provider")) deps.usesMiaodaSecretProvider = true;
155
+ return deps;
156
+ }
157
+ /** Check if the config uses miaoda models (agents.defaults.model.primary or agents.defaults.models). */
158
+ function shouldCheckMiaodaRules(config) {
159
+ if (!config) return false;
160
+ const model = getNestedMap(config, "agents", "defaults", "model");
161
+ if (model && typeof model.primary === "string" && model.primary.startsWith("miaoda/")) return true;
162
+ const defaults = getNestedMap(config, "agents", "defaults");
163
+ if (defaults && defaults.models) {
164
+ if (typeof defaults.models === "object" && !Array.isArray(defaults.models)) {
165
+ for (const key of Object.keys(defaults.models)) if (key.startsWith("miaoda/")) return true;
166
+ }
167
+ if (Array.isArray(defaults.models)) {
168
+ for (const m of defaults.models) if (typeof m === "string" && m.startsWith("miaoda/")) return true;
169
+ }
170
+ }
171
+ return false;
172
+ }
173
+ /** Get the JSON5 parser (bundled as dependency). */
174
+ function loadJSON5() {
175
+ return json5.default;
176
+ }
177
+ //#endregion
178
+ //#region src/rule-engine/io.ts
179
+ /** Read a text file, return content or throw */
180
+ function readFile(filePath) {
181
+ return node_fs.default.readFileSync(filePath, "utf-8").trim();
182
+ }
183
+ /** Write a text file, creating parent dirs if needed */
184
+ function writeFile(filePath, content) {
185
+ const dir = node_path.default.dirname(filePath);
186
+ if (!node_fs.default.existsSync(dir)) node_fs.default.mkdirSync(dir, { recursive: true });
187
+ node_fs.default.writeFileSync(filePath, content, "utf-8");
188
+ }
189
+ /** Check if a file exists */
190
+ function fileExists(filePath) {
191
+ return node_fs.default.existsSync(filePath);
192
+ }
193
+ /** Execute a shell command, return stdout. Throws on failure. */
194
+ function shell(cmd, timeoutMs = 1e4) {
195
+ return (0, node_child_process.execSync)(cmd, {
196
+ encoding: "utf-8",
197
+ timeout: timeoutMs
198
+ }).trim();
199
+ }
200
+ //#endregion
201
+ //#region \0@oxc-project+runtime@0.121.0/helpers/decorate.js
202
+ function __decorate(decorators, target, key, desc) {
203
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
204
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
205
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
206
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
207
+ }
208
+ //#endregion
209
+ //#region src/rules/multi-process-detect.ts
210
+ let ProcessStatusRule = class ProcessStatusRule extends DiagnoseRule {
211
+ validate(_ctx) {
212
+ try {
213
+ const output = shell("count=0; for pid in $(pgrep -f \"[o]penclaw-gateway\" 2>/dev/null); do comm=$(cat /proc/$pid/comm 2>/dev/null || true); case \"$comm\" in openclaw*) count=$((count+1));; esac; done; [ \"$count\" -gt 1 ] && echo \"multiple_gateway_processes:$count\" || true");
214
+ if (output.includes("multiple_gateway_processes")) return {
215
+ pass: false,
216
+ message: "multiple openclaw-gateway processes detected: " + output
217
+ };
218
+ return { pass: true };
219
+ } catch (e) {
220
+ return {
221
+ pass: false,
222
+ message: "process check exec failed: " + e.message
223
+ };
224
+ }
225
+ }
226
+ };
227
+ ProcessStatusRule = __decorate([Rule({
228
+ key: "multi_process_detect",
229
+ repairMode: "standard"
230
+ })], ProcessStatusRule);
231
+ //#endregion
232
+ //#region src/rules/config-file-recover.ts
233
+ /**
234
+ * Find backup files matching `<configBaseName>.bak*` in the same directory.
235
+ */
236
+ function findBackupFiles(configPath) {
237
+ const dir = node_path.default.dirname(configPath);
238
+ const baseName = node_path.default.basename(configPath);
239
+ try {
240
+ return node_fs.default.readdirSync(dir).filter((f) => f.startsWith(baseName + ".bak")).map((f) => node_path.default.join(dir, f));
241
+ } catch {
242
+ return [];
243
+ }
244
+ }
245
+ /**
246
+ * Among backup files, find the one with the highest numeric suffix.
247
+ * `.bak` (no number) is treated as 0, `.bak1` as 1, `.bak2` as 2, etc.
248
+ */
249
+ function findHighestBackup(backupFiles) {
250
+ if (backupFiles.length === 0) return null;
251
+ const bakRegex = /\.bak(\d*)$/;
252
+ let best = null;
253
+ for (const f of backupFiles) {
254
+ const match = bakRegex.exec(f);
255
+ if (!match) continue;
256
+ const n = match[1] ? parseInt(match[1], 10) : 0;
257
+ if (!best || n > best.n) best = {
258
+ path: f,
259
+ n
260
+ };
261
+ }
262
+ return best?.path ?? null;
263
+ }
264
+ let ConfigFileBackupRule = class ConfigFileBackupRule extends DiagnoseRule {
265
+ validate(ctx) {
266
+ const configPath = ctx.config.__configPath;
267
+ if (!configPath) return {
268
+ pass: false,
269
+ message: "configPath not provided"
270
+ };
271
+ if (fileExists(configPath)) return { pass: true };
272
+ if (findBackupFiles(configPath).length > 0) return {
273
+ pass: false,
274
+ message: "配置文件不存在但发现备份文件, 可从备份恢复"
275
+ };
276
+ return { pass: true };
277
+ }
278
+ repair(ctx) {
279
+ const configPath = ctx.config.__configPath;
280
+ if (!configPath) return;
281
+ const best = findHighestBackup(findBackupFiles(configPath));
282
+ if (!best) return;
283
+ const content = readFile(best);
284
+ loadJSON5().parse(content);
285
+ writeFile(configPath, content);
286
+ }
287
+ };
288
+ ConfigFileBackupRule = __decorate([Rule({
289
+ key: "config_file_recover",
290
+ repairMode: "standard"
291
+ })], ConfigFileBackupRule);
292
+ //#endregion
293
+ //#region src/rules/config-file-missing.ts
294
+ /**
295
+ * Check if backup files exist for the given config path.
296
+ */
297
+ function hasBackupFiles(configPath) {
298
+ const dir = node_path.default.dirname(configPath);
299
+ const baseName = node_path.default.basename(configPath);
300
+ try {
301
+ return node_fs.default.readdirSync(dir).some((f) => f.startsWith(baseName + ".bak"));
302
+ } catch {
303
+ return false;
304
+ }
305
+ }
306
+ let ConfigFileMissingRule = class ConfigFileMissingRule extends DiagnoseRule {
307
+ validate(ctx) {
308
+ const configPath = ctx.config.__configPath;
309
+ if (!configPath) return {
310
+ pass: false,
311
+ message: "configPath not provided"
312
+ };
313
+ if (fileExists(configPath)) return { pass: true };
314
+ if (hasBackupFiles(configPath)) return { pass: true };
315
+ return {
316
+ pass: false,
317
+ message: `配置文件不存在且无备份文件, 请创建 ${configPath}`
318
+ };
319
+ }
320
+ };
321
+ ConfigFileMissingRule = __decorate([Rule({
322
+ key: "config_file_missing",
323
+ dependsOn: ["config_file_recover"],
324
+ repairMode: "reset"
325
+ })], ConfigFileMissingRule);
326
+ //#endregion
327
+ //#region src/rules/config-syntax.ts
328
+ let ConfigSyntaxRule = class ConfigSyntaxRule extends DiagnoseRule {
329
+ validate(ctx) {
330
+ const configPath = ctx.config.__configPath;
331
+ if (!fileExists(configPath)) return { pass: true };
332
+ try {
333
+ loadJSON5().parse(readFile(configPath));
334
+ return { pass: true };
335
+ } catch (e) {
336
+ return {
337
+ pass: false,
338
+ message: `配置文件语法不正确, ${e.message}, 请修复${configPath}`
339
+ };
340
+ }
341
+ }
342
+ };
343
+ ConfigSyntaxRule = __decorate([Rule({
344
+ key: "config_syntax_check",
345
+ dependsOn: ["config_file_recover", "config_file_missing"],
346
+ repairMode: "ai"
347
+ })], ConfigSyntaxRule);
348
+ //#endregion
349
+ //#region src/rules/model-provider.ts
350
+ var _ModelProviderRule;
351
+ let ModelProviderRule = class ModelProviderRule extends DiagnoseRule {
352
+ static {
353
+ _ModelProviderRule = this;
354
+ }
355
+ static DEFAULT_API_KEY = {
356
+ source: "file",
357
+ provider: "miaoda-provider",
358
+ id: "value"
359
+ };
360
+ static DEFAULT_X_API_KEY_HEADER = {
361
+ source: "file",
362
+ provider: "miaoda-secret-provider",
363
+ id: "/models_providers_miaoda_headers_x_api_key"
364
+ };
365
+ static DEFAULT_API = "openai-completions";
366
+ static getExpectedBaseUrl(vars) {
367
+ return vars.baseURL + "/api/v1/sgw/model/proxy";
368
+ }
369
+ validate(ctx) {
370
+ const provider = getNestedMap(ctx.config, "models", "providers", "miaoda");
371
+ if (!provider) return {
372
+ pass: false,
373
+ message: "models.providers.miaoda not found"
374
+ };
375
+ const expected = this.getExpected(ctx.vars);
376
+ if (provider.baseUrl !== expected.baseUrl) return {
377
+ pass: false,
378
+ message: "baseUrl mismatch: got " + provider.baseUrl + ", expected " + expected.baseUrl
379
+ };
380
+ const expectedApiKey = expected.apiKey && typeof expected.apiKey === "object" ? expected.apiKey : null;
381
+ if (expectedApiKey) if (typeof provider.apiKey === "object" && provider.apiKey !== null && !Array.isArray(provider.apiKey)) {
382
+ if (!matchMap(provider.apiKey, expectedApiKey)) return {
383
+ pass: false,
384
+ message: "apiKey object mismatch: got " + JSON.stringify(provider.apiKey)
385
+ };
386
+ } else if (typeof provider.apiKey === "string") {
387
+ if (!isValidJWT(provider.apiKey)) return {
388
+ pass: false,
389
+ message: "apiKey is a string but not a valid/unexpired JWT"
390
+ };
391
+ } else return {
392
+ pass: false,
393
+ message: "apiKey has unexpected type"
394
+ };
395
+ if (expected.api !== void 0) {
396
+ if (provider.api !== expected.api) return {
397
+ pass: false,
398
+ message: "api mismatch: got " + provider.api + ", expected " + expected.api
399
+ };
400
+ }
401
+ const expectedHeaders = getNestedMap(expected, "headers");
402
+ const expectedXApiKey = expectedHeaders ? expectedHeaders["x-api-key"] : void 0;
403
+ if (expectedXApiKey && typeof expectedXApiKey === "object") {
404
+ const xKey = (typeof provider.headers === "object" && provider.headers || {})["x-api-key"];
405
+ if (typeof xKey === "object" && xKey !== null && !Array.isArray(xKey)) {
406
+ if (!matchMap(xKey, expectedXApiKey)) return {
407
+ pass: false,
408
+ message: "headers.x-api-key object mismatch: got " + JSON.stringify(xKey)
409
+ };
410
+ } else if (typeof xKey === "string") {
411
+ if (xKey !== ctx.vars.innerAPIKey) return {
412
+ pass: false,
413
+ message: "headers.x-api-key string value mismatch"
414
+ };
415
+ } else return {
416
+ pass: false,
417
+ message: "headers.x-api-key has unexpected type"
418
+ };
419
+ }
420
+ return { pass: true };
421
+ }
422
+ repair(ctx) {
423
+ const expected = this.getExpected(ctx.vars);
424
+ setNestedValue(ctx.config, [
425
+ "models",
426
+ "providers",
427
+ "miaoda",
428
+ "baseUrl"
429
+ ], expected.baseUrl);
430
+ if (expected.apiKey !== void 0) setNestedValue(ctx.config, [
431
+ "models",
432
+ "providers",
433
+ "miaoda",
434
+ "apiKey"
435
+ ], expected.apiKey);
436
+ if (expected.api !== void 0) setNestedValue(ctx.config, [
437
+ "models",
438
+ "providers",
439
+ "miaoda",
440
+ "api"
441
+ ], expected.api);
442
+ if (expected.headers !== void 0) setNestedValue(ctx.config, [
443
+ "models",
444
+ "providers",
445
+ "miaoda",
446
+ "headers"
447
+ ], expected.headers);
448
+ }
449
+ getExpected(vars) {
450
+ return {
451
+ baseUrl: _ModelProviderRule.getExpectedBaseUrl(vars),
452
+ apiKey: _ModelProviderRule.DEFAULT_API_KEY,
453
+ api: _ModelProviderRule.DEFAULT_API,
454
+ headers: { "x-api-key": _ModelProviderRule.DEFAULT_X_API_KEY_HEADER }
455
+ };
456
+ }
457
+ };
458
+ ModelProviderRule = _ModelProviderRule = __decorate([Rule({
459
+ key: "model_provider",
460
+ dependsOn: ["config_syntax_check"],
461
+ repairMode: "standard",
462
+ skipWhen: ({ hasMiaoda }) => !hasMiaoda
463
+ })], ModelProviderRule);
464
+ //#endregion
465
+ //#region src/rules/secret-provider.ts
466
+ var _SecretProviderRule;
467
+ let SecretProviderRule = class SecretProviderRule extends DiagnoseRule {
468
+ static {
469
+ _SecretProviderRule = this;
470
+ }
471
+ static DEFAULT_MIAODA_PROVIDER = {
472
+ source: "file",
473
+ path: "/home/gem/workspace/.force/openclaw/miaoda-provider-key",
474
+ mode: "singleValue"
475
+ };
476
+ static DEFAULT_MIAODA_SECRET_PROVIDER = {
477
+ source: "file",
478
+ path: "/home/gem/workspace/.force/openclaw/miaoda-openclaw-secrets.json",
479
+ mode: "json"
480
+ };
481
+ validate(ctx) {
482
+ const providers = getNestedMap(ctx.config, "secrets", "providers");
483
+ if (!providers) return {
484
+ pass: false,
485
+ message: "secrets.providers not found"
486
+ };
487
+ const { mp: expectedMP, msp: expectedMSP } = this.getExpected();
488
+ if (ctx.providerDeps.usesMiaodaProvider) {
489
+ const mp = providers["miaoda-provider"];
490
+ if (!mp || typeof mp !== "object" || !matchMap(mp, expectedMP)) return {
491
+ pass: false,
492
+ message: "secrets.providers.miaoda-provider mismatch: got " + JSON.stringify(mp)
493
+ };
494
+ }
495
+ if (ctx.providerDeps.usesMiaodaSecretProvider) {
496
+ const msp = providers["miaoda-secret-provider"];
497
+ if (!msp || typeof msp !== "object" || !matchMap(msp, expectedMSP)) return {
498
+ pass: false,
499
+ message: "secrets.providers.miaoda-secret-provider mismatch: got " + JSON.stringify(msp)
500
+ };
501
+ }
502
+ return { pass: true };
503
+ }
504
+ repair(ctx) {
505
+ const { mp: expectedMP, msp: expectedMSP } = this.getExpected();
506
+ if (ctx.providerDeps.usesMiaodaProvider) setNestedValue(ctx.config, [
507
+ "secrets",
508
+ "providers",
509
+ "miaoda-provider"
510
+ ], expectedMP);
511
+ if (ctx.providerDeps.usesMiaodaSecretProvider) setNestedValue(ctx.config, [
512
+ "secrets",
513
+ "providers",
514
+ "miaoda-secret-provider"
515
+ ], expectedMSP);
516
+ }
517
+ getExpected() {
518
+ return {
519
+ mp: _SecretProviderRule.DEFAULT_MIAODA_PROVIDER,
520
+ msp: _SecretProviderRule.DEFAULT_MIAODA_SECRET_PROVIDER
521
+ };
522
+ }
523
+ };
524
+ SecretProviderRule = _SecretProviderRule = __decorate([Rule({
525
+ key: "secret_provider",
526
+ dependsOn: ["config_syntax_check"],
527
+ repairMode: "standard",
528
+ skipWhen: ({ hasMiaoda, deps }) => !hasMiaoda || !deps.usesMiaodaProvider && !deps.usesMiaodaSecretProvider
529
+ })], SecretProviderRule);
530
+ //#endregion
531
+ //#region src/rules/feishu-channel.ts
532
+ var _FeishuChannelRule;
533
+ let FeishuChannelRule = class FeishuChannelRule extends DiagnoseRule {
534
+ static {
535
+ _FeishuChannelRule = this;
536
+ }
537
+ static DEFAULT_APP_SECRET = {
538
+ source: "file",
539
+ provider: "miaoda-secret-provider",
540
+ id: "/channels_feishu_app_secret"
541
+ };
542
+ validate(ctx) {
543
+ const feishu = getNestedMap(ctx.config, "channels", "feishu");
544
+ if (!feishu) return {
545
+ pass: false,
546
+ message: "channels.feishu not found"
547
+ };
548
+ if (feishu.enabled !== true) return {
549
+ pass: false,
550
+ message: "channels.feishu.enabled mismatch: got " + feishu.enabled + ", expected true"
551
+ };
552
+ if (feishu.appId !== ctx.vars.feishuAppID) return {
553
+ pass: false,
554
+ message: "channels.feishu.appId mismatch: got " + feishu.appId + ", expected " + ctx.vars.feishuAppID
555
+ };
556
+ const expectedSecret = _FeishuChannelRule.DEFAULT_APP_SECRET;
557
+ const secret = feishu.appSecret;
558
+ if (typeof secret === "object" && secret !== null && !Array.isArray(secret)) {
559
+ if (!matchMap(secret, expectedSecret)) return {
560
+ pass: false,
561
+ message: "channels.feishu.appSecret object mismatch: got " + JSON.stringify(secret)
562
+ };
563
+ } else if (typeof secret === "string") {
564
+ if (secret !== ctx.vars.feishuAppSecret) return {
565
+ pass: false,
566
+ message: "channels.feishu.appSecret string value mismatch"
567
+ };
568
+ } else return {
569
+ pass: false,
570
+ message: "channels.feishu.appSecret has unexpected type"
571
+ };
572
+ return { pass: true };
573
+ }
574
+ repair(ctx) {
575
+ setNestedValue(ctx.config, [
576
+ "channels",
577
+ "feishu",
578
+ "enabled"
579
+ ], true);
580
+ setNestedValue(ctx.config, [
581
+ "channels",
582
+ "feishu",
583
+ "appId"
584
+ ], ctx.vars.feishuAppID);
585
+ setNestedValue(ctx.config, [
586
+ "channels",
587
+ "feishu",
588
+ "appSecret"
589
+ ], _FeishuChannelRule.DEFAULT_APP_SECRET);
590
+ }
591
+ };
592
+ FeishuChannelRule = _FeishuChannelRule = __decorate([Rule({
593
+ key: "feishu_channel",
594
+ dependsOn: ["config_syntax_check"],
595
+ repairMode: "standard"
596
+ })], FeishuChannelRule);
597
+ //#endregion
598
+ //#region src/rules/gateway.ts
599
+ var _GatewayRule;
600
+ let GatewayRule = class GatewayRule extends DiagnoseRule {
601
+ static {
602
+ _GatewayRule = this;
603
+ }
604
+ static DEFAULT_PORT = 18789;
605
+ static DEFAULT_AUTH_MODE = "token";
606
+ static DEFAULT_AUTH_TOKEN = {
607
+ source: "file",
608
+ provider: "miaoda-secret-provider",
609
+ id: "/gateway_auth_token"
610
+ };
611
+ validate(ctx) {
612
+ const gateway = ctx.config.gateway;
613
+ if (!gateway || typeof gateway !== "object") return {
614
+ pass: false,
615
+ message: "gateway not found"
616
+ };
617
+ const gw = gateway;
618
+ if (gw.port !== _GatewayRule.DEFAULT_PORT) return {
619
+ pass: false,
620
+ message: "gateway.port mismatch: got " + gw.port + ", expected " + _GatewayRule.DEFAULT_PORT
621
+ };
622
+ const auth = gw.auth;
623
+ if (!auth || typeof auth !== "object") return {
624
+ pass: false,
625
+ message: "gateway.auth not found"
626
+ };
627
+ const authObj = auth;
628
+ if (authObj.mode !== _GatewayRule.DEFAULT_AUTH_MODE) return {
629
+ pass: false,
630
+ message: "gateway.auth.mode mismatch: got " + authObj.mode + ", expected " + _GatewayRule.DEFAULT_AUTH_MODE
631
+ };
632
+ const token = authObj.token;
633
+ if (typeof token === "string") {
634
+ if (token !== ctx.vars.gatewayToken) return {
635
+ pass: false,
636
+ message: "gateway.auth.token string value mismatch"
637
+ };
638
+ } else if (typeof token === "object" && token !== null && !Array.isArray(token)) {
639
+ if (!matchMap(token, _GatewayRule.DEFAULT_AUTH_TOKEN)) return {
640
+ pass: false,
641
+ message: "gateway.auth.token object mismatch: got " + JSON.stringify(token)
642
+ };
643
+ } else return {
644
+ pass: false,
645
+ message: "gateway.auth.token has unexpected type"
646
+ };
647
+ const controlUi = gw.controlUi;
648
+ if (!controlUi || typeof controlUi !== "object") return {
649
+ pass: false,
650
+ message: "gateway.controlUi not found"
651
+ };
652
+ if (controlUi.dangerouslyDisableDeviceAuth !== true) return {
653
+ pass: false,
654
+ message: "gateway.controlUi.dangerouslyDisableDeviceAuth must be true, got " + controlUi.dangerouslyDisableDeviceAuth
655
+ };
656
+ return { pass: true };
657
+ }
658
+ repair(ctx) {
659
+ setNestedValue(ctx.config, ["gateway", "port"], _GatewayRule.DEFAULT_PORT);
660
+ setNestedValue(ctx.config, [
661
+ "gateway",
662
+ "auth",
663
+ "mode"
664
+ ], _GatewayRule.DEFAULT_AUTH_MODE);
665
+ setNestedValue(ctx.config, [
666
+ "gateway",
667
+ "auth",
668
+ "token"
669
+ ], _GatewayRule.DEFAULT_AUTH_TOKEN);
670
+ setNestedValue(ctx.config, [
671
+ "gateway",
672
+ "controlUi",
673
+ "dangerouslyDisableDeviceAuth"
674
+ ], true);
675
+ }
676
+ };
677
+ GatewayRule = _GatewayRule = __decorate([Rule({
678
+ key: "gateway",
679
+ dependsOn: ["config_syntax_check"],
680
+ repairMode: "standard"
681
+ })], GatewayRule);
682
+ //#endregion
683
+ //#region src/rules/allowed-origins.ts
684
+ let AllowedOriginsRule = class AllowedOriginsRule extends DiagnoseRule {
685
+ validate(ctx) {
686
+ const expected = getExpectedOrigins(ctx.vars);
687
+ if (expected.length === 0) return { pass: true };
688
+ const missing = findMissing(getCurrentOrigins(ctx.config), expected);
689
+ if (missing.length === 0) return { pass: true };
690
+ return {
691
+ pass: false,
692
+ message: "allowedOrigins missing: " + JSON.stringify(missing)
693
+ };
694
+ }
695
+ repair(ctx) {
696
+ const expected = getExpectedOrigins(ctx.vars);
697
+ const current = getCurrentOrigins(ctx.config);
698
+ const missing = findMissing(current, expected);
699
+ if (missing.length > 0) {
700
+ const seen = /* @__PURE__ */ new Set();
701
+ const merged = [];
702
+ for (const o of current) if (!seen.has(o)) {
703
+ merged.push(o);
704
+ seen.add(o);
705
+ }
706
+ for (const o of missing) if (!seen.has(o)) {
707
+ merged.push(o);
708
+ seen.add(o);
709
+ }
710
+ setNestedValue(ctx.config, [
711
+ "gateway",
712
+ "controlUi",
713
+ "allowedOrigins"
714
+ ], merged);
715
+ }
716
+ }
717
+ };
718
+ AllowedOriginsRule = __decorate([Rule({
719
+ key: "allowed_origins",
720
+ dependsOn: ["config_syntax_check"],
721
+ repairMode: "standard"
722
+ })], AllowedOriginsRule);
723
+ function getExpectedOrigins(vars) {
724
+ const origins = [];
725
+ if (vars.miaodaDomain) origins.push(vars.miaodaDomain);
726
+ if (vars.miaodaOrigin) origins.push(vars.miaodaOrigin);
727
+ if (Array.isArray(vars.miaodaOrigins)) {
728
+ for (const o of vars.miaodaOrigins) if (o) origins.push(o);
729
+ }
730
+ return [...new Set(origins)];
731
+ }
732
+ function getCurrentOrigins(config) {
733
+ const controlUi = getNestedMap(config, "gateway", "controlUi");
734
+ if (!controlUi) return [];
735
+ const raw = controlUi.allowedOrigins;
736
+ if (!Array.isArray(raw)) return [];
737
+ return raw.filter((o) => typeof o === "string");
738
+ }
739
+ function findMissing(current, expected) {
740
+ const set = new Set(current);
741
+ return expected.filter((o) => !set.has(o));
742
+ }
743
+ //#endregion
744
+ //#region src/rules/jwt-token.ts
745
+ let JwtTokenRule = class JwtTokenRule extends DiagnoseRule {
746
+ validate(ctx) {
747
+ const filePath = ctx.vars.providerFilePath;
748
+ if (!filePath) return {
749
+ pass: false,
750
+ message: "providerFilePath not set"
751
+ };
752
+ let token;
753
+ try {
754
+ token = readFile(filePath);
755
+ } catch (e) {
756
+ return {
757
+ pass: false,
758
+ message: "read provider key failed: " + e.message
759
+ };
760
+ }
761
+ if (!token) return {
762
+ pass: false,
763
+ message: "provider key file is empty"
764
+ };
765
+ const parts = token.split(".");
766
+ if (parts.length !== 3) return {
767
+ pass: false,
768
+ message: "invalid JWT format, got " + parts.length + " parts"
769
+ };
770
+ let claims;
771
+ try {
772
+ claims = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf-8"));
773
+ } catch (e) {
774
+ return {
775
+ pass: false,
776
+ message: "decode JWT payload failed: " + e.message
777
+ };
778
+ }
779
+ if (claims.exp === void 0) return {
780
+ pass: false,
781
+ message: "JWT has no exp claim"
782
+ };
783
+ if (typeof claims.exp !== "number") return {
784
+ pass: false,
785
+ message: "JWT exp has unexpected type"
786
+ };
787
+ const expMs = claims.exp * 1e3;
788
+ if (Date.now() >= expMs) return {
789
+ pass: false,
790
+ message: "JWT expired at " + new Date(expMs).toISOString()
791
+ };
792
+ return {
793
+ pass: true,
794
+ message: "JWT valid until " + new Date(expMs).toISOString()
795
+ };
796
+ }
797
+ };
798
+ JwtTokenRule = __decorate([Rule({
799
+ key: "jwt_token",
800
+ dependsOn: ["config_syntax_check"],
801
+ repairMode: "standard",
802
+ skipWhen: ({ hasMiaoda, deps }) => !hasMiaoda || !deps.usesMiaodaProvider
803
+ })], JwtTokenRule);
804
+ //#endregion
805
+ //#region src/rules/secrets-file.ts
806
+ let SecretsRule = class SecretsRule extends DiagnoseRule {
807
+ validate(ctx) {
808
+ const filePath = ctx.vars.secretsFilePath;
809
+ if (!filePath) return {
810
+ pass: false,
811
+ message: "secretsFilePath not set"
812
+ };
813
+ let content;
814
+ try {
815
+ content = readFile(filePath);
816
+ } catch (e) {
817
+ return {
818
+ pass: false,
819
+ message: "read secrets file failed: " + e.message
820
+ };
821
+ }
822
+ if (!content) return {
823
+ pass: false,
824
+ message: "secrets file is empty"
825
+ };
826
+ let secrets;
827
+ try {
828
+ secrets = JSON.parse(content);
829
+ } catch (e) {
830
+ return {
831
+ pass: false,
832
+ message: "unmarshal secrets failed: " + e.message
833
+ };
834
+ }
835
+ if (secrets.channels_feishu_app_secret !== ctx.vars.feishuAppSecret) return {
836
+ pass: false,
837
+ message: "channels_feishu_app_secret mismatch"
838
+ };
839
+ if (secrets.gateway_auth_token !== ctx.vars.gatewayToken) return {
840
+ pass: false,
841
+ message: "gateway_auth_token mismatch"
842
+ };
843
+ if (secrets.models_providers_miaoda_headers_x_api_key !== ctx.vars.innerAPIKey) return {
844
+ pass: false,
845
+ message: "models_providers_miaoda_headers_x_api_key mismatch"
846
+ };
847
+ return {
848
+ pass: true,
849
+ message: "secrets file content is correct"
850
+ };
851
+ }
852
+ };
853
+ SecretsRule = __decorate([Rule({
854
+ key: "secrets_file",
855
+ dependsOn: ["config_syntax_check"],
856
+ repairMode: "standard",
857
+ skipWhen: ({ hasMiaoda, deps }) => !hasMiaoda || !deps.usesMiaodaSecretProvider
858
+ })], SecretsRule);
859
+ //#endregion
860
+ //#region src/check.ts
861
+ function runCheck(input) {
862
+ const result = { failedRules: {
863
+ standard: [],
864
+ ai: [],
865
+ reset: []
866
+ } };
867
+ const disabledSet = new Set(input.disabledRules || []);
868
+ const rules = getAllRules();
869
+ const failedKeys = /* @__PURE__ */ new Set();
870
+ let configParsed = false;
871
+ let ctx = {
872
+ config: { __configPath: input.configPath },
873
+ vars: input.vars,
874
+ providerDeps: {
875
+ usesMiaodaProvider: false,
876
+ usesMiaodaSecretProvider: false
877
+ }
878
+ };
879
+ for (const rule of rules) {
880
+ const meta = rule.meta;
881
+ if (disabledSet.has(meta.key)) continue;
882
+ if (meta.dependsOn?.some((dep) => failedKeys.has(dep))) continue;
883
+ if (meta.dependsOn?.includes("config_syntax_check") && !configParsed) try {
884
+ const parsed = loadJSON5().parse(readFile(input.configPath));
885
+ const deps = analyzeProviderDeps(parsed);
886
+ ctx = {
887
+ config: parsed,
888
+ vars: input.vars,
889
+ providerDeps: deps
890
+ };
891
+ configParsed = true;
892
+ } catch {
893
+ break;
894
+ }
895
+ if (meta.skipWhen && configParsed) {
896
+ const hasMiaoda = shouldCheckMiaodaRules(ctx.config);
897
+ if (meta.skipWhen({
898
+ hasMiaoda,
899
+ deps: ctx.providerDeps
900
+ })) continue;
901
+ }
902
+ const r = rule.validate(ctx);
903
+ if (!r.pass) {
904
+ failedKeys.add(meta.key);
905
+ pushFailedRule(result, meta.repairMode, meta.key, r.message || "");
906
+ }
907
+ }
908
+ return result;
909
+ }
910
+ function pushFailedRule(result, repairMode, key, detail) {
911
+ switch (repairMode) {
912
+ case "standard":
913
+ result.failedRules.standard.push(key);
914
+ break;
915
+ case "ai":
916
+ result.failedRules.ai.push({
917
+ rule_name: key,
918
+ detail
919
+ });
920
+ break;
921
+ case "reset":
922
+ result.failedRules.reset.push({
923
+ rule_name: key,
924
+ detail
925
+ });
926
+ break;
927
+ }
928
+ }
929
+ //#endregion
930
+ //#region src/repair.ts
931
+ function runRepair(input) {
932
+ try {
933
+ const failedSet = new Set(input.failedRules || []);
934
+ const repairData = input.repairData || {};
935
+ const rules = getAllRules();
936
+ for (const rule of rules) {
937
+ if (!failedSet.has(rule.meta.key)) continue;
938
+ if (rule.meta.repairMode !== "standard") continue;
939
+ if (rule.meta.dependsOn?.includes("config_syntax_check")) continue;
940
+ rule.repair({
941
+ config: { __configPath: input.configPath },
942
+ vars: input.vars,
943
+ providerDeps: {
944
+ usesMiaodaProvider: false,
945
+ usesMiaodaSecretProvider: false
946
+ }
947
+ });
948
+ }
949
+ const JSON5 = loadJSON5();
950
+ let config;
951
+ try {
952
+ config = JSON5.parse(readFile(input.configPath));
953
+ } catch (e) {
954
+ return {
955
+ success: false,
956
+ error: "cannot parse config for repair: " + e.message
957
+ };
958
+ }
959
+ const deps = analyzeProviderDeps(config);
960
+ const ctx = {
961
+ config,
962
+ vars: input.vars,
963
+ providerDeps: deps
964
+ };
965
+ let configDirty = false;
966
+ for (const rule of rules) {
967
+ if (!failedSet.has(rule.meta.key)) continue;
968
+ if (rule.meta.repairMode !== "standard") continue;
969
+ if (!rule.meta.dependsOn?.includes("config_syntax_check")) continue;
970
+ rule.repair(ctx);
971
+ configDirty = true;
972
+ }
973
+ if (configDirty) writeFile(input.configPath, JSON.stringify(config, null, 2));
974
+ if (repairData.secretsContent && input.vars.secretsFilePath) writeFile(input.vars.secretsFilePath, repairData.secretsContent);
975
+ if (repairData.providerKeyContent && input.vars.providerFilePath) writeFile(input.vars.providerFilePath, repairData.providerKeyContent);
976
+ if (repairData.restartCommand) try {
977
+ shell(repairData.restartCommand, 3e4);
978
+ } catch (e) {
979
+ return {
980
+ success: false,
981
+ error: "restart command failed: " + e.message
982
+ };
983
+ }
984
+ return { success: true };
985
+ } catch (e) {
986
+ return {
987
+ success: false,
988
+ error: "repair failed: " + e.message
989
+ };
990
+ }
991
+ }
992
+ //#endregion
993
+ //#region src/index.ts
994
+ const args = node_process.default.argv.slice(2);
995
+ const mode = args.find((a) => !a.startsWith("--"));
996
+ const ctx = args.find((a) => a.startsWith("--ctx="))?.slice(6);
997
+ if (!mode || !["check", "repair"].includes(mode)) {
998
+ console.error("Usage: mclaw-diagnose <check|repair> --ctx=<base64>");
999
+ node_process.default.exit(1);
1000
+ }
1001
+ if (!ctx) {
1002
+ console.error("Error: --ctx=<base64> is required");
1003
+ node_process.default.exit(1);
1004
+ }
1005
+ const input = JSON.parse(Buffer.from(ctx, "base64").toString("utf-8"));
1006
+ if (mode === "check") console.log(JSON.stringify(runCheck(input)));
1007
+ else console.log(JSON.stringify(runRepair(input)));
1008
+ //#endregion
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@lark-apaas/openclaw-scripts-diagnose-cli",
3
+ "version": "0.1.1-alpha.0",
4
+ "description": "CLI for OpenClaw config diagnose and repair with JSON5 support",
5
+ "main": "dist/index.cjs",
6
+ "bin": {
7
+ "mclaw-diagnose": "./dist/index.cjs"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsdown",
14
+ "test": "vitest run",
15
+ "test:watch": "vitest",
16
+ "test:integration": "vitest run --config vitest.integration.config.ts",
17
+ "prepublishOnly": "npm run build"
18
+ },
19
+ "keywords": [
20
+ "openclaw",
21
+ "diagnose",
22
+ "cli"
23
+ ],
24
+ "license": "MIT",
25
+ "engines": {
26
+ "node": ">=18.0.0"
27
+ },
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "dependencies": {
32
+ "json5": "^2.2.3"
33
+ },
34
+ "devDependencies": {
35
+ "@types/node": "^22.0.0",
36
+ "tsdown": "^0.21.0",
37
+ "typescript": "^5.9.2",
38
+ "vitest": "^3.2.4"
39
+ }
40
+ }