clay-server 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 (87) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +281 -0
  3. package/bin/cli.js +2385 -0
  4. package/lib/cli-sessions.js +270 -0
  5. package/lib/config.js +237 -0
  6. package/lib/daemon.js +489 -0
  7. package/lib/ipc.js +112 -0
  8. package/lib/notes.js +120 -0
  9. package/lib/pages.js +664 -0
  10. package/lib/project.js +1433 -0
  11. package/lib/public/app.js +2795 -0
  12. package/lib/public/apple-touch-icon-dark.png +0 -0
  13. package/lib/public/apple-touch-icon.png +0 -0
  14. package/lib/public/css/base.css +264 -0
  15. package/lib/public/css/diff.css +128 -0
  16. package/lib/public/css/filebrowser.css +1114 -0
  17. package/lib/public/css/highlight.css +144 -0
  18. package/lib/public/css/icon-strip.css +296 -0
  19. package/lib/public/css/input.css +573 -0
  20. package/lib/public/css/menus.css +856 -0
  21. package/lib/public/css/messages.css +1445 -0
  22. package/lib/public/css/mobile-nav.css +354 -0
  23. package/lib/public/css/overlays.css +697 -0
  24. package/lib/public/css/rewind.css +505 -0
  25. package/lib/public/css/server-settings.css +761 -0
  26. package/lib/public/css/sidebar.css +936 -0
  27. package/lib/public/css/sticky-notes.css +358 -0
  28. package/lib/public/css/title-bar.css +314 -0
  29. package/lib/public/favicon-dark.svg +1 -0
  30. package/lib/public/favicon.svg +1 -0
  31. package/lib/public/icon-192-dark.png +0 -0
  32. package/lib/public/icon-192.png +0 -0
  33. package/lib/public/icon-512-dark.png +0 -0
  34. package/lib/public/icon-512.png +0 -0
  35. package/lib/public/icon-mono.svg +1 -0
  36. package/lib/public/index.html +762 -0
  37. package/lib/public/manifest.json +27 -0
  38. package/lib/public/modules/diff.js +398 -0
  39. package/lib/public/modules/events.js +21 -0
  40. package/lib/public/modules/filebrowser.js +1411 -0
  41. package/lib/public/modules/fileicons.js +172 -0
  42. package/lib/public/modules/icons.js +54 -0
  43. package/lib/public/modules/input.js +584 -0
  44. package/lib/public/modules/markdown.js +356 -0
  45. package/lib/public/modules/notifications.js +649 -0
  46. package/lib/public/modules/qrcode.js +70 -0
  47. package/lib/public/modules/rewind.js +345 -0
  48. package/lib/public/modules/server-settings.js +510 -0
  49. package/lib/public/modules/sidebar.js +1083 -0
  50. package/lib/public/modules/state.js +3 -0
  51. package/lib/public/modules/sticky-notes.js +688 -0
  52. package/lib/public/modules/terminal.js +697 -0
  53. package/lib/public/modules/theme.js +738 -0
  54. package/lib/public/modules/tools.js +1608 -0
  55. package/lib/public/modules/utils.js +56 -0
  56. package/lib/public/style.css +15 -0
  57. package/lib/public/sw.js +75 -0
  58. package/lib/push.js +124 -0
  59. package/lib/sdk-bridge.js +989 -0
  60. package/lib/server.js +582 -0
  61. package/lib/sessions.js +424 -0
  62. package/lib/terminal-manager.js +187 -0
  63. package/lib/terminal.js +24 -0
  64. package/lib/themes/ayu-light.json +9 -0
  65. package/lib/themes/catppuccin-latte.json +9 -0
  66. package/lib/themes/catppuccin-mocha.json +9 -0
  67. package/lib/themes/clay-light.json +10 -0
  68. package/lib/themes/clay.json +10 -0
  69. package/lib/themes/dracula.json +9 -0
  70. package/lib/themes/everforest-light.json +9 -0
  71. package/lib/themes/everforest.json +9 -0
  72. package/lib/themes/github-light.json +9 -0
  73. package/lib/themes/gruvbox-dark.json +9 -0
  74. package/lib/themes/gruvbox-light.json +9 -0
  75. package/lib/themes/monokai.json +9 -0
  76. package/lib/themes/nord-light.json +9 -0
  77. package/lib/themes/nord.json +9 -0
  78. package/lib/themes/one-dark.json +9 -0
  79. package/lib/themes/one-light.json +9 -0
  80. package/lib/themes/rose-pine-dawn.json +9 -0
  81. package/lib/themes/rose-pine.json +9 -0
  82. package/lib/themes/solarized-dark.json +9 -0
  83. package/lib/themes/solarized-light.json +9 -0
  84. package/lib/themes/tokyo-night-light.json +9 -0
  85. package/lib/themes/tokyo-night.json +9 -0
  86. package/lib/updater.js +97 -0
  87. package/package.json +47 -0
@@ -0,0 +1,424 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const config = require("./config");
4
+
5
+ function createSessionManager(opts) {
6
+ var cwd = opts.cwd;
7
+ var send = opts.send; // function(obj) - broadcast to all clients
8
+ var sendAndRecord = null; // set after init via setSendAndRecord
9
+
10
+ // --- Multi-session state ---
11
+ var nextLocalId = 1;
12
+ var sessions = new Map(); // localId -> session object
13
+ var activeSessionId = null; // currently active local ID
14
+ var slashCommands = null; // shared across sessions
15
+ var skillNames = null; // Claude-only skills to filter from slash menu
16
+
17
+ // --- Session persistence (centralized in ~/.clay/sessions/{encoded-cwd}/) ---
18
+ var encodedCwd = cwd.replace(/\//g, "-");
19
+ var sessionsDir = path.join(config.CONFIG_DIR, "sessions", encodedCwd);
20
+ fs.mkdirSync(sessionsDir, { recursive: true });
21
+
22
+ // Auto-migrate sessions from legacy locations:
23
+ // v1: {cwd}/.claude-relay/sessions/
24
+ // v2: ~/.claude-relay/sessions/{encoded-cwd}/ (if config.js rename didn't cover it)
25
+ var legacySessionDirs = [
26
+ path.join(cwd, ".claude-relay", "sessions"),
27
+ path.join(require("os").homedir(), ".claude-relay", "sessions", encodedCwd),
28
+ ];
29
+ for (var li = 0; li < legacySessionDirs.length; li++) {
30
+ var oldSessionsDir = legacySessionDirs[li];
31
+ try {
32
+ var oldFiles = fs.readdirSync(oldSessionsDir);
33
+ var migrated = 0;
34
+ for (var mi = 0; mi < oldFiles.length; mi++) {
35
+ if (!oldFiles[mi].endsWith(".jsonl")) continue;
36
+ var oldFilePath = path.join(oldSessionsDir, oldFiles[mi]);
37
+ var newFilePath = path.join(sessionsDir, oldFiles[mi]);
38
+ if (fs.existsSync(newFilePath)) continue;
39
+ try {
40
+ fs.renameSync(oldFilePath, newFilePath);
41
+ migrated++;
42
+ } catch (renameErr) {
43
+ try {
44
+ fs.copyFileSync(oldFilePath, newFilePath);
45
+ fs.unlinkSync(oldFilePath);
46
+ migrated++;
47
+ } catch (copyErr) {}
48
+ }
49
+ }
50
+ if (migrated > 0) {
51
+ console.log("[sessions] Migrated " + migrated + " session(s) from " + oldSessionsDir);
52
+ }
53
+ // Clean up old directory if empty
54
+ try {
55
+ if (fs.readdirSync(oldSessionsDir).length === 0) {
56
+ fs.rmdirSync(oldSessionsDir);
57
+ var parentDir = path.dirname(oldSessionsDir);
58
+ if (fs.readdirSync(parentDir).length === 0) fs.rmdirSync(parentDir);
59
+ }
60
+ } catch (e) {}
61
+ } catch (e) {
62
+ // Old directory doesn't exist — that's fine
63
+ }
64
+ }
65
+
66
+ function sessionFilePath(cliSessionId) {
67
+ return path.join(sessionsDir, cliSessionId + ".jsonl");
68
+ }
69
+
70
+ function saveSessionFile(session) {
71
+ if (!session.cliSessionId) return;
72
+ session.lastActivity = Date.now();
73
+ try {
74
+ var metaObj = {
75
+ type: "meta",
76
+ localId: session.localId,
77
+ cliSessionId: session.cliSessionId,
78
+ title: session.title,
79
+ createdAt: session.createdAt,
80
+ };
81
+ if (session.lastRewindUuid) metaObj.lastRewindUuid = session.lastRewindUuid;
82
+ var meta = JSON.stringify(metaObj);
83
+ var lines = [meta];
84
+ for (var i = 0; i < session.history.length; i++) {
85
+ lines.push(JSON.stringify(session.history[i]));
86
+ }
87
+ fs.writeFileSync(sessionFilePath(session.cliSessionId), lines.join("\n") + "\n");
88
+ } catch(e) {
89
+ console.error("[session] Failed to save session file:", e.message);
90
+ }
91
+ }
92
+
93
+ function appendToSessionFile(session, obj) {
94
+ if (!session.cliSessionId) return;
95
+ session.lastActivity = Date.now();
96
+ try {
97
+ fs.appendFileSync(sessionFilePath(session.cliSessionId), JSON.stringify(obj) + "\n");
98
+ } catch(e) {
99
+ console.error("[session] Failed to append to session file:", e.message);
100
+ }
101
+ }
102
+
103
+ function loadSessions() {
104
+ var files;
105
+ try { files = fs.readdirSync(sessionsDir); } catch { return; }
106
+
107
+ var loaded = [];
108
+ for (var i = 0; i < files.length; i++) {
109
+ if (!files[i].endsWith(".jsonl")) continue;
110
+ var content;
111
+ try { content = fs.readFileSync(path.join(sessionsDir, files[i]), "utf8"); } catch { continue; }
112
+ var lines = content.trim().split("\n");
113
+ if (lines.length === 0) continue;
114
+
115
+ var meta;
116
+ try { meta = JSON.parse(lines[0]); } catch { continue; }
117
+ if (meta.type !== "meta" || !meta.cliSessionId) continue;
118
+
119
+ var history = [];
120
+ for (var j = 1; j < lines.length; j++) {
121
+ try { history.push(JSON.parse(lines[j])); } catch {}
122
+ }
123
+
124
+ var fileMtime = 0;
125
+ try { fileMtime = fs.statSync(path.join(sessionsDir, files[i])).mtimeMs; } catch {}
126
+ loaded.push({ meta: meta, history: history, mtime: fileMtime });
127
+ }
128
+
129
+ loaded.sort(function(a, b) { return a.meta.createdAt - b.meta.createdAt; });
130
+
131
+ for (var i = 0; i < loaded.length; i++) {
132
+ var m = loaded[i].meta;
133
+ var localId = nextLocalId++;
134
+ // Reconstruct messageUUIDs from history
135
+ var messageUUIDs = [];
136
+ for (var k = 0; k < loaded[i].history.length; k++) {
137
+ if (loaded[i].history[k].type === "message_uuid") {
138
+ messageUUIDs.push({ uuid: loaded[i].history[k].uuid, type: loaded[i].history[k].messageType, historyIndex: k });
139
+ }
140
+ }
141
+ var session = {
142
+ localId: localId,
143
+ queryInstance: null,
144
+ messageQueue: null,
145
+ cliSessionId: m.cliSessionId,
146
+ blocks: {},
147
+ sentToolResults: {},
148
+ pendingPermissions: {},
149
+ pendingAskUser: {},
150
+ isProcessing: false,
151
+ title: m.title || "",
152
+ createdAt: m.createdAt || Date.now(),
153
+ lastActivity: loaded[i].mtime || m.createdAt || Date.now(),
154
+ history: loaded[i].history,
155
+ messageUUIDs: messageUUIDs,
156
+ lastRewindUuid: m.lastRewindUuid || null,
157
+ };
158
+ sessions.set(localId, session);
159
+ }
160
+ }
161
+
162
+ // Load persisted sessions from disk
163
+ loadSessions();
164
+
165
+ function getActiveSession() {
166
+ return sessions.get(activeSessionId) || null;
167
+ }
168
+
169
+ function broadcastSessionList() {
170
+ send({
171
+ type: "session_list",
172
+ sessions: [...sessions.values()].map(function(s) {
173
+ return {
174
+ id: s.localId,
175
+ cliSessionId: s.cliSessionId || null,
176
+ title: s.title || "New Session",
177
+ active: s.localId === activeSessionId,
178
+ isProcessing: s.isProcessing,
179
+ lastActivity: s.lastActivity || s.createdAt || 0,
180
+ };
181
+ }),
182
+ });
183
+ }
184
+
185
+ function createSession() {
186
+ var localId = nextLocalId++;
187
+ var session = {
188
+ localId: localId,
189
+ queryInstance: null,
190
+ messageQueue: null,
191
+ cliSessionId: null,
192
+ blocks: {},
193
+ sentToolResults: {},
194
+ pendingPermissions: {},
195
+ pendingAskUser: {},
196
+ allowedTools: {},
197
+ isProcessing: false,
198
+ title: "",
199
+ createdAt: Date.now(),
200
+ lastActivity: Date.now(),
201
+ history: [],
202
+ messageUUIDs: [],
203
+ };
204
+ sessions.set(localId, session);
205
+ switchSession(localId);
206
+ return session;
207
+ }
208
+
209
+ var HISTORY_PAGE_SIZE = 200;
210
+
211
+ function findTurnBoundary(history, targetIndex) {
212
+ for (var i = targetIndex; i >= 0; i--) {
213
+ if (history[i].type === "user_message") return i;
214
+ }
215
+ return 0;
216
+ }
217
+
218
+ function replayHistory(session, fromIndex) {
219
+ var total = session.history.length;
220
+ if (typeof fromIndex !== "number") {
221
+ if (total <= HISTORY_PAGE_SIZE) {
222
+ fromIndex = 0;
223
+ } else {
224
+ fromIndex = findTurnBoundary(session.history, Math.max(0, total - HISTORY_PAGE_SIZE));
225
+ }
226
+ }
227
+
228
+ send({ type: "history_meta", total: total, from: fromIndex });
229
+
230
+ for (var i = fromIndex; i < total; i++) {
231
+ send(session.history[i]);
232
+ }
233
+
234
+ // Find the last result message in the full history for accurate context data
235
+ var lastUsage = null;
236
+ var lastModelUsage = null;
237
+ var lastCost = null;
238
+ for (var j = total - 1; j >= 0; j--) {
239
+ if (session.history[j].type === "result") {
240
+ var r = session.history[j];
241
+ lastUsage = r.usage || null;
242
+ lastModelUsage = r.modelUsage || null;
243
+ lastCost = r.cost != null ? r.cost : null;
244
+ break;
245
+ }
246
+ }
247
+
248
+ send({ type: "history_done", lastUsage: lastUsage, lastModelUsage: lastModelUsage, lastCost: lastCost });
249
+ }
250
+
251
+ function switchSession(localId) {
252
+ var session = sessions.get(localId);
253
+ if (!session) return;
254
+
255
+ activeSessionId = localId;
256
+ send({ type: "session_switched", id: localId, cliSessionId: session.cliSessionId || null });
257
+ broadcastSessionList();
258
+ replayHistory(session);
259
+
260
+ if (session.isProcessing) {
261
+ send({ type: "status", status: "processing" });
262
+ }
263
+
264
+ // Re-send any pending permission requests
265
+ var pendingIds = Object.keys(session.pendingPermissions);
266
+ for (var i = 0; i < pendingIds.length; i++) {
267
+ var p = session.pendingPermissions[pendingIds[i]];
268
+ send({
269
+ type: "permission_request_pending",
270
+ requestId: p.requestId,
271
+ toolName: p.toolName,
272
+ toolInput: p.toolInput,
273
+ toolUseId: p.toolUseId,
274
+ decisionReason: p.decisionReason,
275
+ });
276
+ }
277
+ }
278
+
279
+ function deleteSession(localId) {
280
+ var session = sessions.get(localId);
281
+ if (!session) return;
282
+
283
+ if (session.abortController) {
284
+ try { session.abortController.abort(); } catch(e) {}
285
+ }
286
+ if (session.messageQueue) {
287
+ try { session.messageQueue.end(); } catch(e) {}
288
+ }
289
+
290
+ if (session.cliSessionId) {
291
+ try { fs.unlinkSync(sessionFilePath(session.cliSessionId)); } catch(e) {}
292
+ }
293
+
294
+ sessions.delete(localId);
295
+
296
+ if (activeSessionId === localId) {
297
+ var remaining = [...sessions.keys()];
298
+ if (remaining.length > 0) {
299
+ switchSession(remaining[remaining.length - 1]);
300
+ } else {
301
+ createSession();
302
+ }
303
+ } else {
304
+ broadcastSessionList();
305
+ }
306
+ }
307
+
308
+ function doSendAndRecord(session, obj) {
309
+ session.history.push(obj);
310
+ appendToSessionFile(session, obj);
311
+ if (session.localId === activeSessionId) {
312
+ send(obj);
313
+ }
314
+ }
315
+
316
+ function resumeSession(cliSessionId, opts) {
317
+ // If a session with this cliSessionId already exists, just switch to it
318
+ var existing = null;
319
+ sessions.forEach(function (s) {
320
+ if (s.cliSessionId === cliSessionId) existing = s;
321
+ });
322
+ if (existing) {
323
+ existing.lastActivity = Date.now();
324
+ switchSession(existing.localId);
325
+ return existing;
326
+ }
327
+
328
+ var cliHistory = (opts && opts.history) || [];
329
+ var title = (opts && opts.title) || "Resumed session";
330
+ var localId = nextLocalId++;
331
+ var session = {
332
+ localId: localId,
333
+ queryInstance: null,
334
+ messageQueue: null,
335
+ cliSessionId: cliSessionId,
336
+ blocks: {},
337
+ sentToolResults: {},
338
+ pendingPermissions: {},
339
+ pendingAskUser: {},
340
+ allowedTools: {},
341
+ isProcessing: false,
342
+ title: title,
343
+ createdAt: Date.now(),
344
+ history: cliHistory,
345
+ messageUUIDs: [],
346
+ };
347
+ sessions.set(localId, session);
348
+ saveSessionFile(session);
349
+ switchSession(localId);
350
+ return session;
351
+ }
352
+
353
+ // --- Spawn initial session only if no persisted sessions ---
354
+ if (sessions.size === 0) {
355
+ createSession();
356
+ } else {
357
+ // Activate the most recently used session
358
+ var allSessions = [...sessions.values()];
359
+ var mostRecent = allSessions[0];
360
+ for (var i = 1; i < allSessions.length; i++) {
361
+ if ((allSessions[i].lastActivity || 0) > (mostRecent.lastActivity || 0)) {
362
+ mostRecent = allSessions[i];
363
+ }
364
+ }
365
+ activeSessionId = mostRecent.localId;
366
+ }
367
+
368
+ function searchSessions(query) {
369
+ if (!query) return [];
370
+ var q = query.toLowerCase();
371
+ var results = [];
372
+ sessions.forEach(function (session) {
373
+ var titleMatch = (session.title || "New Session").toLowerCase().indexOf(q) !== -1;
374
+ var contentMatch = false;
375
+ for (var i = 0; i < session.history.length; i++) {
376
+ var entry = session.history[i];
377
+ if ((entry.type === "delta" || entry.type === "user_message") && entry.text) {
378
+ if (entry.text.toLowerCase().indexOf(q) !== -1) {
379
+ contentMatch = true;
380
+ break;
381
+ }
382
+ }
383
+ }
384
+ if (titleMatch || contentMatch) {
385
+ results.push({
386
+ id: session.localId,
387
+ cliSessionId: session.cliSessionId || null,
388
+ title: session.title || "New Session",
389
+ active: session.localId === activeSessionId,
390
+ isProcessing: session.isProcessing,
391
+ lastActivity: session.lastActivity || session.createdAt || 0,
392
+ matchType: titleMatch && contentMatch ? "both" : titleMatch ? "title" : "content",
393
+ });
394
+ }
395
+ });
396
+ return results;
397
+ }
398
+
399
+ return {
400
+ get activeSessionId() { return activeSessionId; },
401
+ get nextLocalId() { return nextLocalId; },
402
+ get slashCommands() { return slashCommands; },
403
+ set slashCommands(v) { slashCommands = v; },
404
+ get skillNames() { return skillNames; },
405
+ set skillNames(v) { skillNames = v; },
406
+ sessions: sessions,
407
+ sessionsDir: sessionsDir,
408
+ HISTORY_PAGE_SIZE: HISTORY_PAGE_SIZE,
409
+ getActiveSession: getActiveSession,
410
+ createSession: createSession,
411
+ switchSession: switchSession,
412
+ deleteSession: deleteSession,
413
+ resumeSession: resumeSession,
414
+ broadcastSessionList: broadcastSessionList,
415
+ saveSessionFile: saveSessionFile,
416
+ appendToSessionFile: appendToSessionFile,
417
+ sendAndRecord: doSendAndRecord,
418
+ findTurnBoundary: findTurnBoundary,
419
+ replayHistory: replayHistory,
420
+ searchSessions: searchSessions,
421
+ };
422
+ }
423
+
424
+ module.exports = { createSessionManager };
@@ -0,0 +1,187 @@
1
+ var { createTerminal } = require("./terminal");
2
+
3
+ var MAX_TERMINALS = 10;
4
+ var SCROLLBACK_MAX = 50 * 1024; // 50 KB per terminal
5
+
6
+ /**
7
+ * Create a terminal manager for a project.
8
+ * Manages persistent PTY sessions with scrollback buffering.
9
+ * opts: { cwd, send, sendTo }
10
+ */
11
+ function createTerminalManager(opts) {
12
+ var cwd = opts.cwd;
13
+ var send = opts.send;
14
+ var sendTo = opts.sendTo;
15
+
16
+ var nextId = 1;
17
+ var terminals = new Map(); // id -> terminal session
18
+
19
+ function create(cols, rows) {
20
+ if (terminals.size >= MAX_TERMINALS) return null;
21
+
22
+ var pty = createTerminal(cwd, cols, rows);
23
+ if (!pty) return null;
24
+
25
+ var id = nextId++;
26
+ var session = {
27
+ id: id,
28
+ pty: pty,
29
+ scrollback: [],
30
+ scrollbackSize: 0,
31
+ cols: cols || 80,
32
+ rows: rows || 24,
33
+ title: "Terminal " + id,
34
+ exited: false,
35
+ exitCode: null,
36
+ subscribers: new Set(),
37
+ };
38
+
39
+ pty.onData(function (data) {
40
+ // Buffer scrollback
41
+ session.scrollback.push(data);
42
+ session.scrollbackSize += data.length;
43
+ while (session.scrollbackSize > SCROLLBACK_MAX && session.scrollback.length > 1) {
44
+ session.scrollbackSize -= session.scrollback[0].length;
45
+ session.scrollback.shift();
46
+ }
47
+
48
+ // Broadcast to subscribers
49
+ var msg = JSON.stringify({ type: "term_output", id: id, data: data });
50
+ for (var ws of session.subscribers) {
51
+ if (ws.readyState === 1) ws.send(msg);
52
+ }
53
+ });
54
+
55
+ pty.onExit(function (e) {
56
+ session.exited = true;
57
+ session.exitCode = e && e.exitCode != null ? e.exitCode : null;
58
+ session.pty = null;
59
+
60
+ var msg = JSON.stringify({ type: "term_exited", id: id });
61
+ for (var ws of session.subscribers) {
62
+ if (ws.readyState === 1) ws.send(msg);
63
+ }
64
+
65
+ // Broadcast updated list
66
+ send({ type: "term_list", terminals: list() });
67
+ });
68
+
69
+ terminals.set(id, session);
70
+ return session;
71
+ }
72
+
73
+ function attach(id, ws) {
74
+ var session = terminals.get(id);
75
+ if (!session) return false;
76
+
77
+ session.subscribers.add(ws);
78
+
79
+ // Replay scrollback
80
+ if (session.scrollback.length > 0) {
81
+ var replay = session.scrollback.join("");
82
+ sendTo(ws, { type: "term_output", id: id, data: replay });
83
+ }
84
+
85
+ // If already exited, notify
86
+ if (session.exited) {
87
+ sendTo(ws, { type: "term_exited", id: id });
88
+ }
89
+
90
+ return true;
91
+ }
92
+
93
+ function detach(id, ws) {
94
+ var session = terminals.get(id);
95
+ if (!session) return;
96
+ session.subscribers.delete(ws);
97
+ }
98
+
99
+ function detachAll(ws) {
100
+ for (var session of terminals.values()) {
101
+ session.subscribers.delete(ws);
102
+ }
103
+ }
104
+
105
+ function write(id, data) {
106
+ var session = terminals.get(id);
107
+ if (session && session.pty) {
108
+ session.pty.write(data);
109
+ }
110
+ }
111
+
112
+ function resize(id, cols, rows) {
113
+ var session = terminals.get(id);
114
+ if (!session || !session.pty) return;
115
+ if (cols > 0 && rows > 0) {
116
+ try {
117
+ session.pty.resize(cols, rows);
118
+ session.cols = cols;
119
+ session.rows = rows;
120
+ } catch (e) {}
121
+ }
122
+ }
123
+
124
+ function close(id) {
125
+ var session = terminals.get(id);
126
+ if (!session) return;
127
+
128
+ if (session.pty) {
129
+ try { session.pty.kill(); } catch (e) {}
130
+ session.pty = null;
131
+ }
132
+
133
+ // Notify subscribers
134
+ var msg = JSON.stringify({ type: "term_closed", id: id });
135
+ for (var ws of session.subscribers) {
136
+ if (ws.readyState === 1) ws.send(msg);
137
+ }
138
+
139
+ terminals.delete(id);
140
+
141
+ // Reset counter when all terminals are closed
142
+ if (terminals.size === 0) nextId = 1;
143
+ }
144
+
145
+ function rename(id, title) {
146
+ var session = terminals.get(id);
147
+ if (!session) return;
148
+ session.title = String(title).substring(0, 50);
149
+ }
150
+
151
+ function list() {
152
+ var result = [];
153
+ for (var session of terminals.values()) {
154
+ result.push({
155
+ id: session.id,
156
+ title: session.title,
157
+ exited: session.exited,
158
+ });
159
+ }
160
+ return result;
161
+ }
162
+
163
+ function destroyAll() {
164
+ for (var session of terminals.values()) {
165
+ if (session.pty) {
166
+ try { session.pty.kill(); } catch (e) {}
167
+ session.pty = null;
168
+ }
169
+ }
170
+ terminals.clear();
171
+ }
172
+
173
+ return {
174
+ create: create,
175
+ attach: attach,
176
+ detach: detach,
177
+ detachAll: detachAll,
178
+ write: write,
179
+ resize: resize,
180
+ close: close,
181
+ rename: rename,
182
+ list: list,
183
+ destroyAll: destroyAll,
184
+ };
185
+ }
186
+
187
+ module.exports = { createTerminalManager: createTerminalManager };
@@ -0,0 +1,24 @@
1
+ var pty;
2
+ try {
3
+ pty = require("@lydell/node-pty");
4
+ } catch (e) {
5
+ pty = null;
6
+ }
7
+
8
+ function createTerminal(cwd, cols, rows) {
9
+ if (!pty) return null;
10
+
11
+ var shell = process.env.SHELL
12
+ || (process.platform === "win32" ? process.env.COMSPEC || "cmd.exe" : "/bin/bash");
13
+ var term = pty.spawn(shell, [], {
14
+ name: "xterm-256color",
15
+ cols: cols || 80,
16
+ rows: rows || 24,
17
+ cwd: cwd,
18
+ env: Object.assign({}, process.env, { TERM: "xterm-256color" }),
19
+ });
20
+
21
+ return term;
22
+ }
23
+
24
+ module.exports = { createTerminal: createTerminal };
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "Ayu Light",
3
+ "author": "Ike Ku",
4
+ "variant": "light",
5
+ "base00": "FAFAFA", "base01": "EDEFF1", "base02": "D2D4D8", "base03": "A0A6AC",
6
+ "base04": "8A9199", "base05": "5C6166", "base06": "4E5257", "base07": "404447",
7
+ "base08": "F07171", "base09": "FA8D3E", "base0A": "F2AE49", "base0B": "6CBF49",
8
+ "base0C": "4CBF99", "base0D": "399EE6", "base0E": "A37ACC", "base0F": "E6BA7E"
9
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "Catppuccin Latte",
3
+ "author": "catppuccin",
4
+ "variant": "light",
5
+ "base00": "eff1f5", "base01": "e6e9ef", "base02": "ccd0da", "base03": "9ca0b0",
6
+ "base04": "8c8fa1", "base05": "5c5f77", "base06": "4c4f69", "base07": "303446",
7
+ "base08": "d20f39", "base09": "fe640b", "base0A": "df8e1d", "base0B": "40a02b",
8
+ "base0C": "179299", "base0D": "1e66f5", "base0E": "8839ef", "base0F": "dd7878"
9
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "Catppuccin Mocha",
3
+ "author": "catppuccin",
4
+ "variant": "dark",
5
+ "base00": "1e1e2e", "base01": "181825", "base02": "313244", "base03": "45475a",
6
+ "base04": "585b70", "base05": "cdd6f4", "base06": "f5e0dc", "base07": "b4befe",
7
+ "base08": "f38ba8", "base09": "fab387", "base0A": "f9e2af", "base0B": "a6e3a1",
8
+ "base0C": "94e2d5", "base0D": "89b4fa", "base0E": "cba6f7", "base0F": "f2cdcd"
9
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "Clay Light",
3
+ "author": "Clay",
4
+ "variant": "light",
5
+ "base00": "F3EBE7", "base01": "EBE1DC", "base02": "D8CCC6", "base03": "A09590",
6
+ "base04": "786D67", "base05": "504541", "base06": "332925", "base07": "1A1412",
7
+ "base08": "C83520", "base09": "F74728", "base0A": "C08520", "base0B": "008F6B",
8
+ "base0C": "1C8575", "base0D": "3560B0", "base0E": "8C4E8E", "base0F": "A57C45",
9
+ "accent2": "2A26E5"
10
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "Clay Dark",
3
+ "author": "Clay",
4
+ "variant": "dark",
5
+ "base00": "1F1B1B", "base01": "2A2525", "base02": "352F2F", "base03": "7D7370",
6
+ "base04": "A09590", "base05": "C2BAB4", "base06": "E5DED8", "base07": "FFFFFF",
7
+ "base08": "F74728", "base09": "FE7150", "base0A": "E5A040", "base0B": "09E5A3",
8
+ "base0C": "4EC9B0", "base0D": "6BA0E5", "base0E": "D085CC", "base0F": "D09558",
9
+ "accent2": "5857FC"
10
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "Dracula",
3
+ "author": "Jamy Golden",
4
+ "variant": "dark",
5
+ "base00": "282a36", "base01": "363447", "base02": "44475a", "base03": "6272a4",
6
+ "base04": "9ea8c7", "base05": "f8f8f2", "base06": "f0f1f4", "base07": "ffffff",
7
+ "base08": "ff5555", "base09": "ffb86c", "base0A": "f1fa8c", "base0B": "50fa7b",
8
+ "base0C": "8be9fd", "base0D": "80bfff", "base0E": "ff79c6", "base0F": "bd93f9"
9
+ }