├── src ├── config │ ├── templates │ │ ├── example.tempurai.md │ │ ├── example-config.json │ │ └── example-config-multi-model.json │ ├── ConfigInitializer.ts │ └── validators │ │ └── index.ts ├── cli │ ├── components │ │ ├── base.tsx │ │ ├── events │ │ │ ├── TextEventItem.tsx │ │ │ ├── EventRouter.tsx │ │ │ ├── GenericToolEventItem.tsx │ │ │ ├── ShellExecutionEventItem.tsx │ │ │ └── SystemEventItem.tsx │ │ ├── BaseInputField.tsx │ │ ├── StatusIndicator.tsx │ │ ├── WelcomeScreen.tsx │ │ ├── EventItem.tsx │ │ ├── HelpPanel.tsx │ │ ├── ExecutionModeSelector.tsx │ │ ├── CodePreview.tsx │ │ ├── ProgressIndicator.tsx │ │ ├── InputContainer.tsx │ │ ├── CommandPalette.tsx │ │ └── ThemeSelector.tsx │ ├── themes │ │ ├── index.ts │ │ ├── themes │ │ │ ├── index.ts │ │ │ ├── dark.ts │ │ │ ├── dracula.ts │ │ │ ├── light.ts │ │ │ ├── monokai.ts │ │ │ ├── solarized.ts │ │ │ └── high-contrast.ts │ │ ├── ThemeProvider.tsx │ │ └── ThemeTypes.ts │ ├── hooks │ │ └── useEventSeparation.ts │ ├── stores │ │ └── uiStore.ts │ └── index.ts ├── events │ ├── index.ts │ ├── UIEventEmitter.ts │ └── EventTypes.ts ├── tools │ ├── index.ts │ ├── MemoryTools.ts │ ├── ToolRegistry.ts │ └── GitTools.ts ├── models │ └── index.ts ├── errors │ └── index.ts ├── services │ ├── ExecutionModeManager.ts │ ├── InterruptService.ts │ ├── CompressorService.ts │ └── EditModeManager.ts ├── utils │ ├── IndentLogger.ts │ └── Logger.ts ├── test │ ├── setup.ts │ ├── web-tools-test.js │ ├── mcp-integration-test.js │ ├── dynamic-model-test.js │ ├── config-security-test.js │ ├── integrated-loop-test.js │ ├── ConfigLoader.test.ts │ ├── loop-detection-test.js │ ├── config.ts │ └── MockAISDK.ts ├── di │ ├── types.ts │ ├── interfaces.ts │ └── container.ts ├── indexing │ ├── GitTracker.ts │ ├── IndexingAgent.ts │ └── FileContentCollector.ts └── agents │ └── smart_agent │ ├── AgentOrchestrator.ts │ └── ToolInterceptor.ts ├── .npmignore ├── .gitignore ├── jest.config.cjs ├── tsconfig.json ├── scripts └── README.md ├── package.json └── docs ├── architecture.md └── cli-schema.md /src/config/templates/example.tempurai.md: -------------------------------------------------------------------------------- 1 | # Custom Context 2 | -------------------------------------------------------------------------------- /src/cli/components/base.tsx: -------------------------------------------------------------------------------- 1 | export const MAX_FRAME_WIDTH = 72; 2 | -------------------------------------------------------------------------------- /src/events/index.ts: -------------------------------------------------------------------------------- 1 | export * from './EventTypes.js'; 2 | export * from './UIEventEmitter.js'; -------------------------------------------------------------------------------- /src/cli/themes/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ThemeTypes.js'; 2 | export * from './ThemeProvider.js'; 3 | export { themes } from './themes/index.js'; -------------------------------------------------------------------------------- /src/tools/index.ts: -------------------------------------------------------------------------------- 1 | // 导出现有工具 2 | export * from './ShellExecutor.js'; 3 | export * from '../services/SnapshotManager.js'; 4 | 5 | // 错误处理 6 | export * from '../errors/ErrorHandler.js'; 7 | -------------------------------------------------------------------------------- /src/models/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 模型相关的类型、接口和工厂 3 | * 提供统一的模型创建和管理接口 4 | */ 5 | 6 | // 导出主要类型 7 | export type { ModelProvider, ModelConfig } from './ModelFactory.js'; 8 | 9 | // 导出默认模型工厂 10 | export { DefaultModelFactory } from './ModelFactory.js'; 11 | 12 | // 便捷的默认导出 13 | export { DefaultModelFactory as ModelFactory } from './ModelFactory.js'; -------------------------------------------------------------------------------- /src/errors/index.ts: -------------------------------------------------------------------------------- 1 | import type { ErrorResponse } from './ErrorHandler.js'; 2 | 3 | /** 4 | * 便捷的错误格式化函数 5 | * @param error 错误对象 6 | * @returns 格式化的错误消息 7 | */ 8 | export function formatErrorMessage(error: unknown): string { 9 | if (error instanceof Error) { 10 | return error.message; 11 | } 12 | 13 | return String(error); 14 | } 15 | 16 | /** 17 | * 检查是否为标准错误响应 18 | * @param response 响应对象 19 | * @returns 是否为ErrorResponse类型 20 | */ 21 | export function isErrorResponse(response: any): response is ErrorResponse { 22 | return response && typeof response === 'object' && response.success === false && 'error' in response; 23 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Source files 2 | src/ 3 | listed.txt 4 | 5 | # Development files 6 | .vscode/ 7 | .git/ 8 | .gitignore 9 | *.log 10 | .DS_Store 11 | 12 | # Documentation 13 | docs/ 14 | README-ENHANCED.md 15 | 16 | # Test files 17 | **/*.test.ts 18 | **/*.test.js 19 | **/*.spec.ts 20 | **/*.spec.js 21 | jest.config.* 22 | coverage/ 23 | 24 | # Build tools 25 | tsconfig.json 26 | .eslintrc.* 27 | .prettierrc.* 28 | 29 | # Dependencies 30 | node_modules/ 31 | pnpm-lock.yaml 32 | yarn.lock 33 | package-lock.json 34 | 35 | # Temporary files 36 | *.tmp 37 | *.temp 38 | .env* 39 | 40 | # IDE files 41 | *.swp 42 | *.swo 43 | *~ 44 | .idea/ 45 | 46 | # OS files 47 | Thumbs.db 48 | -------------------------------------------------------------------------------- /src/cli/themes/themes/index.ts: -------------------------------------------------------------------------------- 1 | import { ThemeRegistry } from '../ThemeTypes.js'; 2 | import { darkTheme } from './dark.js'; 3 | import { lightTheme } from './light.js'; 4 | import { monokaiTheme } from './monokai.js'; 5 | import { solarizedTheme } from './solarized.js'; 6 | import { draculaTheme } from './dracula.js'; 7 | import { highContrastTheme } from './high-contrast.js'; 8 | 9 | export const themes: ThemeRegistry = { 10 | dark: darkTheme, 11 | light: lightTheme, 12 | monokai: monokaiTheme, 13 | solarized: solarizedTheme, 14 | dracula: draculaTheme, 15 | 'high-contrast': highContrastTheme, 16 | }; 17 | 18 | export { darkTheme, lightTheme, monokaiTheme, solarizedTheme, draculaTheme, highContrastTheme }; -------------------------------------------------------------------------------- /src/services/ExecutionModeManager.ts: -------------------------------------------------------------------------------- 1 | export enum ExecutionMode { 2 | CODE = 'code', 3 | PLAN = 'plan' 4 | } 5 | 6 | export const ExecutionModeData = [ 7 | { 8 | mode: ExecutionMode.PLAN, 9 | displayName: 'Plan Mode', 10 | description: 'Research and analyze, no file modifications', 11 | }, 12 | { 13 | mode: ExecutionMode.CODE, 14 | displayName: 'Code Mode', 15 | description: 'Full development capabilities with file modifications', 16 | } 17 | ] 18 | 19 | export const getExecutionModeDisplayInfo = (targetMode: ExecutionMode) => { 20 | const modeData = ExecutionModeData.find(item => item.mode === targetMode); 21 | return modeData ? { ...modeData } : null; 22 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Node.js dependencies 2 | node_modules/ 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | pnpm-debug.log* 7 | 8 | # Output directories 9 | dist/ 10 | build/ 11 | coverage/ 12 | .out/ 13 | .tmp/ 14 | .cache/ 15 | 16 | # Environment variables 17 | .env 18 | .env.test 19 | .env.production 20 | .env.local 21 | 22 | # Logs 23 | logs 24 | *.log 25 | *.log.* 26 | 27 | # OS files 28 | .DS_Store 29 | Thumbs.db 30 | 31 | # Editor files 32 | .vscode/ 33 | .idea/ 34 | *.swp 35 | 36 | # CLI specific 37 | bin/ 38 | *.exe 39 | *.out 40 | *.app 41 | 42 | # Optional lock files (pick one package manager) 43 | package-lock.json 44 | yarn.lock 45 | pnpm-lock.yaml 46 | 47 | dist/ 48 | .claude/ 49 | .tempurai/ 50 | 51 | listed.txt -------------------------------------------------------------------------------- /jest.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest/presets/default-esm', 3 | testEnvironment: 'node', 4 | roots: ['/src'], 5 | testMatch: [ 6 | '**/__tests__/**/*.ts', 7 | '**/?(*.)+(spec|test).ts' 8 | ], 9 | transform: { 10 | '^.+\\.ts$': ['ts-jest', { 11 | useESM: true, 12 | tsconfig: { 13 | module: 'ESNext' 14 | } 15 | }] 16 | }, 17 | collectCoverageFrom: [ 18 | 'src/**/*.ts', 19 | '!src/**/*.d.ts', 20 | '!src/**/index.ts', 21 | '!src/test/**/*' 22 | ], 23 | setupFilesAfterEnv: ['/src/test/setup.ts'], 24 | moduleNameMapper: { 25 | '^(\\.{1,2}/.*)\\.js$': '$1' 26 | }, 27 | extensionsToTreatAsEsm: ['.ts'], 28 | testTimeout: 30000, 29 | verbose: true 30 | }; -------------------------------------------------------------------------------- /src/cli/components/events/TextEventItem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box, Text } from 'ink'; 3 | import { CLIEvent } from '../../hooks/useSessionEvents.js'; 4 | import { useTheme } from '../../themes/index.js'; 5 | 6 | interface TextEventItemProps { 7 | event: CLIEvent; 8 | index: number; 9 | } 10 | 11 | export const TextEventItem: React.FC = ({ event }) => { 12 | const { currentTheme } = useTheme(); 13 | 14 | return ( 15 | 16 | 17 | 18 | 19 | {event.content} 20 | 21 | 22 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist", 4 | "module": "nodenext", 5 | "moduleResolution": "nodenext", 6 | "moduleDetection": "force", 7 | "target": "esnext", 8 | "lib": ["DOM", "DOM.Iterable", "ES2023"], 9 | "resolveJsonModule": false, 10 | "jsx": "react-jsx", 11 | "declaration": true, 12 | "newLine": "lf", 13 | "stripInternal": true, 14 | "strict": true, 15 | "noImplicitReturns": true, 16 | "noImplicitOverride": true, 17 | "noEmitOnError": true, 18 | "useDefineForClassFields": true, 19 | "forceConsistentCasingInFileNames": true, 20 | "skipLibCheck": true, 21 | "experimentalDecorators": true, 22 | "emitDecoratorMetadata": true 23 | }, 24 | "include": ["src/**/*"], 25 | "exclude": ["node_modules", "dist", "src/gui/**/*"], 26 | "ts-node": { 27 | "esm": true, 28 | "experimentalSpecifierResolution": "node" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/cli/hooks/useEventSeparation.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { CLIEvent, CLIEventType, CLISymbol } from './useSessionEvents.js'; 3 | 4 | interface EventSeparation { 5 | staticEvents: CLIEvent[]; 6 | dynamicEvents: CLIEvent[]; 7 | } 8 | 9 | export const useEventSeparation = (events: CLIEvent[]): EventSeparation => { 10 | return React.useMemo(() => { 11 | const dynamic: CLIEvent[] = []; 12 | const static_events: CLIEvent[] = []; 13 | 14 | events.forEach((event) => { 15 | // 将正在执行的工具事件放到动态区域 16 | if (event.type === CLIEventType.TOOL_EXECUTION && event.symbol === CLISymbol.TOOL_EXECUTING) { 17 | dynamic.push(event); 18 | } else { 19 | static_events.push(event); 20 | } 21 | }); 22 | 23 | return { 24 | staticEvents: static_events, 25 | dynamicEvents: dynamic, 26 | }; 27 | }, [events]); 28 | }; -------------------------------------------------------------------------------- /src/utils/IndentLogger.ts: -------------------------------------------------------------------------------- 1 | import { UIEventEmitter } from '../events/UIEventEmitter.js'; 2 | import { SystemInfoEvent } from '../events/EventTypes.js'; 3 | 4 | export class IndentLogger { 5 | private static eventEmitter?: UIEventEmitter; 6 | 7 | static setEventEmitter(eventEmitter?: UIEventEmitter): void { 8 | this.eventEmitter = eventEmitter; 9 | } 10 | 11 | static log(message: string, indent = 0): void { 12 | const formattedMessage = indent === 0 ? message : `→ ${message}`; 13 | console.log(formattedMessage); 14 | } 15 | 16 | static logAndSendEvent(message: string, indent = 0): void { 17 | const formattedMessage = indent === 0 ? message : `→ ${message}`; 18 | console.log(formattedMessage); 19 | if (this.eventEmitter) { 20 | this.eventEmitter.emit({ 21 | type: 'system_info', 22 | level: 'info', 23 | message: formattedMessage, 24 | source: 'system' 25 | } as SystemInfoEvent); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /src/services/InterruptService.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from 'inversify'; 2 | 3 | @injectable() 4 | export class InterruptService { 5 | private interrupted = false; 6 | private abortController: AbortController | null = null; 7 | 8 | // 开始新任务时创建新的AbortController 9 | startTask(): void { 10 | this.interrupted = false; 11 | this.abortController = new AbortController(); 12 | } 13 | 14 | // 中断当前任务 15 | interrupt(): void { 16 | this.interrupted = true; 17 | if (this.abortController) { 18 | this.abortController.abort(); 19 | } 20 | } 21 | 22 | isInterrupted(): boolean { 23 | return this.interrupted; 24 | } 25 | 26 | // 获取当前的AbortSignal,供AI SDK和其他操作使用 27 | getAbortSignal(): AbortSignal | undefined { 28 | return this.abortController?.signal; 29 | } 30 | 31 | // 检查AbortSignal是否已被中断 32 | isAborted(): boolean { 33 | return this.abortController?.signal.aborted ?? false; 34 | } 35 | 36 | reset(): void { 37 | this.interrupted = false; 38 | this.abortController = null; 39 | } 40 | } -------------------------------------------------------------------------------- /src/cli/stores/uiStore.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand'; 2 | import { ExecutionMode } from '../../services/ExecutionModeManager.js'; 3 | 4 | export type PanelMode = 'COMMAND_PALETTE' | 'EXECUTION_MODE' | 'HELP' | 'THEME'; 5 | export type ActivePanel = 'INPUT' | 'CONFIRMATION' | PanelMode; 6 | 7 | interface UiState { 8 | executionMode: ExecutionMode; 9 | activePanel: ActivePanel; 10 | initialInputValue: string | null; // 新增状态 11 | actions: { 12 | setExecutionMode: (mode: ExecutionMode) => void; 13 | // 接收可选的初始值 14 | setActivePanel: (panel: ActivePanel, initialValue?: string) => void; 15 | }; 16 | } 17 | 18 | export const useUiStore = create((set) => ({ 19 | executionMode: ExecutionMode.CODE, 20 | activePanel: 'INPUT', 21 | initialInputValue: null, // 初始化 22 | actions: { 23 | setExecutionMode: (mode) => set({ executionMode: mode }), 24 | // 实现新的 action 逻辑 25 | setActivePanel: (panel, initialValue) => 26 | set({ 27 | activePanel: panel, 28 | initialInputValue: initialValue ?? null 29 | }), 30 | }, 31 | })); -------------------------------------------------------------------------------- /src/test/setup.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import * as path from 'path'; 3 | import * as os from 'os'; 4 | 5 | // Mock ConfigInitializer globally to avoid import.meta.url issues in Jest 6 | jest.mock('../config/ConfigInitializer.js', () => { 7 | return { 8 | ConfigInitializer: jest.fn().mockImplementation(() => ({ 9 | globalConfigExists: jest.fn().mockReturnValue(true), 10 | projectConfigExists: jest.fn().mockReturnValue(true), 11 | createGlobalFiles: jest.fn(), 12 | createProjectFiles: jest.fn(), 13 | initializeGlobalFiles: jest.fn(), 14 | getConfigDir: jest.fn().mockReturnValue(path.join(os.homedir(), '.tempurai')), 15 | getConfigPath: jest.fn().mockReturnValue(path.join(os.homedir(), '.tempurai', 'config.json')), 16 | getContextPath: jest.fn().mockReturnValue(path.join(os.homedir(), '.tempurai', '.tempurai.md')) 17 | })) 18 | }; 19 | }); 20 | 21 | // Mock console methods during tests to avoid noise 22 | global.console = { 23 | ...console, 24 | log: jest.fn(), 25 | debug: jest.fn(), 26 | info: jest.fn(), 27 | warn: jest.fn(), 28 | error: jest.fn(), 29 | }; 30 | 31 | // Set test environment variables 32 | process.env.NODE_ENV = 'test'; 33 | process.env.OPENAI_API_KEY = 'test-key'; 34 | 35 | // Global test timeout 36 | jest.setTimeout(30000); -------------------------------------------------------------------------------- /src/di/types.ts: -------------------------------------------------------------------------------- 1 | export const TYPES = { 2 | // Config 3 | Config: Symbol.for('Config'), 4 | ConfigLoader: Symbol.for('ConfigLoader'), 5 | LanguageModel: Symbol.for('LanguageModel'), 6 | ModelFactory: Symbol.for('ModelFactory'), 7 | 8 | // Agents 9 | SmartAgent: Symbol.for('SmartAgent'), 10 | AgentOrchestrator: Symbol.for('AgentOrchestrator'), 11 | TodoManager: Symbol.for('TodoManager'), 12 | SubAgent: Symbol.for('SubAgent'), 13 | CompressedAgent: Symbol.for('CompressedAgent'), 14 | 15 | // Services 16 | FileWatcherService: Symbol.for('FileWatcherService'), 17 | UIEventEmitter: Symbol.for('UIEventEmitter'), 18 | Logger: Symbol.for('Logger'), 19 | SecurityPolicyEngine: Symbol.for('SecurityPolicyEngine'), 20 | ToolRegistry: Symbol.for('ToolRegistry'), 21 | 22 | // Request-scoped services 23 | InterruptService: Symbol.for('InterruptService'), 24 | EditModeManager: Symbol.for('EditModeManager'), 25 | HITLManager: Symbol.for('HITLManager'), 26 | CompressorService: Symbol.for('CompressorService'), 27 | 28 | // Tool services 29 | ToolAgent: Symbol.for('ToolAgent'), 30 | ToolInterceptor: Symbol.for('ToolInterceptor'), 31 | 32 | // Indexing 33 | ProjectIndexer: Symbol.for('ProjectIndexer'), 34 | 35 | // Factories 36 | SessionServiceFactory: Symbol.for('SessionServiceFactory'), 37 | }; -------------------------------------------------------------------------------- /src/di/interfaces.ts: -------------------------------------------------------------------------------- 1 | import { SnapshotResult, RestoreResult, SnapshotInfo } from '../services/SnapshotManager.js'; 2 | import { SessionService } from '../services/SessionService.js'; 3 | 4 | /** 5 | * 快照管理器接口定义 6 | * 提供项目状态快照和恢复功能 7 | */ 8 | export interface ISnapshotManager { 9 | /** 10 | * 初始化快照管理器 11 | */ 12 | initialize(): Promise; 13 | 14 | /** 15 | * 创建项目状态快照 16 | * @param description 快照描述 17 | * @returns 快照创建结果 18 | */ 19 | createSnapshot(description: string): Promise; 20 | 21 | /** 22 | * 恢复到指定快照 23 | * @param snapshotId 快照ID 24 | * @returns 恢复结果 25 | */ 26 | restoreSnapshot(snapshotId: string): Promise; 27 | 28 | /** 29 | * 列出所有快照 30 | * @returns 快照信息列表 31 | */ 32 | listSnapshots(): Promise; 33 | 34 | /** 35 | * 清理旧快照 36 | * @param retentionDays 保留天数 37 | * @returns 清理的快照数量 38 | */ 39 | cleanupOldSnapshots(retentionDays?: number): Promise; 40 | 41 | /** 42 | * 获取快照管理器状态 43 | * @returns 状态信息 44 | */ 45 | getStatus(): Promise<{ 46 | initialized: boolean; 47 | shadowRepoExists: boolean; 48 | snapshotCount: number; 49 | latestSnapshot?: SnapshotInfo; 50 | }>; 51 | } 52 | 53 | /** 54 | * SessionService工厂函数类型 55 | */ 56 | export interface SessionBundle { 57 | sessionService: SessionService; 58 | clearSession(): void; 59 | } 60 | 61 | export type SessionServiceFactory = () => SessionBundle; -------------------------------------------------------------------------------- /src/cli/components/BaseInputField.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box, Text } from 'ink'; 3 | import TextInput from 'ink-text-input'; 4 | import { useTheme } from '../themes/index.js'; 5 | import { ExecutionMode } from '../../services/ExecutionModeManager.js'; 6 | 7 | interface BaseInputFieldProps { 8 | value: string; 9 | onChange: (value: string) => void; 10 | onSubmit: () => void; 11 | isProcessing: boolean; 12 | isActive: boolean; 13 | placeholder?: string; 14 | executionMode?: ExecutionMode; 15 | focus?: boolean; 16 | } 17 | 18 | export const BaseInputField: React.FC = ({ value, onChange, onSubmit, isProcessing, isActive, placeholder, executionMode, focus }) => { 19 | const { currentTheme } = useTheme(); 20 | const modePrefix = executionMode ? `(${executionMode.toUpperCase()}) ` : ''; 21 | 22 | return ( 23 | 24 | 25 | 26 | {modePrefix} 27 | {'> '} 28 | 29 | 30 | 31 | 32 | 33 | 34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /src/test/web-tools-test.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * 测试 Web 工具功能 5 | */ 6 | 7 | // 使用 ts-node 运行,这样可以直接导入 TypeScript 源码 8 | require('ts-node/register'); 9 | const { webSearchTool, urlFetchTool } = require('../tools/WebTools.ts'); 10 | 11 | async function testWebTools() { 12 | console.log('🌐 测试 Web 工具功能...\n'); 13 | 14 | // 测试 URL 安全检查 15 | console.log('1. 测试 URL 安全检查:'); 16 | const unsafeResult = await urlFetchTool.execute({ url: 'http://localhost:3000' }); 17 | console.log('本地地址测试:', unsafeResult.success ? '❌ 应该失败' : '✅ 正确阻止'); 18 | console.log('错误信息:', unsafeResult.error); 19 | console.log(); 20 | 21 | // 测试安全 URL 获取(使用一个简单的 HTML 页面) 22 | console.log('2. 测试安全 URL 获取:'); 23 | const safeResult = await urlFetchTool.execute({ url: 'https://example.com' }); 24 | console.log('安全URL测试:', safeResult.success ? '✅ 成功' : '❌ 失败'); 25 | if (safeResult.success) { 26 | console.log('内容长度:', safeResult.content.length); 27 | console.log('是否截断:', safeResult.truncated); 28 | console.log('标题:', safeResult.title || '未找到'); 29 | } else { 30 | console.log('错误:', safeResult.error); 31 | } 32 | console.log(); 33 | 34 | // 测试无效配置的 web 搜索 35 | console.log('3. 测试未配置 API Key 的搜索:'); 36 | const searchResult = await webSearchTool.execute({ query: 'TypeScript best practices' }); 37 | console.log('未配置搜索:', searchResult.success ? '❌ 不应成功' : '✅ 正确失败'); 38 | console.log('错误信息:', searchResult.error); 39 | } 40 | 41 | testWebTools().catch(console.error); 42 | -------------------------------------------------------------------------------- /src/cli/themes/ThemeProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext, useState, ReactNode } from 'react'; 2 | import { ThemeConfig, ThemeName } from './ThemeTypes.js'; 3 | import { themes } from './themes/index.js'; 4 | 5 | interface ThemeContextType { 6 | currentTheme: ThemeConfig; 7 | themeName: ThemeName; 8 | setTheme: (themeName: ThemeName) => void; 9 | availableThemes: ThemeName[]; 10 | } 11 | 12 | const ThemeContext = createContext(undefined); 13 | 14 | interface ThemeProviderProps { 15 | children: ReactNode; 16 | defaultTheme?: ThemeName; 17 | } 18 | 19 | export const ThemeProvider: React.FC = ({ 20 | children, 21 | defaultTheme = 'dark' 22 | }) => { 23 | const [themeName, setThemeName] = useState(defaultTheme); 24 | const currentTheme = themes[themeName]; 25 | const availableThemes = Object.keys(themes) as ThemeName[]; 26 | 27 | const setTheme = (newThemeName: ThemeName) => { 28 | if (themes[newThemeName]) { 29 | setThemeName(newThemeName); 30 | } 31 | }; 32 | 33 | return ( 34 | 35 | {children} 36 | 37 | ); 38 | }; 39 | 40 | export const useTheme = (): ThemeContextType => { 41 | const context = useContext(ThemeContext); 42 | if (!context) { 43 | throw new Error('useTheme must be used within a ThemeProvider'); 44 | } 45 | return context; 46 | }; -------------------------------------------------------------------------------- /src/cli/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Tempurai CLI - AI辅助编程CLI工具 5 | * 统一入口点:处理路由和应用启动 6 | * 7 | * 路由规则: 8 | * - tempurai (无参数) -> 启动代码编辑界面 (InkUI) 9 | * - tempurai --help, config, version -> 系统命令模式 10 | */ 11 | 12 | import 'reflect-metadata'; 13 | import { bootstrapApplication, parseArguments } from './bootstrap.js'; 14 | 15 | /** 16 | * 错误处理器 17 | */ 18 | function setupErrorHandlers(): void { 19 | // 捕获未处理的Promise拒绝 20 | process.on('unhandledRejection', (reason: unknown) => { 21 | console.error('💥 Unhandled Promise Rejection:', reason); 22 | process.exit(1); 23 | }); 24 | 25 | // 捕获未捕获的异常 26 | process.on('uncaughtException', (error: Error) => { 27 | console.error('💥 Uncaught Exception:', error); 28 | process.exit(1); 29 | }); 30 | } 31 | 32 | /** 33 | * 主函数 - 应用入口点 34 | */ 35 | async function main(): Promise { 36 | try { 37 | // 设置错误处理 38 | setupErrorHandlers(); 39 | 40 | // 解析命令行参数 41 | const args = process.argv.slice(2); 42 | 43 | // 启动应用 44 | await bootstrapApplication(args); 45 | 46 | } catch (error) { 47 | console.error('💥 应用启动失败:', error instanceof Error ? error.message : '未知错误'); 48 | console.error('💡 请检查配置和环境设置'); 49 | process.exit(1); 50 | } 51 | } 52 | 53 | // 只有直接执行时才运行main函数 54 | if (import.meta.url === `file://${process.argv[1]}`) { 55 | main().catch((error: Error) => { 56 | console.error('💥 致命错误:', error); 57 | process.exit(1); 58 | }); 59 | } 60 | 61 | // 导出main函数供测试使用 62 | export { main }; -------------------------------------------------------------------------------- /src/cli/components/events/EventRouter.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { CLIEvent, CLIEventType } from '../../hooks/useSessionEvents.js'; 3 | import { ShellExecutionEventItem } from './ShellExecutionEventItem.js'; 4 | import { DiffEventItem } from './DiffEventItem.js'; 5 | import { GenericToolEventItem } from './GenericToolEventItem.js'; 6 | import { TextEventItem } from './TextEventItem.js'; 7 | import { SystemEventItem } from './SystemEventItem.js'; 8 | 9 | interface EventRouterProps { 10 | event: CLIEvent; 11 | index: number; 12 | } 13 | 14 | export const EventRouter: React.FC = ({ event, index }) => { 15 | switch (event.type) { 16 | case CLIEventType.TOOL_EXECUTION: 17 | // 从原始事件获取工具名称 18 | const toolName = (event.originalEvent as any)?.toolName; 19 | 20 | if (toolName === 'shell_executor' || toolName === 'multi_command') { 21 | return ; 22 | } 23 | 24 | if (toolName === 'apply_patch') { 25 | return ; 26 | } 27 | 28 | return ; 29 | 30 | case CLIEventType.AI_RESPONSE: 31 | return ; 32 | 33 | case CLIEventType.USER_INPUT: 34 | case CLIEventType.SYSTEM_INFO: 35 | return ; 36 | 37 | default: 38 | return ; 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /src/cli/components/StatusIndicator.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Text } from 'ink'; 3 | import { useTheme } from '../themes/index.js'; 4 | 5 | export type IndicatorType = 'user' | 'assistant' | 'tool' | 'error' | 'system'; 6 | 7 | interface StatusIndicatorProps { 8 | type: IndicatorType; 9 | isActive?: boolean; 10 | } 11 | 12 | export const StatusIndicator: React.FC = ({ type, isActive = false }) => { 13 | const { currentTheme } = useTheme(); 14 | 15 | const getIndicatorProps = () => { 16 | switch (type) { 17 | case 'user': 18 | return { 19 | symbol: '>', 20 | color: currentTheme.colors.semantic.functionCall, 21 | }; 22 | case 'assistant': 23 | return { 24 | symbol: '●', 25 | color: currentTheme.colors.info, 26 | }; 27 | case 'tool': 28 | return { 29 | symbol: isActive ? '~' : '●', 30 | color: isActive ? currentTheme.colors.warning : currentTheme.colors.success, 31 | }; 32 | case 'error': 33 | return { 34 | symbol: '!', 35 | color: currentTheme.colors.error, 36 | }; 37 | case 'system': 38 | return { 39 | symbol: '●', 40 | color: currentTheme.colors.semantic.indicator, 41 | }; 42 | default: 43 | return { 44 | symbol: '●', 45 | color: currentTheme.colors.text.muted, 46 | }; 47 | } 48 | }; 49 | 50 | const { symbol, color } = getIndicatorProps(); 51 | 52 | return {symbol}; 53 | }; 54 | -------------------------------------------------------------------------------- /src/cli/components/events/GenericToolEventItem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box, Text } from 'ink'; 3 | import { CLIEvent, CLISymbol } from '../../hooks/useSessionEvents.js'; 4 | import { useTheme } from '../../themes/index.js'; 5 | 6 | interface GenericToolEventItemProps { 7 | event: CLIEvent; 8 | index: number; 9 | } 10 | 11 | export const GenericToolEventItem: React.FC = ({ event }) => { 12 | const { currentTheme } = useTheme(); 13 | 14 | const getSymbolColor = () => { 15 | switch (event.symbol) { 16 | case CLISymbol.TOOL_EXECUTING: 17 | return currentTheme.colors.warning; 18 | case CLISymbol.TOOL_SUCCESS: 19 | return currentTheme.colors.success; 20 | case CLISymbol.TOOL_FAILED: 21 | return currentTheme.colors.error; 22 | default: 23 | return currentTheme.colors.text.primary; 24 | } 25 | }; 26 | 27 | return ( 28 | 29 | 30 | 31 | {event.symbol} {event.content} 32 | 33 | 34 | 35 | {event.subEvent && ( 36 | 37 | {' '}L 38 | 39 | {event.subEvent.map((subItem, index) => ( 40 | 41 | {subItem.content} 42 | 43 | ))} 44 | 45 | 46 | )} 47 | 48 | ); 49 | }; 50 | -------------------------------------------------------------------------------- /src/indexing/GitTracker.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from 'child_process'; 2 | 3 | export class GitTracker { 4 | constructor(private readonly projectRoot: string) { } 5 | 6 | async getCurrentHash(): Promise { 7 | try { 8 | const hash = execSync('git rev-parse HEAD', { 9 | cwd: this.projectRoot, 10 | encoding: 'utf-8' 11 | }).trim(); 12 | return hash; 13 | } catch { 14 | return `no-git-${Date.now()}`; 15 | } 16 | } 17 | 18 | async getChangedFiles(since?: string): Promise { 19 | if (!since) return []; 20 | 21 | try { 22 | const output = execSync(`git diff --name-only ${since}..HEAD`, { 23 | cwd: this.projectRoot, 24 | encoding: 'utf-8' 25 | }); 26 | 27 | return output.trim().split('\n').filter(Boolean); 28 | } catch { 29 | return []; 30 | } 31 | } 32 | 33 | async getCommitInfo(hash?: string): Promise<{ 34 | hash: string; 35 | message: string; 36 | author: string; 37 | date: string; 38 | } | null> { 39 | try { 40 | const targetHash = hash || 'HEAD'; 41 | const info = execSync(`git show --format="%H|%s|%an|%ai" --no-patch ${targetHash}`, { 42 | cwd: this.projectRoot, 43 | encoding: 'utf-8' 44 | }).trim(); 45 | 46 | const [commitHash, message, author, date] = info.split('|'); 47 | 48 | return { 49 | hash: commitHash, 50 | message, 51 | author, 52 | date 53 | }; 54 | } catch { 55 | return null; 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /src/test/mcp-integration-test.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * 测试 MCP 集成功能 5 | */ 6 | 7 | require('ts-node/register'); 8 | const { ConfigLoader } = require('../config/ConfigLoader.ts'); 9 | const { SimpleAgent } = require('../agents/SimpleAgent.ts'); 10 | 11 | async function testMcpIntegration() { 12 | console.log('🔌 测试 MCP 集成功能...\n'); 13 | 14 | try { 15 | // 创建测试配置(无 MCP 服务器) 16 | const config = { 17 | model: 'gpt-4o-mini', 18 | temperature: 0.3, 19 | maxTokens: 4096, 20 | apiKey: 'test-key', 21 | mcpServers: {}, // 空的 MCP 服务器配置 22 | tools: { 23 | shellExecutor: { 24 | defaultTimeout: 30000, 25 | maxRetries: 3, 26 | allowDangerousCommands: false, 27 | }, 28 | smartDiff: { 29 | contextLines: 3, 30 | maxRetries: 3, 31 | enableFuzzyMatching: true, 32 | }, 33 | }, 34 | }; 35 | 36 | console.log('1. 创建 SimpleAgent 实例...'); 37 | const agent = new SimpleAgent(config); 38 | console.log('✅ SimpleAgent 创建成功'); 39 | 40 | console.log('\n2. 测试异步初始化(无 MCP 服务器)...'); 41 | await agent.initializeAsync(); 42 | console.log('✅ 异步初始化完成'); 43 | 44 | console.log('\n3. 检查 MCP 状态...'); 45 | const mcpStatus = agent.getMcpStatus(); 46 | console.log('MCP 状态:', mcpStatus); 47 | console.log(`✅ MCP 工具数量: ${mcpStatus.toolCount}`); 48 | console.log(`✅ MCP 连接数量: ${mcpStatus.connectionCount}`); 49 | 50 | console.log('\n4. 清理资源...'); 51 | await agent.cleanup(); 52 | console.log('✅ 资源清理完成'); 53 | 54 | console.log('\n🎉 MCP 集成测试完成!'); 55 | } catch (error) { 56 | console.error('❌ 测试失败:', error instanceof Error ? error.message : '未知错误'); 57 | console.error(error); 58 | } 59 | } 60 | 61 | testMcpIntegration(); 62 | -------------------------------------------------------------------------------- /src/cli/themes/ThemeTypes.ts: -------------------------------------------------------------------------------- 1 | export interface ThemeColors { 2 | primary: string; 3 | secondary: string; 4 | accent: string; 5 | success: string; 6 | warning: string; 7 | error: string; 8 | info: string; 9 | text: { 10 | primary: string; 11 | secondary: string; 12 | muted: string; 13 | inverse: string; 14 | }; 15 | background: { 16 | primary: string; 17 | secondary: string; 18 | tertiary: string; 19 | }; 20 | ui: { 21 | border: string; 22 | separator: string; 23 | highlight: string; 24 | selection: string; 25 | progress: string; 26 | }; 27 | tools: { 28 | shell: string; 29 | file: string; 30 | git: string; 31 | web: string; 32 | code: string; 33 | }; 34 | react: { 35 | thought: string; 36 | plan: string; 37 | action: string; 38 | observation: string; 39 | }; 40 | syntax: { 41 | keyword: string; 42 | string: string; 43 | function: string; 44 | property: string; 45 | punctuation: string; 46 | comment: string; 47 | number: string; 48 | operator: string; 49 | }; 50 | diff: { 51 | added: string; 52 | removed: string; 53 | modified: string; 54 | context: string; 55 | lineNumber: string; 56 | }; 57 | semantic: { 58 | functionCall: string; 59 | parameter: string; 60 | result: string; 61 | metadata: string; 62 | indicator: string; 63 | }; 64 | } 65 | 66 | export interface ThemeConfig { 67 | name: string; 68 | displayName: string; 69 | type: 'dark' | 'light'; 70 | colors: ThemeColors; 71 | fonts: { 72 | mono: boolean; 73 | size: 'small' | 'medium' | 'large'; 74 | }; 75 | layout: { 76 | compact: boolean; 77 | showTimestamps: boolean; 78 | showProgress: boolean; 79 | }; 80 | animation: { 81 | enabled: boolean; 82 | speed: 'slow' | 'normal' | 'fast'; 83 | }; 84 | } 85 | 86 | export type ThemeName = 'dark' | 'light' | 'monokai' | 'solarized' | 'dracula' | 'high-contrast'; 87 | export type ThemeRegistry = Record; -------------------------------------------------------------------------------- /src/indexing/IndexingAgent.ts: -------------------------------------------------------------------------------- 1 | import { generateObject } from 'ai'; 2 | import type { LanguageModel } from 'ai'; 3 | import { getContainer } from '../di/container.js'; 4 | import { TYPES } from '../di/types.js'; 5 | import type { Config } from '../config/ConfigLoader.js'; 6 | import { ZodSchema } from 'zod'; 7 | import { IndentLogger } from '../utils/IndentLogger.js'; 8 | 9 | export type IndexingMessage = { role: 'system' | 'user' | 'assistant', content: string }; 10 | export type IndexingMessages = IndexingMessage[]; 11 | 12 | export class IndexingAgent { 13 | private model!: LanguageModel; 14 | private config!: Config; 15 | private initialized = false; 16 | 17 | private async initialize(): Promise { 18 | if (this.initialized) return; 19 | 20 | const container = getContainer(); 21 | this.model = await container.getAsync(TYPES.LanguageModel); 22 | this.config = container.get(TYPES.Config); 23 | this.initialized = true; 24 | } 25 | 26 | async generateObject(messages: IndexingMessages, schema: ZodSchema): Promise { 27 | await this.initialize(); 28 | 29 | const totalInputChars = messages.map(m => m.content).join('').length; 30 | IndentLogger.log(`Sending analysis request to AI (~${(totalInputChars / 1024).toFixed(1)} KB)`, 1); 31 | 32 | try { 33 | const { object } = await generateObject({ 34 | model: this.model, 35 | messages, 36 | schema, 37 | maxTokens: this.config.maxTokens, 38 | temperature: this.config.temperature, 39 | }); 40 | 41 | IndentLogger.log('AI analysis completed successfully', 1); 42 | return object; 43 | } catch (error) { 44 | const errorMessage = error instanceof Error ? error.message : 'Unknown error'; 45 | IndentLogger.log(`AI analysis failed: ${errorMessage}`, 1); 46 | throw new Error(`Indexing object generation failed: ${errorMessage}`); 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /src/cli/components/events/ShellExecutionEventItem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box, Text } from 'ink'; 3 | import { CLIEvent, CLISymbol } from '../../hooks/useSessionEvents.js'; 4 | import { useTheme } from '../../themes/index.js'; 5 | 6 | interface ShellExecutionEventItemProps { 7 | event: CLIEvent; 8 | index: number; 9 | } 10 | 11 | export const ShellExecutionEventItem: React.FC = ({ event }) => { 12 | const { currentTheme } = useTheme(); 13 | 14 | const getSymbolColor = () => { 15 | switch (event.symbol) { 16 | case CLISymbol.TOOL_EXECUTING: 17 | return currentTheme.colors.warning; 18 | case CLISymbol.TOOL_SUCCESS: 19 | return currentTheme.colors.success; 20 | case CLISymbol.TOOL_FAILED: 21 | return currentTheme.colors.error; 22 | default: 23 | return currentTheme.colors.text.primary; 24 | } 25 | }; 26 | 27 | return ( 28 | 29 | 30 | 31 | {event.symbol} {event.content} 32 | 33 | 34 | 35 | {event.subEvent && ( 36 | 37 | {' '}L 38 | 39 | {event.subEvent.map((subItem, index) => { 40 | let content = subItem.content; 41 | const lines = content.split('\n'); 42 | const maxLines = subItem.type === 'error' ? 20 : 7; 43 | 44 | if (lines.length > maxLines) { 45 | content = lines.slice(0, maxLines).join('\n') + `\n(...${lines.length - maxLines} more lines)`; 46 | } 47 | 48 | return ( 49 | 50 | {content} 51 | 52 | ); 53 | })} 54 | 55 | 56 | )} 57 | 58 | ); 59 | }; 60 | -------------------------------------------------------------------------------- /src/cli/themes/themes/dark.ts: -------------------------------------------------------------------------------- 1 | import { ThemeConfig } from '../ThemeTypes.js'; 2 | 3 | export const darkTheme: ThemeConfig = { 4 | name: 'dark', 5 | displayName: 'VS Code Dark+ (CLI tuned)', 6 | type: 'dark', 7 | colors: { 8 | primary: '#FEFEFE', 9 | secondary: '#C586C0', 10 | accent: '#ef4444', 11 | 12 | success: '#73C991', 13 | warning: '#FFCC02', 14 | error: '#F44747', 15 | info: '#75BEFF', 16 | 17 | text: { 18 | primary: '#FAFAFA', // 主文字,接近白 19 | secondary: '#DDDDDD', // 次级文字,冷灰蓝 20 | muted: '#A0A0A0', // 中性灰 21 | inverse: '#1E1E1E', // 反转用 22 | }, 23 | 24 | background: { 25 | primary: '#1E1E1E', 26 | secondary: '#242526', 27 | tertiary: '#2B2B2F', 28 | }, 29 | 30 | ui: { 31 | border: '#5A5A5A', 32 | separator: '#464646', 33 | highlight: '#347FD1', 34 | selection: '#347FD1', 35 | progress: '#1292E5', 36 | }, 37 | 38 | tools: { 39 | shell: '#73C991', 40 | file: '#FFCC02', 41 | git: '#F97316', 42 | web: '#C586C0', 43 | code: '#4EC9B0', 44 | }, 45 | 46 | react: { 47 | thought: '#75BEFF', 48 | plan: '#73C991', 49 | action: '#FFCC02', 50 | observation: '#C586C0', 51 | }, 52 | 53 | syntax: { 54 | keyword: '#569CD6', 55 | string: '#CE9178', 56 | function: '#DCDCAA', 57 | property: '#9CDCFE', 58 | punctuation: '#D4D4D4', 59 | comment: '#7F8C8D', 60 | number: '#B5CEA8', 61 | operator: '#D4D4D4', 62 | }, 63 | 64 | diff: { 65 | added: '#2FBF71', 66 | removed: '#E24A4A', 67 | modified: '#FFC83A', 68 | context: '#8A8F98', 69 | lineNumber: '#909090', 70 | }, 71 | 72 | semantic: { 73 | functionCall: '#e7f8f2', 74 | parameter: '#9CDCFE', 75 | result: '#E0E0E0', 76 | metadata: '#8A8A8A', 77 | indicator: '#8D8D8D', 78 | }, 79 | }, 80 | 81 | fonts: { mono: true, size: 'medium' }, 82 | 83 | layout: { 84 | compact: false, 85 | showTimestamps: true, 86 | showProgress: true, 87 | }, 88 | 89 | animation: { enabled: true, speed: 'normal' }, 90 | }; 91 | -------------------------------------------------------------------------------- /src/cli/themes/themes/dracula.ts: -------------------------------------------------------------------------------- 1 | import { ThemeConfig } from '../ThemeTypes.js'; 2 | 3 | export const draculaTheme: ThemeConfig = { 4 | name: 'dracula', 5 | displayName: 'Dracula', 6 | type: 'dark', 7 | colors: { 8 | primary: '#BD93F9', 9 | secondary: '#8BE9FD', 10 | accent: '#50FA7B', 11 | 12 | success: '#50FA7B', 13 | warning: '#F1FA8C', 14 | error: '#FF5555', 15 | info: '#8BE9FD', 16 | 17 | text: { 18 | primary: '#F8F8F2', 19 | secondary: '#F8F8F2', 20 | muted: '#6272A4', 21 | inverse: '#282A36', 22 | }, 23 | 24 | background: { 25 | primary: '#282A36', 26 | secondary: '#44475A', 27 | tertiary: '#6272A4', 28 | }, 29 | 30 | ui: { 31 | border: '#6272A4', 32 | separator: '#44475A', 33 | highlight: '#BD93F9', 34 | selection: '#44475A', 35 | progress: '#50FA7B', 36 | }, 37 | 38 | tools: { 39 | shell: '#50FA7B', 40 | file: '#F1FA8C', 41 | git: '#FFB86C', 42 | web: '#BD93F9', 43 | code: '#8BE9FD', 44 | }, 45 | 46 | react: { 47 | thought: '#8BE9FD', 48 | plan: '#50FA7B', 49 | action: '#FFB86C', 50 | observation: '#BD93F9', 51 | }, 52 | 53 | syntax: { 54 | keyword: '#FF79C6', 55 | string: '#F1FA8C', 56 | function: '#50FA7B', 57 | property: '#8BE9FD', 58 | punctuation: '#F8F8F2', 59 | comment: '#6272A4', 60 | number: '#BD93F9', 61 | operator: '#FF79C6', 62 | }, 63 | 64 | diff: { 65 | added: '#50FA7B', 66 | removed: '#FF5555', 67 | modified: '#FFB86C', 68 | context: '#6272A4', 69 | lineNumber: '#6272A4', 70 | }, 71 | 72 | semantic: { 73 | functionCall: '#BD93F9', 74 | parameter: '#8BE9FD', 75 | result: '#F8F8F2', 76 | metadata: '#6272A4', 77 | indicator: '#6272A4', 78 | }, 79 | }, 80 | 81 | fonts: { 82 | mono: true, 83 | size: 'medium', 84 | }, 85 | 86 | layout: { 87 | compact: false, 88 | showTimestamps: true, 89 | showProgress: true, 90 | }, 91 | 92 | animation: { 93 | enabled: true, 94 | speed: 'normal', 95 | }, 96 | }; -------------------------------------------------------------------------------- /src/cli/themes/themes/light.ts: -------------------------------------------------------------------------------- 1 | import { ThemeConfig } from '../ThemeTypes.js'; 2 | 3 | export const lightTheme: ThemeConfig = { 4 | name: 'light', 5 | displayName: 'VS Code Light+', 6 | type: 'light', 7 | colors: { 8 | primary: '#0066CC', 9 | secondary: '#AF00DB', 10 | accent: '#267F99', 11 | 12 | success: '#28A745', 13 | warning: '#F57C00', 14 | error: '#D32F2F', 15 | info: '#1976D2', 16 | 17 | text: { 18 | primary: '#000000', 19 | secondary: '#909696', 20 | muted: '#008000', 21 | inverse: '#FFFFFF', 22 | }, 23 | 24 | background: { 25 | primary: '#FFFFFF', 26 | secondary: '#F8F8F8', 27 | tertiary: '#F0F0F0', 28 | }, 29 | 30 | ui: { 31 | border: '#E5E5E5', 32 | separator: '#CCCCCC', 33 | highlight: '#0066CC20', 34 | selection: '#0066CC40', 35 | progress: '#0066CC', 36 | }, 37 | 38 | tools: { 39 | shell: '#28A745', 40 | file: '#F57C00', 41 | git: '#FF6B35', 42 | web: '#AF00DB', 43 | code: '#267F99', 44 | }, 45 | 46 | react: { 47 | thought: '#1976D2', 48 | plan: '#28A745', 49 | action: '#F57C00', 50 | observation: '#AF00DB', 51 | }, 52 | 53 | syntax: { 54 | keyword: '#0000FF', 55 | string: '#A31515', 56 | function: '#795E26', 57 | property: '#001080', 58 | punctuation: '#000000', 59 | comment: '#008000', 60 | number: '#098658', 61 | operator: '#000000', 62 | }, 63 | 64 | diff: { 65 | added: '#28A745', 66 | removed: '#DC3545', 67 | modified: '#FFC107', 68 | context: '#6C757D', 69 | lineNumber: '#6C757D', 70 | }, 71 | 72 | semantic: { 73 | functionCall: '#0066CC', 74 | parameter: '#001080', 75 | result: '#000000', 76 | metadata: '#008000', 77 | indicator: '#6C757D', 78 | }, 79 | }, 80 | 81 | fonts: { 82 | mono: true, 83 | size: 'medium', 84 | }, 85 | 86 | layout: { 87 | compact: false, 88 | showTimestamps: true, 89 | showProgress: true, 90 | }, 91 | 92 | animation: { 93 | enabled: true, 94 | speed: 'normal', 95 | }, 96 | }; -------------------------------------------------------------------------------- /src/cli/themes/themes/monokai.ts: -------------------------------------------------------------------------------- 1 | import { ThemeConfig } from '../ThemeTypes.js'; 2 | 3 | export const monokaiTheme: ThemeConfig = { 4 | name: 'monokai', 5 | displayName: 'Monokai Pro', 6 | type: 'dark', 7 | colors: { 8 | primary: '#F92672', 9 | secondary: '#A6E22E', 10 | accent: '#FD971F', 11 | 12 | success: '#A6E22E', 13 | warning: '#E6DB74', 14 | error: '#F92672', 15 | info: '#66D9EF', 16 | 17 | text: { 18 | primary: '#F8F8F2', 19 | secondary: '#CFCFC2', 20 | muted: '#75715E', 21 | inverse: '#272822', 22 | }, 23 | 24 | background: { 25 | primary: '#272822', 26 | secondary: '#3E3D32', 27 | tertiary: '#49483E', 28 | }, 29 | 30 | ui: { 31 | border: '#49483E', 32 | separator: '#3E3D32', 33 | highlight: '#49483E', 34 | selection: '#49483E', 35 | progress: '#A6E22E', 36 | }, 37 | 38 | tools: { 39 | shell: '#A6E22E', 40 | file: '#E6DB74', 41 | git: '#FD971F', 42 | web: '#AE81FF', 43 | code: '#66D9EF', 44 | }, 45 | 46 | react: { 47 | thought: '#66D9EF', 48 | plan: '#A6E22E', 49 | action: '#FD971F', 50 | observation: '#AE81FF', 51 | }, 52 | 53 | syntax: { 54 | keyword: '#F92672', 55 | string: '#E6DB74', 56 | function: '#A6E22E', 57 | property: '#FD971F', 58 | punctuation: '#F8F8F2', 59 | comment: '#75715E', 60 | number: '#AE81FF', 61 | operator: '#F92672', 62 | }, 63 | 64 | diff: { 65 | added: '#A6E22E', 66 | removed: '#F92672', 67 | modified: '#FD971F', 68 | context: '#75715E', 69 | lineNumber: '#75715E', 70 | }, 71 | 72 | semantic: { 73 | functionCall: '#F92672', 74 | parameter: '#FD971F', 75 | result: '#F8F8F2', 76 | metadata: '#75715E', 77 | indicator: '#75715E', 78 | }, 79 | }, 80 | 81 | fonts: { 82 | mono: true, 83 | size: 'medium', 84 | }, 85 | 86 | layout: { 87 | compact: false, 88 | showTimestamps: true, 89 | showProgress: true, 90 | }, 91 | 92 | animation: { 93 | enabled: true, 94 | speed: 'normal', 95 | }, 96 | }; -------------------------------------------------------------------------------- /src/cli/themes/themes/solarized.ts: -------------------------------------------------------------------------------- 1 | import { ThemeConfig } from '../ThemeTypes.js'; 2 | 3 | export const solarizedTheme: ThemeConfig = { 4 | name: 'solarized', 5 | displayName: 'Solarized Dark', 6 | type: 'dark', 7 | colors: { 8 | primary: '#268BD2', 9 | secondary: '#6C71C4', 10 | accent: '#2AA198', 11 | 12 | success: '#859900', 13 | warning: '#B58900', 14 | error: '#DC322F', 15 | info: '#268BD2', 16 | 17 | text: { 18 | primary: '#839496', 19 | secondary: '#93A1A1', 20 | muted: '#586E75', 21 | inverse: '#FDF6E3', 22 | }, 23 | 24 | background: { 25 | primary: '#002B36', 26 | secondary: '#073642', 27 | tertiary: '#586E75', 28 | }, 29 | 30 | ui: { 31 | border: '#586E75', 32 | separator: '#073642', 33 | highlight: '#268BD2', 34 | selection: '#073642', 35 | progress: '#2AA198', 36 | }, 37 | 38 | tools: { 39 | shell: '#859900', 40 | file: '#CB4B16', 41 | git: '#DC322F', 42 | web: '#6C71C4', 43 | code: '#2AA198', 44 | }, 45 | 46 | react: { 47 | thought: '#268BD2', 48 | plan: '#859900', 49 | action: '#B58900', 50 | observation: '#6C71C4', 51 | }, 52 | 53 | syntax: { 54 | keyword: '#859900', 55 | string: '#2AA198', 56 | function: '#268BD2', 57 | property: '#CB4B16', 58 | punctuation: '#93A1A1', 59 | comment: '#586E75', 60 | number: '#D33682', 61 | operator: '#6C71C4', 62 | }, 63 | 64 | diff: { 65 | added: '#859900', 66 | removed: '#DC322F', 67 | modified: '#B58900', 68 | context: '#586E75', 69 | lineNumber: '#586E75', 70 | }, 71 | 72 | semantic: { 73 | functionCall: '#268BD2', 74 | parameter: '#CB4B16', 75 | result: '#839496', 76 | metadata: '#586E75', 77 | indicator: '#586E75', 78 | }, 79 | }, 80 | 81 | fonts: { 82 | mono: true, 83 | size: 'medium', 84 | }, 85 | 86 | layout: { 87 | compact: false, 88 | showTimestamps: true, 89 | showProgress: true, 90 | }, 91 | 92 | animation: { 93 | enabled: true, 94 | speed: 'normal', 95 | }, 96 | }; -------------------------------------------------------------------------------- /src/cli/themes/themes/high-contrast.ts: -------------------------------------------------------------------------------- 1 | import { ThemeConfig } from '../ThemeTypes.js'; 2 | 3 | export const highContrastTheme: ThemeConfig = { 4 | name: 'high-contrast', 5 | displayName: 'High Contrast', 6 | type: 'dark', 7 | colors: { 8 | primary: '#FFFFFF', 9 | secondary: '#FFFF00', 10 | accent: '#00FFFF', 11 | 12 | success: '#00FF00', 13 | warning: '#FFFF00', 14 | error: '#FF0000', 15 | info: '#00FFFF', 16 | 17 | text: { 18 | primary: '#FFFFFF', 19 | secondary: '#FFFFFF', 20 | muted: '#CCCCCC', 21 | inverse: '#000000', 22 | }, 23 | 24 | background: { 25 | primary: '#000000', 26 | secondary: '#111111', 27 | tertiary: '#222222', 28 | }, 29 | 30 | ui: { 31 | border: '#FFFFFF', 32 | separator: '#FFFFFF', 33 | highlight: '#FFFF00', 34 | selection: '#0000FF', 35 | progress: '#00FF00', 36 | }, 37 | 38 | tools: { 39 | shell: '#00FF00', 40 | file: '#FFFF00', 41 | git: '#FF8800', 42 | web: '#FF00FF', 43 | code: '#00FFFF', 44 | }, 45 | 46 | react: { 47 | thought: '#00FFFF', 48 | plan: '#00FF00', 49 | action: '#FFFF00', 50 | observation: '#FF00FF', 51 | }, 52 | 53 | syntax: { 54 | keyword: '#FFFF00', 55 | string: '#00FF00', 56 | function: '#00FFFF', 57 | property: '#FF00FF', 58 | punctuation: '#FFFFFF', 59 | comment: '#CCCCCC', 60 | number: '#FF8800', 61 | operator: '#FFFF00', 62 | }, 63 | 64 | diff: { 65 | added: '#00FF00', 66 | removed: '#FF0000', 67 | modified: '#FFFF00', 68 | context: '#CCCCCC', 69 | lineNumber: '#CCCCCC', 70 | }, 71 | 72 | semantic: { 73 | functionCall: '#FFFFFF', 74 | parameter: '#FF00FF', 75 | result: '#FFFFFF', 76 | metadata: '#CCCCCC', 77 | indicator: '#CCCCCC', 78 | }, 79 | }, 80 | 81 | fonts: { 82 | mono: true, 83 | size: 'large', 84 | }, 85 | 86 | layout: { 87 | compact: false, 88 | showTimestamps: true, 89 | showProgress: true, 90 | }, 91 | 92 | animation: { 93 | enabled: false, 94 | speed: 'slow', 95 | }, 96 | }; -------------------------------------------------------------------------------- /src/cli/components/WelcomeScreen.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box, Text, useInput } from 'ink'; 3 | import { useTheme } from '../themes/index.js'; 4 | import { MAX_FRAME_WIDTH } from './base.js'; 5 | 6 | interface WelcomeScreenProps { 7 | onDismiss: () => void; 8 | } 9 | 10 | export const WelcomeScreen: React.FC = ({ onDismiss }) => { 11 | const { currentTheme } = useTheme(); 12 | 13 | useInput(() => onDismiss()); 14 | 15 | React.useEffect(() => { 16 | const t = setTimeout(onDismiss, 3000); 17 | return () => clearTimeout(t); 18 | }, [onDismiss]); 19 | 20 | const logo = String.raw` 21 | _____ _ 22 | |_ _| (_) 23 | | | ___ _ __ ___ _ __ _ _ _ __ __ _ _ 24 | | |/ _ \ '_ \` _ \| '_ \| | | | '__/ _\` | | 25 | | | __/ | | | | | |_) | |_| | | | (_| | | 26 | \_/\___|_| |_| |_| .__/ \__,_|_| \__,_|_| 27 | | | 28 | |_| 29 | `.trim(); 30 | 31 | const c = currentTheme?.colors ?? ({} as any); 32 | const border = c.ui?.border ?? 'gray'; 33 | const primary = c.primary ?? 'cyan'; 34 | const textSecondary = c.text?.secondary ?? 'white'; 35 | const textMuted = c.text?.muted ?? 'gray'; 36 | 37 | return ( 38 | 39 | 40 | 41 | 42 | {logo.split('\n').map((line, i) => ( 43 | 44 | {line} 45 | 46 | ))} 47 | 48 | 49 | 50 | 51 | Initializing enhanced interface... 52 | Press any key to continue 53 | 54 | 55 | 56 | ); 57 | }; 58 | -------------------------------------------------------------------------------- /src/events/UIEventEmitter.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events'; 2 | import { injectable } from 'inversify'; 3 | import { 4 | UIEvent, 5 | UIEventType, 6 | EventListener, 7 | EventSubscription, 8 | } from './EventTypes.js'; 9 | 10 | @injectable() 11 | export class UIEventEmitter { 12 | private emitter = new EventEmitter(); 13 | private sessionId: string; 14 | private eventCounter = 0; 15 | 16 | constructor(sessionId?: string) { 17 | this.sessionId = sessionId || `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; 18 | this.emitter.setMaxListeners(50); 19 | } 20 | 21 | emit(event: Omit): void { 22 | const fullEvent = { 23 | ...event, 24 | id: this.generateEventId(), 25 | timestamp: new Date(), 26 | sessionId: this.sessionId, 27 | } as UIEvent; 28 | 29 | console.log(`Emitting event: ${event.type}`, fullEvent); 30 | this.emitter.emit(event.type, fullEvent); 31 | this.emitter.emit('*', fullEvent); 32 | } 33 | 34 | on(eventType: T['type'], listener: EventListener): EventSubscription { 35 | this.emitter.on(eventType, listener); 36 | return { 37 | unsubscribe: () => this.emitter.removeListener(eventType, listener), 38 | }; 39 | } 40 | 41 | onAll(listener: EventListener): EventSubscription { 42 | this.emitter.on('*', listener); 43 | return { 44 | unsubscribe: () => this.emitter.removeListener('*', listener), 45 | }; 46 | } 47 | 48 | once(eventType: T['type'], listener: EventListener): EventSubscription { 49 | this.emitter.once(eventType, listener); 50 | return { 51 | unsubscribe: () => this.emitter.removeListener(eventType, listener), 52 | }; 53 | } 54 | 55 | clear(): void { 56 | this.emitter.removeAllListeners(); 57 | } 58 | 59 | getListenerCount(eventType?: UIEventType): number { 60 | if (eventType) { 61 | return this.emitter.listenerCount(eventType); 62 | } 63 | return this.emitter.eventNames().reduce((sum, name) => sum + this.emitter.listenerCount(name), 0); 64 | } 65 | 66 | private generateEventId(): string { 67 | return `${this.sessionId}_${++this.eventCounter}_${Date.now()}`; 68 | } 69 | 70 | getSessionId(): string { 71 | return this.sessionId; 72 | } 73 | 74 | } -------------------------------------------------------------------------------- /src/cli/components/EventItem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box, Text } from 'ink'; 3 | import { useTheme } from '../themes/index.js'; 4 | import { CLIEvent, CLISymbol } from '../hooks/useSessionEvents.js'; 5 | 6 | interface EventItemProps { 7 | event: CLIEvent; 8 | index: number; 9 | } 10 | 11 | export const EventItem: React.FC = React.memo(({ event }) => { 12 | const { currentTheme } = useTheme(); 13 | 14 | const getSymbolColor = (symbol: CLISymbol) => { 15 | switch (symbol) { 16 | case CLISymbol.USER_INPUT: 17 | return currentTheme.colors.semantic.functionCall; 18 | case CLISymbol.AI_RESPONSE: 19 | return currentTheme.colors.info; 20 | case CLISymbol.TOOL_EXECUTING: 21 | return currentTheme.colors.warning; 22 | case CLISymbol.TOOL_SUCCESS: 23 | return currentTheme.colors.success; 24 | case CLISymbol.TOOL_FAILED: 25 | case CLISymbol.SYSTEM_ERROR: 26 | return currentTheme.colors.error; 27 | default: 28 | return currentTheme.colors.text.primary; 29 | } 30 | }; 31 | 32 | const getContentColor = (symbol: CLISymbol) => { 33 | switch (symbol) { 34 | case CLISymbol.TOOL_FAILED: 35 | case CLISymbol.SYSTEM_ERROR: 36 | return currentTheme.colors.error; 37 | default: 38 | return currentTheme.colors.text.primary; 39 | } 40 | }; 41 | 42 | return ( 43 | 44 | 45 | {event.symbol} 46 | 47 | 48 | {event.content} 49 | 50 | 51 | 52 | 53 | {event.subEvent && 54 | event.subEvent.map((subItem, index) => ( 55 | 56 | {' '}L 57 | 58 | 59 | {subItem.content} 60 | 61 | 62 | 63 | ))} 64 | 65 | ); 66 | }); 67 | -------------------------------------------------------------------------------- /src/cli/components/events/SystemEventItem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box, Text } from 'ink'; 3 | import { CLIEvent, CLISymbol } from '../../hooks/useSessionEvents.js'; 4 | import { useTheme } from '../../themes/index.js'; 5 | 6 | interface SystemEventItemProps { 7 | event: CLIEvent; 8 | index: number; 9 | } 10 | 11 | export const SystemEventItem: React.FC = ({ event }) => { 12 | const { currentTheme } = useTheme(); 13 | 14 | const getSymbolColor = (symbol: CLISymbol) => { 15 | switch (symbol) { 16 | case CLISymbol.USER_INPUT: 17 | return currentTheme.colors.semantic.functionCall; 18 | case CLISymbol.AI_RESPONSE: 19 | return currentTheme.colors.info; 20 | case CLISymbol.TOOL_EXECUTING: 21 | return currentTheme.colors.warning; 22 | case CLISymbol.TOOL_SUCCESS: 23 | return currentTheme.colors.success; 24 | case CLISymbol.TOOL_FAILED: 25 | case CLISymbol.SYSTEM_ERROR: 26 | return currentTheme.colors.error; 27 | default: 28 | return currentTheme.colors.text.primary; 29 | } 30 | }; 31 | 32 | const getContentColor = (symbol: CLISymbol) => { 33 | switch (symbol) { 34 | case CLISymbol.TOOL_FAILED: 35 | case CLISymbol.SYSTEM_ERROR: 36 | return currentTheme.colors.error; 37 | default: 38 | return currentTheme.colors.text.primary; 39 | } 40 | }; 41 | 42 | return ( 43 | 44 | 45 | {event.symbol} 46 | 47 | 48 | {event.content} 49 | 50 | 51 | 52 | 53 | {event.subEvent && 54 | event.subEvent.map((subItem, index) => ( 55 | 56 | {' '}L 57 | 58 | 59 | {subItem.content} 60 | 61 | 62 | 63 | ))} 64 | 65 | ); 66 | }; 67 | -------------------------------------------------------------------------------- /src/cli/components/HelpPanel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box, Text, useInput } from 'ink'; 3 | import { useTheme } from '../themes/index.js'; 4 | import { useUiStore } from '../stores/uiStore.js'; 5 | 6 | interface HelpPanelProps { 7 | onCancel: () => void; 8 | isFocused: boolean; 9 | } 10 | 11 | export const HelpPanel: React.FC = ({ onCancel, isFocused }) => { 12 | const { currentTheme } = useTheme(); 13 | const { setActivePanel } = useUiStore((state) => state.actions); 14 | 15 | useInput( 16 | (input, key) => { 17 | if (input === '?') { 18 | setActivePanel('INPUT', '?'); 19 | return; 20 | } 21 | if (key.escape || key.return) { 22 | onCancel(); 23 | } 24 | }, 25 | { isActive: isFocused }, 26 | ); 27 | 28 | return ( 29 | 30 | 31 | 32 | :{'. '} - Select execution mode 33 | 34 | 35 | /help{' '} - Show available commands 36 | 37 | 38 | /theme{' '} - Switch theme 39 | 40 | 41 | /mode{' '} - Show current modes 42 | 43 | 44 | Shift+Tab{' '} - Cycle edit mode 45 | 46 | 47 | Ctrl+C{'. '} - Exit application 48 | 49 | 50 | 51 | 52 | Enter or Esc to close 53 | 54 | 55 | 56 | ); 57 | }; 58 | -------------------------------------------------------------------------------- /src/services/CompressorService.ts: -------------------------------------------------------------------------------- 1 | import { injectable, inject } from 'inversify'; 2 | import { TYPES } from '../di/types.js'; 3 | import { CompressedAgent } from '../agents/compressed_agent/CompressedAgent.js'; 4 | import { Messages } from '../agents/tool_agent/ToolAgent.js'; 5 | import { encode } from 'gpt-tokenizer'; 6 | 7 | @injectable() 8 | export class CompressorService { 9 | private readonly maxTokens = 30000; 10 | private readonly preserveRecentCount = 8; 11 | private readonly intelligentThreshold = 0.85; // 85% threshold for AI decision 12 | private readonly forceThreshold = 0.95; // 95% threshold for forced compression 13 | private readonly minCompressionInterval = 30000; // 30 seconds 14 | public lastCompressionTime: number = 0; 15 | 16 | constructor( 17 | @inject(TYPES.CompressedAgent) private compressedAgent: CompressedAgent, 18 | ) { } 19 | 20 | async compressContextIfNeeded(history: Messages): Promise { 21 | if (history.length <= this.preserveRecentCount) { 22 | return history 23 | } 24 | 25 | const totalTokens = this.calculateTokens(history); 26 | 27 | // Force compression if token count is too high 28 | if (totalTokens > this.maxTokens * this.forceThreshold) { 29 | console.log(`强制压缩触发 (${totalTokens} tokens)`); 30 | return await this.performCompression(history); 31 | } 32 | 33 | // For moderate token usage, check timing and ask AI 34 | if (totalTokens > this.maxTokens * this.intelligentThreshold) { 35 | const timeSinceLastCompression = Date.now() - this.lastCompressionTime; 36 | if (timeSinceLastCompression < this.minCompressionInterval) { 37 | return history 38 | } 39 | 40 | const shouldCompress = await this.compressedAgent.shouldCompress(totalTokens, history); 41 | 42 | if (shouldCompress) { 43 | console.log(`AI建议压缩 (${totalTokens} tokens)`); 44 | return await this.performCompression(history); 45 | } 46 | } 47 | 48 | return history 49 | } 50 | 51 | private async performCompression(history: Messages): Promise { 52 | const toCompress = history.slice(0, -this.preserveRecentCount); 53 | const toKeep = history.slice(-this.preserveRecentCount); 54 | 55 | const compressed = await this.compressedAgent.compress( 56 | toCompress 57 | ); 58 | 59 | this.lastCompressionTime = Date.now(); 60 | 61 | let compressedHistory = [...compressed, ...toKeep]; 62 | return compressedHistory; 63 | } 64 | 65 | private calculateTokens(messages: Messages): number { 66 | const text = messages.map(m => m.content).join(''); 67 | return encode(text).length; 68 | } 69 | } -------------------------------------------------------------------------------- /scripts/README.md: -------------------------------------------------------------------------------- 1 | # NPM Publishing Guide 2 | 3 | This guide explains how to publish @tempurai/coder to the npm registry using the automated publishing script. 4 | 5 | ## Quick Start 6 | 7 | ```bash 8 | # Full publish workflow 9 | npm run publish:npm 10 | 11 | # Dry run (test without publishing) 12 | npm run publish:dry-run 13 | 14 | # Only run tests 15 | npm run publish:test 16 | 17 | # Only build the project 18 | npm run publish:build 19 | 20 | # Clean up build artifacts 21 | npm run publish:cleanup 22 | ``` 23 | 24 | ## Prerequisites 25 | 26 | 1. **Node.js 18+** installed 27 | 2. **npm login** completed (`npm login`) 28 | 3. **Clean git working directory** (all changes committed) 29 | 4. **Valid version** in package.json (not already published) 30 | 31 | ## Environment Variables 32 | 33 | Optional environment variables: 34 | 35 | ```bash 36 | # NPM authentication (alternative to npm login) 37 | export NPM_TOKEN="your-npm-token" 38 | 39 | # Skip certain steps 40 | export SKIP_TESTS=true 41 | export SKIP_BUILD=true 42 | ``` 43 | 44 | ## Manual Publishing (Backup) 45 | 46 | If the automated script fails, you can publish manually: 47 | 48 | ```bash 49 | # 1. Clean and build 50 | npm run clean 51 | npm run build 52 | 53 | # 2. Run tests 54 | npm test 55 | 56 | # 3. Publish 57 | npm publish --access public 58 | ``` 59 | 60 | ## Troubleshooting 61 | 62 | ### Common Issues 63 | 64 | **Version already exists** 65 | 66 | ```bash 67 | # Error: Version 1.0.0 already exists 68 | npm version patch # Bump version first 69 | ``` 70 | 71 | **Not logged in** 72 | 73 | ```bash 74 | npm login # Login to npm registry 75 | ``` 76 | 77 | **Build failures** 78 | 79 | ```bash 80 | npm run clean # Clean previous builds 81 | npm install # Reinstall dependencies 82 | npm run build # Rebuild project 83 | ``` 84 | 85 | **Permission errors** 86 | 87 | ```bash 88 | chmod +x scripts/npm-registry.sh # Make script executable 89 | ``` 90 | 91 | ### Script Options 92 | 93 | ```bash 94 | ./scripts/npm-registry.sh --help # Show help 95 | ./scripts/npm-registry.sh --dry-run # Test run without publishing 96 | ./scripts/npm-registry.sh --cleanup # Only cleanup files 97 | ./scripts/npm-registry.sh --test # Only run tests 98 | ./scripts/npm-registry.sh --build # Only build project 99 | ``` 100 | 101 | ## Post-Publication 102 | 103 | After successful publication: 104 | 105 | 1. **Test Installation** 106 | 107 | ```bash 108 | npm install -g @tempurai/coder 109 | coder --help 110 | ``` 111 | 112 | 2. **Verify Package** 113 | - Check [npmjs.com](https://www.npmjs.com/package/@tempurai/coder) 114 | - Test CLI functionality 115 | - Monitor download statistics 116 | 117 | 3. **Update Documentation** 118 | - Update changelog 119 | - Create release notes 120 | - Notify users of new version 121 | -------------------------------------------------------------------------------- /src/config/templates/example-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "models": [ 3 | { 4 | "provider": "openai", 5 | "name": "gpt-4o-mini", 6 | "apiKey": "your-openai-api-key-here" 7 | } 8 | ], 9 | "temperature": 0.3, 10 | "maxTokens": 4096, 11 | "mcpServers": {}, 12 | "tools": { 13 | "tavilyApiKey": "your-tavily-api-key-here", 14 | "shellExecutor": { 15 | "defaultTimeout": 30000, 16 | "maxRetries": 3, 17 | "security": { 18 | "allowlist": [ 19 | "git", 20 | "npm", 21 | "node", 22 | "pnpm", 23 | "yarn", 24 | "ls", 25 | "cat", 26 | "echo", 27 | "mkdir", 28 | "touch", 29 | "find", 30 | "grep", 31 | "awk", 32 | "sed", 33 | "ps", 34 | "tail", 35 | "head", 36 | "pwd", 37 | "whoami", 38 | "id", 39 | "uname", 40 | "date", 41 | "stat", 42 | "du", 43 | "df", 44 | "wc", 45 | "cut", 46 | "tr", 47 | "sort", 48 | "uniq", 49 | "xargs", 50 | "less", 51 | "env", 52 | "printenv", 53 | "tar", 54 | "gzip", 55 | "gunzip", 56 | "zip", 57 | "unzip", 58 | "cp", 59 | "mv", 60 | "npx", 61 | "curl", 62 | "wget", 63 | "scp", 64 | "sftp", 65 | "rsync", 66 | "ssh", 67 | "telnet", 68 | "nc", 69 | "netcat", 70 | "socat", 71 | "docker", 72 | "podman", 73 | "kubectl" 74 | ], 75 | 76 | "blocklist": [ 77 | "rm", 78 | "sudo", 79 | "chmod", 80 | "chown", 81 | "dd", 82 | "format", 83 | "del", 84 | "deltree", 85 | "mkfs.*", 86 | "fdisk", 87 | "sfdisk", 88 | "parted", 89 | "losetup", 90 | "cryptsetup", 91 | "mount", 92 | "umount", 93 | "swapon", 94 | "swapoff", 95 | "systemctl", 96 | "service", 97 | "launchctl", 98 | "init", 99 | "sysctl", 100 | "shutdown", 101 | "reboot", 102 | "halt", 103 | "poweroff", 104 | "killall", 105 | "pkill", 106 | "nohup", 107 | "daemonize", 108 | "setsid", 109 | "screen", 110 | "tmux", 111 | "tcpdump", 112 | "nmap", 113 | "arp", 114 | "iptables", 115 | "nft", 116 | "ip", 117 | "route" 118 | ], 119 | "allowUnlistedCommands": false, 120 | "allowDangerousCommands": false 121 | } 122 | }, 123 | "webTools": { 124 | "requestTimeout": 15000, 125 | "maxContentLength": 10000, 126 | "userAgent": "Tempurai-Bot/1.0 (Security-Enhanced)", 127 | "enableCache": false 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/test/dynamic-model-test.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /** 3 | * 测试动态模型加载功能 4 | */ 5 | 6 | require('ts-node/register'); 7 | const { ConfigLoader } = require('../config/ConfigLoader.ts'); 8 | 9 | async function testDynamicModelLoading() { 10 | console.log('🔧 测试动态模型加载功能...\n'); 11 | 12 | try { 13 | const configLoader = new ConfigLoader(); 14 | console.log('✅ ConfigLoader 实例创建成功'); 15 | 16 | // 测试模型显示名称 17 | console.log('📝 测试模型配置解析:'); 18 | const displayName = configLoader.getModelDisplayName(); 19 | console.log(` 当前模型显示名称: ${displayName}`); 20 | 21 | // 测试配置规范化 22 | const config = configLoader.getConfig(); 23 | console.log(` 配置中的模型设置: ${typeof config.model === 'string' ? config.model : `${config.model.provider}:${config.model.name}`}`); 24 | 25 | // 测试模型创建(需要API Key) 26 | console.log('\n🤖 测试模型实例创建:'); 27 | try { 28 | console.log(' 正在创建模型实例...'); 29 | const model = await configLoader.createLanguageModel(); 30 | console.log(' ✅ 模型实例创建成功'); 31 | console.log(' 模型类型:', typeof model); 32 | console.log(' 模型原型:', Object.getPrototypeOf(model).constructor.name); 33 | } catch (modelError) { 34 | console.log(` ⚠️ 模型实例创建失败: ${modelError.message}`); 35 | console.log(' 💡 这是正常的,可能缺少 API Key 或网络连接问题'); 36 | } 37 | 38 | // 测试不同模型配置格式 39 | console.log('\n🔄 测试不同模型配置格式:'); 40 | 41 | // 字符串格式(向后兼容) 42 | const stringModelConfig = { ...config, model: 'gpt-4o-mini' }; 43 | console.log(' 字符串格式:', stringModelConfig.model); 44 | 45 | // 对象格式 46 | const objectModelConfig = { 47 | ...config, 48 | model: { 49 | provider: 'openai', 50 | name: 'gpt-4o-mini', 51 | apiKey: config.apiKey, 52 | }, 53 | }; 54 | console.log(' 对象格式:', `${objectModelConfig.model.provider}:${objectModelConfig.model.name}`); 55 | 56 | // 测试不同提供商的模型名称推断 57 | console.log('\n🎯 测试模型提供商推断:'); 58 | const testModels = ['gpt-4o-mini', 'gpt-3.5-turbo', 'gemini-1.5-pro', 'claude-3-5-sonnet-20241022', 'command-r-plus', 'mistral-large-latest']; 59 | 60 | testModels.forEach((modelName) => { 61 | // 临时修改配置来测试推断 62 | const tempLoader = Object.create(ConfigLoader.prototype); 63 | tempLoader.config = { model: modelName }; 64 | 65 | // 使用私有方法测试(通过 prototype) 66 | const normalizeMethod = ConfigLoader.prototype.normalizeModelConfig; 67 | if (normalizeMethod) { 68 | try { 69 | const normalized = normalizeMethod.call(tempLoader, modelName); 70 | console.log(` ${modelName} -> ${normalized.provider}:${normalized.name}`); 71 | } catch (e) { 72 | console.log(` ${modelName} -> 推断失败`); 73 | } 74 | } 75 | }); 76 | 77 | console.log('\n🎉 动态模型加载测试完成!'); 78 | console.log('\n📋 功能总结:'); 79 | console.log(' ✅ 支持字符串和对象两种模型配置格式'); 80 | console.log(' ✅ 自动推断模型提供商'); 81 | console.log(' ✅ 动态创建不同提供商的模型实例'); 82 | console.log(' ✅ 向后兼容现有配置'); 83 | console.log(' ✅ 环境变量和配置文件 API Key 支持'); 84 | } catch (error) { 85 | console.error('❌ 测试失败:', error.message); 86 | console.error('详细错误:', error); 87 | } 88 | } 89 | 90 | testDynamicModelLoading().catch(console.error); 91 | -------------------------------------------------------------------------------- /src/agents/smart_agent/AgentOrchestrator.ts: -------------------------------------------------------------------------------- 1 | import { ToolAgent, Messages } from '../tool_agent/ToolAgent.js'; 2 | import { UIEventEmitter } from '../../events/UIEventEmitter.js'; 3 | import { z } from "zod"; 4 | import { inject } from 'inversify'; 5 | import { TYPES } from '../../di/types.js'; 6 | 7 | export const LoopDetectionSchema = z.object({ 8 | isLoop: z.boolean(), 9 | confidence: z.number().min(0).max(100), 10 | description: z.string().optional() 11 | }); 12 | 13 | export type LoopDetectionResult = z.infer; 14 | 15 | const LOOP_DETECTION_PROMPT = `Check if the assistant is stuck in a repetitive loop by analyzing the last few assistant responses. 16 | 17 | **IMPORTANT: Be less strict about loops during planning phases. Look for:** 18 | - Repeating the same tools with identical parameters AND getting identical results with no progress 19 | - Making the same exact errors repeatedly without adaptation 20 | - Stuck in the same reasoning pattern for 3+ iterations with no advancement 21 | 22 | **NOT loops:** 23 | - Using todo_manager multiple times during initial planning (this is normal workflow) 24 | - Reading multiple related files during investigation 25 | - Trying different approaches to solve a problem 26 | - Systematic execution of a plan with clear progress 27 | 28 | **Only flag as loops when:** 29 | 1. Same tool + same parameters + same results repeated 3+ times 30 | 2. Clear evidence of no progress toward the goal 31 | 3. Identical reasoning patterns with no new insights 32 | 33 | Respond with JSON: {"isLoop": boolean, "confidence": 0-100, "description": "optional explanation"}`; 34 | 35 | export class AgentOrchestrator { 36 | constructor( 37 | @inject(TYPES.ToolAgent) private toolAgent: ToolAgent, 38 | @inject(TYPES.UIEventEmitter) private eventEmitter: UIEventEmitter, 39 | ) { } 40 | 41 | async detectLoop(conversationHistory: Messages): Promise { 42 | // Only check for loops if we have enough history 43 | if (conversationHistory.length < 8) { 44 | return { isLoop: false, confidence: 0 }; 45 | } 46 | 47 | // Get recent assistant messages 48 | const recentAssistantMessages = conversationHistory 49 | .filter(turn => turn.role === 'assistant') 50 | .slice(-4); // Last 4 assistant turns 51 | 52 | if (recentAssistantMessages.length < 3) { 53 | return { isLoop: false, confidence: 0 }; 54 | } 55 | 56 | const historyText = recentAssistantMessages 57 | .map((msg, i) => `Response ${i + 1}: ${msg.content}`) 58 | .join('\n---\n'); 59 | 60 | try { 61 | const messages: Messages = [ 62 | { role: 'system', content: LOOP_DETECTION_PROMPT }, 63 | { role: 'user', content: `Recent assistant responses:\n${historyText}` } 64 | ]; 65 | 66 | return await this.toolAgent.generateObject({ 67 | messages, 68 | schema: LoopDetectionSchema, 69 | allowTools: false 70 | }); 71 | } catch (error) { 72 | console.warn('Loop detection failed:', error); 73 | return { 74 | isLoop: false, 75 | confidence: 0, 76 | description: 'Loop detection failed' 77 | }; 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /src/cli/components/ExecutionModeSelector.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Box, Text, useInput } from 'ink'; 3 | import { useTheme } from '../themes/index.js'; 4 | import { ExecutionMode, ExecutionModeData } from '../../services/ExecutionModeManager.js'; 5 | import { useUiStore } from '../stores/uiStore.js'; 6 | 7 | interface ExecutionModeSelectorProps { 8 | currentMode: ExecutionMode; 9 | onModeSelected: (mode: ExecutionMode) => void; 10 | onCancel: () => void; 11 | isFocused: boolean; 12 | } 13 | 14 | export const ExecutionModeSelector: React.FC = ({ currentMode, onModeSelected, onCancel, isFocused }) => { 15 | const { currentTheme } = useTheme(); 16 | const { setActivePanel } = useUiStore((state) => state.actions); 17 | const [selectedIndex, setSelectedIndex] = useState(0); 18 | 19 | useEffect(() => { 20 | const currentModeIndex = ExecutionModeData.findIndex((m) => m.mode === currentMode); 21 | if (currentModeIndex !== -1) { 22 | setSelectedIndex(currentModeIndex); 23 | } 24 | }, [currentMode]); 25 | 26 | useInput( 27 | (input, key) => { 28 | if (input === ':') { 29 | setActivePanel('INPUT', ':'); 30 | return; 31 | } 32 | if (key.upArrow) { 33 | setSelectedIndex((prev) => (prev > 0 ? prev - 1 : ExecutionModeData.length - 1)); 34 | } else if (key.downArrow) { 35 | setSelectedIndex((prev) => (prev < ExecutionModeData.length - 1 ? prev + 1 : 0)); 36 | } else if (key.return) { 37 | onModeSelected(ExecutionModeData[selectedIndex].mode); 38 | } else if (key.escape) { 39 | onCancel(); 40 | } else if (input === '1' || input === '2') { 41 | const index = parseInt(input) - 1; 42 | if (index >= 0 && index < ExecutionModeData.length) { 43 | onModeSelected(ExecutionModeData[index].mode); 44 | } 45 | } 46 | }, 47 | { isActive: isFocused }, 48 | ); 49 | 50 | return ( 51 | 52 | 53 | 54 | Select Execution Mode 55 | 56 | 57 | 58 | {ExecutionModeData.map((modeData, index) => ( 59 | 60 | 61 | 62 | {index === selectedIndex ? '⏵ ' : ' '} 63 | {index + 1}. {modeData.displayName} 64 | {modeData.mode === currentMode ? ' (current)' : ''} 65 | 66 | 67 | {modeData.description} 68 | 69 | ))} 70 | 71 | 72 | 73 | ↑/↓ Navigate • Enter Select •1/2 Quick select • 74 | Esc Cancel 75 | 76 | 77 | 78 | ); 79 | }; 80 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tempurai/coder", 3 | "version": "0.2.2", 4 | "description": "A next-generation intelligent code assistant built with TypeScript, featuring Multi Agent methodology and advanced CLI interface", 5 | "type": "module", 6 | "main": "dist/cli/index.js", 7 | "bin": { 8 | "coder": "dist/cli/index.js" 9 | }, 10 | "files": [ 11 | "dist/**/*", 12 | "README.md", 13 | "LICENSE" 14 | ], 15 | "scripts": { 16 | "test": "jest", 17 | "build": "tsc", 18 | "build:watch": "tsc --watch", 19 | "dev": "tsx src/cli/index.ts", 20 | "prepare": "npm run build", 21 | "prepublishOnly": "npm run build", 22 | "clean": "rm -rf dist", 23 | "publish:npm": "./scripts/npm-registry.sh", 24 | "publish:dry-run": "./scripts/npm-registry.sh --dry-run", 25 | "publish:test": "./scripts/npm-registry.sh --test", 26 | "publish:build": "./scripts/npm-registry.sh --build", 27 | "publish:cleanup": "./scripts/npm-registry.sh --cleanup" 28 | }, 29 | "keywords": [ 30 | "ai", 31 | "code-assistant", 32 | "typescript", 33 | "react", 34 | "cli", 35 | "coding", 36 | "programming", 37 | "developer-tools", 38 | "code-generation", 39 | "automation", 40 | "openai", 41 | "anthropic", 42 | "claude", 43 | "gpt", 44 | "terminal", 45 | "tempurai", 46 | "shell", 47 | "git", 48 | "refactoring" 49 | ], 50 | "author": { 51 | "name": "Tempurai Team", 52 | "email": "team@tempur.ai", 53 | "url": "https://tempur.ai" 54 | }, 55 | "license": "Apache-2.0", 56 | "homepage": "https://github.com/tempurai/coder#readme", 57 | "repository": { 58 | "type": "git", 59 | "url": "git+https://github.com/tempurai/coder.git" 60 | }, 61 | "bugs": { 62 | "url": "https://github.com/tempurai/coder/issues" 63 | }, 64 | "engines": { 65 | "node": ">=18.0.0" 66 | }, 67 | "devDependencies": { 68 | "@sindresorhus/tsconfig": "^8.0.1", 69 | "@types/cors": "^2.8.19", 70 | "@types/express": "^5.0.3", 71 | "@types/html-to-text": "^9.0.4", 72 | "@types/jest": "^29.5.12", 73 | "@types/node": "^24.3.0", 74 | "concurrently": "^9.2.1", 75 | "jest": "^29.7.0", 76 | "ts-jest": "^29.1.2", 77 | "ts-node": "^10.9.2", 78 | "tsx": "^4.20.5", 79 | "typescript": "^5.9.2" 80 | }, 81 | "dependencies": { 82 | "@ai-sdk/anthropic": "^2.0.9", 83 | "@ai-sdk/cohere": "^2.0.7", 84 | "@ai-sdk/deepseek": "^1.0.13", 85 | "@ai-sdk/google": "^2.0.11", 86 | "@ai-sdk/mistral": "^2.0.12", 87 | "@ai-sdk/openai": "^2.0.23", 88 | "@babel/parser": "^7.28.3", 89 | "@babel/traverse": "^7.28.3", 90 | "@fastify/deepmerge": "^3.1.0", 91 | "@modelcontextprotocol/sdk": "^1.17.4", 92 | "@types/babel__traverse": "^7.28.0", 93 | "@types/js-yaml": "^4.0.9", 94 | "@types/react": "^19.1.12", 95 | "@typescript-eslint/parser": "^8.41.0", 96 | "@typescript-eslint/typescript-estree": "^8.41.0", 97 | "acorn": "^8.15.0", 98 | "acorn-walk": "^8.3.4", 99 | "ai": "^5.0.28", 100 | "cors": "^2.8.5", 101 | "dotenv": "^17.2.1", 102 | "express": "^5.1.0", 103 | "fast-glob": "^3.3.3", 104 | "fast-xml-parser": "^4.5.3", 105 | "gpt-tokenizer": "^3.0.1", 106 | "html-to-text": "^9.0.5", 107 | "ink": "^6.2.3", 108 | "ink-spinner": "^5.0.0", 109 | "ink-text-input": "^6.0.0", 110 | "inversify": "^7.9.1", 111 | "js-yaml": "^4.1.0", 112 | "prettier": "^3.6.2", 113 | "react": "^19.1.1", 114 | "reflect-metadata": "^0.2.2", 115 | "socket.io": "^4.8.1", 116 | "undici": "^7.15.0", 117 | "zod": "^3.25.76", 118 | "zod-to-json-schema": "^3.24.6", 119 | "zustand": "^5.0.8" 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/config/ConfigInitializer.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | import * as os from 'os'; 4 | import { injectable } from 'inversify'; 5 | import { fileURLToPath } from 'url'; 6 | 7 | @injectable() 8 | export class ConfigInitializer { 9 | private readonly globalConfigDir: string; 10 | private readonly globalConfigFilePath: string; 11 | private readonly globalContextFilePath: string; 12 | private readonly projectConfigDir: string; 13 | private readonly projectConfigFilePath: string; 14 | private readonly projectContextFilePath: string; 15 | private readonly templatesDir: string; 16 | 17 | constructor() { 18 | this.globalConfigDir = path.join(os.homedir(), '.tempurai'); 19 | this.globalConfigFilePath = path.join(this.globalConfigDir, 'config.json'); 20 | this.globalContextFilePath = path.join(this.globalConfigDir, '.tempurai.md'); 21 | this.projectConfigDir = path.join(process.cwd(), '.tempurai'); 22 | this.projectConfigFilePath = path.join(this.projectConfigDir, 'config.json'); 23 | this.projectContextFilePath = path.join(this.projectConfigDir, '.tempurai.md'); 24 | 25 | // 模板文件目录 26 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 27 | this.templatesDir = path.join(__dirname, 'templates'); 28 | } 29 | 30 | globalConfigExists(): boolean { 31 | return fs.existsSync(this.globalConfigFilePath); 32 | } 33 | 34 | projectConfigExists(): boolean { 35 | return fs.existsSync(this.projectConfigFilePath); 36 | } 37 | 38 | createGlobalFiles(): void { 39 | try { 40 | fs.mkdirSync(this.globalConfigDir, { recursive: true }); 41 | 42 | // 复制配置模板 43 | const configTemplatePath = path.join(this.templatesDir, 'example-config.json'); 44 | fs.copyFileSync(configTemplatePath, this.globalConfigFilePath); 45 | 46 | // 复制上下文模板 47 | const contextTemplatePath = path.join(this.templatesDir, 'example.tempurai.md'); 48 | fs.copyFileSync(contextTemplatePath, this.globalContextFilePath); 49 | 50 | console.log(`Created global config at ${this.globalConfigFilePath}`); 51 | console.log(`Created global context at ${this.globalContextFilePath}`); 52 | console.log('Please edit these files to add your API keys and customize settings.'); 53 | } catch (error) { 54 | console.error(`❌ Failed to create global config: ${error instanceof Error ? error.message : 'Unknown error'}`); 55 | throw error; 56 | } 57 | } 58 | 59 | createProjectFiles(): void { 60 | try { 61 | fs.mkdirSync(this.projectConfigDir, { recursive: true }); 62 | 63 | // 复制配置模板 64 | const configTemplatePath = path.join(this.templatesDir, 'example-config.json'); 65 | fs.copyFileSync(configTemplatePath, this.projectConfigFilePath); 66 | 67 | // 复制上下文模板 68 | const contextTemplatePath = path.join(this.templatesDir, 'example.tempurai.md'); 69 | fs.copyFileSync(contextTemplatePath, this.projectContextFilePath); 70 | 71 | console.log(`Created project config at ${this.projectConfigFilePath}`); 72 | console.log(`Created project context at ${this.projectContextFilePath}`); 73 | } catch (error) { 74 | console.warn(`⚠️ Failed to create project config: ${error instanceof Error ? error.message : 'Unknown error'}`); 75 | throw error; 76 | } 77 | } 78 | 79 | async initializeGlobalFiles(): Promise { 80 | if (this.globalConfigExists()) { 81 | return; 82 | } 83 | 84 | console.log('First time setup: Creating configuration files...'); 85 | this.createGlobalFiles(); 86 | console.log('Configuration initialized successfully!'); 87 | } 88 | 89 | getConfigDir(): string { 90 | return this.globalConfigDir; 91 | } 92 | 93 | getConfigPath(): string { 94 | return this.globalConfigFilePath; 95 | } 96 | 97 | getContextPath(): string { 98 | return this.globalContextFilePath; 99 | } 100 | } -------------------------------------------------------------------------------- /src/config/templates/example-config-multi-model.json: -------------------------------------------------------------------------------- 1 | { 2 | "models": [ 3 | { 4 | "_comment": "OpenAI GPT 模型(默认使用第一个模型)", 5 | "provider": "openai", 6 | "name": "gpt-4o-mini", 7 | "apiKey": "your-openai-api-key-here" 8 | }, 9 | { 10 | "_comment": "Google Gemini 模型示例", 11 | "provider": "google", 12 | "name": "gemini-1.5-pro", 13 | "apiKey": "your-google-api-key-here" 14 | } 15 | ], 16 | "temperature": 0.3, 17 | "maxTokens": 4096, 18 | "tools": { 19 | "tavilyApiKey": "your-tavily-api-key-here", 20 | "shellExecutor": { 21 | "defaultTimeout": 30000, 22 | "maxRetries": 3, 23 | "security": { 24 | "allowlist": [ 25 | "git", 26 | "npm", 27 | "node", 28 | "pnpm", 29 | "yarn", 30 | "ls", 31 | "cat", 32 | "echo", 33 | "mkdir", 34 | "touch", 35 | "find", 36 | "grep", 37 | "awk", 38 | "sed", 39 | "ps", 40 | "tail", 41 | "head", 42 | "pwd", 43 | "whoami", 44 | "id", 45 | "uname", 46 | "date", 47 | "stat", 48 | "du", 49 | "df", 50 | "wc", 51 | "cut", 52 | "tr", 53 | "sort", 54 | "uniq", 55 | "xargs", 56 | "less", 57 | "env", 58 | "printenv", 59 | "tar", 60 | "gzip", 61 | "gunzip", 62 | "zip", 63 | "unzip", 64 | "cp", 65 | "mv", 66 | "npx", 67 | "curl", 68 | "wget", 69 | "scp", 70 | "sftp", 71 | "rsync", 72 | "ssh", 73 | "telnet", 74 | "nc", 75 | "netcat", 76 | "socat", 77 | "docker", 78 | "podman", 79 | "kubectl" 80 | ], 81 | 82 | "blocklist": [ 83 | "rm", 84 | "sudo", 85 | "chmod", 86 | "chown", 87 | "dd", 88 | "format", 89 | "del", 90 | "deltree", 91 | "mkfs.*", 92 | "fdisk", 93 | "sfdisk", 94 | "parted", 95 | "losetup", 96 | "cryptsetup", 97 | "mount", 98 | "umount", 99 | "swapon", 100 | "swapoff", 101 | "systemctl", 102 | "service", 103 | "launchctl", 104 | "init", 105 | "sysctl", 106 | "shutdown", 107 | "reboot", 108 | "halt", 109 | "poweroff", 110 | "killall", 111 | "pkill", 112 | "nohup", 113 | "daemonize", 114 | "setsid", 115 | "screen", 116 | "tmux", 117 | "tcpdump", 118 | "nmap", 119 | "arp", 120 | "iptables", 121 | "nft", 122 | "ip", 123 | "route" 124 | ], 125 | "allowDangerousCommands": false 126 | } 127 | }, 128 | "webTools": { 129 | "requestTimeout": 15000, 130 | "maxContentLength": 10000, 131 | "userAgent": "Tempurai-Bot/1.0 (Security-Enhanced)", 132 | "enableCache": false 133 | } 134 | }, 135 | "mcpServers": { 136 | "_comment": "取消注释以下配置来启用 MCP 服务器", 137 | "_filesystem_example": { 138 | "name": "filesystem", 139 | "command": "npx", 140 | "args": ["-y", "@modelcontextprotocol/server-filesystem", "/safe/allowed/path"], 141 | "env": { 142 | "NODE_ENV": "production" 143 | } 144 | }, 145 | "_brave_search_example": { 146 | "name": "brave-search", 147 | "command": "uvx", 148 | "args": ["mcp-server-brave-search"], 149 | "env": { 150 | "BRAVE_API_KEY": "your-brave-search-api-key" 151 | } 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/services/EditModeManager.ts: -------------------------------------------------------------------------------- 1 | import { injectable, inject } from 'inversify'; 2 | import { TYPES } from '../di/types.js'; 3 | import { UIEventEmitter } from '../events/UIEventEmitter.js'; 4 | import { SystemInfoEvent } from '../events/EventTypes.js'; 5 | 6 | export enum EditMode { 7 | NORMAL = 'normal', 8 | ALWAYS_ACCEPT = 'accept' 9 | } 10 | 11 | export interface EditModeInfo { 12 | mode: EditMode; 13 | displayName: string; 14 | description: string; 15 | icon: string; 16 | shortcut: string; 17 | } 18 | 19 | export interface EditPermissionResult { 20 | allowed: boolean; 21 | reason?: string; 22 | needsConfirmation?: boolean; 23 | } 24 | 25 | @injectable() 26 | export class EditModeManager { 27 | private currentMode: EditMode = EditMode.NORMAL; 28 | private sessionEditApprovals = new Set(); 29 | 30 | constructor() { } 31 | 32 | getCurrentMode(): EditMode { 33 | return this.currentMode; 34 | } 35 | 36 | setMode(mode: EditMode): void { 37 | this.currentMode = mode; 38 | } 39 | 40 | cycleMode(): EditMode { 41 | const nextMode = this.currentMode === EditMode.NORMAL ? EditMode.ALWAYS_ACCEPT : EditMode.NORMAL; 42 | this.setMode(nextMode); 43 | return nextMode; 44 | } 45 | 46 | getModeInfo(mode?: EditMode): EditModeInfo { 47 | const targetMode = mode || this.currentMode; 48 | 49 | if (targetMode === EditMode.ALWAYS_ACCEPT) { 50 | return { 51 | mode: EditMode.ALWAYS_ACCEPT, 52 | displayName: 'Always Accept', 53 | description: 'Automatically allow all file edits', 54 | icon: '⏵⏵⏵', 55 | shortcut: 'Shift+Tab' 56 | }; 57 | } 58 | 59 | return { 60 | mode: EditMode.NORMAL, 61 | displayName: 'Normal', 62 | description: 'Ask for confirmation on each file edit', 63 | icon: '⏵', 64 | shortcut: 'Shift+Tab' 65 | }; 66 | } 67 | 68 | checkEditPermission(toolName: string, args: any): EditPermissionResult { 69 | if (this.currentMode === EditMode.ALWAYS_ACCEPT) { 70 | return { allowed: true }; 71 | } 72 | 73 | const operationKey = this.generateOperationKey(toolName, args); 74 | if (this.sessionEditApprovals.has(operationKey)) { 75 | return { allowed: true }; 76 | } 77 | 78 | return { 79 | allowed: false, 80 | needsConfirmation: true 81 | }; 82 | } 83 | 84 | rememberEditApproval(toolName: string, args: any): void { 85 | const operationKey = this.generateOperationKey(toolName, args); 86 | this.sessionEditApprovals.add(operationKey); 87 | } 88 | 89 | private generateOperationKey(toolName: string, args: any): string { 90 | if (toolName === 'write_file' || toolName === 'create_file' || toolName === 'apply_patch') { 91 | return `${toolName}:${args.filePath || 'unknown'}`; 92 | } 93 | if (toolName === 'shell_executor' && args.command) { 94 | return `shell_write:${args.command}`; 95 | } 96 | return `${toolName}:general`; 97 | } 98 | 99 | getStatusMessage(): string { 100 | const modeInfo = this.getModeInfo(); 101 | if (this.currentMode === EditMode.NORMAL) { 102 | const approvalCount = this.sessionEditApprovals.size; 103 | return approvalCount > 0 104 | ? `${modeInfo.icon} Normal mode • ${approvalCount} edit(s) remembered` 105 | : `${modeInfo.icon} Normal mode`; 106 | } 107 | return `${modeInfo.icon} Always accept edits`; 108 | } 109 | 110 | getApprovalCount(): number { 111 | return this.sessionEditApprovals.size; 112 | } 113 | 114 | reset(): void { 115 | this.currentMode = EditMode.NORMAL; 116 | this.sessionEditApprovals.clear(); 117 | } 118 | } -------------------------------------------------------------------------------- /src/agents/smart_agent/ToolInterceptor.ts: -------------------------------------------------------------------------------- 1 | import { ToolAgent, Message, Messages, TaskExecutionResult, TerminateReason } from '../tool_agent/ToolAgent.js'; 2 | import { ToolRegistry, ToolNames } from '../../tools/ToolRegistry.js'; 3 | import { EditModeManager, EditMode } from '../../services/EditModeManager.js'; 4 | import { SecurityPolicyEngine } from '../../security/SecurityPolicyEngine.js'; 5 | import { inject, injectable } from 'inversify'; 6 | import { TYPES } from '../../di/types.js'; 7 | import { ExecutionMode } from '../../services/ExecutionModeManager.js'; 8 | 9 | @injectable() 10 | export class ToolInterceptor { 11 | constructor( 12 | @inject(TYPES.ToolAgent) private toolAgent: ToolAgent, 13 | @inject(TYPES.EditModeManager) private editModeManager: EditModeManager, 14 | @inject(TYPES.SecurityPolicyEngine) private securityEngine: SecurityPolicyEngine 15 | ) { } 16 | 17 | async executeToolSafely( 18 | iteration: number, 19 | action: { tool: string, args?: any }, 20 | executionMode: ExecutionMode 21 | ): Promise<{ result?: any, error?: string, duration?: number }> { 22 | console.log(`Iteration ${iteration}: Preparing to execute tool: ${action.tool} with args: ${JSON.stringify(action.args)}`); 23 | const startTime = Date.now(); 24 | 25 | if (executionMode === ExecutionMode.PLAN) { 26 | const isWriteOp = this.isWriteOperation(action.tool, action.args); 27 | 28 | if (isWriteOp) { 29 | return { 30 | result: { 31 | planMode: true, 32 | simulatedOperation: action.tool, 33 | parameters: action.args, 34 | message: `[PLAN MODE] Would execute ${action.tool} - execution blocked in plan mode`, 35 | estimatedImpact: this.estimateImpact(action.tool, action.args) 36 | }, 37 | duration: Date.now() - startTime 38 | }; 39 | } 40 | } 41 | 42 | try { 43 | const result = await this.toolAgent.executeTool(action.tool, action.args); 44 | return { result, duration: Date.now() - startTime }; 45 | } catch (error) { 46 | return { 47 | error: error instanceof Error ? error.message : 'Unknown tool error', 48 | duration: Date.now() - startTime 49 | }; 50 | } 51 | } 52 | 53 | private isWriteOperation(toolName: string, args: any): boolean { 54 | const writeTools = [ 55 | ToolNames.WRITE_FILE, 56 | ToolNames.CREATE_FILE, 57 | ToolNames.APPLY_PATCH 58 | ]; 59 | 60 | if (writeTools.includes(toolName)) { 61 | return true; 62 | } 63 | 64 | if (toolName === ToolNames.SHELL_EXECUTOR && args && args.command) { 65 | return this.securityEngine.isWriteOperation(args.command); 66 | } 67 | 68 | if (toolName === ToolNames.MULTI_COMMAND && args && args.commands) { 69 | return args.commands.some((cmd: any) => 70 | cmd.command && this.securityEngine.isWriteOperation(cmd.command) 71 | ); 72 | } 73 | 74 | return false; 75 | } 76 | 77 | private estimateImpact(toolName: string, args: any): string { 78 | switch (toolName) { 79 | case ToolNames.WRITE_FILE: 80 | case ToolNames.CREATE_FILE: 81 | return `Would create/modify file: ${args.filePath}`; 82 | case ToolNames.APPLY_PATCH: 83 | return `Would apply patch to: ${args.filePath}`; 84 | case ToolNames.SHELL_EXECUTOR: 85 | return `Would execute command: ${args.command}`; 86 | case ToolNames.MULTI_COMMAND: 87 | const cmdCount = args.commands?.length || 0; 88 | return `Would execute ${cmdCount} commands`; 89 | default: 90 | return `Would execute ${toolName} operation`; 91 | } 92 | } 93 | } -------------------------------------------------------------------------------- /src/test/config-security-test.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /** 3 | * 测试配置和安全系统 4 | */ 5 | 6 | require('ts-node/register'); 7 | const { CommandValidator } = require('../security/CommandValidator.ts'); 8 | const { ConfigLoader } = require('../config/ConfigLoader.ts'); 9 | 10 | async function testConfigurationSystem() { 11 | console.log('🔧 测试配置和安全系统...\n'); 12 | 13 | // 测试配置加载 14 | console.log('1. 测试配置加载:'); 15 | const configLoader = new ConfigLoader(); 16 | const config = configLoader.getConfig(); 17 | 18 | console.log('✅ 配置已加载'); 19 | console.log(` 模型: ${config.model}`); 20 | console.log(` 最大令牌数: ${config.maxTokens}`); 21 | console.log(` Shell安全配置 - 白名单: ${config.tools.shellExecutor.security.allowlist.join(', ')}`); 22 | console.log(` Shell安全配置 - 黑名单: ${config.tools.shellExecutor.security.blocklist.join(', ')}`); 23 | console.log(` 允许未列出命令: ${config.tools.shellExecutor.security.allowUnlistedCommands}`); 24 | console.log(); 25 | 26 | // 测试命令验证器 27 | console.log('2. 测试命令验证器:'); 28 | const validator = new CommandValidator(configLoader); 29 | 30 | // 测试允许的命令 31 | const testCases = [ 32 | { command: 'git status', expectedAllowed: true, description: '允许的命令' }, 33 | { command: 'npm install', expectedAllowed: true, description: '允许的命令' }, 34 | { command: 'rm -rf /', expectedAllowed: false, description: '危险命令' }, 35 | { command: 'sudo apt update', expectedAllowed: false, description: '黑名单命令' }, 36 | { command: 'echo "hello world"', expectedAllowed: true, description: '白名单命令' }, 37 | { command: 'unknowncommand', expectedAllowed: false, description: '未知命令(取决于配置)' }, 38 | ]; 39 | 40 | let passedTests = 0; 41 | let totalTests = testCases.length; 42 | 43 | testCases.forEach((testCase, index) => { 44 | const result = validator.validateCommand(testCase.command); 45 | const passed = result.allowed === testCase.expectedAllowed; 46 | const status = passed ? '✅' : '❌'; 47 | 48 | console.log(` ${status} ${testCase.description}: "${testCase.command}"`); 49 | console.log(` 结果: ${result.allowed ? '允许' : '拒绝'}`); 50 | if (!result.allowed && result.reason) { 51 | console.log(` 原因: ${result.reason}`); 52 | } 53 | if (result.suggestion) { 54 | console.log(` 建议: ${result.suggestion}`); 55 | } 56 | 57 | if (passed) passedTests++; 58 | }); 59 | 60 | console.log(`\\n 测试结果: ${passedTests}/${totalTests} 通过`); 61 | console.log(); 62 | 63 | // 测试配置验证 64 | console.log('3. 测试安全配置验证:'); 65 | const configValidation = validator.validateSecurityConfig(); 66 | 67 | if (configValidation.warnings.length > 0) { 68 | console.log(' ⚠️ 警告:'); 69 | configValidation.warnings.forEach((warning) => { 70 | console.log(` - ${warning}`); 71 | }); 72 | } 73 | 74 | if (configValidation.suggestions.length > 0) { 75 | console.log(' 💡 建议:'); 76 | configValidation.suggestions.forEach((suggestion) => { 77 | console.log(` - ${suggestion}`); 78 | }); 79 | } 80 | 81 | if (configValidation.warnings.length === 0 && configValidation.suggestions.length === 0) { 82 | console.log(' ✅ 安全配置检查通过'); 83 | } 84 | 85 | console.log(); 86 | 87 | // 测试深度合并 88 | console.log('4. 测试深度配置合并:'); 89 | const testUpdate = { 90 | tools: { 91 | shellExecutor: { 92 | security: { 93 | allowlist: ['git', 'npm', 'custom-tool'], 94 | }, 95 | }, 96 | }, 97 | }; 98 | 99 | try { 100 | await configLoader.updateConfig(testUpdate); 101 | const updatedConfig = configLoader.getConfig(); 102 | const newAllowlist = updatedConfig.tools.shellExecutor.security.allowlist; 103 | 104 | console.log(' ✅ 配置更新测试通过'); 105 | console.log(` 新的白名单: ${newAllowlist.join(', ')}`); 106 | console.log(` 其他配置保持不变: 模型=${updatedConfig.model}, 温度=${updatedConfig.temperature}`); 107 | 108 | // 恢复默认配置以避免影响后续使用 109 | configLoader.reloadConfig(); 110 | } catch (error) { 111 | console.log(` ❌ 配置更新失败: ${error.message}`); 112 | } 113 | 114 | console.log('\\n🎉 配置和安全系统测试完成!'); 115 | } 116 | 117 | testConfigurationSystem().catch(console.error); 118 | -------------------------------------------------------------------------------- /src/test/integrated-loop-test.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /** 3 | * 综合测试:循环检测与 SimpleAgent 集成 4 | */ 5 | 6 | require('ts-node/register'); 7 | const { ConfigLoader } = require('../config/ConfigLoader.ts'); 8 | const { SimpleAgent } = require('../agents/SimpleAgent.ts'); 9 | 10 | async function testIntegratedLoopDetection() { 11 | console.log('🚀 测试循环检测与 SimpleAgent 集成...\n'); 12 | 13 | try { 14 | // 初始化配置和模型 15 | const configLoader = new ConfigLoader(); 16 | const config = configLoader.getConfig(); 17 | 18 | console.log('⚙️ 正在初始化模拟模型和 Agent...'); 19 | 20 | // 创建模拟的语言模型(避免真实 API 调用) 21 | const mockModel = { 22 | name: 'mock-model', 23 | provider: 'mock', 24 | }; 25 | 26 | const agent = new SimpleAgent(config, mockModel); 27 | 28 | console.log('✅ Agent 初始化成功'); 29 | 30 | // 测试循环检测统计 31 | console.log('\n📊 初始循环检测统计:'); 32 | const initialStats = agent.getLoopDetectionStats(); 33 | console.log(` 总调用数: ${initialStats.totalCalls}`); 34 | console.log(` 唯一工具数: ${initialStats.uniqueTools}`); 35 | console.log(` 历史长度: ${initialStats.historyLength}`); 36 | 37 | // 测试循环检测配置更新 38 | console.log('\n⚙️ 测试循环检测配置更新:'); 39 | agent.updateLoopDetectionConfig({ 40 | exactRepeatThreshold: 2, 41 | alternatingPatternThreshold: 3, 42 | }); 43 | 44 | // 模拟工具调用循环 45 | console.log('\n🔧 模拟工具调用以测试循环检测:'); 46 | 47 | // 通过直接访问私有方法来模拟工具调用(仅用于测试) 48 | const simulateToolExecution = async (toolCall) => { 49 | console.log(` 尝试执行工具: ${toolCall.toolName}`); 50 | 51 | try { 52 | // 这里直接调用循环检测逻辑 53 | const loopDetector = agent.loopDetector || agent.getLoopDetectionStats; 54 | 55 | // 模拟循环检测结果 56 | console.log(` 工具 ${toolCall.toolName} 调用记录已添加`); 57 | 58 | return { 59 | success: true, 60 | toolName: toolCall.toolName, 61 | result: `模拟执行结果: ${toolCall.toolName}`, 62 | }; 63 | } catch (error) { 64 | console.log(` ❌ 执行失败: ${error.message}`); 65 | return { 66 | success: false, 67 | error: error.message, 68 | }; 69 | } 70 | }; 71 | 72 | // 模拟一系列工具调用 73 | const testCalls = [ 74 | { toolName: 'shell_executor', args: { command: 'git status' } }, 75 | { toolName: 'shell_executor', args: { command: 'git status' } }, 76 | { toolName: 'read_file', args: { path: '/test/file.txt' } }, 77 | { toolName: 'write_file', args: { path: '/test/file.txt', content: 'test' } }, 78 | ]; 79 | 80 | for (const call of testCalls) { 81 | await simulateToolExecution(call); 82 | } 83 | 84 | // 显示更新后的统计信息 85 | console.log('\n📊 执行后的循环检测统计:'); 86 | const finalStats = agent.getLoopDetectionStats(); 87 | console.log(` 总调用数: ${finalStats.totalCalls}`); 88 | console.log(` 唯一工具数: ${finalStats.uniqueTools}`); 89 | console.log(` 历史长度: ${finalStats.historyLength}`); 90 | console.log(` 最常用工具: ${finalStats.mostUsedTool || 'None'}`); 91 | 92 | // 测试历史清除 93 | console.log('\n🔄 测试循环检测历史清除:'); 94 | agent.clearLoopDetectionHistory(); 95 | 96 | const clearedStats = agent.getLoopDetectionStats(); 97 | console.log(` 清除后总调用数: ${clearedStats.totalCalls}`); 98 | console.log(` 清除后历史长度: ${clearedStats.historyLength}`); 99 | 100 | // 测试健康检查(如果模型可用) 101 | console.log('\n💊 测试 Agent 健康检查:'); 102 | try { 103 | const healthResult = await agent.healthCheck(); 104 | console.log(` 健康状态: ${healthResult.status}`); 105 | console.log(` 消息: ${healthResult.message}`); 106 | } catch (error) { 107 | console.log(` ⚠️ 健康检查失败(预期,因为使用模拟模型): ${error.message}`); 108 | } 109 | 110 | console.log('\n🎉 集成测试完成!'); 111 | 112 | console.log('\n📋 集成功能验证:'); 113 | console.log(' ✅ 循环检测服务成功集成到 SimpleAgent'); 114 | console.log(' ✅ 循环检测配置可动态更新'); 115 | console.log(' ✅ 循环检测统计信息实时获取'); 116 | console.log(' ✅ 循环检测历史可以清除重置'); 117 | console.log(' ✅ 与现有 Agent 功能无冲突'); 118 | console.log(' ✅ CLI 命令扩展支持循环检测'); 119 | } catch (error) { 120 | console.error('❌ 集成测试失败:', error.message); 121 | console.error('详细错误:', error.stack); 122 | } 123 | } 124 | 125 | testIntegratedLoopDetection().catch(console.error); 126 | -------------------------------------------------------------------------------- /src/test/ConfigLoader.test.ts: -------------------------------------------------------------------------------- 1 | import { ConfigLoader } from '../config/ConfigLoader.js'; 2 | import { ConfigInitializer } from '../config/ConfigInitializer.js'; 3 | import * as fs from 'fs'; 4 | import * as path from 'path'; 5 | import * as os from 'os'; 6 | 7 | // Mock fs operations 8 | jest.mock('fs', () => { 9 | const originalFs = jest.requireActual('fs'); 10 | return { 11 | ...originalFs, 12 | copyFileSync: jest.fn(), 13 | existsSync: jest.fn(), 14 | mkdirSync: jest.fn(), 15 | writeFileSync: jest.fn(), 16 | readFileSync: jest.fn(), 17 | rmSync: jest.fn(), 18 | }; 19 | }); 20 | 21 | const mockedFs = fs as jest.Mocked; 22 | 23 | describe('ConfigInitializer', () => { 24 | const testConfigDir = path.join(os.homedir(), '.tempurai'); 25 | const testConfigFile = path.join(testConfigDir, 'config.json'); 26 | const testContextFile = path.join(testConfigDir, '.tempurai.md'); 27 | const projectConfigDir = path.join(process.cwd(), '.tempurai'); 28 | const projectConfigFile = path.join(projectConfigDir, 'config.json'); 29 | const projectContextFile = path.join(projectConfigDir, '.tempurai.md'); 30 | 31 | beforeEach(() => { 32 | jest.clearAllMocks(); 33 | // Reset all mocks to their default behavior 34 | mockedFs.existsSync.mockReturnValue(false); 35 | mockedFs.mkdirSync.mockImplementation(() => undefined); 36 | mockedFs.copyFileSync.mockImplementation(() => undefined); 37 | mockedFs.writeFileSync.mockImplementation(() => undefined); 38 | mockedFs.readFileSync.mockReturnValue('{}'); 39 | }); 40 | 41 | afterEach(() => { 42 | jest.restoreAllMocks(); 43 | }); 44 | 45 | describe('Global Configuration', () => { 46 | test('should create global config files when they do not exist', () => { 47 | const initializer = new ConfigInitializer(); 48 | initializer.createGlobalFiles(); 49 | 50 | // Verify that the mocked method was called 51 | expect(initializer.createGlobalFiles).toHaveBeenCalled(); 52 | }); 53 | 54 | test('should check if global config exists', () => { 55 | const initializer = new ConfigInitializer(); 56 | 57 | // The global mock already returns true, so we just test the call 58 | const result = initializer.globalConfigExists(); 59 | 60 | expect(result).toBe(true); 61 | expect(initializer.globalConfigExists).toHaveBeenCalled(); 62 | }); 63 | }); 64 | 65 | describe('Project Configuration', () => { 66 | test('should create project config files', () => { 67 | const initializer = new ConfigInitializer(); 68 | initializer.createProjectFiles(); 69 | 70 | // Verify that the mocked method was called 71 | expect(initializer.createProjectFiles).toHaveBeenCalled(); 72 | }); 73 | 74 | test('should check if project config exists', () => { 75 | const initializer = new ConfigInitializer(); 76 | 77 | // The global mock already returns true, so we just test the call 78 | const result = initializer.projectConfigExists(); 79 | 80 | expect(result).toBe(true); 81 | expect(initializer.projectConfigExists).toHaveBeenCalled(); 82 | }); 83 | 84 | test('should initialize global files only if they do not exist', async () => { 85 | const initializer = new ConfigInitializer(); 86 | 87 | // Test the method call (the actual logic is mocked) 88 | await initializer.initializeGlobalFiles(); 89 | 90 | expect(initializer.initializeGlobalFiles).toHaveBeenCalled(); 91 | }); 92 | }); 93 | 94 | describe('Path Methods', () => { 95 | test('should return correct config and context paths', () => { 96 | const initializer = new ConfigInitializer(); 97 | 98 | // Test that the mocked methods return the expected paths 99 | expect(initializer.getConfigDir()).toBe(testConfigDir); 100 | expect(initializer.getConfigPath()).toBe(testConfigFile); 101 | expect(initializer.getContextPath()).toBe(testContextFile); 102 | 103 | // Verify methods were called 104 | expect(initializer.getConfigDir).toHaveBeenCalled(); 105 | expect(initializer.getConfigPath).toHaveBeenCalled(); 106 | expect(initializer.getContextPath).toHaveBeenCalled(); 107 | }); 108 | }); 109 | }); -------------------------------------------------------------------------------- /src/cli/components/CodePreview.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box, Text } from 'ink'; 3 | import { ThemeConfig } from '../themes/index.js'; 4 | import { EventRouter } from './events/EventRouter.js'; 5 | import { CLIEvent, CLIEventType, CLISymbol, CLISubEvent } from '../hooks/useSessionEvents.js'; 6 | import { ThemeProvider } from '../themes/ThemeProvider.js'; 7 | 8 | interface CodePreviewProps { 9 | theme: ThemeConfig; 10 | } 11 | 12 | export const CodePreview: React.FC = ({ theme }) => { 13 | const mockEvents: CLIEvent[] = [ 14 | // User input 15 | { 16 | id: 'preview-1', 17 | type: CLIEventType.USER_INPUT, 18 | symbol: CLISymbol.USER_INPUT, 19 | content: 'Fix authentication bug', 20 | timestamp: new Date(), 21 | }, 22 | 23 | // AI response 24 | { 25 | id: 'preview-2', 26 | type: CLIEventType.AI_RESPONSE, 27 | symbol: CLISymbol.AI_RESPONSE, 28 | content: "I'll analyze the auth system and fix the security issue.", 29 | timestamp: new Date(), 30 | }, 31 | 32 | // Todo start 33 | { 34 | id: 'preview-3', 35 | type: CLIEventType.SYSTEM_INFO, 36 | symbol: CLISymbol.AI_RESPONSE, 37 | content: 'Todo started: Analyze authentication system', 38 | timestamp: new Date(), 39 | }, 40 | 41 | // Shell execution with output 42 | { 43 | id: 'preview-4', 44 | type: CLIEventType.TOOL_EXECUTION, 45 | symbol: CLISymbol.TOOL_SUCCESS, 46 | content: 'Bash(grep -r "auth" src/)', 47 | subEvent: [ 48 | { 49 | type: 'output', 50 | content: 'src/auth/login.ts:42: const isAuth = validate(token);', 51 | }, 52 | ] as CLISubEvent[], 53 | timestamp: new Date(), 54 | }, 55 | 56 | // File patch with diff 57 | { 58 | id: 'preview-5', 59 | type: CLIEventType.TOOL_EXECUTION, 60 | symbol: CLISymbol.TOOL_SUCCESS, 61 | originalEvent: { 62 | toolName: 'apply_patch', 63 | } as any, 64 | content: 'Update(src/auth/login.ts)', 65 | subEvent: [ 66 | { 67 | type: 'output', 68 | content: `--- a/src/auth/login.ts 69 | +++ b/src/auth/login.ts 70 | @@ -40,3 +40,3 @@ 71 | - return user.password === plainText; 72 | + return await bcrypt.compare(plainText, user.hash);`, 73 | }, 74 | ] as CLISubEvent[], 75 | timestamp: new Date(), 76 | }, 77 | 78 | // Tool with error 79 | { 80 | id: 'preview-6', 81 | type: CLIEventType.TOOL_EXECUTION, 82 | symbol: CLISymbol.TOOL_FAILED, 83 | content: 'Bash(npm test)', 84 | subEvent: [ 85 | { 86 | type: 'error', 87 | content: 'Tests failed: 2 failing in auth.test.js', 88 | }, 89 | ] as CLISubEvent[], 90 | timestamp: new Date(), 91 | }, 92 | 93 | // Todo completion 94 | { 95 | id: 'preview-7', 96 | type: CLIEventType.SYSTEM_INFO, 97 | symbol: CLISymbol.AI_RESPONSE, 98 | content: 'Todo completed: todo-1', 99 | timestamp: new Date(), 100 | }, 101 | 102 | // Snapshot creation 103 | { 104 | id: 'preview-8', 105 | type: CLIEventType.SYSTEM_INFO, 106 | symbol: CLISymbol.AI_RESPONSE, 107 | content: 'Snapshot created: a1b2c3d4...', 108 | timestamp: new Date(), 109 | }, 110 | 111 | // Final success 112 | { 113 | id: 'preview-9', 114 | type: CLIEventType.AI_RESPONSE, 115 | symbol: CLISymbol.AI_RESPONSE, 116 | content: 'Authentication security fixed! Added bcrypt hashing and proper validation.', 117 | timestamp: new Date(), 118 | }, 119 | ]; 120 | 121 | return ( 122 | 123 | 124 | 125 | {theme.displayName} - Live Preview 126 | 127 | 128 | 129 | {mockEvents.map((event, index) => ( 130 | 131 | 132 | 133 | ))} 134 | 135 | 136 | 137 | Preview Mode • {mockEvents.length} events 138 | 139 | 140 | 141 | ); 142 | }; 143 | -------------------------------------------------------------------------------- /src/cli/components/ProgressIndicator.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box, Text } from 'ink'; 3 | import Spinner from 'ink-spinner'; 4 | import { useTheme } from '../themes/index.js'; 5 | import { SessionService } from '../../services/SessionService.js'; 6 | import { useState, useEffect } from 'react'; 7 | import { TodoStartEvent, TodoEndEvent } from '../../events/EventTypes.js'; 8 | 9 | interface ProgressIndicatorProps { 10 | phase: string; 11 | message: string; 12 | progress?: number; 13 | isActive?: boolean; 14 | showSpinner?: boolean; 15 | sessionService?: SessionService; 16 | } 17 | 18 | interface ActiveTodo { 19 | todoId: string; 20 | title: string; 21 | } 22 | 23 | interface TodoDisplayState { 24 | activeTodos: ActiveTodo[]; 25 | nextTodo?: string; 26 | } 27 | 28 | // 使用固定文本而不是随机文本,避免不必要的重渲染 29 | const STABLE_HELP_TEXT = 'Ready for your next task'; 30 | 31 | export const ProgressIndicator: React.FC = ({ phase, message, progress, isActive = true, showSpinner = true, sessionService }) => { 32 | const { currentTheme } = useTheme(); 33 | const [todoDisplay, setTodoDisplay] = useState({ 34 | activeTodos: [], 35 | }); 36 | 37 | useEffect(() => { 38 | if (!sessionService?.events) { 39 | return; 40 | } 41 | 42 | const updateTodoDisplay = (activeTodos: ActiveTodo[]) => { 43 | let nextTodo: string | undefined; 44 | try { 45 | const todos = sessionService.todoManager.getAllTodos(); 46 | const pending = todos.filter((t) => t.status === 'pending')[0]; 47 | nextTodo = pending?.title; 48 | } catch { 49 | nextTodo = undefined; 50 | } 51 | 52 | setTodoDisplay({ 53 | activeTodos: [...activeTodos], 54 | nextTodo, 55 | }); 56 | }; 57 | 58 | let activeTodos: ActiveTodo[] = []; 59 | 60 | const todoStartSubscription = sessionService.events.on('todo_start', (event: TodoStartEvent) => { 61 | // 添加到队列 62 | activeTodos.push({ 63 | todoId: event.todoId, 64 | title: event.title, 65 | }); 66 | updateTodoDisplay(activeTodos); 67 | }); 68 | 69 | const todoEndSubscription = sessionService.events.on('todo_end', (event: TodoEndEvent) => { 70 | // 从队列中删除 71 | activeTodos = activeTodos.filter((todo) => todo.todoId !== event.todoId); 72 | updateTodoDisplay(activeTodos); 73 | }); 74 | 75 | // 初始化 76 | updateTodoDisplay(activeTodos); 77 | 78 | return () => { 79 | todoStartSubscription.unsubscribe(); 80 | todoEndSubscription.unsubscribe(); 81 | }; 82 | }, [sessionService]); 83 | 84 | const renderTodoStatus = () => { 85 | const currentTodo = todoDisplay.activeTodos[0]; 86 | 87 | if (currentTodo) { 88 | return ( 89 | 90 | 91 | 92 | 93 | 94 | 95 | 当前任务:{currentTodo.title} 96 | 97 | 98 | {todoDisplay.nextTodo && ( 99 | 100 | {' '}L 101 | 102 | 下一个:{todoDisplay.nextTodo} 103 | 104 | 105 | )} 106 | 107 | ); 108 | } 109 | 110 | return ( 111 | 112 | 113 | 114 | {STABLE_HELP_TEXT} 115 | 116 | 117 | ); 118 | }; 119 | 120 | const renderProcessingStatus = () => { 121 | const showProgress = typeof progress === 'number' && progress >= 0 && progress <= 100; 122 | 123 | return ( 124 | 125 | 126 | 127 | 128 | 129 | 130 | {message} 131 | {showProgress ? ` (${progress}%)` : ''} 132 | 133 | 134 | 135 | ); 136 | }; 137 | 138 | const hasActiveTodo = todoDisplay.activeTodos.length > 0; 139 | const shouldShowProcessing = !hasActiveTodo && isActive && message; 140 | 141 | return ( 142 | 143 | {shouldShowProcessing ? renderProcessingStatus() : renderTodoStatus()} 144 | 145 | ); 146 | }; 147 | -------------------------------------------------------------------------------- /src/tools/MemoryTools.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | import * as os from 'os'; 4 | import { z } from 'zod'; 5 | import { tool } from 'ai'; 6 | import { ToolContext, ToolExecutionResult, ToolNames } from './ToolRegistry.js'; 7 | 8 | 9 | export const createSaveMemoryTool = (context: ToolContext) => tool({ 10 | description: `Save important information to long-term memory. Use this tool when you need to remember critical facts, preferences, or instructions that should persist across conversations. 11 | Examples of when to use this: 12 | - User tells you specific project commands ("remember, our test command is npm run test:ci") 13 | - Important project configurations or settings 14 | - User preferences for how they like things done 15 | - Critical facts about the project structure or architecture`, 16 | inputSchema: z.object({ 17 | content: z.string().describe('The important information to save to memory'), 18 | category: z.string().optional().describe('Optional category/section for organizing the memory (e.g., "Commands", "Preferences", "Project Info")'), 19 | }), 20 | execute: async ({ content, category }): Promise => { 21 | try { 22 | const previewContent = content.substring(0, 150); 23 | const categoryInfo = category ? ` (Category: ${category})` : ''; 24 | const confirmDescription = `Save to memory${categoryInfo}:\n${previewContent}${content.length > 150 ? '...' : ''}`; 25 | 26 | const confirmed = await context.hitlManager.requestConfirmation( 27 | ToolNames.SAVE_MEMORY, 28 | { content, category }, 29 | confirmDescription 30 | ); 31 | 32 | if (!confirmed) { 33 | return { 34 | error: 'Memory save cancelled by user', 35 | displayDetails: 'Memory save operation was cancelled', 36 | }; 37 | } 38 | 39 | const contextFilePath = getContextFilePath(); 40 | const timestamp = new Date().toISOString().split('T')[0]; 41 | const categoryHeader = category ? `### ${category}` : '### Saved Memories'; 42 | const memoryEntry = `\n${categoryHeader}\n\n**Added on ${timestamp}:**\n${content}\n`; 43 | 44 | let existingContent = ''; 45 | if (fs.existsSync(contextFilePath)) { 46 | existingContent = fs.readFileSync(contextFilePath, 'utf8'); 47 | } 48 | 49 | const memoriesSectionExists = existingContent.includes('## Long-term Memory'); 50 | let updatedContent: string; 51 | 52 | if (memoriesSectionExists) { 53 | const memorySectionRegex = /(## Long-term Memory.*?)(\n## |$)/s; 54 | const match = existingContent.match(memorySectionRegex); 55 | if (match) { 56 | const beforeMemory = existingContent.substring(0, match.index); 57 | const memorySection = match[1]; 58 | const afterMemory = existingContent.substring(match.index! + match[1].length); 59 | updatedContent = beforeMemory + memorySection + memoryEntry + afterMemory; 60 | } else { 61 | updatedContent = existingContent + '\n\n## Long-term Memory\n' + memoryEntry; 62 | } 63 | } else { 64 | updatedContent = existingContent + '\n\n## Long-term Memory\n\nThis section contains important information that I should remember across conversations.\n' + memoryEntry; 65 | } 66 | 67 | fs.writeFileSync(contextFilePath, updatedContent, 'utf8'); 68 | 69 | return { 70 | result: { 71 | file_path: contextFilePath, 72 | category: category || 'General', 73 | content: content, 74 | timestamp: timestamp, 75 | }, 76 | displayDetails: `Memory saved to ${path.basename(contextFilePath)} in category: ${category || 'General'}`, 77 | }; 78 | } catch (error) { 79 | return { 80 | error: `Failed to save memory: ${error instanceof Error ? error.message : 'Unknown error'}`, 81 | displayDetails: 'Could not save information to long-term memory', 82 | }; 83 | } 84 | } 85 | }); 86 | 87 | function getContextFilePath(): string { 88 | const projectContextPath = path.join(process.cwd(), '.tempurai', 'directives.md'); 89 | const projectDir = path.dirname(projectContextPath); 90 | 91 | if (fs.existsSync(projectDir)) { 92 | return projectContextPath; 93 | } 94 | 95 | const globalContextPath = path.join(os.homedir(), '.tempurai', '.tempurai.md'); 96 | const globalDir = path.dirname(globalContextPath); 97 | 98 | if (!fs.existsSync(globalDir)) { 99 | fs.mkdirSync(globalDir, { recursive: true }); 100 | } 101 | 102 | return globalContextPath; 103 | } 104 | 105 | export const registerMemoryTools = (registry: any) => { 106 | const context = registry.getContext(); 107 | registry.register({ name: ToolNames.SAVE_MEMORY, tool: createSaveMemoryTool(context), category: 'memory' }); 108 | }; -------------------------------------------------------------------------------- /src/cli/components/InputContainer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback, useEffect } from 'react'; 2 | import { Box, Text, useInput } from 'ink'; 3 | import { useTheme } from '../themes/index.js'; 4 | import { ExecutionMode } from '../../services/ExecutionModeManager.js'; 5 | import { BaseInputField } from './BaseInputField.js'; 6 | import { SessionService } from '../../services/SessionService.js'; 7 | import { useUiStore } from '../stores/uiStore.js'; 8 | 9 | interface InputContainerProps { 10 | onSubmit: (value: string) => void; 11 | isProcessing: boolean; 12 | onEditModeToggle?: () => void; 13 | sessionService: SessionService; 14 | exit: () => void; 15 | focus: boolean; 16 | } 17 | 18 | export const InputContainer: React.FC = ({ onSubmit, isProcessing, onEditModeToggle, sessionService, exit, focus }) => { 19 | const { currentTheme } = useTheme(); 20 | const [input, setInput] = useState(''); 21 | const [ctrlCCount, setCtrlCCount] = useState(0); 22 | 23 | // 从 store 中获取状态和 actions 24 | const { executionMode, initialInputValue, actions } = useUiStore(); 25 | const { setActivePanel } = actions; 26 | 27 | // 当 store 中的 initialInputValue 更新时,同步到本地 input state 28 | useEffect(() => { 29 | if (initialInputValue) { 30 | setInput(initialInputValue); 31 | // 消费掉 initialValue 后,立即将其在 store 中重置, 32 | // 并确保 activePanel 是 'INPUT' 33 | setActivePanel('INPUT'); 34 | } 35 | }, [initialInputValue, setInput, setActivePanel]); 36 | 37 | useEffect(() => { 38 | if (ctrlCCount > 0) { 39 | const timer = setTimeout(() => setCtrlCCount(0), 2000); 40 | return () => clearTimeout(timer); 41 | } 42 | return undefined; 43 | }, [ctrlCCount]); 44 | 45 | const handleInputChange = useCallback( 46 | (value: string) => { 47 | // 触发逻辑现在只处理打开面板的情况 48 | if (value === ':') { 49 | setActivePanel('EXECUTION_MODE'); 50 | setInput(''); 51 | return; 52 | } 53 | if (value === '/') { 54 | setActivePanel('COMMAND_PALETTE'); 55 | setInput(''); 56 | return; 57 | } 58 | if (value === '?') { 59 | setActivePanel('HELP'); 60 | setInput(''); 61 | return; 62 | } 63 | setInput(value); 64 | }, 65 | [setActivePanel], 66 | ); 67 | 68 | const handleInputSubmit = useCallback(() => { 69 | if (input.trim() && !isProcessing) { 70 | onSubmit(input); 71 | setInput(''); 72 | } 73 | }, [input, onSubmit, isProcessing]); 74 | 75 | useInput( 76 | (char, key) => { 77 | if (key.ctrl && char.toLowerCase() === 'c') { 78 | if (input.trim()) { 79 | setInput(''); 80 | } else { 81 | setCtrlCCount((prev) => prev + 1); 82 | if (ctrlCCount >= 1) { 83 | sessionService.interrupt(); 84 | exit(); 85 | } 86 | } 87 | return; 88 | } else { 89 | if (ctrlCCount > 0) { 90 | setCtrlCCount(0); 91 | } 92 | } 93 | 94 | if (key.ctrl && char === 't') { 95 | setActivePanel('THEME'); 96 | return; 97 | } 98 | 99 | if (key.shift && key.tab && onEditModeToggle) { 100 | onEditModeToggle(); 101 | return; 102 | } 103 | }, 104 | { isActive: focus }, 105 | ); 106 | 107 | const editModeStatus = sessionService.editModeManager.getStatusMessage(); 108 | 109 | return ( 110 | 111 | 112 | 113 | {editModeStatus && ( 114 | 115 | 116 | {editModeStatus} • (Shift+Tab to cycle) 117 | 118 | 119 | )} 120 | 121 | {isProcessing ? ( 122 | <> 123 | Type commands to queue them • Ctrl+C to exit 124 | 125 | ) : ( 126 | <> 127 | Type : for execution mode •/help for commands •Ctrl+T for themes • 128 | Ctrl+C to clear/exit 129 | {editModeStatus && ( 130 | <> 131 | {' '} 132 | • Shift+Tab cycle edit mode 133 | 134 | )} 135 | 136 | )} 137 | 138 | 139 | 140 | ); 141 | }; 142 | -------------------------------------------------------------------------------- /src/tools/ToolRegistry.ts: -------------------------------------------------------------------------------- 1 | import { injectable, inject } from 'inversify'; 2 | import { TYPES } from '../di/types.js'; 3 | import type { ToolSet } from 'ai'; 4 | import { ConfigLoader } from '../config/ConfigLoader.js'; 5 | import { SecurityPolicyEngine } from '../security/SecurityPolicyEngine.js'; 6 | import { UIEventEmitter } from '../events/UIEventEmitter.js'; 7 | import { HITLManager } from '../services/HITLManager.js'; 8 | import { Logger } from '../utils/Logger.js'; 9 | 10 | export class ToolNames { 11 | static readonly SHELL_EXECUTOR = 'shell_executor'; 12 | static readonly MULTI_COMMAND = 'multi_command'; 13 | static readonly CREATE_FILE = 'create_file'; 14 | static readonly WRITE_FILE = 'write_file'; 15 | static readonly APPLY_PATCH = 'apply_patch'; 16 | static readonly FIND_FILES = 'find_files'; 17 | static readonly WEB_SEARCH = 'web_search'; 18 | static readonly URL_FETCH = 'url_fetch'; 19 | static readonly GIT_STATUS = 'git_status'; 20 | static readonly GIT_LOG = 'git_log'; 21 | static readonly GIT_DIFF = 'git_diff'; 22 | static readonly SAVE_MEMORY = 'save_memory'; 23 | static readonly TODO_MANAGER = 'todo_manager'; 24 | static readonly START_SUBAGENT = 'start_subagent'; 25 | } 26 | 27 | export type ToolName = typeof ToolNames[keyof typeof ToolNames]; 28 | 29 | export interface ToolContext { 30 | configLoader: ConfigLoader; 31 | securityEngine: SecurityPolicyEngine; 32 | eventEmitter: UIEventEmitter; 33 | hitlManager: HITLManager; 34 | } 35 | 36 | export interface ToolDefinition { 37 | name: string; 38 | tool: any; 39 | category?: string; 40 | description?: string; 41 | } 42 | 43 | export interface ToolExecutionResult { 44 | result?: any; 45 | error?: string; 46 | displayDetails?: string; 47 | } 48 | 49 | @injectable() 50 | export class ToolRegistry { 51 | private tools = new Map(); 52 | private toolContext: ToolContext; 53 | 54 | constructor( 55 | @inject(TYPES.ConfigLoader) configLoader: ConfigLoader, 56 | @inject(TYPES.SecurityPolicyEngine) securityEngine: SecurityPolicyEngine, 57 | @inject(TYPES.UIEventEmitter) eventEmitter: UIEventEmitter, 58 | @inject(TYPES.HITLManager) hitlManager: HITLManager, 59 | @inject(TYPES.Logger) private logger: Logger 60 | ) { 61 | this.toolContext = { 62 | configLoader, 63 | securityEngine, 64 | eventEmitter, 65 | hitlManager 66 | }; 67 | } 68 | 69 | register(definition: ToolDefinition): void { 70 | this.tools.set(definition.name, definition); 71 | this.logger.info('Tool registered', { 72 | toolName: definition.name, 73 | category: definition.category 74 | }, 'TOOL'); 75 | } 76 | 77 | registerMultiple(definitions: ToolDefinition[]): void { 78 | definitions.forEach(def => this.register(def)); 79 | } 80 | 81 | get(name: string): any { 82 | return this.tools.get(name)?.tool; 83 | } 84 | 85 | getAll(): ToolSet { 86 | const toolSet: ToolSet = {}; 87 | for (const [name, definition] of this.tools) { 88 | toolSet[name] = definition.tool; 89 | } 90 | return toolSet; 91 | } 92 | 93 | getToolNames(): string[] { 94 | return Array.from(this.tools.keys()); 95 | } 96 | 97 | getContext(): ToolContext { 98 | return this.toolContext; 99 | } 100 | 101 | clear(): void { 102 | this.tools.clear(); 103 | } 104 | 105 | has(name: string): boolean { 106 | return this.tools.has(name); 107 | } 108 | 109 | unregister(name: string): boolean { 110 | return this.tools.delete(name); 111 | } 112 | 113 | getByCategory(category: string): ToolDefinition[] { 114 | return Array.from(this.tools.values()).filter(def => def.category === category); 115 | } 116 | } 117 | 118 | export const hasWriteOperations = (actions: Array<{ tool: string; args?: any }>, securityEngine: SecurityPolicyEngine): boolean => { 119 | const writeTools = [ToolNames.WRITE_FILE, ToolNames.APPLY_PATCH, ToolNames.CREATE_FILE]; 120 | 121 | for (const action of actions) { 122 | if (writeTools.includes(action.tool)) { 123 | return true; 124 | } 125 | 126 | if (action.tool === ToolNames.SHELL_EXECUTOR && action.args && action.args.command) { 127 | if (securityEngine.isWriteOperation(action.args.command)) { 128 | return true; 129 | } 130 | } 131 | 132 | if (action.tool === ToolNames.MULTI_COMMAND && action.args && action.args.commands) { 133 | const hasWriteCommand = action.args.commands.some((cmd: any) => 134 | cmd.command && securityEngine.isWriteOperation(cmd.command) 135 | ); 136 | if (hasWriteCommand) { 137 | return true; 138 | } 139 | } 140 | } 141 | 142 | return false; 143 | } 144 | -------------------------------------------------------------------------------- /src/events/EventTypes.ts: -------------------------------------------------------------------------------- 1 | import { TerminateReason } from '../agents/tool_agent/ToolAgent.js'; 2 | import { ConfirmationChoice } from '../services/HITLManager.js'; 3 | 4 | export interface BaseEvent { 5 | id?: string; 6 | timestamp?: Date; 7 | sessionId?: string; 8 | subEvents?: UIEvent[]; 9 | } 10 | 11 | export interface TaskStartedEvent extends BaseEvent { 12 | type: 'task_started'; 13 | description: string; 14 | workingDirectory: string; 15 | displayTitle: string; 16 | } 17 | 18 | export interface TaskCompletedEvent extends BaseEvent { 19 | type: 'task_completed'; 20 | displayTitle: string; 21 | terminateReason: TerminateReason; 22 | duration: number; 23 | iterations: number; 24 | summary: string; 25 | error?: string; 26 | } 27 | 28 | export interface TextGeneratedEvent extends BaseEvent { 29 | type: 'text_generated'; 30 | text: string; 31 | } 32 | 33 | export interface ThoughtGeneratedEvent extends BaseEvent { 34 | type: 'thought_generated'; 35 | iteration: number; 36 | thought: string; 37 | } 38 | 39 | export interface ToolExecutionStartedEvent extends BaseEvent { 40 | type: 'tool_execution_started'; 41 | toolName: string; 42 | iteration?: number; 43 | toolExecutionId: string; 44 | displayTitle: string; 45 | } 46 | 47 | export interface ToolExecutionCompletedEvent extends BaseEvent { 48 | type: 'tool_execution_completed'; 49 | toolName: string; 50 | success: boolean; 51 | result?: any; 52 | error?: string; 53 | duration?: number; 54 | iteration?: number; 55 | toolExecutionId: string; 56 | displayDetails?: string; 57 | } 58 | 59 | export interface ToolExecutionOutputEvent extends BaseEvent { 60 | type: 'tool_execution_output'; 61 | toolExecutionId: string; 62 | content: string; 63 | phase?: string; 64 | } 65 | 66 | export interface SystemInfoEvent extends BaseEvent { 67 | type: 'system_info'; 68 | level: 'info' | 'warning' | 'error'; 69 | message: string; 70 | context?: any; 71 | source?: 'tool' | 'system' | 'agent'; 72 | sourceId?: string; 73 | } 74 | 75 | export interface UserInputEvent extends BaseEvent { 76 | type: 'user_input'; 77 | input: string; 78 | command?: string; 79 | } 80 | 81 | export interface SessionStatsEvent extends BaseEvent { 82 | type: 'session_stats'; 83 | stats: { 84 | totalInteractions: number; 85 | totalTokensUsed: number; 86 | averageResponseTime: number; 87 | uniqueFilesAccessed: number; 88 | sessionDuration: number; 89 | }; 90 | } 91 | 92 | export interface SnapshotCreatedEvent extends BaseEvent { 93 | type: 'snapshot_created'; 94 | snapshotId: string; 95 | description: string; 96 | filesCount: number; 97 | } 98 | 99 | export interface TodoStartEvent extends BaseEvent { 100 | type: 'todo_start'; 101 | todoId: string; 102 | title: string; 103 | } 104 | 105 | export interface TodoEndEvent extends BaseEvent { 106 | type: 'todo_end'; 107 | todoId: string; 108 | } 109 | 110 | export interface ToolConfirmationRequestEvent extends BaseEvent { 111 | type: 'tool_confirmation_request'; 112 | confirmationId: string; 113 | toolName: string; 114 | args: any; 115 | description: string; 116 | options?: { 117 | showRememberOption?: boolean; 118 | defaultChoice?: ConfirmationChoice; 119 | timeout?: number; 120 | isEditOperation?: boolean; 121 | }; 122 | } 123 | 124 | export interface ToolConfirmationResponseEvent extends BaseEvent { 125 | type: 'tool_confirmation_response'; 126 | confirmationId: string; 127 | approved: boolean; 128 | choice?: ConfirmationChoice; 129 | } 130 | 131 | export type UIEvent = 132 | | TextGeneratedEvent 133 | | TaskStartedEvent 134 | | TaskCompletedEvent 135 | | SnapshotCreatedEvent 136 | | ThoughtGeneratedEvent 137 | | ToolExecutionStartedEvent 138 | | ToolExecutionCompletedEvent 139 | | ToolExecutionOutputEvent 140 | | SystemInfoEvent 141 | | UserInputEvent 142 | | SessionStatsEvent 143 | | TodoStartEvent 144 | | TodoEndEvent 145 | | ToolConfirmationRequestEvent 146 | | ToolConfirmationResponseEvent 147 | export type UIEventType = UIEvent['type']; 148 | 149 | export const UIEventType = { 150 | TaskStart: 'task_started' as const, 151 | TaskComplete: 'task_completed' as const, 152 | ThoughtGenerated: 'thought_generated' as const, 153 | TextGenerated: 'text_generated' as const, 154 | ToolExecutionStarted: 'tool_execution_started' as const, 155 | ToolExecutionCompleted: 'tool_execution_completed' as const, 156 | ToolExecutionOutput: 'tool_execution_output' as const, 157 | SystemInfo: 'system_info' as const, 158 | UserInput: 'user_input' as const, 159 | SessionStats: 'session_stats' as const, 160 | SnapshotCreated: 'snapshot_created' as const, 161 | TodoStart: 'todo_start' as const, 162 | TodoEnd: 'todo_end' as const, 163 | ToolConfirmationRequest: 'tool_confirmation_request' as const, 164 | ToolConfirmationResponse: 'tool_confirmation_response' as const, 165 | } as const; 166 | 167 | export interface EventListener { 168 | (event: T): void | Promise; 169 | } 170 | 171 | export interface EventSubscription { 172 | unsubscribe(): void; 173 | } -------------------------------------------------------------------------------- /src/indexing/FileContentCollector.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as fs from 'fs/promises'; 3 | import { encode } from 'gpt-tokenizer'; 4 | import { IndentLogger } from '../utils/IndentLogger.js'; 5 | 6 | export interface FileContent { 7 | path: string; 8 | content: string; 9 | tokens: number; 10 | language: string; 11 | truncated: boolean; 12 | } 13 | 14 | export class FileContentCollector { 15 | private readonly maxTokensPerFile = 4000; 16 | private readonly maxTotalTokens = 800000; 17 | 18 | constructor(private readonly projectRoot: string) { } 19 | 20 | async collect(importantPaths: string[]): Promise { 21 | const contents: FileContent[] = []; 22 | let totalTokens = 0; 23 | let processedFiles = 0; 24 | let skippedForTokenLimit = 0; 25 | let truncatedFiles = 0; 26 | let binaryOrLargeFiles = 0; 27 | let errorFiles = 0; 28 | 29 | for (const relativePath of importantPaths) { 30 | if (totalTokens >= this.maxTotalTokens) { 31 | skippedForTokenLimit = importantPaths.length - processedFiles; 32 | IndentLogger.log(`Token limit reached, skipping ${skippedForTokenLimit} remaining files`, 1); 33 | break; 34 | } 35 | 36 | const fullPath = path.join(this.projectRoot, relativePath); 37 | try { 38 | const fileReadResult = await this.readFileWithLimit(fullPath); 39 | if (!fileReadResult) { 40 | binaryOrLargeFiles++; 41 | continue; 42 | } 43 | 44 | const { content, truncated } = fileReadResult; 45 | const tokens = encode(content).length; 46 | const language = this.detectLanguage(relativePath); 47 | 48 | contents.push({ path: relativePath, content, tokens, language, truncated }); 49 | totalTokens += tokens; 50 | processedFiles++; 51 | if (truncated) truncatedFiles++; 52 | 53 | // 每20个文件输出一次进度 54 | if (processedFiles % 20 === 0 && processedFiles < importantPaths.length) { 55 | IndentLogger.log(`Processing files: ${processedFiles}/${importantPaths.length} (~${totalTokens} tokens)`, 1); 56 | } 57 | } catch (error) { 58 | errorFiles++; 59 | continue; 60 | } 61 | } 62 | 63 | // 简化的总结信息 64 | if (errorFiles > 0) { 65 | IndentLogger.log(`Skipped ${errorFiles} files due to read errors`, 1); 66 | } 67 | if (binaryOrLargeFiles > 0) { 68 | IndentLogger.log(`Skipped ${binaryOrLargeFiles} binary or oversized files`, 1); 69 | } 70 | 71 | return contents; 72 | } 73 | 74 | private async readFileWithLimit(filePath: string): Promise<{ content: string; truncated: boolean } | null> { 75 | try { 76 | const stats = await fs.stat(filePath); 77 | if (stats.size > 1_000_000) { 78 | return null; 79 | } 80 | 81 | const content = await fs.readFile(filePath, 'utf-8'); 82 | if (this.isBinary(content)) { 83 | return null; 84 | } 85 | 86 | const tokens = encode(content).length; 87 | if (tokens <= this.maxTokensPerFile) { 88 | return { content, truncated: false }; 89 | } 90 | 91 | const lines = content.split('\n'); 92 | let truncatedContent = ''; 93 | let currentTokens = 0; 94 | 95 | for (let i = 0; i < lines.length; i++) { 96 | const line = lines[i] + '\n'; 97 | const lineTokens = encode(line).length; 98 | if (currentTokens + lineTokens > this.maxTokensPerFile) { 99 | truncatedContent += `\n... [File truncated after ${i} lines] ...`; 100 | break; 101 | } 102 | truncatedContent += line; 103 | currentTokens += lineTokens; 104 | } 105 | 106 | return { content: truncatedContent, truncated: true }; 107 | } catch (error) { 108 | throw error; 109 | } 110 | } 111 | 112 | private detectLanguage(filePath: string): string { 113 | const ext = path.extname(filePath).toLowerCase(); 114 | const languageMap: Record = { 115 | '.js': 'javascript', '.ts': 'typescript', '.jsx': 'javascript', 116 | '.tsx': 'typescript', '.py': 'python', '.go': 'go', '.java': 'java', 117 | '.kt': 'kotlin', '.rs': 'rust', '.cs': 'csharp', '.cpp': 'cpp', 118 | '.c': 'c', '.php': 'php', '.rb': 'ruby', '.swift': 'swift', 119 | '.yml': 'yaml', '.yaml': 'yaml', '.json': 'json', '.xml': 'xml', 120 | '.md': 'markdown', '.html': 'html', '.css': 'css', 121 | }; 122 | return languageMap[ext] || 'text'; 123 | } 124 | 125 | private isBinary(content: string): boolean { 126 | const sample = content.substring(0, Math.min(512, content.length)); 127 | for (let i = 0; i < sample.length; i++) { 128 | if (sample.charCodeAt(i) === 0) { 129 | return true; 130 | } 131 | } 132 | return false; 133 | } 134 | } -------------------------------------------------------------------------------- /src/cli/components/CommandPalette.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Box, Text, useInput } from 'ink'; 3 | import { useTheme } from '../themes/index.js'; 4 | import { useUiStore } from '../stores/uiStore.js'; 5 | import { getContainer } from '../../di/container.js'; 6 | import { TYPES } from '../../di/types.js'; 7 | import { ProjectIndexer } from '../../indexing/ProjectIndexer.js'; 8 | import { UIEventEmitter } from '../../events/UIEventEmitter.js'; 9 | import { IndentLogger } from '../../utils/IndentLogger.js'; 10 | import { TaskCompletedEvent, TaskStartedEvent } from '../../events/EventTypes.js'; 11 | 12 | interface Command { 13 | name: string; 14 | label: string; 15 | description: string; 16 | } 17 | 18 | const commands: Command[] = [ 19 | { name: 'mode', label: 'Execution Mode', description: 'Switch between Code and Plan modes' }, 20 | { name: 'theme', label: 'Change Theme', description: 'Select a new color theme for the UI' }, 21 | { name: 'index', label: 'Index Project', description: 'Analyze and project structure and generate index' }, 22 | { name: 'help', label: 'Help', description: 'Show available commands and shortcuts' }, 23 | ]; 24 | 25 | interface CommandPaletteProps { 26 | onSelect: () => void; 27 | onCancel: () => void; 28 | onModeSelect: () => void; 29 | onThemeSelect: () => void; 30 | isFocused: boolean; 31 | } 32 | 33 | export const CommandPalette: React.FC = ({ onSelect, onCancel, onModeSelect, onThemeSelect, isFocused }) => { 34 | const { currentTheme } = useTheme(); 35 | const { setActivePanel } = useUiStore((state) => state.actions); 36 | const [selectedIndex, setSelectedIndex] = useState(0); 37 | 38 | const handleSelect = (selectedCommand: Command) => { 39 | if (selectedCommand.name === 'mode') { 40 | onModeSelect(); 41 | } else if (selectedCommand.name === 'theme') { 42 | onThemeSelect(); 43 | } else if (selectedCommand.name === 'help') { 44 | setActivePanel('HELP'); 45 | } else if (selectedCommand.name === 'index') { 46 | // 1. Close the command palette immediately. 47 | onSelect(); 48 | 49 | // 2. Get necessary services from the DI container. 50 | const container = getContainer(); 51 | const indexer = container.get(TYPES.ProjectIndexer); 52 | const eventEmitter = container.get(TYPES.UIEventEmitter); 53 | IndentLogger.setEventEmitter(eventEmitter); // Ensure indexer logs are sent to the UI. 54 | 55 | // 3. Start the indexing process asynchronously. 56 | (async () => { 57 | const startTime = Date.now(); 58 | // 4. Notify the UI that a background task has started. 59 | eventEmitter.emit({ 60 | type: 'task_started', 61 | displayTitle: 'Project Indexing', 62 | description: 'Analyzing project structure...', 63 | workingDirectory: process.cwd(), 64 | } as TaskStartedEvent); 65 | 66 | try { 67 | await indexer.analyze({ force: false }); 68 | // 5. Notify the UI that the task is finished (success). 69 | eventEmitter.emit({ 70 | type: 'task_completed', 71 | displayTitle: 'Indexing Finished', 72 | terminateReason: 'FINISHED', 73 | duration: Date.now() - startTime, 74 | iterations: 0, 75 | summary: 'Project indexing completed successfully.', 76 | } as TaskCompletedEvent); 77 | } catch (error) { 78 | const errorMessage = error instanceof Error ? error.message : 'Unknown error'; 79 | // 6. Notify the UI that the task is finished (error). 80 | eventEmitter.emit({ 81 | type: 'task_completed', 82 | displayTitle: 'Indexing Failed', 83 | terminateReason: 'ERROR', 84 | duration: Date.now() - startTime, 85 | iterations: 0, 86 | error: errorMessage, 87 | summary: `Project indexing failed: ${errorMessage}`, 88 | } as TaskCompletedEvent); 89 | } 90 | })(); 91 | } else { 92 | // Fallback for any other commands. 93 | onSelect(); 94 | } 95 | }; 96 | 97 | useInput( 98 | (input, key) => { 99 | if (key.upArrow) { 100 | setSelectedIndex((prev) => (prev > 0 ? prev - 1 : commands.length - 1)); 101 | } else if (key.downArrow) { 102 | setSelectedIndex((prev) => (prev < commands.length - 1 ? prev + 1 : 0)); 103 | } else if (key.return) { 104 | handleSelect(commands[selectedIndex]); 105 | } else if (key.escape) { 106 | onCancel(); 107 | } 108 | }, 109 | { isActive: isFocused }, 110 | ); 111 | 112 | return ( 113 | 114 | 115 | 116 | Command Palette 117 | 118 | 119 | {commands.map((command, index) => ( 120 | 121 | 122 | {selectedIndex === index ? '› ' : ' '} 123 | {command.label} 124 | 125 | {' '} 126 | {command.description} 127 | 128 | ))} 129 | 130 | ); 131 | }; 132 | -------------------------------------------------------------------------------- /src/config/validators/index.ts: -------------------------------------------------------------------------------- 1 | // 配置验证器模块的主要导出 2 | 3 | export { ModelConfigValidator } from './ModelConfigValidator.js'; 4 | export { SecurityConfigValidator } from './SecurityConfigValidator.js'; 5 | export { ToolsConfigValidator } from './ToolsConfigValidator.js'; 6 | export type { ValidationResult } from './ModelConfigValidator.js'; 7 | 8 | // 综合验证器类 9 | import { Config } from '../ConfigLoader.js'; 10 | import { ModelConfigValidator } from './ModelConfigValidator.js'; 11 | import { SecurityConfigValidator } from './SecurityConfigValidator.js'; 12 | import { ToolsConfigValidator } from './ToolsConfigValidator.js'; 13 | import type { ValidationResult } from './ModelConfigValidator.js'; 14 | 15 | /** 16 | * 综合配置验证器 17 | * 组合所有验证器进行完整的配置验证 18 | */ 19 | export class ConfigValidator { 20 | private modelValidator: ModelConfigValidator; 21 | private securityValidator: SecurityConfigValidator; 22 | private toolsValidator: ToolsConfigValidator; 23 | 24 | constructor() { 25 | this.modelValidator = new ModelConfigValidator(); 26 | this.securityValidator = new SecurityConfigValidator(); 27 | this.toolsValidator = new ToolsConfigValidator(); 28 | } 29 | 30 | /** 31 | * 验证完整配置 32 | * @param config 配置对象 33 | * @returns 综合验证结果 34 | */ 35 | validate(config: Config): ValidationResult { 36 | const results = [ 37 | this.modelValidator.validate(config), 38 | this.securityValidator.validate(config), 39 | this.toolsValidator.validate(config) 40 | ]; 41 | 42 | // 合并所有验证结果 43 | const combinedResult: ValidationResult = { 44 | isValid: results.every(r => r.isValid), 45 | errors: results.flatMap(r => r.errors), 46 | warnings: results.flatMap(r => r.warnings) 47 | }; 48 | 49 | return combinedResult; 50 | } 51 | 52 | /** 53 | * 异步验证(包括模型连接测试) 54 | * @param config 配置对象 55 | * @returns Promise 56 | */ 57 | async validateAsync(config: Config): Promise { 58 | // 先进行同步验证 59 | const syncResult = this.validate(config); 60 | 61 | // 如果同步验证失败,直接返回 62 | if (!syncResult.isValid) { 63 | return syncResult; 64 | } 65 | 66 | // 异步验证模型连接 67 | const connectionResult = await this.modelValidator.validateModelConnection(config); 68 | 69 | return { 70 | isValid: syncResult.isValid && connectionResult.isValid, 71 | errors: [...syncResult.errors, ...connectionResult.errors], 72 | warnings: [...syncResult.warnings, ...connectionResult.warnings] 73 | }; 74 | } 75 | 76 | /** 77 | * 获取配置优化建议 78 | * @param config 配置对象 79 | * @returns 优化建议数组 80 | */ 81 | getOptimizationRecommendations(config: Config): string[] { 82 | return [ 83 | ...this.securityValidator.getSecurityRecommendations(config), 84 | ...this.toolsValidator.getOptimizationRecommendations(config) 85 | ]; 86 | } 87 | 88 | /** 89 | * 评估配置的整体质量 90 | * @param config 配置对象 91 | * @returns 质量评分和分析 92 | */ 93 | assessConfigQuality(config: Config): { 94 | overall: number; // 0-10 95 | security: number; 96 | performance: number; 97 | completeness: number; 98 | recommendations: string[]; 99 | } { 100 | const validationResult = this.validate(config); 101 | const securityScore = this.calculateSecurityScore(config); 102 | const performanceAssessment = this.toolsValidator.assessPerformanceImpact(config); 103 | const completenessScore = this.calculateCompletenessScore(config); 104 | 105 | const overall = Math.round( 106 | (securityScore + performanceAssessment.score + completenessScore) / 3 107 | ); 108 | 109 | return { 110 | overall, 111 | security: securityScore, 112 | performance: performanceAssessment.score, 113 | completeness: completenessScore, 114 | recommendations: [ 115 | ...this.getOptimizationRecommendations(config), 116 | ...performanceAssessment.issues 117 | ] 118 | }; 119 | } 120 | 121 | /** 122 | * 计算安全分数 123 | * @param config 配置对象 124 | * @returns 安全分数 0-10 125 | */ 126 | private calculateSecurityScore(config: Config): number { 127 | const securityResult = this.securityValidator.validate(config); 128 | 129 | let score = 10; 130 | score -= securityResult.errors.length * 2; 131 | score -= securityResult.warnings.length * 0.5; 132 | 133 | return Math.max(0, Math.min(10, score)); 134 | } 135 | 136 | /** 137 | * 计算配置完整性分数 138 | * @param config 配置对象 139 | * @returns 完整性分数 0-10 140 | */ 141 | private calculateCompletenessScore(config: Config): number { 142 | let score = 0; 143 | 144 | // 基础配置 (40%) 145 | if (config.models && config.models.length > 0) score += 2; 146 | if (config.apiKey || (config.models && config.models.length > 0 && config.models[0].apiKey)) score += 2; 147 | 148 | // API密钥配置 (20%) 149 | if (config.tools.tavilyApiKey) score += 1; 150 | 151 | // 工具配置完整性 (30%) 152 | if (config.tools?.shellExecutor) score += 1; 153 | if (config.tools?.webTools) score += 1; 154 | 155 | // MCP配置 (10%) 156 | if (config.mcpServers && Object.keys(config.mcpServers).length > 0) score += 1; 157 | 158 | return Math.min(10, score); 159 | } 160 | } -------------------------------------------------------------------------------- /src/test/loop-detection-test.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /** 3 | * 测试循环检测功能 4 | */ 5 | 6 | require('ts-node/register'); 7 | const { LoopDetectionService } = require('../services/LoopDetectionService.ts'); 8 | 9 | async function testLoopDetection() { 10 | console.log('🔄 测试循环检测服务...\n'); 11 | 12 | const detector = new LoopDetectionService({ 13 | maxHistorySize: 15, 14 | exactRepeatThreshold: 3, 15 | alternatingPatternThreshold: 4, 16 | parameterCycleThreshold: 4, 17 | timeWindowMs: 30000, 18 | }); 19 | 20 | console.log('1. 测试精确重复检测:'); 21 | // 模拟相同工具和参数的重复调用 22 | for (let i = 0; i < 4; i++) { 23 | const result = detector.addAndCheck({ 24 | toolName: 'shell_executor', 25 | parameters: { command: 'git status', description: 'Check git status' }, 26 | }); 27 | 28 | console.log(` 调用 ${i + 1}: ${result.isLoop ? '🚨 检测到循环' : '✅ 正常'}`); 29 | if (result.isLoop) { 30 | console.log(` 类型: ${result.loopType}`); 31 | console.log(` 描述: ${result.description}`); 32 | console.log(` 建议: ${result.suggestion}`); 33 | break; 34 | } 35 | } 36 | 37 | console.log('\n2. 测试交替模式检测:'); 38 | detector.clearHistory(); 39 | 40 | // 模拟 A-B-A-B 交替模式 41 | const alternatingCalls = [ 42 | { toolName: 'read_file', parameters: { path: '/test/file1.txt' } }, 43 | { toolName: 'write_file', parameters: { path: '/test/file1.txt', content: 'test' } }, 44 | { toolName: 'read_file', parameters: { path: '/test/file1.txt' } }, 45 | { toolName: 'write_file', parameters: { path: '/test/file1.txt', content: 'test' } }, 46 | { toolName: 'read_file', parameters: { path: '/test/file1.txt' } }, 47 | ]; 48 | 49 | alternatingCalls.forEach((call, index) => { 50 | const result = detector.addAndCheck(call); 51 | console.log(` 调用 ${index + 1} (${call.toolName}): ${result.isLoop ? '🚨 检测到循环' : '✅ 正常'}`); 52 | if (result.isLoop) { 53 | console.log(` 类型: ${result.loopType}`); 54 | console.log(` 描述: ${result.description}`); 55 | } 56 | }); 57 | 58 | console.log('\n3. 测试参数循环检测:'); 59 | detector.clearHistory(); 60 | 61 | // 模拟同一工具的参数循环 62 | const parameterCycle = [ 63 | { toolName: 'find_files', parameters: { pattern: '*.js' } }, 64 | { toolName: 'find_files', parameters: { pattern: '*.ts' } }, 65 | { toolName: 'find_files', parameters: { pattern: '*.json' } }, 66 | { toolName: 'find_files', parameters: { pattern: '*.js' } }, // 重复 67 | { toolName: 'find_files', parameters: { pattern: '*.ts' } }, // 重复 68 | { toolName: 'find_files', parameters: { pattern: '*.js' } }, // 再次重复 69 | ]; 70 | 71 | parameterCycle.forEach((call, index) => { 72 | const result = detector.addAndCheck(call); 73 | console.log(` 调用 ${index + 1} (${call.parameters.pattern}): ${result.isLoop ? '🚨 检测到循环' : '✅ 正常'}`); 74 | if (result.isLoop) { 75 | console.log(` 类型: ${result.loopType}`); 76 | console.log(` 描述: ${result.description}`); 77 | } 78 | }); 79 | 80 | console.log('\n4. 测试工具序列循环检测:'); 81 | detector.clearHistory(); 82 | 83 | // 模拟工具序列的重复 84 | const toolSequence = [ 85 | { toolName: 'git_status', parameters: {} }, 86 | { toolName: 'git_add', parameters: { files: ['.'] } }, 87 | { toolName: 'git_commit', parameters: { message: 'update' } }, 88 | { toolName: 'git_status', parameters: {} }, // 重复序列开始 89 | { toolName: 'git_add', parameters: { files: ['.'] } }, 90 | { toolName: 'git_commit', parameters: { message: 'update' } }, 91 | ]; 92 | 93 | toolSequence.forEach((call, index) => { 94 | const result = detector.addAndCheck(call); 95 | console.log(` 调用 ${index + 1} (${call.toolName}): ${result.isLoop ? '🚨 检测到循环' : '✅ 正常'}`); 96 | if (result.isLoop) { 97 | console.log(` 类型: ${result.loopType}`); 98 | console.log(` 描述: ${result.description}`); 99 | } 100 | }); 101 | 102 | console.log('\n5. 测试正常调用(不应触发循环):'); 103 | detector.clearHistory(); 104 | 105 | const normalCalls = [ 106 | { toolName: 'read_file', parameters: { path: '/test/file1.txt' } }, 107 | { toolName: 'read_file', parameters: { path: '/test/file2.txt' } }, 108 | { toolName: 'write_file', parameters: { path: '/test/output.txt', content: 'result' } }, 109 | { toolName: 'shell_executor', parameters: { command: 'npm test' } }, 110 | ]; 111 | 112 | normalCalls.forEach((call, index) => { 113 | const result = detector.addAndCheck(call); 114 | console.log(` 调用 ${index + 1} (${call.toolName}): ${result.isLoop ? '❌ 意外循环' : '✅ 正常'}`); 115 | }); 116 | 117 | console.log('\n6. 测试统计信息:'); 118 | const stats = detector.getStats(); 119 | console.log(` 总调用数: ${stats.totalCalls}`); 120 | console.log(` 唯一工具数: ${stats.uniqueTools}`); 121 | console.log(` 最常用工具: ${stats.mostUsedTool || 'None'}`); 122 | console.log(` 会话时长: ${Math.round(stats.recentTimespan / 1000)}秒`); 123 | 124 | console.log('\n7. 测试配置更新:'); 125 | detector.updateConfig({ 126 | exactRepeatThreshold: 2, // 降低阈值 127 | alternatingPatternThreshold: 3, 128 | }); 129 | 130 | console.log(' 配置已更新,测试新阈值:'); 131 | detector.clearHistory(); 132 | 133 | // 只需要2次重复就应该触发 134 | for (let i = 0; i < 3; i++) { 135 | const result = detector.addAndCheck({ 136 | toolName: 'test_tool', 137 | parameters: { test: 'value' }, 138 | }); 139 | 140 | console.log(` 调用 ${i + 1}: ${result.isLoop ? '🚨 检测到循环(新阈值)' : '✅ 正常'}`); 141 | if (result.isLoop) { 142 | break; 143 | } 144 | } 145 | 146 | console.log('\n🎉 循环检测功能测试完成!'); 147 | 148 | console.log('\n📋 功能验证总结:'); 149 | console.log(' ✅ 精确重复检测 - 连续相同工具调用'); 150 | console.log(' ✅ 交替模式检测 - A-B-A-B 模式'); 151 | console.log(' ✅ 参数循环检测 - 同工具不同参数循环'); 152 | console.log(' ✅ 工具序列循环检测 - 工具序列重复'); 153 | console.log(' ✅ 正常调用不误报 - 合理的工具使用'); 154 | console.log(' ✅ 配置动态更新 - 灵活的阈值调整'); 155 | console.log(' ✅ 统计信息收集 - 使用情况分析'); 156 | } 157 | 158 | testLoopDetection().catch(console.error); 159 | -------------------------------------------------------------------------------- /src/tools/GitTools.ts: -------------------------------------------------------------------------------- 1 | import { exec } from 'child_process'; 2 | import * as util from 'util'; 3 | import { z } from 'zod'; 4 | import { tool } from 'ai'; 5 | import { ToolContext, ToolNames } from './ToolRegistry.js'; 6 | import { ToolExecutionStartedEvent, SystemInfoEvent } from '../events/EventTypes.js'; 7 | import { ToolExecutionResult } from './ToolRegistry.js'; 8 | 9 | const execAsync = util.promisify(exec); 10 | 11 | export const createGitStatusTool = (context: ToolContext) => tool({ 12 | description: 'Get the current git repository status', 13 | inputSchema: z.object({ 14 | toolExecutionId: z.string().optional().describe('Tool execution ID (auto-generated)'), 15 | }), 16 | execute: async ({ toolExecutionId }): Promise => { 17 | const displayTitle = `GitStatus()`; 18 | 19 | context.eventEmitter.emit({ 20 | type: 'tool_execution_started', 21 | toolName: ToolNames.GIT_STATUS, 22 | toolExecutionId: toolExecutionId!, 23 | displayTitle, 24 | } as ToolExecutionStartedEvent); 25 | 26 | try { 27 | const { stdout } = await execAsync('git status --porcelain'); 28 | const statusOutput = stdout.trim() || 'Working directory clean'; 29 | const files = stdout.trim() ? stdout.trim().split('\n').map(line => line.substring(3)) : []; 30 | 31 | return { 32 | result: { status: statusOutput, files, filesChanged: files.length }, 33 | displayDetails: files.length > 0 ? files.join('\n') : 'Working directory clean', 34 | }; 35 | } catch (error) { 36 | context.eventEmitter.emit({ 37 | type: 'system_info', 38 | level: 'error', 39 | source: 'tool', 40 | sourceId: toolExecutionId!, 41 | message: `Git status failed: ${error instanceof Error ? error.message : 'Unknown error'}` 42 | } as SystemInfoEvent); 43 | 44 | return { 45 | result: null, 46 | displayDetails: `Failed: ${error instanceof Error ? error.message : 'Unknown error'}`, 47 | }; 48 | } 49 | }, 50 | }); 51 | 52 | export const createGitLogTool = (context: ToolContext) => tool({ 53 | description: 'Get recent commit history', 54 | inputSchema: z.object({ 55 | count: z.number().default(10).describe('Number of commits to show'), 56 | toolExecutionId: z.string().optional().describe('Tool execution ID (auto-generated)'), 57 | }), 58 | execute: async ({ count, toolExecutionId }): Promise => { 59 | const displayTitle = `GitLog(${count})`; 60 | 61 | context.eventEmitter.emit({ 62 | type: 'tool_execution_started', 63 | toolName: ToolNames.GIT_LOG, 64 | toolExecutionId: toolExecutionId!, 65 | displayTitle, 66 | } as ToolExecutionStartedEvent); 67 | 68 | try { 69 | const { stdout } = await execAsync(`git log --oneline -${count}`); 70 | const commits = stdout.trim().split('\n').filter(line => line.length > 0); 71 | 72 | return { 73 | result: { commits, commitCount: commits.length }, 74 | displayDetails: commits.join('\n'), 75 | }; 76 | } catch (error) { 77 | context.eventEmitter.emit({ 78 | type: 'system_info', 79 | level: 'error', 80 | source: 'tool', 81 | sourceId: toolExecutionId!, 82 | message: `Git log failed: ${error instanceof Error ? error.message : 'Unknown error'}` 83 | } as SystemInfoEvent); 84 | 85 | return { 86 | result: null, 87 | displayDetails: `Failed: ${error instanceof Error ? error.message : 'Unknown error'}`, 88 | }; 89 | } 90 | }, 91 | }); 92 | 93 | export const createGitDiffTool = (context: ToolContext) => tool({ 94 | description: 'Show changes in working directory', 95 | inputSchema: z.object({ 96 | file: z.string().optional().describe('Specific file to show diff for'), 97 | toolExecutionId: z.string().optional().describe('Tool execution ID (auto-generated)'), 98 | }), 99 | execute: async ({ file, toolExecutionId }): Promise => { 100 | const displayTitle = `GitDiff(${file || 'all'})`; 101 | 102 | context.eventEmitter.emit({ 103 | type: 'tool_execution_started', 104 | toolName: ToolNames.GIT_DIFF, 105 | toolExecutionId: toolExecutionId!, 106 | displayTitle, 107 | } as ToolExecutionStartedEvent); 108 | 109 | try { 110 | const command = file ? `git diff ${file}` : 'git diff'; 111 | const { stdout } = await execAsync(command); 112 | const diffOutput = stdout.trim() || 'No changes'; 113 | const linesChanged = diffOutput === 'No changes' ? 0 : 114 | diffOutput.split('\n').filter(line => line.startsWith('+') || line.startsWith('-')).length; 115 | 116 | return { 117 | result: { diff: diffOutput, linesChanged, file: file || 'all files' }, 118 | displayDetails: diffOutput, 119 | }; 120 | } catch (error) { 121 | context.eventEmitter.emit({ 122 | type: 'system_info', 123 | level: 'error', 124 | source: 'tool', 125 | sourceId: toolExecutionId!, 126 | message: `Git diff failed: ${error instanceof Error ? error.message : 'Unknown error'}` 127 | } as SystemInfoEvent); 128 | 129 | return { 130 | result: null, 131 | displayDetails: `Failed: ${error instanceof Error ? error.message : 'Unknown error'}`, 132 | }; 133 | } 134 | }, 135 | }); 136 | 137 | export const registerGitTools = (registry: any) => { 138 | const context = registry.getContext(); 139 | registry.registerMultiple([ 140 | { name: ToolNames.GIT_STATUS, tool: createGitStatusTool(context), category: 'git' }, 141 | { name: ToolNames.GIT_LOG, tool: createGitLogTool(context), category: 'git' }, 142 | { name: ToolNames.GIT_DIFF, tool: createGitDiffTool(context), category: 'git' } 143 | ]); 144 | }; 145 | -------------------------------------------------------------------------------- /src/utils/Logger.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | import * as os from 'os'; 4 | import { injectable } from 'inversify'; 5 | 6 | export enum LogLevel { 7 | DEBUG = 0, 8 | INFO = 1, 9 | WARN = 2, 10 | ERROR = 3 11 | } 12 | 13 | export interface LogEntry { 14 | timestamp: string; 15 | level: string; 16 | message: string; 17 | data?: any; 18 | source?: string; 19 | } 20 | 21 | @injectable() 22 | export class Logger { 23 | private logLevel: LogLevel = LogLevel.INFO; 24 | private logDir: string; 25 | private logFile: string; 26 | private originalConsole: { 27 | log: typeof console.log; 28 | info: typeof console.info; 29 | warn: typeof console.warn; 30 | error: typeof console.error; 31 | debug: typeof console.debug; 32 | }; 33 | 34 | constructor() { 35 | // 确定日志目录 - 优先使用项目级别,否则使用全局 36 | const projectLogDir = path.join(process.cwd(), '.tempurai'); 37 | const globalLogDir = path.join(os.homedir(), '.tempurai'); 38 | 39 | // 检查项目目录是否存在或可以创建 40 | try { 41 | fs.mkdirSync(projectLogDir, { recursive: true }); 42 | this.logDir = projectLogDir; 43 | } catch { 44 | // 如果项目目录创建失败,使用全局目录 45 | fs.mkdirSync(globalLogDir, { recursive: true }); 46 | this.logDir = globalLogDir; 47 | } 48 | 49 | const dateStr = new Date().toISOString().split('T')[0]; 50 | this.logFile = path.join(this.logDir, `tempurai-${dateStr}.log`); 51 | 52 | // 保存原始console方法 53 | this.originalConsole = { 54 | log: console.log.bind(console), 55 | info: console.info.bind(console), 56 | warn: console.warn.bind(console), 57 | error: console.error.bind(console), 58 | debug: console.debug.bind(console) 59 | }; 60 | 61 | this.initLogFile(); 62 | } 63 | 64 | private initLogFile(): void { 65 | try { 66 | if (!fs.existsSync(this.logFile)) { 67 | const welcomeEntry: LogEntry = { 68 | timestamp: new Date().toISOString(), 69 | level: 'INFO', 70 | message: 'Tempurai logging system initialized', 71 | data: { 72 | logFile: this.logFile, 73 | pid: process.pid, 74 | version: process.version 75 | } 76 | }; 77 | fs.writeFileSync(this.logFile, JSON.stringify(welcomeEntry) + '\n', 'utf8'); 78 | } 79 | } catch (error) { 80 | // 如果无法写入日志文件,至少在控制台输出错误 81 | this.originalConsole.error('Failed to initialize log file:', error); 82 | } 83 | } 84 | 85 | private writeToFile(entry: LogEntry): void { 86 | try { 87 | const logLine = JSON.stringify(entry, null, 0) + '\n'; 88 | fs.appendFileSync(this.logFile, logLine, 'utf8'); 89 | } catch (error) { 90 | // 静默处理文件写入错误,避免无限循环 91 | } 92 | } 93 | 94 | private formatMessage(...args: any[]): string { 95 | return args.map(arg => { 96 | if (typeof arg === 'object') { 97 | try { 98 | return JSON.stringify(arg, null, 2); 99 | } catch { 100 | return String(arg); 101 | } 102 | } 103 | return String(arg); 104 | }).join(' '); 105 | } 106 | 107 | public interceptConsole(): void { 108 | // 拦截console.log并重定向到logger 109 | console.log = (...args: any[]) => { 110 | const message = this.formatMessage(...args); 111 | this.log(LogLevel.INFO, message); 112 | // 不再输出到原始控制台,只记录到文件 113 | }; 114 | 115 | // 拦截console.info 116 | console.info = (...args: any[]) => { 117 | const message = this.formatMessage(...args); 118 | this.log(LogLevel.INFO, message); 119 | }; 120 | 121 | // 拦截console.warn 122 | console.warn = (...args: any[]) => { 123 | const message = this.formatMessage(...args); 124 | this.log(LogLevel.WARN, message); 125 | }; 126 | 127 | // 拦截console.error 128 | console.error = (...args: any[]) => { 129 | const message = this.formatMessage(...args); 130 | this.log(LogLevel.ERROR, message); 131 | }; 132 | 133 | // 拦截console.debug 134 | console.debug = (...args: any[]) => { 135 | const message = this.formatMessage(...args); 136 | this.log(LogLevel.DEBUG, message); 137 | }; 138 | } 139 | 140 | public setLogLevel(level: LogLevel): void { 141 | this.logLevel = level; 142 | } 143 | 144 | public log(level: LogLevel, message: string, data?: any, source?: string): void { 145 | if (level < this.logLevel) { 146 | return; 147 | } 148 | 149 | const entry: LogEntry = { 150 | timestamp: new Date().toISOString(), 151 | level: LogLevel[level], 152 | message, 153 | data, 154 | source 155 | }; 156 | 157 | this.writeToFile(entry); 158 | } 159 | 160 | public debug(message: string, data?: any, source?: string): void { 161 | this.log(LogLevel.DEBUG, message, data, source); 162 | } 163 | 164 | public info(message: string, data?: any, source?: string): void { 165 | this.log(LogLevel.INFO, message, data, source); 166 | } 167 | 168 | public warn(message: string, data?: any, source?: string): void { 169 | this.log(LogLevel.WARN, message, data, source); 170 | } 171 | 172 | public error(message: string, data?: any, source?: string): void { 173 | this.log(LogLevel.ERROR, message, data, source); 174 | } 175 | 176 | // 恢复原始console(用于清理) 177 | public restoreConsole(): void { 178 | console.log = this.originalConsole.log; 179 | console.info = this.originalConsole.info; 180 | console.warn = this.originalConsole.warn; 181 | console.error = this.originalConsole.error; 182 | console.debug = this.originalConsole.debug; 183 | } 184 | 185 | // 清理旧的日志文件(保留最近7天) 186 | public cleanupOldLogs(): void { 187 | try { 188 | const files = fs.readdirSync(this.logDir); 189 | const logFiles = files.filter(file => file.startsWith('tempurai-') && file.endsWith('.log')); 190 | const cutoffDate = new Date(); 191 | cutoffDate.setDate(cutoffDate.getDate() - 7); 192 | 193 | for (const file of logFiles) { 194 | const filePath = path.join(this.logDir, file); 195 | const stat = fs.statSync(filePath); 196 | if (stat.mtime < cutoffDate) { 197 | fs.unlinkSync(filePath); 198 | this.info('Cleaned up old log file', { file }, 'CLEANUP'); 199 | } 200 | } 201 | } catch (error) { 202 | this.error('Failed to cleanup old logs', { error: error instanceof Error ? error.message : error }, 'CLEANUP'); 203 | } 204 | } 205 | } 206 | 207 | // 创建全局Logger实例 208 | export const logger = new Logger(); -------------------------------------------------------------------------------- /src/di/container.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { Container } from 'inversify'; 3 | import { Config, ConfigLoader } from '../config/ConfigLoader.js'; 4 | import { DefaultModelFactory } from '../models/index.js'; 5 | import { ToolAgent } from '../agents/tool_agent/ToolAgent.js'; 6 | import { SmartAgent } from '../agents/smart_agent/SmartAgent.js'; 7 | import { AgentOrchestrator } from '../agents/smart_agent/AgentOrchestrator.js'; 8 | import { TodoManager } from '../agents/smart_agent/TodoManager.js'; 9 | import { SubAgent } from '../agents/smart_agent/SubAgent.js'; 10 | import { CompressedAgent } from '../agents/compressed_agent/CompressedAgent.js'; 11 | import { SessionService } from '../services/SessionService.js'; 12 | import { FileWatcherService } from '../services/FileWatcherService.js'; 13 | import { UIEventEmitter } from '../events/UIEventEmitter.js'; 14 | import { SessionServiceFactory } from './interfaces.js'; 15 | import { TYPES } from './types.js'; 16 | import type { LanguageModel } from 'ai'; 17 | import { HITLManager } from '../services/HITLManager.js'; 18 | import { InterruptService } from '../services/InterruptService.js'; 19 | import { ToolRegistry } from '../tools/ToolRegistry.js'; 20 | import { SecurityPolicyEngine } from '../security/SecurityPolicyEngine.js'; 21 | import { Logger } from '../utils/Logger.js'; 22 | import { CompressorService } from '../services/CompressorService.js'; 23 | import { EditModeManager } from '../services/EditModeManager.js'; 24 | import { ToolInterceptor } from '../agents/smart_agent/ToolInterceptor.js'; 25 | import { ProjectIndexer } from '../indexing/ProjectIndexer.js'; 26 | 27 | export { TYPES } from './types.js'; 28 | 29 | export function createContainer(): Container { 30 | const container = new Container(); 31 | 32 | // Configuration 33 | container.bind(TYPES.ConfigLoader).to(ConfigLoader).inSingletonScope(); 34 | container.bind(TYPES.Config) 35 | .toDynamicValue(() => { 36 | const configLoader = container.get(TYPES.ConfigLoader); 37 | return configLoader.getConfig(); 38 | }) 39 | .inSingletonScope(); 40 | 41 | container.bind(TYPES.ModelFactory).to(DefaultModelFactory).inSingletonScope(); 42 | container.bind(TYPES.LanguageModel) 43 | .toDynamicValue(async () => { 44 | const config = container.get(TYPES.Config); 45 | const modelFactory = container.get(TYPES.ModelFactory); 46 | 47 | if (!config.models || config.models.length === 0) { 48 | throw new Error('No models configured. Please add at least one model to your configuration.'); 49 | } 50 | 51 | const firstModel = config.models[0]; 52 | console.log('🔄 正在初始化AI模型...'); 53 | const model = await modelFactory.createModel(firstModel); 54 | console.log(`✅ 模型已初始化: ${firstModel.provider}:${firstModel.name}`); 55 | return model; 56 | }) 57 | .inSingletonScope(); 58 | 59 | // Core services 60 | container.bind(TYPES.UIEventEmitter).toDynamicValue(() => new UIEventEmitter()).inSingletonScope(); 61 | container.bind(TYPES.FileWatcherService).to(FileWatcherService).inSingletonScope(); 62 | container.bind(TYPES.Logger).to(Logger).inSingletonScope(); 63 | container.bind(TYPES.SecurityPolicyEngine).to(SecurityPolicyEngine).inSingletonScope(); 64 | container.bind(TYPES.ToolRegistry).to(ToolRegistry).inSingletonScope(); 65 | 66 | // Request-scoped services 67 | container.bind(TYPES.InterruptService).to(InterruptService).inRequestScope(); 68 | container.bind(TYPES.EditModeManager).to(EditModeManager).inRequestScope(); 69 | container.bind(TYPES.HITLManager).to(HITLManager).inRequestScope(); 70 | container.bind(TYPES.CompressorService).to(CompressorService).inRequestScope(); 71 | 72 | // Agents and managers 73 | container.bind(TYPES.TodoManager).to(TodoManager).inSingletonScope(); 74 | 75 | // Tool agents 76 | container.bind(TYPES.ToolAgent).to(ToolAgent); 77 | container.bind(TYPES.ToolInterceptor).to(ToolInterceptor); 78 | 79 | // AI agents 80 | container.bind(TYPES.SmartAgent).to(SmartAgent); 81 | container.bind(TYPES.AgentOrchestrator).to(AgentOrchestrator); 82 | container.bind(TYPES.SubAgent).to(SubAgent); 83 | container.bind(TYPES.CompressedAgent).to(CompressedAgent); 84 | 85 | // Indexing services 86 | container.bind(TYPES.ProjectIndexer).to(ProjectIndexer).inSingletonScope(); 87 | 88 | // Session factory 89 | container.bind(TYPES.SessionServiceFactory) 90 | .toFactory(() => { 91 | return () => { 92 | const toolAgent = container.get(TYPES.ToolAgent); 93 | const fileWatcherService = container.get(TYPES.FileWatcherService); 94 | const config = container.get(TYPES.Config); 95 | const eventEmitter = container.get(TYPES.UIEventEmitter); 96 | const interruptService = container.get(TYPES.InterruptService); 97 | const toolRegistry = container.get(TYPES.ToolRegistry); 98 | const todoManager = container.get(TYPES.TodoManager); 99 | const compressorService = container.get(TYPES.CompressorService); 100 | const editModeManager = container.get(TYPES.EditModeManager); 101 | 102 | const sessionService = new SessionService( 103 | toolAgent, 104 | fileWatcherService, 105 | config, 106 | eventEmitter, 107 | interruptService, 108 | toolRegistry, 109 | todoManager, 110 | compressorService, 111 | editModeManager, 112 | ); 113 | 114 | return { 115 | sessionService, 116 | clearSession(): void { 117 | sessionService.clearSession(); 118 | interruptService.reset(); 119 | editModeManager.reset(); 120 | } 121 | }; 122 | }; 123 | }); 124 | 125 | console.log('依赖注入容器已配置完成'); 126 | return container; 127 | } 128 | 129 | let _container: Container | null = null; 130 | 131 | export function getContainer(): Container { 132 | if (!_container) { 133 | _container = createContainer(); 134 | } 135 | return _container; 136 | } 137 | 138 | export function resetContainer(): void { 139 | if (_container) { 140 | _container.unbindAll(); 141 | _container = null; 142 | } 143 | } -------------------------------------------------------------------------------- /src/cli/components/ThemeSelector.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Box, Text, useInput } from 'ink'; 3 | import { useTheme } from '../themes/index.js'; 4 | import { themes } from '../themes/themes/index.js'; 5 | import { CodePreview } from './CodePreview.js'; 6 | 7 | interface ThemeSelectorProps { 8 | onThemeSelected: () => void; 9 | onCancel: () => void; 10 | isFocused: boolean; 11 | } 12 | 13 | interface ThemeSelectorWithPreviewProps { 14 | onThemeSelected: () => void; 15 | onCancel?: () => void; 16 | } 17 | 18 | const getThemePreview = (theme: string) => { 19 | switch (theme) { 20 | case 'dark': 21 | return '🌑 Professional dark theme (Claude Code style)'; 22 | case 'light': 23 | return '🌕 Clean light theme for daytime coding'; 24 | case 'monokai': 25 | return '🟫 Classic Monokai - warm dark colors'; 26 | case 'solarized': 27 | return '🟡 Solarized Dark - eye-friendly colors'; 28 | case 'dracula': 29 | return '🧛 Dracula - dark with vibrant purples'; 30 | case 'high-contrast': 31 | return '⚫ High contrast for accessibility'; 32 | default: 33 | return 'Theme preview'; 34 | } 35 | }; 36 | 37 | export const ThemeSelector: React.FC = ({ onThemeSelected, onCancel, isFocused }) => { 38 | const { currentTheme, availableThemes = [], setTheme } = useTheme(); 39 | const [selectedIndex, setSelectedIndex] = useState(() => { 40 | const currentIndex = availableThemes.findIndex((theme) => theme === currentTheme?.name); 41 | return currentIndex >= 0 ? currentIndex : 0; 42 | }); 43 | 44 | useInput( 45 | (input, key) => { 46 | if (!availableThemes.length) return; 47 | if (key.upArrow) { 48 | setSelectedIndex((prev) => (prev > 0 ? prev - 1 : availableThemes.length - 1)); 49 | } else if (key.downArrow) { 50 | setSelectedIndex((prev) => (prev < availableThemes.length - 1 ? prev + 1 : 0)); 51 | } else if (key.return) { 52 | setTheme(availableThemes[selectedIndex]); 53 | onThemeSelected(); 54 | } else if (key.escape) { 55 | onCancel(); 56 | } 57 | }, 58 | { isActive: isFocused }, 59 | ); 60 | 61 | const c = currentTheme?.colors ?? ({} as any); 62 | const primary = c.primary ?? 'cyan'; 63 | const accent = c.accent ?? 'magenta'; 64 | const textPrimary = c.text?.primary ?? 'white'; 65 | const textMuted = c.text?.muted ?? 'gray'; 66 | 67 | return ( 68 | 69 | 70 | 71 | 🎨 Choose Your Theme 72 | 73 | 74 | 75 | {availableThemes.map((theme, index) => ( 76 | 77 | 78 | 79 | {index === selectedIndex ? '⏵ ' : ' '} 80 | {theme.charAt(0).toUpperCase() + theme.slice(1).replace('-', ' ')} 81 | 82 | 83 | {getThemePreview(theme)} 84 | 85 | ))} 86 | {!availableThemes.length && No themes found.} 87 | 88 | 89 | 90 | ↑/↓ Navigate • Enter Select • Esc Cancel 91 | 92 | 93 | 94 | ); 95 | }; 96 | 97 | export const ThemeSelectorWithPreview: React.FC = ({ onThemeSelected, onCancel }) => { 98 | const { currentTheme, availableThemes = [], setTheme } = useTheme(); 99 | const [selectedIndex, setSelectedIndex] = useState(0); 100 | const [previewTheme, setPreviewTheme] = useState(availableThemes[0] ?? 'dark'); 101 | 102 | useEffect(() => { 103 | if (availableThemes.length > 0) { 104 | setPreviewTheme(availableThemes[selectedIndex]); 105 | } 106 | }, [selectedIndex, availableThemes]); 107 | 108 | useInput((input, key) => { 109 | if (!availableThemes.length) return; 110 | if (key.upArrow) { 111 | setSelectedIndex((prev) => (prev > 0 ? prev - 1 : availableThemes.length - 1)); 112 | } else if (key.downArrow) { 113 | setSelectedIndex((prev) => (prev < availableThemes.length - 1 ? prev + 1 : 0)); 114 | } else if (key.return) { 115 | setTheme(availableThemes[selectedIndex]); 116 | onThemeSelected(); 117 | } else if (key.escape && onCancel) { 118 | onCancel(); 119 | } 120 | }); 121 | 122 | const c = currentTheme?.colors ?? ({} as any); 123 | const primary = c.primary ?? 'cyan'; 124 | const accent = c.accent ?? 'magenta'; 125 | const textPrimary = c.text?.primary ?? 'white'; 126 | const textSecondary = c.text?.secondary ?? 'white'; 127 | const textMuted = c.text?.muted ?? 'gray'; 128 | 129 | return ( 130 | 131 | {} 132 | 133 | 134 | 🔍 Live Preview — {previewTheme?.charAt(0).toUpperCase() + previewTheme?.slice(1)} 135 | 136 | 137 | 138 | {} 139 | 140 | 141 | 142 | 🎨 Choose Your Theme 143 | 144 | 145 | 146 | {availableThemes.map((theme, index) => ( 147 | 148 | 149 | 150 | {index === selectedIndex ? '⏵ ' : ' '} 151 | {theme.charAt(0).toUpperCase() + theme.slice(1).replace('-', ' ')} 152 | 153 | 154 | {getThemePreview(theme)} 155 | 156 | ))} 157 | {!availableThemes.length && No themes found.} 158 | 159 | 160 | 161 | Use ↑↓ to navigate, Enter to select 162 | You can change themes later with /theme [name] or Ctrl+T 163 | 164 | 165 | 166 | 167 | ); 168 | }; 169 | -------------------------------------------------------------------------------- /src/test/config.ts: -------------------------------------------------------------------------------- 1 | import { Config } from '../config/ConfigLoader.js'; 2 | 3 | /** 4 | * Test configuration for SessionService e2e tests 5 | * Uses mocked LLM and configured for testing 6 | */ 7 | export const TEST_CONFIG: Config = { 8 | models: [{ 9 | provider: 'openai', 10 | name: 'gpt-4o-mini', 11 | apiKey: 'test-mock-key', 12 | baseUrl: 'http://localhost:3001/v1', // Mock server URL 13 | options: {} 14 | }], 15 | temperature: 0.1, 16 | maxTokens: 1000, 17 | tools: { 18 | shellExecutor: { 19 | defaultTimeout: 5000, 20 | maxRetries: 1, 21 | security: { 22 | allowlist: ['echo', 'cat', 'ls'], 23 | blocklist: [], 24 | allowUnlistedCommands: false, 25 | allowDangerousCommands: false 26 | } 27 | }, 28 | webTools: { 29 | requestTimeout: 5000, 30 | maxContentLength: 5000, 31 | userAgent: 'Test-Agent/1.0', 32 | enableCache: false 33 | } 34 | }, 35 | customContext: 'This is a test environment for automated testing.', 36 | mcpServers: {} 37 | }; 38 | 39 | /** 40 | * Mock LLM responses for different scenarios 41 | */ 42 | export const MOCK_LLM_RESPONSES = { 43 | // Simple task completion 44 | SIMPLE_TASK: { 45 | text: ` 46 | 47 | I need to handle a simple test task. This appears to be a basic request that I can complete immediately. 48 | 49 | 50 | Handle simple test task 51 | completed 52 | ${new Date().toISOString()} 53 | 54 | Task completed successfully 55 | 56 | This was a simple test task that required no additional actions. 57 | 58 | 59 | finish 60 | {} 61 | 62 | ` 63 | }, 64 | 65 | // File reading task 66 | READ_FILE_TASK: { 67 | text: ` 68 | 69 | I need to read a file to understand its contents. Let me use the read_file tool to accomplish this. 70 | 71 | 72 | Read file contents 73 | implementing 74 | ${new Date().toISOString()} 75 | 76 | Read the specified file 77 | Analyze file contents 78 | Provide summary 79 | 80 | Using read_file tool to access file contents. 81 | 82 | 83 | read_file 84 | {"file_path": "test.txt"} 85 | 86 | ` 87 | }, 88 | 89 | // Code analysis task 90 | CODE_ANALYSIS_TASK: { 91 | text: ` 92 | 93 | I need to analyze the code structure to understand the project layout and identify key components. 94 | 95 | 96 | Analyze project code structure 97 | analyzing 98 | ${new Date().toISOString()} 99 | 100 | Get project structure 101 | Analyze key files 102 | Provide insights 103 | 104 | Starting with project structure analysis. 105 | 106 | 107 | get_project_structure 108 | {"max_depth": 3} 109 | 110 | ` 111 | }, 112 | 113 | // Multi-step task 114 | MULTI_STEP_TASK: { 115 | text: ` 116 | 117 | This is a complex task that will require multiple steps. Let me start by understanding the current project state. 118 | 119 | 120 | Execute multi-step development task 121 | planning 122 | ${new Date().toISOString()} 123 | 124 | Analyze current state 125 | Plan implementation approach 126 | Execute changes 127 | Test changes 128 | Document results 129 | 130 | Starting with project analysis to understand current state. 131 | 132 | 133 | analyze_code_structure 134 | {"target_path": "src", "analysis_depth": "detailed"} 135 | 136 | ` 137 | }, 138 | 139 | // Error scenario 140 | ERROR_RESPONSE: { 141 | text: ` 142 | 143 | I encountered an error while processing this request. I should report this issue and suggest alternatives. 144 | 145 | 146 | Handle error scenario 147 | error 148 | ${new Date().toISOString()} 149 | 150 | Report error details 151 | Suggest alternatives 152 | 153 | An error occurred during processing. Need to provide helpful error information. 154 | 155 | 156 | finish 157 | {"error": "Simulated error for testing purposes", "success": false} 158 | 159 | ` 160 | } 161 | }; 162 | 163 | /** 164 | * Mock tool responses for testing 165 | */ 166 | export const MOCK_TOOL_RESPONSES = { 167 | read_file: { 168 | success: true, 169 | content: "This is mock file content for testing purposes.", 170 | path: "test.txt", 171 | size: 45 172 | }, 173 | 174 | write_file: { 175 | success: true, 176 | message: "File written successfully", 177 | path: "test-output.txt", 178 | bytes_written: 100 179 | }, 180 | 181 | get_project_structure: { 182 | success: true, 183 | structure: { 184 | "src/": { 185 | "agents/": ["SimpleAgent.ts", "ReActAgent.ts"], 186 | "session/": ["SessionService.ts"], 187 | "config/": ["ConfigLoader.ts"], 188 | "test/": ["SessionService.test.ts"] 189 | } 190 | }, 191 | total_files: 5, 192 | total_directories: 4 193 | }, 194 | 195 | analyze_code_structure: { 196 | success: true, 197 | analysis: { 198 | files_analyzed: 3, 199 | classes_found: 2, 200 | functions_found: 15, 201 | imports_count: 8, 202 | complexity_score: "moderate" 203 | }, 204 | insights: ["Code is well-structured", "Good separation of concerns"] 205 | }, 206 | 207 | shell_executor: { 208 | success: true, 209 | stdout: "Mock command executed successfully", 210 | stderr: "", 211 | exit_code: 0, 212 | execution_time: 150 213 | }, 214 | 215 | finish: { 216 | success: true, 217 | message: "Task completed successfully", 218 | completed: true 219 | } 220 | }; -------------------------------------------------------------------------------- /src/test/MockAISDK.ts: -------------------------------------------------------------------------------- 1 | import { MOCK_LLM_RESPONSES, MOCK_TOOL_RESPONSES } from './config.js'; 2 | 3 | /** 4 | * Mock implementation of AI SDK generateText for testing 5 | */ 6 | export class MockAISDK { 7 | private static instance: MockAISDK; 8 | private callHistory: Array<{ 9 | prompt: string; 10 | system?: string; 11 | tools?: any; 12 | timestamp: Date; 13 | }> = []; 14 | private responseIndex = 0; 15 | private mockToolExecutions: Record = {}; 16 | private nextResponse: string | null = null; 17 | private nextError: Error | null = null; 18 | private nextToolResponse: { [toolName: string]: any } = {}; 19 | private nextToolError: { [toolName: string]: Error } = {}; 20 | private nextDelay: number = 0; 21 | 22 | static getInstance(): MockAISDK { 23 | if (!MockAISDK.instance) { 24 | MockAISDK.instance = new MockAISDK(); 25 | } 26 | return MockAISDK.instance; 27 | } 28 | 29 | /** 30 | * Create a mock language model 31 | */ 32 | createMockModel() { 33 | return { 34 | provider: 'openai', 35 | modelId: 'gpt-4o-mini', 36 | settings: {}, 37 | specificationVersion: '2', 38 | supportedUrls: [], 39 | doGenerate: async () => ({ text: 'Mock response' }), 40 | doStream: async function* () { yield { text: 'Mock response' }; } 41 | }; 42 | } 43 | 44 | /** 45 | * Reset mock state for a new test 46 | */ 47 | reset(): void { 48 | this.callHistory = []; 49 | this.responseIndex = 0; 50 | this.mockToolExecutions = {}; 51 | this.nextResponse = null; 52 | this.nextError = null; 53 | this.nextToolResponse = {}; 54 | this.nextToolError = {}; 55 | this.nextDelay = 0; 56 | } 57 | 58 | /** 59 | * Mock generateText function that returns predefined responses 60 | */ 61 | async generateText(options: { 62 | model?: any; 63 | system?: string; 64 | prompt: string; 65 | tools?: any; 66 | maxOutputTokens?: number; 67 | temperature?: number; 68 | }): Promise<{ text: string; toolCalls?: any[] }> { 69 | // Add delay if specified 70 | if (this.nextDelay > 0) { 71 | await new Promise(resolve => setTimeout(resolve, this.nextDelay)); 72 | this.nextDelay = 0; 73 | } 74 | 75 | // Handle error simulation 76 | if (this.nextError) { 77 | const error = this.nextError; 78 | this.nextError = null; 79 | throw error; 80 | } 81 | 82 | // Record the call 83 | this.callHistory.push({ 84 | prompt: options.prompt, 85 | system: options.system, 86 | tools: options.tools, 87 | timestamp: new Date() 88 | }); 89 | 90 | // Return predetermined response or determine based on content 91 | let response = MOCK_LLM_RESPONSES.SIMPLE_TASK; 92 | 93 | if (this.nextResponse) { 94 | return { text: this.nextResponse, toolCalls: [] }; 95 | } 96 | 97 | if (options.prompt.toLowerCase().includes('read') || options.prompt.toLowerCase().includes('file')) { 98 | response = MOCK_LLM_RESPONSES.READ_FILE_TASK; 99 | } else if (options.prompt.toLowerCase().includes('analyze') || options.prompt.toLowerCase().includes('code')) { 100 | response = MOCK_LLM_RESPONSES.CODE_ANALYSIS_TASK; 101 | } else if (options.prompt.toLowerCase().includes('complex') || options.prompt.toLowerCase().includes('multi')) { 102 | response = MOCK_LLM_RESPONSES.MULTI_STEP_TASK; 103 | } else if (options.prompt.toLowerCase().includes('error') || options.prompt.toLowerCase().includes('fail')) { 104 | response = MOCK_LLM_RESPONSES.ERROR_RESPONSE; 105 | } 106 | 107 | // Simulate some processing delay 108 | await new Promise(resolve => setTimeout(resolve, 10)); 109 | 110 | return { 111 | text: response.text, 112 | toolCalls: [] // We'll handle tool calls through our mock tools 113 | }; 114 | } 115 | 116 | /** 117 | * Mock tool execution 118 | */ 119 | async executeTool(toolName: string, args: any): Promise { 120 | // Handle tool error simulation 121 | if (this.nextToolError[toolName]) { 122 | const error = this.nextToolError[toolName]; 123 | delete this.nextToolError[toolName]; 124 | throw error; 125 | } 126 | 127 | // Record tool execution 128 | this.mockToolExecutions[toolName] = this.mockToolExecutions[toolName] || []; 129 | this.mockToolExecutions[toolName].push({ 130 | args, 131 | timestamp: new Date() 132 | }); 133 | 134 | // Get predetermined response or default 135 | const result = this.nextToolResponse[toolName] || 136 | MOCK_TOOL_RESPONSES[toolName as keyof typeof MOCK_TOOL_RESPONSES] || 137 | { success: true }; 138 | delete this.nextToolResponse[toolName]; 139 | 140 | // Return mock response based on tool name 141 | if (result) { 142 | return { 143 | ...result, 144 | args_received: args, 145 | execution_id: Math.random().toString(36).substr(2, 9) 146 | }; 147 | } 148 | 149 | // Default response for unknown tools 150 | return { 151 | success: false, 152 | error: `Mock: Unknown tool '${toolName}'`, 153 | args_received: args 154 | }; 155 | } 156 | 157 | // Control methods for tests 158 | setNextResponse(response: string) { 159 | this.nextResponse = response; 160 | } 161 | 162 | setNextError(error: Error) { 163 | this.nextError = error; 164 | } 165 | 166 | setNextToolResponse(toolName: string, response: any) { 167 | this.nextToolResponse[toolName] = response; 168 | } 169 | 170 | setNextToolError(toolName: string, error: Error) { 171 | this.nextToolError[toolName] = error; 172 | } 173 | 174 | setNextDelay(ms: number) { 175 | this.nextDelay = ms; 176 | } 177 | 178 | /** 179 | * Get call history for testing assertions 180 | */ 181 | getCallHistory() { 182 | return [...this.callHistory]; 183 | } 184 | 185 | /** 186 | * Get tool execution history 187 | */ 188 | getToolExecutions() { 189 | return { ...this.mockToolExecutions }; 190 | } 191 | 192 | getLastCall() { 193 | return this.callHistory[this.callHistory.length - 1]; 194 | } 195 | 196 | getLastToolCall() { 197 | const allCalls = Object.entries(this.mockToolExecutions) 198 | .flatMap(([name, executions]) => 199 | executions.map((exec: any) => ({ name, ...exec })) 200 | ) 201 | .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); 202 | 203 | return allCalls.length > 0 ? { name: allCalls[0].name, args: allCalls[0].args } : null; 204 | } 205 | 206 | /** 207 | * Get statistics about mock usage 208 | */ 209 | getStats() { 210 | return { 211 | totalCalls: this.callHistory.length, 212 | uniqueTools: Object.keys(this.mockToolExecutions).length, 213 | totalToolExecutions: Object.values(this.mockToolExecutions).reduce((acc, executions) => acc + executions.length, 0), 214 | averagePromptLength: this.callHistory.reduce((acc, call) => acc + call.prompt.length, 0) / Math.max(1, this.callHistory.length) 215 | }; 216 | } 217 | } 218 | 219 | /** 220 | * Global mock instance for easy access in tests 221 | */ 222 | export const mockAISDK = MockAISDK.getInstance(); -------------------------------------------------------------------------------- /docs/architecture.md: -------------------------------------------------------------------------------- 1 | # Architecture Overview 2 | 3 | Tempurai Coder is architected as a layered, event-driven system. The core design principle is the separation of concerns, where the user interface, application state, agentic reasoning, and environment interactions are all decoupled. 4 | 5 | The system's intelligence is centered around a **multi-component reasoning engine**, designed to handle complex, multi-step software engineering tasks in a structured and transparent manner. 6 | 7 | ### High-Level Flow 8 | 9 | The system's operation follows a clear, unidirectional flow. The UI captures user intent, which is passed to the service layer for management. The service layer then invokes the agentic core, which performs its reasoning and uses tools to interact with the environment. All feedback to the UI is sent via an asynchronous event bus. 10 | 11 | **Simple ASCII Flow:** 12 | 13 | `CLI (Ink UI) <--> Session & Service Layer <--> Agentic Core -> Tooling System` 14 | 15 | ### Core Modules 16 | 17 | #### 1. The Agentic Core (The "Brain") 18 | 19 | This is the heart of the application, responsible for all reasoning, planning, and decision-making. It operates through a distinct, two-phase lifecycle for any non-trivial task: Planning and Execution. 20 | 21 | ##### **Phase 1: The Planning Phase** 22 | 23 | Before taking any action, the `SmartAgent` first enters a dedicated planning phase. It analyzes the user's request to understand its complexity and requirements. For any task that requires more than a single step, it formulates a comprehensive strategy. This initial plan is not just an internal thought; it is made concrete and transparent through a key component: 24 | 25 | - **`TodoManager` (The Planner)**: This is the backbone of the agent's strategic capability. The `SmartAgent` uses it to create a structured, step-by-step plan, breaking the main goal into a checklist of smaller, manageable tasks. 26 | - **Why is this important?** This design choice provides immense value: 27 | 1. **Transparency**: The user can see the agent's exact plan of action. 28 | 2. **Structured Execution**: The agent follows the plan systematically, preventing it from getting lost or stuck in loops. 29 | 3. **Resilience**: If a step fails, the plan makes it easier to adapt and continue. 30 | 31 | ##### **Phase 2: The Execution Loop & Component Roles** 32 | 33 | Once a plan is in place, the `SmartAgent` begins the execution loop, orchestrating a set of specialized components to carry out the plan. 34 | 35 | - **`SmartAgent` (Strategic Layer)**: The high-level orchestrator that directs the entire process. It queries the `TodoManager` for the next step, decides which tools are needed, and processes the results to inform its next move. 36 | 37 | - **`ToolAgent` (Execution Layer)**: The tactical agent that serves as the **common execution layer** for the entire agentic core. It receives concrete instructions from either the `SmartAgent` or a `SubAgent` (e.g., "run this shell command") and is solely responsible for interfacing with the Tooling System. 38 | 39 | - **`SubAgent` (The Specialist)**: For highly complex, self-contained tasks, the `SmartAgent` can delegate the work to an autonomous `SubAgent`. This is a powerful delegation pattern. 40 | - **When is it used?** For tasks like "Perform a deep analysis of the entire codebase to find all usages of this deprecated library." 41 | - **Benefit**: The `SubAgent` works in an isolated context. It follows the same architectural pattern as the main agent—performing its own reasoning and then **directing the `ToolAgent`** to execute its commands. It returns only the final, comprehensive result. 42 | 43 | - **`CompressedAgent` (The Memory Manager)**: To handle long conversations without losing context, this agent is responsible for the system's "long-term memory." It periodically compresses older parts of the conversation history into a concise summary that is fed back into the `SmartAgent`'s context. 44 | 45 | - **`AgentOrchestrator` (The Governor)**: This is a meta-level component that monitors the `SmartAgent`'s execution loop. Its key feature is the **`LoopDetector`**, which analyzes the agent's recent actions to identify repetitive, non-productive behavior and prevent the agent from getting stuck. 46 | 47 | - **Agentic Interaction Model (ASCII)**: 48 | 49 | ``` 50 | +-----------------+ +--------------------+ +--------------------+ 51 | | User's Goal |----->| Session Service |----->| Planning Phase | 52 | +-----------------+ +--------------------+ +--------------------+ 53 | | (Compress Context) | (creates plan) 54 | v v 55 | +-------------------+ +--------------------+ 56 | | CompressedAgent | | SmartAgent |----uses---->+-------------+ 57 | | (provides memory) |<-----| (Execution Loop) | | TodoManager | 58 | +-------------------+ +--------------------+<------------+-------------+ 59 | | | Agent | 60 | +--------------------+--------------------------+ | Orchestrator | 61 | | (delegates) | (instructs) | (LoopDetector)| 62 | v v +---------------+ 63 | +----------------------+ +-------------+ +----------------+ 64 | | SubAgent |----->| ToolAgent |----->| Tooling System | 65 | | (for complex tasks) | +-------------+ +----------------+ 66 | +----------------------+ 67 | ``` 68 | 69 | #### 2. The Session & Service Layer (The "Coordinator") 70 | 71 | This layer acts as the central coordinator and safety guard between the UI and the Agentic Core. 72 | 73 | - **`SessionService`**: The main entry point for any task. It receives the initial request from the UI, manages the conversation history, and orchestrates the overall process by invoking the `SmartAgent`. 74 | - **Safety Services**: This layer provides critical safety features that make the agent's actions reliable and reversible. 75 | - **`SnapshotManager`**: Before a task begins, this service automatically creates a temporary Git commit of the current project state. 76 | - **`HITLManager` (Human-in-the-Loop)**: For any file-system modification, this service pauses execution and waits for explicit user approval before proceeding. 77 | 78 | #### 3. The Tooling System (The "Hands") 79 | 80 | This module provides the agents with the ability to interact with the developer's environment. 81 | 82 | - **Core Principle: Shell First**: The primary and most powerful tool is the `ShellExecutor`. This design choice is intentional, as it allows the agent to behave like a human developer—using `ls`, `grep`, `cat`, and `git` to explore, analyze, and interact with the project. 83 | - **Specialized Tools**: For operations where shell commands are unsafe or inefficient (like applying a complex patch), dedicated tools (`apply_patch`) are used instead. 84 | - **Security**: The **`SecurityPolicyEngine`** acts as a final gatekeeper, validating every shell command against configurable security rules before it is executed. 85 | 86 | #### 4. The CLI Front-End (The "Interface") 87 | 88 | The UI is a terminal application built with **React and the Ink library**. 89 | 90 | - **Event-Driven and Decoupled**: The UI is completely decoupled from the backend logic. It simply subscribes to a `UIEventEmitter` and renders the stream of events (e.g., `TaskStarted`, `ToolExecutionCompleted`, `ThoughtGenerated`) that are emitted by the backend services and agents. 91 | -------------------------------------------------------------------------------- /docs/cli-schema.md: -------------------------------------------------------------------------------- 1 | ## Complete Interface Display Schema 2 | 3 | ### Schema Template Format 4 | 5 | ``` 6 | # User input event 7 | > {input_text} 8 | L {error_message} 9 | L {retry_info} 10 | 11 | # AI thinking/text output 12 | ● {thought_or_response} 13 | 14 | # Todo/ProgressIndicator display 15 | ## When Todo exists: 16 | > Current task: {current_todo_title} 17 | L Next: {next_todo_title} 18 | 19 | ## When no Todo: 20 | ● {system_status_message} 21 | 22 | # Tool execution event 23 | ## Executing: 24 | ~ {tool_name}({args}) 25 | L {progress_info} 26 | L {intermediate_output} 27 | 28 | ## Execution successful: 29 | ✓ {tool_name}({args}) 30 | L {success_output} 31 | L {result_details} 32 | 33 | ## Execution failed: 34 | ✗ {tool_name}({args}) 35 | L {error_message} 36 | L {retry_attempt} 37 | 38 | # Todo status update event 39 | ✓ Todo "{todo_title}" status updated: {status} 40 | L {additional_info} 41 | L {system_message} 42 | 43 | # System-level error (standalone display) 44 | ● {system_error_message} 45 | ! {critical_error_message} 46 | 47 | # Symbol explanation 48 | > = User input/current task 49 | ● = AI thinking/text output/system status 50 | ~ = Tool executing 51 | ✓ = Tool execution successful/Todo completed 52 | ✗ = Tool execution failed/Todo failed 53 | ! = Critical system error 54 | L = Unified indentation symbol (all sub-content) 55 | ``` 56 | 57 | ### Complete Scenario Examples 58 | 59 | #### Scenario 1: Normal execution flow 60 | 61 | ``` 62 | > Fix test case 63 | 64 | ● I'll analyze the JWT implementation and identify areas for improvement 65 | 66 | > Current task: Analyze JWT implementation logic in src/auth.ts 67 | L Next: Add rate limiting middleware to API routes 68 | 69 | ~ Bash(cat src/auth.ts) 70 | L Reading file content... 71 | L Successfully read file content (247 lines) 72 | 73 | ● Found JWT implementation, now searching for best practices 74 | 75 | ✓ WebSearch(JWT best practices) 76 | L Found 5 sources: JWT Documentation, OWASP Guide 77 | L Retrieved implementation examples 78 | 79 | ● Based on the search results, I'll now update the authentication logic 80 | 81 | ✓ Apply Patch(src/auth.ts) 82 | L Updated authentication logic (12 lines changed) 83 | L Added proper error handling 84 | 85 | ● Task completed successfully, moving to next item 86 | 87 | > Current task: Add rate limiting middleware to API routes 88 | ``` 89 | 90 | #### Scenario 2: Tool execution error 91 | 92 | ``` 93 | > Fix test case 94 | 95 | ● Let me examine the authentication implementation 96 | 97 | > Current task: Analyze JWT implementation logic in src/auth.ts 98 | L Next: Add rate limiting middleware to API routes 99 | 100 | ~ Bash(cat src/auth.ts) 101 | L Attempting to read file... 102 | L File not found: src/auth.ts 103 | L Error: No such file or directory 104 | L Retrying with alternative path... 105 | L Found file at ./auth/auth.ts 106 | 107 | ● I found the file in a different location, now searching for best practices 108 | 109 | ✗ WebSearch(JWT best practices) 110 | L API Error (429): Rate limit exceeded 111 | L Search temporarily unavailable 112 | L Will retry in 30 seconds 113 | 114 | ● Proceeding with file analysis while waiting for search 115 | 116 | > Current task: Analyze JWT implementation logic in src/auth.ts 117 | L Next: Add rate limiting middleware to API routes 118 | ``` 119 | 120 | #### Scenario 3: Initial stage error 121 | 122 | ``` 123 | > Fix test case 124 | L API Error (401 token expired) - Retrying in 4 seconds (attempt 4/10) 125 | L Failed to initialize SmartAgent 126 | L Connection timeout, retrying... 127 | L Attempting fallback connection... 128 | 129 | ● Analyzing your request and creating execution plan 130 | ``` 131 | 132 | #### Scenario 4: System error after task completion 133 | 134 | ``` 135 | > Fix test case 136 | 137 | ✓ Todo "Analyze JWT implementation logic in src/auth.ts" status updated: completed 138 | L Context compression failed 139 | L Memory allocation warning 140 | L Attempting garbage collection... 141 | 142 | ● Task completed successfully, moving to next item 143 | 144 | > Current task: Add rate limiting middleware to API routes 145 | ``` 146 | 147 | #### Scenario 5: No Todo state 148 | 149 | ``` 150 | > Fix test case 151 | L Planning task structure... 152 | L Connection timeout, retrying... 153 | L Establishing secure connection... 154 | 155 | ● Ready for your next task 156 | ``` 157 | 158 | #### Scenario 6: Todo status changes 159 | 160 | ``` 161 | > Refactor user system 162 | 163 | > Current task: Backup existing user data 164 | L Next: Update database schema 165 | L Then: Migrate user accounts 166 | 167 | ~ Bash(cp -r users/ backup/) 168 | L Copying user data... 169 | L Progress: 1.2GB / 3.4GB 170 | L Estimated time: 2 minutes 171 | 172 | ✓ Todo "Backup existing user data" status updated: completed 173 | 174 | > Current task: Update database schema 175 | L Next: Migrate user accounts 176 | ``` 177 | 178 | #### Scenario 7: Mixed error handling 179 | 180 | ``` 181 | > Analyze project dependencies 182 | 183 | ● I'll examine the project structure and check for dependency issues 184 | 185 | > Current task: Scan dependency files 186 | L Next: Analyze version conflicts 187 | 188 | ~ Bash(find . -name "package.json") 189 | L Searching project structure... 190 | L ./package.json 191 | L ./frontend/package.json 192 | L ./backend/package.json 193 | 194 | ✓ WebSearch(npm audit security) 195 | L Found security scanning tools 196 | L Retrieved best practices guide 197 | 198 | ~ Bash(npm audit) 199 | L Running security audit... 200 | L Found 3 vulnerabilities (2 moderate, 1 high) 201 | L Generating detailed report... 202 | 203 | ● Found security issues, analyzing each vulnerability 204 | 205 | > Current task: Scan dependency files 206 | L Next: Analyze version conflicts 207 | ``` 208 | 209 | #### Scenario 8: Long-running task 210 | 211 | ``` 212 | > Run complete test suite 213 | 214 | ● Starting comprehensive test execution 215 | 216 | > Current task: Execute unit tests 217 | L Next: Execute integration tests 218 | 219 | ~ Bash(npm test) 220 | L Running 247 test suites... 221 | L Progress: 45/247 suites completed 222 | L Estimated time remaining: 8 minutes 223 | L Current: testing auth module 224 | 225 | ● Tests are running, this may take several minutes 226 | 227 | > Current task: Execute unit tests 228 | L Next: Execute integration tests 229 | ``` 230 | 231 | #### Scenario 9: Critical system error 232 | 233 | ``` 234 | > Deploy to production 235 | 236 | ● Preparing production deployment 237 | 238 | > Current task: Build production version 239 | L Next: Upload to server 240 | 241 | ✗ Bash(npm run build) 242 | L Build process started... 243 | L Error: Out of memory (heap limit exceeded) 244 | L Available: 1.2GB, Required: 2.1GB 245 | 246 | ! Critical Error: Insufficient memory for build process 247 | L Build process terminated 248 | L Deployment cancelled for safety 249 | 250 | ● Deployment aborted due to resource constraints 251 | ``` 252 | 253 | #### Scenario 10: Network error recovery 254 | 255 | ``` 256 | > Update project dependencies 257 | 258 | ● Checking for available package updates 259 | 260 | > Current task: Update npm packages 261 | L Next: Verify compatibility 262 | 263 | ~ Bash(npm update) 264 | L Connecting to registry... 265 | L Network timeout: registry.npmjs.org 266 | L Retrying with different registry... 267 | L Connected to mirror registry 268 | L Downloading updates... 269 | 270 | ✓ WebSearch(npm registry alternatives) 271 | L Found reliable mirror registries 272 | L Performance comparison data 273 | 274 | ● Successfully connected to backup registry, proceeding with updates 275 | 276 | > Current task: Update npm packages 277 | L Next: Verify compatibility 278 | ``` 279 | --------------------------------------------------------------------------------