Files
memind/cursor-agent-usage.mjs
T
john f95d766f69
Memind CI / Test, build, and release guards (push) Failing after 14m36s
feat(billing): bill Cursor executor usage via stream-json token parsing
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>
2026-08-27 10:16:38 +08:00

91 lines
3.1 KiB
JavaScript

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,
};
}