Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
"description": "Platform agnostic core functionality for Roo Code.",
"version": "0.0.0",
"type": "module",
"exports": "./src/index.ts",
"exports": {
".": "./src/index.ts",
"./cli": "./src/cli.ts",
"./browser": "./src/browser.ts"
},
"scripts": {
"lint": "eslint src --ext=ts --max-warnings=0",
"check-types": "tsc --noEmit",
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/browser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* Browser-safe exports for the core package. These can safely be used
* in browser environments like `webview-ui`.
*/

export * from "./message-utils/index.js"
6 changes: 6 additions & 0 deletions packages/core/src/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* Cli-safe exports for the core package.
*/

export * from "./debug-log/index.js"
export * from "./message-utils/index.js"
91 changes: 91 additions & 0 deletions packages/core/src/debug-log/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/**
* File-based debug logging utility
*
* This writes logs to ~/.roo/cli-debug.log, avoiding stdout/stderr
* which would break TUI applications. The log format is timestamped JSON.
*
* Usage:
* import { debugLog, DebugLogger } from "@roo-code/core/debug-log"
*
* // Simple logging
* debugLog("handleModeSwitch", { mode: newMode, configId })
*
* // Or create a named logger for a component
* const log = new DebugLogger("ClineProvider")
* log.info("handleModeSwitch", { mode: newMode })
*/

import * as fs from "fs"
import * as path from "path"
import * as os from "os"

const DEBUG_LOG_PATH = path.join(os.homedir(), ".roo", "cli-debug.log")

/**
* Simple file-based debug log function.
* Writes timestamped entries to ~/.roo/cli-debug.log
*/
export function debugLog(message: string, data?: unknown): void {
try {
const logDir = path.dirname(DEBUG_LOG_PATH)

if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true })
}

const timestamp = new Date().toISOString()

const entry = data
? `[${timestamp}] ${message}: ${JSON.stringify(data, null, 2)}\n`
: `[${timestamp}] ${message}\n`

fs.appendFileSync(DEBUG_LOG_PATH, entry)
} catch {
// NO-OP - don't let logging errors break functionality
}
}

/**
* Debug logger with component context.
* Prefixes all messages with the component name.
*/
export class DebugLogger {
private component: string

constructor(component: string) {
this.component = component
}

/**
* Log a debug message with optional data
*/
debug(message: string, data?: unknown): void {
debugLog(`[${this.component}] ${message}`, data)
}

/**
* Alias for debug
*/
info(message: string, data?: unknown): void {
this.debug(message, data)
}

/**
* Log a warning
*/
warn(message: string, data?: unknown): void {
debugLog(`[${this.component}] WARN: ${message}`, data)
}

/**
* Log an error
*/
error(message: string, data?: unknown): void {
debugLog(`[${this.component}] ERROR: ${message}`, data)
}
}

/**
* Pre-configured logger for provider/mode debugging
*/
export const providerDebugLog = new DebugLogger("ProviderSettings")
2 changes: 2 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export * from "./custom-tools/index.js"
export * from "./debug-log/index.js"
export * from "./message-utils/index.js"
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// npx vitest run packages/core/src/message-utils/__tests__/consolidateApiRequests.spec.ts

import type { ClineMessage } from "@roo-code/types"

import { consolidateApiRequests } from "../consolidateApiRequests.js"

describe("consolidateApiRequests", () => {
// Helper function to create a basic api_req_started message
const createApiReqStarted = (ts: number, data: Record<string, unknown> = {}): ClineMessage => ({
ts,
type: "say",
say: "api_req_started",
text: JSON.stringify(data),
})

// Helper function to create a basic api_req_finished message
const createApiReqFinished = (ts: number, data: Record<string, unknown> = {}): ClineMessage => ({
ts,
type: "say",
say: "api_req_finished",
text: JSON.stringify(data),
})

// Helper function to create a regular text message
const createTextMessage = (ts: number, text: string): ClineMessage => ({
ts,
type: "say",
say: "text",
text,
})

it("should consolidate a matching pair of api_req_started and api_req_finished messages", () => {
const messages: ClineMessage[] = [
createApiReqStarted(1000, { request: "GET /api/data" }),
createApiReqFinished(1001, { cost: 0.005 }),
]

const result = consolidateApiRequests(messages)

expect(result.length).toBe(1)
expect(result[0]!.say).toBe("api_req_started")

const parsedText = JSON.parse(result[0]!.text || "{}")
expect(parsedText.request).toBe("GET /api/data")
expect(parsedText.cost).toBe(0.005)
})

it("should handle messages with no api_req pairs", () => {
const messages: ClineMessage[] = [createTextMessage(1000, "Hello"), createTextMessage(1001, "World")]

const result = consolidateApiRequests(messages)

expect(result).toEqual(messages)
})

it("should handle empty messages array", () => {
const result = consolidateApiRequests([])
expect(result).toEqual([])
})

it("should handle single message array", () => {
const messages: ClineMessage[] = [createTextMessage(1000, "Hello")]
const result = consolidateApiRequests(messages)
expect(result).toEqual(messages)
})

it("should preserve non-api messages in the result", () => {
const messages: ClineMessage[] = [
createTextMessage(1000, "Before"),
createApiReqStarted(1001, { request: "test" }),
createApiReqFinished(1002, { cost: 0.01 }),
createTextMessage(1003, "After"),
]

const result = consolidateApiRequests(messages)

expect(result.length).toBe(3)
expect(result[0]!.text).toBe("Before")
expect(result[1]!.say).toBe("api_req_started")
expect(result[2]!.text).toBe("After")
})

it("should handle multiple api_req pairs", () => {
const messages: ClineMessage[] = [
createApiReqStarted(1000, { request: "first" }),
createApiReqFinished(1001, { cost: 0.01 }),
createApiReqStarted(1002, { request: "second" }),
createApiReqFinished(1003, { cost: 0.02 }),
]

const result = consolidateApiRequests(messages)

expect(result.length).toBe(2)
expect(JSON.parse(result[0]!.text || "{}").request).toBe("first")
expect(JSON.parse(result[1]!.text || "{}").request).toBe("second")
})

it("should handle orphan api_req_started without finish", () => {
const messages: ClineMessage[] = [
createApiReqStarted(1000, { request: "orphan" }),
createTextMessage(1001, "Text"),
]

const result = consolidateApiRequests(messages)

expect(result.length).toBe(2)
expect(result[0]!.say).toBe("api_req_started")
expect(JSON.parse(result[0]!.text || "{}").request).toBe("orphan")
})

it("should handle invalid JSON in message text", () => {
const messages: ClineMessage[] = [
{ ts: 1000, type: "say", say: "api_req_started", text: "invalid json" },
createApiReqFinished(1001, { cost: 0.01 }),
]

const result = consolidateApiRequests(messages)

// Should still consolidate, merging what it can
expect(result.length).toBe(1)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// npx vitest run packages/core/src/message-utils/__tests__/consolidateCommands.spec.ts

import type { ClineMessage } from "@roo-code/types"

import { consolidateCommands, COMMAND_OUTPUT_STRING } from "../consolidateCommands.js"

describe("consolidateCommands", () => {
describe("command sequences", () => {
it("should consolidate command and command_output messages", () => {
const messages: ClineMessage[] = [
{ type: "ask", ask: "command", text: "ls", ts: 1000 },
{ type: "ask", ask: "command_output", text: "file1.txt", ts: 1001 },
{ type: "ask", ask: "command_output", text: "file2.txt", ts: 1002 },
]

const result = consolidateCommands(messages)

expect(result.length).toBe(1)
expect(result[0]!.ask).toBe("command")
expect(result[0]!.text).toBe(`ls\n${COMMAND_OUTPUT_STRING}file1.txt\nfile2.txt`)
})

it("should handle multiple command sequences", () => {
const messages: ClineMessage[] = [
{ type: "ask", ask: "command", text: "ls", ts: 1000 },
{ type: "ask", ask: "command_output", text: "output1", ts: 1001 },
{ type: "ask", ask: "command", text: "pwd", ts: 1002 },
{ type: "ask", ask: "command_output", text: "output2", ts: 1003 },
]

const result = consolidateCommands(messages)

expect(result.length).toBe(2)
expect(result[0]!.text).toBe(`ls\n${COMMAND_OUTPUT_STRING}output1`)
expect(result[1]!.text).toBe(`pwd\n${COMMAND_OUTPUT_STRING}output2`)
})

it("should handle command without output", () => {
const messages: ClineMessage[] = [
{ type: "ask", ask: "command", text: "ls", ts: 1000 },
{ type: "say", say: "text", text: "some text", ts: 1001 },
]

const result = consolidateCommands(messages)

expect(result.length).toBe(2)
expect(result[0]!.ask).toBe("command")
expect(result[0]!.text).toBe("ls")
expect(result[1]!.say).toBe("text")
})

it("should handle duplicate outputs (ask and say with same text)", () => {
const messages: ClineMessage[] = [
{ type: "ask", ask: "command", text: "ls", ts: 1000 },
{ type: "ask", ask: "command_output", text: "same output", ts: 1001 },
{ type: "say", say: "command_output", text: "same output", ts: 1002 },
]

const result = consolidateCommands(messages)

expect(result.length).toBe(1)
expect(result[0]!.text).toBe(`ls\n${COMMAND_OUTPUT_STRING}same output`)
})
})

describe("MCP server sequences", () => {
it("should consolidate use_mcp_server and mcp_server_response messages", () => {
const messages: ClineMessage[] = [
{
type: "ask",
ask: "use_mcp_server",
text: JSON.stringify({ server: "test", tool: "myTool" }),
ts: 1000,
},
{ type: "say", say: "mcp_server_response", text: "response data", ts: 1001 },
]

const result = consolidateCommands(messages)

expect(result.length).toBe(1)
expect(result[0]!.ask).toBe("use_mcp_server")
const parsed = JSON.parse(result[0]!.text || "{}")
expect(parsed.server).toBe("test")
expect(parsed.response).toBe("response data")
})

it("should handle MCP request without response", () => {
const messages: ClineMessage[] = [
{
type: "ask",
ask: "use_mcp_server",
text: JSON.stringify({ server: "test" }),
ts: 1000,
},
]

const result = consolidateCommands(messages)

expect(result.length).toBe(1)
expect(result[0]!.ask).toBe("use_mcp_server")
})

it("should handle multiple MCP responses", () => {
const messages: ClineMessage[] = [
{
type: "ask",
ask: "use_mcp_server",
text: JSON.stringify({ server: "test" }),
ts: 1000,
},
{ type: "say", say: "mcp_server_response", text: "response1", ts: 1001 },
{ type: "say", say: "mcp_server_response", text: "response2", ts: 1002 },
]

const result = consolidateCommands(messages)

expect(result.length).toBe(1)
const parsed = JSON.parse(result[0]!.text || "{}")
expect(parsed.response).toBe("response1\nresponse2")
})
})

describe("mixed messages", () => {
it("should preserve non-command, non-MCP messages", () => {
const messages: ClineMessage[] = [
{ type: "say", say: "text", text: "before", ts: 1000 },
{ type: "ask", ask: "command", text: "ls", ts: 1001 },
{ type: "ask", ask: "command_output", text: "output", ts: 1002 },
{ type: "say", say: "text", text: "after", ts: 1003 },
]

const result = consolidateCommands(messages)

expect(result.length).toBe(3)
expect(result[0]!.text).toBe("before")
expect(result[1]!.text).toBe(`ls\n${COMMAND_OUTPUT_STRING}output`)
expect(result[2]!.text).toBe("after")
})

it("should handle empty array", () => {
const result = consolidateCommands([])
expect(result).toEqual([])
})
})
})
Loading
Loading