metis-code
Version:
Metis Code - multi-model coding CLI agent
1,464 lines (1,437 loc) • 495 kB
JavaScript
#!/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