@clawcipes/recipes 0.1.3 → 0.1.5

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 CHANGED
@@ -4,6 +4,8 @@
4
4
  <img src="./clawcipes_cook.jpg" alt="Clawcipes logo" width="240" />
5
5
  </p>
6
6
 
7
+ > **Experimental:** We’re in active development. Installing should not have any negative impacts, but it’s always good to be safe and copy your `~/.openclaw` folder to a backup.
8
+
7
9
  Clawcipes is an OpenClaw plugin that provides **CLI-first recipes** for scaffolding specialist agents and teams from Markdown.
8
10
 
9
11
  If you like durable workflows: Clawcipes is built around a **file-first team workspace** (inbox/backlog/in-progress/done) that plays nicely with git.
@@ -50,9 +52,10 @@ openclaw recipes dispatch \
50
52
 
51
53
  ## Commands (high level)
52
54
  - `openclaw recipes list|show|status`
53
- - `openclaw recipes scaffold` (agent)
54
- - `openclaw recipes scaffold-team` (team)
55
- - `openclaw recipes install <idOrSlug> [--yes]` (workspace-local skill install)
55
+ - `openclaw recipes scaffold` (agent → `workspace-<agentId>`)
56
+ - `openclaw recipes scaffold-team` (team → `workspace-<teamId>` + `roles/<role>/`)
57
+ - `openclaw recipes install <idOrSlug> [--yes] [--global|--agent-id <id>|--team-id <id>]` (skills: global or scoped)
58
+ - `openclaw recipes bind|bindings` (multi-agent routing)
56
59
  - `openclaw recipes dispatch ...` (request → inbox + ticket + assignment)
57
60
 
58
61
  For full details, see `docs/COMMANDS.md`.
@@ -83,7 +86,12 @@ Reference:
83
86
 
84
87
  (Also see: GitHub repo https://github.com/rjdjohnston/clawcipes)
85
88
  ## Notes / principles
86
- - Workspace-local skills live in `~/.openclaw/workspace/skills` by default.
89
+ - Workspaces:
90
+ - Standalone agents: `~/.openclaw/workspace-<agentId>/`
91
+ - Teams: `~/.openclaw/workspace-<teamId>/` with `roles/<role>/...`
92
+ - Skills:
93
+ - Global (shared): `~/.openclaw/skills/<skill>`
94
+ - Scoped (agent/team): `~/.openclaw/workspace-*/skills/<skill>`
87
95
  - Team IDs end with `-team`; agent IDs are namespaced: `<teamId>-<role>`.
88
96
  - Recipe template rendering is intentionally simple: `{{var}}` replacement only.
89
97
 
@@ -92,10 +100,9 @@ Clawcipes does not (yet) include a first-class `remove-team` command.
92
100
 
93
101
  To remove a scaffolded team created with `scaffold-team --apply-config`, do two things:
94
102
 
95
- 1) Remove team + agent folders (recommended: send to trash):
103
+ 1) Remove the team workspace (recommended: send to trash):
96
104
  ```bash
97
- trash ~/.openclaw/workspace/teams/<teamId>
98
- trash ~/.openclaw/workspace/agents/<teamId>-*
105
+ trash ~/.openclaw/workspace-<teamId>
99
106
  ```
100
107
 
101
108
  2) Remove the agents from OpenClaw config:
@@ -9,12 +9,13 @@ In OpenClaw, an **agent** is a configured assistant persona with:
9
9
  - a **tool policy** (what tools it is allowed to use)
10
10
  - a **model** configuration (defaults come from OpenClaw)
11
11
 
12
- In Clawcipes, an agent is typically created by scaffolding a folder like:
12
+ In Clawcipes, a **standalone** agent recipe scaffolds a dedicated workspace like:
13
13
 
14
14
  ```
15
- ~/.openclaw/workspace/agents/<agentId>/
15
+ ~/.openclaw/workspace-<agentId>/
16
16
  SOUL.md
17
17
  AGENTS.md
18
+ TOOLS.md
18
19
  ...other recipe files...
19
20
  ```
20
21
 
@@ -92,8 +93,14 @@ openclaw recipes install <skill-slug>
92
93
  openclaw recipes install <skill-slug> --yes
93
94
  ```
94
95
 
95
- This runs ClawHub under the hood and installs into:
96
- - `~/.openclaw/workspace/skills/<skill-slug>` (by default)
96
+ This runs ClawHub under the hood and installs into the **current OpenClaw workspace** skills dir:
97
+ - `<workspace>/skills/<skill-slug>`
98
+
99
+ Examples:
100
+ - standalone agent workspace: `~/.openclaw/workspace-<agentId>/skills/<skill-slug>`
101
+ - team workspace: `~/.openclaw/workspace-<teamId>/skills/<skill-slug>`
102
+
103
+ > Note: in the new workspace policy, standalone agents live in `~/.openclaw/workspace-<agentId>` and teams live in `~/.openclaw/workspace-<teamId>`. Skill install targeting is still being refined during the experimental phase.
97
104
 
98
105
  ### Install the skills required by a recipe
99
106
  If a recipe declares skills in `requiredSkills` or `optionalSkills`:
@@ -108,7 +115,7 @@ That installs the recipe’s declared skills.
108
115
  Clawcipes currently does **not** implement a remove command.
109
116
 
110
117
  To remove a workspace-local skill:
111
- - delete the folder: `~/.openclaw/workspace/skills/<skill-slug>`
118
+ - delete the folder: `<workspace>/skills/<skill-slug>`
112
119
  - restart: `openclaw gateway restart`
113
120
 
114
121
  (We can add `openclaw recipes uninstall <slug>` later if you want it to be first-class.)
@@ -118,10 +125,9 @@ Clawcipes does not (yet) include a first-class `remove-team` command.
118
125
 
119
126
  If you scaffolded a team with `scaffold-team --apply-config`, removal has two parts:
120
127
 
121
- 1) Remove the team + agent folders (recommended: send to trash):
128
+ 1) Remove the team workspace (recommended: send to trash):
122
129
  ```bash
123
- trash ~/.openclaw/workspace/teams/<teamId>
124
- trash ~/.openclaw/workspace/agents/<teamId>-*
130
+ trash ~/.openclaw/workspace-<teamId>
125
131
  ```
126
132
 
127
133
  2) Remove the agents from OpenClaw config:
@@ -133,9 +139,26 @@ openclaw gateway restart
133
139
  ```
134
140
 
135
141
  ## Teams: shared workspace + multiple agents
136
- A **team** recipe scaffolds:
137
- - a shared team folder under `teams/<teamId>/...`
138
- - multiple agents under `agents/<teamId>-<role>/...`
142
+ A **team** recipe scaffolds a **shared workspace root** plus role folders:
143
+
144
+ ```
145
+ ~/.openclaw/workspace-<teamId>/
146
+ TEAM.md
147
+ inbox/
148
+ outbox/
149
+ shared/
150
+ notes/
151
+ work/
152
+ backlog/
153
+ in-progress/
154
+ done/
155
+ assignments/
156
+ roles/
157
+ <role>/
158
+ ...role-specific recipe files...
159
+ ```
160
+
161
+ Each role agent is a separate OpenClaw agent id (`<teamId>-<role>`), but they share the same workspace root (`workspace-<teamId>`) so collaboration is file-based.
139
162
 
140
163
  The shared workspace is the source of truth for:
141
164
  - intake (`inbox/`)
@@ -148,7 +171,8 @@ Once an agent exists, there are **two layers** you can update:
148
171
 
149
172
  ### 1) The agent’s files (workspace)
150
173
  Agents are just folders under:
151
- - `~/.openclaw/workspace/agents/<agentId>/`
174
+ - standalone: `~/.openclaw/workspace-<agentId>/`
175
+ - team roles: `~/.openclaw/workspace-<teamId>/roles/<role>/`
152
176
 
153
177
  Common files:
154
178
  - `SOUL.md` — the persona / operating style
@@ -165,7 +189,7 @@ If the agent was created from a recipe, re-running scaffold with `--overwrite` w
165
189
  openclaw recipes scaffold <recipeId> --agent-id <agentId> --overwrite
166
190
  ```
167
191
 
168
- For teams, you typically re-run `scaffold-team`:
192
+ For teams, you typically re-run `scaffold-team` (role files live under `roles/<role>/`):
169
193
 
170
194
  ```bash
171
195
  openclaw recipes scaffold-team <recipeId> --team-id <teamId> --overwrite
package/docs/COMMANDS.md CHANGED
@@ -45,11 +45,11 @@ Options:
45
45
  - `--apply-config` (write/update `agents.list[]` in OpenClaw config)
46
46
 
47
47
  ## `scaffold-team <recipeId>`
48
- Scaffold a team workspace + multiple agents from a **team** recipe.
48
+ Scaffold a shared **team workspace** + multiple agents from a **team** recipe.
49
49
 
50
50
  ```bash
51
51
  openclaw recipes scaffold-team development-team \
52
- --team-id development-team \
52
+ --team-id development-team-team \
53
53
  --overwrite \
54
54
  --apply-config
55
55
  ```
@@ -60,35 +60,86 @@ Options:
60
60
  - `--overwrite`
61
61
  - `--apply-config`
62
62
 
63
- Creates a team directory with standard subfolders:
64
- - `teams/<teamId>/{shared,inbox,outbox,notes,work}`
65
- - `teams/<teamId>/work/{backlog,in-progress,done,assignments}`
63
+ Creates a shared team workspace root:
66
64
 
67
- Also creates agent workspaces under:
68
- - `agents/<teamId>-<role>/...`
65
+ - `~/.openclaw/workspace-<teamId>/...`
66
+
67
+ Standard folders:
68
+ - `inbox/`, `outbox/`, `shared/`, `notes/`
69
+ - `work/{backlog,in-progress,done,assignments}`
70
+ - `roles/<role>/...` (role-specific recipe files)
71
+
72
+ Also creates agent config entries under `agents.list[]` (when `--apply-config`), with agent ids:
73
+ - `<teamId>-<role>`
69
74
 
70
75
  ## `install <idOrSlug> [--yes]`
71
- Install skills into the **workspace-local** skills directory.
76
+ Install skills from ClawHub (confirmation-gated).
77
+
78
+ Default behavior: **global install** into `~/.openclaw/skills`.
72
79
 
73
80
  ```bash
74
- openclaw recipes install local-places
75
- openclaw recipes install local-places --yes
81
+ # Global (shared across all agents)
82
+ openclaw recipes install agentchat --yes
83
+
84
+ # Agent-scoped (into workspace-<agentId>/skills)
85
+ openclaw recipes install agentchat --yes --agent-id dev
86
+
87
+ # Team-scoped (into workspace-<teamId>/skills)
88
+ openclaw recipes install agentchat --yes --team-id development-team-team
76
89
  ```
77
90
 
78
91
  Behavior:
79
92
  - If `idOrSlug` matches a recipe id, installs that recipe’s `requiredSkills` + `optionalSkills`.
80
93
  - Otherwise treats it as a ClawHub skill slug.
81
94
  - Installs via:
82
- - `npx clawhub@latest --workdir <workspaceRoot> --dir skills install <slug>`
95
+ - `npx clawhub@latest --workdir <targetWorkspace> --dir skills install <slug>` (agent/team)
96
+ - `npx clawhub@latest --workdir ~/.openclaw --dir skills install <slug>` (global)
83
97
  - Confirmation-gated unless `--yes`.
84
98
  - In non-interactive mode (no TTY), requires `--yes`.
85
99
 
100
+ ## `bind`
101
+ Add/update a multi-agent routing binding (writes `bindings[]` in `~/.openclaw/openclaw.json`).
102
+
103
+ Examples:
104
+
105
+ ```bash
106
+ # Route one Telegram DM to an agent
107
+ openclaw recipes bind --agent-id dev --channel telegram --peer-kind dm --peer-id 6477250615
108
+
109
+ # Route all Telegram traffic to an agent (broad match)
110
+ openclaw recipes bind --agent-id dev --channel telegram
111
+ ```
112
+
113
+ Notes:
114
+ - `peer.kind` must be one of: `dm|group|channel`.
115
+ - Peer-specific bindings are inserted first (more specific wins).
116
+
117
+ ## `bindings`
118
+ Print the current `bindings[]` from OpenClaw config.
119
+
120
+ ```bash
121
+ openclaw recipes bindings
122
+ ```
123
+
124
+ ## `migrate-team`
125
+ Migrate a legacy team scaffold into the new `workspace-<teamId>` layout.
126
+
127
+ ```bash
128
+ openclaw recipes migrate-team --team-id development-team-team --dry-run
129
+ openclaw recipes migrate-team --team-id development-team-team --mode move
130
+ ```
131
+
132
+ Options:
133
+ - `--dry-run`
134
+ - `--mode move|copy`
135
+ - `--overwrite` (merge into existing destination)
136
+
86
137
  ## `dispatch`
87
138
  Convert a natural-language request into file-first execution artifacts.
88
139
 
89
140
  ```bash
90
141
  openclaw recipes dispatch \
91
- --team-id development-team \
142
+ --team-id development-team-team \
92
143
  --request "Add a customer-support team recipe" \
93
144
  --owner lead
94
145
  ```
@@ -73,4 +73,8 @@ openclaw gateway restart
73
73
  ### `recipes install` fails
74
74
  - Run `npx clawhub@latest --help` to confirm the CLI can run.
75
75
  - Ensure you are logged into ClawHub if required (`npx clawhub@latest login`).
76
- - Confirm installs go into the workspace-local skills dir (default `~/.openclaw/workspace/skills`).
76
+ - Confirm the install scope you intended:
77
+ - global: `~/.openclaw/skills/<skill>`
78
+ - agent: `~/.openclaw/workspace-<agentId>/skills/<skill>`
79
+ - team: `~/.openclaw/workspace-<teamId>/skills/<skill>`
80
+ - If you change installs or config, restart: `openclaw gateway restart`.
package/index.ts CHANGED
@@ -144,10 +144,10 @@ function skillInstallCommands(cfg: Required<RecipesConfig>, skills: string[]) {
144
144
  return lines;
145
145
  }
146
146
 
147
- async function detectMissingSkills(api: OpenClawPluginApi, cfg: Required<RecipesConfig>, skills: string[]) {
147
+ async function detectMissingSkills(installDir: string, skills: string[]) {
148
148
  const missing: string[] = [];
149
149
  for (const s of skills) {
150
- const p = workspacePath(api, cfg.workspaceSkillsDir, s);
150
+ const p = path.join(installDir, s);
151
151
  if (!(await fileExists(p))) missing.push(s);
152
152
  }
153
153
  return missing;
@@ -180,6 +180,20 @@ type AgentConfigSnippet = {
180
180
  tools?: { profile?: string; allow?: string[]; deny?: string[] };
181
181
  };
182
182
 
183
+ type BindingMatch = {
184
+ channel: string;
185
+ accountId?: string;
186
+ // OpenClaw config schema uses: dm | group | channel
187
+ peer?: { kind: "dm" | "group" | "channel"; id: string };
188
+ guildId?: string;
189
+ teamId?: string;
190
+ };
191
+
192
+ type BindingSnippet = {
193
+ agentId: string;
194
+ match: BindingMatch;
195
+ };
196
+
183
197
  function upsertAgentInConfig(cfgObj: any, snippet: AgentConfigSnippet) {
184
198
  if (!cfgObj.agents) cfgObj.agents = {};
185
199
  if (!Array.isArray(cfgObj.agents.list)) cfgObj.agents.list = [];
@@ -242,6 +256,43 @@ function ensureMainFirstInAgentsList(cfgObj: any, api: OpenClawPluginApi) {
242
256
  list.unshift(main);
243
257
  }
244
258
 
259
+ function stableStringify(x: any) {
260
+ const seen = new WeakSet();
261
+ const sortObj = (v: any): any => {
262
+ if (v && typeof v === "object") {
263
+ if (seen.has(v)) return "[Circular]";
264
+ seen.add(v);
265
+ if (Array.isArray(v)) return v.map(sortObj);
266
+ const out: any = {};
267
+ for (const k of Object.keys(v).sort()) out[k] = sortObj(v[k]);
268
+ return out;
269
+ }
270
+ return v;
271
+ };
272
+ return JSON.stringify(sortObj(x));
273
+ }
274
+
275
+ function upsertBindingInConfig(cfgObj: any, binding: BindingSnippet) {
276
+ if (!Array.isArray(cfgObj.bindings)) cfgObj.bindings = [];
277
+ const list: any[] = cfgObj.bindings;
278
+
279
+ const sig = stableStringify({ agentId: binding.agentId, match: binding.match });
280
+ const idx = list.findIndex((b) => stableStringify({ agentId: b?.agentId, match: b?.match }) === sig);
281
+
282
+ if (idx >= 0) {
283
+ // Update in place (preserve ordering)
284
+ list[idx] = { ...list[idx], ...binding };
285
+ return { changed: false, note: "already-present" as const };
286
+ }
287
+
288
+ // Most-specific-first: if a peer match is specified, insert at front so it wins.
289
+ // Otherwise append.
290
+ if (binding.match?.peer) list.unshift(binding);
291
+ else list.push(binding);
292
+
293
+ return { changed: true, note: "added" as const };
294
+ }
295
+
245
296
  async function applyAgentSnippetsToOpenClawConfig(api: OpenClawPluginApi, snippets: AgentConfigSnippet[]) {
246
297
  // Load the latest config from disk (not the snapshot in api.config).
247
298
  const current = (api.runtime as any).config?.loadConfig?.();
@@ -262,6 +313,20 @@ async function applyAgentSnippetsToOpenClawConfig(api: OpenClawPluginApi, snippe
262
313
  return { updatedAgents: snippets.map((s) => s.id) };
263
314
  }
264
315
 
316
+ async function applyBindingSnippetsToOpenClawConfig(api: OpenClawPluginApi, snippets: BindingSnippet[]) {
317
+ const current = (api.runtime as any).config?.loadConfig?.();
318
+ if (!current) throw new Error("Failed to load config via api.runtime.config.loadConfig()");
319
+ const cfgObj = (current.cfg ?? current) as any;
320
+
321
+ const results: any[] = [];
322
+ for (const s of snippets) {
323
+ results.push({ ...s, result: upsertBindingInConfig(cfgObj, s) });
324
+ }
325
+
326
+ await (api.runtime as any).config?.writeConfigFile?.(cfgObj);
327
+ return { updatedBindings: results };
328
+ }
329
+
265
330
  async function scaffoldAgentFromRecipe(
266
331
  api: OpenClawPluginApi,
267
332
  recipe: RecipeFrontmatter,
@@ -389,7 +454,10 @@ const recipesPlugin = {
389
454
  const { frontmatter } = parseFrontmatter(md);
390
455
  if (id && frontmatter.id !== id) continue;
391
456
  const req = frontmatter.requiredSkills ?? [];
392
- const missing = await detectMissingSkills(api, cfg, req);
457
+ const workspaceRoot = api.config.agents?.defaults?.workspace;
458
+ if (!workspaceRoot) throw new Error("agents.defaults.workspace is not set in config");
459
+ const installDir = path.join(workspaceRoot, cfg.workspaceSkillsDir);
460
+ const missing = await detectMissingSkills(installDir, req);
393
461
  out.push({
394
462
  id: frontmatter.id,
395
463
  requiredSkills: req,
@@ -401,11 +469,204 @@ const recipesPlugin = {
401
469
  console.log(JSON.stringify(out, null, 2));
402
470
  });
403
471
 
472
+ cmd
473
+ .command("bind")
474
+ .description("Add/update a multi-agent routing binding (writes openclaw.json bindings[])")
475
+ .requiredOption("--agent-id <agentId>", "Target agent id")
476
+ .requiredOption("--channel <channel>", "Channel name (telegram|whatsapp|discord|slack|...) ")
477
+ .option("--account-id <accountId>", "Channel accountId (if applicable)")
478
+ .option("--peer-kind <kind>", "Peer kind (dm|group|channel) (aliases: direct->dm)")
479
+ .option("--peer-id <id>", "Peer id (DM number/id, group id, or channel id)")
480
+ .option("--guild-id <guildId>", "Discord guildId")
481
+ .option("--team-id <teamId>", "Slack teamId")
482
+ .option("--match <json>", "Full match object as JSON/JSON5 (overrides flags)")
483
+ .action(async (options: any) => {
484
+ const agentId = String(options.agentId);
485
+ let match: BindingMatch;
486
+
487
+ if (options.match) {
488
+ match = JSON5.parse(String(options.match)) as BindingMatch;
489
+ } else {
490
+ match = {
491
+ channel: String(options.channel),
492
+ };
493
+ if (options.accountId) match.accountId = String(options.accountId);
494
+ if (options.guildId) match.guildId = String(options.guildId);
495
+ if (options.teamId) match.teamId = String(options.teamId);
496
+
497
+ if (options.peerKind || options.peerId) {
498
+ if (!options.peerKind || !options.peerId) {
499
+ throw new Error("--peer-kind and --peer-id must be provided together");
500
+ }
501
+ let kind = String(options.peerKind);
502
+ // Back-compat alias
503
+ if (kind === "direct") kind = "dm";
504
+ if (kind !== "dm" && kind !== "group" && kind !== "channel") {
505
+ throw new Error("--peer-kind must be dm|group|channel (or direct as alias for dm)");
506
+ }
507
+ match.peer = { kind, id: String(options.peerId) };
508
+ }
509
+ }
510
+
511
+ if (!match?.channel) throw new Error("match.channel is required");
512
+
513
+ const res = await applyBindingSnippetsToOpenClawConfig(api, [{ agentId, match }]);
514
+ console.log(JSON.stringify(res, null, 2));
515
+ console.error("Binding written. Restart gateway if required for changes to take effect.");
516
+ });
517
+
518
+ cmd
519
+ .command("bindings")
520
+ .description("Show current bindings from openclaw config")
521
+ .action(async () => {
522
+ const current = (api.runtime as any).config?.loadConfig?.();
523
+ if (!current) throw new Error("Failed to load config via api.runtime.config.loadConfig()");
524
+ const cfgObj = (current.cfg ?? current) as any;
525
+ console.log(JSON.stringify(cfgObj.bindings ?? [], null, 2));
526
+ });
527
+
528
+ cmd
529
+ .command("migrate-team")
530
+ .description("Migrate a legacy team scaffold into the new workspace-<teamId> layout")
531
+ .requiredOption("--team-id <teamId>", "Team id (must end with -team)")
532
+ .option("--mode <mode>", "move|copy", "move")
533
+ .option("--dry-run", "Print the plan without writing anything", false)
534
+ .option("--overwrite", "Allow merging into an existing destination (dangerous)", false)
535
+ .action(async (options: any) => {
536
+ const teamId = String(options.teamId);
537
+ if (!teamId.endsWith("-team")) throw new Error("teamId must end with -team");
538
+
539
+ const mode = String(options.mode ?? "move");
540
+ if (mode !== "move" && mode !== "copy") throw new Error("--mode must be move|copy");
541
+
542
+ const baseWorkspace = api.config.agents?.defaults?.workspace;
543
+ if (!baseWorkspace) throw new Error("agents.defaults.workspace is not set in config");
544
+
545
+ const legacyTeamDir = path.resolve(baseWorkspace, "teams", teamId);
546
+ const legacyAgentsDir = path.resolve(baseWorkspace, "agents");
547
+
548
+ const destTeamDir = path.resolve(baseWorkspace, "..", `workspace-${teamId}`);
549
+ const destRolesDir = path.join(destTeamDir, "roles");
550
+
551
+ const exists = async (p: string) => fileExists(p);
552
+
553
+ // Build migration plan
554
+ const plan: any = {
555
+ teamId,
556
+ mode,
557
+ legacy: { teamDir: legacyTeamDir, agentsDir: legacyAgentsDir },
558
+ dest: { teamDir: destTeamDir, rolesDir: destRolesDir },
559
+ steps: [] as any[],
560
+ agentIds: [] as string[],
561
+ };
562
+
563
+ const legacyTeamExists = await exists(legacyTeamDir);
564
+ if (!legacyTeamExists) {
565
+ throw new Error(`Legacy team directory not found: ${legacyTeamDir}`);
566
+ }
567
+
568
+ const destExists = await exists(destTeamDir);
569
+ if (destExists && !options.overwrite) {
570
+ throw new Error(`Destination already exists: ${destTeamDir} (re-run with --overwrite to merge)`);
571
+ }
572
+
573
+ // 1) Move/copy team shared workspace
574
+ plan.steps.push({ kind: "teamDir", from: legacyTeamDir, to: destTeamDir });
575
+
576
+ // 2) Move/copy each role agent directory into roles/<role>/
577
+ const legacyAgentsExist = await exists(legacyAgentsDir);
578
+ let legacyAgentFolders: string[] = [];
579
+ if (legacyAgentsExist) {
580
+ legacyAgentFolders = (await fs.readdir(legacyAgentsDir)).filter((x) => x.startsWith(`${teamId}-`));
581
+ }
582
+
583
+ for (const folder of legacyAgentFolders) {
584
+ const agentId = folder;
585
+ const role = folder.slice((teamId + "-").length);
586
+ const from = path.join(legacyAgentsDir, folder);
587
+ const to = path.join(destRolesDir, role);
588
+ plan.agentIds.push(agentId);
589
+ plan.steps.push({ kind: "roleDir", agentId, role, from, to });
590
+ }
591
+
592
+ const dryRun = !!options.dryRun;
593
+ if (dryRun) {
594
+ console.log(JSON.stringify({ ok: true, dryRun: true, plan }, null, 2));
595
+ return;
596
+ }
597
+
598
+ // Helpers
599
+ const copyDirRecursive = async (src: string, dst: string) => {
600
+ await ensureDir(dst);
601
+ const entries = await fs.readdir(src, { withFileTypes: true });
602
+ for (const ent of entries) {
603
+ const s = path.join(src, ent.name);
604
+ const d = path.join(dst, ent.name);
605
+ if (ent.isDirectory()) await copyDirRecursive(s, d);
606
+ else if (ent.isSymbolicLink()) {
607
+ const link = await fs.readlink(s);
608
+ await fs.symlink(link, d);
609
+ } else {
610
+ await ensureDir(path.dirname(d));
611
+ await fs.copyFile(s, d);
612
+ }
613
+ }
614
+ };
615
+
616
+ const removeDirRecursive = async (p: string) => {
617
+ // node 25 supports fs.rm
618
+ await fs.rm(p, { recursive: true, force: true });
619
+ };
620
+
621
+ const moveDir = async (src: string, dst: string) => {
622
+ await ensureDir(path.dirname(dst));
623
+ try {
624
+ await fs.rename(src, dst);
625
+ } catch {
626
+ // cross-device or existing: fallback to copy+remove
627
+ await copyDirRecursive(src, dst);
628
+ await removeDirRecursive(src);
629
+ }
630
+ };
631
+
632
+ // Execute plan
633
+ if (mode === "copy") {
634
+ await copyDirRecursive(legacyTeamDir, destTeamDir);
635
+ } else {
636
+ await moveDir(legacyTeamDir, destTeamDir);
637
+ }
638
+
639
+ // Ensure roles dir exists
640
+ await ensureDir(destRolesDir);
641
+
642
+ for (const step of plan.steps.filter((s: any) => s.kind === "roleDir")) {
643
+ if (!(await exists(step.from))) continue;
644
+ if (mode === "copy") await copyDirRecursive(step.from, step.to);
645
+ else await moveDir(step.from, step.to);
646
+ }
647
+
648
+ // Update config: set each team agent's workspace to destTeamDir (shared)
649
+ const agentSnippets: AgentConfigSnippet[] = plan.agentIds.map((agentId: string) => ({
650
+ id: agentId,
651
+ workspace: destTeamDir,
652
+ }));
653
+ if (agentSnippets.length) {
654
+ await applyAgentSnippetsToOpenClawConfig(api, agentSnippets);
655
+ }
656
+
657
+ console.log(JSON.stringify({ ok: true, migrated: teamId, destTeamDir, agentIds: plan.agentIds }, null, 2));
658
+ });
659
+
404
660
  cmd
405
661
  .command("install")
406
- .description("Install a ClawHub skill into this OpenClaw workspace (confirmation-gated)")
662
+ .description(
663
+ "Install a skill from ClawHub (confirmation-gated). Default: global (~/.openclaw/skills). Use --agent-id or --team-id for scoped installs.",
664
+ )
407
665
  .argument("<idOrSlug>", "Recipe id OR ClawHub skill slug")
408
666
  .option("--yes", "Skip confirmation prompt")
667
+ .option("--global", "Install into global shared skills (~/.openclaw/skills) (default when no scope flags)")
668
+ .option("--agent-id <agentId>", "Install into a specific agent workspace (workspace-<agentId>)")
669
+ .option("--team-id <teamId>", "Install into a team workspace (workspace-<teamId>)")
409
670
  .action(async (idOrSlug: string, options: any) => {
410
671
  const cfg = getCfg(api);
411
672
 
@@ -420,10 +681,45 @@ const recipesPlugin = {
420
681
  recipe = null;
421
682
  }
422
683
 
423
- const workspaceRoot = api.config.agents?.defaults?.workspace;
424
- if (!workspaceRoot) throw new Error("agents.defaults.workspace is not set in config");
684
+ const baseWorkspace = api.config.agents?.defaults?.workspace;
685
+ if (!baseWorkspace) throw new Error("agents.defaults.workspace is not set in config");
425
686
 
426
- const installDir = path.join(workspaceRoot, cfg.workspaceSkillsDir);
687
+ const stateDir = path.resolve(baseWorkspace, ".."); // ~/.openclaw
688
+
689
+ const scopeFlags = [options.global ? "global" : null, options.agentId ? "agent" : null, options.teamId ? "team" : null].filter(Boolean);
690
+ if (scopeFlags.length > 1) {
691
+ throw new Error("Use only one of: --global, --agent-id, --team-id");
692
+ }
693
+
694
+ const agentIdOpt = typeof options.agentId === "string" ? options.agentId.trim() : "";
695
+ const teamIdOpt = typeof options.teamId === "string" ? options.teamId.trim() : "";
696
+
697
+ // Default is global install when no scope is provided.
698
+ const scope = scopeFlags[0] ?? "global";
699
+
700
+ let workdir: string;
701
+ let dirName: string;
702
+ let installDir: string;
703
+
704
+ if (scope === "agent") {
705
+ if (!agentIdOpt) throw new Error("--agent-id cannot be empty");
706
+ const agentWorkspace = path.resolve(stateDir, `workspace-${agentIdOpt}`);
707
+ workdir = agentWorkspace;
708
+ dirName = cfg.workspaceSkillsDir;
709
+ installDir = path.join(agentWorkspace, dirName);
710
+ } else if (scope === "team") {
711
+ if (!teamIdOpt) throw new Error("--team-id cannot be empty");
712
+ const teamWorkspace = path.resolve(stateDir, `workspace-${teamIdOpt}`);
713
+ workdir = teamWorkspace;
714
+ dirName = cfg.workspaceSkillsDir;
715
+ installDir = path.join(teamWorkspace, dirName);
716
+ } else {
717
+ workdir = stateDir;
718
+ dirName = "skills";
719
+ installDir = path.join(stateDir, dirName);
720
+ }
721
+
722
+ await ensureDir(installDir);
427
723
 
428
724
  const skillsToInstall = recipe
429
725
  ? Array.from(new Set([...(recipe.requiredSkills ?? []), ...(recipe.optionalSkills ?? [])])).filter(Boolean)
@@ -434,11 +730,11 @@ const recipesPlugin = {
434
730
  return;
435
731
  }
436
732
 
437
- const missing = await detectMissingSkills(api, cfg, skillsToInstall);
733
+ const missing = await detectMissingSkills(installDir, skillsToInstall);
438
734
  const already = skillsToInstall.filter((s) => !missing.includes(s));
439
735
 
440
736
  if (already.length) {
441
- console.error(`Already present in workspace skills dir (${installDir}): ${already.join(", ")}`);
737
+ console.error(`Already present in skills dir (${installDir}): ${already.join(", ")}`);
442
738
  }
443
739
 
444
740
  if (!missing.length) {
@@ -446,9 +742,10 @@ const recipesPlugin = {
446
742
  return;
447
743
  }
448
744
 
745
+ const targetLabel = scope === "agent" ? `agent:${agentIdOpt}` : scope === "team" ? `team:${teamIdOpt}` : "global";
449
746
  const header = recipe
450
- ? `Install skills for recipe ${recipe.id} into ${installDir}?\n- ${missing.join("\n- ")}`
451
- : `Install skill into ${installDir}?\n- ${missing.join("\n- ")}`;
747
+ ? `Install skills for recipe ${recipe.id} into ${installDir} (${targetLabel})?\n- ${missing.join("\n- ")}`
748
+ : `Install skill into ${installDir} (${targetLabel})?\n- ${missing.join("\n- ")}`;
452
749
 
453
750
  const requireConfirm = !options.yes;
454
751
  if (requireConfirm) {
@@ -474,12 +771,12 @@ const recipesPlugin = {
474
771
  console.error(header);
475
772
  }
476
773
 
477
- // Use clawhub CLI. Force workspace-local install path.
774
+ // Use clawhub CLI. Force install path based on scope.
478
775
  const { spawnSync } = await import("node:child_process");
479
776
  for (const slug of missing) {
480
777
  const res = spawnSync(
481
778
  "npx",
482
- ["clawhub@latest", "--workdir", workspaceRoot, "--dir", cfg.workspaceSkillsDir, "install", slug],
779
+ ["clawhub@latest", "--workdir", workdir, "--dir", dirName, "install", slug],
483
780
  { stdio: "inherit" },
484
781
  );
485
782
  if (res.status !== 0) {
@@ -659,7 +956,10 @@ const recipesPlugin = {
659
956
  }
660
957
 
661
958
  const cfg = getCfg(api);
662
- const missing = await detectMissingSkills(api, cfg, recipe.requiredSkills ?? []);
959
+ const workspaceRoot = api.config.agents?.defaults?.workspace;
960
+ if (!workspaceRoot) throw new Error("agents.defaults.workspace is not set in config");
961
+ const installDir = path.join(workspaceRoot, cfg.workspaceSkillsDir);
962
+ const missing = await detectMissingSkills(installDir, recipe.requiredSkills ?? []);
663
963
  if (missing.length) {
664
964
  console.error(`Missing skills for recipe ${recipeId}: ${missing.join(", ")}`);
665
965
  console.error(`Install commands (workspace-local):\n${skillInstallCommands(cfg, missing).join("\n")}`);
@@ -709,7 +1009,10 @@ const recipesPlugin = {
709
1009
  }
710
1010
 
711
1011
  const cfg = getCfg(api);
712
- const missing = await detectMissingSkills(api, cfg, recipe.requiredSkills ?? []);
1012
+ const baseWorkspace = api.config.agents?.defaults?.workspace;
1013
+ if (!baseWorkspace) throw new Error("agents.defaults.workspace is not set in config");
1014
+ const installDir = path.join(baseWorkspace, cfg.workspaceSkillsDir);
1015
+ const missing = await detectMissingSkills(installDir, recipe.requiredSkills ?? []);
713
1016
  if (missing.length) {
714
1017
  console.error(`Missing skills for recipe ${recipeId}: ${missing.join(", ")}`);
715
1018
  console.error(`Install commands (workspace-local):\n${skillInstallCommands(cfg, missing).join("\n")}`);
@@ -717,9 +1020,6 @@ const recipesPlugin = {
717
1020
  return;
718
1021
  }
719
1022
 
720
- const baseWorkspace = api.config.agents?.defaults?.workspace;
721
- if (!baseWorkspace) throw new Error("agents.defaults.workspace is not set in config");
722
-
723
1023
  // Team workspace root (shared by all role agents): ~/.openclaw/workspace-<teamId>
724
1024
  const teamDir = path.resolve(baseWorkspace, "..", `workspace-${teamId}`);
725
1025
  await ensureDir(teamDir);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawcipes/recipes",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Clawcipes recipes plugin for OpenClaw (markdown recipes -> scaffold agents/teams)",
5
5
  "main": "index.ts",
6
6
  "type": "commonjs",