feat: Add Mistral Vibe CLI Support (#519)
feat: Add Mistral Vibe CLI support
This commit is contained in:
11
agents/general-purpose.md
Normal file
11
agents/general-purpose.md
Normal file
@@ -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.
|
||||
208
bin/install-vibe.js
Normal file
208
bin/install-vibe.js
Normal file
@@ -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="(?<prompt>[\s\S]*?)", subagent_type="(?<agent>[\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);
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,9 @@
|
||||
"context-engineering",
|
||||
"spec-driven-development",
|
||||
"gemini",
|
||||
"gemini-cli"
|
||||
"gemini-cli",
|
||||
"mistral-vibe",
|
||||
"vibe-cli"
|
||||
],
|
||||
"author": "TÂCHES",
|
||||
"license": "MIT",
|
||||
|
||||
Reference in New Issue
Block a user