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/.github/workflows/publish.yml +27 -0
- package/README.md +138 -0
- package/bin/cli.js +300 -0
- package/package.json +40 -0
- package/public/app.js +1282 -0
- package/public/index.html +209 -0
- package/public/styles.css +1725 -0
- package/server.js +458 -0
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
|
+
});
|