feat(billing): bill Cursor executor usage via stream-json token parsing
Memind CI / Test, build, and release guards (push) Failing after 14m36s
Memind CI / Test, build, and release guards (push) Failing after 14m36s
Parse Cursor CLI usage from stream-json output and charge through billSessionUsage so subscription quota is consumed first and wallet overage applies when needed. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+89
-2
@@ -44,6 +44,7 @@ import {
|
|||||||
} from './executor-display-label.mjs';
|
} from './executor-display-label.mjs';
|
||||||
import { applyCursorFirstAgentExecution } from './cursor-page-routing.mjs';
|
import { applyCursorFirstAgentExecution } from './cursor-page-routing.mjs';
|
||||||
import { cursorDeepseekFallbackEnabled } from './cursor-agent-launch.mjs';
|
import { cursorDeepseekFallbackEnabled } from './cursor-agent-launch.mjs';
|
||||||
|
import { buildCursorBillingTokenState } from './cursor-agent-usage.mjs';
|
||||||
|
|
||||||
const DEFAULT_RUN_RETRY_DELAYS_MS = [1500, 5000, 15000];
|
const DEFAULT_RUN_RETRY_DELAYS_MS = [1500, 5000, 15000];
|
||||||
const TERMINAL_STATUSES = new Set(['succeeded', 'failed']);
|
const TERMINAL_STATUSES = new Set(['succeeded', 'failed']);
|
||||||
@@ -369,13 +370,83 @@ function summarizeText(value, limit = TOOL_GATEWAY_SUMMARY_LIMIT) {
|
|||||||
|
|
||||||
export function buildCodeRunCompletionReply(result) {
|
export function buildCodeRunCompletionReply(result) {
|
||||||
const executor = resolveExecutorDisplayLabel(result?.executor, result?.executorLabel);
|
const executor = resolveExecutorDisplayLabel(result?.executor, result?.executorLabel);
|
||||||
const output = sanitizeUserFacingBrandText(summarizeText(result?.stdout, 2400).trim());
|
const rawOutput = result?.displayStdout ?? result?.stdout;
|
||||||
|
const output = sanitizeUserFacingBrandText(summarizeText(rawOutput, 2400).trim());
|
||||||
return [
|
return [
|
||||||
`已由 ${executor} 完成执行,并通过平台文件验收。`,
|
`已由 ${executor} 完成执行,并通过平台文件验收。`,
|
||||||
output ? `\n${output}` : '',
|
output ? `\n${output}` : '',
|
||||||
].join('').trim();
|
].join('').trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function billCursorToolGatewayUsage({
|
||||||
|
userAuth,
|
||||||
|
userId,
|
||||||
|
sessionId,
|
||||||
|
requestId,
|
||||||
|
usage,
|
||||||
|
}) {
|
||||||
|
if (!usage || !sessionId || !userAuth?.billSessionUsage) return null;
|
||||||
|
const prior = typeof userAuth.getBillingState === 'function'
|
||||||
|
? await userAuth.getBillingState(sessionId).catch(() => null)
|
||||||
|
: null;
|
||||||
|
const tokenState = buildCursorBillingTokenState(usage, prior);
|
||||||
|
if (!tokenState) return null;
|
||||||
|
return userAuth.billSessionUsage(userId, sessionId, tokenState, requestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveCursorBillingSessionId({ deliverySessionId, agentSessionId, runId }) {
|
||||||
|
const delivery = String(deliverySessionId ?? '').trim();
|
||||||
|
if (delivery) return delivery;
|
||||||
|
const session = String(agentSessionId ?? '').trim();
|
||||||
|
if (session) return session;
|
||||||
|
const run = String(runId ?? '').trim();
|
||||||
|
return run ? `h5run_${run}` : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function recordCursorToolGatewayBilling({
|
||||||
|
appendEvent,
|
||||||
|
runId,
|
||||||
|
row,
|
||||||
|
result,
|
||||||
|
userAuth,
|
||||||
|
deliverySessionId = null,
|
||||||
|
}) {
|
||||||
|
if (String(result?.executor ?? '').trim().toLowerCase() !== 'cursor' || !result?.usage) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const billingSessionId = resolveCursorBillingSessionId({
|
||||||
|
deliverySessionId,
|
||||||
|
agentSessionId: row.agent_session_id,
|
||||||
|
runId,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const billing = await billCursorToolGatewayUsage({
|
||||||
|
userAuth,
|
||||||
|
userId: row.user_id,
|
||||||
|
sessionId: billingSessionId,
|
||||||
|
requestId: row.request_id,
|
||||||
|
usage: result.usage,
|
||||||
|
});
|
||||||
|
await appendEvent(runId, 'tool_gateway_billed', {
|
||||||
|
sessionId: billingSessionId,
|
||||||
|
executor: result.executor ?? null,
|
||||||
|
usage: result.usage,
|
||||||
|
billed: Boolean(billing?.ok),
|
||||||
|
costCents: billing?.costCents ?? 0,
|
||||||
|
deltaInputTokens: billing?.deltaInputTokens ?? 0,
|
||||||
|
deltaOutputTokens: billing?.deltaOutputTokens ?? 0,
|
||||||
|
});
|
||||||
|
return billing;
|
||||||
|
} catch (err) {
|
||||||
|
await appendEvent(runId, 'tool_gateway_billing_failed', {
|
||||||
|
sessionId: billingSessionId,
|
||||||
|
executor: result.executor ?? null,
|
||||||
|
message: err instanceof Error ? err.message : String(err),
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeExpectedFileCheck(value) {
|
function normalizeExpectedFileCheck(value) {
|
||||||
if (typeof value === 'string') {
|
if (typeof value === 'string') {
|
||||||
const expectedPath = value.trim();
|
const expectedPath = value.trim();
|
||||||
@@ -1778,8 +1849,9 @@ export function createAgentRunGateway({
|
|||||||
executor: result.executor ?? null,
|
executor: result.executor ?? null,
|
||||||
dryRun: Boolean(result.dryRun),
|
dryRun: Boolean(result.dryRun),
|
||||||
exitCode: result.exitCode ?? null,
|
exitCode: result.exitCode ?? null,
|
||||||
stdoutTail: summarizeText(result.stdout),
|
stdoutTail: summarizeText(result.displayStdout ?? result.stdout),
|
||||||
stderrTail: summarizeText(result.stderr),
|
stderrTail: summarizeText(result.stderr),
|
||||||
|
usage: result.usage ?? null,
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
const validation = await validateToolGatewayResult({
|
const validation = await validateToolGatewayResult({
|
||||||
@@ -1828,8 +1900,23 @@ export function createAgentRunGateway({
|
|||||||
sessionId: delivery.sessionId,
|
sessionId: delivery.sessionId,
|
||||||
executor: result.executor ?? null,
|
executor: result.executor ?? null,
|
||||||
});
|
});
|
||||||
|
await recordCursorToolGatewayBilling({
|
||||||
|
appendEvent,
|
||||||
|
runId,
|
||||||
|
row,
|
||||||
|
result,
|
||||||
|
userAuth,
|
||||||
|
deliverySessionId: delivery.sessionId,
|
||||||
|
});
|
||||||
return { sessionId: delivery.sessionId, routing };
|
return { sessionId: delivery.sessionId, routing };
|
||||||
}
|
}
|
||||||
|
await recordCursorToolGatewayBilling({
|
||||||
|
appendEvent,
|
||||||
|
runId,
|
||||||
|
row,
|
||||||
|
result,
|
||||||
|
userAuth,
|
||||||
|
});
|
||||||
return { sessionId: row.agent_session_id ?? null, routing };
|
return { sessionId: row.agent_session_id ?? null, routing };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!(await fallbackCursorExecutorToDeepseek(err))) throw err;
|
if (!(await fallbackCursorExecutorToDeepseek(err))) throw err;
|
||||||
|
|||||||
@@ -3131,6 +3131,98 @@ test('required Aider run persists a validated result into a chat session', async
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('required Cursor run bills usage after validated delivery', async () => {
|
||||||
|
const pool = createFakePool();
|
||||||
|
const workdir = await fs.mkdtemp(path.join(os.tmpdir(), 'memind-cursor-billing-'));
|
||||||
|
const deliveries = [];
|
||||||
|
const billingCalls = [];
|
||||||
|
const gateway = createAgentRunGateway({
|
||||||
|
pool,
|
||||||
|
userAuth: {
|
||||||
|
async resolveWorkingDir() {
|
||||||
|
return workdir;
|
||||||
|
},
|
||||||
|
async getBillingState() {
|
||||||
|
return { lastInputTokens: 100, lastOutputTokens: 20 };
|
||||||
|
},
|
||||||
|
async billSessionUsage(userId, sessionId, tokenState, requestId) {
|
||||||
|
billingCalls.push({ userId, sessionId, tokenState, requestId });
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
costCents: 3,
|
||||||
|
deltaInputTokens: tokenState.accumulatedInputTokens - 100,
|
||||||
|
deltaOutputTokens: tokenState.accumulatedOutputTokens - 20,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tkmindProxy: {},
|
||||||
|
directChatService: {
|
||||||
|
async respondDeterministically(options) {
|
||||||
|
deliveries.push(options);
|
||||||
|
await options.onSessionReady('h5direct_cursor_result');
|
||||||
|
return { sessionId: 'h5direct_cursor_result' };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
toolGateway: {
|
||||||
|
getStatus() {
|
||||||
|
return {
|
||||||
|
enabled: true,
|
||||||
|
protocol: 'agent-run-v1',
|
||||||
|
executors: ['cursor', 'aider', 'openhands'],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
async executeJob() {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
dryRun: false,
|
||||||
|
executor: 'cursor',
|
||||||
|
exitCode: 0,
|
||||||
|
cwd: workdir,
|
||||||
|
stdout: '{"type":"result","subtype":"success","result":"done","usage":{"inputTokens":1000,"outputTokens":200}}',
|
||||||
|
displayStdout: 'done',
|
||||||
|
usage: { inputTokens: 1000, outputTokens: 200 },
|
||||||
|
stderr: '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
retryDelaysMs: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const run = await gateway.createRun('user-1', {
|
||||||
|
requestId: 'req-cursor-billing',
|
||||||
|
userMessage: {
|
||||||
|
role: 'user',
|
||||||
|
content: [{ type: 'text', text: 'build the page' }],
|
||||||
|
metadata: {
|
||||||
|
displayText: 'build the page',
|
||||||
|
memindRun: {
|
||||||
|
requiredExecutor: 'cursor',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
toolMode: 'code',
|
||||||
|
taskType: 'h5_chat_code_task',
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => pool.runs.get(run.id)?.status === 'succeeded');
|
||||||
|
assert.equal(deliveries.length, 1);
|
||||||
|
assert.match(deliveries[0].reply, /done/);
|
||||||
|
assert.equal(billingCalls.length, 1);
|
||||||
|
assert.equal(billingCalls[0].userId, 'user-1');
|
||||||
|
assert.equal(billingCalls[0].sessionId, 'h5direct_cursor_result');
|
||||||
|
assert.equal(billingCalls[0].requestId, 'req-cursor-billing');
|
||||||
|
assert.deepEqual(billingCalls[0].tokenState, {
|
||||||
|
accumulatedInputTokens: 1100,
|
||||||
|
accumulatedOutputTokens: 220,
|
||||||
|
});
|
||||||
|
assert.equal(
|
||||||
|
pool.events.some(
|
||||||
|
(event) => event.runId === run.id && event.eventType === 'tool_gateway_billed',
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('agent run fails non-retryably when tool gateway artifact validation fails', async () => {
|
test('agent run fails non-retryably when tool gateway artifact validation fails', async () => {
|
||||||
const pool = createFakePool();
|
const pool = createFakePool();
|
||||||
const workdir = await fs.mkdtemp(path.join(os.tmpdir(), 'memind-tool-validation-missing-'));
|
const workdir = await fs.mkdtemp(path.join(os.tmpdir(), 'memind-tool-validation-missing-'));
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ export function buildCursorExecutorLaunchPlan({
|
|||||||
'--force',
|
'--force',
|
||||||
'--approve-mcps',
|
'--approve-mcps',
|
||||||
'--output-format',
|
'--output-format',
|
||||||
'text',
|
'stream-json',
|
||||||
'--workspace',
|
'--workspace',
|
||||||
workspace,
|
workspace,
|
||||||
prompt,
|
prompt,
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ test('buildCursorExecutorLaunchPlan includes workspace and mindspace hints', asy
|
|||||||
assert.equal(plan.executor, 'cursor');
|
assert.equal(plan.executor, 'cursor');
|
||||||
assert.equal(plan.cwd, '/tmp/mindspace/user-1');
|
assert.equal(plan.cwd, '/tmp/mindspace/user-1');
|
||||||
assert.equal(plan.command, agentStub);
|
assert.equal(plan.command, agentStub);
|
||||||
|
assert.ok(plan.args.includes('--output-format'));
|
||||||
|
assert.ok(plan.args.includes('stream-json'));
|
||||||
assert.ok(plan.args.includes('--workspace'));
|
assert.ok(plan.args.includes('--workspace'));
|
||||||
assert.ok(plan.args.includes('/tmp/mindspace/user-1'));
|
assert.ok(plan.args.includes('/tmp/mindspace/user-1'));
|
||||||
assert.match(plan.args.at(-1) ?? '', /public\/.*\.html/);
|
assert.match(plan.args.at(-1) ?? '', /public\/.*\.html/);
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
function parseStreamJsonLine(line) {
|
||||||
|
const text = String(line ?? '').trim();
|
||||||
|
if (!text) return null;
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(text);
|
||||||
|
return parsed && typeof parsed === 'object' ? parsed : null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseCursorAgentStreamJsonEvents(stdout) {
|
||||||
|
const events = [];
|
||||||
|
for (const line of String(stdout ?? '').split('\n')) {
|
||||||
|
const parsed = parseStreamJsonLine(line);
|
||||||
|
if (parsed) events.push(parsed);
|
||||||
|
}
|
||||||
|
return events;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeCursorAgentUsage(raw) {
|
||||||
|
if (!raw || typeof raw !== 'object') return null;
|
||||||
|
const inputTokens = Number(raw.inputTokens ?? raw.input_tokens ?? 0);
|
||||||
|
const outputTokens = Number(raw.outputTokens ?? raw.output_tokens ?? 0);
|
||||||
|
const cacheReadTokens = Number(raw.cacheReadTokens ?? raw.cache_read_tokens ?? 0);
|
||||||
|
const cacheWriteTokens = Number(raw.cacheWriteTokens ?? raw.cache_write_tokens ?? 0);
|
||||||
|
const billableInput = inputTokens + cacheWriteTokens;
|
||||||
|
const billableOutput = outputTokens;
|
||||||
|
if (billableInput <= 0 && billableOutput <= 0) return null;
|
||||||
|
return {
|
||||||
|
inputTokens: billableInput,
|
||||||
|
outputTokens: billableOutput,
|
||||||
|
cacheReadTokens: cacheReadTokens > 0 ? cacheReadTokens : 0,
|
||||||
|
cacheWriteTokens: cacheWriteTokens > 0 ? cacheWriteTokens : 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseCursorAgentUsage(stdout) {
|
||||||
|
const events = parseCursorAgentStreamJsonEvents(stdout);
|
||||||
|
for (let index = events.length - 1; index >= 0; index -= 1) {
|
||||||
|
const event = events[index];
|
||||||
|
if (event?.type !== 'result') continue;
|
||||||
|
const usage = normalizeCursorAgentUsage(event.usage);
|
||||||
|
if (usage) return usage;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractAssistantText(message) {
|
||||||
|
if (!message || typeof message !== 'object') return '';
|
||||||
|
const content = message.content ?? message.message?.content;
|
||||||
|
if (typeof content === 'string') return content.trim();
|
||||||
|
if (!Array.isArray(content)) return '';
|
||||||
|
return content
|
||||||
|
.map((part) => {
|
||||||
|
if (!part || typeof part !== 'object') return '';
|
||||||
|
if (part.type === 'text') return String(part.text ?? '').trim();
|
||||||
|
return '';
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
.join('\n')
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractCursorAgentDisplayText(stdout) {
|
||||||
|
const events = parseCursorAgentStreamJsonEvents(stdout);
|
||||||
|
for (let index = events.length - 1; index >= 0; index -= 1) {
|
||||||
|
const event = events[index];
|
||||||
|
if (event?.type === 'result') {
|
||||||
|
const resultText = String(event.result ?? '').trim();
|
||||||
|
if (resultText) return resultText;
|
||||||
|
}
|
||||||
|
if (event?.type === 'assistant') {
|
||||||
|
const assistantText = extractAssistantText(event.message ?? event);
|
||||||
|
if (assistantText) return assistantText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildCursorBillingTokenState(usage, priorBillingState = null) {
|
||||||
|
const normalized = normalizeCursorAgentUsage(usage);
|
||||||
|
if (!normalized) return null;
|
||||||
|
const priorIn = Number(priorBillingState?.lastInputTokens ?? 0);
|
||||||
|
const priorOut = Number(priorBillingState?.lastOutputTokens ?? 0);
|
||||||
|
return {
|
||||||
|
accumulatedInputTokens: priorIn + normalized.inputTokens,
|
||||||
|
accumulatedOutputTokens: priorOut + normalized.outputTokens,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
import {
|
||||||
|
buildCursorBillingTokenState,
|
||||||
|
extractCursorAgentDisplayText,
|
||||||
|
normalizeCursorAgentUsage,
|
||||||
|
parseCursorAgentUsage,
|
||||||
|
} from './cursor-agent-usage.mjs';
|
||||||
|
|
||||||
|
const SAMPLE_STREAM_JSON = [
|
||||||
|
'{"type":"system","subtype":"init","cwd":"/tmp","session_id":"abc","model":"Auto"}',
|
||||||
|
'{"type":"user","message":{"role":"user","content":[{"type":"text","text":"reply with exactly: ok"}]}}',
|
||||||
|
'{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"ok"}]}}',
|
||||||
|
'{"type":"result","subtype":"success","duration_ms":13064,"result":"ok","usage":{"inputTokens":7765,"outputTokens":64,"cacheReadTokens":5427,"cacheWriteTokens":0}}',
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
test('parseCursorAgentUsage reads result usage from stream-json stdout', () => {
|
||||||
|
const usage = parseCursorAgentUsage(SAMPLE_STREAM_JSON);
|
||||||
|
assert.deepEqual(usage, {
|
||||||
|
inputTokens: 7765,
|
||||||
|
outputTokens: 64,
|
||||||
|
cacheReadTokens: 5427,
|
||||||
|
cacheWriteTokens: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('normalizeCursorAgentUsage includes cache write tokens in billable input', () => {
|
||||||
|
const usage = normalizeCursorAgentUsage({
|
||||||
|
inputTokens: 1000,
|
||||||
|
outputTokens: 200,
|
||||||
|
cacheWriteTokens: 300,
|
||||||
|
});
|
||||||
|
assert.deepEqual(usage, {
|
||||||
|
inputTokens: 1300,
|
||||||
|
outputTokens: 200,
|
||||||
|
cacheReadTokens: 0,
|
||||||
|
cacheWriteTokens: 300,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('extractCursorAgentDisplayText prefers result text', () => {
|
||||||
|
assert.equal(extractCursorAgentDisplayText(SAMPLE_STREAM_JSON), 'ok');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildCursorBillingTokenState accumulates on prior session billing', () => {
|
||||||
|
const tokenState = buildCursorBillingTokenState(
|
||||||
|
{ inputTokens: 1000, outputTokens: 200 },
|
||||||
|
{ lastInputTokens: 5000, lastOutputTokens: 800 },
|
||||||
|
);
|
||||||
|
assert.deepEqual(tokenState, {
|
||||||
|
accumulatedInputTokens: 6000,
|
||||||
|
accumulatedOutputTokens: 1000,
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -15,13 +15,14 @@ test('buildCodeRunCompletionReply never exposes cursor brand to users', () => {
|
|||||||
assert.doesNotMatch(reply, /cursor/i);
|
assert.doesNotMatch(reply, /cursor/i);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('buildCodeRunCompletionReply sanitizes cursor wording in stdout', () => {
|
test('buildCodeRunCompletionReply prefers displayStdout over raw stream-json stdout', () => {
|
||||||
const reply = buildCodeRunCompletionReply({
|
const reply = buildCodeRunCompletionReply({
|
||||||
executor: 'cursor',
|
executor: 'cursor',
|
||||||
stdout: 'cursor agent finished editing public/page.html',
|
stdout: '{"type":"result","result":"hidden"}\n',
|
||||||
|
displayStdout: '已写入 public/spring-poem.html',
|
||||||
});
|
});
|
||||||
assert.match(reply, /TKMind 智趣 finished editing public\/page\.html/);
|
assert.match(reply, /已写入 public\/spring-poem\.html/);
|
||||||
assert.doesNotMatch(reply, /\bcursor\b/i);
|
assert.doesNotMatch(reply, /"type":"result"/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('sanitizeUserFacingBrandText replaces cursor wording', () => {
|
test('sanitizeUserFacingBrandText replaces cursor wording', () => {
|
||||||
|
|||||||
+1
-1
@@ -113,7 +113,7 @@
|
|||||||
"verify:page-data-delivery": "node scripts/repair-page-data-workspace-bindings.mjs --dry-run",
|
"verify:page-data-delivery": "node scripts/repair-page-data-workspace-bindings.mjs --dry-run",
|
||||||
"verify:template-catalog-portal": "node scripts/verify-template-catalog-portal.mjs",
|
"verify:template-catalog-portal": "node scripts/verify-template-catalog-portal.mjs",
|
||||||
"verify:template-catalog-e2e": "node scripts/verify-template-catalog-e2e.mjs",
|
"verify:template-catalog-e2e": "node scripts/verify-template-catalog-e2e.mjs",
|
||||||
"verify:cursor-executor": "node --test cursor-agent-launch.test.mjs cursor-page-routing.test.mjs tool-gateway.test.mjs llm-providers.test.mjs help-escalation.test.mjs executor-display-label.test.mjs agent-run-gateway.test.mjs agent-run-routes.test.mjs wechat-cursor-executor-policy.test.mjs",
|
"verify:cursor-executor": "node --test cursor-agent-launch.test.mjs cursor-agent-usage.test.mjs cursor-page-routing.test.mjs tool-gateway.test.mjs llm-providers.test.mjs help-escalation.test.mjs executor-display-label.test.mjs agent-run-gateway.test.mjs agent-run-routes.test.mjs wechat-cursor-executor-policy.test.mjs",
|
||||||
"repair:page-data-bindings": "node scripts/repair-page-data-workspace-bindings.mjs",
|
"repair:page-data-bindings": "node scripts/repair-page-data-workspace-bindings.mjs",
|
||||||
"repair:page-data:103": "node scripts/ensure-page-data-datasets.mjs && node scripts/repair-page-data-workspace-bindings.mjs",
|
"repair:page-data:103": "node scripts/ensure-page-data-datasets.mjs && node scripts/repair-page-data-workspace-bindings.mjs",
|
||||||
"verify:wechat-channel-isolation": "node scripts/check-wechat-channel-isolation.mjs",
|
"verify:wechat-channel-isolation": "node scripts/check-wechat-channel-isolation.mjs",
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ import { EventEmitter } from 'node:events';
|
|||||||
import fs from 'node:fs/promises';
|
import fs from 'node:fs/promises';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { buildCursorExecutorLaunchPlan } from './cursor-agent-launch.mjs';
|
import { buildCursorExecutorLaunchPlan } from './cursor-agent-launch.mjs';
|
||||||
|
import {
|
||||||
|
extractCursorAgentDisplayText,
|
||||||
|
parseCursorAgentUsage,
|
||||||
|
} from './cursor-agent-usage.mjs';
|
||||||
import { resolveExecutorDisplayLabel } from './executor-display-label.mjs';
|
import { resolveExecutorDisplayLabel } from './executor-display-label.mjs';
|
||||||
|
|
||||||
const BASE_CODE_EXECUTORS = ['aider', 'openhands'];
|
const BASE_CODE_EXECUTORS = ['aider', 'openhands'];
|
||||||
@@ -308,6 +312,10 @@ export function createToolGateway({
|
|||||||
child.on('exit', (code, signal) => {
|
child.on('exit', (code, signal) => {
|
||||||
cleanup();
|
cleanup();
|
||||||
if (code === 0) {
|
if (code === 0) {
|
||||||
|
const usage = executor === 'cursor' ? parseCursorAgentUsage(stdout) : null;
|
||||||
|
const displayStdout = executor === 'cursor'
|
||||||
|
? extractCursorAgentDisplayText(stdout)
|
||||||
|
: '';
|
||||||
resolve({
|
resolve({
|
||||||
ok: true,
|
ok: true,
|
||||||
executor,
|
executor,
|
||||||
@@ -319,6 +327,8 @@ export function createToolGateway({
|
|||||||
exitCode: code,
|
exitCode: code,
|
||||||
signal: signal ?? null,
|
signal: signal ?? null,
|
||||||
stdout,
|
stdout,
|
||||||
|
displayStdout: displayStdout || undefined,
|
||||||
|
usage: usage ?? undefined,
|
||||||
stderr,
|
stderr,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
|||||||
Reference in New Issue
Block a user