@drunkcoding/agents-and-skills 0.0.42 → 0.0.44
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 +29 -0
- package/plugins/multica-tool/scripts/multica-export.mjs +127 -33
- package/plugins/multica-tool/scripts/multica-import.mjs +163 -10
- package/plugins/multica-tool/scripts/multica-sync.mjs +8 -3
- package/plugins/multica-tool/skills/export/SKILL.md +8 -5
- package/plugins/multica-tool/skills/import/SKILL.md +16 -7
- package/plugins/multica-tool/skills/sync/SKILL.md +6 -3
- 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.44",
|
|
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.44",
|
|
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.44",
|
|
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.44",
|
|
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.44",
|
|
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.44",
|
|
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"
|
|
@@ -53,6 +53,9 @@ export function resolveWorkspaceId(cli, name) {
|
|
|
53
53
|
export const listRuntimes = (cli) => cli.json(["runtime", "list"]);
|
|
54
54
|
export const listSkills = (cli) => cli.json(["skill", "list"]);
|
|
55
55
|
export const listAgents = (cli) => cli.json(["agent", "list"]);
|
|
56
|
+
// Includes archived agents (archived_at set) — used wherever a name match or an
|
|
57
|
+
// id lookup must also see agents excluded from the default listing above.
|
|
58
|
+
export const listAgentsIncludingArchived = (cli) => cli.json(["agent", "list", "--include-archived"]);
|
|
56
59
|
export const listSquads = (cli) => cli.json(["squad", "list"]);
|
|
57
60
|
export const listWorkspaceMembers = (cli) => cli.json(["workspace", "member", "list"]);
|
|
58
61
|
|
|
@@ -72,6 +75,8 @@ export function getAgent(cli, id) {
|
|
|
72
75
|
const a = cli.json(["agent", "get", id]);
|
|
73
76
|
return {
|
|
74
77
|
id: a.id, name: a.name, description: a.description, instructions: a.instructions,
|
|
78
|
+
// Export-time signal only (never written to a bundle record — see redactAgent).
|
|
79
|
+
archived_at: a.archived_at ?? null,
|
|
75
80
|
model: a.model, visibility: a.visibility, avatar_url: a.avatar_url ?? null,
|
|
76
81
|
service_tier: a.service_tier ?? "",
|
|
77
82
|
permission_mode: a.permission_mode ?? null,
|
|
@@ -127,3 +132,27 @@ export const getProjectResources = (cli, id) =>
|
|
|
127
132
|
(cli.json(["project", "resource", "list", id]) ?? []).map((r) => ({
|
|
128
133
|
resource_type: r.resource_type, resource_ref: r.resource_ref, label: r.label ?? null,
|
|
129
134
|
}));
|
|
135
|
+
|
|
136
|
+
export const listAutopilots = (cli) => cli.json(["autopilot", "list"]).autopilots ?? [];
|
|
137
|
+
|
|
138
|
+
// `autopilot get` wraps the record as {autopilot, collaborators, triggers} — only
|
|
139
|
+
// autopilot + triggers are portable; collaborators/can_write/can_manage_access are
|
|
140
|
+
// caller-relative and never bundled. `priority` is accepted by `autopilot
|
|
141
|
+
// create`/`update` but never present in this response (verified live) — kept as a
|
|
142
|
+
// field anyway so this starts round-tripping automatically if the API adds it.
|
|
143
|
+
export function getAutopilot(cli, id) {
|
|
144
|
+
const r = cli.json(["autopilot", "get", id]);
|
|
145
|
+
const a = r.autopilot;
|
|
146
|
+
return {
|
|
147
|
+
id: a.id, title: a.title, description: a.description ?? "",
|
|
148
|
+
execution_mode: a.execution_mode, issue_title_template: a.issue_title_template ?? null,
|
|
149
|
+
priority: a.priority ?? null,
|
|
150
|
+
project_id: a.project_id ?? null,
|
|
151
|
+
assignee_id: a.assignee_id, assignee_type: a.assignee_type,
|
|
152
|
+
subscribers: (a.subscribers ?? []).map((s) => ({ user_id: s.user_id, user_type: s.user_type })),
|
|
153
|
+
triggers: (r.triggers ?? []).map((t) => ({
|
|
154
|
+
kind: t.kind, label: t.label ?? null, enabled: !!t.enabled,
|
|
155
|
+
cron_expression: t.cron_expression ?? null, timezone: t.timezone ?? null,
|
|
156
|
+
})),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
@@ -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, listProjects, getProject, getProjectResources, makeCli, realExec, requireAuth, resolveWorkspaceId } from "./lib.mjs";
|
|
4
|
+
import { slugify, getSkill, getAgent, getAgentCustomEnv, getSquad, getSquadMembers, listRuntimes, listSkills, listAgents, listAgentsIncludingArchived, listSquads, listProjects, getProject, getProjectResources, listWorkspaceMembers, getAutopilot, 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, instructions, ...rest } = a;
|
|
34
|
+
const { id, has_custom_env, mcp_config_redacted, custom_env_fetch_failed, mcp_config, custom_env, skills, runtime_id, instructions, archived_at, ...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
|
|
@@ -57,7 +57,7 @@ export function redactAgent(a) {
|
|
|
57
57
|
};
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
export function buildManifest({ scope, sourceWorkspaceId, skills, agents, squads, projects }) {
|
|
60
|
+
export function buildManifest({ scope, sourceWorkspaceId, skills, agents, squads, projects, autopilots }) {
|
|
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();
|
|
@@ -81,6 +81,11 @@ export function buildManifest({ scope, sourceWorkspaceId, skills, agents, squads
|
|
|
81
81
|
title: p.title, file: `projects/${slugify(p.title)}.json`,
|
|
82
82
|
source_id: p.source_id, lead_name: p.lead_name ?? null, lead_type: p.lead_type ?? null,
|
|
83
83
|
})),
|
|
84
|
+
autopilots: (autopilots ?? []).map((ap) => ({
|
|
85
|
+
title: ap.title, file: `autopilots/${slugify(ap.title)}.json`, source_id: ap.source_id,
|
|
86
|
+
assignee_type: ap.assignee_type, assignee_name: ap.assignee_name,
|
|
87
|
+
project_title: ap.project_title, had_webhook_trigger: ap.had_webhook_trigger,
|
|
88
|
+
})),
|
|
84
89
|
};
|
|
85
90
|
}
|
|
86
91
|
|
|
@@ -91,10 +96,15 @@ function collectSkill(cli, id, skills) {
|
|
|
91
96
|
}
|
|
92
97
|
|
|
93
98
|
// Keyed by agent id (so squad leader_id/member_id resolve to names). Stores the
|
|
94
|
-
// normalized agent, its redaction result, and its skill names.
|
|
99
|
+
// normalized agent, its redaction result, and its skill names. Returns
|
|
100
|
+
// `{ archived: true, name }` instead of collecting when the agent is archived —
|
|
101
|
+
// callers reached via a squad/project/explicit id lookup must decide how to
|
|
102
|
+
// handle that (the workspace-listing loop never hits this: listAgents() already
|
|
103
|
+
// excludes archived agents server-side).
|
|
95
104
|
function collectAgent(cli, id, agentsById, skills, providerById) {
|
|
96
105
|
if (agentsById.has(id)) return agentsById.get(id);
|
|
97
106
|
const a = getAgent(cli, id);
|
|
107
|
+
if (a.archived_at) return { archived: true, name: a.name };
|
|
98
108
|
a.source_runtime_provider = providerById.get(a.runtime_id) ?? null;
|
|
99
109
|
a.custom_env = {};
|
|
100
110
|
a.custom_env_fetch_failed = false;
|
|
@@ -112,38 +122,45 @@ function collectAgent(cli, id, agentsById, skills, providerById) {
|
|
|
112
122
|
return entry;
|
|
113
123
|
}
|
|
114
124
|
|
|
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
|
-
|
|
131
125
|
export function exportResource({ cli, scope, ids, outDir, sourceWorkspaceId, fs = nodeFs, download = fetchBinary }) {
|
|
132
126
|
const skills = new Map(); // name -> normalized skill
|
|
133
127
|
const agentsById = new Map(); // id -> { raw, red, skill_names }
|
|
134
128
|
const squads = [];
|
|
135
129
|
const projects = [];
|
|
130
|
+
const autopilots = [];
|
|
136
131
|
// Lazy + memoized: only fetched when an agent is actually collected (skips
|
|
137
132
|
// the extra CLI call on skill-only exports).
|
|
138
133
|
let providerById = null;
|
|
139
134
|
const getProviderById = () => providerById ??= new Map(listRuntimes(cli).map((r) => [r.id, r.provider]));
|
|
140
135
|
|
|
141
|
-
//
|
|
136
|
+
// Archived agents skipped anywhere below, deduped by (name, path, detail) —
|
|
137
|
+
// the operator-facing "why isn't X in my bundle" report.
|
|
138
|
+
const archivedAgentsSkipped = [];
|
|
139
|
+
const seenArchivedSkip = new Set();
|
|
140
|
+
function recordArchivedSkip(name, path, detail) {
|
|
141
|
+
const key = [name, path, detail ?? ""].join("|");
|
|
142
|
+
if (seenArchivedSkip.has(key)) return;
|
|
143
|
+
seenArchivedSkip.add(key);
|
|
144
|
+
archivedAgentsSkipped.push(detail ? { name, path, detail } : { name, path });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Collect a squad's agents (leader + members) and return the squad bundle
|
|
148
|
+
// object — or null when the leader is archived, since a squad can't import
|
|
149
|
+
// without one (the whole squad is excluded from the bundle, not emitted leaderless).
|
|
142
150
|
function collectOneSquad(squadId) {
|
|
143
151
|
const sq = getSquad(cli, squadId);
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
152
|
+
const leaderEntry = collectAgent(cli, sq.leader_id, agentsById, skills, getProviderById());
|
|
153
|
+
if (leaderEntry.archived) {
|
|
154
|
+
recordArchivedSkip(leaderEntry.name, "squad leader", sq.name);
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
const allMembers = getSquadMembers(cli, squadId).filter((m) => m.member_type === "agent");
|
|
158
|
+
const members = [];
|
|
159
|
+
for (const m of allMembers) {
|
|
160
|
+
const entry = collectAgent(cli, m.member_id, agentsById, skills, getProviderById());
|
|
161
|
+
if (entry.archived) { recordArchivedSkip(entry.name, "squad member", sq.name); continue; }
|
|
162
|
+
members.push(m);
|
|
163
|
+
}
|
|
147
164
|
const nameOf = (id) => agentsById.get(id)?.raw.name;
|
|
148
165
|
return {
|
|
149
166
|
name: sq.name, description: sq.description, instructions: sq.instructions, avatar_url: sq.avatar_url,
|
|
@@ -152,16 +169,83 @@ export function exportResource({ cli, scope, ids, outDir, sourceWorkspaceId, fs
|
|
|
152
169
|
};
|
|
153
170
|
}
|
|
154
171
|
|
|
172
|
+
// Collect a project's portable metadata + its lead agent (bundled, like a
|
|
173
|
+
// squad leader) and github_repo/other resources. An archived lead is dropped
|
|
174
|
+
// (project still exports, just with no lead set) — unlike a squad, a project
|
|
175
|
+
// has no hard requirement for one.
|
|
176
|
+
function collectProject(id) {
|
|
177
|
+
const p = getProject(cli, id);
|
|
178
|
+
let lead_name = null, lead_type = p.lead_type, lead_source_id = p.lead_id;
|
|
179
|
+
if (p.lead_type === "agent" && p.lead_id) {
|
|
180
|
+
const entry = collectAgent(cli, p.lead_id, agentsById, skills, getProviderById());
|
|
181
|
+
if (entry.archived) {
|
|
182
|
+
recordArchivedSkip(entry.name, "project lead", p.title);
|
|
183
|
+
lead_type = null; lead_source_id = null;
|
|
184
|
+
} else {
|
|
185
|
+
lead_name = entry.raw.name;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
title: p.title, description: p.description, icon: p.icon,
|
|
190
|
+
priority: p.priority, status: p.status, due_date: p.due_date, start_date: p.start_date,
|
|
191
|
+
source_id: id, lead_type, lead_name, lead_source_id,
|
|
192
|
+
resources: getProjectResources(cli, id),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Collect a single autopilot: its assignee (agent, or squad + members + skills)
|
|
197
|
+
// and, when present, the destination-portable project title / subscriber names.
|
|
198
|
+
// Webhook trigger secrets (url/token) are never read into the bundle — only
|
|
199
|
+
// kind/label/enabled, matching the existing agent MCP/env redaction pattern.
|
|
200
|
+
function collectOneAutopilot(autopilotId) {
|
|
201
|
+
const ap = getAutopilot(cli, autopilotId);
|
|
202
|
+
let assignee_name = null;
|
|
203
|
+
if (ap.assignee_type === "agent") {
|
|
204
|
+
const entry = collectAgent(cli, ap.assignee_id, agentsById, skills, getProviderById());
|
|
205
|
+
assignee_name = entry.archived ? null : entry.raw.name;
|
|
206
|
+
} else if (ap.assignee_type === "squad") {
|
|
207
|
+
const sq = collectOneSquad(ap.assignee_id);
|
|
208
|
+
if (sq) { squads.push(sq); assignee_name = sq.name; }
|
|
209
|
+
}
|
|
210
|
+
let project_title = null;
|
|
211
|
+
if (ap.project_id) {
|
|
212
|
+
try { project_title = getProject(cli, ap.project_id).title; } catch { project_title = null; }
|
|
213
|
+
}
|
|
214
|
+
let subscriber_names = [];
|
|
215
|
+
if (ap.subscribers.length) {
|
|
216
|
+
const members = listWorkspaceMembers(cli);
|
|
217
|
+
subscriber_names = ap.subscribers.map((s) => members.find((m) => m.user_id === s.user_id)?.name).filter(Boolean);
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
title: ap.title, source_id: ap.id, description: ap.description,
|
|
221
|
+
execution_mode: ap.execution_mode, issue_title_template: ap.issue_title_template,
|
|
222
|
+
priority: ap.priority, project_title, assignee_type: ap.assignee_type, assignee_name,
|
|
223
|
+
subscriber_names, had_webhook_trigger: ap.triggers.some((t) => t.kind === "webhook"),
|
|
224
|
+
triggers: ap.triggers.map((t) => t.kind === "webhook"
|
|
225
|
+
? { kind: "webhook", label: t.label, enabled: t.enabled }
|
|
226
|
+
: { kind: t.kind, label: t.label, enabled: t.enabled, cron_expression: t.cron_expression, timezone: t.timezone }),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
155
230
|
if (scope === "skill") collectSkill(cli, ids.skillId, skills);
|
|
156
|
-
else if (scope === "agent")
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
231
|
+
else if (scope === "agent") {
|
|
232
|
+
const entry = collectAgent(cli, ids.agentId, agentsById, skills, getProviderById());
|
|
233
|
+
if (entry.archived) throw new Error(`Cannot export agent "${entry.name}": it is archived`);
|
|
234
|
+
}
|
|
235
|
+
else if (scope === "squad") { const sq = collectOneSquad(ids.squadId); if (sq) squads.push(sq); }
|
|
236
|
+
else if (scope === "project") projects.push(collectProject(ids.projectId));
|
|
237
|
+
else if (scope === "projects") for (const p of listProjects(cli)) projects.push(collectProject(p.id));
|
|
238
|
+
else if (scope === "autopilot") autopilots.push(collectOneAutopilot(ids.autopilotId));
|
|
160
239
|
else if (scope === "all") {
|
|
161
240
|
for (const s of listSkills(cli)) collectSkill(cli, s.id, skills);
|
|
162
241
|
for (const a of listAgents(cli)) collectAgent(cli, a.id, agentsById, skills, getProviderById());
|
|
163
|
-
for (const sq of listSquads(cli))
|
|
164
|
-
for (const p of listProjects(cli)) projects.push(collectProject(
|
|
242
|
+
for (const sq of listSquads(cli)) { const built = collectOneSquad(sq.id); if (built) squads.push(built); }
|
|
243
|
+
for (const p of listProjects(cli)) projects.push(collectProject(p.id));
|
|
244
|
+
// listAgents() above already excludes archived agents server-side — surface
|
|
245
|
+
// that exclusion in the report too, not just the squad/project paths.
|
|
246
|
+
for (const a of listAgentsIncludingArchived(cli)) {
|
|
247
|
+
if (a.archived_at) recordArchivedSkip(a.name, "workspace listing");
|
|
248
|
+
}
|
|
165
249
|
}
|
|
166
250
|
|
|
167
251
|
// Orphan-skill cleanup: drop skills that no exported agent references via its
|
|
@@ -183,6 +267,7 @@ export function exportResource({ cli, scope, ids, outDir, sourceWorkspaceId, fs
|
|
|
183
267
|
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 })),
|
|
184
268
|
squads,
|
|
185
269
|
projects,
|
|
270
|
+
autopilots,
|
|
186
271
|
});
|
|
187
272
|
|
|
188
273
|
const warnings = [];
|
|
@@ -241,8 +326,16 @@ export function exportResource({ cli, scope, ids, outDir, sourceWorkspaceId, fs
|
|
|
241
326
|
fs.mkdirSync(`${outDir}/projects`, { recursive: true });
|
|
242
327
|
fs.writeFileSync(`${outDir}/${entry.file}`, JSON.stringify(projectByTitle.get(entry.title), null, 2));
|
|
243
328
|
}
|
|
329
|
+
const autopilotByTitle = new Map(autopilots.map((a) => [a.title, a]));
|
|
330
|
+
for (const entry of manifest.autopilots) {
|
|
331
|
+
fs.mkdirSync(`${outDir}/autopilots`, { recursive: true });
|
|
332
|
+
fs.writeFileSync(`${outDir}/${entry.file}`, JSON.stringify(autopilotByTitle.get(entry.title), null, 2));
|
|
333
|
+
}
|
|
244
334
|
fs.writeFileSync(`${outDir}/manifest.json`, JSON.stringify(manifest, null, 2));
|
|
245
|
-
return {
|
|
335
|
+
return {
|
|
336
|
+
manifest, warnings, pruned_skills, archivedAgentsSkipped,
|
|
337
|
+
autopilotWebhookTriggers: autopilots.filter((a) => a.had_webhook_trigger).map((a) => a.title),
|
|
338
|
+
};
|
|
246
339
|
}
|
|
247
340
|
|
|
248
341
|
function main() {
|
|
@@ -255,7 +348,7 @@ function main() {
|
|
|
255
348
|
const workspace = get("--workspace"); // optional: source workspace name
|
|
256
349
|
|
|
257
350
|
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)");
|
|
351
|
+
console.error("Usage: multica-export.mjs --scope <skill|agent|squad|project|projects|autopilot|all> --id <id> --out <dir> [--workspace <name>] (--id not needed for --scope all|projects)");
|
|
259
352
|
process.exit(1);
|
|
260
353
|
}
|
|
261
354
|
|
|
@@ -272,8 +365,9 @@ function main() {
|
|
|
272
365
|
else if (scope === "squad") ids.squadId = id;
|
|
273
366
|
else if (scope === "project") ids.projectId = id;
|
|
274
367
|
else if (scope === "projects") { /* all projects — no id */ }
|
|
368
|
+
else if (scope === "autopilot") ids.autopilotId = id;
|
|
275
369
|
else if (scope === "all") { /* whole workspace — no id */ }
|
|
276
|
-
else { console.error(`Unknown scope "${scope}" — use skill|agent|squad|project|projects|all`); process.exit(1); }
|
|
370
|
+
else { console.error(`Unknown scope "${scope}" — use skill|agent|squad|project|projects|autopilot|all`); process.exit(1); }
|
|
277
371
|
|
|
278
372
|
const result = exportResource({ cli, scope, ids, outDir: out, sourceWorkspaceId, fs: nodeFs });
|
|
279
373
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as nodeFs from "node:fs";
|
|
2
|
-
import { listSkills, listAgents, listSquads, listRuntimes, listWorkspaceMembers, getSquadMembers, findByName, makeCli, realExec, requireAuth, resolveWorkspaceId, listProjects, getProjectResources, findByTitle } from "./lib.mjs";
|
|
2
|
+
import { listSkills, listAgents, listAgentsIncludingArchived, listSquads, listRuntimes, listWorkspaceMembers, getSquadMembers, findByName, makeCli, realExec, requireAuth, resolveWorkspaceId, listProjects, getProjectResources, findByTitle, listAutopilots, getAutopilot } from "./lib.mjs";
|
|
3
3
|
|
|
4
|
-
// User-facing selectable types are agents/squads/projects; skills follow agents.
|
|
4
|
+
// User-facing selectable types are agents/squads/projects/autopilots; skills follow agents.
|
|
5
5
|
export function parseInclude(raw) {
|
|
6
6
|
const set = new Set((raw ? raw.split(",") : ["agents", "squads"]).map((s) => s.trim()).filter(Boolean));
|
|
7
7
|
if (set.has("agents")) set.add("skills");
|
|
@@ -86,8 +86,10 @@ export function importAgents({ cli, manifest, dir, skillIdMap, runtimeMap, fs =
|
|
|
86
86
|
// Lazy + memoized: destination member user_ids, only listed when an agent needs them.
|
|
87
87
|
let destMemberIds = null;
|
|
88
88
|
const getDestMemberIds = () => destMemberIds ??= new Set(listWorkspaceMembers(cli).map((m) => m.user_id));
|
|
89
|
-
let created = 0, updated = 0;
|
|
90
|
-
|
|
89
|
+
let created = 0, updated = 0, reused = 0;
|
|
90
|
+
// Includes archived agents so a bundle name matching a retired agent restores
|
|
91
|
+
// and reuses it instead of failing to create under the held name.
|
|
92
|
+
const existing = listAgentsIncludingArchived(cli);
|
|
91
93
|
|
|
92
94
|
for (const a of manifest.agents) {
|
|
93
95
|
const rec = JSON.parse(fs.readFileSync(`${dir}/${a.file}`, "utf8"));
|
|
@@ -108,7 +110,14 @@ export function importAgents({ cli, manifest, dir, skillIdMap, runtimeMap, fs =
|
|
|
108
110
|
if (rec.service_tier) common.push("--service-tier", rec.service_tier);
|
|
109
111
|
const match = findByName(existing, rec.name);
|
|
110
112
|
let id;
|
|
111
|
-
if (match) {
|
|
113
|
+
if (match && match.archived_at) {
|
|
114
|
+
// Restored agent then updated exactly like an active-name match — the
|
|
115
|
+
// matched id is now active, so a re-import of this bundle will find it
|
|
116
|
+
// via the plain `match` branch below and never restore twice.
|
|
117
|
+
cli.run(["agent", "restore", match.id]);
|
|
118
|
+
cli.run(["agent", "update", match.id, "--runtime-id", targetRuntime, ...common]);
|
|
119
|
+
id = match.id; reused++;
|
|
120
|
+
} else if (match) {
|
|
112
121
|
cli.run(["agent", "update", match.id, "--runtime-id", targetRuntime, ...common]);
|
|
113
122
|
id = match.id; updated++;
|
|
114
123
|
} else {
|
|
@@ -178,7 +187,7 @@ export function importAgents({ cli, manifest, dir, skillIdMap, runtimeMap, fs =
|
|
|
178
187
|
}
|
|
179
188
|
}
|
|
180
189
|
}
|
|
181
|
-
return { idMap, sourceIdMap, created, updated, secretsApplyFailures, avatarApplyFailures, avatarUnsupported, permissionApplyFailures, permissionUnsupported };
|
|
190
|
+
return { idMap, sourceIdMap, created, updated, reused, secretsApplyFailures, avatarApplyFailures, avatarUnsupported, permissionApplyFailures, permissionUnsupported };
|
|
182
191
|
}
|
|
183
192
|
|
|
184
193
|
// Rewrites `mention://agent/<id>` links (e.g. `[@dev-backend](mention://agent/<id>)`)
|
|
@@ -298,6 +307,99 @@ export function importProjects({ cli, manifest, dir, agentIdMap, fs = nodeFs })
|
|
|
298
307
|
return { idMap, created, updated, priorityUnsupported, resourcesUnsupported, leadUnresolved };
|
|
299
308
|
}
|
|
300
309
|
|
|
310
|
+
// `autopilot create`/`update` only accept `--agent` (name or ID) — there is no
|
|
311
|
+
// `--squad` flag, even though the data model supports squad-assigned autopilots
|
|
312
|
+
// (dispatch resolves to the squad leader). A squad assignee, or an agent name not
|
|
313
|
+
// resolvable in this import or the destination, is reported up front so the whole
|
|
314
|
+
// import aborts before any write — mirroring the unmapped-runtime guard above,
|
|
315
|
+
// never silently dropped or reassigned to the wrong agent.
|
|
316
|
+
function unresolvedAutopilotAssignees({ cli, manifest, recs, agentIdMap }) {
|
|
317
|
+
let destAgentNames = null;
|
|
318
|
+
const agentResolvable = (name) => agentIdMap.has(name) || (destAgentNames ??= new Set(listAgents(cli).map((a) => a.name))).has(name);
|
|
319
|
+
const unresolved = [];
|
|
320
|
+
for (const rec of recs) {
|
|
321
|
+
if (rec.assignee_type === "squad") {
|
|
322
|
+
unresolved.push(`"${rec.title}" → squad "${rec.assignee_name}" (the multica CLI has no command to assign a squad to an autopilot)`);
|
|
323
|
+
} else if (!agentResolvable(rec.assignee_name)) {
|
|
324
|
+
unresolved.push(`"${rec.title}" → agent "${rec.assignee_name}" (not found in this import or the destination workspace)`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return unresolved;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function importAutopilots({ cli, manifest, dir, agentIdMap, fs = nodeFs }) {
|
|
331
|
+
const entries = manifest.autopilots ?? [];
|
|
332
|
+
const recs = entries.map((entry) => JSON.parse(fs.readFileSync(`${dir}/${entry.file}`, "utf8")));
|
|
333
|
+
|
|
334
|
+
const unresolved = unresolvedAutopilotAssignees({ cli, manifest, recs, agentIdMap });
|
|
335
|
+
if (unresolved.length) {
|
|
336
|
+
throw new Error(`Unresolved autopilot assignees: ${unresolved.join(", ")} — aborting before any write`);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const idMap = new Map();
|
|
340
|
+
let created = 0, updated = 0;
|
|
341
|
+
const projectUnresolved = [], subscribersUnresolved = [], priorityNotCaptured = [], webhookReissued = [];
|
|
342
|
+
const existing = listAutopilots(cli);
|
|
343
|
+
let destProjects = null;
|
|
344
|
+
const resolveProjectId = (title) => (destProjects ??= listProjects(cli)).find((p) => p.title === title)?.id ?? null;
|
|
345
|
+
let destMembers = null;
|
|
346
|
+
const memberExists = (name) => (destMembers ??= listWorkspaceMembers(cli)).some((m) => m.name === name);
|
|
347
|
+
|
|
348
|
+
for (const rec of recs) {
|
|
349
|
+
const common = [];
|
|
350
|
+
if (rec.description) common.push("--description", rec.description);
|
|
351
|
+
if (rec.issue_title_template) common.push("--issue-title-template", rec.issue_title_template);
|
|
352
|
+
// priority is never present today — the multica CLI/API accepts it on write but
|
|
353
|
+
// never returns it on read, so export can't capture the source's real value.
|
|
354
|
+
if (rec.priority && rec.priority !== "none") common.push("--priority", rec.priority);
|
|
355
|
+
else priorityNotCaptured.push(rec.title);
|
|
356
|
+
|
|
357
|
+
if (rec.project_title) {
|
|
358
|
+
const projectId = resolveProjectId(rec.project_title);
|
|
359
|
+
if (projectId) common.push("--project", projectId);
|
|
360
|
+
else projectUnresolved.push(rec.title);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const wantedSubscribers = rec.subscriber_names ?? [];
|
|
364
|
+
const resolvableSubscribers = wantedSubscribers.filter(memberExists);
|
|
365
|
+
subscribersUnresolved.push(...wantedSubscribers.filter((n) => !memberExists(n)).map((n) => `${rec.title}:${n}`));
|
|
366
|
+
if (resolvableSubscribers.length) common.push(...resolvableSubscribers.flatMap((n) => ["--subscriber", n]));
|
|
367
|
+
|
|
368
|
+
const match = findByTitle(existing, rec.title);
|
|
369
|
+
let id;
|
|
370
|
+
if (match) {
|
|
371
|
+
cli.run(["autopilot", "update", match.id, "--agent", rec.assignee_name, "--mode", rec.execution_mode, ...common]);
|
|
372
|
+
id = match.id; updated++;
|
|
373
|
+
} else {
|
|
374
|
+
const out = cli.run(["autopilot", "create", "--title", rec.title, "--agent", rec.assignee_name, "--mode", rec.execution_mode, ...common]);
|
|
375
|
+
id = JSON.parse(out).id;
|
|
376
|
+
// Always arrives paused, regardless of source status — activation is a
|
|
377
|
+
// deliberate follow-up action, never automatic on import.
|
|
378
|
+
cli.run(["autopilot", "update", id, "--status", "paused"]);
|
|
379
|
+
created++;
|
|
380
|
+
}
|
|
381
|
+
idMap.set(rec.title, id);
|
|
382
|
+
|
|
383
|
+
// Triggers are additive and idempotent by kind+label — a re-import never
|
|
384
|
+
// duplicates a trigger already present on the matched autopilot. Webhook
|
|
385
|
+
// triggers are always reissued fresh (new secret), never copied from the bundle.
|
|
386
|
+
const existingTriggers = match ? getAutopilot(cli, id).triggers : [];
|
|
387
|
+
const present = new Set(existingTriggers.map((t) => `${t.kind}:${t.label ?? ""}`));
|
|
388
|
+
for (const t of rec.triggers ?? []) {
|
|
389
|
+
if (present.has(`${t.kind}:${t.label ?? ""}`)) continue;
|
|
390
|
+
const flags = ["--kind", t.kind];
|
|
391
|
+
if (t.kind === "schedule") {
|
|
392
|
+
flags.push("--cron", t.cron_expression);
|
|
393
|
+
if (t.timezone) flags.push("--timezone", t.timezone);
|
|
394
|
+
}
|
|
395
|
+
if (t.label) flags.push("--label", t.label);
|
|
396
|
+
cli.run(["autopilot", "trigger-add", id, ...flags]);
|
|
397
|
+
if (t.kind === "webhook") webhookReissued.push(rec.title);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return { idMap, created, updated, projectUnresolved, subscribersUnresolved, priorityNotCaptured, webhookReissued };
|
|
401
|
+
}
|
|
402
|
+
|
|
301
403
|
export function collectSourceRuntimes(manifest) {
|
|
302
404
|
return [...new Set((manifest.agents ?? []).map((a) => a.source_runtime_id).filter(Boolean))];
|
|
303
405
|
}
|
|
@@ -355,7 +457,7 @@ export function importBundle({ cli, dir, runtimeMap, include, fs = nodeFs }) {
|
|
|
355
457
|
: { idMap: new Map(), created: 0, updated: 0 };
|
|
356
458
|
const agentRes = inc.has("agents")
|
|
357
459
|
? 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: [] };
|
|
460
|
+
: { idMap: new Map(), sourceIdMap: new Map(), created: 0, updated: 0, reused: 0, secretsApplyFailures: [], avatarApplyFailures: [], avatarUnsupported: [], permissionApplyFailures: [], permissionUnsupported: [] };
|
|
359
461
|
// Runs after every agent exists so forward-referencing mentions resolve.
|
|
360
462
|
const mentionRes = inc.has("agents")
|
|
361
463
|
? rewriteAgentMentions({ cli, manifest, dir, agentIdMap: agentRes.idMap, sourceIdMap: agentRes.sourceIdMap, fs })
|
|
@@ -380,15 +482,24 @@ export function importBundle({ cli, dir, runtimeMap, include, fs = nodeFs }) {
|
|
|
380
482
|
projectRes = importProjects({ cli, manifest, dir, agentIdMap: agentRes.idMap, fs });
|
|
381
483
|
}
|
|
382
484
|
|
|
485
|
+
let autopilotRes = { idMap: new Map(), created: 0, updated: 0, projectUnresolved: [], subscribersUnresolved: [], priorityNotCaptured: [], webhookReissued: [] };
|
|
486
|
+
if (inc.has("autopilots")) {
|
|
487
|
+
autopilotRes = importAutopilots({ cli, manifest, dir, agentIdMap: agentRes.idMap, fs });
|
|
488
|
+
}
|
|
489
|
+
|
|
383
490
|
return {
|
|
384
491
|
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 },
|
|
492
|
+
created: { skills: skillRes.created, agents: agentRes.created, squads: squadsCreated, projects: projectRes.created, autopilots: autopilotRes.created },
|
|
493
|
+
updated: { skills: skillRes.updated, agents: agentRes.updated, squads: squadsUpdated, projects: projectRes.updated, autopilots: autopilotRes.updated },
|
|
494
|
+
// Agents restored from archived + updated to the bundle's definition — distinct
|
|
495
|
+
// from a plain update (an active-name match). Only agents can be reused today.
|
|
496
|
+
reused: { agents: agentRes.reused },
|
|
387
497
|
mentionsRewritten: mentionRes.updated,
|
|
388
498
|
skillIdMap: Object.fromEntries(skillRes.idMap),
|
|
389
499
|
agentIdMap: Object.fromEntries(agentRes.idMap),
|
|
390
500
|
squadIdMap: Object.fromEntries(squadIdMap),
|
|
391
501
|
projectIdMap: Object.fromEntries(projectRes.idMap),
|
|
502
|
+
autopilotIdMap: Object.fromEntries(autopilotRes.idMap),
|
|
392
503
|
secretsReminder: (manifest.agents ?? []).filter((a) => a.had_secrets).map((a) => a.name),
|
|
393
504
|
secretsApplyFailures: agentRes.secretsApplyFailures,
|
|
394
505
|
avatarApplyFailures: agentRes.avatarApplyFailures,
|
|
@@ -399,6 +510,10 @@ export function importBundle({ cli, dir, runtimeMap, include, fs = nodeFs }) {
|
|
|
399
510
|
priorityUnsupported: projectRes.priorityUnsupported,
|
|
400
511
|
resourcesUnsupported: projectRes.resourcesUnsupported,
|
|
401
512
|
leadUnresolved: projectRes.leadUnresolved,
|
|
513
|
+
autopilotProjectUnresolved: autopilotRes.projectUnresolved,
|
|
514
|
+
autopilotSubscribersUnresolved: autopilotRes.subscribersUnresolved,
|
|
515
|
+
autopilotPriorityNotCaptured: autopilotRes.priorityNotCaptured,
|
|
516
|
+
autopilotWebhookReissued: autopilotRes.webhookReissued,
|
|
402
517
|
};
|
|
403
518
|
}
|
|
404
519
|
|
|
@@ -406,12 +521,13 @@ export function preflight({ cli, dir, runtimeMap, include, fs = nodeFs }) {
|
|
|
406
521
|
const inc = include ?? new Set(["skills", "agents", "squads"]);
|
|
407
522
|
const manifest = JSON.parse(fs.readFileSync(`${dir}/manifest.json`, "utf8"));
|
|
408
523
|
const count = (k) => (manifest[k] ?? []).length;
|
|
409
|
-
const bundle = { skills: count("skills"), agents: count("agents"), squads: count("squads"), projects: count("projects") };
|
|
524
|
+
const bundle = { skills: count("skills"), agents: count("agents"), squads: count("squads"), projects: count("projects"), autopilots: count("autopilots") };
|
|
410
525
|
const willImport = {
|
|
411
526
|
skills: inc.has("skills") ? bundle.skills : 0,
|
|
412
527
|
agents: inc.has("agents") ? bundle.agents : 0,
|
|
413
528
|
squads: inc.has("squads") ? bundle.squads : 0,
|
|
414
529
|
projects: inc.has("projects") ? bundle.projects : 0,
|
|
530
|
+
autopilots: inc.has("autopilots") ? bundle.autopilots : 0,
|
|
415
531
|
};
|
|
416
532
|
|
|
417
533
|
const incompatibilities = [];
|
|
@@ -427,6 +543,16 @@ export function preflight({ cli, dir, runtimeMap, include, fs = nodeFs }) {
|
|
|
427
543
|
? `${u.srcId} (provider "${u.provider}": ${u.matchCount} matches, expected 1)`
|
|
428
544
|
: `${u.srcId} (no provider recorded)` });
|
|
429
545
|
}
|
|
546
|
+
|
|
547
|
+
// Read-only check, mirrors importAgents' restore-and-reuse match — never
|
|
548
|
+
// calls restore itself, just reports what a real import would do.
|
|
549
|
+
const existingAgents = listAgentsIncludingArchived(cli);
|
|
550
|
+
for (const a of manifest.agents ?? []) {
|
|
551
|
+
const match = findByName(existingAgents, a.name);
|
|
552
|
+
if (match && match.archived_at) {
|
|
553
|
+
incompatibilities.push({ type: "agent-archived-will-restore", detail: `${a.name} (archived in destination — import would restore and reuse it)` });
|
|
554
|
+
}
|
|
555
|
+
}
|
|
430
556
|
}
|
|
431
557
|
|
|
432
558
|
if (inc.has("projects")) {
|
|
@@ -448,6 +574,33 @@ export function preflight({ cli, dir, runtimeMap, include, fs = nodeFs }) {
|
|
|
448
574
|
}
|
|
449
575
|
}
|
|
450
576
|
|
|
577
|
+
if (inc.has("autopilots")) {
|
|
578
|
+
const bundleAgentNames = new Set((manifest.agents ?? []).map((a) => a.name));
|
|
579
|
+
let destAgentNames = null;
|
|
580
|
+
const agentAvailable = (name) => (inc.has("agents") && bundleAgentNames.has(name))
|
|
581
|
+
|| (destAgentNames ??= new Set(listAgents(cli).map((a) => a.name))).has(name);
|
|
582
|
+
let destProjectTitles = null;
|
|
583
|
+
const projectAvailable = (title) => (destProjectTitles ??= new Set(listProjects(cli).map((p) => p.title))).has(title);
|
|
584
|
+
|
|
585
|
+
for (const entry of manifest.autopilots ?? []) {
|
|
586
|
+
const rec = JSON.parse(fs.readFileSync(`${dir}/${entry.file}`, "utf8"));
|
|
587
|
+
if (rec.assignee_type === "squad") {
|
|
588
|
+
incompatibilities.push({ type: "autopilot-squad-assignee-unsupported", detail: `${rec.title} → squad "${rec.assignee_name}" (the multica CLI has no command to assign a squad to an autopilot)` });
|
|
589
|
+
} else if (!agentAvailable(rec.assignee_name)) {
|
|
590
|
+
incompatibilities.push({ type: "autopilot-assignee-missing", detail: `${rec.title} → agent "${rec.assignee_name}" (ensure this agent exists in the destination or is included in this import)` });
|
|
591
|
+
}
|
|
592
|
+
if (!rec.priority) {
|
|
593
|
+
incompatibilities.push({ type: "autopilot-priority-not-captured", detail: `${rec.title} (the multica CLI/API never returns an autopilot's priority, so export could not capture it)` });
|
|
594
|
+
}
|
|
595
|
+
if (rec.project_title && !projectAvailable(rec.project_title)) {
|
|
596
|
+
incompatibilities.push({ type: "autopilot-project-missing", detail: `${rec.title} → project "${rec.project_title}" (not found in the destination)` });
|
|
597
|
+
}
|
|
598
|
+
if (rec.had_webhook_trigger) {
|
|
599
|
+
incompatibilities.push({ type: "autopilot-webhook-reissued", detail: `${rec.title} (webhook trigger will get a newly issued URL — the old one never travels)` });
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
451
604
|
const secretsReminder = (manifest.agents ?? []).filter((a) => a.had_secrets).map((a) => a.name);
|
|
452
605
|
return { bundle, willImport, runtimes, incompatibilities, secretsReminder };
|
|
453
606
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
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, findByTitle, listSkills, listAgents, listSquads, listProjects, realExec, requireAuth } from "./lib.mjs";
|
|
4
|
+
import { makeCli, resolveWorkspaceId, findByName, findByTitle, listSkills, listAgents, listSquads, listProjects, listAutopilots, realExec, requireAuth } from "./lib.mjs";
|
|
5
5
|
import { exportResource } from "./multica-export.mjs";
|
|
6
6
|
import { importBundle } from "./multica-import.mjs";
|
|
7
7
|
|
|
@@ -11,8 +11,13 @@ export function resolveScopeId(cli, type, name) {
|
|
|
11
11
|
if (!match) throw new Error(`Unknown project "${name}" in source workspace`);
|
|
12
12
|
return { scope: "project", ids: { projectId: match.id } };
|
|
13
13
|
}
|
|
14
|
+
if (type === "autopilot") {
|
|
15
|
+
const match = findByTitle(listAutopilots(cli), name);
|
|
16
|
+
if (!match) throw new Error(`Unknown autopilot "${name}" in source workspace`);
|
|
17
|
+
return { scope: "autopilot", ids: { autopilotId: match.id } };
|
|
18
|
+
}
|
|
14
19
|
const lists = { skill: listSkills, agent: listAgents, squad: listSquads };
|
|
15
|
-
if (!lists[type]) throw new Error(`Unknown type "${type}" (skill|agent|squad|project)`);
|
|
20
|
+
if (!lists[type]) throw new Error(`Unknown type "${type}" (skill|agent|squad|project|autopilot)`);
|
|
16
21
|
const match = findByName(lists[type](cli), name);
|
|
17
22
|
if (!match) throw new Error(`Unknown ${type} "${name}" in source workspace`);
|
|
18
23
|
const key = { skill: "skillId", agent: "agentId", squad: "squadId" }[type];
|
|
@@ -28,7 +33,7 @@ export function sync({ exec, type, name, srcWsName, destWsName, tmpDir, runtimeM
|
|
|
28
33
|
|
|
29
34
|
const { scope, ids } = resolveScopeId(srcCli, type, name);
|
|
30
35
|
exportResource({ cli: srcCli, scope, ids, outDir: tmpDir, sourceWorkspaceId: srcId, fs });
|
|
31
|
-
const includeByType = { skill: ["skills"], agent: ["agents"], squad: ["agents", "squads"], project: ["agents", "projects"] };
|
|
36
|
+
const includeByType = { skill: ["skills"], agent: ["agents"], squad: ["agents", "squads"], project: ["agents", "projects"], autopilot: ["agents", "squads", "autopilots"] };
|
|
32
37
|
const include = new Set(includeByType[type] ?? ["agents", "squads"]);
|
|
33
38
|
if (include.has("agents")) include.add("skills");
|
|
34
39
|
return importBundle({ cli: destCli, dir: tmpDir, runtimeMap, include, fs });
|
|
@@ -6,7 +6,7 @@ allowed-tools: Bash, Read
|
|
|
6
6
|
|
|
7
7
|
# export
|
|
8
8
|
|
|
9
|
-
Export a Multica resource (skill, agent, or
|
|
9
|
+
Export a Multica resource (skill, agent, squad, project, or autopilot) to a local bundle directory.
|
|
10
10
|
|
|
11
11
|
## Step 1 — Verify authentication
|
|
12
12
|
|
|
@@ -20,7 +20,7 @@ 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`, `squad`, or `
|
|
23
|
+
If the user named a specific resource and type (`skill`, `agent`, `squad`, `project`, or `autopilot`), use those directly.
|
|
24
24
|
|
|
25
25
|
Otherwise, list available resources for the chosen type and present a pick list:
|
|
26
26
|
|
|
@@ -50,7 +50,7 @@ Slugify the resolved name for filesystem safety — lowercase it, replace each r
|
|
|
50
50
|
Then construct the default directory by scope:
|
|
51
51
|
|
|
52
52
|
- `all` or `projects` (whole workspace) → `export/<workspace-name>`
|
|
53
|
-
- a single resource (`skill`, `agent`, `squad`, or `
|
|
53
|
+
- a single resource (`skill`, `agent`, `squad`, `project`, or `autopilot`) → `export/<workspace-name>/<slug>-<type>`, where `<slug>` is the slugified resource name and `<type>` is the resource type.
|
|
54
54
|
|
|
55
55
|
Examples: `export all from mx-workspace` → `export/mx-workspace`; `export skill "Foo Bar" from mx-workspace` → `export/mx-workspace/foo-bar-skill`.
|
|
56
56
|
|
|
@@ -58,13 +58,13 @@ Examples: `export all from mx-workspace` → `export/mx-workspace`; `export skil
|
|
|
58
58
|
|
|
59
59
|
```bash
|
|
60
60
|
node "${CLAUDE_PLUGIN_ROOT}/scripts/multica-export.mjs" \
|
|
61
|
-
--scope <skill|agent|squad|project|projects|all> \
|
|
61
|
+
--scope <skill|agent|squad|project|projects|autopilot|all> \
|
|
62
62
|
--id <id> \
|
|
63
63
|
--out <dir> \
|
|
64
64
|
[--workspace <workspace-name>]
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
-
`--id` is required for `skill`, `agent`, `squad`, and `
|
|
67
|
+
`--id` is required for `skill`, `agent`, `squad`, `project`, and `autopilot` (a single named resource); it is **not needed** for `projects` (every project in the workspace) or `all` (the entire workspace). `all` does **not** include autopilots — an autopilot is always exported explicitly, by name, never auto-bundled.
|
|
68
68
|
|
|
69
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
70
|
|
|
@@ -74,6 +74,8 @@ The script writes `manifest.json`, skill `SKILL.md` files, agent JSON files, and
|
|
|
74
74
|
|
|
75
75
|
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.
|
|
76
76
|
|
|
77
|
+
Exporting `--scope autopilot` bundles the autopilot's **assignee** so the bundle is self-contained — an agent assignee (and its skills), or a squad assignee (its members, their skills, and the squad itself). It also captures the schedule and webhook triggers, the target project's **title** (not its ID — resolved by name on import) when one is set, and human subscribers' **names** (not their IDs). A webhook trigger's secret (URL/token) is **never** written to the bundle — only that it exists (its label); a fresh secret is issued on import.
|
|
78
|
+
|
|
77
79
|
## Step 5 — Report results
|
|
78
80
|
|
|
79
81
|
Parse the JSON output from the script and report:
|
|
@@ -82,3 +84,4 @@ Parse the JSON output from the script and report:
|
|
|
82
84
|
- Count of skills, agents, squads, and projects exported.
|
|
83
85
|
- 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).
|
|
84
86
|
- 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>`."
|
|
87
|
+
- If `autopilotWebhookTriggers` is non-empty, surface every autopilot title verbatim with: "NOTE: the following autopilots have a webhook trigger — its secret was NOT exported; a newly issued URL will be created on import: `<autopilot-title>`."
|
|
@@ -19,13 +19,13 @@ No existence check is needed here: the Step 2 dry-run fails with `Unknown worksp
|
|
|
19
19
|
|
|
20
20
|
## Step 2 — Pre-flight (dry run)
|
|
21
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:
|
|
22
|
+
Before writing anything, run the import script with `--dry-run` to preview the bundle. Preview against the **full** set (`--include agents,squads,projects,autopilots`) 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 or autopilot's cost before deciding whether to opt it in:
|
|
23
23
|
|
|
24
24
|
```bash
|
|
25
25
|
node "${CLAUDE_PLUGIN_ROOT}/scripts/multica-import.mjs" \
|
|
26
26
|
--dir <folder> \
|
|
27
27
|
--workspace <workspace-name> \
|
|
28
|
-
--include agents,squads,projects \
|
|
28
|
+
--include agents,squads,projects,autopilots \
|
|
29
29
|
[--runtime-map <srcId1=dstId1,srcId2=dstId2,...>] \
|
|
30
30
|
--dry-run
|
|
31
31
|
```
|
|
@@ -34,9 +34,9 @@ Present the `bundle` and `willImport` counts, and every entry in `incompatibilit
|
|
|
34
34
|
|
|
35
35
|
## Step 3 — Select which types to import
|
|
36
36
|
|
|
37
|
-
Ask the user which of `agents`, `squads`, `projects` they want to import. **Default is `agents,squads`** — `projects`
|
|
37
|
+
Ask the user which of `agents`, `squads`, `projects`, `autopilots` they want to import. **Default is `agents,squads`** — `projects` and `autopilots` each require explicit opt-in.
|
|
38
38
|
|
|
39
|
-
If the pre-flight's `incompatibilities` list contains an `unmapped-runtime` entry, tell the user it
|
|
39
|
+
If the pre-flight's `incompatibilities` list contains an `unmapped-runtime` or `autopilot-squad-assignee-unsupported` entry, tell the user it **aborts the import before any write** (see Step 4 below) — `unmapped-runtime` needs `--runtime-map`; a squad-assigned autopilot has no fix, since the multica CLI has no command to assign a squad to an autopilot (only `--agent`) — drop that autopilot from the bundle or recreate its assignment by hand after importing the squad. An `autopilot-assignee-missing` entry also aborts unless resolved by including `agents` or ensuring that agent already exists in the destination. Other incompatibility kinds are informational only and applied best-effort, fixed up afterward in the Multica UI: `priority-not-settable` (project 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), `lead-agent-missing` (a non-agent lead isn't re-applied to the imported project), `autopilot-priority-not-captured` (the multica CLI/API never returns an autopilot's priority, so it can't be captured or restored at all — a known platform gap, not a bug in this tool), `autopilot-project-missing` (the autopilot's target project isn't found by title in the destination — it's created/updated with no project set), and `autopilot-webhook-reissued` (informational — the webhook trigger always gets a freshly issued URL).
|
|
40
40
|
|
|
41
41
|
## Step 4 — Run the import (auto-mapping first)
|
|
42
42
|
|
|
@@ -46,7 +46,7 @@ Each exported agent record carries its source runtime's `provider` (e.g. `claude
|
|
|
46
46
|
node "${CLAUDE_PLUGIN_ROOT}/scripts/multica-import.mjs" \
|
|
47
47
|
--dir <folder> \
|
|
48
48
|
--workspace <workspace-name> \
|
|
49
|
-
--include <agents,squads,projects>
|
|
49
|
+
--include <agents,squads,projects,autopilots>
|
|
50
50
|
```
|
|
51
51
|
|
|
52
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:
|
|
@@ -62,23 +62,28 @@ Ask the user to pick a matching target runtime by name or ID for each unmapped `
|
|
|
62
62
|
node "${CLAUDE_PLUGIN_ROOT}/scripts/multica-import.mjs" \
|
|
63
63
|
--dir <folder> \
|
|
64
64
|
--workspace <workspace-name> \
|
|
65
|
-
--include <agents,squads,projects> \
|
|
65
|
+
--include <agents,squads,projects,autopilots> \
|
|
66
66
|
--runtime-map <srcId1=dstId1,srcId2=dstId2,...>
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
+
If it instead aborts with `Unresolved autopilot assignees: ...`, that is not fixable with `--runtime-map` — either the autopilot is squad-assigned (no CLI support, see Step 3) or its assignee agent isn't available in this import or the destination; ensure `agents` is included, or that the agent already exists in the destination workspace under the same name, then re-run.
|
|
70
|
+
|
|
69
71
|
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.
|
|
70
72
|
|
|
71
73
|
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.
|
|
72
74
|
|
|
73
75
|
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.
|
|
74
76
|
|
|
77
|
+
Importing `autopilots` always creates a brand-new autopilot **paused**, regardless of the source's status — activation is left as a deliberate follow-up action, never automatic. Re-importing an already-imported autopilot updates it by title (never duplicates) but leaves its current status alone. Its triggers are upserted by kind+label — a re-import never adds a duplicate trigger — and a webhook trigger always gets a freshly issued URL, never the source's.
|
|
78
|
+
|
|
75
79
|
## Step 5 — Report results
|
|
76
80
|
|
|
77
81
|
Parse the JSON output and report:
|
|
78
82
|
|
|
79
|
-
- Created and updated counts for skills, agents, squads, and
|
|
83
|
+
- Created and updated counts for skills, agents, squads, projects, and autopilots (`created.autopilots`/`updated.autopilots`).
|
|
80
84
|
- Name-to-ID maps for skills and agents (`skillIdMap`, `agentIdMap`).
|
|
81
85
|
- `squadIdMap`: name-to-ID map for every squad imported.
|
|
86
|
+
- `autopilotIdMap`: name-to-ID map for every autopilot imported.
|
|
82
87
|
- `mentionsRewritten`: how many agents had an agent-mention link rewritten to its new id.
|
|
83
88
|
- 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>`."
|
|
84
89
|
- 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>`."
|
|
@@ -87,3 +92,7 @@ Parse the JSON output and report:
|
|
|
87
92
|
- If `avatarUnsupported` is non-empty, surface every agent name verbatim with: "NOTE: the following agents had an emoji avatar at the source, which the CLI cannot set on an agent — set it manually in the Multica UI: `<agent-name>`."
|
|
88
93
|
- If `permissionApplyFailures` is non-empty, surface every agent name verbatim with: "WARNING: member-specific sharing failed to apply for the following agents — set their invocation permissions manually in the Multica UI: `<agent-name>`."
|
|
89
94
|
- If `permissionUnsupported` is non-empty, surface every agent name verbatim with: "NOTE: the following agents were shared with specific members at the source, but none of those users exist in the target workspace — re-share manually if needed: `<agent-name>`."
|
|
95
|
+
- If `autopilotProjectUnresolved` is non-empty, surface every autopilot title verbatim with: "NOTE: the following autopilots' target project could not be matched by title in the target workspace — created/updated with no project set: `<autopilot-title>`."
|
|
96
|
+
- If `autopilotSubscribersUnresolved` is non-empty, surface every `<title>:<member-name>` entry verbatim with: "NOTE: the following autopilot subscribers could not be matched by name in the target workspace — skipped: `<entry>`."
|
|
97
|
+
- If `autopilotPriorityNotCaptured` is non-empty, surface every autopilot title verbatim with: "NOTE: the following autopilots' priority could not be captured from the source (the multica CLI/API never returns it) — a known platform gap, not something this tool can fix: `<autopilot-title>`."
|
|
98
|
+
- If `autopilotWebhookReissued` is non-empty, surface every autopilot title verbatim with: "NOTE: the following autopilots had a webhook trigger created with a freshly issued URL — the previous URL does not carry over: `<autopilot-title>`."
|
|
@@ -16,9 +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`, `squad`, or `
|
|
19
|
+
Where `<type>` is `skill`, `agent`, `squad`, `project`, or `autopilot`; `<name>` is the resource name (for `project` and `autopilot`, its **title**); `<src-ws>` and `<dest-ws>` are workspace names registered in Multica.
|
|
20
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.
|
|
21
|
+
Projects and autopilots 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. `multica-sync.mjs autopilot "<title>" from <src-ws> <dest-ws>` syncs the autopilot's assignee (agent, or squad + members) alongside it; a squad-assigned autopilot aborts (see the import skill — the multica CLI has no command to assign a squad to an autopilot).
|
|
22
22
|
|
|
23
23
|
## Step 2 — Run the sync (auto-mapping first)
|
|
24
24
|
|
|
@@ -49,9 +49,12 @@ The script exports to a temporary directory, imports into the destination worksp
|
|
|
49
49
|
|
|
50
50
|
Parse the JSON output and report:
|
|
51
51
|
|
|
52
|
-
- Created and updated counts for skills, agents, squads, and
|
|
52
|
+
- Created and updated counts for skills, agents, squads, projects, and autopilots.
|
|
53
53
|
- Name-to-ID maps (`skillIdMap`, `agentIdMap`).
|
|
54
54
|
- `squadIdMap`: name-to-ID map for every squad synced.
|
|
55
|
+
- `autopilotIdMap`: name-to-ID map for every autopilot synced. Importing always creates a new autopilot **paused**, regardless of source status; a re-sync updates the existing one by title without reactivating or duplicating it, and never re-adds a trigger already present.
|
|
56
|
+
- If `autopilotProjectUnresolved`, `autopilotSubscribersUnresolved`, or `autopilotPriorityNotCaptured` is non-empty, surface each entry verbatim as an "applied best-effort" note (project/subscribers skipped when unmatched by name in the destination; priority can never be captured — a platform gap, not a bug here).
|
|
57
|
+
- If `autopilotWebhookReissued` is non-empty, surface every autopilot title verbatim with: "NOTE: the following autopilots had a webhook trigger created with a freshly issued URL in the destination — the previous URL does not carry over: `<autopilot-title>`."
|
|
55
58
|
- 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>`."
|
|
56
59
|
- If `secretsApplyFailures` is non-empty, surface every agent name verbatim with: "WARNING: mcp_config or custom_env failed to apply to the following agents during sync (the agent itself was still created/updated) — set them manually in the Multica UI: `<agent-name>`."
|
|
57
60
|
- 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 destination workspace: `<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.44",
|
|
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.44",
|
|
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"
|