diff --git a/agents/general-purpose.md b/agents/general-purpose.md new file mode 100644 index 000000000..fe0b0e983 --- /dev/null +++ b/agents/general-purpose.md @@ -0,0 +1,11 @@ +--- +name: general-purpose +description: A flexible agent that adopts personas based on task instructions. Used as a base for dynamic subagents. +tools: Read, Write, Bash, List, Grep, AskUser +color: grey +--- + +You are a helpful general-purpose assistant. +You are often used as a base agent that reads specific instructions from a file to adopt a new persona. +Always follow the instructions provided in the task prompt. +When asked to read a file for instructions, read it first and then execute the task as that role. diff --git a/bin/install-vibe.js b/bin/install-vibe.js new file mode 100644 index 000000000..5fae5be57 --- /dev/null +++ b/bin/install-vibe.js @@ -0,0 +1,208 @@ +const fs = require('fs'); +const path = require('path'); +const os = require('os'); +const { execSync } = require('child_process'); + +const VIBE_HOME = process.env.VIBE_HOME || path.join(os.homedir(), '.vibe'); +const GSD_SRC = path.resolve(__dirname, '..'); +const GSD_DEST = path.join(VIBE_HOME, 'get-shit-done'); + +// Directories to create +const DIRS = [ + path.join(VIBE_HOME, 'skills'), + path.join(VIBE_HOME, 'agents'), + path.join(VIBE_HOME, 'prompts'), + GSD_DEST, + path.join(GSD_DEST, 'bin'), + path.join(GSD_DEST, 'workflows'), + path.join(GSD_DEST, 'templates'), + path.join(GSD_DEST, 'references'), +]; + +// Helper to copy directory recursively +function copyDir(src, dest) { + if (!fs.existsSync(src)) return; + if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true }); + + const entries = fs.readdirSync(src, { withFileTypes: true }); + for (const entry of entries) { + const srcPath = path.join(src, entry.name); + const destPath = path.join(dest, entry.name); + if (entry.isDirectory()) { + copyDir(srcPath, destPath); + } else { + fs.copyFileSync(srcPath, destPath); + } + } +} + +// 1. Setup Directories +function ensureDirectories() { + console.log('Setting up directories...'); + DIRS.forEach(dir => { + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + console.log(`Created ${dir}`); + } + }); +} + +// 2. Copy Core Files +function copyCoreFiles() { + console.log('Copying core GSD files...'); + copyDir(path.join(GSD_SRC, 'get-shit-done/bin'), path.join(GSD_DEST, 'bin')); + copyDir(path.join(GSD_SRC, 'get-shit-done/templates'), path.join(GSD_DEST, 'templates')); + copyDir(path.join(GSD_SRC, 'get-shit-done/references'), path.join(GSD_DEST, 'references')); + + // Make bin executable + try { + if (fs.existsSync(path.join(GSD_DEST, 'bin'))) { + const binFiles = fs.readdirSync(path.join(GSD_DEST, 'bin')); + binFiles.forEach(f => fs.chmodSync(path.join(GSD_DEST, 'bin', f), '755')); + } + } catch (e) { + console.warn('Could not chmod bin files (expected on Windows).'); + } +} + +// 3. Convert Commands to Skills +function convertCommandsToSkills() { + console.log('Converting commands to skills...'); + const commandsDir = path.join(GSD_SRC, 'commands/gsd'); + if (fs.existsSync(commandsDir)) { + const files = fs.readdirSync(commandsDir).filter(f => f.endsWith('.md')); + + files.forEach(file => { + const content = fs.readFileSync(path.join(commandsDir, file), 'utf8'); + const slug = file.replace('.md', ''); + const skillName = `gsd-${slug}`; + const skillDir = path.join(VIBE_HOME, 'skills', skillName); + + if (!fs.existsSync(skillDir)) fs.mkdirSync(skillDir, { recursive: true }); + + // Transform content + let newContent = content + .replace(/~\/\.claude/g, '~/.vibe') + .replace(/name: gsd:([\w-]+)/, `name: gsd-$1\nuser-invocable: true`) + .replace(/Task\(prompt="(?[\s\S]*?)", subagent_type="(?[\w-]+)",.*?\)/g, (match, prompt, agent) => { + return `task(task="${prompt.replace(/"/g, '\\"')}", agent="${agent}")`; + }); + + // Fix allowed tools + const toolMap = { + 'Read': 'read_file', + 'Write': 'write_file', + 'Bash': 'bash', + 'Glob': 'list_dir', + 'Grep': 'grep', + 'AskUserQuestion': 'ask_user_question', + 'Task': 'task' + }; + + newContent = newContent.replace(/allowed-tools:\s*\n((?:\s+-\s+\w+\n?)+)/, (match, toolsBlock) => { + const tools = toolsBlock.split('\n') + .map(t => t.trim().replace('- ', '')) + .filter(t => t) + .map(t => toolMap[t] || t) + .map(t => ` - ${t}`) + .join('\n'); + return `allowed-tools:\n${tools}\n`; + }); + + fs.writeFileSync(path.join(skillDir, 'SKILL.md'), newContent); + console.log(`Created skill: ${skillName}`); + }); + } +} + +// 4. Convert Agents +function convertAgents() { + console.log('Converting agents...'); + const agentsDir = path.join(GSD_SRC, 'agents'); + if (fs.existsSync(agentsDir)) { + const files = fs.readdirSync(agentsDir).filter(f => f.endsWith('.md')); + + files.forEach(file => { + const content = fs.readFileSync(path.join(agentsDir, file), 'utf8'); + const agentName = file.replace('.md', ''); + + // 4.1 Write System Prompt + const promptPath = path.join(VIBE_HOME, 'prompts', `${agentName}.md`); + // Remove frontmatter for the prompt file + const promptContent = content.replace(/^---\n[\s\S]+?\n---\n/, ''); + fs.writeFileSync(promptPath, promptContent); + + // 4.2 Write Agent TOML + const tomlPath = path.join(VIBE_HOME, 'agents', `${agentName}.toml`); + const tomlContent = ` +system_prompt_id = "${agentName}" +active_model = "mistral-large-latest" +agent_type = "subagent" + +[tools] +read_file.permission = "always" +write_file.permission = "always" +bash.permission = "always" +grep.permission = "always" +list_dir.permission = "always" +ask_user_question.permission = "always" +`; + fs.writeFileSync(tomlPath, tomlContent.trim()); + console.log(`Created agent: ${agentName}`); + }); + } +} + + +// 5. Patch Workflows +function patchWorkflows() { + console.log('Patching workflows...'); + const workflowsSrc = path.join(GSD_SRC, 'get-shit-done/workflows'); + const workflowsDest = path.join(GSD_DEST, 'workflows'); + + if (fs.existsSync(workflowsSrc)) { + const files = fs.readdirSync(workflowsSrc).filter(f => f.endsWith('.md')); + + files.forEach(file => { + let content = fs.readFileSync(path.join(workflowsSrc, file), 'utf8'); + + // Patch paths + content = content.replace(/~\/\.claude/g, '~/.vibe'); + + // Patch Task calls + content = content.replace(/Task\(\s*prompt\s*=\s*"([\s\S]*?)",\s*subagent_type\s*=\s*"([^"]+)"(?:,\s*model\s*=\s*"[^"]+")?\s*\)/g, (match, prompt, agent) => { + return `task(task="${prompt}", agent="${agent}")`; + }); + + // Fix path to agent prompts (which are in prompts/ dir, not agents/ dir) + content = content.replace(/~\/\.vibe\/agents\/([a-zA-Z0-9_-]+\.md)/g, '~/.vibe/prompts/$1'); + + fs.writeFileSync(path.join(workflowsDest, file), content); + console.log(`Patched workflow: ${file}`); + }); + } +} + +function install() { + console.log('Starting GSD Vibe installation...'); + + ensureDirectories(); + copyCoreFiles(); + convertCommandsToSkills(); + convertAgents(); + patchWorkflows(); + + console.log('Installation complete! Please restart Vibe CLI.'); + return { runtime: 'vibe', success: true }; +} + +module.exports = { install }; + +if (require.main === module) { + try { + install(); + } catch (err) { + console.error('Installation failed:', err); + process.exit(1); + } +} diff --git a/bin/install.js b/bin/install.js index 9a6f72238..ffc72f3c9 100755 --- a/bin/install.js +++ b/bin/install.js @@ -15,6 +15,7 @@ const reset = '\x1b[0m'; // Get version from package.json const pkg = require('../package.json'); +const { install: installVibe } = require('./install-vibe'); // Parse args const args = process.argv.slice(2); @@ -23,6 +24,7 @@ const hasLocal = args.includes('--local') || args.includes('-l'); const hasOpencode = args.includes('--opencode'); const hasClaude = args.includes('--claude'); const hasGemini = args.includes('--gemini'); +const hasVibe = args.includes('--vibe'); const hasBoth = args.includes('--both'); // Legacy flag, keeps working const hasAll = args.includes('--all'); const hasUninstall = args.includes('--uninstall') || args.includes('-u'); @@ -30,19 +32,21 @@ const hasUninstall = args.includes('--uninstall') || args.includes('-u'); // Runtime selection - can be set by flags or interactive prompt let selectedRuntimes = []; if (hasAll) { - selectedRuntimes = ['claude', 'opencode', 'gemini']; + selectedRuntimes = ['claude', 'opencode', 'gemini', 'vibe']; } else if (hasBoth) { selectedRuntimes = ['claude', 'opencode']; } else { if (hasOpencode) selectedRuntimes.push('opencode'); if (hasClaude) selectedRuntimes.push('claude'); if (hasGemini) selectedRuntimes.push('gemini'); + if (hasVibe) selectedRuntimes.push('vibe'); } // Helper to get directory name for a runtime (used for local/project installs) function getDirName(runtime) { if (runtime === 'opencode') return '.opencode'; if (runtime === 'gemini') return '.gemini'; + if (runtime === 'vibe') return '.vibe'; return '.claude'; } @@ -56,17 +60,17 @@ function getOpencodeGlobalDir() { if (process.env.OPENCODE_CONFIG_DIR) { return expandTilde(process.env.OPENCODE_CONFIG_DIR); } - + // 2. OPENCODE_CONFIG env var (use its directory) if (process.env.OPENCODE_CONFIG) { return path.dirname(expandTilde(process.env.OPENCODE_CONFIG)); } - + // 3. XDG_CONFIG_HOME/opencode if (process.env.XDG_CONFIG_HOME) { return path.join(expandTilde(process.env.XDG_CONFIG_HOME), 'opencode'); } - + // 4. Default: ~/.config/opencode (XDG default) return path.join(os.homedir(), '.config', 'opencode'); } @@ -84,7 +88,7 @@ function getGlobalDir(runtime, explicitDir = null) { } return getOpencodeGlobalDir(); } - + if (runtime === 'gemini') { // Gemini: --config-dir > GEMINI_CONFIG_DIR > ~/.gemini if (explicitDir) { @@ -95,7 +99,14 @@ function getGlobalDir(runtime, explicitDir = null) { } return path.join(os.homedir(), '.gemini'); } - + + if (runtime === 'vibe') { + if (process.env.VIBE_HOME) { + return expandTilde(process.env.VIBE_HOME); + } + return path.join(os.homedir(), '.vibe'); + } + // Claude Code: --config-dir > CLAUDE_CONFIG_DIR > ~/.claude if (explicitDir) { return expandTilde(explicitDir); @@ -560,7 +571,7 @@ function convertClaudeToGeminiToml(content) { const frontmatter = content.substring(3, endIndex).trim(); const body = content.substring(endIndex + 3).trim(); - + // Extract description from frontmatter let description = ''; const lines = frontmatter.split('\n'); @@ -577,9 +588,9 @@ function convertClaudeToGeminiToml(content) { if (description) { toml += `description = ${JSON.stringify(description)}\n`; } - + toml += `prompt = ${JSON.stringify(body)}\n`; - + return toml; } @@ -598,7 +609,7 @@ function copyFlattenedCommands(srcDir, destDir, prefix, pathPrefix, runtime) { if (!fs.existsSync(srcDir)) { return; } - + // Remove old gsd-*.md files before copying new ones if (fs.existsSync(destDir)) { for (const file of fs.readdirSync(destDir)) { @@ -609,12 +620,12 @@ function copyFlattenedCommands(srcDir, destDir, prefix, pathPrefix, runtime) { } else { fs.mkdirSync(destDir, { recursive: true }); } - + const entries = fs.readdirSync(srcDir, { withFileTypes: true }); - + for (const entry of entries) { const srcPath = path.join(srcDir, entry.name); - + if (entry.isDirectory()) { // Recurse into subdirectories, adding to prefix // e.g., commands/gsd/debug/start.md -> command/gsd-debug-start.md @@ -753,8 +764,8 @@ function cleanupOrphanedHooks(settings) { // Fix #330: Update statusLine if it points to old statusline.js path if (settings.statusLine && settings.statusLine.command && - settings.statusLine.command.includes('statusline.js') && - !settings.statusLine.command.includes('gsd-statusline.js')) { + settings.statusLine.command.includes('statusline.js') && + !settings.statusLine.command.includes('gsd-statusline.js')) { // Replace old path with new path settings.statusLine.command = settings.statusLine.command.replace( /statusline\.js/, @@ -875,7 +886,7 @@ function uninstall(isGlobal, runtime = 'claude') { // Remove GSD statusline if it references our hook if (settings.statusLine && settings.statusLine.command && - settings.statusLine.command.includes('gsd-statusline')) { + settings.statusLine.command.includes('gsd-statusline')) { delete settings.statusLine; settingsModified = true; console.log(` ${green}✓${reset} Removed GSD statusline from settings`); @@ -1065,7 +1076,7 @@ function configureOpencodePermissions() { const gsdPath = opencodeConfigDir === defaultConfigDir ? '~/.config/opencode/get-shit-done/*' : `${opencodeConfigDir.replace(/\\/g, '/')}/get-shit-done/*`; - + let modified = false; // Configure read permission @@ -1309,7 +1320,7 @@ function install(isGlobal, runtime = 'claude') { // OpenCode: flat structure in command/ directory const commandDir = path.join(targetDir, 'command'); fs.mkdirSync(commandDir, { recursive: true }); - + // Copy commands/gsd/*.md as command/gsd-*.md (flatten structure) const gsdSrc = path.join(src, 'commands', 'gsd'); copyFlattenedCommands(gsdSrc, commandDir, 'gsd', pathPrefix, runtime); @@ -1323,7 +1334,7 @@ function install(isGlobal, runtime = 'claude') { // Claude Code & Gemini: nested structure in commands/ directory const commandsDir = path.join(targetDir, 'commands'); fs.mkdirSync(commandsDir, { recursive: true }); - + const gsdSrc = path.join(src, 'commands', 'gsd'); const gsdDest = path.join(commandsDir, 'gsd'); copyWithPathReplacement(gsdSrc, gsdDest, pathPrefix, runtime); @@ -1607,6 +1618,7 @@ function promptRuntime(callback) { ${cyan}2${reset}) OpenCode ${dim}(~/.config/opencode)${reset} - open source, free models ${cyan}3${reset}) Gemini ${dim}(~/.gemini)${reset} ${cyan}4${reset}) All + ${cyan}5${reset}) Mistral Vibe `); rl.question(` Choice ${dim}[1]${reset}: `, (answer) => { @@ -1614,11 +1626,13 @@ function promptRuntime(callback) { rl.close(); const choice = answer.trim() || '1'; if (choice === '4') { - callback(['claude', 'opencode', 'gemini']); + callback(['claude', 'opencode', 'gemini', 'vibe']); } else if (choice === '3') { callback(['gemini']); } else if (choice === '2') { callback(['opencode']); + } else if (choice === '5') { + callback(['vibe']); } else { callback(['claude']); } @@ -1677,6 +1691,15 @@ function installAllRuntimes(runtimes, isGlobal, isInteractive) { const results = []; for (const runtime of runtimes) { + if (runtime === 'vibe') { + try { + const result = installVibe(); + results.push(result); + } catch (e) { + console.error('Vibe installation failed', e); + } + continue; + } const result = install(isGlobal, runtime); results.push(result); } @@ -1687,28 +1710,30 @@ function installAllRuntimes(runtimes, isGlobal, isInteractive) { // Logic: if both are present, ask once if interactive? Or ask for each? // Simpler: Ask once and apply to both if applicable. - + if (claudeResult || geminiResult) { // Use whichever settings exist to check for existing statusline const primaryResult = claudeResult || geminiResult; - + handleStatusline(primaryResult.settings, isInteractive, (shouldInstallStatusline) => { if (claudeResult) { finishInstall(claudeResult.settingsPath, claudeResult.settings, claudeResult.statuslineCommand, shouldInstallStatusline, 'claude'); } if (geminiResult) { - finishInstall(geminiResult.settingsPath, geminiResult.settings, geminiResult.statuslineCommand, shouldInstallStatusline, 'gemini'); + finishInstall(geminiResult.settingsPath, geminiResult.settings, geminiResult.statuslineCommand, shouldInstallStatusline, 'gemini'); } - + const opencodeResult = results.find(r => r.runtime === 'opencode'); if (opencodeResult) { finishInstall(opencodeResult.settingsPath, opencodeResult.settings, opencodeResult.statuslineCommand, false, 'opencode'); } }); } else { - // Only OpenCode - const opencodeResult = results[0]; - finishInstall(opencodeResult.settingsPath, opencodeResult.settings, opencodeResult.statuslineCommand, false, 'opencode'); + // Only OpenCode or Vibe + const opencodeResult = results.find(r => r.runtime === 'opencode'); + if (opencodeResult) { + finishInstall(opencodeResult.settingsPath, opencodeResult.settings, opencodeResult.statuslineCommand, false, 'opencode'); + } } } diff --git a/package.json b/package.json index e02b706c3..5a6103338 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,9 @@ "context-engineering", "spec-driven-development", "gemini", - "gemini-cli" + "gemini-cli", + "mistral-vibe", + "vibe-cli" ], "author": "TÂCHES", "license": "MIT",