clay-server 2.15.2 → 2.16.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/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
- "Respond with exactly one of:\n" +
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]\n\n" +
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 should complete quickly (no tool use), 3 minutes is generous
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
- }, 3 * 60 * 1000);
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
- // No judge: force single iteration
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
- // Go straight to approval (no crafting needed)
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: !!wData.judgeText, bothReady: !!wData.judgeText });
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().length > 0;
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) {