@drunkcoding/agents-and-skills 0.0.37 → 0.0.39

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.37",
15
+ "version": "0.0.39",
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.37",
29
+ "version": "0.0.39",
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.37",
44
+ "version": "0.0.39",
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.37",
60
+ "version": "0.0.39",
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.37",
76
+ "version": "0.0.39",
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.37",
3
+ "version": "0.0.39",
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.37",
4
+ "version": "0.0.39",
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.37",
3
+ "version": "0.0.39",
4
4
  "description": "Export, import, and sync Multica skills, agents, and squads between workspaces via the multica CLI."
5
5
  }
@@ -54,9 +54,11 @@ 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
- // Get-wrappers: the ONLY place that knows the raw CLI field names. They
59
- // return a stable camelCase shape so downstream code never sees snake_case.
59
+ // Get-wrappers: the ONLY place that knows the raw CLI field names — an
60
+ // explicit allow-list, so unexpected/internal CLI fields never leak into a
61
+ // bundle. Field names mirror the CLI's own snake_case; nothing is renamed.
60
62
  export function getSkill(cli, id) {
61
63
  const s = cli.json(["skill", "get", id]);
62
64
  return {
@@ -70,15 +72,18 @@ export function getAgent(cli, id) {
70
72
  const a = cli.json(["agent", "get", id]);
71
73
  return {
72
74
  id: a.id, name: a.name, description: a.description, instructions: a.instructions,
73
- model: a.model, visibility: a.visibility,
74
- maxConcurrentTasks: a.max_concurrent_tasks,
75
- runtimeConfig: a.runtime_config,
76
- customArgs: a.custom_args,
77
- thinkingLevel: a.thinking_level,
78
- runtimeId: a.runtime_id,
79
- hasCustomEnv: a.has_custom_env,
80
- mcpConfig: a.mcp_config,
81
- mcpConfigRedacted: !!a.mcp_config_redacted,
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 })),
79
+ max_concurrent_tasks: a.max_concurrent_tasks,
80
+ runtime_config: a.runtime_config,
81
+ custom_args: a.custom_args,
82
+ thinking_level: a.thinking_level,
83
+ runtime_id: a.runtime_id,
84
+ has_custom_env: a.has_custom_env,
85
+ mcp_config: a.mcp_config,
86
+ mcp_config_redacted: !!a.mcp_config_redacted,
82
87
  skills: (a.skills ?? []).map((sk) => ({ id: sk.id, name: sk.name })),
83
88
  };
84
89
  }
@@ -92,10 +97,10 @@ export function getAgentCustomEnv(cli, id) {
92
97
 
93
98
  export function getSquad(cli, id) {
94
99
  const s = cli.json(["squad", "get", id]);
95
- return { id: s.id, name: s.name, description: s.description, instructions: s.instructions, leaderId: 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 };
96
101
  }
97
102
 
98
103
  export const getSquadMembers = (cli, id) =>
99
104
  (cli.json(["squad", "member", "list", id]) ?? []).map((m) => ({
100
- memberId: m.member_id, memberType: m.member_type, role: m.role || "member",
105
+ member_id: m.member_id, member_type: m.member_type, role: m.role || "member",
101
106
  }));
@@ -1,37 +1,60 @@
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
- // a is a normalized (camelCase) agent from getAgent, with `customEnv`/
9
- // `customEnvFetchFailed` attached by the caller (collectAgent) — getAgent
31
+ // a is a normalized agent from getAgent, with `custom_env`/
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, hasCustomEnv, mcpConfigRedacted, customEnvFetchFailed, mcpConfig, customEnv, skills, runtimeId, ...rest } = a;
12
- const mcpUsable = !mcpConfigRedacted && nonEmpty(mcpConfig);
13
- const envUsable = !customEnvFetchFailed && nonEmpty(customEnv);
14
- // mcpConfigRedacted / customEnvFetchFailed alone still flag hadSecrets even
34
+ const { id, has_custom_env, mcp_config_redacted, custom_env_fetch_failed, mcp_config, custom_env, skills, runtime_id, ...rest } = a;
35
+ const mcpUsable = !mcp_config_redacted && nonEmpty(mcp_config);
36
+ const envUsable = !custom_env_fetch_failed && nonEmpty(custom_env);
37
+ // mcp_config_redacted / custom_env_fetch_failed alone still flag hadSecrets even
15
38
  // when unusable — the user should know something was present at the source
16
39
  // but couldn't be captured, not just silently see an empty bundle.
17
- const hadSecrets = mcpUsable || envUsable || !!mcpConfigRedacted || !!customEnvFetchFailed;
40
+ const hadSecrets = mcpUsable || envUsable || !!mcp_config_redacted || !!custom_env_fetch_failed;
18
41
  return {
19
- // sourceId lets import-time mention rewriting map stale `mention://agent/<id>`
42
+ // source_id lets import-time mention rewriting map stale `mention://agent/<id>`
20
43
  // links (in this or another agent's/squad's instructions) to the new id.
21
44
  record: {
22
45
  ...rest,
23
- sourceId: id,
24
- sourceRuntimeId: runtimeId,
25
- skillNames: [],
26
- mcpConfig: mcpUsable ? mcpConfig : null,
27
- customEnv: envUsable ? customEnv : null,
28
- hadSecrets,
46
+ source_id: id,
47
+ source_runtime_id: runtime_id,
48
+ skill_names: [],
49
+ mcp_config: mcpUsable ? mcp_config : null,
50
+ custom_env: envUsable ? custom_env : null,
51
+ had_secrets: hadSecrets,
29
52
  },
30
53
  hadSecrets,
31
54
  };
32
55
  }
33
56
 
34
- export function buildManifest({ scope, sourceWorkspaceId, skills, agents, squad }) {
57
+ export function buildManifest({ scope, sourceWorkspaceId, skills, agents, squads }) {
35
58
  const seenSkills = new Map();
36
59
  for (const s of skills) if (!seenSkills.has(s.name)) seenSkills.set(s.name, s);
37
60
  const seenAgents = new Map();
@@ -39,10 +62,10 @@ export function buildManifest({ scope, sourceWorkspaceId, skills, agents, squad
39
62
  return {
40
63
  version: "1",
41
64
  scope,
42
- sourceWorkspaceId,
43
- skills: [...seenSkills.values()].map((s) => ({ name: s.name, dir: `skills/${slugify(s.name)}`, sourceId: s.sourceId })),
44
- agents: [...seenAgents.values()].map((a) => ({ name: a.name, file: `agents/${slugify(a.name)}.json`, sourceId: a.sourceId, sourceRuntimeId: a.sourceRuntimeId, sourceRuntimeProvider: a.sourceRuntimeProvider ?? null, skillNames: a.skillNames, hadSecrets: !!a.hadSecrets })),
45
- squads: squad ? [{ name: squad.name, file: `squads/${slugify(squad.name)}.json`, description: squad.description ?? "", instructions: squad.instructions ?? "", leaderName: squad.leaderName, members: squad.members }] : [],
65
+ source_workspace_id: sourceWorkspaceId,
66
+ skills: [...seenSkills.values()].map((s) => ({ name: s.name, dir: `skills/${slugify(s.name)}`, source_id: s.source_id })),
67
+ 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 })),
68
+ squads: (squads ?? []).map((squad) => ({ name: squad.name, file: `squads/${slugify(squad.name)}.json`, description: squad.description ?? "", instructions: squad.instructions ?? "", avatar_url: squad.avatar_url ?? null, leader_name: squad.leader_name, members: squad.members })),
46
69
  };
47
70
  }
48
71
 
@@ -52,59 +75,65 @@ function collectSkill(cli, id, skills) {
52
75
  return s.name;
53
76
  }
54
77
 
55
- // Keyed by agent id (so squad leaderId/memberId resolve to names). Stores the
78
+ // Keyed by agent id (so squad leader_id/member_id resolve to names). Stores the
56
79
  // normalized agent, its redaction result, and its skill names.
57
80
  function collectAgent(cli, id, agentsById, skills, providerById) {
58
81
  if (agentsById.has(id)) return agentsById.get(id);
59
82
  const a = getAgent(cli, id);
60
- a.sourceRuntimeProvider = providerById.get(a.runtimeId) ?? null;
61
- a.customEnv = {};
62
- a.customEnvFetchFailed = false;
63
- if (a.hasCustomEnv) {
83
+ a.source_runtime_provider = providerById.get(a.runtime_id) ?? null;
84
+ a.custom_env = {};
85
+ a.custom_env_fetch_failed = false;
86
+ if (a.has_custom_env) {
64
87
  try {
65
- a.customEnv = getAgentCustomEnv(cli, id);
88
+ a.custom_env = getAgentCustomEnv(cli, id);
66
89
  } catch {
67
- a.customEnvFetchFailed = true; // e.g. insufficient permission — non-fatal, warned via hadSecrets
90
+ a.custom_env_fetch_failed = true; // e.g. insufficient permission — non-fatal, warned via hadSecrets
68
91
  }
69
92
  }
70
- const skillNames = a.skills.map((sk) => collectSkill(cli, sk.id, skills));
93
+ const skill_names = a.skills.map((sk) => collectSkill(cli, sk.id, skills));
71
94
  const red = redactAgent(a);
72
- const entry = { raw: a, red, skillNames };
95
+ const entry = { raw: a, red, skill_names };
73
96
  agentsById.set(id, entry);
74
97
  return entry;
75
98
  }
76
99
 
77
- export function exportResource({ cli, scope, ids, outDir, sourceWorkspaceId, fs = nodeFs }) {
100
+ export function exportResource({ cli, scope, ids, outDir, sourceWorkspaceId, fs = nodeFs, download = fetchBinary }) {
78
101
  const skills = new Map(); // name -> normalized skill
79
- const agentsById = new Map(); // id -> { raw, red, skillNames }
80
- let squad = null;
102
+ const agentsById = new Map(); // id -> { raw, red, skill_names }
103
+ const squads = [];
81
104
  // Lazy + memoized: only fetched when an agent is actually collected (skips
82
105
  // the extra CLI call on skill-only exports).
83
106
  let providerById = null;
84
107
  const getProviderById = () => providerById ??= new Map(listRuntimes(cli).map((r) => [r.id, r.provider]));
85
108
 
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.memberType === "agent");
91
- for (const m of members) collectAgent(cli, m.memberId, agentsById, skills, getProviderById());
92
- if (!agentsById.has(sq.leaderId)) collectAgent(cli, sq.leaderId, agentsById, skills, getProviderById());
109
+ // Collect a squad's agents (leader + members) and return the squad bundle object.
110
+ function collectOneSquad(squadId) {
111
+ const sq = getSquad(cli, squadId);
112
+ const members = getSquadMembers(cli, squadId).filter((m) => m.member_type === "agent");
113
+ for (const m of members) collectAgent(cli, m.member_id, agentsById, skills, getProviderById());
114
+ if (!agentsById.has(sq.leader_id)) collectAgent(cli, sq.leader_id, agentsById, skills, getProviderById());
93
115
  const nameOf = (id) => agentsById.get(id)?.raw.name;
94
- squad = {
95
- name: sq.name,
96
- description: sq.description,
97
- instructions: sq.instructions,
98
- leaderName: nameOf(sq.leaderId),
99
- members: members.map((m) => ({ agentName: nameOf(m.memberId), role: m.role })),
116
+ return {
117
+ name: sq.name, description: sq.description, instructions: sq.instructions, avatar_url: sq.avatar_url,
118
+ leader_name: nameOf(sq.leader_id),
119
+ members: members.map((m) => ({ agent_name: nameOf(m.member_id), role: m.role })),
100
120
  };
101
121
  }
102
122
 
123
+ if (scope === "skill") collectSkill(cli, ids.skillId, skills);
124
+ else if (scope === "agent") collectAgent(cli, ids.agentId, agentsById, skills, getProviderById());
125
+ else if (scope === "squad") squads.push(collectOneSquad(ids.squadId));
126
+ else if (scope === "all") {
127
+ for (const s of listSkills(cli)) collectSkill(cli, s.id, skills);
128
+ for (const a of listAgents(cli)) collectAgent(cli, a.id, agentsById, skills, getProviderById());
129
+ for (const sq of listSquads(cli)) squads.push(collectOneSquad(sq.id));
130
+ }
131
+
103
132
  const manifest = buildManifest({
104
133
  scope, sourceWorkspaceId,
105
- skills: [...skills.values()].map((s) => ({ name: s.name, sourceId: s.id })),
106
- agents: [...agentsById.values()].map((a) => ({ name: a.raw.name, sourceId: a.raw.id, sourceRuntimeId: a.raw.runtimeId, sourceRuntimeProvider: a.raw.sourceRuntimeProvider, skillNames: a.skillNames, hadSecrets: a.red.hadSecrets })),
107
- squad,
134
+ skills: [...skills.values()].map((s) => ({ name: s.name, source_id: s.id })),
135
+ 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 })),
136
+ squads,
108
137
  });
109
138
 
110
139
  const warnings = [];
@@ -125,10 +154,22 @@ export function exportResource({ cli, scope, ids, outDir, sourceWorkspaceId, fs
125
154
  // Index agent entries by name for the manifest writing loop.
126
155
  const agentByName = new Map([...agentsById.values()].map((a) => [a.raw.name, a]));
127
156
  for (const entry of manifest.agents) {
128
- const { raw, red, skillNames } = agentByName.get(entry.name);
129
- const record = { ...red.record, skillNames };
157
+ const { raw, red, skill_names } = agentByName.get(entry.name);
158
+ const record = { ...red.record, skill_names };
130
159
  if (red.hadSecrets) warnings.push(raw.name);
131
160
  fs.mkdirSync(`${outDir}/agents`, { recursive: true });
161
+ // Download an uploaded-image avatar into the bundle so import can re-upload
162
+ // it (the only way to set an agent avatar). Emoji avatars stay as the
163
+ // recorded avatar_url string; a failed download leaves avatar_url without a
164
+ // file, and import simply won't restore it.
165
+ if (isImageAvatar(record.avatar_url)) {
166
+ const rel = entry.file.replace(/\.json$/, `.avatar${avatarExt(record.avatar_url)}`);
167
+ const bytes = download(record.avatar_url);
168
+ if (bytes && bytes.length) {
169
+ fs.writeFileSync(`${outDir}/${rel}`, bytes);
170
+ record.avatar_file = rel;
171
+ }
172
+ }
132
173
  fs.writeFileSync(`${outDir}/${entry.file}`, JSON.stringify(record, null, 2));
133
174
  }
134
175
  for (const entry of manifest.squads) {
@@ -148,8 +189,8 @@ function main() {
148
189
  const out = get("--out");
149
190
  const workspace = get("--workspace"); // optional: source workspace name
150
191
 
151
- if (!scope || !id || !out) {
152
- console.error("Usage: multica-export.mjs --scope <skill|agent|squad> --id <id> --out <dir> [--workspace <name>]");
192
+ if (!scope || !out || (scope !== "all" && !id)) {
193
+ console.error("Usage: multica-export.mjs --scope <skill|agent|squad|all> --id <id> --out <dir> [--workspace <name>] (--id not needed for --scope all)");
153
194
  process.exit(1);
154
195
  }
155
196
 
@@ -164,7 +205,8 @@ function main() {
164
205
  if (scope === "skill") ids.skillId = id;
165
206
  else if (scope === "agent") ids.agentId = id;
166
207
  else if (scope === "squad") ids.squadId = id;
167
- else { console.error(`Unknown scope "${scope}"use skill|agent|squad`); process.exit(1); }
208
+ else if (scope === "all") { /* whole workspace no id */ }
209
+ else { console.error(`Unknown scope "${scope}" — use skill|agent|squad|all`); process.exit(1); }
168
210
 
169
211
  const result = exportResource({ cli, scope, ids, outDir: out, sourceWorkspaceId, fs: nodeFs });
170
212
  console.log(JSON.stringify(result, null, 2));
@@ -1,5 +1,5 @@
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
3
 
4
4
  // Relative paths of every file under root (recursing into subdirs like scripts/).
5
5
  function walkSkillFiles(fs, root, rel = "") {
@@ -55,28 +55,39 @@ export function importSkills({ cli, manifest, dir, fs = nodeFs }) {
55
55
  return { idMap, created, updated };
56
56
  }
57
57
 
58
+ // True when a resource already carries an avatar (uploaded image or emoji).
59
+ const hasAvatar = (r) => !!(r && typeof r.avatar_url === "string" && r.avatar_url);
60
+
58
61
  export function importAgents({ cli, manifest, dir, skillIdMap, runtimeMap, fs = nodeFs }) {
59
62
  const idMap = new Map();
60
63
  const sourceIdMap = new Map(); // source agent id -> new agent id, for mention rewriting
61
64
  const secretsApplyFailures = [];
65
+ const avatarApplyFailures = []; // image upload rejected
66
+ const avatarUnsupported = []; // emoji avatar — no CLI setter for agents
67
+ const permissionApplyFailures = []; // CLI rejected --public-to-member
68
+ const permissionUnsupported = []; // no member target resolved in the destination
69
+ // Lazy + memoized: destination member user_ids, only listed when an agent needs them.
70
+ let destMemberIds = null;
71
+ const getDestMemberIds = () => destMemberIds ??= new Set(listWorkspaceMembers(cli).map((m) => m.user_id));
62
72
  let created = 0, updated = 0;
63
73
  const existing = listAgents(cli);
64
74
 
65
75
  for (const a of manifest.agents) {
66
76
  const rec = JSON.parse(fs.readFileSync(`${dir}/${a.file}`, "utf8"));
67
- const targetRuntime = runtimeMap.get(rec.sourceRuntimeId);
68
- if (!targetRuntime) throw new Error(`Unmapped runtime "${rec.sourceRuntimeId}" for agent "${rec.name}"`);
77
+ const targetRuntime = runtimeMap.get(rec.source_runtime_id);
78
+ if (!targetRuntime) throw new Error(`Unmapped runtime "${rec.source_runtime_id}" for agent "${rec.name}"`);
69
79
  // Only pass optional flags when present — `--model ""` would CLEAR the model.
70
80
  const common = [
71
81
  "--visibility", rec.visibility ?? "private",
72
- "--max-concurrent-tasks", String(rec.maxConcurrentTasks ?? 6),
82
+ "--max-concurrent-tasks", String(rec.max_concurrent_tasks ?? 6),
73
83
  ];
74
84
  if (rec.description) common.push("--description", rec.description);
75
85
  if (rec.instructions) common.push("--instructions", rec.instructions);
76
86
  if (rec.model) common.push("--model", rec.model);
77
- if (rec.thinkingLevel) common.push("--thinking-level", rec.thinkingLevel);
78
- if (rec.runtimeConfig && Object.keys(rec.runtimeConfig).length) common.push("--runtime-config", JSON.stringify(rec.runtimeConfig));
79
- if (Array.isArray(rec.customArgs) && rec.customArgs.length) common.push("--custom-args", JSON.stringify(rec.customArgs));
87
+ if (rec.thinking_level) common.push("--thinking-level", rec.thinking_level);
88
+ if (rec.runtime_config && Object.keys(rec.runtime_config).length) common.push("--runtime-config", JSON.stringify(rec.runtime_config));
89
+ if (Array.isArray(rec.custom_args) && rec.custom_args.length) common.push("--custom-args", JSON.stringify(rec.custom_args));
90
+ if (rec.service_tier) common.push("--service-tier", rec.service_tier);
80
91
  const match = findByName(existing, rec.name);
81
92
  let id;
82
93
  if (match) {
@@ -87,36 +98,69 @@ export function importAgents({ cli, manifest, dir, skillIdMap, runtimeMap, fs =
87
98
  id = JSON.parse(out).id; created++;
88
99
  }
89
100
  idMap.set(rec.name, id);
90
- if (rec.sourceId) sourceIdMap.set(rec.sourceId, id);
91
- const skillIds = (rec.skillNames ?? []).map((n) => skillIdMap.get(n)).filter(Boolean);
101
+ if (rec.source_id) sourceIdMap.set(rec.source_id, id);
102
+ const skillIds = (rec.skill_names ?? []).map((n) => skillIdMap.get(n)).filter(Boolean);
92
103
  cli.run(["agent", "skills", "set", id, "--skill-ids", skillIds.join(",")]);
93
104
 
105
+ // Avatar: only ever set it when the target agent has none — never clobber an
106
+ // avatar an existing agent already has. Agents accept only image uploads
107
+ // (`agent avatar --file`); an emoji-only avatar can't be restored via the CLI.
108
+ if (!hasAvatar(match)) {
109
+ if (rec.avatar_file) {
110
+ try {
111
+ cli.run(["agent", "avatar", id, "--file", `${dir}/${rec.avatar_file}`]);
112
+ } catch {
113
+ avatarApplyFailures.push(rec.name);
114
+ }
115
+ } else if (typeof rec.avatar_url === "string" && rec.avatar_url.startsWith("emoji:")) {
116
+ avatarUnsupported.push(rec.name);
117
+ }
118
+ }
119
+
120
+ // Restore member-specific public_to sharing that --visibility can't express.
121
+ // (private and workspace-wide public_to already round-trip via --visibility.)
122
+ if (rec.permission_mode === "public_to") {
123
+ const memberTargets = (rec.invocation_targets ?? []).filter((t) => t.target_type === "user");
124
+ if (memberTargets.length) {
125
+ const resolvable = memberTargets.map((t) => t.target_id).filter((tid) => getDestMemberIds().has(tid));
126
+ if (!resolvable.length) {
127
+ permissionUnsupported.push(rec.name);
128
+ } else {
129
+ try {
130
+ cli.run(["agent", "update", id, "--permission-mode", "public_to", ...resolvable.flatMap((tid) => ["--public-to-member", tid])]);
131
+ } catch {
132
+ permissionApplyFailures.push(rec.name);
133
+ }
134
+ }
135
+ }
136
+ }
137
+
94
138
  // mcp_config/custom_env carry real secrets. Each is applied via its OWN
95
139
  // follow-up call, never bundled into the create/update call above — that
96
140
  // keeps a rejected secret from failing the whole agent create/update, and
97
141
  // sidesteps the fact that only one stdin payload can be read per process
98
142
  // anyway. `agent update --mcp-config-stdin` works on a freshly-created id
99
143
  // too, so no create/update branching is needed here.
100
- const hasMcpConfig = rec.mcpConfig && Object.keys(rec.mcpConfig).length > 0;
144
+ const hasMcpConfig = rec.mcp_config && Object.keys(rec.mcp_config).length > 0;
101
145
  if (hasMcpConfig) {
102
146
  try {
103
- cli.run(["agent", "update", id, "--mcp-config-stdin"], { input: JSON.stringify(rec.mcpConfig) });
147
+ cli.run(["agent", "update", id, "--mcp-config-stdin"], { input: JSON.stringify(rec.mcp_config) });
104
148
  } catch {
105
149
  secretsApplyFailures.push(rec.name);
106
150
  }
107
151
  }
108
152
  // custom_env has no flag on `agent update` at all — `agent env set` is the
109
153
  // only way to set it on an existing agent, so it's always a follow-up call.
110
- const hasCustomEnv = rec.customEnv && Object.keys(rec.customEnv).length > 0;
154
+ const hasCustomEnv = rec.custom_env && Object.keys(rec.custom_env).length > 0;
111
155
  if (hasCustomEnv) {
112
156
  try {
113
- cli.run(["agent", "env", "set", id, "--custom-env-stdin"], { input: JSON.stringify(rec.customEnv) });
157
+ cli.run(["agent", "env", "set", id, "--custom-env-stdin"], { input: JSON.stringify(rec.custom_env) });
114
158
  } catch {
115
159
  secretsApplyFailures.push(rec.name);
116
160
  }
117
161
  }
118
162
  }
119
- return { idMap, sourceIdMap, created, updated, secretsApplyFailures };
163
+ return { idMap, sourceIdMap, created, updated, secretsApplyFailures, avatarApplyFailures, avatarUnsupported, permissionApplyFailures, permissionUnsupported };
120
164
  }
121
165
 
122
166
  // Rewrites `mention://agent/<id>` links (e.g. `[@dev-backend](mention://agent/<id>)`)
@@ -151,7 +195,7 @@ export function rewriteAgentMentions({ cli, manifest, dir, agentIdMap, sourceIdM
151
195
 
152
196
  export function importSquad({ cli, squad, agentIdMap, sourceIdMap }) {
153
197
  const existing = listSquads(cli);
154
- const leaderId = agentIdMap.get(squad.leaderName);
198
+ const leaderId = agentIdMap.get(squad.leader_name);
155
199
  const match = findByName(existing, squad.name);
156
200
  let id, created = 0, updated = 0;
157
201
  // Squad instructions commonly list @mentions of teammate agents by their
@@ -165,11 +209,18 @@ export function importSquad({ cli, squad, agentIdMap, sourceIdMap }) {
165
209
  const out = cli.run(["squad", "create", "--name", squad.name, "--leader", leaderId, "--description", squad.description ?? "", ...instr]);
166
210
  id = JSON.parse(out).id; created++;
167
211
  }
212
+ // Avatar: squads accept only an avatar-url string (emoji or URL) via
213
+ // `squad update`. Set it only when the target squad has none — never clobber
214
+ // an existing squad's avatar.
215
+ if (!hasAvatar(match) && squad.avatar_url) {
216
+ cli.run(["squad", "update", id, "--avatar-url", squad.avatar_url]);
217
+ }
218
+
168
219
  // Add non-leader members, skipping any already present so re-runs are idempotent.
169
- const present = new Set(getSquadMembers(cli, id).map((m) => m.memberId));
220
+ const present = new Set(getSquadMembers(cli, id).map((m) => m.member_id));
170
221
  for (const m of squad.members) {
171
- if (m.agentName === squad.leaderName) continue;
172
- const memberId = agentIdMap.get(m.agentName);
222
+ if (m.agent_name === squad.leader_name) continue;
223
+ const memberId = agentIdMap.get(m.agent_name);
173
224
  if (present.has(memberId)) continue;
174
225
  cli.run(["squad", "member", "add", id, "--member-id", memberId, "--role", m.role, "--type", "agent"]);
175
226
  }
@@ -177,15 +228,15 @@ export function importSquad({ cli, squad, agentIdMap, sourceIdMap }) {
177
228
  }
178
229
 
179
230
  export function collectSourceRuntimes(manifest) {
180
- return [...new Set((manifest.agents ?? []).map((a) => a.sourceRuntimeId).filter(Boolean))];
231
+ return [...new Set((manifest.agents ?? []).map((a) => a.source_runtime_id).filter(Boolean))];
181
232
  }
182
233
 
183
- // sourceRuntimeId -> provider (e.g. "claude", "opencode"), from whichever agent recorded it.
234
+ // source_runtime_id -> provider (e.g. "claude", "opencode"), from whichever agent recorded it.
184
235
  function collectRuntimeProviders(manifest) {
185
236
  const map = new Map();
186
237
  for (const a of manifest.agents ?? []) {
187
- if (a.sourceRuntimeId && a.sourceRuntimeProvider && !map.has(a.sourceRuntimeId)) {
188
- map.set(a.sourceRuntimeId, a.sourceRuntimeProvider);
238
+ if (a.source_runtime_id && a.source_runtime_provider && !map.has(a.source_runtime_id)) {
239
+ map.set(a.source_runtime_id, a.source_runtime_provider);
189
240
  }
190
241
  }
191
242
  return map;
@@ -226,18 +277,28 @@ export function importBundle({ cli, dir, runtimeMap, fs = nodeFs }) {
226
277
  const agentRes = importAgents({ cli, manifest, dir, skillIdMap: skillRes.idMap, runtimeMap: effective, fs });
227
278
  // Runs after every agent exists so forward-referencing mentions resolve.
228
279
  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 });
280
+ const squadIdMap = new Map();
281
+ let squadsCreated = 0, squadsUpdated = 0;
282
+ for (const squad of manifest.squads ?? []) {
283
+ const r = importSquad({ cli, squad, agentIdMap: agentRes.idMap, sourceIdMap: agentRes.sourceIdMap });
284
+ squadIdMap.set(squad.name, r.newId);
285
+ squadsCreated += r.created;
286
+ squadsUpdated += r.updated;
287
+ }
231
288
 
232
289
  return {
233
- created: { skills: skillRes.created, agents: agentRes.created, squads: squadRes.created },
234
- updated: { skills: skillRes.updated, agents: agentRes.updated, squads: squadRes.updated },
290
+ created: { skills: skillRes.created, agents: agentRes.created, squads: squadsCreated },
291
+ updated: { skills: skillRes.updated, agents: agentRes.updated, squads: squadsUpdated },
235
292
  mentionsRewritten: mentionRes.updated,
236
293
  skillIdMap: Object.fromEntries(skillRes.idMap),
237
294
  agentIdMap: Object.fromEntries(agentRes.idMap),
238
- squadId: squadRes.newId,
239
- secretsReminder: (manifest.agents ?? []).filter((a) => a.hadSecrets).map((a) => a.name),
295
+ squadIdMap: Object.fromEntries(squadIdMap),
296
+ secretsReminder: (manifest.agents ?? []).filter((a) => a.had_secrets).map((a) => a.name),
240
297
  secretsApplyFailures: agentRes.secretsApplyFailures,
298
+ avatarApplyFailures: agentRes.avatarApplyFailures,
299
+ avatarUnsupported: agentRes.avatarUnsupported,
300
+ permissionApplyFailures: agentRes.permissionApplyFailures,
301
+ permissionUnsupported: agentRes.permissionUnsupported,
241
302
  };
242
303
  }
243
304
 
@@ -44,14 +44,18 @@ 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
+ 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
+
53
55
  The script writes `manifest.json`, skill SKILL.md files, agent JSON files, and squad JSON files into `<dir>`.
54
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.
58
+
55
59
  ## Step 5 — Report results
56
60
 
57
61
  Parse the JSON output from the script and report:
@@ -25,11 +25,11 @@ node "${CLAUDE_PLUGIN_ROOT}/scripts/multica-import.mjs" \
25
25
  If it aborts with `Unmapped runtimes: ...` (0 or 2+ runtimes share that provider in the target workspace, or the bundle predates provider capture), resolve manually:
26
26
 
27
27
  ```bash
28
- cat <folder>/manifest.json # note each distinct sourceRuntimeId
28
+ cat <folder>/manifest.json # note each distinct source_runtime_id
29
29
  multica runtime list --output json # list target workspace runtimes
30
30
  ```
31
31
 
32
- Ask the user to pick a matching target runtime by name or ID for each unmapped `sourceRuntimeId`, then re-run with an explicit map (explicit entries always take precedence over auto-mapping):
32
+ Ask the user to pick a matching target runtime by name or ID for each unmapped `source_runtime_id`, then re-run with an explicit map (explicit entries always take precedence over auto-mapping):
33
33
 
34
34
  ```bash
35
35
  node "${CLAUDE_PLUGIN_ROOT}/scripts/multica-import.mjs" \
@@ -40,13 +40,19 @@ 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
+ 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.
44
+
43
45
  ## Step 3 — Report results
44
46
 
45
47
  Parse the JSON output and report:
46
48
 
47
49
  - Created and updated counts for skills, agents, and squads.
48
50
  - Name-to-ID maps for skills and agents (`skillIdMap`, `agentIdMap`).
49
- - Squad ID if a squad was imported.
51
+ - `squadIdMap`: name-to-ID map for every squad imported.
50
52
  - `mentionsRewritten`: how many agents had an agent-mention link rewritten to its new id.
51
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 source export directory should be treated as sensitive: `<agent-name>`."
52
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 import (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 Multica UI: `<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 Multica UI: `<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 Multica UI: `<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 target workspace — re-share manually if needed: `<agent-name>`."
@@ -33,7 +33,7 @@ If it aborts with `Unmapped runtimes: ...` (0 or 2+ runtimes share that provider
33
33
  multica runtime list --workspace-id <dest-ws-id> --output json # list destination workspace runtimes
34
34
  ```
35
35
 
36
- Ask the user to select a matching target runtime by name or ID for each unmapped `sourceRuntimeId`, then re-run with an explicit map (explicit entries always take precedence over auto-mapping):
36
+ Ask the user to select a matching target runtime by name or ID for each unmapped `source_runtime_id`, then re-run with an explicit map (explicit entries always take precedence over auto-mapping):
37
37
 
38
38
  ```bash
39
39
  node "${CLAUDE_PLUGIN_ROOT}/scripts/multica-sync.mjs" \
@@ -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.37",
4
+ "version": "0.0.39",
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.37",
4
+ "version": "0.0.39",
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.37",
3
+ "version": "0.0.39",
4
4
  "description": "Step-by-step wizard for generating technical diagrams as SVG+PNG.",
5
5
  "author": {
6
6
  "name": "steven"