agent-orchestrator-kit 0.1.9 → 0.1.11
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/CHANGELOG.md +30 -0
- package/README.md +107 -16
- package/bin/agent-orchestrator.js +373 -9
- package/package.json +7 -2
- package/profiles/generic/orchestrator.yaml +7 -0
- package/profiles/mvp/orchestrator.yaml +7 -0
- package/profiles/node/orchestrator.yaml +7 -0
- package/profiles/vue3/orchestrator.yaml +6 -0
- package/templates/.agents/amp.settings.json.example +4 -0
- package/templates/.agents/commands/opsx-apply.md +3 -3
- package/templates/.agents/commands/opsx-archive.md +2 -2
- package/templates/.agents/commands/opsx-design.md +2 -2
- package/templates/.agents/commands/opsx-explore.md +2 -2
- package/templates/.agents/commands/opsx-propose.md +5 -5
- package/templates/.agents/commands/opsx-quick.md +2 -2
- package/templates/.agents/commands/opsx-review.md +3 -3
- package/templates/.agents/commands/opsx-sync.md +2 -2
- package/templates/.agents/figma.local.env.example +10 -0
- package/templates/.agents/mcp.json.example +4 -0
- package/templates/.agents/rules/agent-orchestration.mdc +1 -0
- package/templates/.agents/rules/cli-via-npm.mdc +40 -0
- package/templates/.agents/rules/figma-token-setup.mdc +35 -0
- package/templates/.agents/rules/openspec-workflow.mdc +8 -5
- package/templates/.agents/skills/agent-orchestration/SKILL.md +8 -7
- package/templates/.agents/skills/openspec-apply-change/SKILL.md +3 -3
- package/templates/.agents/skills/openspec-archive-change/SKILL.md +2 -2
- package/templates/.agents/skills/openspec-explore/SKILL.md +2 -2
- package/templates/.agents/skills/openspec-howto/SKILL.md +4 -0
- package/templates/.agents/skills/openspec-propose/SKILL.md +5 -5
- package/templates/.agents/skills/openspec-sync-specs/SKILL.md +2 -2
- package/templates/.agents/subagents/code-reviewer.md +32 -0
- package/templates/.agents/subagents/code-writer.md +21 -0
- package/templates/.agents/subagents/design-implementer.md +28 -0
- package/templates/.agents/subagents/openspec-guide.md +24 -0
- package/templates/.agents/subagents/setup-doctor.md +26 -0
- package/templates/.agents/subagents/test-writer.md +17 -0
- package/templates/.github/workflows/agent-verify.yml +3 -3
- package/templates/.github/workflows/spec-verify.yml +3 -3
- package/templates/AGENTS.md +15 -3
- package/templates/CLAUDE.md +3 -2
- package/templates/orchestrator.yaml +15 -0
- package/templates/scripts/figma-mcp-launcher.cjs +67 -0
- package/templates/scripts/sync-local-agent-skills.sh +52 -5
|
@@ -26,12 +26,17 @@ const KIT_SKILL_DIRS = [
|
|
|
26
26
|
const KIT_MANAGED_PATHS = [
|
|
27
27
|
'.agents/commands',
|
|
28
28
|
'.agents/rules',
|
|
29
|
+
'.agents/subagents',
|
|
29
30
|
...KIT_SKILL_DIRS.map((s) => `.agents/skills/${s}`),
|
|
30
|
-
'.github/workflows/agent-verify.yml',
|
|
31
|
-
'.gitlab/agent-verify.yml',
|
|
32
31
|
'scripts/sync-local-agent-skills.sh',
|
|
33
32
|
];
|
|
34
33
|
|
|
34
|
+
// CI workflow files are provider-specific and chosen once at `init --ci`.
|
|
35
|
+
// `update` must only refresh them if already present — never resurrect a
|
|
36
|
+
// workflow file for a provider the project doesn't use (e.g. after switching
|
|
37
|
+
// from GitHub Actions to GitLab CI and deleting the GitHub workflow).
|
|
38
|
+
const CI_WORKFLOW_PATHS = ['.github/workflows/agent-verify.yml', '.gitlab/agent-verify.yml'];
|
|
39
|
+
|
|
35
40
|
// Opt-in AI Spec Verifier files, per CI provider. `scripts/verify-specs.sh` is
|
|
36
41
|
// shared — it is stack- and CI-agnostic already.
|
|
37
42
|
const GITLAB_SPEC_VERIFY_PATHS = [
|
|
@@ -55,7 +60,23 @@ function specVerifyPathsFor(ci) {
|
|
|
55
60
|
const VALID_CI_PROVIDERS = ['gitlab', 'github', 'none'];
|
|
56
61
|
const VERIFY_OPENSPEC_SCRIPT = 'npx openspec validate --all --strict';
|
|
57
62
|
|
|
58
|
-
const GITIGNORE_LINES = [
|
|
63
|
+
const GITIGNORE_LINES = [
|
|
64
|
+
'.cursor',
|
|
65
|
+
'.cursor/memory.json',
|
|
66
|
+
'.amp/settings.json',
|
|
67
|
+
'.claude',
|
|
68
|
+
'.agents/figma.local.env',
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
const FIGMA_ENV_REL = join('.agents', 'figma.local.env');
|
|
72
|
+
const FIGMA_ENV_EXAMPLE_REL = join('.agents', 'figma.local.env.example');
|
|
73
|
+
const FIGMA_LAUNCHER_REL = join('scripts', 'figma-mcp-launcher.cjs');
|
|
74
|
+
const FIGMA_MANAGED_PATHS = [
|
|
75
|
+
FIGMA_ENV_EXAMPLE_REL,
|
|
76
|
+
FIGMA_LAUNCHER_REL,
|
|
77
|
+
join('.agents', 'mcp.json.example'),
|
|
78
|
+
join('.agents', 'amp.settings.json.example'),
|
|
79
|
+
];
|
|
59
80
|
|
|
60
81
|
const log = {
|
|
61
82
|
info: (msg) => console.log(pc.cyan(' →'), msg),
|
|
@@ -113,6 +134,141 @@ function mergeGitignore(projectDir, lines) {
|
|
|
113
134
|
log.ok('.gitignore updated');
|
|
114
135
|
}
|
|
115
136
|
|
|
137
|
+
function parseEnvFile(filePath) {
|
|
138
|
+
if (!existsSync(filePath)) return {};
|
|
139
|
+
const values = {};
|
|
140
|
+
for (const line of readFileSync(filePath, 'utf-8').split(/\r?\n/)) {
|
|
141
|
+
const trimmed = line.trim();
|
|
142
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
143
|
+
const eq = trimmed.indexOf('=');
|
|
144
|
+
if (eq === -1) continue;
|
|
145
|
+
const key = trimmed.slice(0, eq).trim();
|
|
146
|
+
let value = trimmed.slice(eq + 1).trim();
|
|
147
|
+
if (
|
|
148
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
149
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
150
|
+
) {
|
|
151
|
+
value = value.slice(1, -1);
|
|
152
|
+
}
|
|
153
|
+
values[key] = value;
|
|
154
|
+
}
|
|
155
|
+
return values;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function readFigmaToken(projectDir) {
|
|
159
|
+
const envPath = join(projectDir, FIGMA_ENV_REL);
|
|
160
|
+
const values = parseEnvFile(envPath);
|
|
161
|
+
return values.FIGMA_ACCESS_TOKEN || values.FIGMA_API_KEY || '';
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function isFigmaConfigured(projectDir) {
|
|
165
|
+
return Boolean(readFigmaToken(projectDir));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function ensureFigmaEnvFile(projectDir) {
|
|
169
|
+
const dest = join(projectDir, FIGMA_ENV_REL);
|
|
170
|
+
const example = join(projectDir, FIGMA_ENV_EXAMPLE_REL);
|
|
171
|
+
const kitExample = join(KIT_ROOT, 'templates', FIGMA_ENV_EXAMPLE_REL);
|
|
172
|
+
|
|
173
|
+
if (existsSync(dest)) {
|
|
174
|
+
return { created: false, path: dest };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const src = existsSync(example) ? example : kitExample;
|
|
178
|
+
if (!existsSync(src)) {
|
|
179
|
+
throw new Error(`Missing template: ${FIGMA_ENV_EXAMPLE_REL}`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
183
|
+
copyFileSync(src, dest);
|
|
184
|
+
return { created: true, path: dest };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function refreshFigmaManagedFiles(projectDir) {
|
|
188
|
+
const templateDir = join(KIT_ROOT, 'templates');
|
|
189
|
+
for (const rel of FIGMA_MANAGED_PATHS) {
|
|
190
|
+
const src = join(templateDir, rel);
|
|
191
|
+
const dest = join(projectDir, rel);
|
|
192
|
+
if (!existsSync(src)) continue;
|
|
193
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
194
|
+
copyFileSync(src, dest);
|
|
195
|
+
log.ok(rel);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function ensureFigmaMcpEntry(projectDir) {
|
|
200
|
+
const figmaServer = {
|
|
201
|
+
command: 'node',
|
|
202
|
+
args: ['scripts/figma-mcp-launcher.cjs'],
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const cursorPath = join(projectDir, '.mcp.json');
|
|
206
|
+
if (existsSync(cursorPath)) {
|
|
207
|
+
try {
|
|
208
|
+
const cfg = JSON.parse(readFileSync(cursorPath, 'utf-8'));
|
|
209
|
+
cfg.mcpServers = cfg.mcpServers || {};
|
|
210
|
+
if (!cfg.mcpServers.figma) {
|
|
211
|
+
cfg.mcpServers.figma = figmaServer;
|
|
212
|
+
writeFileSync(cursorPath, `${JSON.stringify(cfg, null, 2)}\n`);
|
|
213
|
+
log.ok('.mcp.json ← added figma server');
|
|
214
|
+
} else {
|
|
215
|
+
log.ok('.mcp.json already has figma server');
|
|
216
|
+
}
|
|
217
|
+
} catch {
|
|
218
|
+
log.warn('.mcp.json present but invalid JSON — merge figma server manually from .agents/mcp.json.example');
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const ampPath = join(projectDir, '.amp', 'settings.json');
|
|
223
|
+
if (existsSync(ampPath)) {
|
|
224
|
+
try {
|
|
225
|
+
const cfg = JSON.parse(readFileSync(ampPath, 'utf-8'));
|
|
226
|
+
cfg['amp.mcpServers'] = cfg['amp.mcpServers'] || {};
|
|
227
|
+
if (!cfg['amp.mcpServers'].figma) {
|
|
228
|
+
cfg['amp.mcpServers'].figma = figmaServer;
|
|
229
|
+
writeFileSync(ampPath, `${JSON.stringify(cfg, null, 2)}\n`);
|
|
230
|
+
log.ok('.amp/settings.json ← added figma server');
|
|
231
|
+
} else {
|
|
232
|
+
log.ok('.amp/settings.json already has figma server');
|
|
233
|
+
}
|
|
234
|
+
} catch {
|
|
235
|
+
log.warn('.amp/settings.json present but invalid JSON — merge figma server manually');
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function parseFigmaUrl(url) {
|
|
241
|
+
try {
|
|
242
|
+
const parsed = new URL(url);
|
|
243
|
+
const parts = parsed.pathname.split('/').filter(Boolean);
|
|
244
|
+
const designIdx = parts.findIndex((p) => p === 'design' || p === 'file' || p === 'proto');
|
|
245
|
+
const fileKey = designIdx >= 0 ? parts[designIdx + 1] : '';
|
|
246
|
+
const nodeParam = parsed.searchParams.get('node-id') || '';
|
|
247
|
+
const nodeId = nodeParam ? nodeParam.replace(/-/g, ':') : '';
|
|
248
|
+
return { fileKey, nodeId };
|
|
249
|
+
} catch {
|
|
250
|
+
return { fileKey: '', nodeId: '' };
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function figmaApiGet(token, path) {
|
|
255
|
+
const response = await fetch(`https://api.figma.com/v1${path}`, {
|
|
256
|
+
headers: { 'X-Figma-Token': token },
|
|
257
|
+
});
|
|
258
|
+
const text = await response.text();
|
|
259
|
+
let data;
|
|
260
|
+
try {
|
|
261
|
+
data = JSON.parse(text);
|
|
262
|
+
} catch {
|
|
263
|
+
data = { err: text };
|
|
264
|
+
}
|
|
265
|
+
if (!response.ok) {
|
|
266
|
+
const message = data?.err || data?.message || response.statusText || `HTTP ${response.status}`;
|
|
267
|
+
throw new Error(String(message));
|
|
268
|
+
}
|
|
269
|
+
return data;
|
|
270
|
+
}
|
|
271
|
+
|
|
116
272
|
function resolveTemplate(templateName, profile) {
|
|
117
273
|
const profilePath = join(KIT_ROOT, 'profiles', profile, templateName);
|
|
118
274
|
if (existsSync(profilePath)) return profilePath;
|
|
@@ -401,14 +557,17 @@ function printNextSteps(profile, projectDir, ci = 'github', specVerify = false)
|
|
|
401
557
|
lines.push(` 3. Install Vue/JS stack skills:`);
|
|
402
558
|
lines.push(` ${pc.cyan('npx frontend-agent-skills install --agent all --yes')}`);
|
|
403
559
|
lines.push(` 4. MCP: copy .mcp.json (Cursor) / .amp/settings.json (Amp) from *.example files`);
|
|
404
|
-
lines.push(` 5.
|
|
560
|
+
lines.push(` 5. Optional Figma: ${pc.cyan('npx agent-orchestrator-kit figma-setup')} then paste token into ${pc.cyan('.agents/figma.local.env')} (never in chat)`);
|
|
561
|
+
lines.push(` 6. Start your first change:`);
|
|
405
562
|
} else if (profile === 'mvp') {
|
|
406
563
|
lines.push(` 3. For quick demos use ${pc.cyan('/opsx:quick <name>')} (propose + apply, no review gate)`);
|
|
407
564
|
lines.push(` 4. MCP: copy .mcp.json (Cursor) / .amp/settings.json (Amp) from *.example files`);
|
|
408
|
-
lines.push(` 5.
|
|
565
|
+
lines.push(` 5. Optional Figma: ${pc.cyan('npx agent-orchestrator-kit figma-setup')} then paste token into ${pc.cyan('.agents/figma.local.env')} (never in chat)`);
|
|
566
|
+
lines.push(` 6. Start exploring:`);
|
|
409
567
|
} else {
|
|
410
568
|
lines.push(` 3. MCP: copy .mcp.json (Cursor) / .amp/settings.json (Amp) from *.example files`);
|
|
411
|
-
lines.push(` 4.
|
|
569
|
+
lines.push(` 4. Optional Figma: ${pc.cyan('npx agent-orchestrator-kit figma-setup')} then paste token into ${pc.cyan('.agents/figma.local.env')} (never in chat)`);
|
|
570
|
+
lines.push(` 5. Start your first change:`);
|
|
412
571
|
}
|
|
413
572
|
|
|
414
573
|
const startCmd = profile === 'mvp' ? '/opsx:quick' : '/opsx:explore';
|
|
@@ -439,8 +598,65 @@ function printNextSteps(profile, projectDir, ci = 'github', specVerify = false)
|
|
|
439
598
|
console.log('\n' + lines.join('\n') + '\n');
|
|
440
599
|
}
|
|
441
600
|
|
|
601
|
+
// Amp has no file-based custom subagents (only skills and plugin agents), but
|
|
602
|
+
// it natively loads skills from .agents/skills/ with the same description-driven
|
|
603
|
+
// delegation. Each .agents/subagents/<name>.md therefore gets a committed skill
|
|
604
|
+
// wrapper .agents/skills/subagent-<name>/SKILL.md so subagents work in Amp with
|
|
605
|
+
// zero local setup. Wrappers are regenerated on init/update/sync and stale ones
|
|
606
|
+
// are removed when their source subagent is deleted.
|
|
607
|
+
const AMP_SUBAGENT_SKILL_PREFIX = 'subagent-';
|
|
608
|
+
|
|
609
|
+
function listAmpSubagentWrappers(projectDir) {
|
|
610
|
+
const skillsDir = join(projectDir, '.agents', 'skills');
|
|
611
|
+
if (!existsSync(skillsDir)) return [];
|
|
612
|
+
return readdirSync(skillsDir).filter((entry) => entry.startsWith(AMP_SUBAGENT_SKILL_PREFIX));
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function generateAmpSubagentSkills(projectDir) {
|
|
616
|
+
const subagentsDir = join(projectDir, '.agents', 'subagents');
|
|
617
|
+
const skillsDir = join(projectDir, '.agents', 'skills');
|
|
618
|
+
const expected = new Set();
|
|
619
|
+
|
|
620
|
+
if (existsSync(subagentsDir)) {
|
|
621
|
+
for (const file of readdirSync(subagentsDir).filter((f) => f.endsWith('.md'))) {
|
|
622
|
+
const content = readFileSync(join(subagentsDir, file), 'utf-8');
|
|
623
|
+
const parsed = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
624
|
+
const name = parsed?.[1].match(/^name:\s*(.+)$/m)?.[1]?.trim();
|
|
625
|
+
const description = parsed?.[1].match(/^description:\s*(.+)$/m)?.[1]?.trim();
|
|
626
|
+
if (!name || !description) {
|
|
627
|
+
log.warn(`skip Amp wrapper (missing name/description frontmatter): .agents/subagents/${file}`);
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const skillName = `${AMP_SUBAGENT_SKILL_PREFIX}${name}`;
|
|
632
|
+
expected.add(skillName);
|
|
633
|
+
mkdirSync(join(skillsDir, skillName), { recursive: true });
|
|
634
|
+
const skill = [
|
|
635
|
+
'---',
|
|
636
|
+
`name: ${skillName}`,
|
|
637
|
+
`description: ${description}`,
|
|
638
|
+
'---',
|
|
639
|
+
'',
|
|
640
|
+
`<!-- AUTO-GENERATED from .agents/subagents/${file} — edit the source file, then run: npx agent-orchestrator-kit sync -->`,
|
|
641
|
+
'',
|
|
642
|
+
parsed[2].trim(),
|
|
643
|
+
'',
|
|
644
|
+
].join('\n');
|
|
645
|
+
writeFileSync(join(skillsDir, skillName, 'SKILL.md'), skill);
|
|
646
|
+
log.ok(`.agents/skills/${skillName}/SKILL.md (Amp wrapper)`);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
for (const entry of listAmpSubagentWrappers(projectDir)) {
|
|
651
|
+
if (!expected.has(entry)) {
|
|
652
|
+
rmSync(join(projectDir, '.agents', 'skills', entry), { recursive: true, force: true });
|
|
653
|
+
log.warn(`removed stale Amp wrapper: .agents/skills/${entry}`);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
442
658
|
function syncAmp(projectDir) {
|
|
443
|
-
log.info('Amp Code reads .agents/ natively —
|
|
659
|
+
log.info('Amp Code reads .agents/ natively — subagents exposed via skill wrappers');
|
|
444
660
|
mkdirSync(join(projectDir, '.amp'), { recursive: true });
|
|
445
661
|
const ampExample = join(projectDir, '.agents', 'amp.settings.json.example');
|
|
446
662
|
const ampDest = join(projectDir, '.amp', 'settings.json');
|
|
@@ -491,6 +707,7 @@ program
|
|
|
491
707
|
if (existsSync(join(profileDir, '.agents'))) {
|
|
492
708
|
copyDir(join(profileDir, '.agents'), join(projectDir, '.agents'), { overwrite: opts.force });
|
|
493
709
|
}
|
|
710
|
+
generateAmpSubagentSkills(projectDir);
|
|
494
711
|
|
|
495
712
|
log.title('Installing scripts/');
|
|
496
713
|
copyDir(join(templateDir, 'scripts'), join(projectDir, 'scripts'), {
|
|
@@ -578,6 +795,16 @@ program
|
|
|
578
795
|
}
|
|
579
796
|
}
|
|
580
797
|
|
|
798
|
+
generateAmpSubagentSkills(projectDir);
|
|
799
|
+
|
|
800
|
+
for (const rel of CI_WORKFLOW_PATHS) {
|
|
801
|
+
const src = join(templateDir, rel);
|
|
802
|
+
const dest = join(projectDir, rel);
|
|
803
|
+
if (!existsSync(src) || !existsSync(dest)) continue;
|
|
804
|
+
copyFileSync(src, dest);
|
|
805
|
+
log.ok(rel);
|
|
806
|
+
}
|
|
807
|
+
|
|
581
808
|
for (const rel of KIT_OPTIN_PATHS) {
|
|
582
809
|
const src = join(templateDir, rel);
|
|
583
810
|
const dest = join(projectDir, rel);
|
|
@@ -586,8 +813,17 @@ program
|
|
|
586
813
|
log.ok(`${rel} (opt-in)`);
|
|
587
814
|
}
|
|
588
815
|
|
|
816
|
+
try {
|
|
817
|
+
execSync(`chmod +x ${join(projectDir, 'scripts', 'sync-local-agent-skills.sh')}`);
|
|
818
|
+
} catch {}
|
|
819
|
+
|
|
820
|
+
log.title('Refreshing Figma setup templates');
|
|
821
|
+
refreshFigmaManagedFiles(projectDir);
|
|
822
|
+
mergeGitignore(projectDir, GITIGNORE_LINES);
|
|
823
|
+
|
|
589
824
|
log.ok(`Updated to v${KIT_VERSION}`);
|
|
590
825
|
log.info('Run ./scripts/sync-local-agent-skills.sh to sync to local IDE');
|
|
826
|
+
log.info('Optional Figma: npx agent-orchestrator-kit figma-setup');
|
|
591
827
|
});
|
|
592
828
|
|
|
593
829
|
program
|
|
@@ -603,10 +839,20 @@ program
|
|
|
603
839
|
const syncClaude = ['claude', 'all'].includes(opts.target);
|
|
604
840
|
const syncAmpTarget = ['amp', 'all'].includes(opts.target);
|
|
605
841
|
|
|
842
|
+
generateAmpSubagentSkills(projectDir);
|
|
843
|
+
|
|
844
|
+
// Amp skill wrappers are redundant in Cursor/Claude (they get native
|
|
845
|
+
// subagents from .agents/subagents/), so exclude them from skill sync.
|
|
846
|
+
const ampWrappers = listAmpSubagentWrappers(projectDir);
|
|
847
|
+
|
|
606
848
|
if (syncCursor) {
|
|
607
849
|
log.info('Syncing .agents/ → .cursor/');
|
|
608
|
-
copyDir(join(projectDir, '.agents', 'skills'), join(projectDir, '.cursor', 'skills'), { overwrite: true, delete: true });
|
|
850
|
+
copyDir(join(projectDir, '.agents', 'skills'), join(projectDir, '.cursor', 'skills'), { overwrite: true, delete: true, skip: ampWrappers });
|
|
851
|
+
for (const wrapper of ampWrappers) {
|
|
852
|
+
rmSync(join(projectDir, '.cursor', 'skills', wrapper), { recursive: true, force: true });
|
|
853
|
+
}
|
|
609
854
|
copyDir(join(projectDir, '.agents', 'rules'), join(projectDir, '.cursor', 'rules'), { overwrite: true, delete: true });
|
|
855
|
+
copyDir(join(projectDir, '.agents', 'subagents'), join(projectDir, '.cursor', 'agents'), { overwrite: true, delete: true });
|
|
610
856
|
|
|
611
857
|
const mcpExample = join(projectDir, '.agents', 'mcp.json.example');
|
|
612
858
|
const mcpDest = join(projectDir, '.mcp.json');
|
|
@@ -618,7 +864,11 @@ program
|
|
|
618
864
|
|
|
619
865
|
if (syncClaude) {
|
|
620
866
|
log.info('Syncing .agents/ → .claude/');
|
|
621
|
-
copyDir(join(projectDir, '.agents', 'skills'), join(projectDir, '.claude', 'skills'), { overwrite: true, delete: true });
|
|
867
|
+
copyDir(join(projectDir, '.agents', 'skills'), join(projectDir, '.claude', 'skills'), { overwrite: true, delete: true, skip: ampWrappers });
|
|
868
|
+
for (const wrapper of ampWrappers) {
|
|
869
|
+
rmSync(join(projectDir, '.claude', 'skills', wrapper), { recursive: true, force: true });
|
|
870
|
+
}
|
|
871
|
+
copyDir(join(projectDir, '.agents', 'subagents'), join(projectDir, '.claude', 'agents'), { overwrite: true, delete: true });
|
|
622
872
|
|
|
623
873
|
const claudeMd = join(projectDir, 'CLAUDE.md');
|
|
624
874
|
const claudeDir = join(projectDir, '.claude');
|
|
@@ -748,4 +998,118 @@ program
|
|
|
748
998
|
}
|
|
749
999
|
});
|
|
750
1000
|
|
|
1001
|
+
program
|
|
1002
|
+
.command('figma-setup')
|
|
1003
|
+
.description('Create local Figma token env file (never prints the token)')
|
|
1004
|
+
.action(() => {
|
|
1005
|
+
const projectDir = process.cwd();
|
|
1006
|
+
log.title('agent-orchestrator figma-setup');
|
|
1007
|
+
|
|
1008
|
+
refreshFigmaManagedFiles(projectDir);
|
|
1009
|
+
mergeGitignore(projectDir, GITIGNORE_LINES);
|
|
1010
|
+
|
|
1011
|
+
const result = ensureFigmaEnvFile(projectDir);
|
|
1012
|
+
if (result.created) {
|
|
1013
|
+
log.ok(`Created ${FIGMA_ENV_REL}`);
|
|
1014
|
+
} else {
|
|
1015
|
+
log.ok(`${FIGMA_ENV_REL} already exists`);
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
ensureFigmaMcpEntry(projectDir);
|
|
1019
|
+
|
|
1020
|
+
if (isFigmaConfigured(projectDir)) {
|
|
1021
|
+
log.ok('Figma token: configured');
|
|
1022
|
+
} else {
|
|
1023
|
+
log.warn('Figma token: missing — open .agents/figma.local.env and set FIGMA_ACCESS_TOKEN locally (do not paste into chat)');
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
log.info('Restart Cursor / Amp after saving the token');
|
|
1027
|
+
log.info('Check: npx agent-orchestrator-kit figma-status');
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
program
|
|
1031
|
+
.command('figma-status')
|
|
1032
|
+
.description('Report whether a local Figma token is configured (never prints the token)')
|
|
1033
|
+
.action(() => {
|
|
1034
|
+
const projectDir = process.cwd();
|
|
1035
|
+
log.title('agent-orchestrator figma-status');
|
|
1036
|
+
|
|
1037
|
+
const envPath = join(projectDir, FIGMA_ENV_REL);
|
|
1038
|
+
if (!existsSync(envPath)) {
|
|
1039
|
+
log.err(`Figma token: not configured (missing ${FIGMA_ENV_REL})`);
|
|
1040
|
+
log.info('Run: npx agent-orchestrator-kit figma-setup');
|
|
1041
|
+
process.exitCode = 1;
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
if (!isFigmaConfigured(projectDir)) {
|
|
1046
|
+
log.err('Figma token: not configured (FIGMA_ACCESS_TOKEN is empty)');
|
|
1047
|
+
log.info('Edit .agents/figma.local.env locally — never paste the token into chat');
|
|
1048
|
+
process.exitCode = 1;
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
log.ok('Figma token: configured');
|
|
1053
|
+
if (existsSync(join(projectDir, FIGMA_LAUNCHER_REL))) {
|
|
1054
|
+
log.ok(`MCP launcher: ${FIGMA_LAUNCHER_REL}`);
|
|
1055
|
+
} else {
|
|
1056
|
+
log.warn(`MCP launcher missing — run npx agent-orchestrator-kit update`);
|
|
1057
|
+
}
|
|
1058
|
+
});
|
|
1059
|
+
|
|
1060
|
+
program
|
|
1061
|
+
.command('figma-fetch')
|
|
1062
|
+
.description('Fetch Figma file/nodes JSON via REST API using the local token')
|
|
1063
|
+
.option('--url <url>', 'Figma design URL (file key + optional node-id)')
|
|
1064
|
+
.option('--file <key>', 'Figma file key')
|
|
1065
|
+
.option('--nodes <ids>', 'Comma-separated node ids (1:2 or 1-2)')
|
|
1066
|
+
.option('--out <path>', 'Output JSON path', 'figma-nodes.json')
|
|
1067
|
+
.action(async (opts) => {
|
|
1068
|
+
const projectDir = process.cwd();
|
|
1069
|
+
log.title('agent-orchestrator figma-fetch');
|
|
1070
|
+
|
|
1071
|
+
const token = readFigmaToken(projectDir);
|
|
1072
|
+
if (!token) {
|
|
1073
|
+
log.err('Figma token: not configured');
|
|
1074
|
+
log.info('Run: npx agent-orchestrator-kit figma-setup');
|
|
1075
|
+
process.exitCode = 1;
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
let fileKey = opts.file || '';
|
|
1080
|
+
let nodes = opts.nodes || '';
|
|
1081
|
+
if (opts.url) {
|
|
1082
|
+
const parsed = parseFigmaUrl(opts.url);
|
|
1083
|
+
fileKey = fileKey || parsed.fileKey;
|
|
1084
|
+
nodes = nodes || parsed.nodeId;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
if (!fileKey) {
|
|
1088
|
+
log.err('Missing --file <key> or --url <figma-url>');
|
|
1089
|
+
process.exitCode = 1;
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
const nodeIds = String(nodes || '')
|
|
1094
|
+
.split(',')
|
|
1095
|
+
.map((id) => id.trim())
|
|
1096
|
+
.filter(Boolean)
|
|
1097
|
+
.map((id) => id.replace(/-/g, ':'));
|
|
1098
|
+
|
|
1099
|
+
try {
|
|
1100
|
+
const path = nodeIds.length
|
|
1101
|
+
? `/files/${encodeURIComponent(fileKey)}/nodes?ids=${encodeURIComponent(nodeIds.join(','))}`
|
|
1102
|
+
: `/files/${encodeURIComponent(fileKey)}`;
|
|
1103
|
+
log.info(nodeIds.length ? `Fetching ${nodeIds.length} node(s)…` : 'Fetching full file…');
|
|
1104
|
+
const data = await figmaApiGet(token, path);
|
|
1105
|
+
const outPath = join(projectDir, opts.out);
|
|
1106
|
+
mkdirSync(dirname(outPath), { recursive: true });
|
|
1107
|
+
writeFileSync(outPath, `${JSON.stringify(data, null, 2)}\n`);
|
|
1108
|
+
log.ok(`Wrote ${opts.out}`);
|
|
1109
|
+
} catch (error) {
|
|
1110
|
+
log.err(`Figma API error: ${error.message}`);
|
|
1111
|
+
process.exitCode = 1;
|
|
1112
|
+
}
|
|
1113
|
+
});
|
|
1114
|
+
|
|
751
1115
|
program.parse();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-orchestrator-kit",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Universal AI agent orchestration kit for Cursor, Claude Code, and Amp Code — spec-driven pipeline
|
|
3
|
+
"version": "0.1.11",
|
|
4
|
+
"description": "Universal AI agent orchestration kit for Cursor, Claude Code, and Amp Code — spec-driven OpenSpec pipeline, cross-IDE subagents, and optional local Figma PAT setup (figma-setup / figma-status / figma-fetch)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-agent",
|
|
7
7
|
"cursor",
|
|
@@ -10,6 +10,11 @@
|
|
|
10
10
|
"openspec",
|
|
11
11
|
"sdd",
|
|
12
12
|
"agent-orchestration",
|
|
13
|
+
"subagents",
|
|
14
|
+
"custom-subagents",
|
|
15
|
+
"design-to-code",
|
|
16
|
+
"figma",
|
|
17
|
+
"figma-mcp",
|
|
13
18
|
"developer-tools"
|
|
14
19
|
],
|
|
15
20
|
"author": "Maksim Shevyakov <makshc2>",
|
|
@@ -57,9 +57,15 @@ mcp:
|
|
|
57
57
|
baseline:
|
|
58
58
|
- memory
|
|
59
59
|
optional:
|
|
60
|
+
- figma
|
|
60
61
|
- github
|
|
61
62
|
- browser
|
|
62
63
|
|
|
64
|
+
figma:
|
|
65
|
+
env_file: .agents/figma.local.env
|
|
66
|
+
token_key: FIGMA_ACCESS_TOKEN
|
|
67
|
+
mcp_launcher: scripts/figma-mcp-launcher.cjs
|
|
68
|
+
|
|
63
69
|
verifier:
|
|
64
70
|
lint_command: "npm run lint"
|
|
65
71
|
build_command: "npm run build"
|
|
@@ -16,7 +16,7 @@ Implement tasks from an OpenSpec change.
|
|
|
16
16
|
If a name is provided, use it. Otherwise:
|
|
17
17
|
- Infer from conversation context if the user mentioned a change
|
|
18
18
|
- Auto-select if only one active change exists
|
|
19
|
-
- If ambiguous, run `openspec list --json` to get available changes and use the **AskUserQuestion tool** to let the user select
|
|
19
|
+
- If ambiguous, run `npx openspec list --json` to get available changes and use the **AskUserQuestion tool** to let the user select
|
|
20
20
|
|
|
21
21
|
Always announce: "Using change: <name>" and how to override (e.g., `/opsx:apply <other>`).
|
|
22
22
|
|
|
@@ -35,7 +35,7 @@ Implement tasks from an OpenSpec change.
|
|
|
35
35
|
|
|
36
36
|
2. **Check status to understand the schema**
|
|
37
37
|
```bash
|
|
38
|
-
openspec status --change "<name>" --json
|
|
38
|
+
npx openspec status --change "<name>" --json
|
|
39
39
|
```
|
|
40
40
|
Parse the JSON to understand:
|
|
41
41
|
- `schemaName`: The workflow being used (e.g., "spec-driven")
|
|
@@ -45,7 +45,7 @@ Implement tasks from an OpenSpec change.
|
|
|
45
45
|
3. **Get apply instructions**
|
|
46
46
|
|
|
47
47
|
```bash
|
|
48
|
-
openspec instructions apply --change "<name>" --json
|
|
48
|
+
npx openspec instructions apply --change "<name>" --json
|
|
49
49
|
```
|
|
50
50
|
|
|
51
51
|
This returns:
|
|
@@ -13,7 +13,7 @@ Archive a completed change in the experimental workflow.
|
|
|
13
13
|
|
|
14
14
|
1. **If no change name provided, prompt for selection**
|
|
15
15
|
|
|
16
|
-
Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select.
|
|
16
|
+
Run `npx openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select.
|
|
17
17
|
|
|
18
18
|
Show only active changes (not already archived).
|
|
19
19
|
Include the schema used for each change if available.
|
|
@@ -22,7 +22,7 @@ Archive a completed change in the experimental workflow.
|
|
|
22
22
|
|
|
23
23
|
2. **Check artifact completion status**
|
|
24
24
|
|
|
25
|
-
Run `openspec status --change "<name>" --json` to check artifact completion.
|
|
25
|
+
Run `npx openspec status --change "<name>" --json` to check artifact completion.
|
|
26
26
|
|
|
27
27
|
Parse the JSON to understand:
|
|
28
28
|
- `schemaName`: The workflow being used
|
|
@@ -18,7 +18,7 @@ Capture design into a durable brief for an OpenSpec change. One-shot intake from
|
|
|
18
18
|
### 1. Select the change
|
|
19
19
|
|
|
20
20
|
If name provided — use it. Otherwise:
|
|
21
|
-
- Run `openspec list --json` to list active changes.
|
|
21
|
+
- Run `npx openspec list --json` to list active changes.
|
|
22
22
|
- Auto-select if only one exists.
|
|
23
23
|
- Ask the user if ambiguous.
|
|
24
24
|
|
|
@@ -28,7 +28,7 @@ Announce: "Design intake for change: **<name>**"
|
|
|
28
28
|
|
|
29
29
|
Use the first available source; do not climb the ladder twice:
|
|
30
30
|
|
|
31
|
-
1. **Figma MCP** — one pass only (`get_design_context` / screenshot / metadata). Capture everything needed immediately; never call Figma again during later apply.
|
|
31
|
+
1. **Figma MCP** — one pass only (`get_design_context` / screenshot / metadata). Capture everything needed immediately; never call Figma again during later apply. If MCP fails, check `npx agent-orchestrator-kit figma-status`. If the token is missing, tell the user to run `figma-setup` and edit `.agents/figma.local.env` locally — **never ask them to paste the token into chat**. Optional REST dump: `npx agent-orchestrator-kit figma-fetch --url "<figma-url>" --out openspec/changes/<name>/assets/figma-nodes.json`.
|
|
32
32
|
2. **Exported images** — PNG/SVG already in the repo or attached by the user.
|
|
33
33
|
3. **Screenshots** — UI captures (desktop/mobile).
|
|
34
34
|
4. **Photos** — physical mockups or whiteboard photos.
|
|
@@ -86,7 +86,7 @@ You have full context of the OpenSpec system. Use it naturally, don't force it.
|
|
|
86
86
|
|
|
87
87
|
At the start, quickly check what exists:
|
|
88
88
|
```bash
|
|
89
|
-
openspec list --json
|
|
89
|
+
npx openspec list --json
|
|
90
90
|
```
|
|
91
91
|
|
|
92
92
|
This tells you:
|
|
@@ -108,7 +108,7 @@ Think freely. When insights crystallize, you might offer:
|
|
|
108
108
|
If the user mentions a change or you detect one is relevant:
|
|
109
109
|
|
|
110
110
|
1. **Resolve and read existing artifacts for context**
|
|
111
|
-
- Run `openspec status --change "<name>" --json`.
|
|
111
|
+
- Run `npx openspec status --change "<name>" --json`.
|
|
112
112
|
- Use `changeRoot`, `artifactPaths`, and `actionContext` from the status JSON.
|
|
113
113
|
- Read existing files from `artifactPaths.<artifact>.existingOutputPaths`.
|
|
114
114
|
|