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.
- package/LICENSE +21 -0
- package/README.md +281 -0
- package/bin/cli.js +2385 -0
- package/lib/cli-sessions.js +270 -0
- package/lib/config.js +237 -0
- package/lib/daemon.js +489 -0
- package/lib/ipc.js +112 -0
- package/lib/notes.js +120 -0
- package/lib/pages.js +664 -0
- package/lib/project.js +1433 -0
- package/lib/public/app.js +2795 -0
- package/lib/public/apple-touch-icon-dark.png +0 -0
- package/lib/public/apple-touch-icon.png +0 -0
- package/lib/public/css/base.css +264 -0
- package/lib/public/css/diff.css +128 -0
- package/lib/public/css/filebrowser.css +1114 -0
- package/lib/public/css/highlight.css +144 -0
- package/lib/public/css/icon-strip.css +296 -0
- package/lib/public/css/input.css +573 -0
- package/lib/public/css/menus.css +856 -0
- package/lib/public/css/messages.css +1445 -0
- package/lib/public/css/mobile-nav.css +354 -0
- package/lib/public/css/overlays.css +697 -0
- package/lib/public/css/rewind.css +505 -0
- package/lib/public/css/server-settings.css +761 -0
- package/lib/public/css/sidebar.css +936 -0
- package/lib/public/css/sticky-notes.css +358 -0
- package/lib/public/css/title-bar.css +314 -0
- package/lib/public/favicon-dark.svg +1 -0
- package/lib/public/favicon.svg +1 -0
- package/lib/public/icon-192-dark.png +0 -0
- package/lib/public/icon-192.png +0 -0
- package/lib/public/icon-512-dark.png +0 -0
- package/lib/public/icon-512.png +0 -0
- package/lib/public/icon-mono.svg +1 -0
- package/lib/public/index.html +762 -0
- package/lib/public/manifest.json +27 -0
- package/lib/public/modules/diff.js +398 -0
- package/lib/public/modules/events.js +21 -0
- package/lib/public/modules/filebrowser.js +1411 -0
- package/lib/public/modules/fileicons.js +172 -0
- package/lib/public/modules/icons.js +54 -0
- package/lib/public/modules/input.js +584 -0
- package/lib/public/modules/markdown.js +356 -0
- package/lib/public/modules/notifications.js +649 -0
- package/lib/public/modules/qrcode.js +70 -0
- package/lib/public/modules/rewind.js +345 -0
- package/lib/public/modules/server-settings.js +510 -0
- package/lib/public/modules/sidebar.js +1083 -0
- package/lib/public/modules/state.js +3 -0
- package/lib/public/modules/sticky-notes.js +688 -0
- package/lib/public/modules/terminal.js +697 -0
- package/lib/public/modules/theme.js +738 -0
- package/lib/public/modules/tools.js +1608 -0
- package/lib/public/modules/utils.js +56 -0
- package/lib/public/style.css +15 -0
- package/lib/public/sw.js +75 -0
- package/lib/push.js +124 -0
- package/lib/sdk-bridge.js +989 -0
- package/lib/server.js +582 -0
- package/lib/sessions.js +424 -0
- package/lib/terminal-manager.js +187 -0
- package/lib/terminal.js +24 -0
- package/lib/themes/ayu-light.json +9 -0
- package/lib/themes/catppuccin-latte.json +9 -0
- package/lib/themes/catppuccin-mocha.json +9 -0
- package/lib/themes/clay-light.json +10 -0
- package/lib/themes/clay.json +10 -0
- package/lib/themes/dracula.json +9 -0
- package/lib/themes/everforest-light.json +9 -0
- package/lib/themes/everforest.json +9 -0
- package/lib/themes/github-light.json +9 -0
- package/lib/themes/gruvbox-dark.json +9 -0
- package/lib/themes/gruvbox-light.json +9 -0
- package/lib/themes/monokai.json +9 -0
- package/lib/themes/nord-light.json +9 -0
- package/lib/themes/nord.json +9 -0
- package/lib/themes/one-dark.json +9 -0
- package/lib/themes/one-light.json +9 -0
- package/lib/themes/rose-pine-dawn.json +9 -0
- package/lib/themes/rose-pine.json +9 -0
- package/lib/themes/solarized-dark.json +9 -0
- package/lib/themes/solarized-light.json +9 -0
- package/lib/themes/tokyo-night-light.json +9 -0
- package/lib/themes/tokyo-night.json +9 -0
- package/lib/updater.js +97 -0
- package/package.json +47 -0
package/lib/sessions.js
ADDED
|
@@ -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 };
|
package/lib/terminal.js
ADDED
|
@@ -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
|
+
}
|