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.
Files changed (43) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +107 -16
  3. package/bin/agent-orchestrator.js +373 -9
  4. package/package.json +7 -2
  5. package/profiles/generic/orchestrator.yaml +7 -0
  6. package/profiles/mvp/orchestrator.yaml +7 -0
  7. package/profiles/node/orchestrator.yaml +7 -0
  8. package/profiles/vue3/orchestrator.yaml +6 -0
  9. package/templates/.agents/amp.settings.json.example +4 -0
  10. package/templates/.agents/commands/opsx-apply.md +3 -3
  11. package/templates/.agents/commands/opsx-archive.md +2 -2
  12. package/templates/.agents/commands/opsx-design.md +2 -2
  13. package/templates/.agents/commands/opsx-explore.md +2 -2
  14. package/templates/.agents/commands/opsx-propose.md +5 -5
  15. package/templates/.agents/commands/opsx-quick.md +2 -2
  16. package/templates/.agents/commands/opsx-review.md +3 -3
  17. package/templates/.agents/commands/opsx-sync.md +2 -2
  18. package/templates/.agents/figma.local.env.example +10 -0
  19. package/templates/.agents/mcp.json.example +4 -0
  20. package/templates/.agents/rules/agent-orchestration.mdc +1 -0
  21. package/templates/.agents/rules/cli-via-npm.mdc +40 -0
  22. package/templates/.agents/rules/figma-token-setup.mdc +35 -0
  23. package/templates/.agents/rules/openspec-workflow.mdc +8 -5
  24. package/templates/.agents/skills/agent-orchestration/SKILL.md +8 -7
  25. package/templates/.agents/skills/openspec-apply-change/SKILL.md +3 -3
  26. package/templates/.agents/skills/openspec-archive-change/SKILL.md +2 -2
  27. package/templates/.agents/skills/openspec-explore/SKILL.md +2 -2
  28. package/templates/.agents/skills/openspec-howto/SKILL.md +4 -0
  29. package/templates/.agents/skills/openspec-propose/SKILL.md +5 -5
  30. package/templates/.agents/skills/openspec-sync-specs/SKILL.md +2 -2
  31. package/templates/.agents/subagents/code-reviewer.md +32 -0
  32. package/templates/.agents/subagents/code-writer.md +21 -0
  33. package/templates/.agents/subagents/design-implementer.md +28 -0
  34. package/templates/.agents/subagents/openspec-guide.md +24 -0
  35. package/templates/.agents/subagents/setup-doctor.md +26 -0
  36. package/templates/.agents/subagents/test-writer.md +17 -0
  37. package/templates/.github/workflows/agent-verify.yml +3 -3
  38. package/templates/.github/workflows/spec-verify.yml +3 -3
  39. package/templates/AGENTS.md +15 -3
  40. package/templates/CLAUDE.md +3 -2
  41. package/templates/orchestrator.yaml +15 -0
  42. package/templates/scripts/figma-mcp-launcher.cjs +67 -0
  43. 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 = ['.cursor', '.cursor/memory.json', '.amp/settings.json', '.claude'];
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. Start your first change:`);
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. Start exploring:`);
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. Start your first change:`);
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 — no skill sync needed');
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.9",
4
- "description": "Universal AI agent orchestration kit for Cursor, Claude Code, and Amp Code — spec-driven pipeline with OpenSpec integration",
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>",
@@ -52,6 +52,13 @@ memory:
52
52
  mcp:
53
53
  baseline:
54
54
  - memory
55
+ optional:
56
+ - figma
57
+
58
+ figma:
59
+ env_file: .agents/figma.local.env
60
+ token_key: FIGMA_ACCESS_TOKEN
61
+ mcp_launcher: scripts/figma-mcp-launcher.cjs
55
62
 
56
63
  verifier:
57
64
  lint_command: ""
@@ -59,6 +59,13 @@ memory:
59
59
  mcp:
60
60
  baseline:
61
61
  - memory
62
+ optional:
63
+ - figma
64
+
65
+ figma:
66
+ env_file: .agents/figma.local.env
67
+ token_key: FIGMA_ACCESS_TOKEN
68
+ mcp_launcher: scripts/figma-mcp-launcher.cjs
62
69
 
63
70
  verifier:
64
71
  lint_command: "npm run lint"
@@ -57,6 +57,13 @@ memory:
57
57
  mcp:
58
58
  baseline:
59
59
  - memory
60
+ optional:
61
+ - figma
62
+
63
+ figma:
64
+ env_file: .agents/figma.local.env
65
+ token_key: FIGMA_ACCESS_TOKEN
66
+ mcp_launcher: scripts/figma-mcp-launcher.cjs
60
67
 
61
68
  verifier:
62
69
  lint_command: "npm run lint"
@@ -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"
@@ -6,6 +6,10 @@
6
6
  "env": {
7
7
  "MEMORY_FILE_PATH": ".cursor/memory.json"
8
8
  }
9
+ },
10
+ "figma": {
11
+ "command": "node",
12
+ "args": ["scripts/figma-mcp-launcher.cjs"]
9
13
  }
10
14
  }
11
15
  }
@@ -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