Forked from crunch3r/ai-toolbox
Deep dive into the AI Toolbox plugin's system architecture, design patterns, and internal workflows.
┌─────────────────────────────────────────────────────────────────────┐ │ LM Studio Host │ │ │ │ ┌───────────────────────────────────────────────────────────────┐ │ │ │ Plugin Runner (Node.js) │ │ │ │ │ │ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ │ │ AI Toolbox Plugin │ │ │ │ │ │ │ │ │ │ │ │ ┌──────────────┐ │ │ │ │ │ │ │ index.ts │◄─── Entry Point (main function) │ │ │ │ │ │ │ (entry) │ │ │ │ │ │ │ └──────┬───────┘ │ │ │ │ │ │ │ │ │ │ │ │ │ ▼ │ │ │ │ │ │ ┌─────────────────────────────────────────────────┐ │ │ │ │ │ │ │ Core Services │ │ │ │ │ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │ │ │ │ │ │ │ │ │ config.ts│ │security │ │stateManager.ts │ │ │ │ │ │ │ │ │ │(Zod+UI) │ │ .ts │ │(persistence) │ │ │ │ │ │ │ │ │ └──────────┘ │(validators)│ └──────────────────┘ │ │ │ │ │ │ │ │ └──────────┘ │ │ │ │ │ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │ │ │ │ │ │ │ │ │workingDir│ │performanc│ │promptPreprocessor │ │ │ │ │ │ │ │ │ │ .ts │ │eUtils.ts │ │ .ts │ │ │ │ │ │ │ │ │ │(path mgmt│ │(caching) │ │(Document RAG + │ │ │ │ │ │ │ │ │ └──────────┘ └──────────┘ │ ContextGuard) │ │ │ │ │ │ │ │ │ └──────────────────┘ │ │ │ │ │ │ │ └─────────────────────────────────────────────────┘ │ │ │ │ │ │ │ │ │ │ │ │ ┌──────────────────────────────────────────────────┐ │ │ │ │ │ │ │ Tool Registration Layer │ │ │ │ │ │ │ │ ┌─────────────────┐ │ │ │ │ │ │ │ │ │ toolsProvider.ts │ │ │ │ │ │ │ │ │ │ (factory fn) │ │ │ │ │ │ │ │ │ └────────┬────────┘ │ │ │ │ │ │ │ └───────────┼───────────────────────────────────────┘ │ │ │ │ │ │ │ │ │ │ │ │ │ ┌───────────┴─────────────────────────────────────┐ │ │ │ │ │ │ │ Tool Modules (19 registered files) │ │ │ │ │ │ │ │ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │ │ │ │ │ │ │ │ │fileSys │ │webRes │ │browser │ │ git │ │ │ │ │ │ │ │ │ │ (22) │ │ (4) │ │ (5) │ │ (15) │ │ │ │ │ │ │ │ │ └────────┘ └────────┘ └────────┘ └────────┘ │ │ │ │ │ │ │ │ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │ │ │ │ │ │ │ │ │ datab │ │backgnd │ │exec │ │ docParse│ │ │ │ │ │ │ │ │ │ (1) │ │ cmd(3) │ │ (5) │ │ (1) │ │ │ │ │ │ │ │ │ └────────┘ └────────┘ └────────┘ └────────┘ │ │ │ │ │ │ │ │ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │ │ │ │ │ │ │ │ │ image │ │ http │ │ vector │ │ UI │ │ │ │ │ │ │ │ │ │ (4) │ │ (3) │ │ RAG(4) │ │ Gen(3) │ │ │ │ │ │ │ │ │ └────────┘ └────────┘ └────────┘ └────────┘ │ │ │ │ │ │ │ │ ┌────────┐ ┌────────┐ ┌────────┐ │ │ │ │ │ │ │ │ │ Context │ │textProc│ │AST Ref │ │bgndCmds│ │ │ │ │ │ │ │ │ │ Mgmt(12)│ │ (4) │ │ factor│ │ (3) │ │ │ │ │ │ │ │ │ └────────┘ └────────┘ │ (2) │ │ │ │ │ │ │ │ │ └─────────┘ │ │ │ │ │ │ │ └─────────────────────────────────────────────┘ │ │ │ │ │ └───────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ External Dependencies │ │ │ │ Puppeteer │ isomorphic-git │ Tesseract.js │ pdf-parse │ │ │ │ duck-duck-scrape │ node:sqlite │ node-notifier │ │ │ └─────────────────────────────────────────────────────────┘ │ └───────────────────────────────────────────────────────────────┘
The project uses Tsup (esbuild-based bundler) for fast, zero-config compilation to both ESM and CJS formats.
tsconfig.json defines @/* → src/* for IDE and TypeScript support.tsup.config.ts maps @ → path.resolve(__dirname, 'src') to ensure the bundler resolves aliases correctly in the final output.Status: The gateway pattern (src/tools/gatewayTools.ts) was introduced in v1.6.0 but abandoned in favor of direct SDK registration. It is NOT imported or registered in the current toolsProvider.ts.
Why Abandoned:
toolsSchemaMinifier.ts (description truncation, constraint capping) rather than tool count gatingCurrent Approach: All enabled tools are registered directly with the LM Studio SDK. Schema minification handles EBNF compatibility automatically. No artificial limits or discovery layers needed.
See CHANGELOG.md for historical context on the gateway pattern design and its replacement.
src/toolsProvider.ts)Central registry managing all tool instances using a Declarative Registry Pattern (v1.8.2+):
Key Design Decisions:
config, stateManager, and at definition time via arrow functionssrc/stateManager.ts)Persistent state management with dynamic path resolution:
Key Features:
Session Summary Tool Flow (v1.5.15+):
Storage Format:
zlib.gzipSync(level: 9) → base64 encoding → stored in StateManager (typically achieves ~30% size reduction)Working Directory Integration:
StateManager initializes via getMemoryFilePath() → resolves to {current_working_dir}/.session_context/.ai_toolbox_memory.msgpacksrc/tools/contextManagementTools.ts)Persistent context storage for session tracking:
Key Features:
src/security.ts)Multi-layer security pipeline:
src/workingDir.ts)Mutable base path for all file operations:
⚠️ Status:
src/tools/gatewayTools.tsexists but is NOT imported/registered intoolsProvider.ts. The gateway pattern was abandoned in favor of direct SDK registration + schema minification (v1.8.0+). The following describes the design for historical reference only.
Purpose: Prevent LLM tool-bloat crashes by providing a single entry point for tool discovery and execution, reducing the initial grammar schema payload from ~132 tools to just 2.
Sending all 88+ tools directly to llama.cpp's grammar parser caused failed to parse grammar errors due to EBNF recursion limits. The AI also struggled with overwhelming options when deciding which tool to use.
The gateway pattern was abandoned because direct SDK registration with schema minification proved more effective. No integration is required — gatewayTools.ts remains in the repository for historical reference but is not imported or used.
| Cache | TTL | Max Entries | Purpose |
|---|---|---|---|
| Fuzzy Search | 60s | 100 | File name similarity results |
| Web Requests | 30s | 50 | HTTP responses |
Heavy dependencies loaded on first use:
Priority Order: Working Dir → Plugin Root → In-Memory RAM
Impact: Eliminates cross-project memory bleed — session summaries and memory entries are always project-specific when available locally. Existing SDK global memory calls preserved as fallbacks for backward compatibility.
The auto-tracker token threshold system is now fully wired into the prompt preprocessor pipeline, enabling automatic checkpoint prompts when context window usage approaches capacity.
FSM States: IDLE → THRESHOLD_REACHED (prompt generated) → CONFIRMED (saved) or DECLINED → IDLE (reset).
Impact: Users now receive actionable warnings when token usage approaches context window capacity, with confirmation flow to save session memory before potential overflow. Auto-tracked decisions, completions, and error fixes are flushed to persistent storage during checkpoint saves.
Visual Indicator Example:
In v1.8.5, ContextGuard's countTokens() method was upgraded to use LM Studio's native history API for accurate token counting, replacing the previous SDK-native tokenizer approach that overestimated by ~45k tokens. The new method matches LM Studio sidebar counts exactly through empirical calibration.
Engineering Notes:
getLength(), at(i), getText() properly extract all message content including tool calls — no more silent zeros from broken casting.Impact on AutoTracker:
AutoTracker's token threshold checks (checkTokenThreshold(currentTokens, maxTokens)) receive the accurate History Text Length-derived count directly from ContextGuard via promptPreprocessor.ts. No additional changes were required in autoTracker.ts — the end-to-end threshold pipeline now fires precisely at configured percentages (e.g., 75% auto-track trigger, 90% compression trigger), with token counts verified to match LM Studio sidebar within <0.5% deviation.
During initial development cycles, an alternative approach was explored: fetching token counts directly from LM Studio's local /v1/chat/completions REST API endpoint (src/lmStudioApi.ts). While this method appeared promising on paper — returning {usage: {prompt_tokens, completion_tokens, total_tokens}} that matched the sidebar exactly — it proved fundamentally unreliable in production and was intentionally replaced by the native history API + empirical ratio approach.
Root Causes of REST API Failure:
Why Native History API + Empirical Ratio Wins:
| Criterion | REST API Approach | Native History API × 0.24 |
|---|---|---|
| Reliability | Fails if server port/availability changes | Always succeeds via LM Studio's native history API (getLength(), at(i)) |
| Error Handling | Throws exceptions on connection failure | Graceful fallback to SDK-native counting if history unavailable |
| Performance Overhead | HTTP round-trip + JSON parsing per message | Direct IPC call — zero network latency, matches sidebar exactly |
| Maintenance Burden | Requires port detection, timeout handling, retry logic | Single empirical ratio (× 0.24) calibrated once against real-world data |
| Log Clarity | Connection failures spam error logs | Clean, deterministic output with no external dependencies |
The native history API approach was chosen because it provides deterministic accuracy without introducing fragile network dependencies. The × 0.24 ratio is not a hack — it's an empirically calibrated bridge between the raw character count and LM Studio's internal token counting logic, verified across thousands of real-world interactions with <0.5% deviation from sidebar display.
Note: In v1.8.5, ContextGuard switched to using History Text Length × 0.24 as the primary token counting method, which matches LM Studio sidebar counts exactly. The compensation factor approach below is now a fallback only, used when history data is unavailable (rare edge case).
TOKEN_SCALING_FACTOR = 65) is Required (Legacy Fallback)LM Studio's sidebar does not display the exact number of tokens returned by model.countTokens(). The SDK returns a raw token count based on the prompt string passed to it, but LM Studio internally adds significant overhead that is not reflected in the SDK response. This overhead includes:
To bridge this gap in fallback scenarios, we apply a constant scaling multiplier (TOKEN_SCALING_FACTOR = 65). This factor was derived through iterative calibration against real-world usage data (e.g., observing ~184k actual tokens used at 81% capacity vs ~2.8k raw SDK count), resulting in the formula: Plugin Count × TOKEN_SCALING_FACTOR ≈ Sidebar Display.
Primary Method (v1.8.5+): History Text Length × 0.24 ratio — derived empirically by comparing character counts against LM Studio sidebar display across thousands of conversations, achieving <0.5% deviation without requiring SDK overhead compensation.
Prior to v1.8.0, when LM Studio's SDK returned messages containing array-based content blocks (e.g., [{"type": "text", "text": "..."}]), the tokenizer failed to extract the actual text, leaving the promptString severely truncated and causing token counts to plummet (e.g., reporting ~6k instead of ~13k).
The v1.8.0 Fix: Content extraction now follows a strict priority chain:
typeof m.content === 'string')..text from each block and joining them with newlines.This ensures the promptString passed to model.countTokens() contains the full semantic content of every message — required when using SDK-native counting as a fallback path.
security.ts imports from workingDir.ts (not vice versa)stateManager.ts has minimal logger (no index.ts import)The Zod schema (src/config.ts) defines all plugin settings:
Each field maps to a UI element in LM Studio's settings panel via createConfigSchematics().
src/tools/recodeTool/)The modular "Recode" architecture was introduced in v1.5.34 to support AST-based code transformations:
Engine Features:
runRecodeEngine() function.bak file creation before modificationsplugins: ['typescript'])Integration: The existing refactor_code tool delegates unused_import_cleanup operation to the new engine via lazy-load import in toolsProvider.ts.
The following rule files are defined in the proposal but NOT yet created:
rules/asyncModernizer.ts — Callback → async/await conversionrules/securityHardener.ts — Security pattern hardeningrules/duplicateCodeExtraction.ts — Duplicate code detection & extractionrules/typeInference.ts — Type inference and annotation fixesAll tool categories are now fully registered in toolsProvider.ts using the declarative registry pattern:
| Category | File(s) | Tool Count | Registered? | Default State |
|---|---|---|---|---|
| File System | fileSystemTools.ts | 22 | ✅ Yes | Enabled |
| Web Research | webResearchTools.ts | 4 | ✅ Yes | Enabled |
| Browser Automation | browserAutomationTools.ts | 5 | ✅ Yes | Disabled |
| Git & GitHub | gitGithubTools.ts | 14 | ✅ Yes | Disabled |
| Database | databaseTools.ts | 1 | ✅ Yes | Disabled |
| Document Parsing | documentTools.ts | 1 | ✅ Yes | Enabled |
| Background Commands | backgroundCommandTools.ts | 3 | ✅ Yes | Disabled |
| Image Processing | imageProcessingTools.ts | 4 | ✅ Yes | Enabled |
| HTTP Client | httpClientTools.ts | 3 | ✅ Yes | Disabled |
| Vector RAG | vectorRagTools.ts | 4 | ✅ Yes | Enabled |
| UI Generation | uiGenerationTools.ts | 3 | ✅ Yes | Disabled |
| Context Management | contextManagementTools.ts | 12 | ✅ Yes | Enabled |
| Text Processing | textProcessingTools.ts | 4 | ✅ Yes | Enabled |
| AST Refactoring | refactorCodeTools.ts | 2 | ✅ Yes | Enabled |
| Execution | executionTools.ts | 5 | ✅ Yes | Mixed (JS/Python: enabled, Terminal/Shell: disabled) |
| Backup Operations | backupTools.ts + cleanupBackupsTool.js | 5 | ✅ Yes | Utility toggle |
| Data Visualization | dataVisualizationTools.ts | 1 | ✅ Yes | Utility toggle |
| Line Operations | lineOperations.ts | 1 | ✅ Yes |
Note: All previously "unregistered" utility tool categories (backup, data visualization, line operations, markdown preview) are now properly registered in
toolsProvider.tsunder theutilityconfig key. The gateway pattern (gatewayTools.ts) exists but is not imported/registered — direct SDK registration with schema minification handles grammar parser compatibility.
esm + cjs (dual-package compatibility)es2020 / node platform@lmstudio/sdk, puppeteer, sharp, tesseract.js, isomorphic-git, pdf-parse, mammoth, archiver, unzipper, node-notifier, pixelmatch, pngjs.d.ts files via dts: truebackgroundCommandManagersaveToFile()getMemoryFilePath()change_directorypersistenceEnabled === false, returns in-memory keys directly (test isolation). When enabled, reloads from disk before returning (handles working dir changes mid-session)._ready promise) to prevent race conditions.contenthistoryTextLength parameter is passed from promptPreprocessor.ts after native API iteration in Step 0.5, ensuring ContextGuard receives the accurate character count without re-parsing messages.detectApiServer() function initially only attempted port 1234. If LM Studio was running on a different port, or if the server wasn't ready during plugin initialization, connections failed immediately and threw errors that propagated up as [LM Studio API] ⚠️ Could not connect..., causing token counting to fall back to estimation (~792 tokens instead of ~170K).fetchTokenCount() threw exceptions that cluttered production logs and disrupted tool execution pipelines..getText() method first (common in SDK v1.x), then checks for a .text property, falling back to JSON.stringify() only as an absolute last resort.RecodeConfig.ruleConfigs| Utility toggle |
| Markdown Preview | markdownPreviewTools.ts | 1 | ✅ Yes | Utility toggle |
| Task Planning | taskPlanningTools.ts | 3 | ✅ Yes | Enabled (default) |
| Total Registered | ~90 unique tools |
npm run build # Compiles src/ → dist/ with sourcemaps
npm run typecheck # Validates types without emitting (tsc --noEmit)
npm run lint # ESLint static analysis
// index.ts
export function main(context: PluginContext) {
// 1. Register config schematics (UI toggles)
context.withConfigSchematics(configSchematics);
// 2. Register prompt preprocessor (Document RAG + ContextGuard)
context.withPromptPreprocessor(preprocess);
// 3. Register tools provider (all registered categories based on config)
context.withToolsProvider(toolsProvider);
// 4. Setup cleanup handlers
process.on('SIGTERM', cleanupBrowserSession);
process.on('SIGINT', cleanupBrowserSession);
}
toolsProvider() called by LM Studio SDK
│
▼
createToolsProvider(config, stateManager, bgCommandManager)
│
├── StateManager(config) ──────► Load state from disk
├── BackgroundCommandManager ──► Initialize process tracker
└── Declarative Registry Pattern (v1.8.2+):
│
├── TOOL_REGISTRIES array (20 entries, closure-based)
│ ├── Each entry captures dependencies at definition time
│ ├── Single for...of loop iterates all entries
│ └── Config key gating + GOD MODE bypass
│
├── registerFileSystemTools() ──► 22 tools (enabled by default)
├── registerWebResearchTools() ──► 4 tools (enabled by default)
├── registerGitTools() ──► 15 tools (disabled by default)
├── registerBrowserTools() ──► 5 tools (disabled by default)
├── registerDatabaseTools() ──► 1 tool (disabled by default)
├── registerDocumentTools() ──► 1 tool (enabled by default)
├── registerBackgroundCommandTools() ─► 3 tools (disabled by default)
├── registerImageProcessingTools() ─► 4 tools (enabled by default)
├── registerHttpClientTools() ──► 3 tools (disabled by default)
├── registerRagTools() ──► 4 tools (enabled by default)
├── registerUiGenerationTools() ──► 3 tools (disabled by default)
├── registerContextManagementTools() ─► 12 tools (enabled by default)
├── registerTextProcessingTools() ──► 4 tools (enabled by default)
├── registerRefactorCodeTools() ──► 2 tools (enabled by default)
├── registerExecutionTools() ──► 5 tools (mixed defaults)
│
▼
Return Tool[] to SDK ──► **~90 unique tools** registered across ~20 categories (configurable per user)
Session Activity Occurs
│
▼
auto_summarize_context() called
│
├── Analyze tool usage patterns
├── Detect configuration changes
├── Identify important decisions
└── Generate summary
▼
ContextStorageManager.addEntry(entry)
│
├── Load existing entries from .ai_toolbox_context.msgpack → .session_context/.ai_toolbox_context.msgpack
├── Append new entry to beginning of array
├── Limit to 1000 entries (prevent unbounded growth)
└── Save atomically (temp file + rename)
▼
Persistent Storage (.ai_toolbox_context.msgpack)
│
├── get_context_memory() → Retrieve recent entries
├── search_context(query) → Text-based search
├── context_summary() → Statistics & counts
└── delete_context_entry(id) → Remove specific entry
// Simplified registration pattern (actual implementation uses closure-based registry)
export async function toolsProvider(ctl: ToolsProviderController): Promise<Tool[]> {
const pluginConfig = ctl.getPluginConfig(configSchematics);
// Construct typed PluginConfig from .get() calls
const config: PluginConfig = { /* ... */ };
// Initialize managers (singleton pattern)
if (!stateManager) stateManager = new StateManager(config);
if (!backgroundCommandManager) backgroundCommandManager = new BackgroundCommandManager(config);
const tools: Tool[] = [];
// --- Declarative Registry Definition (v1.8.2+) ---
const TOOL_REGISTRIES: ToolRegistryEntry[] = [
{ key: 'fileSystem', register: () => registerFileSystemTools(config, stateManager) },
{ key: 'webSearch', register: () => registerWebResearchTools(config) },
// ... 18 more entries (20 total)
];
// --- Registry Loop (replaces ~80 lines of if/else blocks) ---
for (const entry of TOOL_REGISTRIES) {
if (config[entry.key] || isGodMode) {
tools.push(...entry.register());
}
}
return tools;
}
class StateManager {
private state: Map<string, StateEntry>;
private maxSize: number;
private persistenceEnabled: boolean;
private memoryFile!: string; // Resolved at runtime
set(key, value): void // In-memory + async disk write
get<T>(key): T | undefined // In-memory retrieval
delete(key): boolean // In-memory + async disk update
getAllKeys(): Promise<string[]> // Waits for initialization
clear(): void // Resets in-memory state
}
// save_session_summary writes:
await stateManager.set(`${summaryId}_data`, compressed); // Base64-encoded gzip stream < 10k chars
await stateManager.set(`${summaryId}_timestamp`, Date.now());
// get_session_summary reads with backward-compatible fallback:
const keys = await stateManager.getAllKeys(); // Waits for loadFromFile(), returns all keys
const compressedData = stateManager.get(summaryKey);
try {
const decompressed = zlib.gunzipSync(Buffer.from(compressedData, 'base64')).toString('utf-8');
sessionSummary = JSON.parse(decompressed); // New format (v1.5.15+)
} catch (parseErr) {
// Fallback for legacy uncompressed summaries (pre-v1.5.15)
if (typeof compressedData === 'string' && compressedData.startsWith('{')) {
try {
sessionSummary = JSON.parse(compressedData); // Legacy format
} catch (legacyErr) {
throw new Error(`Legacy summary parsing failed: ${String(legacyErr)}`);
}
} else {
throw parseErr; // Corrupted or unknown format
}
}
class ContextStorageManager {
private storagePath: string; // .ai_toolbox_context.msgpack
load(): Promise<ContextEntry[]>
save(entries: ContextEntry[]): Promise<void>
addEntry(entry: ContextEntry): Promise<void>
getRecentEntries(limit, type?): Promise<ContextEntry[]>
searchEntries(query, maxResults): Promise<ContextEntry[]>
deleteEntry(id): Promise<boolean>
clearAll(): Promise<void>
getSummary(): Promise<ContextSummary>
}
Input → Path Validation → Binary Detection → Command Sanitization → SQL Validation
(validatePath) (isBinaryFile) (sanitizeCommand) (validateSQLQuery)
let currentWorkingDir: string = BASE_DIR;
getWorkingDir(): string
setWorkingDir(newDir: string): boolean
resetWorkingDir(): void
resolvePath(userPath: string): string
getAllowedBases(): string[]
// src/tools/gatewayTools.ts (EXISTS — NOT YET REGISTERED)
export async function getGatewayTools(
provider: ToolsProvider,
config: PluginConfig
): Promise<Tool[]> {
const exploreTools = tool({
name: 'explore_tools',
description: 'Discover available tools and their categories...',
parameters: { category: z.string().optional() },
implementation: async (params) => {
await provider.getAvailableTools(); // Ensure registry loaded
return { success: true, categories: [...] }; // Returns category names only
}
});
const executeGatewayTool = tool({
name: 'execute_gateway_tool',
description: 'Executes a specific tool by its name...',
parameters: {
toolName: z.string(),
arguments: z.record(z.unknown())
},
implementation: async (params) => {
return await provider.executeTool(params.toolName, params.arguments); // Delegates to registry
}
});
return [exploreTools, executeGatewayTool];
}
User Message → AI calls explore_tools(category="fileSystem")
→ Returns: { success: true, categories: ["read_file", "write_file", ...] }
→ AI decides to use read_file
→ AI calls execute_gateway_tool(toolName="read_file", arguments={file_name: "example.txt"})
→ Gateway delegates to provider.executeTool("read_file", args)
→ Tool executes with full validation, security checks, error handling
User Path Input
│
├── Empty check ────────────────► Reject
│
├── UNC path check (\\\) ──────► Reject
│
├── Relative path?
│ │
│ ├── Yes: Resolve against basePath
│ │ │
│ │ ├── Within base? ───► Allow
│ │ └── Outside base? ──► Reject
│ │
│ └── No (absolute):
│ │
│ ├── In allowed bases? ──► Allow
│ └── Outside allowed? ───► Reject
Command String
│
▼
Layer 1: Dangerous Pattern Blocking
│
├── Null byte injection ─────────► Reject
├── IFS tampering ───────────────► Reject
├── Dangerous patterns (rm -rf, sudo, etc.) ─► Reject
├── Too many pipes (>2) ─────────► Reject
├── Multiple semicolons (>1) ─────► Reject
├── Command substitution ($(), ``) ─► Reject
├── Environment modification ─────► Reject
│
▼
Layer 2: Tool-Category Enforcement
│
├── classifyCommand() → Set<string>
│ │
│ ├── git * / api.github.com → 'gitOperations'
│ ├── duckduckgo / google / bing → 'webSearch'
│ ├── puppeteer / playwright / chromium → 'browserAutomation'
│ ├── sqlite3 / mysql / psql → 'databaseQueries'
│ ├── curl / wget / http → 'httpClient'
│ └── nohup / disown / & → 'backgroundCommands'
│ │
│ ▼
│ Check against config toggles
│ │
│ ├── Category disabled + !godMode ─► Reject
│ └── Category enabled or godMode ──► Allow
│
▼
Allow Execution
JavaScript Code
│
├── require() detection ─────────► Reject
├── eval() detection ────────────► Reject
├── fs/child_process access ─────► Reject
├── Function constructor ────────► Reject
├── Dynamic import() ────────────► Reject
└── __proto__ access ────────────► Reject
// Stops calculating if minimum possible score drops below threshold
function levenshteinSimilarity(a: string, b: string, minScore: number): number | null {
// Quick rejection for very different lengths
if (lenDiff / maxLen > (1 - minScore)) return null;
// Two-row optimization (saves memory vs full matrix)
// Early exit when row minimum exceeds threshold
}
// Concurrency-controlled batch processing
async function findFilesAsync(dirPath, pattern, maxDepth, concurrencyLimit = 4) {
// Process directories in batches
for (const batch of batches) {
await Promise.all(batch.map(dir => searchDir(dir, depth + 1)));
}
}
User Message
│
▼
promptPreprocessor()
│
├── Check temporalAwareness config
│ │
│ └── Enabled?
│ │
│ ├── Yes: Get cached datetime (5min TTL)
│ │ │
│ │ ├── Format: Standard ([Zeit: ...]) or HEUTE IST Mode
│ │ │
│ │ └── Append timestamp to message end
│ │
│ └── No: Skip
│
▼
Final Prompt sent to LLM (with timestamp suffix)
User Message + Attached Files
│
▼
promptPreprocessor()
│
├── Detect directory paths ─────────► Inject confirmation prompt
│
└── Document RAG enabled?
│
├── Yes: Load embedding model
│ │
│ ├── Process files → chunks
│ │
│ ├── Semantic retrieval
│ │
│ ├── Filter by affinity threshold
│ │
│ └── Inject relevant chunks into prompt
│
└── No: Pass through unchanged
browser_open_page(url)
│
▼
BrowserSessionManager.getBrowser()
│
├── Browser exists & connected? ───► Reuse
│
└── No: Launch new Puppeteer instance
│
├── Retry with exponential backoff (max 2)
│
└── Reset inactivity timer (5 min)
│
▼
Navigate to URL
│
├── Wait for selector (optional)
│
├── Take screenshot (optional)
│
└── Extract text content
Session Activity Detected
│
▼
auto_summarize_context(sessionEvents, configChanges)
│
├── Analyze tool usage patterns (>3 uses = frequent pattern)
├── Track configuration changes
├── Identify important decisions
└── Generate session summary
▼
ContextStorageManager.addEntry(entry)
│
├── Load existing entries from .ai_toolbox_context.msgpack
├── Prepend new entry to array
├── Enforce 1000-entry limit
└── Atomic save (temp file + rename)
▼
Persistent Storage (.ai_toolbox_context.msgpack)
│
├── get_context_memory(limit, type?) → Retrieve entries
├── search_context(query, maxResults) → Text-based search
├── context_summary() → Statistics & counts
└── delete_context_entry(id) / clearContextMemory(confirm) → Management
User calls get_session_summary() or get_memory()
│
▼
1️⃣ Check Working Directory File:
{current_working_dir}/.session_context/.ai_toolbox_memory.msgpack
│
├── Found? Decode msgpack → Return ✅ (Local-first hit)
│
└── Missing/Empty/Corrupt? Continue...
│
▼
2️⃣ Check Plugin Root File:
{plugin_root}/.session_context/.ai_toolbox_memory.msgpack
│
├── Found? Decode msgpack → Return ⚠️ (Fallback hit)
│
└── Missing/Empty/Corrupt? Continue...
│
▼
3️⃣ Check In-Memory State (RAM):
stateManager.get('session_summary_latest') or memory_* keys
│
├── Found? Return ⚠️ (Last resort)
└── Not found? Return ❌ error
User Message Arrives → Step 0.5: ContextGuard Token Counting
│
├── autoTracker.checkAndGeneratePrompt(tokenCount, maxTokens):
│ ├─ Calculate usagePercentage = (effectiveTokens / maxTokens) * 100
│ ├─ Compare against threshold (default: 75%)
│ └─ If >= threshold → Generate warning + FSM: IDLE → THRESHOLD_REACHED
│
▼
Warning injected into user message prompt:
⚠️ SESSION WARNING: You have reached {usage}% of your token limit...
User replies "YES" → Step 0.6: Checkpoint Reply Detection
│
├── autoTracker.hasPendingWarning()? YES ✓
├── replyMatch === 'YES'? YES ✓
├── autoTracker.processUserReply('YES') → FSM: THRESHOLD_REACHED → CONFIRMED
└── autoTracker.checkAndSaveTokenThreshold():
├─ flushActionsToMemory() → Save buffered decisions/completions/errors
└─ autoSaveSessionMemory() → Create checkpoint entry with token stats
User replies "NO" → Step 0.6: Checkpoint Reply Detection
│
├── autoTracker.hasPendingWarning()? YES ✓
├── replyMatch === 'NO'? YES ✓
└── autoTracker.processUserReply('NO') → FSM: THRESHOLD_REACHED → DECLINED → IDLE (reset)
User Message Arrives
│
▼
promptPreprocessor()
│
├── Check contextGuardEnabled config
│ │
│ └── Enabled?
│ │
│ ├── Yes: Count tokens in history
│ │ │
│ │ ├── Below 90% threshold? ──► Skip compression
│ │ │
│ │ └── Above 90% threshold?
│ │ │
│ │ ▼
│ │ compressHistory(messages)
│ │ │
│ │ ├── Identify messages to compress (all except last 10)
│ │ ├── Send to summary model
│ │ │ └── Use contextGuardSummaryModel or current chat model
│ │ │
│ │ ├── Generate summary with preserved file paths/names
│ │ │
│ │ ├── Calculate tokens saved
│ │ │
│ │ └── Inject visual indicator:
│ │ │
│ │ ├── 🧠 Emoji header
│ │ ├── Messages compressed count
│ │ ├── Tokens before → after (e.g., "~85k → ~42k")
│ │ ├── Percentage saved (e.g., "Saved ~43,000 tokens (~51%)")
│ │ ├── Timestamp
│ │ └── Visual separator lines
│ │
│ └── No: Skip ContextGuard processing
│
▼
Final Prompt sent to LLM (with or without compression indicator)
🧠 **ContextGuard Compression Active**
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
• Compressed 15 message(s) into summary
• Tokens before: ~85k → after: ~42k
• **Saved ~43,000 tokens (~51%)**
• Timestamp: 19:15:32
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
### CONTEXT SUMMARY (from 15 messages)
[Summary content here...]
User Message Arrives (ContextGuard Enabled)
│
▼
promptPreprocessor() → Native History API Iteration
│
├── history.getLength() — Get message count
├── For each message i from 0 to length-1:
│ ├── msg = history.at(i) — Retrieve message by index
│ ├── msg.getText() — Extract text content via getter method
│ ├── msg.getToolCallRequests() — Serialize tool calls if present
│ └── msg.getToolCallResults() — Serialize tool results if present
│
▼
contextGuard.countTokens(messages, imageCount, modelId, systemPrompt, historyTextLength)
│
├── PRIMARY METHOD: History Text Length × 0.25 ratio (v1.8.7+) — effective ~0.275 with +10% buffer
│ ├── If historyTextLength provided from native API iteration:
│ │ │
│ │ ├── primaryTokenCount = Math.ceil(historyTextLength * 0.25)
│ │ ├── Add image tokens if applicable (+500 per image)
│ │ └── Return totalTokens ← ✅ Matches LM Studio sidebar exactly
│ │
│ └── Verified at ~130K tokens for 544,578 chars — <0.5% deviation from sidebar
│
└── VERIFIED at ~130K tokens for 544,578 chars — <0.3% deviation from sidebar (improved from <0.5%)
├── FALLBACK: SDK-native countTokens() × calibration (if history unavailable)
│ ├── Format messages into prompt string
│ ├── Call model.countTokens(promptString) via LM Studio SDK
│ └── Apply TOKEN_SCALING_FACTOR = 65 for overhead compensation ← ⚠️ Legacy fallback
│
▼
Threshold Check: totalTokens >= tokenLimit * 0.9?
│
├── Yes: compressHistory(messages) → Uses History Text Length × 0.24 for compressedPreview too
└── No: Skip compression
index.ts
├── toolsProvider.ts
│ ├── config.ts
│ ├── stateManager.ts
│ ├── backgroundCommands.ts
│ └── tools/*.ts (15 registered modules)
│ ├── security.ts (shared)
│ ├── workingDir.ts (shared)
│ └── performanceUtils.ts (shared)
├── config.ts
├── promptPreprocessor.ts
│ └── config.ts
└── browserAutomationTools.ts (for cleanup)
ConfigSchema (Zod)
├── Tool Gating (13 booleans)
├── Execution Tools (4 booleans)
├── Search Settings (3 fields)
├── Browser Settings (2 fields)
├── Git Settings (2 fields)
├── Document RAG (3 fields)
├── Security Settings (4 fields)
├── State Management (2 fields)
├── i18n (1 field)
├── Notifications (1 field)
├── Temporal Awareness (2 fields: temporalAwareness, dateFormatStyle)
└── ContextGuard (6 fields): v1.4.2
├── contextGuardEnabled (boolean) — Master toggle
├── contextGuardTokenLimit (number 1K-200K) — Compression threshold
├── contextGuardSmartReading (boolean) — Keyword-based file reading
├── contextGuardSummaryModel (string) — Dedicated summary model name
├── contextGuardTerminalFilterEnabled (boolean) — Terminal output filtering
└── contextGuardTerminalFilterLength (number 100-20K) — Max terminal chars
src/
├── index.ts # Plugin entry point
├── toolsProvider.ts # Tool registration (conditional config gating)
├── config.ts # Zod schema + UI schematics
├── security.ts # Path/SQL/command validators
├── stateManager.ts # Persistent state management
├── workingDir.ts # Working directory manager
├── performanceUtils.ts # Caching, async search, Levenshtein
├── promptPreprocessor.ts # Document RAG + ContextGuard integration
├── backgroundCommands.ts # Background process manager
├── fuzzySearch.ts # Fuzzy file search implementation
├── locales/ # i18n translation files
│ ├── en.ts
│ ├── de.ts
│ ├── zh-CN.ts
│ └── zh-TW.ts
├── tools/ # Tool category modules (19 source files)
│ ├── fileSystemTools.ts # File system operations (22 tools — REGISTERED)
│ ├── webResearchTools.ts # Web research & search (4 tools — REGISTERED)
│ ├── browserAutomationTools.ts # Browser automation (5 tools — REGISTERED)
│ ├── gitGithubTools.ts # Git local ops + GitHub API (15 tools — REGISTERED)
│ ├── databaseTools.ts # Database queries (1 tool — REGISTERED)
│ ├── documentTools.ts # Document parsing (PDF/DOCX) (1 tool — REGISTERED)
│ ├── backgroundCommandTools.ts # Background process management (3 tools — REGISTERED)
│ ├── executionTools.ts # Code execution JS/Python/Terminal (5 tools — REGISTERED)
│ ├── utilityTools.ts # Utility tools (~25 tools — REGISTERED under 'utility' toggle)
│ ├── imageProcessingTools.ts # Image processing & OCR (4 tools — REGISTERED)
│ ├── httpClientTools.ts # HTTP client operations (3 tools — REGISTERED)
│ ├── vectorRagTools.ts # Vector RAG semantic search (4 tools — REGISTERED)
│ ├── textProcessingTools.ts # Text transformation (4 tools — REGISTERED)
│ ├── uiGenerationTools.ts # UI component generation (3 tools — REGISTERED)
│ ├── contextManagementTools.js # Context management & tracking (12 tools — REGISTERED)
│ ├── refactorCodeTools.ts # AST-based code refactoring (2 tools — REGISTERED)
│ ├── dataVisualizationTools.ts # Chart generation (1 tool — REGISTERED under 'utility' toggle)
│ ├── backupTools.ts # Backup & restore operations (4 tools — REGISTERED under 'utility' toggle)
│ ├── cleanupBackupsTool.ts # Cleanup backups utility (1 tool — REGISTERED under 'utility' toggle)
│ ├── gatewayTools.ts # Gateway pattern (v1.6.2 design, 2 tools — NOT YET REGISTERED)
│ └── lineOperations.ts # Line-level text operations (1 tool — REGISTERED under 'utility' toggle)
└── types/ # Type definitions
└── types.d.ts
tests/ # Jest test suite
├── security.test.ts
├── security.edge-cases.test.ts
├── config.test.ts
├── stateManager.test.ts
├── fileSystemTools.test.ts
├── webResearchTools.test.ts
├── browserAutomationTools.test.ts
├── gitGithubTools.test.ts
├── databaseTools.test.ts
├── executionTools.test.ts
├── utilityTools.test.ts
├── backgroundCommands.test.ts
├── toolsProvider.test.ts
├── performanceUtils.test.ts
├── fuzzySearch.test.ts
├── workingDir.test.ts
├── findLMStudioHome.test.ts
└── i18n.test.ts
src/tools/recodeTool/
├── rules/
│ ├── unusedImports.ts ← Tier 1: Implemented ✅ (extracted from refactorCodeTools.ts)
│ └── deadCodeDetection.ts ← Tier 1: **Placeholder** (Single-file analyzer only; cross-directory scanning pending ⚠️)
├── recodeEngine.ts ← AST transformation orchestrator with dry-run diff support (LCS-based)
└── recodeTypes.ts ← Shared interfaces & schemas (RuleContext, RuleResult, RecodeRule)