claude-relay 2.4.2 → 2.5.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.
Files changed (75) hide show
  1. package/bin/cli.js +1 -2350
  2. package/package.json +7 -42
  3. package/LICENSE +0 -21
  4. package/README.md +0 -281
  5. package/lib/cli-sessions.js +0 -270
  6. package/lib/config.js +0 -222
  7. package/lib/daemon.js +0 -423
  8. package/lib/ipc.js +0 -112
  9. package/lib/pages.js +0 -714
  10. package/lib/project.js +0 -1224
  11. package/lib/public/app.js +0 -2157
  12. package/lib/public/apple-touch-icon.png +0 -0
  13. package/lib/public/css/base.css +0 -145
  14. package/lib/public/css/diff.css +0 -128
  15. package/lib/public/css/filebrowser.css +0 -1076
  16. package/lib/public/css/highlight.css +0 -144
  17. package/lib/public/css/input.css +0 -512
  18. package/lib/public/css/menus.css +0 -683
  19. package/lib/public/css/messages.css +0 -1159
  20. package/lib/public/css/overlays.css +0 -731
  21. package/lib/public/css/rewind.css +0 -529
  22. package/lib/public/css/sidebar.css +0 -794
  23. package/lib/public/favicon.svg +0 -26
  24. package/lib/public/icon-192.png +0 -0
  25. package/lib/public/icon-512.png +0 -0
  26. package/lib/public/icon-mono.svg +0 -19
  27. package/lib/public/index.html +0 -460
  28. package/lib/public/manifest.json +0 -27
  29. package/lib/public/modules/diff.js +0 -398
  30. package/lib/public/modules/events.js +0 -21
  31. package/lib/public/modules/filebrowser.js +0 -1375
  32. package/lib/public/modules/fileicons.js +0 -172
  33. package/lib/public/modules/icons.js +0 -54
  34. package/lib/public/modules/input.js +0 -578
  35. package/lib/public/modules/markdown.js +0 -149
  36. package/lib/public/modules/notifications.js +0 -643
  37. package/lib/public/modules/qrcode.js +0 -70
  38. package/lib/public/modules/rewind.js +0 -334
  39. package/lib/public/modules/sidebar.js +0 -628
  40. package/lib/public/modules/state.js +0 -3
  41. package/lib/public/modules/terminal.js +0 -658
  42. package/lib/public/modules/theme.js +0 -622
  43. package/lib/public/modules/tools.js +0 -1410
  44. package/lib/public/modules/utils.js +0 -56
  45. package/lib/public/style.css +0 -10
  46. package/lib/public/sw.js +0 -75
  47. package/lib/push.js +0 -125
  48. package/lib/sdk-bridge.js +0 -771
  49. package/lib/server.js +0 -577
  50. package/lib/sessions.js +0 -402
  51. package/lib/terminal-manager.js +0 -187
  52. package/lib/terminal.js +0 -24
  53. package/lib/themes/ayu-light.json +0 -9
  54. package/lib/themes/catppuccin-latte.json +0 -9
  55. package/lib/themes/catppuccin-mocha.json +0 -9
  56. package/lib/themes/claude-light.json +0 -9
  57. package/lib/themes/claude.json +0 -9
  58. package/lib/themes/dracula.json +0 -9
  59. package/lib/themes/everforest-light.json +0 -9
  60. package/lib/themes/everforest.json +0 -9
  61. package/lib/themes/github-light.json +0 -9
  62. package/lib/themes/gruvbox-dark.json +0 -9
  63. package/lib/themes/gruvbox-light.json +0 -9
  64. package/lib/themes/monokai.json +0 -9
  65. package/lib/themes/nord-light.json +0 -9
  66. package/lib/themes/nord.json +0 -9
  67. package/lib/themes/one-dark.json +0 -9
  68. package/lib/themes/one-light.json +0 -9
  69. package/lib/themes/rose-pine-dawn.json +0 -9
  70. package/lib/themes/rose-pine.json +0 -9
  71. package/lib/themes/solarized-dark.json +0 -9
  72. package/lib/themes/solarized-light.json +0 -9
  73. package/lib/themes/tokyo-night-light.json +0 -9
  74. package/lib/themes/tokyo-night.json +0 -9
  75. package/lib/updater.js +0 -96
package/lib/project.js DELETED
@@ -1,1224 +0,0 @@
1
- var fs = require("fs");
2
- var path = require("path");
3
- var { createSessionManager } = require("./sessions");
4
- var { createSDKBridge } = require("./sdk-bridge");
5
- var { createTerminalManager } = require("./terminal-manager");
6
- var { fetchLatestVersion, isNewer } = require("./updater");
7
- var { execFileSync } = require("child_process");
8
-
9
- // SDK loaded dynamically (ESM module)
10
- var sdkModule = null;
11
- function getSDK() {
12
- if (!sdkModule) sdkModule = import("@anthropic-ai/claude-agent-sdk");
13
- return sdkModule;
14
- }
15
-
16
- // --- Shared constants ---
17
- var IGNORED_DIRS = new Set(["node_modules", ".git", ".next", "__pycache__", ".cache", "dist", "build", ".claude-relay"]);
18
- var BINARY_EXTS = new Set([
19
- ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".ico", ".webp",
20
- ".woff", ".woff2", ".ttf", ".eot", ".otf",
21
- ".zip", ".tar", ".gz", ".bz2", ".7z", ".rar",
22
- ".pdf", ".doc", ".docx", ".xls", ".xlsx",
23
- ".exe", ".dll", ".so", ".dylib",
24
- ".mp3", ".mp4", ".wav", ".avi", ".mov",
25
- ".pyc", ".o", ".a", ".class",
26
- ]);
27
- var IMAGE_EXTS = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".bmp", ".ico"]);
28
- var FS_MAX_SIZE = 512 * 1024;
29
- var MIME_TYPES = {
30
- ".html": "text/html",
31
- ".css": "text/css",
32
- ".js": "application/javascript",
33
- ".json": "application/json",
34
- ".png": "image/png",
35
- ".jpg": "image/jpeg",
36
- ".jpeg": "image/jpeg",
37
- ".gif": "image/gif",
38
- ".webp": "image/webp",
39
- ".bmp": "image/bmp",
40
- ".svg": "image/svg+xml",
41
- ".ico": "image/x-icon",
42
- };
43
-
44
- function safePath(base, requested) {
45
- var resolved = path.resolve(base, requested);
46
- if (resolved !== base && !resolved.startsWith(base + path.sep)) return null;
47
- try {
48
- var real = fs.realpathSync(resolved);
49
- if (real !== base && !real.startsWith(base + path.sep)) return null;
50
- return real;
51
- } catch (e) {
52
- return null;
53
- }
54
- }
55
-
56
- /**
57
- * Create a project context — per-project state and handlers.
58
- * opts: { cwd, slug, title, pushModule, debug, dangerouslySkipPermissions, currentVersion }
59
- */
60
- function createProjectContext(opts) {
61
- var cwd = opts.cwd;
62
- var slug = opts.slug;
63
- var project = path.basename(cwd);
64
- var title = opts.title || null;
65
- var pushModule = opts.pushModule || null;
66
- var debug = opts.debug || false;
67
- var dangerouslySkipPermissions = opts.dangerouslySkipPermissions || false;
68
- var currentVersion = opts.currentVersion;
69
- var lanHost = opts.lanHost || null;
70
- var getProjectCount = opts.getProjectCount || function () { return 1; };
71
- var getProjectList = opts.getProjectList || function () { return []; };
72
- var latestVersion = null;
73
-
74
- // --- Per-project clients ---
75
- var clients = new Set();
76
-
77
- function send(obj) {
78
- var data = JSON.stringify(obj);
79
- for (var ws of clients) {
80
- if (ws.readyState === 1) ws.send(data);
81
- }
82
- }
83
-
84
- function sendTo(ws, obj) {
85
- if (ws.readyState === 1) ws.send(JSON.stringify(obj));
86
- }
87
-
88
- function broadcastClientCount() {
89
- send({ type: "client_count", count: clients.size });
90
- }
91
-
92
- function sendToOthers(sender, obj) {
93
- var data = JSON.stringify(obj);
94
- for (var ws of clients) {
95
- if (ws !== sender && ws.readyState === 1) ws.send(data);
96
- }
97
- }
98
-
99
- // --- File watcher ---
100
- var fileWatcher = null;
101
- var watchedPath = null;
102
- var watchDebounce = null;
103
-
104
- function startFileWatch(relPath) {
105
- var absPath = safePath(cwd, relPath);
106
- if (!absPath) return;
107
- if (watchedPath === relPath) return;
108
- stopFileWatch();
109
- watchedPath = relPath;
110
- try {
111
- fileWatcher = fs.watch(absPath, function () {
112
- clearTimeout(watchDebounce);
113
- watchDebounce = setTimeout(function () {
114
- try {
115
- var stat = fs.statSync(absPath);
116
- var ext = path.extname(absPath).toLowerCase();
117
- if (stat.size > FS_MAX_SIZE || BINARY_EXTS.has(ext)) return;
118
- var content = fs.readFileSync(absPath, "utf8");
119
- send({ type: "fs_file_changed", path: relPath, content: content, size: stat.size });
120
- } catch (e) {
121
- stopFileWatch();
122
- }
123
- }, 200);
124
- });
125
- fileWatcher.on("error", function () { stopFileWatch(); });
126
- } catch (e) {
127
- watchedPath = null;
128
- }
129
- }
130
-
131
- function stopFileWatch() {
132
- if (fileWatcher) {
133
- try { fileWatcher.close(); } catch (e) {}
134
- fileWatcher = null;
135
- }
136
- clearTimeout(watchDebounce);
137
- watchDebounce = null;
138
- watchedPath = null;
139
- }
140
-
141
- // --- Directory watcher ---
142
- var dirWatchers = {}; // relPath -> { watcher, debounce }
143
-
144
- function startDirWatch(relPath) {
145
- if (dirWatchers[relPath]) return;
146
- var absPath = safePath(cwd, relPath);
147
- if (!absPath) return;
148
- try {
149
- var debounce = null;
150
- var watcher = fs.watch(absPath, function () {
151
- clearTimeout(debounce);
152
- debounce = setTimeout(function () {
153
- // Re-read directory and broadcast to all clients
154
- try {
155
- var items = fs.readdirSync(absPath, { withFileTypes: true });
156
- var entries = [];
157
- for (var i = 0; i < items.length; i++) {
158
- if (items[i].isDirectory() && IGNORED_DIRS.has(items[i].name)) continue;
159
- entries.push({
160
- name: items[i].name,
161
- type: items[i].isDirectory() ? "dir" : "file",
162
- path: path.relative(cwd, path.join(absPath, items[i].name)).split(path.sep).join("/"),
163
- });
164
- }
165
- send({ type: "fs_dir_changed", path: relPath, entries: entries });
166
- } catch (e) {
167
- stopDirWatch(relPath);
168
- }
169
- }, 300);
170
- });
171
- watcher.on("error", function () { stopDirWatch(relPath); });
172
- dirWatchers[relPath] = { watcher: watcher, debounce: debounce };
173
- } catch (e) {}
174
- }
175
-
176
- function stopDirWatch(relPath) {
177
- var entry = dirWatchers[relPath];
178
- if (entry) {
179
- clearTimeout(entry.debounce);
180
- try { entry.watcher.close(); } catch (e) {}
181
- delete dirWatchers[relPath];
182
- }
183
- }
184
-
185
- function stopAllDirWatches() {
186
- var paths = Object.keys(dirWatchers);
187
- for (var i = 0; i < paths.length; i++) {
188
- stopDirWatch(paths[i]);
189
- }
190
- }
191
-
192
- // --- Session manager ---
193
- var sm = createSessionManager({ cwd: cwd, send: send });
194
-
195
- // --- SDK bridge ---
196
- var sdk = createSDKBridge({
197
- cwd: cwd,
198
- slug: slug,
199
- sessionManager: sm,
200
- send: send,
201
- pushModule: pushModule,
202
- getSDK: getSDK,
203
- dangerouslySkipPermissions: dangerouslySkipPermissions,
204
- });
205
-
206
- // --- Terminal manager ---
207
- var tm = createTerminalManager({ cwd: cwd, send: send, sendTo: sendTo });
208
-
209
- // Check for updates in background
210
- fetchLatestVersion().then(function (v) {
211
- if (v && isNewer(v, currentVersion)) {
212
- latestVersion = v;
213
- send({ type: "update_available", version: v });
214
- }
215
- });
216
-
217
- // --- WS connection handler ---
218
- function handleConnection(ws) {
219
- clients.add(ws);
220
- broadcastClientCount();
221
-
222
- // Send cached state
223
- sendTo(ws, { type: "info", cwd: cwd, slug: slug, project: title || project, version: currentVersion, debug: !!debug, dangerouslySkipPermissions: dangerouslySkipPermissions, lanHost: lanHost, projectCount: getProjectCount(), projects: getProjectList() });
224
- if (latestVersion) {
225
- sendTo(ws, { type: "update_available", version: latestVersion });
226
- }
227
- if (sm.slashCommands) {
228
- sendTo(ws, { type: "slash_commands", commands: sm.slashCommands });
229
- }
230
- if (sm.currentModel) {
231
- sendTo(ws, { type: "model_info", model: sm.currentModel, models: sm.availableModels || [] });
232
- }
233
- sendTo(ws, { type: "term_list", terminals: tm.list() });
234
-
235
- // Session list
236
- sendTo(ws, {
237
- type: "session_list",
238
- sessions: [].concat(Array.from(sm.sessions.values())).map(function (s) {
239
- return {
240
- id: s.localId,
241
- cliSessionId: s.cliSessionId || null,
242
- title: s.title || "New Session",
243
- active: s.localId === sm.activeSessionId,
244
- isProcessing: s.isProcessing,
245
- lastActivity: s.lastActivity || s.createdAt || 0,
246
- };
247
- }),
248
- });
249
-
250
- // Restore active session for this client
251
- var active = sm.getActiveSession();
252
- if (active) {
253
- sendTo(ws, { type: "session_switched", id: active.localId, cliSessionId: active.cliSessionId || null });
254
-
255
- var total = active.history.length;
256
- var fromIndex = 0;
257
- if (total > sm.HISTORY_PAGE_SIZE) {
258
- fromIndex = sm.findTurnBoundary(active.history, Math.max(0, total - sm.HISTORY_PAGE_SIZE));
259
- }
260
- sendTo(ws, { type: "history_meta", total: total, from: fromIndex });
261
- for (var i = fromIndex; i < total; i++) {
262
- sendTo(ws, active.history[i]);
263
- }
264
- sendTo(ws, { type: "history_done" });
265
-
266
- if (active.isProcessing) {
267
- sendTo(ws, { type: "status", status: "processing" });
268
- }
269
- var pendingIds = Object.keys(active.pendingPermissions);
270
- for (var pi = 0; pi < pendingIds.length; pi++) {
271
- var p = active.pendingPermissions[pendingIds[pi]];
272
- sendTo(ws, {
273
- type: "permission_request_pending",
274
- requestId: p.requestId,
275
- toolName: p.toolName,
276
- toolInput: p.toolInput,
277
- toolUseId: p.toolUseId,
278
- decisionReason: p.decisionReason,
279
- });
280
- }
281
- }
282
-
283
- ws.on("message", function (raw) {
284
- var msg;
285
- try { msg = JSON.parse(raw.toString()); } catch (e) { return; }
286
- handleMessage(ws, msg);
287
- });
288
-
289
- ws.on("close", function () {
290
- handleDisconnection(ws);
291
- });
292
- }
293
-
294
- // --- WS message handler ---
295
- function handleMessage(ws, msg) {
296
- if (msg.type === "push_subscribe") {
297
- if (pushModule && msg.subscription) pushModule.addSubscription(msg.subscription, msg.replaceEndpoint);
298
- return;
299
- }
300
-
301
- if (msg.type === "load_more_history") {
302
- var session = sm.getActiveSession();
303
- if (!session || typeof msg.before !== "number") return;
304
- var before = msg.before;
305
- var from = sm.findTurnBoundary(session.history, Math.max(0, before - sm.HISTORY_PAGE_SIZE));
306
- var to = before;
307
- var items = session.history.slice(from, to);
308
- sendTo(ws, {
309
- type: "history_prepend",
310
- items: items,
311
- meta: { from: from, to: to, hasMore: from > 0 },
312
- });
313
- return;
314
- }
315
-
316
- if (msg.type === "new_session") {
317
- sm.createSession();
318
- return;
319
- }
320
-
321
- if (msg.type === "resume_session") {
322
- if (!msg.cliSessionId) return;
323
- var cliSess = require("./cli-sessions");
324
- cliSess.readCliSessionHistory(cwd, msg.cliSessionId).then(function (history) {
325
- var title = "Resumed session";
326
- for (var i = 0; i < history.length; i++) {
327
- if (history[i].type === "user_message" && history[i].text) {
328
- title = history[i].text.substring(0, 50);
329
- break;
330
- }
331
- }
332
- sm.resumeSession(msg.cliSessionId, { history: history, title: title });
333
- }).catch(function () {
334
- sm.resumeSession(msg.cliSessionId);
335
- });
336
- return;
337
- }
338
-
339
- if (msg.type === "list_cli_sessions") {
340
- var cliSessions = require("./cli-sessions");
341
- var _fs = require("fs");
342
- var _path = require("path");
343
- // Collect session IDs already in relay (in-memory + persisted on disk)
344
- var relayIds = {};
345
- sm.sessions.forEach(function (s) {
346
- if (s.cliSessionId) relayIds[s.cliSessionId] = true;
347
- });
348
- try {
349
- var sessDir = sm.sessionsDir;
350
- var diskFiles = _fs.readdirSync(sessDir);
351
- for (var fi = 0; fi < diskFiles.length; fi++) {
352
- if (diskFiles[fi].endsWith(".jsonl")) {
353
- relayIds[diskFiles[fi].replace(".jsonl", "")] = true;
354
- }
355
- }
356
- } catch (e) {}
357
- cliSessions.listCliSessions(cwd).then(function (sessions) {
358
- var filtered = sessions.filter(function (s) {
359
- return !relayIds[s.sessionId];
360
- });
361
- sendTo(ws, { type: "cli_session_list", sessions: filtered });
362
- }).catch(function () {
363
- sendTo(ws, { type: "cli_session_list", sessions: [] });
364
- });
365
- return;
366
- }
367
-
368
-
369
- if (msg.type === "switch_session") {
370
- if (msg.id && sm.sessions.has(msg.id)) {
371
- sm.switchSession(msg.id);
372
- }
373
- return;
374
- }
375
-
376
- if (msg.type === "delete_session") {
377
- if (msg.id && sm.sessions.has(msg.id)) {
378
- sm.deleteSession(msg.id);
379
- }
380
- return;
381
- }
382
-
383
- if (msg.type === "rename_session") {
384
- if (msg.id && sm.sessions.has(msg.id) && msg.title) {
385
- var s = sm.sessions.get(msg.id);
386
- s.title = String(msg.title).substring(0, 100);
387
- sm.saveSessionFile(s);
388
- sm.broadcastSessionList();
389
- }
390
- return;
391
- }
392
-
393
- if (msg.type === "search_sessions") {
394
- var results = sm.searchSessions(msg.query || "");
395
- sendTo(ws, { type: "search_results", query: msg.query || "", results: results });
396
- return;
397
- }
398
-
399
- if (msg.type === "check_update") {
400
- fetchLatestVersion().then(function (v) {
401
- if (v && isNewer(v, currentVersion)) {
402
- latestVersion = v;
403
- sendTo(ws, { type: "update_available", version: v });
404
- }
405
- }).catch(function () {});
406
- return;
407
- }
408
-
409
- if (msg.type === "update_now") {
410
- send({ type: "update_started", version: latestVersion || "" });
411
- var _ipc = require("./ipc");
412
- var _config = require("./config");
413
- _ipc.sendIPCCommand(_config.socketPath(), { cmd: "update" });
414
- return;
415
- }
416
-
417
- if (msg.type === "process_stats") {
418
- var sessionCount = sm.sessions.size;
419
- var processingCount = 0;
420
- sm.sessions.forEach(function (s) {
421
- if (s.isProcessing) processingCount++;
422
- });
423
- var mem = process.memoryUsage();
424
- sendTo(ws, {
425
- type: "process_stats",
426
- pid: process.pid,
427
- uptime: process.uptime(),
428
- memory: {
429
- rss: mem.rss,
430
- heapUsed: mem.heapUsed,
431
- heapTotal: mem.heapTotal,
432
- external: mem.external,
433
- },
434
- sessions: sessionCount,
435
- processing: processingCount,
436
- clients: clients.size,
437
- terminals: tm.list().length,
438
- });
439
- return;
440
- }
441
-
442
- if (msg.type === "stop") {
443
- var session = sm.getActiveSession();
444
- if (session && session.abortController && session.isProcessing) {
445
- session.abortController.abort();
446
- }
447
- return;
448
- }
449
-
450
-
451
- if (msg.type === "set_model" && msg.model) {
452
- var session = sm.getActiveSession();
453
- if (session) {
454
- sdk.setModel(session, msg.model);
455
- }
456
- return;
457
- }
458
-
459
- if (msg.type === "rewind_preview") {
460
- var session = sm.getActiveSession();
461
- if (!session || !session.cliSessionId || !msg.uuid) return;
462
-
463
- (async function () {
464
- var result;
465
- try {
466
- result = await sdk.getOrCreateRewindQuery(session);
467
- var preview = await result.query.rewindFiles(msg.uuid, { dryRun: true });
468
- var diffs = {};
469
- var changedFiles = preview.filesChanged || [];
470
- for (var f = 0; f < changedFiles.length; f++) {
471
- try {
472
- diffs[changedFiles[f]] = execFileSync(
473
- "git", ["diff", "HEAD", "--", changedFiles[f]],
474
- { cwd: cwd, encoding: "utf8", timeout: 5000 }
475
- ) || "";
476
- } catch (e) { diffs[changedFiles[f]] = ""; }
477
- }
478
- sendTo(ws, { type: "rewind_preview_result", preview: preview, diffs: diffs, uuid: msg.uuid });
479
- } catch (err) {
480
- sendTo(ws, { type: "rewind_error", text: "Failed to preview rewind: " + err.message });
481
- } finally {
482
- if (result && result.isTemp) result.cleanup();
483
- }
484
- })();
485
- return;
486
- }
487
-
488
- if (msg.type === "rewind_execute") {
489
- var session = sm.getActiveSession();
490
- if (!session || !session.cliSessionId || !msg.uuid) return;
491
- var mode = msg.mode || "both";
492
-
493
- (async function () {
494
- var result;
495
- try {
496
- // File restoration (skip for chat-only mode)
497
- if (mode !== "chat") {
498
- result = await sdk.getOrCreateRewindQuery(session);
499
- await result.query.rewindFiles(msg.uuid, { dryRun: false });
500
- }
501
-
502
- // Conversation rollback (skip for files-only mode)
503
- if (mode !== "files") {
504
- var targetIdx = -1;
505
- for (var i = 0; i < session.messageUUIDs.length; i++) {
506
- if (session.messageUUIDs[i].uuid === msg.uuid) {
507
- targetIdx = i;
508
- break;
509
- }
510
- }
511
-
512
- if (targetIdx >= 0) {
513
- var trimTo = session.messageUUIDs[targetIdx].historyIndex;
514
- for (var k = trimTo - 1; k >= 0; k--) {
515
- if (session.history[k].type === "user_message") {
516
- trimTo = k;
517
- break;
518
- }
519
- }
520
- session.history = session.history.slice(0, trimTo);
521
- session.messageUUIDs = session.messageUUIDs.slice(0, targetIdx);
522
- }
523
-
524
- session.lastRewindUuid = msg.uuid;
525
- }
526
-
527
- if (session.abortController) {
528
- try { session.abortController.abort(); } catch (e) {}
529
- }
530
- if (session.messageQueue) {
531
- try { session.messageQueue.end(); } catch (e) {}
532
- }
533
- session.queryInstance = null;
534
- session.messageQueue = null;
535
- session.abortController = null;
536
- session.blocks = {};
537
- session.sentToolResults = {};
538
- session.pendingPermissions = {};
539
- session.pendingAskUser = {};
540
- session.isProcessing = false;
541
-
542
- sm.saveSessionFile(session);
543
- sm.switchSession(session.localId);
544
- sm.sendAndRecord(session, { type: "rewind_complete", mode: mode });
545
- sm.broadcastSessionList();
546
- } catch (err) {
547
- send({ type: "rewind_error", text: "Rewind failed: " + err.message });
548
- } finally {
549
- if (result && result.isTemp) result.cleanup();
550
- }
551
- })();
552
- return;
553
- }
554
-
555
- if (msg.type === "ask_user_response") {
556
- var session = sm.getActiveSession();
557
- if (!session) return;
558
- var toolId = msg.toolId;
559
- var answers = msg.answers || {};
560
- var pending = session.pendingAskUser[toolId];
561
- if (!pending) return;
562
- delete session.pendingAskUser[toolId];
563
- sm.sendAndRecord(session, { type: "ask_user_answered", toolId: toolId });
564
- pending.resolve({
565
- behavior: "allow",
566
- updatedInput: Object.assign({}, pending.input, { answers: answers }),
567
- });
568
- return;
569
- }
570
-
571
- if (msg.type === "input_sync") {
572
- sendToOthers(ws, msg);
573
- return;
574
- }
575
-
576
- if (msg.type === "permission_response") {
577
- var session = sm.getActiveSession();
578
- if (!session) return;
579
- var requestId = msg.requestId;
580
- var decision = msg.decision;
581
- var pending = session.pendingPermissions[requestId];
582
- if (!pending) return;
583
- delete session.pendingPermissions[requestId];
584
-
585
- if (decision === "allow" || decision === "allow_always") {
586
- if (decision === "allow_always") {
587
- if (!session.allowedTools) session.allowedTools = {};
588
- session.allowedTools[pending.toolName] = true;
589
- }
590
- pending.resolve({ behavior: "allow", updatedInput: pending.toolInput });
591
- } else {
592
- pending.resolve({ behavior: "deny", message: "User denied permission" });
593
- }
594
-
595
- sm.sendAndRecord(session, {
596
- type: "permission_resolved",
597
- requestId: requestId,
598
- decision: decision,
599
- });
600
- return;
601
- }
602
-
603
- // --- Browse directories (for add-project autocomplete) ---
604
- if (msg.type === "browse_dir") {
605
- var rawPath = (msg.path || "").replace(/^~/, process.env.HOME || "/");
606
- var absTarget = path.resolve(rawPath);
607
- var parentDir, prefix;
608
- try {
609
- var stat = fs.statSync(absTarget);
610
- if (stat.isDirectory()) {
611
- // Input is an existing directory — list its children
612
- parentDir = absTarget;
613
- prefix = "";
614
- } else {
615
- parentDir = path.dirname(absTarget);
616
- prefix = path.basename(absTarget).toLowerCase();
617
- }
618
- } catch (e) {
619
- // Path doesn't exist — list parent and filter by typed prefix
620
- parentDir = path.dirname(absTarget);
621
- prefix = path.basename(absTarget).toLowerCase();
622
- }
623
- try {
624
- var dirItems = fs.readdirSync(parentDir, { withFileTypes: true });
625
- var dirEntries = [];
626
- for (var di = 0; di < dirItems.length; di++) {
627
- var d = dirItems[di];
628
- if (!d.isDirectory()) continue;
629
- if (d.name.charAt(0) === ".") continue;
630
- if (IGNORED_DIRS.has(d.name)) continue;
631
- if (prefix && !d.name.toLowerCase().startsWith(prefix)) continue;
632
- dirEntries.push({ name: d.name, path: path.join(parentDir, d.name) });
633
- }
634
- dirEntries.sort(function (a, b) { return a.name.localeCompare(b.name); });
635
- sendTo(ws, { type: "browse_dir_result", path: msg.path, entries: dirEntries });
636
- } catch (e) {
637
- sendTo(ws, { type: "browse_dir_result", path: msg.path, entries: [], error: e.message });
638
- }
639
- return;
640
- }
641
-
642
- // --- Add project from web UI ---
643
- if (msg.type === "add_project") {
644
- var addPath = (msg.path || "").replace(/^~/, process.env.HOME || "/");
645
- var addAbs = path.resolve(addPath);
646
- try {
647
- var addStat = fs.statSync(addAbs);
648
- if (!addStat.isDirectory()) {
649
- sendTo(ws, { type: "add_project_result", ok: false, error: "Not a directory" });
650
- return;
651
- }
652
- } catch (e) {
653
- sendTo(ws, { type: "add_project_result", ok: false, error: "Directory not found" });
654
- return;
655
- }
656
- if (typeof opts.onAddProject === "function") {
657
- var result = opts.onAddProject(addAbs);
658
- sendTo(ws, { type: "add_project_result", ok: result.ok, slug: result.slug, error: result.error, existing: result.existing });
659
- } else {
660
- sendTo(ws, { type: "add_project_result", ok: false, error: "Not supported" });
661
- }
662
- return;
663
- }
664
-
665
- // --- Remove project from web UI ---
666
- if (msg.type === "remove_project") {
667
- var removeSlug = msg.slug;
668
- if (!removeSlug) {
669
- sendTo(ws, { type: "remove_project_result", ok: false, error: "Missing slug" });
670
- return;
671
- }
672
- if (typeof opts.onRemoveProject === "function") {
673
- var removeResult = opts.onRemoveProject(removeSlug);
674
- sendTo(ws, { type: "remove_project_result", ok: removeResult.ok, slug: removeSlug, error: removeResult.error });
675
- } else {
676
- sendTo(ws, { type: "remove_project_result", ok: false, error: "Not supported" });
677
- }
678
- return;
679
- }
680
-
681
- // --- File browser ---
682
- if (msg.type === "fs_list") {
683
- var fsDir = safePath(cwd, msg.path || ".");
684
- if (!fsDir) {
685
- sendTo(ws, { type: "fs_list_result", path: msg.path, entries: [], error: "Access denied" });
686
- return;
687
- }
688
- try {
689
- var items = fs.readdirSync(fsDir, { withFileTypes: true });
690
- var entries = [];
691
- for (var fi = 0; fi < items.length; fi++) {
692
- var item = items[fi];
693
- if (item.isDirectory() && IGNORED_DIRS.has(item.name)) continue;
694
- entries.push({
695
- name: item.name,
696
- type: item.isDirectory() ? "dir" : "file",
697
- path: path.relative(cwd, path.join(fsDir, item.name)).split(path.sep).join("/"),
698
- });
699
- }
700
- sendTo(ws, { type: "fs_list_result", path: msg.path || ".", entries: entries });
701
- // Auto-watch the directory for changes
702
- startDirWatch(msg.path || ".");
703
- } catch (e) {
704
- sendTo(ws, { type: "fs_list_result", path: msg.path, entries: [], error: e.message });
705
- }
706
- return;
707
- }
708
-
709
- if (msg.type === "fs_read") {
710
- var fsFile = safePath(cwd, msg.path);
711
- if (!fsFile) {
712
- sendTo(ws, { type: "fs_read_result", path: msg.path, error: "Access denied" });
713
- return;
714
- }
715
- try {
716
- var stat = fs.statSync(fsFile);
717
- var ext = path.extname(fsFile).toLowerCase();
718
- if (stat.size > FS_MAX_SIZE) {
719
- 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)" });
720
- return;
721
- }
722
- if (BINARY_EXTS.has(ext)) {
723
- var result = { type: "fs_read_result", path: msg.path, binary: true, size: stat.size };
724
- if (IMAGE_EXTS.has(ext)) result.imageUrl = "api/file?path=" + encodeURIComponent(msg.path);
725
- sendTo(ws, result);
726
- return;
727
- }
728
- var content = fs.readFileSync(fsFile, "utf8");
729
- sendTo(ws, { type: "fs_read_result", path: msg.path, content: content, size: stat.size });
730
- } catch (e) {
731
- sendTo(ws, { type: "fs_read_result", path: msg.path, error: e.message });
732
- }
733
- return;
734
- }
735
-
736
- // --- File watcher ---
737
- if (msg.type === "fs_watch") {
738
- if (msg.path) startFileWatch(msg.path);
739
- return;
740
- }
741
-
742
- if (msg.type === "fs_unwatch") {
743
- stopFileWatch();
744
- return;
745
- }
746
-
747
- // --- File edit history ---
748
- if (msg.type === "fs_file_history") {
749
- var histPath = msg.path;
750
- if (!histPath) {
751
- sendTo(ws, { type: "fs_file_history_result", path: histPath, entries: [] });
752
- return;
753
- }
754
- var absHistPath = path.resolve(cwd, histPath);
755
- var entries = [];
756
-
757
- // Collect session edits
758
- sm.sessions.forEach(function (session) {
759
- var sessionLocalId = session.localId;
760
- var sessionTitle = session.title || "Untitled";
761
- var histLen = session.history.length || 1;
762
-
763
- for (var hi = 0; hi < session.history.length; hi++) {
764
- var entry = session.history[hi];
765
- if (entry.type !== "tool_executing") continue;
766
- if (entry.name !== "Edit" && entry.name !== "Write") continue;
767
- if (!entry.input || !entry.input.file_path) continue;
768
- if (entry.input.file_path !== absHistPath) continue;
769
-
770
- // Find parent assistant UUID + message snippet by scanning backwards
771
- var assistantUuid = null;
772
- var uuidIndex = -1;
773
- for (var hj = hi - 1; hj >= 0; hj--) {
774
- if (session.history[hj].type === "message_uuid" && session.history[hj].messageType === "assistant") {
775
- assistantUuid = session.history[hj].uuid;
776
- uuidIndex = hj;
777
- break;
778
- }
779
- }
780
-
781
- // Find user prompt by scanning backwards from the assistant uuid
782
- var messageSnippet = "";
783
- var searchFrom = uuidIndex >= 0 ? uuidIndex : hi;
784
- for (var hk = searchFrom - 1; hk >= 0; hk--) {
785
- if (session.history[hk].type === "user_message" && session.history[hk].text) {
786
- messageSnippet = session.history[hk].text.trim().substring(0, 100);
787
- break;
788
- }
789
- }
790
-
791
- // Collect Claude's explanation: scan backwards from tool_executing
792
- // to find the nearest delta text block (skipping tool_start).
793
- // If no delta found immediately before this tool, scan past
794
- // intervening tool blocks to find the last delta text within
795
- // the same assistant turn.
796
- var assistantSnippet = "";
797
- var deltaChunks = [];
798
- for (var hd = hi - 1; hd >= 0; hd--) {
799
- var hEntry = session.history[hd];
800
- if (hEntry.type === "tool_start") continue;
801
- if (hEntry.type === "delta" && hEntry.text) {
802
- deltaChunks.unshift(hEntry.text);
803
- } else {
804
- break;
805
- }
806
- }
807
- if (deltaChunks.length === 0) {
808
- // No delta immediately before; scan past tool blocks
809
- // to find the nearest preceding delta in the same turn
810
- for (var hd2 = hi - 1; hd2 >= 0; hd2--) {
811
- var hEntry2 = session.history[hd2];
812
- if (hEntry2.type === "tool_start" || hEntry2.type === "tool_executing" || hEntry2.type === "tool_result") continue;
813
- if (hEntry2.type === "delta" && hEntry2.text) {
814
- // Found a delta before an earlier tool in the same turn.
815
- // Collect this contiguous block of deltas.
816
- for (var hd3 = hd2; hd3 >= 0; hd3--) {
817
- var hEntry3 = session.history[hd3];
818
- if (hEntry3.type === "tool_start") continue;
819
- if (hEntry3.type === "delta" && hEntry3.text) {
820
- deltaChunks.unshift(hEntry3.text);
821
- } else {
822
- break;
823
- }
824
- }
825
- break;
826
- } else {
827
- // Hit message_uuid, user_message, etc. Stop.
828
- break;
829
- }
830
- }
831
- }
832
- assistantSnippet = deltaChunks.join("").trim().substring(0, 150);
833
-
834
- // Approximate timestamp: interpolate between session creation and last activity
835
- var tStart = session.createdAt || 0;
836
- var tEnd = session.lastActivity || tStart;
837
- var ts = tStart + Math.floor((hi / histLen) * (tEnd - tStart));
838
-
839
- var editRecord = {
840
- source: "session",
841
- timestamp: ts,
842
- sessionLocalId: sessionLocalId,
843
- sessionTitle: sessionTitle,
844
- assistantUuid: assistantUuid,
845
- toolId: entry.id,
846
- messageSnippet: messageSnippet,
847
- assistantSnippet: assistantSnippet,
848
- toolName: entry.name,
849
- };
850
-
851
- if (entry.name === "Edit") {
852
- editRecord.old_string = entry.input.old_string || "";
853
- editRecord.new_string = entry.input.new_string || "";
854
- } else {
855
- editRecord.isFullWrite = true;
856
- }
857
-
858
- entries.push(editRecord);
859
- }
860
- });
861
-
862
- // Collect git commits
863
- try {
864
- var gitLog = execFileSync(
865
- "git", ["log", "--format=%H|%at|%an|%s", "--follow", "--", histPath],
866
- { cwd: cwd, encoding: "utf8", timeout: 5000 }
867
- );
868
- var gitLines = gitLog.trim().split("\n");
869
- for (var gi = 0; gi < gitLines.length; gi++) {
870
- if (!gitLines[gi]) continue;
871
- var parts = gitLines[gi].split("|");
872
- if (parts.length < 4) continue;
873
- entries.push({
874
- source: "git",
875
- hash: parts[0],
876
- timestamp: parseInt(parts[1], 10) * 1000,
877
- author: parts[2],
878
- message: parts.slice(3).join("|"),
879
- });
880
- }
881
- } catch (e) {
882
- // Not a git repo or file not tracked, that's fine
883
- }
884
-
885
- // Sort by timestamp descending (newest first)
886
- entries.sort(function (a, b) { return b.timestamp - a.timestamp; });
887
-
888
- sendTo(ws, { type: "fs_file_history_result", path: histPath, entries: entries });
889
- return;
890
- }
891
-
892
- // --- Git diff for file history ---
893
- if (msg.type === "fs_git_diff") {
894
- var diffPath = msg.path;
895
- var hash = msg.hash;
896
- var hash2 = msg.hash2 || null;
897
- if (!diffPath || !hash) {
898
- sendTo(ws, { type: "fs_git_diff_result", hash: hash, path: diffPath, diff: "", error: "Missing params" });
899
- return;
900
- }
901
- try {
902
- var diff;
903
- if (hash2) {
904
- diff = execFileSync("git", ["diff", hash, hash2, "--", diffPath],
905
- { cwd: cwd, encoding: "utf8", timeout: 5000 });
906
- } else {
907
- diff = execFileSync("git", ["show", hash, "--format=", "--", diffPath],
908
- { cwd: cwd, encoding: "utf8", timeout: 5000 });
909
- }
910
- sendTo(ws, { type: "fs_git_diff_result", hash: hash, hash2: hash2, path: diffPath, diff: diff || "" });
911
- } catch (e) {
912
- sendTo(ws, { type: "fs_git_diff_result", hash: hash, hash2: hash2, path: diffPath, diff: "", error: e.message });
913
- }
914
- return;
915
- }
916
-
917
- // --- File content at a git commit ---
918
- if (msg.type === "fs_file_at") {
919
- var atPath = msg.path;
920
- var atHash = msg.hash;
921
- if (!atPath || !atHash) {
922
- sendTo(ws, { type: "fs_file_at_result", hash: atHash, path: atPath, content: "", error: "Missing params" });
923
- return;
924
- }
925
- try {
926
- // Convert to repo-relative path (git show requires hash:relative/path)
927
- var atAbsPath = path.resolve(cwd, atPath);
928
- var atRelPath = path.relative(cwd, atAbsPath);
929
- var content = execFileSync("git", ["show", atHash + ":" + atRelPath],
930
- { cwd: cwd, encoding: "utf8", timeout: 5000 });
931
- sendTo(ws, { type: "fs_file_at_result", hash: atHash, path: atPath, content: content });
932
- } catch (e) {
933
- sendTo(ws, { type: "fs_file_at_result", hash: atHash, path: atPath, content: "", error: e.message });
934
- }
935
- return;
936
- }
937
-
938
- // --- Web terminal ---
939
- if (msg.type === "term_create") {
940
- var t = tm.create(msg.cols || 80, msg.rows || 24);
941
- if (!t) {
942
- sendTo(ws, { type: "term_error", error: "Cannot create terminal (node-pty not available or limit reached)" });
943
- return;
944
- }
945
- tm.attach(t.id, ws);
946
- send({ type: "term_list", terminals: tm.list() });
947
- sendTo(ws, { type: "term_created", id: t.id });
948
- return;
949
- }
950
-
951
- if (msg.type === "term_attach") {
952
- if (msg.id) tm.attach(msg.id, ws);
953
- return;
954
- }
955
-
956
- if (msg.type === "term_detach") {
957
- if (msg.id) tm.detach(msg.id, ws);
958
- return;
959
- }
960
-
961
- if (msg.type === "term_input") {
962
- if (msg.id) tm.write(msg.id, msg.data);
963
- return;
964
- }
965
-
966
- if (msg.type === "term_resize") {
967
- if (msg.id && msg.cols > 0 && msg.rows > 0) {
968
- tm.resize(msg.id, msg.cols, msg.rows);
969
- }
970
- return;
971
- }
972
-
973
- if (msg.type === "term_close") {
974
- if (msg.id) {
975
- tm.close(msg.id);
976
- send({ type: "term_list", terminals: tm.list() });
977
- }
978
- return;
979
- }
980
-
981
- if (msg.type === "term_rename") {
982
- if (msg.id && msg.title) {
983
- tm.rename(msg.id, msg.title);
984
- send({ type: "term_list", terminals: tm.list() });
985
- }
986
- return;
987
- }
988
-
989
- if (msg.type !== "message") return;
990
- if (!msg.text && (!msg.images || msg.images.length === 0) && (!msg.pastes || msg.pastes.length === 0)) return;
991
-
992
- var session = sm.getActiveSession();
993
- if (!session) return;
994
-
995
- var userMsg = { type: "user_message", text: msg.text || "" };
996
- if (msg.images && msg.images.length > 0) {
997
- userMsg.imageCount = msg.images.length;
998
- }
999
- if (msg.pastes && msg.pastes.length > 0) {
1000
- userMsg.pastes = msg.pastes;
1001
- }
1002
- session.history.push(userMsg);
1003
- sm.appendToSessionFile(session, userMsg);
1004
- sendToOthers(ws, userMsg);
1005
-
1006
- if (!session.title) {
1007
- session.title = (msg.text || "Image").substring(0, 50);
1008
- sm.saveSessionFile(session);
1009
- sm.broadcastSessionList();
1010
- }
1011
-
1012
- var fullText = msg.text || "";
1013
- if (msg.pastes && msg.pastes.length > 0) {
1014
- for (var pi = 0; pi < msg.pastes.length; pi++) {
1015
- if (fullText) fullText += "\n\n";
1016
- fullText += msg.pastes[pi];
1017
- }
1018
- }
1019
-
1020
- if (!session.isProcessing) {
1021
- session.isProcessing = true;
1022
- session.sentToolResults = {};
1023
- send({ type: "status", status: "processing" });
1024
- if (!session.queryInstance) {
1025
- sdk.startQuery(session, fullText, msg.images);
1026
- } else {
1027
- sdk.pushMessage(session, fullText, msg.images);
1028
- }
1029
- } else {
1030
- sdk.pushMessage(session, fullText, msg.images);
1031
- }
1032
- sm.broadcastSessionList();
1033
- }
1034
-
1035
- // --- WS disconnection handler ---
1036
- function handleDisconnection(ws) {
1037
- tm.detachAll(ws);
1038
- clients.delete(ws);
1039
- if (clients.size === 0) {
1040
- stopFileWatch();
1041
- stopAllDirWatches();
1042
- }
1043
- broadcastClientCount();
1044
- }
1045
-
1046
- // --- Handle project-scoped HTTP requests ---
1047
- function handleHTTP(req, res, urlPath) {
1048
- // Push subscribe
1049
- if (req.method === "POST" && urlPath === "/api/push-subscribe") {
1050
- parseJsonBody(req).then(function (body) {
1051
- var sub = body.subscription || body;
1052
- if (pushModule) pushModule.addSubscription(sub, body.replaceEndpoint);
1053
- res.writeHead(200, { "Content-Type": "application/json" });
1054
- res.end('{"ok":true}');
1055
- }).catch(function () {
1056
- res.writeHead(400);
1057
- res.end("Bad request");
1058
- });
1059
- return true;
1060
- }
1061
-
1062
- // Permission response from push notification
1063
- if (req.method === "POST" && urlPath === "/api/permission-response") {
1064
- parseJsonBody(req).then(function (data) {
1065
- var requestId = data.requestId;
1066
- var decision = data.decision;
1067
- if (!requestId || !decision) {
1068
- res.writeHead(400, { "Content-Type": "application/json" });
1069
- res.end('{"error":"missing requestId or decision"}');
1070
- return;
1071
- }
1072
- var found = false;
1073
- sm.sessions.forEach(function (session) {
1074
- var pending = session.pendingPermissions[requestId];
1075
- if (!pending) return;
1076
- found = true;
1077
- delete session.pendingPermissions[requestId];
1078
- if (decision === "allow") {
1079
- pending.resolve({ behavior: "allow", updatedInput: pending.toolInput });
1080
- } else {
1081
- pending.resolve({ behavior: "deny", message: "Denied via push notification" });
1082
- }
1083
- sm.sendAndRecord(session, {
1084
- type: "permission_resolved",
1085
- requestId: requestId,
1086
- decision: decision,
1087
- });
1088
- });
1089
- if (found) {
1090
- res.writeHead(200, { "Content-Type": "application/json" });
1091
- res.end('{"ok":true}');
1092
- } else {
1093
- res.writeHead(404, { "Content-Type": "application/json" });
1094
- res.end('{"error":"permission request not found"}');
1095
- }
1096
- }).catch(function () {
1097
- res.writeHead(400);
1098
- res.end("Bad request");
1099
- });
1100
- return true;
1101
- }
1102
-
1103
- // VAPID public key
1104
- if (req.method === "GET" && urlPath === "/api/vapid-public-key") {
1105
- if (pushModule) {
1106
- res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-cache, no-store" });
1107
- res.end(JSON.stringify({ publicKey: pushModule.publicKey }));
1108
- } else {
1109
- res.writeHead(404, { "Content-Type": "application/json" });
1110
- res.end('{"error":"push not available"}');
1111
- }
1112
- return true;
1113
- }
1114
-
1115
- // File browser: serve project images
1116
- if (req.method === "GET" && urlPath.startsWith("/api/file?")) {
1117
- var qIdx = urlPath.indexOf("?");
1118
- var params = new URLSearchParams(urlPath.substring(qIdx));
1119
- var reqFilePath = params.get("path");
1120
- if (!reqFilePath) { res.writeHead(400); res.end("Missing path"); return true; }
1121
- var absFile = safePath(cwd, reqFilePath);
1122
- if (!absFile) { res.writeHead(403); res.end("Access denied"); return true; }
1123
- var fileExt = path.extname(absFile).toLowerCase();
1124
- if (!IMAGE_EXTS.has(fileExt)) { res.writeHead(403); res.end("Only image files"); return true; }
1125
- try {
1126
- var fileContent = fs.readFileSync(absFile);
1127
- var fileMime = MIME_TYPES[fileExt] || "application/octet-stream";
1128
- res.writeHead(200, { "Content-Type": fileMime, "Cache-Control": "no-cache" });
1129
- res.end(fileContent);
1130
- } catch (e) {
1131
- res.writeHead(404); res.end("Not found");
1132
- }
1133
- return true;
1134
- }
1135
-
1136
- // Info endpoint
1137
- if (req.method === "GET" && urlPath === "/info") {
1138
- res.writeHead(200, {
1139
- "Content-Type": "application/json",
1140
- "Access-Control-Allow-Origin": "*",
1141
- });
1142
- res.end(JSON.stringify({ cwd: cwd, project: project, slug: slug }));
1143
- return true;
1144
- }
1145
-
1146
- return false; // not handled
1147
- }
1148
-
1149
- // --- Destroy ---
1150
- function destroy() {
1151
- stopFileWatch();
1152
- stopAllDirWatches();
1153
- // Abort all active sessions
1154
- sm.sessions.forEach(function (session) {
1155
- if (session.abortController) {
1156
- try { session.abortController.abort(); } catch (e) {}
1157
- }
1158
- if (session.messageQueue) {
1159
- try { session.messageQueue.end(); } catch (e) {}
1160
- }
1161
- });
1162
- // Kill all terminals
1163
- tm.destroyAll();
1164
- for (var ws of clients) {
1165
- try { ws.close(); } catch (e) {}
1166
- }
1167
- clients.clear();
1168
- }
1169
-
1170
- // --- Status info ---
1171
- function getStatus() {
1172
- var sessionCount = sm.sessions.size;
1173
- var hasProcessing = false;
1174
- sm.sessions.forEach(function (s) {
1175
- if (s.isProcessing) hasProcessing = true;
1176
- });
1177
- return {
1178
- slug: slug,
1179
- path: cwd,
1180
- project: project,
1181
- title: title,
1182
- clients: clients.size,
1183
- sessions: sessionCount,
1184
- isProcessing: hasProcessing,
1185
- };
1186
- }
1187
-
1188
- function setTitle(newTitle) {
1189
- title = newTitle || null;
1190
- send({ type: "info", cwd: cwd, slug: slug, project: title || project, version: currentVersion, debug: !!debug, lanHost: lanHost, projectCount: getProjectCount(), projects: getProjectList() });
1191
- }
1192
-
1193
- return {
1194
- cwd: cwd,
1195
- slug: slug,
1196
- project: project,
1197
- clients: clients,
1198
- sm: sm,
1199
- sdk: sdk,
1200
- send: send,
1201
- sendTo: sendTo,
1202
- handleConnection: handleConnection,
1203
- handleMessage: handleMessage,
1204
- handleDisconnection: handleDisconnection,
1205
- handleHTTP: handleHTTP,
1206
- getStatus: getStatus,
1207
- setTitle: setTitle,
1208
- warmup: function () { sdk.warmup(); },
1209
- destroy: destroy,
1210
- };
1211
- }
1212
-
1213
- function parseJsonBody(req) {
1214
- return new Promise(function (resolve, reject) {
1215
- var body = "";
1216
- req.on("data", function (chunk) { body += chunk; });
1217
- req.on("end", function () {
1218
- try { resolve(JSON.parse(body)); }
1219
- catch (e) { reject(e); }
1220
- });
1221
- });
1222
- }
1223
-
1224
- module.exports = { createProjectContext: createProjectContext };