@groupby/ai-dev 0.1.1 → 0.2.1

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 (50) hide show
  1. package/README.md +9 -0
  2. package/dist/index.js +260 -21
  3. package/package.json +16 -11
  4. package/toolsets/rzlv-flow/README.md +57 -0
  5. package/toolsets/rzlv-flow/docs/mcp-setup.md +126 -0
  6. package/toolsets/rzlv-flow/resources/README.md +16 -0
  7. package/toolsets/rzlv-flow/resources/confluence-file-structure.md +179 -0
  8. package/toolsets/rzlv-flow/resources/confluence-page-templates/README.md +19 -0
  9. package/toolsets/rzlv-flow/resources/confluence-page-templates/decisions.md +36 -0
  10. package/toolsets/rzlv-flow/resources/confluence-page-templates/initiative-overview.md +40 -0
  11. package/toolsets/rzlv-flow/resources/confluence-page-templates/strategic-context.md +44 -0
  12. package/toolsets/rzlv-flow/resources/confluence-page-templates/technical-architecture.md +48 -0
  13. package/toolsets/rzlv-flow/resources/fcmp-protocol.md +331 -0
  14. package/toolsets/rzlv-flow/resources/jira-file-structure.md +177 -0
  15. package/toolsets/rzlv-flow/resources/sync-state-format.md +209 -0
  16. package/toolsets/rzlv-flow/skills/atlassian-orchestrator/SKILL.md +643 -0
  17. package/toolsets/rzlv-flow/skills/context-analyst/SKILL.md +265 -0
  18. package/toolsets/rzlv-flow/skills/jira-comment/SKILL.md +89 -0
  19. package/toolsets/rzlv-flow/skills/jira-daily-triage/SKILL.md +135 -0
  20. package/toolsets/rzlv-flow/skills/jira-sprint-status/SKILL.md +116 -0
  21. package/toolsets/rzlv-flow/skills/jira-status/SKILL.md +97 -0
  22. package/toolsets/rzlv-flow/skills/jira-sync/SKILL.md +148 -0
  23. package/toolsets/rzlv-flow/skills/jira-ticket-focus/SKILL.md +240 -0
  24. package/toolsets/rzlv-flow/skills/jira-ticket-trace/SKILL.md +112 -0
  25. package/toolsets/rzlv-flow/skills/jira-wrap-sync/SKILL.md +227 -0
  26. package/toolsets/toolsets/rzlv-flow/README.md +102 -0
  27. package/toolsets/toolsets/rzlv-flow/docs/getting-started.md +102 -0
  28. package/toolsets/toolsets/rzlv-flow/docs/mcp-setup.md +126 -0
  29. package/toolsets/toolsets/rzlv-flow/resources/README.md +16 -0
  30. package/toolsets/toolsets/rzlv-flow/resources/confluence-file-structure.md +285 -0
  31. package/toolsets/toolsets/rzlv-flow/resources/confluence-page-templates/README.md +19 -0
  32. package/toolsets/toolsets/rzlv-flow/resources/confluence-page-templates/decisions.md +36 -0
  33. package/toolsets/toolsets/rzlv-flow/resources/confluence-page-templates/initiative-overview.md +40 -0
  34. package/toolsets/toolsets/rzlv-flow/resources/confluence-page-templates/strategic-context.md +44 -0
  35. package/toolsets/toolsets/rzlv-flow/resources/confluence-page-templates/technical-architecture.md +48 -0
  36. package/toolsets/toolsets/rzlv-flow/resources/fcmp-protocol.md +331 -0
  37. package/toolsets/toolsets/rzlv-flow/resources/jira-file-structure.md +177 -0
  38. package/toolsets/toolsets/rzlv-flow/resources/sync-state-format.md +318 -0
  39. package/toolsets/toolsets/rzlv-flow/skills/atlassian-orchestrator/SKILL.md +643 -0
  40. package/toolsets/toolsets/rzlv-flow/skills/confluence-fetch/SKILL.md +189 -0
  41. package/toolsets/toolsets/rzlv-flow/skills/confluence-publish/SKILL.md +178 -0
  42. package/toolsets/toolsets/rzlv-flow/skills/context-analyst/SKILL.md +265 -0
  43. package/toolsets/toolsets/rzlv-flow/skills/jira-comment/SKILL.md +89 -0
  44. package/toolsets/toolsets/rzlv-flow/skills/jira-daily-triage/SKILL.md +143 -0
  45. package/toolsets/toolsets/rzlv-flow/skills/jira-sprint-status/SKILL.md +143 -0
  46. package/toolsets/toolsets/rzlv-flow/skills/jira-status/SKILL.md +97 -0
  47. package/toolsets/toolsets/rzlv-flow/skills/jira-sync/SKILL.md +148 -0
  48. package/toolsets/toolsets/rzlv-flow/skills/jira-ticket-focus/SKILL.md +245 -0
  49. package/toolsets/toolsets/rzlv-flow/skills/jira-ticket-trace/SKILL.md +112 -0
  50. package/toolsets/toolsets/rzlv-flow/skills/jira-wrap-sync/SKILL.md +260 -0
package/README.md CHANGED
@@ -88,6 +88,15 @@ npm run build
88
88
  # Output is in dist/
89
89
  ```
90
90
 
91
+ ### Running tests
92
+
93
+ ```sh
94
+ npm test # Run all tests once
95
+ npm run test:watch # Watch mode (re-runs on file changes)
96
+ ```
97
+
98
+ Tests use [Vitest](https://vitest.dev/) and cover frontmatter parsing, skill discovery, installation logic, and SKILL.md validation across the repo.
99
+
91
100
  The CLI is written in TypeScript and bundled to ESM with [tsup](https://github.com/egoist/tsup).
92
101
 
93
102
  ## Repository
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ import { Command } from "commander";
5
5
 
6
6
  // src/commands/list.ts
7
7
  import chalk from "chalk";
8
+ import fs2 from "fs-extra";
8
9
 
9
10
  // src/lib/discovery.ts
10
11
  import path from "path";
@@ -34,7 +35,8 @@ var PACKAGE_ROOT = path.resolve(__dirname, "..");
34
35
  async function discoverSkills() {
35
36
  const libraryPattern = "skills/library/*/SKILL.md";
36
37
  const teamPattern = "teams/*/skills/*/SKILL.md";
37
- const matches = await fg([libraryPattern, teamPattern], {
38
+ const toolsetPattern = "toolsets/*/skills/*/SKILL.md";
39
+ const matches = await fg([libraryPattern, teamPattern, toolsetPattern], {
38
40
  cwd: PACKAGE_ROOT,
39
41
  absolute: true
40
42
  });
@@ -46,8 +48,13 @@ async function discoverSkills() {
46
48
  const relativePath = path.relative(PACKAGE_ROOT, match);
47
49
  let sourceType;
48
50
  let teamName;
51
+ let toolsetName;
49
52
  if (relativePath.startsWith("skills/library/")) {
50
53
  sourceType = "library";
54
+ } else if (relativePath.startsWith("toolsets/")) {
55
+ sourceType = "toolset";
56
+ const parts = relativePath.split(path.sep);
57
+ toolsetName = parts[1];
51
58
  } else {
52
59
  sourceType = "team";
53
60
  const parts = relativePath.split(path.sep);
@@ -59,6 +66,7 @@ async function discoverSkills() {
59
66
  sourcePath: skillFolder,
60
67
  sourceType,
61
68
  teamName,
69
+ toolsetName,
62
70
  frontmatter: data
63
71
  });
64
72
  }
@@ -79,9 +87,37 @@ async function discoverTeams() {
79
87
  skills: skills2
80
88
  }));
81
89
  }
90
+ async function discoverToolsets() {
91
+ const skills = await discoverSkills();
92
+ const toolsetMap = /* @__PURE__ */ new Map();
93
+ for (const skill of skills) {
94
+ if (skill.sourceType === "toolset" && skill.toolsetName) {
95
+ const existing = toolsetMap.get(skill.toolsetName) || [];
96
+ existing.push(skill);
97
+ toolsetMap.set(skill.toolsetName, existing);
98
+ }
99
+ }
100
+ const toolsets = [];
101
+ for (const [name, toolsetSkills] of toolsetMap) {
102
+ const resourcesPath = path.join(PACKAGE_ROOT, "toolsets", name, "resources");
103
+ let hasResources = false;
104
+ if (await fs.pathExists(resourcesPath)) {
105
+ const entries = await fs.readdir(resourcesPath);
106
+ hasResources = entries.some((e) => e !== "README.md");
107
+ }
108
+ toolsets.push({ name, skills: toolsetSkills, resourcesPath, hasResources });
109
+ }
110
+ return toolsets;
111
+ }
82
112
  async function findSkill(name) {
83
113
  const skills = await discoverSkills();
84
- return skills.find((s) => s.name === name && s.sourceType === "library") || skills.find((s) => s.name === name);
114
+ return skills.find((s) => s.name === name && s.sourceType === "library") || skills.find((s) => s.name === name && s.sourceType === "team") || skills.find((s) => s.name === name);
115
+ }
116
+ async function findToolset(name) {
117
+ const toolsets = await discoverToolsets();
118
+ return toolsets.find(
119
+ (t) => t.name === name || t.name.toLowerCase() === name.toLowerCase()
120
+ );
85
121
  }
86
122
 
87
123
  // src/commands/list.ts
@@ -100,6 +136,7 @@ async function listSkills() {
100
136
  }
101
137
  const librarySkills = skills.filter((s) => s.sourceType === "library");
102
138
  const teamSkills = skills.filter((s) => s.sourceType === "team");
139
+ const toolsetSkills = skills.filter((s) => s.sourceType === "toolset");
103
140
  if (librarySkills.length > 0) {
104
141
  console.log(chalk.bold("\nLibrary"));
105
142
  for (const s of librarySkills) {
@@ -120,6 +157,20 @@ ${formatTeamName(team)}`));
120
157
  console.log(` ${chalk.cyan(s.name.padEnd(24))} ${truncate(s.description, 60)}`);
121
158
  }
122
159
  }
160
+ const toolsetMap = /* @__PURE__ */ new Map();
161
+ for (const s of toolsetSkills) {
162
+ const tsName = s.toolsetName || "unknown";
163
+ const existing = toolsetMap.get(tsName) || [];
164
+ existing.push(s);
165
+ toolsetMap.set(tsName, existing);
166
+ }
167
+ for (const [tsName, skillList] of toolsetMap) {
168
+ console.log(chalk.bold(`
169
+ ${tsName} ${chalk.dim("(toolset)")}`));
170
+ for (const s of skillList) {
171
+ console.log(` ${chalk.cyan(s.name.padEnd(24))} ${truncate(s.description, 60)}`);
172
+ }
173
+ }
123
174
  console.log();
124
175
  }
125
176
  async function listTeams() {
@@ -136,6 +187,24 @@ async function listTeams() {
136
187
  }
137
188
  console.log();
138
189
  }
190
+ async function listToolsets() {
191
+ const toolsets = await discoverToolsets();
192
+ if (toolsets.length === 0) {
193
+ console.log(chalk.yellow("No toolsets found."));
194
+ return;
195
+ }
196
+ console.log(chalk.bold("\nToolsets"));
197
+ for (const t of toolsets) {
198
+ const parts = [`${t.skills.length} skill${t.skills.length !== 1 ? "s" : ""}`];
199
+ if (t.hasResources) {
200
+ const entries = await fs2.readdir(t.resourcesPath);
201
+ const count = entries.filter((e) => e !== "README.md").length;
202
+ parts.push(`${count} resource${count !== 1 ? "s" : ""}`);
203
+ }
204
+ console.log(` ${chalk.cyan(t.name.padEnd(24))} ${parts.join(", ")}`);
205
+ }
206
+ console.log();
207
+ }
139
208
  async function listAll() {
140
209
  await listSkills();
141
210
  const teams = await discoverTeams();
@@ -148,11 +217,26 @@ async function listAll() {
148
217
  }
149
218
  console.log();
150
219
  }
220
+ const toolsets = await discoverToolsets();
221
+ if (toolsets.length > 0) {
222
+ console.log(chalk.bold("Toolsets"));
223
+ for (const t of toolsets) {
224
+ const parts = [`${t.skills.length} skill${t.skills.length !== 1 ? "s" : ""}`];
225
+ if (t.hasResources) {
226
+ const entries = await fs2.readdir(t.resourcesPath);
227
+ const count = entries.filter((e) => e !== "README.md").length;
228
+ parts.push(`${count} resource${count !== 1 ? "s" : ""}`);
229
+ }
230
+ console.log(` ${chalk.cyan(t.name.padEnd(24))} ${parts.join(", ")}`);
231
+ }
232
+ console.log();
233
+ }
151
234
  }
152
235
  function registerListCommand(program2) {
153
236
  const list = program2.command("list").description("List available skills and teams");
154
237
  list.command("skills").description("List all available skills").action(listSkills);
155
238
  list.command("teams").description("List teams and their skill counts").action(listTeams);
239
+ list.command("toolsets").description("List toolsets with skill and resource counts").action(listToolsets);
156
240
  list.action(listAll);
157
241
  }
158
242
 
@@ -162,7 +246,7 @@ import chalk3 from "chalk";
162
246
 
163
247
  // src/lib/clients.ts
164
248
  import path2 from "path";
165
- import fs2 from "fs-extra";
249
+ import fs3 from "fs-extra";
166
250
  var ALL_CLIENTS = [
167
251
  { name: "Copilot", skillsDir: ".github/skills", detectDir: ".github" },
168
252
  { name: "Claude Code", skillsDir: ".claude/skills", detectDir: ".claude" },
@@ -172,7 +256,7 @@ async function detectClients(targetDir) {
172
256
  const detected = [];
173
257
  for (const client of ALL_CLIENTS) {
174
258
  const dirPath = path2.join(targetDir, client.detectDir);
175
- if (await fs2.pathExists(dirPath)) {
259
+ if (await fs3.pathExists(dirPath)) {
176
260
  detected.push(client);
177
261
  }
178
262
  }
@@ -181,20 +265,20 @@ async function detectClients(targetDir) {
181
265
 
182
266
  // src/lib/installer.ts
183
267
  import path3 from "path";
184
- import fs3 from "fs-extra";
268
+ import fs4 from "fs-extra";
185
269
  async function handleFile(srcPath, destPath, options, result, onConflict, contentOverride) {
186
270
  const relativeDest = path3.relative(options.targetDir, destPath);
187
- if (await fs3.pathExists(destPath)) {
188
- const existingContent = await fs3.readFile(destPath, "utf-8");
189
- const newContent = contentOverride ?? await fs3.readFile(srcPath, "utf-8");
271
+ if (await fs4.pathExists(destPath)) {
272
+ const existingContent = await fs4.readFile(destPath, "utf-8");
273
+ const newContent = contentOverride ?? await fs4.readFile(srcPath, "utf-8");
190
274
  if (existingContent === newContent) {
191
275
  result.skipped.push(relativeDest);
192
276
  return;
193
277
  }
194
278
  if (options.force) {
195
279
  if (!options.dryRun) {
196
- await fs3.ensureDir(path3.dirname(destPath));
197
- await fs3.writeFile(destPath, newContent);
280
+ await fs4.ensureDir(path3.dirname(destPath));
281
+ await fs4.writeFile(destPath, newContent);
198
282
  }
199
283
  result.overwritten.push(relativeDest);
200
284
  } else if (options.skipExisting) {
@@ -203,8 +287,8 @@ async function handleFile(srcPath, destPath, options, result, onConflict, conten
203
287
  const choice = await onConflict(relativeDest);
204
288
  if (choice === "overwrite") {
205
289
  if (!options.dryRun) {
206
- await fs3.ensureDir(path3.dirname(destPath));
207
- await fs3.writeFile(destPath, newContent);
290
+ await fs4.ensureDir(path3.dirname(destPath));
291
+ await fs4.writeFile(destPath, newContent);
208
292
  }
209
293
  result.overwritten.push(relativeDest);
210
294
  } else {
@@ -215,11 +299,11 @@ async function handleFile(srcPath, destPath, options, result, onConflict, conten
215
299
  }
216
300
  } else {
217
301
  if (!options.dryRun) {
218
- await fs3.ensureDir(path3.dirname(destPath));
302
+ await fs4.ensureDir(path3.dirname(destPath));
219
303
  if (contentOverride) {
220
- await fs3.writeFile(destPath, contentOverride);
304
+ await fs4.writeFile(destPath, contentOverride);
221
305
  } else {
222
- await fs3.copy(srcPath, destPath);
306
+ await fs4.copy(srcPath, destPath);
223
307
  }
224
308
  }
225
309
  result.created.push(relativeDest);
@@ -233,10 +317,10 @@ async function installSkill(skill, options, onConflict) {
233
317
  overwritten: []
234
318
  };
235
319
  const destFolder = path3.join(options.targetDir, "docs", "ai", "skills", skill.name);
236
- const sourceFiles = await fs3.readdir(skill.sourcePath);
320
+ const sourceFiles = await fs4.readdir(skill.sourcePath);
237
321
  for (const file of sourceFiles) {
238
322
  const srcFile = path3.join(skill.sourcePath, file);
239
- const stat = await fs3.stat(srcFile);
323
+ const stat = await fs4.stat(srcFile);
240
324
  if (stat.isFile()) {
241
325
  const destFile = path3.join(destFolder, file);
242
326
  await handleFile(srcFile, destFile, options, result, onConflict);
@@ -261,6 +345,30 @@ async function installSkills(skills, options, onConflict) {
261
345
  }
262
346
  return results;
263
347
  }
348
+ async function installResources(toolset, options, onConflict) {
349
+ const result = {
350
+ toolset: toolset.name,
351
+ created: [],
352
+ skipped: [],
353
+ overwritten: []
354
+ };
355
+ if (!toolset.hasResources) return result;
356
+ const entries = await fs4.readdir(toolset.resourcesPath);
357
+ const resourceFiles = entries.filter((e) => e !== "README.md");
358
+ const destDir = path3.join(options.targetDir, "docs", "ai", "resources");
359
+ const fileResult = { skill: toolset.name, created: [], skipped: [], overwritten: [] };
360
+ for (const file of resourceFiles) {
361
+ const srcFile = path3.join(toolset.resourcesPath, file);
362
+ const stat = await fs4.stat(srcFile);
363
+ if (!stat.isFile()) continue;
364
+ const destFile = path3.join(destDir, file);
365
+ await handleFile(srcFile, destFile, options, fileResult, onConflict);
366
+ }
367
+ result.created = fileResult.created;
368
+ result.skipped = fileResult.skipped;
369
+ result.overwritten = fileResult.overwritten;
370
+ return result;
371
+ }
264
372
 
265
373
  // src/lib/prompts.ts
266
374
  import { select, checkbox, confirm } from "@inquirer/prompts";
@@ -268,6 +376,7 @@ import chalk2 from "chalk";
268
376
  async function promptSelectSkills(skills) {
269
377
  const librarySkills = skills.filter((s) => s.sourceType === "library");
270
378
  const teamSkills = skills.filter((s) => s.sourceType === "team");
379
+ const toolsetSkills = skills.filter((s) => s.sourceType === "toolset");
271
380
  const choices = [];
272
381
  if (librarySkills.length > 0) {
273
382
  choices.push(
@@ -292,6 +401,21 @@ async function promptSelectSkills(skills) {
292
401
  }))
293
402
  );
294
403
  }
404
+ const toolsetMap = /* @__PURE__ */ new Map();
405
+ for (const s of toolsetSkills) {
406
+ const tsName = s.toolsetName || "unknown";
407
+ const existing = toolsetMap.get(tsName) || [];
408
+ existing.push(s);
409
+ toolsetMap.set(tsName, existing);
410
+ }
411
+ for (const [tsName, tsList] of toolsetMap) {
412
+ choices.push(
413
+ ...tsList.map((s) => ({
414
+ name: `${chalk2.dim(`[${tsName}]`)} ${s.name} \u2014 ${truncate2(s.description, 60)}`,
415
+ value: s
416
+ }))
417
+ );
418
+ }
295
419
  return checkbox({
296
420
  message: "Select skills to install:",
297
421
  choices,
@@ -307,6 +431,15 @@ async function promptSelectTeam(teams) {
307
431
  }))
308
432
  });
309
433
  }
434
+ async function promptSelectToolset(toolsets) {
435
+ return select({
436
+ message: "Select a toolset:",
437
+ choices: toolsets.map((t) => ({
438
+ name: `${t.name} (${t.skills.length} skill${t.skills.length !== 1 ? "s" : ""}${t.hasResources ? " + resources" : ""})`,
439
+ value: t
440
+ }))
441
+ });
442
+ }
310
443
  async function promptSelectClients(allClients, detectedClients) {
311
444
  const detectedNames = new Set(detectedClients.map((c) => c.name));
312
445
  return checkbox({
@@ -352,7 +485,7 @@ function formatTeamName2(name) {
352
485
  }
353
486
 
354
487
  // src/commands/install.ts
355
- function printResults(results, options) {
488
+ function printResults(results, options, resourceResult) {
356
489
  const prefix = options.dryRun ? chalk3.yellow("[DRY RUN] ") : "";
357
490
  const createdWord = options.dryRun ? "would create" : "created";
358
491
  const skippedWord = "up to date";
@@ -360,6 +493,16 @@ function printResults(results, options) {
360
493
  console.log(`
361
494
  ${prefix}${chalk3.bold("Install complete")}
362
495
  `);
496
+ if (resourceResult) {
497
+ const rParts = [];
498
+ if (resourceResult.created.length > 0) rParts.push(chalk3.green(`${resourceResult.created.length} ${createdWord}`));
499
+ if (resourceResult.skipped.length > 0) rParts.push(chalk3.dim(`${resourceResult.skipped.length} ${skippedWord}`));
500
+ if (resourceResult.overwritten.length > 0) rParts.push(chalk3.yellow(`${resourceResult.overwritten.length} ${overwrittenWord}`));
501
+ if (rParts.length > 0) {
502
+ console.log(` ${chalk3.bold("Resources")} ${rParts.join(", ")}`);
503
+ console.log();
504
+ }
505
+ }
363
506
  console.log(" Skill Status");
364
507
  console.log(" " + "\u2500".repeat(50));
365
508
  for (const r of results) {
@@ -404,7 +547,14 @@ async function installSkillCmd(name, _opts, cmd) {
404
547
  dryRun
405
548
  };
406
549
  const result = await installSkill(skill, options, force || skipExisting ? void 0 : promptConflict);
407
- printResults([result], options);
550
+ let resourceResult;
551
+ if (skill.sourceType === "toolset" && skill.toolsetName) {
552
+ const toolset = await findToolset(skill.toolsetName);
553
+ if (toolset && toolset.hasResources) {
554
+ resourceResult = await installResources(toolset, options, force || skipExisting ? void 0 : promptConflict);
555
+ }
556
+ }
557
+ printResults([result], options, resourceResult);
408
558
  }
409
559
  async function installTeamCmd(name, _opts, cmd) {
410
560
  const parentOpts = cmd.parent.opts();
@@ -451,16 +601,56 @@ async function installTeamCmd(name, _opts, cmd) {
451
601
  );
452
602
  printResults(results, options);
453
603
  }
604
+ async function installToolsetCmd(name, _opts, cmd) {
605
+ const parentOpts = cmd.parent.opts();
606
+ const force = Boolean(parentOpts.force);
607
+ const skipExisting = Boolean(parentOpts.skipExisting);
608
+ const dryRun = Boolean(parentOpts.dryRun);
609
+ const targetDir = path4.resolve(
610
+ parentOpts.target || process.cwd()
611
+ );
612
+ const toolset = await findToolset(name);
613
+ if (!toolset) {
614
+ const allToolsets = await discoverToolsets();
615
+ console.log(chalk3.red(`Toolset "${name}" not found.`));
616
+ if (allToolsets.length > 0) {
617
+ console.log(`Available toolsets: ${allToolsets.map((t) => t.name).join(", ")}`);
618
+ }
619
+ process.exit(1);
620
+ }
621
+ const detected = await detectClients(targetDir);
622
+ let clients;
623
+ if (force) {
624
+ clients = detected.length > 0 ? detected : ALL_CLIENTS;
625
+ } else {
626
+ clients = await promptSelectClients(ALL_CLIENTS, detected);
627
+ }
628
+ const options = {
629
+ targetDir,
630
+ clients,
631
+ force,
632
+ skipExisting,
633
+ dryRun
634
+ };
635
+ const conflictHandler = force || skipExisting ? void 0 : promptConflict;
636
+ let resourceResult;
637
+ if (toolset.hasResources) {
638
+ resourceResult = await installResources(toolset, options, conflictHandler);
639
+ }
640
+ const results = await installSkills(toolset.skills, options, conflictHandler);
641
+ printResults(results, options, resourceResult);
642
+ }
454
643
  function registerInstallCommand(program2) {
455
644
  const install = program2.command("install").description("Install skills").option("--force", "Overwrite existing files without prompting").option("--skip-existing", "Skip files that already exist without prompting").option("--dry-run", "Show what would be installed without writing files").option("--target <dir>", "Install to a different directory (default: CWD)");
456
645
  install.command("skill <name>").description("Install a specific skill").action(installSkillCmd);
457
646
  install.command("team <name>").description("Install all skills for a team").action(installTeamCmd);
647
+ install.command("toolset <name>").description("Install all skills and resources for a toolset").action(installToolsetCmd);
458
648
  }
459
649
 
460
650
  // src/commands/interactive.ts
461
651
  import { select as select2 } from "@inquirer/prompts";
462
652
  import chalk4 from "chalk";
463
- function printResults2(results, options) {
653
+ function printResults2(results, options, resourceResult) {
464
654
  const prefix = options.dryRun ? chalk4.yellow("[DRY RUN] ") : "";
465
655
  const createdWord = options.dryRun ? "would create" : "created";
466
656
  const skippedWord = "up to date";
@@ -468,6 +658,16 @@ function printResults2(results, options) {
468
658
  console.log(`
469
659
  ${prefix}${chalk4.bold("Install complete")}
470
660
  `);
661
+ if (resourceResult) {
662
+ const rParts = [];
663
+ if (resourceResult.created.length > 0) rParts.push(chalk4.green(`${resourceResult.created.length} ${createdWord}`));
664
+ if (resourceResult.skipped.length > 0) rParts.push(chalk4.dim(`${resourceResult.skipped.length} ${skippedWord}`));
665
+ if (resourceResult.overwritten.length > 0) rParts.push(chalk4.yellow(`${resourceResult.overwritten.length} ${overwrittenWord}`));
666
+ if (rParts.length > 0) {
667
+ console.log(` ${chalk4.bold("Resources")} ${rParts.join(", ")}`);
668
+ console.log();
669
+ }
670
+ }
471
671
  console.log(" Skill Status");
472
672
  console.log(" " + "\u2500".repeat(50));
473
673
  for (const r of results) {
@@ -521,6 +721,15 @@ ${formatTeamName3(team)}`));
521
721
  );
522
722
  }
523
723
  }
724
+ const toolsets = await discoverToolsets();
725
+ if (toolsets.length > 0) {
726
+ console.log(chalk4.bold("\nToolsets"));
727
+ for (const t of toolsets) {
728
+ console.log(
729
+ ` ${chalk4.cyan(t.name.padEnd(24))} ${t.skills.length} skill${t.skills.length !== 1 ? "s" : ""}`
730
+ );
731
+ }
732
+ }
524
733
  console.log();
525
734
  }
526
735
  async function runInteractive() {
@@ -530,6 +739,7 @@ async function runInteractive() {
530
739
  choices: [
531
740
  { name: "Install skill(s)", value: "install-skills" },
532
741
  { name: "Install team skills", value: "install-team" },
742
+ { name: "Install toolset", value: "install-toolset" },
533
743
  { name: "List available skills & teams", value: "list" }
534
744
  ]
535
745
  });
@@ -591,6 +801,33 @@ async function runInteractive() {
591
801
  };
592
802
  const results = await installSkills(skillsToInstall, options, promptConflict);
593
803
  printResults2(results, options);
804
+ } else if (action === "install-toolset") {
805
+ const toolsets = await discoverToolsets();
806
+ if (toolsets.length === 0) {
807
+ console.log(chalk4.yellow("No toolsets found."));
808
+ return;
809
+ }
810
+ const toolset = await promptSelectToolset(toolsets);
811
+ const detected = await detectClients(targetDir);
812
+ const clients = await promptSelectClients(ALL_CLIENTS, detected);
813
+ const confirmed = await promptConfirmInstall(toolset.skills, clients, targetDir);
814
+ if (!confirmed) {
815
+ console.log(chalk4.dim("Cancelled."));
816
+ return;
817
+ }
818
+ const options = {
819
+ targetDir,
820
+ clients,
821
+ force: false,
822
+ skipExisting: false,
823
+ dryRun: false
824
+ };
825
+ let resourceResult;
826
+ if (toolset.hasResources) {
827
+ resourceResult = await installResources(toolset, options, promptConflict);
828
+ }
829
+ const results = await installSkills(toolset.skills, options, promptConflict);
830
+ printResults2(results, options, resourceResult);
594
831
  }
595
832
  }
596
833
 
@@ -604,9 +841,11 @@ program.addHelpText(
604
841
  `
605
842
  Examples:
606
843
  $ npx @groupby/ai-dev Interactive mode
607
- $ npx @groupby/ai-dev list List skills and teams
844
+ $ npx @groupby/ai-dev list List skills, teams, and toolsets
845
+ $ npx @groupby/ai-dev list toolsets List available toolsets
608
846
  $ npx @groupby/ai-dev install skill frontend-design Install a single skill
609
847
  $ npx @groupby/ai-dev install team brain-studio Install all team skills
848
+ $ npx @groupby/ai-dev install toolset rzlv-flow Install all skills + resources for a toolset
610
849
  $ npx @groupby/ai-dev install team brain-studio --dry-run`
611
850
  );
612
851
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groupby/ai-dev",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "Interactive installer for Rezolve Ai development skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,12 +9,16 @@
9
9
  "files": [
10
10
  "dist/",
11
11
  "skills/",
12
- "teams/"
12
+ "teams/",
13
+ "toolsets/"
13
14
  ],
14
15
  "scripts": {
15
- "prebuild": "cp -r ../skills ./skills && cp -r ../teams ./teams",
16
+ "prebuild": "cp -r ../skills ./skills && cp -r ../teams ./teams && cp -r ../toolsets ./toolsets",
16
17
  "build": "tsup",
17
- "prepublishOnly": "npm run build"
18
+ "prepublishOnly": "npm run build",
19
+ "test": "vitest run",
20
+ "test:watch": "vitest",
21
+ "test:coverage": "vitest run --coverage"
18
22
  },
19
23
  "repository": {
20
24
  "type": "git",
@@ -25,20 +29,21 @@
25
29
  "node": ">=18"
26
30
  },
27
31
  "publishConfig": {
28
- "access": "restricted"
32
+ "access": "public"
29
33
  },
30
34
  "dependencies": {
31
- "commander": "^13.0.0",
32
35
  "@inquirer/prompts": "^7.0.0",
33
36
  "chalk": "^5.3.0",
34
- "gray-matter": "^4.0.3",
37
+ "commander": "^13.0.0",
35
38
  "fast-glob": "^3.3.0",
36
- "fs-extra": "^11.2.0"
39
+ "fs-extra": "^11.2.0",
40
+ "gray-matter": "^4.0.3"
37
41
  },
38
42
  "devDependencies": {
39
- "typescript": "^5.5.0",
40
- "@types/node": "^20.0.0",
41
43
  "@types/fs-extra": "^11.0.0",
42
- "tsup": "^8.0.0"
44
+ "@types/node": "^20.0.0",
45
+ "tsup": "^8.0.0",
46
+ "typescript": "^5.5.0",
47
+ "vitest": "^4.1.3"
43
48
  }
44
49
  }
@@ -0,0 +1,57 @@
1
+ # rzlv-flow: Atlassian Intelligence Skills
2
+
3
+ A toolset of modular, composable skills for Jira and Confluence developer workflows. Covers daily triage, ticket focus, wrap/sync, sprint status, context loading, and more — all without requiring the full BMAD Method or the rzlv-flow monolith.
4
+
5
+ Each skill is standalone and can be installed individually or as a complete suite.
6
+
7
+ ## Skills
8
+
9
+ | Skill | Description | Status |
10
+ |-------|-------------|--------|
11
+ | `jira-daily-triage` | Start-of-day ticket triage with sprint detection | Planned |
12
+ | `jira-ticket-focus` | Deep-load a ticket with full context | Planned |
13
+ | `jira-wrap-sync` | Wrap up work, draft comment, multi-action sync | Planned |
14
+ | `jira-ticket-trace` | Trace code back to Jira requirements | Planned |
15
+ | `jira-comment` | Add a comment to a Jira ticket with FCMP safety | Planned |
16
+ | `jira-status` | Change ticket status with transition validation | Planned |
17
+ | `jira-sprint-status` | Sprint status report with completion metrics | Planned |
18
+ | `jira-sync` | Force-sync local Jira context with remote state | Planned |
19
+ | `atlassian-orchestrator` | Pull surrounding ticket context and create structures | Complete |
20
+ | `context-analyst` | Transform meeting transcripts into structured docs | Complete |
21
+
22
+ ## Prerequisites
23
+
24
+ ### Required: Atlassian MCP (`atlassian-rovo`)
25
+
26
+ All Jira and Confluence skills require the Atlassian MCP server for API access to your Atlassian instance.
27
+
28
+ ### Optional: GitHub MCP
29
+
30
+ Only needed for PR creation features in `jira-wrap-sync`. Not required for any other skills.
31
+
32
+ See [docs/mcp-setup.md](docs/mcp-setup.md) for detailed setup instructions.
33
+
34
+ ## Installation
35
+
36
+ ```bash
37
+ npx @groupby/ai-dev install toolset rzlv-flow # All skills + resources
38
+ npx @groupby/ai-dev install skill jira-daily-triage # Individual skill
39
+ ```
40
+
41
+ > **Note:** CLI toolset support is coming soon. For now, skills can be copied manually from this repository into your project's `docs/ai/skills/` directory, and resources into `docs/ai/resources/`.
42
+
43
+ ## BMAD Compatibility
44
+
45
+ These skills are compatible with BMAD's Jira file structure (`docs/jira/{instance}/{project}/...`) but do **not** require BMAD. They were originally developed as part of the BMAD "Atlassian Intelligence Suite" and have been extracted as standalone skills. If BMAD is installed in your project, these skills coexist without conflict.
46
+
47
+ ## Shared Resources
48
+
49
+ Skills reference shared resource files installed to `docs/ai/resources/` in your project:
50
+
51
+ | Resource | Description |
52
+ |----------|-------------|
53
+ | `fcmp-protocol.md` | Fetch-Compare-Merge-Push sync pattern for safe Atlassian read/write |
54
+ | `jira-file-structure.md` | Standard directory layout for local Jira mirrors |
55
+ | `confluence-file-structure.md` | Leaf Bundle pattern for Confluence page mirrors |
56
+ | `sync-state-format.md` | YAML frontmatter schema for ticket and page files |
57
+ | `confluence-page-templates/` | Starter scaffolds for Confluence pages (initiative overview, strategic context, technical architecture, decisions) |