clawchef 0.1.12 → 0.1.14
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 -2
- 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 +252 -51
- 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 +168 -86
- package/dist/recipe.js +39 -1
- package/dist/schema.d.ts +5 -0
- package/dist/schema.js +1 -0
- 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 +287 -54
- 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 +186 -98
- package/src/recipe.ts +47 -1
- package/src/schema.ts +1 -0
- package/src/types.ts +3 -1
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,14 +220,6 @@ function telegramGroupPolicyPath(account: string | undefined): string {
|
|
|
186
220
|
return `channels.telegram.accounts[${trimmed}].groupPolicy`;
|
|
187
221
|
}
|
|
188
222
|
|
|
189
|
-
function telegramEnabledPath(account: string | undefined): string {
|
|
190
|
-
const trimmed = account?.trim();
|
|
191
|
-
if (!trimmed) {
|
|
192
|
-
return "channels.telegram.enabled";
|
|
193
|
-
}
|
|
194
|
-
return `channels.telegram.accounts[${trimmed}].enabled`;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
223
|
function shouldAutoDisableTelegramChannel(channel: ChannelDef): boolean {
|
|
198
224
|
if (channel.channel !== "telegram") {
|
|
199
225
|
return false;
|
|
@@ -205,6 +231,177 @@ function shouldAutoDisableTelegramChannel(channel: ChannelDef): boolean {
|
|
|
205
231
|
|
|
206
232
|
type VersionMismatchChoice = "ignore" | "abort" | "force";
|
|
207
233
|
|
|
234
|
+
type JsonPatchValue = null | boolean | number | string | JsonPatchValue[] | { [key: string]: JsonPatchValue };
|
|
235
|
+
type ConfigJson = { [key: string]: unknown };
|
|
236
|
+
|
|
237
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
238
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function splitConfigPath(pathExpression: string): string[] {
|
|
242
|
+
const segments: string[] = [];
|
|
243
|
+
let token = "";
|
|
244
|
+
for (let i = 0; i < pathExpression.length; i += 1) {
|
|
245
|
+
const ch = pathExpression[i];
|
|
246
|
+
if (ch === ".") {
|
|
247
|
+
if (token) {
|
|
248
|
+
segments.push(token);
|
|
249
|
+
token = "";
|
|
250
|
+
}
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
if (ch === "[") {
|
|
254
|
+
if (token) {
|
|
255
|
+
segments.push(token);
|
|
256
|
+
token = "";
|
|
257
|
+
}
|
|
258
|
+
const end = pathExpression.indexOf("]", i + 1);
|
|
259
|
+
if (end < 0) {
|
|
260
|
+
throw new ClawChefError(`Invalid config path expression: ${pathExpression}`);
|
|
261
|
+
}
|
|
262
|
+
const bracketToken = pathExpression.slice(i + 1, end).trim();
|
|
263
|
+
if (!bracketToken) {
|
|
264
|
+
throw new ClawChefError(`Invalid empty bracket token in config path: ${pathExpression}`);
|
|
265
|
+
}
|
|
266
|
+
segments.push(bracketToken);
|
|
267
|
+
i = end;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
token += ch;
|
|
271
|
+
}
|
|
272
|
+
if (token) {
|
|
273
|
+
segments.push(token);
|
|
274
|
+
}
|
|
275
|
+
if (segments.length === 0) {
|
|
276
|
+
throw new ClawChefError(`Invalid config path expression: ${pathExpression}`);
|
|
277
|
+
}
|
|
278
|
+
return segments;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function getConfigValue(root: unknown, pathExpression: string): unknown {
|
|
282
|
+
const segments = splitConfigPath(pathExpression);
|
|
283
|
+
let cursor: unknown = root;
|
|
284
|
+
for (const segment of segments) {
|
|
285
|
+
if (!isPlainObject(cursor)) {
|
|
286
|
+
return undefined;
|
|
287
|
+
}
|
|
288
|
+
cursor = (cursor as Record<string, unknown>)[segment];
|
|
289
|
+
}
|
|
290
|
+
return cursor;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function setConfigValue(root: ConfigJson, pathExpression: string, value: unknown): void {
|
|
294
|
+
const segments = splitConfigPath(pathExpression);
|
|
295
|
+
let cursor: Record<string, unknown> = root;
|
|
296
|
+
|
|
297
|
+
for (let i = 0; i < segments.length - 1; i += 1) {
|
|
298
|
+
const segment = segments[i];
|
|
299
|
+
const existing = cursor[segment];
|
|
300
|
+
if (isPlainObject(existing)) {
|
|
301
|
+
cursor = existing;
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
const next: Record<string, unknown> = {};
|
|
305
|
+
cursor[segment] = next;
|
|
306
|
+
cursor = next;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
cursor[segments[segments.length - 1]] = value;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function deepMergeConfig(base: unknown, patch: JsonPatchValue): JsonPatchValue {
|
|
313
|
+
if (!isPlainObject(patch)) {
|
|
314
|
+
return patch;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const baseObject = isPlainObject(base) ? base : {};
|
|
318
|
+
const result: Record<string, JsonPatchValue> = {};
|
|
319
|
+
|
|
320
|
+
for (const [k, v] of Object.entries(baseObject)) {
|
|
321
|
+
result[k] = toJsonPatchValue(v, `base.${k}`);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
325
|
+
const current = result[k];
|
|
326
|
+
if (isPlainObject(v) && isPlainObject(current)) {
|
|
327
|
+
result[k] = deepMergeConfig(current, v);
|
|
328
|
+
} else {
|
|
329
|
+
result[k] = v;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return result;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function configPath(): string {
|
|
337
|
+
const fromEnv = process.env.OPENCLAW_CONFIG_PATH?.trim();
|
|
338
|
+
if (fromEnv) {
|
|
339
|
+
return fromEnv;
|
|
340
|
+
}
|
|
341
|
+
return path.join(homedir(), ".openclaw", "openclaw.json");
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async function loadConfigJson(configFilePath: string): Promise<ConfigJson> {
|
|
345
|
+
let raw: string;
|
|
346
|
+
try {
|
|
347
|
+
raw = await readFile(configFilePath, "utf8");
|
|
348
|
+
} catch (err) {
|
|
349
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
350
|
+
throw new ClawChefError(`Failed to read OpenClaw config at ${configFilePath}: ${message}`);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
355
|
+
if (!isPlainObject(parsed)) {
|
|
356
|
+
throw new ClawChefError(`OpenClaw config root must be an object: ${configFilePath}`);
|
|
357
|
+
}
|
|
358
|
+
return parsed;
|
|
359
|
+
} catch (err) {
|
|
360
|
+
if (err instanceof ClawChefError) {
|
|
361
|
+
throw err;
|
|
362
|
+
}
|
|
363
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
364
|
+
throw new ClawChefError(`Failed to parse OpenClaw config JSON at ${configFilePath}: ${message}`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async function saveConfigJson(configFilePath: string, config: ConfigJson, dryRun: boolean): Promise<void> {
|
|
369
|
+
if (dryRun) {
|
|
370
|
+
traceDebug(`CONFIG DRY-RUN write: ${configFilePath}`);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const dir = path.dirname(configFilePath);
|
|
375
|
+
await mkdir(dir, { recursive: true });
|
|
376
|
+
const tempPath = `${configFilePath}.tmp-${process.pid}-${Date.now()}`;
|
|
377
|
+
const payload = `${JSON.stringify(config, null, 2)}\n`;
|
|
378
|
+
await writeFile(tempPath, payload, "utf8");
|
|
379
|
+
await rename(tempPath, configFilePath);
|
|
380
|
+
traceDebug(`CONFIG WRITE: ${configFilePath}`);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function toJsonPatchValue(value: unknown, pathLabel: string): JsonPatchValue {
|
|
384
|
+
if (
|
|
385
|
+
value === null ||
|
|
386
|
+
typeof value === "string" ||
|
|
387
|
+
typeof value === "number" ||
|
|
388
|
+
typeof value === "boolean"
|
|
389
|
+
) {
|
|
390
|
+
return value;
|
|
391
|
+
}
|
|
392
|
+
if (Array.isArray(value)) {
|
|
393
|
+
return value.map((item, index) => toJsonPatchValue(item, `${pathLabel}[${index}]`));
|
|
394
|
+
}
|
|
395
|
+
if (isPlainObject(value)) {
|
|
396
|
+
const out: { [key: string]: JsonPatchValue } = {};
|
|
397
|
+
for (const [k, v] of Object.entries(value)) {
|
|
398
|
+
out[k] = toJsonPatchValue(v, `${pathLabel}.${k}`);
|
|
399
|
+
}
|
|
400
|
+
return out;
|
|
401
|
+
}
|
|
402
|
+
throw new ClawChefError(`openclaw.config_patch contains unsupported value at ${pathLabel}`);
|
|
403
|
+
}
|
|
404
|
+
|
|
208
405
|
async function chooseVersionMismatchAction(
|
|
209
406
|
currentVersion: string,
|
|
210
407
|
expectedVersion: string,
|
|
@@ -338,24 +535,23 @@ function isAccountLevelBinding(item: BindingItem, channel: string, account: stri
|
|
|
338
535
|
);
|
|
339
536
|
}
|
|
340
537
|
|
|
341
|
-
function
|
|
342
|
-
if (
|
|
538
|
+
function parseBindingsValue(value: unknown): BindingItem[] {
|
|
539
|
+
if (value === undefined || value === null) {
|
|
343
540
|
return [];
|
|
344
541
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
if (!Array.isArray(parsed)) {
|
|
348
|
-
throw new ClawChefError("openclaw config bindings is not an array");
|
|
349
|
-
}
|
|
350
|
-
return parsed as BindingItem[];
|
|
351
|
-
} catch (err) {
|
|
352
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
353
|
-
throw new ClawChefError(`Failed to parse openclaw bindings JSON: ${message}`);
|
|
542
|
+
if (!Array.isArray(value)) {
|
|
543
|
+
throw new ClawChefError("openclaw config bindings is not an array");
|
|
354
544
|
}
|
|
545
|
+
return value as BindingItem[];
|
|
355
546
|
}
|
|
356
547
|
|
|
357
548
|
export class CommandOpenClawProvider implements OpenClawProvider {
|
|
358
549
|
private readonly stagedMessages = new Map<string, StagedMessage[]>();
|
|
550
|
+
private readonly enabledChannelPlugins = new Set<string>();
|
|
551
|
+
|
|
552
|
+
constructor(verboseEnabled = false) {
|
|
553
|
+
TRACE_VERBOSE = verboseEnabled;
|
|
554
|
+
}
|
|
359
555
|
|
|
360
556
|
async ensureVersion(
|
|
361
557
|
config: OpenClawSection,
|
|
@@ -527,16 +723,17 @@ export class CommandOpenClawProvider implements OpenClawProvider {
|
|
|
527
723
|
|
|
528
724
|
async configureChannel(config: OpenClawSection, channel: ChannelDef, dryRun: boolean): Promise<void> {
|
|
529
725
|
const bin = config.bin ?? "openclaw";
|
|
726
|
+
const cfgPath = configPath();
|
|
530
727
|
|
|
531
728
|
if (shouldAutoDisableTelegramChannel(channel)) {
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
729
|
+
traceDebug(
|
|
730
|
+
`Skip telegram channel with empty token: ${channel.channel}${channel.account ? `/${channel.account}` : ""}`,
|
|
731
|
+
);
|
|
535
732
|
return;
|
|
536
733
|
}
|
|
537
734
|
|
|
538
735
|
const enablePluginTemplate = config.commands?.enable_plugin;
|
|
539
|
-
if (enablePluginTemplate?.trim()) {
|
|
736
|
+
if (enablePluginTemplate?.trim() && !this.enabledChannelPlugins.has(channel.channel)) {
|
|
540
737
|
const enablePluginCmd = fillTemplate(enablePluginTemplate, {
|
|
541
738
|
bin,
|
|
542
739
|
version: config.version,
|
|
@@ -546,6 +743,9 @@ export class CommandOpenClawProvider implements OpenClawProvider {
|
|
|
546
743
|
if (enablePluginCmd.trim()) {
|
|
547
744
|
await runShell(enablePluginCmd, dryRun);
|
|
548
745
|
}
|
|
746
|
+
this.enabledChannelPlugins.add(channel.channel);
|
|
747
|
+
} else if (enablePluginTemplate?.trim()) {
|
|
748
|
+
traceDebug(`Skip plugin enable for channel=${channel.channel}; already enabled in this run`);
|
|
549
749
|
}
|
|
550
750
|
|
|
551
751
|
const flags: string[] = [
|
|
@@ -593,11 +793,68 @@ export class CommandOpenClawProvider implements OpenClawProvider {
|
|
|
593
793
|
await runShell(cmd, dryRun);
|
|
594
794
|
|
|
595
795
|
if (channel.channel === "telegram" && channel.group_policy) {
|
|
596
|
-
const
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
796
|
+
const openclawConfig = await loadConfigJson(cfgPath);
|
|
797
|
+
setConfigValue(openclawConfig, telegramGroupPolicyPath(channel.account), channel.group_policy);
|
|
798
|
+
await saveConfigJson(cfgPath, openclawConfig, dryRun);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
async applyConfigPatch(config: OpenClawSection, patch: Record<string, unknown>, dryRun: boolean): Promise<void> {
|
|
803
|
+
const normalized = toJsonPatchValue(patch, "openclaw.config_patch");
|
|
804
|
+
if (!isPlainObject(normalized)) {
|
|
805
|
+
throw new ClawChefError("openclaw.config_patch must be an object");
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
const cfgPath = configPath();
|
|
809
|
+
const openclawConfig = await loadConfigJson(cfgPath);
|
|
810
|
+
const merged = deepMergeConfig(openclawConfig, normalized);
|
|
811
|
+
if (!isPlainObject(merged)) {
|
|
812
|
+
throw new ClawChefError("Merged OpenClaw config must be an object");
|
|
600
813
|
}
|
|
814
|
+
await saveConfigJson(cfgPath, merged, dryRun);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
async bindChannelAgents(config: OpenClawSection, bindingsInput: ChannelAgentBinding[], dryRun: boolean): Promise<void> {
|
|
818
|
+
if (bindingsInput.length === 0) {
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
const customTemplate = config.commands?.bind_channel_agent;
|
|
823
|
+
if (customTemplate?.trim()) {
|
|
824
|
+
for (const binding of bindingsInput) {
|
|
825
|
+
await this.bindChannelAgent(config, binding.channel, binding.agent, dryRun);
|
|
826
|
+
}
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
const cfgPath = configPath();
|
|
831
|
+
const openclawConfig = await loadConfigJson(cfgPath);
|
|
832
|
+
const bindings = parseBindingsValue(getConfigValue(openclawConfig, "bindings"));
|
|
833
|
+
|
|
834
|
+
for (const binding of bindingsInput) {
|
|
835
|
+
const account = binding.channel.account?.trim();
|
|
836
|
+
if (!account) {
|
|
837
|
+
throw new ClawChefError(`Channel ${binding.channel.channel} requires account for agent binding`);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
const nextBinding: BindingItem = {
|
|
841
|
+
agentId: binding.agent,
|
|
842
|
+
match: {
|
|
843
|
+
channel: binding.channel.channel,
|
|
844
|
+
accountId: account,
|
|
845
|
+
},
|
|
846
|
+
};
|
|
847
|
+
|
|
848
|
+
const index = bindings.findIndex((item: BindingItem) => isAccountLevelBinding(item, binding.channel.channel, account));
|
|
849
|
+
if (index >= 0) {
|
|
850
|
+
bindings[index] = nextBinding;
|
|
851
|
+
} else {
|
|
852
|
+
bindings.push(nextBinding);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
setConfigValue(openclawConfig, "bindings", bindings);
|
|
857
|
+
await saveConfigJson(cfgPath, openclawConfig, dryRun);
|
|
601
858
|
}
|
|
602
859
|
|
|
603
860
|
async bindChannelAgent(config: OpenClawSection, channel: ChannelDef, agent: string, dryRun: boolean): Promise<void> {
|
|
@@ -625,31 +882,7 @@ export class CommandOpenClawProvider implements OpenClawProvider {
|
|
|
625
882
|
return;
|
|
626
883
|
}
|
|
627
884
|
|
|
628
|
-
|
|
629
|
-
return;
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
const getCmd = `${bin} config get bindings --json 2>/dev/null || printf '[]'`;
|
|
633
|
-
const rawBindings = await runShell(getCmd, false);
|
|
634
|
-
const bindings = parseBindingsJson(rawBindings);
|
|
635
|
-
const nextBinding: BindingItem = {
|
|
636
|
-
agentId: agent,
|
|
637
|
-
match: {
|
|
638
|
-
channel: channel.channel,
|
|
639
|
-
accountId: account,
|
|
640
|
-
},
|
|
641
|
-
};
|
|
642
|
-
|
|
643
|
-
const index = bindings.findIndex((item) => isAccountLevelBinding(item, channel.channel, account));
|
|
644
|
-
if (index >= 0) {
|
|
645
|
-
bindings[index] = nextBinding;
|
|
646
|
-
} else {
|
|
647
|
-
bindings.push(nextBinding);
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
const json = JSON.stringify(bindings);
|
|
651
|
-
const setCmd = `${bin} config set bindings ${shellQuote(json)} --json`;
|
|
652
|
-
await runShell(setCmd, false);
|
|
885
|
+
await this.bindChannelAgents(config, [{ channel, agent }], dryRun);
|
|
653
886
|
}
|
|
654
887
|
|
|
655
888
|
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?(
|
|
@@ -29,6 +29,17 @@ interface RemoteOperationResponse {
|
|
|
29
29
|
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
30
30
|
const DEFAULT_OPERATION_PATH = "/v1/clawchef/operation";
|
|
31
31
|
|
|
32
|
+
function timestamp(): string {
|
|
33
|
+
const now = new Date();
|
|
34
|
+
const year = now.getFullYear();
|
|
35
|
+
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
36
|
+
const day = String(now.getDate()).padStart(2, "0");
|
|
37
|
+
const hours = String(now.getHours()).padStart(2, "0");
|
|
38
|
+
const minutes = String(now.getMinutes()).padStart(2, "0");
|
|
39
|
+
const seconds = String(now.getSeconds()).padStart(2, "0");
|
|
40
|
+
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
32
43
|
function parseResponseBody(raw: string): RemoteOperationResponse {
|
|
33
44
|
if (!raw.trim()) {
|
|
34
45
|
return {};
|
|
@@ -84,9 +95,18 @@ function assertRemoteConfig(remote: Partial<OpenClawRemoteConfig>): OpenClawRemo
|
|
|
84
95
|
export class RemoteOpenClawProvider implements OpenClawProvider {
|
|
85
96
|
private readonly stagedMessages = new Map<string, StagedMessage[]>();
|
|
86
97
|
private readonly remoteConfig: Partial<OpenClawRemoteConfig>;
|
|
98
|
+
private readonly verboseEnabled: boolean;
|
|
87
99
|
|
|
88
|
-
constructor(remoteConfig: Partial<OpenClawRemoteConfig
|
|
100
|
+
constructor(remoteConfig: Partial<OpenClawRemoteConfig>, verboseEnabled = false) {
|
|
89
101
|
this.remoteConfig = remoteConfig;
|
|
102
|
+
this.verboseEnabled = verboseEnabled;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private debug(message: string): void {
|
|
106
|
+
if (!this.verboseEnabled) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
process.stdout.write(`[${timestamp()}] [DEBUG] ${message}\n`);
|
|
90
110
|
}
|
|
91
111
|
|
|
92
112
|
private async perform(
|
|
@@ -96,6 +116,7 @@ export class RemoteOpenClawProvider implements OpenClawProvider {
|
|
|
96
116
|
dryRun: boolean,
|
|
97
117
|
): Promise<RemoteOperationResponse> {
|
|
98
118
|
if (dryRun) {
|
|
119
|
+
this.debug(`REMOTE DRY-RUN op=${operation}`);
|
|
99
120
|
return { ok: true };
|
|
100
121
|
}
|
|
101
122
|
|
|
@@ -109,6 +130,8 @@ export class RemoteOpenClawProvider implements OpenClawProvider {
|
|
|
109
130
|
recipe_version: config.version,
|
|
110
131
|
payload,
|
|
111
132
|
};
|
|
133
|
+
const startedAt = Date.now();
|
|
134
|
+
this.debug(`REMOTE START op=${operation} url=${operationUrl(remote)}`);
|
|
112
135
|
|
|
113
136
|
try {
|
|
114
137
|
const response = await fetch(operationUrl(remote), {
|
|
@@ -130,8 +153,11 @@ export class RemoteOpenClawProvider implements OpenClawProvider {
|
|
|
130
153
|
throw new ClawChefError(`Remote operation failed for ${operation}: ${parsed.message ?? "unknown error"}`);
|
|
131
154
|
}
|
|
132
155
|
|
|
156
|
+
this.debug(`REMOTE DONE op=${operation} status=${response.status} (${Date.now() - startedAt}ms)`);
|
|
157
|
+
|
|
133
158
|
return parsed;
|
|
134
159
|
} catch (err) {
|
|
160
|
+
this.debug(`REMOTE FAIL op=${operation} (${Date.now() - startedAt}ms)`);
|
|
135
161
|
if (err instanceof ClawChefError) {
|
|
136
162
|
throw err;
|
|
137
163
|
}
|
|
@@ -192,6 +218,10 @@ export class RemoteOpenClawProvider implements OpenClawProvider {
|
|
|
192
218
|
await this.perform(config, "configure_channel", { channel }, dryRun);
|
|
193
219
|
}
|
|
194
220
|
|
|
221
|
+
async applyConfigPatch(config: OpenClawSection, patch: Record<string, unknown>, dryRun: boolean): Promise<void> {
|
|
222
|
+
await this.perform(config, "apply_config_patch", { patch }, dryRun);
|
|
223
|
+
}
|
|
224
|
+
|
|
195
225
|
async bindChannelAgent(config: OpenClawSection, channel: ChannelDef, agent: string, dryRun: boolean): Promise<void> {
|
|
196
226
|
await this.perform(
|
|
197
227
|
config,
|