feat(ide): add Bob IDE installer support

- bob.js: fix installCustomAgentLauncher using wrong mode schema
  (description/prompt/always/permissions → roleDefinition/whenToUse/
  customInstructions/groups)
- bob.js: add detectionPaths for .bob directory
- manager.js: update comments to include bob.js in custom installer list
- platform-codes.yaml: move bob entry to correct alphabetical position
- eslint.config.mjs: add .bob/** to eslint ignores (like .claude, .roo, etc.)
This commit is contained in:
Tim Graepel 2026-02-27 22:36:52 +01:00 committed by timgraepel
parent 3bbfc89c27
commit d21ed92ae8
4 changed files with 39 additions and 26 deletions

View File

@ -20,6 +20,7 @@ export default [
'website/**',
// Gitignored patterns
'z*/**', // z-samples, z1, z2, etc.
'.bob/**',
'.claude/**',
'.codex/**',
'.github/chatmodes/**',

View File

@ -8,12 +8,13 @@ const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generat
/**
* IBM Bob IDE setup handler
* Creates custom modes in .bobmodes file (similar to Kilo)
* Creates custom modes in .bob/custom_modes.yaml file
*/
class BobSetup extends BaseIdeSetup {
constructor() {
super('bob', 'IBM Bob');
this.configFile = '.bobmodes';
this.configFile = '.bob/custom_modes.yaml';
this.detectionPaths = ['.bob'];
}
/**
@ -38,7 +39,7 @@ class BobSetup extends BaseIdeSetup {
config = yaml.parse(existingContent) || {};
} catch {
// If parsing fails, start fresh but warn user
await prompts.log.warn('Warning: Could not parse existing .bobmodes, starting fresh');
await prompts.log.warn('Warning: Could not parse existing .bob/custom_modes.yaml, starting fresh');
config = {};
}
}
@ -61,7 +62,7 @@ class BobSetup extends BaseIdeSetup {
addedCount++;
}
// Write .bobmodes file with proper YAML structure
// Write .bob/custom_modes.yaml file with proper YAML structure
const finalContent = yaml.stringify(config, { lineWidth: 0 });
await this.writeFile(bobModesPath, finalContent);
@ -110,23 +111,25 @@ class BobSetup extends BaseIdeSetup {
* @returns {Object} Mode object for YAML serialization
*/
async createModeObject(artifact, projectDir) {
// Extract metadata from launcher content
const titleMatch = artifact.content.match(/title="([^"]+)"/);
const title = titleMatch ? titleMatch[1] : this.formatTitle(artifact.name);
// Extract title and icon from the compiled agent file's <agent> XML tag
// artifact.content is the launcher template which does NOT contain these attributes
let title = this.formatTitle(artifact.name);
let icon = '🤖';
const iconMatch = artifact.content.match(/icon="([^"]+)"/);
const icon = iconMatch ? iconMatch[1] : '🤖';
if (artifact.sourcePath && (await this.pathExists(artifact.sourcePath))) {
const agentContent = await this.readFile(artifact.sourcePath);
const titleMatch = agentContent.match(/<agent[^>]*\stitle="([^"]+)"/);
if (titleMatch) title = titleMatch[1];
const iconMatch = agentContent.match(/<agent[^>]*\sicon="([^"]+)"/);
if (iconMatch) icon = iconMatch[1];
}
const whenToUseMatch = artifact.content.match(/whenToUse="([^"]+)"/);
const whenToUse = whenToUseMatch ? whenToUseMatch[1] : `Use for ${title} tasks`;
const whenToUse = `Use for ${title} tasks`;
// Get the activation header from central template (trim to avoid YAML formatting issues)
const activationHeader = (await this.getAgentCommandHeader()).trim();
const roleDefinitionMatch = artifact.content.match(/roleDefinition="([^"]+)"/);
const roleDefinition = roleDefinitionMatch
? roleDefinitionMatch[1]
: `You are a ${title} specializing in ${title.toLowerCase()} tasks.`;
const roleDefinition = `You are a ${title} specializing in ${title.toLowerCase()} tasks.`;
// Get relative path
const relativePath = path.relative(projectDir, artifact.sourcePath).replaceAll('\\', '/');
@ -189,12 +192,12 @@ class BobSetup extends BaseIdeSetup {
if (removedCount > 0) {
await fs.writeFile(bobModesPath, yaml.stringify(config, { lineWidth: 0 }));
if (!options.silent) await prompts.log.message(`Removed ${removedCount} BMAD modes from .bobmodes`);
if (!options.silent) await prompts.log.message(`Removed ${removedCount} BMAD modes from .bob/custom_modes.yaml`);
}
}
} catch {
// If parsing fails, leave file as-is
if (!options.silent) await prompts.log.warn('Warning: Could not parse .bobmodes for cleanup');
if (!options.silent) await prompts.log.warn('Warning: Could not parse .bob/custom_modes.yaml for cleanup');
}
}
@ -215,7 +218,7 @@ class BobSetup extends BaseIdeSetup {
const bobmodesPath = path.join(projectDir, this.configFile);
let config = {};
// Read existing .bobmodes file
// Read existing .bob/custom_modes.yaml file
if (await this.pathExists(bobmodesPath)) {
const existingContent = await this.readFile(bobmodesPath);
try {
@ -245,16 +248,18 @@ class BobSetup extends BaseIdeSetup {
}
// Add custom mode object
const title = `BMAD Custom: ${agentName}`;
const activationHeader = (await this.getAgentCommandHeader()).trim();
config.customModes.push({
slug: slug,
name: `BMAD Custom: ${agentName}`,
description: `Custom BMAD agent: ${agentName}\n\n**⚠️ IMPORTANT**: Run @${agentPath} first to load the complete agent!\n\nThis is a launcher for the custom BMAD agent "${agentName}". The agent will follow the persona and instructions from the main agent file.\n`,
prompt: `@${agentPath}\n`,
always: false,
permissions: 'all',
name: title,
roleDefinition: `You are a custom BMAD agent "${agentName}". Follow the persona and instructions from the agent file.`,
whenToUse: `Use for custom BMAD agent "${agentName}" tasks`,
customInstructions: `${activationHeader} Read the full agent from ${agentPath} start activation to alter your state of being follow startup section instructions stay in this being until told to exit this mode\n`,
groups: ['read', 'edit', 'browser', 'command', 'mcp'],
});
// Write .bobmodes file with proper YAML structure
// Write .bob/custom_modes.yaml file with proper YAML structure
await this.writeFile(bobmodesPath, yaml.stringify(config, { lineWidth: 0 }));
return {

View File

@ -8,7 +8,7 @@ const prompts = require('../../../lib/prompts');
* Dynamically discovers and loads IDE handlers
*
* Loading strategy:
* 1. Custom installer files (codex.js, github-copilot.js, kilo.js, rovodev.js) - for platforms with unique installation logic
* 1. Custom installer files (bob.js, codex.js, github-copilot.js, kilo.js, rovodev.js) - for platforms with unique installation logic
* 2. Config-driven handlers (from platform-codes.yaml) - for standard IDE installation patterns
*/
class IdeManager {
@ -44,7 +44,7 @@ class IdeManager {
/**
* Dynamically load all IDE handlers
* 1. Load custom installer files first (codex.js, github-copilot.js, kilo.js, rovodev.js)
* 1. Load custom installer files first (bob.js, codex.js, github-copilot.js, kilo.js, rovodev.js)
* 2. Load config-driven handlers from platform-codes.yaml
*/
async loadHandlers() {

View File

@ -32,6 +32,13 @@ platforms:
target_dir: .augment/commands
template_type: default
bob:
name: "IBM Bob"
preferred: false
category: ide
description: "IBM's AI development environment"
# No installer config - uses custom bob.js (creates .bob/custom_modes.yaml)
claude-code:
name: "Claude Code"
preferred: true