From 35b2059729e2d2b15a44d58e27ef15d495082b72 Mon Sep 17 00:00:00 2001 From: Eduard Ruzga Date: Tue, 30 Sep 2025 21:29:38 +0300 Subject: [PATCH 1/3] Add new tool to capture and query recent tool call history, helps to restore lost chats or pass context between chats about work that was done recently --- README.md | 1 + manifest.template.json | 4 + src/handlers/history-handlers.ts | 40 ++++++ src/handlers/index.ts | 1 + src/server.ts | 46 +++++++ src/tools/config.ts | 16 ++- src/tools/schemas.ts | 7 + src/utils/toolHistory.ts | 229 +++++++++++++++++++++++++++++++ 8 files changed, 343 insertions(+), 1 deletion(-) create mode 100644 src/handlers/history-handlers.ts create mode 100644 src/utils/toolHistory.ts diff --git a/README.md b/README.md index 60788009..111903b3 100644 --- a/README.md +++ b/README.md @@ -436,6 +436,7 @@ The server provides a comprehensive set of tools organized into several categori | | `get_file_info` | Retrieve detailed metadata about a file or directory | | **Text Editing** | `edit_block` | Apply targeted text replacements with enhanced prompting for smaller edits (includes character-level diff feedback) | | **Analytics** | `get_usage_stats` | Get usage statistics for your own insight | +| | `get_recent_tool_calls` | Get recent tool call history with arguments and outputs for debugging and context recovery | | | `give_feedback_to_desktop_commander` | Open feedback form in browser to provide feedback to Desktop Commander Team | ### Quick Examples diff --git a/manifest.template.json b/manifest.template.json index db67aaa6..556f90e4 100644 --- a/manifest.template.json +++ b/manifest.template.json @@ -122,6 +122,10 @@ "name": "get_usage_stats", "description": "Get usage statistics for debugging and analysis including tool usage and performance metrics." }, + { + "name": "get_recent_tool_calls", + "description": "Get recent tool call history with their arguments and outputs. Returns chronological list of tool calls made during this session for debugging, context recovery, and onboarding new chats." + }, { "name": "give_feedback_to_desktop_commander", "description": "Open feedback form in browser to provide feedback about Desktop Commander." diff --git a/src/handlers/history-handlers.ts b/src/handlers/history-handlers.ts new file mode 100644 index 00000000..61a2ccc2 --- /dev/null +++ b/src/handlers/history-handlers.ts @@ -0,0 +1,40 @@ +import { toolHistory } from '../utils/toolHistory.js'; +import { GetRecentToolCallsArgsSchema } from '../tools/schemas.js'; +import { ServerResult } from '../types.js'; + +/** + * Handle get_recent_tool_calls command + */ +export async function handleGetRecentToolCalls(args: unknown): Promise { + try { + const parsed = GetRecentToolCallsArgsSchema.parse(args); + + // Use formatted version with local timezone + const calls = toolHistory.getRecentCallsFormatted({ + maxResults: parsed.maxResults, + toolName: parsed.toolName, + since: parsed.since + }); + + const stats = toolHistory.getStats(); + + // Format the response (excluding file path per user request) + const summary = `Tool Call History (${calls.length} results, ${stats.totalEntries} total in memory)`; + const historyJson = JSON.stringify(calls, null, 2); + + return { + content: [{ + type: "text", + text: `${summary}\n\n${historyJson}` + }] + }; + } catch (error) { + return { + content: [{ + type: "text", + text: `Error getting tool history: ${error instanceof Error ? error.message : String(error)}` + }], + isError: true + }; + } +} diff --git a/src/handlers/index.ts b/src/handlers/index.ts index 101f4eee..1ac19090 100644 --- a/src/handlers/index.ts +++ b/src/handlers/index.ts @@ -4,3 +4,4 @@ export * from './terminal-handlers.js'; export * from './process-handlers.js'; export * from './edit-search-handlers.js'; export * from './search-handlers.js'; +export * from './history-handlers.js'; diff --git a/src/server.ts b/src/server.ts index 6ba98ff3..4664835a 100644 --- a/src/server.ts +++ b/src/server.ts @@ -45,6 +45,7 @@ import { StopSearchArgsSchema, ListSearchesArgsSchema, GetPromptsArgsSchema, + GetRecentToolCallsArgsSchema, } from './tools/schemas.js'; import {getConfig, setConfigValue} from './tools/config.js'; import {getUsageStats} from './tools/usage.js'; @@ -53,6 +54,7 @@ import {getPrompts} from './tools/prompts.js'; import {trackToolCall} from './utils/trackTools.js'; import {usageTracker} from './utils/usageTracker.js'; import {processDockerPrompt} from './utils/dockerPrompt.js'; +import {toolHistory} from './utils/toolHistory.js'; import {VERSION} from './version.js'; import {capture, capture_call_tool} from "./utils/capture.js"; @@ -811,6 +813,27 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { readOnlyHint: true, }, }, + { + name: "get_recent_tool_calls", + description: ` + Get recent tool call history with their arguments and outputs. + Returns chronological list of tool calls made during this session. + + Useful for: + - Onboarding new chats about work already done + - Recovering context after chat history loss + - Debugging tool call sequences + + Note: Does not track its own calls or other meta/query tools. + History kept in memory (last 1000 calls, lost on restart). + + ${CMD_PREFIX_DESCRIPTION}`, + inputSchema: zodToJsonSchema(GetRecentToolCallsArgsSchema), + annotations: { + title: "Get Recent Tool Calls", + readOnlyHint: true, + }, + }, { name: "give_feedback_to_desktop_commander", description: ` @@ -895,6 +918,7 @@ import {ServerResult} from './types.js'; server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest): Promise => { const {name, arguments: args} = request.params; + const startTime = Date.now(); try { // Prepare telemetry data - add config key for set_config_value @@ -1042,6 +1066,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest) } break; + case "get_recent_tool_calls": + try { + result = await handlers.handleGetRecentToolCalls(args); + } catch (error) { + capture('server_request_error', {message: `Error in get_recent_tool_calls handler: ${error}`}); + result = { + content: [{type: "text", text: `Error: Failed to get tool call history`}], + isError: true, + }; + } + break; + case "give_feedback_to_desktop_commander": try { result = await giveFeedbackToDesktopCommander(args); @@ -1143,6 +1179,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest) }; } + // Add tool call to history (exclude only get_recent_tool_calls to prevent recursion) + const duration = Date.now() - startTime; + const EXCLUDED_TOOLS = [ + 'get_recent_tool_calls' + ]; + + if (!EXCLUDED_TOOLS.includes(name)) { + toolHistory.addCall(name, args, result, duration); + } + // Track success or failure based on result if (result.isError) { await usageTracker.trackFailure(name); diff --git a/src/tools/config.ts b/src/tools/config.ts index 81ab3e05..e370d6c5 100644 --- a/src/tools/config.ts +++ b/src/tools/config.ts @@ -13,10 +13,24 @@ export async function getConfig() { // Add system information and current client to the config response const systemInfo = getSystemInfo(); + + // Get memory usage + const memoryUsage = process.memoryUsage(); + const memory = { + rss: `${(memoryUsage.rss / 1024 / 1024).toFixed(2)} MB`, + heapTotal: `${(memoryUsage.heapTotal / 1024 / 1024).toFixed(2)} MB`, + heapUsed: `${(memoryUsage.heapUsed / 1024 / 1024).toFixed(2)} MB`, + external: `${(memoryUsage.external / 1024 / 1024).toFixed(2)} MB`, + arrayBuffers: `${(memoryUsage.arrayBuffers / 1024 / 1024).toFixed(2)} MB` + }; + const configWithSystemInfo = { ...config, currentClient, - systemInfo + systemInfo: { + ...systemInfo, + memory + } }; console.error(`getConfig result: ${JSON.stringify(configWithSystemInfo, null, 2)}`); diff --git a/src/tools/schemas.ts b/src/tools/schemas.ts index 958c41dc..35dd7edf 100644 --- a/src/tools/schemas.ts +++ b/src/tools/schemas.ts @@ -136,4 +136,11 @@ export const GetPromptsArgsSchema = z.object({ action: z.enum(['list_categories', 'list_prompts', 'get_prompt']), category: z.string().optional(), promptId: z.string().optional(), +}); + +// Tool history schema +export const GetRecentToolCallsArgsSchema = z.object({ + maxResults: z.number().min(1).max(1000).optional().default(50), + toolName: z.string().optional(), + since: z.string().datetime().optional(), }); \ No newline at end of file diff --git a/src/utils/toolHistory.ts b/src/utils/toolHistory.ts new file mode 100644 index 00000000..84c87f12 --- /dev/null +++ b/src/utils/toolHistory.ts @@ -0,0 +1,229 @@ +import { ServerResult } from '../types.js'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as os from 'os'; + +export interface ToolCallRecord { + timestamp: string; + toolName: string; + arguments: any; + output: ServerResult; + duration?: number; +} + +// Format timestamp in local timezone for display +function formatLocalTimestamp(isoTimestamp: string): string { + const date = new Date(isoTimestamp); + return date.toLocaleString('en-US', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false + }); +} + +class ToolHistory { + private history: ToolCallRecord[] = []; + private readonly MAX_ENTRIES = 1000; + private readonly historyFile: string; + private writeQueue: ToolCallRecord[] = []; + private isWriting = false; + + constructor() { + // Store history in same directory as config to keep everything together + const historyDir = path.join(os.homedir(), '.claude-server-commander'); + + // Ensure directory exists + if (!fs.existsSync(historyDir)) { + fs.mkdirSync(historyDir, { recursive: true }); + } + + // Use append-only JSONL format (JSON Lines) + this.historyFile = path.join(historyDir, 'tool-history.jsonl'); + + // Load existing history on startup + this.loadFromDisk(); + + // Start async write processor + this.startWriteProcessor(); + } + + /** + * Load history from disk (all instances share the same file) + */ + private loadFromDisk(): void { + try { + if (!fs.existsSync(this.historyFile)) { + console.error('[ToolHistory] No history file found, starting fresh'); + return; + } + + const content = fs.readFileSync(this.historyFile, 'utf-8'); + const lines = content.trim().split('\n').filter(line => line.trim()); + + // Parse each line as JSON + const records: ToolCallRecord[] = []; + for (const line of lines) { + try { + records.push(JSON.parse(line)); + } catch (e) { + console.error('[ToolHistory] Failed to parse line:', line); + } + } + + // Keep only last 1000 entries + this.history = records.slice(-this.MAX_ENTRIES); + console.error(`[ToolHistory] Loaded ${this.history.length} entries from disk`); + + // If file is getting too large, trim it + if (lines.length > this.MAX_ENTRIES * 2) { + this.trimHistoryFile(); + } + } catch (error) { + console.error('[ToolHistory] Failed to load history:', error); + } + } + + /** + * Trim history file to prevent it from growing indefinitely + */ + private trimHistoryFile(): void { + try { + console.error('[ToolHistory] Trimming history file...'); + + // Keep last 1000 entries in memory + const keepEntries = this.history.slice(-this.MAX_ENTRIES); + + // Write them back + const lines = keepEntries.map(entry => JSON.stringify(entry)).join('\n') + '\n'; + fs.writeFileSync(this.historyFile, lines, 'utf-8'); + + console.error(`[ToolHistory] Trimmed to ${keepEntries.length} entries`); + } catch (error) { + console.error('[ToolHistory] Failed to trim history file:', error); + } + } + + /** + * Async write processor - batches writes to avoid blocking + */ + private startWriteProcessor(): void { + setInterval(() => { + if (this.writeQueue.length > 0 && !this.isWriting) { + this.flushToDisk(); + } + }, 1000); // Flush every second + } + + /** + * Flush queued writes to disk + */ + private async flushToDisk(): Promise { + if (this.isWriting || this.writeQueue.length === 0) return; + + this.isWriting = true; + const toWrite = [...this.writeQueue]; + this.writeQueue = []; + + try { + // Append to file (atomic append operation) + const lines = toWrite.map(entry => JSON.stringify(entry)).join('\n') + '\n'; + fs.appendFileSync(this.historyFile, lines, 'utf-8'); + } catch (error) { + console.error('[ToolHistory] Failed to write to disk:', error); + // Put back in queue on failure + this.writeQueue.unshift(...toWrite); + } finally { + this.isWriting = false; + } + } + + /** + * Add a tool call to history + */ + addCall( + toolName: string, + args: any, + output: ServerResult, + duration?: number + ): void { + const record: ToolCallRecord = { + timestamp: new Date().toISOString(), + toolName, + arguments: args, + output, + duration + }; + + this.history.push(record); + + // Keep only last 1000 in memory + if (this.history.length > this.MAX_ENTRIES) { + this.history.shift(); + } + + // Queue for async write + this.writeQueue.push(record); + } + + /** + * Get recent tool calls with filters + */ + getRecentCalls(options: { + maxResults?: number; + toolName?: string; + since?: string; + }): ToolCallRecord[] { + let results = [...this.history]; + + // Filter by tool name + if (options.toolName) { + results = results.filter(r => r.toolName === options.toolName); + } + + // Filter by timestamp + if (options.since) { + const sinceDate = new Date(options.since); + results = results.filter(r => new Date(r.timestamp) >= sinceDate); + } + + // Limit results (default 50, max 1000) + const limit = Math.min(options.maxResults || 50, 1000); + return results.slice(-limit); + } + + /** + * Get recent calls formatted with local timezone + */ + getRecentCallsFormatted(options: { + maxResults?: number; + toolName?: string; + since?: string; + }): any[] { + const calls = this.getRecentCalls(options); + + // Format timestamps to local timezone + return calls.map(call => ({ + ...call, + timestamp: formatLocalTimestamp(call.timestamp) + })); + } + + /** + * Get current stats + */ + getStats() { + return { + totalEntries: this.history.length, + oldestEntry: this.history[0]?.timestamp, + newestEntry: this.history[this.history.length - 1]?.timestamp, + historyFile: this.historyFile, + queuedWrites: this.writeQueue.length + }; + } +} + +export const toolHistory = new ToolHistory(); From 5d564b1732ab604dde270a7c3d5f4d11a4d8694c Mon Sep 17 00:00:00 2001 From: Eduard Ruzga Date: Wed, 1 Oct 2025 14:33:52 +0300 Subject: [PATCH 2/3] Fix: Prevent toolHistory interval from blocking process exit - Add writeInterval property to track setInterval reference - Use unref() to allow graceful process shutdown - Add cleanup() method to properly dispose resources - Fixes test hang issue where Node process never exited --- src/utils/toolHistory.ts | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/utils/toolHistory.ts b/src/utils/toolHistory.ts index 84c87f12..7bb21726 100644 --- a/src/utils/toolHistory.ts +++ b/src/utils/toolHistory.ts @@ -31,6 +31,7 @@ class ToolHistory { private readonly historyFile: string; private writeQueue: ToolCallRecord[] = []; private isWriting = false; + private writeInterval?: NodeJS.Timeout; constructor() { // Store history in same directory as config to keep everything together @@ -111,11 +112,14 @@ class ToolHistory { * Async write processor - batches writes to avoid blocking */ private startWriteProcessor(): void { - setInterval(() => { + this.writeInterval = setInterval(() => { if (this.writeQueue.length > 0 && !this.isWriting) { this.flushToDisk(); } }, 1000); // Flush every second + + // Prevent interval from keeping process alive during shutdown/tests + this.writeInterval.unref(); } /** @@ -224,6 +228,23 @@ class ToolHistory { queuedWrites: this.writeQueue.length }; } + + /** + * Cleanup method - clears interval and flushes pending writes + * Call this during shutdown or in tests + */ + async cleanup(): Promise { + // Clear the interval + if (this.writeInterval) { + clearInterval(this.writeInterval); + this.writeInterval = undefined; + } + + // Flush any remaining writes + if (this.writeQueue.length > 0) { + await this.flushToDisk(); + } + } } export const toolHistory = new ToolHistory(); From e816bb512a0755e74c142b7f6519ddde6fd6f433 Mon Sep 17 00:00:00 2001 From: Eduard Ruzga Date: Wed, 1 Oct 2025 14:46:49 +0300 Subject: [PATCH 3/3] Typefix --- src/utils/toolHistory.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/utils/toolHistory.ts b/src/utils/toolHistory.ts index 7bb21726..21be9571 100644 --- a/src/utils/toolHistory.ts +++ b/src/utils/toolHistory.ts @@ -11,6 +11,10 @@ export interface ToolCallRecord { duration?: number; } +interface FormattedToolCallRecord extends Omit { + timestamp: string; // formatted local time string +} + // Format timestamp in local timezone for display function formatLocalTimestamp(isoTimestamp: string): string { const date = new Date(isoTimestamp); @@ -206,7 +210,7 @@ class ToolHistory { maxResults?: number; toolName?: string; since?: string; - }): any[] { + }): FormattedToolCallRecord[] { const calls = this.getRecentCalls(options); // Format timestamps to local timezone