UNPKG

metis-code

Version:

Metis Code - multi-model coding CLI agent

1,464 lines (1,437 loc) 495 kB
#!/usr/bin/env node "use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __esm = (fn, res) => function __init() { return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res; }; var __commonJS = (cb, mod) => function __require() { return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; }; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); // src/tools/files.ts function ensureDirFor(filePath) { const dir = import_path.default.dirname(filePath); if (!import_fs.default.existsSync(dir)) import_fs.default.mkdirSync(dir, { recursive: true }); } function writeText(filePath, content) { ensureDirFor(filePath); import_fs.default.writeFileSync(filePath, content); } function listFiles(root, ignore = []) { const out = []; const relRoot = import_path.default.resolve(root); const ig = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".metis", ...ignore.map((g) => g.replace("/**", ""))]); function walk(dir) { const entries = import_fs.default.readdirSync(dir, { withFileTypes: true }); for (const e of entries) { const abs = import_path.default.join(dir, e.name); const rel = import_path.default.relative(relRoot, abs); if (e.isDirectory()) { if (ig.has(e.name)) continue; walk(abs); } else if (e.isFile()) { out.push(rel); } } } walk(relRoot); return out; } function withinCwdSafe(targetPath, cwd = process.cwd()) { const abs = import_path.default.resolve(cwd, targetPath); const root = import_path.default.resolve(cwd); return abs.startsWith(root + import_path.default.sep) || abs === root; } var import_fs, import_path; var init_files = __esm({ "src/tools/files.ts"() { "use strict"; import_fs = __toESM(require("fs")); import_path = __toESM(require("path")); } }); // src/config/index.ts function getGlobalConfigDir() { const homeDir = import_os.default.homedir(); return import_path2.default.join(homeDir, ".metis"); } function getGlobalSecretsPath() { return import_path2.default.join(getGlobalConfigDir(), "secrets.json"); } function getGlobalConfigPath() { return import_path2.default.join(getGlobalConfigDir(), "config.json"); } function loadConfig() { const globalConfigPath = getGlobalConfigPath(); let base = { provider: process.env.METIS_PROVIDER || "", model: process.env.METIS_MODEL || "", temperature: process.env.METIS_TEMPERATURE ? Number(process.env.METIS_TEMPERATURE) : 0.2, safety: { dryRun: false, requireExecApproval: true }, ignore: ["node_modules/**", ".git/**", "dist/**", ".metis/sessions/**"] }; if (import_fs2.default.existsSync(globalConfigPath)) { try { const disk = JSON.parse(import_fs2.default.readFileSync(globalConfigPath, "utf8")); base = { ...base, ...disk }; } catch (e) { console.warn("Failed to parse global config; using defaults."); } } return base; } function saveGlobalConfig(config) { const globalConfigPath = getGlobalConfigPath(); const globalDir = getGlobalConfigDir(); if (!import_fs2.default.existsSync(globalDir)) { import_fs2.default.mkdirSync(globalDir, { recursive: true }); } import_fs2.default.writeFileSync(globalConfigPath, JSON.stringify(config, null, 2)); } function getGlobalConfigLocation() { return getGlobalConfigPath(); } function loadSecrets(cwd = process.cwd()) { const out = {}; if (process.env.OPENAI_API_KEY) out.openai = process.env.OPENAI_API_KEY; if (process.env.ANTHROPIC_API_KEY) out.anthropic = process.env.ANTHROPIC_API_KEY; if (process.env.GROQ_API_KEY) out.groq = process.env.GROQ_API_KEY; const globalSecretsPath = getGlobalSecretsPath(); if (import_fs2.default.existsSync(globalSecretsPath)) { try { const globalSecrets = JSON.parse(import_fs2.default.readFileSync(globalSecretsPath, "utf8")); for (const [key, value] of Object.entries(globalSecrets)) { if (!(key in out)) { out[key] = value; } } } catch { } } const localMetisDir = import_path2.default.join(cwd, ".metis"); const localSecretsPath = import_path2.default.join(localMetisDir, "secrets.json"); if (import_fs2.default.existsSync(localSecretsPath)) { try { const localSecrets = JSON.parse(import_fs2.default.readFileSync(localSecretsPath, "utf8")); for (const [key, value] of Object.entries(localSecrets)) { if (!(key in out)) { out[key] = value; } } } catch { } } return out; } function saveGlobalSecrets(secrets) { const globalConfigDir = getGlobalConfigDir(); const globalSecretsPath = getGlobalSecretsPath(); if (!import_fs2.default.existsSync(globalConfigDir)) { import_fs2.default.mkdirSync(globalConfigDir, { recursive: true }); } let existingSecrets = {}; if (import_fs2.default.existsSync(globalSecretsPath)) { try { existingSecrets = JSON.parse(import_fs2.default.readFileSync(globalSecretsPath, "utf8")); } catch { } } const mergedSecrets = { ...existingSecrets, ...secrets }; import_fs2.default.writeFileSync(globalSecretsPath, JSON.stringify(mergedSecrets, null, 2) + "\n"); try { import_fs2.default.chmodSync(globalSecretsPath, 384); } catch { } } function getGlobalSecretsLocation() { return getGlobalSecretsPath(); } var import_fs2, import_path2, import_os; var init_config = __esm({ "src/config/index.ts"() { "use strict"; import_fs2 = __toESM(require("fs")); import_path2 = __toESM(require("path")); import_os = __toESM(require("os")); } }); // src/tools/repo.ts function scanRepo(cwd = process.cwd()) { const cfg = loadConfig(cwd); const files = listFiles(cwd, cfg.ignore); const byExt = {}; for (const f of files) { const ext = import_path3.default.extname(f) || "(noext)"; byExt[ext] = (byExt[ext] || 0) + 1; } let scripts; const pkg2 = import_path3.default.join(cwd, "package.json"); if (import_fs3.default.existsSync(pkg2)) { try { const data = JSON.parse(import_fs3.default.readFileSync(pkg2, "utf8")); if (data?.scripts) scripts = data.scripts; } catch { } } return { root: cwd, files, counts: { total: files.length, byExt }, scripts }; } function summarizeRepo(maxFiles = 60, cwd = process.cwd()) { const r = scanRepo(cwd); const top = r.files.slice(0, maxFiles); const extCounts = Object.entries(r.counts.byExt).sort((a, b) => b[1] - a[1]).slice(0, 10).map(([ext, n]) => `${ext}:${n}`).join(", "); const scripts = r.scripts ? Object.keys(r.scripts).slice(0, 10).map((k) => `${k}`).join(", ") : "none"; return [ `Files: ${r.counts.total} total; top extensions: ${extCounts}`, `package.json scripts: ${scripts}`, `Sample files (${top.length}):`, ...top.map((f) => `- ${f}`) ].join("\n"); } var import_fs3, import_path3; var init_repo = __esm({ "src/tools/repo.ts"() { "use strict"; import_fs3 = __toESM(require("fs")); import_path3 = __toESM(require("path")); init_files(); init_config(); } }); // src/errors/MetisError.ts var MetisError; var init_MetisError = __esm({ "src/errors/MetisError.ts"() { "use strict"; MetisError = class _MetisError extends Error { constructor(message, code, category, recoverable = true, suggestions = []) { super(message); this.name = "MetisError"; this.code = code; this.category = category; this.recoverable = recoverable; this.suggestions = suggestions; } static configMissing(configPath) { return new _MetisError( `Configuration file not found: ${configPath}`, "CONFIG_MISSING", "config", true, [ 'Run "metiscode init" to create initial configuration', "Create metis.config.json manually in your project root" ] ); } static apiKeyMissing(provider) { return new _MetisError( `API key missing for provider: ${provider}`, "API_KEY_MISSING", "config", true, [ `Run "metiscode auth set --provider ${provider} --key YOUR_API_KEY"`, `Set ${provider.toUpperCase()}_API_KEY environment variable`, "Check that .metis/secrets.json exists and contains your API key" ] ); } static toolExecutionFailed(toolName, reason) { return new _MetisError( `Tool '${toolName}' execution failed: ${reason}`, "TOOL_EXECUTION_FAILED", "tool", true, [ "Try running the tool individually to debug the issue", "Check that required files and directories exist", "Verify tool parameters are correct" ] ); } static providerRequestFailed(provider, httpCode) { const codeMsg = httpCode ? ` (HTTP ${httpCode})` : ""; return new _MetisError( `${provider} API request failed${codeMsg}`, "PROVIDER_REQUEST_FAILED", "provider", true, [ "Check your internet connection", "Verify your API key is valid and has sufficient credits", "Try again in a few moments - the service may be temporarily unavailable", httpCode === 429 ? "You may have hit rate limits - wait before retrying" : "" ].filter(Boolean) ); } static fileNotFound(path27) { return new _MetisError( `File not found: ${path27}`, "FILE_NOT_FOUND", "user", true, [ "Check that the file path is correct", "Ensure the file exists in your project", "Use relative paths from your project root" ] ); } static taskTooComplex() { return new _MetisError( "Task did not complete within maximum iterations", "TASK_TOO_COMPLEX", "agent", true, [ "Break your task into smaller, more specific steps", "Try being more explicit about what files to modify", "Run individual operations separately" ] ); } static unsupportedProvider(provider) { return new _MetisError( `Unsupported provider: ${provider}`, "UNSUPPORTED_PROVIDER", "config", false, [ "Supported providers: openai, anthropic", "Check your metis.config.json file", "Update to a supported provider" ] ); } toUserFriendlyString() { let msg = `\u274C ${this.message}`; if (this.suggestions.length > 0) { msg += "\n\nSuggestions:"; this.suggestions.forEach((suggestion) => { msg += ` \u2022 ${suggestion}`; }); } if (this.recoverable) { msg += "\n\n\u{1F4A1} This issue can usually be resolved by following the suggestions above."; } return msg; } }; } }); // src/providers/openai.ts var import_openai, OpenAIProvider; var init_openai = __esm({ "src/providers/openai.ts"() { "use strict"; import_openai = __toESM(require("openai")); init_MetisError(); OpenAIProvider = class { constructor(init) { this.name = "openai"; if (!init.apiKey) { console.error("\u274C OpenAI API key not configured"); console.error("To fix this, run: metiscode config set apikey your-api-key"); this.client = {}; this.model = init.model || "gpt-4o"; this.temperature = init.temperature || 0.2; return; } try { this.client = new import_openai.default({ apiKey: init.apiKey }); this.model = init.model; this.temperature = init.temperature; } catch (error) { throw MetisError.providerRequestFailed("openai"); } } async send(messages, opts) { const temperature = opts?.temperature ?? this.temperature; try { const requestConfig = { model: this.model, temperature, messages: messages.map((m) => ({ role: m.role, content: m.content, tool_calls: m.tool_calls, tool_call_id: m.tool_call_id, name: m.name })) }; const res = await this.client.chat.completions.create(requestConfig); const choice = res.choices?.[0]?.message?.content ?? ""; return typeof choice === "string" ? choice : JSON.stringify(choice); } catch (error) { if (error.status) { throw MetisError.providerRequestFailed("openai", error.status); } throw MetisError.providerRequestFailed("openai"); } } async sendWithTools(messages, tools, opts) { const temperature = opts?.temperature ?? this.temperature; try { const requestConfig = { model: this.model, temperature, max_tokens: opts?.max_tokens, messages: messages.map((m) => ({ role: m.role, content: m.content, tool_calls: m.tool_calls, tool_call_id: m.tool_call_id, name: m.name })), tools: tools.map((tool) => ({ type: "function", function: { name: tool.name, description: tool.description, parameters: tool.parameters } })), tool_choice: "auto" }; const res = await this.client.chat.completions.create(requestConfig); const message = res.choices?.[0]?.message; if (!message) { throw MetisError.providerRequestFailed("openai"); } if (message.tool_calls && message.tool_calls.length > 0) { return { type: "tool_call", content: message.content || "", tool_calls: message.tool_calls.map((tc) => ({ id: tc.id, type: "function", function: { name: tc.function.name, arguments: tc.function.arguments } })), usage: res.usage ? { prompt_tokens: res.usage.prompt_tokens, completion_tokens: res.usage.completion_tokens, total_tokens: res.usage.total_tokens } : void 0 }; } return { type: "text", content: message.content || "", usage: res.usage ? { prompt_tokens: res.usage.prompt_tokens, completion_tokens: res.usage.completion_tokens, total_tokens: res.usage.total_tokens } : void 0 }; } catch (error) { if (error.status) { throw MetisError.providerRequestFailed("openai", error.status); } throw MetisError.providerRequestFailed("openai"); } } supportsTools() { return !this.model.includes("gpt-3.5-turbo-instruct"); } }; } }); // src/providers/anthropic.ts var import_sdk, AnthropicProvider; var init_anthropic = __esm({ "src/providers/anthropic.ts"() { "use strict"; import_sdk = __toESM(require("@anthropic-ai/sdk")); AnthropicProvider = class { constructor(init) { this.name = "anthropic"; if (!init.apiKey) throw new Error("ANTHROPIC_API_KEY missing"); this.client = new import_sdk.default({ apiKey: init.apiKey }); this.model = init.model; this.temperature = init.temperature; } async send(messages, opts) { const temperature = opts?.temperature ?? this.temperature; const sys = messages.find((m) => m.role === "system")?.content; const userAssistantPairs = messages.filter((m) => m.role !== "system"); const content = userAssistantPairs.map((m) => ({ role: m.role === "assistant" ? "assistant" : "user", content: m.content })); const res = await this.client.messages.create({ model: this.model, temperature, system: sys, max_tokens: 1024, messages: content }); const text = res.content.map((c) => c.type === "text" ? c.text : "").join("").trim(); return text; } async sendWithTools(messages, tools, opts) { const temperature = opts?.temperature ?? this.temperature; const sys = messages.find((m) => m.role === "system")?.content; const userAssistantPairs = messages.filter((m) => m.role !== "system"); const content = userAssistantPairs.map((m) => { if (m.role === "tool") { return { role: "user", content: `Tool ${m.name || "unknown"} result: ${m.content}` }; } return { role: m.role === "assistant" ? "assistant" : "user", content: m.content }; }); const anthropicTools = tools.map((tool) => ({ name: tool.name, description: tool.description, input_schema: tool.parameters })); const res = await this.client.messages.create({ model: this.model, temperature, system: sys, max_tokens: opts?.max_tokens || 1024, messages: content, tools: anthropicTools }); const toolUseBlocks = res.content.filter((c) => c.type === "tool_use"); const textBlocks = res.content.filter((c) => c.type === "text"); if (toolUseBlocks.length > 0) { const toolCalls = toolUseBlocks.map((block) => ({ id: block.id, type: "function", function: { name: block.name, arguments: JSON.stringify(block.input) } })); return { type: "tool_call", content: textBlocks.map((c) => c.text).join("").trim(), tool_calls: toolCalls, usage: res.usage ? { prompt_tokens: res.usage.input_tokens, completion_tokens: res.usage.output_tokens, total_tokens: res.usage.input_tokens + res.usage.output_tokens } : void 0 }; } return { type: "text", content: textBlocks.map((c) => c.text).join("").trim(), usage: res.usage ? { prompt_tokens: res.usage.input_tokens, completion_tokens: res.usage.output_tokens, total_tokens: res.usage.input_tokens + res.usage.output_tokens } : void 0 }; } supportsTools() { return this.model.includes("claude-3") || this.model.includes("claude-sonnet") || this.model.includes("claude-haiku"); } }; } }); // src/providers/groq.ts var GroqProvider; var init_groq = __esm({ "src/providers/groq.ts"() { "use strict"; init_MetisError(); GroqProvider = class { constructor(init) { this.name = "groq"; this.baseURL = "https://api.groq.com/openai/v1"; if (!init.apiKey) { throw MetisError.apiKeyMissing("groq"); } this.apiKey = init.apiKey; this.model = init.model; this.temperature = init.temperature; } async send(messages, opts) { const temperature = opts?.temperature ?? this.temperature; try { const response = await this.makeRequest("/chat/completions", { model: this.model, temperature, messages: messages.map((m) => ({ role: m.role, content: m.content, tool_calls: m.tool_calls, tool_call_id: m.tool_call_id, name: m.name })) }); const choice = response.choices?.[0]?.message?.content ?? ""; return typeof choice === "string" ? choice : JSON.stringify(choice); } catch (error) { if (error.status) { throw MetisError.providerRequestFailed("groq", error.status); } throw MetisError.providerRequestFailed("groq"); } } async sendWithTools(messages, tools, opts) { const temperature = opts?.temperature ?? this.temperature; try { const requestConfig = { model: this.model, temperature, max_tokens: opts?.max_tokens, messages: messages.map((m) => ({ role: m.role, content: m.content, tool_calls: m.tool_calls, tool_call_id: m.tool_call_id, name: m.name })), tools: tools.map((tool) => ({ type: "function", function: { name: tool.name, description: tool.description, parameters: tool.parameters } })), tool_choice: "auto" }; if (this.supportsAdvancedFeatures()) { requestConfig.service_tier = "on_demand"; } const response = await this.makeRequest("/chat/completions", requestConfig); const message = response.choices?.[0]?.message; if (!message) { throw MetisError.providerRequestFailed("groq"); } if (message.tool_calls && message.tool_calls.length > 0) { return { type: "tool_call", content: message.content || "", tool_calls: message.tool_calls.map((tc) => ({ id: tc.id, type: "function", function: { name: tc.function.name, arguments: tc.function.arguments } })), usage: response.usage ? { prompt_tokens: response.usage.prompt_tokens, completion_tokens: response.usage.completion_tokens, total_tokens: response.usage.total_tokens } : void 0 }; } return { type: "text", content: message.content || "", usage: response.usage ? { prompt_tokens: response.usage.prompt_tokens, completion_tokens: response.usage.completion_tokens, total_tokens: response.usage.total_tokens } : void 0 }; } catch (error) { if (error.status) { throw MetisError.providerRequestFailed("groq", error.status); } throw MetisError.providerRequestFailed("groq"); } } supportsTools() { const toolSupportedModels = [ "llama-3.1-70b-versatile", "llama-3.1-8b-instant", "llama-3.3-70b-versatile", "mixtral-8x7b-32768", "gemma2-9b-it", "gemma-7b-it" ]; return toolSupportedModels.some((model) => this.model.includes(model)) || this.model.includes("tool-use") || this.model.includes("function"); } supportsAdvancedFeatures() { return this.model.includes("llama-4") || this.model.includes("mixtral") || this.model.includes("gemma2"); } async makeRequest(endpoint, body) { const url = `${this.baseURL}${endpoint}`; try { const response = await fetch(url, { method: "POST", headers: { "Authorization": `Bearer ${this.apiKey}`, "Content-Type": "application/json" }, body: JSON.stringify(body) }); if (!response.ok) { const errorText = await response.text(); let errorMessage = `HTTP ${response.status}`; try { const errorJson = JSON.parse(errorText); errorMessage = errorJson.error?.message || errorMessage; } catch { } throw { status: response.status, message: errorMessage }; } return await response.json(); } catch (error) { if (error.status) { throw error; } throw { message: error.message || "Network error" }; } } // Helper method to get available models static getAvailableModels() { return [ // Llama models "llama3-groq-70b-8192-tool-use-preview", "llama3-groq-8b-8192-tool-use-preview", "meta-llama/llama-4-scout-17b-16e-instruct", "llama-3.3-70b-versatile", "llama-3.1-70b-versatile", "llama-3.1-8b-instant", // Mixtral models "mixtral-8x7b-32768", // Gemma models "gemma2-9b-it", "gemma-7b-it", // Qwen models "qwen2.5-72b-instruct", // DeepSeek models "deepseek-r1-distill-llama-70b" ]; } }; } }); // src/assets/loader.ts var import_fs4, import_path4, import_js_yaml, AssetLoader; var init_loader = __esm({ "src/assets/loader.ts"() { "use strict"; import_fs4 = __toESM(require("fs")); import_path4 = __toESM(require("path")); import_js_yaml = __toESM(require("js-yaml")); AssetLoader = class { constructor(basePath = process.cwd()) { this.basePath = import_path4.default.join(basePath, ".metis"); } // Persona loading async loadPersona(name) { const personaPath = import_path4.default.join(this.basePath, "personas", `${name}.yaml`); if (!import_fs4.default.existsSync(personaPath)) { const builtinPath = import_path4.default.join(__dirname, "..", "..", "assets", "personas", `${name}.yaml`); if (!import_fs4.default.existsSync(builtinPath)) { throw new Error(`Persona not found: ${name}`); } return this.parsePersonaFile(builtinPath); } return this.parsePersonaFile(personaPath); } async listPersonas() { const personas = []; const workspaceDir = import_path4.default.join(this.basePath, "personas"); if (import_fs4.default.existsSync(workspaceDir)) { const files = import_fs4.default.readdirSync(workspaceDir).filter((f) => f.endsWith(".yaml") || f.endsWith(".yml")).map((f) => import_path4.default.basename(f, import_path4.default.extname(f))); personas.push(...files); } const builtinDir = import_path4.default.join(__dirname, "..", "..", "assets", "personas"); if (import_fs4.default.existsSync(builtinDir)) { const files = import_fs4.default.readdirSync(builtinDir).filter((f) => f.endsWith(".yaml") || f.endsWith(".yml")).map((f) => import_path4.default.basename(f, import_path4.default.extname(f))); personas.push(...files.filter((f) => !personas.includes(f))); } return personas; } // Workflow loading async loadWorkflow(name) { const workflowPath = import_path4.default.join(this.basePath, "workflows", `${name}.yaml`); if (!import_fs4.default.existsSync(workflowPath)) { throw new Error(`Workflow not found: ${name}`); } const content = import_fs4.default.readFileSync(workflowPath, "utf8"); const workflow = import_js_yaml.default.load(content); if (!workflow.name || !workflow.steps) { throw new Error(`Invalid workflow format: ${name}`); } return workflow; } async listWorkflows() { const workflowsDir = import_path4.default.join(this.basePath, "workflows"); if (!import_fs4.default.existsSync(workflowsDir)) { return []; } return import_fs4.default.readdirSync(workflowsDir).filter((f) => f.endsWith(".yaml") || f.endsWith(".yml")).map((f) => import_path4.default.basename(f, import_path4.default.extname(f))); } // Skill loading async loadSkill(name) { const skillPath = import_path4.default.join(this.basePath, "skills", `${name}.yaml`); if (!import_fs4.default.existsSync(skillPath)) { throw new Error(`Skill not found: ${name}`); } const content = import_fs4.default.readFileSync(skillPath, "utf8"); const skill = import_js_yaml.default.load(content); if (!skill.name || !skill.tools) { throw new Error(`Invalid skill format: ${name}`); } return skill; } async listSkills() { const skillsDir = import_path4.default.join(this.basePath, "skills"); if (!import_fs4.default.existsSync(skillsDir)) { return []; } return import_fs4.default.readdirSync(skillsDir).filter((f) => f.endsWith(".yaml") || f.endsWith(".yml")).map((f) => import_path4.default.basename(f, import_path4.default.extname(f))); } parsePersonaFile(filePath) { const content = import_fs4.default.readFileSync(filePath, "utf8"); const persona = import_js_yaml.default.load(content); if (!persona.name || !persona.system_prompt) { throw new Error(`Invalid persona format: ${filePath}`); } return persona; } // Utility methods async validateAsset(type, name) { try { switch (type) { case "persona": await this.loadPersona(name); break; case "workflow": await this.loadWorkflow(name); break; case "skill": await this.loadSkill(name); break; } return true; } catch { return false; } } async createPersona(persona, overwrite = false) { const personaPath = import_path4.default.join(this.basePath, "personas", `${persona.name}.yaml`); if (import_fs4.default.existsSync(personaPath) && !overwrite) { throw new Error(`Persona already exists: ${persona.name}`); } const dir = import_path4.default.dirname(personaPath); if (!import_fs4.default.existsSync(dir)) { import_fs4.default.mkdirSync(dir, { recursive: true }); } const yamlContent = import_js_yaml.default.dump(persona, { indent: 2 }); import_fs4.default.writeFileSync(personaPath, yamlContent); } }; } }); // src/agent/simpleAgent.ts var simpleAgent_exports = {}; __export(simpleAgent_exports, { makeProvider: () => makeProvider, runSimpleAgent: () => runSimpleAgent }); function makeProvider() { const cfg = loadConfig(); const secrets = loadSecrets(); const base = { model: cfg.model, temperature: cfg.temperature }; if (cfg.provider === "openai") { return new OpenAIProvider({ ...base, apiKey: secrets.openai || process.env.OPENAI_API_KEY }); } if (cfg.provider === "anthropic") { return new AnthropicProvider({ ...base, apiKey: secrets.anthropic || process.env.ANTHROPIC_API_KEY }); } if (cfg.provider === "groq") { return new GroqProvider({ ...base, apiKey: secrets.groq || process.env.GROQ_API_KEY }); } throw new Error(`Unknown provider in config: ${cfg.provider}`); } async function runSimpleAgent(mode, task) { const cfg = loadConfig(); const provider = makeProvider(); const repoSummary = summarizeRepo(60); const personaName = process.env.METIS_PERSONA || "default"; const loader = new AssetLoader(); let persona; try { persona = await loader.loadPersona(personaName); if (process.env.METIS_VERBOSE === "true") { console.log(`Using persona: ${persona.name} - ${persona.description}`); } } catch (error) { console.warn(`Failed to load persona '${personaName}': ${error.message}`); console.warn("Falling back to default behavior"); persona = { name: "fallback", version: "1.0", description: "Fallback persona", system_prompt: "You are Metis, a helpful coding assistant.", temperature: cfg.temperature }; } const systemPrompt = buildSystemPrompt(mode, persona, repoSummary); const messages = [ { role: "system", content: systemPrompt }, { role: "user", content: task.trim() || "Plan repository changes for the task." } ]; const temperature = persona.temperature !== void 0 ? persona.temperature : cfg.temperature; const out = await provider.send(messages, { temperature }); return out.trim(); } function buildSystemPrompt(mode, persona, repoSummary) { if (mode === "plan") { return `${persona.system_prompt} Your task is to propose a clear, minimal plan of steps to implement the user's request in this repository. Prefer diffs and focused changes. Repository summary: ${repoSummary}`; } else { return `${persona.system_prompt} Your task is to produce specific, minimal file-level changes and a patch. Format strictly as a Metis Patch: *** Begin Patch *** Add File: path/relative/to/repo.ext <full new file content> *** Update File: another/path.ext <full updated file content> *** Delete File: another/path.ext *** End Patch Rules: - For Add/Update, include the FULL file content exactly as it should be saved. - Do not include code fences or explanations outside the patch envelope. - Only touch files needed for the task. - Use POSIX newlines. Repository summary: ${repoSummary}`; } } var init_simpleAgent = __esm({ "src/agent/simpleAgent.ts"() { "use strict"; init_config(); init_openai(); init_anthropic(); init_groq(); init_repo(); init_loader(); } }); // src/mcp/transport/stdio.ts var import_events, import_child_process8, StdioTransport; var init_stdio = __esm({ "src/mcp/transport/stdio.ts"() { "use strict"; import_events = require("events"); import_child_process8 = require("child_process"); StdioTransport = class extends import_events.EventEmitter { constructor(config) { super(); this.config = config; this.process = null; this.buffer = ""; this.isConnected = false; } async connect() { if (this.isConnected) { return; } return new Promise((resolve, reject) => { try { this.process = (0, import_child_process8.spawn)(this.config.command, this.config.args || [], { stdio: ["pipe", "pipe", "pipe"], env: { ...process.env, ...this.config.env }, cwd: this.config.cwd }); this.process.on("error", (error) => { this.emit("error", error); reject(error); }); this.process.on("exit", (code, signal) => { this.isConnected = false; this.emit("disconnect", { code, signal }); }); this.process.stdout.on("data", (chunk) => { this.buffer += chunk.toString(); this.processBuffer(); }); this.process.stderr.on("data", (chunk) => { this.emit("stderr", chunk.toString()); }); this.isConnected = true; this.emit("connect"); resolve(); } catch (error) { reject(error); } }); } processBuffer() { const lines = this.buffer.split("\n"); this.buffer = lines.pop() || ""; for (const line of lines) { if (line.trim()) { try { const message = JSON.parse(line); this.emit("message", message); } catch (error) { this.emit("error", new Error(`Invalid JSON: ${line}`)); } } } } async send(message) { if (!this.isConnected || !this.process?.stdin?.writable) { throw new Error("Transport not connected"); } const jsonMessage = JSON.stringify(message) + "\n"; return new Promise((resolve, reject) => { this.process.stdin.write(jsonMessage, (error) => { if (error) { reject(error); } else { resolve(); } }); }); } async close() { if (!this.isConnected || !this.process) { return; } return new Promise((resolve) => { if (this.process) { this.process.on("exit", () => { this.isConnected = false; this.emit("disconnect"); resolve(); }); this.process.stdin?.end(); setTimeout(() => { if (this.process && !this.process.killed) { this.process.kill("SIGTERM"); setTimeout(() => { if (this.process && !this.process.killed) { this.process.kill("SIGKILL"); } }, 2e3); } }, 5e3); } else { resolve(); } }); } isConnected() { return this.isConnected && this.process !== null && !this.process.killed; } }; } }); // src/mcp/transport/websocket.ts var import_events2, import_ws, WebSocketTransport; var init_websocket = __esm({ "src/mcp/transport/websocket.ts"() { "use strict"; import_events2 = require("events"); import_ws = require("ws"); WebSocketTransport = class extends import_events2.EventEmitter { constructor(config) { super(); this.config = config; this.ws = null; this.isConnected = false; this.reconnectCount = 0; this.reconnectTimer = null; this.config.reconnectAttempts = this.config.reconnectAttempts ?? 3; this.config.reconnectDelay = this.config.reconnectDelay ?? 1e3; } async connect() { if (this.isConnected) { return; } return new Promise((resolve, reject) => { try { this.ws = new import_ws.WebSocket(this.config.url, this.config.protocols, { headers: this.config.headers }); this.ws.on("open", () => { this.isConnected = true; this.reconnectCount = 0; this.emit("connect"); resolve(); }); this.ws.on("message", (data) => { try { const message = JSON.parse(data.toString()); this.emit("message", message); } catch (error) { this.emit("error", new Error(`Invalid JSON: ${data.toString()}`)); } }); this.ws.on("error", (error) => { this.emit("error", error); if (!this.isConnected) { reject(error); } else { this.handleDisconnect(); } }); this.ws.on("close", (code, reason) => { this.isConnected = false; this.emit("disconnect", { code, reason: reason.toString() }); if (this.shouldReconnect(code)) { this.scheduleReconnect(); } }); } catch (error) { reject(error); } }); } async send(message) { if (!this.isConnected || !this.ws || this.ws.readyState !== import_ws.WebSocket.OPEN) { throw new Error("Transport not connected"); } const jsonMessage = JSON.stringify(message); return new Promise((resolve, reject) => { this.ws.send(jsonMessage, (error) => { if (error) { reject(error); } else { resolve(); } }); }); } async close() { if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); this.reconnectTimer = null; } if (!this.isConnected || !this.ws) { return; } return new Promise((resolve) => { if (this.ws) { this.ws.on("close", () => { this.isConnected = false; this.emit("disconnect"); resolve(); }); this.ws.close(1e3, "Client disconnect"); } else { resolve(); } }); } isConnected() { return this.isConnected && this.ws !== null && this.ws.readyState === import_ws.WebSocket.OPEN; } handleDisconnect() { this.isConnected = false; if (this.reconnectCount < this.config.reconnectAttempts) { this.scheduleReconnect(); } } shouldReconnect(code) { return code !== 1e3 && code !== 1008 && this.reconnectCount < this.config.reconnectAttempts; } scheduleReconnect() { if (this.reconnectTimer) { return; } const delay = this.config.reconnectDelay * Math.pow(2, this.reconnectCount); this.reconnectTimer = setTimeout(async () => { this.reconnectTimer = null; this.reconnectCount++; try { await this.connect(); } catch (error) { this.emit("reconnectFailed", error); if (this.reconnectCount < this.config.reconnectAttempts) { this.scheduleReconnect(); } } }, delay); } }; } }); // src/mcp/transport/http.ts var import_events3, HttpTransport; var init_http = __esm({ "src/mcp/transport/http.ts"() { "use strict"; import_events3 = require("events"); HttpTransport = class extends import_events3.EventEmitter { constructor(config) { super(); this.config = config; this.isConnected = false; this.config.timeout = this.config.timeout ?? 3e4; this.config.method = this.config.method ?? "POST"; } async connect() { if (this.isConnected) { return; } try { const testResponse = await this.makeRequest({ jsonrpc: "2.0", id: 1, method: "ping" }); this.isConnected = true; this.emit("connect"); } catch (error) { throw new Error(`HTTP transport connection failed: ${error}`); } } async send(message) { if (!this.isConnected) { throw new Error("Transport not connected"); } try { const response = await this.makeRequest(message); if (response) { this.emit("message", response); } } catch (error) { this.emit("error", error); throw error; } } async close() { if (!this.isConnected) { return; } this.isConnected = false; this.emit("disconnect"); } isConnected() { return this.isConnected; } async makeRequest(message) { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.config.timeout); try { const response = await fetch(this.config.endpoint, { method: this.config.method, headers: { "Content-Type": "application/json", ...this.config.headers }, body: JSON.stringify(message), signal: controller.signal }); clearTimeout(timeoutId); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const responseText = await response.text(); if (!responseText.trim()) { return null; } try { return JSON.parse(responseText); } catch (parseError) { throw new Error(`Invalid JSON response: ${responseText}`); } } catch (error) { clearTimeout(timeoutId); if (error instanceof Error && error.name === "AbortError") { throw new Error("Request timeout"); } throw error; } } }; } }); // src/mcp/transport/index.ts var transport_exports = {}; __export(transport_exports, { HttpTransport: () => HttpTransport, StdioTransport: () => StdioTransport, WebSocketTransport: () => WebSocketTransport, createTransport: () => createTransport }); function createTransport(config) { switch (config.type) { case "stdio": return new StdioTransport({ command: config.command, args: config.args, env: config.env, cwd: config.cwd }); case "websocket": return new WebSocketTransport({ url: config.url, protocols: config.protocols, headers: config.headers, reconnectAttempts: config.reconnectAttempts, reconnectDelay: config.reconnectDelay }); case "http": return new HttpTransport({ endpoint: config.endpoint, headers: config.headers, timeout: config.timeout, method: config.method }); default: throw new Error(`Unknown transport type: ${config.type}`); } } var init_transport = __esm({ "src/mcp/transport/index.ts"() { "use strict"; init_stdio(); init_websocket(); init_http(); init_stdio(); init_websocket(); init_http(); } }); // package.json var require_package = __commonJS({ "package.json"(exports2, module2) { module2.exports = { name: "metis-code", version: "0.6.4", description: "Metis Code - multi-model coding CLI agent", private: false, bin: { metiscode: "dist/cli/index.js", metis: "dist/cli/index.js" }, type: "commonjs", main: "dist/index.js", files: [ "dist", "assets", "README.md", "LICENSE" ], scripts: { build: "tsup", compile: "tsc -p tsconfig.json", start: "node dist/cli/index.js", metiscode: "node dist/cli/index.js", metis: "node dist/cli/index.js", test: "vitest run", "test:watch": "vitest", prepare: "npm run build", hello: "ts-node src/hello.ts" }, keywords: [ "cli", "ai", "agent", "coding", "assistant", "development", "metis", "claude", "openai", "anthropic", "tool-calling", "automation" ], license: "MIT", engines: { node: ">=18.0.0" }, dependencies: { "@anthropic-ai/sdk": "^0.22.0", "@types/uuid": "^10.0.0", commander: "^12.1.0", diff: "^5.2.0", "js-yaml": "^4.1.0", kleur: "^4.1.5", openai: "^4.55.0", uuid: "^11.1.0", ws: "^8.18.3" }, devDependencies: { "@types/js-yaml": "^4.0.9", "@types/node": "^20.12.12", "@types/ws": "^8.18.1", tsup: "^8.0.2", typescript: "^5.5.4", vitest: "^1.6.0" } }; } }); // src/cli/index.ts var import_commander = require("commander"); var import_kleur13 = __toESM(require("kleur")); // src/cli/commands/init.ts var import_fs5 = __toESM(require("fs")); var import_path5 = __toESM(require("path")); var import_kleur = __toESM(require("kleur")); init_repo(); init_simpleAgent(); async function analyzeProjectAndGenerateAgentMd(projectPath) { console.log(import_kleur.default.blue("\u{1F50D} Analyzing project structure...")); try { const provider = makeProvider(); const repoSummary = summarizeRepo(120); const analysisPrompt = `Analyze this project and generate a comprehensive Agent.md file with project-specific instructions for an AI coding assistant. PROJECT ANALYSIS: ${repoSummary} Based on the project structure, files, and patterns you can see, create an Agent.md that includes: 1. **Project Context**: Describe what this project appears to be and its purpose 2. **Tech Stack**: Identify the main technologies, frameworks, and tools 3. **Architecture**: Describe the project structure and organization patterns 4. **Coding Standards**: Infer coding conventions from existing code 5. **Key Components**: Highlight important files, directories, and their purposes 6. **Development Workflow**: Identify build scripts, test setup, and development commands 7. **Specific Guidelines**: Project-specific best practices and constraints Generate a professional, detailed Agent.md file that will help an AI assistant understand this specific project and provide better, more contextual assistance. Format as a proper markdown file with clear sections and actionable guidance.`; const agentMdContent = await provider.send([ { role: "system", content: "You are an expert software ar