clawchef 0.1.11 → 0.1.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.
- package/README.md +54 -1
- package/dist/api.d.ts +1 -0
- package/dist/api.js +5 -0
- package/dist/cli.js +12 -3
- package/dist/logger.d.ts +1 -0
- package/dist/logger.js +13 -3
- package/dist/openclaw/command-provider.d.ts +5 -1
- package/dist/openclaw/command-provider.js +272 -41
- package/dist/openclaw/factory.js +2 -2
- package/dist/openclaw/mock-provider.d.ts +1 -0
- package/dist/openclaw/mock-provider.js +3 -0
- package/dist/openclaw/provider.d.ts +6 -0
- package/dist/openclaw/remote-provider.d.ts +4 -1
- package/dist/openclaw/remote-provider.js +27 -1
- package/dist/orchestrator.js +177 -82
- package/dist/recipe.js +50 -1
- package/dist/schema.d.ts +5 -0
- package/dist/schema.js +3 -2
- package/dist/types.d.ts +3 -1
- package/package.json +1 -1
- package/src/api.ts +8 -0
- package/src/cli.ts +13 -3
- package/src/logger.ts +14 -3
- package/src/openclaw/command-provider.ts +309 -43
- package/src/openclaw/factory.ts +2 -2
- package/src/openclaw/mock-provider.ts +4 -0
- package/src/openclaw/provider.ts +7 -0
- package/src/openclaw/remote-provider.ts +31 -1
- package/src/orchestrator.ts +199 -94
- package/src/recipe.ts +61 -2
- package/src/schema.ts +3 -2
- package/src/types.ts +3 -1
package/src/api.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type { ScaffoldOptions, ScaffoldResult } from "./scaffold.js";
|
|
|
12
12
|
export interface CookOptions {
|
|
13
13
|
vars?: Record<string, string>;
|
|
14
14
|
plugins?: string[];
|
|
15
|
+
filePatterns?: string[];
|
|
15
16
|
dryRun?: boolean;
|
|
16
17
|
allowMissing?: boolean;
|
|
17
18
|
verbose?: boolean;
|
|
@@ -27,6 +28,9 @@ export interface CookOptions {
|
|
|
27
28
|
|
|
28
29
|
function normalizeCookOptions(options: CookOptions): RunOptions {
|
|
29
30
|
const plugins = Array.from(new Set((options.plugins ?? []).map((value) => value.trim()).filter((value) => value.length > 0)));
|
|
31
|
+
const filePatterns = Array.from(
|
|
32
|
+
new Set((options.filePatterns ?? []).map((value) => value.trim()).filter((value) => value.length > 0)),
|
|
33
|
+
);
|
|
30
34
|
const scope = options.scope ?? "full";
|
|
31
35
|
const workspaceName = options.workspaceName?.trim() || undefined;
|
|
32
36
|
if (scope === "workspace" && !workspaceName) {
|
|
@@ -35,9 +39,13 @@ function normalizeCookOptions(options: CookOptions): RunOptions {
|
|
|
35
39
|
if (scope !== "workspace" && workspaceName) {
|
|
36
40
|
throw new ClawChefError("workspaceName is only allowed when scope=workspace");
|
|
37
41
|
}
|
|
42
|
+
if (scope !== "files" && filePatterns.length > 0) {
|
|
43
|
+
throw new ClawChefError("filePatterns is only allowed when scope=files");
|
|
44
|
+
}
|
|
38
45
|
return {
|
|
39
46
|
vars: options.vars ?? {},
|
|
40
47
|
plugins,
|
|
48
|
+
filePatterns,
|
|
41
49
|
scope,
|
|
42
50
|
workspaceName,
|
|
43
51
|
gatewayMode: options.gatewayMode ?? "service",
|
package/src/cli.ts
CHANGED
|
@@ -45,6 +45,10 @@ function parsePluginFlags(values: string[]): string[] {
|
|
|
45
45
|
return Array.from(new Set(values.map((value) => value.trim()).filter((value) => value.length > 0)));
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
function parseFileFlags(values: string[]): string[] {
|
|
49
|
+
return Array.from(new Set(values.map((value) => value.trim()).filter((value) => value.length > 0)));
|
|
50
|
+
}
|
|
51
|
+
|
|
48
52
|
function readEnv(name: string): string | undefined {
|
|
49
53
|
const value = process.env[name];
|
|
50
54
|
if (value === undefined) {
|
|
@@ -62,10 +66,10 @@ function parseProvider(value: string): "command" | "mock" | "remote" {
|
|
|
62
66
|
}
|
|
63
67
|
|
|
64
68
|
function parseScope(value: string): RunScope {
|
|
65
|
-
if (value === "full" || value === "files" || value === "workspace") {
|
|
69
|
+
if (value === "full" || value === "stateful" || value === "files" || value === "workspace") {
|
|
66
70
|
return value;
|
|
67
71
|
}
|
|
68
|
-
throw new ClawChefError(`Invalid --scope value: ${value}. Expected full, files, or workspace`);
|
|
72
|
+
throw new ClawChefError(`Invalid --scope value: ${value}. Expected full, stateful, files, or workspace`);
|
|
69
73
|
}
|
|
70
74
|
|
|
71
75
|
function parseGatewayMode(value: string): GatewayMode {
|
|
@@ -117,7 +121,8 @@ export function buildCli(): Command {
|
|
|
117
121
|
.option("--allow-missing", "Allow unresolved template variables", false)
|
|
118
122
|
.option("--verbose", "Verbose logging", false)
|
|
119
123
|
.option("-s, --silent", "Skip reset confirmation prompt", false)
|
|
120
|
-
.option("--scope <scope>", "Run scope: full | files | workspace", "full")
|
|
124
|
+
.option("--scope <scope>", "Run scope: full | stateful | files | workspace", "full")
|
|
125
|
+
.option("--file <pattern>", "File pattern filter (only with --scope files, repeatable)", (v, p: string[]) => p.concat([v]), [])
|
|
121
126
|
.option("--workspace <name>", "Workspace name (required when --scope workspace)")
|
|
122
127
|
.option("--gateway-mode <mode>", "Gateway mode: service | run | none", "service")
|
|
123
128
|
.option("--dotenv-ref <path-or-url>", "Load env vars from local file or HTTP URL")
|
|
@@ -139,6 +144,7 @@ export function buildCli(): Command {
|
|
|
139
144
|
const provider = parseProvider(opts.provider ?? readEnv("CLAWCHEF_PROVIDER") ?? "command");
|
|
140
145
|
const scope = parseScope(String(opts.scope ?? "full"));
|
|
141
146
|
const gatewayMode = parseGatewayMode(String(opts.gatewayMode ?? "service"));
|
|
147
|
+
const filePatterns = parseFileFlags(opts.file);
|
|
142
148
|
const workspaceName = opts.workspace?.trim() ? String(opts.workspace).trim() : undefined;
|
|
143
149
|
if (scope === "workspace" && !workspaceName) {
|
|
144
150
|
throw new ClawChefError("--scope workspace requires --workspace <name>");
|
|
@@ -146,9 +152,13 @@ export function buildCli(): Command {
|
|
|
146
152
|
if (scope !== "workspace" && workspaceName) {
|
|
147
153
|
throw new ClawChefError("--workspace is only allowed when --scope workspace");
|
|
148
154
|
}
|
|
155
|
+
if (scope !== "files" && filePatterns.length > 0) {
|
|
156
|
+
throw new ClawChefError("--file is only allowed when --scope files");
|
|
157
|
+
}
|
|
149
158
|
const options: RunOptions = {
|
|
150
159
|
vars: parseVarFlags(opts.var),
|
|
151
160
|
plugins: parsePluginFlags(opts.plugin),
|
|
161
|
+
filePatterns,
|
|
152
162
|
scope,
|
|
153
163
|
workspaceName,
|
|
154
164
|
gatewayMode,
|
package/src/logger.ts
CHANGED
|
@@ -1,17 +1,28 @@
|
|
|
1
1
|
export class Logger {
|
|
2
2
|
constructor(private readonly verboseEnabled: boolean) {}
|
|
3
3
|
|
|
4
|
+
private timestamp(): string {
|
|
5
|
+
const now = new Date();
|
|
6
|
+
const year = now.getFullYear();
|
|
7
|
+
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
8
|
+
const day = String(now.getDate()).padStart(2, "0");
|
|
9
|
+
const hours = String(now.getHours()).padStart(2, "0");
|
|
10
|
+
const minutes = String(now.getMinutes()).padStart(2, "0");
|
|
11
|
+
const seconds = String(now.getSeconds()).padStart(2, "0");
|
|
12
|
+
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
4
15
|
info(message: string): void {
|
|
5
|
-
process.stdout.write(`[INFO] ${message}\n`);
|
|
16
|
+
process.stdout.write(`[${this.timestamp()}] [INFO] ${message}\n`);
|
|
6
17
|
}
|
|
7
18
|
|
|
8
19
|
warn(message: string): void {
|
|
9
|
-
process.stdout.write(`[WARN] ${message}\n`);
|
|
20
|
+
process.stdout.write(`[${this.timestamp()}] [WARN] ${message}\n`);
|
|
10
21
|
}
|
|
11
22
|
|
|
12
23
|
debug(message: string): void {
|
|
13
24
|
if (this.verboseEnabled) {
|
|
14
|
-
process.stdout.write(`[DEBUG] ${message}\n`);
|
|
25
|
+
process.stdout.write(`[${this.timestamp()}] [DEBUG] ${message}\n`);
|
|
15
26
|
}
|
|
16
27
|
}
|
|
17
28
|
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { homedir, tmpdir } from "node:os";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import process from "node:process";
|
|
4
|
-
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { mkdtemp, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
6
|
import { createInterface } from "node:readline/promises";
|
|
7
7
|
import { stdin as input, stdout as output } from "node:process";
|
|
8
8
|
import { ClawChefError } from "../errors.js";
|
|
9
9
|
import type { AgentDef, ChannelDef, ConversationDef, GatewayMode, OpenClawBootstrap, OpenClawSection } from "../types.js";
|
|
10
|
-
import type { EnsureVersionResult, OpenClawProvider, ResolvedWorkspaceDef } from "./provider.js";
|
|
10
|
+
import type { ChannelAgentBinding, EnsureVersionResult, OpenClawProvider, ResolvedWorkspaceDef } from "./provider.js";
|
|
11
11
|
|
|
12
12
|
const DEFAULT_COMMANDS = {
|
|
13
13
|
use_version: "${bin} --version",
|
|
@@ -50,6 +50,26 @@ interface BindingItem {
|
|
|
50
50
|
const SECRET_FLAG_RE =
|
|
51
51
|
/(--[A-Za-z0-9-]*(?:api-key|token|password|secret)[A-Za-z0-9-]*\s+)(?:'[^']*'|"[^"]*"|\S+)/g;
|
|
52
52
|
|
|
53
|
+
let TRACE_VERBOSE = false;
|
|
54
|
+
|
|
55
|
+
function timestamp(): string {
|
|
56
|
+
const now = new Date();
|
|
57
|
+
const year = now.getFullYear();
|
|
58
|
+
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
59
|
+
const day = String(now.getDate()).padStart(2, "0");
|
|
60
|
+
const hours = String(now.getHours()).padStart(2, "0");
|
|
61
|
+
const minutes = String(now.getMinutes()).padStart(2, "0");
|
|
62
|
+
const seconds = String(now.getSeconds()).padStart(2, "0");
|
|
63
|
+
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function traceDebug(message: string): void {
|
|
67
|
+
if (!TRACE_VERBOSE) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
process.stdout.write(`[${timestamp()}] [DEBUG] ${message}\n`);
|
|
71
|
+
}
|
|
72
|
+
|
|
53
73
|
type BootstrapStringField =
|
|
54
74
|
| "cloudflare_ai_gateway_account_id"
|
|
55
75
|
| "cloudflare_ai_gateway_gateway_id"
|
|
@@ -111,10 +131,15 @@ async function commandExists(bin: string): Promise<boolean> {
|
|
|
111
131
|
}
|
|
112
132
|
|
|
113
133
|
async function runShell(command: string, dryRun: boolean, extraEnv?: Record<string, string>): Promise<string> {
|
|
134
|
+
const sanitized = sanitizeCommand(command);
|
|
114
135
|
if (dryRun) {
|
|
136
|
+
traceDebug(`CMD DRY-RUN: ${sanitized}`);
|
|
115
137
|
return "";
|
|
116
138
|
}
|
|
117
139
|
|
|
140
|
+
const startedAt = Date.now();
|
|
141
|
+
traceDebug(`CMD START: ${sanitized}`);
|
|
142
|
+
|
|
118
143
|
return new Promise<string>((resolve, reject) => {
|
|
119
144
|
const child = spawn(command, {
|
|
120
145
|
shell: true,
|
|
@@ -135,19 +160,26 @@ async function runShell(command: string, dryRun: boolean, extraEnv?: Record<stri
|
|
|
135
160
|
});
|
|
136
161
|
child.on("close", (code) => {
|
|
137
162
|
if (code === 0) {
|
|
163
|
+
traceDebug(`CMD DONE (${Date.now() - startedAt}ms): ${sanitized}`);
|
|
138
164
|
resolve(stdout.trim());
|
|
139
165
|
return;
|
|
140
166
|
}
|
|
167
|
+
traceDebug(`CMD FAIL (${Date.now() - startedAt}ms) code=${String(code)}: ${sanitized}`);
|
|
141
168
|
reject(new ClawChefError(`Command failed (${code}): ${sanitizeCommand(command)}\n${stderr.trim()}`));
|
|
142
169
|
});
|
|
143
170
|
});
|
|
144
171
|
}
|
|
145
172
|
|
|
146
173
|
async function runShellInteractive(command: string, dryRun: boolean): Promise<void> {
|
|
174
|
+
const sanitized = sanitizeCommand(command);
|
|
147
175
|
if (dryRun) {
|
|
176
|
+
traceDebug(`CMD DRY-RUN (interactive): ${sanitized}`);
|
|
148
177
|
return;
|
|
149
178
|
}
|
|
150
179
|
|
|
180
|
+
const startedAt = Date.now();
|
|
181
|
+
traceDebug(`CMD START (interactive): ${sanitized}`);
|
|
182
|
+
|
|
151
183
|
return new Promise<void>((resolve, reject) => {
|
|
152
184
|
const child = spawn(command, {
|
|
153
185
|
shell: true,
|
|
@@ -160,9 +192,11 @@ async function runShellInteractive(command: string, dryRun: boolean): Promise<vo
|
|
|
160
192
|
});
|
|
161
193
|
child.on("close", (code) => {
|
|
162
194
|
if (code === 0) {
|
|
195
|
+
traceDebug(`CMD DONE (interactive, ${Date.now() - startedAt}ms): ${sanitized}`);
|
|
163
196
|
resolve();
|
|
164
197
|
return;
|
|
165
198
|
}
|
|
199
|
+
traceDebug(`CMD FAIL (interactive, ${Date.now() - startedAt}ms) code=${String(code)}: ${sanitized}`);
|
|
166
200
|
reject(new ClawChefError(`Command failed (${code}): ${sanitizeCommand(command)}`));
|
|
167
201
|
});
|
|
168
202
|
});
|
|
@@ -186,8 +220,196 @@ function telegramGroupPolicyPath(account: string | undefined): string {
|
|
|
186
220
|
return `channels.telegram.accounts[${trimmed}].groupPolicy`;
|
|
187
221
|
}
|
|
188
222
|
|
|
223
|
+
function telegramEnabledPath(account: string | undefined): string {
|
|
224
|
+
const trimmed = account?.trim();
|
|
225
|
+
if (!trimmed) {
|
|
226
|
+
return "channels.telegram.enabled";
|
|
227
|
+
}
|
|
228
|
+
return `channels.telegram.accounts[${trimmed}].enabled`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function shouldAutoDisableTelegramChannel(channel: ChannelDef): boolean {
|
|
232
|
+
if (channel.channel !== "telegram") {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
const emptyToken = channel.token !== undefined && channel.token.trim().length === 0;
|
|
236
|
+
const emptyBotToken = channel.bot_token !== undefined && channel.bot_token.trim().length === 0;
|
|
237
|
+
return emptyToken || emptyBotToken;
|
|
238
|
+
}
|
|
239
|
+
|
|
189
240
|
type VersionMismatchChoice = "ignore" | "abort" | "force";
|
|
190
241
|
|
|
242
|
+
type JsonPatchValue = null | boolean | number | string | JsonPatchValue[] | { [key: string]: JsonPatchValue };
|
|
243
|
+
type ConfigJson = { [key: string]: unknown };
|
|
244
|
+
|
|
245
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
246
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function splitConfigPath(pathExpression: string): string[] {
|
|
250
|
+
const segments: string[] = [];
|
|
251
|
+
let token = "";
|
|
252
|
+
for (let i = 0; i < pathExpression.length; i += 1) {
|
|
253
|
+
const ch = pathExpression[i];
|
|
254
|
+
if (ch === ".") {
|
|
255
|
+
if (token) {
|
|
256
|
+
segments.push(token);
|
|
257
|
+
token = "";
|
|
258
|
+
}
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (ch === "[") {
|
|
262
|
+
if (token) {
|
|
263
|
+
segments.push(token);
|
|
264
|
+
token = "";
|
|
265
|
+
}
|
|
266
|
+
const end = pathExpression.indexOf("]", i + 1);
|
|
267
|
+
if (end < 0) {
|
|
268
|
+
throw new ClawChefError(`Invalid config path expression: ${pathExpression}`);
|
|
269
|
+
}
|
|
270
|
+
const bracketToken = pathExpression.slice(i + 1, end).trim();
|
|
271
|
+
if (!bracketToken) {
|
|
272
|
+
throw new ClawChefError(`Invalid empty bracket token in config path: ${pathExpression}`);
|
|
273
|
+
}
|
|
274
|
+
segments.push(bracketToken);
|
|
275
|
+
i = end;
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
token += ch;
|
|
279
|
+
}
|
|
280
|
+
if (token) {
|
|
281
|
+
segments.push(token);
|
|
282
|
+
}
|
|
283
|
+
if (segments.length === 0) {
|
|
284
|
+
throw new ClawChefError(`Invalid config path expression: ${pathExpression}`);
|
|
285
|
+
}
|
|
286
|
+
return segments;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function getConfigValue(root: unknown, pathExpression: string): unknown {
|
|
290
|
+
const segments = splitConfigPath(pathExpression);
|
|
291
|
+
let cursor: unknown = root;
|
|
292
|
+
for (const segment of segments) {
|
|
293
|
+
if (!isPlainObject(cursor)) {
|
|
294
|
+
return undefined;
|
|
295
|
+
}
|
|
296
|
+
cursor = (cursor as Record<string, unknown>)[segment];
|
|
297
|
+
}
|
|
298
|
+
return cursor;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function setConfigValue(root: ConfigJson, pathExpression: string, value: unknown): void {
|
|
302
|
+
const segments = splitConfigPath(pathExpression);
|
|
303
|
+
let cursor: Record<string, unknown> = root;
|
|
304
|
+
|
|
305
|
+
for (let i = 0; i < segments.length - 1; i += 1) {
|
|
306
|
+
const segment = segments[i];
|
|
307
|
+
const existing = cursor[segment];
|
|
308
|
+
if (isPlainObject(existing)) {
|
|
309
|
+
cursor = existing;
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
const next: Record<string, unknown> = {};
|
|
313
|
+
cursor[segment] = next;
|
|
314
|
+
cursor = next;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
cursor[segments[segments.length - 1]] = value;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function deepMergeConfig(base: unknown, patch: JsonPatchValue): JsonPatchValue {
|
|
321
|
+
if (!isPlainObject(patch)) {
|
|
322
|
+
return patch;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const baseObject = isPlainObject(base) ? base : {};
|
|
326
|
+
const result: Record<string, JsonPatchValue> = {};
|
|
327
|
+
|
|
328
|
+
for (const [k, v] of Object.entries(baseObject)) {
|
|
329
|
+
result[k] = toJsonPatchValue(v, `base.${k}`);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
333
|
+
const current = result[k];
|
|
334
|
+
if (isPlainObject(v) && isPlainObject(current)) {
|
|
335
|
+
result[k] = deepMergeConfig(current, v);
|
|
336
|
+
} else {
|
|
337
|
+
result[k] = v;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return result;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function configPath(): string {
|
|
345
|
+
const fromEnv = process.env.OPENCLAW_CONFIG_PATH?.trim();
|
|
346
|
+
if (fromEnv) {
|
|
347
|
+
return fromEnv;
|
|
348
|
+
}
|
|
349
|
+
return path.join(homedir(), ".openclaw", "openclaw.json");
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async function loadConfigJson(configFilePath: string): Promise<ConfigJson> {
|
|
353
|
+
let raw: string;
|
|
354
|
+
try {
|
|
355
|
+
raw = await readFile(configFilePath, "utf8");
|
|
356
|
+
} catch (err) {
|
|
357
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
358
|
+
throw new ClawChefError(`Failed to read OpenClaw config at ${configFilePath}: ${message}`);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
try {
|
|
362
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
363
|
+
if (!isPlainObject(parsed)) {
|
|
364
|
+
throw new ClawChefError(`OpenClaw config root must be an object: ${configFilePath}`);
|
|
365
|
+
}
|
|
366
|
+
return parsed;
|
|
367
|
+
} catch (err) {
|
|
368
|
+
if (err instanceof ClawChefError) {
|
|
369
|
+
throw err;
|
|
370
|
+
}
|
|
371
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
372
|
+
throw new ClawChefError(`Failed to parse OpenClaw config JSON at ${configFilePath}: ${message}`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async function saveConfigJson(configFilePath: string, config: ConfigJson, dryRun: boolean): Promise<void> {
|
|
377
|
+
if (dryRun) {
|
|
378
|
+
traceDebug(`CONFIG DRY-RUN write: ${configFilePath}`);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const dir = path.dirname(configFilePath);
|
|
383
|
+
await mkdir(dir, { recursive: true });
|
|
384
|
+
const tempPath = `${configFilePath}.tmp-${process.pid}-${Date.now()}`;
|
|
385
|
+
const payload = `${JSON.stringify(config, null, 2)}\n`;
|
|
386
|
+
await writeFile(tempPath, payload, "utf8");
|
|
387
|
+
await rename(tempPath, configFilePath);
|
|
388
|
+
traceDebug(`CONFIG WRITE: ${configFilePath}`);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function toJsonPatchValue(value: unknown, pathLabel: string): JsonPatchValue {
|
|
392
|
+
if (
|
|
393
|
+
value === null ||
|
|
394
|
+
typeof value === "string" ||
|
|
395
|
+
typeof value === "number" ||
|
|
396
|
+
typeof value === "boolean"
|
|
397
|
+
) {
|
|
398
|
+
return value;
|
|
399
|
+
}
|
|
400
|
+
if (Array.isArray(value)) {
|
|
401
|
+
return value.map((item, index) => toJsonPatchValue(item, `${pathLabel}[${index}]`));
|
|
402
|
+
}
|
|
403
|
+
if (isPlainObject(value)) {
|
|
404
|
+
const out: { [key: string]: JsonPatchValue } = {};
|
|
405
|
+
for (const [k, v] of Object.entries(value)) {
|
|
406
|
+
out[k] = toJsonPatchValue(v, `${pathLabel}.${k}`);
|
|
407
|
+
}
|
|
408
|
+
return out;
|
|
409
|
+
}
|
|
410
|
+
throw new ClawChefError(`openclaw.config_patch contains unsupported value at ${pathLabel}`);
|
|
411
|
+
}
|
|
412
|
+
|
|
191
413
|
async function chooseVersionMismatchAction(
|
|
192
414
|
currentVersion: string,
|
|
193
415
|
expectedVersion: string,
|
|
@@ -321,24 +543,23 @@ function isAccountLevelBinding(item: BindingItem, channel: string, account: stri
|
|
|
321
543
|
);
|
|
322
544
|
}
|
|
323
545
|
|
|
324
|
-
function
|
|
325
|
-
if (
|
|
546
|
+
function parseBindingsValue(value: unknown): BindingItem[] {
|
|
547
|
+
if (value === undefined || value === null) {
|
|
326
548
|
return [];
|
|
327
549
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
if (!Array.isArray(parsed)) {
|
|
331
|
-
throw new ClawChefError("openclaw config bindings is not an array");
|
|
332
|
-
}
|
|
333
|
-
return parsed as BindingItem[];
|
|
334
|
-
} catch (err) {
|
|
335
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
336
|
-
throw new ClawChefError(`Failed to parse openclaw bindings JSON: ${message}`);
|
|
550
|
+
if (!Array.isArray(value)) {
|
|
551
|
+
throw new ClawChefError("openclaw config bindings is not an array");
|
|
337
552
|
}
|
|
553
|
+
return value as BindingItem[];
|
|
338
554
|
}
|
|
339
555
|
|
|
340
556
|
export class CommandOpenClawProvider implements OpenClawProvider {
|
|
341
557
|
private readonly stagedMessages = new Map<string, StagedMessage[]>();
|
|
558
|
+
private readonly enabledChannelPlugins = new Set<string>();
|
|
559
|
+
|
|
560
|
+
constructor(verboseEnabled = false) {
|
|
561
|
+
TRACE_VERBOSE = verboseEnabled;
|
|
562
|
+
}
|
|
342
563
|
|
|
343
564
|
async ensureVersion(
|
|
344
565
|
config: OpenClawSection,
|
|
@@ -510,8 +731,17 @@ export class CommandOpenClawProvider implements OpenClawProvider {
|
|
|
510
731
|
|
|
511
732
|
async configureChannel(config: OpenClawSection, channel: ChannelDef, dryRun: boolean): Promise<void> {
|
|
512
733
|
const bin = config.bin ?? "openclaw";
|
|
734
|
+
const cfgPath = configPath();
|
|
735
|
+
|
|
736
|
+
if (shouldAutoDisableTelegramChannel(channel)) {
|
|
737
|
+
const openclawConfig = await loadConfigJson(cfgPath);
|
|
738
|
+
setConfigValue(openclawConfig, telegramEnabledPath(channel.account), false);
|
|
739
|
+
await saveConfigJson(cfgPath, openclawConfig, dryRun);
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
|
|
513
743
|
const enablePluginTemplate = config.commands?.enable_plugin;
|
|
514
|
-
if (enablePluginTemplate?.trim()) {
|
|
744
|
+
if (enablePluginTemplate?.trim() && !this.enabledChannelPlugins.has(channel.channel)) {
|
|
515
745
|
const enablePluginCmd = fillTemplate(enablePluginTemplate, {
|
|
516
746
|
bin,
|
|
517
747
|
version: config.version,
|
|
@@ -521,6 +751,9 @@ export class CommandOpenClawProvider implements OpenClawProvider {
|
|
|
521
751
|
if (enablePluginCmd.trim()) {
|
|
522
752
|
await runShell(enablePluginCmd, dryRun);
|
|
523
753
|
}
|
|
754
|
+
this.enabledChannelPlugins.add(channel.channel);
|
|
755
|
+
} else if (enablePluginTemplate?.trim()) {
|
|
756
|
+
traceDebug(`Skip plugin enable for channel=${channel.channel}; already enabled in this run`);
|
|
524
757
|
}
|
|
525
758
|
|
|
526
759
|
const flags: string[] = [
|
|
@@ -568,13 +801,70 @@ export class CommandOpenClawProvider implements OpenClawProvider {
|
|
|
568
801
|
await runShell(cmd, dryRun);
|
|
569
802
|
|
|
570
803
|
if (channel.channel === "telegram" && channel.group_policy) {
|
|
571
|
-
const
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
await runShell(setPolicyCmd, dryRun);
|
|
804
|
+
const openclawConfig = await loadConfigJson(cfgPath);
|
|
805
|
+
setConfigValue(openclawConfig, telegramGroupPolicyPath(channel.account), channel.group_policy);
|
|
806
|
+
await saveConfigJson(cfgPath, openclawConfig, dryRun);
|
|
575
807
|
}
|
|
576
808
|
}
|
|
577
809
|
|
|
810
|
+
async applyConfigPatch(config: OpenClawSection, patch: Record<string, unknown>, dryRun: boolean): Promise<void> {
|
|
811
|
+
const normalized = toJsonPatchValue(patch, "openclaw.config_patch");
|
|
812
|
+
if (!isPlainObject(normalized)) {
|
|
813
|
+
throw new ClawChefError("openclaw.config_patch must be an object");
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
const cfgPath = configPath();
|
|
817
|
+
const openclawConfig = await loadConfigJson(cfgPath);
|
|
818
|
+
const merged = deepMergeConfig(openclawConfig, normalized);
|
|
819
|
+
if (!isPlainObject(merged)) {
|
|
820
|
+
throw new ClawChefError("Merged OpenClaw config must be an object");
|
|
821
|
+
}
|
|
822
|
+
await saveConfigJson(cfgPath, merged, dryRun);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
async bindChannelAgents(config: OpenClawSection, bindingsInput: ChannelAgentBinding[], dryRun: boolean): Promise<void> {
|
|
826
|
+
if (bindingsInput.length === 0) {
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
const customTemplate = config.commands?.bind_channel_agent;
|
|
831
|
+
if (customTemplate?.trim()) {
|
|
832
|
+
for (const binding of bindingsInput) {
|
|
833
|
+
await this.bindChannelAgent(config, binding.channel, binding.agent, dryRun);
|
|
834
|
+
}
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
const cfgPath = configPath();
|
|
839
|
+
const openclawConfig = await loadConfigJson(cfgPath);
|
|
840
|
+
const bindings = parseBindingsValue(getConfigValue(openclawConfig, "bindings"));
|
|
841
|
+
|
|
842
|
+
for (const binding of bindingsInput) {
|
|
843
|
+
const account = binding.channel.account?.trim();
|
|
844
|
+
if (!account) {
|
|
845
|
+
throw new ClawChefError(`Channel ${binding.channel.channel} requires account for agent binding`);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
const nextBinding: BindingItem = {
|
|
849
|
+
agentId: binding.agent,
|
|
850
|
+
match: {
|
|
851
|
+
channel: binding.channel.channel,
|
|
852
|
+
accountId: account,
|
|
853
|
+
},
|
|
854
|
+
};
|
|
855
|
+
|
|
856
|
+
const index = bindings.findIndex((item: BindingItem) => isAccountLevelBinding(item, binding.channel.channel, account));
|
|
857
|
+
if (index >= 0) {
|
|
858
|
+
bindings[index] = nextBinding;
|
|
859
|
+
} else {
|
|
860
|
+
bindings.push(nextBinding);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
setConfigValue(openclawConfig, "bindings", bindings);
|
|
865
|
+
await saveConfigJson(cfgPath, openclawConfig, dryRun);
|
|
866
|
+
}
|
|
867
|
+
|
|
578
868
|
async bindChannelAgent(config: OpenClawSection, channel: ChannelDef, agent: string, dryRun: boolean): Promise<void> {
|
|
579
869
|
const account = channel.account?.trim();
|
|
580
870
|
if (!account) {
|
|
@@ -600,31 +890,7 @@ export class CommandOpenClawProvider implements OpenClawProvider {
|
|
|
600
890
|
return;
|
|
601
891
|
}
|
|
602
892
|
|
|
603
|
-
|
|
604
|
-
return;
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
const getCmd = `${bin} config get bindings --json 2>/dev/null || printf '[]'`;
|
|
608
|
-
const rawBindings = await runShell(getCmd, false);
|
|
609
|
-
const bindings = parseBindingsJson(rawBindings);
|
|
610
|
-
const nextBinding: BindingItem = {
|
|
611
|
-
agentId: agent,
|
|
612
|
-
match: {
|
|
613
|
-
channel: channel.channel,
|
|
614
|
-
accountId: account,
|
|
615
|
-
},
|
|
616
|
-
};
|
|
617
|
-
|
|
618
|
-
const index = bindings.findIndex((item) => isAccountLevelBinding(item, channel.channel, account));
|
|
619
|
-
if (index >= 0) {
|
|
620
|
-
bindings[index] = nextBinding;
|
|
621
|
-
} else {
|
|
622
|
-
bindings.push(nextBinding);
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
const json = JSON.stringify(bindings);
|
|
626
|
-
const setCmd = `${bin} config set bindings ${shellQuote(json)} --json`;
|
|
627
|
-
await runShell(setCmd, false);
|
|
893
|
+
await this.bindChannelAgents(config, [{ channel, agent }], dryRun);
|
|
628
894
|
}
|
|
629
895
|
|
|
630
896
|
async loginChannel(config: OpenClawSection, channel: ChannelDef, dryRun: boolean): Promise<void> {
|
package/src/openclaw/factory.ts
CHANGED
|
@@ -10,7 +10,7 @@ export function createProvider(options: RunOptions): OpenClawProvider {
|
|
|
10
10
|
return new MockOpenClawProvider();
|
|
11
11
|
}
|
|
12
12
|
if (provider === "remote") {
|
|
13
|
-
return new RemoteOpenClawProvider(options.remote);
|
|
13
|
+
return new RemoteOpenClawProvider(options.remote, options.verbose);
|
|
14
14
|
}
|
|
15
|
-
return new CommandOpenClawProvider();
|
|
15
|
+
return new CommandOpenClawProvider(options.verbose);
|
|
16
16
|
}
|
|
@@ -69,6 +69,10 @@ export class MockOpenClawProvider implements OpenClawProvider {
|
|
|
69
69
|
this.state.channels.add(`${channel.channel}::${channel.account ?? "default"}`);
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
async applyConfigPatch(_config: OpenClawSection, _patch: Record<string, unknown>, _dryRun: boolean): Promise<void> {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
72
76
|
async bindChannelAgent(
|
|
73
77
|
_config: OpenClawSection,
|
|
74
78
|
_channel: ChannelDef,
|
package/src/openclaw/provider.ts
CHANGED
|
@@ -6,6 +6,11 @@ export interface EnsureVersionResult {
|
|
|
6
6
|
installedThisRun: boolean;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
export interface ChannelAgentBinding {
|
|
10
|
+
channel: ChannelDef;
|
|
11
|
+
agent: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
9
14
|
export interface OpenClawProvider {
|
|
10
15
|
ensureVersion(
|
|
11
16
|
config: OpenClawSection,
|
|
@@ -18,6 +23,8 @@ export interface OpenClawProvider {
|
|
|
18
23
|
startGateway(config: OpenClawSection, mode: GatewayMode, dryRun: boolean): Promise<void>;
|
|
19
24
|
createWorkspace(config: OpenClawSection, workspace: ResolvedWorkspaceDef, dryRun: boolean): Promise<void>;
|
|
20
25
|
configureChannel(config: OpenClawSection, channel: ChannelDef, dryRun: boolean): Promise<void>;
|
|
26
|
+
applyConfigPatch(config: OpenClawSection, patch: Record<string, unknown>, dryRun: boolean): Promise<void>;
|
|
27
|
+
bindChannelAgents?(config: OpenClawSection, bindings: ChannelAgentBinding[], dryRun: boolean): Promise<void>;
|
|
21
28
|
bindChannelAgent(config: OpenClawSection, channel: ChannelDef, agent: string, dryRun: boolean): Promise<void>;
|
|
22
29
|
loginChannel(config: OpenClawSection, channel: ChannelDef, dryRun: boolean): Promise<void>;
|
|
23
30
|
materializeFile?(
|