@drunkcoding/agents-and-skills 0.0.38 → 0.0.40

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.
@@ -12,7 +12,7 @@
12
12
  "name": "tech-graph",
13
13
  "source": "./plugins/tech-graph",
14
14
  "description": "6-step wizard for technical diagrams (SVG/PNG) via fireworks-tech-graph",
15
- "version": "0.0.38",
15
+ "version": "0.0.40",
16
16
  "category": "diagram",
17
17
  "keywords": [
18
18
  "diagram",
@@ -26,7 +26,7 @@
26
26
  "name": "html-effectiveness",
27
27
  "source": "./plugins/html-effectiveness",
28
28
  "description": "Generate self-contained interactive HTML reports from 20 upstream templates via a conversational agent.",
29
- "version": "0.0.38",
29
+ "version": "0.0.40",
30
30
  "category": "reports",
31
31
  "keywords": [
32
32
  "html",
@@ -41,7 +41,7 @@
41
41
  "name": "plugin-validator",
42
42
  "source": "./plugins/plugin-validator",
43
43
  "description": "Orchestrated validator for Claude Code plugins — validates skills, agents, commands, and hooks across every plugin under plugins/**.",
44
- "version": "0.0.38",
44
+ "version": "0.0.40",
45
45
  "category": "tooling",
46
46
  "keywords": [
47
47
  "validation",
@@ -57,7 +57,7 @@
57
57
  "name": "team-share",
58
58
  "source": "./plugins/team-share",
59
59
  "description": "Onboard your team with an interactive setup menu: install CodeGraph, build the Understand-Anything knowledge graph, and share Claude Code settings — run any combination, all idempotent.",
60
- "version": "0.0.38",
60
+ "version": "0.0.40",
61
61
  "category": "tooling",
62
62
  "keywords": [
63
63
  "onboarding",
@@ -73,7 +73,7 @@
73
73
  "name": "multica-tool",
74
74
  "source": "./plugins/multica-tool",
75
75
  "description": "Export, import, and sync Multica skills, agents, and squads between workspaces.",
76
- "version": "0.0.38",
76
+ "version": "0.0.40",
77
77
  "category": "workflow",
78
78
  "keywords": [
79
79
  "multica",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drunkcoding/agents-and-skills",
3
- "version": "0.0.38",
3
+ "version": "0.0.40",
4
4
  "description": "Personal collection of Claude Code skills and agents, installable via `npx skills`.",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "html-effectiveness",
3
3
  "displayName": "HTML Effectiveness Reports",
4
- "version": "0.0.38",
4
+ "version": "0.0.40",
5
5
  "description": "Generate self-contained interactive HTML reports from 20 upstream templates via a conversational agent.",
6
6
  "author": {
7
7
  "name": "Steven Hoang"
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "multica-tool",
3
- "version": "0.0.38",
3
+ "version": "0.0.40",
4
4
  "description": "Export, import, and sync Multica skills, agents, and squads between workspaces via the multica CLI."
5
5
  }
@@ -54,6 +54,7 @@ export const listRuntimes = (cli) => cli.json(["runtime", "list"]);
54
54
  export const listSkills = (cli) => cli.json(["skill", "list"]);
55
55
  export const listAgents = (cli) => cli.json(["agent", "list"]);
56
56
  export const listSquads = (cli) => cli.json(["squad", "list"]);
57
+ export const listWorkspaceMembers = (cli) => cli.json(["workspace", "member", "list"]);
57
58
 
58
59
  // Get-wrappers: the ONLY place that knows the raw CLI field names — an
59
60
  // explicit allow-list, so unexpected/internal CLI fields never leak into a
@@ -71,7 +72,10 @@ export function getAgent(cli, id) {
71
72
  const a = cli.json(["agent", "get", id]);
72
73
  return {
73
74
  id: a.id, name: a.name, description: a.description, instructions: a.instructions,
74
- model: a.model, visibility: a.visibility,
75
+ model: a.model, visibility: a.visibility, avatar_url: a.avatar_url ?? null,
76
+ service_tier: a.service_tier ?? "",
77
+ permission_mode: a.permission_mode ?? null,
78
+ invocation_targets: (a.invocation_targets ?? []).map((t) => ({ target_id: t.target_id, target_type: t.target_type })),
75
79
  max_concurrent_tasks: a.max_concurrent_tasks,
76
80
  runtime_config: a.runtime_config,
77
81
  custom_args: a.custom_args,
@@ -93,7 +97,7 @@ export function getAgentCustomEnv(cli, id) {
93
97
 
94
98
  export function getSquad(cli, id) {
95
99
  const s = cli.json(["squad", "get", id]);
96
- return { id: s.id, name: s.name, description: s.description, instructions: s.instructions, leader_id: s.leader_id };
100
+ return { id: s.id, name: s.name, description: s.description, instructions: s.instructions, leader_id: s.leader_id, avatar_url: s.avatar_url ?? null };
97
101
  }
98
102
 
99
103
  export const getSquadMembers = (cli, id) =>
@@ -1,14 +1,37 @@
1
1
  import * as nodeFs from "node:fs";
2
+ import { spawnSync } from "node:child_process";
2
3
  import { dirname } from "node:path";
3
- import { slugify, getSkill, getAgent, getAgentCustomEnv, getSquad, getSquadMembers, listRuntimes, makeCli, realExec, requireAuth, resolveWorkspaceId } from "./lib.mjs";
4
+ import { slugify, getSkill, getAgent, getAgentCustomEnv, getSquad, getSquadMembers, listRuntimes, listSkills, listAgents, listSquads, makeCli, realExec, requireAuth, resolveWorkspaceId } from "./lib.mjs";
4
5
 
5
6
  const nonEmpty = (v) => v && typeof v === "object" && Object.keys(v).length > 0;
6
7
 
8
+ // An avatar_url is either an uploaded-image URL (http[s]://…) or an inline
9
+ // "emoji:🦍" marker. Only image URLs carry bytes worth bundling; emoji markers
10
+ // are just carried as strings in the record.
11
+ const isImageAvatar = (url) => typeof url === "string" && /^https?:\/\//.test(url);
12
+
13
+ // Extension for the bundled avatar file, from the URL path; defaults to .png.
14
+ function avatarExt(url) {
15
+ const m = /\.([a-z0-9]{2,5})(?:[?#]|$)/i.exec(url.split("/").pop() || "");
16
+ return m ? `.${m[1].toLowerCase()}` : ".png";
17
+ }
18
+
19
+ // Synchronous binary fetch (keeps exportResource sync). Runs the download in a
20
+ // short-lived node subprocess so no top-level async is needed; returns a Buffer,
21
+ // or null on any failure (non-2xx, network error) — a missing avatar is
22
+ // non-fatal, the export continues with just the recorded avatar_url string.
23
+ export function fetchBinary(url) {
24
+ const script = "fetch(process.argv[1]).then(r=>{if(!r.ok)process.exit(3);return r.arrayBuffer()}).then(b=>process.stdout.write(Buffer.from(b))).catch(()=>process.exit(4))";
25
+ const res = spawnSync(process.execPath, ["-e", script, "--", url], { maxBuffer: 64 * 1024 * 1024, timeout: 15_000 });
26
+ if (res.status !== 0 || !res.stdout?.length) return null;
27
+ return res.stdout;
28
+ }
29
+
7
30
  export function redactAgent(a) {
8
31
  // a is a normalized agent from getAgent, with `custom_env`/
9
32
  // `custom_env_fetch_failed` attached by the caller (collectAgent) — getAgent
10
33
  // itself never fetches custom_env, since it requires a separate audited call.
11
- const { id, has_custom_env, mcp_config_redacted, custom_env_fetch_failed, mcp_config, custom_env, skills, runtime_id, ...rest } = a;
34
+ const { id, has_custom_env, mcp_config_redacted, custom_env_fetch_failed, mcp_config, custom_env, skills, runtime_id, instructions, ...rest } = a;
12
35
  const mcpUsable = !mcp_config_redacted && nonEmpty(mcp_config);
13
36
  const envUsable = !custom_env_fetch_failed && nonEmpty(custom_env);
14
37
  // mcp_config_redacted / custom_env_fetch_failed alone still flag hadSecrets even
@@ -16,10 +39,10 @@ export function redactAgent(a) {
16
39
  // but couldn't be captured, not just silently see an empty bundle.
17
40
  const hadSecrets = mcpUsable || envUsable || !!mcp_config_redacted || !!custom_env_fetch_failed;
18
41
  return {
19
- // source_id lets import-time mention rewriting map stale `mention://agent/<id>`
20
- // links (in this or another agent's/squad's instructions) to the new id.
21
42
  record: {
22
43
  ...rest,
44
+ // source_id lets import-time mention rewriting map stale `mention://agent/<id>`
45
+ // links (in this or another agent's/squad's instructions) to the new id.
23
46
  source_id: id,
24
47
  source_runtime_id: runtime_id,
25
48
  skill_names: [],
@@ -28,10 +51,13 @@ export function redactAgent(a) {
28
51
  had_secrets: hadSecrets,
29
52
  },
30
53
  hadSecrets,
54
+ // instructions are written to a sibling .md by the caller (see avatar_file),
55
+ // never embedded in the JSON record.
56
+ instructions: instructions ?? "",
31
57
  };
32
58
  }
33
59
 
34
- export function buildManifest({ scope, sourceWorkspaceId, skills, agents, squad }) {
60
+ export function buildManifest({ scope, sourceWorkspaceId, skills, agents, squads }) {
35
61
  const seenSkills = new Map();
36
62
  for (const s of skills) if (!seenSkills.has(s.name)) seenSkills.set(s.name, s);
37
63
  const seenAgents = new Map();
@@ -42,7 +68,13 @@ export function buildManifest({ scope, sourceWorkspaceId, skills, agents, squad
42
68
  source_workspace_id: sourceWorkspaceId,
43
69
  skills: [...seenSkills.values()].map((s) => ({ name: s.name, dir: `skills/${slugify(s.name)}`, source_id: s.source_id })),
44
70
  agents: [...seenAgents.values()].map((a) => ({ name: a.name, file: `agents/${slugify(a.name)}.json`, source_id: a.source_id, source_runtime_id: a.source_runtime_id, source_runtime_provider: a.source_runtime_provider ?? null, skill_names: a.skill_names, had_secrets: !!a.had_secrets })),
45
- squads: squad ? [{ name: squad.name, file: `squads/${slugify(squad.name)}.json`, description: squad.description ?? "", instructions: squad.instructions ?? "", leader_name: squad.leader_name, members: squad.members }] : [],
71
+ squads: (squads ?? []).map((squad) => {
72
+ const file = `squads/${slugify(squad.name)}.json`;
73
+ const entry = { name: squad.name, file, description: squad.description ?? "", avatar_url: squad.avatar_url ?? null, leader_name: squad.leader_name, members: squad.members };
74
+ // Instructions go to a sibling .md (see squad write loop); only referenced when non-empty.
75
+ if (squad.instructions) entry.instructions_file = file.replace(/\.json$/, ".md");
76
+ return entry;
77
+ }),
46
78
  };
47
79
  }
48
80
 
@@ -74,37 +106,43 @@ function collectAgent(cli, id, agentsById, skills, providerById) {
74
106
  return entry;
75
107
  }
76
108
 
77
- export function exportResource({ cli, scope, ids, outDir, sourceWorkspaceId, fs = nodeFs }) {
109
+ export function exportResource({ cli, scope, ids, outDir, sourceWorkspaceId, fs = nodeFs, download = fetchBinary }) {
78
110
  const skills = new Map(); // name -> normalized skill
79
111
  const agentsById = new Map(); // id -> { raw, red, skill_names }
80
- let squad = null;
112
+ const squads = [];
81
113
  // Lazy + memoized: only fetched when an agent is actually collected (skips
82
114
  // the extra CLI call on skill-only exports).
83
115
  let providerById = null;
84
116
  const getProviderById = () => providerById ??= new Map(listRuntimes(cli).map((r) => [r.id, r.provider]));
85
117
 
86
- if (scope === "skill") collectSkill(cli, ids.skillId, skills);
87
- if (scope === "agent") collectAgent(cli, ids.agentId, agentsById, skills, getProviderById());
88
- if (scope === "squad") {
89
- const sq = getSquad(cli, ids.squadId);
90
- const members = getSquadMembers(cli, ids.squadId).filter((m) => m.member_type === "agent");
118
+ // Collect a squad's agents (leader + members) and return the squad bundle object.
119
+ function collectOneSquad(squadId) {
120
+ const sq = getSquad(cli, squadId);
121
+ const members = getSquadMembers(cli, squadId).filter((m) => m.member_type === "agent");
91
122
  for (const m of members) collectAgent(cli, m.member_id, agentsById, skills, getProviderById());
92
123
  if (!agentsById.has(sq.leader_id)) collectAgent(cli, sq.leader_id, agentsById, skills, getProviderById());
93
124
  const nameOf = (id) => agentsById.get(id)?.raw.name;
94
- squad = {
95
- name: sq.name,
96
- description: sq.description,
97
- instructions: sq.instructions,
125
+ return {
126
+ name: sq.name, description: sq.description, instructions: sq.instructions, avatar_url: sq.avatar_url,
98
127
  leader_name: nameOf(sq.leader_id),
99
128
  members: members.map((m) => ({ agent_name: nameOf(m.member_id), role: m.role })),
100
129
  };
101
130
  }
102
131
 
132
+ if (scope === "skill") collectSkill(cli, ids.skillId, skills);
133
+ else if (scope === "agent") collectAgent(cli, ids.agentId, agentsById, skills, getProviderById());
134
+ else if (scope === "squad") squads.push(collectOneSquad(ids.squadId));
135
+ else if (scope === "all") {
136
+ for (const s of listSkills(cli)) collectSkill(cli, s.id, skills);
137
+ for (const a of listAgents(cli)) collectAgent(cli, a.id, agentsById, skills, getProviderById());
138
+ for (const sq of listSquads(cli)) squads.push(collectOneSquad(sq.id));
139
+ }
140
+
103
141
  const manifest = buildManifest({
104
142
  scope, sourceWorkspaceId,
105
143
  skills: [...skills.values()].map((s) => ({ name: s.name, source_id: s.id })),
106
144
  agents: [...agentsById.values()].map((a) => ({ name: a.raw.name, source_id: a.raw.id, source_runtime_id: a.raw.runtime_id, source_runtime_provider: a.raw.source_runtime_provider, skill_names: a.skill_names, had_secrets: a.red.hadSecrets })),
107
- squad,
145
+ squads,
108
146
  });
109
147
 
110
148
  const warnings = [];
@@ -129,10 +167,33 @@ export function exportResource({ cli, scope, ids, outDir, sourceWorkspaceId, fs
129
167
  const record = { ...red.record, skill_names };
130
168
  if (red.hadSecrets) warnings.push(raw.name);
131
169
  fs.mkdirSync(`${outDir}/agents`, { recursive: true });
170
+ // Download an uploaded-image avatar into the bundle so import can re-upload
171
+ // it (the only way to set an agent avatar). Emoji avatars stay as the
172
+ // recorded avatar_url string; a failed download leaves avatar_url without a
173
+ // file, and import simply won't restore it.
174
+ if (isImageAvatar(record.avatar_url)) {
175
+ const rel = entry.file.replace(/\.json$/, `.avatar${avatarExt(record.avatar_url)}`);
176
+ const bytes = download(record.avatar_url);
177
+ if (bytes && bytes.length) {
178
+ fs.writeFileSync(`${outDir}/${rel}`, bytes);
179
+ record.avatar_file = rel;
180
+ }
181
+ }
182
+ // Instructions live in a sibling .md for reviewability (same sibling-file
183
+ // pattern as avatar_file); only written when non-empty.
184
+ if (red.instructions) {
185
+ const rel = entry.file.replace(/\.json$/, ".md");
186
+ fs.writeFileSync(`${outDir}/${rel}`, red.instructions);
187
+ record.instructions_file = rel;
188
+ }
132
189
  fs.writeFileSync(`${outDir}/${entry.file}`, JSON.stringify(record, null, 2));
133
190
  }
191
+ const squadInstrByName = new Map(squads.map((s) => [s.name, s.instructions ?? ""]));
134
192
  for (const entry of manifest.squads) {
135
193
  fs.mkdirSync(`${outDir}/squads`, { recursive: true });
194
+ if (entry.instructions_file) {
195
+ fs.writeFileSync(`${outDir}/${entry.instructions_file}`, squadInstrByName.get(entry.name) ?? "");
196
+ }
136
197
  fs.writeFileSync(`${outDir}/${entry.file}`, JSON.stringify(entry, null, 2));
137
198
  }
138
199
  fs.writeFileSync(`${outDir}/manifest.json`, JSON.stringify(manifest, null, 2));
@@ -148,8 +209,8 @@ function main() {
148
209
  const out = get("--out");
149
210
  const workspace = get("--workspace"); // optional: source workspace name
150
211
 
151
- if (!scope || !id || !out) {
152
- console.error("Usage: multica-export.mjs --scope <skill|agent|squad> --id <id> --out <dir> [--workspace <name>]");
212
+ if (!scope || !out || (scope !== "all" && !id)) {
213
+ console.error("Usage: multica-export.mjs --scope <skill|agent|squad|all> --id <id> --out <dir> [--workspace <name>] (--id not needed for --scope all)");
153
214
  process.exit(1);
154
215
  }
155
216
 
@@ -164,7 +225,8 @@ function main() {
164
225
  if (scope === "skill") ids.skillId = id;
165
226
  else if (scope === "agent") ids.agentId = id;
166
227
  else if (scope === "squad") ids.squadId = id;
167
- else { console.error(`Unknown scope "${scope}"use skill|agent|squad`); process.exit(1); }
228
+ else if (scope === "all") { /* whole workspace no id */ }
229
+ else { console.error(`Unknown scope "${scope}" — use skill|agent|squad|all`); process.exit(1); }
168
230
 
169
231
  const result = exportResource({ cli, scope, ids, outDir: out, sourceWorkspaceId, fs: nodeFs });
170
232
  console.log(JSON.stringify(result, null, 2));
@@ -1,5 +1,15 @@
1
1
  import * as nodeFs from "node:fs";
2
- import { listSkills, listAgents, listSquads, listRuntimes, getSquadMembers, findByName, makeCli, realExec, requireAuth, resolveWorkspaceId } from "./lib.mjs";
2
+ import { listSkills, listAgents, listSquads, listRuntimes, listWorkspaceMembers, getSquadMembers, findByName, makeCli, realExec, requireAuth, resolveWorkspaceId } from "./lib.mjs";
3
+
4
+ // Instructions live in a sibling .md referenced by `instructions_file` (mirrors
5
+ // avatar_file). Legacy bundles carry no instructions_file and keep instructions
6
+ // inline in the JSON — fall back to that so older exports still import.
7
+ function readInstructions(fs, dir, rec) {
8
+ if (rec.instructions_file && fs.existsSync(`${dir}/${rec.instructions_file}`)) {
9
+ return fs.readFileSync(`${dir}/${rec.instructions_file}`, "utf8");
10
+ }
11
+ return rec.instructions ?? "";
12
+ }
3
13
 
4
14
  // Relative paths of every file under root (recursing into subdirs like scripts/).
5
15
  function walkSkillFiles(fs, root, rel = "") {
@@ -55,10 +65,20 @@ export function importSkills({ cli, manifest, dir, fs = nodeFs }) {
55
65
  return { idMap, created, updated };
56
66
  }
57
67
 
68
+ // True when a resource already carries an avatar (uploaded image or emoji).
69
+ const hasAvatar = (r) => !!(r && typeof r.avatar_url === "string" && r.avatar_url);
70
+
58
71
  export function importAgents({ cli, manifest, dir, skillIdMap, runtimeMap, fs = nodeFs }) {
59
72
  const idMap = new Map();
60
73
  const sourceIdMap = new Map(); // source agent id -> new agent id, for mention rewriting
61
74
  const secretsApplyFailures = [];
75
+ const avatarApplyFailures = []; // image upload rejected
76
+ const avatarUnsupported = []; // emoji avatar — no CLI setter for agents
77
+ const permissionApplyFailures = []; // CLI rejected --public-to-member
78
+ const permissionUnsupported = []; // no member target resolved in the destination
79
+ // Lazy + memoized: destination member user_ids, only listed when an agent needs them.
80
+ let destMemberIds = null;
81
+ const getDestMemberIds = () => destMemberIds ??= new Set(listWorkspaceMembers(cli).map((m) => m.user_id));
62
82
  let created = 0, updated = 0;
63
83
  const existing = listAgents(cli);
64
84
 
@@ -72,11 +92,13 @@ export function importAgents({ cli, manifest, dir, skillIdMap, runtimeMap, fs =
72
92
  "--max-concurrent-tasks", String(rec.max_concurrent_tasks ?? 6),
73
93
  ];
74
94
  if (rec.description) common.push("--description", rec.description);
75
- if (rec.instructions) common.push("--instructions", rec.instructions);
95
+ const instructions = readInstructions(fs, dir, rec);
96
+ if (instructions) common.push("--instructions", instructions);
76
97
  if (rec.model) common.push("--model", rec.model);
77
98
  if (rec.thinking_level) common.push("--thinking-level", rec.thinking_level);
78
99
  if (rec.runtime_config && Object.keys(rec.runtime_config).length) common.push("--runtime-config", JSON.stringify(rec.runtime_config));
79
100
  if (Array.isArray(rec.custom_args) && rec.custom_args.length) common.push("--custom-args", JSON.stringify(rec.custom_args));
101
+ if (rec.service_tier) common.push("--service-tier", rec.service_tier);
80
102
  const match = findByName(existing, rec.name);
81
103
  let id;
82
104
  if (match) {
@@ -91,6 +113,39 @@ export function importAgents({ cli, manifest, dir, skillIdMap, runtimeMap, fs =
91
113
  const skillIds = (rec.skill_names ?? []).map((n) => skillIdMap.get(n)).filter(Boolean);
92
114
  cli.run(["agent", "skills", "set", id, "--skill-ids", skillIds.join(",")]);
93
115
 
116
+ // Avatar: only ever set it when the target agent has none — never clobber an
117
+ // avatar an existing agent already has. Agents accept only image uploads
118
+ // (`agent avatar --file`); an emoji-only avatar can't be restored via the CLI.
119
+ if (!hasAvatar(match)) {
120
+ if (rec.avatar_file) {
121
+ try {
122
+ cli.run(["agent", "avatar", id, "--file", `${dir}/${rec.avatar_file}`]);
123
+ } catch {
124
+ avatarApplyFailures.push(rec.name);
125
+ }
126
+ } else if (typeof rec.avatar_url === "string" && rec.avatar_url.startsWith("emoji:")) {
127
+ avatarUnsupported.push(rec.name);
128
+ }
129
+ }
130
+
131
+ // Restore member-specific public_to sharing that --visibility can't express.
132
+ // (private and workspace-wide public_to already round-trip via --visibility.)
133
+ if (rec.permission_mode === "public_to") {
134
+ const memberTargets = (rec.invocation_targets ?? []).filter((t) => t.target_type === "user");
135
+ if (memberTargets.length) {
136
+ const resolvable = memberTargets.map((t) => t.target_id).filter((tid) => getDestMemberIds().has(tid));
137
+ if (!resolvable.length) {
138
+ permissionUnsupported.push(rec.name);
139
+ } else {
140
+ try {
141
+ cli.run(["agent", "update", id, "--permission-mode", "public_to", ...resolvable.flatMap((tid) => ["--public-to-member", tid])]);
142
+ } catch {
143
+ permissionApplyFailures.push(rec.name);
144
+ }
145
+ }
146
+ }
147
+ }
148
+
94
149
  // mcp_config/custom_env carry real secrets. Each is applied via its OWN
95
150
  // follow-up call, never bundled into the create/update call above — that
96
151
  // keeps a rejected secret from failing the whole agent create/update, and
@@ -116,7 +171,7 @@ export function importAgents({ cli, manifest, dir, skillIdMap, runtimeMap, fs =
116
171
  }
117
172
  }
118
173
  }
119
- return { idMap, sourceIdMap, created, updated, secretsApplyFailures };
174
+ return { idMap, sourceIdMap, created, updated, secretsApplyFailures, avatarApplyFailures, avatarUnsupported, permissionApplyFailures, permissionUnsupported };
120
175
  }
121
176
 
122
177
  // Rewrites `mention://agent/<id>` links (e.g. `[@dev-backend](mention://agent/<id>)`)
@@ -140,9 +195,10 @@ export function rewriteAgentMentions({ cli, manifest, dir, agentIdMap, sourceIdM
140
195
  let updated = 0;
141
196
  for (const a of manifest.agents) {
142
197
  const rec = JSON.parse(fs.readFileSync(`${dir}/${a.file}`, "utf8"));
143
- if (!rec.instructions) continue;
144
- const rewritten = rewriteMentions(rec.instructions, sourceIdMap);
145
- if (rewritten === rec.instructions) continue;
198
+ const instructions = readInstructions(fs, dir, rec);
199
+ if (!instructions) continue;
200
+ const rewritten = rewriteMentions(instructions, sourceIdMap);
201
+ if (rewritten === instructions) continue;
146
202
  cli.run(["agent", "update", agentIdMap.get(rec.name), "--instructions", rewritten]);
147
203
  updated++;
148
204
  }
@@ -165,6 +221,13 @@ export function importSquad({ cli, squad, agentIdMap, sourceIdMap }) {
165
221
  const out = cli.run(["squad", "create", "--name", squad.name, "--leader", leaderId, "--description", squad.description ?? "", ...instr]);
166
222
  id = JSON.parse(out).id; created++;
167
223
  }
224
+ // Avatar: squads accept only an avatar-url string (emoji or URL) via
225
+ // `squad update`. Set it only when the target squad has none — never clobber
226
+ // an existing squad's avatar.
227
+ if (!hasAvatar(match) && squad.avatar_url) {
228
+ cli.run(["squad", "update", id, "--avatar-url", squad.avatar_url]);
229
+ }
230
+
168
231
  // Add non-leader members, skipping any already present so re-runs are idempotent.
169
232
  const present = new Set(getSquadMembers(cli, id).map((m) => m.member_id));
170
233
  for (const m of squad.members) {
@@ -226,18 +289,29 @@ export function importBundle({ cli, dir, runtimeMap, fs = nodeFs }) {
226
289
  const agentRes = importAgents({ cli, manifest, dir, skillIdMap: skillRes.idMap, runtimeMap: effective, fs });
227
290
  // Runs after every agent exists so forward-referencing mentions resolve.
228
291
  const mentionRes = rewriteAgentMentions({ cli, manifest, dir, agentIdMap: agentRes.idMap, sourceIdMap: agentRes.sourceIdMap, fs });
229
- let squadRes = { newId: null, created: 0, updated: 0 };
230
- if (manifest.squads?.length) squadRes = importSquad({ cli, squad: manifest.squads[0], agentIdMap: agentRes.idMap, sourceIdMap: agentRes.sourceIdMap });
292
+ const squadIdMap = new Map();
293
+ let squadsCreated = 0, squadsUpdated = 0;
294
+ for (const squad of manifest.squads ?? []) {
295
+ squad.instructions = readInstructions(fs, dir, squad);
296
+ const r = importSquad({ cli, squad, agentIdMap: agentRes.idMap, sourceIdMap: agentRes.sourceIdMap });
297
+ squadIdMap.set(squad.name, r.newId);
298
+ squadsCreated += r.created;
299
+ squadsUpdated += r.updated;
300
+ }
231
301
 
232
302
  return {
233
- created: { skills: skillRes.created, agents: agentRes.created, squads: squadRes.created },
234
- updated: { skills: skillRes.updated, agents: agentRes.updated, squads: squadRes.updated },
303
+ created: { skills: skillRes.created, agents: agentRes.created, squads: squadsCreated },
304
+ updated: { skills: skillRes.updated, agents: agentRes.updated, squads: squadsUpdated },
235
305
  mentionsRewritten: mentionRes.updated,
236
306
  skillIdMap: Object.fromEntries(skillRes.idMap),
237
307
  agentIdMap: Object.fromEntries(agentRes.idMap),
238
- squadId: squadRes.newId,
308
+ squadIdMap: Object.fromEntries(squadIdMap),
239
309
  secretsReminder: (manifest.agents ?? []).filter((a) => a.had_secrets).map((a) => a.name),
240
310
  secretsApplyFailures: agentRes.secretsApplyFailures,
311
+ avatarApplyFailures: agentRes.avatarApplyFailures,
312
+ avatarUnsupported: agentRes.avatarUnsupported,
313
+ permissionApplyFailures: agentRes.permissionApplyFailures,
314
+ permissionUnsupported: agentRes.permissionUnsupported,
241
315
  };
242
316
  }
243
317
 
@@ -44,13 +44,17 @@ where `<slug>` is a lowercased, hyphenated form of the resource name.
44
44
 
45
45
  ```bash
46
46
  node "${CLAUDE_PLUGIN_ROOT}/scripts/multica-export.mjs" \
47
- --scope <type> \
47
+ --scope <type|all> \
48
48
  --id <id> \
49
49
  --out <dir> \
50
50
  [--workspace <workspace-name>]
51
51
  ```
52
52
 
53
- The script writes `manifest.json`, skill SKILL.md files, agent JSON files, and squad JSON files into `<dir>`.
53
+ Pass `--scope all` (with no `--id`) to export the **entire workspace** — every skill, agent, and squad into one flat, deduped bundle. A skill or agent shared across many agents/squads is written exactly once and referenced by name.
54
+
55
+ The script writes `manifest.json`, skill `SKILL.md` files, agent JSON files, and squad JSON files into `<dir>`. Each agent's and squad's **instructions** (system prompt / charter) are written to a sibling Markdown file — `agents/<slug>.md`, `squads/<slug>.md` — referenced by an `instructions_file` key in the JSON, so the prose is easy to read, diff, and edit. Agents/squads with no instructions get no `.md`.
56
+
57
+ Avatars are captured automatically: an agent's uploaded-image avatar is downloaded into the bundle (`agents/<slug>.avatar.<ext>`) and referenced by `avatar_file`; emoji avatars (agents and squads) and a squad's avatar are recorded as the `avatar_url` string.
54
58
 
55
59
  ## Step 5 — Report results
56
60
 
@@ -40,13 +40,21 @@ node "${CLAUDE_PLUGIN_ROOT}/scripts/multica-import.mjs" \
40
40
 
41
41
  The import also rewrites any `mention://agent/<id>` link inside squad and agent instructions (e.g. `[@dev-backend](mention://agent/<id>)`) from the source agent's id to its new id in the target workspace — the CLI does this automatically for every agent captured in the bundle; no extra flag needed. Mentions pointing to an agent outside the bundle are left untouched.
42
42
 
43
+ Instructions are read back from each resource's sibling `.md` (`agents/<slug>.md`, `squads/<slug>.md`) when present — editing that Markdown is the supported way to review and enhance an agent's or squad's instructions before import. Older bundles that predate the split (instructions inline in the JSON, no `instructions_file`) still import unchanged.
44
+
45
+ Avatars are restored automatically, but **only when the target resource has none** — an existing agent or squad that already carries an avatar is never overwritten. New agents get their bundled image re-uploaded; new squads get their `avatar_url` (emoji or URL) set. An agent whose source avatar was an emoji can't be restored (the CLI has no emoji setter for agents) and is reported as unsupported.
46
+
43
47
  ## Step 3 — Report results
44
48
 
45
49
  Parse the JSON output and report:
46
50
 
47
51
  - Created and updated counts for skills, agents, and squads.
48
52
  - Name-to-ID maps for skills and agents (`skillIdMap`, `agentIdMap`).
49
- - Squad ID if a squad was imported.
53
+ - `squadIdMap`: name-to-ID map for every squad imported.
50
54
  - `mentionsRewritten`: how many agents had an agent-mention link rewritten to its new id.
51
55
  - If `secretsReminder` is non-empty, surface every agent name verbatim with: "WARNING: the following agents' bundle files contained custom environment variables or MCP config in PLAINTEXT — the source export directory should be treated as sensitive: `<agent-name>`."
52
56
  - If `secretsApplyFailures` is non-empty, surface every agent name verbatim with: "WARNING: mcp_config or custom_env failed to apply to the following agents during import (the agent itself was still created/updated) — set them manually in the Multica UI: `<agent-name>`."
57
+ - If `avatarApplyFailures` is non-empty, surface every agent name verbatim with: "WARNING: the avatar image failed to upload for the following agents — set it manually in the Multica UI: `<agent-name>`."
58
+ - If `avatarUnsupported` is non-empty, surface every agent name verbatim with: "NOTE: the following agents had an emoji avatar at the source, which the CLI cannot set on an agent — set it manually in the Multica UI: `<agent-name>`."
59
+ - If `permissionApplyFailures` is non-empty, surface every agent name verbatim with: "WARNING: member-specific sharing failed to apply for the following agents — set their invocation permissions manually in the Multica UI: `<agent-name>`."
60
+ - If `permissionUnsupported` is non-empty, surface every agent name verbatim with: "NOTE: the following agents were shared with specific members at the source, but none of those users exist in the target workspace — re-share manually if needed: `<agent-name>`."
@@ -49,6 +49,12 @@ Parse the JSON output and report:
49
49
 
50
50
  - Created and updated counts for skills, agents, and squads.
51
51
  - Name-to-ID maps (`skillIdMap`, `agentIdMap`).
52
- - Squad ID if a squad was synced.
52
+ - `squadIdMap`: name-to-ID map for every squad synced.
53
53
  - If `secretsReminder` is non-empty, surface every agent name verbatim with: "WARNING: the following agents' bundle files contained custom environment variables or MCP config in PLAINTEXT — the temporary export directory (already cleaned up) briefly held these secrets in plaintext: `<agent-name>`."
54
54
  - If `secretsApplyFailures` is non-empty, surface every agent name verbatim with: "WARNING: mcp_config or custom_env failed to apply to the following agents during sync (the agent itself was still created/updated) — set them manually in the Multica UI: `<agent-name>`."
55
+ - If `avatarApplyFailures` is non-empty, surface every agent name verbatim with: "WARNING: the avatar image failed to upload for the following agents — set it manually in the destination workspace: `<agent-name>`."
56
+ - If `avatarUnsupported` is non-empty, surface every agent name verbatim with: "NOTE: the following agents had an emoji avatar at the source, which the CLI cannot set on an agent — set it manually in the destination workspace: `<agent-name>`."
57
+ - If `permissionApplyFailures` is non-empty, surface every agent name verbatim with: "WARNING: member-specific sharing failed to apply for the following agents — set their invocation permissions manually in the destination workspace: `<agent-name>`."
58
+ - If `permissionUnsupported` is non-empty, surface every agent name verbatim with: "NOTE: the following agents were shared with specific members at the source, but none of those users exist in the destination workspace — re-share manually if needed: `<agent-name>`."
59
+
60
+ Avatars sync automatically, but **only when the destination resource has none** — an existing agent or squad that already carries an avatar is never overwritten.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "plugin-validator",
3
3
  "displayName": "Plugin Validator",
4
- "version": "0.0.38",
4
+ "version": "0.0.40",
5
5
  "description": "Orchestrated validator for Claude Code plugins — validates skills, agents, commands, and hooks across every plugin under plugins/**.",
6
6
  "author": {
7
7
  "name": "Steven Hoang"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "team-share",
3
3
  "displayName": "Team Share",
4
- "version": "0.0.38",
4
+ "version": "0.0.40",
5
5
  "description": "Onboard your team with an interactive setup menu: install CodeGraph, build the Understand-Anything knowledge graph, and share Claude Code settings — run any combination, all idempotent.",
6
6
  "author": {
7
7
  "name": "Steven Hoang"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tech-graph",
3
- "version": "0.0.38",
3
+ "version": "0.0.40",
4
4
  "description": "Step-by-step wizard for generating technical diagrams as SVG+PNG.",
5
5
  "author": {
6
6
  "name": "steven"