@drunkcoding/agents-and-skills 0.0.40 → 0.0.42

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.40",
15
+ "version": "0.0.42",
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.40",
29
+ "version": "0.0.42",
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.40",
44
+ "version": "0.0.42",
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.40",
60
+ "version": "0.0.42",
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.40",
76
+ "version": "0.0.42",
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.40",
3
+ "version": "0.0.42",
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.40",
4
+ "version": "0.0.42",
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.40",
3
+ "version": "0.0.42",
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
 
@@ -57,11 +57,13 @@ export function redactAgent(a) {
57
57
  };
58
58
  }
59
59
 
60
- export function buildManifest({ scope, sourceWorkspaceId, skills, agents, squads }) {
60
+ export function buildManifest({ scope, sourceWorkspaceId, skills, agents, squads, projects }) {
61
61
  const seenSkills = new Map();
62
62
  for (const s of skills) if (!seenSkills.has(s.name)) seenSkills.set(s.name, s);
63
63
  const seenAgents = new Map();
64
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);
65
67
  return {
66
68
  version: "1",
67
69
  scope,
@@ -75,6 +77,10 @@ export function buildManifest({ scope, sourceWorkspaceId, skills, agents, squads
75
77
  if (squad.instructions) entry.instructions_file = file.replace(/\.json$/, ".md");
76
78
  return entry;
77
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
+ })),
78
84
  };
79
85
  }
80
86
 
@@ -106,10 +112,27 @@ function collectAgent(cli, id, agentsById, skills, providerById) {
106
112
  return entry;
107
113
  }
108
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
+
109
131
  export function exportResource({ cli, scope, ids, outDir, sourceWorkspaceId, fs = nodeFs, download = fetchBinary }) {
110
132
  const skills = new Map(); // name -> normalized skill
111
133
  const agentsById = new Map(); // id -> { raw, red, skill_names }
112
134
  const squads = [];
135
+ const projects = [];
113
136
  // Lazy + memoized: only fetched when an agent is actually collected (skips
114
137
  // the extra CLI call on skill-only exports).
115
138
  let providerById = null;
@@ -132,10 +155,26 @@ export function exportResource({ cli, scope, ids, outDir, sourceWorkspaceId, fs
132
155
  if (scope === "skill") collectSkill(cli, ids.skillId, skills);
133
156
  else if (scope === "agent") collectAgent(cli, ids.agentId, agentsById, skills, getProviderById());
134
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()));
135
160
  else if (scope === "all") {
136
161
  for (const s of listSkills(cli)) collectSkill(cli, s.id, skills);
137
162
  for (const a of listAgents(cli)) collectAgent(cli, a.id, agentsById, skills, getProviderById());
138
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
+ }
139
178
  }
140
179
 
141
180
  const manifest = buildManifest({
@@ -143,6 +182,7 @@ export function exportResource({ cli, scope, ids, outDir, sourceWorkspaceId, fs
143
182
  skills: [...skills.values()].map((s) => ({ name: s.name, source_id: s.id })),
144
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 })),
145
184
  squads,
185
+ projects,
146
186
  });
147
187
 
148
188
  const warnings = [];
@@ -196,8 +236,13 @@ export function exportResource({ cli, scope, ids, outDir, sourceWorkspaceId, fs
196
236
  }
197
237
  fs.writeFileSync(`${outDir}/${entry.file}`, JSON.stringify(entry, null, 2));
198
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
+ }
199
244
  fs.writeFileSync(`${outDir}/manifest.json`, JSON.stringify(manifest, null, 2));
200
- return { manifest, warnings };
245
+ return { manifest, warnings, pruned_skills };
201
246
  }
202
247
 
203
248
  function main() {
@@ -209,8 +254,8 @@ function main() {
209
254
  const out = get("--out");
210
255
  const workspace = get("--workspace"); // optional: source workspace name
211
256
 
212
- if (!scope || !out || (scope !== "all" && !id)) {
213
- console.error("Usage: multica-export.mjs --scope <skill|agent|squad|all> --id <id> --out <dir> [--workspace <name>] (--id not needed for --scope all)");
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)");
214
259
  process.exit(1);
215
260
  }
216
261
 
@@ -225,8 +270,10 @@ function main() {
225
270
  if (scope === "skill") ids.skillId = id;
226
271
  else if (scope === "agent") ids.agentId = id;
227
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 */ }
228
275
  else if (scope === "all") { /* whole workspace — no id */ }
229
- 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); }
230
277
 
231
278
  const result = exportResource({ cli, scope, ids, outDir: out, sourceWorkspaceId, fs: nodeFs });
232
279
  console.log(JSON.stringify(result, null, 2));
@@ -1,5 +1,12 @@
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
+ }
3
10
 
4
11
  // Instructions live in a sibling .md referenced by `instructions_file` (mirrors
5
12
  // avatar_file). Legacy bundles carry no instructions_file and keep instructions
@@ -208,6 +215,7 @@ export function rewriteAgentMentions({ cli, manifest, dir, agentIdMap, sourceIdM
208
215
  export function importSquad({ cli, squad, agentIdMap, sourceIdMap }) {
209
216
  const existing = listSquads(cli);
210
217
  const leaderId = agentIdMap.get(squad.leader_name);
218
+ if (!leaderId) return { skipped: true, created: 0, updated: 0 };
211
219
  const match = findByName(existing, squad.name);
212
220
  let id, created = 0, updated = 0;
213
221
  // Squad instructions commonly list @mentions of teammate agents by their
@@ -233,12 +241,63 @@ export function importSquad({ cli, squad, agentIdMap, sourceIdMap }) {
233
241
  for (const m of squad.members) {
234
242
  if (m.agent_name === squad.leader_name) continue;
235
243
  const memberId = agentIdMap.get(m.agent_name);
236
- if (present.has(memberId)) continue;
244
+ if (!memberId || present.has(memberId)) continue;
237
245
  cli.run(["squad", "member", "add", id, "--member-id", memberId, "--role", m.role, "--type", "agent"]);
238
246
  }
239
247
  return { newId: id, created, updated };
240
248
  }
241
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
+
242
301
  export function collectSourceRuntimes(manifest) {
243
302
  return [...new Set((manifest.agents ?? []).map((a) => a.source_runtime_id).filter(Boolean))];
244
303
  }
@@ -275,46 +334,124 @@ export function resolveRuntimeMap({ cli, manifest, runtimeMap }) {
275
334
  return { effective, unresolved };
276
335
  }
277
336
 
278
- 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"]);
279
339
  const manifest = JSON.parse(fs.readFileSync(`${dir}/manifest.json`, "utf8"));
280
- const { effective, unresolved } = resolveRuntimeMap({ cli, manifest, runtimeMap });
281
- if (unresolved.length) {
282
- const detail = unresolved.map(({ srcId, provider, matchCount }) => provider
283
- ? `${srcId} (provider "${provider}": ${matchCount} matching runtimes in destination, expected exactly 1)`
284
- : `${srcId} (no provider recorded)`).join(", ");
285
- 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;
286
351
  }
287
352
 
288
- const skillRes = importSkills({ cli, manifest, dir, fs });
289
- 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: [] };
290
359
  // Runs after every agent exists so forward-referencing mentions resolve.
291
- 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
+
292
364
  const squadIdMap = new Map();
293
365
  let squadsCreated = 0, squadsUpdated = 0;
294
- for (const squad of manifest.squads ?? []) {
295
- squad.instructions = readInstructions(fs, dir, squad);
296
- const r = importSquad({ cli, squad, agentIdMap: agentRes.idMap, sourceIdMap: agentRes.sourceIdMap });
297
- squadIdMap.set(squad.name, r.newId);
298
- squadsCreated += r.created;
299
- squadsUpdated += r.updated;
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 });
300
381
  }
301
382
 
302
383
  return {
303
- created: { skills: skillRes.created, agents: agentRes.created, squads: squadsCreated },
304
- 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 },
305
387
  mentionsRewritten: mentionRes.updated,
306
388
  skillIdMap: Object.fromEntries(skillRes.idMap),
307
389
  agentIdMap: Object.fromEntries(agentRes.idMap),
308
390
  squadIdMap: Object.fromEntries(squadIdMap),
391
+ projectIdMap: Object.fromEntries(projectRes.idMap),
309
392
  secretsReminder: (manifest.agents ?? []).filter((a) => a.had_secrets).map((a) => a.name),
310
393
  secretsApplyFailures: agentRes.secretsApplyFailures,
311
394
  avatarApplyFailures: agentRes.avatarApplyFailures,
312
395
  avatarUnsupported: agentRes.avatarUnsupported,
313
396
  permissionApplyFailures: agentRes.permissionApplyFailures,
314
397
  permissionUnsupported: agentRes.permissionUnsupported,
398
+ squadsSkipped,
399
+ priorityUnsupported: projectRes.priorityUnsupported,
400
+ resourcesUnsupported: projectRes.resourcesUnsupported,
401
+ leadUnresolved: projectRes.leadUnresolved,
315
402
  };
316
403
  }
317
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
+
318
455
  function parseRuntimeMap(raw) {
319
456
  // Parse "srcId1=dstId1,srcId2=dstId2" into a Map.
320
457
  const map = new Map();
@@ -334,9 +471,11 @@ function main() {
334
471
  const dir = get("--dir");
335
472
  const workspace = get("--workspace");
336
473
  const rawMap = get("--runtime-map");
474
+ const rawInclude = get("--include");
475
+ const dryRun = args.includes("--dry-run");
337
476
 
338
477
  if (!dir || !workspace) {
339
- 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]");
340
479
  process.exit(1);
341
480
  }
342
481
 
@@ -345,8 +484,13 @@ function main() {
345
484
  const wsId = resolveWorkspaceId(resolver, workspace);
346
485
  const cli = makeCli(realExec, { workspaceId: wsId });
347
486
  const runtimeMap = parseRuntimeMap(rawMap);
487
+ const include = parseInclude(rawInclude);
348
488
 
349
- 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 });
350
494
  console.log(JSON.stringify(result, null, 2));
351
495
  }
352
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
 
@@ -32,25 +32,43 @@ Ask the user to select a resource by name. Resolve its `id` from the list output
32
32
 
33
33
  ## Step 3 — Determine output directory
34
34
 
35
- If the user specified an output directory, use it. Otherwise default to:
35
+ If the user specified an output directory, use it verbatim. Otherwise default to a workspace-rooted path.
36
36
 
37
+ First resolve `<workspace-name>`:
38
+
39
+ - If the user named a source workspace (the value you would pass as `--workspace <name>`), use that name.
40
+ - Otherwise, read the current default workspace's name:
41
+
42
+ ```bash
43
+ multica workspace get --output json
37
44
  ```
38
- ./multica-export-<slug>-<type>
39
- ```
40
45
 
41
- where `<slug>` is a lowercased, hyphenated form of the resource name.
46
+ and take its `.name`.
47
+
48
+ Slugify the resolved name for filesystem safety — lowercase it, replace each run of non-`[a-z0-9]` characters with a single `-`, and trim leading/trailing `-` (the same rule the scripts use internally).
49
+
50
+ Then construct the default directory by scope:
51
+
52
+ - `all` or `projects` (whole workspace) → `export/<workspace-name>`
53
+ - a single resource (`skill`, `agent`, `squad`, or `project`) → `export/<workspace-name>/<slug>-<type>`, where `<slug>` is the slugified resource name and `<type>` is the resource type.
54
+
55
+ Examples: `export all from mx-workspace` → `export/mx-workspace`; `export skill "Foo Bar" from mx-workspace` → `export/mx-workspace/foo-bar-skill`.
42
56
 
43
57
  ## Step 4 — Run the export
44
58
 
45
59
  ```bash
46
60
  node "${CLAUDE_PLUGIN_ROOT}/scripts/multica-export.mjs" \
47
- --scope <type|all> \
61
+ --scope <skill|agent|squad|project|projects|all> \
48
62
  --id <id> \
49
63
  --out <dir> \
50
64
  [--workspace <workspace-name>]
51
65
  ```
52
66
 
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.
67
+ `--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).
68
+
69
+ 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.
70
+
71
+ 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.
54
72
 
55
73
  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
74
 
@@ -61,5 +79,6 @@ Avatars are captured automatically: an agent's uploaded-image avatar is download
61
79
  Parse the JSON output from the script and report:
62
80
 
63
81
  - Directory written to.
64
- - Count of skills, agents, and squads exported.
82
+ - Count of skills, agents, squads, and projects exported.
83
+ - 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
84
  - 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>`."
@@ -8,18 +8,45 @@ allowed-tools: Bash, Read
8
8
 
9
9
  Import a local Multica bundle (produced by the export skill) into a target workspace.
10
10
 
11
- ## Step 1 — Confirm the target workspace
11
+ ## Step 1 — Determine the target workspace
12
12
 
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.
13
+ You need the exact workspace name as registered in Multica for `--workspace` in the steps below.
14
14
 
15
- ## Step 2 Run the import (auto-mapping first)
15
+ - If the user named a target workspace, use it.
16
+ - If the user did **not** name one, default to the **basename of the import folder** — e.g. importing from `export/mx-workspace` defaults the target workspace to `mx-workspace`. State the inferred workspace name to the user before continuing.
16
17
 
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:
18
+ No existence check is needed here: the Step 2 dry-run fails with `Unknown workspace "<name>"` if the inferred workspace is not present in the target account, at which point ask the user for the correct name.
19
+
20
+ ## Step 2 — Pre-flight (dry run)
21
+
22
+ 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
23
 
19
24
  ```bash
20
25
  node "${CLAUDE_PLUGIN_ROOT}/scripts/multica-import.mjs" \
21
26
  --dir <folder> \
22
- --workspace <workspace-name>
27
+ --workspace <workspace-name> \
28
+ --include agents,squads,projects \
29
+ [--runtime-map <srcId1=dstId1,srcId2=dstId2,...>] \
30
+ --dry-run
31
+ ```
32
+
33
+ Present the `bundle` and `willImport` counts, and every entry in `incompatibilities`, to the user.
34
+
35
+ ## Step 3 — Select which types to import
36
+
37
+ Ask the user which of `agents`, `squads`, `projects` they want to import. **Default is `agents,squads`** — `projects` requires explicit opt-in.
38
+
39
+ 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).
40
+
41
+ ## Step 4 — Run the import (auto-mapping first)
42
+
43
+ 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`:
44
+
45
+ ```bash
46
+ node "${CLAUDE_PLUGIN_ROOT}/scripts/multica-import.mjs" \
47
+ --dir <folder> \
48
+ --workspace <workspace-name> \
49
+ --include <agents,squads,projects>
23
50
  ```
24
51
 
25
52
  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,6 +62,7 @@ Ask the user to pick a matching target runtime by name or ID for each unmapped `
35
62
  node "${CLAUDE_PLUGIN_ROOT}/scripts/multica-import.mjs" \
36
63
  --dir <folder> \
37
64
  --workspace <workspace-name> \
65
+ --include <agents,squads,projects> \
38
66
  --runtime-map <srcId1=dstId1,srcId2=dstId2,...>
39
67
  ```
40
68
 
@@ -44,14 +72,15 @@ Instructions are read back from each resource's sibling `.md` (`agents/<slug>.md
44
72
 
45
73
  Avatars are restored automatically, but **only when the target resource has none** — an existing agent or squad that already carries an avatar is never overwritten. New agents get their bundled image re-uploaded; new squads get their `avatar_url` (emoji or URL) set. An agent whose source avatar was an emoji can't be restored (the CLI has no emoji setter for agents) and is reported as unsupported.
46
74
 
47
- ## Step 3 — Report results
75
+ ## Step 5 — Report results
48
76
 
49
77
  Parse the JSON output and report:
50
78
 
51
- - Created and updated counts for skills, agents, and squads.
79
+ - Created and updated counts for skills, agents, squads, and projects (`created.projects`/`updated.projects`).
52
80
  - Name-to-ID maps for skills and agents (`skillIdMap`, `agentIdMap`).
53
81
  - `squadIdMap`: name-to-ID map for every squad imported.
54
82
  - `mentionsRewritten`: how many agents had an agent-mention link rewritten to its new id.
83
+ - 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>`."
55
84
  - 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>`."
56
85
  - If `secretsApplyFailures` is non-empty, surface every agent name verbatim with: "WARNING: mcp_config or custom_env failed to apply to the following agents during import (the agent itself was still created/updated) — set them manually in the Multica UI: `<agent-name>`."
57
86
  - 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.40",
4
+ "version": "0.0.42",
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.40",
4
+ "version": "0.0.42",
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.40",
3
+ "version": "0.0.42",
4
4
  "description": "Step-by-step wizard for generating technical diagrams as SVG+PNG.",
5
5
  "author": {
6
6
  "name": "steven"