improve cursor tool call logs
This commit is contained in:
+143
-30
@@ -2,10 +2,136 @@ import { spawn } from "node:child_process";
|
|||||||
import { mkdirSync, writeFileSync } from "node:fs";
|
import { mkdirSync, writeFileSync } from "node:fs";
|
||||||
import { homedir } from "node:os";
|
import { homedir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { log } from "../utils/cli.ts";
|
import { formatToolCallLog, log } from "../utils/cli.ts";
|
||||||
import { addInstructions } from "./instructions.ts";
|
import { addInstructions } from "./instructions.ts";
|
||||||
import { agent, type ConfigureMcpServersParams, installFromCurl } from "./shared.ts";
|
import { agent, type ConfigureMcpServersParams, installFromCurl } from "./shared.ts";
|
||||||
|
|
||||||
|
// cursor cli event types inferred from stream-json output
|
||||||
|
interface CursorSystemEvent {
|
||||||
|
type: "system";
|
||||||
|
subtype?: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CursorUserEvent {
|
||||||
|
type: "user";
|
||||||
|
message?: {
|
||||||
|
role: string;
|
||||||
|
content: Array<{ type: string; text?: string }>;
|
||||||
|
};
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CursorThinkingEvent {
|
||||||
|
type: "thinking";
|
||||||
|
subtype: "delta" | "completed";
|
||||||
|
text?: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CursorAssistantEvent {
|
||||||
|
type: "assistant";
|
||||||
|
model_call_id?: string;
|
||||||
|
message?: {
|
||||||
|
role: string;
|
||||||
|
content: Array<{ type: string; text?: string }>;
|
||||||
|
};
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CursorToolCallEvent {
|
||||||
|
type: "tool_call";
|
||||||
|
subtype: "started" | "completed";
|
||||||
|
call_id?: string;
|
||||||
|
tool_call?: {
|
||||||
|
mcpToolCall?: {
|
||||||
|
args?: {
|
||||||
|
name?: string;
|
||||||
|
args?: unknown;
|
||||||
|
toolName?: string;
|
||||||
|
providerIdentifier?: string;
|
||||||
|
};
|
||||||
|
result?: {
|
||||||
|
success?: {
|
||||||
|
content?: Array<{ text?: { text?: string } }>;
|
||||||
|
isError?: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CursorResultEvent {
|
||||||
|
type: "result";
|
||||||
|
subtype: "success" | "error";
|
||||||
|
result?: string;
|
||||||
|
duration_ms?: number;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
type CursorEvent =
|
||||||
|
| CursorSystemEvent
|
||||||
|
| CursorUserEvent
|
||||||
|
| CursorThinkingEvent
|
||||||
|
| CursorAssistantEvent
|
||||||
|
| CursorToolCallEvent
|
||||||
|
| CursorResultEvent;
|
||||||
|
|
||||||
|
const messageHandlers = {
|
||||||
|
system: (_event: CursorSystemEvent) => {
|
||||||
|
// system init events - no logging needed
|
||||||
|
},
|
||||||
|
user: (_event: CursorUserEvent) => {
|
||||||
|
// user messages already logged in prompt box
|
||||||
|
},
|
||||||
|
thinking: (_event: CursorThinkingEvent) => {
|
||||||
|
// thinking events are internal - no logging needed
|
||||||
|
},
|
||||||
|
assistant: (event: CursorAssistantEvent) => {
|
||||||
|
// only log finalized messages (ones with model_call_id)
|
||||||
|
// cursor emits each message twice: once without model_call_id, then again with it
|
||||||
|
if (event.model_call_id) {
|
||||||
|
const text = event.message?.content?.[0]?.text;
|
||||||
|
if (text?.trim()) {
|
||||||
|
log.box(text.trim(), { title: "Cursor" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tool_call: (event: CursorToolCallEvent) => {
|
||||||
|
if (event.subtype === "started") {
|
||||||
|
// handle both MCP tools and built-in tools (bash, WebFetch, etc)
|
||||||
|
const mcpToolCall = event.tool_call?.mcpToolCall;
|
||||||
|
const builtinToolCall = (event.tool_call as any)?.builtinToolCall;
|
||||||
|
|
||||||
|
if (mcpToolCall?.args?.toolName && mcpToolCall?.args?.args) {
|
||||||
|
const formatted = formatToolCallLog({
|
||||||
|
toolName: mcpToolCall.args.toolName,
|
||||||
|
input: mcpToolCall.args.args,
|
||||||
|
});
|
||||||
|
log.info(formatted);
|
||||||
|
} else if (builtinToolCall?.args?.name && builtinToolCall?.args?.args) {
|
||||||
|
const formatted = formatToolCallLog({
|
||||||
|
toolName: builtinToolCall.args.name,
|
||||||
|
input: builtinToolCall.args.args,
|
||||||
|
});
|
||||||
|
log.info(formatted);
|
||||||
|
}
|
||||||
|
} else if (event.subtype === "completed") {
|
||||||
|
const isError = event.tool_call?.mcpToolCall?.result?.success?.isError;
|
||||||
|
if (isError) {
|
||||||
|
log.warning("Tool call failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
result: async (event: CursorResultEvent) => {
|
||||||
|
if (event.subtype === "success" && event.duration_ms) {
|
||||||
|
const durationSec = (event.duration_ms / 1000).toFixed(1);
|
||||||
|
log.debug(`Cursor completed in ${durationSec}s`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const cursor = agent({
|
export const cursor = agent({
|
||||||
name: "cursor",
|
name: "cursor",
|
||||||
install: async () => {
|
install: async () => {
|
||||||
@@ -42,9 +168,12 @@ export const cursor = agent({
|
|||||||
{
|
{
|
||||||
cwd: process.cwd(),
|
cwd: process.cwd(),
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
|
||||||
CURSOR_API_KEY: apiKey,
|
CURSOR_API_KEY: apiKey,
|
||||||
GITHUB_INSTALLATION_TOKEN: githubInstallationToken,
|
GITHUB_INSTALLATION_TOKEN: githubInstallationToken,
|
||||||
|
LOG_LEVEL: process.env.LOG_LEVEL,
|
||||||
|
NODE_ENV: process.env.NODE_ENV,
|
||||||
|
HOME: process.env.HOME,
|
||||||
|
PATH: process.env.PATH,
|
||||||
// Don't override HOME - Cursor CLI needs access to macOS keychain
|
// Don't override HOME - Cursor CLI needs access to macOS keychain
|
||||||
// MCP config is written to tempDir/.cursor/mcp.json which Cursor will find
|
// MCP config is written to tempDir/.cursor/mcp.json which Cursor will find
|
||||||
},
|
},
|
||||||
@@ -54,7 +183,6 @@ export const cursor = agent({
|
|||||||
|
|
||||||
let stdout = "";
|
let stdout = "";
|
||||||
let stderr = "";
|
let stderr = "";
|
||||||
let jsonBuffer = "";
|
|
||||||
|
|
||||||
child.on("spawn", () => {
|
child.on("spawn", () => {
|
||||||
log.debug("Cursor CLI process spawned");
|
log.debug("Cursor CLI process spawned");
|
||||||
@@ -63,26 +191,21 @@ export const cursor = agent({
|
|||||||
child.stdout?.on("data", async (data) => {
|
child.stdout?.on("data", async (data) => {
|
||||||
const text = data.toString();
|
const text = data.toString();
|
||||||
stdout += text;
|
stdout += text;
|
||||||
jsonBuffer += text;
|
|
||||||
|
|
||||||
// parse ndjson (newline-delimited json)
|
try {
|
||||||
const lines = jsonBuffer.split("\n");
|
const event = JSON.parse(text) as CursorEvent;
|
||||||
// keep last incomplete line in buffer
|
|
||||||
jsonBuffer = lines.pop() || "";
|
|
||||||
|
|
||||||
for (const line of lines) {
|
// route to appropriate handler
|
||||||
const trimmedLine = line.trim();
|
const handler = messageHandlers[event.type as keyof typeof messageHandlers];
|
||||||
if (!trimmedLine) continue;
|
if (handler) {
|
||||||
|
await handler(event as never);
|
||||||
try {
|
|
||||||
const event = JSON.parse(trimmedLine);
|
|
||||||
// log everything for now - we'll infer types from real output
|
|
||||||
log.debug(`[cursor event] ${JSON.stringify(event, null, 2)}`);
|
|
||||||
} catch {
|
|
||||||
// if json parse fails, might be partial line or non-json output
|
|
||||||
// log debug info but don't crash
|
|
||||||
log.debug(`failed to parse json line: ${trimmedLine.substring(0, 100)}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// debug: log all events
|
||||||
|
log.debug(`[cursor event] ${JSON.stringify(event, null, 2)}`);
|
||||||
|
} catch {
|
||||||
|
// ignore parse errors - might be formatted tool call logs from cursor cli
|
||||||
|
// our handlers log tool calls instead, so we don't need to display these
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -94,16 +217,6 @@ export const cursor = agent({
|
|||||||
});
|
});
|
||||||
|
|
||||||
child.on("close", async (code, signal) => {
|
child.on("close", async (code, signal) => {
|
||||||
// process any remaining buffered json
|
|
||||||
if (jsonBuffer.trim()) {
|
|
||||||
try {
|
|
||||||
const event = JSON.parse(jsonBuffer.trim());
|
|
||||||
log.debug(`[cursor event] ${JSON.stringify(event, null, 2)}`);
|
|
||||||
} catch {
|
|
||||||
// ignore parse errors for final buffer
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (signal) {
|
if (signal) {
|
||||||
log.warning(`Cursor CLI terminated by signal: ${signal}`);
|
log.warning(`Cursor CLI terminated by signal: ${signal}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ export const gemini = agent({
|
|||||||
env: {
|
env: {
|
||||||
GEMINI_API_KEY: apiKey,
|
GEMINI_API_KEY: apiKey,
|
||||||
GITHUB_INSTALLATION_TOKEN: githubInstallationToken,
|
GITHUB_INSTALLATION_TOKEN: githubInstallationToken,
|
||||||
|
LOG_LEVEL: process.env.LOG_LEVEL!,
|
||||||
|
NODE_ENV: process.env.NODE_ENV!,
|
||||||
},
|
},
|
||||||
onStdout: (chunk) => {
|
onStdout: (chunk) => {
|
||||||
const trimmed = chunk.trim();
|
const trimmed = chunk.trim();
|
||||||
|
|||||||
+4
-1
@@ -302,8 +302,11 @@ export async function installFromCurl({
|
|||||||
const installResult = spawnSync("bash", [installScriptPath], {
|
const installResult = spawnSync("bash", [installScriptPath], {
|
||||||
cwd: tempDir,
|
cwd: tempDir,
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
|
||||||
HOME: tempDir, // Cursor install script uses HOME for installation path
|
HOME: tempDir, // Cursor install script uses HOME for installation path
|
||||||
|
PATH: process.env.PATH || "",
|
||||||
|
SHELL: process.env.SHELL || "/bin/bash",
|
||||||
|
USER: process.env.USER || "",
|
||||||
|
TMPDIR: process.env.TMPDIR || "/tmp",
|
||||||
},
|
},
|
||||||
stdio: "pipe",
|
stdio: "pipe",
|
||||||
encoding: "utf-8",
|
encoding: "utf-8",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { mkdtemp, writeFile } from "node:fs/promises";
|
|||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { flatMorph } from "@ark/util";
|
import { flatMorph } from "@ark/util";
|
||||||
|
import { encode as toonEncode } from "@toon-format/toon";
|
||||||
import { type } from "arktype";
|
import { type } from "arktype";
|
||||||
import { agents } from "./agents/index.ts";
|
import { agents } from "./agents/index.ts";
|
||||||
import type { AgentResult } from "./agents/shared.ts";
|
import type { AgentResult } from "./agents/shared.ts";
|
||||||
@@ -316,7 +317,11 @@ function validateApiKey(ctx: MainContext): void {
|
|||||||
|
|
||||||
async function runAgent(ctx: MainContext): Promise<AgentResult> {
|
async function runAgent(ctx: MainContext): Promise<AgentResult> {
|
||||||
log.info(`Running ${ctx.agentName}...`);
|
log.info(`Running ${ctx.agentName}...`);
|
||||||
log.box(ctx.payload.prompt, { title: "Prompt" });
|
// strip context from event
|
||||||
|
const { context: _context, ...eventWithoutContext } = ctx.payload.event;
|
||||||
|
// format: prompt + two newlines + TOON encoded event
|
||||||
|
const promptContent = `${ctx.payload.prompt}\n\n${toonEncode(eventWithoutContext)}`;
|
||||||
|
log.box(promptContent, { title: "Prompt" });
|
||||||
|
|
||||||
return ctx.agent.run({
|
return ctx.agent.run({
|
||||||
payload: ctx.payload,
|
payload: ctx.payload,
|
||||||
|
|||||||
+19
-16
@@ -56,25 +56,28 @@ export const tool = <const params>(toolDef: Tool<any, StandardSchemaV1<params>>)
|
|||||||
toolDef.execute = async (args: params, context: any) => {
|
toolDef.execute = async (args: params, context: any) => {
|
||||||
try {
|
try {
|
||||||
const result = await originalExecute(args, context);
|
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;
|
|
||||||
const resultData =
|
|
||||||
result && typeof result === "object" && "content" in result
|
|
||||||
? (result as ToolResult).content[0]?.text
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
if (isError && resultData) {
|
// TOOL CALL LOGGING DISABLED — now handled by each agent
|
||||||
log.toolCall({ toolName, request: args, error: resultData });
|
|
||||||
} else if (resultData) {
|
// Check if result is a ToolResult with isError property
|
||||||
log.toolCall({ toolName, request: args, result: resultData });
|
// const isError =
|
||||||
} else {
|
// result && typeof result === "object" && "isError" in result && result.isError === true;
|
||||||
log.toolCall({ toolName, request: args });
|
// const resultData =
|
||||||
}
|
// result && typeof result === "object" && "content" in result
|
||||||
|
// ? (result as ToolResult).content[0]?.text
|
||||||
|
// : undefined;
|
||||||
|
|
||||||
|
// if (isError && resultData) {
|
||||||
|
// log.toolCall({ toolName, request: args, error: resultData });
|
||||||
|
// } else if (resultData) {
|
||||||
|
// log.toolCall({ toolName, request: args, result: resultData });
|
||||||
|
// } else {
|
||||||
|
// log.toolCall({ toolName, request: args });
|
||||||
|
// }
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
// const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
log.toolCall({ toolName, request: args, error: errorMessage });
|
// log.toolCall({ toolName, request: args, error: errorMessage });
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
+28
-4
@@ -9,7 +9,7 @@ import * as core from "@actions/core";
|
|||||||
import { table } from "table";
|
import { table } from "table";
|
||||||
|
|
||||||
const isGitHubActions = !!process.env.GITHUB_ACTIONS;
|
const isGitHubActions = !!process.env.GITHUB_ACTIONS;
|
||||||
const isDebugEnabled = process.env.LOG_LEVEL === "debug";
|
const isDebugEnabled = () => process.env.LOG_LEVEL === "debug";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start a collapsed group (GitHub Actions) or regular group (local)
|
* Start a collapsed group (GitHub Actions) or regular group (local)
|
||||||
@@ -258,7 +258,7 @@ export const log = {
|
|||||||
* Print debug message (only if LOG_LEVEL=debug)
|
* Print debug message (only if LOG_LEVEL=debug)
|
||||||
*/
|
*/
|
||||||
debug: (message: string): void => {
|
debug: (message: string): void => {
|
||||||
if (isDebugEnabled) {
|
if (isDebugEnabled()) {
|
||||||
if (isGitHubActions) {
|
if (isGitHubActions) {
|
||||||
core.debug(message);
|
core.debug(message);
|
||||||
} else {
|
} else {
|
||||||
@@ -345,7 +345,7 @@ function getMcpLogPath(): string {
|
|||||||
/**
|
/**
|
||||||
* Format a value as JSON, using compact format for simple values and pretty-printed for complex ones
|
* Format a value as JSON, using compact format for simple values and pretty-printed for complex ones
|
||||||
*/
|
*/
|
||||||
function formatJsonValue(value: unknown): string {
|
export function formatJsonValue(value: unknown): string {
|
||||||
const compact = JSON.stringify(value);
|
const compact = JSON.stringify(value);
|
||||||
return compact.length > 80 || compact.includes("\n") ? JSON.stringify(value, null, 2) : compact;
|
return compact.length > 80 || compact.includes("\n") ? JSON.stringify(value, null, 2) : compact;
|
||||||
}
|
}
|
||||||
@@ -354,7 +354,7 @@ function formatJsonValue(value: unknown): string {
|
|||||||
* Format a multi-line string with proper indentation for tool call output
|
* Format a multi-line string with proper indentation for tool call output
|
||||||
* First line has the label, subsequent lines are indented 4 spaces
|
* First line has the label, subsequent lines are indented 4 spaces
|
||||||
*/
|
*/
|
||||||
function formatIndentedField(label: string, content: string): string {
|
export function formatIndentedField(label: string, content: string): string {
|
||||||
if (!content.includes("\n")) {
|
if (!content.includes("\n")) {
|
||||||
return ` ${label}: ${content}\n`;
|
return ` ${label}: ${content}\n`;
|
||||||
}
|
}
|
||||||
@@ -420,6 +420,30 @@ function formatToolCall({
|
|||||||
return logEntry;
|
return logEntry;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a tool call log entry with tool name and input
|
||||||
|
*/
|
||||||
|
export function formatToolCallLog({
|
||||||
|
toolName,
|
||||||
|
input,
|
||||||
|
result: _result,
|
||||||
|
}: {
|
||||||
|
toolName: string;
|
||||||
|
input: unknown;
|
||||||
|
result?: unknown;
|
||||||
|
}): string {
|
||||||
|
let output = `→ ${toolName}\n`;
|
||||||
|
|
||||||
|
const inputFormatted = formatJsonValue(input);
|
||||||
|
if (inputFormatted !== "{}") {
|
||||||
|
output += formatIndentedField("input", inputFormatted);
|
||||||
|
}
|
||||||
|
|
||||||
|
// result parameter added for future use but not implemented yet
|
||||||
|
|
||||||
|
return output.trimEnd();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds a CLI executable path by checking if it's installed globally
|
* Finds a CLI executable path by checking if it's installed globally
|
||||||
* @param name The name of the CLI executable to find
|
* @param name The name of the CLI executable to find
|
||||||
|
|||||||
+5
-1
@@ -29,8 +29,12 @@ export async function spawn(options: SpawnOptions): Promise<SpawnResult> {
|
|||||||
let stderrBuffer = "";
|
let stderrBuffer = "";
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
// security: caller must provide complete env object, not merged with process.env
|
||||||
const child = nodeSpawn(cmd, args, {
|
const child = nodeSpawn(cmd, args, {
|
||||||
env: env ? { ...process.env, ...env } : process.env,
|
env: env || {
|
||||||
|
PATH: process.env.PATH || "",
|
||||||
|
HOME: process.env.HOME || "",
|
||||||
|
},
|
||||||
stdio: ["pipe", "pipe", "pipe"],
|
stdio: ["pipe", "pipe", "pipe"],
|
||||||
cwd: cwd || process.cwd(),
|
cwd: cwd || process.cwd(),
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user