agentvibes 5.14.0 → 5.15.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.
@@ -192,9 +192,63 @@ function _parseCSVLine(line) {
192
192
  // ---------------------------------------------------------------------------
193
193
  // BMAD agent scanner (story 11.5) — fallback when manifest is unavailable
194
194
 
195
+ /** Title-case a hyphenated id: "tech-writer" → "Tech Writer". */
196
+ function _titleCaseId(id) {
197
+ return id.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
198
+ }
199
+
200
+ /**
201
+ * Find v6.6+ persona-agent skills: top-level skill directories named
202
+ * `bmad-agent-<role>` under <root>/.claude/skills (e.g. bmad-agent-analyst →
203
+ * "analyst"). Deliberately IGNORES a skill's private `agents/` subfolder (e.g.
204
+ * bmad-prfaq/agents/) — those hold a skill's internal helper sub-agents, not
205
+ * BMAD persona agents. Matching them produced a bogus "2 agents" roster in the
206
+ * Agents tab when no real BMAD roster was installed.
207
+ *
208
+ * @param {string} root
209
+ * @returns {{ id: string, dir: string }[]}
210
+ */
211
+ function _findBmadAgentSkills(root) {
212
+ const skillsDir = path.resolve(root, '.claude', 'skills');
213
+ const out = [];
214
+ if (!fs.existsSync(skillsDir)) return out;
215
+ try {
216
+ for (const skill of fs.readdirSync(skillsDir)) {
217
+ if (!skill.startsWith('bmad-agent-')) continue;
218
+ const id = skill.slice('bmad-agent-'.length);
219
+ if (!_isValidAgentId(id)) continue;
220
+ out.push({ id, dir: path.resolve(skillsDir, skill) });
221
+ }
222
+ } catch { /* skip */ }
223
+ return out;
224
+ }
225
+
226
+ /**
227
+ * Build an agent record from a `bmad-agent-*` skill dir, reading the SKILL.md
228
+ * heading ("# Mary — Business Analyst") for the persona name + title. Falls back
229
+ * to a title-cased id when the heading is missing/unreadable.
230
+ */
231
+ function _agentFromSkill(id, skillDir) {
232
+ let displayName = _titleCaseId(id);
233
+ let title = '';
234
+ try {
235
+ const md = fs.readFileSync(path.resolve(skillDir, 'SKILL.md'), 'utf8');
236
+ // Persona heading format: "# Mary — Business Analyst" (name <dash> title).
237
+ // Require the spaced dash so a generic section heading like "# Overview" is
238
+ // skipped and we fall back to the title-cased id instead of mislabeling.
239
+ const m = md.match(/^#\s+([^\n]+?)\s+[—–-]\s+([^\n]+?)\s*$/m); // em / en / hyphen
240
+ if (m) {
241
+ displayName = m[1].trim() || displayName;
242
+ title = m[2].trim();
243
+ }
244
+ } catch { /* use derived displayName */ }
245
+ return { id, displayName, title, icon: '', module: 'bmm' };
246
+ }
247
+
195
248
  /**
196
249
  * Scan for BMAD agents in the project root.
197
- * Prefers manifest-based discovery; falls back to directory scan.
250
+ * Prefers manifest-based discovery; falls back to legacy dir scan, then to the
251
+ * v6.6+ skills-only layout (`bmad-agent-*` skills).
198
252
  *
199
253
  * @param {string} projectRoot
200
254
  * @returns {{ id: string, displayName: string, title: string, icon: string, module: string }[]}
@@ -207,50 +261,42 @@ export function scanBmadAgents(projectRoot) {
207
261
  // Fallback: directory scan — check project-local then home dir
208
262
  const safeRoot = path.resolve(projectRoot ?? process.cwd());
209
263
  const homeDir2 = os.homedir();
264
+ const roots = safeRoot !== homeDir2 ? [safeRoot, homeDir2] : [safeRoot];
210
265
 
211
- // v6.6+: agents under .claude/skills/*/agents/ collect all such dirs
212
- const skillsAgentDirs = [];
213
- for (const root of (safeRoot !== homeDir2 ? [safeRoot, homeDir2] : [safeRoot])) {
214
- const skillsDir = path.resolve(root, '.claude', 'skills');
215
- if (fs.existsSync(skillsDir)) {
216
- try {
217
- for (const skill of fs.readdirSync(skillsDir)) {
218
- const agentsDir = path.resolve(skillsDir, skill, 'agents');
219
- if (fs.existsSync(agentsDir)) skillsAgentDirs.push(agentsDir);
220
- }
221
- } catch { /* skip */ }
222
- }
266
+ // Legacy layouts: <root>/_bmad/bmm/agents and <root>/.bmad/agents hold one
267
+ // .md per agent.
268
+ const candidateDirs = [];
269
+ for (const root of roots) {
270
+ candidateDirs.push(path.resolve(root, '_bmad', 'bmm', 'agents'));
271
+ candidateDirs.push(path.resolve(root, '.bmad', 'agents'));
223
272
  }
224
-
225
- const candidateDirs = [
226
- path.resolve(safeRoot, '_bmad', 'bmm', 'agents'),
227
- path.resolve(safeRoot, '.bmad', 'agents'),
228
- ...(safeRoot !== homeDir2 ? [
229
- path.resolve(homeDir2, '_bmad', 'bmm', 'agents'),
230
- path.resolve(homeDir2, '.bmad', 'agents'),
231
- ] : []),
232
- ...skillsAgentDirs,
233
- ];
234
-
235
273
  for (const dir of candidateDirs) {
236
274
  if (!fs.existsSync(dir)) continue;
237
275
  try {
238
- const files = fs.readdirSync(dir);
239
- return files
276
+ const agents = fs.readdirSync(dir)
240
277
  .filter(f => f.endsWith('.md') && !f.includes('.backup') && !f.includes('.bak'))
241
278
  .map(f => {
242
279
  const id = f.replace(/\.md$/, '');
243
- const displayName = id
244
- .split('-')
245
- .map(w => w.charAt(0).toUpperCase() + w.slice(1))
246
- .join(' ');
247
- return { id, displayName, title: '', icon: '', module: 'bmm' };
248
- })
249
- .sort((a, b) => a.id.localeCompare(b.id));
280
+ return { id, displayName: _titleCaseId(id), title: '', icon: '', module: 'bmm' };
281
+ });
282
+ if (agents.length > 0) return agents.sort((a, b) => a.id.localeCompare(b.id));
250
283
  } catch {
251
284
  // Directory not readable — skip
252
285
  }
253
286
  }
287
+
288
+ // v6.6+ skills-only install: persona agents are `bmad-agent-<role>` skills.
289
+ const seen = new Set();
290
+ const skillAgents = [];
291
+ for (const root of roots) {
292
+ for (const { id, dir } of _findBmadAgentSkills(root)) {
293
+ if (seen.has(id)) continue;
294
+ seen.add(id);
295
+ skillAgents.push(_agentFromSkill(id, dir));
296
+ }
297
+ }
298
+ if (skillAgents.length > 0) return skillAgents.sort((a, b) => a.id.localeCompare(b.id));
299
+
254
300
  return [];
255
301
  }
256
302
 
@@ -277,14 +323,10 @@ export function isBmadDetected(projectRoot) {
277
323
  ];
278
324
  if (dirs.some(d => fs.existsSync(d))) return true;
279
325
 
280
- // v6.6+: agents live under .claude/skills/*/agents/
281
- const skillsDir = path.resolve(root, '.claude', 'skills');
282
- if (fs.existsSync(skillsDir)) {
283
- try {
284
- const skills = fs.readdirSync(skillsDir);
285
- if (skills.some(s => fs.existsSync(path.resolve(skillsDir, s, 'agents')))) return true;
286
- } catch { /* skip */ }
287
- }
326
+ // v6.6+ skills-only install: persona agents are `bmad-agent-<role>` skills.
327
+ // (NOT `<skill>/agents/` subfolders — those are a skill's private helper
328
+ // sub-agents and must not count as a BMAD install.)
329
+ if (_findBmadAgentSkills(root).length > 0) return true;
288
330
  }
289
331
 
290
332
  return false;