seeclaudecode 1.0.0

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/server.js ADDED
@@ -0,0 +1,458 @@
1
+ import express from 'express';
2
+ import { WebSocketServer } from 'ws';
3
+ import { createServer } from 'http';
4
+ import chokidar from 'chokidar';
5
+ import simpleGit from 'simple-git';
6
+ import { fileURLToPath } from 'url';
7
+ import { dirname, join, relative, sep } from 'path';
8
+ import { readdir, stat } from 'fs/promises';
9
+
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = dirname(__filename);
12
+
13
+ const app = express();
14
+ const server = createServer(app);
15
+ const wss = new WebSocketServer({ server });
16
+
17
+ // Configuration
18
+ const PORT = process.env.PORT || 3847;
19
+ const TARGET_REPO = process.argv[2] || process.cwd();
20
+
21
+ console.log(`\nšŸ‘ļø SeeClaudeCode`);
22
+ console.log(`šŸ“ Monitoring: ${TARGET_REPO}`);
23
+ console.log(`🌐 Starting server on http://localhost:${PORT}\n`);
24
+
25
+ const git = simpleGit(TARGET_REPO);
26
+
27
+ // Track active edits and changes
28
+ const state = {
29
+ activeFiles: new Set(),
30
+ changedFiles: new Set(),
31
+ repoStructure: null,
32
+ lastActivity: Date.now()
33
+ };
34
+
35
+ // Serve static files
36
+ app.use(express.static(join(__dirname, 'public')));
37
+
38
+ // API endpoint to get repo structure
39
+ app.get('/api/structure', async (req, res) => {
40
+ try {
41
+ const structure = await buildRepoStructure(TARGET_REPO);
42
+ state.repoStructure = structure;
43
+ res.json(structure);
44
+ } catch (error) {
45
+ res.status(500).json({ error: error.message });
46
+ }
47
+ });
48
+
49
+ // API endpoint to get git diff
50
+ app.get('/api/diff', async (req, res) => {
51
+ try {
52
+ const diff = await getGitDiff();
53
+ res.json(diff);
54
+ } catch (error) {
55
+ res.status(500).json({ error: error.message });
56
+ }
57
+ });
58
+
59
+ // API endpoint to get diff for a specific file or directory
60
+ app.get('/api/diff/:path(*)', async (req, res) => {
61
+ try {
62
+ const targetPath = req.params.path;
63
+
64
+ // Check if it's a directory
65
+ const fullPath = join(TARGET_REPO, targetPath);
66
+ const stats = await stat(fullPath).catch(() => null);
67
+
68
+ if (stats && stats.isDirectory()) {
69
+ // Get diffs for all changed files in this directory
70
+ const diff = await getDirectoryDiff(targetPath);
71
+ res.json(diff);
72
+ } else {
73
+ // Single file diff
74
+ const diff = await getFileDiff(targetPath);
75
+ res.json(diff);
76
+ }
77
+ } catch (error) {
78
+ res.status(500).json({ error: error.message });
79
+ }
80
+ });
81
+
82
+ // API endpoint to get current state
83
+ app.get('/api/state', (req, res) => {
84
+ res.json({
85
+ activeFiles: Array.from(state.activeFiles),
86
+ changedFiles: Array.from(state.changedFiles),
87
+ lastActivity: state.lastActivity
88
+ });
89
+ });
90
+
91
+ // Build repo structure recursively
92
+ async function buildRepoStructure(dirPath, depth = 0, maxDepth = 6) {
93
+ const name = dirPath.split(sep).pop() || dirPath;
94
+ const relativePath = relative(TARGET_REPO, dirPath) || '.';
95
+
96
+ const node = {
97
+ name,
98
+ path: relativePath,
99
+ type: 'directory',
100
+ children: [],
101
+ depth
102
+ };
103
+
104
+ if (depth >= maxDepth) return node;
105
+
106
+ try {
107
+ const entries = await readdir(dirPath, { withFileTypes: true });
108
+
109
+ // Filter out common ignored directories/files
110
+ const ignoredPatterns = [
111
+ 'node_modules', '.git', '.next', 'dist', 'build',
112
+ '.cache', '.turbo', 'coverage', '__pycache__',
113
+ '.DS_Store', 'thumbs.db', '.env.local'
114
+ ];
115
+
116
+ const filtered = entries.filter(entry =>
117
+ !ignoredPatterns.includes(entry.name) &&
118
+ !entry.name.startsWith('.')
119
+ );
120
+
121
+ for (const entry of filtered) {
122
+ const fullPath = join(dirPath, entry.name);
123
+
124
+ if (entry.isDirectory()) {
125
+ const child = await buildRepoStructure(fullPath, depth + 1, maxDepth);
126
+ if (child.children.length > 0 || depth < 2) {
127
+ node.children.push(child);
128
+ }
129
+ } else if (entry.isFile()) {
130
+ const ext = entry.name.split('.').pop()?.toLowerCase() || '';
131
+ node.children.push({
132
+ name: entry.name,
133
+ path: relative(TARGET_REPO, fullPath),
134
+ type: 'file',
135
+ extension: ext,
136
+ category: getFileCategory(ext)
137
+ });
138
+ }
139
+ }
140
+
141
+ // Sort: directories first, then files
142
+ node.children.sort((a, b) => {
143
+ if (a.type === 'directory' && b.type === 'file') return -1;
144
+ if (a.type === 'file' && b.type === 'directory') return 1;
145
+ return a.name.localeCompare(b.name);
146
+ });
147
+
148
+ } catch (error) {
149
+ console.error(`Error reading ${dirPath}:`, error.message);
150
+ }
151
+
152
+ return node;
153
+ }
154
+
155
+ // Categorize files for visual grouping
156
+ function getFileCategory(ext) {
157
+ const categories = {
158
+ code: ['js', 'ts', 'jsx', 'tsx', 'py', 'rb', 'go', 'rs', 'java', 'c', 'cpp', 'h', 'cs', 'php', 'swift', 'kt'],
159
+ style: ['css', 'scss', 'sass', 'less', 'styl'],
160
+ markup: ['html', 'htm', 'xml', 'svg'],
161
+ config: ['json', 'yaml', 'yml', 'toml', 'ini', 'env', 'config'],
162
+ docs: ['md', 'mdx', 'txt', 'rst', 'doc', 'pdf'],
163
+ data: ['sql', 'graphql', 'prisma'],
164
+ test: ['test', 'spec'],
165
+ image: ['png', 'jpg', 'jpeg', 'gif', 'webp', 'ico', 'bmp']
166
+ };
167
+
168
+ for (const [category, extensions] of Object.entries(categories)) {
169
+ if (extensions.includes(ext)) return category;
170
+ }
171
+ return 'other';
172
+ }
173
+
174
+ // Get git diff for changed files
175
+ async function getGitDiff() {
176
+ try {
177
+ const status = await git.status();
178
+ const diff = await git.diff(['--stat']);
179
+
180
+ const changedFiles = [
181
+ ...status.modified,
182
+ ...status.created,
183
+ ...status.deleted,
184
+ ...status.renamed.map(r => r.to)
185
+ ];
186
+
187
+ // Get detailed diff for each file
188
+ const fileDiffs = [];
189
+ for (const file of status.modified.slice(0, 20)) {
190
+ try {
191
+ const fileDiff = await git.diff([file]);
192
+ const additions = (fileDiff.match(/^\+[^+]/gm) || []).length;
193
+ const deletions = (fileDiff.match(/^-[^-]/gm) || []).length;
194
+ fileDiffs.push({ file, additions, deletions });
195
+ } catch (e) {
196
+ fileDiffs.push({ file, additions: 0, deletions: 0 });
197
+ }
198
+ }
199
+
200
+ return {
201
+ modified: status.modified,
202
+ created: status.created,
203
+ deleted: status.deleted,
204
+ staged: status.staged,
205
+ fileDiffs,
206
+ summary: diff
207
+ };
208
+ } catch (error) {
209
+ return { error: error.message, modified: [], created: [], deleted: [], staged: [], fileDiffs: [] };
210
+ }
211
+ }
212
+
213
+ // Get diff for all changed files in a directory
214
+ async function getDirectoryDiff(dirPath) {
215
+ try {
216
+ const status = await git.status();
217
+
218
+ // Find all changed files that are in this directory
219
+ const allChanged = [
220
+ ...status.modified.map(f => ({ file: f, status: 'modified' })),
221
+ ...status.created.map(f => ({ file: f, status: 'created' })),
222
+ ...status.deleted.map(f => ({ file: f, status: 'deleted' }))
223
+ ];
224
+
225
+ // Filter to files in this directory (and subdirectories)
226
+ const dirPrefix = dirPath === '.' ? '' : dirPath + '/';
227
+ const filesInDir = allChanged.filter(f =>
228
+ dirPath === '.' || f.file === dirPath || f.file.startsWith(dirPrefix)
229
+ );
230
+
231
+ if (filesInDir.length === 0) {
232
+ return {
233
+ path: dirPath,
234
+ type: 'directory',
235
+ status: 'unchanged',
236
+ files: [],
237
+ totalAdditions: 0,
238
+ totalDeletions: 0
239
+ };
240
+ }
241
+
242
+ // Get diffs for each file
243
+ const fileDiffs = [];
244
+ let totalAdditions = 0;
245
+ let totalDeletions = 0;
246
+
247
+ for (const { file, status: fileStatus } of filesInDir.slice(0, 10)) {
248
+ const fileDiff = await getFileDiff(file);
249
+ fileDiffs.push({
250
+ ...fileDiff,
251
+ status: fileStatus
252
+ });
253
+ totalAdditions += fileDiff.additions || 0;
254
+ totalDeletions += fileDiff.deletions || 0;
255
+ }
256
+
257
+ return {
258
+ path: dirPath,
259
+ type: 'directory',
260
+ status: 'changed',
261
+ files: fileDiffs,
262
+ totalAdditions,
263
+ totalDeletions,
264
+ fileCount: filesInDir.length,
265
+ showing: Math.min(filesInDir.length, 10)
266
+ };
267
+ } catch (error) {
268
+ return { path: dirPath, type: 'directory', error: error.message, files: [] };
269
+ }
270
+ }
271
+
272
+ // Get diff for a specific file
273
+ async function getFileDiff(filePath) {
274
+ try {
275
+ const status = await git.status();
276
+
277
+ // Check if file is modified, created, or deleted
278
+ const isModified = status.modified.includes(filePath);
279
+ const isCreated = status.created.includes(filePath);
280
+ const isDeleted = status.deleted.includes(filePath);
281
+ const isStaged = status.staged.includes(filePath);
282
+
283
+ if (!isModified && !isCreated && !isDeleted && !isStaged) {
284
+ return { file: filePath, status: 'unchanged', lines: [] };
285
+ }
286
+
287
+ let diffOutput = '';
288
+ if (isModified || isDeleted) {
289
+ diffOutput = await git.diff([filePath]);
290
+ } else if (isCreated) {
291
+ // For new files, show entire content as additions
292
+ diffOutput = await git.diff(['--cached', filePath]).catch(() => '');
293
+ if (!diffOutput) {
294
+ // If not staged, get the file content
295
+ try {
296
+ const { readFile } = await import('fs/promises');
297
+ const content = await readFile(join(TARGET_REPO, filePath), 'utf8');
298
+ const lines = content.split('\n').map(line => `+${line}`);
299
+ return {
300
+ file: filePath,
301
+ status: 'created',
302
+ additions: lines.length,
303
+ deletions: 0,
304
+ lines: lines.map((line, i) => ({
305
+ type: 'added',
306
+ lineNumber: i + 1,
307
+ content: line.slice(1)
308
+ }))
309
+ };
310
+ } catch (e) {
311
+ return { file: filePath, status: 'created', lines: [], error: e.message };
312
+ }
313
+ }
314
+ }
315
+
316
+ // Parse the diff output into lines
317
+ const lines = [];
318
+ let lineNum = 0;
319
+ let additions = 0;
320
+ let deletions = 0;
321
+
322
+ diffOutput.split('\n').forEach(line => {
323
+ if (line.startsWith('@@')) {
324
+ // Parse hunk header for line numbers
325
+ const match = line.match(/@@ -\d+(?:,\d+)? \+(\d+)/);
326
+ if (match) {
327
+ lineNum = parseInt(match[1], 10) - 1;
328
+ }
329
+ lines.push({ type: 'hunk', content: line });
330
+ } else if (line.startsWith('+') && !line.startsWith('+++')) {
331
+ lineNum++;
332
+ additions++;
333
+ lines.push({ type: 'added', lineNumber: lineNum, content: line.slice(1) });
334
+ } else if (line.startsWith('-') && !line.startsWith('---')) {
335
+ deletions++;
336
+ lines.push({ type: 'removed', content: line.slice(1) });
337
+ } else if (!line.startsWith('diff') && !line.startsWith('index') && !line.startsWith('---') && !line.startsWith('+++')) {
338
+ lineNum++;
339
+ lines.push({ type: 'context', lineNumber: lineNum, content: line });
340
+ }
341
+ });
342
+
343
+ return {
344
+ file: filePath,
345
+ status: isModified ? 'modified' : isCreated ? 'created' : 'deleted',
346
+ additions,
347
+ deletions,
348
+ lines
349
+ };
350
+ } catch (error) {
351
+ return { file: filePath, error: error.message, lines: [] };
352
+ }
353
+ }
354
+
355
+ // Broadcast to all connected clients
356
+ function broadcast(data) {
357
+ const message = JSON.stringify(data);
358
+ wss.clients.forEach(client => {
359
+ if (client.readyState === 1) {
360
+ client.send(message);
361
+ }
362
+ });
363
+ }
364
+
365
+ // WebSocket connection handler
366
+ wss.on('connection', (ws) => {
367
+ console.log('šŸ“± Client connected');
368
+
369
+ // Send current state to new client
370
+ ws.send(JSON.stringify({
371
+ type: 'init',
372
+ activeFiles: Array.from(state.activeFiles),
373
+ changedFiles: Array.from(state.changedFiles)
374
+ }));
375
+
376
+ ws.on('close', () => {
377
+ console.log('šŸ““ Client disconnected');
378
+ });
379
+ });
380
+
381
+ // File watcher for real-time updates
382
+ const watcher = chokidar.watch(TARGET_REPO, {
383
+ ignored: [
384
+ /(^|[\/\\])\../, // dotfiles
385
+ /node_modules/,
386
+ /\.git/,
387
+ /dist/,
388
+ /build/,
389
+ /__pycache__/
390
+ ],
391
+ persistent: true,
392
+ ignoreInitial: true,
393
+ awaitWriteFinish: {
394
+ stabilityThreshold: 300,
395
+ pollInterval: 100
396
+ }
397
+ });
398
+
399
+ // Debounce timer for activity detection
400
+ let activityTimer = null;
401
+ const ACTIVITY_TIMEOUT = 2000; // 2 seconds of no activity = editing complete
402
+
403
+ function handleFileActivity(eventType, filePath) {
404
+ const relativePath = relative(TARGET_REPO, filePath);
405
+
406
+ // Skip if it's in an ignored directory
407
+ if (relativePath.includes('node_modules') || relativePath.startsWith('.git')) {
408
+ return;
409
+ }
410
+
411
+ console.log(`šŸ“ ${eventType}: ${relativePath}`);
412
+
413
+ // Mark file as actively being edited
414
+ state.activeFiles.add(relativePath);
415
+ state.changedFiles.add(relativePath);
416
+ state.lastActivity = Date.now();
417
+
418
+ // Broadcast active edit
419
+ broadcast({
420
+ type: 'active',
421
+ file: relativePath,
422
+ event: eventType,
423
+ timestamp: Date.now()
424
+ });
425
+
426
+ // Clear previous timer and set new one
427
+ if (activityTimer) clearTimeout(activityTimer);
428
+
429
+ activityTimer = setTimeout(async () => {
430
+ // Activity stopped - editing complete
431
+ const completedFiles = Array.from(state.activeFiles);
432
+ state.activeFiles.clear();
433
+
434
+ // Get git diff for changed files
435
+ const diff = await getGitDiff();
436
+
437
+ broadcast({
438
+ type: 'complete',
439
+ files: completedFiles,
440
+ diff,
441
+ timestamp: Date.now()
442
+ });
443
+
444
+ console.log(`āœ… Editing complete. Changed files: ${completedFiles.join(', ')}`);
445
+ }, ACTIVITY_TIMEOUT);
446
+ }
447
+
448
+ watcher
449
+ .on('change', (path) => handleFileActivity('change', path))
450
+ .on('add', (path) => handleFileActivity('add', path))
451
+ .on('unlink', (path) => handleFileActivity('delete', path));
452
+
453
+ // Start server
454
+ server.listen(PORT, () => {
455
+ console.log(`✨ Server running at http://localhost:${PORT}`);
456
+ console.log(`šŸ“‚ Watching for changes in: ${TARGET_REPO}`);
457
+ console.log(`\nOpen the URL in your browser to see the visualization.\n`);
458
+ });