@clawcipes/recipes 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/index.ts +110 -16
  2. package/package.json +1 -1
package/index.ts CHANGED
@@ -200,8 +200,46 @@ function upsertAgentInConfig(cfgObj: any, snippet: AgentConfigSnippet) {
200
200
  tools: snippet.tools ? { ...snippet.tools } : prev?.tools,
201
201
  };
202
202
 
203
- if (idx >= 0) list[idx] = nextAgent;
204
- else list.push(nextAgent);
203
+ if (idx >= 0) {
204
+ list[idx] = nextAgent;
205
+ return;
206
+ }
207
+
208
+ // New agent: append to end of list.
209
+ // (We still separately enforce that main exists and stays first/default.)
210
+ list.push(nextAgent);
211
+ }
212
+
213
+ function ensureMainFirstInAgentsList(cfgObj: any, api: OpenClawPluginApi) {
214
+ if (!cfgObj.agents) cfgObj.agents = {};
215
+ if (!Array.isArray(cfgObj.agents.list)) cfgObj.agents.list = [];
216
+
217
+ const list: any[] = cfgObj.agents.list;
218
+
219
+ const workspaceRoot =
220
+ cfgObj.agents?.defaults?.workspace ??
221
+ api.config.agents?.defaults?.workspace ??
222
+ "~/.openclaw/workspace";
223
+
224
+ const idx = list.findIndex((a) => a?.id === "main");
225
+ const prevMain = idx >= 0 ? list[idx] : {};
226
+
227
+ // Enforce: main exists, is first, and is the default.
228
+ const main = {
229
+ ...prevMain,
230
+ id: "main",
231
+ default: true,
232
+ workspace: prevMain?.workspace ?? workspaceRoot,
233
+ sandbox: prevMain?.sandbox ?? { mode: "off" },
234
+ };
235
+
236
+ // Ensure only one default.
237
+ for (const a of list) {
238
+ if (a?.id !== "main" && a?.default) a.default = false;
239
+ }
240
+
241
+ if (idx >= 0) list.splice(idx, 1);
242
+ list.unshift(main);
205
243
  }
206
244
 
207
245
  async function applyAgentSnippetsToOpenClawConfig(api: OpenClawPluginApi, snippets: AgentConfigSnippet[]) {
@@ -211,22 +249,36 @@ async function applyAgentSnippetsToOpenClawConfig(api: OpenClawPluginApi, snippe
211
249
 
212
250
  // Some loaders return { cfg, ... }. If so, normalize.
213
251
  const cfgObj = (current.cfg ?? current) as any;
252
+
253
+ // Always keep main first/default when multi-agent workflows are in play.
254
+ ensureMainFirstInAgentsList(cfgObj, api);
255
+
214
256
  for (const s of snippets) upsertAgentInConfig(cfgObj, s);
215
257
 
258
+ // Re-assert ordering/default after upserts.
259
+ ensureMainFirstInAgentsList(cfgObj, api);
260
+
216
261
  await (api.runtime as any).config?.writeConfigFile?.(cfgObj);
217
262
  return { updatedAgents: snippets.map((s) => s.id) };
218
263
  }
219
264
 
220
- async function scaffoldAgentFromRecipe(api: OpenClawPluginApi, recipe: RecipeFrontmatter, opts: {
221
- agentId: string;
222
- agentName?: string;
223
- update?: boolean;
224
- vars?: Record<string, string>;
225
- }) {
226
- const cfg = getCfg(api);
265
+ async function scaffoldAgentFromRecipe(
266
+ api: OpenClawPluginApi,
267
+ recipe: RecipeFrontmatter,
268
+ opts: {
269
+ agentId: string;
270
+ agentName?: string;
271
+ update?: boolean;
272
+ vars?: Record<string, string>;
227
273
 
228
- const agentDir = workspacePath(api, cfg.workspaceAgentsDir, opts.agentId);
229
- await ensureDir(agentDir);
274
+ // Where to write the scaffolded files (may be a shared team workspace role folder)
275
+ filesRootDir: string;
276
+
277
+ // What to set in agents.list[].workspace (may be shared team workspace root)
278
+ workspaceRootDir: string;
279
+ },
280
+ ) {
281
+ await ensureDir(opts.filesRootDir);
230
282
 
231
283
  const templates = recipe.templates ?? {};
232
284
  const files = recipe.files ?? [];
@@ -237,7 +289,7 @@ async function scaffoldAgentFromRecipe(api: OpenClawPluginApi, recipe: RecipeFro
237
289
  const raw = templates[f.template];
238
290
  if (typeof raw !== "string") throw new Error(`Missing template: ${f.template}`);
239
291
  const rendered = renderTemplate(raw, vars);
240
- const target = path.join(agentDir, f.path);
292
+ const target = path.join(opts.filesRootDir, f.path);
241
293
  const mode = opts.update ? (f.mode ?? "overwrite") : (f.mode ?? "createOnly");
242
294
  const r = await writeFileSafely(target, rendered, mode);
243
295
  fileResults.push({ path: target, wrote: r.wrote, reason: r.reason });
@@ -245,13 +297,14 @@ async function scaffoldAgentFromRecipe(api: OpenClawPluginApi, recipe: RecipeFro
245
297
 
246
298
  const configSnippet: AgentConfigSnippet = {
247
299
  id: opts.agentId,
248
- workspace: agentDir,
300
+ workspace: opts.workspaceRootDir,
249
301
  identity: { name: opts.agentName ?? recipe.name ?? opts.agentId },
250
302
  tools: recipe.tools ?? {},
251
303
  };
252
304
 
253
305
  return {
254
- agentDir,
306
+ filesRootDir: opts.filesRootDir,
307
+ workspaceRootDir: opts.workspaceRootDir,
255
308
  fileResults,
256
309
  next: {
257
310
  configSnippet,
@@ -269,6 +322,27 @@ const recipesPlugin = {
269
322
  properties: {},
270
323
  },
271
324
  register(api: OpenClawPluginApi) {
325
+ // On plugin load, ensure multi-agent config has an explicit agents.list with main at top.
326
+ // This is idempotent and only writes if a change is required.
327
+ (async () => {
328
+ try {
329
+ const current = (api.runtime as any).config?.loadConfig?.();
330
+ if (!current) return;
331
+ const cfgObj = (current.cfg ?? current) as any;
332
+
333
+ const before = JSON.stringify(cfgObj.agents?.list ?? null);
334
+ ensureMainFirstInAgentsList(cfgObj, api);
335
+ const after = JSON.stringify(cfgObj.agents?.list ?? null);
336
+
337
+ if (before !== after) {
338
+ await (api.runtime as any).config?.writeConfigFile?.(cfgObj);
339
+ console.error("[recipes] ensured agents.list includes main as first/default");
340
+ }
341
+ } catch (e) {
342
+ console.error(`[recipes] warning: failed to ensure main agent in agents.list: ${(e as Error).message}`);
343
+ }
344
+ })();
345
+
272
346
  api.registerCli(
273
347
  ({ program }) => {
274
348
  const cmd = program.command("recipes").description("Manage markdown recipes (scaffold agents/teams)");
@@ -443,7 +517,8 @@ const recipesPlugin = {
443
517
  if (!workspaceRoot) throw new Error("agents.defaults.workspace is not set in config");
444
518
 
445
519
  const teamId = String(options.teamId);
446
- const teamDir = workspacePath(api, cfg.workspaceTeamsDir, teamId);
520
+ // Team workspace root (shared by all role agents): ~/.openclaw/workspace-<teamId>
521
+ const teamDir = path.resolve(workspaceRoot, "..", `workspace-${teamId}`);
447
522
 
448
523
  const inboxDir = path.join(teamDir, "inbox");
449
524
  const backlogDir = path.join(teamDir, "work", "backlog");
@@ -592,10 +667,16 @@ const recipesPlugin = {
592
667
  return;
593
668
  }
594
669
 
670
+ const baseWorkspace = api.config.agents?.defaults?.workspace ?? "~/.openclaw/workspace";
671
+ // Put standalone agent workspaces alongside the default workspace (same parent dir).
672
+ const resolvedWorkspaceRoot = path.resolve(baseWorkspace, "..", `workspace-${options.agentId}`);
673
+
595
674
  const result = await scaffoldAgentFromRecipe(api, recipe, {
596
675
  agentId: options.agentId,
597
676
  agentName: options.name,
598
677
  update: !!options.overwrite,
678
+ filesRootDir: resolvedWorkspaceRoot,
679
+ workspaceRootDir: resolvedWorkspaceRoot,
599
680
  vars: {
600
681
  agentId: options.agentId,
601
682
  agentName: options.name ?? recipe.name ?? options.agentId,
@@ -636,8 +717,15 @@ const recipesPlugin = {
636
717
  return;
637
718
  }
638
719
 
639
- const teamDir = workspacePath(api, cfg.workspaceTeamsDir, teamId);
720
+ const baseWorkspace = api.config.agents?.defaults?.workspace;
721
+ if (!baseWorkspace) throw new Error("agents.defaults.workspace is not set in config");
722
+
723
+ // Team workspace root (shared by all role agents): ~/.openclaw/workspace-<teamId>
724
+ const teamDir = path.resolve(baseWorkspace, "..", `workspace-${teamId}`);
640
725
  await ensureDir(teamDir);
726
+
727
+ const rolesDir = path.join(teamDir, "roles");
728
+ await ensureDir(rolesDir);
641
729
  const notesDir = path.join(teamDir, "notes");
642
730
  const workDir = path.join(teamDir, "work");
643
731
  const backlogDir = path.join(workDir, "backlog");
@@ -695,16 +783,22 @@ const recipesPlugin = {
695
783
  tools: a.tools ?? recipe.tools,
696
784
  };
697
785
 
786
+ const roleDir = path.join(rolesDir, role);
698
787
  const r = await scaffoldAgentFromRecipe(api, scopedRecipe, {
699
788
  agentId,
700
789
  agentName,
701
790
  update: !!options.overwrite,
791
+ // Write role-specific files under roles/<role>/
792
+ filesRootDir: roleDir,
793
+ // But set the agent workspace root to the shared team workspace
794
+ workspaceRootDir: teamDir,
702
795
  vars: {
703
796
  teamId,
704
797
  teamDir,
705
798
  role,
706
799
  agentId,
707
800
  agentName,
801
+ roleDir,
708
802
  },
709
803
  });
710
804
  results.push({ role, agentId, ...r });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawcipes/recipes",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Clawcipes recipes plugin for OpenClaw (markdown recipes -> scaffold agents/teams)",
5
5
  "main": "index.ts",
6
6
  "type": "commonjs",