Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 48 additions & 12 deletions commands/artifacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,16 @@
import { initializeRuntime } from "../lib/runtime-init.js";
import { validateRepo } from "../lib/gh.js";
import { loadConfig } from "../lib/config.js";
import { findLatestAgentByPrUrl, getArtifactDownloadUrl, listAgentArtifacts } from "../lib/cursor-api.js";
import {
findLatestAgentByPrUrl as findLatestCursorAgentByPrUrl,
getArtifactDownloadUrl as getCursorArtifactDownloadUrl,
listAgentArtifacts as listCursorAgentArtifacts,
} from "../lib/cursor-api.js";
import {
findLatestAgentByPrUrl as findLatestClaudeAgentByPrUrl,
getArtifactDownloadUrl as getClaudeArtifactDownloadUrl,
listAgentArtifacts as listClaudeAgentArtifacts,
} from "../lib/claude-api.js";
import { formatBytes } from "../lib/format.js";
import { createWriteStream } from "node:fs";
import { pipeline } from "node:stream/promises";
Expand Down Expand Up @@ -107,30 +116,57 @@ async function main(): Promise<void> {
const { repo, prNumber, downloadPath, outFile } = parseArgs(process.argv.slice(2));
validateRepo(repo);

const cursorApiKey = loadConfig()?.cursorApiKey?.trim() || "";
if (!cursorApiKey) {
console.error('Cursor API not configured. Set "cursorApiKey" in .copserc.');
const config = loadConfig();
const cursorApiKey = config?.cursorApiKey?.trim() || "";
const claudeApiKey = config?.claudeApiKey?.trim() || "";
if (!cursorApiKey && !claudeApiKey) {
console.error('No agent API configured. Set "cursorApiKey" or "claudeApiKey" in .copserc.');
process.exit(1);
}

const prUrl = `https://github.com/${repo}/pull/${prNumber}`;
const agent = await findLatestAgentByPrUrl(cursorApiKey, prUrl);
if (!agent) {
console.error(`No Cursor agent linked to ${prUrl}`);

// Try Cursor first, then Claude
let agentId: string | null = null;
let agentLabel = "";
let artifactList: Array<{ absolutePath: string; sizeBytes?: number; updatedAt?: string }> = [];
let getDownloadUrl: ((agId: string, path: string) => Promise<{ url: string; expiresAt?: string }>) | null = null;

if (cursorApiKey) {
const agent = await findLatestCursorAgentByPrUrl(cursorApiKey, prUrl);
if (agent) {
agentId = agent.id;
agentLabel = "Cursor";
artifactList = await listCursorAgentArtifacts(cursorApiKey, agent.id);
getDownloadUrl = (agId, path) => getCursorArtifactDownloadUrl(cursorApiKey, agId, path);
}
}

if (!agentId && claudeApiKey) {
const agent = await findLatestClaudeAgentByPrUrl(claudeApiKey, prUrl);
if (agent) {
agentId = agent.id;
agentLabel = "Claude";
artifactList = await listClaudeAgentArtifacts(claudeApiKey, agent.id);
getDownloadUrl = (agId, path) => getClaudeArtifactDownloadUrl(claudeApiKey, agId, path);
}
}

if (!agentId) {
console.error(`No agent linked to ${prUrl}`);
process.exit(1);
}

const artifacts = await listAgentArtifacts(cursorApiKey, agent.id);
console.log(`Cursor agent: ${agent.id}`);
console.log(`${agentLabel} agent: ${agentId}`);
console.log(`PR: ${prUrl}`);
console.log("");

if (artifacts.length === 0) {
if (artifactList.length === 0) {
console.log("No artifacts found.");
process.exit(0);
}

for (const a of artifacts) {
for (const a of artifactList) {
const size = formatBytes(a.sizeBytes ?? null);
const updated = a.updatedAt ? new Date(a.updatedAt).toISOString() : "";
console.log(`${a.absolutePath} ${size}${updated ? ` ${updated}` : ""}`);
Expand All @@ -139,7 +175,7 @@ async function main(): Promise<void> {
if (!downloadPath) return;

const out = outFile || `./${pathBasename(downloadPath) || "artifact"}`;
const { url } = await getArtifactDownloadUrl(cursorApiKey, agent.id, downloadPath);
const { url } = await getDownloadUrl!(agentId, downloadPath);
await downloadFile(url, out);
console.log("");
console.log(`Downloaded to ${out}`);
Expand Down
38 changes: 36 additions & 2 deletions commands/create-issue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ import { initializeRuntime } from "../lib/runtime-init.js";
import { REPO_PATTERN, validateRepo, validateAgent } from "../lib/gh.js";
import { getOriginRepo } from "../lib/utils.js";
import { loadConfig } from "../lib/config.js";
import { launchAgentForRepository } from "../lib/cursor-api.js";
import { launchAgentForRepository as launchCursorAgentForRepository } from "../lib/cursor-api.js";
import { launchAgentForRepository as launchClaudeAgentForRepository } from "../lib/claude-api.js";

initializeRuntime();

Expand Down Expand Up @@ -211,7 +212,7 @@ async function sendInstructionViaCursorApi(
return;
}

const id = await launchAgentForRepository(
const id = await launchCursorAgentForRepository(
cursorApiKey,
`https://github.com/${repo}`,
prompt,
Expand All @@ -220,6 +221,31 @@ async function sendInstructionViaCursorApi(
console.error(`${ANSI.green}Cursor agent launched: ${id}${ANSI.reset}`);
}

async function sendInstructionViaClaudeApi(
repo: string,
issueNumber: number,
agent: string,
comment: string,
claudeApiKey: string,
dryRun: boolean
): Promise<void> {
const issueUrl = `https://github.com/${repo}/issues/${issueNumber}`;
const instruction = stripAgentMention(comment, agent);
const prompt = `${instruction}\n\nIssue: ${issueUrl}`;
if (dryRun) {
console.error(`Would launch Claude agent for ${issueUrl} with prompt: "${prompt.slice(0, 120)}${prompt.length > 120 ? "..." : ""}"`);
return;
}

const id = await launchClaudeAgentForRepository(
claudeApiKey,
`https://github.com/${repo}`,
prompt,
{ autoCreatePr: true }
);
console.error(`${ANSI.green}Claude agent launched: ${id}${ANSI.reset}`);
}

async function promptTitle(rl: readline.Interface): Promise<string> {
for (;;) {
const raw = await rl.question(`${ANSI.bold}Issue title:${ANSI.reset} `);
Expand Down Expand Up @@ -396,7 +422,9 @@ Examples:
agent = validateAgent(agent);
const config = loadConfig();
const cursorApiKey = config?.cursorApiKey?.trim() || null;
const claudeApiKey = config?.claudeApiKey?.trim() || null;
const shouldUseCursorApi = agent === "cursor" && cursorApiKey !== null;
const shouldUseClaudeApi = agent === "claude" && claudeApiKey !== null;

const isInteractive = stdout.isTTY;

Expand Down Expand Up @@ -442,6 +470,8 @@ Examples:
if (comment) {
if (shouldUseCursorApi) {
await sendInstructionViaCursorApi(repo, issueNumber, agent, comment, cursorApiKey!, dryRun);
} else if (shouldUseClaudeApi) {
await sendInstructionViaClaudeApi(repo, issueNumber, agent, comment, claudeApiKey!, dryRun);
} else {
addComment(repo, issueNumber, comment, dryRun);
}
Expand All @@ -451,6 +481,8 @@ Examples:
if (!dryRun && comment) {
if (shouldUseCursorApi) {
console.error(`${ANSI.green}Sent instruction to Cursor API (instead of issue comment).${ANSI.reset}`);
} else if (shouldUseClaudeApi) {
console.error(`${ANSI.green}Sent instruction to Claude API (instead of issue comment).${ANSI.reset}`);
} else {
console.error(`${ANSI.green}Commented: ${comment}${ANSI.reset}`);
}
Expand All @@ -459,6 +491,8 @@ Examples:
if (comment) {
if (shouldUseCursorApi) {
console.error(`Would send instruction to Cursor API: "${comment}"`);
} else if (shouldUseClaudeApi) {
console.error(`Would send instruction to Claude API: "${comment}"`);
} else {
console.error(`Would add comment: "${comment}"`);
}
Expand Down
31 changes: 27 additions & 4 deletions commands/pr-comments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { parseStandardFlags, parseTemplatesOption } from "../lib/args.js";
import { filterPRs, getUserForDisplay, buildFetchMessage } from "../lib/filters.js";
import { loadConfig } from "../lib/config.js";
import { sendReplyViaCursorApi } from "../lib/cursor-replies.js";
import { sendReplyViaClaudeApi } from "../lib/claude-replies.js";
import {
loadTemplates,
scaffoldTemplates,
Expand Down Expand Up @@ -181,17 +182,19 @@ Examples:

let templatesPath: string;
let cursorApiKey: string | null = null;
let claudeApiKey: string | null = null;
try {
const templatesFromFlag = parseTemplatesOption(process.argv.slice(2));
const config = loadConfig();
cursorApiKey = config?.cursorApiKey?.trim() || null;
claudeApiKey = config?.claudeApiKey?.trim() || null;
templatesPath = resolveTemplatesPath(templatesFromFlag ?? null, config?.commentTemplates ?? null);
} catch (e: unknown) {
console.error((e as Error).message);
process.exit(1);
}

runInteractiveLoop(repo, comments, templatesPath, cursorApiKey).catch((e: unknown) => {
runInteractiveLoop(repo, comments, templatesPath, cursorApiKey, claudeApiKey).catch((e: unknown) => {
console.error(`\x1b[31merror\x1b[0m ${(e as Error).message}`);
process.exit(1);
});
Expand All @@ -201,7 +204,8 @@ async function runInteractiveLoop(
repo: string,
comments: CommentWithContext[],
templatesPath: string,
cursorApiKey: string | null
cursorApiKey: string | null,
claudeApiKey: string | null
): Promise<void> {
const rl = readline.createInterface({ input: stdin, output: stdout });

Expand Down Expand Up @@ -284,12 +288,31 @@ async function runInteractiveLoop(
}

try {
if (cursorApiKey) {
const agentApiKey =
ctx.agent === "claude" && claudeApiKey ? { agent: "claude" as const, key: claudeApiKey } :
ctx.agent === "cursor" && cursorApiKey ? { agent: "cursor" as const, key: cursorApiKey } :
cursorApiKey ? { agent: "cursor" as const, key: cursorApiKey } :
claudeApiKey ? { agent: "claude" as const, key: claudeApiKey } :
null;

if (agentApiKey?.agent === "claude") {
const result = await sendReplyViaClaudeApi({
repo,
prNumber: ctx.prNumber,
replyText: replyTrimmed,
claudeApiKey: agentApiKey.key,
});
if (result.mode === "followup") {
console.log(`\x1b[32mReply sent to Claude agent (${result.agentId}) via follow-up.\x1b[0m`);
} else {
console.log(`\x1b[32mNo linked agent found; launched new Claude agent (${result.agentId}).\x1b[0m`);
}
} else if (agentApiKey?.agent === "cursor") {
const result = await sendReplyViaCursorApi({
repo,
prNumber: ctx.prNumber,
replyText: replyTrimmed,
cursorApiKey,
cursorApiKey: agentApiKey.key,
});
if (result.mode === "followup") {
console.log(`\x1b[32mReply sent to Cursor agent (${result.agentId}) via follow-up.\x1b[0m`);
Expand Down
Loading