@drunkcoding/agents-and-skills 0.0.31 → 0.0.33

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.31",
15
+ "version": "0.0.33",
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.31",
29
+ "version": "0.0.33",
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.31",
44
+ "version": "0.0.33",
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.31",
60
+ "version": "0.0.33",
61
61
  "category": "tooling",
62
62
  "keywords": [
63
63
  "onboarding",
@@ -68,6 +68,23 @@
68
68
  "understand-anything",
69
69
  "git-lfs"
70
70
  ]
71
+ },
72
+ {
73
+ "name": "multica-tool",
74
+ "source": "./plugins/multica-tool",
75
+ "description": "Export, import, and sync Multica skills, agents, and squads between workspaces.",
76
+ "version": "0.0.33",
77
+ "category": "workflow",
78
+ "keywords": [
79
+ "multica",
80
+ "migration",
81
+ "export",
82
+ "import",
83
+ "sync",
84
+ "skills",
85
+ "agents",
86
+ "squads"
87
+ ]
71
88
  }
72
89
  ]
73
90
  }
package/README.md CHANGED
@@ -14,6 +14,7 @@ Published on npm as [`@drunkcoding/agents-and-skills`](https://www.npmjs.com/pac
14
14
  | [`plugin-validator`](plugins/plugin-validator) | Orchestrated validator that checks every plugin's skills, agents, commands, and hooks for spec compliance — runs in parallel and proposes batched fixes. |
15
15
  | [`team-share`](plugins/team-share) | Onboarding helper that shares Claude plugin settings, scaffolds `CLAUDE.md` when missing, refreshes the Understand-Anything graph, and stages the resulting repo artifacts for review. |
16
16
  | [`auto-power`](plugins/auto-power) | Single-command hands-off pipeline that wraps `obra/superpowers`. Auto-answers safe clarifying questions during spec, then runs plan → arch+sec → impl → verify → review → ff-merge with no further touchpoints. Checkpointed and resumable. Escalates on substantive failures. |
17
+ | [`multica-tool`](plugins/multica-tool) | Export, import, and sync Multica skills, agents, and squads between workspaces. |
17
18
 
18
19
  ## team-superpower v3
19
20
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drunkcoding/agents-and-skills",
3
- "version": "0.0.31",
3
+ "version": "0.0.33",
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.31",
4
+ "version": "0.0.33",
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"
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "multica-tool",
3
+ "version": "0.0.33",
4
+ "description": "Export, import, and sync Multica skills, agents, and squads between workspaces via the multica CLI."
5
+ }
@@ -0,0 +1,17 @@
1
+ ---
2
+ name: export
3
+ description: Use when delegating a Multica export as a background task. Runs the multica-tool:export skill in its own context for long background migrations and returns the final JSON report.
4
+ tools: Bash, Read, Skill
5
+ ---
6
+
7
+ # Multica Export Executor
8
+
9
+ Standalone executor for the `multica-tool:export` skill. Runs the export operation to completion and returns the final JSON report.
10
+
11
+ ## Behavior
12
+
13
+ You are a standalone executor. Invoke the `multica-tool:export` skill with the arguments passed to you, run it to completion, and return the final JSON report. You are NOT part of a coordinated team.
14
+
15
+ 1. Invoke the `multica-tool:export` skill, forwarding all arguments received.
16
+ 2. Wait for the skill to complete.
17
+ 3. Return the final JSON report from the skill output.
@@ -0,0 +1,17 @@
1
+ ---
2
+ name: import
3
+ description: Use when delegating a Multica import as a background task. Runs the multica-tool:import skill in its own context for long background migrations and returns the final JSON report.
4
+ tools: Bash, Read, Skill
5
+ ---
6
+
7
+ # Multica Import Executor
8
+
9
+ Standalone executor for the `multica-tool:import` skill. Runs the import operation to completion and returns the final JSON report.
10
+
11
+ ## Behavior
12
+
13
+ You are a standalone executor. Invoke the `multica-tool:import` skill with the arguments passed to you, run it to completion, and return the final JSON report. You are NOT part of a coordinated team.
14
+
15
+ 1. Invoke the `multica-tool:import` skill, forwarding all arguments received.
16
+ 2. Wait for the skill to complete.
17
+ 3. Return the final JSON report from the skill output.
@@ -0,0 +1,17 @@
1
+ ---
2
+ name: sync
3
+ description: Use when delegating a Multica sync as a background task. Runs the multica-tool:sync skill in its own context for long background migrations and returns the final JSON report.
4
+ tools: Bash, Read, Skill
5
+ ---
6
+
7
+ # Multica Sync Executor
8
+
9
+ Standalone executor for the `multica-tool:sync` skill. Runs the sync operation to completion and returns the final JSON report.
10
+
11
+ ## Behavior
12
+
13
+ You are a standalone executor. Invoke the `multica-tool:sync` skill with the arguments passed to you, run it to completion, and return the final JSON report. You are NOT part of a coordinated team.
14
+
15
+ 1. Invoke the `multica-tool:sync` skill, forwarding all arguments received.
16
+ 2. Wait for the skill to complete.
17
+ 3. Return the final JSON report from the skill output.
@@ -0,0 +1,6 @@
1
+ ---
2
+ description: Export Multica skills, agents, or squads to a local folder for backup or migration.
3
+ argument-hint: [optional target path or options]
4
+ ---
5
+
6
+ Invoke the `multica-tool:export` skill. Arguments: $ARGUMENTS
@@ -0,0 +1,6 @@
1
+ ---
2
+ description: Import a Multica export bundle into a target workspace, mapping source runtimes to target runtimes.
3
+ argument-hint: [optional bundle path or options]
4
+ ---
5
+
6
+ Invoke the `multica-tool:import` skill. Arguments: $ARGUMENTS
@@ -0,0 +1,6 @@
1
+ ---
2
+ description: Sync a Multica skill, agent, or squad from one workspace to another in a single operation.
3
+ argument-hint: [optional source/target workspace or options]
4
+ ---
5
+
6
+ Invoke the `multica-tool:sync` skill. Arguments: $ARGUMENTS
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env node
2
+ // Generate OpenCode discovery artifacts from the canonical Claude Code plugin.
3
+ // Source of truth: this plugin's skills/ + agents/ + commands/ + scripts/.
4
+ // Output: dist/opencode/{skills,agents,commands}/ — copy into .opencode/ (project)
5
+ // or ~/.config/opencode/ (global). Run: node plugins/multica-tool/scripts/build-dist.mjs
6
+ import { readFileSync, writeFileSync, mkdirSync, readdirSync, rmSync, copyFileSync } from "node:fs";
7
+ import { dirname, join } from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+
10
+ const PLUGIN_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
11
+ const REPO_ROOT = dirname(dirname(PLUGIN_ROOT));
12
+ const OUT = join(REPO_ROOT, "dist", "opencode");
13
+ const NAMES = ["export", "import", "sync"];
14
+ // Runtime scripts shared by every skill (exclude this generator).
15
+ const SCRIPTS = readdirSync(join(PLUGIN_ROOT, "scripts")).filter(
16
+ (f) => f.endsWith(".mjs") && f !== "build-dist.mjs",
17
+ );
18
+
19
+ // Naive frontmatter split — every source file uses single-line `key: value`.
20
+ function parse(text) {
21
+ const m = text.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
22
+ if (!m) throw new Error("missing frontmatter");
23
+ const fm = {};
24
+ for (const line of m[1].split("\n")) {
25
+ const i = line.indexOf(":");
26
+ if (i !== -1) fm[line.slice(0, i).trim()] = line.slice(i + 1).trim();
27
+ }
28
+ return { fm, body: m[2] };
29
+ }
30
+
31
+ const read = (...p) => readFileSync(join(PLUGIN_ROOT, ...p), "utf8");
32
+ function write(rel, content) {
33
+ const abs = join(OUT, rel);
34
+ mkdirSync(dirname(abs), { recursive: true });
35
+ writeFileSync(abs, content);
36
+ }
37
+ // OpenCode resolves a skill's relative paths against the skill base dir it injects.
38
+ const toOpencodePath = (s) => s.replace(/"\$\{CLAUDE_PLUGIN_ROOT\}\/scripts\//g, '"scripts/');
39
+ const stripSelfPrefix = (s) => s.replace(/multica-tool:/g, "");
40
+
41
+ rmSync(OUT, { recursive: true, force: true });
42
+
43
+ for (const name of NAMES) {
44
+ // Skill: keep prose, rewrite script path, drop Claude-only allowed-tools, bundle scripts.
45
+ const skill = parse(read("skills", name, "SKILL.md"));
46
+ const fm = `---\nname: ${skill.fm.name}\ndescription: ${skill.fm.description}\n---\n`;
47
+ write(`skills/${name}/SKILL.md`, fm + toOpencodePath(skill.body));
48
+ for (const s of SCRIPTS) {
49
+ mkdirSync(join(OUT, "skills", name, "scripts"), { recursive: true });
50
+ copyFileSync(join(PLUGIN_ROOT, "scripts", s), join(OUT, "skills", name, "scripts", s));
51
+ }
52
+
53
+ // Command: description only; body invokes the local (unprefixed) skill name.
54
+ const cmd = parse(read("commands", `${name}.md`));
55
+ write(`commands/${name}.md`, `---\ndescription: ${stripSelfPrefix(cmd.fm.description)}\n---\n${stripSelfPrefix(cmd.body)}`);
56
+
57
+ // Agent: tools -> permission.bash; body unprefixed.
58
+ const agent = parse(read("agents", `${name}.md`));
59
+ const head = `---\ndescription: ${stripSelfPrefix(agent.fm.description)}\nmode: subagent\npermission:\n bash: allow\n---\n`;
60
+ write(`agents/${name}.md`, head + stripSelfPrefix(agent.body));
61
+ }
62
+
63
+ // Self-check: outputs must carry no Claude-only path/env and must bundle scripts.
64
+ for (const name of NAMES) {
65
+ const md = readFileSync(join(OUT, "skills", name, "SKILL.md"), "utf8");
66
+ if (md.includes("CLAUDE_PLUGIN_ROOT") || md.includes("plugins/multica-tool"))
67
+ throw new Error(`leaked Claude path in ${name}/SKILL.md`);
68
+ for (const s of SCRIPTS) readFileSync(join(OUT, "skills", name, "scripts", s)); // throws if missing
69
+ }
70
+
71
+ console.log(`Wrote OpenCode artifacts for [${NAMES.join(", ")}] to ${OUT}`);
@@ -0,0 +1,93 @@
1
+ import { spawnSync } from "node:child_process";
2
+
3
+ export function slugify(name) {
4
+ const s = String(name)
5
+ .toLowerCase()
6
+ .replace(/[^a-z0-9]+/g, "-")
7
+ .replace(/^-+|-+$/g, "");
8
+ return s || "unnamed";
9
+ }
10
+
11
+ export function realExec(args) {
12
+ return spawnSync("multica", args, { encoding: "utf8" });
13
+ }
14
+
15
+ export function makeCli(exec, { workspaceId } = {}) {
16
+ function run(args) {
17
+ let full = args;
18
+ if (workspaceId) {
19
+ full = [...args, "--workspace-id", workspaceId];
20
+ }
21
+ const res = exec(full);
22
+ if (res.status !== 0) throw new Error(res.stderr?.trim() || `multica exited ${res.status}`);
23
+ return res.stdout;
24
+ }
25
+ function json(args) {
26
+ const fullArgs = workspaceId ? [...args, "--workspace-id", workspaceId, "--output", "json"] : [...args, "--output", "json"];
27
+ const res = exec(fullArgs);
28
+ if (res.status !== 0) throw new Error(res.stderr?.trim() || `multica exited ${res.status}`);
29
+ return JSON.parse(res.stdout);
30
+ }
31
+ return { run, json };
32
+ }
33
+
34
+ export function requireAuth(exec) {
35
+ const res = exec(["auth", "status"]);
36
+ if (res.status !== 0) throw new Error("Not authenticated. Run: multica login");
37
+ }
38
+
39
+ export function findByName(list, name) {
40
+ const hits = (list || []).filter((x) => x.name === name);
41
+ if (hits.length > 1) throw new Error(`Duplicate name "${name}" — refusing to guess`);
42
+ return hits[0] || null;
43
+ }
44
+
45
+ export function resolveWorkspaceId(cli, name) {
46
+ const list = cli.json(["workspace", "list"]);
47
+ const hits = list.filter((w) => w.name === name);
48
+ if (hits.length === 0) throw new Error(`Unknown workspace "${name}"`);
49
+ if (hits.length > 1) throw new Error(`Ambiguous workspace "${name}"`);
50
+ return hits[0].id;
51
+ }
52
+
53
+ export const listRuntimes = (cli) => cli.json(["runtime", "list"]);
54
+ export const listSkills = (cli) => cli.json(["skill", "list"]);
55
+ export const listAgents = (cli) => cli.json(["agent", "list"]);
56
+ export const listSquads = (cli) => cli.json(["squad", "list"]);
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.
60
+ export function getSkill(cli, id) {
61
+ const s = cli.json(["skill", "get", id]);
62
+ return {
63
+ id: s.id, name: s.name, description: s.description,
64
+ content: s.content ?? "", config: s.config ?? {},
65
+ files: (s.files ?? []).map((f) => ({ path: f.path, content: f.content })),
66
+ };
67
+ }
68
+
69
+ export function getAgent(cli, id) {
70
+ const a = cli.json(["agent", "get", id]);
71
+ return {
72
+ id: a.id, name: a.name, description: a.description, instructions: a.instructions,
73
+ model: a.model, visibility: a.visibility,
74
+ maxConcurrentTasks: a.max_concurrent_tasks,
75
+ runtimeConfig: a.runtime_config,
76
+ customArgs: a.custom_args,
77
+ thinkingLevel: a.thinking_level,
78
+ runtimeId: a.runtime_id,
79
+ hasCustomEnv: a.has_custom_env,
80
+ mcpConfig: a.mcp_config,
81
+ skills: (a.skills ?? []).map((sk) => ({ id: sk.id, name: sk.name })),
82
+ };
83
+ }
84
+
85
+ export function getSquad(cli, id) {
86
+ const s = cli.json(["squad", "get", id]);
87
+ return { id: s.id, name: s.name, description: s.description, leaderId: s.leader_id };
88
+ }
89
+
90
+ export const getSquadMembers = (cli, id) =>
91
+ (cli.json(["squad", "member", "list", id]) ?? []).map((m) => ({
92
+ memberId: m.member_id, memberType: m.member_type, role: m.role || "member",
93
+ }));
@@ -0,0 +1,136 @@
1
+ import * as nodeFs from "node:fs";
2
+ import { slugify, getSkill, getAgent, getSquad, getSquadMembers, makeCli, realExec, requireAuth, resolveWorkspaceId } from "./lib.mjs";
3
+
4
+ const nonEmpty = (v) => v && typeof v === "object" && Object.keys(v).length > 0;
5
+
6
+ export function redactAgent(a) {
7
+ // a is a normalized (camelCase) agent from getAgent.
8
+ const { id, hasCustomEnv, mcpConfig, skills, runtimeId, ...rest } = a;
9
+ const hadSecrets = !!hasCustomEnv || nonEmpty(mcpConfig);
10
+ return {
11
+ record: { ...rest, sourceRuntimeId: runtimeId, skillNames: [], hadSecrets },
12
+ hadSecrets,
13
+ };
14
+ }
15
+
16
+ export function buildManifest({ scope, sourceWorkspaceId, skills, agents, squad }) {
17
+ const seenSkills = new Map();
18
+ for (const s of skills) if (!seenSkills.has(s.name)) seenSkills.set(s.name, s);
19
+ const seenAgents = new Map();
20
+ for (const a of agents) if (!seenAgents.has(a.name)) seenAgents.set(a.name, a);
21
+ return {
22
+ version: "1",
23
+ scope,
24
+ sourceWorkspaceId,
25
+ skills: [...seenSkills.values()].map((s) => ({ name: s.name, dir: `skills/${slugify(s.name)}`, sourceId: s.sourceId })),
26
+ agents: [...seenAgents.values()].map((a) => ({ name: a.name, file: `agents/${slugify(a.name)}.json`, sourceRuntimeId: a.sourceRuntimeId, skillNames: a.skillNames, hadSecrets: !!a.hadSecrets })),
27
+ squads: squad ? [{ name: squad.name, file: `squads/${slugify(squad.name)}.json`, description: squad.description ?? "", leaderName: squad.leaderName, members: squad.members }] : [],
28
+ };
29
+ }
30
+
31
+ function collectSkill(cli, id, skills) {
32
+ const s = getSkill(cli, id);
33
+ if (!skills.has(s.name)) skills.set(s.name, s);
34
+ return s.name;
35
+ }
36
+
37
+ // Keyed by agent id (so squad leaderId/memberId resolve to names). Stores the
38
+ // normalized agent, its redaction result, and its skill names.
39
+ function collectAgent(cli, id, agentsById, skills) {
40
+ if (agentsById.has(id)) return agentsById.get(id);
41
+ const a = getAgent(cli, id);
42
+ const skillNames = a.skills.map((sk) => collectSkill(cli, sk.id, skills));
43
+ const red = redactAgent(a);
44
+ const entry = { raw: a, red, skillNames };
45
+ agentsById.set(id, entry);
46
+ return entry;
47
+ }
48
+
49
+ export function exportResource({ cli, scope, ids, outDir, sourceWorkspaceId, fs = nodeFs }) {
50
+ const skills = new Map(); // name -> normalized skill
51
+ const agentsById = new Map(); // id -> { raw, red, skillNames }
52
+ let squad = null;
53
+
54
+ if (scope === "skill") collectSkill(cli, ids.skillId, skills);
55
+ if (scope === "agent") collectAgent(cli, ids.agentId, agentsById, skills);
56
+ if (scope === "squad") {
57
+ const sq = getSquad(cli, ids.squadId);
58
+ const members = getSquadMembers(cli, ids.squadId).filter((m) => m.memberType === "agent");
59
+ for (const m of members) collectAgent(cli, m.memberId, agentsById, skills);
60
+ if (!agentsById.has(sq.leaderId)) collectAgent(cli, sq.leaderId, agentsById, skills);
61
+ const nameOf = (id) => agentsById.get(id)?.raw.name;
62
+ squad = {
63
+ name: sq.name,
64
+ description: sq.description,
65
+ leaderName: nameOf(sq.leaderId),
66
+ members: members.map((m) => ({ agentName: nameOf(m.memberId), role: m.role })),
67
+ };
68
+ }
69
+
70
+ const manifest = buildManifest({
71
+ scope, sourceWorkspaceId,
72
+ skills: [...skills.values()].map((s) => ({ name: s.name, sourceId: s.id })),
73
+ agents: [...agentsById.values()].map((a) => ({ name: a.raw.name, sourceRuntimeId: a.raw.runtimeId, skillNames: a.skillNames, hadSecrets: a.red.hadSecrets })),
74
+ squad,
75
+ });
76
+
77
+ const warnings = [];
78
+ fs.mkdirSync(outDir, { recursive: true });
79
+
80
+ for (const entry of manifest.skills) {
81
+ const s = skills.get(entry.name);
82
+ const dir = `${outDir}/${entry.dir}`;
83
+ fs.mkdirSync(dir, { recursive: true });
84
+ fs.writeFileSync(`${dir}/SKILL.md`, s.content ?? "");
85
+ fs.writeFileSync(`${dir}/config.json`, JSON.stringify(s.config ?? {}, null, 2));
86
+ for (const f of s.files ?? []) fs.writeFileSync(`${dir}/${f.path}`, f.content ?? "");
87
+ }
88
+ // Index agent entries by name for the manifest writing loop.
89
+ const agentByName = new Map([...agentsById.values()].map((a) => [a.raw.name, a]));
90
+ for (const entry of manifest.agents) {
91
+ const { raw, red, skillNames } = agentByName.get(entry.name);
92
+ const record = { ...red.record, skillNames };
93
+ if (red.hadSecrets) warnings.push(raw.name);
94
+ fs.mkdirSync(`${outDir}/agents`, { recursive: true });
95
+ fs.writeFileSync(`${outDir}/${entry.file}`, JSON.stringify(record, null, 2));
96
+ }
97
+ for (const entry of manifest.squads) {
98
+ fs.mkdirSync(`${outDir}/squads`, { recursive: true });
99
+ fs.writeFileSync(`${outDir}/${entry.file}`, JSON.stringify(entry, null, 2));
100
+ }
101
+ fs.writeFileSync(`${outDir}/manifest.json`, JSON.stringify(manifest, null, 2));
102
+ return { manifest, warnings };
103
+ }
104
+
105
+ function main() {
106
+ const args = process.argv.slice(2);
107
+ const get = (flag) => { const i = args.indexOf(flag); return i !== -1 ? args[i + 1] : null; };
108
+
109
+ const scope = get("--scope");
110
+ const id = get("--id");
111
+ const out = get("--out");
112
+ const workspace = get("--workspace"); // optional: source workspace name
113
+
114
+ if (!scope || !id || !out) {
115
+ console.error("Usage: multica-export.mjs --scope <skill|agent|squad> --id <id> --out <dir> [--workspace <name>]");
116
+ process.exit(1);
117
+ }
118
+
119
+ requireAuth(realExec);
120
+
121
+ let sourceWorkspaceId = "";
122
+ const resolver = makeCli(realExec);
123
+ if (workspace) sourceWorkspaceId = resolveWorkspaceId(resolver, workspace);
124
+ const cli = workspace ? makeCli(realExec, { workspaceId: sourceWorkspaceId }) : resolver;
125
+
126
+ const ids = {};
127
+ if (scope === "skill") ids.skillId = id;
128
+ else if (scope === "agent") ids.agentId = id;
129
+ else if (scope === "squad") ids.squadId = id;
130
+ else { console.error(`Unknown scope "${scope}" — use skill|agent|squad`); process.exit(1); }
131
+
132
+ const result = exportResource({ cli, scope, ids, outDir: out, sourceWorkspaceId, fs: nodeFs });
133
+ console.log(JSON.stringify(result, null, 2));
134
+ }
135
+
136
+ if (import.meta.url === `file://${process.argv[1]}`) main();
@@ -0,0 +1,146 @@
1
+ import * as nodeFs from "node:fs";
2
+ import { listSkills, listAgents, listSquads, findByName, makeCli, realExec, requireAuth, resolveWorkspaceId } from "./lib.mjs";
3
+
4
+ export function importSkills({ cli, manifest, dir, fs = nodeFs }) {
5
+ const idMap = new Map();
6
+ let created = 0, updated = 0;
7
+ const existing = listSkills(cli);
8
+
9
+ for (const s of manifest.skills) {
10
+ const sdir = dir === "." ? s.dir : `${dir}/${s.dir}`;
11
+ const contentPath = `${sdir}/SKILL.md`;
12
+ const configPath = `${sdir}/config.json`;
13
+ const config = fs.existsSync(configPath) ? fs.readFileSync(configPath, "utf8") : "{}";
14
+ const match = findByName(existing, s.name);
15
+ let id;
16
+ if (match) {
17
+ cli.run(["skill", "update", match.id, "--content-file", contentPath, "--config", config]);
18
+ id = match.id; updated++;
19
+ } else {
20
+ const out = cli.run(["skill", "create", "--name", s.name, "--content-file", contentPath, "--config", config]);
21
+ id = JSON.parse(out).id; created++;
22
+ }
23
+ idMap.set(s.name, id);
24
+ // upsert extra files (everything except SKILL.md and config.json)
25
+ for (const f of fs.readdirSync(sdir)) {
26
+ if (f === "SKILL.md" || f === "config.json") continue;
27
+ cli.run(["skill", "files", "upsert", id, "--path", f, "--content-file", `${sdir}/${f}`]);
28
+ }
29
+ }
30
+ return { idMap, created, updated };
31
+ }
32
+
33
+ export function importAgents({ cli, manifest, dir, skillIdMap, runtimeMap, fs = nodeFs }) {
34
+ const idMap = new Map();
35
+ let created = 0, updated = 0;
36
+ const existing = listAgents(cli);
37
+
38
+ for (const a of manifest.agents) {
39
+ const rec = JSON.parse(fs.readFileSync(`${dir}/${a.file}`, "utf8"));
40
+ const targetRuntime = runtimeMap.get(rec.sourceRuntimeId);
41
+ if (!targetRuntime) throw new Error(`Unmapped runtime "${rec.sourceRuntimeId}" for agent "${rec.name}"`);
42
+ // Only pass optional flags when present — `--model ""` would CLEAR the model.
43
+ const common = [
44
+ "--visibility", rec.visibility ?? "private",
45
+ "--max-concurrent-tasks", String(rec.maxConcurrentTasks ?? 6),
46
+ ];
47
+ if (rec.instructions) common.push("--instructions", rec.instructions);
48
+ if (rec.model) common.push("--model", rec.model);
49
+ if (rec.thinkingLevel) common.push("--thinking-level", rec.thinkingLevel);
50
+ if (rec.runtimeConfig && Object.keys(rec.runtimeConfig).length) common.push("--runtime-config", JSON.stringify(rec.runtimeConfig));
51
+ if (Array.isArray(rec.customArgs) && rec.customArgs.length) common.push("--custom-args", JSON.stringify(rec.customArgs));
52
+ const match = findByName(existing, rec.name);
53
+ let id;
54
+ if (match) {
55
+ cli.run(["agent", "update", match.id, "--runtime-id", targetRuntime, ...common]);
56
+ id = match.id; updated++;
57
+ } else {
58
+ const out = cli.run(["agent", "create", "--name", rec.name, "--runtime-id", targetRuntime, ...common]);
59
+ id = JSON.parse(out).id; created++;
60
+ }
61
+ idMap.set(rec.name, id);
62
+ const skillIds = (rec.skillNames ?? []).map((n) => skillIdMap.get(n)).filter(Boolean);
63
+ cli.run(["agent", "skills", "set", id, "--skill-ids", skillIds.join(",")]);
64
+ }
65
+ return { idMap, created, updated };
66
+ }
67
+
68
+ export function importSquad({ cli, squad, agentIdMap }) {
69
+ const existing = listSquads(cli);
70
+ const leaderId = agentIdMap.get(squad.leaderName);
71
+ const match = findByName(existing, squad.name);
72
+ let id, created = 0, updated = 0;
73
+ if (match) {
74
+ cli.run(["squad", "update", match.id, "--leader", leaderId, "--description", squad.description ?? ""]);
75
+ id = match.id; updated++;
76
+ } else {
77
+ const out = cli.run(["squad", "create", "--name", squad.name, "--leader", leaderId, "--description", squad.description ?? ""]);
78
+ id = JSON.parse(out).id; created++;
79
+ }
80
+ for (const m of squad.members) {
81
+ if (m.agentName === squad.leaderName) continue;
82
+ cli.run(["squad", "member", "add", id, "--member-id", agentIdMap.get(m.agentName), "--role", m.role, "--type", "agent"]);
83
+ }
84
+ return { newId: id, created, updated };
85
+ }
86
+
87
+ export function collectSourceRuntimes(manifest) {
88
+ return [...new Set((manifest.agents ?? []).map((a) => a.sourceRuntimeId).filter(Boolean))];
89
+ }
90
+
91
+ export function importBundle({ cli, dir, runtimeMap, fs = nodeFs }) {
92
+ const manifest = JSON.parse(fs.readFileSync(`${dir}/manifest.json`, "utf8"));
93
+ const missing = collectSourceRuntimes(manifest).filter((r) => !runtimeMap.has(r));
94
+ if (missing.length) throw new Error(`Unmapped runtimes: ${missing.join(", ")} — aborting before any write`);
95
+
96
+ const skillRes = importSkills({ cli, manifest, dir, fs });
97
+ const agentRes = importAgents({ cli, manifest, dir, skillIdMap: skillRes.idMap, runtimeMap, fs });
98
+ let squadRes = { newId: null, created: 0, updated: 0 };
99
+ if (manifest.squads?.length) squadRes = importSquad({ cli, squad: manifest.squads[0], agentIdMap: agentRes.idMap });
100
+
101
+ return {
102
+ created: { skills: skillRes.created, agents: agentRes.created, squads: squadRes.created },
103
+ updated: { skills: skillRes.updated, agents: agentRes.updated, squads: squadRes.updated },
104
+ skillIdMap: Object.fromEntries(skillRes.idMap),
105
+ agentIdMap: Object.fromEntries(agentRes.idMap),
106
+ squadId: squadRes.newId,
107
+ secretsReminder: (manifest.agents ?? []).filter((a) => a.hadSecrets).map((a) => a.name),
108
+ };
109
+ }
110
+
111
+ function parseRuntimeMap(raw) {
112
+ // Parse "srcId1=dstId1,srcId2=dstId2" into a Map.
113
+ const map = new Map();
114
+ if (!raw) return map;
115
+ for (const pair of raw.split(",")) {
116
+ const eq = pair.indexOf("=");
117
+ if (eq === -1) continue;
118
+ map.set(pair.slice(0, eq).trim(), pair.slice(eq + 1).trim());
119
+ }
120
+ return map;
121
+ }
122
+
123
+ function main() {
124
+ const args = process.argv.slice(2);
125
+ const get = (flag) => { const i = args.indexOf(flag); return i !== -1 ? args[i + 1] : null; };
126
+
127
+ const dir = get("--dir");
128
+ const workspace = get("--workspace");
129
+ const rawMap = get("--runtime-map");
130
+
131
+ if (!dir || !workspace) {
132
+ console.error("Usage: multica-import.mjs --dir <folder> --workspace <name> [--runtime-map <src=dst,...>]");
133
+ process.exit(1);
134
+ }
135
+
136
+ requireAuth(realExec);
137
+ const resolver = makeCli(realExec);
138
+ const wsId = resolveWorkspaceId(resolver, workspace);
139
+ const cli = makeCli(realExec, { workspaceId: wsId });
140
+ const runtimeMap = parseRuntimeMap(rawMap);
141
+
142
+ const result = importBundle({ cli, dir, runtimeMap, fs: nodeFs });
143
+ console.log(JSON.stringify(result, null, 2));
144
+ }
145
+
146
+ if (import.meta.url === `file://${process.argv[1]}`) main();
@@ -0,0 +1,66 @@
1
+ import * as nodeFs from "node:fs";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+ import { makeCli, resolveWorkspaceId, findByName, listSkills, listAgents, listSquads, realExec, requireAuth } from "./lib.mjs";
5
+ import { exportResource } from "./multica-export.mjs";
6
+ import { importBundle } from "./multica-import.mjs";
7
+
8
+ export function resolveScopeId(cli, type, name) {
9
+ const lists = { skill: listSkills, agent: listAgents, squad: listSquads };
10
+ if (!lists[type]) throw new Error(`Unknown type "${type}" (skill|agent|squad)`);
11
+ const match = findByName(lists[type](cli), name);
12
+ if (!match) throw new Error(`Unknown ${type} "${name}" in source workspace`);
13
+ const key = { skill: "skillId", agent: "agentId", squad: "squadId" }[type];
14
+ return { scope: type, ids: { [key]: match.id } };
15
+ }
16
+
17
+ export function sync({ exec, type, name, srcWsName, destWsName, tmpDir, runtimeMap, fs = nodeFs }) {
18
+ const resolver = makeCli(exec); // no ws — for workspace list
19
+ const srcId = resolveWorkspaceId(resolver, srcWsName);
20
+ const destId = resolveWorkspaceId(resolver, destWsName);
21
+ const srcCli = makeCli(exec, { workspaceId: srcId });
22
+ const destCli = makeCli(exec, { workspaceId: destId });
23
+
24
+ const { scope, ids } = resolveScopeId(srcCli, type, name);
25
+ exportResource({ cli: srcCli, scope, ids, outDir: tmpDir, sourceWorkspaceId: srcId, fs });
26
+ return importBundle({ cli: destCli, dir: tmpDir, runtimeMap, fs });
27
+ }
28
+
29
+ function parseRuntimeMap(raw) {
30
+ const map = new Map();
31
+ if (!raw) return map;
32
+ for (const pair of raw.split(",")) {
33
+ const eq = pair.indexOf("=");
34
+ if (eq === -1) continue;
35
+ map.set(pair.slice(0, eq).trim(), pair.slice(eq + 1).trim());
36
+ }
37
+ return map;
38
+ }
39
+
40
+ function main() {
41
+ // Usage: multica-sync.mjs <type> <name> from <src-ws> <dest-ws> [--runtime-map <src=dst,...>]
42
+ const args = process.argv.slice(2);
43
+ const get = (flag) => { const i = args.indexOf(flag); return i !== -1 ? args[i + 1] : null; };
44
+
45
+ // Positional: type name from srcWs destWs
46
+ const [type, name, _from, srcWsName, destWsName] = args;
47
+ const rawMap = get("--runtime-map");
48
+
49
+ if (!type || !name || _from !== "from" || !srcWsName || !destWsName) {
50
+ console.error("Usage: multica-sync.mjs <type> <name> from <src-ws> <dest-ws> [--runtime-map <src=dst,...>]");
51
+ process.exit(1);
52
+ }
53
+
54
+ requireAuth(realExec);
55
+ const runtimeMap = parseRuntimeMap(rawMap);
56
+ const tmpDir = nodeFs.mkdtempSync(path.join(os.tmpdir(), "multica-sync-"));
57
+
58
+ try {
59
+ const result = sync({ exec: realExec, type, name, srcWsName, destWsName, tmpDir, runtimeMap, fs: nodeFs });
60
+ console.log(JSON.stringify(result, null, 2));
61
+ } finally {
62
+ nodeFs.rmSync(tmpDir, { recursive: true, force: true });
63
+ }
64
+ }
65
+
66
+ if (import.meta.url === `file://${process.argv[1]}`) main();
@@ -0,0 +1,61 @@
1
+ ---
2
+ name: export
3
+ description: Use when the user wants to export Multica skills, agents, or squads to a local folder for backup, version control, or cross-workspace migration.
4
+ allowed-tools: Bash, Read
5
+ ---
6
+
7
+ # export
8
+
9
+ Export a Multica resource (skill, agent, or squad) to a local bundle directory.
10
+
11
+ ## Step 1 — Verify authentication
12
+
13
+ Run the export script; it calls `multica auth status` internally and exits with an error message if unauthenticated:
14
+
15
+ ```bash
16
+ node "${CLAUDE_PLUGIN_ROOT}/scripts/multica-export.mjs" --help 2>&1 || true
17
+ ```
18
+
19
+ If `multica login` is required, surface that message verbatim and stop.
20
+
21
+ ## Step 2 — Determine scope and resource ID
22
+
23
+ If the user named a specific resource and type (`skill`, `agent`, or `squad`), use those directly.
24
+
25
+ Otherwise, list available resources for the chosen type and present a pick list:
26
+
27
+ ```bash
28
+ multica <type> list --output json
29
+ ```
30
+
31
+ Ask the user to select a resource by name. Resolve its `id` from the list output.
32
+
33
+ ## Step 3 — Determine output directory
34
+
35
+ If the user specified an output directory, use it. Otherwise default to:
36
+
37
+ ```
38
+ ./multica-export-<slug>-<type>
39
+ ```
40
+
41
+ where `<slug>` is a lowercased, hyphenated form of the resource name.
42
+
43
+ ## Step 4 — Run the export
44
+
45
+ ```bash
46
+ node "${CLAUDE_PLUGIN_ROOT}/scripts/multica-export.mjs" \
47
+ --scope <type> \
48
+ --id <id> \
49
+ --out <dir> \
50
+ [--workspace <workspace-name>]
51
+ ```
52
+
53
+ The script writes `manifest.json`, skill SKILL.md files, agent JSON files, and squad JSON files into `<dir>`.
54
+
55
+ ## Step 5 — Report results
56
+
57
+ Parse the JSON output from the script and report:
58
+
59
+ - Directory written to.
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>`."
@@ -0,0 +1,49 @@
1
+ ---
2
+ name: import
3
+ description: Use when the user wants to import a Multica export bundle into a target workspace, mapping source runtimes to target runtimes before writing any resources.
4
+ allowed-tools: Bash, Read
5
+ ---
6
+
7
+ # import
8
+
9
+ Import a local Multica bundle (produced by the export skill) into a target workspace.
10
+
11
+ ## Step 1 — Confirm the target workspace
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.
14
+
15
+ ## Step 2 — Plan pass: read the bundle and collect unmapped runtimes
16
+
17
+ Read the manifest to discover which source runtimes are referenced:
18
+
19
+ ```bash
20
+ cat <folder>/manifest.json
21
+ ```
22
+
23
+ Collect each distinct `sourceRuntimeId` from the `agents` array.
24
+
25
+ List runtimes available in the target workspace:
26
+
27
+ ```bash
28
+ multica runtime list --output json
29
+ ```
30
+
31
+ For each distinct `sourceRuntimeId`, ask the user to pick a matching target runtime by name or ID. Build a mapping in the form `srcId=dstId`. If any source runtime has no mapping selected, abort and explain which IDs remain unmapped — do not write any resources.
32
+
33
+ ## Step 3 — Run the import
34
+
35
+ ```bash
36
+ node "${CLAUDE_PLUGIN_ROOT}/scripts/multica-import.mjs" \
37
+ --dir <folder> \
38
+ --workspace <workspace-name> \
39
+ --runtime-map <srcId1=dstId1,srcId2=dstId2,...>
40
+ ```
41
+
42
+ ## Step 4 — Report results
43
+
44
+ Parse the JSON output and report:
45
+
46
+ - Created and updated counts for skills, agents, and squads.
47
+ - Name-to-ID maps for skills and agents (`skillIdMap`, `agentIdMap`).
48
+ - Squad ID if a squad was imported.
49
+ - 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>`."
@@ -0,0 +1,50 @@
1
+ ---
2
+ name: sync
3
+ description: Use when the user wants to sync a Multica skill, agent, or squad from one workspace to another in a single operation.
4
+ allowed-tools: Bash, Read
5
+ ---
6
+
7
+ # sync
8
+
9
+ Copy a Multica resource from a source workspace to a destination workspace.
10
+
11
+ ## Step 1 — Parse the sync request
12
+
13
+ Expect the user's request in the form:
14
+
15
+ ```
16
+ sync <type> <name> from <src-ws> to <dest-ws>
17
+ ```
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.
20
+
21
+ ## Step 2 — Build the runtime map
22
+
23
+ List runtimes available in the destination workspace:
24
+
25
+ ```bash
26
+ multica runtime list --output json
27
+ ```
28
+
29
+ Using the source workspace, fetch the resource to discover which `sourceRuntimeId` values are referenced (for agents and squads). For each distinct `sourceRuntimeId`, ask the user to select a matching target runtime by name or ID. Build a mapping in the form `srcId=dstId`. If any source runtime has no mapping, abort before running the sync.
30
+
31
+ For skills (which have no runtime dependency), an empty runtime map is acceptable.
32
+
33
+ ## Step 3 — Run the sync
34
+
35
+ ```bash
36
+ node "${CLAUDE_PLUGIN_ROOT}/scripts/multica-sync.mjs" \
37
+ <type> <name> from <src-ws> <dest-ws> \
38
+ [--runtime-map <srcId1=dstId1,srcId2=dstId2,...>]
39
+ ```
40
+
41
+ The script exports to a temporary directory, imports into the destination workspace, then cleans up the temporary files automatically.
42
+
43
+ ## Step 4 — Report results
44
+
45
+ Parse the JSON output and report:
46
+
47
+ - Created and updated counts for skills, agents, and squads.
48
+ - Name-to-ID maps (`skillIdMap`, `agentIdMap`).
49
+ - Squad ID if a squad was synced.
50
+ - 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>`."
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "plugin-validator",
3
3
  "displayName": "Plugin Validator",
4
- "version": "0.0.31",
4
+ "version": "0.0.33",
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.31",
4
+ "version": "0.0.33",
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.31",
3
+ "version": "0.0.33",
4
4
  "description": "Step-by-step wizard for generating technical diagrams as SVG+PNG.",
5
5
  "author": {
6
6
  "name": "steven"