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 +2 -1
- package/lib/config.js +7 -4
- package/lib/notes.js +1 -1
- package/lib/project.js +178 -6
- package/lib/public/app.js +219 -56
- package/lib/public/css/loop.css +62 -1
- package/lib/public/css/menus.css +5 -0
- package/lib/public/css/mobile-nav.css +15 -15
- package/lib/public/css/scheduler-modal.css +546 -0
- package/lib/public/css/scheduler.css +944 -0
- package/lib/public/css/title-bar.css +6 -6
- package/lib/public/index.html +85 -11
- package/lib/public/modules/input.js +13 -3
- package/lib/public/modules/markdown.js +10 -0
- package/lib/public/modules/scheduler.js +1240 -0
- package/lib/public/style.css +2 -0
- package/lib/scheduler.js +362 -0
- package/lib/sdk-bridge.js +8 -0
- package/lib/sessions.js +7 -4
- package/lib/utils.js +49 -3
- package/package.json +1 -1
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
|
|
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:
|
|
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
|
|
2055
|
-
"
|
|
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
|