overseer-mcp 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +40 -0
- package/LICENSE +21 -0
- package/README.md +231 -0
- package/configure-overseer.bat +239 -0
- package/dist/config.js +150 -0
- package/dist/index.js +64 -0
- package/dist/init/codexConfig.js +27 -0
- package/dist/init/codexConfigCommand.js +78 -0
- package/dist/init/command.js +98 -0
- package/dist/init/config.js +204 -0
- package/dist/init/instructions.js +137 -0
- package/dist/init/instructionsCommand.js +11 -0
- package/dist/prompts.js +144 -0
- package/dist/providers/anthropic.js +50 -0
- package/dist/providers/cli.js +52 -0
- package/dist/providers/openaiCompatible.js +42 -0
- package/dist/providers/types.js +12 -0
- package/dist/repo/files.js +47 -0
- package/dist/repo/findRoot.js +19 -0
- package/dist/repo/git.js +63 -0
- package/dist/repo/retrieve.js +118 -0
- package/dist/repo/tests.js +21 -0
- package/dist/tools/consultExpert.js +44 -0
- package/dist/tools/getPlan.js +82 -0
- package/dist/tools/requestReview.js +48 -0
- package/dist/tools/shared.js +35 -0
- package/package.json +44 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
3
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import { loadConfig } from "./config.js";
|
|
6
|
+
import { runCodexConfig } from "./init/codexConfigCommand.js";
|
|
7
|
+
import { runInit } from "./init/command.js";
|
|
8
|
+
import { runInstructions } from "./init/instructionsCommand.js";
|
|
9
|
+
import { consultExpertInputSchema, handleConsultExpert } from "./tools/consultExpert.js";
|
|
10
|
+
import { getPlanInputSchema, handleGetPlan } from "./tools/getPlan.js";
|
|
11
|
+
import { handleRequestReview, requestReviewInputSchema } from "./tools/requestReview.js";
|
|
12
|
+
export function createServer(config) {
|
|
13
|
+
const server = new McpServer({ name: "overseer-mcp", version: "0.1.1" });
|
|
14
|
+
server.registerTool("get_plan", {
|
|
15
|
+
title: "Get implementation plan",
|
|
16
|
+
description: "Call before writing any code. Forward the user's request so the expert can produce a surgical plan from real git and disk context.",
|
|
17
|
+
inputSchema: getPlanInputSchema,
|
|
18
|
+
}, async (args) => handleGetPlan(args, config));
|
|
19
|
+
server.registerTool("consult_expert", {
|
|
20
|
+
title: "Consult expert",
|
|
21
|
+
description: "Call BEFORE improvising whenever you hit a non-trivial decision, an error you don't fully understand, or an ambiguity in the plan. Attach relevant file_paths and literal error_output.",
|
|
22
|
+
inputSchema: consultExpertInputSchema,
|
|
23
|
+
}, async (args) => handleConsultExpert(args, config));
|
|
24
|
+
server.registerTool("request_review", {
|
|
25
|
+
title: "Request final review",
|
|
26
|
+
description: "Call when implementation is done. The server sends real git diffs and optional validation output to the expert; finish only after APPROVE.",
|
|
27
|
+
inputSchema: requestReviewInputSchema,
|
|
28
|
+
}, async (args) => handleRequestReview(args, config));
|
|
29
|
+
return server;
|
|
30
|
+
}
|
|
31
|
+
export async function main() {
|
|
32
|
+
if (process.argv[2] === "init") {
|
|
33
|
+
await runInit(process.argv.slice(3), process, process.argv[1]);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (process.argv[2] === "instructions") {
|
|
37
|
+
await runInstructions(process.argv.slice(3), process);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (process.argv[2] === "codex-config") {
|
|
41
|
+
await runCodexConfig(process.argv.slice(3), process);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
let config;
|
|
45
|
+
try {
|
|
46
|
+
config = loadConfig();
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
50
|
+
console.error(message);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
const server = createServer(config);
|
|
54
|
+
const transport = new StdioServerTransport();
|
|
55
|
+
await server.connect(transport);
|
|
56
|
+
console.error(`overseer-mcp ready (${config.provider.id}) repoRoot=${config.repoRoot} cwd=${process.cwd()} VSCODE_CWD=${process.env.VSCODE_CWD ?? "unset"}`);
|
|
57
|
+
}
|
|
58
|
+
const entrypoint = process.argv[1] ? pathToFileURL(process.argv[1]).href : undefined;
|
|
59
|
+
if (entrypoint === import.meta.url) {
|
|
60
|
+
main().catch((err) => {
|
|
61
|
+
console.error(err);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { copyFile, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
const serviceTierLine = /^(\s*)service_tier\s*=\s*"([^"]*)".*$/m;
|
|
3
|
+
export function inspectCodexServiceTier(text) {
|
|
4
|
+
const match = serviceTierLine.exec(text);
|
|
5
|
+
if (!match)
|
|
6
|
+
return { state: "missing" };
|
|
7
|
+
const value = match[2];
|
|
8
|
+
if (value === "fast" || value === "flex")
|
|
9
|
+
return { state: "valid", value };
|
|
10
|
+
return { state: "invalid", value };
|
|
11
|
+
}
|
|
12
|
+
function applyServiceTierChoice(text, choice) {
|
|
13
|
+
if (choice === "remove") {
|
|
14
|
+
return text.replace(serviceTierLine, "").replace(/\n{3,}/g, "\n\n");
|
|
15
|
+
}
|
|
16
|
+
return text.replace(serviceTierLine, (_line, indent) => `${indent}service_tier = "${choice}"`);
|
|
17
|
+
}
|
|
18
|
+
export async function repairCodexConfigFile(configPath, choice) {
|
|
19
|
+
const text = await readFile(configPath, "utf8");
|
|
20
|
+
const status = inspectCodexServiceTier(text);
|
|
21
|
+
if (status.state !== "invalid")
|
|
22
|
+
return { changed: false, status };
|
|
23
|
+
const backupPath = `${configPath}.backup-${Date.now()}`;
|
|
24
|
+
await copyFile(configPath, backupPath);
|
|
25
|
+
await writeFile(configPath, applyServiceTierChoice(text, choice), "utf8");
|
|
26
|
+
return { changed: true, backupPath, status };
|
|
27
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { access, readFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import readline from "node:readline/promises";
|
|
5
|
+
import { inspectCodexServiceTier, repairCodexConfigFile } from "./codexConfig.js";
|
|
6
|
+
function write(stream, text) {
|
|
7
|
+
stream.write(text);
|
|
8
|
+
}
|
|
9
|
+
function getFlag(argv, key) {
|
|
10
|
+
const prefix = `--${key}=`;
|
|
11
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
12
|
+
const arg = argv[index];
|
|
13
|
+
if (arg === `--${key}`)
|
|
14
|
+
return argv[index + 1];
|
|
15
|
+
if (arg.startsWith(prefix))
|
|
16
|
+
return arg.slice(prefix.length);
|
|
17
|
+
}
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
function parseChoice(value) {
|
|
21
|
+
if (value === undefined)
|
|
22
|
+
return undefined;
|
|
23
|
+
const normalized = value.trim().toLowerCase();
|
|
24
|
+
if (!normalized || normalized === "1" || normalized === "flex")
|
|
25
|
+
return "flex";
|
|
26
|
+
if (normalized === "2" || normalized === "fast")
|
|
27
|
+
return "fast";
|
|
28
|
+
if (normalized === "3" || normalized === "remove")
|
|
29
|
+
return "remove";
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
async function chooseServiceTier(io) {
|
|
33
|
+
write(io.stdout, [
|
|
34
|
+
"Choose Codex service_tier:",
|
|
35
|
+
" 1. flex (recommended)",
|
|
36
|
+
" 2. fast",
|
|
37
|
+
" 3. remove service_tier line",
|
|
38
|
+
"",
|
|
39
|
+
].join("\n"));
|
|
40
|
+
const rl = readline.createInterface({ input: io.stdin, output: io.stdout });
|
|
41
|
+
try {
|
|
42
|
+
const answer = (await rl.question("Choose [1]: ")).trim();
|
|
43
|
+
const choice = parseChoice(answer);
|
|
44
|
+
if (!choice)
|
|
45
|
+
throw new Error("Invalid service_tier choice");
|
|
46
|
+
return choice;
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
rl.close();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export async function runCodexConfig(argv, io = process) {
|
|
53
|
+
const configPath = getFlag(argv, "path") ?? path.join(homedir(), ".codex", "config.toml");
|
|
54
|
+
const explicitChoice = parseChoice(getFlag(argv, "choice"));
|
|
55
|
+
try {
|
|
56
|
+
await access(configPath);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
write(io.stdout, `No Codex config found at ${configPath}; skipping.\n`);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const status = inspectCodexServiceTier(await readFile(configPath, "utf8"));
|
|
63
|
+
if (status.state !== "invalid") {
|
|
64
|
+
write(io.stdout, `Codex config service_tier is OK at ${configPath}.\n`);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
write(io.stdout, [
|
|
68
|
+
`Codex config has unsupported service_tier "${status.value}" at:`,
|
|
69
|
+
configPath,
|
|
70
|
+
'Codex CLI currently expects "fast" or "flex".',
|
|
71
|
+
"",
|
|
72
|
+
].join("\n"));
|
|
73
|
+
const choice = explicitChoice ?? (await chooseServiceTier(io));
|
|
74
|
+
const result = await repairCodexConfigFile(configPath, choice);
|
|
75
|
+
if (result.changed) {
|
|
76
|
+
write(io.stdout, `Updated service_tier using "${choice}". Backup: ${result.backupPath}\n`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import readline from "node:readline/promises";
|
|
3
|
+
import { buildCodexToml, buildGenericMcpJson, parseInitArgs, } from "./config.js";
|
|
4
|
+
function write(stream, text) {
|
|
5
|
+
stream.write(text);
|
|
6
|
+
}
|
|
7
|
+
async function ask(rl, question, fallback) {
|
|
8
|
+
const suffix = fallback ? ` (${fallback})` : "";
|
|
9
|
+
const answer = (await rl.question(`${question}${suffix}: `)).trim();
|
|
10
|
+
return answer || fallback || "";
|
|
11
|
+
}
|
|
12
|
+
function parseExpertAnswer(answer) {
|
|
13
|
+
if (answer === "1" || answer === "openai-compatible")
|
|
14
|
+
return "openai-compatible";
|
|
15
|
+
if (answer === "2" || answer === "openai")
|
|
16
|
+
return "openai";
|
|
17
|
+
if (answer === "3" || answer === "anthropic")
|
|
18
|
+
return "anthropic";
|
|
19
|
+
if (answer === "4" || answer === "cli")
|
|
20
|
+
return "cli";
|
|
21
|
+
throw new Error("Invalid expert choice");
|
|
22
|
+
}
|
|
23
|
+
async function interactiveArgs(io, scriptPath) {
|
|
24
|
+
const rl = readline.createInterface({ input: io.stdin, output: io.stdout });
|
|
25
|
+
try {
|
|
26
|
+
write(io.stdout, [
|
|
27
|
+
"overseer-mcp init",
|
|
28
|
+
"",
|
|
29
|
+
"Expert options:",
|
|
30
|
+
"1. openai-compatible (custom baseUrl + API key: opencode-go, GLM, gateways)",
|
|
31
|
+
"2. openai (OpenAI API)",
|
|
32
|
+
"3. anthropic (Anthropic API)",
|
|
33
|
+
"4. cli (Codex/Claude/local CLI, optional login)",
|
|
34
|
+
"",
|
|
35
|
+
].join("\n"));
|
|
36
|
+
const expert = parseExpertAnswer(await ask(rl, "Choose expert", "1"));
|
|
37
|
+
const args = ["--print", "--expert", expert, "--name", await ask(rl, "MCP server name", "overseer")];
|
|
38
|
+
args.push("--repo-root", await ask(rl, "Repo root", process.cwd()));
|
|
39
|
+
const testCommand = await ask(rl, "Validation command, empty to skip");
|
|
40
|
+
if (testCommand)
|
|
41
|
+
args.push("--test-command", testCommand);
|
|
42
|
+
if (expert === "cli") {
|
|
43
|
+
args.push("--cli-command", await ask(rl, "CLI command", "codex"));
|
|
44
|
+
const cliArgs = await ask(rl, "CLI args as JSON array", '["exec"]');
|
|
45
|
+
if (cliArgs)
|
|
46
|
+
args.push("--cli-args", cliArgs);
|
|
47
|
+
const login = await ask(rl, "Run CLI login now? yes/no", "no");
|
|
48
|
+
if (login.toLowerCase().startsWith("y"))
|
|
49
|
+
args.push("--run-login");
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
args.push("--model", await ask(rl, "Expert model"));
|
|
53
|
+
const defaultKey = expert === "anthropic" ? "ANTHROPIC_API_KEY" : "OPENAI_API_KEY";
|
|
54
|
+
args.push("--api-key-env", await ask(rl, "Environment variable that contains the API key", defaultKey));
|
|
55
|
+
const baseUrl = await ask(rl, "Base URL, empty for provider default");
|
|
56
|
+
if (baseUrl)
|
|
57
|
+
args.push("--base-url", baseUrl);
|
|
58
|
+
}
|
|
59
|
+
return parseInitArgs(args, scriptPath);
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
rl.close();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async function runLogin(parsed, io) {
|
|
66
|
+
if (!parsed.runLogin)
|
|
67
|
+
return;
|
|
68
|
+
if (!parsed.loginCommand || parsed.loginCommand.length === 0) {
|
|
69
|
+
write(io.stderr, "No login command is known for this CLI. Skipping login.\n");
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const [command, ...args] = parsed.loginCommand;
|
|
73
|
+
write(io.stderr, `Running: ${command} ${args.join(" ")}\n`);
|
|
74
|
+
await new Promise((resolve) => {
|
|
75
|
+
const child = spawn(command, args, {
|
|
76
|
+
stdio: ["inherit", process.stderr, "inherit"],
|
|
77
|
+
shell: false,
|
|
78
|
+
});
|
|
79
|
+
child.on("error", (err) => {
|
|
80
|
+
write(io.stderr, `Login failed to start: ${err.message}. Run "${command} login" manually in a terminal.\n`);
|
|
81
|
+
resolve();
|
|
82
|
+
});
|
|
83
|
+
child.on("close", (code) => {
|
|
84
|
+
if (code === 0)
|
|
85
|
+
resolve();
|
|
86
|
+
else {
|
|
87
|
+
write(io.stderr, `Login exited with code ${code}. You can run "${command} login" manually later.\n`);
|
|
88
|
+
resolve();
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
export async function runInit(argv, io = process, scriptPath) {
|
|
94
|
+
const parsed = argv.length > 0 ? parseInitArgs(argv, scriptPath) : await interactiveArgs(io, scriptPath);
|
|
95
|
+
await runLogin(parsed, io);
|
|
96
|
+
const text = parsed.target === "json" ? buildGenericMcpJson([parsed.profile]) : `${buildCodexToml([parsed.profile])}\n`;
|
|
97
|
+
write(io.stdout, text);
|
|
98
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
function requireOption(value, name) {
|
|
3
|
+
if (!value) {
|
|
4
|
+
throw new Error(`Missing required init option: ${name}`);
|
|
5
|
+
}
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
function configPath(value) {
|
|
9
|
+
return value.replace(/\\/g, "/");
|
|
10
|
+
}
|
|
11
|
+
function quoteToml(value) {
|
|
12
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
13
|
+
}
|
|
14
|
+
function arrayToml(values) {
|
|
15
|
+
return `[${values.map(quoteToml).join(", ")}]`;
|
|
16
|
+
}
|
|
17
|
+
function parseList(value) {
|
|
18
|
+
if (!value)
|
|
19
|
+
return [];
|
|
20
|
+
const trimmed = value.trim();
|
|
21
|
+
if (!trimmed)
|
|
22
|
+
return [];
|
|
23
|
+
if (trimmed.startsWith("[")) {
|
|
24
|
+
const parsed = JSON.parse(trimmed);
|
|
25
|
+
if (!Array.isArray(parsed) || !parsed.every((item) => typeof item === "string")) {
|
|
26
|
+
throw new Error("Expected a JSON string array");
|
|
27
|
+
}
|
|
28
|
+
return parsed;
|
|
29
|
+
}
|
|
30
|
+
return trimmed.split(/\s+/);
|
|
31
|
+
}
|
|
32
|
+
function readFlagMap(argv) {
|
|
33
|
+
const map = new Map();
|
|
34
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
35
|
+
const arg = argv[index];
|
|
36
|
+
if (!arg.startsWith("--"))
|
|
37
|
+
continue;
|
|
38
|
+
const inline = arg.indexOf("=");
|
|
39
|
+
const key = inline === -1 ? arg.slice(2) : arg.slice(2, inline);
|
|
40
|
+
const current = map.get(key) ?? [];
|
|
41
|
+
if (inline !== -1) {
|
|
42
|
+
current.push(arg.slice(inline + 1));
|
|
43
|
+
}
|
|
44
|
+
else if (argv[index + 1] && !argv[index + 1].startsWith("--")) {
|
|
45
|
+
current.push(argv[index + 1]);
|
|
46
|
+
index += 1;
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
current.push("true");
|
|
50
|
+
}
|
|
51
|
+
map.set(key, current);
|
|
52
|
+
}
|
|
53
|
+
return map;
|
|
54
|
+
}
|
|
55
|
+
function getOne(flags, key) {
|
|
56
|
+
return flags.get(key)?.at(-1);
|
|
57
|
+
}
|
|
58
|
+
function getMany(flags, key) {
|
|
59
|
+
return flags.get(key) ?? [];
|
|
60
|
+
}
|
|
61
|
+
function parseTarget(value) {
|
|
62
|
+
if (!value)
|
|
63
|
+
return "codex";
|
|
64
|
+
if (value === "codex" || value === "json")
|
|
65
|
+
return value;
|
|
66
|
+
throw new Error("Invalid --target: expected codex or json");
|
|
67
|
+
}
|
|
68
|
+
function parseExpert(value) {
|
|
69
|
+
const expert = value ?? "openai-compatible";
|
|
70
|
+
if (expert === "openai" || expert === "openai-compatible" || expert === "anthropic" || expert === "cli") {
|
|
71
|
+
return expert;
|
|
72
|
+
}
|
|
73
|
+
throw new Error("Invalid --expert: expected openai, openai-compatible, anthropic, or cli");
|
|
74
|
+
}
|
|
75
|
+
function parsePromptVia(value) {
|
|
76
|
+
if (!value)
|
|
77
|
+
return undefined;
|
|
78
|
+
if (value === "stdin" || value === "arg")
|
|
79
|
+
return value;
|
|
80
|
+
throw new Error("Invalid --cli-prompt-via: expected stdin or arg");
|
|
81
|
+
}
|
|
82
|
+
export function buildProfileEnv(options) {
|
|
83
|
+
const env = {};
|
|
84
|
+
if (options.repoRoot)
|
|
85
|
+
env.OVERSEER_REPO_ROOT = configPath(options.repoRoot);
|
|
86
|
+
if (options.testCommand)
|
|
87
|
+
env.OVERSEER_TEST_COMMAND = options.testCommand;
|
|
88
|
+
if (options.expert === "cli") {
|
|
89
|
+
env.OVERSEER_PROVIDER = "cli";
|
|
90
|
+
env.OVERSEER_CLI_COMMAND = requireOption(options.cliCommand, "--cli-command");
|
|
91
|
+
if (options.cliArgs && options.cliArgs.length > 0) {
|
|
92
|
+
env.OVERSEER_CLI_ARGS = JSON.stringify(options.cliArgs);
|
|
93
|
+
}
|
|
94
|
+
env.OVERSEER_CLI_PROMPT_VIA = options.cliPromptVia ?? "stdin";
|
|
95
|
+
if (options.cliTimeoutMs)
|
|
96
|
+
env.OVERSEER_CLI_TIMEOUT_MS = options.cliTimeoutMs;
|
|
97
|
+
return env;
|
|
98
|
+
}
|
|
99
|
+
env.OVERSEER_PROVIDER = options.expert === "anthropic" ? "anthropic" : "openai";
|
|
100
|
+
if (options.apiKeyEnv) {
|
|
101
|
+
env.OVERSEER_API_KEY_ENV = options.apiKeyEnv;
|
|
102
|
+
}
|
|
103
|
+
else if (options.apiKey) {
|
|
104
|
+
env.OVERSEER_API_KEY = options.apiKey;
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
env.OVERSEER_API_KEY_ENV = options.expert === "anthropic" ? "ANTHROPIC_API_KEY" : "OPENAI_API_KEY";
|
|
108
|
+
}
|
|
109
|
+
if (options.baseUrl)
|
|
110
|
+
env.OVERSEER_BASE_URL = options.baseUrl;
|
|
111
|
+
env.OVERSEER_MODEL = requireOption(options.model, "--model");
|
|
112
|
+
return env;
|
|
113
|
+
}
|
|
114
|
+
export function buildCodexToml(profiles) {
|
|
115
|
+
return profiles
|
|
116
|
+
.map((profile) => {
|
|
117
|
+
const lines = [
|
|
118
|
+
`[mcp_servers.${profile.name}]`,
|
|
119
|
+
`command = ${quoteToml(profile.command)}`,
|
|
120
|
+
`args = ${arrayToml(profile.args)}`,
|
|
121
|
+
];
|
|
122
|
+
if (profile.cwd)
|
|
123
|
+
lines.push(`cwd = ${quoteToml(configPath(profile.cwd))}`);
|
|
124
|
+
if (profile.envVars && profile.envVars.length > 0) {
|
|
125
|
+
lines.push(`env_vars = ${arrayToml(profile.envVars)}`);
|
|
126
|
+
}
|
|
127
|
+
lines.push("", `[mcp_servers.${profile.name}.env]`);
|
|
128
|
+
for (const [key, value] of Object.entries(profile.env)) {
|
|
129
|
+
lines.push(`${key} = ${quoteToml(value)}`);
|
|
130
|
+
}
|
|
131
|
+
return lines.join("\n");
|
|
132
|
+
})
|
|
133
|
+
.join("\n\n");
|
|
134
|
+
}
|
|
135
|
+
export function buildGenericMcpJson(profiles) {
|
|
136
|
+
const mcpServers = {};
|
|
137
|
+
for (const profile of profiles) {
|
|
138
|
+
mcpServers[profile.name] = {
|
|
139
|
+
command: profile.command,
|
|
140
|
+
args: profile.args,
|
|
141
|
+
...(profile.cwd ? { cwd: configPath(profile.cwd) } : {}),
|
|
142
|
+
env: profile.env,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
return `${JSON.stringify({ mcpServers }, null, 2)}\n`;
|
|
146
|
+
}
|
|
147
|
+
export function defaultServerCommand(scriptPath = process.argv[1] ?? "dist/index.js") {
|
|
148
|
+
return {
|
|
149
|
+
command: "node",
|
|
150
|
+
args: [configPath(path.resolve(scriptPath))],
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
export function defaultLoginCommand(cliCommand) {
|
|
154
|
+
if (cliCommand === "codex" || cliCommand === "claude") {
|
|
155
|
+
return [cliCommand, "login"];
|
|
156
|
+
}
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
export function parseInitArgs(argv, scriptPath) {
|
|
160
|
+
const flags = readFlagMap(argv);
|
|
161
|
+
const target = parseTarget(getOne(flags, "target"));
|
|
162
|
+
const expert = parseExpert(getOne(flags, "expert"));
|
|
163
|
+
const server = {
|
|
164
|
+
command: getOne(flags, "server-command") ?? defaultServerCommand(scriptPath).command,
|
|
165
|
+
args: getMany(flags, "server-arg"),
|
|
166
|
+
};
|
|
167
|
+
if (server.args.length === 0) {
|
|
168
|
+
server.args = defaultServerCommand(scriptPath).args;
|
|
169
|
+
}
|
|
170
|
+
const repoRoot = getOne(flags, "repo-root") ?? process.cwd();
|
|
171
|
+
const cliArgs = parseList(getOne(flags, "cli-args"));
|
|
172
|
+
const env = buildProfileEnv({
|
|
173
|
+
expert,
|
|
174
|
+
repoRoot,
|
|
175
|
+
testCommand: getOne(flags, "test-command"),
|
|
176
|
+
apiKeyEnv: getOne(flags, "api-key-env"),
|
|
177
|
+
apiKey: getOne(flags, "api-key"),
|
|
178
|
+
baseUrl: getOne(flags, "base-url"),
|
|
179
|
+
model: getOne(flags, "model"),
|
|
180
|
+
cliCommand: getOne(flags, "cli-command"),
|
|
181
|
+
cliArgs,
|
|
182
|
+
cliPromptVia: parsePromptVia(getOne(flags, "cli-prompt-via")),
|
|
183
|
+
cliTimeoutMs: getOne(flags, "cli-timeout-ms"),
|
|
184
|
+
});
|
|
185
|
+
const apiKeyEnv = env.OVERSEER_API_KEY_ENV;
|
|
186
|
+
const envVars = apiKeyEnv ? [apiKeyEnv] : undefined;
|
|
187
|
+
const cliCommand = getOne(flags, "cli-command");
|
|
188
|
+
const loginCommand = parseList(getOne(flags, "login-command"));
|
|
189
|
+
const defaultLogin = defaultLoginCommand(cliCommand);
|
|
190
|
+
return {
|
|
191
|
+
print: flags.has("print"),
|
|
192
|
+
target,
|
|
193
|
+
runLogin: flags.has("run-login"),
|
|
194
|
+
loginCommand: loginCommand.length > 0 ? loginCommand : defaultLogin,
|
|
195
|
+
profile: {
|
|
196
|
+
name: getOne(flags, "name") ?? "overseer",
|
|
197
|
+
command: server.command,
|
|
198
|
+
args: server.args,
|
|
199
|
+
cwd: repoRoot,
|
|
200
|
+
envVars,
|
|
201
|
+
env,
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
export function buildAgentInstructions(options = {}) {
|
|
2
|
+
const mcpName = options.mcpServerName ?? "overseer";
|
|
3
|
+
return `# Overseer MCP Agent Instructions
|
|
4
|
+
|
|
5
|
+
> **REGLA ABSOLUTA: Este archivo es un CONTRATO OBLIGATORIO, no una guia. Cada seccion marcada con BLOQUEO te impide avanzar si no la cumplis. Ignorar el flujo invalida todo el trabajo.**
|
|
6
|
+
|
|
7
|
+
Estas instrucciones son para el agente operador: el modelo barato o ejecutor que edita el codigo. Aplican igual si el agente es Codex, Mimo, Qoder, Cline, Roo, Trae, Claude Code u otro agente compatible con MCP.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## BLOQUEO 0 — Verificacion de MCP (ANTES de cualquier otra accion)
|
|
12
|
+
|
|
13
|
+
**NO podes escribir codigo, NO podes planificar, NO podes analizar archivos, NO podes responder al usuario con un plan propio hasta que este bloqueo se resuelva.**
|
|
14
|
+
|
|
15
|
+
1. Busca en tu lista de herramientas MCP las tres herramientas: \`get_plan\`, \`consult_expert\`, \`request_review\`.
|
|
16
|
+
2. Si las tres existen y estan disponibles → desbloqueado, continua.
|
|
17
|
+
3. Si NO existen → **DETENETE INMEDIATAMENTE**. No hagas nada mas. Responde al usuario EXACTAMENTE esto:
|
|
18
|
+
> "El servidor MCP \`${mcpName}\` no esta disponible. Necesito que cargues \`overseer-mcp.json\` desde la raiz del repo en la configuracion MCP del IDE, y reinicies o recargues los servidores MCP. No puedo continuar sin estas herramientas."
|
|
19
|
+
4. Si tu IDE permite instalar MCPs desde el agente, importa \`overseer-mcp.json\` de la raiz del repo (o \`generated/overseer-mcp.json\`). Despues de instalar, avisale al usuario que debe reiniciar el IDE.
|
|
20
|
+
|
|
21
|
+
**PROHIBIDO:** inventar un reemplazo, simular llamadas MCP, o continuar sin el servidor.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## BLOQUEO 1 — Plan obligatorio (GATE de entrada)
|
|
26
|
+
|
|
27
|
+
**NO podes escribir UNA SOLA LINEA de codigo sin haber llamado \`get_plan\` primero y haber recibido el plan del experto.**
|
|
28
|
+
|
|
29
|
+
Flujo:
|
|
30
|
+
1. Llama \`get_plan\` con la solicitud original del usuario. Pasale \`focus_paths\` si ya sabes que archivos importan.
|
|
31
|
+
2. Espera la respuesta. El plan que devuelve ES tu contrato de trabajo.
|
|
32
|
+
3. Lee el plan completo. Los \`STEPS\` del plan son tu lista de tareas obligatoria.
|
|
33
|
+
4. **NO modifiques el plan. NO lo resumas. NO lo ignores.** Segui los pasos en orden.
|
|
34
|
+
|
|
35
|
+
**PROHIBIDO:**
|
|
36
|
+
- Escribir codigo antes de llamar \`get_plan\`.
|
|
37
|
+
- Inventar tu propio plan y ejecutarlo sin consultar al experto.
|
|
38
|
+
- Saltear pasos del plan recibido.
|
|
39
|
+
- Modificar archivos que el plan marca como \`DO NOT TOUCH\`.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## BLOQUEO 2 — Implementacion con supervision
|
|
44
|
+
|
|
45
|
+
Implementa el plan paso a paso. Durante la implementacion:
|
|
46
|
+
|
|
47
|
+
**OBLIGATORIO llamar \`consult_expert\` ANTES de:**
|
|
48
|
+
- Tomar una decision de diseno que el plan no cubre.
|
|
49
|
+
- Interpretar un error que no entendes completamente.
|
|
50
|
+
- Resolver una ambiguedad en el plan.
|
|
51
|
+
- Elegir entre dos o mas enfoques posibles.
|
|
52
|
+
|
|
53
|
+
**OBLIGATORIO pasar evidencia real:**
|
|
54
|
+
- \`file_paths\`: rutas reales de los archivos involucrados.
|
|
55
|
+
- \`error_output\`: salida literal del error o test, copiada de la terminal.
|
|
56
|
+
- \`options_considered\`: las alternativas que ya evaluaste.
|
|
57
|
+
|
|
58
|
+
**PROHIBIDO:**
|
|
59
|
+
- Improvisar una decision sin consultar.
|
|
60
|
+
- Usar tu propio resumen como evidencia en lugar de archivos y outputs reales.
|
|
61
|
+
- Continuar implementando despues de un \`NEED_INFO\` sin aportar la evidencia pedida.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## BLOQUEO 3 — Revision final obligatoria (GATE de salida)
|
|
66
|
+
|
|
67
|
+
**NO podes declarar la tarea completa sin que \`request_review\` devuelva \`APPROVE\`.**
|
|
68
|
+
|
|
69
|
+
1. Cuando termines la implementacion, llama \`request_review\`.
|
|
70
|
+
2. Si devuelve \`APPROVE\` → tarea terminada. Crea el reporte final.
|
|
71
|
+
3. Si devuelve \`REQUEST_CHANGES\` → aplica SOLO los \`REQUIRED_FIXES\` listados y volve a llamar \`request_review\`.
|
|
72
|
+
4. Repeti el ciclo hasta obtener \`APPROVE\`.
|
|
73
|
+
|
|
74
|
+
**PROHIBIDO:**
|
|
75
|
+
- Decir "la tarea esta lista" o "termine" sin haber recibido \`APPROVE\`.
|
|
76
|
+
- Ignorar los \`REQUIRED_FIXES\` del review.
|
|
77
|
+
- Hacer cambios adicionales despues de un \`REQUEST_CHANGES\` que no sean los fixes pedidos.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Archivo MCP local
|
|
82
|
+
|
|
83
|
+
El archivo \`overseer-mcp.json\` en la raiz del repo contiene la configuracion local con rutas absolutas de esta maquina.
|
|
84
|
+
|
|
85
|
+
- Usalo para instalar o recargar el MCP en este IDE.
|
|
86
|
+
- No lo subas a GitHub si contiene rutas absolutas o datos de tu maquina.
|
|
87
|
+
|
|
88
|
+
## Login del experto CLI
|
|
89
|
+
|
|
90
|
+
Si el experto es un CLI (Codex CLI, Claude CLI), el login lo guarda ese CLI en su config global, no el MCP.
|
|
91
|
+
|
|
92
|
+
- Si hay error de autenticacion, avisale al usuario que ejecute el login del CLI correspondiente y reinicie el IDE.
|
|
93
|
+
- No pidas reinstalar \`overseer-mcp\` por un problema de login.
|
|
94
|
+
|
|
95
|
+
## Como llegan estas instrucciones
|
|
96
|
+
|
|
97
|
+
- Si esta instruccion esta en \`AGENTS.md\`, \`CLAUDE.md\`, \`GEMINI.md\` o reglas persistentes del IDE, es **obligatoria y persistente**.
|
|
98
|
+
- Si la recibis pegada en el chat, es instruccion de sesion.
|
|
99
|
+
- Si hay conflicto con otro archivo, segui la instruccion mas especifica del usuario.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Resumen del flujo (checklist obligatorio)
|
|
104
|
+
|
|
105
|
+
\`\`\`
|
|
106
|
+
[ ] BLOQUEO 0: Verifique que get_plan, consult_expert, request_review existen en mis herramientas MCP
|
|
107
|
+
[ ] BLOQUEO 1: Llame get_plan → recibi plan → lo lei completo
|
|
108
|
+
[ ] BLOQUEO 2: Implemente paso a paso, consulte expert en cada decision no trivial
|
|
109
|
+
[ ] BLOQUEO 3: Llame request_review → obtuve APPROVE → cree reporte final
|
|
110
|
+
\`\`\`
|
|
111
|
+
|
|
112
|
+
**Si NO podes marcar un bloqueo como cumplido, NO avances al siguiente. Detenete y resolve el bloqueo.**
|
|
113
|
+
|
|
114
|
+
## Reglas de implementacion
|
|
115
|
+
|
|
116
|
+
- Cambios minimos y quirurgicos.
|
|
117
|
+
- No inventes APIs, rutas, tablas, comandos ni dependencias.
|
|
118
|
+
- No borres datos.
|
|
119
|
+
- No cambies diseno o UX salvo que el usuario lo pida.
|
|
120
|
+
- No refactorices codigo adyacente por gusto.
|
|
121
|
+
- Preserva comportamiento existente.
|
|
122
|
+
- Ejecuta la validacion indicada por el plan o por el proyecto.
|
|
123
|
+
|
|
124
|
+
## Reporte final del agente operador
|
|
125
|
+
|
|
126
|
+
Despues de recibir \`APPROVE\`, crea o actualiza \`OVERSEER_FINAL_REPORT.md\` en la raiz del repo con:
|
|
127
|
+
|
|
128
|
+
- Resumen de lo que cambiaste.
|
|
129
|
+
- Archivos tocados.
|
|
130
|
+
- Decisiones tomadas y alternativas descartadas, si hubo.
|
|
131
|
+
- Validacion ejecutada y resultado real.
|
|
132
|
+
- Resultado de \`request_review\`.
|
|
133
|
+
- Pendientes, riesgos o supuestos.
|
|
134
|
+
|
|
135
|
+
Si \`request_review\` no devuelve \`APPROVE\`, el reporte debe decir que la tarea no esta terminada y listar los cambios requeridos.
|
|
136
|
+
`;
|
|
137
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { buildAgentInstructions } from "./instructions.js";
|
|
2
|
+
function valueAfter(argv, name) {
|
|
3
|
+
const index = argv.indexOf(name);
|
|
4
|
+
if (index === -1)
|
|
5
|
+
return undefined;
|
|
6
|
+
return argv[index + 1];
|
|
7
|
+
}
|
|
8
|
+
export async function runInstructions(argv, io = process) {
|
|
9
|
+
const mcpServerName = valueAfter(argv, "--mcp-name") ?? "overseer";
|
|
10
|
+
io.stdout.write(buildAgentInstructions({ mcpServerName }));
|
|
11
|
+
}
|