@drunkcoding/agents-and-skills 0.0.36 → 0.0.38

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.36",
15
+ "version": "0.0.38",
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.36",
29
+ "version": "0.0.38",
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.36",
44
+ "version": "0.0.38",
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.36",
60
+ "version": "0.0.38",
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.36",
76
+ "version": "0.0.38",
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.36",
3
+ "version": "0.0.38",
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.36",
4
+ "version": "0.0.38",
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.36",
3
+ "version": "0.0.38",
4
4
  "description": "Export, import, and sync Multica skills, agents, and squads between workspaces via the multica CLI."
5
5
  }
@@ -8,17 +8,17 @@ export function slugify(name) {
8
8
  return s || "unnamed";
9
9
  }
10
10
 
11
- export function realExec(args) {
12
- return spawnSync("multica", args, { encoding: "utf8" });
11
+ export function realExec(args, opts = {}) {
12
+ return spawnSync("multica", args, { encoding: "utf8", ...opts });
13
13
  }
14
14
 
15
15
  export function makeCli(exec, { workspaceId } = {}) {
16
- function run(args) {
16
+ function run(args, opts) {
17
17
  let full = args;
18
18
  if (workspaceId) {
19
19
  full = [...args, "--workspace-id", workspaceId];
20
20
  }
21
- const res = exec(full);
21
+ const res = exec(full, opts);
22
22
  if (res.status !== 0) throw new Error(res.stderr?.trim() || `multica exited ${res.status}`);
23
23
  return res.stdout;
24
24
  }
@@ -55,8 +55,9 @@ 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
57
 
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.
58
+ // Get-wrappers: the ONLY place that knows the raw CLI field names — an
59
+ // explicit allow-list, so unexpected/internal CLI fields never leak into a
60
+ // bundle. Field names mirror the CLI's own snake_case; nothing is renamed.
60
61
  export function getSkill(cli, id) {
61
62
  const s = cli.json(["skill", "get", id]);
62
63
  return {
@@ -71,23 +72,31 @@ export function getAgent(cli, id) {
71
72
  return {
72
73
  id: a.id, name: a.name, description: a.description, instructions: a.instructions,
73
74
  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,
75
+ max_concurrent_tasks: a.max_concurrent_tasks,
76
+ runtime_config: a.runtime_config,
77
+ custom_args: a.custom_args,
78
+ thinking_level: a.thinking_level,
79
+ runtime_id: a.runtime_id,
80
+ has_custom_env: a.has_custom_env,
81
+ mcp_config: a.mcp_config,
82
+ mcp_config_redacted: !!a.mcp_config_redacted,
81
83
  skills: (a.skills ?? []).map((sk) => ({ id: sk.id, name: sk.name })),
82
84
  };
83
85
  }
84
86
 
87
+ // Custom env is never included in `agent get` — only `has_custom_env`/`custom_env_key_count`.
88
+ // Reading actual values requires this dedicated, audited, owner/admin-only command.
89
+ export function getAgentCustomEnv(cli, id) {
90
+ const r = cli.json(["agent", "env", "get", id]);
91
+ return r.custom_env ?? {};
92
+ }
93
+
85
94
  export function getSquad(cli, id) {
86
95
  const s = cli.json(["squad", "get", id]);
87
- return { id: s.id, name: s.name, description: s.description, instructions: s.instructions, leaderId: s.leader_id };
96
+ return { id: s.id, name: s.name, description: s.description, instructions: s.instructions, leader_id: s.leader_id };
88
97
  }
89
98
 
90
99
  export const getSquadMembers = (cli, id) =>
91
100
  (cli.json(["squad", "member", "list", id]) ?? []).map((m) => ({
92
- memberId: m.member_id, memberType: m.member_type, role: m.role || "member",
101
+ member_id: m.member_id, member_type: m.member_type, role: m.role || "member",
93
102
  }));
@@ -1,15 +1,32 @@
1
1
  import * as nodeFs from "node:fs";
2
2
  import { dirname } from "node:path";
3
- import { slugify, getSkill, getAgent, getSquad, getSquadMembers, listRuntimes, makeCli, realExec, requireAuth, resolveWorkspaceId } from "./lib.mjs";
3
+ import { slugify, getSkill, getAgent, getAgentCustomEnv, getSquad, getSquadMembers, listRuntimes, makeCli, realExec, requireAuth, resolveWorkspaceId } from "./lib.mjs";
4
4
 
5
5
  const nonEmpty = (v) => v && typeof v === "object" && Object.keys(v).length > 0;
6
6
 
7
7
  export function redactAgent(a) {
8
- // a is a normalized (camelCase) agent from getAgent.
9
- const { id, hasCustomEnv, mcpConfig, skills, runtimeId, ...rest } = a;
10
- const hadSecrets = !!hasCustomEnv || nonEmpty(mcpConfig);
8
+ // a is a normalized agent from getAgent, with `custom_env`/
9
+ // `custom_env_fetch_failed` attached by the caller (collectAgent) getAgent
10
+ // 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;
12
+ const mcpUsable = !mcp_config_redacted && nonEmpty(mcp_config);
13
+ const envUsable = !custom_env_fetch_failed && nonEmpty(custom_env);
14
+ // mcp_config_redacted / custom_env_fetch_failed alone still flag hadSecrets even
15
+ // when unusable — the user should know something was present at the source
16
+ // but couldn't be captured, not just silently see an empty bundle.
17
+ const hadSecrets = mcpUsable || envUsable || !!mcp_config_redacted || !!custom_env_fetch_failed;
11
18
  return {
12
- record: { ...rest, sourceRuntimeId: runtimeId, skillNames: [], hadSecrets },
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
+ record: {
22
+ ...rest,
23
+ source_id: id,
24
+ source_runtime_id: runtime_id,
25
+ skill_names: [],
26
+ mcp_config: mcpUsable ? mcp_config : null,
27
+ custom_env: envUsable ? custom_env : null,
28
+ had_secrets: hadSecrets,
29
+ },
13
30
  hadSecrets,
14
31
  };
15
32
  }
@@ -22,10 +39,10 @@ export function buildManifest({ scope, sourceWorkspaceId, skills, agents, squad
22
39
  return {
23
40
  version: "1",
24
41
  scope,
25
- sourceWorkspaceId,
26
- skills: [...seenSkills.values()].map((s) => ({ name: s.name, dir: `skills/${slugify(s.name)}`, sourceId: s.sourceId })),
27
- agents: [...seenAgents.values()].map((a) => ({ name: a.name, file: `agents/${slugify(a.name)}.json`, sourceRuntimeId: a.sourceRuntimeId, sourceRuntimeProvider: a.sourceRuntimeProvider ?? null, skillNames: a.skillNames, hadSecrets: !!a.hadSecrets })),
28
- squads: squad ? [{ name: squad.name, file: `squads/${slugify(squad.name)}.json`, description: squad.description ?? "", instructions: squad.instructions ?? "", leaderName: squad.leaderName, members: squad.members }] : [],
42
+ source_workspace_id: sourceWorkspaceId,
43
+ skills: [...seenSkills.values()].map((s) => ({ name: s.name, dir: `skills/${slugify(s.name)}`, source_id: s.source_id })),
44
+ 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 }] : [],
29
46
  };
30
47
  }
31
48
 
@@ -35,22 +52,31 @@ function collectSkill(cli, id, skills) {
35
52
  return s.name;
36
53
  }
37
54
 
38
- // Keyed by agent id (so squad leaderId/memberId resolve to names). Stores the
55
+ // Keyed by agent id (so squad leader_id/member_id resolve to names). Stores the
39
56
  // normalized agent, its redaction result, and its skill names.
40
57
  function collectAgent(cli, id, agentsById, skills, providerById) {
41
58
  if (agentsById.has(id)) return agentsById.get(id);
42
59
  const a = getAgent(cli, id);
43
- a.sourceRuntimeProvider = providerById.get(a.runtimeId) ?? null;
44
- const skillNames = a.skills.map((sk) => collectSkill(cli, sk.id, skills));
60
+ a.source_runtime_provider = providerById.get(a.runtime_id) ?? null;
61
+ a.custom_env = {};
62
+ a.custom_env_fetch_failed = false;
63
+ if (a.has_custom_env) {
64
+ try {
65
+ a.custom_env = getAgentCustomEnv(cli, id);
66
+ } catch {
67
+ a.custom_env_fetch_failed = true; // e.g. insufficient permission — non-fatal, warned via hadSecrets
68
+ }
69
+ }
70
+ const skill_names = a.skills.map((sk) => collectSkill(cli, sk.id, skills));
45
71
  const red = redactAgent(a);
46
- const entry = { raw: a, red, skillNames };
72
+ const entry = { raw: a, red, skill_names };
47
73
  agentsById.set(id, entry);
48
74
  return entry;
49
75
  }
50
76
 
51
77
  export function exportResource({ cli, scope, ids, outDir, sourceWorkspaceId, fs = nodeFs }) {
52
78
  const skills = new Map(); // name -> normalized skill
53
- const agentsById = new Map(); // id -> { raw, red, skillNames }
79
+ const agentsById = new Map(); // id -> { raw, red, skill_names }
54
80
  let squad = null;
55
81
  // Lazy + memoized: only fetched when an agent is actually collected (skips
56
82
  // the extra CLI call on skill-only exports).
@@ -61,23 +87,23 @@ export function exportResource({ cli, scope, ids, outDir, sourceWorkspaceId, fs
61
87
  if (scope === "agent") collectAgent(cli, ids.agentId, agentsById, skills, getProviderById());
62
88
  if (scope === "squad") {
63
89
  const sq = getSquad(cli, ids.squadId);
64
- const members = getSquadMembers(cli, ids.squadId).filter((m) => m.memberType === "agent");
65
- for (const m of members) collectAgent(cli, m.memberId, agentsById, skills, getProviderById());
66
- if (!agentsById.has(sq.leaderId)) collectAgent(cli, sq.leaderId, agentsById, skills, getProviderById());
90
+ const members = getSquadMembers(cli, ids.squadId).filter((m) => m.member_type === "agent");
91
+ for (const m of members) collectAgent(cli, m.member_id, agentsById, skills, getProviderById());
92
+ if (!agentsById.has(sq.leader_id)) collectAgent(cli, sq.leader_id, agentsById, skills, getProviderById());
67
93
  const nameOf = (id) => agentsById.get(id)?.raw.name;
68
94
  squad = {
69
95
  name: sq.name,
70
96
  description: sq.description,
71
97
  instructions: sq.instructions,
72
- leaderName: nameOf(sq.leaderId),
73
- members: members.map((m) => ({ agentName: nameOf(m.memberId), role: m.role })),
98
+ leader_name: nameOf(sq.leader_id),
99
+ members: members.map((m) => ({ agent_name: nameOf(m.member_id), role: m.role })),
74
100
  };
75
101
  }
76
102
 
77
103
  const manifest = buildManifest({
78
104
  scope, sourceWorkspaceId,
79
- skills: [...skills.values()].map((s) => ({ name: s.name, sourceId: s.id })),
80
- agents: [...agentsById.values()].map((a) => ({ name: a.raw.name, sourceRuntimeId: a.raw.runtimeId, sourceRuntimeProvider: a.raw.sourceRuntimeProvider, skillNames: a.skillNames, hadSecrets: a.red.hadSecrets })),
105
+ skills: [...skills.values()].map((s) => ({ name: s.name, source_id: s.id })),
106
+ 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 })),
81
107
  squad,
82
108
  });
83
109
 
@@ -99,8 +125,8 @@ export function exportResource({ cli, scope, ids, outDir, sourceWorkspaceId, fs
99
125
  // Index agent entries by name for the manifest writing loop.
100
126
  const agentByName = new Map([...agentsById.values()].map((a) => [a.raw.name, a]));
101
127
  for (const entry of manifest.agents) {
102
- const { raw, red, skillNames } = agentByName.get(entry.name);
103
- const record = { ...red.record, skillNames };
128
+ const { raw, red, skill_names } = agentByName.get(entry.name);
129
+ const record = { ...red.record, skill_names };
104
130
  if (red.hadSecrets) warnings.push(raw.name);
105
131
  fs.mkdirSync(`${outDir}/agents`, { recursive: true });
106
132
  fs.writeFileSync(`${outDir}/${entry.file}`, JSON.stringify(record, null, 2));
@@ -57,24 +57,26 @@ export function importSkills({ cli, manifest, dir, fs = nodeFs }) {
57
57
 
58
58
  export function importAgents({ cli, manifest, dir, skillIdMap, runtimeMap, fs = nodeFs }) {
59
59
  const idMap = new Map();
60
+ const sourceIdMap = new Map(); // source agent id -> new agent id, for mention rewriting
61
+ const secretsApplyFailures = [];
60
62
  let created = 0, updated = 0;
61
63
  const existing = listAgents(cli);
62
64
 
63
65
  for (const a of manifest.agents) {
64
66
  const rec = JSON.parse(fs.readFileSync(`${dir}/${a.file}`, "utf8"));
65
- const targetRuntime = runtimeMap.get(rec.sourceRuntimeId);
66
- if (!targetRuntime) throw new Error(`Unmapped runtime "${rec.sourceRuntimeId}" for agent "${rec.name}"`);
67
+ const targetRuntime = runtimeMap.get(rec.source_runtime_id);
68
+ if (!targetRuntime) throw new Error(`Unmapped runtime "${rec.source_runtime_id}" for agent "${rec.name}"`);
67
69
  // Only pass optional flags when present — `--model ""` would CLEAR the model.
68
70
  const common = [
69
71
  "--visibility", rec.visibility ?? "private",
70
- "--max-concurrent-tasks", String(rec.maxConcurrentTasks ?? 6),
72
+ "--max-concurrent-tasks", String(rec.max_concurrent_tasks ?? 6),
71
73
  ];
72
74
  if (rec.description) common.push("--description", rec.description);
73
75
  if (rec.instructions) common.push("--instructions", rec.instructions);
74
76
  if (rec.model) common.push("--model", rec.model);
75
- if (rec.thinkingLevel) common.push("--thinking-level", rec.thinkingLevel);
76
- if (rec.runtimeConfig && Object.keys(rec.runtimeConfig).length) common.push("--runtime-config", JSON.stringify(rec.runtimeConfig));
77
- if (Array.isArray(rec.customArgs) && rec.customArgs.length) common.push("--custom-args", JSON.stringify(rec.customArgs));
77
+ if (rec.thinking_level) common.push("--thinking-level", rec.thinking_level);
78
+ if (rec.runtime_config && Object.keys(rec.runtime_config).length) common.push("--runtime-config", JSON.stringify(rec.runtime_config));
79
+ if (Array.isArray(rec.custom_args) && rec.custom_args.length) common.push("--custom-args", JSON.stringify(rec.custom_args));
78
80
  const match = findByName(existing, rec.name);
79
81
  let id;
80
82
  if (match) {
@@ -85,18 +87,77 @@ export function importAgents({ cli, manifest, dir, skillIdMap, runtimeMap, fs =
85
87
  id = JSON.parse(out).id; created++;
86
88
  }
87
89
  idMap.set(rec.name, id);
88
- const skillIds = (rec.skillNames ?? []).map((n) => skillIdMap.get(n)).filter(Boolean);
90
+ if (rec.source_id) sourceIdMap.set(rec.source_id, id);
91
+ const skillIds = (rec.skill_names ?? []).map((n) => skillIdMap.get(n)).filter(Boolean);
89
92
  cli.run(["agent", "skills", "set", id, "--skill-ids", skillIds.join(",")]);
93
+
94
+ // mcp_config/custom_env carry real secrets. Each is applied via its OWN
95
+ // follow-up call, never bundled into the create/update call above — that
96
+ // keeps a rejected secret from failing the whole agent create/update, and
97
+ // sidesteps the fact that only one stdin payload can be read per process
98
+ // anyway. `agent update --mcp-config-stdin` works on a freshly-created id
99
+ // too, so no create/update branching is needed here.
100
+ const hasMcpConfig = rec.mcp_config && Object.keys(rec.mcp_config).length > 0;
101
+ if (hasMcpConfig) {
102
+ try {
103
+ cli.run(["agent", "update", id, "--mcp-config-stdin"], { input: JSON.stringify(rec.mcp_config) });
104
+ } catch {
105
+ secretsApplyFailures.push(rec.name);
106
+ }
107
+ }
108
+ // custom_env has no flag on `agent update` at all — `agent env set` is the
109
+ // only way to set it on an existing agent, so it's always a follow-up call.
110
+ const hasCustomEnv = rec.custom_env && Object.keys(rec.custom_env).length > 0;
111
+ if (hasCustomEnv) {
112
+ try {
113
+ cli.run(["agent", "env", "set", id, "--custom-env-stdin"], { input: JSON.stringify(rec.custom_env) });
114
+ } catch {
115
+ secretsApplyFailures.push(rec.name);
116
+ }
117
+ }
90
118
  }
91
- return { idMap, created, updated };
119
+ return { idMap, sourceIdMap, created, updated, secretsApplyFailures };
120
+ }
121
+
122
+ // Rewrites `mention://agent/<id>` links (e.g. `[@dev-backend](mention://agent/<id>)`)
123
+ // from a source-workspace agent id to its id in the destination workspace.
124
+ // Mentions with no entry in idMap (agent outside this bundle) are left as-is.
125
+ const MENTION_RE = /mention:\/\/agent\/([^)\s]+)/g;
126
+
127
+ export function rewriteMentions(text, idMap) {
128
+ if (!text) return text;
129
+ return text.replace(MENTION_RE, (full, oldId) => {
130
+ const newId = idMap.get(oldId);
131
+ return newId ? `mention://agent/${newId}` : full;
132
+ });
133
+ }
134
+
135
+ // Second pass over agent instructions: an agent's instructions may @mention a
136
+ // sibling agent from the same bundle by its SOURCE id, which is only knowable
137
+ // once every agent in the bundle has been created/updated (idMap complete) —
138
+ // so this must run after the importAgents loop above, not inside it.
139
+ export function rewriteAgentMentions({ cli, manifest, dir, agentIdMap, sourceIdMap, fs = nodeFs }) {
140
+ let updated = 0;
141
+ for (const a of manifest.agents) {
142
+ 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;
146
+ cli.run(["agent", "update", agentIdMap.get(rec.name), "--instructions", rewritten]);
147
+ updated++;
148
+ }
149
+ return { updated };
92
150
  }
93
151
 
94
- export function importSquad({ cli, squad, agentIdMap }) {
152
+ export function importSquad({ cli, squad, agentIdMap, sourceIdMap }) {
95
153
  const existing = listSquads(cli);
96
- const leaderId = agentIdMap.get(squad.leaderName);
154
+ const leaderId = agentIdMap.get(squad.leader_name);
97
155
  const match = findByName(existing, squad.name);
98
156
  let id, created = 0, updated = 0;
99
- const instr = squad.instructions ? ["--instructions", squad.instructions] : [];
157
+ // Squad instructions commonly list @mentions of teammate agents by their
158
+ // SOURCE id — rewrite to the destination ids before the squad is created.
159
+ const instructions = sourceIdMap ? rewriteMentions(squad.instructions, sourceIdMap) : squad.instructions;
160
+ const instr = instructions ? ["--instructions", instructions] : [];
100
161
  if (match) {
101
162
  cli.run(["squad", "update", match.id, "--leader", leaderId, "--description", squad.description ?? "", ...instr]);
102
163
  id = match.id; updated++;
@@ -105,10 +166,10 @@ export function importSquad({ cli, squad, agentIdMap }) {
105
166
  id = JSON.parse(out).id; created++;
106
167
  }
107
168
  // Add non-leader members, skipping any already present so re-runs are idempotent.
108
- const present = new Set(getSquadMembers(cli, id).map((m) => m.memberId));
169
+ const present = new Set(getSquadMembers(cli, id).map((m) => m.member_id));
109
170
  for (const m of squad.members) {
110
- if (m.agentName === squad.leaderName) continue;
111
- const memberId = agentIdMap.get(m.agentName);
171
+ if (m.agent_name === squad.leader_name) continue;
172
+ const memberId = agentIdMap.get(m.agent_name);
112
173
  if (present.has(memberId)) continue;
113
174
  cli.run(["squad", "member", "add", id, "--member-id", memberId, "--role", m.role, "--type", "agent"]);
114
175
  }
@@ -116,15 +177,15 @@ export function importSquad({ cli, squad, agentIdMap }) {
116
177
  }
117
178
 
118
179
  export function collectSourceRuntimes(manifest) {
119
- return [...new Set((manifest.agents ?? []).map((a) => a.sourceRuntimeId).filter(Boolean))];
180
+ return [...new Set((manifest.agents ?? []).map((a) => a.source_runtime_id).filter(Boolean))];
120
181
  }
121
182
 
122
- // sourceRuntimeId -> provider (e.g. "claude", "opencode"), from whichever agent recorded it.
183
+ // source_runtime_id -> provider (e.g. "claude", "opencode"), from whichever agent recorded it.
123
184
  function collectRuntimeProviders(manifest) {
124
185
  const map = new Map();
125
186
  for (const a of manifest.agents ?? []) {
126
- if (a.sourceRuntimeId && a.sourceRuntimeProvider && !map.has(a.sourceRuntimeId)) {
127
- map.set(a.sourceRuntimeId, a.sourceRuntimeProvider);
187
+ if (a.source_runtime_id && a.source_runtime_provider && !map.has(a.source_runtime_id)) {
188
+ map.set(a.source_runtime_id, a.source_runtime_provider);
128
189
  }
129
190
  }
130
191
  return map;
@@ -163,16 +224,20 @@ export function importBundle({ cli, dir, runtimeMap, fs = nodeFs }) {
163
224
 
164
225
  const skillRes = importSkills({ cli, manifest, dir, fs });
165
226
  const agentRes = importAgents({ cli, manifest, dir, skillIdMap: skillRes.idMap, runtimeMap: effective, fs });
227
+ // Runs after every agent exists so forward-referencing mentions resolve.
228
+ const mentionRes = rewriteAgentMentions({ cli, manifest, dir, agentIdMap: agentRes.idMap, sourceIdMap: agentRes.sourceIdMap, fs });
166
229
  let squadRes = { newId: null, created: 0, updated: 0 };
167
- if (manifest.squads?.length) squadRes = importSquad({ cli, squad: manifest.squads[0], agentIdMap: agentRes.idMap });
230
+ if (manifest.squads?.length) squadRes = importSquad({ cli, squad: manifest.squads[0], agentIdMap: agentRes.idMap, sourceIdMap: agentRes.sourceIdMap });
168
231
 
169
232
  return {
170
233
  created: { skills: skillRes.created, agents: agentRes.created, squads: squadRes.created },
171
234
  updated: { skills: skillRes.updated, agents: agentRes.updated, squads: squadRes.updated },
235
+ mentionsRewritten: mentionRes.updated,
172
236
  skillIdMap: Object.fromEntries(skillRes.idMap),
173
237
  agentIdMap: Object.fromEntries(agentRes.idMap),
174
238
  squadId: squadRes.newId,
175
- secretsReminder: (manifest.agents ?? []).filter((a) => a.hadSecrets).map((a) => a.name),
239
+ secretsReminder: (manifest.agents ?? []).filter((a) => a.had_secrets).map((a) => a.name),
240
+ secretsApplyFailures: agentRes.secretsApplyFailures,
176
241
  };
177
242
  }
178
243
 
@@ -58,4 +58,4 @@ Parse the JSON output from the script and report:
58
58
 
59
59
  - Directory written to.
60
60
  - Count of skills, agents, and squads exported.
61
- - If `warnings` is non-empty, surface every agent name verbatim with this message: "WARNING: the following agents had custom environment variables or MCP config that were NOT exported re-add secrets manually after import: `<agent-name>`."
61
+ - If `warnings` is non-empty, surface every agent name verbatim with this message: "WARNING: the following agents' exported files contain custom environment variables or MCP config in PLAINTEXT treat the export directory as sensitive (avoid committing it to a public repo, restrict file permissions, delete it once the import is done): `<agent-name>`."
@@ -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" \
@@ -38,6 +38,8 @@ node "${CLAUDE_PLUGIN_ROOT}/scripts/multica-import.mjs" \
38
38
  --runtime-map <srcId1=dstId1,srcId2=dstId2,...>
39
39
  ```
40
40
 
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
+
41
43
  ## Step 3 — Report results
42
44
 
43
45
  Parse the JSON output and report:
@@ -45,4 +47,6 @@ Parse the JSON output and report:
45
47
  - Created and updated counts for skills, agents, and squads.
46
48
  - Name-to-ID maps for skills and agents (`skillIdMap`, `agentIdMap`).
47
49
  - Squad ID if a squad was imported.
48
- - If `secretsReminder` is non-empty, surface every agent name verbatim with: "WARNING: the following agents had custom environment variables or MCP config that were NOT exported — re-add secrets manually in the Multica UI: `<agent-name>`."
50
+ - `mentionsRewritten`: how many agents had an agent-mention link rewritten to its new id.
51
+ - 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
+ - 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>`."
@@ -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" \
@@ -50,4 +50,5 @@ Parse the JSON output and report:
50
50
  - Created and updated counts for skills, agents, and squads.
51
51
  - Name-to-ID maps (`skillIdMap`, `agentIdMap`).
52
52
  - Squad ID if a squad was synced.
53
- - If `secretsReminder` is non-empty, surface every agent name verbatim with: "WARNING: the following agents had custom environment variables or MCP config that were NOT exported re-add secrets manually in the Multica UI: `<agent-name>`."
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
+ - 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>`."
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "plugin-validator",
3
3
  "displayName": "Plugin Validator",
4
- "version": "0.0.36",
4
+ "version": "0.0.38",
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.36",
4
+ "version": "0.0.38",
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.36",
3
+ "version": "0.0.38",
4
4
  "description": "Step-by-step wizard for generating technical diagrams as SVG+PNG.",
5
5
  "author": {
6
6
  "name": "steven"