clawchef 0.1.6 → 0.1.8

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/src/recipe.ts CHANGED
@@ -32,24 +32,18 @@ export interface LoadedRecipeText {
32
32
  }
33
33
 
34
34
  const AUTH_CHOICE_TO_FIELD: Record<string, string> = {
35
- "openai-api-key": "openai_api_key",
36
- "anthropic-api-key": "anthropic_api_key",
37
- "openrouter-api-key": "openrouter_api_key",
38
- "xai-api-key": "xai_api_key",
39
- "gemini-api-key": "gemini_api_key",
40
- "ai-gateway-api-key": "ai_gateway_api_key",
41
- "cloudflare-ai-gateway-api-key": "cloudflare_ai_gateway_api_key",
35
+ "openai-api-key": "llm_api_key",
36
+ "anthropic-api-key": "llm_api_key",
37
+ "openrouter-api-key": "llm_api_key",
38
+ "xai-api-key": "llm_api_key",
39
+ "gemini-api-key": "llm_api_key",
40
+ "ai-gateway-api-key": "llm_api_key",
41
+ "cloudflare-ai-gateway-api-key": "llm_api_key",
42
42
  token: "token",
43
43
  };
44
44
 
45
45
  const SECRET_BOOTSTRAP_FIELDS = [
46
- "openai_api_key",
47
- "anthropic_api_key",
48
- "openrouter_api_key",
49
- "xai_api_key",
50
- "gemini_api_key",
51
- "ai_gateway_api_key",
52
- "cloudflare_ai_gateway_api_key",
46
+ "llm_api_key",
53
47
  "token",
54
48
  ] as const;
55
49
 
@@ -124,7 +118,7 @@ function assertNoInlineSecrets(recipe: Recipe): void {
124
118
  }
125
119
  }
126
120
 
127
- function collectVars(recipe: Recipe, cliVars: Record<string, string>): Record<string, string> {
121
+ function collectVars(recipe: Recipe, cliVars: Record<string, string>, requiredKeys?: Set<string>): Record<string, string> {
128
122
  const vars: Record<string, string> = {};
129
123
  const params = recipe.params ?? {};
130
124
 
@@ -155,7 +149,7 @@ function collectVars(recipe: Recipe, cliVars: Record<string, string>): Record<st
155
149
  vars[key] = def.default;
156
150
  continue;
157
151
  }
158
- if (def.required) {
152
+ if (def.required && (requiredKeys === undefined || requiredKeys.has(key))) {
159
153
  throw new ClawChefError(`Parameter ${key} is required but was not provided via --var or environment`);
160
154
  }
161
155
  }
@@ -167,21 +161,55 @@ function collectVars(recipe: Recipe, cliVars: Record<string, string>): Record<st
167
161
  return vars;
168
162
  }
169
163
 
164
+ function projectRecipeForScope(recipe: Recipe, options: RunOptions): Recipe {
165
+ if (options.scope !== "workspace") {
166
+ return recipe;
167
+ }
168
+
169
+ return {
170
+ ...recipe,
171
+ openclaw: {
172
+ ...recipe.openclaw,
173
+ bootstrap: undefined,
174
+ },
175
+ channels: [],
176
+ conversations: [],
177
+ };
178
+ }
179
+
180
+ function filterRecipeByWorkspaceName(recipe: Recipe, workspaceName: string): Recipe {
181
+ const workspace = (recipe.workspaces ?? []).find((ws) => ws.name === workspaceName);
182
+ if (!workspace) {
183
+ throw new ClawChefError(`Workspace not found in recipe: ${workspaceName}`);
184
+ }
185
+
186
+ return {
187
+ ...recipe,
188
+ workspaces: [workspace],
189
+ agents: (recipe.agents ?? []).filter((agent) => agent.workspace === workspaceName),
190
+ conversations: (recipe.conversations ?? []).filter((conv) => conv.workspace === workspaceName),
191
+ };
192
+ }
193
+
170
194
  function semanticValidate(recipe: Recipe): void {
171
195
  const ws = new Set((recipe.workspaces ?? []).map((w) => w.name));
196
+ const agentNameCounts = new Map<string, number>();
172
197
  for (const workspace of recipe.workspaces ?? []) {
173
198
  if (workspace.assets !== undefined && !workspace.assets.trim()) {
174
199
  throw new ClawChefError(`Workspace ${workspace.name} has empty assets path`);
175
200
  }
176
201
  }
177
202
  for (const agent of recipe.agents ?? []) {
203
+ agentNameCounts.set(agent.name, (agentNameCounts.get(agent.name) ?? 0) + 1);
178
204
  if (!ws.has(agent.workspace)) {
179
205
  throw new ClawChefError(`Agent ${agent.name} references missing workspace: ${agent.workspace}`);
180
206
  }
181
207
  }
182
- for (const file of recipe.files ?? []) {
183
- if (!ws.has(file.workspace)) {
184
- throw new ClawChefError(`File ${file.path} references missing workspace: ${file.workspace}`);
208
+ for (const workspace of recipe.workspaces ?? []) {
209
+ for (const file of workspace.files ?? []) {
210
+ if (!file.path.trim()) {
211
+ throw new ClawChefError(`Workspace ${workspace.name} has file with empty path`);
212
+ }
185
213
  }
186
214
  }
187
215
  const agents = new Set((recipe.agents ?? []).map((a) => `${a.workspace}::${a.name}`));
@@ -212,6 +240,23 @@ function semanticValidate(recipe: Recipe): void {
212
240
  );
213
241
  }
214
242
 
243
+ if (channel.agent?.trim()) {
244
+ if (channel.channel !== "telegram") {
245
+ throw new ClawChefError(
246
+ `channels[] entry for ${channel.channel} does not support agent binding. Use channel: telegram with agent.`,
247
+ );
248
+ }
249
+ const matched = agentNameCounts.get(channel.agent) ?? 0;
250
+ if (matched === 0) {
251
+ throw new ClawChefError(`channels[] entry references missing agent by name: ${channel.agent}`);
252
+ }
253
+ if (matched > 1) {
254
+ throw new ClawChefError(
255
+ `channels[] entry references duplicate agent name: ${channel.agent}. Agent names must be unique for channel binding.`,
256
+ );
257
+ }
258
+ }
259
+
215
260
  const hasAuth =
216
261
  Boolean(channel.use_env) ||
217
262
  Boolean(channel.token?.trim()) ||
@@ -752,18 +797,31 @@ export async function loadRecipe(recipePath: string, options: RunOptions): Promi
752
797
  throw new ClawChefError(`Recipe format is invalid: ${firstParse.error.message}`);
753
798
  }
754
799
 
755
- assertNoInlineSecrets(firstParse.data);
800
+ const projected = projectRecipeForScope(firstParse.data, options);
801
+
802
+ assertNoInlineSecrets(projected);
756
803
 
757
- const vars = collectVars(firstParse.data, options.vars);
758
- const rendered = deepResolveTemplates(firstParse.data, vars, options.allowMissing);
804
+ const requiredKeys = options.scope === "workspace" ? new Set<string>() : undefined;
805
+ const vars = collectVars(projected, options.vars, requiredKeys);
806
+ const rendered = deepResolveTemplates(projected, vars, options.allowMissing);
759
807
  const secondParse = recipeSchema.safeParse(rendered);
760
808
  if (!secondParse.success) {
761
809
  throw new ClawChefError(`Recipe is invalid after parameter resolution: ${secondParse.error.message}`);
762
810
  }
763
811
 
764
- semanticValidate(secondParse.data);
812
+ const scopedRecipe = (() => {
813
+ if (options.scope !== "workspace") {
814
+ return secondParse.data;
815
+ }
816
+ if (!options.workspaceName) {
817
+ throw new ClawChefError("scope=workspace requires a workspace name");
818
+ }
819
+ return filterRecipeByWorkspaceName(secondParse.data, options.workspaceName);
820
+ })();
821
+
822
+ semanticValidate(scopedRecipe);
765
823
  return {
766
- recipe: secondParse.data,
824
+ recipe: scopedRecipe,
767
825
  origin: recipeRef.origin,
768
826
  };
769
827
  });
package/src/schema.ts CHANGED
@@ -15,6 +15,7 @@ const openClawCommandsSchema = z
15
15
  factory_reset: z.string().optional(),
16
16
  start_gateway: z.string().optional(),
17
17
  enable_plugin: z.string().optional(),
18
+ bind_channel_agent: z.string().optional(),
18
19
  login_channel: z.string().optional(),
19
20
  create_workspace: z.string().optional(),
20
21
  create_agent: z.string().optional(),
@@ -39,13 +40,7 @@ const openClawBootstrapSchema = z
39
40
  skip_ui: z.boolean().optional(),
40
41
  skip_daemon: z.boolean().optional(),
41
42
  install_daemon: z.boolean().optional(),
42
- openai_api_key: z.string().optional(),
43
- anthropic_api_key: z.string().optional(),
44
- openrouter_api_key: z.string().optional(),
45
- xai_api_key: z.string().optional(),
46
- gemini_api_key: z.string().optional(),
47
- ai_gateway_api_key: z.string().optional(),
48
- cloudflare_ai_gateway_api_key: z.string().optional(),
43
+ llm_api_key: z.string().optional(),
49
44
  cloudflare_ai_gateway_account_id: z.string().optional(),
50
45
  cloudflare_ai_gateway_gateway_id: z.string().optional(),
51
46
  token: z.string().optional(),
@@ -70,6 +65,22 @@ const workspaceSchema = z
70
65
  name: z.string().min(1),
71
66
  path: z.string().min(1).optional(),
72
67
  assets: z.string().min(1).optional(),
68
+ files: z
69
+ .array(
70
+ z
71
+ .object({
72
+ path: z.string().min(1),
73
+ content: z.string().optional(),
74
+ content_from: z.string().min(1).optional(),
75
+ source: z.string().optional(),
76
+ overwrite: z.boolean().optional(),
77
+ })
78
+ .strict()
79
+ .refine((v) => [v.content, v.content_from, v.source].filter((item) => item !== undefined).length === 1, {
80
+ message: "workspaces[].files[] requires exactly one of content, content_from, or source",
81
+ }),
82
+ )
83
+ .optional(),
73
84
  })
74
85
  .strict();
75
86
 
@@ -77,6 +88,7 @@ const channelSchema = z
77
88
  .object({
78
89
  channel: z.string().min(1),
79
90
  account: z.string().min(1).optional(),
91
+ agent: z.string().min(1).optional(),
80
92
  login: z.boolean().optional(),
81
93
  login_mode: z.enum(["interactive"]).optional(),
82
94
  login_account: z.string().min(1).optional(),
@@ -104,20 +116,6 @@ const agentSchema = z
104
116
  })
105
117
  .strict();
106
118
 
107
- const fileSchema = z
108
- .object({
109
- workspace: z.string().min(1),
110
- path: z.string().min(1),
111
- content: z.string().optional(),
112
- content_from: z.string().min(1).optional(),
113
- source: z.string().optional(),
114
- overwrite: z.boolean().optional(),
115
- })
116
- .strict()
117
- .refine((v) => [v.content, v.content_from, v.source].filter((item) => item !== undefined).length === 1, {
118
- message: "files[] requires exactly one of content, content_from, or source",
119
- });
120
-
121
119
  const conversationExpectSchema = z
122
120
  .object({
123
121
  contains: z.array(z.string()).optional(),
@@ -152,7 +150,6 @@ export const recipeSchema = z
152
150
  workspaces: z.array(workspaceSchema).optional(),
153
151
  channels: z.array(channelSchema).optional(),
154
152
  agents: z.array(agentSchema).optional(),
155
- files: z.array(fileSchema).optional(),
156
153
  conversations: z.array(conversationSchema).optional(),
157
154
  })
158
155
  .strict();
package/src/types.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export type InstallPolicy = "auto" | "always" | "never";
2
2
  export type OpenClawProvider = "command" | "mock" | "remote";
3
+ export type RunScope = "full" | "files" | "workspace";
3
4
 
4
5
  export interface OpenClawRemoteConfig {
5
6
  base_url: string;
@@ -24,6 +25,7 @@ export interface OpenClawCommandOverrides {
24
25
  factory_reset?: string;
25
26
  start_gateway?: string;
26
27
  enable_plugin?: string;
28
+ bind_channel_agent?: string;
27
29
  login_channel?: string;
28
30
  create_workspace?: string;
29
31
  create_agent?: string;
@@ -46,13 +48,7 @@ export interface OpenClawBootstrap {
46
48
  skip_ui?: boolean;
47
49
  skip_daemon?: boolean;
48
50
  install_daemon?: boolean;
49
- openai_api_key?: string;
50
- anthropic_api_key?: string;
51
- openrouter_api_key?: string;
52
- xai_api_key?: string;
53
- gemini_api_key?: string;
54
- ai_gateway_api_key?: string;
55
- cloudflare_ai_gateway_api_key?: string;
51
+ llm_api_key?: string;
56
52
  cloudflare_ai_gateway_account_id?: string;
57
53
  cloudflare_ai_gateway_gateway_id?: string;
58
54
  token?: string;
@@ -73,11 +69,13 @@ export interface WorkspaceDef {
73
69
  name: string;
74
70
  path?: string;
75
71
  assets?: string;
72
+ files?: WorkspaceFileDef[];
76
73
  }
77
74
 
78
75
  export interface ChannelDef {
79
76
  channel: string;
80
77
  account?: string;
78
+ agent?: string;
81
79
  login?: boolean;
82
80
  login_mode?: "interactive";
83
81
  login_account?: string;
@@ -102,8 +100,7 @@ export interface AgentDef {
102
100
  skills?: string[];
103
101
  }
104
102
 
105
- export interface FileDef {
106
- workspace: string;
103
+ export interface WorkspaceFileDef {
107
104
  path: string;
108
105
  content?: string;
109
106
  content_from?: string;
@@ -138,18 +135,18 @@ export interface Recipe {
138
135
  workspaces?: WorkspaceDef[];
139
136
  channels?: ChannelDef[];
140
137
  agents?: AgentDef[];
141
- files?: FileDef[];
142
138
  conversations?: ConversationDef[];
143
139
  }
144
140
 
145
141
  export interface RunOptions {
146
142
  vars: Record<string, string>;
147
143
  plugins: string[];
144
+ scope: RunScope;
145
+ workspaceName?: string;
148
146
  dryRun: boolean;
149
147
  allowMissing: boolean;
150
148
  verbose: boolean;
151
149
  silent: boolean;
152
- keepOpenClawState: boolean;
153
150
  provider: OpenClawProvider;
154
151
  remote: Partial<OpenClawRemoteConfig>;
155
152
  }