add Cursor, fix Gemini
This commit is contained in:
@@ -15,9 +15,6 @@ export const claude = agent({
|
||||
executablePath: "cli.js",
|
||||
});
|
||||
},
|
||||
addMcpServer: () => {
|
||||
// Claude accepts mcpServers directly in the query options, no configuration needed
|
||||
},
|
||||
run: async ({ prompt, mcpServers, apiKey, cliPath }) => {
|
||||
process.env.ANTHROPIC_API_KEY = apiKey;
|
||||
|
||||
|
||||
+37
-30
@@ -1,9 +1,8 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import type { McpServerConfig } from "@anthropic-ai/claude-agent-sdk";
|
||||
import { Codex, type CodexOptions, type ThreadEvent } from "@openai/codex-sdk";
|
||||
import { log } from "../utils/cli.ts";
|
||||
import { addInstructions } from "./instructions.ts";
|
||||
import { agent, installFromNpmTarball } from "./shared.ts";
|
||||
import { agent, type ConfigureMcpServersParams, installFromNpmTarball } from "./shared.ts";
|
||||
|
||||
export const codex = agent({
|
||||
name: "codex",
|
||||
@@ -15,38 +14,12 @@ export const codex = agent({
|
||||
executablePath: "bin/codex.js",
|
||||
});
|
||||
},
|
||||
addMcpServer: ({ serverName, serverConfig, cliPath }) => {
|
||||
// Codex CLI syntax: codex mcp add <name> --env KEY=value -- <command> [args...]
|
||||
const command = serverConfig.command;
|
||||
const args = serverConfig.args || [];
|
||||
const envVars = serverConfig.env || {};
|
||||
|
||||
const addArgs = ["mcp", "add", serverName];
|
||||
|
||||
// Add environment variables as --env flags first
|
||||
for (const [key, value] of Object.entries(envVars)) {
|
||||
addArgs.push("--env", `${key}=${value}`);
|
||||
}
|
||||
|
||||
addArgs.push("--", command, ...args);
|
||||
|
||||
log.info(`Adding MCP server '${serverName}'...`);
|
||||
const addResult = spawnSync("node", [cliPath, ...addArgs], {
|
||||
stdio: "pipe",
|
||||
encoding: "utf-8",
|
||||
});
|
||||
|
||||
if (addResult.status !== 0) {
|
||||
throw new Error(
|
||||
`codex mcp add failed: ${addResult.stderr || addResult.stdout || "Unknown error"}`
|
||||
);
|
||||
}
|
||||
log.info(`✓ MCP server '${serverName}' configured`);
|
||||
},
|
||||
run: async ({ prompt, mcpServers, apiKey, cliPath, githubInstallationToken }) => {
|
||||
process.env.OPENAI_API_KEY = apiKey;
|
||||
process.env.GITHUB_INSTALLATION_TOKEN = githubInstallationToken;
|
||||
|
||||
configureCodexMcpServers({ mcpServers, cliPath });
|
||||
|
||||
// Configure Codex
|
||||
const codexOptions: CodexOptions = {
|
||||
apiKey,
|
||||
@@ -183,3 +156,37 @@ const messageHandlers: {
|
||||
log.error(`Error: ${event.message}`);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Configure MCP servers for Codex using the CLI.
|
||||
* Codex CLI syntax: codex mcp add <name> --env KEY=value -- <command> [args...]
|
||||
*/
|
||||
function configureCodexMcpServers({ mcpServers, cliPath }: ConfigureMcpServersParams): void {
|
||||
for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
|
||||
const command = serverConfig.command;
|
||||
const args = serverConfig.args || [];
|
||||
const envVars = serverConfig.env || {};
|
||||
|
||||
const addArgs = ["mcp", "add", serverName];
|
||||
|
||||
// Add environment variables as --env flags first
|
||||
for (const [key, value] of Object.entries(envVars)) {
|
||||
addArgs.push("--env", `${key}=${value}`);
|
||||
}
|
||||
|
||||
addArgs.push("--", command, ...args);
|
||||
|
||||
log.info(`Adding MCP server '${serverName}'...`);
|
||||
const addResult = spawnSync("node", [cliPath, ...addArgs], {
|
||||
stdio: "pipe",
|
||||
encoding: "utf-8",
|
||||
});
|
||||
|
||||
if (addResult.status !== 0) {
|
||||
throw new Error(
|
||||
`codex mcp add failed: ${addResult.stderr || addResult.stdout || "Unknown error"}`
|
||||
);
|
||||
}
|
||||
log.info(`✓ MCP server '${serverName}' configured`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { mkdirSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { log } from "../utils/cli.ts";
|
||||
import { addInstructions } from "./instructions.ts";
|
||||
import { agent, type ConfigureMcpServersParams, installFromCurl } from "./shared.ts";
|
||||
|
||||
export const cursor = agent({
|
||||
name: "cursor",
|
||||
inputKeys: ["cursor_api_key"],
|
||||
install: async () => {
|
||||
return await installFromCurl({
|
||||
installUrl: "https://cursor.com/install",
|
||||
executableName: "cursor-agent",
|
||||
});
|
||||
},
|
||||
run: async ({ prompt, apiKey, cliPath, githubInstallationToken, mcpServers }) => {
|
||||
process.env.CURSOR_API_KEY = apiKey;
|
||||
process.env.GITHUB_INSTALLATION_TOKEN = githubInstallationToken;
|
||||
|
||||
configureCursorMcpServers({ mcpServers, cliPath });
|
||||
|
||||
try {
|
||||
// Run cursor-agent in non-interactive mode with the prompt
|
||||
// Using -p flag for prompt, --output-format text for plain text output
|
||||
// and --approve-mcps to automatically approve all MCP servers
|
||||
const fullPrompt = addInstructions(prompt);
|
||||
|
||||
// Find temp directory from cliPath to set HOME for MCP config lookup
|
||||
const tempDir = cliPath.split("/.local/bin/")[0];
|
||||
|
||||
log.info("Running Cursor CLI...");
|
||||
|
||||
// Use spawn to handle streaming output
|
||||
// Use --print flag explicitly for non-interactive mode
|
||||
return new Promise((resolve) => {
|
||||
const child = spawn(
|
||||
cliPath,
|
||||
["--print", fullPrompt, "--output-format", "text", "--approve-mcps", "--force"],
|
||||
{
|
||||
cwd: process.cwd(), // Run in current working directory
|
||||
env: {
|
||||
...process.env,
|
||||
CURSOR_API_KEY: apiKey,
|
||||
GITHUB_INSTALLATION_TOKEN: githubInstallationToken,
|
||||
HOME: tempDir, // Set HOME so Cursor CLI can find .cursor/mcp.json
|
||||
},
|
||||
stdio: ["ignore", "pipe", "pipe"], // Ignore stdin, pipe stdout/stderr
|
||||
}
|
||||
);
|
||||
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
|
||||
// Log when process starts
|
||||
child.on("spawn", () => {
|
||||
log.debug("Cursor CLI process spawned");
|
||||
});
|
||||
|
||||
child.stdout?.on("data", (data) => {
|
||||
const text = data.toString();
|
||||
stdout += text;
|
||||
// Stream output in real-time
|
||||
process.stdout.write(text);
|
||||
});
|
||||
|
||||
child.stderr?.on("data", (data) => {
|
||||
const text = data.toString();
|
||||
stderr += text;
|
||||
// Log errors as they come - but also write to stdout so we can see it
|
||||
process.stderr.write(text);
|
||||
log.warning(text);
|
||||
});
|
||||
|
||||
// Handle process exit
|
||||
child.on("close", (code, signal) => {
|
||||
if (signal) {
|
||||
log.warning(`Cursor CLI terminated by signal: ${signal}`);
|
||||
}
|
||||
|
||||
if (code === 0) {
|
||||
log.success("Cursor CLI completed successfully");
|
||||
resolve({
|
||||
success: true,
|
||||
output: stdout.trim(),
|
||||
});
|
||||
} else {
|
||||
const errorMessage = stderr || `Cursor CLI exited with code ${code}`;
|
||||
log.error(`Cursor CLI failed: ${errorMessage}`);
|
||||
resolve({
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
output: stdout.trim(),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
child.on("error", (error) => {
|
||||
const errorMessage = error.message || String(error);
|
||||
log.error(`Cursor CLI execution failed: ${errorMessage}`);
|
||||
resolve({
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
output: stdout.trim(),
|
||||
});
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
log.error(`Cursor execution failed: ${errorMessage}`);
|
||||
return {
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
output: "",
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Configure MCP servers for Cursor by writing to the Cursor configuration file.
|
||||
* For cursor, we need to add the MCP servers to the Cursor configuration file manually as there is no CLI command to do this.
|
||||
*/
|
||||
function configureCursorMcpServers({ mcpServers, cliPath }: ConfigureMcpServersParams) {
|
||||
const tempDir = cliPath.split("/.local/bin/")[0];
|
||||
const cursorConfigDir = join(tempDir, ".cursor");
|
||||
const mcpConfigPath = join(cursorConfigDir, "mcp.json");
|
||||
mkdirSync(cursorConfigDir, { recursive: true });
|
||||
writeFileSync(mcpConfigPath, JSON.stringify({ mcpServers }, null, 2), "utf-8");
|
||||
}
|
||||
+33
-28
@@ -1,9 +1,8 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import type { McpServerConfig } from "@anthropic-ai/claude-agent-sdk";
|
||||
import { log } from "../utils/cli.ts";
|
||||
import { spawn } from "../utils/subprocess.ts";
|
||||
import { addInstructions } from "./instructions.ts";
|
||||
import { agent, installFromNpmTarball } from "./shared.ts";
|
||||
import { agent, type ConfigureMcpServersParams, installFromNpmTarball } from "./shared.ts";
|
||||
|
||||
export const gemini = agent({
|
||||
name: "gemini",
|
||||
@@ -16,33 +15,8 @@ export const gemini = agent({
|
||||
installDependencies: true,
|
||||
});
|
||||
},
|
||||
addMcpServer: ({ serverName, serverConfig, cliPath }) => {
|
||||
// Gemini CLI syntax: gemini mcp add <name> <commandOrUrl> [args...] --env KEY=value
|
||||
const command = serverConfig.command;
|
||||
const args = serverConfig.args || [];
|
||||
const envVars = serverConfig.env || {};
|
||||
|
||||
const addArgs = ["mcp", "add", serverName, command, ...args];
|
||||
|
||||
// Add environment variables as --env flags
|
||||
for (const [key, value] of Object.entries(envVars)) {
|
||||
addArgs.push("--env", `${key}=${value}`);
|
||||
}
|
||||
|
||||
log.info(`Adding MCP server '${serverName}'...`);
|
||||
const addResult = spawnSync("node", [cliPath, ...addArgs], {
|
||||
stdio: "pipe",
|
||||
encoding: "utf-8",
|
||||
});
|
||||
|
||||
if (addResult.status !== 0) {
|
||||
throw new Error(
|
||||
`gemini mcp add failed: ${addResult.stderr || addResult.stdout || "Unknown error"}`
|
||||
);
|
||||
}
|
||||
log.info(`✓ MCP server '${serverName}' configured`);
|
||||
},
|
||||
run: async ({ prompt, apiKey, mcpServers, githubInstallationToken, cliPath }) => {
|
||||
configureGeminiMcpServers({ mcpServers, cliPath });
|
||||
if (!apiKey) {
|
||||
throw new Error("google_api_key or gemini_api_key is required for gemini agent");
|
||||
}
|
||||
@@ -108,3 +82,34 @@ export const gemini = agent({
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Configure MCP servers for Gemini using the CLI.
|
||||
* Gemini CLI syntax: gemini mcp add <name> <commandOrUrl> [args...] --env KEY=value
|
||||
*/
|
||||
function configureGeminiMcpServers({ mcpServers, cliPath }: ConfigureMcpServersParams): void {
|
||||
for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
|
||||
const command = serverConfig.command;
|
||||
const args = serverConfig.args || [];
|
||||
const envVars = serverConfig.env || {};
|
||||
|
||||
const addArgs = ["mcp", "add", serverName, command, ...args];
|
||||
|
||||
// Add environment variables as --env flags
|
||||
for (const [key, value] of Object.entries(envVars)) {
|
||||
addArgs.push("--env", `${key}=${value}`);
|
||||
}
|
||||
|
||||
log.info(`Adding MCP server '${serverName}'...`);
|
||||
const addResult = spawnSync("node", [cliPath, ...addArgs], {
|
||||
stdio: "pipe",
|
||||
encoding: "utf-8",
|
||||
});
|
||||
|
||||
if (addResult.status !== 0) {
|
||||
throw new Error(
|
||||
`gemini mcp add failed: ${addResult.stderr || addResult.stdout || "Unknown error"}`
|
||||
);
|
||||
}
|
||||
log.info(`✓ MCP server '${serverName}' configured`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { claude } from "./claude.ts";
|
||||
import { codex } from "./codex.ts";
|
||||
import { cursor } from "./cursor.ts";
|
||||
import { gemini } from "./gemini.ts";
|
||||
|
||||
export const agents = {
|
||||
claude,
|
||||
codex,
|
||||
cursor,
|
||||
gemini,
|
||||
} as const;
|
||||
|
||||
|
||||
+109
-22
@@ -4,7 +4,7 @@ import { mkdtemp } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { pipeline } from "node:stream/promises";
|
||||
import type { McpServerConfig } from "@anthropic-ai/claude-agent-sdk";
|
||||
import type { McpStdioServerConfig } from "@anthropic-ai/claude-agent-sdk";
|
||||
import { log } from "../utils/cli.ts";
|
||||
|
||||
/**
|
||||
@@ -24,10 +24,44 @@ export interface AgentConfig {
|
||||
apiKey: string;
|
||||
githubInstallationToken: string;
|
||||
prompt: string;
|
||||
mcpServers: Record<string, McpServerConfig>;
|
||||
mcpServers: Record<string, McpStdioServerConfig>;
|
||||
cliPath: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for configuring MCP servers
|
||||
*/
|
||||
export interface ConfigureMcpServersParams {
|
||||
mcpServers: Record<string, McpStdioServerConfig>;
|
||||
cliPath: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for installing from npm tarball
|
||||
*/
|
||||
export interface InstallFromNpmTarballParams {
|
||||
packageName: string;
|
||||
version: string;
|
||||
executablePath: string;
|
||||
installDependencies?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for installing from curl script
|
||||
*/
|
||||
export interface InstallFromCurlParams {
|
||||
installUrl: string;
|
||||
executableName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* NPM registry response data structure
|
||||
*/
|
||||
export interface NpmRegistryData {
|
||||
"dist-tags": { latest: string };
|
||||
versions: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Install a CLI tool from an npm package tarball
|
||||
* Downloads the tarball, extracts it to a temp directory, and returns the path to the CLI executable
|
||||
@@ -38,12 +72,7 @@ export async function installFromNpmTarball({
|
||||
version,
|
||||
executablePath,
|
||||
installDependencies,
|
||||
}: {
|
||||
packageName: string;
|
||||
version: string;
|
||||
executablePath: string;
|
||||
installDependencies?: boolean;
|
||||
}): Promise<string> {
|
||||
}: InstallFromNpmTarballParams): Promise<string> {
|
||||
// Resolve version if it's a range or "latest"
|
||||
let resolvedVersion = version;
|
||||
if (version.startsWith("^") || version.startsWith("~") || version === "latest") {
|
||||
@@ -54,10 +83,7 @@ export async function installFromNpmTarball({
|
||||
if (!registryResponse.ok) {
|
||||
throw new Error(`Failed to query registry: ${registryResponse.status}`);
|
||||
}
|
||||
const registryData = (await registryResponse.json()) as {
|
||||
"dist-tags": { latest: string };
|
||||
versions: Record<string, unknown>;
|
||||
};
|
||||
const registryData = (await registryResponse.json()) as NpmRegistryData;
|
||||
resolvedVersion = registryData["dist-tags"].latest;
|
||||
log.info(`Resolved to version ${resolvedVersion}`);
|
||||
} catch (error) {
|
||||
@@ -145,23 +171,84 @@ export async function installFromNpmTarball({
|
||||
return cliPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Install a CLI tool from a curl-based install script
|
||||
* Downloads the install script, runs it with HOME set to temp directory, and returns the path to the CLI executable
|
||||
* The temp directory will be cleaned up by the OS automatically
|
||||
*/
|
||||
export async function installFromCurl({
|
||||
installUrl,
|
||||
executableName,
|
||||
}: InstallFromCurlParams): Promise<string> {
|
||||
log.info(`📦 Installing ${executableName}...`);
|
||||
|
||||
// Derive temp directory prefix from executable name (sanitize similar to package name)
|
||||
// Replace any special characters with - and ensure trailing -
|
||||
const tempDirPrefix = executableName.replace(/[^a-zA-Z0-9]/g, "-") + "-";
|
||||
|
||||
// Create temp directory
|
||||
const tempDir = await mkdtemp(join(tmpdir(), tempDirPrefix));
|
||||
const installScriptPath = join(tempDir, "install.sh");
|
||||
|
||||
// Download the install script
|
||||
log.info(`Downloading install script from ${installUrl}...`);
|
||||
const installScriptResponse = await fetch(installUrl);
|
||||
if (!installScriptResponse.ok) {
|
||||
throw new Error(`Failed to download install script: ${installScriptResponse.status}`);
|
||||
}
|
||||
|
||||
if (!installScriptResponse.body) throw new Error("Response body is null");
|
||||
const fileStream = createWriteStream(installScriptPath);
|
||||
await pipeline(installScriptResponse.body, fileStream);
|
||||
log.info(`Downloaded install script to ${installScriptPath}`);
|
||||
|
||||
// Make install script executable
|
||||
chmodSync(installScriptPath, 0o755);
|
||||
|
||||
log.info("Installing to temp directory...");
|
||||
|
||||
// Run the install script with HOME set to temp directory
|
||||
// The Cursor install script installs to $HOME/.local/bin/{executableName}
|
||||
// By setting HOME=tempDir, we ensure it installs to tempDir/.local/bin/{executableName}
|
||||
const installResult = spawnSync("bash", [installScriptPath], {
|
||||
cwd: tempDir,
|
||||
env: {
|
||||
...process.env,
|
||||
HOME: tempDir, // Cursor install script uses HOME for installation path
|
||||
},
|
||||
stdio: "pipe",
|
||||
encoding: "utf-8",
|
||||
});
|
||||
|
||||
if (installResult.status !== 0) {
|
||||
const errorOutput = installResult.stderr || installResult.stdout || "No output";
|
||||
throw new Error(
|
||||
`Failed to install ${executableName}. Install script exited with code ${installResult.status}. Output: ${errorOutput}`
|
||||
);
|
||||
}
|
||||
|
||||
// The Cursor install script creates a symlink at $HOME/.local/bin/{executableName}
|
||||
// Since we set HOME=tempDir, the deterministic path is:
|
||||
const cliPath = join(tempDir, ".local", "bin", executableName);
|
||||
|
||||
if (!existsSync(cliPath)) {
|
||||
throw new Error(`Executable not found at ${cliPath}`);
|
||||
}
|
||||
|
||||
// Ensure binary is executable
|
||||
chmodSync(cliPath, 0o755);
|
||||
log.info(`✓ ${executableName} installed at ${cliPath}`);
|
||||
|
||||
return cliPath;
|
||||
}
|
||||
|
||||
export const agent = <const agent extends Agent>(agent: agent): agent => {
|
||||
return agent;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parameters for adding an MCP server to an agent
|
||||
*/
|
||||
export interface AddMcpServerParams {
|
||||
serverName: string;
|
||||
serverConfig: Extract<McpServerConfig, { command: string }>;
|
||||
cliPath: string;
|
||||
}
|
||||
|
||||
export type Agent = {
|
||||
name: string;
|
||||
inputKeys: string[];
|
||||
install: () => Promise<string>;
|
||||
addMcpServer: (params: AddMcpServerParams) => void;
|
||||
run: (config: AgentConfig) => Promise<AgentResult>;
|
||||
};
|
||||
|
||||
@@ -33346,6 +33346,49 @@ async function installFromNpmTarball({
|
||||
log.info(`\u2713 ${packageName} installed at ${cliPath}`);
|
||||
return cliPath;
|
||||
}
|
||||
async function installFromCurl({
|
||||
installUrl,
|
||||
executableName
|
||||
}) {
|
||||
log.info(`\u{1F4E6} Installing ${executableName}...`);
|
||||
const tempDirPrefix = executableName.replace(/[^a-zA-Z0-9]/g, "-") + "-";
|
||||
const tempDir = await mkdtemp(join4(tmpdir(), tempDirPrefix));
|
||||
const installScriptPath = join4(tempDir, "install.sh");
|
||||
log.info(`Downloading install script from ${installUrl}...`);
|
||||
const installScriptResponse = await fetch(installUrl);
|
||||
if (!installScriptResponse.ok) {
|
||||
throw new Error(`Failed to download install script: ${installScriptResponse.status}`);
|
||||
}
|
||||
if (!installScriptResponse.body) throw new Error("Response body is null");
|
||||
const fileStream = createWriteStream2(installScriptPath);
|
||||
await pipeline(installScriptResponse.body, fileStream);
|
||||
log.info(`Downloaded install script to ${installScriptPath}`);
|
||||
chmodSync(installScriptPath, 493);
|
||||
log.info("Installing to temp directory...");
|
||||
const installResult = spawnSync("bash", [installScriptPath], {
|
||||
cwd: tempDir,
|
||||
env: {
|
||||
...process.env,
|
||||
HOME: tempDir
|
||||
// Cursor install script uses HOME for installation path
|
||||
},
|
||||
stdio: "pipe",
|
||||
encoding: "utf-8"
|
||||
});
|
||||
if (installResult.status !== 0) {
|
||||
const errorOutput = installResult.stderr || installResult.stdout || "No output";
|
||||
throw new Error(
|
||||
`Failed to install ${executableName}. Install script exited with code ${installResult.status}. Output: ${errorOutput}`
|
||||
);
|
||||
}
|
||||
const cliPath = join4(tempDir, ".local", "bin", executableName);
|
||||
if (!existsSync2(cliPath)) {
|
||||
throw new Error(`Executable not found at ${cliPath}`);
|
||||
}
|
||||
chmodSync(cliPath, 493);
|
||||
log.info(`\u2713 ${executableName} installed at ${cliPath}`);
|
||||
return cliPath;
|
||||
}
|
||||
var agent = (agent2) => {
|
||||
return agent2;
|
||||
};
|
||||
@@ -33962,6 +34005,139 @@ function configureMcpServers({
|
||||
}
|
||||
}
|
||||
|
||||
// agents/cursor.ts
|
||||
import { spawn as spawn3 } from "node:child_process";
|
||||
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
||||
import { join as join5 } from "node:path";
|
||||
var cursor = agent({
|
||||
name: "cursor",
|
||||
inputKey: "cursor_api_key",
|
||||
install: async () => {
|
||||
return await installFromCurl({
|
||||
installUrl: "https://cursor.com/install",
|
||||
executableName: "cursor-agent"
|
||||
});
|
||||
},
|
||||
run: async ({ prompt, mcpServers, apiKey, cliPath, githubInstallationToken }) => {
|
||||
process.env.CURSOR_API_KEY = apiKey;
|
||||
process.env.GITHUB_INSTALLATION_TOKEN = githubInstallationToken;
|
||||
if (mcpServers && Object.keys(mcpServers).length > 0) {
|
||||
configureMcpServers2({ mcpServers, cliPath });
|
||||
}
|
||||
try {
|
||||
const fullPrompt = addInstructions(prompt);
|
||||
const tempDir = cliPath.split("/.local/bin/")[0];
|
||||
log.info("Running Cursor CLI...");
|
||||
return new Promise((resolve) => {
|
||||
const child = spawn3(
|
||||
cliPath,
|
||||
["--print", fullPrompt, "--output-format", "text", "--approve-mcps"],
|
||||
{
|
||||
cwd: process.cwd(),
|
||||
// Run in current working directory
|
||||
env: {
|
||||
...process.env,
|
||||
CURSOR_API_KEY: apiKey,
|
||||
GITHUB_INSTALLATION_TOKEN: githubInstallationToken,
|
||||
HOME: tempDir
|
||||
// Set HOME so Cursor CLI can find .cursor/mcp.json
|
||||
},
|
||||
stdio: ["ignore", "pipe", "pipe"]
|
||||
// Ignore stdin, pipe stdout/stderr
|
||||
}
|
||||
);
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
let hasOutput = false;
|
||||
const timeout = setTimeout(() => {
|
||||
if (!hasOutput && child.exitCode === null) {
|
||||
log.warning("Cursor CLI appears to be hanging, killing process...");
|
||||
child.kill("SIGTERM");
|
||||
resolve({
|
||||
success: false,
|
||||
error: "Cursor CLI timed out - no output received",
|
||||
output: stdout.trim()
|
||||
});
|
||||
}
|
||||
}, 3e5);
|
||||
child.on("spawn", () => {
|
||||
log.debug("Cursor CLI process spawned");
|
||||
});
|
||||
child.stdout?.on("data", (data) => {
|
||||
hasOutput = true;
|
||||
const text = data.toString();
|
||||
stdout += text;
|
||||
process.stdout.write(text);
|
||||
});
|
||||
child.stderr?.on("data", (data) => {
|
||||
hasOutput = true;
|
||||
const text = data.toString();
|
||||
stderr += text;
|
||||
process.stderr.write(text);
|
||||
log.warning(text);
|
||||
});
|
||||
child.on("close", (code) => {
|
||||
clearTimeout(timeout);
|
||||
if (code !== 0) {
|
||||
const errorMessage = stderr || `Cursor CLI exited with code ${code}`;
|
||||
log.error(`Cursor CLI failed: ${errorMessage}`);
|
||||
resolve({
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
output: stdout.trim()
|
||||
});
|
||||
}
|
||||
});
|
||||
child.on("error", (error2) => {
|
||||
const errorMessage = error2.message || String(error2);
|
||||
log.error(`Cursor CLI execution failed: ${errorMessage}`);
|
||||
resolve({
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
output: stdout.trim()
|
||||
});
|
||||
});
|
||||
});
|
||||
} catch (error2) {
|
||||
const errorMessage = error2 instanceof Error ? error2.message : String(error2);
|
||||
log.error(`Cursor execution failed: ${errorMessage}`);
|
||||
return {
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
output: ""
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
function configureMcpServers2({
|
||||
mcpServers,
|
||||
cliPath
|
||||
}) {
|
||||
log.info("Configuring MCP servers for Cursor...");
|
||||
const tempDir = cliPath.split("/.local/bin/")[0];
|
||||
const cursorConfigDir = join5(tempDir, ".cursor");
|
||||
const mcpConfigPath = join5(cursorConfigDir, "mcp.json");
|
||||
const mcpConfig = {
|
||||
mcpServers: {}
|
||||
};
|
||||
for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
|
||||
if (!("command" in serverConfig)) {
|
||||
log.warning(`MCP server '${serverName}' is not a stdio server, skipping...`);
|
||||
continue;
|
||||
}
|
||||
mcpConfig.mcpServers[serverName] = serverConfig;
|
||||
log.info(`Adding MCP server '${serverName}'...`);
|
||||
}
|
||||
if (Object.keys(mcpConfig.mcpServers).length === 0) {
|
||||
log.info("No MCP servers to configure");
|
||||
return;
|
||||
}
|
||||
mkdirSync2(cursorConfigDir, { recursive: true });
|
||||
writeFileSync2(mcpConfigPath, JSON.stringify(mcpConfig, null, 2), "utf-8");
|
||||
log.info(`\u2713 MCP configuration written to ${mcpConfigPath}`);
|
||||
log.info("MCP servers configured. Cursor CLI will use --approve-mcps to auto-approve servers.");
|
||||
}
|
||||
|
||||
// utils/github.ts
|
||||
var core2 = __toESM(require_core(), 1);
|
||||
import { createSign } from "node:crypto";
|
||||
@@ -34152,7 +34328,7 @@ function parseRepoContext() {
|
||||
|
||||
// utils/subprocess.ts
|
||||
import { spawn as nodeSpawn } from "node:child_process";
|
||||
async function spawn3(options) {
|
||||
async function spawn4(options) {
|
||||
const { cmd, args: args2, env: env2, input, timeout, cwd: cwd4, onStdout, onStderr } = options;
|
||||
const startTime = Date.now();
|
||||
let stdoutBuffer = "";
|
||||
@@ -34254,7 +34430,7 @@ var jules = agent({
|
||||
log.info(`Starting session with prompt: ${prompt.substring(0, 100)}...`);
|
||||
let sessionId;
|
||||
try {
|
||||
const createResult = await spawn3({
|
||||
const createResult = await spawn4({
|
||||
cmd: "node",
|
||||
args: [cliPath, "remote", "new", "--repo", repoName, "--session", sessionPrompt],
|
||||
onStdout: (chunk) => {
|
||||
@@ -34304,7 +34480,7 @@ var jules = agent({
|
||||
await new Promise((resolve) => setTimeout(resolve, 1e4));
|
||||
pollAttempts++;
|
||||
try {
|
||||
const listResult = await spawn3({
|
||||
const listResult = await spawn4({
|
||||
cmd: "node",
|
||||
args: [cliPath, "remote", "list", "--session"],
|
||||
onStdout: (chunk) => {
|
||||
@@ -34332,7 +34508,7 @@ var jules = agent({
|
||||
if (sessionId) {
|
||||
try {
|
||||
log.info(`Pulling results for session ${sessionId}...`);
|
||||
const pullResult = await spawn3({
|
||||
const pullResult = await spawn4({
|
||||
cmd: "node",
|
||||
args: [cliPath, "remote", "pull", "--session", sessionId],
|
||||
onStdout: (chunk) => {
|
||||
@@ -34368,6 +34544,7 @@ var jules = agent({
|
||||
var agents = {
|
||||
claude,
|
||||
codex,
|
||||
cursor,
|
||||
jules
|
||||
};
|
||||
|
||||
@@ -42098,7 +42275,7 @@ var caller = (options = {}) => {
|
||||
};
|
||||
|
||||
// node_modules/.pnpm/@ark+fs@0.53.0/node_modules/@ark/fs/out/fs.js
|
||||
import { dirname as dirname2, join as join5, parse } from "node:path";
|
||||
import { dirname as dirname2, join as join6, parse } from "node:path";
|
||||
import * as process3 from "node:process";
|
||||
import { URL as URL2, fileURLToPath as fileURLToPath4 } from "node:url";
|
||||
var filePath = (path4) => {
|
||||
@@ -42112,7 +42289,7 @@ var filePath = (path4) => {
|
||||
return file;
|
||||
};
|
||||
var dirOfCaller = () => dirname2(filePath(caller({ methodName: "dirOfCaller", upStackBy: 1 }).file));
|
||||
var fromHere = (...joinWith) => join5(dirOfCaller(), ...joinWith);
|
||||
var fromHere = (...joinWith) => join6(dirOfCaller(), ...joinWith);
|
||||
var fsRoot = parse(process3.cwd()).root;
|
||||
|
||||
// mcp/config.ts
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
create a comment on https://github.com/pullfrogai/scratch/issues/21 with an implementation of an mcp tool for fetching issue comments from github
|
||||
create a comment on https://github.com/pullfrogai/scratch/pull/29 that says GEM ribbit
|
||||
@@ -1,7 +1,7 @@
|
||||
import { flatMorph } from "@ark/util";
|
||||
import { type } from "arktype";
|
||||
import { agents } from "./agents/index.ts";
|
||||
import { createMcpConfigs, forEachStdioMcpServer } from "./mcp/config.ts";
|
||||
import { createMcpConfigs } from "./mcp/config.ts";
|
||||
import packageJson from "./package.json" with { type: "json" };
|
||||
import { fetchRepoSettings } from "./utils/api.ts";
|
||||
import { log } from "./utils/cli.ts";
|
||||
@@ -141,18 +141,6 @@ export async function main(inputs: Inputs): Promise<MainResult> {
|
||||
|
||||
const apiKey = inputs[matchingInputKey]!;
|
||||
|
||||
// Configure MCP servers if provided (global config is fine - not part of repo)
|
||||
if (mcpServers && Object.keys(mcpServers).length > 0) {
|
||||
log.info(`Configuring MCP servers for ${agentName}...`);
|
||||
forEachStdioMcpServer(mcpServers, (serverName, serverConfig) => {
|
||||
agent.addMcpServer({
|
||||
serverName,
|
||||
serverConfig,
|
||||
cliPath,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const result = await agent.run({
|
||||
prompt: inputs.prompt,
|
||||
mcpServers,
|
||||
|
||||
+4
-5
@@ -2,7 +2,7 @@
|
||||
* Simple MCP configuration helper for adding our minimal GitHub comment server
|
||||
*/
|
||||
|
||||
import type { McpServerConfig } from "@anthropic-ai/claude-agent-sdk";
|
||||
import type { McpServerConfig, McpStdioServerConfig } from "@anthropic-ai/claude-agent-sdk";
|
||||
import { fromHere } from "@ark/fs";
|
||||
import { log } from "../utils/cli.ts";
|
||||
import { parseRepoContext } from "../utils/github.ts";
|
||||
@@ -10,7 +10,7 @@ import { ghPullfrogMcpName } from "./index.ts";
|
||||
|
||||
export type McpName = typeof ghPullfrogMcpName;
|
||||
|
||||
export type McpConfigs = Record<McpName, McpServerConfig>;
|
||||
export type McpConfigs = Record<McpName, McpStdioServerConfig>;
|
||||
|
||||
export function createMcpConfigs(githubInstallationToken: string): McpConfigs {
|
||||
const repoContext = parseRepoContext();
|
||||
@@ -38,7 +38,7 @@ export function createMcpConfigs(githubInstallationToken: string): McpConfigs {
|
||||
*/
|
||||
export function forEachStdioMcpServer(
|
||||
mcpServers: Record<string, McpServerConfig>,
|
||||
handler: (serverName: string, serverConfig: Extract<McpServerConfig, { command: string }>) => void
|
||||
handler: (serverName: string, serverConfig: McpStdioServerConfig) => void
|
||||
): void {
|
||||
for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
|
||||
// Only configure stdio servers (CLIs support stdio MCP servers)
|
||||
@@ -46,7 +46,6 @@ export function forEachStdioMcpServer(
|
||||
log.warning(`MCP server '${serverName}' is not a stdio server, skipping...`);
|
||||
continue;
|
||||
}
|
||||
handler(serverName, serverConfig as Extract<McpServerConfig, { command: string }>);
|
||||
handler(serverName, serverConfig);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+67
-1
@@ -1,3 +1,5 @@
|
||||
import { appendFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { cached } from "@ark/util";
|
||||
import { Octokit } from "@octokit/rest";
|
||||
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
||||
@@ -30,7 +32,71 @@ export interface McpContext extends RepoContext {
|
||||
octokit: Octokit;
|
||||
}
|
||||
|
||||
export const tool = <const params>(tool: Tool<any, StandardSchemaV1<params>>) => tool;
|
||||
/**
|
||||
* Get the log file path
|
||||
*/
|
||||
function getLogPath(): string {
|
||||
return join(process.cwd(), "log.txt");
|
||||
}
|
||||
|
||||
/**
|
||||
* Log MCP tool call information to log.txt
|
||||
*/
|
||||
function logToolCall({
|
||||
toolName,
|
||||
request,
|
||||
error,
|
||||
success,
|
||||
}: {
|
||||
toolName: string;
|
||||
request: unknown;
|
||||
error?: unknown;
|
||||
success?: boolean;
|
||||
}) {
|
||||
const logPath = getLogPath();
|
||||
const timestamp = new Date().toISOString();
|
||||
const requestStr = JSON.stringify(request, null, 2);
|
||||
|
||||
let logEntry = `[${timestamp}] Tool: ${toolName}\nRequest: ${requestStr}\n`;
|
||||
|
||||
if (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
const errorStack = error instanceof Error ? error.stack : undefined;
|
||||
logEntry += `Error: ${errorMessage}\n`;
|
||||
if (errorStack) {
|
||||
logEntry += `Stack: ${errorStack}\n`;
|
||||
}
|
||||
logEntry += `Status: FAILED\n`;
|
||||
} else if (success !== undefined) {
|
||||
logEntry += `Status: ${success ? "SUCCESS" : "FAILED"}\n`;
|
||||
}
|
||||
|
||||
logEntry += `${"=".repeat(80)}\n\n`;
|
||||
appendFileSync(logPath, logEntry, "utf-8");
|
||||
}
|
||||
|
||||
export const tool = <const params>(toolDef: Tool<any, StandardSchemaV1<params>>) => {
|
||||
// Wrap the execute function to add logging with the tool name
|
||||
const toolName = toolDef.name;
|
||||
const originalExecute = toolDef.execute;
|
||||
|
||||
toolDef.execute = async (args: params, context: any) => {
|
||||
try {
|
||||
logToolCall({ toolName, request: args });
|
||||
const result = await originalExecute(args, context);
|
||||
// Check if result is a ToolResult with isError property
|
||||
const isError =
|
||||
result && typeof result === "object" && "isError" in result && result.isError === true;
|
||||
logToolCall({ toolName, request: args, success: !isError });
|
||||
return result;
|
||||
} catch (error) {
|
||||
logToolCall({ toolName, request: args, error });
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
return toolDef;
|
||||
};
|
||||
|
||||
export const addTools = (server: FastMCP, tools: Tool<any, any>[]) => {
|
||||
for (const tool of tools) {
|
||||
|
||||
Reference in New Issue
Block a user