clay-server 2.7.0 → 2.7.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/bin/cli.js CHANGED
@@ -21,8 +21,9 @@ var { execSync, execFileSync, spawn } = require("child_process");
21
21
  var qrcode = require("qrcode-terminal");
22
22
  var net = require("net");
23
23
 
24
- // Detect dev mode (no separate storage — dev and prod share ~/.clay)
24
+ // Detect dev mode — dev and prod use separate daemon files so they can run simultaneously
25
25
  var _isDev = (process.argv[1] && path.basename(process.argv[1]) === "clay-dev") || process.argv.includes("--dev");
26
+ if (_isDev) process.env.CLAY_DEV = "1";
26
27
 
27
28
  var { loadConfig, saveConfig, configPath, socketPath, logPath, ensureConfigDir, isDaemonAlive, isDaemonAliveAsync, generateSlug, clearStaleConfig, loadClayrc, saveClayrc, readCrashInfo } = require("../lib/config");
28
29
  var { sendIPCCommand } = require("../lib/ipc");
package/lib/config.js CHANGED
@@ -70,20 +70,23 @@ var CONFIG_DIR = CLAY_HOME;
70
70
  var CLAYRC_PATH = path.join(os.homedir(), ".clayrc");
71
71
  var CRASH_INFO_PATH = path.join(CONFIG_DIR, "crash.json");
72
72
 
73
+ // Dev mode uses separate daemon files so dev and prod can run simultaneously
74
+ var _devMode = !!process.env.CLAY_DEV;
75
+
73
76
  function configPath() {
74
- return path.join(CONFIG_DIR, "daemon.json");
77
+ return path.join(CONFIG_DIR, _devMode ? "daemon-dev.json" : "daemon.json");
75
78
  }
76
79
 
77
80
  function socketPath() {
78
81
  if (process.platform === "win32") {
79
- var pipeName = "clay-daemon";
82
+ var pipeName = _devMode ? "clay-daemon-dev" : "clay-daemon";
80
83
  return "\\\\.\\pipe\\" + pipeName;
81
84
  }
82
- return path.join(CONFIG_DIR, "daemon.sock");
85
+ return path.join(CONFIG_DIR, _devMode ? "daemon-dev.sock" : "daemon.sock");
83
86
  }
84
87
 
85
88
  function logPath() {
86
- return path.join(CONFIG_DIR, "daemon.log");
89
+ return path.join(CONFIG_DIR, _devMode ? "daemon-dev.log" : "daemon.log");
87
90
  }
88
91
 
89
92
  function ensureConfigDir() {
package/lib/notes.js CHANGED
@@ -8,8 +8,8 @@ function createNotesManager(opts) {
8
8
  var cwd = opts.cwd;
9
9
 
10
10
  // Storage path: ~/.clay/notes/{encodedCwd}.json
11
- var encodedCwd = utils.encodeCwd(cwd);
12
11
  var notesDir = path.join(config.CONFIG_DIR, "notes");
12
+ var encodedCwd = utils.resolveEncodedFile(notesDir, cwd, ".json");
13
13
  var notesFile = path.join(notesDir, encodedCwd + ".json");
14
14
 
15
15
  // In-memory cache
package/lib/project.js CHANGED
@@ -8,6 +8,7 @@ var { createTerminalManager } = require("./terminal-manager");
8
8
  var { createNotesManager } = require("./notes");
9
9
  var { fetchLatestVersion, isNewer } = require("./updater");
10
10
  var { execFileSync, spawn } = require("child_process");
11
+ var { createLoopRegistry } = require("./scheduler");
11
12
 
12
13
  var MAX_UPLOAD_BYTES = 50 * 1024 * 1024; // 50 MB
13
14
 
@@ -253,8 +254,8 @@ function createProjectContext(opts) {
253
254
  // Loop state persistence
254
255
  var _loopConfig = require("./config");
255
256
  var _loopUtils = require("./utils");
256
- var _loopEncodedCwd = _loopUtils.encodeCwd(cwd);
257
257
  var _loopDir = path.join(_loopConfig.CONFIG_DIR, "loops");
258
+ var _loopEncodedCwd = _loopUtils.resolveEncodedFile(_loopDir, cwd, ".json");
258
259
  var _loopStatePath = path.join(_loopDir, _loopEncodedCwd + ".json");
259
260
 
260
261
  function saveLoopState() {
@@ -395,17 +396,71 @@ function createProjectContext(opts) {
395
396
  judgeReady: hasJudge,
396
397
  loopJsonReady: hasLoopJson,
397
398
  bothReady: hasPrompt && hasJudge,
399
+ taskId: loopState.loopId,
398
400
  });
399
401
  // Auto-transition to approval phase when both files appear
400
402
  if (hasPrompt && hasJudge && loopState.phase === "crafting") {
401
403
  loopState.phase = "approval";
402
404
  saveLoopState();
405
+
406
+ // Parse recommended title from crafting session conversation
407
+ if (loopState.craftingSessionId && loopState.loopId) {
408
+ var craftSess = sm.sessions.get(loopState.craftingSessionId);
409
+ if (craftSess && craftSess.history) {
410
+ for (var hi = craftSess.history.length - 1; hi >= 0; hi--) {
411
+ var entry = craftSess.history[hi];
412
+ var entryText = entry.text || "";
413
+ var titleMatch = entryText.match(/\[\[LOOP_TITLE:\s*(.+?)\]\]/);
414
+ if (titleMatch) {
415
+ var suggestedTitle = titleMatch[1].trim();
416
+ if (suggestedTitle) {
417
+ loopRegistry.updateRecord(loopState.loopId, { name: suggestedTitle });
418
+ }
419
+ break;
420
+ }
421
+ }
422
+ }
423
+ }
403
424
  }
404
425
  }
405
426
 
406
427
  // Load persisted state on startup
407
428
  loadLoopState();
408
429
 
430
+ // --- Loop Registry (unified one-off + scheduled) ---
431
+ var activeRegistryId = null; // track which registry record triggered current loop
432
+
433
+ var loopRegistry = createLoopRegistry({
434
+ cwd: cwd,
435
+ onTrigger: function (record) {
436
+ // Only trigger if no loop is currently active
437
+ if (loopState.active || loopState.phase === "executing") {
438
+ console.log("[loop-registry] Skipping trigger — loop already active");
439
+ return;
440
+ }
441
+ // Verify the loop directory and files exist
442
+ var recDir = path.join(cwd, ".claude", "loops", record.id);
443
+ try {
444
+ fs.accessSync(path.join(recDir, "PROMPT.md"));
445
+ fs.accessSync(path.join(recDir, "JUDGE.md"));
446
+ } catch (e) {
447
+ console.error("[loop-registry] Loop files missing for " + record.id);
448
+ return;
449
+ }
450
+ // Set the loopId and start
451
+ loopState.loopId = record.id;
452
+ activeRegistryId = record.id;
453
+ console.log("[loop-registry] Auto-starting loop: " + record.name);
454
+ send({ type: "schedule_run_started", recordId: record.id });
455
+ startLoop();
456
+ },
457
+ onChange: function (records) {
458
+ send({ type: "loop_registry_updated", records: records });
459
+ },
460
+ });
461
+ loopRegistry.load();
462
+ loopRegistry.startTimer();
463
+
409
464
  function startLoop(opts) {
410
465
  var loopOpts = opts || {};
411
466
  var dir = loopDir();
@@ -654,6 +709,19 @@ function createProjectContext(opts) {
654
709
  results: loopState.results,
655
710
  });
656
711
 
712
+ // Record result in loop registry
713
+ if (loopState.loopId) {
714
+ loopRegistry.recordRun(loopState.loopId, {
715
+ reason: reason,
716
+ startedAt: loopState.startedAt,
717
+ iterations: loopState.iteration,
718
+ });
719
+ }
720
+ if (activeRegistryId) {
721
+ send({ type: "schedule_run_finished", recordId: activeRegistryId, reason: reason, iterations: loopState.iteration });
722
+ activeRegistryId = null;
723
+ }
724
+
657
725
  if (pushModule) {
658
726
  var body = reason === "pass"
659
727
  ? "Task completed after " + loopState.iteration + " iteration(s)"
@@ -773,6 +841,7 @@ function createProjectContext(opts) {
773
841
  sendTo(ws, { type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [] });
774
842
  sendTo(ws, { type: "term_list", terminals: tm.list() });
775
843
  sendTo(ws, { type: "notes_list", notes: nm.list() });
844
+ sendTo(ws, { type: "loop_registry_updated", records: loopRegistry.getAll() });
776
845
 
777
846
  // Ralph Loop availability
778
847
  var hasLoopFiles = false;
@@ -809,6 +878,7 @@ function createProjectContext(opts) {
809
878
  promptReady: _hasPrompt,
810
879
  judgeReady: _hasJudge,
811
880
  bothReady: _hasPrompt && _hasJudge,
881
+ taskId: loopState.loopId,
812
882
  });
813
883
  }
814
884
 
@@ -2019,6 +2089,16 @@ function createProjectContext(opts) {
2019
2089
  }
2020
2090
 
2021
2091
  if (msg.type === "loop_start") {
2092
+ // If this loop has a cron schedule, don't run immediately — just confirm registration
2093
+ if (loopState.wizardData && loopState.wizardData.cron) {
2094
+ loopState.active = false;
2095
+ loopState.phase = "done";
2096
+ saveLoopState();
2097
+ send({ type: "loop_finished", reason: "scheduled", iterations: 0, results: [] });
2098
+ send({ type: "ralph_phase", phase: "idle", wizardData: null });
2099
+ send({ type: "loop_scheduled", recordId: loopState.loopId, cron: loopState.wizardData.cron });
2100
+ return;
2101
+ }
2022
2102
  startLoop();
2023
2103
  return;
2024
2104
  }
@@ -2031,17 +2111,29 @@ function createProjectContext(opts) {
2031
2111
  if (msg.type === "ralph_wizard_complete") {
2032
2112
  var wData = msg.data || {};
2033
2113
  var maxIter = wData.maxIterations || 25;
2114
+ var wizardCron = wData.cron || null;
2034
2115
  var newLoopId = generateLoopId();
2035
2116
  loopState.loopId = newLoopId;
2036
2117
  loopState.wizardData = {
2037
- name: (wData.name || "").replace(/[^a-zA-Z0-9_-]/g, "") || "ralph",
2118
+ name: wData.name || wData.task || "Untitled",
2038
2119
  task: wData.task || "",
2039
2120
  maxIterations: maxIter,
2121
+ cron: wizardCron,
2040
2122
  };
2041
2123
  loopState.phase = "crafting";
2042
2124
  loopState.startedAt = Date.now();
2043
2125
  saveLoopState();
2044
2126
 
2127
+ // Register in loop registry
2128
+ loopRegistry.register({
2129
+ id: newLoopId,
2130
+ name: loopState.wizardData.name,
2131
+ task: wData.task || "",
2132
+ cron: wizardCron,
2133
+ enabled: wizardCron ? true : false,
2134
+ maxIterations: maxIter,
2135
+ });
2136
+
2045
2137
  // Create loop directory and write LOOP.json
2046
2138
  var lDir = loopDir();
2047
2139
  try { fs.mkdirSync(lDir, { recursive: true }); } catch (e) {}
@@ -2051,19 +2143,26 @@ function createProjectContext(opts) {
2051
2143
  fs.renameSync(tmpLoopJson, loopJsonPath);
2052
2144
 
2053
2145
  // Assemble prompt for clay-ralph skill (include loop dir path so skill knows where to write)
2054
- var craftingPrompt = "/clay-ralph\n## Task\n" + (wData.task || "") +
2055
- "\n## Loop Directory\n" + lDir;
2146
+ var craftingPrompt = "Use the /clay-ralph skill to design a Ralph Loop for the following task. " +
2147
+ "You MUST invoke the clay-ralph skill — do NOT execute the task yourself. " +
2148
+ "Your job is to interview me, then create PROMPT.md and JUDGE.md files " +
2149
+ "that a future autonomous session will execute.\n\n" +
2150
+ "## Task\n" + (wData.task || "") +
2151
+ "\n\n## Loop Directory\n" + lDir;
2056
2152
 
2057
2153
  // Create a new session for crafting
2058
2154
  var craftingSession = sm.createSession();
2059
2155
  var craftName = (loopState.wizardData && loopState.wizardData.name) || "";
2060
2156
  craftingSession.title = "Ralph" + (craftName ? " " + craftName : "") + " Crafting";
2061
2157
  craftingSession.ralphCraftingMode = true;
2158
+ craftingSession.hidden = true;
2062
2159
  sm.saveSessionFile(craftingSession);
2063
2160
  sm.switchSession(craftingSession.localId);
2064
- sm.broadcastSessionList();
2065
2161
  loopState.craftingSessionId = craftingSession.localId;
2066
2162
 
2163
+ // Store crafting session ID in the registry record
2164
+ loopRegistry.updateRecord(newLoopId, { craftingSessionId: craftingSession.localId });
2165
+
2067
2166
  // Start .claude/ directory watcher
2068
2167
  startClaudeDirWatch();
2069
2168
 
@@ -2077,11 +2176,27 @@ function createProjectContext(opts) {
2077
2176
  send({ type: "status", status: "processing" });
2078
2177
  sdk.startQuery(craftingSession, craftingPrompt);
2079
2178
 
2080
- send({ type: "ralph_crafting_started", sessionId: craftingSession.localId });
2179
+ send({ type: "ralph_crafting_started", sessionId: craftingSession.localId, taskId: newLoopId });
2081
2180
  send({ type: "ralph_phase", phase: "crafting", wizardData: loopState.wizardData, craftingSessionId: craftingSession.localId });
2082
2181
  return;
2083
2182
  }
2084
2183
 
2184
+ if (msg.type === "loop_registry_files") {
2185
+ var recId = msg.id;
2186
+ var lDir = path.join(cwd, ".claude", "loops", recId);
2187
+ var promptContent = "";
2188
+ var judgeContent = "";
2189
+ try { promptContent = fs.readFileSync(path.join(lDir, "PROMPT.md"), "utf8"); } catch (e) {}
2190
+ try { judgeContent = fs.readFileSync(path.join(lDir, "JUDGE.md"), "utf8"); } catch (e) {}
2191
+ send({
2192
+ type: "loop_registry_files_content",
2193
+ id: recId,
2194
+ prompt: promptContent,
2195
+ judge: judgeContent,
2196
+ });
2197
+ return;
2198
+ }
2199
+
2085
2200
  if (msg.type === "ralph_preview_files") {
2086
2201
  var promptContent = "";
2087
2202
  var judgeContent = "";
@@ -2129,6 +2244,62 @@ function createProjectContext(opts) {
2129
2244
  return;
2130
2245
  }
2131
2246
 
2247
+ // --- Loop Registry messages ---
2248
+ if (msg.type === "loop_registry_list") {
2249
+ sendTo(ws, { type: "loop_registry_updated", records: loopRegistry.getAll() });
2250
+ return;
2251
+ }
2252
+
2253
+ if (msg.type === "loop_registry_update") {
2254
+ var updatedRec = loopRegistry.update(msg.id, msg.data || {});
2255
+ if (!updatedRec) {
2256
+ sendTo(ws, { type: "loop_registry_error", text: "Record not found" });
2257
+ }
2258
+ return;
2259
+ }
2260
+
2261
+ if (msg.type === "loop_registry_remove") {
2262
+ var removedRec = loopRegistry.remove(msg.id);
2263
+ if (!removedRec) {
2264
+ sendTo(ws, { type: "loop_registry_error", text: "Record not found" });
2265
+ }
2266
+ return;
2267
+ }
2268
+
2269
+ if (msg.type === "loop_registry_toggle") {
2270
+ var toggledRec = loopRegistry.toggleEnabled(msg.id);
2271
+ if (!toggledRec) {
2272
+ sendTo(ws, { type: "loop_registry_error", text: "Record not found or not scheduled" });
2273
+ }
2274
+ return;
2275
+ }
2276
+
2277
+ if (msg.type === "loop_registry_rerun") {
2278
+ // Re-run an existing job (one-off from library)
2279
+ if (loopState.active || loopState.phase === "executing") {
2280
+ sendTo(ws, { type: "loop_registry_error", text: "A loop is already running" });
2281
+ return;
2282
+ }
2283
+ var rerunRec = loopRegistry.getById(msg.id);
2284
+ if (!rerunRec) {
2285
+ sendTo(ws, { type: "loop_registry_error", text: "Record not found" });
2286
+ return;
2287
+ }
2288
+ var rerunDir = path.join(cwd, ".claude", "loops", rerunRec.id);
2289
+ try {
2290
+ fs.accessSync(path.join(rerunDir, "PROMPT.md"));
2291
+ fs.accessSync(path.join(rerunDir, "JUDGE.md"));
2292
+ } catch (e) {
2293
+ sendTo(ws, { type: "loop_registry_error", text: "Loop files missing for " + rerunRec.id });
2294
+ return;
2295
+ }
2296
+ loopState.loopId = rerunRec.id;
2297
+ activeRegistryId = null; // not a scheduled trigger
2298
+ send({ type: "loop_rerun_started", recordId: rerunRec.id });
2299
+ startLoop();
2300
+ return;
2301
+ }
2302
+
2132
2303
  if (msg.type !== "message") return;
2133
2304
  if (!msg.text && (!msg.images || msg.images.length === 0) && (!msg.pastes || msg.pastes.length === 0)) return;
2134
2305
 
@@ -2491,6 +2662,7 @@ function createProjectContext(opts) {
2491
2662
 
2492
2663
  // --- Destroy ---
2493
2664
  function destroy() {
2665
+ loopRegistry.stopTimer();
2494
2666
  stopFileWatch();
2495
2667
  stopAllDirWatches();
2496
2668
  // Abort all active sessions