Skip to content
Closed
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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ reports/
GEMINI.md
package-lock.json
/.claude
coverage/
coverage/
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"dev:code": "bun run profile:code && bun run dev:profile",
"dev:grpc": "bun run scripts/start-grpc.ts",
"dev:grpc:cli": "bun run scripts/grpc-cli.ts",
"dev:router": "bun run scripts/smart-router.ts",
"start": "node dist/cli.mjs",
"test": "bun test",
"test:coverage": "bun test --coverage --coverage-reporter=lcov --coverage-dir=coverage --max-concurrency=1 && bun run scripts/render-coverage-heatmap.ts",
Expand Down Expand Up @@ -159,4 +160,4 @@
"overrides": {
"lodash-es": "4.18.1"
}
}
}
147 changes: 147 additions & 0 deletions scripts/smart-router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
#!/usr/bin/env bun
/**
* Smart Router Launcher
* ---------------------
* Spawns the Python smart router as a subprocess and bridges
* communication between Claude Code terminal and the router.
*
* Usage:
* bun run scripts/smart-router.ts
* bun run scripts/smart-router.ts --strategy=latency
* bun run scripts/smart-router.ts --port=8080
*
* Environment:
* SMART_ROUTER_STRATEGY=latency|cost|balanced
* SMART_ROUTER_PORT=8080
* SMART_ROUTER_FALLBACK=true
*/

import { spawn } from 'child_process'
import { resolve } from 'path'
import { fileURLToPath } from 'url'

const __dirname = fileURLToPath(new URL('.', import.meta.url))

interface RouterConfig {
strategy: 'latency' | 'cost' | 'balanced'
port: number
fallback: boolean
verbose: boolean
}

function parseArgs(): RouterConfig {
const args = process.argv.slice(2)
const config: RouterConfig = {
strategy: (process.env.SMART_ROUTER_STRATEGY as RouterConfig['strategy']) || 'balanced',
port: parseInt(process.env.SMART_ROUTER_PORT || '8080', 10),
fallback: process.env.SMART_ROUTER_FALLBACK !== 'false',
verbose: args.includes('--verbose') || args.includes('-v'),
}

for (const arg of args) {
if (arg.startsWith('--strategy=')) {
config.strategy = arg.split('=')[1] as RouterConfig['strategy']
}
if (arg.startsWith('--port=')) {
config.port = parseInt(arg.split('=')[1], 10)
}
if (arg === '--fallback' || arg === '-f') {
config.fallback = true
}
}

return config
}

async function main() {
const config = parseArgs()

console.log('🚀 Starting Smart Router...')
console.log(` Strategy: ${config.strategy}`)
console.log(` Port: ${config.port}`)
console.log(` Fallback: ${config.fallback}`)

const pythonScript = resolve(__dirname, '../python/smart_router.py')

// Set environment for Python router
const env = {
...process.env,
ROUTER_MODE: 'smart',
ROUTER_STRATEGY: config.strategy,
ROUTER_FALLBACK: config.fallback ? 'true' : 'false',
ROUTER_PORT: config.port.toString(),
PYTHONUNBUFFERED: '1',
}

// Spawn Python smart router
const pythonProcess = spawn('python', ['-u', pythonScript], {
env,
stdio: ['pipe', 'pipe', 'pipe'],
})

let ready = false

pythonProcess.stdout.on('data', (data: Buffer) => {
const lines = data.toString().trim().split('\n')
for (const line of lines) {
if (config.verbose || line.includes('ERROR') || line.includes('WARN')) {
console.log(`[SmartRouter] ${line}`)
}
if (line.includes('Router ready') || line.includes('Listening on port')) {
ready = true
console.log('✅ Smart Router is ready')
console.log(` API: http://localhost:${config.port}/route`)
console.log(` Health: http://localhost:${config.port}/health`)
}
}
})

pythonProcess.stderr.on('data', (data: Buffer) => {
console.error(`[SmartRouter Error] ${data.toString().trim()}`)
})

pythonProcess.on('close', (code) => {
console.log(`Smart Router exited with code ${code}`)
process.exit(code || 0)
})

pythonProcess.on('error', (err) => {
console.error('Failed to start Smart Router:', err)
process.exit(1)
})

// Graceful shutdown
process.on('SIGINT', () => {
console.log('\n🛑 Shutting down Smart Router...')
pythonProcess.kill('SIGTERM')
})

process.on('SIGTERM', () => {
pythonProcess.kill('SIGTERM')
})

// Wait for router to be ready
await new Promise<void>((resolve, reject) => {
const timeout = setTimeout(() => {
if (!ready) {
reject(new Error('Smart Router failed to start within 30s'))
}
}, 30000)

const checkReady = setInterval(() => {
if (ready) {
clearTimeout(timeout)
clearInterval(checkReady)
resolve()
}
}, 100)
})

// Keep process alive
await new Promise(() => {})
}

main().catch((err) => {
console.error('Fatal error:', err)
process.exit(1)
})
39 changes: 39 additions & 0 deletions src/commands/auto-fix.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { expect, test } from 'bun:test'

import command from './auto-fix.js'
import type { PromptCommand } from '../types/command.js'

test('auto-fix command has correct metadata', () => {
expect(command.name).toBe('auto-fix')
expect(command.type).toBe('prompt')
expect(command.isEnabled()).toBe(true)
expect(command.description).toContain('auto-fix')
if (command.type === 'prompt') {
expect(command.source).toBe('builtin')
}
})

test('auto-fix getPromptForCommand returns configuration prompt', async () => {
if (command.type !== 'prompt') {
throw new Error('Expected prompt command')
}

const prompt = await command.getPromptForCommand('', {} as any)

expect(Array.isArray(prompt)).toBe(true)
expect(prompt.length).toBeGreaterThan(0)
const firstBlock = prompt[0]
if (firstBlock.type === 'text') {
expect(firstBlock.text).toContain('auto-fix')
expect(firstBlock.text).toContain('lint')
expect(firstBlock.text).toContain('test')
expect(firstBlock.text).toContain('settings.json')
}
})

test('auto-fix progress message is set', () => {
if (command.type !== 'prompt') {
throw new Error('Expected prompt command')
}
expect(command.progressMessage).toBe('Configuring auto-fix...')
})
2 changes: 1 addition & 1 deletion src/commands/clear/caches.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { resetPromptCacheBreakDetection } from '../../services/api/promptCacheBr
import { clearAllSessions } from '../../services/api/sessionIngress.js'
import { runPostCompactCleanup } from '../../services/compact/postCompactCleanup.js'
import { resetAllLSPDiagnosticState } from '../../services/lsp/LSPDiagnosticRegistry.js'
import { clearTrackedMagicDocs } from '../../services/MagicDocs/magicDocs.js'
import { clearTrackedMagicDocs } from '../../services/magicDocs/magicDocs.js'
import { clearDynamicSkills } from '../../skills/loadSkillsDir.js'
import { resetSentSkillNames } from '../../utils/attachments.js'
import { clearCommandPrefixCaches } from '../../utils/bash/commands.js'
Expand Down
2 changes: 1 addition & 1 deletion src/commands/compact/compact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { suppressCompactWarning } from '../../services/compact/compactWarningSta
import { microcompactMessages } from '../../services/compact/microCompact.js'
import { runPostCompactCleanup } from '../../services/compact/postCompactCleanup.js'
import { trySessionMemoryCompaction } from '../../services/compact/sessionMemoryCompact.js'
import { setLastSummarizedMessageId } from '../../services/SessionMemory/sessionMemoryUtils.js'
import { setLastSummarizedMessageId } from '../../services/sessionMemory/sessionMemoryUtils.js'
import type { ToolUseContext } from '../../Tool.js'
import type { LocalCommandCall } from '../../types/command.js'
import type { Message } from '../../types/message.js'
Expand Down
61 changes: 61 additions & 0 deletions src/commands/mcp/mcp.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { expect, test } from 'bun:test'
import React from 'react'

import { call } from './mcp.js'
import type { LocalJSXCommandOnDone } from '../../types/command.js'

test('call returns MCPSettings component with no args', async () => {
const onDone: LocalJSXCommandOnDone = () => { }
const result = await call(onDone, null)

expect(result).not.toBeNull()
expect(React.isValidElement(result)).toBe(true)
})

test('call returns MCPSettings with no-redirect arg', async () => {
const onDone: LocalJSXCommandOnDone = () => { }
const result = await call(onDone, null, 'no-redirect')

expect(result).not.toBeNull()
expect(React.isValidElement(result)).toBe(true)
})

test('call returns MCPReconnect with reconnect arg', async () => {
const onDone: LocalJSXCommandOnDone = () => { }
const result = await call(onDone, null, 'reconnect test-server')

expect(result).not.toBeNull()
expect(React.isValidElement(result)).toBe(true)
})

test('call returns MCPToggle with enable arg', async () => {
const onDone: LocalJSXCommandOnDone = () => { }
const result = await call(onDone, null, 'enable')

expect(result).not.toBeNull()
expect(React.isValidElement(result)).toBe(true)
})

test('call returns MCPToggle with disable arg', async () => {
const onDone: LocalJSXCommandOnDone = () => { }
const result = await call(onDone, null, 'disable')

expect(result).not.toBeNull()
expect(React.isValidElement(result)).toBe(true)
})

test('call returns MCPToggle with enable and specific server', async () => {
const onDone: LocalJSXCommandOnDone = () => { }
const result = await call(onDone, null, 'enable filesystem')

expect(result).not.toBeNull()
expect(React.isValidElement(result)).toBe(true)
})

test('call returns MCPToggle with disable and specific server', async () => {
const onDone: LocalJSXCommandOnDone = () => { }
const result = await call(onDone, null, 'disable filesystem')

expect(result).not.toBeNull()
expect(React.isValidElement(result)).toBe(true)
})
13 changes: 5 additions & 8 deletions src/commands/model/model.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,23 @@
import { c as _c } from "react-compiler-runtime";
import chalk from 'chalk';
import * as React from 'react';
import { c as _c } from "react-compiler-runtime";
import type { CommandResultDisplay } from '../../commands.js';
import { ModelPicker } from '../../components/ModelPicker.js';
import { COMMON_HELP_ARGS, COMMON_INFO_ARGS } from '../../constants/xml.js';
import { fetchBootstrapData } from '../../services/api/bootstrap.js';
import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../services/analytics/index.js';
import { getAdditionalModelOptionsCacheScope } from '../../services/api/providerConfig.js';
import { useAppState, useSetAppState } from '../../state/AppState.js';
import type { LocalJSXCommandCall } from '../../types/command.js';
import type { EffortLevel } from '../../utils/effort.js';
import { isBilledAsExtraUsage } from '../../utils/extraUsage.js';
import { clearFastModeCooldown, isFastModeAvailable, isFastModeEnabled, isFastModeSupportedByModel } from '../../utils/fastMode.js';
import { MODEL_ALIASES } from '../../utils/model/aliases.js';
import { checkOpus1mAccess, checkSonnet1mAccess } from '../../utils/model/check1mAccess.js';
import type { ModelOption } from '../../utils/model/modelOptions.js';
import { discoverOpenAICompatibleModelOptions } from '../../utils/model/openaiModelDiscovery.js';
import { getAPIProvider } from '../../utils/model/providers.js';
import { getActiveOpenAIModelOptionsCache, setActiveOpenAIModelOptionsCache } from '../../utils/providerProfiles.js';
import { getDefaultMainLoopModelSetting, isOpus1mMergeEnabled, renderDefaultModelSetting } from '../../utils/model/model.js';
import { isModelAllowed } from '../../utils/model/modelAllowlist.js';
import type { ModelOption } from '../../utils/model/modelOptions.js';
import { discoverOpenAICompatibleModelOptions } from '../../utils/model/openaiModelDiscovery.js';
import { validateModel } from '../../utils/model/validateModel.js';
import { getAdditionalModelOptionsCacheScope } from '../../services/api/providerConfig.js';
import { getActiveOpenAIModelOptionsCache, setActiveOpenAIModelOptionsCache } from '../../utils/providerProfiles.js';
function ModelPickerWrapper(t0) {
const $ = _c(17);
const {
Expand Down
52 changes: 52 additions & 0 deletions src/commands/version.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { expect, test } from 'bun:test'

import command from './version.js'
import type { LocalJSXCommandContext } from '../types/command.js'

(globalThis as { MACRO?: { VERSION?: string; BUILD_TIME?: string } }).MACRO = {
VERSION: '0.1.8',
BUILD_TIME: '2024-01-15T10:30:00Z',
}

const mockContext: LocalJSXCommandContext = {
getAppState: () => ({ mainLoopModel: 'claude-sonnet-4-20250514' } as any),
setAppState: () => { },
setMessages: () => { },
onChangeAPIKey: () => { },
options: {
ideInstallationStatus: null,
theme: 'dark',
},
} as any

test('version command has correct metadata', () => {
expect(command.name).toBe('version')
expect(command.type).toBe('local')
expect(command.supportsNonInteractive).toBe(true)
expect(command.description).toContain('version')
})

test('version call returns version with build time', async () => {
const loaded = await command.load()
const result = await loaded.call('', mockContext)

expect(result.type).toBe('text')
if (result.type === 'text') {
expect(result.value).toContain('0.1.8')
expect(result.value).toContain('built')
}
})

test('version call returns version without build time when BUILD_TIME is undefined', async () => {
; (globalThis as { MACRO?: { VERSION?: string; BUILD_TIME?: string } }).MACRO = {
VERSION: '0.1.8',
}

const loaded = await command.load()
const result = await loaded.call('', mockContext)

expect(result.type).toBe('text')
if (result.type === 'text') {
expect(result.value).toBe('0.1.8')
}
})
2 changes: 1 addition & 1 deletion src/components/TrustDialog/TrustDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ export function TrustDialog(t0) {
let t20;
if ($[24] === Symbol.for("react.memo_cache_sentinel")) {
t20 = [{
label: "Yes, I trust this folder",
label: "Yes, I trust this folder asba",
value: "enable_all"
}, {
label: "No, exit",
Expand Down
Loading