clay-server 2.15.2 → 2.16.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/daemon.js +9 -0
- package/lib/project.js +156 -15
- package/lib/public/app.js +381 -83
- package/lib/public/css/command-palette.css +43 -3
- package/lib/public/css/filebrowser.css +28 -3
- package/lib/public/css/home-hub.css +77 -0
- package/lib/public/css/icon-strip.css +15 -1
- package/lib/public/css/input.css +1 -1
- package/lib/public/css/mates.css +207 -3
- package/lib/public/css/messages.css +44 -2
- package/lib/public/css/mobile-nav.css +412 -7
- package/lib/public/css/rewind.css +13 -0
- package/lib/public/css/server-settings.css +21 -5
- package/lib/public/css/session-search.css +1 -1
- package/lib/public/css/sidebar.css +20 -2
- package/lib/public/css/title-bar.css +1 -23
- package/lib/public/index.html +57 -6
- package/lib/public/modules/longpress.js +74 -0
- package/lib/public/modules/mate-knowledge.js +30 -0
- package/lib/public/modules/mate-sidebar.js +14 -9
- package/lib/public/modules/notifications.js +10 -0
- package/lib/public/modules/project-settings.js +14 -0
- package/lib/public/modules/server-settings.js +17 -0
- package/lib/public/modules/sidebar.js +681 -31
- package/lib/public/modules/stt.js +5 -1
- package/lib/public/modules/terminal.js +28 -1
- package/lib/public/modules/tools.js +5 -1
- package/lib/sdk-bridge.js +7 -6
- package/lib/server.js +53 -3
- package/package.json +1 -1
package/lib/daemon.js
CHANGED
|
@@ -647,8 +647,17 @@ var relay = createServer({
|
|
|
647
647
|
hostname: os2.hostname(),
|
|
648
648
|
lanIp: lanIp || null,
|
|
649
649
|
updateChannel: config.updateChannel || "stable",
|
|
650
|
+
imageRetentionDays: config.imageRetentionDays !== undefined ? config.imageRetentionDays : 7,
|
|
650
651
|
};
|
|
651
652
|
},
|
|
653
|
+
onSetImageRetention: function (days) {
|
|
654
|
+
var d = parseInt(days, 10);
|
|
655
|
+
if (isNaN(d) || d < 0) d = 7;
|
|
656
|
+
config.imageRetentionDays = d;
|
|
657
|
+
saveConfig(config);
|
|
658
|
+
console.log("[daemon] Image retention:", d === 0 ? "forever" : d + " days");
|
|
659
|
+
return { ok: true, days: d };
|
|
660
|
+
},
|
|
652
661
|
onSetUpdateChannel: function (channel) {
|
|
653
662
|
config.updateChannel = channel === "beta" ? "beta" : "stable";
|
|
654
663
|
saveConfig(config);
|
package/lib/project.js
CHANGED
|
@@ -119,6 +119,47 @@ function createProjectContext(opts) {
|
|
|
119
119
|
var onCreateWorktree = opts.onCreateWorktree || null;
|
|
120
120
|
var latestVersion = null;
|
|
121
121
|
|
|
122
|
+
// --- Chat image storage ---
|
|
123
|
+
var _imgConfig = require("./config");
|
|
124
|
+
var _imgUtils = require("./utils");
|
|
125
|
+
var _imagesBaseDir = path.join(_imgConfig.CONFIG_DIR, "images");
|
|
126
|
+
var _imagesEncodedCwd = _imgUtils.encodeCwd(cwd);
|
|
127
|
+
var imagesDir = path.join(_imagesBaseDir, _imagesEncodedCwd);
|
|
128
|
+
|
|
129
|
+
// Convert imageRefs in history entries to images with URLs for the client
|
|
130
|
+
function hydrateImageRefs(entry) {
|
|
131
|
+
if (!entry || entry.type !== "user_message" || !entry.imageRefs) return entry;
|
|
132
|
+
var images = [];
|
|
133
|
+
for (var ri = 0; ri < entry.imageRefs.length; ri++) {
|
|
134
|
+
var ref = entry.imageRefs[ri];
|
|
135
|
+
images.push({ mediaType: ref.mediaType, url: "/p/" + slug + "/images/" + ref.file });
|
|
136
|
+
}
|
|
137
|
+
var hydrated = {};
|
|
138
|
+
for (var k in entry) {
|
|
139
|
+
if (k !== "imageRefs") hydrated[k] = entry[k];
|
|
140
|
+
}
|
|
141
|
+
hydrated.images = images;
|
|
142
|
+
return hydrated;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function saveImageFile(mediaType, base64data) {
|
|
146
|
+
try { fs.mkdirSync(imagesDir, { recursive: true }); } catch (e) {}
|
|
147
|
+
var ext = mediaType === "image/png" ? ".png" : mediaType === "image/gif" ? ".gif" : mediaType === "image/webp" ? ".webp" : ".jpg";
|
|
148
|
+
var hash = crypto.createHash("sha256").update(base64data).digest("hex").substring(0, 16);
|
|
149
|
+
var fileName = Date.now() + "-" + hash + ext;
|
|
150
|
+
var filePath = path.join(imagesDir, fileName);
|
|
151
|
+
try {
|
|
152
|
+
fs.writeFileSync(filePath, Buffer.from(base64data, "base64"));
|
|
153
|
+
if (process.platform !== "win32") {
|
|
154
|
+
try { fs.chmodSync(filePath, 0o600); } catch (e) {}
|
|
155
|
+
}
|
|
156
|
+
return fileName;
|
|
157
|
+
} catch (e) {
|
|
158
|
+
console.error("[images] Failed to save image:", e.message);
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
122
163
|
// --- OS-level user isolation helper ---
|
|
123
164
|
// Returns the Linux username for the session owner.
|
|
124
165
|
// Each session uses its own owner's Claude account and credits.
|
|
@@ -372,6 +413,7 @@ function createProjectContext(opts) {
|
|
|
372
413
|
send: send,
|
|
373
414
|
pushModule: pushModule,
|
|
374
415
|
getSDK: getSDK,
|
|
416
|
+
mateDisplayName: opts.mateDisplayName || "",
|
|
375
417
|
dangerouslySkipPermissions: dangerouslySkipPermissions,
|
|
376
418
|
onProcessingChanged: onProcessingChanged,
|
|
377
419
|
});
|
|
@@ -844,15 +886,25 @@ function createProjectContext(opts) {
|
|
|
844
886
|
return;
|
|
845
887
|
}
|
|
846
888
|
|
|
889
|
+
var gitLog = "";
|
|
890
|
+
try {
|
|
891
|
+
gitLog = execFileSync("git", ["log", "--oneline", loopState.baseCommit + "..HEAD"], {
|
|
892
|
+
cwd: cwd, encoding: "utf8", timeout: 10000,
|
|
893
|
+
}).trim();
|
|
894
|
+
} catch (e) {}
|
|
895
|
+
|
|
847
896
|
var judgePrompt = "You are a judge evaluating whether a coding task has been completed.\n\n" +
|
|
848
897
|
"## Original Task (PROMPT.md)\n\n" + loopState.promptText + "\n\n" +
|
|
849
898
|
"## Evaluation Criteria (JUDGE.md)\n\n" + loopState.judgeText + "\n\n" +
|
|
899
|
+
"## Commit History\n\n```\n" + (gitLog || "(no commits yet)") + "\n```\n\n" +
|
|
850
900
|
"## Changes Made (git diff)\n\n```diff\n" + diff + "\n```\n\n" +
|
|
851
901
|
"Based on the evaluation criteria, has the task been completed successfully?\n\n" +
|
|
852
|
-
"
|
|
902
|
+
"IMPORTANT: The git diff above may not show everything. If criteria involve checking whether " +
|
|
903
|
+
"specific files, classes, or features exist, use tools (Read, Glob, Grep, Bash) to verify " +
|
|
904
|
+
"directly in the codebase. Do NOT assume something is missing just because it is not in the diff.\n\n" +
|
|
905
|
+
"After your evaluation, respond with exactly one of:\n" +
|
|
853
906
|
"- PASS: [brief explanation]\n" +
|
|
854
|
-
"- FAIL: [brief explanation of what is still missing]
|
|
855
|
-
"Do NOT use any tools. Just analyze and respond.";
|
|
907
|
+
"- FAIL: [brief explanation of what is still missing]";
|
|
856
908
|
|
|
857
909
|
var judgeSession = sm.createSession();
|
|
858
910
|
var judgeName = (loopState.wizardData && loopState.wizardData.name) || "";
|
|
@@ -900,7 +952,7 @@ function createProjectContext(opts) {
|
|
|
900
952
|
}
|
|
901
953
|
};
|
|
902
954
|
|
|
903
|
-
// Watchdog: judge
|
|
955
|
+
// Watchdog: judge may use tools to verify, so allow more time
|
|
904
956
|
var judgeWatchdog = setTimeout(function() {
|
|
905
957
|
if (!judgeCompleted && loopState.active && !loopState.stopping) {
|
|
906
958
|
console.error("[ralph-loop] Judge #" + loopState.iteration + " watchdog triggered — onQueryComplete never fired");
|
|
@@ -918,7 +970,7 @@ function createProjectContext(opts) {
|
|
|
918
970
|
});
|
|
919
971
|
setTimeout(function() { runNextIteration(); }, 2000);
|
|
920
972
|
}
|
|
921
|
-
},
|
|
973
|
+
}, 10 * 60 * 1000);
|
|
922
974
|
|
|
923
975
|
var userMsg = { type: "user_message", text: judgePrompt };
|
|
924
976
|
judgeSession.history.push(userMsg);
|
|
@@ -1235,7 +1287,7 @@ function createProjectContext(opts) {
|
|
|
1235
1287
|
}
|
|
1236
1288
|
sendTo(ws, { type: "history_meta", total: total, from: fromIndex });
|
|
1237
1289
|
for (var i = fromIndex; i < total; i++) {
|
|
1238
|
-
sendTo(ws, active.history[i]);
|
|
1290
|
+
sendTo(ws, hydrateImageRefs(active.history[i]));
|
|
1239
1291
|
}
|
|
1240
1292
|
sendTo(ws, { type: "history_done" });
|
|
1241
1293
|
|
|
@@ -1455,7 +1507,7 @@ function createProjectContext(opts) {
|
|
|
1455
1507
|
var targetFrom = typeof msg.target === "number" ? msg.target : before - sm.HISTORY_PAGE_SIZE;
|
|
1456
1508
|
var from = sm.findTurnBoundary(session.history, Math.max(0, targetFrom));
|
|
1457
1509
|
var to = before;
|
|
1458
|
-
var items = session.history.slice(from, to);
|
|
1510
|
+
var items = session.history.slice(from, to).map(hydrateImageRefs);
|
|
1459
1511
|
sendTo(ws, {
|
|
1460
1512
|
type: "history_prepend",
|
|
1461
1513
|
items: items,
|
|
@@ -2418,7 +2470,7 @@ function createProjectContext(opts) {
|
|
|
2418
2470
|
|
|
2419
2471
|
// --- Daemon config / server management (admin-only in multi-user mode) ---
|
|
2420
2472
|
if (msg.type === "get_daemon_config" || msg.type === "set_pin" || msg.type === "set_keep_awake" ||
|
|
2421
|
-
msg.type === "shutdown_server" || msg.type === "restart_server") {
|
|
2473
|
+
msg.type === "set_image_retention" || msg.type === "shutdown_server" || msg.type === "restart_server") {
|
|
2422
2474
|
if (usersModule.isMultiUser()) {
|
|
2423
2475
|
var _wsUser = ws._clayUser;
|
|
2424
2476
|
if (!_wsUser || _wsUser.role !== "admin") {
|
|
@@ -2453,6 +2505,14 @@ function createProjectContext(opts) {
|
|
|
2453
2505
|
return;
|
|
2454
2506
|
}
|
|
2455
2507
|
|
|
2508
|
+
if (msg.type === "set_image_retention") {
|
|
2509
|
+
if (typeof opts.onSetImageRetention === "function") {
|
|
2510
|
+
var irResult = opts.onSetImageRetention(msg.days);
|
|
2511
|
+
sendTo(ws, { type: "set_image_retention_result", ok: irResult.ok, days: irResult.days });
|
|
2512
|
+
}
|
|
2513
|
+
return;
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2456
2516
|
if (msg.type === "shutdown_server") {
|
|
2457
2517
|
if (typeof opts.onShutdown === "function") {
|
|
2458
2518
|
sendTo(ws, { type: "shutdown_server_result", ok: true });
|
|
@@ -3073,18 +3133,62 @@ function createProjectContext(opts) {
|
|
|
3073
3133
|
var tmpJudge = judgePath + ".tmp";
|
|
3074
3134
|
fs.writeFileSync(tmpJudge, wData.judgeText);
|
|
3075
3135
|
fs.renameSync(tmpJudge, judgePath);
|
|
3076
|
-
} else {
|
|
3077
|
-
//
|
|
3136
|
+
} else if (!recordSource) {
|
|
3137
|
+
// Scheduled task with no judge: force single iteration and go to approval
|
|
3078
3138
|
var singleJson = loopJsonPath + ".tmp2";
|
|
3079
3139
|
fs.writeFileSync(singleJson, JSON.stringify({ maxIterations: 1 }, null, 2));
|
|
3080
3140
|
fs.renameSync(singleJson, loopJsonPath);
|
|
3141
|
+
|
|
3142
|
+
loopState.phase = "approval";
|
|
3143
|
+
saveLoopState();
|
|
3144
|
+
send({ type: "ralph_phase", phase: "approval", source: recordSource, wizardData: loopState.wizardData });
|
|
3145
|
+
send({ type: "ralph_files_status", promptReady: true, judgeReady: false, bothReady: true });
|
|
3146
|
+
return;
|
|
3147
|
+
} else {
|
|
3148
|
+
// Ralph with no judge: start a crafting session to create JUDGE.md
|
|
3149
|
+
loopState.phase = "crafting";
|
|
3150
|
+
saveLoopState();
|
|
3151
|
+
|
|
3152
|
+
var judgeCraftPrompt = "Use the /clay-ralph skill to design ONLY a JUDGE.md for an existing Ralph Loop. " +
|
|
3153
|
+
"The user has already provided PROMPT.md, so do NOT create or modify PROMPT.md. " +
|
|
3154
|
+
"You MUST invoke the clay-ralph skill — do NOT execute the task yourself. " +
|
|
3155
|
+
"Your job is to read the existing PROMPT.md and create a JUDGE.md " +
|
|
3156
|
+
"that will evaluate whether the coder session completed the task successfully.\n\n" +
|
|
3157
|
+
"## Task\n" + (wData.task || "") +
|
|
3158
|
+
"\n\n## Loop Directory\n" + lDir;
|
|
3159
|
+
|
|
3160
|
+
var judgeCraftSession = sm.createSession();
|
|
3161
|
+
judgeCraftSession.title = (isRalphCraft ? "Ralph" : "Task") + (craftName ? " " + craftName : "") + " Crafting";
|
|
3162
|
+
judgeCraftSession.ralphCraftingMode = true;
|
|
3163
|
+
judgeCraftSession.loop = { active: true, iteration: 0, role: "crafting", loopId: newLoopId, name: craftName, source: recordSource, startedAt: loopState.startedAt };
|
|
3164
|
+
sm.saveSessionFile(judgeCraftSession);
|
|
3165
|
+
sm.switchSession(judgeCraftSession.localId);
|
|
3166
|
+
loopState.craftingSessionId = judgeCraftSession.localId;
|
|
3167
|
+
|
|
3168
|
+
loopRegistry.updateRecord(newLoopId, { craftingSessionId: judgeCraftSession.localId });
|
|
3169
|
+
|
|
3170
|
+
startClaudeDirWatch();
|
|
3171
|
+
|
|
3172
|
+
judgeCraftSession.history.push({ type: "user_message", text: judgeCraftPrompt });
|
|
3173
|
+
sm.appendToSessionFile(judgeCraftSession, { type: "user_message", text: judgeCraftPrompt });
|
|
3174
|
+
sendToSession(judgeCraftSession.localId, { type: "user_message", text: judgeCraftPrompt });
|
|
3175
|
+
judgeCraftSession.isProcessing = true;
|
|
3176
|
+
onProcessingChanged();
|
|
3177
|
+
judgeCraftSession.sentToolResults = {};
|
|
3178
|
+
sendToSession(judgeCraftSession.localId, { type: "status", status: "processing" });
|
|
3179
|
+
sdk.startQuery(judgeCraftSession, judgeCraftPrompt, undefined, getLinuxUserForSession(judgeCraftSession));
|
|
3180
|
+
|
|
3181
|
+
send({ type: "ralph_crafting_started", sessionId: judgeCraftSession.localId, taskId: newLoopId, source: recordSource });
|
|
3182
|
+
send({ type: "ralph_phase", phase: "crafting", wizardData: loopState.wizardData, craftingSessionId: judgeCraftSession.localId });
|
|
3183
|
+
send({ type: "ralph_files_status", promptReady: true, judgeReady: false, bothReady: false });
|
|
3184
|
+
return;
|
|
3081
3185
|
}
|
|
3082
3186
|
|
|
3083
|
-
//
|
|
3187
|
+
// Both prompt and judge provided: go straight to approval
|
|
3084
3188
|
loopState.phase = "approval";
|
|
3085
3189
|
saveLoopState();
|
|
3086
|
-
send({ type: "ralph_phase", phase: "approval", wizardData: loopState.wizardData });
|
|
3087
|
-
send({ type: "ralph_files_status", promptReady: true, judgeReady:
|
|
3190
|
+
send({ type: "ralph_phase", phase: "approval", source: recordSource, wizardData: loopState.wizardData });
|
|
3191
|
+
send({ type: "ralph_files_status", promptReady: true, judgeReady: true, bothReady: true });
|
|
3088
3192
|
return;
|
|
3089
3193
|
}
|
|
3090
3194
|
|
|
@@ -3320,13 +3424,25 @@ function createProjectContext(opts) {
|
|
|
3320
3424
|
var userMsg = { type: "user_message", text: msg.text || "" };
|
|
3321
3425
|
if (msg.images && msg.images.length > 0) {
|
|
3322
3426
|
userMsg.imageCount = msg.images.length;
|
|
3427
|
+
// Save images as files, store URL references in history
|
|
3428
|
+
var imageRefs = [];
|
|
3429
|
+
for (var imgIdx = 0; imgIdx < msg.images.length; imgIdx++) {
|
|
3430
|
+
var img = msg.images[imgIdx];
|
|
3431
|
+
var savedName = saveImageFile(img.mediaType, img.data);
|
|
3432
|
+
if (savedName) {
|
|
3433
|
+
imageRefs.push({ mediaType: img.mediaType, file: savedName });
|
|
3434
|
+
}
|
|
3435
|
+
}
|
|
3436
|
+
if (imageRefs.length > 0) {
|
|
3437
|
+
userMsg.imageRefs = imageRefs;
|
|
3438
|
+
}
|
|
3323
3439
|
}
|
|
3324
3440
|
if (msg.pastes && msg.pastes.length > 0) {
|
|
3325
3441
|
userMsg.pastes = msg.pastes;
|
|
3326
3442
|
}
|
|
3327
3443
|
session.history.push(userMsg);
|
|
3328
3444
|
sm.appendToSessionFile(session, userMsg);
|
|
3329
|
-
sendToSessionOthers(ws, session.localId, userMsg);
|
|
3445
|
+
sendToSessionOthers(ws, session.localId, hydrateImageRefs(userMsg));
|
|
3330
3446
|
|
|
3331
3447
|
if (!session.title) {
|
|
3332
3448
|
session.title = (msg.text || "Image").substring(0, 50);
|
|
@@ -3407,6 +3523,29 @@ function createProjectContext(opts) {
|
|
|
3407
3523
|
|
|
3408
3524
|
// --- Handle project-scoped HTTP requests ---
|
|
3409
3525
|
function handleHTTP(req, res, urlPath) {
|
|
3526
|
+
// Serve chat images
|
|
3527
|
+
if (req.method === "GET" && urlPath.indexOf("/images/") === 0) {
|
|
3528
|
+
var imgName = path.basename(urlPath);
|
|
3529
|
+
// Sanitize: only allow expected filename pattern
|
|
3530
|
+
if (!/^\d+-[a-f0-9]+\.\w+$/.test(imgName)) {
|
|
3531
|
+
res.writeHead(400);
|
|
3532
|
+
res.end("Bad request");
|
|
3533
|
+
return true;
|
|
3534
|
+
}
|
|
3535
|
+
var imgPath = path.join(imagesDir, imgName);
|
|
3536
|
+
try {
|
|
3537
|
+
var imgBuf = fs.readFileSync(imgPath);
|
|
3538
|
+
var ext = path.extname(imgName).toLowerCase();
|
|
3539
|
+
var mime = ext === ".png" ? "image/png" : ext === ".gif" ? "image/gif" : ext === ".webp" ? "image/webp" : "image/jpeg";
|
|
3540
|
+
res.writeHead(200, { "Content-Type": mime, "Cache-Control": "public, max-age=86400" });
|
|
3541
|
+
res.end(imgBuf);
|
|
3542
|
+
} catch (e) {
|
|
3543
|
+
res.writeHead(404);
|
|
3544
|
+
res.end("Not found");
|
|
3545
|
+
}
|
|
3546
|
+
return true;
|
|
3547
|
+
}
|
|
3548
|
+
|
|
3410
3549
|
// File upload
|
|
3411
3550
|
if (req.method === "POST" && urlPath === "/api/upload") {
|
|
3412
3551
|
parseJsonBody(req).then(function (body) {
|
|
@@ -3861,7 +4000,9 @@ function createProjectContext(opts) {
|
|
|
3861
4000
|
var execSync = require("child_process").execSync;
|
|
3862
4001
|
try {
|
|
3863
4002
|
var out = execSync("git status --porcelain", { cwd: cwd, encoding: "utf8", timeout: 5000 });
|
|
3864
|
-
var dirty = out.trim().
|
|
4003
|
+
var dirty = out.trim().split("\n").some(function (line) {
|
|
4004
|
+
return line.trim().length > 0 && !line.startsWith("??");
|
|
4005
|
+
});
|
|
3865
4006
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3866
4007
|
res.end(JSON.stringify({ dirty: dirty }));
|
|
3867
4008
|
} catch (e) {
|