Files
bitvid/tools/fleet/adapters/cursor.mjs
T

107 lines
4.4 KiB
JavaScript

// adapters/cursor.mjs — Cursor's agent CLI (`agent`, same binary as `cursor-agent`).
//
// Verified from `agent --help` of 2026.10.16-e3cb207 and the parameter / output-format
// docs. No authenticated model turn was run. See docs/adapters.md.
//
// agent --trust [--model M] [prompt] new interactive session
// agent --trust [--model M] --resume <id> | --continue resume
// agent -p --output-format json --trust --force [--model M] one-shot
//
// There is no --append-system-prompt-file, --autocompact, --remote-control, -n, or
// --bg. The composed prompt is a file the first message tells the agent to read.
// `agent persist` exists but its flags were not shown to match a foreground argv,
// so background mode stays unsupported (tmux is the launch path).
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import crypto from 'node:crypto';
import { messaging as mailboxMessaging } from './mailbox.mjs';
import { baseVerdict, modelArgs, parseJsonRecords, walkFiles } from './contract.mjs';
export const name = 'cursor';
export const complete = true;
export const experimental = false;
export const bin = 'agent';
export const tiers = { strong: null, standard: null, light: null };
export const capabilities = {
interactive: true,
resume: true,
background: false,
headless: true,
doneSignal: 'json-result',
prompt: 'prompt-file-in-first-message',
autoMode: ['--trust', '--force'],
contextFlag: null,
remoteControl: null,
nativeMessaging: false,
};
export const skillsDir = '.cursor/rules';
const home = (env) => env?.HOME || os.homedir();
function promptLead(promptFile, firstPrompt) {
const note = promptFile
? `Read and follow the fleet prompt at ${promptFile} before acting. The file on disk wins over anything remembered.`
: '';
return [note, firstPrompt].filter(Boolean).join('\n\n');
}
function storeOf(sessionId, env) {
const root = path.join(home(env), '.cursor', 'chats');
return walkFiles(root, (name, p) => name === sessionId && fs.existsSync(path.join(p, 'store.db')), 0, 2);
}
function workspaceHasChat(cwd, env) {
if (!cwd) return false;
const dir = path.join(home(env), '.cursor', 'chats', crypto.createHash('md5').update(path.resolve(cwd)).digest('hex'));
try { return fs.readdirSync(dir).some((id) => fs.existsSync(path.join(dir, id, 'store.db'))); }
catch { return false; }
}
/** New interactive session. sessionId is not a Cursor chat id until create-chat, which we do not call. */
export function start({ model, promptFile, firstPrompt } = {}) {
const prompt = promptLead(promptFile, firstPrompt);
return [bin, '--trust', ...modelArgs(model), ...(prompt ? [prompt] : [])];
}
/** Resume a known chat id, or the latest chat in this workspace. */
export function resume({ model, sessionId, cwd, env } = {}) {
if (sessionId && storeOf(sessionId, env)) return [bin, '--trust', ...modelArgs(model), '--resume', sessionId];
if (workspaceHasChat(cwd, env)) return [bin, '--trust', ...modelArgs(model), '--continue'];
return null;
}
export const background = null;
export function headless({ prompt, model, extraArgs = [] } = {}) {
const extras = [...extraArgs];
for (const flag of ['--trust', '--force']) if (!extras.includes(flag)) extras.push(flag);
return [bin, '-p', '--output-format', 'json', ...modelArgs(model), ...extras, prompt];
}
/**
* Success is a JSON result object with subtype "success" and is_error false.
* Exit 0 without that object is failure. Works for `json` and `stream-json`.
*/
export function completed(output, meta = {}) {
const base = baseVerdict(meta);
if (base) return base;
if (meta.status !== 0) return { ok: false, reason: `exit:${meta.status}` };
const result = [...parseJsonRecords(output)].reverse().find((r) => r && r.type === 'result');
if (!result || result.is_error || result.subtype !== 'success') {
return { ok: false, reason: result ? `result:${result.subtype ?? 'error'}` : 'no-completed-result', sessionId: result?.session_id ?? null };
}
return { ok: true, reason: 'result', sessionId: result.session_id ?? null };
}
export function canResume(sessionId, { cwd, env } = {}) {
if (sessionId && storeOf(sessionId, env)) return true;
return workspaceHasChat(cwd, env);
}
export const list = () => null;
export const stop = null;
export const cleanEnv = (env = process.env) => ({ ...env });
export const messaging = mailboxMessaging;
export const usage = () => null;