@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.
- package/.claude-plugin/marketplace.json +5 -5
- package/package.json +1 -1
- package/plugins/html-effectiveness/.claude-plugin/plugin.json +1 -1
- package/plugins/multica-tool/.claude-plugin/plugin.json +1 -1
- package/plugins/multica-tool/scripts/lib.mjs +23 -0
- package/plugins/multica-tool/scripts/multica-export.mjs +77 -10
- package/plugins/multica-tool/scripts/multica-import.mjs +182 -25
- package/plugins/multica-tool/scripts/multica-sync.mjs +11 -3
- package/plugins/multica-tool/skills/export/SKILL.md +12 -7
- package/plugins/multica-tool/skills/import/SKILL.md +31 -5
- package/plugins/multica-tool/skills/sync/SKILL.md +4 -2
- package/plugins/plugin-validator/.claude-plugin/plugin.json +1 -1
- package/plugins/team-share/.claude-plugin/plugin.json +1 -1
- package/plugins/tech-graph/.claude-plugin/plugin.json +1 -1
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
76
|
+
"version": "0.0.41",
|
|
77
77
|
"category": "workflow",
|
|
78
78
|
"keywords": [
|
|
79
79
|
"multica",
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "html-effectiveness",
|
|
3
3
|
"displayName": "HTML Effectiveness Reports",
|
|
4
|
-
"version": "0.0.
|
|
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"
|
|
@@ -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) =>
|
|
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
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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 =
|
|
277
|
-
|
|
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 =
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
|
|
291
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 `
|
|
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 <
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 —
|
|
15
|
+
## Step 2 — Pre-flight (dry run)
|
|
16
16
|
|
|
17
|
-
|
|
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
|
|
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
|
|
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 `
|
|
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
|
|
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.
|
|
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.
|
|
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"
|