orc-server 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/__tests__/auth.test.js +49 -0
  2. package/dist/__tests__/watcher.test.js +58 -0
  3. package/dist/claude/chatService.js +175 -0
  4. package/dist/claude/sessionBrowser.js +743 -0
  5. package/dist/claude/watcher.js +242 -0
  6. package/dist/config.js +44 -0
  7. package/dist/files/browser.js +227 -0
  8. package/dist/files/reader.js +159 -0
  9. package/dist/files/search.js +124 -0
  10. package/dist/git/gitHandler.js +177 -0
  11. package/dist/git/gitService.js +299 -0
  12. package/dist/git/index.js +8 -0
  13. package/dist/http/server.js +96 -0
  14. package/dist/index.js +77 -0
  15. package/dist/ssh/index.js +9 -0
  16. package/dist/ssh/sshHandler.js +205 -0
  17. package/dist/ssh/sshManager.js +329 -0
  18. package/dist/terminal/index.js +11 -0
  19. package/dist/terminal/localTerminalHandler.js +176 -0
  20. package/dist/terminal/localTerminalManager.js +497 -0
  21. package/dist/terminal/terminalWebSocket.js +136 -0
  22. package/dist/types.js +2 -0
  23. package/dist/utils/logger.js +42 -0
  24. package/dist/websocket/auth.js +18 -0
  25. package/dist/websocket/server.js +631 -0
  26. package/package.json +66 -0
  27. package/web-dist/assets/highlight-l0sNRNKZ.js +1 -0
  28. package/web-dist/assets/index-C8TJGN-T.css +41 -0
  29. package/web-dist/assets/index-DjLLxjMD.js +39 -0
  30. package/web-dist/assets/markdown-C_j0ZeeY.js +51 -0
  31. package/web-dist/assets/react-vendor-CqP5oCk4.js +9 -0
  32. package/web-dist/assets/xterm-BCk906R6.js +9 -0
  33. package/web-dist/icon-192.png +0 -0
  34. package/web-dist/icon-512.png +0 -0
  35. package/web-dist/index.html +23 -0
  36. package/web-dist/manifest.json +24 -0
  37. package/web-dist/sw.js +35 -0
@@ -0,0 +1,242 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ClaudeWatcher = void 0;
7
+ const events_1 = require("events");
8
+ const chokidar_1 = __importDefault(require("chokidar"));
9
+ const promises_1 = require("fs/promises");
10
+ const path_1 = require("path");
11
+ const config_1 = require("../config");
12
+ const logger_1 = require("../utils/logger");
13
+ class ClaudeWatcher extends events_1.EventEmitter {
14
+ constructor() {
15
+ super();
16
+ this.historyWatcher = null;
17
+ this.sessionWatcher = null;
18
+ this.lastHistorySize = 0;
19
+ this.lastSessionSize = 0;
20
+ this.activeSessionId = null;
21
+ this.activeProjectPath = null;
22
+ // Queue mechanism for handling rapid updates
23
+ this.messageQueue = [];
24
+ this.isProcessing = false;
25
+ this.historyPath = (0, path_1.join)(config_1.CONFIG.claudeHome, 'history.jsonl');
26
+ }
27
+ async start() {
28
+ logger_1.logger.info('Starting Claude Code watcher');
29
+ await this.watchHistory();
30
+ this.on('user_input', (data) => {
31
+ if (data.sessionId !== this.activeSessionId) {
32
+ this.activeSessionId = data.sessionId;
33
+ this.activeProjectPath = this.projectPathToDir(data.project);
34
+ this.watchSession();
35
+ }
36
+ });
37
+ }
38
+ stop() {
39
+ logger_1.logger.info('Stopping Claude Code watcher');
40
+ if (this.historyWatcher) {
41
+ this.historyWatcher.close();
42
+ }
43
+ if (this.sessionWatcher) {
44
+ this.sessionWatcher.close();
45
+ }
46
+ }
47
+ async watchHistory() {
48
+ this.historyWatcher = chokidar_1.default.watch(this.historyPath, {
49
+ persistent: true,
50
+ usePolling: false,
51
+ awaitWriteFinish: {
52
+ stabilityThreshold: 100,
53
+ pollInterval: 50
54
+ }
55
+ });
56
+ this.historyWatcher.on('change', async () => {
57
+ await this.processHistoryChanges();
58
+ });
59
+ this.historyWatcher.on('error', (error) => {
60
+ logger_1.logger.error('History watcher error:', error);
61
+ });
62
+ logger_1.logger.info(`Watching history: ${this.historyPath}`);
63
+ }
64
+ async processHistoryChanges() {
65
+ try {
66
+ const content = await (0, promises_1.readFile)(this.historyPath, 'utf-8');
67
+ const lines = content.split('\n').filter(l => l.trim());
68
+ if (lines.length > this.lastHistorySize) {
69
+ const newLines = lines.slice(this.lastHistorySize);
70
+ // Add to queue instead of processing immediately
71
+ this.messageQueue.push(...newLines.map(line => ({ type: 'history', line })));
72
+ // Start processing if not already processing
73
+ if (!this.isProcessing) {
74
+ this.isProcessing = true;
75
+ await this.processQueue();
76
+ this.isProcessing = false;
77
+ }
78
+ this.lastHistorySize = lines.length;
79
+ }
80
+ }
81
+ catch (error) {
82
+ logger_1.logger.error('Error processing history changes:', error);
83
+ }
84
+ }
85
+ projectPathToDir(path) {
86
+ return path.replace(/\//g, '-').replace(/^-/, '');
87
+ }
88
+ watchSession() {
89
+ if (!this.activeSessionId || !this.activeProjectPath)
90
+ return;
91
+ if (this.sessionWatcher) {
92
+ this.sessionWatcher.close();
93
+ }
94
+ const sessionPath = (0, path_1.join)(config_1.CONFIG.claudeHome, 'projects', this.activeProjectPath, `${this.activeSessionId}.jsonl`);
95
+ this.lastSessionSize = 0;
96
+ this.sessionWatcher = chokidar_1.default.watch(sessionPath, {
97
+ persistent: true,
98
+ usePolling: false,
99
+ awaitWriteFinish: {
100
+ stabilityThreshold: 100,
101
+ pollInterval: 50
102
+ }
103
+ });
104
+ this.sessionWatcher.on('change', async () => {
105
+ await this.processSessionChanges(sessionPath);
106
+ });
107
+ this.sessionWatcher.on('error', (error) => {
108
+ logger_1.logger.error('Session watcher error:', error);
109
+ });
110
+ logger_1.logger.info(`Watching session: ${sessionPath}`);
111
+ }
112
+ async processSessionChanges(sessionPath) {
113
+ try {
114
+ const content = await (0, promises_1.readFile)(sessionPath, 'utf-8');
115
+ const lines = content.split('\n').filter(l => l.trim());
116
+ if (lines.length > this.lastSessionSize) {
117
+ const newLines = lines.slice(this.lastSessionSize);
118
+ // Add to queue
119
+ this.messageQueue.push(...newLines.map(line => ({ type: 'session', line })));
120
+ // Start processing if not already processing
121
+ if (!this.isProcessing) {
122
+ this.isProcessing = true;
123
+ await this.processQueue();
124
+ this.isProcessing = false;
125
+ }
126
+ this.lastSessionSize = lines.length;
127
+ }
128
+ }
129
+ catch (error) {
130
+ logger_1.logger.error('Error processing session changes:', error);
131
+ }
132
+ }
133
+ async processQueue() {
134
+ while (this.messageQueue.length > 0) {
135
+ const batch = this.messageQueue.splice(0, 10);
136
+ for (const item of batch) {
137
+ try {
138
+ if (item.type === 'history') {
139
+ const entry = JSON.parse(item.line);
140
+ this.emit('user_input', {
141
+ message: entry.display,
142
+ timestamp: entry.timestamp,
143
+ sessionId: entry.sessionId,
144
+ project: entry.project
145
+ });
146
+ logger_1.logger.debug(`User input: ${entry.display.substring(0, 50)}...`);
147
+ }
148
+ else if (item.type === 'session') {
149
+ const entry = JSON.parse(item.line);
150
+ await this.processSessionEntry(entry);
151
+ }
152
+ }
153
+ catch (error) {
154
+ logger_1.logger.error('Error processing queue item:', error);
155
+ }
156
+ }
157
+ await new Promise(resolve => setTimeout(resolve, 100));
158
+ }
159
+ }
160
+ async processSessionEntry(entry) {
161
+ switch (entry.type) {
162
+ case 'assistant':
163
+ this.handleAssistantMessage(entry);
164
+ break;
165
+ case 'system':
166
+ this.handleSystemMessage(entry);
167
+ break;
168
+ case 'progress':
169
+ this.emit('progress', {
170
+ message: entry.data?.message,
171
+ timestamp: entry.timestamp
172
+ });
173
+ break;
174
+ }
175
+ }
176
+ handleAssistantMessage(entry) {
177
+ const content = entry.message?.content || [];
178
+ for (const block of content) {
179
+ if (block.type === 'text') {
180
+ this.emit('assistant_message', {
181
+ content: block.text,
182
+ timestamp: entry.timestamp,
183
+ messageId: entry.message.id
184
+ });
185
+ logger_1.logger.debug(`Assistant: ${block.text.substring(0, 50)}...`);
186
+ }
187
+ else if (block.type === 'tool_use') {
188
+ this.emit('tool_call', {
189
+ toolName: block.name,
190
+ toolId: block.id,
191
+ input: block.input,
192
+ timestamp: entry.timestamp
193
+ });
194
+ logger_1.logger.debug(`Tool: ${block.name}`);
195
+ if (['Edit', 'Write'].includes(block.name)) {
196
+ this.handleFileOperation(block, entry.timestamp);
197
+ }
198
+ }
199
+ }
200
+ }
201
+ handleSystemMessage(entry) {
202
+ if (entry.data?.type === 'tool_result') {
203
+ this.emit('tool_result', {
204
+ toolId: entry.data.tool_use_id,
205
+ content: entry.data.content,
206
+ timestamp: entry.timestamp
207
+ });
208
+ }
209
+ }
210
+ async handleFileOperation(toolUse, timestamp) {
211
+ const { name, input } = toolUse;
212
+ const filePath = input.file_path;
213
+ if (!filePath)
214
+ return;
215
+ try {
216
+ const oldContent = await (0, promises_1.readFile)(filePath, 'utf-8').catch(() => '');
217
+ let newContent = '';
218
+ if (name === 'Edit') {
219
+ newContent = oldContent.replace(input.old_string, input.new_string);
220
+ }
221
+ else if (name === 'Write') {
222
+ newContent = input.content;
223
+ }
224
+ this.emit('file_change', {
225
+ filePath,
226
+ operation: name.toLowerCase(),
227
+ oldContent,
228
+ newContent,
229
+ timestamp
230
+ });
231
+ }
232
+ catch (error) {
233
+ logger_1.logger.error('Error handling file operation:', error);
234
+ }
235
+ }
236
+ computeSimpleDiff(oldContent, newContent) {
237
+ const oldLines = oldContent.split('\n');
238
+ const newLines = newContent.split('\n');
239
+ return `+${newLines.length - oldLines.length} lines`;
240
+ }
241
+ }
242
+ exports.ClaudeWatcher = ClaudeWatcher;
package/dist/config.js ADDED
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CONFIG = void 0;
4
+ const dotenv_1 = require("dotenv");
5
+ const os_1 = require("os");
6
+ (0, dotenv_1.config)();
7
+ /**
8
+ * Parse CLI arguments: --port, --host, --token, --claude-home
9
+ * These take precedence over environment variables.
10
+ */
11
+ function parseArgs() {
12
+ const args = {};
13
+ const argv = process.argv.slice(2);
14
+ for (let i = 0; i < argv.length; i++) {
15
+ const arg = argv[i];
16
+ if (arg.startsWith('--') && i + 1 < argv.length && !argv[i + 1].startsWith('--')) {
17
+ const key = arg.slice(2);
18
+ args[key] = argv[i + 1];
19
+ i++;
20
+ }
21
+ else if (arg.startsWith('--') && arg.includes('=')) {
22
+ const [key, value] = arg.slice(2).split('=');
23
+ args[key] = value;
24
+ }
25
+ }
26
+ return args;
27
+ }
28
+ const args = parseArgs();
29
+ exports.CONFIG = {
30
+ host: args['host'] || process.env.HOST || '0.0.0.0',
31
+ port: parseInt(args['port'] || process.env.PORT || '9080'),
32
+ authToken: args['token'] || process.env.AUTH_TOKEN || '',
33
+ claudeHome: args['claude-home'] || process.env.CLAUDE_HOME || `${(0, os_1.homedir)()}/.claude`,
34
+ maxFileSize: parseInt(process.env.MAX_FILE_SIZE || '10485760'),
35
+ searchTimeout: parseInt(process.env.SEARCH_TIMEOUT || '5000'),
36
+ logLevel: args['log-level'] || process.env.LOG_LEVEL || 'info',
37
+ // File tree limits to prevent OOM on large directories
38
+ fileTreeMaxDepth: parseInt(process.env.FILE_TREE_MAX_DEPTH || '3'),
39
+ fileTreeMaxNodes: parseInt(process.env.FILE_TREE_MAX_NODES || '5000'),
40
+ fileTreeExpandMaxNodes: parseInt(process.env.FILE_TREE_EXPAND_MAX_NODES || '500'),
41
+ };
42
+ if (!exports.CONFIG.authToken) {
43
+ console.warn('WARNING: AUTH_TOKEN not set. Generate: openssl rand -hex 32');
44
+ }
@@ -0,0 +1,227 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fileBrowser = exports.FileBrowser = void 0;
4
+ const promises_1 = require("fs/promises");
5
+ const path_1 = require("path");
6
+ const logger_1 = require("../utils/logger");
7
+ class FileBrowser {
8
+ constructor() {
9
+ this.ignoredDirs = new Set([
10
+ 'node_modules',
11
+ '.git',
12
+ 'dist',
13
+ 'build',
14
+ '.next',
15
+ 'coverage',
16
+ '.cache',
17
+ '__pycache__',
18
+ '.venv',
19
+ 'venv',
20
+ ]);
21
+ this.ignoredFiles = new Set([
22
+ '.DS_Store',
23
+ 'Thumbs.db',
24
+ '.env',
25
+ '.env.local',
26
+ ]);
27
+ }
28
+ /**
29
+ * Generate file tree structure for a given directory
30
+ * @param rootPath - Root directory to scan
31
+ * @param maxDepth - Maximum depth to traverse (default: 3)
32
+ * @param maxNodes - Maximum number of nodes to return (default: 5000)
33
+ * @returns FileTreeResult with tree, totalNodes, truncated flag, and accessErrors
34
+ */
35
+ async generateTree(rootPath, maxDepth = 3, maxNodes = 5000) {
36
+ const context = {
37
+ nodeCount: 0,
38
+ truncated: false,
39
+ accessErrors: [],
40
+ };
41
+ const tree = await this.buildTree(rootPath, rootPath, 0, maxDepth, maxNodes, context);
42
+ return {
43
+ tree,
44
+ totalNodes: context.nodeCount,
45
+ truncated: context.truncated,
46
+ accessErrors: context.accessErrors,
47
+ };
48
+ }
49
+ async buildTree(rootPath, currentPath, depth, maxDepth, maxNodes, context) {
50
+ // Check if we've hit the node limit
51
+ if (context.nodeCount >= maxNodes) {
52
+ context.truncated = true;
53
+ return {
54
+ name: '.',
55
+ path: '.',
56
+ type: 'directory',
57
+ hasChildren: true,
58
+ };
59
+ }
60
+ const stats = await (0, promises_1.stat)(currentPath);
61
+ const name = currentPath === rootPath ? '.' : (0, path_1.relative)(rootPath, currentPath).split('/').pop() || '.';
62
+ context.nodeCount++;
63
+ if (stats.isFile()) {
64
+ return {
65
+ name,
66
+ path: (0, path_1.relative)(rootPath, currentPath),
67
+ type: 'file',
68
+ size: stats.size,
69
+ };
70
+ }
71
+ // Directory
72
+ const node = {
73
+ name,
74
+ path: (0, path_1.relative)(rootPath, currentPath) || '.',
75
+ type: 'directory',
76
+ children: [],
77
+ };
78
+ // Stop at max depth but indicate there might be children
79
+ if (depth >= maxDepth) {
80
+ // Check if directory has children without reading all of them
81
+ try {
82
+ const entries = await (0, promises_1.readdir)(currentPath);
83
+ const hasVisibleChildren = entries.some(e => !this.ignoredDirs.has(e) && !this.ignoredFiles.has(e));
84
+ node.hasChildren = hasVisibleChildren;
85
+ }
86
+ catch {
87
+ node.hasChildren = false;
88
+ }
89
+ delete node.children;
90
+ return node;
91
+ }
92
+ try {
93
+ const entries = await (0, promises_1.readdir)(currentPath);
94
+ const children = [];
95
+ for (const entry of entries) {
96
+ // Check node limit before processing each child
97
+ if (context.nodeCount >= maxNodes) {
98
+ context.truncated = true;
99
+ break;
100
+ }
101
+ // Skip ignored directories and files
102
+ if (this.ignoredDirs.has(entry) || this.ignoredFiles.has(entry)) {
103
+ continue;
104
+ }
105
+ const entryPath = (0, path_1.join)(currentPath, entry);
106
+ try {
107
+ const childNode = await this.buildTree(rootPath, entryPath, depth + 1, maxDepth, maxNodes, context);
108
+ children.push(childNode);
109
+ }
110
+ catch (error) {
111
+ // Handle permission errors specifically
112
+ if (error?.code === 'EACCES' || error?.code === 'EPERM') {
113
+ const relativePath = (0, path_1.relative)(rootPath, entryPath);
114
+ context.accessErrors.push(relativePath);
115
+ // Add node with accessDenied flag
116
+ try {
117
+ const entryStat = await (0, promises_1.stat)(entryPath).catch(() => null);
118
+ children.push({
119
+ name: entry,
120
+ path: relativePath,
121
+ type: entryStat?.isDirectory() ? 'directory' : 'file',
122
+ accessDenied: true,
123
+ });
124
+ context.nodeCount++;
125
+ }
126
+ catch {
127
+ // If we can't even stat it, still add as directory with access denied
128
+ children.push({
129
+ name: entry,
130
+ path: relativePath,
131
+ type: 'directory',
132
+ accessDenied: true,
133
+ });
134
+ context.nodeCount++;
135
+ }
136
+ }
137
+ else {
138
+ logger_1.logger.warn(`Failed to process ${entryPath}:`, error);
139
+ }
140
+ }
141
+ }
142
+ // Sort: directories first, then files, alphabetically
143
+ node.children = children.sort((a, b) => {
144
+ if (a.type !== b.type) {
145
+ return a.type === 'directory' ? -1 : 1;
146
+ }
147
+ return a.name.localeCompare(b.name);
148
+ });
149
+ }
150
+ catch (error) {
151
+ if (error?.code === 'EACCES' || error?.code === 'EPERM') {
152
+ const relativePath = (0, path_1.relative)(rootPath, currentPath) || '.';
153
+ context.accessErrors.push(relativePath);
154
+ node.accessDenied = true;
155
+ delete node.children;
156
+ }
157
+ else {
158
+ logger_1.logger.error(`Failed to read directory ${currentPath}:`, error);
159
+ }
160
+ }
161
+ return node;
162
+ }
163
+ /**
164
+ * Expand a single directory and return its immediate children
165
+ * Used for lazy loading in the client
166
+ * @param rootPath - The root path of the file tree
167
+ * @param dirPath - The directory path to expand (relative to rootPath)
168
+ * @param maxDepth - How many levels deep to scan (default: 1)
169
+ * @param maxNodes - Maximum nodes to return (default: 500)
170
+ */
171
+ async expandDirectory(rootPath, dirPath, maxDepth = 1, maxNodes = 500) {
172
+ const absolutePath = dirPath === '.' ? rootPath : (0, path_1.join)(rootPath, dirPath);
173
+ const context = {
174
+ nodeCount: 0,
175
+ truncated: false,
176
+ accessErrors: [],
177
+ };
178
+ const node = await this.buildTree(rootPath, absolutePath, 0, maxDepth, maxNodes, context);
179
+ return {
180
+ tree: node,
181
+ totalNodes: context.nodeCount,
182
+ truncated: context.truncated,
183
+ accessErrors: context.accessErrors,
184
+ };
185
+ }
186
+ /**
187
+ * Get directory listing (non-recursive)
188
+ */
189
+ async listDirectory(dirPath) {
190
+ try {
191
+ const entries = await (0, promises_1.readdir)(dirPath);
192
+ const nodes = [];
193
+ for (const entry of entries) {
194
+ // Skip ignored items
195
+ if (this.ignoredDirs.has(entry) || this.ignoredFiles.has(entry)) {
196
+ continue;
197
+ }
198
+ const entryPath = (0, path_1.join)(dirPath, entry);
199
+ try {
200
+ const stats = await (0, promises_1.stat)(entryPath);
201
+ nodes.push({
202
+ name: entry,
203
+ path: entryPath,
204
+ type: stats.isDirectory() ? 'directory' : 'file',
205
+ size: stats.isFile() ? stats.size : undefined,
206
+ });
207
+ }
208
+ catch (error) {
209
+ logger_1.logger.warn(`Failed to stat ${entryPath}:`, error);
210
+ }
211
+ }
212
+ // Sort: directories first, then files
213
+ return nodes.sort((a, b) => {
214
+ if (a.type !== b.type) {
215
+ return a.type === 'directory' ? -1 : 1;
216
+ }
217
+ return a.name.localeCompare(b.name);
218
+ });
219
+ }
220
+ catch (error) {
221
+ logger_1.logger.error(`Failed to list directory ${dirPath}:`, error);
222
+ throw new Error(`Failed to list directory: ${error}`);
223
+ }
224
+ }
225
+ }
226
+ exports.FileBrowser = FileBrowser;
227
+ exports.fileBrowser = new FileBrowser();
@@ -0,0 +1,159 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fileReader = exports.FileReader = void 0;
4
+ const promises_1 = require("fs/promises");
5
+ const path_1 = require("path");
6
+ const config_1 = require("../config");
7
+ const logger_1 = require("../utils/logger");
8
+ class FileReader {
9
+ constructor() {
10
+ this.languageMap = {
11
+ '.js': 'javascript',
12
+ '.jsx': 'javascript',
13
+ '.ts': 'typescript',
14
+ '.tsx': 'typescript',
15
+ '.py': 'python',
16
+ '.rb': 'ruby',
17
+ '.go': 'go',
18
+ '.rs': 'rust',
19
+ '.java': 'java',
20
+ '.c': 'c',
21
+ '.cpp': 'cpp',
22
+ '.h': 'c',
23
+ '.hpp': 'cpp',
24
+ '.cs': 'csharp',
25
+ '.php': 'php',
26
+ '.swift': 'swift',
27
+ '.kt': 'kotlin',
28
+ '.scala': 'scala',
29
+ '.sh': 'bash',
30
+ '.bash': 'bash',
31
+ '.zsh': 'bash',
32
+ '.fish': 'fish',
33
+ '.ps1': 'powershell',
34
+ '.html': 'html',
35
+ '.htm': 'html',
36
+ '.xml': 'xml',
37
+ '.css': 'css',
38
+ '.scss': 'scss',
39
+ '.sass': 'sass',
40
+ '.less': 'less',
41
+ '.json': 'json',
42
+ '.yaml': 'yaml',
43
+ '.yml': 'yaml',
44
+ '.toml': 'toml',
45
+ '.ini': 'ini',
46
+ '.md': 'markdown',
47
+ '.sql': 'sql',
48
+ '.graphql': 'graphql',
49
+ '.gql': 'graphql',
50
+ '.vue': 'vue',
51
+ '.svelte': 'svelte',
52
+ '.dockerfile': 'dockerfile',
53
+ '.Dockerfile': 'dockerfile',
54
+ };
55
+ this.binaryExtensions = new Set([
56
+ '.png', '.jpg', '.jpeg', '.gif', '.bmp', '.ico', '.svg',
57
+ '.pdf', '.zip', '.tar', '.gz', '.rar', '.7z',
58
+ '.exe', '.dll', '.so', '.dylib',
59
+ '.mp3', '.mp4', '.avi', '.mov', '.wav',
60
+ '.ttf', '.otf', '.woff', '.woff2',
61
+ '.bin', '.dat', '.db', '.sqlite',
62
+ ]);
63
+ }
64
+ /**
65
+ * Read file content with size limit
66
+ */
67
+ async readFile(filePath) {
68
+ try {
69
+ const stats = await (0, promises_1.stat)(filePath);
70
+ const ext = (0, path_1.extname)(filePath).toLowerCase();
71
+ const isBinary = this.isBinaryFile(ext);
72
+ // Check if file is too large
73
+ if (stats.size > config_1.CONFIG.maxFileSize) {
74
+ logger_1.logger.warn(`File too large: ${filePath} (${stats.size} bytes)`);
75
+ return {
76
+ path: filePath,
77
+ content: '',
78
+ size: stats.size,
79
+ language: this.detectLanguage(ext),
80
+ isBinary,
81
+ truncated: true,
82
+ };
83
+ }
84
+ // Don't read binary files
85
+ if (isBinary) {
86
+ return {
87
+ path: filePath,
88
+ content: '[Binary file]',
89
+ size: stats.size,
90
+ language: 'binary',
91
+ isBinary: true,
92
+ truncated: false,
93
+ };
94
+ }
95
+ // Read file content
96
+ const content = await (0, promises_1.readFile)(filePath, 'utf-8');
97
+ return {
98
+ path: filePath,
99
+ content,
100
+ size: stats.size,
101
+ language: this.detectLanguage(ext),
102
+ isBinary: false,
103
+ truncated: false,
104
+ };
105
+ }
106
+ catch (error) {
107
+ logger_1.logger.error(`Failed to read file ${filePath}:`, error);
108
+ throw new Error(`Failed to read file: ${error}`);
109
+ }
110
+ }
111
+ /**
112
+ * Read file with line range
113
+ */
114
+ async readFileLines(filePath, startLine, endLine) {
115
+ const fileContent = await this.readFile(filePath);
116
+ if (fileContent.isBinary || fileContent.truncated) {
117
+ return fileContent;
118
+ }
119
+ const lines = fileContent.content.split('\n');
120
+ const selectedLines = lines.slice(startLine - 1, endLine);
121
+ return {
122
+ ...fileContent,
123
+ content: selectedLines.join('\n'),
124
+ };
125
+ }
126
+ /**
127
+ * Detect file language from extension
128
+ */
129
+ detectLanguage(ext) {
130
+ return this.languageMap[ext] || 'plaintext';
131
+ }
132
+ /**
133
+ * Check if file is binary based on extension
134
+ */
135
+ isBinaryFile(ext) {
136
+ return this.binaryExtensions.has(ext);
137
+ }
138
+ /**
139
+ * Get file metadata without reading content
140
+ */
141
+ async getFileInfo(filePath) {
142
+ try {
143
+ const stats = await (0, promises_1.stat)(filePath);
144
+ const ext = (0, path_1.extname)(filePath).toLowerCase();
145
+ return {
146
+ path: filePath,
147
+ size: stats.size,
148
+ language: this.detectLanguage(ext),
149
+ isBinary: this.isBinaryFile(ext),
150
+ };
151
+ }
152
+ catch (error) {
153
+ logger_1.logger.error(`Failed to get file info ${filePath}:`, error);
154
+ throw new Error(`Failed to get file info: ${error}`);
155
+ }
156
+ }
157
+ }
158
+ exports.FileReader = FileReader;
159
+ exports.fileReader = new FileReader();