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.
- package/dist/__tests__/auth.test.js +49 -0
- package/dist/__tests__/watcher.test.js +58 -0
- package/dist/claude/chatService.js +175 -0
- package/dist/claude/sessionBrowser.js +743 -0
- package/dist/claude/watcher.js +242 -0
- package/dist/config.js +44 -0
- package/dist/files/browser.js +227 -0
- package/dist/files/reader.js +159 -0
- package/dist/files/search.js +124 -0
- package/dist/git/gitHandler.js +177 -0
- package/dist/git/gitService.js +299 -0
- package/dist/git/index.js +8 -0
- package/dist/http/server.js +96 -0
- package/dist/index.js +77 -0
- package/dist/ssh/index.js +9 -0
- package/dist/ssh/sshHandler.js +205 -0
- package/dist/ssh/sshManager.js +329 -0
- package/dist/terminal/index.js +11 -0
- package/dist/terminal/localTerminalHandler.js +176 -0
- package/dist/terminal/localTerminalManager.js +497 -0
- package/dist/terminal/terminalWebSocket.js +136 -0
- package/dist/types.js +2 -0
- package/dist/utils/logger.js +42 -0
- package/dist/websocket/auth.js +18 -0
- package/dist/websocket/server.js +631 -0
- package/package.json +66 -0
- package/web-dist/assets/highlight-l0sNRNKZ.js +1 -0
- package/web-dist/assets/index-C8TJGN-T.css +41 -0
- package/web-dist/assets/index-DjLLxjMD.js +39 -0
- package/web-dist/assets/markdown-C_j0ZeeY.js +51 -0
- package/web-dist/assets/react-vendor-CqP5oCk4.js +9 -0
- package/web-dist/assets/xterm-BCk906R6.js +9 -0
- package/web-dist/icon-192.png +0 -0
- package/web-dist/icon-512.png +0 -0
- package/web-dist/index.html +23 -0
- package/web-dist/manifest.json +24 -0
- 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();
|