@drunkcoding/agents-and-skills 0.0.39 → 0.0.41

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.39",
15
+ "version": "0.0.41",
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.39",
29
+ "version": "0.0.41",
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.39",
44
+ "version": "0.0.41",
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.39",
60
+ "version": "0.0.41",
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.39",
76
+ "version": "0.0.41",
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.39",
3
+ "version": "0.0.41",
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.39",
4
+ "version": "0.0.41",
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.39",
3
+ "version": "0.0.41",
4
4
  "description": "Export, import, and sync Multica skills, agents, and squads between workspaces via the multica CLI."
5
5
  }
@@ -104,3 +104,26 @@ export const getSquadMembers = (cli, id) =>
104
104
  (cli.json(["squad", "member", "list", id]) ?? []).map((m) => ({
105
105
  member_id: m.member_id, member_type: m.member_type, role: m.role || "member",
106
106
  }));
107
+
108
+ export const listProjects = (cli) => cli.json(["project", "list"]);
109
+
110
+ export function findByTitle(list, title) {
111
+ const hits = (list || []).filter((x) => x.title === title);
112
+ if (hits.length > 1) throw new Error(`Duplicate title "${title}" — refusing to guess`);
113
+ return hits[0] || null;
114
+ }
115
+
116
+ export function getProject(cli, id) {
117
+ const p = cli.json(["project", "get", id]);
118
+ return {
119
+ id: p.id, title: p.title, description: p.description ?? "",
120
+ icon: p.icon ?? null, priority: p.priority ?? "none", status: p.status ?? null,
121
+ due_date: p.due_date ?? null, start_date: p.start_date ?? null,
122
+ lead_id: p.lead_id ?? null, lead_type: p.lead_type ?? null,
123
+ };
124
+ }
125
+
126
+ export const getProjectResources = (cli, id) =>
127
+ (cli.json(["project", "resource", "list", id]) ?? []).map((r) => ({
128
+ resource_type: r.resource_type, resource_ref: r.resource_ref, label: r.label ?? null,
129
+ }));
@@ -1,7 +1,7 @@
1
1
  import * as nodeFs from "node:fs";
2
2
  import { spawnSync } from "node:child_process";
3
3
  import { dirname } from "node:path";
4
- import { slugify, getSkill, getAgent, getAgentCustomEnv, getSquad, getSquadMembers, listRuntimes, listSkills, listAgents, listSquads, makeCli, realExec, requireAuth, resolveWorkspaceId } from "./lib.mjs";
4
+ import { slugify, getSkill, getAgent, getAgentCustomEnv, getSquad, getSquadMembers, listRuntimes, listSkills, listAgents, listSquads, listProjects, getProject, getProjectResources, makeCli, realExec, requireAuth, resolveWorkspaceId } from "./lib.mjs";
5
5
 
6
6
  const nonEmpty = (v) => v && typeof v === "object" && Object.keys(v).length > 0;
7
7
 
@@ -31,7 +31,7 @@ export function redactAgent(a) {
31
31
  // a is a normalized agent from getAgent, with `custom_env`/
32
32
  // `custom_env_fetch_failed` attached by the caller (collectAgent) — getAgent
33
33
  // itself never fetches custom_env, since it requires a separate audited call.
34
- 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;
35
35
  const mcpUsable = !mcp_config_redacted && nonEmpty(mcp_config);
36
36
  const envUsable = !custom_env_fetch_failed && nonEmpty(custom_env);
37
37
  // mcp_config_redacted / custom_env_fetch_failed alone still flag hadSecrets even
@@ -39,10 +39,10 @@ export function redactAgent(a) {
39
39
  // but couldn't be captured, not just silently see an empty bundle.
40
40
  const hadSecrets = mcpUsable || envUsable || !!mcp_config_redacted || !!custom_env_fetch_failed;
41
41
  return {
42
- // source_id lets import-time mention rewriting map stale `mention://agent/<id>`
43
- // links (in this or another agent's/squad's instructions) to the new id.
44
42
  record: {
45
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.
46
46
  source_id: id,
47
47
  source_runtime_id: runtime_id,
48
48
  skill_names: [],
@@ -51,21 +51,36 @@ export function redactAgent(a) {
51
51
  had_secrets: hadSecrets,
52
52
  },
53
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 ?? "",
54
57
  };
55
58
  }
56
59
 
57
- export function buildManifest({ scope, sourceWorkspaceId, skills, agents, squads }) {
60
+ export function buildManifest({ scope, sourceWorkspaceId, skills, agents, squads, projects }) {
58
61
  const seenSkills = new Map();
59
62
  for (const s of skills) if (!seenSkills.has(s.name)) seenSkills.set(s.name, s);
60
63
  const seenAgents = new Map();
61
64
  for (const a of agents) if (!seenAgents.has(a.name)) seenAgents.set(a.name, a);
65
+ const seenProjects = new Map();
66
+ for (const p of projects ?? []) if (!seenProjects.has(p.title)) seenProjects.set(p.title, p);
62
67
  return {
63
68
  version: "1",
64
69
  scope,
65
70
  source_workspace_id: sourceWorkspaceId,
66
71
  skills: [...seenSkills.values()].map((s) => ({ name: s.name, dir: `skills/${slugify(s.name)}`, source_id: s.source_id })),
67
72
  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 })),
73
+ squads: (squads ?? []).map((squad) => {
74
+ const file = `squads/${slugify(squad.name)}.json`;
75
+ const entry = { name: squad.name, file, description: squad.description ?? "", avatar_url: squad.avatar_url ?? null, leader_name: squad.leader_name, members: squad.members };
76
+ // Instructions go to a sibling .md (see squad write loop); only referenced when non-empty.
77
+ if (squad.instructions) entry.instructions_file = file.replace(/\.json$/, ".md");
78
+ return entry;
79
+ }),
80
+ projects: [...seenProjects.values()].map((p) => ({
81
+ title: p.title, file: `projects/${slugify(p.title)}.json`,
82
+ source_id: p.source_id, lead_name: p.lead_name ?? null, lead_type: p.lead_type ?? null,
83
+ })),
69
84
  };
70
85
  }
71
86
 
@@ -97,10 +112,27 @@ function collectAgent(cli, id, agentsById, skills, providerById) {
97
112
  return entry;
98
113
  }
99
114
 
115
+ // Collect a project's portable metadata + its lead agent (bundled, like a squad
116
+ // leader) and github_repo/other resources. Returns the project bundle record.
117
+ function collectProject(cli, id, agentsById, skills, providerById) {
118
+ const p = getProject(cli, id);
119
+ let lead_name = null;
120
+ if (p.lead_type === "agent" && p.lead_id) {
121
+ lead_name = collectAgent(cli, p.lead_id, agentsById, skills, providerById).raw.name;
122
+ }
123
+ return {
124
+ title: p.title, description: p.description, icon: p.icon,
125
+ priority: p.priority, status: p.status, due_date: p.due_date, start_date: p.start_date,
126
+ source_id: id, lead_type: p.lead_type, lead_name, lead_source_id: p.lead_id,
127
+ resources: getProjectResources(cli, id),
128
+ };
129
+ }
130
+
100
131
  export function exportResource({ cli, scope, ids, outDir, sourceWorkspaceId, fs = nodeFs, download = fetchBinary }) {
101
132
  const skills = new Map(); // name -> normalized skill
102
133
  const agentsById = new Map(); // id -> { raw, red, skill_names }
103
134
  const squads = [];
135
+ const projects = [];
104
136
  // Lazy + memoized: only fetched when an agent is actually collected (skips
105
137
  // the extra CLI call on skill-only exports).
106
138
  let providerById = null;
@@ -123,10 +155,26 @@ export function exportResource({ cli, scope, ids, outDir, sourceWorkspaceId, fs
123
155
  if (scope === "skill") collectSkill(cli, ids.skillId, skills);
124
156
  else if (scope === "agent") collectAgent(cli, ids.agentId, agentsById, skills, getProviderById());
125
157
  else if (scope === "squad") squads.push(collectOneSquad(ids.squadId));
158
+ else if (scope === "project") projects.push(collectProject(cli, ids.projectId, agentsById, skills, getProviderById()));
159
+ else if (scope === "projects") for (const p of listProjects(cli)) projects.push(collectProject(cli, p.id, agentsById, skills, getProviderById()));
126
160
  else if (scope === "all") {
127
161
  for (const s of listSkills(cli)) collectSkill(cli, s.id, skills);
128
162
  for (const a of listAgents(cli)) collectAgent(cli, a.id, agentsById, skills, getProviderById());
129
163
  for (const sq of listSquads(cli)) squads.push(collectOneSquad(sq.id));
164
+ for (const p of listProjects(cli)) projects.push(collectProject(cli, p.id, agentsById, skills, getProviderById()));
165
+ }
166
+
167
+ // Orphan-skill cleanup: drop skills that no exported agent references via its
168
+ // skill_names. Only `all` ever produces these — standalone workspace skills
169
+ // from listSkills that no agent uses. Skipped for `skill` scope: its one
170
+ // skill is the explicit target, not an orphan.
171
+ const pruned_skills = [];
172
+ if (scope !== "skill") {
173
+ const referenced = new Set();
174
+ for (const a of agentsById.values()) for (const n of a.skill_names) referenced.add(n);
175
+ for (const name of [...skills.keys()]) {
176
+ if (!referenced.has(name)) { pruned_skills.push(name); skills.delete(name); }
177
+ }
130
178
  }
131
179
 
132
180
  const manifest = buildManifest({
@@ -134,6 +182,7 @@ export function exportResource({ cli, scope, ids, outDir, sourceWorkspaceId, fs
134
182
  skills: [...skills.values()].map((s) => ({ name: s.name, source_id: s.id })),
135
183
  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
184
  squads,
185
+ projects,
137
186
  });
138
187
 
139
188
  const warnings = [];
@@ -170,14 +219,30 @@ export function exportResource({ cli, scope, ids, outDir, sourceWorkspaceId, fs
170
219
  record.avatar_file = rel;
171
220
  }
172
221
  }
222
+ // Instructions live in a sibling .md for reviewability (same sibling-file
223
+ // pattern as avatar_file); only written when non-empty.
224
+ if (red.instructions) {
225
+ const rel = entry.file.replace(/\.json$/, ".md");
226
+ fs.writeFileSync(`${outDir}/${rel}`, red.instructions);
227
+ record.instructions_file = rel;
228
+ }
173
229
  fs.writeFileSync(`${outDir}/${entry.file}`, JSON.stringify(record, null, 2));
174
230
  }
231
+ const squadInstrByName = new Map(squads.map((s) => [s.name, s.instructions ?? ""]));
175
232
  for (const entry of manifest.squads) {
176
233
  fs.mkdirSync(`${outDir}/squads`, { recursive: true });
234
+ if (entry.instructions_file) {
235
+ fs.writeFileSync(`${outDir}/${entry.instructions_file}`, squadInstrByName.get(entry.name) ?? "");
236
+ }
177
237
  fs.writeFileSync(`${outDir}/${entry.file}`, JSON.stringify(entry, null, 2));
178
238
  }
239
+ const projectByTitle = new Map(projects.map((p) => [p.title, p]));
240
+ for (const entry of manifest.projects) {
241
+ fs.mkdirSync(`${outDir}/projects`, { recursive: true });
242
+ fs.writeFileSync(`${outDir}/${entry.file}`, JSON.stringify(projectByTitle.get(entry.title), null, 2));
243
+ }
179
244
  fs.writeFileSync(`${outDir}/manifest.json`, JSON.stringify(manifest, null, 2));
180
- return { manifest, warnings };
245
+ return { manifest, warnings, pruned_skills };
181
246
  }
182
247
 
183
248
  function main() {
@@ -189,8 +254,8 @@ function main() {
189
254
  const out = get("--out");
190
255
  const workspace = get("--workspace"); // optional: source workspace name
191
256
 
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)");
257
+ if (!scope || !out || (scope !== "all" && scope !== "projects" && !id)) {
258
+ console.error("Usage: multica-export.mjs --scope <skill|agent|squad|project|projects|all> --id <id> --out <dir> [--workspace <name>] (--id not needed for --scope all|projects)");
194
259
  process.exit(1);
195
260
  }
196
261
 
@@ -205,8 +270,10 @@ function main() {
205
270
  if (scope === "skill") ids.skillId = id;
206
271
  else if (scope === "agent") ids.agentId = id;
207
272
  else if (scope === "squad") ids.squadId = id;
273
+ else if (scope === "project") ids.projectId = id;
274
+ else if (scope === "projects") { /* all projects — no id */ }
208
275
  else if (scope === "all") { /* whole workspace — no id */ }
209
- else { console.error(`Unknown scope "${scope}" — use skill|agent|squad|all`); process.exit(1); }
276
+ else { console.error(`Unknown scope "${scope}" — use skill|agent|squad|project|projects|all`); process.exit(1); }
210
277
 
211
278
  const result = exportResource({ cli, scope, ids, outDir: out, sourceWorkspaceId, fs: nodeFs });
212
279
  console.log(JSON.stringify(result, null, 2));
@@ -1,5 +1,22 @@
1
1
  import * as nodeFs from "node:fs";
2
- import { listSkills, listAgents, listSquads, listRuntimes, listWorkspaceMembers, getSquadMembers, findByName, makeCli, realExec, requireAuth, resolveWorkspaceId } from "./lib.mjs";
2
+ import { listSkills, listAgents, listSquads, listRuntimes, listWorkspaceMembers, getSquadMembers, findByName, makeCli, realExec, requireAuth, resolveWorkspaceId, listProjects, getProjectResources, findByTitle } from "./lib.mjs";
3
+
4
+ // User-facing selectable types are agents/squads/projects; skills follow agents.
5
+ export function parseInclude(raw) {
6
+ const set = new Set((raw ? raw.split(",") : ["agents", "squads"]).map((s) => s.trim()).filter(Boolean));
7
+ if (set.has("agents")) set.add("skills");
8
+ return set;
9
+ }
10
+
11
+ // Instructions live in a sibling .md referenced by `instructions_file` (mirrors
12
+ // avatar_file). Legacy bundles carry no instructions_file and keep instructions
13
+ // inline in the JSON — fall back to that so older exports still import.
14
+ function readInstructions(fs, dir, rec) {
15
+ if (rec.instructions_file && fs.existsSync(`${dir}/${rec.instructions_file}`)) {
16
+ return fs.readFileSync(`${dir}/${rec.instructions_file}`, "utf8");
17
+ }
18
+ return rec.instructions ?? "";
19
+ }
3
20
 
4
21
  // Relative paths of every file under root (recursing into subdirs like scripts/).
5
22
  function walkSkillFiles(fs, root, rel = "") {
@@ -82,7 +99,8 @@ export function importAgents({ cli, manifest, dir, skillIdMap, runtimeMap, fs =
82
99
  "--max-concurrent-tasks", String(rec.max_concurrent_tasks ?? 6),
83
100
  ];
84
101
  if (rec.description) common.push("--description", rec.description);
85
- if (rec.instructions) common.push("--instructions", rec.instructions);
102
+ const instructions = readInstructions(fs, dir, rec);
103
+ if (instructions) common.push("--instructions", instructions);
86
104
  if (rec.model) common.push("--model", rec.model);
87
105
  if (rec.thinking_level) common.push("--thinking-level", rec.thinking_level);
88
106
  if (rec.runtime_config && Object.keys(rec.runtime_config).length) common.push("--runtime-config", JSON.stringify(rec.runtime_config));
@@ -184,9 +202,10 @@ export function rewriteAgentMentions({ cli, manifest, dir, agentIdMap, sourceIdM
184
202
  let updated = 0;
185
203
  for (const a of manifest.agents) {
186
204
  const rec = JSON.parse(fs.readFileSync(`${dir}/${a.file}`, "utf8"));
187
- if (!rec.instructions) continue;
188
- const rewritten = rewriteMentions(rec.instructions, sourceIdMap);
189
- if (rewritten === rec.instructions) continue;
205
+ const instructions = readInstructions(fs, dir, rec);
206
+ if (!instructions) continue;
207
+ const rewritten = rewriteMentions(instructions, sourceIdMap);
208
+ if (rewritten === instructions) continue;
190
209
  cli.run(["agent", "update", agentIdMap.get(rec.name), "--instructions", rewritten]);
191
210
  updated++;
192
211
  }
@@ -196,6 +215,7 @@ export function rewriteAgentMentions({ cli, manifest, dir, agentIdMap, sourceIdM
196
215
  export function importSquad({ cli, squad, agentIdMap, sourceIdMap }) {
197
216
  const existing = listSquads(cli);
198
217
  const leaderId = agentIdMap.get(squad.leader_name);
218
+ if (!leaderId) return { skipped: true, created: 0, updated: 0 };
199
219
  const match = findByName(existing, squad.name);
200
220
  let id, created = 0, updated = 0;
201
221
  // Squad instructions commonly list @mentions of teammate agents by their
@@ -221,12 +241,63 @@ export function importSquad({ cli, squad, agentIdMap, sourceIdMap }) {
221
241
  for (const m of squad.members) {
222
242
  if (m.agent_name === squad.leader_name) continue;
223
243
  const memberId = agentIdMap.get(m.agent_name);
224
- if (present.has(memberId)) continue;
244
+ if (!memberId || present.has(memberId)) continue;
225
245
  cli.run(["squad", "member", "add", id, "--member-id", memberId, "--role", m.role, "--type", "agent"]);
226
246
  }
227
247
  return { newId: id, created, updated };
228
248
  }
229
249
 
250
+ export function importProjects({ cli, manifest, dir, agentIdMap, fs = nodeFs }) {
251
+ const idMap = new Map();
252
+ let created = 0, updated = 0;
253
+ const priorityUnsupported = [], resourcesUnsupported = [], leadUnresolved = [];
254
+ const existing = listProjects(cli);
255
+ // Lead resolves against just-imported agents first, then destination agents.
256
+ let destAgentNames = null;
257
+ const leadResolvable = (name) =>
258
+ agentIdMap.has(name) || (destAgentNames ??= new Set(listAgents(cli).map((a) => a.name))).has(name);
259
+
260
+ for (const entry of manifest.projects ?? []) {
261
+ const rec = JSON.parse(fs.readFileSync(`${dir}/${entry.file}`, "utf8"));
262
+ const flags = ["--title", rec.title];
263
+ if (rec.description) flags.push("--description", rec.description);
264
+ if (rec.icon) flags.push("--icon", rec.icon);
265
+ if (rec.status) flags.push("--status", rec.status);
266
+ if (rec.due_date) flags.push("--due-date", rec.due_date);
267
+ if (rec.start_date) flags.push("--start-date", rec.start_date);
268
+ const wantsLead = rec.lead_type === "agent" && !!rec.lead_name;
269
+ const leadOk = wantsLead && leadResolvable(rec.lead_name);
270
+ if (leadOk) flags.push("--lead", rec.lead_name);
271
+
272
+ const match = findByTitle(existing, rec.title);
273
+ let id;
274
+ if (match) {
275
+ cli.run(["project", "update", match.id, ...flags]);
276
+ id = match.id; updated++;
277
+ } else {
278
+ id = JSON.parse(cli.run(["project", "create", ...flags])).id;
279
+ created++;
280
+ }
281
+ idMap.set(rec.title, id);
282
+
283
+ if (wantsLead && !leadOk) leadUnresolved.push(rec.title);
284
+ if (rec.priority && rec.priority !== "none") priorityUnsupported.push(rec.title);
285
+
286
+ // Resources: recreate github_repo only, idempotent by url.
287
+ const existingUrls = new Set(
288
+ getProjectResources(cli, id).filter((r) => r.resource_type === "github_repo").map((r) => r.resource_ref?.url).filter(Boolean),
289
+ );
290
+ for (const r of rec.resources ?? []) {
291
+ if (r.resource_type !== "github_repo") { resourcesUnsupported.push(`${rec.title}:${r.resource_type}`); continue; }
292
+ const url = r.resource_ref?.url;
293
+ if (!url || existingUrls.has(url)) continue;
294
+ cli.run(["project", "resource", "add", id, "--type", "github_repo", "--url", url, ...(r.label ? ["--label", r.label] : [])]);
295
+ existingUrls.add(url);
296
+ }
297
+ }
298
+ return { idMap, created, updated, priorityUnsupported, resourcesUnsupported, leadUnresolved };
299
+ }
300
+
230
301
  export function collectSourceRuntimes(manifest) {
231
302
  return [...new Set((manifest.agents ?? []).map((a) => a.source_runtime_id).filter(Boolean))];
232
303
  }
@@ -263,45 +334,124 @@ export function resolveRuntimeMap({ cli, manifest, runtimeMap }) {
263
334
  return { effective, unresolved };
264
335
  }
265
336
 
266
- export function importBundle({ cli, dir, runtimeMap, fs = nodeFs }) {
337
+ export function importBundle({ cli, dir, runtimeMap, include, fs = nodeFs }) {
338
+ const inc = include ?? new Set(["skills", "agents", "squads"]);
267
339
  const manifest = JSON.parse(fs.readFileSync(`${dir}/manifest.json`, "utf8"));
268
- const { effective, unresolved } = resolveRuntimeMap({ cli, manifest, runtimeMap });
269
- if (unresolved.length) {
270
- const detail = unresolved.map(({ srcId, provider, matchCount }) => provider
271
- ? `${srcId} (provider "${provider}": ${matchCount} matching runtimes in destination, expected exactly 1)`
272
- : `${srcId} (no provider recorded)`).join(", ");
273
- throw new Error(`Unmapped runtimes: ${detail} pass --runtime-map, aborting before any write`);
340
+
341
+ let effective = new Map();
342
+ if (inc.has("agents")) {
343
+ const r = resolveRuntimeMap({ cli, manifest, runtimeMap });
344
+ if (r.unresolved.length) {
345
+ const detail = r.unresolved.map(({ srcId, provider, matchCount }) => provider
346
+ ? `${srcId} (provider "${provider}": ${matchCount} matching runtimes in destination, expected exactly 1)`
347
+ : `${srcId} (no provider recorded)`).join(", ");
348
+ throw new Error(`Unmapped runtimes: ${detail} — pass --runtime-map, aborting before any write`);
349
+ }
350
+ effective = r.effective;
274
351
  }
275
352
 
276
- const skillRes = importSkills({ cli, manifest, dir, fs });
277
- const agentRes = importAgents({ cli, manifest, dir, skillIdMap: skillRes.idMap, runtimeMap: effective, fs });
353
+ const skillRes = inc.has("skills")
354
+ ? importSkills({ cli, manifest, dir, fs })
355
+ : { idMap: new Map(), created: 0, updated: 0 };
356
+ const agentRes = inc.has("agents")
357
+ ? importAgents({ cli, manifest, dir, skillIdMap: skillRes.idMap, runtimeMap: effective, fs })
358
+ : { idMap: new Map(), sourceIdMap: new Map(), created: 0, updated: 0, secretsApplyFailures: [], avatarApplyFailures: [], avatarUnsupported: [], permissionApplyFailures: [], permissionUnsupported: [] };
278
359
  // Runs after every agent exists so forward-referencing mentions resolve.
279
- const mentionRes = rewriteAgentMentions({ cli, manifest, dir, agentIdMap: agentRes.idMap, sourceIdMap: agentRes.sourceIdMap, fs });
360
+ const mentionRes = inc.has("agents")
361
+ ? rewriteAgentMentions({ cli, manifest, dir, agentIdMap: agentRes.idMap, sourceIdMap: agentRes.sourceIdMap, fs })
362
+ : { updated: 0 };
363
+
280
364
  const squadIdMap = new Map();
281
365
  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;
366
+ const squadsSkipped = [];
367
+ if (inc.has("squads")) {
368
+ for (const squad of manifest.squads ?? []) {
369
+ squad.instructions = readInstructions(fs, dir, squad);
370
+ const r = importSquad({ cli, squad, agentIdMap: agentRes.idMap, sourceIdMap: agentRes.sourceIdMap });
371
+ if (r.skipped) { squadsSkipped.push(squad.name); continue; }
372
+ squadIdMap.set(squad.name, r.newId);
373
+ squadsCreated += r.created;
374
+ squadsUpdated += r.updated;
375
+ }
376
+ }
377
+
378
+ let projectRes = { idMap: new Map(), created: 0, updated: 0, priorityUnsupported: [], resourcesUnsupported: [], leadUnresolved: [] };
379
+ if (inc.has("projects")) {
380
+ projectRes = importProjects({ cli, manifest, dir, agentIdMap: agentRes.idMap, fs });
287
381
  }
288
382
 
289
383
  return {
290
- created: { skills: skillRes.created, agents: agentRes.created, squads: squadsCreated },
291
- updated: { skills: skillRes.updated, agents: agentRes.updated, squads: squadsUpdated },
384
+ include: [...inc],
385
+ created: { skills: skillRes.created, agents: agentRes.created, squads: squadsCreated, projects: projectRes.created },
386
+ updated: { skills: skillRes.updated, agents: agentRes.updated, squads: squadsUpdated, projects: projectRes.updated },
292
387
  mentionsRewritten: mentionRes.updated,
293
388
  skillIdMap: Object.fromEntries(skillRes.idMap),
294
389
  agentIdMap: Object.fromEntries(agentRes.idMap),
295
390
  squadIdMap: Object.fromEntries(squadIdMap),
391
+ projectIdMap: Object.fromEntries(projectRes.idMap),
296
392
  secretsReminder: (manifest.agents ?? []).filter((a) => a.had_secrets).map((a) => a.name),
297
393
  secretsApplyFailures: agentRes.secretsApplyFailures,
298
394
  avatarApplyFailures: agentRes.avatarApplyFailures,
299
395
  avatarUnsupported: agentRes.avatarUnsupported,
300
396
  permissionApplyFailures: agentRes.permissionApplyFailures,
301
397
  permissionUnsupported: agentRes.permissionUnsupported,
398
+ squadsSkipped,
399
+ priorityUnsupported: projectRes.priorityUnsupported,
400
+ resourcesUnsupported: projectRes.resourcesUnsupported,
401
+ leadUnresolved: projectRes.leadUnresolved,
302
402
  };
303
403
  }
304
404
 
405
+ export function preflight({ cli, dir, runtimeMap, include, fs = nodeFs }) {
406
+ const inc = include ?? new Set(["skills", "agents", "squads"]);
407
+ const manifest = JSON.parse(fs.readFileSync(`${dir}/manifest.json`, "utf8"));
408
+ const count = (k) => (manifest[k] ?? []).length;
409
+ const bundle = { skills: count("skills"), agents: count("agents"), squads: count("squads"), projects: count("projects") };
410
+ const willImport = {
411
+ skills: inc.has("skills") ? bundle.skills : 0,
412
+ agents: inc.has("agents") ? bundle.agents : 0,
413
+ squads: inc.has("squads") ? bundle.squads : 0,
414
+ projects: inc.has("projects") ? bundle.projects : 0,
415
+ };
416
+
417
+ const incompatibilities = [];
418
+ let runtimes = { resolved: [], unresolved: [] };
419
+ if (inc.has("agents")) {
420
+ const { effective, unresolved } = resolveRuntimeMap({ cli, manifest, runtimeMap });
421
+ runtimes = {
422
+ resolved: [...effective.entries()].map(([s, d]) => `${s}=${d}`),
423
+ unresolved: unresolved.map((u) => u.srcId),
424
+ };
425
+ for (const u of unresolved) {
426
+ incompatibilities.push({ type: "unmapped-runtime", detail: u.provider
427
+ ? `${u.srcId} (provider "${u.provider}": ${u.matchCount} matches, expected 1)`
428
+ : `${u.srcId} (no provider recorded)` });
429
+ }
430
+ }
431
+
432
+ if (inc.has("projects")) {
433
+ const bundleAgentNames = new Set((manifest.agents ?? []).map((a) => a.name));
434
+ for (const entry of manifest.projects ?? []) {
435
+ const rec = JSON.parse(fs.readFileSync(`${dir}/${entry.file}`, "utf8"));
436
+ if (rec.priority && rec.priority !== "none") {
437
+ incompatibilities.push({ type: "priority-not-settable", detail: `${rec.title} (priority "${rec.priority}")` });
438
+ }
439
+ for (const r of rec.resources ?? []) {
440
+ if (r.resource_type !== "github_repo") {
441
+ incompatibilities.push({ type: "resource-not-portable", detail: `${rec.title} (${r.resource_type})` });
442
+ }
443
+ }
444
+ const leadAvailable = inc.has("agents") && bundleAgentNames.has(rec.lead_name);
445
+ if (rec.lead_type === "agent" && rec.lead_name && !leadAvailable) {
446
+ incompatibilities.push({ type: "lead-agent-missing", detail: `${rec.title} → ${rec.lead_name} (ensure this agent exists in the destination)` });
447
+ }
448
+ }
449
+ }
450
+
451
+ const secretsReminder = (manifest.agents ?? []).filter((a) => a.had_secrets).map((a) => a.name);
452
+ return { bundle, willImport, runtimes, incompatibilities, secretsReminder };
453
+ }
454
+
305
455
  function parseRuntimeMap(raw) {
306
456
  // Parse "srcId1=dstId1,srcId2=dstId2" into a Map.
307
457
  const map = new Map();
@@ -321,9 +471,11 @@ function main() {
321
471
  const dir = get("--dir");
322
472
  const workspace = get("--workspace");
323
473
  const rawMap = get("--runtime-map");
474
+ const rawInclude = get("--include");
475
+ const dryRun = args.includes("--dry-run");
324
476
 
325
477
  if (!dir || !workspace) {
326
- console.error("Usage: multica-import.mjs --dir <folder> --workspace <name> [--runtime-map <src=dst,...>]");
478
+ console.error("Usage: multica-import.mjs --dir <folder> --workspace <name> [--runtime-map <src=dst,...>] [--include <csv>] [--dry-run]");
327
479
  process.exit(1);
328
480
  }
329
481
 
@@ -332,8 +484,13 @@ function main() {
332
484
  const wsId = resolveWorkspaceId(resolver, workspace);
333
485
  const cli = makeCli(realExec, { workspaceId: wsId });
334
486
  const runtimeMap = parseRuntimeMap(rawMap);
487
+ const include = parseInclude(rawInclude);
335
488
 
336
- const result = importBundle({ cli, dir, runtimeMap, fs: nodeFs });
489
+ if (dryRun) {
490
+ console.log(JSON.stringify(preflight({ cli, dir, runtimeMap, include, fs: nodeFs }), null, 2));
491
+ return;
492
+ }
493
+ const result = importBundle({ cli, dir, runtimeMap, include, fs: nodeFs });
337
494
  console.log(JSON.stringify(result, null, 2));
338
495
  }
339
496
 
@@ -1,13 +1,18 @@
1
1
  import * as nodeFs from "node:fs";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
- import { makeCli, resolveWorkspaceId, findByName, listSkills, listAgents, listSquads, realExec, requireAuth } from "./lib.mjs";
4
+ import { makeCli, resolveWorkspaceId, findByName, findByTitle, listSkills, listAgents, listSquads, listProjects, realExec, requireAuth } from "./lib.mjs";
5
5
  import { exportResource } from "./multica-export.mjs";
6
6
  import { importBundle } from "./multica-import.mjs";
7
7
 
8
8
  export function resolveScopeId(cli, type, name) {
9
+ if (type === "project") {
10
+ const match = findByTitle(listProjects(cli), name);
11
+ if (!match) throw new Error(`Unknown project "${name}" in source workspace`);
12
+ return { scope: "project", ids: { projectId: match.id } };
13
+ }
9
14
  const lists = { skill: listSkills, agent: listAgents, squad: listSquads };
10
- if (!lists[type]) throw new Error(`Unknown type "${type}" (skill|agent|squad)`);
15
+ if (!lists[type]) throw new Error(`Unknown type "${type}" (skill|agent|squad|project)`);
11
16
  const match = findByName(lists[type](cli), name);
12
17
  if (!match) throw new Error(`Unknown ${type} "${name}" in source workspace`);
13
18
  const key = { skill: "skillId", agent: "agentId", squad: "squadId" }[type];
@@ -23,7 +28,10 @@ export function sync({ exec, type, name, srcWsName, destWsName, tmpDir, runtimeM
23
28
 
24
29
  const { scope, ids } = resolveScopeId(srcCli, type, name);
25
30
  exportResource({ cli: srcCli, scope, ids, outDir: tmpDir, sourceWorkspaceId: srcId, fs });
26
- return importBundle({ cli: destCli, dir: tmpDir, runtimeMap, fs });
31
+ const includeByType = { skill: ["skills"], agent: ["agents"], squad: ["agents", "squads"], project: ["agents", "projects"] };
32
+ const include = new Set(includeByType[type] ?? ["agents", "squads"]);
33
+ if (include.has("agents")) include.add("skills");
34
+ return importBundle({ cli: destCli, dir: tmpDir, runtimeMap, include, fs });
27
35
  }
28
36
 
29
37
  function parseRuntimeMap(raw) {
@@ -10,17 +10,17 @@ Export a Multica resource (skill, agent, or squad) to a local bundle directory.
10
10
 
11
11
  ## Step 1 — Verify authentication
12
12
 
13
- Run the export script; it calls `multica auth status` internally and exits with an error message if unauthenticated:
13
+ Run `multica auth status` directly. The export script performs this same check internally before doing any work (via `requireAuth` in `scripts/lib.mjs`), but only after `--scope`/`--out` are supplied — so checking it here up front avoids masking an auth failure behind a later usage error:
14
14
 
15
15
  ```bash
16
- node "${CLAUDE_PLUGIN_ROOT}/scripts/multica-export.mjs" --help 2>&1 || true
16
+ multica auth status 2>&1 || true
17
17
  ```
18
18
 
19
19
  If `multica login` is required, surface that message verbatim and stop.
20
20
 
21
21
  ## Step 2 — Determine scope and resource ID
22
22
 
23
- If the user named a specific resource and type (`skill`, `agent`, or `squad`), use those directly.
23
+ If the user named a specific resource and type (`skill`, `agent`, `squad`, or `project`), use those directly.
24
24
 
25
25
  Otherwise, list available resources for the chosen type and present a pick list:
26
26
 
@@ -44,15 +44,19 @@ 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|all> \
47
+ --scope <skill|agent|squad|project|projects|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.
53
+ `--id` is required for `skill`, `agent`, `squad`, and `project` (a single named resource); it is **not needed** for `projects` (every project in the workspace) or `all` (the entire workspace).
54
54
 
55
- The script writes `manifest.json`, skill SKILL.md files, agent JSON files, and squad JSON files into `<dir>`.
55
+ Pass `--scope all` (with no `--id`) to export the **entire workspace** — every skill, agent, squad, and project into one flat, deduped bundle. A skill or agent shared across many agents/squads is written exactly once and referenced by name.
56
+
57
+ Exporting a project (or `projects`/`all`) also **bundles the project's lead agent** so the bundle is self-contained; projects carry metadata only (title, description, icon, priority, status, dates, lead mapping) plus their attached resource records — never issues. On import, only `github_repo` resources are portable and recreated; other resource types are reported and skipped.
58
+
59
+ 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
60
 
57
61
  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
62
 
@@ -61,5 +65,6 @@ Avatars are captured automatically: an agent's uploaded-image avatar is download
61
65
  Parse the JSON output from the script and report:
62
66
 
63
67
  - Directory written to.
64
- - Count of skills, agents, and squads exported.
68
+ - Count of skills, agents, squads, and projects exported.
69
+ - If `pruned_skills` is non-empty, note it: "Pruned N orphan skill(s) not linked to any agent: `<name>`, …" (these are standalone workspace skills that no exported agent references — only `--scope all` produces them).
65
70
  - 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>`."
@@ -12,14 +12,36 @@ Import a local Multica bundle (produced by the export skill) into a target works
12
12
 
13
13
  Ask the user to confirm the name of the target workspace if not already stated. You will need the exact workspace name as registered in Multica.
14
14
 
15
- ## Step 2 — Run the import (auto-mapping first)
15
+ ## Step 2 — Pre-flight (dry run)
16
16
 
17
- Each exported agent record carries its source runtime's `provider` (e.g. `claude`, `opencode`) alongside its ID. The import script auto-maps a source runtime to the target workspace's runtime when there is **exactly one** runtime of that provider there no manual mapping needed in the common case. Try the import without `--runtime-map` first:
17
+ Before writing anything, run the import script with `--dry-run` to preview the bundle. Preview against the **full** set (`--include agents,squads,projects`) regardless of what the user ultimately chooses to import incompatibilities for a type (e.g. project caveats) are only computed when that type is included, so previewing everything up front is what lets the user see a project's cost before deciding whether to opt it in:
18
18
 
19
19
  ```bash
20
20
  node "${CLAUDE_PLUGIN_ROOT}/scripts/multica-import.mjs" \
21
21
  --dir <folder> \
22
- --workspace <workspace-name>
22
+ --workspace <workspace-name> \
23
+ --include agents,squads,projects \
24
+ [--runtime-map <srcId1=dstId1,srcId2=dstId2,...>] \
25
+ --dry-run
26
+ ```
27
+
28
+ Present the `bundle` and `willImport` counts, and every entry in `incompatibilities`, to the user.
29
+
30
+ ## Step 3 — Select which types to import
31
+
32
+ Ask the user which of `agents`, `squads`, `projects` they want to import. **Default is `agents,squads`** — `projects` requires explicit opt-in.
33
+
34
+ If the pre-flight's `incompatibilities` list contains an `unmapped-runtime` entry, tell the user it must be resolved with `--runtime-map` before the real import — the import **aborts** otherwise (see Step 4 below). Other incompatibility kinds are informational only and applied best-effort, fixed up afterward in the Multica UI: `priority-not-settable` (priority isn't settable via the CLI, so it never round-trips), `resource-not-portable` (only `github_repo` resources are portable — other resource kinds are dropped), and `lead-agent-missing` (a non-agent lead isn't re-applied to the imported project).
35
+
36
+ ## Step 4 — Run the import (auto-mapping first)
37
+
38
+ Each exported agent record carries its source runtime's `provider` (e.g. `claude`, `opencode`) alongside its ID. The import script auto-maps a source runtime to the target workspace's runtime when there is **exactly one** runtime of that provider there — no manual mapping needed in the common case. Try the import without `--runtime-map` first, passing the selected types from Step 3 via `--include`:
39
+
40
+ ```bash
41
+ node "${CLAUDE_PLUGIN_ROOT}/scripts/multica-import.mjs" \
42
+ --dir <folder> \
43
+ --workspace <workspace-name> \
44
+ --include <agents,squads,projects>
23
45
  ```
24
46
 
25
47
  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:
@@ -35,21 +57,25 @@ Ask the user to pick a matching target runtime by name or ID for each unmapped `
35
57
  node "${CLAUDE_PLUGIN_ROOT}/scripts/multica-import.mjs" \
36
58
  --dir <folder> \
37
59
  --workspace <workspace-name> \
60
+ --include <agents,squads,projects> \
38
61
  --runtime-map <srcId1=dstId1,srcId2=dstId2,...>
39
62
  ```
40
63
 
41
64
  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
65
 
66
+ 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.
67
+
43
68
  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
69
 
45
- ## Step 3 — Report results
70
+ ## Step 5 — Report results
46
71
 
47
72
  Parse the JSON output and report:
48
73
 
49
- - Created and updated counts for skills, agents, and squads.
74
+ - Created and updated counts for skills, agents, squads, and projects (`created.projects`/`updated.projects`).
50
75
  - Name-to-ID maps for skills and agents (`skillIdMap`, `agentIdMap`).
51
76
  - `squadIdMap`: name-to-ID map for every squad imported.
52
77
  - `mentionsRewritten`: how many agents had an agent-mention link rewritten to its new id.
78
+ - If `leadUnresolved`, `priorityUnsupported`, `resourcesUnsupported`, or `squadsSkipped` is non-empty, surface each entry verbatim as an "applied best-effort; adjust in the UI" note — e.g. "NOTE: applied best-effort; adjust in the UI — `<entry>`."
53
79
  - 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>`."
54
80
  - 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
81
  - 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>`."
@@ -16,7 +16,9 @@ Expect the user's request in the form:
16
16
  sync <type> <name> from <src-ws> to <dest-ws>
17
17
  ```
18
18
 
19
- Where `<type>` is `skill`, `agent`, or `squad`; `<name>` is the resource name; `<src-ws>` and `<dest-ws>` are workspace names registered in Multica.
19
+ Where `<type>` is `skill`, `agent`, `squad`, or `project`; `<name>` is the resource name (for `project`, its **title**); `<src-ws>` and `<dest-ws>` are workspace names registered in Multica.
20
+
21
+ Projects are resolved by title, not ID, e.g. `multica-sync.mjs project "<title>" from <src-ws> <dest-ws>` — the project's lead agent is synced alongside it.
20
22
 
21
23
  ## Step 2 — Run the sync (auto-mapping first)
22
24
 
@@ -47,7 +49,7 @@ The script exports to a temporary directory, imports into the destination worksp
47
49
 
48
50
  Parse the JSON output and report:
49
51
 
50
- - Created and updated counts for skills, agents, and squads.
52
+ - Created and updated counts for skills, agents, squads, and projects.
51
53
  - Name-to-ID maps (`skillIdMap`, `agentIdMap`).
52
54
  - `squadIdMap`: name-to-ID map for every squad synced.
53
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 temporary export directory (already cleaned up) briefly held these secrets in plaintext: `<agent-name>`."
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "plugin-validator",
3
3
  "displayName": "Plugin Validator",
4
- "version": "0.0.39",
4
+ "version": "0.0.41",
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.39",
4
+ "version": "0.0.41",
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.39",
3
+ "version": "0.0.41",
4
4
  "description": "Step-by-step wizard for generating technical diagrams as SVG+PNG.",
5
5
  "author": {
6
6
  "name": "steven"