mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-02 14:19:56 +00:00
fix(cursor): make bare cursor command useful and harden tool-result translation
This commit is contained in:
@@ -184,7 +184,7 @@ The dashboard provides visual management for all account types:
|
|||||||
ccs # Default Claude session
|
ccs # Default Claude session
|
||||||
ccs gemini # Gemini (OAuth)
|
ccs gemini # Gemini (OAuth)
|
||||||
ccs codex # OpenAI Codex (OAuth)
|
ccs codex # OpenAI Codex (OAuth)
|
||||||
ccs cursor # Cursor IDE integration (token import + local daemon)
|
ccs cursor # Cursor status + local daemon connection details
|
||||||
ccs kiro # Kiro/AWS CodeWhisperer (OAuth)
|
ccs kiro # Kiro/AWS CodeWhisperer (OAuth)
|
||||||
ccs ghcp # GitHub Copilot (OAuth device flow)
|
ccs ghcp # GitHub Copilot (OAuth device flow)
|
||||||
ccs agy # Antigravity (OAuth)
|
ccs agy # Antigravity (OAuth)
|
||||||
|
|||||||
@@ -49,6 +49,13 @@ ccs cursor start
|
|||||||
ccs cursor status
|
ccs cursor status
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Or use the bare command to see the same status view plus the local endpoint details
|
||||||
|
needed for OpenAI-compatible and Anthropic-compatible clients:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ccs cursor
|
||||||
|
```
|
||||||
|
|
||||||
### 5) Stop daemon
|
### 5) Stop daemon
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export function renderCursorHelp(): number {
|
|||||||
printLines([
|
printLines([
|
||||||
'Cursor IDE Integration',
|
'Cursor IDE Integration',
|
||||||
'',
|
'',
|
||||||
'Usage: ccs cursor <subcommand> [options]',
|
'Usage: ccs cursor [subcommand] [options]',
|
||||||
'',
|
'',
|
||||||
'Subcommands:',
|
'Subcommands:',
|
||||||
' auth Import Cursor IDE authentication token',
|
' auth Import Cursor IDE authentication token',
|
||||||
@@ -32,6 +32,7 @@ export function renderCursorHelp(): number {
|
|||||||
' 1. ccs cursor enable # Enable integration',
|
' 1. ccs cursor enable # Enable integration',
|
||||||
' 2. ccs cursor auth # Import Cursor IDE token',
|
' 2. ccs cursor auth # Import Cursor IDE token',
|
||||||
' 3. ccs cursor start # Start daemon',
|
' 3. ccs cursor start # Start daemon',
|
||||||
|
' 4. ccs cursor # Show status and runtime connection details',
|
||||||
'',
|
'',
|
||||||
'Or use the web UI: ccs config -> Cursor page',
|
'Or use the web UI: ccs config -> Cursor page',
|
||||||
'',
|
'',
|
||||||
@@ -45,6 +46,10 @@ export function renderCursorStatus(
|
|||||||
authStatus: CursorAuthStatus,
|
authStatus: CursorAuthStatus,
|
||||||
daemonStatus: CursorDaemonStatus
|
daemonStatus: CursorDaemonStatus
|
||||||
): void {
|
): void {
|
||||||
|
const localBaseUrl = `http://127.0.0.1:${cursorConfig.port}`;
|
||||||
|
const isReady =
|
||||||
|
cursorConfig.enabled && authStatus.authenticated && !authStatus.expired && daemonStatus.running;
|
||||||
|
|
||||||
console.log('Cursor IDE Status');
|
console.log('Cursor IDE Status');
|
||||||
console.log('─────────────────');
|
console.log('─────────────────');
|
||||||
console.log('');
|
console.log('');
|
||||||
@@ -77,15 +82,23 @@ export function renderCursorStatus(
|
|||||||
console.log(` Ghost mode: ${cursorConfig.ghost_mode ? 'On' : 'Off'}`);
|
console.log(` Ghost mode: ${cursorConfig.ghost_mode ? 'On' : 'Off'}`);
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
||||||
if (
|
console.log('Runtime:');
|
||||||
cursorConfig.enabled &&
|
console.log(` OpenAI base: ${localBaseUrl}/v1`);
|
||||||
authStatus.authenticated &&
|
console.log(` Anthropic base: ${localBaseUrl}`);
|
||||||
!authStatus.expired &&
|
console.log(` Chat route: ${localBaseUrl}/v1/chat/completions`);
|
||||||
daemonStatus.running
|
console.log(` Messages route: ${localBaseUrl}/v1/messages`);
|
||||||
) {
|
console.log(` Models route: ${localBaseUrl}/v1/models`);
|
||||||
|
console.log('');
|
||||||
|
console.log('Client setup:');
|
||||||
|
console.log(' Raw settings: ~/.ccs/cursor.settings.json');
|
||||||
|
console.log(' Subcommands: ccs cursor help');
|
||||||
|
|
||||||
|
if (isReady) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
console.log('Next steps:');
|
console.log('Next steps:');
|
||||||
if (!cursorConfig.enabled) {
|
if (!cursorConfig.enabled) {
|
||||||
console.log(' - Enable: ccs cursor enable');
|
console.log(' - Enable: ccs cursor enable');
|
||||||
@@ -96,6 +109,7 @@ export function renderCursorStatus(
|
|||||||
if (!daemonStatus.running) {
|
if (!daemonStatus.running) {
|
||||||
console.log(' - Start: ccs cursor start');
|
console.log(' - Start: ccs cursor start');
|
||||||
}
|
}
|
||||||
|
console.log(' - Help: ccs cursor help');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderCursorModels(models: CursorModel[], defaultModel: string): void {
|
export function renderCursorModels(models: CursorModel[], defaultModel: string): void {
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ export async function handleCursorCommand(args: string[]): Promise<number> {
|
|||||||
case 'disable':
|
case 'disable':
|
||||||
return handleDisable();
|
return handleDisable();
|
||||||
case undefined:
|
case undefined:
|
||||||
|
return handleStatus();
|
||||||
case 'help':
|
case 'help':
|
||||||
case '--help':
|
case '--help':
|
||||||
case '-h':
|
case '-h':
|
||||||
|
|||||||
+318
-83
@@ -1,14 +1,42 @@
|
|||||||
/**
|
/**
|
||||||
* OpenAI to Cursor Request Translator
|
* OpenAI to Cursor Request Translator
|
||||||
* Converts OpenAI messages to Cursor format
|
* Converts OpenAI messages to Cursor format.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { CursorMessage, CursorToolResult, CursorTool } from './cursor-protobuf-schema.js';
|
import type { CursorMessage, CursorTool } from './cursor-protobuf-schema.js';
|
||||||
|
|
||||||
|
interface OpenAITextPart {
|
||||||
|
type: 'text';
|
||||||
|
text?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OpenAIToolUsePart {
|
||||||
|
type: 'tool_use';
|
||||||
|
id?: string;
|
||||||
|
name?: string;
|
||||||
|
input?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OpenAIToolResultPart {
|
||||||
|
type: 'tool_result';
|
||||||
|
tool_use_id?: string;
|
||||||
|
content?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OpenAIUnknownPart {
|
||||||
|
type: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
type OpenAIContentPart =
|
||||||
|
| OpenAITextPart
|
||||||
|
| OpenAIToolUsePart
|
||||||
|
| OpenAIToolResultPart
|
||||||
|
| OpenAIUnknownPart;
|
||||||
|
|
||||||
/** OpenAI message format */
|
|
||||||
interface OpenAIMessage {
|
interface OpenAIMessage {
|
||||||
role: string;
|
role: string;
|
||||||
content: string | Array<{ type: string; text?: string }>;
|
content: string | OpenAIContentPart[];
|
||||||
name?: string;
|
name?: string;
|
||||||
tool_call_id?: string;
|
tool_call_id?: string;
|
||||||
tool_calls?: Array<{
|
tool_calls?: Array<{
|
||||||
@@ -18,113 +46,326 @@ interface OpenAIMessage {
|
|||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** OpenAI request body */
|
|
||||||
interface OpenAIRequestBody {
|
interface OpenAIRequestBody {
|
||||||
messages: OpenAIMessage[];
|
messages: OpenAIMessage[];
|
||||||
tools?: CursorTool[];
|
tools?: CursorTool[];
|
||||||
reasoning_effort?: string;
|
reasoning_effort?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MAX_TOOL_RESULT_CHARS = 12_000;
|
||||||
|
const TOOL_RESULT_SERIALIZATION_FALLBACK = '[unserializable content]';
|
||||||
|
const TOOL_USE_ARGUMENTS_FALLBACK = '{}';
|
||||||
|
|
||||||
|
function isTextPart(part: OpenAIContentPart): part is OpenAITextPart {
|
||||||
|
return part.type === 'text';
|
||||||
|
}
|
||||||
|
|
||||||
|
function isToolUsePart(part: OpenAIContentPart): part is OpenAIToolUsePart {
|
||||||
|
return part.type === 'tool_use';
|
||||||
|
}
|
||||||
|
|
||||||
|
function isToolResultPart(part: OpenAIContentPart): part is OpenAIToolResultPart {
|
||||||
|
return part.type === 'tool_result';
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractTextContent(content: OpenAIMessage['content']): string {
|
||||||
|
if (typeof content === 'string') {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
let text = '';
|
||||||
|
for (const part of content) {
|
||||||
|
if (isTextPart(part) && part.text) {
|
||||||
|
text += part.text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringifyUnknown(value: unknown, fallback = ''): string {
|
||||||
|
try {
|
||||||
|
const serialized = JSON.stringify(value);
|
||||||
|
return typeof serialized === 'string' ? serialized : fallback;
|
||||||
|
} catch {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeToolResultText(text: string): string {
|
||||||
|
return text.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function truncateToolResultText(text: string): string {
|
||||||
|
if (text.length <= MAX_TOOL_RESULT_CHARS) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
let suffix = '\n[truncated]';
|
||||||
|
let keepLength = Math.max(MAX_TOOL_RESULT_CHARS - suffix.length, 0);
|
||||||
|
let omittedChars = text.length - keepLength;
|
||||||
|
|
||||||
|
suffix = `\n[truncated ${omittedChars} chars]`;
|
||||||
|
keepLength = Math.max(MAX_TOOL_RESULT_CHARS - suffix.length, 0);
|
||||||
|
omittedChars = text.length - keepLength;
|
||||||
|
suffix = `\n[truncated ${omittedChars} chars]`;
|
||||||
|
|
||||||
|
return `${text.slice(0, keepLength)}${suffix}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeXml(text: string): string {
|
||||||
|
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildToolResultBlock(toolName: string, toolCallId: string, resultText: string): string {
|
||||||
|
const cleanResult = truncateToolResultText(escapeXml(sanitizeToolResultText(resultText)));
|
||||||
|
|
||||||
|
return [
|
||||||
|
'<tool_result>',
|
||||||
|
`<tool_name>${escapeXml(toolName || 'tool')}</tool_name>`,
|
||||||
|
`<tool_call_id>${escapeXml(toolCallId)}</tool_call_id>`,
|
||||||
|
`<result>${cleanResult}</result>`,
|
||||||
|
'</tool_result>',
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeToolCallId(toolCallId: string | undefined): string {
|
||||||
|
return typeof toolCallId === 'string' ? toolCallId.split('\n')[0] : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractToolResultText(content: unknown): string {
|
||||||
|
if (content === undefined) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof content === 'string') {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(content)) {
|
||||||
|
return content
|
||||||
|
.filter(isTextPart)
|
||||||
|
.map((part) => part.text || '')
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
return stringifyUnknown(content, TOOL_RESULT_SERIALIZATION_FALLBACK);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createFallbackToolUseId(messageIndex: number, partIndex: number): string {
|
||||||
|
return `toolu_cursor_fallback_${messageIndex}_${partIndex}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveToolUseId(
|
||||||
|
part: OpenAIToolUsePart,
|
||||||
|
messageIndex: number,
|
||||||
|
partIndex: number
|
||||||
|
): string {
|
||||||
|
return typeof part.id === 'string' && part.id.length > 0
|
||||||
|
? part.id
|
||||||
|
: createFallbackToolUseId(messageIndex, partIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
function rememberToolCallMeta(
|
||||||
|
toolCallMetaMap: Map<string, { name: string }>,
|
||||||
|
toolCalls: NonNullable<OpenAIMessage['tool_calls']>
|
||||||
|
): void {
|
||||||
|
for (const toolCall of toolCalls) {
|
||||||
|
const toolCallId = toolCall.id || '';
|
||||||
|
const toolName = toolCall.function?.name || 'tool';
|
||||||
|
if (!toolCallId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
toolCallMetaMap.set(toolCallId, { name: toolName });
|
||||||
|
|
||||||
|
const normalizedId = normalizeToolCallId(toolCallId);
|
||||||
|
if (normalizedId && normalizedId !== toolCallId) {
|
||||||
|
toolCallMetaMap.set(normalizedId, { name: toolName });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function rememberToolUseParts(
|
||||||
|
toolCallMetaMap: Map<string, { name: string }>,
|
||||||
|
content: OpenAIMessage['content'],
|
||||||
|
messageIndex: number
|
||||||
|
): void {
|
||||||
|
if (!Array.isArray(content)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let partIndex = 0; partIndex < content.length; partIndex++) {
|
||||||
|
const part = content[partIndex];
|
||||||
|
if (!isToolUsePart(part)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const toolCallId = resolveToolUseId(part, messageIndex, partIndex);
|
||||||
|
const toolName = part.name || 'tool';
|
||||||
|
toolCallMetaMap.set(toolCallId, { name: toolName });
|
||||||
|
|
||||||
|
const normalizedId = normalizeToolCallId(toolCallId);
|
||||||
|
if (normalizedId && normalizedId !== toolCallId) {
|
||||||
|
toolCallMetaMap.set(normalizedId, { name: toolName });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractToolCallsFromContent(
|
||||||
|
content: OpenAIMessage['content'],
|
||||||
|
messageIndex: number
|
||||||
|
): NonNullable<OpenAIMessage['tool_calls']> {
|
||||||
|
if (!Array.isArray(content)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return content.flatMap((part, partIndex) => {
|
||||||
|
if (!isToolUsePart(part)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: resolveToolUseId(part, messageIndex, partIndex),
|
||||||
|
type: 'function',
|
||||||
|
function: {
|
||||||
|
name: part.name || 'tool',
|
||||||
|
arguments: stringifyUnknown(part.input ?? {}, TOOL_USE_ARGUMENTS_FALLBACK),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeAssistantToolCalls(
|
||||||
|
primaryToolCalls: NonNullable<OpenAIMessage['tool_calls']>,
|
||||||
|
secondaryToolCalls: NonNullable<OpenAIMessage['tool_calls']>
|
||||||
|
): NonNullable<OpenAIMessage['tool_calls']> {
|
||||||
|
const merged: NonNullable<OpenAIMessage['tool_calls']> = [];
|
||||||
|
const seenIds = new Set<string>();
|
||||||
|
|
||||||
|
for (const toolCall of [...primaryToolCalls, ...secondaryToolCalls]) {
|
||||||
|
if (seenIds.has(toolCall.id)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
seenIds.add(toolCall.id);
|
||||||
|
merged.push(toolCall);
|
||||||
|
}
|
||||||
|
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderUserContent(
|
||||||
|
content: OpenAIMessage['content'],
|
||||||
|
toolCallMetaMap: Map<string, { name: string }>
|
||||||
|
): string {
|
||||||
|
if (typeof content === 'string') {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts: string[] = [];
|
||||||
|
let textBuffer = '';
|
||||||
|
for (const part of content) {
|
||||||
|
if (isTextPart(part) && part.text) {
|
||||||
|
textBuffer += part.text;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isToolResultPart(part)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (textBuffer) {
|
||||||
|
parts.push(textBuffer);
|
||||||
|
textBuffer = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const toolCallId = part.tool_use_id || '';
|
||||||
|
const normalizedId = normalizeToolCallId(toolCallId);
|
||||||
|
const toolName =
|
||||||
|
toolCallMetaMap.get(toolCallId)?.name || toolCallMetaMap.get(normalizedId)?.name || 'tool';
|
||||||
|
parts.push(buildToolResultBlock(toolName, toolCallId, extractToolResultText(part.content)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (textBuffer) {
|
||||||
|
parts.push(textBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert OpenAI messages to Cursor format with native tool_results support
|
* Convert OpenAI messages to Cursor format with a safer tool-result strategy.
|
||||||
* - system → user with [System Instructions] prefix
|
* - system → user with [System Instructions] prefix
|
||||||
* - tool → accumulate into tool_results array for next user/assistant message
|
* - tool → flatten into a structured user text block for Cursor compatibility
|
||||||
* - assistant with tool_calls → keep tool_calls structure (Cursor supports it natively)
|
* - assistant with tool_calls → keep tool_calls in the translated shape for metadata recovery
|
||||||
*/
|
*/
|
||||||
function convertMessages(messages: OpenAIMessage[]): CursorMessage[] {
|
function convertMessages(messages: OpenAIMessage[]): CursorMessage[] {
|
||||||
const result: CursorMessage[] = [];
|
const result: CursorMessage[] = [];
|
||||||
let pendingToolResults: CursorToolResult[] = [];
|
const toolCallMetaMap = new Map<string, { name: string }>();
|
||||||
|
|
||||||
for (let i = 0; i < messages.length; i++) {
|
|
||||||
const msg = messages[i];
|
|
||||||
|
|
||||||
|
for (let messageIndex = 0; messageIndex < messages.length; messageIndex++) {
|
||||||
|
const msg = messages[messageIndex];
|
||||||
if (msg.role === 'system') {
|
if (msg.role === 'system') {
|
||||||
let content = '';
|
|
||||||
if (typeof msg.content === 'string') {
|
|
||||||
content = msg.content;
|
|
||||||
} else if (Array.isArray(msg.content)) {
|
|
||||||
for (const part of msg.content) {
|
|
||||||
if (part.type === 'text' && part.text) content += part.text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result.push({
|
result.push({
|
||||||
role: 'user',
|
role: 'user',
|
||||||
content: `[System Instructions]\n${content}`,
|
content: `[System Instructions]\n${extractTextContent(msg.content)}`,
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (msg.role === 'tool') {
|
if (msg.role === 'tool') {
|
||||||
let toolContent = '';
|
|
||||||
if (typeof msg.content === 'string') {
|
|
||||||
toolContent = msg.content;
|
|
||||||
} else if (Array.isArray(msg.content)) {
|
|
||||||
for (const part of msg.content) {
|
|
||||||
if (part.type === 'text' && part.text) {
|
|
||||||
toolContent += part.text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const toolName = msg.name || 'tool';
|
|
||||||
const toolCallId = msg.tool_call_id || '';
|
const toolCallId = msg.tool_call_id || '';
|
||||||
|
const normalizedToolCallId = normalizeToolCallId(toolCallId);
|
||||||
|
const rememberedToolName =
|
||||||
|
toolCallMetaMap.get(toolCallId)?.name || toolCallMetaMap.get(normalizedToolCallId)?.name;
|
||||||
|
const toolName = msg.name || rememberedToolName || 'tool';
|
||||||
|
|
||||||
// Accumulate tool result
|
result.push({
|
||||||
pendingToolResults.push({
|
role: 'user',
|
||||||
tool_call_id: toolCallId,
|
content: buildToolResultBlock(toolName, toolCallId, extractTextContent(msg.content)),
|
||||||
name: toolName,
|
|
||||||
index: pendingToolResults.length,
|
|
||||||
raw_args: toolContent,
|
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (msg.role === 'user' || msg.role === 'assistant') {
|
if (msg.role === 'user' || msg.role === 'assistant') {
|
||||||
let content = '';
|
if (msg.role === 'assistant') {
|
||||||
|
const assistantToolCalls = mergeAssistantToolCalls(
|
||||||
if (typeof msg.content === 'string') {
|
msg.tool_calls || [],
|
||||||
content = msg.content;
|
extractToolCallsFromContent(msg.content, messageIndex)
|
||||||
} else if (Array.isArray(msg.content)) {
|
);
|
||||||
for (const part of msg.content) {
|
if (msg.tool_calls?.length) {
|
||||||
if (part.type === 'text' && part.text) {
|
rememberToolCallMeta(toolCallMetaMap, msg.tool_calls);
|
||||||
content += part.text;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
rememberToolUseParts(toolCallMetaMap, msg.content, messageIndex);
|
||||||
|
|
||||||
// Keep tool_calls structure for assistant messages
|
const content = extractTextContent(msg.content);
|
||||||
if (msg.role === 'assistant' && msg.tool_calls && msg.tool_calls.length > 0) {
|
if (assistantToolCalls.length > 0) {
|
||||||
const assistantMsg: CursorMessage = { role: 'assistant', content: '' };
|
result.push({
|
||||||
|
role: 'assistant',
|
||||||
|
content,
|
||||||
|
tool_calls: assistantToolCalls,
|
||||||
|
});
|
||||||
|
} else if (content) {
|
||||||
|
result.push({
|
||||||
|
role: 'assistant',
|
||||||
|
content,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const content = renderUserContent(msg.content, toolCallMetaMap);
|
||||||
if (content) {
|
if (content) {
|
||||||
assistantMsg.content = content;
|
result.push({
|
||||||
|
role: 'user',
|
||||||
|
content,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
assistantMsg.tool_calls = msg.tool_calls;
|
|
||||||
|
|
||||||
// Attach pending tool results to assistant message with tool_calls
|
|
||||||
if (pendingToolResults.length > 0) {
|
|
||||||
assistantMsg.tool_results = pendingToolResults;
|
|
||||||
pendingToolResults = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
result.push(assistantMsg);
|
|
||||||
} else if (content || pendingToolResults.length > 0) {
|
|
||||||
const msgObj: CursorMessage = {
|
|
||||||
role: msg.role,
|
|
||||||
content: content || '',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Attach pending tool results to this message
|
|
||||||
if (pendingToolResults.length > 0) {
|
|
||||||
msgObj.tool_results = pendingToolResults;
|
|
||||||
pendingToolResults = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
result.push(msgObj);
|
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unknown role - skip with debug warning
|
|
||||||
if (process.env.CCS_DEBUG) {
|
if (process.env.CCS_DEBUG) {
|
||||||
console.error(`[cursor] Unknown message role: ${msg.role}, skipping`);
|
console.error(`[cursor] Unknown message role: ${msg.role}, skipping`);
|
||||||
}
|
}
|
||||||
@@ -133,10 +374,6 @@ function convertMessages(messages: OpenAIMessage[]): CursorMessage[] {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Transform OpenAI request to Cursor format
|
|
||||||
* Returns modified body with converted messages
|
|
||||||
*/
|
|
||||||
export function buildCursorRequest(
|
export function buildCursorRequest(
|
||||||
_model: string,
|
_model: string,
|
||||||
body: OpenAIRequestBody,
|
body: OpenAIRequestBody,
|
||||||
@@ -146,10 +383,8 @@ export function buildCursorRequest(
|
|||||||
messages: CursorMessage[];
|
messages: CursorMessage[];
|
||||||
tools?: CursorTool[];
|
tools?: CursorTool[];
|
||||||
} {
|
} {
|
||||||
const messages = convertMessages(body.messages || []);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...body,
|
...body,
|
||||||
messages,
|
messages: convertMessages(body.messages || []),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,12 @@ import {
|
|||||||
} from '../../../src/cursor/cursor-daemon';
|
} from '../../../src/cursor/cursor-daemon';
|
||||||
import { getCcsDir } from '../../../src/utils/config-manager';
|
import { getCcsDir } from '../../../src/utils/config-manager';
|
||||||
import { handleCursorCommand } from '../../../src/commands/cursor-command';
|
import { handleCursorCommand } from '../../../src/commands/cursor-command';
|
||||||
|
import {
|
||||||
|
renderCursorHelp,
|
||||||
|
renderCursorStatus,
|
||||||
|
} from '../../../src/commands/cursor-command-display';
|
||||||
import { loadCredentials } from '../../../src/cursor/cursor-auth';
|
import { loadCredentials } from '../../../src/cursor/cursor-auth';
|
||||||
|
import { DEFAULT_CURSOR_CONFIG } from '../../../src/config/unified-config-types';
|
||||||
|
|
||||||
// Test isolation
|
// Test isolation
|
||||||
let originalCcsHome: string | undefined;
|
let originalCcsHome: string | undefined;
|
||||||
@@ -271,6 +276,34 @@ describe('stopDaemon', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('handleCursorCommand', () => {
|
describe('handleCursorCommand', () => {
|
||||||
|
it('treats bare ccs cursor as a status entrypoint instead of help', async () => {
|
||||||
|
const originalLog = console.log;
|
||||||
|
const originalError = console.error;
|
||||||
|
const logs: string[] = [];
|
||||||
|
const errors: string[] = [];
|
||||||
|
|
||||||
|
console.log = (...args: unknown[]) => {
|
||||||
|
logs.push(args.map((arg) => String(arg)).join(' '));
|
||||||
|
};
|
||||||
|
console.error = (...args: unknown[]) => {
|
||||||
|
errors.push(args.map((arg) => String(arg)).join(' '));
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const exitCode = await handleCursorCommand([]);
|
||||||
|
|
||||||
|
expect(exitCode).toBe(0);
|
||||||
|
expect(errors).toHaveLength(0);
|
||||||
|
expect(logs.some((line) => line.includes('Cursor IDE Status'))).toBe(true);
|
||||||
|
expect(logs.some((line) => line.includes('Usage: ccs cursor <subcommand> [options]'))).toBe(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
console.log = originalLog;
|
||||||
|
console.error = originalError;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('returns exit code 1 for unknown subcommand', async () => {
|
it('returns exit code 1 for unknown subcommand', async () => {
|
||||||
const exitCode = await handleCursorCommand(['nonexistent']);
|
const exitCode = await handleCursorCommand(['nonexistent']);
|
||||||
expect(exitCode).toBe(1);
|
expect(exitCode).toBe(1);
|
||||||
@@ -297,3 +330,102 @@ describe('handleCursorCommand', () => {
|
|||||||
expect(credentials?.machineId).toBe(machineId);
|
expect(credentials?.machineId).toBe(machineId);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('renderCursorStatus', () => {
|
||||||
|
it('shows runtime endpoint guidance when Cursor is ready', () => {
|
||||||
|
const originalLog = console.log;
|
||||||
|
const logs: string[] = [];
|
||||||
|
|
||||||
|
console.log = (...args: unknown[]) => {
|
||||||
|
logs.push(args.map((arg) => String(arg)).join(' '));
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
renderCursorStatus(
|
||||||
|
{ ...DEFAULT_CURSOR_CONFIG, enabled: true, port: 20129 },
|
||||||
|
{
|
||||||
|
authenticated: true,
|
||||||
|
expired: false,
|
||||||
|
tokenAge: 0,
|
||||||
|
credentials: {
|
||||||
|
accessToken: 'a'.repeat(60),
|
||||||
|
machineId: '1234567890abcdef1234567890abcdef',
|
||||||
|
authMethod: 'manual',
|
||||||
|
importedAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ running: true, port: 20129, pid: 1234 }
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(logs.some((line) => line.includes('OpenAI base: http://127.0.0.1:20129/v1'))).toBe(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
logs.some((line) => line.includes('Chat route: http://127.0.0.1:20129/v1/chat/completions'))
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
logs.some((line) => line.includes('Anthropic base: http://127.0.0.1:20129'))
|
||||||
|
).toBe(true);
|
||||||
|
expect(logs.some((line) => line.includes('Raw settings: ~/.ccs/cursor.settings.json'))).toBe(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
console.log = originalLog;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows runtime guidance even when setup is incomplete', () => {
|
||||||
|
const originalLog = console.log;
|
||||||
|
const logs: string[] = [];
|
||||||
|
|
||||||
|
console.log = (...args: unknown[]) => {
|
||||||
|
logs.push(args.map((arg) => String(arg)).join(' '));
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
renderCursorStatus(
|
||||||
|
{ ...DEFAULT_CURSOR_CONFIG, enabled: false, port: 20129 },
|
||||||
|
{
|
||||||
|
authenticated: false,
|
||||||
|
expired: false,
|
||||||
|
tokenAge: undefined,
|
||||||
|
credentials: undefined,
|
||||||
|
},
|
||||||
|
{ running: false, port: 20129, pid: undefined }
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(logs.some((line) => line.includes('OpenAI base: http://127.0.0.1:20129/v1'))).toBe(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
expect(logs.some((line) => line.includes('Next steps:'))).toBe(true);
|
||||||
|
expect(logs.some((line) => line.includes(' - Help: ccs cursor help'))).toBe(true);
|
||||||
|
} finally {
|
||||||
|
console.log = originalLog;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('renderCursorHelp', () => {
|
||||||
|
it('shows bare ccs cursor as an optional entrypoint', () => {
|
||||||
|
const originalLog = console.log;
|
||||||
|
const logs: string[] = [];
|
||||||
|
|
||||||
|
console.log = (...args: unknown[]) => {
|
||||||
|
logs.push(args.map((arg) => String(arg)).join(' '));
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const exitCode = renderCursorHelp();
|
||||||
|
|
||||||
|
expect(exitCode).toBe(0);
|
||||||
|
expect(logs.some((line) => line.includes('Usage: ccs cursor [subcommand] [options]'))).toBe(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
logs.some((line) => line.includes('4. ccs cursor # Show status and runtime connection details'))
|
||||||
|
).toBe(true);
|
||||||
|
} finally {
|
||||||
|
console.log = originalLog;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
decodeVarint,
|
decodeVarint,
|
||||||
decodeField,
|
decodeField,
|
||||||
|
decodeMessage,
|
||||||
parseConnectRPCFrame,
|
parseConnectRPCFrame,
|
||||||
} from '../../../src/cursor/cursor-protobuf-decoder';
|
} from '../../../src/cursor/cursor-protobuf-decoder';
|
||||||
import { buildCursorRequest } from '../../../src/cursor/cursor-translator';
|
import { buildCursorRequest } from '../../../src/cursor/cursor-translator';
|
||||||
@@ -251,11 +252,256 @@ describe('Message Translation', () => {
|
|||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
expect(result.messages).toHaveLength(3);
|
||||||
|
expect(result.messages[1].role).toBe('user');
|
||||||
|
expect(result.messages[1].content).toContain('<tool_result>');
|
||||||
|
expect(result.messages[1].content).toContain('<tool_name>get_weather</tool_name>');
|
||||||
|
expect(result.messages[1].content).toContain('<tool_call_id>call_123</tool_call_id>');
|
||||||
|
expect(result.messages[2]).toEqual({ role: 'user', content: 'What is the weather?' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should recover tool names for tool results without a name field', () => {
|
||||||
|
const result = buildCursorRequest(
|
||||||
|
'gpt-4',
|
||||||
|
{
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: 'assistant',
|
||||||
|
content: '',
|
||||||
|
tool_calls: [
|
||||||
|
{
|
||||||
|
id: 'call_456',
|
||||||
|
type: 'function',
|
||||||
|
function: { name: 'search_docs', arguments: '{"q":"cursor"}' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'tool',
|
||||||
|
content: 'done',
|
||||||
|
tool_call_id: 'call_456',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
expect(result.messages).toHaveLength(2);
|
expect(result.messages).toHaveLength(2);
|
||||||
// Tool result should be attached to next message
|
expect(result.messages[1].content).toContain('<tool_name>search_docs</tool_name>');
|
||||||
expect(result.messages[1].tool_results).toBeDefined();
|
expect(result.messages[1].tool_results).toBeUndefined();
|
||||||
expect(result.messages[1].tool_results).toHaveLength(1);
|
});
|
||||||
expect(result.messages[1].tool_results![0].tool_call_id).toBe('call_123');
|
|
||||||
|
it('should flatten user content arrays that include tool_result blocks', () => {
|
||||||
|
const result = buildCursorRequest(
|
||||||
|
'gpt-4',
|
||||||
|
{
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: 'assistant',
|
||||||
|
content: '',
|
||||||
|
tool_calls: [
|
||||||
|
{
|
||||||
|
id: 'call_789',
|
||||||
|
type: 'function',
|
||||||
|
function: { name: 'search_docs', arguments: '{"q":"cursor"}' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content: [
|
||||||
|
{ type: 'text', text: 'Tool finished.' },
|
||||||
|
{
|
||||||
|
type: 'tool_result',
|
||||||
|
tool_use_id: 'call_789',
|
||||||
|
content: { answer: 'Cursor integration ready' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.messages).toHaveLength(2);
|
||||||
|
expect(result.messages[1].role).toBe('user');
|
||||||
|
expect(result.messages[1].content).toContain('Tool finished.');
|
||||||
|
expect(result.messages[1].content).toContain('<tool_name>search_docs</tool_name>');
|
||||||
|
expect(result.messages[1].content).toContain('{"answer":"Cursor integration ready"}');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should convert assistant tool_use content blocks into tool_calls', () => {
|
||||||
|
const result = buildCursorRequest(
|
||||||
|
'gpt-4',
|
||||||
|
{
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: 'assistant',
|
||||||
|
content: [
|
||||||
|
{ type: 'text', text: 'Inspecting the workspace.' },
|
||||||
|
{
|
||||||
|
type: 'tool_use',
|
||||||
|
id: 'call_999',
|
||||||
|
name: 'list_files',
|
||||||
|
input: { path: '/tmp' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.messages).toHaveLength(1);
|
||||||
|
expect(result.messages[0].role).toBe('assistant');
|
||||||
|
expect(result.messages[0].content).toBe('Inspecting the workspace.');
|
||||||
|
expect(result.messages[0].tool_calls).toEqual([
|
||||||
|
{
|
||||||
|
id: 'call_999',
|
||||||
|
type: 'function',
|
||||||
|
function: { name: 'list_files', arguments: '{"path":"/tmp"}' },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should synthesize fallback ids for tool_use blocks that omit them', () => {
|
||||||
|
const result = buildCursorRequest(
|
||||||
|
'gpt-4',
|
||||||
|
{
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: 'assistant',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'tool_use',
|
||||||
|
name: 'search_docs',
|
||||||
|
input: { q: 'cursor' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.messages).toHaveLength(1);
|
||||||
|
expect(result.messages[0].tool_calls).toHaveLength(1);
|
||||||
|
expect(result.messages[0].tool_calls![0].id).toBe('toolu_cursor_fallback_0_0');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should dedupe tool calls when assistant messages contain both tool_calls and tool_use blocks', () => {
|
||||||
|
const result = buildCursorRequest(
|
||||||
|
'gpt-4',
|
||||||
|
{
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: 'assistant',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'tool_use',
|
||||||
|
id: 'call_dupe',
|
||||||
|
name: 'search_docs',
|
||||||
|
input: { q: 'cursor' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
tool_calls: [
|
||||||
|
{
|
||||||
|
id: 'call_dupe',
|
||||||
|
type: 'function',
|
||||||
|
function: { name: 'search_docs', arguments: '{"q":"cursor"}' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.messages).toHaveLength(1);
|
||||||
|
expect(result.messages[0].tool_calls).toEqual([
|
||||||
|
{
|
||||||
|
id: 'call_dupe',
|
||||||
|
type: 'function',
|
||||||
|
function: { name: 'search_docs', arguments: '{"q":"cursor"}' },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should truncate oversized tool result payloads', () => {
|
||||||
|
const oversizedResult = '&'.repeat(12_050);
|
||||||
|
const result = buildCursorRequest(
|
||||||
|
'gpt-4',
|
||||||
|
{
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: 'assistant',
|
||||||
|
content: '',
|
||||||
|
tool_calls: [
|
||||||
|
{
|
||||||
|
id: 'call_big',
|
||||||
|
type: 'function',
|
||||||
|
function: { name: 'read_file', arguments: '{"path":"big.txt"}' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'tool',
|
||||||
|
content: oversizedResult,
|
||||||
|
tool_call_id: 'call_big',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.messages).toHaveLength(2);
|
||||||
|
expect(result.messages[1].content).toContain('[truncated ');
|
||||||
|
expect(result.messages[1].content.length).toBeLessThan(12_250);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should mark unserializable structured tool results explicitly', () => {
|
||||||
|
const circular: Record<string, unknown> = {};
|
||||||
|
circular.self = circular;
|
||||||
|
|
||||||
|
const result = buildCursorRequest(
|
||||||
|
'gpt-4',
|
||||||
|
{
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: 'assistant',
|
||||||
|
content: '',
|
||||||
|
tool_calls: [
|
||||||
|
{
|
||||||
|
id: 'call_bad',
|
||||||
|
type: 'function',
|
||||||
|
function: { name: 'read_json', arguments: '{"path":"bad.json"}' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'tool_result',
|
||||||
|
tool_use_id: 'call_bad',
|
||||||
|
content: circular,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.messages).toHaveLength(2);
|
||||||
|
expect(result.messages[1].content).toContain('[unserializable content]');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle array content format', () => {
|
it('should handle array content format', () => {
|
||||||
@@ -344,6 +590,57 @@ describe('Request Encoding', () => {
|
|||||||
expect(result).toBeInstanceOf(Uint8Array);
|
expect(result).toBeInstanceOf(Uint8Array);
|
||||||
expect(result.length).toBeGreaterThan(0);
|
expect(result.length).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should preserve flattened tool_result blocks through protobuf encoding', () => {
|
||||||
|
const translated = buildCursorRequest(
|
||||||
|
'gpt-4',
|
||||||
|
{
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: 'assistant',
|
||||||
|
content: '',
|
||||||
|
tool_calls: [
|
||||||
|
{
|
||||||
|
id: 'call_wire',
|
||||||
|
type: 'function',
|
||||||
|
function: { name: 'read_file', arguments: '{"path":"notes.txt"}' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'tool',
|
||||||
|
content: 'workspace snapshot',
|
||||||
|
tool_call_id: 'call_wire',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
const body = generateCursorBody(translated.messages, 'gpt-4', [], null);
|
||||||
|
const frame = parseConnectRPCFrame(Buffer.from(body));
|
||||||
|
expect(frame).not.toBeNull();
|
||||||
|
|
||||||
|
const topLevel = decodeMessage(frame!.payload);
|
||||||
|
const requestPayload = topLevel.get(FIELD.Request.REQUEST)?.[0]?.value as Uint8Array;
|
||||||
|
const chatRequest = decodeMessage(requestPayload);
|
||||||
|
const encodedMessages = (chatRequest.get(FIELD.Chat.MESSAGES) || []).map((entry) =>
|
||||||
|
decodeMessage(entry.value as Uint8Array)
|
||||||
|
);
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
const contents = encodedMessages.map((message) =>
|
||||||
|
decoder.decode(message.get(FIELD.Message.CONTENT)?.[0]?.value as Uint8Array)
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(contents.some((content) => content.includes('<tool_result>'))).toBe(true);
|
||||||
|
expect(contents.some((content) => content.includes('<tool_name>read_file</tool_name>'))).toBe(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
contents.some((content) => content.includes('<tool_call_id>call_wire</tool_call_id>'))
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Edge cases', () => {
|
describe('Edge cases', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user