@lonely9206/cc-hooks 0.1.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/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "@lonely9206/cc-hooks",
3
+ "version": "0.1.0",
4
+ "description": "Claude Code hooks for logging prompts and file changes",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "cc-hooks": "bin/cli.js",
8
+ "cc-hooks-install": "bin/install.js"
9
+ },
10
+ "scripts": {
11
+ "build": "ncc build src/index.js -o dist",
12
+ "prepublishOnly": "npm run build",
13
+ "postinstall": "node bin/install.js"
14
+ },
15
+ "dependencies": {
16
+ "axios": "^1.6.0"
17
+ },
18
+ "devDependencies": {
19
+ "@vercel/ncc": "^0.38.0"
20
+ }
21
+ }
package/src/api.js ADDED
@@ -0,0 +1,33 @@
1
+ const axios = require('axios');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+
5
+ // 加载 .env 文件
6
+ const envFile = path.join(process.cwd(), '.env');
7
+ if (fs.existsSync(envFile)) {
8
+ const envContent = fs.readFileSync(envFile, 'utf8');
9
+ envContent.split('\n').forEach(line => {
10
+ const [key, ...valueParts] = line.split('=');
11
+ if (key && valueParts.length > 0) {
12
+ process.env[key.trim()] = valueParts.join('=').trim();
13
+ }
14
+ });
15
+ }
16
+
17
+ const API_BASE = process.env.CC_HOOKS_API_URL || 'http://192.168.1.224:8000';
18
+
19
+ async function logPrompt(prompt) {
20
+ const response = await axios.post(`${API_BASE}/api/entries/prompt`, {
21
+ prompt: prompt.substring(0, 500)
22
+ });
23
+ return response.data;
24
+ }
25
+
26
+ async function logChanges(filesChanged) {
27
+ if (!filesChanged || filesChanged.length === 0) return;
28
+ await axios.post(`${API_BASE}/api/entries/changes`, {
29
+ files_changed: filesChanged
30
+ });
31
+ }
32
+
33
+ module.exports = { logPrompt, logChanges };
@@ -0,0 +1,219 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const crypto = require('crypto');
4
+
5
+ // ============== FileTracker ==============
6
+
7
+ class FileTracker {
8
+ constructor(watchDir = ".") {
9
+ this.watchDir = path.resolve(watchDir);
10
+ this.snapshotFile = path.join("track_logs", ".file_snapshot.json");
11
+ }
12
+
13
+ static EXCLUDE_FILES = new Set([
14
+ '.file_snapshot.json',
15
+ '.current_session',
16
+ '.current_entry',
17
+ '.DS_Store',
18
+ 'Thumbs.db',
19
+ 'node_modules',
20
+ '__pycache__',
21
+ '.git',
22
+ '.venv',
23
+ 'venv',
24
+ 'dist',
25
+ 'build',
26
+ '.next',
27
+ ]);
28
+
29
+ static ALLOWED_EXTENSIONS = new Set([
30
+ '.py', '.java', '.cs',
31
+ '.html', '.css', '.scss', '.sass', '.less',
32
+ '.js', '.ts', '.jsx', '.tsx', '.vue',
33
+ ]);
34
+
35
+ _shouldExclude(filename) {
36
+ if (FileTracker.EXCLUDE_FILES.has(filename)) {
37
+ return true;
38
+ }
39
+ const ext = path.extname(filename).toLowerCase();
40
+ return !FileTracker.ALLOWED_EXTENSIONS.has(ext);
41
+ }
42
+
43
+ _getFileInfo(filePath) {
44
+ try {
45
+ const stat = fs.statSync(filePath);
46
+ const content = fs.readFileSync(filePath, 'utf8');
47
+ const lines = content.split('\n');
48
+ return {
49
+ mtime: stat.mtimeMs,
50
+ size: stat.size,
51
+ lines: lines.length,
52
+ hash: this._hash(content)
53
+ };
54
+ } catch (e) {
55
+ return {};
56
+ }
57
+ }
58
+
59
+ _hash(content) {
60
+ return crypto.createHash('md5').update(content).digest('hex');
61
+ }
62
+
63
+ takeSnapshot() {
64
+ const snapshot = {};
65
+ if (!fs.existsSync(this.watchDir)) {
66
+ return snapshot;
67
+ }
68
+
69
+ const walk = (dir) => {
70
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
71
+ for (const entry of entries) {
72
+ const fullPath = path.join(dir, entry.name);
73
+
74
+ if (entry.isDirectory()) {
75
+ if (!this._shouldExclude(entry.name) && !entry.name.startsWith('.')) {
76
+ walk(fullPath);
77
+ }
78
+ } else {
79
+ if (this._shouldExclude(entry.name)) {
80
+ continue;
81
+ }
82
+ const info = this._getFileInfo(fullPath);
83
+ if (Object.keys(info).length > 0) {
84
+ snapshot[fullPath] = info;
85
+ }
86
+ }
87
+ }
88
+ };
89
+
90
+ walk(this.watchDir);
91
+
92
+ if (!fs.existsSync(this.snapshotFile)) {
93
+ this.saveSnapshot(snapshot);
94
+ }
95
+ return snapshot;
96
+ }
97
+
98
+ saveSnapshot(snapshot) {
99
+ const dir = path.dirname(this.snapshotFile);
100
+ if (!fs.existsSync(dir)) {
101
+ fs.mkdirSync(dir, { recursive: true });
102
+ }
103
+ fs.writeFileSync(this.snapshotFile, JSON.stringify(snapshot));
104
+ }
105
+
106
+ loadSnapshot() {
107
+ if (!fs.existsSync(this.snapshotFile)) {
108
+ return {};
109
+ }
110
+ try {
111
+ const content = fs.readFileSync(this.snapshotFile, 'utf8');
112
+ return JSON.parse(content);
113
+ } catch (e) {
114
+ return {};
115
+ }
116
+ }
117
+
118
+ cleanupOrphanedEntries(existingPaths) {
119
+ const oldSnapshot = this.loadSnapshot();
120
+ if (Object.keys(oldSnapshot).length === 0) {
121
+ return;
122
+ }
123
+ const orphaned = new Set(Object.keys(oldSnapshot)).difference(existingPaths);
124
+ if (orphaned.size > 0) {
125
+ for (const p of orphaned) {
126
+ delete oldSnapshot[p];
127
+ }
128
+ this.saveSnapshot(oldSnapshot);
129
+ }
130
+ }
131
+
132
+ _countLinesDiff(oldLines, newLines) {
133
+ const diff = newLines - oldLines;
134
+ return {
135
+ lines_added: Math.max(0, diff),
136
+ lines_deleted: Math.max(0, -diff)
137
+ };
138
+ }
139
+
140
+ detectChanges(sinceMinutes = 30) {
141
+ const oldSnapshot = this.loadSnapshot();
142
+ const newSnapshot = this.takeSnapshot();
143
+ const cutoffTime = Date.now() - sinceMinutes * 60 * 1000;
144
+ const cutoffTs = cutoffTime / 1000;
145
+
146
+ const changes = [];
147
+ const oldPaths = new Set(Object.keys(oldSnapshot));
148
+ const newPaths = new Set(Object.keys(newSnapshot));
149
+
150
+ // New files
151
+ for (const filePath of newPaths.difference(oldPaths)) {
152
+ const filename = path.basename(filePath);
153
+ if (this._shouldExclude(filename)) {
154
+ continue;
155
+ }
156
+ const fileInfo = newSnapshot[filePath];
157
+ if (fileInfo.mtime >= cutoffTs) {
158
+ const lineInfo = this._countLinesDiff(0, fileInfo.lines);
159
+ if (lineInfo.lines_added === 0) {
160
+ continue;
161
+ }
162
+ changes.push({
163
+ path: path.relative(this.watchDir, filePath),
164
+ action: "created",
165
+ timestamp: new Date(fileInfo.mtime).toISOString(),
166
+ lines_added: lineInfo.lines_added,
167
+ lines_deleted: lineInfo.lines_deleted
168
+ });
169
+ }
170
+ }
171
+
172
+ // Deleted files
173
+ for (const filePath of oldPaths.difference(newPaths)) {
174
+ const filename = path.basename(filePath);
175
+ if (this._shouldExclude(filename)) {
176
+ continue;
177
+ }
178
+ const oldInfo = oldSnapshot[filePath];
179
+ const lineInfo = this._countLinesDiff(oldInfo.lines, 0);
180
+ changes.push({
181
+ path: path.relative(this.watchDir, filePath),
182
+ action: "deleted",
183
+ timestamp: new Date().toISOString(),
184
+ lines_added: 0,
185
+ lines_deleted: lineInfo.lines_deleted
186
+ });
187
+ }
188
+
189
+ // Modified files
190
+ for (const filePath of oldPaths.intersection(newPaths)) {
191
+ const filename = path.basename(filePath);
192
+ if (this._shouldExclude(filename)) {
193
+ continue;
194
+ }
195
+ const oldInfo = oldSnapshot[filePath];
196
+ const newInfo = newSnapshot[filePath];
197
+ if (oldInfo.hash !== newInfo.hash && newInfo.mtime >= cutoffTs) {
198
+ const lineInfo = this._countLinesDiff(oldInfo.lines, newInfo.lines);
199
+ if (lineInfo.lines_added === 0 && lineInfo.lines_deleted === 0) {
200
+ continue;
201
+ }
202
+ changes.push({
203
+ path: path.relative(this.watchDir, filePath),
204
+ action: "modified",
205
+ timestamp: new Date(newInfo.mtime).toISOString(),
206
+ lines_added: lineInfo.lines_added,
207
+ lines_deleted: lineInfo.lines_deleted
208
+ });
209
+ }
210
+ }
211
+
212
+ this.saveSnapshot(newSnapshot);
213
+ this.cleanupOrphanedEntries(new Set(Object.keys(newSnapshot)));
214
+
215
+ return changes;
216
+ }
217
+ }
218
+
219
+ module.exports = { FileTracker };
package/src/git.js ADDED
@@ -0,0 +1,12 @@
1
+ const { execSync } = require('child_process');
2
+
3
+ function getGitAuthor() {
4
+ try {
5
+ const author = execSync('git log -1 --format=%an', { encoding: 'utf8' }).trim();
6
+ return author || 'unknown';
7
+ } catch {
8
+ return 'unknown';
9
+ }
10
+ }
11
+
12
+ module.exports = { getGitAuthor };
package/src/index.js ADDED
@@ -0,0 +1,4 @@
1
+ const { logPrompt, logChanges } = require('./api');
2
+ const { FileTracker } = require('./fileTracker');
3
+
4
+ module.exports = { logPrompt, logChanges, FileTracker };
@@ -0,0 +1 @@
1
+ 55f754bf