clay-server 2.26.1-beta.1 → 2.27.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/project-connection.js +259 -0
- package/lib/project-file-watch.js +120 -0
- package/lib/project-filesystem.js +482 -0
- package/lib/project-http.js +685 -0
- package/lib/project-image.js +94 -0
- package/lib/project-knowledge.js +161 -0
- package/lib/project-loop.js +1160 -0
- package/lib/project-sessions.js +1152 -0
- package/lib/project-user-message.js +631 -0
- package/lib/project.js +357 -4438
- package/lib/server.js +30 -0
- package/package.json +1 -1
package/lib/project.js
CHANGED
|
@@ -8,7 +8,6 @@ var { createTerminalManager } = require("./terminal-manager");
|
|
|
8
8
|
var { createNotesManager } = require("./notes");
|
|
9
9
|
var { fetchLatestVersion, fetchVersion, isNewer } = require("./updater");
|
|
10
10
|
var { execFileSync, spawn } = require("child_process");
|
|
11
|
-
var { createLoopRegistry } = require("./scheduler");
|
|
12
11
|
var usersModule = require("./users");
|
|
13
12
|
var { resolveOsUserInfo, fsAsUser } = require("./os-users");
|
|
14
13
|
var crisisSafety = require("./crisis-safety");
|
|
@@ -18,7 +17,15 @@ var userPresence = require("./user-presence");
|
|
|
18
17
|
var { attachDebate } = require("./project-debate");
|
|
19
18
|
var { attachMemory } = require("./project-memory");
|
|
20
19
|
var { attachMateInteraction } = require("./project-mate-interaction");
|
|
21
|
-
var
|
|
20
|
+
var { attachLoop } = require("./project-loop");
|
|
21
|
+
var { attachFileWatch } = require("./project-file-watch");
|
|
22
|
+
var { attachHTTP } = require("./project-http");
|
|
23
|
+
var { attachImage } = require("./project-image");
|
|
24
|
+
var { attachKnowledge } = require("./project-knowledge");
|
|
25
|
+
var { attachFilesystem } = require("./project-filesystem");
|
|
26
|
+
var { attachSessions } = require("./project-sessions");
|
|
27
|
+
var { attachUserMessage } = require("./project-user-message");
|
|
28
|
+
var { attachConnection } = require("./project-connection");
|
|
22
29
|
|
|
23
30
|
// --- Context Sources persistence ---
|
|
24
31
|
var _ctxSrcConfig = require("./config");
|
|
@@ -91,21 +98,6 @@ var BINARY_EXTS = new Set([
|
|
|
91
98
|
]);
|
|
92
99
|
var IMAGE_EXTS = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".bmp", ".ico"]);
|
|
93
100
|
var FS_MAX_SIZE = 512 * 1024;
|
|
94
|
-
var MIME_TYPES = {
|
|
95
|
-
".html": "text/html",
|
|
96
|
-
".css": "text/css",
|
|
97
|
-
".js": "application/javascript",
|
|
98
|
-
".json": "application/json",
|
|
99
|
-
".png": "image/png",
|
|
100
|
-
".jpg": "image/jpeg",
|
|
101
|
-
".jpeg": "image/jpeg",
|
|
102
|
-
".gif": "image/gif",
|
|
103
|
-
".webp": "image/webp",
|
|
104
|
-
".bmp": "image/bmp",
|
|
105
|
-
".svg": "image/svg+xml",
|
|
106
|
-
".ico": "image/x-icon",
|
|
107
|
-
};
|
|
108
|
-
|
|
109
101
|
function safePath(base, requested) {
|
|
110
102
|
var resolved = path.resolve(base, requested);
|
|
111
103
|
if (resolved !== base && !resolved.startsWith(base + path.sep)) return null;
|
|
@@ -168,77 +160,11 @@ function createProjectContext(opts) {
|
|
|
168
160
|
// Browser MCP server runs in-process via createSdkMcpServer (no child process spawn).
|
|
169
161
|
// Do NOT write to .claude-local/settings.json -- the SDK reads that too, causing duplicate spawns.
|
|
170
162
|
|
|
171
|
-
// ---
|
|
172
|
-
var
|
|
173
|
-
var
|
|
174
|
-
var
|
|
175
|
-
var
|
|
176
|
-
var imagesDir = path.join(_imagesBaseDir, _imagesEncodedCwd);
|
|
177
|
-
|
|
178
|
-
// Convert imageRefs in history entries to images with URLs for the client
|
|
179
|
-
function hydrateImageRefs(entry) {
|
|
180
|
-
if (!entry) return entry;
|
|
181
|
-
// Hydrate context_preview: convert screenshotFile to screenshotUrl
|
|
182
|
-
if (entry.type === "context_preview" && entry.tab && entry.tab.screenshotFile) {
|
|
183
|
-
var hydrated = {};
|
|
184
|
-
for (var k in entry) hydrated[k] = entry[k];
|
|
185
|
-
hydrated.tab = {};
|
|
186
|
-
for (var tk in entry.tab) hydrated.tab[tk] = entry.tab[tk];
|
|
187
|
-
hydrated.tab.screenshotUrl = "/p/" + slug + "/images/" + entry.tab.screenshotFile;
|
|
188
|
-
delete hydrated.tab.screenshotFile;
|
|
189
|
-
return hydrated;
|
|
190
|
-
}
|
|
191
|
-
if (!entry.imageRefs) return entry;
|
|
192
|
-
if (entry.type !== "user_message" && entry.type !== "mention_user") return entry;
|
|
193
|
-
var images = [];
|
|
194
|
-
for (var ri = 0; ri < entry.imageRefs.length; ri++) {
|
|
195
|
-
var ref = entry.imageRefs[ri];
|
|
196
|
-
images.push({ mediaType: ref.mediaType, url: "/p/" + slug + "/images/" + ref.file });
|
|
197
|
-
}
|
|
198
|
-
var hydrated = {};
|
|
199
|
-
for (var k2 in entry) {
|
|
200
|
-
if (k2 !== "imageRefs") hydrated[k2] = entry[k2];
|
|
201
|
-
}
|
|
202
|
-
hydrated.images = images;
|
|
203
|
-
return hydrated;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function saveImageFile(mediaType, base64data, ownerLinuxUser) {
|
|
207
|
-
try { fs.mkdirSync(imagesDir, { recursive: true }); } catch (e) {}
|
|
208
|
-
var ext = mediaType === "image/png" ? ".png" : mediaType === "image/gif" ? ".gif" : mediaType === "image/webp" ? ".webp" : ".jpg";
|
|
209
|
-
var hash = crypto.createHash("sha256").update(base64data).digest("hex").substring(0, 16);
|
|
210
|
-
var fileName = Date.now() + "-" + hash + ext;
|
|
211
|
-
var filePath = path.join(imagesDir, fileName);
|
|
212
|
-
try {
|
|
213
|
-
fs.writeFileSync(filePath, Buffer.from(base64data, "base64"));
|
|
214
|
-
if (process.platform !== "win32") {
|
|
215
|
-
// 644 so all local users can read (needed for git, copy, etc.)
|
|
216
|
-
try { fs.chmodSync(filePath, 0o644); } catch (e) {}
|
|
217
|
-
// In OS-user mode the daemon runs as root, so chown the file
|
|
218
|
-
// (and parent dirs) to the session owner to avoid permission issues.
|
|
219
|
-
if (ownerLinuxUser) {
|
|
220
|
-
try {
|
|
221
|
-
var osUsersMod = require("./os-users");
|
|
222
|
-
var uid = osUsersMod.getLinuxUserUid(ownerLinuxUser);
|
|
223
|
-
if (uid != null) {
|
|
224
|
-
require("child_process").execSync("chown " + uid + " " + JSON.stringify(filePath));
|
|
225
|
-
// Also fix parent dirs if root-owned
|
|
226
|
-
try {
|
|
227
|
-
var dirStat = fs.statSync(imagesDir);
|
|
228
|
-
if (dirStat.uid !== uid) {
|
|
229
|
-
require("child_process").execSync("chown " + uid + " " + JSON.stringify(imagesDir));
|
|
230
|
-
}
|
|
231
|
-
} catch (e2) {}
|
|
232
|
-
}
|
|
233
|
-
} catch (e) {}
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
return fileName;
|
|
237
|
-
} catch (e) {
|
|
238
|
-
console.error("[images] Failed to save image:", e.message);
|
|
239
|
-
return null;
|
|
240
|
-
}
|
|
241
|
-
}
|
|
163
|
+
// --- Image engine (delegated to project-image.js) ---
|
|
164
|
+
var _image = attachImage({ cwd: cwd, slug: slug });
|
|
165
|
+
var imagesDir = _image.imagesDir;
|
|
166
|
+
var hydrateImageRefs = _image.hydrateImageRefs;
|
|
167
|
+
var saveImageFile = _image.saveImageFile;
|
|
242
168
|
|
|
243
169
|
// --- OS-level user isolation helper ---
|
|
244
170
|
// Returns the Linux username for the session owner.
|
|
@@ -415,98 +341,29 @@ function createProjectContext(opts) {
|
|
|
415
341
|
}
|
|
416
342
|
}
|
|
417
343
|
|
|
418
|
-
// ---
|
|
419
|
-
var
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
if (watchedPath === relPath) return;
|
|
427
|
-
stopFileWatch();
|
|
428
|
-
watchedPath = relPath;
|
|
429
|
-
try {
|
|
430
|
-
fileWatcher = fs.watch(absPath, function () {
|
|
431
|
-
clearTimeout(watchDebounce);
|
|
432
|
-
watchDebounce = setTimeout(function () {
|
|
433
|
-
try {
|
|
434
|
-
var stat = fs.statSync(absPath);
|
|
435
|
-
var ext = path.extname(absPath).toLowerCase();
|
|
436
|
-
if (stat.size > FS_MAX_SIZE || BINARY_EXTS.has(ext)) return;
|
|
437
|
-
var content = fs.readFileSync(absPath, "utf8");
|
|
438
|
-
send({ type: "fs_file_changed", path: relPath, content: content, size: stat.size });
|
|
439
|
-
} catch (e) {
|
|
440
|
-
stopFileWatch();
|
|
441
|
-
}
|
|
442
|
-
}, 200);
|
|
443
|
-
});
|
|
444
|
-
fileWatcher.on("error", function () { stopFileWatch(); });
|
|
445
|
-
} catch (e) {
|
|
446
|
-
watchedPath = null;
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
function stopFileWatch() {
|
|
451
|
-
if (fileWatcher) {
|
|
452
|
-
try { fileWatcher.close(); } catch (e) {}
|
|
453
|
-
fileWatcher = null;
|
|
454
|
-
}
|
|
455
|
-
clearTimeout(watchDebounce);
|
|
456
|
-
watchDebounce = null;
|
|
457
|
-
watchedPath = null;
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
// --- Directory watcher ---
|
|
461
|
-
var dirWatchers = {}; // relPath -> { watcher, debounce }
|
|
462
|
-
|
|
463
|
-
function startDirWatch(relPath) {
|
|
464
|
-
if (dirWatchers[relPath]) return;
|
|
465
|
-
var absPath = safePath(cwd, relPath);
|
|
466
|
-
if (!absPath) return;
|
|
467
|
-
try {
|
|
468
|
-
var debounce = null;
|
|
469
|
-
var watcher = fs.watch(absPath, function () {
|
|
470
|
-
clearTimeout(debounce);
|
|
471
|
-
debounce = setTimeout(function () {
|
|
472
|
-
// Re-read directory and broadcast to all clients
|
|
473
|
-
try {
|
|
474
|
-
var items = fs.readdirSync(absPath, { withFileTypes: true });
|
|
475
|
-
var entries = [];
|
|
476
|
-
for (var i = 0; i < items.length; i++) {
|
|
477
|
-
if (items[i].isDirectory() && IGNORED_DIRS.has(items[i].name)) continue;
|
|
478
|
-
entries.push({
|
|
479
|
-
name: items[i].name,
|
|
480
|
-
type: items[i].isDirectory() ? "dir" : "file",
|
|
481
|
-
path: path.relative(cwd, path.join(absPath, items[i].name)).split(path.sep).join("/"),
|
|
482
|
-
});
|
|
483
|
-
}
|
|
484
|
-
send({ type: "fs_dir_changed", path: relPath, entries: entries });
|
|
485
|
-
} catch (e) {
|
|
486
|
-
stopDirWatch(relPath);
|
|
487
|
-
}
|
|
488
|
-
}, 300);
|
|
489
|
-
});
|
|
490
|
-
watcher.on("error", function () { stopDirWatch(relPath); });
|
|
491
|
-
dirWatchers[relPath] = { watcher: watcher, debounce: debounce };
|
|
492
|
-
} catch (e) {}
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
function stopDirWatch(relPath) {
|
|
496
|
-
var entry = dirWatchers[relPath];
|
|
497
|
-
if (entry) {
|
|
498
|
-
clearTimeout(entry.debounce);
|
|
499
|
-
try { entry.watcher.close(); } catch (e) {}
|
|
500
|
-
delete dirWatchers[relPath];
|
|
501
|
-
}
|
|
502
|
-
}
|
|
344
|
+
// --- Knowledge engine (delegated to project-knowledge.js) ---
|
|
345
|
+
var _knowledge = attachKnowledge({
|
|
346
|
+
cwd: cwd,
|
|
347
|
+
isMate: isMate,
|
|
348
|
+
sendTo: sendTo,
|
|
349
|
+
matesModule: matesModule,
|
|
350
|
+
getProjectOwnerId: function () { return projectOwnerId; },
|
|
351
|
+
});
|
|
503
352
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
353
|
+
// --- File/directory watcher engine (delegated to project-file-watch.js) ---
|
|
354
|
+
var _fileWatch = attachFileWatch({
|
|
355
|
+
cwd: cwd,
|
|
356
|
+
send: send,
|
|
357
|
+
safePath: safePath,
|
|
358
|
+
BINARY_EXTS: BINARY_EXTS,
|
|
359
|
+
FS_MAX_SIZE: FS_MAX_SIZE,
|
|
360
|
+
IGNORED_DIRS: IGNORED_DIRS,
|
|
361
|
+
});
|
|
362
|
+
var startFileWatch = _fileWatch.startFileWatch;
|
|
363
|
+
var stopFileWatch = _fileWatch.stopFileWatch;
|
|
364
|
+
var startDirWatch = _fileWatch.startDirWatch;
|
|
365
|
+
var stopDirWatch = _fileWatch.stopDirWatch;
|
|
366
|
+
var stopAllDirWatches = _fileWatch.stopAllDirWatches;
|
|
510
367
|
|
|
511
368
|
// --- Session manager ---
|
|
512
369
|
var sm = createSessionManager({
|
|
@@ -641,3665 +498,206 @@ function createProjectContext(opts) {
|
|
|
641
498
|
},
|
|
642
499
|
});
|
|
643
500
|
|
|
644
|
-
// ---
|
|
645
|
-
var
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
501
|
+
// --- Loop engine (delegated to project-loop.js) ---
|
|
502
|
+
var _loop = attachLoop({
|
|
503
|
+
cwd: cwd,
|
|
504
|
+
slug: slug,
|
|
505
|
+
sm: sm,
|
|
506
|
+
sdk: sdk,
|
|
507
|
+
send: send,
|
|
508
|
+
sendTo: sendTo,
|
|
509
|
+
sendToSession: sendToSession,
|
|
510
|
+
pushModule: pushModule,
|
|
511
|
+
getHubSchedules: getHubSchedules,
|
|
512
|
+
getLinuxUserForSession: getLinuxUserForSession,
|
|
513
|
+
onProcessingChanged: onProcessingChanged,
|
|
514
|
+
hydrateImageRefs: hydrateImageRefs,
|
|
515
|
+
});
|
|
516
|
+
var loopState = _loop.loopState;
|
|
517
|
+
var loopRegistry = _loop.loopRegistry;
|
|
518
|
+
var loopDir = _loop.loopDir;
|
|
519
|
+
var startLoop = _loop.startLoop;
|
|
520
|
+
var stopLoop = _loop.stopLoop;
|
|
521
|
+
var resumeLoop = _loop.resumeLoop;
|
|
663
522
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
return path.join(cwd, ".claude", "loops", id);
|
|
668
|
-
}
|
|
523
|
+
// Mate CLAUDE.md crisis safety watcher
|
|
524
|
+
var crisisWatcher = null;
|
|
525
|
+
var crisisDebounce = null;
|
|
669
526
|
|
|
670
|
-
function generateLoopId() {
|
|
671
|
-
return "loop_" + Date.now() + "_" + crypto.randomBytes(3).toString("hex");
|
|
672
|
-
}
|
|
673
527
|
|
|
674
|
-
// Loop state persistence
|
|
675
|
-
var _loopConfig = require("./config");
|
|
676
|
-
var _loopUtils = require("./utils");
|
|
677
|
-
var _loopDir = path.join(_loopConfig.CONFIG_DIR, "loops");
|
|
678
|
-
var _loopEncodedCwd = _loopUtils.resolveEncodedFile(_loopDir, cwd, ".json");
|
|
679
|
-
var _loopStatePath = path.join(_loopDir, _loopEncodedCwd + ".json");
|
|
680
528
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
var data = {
|
|
685
|
-
phase: loopState.phase,
|
|
686
|
-
active: loopState.active,
|
|
687
|
-
iteration: loopState.iteration,
|
|
688
|
-
maxIterations: loopState.maxIterations,
|
|
689
|
-
baseCommit: loopState.baseCommit,
|
|
690
|
-
results: loopState.results,
|
|
691
|
-
wizardData: loopState.wizardData,
|
|
692
|
-
startedAt: loopState.startedAt,
|
|
693
|
-
loopId: loopState.loopId,
|
|
694
|
-
loopFilesId: loopState.loopFilesId || null,
|
|
695
|
-
};
|
|
696
|
-
var tmpPath = _loopStatePath + ".tmp";
|
|
697
|
-
fs.writeFileSync(tmpPath, JSON.stringify(data, null, 2));
|
|
698
|
-
fs.renameSync(tmpPath, _loopStatePath);
|
|
699
|
-
} catch (e) {
|
|
700
|
-
console.error("[ralph-loop] Failed to save state:", e.message);
|
|
701
|
-
}
|
|
702
|
-
}
|
|
529
|
+
// --- Terminal manager ---
|
|
530
|
+
var tm = createTerminalManager({ cwd: cwd, send: send, sendTo: sendTo });
|
|
531
|
+
var nm = createNotesManager({ cwd: cwd, send: send, sendTo: sendTo });
|
|
703
532
|
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
loopState.active = data.active || false;
|
|
710
|
-
loopState.iteration = data.iteration || 0;
|
|
711
|
-
loopState.maxIterations = data.maxIterations || 20;
|
|
712
|
-
loopState.baseCommit = data.baseCommit || null;
|
|
713
|
-
loopState.results = data.results || [];
|
|
714
|
-
loopState.wizardData = data.wizardData || null;
|
|
715
|
-
loopState.startedAt = data.startedAt || null;
|
|
716
|
-
loopState.loopId = data.loopId || null;
|
|
717
|
-
loopState.loopFilesId = data.loopFilesId || null;
|
|
718
|
-
// SDK sessions cannot survive daemon restart
|
|
719
|
-
loopState.currentSessionId = null;
|
|
720
|
-
loopState.judgeSessionId = null;
|
|
721
|
-
loopState.craftingSessionId = null;
|
|
722
|
-
loopState.stopping = false;
|
|
723
|
-
// If was executing, schedule resume after SDK is ready
|
|
724
|
-
if (loopState.phase === "executing" && loopState.active) {
|
|
725
|
-
loopState._needsResume = true;
|
|
726
|
-
}
|
|
727
|
-
// If was crafting, check if files exist and move to approval
|
|
728
|
-
if (loopState.phase === "crafting") {
|
|
729
|
-
var hasFiles = checkLoopFilesExist();
|
|
730
|
-
if (hasFiles) {
|
|
731
|
-
loopState.phase = "approval";
|
|
732
|
-
saveLoopState();
|
|
733
|
-
} else {
|
|
734
|
-
loopState.phase = "idle";
|
|
735
|
-
saveLoopState();
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
} catch (e) {
|
|
739
|
-
// No saved state, use defaults
|
|
740
|
-
}
|
|
741
|
-
// Recover orphaned loops: if idle but completed loop files exist in .claude/loops/
|
|
742
|
-
if (loopState.phase === "idle") {
|
|
743
|
-
var _loopsBase = path.join(cwd, ".claude", "loops");
|
|
744
|
-
try {
|
|
745
|
-
var _loopDirs = fs.readdirSync(_loopsBase).filter(function (d) {
|
|
746
|
-
return d.indexOf("loop_") === 0;
|
|
747
|
-
});
|
|
748
|
-
for (var _li = 0; _li < _loopDirs.length; _li++) {
|
|
749
|
-
var _ld = path.join(_loopsBase, _loopDirs[_li]);
|
|
750
|
-
try {
|
|
751
|
-
fs.accessSync(path.join(_ld, "PROMPT.md"));
|
|
752
|
-
fs.accessSync(path.join(_ld, "JUDGE.md"));
|
|
753
|
-
fs.accessSync(path.join(_ld, "LOOP.json"));
|
|
754
|
-
// Found a completed loop — recover to approval phase
|
|
755
|
-
loopState.loopId = _loopDirs[_li];
|
|
756
|
-
loopState.phase = "approval";
|
|
757
|
-
var _loopCfg = JSON.parse(fs.readFileSync(path.join(_ld, "LOOP.json"), "utf8"));
|
|
758
|
-
loopState.maxIterations = _loopCfg.maxIterations || 20;
|
|
759
|
-
saveLoopState();
|
|
760
|
-
console.log("[ralph-loop] Recovered orphaned loop: " + _loopDirs[_li]);
|
|
761
|
-
break;
|
|
762
|
-
} catch (e) {}
|
|
763
|
-
}
|
|
764
|
-
} catch (e) {}
|
|
533
|
+
// Check for updates in background (admin only)
|
|
534
|
+
fetchVersion(updateChannel).then(function (v) {
|
|
535
|
+
if (v && isNewer(v, currentVersion)) {
|
|
536
|
+
latestVersion = v;
|
|
537
|
+
sendToAdmins({ type: "update_available", version: v });
|
|
765
538
|
}
|
|
766
|
-
}
|
|
539
|
+
}).catch(function (e) {
|
|
540
|
+
console.error("[project] Background version check failed:", e.message || e);
|
|
541
|
+
});
|
|
767
542
|
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
loopState.promptText = "";
|
|
772
|
-
loopState.judgeText = "";
|
|
773
|
-
loopState.iteration = 0;
|
|
774
|
-
loopState.maxIterations = 20;
|
|
775
|
-
loopState.baseCommit = null;
|
|
776
|
-
loopState.currentSessionId = null;
|
|
777
|
-
loopState.judgeSessionId = null;
|
|
778
|
-
loopState.results = [];
|
|
779
|
-
loopState.stopping = false;
|
|
780
|
-
loopState.wizardData = null;
|
|
781
|
-
loopState.craftingSessionId = null;
|
|
782
|
-
loopState.startedAt = null;
|
|
783
|
-
loopState.loopId = null;
|
|
784
|
-
loopState.loopFilesId = null;
|
|
785
|
-
saveLoopState();
|
|
543
|
+
// --- WS connection handler (delegated to project-connection.js) ---
|
|
544
|
+
function handleConnection(ws, wsUser) {
|
|
545
|
+
_connection.handleConnection(ws, wsUser, handleMessage, handleDisconnection);
|
|
786
546
|
}
|
|
787
547
|
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
var hasPrompt = false;
|
|
792
|
-
var hasJudge = false;
|
|
793
|
-
try { fs.accessSync(path.join(dir, "PROMPT.md")); hasPrompt = true; } catch (e) {}
|
|
794
|
-
try { fs.accessSync(path.join(dir, "JUDGE.md")); hasJudge = true; } catch (e) {}
|
|
795
|
-
return hasPrompt && hasJudge;
|
|
548
|
+
// --- WS message handler ---
|
|
549
|
+
function getSessionForWs(ws) {
|
|
550
|
+
return sm.sessions.get(ws._clayActiveSession) || null;
|
|
796
551
|
}
|
|
797
552
|
|
|
798
|
-
//
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
var crisisDebounce = null;
|
|
805
|
-
|
|
806
|
-
function startClaudeDirWatch() {
|
|
807
|
-
if (claudeDirWatcher) return;
|
|
808
|
-
var watchDir = loopDir();
|
|
809
|
-
if (!watchDir) return;
|
|
810
|
-
try { fs.mkdirSync(watchDir, { recursive: true }); } catch (e) {}
|
|
811
|
-
try {
|
|
812
|
-
claudeDirWatcher = fs.watch(watchDir, function () {
|
|
813
|
-
if (claudeDirDebounce) clearTimeout(claudeDirDebounce);
|
|
814
|
-
claudeDirDebounce = setTimeout(function () {
|
|
815
|
-
broadcastLoopFilesStatus();
|
|
816
|
-
}, 300);
|
|
817
|
-
});
|
|
818
|
-
claudeDirWatcher.on("error", function () {});
|
|
819
|
-
} catch (e) {
|
|
820
|
-
console.error("[ralph-loop] Failed to watch .claude/:", e.message);
|
|
553
|
+
// --- Schedule / cancel a message (used by WS handler and auto-continue) ---
|
|
554
|
+
function scheduleMessage(session, text, resetsAt) {
|
|
555
|
+
if (!session || !text || !resetsAt) return;
|
|
556
|
+
// Cancel any existing scheduled message
|
|
557
|
+
if (session.scheduledMessage && session.scheduledMessage.timer) {
|
|
558
|
+
clearTimeout(session.scheduledMessage.timer);
|
|
821
559
|
}
|
|
560
|
+
var isPastReset = resetsAt <= Date.now();
|
|
561
|
+
var schedDelay = isPastReset ? 5000 : Math.max(0, resetsAt - Date.now()) + 60000; // +1min buffer after reset, or 5s for immediate
|
|
562
|
+
var sendsAt = Date.now() + schedDelay;
|
|
563
|
+
var schedEntry = {
|
|
564
|
+
type: "scheduled_message_queued",
|
|
565
|
+
text: text,
|
|
566
|
+
resetsAt: sendsAt,
|
|
567
|
+
scheduledAt: Date.now(),
|
|
568
|
+
};
|
|
569
|
+
sm.sendAndRecord(session, schedEntry);
|
|
570
|
+
session.scheduledMessage = {
|
|
571
|
+
text: text,
|
|
572
|
+
resetsAt: resetsAt,
|
|
573
|
+
timer: setTimeout(function () {
|
|
574
|
+
session.scheduledMessage = null;
|
|
575
|
+
if (session.destroying) return;
|
|
576
|
+
console.log("[project] Scheduled message firing for session " + session.localId);
|
|
577
|
+
sm.sendAndRecord(session, { type: "scheduled_message_sent" });
|
|
578
|
+
var schedUserMsg = { type: "user_message", text: text };
|
|
579
|
+
session.history.push(schedUserMsg);
|
|
580
|
+
sm.appendToSessionFile(session, schedUserMsg);
|
|
581
|
+
sendToSession(session.localId, schedUserMsg);
|
|
582
|
+
session.isProcessing = true;
|
|
583
|
+
onProcessingChanged();
|
|
584
|
+
sendToSession(session.localId, { type: "status", status: "processing" });
|
|
585
|
+
sdk.startQuery(session, text, null, getLinuxUserForSession(session));
|
|
586
|
+
sm.broadcastSessionList();
|
|
587
|
+
}, schedDelay),
|
|
588
|
+
};
|
|
822
589
|
}
|
|
823
590
|
|
|
824
|
-
function
|
|
825
|
-
if (
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
claudeDirDebounce = null;
|
|
591
|
+
function cancelScheduledMessage(session) {
|
|
592
|
+
if (!session) return;
|
|
593
|
+
if (session.scheduledMessage && session.scheduledMessage.timer) {
|
|
594
|
+
clearTimeout(session.scheduledMessage.timer);
|
|
595
|
+
session.scheduledMessage = null;
|
|
596
|
+
session.rateLimitAutoContinuePending = false;
|
|
597
|
+
sm.sendAndRecord(session, { type: "scheduled_message_cancelled" });
|
|
832
598
|
}
|
|
833
599
|
}
|
|
834
600
|
|
|
835
|
-
function
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
if (dir) {
|
|
841
|
-
try { fs.accessSync(path.join(dir, "PROMPT.md")); hasPrompt = true; } catch (e) {}
|
|
842
|
-
try { fs.accessSync(path.join(dir, "JUDGE.md")); hasJudge = true; } catch (e) {}
|
|
843
|
-
try { fs.accessSync(path.join(dir, "LOOP.json")); hasLoopJson = true; } catch (e) {}
|
|
844
|
-
}
|
|
845
|
-
send({
|
|
846
|
-
type: "ralph_files_status",
|
|
847
|
-
promptReady: hasPrompt,
|
|
848
|
-
judgeReady: hasJudge,
|
|
849
|
-
loopJsonReady: hasLoopJson,
|
|
850
|
-
bothReady: hasPrompt && hasJudge,
|
|
851
|
-
taskId: loopState.loopId,
|
|
852
|
-
});
|
|
853
|
-
// Auto-transition to approval phase when both files appear
|
|
854
|
-
if (hasPrompt && hasJudge && loopState.phase === "crafting") {
|
|
855
|
-
loopState.phase = "approval";
|
|
856
|
-
saveLoopState();
|
|
857
|
-
|
|
858
|
-
// Parse recommended title from crafting session conversation
|
|
859
|
-
if (loopState.craftingSessionId && loopState.loopId) {
|
|
860
|
-
var craftSess = sm.sessions.get(loopState.craftingSessionId);
|
|
861
|
-
if (craftSess && craftSess.history) {
|
|
862
|
-
for (var hi = craftSess.history.length - 1; hi >= 0; hi--) {
|
|
863
|
-
var entry = craftSess.history[hi];
|
|
864
|
-
var entryText = entry.text || "";
|
|
865
|
-
var titleMatch = entryText.match(/\[\[LOOP_TITLE:\s*(.+?)\]\]/);
|
|
866
|
-
if (titleMatch) {
|
|
867
|
-
var suggestedTitle = titleMatch[1].trim();
|
|
868
|
-
if (suggestedTitle) {
|
|
869
|
-
loopRegistry.updateRecord(loopState.loopId, { name: suggestedTitle });
|
|
870
|
-
}
|
|
871
|
-
break;
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
}
|
|
601
|
+
function handleMessage(ws, msg) {
|
|
602
|
+
// --- DM messages (delegated to server-level handler) ---
|
|
603
|
+
if (msg.type === "dm_open" || msg.type === "dm_send" || msg.type === "dm_list" || msg.type === "dm_typing" || msg.type === "dm_add_favorite" || msg.type === "dm_remove_favorite" || msg.type === "mate_create" || msg.type === "mate_list" || msg.type === "mate_delete" || msg.type === "mate_update" || msg.type === "mate_readd_builtin" || msg.type === "mate_list_available_builtins") {
|
|
604
|
+
if (typeof opts.onDmMessage === "function") {
|
|
605
|
+
opts.onDmMessage(ws, msg);
|
|
875
606
|
}
|
|
607
|
+
return;
|
|
876
608
|
}
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
// Load persisted state on startup
|
|
880
|
-
loadLoopState();
|
|
881
|
-
|
|
882
|
-
// --- Loop Registry (unified one-off + scheduled) ---
|
|
883
|
-
var activeRegistryId = null; // track which registry record triggered current loop
|
|
884
609
|
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
if (record.skipIfRunning !== false) {
|
|
891
|
-
console.log("[loop-registry] Skipping trigger for " + record.name + " — loop already active (skipIfRunning)");
|
|
892
|
-
return;
|
|
893
|
-
}
|
|
894
|
-
console.log("[loop-registry] Loop active but skipIfRunning disabled for " + record.name + "; deferring");
|
|
895
|
-
return;
|
|
896
|
-
}
|
|
610
|
+
// --- @Mention: invoke another Mate inline ---
|
|
611
|
+
if (msg.type === "mention") {
|
|
612
|
+
handleMention(ws, msg);
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
897
615
|
|
|
898
|
-
|
|
899
|
-
var
|
|
900
|
-
if (
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
616
|
+
if (msg.type === "mention_stop") {
|
|
617
|
+
var session = getSessionForWs(ws);
|
|
618
|
+
if (session && session._mentionInProgress) {
|
|
619
|
+
// Abort the active mention session for this mate
|
|
620
|
+
var mateId = msg.mateId;
|
|
621
|
+
if (mateId && session._mentionSessions && session._mentionSessions[mateId]) {
|
|
622
|
+
session._mentionSessions[mateId].abort();
|
|
623
|
+
session._mentionSessions[mateId].close();
|
|
624
|
+
delete session._mentionSessions[mateId];
|
|
904
625
|
}
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
// Verify the loop directory and PROMPT.md exist
|
|
910
|
-
var recDir = path.join(cwd, ".claude", "loops", loopFilesId);
|
|
911
|
-
try {
|
|
912
|
-
fs.accessSync(path.join(recDir, "PROMPT.md"));
|
|
913
|
-
} catch (e) {
|
|
914
|
-
console.error("[loop-registry] PROMPT.md missing for " + loopFilesId);
|
|
915
|
-
return;
|
|
626
|
+
session._mentionInProgress = false;
|
|
627
|
+
sendToSession(session.localId, { type: "mention_done", mateId: mateId, stopped: true });
|
|
628
|
+
send({ type: "mention_processing", mateId: mateId, active: false });
|
|
916
629
|
}
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
loopState.loopFilesId = loopFilesId;
|
|
920
|
-
loopState.wizardData = null;
|
|
921
|
-
activeRegistryId = record.id;
|
|
922
|
-
console.log("[loop-registry] Auto-starting loop: " + record.name + " (" + loopState.loopId + ")");
|
|
923
|
-
send({ type: "schedule_run_started", recordId: record.id });
|
|
924
|
-
startLoop({ maxIterations: record.maxIterations, name: record.name });
|
|
925
|
-
},
|
|
926
|
-
onChange: function () {
|
|
927
|
-
send({ type: "loop_registry_updated", records: getHubSchedules() });
|
|
928
|
-
},
|
|
929
|
-
});
|
|
930
|
-
loopRegistry.load();
|
|
931
|
-
loopRegistry.startTimer();
|
|
932
|
-
|
|
933
|
-
// Wire loop info resolution for session list broadcasts
|
|
934
|
-
sm.setResolveLoopInfo(function (loopId) {
|
|
935
|
-
var rec = loopRegistry.getById(loopId);
|
|
936
|
-
if (!rec) return null;
|
|
937
|
-
return { name: rec.name || null, source: rec.source || null };
|
|
938
|
-
});
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
939
632
|
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
if (!dir) {
|
|
944
|
-
send({ type: "loop_error", text: "No loop directory. Run the wizard first." });
|
|
633
|
+
// --- Debate ---
|
|
634
|
+
if (msg.type === "debate_start") {
|
|
635
|
+
handleDebateStart(ws, msg);
|
|
945
636
|
return;
|
|
946
637
|
}
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
var promptText, judgeText;
|
|
950
|
-
try {
|
|
951
|
-
promptText = fs.readFileSync(promptPath, "utf8");
|
|
952
|
-
} catch (e) {
|
|
953
|
-
send({ type: "loop_error", text: "Missing PROMPT.md in " + dir });
|
|
638
|
+
if (msg.type === "debate_hand_raise") {
|
|
639
|
+
handleDebateHandRaise(ws);
|
|
954
640
|
return;
|
|
955
641
|
}
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
judgeText = null;
|
|
642
|
+
if (msg.type === "debate_comment") {
|
|
643
|
+
handleDebateComment(ws, msg);
|
|
644
|
+
return;
|
|
960
645
|
}
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
try {
|
|
964
|
-
baseCommit = execFileSync("git", ["rev-parse", "HEAD"], {
|
|
965
|
-
cwd: cwd, encoding: "utf8", timeout: 5000,
|
|
966
|
-
}).trim();
|
|
967
|
-
} catch (e) {
|
|
968
|
-
send({ type: "loop_error", text: "Failed to get git HEAD: " + e.message });
|
|
646
|
+
if (msg.type === "debate_stop") {
|
|
647
|
+
handleDebateStop(ws);
|
|
969
648
|
return;
|
|
970
649
|
}
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
var loopConfig = {};
|
|
974
|
-
try {
|
|
975
|
-
loopConfig = JSON.parse(fs.readFileSync(path.join(dir, "LOOP.json"), "utf8"));
|
|
976
|
-
} catch (e) {}
|
|
977
|
-
|
|
978
|
-
loopState.active = true;
|
|
979
|
-
loopState.phase = "executing";
|
|
980
|
-
loopState.promptText = promptText;
|
|
981
|
-
loopState.judgeText = judgeText;
|
|
982
|
-
loopState.iteration = 0;
|
|
983
|
-
loopState.maxIterations = judgeText ? ((loopOpts.maxIterations >= 1 ? loopOpts.maxIterations : null) || loopConfig.maxIterations || 20) : 1;
|
|
984
|
-
loopState.baseCommit = baseCommit;
|
|
985
|
-
loopState.currentSessionId = null;
|
|
986
|
-
loopState.judgeSessionId = null;
|
|
987
|
-
loopState.results = [];
|
|
988
|
-
loopState.stopping = false;
|
|
989
|
-
loopState.name = loopOpts.name || null;
|
|
990
|
-
loopState.startedAt = Date.now();
|
|
991
|
-
saveLoopState();
|
|
992
|
-
|
|
993
|
-
stopClaudeDirWatch();
|
|
994
|
-
|
|
995
|
-
send({ type: "loop_started", maxIterations: loopState.maxIterations, name: loopState.name });
|
|
996
|
-
runNextIteration();
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
function runNextIteration() {
|
|
1000
|
-
console.log("[ralph-loop] runNextIteration called, iteration: " + loopState.iteration + ", active: " + loopState.active + ", stopping: " + loopState.stopping);
|
|
1001
|
-
if (!loopState.active || loopState.stopping) {
|
|
1002
|
-
finishLoop("stopped");
|
|
650
|
+
if (msg.type === "debate_conclude_response") {
|
|
651
|
+
handleDebateConcludeResponse(ws, msg);
|
|
1003
652
|
return;
|
|
1004
653
|
}
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
if (loopState.iteration > loopState.maxIterations) {
|
|
1008
|
-
finishLoop("max_iterations");
|
|
654
|
+
if (msg.type === "debate_confirm_brief") {
|
|
655
|
+
handleDebateConfirmBrief(ws);
|
|
1009
656
|
return;
|
|
1010
657
|
}
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
iteration: loopState.iteration,
|
|
1027
|
-
maxIterations: loopState.maxIterations,
|
|
1028
|
-
sessionId: session.localId,
|
|
1029
|
-
});
|
|
1030
|
-
|
|
1031
|
-
var coderCompleted = false;
|
|
1032
|
-
session.onQueryComplete = function(completedSession) {
|
|
1033
|
-
if (coderCompleted) return;
|
|
1034
|
-
coderCompleted = true;
|
|
1035
|
-
if (coderWatchdog) { clearTimeout(coderWatchdog); coderWatchdog = null; }
|
|
1036
|
-
console.log("[ralph-loop] Coder #" + loopState.iteration + " onQueryComplete fired, history length: " + completedSession.history.length);
|
|
1037
|
-
if (!loopState.active) { console.log("[ralph-loop] Coder: loopState.active is false, skipping"); return; }
|
|
1038
|
-
// Check if session ended with error
|
|
1039
|
-
var lastItems = completedSession.history.slice(-3);
|
|
1040
|
-
var hadError = false;
|
|
1041
|
-
for (var i = 0; i < lastItems.length; i++) {
|
|
1042
|
-
if (lastItems[i].type === "error" || (lastItems[i].type === "done" && lastItems[i].code === 1)) {
|
|
1043
|
-
hadError = true;
|
|
1044
|
-
break;
|
|
658
|
+
if (msg.type === "debate_proposal_response") {
|
|
659
|
+
// Match the most recent pending proposal (proposalId may not be
|
|
660
|
+
// available on the client since it's not part of the tool input)
|
|
661
|
+
var _dpKeys = Object.keys(_pendingDebateProposals);
|
|
662
|
+
if (_dpKeys.length === 0) return;
|
|
663
|
+
var _dpKey = msg.proposalId || _dpKeys[_dpKeys.length - 1];
|
|
664
|
+
var pending = _pendingDebateProposals[_dpKey];
|
|
665
|
+
if (!pending) return;
|
|
666
|
+
delete _pendingDebateProposals[_dpKey];
|
|
667
|
+
if (msg.action === "start") {
|
|
668
|
+
// Set up debate state on the session, then transition to live
|
|
669
|
+
var _dpSession = getSessionForWs(ws);
|
|
670
|
+
if (_dpSession) {
|
|
671
|
+
var _dpMateId = isMate ? path.basename(cwd) : null;
|
|
672
|
+
handleMcpDebateApproval(_dpSession, pending.briefData, _dpMateId, ws);
|
|
1045
673
|
}
|
|
1046
|
-
|
|
1047
|
-
if (hadError) {
|
|
1048
|
-
loopState.results.push({
|
|
1049
|
-
iteration: loopState.iteration,
|
|
1050
|
-
verdict: "error",
|
|
1051
|
-
summary: "Iteration ended with error",
|
|
1052
|
-
});
|
|
1053
|
-
send({
|
|
1054
|
-
type: "loop_verdict",
|
|
1055
|
-
iteration: loopState.iteration,
|
|
1056
|
-
verdict: "error",
|
|
1057
|
-
summary: "Iteration ended with error, retrying...",
|
|
1058
|
-
});
|
|
1059
|
-
setTimeout(function() { runNextIteration(); }, 2000);
|
|
1060
|
-
return;
|
|
1061
|
-
}
|
|
1062
|
-
if (loopState.judgeText && loopState.maxIterations > 1) {
|
|
1063
|
-
runJudge();
|
|
674
|
+
pending.resolve({ action: "start" });
|
|
1064
675
|
} else {
|
|
1065
|
-
|
|
1066
|
-
}
|
|
1067
|
-
};
|
|
1068
|
-
|
|
1069
|
-
// Watchdog: if onQueryComplete hasn't fired after 10 minutes, force error and retry
|
|
1070
|
-
var coderWatchdog = setTimeout(function() {
|
|
1071
|
-
if (!coderCompleted && loopState.active && !loopState.stopping) {
|
|
1072
|
-
console.error("[ralph-loop] Coder #" + loopState.iteration + " watchdog triggered — onQueryComplete never fired");
|
|
1073
|
-
coderCompleted = true;
|
|
1074
|
-
loopState.results.push({
|
|
1075
|
-
iteration: loopState.iteration,
|
|
1076
|
-
verdict: "error",
|
|
1077
|
-
summary: "Coder session timed out (no completion signal)",
|
|
1078
|
-
});
|
|
1079
|
-
send({
|
|
1080
|
-
type: "loop_verdict",
|
|
1081
|
-
iteration: loopState.iteration,
|
|
1082
|
-
verdict: "error",
|
|
1083
|
-
summary: "Coder session timed out, retrying...",
|
|
1084
|
-
});
|
|
1085
|
-
setTimeout(function() { runNextIteration(); }, 2000);
|
|
676
|
+
pending.resolve({ action: "cancel" });
|
|
1086
677
|
}
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
sm.appendToSessionFile(session, userMsg);
|
|
1092
|
-
|
|
1093
|
-
session.isProcessing = true;
|
|
1094
|
-
onProcessingChanged();
|
|
1095
|
-
session.sentToolResults = {};
|
|
1096
|
-
sendToSession(session.localId, { type: "status", status: "processing" });
|
|
1097
|
-
session.acceptEditsAfterStart = true;
|
|
1098
|
-
session.singleTurn = true;
|
|
1099
|
-
sdk.startQuery(session, loopState.promptText, undefined, getLinuxUserForSession(session));
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
function runJudge() {
|
|
1103
|
-
if (!loopState.active || loopState.stopping) {
|
|
1104
|
-
finishLoop("stopped");
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
if (msg.type === "debate_user_floor_response") {
|
|
681
|
+
handleDebateUserFloorResponse(ws, msg);
|
|
1105
682
|
return;
|
|
1106
683
|
}
|
|
1107
684
|
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
diff = execFileSync("git", ["diff", loopState.baseCommit], {
|
|
1111
|
-
cwd: cwd, encoding: "utf8", timeout: 30000,
|
|
1112
|
-
maxBuffer: 10 * 1024 * 1024,
|
|
1113
|
-
});
|
|
1114
|
-
} catch (e) {
|
|
1115
|
-
send({ type: "loop_error", text: "Failed to generate git diff: " + e.message });
|
|
1116
|
-
finishLoop("error");
|
|
1117
|
-
return;
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
var gitLog = "";
|
|
1121
|
-
try {
|
|
1122
|
-
gitLog = execFileSync("git", ["log", "--oneline", loopState.baseCommit + "..HEAD"], {
|
|
1123
|
-
cwd: cwd, encoding: "utf8", timeout: 10000,
|
|
1124
|
-
}).trim();
|
|
1125
|
-
} catch (e) {}
|
|
1126
|
-
|
|
1127
|
-
var judgePrompt = "You are a judge evaluating whether a coding task has been completed.\n\n" +
|
|
1128
|
-
"## Original Task (PROMPT.md)\n\n" + loopState.promptText + "\n\n" +
|
|
1129
|
-
"## Evaluation Criteria (JUDGE.md)\n\n" + loopState.judgeText + "\n\n" +
|
|
1130
|
-
"## Commit History\n\n```\n" + (gitLog || "(no commits yet)") + "\n```\n\n" +
|
|
1131
|
-
"## Changes Made (git diff)\n\n```diff\n" + diff + "\n```\n\n" +
|
|
1132
|
-
"Based on the evaluation criteria, has the task been completed successfully?\n\n" +
|
|
1133
|
-
"IMPORTANT: The git diff above may not show everything. If criteria involve checking whether " +
|
|
1134
|
-
"specific files, classes, or features exist, use tools (Read, Glob, Grep, Bash) to verify " +
|
|
1135
|
-
"directly in the codebase. Do NOT assume something is missing just because it is not in the diff.\n\n" +
|
|
1136
|
-
"After your evaluation, respond with exactly one of:\n" +
|
|
1137
|
-
"- PASS: [brief explanation]\n" +
|
|
1138
|
-
"- FAIL: [brief explanation of what is still missing]";
|
|
1139
|
-
|
|
1140
|
-
var judgeSession = sm.createSession();
|
|
1141
|
-
var judgeSource = loopRegistry.getById(loopState.loopId);
|
|
1142
|
-
var judgeName = (loopState.wizardData && loopState.wizardData.name) || (judgeSource && judgeSource.name) || "";
|
|
1143
|
-
var judgeSourceTag = (judgeSource && judgeSource.source) || null;
|
|
1144
|
-
var isRalphJudge = judgeSourceTag === "ralph";
|
|
1145
|
-
judgeSession.loop = { active: true, iteration: loopState.iteration, role: "judge", loopId: loopState.loopId, name: judgeName, source: judgeSourceTag, startedAt: loopState.startedAt };
|
|
1146
|
-
judgeSession.title = (isRalphJudge ? "Ralph" : "Task") + (judgeName ? " " + judgeName : "") + " Judge #" + loopState.iteration;
|
|
1147
|
-
sm.saveSessionFile(judgeSession);
|
|
1148
|
-
sm.broadcastSessionList();
|
|
1149
|
-
loopState.judgeSessionId = judgeSession.localId;
|
|
1150
|
-
|
|
1151
|
-
send({
|
|
1152
|
-
type: "loop_judging",
|
|
1153
|
-
iteration: loopState.iteration,
|
|
1154
|
-
sessionId: judgeSession.localId,
|
|
1155
|
-
});
|
|
1156
|
-
|
|
1157
|
-
var judgeCompleted = false;
|
|
1158
|
-
judgeSession.onQueryComplete = function(completedSession) {
|
|
1159
|
-
if (judgeCompleted) return;
|
|
1160
|
-
judgeCompleted = true;
|
|
1161
|
-
if (judgeWatchdog) { clearTimeout(judgeWatchdog); judgeWatchdog = null; }
|
|
1162
|
-
console.log("[ralph-loop] Judge #" + loopState.iteration + " onQueryComplete fired, history length: " + completedSession.history.length);
|
|
1163
|
-
var verdict = parseJudgeVerdict(completedSession);
|
|
1164
|
-
console.log("[ralph-loop] Judge verdict: " + (verdict.pass ? "PASS" : "FAIL") + " - " + verdict.explanation);
|
|
1165
|
-
|
|
1166
|
-
loopState.results.push({
|
|
1167
|
-
iteration: loopState.iteration,
|
|
1168
|
-
verdict: verdict.pass ? "pass" : "fail",
|
|
1169
|
-
summary: verdict.explanation,
|
|
1170
|
-
});
|
|
1171
|
-
|
|
1172
|
-
send({
|
|
1173
|
-
type: "loop_verdict",
|
|
1174
|
-
iteration: loopState.iteration,
|
|
1175
|
-
verdict: verdict.pass ? "pass" : "fail",
|
|
1176
|
-
summary: verdict.explanation,
|
|
1177
|
-
});
|
|
1178
|
-
|
|
1179
|
-
if (verdict.pass) {
|
|
1180
|
-
finishLoop("pass");
|
|
1181
|
-
} else {
|
|
1182
|
-
setTimeout(function() { runNextIteration(); }, 1000);
|
|
1183
|
-
}
|
|
1184
|
-
};
|
|
1185
|
-
|
|
1186
|
-
// Watchdog: judge may use tools to verify, so allow more time
|
|
1187
|
-
var judgeWatchdog = setTimeout(function() {
|
|
1188
|
-
if (!judgeCompleted && loopState.active && !loopState.stopping) {
|
|
1189
|
-
console.error("[ralph-loop] Judge #" + loopState.iteration + " watchdog triggered — onQueryComplete never fired");
|
|
1190
|
-
judgeCompleted = true;
|
|
1191
|
-
loopState.results.push({
|
|
1192
|
-
iteration: loopState.iteration,
|
|
1193
|
-
verdict: "error",
|
|
1194
|
-
summary: "Judge session timed out (no completion signal)",
|
|
1195
|
-
});
|
|
1196
|
-
send({
|
|
1197
|
-
type: "loop_verdict",
|
|
1198
|
-
iteration: loopState.iteration,
|
|
1199
|
-
verdict: "error",
|
|
1200
|
-
summary: "Judge session timed out, retrying...",
|
|
1201
|
-
});
|
|
1202
|
-
setTimeout(function() { runNextIteration(); }, 2000);
|
|
1203
|
-
}
|
|
1204
|
-
}, 10 * 60 * 1000);
|
|
1205
|
-
|
|
1206
|
-
var userMsg = { type: "user_message", text: judgePrompt };
|
|
1207
|
-
judgeSession.history.push(userMsg);
|
|
1208
|
-
sm.appendToSessionFile(judgeSession, userMsg);
|
|
1209
|
-
|
|
1210
|
-
judgeSession.isProcessing = true;
|
|
1211
|
-
onProcessingChanged();
|
|
1212
|
-
judgeSession.sentToolResults = {};
|
|
1213
|
-
judgeSession.acceptEditsAfterStart = true;
|
|
1214
|
-
judgeSession.singleTurn = true;
|
|
1215
|
-
sdk.startQuery(judgeSession, judgePrompt, undefined, getLinuxUserForSession(judgeSession));
|
|
1216
|
-
}
|
|
1217
|
-
|
|
1218
|
-
function parseJudgeVerdict(session) {
|
|
1219
|
-
var text = "";
|
|
1220
|
-
for (var i = 0; i < session.history.length; i++) {
|
|
1221
|
-
var h = session.history[i];
|
|
1222
|
-
if (h.type === "delta" && h.text) text += h.text;
|
|
1223
|
-
if (h.type === "text" && h.text) text += h.text;
|
|
1224
|
-
}
|
|
1225
|
-
console.log("[ralph-loop] Judge raw text (last 500 chars): " + text.slice(-500));
|
|
1226
|
-
var upper = text.toUpperCase();
|
|
1227
|
-
var passIdx = upper.indexOf("PASS");
|
|
1228
|
-
var failIdx = upper.indexOf("FAIL");
|
|
1229
|
-
if (passIdx !== -1 && (failIdx === -1 || passIdx < failIdx)) {
|
|
1230
|
-
var explanation = text.substring(passIdx + 4).replace(/^[\s:]+/, "").split("\n")[0].trim();
|
|
1231
|
-
return { pass: true, explanation: explanation || "Task completed" };
|
|
1232
|
-
}
|
|
1233
|
-
if (failIdx !== -1) {
|
|
1234
|
-
var explanation = text.substring(failIdx + 4).replace(/^[\s:]+/, "").split("\n")[0].trim();
|
|
1235
|
-
return { pass: false, explanation: explanation || "Task not yet complete" };
|
|
1236
|
-
}
|
|
1237
|
-
return { pass: false, explanation: "Could not parse judge verdict" };
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
|
-
function finishLoop(reason) {
|
|
1241
|
-
console.log("[ralph-loop] finishLoop called, reason: " + reason + ", iteration: " + loopState.iteration);
|
|
1242
|
-
loopState.active = false;
|
|
1243
|
-
loopState.phase = "done";
|
|
1244
|
-
loopState.stopping = false;
|
|
1245
|
-
loopState.currentSessionId = null;
|
|
1246
|
-
loopState.judgeSessionId = null;
|
|
1247
|
-
saveLoopState();
|
|
1248
|
-
|
|
1249
|
-
send({
|
|
1250
|
-
type: "loop_finished",
|
|
1251
|
-
reason: reason,
|
|
1252
|
-
iterations: loopState.iteration,
|
|
1253
|
-
results: loopState.results,
|
|
1254
|
-
});
|
|
1255
|
-
|
|
1256
|
-
// Record result in loop registry
|
|
1257
|
-
if (loopState.loopId) {
|
|
1258
|
-
loopRegistry.recordRun(loopState.loopId, {
|
|
1259
|
-
reason: reason,
|
|
1260
|
-
startedAt: loopState.startedAt,
|
|
1261
|
-
iterations: loopState.iteration,
|
|
1262
|
-
});
|
|
1263
|
-
}
|
|
1264
|
-
if (activeRegistryId) {
|
|
1265
|
-
send({ type: "schedule_run_finished", recordId: activeRegistryId, reason: reason, iterations: loopState.iteration });
|
|
1266
|
-
activeRegistryId = null;
|
|
1267
|
-
}
|
|
1268
|
-
|
|
1269
|
-
if (pushModule) {
|
|
1270
|
-
var body = reason === "pass"
|
|
1271
|
-
? "Task completed after " + loopState.iteration + " iteration(s)"
|
|
1272
|
-
: reason === "max_iterations"
|
|
1273
|
-
? "Reached max iterations (" + loopState.maxIterations + ")"
|
|
1274
|
-
: reason === "stopped"
|
|
1275
|
-
? "Loop stopped by user"
|
|
1276
|
-
: "Loop ended due to error";
|
|
1277
|
-
pushModule.sendPush({
|
|
1278
|
-
type: "done",
|
|
1279
|
-
slug: slug,
|
|
1280
|
-
title: "Ralph Loop Complete",
|
|
1281
|
-
body: body,
|
|
1282
|
-
tag: "ralph-loop-done",
|
|
1283
|
-
});
|
|
1284
|
-
}
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
function resumeLoop() {
|
|
1288
|
-
var dir = loopDir();
|
|
1289
|
-
if (!dir) {
|
|
1290
|
-
console.error("[ralph-loop] Cannot resume: no loop directory");
|
|
1291
|
-
loopState.active = false;
|
|
1292
|
-
loopState.phase = "idle";
|
|
1293
|
-
saveLoopState();
|
|
1294
|
-
return;
|
|
1295
|
-
}
|
|
1296
|
-
try {
|
|
1297
|
-
loopState.promptText = fs.readFileSync(path.join(dir, "PROMPT.md"), "utf8");
|
|
1298
|
-
} catch (e) {
|
|
1299
|
-
console.error("[ralph-loop] Cannot resume: missing PROMPT.md");
|
|
1300
|
-
loopState.active = false;
|
|
1301
|
-
loopState.phase = "idle";
|
|
1302
|
-
saveLoopState();
|
|
1303
|
-
return;
|
|
1304
|
-
}
|
|
1305
|
-
try {
|
|
1306
|
-
loopState.judgeText = fs.readFileSync(path.join(dir, "JUDGE.md"), "utf8");
|
|
1307
|
-
} catch (e) {
|
|
1308
|
-
console.error("[ralph-loop] Cannot resume: missing JUDGE.md");
|
|
1309
|
-
loopState.active = false;
|
|
1310
|
-
loopState.phase = "idle";
|
|
1311
|
-
saveLoopState();
|
|
1312
|
-
return;
|
|
1313
|
-
}
|
|
1314
|
-
// Retry the interrupted iteration (runNextIteration will increment)
|
|
1315
|
-
if (loopState.iteration > 0) {
|
|
1316
|
-
loopState.iteration--;
|
|
1317
|
-
}
|
|
1318
|
-
console.log("[ralph-loop] Resuming loop, next iteration will be " + (loopState.iteration + 1) + "/" + loopState.maxIterations);
|
|
1319
|
-
send({ type: "loop_started", maxIterations: loopState.maxIterations });
|
|
1320
|
-
runNextIteration();
|
|
1321
|
-
}
|
|
1322
|
-
|
|
1323
|
-
function stopLoop() {
|
|
1324
|
-
if (!loopState.active) return;
|
|
1325
|
-
console.log("[ralph-loop] stopLoop called");
|
|
1326
|
-
loopState.stopping = true;
|
|
1327
|
-
|
|
1328
|
-
// Abort all loop-related sessions (coder + judge)
|
|
1329
|
-
var sessionIds = [loopState.currentSessionId, loopState.judgeSessionId];
|
|
1330
|
-
for (var i = 0; i < sessionIds.length; i++) {
|
|
1331
|
-
if (sessionIds[i] == null) continue;
|
|
1332
|
-
var s = sm.sessions.get(sessionIds[i]);
|
|
1333
|
-
if (!s) continue;
|
|
1334
|
-
// End message queue so SDK exits prompt wait
|
|
1335
|
-
if (s.messageQueue) { try { s.messageQueue.end(); } catch (e) {} }
|
|
1336
|
-
// Abort active API call
|
|
1337
|
-
if (s.abortController) { try { s.abortController.abort(); } catch (e) {} }
|
|
1338
|
-
}
|
|
1339
|
-
|
|
1340
|
-
send({ type: "loop_stopping" });
|
|
1341
|
-
|
|
1342
|
-
// Fallback: force finish if onQueryComplete hasn't fired after 5s
|
|
1343
|
-
setTimeout(function() {
|
|
1344
|
-
if (loopState.active && loopState.stopping) {
|
|
1345
|
-
console.log("[ralph-loop] Stop fallback triggered — forcing finishLoop");
|
|
1346
|
-
finishLoop("stopped");
|
|
1347
|
-
}
|
|
1348
|
-
}, 5000);
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
|
-
// --- Terminal manager ---
|
|
1352
|
-
var tm = createTerminalManager({ cwd: cwd, send: send, sendTo: sendTo });
|
|
1353
|
-
var nm = createNotesManager({ cwd: cwd, send: send, sendTo: sendTo });
|
|
1354
|
-
|
|
1355
|
-
// Check for updates in background (admin only)
|
|
1356
|
-
fetchVersion(updateChannel).then(function (v) {
|
|
1357
|
-
if (v && isNewer(v, currentVersion)) {
|
|
1358
|
-
latestVersion = v;
|
|
1359
|
-
sendToAdmins({ type: "update_available", version: v });
|
|
1360
|
-
}
|
|
1361
|
-
}).catch(function (e) {
|
|
1362
|
-
console.error("[project] Background version check failed:", e.message || e);
|
|
1363
|
-
});
|
|
1364
|
-
|
|
1365
|
-
// --- WS connection handler ---
|
|
1366
|
-
function handleConnection(ws, wsUser) {
|
|
1367
|
-
ws._clayUser = wsUser || null;
|
|
1368
|
-
clients.add(ws);
|
|
1369
|
-
broadcastClientCount();
|
|
1370
|
-
|
|
1371
|
-
// Resume loop if server restarted mid-execution (deferred so client gets initial state first)
|
|
1372
|
-
if (loopState._needsResume) {
|
|
1373
|
-
delete loopState._needsResume;
|
|
1374
|
-
setTimeout(function() { resumeLoop(); }, 500);
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
|
-
// Auto-assign owner if project has none and a user connects (e.g. IPC-added projects)
|
|
1378
|
-
if (!projectOwnerId && ws._clayUser && ws._clayUser.id && !isMate) {
|
|
1379
|
-
projectOwnerId = ws._clayUser.id;
|
|
1380
|
-
if (opts.onProjectOwnerChanged) {
|
|
1381
|
-
opts.onProjectOwnerChanged(slug, projectOwnerId);
|
|
1382
|
-
}
|
|
1383
|
-
console.log("[project] Auto-assigned owner for " + slug + ": " + projectOwnerId);
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1386
|
-
// Send cached state
|
|
1387
|
-
var _userId = ws._clayUser ? ws._clayUser.id : null;
|
|
1388
|
-
var _filteredProjects = getProjectList(_userId);
|
|
1389
|
-
sendTo(ws, { type: "info", cwd: cwd, slug: slug, project: title || project, version: currentVersion, debug: !!debug, dangerouslySkipPermissions: dangerouslySkipPermissions, osUsers: osUsers, lanHost: lanHost, projectCount: _filteredProjects.length, projects: _filteredProjects, projectOwnerId: projectOwnerId });
|
|
1390
|
-
if (latestVersion && ws._clayUser && ws._clayUser.role === "admin") {
|
|
1391
|
-
sendTo(ws, { type: "update_available", version: latestVersion });
|
|
1392
|
-
}
|
|
1393
|
-
if (sm.slashCommands) {
|
|
1394
|
-
sendTo(ws, { type: "slash_commands", commands: sm.slashCommands });
|
|
1395
|
-
}
|
|
1396
|
-
if (sm.currentModel) {
|
|
1397
|
-
sendTo(ws, { type: "model_info", model: sm.currentModel, models: sm.availableModels || [] });
|
|
1398
|
-
}
|
|
1399
|
-
sendTo(ws, { type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
|
|
1400
|
-
sendTo(ws, { type: "term_list", terminals: tm.list() });
|
|
1401
|
-
// Restore context sources (keep tab: sources — validated against _browserTabList at query time)
|
|
1402
|
-
var restoredSources = loadContextSources(slug);
|
|
1403
|
-
sendTo(ws, { type: "context_sources_state", active: restoredSources });
|
|
1404
|
-
sendTo(ws, { type: "notes_list", notes: nm.list() });
|
|
1405
|
-
sendTo(ws, { type: "loop_registry_updated", records: getHubSchedules() });
|
|
1406
|
-
|
|
1407
|
-
// Ralph Loop availability
|
|
1408
|
-
var hasLoopFiles = false;
|
|
1409
|
-
try {
|
|
1410
|
-
fs.accessSync(path.join(cwd, ".claude", "PROMPT.md"));
|
|
1411
|
-
fs.accessSync(path.join(cwd, ".claude", "JUDGE.md"));
|
|
1412
|
-
hasLoopFiles = true;
|
|
1413
|
-
} catch (e) {}
|
|
1414
|
-
// Also check loop directory files
|
|
1415
|
-
if (!hasLoopFiles && loopState.loopId) {
|
|
1416
|
-
var _avDir = loopDir();
|
|
1417
|
-
if (_avDir) {
|
|
1418
|
-
try {
|
|
1419
|
-
fs.accessSync(path.join(_avDir, "PROMPT.md"));
|
|
1420
|
-
fs.accessSync(path.join(_avDir, "JUDGE.md"));
|
|
1421
|
-
hasLoopFiles = true;
|
|
1422
|
-
} catch (e) {}
|
|
1423
|
-
}
|
|
1424
|
-
}
|
|
1425
|
-
sendTo(ws, {
|
|
1426
|
-
type: "loop_available",
|
|
1427
|
-
available: hasLoopFiles,
|
|
1428
|
-
active: loopState.active,
|
|
1429
|
-
iteration: loopState.iteration,
|
|
1430
|
-
maxIterations: loopState.maxIterations,
|
|
1431
|
-
name: loopState.name || null,
|
|
1432
|
-
});
|
|
1433
|
-
|
|
1434
|
-
// Ralph phase state
|
|
1435
|
-
sendTo(ws, {
|
|
1436
|
-
type: "ralph_phase",
|
|
1437
|
-
phase: loopState.phase,
|
|
1438
|
-
wizardData: loopState.wizardData,
|
|
1439
|
-
craftingSessionId: loopState.craftingSessionId || null,
|
|
1440
|
-
});
|
|
1441
|
-
if (loopState.phase === "crafting" || loopState.phase === "approval") {
|
|
1442
|
-
var _hasPrompt = false;
|
|
1443
|
-
var _hasJudge = false;
|
|
1444
|
-
var _lDir = loopDir();
|
|
1445
|
-
if (_lDir) {
|
|
1446
|
-
try { fs.accessSync(path.join(_lDir, "PROMPT.md")); _hasPrompt = true; } catch (e) {}
|
|
1447
|
-
try { fs.accessSync(path.join(_lDir, "JUDGE.md")); _hasJudge = true; } catch (e) {}
|
|
1448
|
-
}
|
|
1449
|
-
sendTo(ws, {
|
|
1450
|
-
type: "ralph_files_status",
|
|
1451
|
-
promptReady: _hasPrompt,
|
|
1452
|
-
judgeReady: _hasJudge,
|
|
1453
|
-
bothReady: _hasPrompt && _hasJudge,
|
|
1454
|
-
taskId: loopState.loopId,
|
|
1455
|
-
});
|
|
1456
|
-
}
|
|
1457
|
-
|
|
1458
|
-
// Session list (filtered for access control)
|
|
1459
|
-
var allSessions = [].concat(Array.from(sm.sessions.values())).filter(function (s) { return !s.hidden; });
|
|
1460
|
-
if (usersModule.isMultiUser() && wsUser) {
|
|
1461
|
-
allSessions = allSessions.filter(function (s) {
|
|
1462
|
-
return usersModule.canAccessSession(wsUser.id, s, { visibility: "public" });
|
|
1463
|
-
});
|
|
1464
|
-
} else if (!usersModule.isMultiUser()) {
|
|
1465
|
-
allSessions = allSessions.filter(function (s) { return !s.ownerId; });
|
|
1466
|
-
}
|
|
1467
|
-
sendTo(ws, {
|
|
1468
|
-
type: "session_list",
|
|
1469
|
-
sessions: allSessions.map(function (s) {
|
|
1470
|
-
var loop = s.loop ? Object.assign({}, s.loop) : null;
|
|
1471
|
-
if (loop && loop.loopId && loopRegistry) {
|
|
1472
|
-
var rec = loopRegistry.getById(loop.loopId);
|
|
1473
|
-
if (rec) {
|
|
1474
|
-
if (rec.name) loop.name = rec.name;
|
|
1475
|
-
if (rec.source) loop.source = rec.source;
|
|
1476
|
-
}
|
|
1477
|
-
}
|
|
1478
|
-
return {
|
|
1479
|
-
id: s.localId,
|
|
1480
|
-
cliSessionId: s.cliSessionId || null,
|
|
1481
|
-
title: s.title || "New Session",
|
|
1482
|
-
active: s.localId === sm.activeSessionId,
|
|
1483
|
-
isProcessing: s.isProcessing,
|
|
1484
|
-
lastActivity: s.lastActivity || s.createdAt || 0,
|
|
1485
|
-
loop: loop,
|
|
1486
|
-
ownerId: s.ownerId || null,
|
|
1487
|
-
sessionVisibility: s.sessionVisibility || "shared",
|
|
1488
|
-
};
|
|
1489
|
-
}),
|
|
1490
|
-
});
|
|
1491
|
-
|
|
1492
|
-
// Restore active session for this client from server-side presence
|
|
1493
|
-
var active = null;
|
|
1494
|
-
var presenceKey = wsUser ? wsUser.id : "_default";
|
|
1495
|
-
var storedPresence = userPresence.getPresence(slug, presenceKey);
|
|
1496
|
-
if (storedPresence && storedPresence.sessionId) {
|
|
1497
|
-
// Look up stored session by localId
|
|
1498
|
-
if (sm.sessions.has(storedPresence.sessionId)) {
|
|
1499
|
-
active = sm.sessions.get(storedPresence.sessionId);
|
|
1500
|
-
} else {
|
|
1501
|
-
// Try matching by cliSessionId (survives server restarts where localIds change)
|
|
1502
|
-
sm.sessions.forEach(function (s) {
|
|
1503
|
-
if (s.cliSessionId && s.cliSessionId === storedPresence.sessionId) active = s;
|
|
1504
|
-
});
|
|
1505
|
-
}
|
|
1506
|
-
// Validate access
|
|
1507
|
-
if (active && usersModule.isMultiUser() && wsUser) {
|
|
1508
|
-
if (!usersModule.canAccessSession(wsUser.id, active, { visibility: "public" })) active = null;
|
|
1509
|
-
} else if (active && !usersModule.isMultiUser() && active.ownerId) {
|
|
1510
|
-
active = null;
|
|
1511
|
-
}
|
|
1512
|
-
}
|
|
1513
|
-
// Fallback: pick the most recent accessible session
|
|
1514
|
-
if (!active && allSessions.length > 0) {
|
|
1515
|
-
active = allSessions[0];
|
|
1516
|
-
for (var fi = 1; fi < allSessions.length; fi++) {
|
|
1517
|
-
if ((allSessions[fi].lastActivity || 0) > (active.lastActivity || 0)) {
|
|
1518
|
-
active = allSessions[fi];
|
|
1519
|
-
}
|
|
1520
|
-
}
|
|
1521
|
-
}
|
|
1522
|
-
// Auto-create a session if none exist for this client
|
|
1523
|
-
var autoCreated = false;
|
|
1524
|
-
if (!active) {
|
|
1525
|
-
var autoOpts = {};
|
|
1526
|
-
if (wsUser && usersModule.isMultiUser()) autoOpts.ownerId = wsUser.id;
|
|
1527
|
-
active = sm.createSession(autoOpts, ws);
|
|
1528
|
-
autoCreated = true;
|
|
1529
|
-
}
|
|
1530
|
-
if (active && !autoCreated) {
|
|
1531
|
-
// Backfill ownerId for legacy sessions restored without one (multi-user only)
|
|
1532
|
-
if (!active.ownerId && wsUser && usersModule.isMultiUser()) {
|
|
1533
|
-
active.ownerId = wsUser.id;
|
|
1534
|
-
sm.saveSessionFile(active);
|
|
1535
|
-
}
|
|
1536
|
-
ws._clayActiveSession = active.localId;
|
|
1537
|
-
sendTo(ws, { type: "session_switched", id: active.localId, cliSessionId: active.cliSessionId || null, loop: active.loop || null });
|
|
1538
|
-
|
|
1539
|
-
var total = active.history.length;
|
|
1540
|
-
var fromIndex = 0;
|
|
1541
|
-
if (total > sm.HISTORY_PAGE_SIZE) {
|
|
1542
|
-
fromIndex = sm.findTurnBoundary(active.history, Math.max(0, total - sm.HISTORY_PAGE_SIZE));
|
|
1543
|
-
}
|
|
1544
|
-
sendTo(ws, { type: "history_meta", total: total, from: fromIndex });
|
|
1545
|
-
for (var i = fromIndex; i < total; i++) {
|
|
1546
|
-
var _hitem = active.history[i];
|
|
1547
|
-
if (_hitem && (_hitem.type === "mention_user" || _hitem.type === "mention_response")) {
|
|
1548
|
-
console.log("[DEBUG handleConnection] sending mention at index=" + i + " from=" + fromIndex + " total=" + total + " type=" + _hitem.type + " mate=" + (_hitem.mateName || "") + " slug=" + slug);
|
|
1549
|
-
}
|
|
1550
|
-
sendTo(ws, hydrateImageRefs(_hitem));
|
|
1551
|
-
}
|
|
1552
|
-
// Include last result data + cached context usage for accurate restore
|
|
1553
|
-
var _lastUsage = null, _lastModelUsage = null, _lastCost = null, _lastStreamInputTokens = null;
|
|
1554
|
-
for (var _ri = total - 1; _ri >= 0; _ri--) {
|
|
1555
|
-
if (active.history[_ri].type === "result") {
|
|
1556
|
-
var _r = active.history[_ri];
|
|
1557
|
-
_lastUsage = _r.usage || null;
|
|
1558
|
-
_lastModelUsage = _r.modelUsage || null;
|
|
1559
|
-
_lastCost = _r.cost != null ? _r.cost : null;
|
|
1560
|
-
_lastStreamInputTokens = _r.lastStreamInputTokens || null;
|
|
1561
|
-
break;
|
|
1562
|
-
}
|
|
1563
|
-
}
|
|
1564
|
-
sendTo(ws, { type: "history_done", lastUsage: _lastUsage, lastModelUsage: _lastModelUsage, lastCost: _lastCost, lastStreamInputTokens: _lastStreamInputTokens, contextUsage: active.lastContextUsage || null });
|
|
1565
|
-
|
|
1566
|
-
if (active.isProcessing) {
|
|
1567
|
-
sendTo(ws, { type: "status", status: "processing" });
|
|
1568
|
-
}
|
|
1569
|
-
var pendingIds = Object.keys(active.pendingPermissions);
|
|
1570
|
-
for (var pi = 0; pi < pendingIds.length; pi++) {
|
|
1571
|
-
var p = active.pendingPermissions[pendingIds[pi]];
|
|
1572
|
-
sendTo(ws, {
|
|
1573
|
-
type: "permission_request_pending",
|
|
1574
|
-
requestId: p.requestId,
|
|
1575
|
-
toolName: p.toolName,
|
|
1576
|
-
toolInput: p.toolInput,
|
|
1577
|
-
toolUseId: p.toolUseId,
|
|
1578
|
-
decisionReason: p.decisionReason,
|
|
1579
|
-
mateId: p.mateId || undefined,
|
|
1580
|
-
});
|
|
1581
|
-
}
|
|
1582
|
-
}
|
|
1583
|
-
|
|
1584
|
-
// Record presence for this user + send mate DM restore hint if applicable
|
|
1585
|
-
if (active) {
|
|
1586
|
-
userPresence.setPresence(slug, presenceKey, active.localId, storedPresence ? storedPresence.mateDm : null);
|
|
1587
|
-
}
|
|
1588
|
-
if (storedPresence && storedPresence.mateDm && !isMate) {
|
|
1589
|
-
sendTo(ws, { type: "restore_mate_dm", mateId: storedPresence.mateDm });
|
|
1590
|
-
}
|
|
1591
|
-
|
|
1592
|
-
broadcastPresence();
|
|
1593
|
-
|
|
1594
|
-
// Restore debate state and brief watcher if a debate was in progress
|
|
1595
|
-
restoreDebateState(ws);
|
|
1596
|
-
|
|
1597
|
-
ws.on("message", function (raw) {
|
|
1598
|
-
var msg;
|
|
1599
|
-
try { msg = JSON.parse(raw.toString()); } catch (e) { return; }
|
|
1600
|
-
handleMessage(ws, msg);
|
|
1601
|
-
});
|
|
1602
|
-
|
|
1603
|
-
ws.on("close", function () {
|
|
1604
|
-
handleDisconnection(ws);
|
|
1605
|
-
});
|
|
1606
|
-
}
|
|
1607
|
-
|
|
1608
|
-
// --- WS message handler ---
|
|
1609
|
-
function getSessionForWs(ws) {
|
|
1610
|
-
return sm.sessions.get(ws._clayActiveSession) || null;
|
|
1611
|
-
}
|
|
1612
|
-
|
|
1613
|
-
// --- Schedule / cancel a message (used by WS handler and auto-continue) ---
|
|
1614
|
-
function scheduleMessage(session, text, resetsAt) {
|
|
1615
|
-
if (!session || !text || !resetsAt) return;
|
|
1616
|
-
// Cancel any existing scheduled message
|
|
1617
|
-
if (session.scheduledMessage && session.scheduledMessage.timer) {
|
|
1618
|
-
clearTimeout(session.scheduledMessage.timer);
|
|
1619
|
-
}
|
|
1620
|
-
var isPastReset = resetsAt <= Date.now();
|
|
1621
|
-
var schedDelay = isPastReset ? 5000 : Math.max(0, resetsAt - Date.now()) + 60000; // +1min buffer after reset, or 5s for immediate
|
|
1622
|
-
var sendsAt = Date.now() + schedDelay;
|
|
1623
|
-
var schedEntry = {
|
|
1624
|
-
type: "scheduled_message_queued",
|
|
1625
|
-
text: text,
|
|
1626
|
-
resetsAt: sendsAt,
|
|
1627
|
-
scheduledAt: Date.now(),
|
|
1628
|
-
};
|
|
1629
|
-
sm.sendAndRecord(session, schedEntry);
|
|
1630
|
-
session.scheduledMessage = {
|
|
1631
|
-
text: text,
|
|
1632
|
-
resetsAt: resetsAt,
|
|
1633
|
-
timer: setTimeout(function () {
|
|
1634
|
-
session.scheduledMessage = null;
|
|
1635
|
-
if (session.destroying) return;
|
|
1636
|
-
console.log("[project] Scheduled message firing for session " + session.localId);
|
|
1637
|
-
sm.sendAndRecord(session, { type: "scheduled_message_sent" });
|
|
1638
|
-
var schedUserMsg = { type: "user_message", text: text };
|
|
1639
|
-
session.history.push(schedUserMsg);
|
|
1640
|
-
sm.appendToSessionFile(session, schedUserMsg);
|
|
1641
|
-
sendToSession(session.localId, schedUserMsg);
|
|
1642
|
-
session.isProcessing = true;
|
|
1643
|
-
onProcessingChanged();
|
|
1644
|
-
sendToSession(session.localId, { type: "status", status: "processing" });
|
|
1645
|
-
sdk.startQuery(session, text, null, getLinuxUserForSession(session));
|
|
1646
|
-
sm.broadcastSessionList();
|
|
1647
|
-
}, schedDelay),
|
|
1648
|
-
};
|
|
1649
|
-
}
|
|
1650
|
-
|
|
1651
|
-
function cancelScheduledMessage(session) {
|
|
1652
|
-
if (!session) return;
|
|
1653
|
-
if (session.scheduledMessage && session.scheduledMessage.timer) {
|
|
1654
|
-
clearTimeout(session.scheduledMessage.timer);
|
|
1655
|
-
session.scheduledMessage = null;
|
|
1656
|
-
session.rateLimitAutoContinuePending = false;
|
|
1657
|
-
sm.sendAndRecord(session, { type: "scheduled_message_cancelled" });
|
|
1658
|
-
}
|
|
1659
|
-
}
|
|
1660
|
-
|
|
1661
|
-
function handleMessage(ws, msg) {
|
|
1662
|
-
// --- DM messages (delegated to server-level handler) ---
|
|
1663
|
-
if (msg.type === "dm_open" || msg.type === "dm_send" || msg.type === "dm_list" || msg.type === "dm_typing" || msg.type === "dm_add_favorite" || msg.type === "dm_remove_favorite" || msg.type === "mate_create" || msg.type === "mate_list" || msg.type === "mate_delete" || msg.type === "mate_update" || msg.type === "mate_readd_builtin" || msg.type === "mate_list_available_builtins") {
|
|
1664
|
-
if (typeof opts.onDmMessage === "function") {
|
|
1665
|
-
opts.onDmMessage(ws, msg);
|
|
1666
|
-
}
|
|
1667
|
-
return;
|
|
1668
|
-
}
|
|
1669
|
-
|
|
1670
|
-
// --- @Mention: invoke another Mate inline ---
|
|
1671
|
-
if (msg.type === "mention") {
|
|
1672
|
-
handleMention(ws, msg);
|
|
1673
|
-
return;
|
|
1674
|
-
}
|
|
1675
|
-
|
|
1676
|
-
if (msg.type === "mention_stop") {
|
|
1677
|
-
var session = getSessionForWs(ws);
|
|
1678
|
-
if (session && session._mentionInProgress) {
|
|
1679
|
-
// Abort the active mention session for this mate
|
|
1680
|
-
var mateId = msg.mateId;
|
|
1681
|
-
if (mateId && session._mentionSessions && session._mentionSessions[mateId]) {
|
|
1682
|
-
session._mentionSessions[mateId].abort();
|
|
1683
|
-
session._mentionSessions[mateId].close();
|
|
1684
|
-
delete session._mentionSessions[mateId];
|
|
1685
|
-
}
|
|
1686
|
-
session._mentionInProgress = false;
|
|
1687
|
-
sendToSession(session.localId, { type: "mention_done", mateId: mateId, stopped: true });
|
|
1688
|
-
send({ type: "mention_processing", mateId: mateId, active: false });
|
|
1689
|
-
}
|
|
1690
|
-
return;
|
|
1691
|
-
}
|
|
1692
|
-
|
|
1693
|
-
// --- Debate ---
|
|
1694
|
-
if (msg.type === "debate_start") {
|
|
1695
|
-
handleDebateStart(ws, msg);
|
|
1696
|
-
return;
|
|
1697
|
-
}
|
|
1698
|
-
if (msg.type === "debate_hand_raise") {
|
|
1699
|
-
handleDebateHandRaise(ws);
|
|
1700
|
-
return;
|
|
1701
|
-
}
|
|
1702
|
-
if (msg.type === "debate_comment") {
|
|
1703
|
-
handleDebateComment(ws, msg);
|
|
1704
|
-
return;
|
|
1705
|
-
}
|
|
1706
|
-
if (msg.type === "debate_stop") {
|
|
1707
|
-
handleDebateStop(ws);
|
|
1708
|
-
return;
|
|
1709
|
-
}
|
|
1710
|
-
if (msg.type === "debate_conclude_response") {
|
|
1711
|
-
handleDebateConcludeResponse(ws, msg);
|
|
1712
|
-
return;
|
|
1713
|
-
}
|
|
1714
|
-
if (msg.type === "debate_confirm_brief") {
|
|
1715
|
-
handleDebateConfirmBrief(ws);
|
|
1716
|
-
return;
|
|
1717
|
-
}
|
|
1718
|
-
if (msg.type === "debate_proposal_response") {
|
|
1719
|
-
// Match the most recent pending proposal (proposalId may not be
|
|
1720
|
-
// available on the client since it's not part of the tool input)
|
|
1721
|
-
var _dpKeys = Object.keys(_pendingDebateProposals);
|
|
1722
|
-
if (_dpKeys.length === 0) return;
|
|
1723
|
-
var _dpKey = msg.proposalId || _dpKeys[_dpKeys.length - 1];
|
|
1724
|
-
var pending = _pendingDebateProposals[_dpKey];
|
|
1725
|
-
if (!pending) return;
|
|
1726
|
-
delete _pendingDebateProposals[_dpKey];
|
|
1727
|
-
if (msg.action === "start") {
|
|
1728
|
-
// Set up debate state on the session, then transition to live
|
|
1729
|
-
var _dpSession = getSessionForWs(ws);
|
|
1730
|
-
if (_dpSession) {
|
|
1731
|
-
var _dpMateId = isMate ? path.basename(cwd) : null;
|
|
1732
|
-
handleMcpDebateApproval(_dpSession, pending.briefData, _dpMateId, ws);
|
|
1733
|
-
}
|
|
1734
|
-
pending.resolve({ action: "start" });
|
|
1735
|
-
} else {
|
|
1736
|
-
pending.resolve({ action: "cancel" });
|
|
1737
|
-
}
|
|
1738
|
-
return;
|
|
1739
|
-
}
|
|
1740
|
-
if (msg.type === "debate_user_floor_response") {
|
|
1741
|
-
handleDebateUserFloorResponse(ws, msg);
|
|
1742
|
-
return;
|
|
1743
|
-
}
|
|
1744
|
-
|
|
1745
|
-
// --- Knowledge file management ---
|
|
1746
|
-
if (msg.type === "knowledge_list") {
|
|
1747
|
-
var knowledgeDir = path.join(cwd, "knowledge");
|
|
1748
|
-
var files = [];
|
|
1749
|
-
try {
|
|
1750
|
-
var entries = fs.readdirSync(knowledgeDir);
|
|
1751
|
-
for (var ki = 0; ki < entries.length; ki++) {
|
|
1752
|
-
if (entries[ki] === "session-digests.jsonl") continue;
|
|
1753
|
-
if (entries[ki] === "sticky-notes.md") continue;
|
|
1754
|
-
if (entries[ki] === "memory-summary.md") continue;
|
|
1755
|
-
if (entries[ki].endsWith(".md") || entries[ki].endsWith(".jsonl")) {
|
|
1756
|
-
var stat = fs.statSync(path.join(knowledgeDir, entries[ki]));
|
|
1757
|
-
files.push({ name: entries[ki], size: stat.size, mtime: stat.mtimeMs, common: false });
|
|
1758
|
-
}
|
|
1759
|
-
}
|
|
1760
|
-
} catch (e) { /* dir may not exist */ }
|
|
1761
|
-
files.sort(function (a, b) { return b.mtime - a.mtime; });
|
|
1762
|
-
|
|
1763
|
-
// For mate projects, check which files are promoted and include common files from other mates
|
|
1764
|
-
if (isMate) {
|
|
1765
|
-
var mateCtx = matesModule.buildMateCtx(projectOwnerId);
|
|
1766
|
-
var thisMateId = path.basename(cwd);
|
|
1767
|
-
// Tag promoted files
|
|
1768
|
-
for (var pi = 0; pi < files.length; pi++) {
|
|
1769
|
-
files[pi].promoted = matesModule.isPromoted(mateCtx, thisMateId, files[pi].name);
|
|
1770
|
-
}
|
|
1771
|
-
// Get common files from other mates
|
|
1772
|
-
var commonFiles = matesModule.getCommonKnowledgeForMate(mateCtx, thisMateId);
|
|
1773
|
-
// Filter out entries that belong to THIS mate (those are already in the list as promoted)
|
|
1774
|
-
for (var ci = 0; ci < commonFiles.length; ci++) {
|
|
1775
|
-
if (commonFiles[ci].ownMateId !== thisMateId) {
|
|
1776
|
-
files.push(commonFiles[ci]);
|
|
1777
|
-
}
|
|
1778
|
-
}
|
|
1779
|
-
}
|
|
1780
|
-
|
|
1781
|
-
sendTo(ws, { type: "knowledge_list", files: files });
|
|
1782
|
-
return;
|
|
1783
|
-
}
|
|
1784
|
-
|
|
1785
|
-
if (msg.type === "knowledge_read") {
|
|
1786
|
-
if (!msg.name) return;
|
|
1787
|
-
var safeName = path.basename(msg.name);
|
|
1788
|
-
var filePath;
|
|
1789
|
-
if (msg.common && msg.ownMateId && isMate) {
|
|
1790
|
-
// Reading a common file from another mate
|
|
1791
|
-
var mateCtx = matesModule.buildMateCtx(projectOwnerId);
|
|
1792
|
-
try {
|
|
1793
|
-
var content = matesModule.readCommonKnowledgeFile(mateCtx, msg.ownMateId, safeName);
|
|
1794
|
-
sendTo(ws, { type: "knowledge_content", name: safeName, content: content, common: true, ownMateId: msg.ownMateId });
|
|
1795
|
-
} catch (e) {
|
|
1796
|
-
sendTo(ws, { type: "knowledge_content", name: safeName, content: "", error: "File not found", common: true });
|
|
1797
|
-
}
|
|
1798
|
-
} else {
|
|
1799
|
-
filePath = path.join(cwd, "knowledge", safeName);
|
|
1800
|
-
try {
|
|
1801
|
-
var content = fs.readFileSync(filePath, "utf8");
|
|
1802
|
-
sendTo(ws, { type: "knowledge_content", name: safeName, content: content });
|
|
1803
|
-
} catch (e) {
|
|
1804
|
-
sendTo(ws, { type: "knowledge_content", name: safeName, content: "", error: "File not found" });
|
|
1805
|
-
}
|
|
1806
|
-
}
|
|
1807
|
-
return;
|
|
1808
|
-
}
|
|
1809
|
-
|
|
1810
|
-
if (msg.type === "knowledge_save") {
|
|
1811
|
-
if (!msg.name || typeof msg.content !== "string") return;
|
|
1812
|
-
var safeName = path.basename(msg.name);
|
|
1813
|
-
if (!safeName.endsWith(".md") && !safeName.endsWith(".jsonl")) safeName += ".md";
|
|
1814
|
-
var knowledgeDir = path.join(cwd, "knowledge");
|
|
1815
|
-
fs.mkdirSync(knowledgeDir, { recursive: true });
|
|
1816
|
-
fs.writeFileSync(path.join(knowledgeDir, safeName), msg.content);
|
|
1817
|
-
// Return updated list
|
|
1818
|
-
var files = [];
|
|
1819
|
-
try {
|
|
1820
|
-
var entries = fs.readdirSync(knowledgeDir);
|
|
1821
|
-
for (var ki = 0; ki < entries.length; ki++) {
|
|
1822
|
-
if (entries[ki].endsWith(".md") || entries[ki].endsWith(".jsonl")) {
|
|
1823
|
-
var stat = fs.statSync(path.join(knowledgeDir, entries[ki]));
|
|
1824
|
-
files.push({ name: entries[ki], size: stat.size, mtime: stat.mtimeMs });
|
|
1825
|
-
}
|
|
1826
|
-
}
|
|
1827
|
-
} catch (e) {}
|
|
1828
|
-
files.sort(function (a, b) { return b.mtime - a.mtime; });
|
|
1829
|
-
// Tag files for mate projects
|
|
1830
|
-
if (isMate) {
|
|
1831
|
-
var mateCtx = matesModule.buildMateCtx(projectOwnerId);
|
|
1832
|
-
var thisMateId = path.basename(cwd);
|
|
1833
|
-
for (var pi = 0; pi < files.length; pi++) {
|
|
1834
|
-
files[pi].common = false;
|
|
1835
|
-
files[pi].promoted = matesModule.isPromoted(mateCtx, thisMateId, files[pi].name);
|
|
1836
|
-
}
|
|
1837
|
-
var commonFiles = matesModule.getCommonKnowledgeForMate(mateCtx, thisMateId);
|
|
1838
|
-
for (var ci = 0; ci < commonFiles.length; ci++) {
|
|
1839
|
-
if (commonFiles[ci].ownMateId !== thisMateId) files.push(commonFiles[ci]);
|
|
1840
|
-
}
|
|
1841
|
-
}
|
|
1842
|
-
sendTo(ws, { type: "knowledge_saved", name: safeName });
|
|
1843
|
-
sendTo(ws, { type: "knowledge_list", files: files });
|
|
1844
|
-
return;
|
|
1845
|
-
}
|
|
1846
|
-
|
|
1847
|
-
if (msg.type === "knowledge_delete") {
|
|
1848
|
-
if (!msg.name) return;
|
|
1849
|
-
var safeName = path.basename(msg.name);
|
|
1850
|
-
var filePath = path.join(cwd, "knowledge", safeName);
|
|
1851
|
-
try { fs.unlinkSync(filePath); } catch (e) {}
|
|
1852
|
-
// Return updated list
|
|
1853
|
-
var knowledgeDir = path.join(cwd, "knowledge");
|
|
1854
|
-
var files = [];
|
|
1855
|
-
try {
|
|
1856
|
-
var entries = fs.readdirSync(knowledgeDir);
|
|
1857
|
-
for (var ki = 0; ki < entries.length; ki++) {
|
|
1858
|
-
if (entries[ki].endsWith(".md") || entries[ki].endsWith(".jsonl")) {
|
|
1859
|
-
var stat = fs.statSync(path.join(knowledgeDir, entries[ki]));
|
|
1860
|
-
files.push({ name: entries[ki], size: stat.size, mtime: stat.mtimeMs });
|
|
1861
|
-
}
|
|
1862
|
-
}
|
|
1863
|
-
} catch (e) {}
|
|
1864
|
-
files.sort(function (a, b) { return b.mtime - a.mtime; });
|
|
1865
|
-
// Tag files for mate projects
|
|
1866
|
-
if (isMate) {
|
|
1867
|
-
var mateCtx = matesModule.buildMateCtx(projectOwnerId);
|
|
1868
|
-
var thisMateId = path.basename(cwd);
|
|
1869
|
-
for (var pi = 0; pi < files.length; pi++) {
|
|
1870
|
-
files[pi].common = false;
|
|
1871
|
-
files[pi].promoted = matesModule.isPromoted(mateCtx, thisMateId, files[pi].name);
|
|
1872
|
-
}
|
|
1873
|
-
var commonFiles = matesModule.getCommonKnowledgeForMate(mateCtx, thisMateId);
|
|
1874
|
-
for (var ci = 0; ci < commonFiles.length; ci++) {
|
|
1875
|
-
if (commonFiles[ci].ownMateId !== thisMateId) files.push(commonFiles[ci]);
|
|
1876
|
-
}
|
|
1877
|
-
}
|
|
1878
|
-
sendTo(ws, { type: "knowledge_deleted", name: safeName });
|
|
1879
|
-
sendTo(ws, { type: "knowledge_list", files: files });
|
|
1880
|
-
return;
|
|
1881
|
-
}
|
|
1882
|
-
|
|
1883
|
-
if (msg.type === "knowledge_promote") {
|
|
1884
|
-
if (!isMate || !msg.name) return;
|
|
1885
|
-
var safeName = path.basename(msg.name);
|
|
1886
|
-
var mateCtx = matesModule.buildMateCtx(projectOwnerId);
|
|
1887
|
-
var thisMateId = path.basename(cwd);
|
|
1888
|
-
var mate = matesModule.getMate(mateCtx, thisMateId);
|
|
1889
|
-
var mateName = (mate && mate.name) || null;
|
|
1890
|
-
matesModule.promoteKnowledge(mateCtx, thisMateId, mateName, safeName);
|
|
1891
|
-
sendTo(ws, { type: "knowledge_promoted", name: safeName });
|
|
1892
|
-
// Re-send updated list (reuse knowledge_list logic)
|
|
1893
|
-
handleMessage(ws, { type: "knowledge_list" });
|
|
1894
|
-
return;
|
|
1895
|
-
}
|
|
1896
|
-
|
|
1897
|
-
if (msg.type === "knowledge_depromote") {
|
|
1898
|
-
if (!isMate || !msg.name) return;
|
|
1899
|
-
var safeName = path.basename(msg.name);
|
|
1900
|
-
var mateCtx = matesModule.buildMateCtx(projectOwnerId);
|
|
1901
|
-
var thisMateId = path.basename(cwd);
|
|
1902
|
-
matesModule.depromoteKnowledge(mateCtx, thisMateId, safeName);
|
|
1903
|
-
sendTo(ws, { type: "knowledge_depromoted", name: safeName });
|
|
1904
|
-
handleMessage(ws, { type: "knowledge_list" });
|
|
1905
|
-
return;
|
|
1906
|
-
}
|
|
1907
|
-
|
|
1908
|
-
// --- Memory (session digests) management (delegated to project-memory.js) ---
|
|
1909
|
-
if (msg.type === "memory_list") { _memory.handleMemoryList(ws); return; }
|
|
1910
|
-
if (msg.type === "memory_search") { _memory.handleMemorySearch(ws, msg); return; }
|
|
1911
|
-
if (msg.type === "memory_delete") { _memory.handleMemoryDelete(ws, msg); return; }
|
|
1912
|
-
|
|
1913
|
-
if (msg.type === "push_subscribe") {
|
|
1914
|
-
var _pushUserId = ws._clayUser ? ws._clayUser.id : null;
|
|
1915
|
-
if (pushModule && msg.subscription) pushModule.addSubscription(msg.subscription, msg.replaceEndpoint, _pushUserId);
|
|
1916
|
-
return;
|
|
1917
|
-
}
|
|
1918
|
-
|
|
1919
|
-
if (msg.type === "load_more_history") {
|
|
1920
|
-
var session = getSessionForWs(ws);
|
|
1921
|
-
if (!session || typeof msg.before !== "number") return;
|
|
1922
|
-
var before = msg.before;
|
|
1923
|
-
var targetFrom = typeof msg.target === "number" ? msg.target : before - sm.HISTORY_PAGE_SIZE;
|
|
1924
|
-
var from = sm.findTurnBoundary(session.history, Math.max(0, targetFrom));
|
|
1925
|
-
var to = before;
|
|
1926
|
-
var items = session.history.slice(from, to).map(hydrateImageRefs);
|
|
1927
|
-
sendTo(ws, {
|
|
1928
|
-
type: "history_prepend",
|
|
1929
|
-
items: items,
|
|
1930
|
-
meta: { from: from, to: to, hasMore: from > 0 },
|
|
1931
|
-
});
|
|
1932
|
-
return;
|
|
1933
|
-
}
|
|
1934
|
-
|
|
1935
|
-
if (msg.type === "new_session") {
|
|
1936
|
-
var sessionOpts = {};
|
|
1937
|
-
if (ws._clayUser && usersModule.isMultiUser()) sessionOpts.ownerId = ws._clayUser.id;
|
|
1938
|
-
if (msg.sessionVisibility) sessionOpts.sessionVisibility = msg.sessionVisibility;
|
|
1939
|
-
var newSess = sm.createSession(sessionOpts, ws);
|
|
1940
|
-
ws._clayActiveSession = newSess.localId;
|
|
1941
|
-
var nsPresKey = ws._clayUser ? ws._clayUser.id : "_default";
|
|
1942
|
-
userPresence.setPresence(slug, nsPresKey, newSess.localId, null);
|
|
1943
|
-
if (usersModule.isMultiUser()) {
|
|
1944
|
-
broadcastPresence();
|
|
1945
|
-
}
|
|
1946
|
-
return;
|
|
1947
|
-
}
|
|
1948
|
-
|
|
1949
|
-
if (msg.type === "set_session_visibility") {
|
|
1950
|
-
if (typeof msg.sessionId === "number" && (msg.visibility === "shared" || msg.visibility === "private")) {
|
|
1951
|
-
sm.setSessionVisibility(msg.sessionId, msg.visibility);
|
|
1952
|
-
}
|
|
1953
|
-
return;
|
|
1954
|
-
}
|
|
1955
|
-
|
|
1956
|
-
if (msg.type === "transfer_project_owner") {
|
|
1957
|
-
var isAdmin = ws._clayUser && ws._clayUser.role === "admin";
|
|
1958
|
-
var isProjectOwner = ws._clayUser && projectOwnerId && ws._clayUser.id === projectOwnerId;
|
|
1959
|
-
if (!ws._clayUser || (!isAdmin && !isProjectOwner)) {
|
|
1960
|
-
sendTo(ws, { type: "error", text: "Only project owners or admins can transfer ownership." });
|
|
1961
|
-
return;
|
|
1962
|
-
}
|
|
1963
|
-
var targetUser = msg.userId ? usersModule.findUserById(msg.userId) : null;
|
|
1964
|
-
if (!targetUser) {
|
|
1965
|
-
sendTo(ws, { type: "error", text: "User not found." });
|
|
1966
|
-
return;
|
|
1967
|
-
}
|
|
1968
|
-
projectOwnerId = targetUser.id;
|
|
1969
|
-
// Persist via daemon callback
|
|
1970
|
-
if (opts.onProjectOwnerChanged) {
|
|
1971
|
-
opts.onProjectOwnerChanged(slug, projectOwnerId);
|
|
1972
|
-
}
|
|
1973
|
-
send({ type: "project_owner_changed", ownerId: projectOwnerId, ownerName: targetUser.displayName || targetUser.username });
|
|
1974
|
-
return;
|
|
1975
|
-
}
|
|
1976
|
-
|
|
1977
|
-
if (msg.type === "resume_session") {
|
|
1978
|
-
if (!msg.cliSessionId) return;
|
|
1979
|
-
var cliSess = require("./cli-sessions");
|
|
1980
|
-
// Try SDK for title first, then fall back to manual parsing
|
|
1981
|
-
var titlePromise = getSDK().then(function(sdkMod) {
|
|
1982
|
-
return sdkMod.getSessionInfo(msg.cliSessionId, { dir: cwd });
|
|
1983
|
-
}).then(function(info) {
|
|
1984
|
-
return (info && info.summary) ? info.summary.substring(0, 100) : null;
|
|
1985
|
-
}).catch(function() { return null; });
|
|
1986
|
-
|
|
1987
|
-
Promise.all([
|
|
1988
|
-
cliSess.readCliSessionHistory(cwd, msg.cliSessionId),
|
|
1989
|
-
titlePromise
|
|
1990
|
-
]).then(function(results) {
|
|
1991
|
-
var history = results[0];
|
|
1992
|
-
var sdkTitle = results[1];
|
|
1993
|
-
var title = sdkTitle || "Resumed session";
|
|
1994
|
-
if (!sdkTitle) {
|
|
1995
|
-
for (var i = 0; i < history.length; i++) {
|
|
1996
|
-
if (history[i].type === "user_message" && history[i].text) {
|
|
1997
|
-
title = history[i].text.substring(0, 50);
|
|
1998
|
-
break;
|
|
1999
|
-
}
|
|
2000
|
-
}
|
|
2001
|
-
}
|
|
2002
|
-
var resumed = sm.resumeSession(msg.cliSessionId, { history: history, title: title }, ws);
|
|
2003
|
-
if (resumed) ws._clayActiveSession = resumed.localId;
|
|
2004
|
-
}).catch(function() {
|
|
2005
|
-
var resumed = sm.resumeSession(msg.cliSessionId, undefined, ws);
|
|
2006
|
-
if (resumed) ws._clayActiveSession = resumed.localId;
|
|
2007
|
-
});
|
|
2008
|
-
return;
|
|
2009
|
-
}
|
|
2010
|
-
|
|
2011
|
-
if (msg.type === "list_cli_sessions") {
|
|
2012
|
-
var _fs = require("fs");
|
|
2013
|
-
// Collect session IDs already in relay (in-memory + persisted on disk)
|
|
2014
|
-
var relayIds = {};
|
|
2015
|
-
sm.sessions.forEach(function (s) {
|
|
2016
|
-
if (s.cliSessionId) relayIds[s.cliSessionId] = true;
|
|
2017
|
-
});
|
|
2018
|
-
try {
|
|
2019
|
-
var sessDir = sm.sessionsDir;
|
|
2020
|
-
var diskFiles = _fs.readdirSync(sessDir);
|
|
2021
|
-
for (var fi = 0; fi < diskFiles.length; fi++) {
|
|
2022
|
-
if (diskFiles[fi].endsWith(".jsonl")) {
|
|
2023
|
-
relayIds[diskFiles[fi].replace(".jsonl", "")] = true;
|
|
2024
|
-
}
|
|
2025
|
-
}
|
|
2026
|
-
} catch (e) {}
|
|
2027
|
-
|
|
2028
|
-
getSDK().then(function(sdkMod) {
|
|
2029
|
-
return sdkMod.listSessions({ dir: cwd });
|
|
2030
|
-
}).then(function(sdkSessions) {
|
|
2031
|
-
var filtered = sdkSessions.filter(function(s) {
|
|
2032
|
-
return !relayIds[s.sessionId];
|
|
2033
|
-
}).map(function(s) {
|
|
2034
|
-
return {
|
|
2035
|
-
sessionId: s.sessionId,
|
|
2036
|
-
firstPrompt: s.summary || s.firstPrompt || "",
|
|
2037
|
-
model: null,
|
|
2038
|
-
gitBranch: s.gitBranch || null,
|
|
2039
|
-
startTime: s.createdAt ? new Date(s.createdAt).toISOString() : null,
|
|
2040
|
-
lastActivity: s.lastModified ? new Date(s.lastModified).toISOString() : null,
|
|
2041
|
-
};
|
|
2042
|
-
});
|
|
2043
|
-
sendTo(ws, { type: "cli_session_list", sessions: filtered });
|
|
2044
|
-
}).catch(function() {
|
|
2045
|
-
// Fallback to manual parsing if SDK fails
|
|
2046
|
-
var cliSessions = require("./cli-sessions");
|
|
2047
|
-
cliSessions.listCliSessions(cwd).then(function(sessions) {
|
|
2048
|
-
var filtered = sessions.filter(function(s) {
|
|
2049
|
-
return !relayIds[s.sessionId];
|
|
2050
|
-
});
|
|
2051
|
-
sendTo(ws, { type: "cli_session_list", sessions: filtered });
|
|
2052
|
-
}).catch(function() {
|
|
2053
|
-
sendTo(ws, { type: "cli_session_list", sessions: [] });
|
|
2054
|
-
});
|
|
2055
|
-
});
|
|
2056
|
-
return;
|
|
2057
|
-
}
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
if (msg.type === "switch_session") {
|
|
2061
|
-
if (msg.id && sm.sessions.has(msg.id)) {
|
|
2062
|
-
// Check access in multi-user mode
|
|
2063
|
-
if (usersModule.isMultiUser() && ws._clayUser) {
|
|
2064
|
-
var switchTarget = sm.sessions.get(msg.id);
|
|
2065
|
-
if (!usersModule.canAccessSession(ws._clayUser.id, switchTarget, { visibility: "public" })) return;
|
|
2066
|
-
ws._clayActiveSession = msg.id;
|
|
2067
|
-
sm.switchSession(msg.id, ws, hydrateImageRefs);
|
|
2068
|
-
broadcastPresence();
|
|
2069
|
-
} else {
|
|
2070
|
-
ws._clayActiveSession = msg.id;
|
|
2071
|
-
sm.switchSession(msg.id, ws, hydrateImageRefs);
|
|
2072
|
-
}
|
|
2073
|
-
var swPresKey = ws._clayUser ? ws._clayUser.id : "_default";
|
|
2074
|
-
userPresence.setPresence(slug, swPresKey, msg.id, null);
|
|
2075
|
-
}
|
|
2076
|
-
return;
|
|
2077
|
-
}
|
|
2078
|
-
|
|
2079
|
-
if (msg.type === "set_mate_dm") {
|
|
2080
|
-
// Only store mateDm on non-mate projects (main project presence).
|
|
2081
|
-
// Mate projects should never hold mateDm to avoid circular restore loops.
|
|
2082
|
-
if (!isMate) {
|
|
2083
|
-
var dmPresKey = ws._clayUser ? ws._clayUser.id : "_default";
|
|
2084
|
-
userPresence.setMateDm(slug, dmPresKey, msg.mateId || null);
|
|
2085
|
-
}
|
|
2086
|
-
return;
|
|
2087
|
-
}
|
|
2088
|
-
|
|
2089
|
-
if (msg.type === "delete_session") {
|
|
2090
|
-
if (ws._clayUser) {
|
|
2091
|
-
var sdPerms = usersModule.getEffectivePermissions(ws._clayUser, osUsers);
|
|
2092
|
-
if (!sdPerms.sessionDelete) {
|
|
2093
|
-
sendTo(ws, { type: "error", text: "You do not have permission to delete sessions" });
|
|
2094
|
-
return;
|
|
2095
|
-
}
|
|
2096
|
-
}
|
|
2097
|
-
if (msg.id && sm.sessions.has(msg.id)) {
|
|
2098
|
-
sm.deleteSession(msg.id, ws);
|
|
2099
|
-
}
|
|
2100
|
-
return;
|
|
2101
|
-
}
|
|
2102
|
-
|
|
2103
|
-
if (msg.type === "rename_session") {
|
|
2104
|
-
if (msg.id && sm.sessions.has(msg.id) && msg.title) {
|
|
2105
|
-
var s = sm.sessions.get(msg.id);
|
|
2106
|
-
s.title = String(msg.title).substring(0, 100);
|
|
2107
|
-
sm.saveSessionFile(s);
|
|
2108
|
-
sm.broadcastSessionList();
|
|
2109
|
-
// Sync title to SDK session
|
|
2110
|
-
if (s.cliSessionId) {
|
|
2111
|
-
getSDK().then(function(sdk) {
|
|
2112
|
-
sdk.renameSession(s.cliSessionId, s.title, { dir: cwd }).catch(function(e) {
|
|
2113
|
-
console.error("[project] SDK renameSession failed:", e.message);
|
|
2114
|
-
});
|
|
2115
|
-
}).catch(function() {});
|
|
2116
|
-
}
|
|
2117
|
-
}
|
|
2118
|
-
return;
|
|
2119
|
-
}
|
|
2120
|
-
|
|
2121
|
-
if (msg.type === "search_sessions") {
|
|
2122
|
-
var results = sm.searchSessions(msg.query || "");
|
|
2123
|
-
sendTo(ws, { type: "search_results", query: msg.query || "", results: results });
|
|
2124
|
-
return;
|
|
2125
|
-
}
|
|
2126
|
-
|
|
2127
|
-
if (msg.type === "search_session_content") {
|
|
2128
|
-
var targetSession = msg.id ? sm.sessions.get(msg.id) : getSessionForWs(ws);
|
|
2129
|
-
if (!targetSession) return;
|
|
2130
|
-
var contentResults = sm.searchSessionContent(targetSession.localId, msg.query || "");
|
|
2131
|
-
var searchResp = { type: "search_content_results", query: msg.query || "", sessionId: targetSession.localId, hits: contentResults.hits, total: contentResults.total };
|
|
2132
|
-
if (msg.source) searchResp.source = msg.source;
|
|
2133
|
-
sendTo(ws, searchResp);
|
|
2134
|
-
return;
|
|
2135
|
-
}
|
|
2136
|
-
|
|
2137
|
-
if (msg.type === "set_update_channel") {
|
|
2138
|
-
if (usersModule.isMultiUser() && (!ws._clayUser || ws._clayUser.role !== "admin")) return;
|
|
2139
|
-
var newChannel = msg.channel === "beta" ? "beta" : "stable";
|
|
2140
|
-
updateChannel = newChannel;
|
|
2141
|
-
latestVersion = null;
|
|
2142
|
-
if (typeof opts.onSetUpdateChannel === "function") {
|
|
2143
|
-
opts.onSetUpdateChannel(newChannel);
|
|
2144
|
-
}
|
|
2145
|
-
// Re-fetch with new channel and broadcast to admin clients
|
|
2146
|
-
fetchVersion(updateChannel).then(function (v) {
|
|
2147
|
-
if (v && isNewer(v, currentVersion)) {
|
|
2148
|
-
latestVersion = v;
|
|
2149
|
-
sendToAdmins({ type: "update_available", version: v });
|
|
2150
|
-
}
|
|
2151
|
-
}).catch(function () {});
|
|
2152
|
-
return;
|
|
2153
|
-
}
|
|
2154
|
-
|
|
2155
|
-
if (msg.type === "check_update") {
|
|
2156
|
-
if (usersModule.isMultiUser() && (!ws._clayUser || ws._clayUser.role !== "admin")) return;
|
|
2157
|
-
fetchVersion(updateChannel).then(function (v) {
|
|
2158
|
-
if (v && isNewer(v, currentVersion)) {
|
|
2159
|
-
latestVersion = v;
|
|
2160
|
-
sendTo(ws, { type: "update_available", version: v });
|
|
2161
|
-
} else {
|
|
2162
|
-
sendTo(ws, { type: "up_to_date", version: currentVersion });
|
|
2163
|
-
}
|
|
2164
|
-
}).catch(function () {});
|
|
2165
|
-
return;
|
|
2166
|
-
}
|
|
2167
|
-
|
|
2168
|
-
if (msg.type === "update_now") {
|
|
2169
|
-
if (usersModule.isMultiUser() && (!ws._clayUser || ws._clayUser.role !== "admin")) return;
|
|
2170
|
-
send({ type: "update_started", version: latestVersion || "" });
|
|
2171
|
-
var _ipc = require("./ipc");
|
|
2172
|
-
var _config = require("./config");
|
|
2173
|
-
_ipc.sendIPCCommand(_config.socketPath(), { cmd: "update" });
|
|
2174
|
-
return;
|
|
2175
|
-
}
|
|
2176
|
-
|
|
2177
|
-
if (msg.type === "process_stats") {
|
|
2178
|
-
var sessionCount = sm.sessions.size;
|
|
2179
|
-
var processingCount = 0;
|
|
2180
|
-
sm.sessions.forEach(function (s) {
|
|
2181
|
-
if (s.isProcessing) processingCount++;
|
|
2182
|
-
});
|
|
2183
|
-
var mem = process.memoryUsage();
|
|
2184
|
-
sendTo(ws, {
|
|
2185
|
-
type: "process_stats",
|
|
2186
|
-
pid: process.pid,
|
|
2187
|
-
uptime: process.uptime(),
|
|
2188
|
-
memory: {
|
|
2189
|
-
rss: mem.rss,
|
|
2190
|
-
heapUsed: mem.heapUsed,
|
|
2191
|
-
heapTotal: mem.heapTotal,
|
|
2192
|
-
external: mem.external,
|
|
2193
|
-
},
|
|
2194
|
-
sessions: sessionCount,
|
|
2195
|
-
processing: processingCount,
|
|
2196
|
-
clients: clients.size,
|
|
2197
|
-
terminals: tm.list().length,
|
|
2198
|
-
});
|
|
2199
|
-
return;
|
|
2200
|
-
}
|
|
2201
|
-
|
|
2202
|
-
if (msg.type === "stop") {
|
|
2203
|
-
var session = getSessionForWs(ws);
|
|
2204
|
-
if (session && session.abortController && session.isProcessing) {
|
|
2205
|
-
session.abortController.abort();
|
|
2206
|
-
}
|
|
2207
|
-
return;
|
|
2208
|
-
}
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
if (msg.type === "stop_task") {
|
|
2212
|
-
if (msg.taskId) {
|
|
2213
|
-
sdk.stopTask(msg.taskId);
|
|
2214
|
-
}
|
|
2215
|
-
return;
|
|
2216
|
-
}
|
|
2217
|
-
|
|
2218
|
-
if (msg.type === "kill_process") {
|
|
2219
|
-
var pid = msg.pid;
|
|
2220
|
-
if (!pid || typeof pid !== "number") return;
|
|
2221
|
-
// Verify target is actually a claude process before killing
|
|
2222
|
-
if (!sdk.isClaudeProcess(pid)) {
|
|
2223
|
-
console.error("[project] Refused to kill PID " + pid + ": not a claude process");
|
|
2224
|
-
sendTo(ws, { type: "error", text: "Process " + pid + " is not a Claude process." });
|
|
2225
|
-
return;
|
|
2226
|
-
}
|
|
2227
|
-
try {
|
|
2228
|
-
process.kill(pid, "SIGTERM");
|
|
2229
|
-
console.log("[project] Sent SIGTERM to conflicting Claude process PID " + pid);
|
|
2230
|
-
sendTo(ws, { type: "process_killed", pid: pid });
|
|
2231
|
-
} catch (e) {
|
|
2232
|
-
console.error("[project] Failed to kill PID " + pid + ":", e.message);
|
|
2233
|
-
sendTo(ws, { type: "error", text: "Failed to kill process " + pid + ": " + (e.message || e) });
|
|
2234
|
-
}
|
|
2235
|
-
return;
|
|
2236
|
-
}
|
|
2237
|
-
|
|
2238
|
-
if (msg.type === "set_model" && msg.model) {
|
|
2239
|
-
var session = getSessionForWs(ws);
|
|
2240
|
-
if (session) {
|
|
2241
|
-
sdk.setModel(session, msg.model);
|
|
2242
|
-
}
|
|
2243
|
-
return;
|
|
2244
|
-
}
|
|
2245
|
-
|
|
2246
|
-
if (msg.type === "set_server_default_model" && msg.model) {
|
|
2247
|
-
if (typeof opts.onSetServerDefaultModel === "function") {
|
|
2248
|
-
opts.onSetServerDefaultModel(msg.model);
|
|
2249
|
-
}
|
|
2250
|
-
var session = getSessionForWs(ws);
|
|
2251
|
-
if (session) {
|
|
2252
|
-
sdk.setModel(session, msg.model);
|
|
2253
|
-
}
|
|
2254
|
-
return;
|
|
2255
|
-
}
|
|
2256
|
-
|
|
2257
|
-
if (msg.type === "set_project_default_model" && msg.model) {
|
|
2258
|
-
if (typeof opts.onSetProjectDefaultModel === "function") {
|
|
2259
|
-
opts.onSetProjectDefaultModel(slug, msg.model);
|
|
2260
|
-
}
|
|
2261
|
-
var session = getSessionForWs(ws);
|
|
2262
|
-
if (session) {
|
|
2263
|
-
sdk.setModel(session, msg.model);
|
|
2264
|
-
}
|
|
2265
|
-
return;
|
|
2266
|
-
}
|
|
2267
|
-
|
|
2268
|
-
if (msg.type === "set_permission_mode" && msg.mode) {
|
|
2269
|
-
sm.currentPermissionMode = msg.mode;
|
|
2270
|
-
var session = getSessionForWs(ws);
|
|
2271
|
-
if (session) {
|
|
2272
|
-
sdk.setPermissionMode(session, msg.mode);
|
|
2273
|
-
}
|
|
2274
|
-
send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
|
|
2275
|
-
return;
|
|
2276
|
-
}
|
|
2277
|
-
|
|
2278
|
-
if (msg.type === "set_server_default_mode" && msg.mode) {
|
|
2279
|
-
if (typeof opts.onSetServerDefaultMode === "function") {
|
|
2280
|
-
opts.onSetServerDefaultMode(msg.mode);
|
|
2281
|
-
}
|
|
2282
|
-
sm.currentPermissionMode = msg.mode;
|
|
2283
|
-
var session = getSessionForWs(ws);
|
|
2284
|
-
if (session) {
|
|
2285
|
-
sdk.setPermissionMode(session, msg.mode);
|
|
2286
|
-
}
|
|
2287
|
-
send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
|
|
2288
|
-
return;
|
|
2289
|
-
}
|
|
2290
|
-
|
|
2291
|
-
if (msg.type === "set_project_default_mode" && msg.mode) {
|
|
2292
|
-
if (typeof opts.onSetProjectDefaultMode === "function") {
|
|
2293
|
-
opts.onSetProjectDefaultMode(slug, msg.mode);
|
|
2294
|
-
}
|
|
2295
|
-
sm.currentPermissionMode = msg.mode;
|
|
2296
|
-
var session = getSessionForWs(ws);
|
|
2297
|
-
if (session) {
|
|
2298
|
-
sdk.setPermissionMode(session, msg.mode);
|
|
2299
|
-
}
|
|
2300
|
-
send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
|
|
2301
|
-
return;
|
|
2302
|
-
}
|
|
2303
|
-
|
|
2304
|
-
if (msg.type === "set_effort" && msg.effort) {
|
|
2305
|
-
sm.currentEffort = msg.effort;
|
|
2306
|
-
var session = getSessionForWs(ws);
|
|
2307
|
-
if (session) {
|
|
2308
|
-
sdk.setEffort(session, msg.effort);
|
|
2309
|
-
}
|
|
2310
|
-
send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort, betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
|
|
2311
|
-
return;
|
|
2312
|
-
}
|
|
2313
|
-
|
|
2314
|
-
if (msg.type === "set_server_default_effort" && msg.effort) {
|
|
2315
|
-
if (typeof opts.onSetServerDefaultEffort === "function") {
|
|
2316
|
-
opts.onSetServerDefaultEffort(msg.effort);
|
|
2317
|
-
}
|
|
2318
|
-
sm.currentEffort = msg.effort;
|
|
2319
|
-
send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort, betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
|
|
2320
|
-
return;
|
|
2321
|
-
}
|
|
2322
|
-
|
|
2323
|
-
if (msg.type === "set_project_default_effort" && msg.effort) {
|
|
2324
|
-
if (typeof opts.onSetProjectDefaultEffort === "function") {
|
|
2325
|
-
opts.onSetProjectDefaultEffort(slug, msg.effort);
|
|
2326
|
-
}
|
|
2327
|
-
sm.currentEffort = msg.effort;
|
|
2328
|
-
send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort, betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
|
|
2329
|
-
return;
|
|
2330
|
-
}
|
|
2331
|
-
|
|
2332
|
-
if (msg.type === "set_betas") {
|
|
2333
|
-
sm.currentBetas = msg.betas || [];
|
|
2334
|
-
send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas, thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
|
|
2335
|
-
return;
|
|
2336
|
-
}
|
|
2337
|
-
|
|
2338
|
-
if (msg.type === "set_thinking") {
|
|
2339
|
-
sm.currentThinking = msg.thinking || "adaptive";
|
|
2340
|
-
if (msg.budgetTokens) sm.currentThinkingBudget = msg.budgetTokens;
|
|
2341
|
-
send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
|
|
2342
|
-
return;
|
|
2343
|
-
}
|
|
2344
|
-
|
|
2345
|
-
if (msg.type === "rewind_preview") {
|
|
2346
|
-
var session = getSessionForWs(ws);
|
|
2347
|
-
if (!session || !session.cliSessionId || !msg.uuid) return;
|
|
2348
|
-
// Reject preview requests while a rewind is executing
|
|
2349
|
-
if (session._rewindInProgress) return;
|
|
2350
|
-
|
|
2351
|
-
(async function () {
|
|
2352
|
-
var result;
|
|
2353
|
-
try {
|
|
2354
|
-
result = await sdk.getOrCreateRewindQuery(session);
|
|
2355
|
-
var preview = await result.query.rewindFiles(msg.uuid, { dryRun: true });
|
|
2356
|
-
var diffs = {};
|
|
2357
|
-
var changedFiles = preview.filesChanged || [];
|
|
2358
|
-
for (var f = 0; f < changedFiles.length; f++) {
|
|
2359
|
-
try {
|
|
2360
|
-
diffs[changedFiles[f]] = execFileSync(
|
|
2361
|
-
"git", ["diff", "HEAD", "--", changedFiles[f]],
|
|
2362
|
-
{ cwd: cwd, encoding: "utf8", timeout: 5000 }
|
|
2363
|
-
) || "";
|
|
2364
|
-
} catch (e) { diffs[changedFiles[f]] = ""; }
|
|
2365
|
-
}
|
|
2366
|
-
sendTo(ws, { type: "rewind_preview_result", preview: preview, diffs: diffs, uuid: msg.uuid });
|
|
2367
|
-
} catch (err) {
|
|
2368
|
-
sendTo(ws, { type: "rewind_error", text: "Failed to preview rewind: " + err.message });
|
|
2369
|
-
} finally {
|
|
2370
|
-
if (result && result.isTemp) result.cleanup();
|
|
2371
|
-
}
|
|
2372
|
-
})();
|
|
2373
|
-
return;
|
|
2374
|
-
}
|
|
2375
|
-
|
|
2376
|
-
if (msg.type === "rewind_execute") {
|
|
2377
|
-
var session = getSessionForWs(ws);
|
|
2378
|
-
if (!session || !session.cliSessionId || !msg.uuid) return;
|
|
2379
|
-
// Guard against concurrent rewind executions
|
|
2380
|
-
if (session._rewindInProgress) {
|
|
2381
|
-
sendTo(ws, { type: "rewind_error", text: "Rewind already in progress." });
|
|
2382
|
-
return;
|
|
2383
|
-
}
|
|
2384
|
-
session._rewindInProgress = true;
|
|
2385
|
-
var mode = msg.mode || "both";
|
|
2386
|
-
|
|
2387
|
-
(async function () {
|
|
2388
|
-
var result;
|
|
2389
|
-
try {
|
|
2390
|
-
// File restoration (skip for chat-only mode)
|
|
2391
|
-
if (mode !== "chat") {
|
|
2392
|
-
result = await sdk.getOrCreateRewindQuery(session);
|
|
2393
|
-
await result.query.rewindFiles(msg.uuid, { dryRun: false });
|
|
2394
|
-
}
|
|
2395
|
-
|
|
2396
|
-
// Conversation rollback (skip for files-only mode)
|
|
2397
|
-
if (mode !== "files") {
|
|
2398
|
-
var targetIdx = -1;
|
|
2399
|
-
for (var i = 0; i < session.messageUUIDs.length; i++) {
|
|
2400
|
-
if (session.messageUUIDs[i].uuid === msg.uuid) {
|
|
2401
|
-
targetIdx = i;
|
|
2402
|
-
break;
|
|
2403
|
-
}
|
|
2404
|
-
}
|
|
2405
|
-
|
|
2406
|
-
if (targetIdx >= 0) {
|
|
2407
|
-
var trimTo = session.messageUUIDs[targetIdx].historyIndex;
|
|
2408
|
-
for (var k = trimTo - 1; k >= 0; k--) {
|
|
2409
|
-
if (session.history[k].type === "user_message") {
|
|
2410
|
-
trimTo = k;
|
|
2411
|
-
break;
|
|
2412
|
-
}
|
|
2413
|
-
}
|
|
2414
|
-
session.history = session.history.slice(0, trimTo);
|
|
2415
|
-
session.messageUUIDs = session.messageUUIDs.slice(0, targetIdx);
|
|
2416
|
-
}
|
|
2417
|
-
|
|
2418
|
-
var kept = session.messageUUIDs;
|
|
2419
|
-
session.lastRewindUuid = kept.length > 0 ? kept[kept.length - 1].uuid : null;
|
|
2420
|
-
}
|
|
2421
|
-
|
|
2422
|
-
if (session.abortController) {
|
|
2423
|
-
try { session.abortController.abort(); } catch (e) {}
|
|
2424
|
-
}
|
|
2425
|
-
if (session.messageQueue) {
|
|
2426
|
-
try { session.messageQueue.end(); } catch (e) {}
|
|
2427
|
-
}
|
|
2428
|
-
session.queryInstance = null;
|
|
2429
|
-
session.messageQueue = null;
|
|
2430
|
-
session.abortController = null;
|
|
2431
|
-
session.blocks = {};
|
|
2432
|
-
session.sentToolResults = {};
|
|
2433
|
-
session.pendingPermissions = {};
|
|
2434
|
-
session.pendingAskUser = {};
|
|
2435
|
-
session.isProcessing = false;
|
|
2436
|
-
onProcessingChanged();
|
|
2437
|
-
|
|
2438
|
-
sm.saveSessionFile(session);
|
|
2439
|
-
sm.switchSession(session.localId, ws, hydrateImageRefs);
|
|
2440
|
-
sm.sendAndRecord(session, { type: "rewind_complete", mode: mode });
|
|
2441
|
-
sm.broadcastSessionList();
|
|
2442
|
-
} catch (err) {
|
|
2443
|
-
sendTo(ws, { type: "rewind_error", text: "Rewind failed: " + err.message });
|
|
2444
|
-
} finally {
|
|
2445
|
-
session._rewindInProgress = false;
|
|
2446
|
-
if (result && result.isTemp) result.cleanup();
|
|
2447
|
-
}
|
|
2448
|
-
})();
|
|
2449
|
-
return;
|
|
2450
|
-
}
|
|
2451
|
-
|
|
2452
|
-
if (msg.type === "fork_session" && msg.uuid) {
|
|
2453
|
-
var session = getSessionForWs(ws);
|
|
2454
|
-
if (!session || !session.cliSessionId) {
|
|
2455
|
-
sendTo(ws, { type: "error", text: "Cannot fork: no CLI session" });
|
|
2456
|
-
return;
|
|
2457
|
-
}
|
|
2458
|
-
var forkCliId = session.cliSessionId;
|
|
2459
|
-
var forkTitle = (session.title || "New Session") + " (fork)";
|
|
2460
|
-
getSDK().then(function(sdkMod) {
|
|
2461
|
-
return sdkMod.forkSession(forkCliId, {
|
|
2462
|
-
upToMessageId: msg.uuid,
|
|
2463
|
-
dir: cwd,
|
|
2464
|
-
});
|
|
2465
|
-
}).then(function(result) {
|
|
2466
|
-
var cliSess = require("./cli-sessions");
|
|
2467
|
-
return cliSess.readCliSessionHistory(cwd, result.sessionId).then(function(history) {
|
|
2468
|
-
var forked = sm.resumeSession(result.sessionId, { history: history, title: forkTitle }, ws);
|
|
2469
|
-
if (forked) {
|
|
2470
|
-
ws._clayActiveSession = forked.localId;
|
|
2471
|
-
sendTo(ws, { type: "fork_complete", sessionId: forked.localId });
|
|
2472
|
-
}
|
|
2473
|
-
});
|
|
2474
|
-
}).catch(function(e) {
|
|
2475
|
-
sendTo(ws, { type: "error", text: "Fork failed: " + (e.message || e) });
|
|
2476
|
-
});
|
|
2477
|
-
return;
|
|
2478
|
-
}
|
|
2479
|
-
|
|
2480
|
-
if (msg.type === "ask_user_response") {
|
|
2481
|
-
var session = getSessionForWs(ws);
|
|
2482
|
-
if (!session) return;
|
|
2483
|
-
var toolId = msg.toolId;
|
|
2484
|
-
var answers = msg.answers || {};
|
|
2485
|
-
var pending = session.pendingAskUser[toolId];
|
|
2486
|
-
if (!pending) return;
|
|
2487
|
-
delete session.pendingAskUser[toolId];
|
|
2488
|
-
sm.sendAndRecord(session, { type: "ask_user_answered", toolId: toolId, answers: answers });
|
|
2489
|
-
pending.resolve({
|
|
2490
|
-
behavior: "allow",
|
|
2491
|
-
updatedInput: Object.assign({}, pending.input, { answers: answers }),
|
|
2492
|
-
});
|
|
2493
|
-
return;
|
|
2494
|
-
}
|
|
2495
|
-
|
|
2496
|
-
if (msg.type === "input_sync") {
|
|
2497
|
-
sendToSessionOthers(ws, ws._clayActiveSession, msg);
|
|
2498
|
-
return;
|
|
2499
|
-
}
|
|
2500
|
-
|
|
2501
|
-
if (msg.type === "cursor_move" || msg.type === "cursor_leave" || msg.type === "text_select") {
|
|
2502
|
-
if (!usersModule.isMultiUser() || !ws._clayUser) return;
|
|
2503
|
-
var u = ws._clayUser;
|
|
2504
|
-
var p = u.profile || {};
|
|
2505
|
-
var cursorMsg = {
|
|
2506
|
-
type: msg.type,
|
|
2507
|
-
userId: u.id,
|
|
2508
|
-
displayName: p.name || u.displayName || u.username,
|
|
2509
|
-
avatarStyle: p.avatarStyle || "thumbs",
|
|
2510
|
-
avatarSeed: p.avatarSeed || u.username,
|
|
2511
|
-
avatarCustom: p.avatarCustom || "",
|
|
2512
|
-
};
|
|
2513
|
-
if (msg.type === "cursor_move") {
|
|
2514
|
-
cursorMsg.turn = msg.turn;
|
|
2515
|
-
if (msg.rx != null) cursorMsg.rx = msg.rx;
|
|
2516
|
-
if (msg.ry != null) cursorMsg.ry = msg.ry;
|
|
2517
|
-
}
|
|
2518
|
-
if (msg.type === "text_select") {
|
|
2519
|
-
cursorMsg.ranges = msg.ranges || [];
|
|
2520
|
-
}
|
|
2521
|
-
sendToSessionOthers(ws, ws._clayActiveSession, cursorMsg);
|
|
2522
|
-
return;
|
|
2523
|
-
}
|
|
2524
|
-
|
|
2525
|
-
if (msg.type === "permission_response") {
|
|
2526
|
-
var session = getSessionForWs(ws);
|
|
2527
|
-
if (!session) return;
|
|
2528
|
-
var requestId = msg.requestId;
|
|
2529
|
-
var decision = msg.decision;
|
|
2530
|
-
var pending = session.pendingPermissions[requestId];
|
|
2531
|
-
if (!pending) return;
|
|
2532
|
-
delete session.pendingPermissions[requestId];
|
|
2533
|
-
onProcessingChanged(); // update cross-project permission badge
|
|
2534
|
-
|
|
2535
|
-
// --- Plan approval: "allow_accept_edits" — approve + switch to acceptEdits mode ---
|
|
2536
|
-
if (decision === "allow_accept_edits") {
|
|
2537
|
-
sdk.setPermissionMode(session, "acceptEdits");
|
|
2538
|
-
sm.currentPermissionMode = "acceptEdits";
|
|
2539
|
-
send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
|
|
2540
|
-
pending.resolve({ behavior: "allow", updatedInput: pending.toolInput });
|
|
2541
|
-
sm.sendAndRecord(session, { type: "permission_resolved", requestId: requestId, decision: decision });
|
|
2542
|
-
return;
|
|
2543
|
-
}
|
|
2544
|
-
|
|
2545
|
-
// --- Plan approval: "allow_clear_context" — new session + plan as first message + acceptEdits ---
|
|
2546
|
-
if (decision === "allow_clear_context") {
|
|
2547
|
-
// Deny current plan to end the turn
|
|
2548
|
-
pending.resolve({ behavior: "deny", message: "User chose to clear context and restart" });
|
|
2549
|
-
sm.sendAndRecord(session, { type: "permission_resolved", requestId: requestId, decision: decision });
|
|
2550
|
-
|
|
2551
|
-
// Abort the old session's query — but defer to next tick so the SDK's
|
|
2552
|
-
// deny write (scheduled as microtask by pending.resolve) completes first.
|
|
2553
|
-
// Aborting synchronously would kill the subprocess before the write,
|
|
2554
|
-
// causing an "Operation aborted" crash in the SDK.
|
|
2555
|
-
session.isProcessing = false;
|
|
2556
|
-
onProcessingChanged();
|
|
2557
|
-
session.pendingPermissions = {};
|
|
2558
|
-
session.pendingAskUser = {};
|
|
2559
|
-
sm.broadcastSessionList();
|
|
2560
|
-
setImmediate(function () {
|
|
2561
|
-
if (session.abortController) {
|
|
2562
|
-
session.abortController.abort();
|
|
2563
|
-
}
|
|
2564
|
-
});
|
|
2565
|
-
|
|
2566
|
-
// Update permission mode for the new session
|
|
2567
|
-
sm.currentPermissionMode = "acceptEdits";
|
|
2568
|
-
send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
|
|
2569
|
-
|
|
2570
|
-
// Build prompt from plan content (sent from client) or plan file path
|
|
2571
|
-
var clientPlanContent = msg.planContent || "";
|
|
2572
|
-
var planPrompt;
|
|
2573
|
-
if (clientPlanContent) {
|
|
2574
|
-
planPrompt = "Execute the following plan. Do NOT re-enter plan mode — just implement it step by step.\n\n" + clientPlanContent;
|
|
2575
|
-
} else {
|
|
2576
|
-
var planFilePath = (pending.toolInput && pending.toolInput.planFilePath) || "";
|
|
2577
|
-
planPrompt = "Execute the plan in " + planFilePath + ". Do NOT re-enter plan mode — read the plan file and implement it step by step.";
|
|
2578
|
-
}
|
|
2579
|
-
|
|
2580
|
-
// Wait for old query stream to fully terminate, then create new session + send plan
|
|
2581
|
-
var oldStreamPromise = session.streamPromise || Promise.resolve();
|
|
2582
|
-
Promise.race([
|
|
2583
|
-
oldStreamPromise,
|
|
2584
|
-
new Promise(function (resolve) { setTimeout(resolve, 3000); }),
|
|
2585
|
-
]).then(function () {
|
|
2586
|
-
try {
|
|
2587
|
-
var newSession = sm.createSession(null, ws);
|
|
2588
|
-
// Send the plan as the first user message (with planContent for UI rendering)
|
|
2589
|
-
var userMsg = { type: "user_message", text: planPrompt, planContent: clientPlanContent || null };
|
|
2590
|
-
newSession.history.push(userMsg);
|
|
2591
|
-
sm.appendToSessionFile(newSession, userMsg);
|
|
2592
|
-
newSession.title = "Plan execution (cleared context)";
|
|
2593
|
-
sm.saveSessionFile(newSession);
|
|
2594
|
-
sm.broadcastSessionList();
|
|
2595
|
-
sendToSession(newSession.localId, userMsg);
|
|
2596
|
-
|
|
2597
|
-
newSession.isProcessing = true;
|
|
2598
|
-
onProcessingChanged();
|
|
2599
|
-
newSession.sentToolResults = {};
|
|
2600
|
-
sendToSession(newSession.localId, { type: "status", status: "processing" });
|
|
2601
|
-
newSession.acceptEditsAfterStart = true;
|
|
2602
|
-
sdk.startQuery(newSession, planPrompt, undefined, getLinuxUserForSession(newSession));
|
|
2603
|
-
} catch (e) {
|
|
2604
|
-
console.error("[project] Error starting plan execution:", e);
|
|
2605
|
-
sendTo(ws, { type: "error", text: "Failed to start plan execution: " + (e.message || e) });
|
|
2606
|
-
}
|
|
2607
|
-
}).catch(function (e) {
|
|
2608
|
-
console.error("[project] Plan execution stream wait failed:", e.message || e);
|
|
2609
|
-
});
|
|
2610
|
-
return;
|
|
2611
|
-
}
|
|
2612
|
-
|
|
2613
|
-
// --- Plan approval: "deny_with_feedback" — deny + send feedback as follow-up message ---
|
|
2614
|
-
if (decision === "deny_with_feedback") {
|
|
2615
|
-
var feedback = msg.feedback || "";
|
|
2616
|
-
pending.resolve({ behavior: "deny", message: feedback || "User provided feedback" });
|
|
2617
|
-
sm.sendAndRecord(session, { type: "permission_resolved", requestId: requestId, decision: decision });
|
|
2618
|
-
|
|
2619
|
-
// Send feedback as next user message if there's text
|
|
2620
|
-
if (feedback) {
|
|
2621
|
-
setTimeout(function () {
|
|
2622
|
-
var userMsg = { type: "user_message", text: feedback };
|
|
2623
|
-
session.history.push(userMsg);
|
|
2624
|
-
sm.appendToSessionFile(session, userMsg);
|
|
2625
|
-
sendToSession(session.localId, userMsg);
|
|
2626
|
-
|
|
2627
|
-
if (!session.isProcessing) {
|
|
2628
|
-
session.isProcessing = true;
|
|
2629
|
-
onProcessingChanged();
|
|
2630
|
-
session.sentToolResults = {};
|
|
2631
|
-
sendToSession(session.localId, { type: "status", status: "processing" });
|
|
2632
|
-
if (!session.queryInstance && !session.worker) {
|
|
2633
|
-
sdk.startQuery(session, feedback, undefined, getLinuxUserForSession(session));
|
|
2634
|
-
} else {
|
|
2635
|
-
sdk.pushMessage(session, feedback);
|
|
2636
|
-
}
|
|
2637
|
-
} else {
|
|
2638
|
-
sdk.pushMessage(session, feedback);
|
|
2639
|
-
}
|
|
2640
|
-
}, 200);
|
|
2641
|
-
}
|
|
2642
|
-
return;
|
|
2643
|
-
}
|
|
2644
|
-
|
|
2645
|
-
if (decision === "allow" || decision === "allow_always") {
|
|
2646
|
-
if (decision === "allow_always") {
|
|
2647
|
-
if (!session.allowedTools) session.allowedTools = {};
|
|
2648
|
-
session.allowedTools[pending.toolName] = true;
|
|
2649
|
-
}
|
|
2650
|
-
pending.resolve({ behavior: "allow", updatedInput: pending.toolInput });
|
|
2651
|
-
} else {
|
|
2652
|
-
pending.resolve({ behavior: "deny", message: "User denied permission" });
|
|
2653
|
-
}
|
|
2654
|
-
|
|
2655
|
-
sm.sendAndRecord(session, {
|
|
2656
|
-
type: "permission_resolved",
|
|
2657
|
-
requestId: requestId,
|
|
2658
|
-
decision: decision,
|
|
2659
|
-
});
|
|
2660
|
-
return;
|
|
2661
|
-
}
|
|
2662
|
-
|
|
2663
|
-
// --- MCP elicitation response ---
|
|
2664
|
-
if (msg.type === "elicitation_response") {
|
|
2665
|
-
var session = getSessionForWs(ws);
|
|
2666
|
-
if (!session) return;
|
|
2667
|
-
var pending = session.pendingElicitations && session.pendingElicitations[msg.requestId];
|
|
2668
|
-
if (!pending) return;
|
|
2669
|
-
delete session.pendingElicitations[msg.requestId];
|
|
2670
|
-
if (msg.action === "accept") {
|
|
2671
|
-
pending.resolve({ action: "accept", content: msg.content || {} });
|
|
2672
|
-
} else {
|
|
2673
|
-
pending.resolve({ action: "reject" });
|
|
2674
|
-
}
|
|
2675
|
-
sm.sendAndRecord(session, {
|
|
2676
|
-
type: "elicitation_resolved",
|
|
2677
|
-
requestId: msg.requestId,
|
|
2678
|
-
action: msg.action,
|
|
2679
|
-
});
|
|
2680
|
-
return;
|
|
2681
|
-
}
|
|
2682
|
-
|
|
2683
|
-
// --- Browse directories (for add-project autocomplete) ---
|
|
2684
|
-
if (msg.type === "browse_dir") {
|
|
2685
|
-
var rawPath = (msg.path || "").replace(/^~/, require("./config").REAL_HOME);
|
|
2686
|
-
var absTarget = path.resolve(rawPath);
|
|
2687
|
-
var parentDir, prefix;
|
|
2688
|
-
try {
|
|
2689
|
-
var stat = fs.statSync(absTarget);
|
|
2690
|
-
if (stat.isDirectory()) {
|
|
2691
|
-
// Input is an existing directory — list its children
|
|
2692
|
-
parentDir = absTarget;
|
|
2693
|
-
prefix = "";
|
|
2694
|
-
} else {
|
|
2695
|
-
parentDir = path.dirname(absTarget);
|
|
2696
|
-
prefix = path.basename(absTarget).toLowerCase();
|
|
2697
|
-
}
|
|
2698
|
-
} catch (e) {
|
|
2699
|
-
// Path doesn't exist — list parent and filter by typed prefix
|
|
2700
|
-
parentDir = path.dirname(absTarget);
|
|
2701
|
-
prefix = path.basename(absTarget).toLowerCase();
|
|
2702
|
-
}
|
|
2703
|
-
try {
|
|
2704
|
-
var dirItems = fs.readdirSync(parentDir, { withFileTypes: true });
|
|
2705
|
-
var dirEntries = [];
|
|
2706
|
-
for (var di = 0; di < dirItems.length; di++) {
|
|
2707
|
-
var d = dirItems[di];
|
|
2708
|
-
if (!d.isDirectory()) continue;
|
|
2709
|
-
if (d.name.charAt(0) === ".") continue;
|
|
2710
|
-
if (IGNORED_DIRS.has(d.name)) continue;
|
|
2711
|
-
if (prefix && !d.name.toLowerCase().startsWith(prefix)) continue;
|
|
2712
|
-
dirEntries.push({ name: d.name, path: path.join(parentDir, d.name) });
|
|
2713
|
-
}
|
|
2714
|
-
dirEntries.sort(function (a, b) { return a.name.localeCompare(b.name); });
|
|
2715
|
-
sendTo(ws, { type: "browse_dir_result", path: msg.path, entries: dirEntries });
|
|
2716
|
-
} catch (e) {
|
|
2717
|
-
sendTo(ws, { type: "browse_dir_result", path: msg.path, entries: [], error: e.message });
|
|
2718
|
-
}
|
|
2719
|
-
return;
|
|
2720
|
-
}
|
|
2721
|
-
|
|
2722
|
-
// --- Add project from web UI ---
|
|
2723
|
-
if (msg.type === "add_project") {
|
|
2724
|
-
var addPath = (msg.path || "").replace(/^~/, require("./config").REAL_HOME);
|
|
2725
|
-
var addAbs = path.resolve(addPath);
|
|
2726
|
-
try {
|
|
2727
|
-
var addStat = fs.statSync(addAbs);
|
|
2728
|
-
if (!addStat.isDirectory()) {
|
|
2729
|
-
sendTo(ws, { type: "add_project_result", ok: false, error: "Not a directory" });
|
|
2730
|
-
return;
|
|
2731
|
-
}
|
|
2732
|
-
} catch (e) {
|
|
2733
|
-
sendTo(ws, { type: "add_project_result", ok: false, error: "Directory not found" });
|
|
2734
|
-
return;
|
|
2735
|
-
}
|
|
2736
|
-
if (typeof opts.onAddProject === "function") {
|
|
2737
|
-
var result = opts.onAddProject(addAbs, ws._clayUser);
|
|
2738
|
-
sendTo(ws, { type: "add_project_result", ok: result.ok, slug: result.slug, error: result.error, existing: result.existing });
|
|
2739
|
-
} else {
|
|
2740
|
-
sendTo(ws, { type: "add_project_result", ok: false, error: "Not supported" });
|
|
2741
|
-
}
|
|
2742
|
-
return;
|
|
2743
|
-
}
|
|
2744
|
-
|
|
2745
|
-
// --- Create new empty project ---
|
|
2746
|
-
if (msg.type === "create_project" || msg.type === "clone_project") {
|
|
2747
|
-
if (ws._clayUser) {
|
|
2748
|
-
var cpPerms = usersModule.getEffectivePermissions(ws._clayUser, osUsers);
|
|
2749
|
-
if (!cpPerms.createProject) {
|
|
2750
|
-
sendTo(ws, { type: "add_project_result", ok: false, error: "You do not have permission to create projects" });
|
|
2751
|
-
return;
|
|
2752
|
-
}
|
|
2753
|
-
}
|
|
2754
|
-
}
|
|
2755
|
-
if (msg.type === "create_project") {
|
|
2756
|
-
var createName = (msg.name || "").trim();
|
|
2757
|
-
if (!createName || !/^[a-zA-Z0-9_-]+$/.test(createName)) {
|
|
2758
|
-
sendTo(ws, { type: "add_project_result", ok: false, error: "Invalid name. Use only letters, numbers, dashes, and underscores." });
|
|
2759
|
-
return;
|
|
2760
|
-
}
|
|
2761
|
-
if (typeof opts.onCreateProject === "function") {
|
|
2762
|
-
var createResult = opts.onCreateProject(createName, ws._clayUser);
|
|
2763
|
-
sendTo(ws, { type: "add_project_result", ok: createResult.ok, slug: createResult.slug, error: createResult.error });
|
|
2764
|
-
} else {
|
|
2765
|
-
sendTo(ws, { type: "add_project_result", ok: false, error: "Not supported" });
|
|
2766
|
-
}
|
|
2767
|
-
return;
|
|
2768
|
-
}
|
|
2769
|
-
|
|
2770
|
-
// --- Clone project from GitHub ---
|
|
2771
|
-
if (msg.type === "clone_project") {
|
|
2772
|
-
var cloneUrl = (msg.url || "").trim();
|
|
2773
|
-
if (!cloneUrl || (!/^https?:\/\//.test(cloneUrl) && !/^git@/.test(cloneUrl))) {
|
|
2774
|
-
sendTo(ws, { type: "add_project_result", ok: false, error: "Invalid URL. Use https:// or git@ format." });
|
|
2775
|
-
return;
|
|
2776
|
-
}
|
|
2777
|
-
sendTo(ws, { type: "clone_project_progress", status: "cloning" });
|
|
2778
|
-
if (typeof opts.onCloneProject === "function") {
|
|
2779
|
-
opts.onCloneProject(cloneUrl, ws._clayUser, function (cloneResult) {
|
|
2780
|
-
sendTo(ws, { type: "add_project_result", ok: cloneResult.ok, slug: cloneResult.slug, error: cloneResult.error });
|
|
2781
|
-
});
|
|
2782
|
-
} else {
|
|
2783
|
-
sendTo(ws, { type: "add_project_result", ok: false, error: "Not supported" });
|
|
2784
|
-
}
|
|
2785
|
-
return;
|
|
2786
|
-
}
|
|
2787
|
-
|
|
2788
|
-
// --- Create worktree from web UI ---
|
|
2789
|
-
if (msg.type === "create_worktree") {
|
|
2790
|
-
var wtBranch = (msg.branch || "").trim();
|
|
2791
|
-
var wtDirName = (msg.dirName || "").trim() || wtBranch.replace(/\//g, "-");
|
|
2792
|
-
var wtBase = (msg.baseBranch || "").trim() || null;
|
|
2793
|
-
if (!wtBranch || !/^[a-zA-Z0-9_\/.@-]+$/.test(wtBranch)) {
|
|
2794
|
-
sendTo(ws, { type: "create_worktree_result", ok: false, error: "Invalid branch name" });
|
|
2795
|
-
return;
|
|
2796
|
-
}
|
|
2797
|
-
if (typeof onCreateWorktree === "function") {
|
|
2798
|
-
var wtResult = onCreateWorktree(slug, wtBranch, wtDirName, wtBase);
|
|
2799
|
-
sendTo(ws, { type: "create_worktree_result", ok: wtResult.ok, slug: wtResult.slug, error: wtResult.error });
|
|
2800
|
-
} else {
|
|
2801
|
-
sendTo(ws, { type: "create_worktree_result", ok: false, error: "Not supported" });
|
|
2802
|
-
}
|
|
2803
|
-
return;
|
|
2804
|
-
}
|
|
2805
|
-
|
|
2806
|
-
// --- Pre-check: does the project have tasks/schedules? ---
|
|
2807
|
-
if (msg.type === "remove_project_check") {
|
|
2808
|
-
var checkSlug = msg.slug;
|
|
2809
|
-
if (!checkSlug) {
|
|
2810
|
-
sendTo(ws, { type: "remove_project_check_result", slug: checkSlug, name: msg.name || checkSlug, count: 0 });
|
|
2811
|
-
return;
|
|
2812
|
-
}
|
|
2813
|
-
var schedCount = getScheduleCount(checkSlug);
|
|
2814
|
-
sendTo(ws, { type: "remove_project_check_result", slug: checkSlug, name: msg.name || checkSlug, count: schedCount });
|
|
2815
|
-
return;
|
|
2816
|
-
}
|
|
2817
|
-
|
|
2818
|
-
// --- Remove project from web UI ---
|
|
2819
|
-
if (msg.type === "remove_project") {
|
|
2820
|
-
if (ws._clayUser) {
|
|
2821
|
-
var dpPerms = usersModule.getEffectivePermissions(ws._clayUser, osUsers);
|
|
2822
|
-
if (!dpPerms.deleteProject) {
|
|
2823
|
-
sendTo(ws, { type: "remove_project_result", ok: false, error: "You do not have permission to delete projects" });
|
|
2824
|
-
return;
|
|
2825
|
-
}
|
|
2826
|
-
}
|
|
2827
|
-
var removeSlug = msg.slug;
|
|
2828
|
-
if (!removeSlug) {
|
|
2829
|
-
sendTo(ws, { type: "remove_project_result", ok: false, error: "Missing slug" });
|
|
2830
|
-
return;
|
|
2831
|
-
}
|
|
2832
|
-
// If client chose to move tasks to another project before removing
|
|
2833
|
-
if (msg.moveTasksTo) {
|
|
2834
|
-
moveAllSchedulesToProject(removeSlug, msg.moveTasksTo);
|
|
2835
|
-
}
|
|
2836
|
-
if (typeof opts.onRemoveProject === "function") {
|
|
2837
|
-
// Send result before removing so the WS is still open
|
|
2838
|
-
sendTo(ws, { type: "remove_project_result", ok: true, slug: removeSlug });
|
|
2839
|
-
var removeUserId = ws._clayUser ? ws._clayUser.id : null;
|
|
2840
|
-
opts.onRemoveProject(removeSlug, removeUserId);
|
|
2841
|
-
} else {
|
|
2842
|
-
sendTo(ws, { type: "remove_project_result", ok: false, error: "Not supported" });
|
|
2843
|
-
}
|
|
2844
|
-
return;
|
|
2845
|
-
}
|
|
2846
|
-
|
|
2847
|
-
// --- Move a single schedule to another project ---
|
|
2848
|
-
if (msg.type === "schedule_move") {
|
|
2849
|
-
var moveResult = moveScheduleToProject(msg.recordId, msg.fromSlug, msg.toSlug);
|
|
2850
|
-
if (moveResult.ok) {
|
|
2851
|
-
// Re-broadcast updated records to this project's clients
|
|
2852
|
-
send({ type: "loop_registry_updated", records: getHubSchedules() });
|
|
2853
|
-
}
|
|
2854
|
-
sendTo(ws, { type: "schedule_move_result", ok: moveResult.ok, error: moveResult.error });
|
|
2855
|
-
return;
|
|
2856
|
-
}
|
|
2857
|
-
|
|
2858
|
-
// --- Reorder projects ---
|
|
2859
|
-
if (msg.type === "reorder_projects") {
|
|
2860
|
-
var slugs = msg.slugs;
|
|
2861
|
-
if (!Array.isArray(slugs) || slugs.length === 0) {
|
|
2862
|
-
sendTo(ws, { type: "reorder_projects_result", ok: false, error: "Missing slugs" });
|
|
2863
|
-
return;
|
|
2864
|
-
}
|
|
2865
|
-
if (typeof opts.onReorderProjects === "function") {
|
|
2866
|
-
var reorderResult = opts.onReorderProjects(slugs);
|
|
2867
|
-
sendTo(ws, { type: "reorder_projects_result", ok: reorderResult.ok, error: reorderResult.error });
|
|
2868
|
-
} else {
|
|
2869
|
-
sendTo(ws, { type: "reorder_projects_result", ok: false, error: "Not supported" });
|
|
2870
|
-
}
|
|
2871
|
-
return;
|
|
2872
|
-
}
|
|
2873
|
-
|
|
2874
|
-
// --- Set project title (rename) ---
|
|
2875
|
-
if (msg.type === "set_project_title") {
|
|
2876
|
-
if (!msg.slug) {
|
|
2877
|
-
sendTo(ws, { type: "set_project_title_result", ok: false, error: "Missing slug" });
|
|
2878
|
-
return;
|
|
2879
|
-
}
|
|
2880
|
-
if (typeof opts.onSetProjectTitle === "function") {
|
|
2881
|
-
var titleResult = opts.onSetProjectTitle(msg.slug, msg.title || null);
|
|
2882
|
-
sendTo(ws, { type: "set_project_title_result", ok: titleResult.ok, slug: msg.slug, error: titleResult.error });
|
|
2883
|
-
} else {
|
|
2884
|
-
sendTo(ws, { type: "set_project_title_result", ok: false, error: "Not supported" });
|
|
2885
|
-
}
|
|
2886
|
-
return;
|
|
2887
|
-
}
|
|
2888
|
-
|
|
2889
|
-
// --- Set project icon (emoji) ---
|
|
2890
|
-
if (msg.type === "set_project_icon") {
|
|
2891
|
-
if (!msg.slug) {
|
|
2892
|
-
sendTo(ws, { type: "set_project_icon_result", ok: false, error: "Missing slug" });
|
|
2893
|
-
return;
|
|
2894
|
-
}
|
|
2895
|
-
if (typeof opts.onSetProjectIcon === "function") {
|
|
2896
|
-
var iconResult = opts.onSetProjectIcon(msg.slug, msg.icon || null);
|
|
2897
|
-
sendTo(ws, { type: "set_project_icon_result", ok: iconResult.ok, slug: msg.slug, error: iconResult.error });
|
|
2898
|
-
} else {
|
|
2899
|
-
sendTo(ws, { type: "set_project_icon_result", ok: false, error: "Not supported" });
|
|
2900
|
-
}
|
|
2901
|
-
return;
|
|
2902
|
-
}
|
|
2903
|
-
|
|
2904
|
-
// --- Daemon config / server management (admin-only in multi-user mode) ---
|
|
2905
|
-
if (msg.type === "get_daemon_config" || msg.type === "set_pin" || msg.type === "set_keep_awake" ||
|
|
2906
|
-
msg.type === "set_auto_continue" || msg.type === "set_image_retention" || msg.type === "shutdown_server" || msg.type === "restart_server") {
|
|
2907
|
-
if (usersModule.isMultiUser()) {
|
|
2908
|
-
var _wsUser = ws._clayUser;
|
|
2909
|
-
if (!_wsUser || _wsUser.role !== "admin") {
|
|
2910
|
-
sendTo(ws, { type: "error", message: "Admin access required" });
|
|
2911
|
-
return;
|
|
2912
|
-
}
|
|
2913
|
-
}
|
|
2914
|
-
}
|
|
2915
|
-
|
|
2916
|
-
if (msg.type === "get_daemon_config") {
|
|
2917
|
-
if (typeof opts.onGetDaemonConfig === "function") {
|
|
2918
|
-
var daemonConfig = opts.onGetDaemonConfig();
|
|
2919
|
-
sendTo(ws, { type: "daemon_config", config: daemonConfig });
|
|
2920
|
-
}
|
|
2921
|
-
return;
|
|
2922
|
-
}
|
|
2923
|
-
|
|
2924
|
-
if (msg.type === "set_pin") {
|
|
2925
|
-
if (typeof opts.onSetPin === "function") {
|
|
2926
|
-
var pinResult = opts.onSetPin(msg.pin || null);
|
|
2927
|
-
sendTo(ws, { type: "set_pin_result", ok: pinResult.ok, pinEnabled: pinResult.pinEnabled });
|
|
2928
|
-
}
|
|
2929
|
-
return;
|
|
2930
|
-
}
|
|
2931
|
-
|
|
2932
|
-
if (msg.type === "set_keep_awake") {
|
|
2933
|
-
if (typeof opts.onSetKeepAwake === "function") {
|
|
2934
|
-
var kaResult = opts.onSetKeepAwake(msg.value);
|
|
2935
|
-
sendTo(ws, { type: "set_keep_awake_result", ok: kaResult.ok, keepAwake: kaResult.keepAwake });
|
|
2936
|
-
send({ type: "keep_awake_changed", keepAwake: kaResult.keepAwake });
|
|
2937
|
-
}
|
|
2938
|
-
return;
|
|
2939
|
-
}
|
|
2940
|
-
|
|
2941
|
-
if (msg.type === "set_auto_continue") {
|
|
2942
|
-
if (typeof opts.onSetAutoContinue === "function") {
|
|
2943
|
-
var acResult = opts.onSetAutoContinue(msg.value);
|
|
2944
|
-
sendTo(ws, { type: "set_auto_continue_result", ok: acResult.ok, autoContinueOnRateLimit: acResult.autoContinueOnRateLimit });
|
|
2945
|
-
send({ type: "auto_continue_changed", autoContinueOnRateLimit: acResult.autoContinueOnRateLimit });
|
|
2946
|
-
}
|
|
2947
|
-
return;
|
|
2948
|
-
}
|
|
2949
|
-
|
|
2950
|
-
if (msg.type === "set_image_retention") {
|
|
2951
|
-
if (typeof opts.onSetImageRetention === "function") {
|
|
2952
|
-
var irResult = opts.onSetImageRetention(msg.days);
|
|
2953
|
-
sendTo(ws, { type: "set_image_retention_result", ok: irResult.ok, days: irResult.days });
|
|
2954
|
-
}
|
|
2955
|
-
return;
|
|
2956
|
-
}
|
|
2957
|
-
|
|
2958
|
-
if (msg.type === "shutdown_server") {
|
|
2959
|
-
if (typeof opts.onShutdown === "function") {
|
|
2960
|
-
sendTo(ws, { type: "shutdown_server_result", ok: true });
|
|
2961
|
-
send({ type: "toast", level: "warn", message: "Server is shutting down..." });
|
|
2962
|
-
// Small delay so the response has time to reach clients
|
|
2963
|
-
setTimeout(function () {
|
|
2964
|
-
opts.onShutdown();
|
|
2965
|
-
}, 500);
|
|
2966
|
-
} else {
|
|
2967
|
-
sendTo(ws, { type: "shutdown_server_result", ok: false, error: "Shutdown not supported" });
|
|
2968
|
-
}
|
|
2969
|
-
return;
|
|
2970
|
-
}
|
|
2971
|
-
|
|
2972
|
-
if (msg.type === "restart_server") {
|
|
2973
|
-
if (typeof opts.onRestart === "function") {
|
|
2974
|
-
sendTo(ws, { type: "restart_server_result", ok: true });
|
|
2975
|
-
send({ type: "toast", level: "info", message: "Server is restarting..." });
|
|
2976
|
-
// Small delay so the response has time to reach clients
|
|
2977
|
-
setTimeout(function () {
|
|
2978
|
-
opts.onRestart();
|
|
2979
|
-
}, 500);
|
|
2980
|
-
} else {
|
|
2981
|
-
sendTo(ws, { type: "restart_server_result", ok: false, error: "Restart not supported" });
|
|
2982
|
-
}
|
|
2983
|
-
return;
|
|
2984
|
-
}
|
|
2985
|
-
|
|
2986
|
-
// --- File browser ---
|
|
2987
|
-
if (msg.type === "fs_list" || msg.type === "fs_read" || msg.type === "fs_write" || msg.type === "fs_delete" || msg.type === "fs_rename" || msg.type === "fs_mkdir" || msg.type === "fs_upload") {
|
|
2988
|
-
if (ws._clayUser) {
|
|
2989
|
-
var fbPerms = usersModule.getEffectivePermissions(ws._clayUser, osUsers);
|
|
2990
|
-
if (!fbPerms.fileBrowser) {
|
|
2991
|
-
sendTo(ws, { type: msg.type + "_result", error: "File browser access is not permitted" });
|
|
2992
|
-
return;
|
|
2993
|
-
}
|
|
2994
|
-
}
|
|
2995
|
-
}
|
|
2996
|
-
if (msg.type === "fs_list") {
|
|
2997
|
-
var fsDir = safePath(cwd, msg.path || ".");
|
|
2998
|
-
// In OS user mode, fall back to absolute path resolution (ACL enforces access)
|
|
2999
|
-
if (!fsDir && getOsUserInfoForWs(ws)) {
|
|
3000
|
-
fsDir = safeAbsPath(msg.path);
|
|
3001
|
-
}
|
|
3002
|
-
if (!fsDir) {
|
|
3003
|
-
sendTo(ws, { type: "fs_list_result", path: msg.path, entries: [], error: "Access denied" });
|
|
3004
|
-
return;
|
|
3005
|
-
}
|
|
3006
|
-
try {
|
|
3007
|
-
var fsListUserInfo = getOsUserInfoForWs(ws);
|
|
3008
|
-
var entries = [];
|
|
3009
|
-
if (fsListUserInfo) {
|
|
3010
|
-
// Run as target OS user to respect Linux file permissions
|
|
3011
|
-
var rawEntries = fsAsUser("list", { dir: fsDir }, fsListUserInfo);
|
|
3012
|
-
for (var fi = 0; fi < rawEntries.length; fi++) {
|
|
3013
|
-
var re = rawEntries[fi];
|
|
3014
|
-
if (re.isDir && IGNORED_DIRS.has(re.name)) continue;
|
|
3015
|
-
entries.push({
|
|
3016
|
-
name: re.name,
|
|
3017
|
-
type: re.isDir ? "dir" : "file",
|
|
3018
|
-
path: path.relative(cwd, path.join(fsDir, re.name)).split(path.sep).join("/"),
|
|
3019
|
-
});
|
|
3020
|
-
}
|
|
3021
|
-
} else {
|
|
3022
|
-
var items = fs.readdirSync(fsDir, { withFileTypes: true });
|
|
3023
|
-
for (var fi = 0; fi < items.length; fi++) {
|
|
3024
|
-
var item = items[fi];
|
|
3025
|
-
if (item.isDirectory() && IGNORED_DIRS.has(item.name)) continue;
|
|
3026
|
-
entries.push({
|
|
3027
|
-
name: item.name,
|
|
3028
|
-
type: item.isDirectory() ? "dir" : "file",
|
|
3029
|
-
path: path.relative(cwd, path.join(fsDir, item.name)).split(path.sep).join("/"),
|
|
3030
|
-
});
|
|
3031
|
-
}
|
|
3032
|
-
}
|
|
3033
|
-
sendTo(ws, { type: "fs_list_result", path: msg.path || ".", entries: entries });
|
|
3034
|
-
// Auto-watch the directory for changes
|
|
3035
|
-
startDirWatch(msg.path || ".");
|
|
3036
|
-
} catch (e) {
|
|
3037
|
-
sendTo(ws, { type: "fs_list_result", path: msg.path, entries: [], error: e.message });
|
|
3038
|
-
}
|
|
3039
|
-
return;
|
|
3040
|
-
}
|
|
3041
|
-
|
|
3042
|
-
if (msg.type === "fs_read") {
|
|
3043
|
-
var fsFile = safePath(cwd, msg.path);
|
|
3044
|
-
if (!fsFile && getOsUserInfoForWs(ws)) {
|
|
3045
|
-
fsFile = safeAbsPath(msg.path);
|
|
3046
|
-
}
|
|
3047
|
-
if (!fsFile) {
|
|
3048
|
-
sendTo(ws, { type: "fs_read_result", path: msg.path, error: "Access denied" });
|
|
3049
|
-
return;
|
|
3050
|
-
}
|
|
3051
|
-
try {
|
|
3052
|
-
var fsReadUserInfo = getOsUserInfoForWs(ws);
|
|
3053
|
-
var ext = path.extname(fsFile).toLowerCase();
|
|
3054
|
-
if (fsReadUserInfo) {
|
|
3055
|
-
// Run stat and read as target OS user
|
|
3056
|
-
var statResult = fsAsUser("stat", { file: fsFile }, fsReadUserInfo);
|
|
3057
|
-
if (statResult.size > FS_MAX_SIZE) {
|
|
3058
|
-
sendTo(ws, { type: "fs_read_result", path: msg.path, binary: true, size: statResult.size, error: "File too large (" + (statResult.size / 1024 / 1024).toFixed(1) + " MB)" });
|
|
3059
|
-
return;
|
|
3060
|
-
}
|
|
3061
|
-
if (BINARY_EXTS.has(ext)) {
|
|
3062
|
-
var result = { type: "fs_read_result", path: msg.path, binary: true, size: statResult.size };
|
|
3063
|
-
if (IMAGE_EXTS.has(ext)) result.imageUrl = "api/file?path=" + encodeURIComponent(msg.path);
|
|
3064
|
-
sendTo(ws, result);
|
|
3065
|
-
return;
|
|
3066
|
-
}
|
|
3067
|
-
var readResult = fsAsUser("read", { file: fsFile, readContent: true }, fsReadUserInfo);
|
|
3068
|
-
sendTo(ws, { type: "fs_read_result", path: msg.path, content: readResult.content, size: statResult.size });
|
|
3069
|
-
} else {
|
|
3070
|
-
var stat = fs.statSync(fsFile);
|
|
3071
|
-
if (stat.size > FS_MAX_SIZE) {
|
|
3072
|
-
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)" });
|
|
3073
|
-
return;
|
|
3074
|
-
}
|
|
3075
|
-
if (BINARY_EXTS.has(ext)) {
|
|
3076
|
-
var result = { type: "fs_read_result", path: msg.path, binary: true, size: stat.size };
|
|
3077
|
-
if (IMAGE_EXTS.has(ext)) result.imageUrl = "api/file?path=" + encodeURIComponent(msg.path);
|
|
3078
|
-
sendTo(ws, result);
|
|
3079
|
-
return;
|
|
3080
|
-
}
|
|
3081
|
-
var content = fs.readFileSync(fsFile, "utf8");
|
|
3082
|
-
sendTo(ws, { type: "fs_read_result", path: msg.path, content: content, size: stat.size });
|
|
3083
|
-
}
|
|
3084
|
-
} catch (e) {
|
|
3085
|
-
sendTo(ws, { type: "fs_read_result", path: msg.path, error: e.message });
|
|
3086
|
-
}
|
|
3087
|
-
return;
|
|
3088
|
-
}
|
|
3089
|
-
|
|
3090
|
-
// --- File write ---
|
|
3091
|
-
if (msg.type === "fs_write") {
|
|
3092
|
-
var fsWriteFile = safePath(cwd, msg.path);
|
|
3093
|
-
if (!fsWriteFile && getOsUserInfoForWs(ws)) {
|
|
3094
|
-
fsWriteFile = safeAbsPath(msg.path);
|
|
3095
|
-
}
|
|
3096
|
-
if (!fsWriteFile) {
|
|
3097
|
-
sendTo(ws, { type: "fs_write_result", path: msg.path, ok: false, error: "Access denied" });
|
|
3098
|
-
return;
|
|
3099
|
-
}
|
|
3100
|
-
try {
|
|
3101
|
-
var fsWriteUserInfo = getOsUserInfoForWs(ws);
|
|
3102
|
-
if (fsWriteUserInfo) {
|
|
3103
|
-
fsAsUser("write", { file: fsWriteFile, content: msg.content || "" }, fsWriteUserInfo);
|
|
3104
|
-
} else {
|
|
3105
|
-
fs.writeFileSync(fsWriteFile, msg.content || "", "utf8");
|
|
3106
|
-
}
|
|
3107
|
-
sendTo(ws, { type: "fs_write_result", path: msg.path, ok: true });
|
|
3108
|
-
} catch (e) {
|
|
3109
|
-
sendTo(ws, { type: "fs_write_result", path: msg.path, ok: false, error: e.message });
|
|
3110
|
-
}
|
|
3111
|
-
return;
|
|
3112
|
-
}
|
|
3113
|
-
|
|
3114
|
-
// --- Project settings permission gate ---
|
|
3115
|
-
if (msg.type === "get_project_env" || msg.type === "set_project_env" ||
|
|
3116
|
-
msg.type === "read_global_claude_md" || msg.type === "write_global_claude_md" ||
|
|
3117
|
-
msg.type === "get_shared_env" || msg.type === "set_shared_env" ||
|
|
3118
|
-
msg.type === "transfer_project_owner") {
|
|
3119
|
-
if (ws._clayUser) {
|
|
3120
|
-
var psPerms = usersModule.getEffectivePermissions(ws._clayUser, osUsers);
|
|
3121
|
-
if (!psPerms.projectSettings) {
|
|
3122
|
-
sendTo(ws, { type: "error", text: "Project settings access is not permitted" });
|
|
3123
|
-
return;
|
|
3124
|
-
}
|
|
3125
|
-
}
|
|
3126
|
-
}
|
|
3127
|
-
|
|
3128
|
-
// --- Project environment variables ---
|
|
3129
|
-
if (msg.type === "get_project_env") {
|
|
3130
|
-
var envrc = "";
|
|
3131
|
-
var hasEnvrc = false;
|
|
3132
|
-
if (typeof opts.onGetProjectEnv === "function") {
|
|
3133
|
-
var envResult = opts.onGetProjectEnv(msg.slug);
|
|
3134
|
-
envrc = envResult.envrc || "";
|
|
3135
|
-
}
|
|
3136
|
-
try {
|
|
3137
|
-
var envrcPath = path.join(cwd, ".envrc");
|
|
3138
|
-
hasEnvrc = fs.existsSync(envrcPath);
|
|
3139
|
-
} catch (e) {}
|
|
3140
|
-
sendTo(ws, { type: "project_env_result", slug: msg.slug, envrc: envrc, hasEnvrc: hasEnvrc });
|
|
3141
|
-
return;
|
|
3142
|
-
}
|
|
3143
|
-
|
|
3144
|
-
if (msg.type === "set_project_env") {
|
|
3145
|
-
if (typeof opts.onSetProjectEnv === "function") {
|
|
3146
|
-
var envError = validateEnvString(msg.envrc || "");
|
|
3147
|
-
if (envError) {
|
|
3148
|
-
sendTo(ws, { type: "set_project_env_result", ok: false, slug: msg.slug, error: envError });
|
|
3149
|
-
return;
|
|
3150
|
-
}
|
|
3151
|
-
var setResult = opts.onSetProjectEnv(msg.slug, msg.envrc || "");
|
|
3152
|
-
sendTo(ws, { type: "set_project_env_result", ok: setResult.ok, slug: msg.slug, error: setResult.error });
|
|
3153
|
-
} else {
|
|
3154
|
-
sendTo(ws, { type: "set_project_env_result", ok: false, error: "Not supported" });
|
|
3155
|
-
}
|
|
3156
|
-
return;
|
|
3157
|
-
}
|
|
3158
|
-
|
|
3159
|
-
// --- Global CLAUDE.md ---
|
|
3160
|
-
if (msg.type === "read_global_claude_md") {
|
|
3161
|
-
var globalMdPath = path.join(require("./config").REAL_HOME, ".claude", "CLAUDE.md");
|
|
3162
|
-
try {
|
|
3163
|
-
var globalMdContent = fs.readFileSync(globalMdPath, "utf8");
|
|
3164
|
-
sendTo(ws, { type: "global_claude_md_result", content: globalMdContent });
|
|
3165
|
-
} catch (e) {
|
|
3166
|
-
sendTo(ws, { type: "global_claude_md_result", error: e.message });
|
|
3167
|
-
}
|
|
3168
|
-
return;
|
|
3169
|
-
}
|
|
3170
|
-
|
|
3171
|
-
if (msg.type === "write_global_claude_md") {
|
|
3172
|
-
var globalMdDir = path.join(require("./config").REAL_HOME, ".claude");
|
|
3173
|
-
var globalMdWritePath = path.join(globalMdDir, "CLAUDE.md");
|
|
3174
|
-
try {
|
|
3175
|
-
if (!fs.existsSync(globalMdDir)) {
|
|
3176
|
-
fs.mkdirSync(globalMdDir, { recursive: true });
|
|
3177
|
-
}
|
|
3178
|
-
fs.writeFileSync(globalMdWritePath, msg.content || "", "utf8");
|
|
3179
|
-
sendTo(ws, { type: "write_global_claude_md_result", ok: true });
|
|
3180
|
-
} catch (e) {
|
|
3181
|
-
sendTo(ws, { type: "write_global_claude_md_result", ok: false, error: e.message });
|
|
3182
|
-
}
|
|
3183
|
-
return;
|
|
3184
|
-
}
|
|
3185
|
-
|
|
3186
|
-
// --- Shared environment variables ---
|
|
3187
|
-
if (msg.type === "get_shared_env") {
|
|
3188
|
-
var sharedEnvrc = "";
|
|
3189
|
-
if (typeof opts.onGetSharedEnv === "function") {
|
|
3190
|
-
var sharedResult = opts.onGetSharedEnv();
|
|
3191
|
-
sharedEnvrc = sharedResult.envrc || "";
|
|
3192
|
-
}
|
|
3193
|
-
sendTo(ws, { type: "shared_env_result", envrc: sharedEnvrc });
|
|
3194
|
-
return;
|
|
3195
|
-
}
|
|
3196
|
-
|
|
3197
|
-
if (msg.type === "set_shared_env") {
|
|
3198
|
-
if (typeof opts.onSetSharedEnv === "function") {
|
|
3199
|
-
var sharedEnvError = validateEnvString(msg.envrc || "");
|
|
3200
|
-
if (sharedEnvError) {
|
|
3201
|
-
sendTo(ws, { type: "set_shared_env_result", ok: false, error: sharedEnvError });
|
|
3202
|
-
return;
|
|
3203
|
-
}
|
|
3204
|
-
var sharedSetResult = opts.onSetSharedEnv(msg.envrc || "");
|
|
3205
|
-
sendTo(ws, { type: "set_shared_env_result", ok: sharedSetResult.ok, error: sharedSetResult.error });
|
|
3206
|
-
} else {
|
|
3207
|
-
sendTo(ws, { type: "set_shared_env_result", ok: false, error: "Not supported" });
|
|
3208
|
-
}
|
|
3209
|
-
return;
|
|
3210
|
-
}
|
|
3211
|
-
|
|
3212
|
-
// --- File watcher ---
|
|
3213
|
-
if (msg.type === "fs_watch") {
|
|
3214
|
-
if (msg.path) startFileWatch(msg.path);
|
|
3215
|
-
return;
|
|
3216
|
-
}
|
|
3217
|
-
|
|
3218
|
-
if (msg.type === "fs_unwatch") {
|
|
3219
|
-
stopFileWatch();
|
|
3220
|
-
return;
|
|
3221
|
-
}
|
|
3222
|
-
|
|
3223
|
-
// --- File edit history ---
|
|
3224
|
-
if (msg.type === "fs_file_history") {
|
|
3225
|
-
var histPath = msg.path;
|
|
3226
|
-
if (!histPath) {
|
|
3227
|
-
sendTo(ws, { type: "fs_file_history_result", path: histPath, entries: [] });
|
|
3228
|
-
return;
|
|
3229
|
-
}
|
|
3230
|
-
var absHistPath = path.resolve(cwd, histPath);
|
|
3231
|
-
var entries = [];
|
|
3232
|
-
|
|
3233
|
-
// Collect session edits
|
|
3234
|
-
sm.sessions.forEach(function (session) {
|
|
3235
|
-
var sessionLocalId = session.localId;
|
|
3236
|
-
var sessionTitle = session.title || "Untitled";
|
|
3237
|
-
var histLen = session.history.length || 1;
|
|
3238
|
-
|
|
3239
|
-
for (var hi = 0; hi < session.history.length; hi++) {
|
|
3240
|
-
var entry = session.history[hi];
|
|
3241
|
-
if (entry.type !== "tool_executing") continue;
|
|
3242
|
-
if (entry.name !== "Edit" && entry.name !== "Write") continue;
|
|
3243
|
-
if (!entry.input || !entry.input.file_path) continue;
|
|
3244
|
-
if (entry.input.file_path !== absHistPath) continue;
|
|
3245
|
-
|
|
3246
|
-
// Find parent assistant UUID + message snippet by scanning backwards
|
|
3247
|
-
var assistantUuid = null;
|
|
3248
|
-
var uuidIndex = -1;
|
|
3249
|
-
for (var hj = hi - 1; hj >= 0; hj--) {
|
|
3250
|
-
if (session.history[hj].type === "message_uuid" && session.history[hj].messageType === "assistant") {
|
|
3251
|
-
assistantUuid = session.history[hj].uuid;
|
|
3252
|
-
uuidIndex = hj;
|
|
3253
|
-
break;
|
|
3254
|
-
}
|
|
3255
|
-
}
|
|
3256
|
-
|
|
3257
|
-
// Find user prompt by scanning backwards from the assistant uuid
|
|
3258
|
-
var messageSnippet = "";
|
|
3259
|
-
var searchFrom = uuidIndex >= 0 ? uuidIndex : hi;
|
|
3260
|
-
for (var hk = searchFrom - 1; hk >= 0; hk--) {
|
|
3261
|
-
if (session.history[hk].type === "user_message" && session.history[hk].text) {
|
|
3262
|
-
messageSnippet = session.history[hk].text.trim().substring(0, 100);
|
|
3263
|
-
break;
|
|
3264
|
-
}
|
|
3265
|
-
}
|
|
3266
|
-
|
|
3267
|
-
// Collect Claude's explanation: scan backwards from tool_executing
|
|
3268
|
-
// to find the nearest delta text block (skipping tool_start).
|
|
3269
|
-
// If no delta found immediately before this tool, scan past
|
|
3270
|
-
// intervening tool blocks to find the last delta text within
|
|
3271
|
-
// the same assistant turn.
|
|
3272
|
-
var assistantSnippet = "";
|
|
3273
|
-
var deltaChunks = [];
|
|
3274
|
-
for (var hd = hi - 1; hd >= 0; hd--) {
|
|
3275
|
-
var hEntry = session.history[hd];
|
|
3276
|
-
if (hEntry.type === "tool_start") continue;
|
|
3277
|
-
if (hEntry.type === "delta" && hEntry.text) {
|
|
3278
|
-
deltaChunks.unshift(hEntry.text);
|
|
3279
|
-
} else {
|
|
3280
|
-
break;
|
|
3281
|
-
}
|
|
3282
|
-
}
|
|
3283
|
-
if (deltaChunks.length === 0) {
|
|
3284
|
-
// No delta immediately before; scan past tool blocks
|
|
3285
|
-
// to find the nearest preceding delta in the same turn
|
|
3286
|
-
for (var hd2 = hi - 1; hd2 >= 0; hd2--) {
|
|
3287
|
-
var hEntry2 = session.history[hd2];
|
|
3288
|
-
if (hEntry2.type === "tool_start" || hEntry2.type === "tool_executing" || hEntry2.type === "tool_result") continue;
|
|
3289
|
-
if (hEntry2.type === "delta" && hEntry2.text) {
|
|
3290
|
-
// Found a delta before an earlier tool in the same turn.
|
|
3291
|
-
// Collect this contiguous block of deltas.
|
|
3292
|
-
for (var hd3 = hd2; hd3 >= 0; hd3--) {
|
|
3293
|
-
var hEntry3 = session.history[hd3];
|
|
3294
|
-
if (hEntry3.type === "tool_start") continue;
|
|
3295
|
-
if (hEntry3.type === "delta" && hEntry3.text) {
|
|
3296
|
-
deltaChunks.unshift(hEntry3.text);
|
|
3297
|
-
} else {
|
|
3298
|
-
break;
|
|
3299
|
-
}
|
|
3300
|
-
}
|
|
3301
|
-
break;
|
|
3302
|
-
} else {
|
|
3303
|
-
// Hit message_uuid, user_message, etc. Stop.
|
|
3304
|
-
break;
|
|
3305
|
-
}
|
|
3306
|
-
}
|
|
3307
|
-
}
|
|
3308
|
-
assistantSnippet = deltaChunks.join("").trim().substring(0, 150);
|
|
3309
|
-
|
|
3310
|
-
// Approximate timestamp: interpolate between session creation and last activity
|
|
3311
|
-
var tStart = session.createdAt || 0;
|
|
3312
|
-
var tEnd = session.lastActivity || tStart;
|
|
3313
|
-
var ts = tStart + Math.floor((hi / histLen) * (tEnd - tStart));
|
|
3314
|
-
|
|
3315
|
-
var editRecord = {
|
|
3316
|
-
source: "session",
|
|
3317
|
-
timestamp: ts,
|
|
3318
|
-
sessionLocalId: sessionLocalId,
|
|
3319
|
-
sessionTitle: sessionTitle,
|
|
3320
|
-
assistantUuid: assistantUuid,
|
|
3321
|
-
toolId: entry.id,
|
|
3322
|
-
messageSnippet: messageSnippet,
|
|
3323
|
-
assistantSnippet: assistantSnippet,
|
|
3324
|
-
toolName: entry.name,
|
|
3325
|
-
};
|
|
3326
|
-
|
|
3327
|
-
if (entry.name === "Edit") {
|
|
3328
|
-
editRecord.old_string = entry.input.old_string || "";
|
|
3329
|
-
editRecord.new_string = entry.input.new_string || "";
|
|
3330
|
-
} else {
|
|
3331
|
-
editRecord.isFullWrite = true;
|
|
3332
|
-
}
|
|
3333
|
-
|
|
3334
|
-
entries.push(editRecord);
|
|
3335
|
-
}
|
|
3336
|
-
});
|
|
3337
|
-
|
|
3338
|
-
// Collect git commits
|
|
3339
|
-
try {
|
|
3340
|
-
var gitLog = execFileSync(
|
|
3341
|
-
"git", ["log", "--format=%H|%at|%an|%s", "--follow", "--", histPath],
|
|
3342
|
-
{ cwd: cwd, encoding: "utf8", timeout: 5000 }
|
|
3343
|
-
);
|
|
3344
|
-
var gitLines = gitLog.trim().split("\n");
|
|
3345
|
-
for (var gi = 0; gi < gitLines.length; gi++) {
|
|
3346
|
-
if (!gitLines[gi]) continue;
|
|
3347
|
-
var parts = gitLines[gi].split("|");
|
|
3348
|
-
if (parts.length < 4) continue;
|
|
3349
|
-
entries.push({
|
|
3350
|
-
source: "git",
|
|
3351
|
-
hash: parts[0],
|
|
3352
|
-
timestamp: parseInt(parts[1], 10) * 1000,
|
|
3353
|
-
author: parts[2],
|
|
3354
|
-
message: parts.slice(3).join("|"),
|
|
3355
|
-
});
|
|
3356
|
-
}
|
|
3357
|
-
} catch (e) {
|
|
3358
|
-
// Not a git repo or file not tracked, that's fine
|
|
3359
|
-
}
|
|
3360
|
-
|
|
3361
|
-
// Sort by timestamp descending (newest first)
|
|
3362
|
-
entries.sort(function (a, b) { return b.timestamp - a.timestamp; });
|
|
3363
|
-
|
|
3364
|
-
sendTo(ws, { type: "fs_file_history_result", path: histPath, entries: entries });
|
|
3365
|
-
return;
|
|
3366
|
-
}
|
|
3367
|
-
|
|
3368
|
-
// --- Git diff for file history ---
|
|
3369
|
-
if (msg.type === "fs_git_diff") {
|
|
3370
|
-
var diffPath = msg.path;
|
|
3371
|
-
var hash = msg.hash;
|
|
3372
|
-
var hash2 = msg.hash2 || null;
|
|
3373
|
-
if (!diffPath || !hash) {
|
|
3374
|
-
sendTo(ws, { type: "fs_git_diff_result", hash: hash, path: diffPath, diff: "", error: "Missing params" });
|
|
3375
|
-
return;
|
|
3376
|
-
}
|
|
3377
|
-
try {
|
|
3378
|
-
var diff;
|
|
3379
|
-
if (hash2) {
|
|
3380
|
-
diff = execFileSync("git", ["diff", hash, hash2, "--", diffPath],
|
|
3381
|
-
{ cwd: cwd, encoding: "utf8", timeout: 5000 });
|
|
3382
|
-
} else {
|
|
3383
|
-
diff = execFileSync("git", ["show", hash, "--format=", "--", diffPath],
|
|
3384
|
-
{ cwd: cwd, encoding: "utf8", timeout: 5000 });
|
|
3385
|
-
}
|
|
3386
|
-
sendTo(ws, { type: "fs_git_diff_result", hash: hash, hash2: hash2, path: diffPath, diff: diff || "" });
|
|
3387
|
-
} catch (e) {
|
|
3388
|
-
sendTo(ws, { type: "fs_git_diff_result", hash: hash, hash2: hash2, path: diffPath, diff: "", error: e.message });
|
|
3389
|
-
}
|
|
3390
|
-
return;
|
|
3391
|
-
}
|
|
3392
|
-
|
|
3393
|
-
// --- File content at a git commit ---
|
|
3394
|
-
if (msg.type === "fs_file_at") {
|
|
3395
|
-
var atPath = msg.path;
|
|
3396
|
-
var atHash = msg.hash;
|
|
3397
|
-
if (!atPath || !atHash) {
|
|
3398
|
-
sendTo(ws, { type: "fs_file_at_result", hash: atHash, path: atPath, content: "", error: "Missing params" });
|
|
3399
|
-
return;
|
|
3400
|
-
}
|
|
3401
|
-
try {
|
|
3402
|
-
// Convert to repo-relative path (git show requires hash:relative/path)
|
|
3403
|
-
var atAbsPath = path.resolve(cwd, atPath);
|
|
3404
|
-
var atRelPath = path.relative(cwd, atAbsPath);
|
|
3405
|
-
var content = execFileSync("git", ["show", atHash + ":" + atRelPath],
|
|
3406
|
-
{ cwd: cwd, encoding: "utf8", timeout: 5000 });
|
|
3407
|
-
sendTo(ws, { type: "fs_file_at_result", hash: atHash, path: atPath, content: content });
|
|
3408
|
-
} catch (e) {
|
|
3409
|
-
sendTo(ws, { type: "fs_file_at_result", hash: atHash, path: atPath, content: "", error: e.message });
|
|
3410
|
-
}
|
|
3411
|
-
return;
|
|
3412
|
-
}
|
|
3413
|
-
|
|
3414
|
-
// --- Sticky notes ---
|
|
3415
|
-
function syncNotesKnowledge() {
|
|
3416
|
-
if (!isMate) return;
|
|
3417
|
-
try {
|
|
3418
|
-
var knDir = path.join(cwd, "knowledge");
|
|
3419
|
-
var knFile = path.join(knDir, "sticky-notes.md");
|
|
3420
|
-
var text = nm.getActiveNotesText();
|
|
3421
|
-
if (text) {
|
|
3422
|
-
fs.mkdirSync(knDir, { recursive: true });
|
|
3423
|
-
fs.writeFileSync(knFile, text);
|
|
3424
|
-
} else {
|
|
3425
|
-
try { fs.unlinkSync(knFile); } catch (e) {}
|
|
3426
|
-
}
|
|
3427
|
-
} catch (e) {
|
|
3428
|
-
console.error("[project] Failed to sync sticky-notes.md:", e.message);
|
|
3429
|
-
}
|
|
3430
|
-
}
|
|
3431
|
-
|
|
3432
|
-
if (msg.type === "note_create") {
|
|
3433
|
-
var note = nm.create(msg);
|
|
3434
|
-
if (note) {
|
|
3435
|
-
send({ type: "note_created", note: note });
|
|
3436
|
-
syncNotesKnowledge();
|
|
3437
|
-
}
|
|
3438
|
-
return;
|
|
3439
|
-
}
|
|
3440
|
-
|
|
3441
|
-
if (msg.type === "note_update") {
|
|
3442
|
-
if (!msg.id) return;
|
|
3443
|
-
var updated = nm.update(msg.id, msg);
|
|
3444
|
-
if (updated) {
|
|
3445
|
-
send({ type: "note_updated", note: updated });
|
|
3446
|
-
if (msg.text !== undefined || msg.hidden !== undefined) syncNotesKnowledge();
|
|
3447
|
-
}
|
|
3448
|
-
return;
|
|
3449
|
-
}
|
|
3450
|
-
|
|
3451
|
-
if (msg.type === "note_delete") {
|
|
3452
|
-
if (!msg.id) return;
|
|
3453
|
-
if (nm.remove(msg.id)) {
|
|
3454
|
-
send({ type: "note_deleted", id: msg.id });
|
|
3455
|
-
syncNotesKnowledge();
|
|
3456
|
-
}
|
|
3457
|
-
return;
|
|
3458
|
-
}
|
|
3459
|
-
|
|
3460
|
-
if (msg.type === "note_list_request") {
|
|
3461
|
-
sendTo(ws, { type: "notes_list", notes: nm.list() });
|
|
3462
|
-
return;
|
|
3463
|
-
}
|
|
3464
|
-
|
|
3465
|
-
if (msg.type === "note_bring_front") {
|
|
3466
|
-
if (!msg.id) return;
|
|
3467
|
-
var front = nm.bringToFront(msg.id);
|
|
3468
|
-
if (front) send({ type: "note_updated", note: front });
|
|
3469
|
-
return;
|
|
3470
|
-
}
|
|
3471
|
-
|
|
3472
|
-
// --- Web terminal ---
|
|
3473
|
-
if (msg.type === "term_create") {
|
|
3474
|
-
if (ws._clayUser) {
|
|
3475
|
-
var termPerms = usersModule.getEffectivePermissions(ws._clayUser, osUsers);
|
|
3476
|
-
if (!termPerms.terminal) {
|
|
3477
|
-
sendTo(ws, { type: "term_error", error: "Terminal access is not permitted" });
|
|
3478
|
-
return;
|
|
3479
|
-
}
|
|
3480
|
-
}
|
|
3481
|
-
var t = tm.create(msg.cols || 80, msg.rows || 24, getOsUserInfoForWs(ws), ws);
|
|
3482
|
-
if (!t) {
|
|
3483
|
-
sendTo(ws, { type: "term_error", error: "Cannot create terminal (node-pty not available or limit reached)" });
|
|
3484
|
-
return;
|
|
3485
|
-
}
|
|
3486
|
-
tm.attach(t.id, ws);
|
|
3487
|
-
send({ type: "term_list", terminals: tm.list() });
|
|
3488
|
-
sendTo(ws, { type: "term_created", id: t.id });
|
|
3489
|
-
return;
|
|
3490
|
-
}
|
|
3491
|
-
|
|
3492
|
-
if (msg.type === "term_attach") {
|
|
3493
|
-
if (msg.id) tm.attach(msg.id, ws);
|
|
3494
|
-
return;
|
|
3495
|
-
}
|
|
3496
|
-
|
|
3497
|
-
if (msg.type === "term_detach") {
|
|
3498
|
-
if (msg.id) tm.detach(msg.id, ws);
|
|
3499
|
-
return;
|
|
3500
|
-
}
|
|
3501
|
-
|
|
3502
|
-
if (msg.type === "term_input") {
|
|
3503
|
-
if (msg.id) tm.write(msg.id, msg.data);
|
|
3504
|
-
return;
|
|
3505
|
-
}
|
|
3506
|
-
|
|
3507
|
-
if (msg.type === "term_resize") {
|
|
3508
|
-
if (msg.id && msg.cols > 0 && msg.rows > 0) {
|
|
3509
|
-
tm.resize(msg.id, msg.cols, msg.rows, ws);
|
|
3510
|
-
}
|
|
3511
|
-
return;
|
|
3512
|
-
}
|
|
3513
|
-
|
|
3514
|
-
if (msg.type === "term_close") {
|
|
3515
|
-
if (msg.id) {
|
|
3516
|
-
tm.close(msg.id);
|
|
3517
|
-
send({ type: "term_list", terminals: tm.list() });
|
|
3518
|
-
// Remove closed terminal from context sources
|
|
3519
|
-
var saved = loadContextSources(slug);
|
|
3520
|
-
var termKey = "term:" + msg.id;
|
|
3521
|
-
var filtered = saved.filter(function(id) { return id !== termKey; });
|
|
3522
|
-
if (filtered.length !== saved.length) {
|
|
3523
|
-
saveContextSources(slug, filtered);
|
|
3524
|
-
send({ type: "context_sources_state", active: filtered });
|
|
3525
|
-
}
|
|
3526
|
-
}
|
|
3527
|
-
return;
|
|
3528
|
-
}
|
|
3529
|
-
|
|
3530
|
-
if (msg.type === "term_rename") {
|
|
3531
|
-
if (msg.id && msg.title) {
|
|
3532
|
-
tm.rename(msg.id, msg.title);
|
|
3533
|
-
send({ type: "term_list", terminals: tm.list() });
|
|
3534
|
-
}
|
|
3535
|
-
return;
|
|
3536
|
-
}
|
|
3537
|
-
|
|
3538
|
-
// --- Context Sources ---
|
|
3539
|
-
if (msg.type === "context_sources_save") {
|
|
3540
|
-
var activeIds = msg.active || [];
|
|
3541
|
-
saveContextSources(slug, activeIds);
|
|
3542
|
-
return;
|
|
3543
|
-
}
|
|
3544
|
-
|
|
3545
|
-
// --- Browser Extension ---
|
|
3546
|
-
if (msg.type === "browser_tab_list") {
|
|
3547
|
-
_extensionWs = ws; // Track which client has the extension
|
|
3548
|
-
var tabs = msg.tabs || [];
|
|
3549
|
-
_browserTabList = {};
|
|
3550
|
-
for (var bti = 0; bti < tabs.length; bti++) {
|
|
3551
|
-
_browserTabList[tabs[bti].id] = tabs[bti];
|
|
3552
|
-
}
|
|
3553
|
-
return;
|
|
3554
|
-
}
|
|
3555
|
-
|
|
3556
|
-
if (msg.type === "extension_result") {
|
|
3557
|
-
var pending = pendingExtensionRequests[msg.requestId];
|
|
3558
|
-
if (pending) {
|
|
3559
|
-
clearTimeout(pending.timer);
|
|
3560
|
-
pending.resolve(msg.result);
|
|
3561
|
-
delete pendingExtensionRequests[msg.requestId];
|
|
3562
|
-
}
|
|
3563
|
-
return;
|
|
3564
|
-
}
|
|
3565
|
-
|
|
3566
|
-
// --- Scheduled tasks permission gate ---
|
|
3567
|
-
if (msg.type === "loop_start" || msg.type === "loop_stop" || msg.type === "loop_registry_files" ||
|
|
3568
|
-
msg.type === "loop_registry_list" || msg.type === "loop_registry_update" || msg.type === "loop_registry_rename" ||
|
|
3569
|
-
msg.type === "loop_registry_remove" || msg.type === "loop_registry_convert" || msg.type === "loop_registry_toggle" ||
|
|
3570
|
-
msg.type === "loop_registry_rerun" || msg.type === "schedule_create" || msg.type === "schedule_move") {
|
|
3571
|
-
if (ws._clayUser) {
|
|
3572
|
-
var schPerms = usersModule.getEffectivePermissions(ws._clayUser, osUsers);
|
|
3573
|
-
if (!schPerms.scheduledTasks) {
|
|
3574
|
-
sendTo(ws, { type: "error", text: "Scheduled tasks access is not permitted" });
|
|
3575
|
-
return;
|
|
3576
|
-
}
|
|
3577
|
-
}
|
|
3578
|
-
}
|
|
3579
|
-
|
|
3580
|
-
if (msg.type === "loop_start") {
|
|
3581
|
-
// If this loop has a cron schedule, don't run immediately — just confirm registration
|
|
3582
|
-
if (loopState.wizardData && loopState.wizardData.cron) {
|
|
3583
|
-
loopState.active = false;
|
|
3584
|
-
loopState.phase = "done";
|
|
3585
|
-
saveLoopState();
|
|
3586
|
-
send({ type: "loop_finished", reason: "scheduled", iterations: 0, results: [] });
|
|
3587
|
-
send({ type: "ralph_phase", phase: "idle", wizardData: null });
|
|
3588
|
-
send({ type: "loop_scheduled", recordId: loopState.loopId, cron: loopState.wizardData.cron });
|
|
3589
|
-
return;
|
|
3590
|
-
}
|
|
3591
|
-
startLoop();
|
|
3592
|
-
return;
|
|
3593
|
-
}
|
|
3594
|
-
|
|
3595
|
-
if (msg.type === "loop_stop") {
|
|
3596
|
-
stopLoop();
|
|
3597
|
-
return;
|
|
3598
|
-
}
|
|
3599
|
-
|
|
3600
|
-
if (msg.type === "ralph_wizard_complete") {
|
|
3601
|
-
var wData = msg.data || {};
|
|
3602
|
-
var maxIter = wData.maxIterations || 3;
|
|
3603
|
-
var wizardCron = wData.cron || null;
|
|
3604
|
-
var newLoopId = generateLoopId();
|
|
3605
|
-
loopState.loopId = newLoopId;
|
|
3606
|
-
loopState.wizardData = {
|
|
3607
|
-
name: wData.name || wData.task || "Untitled",
|
|
3608
|
-
task: wData.task || "",
|
|
3609
|
-
maxIterations: maxIter,
|
|
3610
|
-
cron: wizardCron,
|
|
3611
|
-
};
|
|
3612
|
-
loopState.phase = "crafting";
|
|
3613
|
-
loopState.startedAt = Date.now();
|
|
3614
|
-
saveLoopState();
|
|
3615
|
-
|
|
3616
|
-
// Register in loop registry
|
|
3617
|
-
var recordSource = wData.source === "task" ? null : "ralph";
|
|
3618
|
-
loopRegistry.register({
|
|
3619
|
-
id: newLoopId,
|
|
3620
|
-
name: loopState.wizardData.name,
|
|
3621
|
-
task: wData.task || "",
|
|
3622
|
-
cron: wizardCron,
|
|
3623
|
-
enabled: wizardCron ? true : false,
|
|
3624
|
-
maxIterations: maxIter,
|
|
3625
|
-
source: recordSource,
|
|
3626
|
-
});
|
|
3627
|
-
|
|
3628
|
-
// Create loop directory and write LOOP.json
|
|
3629
|
-
var lDir = loopDir();
|
|
3630
|
-
try { fs.mkdirSync(lDir, { recursive: true }); } catch (e) {}
|
|
3631
|
-
var loopJsonPath = path.join(lDir, "LOOP.json");
|
|
3632
|
-
var tmpLoopJson = loopJsonPath + ".tmp";
|
|
3633
|
-
fs.writeFileSync(tmpLoopJson, JSON.stringify({ maxIterations: maxIter }, null, 2));
|
|
3634
|
-
fs.renameSync(tmpLoopJson, loopJsonPath);
|
|
3635
|
-
|
|
3636
|
-
var craftName = (loopState.wizardData && loopState.wizardData.name) || "";
|
|
3637
|
-
var isRalphCraft = recordSource === "ralph";
|
|
3638
|
-
|
|
3639
|
-
// User provided their own PROMPT.md (and optionally JUDGE.md)
|
|
3640
|
-
if (wData.mode === "own" && wData.promptText) {
|
|
3641
|
-
// Write PROMPT.md
|
|
3642
|
-
var promptPath = path.join(lDir, "PROMPT.md");
|
|
3643
|
-
var tmpPrompt = promptPath + ".tmp";
|
|
3644
|
-
fs.writeFileSync(tmpPrompt, wData.promptText);
|
|
3645
|
-
fs.renameSync(tmpPrompt, promptPath);
|
|
3646
|
-
|
|
3647
|
-
if (wData.judgeText) {
|
|
3648
|
-
// Both provided: write JUDGE.md too
|
|
3649
|
-
var judgePath = path.join(lDir, "JUDGE.md");
|
|
3650
|
-
var tmpJudge = judgePath + ".tmp";
|
|
3651
|
-
fs.writeFileSync(tmpJudge, wData.judgeText);
|
|
3652
|
-
fs.renameSync(tmpJudge, judgePath);
|
|
3653
|
-
} else if (!recordSource) {
|
|
3654
|
-
// Scheduled task with no judge: force single iteration and go to approval
|
|
3655
|
-
var singleJson = loopJsonPath + ".tmp2";
|
|
3656
|
-
fs.writeFileSync(singleJson, JSON.stringify({ maxIterations: 1 }, null, 2));
|
|
3657
|
-
fs.renameSync(singleJson, loopJsonPath);
|
|
3658
|
-
|
|
3659
|
-
loopState.phase = "approval";
|
|
3660
|
-
saveLoopState();
|
|
3661
|
-
send({ type: "ralph_phase", phase: "approval", source: recordSource, wizardData: loopState.wizardData });
|
|
3662
|
-
send({ type: "ralph_files_status", promptReady: true, judgeReady: false, bothReady: true });
|
|
3663
|
-
return;
|
|
3664
|
-
} else {
|
|
3665
|
-
// Ralph with no judge: start a crafting session to create JUDGE.md
|
|
3666
|
-
loopState.phase = "crafting";
|
|
3667
|
-
saveLoopState();
|
|
3668
|
-
|
|
3669
|
-
var judgeCraftPrompt = "Use the /clay-ralph skill to design ONLY a JUDGE.md for an existing Ralph Loop. " +
|
|
3670
|
-
"The user has already provided PROMPT.md, so do NOT create or modify PROMPT.md. " +
|
|
3671
|
-
"You MUST invoke the clay-ralph skill — do NOT execute the task yourself. " +
|
|
3672
|
-
"Your job is to read the existing PROMPT.md and create a JUDGE.md " +
|
|
3673
|
-
"that will evaluate whether the coder session completed the task successfully.\n\n" +
|
|
3674
|
-
"## Task\n" + (wData.task || "") +
|
|
3675
|
-
"\n\n## Loop Directory\n" + lDir;
|
|
3676
|
-
|
|
3677
|
-
var judgeCraftSession = sm.createSession();
|
|
3678
|
-
judgeCraftSession.title = (isRalphCraft ? "Ralph" : "Task") + (craftName ? " " + craftName : "") + " Crafting";
|
|
3679
|
-
judgeCraftSession.ralphCraftingMode = true;
|
|
3680
|
-
judgeCraftSession.loop = { active: true, iteration: 0, role: "crafting", loopId: newLoopId, name: craftName, source: recordSource, startedAt: loopState.startedAt };
|
|
3681
|
-
sm.saveSessionFile(judgeCraftSession);
|
|
3682
|
-
sm.switchSession(judgeCraftSession.localId, null, hydrateImageRefs);
|
|
3683
|
-
loopState.craftingSessionId = judgeCraftSession.localId;
|
|
3684
|
-
|
|
3685
|
-
loopRegistry.updateRecord(newLoopId, { craftingSessionId: judgeCraftSession.localId });
|
|
3686
|
-
|
|
3687
|
-
startClaudeDirWatch();
|
|
3688
|
-
|
|
3689
|
-
judgeCraftSession.history.push({ type: "user_message", text: judgeCraftPrompt });
|
|
3690
|
-
sm.appendToSessionFile(judgeCraftSession, { type: "user_message", text: judgeCraftPrompt });
|
|
3691
|
-
sendToSession(judgeCraftSession.localId, { type: "user_message", text: judgeCraftPrompt });
|
|
3692
|
-
judgeCraftSession.isProcessing = true;
|
|
3693
|
-
onProcessingChanged();
|
|
3694
|
-
judgeCraftSession.sentToolResults = {};
|
|
3695
|
-
sendToSession(judgeCraftSession.localId, { type: "status", status: "processing" });
|
|
3696
|
-
sdk.startQuery(judgeCraftSession, judgeCraftPrompt, undefined, getLinuxUserForSession(judgeCraftSession));
|
|
3697
|
-
|
|
3698
|
-
send({ type: "ralph_crafting_started", sessionId: judgeCraftSession.localId, taskId: newLoopId, source: recordSource });
|
|
3699
|
-
send({ type: "ralph_phase", phase: "crafting", wizardData: loopState.wizardData, craftingSessionId: judgeCraftSession.localId });
|
|
3700
|
-
send({ type: "ralph_files_status", promptReady: true, judgeReady: false, bothReady: false });
|
|
3701
|
-
return;
|
|
3702
|
-
}
|
|
3703
|
-
|
|
3704
|
-
// Both prompt and judge provided: go straight to approval
|
|
3705
|
-
loopState.phase = "approval";
|
|
3706
|
-
saveLoopState();
|
|
3707
|
-
send({ type: "ralph_phase", phase: "approval", source: recordSource, wizardData: loopState.wizardData });
|
|
3708
|
-
send({ type: "ralph_files_status", promptReady: true, judgeReady: true, bothReady: true });
|
|
3709
|
-
return;
|
|
3710
|
-
}
|
|
3711
|
-
|
|
3712
|
-
// Default: "draft" mode — Clay crafts both PROMPT.md and JUDGE.md
|
|
3713
|
-
var craftingPrompt = "Use the /clay-ralph skill to design a Ralph Loop for the following task. " +
|
|
3714
|
-
"You MUST invoke the clay-ralph skill — do NOT execute the task yourself. " +
|
|
3715
|
-
"Your job is to interview me, then create PROMPT.md and JUDGE.md files " +
|
|
3716
|
-
"that a future autonomous session will execute.\n\n" +
|
|
3717
|
-
"## Task\n" + (wData.task || "") +
|
|
3718
|
-
"\n\n## Loop Directory\n" + lDir;
|
|
3719
|
-
|
|
3720
|
-
// Create a new session for crafting
|
|
3721
|
-
var craftingSession = sm.createSession();
|
|
3722
|
-
craftingSession.title = (isRalphCraft ? "Ralph" : "Task") + (craftName ? " " + craftName : "") + " Crafting";
|
|
3723
|
-
craftingSession.ralphCraftingMode = true;
|
|
3724
|
-
craftingSession.loop = { active: true, iteration: 0, role: "crafting", loopId: newLoopId, name: craftName, source: recordSource, startedAt: loopState.startedAt };
|
|
3725
|
-
sm.saveSessionFile(craftingSession);
|
|
3726
|
-
sm.switchSession(craftingSession.localId, null, hydrateImageRefs);
|
|
3727
|
-
loopState.craftingSessionId = craftingSession.localId;
|
|
3728
|
-
|
|
3729
|
-
// Store crafting session ID in the registry record
|
|
3730
|
-
loopRegistry.updateRecord(newLoopId, { craftingSessionId: craftingSession.localId });
|
|
3731
|
-
|
|
3732
|
-
// Start .claude/ directory watcher
|
|
3733
|
-
startClaudeDirWatch();
|
|
3734
|
-
|
|
3735
|
-
// Send crafting prompt and start the conversation with Claude.
|
|
3736
|
-
craftingSession.history.push({ type: "user_message", text: craftingPrompt });
|
|
3737
|
-
sm.appendToSessionFile(craftingSession, { type: "user_message", text: craftingPrompt });
|
|
3738
|
-
sendToSession(craftingSession.localId, { type: "user_message", text: craftingPrompt });
|
|
3739
|
-
craftingSession.isProcessing = true;
|
|
3740
|
-
onProcessingChanged();
|
|
3741
|
-
craftingSession.sentToolResults = {};
|
|
3742
|
-
sendToSession(craftingSession.localId, { type: "status", status: "processing" });
|
|
3743
|
-
sdk.startQuery(craftingSession, craftingPrompt, undefined, getLinuxUserForSession(craftingSession));
|
|
3744
|
-
|
|
3745
|
-
send({ type: "ralph_crafting_started", sessionId: craftingSession.localId, taskId: newLoopId, source: recordSource });
|
|
3746
|
-
send({ type: "ralph_phase", phase: "crafting", wizardData: loopState.wizardData, craftingSessionId: craftingSession.localId });
|
|
3747
|
-
return;
|
|
3748
|
-
}
|
|
3749
|
-
|
|
3750
|
-
if (msg.type === "loop_registry_files") {
|
|
3751
|
-
var recId = msg.id;
|
|
3752
|
-
var lDir = path.join(cwd, ".claude", "loops", recId);
|
|
3753
|
-
var promptContent = "";
|
|
3754
|
-
var judgeContent = "";
|
|
3755
|
-
try { promptContent = fs.readFileSync(path.join(lDir, "PROMPT.md"), "utf8"); } catch (e) {}
|
|
3756
|
-
try { judgeContent = fs.readFileSync(path.join(lDir, "JUDGE.md"), "utf8"); } catch (e) {}
|
|
3757
|
-
send({
|
|
3758
|
-
type: "loop_registry_files_content",
|
|
3759
|
-
id: recId,
|
|
3760
|
-
prompt: promptContent,
|
|
3761
|
-
judge: judgeContent,
|
|
3762
|
-
});
|
|
3763
|
-
return;
|
|
3764
|
-
}
|
|
3765
|
-
|
|
3766
|
-
if (msg.type === "ralph_preview_files") {
|
|
3767
|
-
var promptContent = "";
|
|
3768
|
-
var judgeContent = "";
|
|
3769
|
-
var previewDir = loopDir();
|
|
3770
|
-
if (previewDir) {
|
|
3771
|
-
try { promptContent = fs.readFileSync(path.join(previewDir, "PROMPT.md"), "utf8"); } catch (e) {}
|
|
3772
|
-
try { judgeContent = fs.readFileSync(path.join(previewDir, "JUDGE.md"), "utf8"); } catch (e) {}
|
|
3773
|
-
}
|
|
3774
|
-
sendTo(ws, {
|
|
3775
|
-
type: "ralph_files_content",
|
|
3776
|
-
prompt: promptContent,
|
|
3777
|
-
judge: judgeContent,
|
|
3778
|
-
});
|
|
3779
|
-
return;
|
|
3780
|
-
}
|
|
3781
|
-
|
|
3782
|
-
if (msg.type === "ralph_wizard_cancel") {
|
|
3783
|
-
stopClaudeDirWatch();
|
|
3784
|
-
// Clean up loop directory
|
|
3785
|
-
var cancelDir = loopDir();
|
|
3786
|
-
if (cancelDir) {
|
|
3787
|
-
try { fs.rmSync(cancelDir, { recursive: true, force: true }); } catch (e) {}
|
|
3788
|
-
}
|
|
3789
|
-
clearLoopState();
|
|
3790
|
-
send({ type: "ralph_phase", phase: "idle", wizardData: null });
|
|
3791
|
-
return;
|
|
3792
|
-
}
|
|
3793
|
-
|
|
3794
|
-
if (msg.type === "ralph_cancel_crafting") {
|
|
3795
|
-
// Abort the crafting session if running
|
|
3796
|
-
if (loopState.craftingSessionId != null) {
|
|
3797
|
-
var craftSession = sm.sessions.get(loopState.craftingSessionId) || null;
|
|
3798
|
-
if (craftSession && craftSession.abortController) {
|
|
3799
|
-
craftSession.abortController.abort();
|
|
3800
|
-
}
|
|
3801
|
-
}
|
|
3802
|
-
stopClaudeDirWatch();
|
|
3803
|
-
// Clean up loop directory
|
|
3804
|
-
var craftCancelDir = loopDir();
|
|
3805
|
-
if (craftCancelDir) {
|
|
3806
|
-
try { fs.rmSync(craftCancelDir, { recursive: true, force: true }); } catch (e) {}
|
|
3807
|
-
}
|
|
3808
|
-
clearLoopState();
|
|
3809
|
-
send({ type: "ralph_phase", phase: "idle", wizardData: null });
|
|
3810
|
-
return;
|
|
3811
|
-
}
|
|
3812
|
-
|
|
3813
|
-
// --- Schedule create (from calendar click) ---
|
|
3814
|
-
if (msg.type === "schedule_create") {
|
|
3815
|
-
var sData = msg.data || {};
|
|
3816
|
-
var newRec = loopRegistry.register({
|
|
3817
|
-
name: sData.name || "Untitled",
|
|
3818
|
-
task: sData.name || "",
|
|
3819
|
-
description: sData.description || "",
|
|
3820
|
-
date: sData.date || null,
|
|
3821
|
-
time: sData.time || null,
|
|
3822
|
-
allDay: sData.allDay !== undefined ? sData.allDay : true,
|
|
3823
|
-
linkedTaskId: sData.taskId || null,
|
|
3824
|
-
cron: sData.cron || null,
|
|
3825
|
-
enabled: sData.cron ? (sData.enabled !== false) : false,
|
|
3826
|
-
maxIterations: sData.maxIterations || 3,
|
|
3827
|
-
source: "schedule",
|
|
3828
|
-
color: sData.color || null,
|
|
3829
|
-
recurrenceEnd: sData.recurrenceEnd || null,
|
|
3830
|
-
skipIfRunning: sData.skipIfRunning !== undefined ? sData.skipIfRunning : true,
|
|
3831
|
-
intervalEnd: sData.intervalEnd || null,
|
|
3832
|
-
});
|
|
3833
|
-
return;
|
|
3834
|
-
}
|
|
3835
|
-
|
|
3836
|
-
// --- Hub: cross-project schedule aggregation ---
|
|
3837
|
-
if (msg.type === "hub_schedules_list") {
|
|
3838
|
-
sendTo(ws, { type: "hub_schedules", schedules: getHubSchedules() });
|
|
3839
|
-
return;
|
|
3840
|
-
}
|
|
3841
|
-
|
|
3842
|
-
// --- Loop Registry messages ---
|
|
3843
|
-
if (msg.type === "loop_registry_list") {
|
|
3844
|
-
sendTo(ws, { type: "loop_registry_updated", records: getHubSchedules() });
|
|
3845
|
-
return;
|
|
3846
|
-
}
|
|
3847
|
-
|
|
3848
|
-
if (msg.type === "loop_registry_update") {
|
|
3849
|
-
var updatedRec = loopRegistry.update(msg.id, msg.data || {});
|
|
3850
|
-
if (!updatedRec) {
|
|
3851
|
-
sendTo(ws, { type: "loop_registry_error", text: "Record not found" });
|
|
3852
|
-
}
|
|
3853
|
-
return;
|
|
3854
|
-
}
|
|
3855
|
-
|
|
3856
|
-
if (msg.type === "loop_registry_rename") {
|
|
3857
|
-
if (msg.id && msg.name) {
|
|
3858
|
-
loopRegistry.updateRecord(msg.id, { name: String(msg.name).substring(0, 100) });
|
|
3859
|
-
sm.broadcastSessionList();
|
|
3860
|
-
}
|
|
3861
|
-
return;
|
|
3862
|
-
}
|
|
3863
|
-
|
|
3864
|
-
if (msg.type === "loop_registry_remove") {
|
|
3865
|
-
var removedRec = loopRegistry.remove(msg.id);
|
|
3866
|
-
if (!removedRec) {
|
|
3867
|
-
sendTo(ws, { type: "loop_registry_error", text: "Record not found" });
|
|
3868
|
-
}
|
|
3869
|
-
return;
|
|
3870
|
-
}
|
|
3871
|
-
|
|
3872
|
-
if (msg.type === "loop_registry_convert") {
|
|
3873
|
-
// Convert ralph source to regular task (remove source tag)
|
|
3874
|
-
if (msg.id) {
|
|
3875
|
-
loopRegistry.updateRecord(msg.id, { source: null });
|
|
3876
|
-
sm.broadcastSessionList();
|
|
3877
|
-
}
|
|
3878
|
-
return;
|
|
3879
|
-
}
|
|
3880
|
-
|
|
3881
|
-
if (msg.type === "delete_loop_group") {
|
|
3882
|
-
// Delete all sessions belonging to this loopId, then remove registry record
|
|
3883
|
-
var loopIdToDel = msg.loopId;
|
|
3884
|
-
if (!loopIdToDel) return;
|
|
3885
|
-
var sessionIds = [];
|
|
3886
|
-
sm.sessions.forEach(function (s, lid) {
|
|
3887
|
-
if (s.loop && s.loop.loopId === loopIdToDel) sessionIds.push(lid);
|
|
3888
|
-
});
|
|
3889
|
-
for (var di = 0; di < sessionIds.length; di++) {
|
|
3890
|
-
sm.deleteSessionQuiet(sessionIds[di]);
|
|
3891
|
-
}
|
|
3892
|
-
loopRegistry.remove(loopIdToDel);
|
|
3893
|
-
sm.broadcastSessionList();
|
|
3894
|
-
return;
|
|
3895
|
-
}
|
|
685
|
+
// --- Knowledge file management (delegated to project-knowledge.js) ---
|
|
686
|
+
if (_knowledge.handleKnowledgeMessage(ws, msg)) return;
|
|
3896
687
|
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
}
|
|
3902
|
-
return;
|
|
3903
|
-
}
|
|
3904
|
-
|
|
3905
|
-
if (msg.type === "loop_registry_rerun") {
|
|
3906
|
-
// Re-run an existing job (one-off from library)
|
|
3907
|
-
if (loopState.active || loopState.phase === "executing") {
|
|
3908
|
-
sendTo(ws, { type: "loop_registry_error", text: "A loop is already running" });
|
|
3909
|
-
return;
|
|
3910
|
-
}
|
|
3911
|
-
var rerunRec = loopRegistry.getById(msg.id);
|
|
3912
|
-
if (!rerunRec) {
|
|
3913
|
-
sendTo(ws, { type: "loop_registry_error", text: "Record not found" });
|
|
3914
|
-
return;
|
|
3915
|
-
}
|
|
3916
|
-
var rerunDir = path.join(cwd, ".claude", "loops", rerunRec.id);
|
|
3917
|
-
try {
|
|
3918
|
-
fs.accessSync(path.join(rerunDir, "PROMPT.md"));
|
|
3919
|
-
} catch (e) {
|
|
3920
|
-
sendTo(ws, { type: "loop_registry_error", text: "PROMPT.md missing for " + rerunRec.id });
|
|
3921
|
-
return;
|
|
3922
|
-
}
|
|
3923
|
-
loopState.loopId = rerunRec.id;
|
|
3924
|
-
loopState.loopFilesId = null;
|
|
3925
|
-
activeRegistryId = null; // not a scheduled trigger
|
|
3926
|
-
send({ type: "loop_rerun_started", recordId: rerunRec.id });
|
|
3927
|
-
startLoop();
|
|
3928
|
-
return;
|
|
3929
|
-
}
|
|
3930
|
-
|
|
3931
|
-
// --- Schedule message for after rate limit resets ---
|
|
3932
|
-
if (msg.type === "schedule_message") {
|
|
3933
|
-
var schedSession = getSessionForWs(ws);
|
|
3934
|
-
if (!schedSession || !msg.text || !msg.resetsAt) return;
|
|
3935
|
-
scheduleMessage(schedSession, msg.text, msg.resetsAt);
|
|
3936
|
-
return;
|
|
3937
|
-
}
|
|
3938
|
-
|
|
3939
|
-
if (msg.type === "cancel_scheduled_message") {
|
|
3940
|
-
var cancelSession = getSessionForWs(ws);
|
|
3941
|
-
if (!cancelSession) return;
|
|
3942
|
-
cancelScheduledMessage(cancelSession);
|
|
3943
|
-
return;
|
|
3944
|
-
}
|
|
3945
|
-
|
|
3946
|
-
if (msg.type === "send_scheduled_now") {
|
|
3947
|
-
var nowSession = getSessionForWs(ws);
|
|
3948
|
-
if (!nowSession || !nowSession.scheduledMessage) return;
|
|
3949
|
-
var schedText = nowSession.scheduledMessage.text;
|
|
3950
|
-
clearTimeout(nowSession.scheduledMessage.timer);
|
|
3951
|
-
nowSession.scheduledMessage = null;
|
|
3952
|
-
console.log("[project] Scheduled message sent immediately for session " + nowSession.localId);
|
|
3953
|
-
sm.sendAndRecord(nowSession, { type: "scheduled_message_sent" });
|
|
3954
|
-
var userMsg = { type: "user_message", text: schedText };
|
|
3955
|
-
nowSession.history.push(userMsg);
|
|
3956
|
-
sm.appendToSessionFile(nowSession, userMsg);
|
|
3957
|
-
sendToSession(nowSession.localId, userMsg);
|
|
3958
|
-
nowSession.isProcessing = true;
|
|
3959
|
-
onProcessingChanged();
|
|
3960
|
-
sendToSession(nowSession.localId, { type: "status", status: "processing" });
|
|
3961
|
-
sdk.startQuery(nowSession, schedText, null, getLinuxUserForSession(nowSession));
|
|
3962
|
-
sm.broadcastSessionList();
|
|
3963
|
-
return;
|
|
3964
|
-
}
|
|
3965
|
-
|
|
3966
|
-
if (msg.type !== "message") return;
|
|
3967
|
-
if (!msg.text && (!msg.images || msg.images.length === 0) && (!msg.pastes || msg.pastes.length === 0)) return;
|
|
3968
|
-
|
|
3969
|
-
var session = getSessionForWs(ws);
|
|
3970
|
-
if (!session) return;
|
|
3971
|
-
|
|
3972
|
-
// Backfill ownerId for legacy sessions restored without one (multi-user only)
|
|
3973
|
-
if (!session.ownerId && ws._clayUser && usersModule.isMultiUser()) {
|
|
3974
|
-
session.ownerId = ws._clayUser.id;
|
|
3975
|
-
sm.saveSessionFile(session);
|
|
3976
|
-
}
|
|
3977
|
-
|
|
3978
|
-
// Keep any pending scheduled message alive when user sends a regular message
|
|
3979
|
-
|
|
3980
|
-
var userMsg = { type: "user_message", text: msg.text || "" };
|
|
3981
|
-
// Attach sender info for multi-user attribution (backward-compatible: old clients ignore these)
|
|
3982
|
-
if (ws._clayUser) {
|
|
3983
|
-
userMsg.from = ws._clayUser.id;
|
|
3984
|
-
userMsg.fromName = ws._clayUser.displayName || ws._clayUser.username || "";
|
|
3985
|
-
}
|
|
3986
|
-
var savedImagePaths = [];
|
|
3987
|
-
if (msg.images && msg.images.length > 0) {
|
|
3988
|
-
userMsg.imageCount = msg.images.length;
|
|
3989
|
-
// Save images as files, store URL references in history
|
|
3990
|
-
var imageRefs = [];
|
|
3991
|
-
for (var imgIdx = 0; imgIdx < msg.images.length; imgIdx++) {
|
|
3992
|
-
var img = msg.images[imgIdx];
|
|
3993
|
-
var savedName = saveImageFile(img.mediaType, img.data, getLinuxUserForSession(session));
|
|
3994
|
-
if (savedName) {
|
|
3995
|
-
imageRefs.push({ mediaType: img.mediaType, file: savedName });
|
|
3996
|
-
savedImagePaths.push(path.join(imagesDir, savedName));
|
|
3997
|
-
}
|
|
3998
|
-
}
|
|
3999
|
-
if (imageRefs.length > 0) {
|
|
4000
|
-
userMsg.imageRefs = imageRefs;
|
|
4001
|
-
}
|
|
4002
|
-
}
|
|
4003
|
-
if (msg.pastes && msg.pastes.length > 0) {
|
|
4004
|
-
userMsg.pastes = msg.pastes;
|
|
4005
|
-
}
|
|
4006
|
-
session.history.push(userMsg);
|
|
4007
|
-
sm.appendToSessionFile(session, userMsg);
|
|
4008
|
-
sendToSessionOthers(ws, session.localId, hydrateImageRefs(userMsg));
|
|
4009
|
-
|
|
4010
|
-
if (!session.title) {
|
|
4011
|
-
session.title = (msg.text || "Image").substring(0, 50);
|
|
4012
|
-
sm.saveSessionFile(session);
|
|
4013
|
-
sm.broadcastSessionList();
|
|
4014
|
-
// Sync auto-title to SDK
|
|
4015
|
-
if (session.cliSessionId) {
|
|
4016
|
-
getSDK().then(function(sdk) {
|
|
4017
|
-
sdk.renameSession(session.cliSessionId, session.title, { dir: cwd }).catch(function(e) {
|
|
4018
|
-
console.error("[project] SDK renameSession failed:", e.message);
|
|
4019
|
-
});
|
|
4020
|
-
}).catch(function() {});
|
|
4021
|
-
}
|
|
4022
|
-
}
|
|
4023
|
-
|
|
4024
|
-
var fullText = msg.text || "";
|
|
4025
|
-
// Prepend saved image paths so Claude can copy/save them
|
|
4026
|
-
if (savedImagePaths.length > 0) {
|
|
4027
|
-
var imgPathLines = savedImagePaths.map(function (p) { return "[Uploaded image: " + p + "]"; }).join("\n");
|
|
4028
|
-
fullText = imgPathLines + (fullText ? "\n" + fullText : "");
|
|
4029
|
-
}
|
|
4030
|
-
if (msg.pastes && msg.pastes.length > 0) {
|
|
4031
|
-
for (var pi = 0; pi < msg.pastes.length; pi++) {
|
|
4032
|
-
if (fullText) fullText += "\n\n";
|
|
4033
|
-
fullText += msg.pastes[pi];
|
|
4034
|
-
}
|
|
4035
|
-
}
|
|
4036
|
-
|
|
4037
|
-
// Inject pending @mention context so the current agent sees the exchange
|
|
4038
|
-
if (session.pendingMentionContexts && session.pendingMentionContexts.length > 0) {
|
|
4039
|
-
var mentionPrefix = session.pendingMentionContexts.join("\n\n");
|
|
4040
|
-
session.pendingMentionContexts = [];
|
|
4041
|
-
fullText = mentionPrefix + "\n\n" + fullText;
|
|
4042
|
-
}
|
|
4043
|
-
|
|
4044
|
-
// Inject active terminal context sources (delta only: send new output since last message)
|
|
4045
|
-
var TERM_CONTEXT_MAX = 8192; // 8KB max per terminal per message
|
|
4046
|
-
var TERM_HEAD_SIZE = 2048; // keep first 2KB for error context
|
|
4047
|
-
var TERM_TAIL_SIZE = 6144; // keep last 6KB for recent state
|
|
4048
|
-
var ctxSources = loadContextSources(slug);
|
|
4049
|
-
if (ctxSources.length > 0) {
|
|
4050
|
-
if (!session._termContextCursors) session._termContextCursors = {};
|
|
4051
|
-
var termContextParts = [];
|
|
4052
|
-
for (var ci = 0; ci < ctxSources.length; ci++) {
|
|
4053
|
-
var srcId = ctxSources[ci];
|
|
4054
|
-
if (srcId.startsWith("term:")) {
|
|
4055
|
-
var termId = parseInt(srcId.split(":")[1], 10);
|
|
4056
|
-
var sb = tm.getScrollback(termId);
|
|
4057
|
-
if (sb) {
|
|
4058
|
-
var lastCursor;
|
|
4059
|
-
if (termId in session._termContextCursors) {
|
|
4060
|
-
lastCursor = session._termContextCursors[termId];
|
|
4061
|
-
// Terminal was recycled (closed and reopened with same ID) — reset cursor
|
|
4062
|
-
if (lastCursor > sb.totalBytesWritten) lastCursor = 0;
|
|
4063
|
-
} else {
|
|
4064
|
-
// First time seeing this terminal — include last 8KB (what user can see now)
|
|
4065
|
-
lastCursor = Math.max(0, sb.totalBytesWritten - TERM_CONTEXT_MAX);
|
|
4066
|
-
}
|
|
4067
|
-
var newBytes = sb.totalBytesWritten - lastCursor;
|
|
4068
|
-
session._termContextCursors[termId] = sb.totalBytesWritten;
|
|
4069
|
-
if (newBytes <= 0) continue;
|
|
4070
|
-
// Build timestamped delta from chunks
|
|
4071
|
-
var deltaChunks = [];
|
|
4072
|
-
var bytePos = sb.bufferStart;
|
|
4073
|
-
for (var chunkIdx = 0; chunkIdx < sb.chunks.length; chunkIdx++) {
|
|
4074
|
-
var chunk = sb.chunks[chunkIdx];
|
|
4075
|
-
var chunkEnd = bytePos + chunk.data.length;
|
|
4076
|
-
if (chunkEnd > lastCursor) {
|
|
4077
|
-
// This chunk has new content
|
|
4078
|
-
var chunkData = chunk.data;
|
|
4079
|
-
if (bytePos < lastCursor) {
|
|
4080
|
-
// Partial chunk: only the part after lastCursor
|
|
4081
|
-
chunkData = chunkData.slice(lastCursor - bytePos);
|
|
4082
|
-
}
|
|
4083
|
-
deltaChunks.push({ ts: chunk.ts, data: chunkData });
|
|
4084
|
-
}
|
|
4085
|
-
bytePos = chunkEnd;
|
|
4086
|
-
}
|
|
4087
|
-
if (deltaChunks.length === 0) continue;
|
|
4088
|
-
// Format with timestamps: group by second to avoid excessive timestamps
|
|
4089
|
-
var lines = [];
|
|
4090
|
-
var lastTimeSec = 0;
|
|
4091
|
-
for (var di = 0; di < deltaChunks.length; di++) {
|
|
4092
|
-
var dc = deltaChunks[di];
|
|
4093
|
-
var cleaned = dc.data.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "");
|
|
4094
|
-
if (!cleaned) continue;
|
|
4095
|
-
var timeSec = Math.floor(dc.ts / 1000);
|
|
4096
|
-
if (timeSec !== lastTimeSec) {
|
|
4097
|
-
var d = new Date(dc.ts);
|
|
4098
|
-
var timeStr = d.toTimeString().slice(0, 8); // HH:MM:SS
|
|
4099
|
-
lines.push("[" + timeStr + "] " + cleaned);
|
|
4100
|
-
lastTimeSec = timeSec;
|
|
4101
|
-
} else {
|
|
4102
|
-
lines.push(cleaned);
|
|
4103
|
-
}
|
|
4104
|
-
}
|
|
4105
|
-
var delta = lines.join("").trim();
|
|
4106
|
-
if (!delta) continue;
|
|
4107
|
-
var termInfo = tm.list().find(function(t) { return t.id === termId; });
|
|
4108
|
-
var termTitle = termInfo ? termInfo.title : "Terminal " + termId;
|
|
4109
|
-
var header;
|
|
4110
|
-
if (delta.length > TERM_CONTEXT_MAX) {
|
|
4111
|
-
var head = delta.slice(0, TERM_HEAD_SIZE);
|
|
4112
|
-
var tail = delta.slice(-TERM_TAIL_SIZE);
|
|
4113
|
-
var omittedBytes = delta.length - TERM_HEAD_SIZE - TERM_TAIL_SIZE;
|
|
4114
|
-
var omittedLines = delta.slice(TERM_HEAD_SIZE, delta.length - TERM_TAIL_SIZE).split("\n").length;
|
|
4115
|
-
delta = head + "\n\n... (" + omittedLines + " lines / " + Math.round(omittedBytes / 1024) + "KB omitted) ...\n\n" + tail;
|
|
4116
|
-
header = "[New terminal output from " + termTitle + " (large output, head+tail shown)]";
|
|
4117
|
-
} else {
|
|
4118
|
-
header = "[New terminal output from " + termTitle + "]";
|
|
4119
|
-
}
|
|
4120
|
-
termContextParts.push(header + "\n```\n" + delta + "\n```");
|
|
4121
|
-
}
|
|
4122
|
-
}
|
|
4123
|
-
}
|
|
4124
|
-
if (termContextParts.length > 0) {
|
|
4125
|
-
fullText = termContextParts.join("\n\n") + "\n\n" + fullText;
|
|
4126
|
-
}
|
|
4127
|
-
}
|
|
4128
|
-
|
|
4129
|
-
// Collect browser tab context (async: requires round-trip to client extension)
|
|
4130
|
-
var tabSources = ctxSources.filter(function(id) {
|
|
4131
|
-
if (!id.startsWith("tab:")) return false;
|
|
4132
|
-
// Only include tabs that currently exist in the browser
|
|
4133
|
-
var tid = parseInt(id.split(":")[1], 10);
|
|
4134
|
-
return !!_browserTabList[tid];
|
|
4135
|
-
});
|
|
4136
|
-
|
|
4137
|
-
function dispatchToSdk(finalText) {
|
|
4138
|
-
if (!session.isProcessing) {
|
|
4139
|
-
session.isProcessing = true;
|
|
4140
|
-
onProcessingChanged();
|
|
4141
|
-
session.sentToolResults = {};
|
|
4142
|
-
sendToSession(session.localId, { type: "status", status: "processing" });
|
|
4143
|
-
if (!session.queryInstance && (!session.worker || session.messageQueue !== "worker")) {
|
|
4144
|
-
// No active query (or worker idle between queries): start a new query
|
|
4145
|
-
session._queryStartTs = Date.now();
|
|
4146
|
-
console.log("[PERF] project.js: startQuery called, localId=" + session.localId + " t=0ms");
|
|
4147
|
-
sdk.startQuery(session, finalText, msg.images, getLinuxUserForSession(session));
|
|
4148
|
-
} else {
|
|
4149
|
-
sdk.pushMessage(session, finalText, msg.images);
|
|
4150
|
-
}
|
|
4151
|
-
} else {
|
|
4152
|
-
sdk.pushMessage(session, finalText, msg.images);
|
|
4153
|
-
}
|
|
4154
|
-
sm.broadcastSessionList();
|
|
4155
|
-
}
|
|
4156
|
-
|
|
4157
|
-
if (tabSources.length > 0) {
|
|
4158
|
-
// Request tab context from all active browser tab sources
|
|
4159
|
-
var tabPromises = tabSources.map(function(srcId) {
|
|
4160
|
-
var tabId = parseInt(srcId.split(":")[1], 10);
|
|
4161
|
-
return requestTabContext(ws, tabId);
|
|
4162
|
-
});
|
|
4163
|
-
Promise.all(tabPromises).then(function(results) {
|
|
4164
|
-
var tabContextParts = [];
|
|
4165
|
-
var screenshotImages = [];
|
|
4166
|
-
|
|
4167
|
-
for (var ti = 0; ti < results.length; ti++) {
|
|
4168
|
-
if (!results[ti]) continue;
|
|
4169
|
-
var tabId2 = parseInt(tabSources[ti].split(":")[1], 10);
|
|
4170
|
-
var tabInfo = _browserTabList[tabId2];
|
|
4171
|
-
var tabLabel = tabInfo ? (tabInfo.title || tabInfo.url || "Tab " + tabId2) : "Tab " + tabId2;
|
|
4172
|
-
var r = results[ti];
|
|
4173
|
-
var parts = [];
|
|
4174
|
-
|
|
4175
|
-
// Console logs
|
|
4176
|
-
if (r.console && r.console.logs) {
|
|
4177
|
-
try {
|
|
4178
|
-
var logs = typeof r.console.logs === "string" ? JSON.parse(r.console.logs) : r.console.logs;
|
|
4179
|
-
if (logs && logs.length > 0) {
|
|
4180
|
-
var logLines = [];
|
|
4181
|
-
var logSlice = logs.slice(-50);
|
|
4182
|
-
for (var li = 0; li < logSlice.length; li++) {
|
|
4183
|
-
var entry = logSlice[li];
|
|
4184
|
-
var ts = entry.ts ? new Date(entry.ts).toTimeString().slice(0, 8) : "";
|
|
4185
|
-
var lvl = (entry.level || "log").toUpperCase();
|
|
4186
|
-
logLines.push("[" + ts + " " + lvl + "] " + (entry.text || ""));
|
|
4187
|
-
}
|
|
4188
|
-
parts.push("Console:\n" + logLines.join("\n"));
|
|
4189
|
-
}
|
|
4190
|
-
} catch (e) {
|
|
4191
|
-
// ignore parse errors
|
|
4192
|
-
}
|
|
4193
|
-
}
|
|
4194
|
-
|
|
4195
|
-
// Network requests
|
|
4196
|
-
if (r.network && r.network.network) {
|
|
4197
|
-
try {
|
|
4198
|
-
var netLog = typeof r.network.network === "string" ? JSON.parse(r.network.network) : r.network.network;
|
|
4199
|
-
if (netLog && netLog.length > 0) {
|
|
4200
|
-
var netLines = [];
|
|
4201
|
-
var netSlice = netLog.slice(-30);
|
|
4202
|
-
for (var ni = 0; ni < netSlice.length; ni++) {
|
|
4203
|
-
var req = netSlice[ni];
|
|
4204
|
-
var line = (req.method || "GET") + " " + (req.url || "") + " " + (req.status || 0) + " " + (req.duration || 0) + "ms";
|
|
4205
|
-
if (req.error) line += " [" + req.error + "]";
|
|
4206
|
-
netLines.push(line);
|
|
4207
|
-
}
|
|
4208
|
-
parts.push("Network (last " + netSlice.length + " requests):\n" + netLines.join("\n"));
|
|
4209
|
-
}
|
|
4210
|
-
} catch (e) {
|
|
4211
|
-
// ignore parse errors
|
|
4212
|
-
}
|
|
4213
|
-
}
|
|
4214
|
-
|
|
4215
|
-
// Page text (from tab_page_text command)
|
|
4216
|
-
if (r.pageText && (r.pageText.text || r.pageText.value)) {
|
|
4217
|
-
var pageContent = r.pageText.text || r.pageText.value;
|
|
4218
|
-
if (pageContent.length > 0) {
|
|
4219
|
-
if (pageContent.length > 32768) {
|
|
4220
|
-
pageContent = pageContent.substring(0, 32768) + "\n... (truncated)";
|
|
4221
|
-
}
|
|
4222
|
-
parts.push("Page text:\n" + pageContent);
|
|
4223
|
-
}
|
|
4224
|
-
}
|
|
4225
|
-
|
|
4226
|
-
// Screenshot — save to disk and add to images for SDK
|
|
4227
|
-
if (r.screenshot && r.screenshot.image) {
|
|
4228
|
-
try {
|
|
4229
|
-
var screenshotData = r.screenshot.image;
|
|
4230
|
-
var screenshotName = saveImageFile("image/png", screenshotData, getLinuxUserForSession(session));
|
|
4231
|
-
if (screenshotName) {
|
|
4232
|
-
var screenshotPath = path.join(imagesDir, screenshotName);
|
|
4233
|
-
// Add to images array for SDK multimodal
|
|
4234
|
-
screenshotImages.push({
|
|
4235
|
-
mediaType: "image/png",
|
|
4236
|
-
data: screenshotData,
|
|
4237
|
-
file: screenshotName,
|
|
4238
|
-
tabTitle: tabLabel,
|
|
4239
|
-
tabUrl: tabInfo ? tabInfo.url : "",
|
|
4240
|
-
tabFavIconUrl: tabInfo ? tabInfo.favIconUrl : ""
|
|
4241
|
-
});
|
|
4242
|
-
parts.push("[Screenshot saved: " + screenshotPath + "]");
|
|
4243
|
-
}
|
|
4244
|
-
} catch (e) {
|
|
4245
|
-
// ignore screenshot save errors
|
|
4246
|
-
}
|
|
4247
|
-
}
|
|
4248
|
-
|
|
4249
|
-
if (r.console && r.console.error) {
|
|
4250
|
-
parts.push("(Console error: " + r.console.error + ")");
|
|
4251
|
-
}
|
|
4252
|
-
if (r.network && r.network.error) {
|
|
4253
|
-
parts.push("(Network error: " + r.network.error + ")");
|
|
4254
|
-
}
|
|
4255
|
-
|
|
4256
|
-
if (parts.length > 0) {
|
|
4257
|
-
tabContextParts.push("[Browser tab: " + tabLabel + "]\n" + parts.join("\n\n"));
|
|
4258
|
-
}
|
|
4259
|
-
}
|
|
688
|
+
// --- Memory (session digests) management (delegated to project-memory.js) ---
|
|
689
|
+
if (msg.type === "memory_list") { _memory.handleMemoryList(ws); return; }
|
|
690
|
+
if (msg.type === "memory_search") { _memory.handleMemorySearch(ws, msg); return; }
|
|
691
|
+
if (msg.type === "memory_delete") { _memory.handleMemoryDelete(ws, msg); return; }
|
|
4260
692
|
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
tabContextParts.join("\n\n---\n\n") + "\n\n" + fullText;
|
|
4264
|
-
}
|
|
693
|
+
// --- Sessions, config, project mgmt (delegated to project-sessions.js) ---
|
|
694
|
+
if (_sessions.handleSessionsMessage(ws, msg)) return;
|
|
4265
695
|
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
if (!msg.images) msg.images = [];
|
|
4269
|
-
for (var si = 0; si < screenshotImages.length; si++) {
|
|
4270
|
-
var ss = screenshotImages[si];
|
|
4271
|
-
// Save context_preview to history so it restores on session load
|
|
4272
|
-
var previewEntry = {
|
|
4273
|
-
type: "context_preview",
|
|
4274
|
-
tab: {
|
|
4275
|
-
title: ss.tabTitle || "",
|
|
4276
|
-
url: ss.tabUrl || "",
|
|
4277
|
-
favIconUrl: ss.tabFavIconUrl || "",
|
|
4278
|
-
screenshotFile: ss.file
|
|
4279
|
-
}
|
|
4280
|
-
};
|
|
4281
|
-
session.history.push(previewEntry);
|
|
4282
|
-
// Send context card to all clients
|
|
4283
|
-
sendToSession(session.localId, {
|
|
4284
|
-
type: "context_preview",
|
|
4285
|
-
tab: {
|
|
4286
|
-
title: ss.tabTitle || "",
|
|
4287
|
-
url: ss.tabUrl || "",
|
|
4288
|
-
favIconUrl: ss.tabFavIconUrl || "",
|
|
4289
|
-
screenshotUrl: "/p/" + slug + "/images/" + ss.file
|
|
4290
|
-
}
|
|
4291
|
-
});
|
|
4292
|
-
// Add to SDK images for multimodal
|
|
4293
|
-
msg.images.push({ mediaType: ss.mediaType, data: ss.data });
|
|
4294
|
-
}
|
|
4295
|
-
sm.saveSessionFile(session);
|
|
4296
|
-
}
|
|
696
|
+
// --- Filesystem, settings, env (delegated to project-filesystem.js) ---
|
|
697
|
+
if (_filesystem.handleFilesystemMessage(ws, msg)) return;
|
|
4297
698
|
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
} else {
|
|
4301
|
-
dispatchToSdk(fullText);
|
|
4302
|
-
}
|
|
699
|
+
// --- Notes, terminals, context, user message (delegated to project-user-message.js) ---
|
|
700
|
+
if (_userMessage.handleUserMessage(ws, msg)) return;
|
|
4303
701
|
}
|
|
4304
702
|
|
|
4305
703
|
// --- Shared helpers ---
|
|
@@ -4415,647 +813,179 @@ function createProjectContext(opts) {
|
|
|
4415
813
|
send({ type: "session_presence", presence: presence });
|
|
4416
814
|
}
|
|
4417
815
|
|
|
4418
|
-
// --- WS disconnection handler ---
|
|
816
|
+
// --- WS disconnection handler (delegated to project-connection.js) ---
|
|
4419
817
|
function handleDisconnection(ws) {
|
|
4420
|
-
|
|
4421
|
-
if (ws._clayActiveSession) {
|
|
4422
|
-
var dcPresKey = ws._clayUser ? ws._clayUser.id : "_default";
|
|
4423
|
-
var dcExisting = userPresence.getPresence(slug, dcPresKey);
|
|
4424
|
-
userPresence.setPresence(slug, dcPresKey, ws._clayActiveSession, dcExisting ? dcExisting.mateDm : null);
|
|
4425
|
-
}
|
|
4426
|
-
tm.detachAll(ws);
|
|
4427
|
-
clients.delete(ws);
|
|
4428
|
-
if (clients.size === 0) {
|
|
4429
|
-
stopFileWatch();
|
|
4430
|
-
stopAllDirWatches();
|
|
4431
|
-
}
|
|
4432
|
-
broadcastClientCount();
|
|
4433
|
-
broadcastPresence();
|
|
818
|
+
_connection.handleDisconnection(ws);
|
|
4434
819
|
}
|
|
4435
820
|
|
|
4436
|
-
// ---
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
return true;
|
|
4484
|
-
}
|
|
4485
|
-
var imgPath = path.join(imagesDir, imgName);
|
|
4486
|
-
try {
|
|
4487
|
-
var imgBuf = fs.readFileSync(imgPath);
|
|
4488
|
-
var ext = path.extname(imgName).toLowerCase();
|
|
4489
|
-
var mime = ext === ".png" ? "image/png" : ext === ".gif" ? "image/gif" : ext === ".webp" ? "image/webp" : "image/jpeg";
|
|
4490
|
-
res.writeHead(200, { "Content-Type": mime, "Cache-Control": "public, max-age=86400" });
|
|
4491
|
-
res.end(imgBuf);
|
|
4492
|
-
} catch (e) {
|
|
4493
|
-
res.writeHead(404);
|
|
4494
|
-
res.end("Not found");
|
|
4495
|
-
}
|
|
4496
|
-
return true;
|
|
4497
|
-
}
|
|
4498
|
-
|
|
4499
|
-
// File upload
|
|
4500
|
-
if (req.method === "POST" && urlPath === "/api/upload") {
|
|
4501
|
-
parseJsonBody(req).then(function (body) {
|
|
4502
|
-
var fileName = body.name;
|
|
4503
|
-
var fileData = body.data; // base64
|
|
4504
|
-
if (!fileName || !fileData) {
|
|
4505
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
4506
|
-
res.end('{"error":"missing name or data"}');
|
|
4507
|
-
return;
|
|
4508
|
-
}
|
|
4509
|
-
// Sanitize filename — strip path separators
|
|
4510
|
-
var safeName = path.basename(fileName).replace(/[\x00-\x1f\/\\:*?"<>|]/g, "_");
|
|
4511
|
-
if (!safeName) safeName = "upload";
|
|
4512
|
-
|
|
4513
|
-
// Check size
|
|
4514
|
-
var estimatedBytes = fileData.length * 0.75;
|
|
4515
|
-
if (estimatedBytes > MAX_UPLOAD_BYTES) {
|
|
4516
|
-
res.writeHead(413, { "Content-Type": "application/json" });
|
|
4517
|
-
res.end('{"error":"file too large (max 50MB)"}');
|
|
4518
|
-
return;
|
|
4519
|
-
}
|
|
4520
|
-
|
|
4521
|
-
// Create tmp dir: os.tmpdir()/clay-{hash}/
|
|
4522
|
-
var cwdHash = crypto.createHash("sha256").update(cwd).digest("hex").substring(0, 12);
|
|
4523
|
-
var tmpDir = path.join(os.tmpdir(), "clay-" + cwdHash);
|
|
4524
|
-
try { fs.mkdirSync(tmpDir, { recursive: true }); } catch (e) {}
|
|
4525
|
-
|
|
4526
|
-
// Add timestamp prefix to avoid collisions
|
|
4527
|
-
var ts = Date.now();
|
|
4528
|
-
var destName = ts + "-" + safeName;
|
|
4529
|
-
var destPath = path.join(tmpDir, destName);
|
|
4530
|
-
|
|
4531
|
-
try {
|
|
4532
|
-
var buf = Buffer.from(fileData, "base64");
|
|
4533
|
-
fs.writeFileSync(destPath, buf);
|
|
4534
|
-
// Make readable by all local users and chown to session owner
|
|
4535
|
-
try { fs.chmodSync(destPath, 0o644); } catch (e2) {}
|
|
4536
|
-
try { fs.chmodSync(tmpDir, 0o755); } catch (e2) {}
|
|
4537
|
-
if (req._clayUser && req._clayUser.linuxUser) {
|
|
4538
|
-
try {
|
|
4539
|
-
var _osUM = require("./os-users");
|
|
4540
|
-
var _uid = _osUM.getLinuxUserUid(req._clayUser.linuxUser);
|
|
4541
|
-
if (_uid != null) {
|
|
4542
|
-
require("child_process").execSync("chown " + _uid + " " + JSON.stringify(destPath));
|
|
4543
|
-
require("child_process").execSync("chown " + _uid + " " + JSON.stringify(tmpDir));
|
|
4544
|
-
}
|
|
4545
|
-
} catch (e2) {}
|
|
4546
|
-
}
|
|
4547
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4548
|
-
res.end(JSON.stringify({ path: destPath, name: safeName }));
|
|
4549
|
-
} catch (e) {
|
|
4550
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
4551
|
-
res.end(JSON.stringify({ error: "failed to save: " + (e.message || e) }));
|
|
4552
|
-
}
|
|
4553
|
-
}).catch(function () {
|
|
4554
|
-
res.writeHead(400);
|
|
4555
|
-
res.end("Bad request");
|
|
4556
|
-
});
|
|
4557
|
-
return true;
|
|
4558
|
-
}
|
|
4559
|
-
|
|
4560
|
-
// Push subscribe
|
|
4561
|
-
if (req.method === "POST" && urlPath === "/api/push-subscribe") {
|
|
4562
|
-
parseJsonBody(req).then(function (body) {
|
|
4563
|
-
var sub = body.subscription || body;
|
|
4564
|
-
if (pushModule) pushModule.addSubscription(sub, body.replaceEndpoint);
|
|
4565
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4566
|
-
res.end('{"ok":true}');
|
|
4567
|
-
}).catch(function () {
|
|
4568
|
-
res.writeHead(400);
|
|
4569
|
-
res.end("Bad request");
|
|
4570
|
-
});
|
|
4571
|
-
return true;
|
|
4572
|
-
}
|
|
4573
|
-
|
|
4574
|
-
// Permission response from push notification
|
|
4575
|
-
if (req.method === "POST" && urlPath === "/api/permission-response") {
|
|
4576
|
-
parseJsonBody(req).then(function (data) {
|
|
4577
|
-
var requestId = data.requestId;
|
|
4578
|
-
var decision = data.decision;
|
|
4579
|
-
if (!requestId || !decision) {
|
|
4580
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
4581
|
-
res.end('{"error":"missing requestId or decision"}');
|
|
4582
|
-
return;
|
|
4583
|
-
}
|
|
4584
|
-
var found = false;
|
|
4585
|
-
sm.sessions.forEach(function (session) {
|
|
4586
|
-
var pending = session.pendingPermissions[requestId];
|
|
4587
|
-
if (!pending) return;
|
|
4588
|
-
found = true;
|
|
4589
|
-
delete session.pendingPermissions[requestId];
|
|
4590
|
-
if (decision === "allow") {
|
|
4591
|
-
pending.resolve({ behavior: "allow", updatedInput: pending.toolInput });
|
|
4592
|
-
} else {
|
|
4593
|
-
pending.resolve({ behavior: "deny", message: "Denied via push notification" });
|
|
4594
|
-
}
|
|
4595
|
-
sm.sendAndRecord(session, {
|
|
4596
|
-
type: "permission_resolved",
|
|
4597
|
-
requestId: requestId,
|
|
4598
|
-
decision: decision,
|
|
4599
|
-
});
|
|
4600
|
-
});
|
|
4601
|
-
if (found) {
|
|
4602
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4603
|
-
res.end('{"ok":true}');
|
|
4604
|
-
} else {
|
|
4605
|
-
res.writeHead(404, { "Content-Type": "application/json" });
|
|
4606
|
-
res.end('{"error":"permission request not found"}');
|
|
4607
|
-
}
|
|
4608
|
-
}).catch(function () {
|
|
4609
|
-
res.writeHead(400);
|
|
4610
|
-
res.end("Bad request");
|
|
4611
|
-
});
|
|
4612
|
-
return true;
|
|
4613
|
-
}
|
|
4614
|
-
|
|
4615
|
-
// VAPID public key
|
|
4616
|
-
if (req.method === "GET" && urlPath === "/api/vapid-public-key") {
|
|
4617
|
-
if (pushModule) {
|
|
4618
|
-
res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-cache, no-store" });
|
|
4619
|
-
res.end(JSON.stringify({ publicKey: pushModule.publicKey }));
|
|
4620
|
-
} else {
|
|
4621
|
-
res.writeHead(404, { "Content-Type": "application/json" });
|
|
4622
|
-
res.end('{"error":"push not available"}');
|
|
4623
|
-
}
|
|
4624
|
-
return true;
|
|
4625
|
-
}
|
|
4626
|
-
|
|
4627
|
-
// File browser: serve project images
|
|
4628
|
-
if (req.method === "GET" && urlPath.startsWith("/api/file?")) {
|
|
4629
|
-
var qIdx = urlPath.indexOf("?");
|
|
4630
|
-
var params = new URLSearchParams(urlPath.substring(qIdx));
|
|
4631
|
-
var reqFilePath = params.get("path");
|
|
4632
|
-
if (!reqFilePath) { res.writeHead(400); res.end("Missing path"); return true; }
|
|
4633
|
-
var absFile = safePath(cwd, reqFilePath);
|
|
4634
|
-
if (!absFile && getOsUserInfoForReq(req)) {
|
|
4635
|
-
absFile = safeAbsPath(reqFilePath);
|
|
4636
|
-
}
|
|
4637
|
-
if (!absFile) { res.writeHead(403); res.end("Access denied"); return true; }
|
|
4638
|
-
var fileExt = path.extname(absFile).toLowerCase();
|
|
4639
|
-
if (!IMAGE_EXTS.has(fileExt)) { res.writeHead(403); res.end("Only image files"); return true; }
|
|
4640
|
-
try {
|
|
4641
|
-
var fileServeUserInfo = getOsUserInfoForReq(req);
|
|
4642
|
-
var fileContent;
|
|
4643
|
-
if (fileServeUserInfo) {
|
|
4644
|
-
var binResult = fsAsUser("read_binary", { file: absFile }, fileServeUserInfo);
|
|
4645
|
-
fileContent = binResult.buffer;
|
|
4646
|
-
} else {
|
|
4647
|
-
fileContent = fs.readFileSync(absFile);
|
|
4648
|
-
}
|
|
4649
|
-
var fileMime = MIME_TYPES[fileExt] || "application/octet-stream";
|
|
4650
|
-
res.writeHead(200, { "Content-Type": fileMime, "Cache-Control": "no-cache" });
|
|
4651
|
-
res.end(fileContent);
|
|
4652
|
-
} catch (e) {
|
|
4653
|
-
res.writeHead(404); res.end("Not found");
|
|
4654
|
-
}
|
|
4655
|
-
return true;
|
|
4656
|
-
}
|
|
4657
|
-
|
|
4658
|
-
// Skills permission gate
|
|
4659
|
-
if (urlPath === "/api/install-skill" || urlPath === "/api/uninstall-skill" || urlPath === "/api/installed-skills") {
|
|
4660
|
-
if (req._clayUser) {
|
|
4661
|
-
var skPerms = usersModule.getEffectivePermissions(req._clayUser, osUsers);
|
|
4662
|
-
if (!skPerms.skills) {
|
|
4663
|
-
res.writeHead(403, { "Content-Type": "application/json" });
|
|
4664
|
-
res.end('{"error":"Skills access is not permitted"}');
|
|
4665
|
-
return true;
|
|
4666
|
-
}
|
|
4667
|
-
}
|
|
4668
|
-
}
|
|
4669
|
-
|
|
4670
|
-
// Install a skill (background spawn)
|
|
4671
|
-
if (req.method === "POST" && urlPath === "/api/install-skill") {
|
|
4672
|
-
parseJsonBody(req).then(function (body) {
|
|
4673
|
-
var url = body.url;
|
|
4674
|
-
var skill = body.skill;
|
|
4675
|
-
var scope = body.scope; // "global" or "project"
|
|
4676
|
-
if (!url || !skill || !scope) {
|
|
4677
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
4678
|
-
res.end('{"error":"missing url, skill, or scope"}');
|
|
4679
|
-
return;
|
|
4680
|
-
}
|
|
4681
|
-
// Validate skill name: alphanumeric, hyphens, underscores only
|
|
4682
|
-
if (!/^[a-zA-Z0-9_-]+$/.test(skill)) {
|
|
4683
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
4684
|
-
res.end('{"error":"invalid skill name"}');
|
|
4685
|
-
return;
|
|
4686
|
-
}
|
|
4687
|
-
// Validate URL: must be https://
|
|
4688
|
-
if (!/^https:\/\//i.test(url)) {
|
|
4689
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
4690
|
-
res.end('{"error":"only https:// URLs are allowed"}');
|
|
4691
|
-
return;
|
|
4692
|
-
}
|
|
4693
|
-
var skillUserInfo = getOsUserInfoForReq(req);
|
|
4694
|
-
var spawnCwd = scope === "global" ? (skillUserInfo ? skillUserInfo.home : require("./config").REAL_HOME) : cwd;
|
|
4695
|
-
var scopeFlag = scope === "global" ? "--global" : "--project";
|
|
4696
|
-
var skillSpawnOpts = {
|
|
4697
|
-
cwd: spawnCwd,
|
|
4698
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
4699
|
-
detached: false,
|
|
4700
|
-
};
|
|
4701
|
-
if (skillUserInfo) {
|
|
4702
|
-
skillSpawnOpts.uid = skillUserInfo.uid;
|
|
4703
|
-
skillSpawnOpts.gid = skillUserInfo.gid;
|
|
4704
|
-
skillSpawnOpts.env = Object.assign({}, process.env, {
|
|
4705
|
-
HOME: skillUserInfo.home,
|
|
4706
|
-
npm_config_cache: require("path").join(skillUserInfo.home, ".npm"),
|
|
4707
|
-
});
|
|
4708
|
-
}
|
|
4709
|
-
console.log("[skill-install] spawning: npx skills add " + url + " --skill " + skill + " --yes " + scopeFlag + " (cwd: " + spawnCwd + ")");
|
|
4710
|
-
var child = spawn("npx", ["skills", "add", url, "--skill", skill, "--yes", scopeFlag], skillSpawnOpts);
|
|
4711
|
-
var stdoutBuf = "";
|
|
4712
|
-
var stderrBuf = "";
|
|
4713
|
-
child.stdout.on("data", function (chunk) {
|
|
4714
|
-
stdoutBuf += chunk.toString();
|
|
4715
|
-
console.log("[skill-install] " + skill + " stdout chunk: " + chunk.toString().trim().slice(0, 500));
|
|
4716
|
-
});
|
|
4717
|
-
child.stderr.on("data", function (chunk) {
|
|
4718
|
-
stderrBuf += chunk.toString();
|
|
4719
|
-
console.log("[skill-install] " + skill + " stderr chunk: " + chunk.toString().trim().slice(0, 500));
|
|
4720
|
-
});
|
|
4721
|
-
// Timeout after 60 seconds
|
|
4722
|
-
var installTimeout = setTimeout(function () {
|
|
4723
|
-
console.error("[skill-install] " + skill + " timed out after 60s, killing process");
|
|
4724
|
-
try { child.kill("SIGTERM"); } catch (e) {}
|
|
4725
|
-
try {
|
|
4726
|
-
send({ type: "skill_installed", skill: skill, scope: scope, success: false, error: "Installation timed out after 60 seconds" });
|
|
4727
|
-
} catch (e) {}
|
|
4728
|
-
}, 60000);
|
|
4729
|
-
child.on("close", function (code) {
|
|
4730
|
-
clearTimeout(installTimeout);
|
|
4731
|
-
console.log("[skill-install] " + skill + " exited with code " + code + " (stdout=" + stdoutBuf.length + "b, stderr=" + stderrBuf.length + "b)");
|
|
4732
|
-
if (stdoutBuf) console.log("[skill-install] stdout: " + stdoutBuf.slice(0, 2000));
|
|
4733
|
-
if (stderrBuf) console.log("[skill-install] stderr: " + stderrBuf.slice(0, 2000));
|
|
4734
|
-
try {
|
|
4735
|
-
var success = code === 0;
|
|
4736
|
-
send({
|
|
4737
|
-
type: "skill_installed",
|
|
4738
|
-
skill: skill,
|
|
4739
|
-
scope: scope,
|
|
4740
|
-
success: success,
|
|
4741
|
-
error: success ? null : "Process exited with code " + code,
|
|
4742
|
-
});
|
|
4743
|
-
} catch (e) {
|
|
4744
|
-
console.error("[project] skill_installed send failed:", e.message || e);
|
|
4745
|
-
}
|
|
4746
|
-
});
|
|
4747
|
-
child.on("error", function (err) {
|
|
4748
|
-
clearTimeout(installTimeout);
|
|
4749
|
-
console.error("[skill-install] " + skill + " spawn error:", err.message || err);
|
|
4750
|
-
try {
|
|
4751
|
-
send({
|
|
4752
|
-
type: "skill_installed",
|
|
4753
|
-
skill: skill,
|
|
4754
|
-
scope: scope,
|
|
4755
|
-
success: false,
|
|
4756
|
-
error: err.message,
|
|
4757
|
-
});
|
|
4758
|
-
} catch (e) {
|
|
4759
|
-
console.error("[skill-install] " + skill + " send failed:", e.message || e);
|
|
4760
|
-
}
|
|
4761
|
-
});
|
|
4762
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4763
|
-
res.end('{"ok":true}');
|
|
4764
|
-
}).catch(function () {
|
|
4765
|
-
res.writeHead(400);
|
|
4766
|
-
res.end("Bad request");
|
|
4767
|
-
});
|
|
4768
|
-
return true;
|
|
4769
|
-
}
|
|
4770
|
-
|
|
4771
|
-
// Uninstall a skill (remove directory)
|
|
4772
|
-
if (req.method === "POST" && urlPath === "/api/uninstall-skill") {
|
|
4773
|
-
parseJsonBody(req).then(function (body) {
|
|
4774
|
-
var skill = body.skill;
|
|
4775
|
-
var scope = body.scope; // "global" or "project"
|
|
4776
|
-
if (!skill || !scope) {
|
|
4777
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
4778
|
-
res.end('{"error":"missing skill or scope"}');
|
|
4779
|
-
return;
|
|
4780
|
-
}
|
|
4781
|
-
// Validate skill name
|
|
4782
|
-
if (!/^[a-zA-Z0-9_-]+$/.test(skill)) {
|
|
4783
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
4784
|
-
res.end('{"error":"invalid skill name"}');
|
|
4785
|
-
return;
|
|
4786
|
-
}
|
|
4787
|
-
var uninstallUserInfo = getOsUserInfoForReq(req);
|
|
4788
|
-
var baseDir = scope === "global" ? (uninstallUserInfo ? uninstallUserInfo.home : require("./config").REAL_HOME) : cwd;
|
|
4789
|
-
var skillDir = path.join(baseDir, ".claude", "skills", skill);
|
|
4790
|
-
// Safety: ensure skillDir is inside the expected .claude/skills directory
|
|
4791
|
-
var expectedParent = path.join(baseDir, ".claude", "skills");
|
|
4792
|
-
var resolved = path.resolve(skillDir);
|
|
4793
|
-
if (!resolved.startsWith(expectedParent + path.sep)) {
|
|
4794
|
-
res.writeHead(403, { "Content-Type": "application/json" });
|
|
4795
|
-
res.end('{"error":"invalid skill path"}');
|
|
4796
|
-
return;
|
|
4797
|
-
}
|
|
4798
|
-
try {
|
|
4799
|
-
if (uninstallUserInfo) {
|
|
4800
|
-
// Run rm as target user to respect permissions
|
|
4801
|
-
var rmScript = "var fs = require('fs'); fs.rmSync(" + JSON.stringify(resolved) + ", { recursive: true, force: true });";
|
|
4802
|
-
execFileSync(process.execPath, ["-e", rmScript], {
|
|
4803
|
-
uid: uninstallUserInfo.uid,
|
|
4804
|
-
gid: uninstallUserInfo.gid,
|
|
4805
|
-
timeout: 10000,
|
|
4806
|
-
});
|
|
4807
|
-
} else {
|
|
4808
|
-
fs.rmSync(resolved, { recursive: true, force: true });
|
|
4809
|
-
}
|
|
4810
|
-
send({
|
|
4811
|
-
type: "skill_uninstalled",
|
|
4812
|
-
skill: skill,
|
|
4813
|
-
scope: scope,
|
|
4814
|
-
success: true,
|
|
4815
|
-
});
|
|
4816
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4817
|
-
res.end('{"ok":true}');
|
|
4818
|
-
} catch (err) {
|
|
4819
|
-
send({
|
|
4820
|
-
type: "skill_uninstalled",
|
|
4821
|
-
skill: skill,
|
|
4822
|
-
scope: scope,
|
|
4823
|
-
success: false,
|
|
4824
|
-
error: err.message,
|
|
4825
|
-
});
|
|
4826
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
4827
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
4828
|
-
}
|
|
4829
|
-
}).catch(function () {
|
|
4830
|
-
res.writeHead(400);
|
|
4831
|
-
res.end("Bad request");
|
|
4832
|
-
});
|
|
4833
|
-
return true;
|
|
4834
|
-
}
|
|
4835
|
-
|
|
4836
|
-
// Installed skills (global + project)
|
|
4837
|
-
if (req.method === "GET" && urlPath === "/api/installed-skills") {
|
|
4838
|
-
var installed = {};
|
|
4839
|
-
var globalDir = path.join(require("./config").REAL_HOME, ".claude", "skills");
|
|
4840
|
-
var projectDir = path.join(cwd, ".claude", "skills");
|
|
4841
|
-
var scanDirs = [
|
|
4842
|
-
{ dir: globalDir, scope: "global" },
|
|
4843
|
-
{ dir: projectDir, scope: "project" },
|
|
4844
|
-
];
|
|
4845
|
-
for (var sd = 0; sd < scanDirs.length; sd++) {
|
|
4846
|
-
var entries;
|
|
4847
|
-
try { entries = fs.readdirSync(scanDirs[sd].dir, { withFileTypes: true }); } catch (e) { continue; }
|
|
4848
|
-
for (var si = 0; si < entries.length; si++) {
|
|
4849
|
-
var ent = entries[si];
|
|
4850
|
-
if (!ent.isDirectory() && !ent.isSymbolicLink()) continue;
|
|
4851
|
-
var mdPath = path.join(scanDirs[sd].dir, ent.name, "SKILL.md");
|
|
4852
|
-
try {
|
|
4853
|
-
var mdContent = fs.readFileSync(mdPath, "utf8");
|
|
4854
|
-
var desc = "";
|
|
4855
|
-
// Parse YAML frontmatter for description
|
|
4856
|
-
var version = "";
|
|
4857
|
-
if (mdContent.startsWith("---")) {
|
|
4858
|
-
var endIdx = mdContent.indexOf("---", 3);
|
|
4859
|
-
if (endIdx !== -1) {
|
|
4860
|
-
var frontmatter = mdContent.substring(3, endIdx);
|
|
4861
|
-
var descMatch = frontmatter.match(/^description:\s*(.+)/m);
|
|
4862
|
-
if (descMatch) desc = descMatch[1].trim();
|
|
4863
|
-
var verMatch = frontmatter.match(/version:\s*"?([^"\n]+)"?/m);
|
|
4864
|
-
if (verMatch) version = verMatch[1].trim();
|
|
4865
|
-
}
|
|
4866
|
-
}
|
|
4867
|
-
if (!installed[ent.name]) {
|
|
4868
|
-
installed[ent.name] = { scope: scanDirs[sd].scope, description: desc, version: version, path: path.join(scanDirs[sd].dir, ent.name) };
|
|
4869
|
-
} else {
|
|
4870
|
-
// project-level adds to existing global entry
|
|
4871
|
-
installed[ent.name].scope = "both";
|
|
4872
|
-
if (desc && !installed[ent.name].description) installed[ent.name].description = desc;
|
|
4873
|
-
if (version && !installed[ent.name].version) installed[ent.name].version = version;
|
|
4874
|
-
}
|
|
4875
|
-
} catch (e) {}
|
|
4876
|
-
}
|
|
4877
|
-
}
|
|
4878
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4879
|
-
res.end(JSON.stringify({ installed: installed }));
|
|
4880
|
-
return true;
|
|
4881
|
-
}
|
|
4882
|
-
|
|
4883
|
-
// Check skill updates (compare installed vs remote versions)
|
|
4884
|
-
if (req.method === "POST" && urlPath === "/api/check-skill-updates") {
|
|
4885
|
-
parseJsonBody(req).then(function (body) {
|
|
4886
|
-
var skills = body.skills; // [{ name, url, scope }]
|
|
4887
|
-
if (!Array.isArray(skills) || skills.length === 0) {
|
|
4888
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
4889
|
-
res.end('{"error":"missing skills array"}');
|
|
4890
|
-
return;
|
|
4891
|
-
}
|
|
4892
|
-
// Read installed versions (use requesting user's home in multi-user setups)
|
|
4893
|
-
var skillUserHome = (function () {
|
|
4894
|
-
var sui = getOsUserInfoForReq(req);
|
|
4895
|
-
return sui ? sui.home : require("./config").REAL_HOME;
|
|
4896
|
-
})();
|
|
4897
|
-
var globalSkillsDir = path.join(skillUserHome, ".claude", "skills");
|
|
4898
|
-
var projectSkillsDir = path.join(cwd, ".claude", "skills");
|
|
4899
|
-
var results = [];
|
|
4900
|
-
var pending = skills.length;
|
|
4901
|
-
|
|
4902
|
-
function parseVersionFromSkillMd(content) {
|
|
4903
|
-
if (!content || !content.startsWith("---")) return "";
|
|
4904
|
-
var endIdx = content.indexOf("---", 3);
|
|
4905
|
-
if (endIdx === -1) return "";
|
|
4906
|
-
var fm = content.substring(3, endIdx);
|
|
4907
|
-
var m = fm.match(/version:\s*"?([^"\n]+)"?/m);
|
|
4908
|
-
return m ? m[1].trim() : "";
|
|
4909
|
-
}
|
|
4910
|
-
|
|
4911
|
-
function getInstalledVersion(name) {
|
|
4912
|
-
var dirs = [path.join(globalSkillsDir, name, "SKILL.md"), path.join(projectSkillsDir, name, "SKILL.md")];
|
|
4913
|
-
for (var d = 0; d < dirs.length; d++) {
|
|
4914
|
-
try {
|
|
4915
|
-
var c = fs.readFileSync(dirs[d], "utf8");
|
|
4916
|
-
var v = parseVersionFromSkillMd(c);
|
|
4917
|
-
if (v) return v;
|
|
4918
|
-
} catch (e) {}
|
|
4919
|
-
}
|
|
4920
|
-
return "";
|
|
4921
|
-
}
|
|
4922
|
-
|
|
4923
|
-
function compareVersions(a, b) {
|
|
4924
|
-
// returns -1 if a < b, 0 if equal, 1 if a > b
|
|
4925
|
-
if (!a && !b) return 0;
|
|
4926
|
-
if (!a) return -1;
|
|
4927
|
-
if (!b) return 1;
|
|
4928
|
-
var pa = a.split(".").map(Number);
|
|
4929
|
-
var pb = b.split(".").map(Number);
|
|
4930
|
-
for (var i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
4931
|
-
var va = pa[i] || 0;
|
|
4932
|
-
var vb = pb[i] || 0;
|
|
4933
|
-
if (va < vb) return -1;
|
|
4934
|
-
if (va > vb) return 1;
|
|
4935
|
-
}
|
|
4936
|
-
return 0;
|
|
4937
|
-
}
|
|
4938
|
-
|
|
4939
|
-
function finishOne() {
|
|
4940
|
-
pending--;
|
|
4941
|
-
if (pending === 0) {
|
|
4942
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4943
|
-
res.end(JSON.stringify({ results: results }));
|
|
4944
|
-
}
|
|
4945
|
-
}
|
|
4946
|
-
|
|
4947
|
-
for (var si = 0; si < skills.length; si++) {
|
|
4948
|
-
(function (skill) {
|
|
4949
|
-
var installedVer = getInstalledVersion(skill.name);
|
|
4950
|
-
var installed = !!installedVer;
|
|
4951
|
-
console.log("[skill-check] " + skill.name + " installed=" + installed + " localVersion=" + (installedVer || "none"));
|
|
4952
|
-
// Convert GitHub repo URL to raw SKILL.md URL
|
|
4953
|
-
var rawUrl = "";
|
|
4954
|
-
var ghMatch = skill.url.match(/github\.com\/([^/]+)\/([^/]+)/);
|
|
4955
|
-
if (ghMatch) {
|
|
4956
|
-
rawUrl = "https://raw.githubusercontent.com/" + ghMatch[1] + "/" + ghMatch[2] + "/main/SKILL.md";
|
|
4957
|
-
}
|
|
4958
|
-
if (!rawUrl) {
|
|
4959
|
-
console.log("[skill-check] " + skill.name + " no valid GitHub URL, skipping remote check");
|
|
4960
|
-
results.push({ name: skill.name, installed: installed, installedVersion: installedVer, remoteVersion: "", status: installed ? "ok" : "missing" });
|
|
4961
|
-
finishOne();
|
|
4962
|
-
return;
|
|
4963
|
-
}
|
|
4964
|
-
console.log("[skill-check] " + skill.name + " fetching remote: " + rawUrl);
|
|
4965
|
-
// Fetch remote SKILL.md
|
|
4966
|
-
var https = require("https");
|
|
4967
|
-
https.get(rawUrl, function (resp) {
|
|
4968
|
-
console.log("[skill-check] " + skill.name + " remote response status=" + resp.statusCode);
|
|
4969
|
-
var data = "";
|
|
4970
|
-
resp.on("data", function (chunk) { data += chunk; });
|
|
4971
|
-
resp.on("end", function () {
|
|
4972
|
-
try {
|
|
4973
|
-
var remoteVer = parseVersionFromSkillMd(data);
|
|
4974
|
-
var status = "ok";
|
|
4975
|
-
if (!installed) {
|
|
4976
|
-
status = "missing";
|
|
4977
|
-
} else if (remoteVer && compareVersions(installedVer, remoteVer) < 0) {
|
|
4978
|
-
status = "outdated";
|
|
4979
|
-
}
|
|
4980
|
-
console.log("[skill-check] " + skill.name + " remoteVersion=" + remoteVer + " status=" + status);
|
|
4981
|
-
results.push({ name: skill.name, installed: installed, installedVersion: installedVer, remoteVersion: remoteVer, status: status });
|
|
4982
|
-
finishOne();
|
|
4983
|
-
} catch (e) {
|
|
4984
|
-
console.error("[skill-check] " + skill.name + " version parse failed:", e.message || e);
|
|
4985
|
-
results.push({ name: skill.name, installed: installed, installedVersion: installedVer, remoteVersion: "", status: installed ? "ok" : "error" });
|
|
4986
|
-
finishOne();
|
|
4987
|
-
}
|
|
4988
|
-
});
|
|
4989
|
-
}).on("error", function (err) {
|
|
4990
|
-
console.error("[skill-check] " + skill.name + " fetch error:", err.message || err);
|
|
4991
|
-
results.push({ name: skill.name, installed: installed, installedVersion: installedVer, remoteVersion: "", status: installed ? "ok" : "missing" });
|
|
4992
|
-
finishOne();
|
|
4993
|
-
});
|
|
4994
|
-
})(skills[si]);
|
|
4995
|
-
}
|
|
4996
|
-
}).catch(function () {
|
|
4997
|
-
res.writeHead(400);
|
|
4998
|
-
res.end("Bad request");
|
|
4999
|
-
});
|
|
5000
|
-
return true;
|
|
5001
|
-
}
|
|
821
|
+
// --- Sessions/config/project handler (delegated to project-sessions.js) ---
|
|
822
|
+
var _sessions = attachSessions({
|
|
823
|
+
cwd: cwd,
|
|
824
|
+
slug: slug,
|
|
825
|
+
isMate: isMate,
|
|
826
|
+
osUsers: osUsers,
|
|
827
|
+
debug: debug,
|
|
828
|
+
dangerouslySkipPermissions: dangerouslySkipPermissions,
|
|
829
|
+
currentVersion: currentVersion,
|
|
830
|
+
sm: sm,
|
|
831
|
+
sdk: sdk,
|
|
832
|
+
tm: tm,
|
|
833
|
+
clients: clients,
|
|
834
|
+
send: send,
|
|
835
|
+
sendTo: sendTo,
|
|
836
|
+
sendToAdmins: sendToAdmins,
|
|
837
|
+
sendToSession: sendToSession,
|
|
838
|
+
sendToSessionOthers: sendToSessionOthers,
|
|
839
|
+
opts: opts,
|
|
840
|
+
usersModule: usersModule,
|
|
841
|
+
userPresence: userPresence,
|
|
842
|
+
matesModule: matesModule,
|
|
843
|
+
pushModule: pushModule,
|
|
844
|
+
getSessionForWs: getSessionForWs,
|
|
845
|
+
getLinuxUserForSession: getLinuxUserForSession,
|
|
846
|
+
getOsUserInfoForWs: getOsUserInfoForWs,
|
|
847
|
+
hydrateImageRefs: hydrateImageRefs,
|
|
848
|
+
onProcessingChanged: onProcessingChanged,
|
|
849
|
+
broadcastPresence: broadcastPresence,
|
|
850
|
+
getSDK: getSDK,
|
|
851
|
+
getProjectList: getProjectList,
|
|
852
|
+
getProjectCount: getProjectCount,
|
|
853
|
+
getScheduleCount: getScheduleCount,
|
|
854
|
+
moveScheduleToProject: moveScheduleToProject,
|
|
855
|
+
moveAllSchedulesToProject: moveAllSchedulesToProject,
|
|
856
|
+
getHubSchedules: getHubSchedules,
|
|
857
|
+
fetchVersion: fetchVersion,
|
|
858
|
+
isNewer: isNewer,
|
|
859
|
+
scheduleMessage: scheduleMessage,
|
|
860
|
+
cancelScheduledMessage: cancelScheduledMessage,
|
|
861
|
+
getProjectOwnerId: function () { return projectOwnerId; },
|
|
862
|
+
setProjectOwnerId: function (id) { projectOwnerId = id; },
|
|
863
|
+
getUpdateChannel: function () { return updateChannel; },
|
|
864
|
+
setUpdateChannel: function (ch) { updateChannel = ch; },
|
|
865
|
+
getLatestVersion: function () { return latestVersion; },
|
|
866
|
+
setLatestVersion: function (v) { latestVersion = v; },
|
|
867
|
+
});
|
|
5002
868
|
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
869
|
+
// --- User message handler (delegated to project-user-message.js) ---
|
|
870
|
+
var _userMessage = attachUserMessage({
|
|
871
|
+
cwd: cwd,
|
|
872
|
+
slug: slug,
|
|
873
|
+
isMate: isMate,
|
|
874
|
+
osUsers: osUsers,
|
|
875
|
+
sm: sm,
|
|
876
|
+
sdk: sdk,
|
|
877
|
+
nm: nm,
|
|
878
|
+
tm: tm,
|
|
879
|
+
clients: clients,
|
|
880
|
+
send: send,
|
|
881
|
+
sendTo: sendTo,
|
|
882
|
+
sendToSession: sendToSession,
|
|
883
|
+
sendToSessionOthers: sendToSessionOthers,
|
|
884
|
+
opts: opts,
|
|
885
|
+
usersModule: usersModule,
|
|
886
|
+
matesModule: matesModule,
|
|
887
|
+
getSessionForWs: getSessionForWs,
|
|
888
|
+
getLinuxUserForSession: getLinuxUserForSession,
|
|
889
|
+
getOsUserInfoForWs: getOsUserInfoForWs,
|
|
890
|
+
hydrateImageRefs: hydrateImageRefs,
|
|
891
|
+
saveImageFile: saveImageFile,
|
|
892
|
+
imagesDir: imagesDir,
|
|
893
|
+
onProcessingChanged: onProcessingChanged,
|
|
894
|
+
_loop: _loop,
|
|
895
|
+
browserState: { _browserTabList: _browserTabList, _extensionWs: _extensionWs, pendingExtensionRequests: pendingExtensionRequests },
|
|
896
|
+
sendExtensionCommandAny: sendExtensionCommandAny,
|
|
897
|
+
scheduleMessage: scheduleMessage,
|
|
898
|
+
cancelScheduledMessage: cancelScheduledMessage,
|
|
899
|
+
loadContextSources: loadContextSources,
|
|
900
|
+
saveContextSources: saveContextSources,
|
|
901
|
+
digestDmTurn: digestDmTurn,
|
|
902
|
+
gateMemory: gateMemory,
|
|
903
|
+
escapeRegex: escapeRegex,
|
|
904
|
+
getSDK: getSDK,
|
|
905
|
+
getHubSchedules: getHubSchedules,
|
|
906
|
+
getProjectOwnerId: function () { return projectOwnerId; },
|
|
907
|
+
});
|
|
5019
908
|
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
909
|
+
// --- Filesystem handler (delegated to project-filesystem.js) ---
|
|
910
|
+
var _filesystem = attachFilesystem({
|
|
911
|
+
cwd: cwd,
|
|
912
|
+
slug: slug,
|
|
913
|
+
osUsers: osUsers,
|
|
914
|
+
sm: sm,
|
|
915
|
+
send: send,
|
|
916
|
+
sendTo: sendTo,
|
|
917
|
+
safePath: safePath,
|
|
918
|
+
safeAbsPath: safeAbsPath,
|
|
919
|
+
getOsUserInfoForWs: getOsUserInfoForWs,
|
|
920
|
+
startFileWatch: startFileWatch,
|
|
921
|
+
stopFileWatch: stopFileWatch,
|
|
922
|
+
startDirWatch: startDirWatch,
|
|
923
|
+
usersModule: usersModule,
|
|
924
|
+
fsAsUser: fsAsUser,
|
|
925
|
+
validateEnvString: validateEnvString,
|
|
926
|
+
opts: opts,
|
|
927
|
+
IGNORED_DIRS: IGNORED_DIRS,
|
|
928
|
+
BINARY_EXTS: BINARY_EXTS,
|
|
929
|
+
IMAGE_EXTS: IMAGE_EXTS,
|
|
930
|
+
FS_MAX_SIZE: FS_MAX_SIZE,
|
|
931
|
+
});
|
|
5042
932
|
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
933
|
+
// --- HTTP handler (delegated to project-http.js) ---
|
|
934
|
+
var _http = attachHTTP({
|
|
935
|
+
cwd: cwd,
|
|
936
|
+
slug: slug,
|
|
937
|
+
project: title || project,
|
|
938
|
+
sm: sm,
|
|
939
|
+
send: send,
|
|
940
|
+
imagesDir: imagesDir,
|
|
941
|
+
osUsers: osUsers,
|
|
942
|
+
pushModule: pushModule,
|
|
943
|
+
safePath: safePath,
|
|
944
|
+
safeAbsPath: safeAbsPath,
|
|
945
|
+
getOsUserInfoForReq: getOsUserInfoForReq,
|
|
946
|
+
sendExtensionCommandAny: sendExtensionCommandAny,
|
|
947
|
+
_extToken: _extToken,
|
|
948
|
+
_browserTabList: _browserTabList,
|
|
949
|
+
});
|
|
950
|
+
var handleHTTP = _http.handleHTTP;
|
|
5052
951
|
|
|
5053
|
-
|
|
5054
|
-
|
|
952
|
+
// --- Connection handler (delegated to project-connection.js) ---
|
|
953
|
+
var _connection = attachConnection({
|
|
954
|
+
cwd: cwd,
|
|
955
|
+
slug: slug,
|
|
956
|
+
isMate: isMate,
|
|
957
|
+
osUsers: osUsers,
|
|
958
|
+
debug: debug,
|
|
959
|
+
dangerouslySkipPermissions: dangerouslySkipPermissions,
|
|
960
|
+
currentVersion: currentVersion,
|
|
961
|
+
lanHost: lanHost,
|
|
962
|
+
sm: sm,
|
|
963
|
+
tm: tm,
|
|
964
|
+
nm: nm,
|
|
965
|
+
clients: clients,
|
|
966
|
+
send: send,
|
|
967
|
+
sendTo: sendTo,
|
|
968
|
+
opts: opts,
|
|
969
|
+
_loop: _loop,
|
|
970
|
+
hydrateImageRefs: hydrateImageRefs,
|
|
971
|
+
broadcastClientCount: broadcastClientCount,
|
|
972
|
+
broadcastPresence: broadcastPresence,
|
|
973
|
+
getProjectList: getProjectList,
|
|
974
|
+
getHubSchedules: getHubSchedules,
|
|
975
|
+
loadContextSources: loadContextSources,
|
|
976
|
+
restoreDebateState: restoreDebateState,
|
|
977
|
+
stopFileWatch: stopFileWatch,
|
|
978
|
+
stopAllDirWatches: stopAllDirWatches,
|
|
979
|
+
getProjectOwnerId: function () { return projectOwnerId; },
|
|
980
|
+
setProjectOwnerId: function (id) { projectOwnerId = id; },
|
|
981
|
+
getLatestVersion: function () { return latestVersion; },
|
|
982
|
+
getTitle: function () { return title; },
|
|
983
|
+
getProject: function () { return project; },
|
|
984
|
+
});
|
|
5055
985
|
|
|
5056
986
|
// --- Destroy ---
|
|
5057
987
|
function destroy() {
|
|
5058
|
-
|
|
988
|
+
_loop.stopTimer();
|
|
5059
989
|
stopFileWatch();
|
|
5060
990
|
stopAllDirWatches();
|
|
5061
991
|
// Abort all active sessions and clean up mention sessions
|
|
@@ -5232,9 +1162,9 @@ function createProjectContext(opts) {
|
|
|
5232
1162
|
handleHTTP: handleHTTP,
|
|
5233
1163
|
getStatus: getStatus,
|
|
5234
1164
|
getSessionManager: function () { return sm; },
|
|
5235
|
-
getSchedules:
|
|
5236
|
-
importSchedule:
|
|
5237
|
-
removeSchedule:
|
|
1165
|
+
getSchedules: _loop.getSchedules,
|
|
1166
|
+
importSchedule: _loop.importSchedule,
|
|
1167
|
+
removeSchedule: _loop.removeSchedule,
|
|
5238
1168
|
setTitle: setTitle,
|
|
5239
1169
|
setIcon: setIcon,
|
|
5240
1170
|
setProjectOwner: function (ownerId) { projectOwnerId = ownerId; },
|
|
@@ -5259,15 +1189,4 @@ function createProjectContext(opts) {
|
|
|
5259
1189
|
};
|
|
5260
1190
|
}
|
|
5261
1191
|
|
|
5262
|
-
function parseJsonBody(req) {
|
|
5263
|
-
return new Promise(function (resolve, reject) {
|
|
5264
|
-
var body = "";
|
|
5265
|
-
req.on("data", function (chunk) { body += chunk; });
|
|
5266
|
-
req.on("end", function () {
|
|
5267
|
-
try { resolve(JSON.parse(body)); }
|
|
5268
|
-
catch (e) { reject(e); }
|
|
5269
|
-
});
|
|
5270
|
-
});
|
|
5271
|
-
}
|
|
5272
|
-
|
|
5273
1192
|
module.exports = { createProjectContext: createProjectContext, safePath: safePath, validateEnvString: validateEnvString };
|