clay-server 2.26.0 → 2.27.0-beta.1
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/lib/project-connection.js +259 -0
- package/lib/project-file-watch.js +120 -0
- package/lib/project-filesystem.js +482 -0
- package/lib/project-http.js +685 -0
- package/lib/project-image.js +94 -0
- package/lib/project-knowledge.js +161 -0
- package/lib/project-loop.js +1160 -0
- package/lib/project-sessions.js +1152 -0
- package/lib/project-user-message.js +631 -0
- package/lib/project.js +356 -4438
- package/lib/public/app.js +79 -52
- package/lib/server.js +30 -0
- package/package.json +1 -1
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
var fs = require("fs");
|
|
2
|
+
var path = require("path");
|
|
3
|
+
var execFileSync = require("child_process").execFileSync;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Attach filesystem-related message handlers to a project context.
|
|
7
|
+
*
|
|
8
|
+
* ctx fields:
|
|
9
|
+
* cwd, slug, osUsers
|
|
10
|
+
* sm (session manager)
|
|
11
|
+
* send, sendTo
|
|
12
|
+
* safePath, safeAbsPath (functions)
|
|
13
|
+
* getOsUserInfoForWs (function)
|
|
14
|
+
* startFileWatch, stopFileWatch, startDirWatch (from _fileWatch)
|
|
15
|
+
* usersModule, fsAsUser
|
|
16
|
+
* validateEnvString (function)
|
|
17
|
+
* opts (for onGetProjectEnv, onSetProjectEnv, onGetSharedEnv, onSetSharedEnv callbacks)
|
|
18
|
+
* IGNORED_DIRS, BINARY_EXTS, IMAGE_EXTS, FS_MAX_SIZE (constants)
|
|
19
|
+
*/
|
|
20
|
+
function attachFilesystem(ctx) {
|
|
21
|
+
var cwd = ctx.cwd;
|
|
22
|
+
var slug = ctx.slug;
|
|
23
|
+
var osUsers = ctx.osUsers;
|
|
24
|
+
var sm = ctx.sm;
|
|
25
|
+
var send = ctx.send;
|
|
26
|
+
var sendTo = ctx.sendTo;
|
|
27
|
+
var safePath = ctx.safePath;
|
|
28
|
+
var safeAbsPath = ctx.safeAbsPath;
|
|
29
|
+
var getOsUserInfoForWs = ctx.getOsUserInfoForWs;
|
|
30
|
+
var startFileWatch = ctx.startFileWatch;
|
|
31
|
+
var stopFileWatch = ctx.stopFileWatch;
|
|
32
|
+
var startDirWatch = ctx.startDirWatch;
|
|
33
|
+
var usersModule = ctx.usersModule;
|
|
34
|
+
var fsAsUser = ctx.fsAsUser;
|
|
35
|
+
var validateEnvString = ctx.validateEnvString;
|
|
36
|
+
var opts = ctx.opts;
|
|
37
|
+
var IGNORED_DIRS = ctx.IGNORED_DIRS;
|
|
38
|
+
var BINARY_EXTS = ctx.BINARY_EXTS;
|
|
39
|
+
var IMAGE_EXTS = ctx.IMAGE_EXTS;
|
|
40
|
+
var FS_MAX_SIZE = ctx.FS_MAX_SIZE;
|
|
41
|
+
|
|
42
|
+
function handleFilesystemMessage(ws, msg) {
|
|
43
|
+
// --- File browser permission gate ---
|
|
44
|
+
if (msg.type === "fs_list" || msg.type === "fs_read" || msg.type === "fs_write" || msg.type === "fs_delete" || msg.type === "fs_rename" || msg.type === "fs_mkdir" || msg.type === "fs_upload") {
|
|
45
|
+
if (ws._clayUser) {
|
|
46
|
+
var fbPerms = usersModule.getEffectivePermissions(ws._clayUser, osUsers);
|
|
47
|
+
if (!fbPerms.fileBrowser) {
|
|
48
|
+
sendTo(ws, { type: msg.type + "_result", error: "File browser access is not permitted" });
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// --- fs_list ---
|
|
55
|
+
if (msg.type === "fs_list") {
|
|
56
|
+
var fsDir = safePath(cwd, msg.path || ".");
|
|
57
|
+
// In OS user mode, fall back to absolute path resolution (ACL enforces access)
|
|
58
|
+
if (!fsDir && getOsUserInfoForWs(ws)) {
|
|
59
|
+
fsDir = safeAbsPath(msg.path);
|
|
60
|
+
}
|
|
61
|
+
if (!fsDir) {
|
|
62
|
+
sendTo(ws, { type: "fs_list_result", path: msg.path, entries: [], error: "Access denied" });
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
var fsListUserInfo = getOsUserInfoForWs(ws);
|
|
67
|
+
var entries = [];
|
|
68
|
+
if (fsListUserInfo) {
|
|
69
|
+
// Run as target OS user to respect Linux file permissions
|
|
70
|
+
var rawEntries = fsAsUser("list", { dir: fsDir }, fsListUserInfo);
|
|
71
|
+
for (var fi = 0; fi < rawEntries.length; fi++) {
|
|
72
|
+
var re = rawEntries[fi];
|
|
73
|
+
if (re.isDir && IGNORED_DIRS.has(re.name)) continue;
|
|
74
|
+
entries.push({
|
|
75
|
+
name: re.name,
|
|
76
|
+
type: re.isDir ? "dir" : "file",
|
|
77
|
+
path: path.relative(cwd, path.join(fsDir, re.name)).split(path.sep).join("/"),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
var items = fs.readdirSync(fsDir, { withFileTypes: true });
|
|
82
|
+
for (var fi = 0; fi < items.length; fi++) {
|
|
83
|
+
var item = items[fi];
|
|
84
|
+
if (item.isDirectory() && IGNORED_DIRS.has(item.name)) continue;
|
|
85
|
+
entries.push({
|
|
86
|
+
name: item.name,
|
|
87
|
+
type: item.isDirectory() ? "dir" : "file",
|
|
88
|
+
path: path.relative(cwd, path.join(fsDir, item.name)).split(path.sep).join("/"),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
sendTo(ws, { type: "fs_list_result", path: msg.path || ".", entries: entries });
|
|
93
|
+
// Auto-watch the directory for changes
|
|
94
|
+
startDirWatch(msg.path || ".");
|
|
95
|
+
} catch (e) {
|
|
96
|
+
sendTo(ws, { type: "fs_list_result", path: msg.path, entries: [], error: e.message });
|
|
97
|
+
}
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// --- fs_read ---
|
|
102
|
+
if (msg.type === "fs_read") {
|
|
103
|
+
var fsFile = safePath(cwd, msg.path);
|
|
104
|
+
if (!fsFile && getOsUserInfoForWs(ws)) {
|
|
105
|
+
fsFile = safeAbsPath(msg.path);
|
|
106
|
+
}
|
|
107
|
+
if (!fsFile) {
|
|
108
|
+
sendTo(ws, { type: "fs_read_result", path: msg.path, error: "Access denied" });
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
var fsReadUserInfo = getOsUserInfoForWs(ws);
|
|
113
|
+
var ext = path.extname(fsFile).toLowerCase();
|
|
114
|
+
if (fsReadUserInfo) {
|
|
115
|
+
// Run stat and read as target OS user
|
|
116
|
+
var statResult = fsAsUser("stat", { file: fsFile }, fsReadUserInfo);
|
|
117
|
+
if (statResult.size > FS_MAX_SIZE) {
|
|
118
|
+
sendTo(ws, { type: "fs_read_result", path: msg.path, binary: true, size: statResult.size, error: "File too large (" + (statResult.size / 1024 / 1024).toFixed(1) + " MB)" });
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
if (BINARY_EXTS.has(ext)) {
|
|
122
|
+
var result = { type: "fs_read_result", path: msg.path, binary: true, size: statResult.size };
|
|
123
|
+
if (IMAGE_EXTS.has(ext)) result.imageUrl = "api/file?path=" + encodeURIComponent(msg.path);
|
|
124
|
+
sendTo(ws, result);
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
var readResult = fsAsUser("read", { file: fsFile, readContent: true }, fsReadUserInfo);
|
|
128
|
+
sendTo(ws, { type: "fs_read_result", path: msg.path, content: readResult.content, size: statResult.size });
|
|
129
|
+
} else {
|
|
130
|
+
var stat = fs.statSync(fsFile);
|
|
131
|
+
if (stat.size > FS_MAX_SIZE) {
|
|
132
|
+
sendTo(ws, { type: "fs_read_result", path: msg.path, binary: true, size: stat.size, error: "File too large (" + (stat.size / 1024 / 1024).toFixed(1) + " MB)" });
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
if (BINARY_EXTS.has(ext)) {
|
|
136
|
+
var result = { type: "fs_read_result", path: msg.path, binary: true, size: stat.size };
|
|
137
|
+
if (IMAGE_EXTS.has(ext)) result.imageUrl = "api/file?path=" + encodeURIComponent(msg.path);
|
|
138
|
+
sendTo(ws, result);
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
var content = fs.readFileSync(fsFile, "utf8");
|
|
142
|
+
sendTo(ws, { type: "fs_read_result", path: msg.path, content: content, size: stat.size });
|
|
143
|
+
}
|
|
144
|
+
} catch (e) {
|
|
145
|
+
sendTo(ws, { type: "fs_read_result", path: msg.path, error: e.message });
|
|
146
|
+
}
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// --- fs_write ---
|
|
151
|
+
if (msg.type === "fs_write") {
|
|
152
|
+
var fsWriteFile = safePath(cwd, msg.path);
|
|
153
|
+
if (!fsWriteFile && getOsUserInfoForWs(ws)) {
|
|
154
|
+
fsWriteFile = safeAbsPath(msg.path);
|
|
155
|
+
}
|
|
156
|
+
if (!fsWriteFile) {
|
|
157
|
+
sendTo(ws, { type: "fs_write_result", path: msg.path, ok: false, error: "Access denied" });
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
var fsWriteUserInfo = getOsUserInfoForWs(ws);
|
|
162
|
+
if (fsWriteUserInfo) {
|
|
163
|
+
fsAsUser("write", { file: fsWriteFile, content: msg.content || "" }, fsWriteUserInfo);
|
|
164
|
+
} else {
|
|
165
|
+
fs.writeFileSync(fsWriteFile, msg.content || "", "utf8");
|
|
166
|
+
}
|
|
167
|
+
sendTo(ws, { type: "fs_write_result", path: msg.path, ok: true });
|
|
168
|
+
} catch (e) {
|
|
169
|
+
sendTo(ws, { type: "fs_write_result", path: msg.path, ok: false, error: e.message });
|
|
170
|
+
}
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// --- Project settings permission gate ---
|
|
175
|
+
if (msg.type === "get_project_env" || msg.type === "set_project_env" ||
|
|
176
|
+
msg.type === "read_global_claude_md" || msg.type === "write_global_claude_md" ||
|
|
177
|
+
msg.type === "get_shared_env" || msg.type === "set_shared_env" ||
|
|
178
|
+
msg.type === "transfer_project_owner") {
|
|
179
|
+
if (ws._clayUser) {
|
|
180
|
+
var psPerms = usersModule.getEffectivePermissions(ws._clayUser, osUsers);
|
|
181
|
+
if (!psPerms.projectSettings) {
|
|
182
|
+
sendTo(ws, { type: "error", text: "Project settings access is not permitted" });
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// --- Project environment variables ---
|
|
189
|
+
if (msg.type === "get_project_env") {
|
|
190
|
+
var envrc = "";
|
|
191
|
+
var hasEnvrc = false;
|
|
192
|
+
if (typeof opts.onGetProjectEnv === "function") {
|
|
193
|
+
var envResult = opts.onGetProjectEnv(msg.slug);
|
|
194
|
+
envrc = envResult.envrc || "";
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
var envrcPath = path.join(cwd, ".envrc");
|
|
198
|
+
hasEnvrc = fs.existsSync(envrcPath);
|
|
199
|
+
} catch (e) {}
|
|
200
|
+
sendTo(ws, { type: "project_env_result", slug: msg.slug, envrc: envrc, hasEnvrc: hasEnvrc });
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (msg.type === "set_project_env") {
|
|
205
|
+
if (typeof opts.onSetProjectEnv === "function") {
|
|
206
|
+
var envError = validateEnvString(msg.envrc || "");
|
|
207
|
+
if (envError) {
|
|
208
|
+
sendTo(ws, { type: "set_project_env_result", ok: false, slug: msg.slug, error: envError });
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
var setResult = opts.onSetProjectEnv(msg.slug, msg.envrc || "");
|
|
212
|
+
sendTo(ws, { type: "set_project_env_result", ok: setResult.ok, slug: msg.slug, error: setResult.error });
|
|
213
|
+
} else {
|
|
214
|
+
sendTo(ws, { type: "set_project_env_result", ok: false, error: "Not supported" });
|
|
215
|
+
}
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// --- Global CLAUDE.md ---
|
|
220
|
+
if (msg.type === "read_global_claude_md") {
|
|
221
|
+
var globalMdPath = path.join(require("./config").REAL_HOME, ".claude", "CLAUDE.md");
|
|
222
|
+
try {
|
|
223
|
+
var globalMdContent = fs.readFileSync(globalMdPath, "utf8");
|
|
224
|
+
sendTo(ws, { type: "global_claude_md_result", content: globalMdContent });
|
|
225
|
+
} catch (e) {
|
|
226
|
+
sendTo(ws, { type: "global_claude_md_result", error: e.message });
|
|
227
|
+
}
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (msg.type === "write_global_claude_md") {
|
|
232
|
+
var globalMdDir = path.join(require("./config").REAL_HOME, ".claude");
|
|
233
|
+
var globalMdWritePath = path.join(globalMdDir, "CLAUDE.md");
|
|
234
|
+
try {
|
|
235
|
+
if (!fs.existsSync(globalMdDir)) {
|
|
236
|
+
fs.mkdirSync(globalMdDir, { recursive: true });
|
|
237
|
+
}
|
|
238
|
+
fs.writeFileSync(globalMdWritePath, msg.content || "", "utf8");
|
|
239
|
+
sendTo(ws, { type: "write_global_claude_md_result", ok: true });
|
|
240
|
+
} catch (e) {
|
|
241
|
+
sendTo(ws, { type: "write_global_claude_md_result", ok: false, error: e.message });
|
|
242
|
+
}
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// --- Shared environment variables ---
|
|
247
|
+
if (msg.type === "get_shared_env") {
|
|
248
|
+
var sharedEnvrc = "";
|
|
249
|
+
if (typeof opts.onGetSharedEnv === "function") {
|
|
250
|
+
var sharedResult = opts.onGetSharedEnv();
|
|
251
|
+
sharedEnvrc = sharedResult.envrc || "";
|
|
252
|
+
}
|
|
253
|
+
sendTo(ws, { type: "shared_env_result", envrc: sharedEnvrc });
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (msg.type === "set_shared_env") {
|
|
258
|
+
if (typeof opts.onSetSharedEnv === "function") {
|
|
259
|
+
var sharedEnvError = validateEnvString(msg.envrc || "");
|
|
260
|
+
if (sharedEnvError) {
|
|
261
|
+
sendTo(ws, { type: "set_shared_env_result", ok: false, error: sharedEnvError });
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
var sharedSetResult = opts.onSetSharedEnv(msg.envrc || "");
|
|
265
|
+
sendTo(ws, { type: "set_shared_env_result", ok: sharedSetResult.ok, error: sharedSetResult.error });
|
|
266
|
+
} else {
|
|
267
|
+
sendTo(ws, { type: "set_shared_env_result", ok: false, error: "Not supported" });
|
|
268
|
+
}
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// --- File watcher ---
|
|
273
|
+
if (msg.type === "fs_watch") {
|
|
274
|
+
if (msg.path) startFileWatch(msg.path);
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (msg.type === "fs_unwatch") {
|
|
279
|
+
stopFileWatch();
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// --- File edit history ---
|
|
284
|
+
if (msg.type === "fs_file_history") {
|
|
285
|
+
var histPath = msg.path;
|
|
286
|
+
if (!histPath) {
|
|
287
|
+
sendTo(ws, { type: "fs_file_history_result", path: histPath, entries: [] });
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
var absHistPath = path.resolve(cwd, histPath);
|
|
291
|
+
var entries = [];
|
|
292
|
+
|
|
293
|
+
// Collect session edits
|
|
294
|
+
sm.sessions.forEach(function (session) {
|
|
295
|
+
var sessionLocalId = session.localId;
|
|
296
|
+
var sessionTitle = session.title || "Untitled";
|
|
297
|
+
var histLen = session.history.length || 1;
|
|
298
|
+
|
|
299
|
+
for (var hi = 0; hi < session.history.length; hi++) {
|
|
300
|
+
var entry = session.history[hi];
|
|
301
|
+
if (entry.type !== "tool_executing") continue;
|
|
302
|
+
if (entry.name !== "Edit" && entry.name !== "Write") continue;
|
|
303
|
+
if (!entry.input || !entry.input.file_path) continue;
|
|
304
|
+
if (entry.input.file_path !== absHistPath) continue;
|
|
305
|
+
|
|
306
|
+
// Find parent assistant UUID + message snippet by scanning backwards
|
|
307
|
+
var assistantUuid = null;
|
|
308
|
+
var uuidIndex = -1;
|
|
309
|
+
for (var hj = hi - 1; hj >= 0; hj--) {
|
|
310
|
+
if (session.history[hj].type === "message_uuid" && session.history[hj].messageType === "assistant") {
|
|
311
|
+
assistantUuid = session.history[hj].uuid;
|
|
312
|
+
uuidIndex = hj;
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Find user prompt by scanning backwards from the assistant uuid
|
|
318
|
+
var messageSnippet = "";
|
|
319
|
+
var searchFrom = uuidIndex >= 0 ? uuidIndex : hi;
|
|
320
|
+
for (var hk = searchFrom - 1; hk >= 0; hk--) {
|
|
321
|
+
if (session.history[hk].type === "user_message" && session.history[hk].text) {
|
|
322
|
+
messageSnippet = session.history[hk].text.trim().substring(0, 100);
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Collect Claude's explanation: scan backwards from tool_executing
|
|
328
|
+
// to find the nearest delta text block (skipping tool_start).
|
|
329
|
+
// If no delta found immediately before this tool, scan past
|
|
330
|
+
// intervening tool blocks to find the last delta text within
|
|
331
|
+
// the same assistant turn.
|
|
332
|
+
var assistantSnippet = "";
|
|
333
|
+
var deltaChunks = [];
|
|
334
|
+
for (var hd = hi - 1; hd >= 0; hd--) {
|
|
335
|
+
var hEntry = session.history[hd];
|
|
336
|
+
if (hEntry.type === "tool_start") continue;
|
|
337
|
+
if (hEntry.type === "delta" && hEntry.text) {
|
|
338
|
+
deltaChunks.unshift(hEntry.text);
|
|
339
|
+
} else {
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (deltaChunks.length === 0) {
|
|
344
|
+
// No delta immediately before; scan past tool blocks
|
|
345
|
+
// to find the nearest preceding delta in the same turn
|
|
346
|
+
for (var hd2 = hi - 1; hd2 >= 0; hd2--) {
|
|
347
|
+
var hEntry2 = session.history[hd2];
|
|
348
|
+
if (hEntry2.type === "tool_start" || hEntry2.type === "tool_executing" || hEntry2.type === "tool_result") continue;
|
|
349
|
+
if (hEntry2.type === "delta" && hEntry2.text) {
|
|
350
|
+
// Found a delta before an earlier tool in the same turn.
|
|
351
|
+
// Collect this contiguous block of deltas.
|
|
352
|
+
for (var hd3 = hd2; hd3 >= 0; hd3--) {
|
|
353
|
+
var hEntry3 = session.history[hd3];
|
|
354
|
+
if (hEntry3.type === "tool_start") continue;
|
|
355
|
+
if (hEntry3.type === "delta" && hEntry3.text) {
|
|
356
|
+
deltaChunks.unshift(hEntry3.text);
|
|
357
|
+
} else {
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
break;
|
|
362
|
+
} else {
|
|
363
|
+
// Hit message_uuid, user_message, etc. Stop.
|
|
364
|
+
break;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
assistantSnippet = deltaChunks.join("").trim().substring(0, 150);
|
|
369
|
+
|
|
370
|
+
// Approximate timestamp: interpolate between session creation and last activity
|
|
371
|
+
var tStart = session.createdAt || 0;
|
|
372
|
+
var tEnd = session.lastActivity || tStart;
|
|
373
|
+
var ts = tStart + Math.floor((hi / histLen) * (tEnd - tStart));
|
|
374
|
+
|
|
375
|
+
var editRecord = {
|
|
376
|
+
source: "session",
|
|
377
|
+
timestamp: ts,
|
|
378
|
+
sessionLocalId: sessionLocalId,
|
|
379
|
+
sessionTitle: sessionTitle,
|
|
380
|
+
assistantUuid: assistantUuid,
|
|
381
|
+
toolId: entry.id,
|
|
382
|
+
messageSnippet: messageSnippet,
|
|
383
|
+
assistantSnippet: assistantSnippet,
|
|
384
|
+
toolName: entry.name,
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
if (entry.name === "Edit") {
|
|
388
|
+
editRecord.old_string = entry.input.old_string || "";
|
|
389
|
+
editRecord.new_string = entry.input.new_string || "";
|
|
390
|
+
} else {
|
|
391
|
+
editRecord.isFullWrite = true;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
entries.push(editRecord);
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
// Collect git commits
|
|
399
|
+
try {
|
|
400
|
+
var gitLog = execFileSync(
|
|
401
|
+
"git", ["log", "--format=%H|%at|%an|%s", "--follow", "--", histPath],
|
|
402
|
+
{ cwd: cwd, encoding: "utf8", timeout: 5000 }
|
|
403
|
+
);
|
|
404
|
+
var gitLines = gitLog.trim().split("\n");
|
|
405
|
+
for (var gi = 0; gi < gitLines.length; gi++) {
|
|
406
|
+
if (!gitLines[gi]) continue;
|
|
407
|
+
var parts = gitLines[gi].split("|");
|
|
408
|
+
if (parts.length < 4) continue;
|
|
409
|
+
entries.push({
|
|
410
|
+
source: "git",
|
|
411
|
+
hash: parts[0],
|
|
412
|
+
timestamp: parseInt(parts[1], 10) * 1000,
|
|
413
|
+
author: parts[2],
|
|
414
|
+
message: parts.slice(3).join("|"),
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
} catch (e) {
|
|
418
|
+
// Not a git repo or file not tracked, that is fine
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Sort by timestamp descending (newest first)
|
|
422
|
+
entries.sort(function (a, b) { return b.timestamp - a.timestamp; });
|
|
423
|
+
|
|
424
|
+
sendTo(ws, { type: "fs_file_history_result", path: histPath, entries: entries });
|
|
425
|
+
return true;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// --- Git diff for file history ---
|
|
429
|
+
if (msg.type === "fs_git_diff") {
|
|
430
|
+
var diffPath = msg.path;
|
|
431
|
+
var hash = msg.hash;
|
|
432
|
+
var hash2 = msg.hash2 || null;
|
|
433
|
+
if (!diffPath || !hash) {
|
|
434
|
+
sendTo(ws, { type: "fs_git_diff_result", hash: hash, path: diffPath, diff: "", error: "Missing params" });
|
|
435
|
+
return true;
|
|
436
|
+
}
|
|
437
|
+
try {
|
|
438
|
+
var diff;
|
|
439
|
+
if (hash2) {
|
|
440
|
+
diff = execFileSync("git", ["diff", hash, hash2, "--", diffPath],
|
|
441
|
+
{ cwd: cwd, encoding: "utf8", timeout: 5000 });
|
|
442
|
+
} else {
|
|
443
|
+
diff = execFileSync("git", ["show", hash, "--format=", "--", diffPath],
|
|
444
|
+
{ cwd: cwd, encoding: "utf8", timeout: 5000 });
|
|
445
|
+
}
|
|
446
|
+
sendTo(ws, { type: "fs_git_diff_result", hash: hash, hash2: hash2, path: diffPath, diff: diff || "" });
|
|
447
|
+
} catch (e) {
|
|
448
|
+
sendTo(ws, { type: "fs_git_diff_result", hash: hash, hash2: hash2, path: diffPath, diff: "", error: e.message });
|
|
449
|
+
}
|
|
450
|
+
return true;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// --- File content at a git commit ---
|
|
454
|
+
if (msg.type === "fs_file_at") {
|
|
455
|
+
var atPath = msg.path;
|
|
456
|
+
var atHash = msg.hash;
|
|
457
|
+
if (!atPath || !atHash) {
|
|
458
|
+
sendTo(ws, { type: "fs_file_at_result", hash: atHash, path: atPath, content: "", error: "Missing params" });
|
|
459
|
+
return true;
|
|
460
|
+
}
|
|
461
|
+
try {
|
|
462
|
+
// Convert to repo-relative path (git show requires hash:relative/path)
|
|
463
|
+
var atAbsPath = path.resolve(cwd, atPath);
|
|
464
|
+
var atRelPath = path.relative(cwd, atAbsPath);
|
|
465
|
+
var content = execFileSync("git", ["show", atHash + ":" + atRelPath],
|
|
466
|
+
{ cwd: cwd, encoding: "utf8", timeout: 5000 });
|
|
467
|
+
sendTo(ws, { type: "fs_file_at_result", hash: atHash, path: atPath, content: content });
|
|
468
|
+
} catch (e) {
|
|
469
|
+
sendTo(ws, { type: "fs_file_at_result", hash: atHash, path: atPath, content: "", error: e.message });
|
|
470
|
+
}
|
|
471
|
+
return true;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return false;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return {
|
|
478
|
+
handleFilesystemMessage: handleFilesystemMessage,
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
module.exports = { attachFilesystem: attachFilesystem };
|