clay-server 2.7.2 → 2.8.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/bin/cli.js +31 -17
- package/lib/config.js +7 -4
- package/lib/project.js +343 -15
- package/lib/public/app.js +1039 -134
- package/lib/public/apple-touch-icon-dark.png +0 -0
- package/lib/public/apple-touch-icon.png +0 -0
- package/lib/public/clay-logo.png +0 -0
- package/lib/public/css/base.css +18 -1
- package/lib/public/css/filebrowser.css +1 -0
- package/lib/public/css/home-hub.css +455 -0
- package/lib/public/css/icon-strip.css +6 -5
- package/lib/public/css/loop.css +141 -23
- package/lib/public/css/messages.css +2 -0
- package/lib/public/css/mobile-nav.css +38 -12
- package/lib/public/css/overlays.css +205 -169
- package/lib/public/css/playbook.css +264 -0
- package/lib/public/css/profile.css +268 -0
- package/lib/public/css/scheduler-modal.css +1429 -0
- package/lib/public/css/scheduler.css +1305 -0
- package/lib/public/css/sidebar.css +305 -11
- package/lib/public/css/sticky-notes.css +23 -19
- package/lib/public/css/stt.css +155 -0
- package/lib/public/css/title-bar.css +14 -6
- package/lib/public/favicon-banded-32.png +0 -0
- package/lib/public/favicon-banded.png +0 -0
- package/lib/public/icon-192-dark.png +0 -0
- package/lib/public/icon-192.png +0 -0
- package/lib/public/icon-512-dark.png +0 -0
- package/lib/public/icon-512.png +0 -0
- package/lib/public/icon-banded-76.png +0 -0
- package/lib/public/icon-banded-96.png +0 -0
- package/lib/public/index.html +336 -44
- package/lib/public/modules/ascii-logo.js +442 -0
- package/lib/public/modules/markdown.js +18 -0
- package/lib/public/modules/notifications.js +50 -63
- package/lib/public/modules/playbook.js +578 -0
- package/lib/public/modules/profile.js +357 -0
- package/lib/public/modules/project-settings.js +1 -9
- package/lib/public/modules/scheduler.js +2826 -0
- package/lib/public/modules/server-settings.js +1 -1
- package/lib/public/modules/sidebar.js +376 -32
- package/lib/public/modules/stt.js +272 -0
- package/lib/public/modules/terminal.js +32 -0
- package/lib/public/modules/theme.js +3 -10
- package/lib/public/style.css +6 -0
- package/lib/public/sw.js +82 -3
- package/lib/public/wordmark-banded-20.png +0 -0
- package/lib/public/wordmark-banded-32.png +0 -0
- package/lib/public/wordmark-banded-64.png +0 -0
- package/lib/public/wordmark-banded-80.png +0 -0
- package/lib/scheduler.js +402 -0
- package/lib/sdk-bridge.js +3 -2
- package/lib/server.js +124 -3
- package/lib/sessions.js +35 -2
- 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");
|
|
@@ -120,7 +121,7 @@ for (var i = 0; i < args.length; i++) {
|
|
|
120
121
|
console.log(" --list List all registered projects");
|
|
121
122
|
console.log(" --headless Start daemon and exit immediately (implies --yes)");
|
|
122
123
|
console.log(" --dangerously-skip-permissions");
|
|
123
|
-
console.log(" Bypass all permission prompts
|
|
124
|
+
console.log(" Bypass all permission prompts");
|
|
124
125
|
process.exit(0);
|
|
125
126
|
}
|
|
126
127
|
}
|
|
@@ -1242,7 +1243,30 @@ function setup(callback) {
|
|
|
1242
1243
|
port = p;
|
|
1243
1244
|
log(sym.bar);
|
|
1244
1245
|
|
|
1245
|
-
|
|
1246
|
+
function askPin() {
|
|
1247
|
+
promptPin(function (pin) {
|
|
1248
|
+
if (dangerouslySkipPermissions && !pin) {
|
|
1249
|
+
log(sym.bar);
|
|
1250
|
+
log(sym.warn + " " + a.yellow + "WARNING: No PIN + skip permissions = anyone with the URL" + a.reset);
|
|
1251
|
+
log(sym.bar + " " + a.yellow + "can execute any command without approval." + a.reset);
|
|
1252
|
+
log(sym.bar);
|
|
1253
|
+
promptToggle("Continue without PIN?", null, false, function (confirmed) {
|
|
1254
|
+
if (!confirmed) {
|
|
1255
|
+
clearUp(6);
|
|
1256
|
+
log(sym.done + " PIN protection " + a.dim + "·" + a.reset + " " + a.yellow + "Required for skip permissions" + a.reset);
|
|
1257
|
+
log(sym.bar);
|
|
1258
|
+
askPin();
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
afterPin(pin);
|
|
1262
|
+
});
|
|
1263
|
+
} else {
|
|
1264
|
+
afterPin(pin);
|
|
1265
|
+
}
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
function afterPin(pin) {
|
|
1246
1270
|
if (process.platform === "darwin") {
|
|
1247
1271
|
promptToggle("Keep awake", "Prevent system sleep while relay is running", false, function (keepAwake) {
|
|
1248
1272
|
callback(pin, keepAwake);
|
|
@@ -1250,7 +1274,9 @@ function setup(callback) {
|
|
|
1250
1274
|
} else {
|
|
1251
1275
|
callback(pin, false);
|
|
1252
1276
|
}
|
|
1253
|
-
}
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
askPin();
|
|
1254
1280
|
});
|
|
1255
1281
|
});
|
|
1256
1282
|
}
|
|
@@ -2397,15 +2423,10 @@ var currentVersion = require("../package.json").version;
|
|
|
2397
2423
|
// No daemon running — first-time setup
|
|
2398
2424
|
if (autoYes) {
|
|
2399
2425
|
var pin = cliPin || null;
|
|
2400
|
-
if (dangerouslySkipPermissions && !pin) {
|
|
2401
|
-
console.error(" " + sym.warn + " " + a.red + "--dangerously-skip-permissions requires --pin <pin>" + a.reset);
|
|
2402
|
-
process.exit(1);
|
|
2403
|
-
return;
|
|
2404
|
-
}
|
|
2405
2426
|
console.log(" " + sym.done + " Auto-accepted disclaimer");
|
|
2406
2427
|
console.log(" " + sym.done + " PIN: " + (pin ? "Enabled" : "Skipped"));
|
|
2407
2428
|
if (dangerouslySkipPermissions) {
|
|
2408
|
-
console.log(" " + sym.warn + " " + a.yellow + "Skip permissions mode enabled" + a.reset);
|
|
2429
|
+
console.log(" " + sym.warn + " " + a.yellow + "Skip permissions mode enabled" + (pin ? "" : " (no PIN)") + a.reset);
|
|
2409
2430
|
}
|
|
2410
2431
|
var autoRc = loadClayrc();
|
|
2411
2432
|
var autoRestorable = (autoRc.recentProjects || []).filter(function (p) {
|
|
@@ -2422,13 +2443,6 @@ var currentVersion = require("../package.json").version;
|
|
|
2422
2443
|
await forkDaemon(pin, false, autoRestorable.length > 0 ? autoRestorable : undefined, addCwd);
|
|
2423
2444
|
} else {
|
|
2424
2445
|
setup(function (pin, keepAwake) {
|
|
2425
|
-
if (dangerouslySkipPermissions && !pin) {
|
|
2426
|
-
log(sym.warn + " " + a.red + "--dangerously-skip-permissions requires a PIN." + a.reset);
|
|
2427
|
-
log(a.dim + " Please set a PIN to use skip permissions mode." + a.reset);
|
|
2428
|
-
process.exit(1);
|
|
2429
|
-
return;
|
|
2430
|
-
}
|
|
2431
|
-
|
|
2432
2446
|
// Check ~/.clayrc for previous projects to restore
|
|
2433
2447
|
var rc = loadClayrc();
|
|
2434
2448
|
var restorable = (rc.recentProjects || []).filter(function (p) {
|
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/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
|
|
|
@@ -75,6 +76,10 @@ function createProjectContext(opts) {
|
|
|
75
76
|
var lanHost = opts.lanHost || null;
|
|
76
77
|
var getProjectCount = opts.getProjectCount || function () { return 1; };
|
|
77
78
|
var getProjectList = opts.getProjectList || function () { return []; };
|
|
79
|
+
var getHubSchedules = opts.getHubSchedules || function () { return []; };
|
|
80
|
+
var moveScheduleToProject = opts.moveScheduleToProject || function () { return { ok: false, error: "Not supported" }; };
|
|
81
|
+
var moveAllSchedulesToProject = opts.moveAllSchedulesToProject || function () { return { ok: false, error: "Not supported" }; };
|
|
82
|
+
var getScheduleCount = opts.getScheduleCount || function () { return 0; };
|
|
78
83
|
var onProcessingChanged = opts.onProcessingChanged || function () {};
|
|
79
84
|
var latestVersion = null;
|
|
80
85
|
|
|
@@ -395,17 +400,90 @@ function createProjectContext(opts) {
|
|
|
395
400
|
judgeReady: hasJudge,
|
|
396
401
|
loopJsonReady: hasLoopJson,
|
|
397
402
|
bothReady: hasPrompt && hasJudge,
|
|
403
|
+
taskId: loopState.loopId,
|
|
398
404
|
});
|
|
399
405
|
// Auto-transition to approval phase when both files appear
|
|
400
406
|
if (hasPrompt && hasJudge && loopState.phase === "crafting") {
|
|
401
407
|
loopState.phase = "approval";
|
|
402
408
|
saveLoopState();
|
|
409
|
+
|
|
410
|
+
// Parse recommended title from crafting session conversation
|
|
411
|
+
if (loopState.craftingSessionId && loopState.loopId) {
|
|
412
|
+
var craftSess = sm.sessions.get(loopState.craftingSessionId);
|
|
413
|
+
if (craftSess && craftSess.history) {
|
|
414
|
+
for (var hi = craftSess.history.length - 1; hi >= 0; hi--) {
|
|
415
|
+
var entry = craftSess.history[hi];
|
|
416
|
+
var entryText = entry.text || "";
|
|
417
|
+
var titleMatch = entryText.match(/\[\[LOOP_TITLE:\s*(.+?)\]\]/);
|
|
418
|
+
if (titleMatch) {
|
|
419
|
+
var suggestedTitle = titleMatch[1].trim();
|
|
420
|
+
if (suggestedTitle) {
|
|
421
|
+
loopRegistry.updateRecord(loopState.loopId, { name: suggestedTitle });
|
|
422
|
+
}
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
403
428
|
}
|
|
404
429
|
}
|
|
405
430
|
|
|
406
431
|
// Load persisted state on startup
|
|
407
432
|
loadLoopState();
|
|
408
433
|
|
|
434
|
+
// --- Loop Registry (unified one-off + scheduled) ---
|
|
435
|
+
var activeRegistryId = null; // track which registry record triggered current loop
|
|
436
|
+
|
|
437
|
+
var loopRegistry = createLoopRegistry({
|
|
438
|
+
cwd: cwd,
|
|
439
|
+
onTrigger: function (record) {
|
|
440
|
+
// Only trigger if no loop is currently active
|
|
441
|
+
if (loopState.active || loopState.phase === "executing") {
|
|
442
|
+
console.log("[loop-registry] Skipping trigger — loop already active");
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// For schedule records, resolve the linked task to get loop files
|
|
447
|
+
var loopId = record.id;
|
|
448
|
+
if (record.source === "schedule") {
|
|
449
|
+
if (!record.linkedTaskId) {
|
|
450
|
+
console.error("[loop-registry] Schedule has no linked task: " + record.name);
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
loopId = record.linkedTaskId;
|
|
454
|
+
console.log("[loop-registry] Schedule triggered: " + record.name + " → linked task " + loopId);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Verify the loop directory and files exist
|
|
458
|
+
var recDir = path.join(cwd, ".claude", "loops", loopId);
|
|
459
|
+
try {
|
|
460
|
+
fs.accessSync(path.join(recDir, "PROMPT.md"));
|
|
461
|
+
fs.accessSync(path.join(recDir, "JUDGE.md"));
|
|
462
|
+
} catch (e) {
|
|
463
|
+
console.error("[loop-registry] Loop files missing for " + loopId);
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
// Set the loopId and start
|
|
467
|
+
loopState.loopId = loopId;
|
|
468
|
+
activeRegistryId = record.id;
|
|
469
|
+
console.log("[loop-registry] Auto-starting loop: " + record.name + " (" + loopId + ")");
|
|
470
|
+
send({ type: "schedule_run_started", recordId: record.id });
|
|
471
|
+
startLoop();
|
|
472
|
+
},
|
|
473
|
+
onChange: function () {
|
|
474
|
+
send({ type: "loop_registry_updated", records: getHubSchedules() });
|
|
475
|
+
},
|
|
476
|
+
});
|
|
477
|
+
loopRegistry.load();
|
|
478
|
+
loopRegistry.startTimer();
|
|
479
|
+
|
|
480
|
+
// Wire loop info resolution for session list broadcasts
|
|
481
|
+
sm.setResolveLoopInfo(function (loopId) {
|
|
482
|
+
var rec = loopRegistry.getById(loopId);
|
|
483
|
+
if (!rec) return null;
|
|
484
|
+
return { name: rec.name || null, source: rec.source || null };
|
|
485
|
+
});
|
|
486
|
+
|
|
409
487
|
function startLoop(opts) {
|
|
410
488
|
var loopOpts = opts || {};
|
|
411
489
|
var dir = loopDir();
|
|
@@ -479,9 +557,12 @@ function createProjectContext(opts) {
|
|
|
479
557
|
}
|
|
480
558
|
|
|
481
559
|
var session = sm.createSession();
|
|
482
|
-
session.loop = { active: true, iteration: loopState.iteration, role: "coder" };
|
|
483
560
|
var loopName = (loopState.wizardData && loopState.wizardData.name) || "";
|
|
484
|
-
|
|
561
|
+
var loopSource = loopRegistry.getById(loopState.loopId);
|
|
562
|
+
var loopSourceTag = (loopSource && loopSource.source) || null;
|
|
563
|
+
var isRalphLoop = loopSourceTag === "ralph";
|
|
564
|
+
session.loop = { active: true, iteration: loopState.iteration, role: "coder", loopId: loopState.loopId, name: loopName, source: loopSourceTag, startedAt: loopState.startedAt };
|
|
565
|
+
session.title = (isRalphLoop ? "Ralph" : "Task") + (loopName ? " " + loopName : "") + " #" + loopState.iteration;
|
|
485
566
|
sm.saveSessionFile(session);
|
|
486
567
|
sm.broadcastSessionList();
|
|
487
568
|
|
|
@@ -566,9 +647,12 @@ function createProjectContext(opts) {
|
|
|
566
647
|
"Do NOT use any tools. Just analyze and respond.";
|
|
567
648
|
|
|
568
649
|
var judgeSession = sm.createSession();
|
|
569
|
-
judgeSession.loop = { active: true, iteration: loopState.iteration, role: "judge" };
|
|
570
650
|
var judgeName = (loopState.wizardData && loopState.wizardData.name) || "";
|
|
571
|
-
|
|
651
|
+
var judgeSource = loopRegistry.getById(loopState.loopId);
|
|
652
|
+
var judgeSourceTag = (judgeSource && judgeSource.source) || null;
|
|
653
|
+
var isRalphJudge = judgeSourceTag === "ralph";
|
|
654
|
+
judgeSession.loop = { active: true, iteration: loopState.iteration, role: "judge", loopId: loopState.loopId, name: judgeName, source: judgeSourceTag, startedAt: loopState.startedAt };
|
|
655
|
+
judgeSession.title = (isRalphJudge ? "Ralph" : "Task") + (judgeName ? " " + judgeName : "") + " Judge #" + loopState.iteration;
|
|
572
656
|
sm.saveSessionFile(judgeSession);
|
|
573
657
|
sm.broadcastSessionList();
|
|
574
658
|
loopState.judgeSessionId = judgeSession.localId;
|
|
@@ -654,6 +738,19 @@ function createProjectContext(opts) {
|
|
|
654
738
|
results: loopState.results,
|
|
655
739
|
});
|
|
656
740
|
|
|
741
|
+
// Record result in loop registry
|
|
742
|
+
if (loopState.loopId) {
|
|
743
|
+
loopRegistry.recordRun(loopState.loopId, {
|
|
744
|
+
reason: reason,
|
|
745
|
+
startedAt: loopState.startedAt,
|
|
746
|
+
iterations: loopState.iteration,
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
if (activeRegistryId) {
|
|
750
|
+
send({ type: "schedule_run_finished", recordId: activeRegistryId, reason: reason, iterations: loopState.iteration });
|
|
751
|
+
activeRegistryId = null;
|
|
752
|
+
}
|
|
753
|
+
|
|
657
754
|
if (pushModule) {
|
|
658
755
|
var body = reason === "pass"
|
|
659
756
|
? "Task completed after " + loopState.iteration + " iteration(s)"
|
|
@@ -773,6 +870,7 @@ function createProjectContext(opts) {
|
|
|
773
870
|
sendTo(ws, { type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [] });
|
|
774
871
|
sendTo(ws, { type: "term_list", terminals: tm.list() });
|
|
775
872
|
sendTo(ws, { type: "notes_list", notes: nm.list() });
|
|
873
|
+
sendTo(ws, { type: "loop_registry_updated", records: getHubSchedules() });
|
|
776
874
|
|
|
777
875
|
// Ralph Loop availability
|
|
778
876
|
var hasLoopFiles = false;
|
|
@@ -809,13 +907,22 @@ function createProjectContext(opts) {
|
|
|
809
907
|
promptReady: _hasPrompt,
|
|
810
908
|
judgeReady: _hasJudge,
|
|
811
909
|
bothReady: _hasPrompt && _hasJudge,
|
|
910
|
+
taskId: loopState.loopId,
|
|
812
911
|
});
|
|
813
912
|
}
|
|
814
913
|
|
|
815
914
|
// Session list
|
|
816
915
|
sendTo(ws, {
|
|
817
916
|
type: "session_list",
|
|
818
|
-
sessions: [].concat(Array.from(sm.sessions.values())).map(function (s) {
|
|
917
|
+
sessions: [].concat(Array.from(sm.sessions.values())).filter(function (s) { return !s.hidden; }).map(function (s) {
|
|
918
|
+
var loop = s.loop ? Object.assign({}, s.loop) : null;
|
|
919
|
+
if (loop && loop.loopId && loopRegistry) {
|
|
920
|
+
var rec = loopRegistry.getById(loop.loopId);
|
|
921
|
+
if (rec) {
|
|
922
|
+
if (rec.name) loop.name = rec.name;
|
|
923
|
+
if (rec.source) loop.source = rec.source;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
819
926
|
return {
|
|
820
927
|
id: s.localId,
|
|
821
928
|
cliSessionId: s.cliSessionId || null,
|
|
@@ -823,6 +930,7 @@ function createProjectContext(opts) {
|
|
|
823
930
|
active: s.localId === sm.activeSessionId,
|
|
824
931
|
isProcessing: s.isProcessing,
|
|
825
932
|
lastActivity: s.lastActivity || s.createdAt || 0,
|
|
933
|
+
loop: loop,
|
|
826
934
|
};
|
|
827
935
|
}),
|
|
828
936
|
});
|
|
@@ -830,7 +938,7 @@ function createProjectContext(opts) {
|
|
|
830
938
|
// Restore active session for this client
|
|
831
939
|
var active = sm.getActiveSession();
|
|
832
940
|
if (active) {
|
|
833
|
-
sendTo(ws, { type: "session_switched", id: active.localId, cliSessionId: active.cliSessionId || null });
|
|
941
|
+
sendTo(ws, { type: "session_switched", id: active.localId, cliSessionId: active.cliSessionId || null, loop: active.loop || null });
|
|
834
942
|
|
|
835
943
|
var total = active.history.length;
|
|
836
944
|
var fromIndex = 0;
|
|
@@ -1475,6 +1583,18 @@ function createProjectContext(opts) {
|
|
|
1475
1583
|
return;
|
|
1476
1584
|
}
|
|
1477
1585
|
|
|
1586
|
+
// --- Pre-check: does the project have tasks/schedules? ---
|
|
1587
|
+
if (msg.type === "remove_project_check") {
|
|
1588
|
+
var checkSlug = msg.slug;
|
|
1589
|
+
if (!checkSlug) {
|
|
1590
|
+
sendTo(ws, { type: "remove_project_check_result", slug: checkSlug, name: msg.name || checkSlug, count: 0 });
|
|
1591
|
+
return;
|
|
1592
|
+
}
|
|
1593
|
+
var schedCount = getScheduleCount(checkSlug);
|
|
1594
|
+
sendTo(ws, { type: "remove_project_check_result", slug: checkSlug, name: msg.name || checkSlug, count: schedCount });
|
|
1595
|
+
return;
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1478
1598
|
// --- Remove project from web UI ---
|
|
1479
1599
|
if (msg.type === "remove_project") {
|
|
1480
1600
|
var removeSlug = msg.slug;
|
|
@@ -1482,6 +1602,10 @@ function createProjectContext(opts) {
|
|
|
1482
1602
|
sendTo(ws, { type: "remove_project_result", ok: false, error: "Missing slug" });
|
|
1483
1603
|
return;
|
|
1484
1604
|
}
|
|
1605
|
+
// If client chose to move tasks to another project before removing
|
|
1606
|
+
if (msg.moveTasksTo) {
|
|
1607
|
+
moveAllSchedulesToProject(removeSlug, msg.moveTasksTo);
|
|
1608
|
+
}
|
|
1485
1609
|
if (typeof opts.onRemoveProject === "function") {
|
|
1486
1610
|
var removeResult = opts.onRemoveProject(removeSlug);
|
|
1487
1611
|
sendTo(ws, { type: "remove_project_result", ok: removeResult.ok, slug: removeSlug, error: removeResult.error });
|
|
@@ -1491,6 +1615,17 @@ function createProjectContext(opts) {
|
|
|
1491
1615
|
return;
|
|
1492
1616
|
}
|
|
1493
1617
|
|
|
1618
|
+
// --- Move a single schedule to another project ---
|
|
1619
|
+
if (msg.type === "schedule_move") {
|
|
1620
|
+
var moveResult = moveScheduleToProject(msg.recordId, msg.fromSlug, msg.toSlug);
|
|
1621
|
+
if (moveResult.ok) {
|
|
1622
|
+
// Re-broadcast updated records to this project's clients
|
|
1623
|
+
send({ type: "loop_registry_updated", records: getHubSchedules() });
|
|
1624
|
+
}
|
|
1625
|
+
sendTo(ws, { type: "schedule_move_result", ok: moveResult.ok, error: moveResult.error });
|
|
1626
|
+
return;
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1494
1629
|
// --- Reorder projects ---
|
|
1495
1630
|
if (msg.type === "reorder_projects") {
|
|
1496
1631
|
var slugs = msg.slugs;
|
|
@@ -2019,6 +2154,16 @@ function createProjectContext(opts) {
|
|
|
2019
2154
|
}
|
|
2020
2155
|
|
|
2021
2156
|
if (msg.type === "loop_start") {
|
|
2157
|
+
// If this loop has a cron schedule, don't run immediately — just confirm registration
|
|
2158
|
+
if (loopState.wizardData && loopState.wizardData.cron) {
|
|
2159
|
+
loopState.active = false;
|
|
2160
|
+
loopState.phase = "done";
|
|
2161
|
+
saveLoopState();
|
|
2162
|
+
send({ type: "loop_finished", reason: "scheduled", iterations: 0, results: [] });
|
|
2163
|
+
send({ type: "ralph_phase", phase: "idle", wizardData: null });
|
|
2164
|
+
send({ type: "loop_scheduled", recordId: loopState.loopId, cron: loopState.wizardData.cron });
|
|
2165
|
+
return;
|
|
2166
|
+
}
|
|
2022
2167
|
startLoop();
|
|
2023
2168
|
return;
|
|
2024
2169
|
}
|
|
@@ -2030,18 +2175,32 @@ function createProjectContext(opts) {
|
|
|
2030
2175
|
|
|
2031
2176
|
if (msg.type === "ralph_wizard_complete") {
|
|
2032
2177
|
var wData = msg.data || {};
|
|
2033
|
-
var maxIter = wData.maxIterations ||
|
|
2178
|
+
var maxIter = wData.maxIterations || 3;
|
|
2179
|
+
var wizardCron = wData.cron || null;
|
|
2034
2180
|
var newLoopId = generateLoopId();
|
|
2035
2181
|
loopState.loopId = newLoopId;
|
|
2036
2182
|
loopState.wizardData = {
|
|
2037
|
-
name:
|
|
2183
|
+
name: wData.name || wData.task || "Untitled",
|
|
2038
2184
|
task: wData.task || "",
|
|
2039
2185
|
maxIterations: maxIter,
|
|
2186
|
+
cron: wizardCron,
|
|
2040
2187
|
};
|
|
2041
2188
|
loopState.phase = "crafting";
|
|
2042
2189
|
loopState.startedAt = Date.now();
|
|
2043
2190
|
saveLoopState();
|
|
2044
2191
|
|
|
2192
|
+
// Register in loop registry
|
|
2193
|
+
var recordSource = wData.source === "task" ? null : "ralph";
|
|
2194
|
+
loopRegistry.register({
|
|
2195
|
+
id: newLoopId,
|
|
2196
|
+
name: loopState.wizardData.name,
|
|
2197
|
+
task: wData.task || "",
|
|
2198
|
+
cron: wizardCron,
|
|
2199
|
+
enabled: wizardCron ? true : false,
|
|
2200
|
+
maxIterations: maxIter,
|
|
2201
|
+
source: recordSource,
|
|
2202
|
+
});
|
|
2203
|
+
|
|
2045
2204
|
// Create loop directory and write LOOP.json
|
|
2046
2205
|
var lDir = loopDir();
|
|
2047
2206
|
try { fs.mkdirSync(lDir, { recursive: true }); } catch (e) {}
|
|
@@ -2051,23 +2210,31 @@ function createProjectContext(opts) {
|
|
|
2051
2210
|
fs.renameSync(tmpLoopJson, loopJsonPath);
|
|
2052
2211
|
|
|
2053
2212
|
// Assemble prompt for clay-ralph skill (include loop dir path so skill knows where to write)
|
|
2054
|
-
var craftingPrompt = "/clay-ralph
|
|
2055
|
-
"
|
|
2213
|
+
var craftingPrompt = "Use the /clay-ralph skill to design a Ralph Loop for the following task. " +
|
|
2214
|
+
"You MUST invoke the clay-ralph skill — do NOT execute the task yourself. " +
|
|
2215
|
+
"Your job is to interview me, then create PROMPT.md and JUDGE.md files " +
|
|
2216
|
+
"that a future autonomous session will execute.\n\n" +
|
|
2217
|
+
"## Task\n" + (wData.task || "") +
|
|
2218
|
+
"\n\n## Loop Directory\n" + lDir;
|
|
2056
2219
|
|
|
2057
2220
|
// Create a new session for crafting
|
|
2058
2221
|
var craftingSession = sm.createSession();
|
|
2059
2222
|
var craftName = (loopState.wizardData && loopState.wizardData.name) || "";
|
|
2060
|
-
|
|
2223
|
+
var isRalphCraft = recordSource === "ralph";
|
|
2224
|
+
craftingSession.title = (isRalphCraft ? "Ralph" : "Task") + (craftName ? " " + craftName : "") + " Crafting";
|
|
2061
2225
|
craftingSession.ralphCraftingMode = true;
|
|
2226
|
+
craftingSession.loop = { active: true, iteration: 0, role: "crafting", loopId: newLoopId, name: craftName, source: recordSource };
|
|
2062
2227
|
sm.saveSessionFile(craftingSession);
|
|
2063
2228
|
sm.switchSession(craftingSession.localId);
|
|
2064
|
-
sm.broadcastSessionList();
|
|
2065
2229
|
loopState.craftingSessionId = craftingSession.localId;
|
|
2066
2230
|
|
|
2231
|
+
// Store crafting session ID in the registry record
|
|
2232
|
+
loopRegistry.updateRecord(newLoopId, { craftingSessionId: craftingSession.localId });
|
|
2233
|
+
|
|
2067
2234
|
// Start .claude/ directory watcher
|
|
2068
2235
|
startClaudeDirWatch();
|
|
2069
2236
|
|
|
2070
|
-
//
|
|
2237
|
+
// Send crafting prompt and start the conversation with Claude.
|
|
2071
2238
|
craftingSession.history.push({ type: "user_message", text: craftingPrompt });
|
|
2072
2239
|
sm.appendToSessionFile(craftingSession, { type: "user_message", text: craftingPrompt });
|
|
2073
2240
|
send({ type: "user_message", text: craftingPrompt });
|
|
@@ -2077,11 +2244,27 @@ function createProjectContext(opts) {
|
|
|
2077
2244
|
send({ type: "status", status: "processing" });
|
|
2078
2245
|
sdk.startQuery(craftingSession, craftingPrompt);
|
|
2079
2246
|
|
|
2080
|
-
send({ type: "ralph_crafting_started", sessionId: craftingSession.localId });
|
|
2247
|
+
send({ type: "ralph_crafting_started", sessionId: craftingSession.localId, taskId: newLoopId, source: recordSource });
|
|
2081
2248
|
send({ type: "ralph_phase", phase: "crafting", wizardData: loopState.wizardData, craftingSessionId: craftingSession.localId });
|
|
2082
2249
|
return;
|
|
2083
2250
|
}
|
|
2084
2251
|
|
|
2252
|
+
if (msg.type === "loop_registry_files") {
|
|
2253
|
+
var recId = msg.id;
|
|
2254
|
+
var lDir = path.join(cwd, ".claude", "loops", recId);
|
|
2255
|
+
var promptContent = "";
|
|
2256
|
+
var judgeContent = "";
|
|
2257
|
+
try { promptContent = fs.readFileSync(path.join(lDir, "PROMPT.md"), "utf8"); } catch (e) {}
|
|
2258
|
+
try { judgeContent = fs.readFileSync(path.join(lDir, "JUDGE.md"), "utf8"); } catch (e) {}
|
|
2259
|
+
send({
|
|
2260
|
+
type: "loop_registry_files_content",
|
|
2261
|
+
id: recId,
|
|
2262
|
+
prompt: promptContent,
|
|
2263
|
+
judge: judgeContent,
|
|
2264
|
+
});
|
|
2265
|
+
return;
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2085
2268
|
if (msg.type === "ralph_preview_files") {
|
|
2086
2269
|
var promptContent = "";
|
|
2087
2270
|
var judgeContent = "";
|
|
@@ -2129,6 +2312,122 @@ function createProjectContext(opts) {
|
|
|
2129
2312
|
return;
|
|
2130
2313
|
}
|
|
2131
2314
|
|
|
2315
|
+
// --- Schedule create (from calendar click) ---
|
|
2316
|
+
if (msg.type === "schedule_create") {
|
|
2317
|
+
var sData = msg.data || {};
|
|
2318
|
+
var newRec = loopRegistry.register({
|
|
2319
|
+
name: sData.name || "Untitled",
|
|
2320
|
+
task: sData.name || "",
|
|
2321
|
+
description: sData.description || "",
|
|
2322
|
+
date: sData.date || null,
|
|
2323
|
+
time: sData.time || null,
|
|
2324
|
+
allDay: sData.allDay !== undefined ? sData.allDay : true,
|
|
2325
|
+
linkedTaskId: sData.taskId || null,
|
|
2326
|
+
cron: sData.cron || null,
|
|
2327
|
+
enabled: sData.cron ? (sData.enabled !== false) : false,
|
|
2328
|
+
maxIterations: sData.maxIterations || 3,
|
|
2329
|
+
source: "schedule",
|
|
2330
|
+
color: sData.color || null,
|
|
2331
|
+
recurrenceEnd: sData.recurrenceEnd || null,
|
|
2332
|
+
});
|
|
2333
|
+
return;
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
// --- Hub: cross-project schedule aggregation ---
|
|
2337
|
+
if (msg.type === "hub_schedules_list") {
|
|
2338
|
+
sendTo(ws, { type: "hub_schedules", schedules: getHubSchedules() });
|
|
2339
|
+
return;
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
// --- Loop Registry messages ---
|
|
2343
|
+
if (msg.type === "loop_registry_list") {
|
|
2344
|
+
sendTo(ws, { type: "loop_registry_updated", records: getHubSchedules() });
|
|
2345
|
+
return;
|
|
2346
|
+
}
|
|
2347
|
+
|
|
2348
|
+
if (msg.type === "loop_registry_update") {
|
|
2349
|
+
var updatedRec = loopRegistry.update(msg.id, msg.data || {});
|
|
2350
|
+
if (!updatedRec) {
|
|
2351
|
+
sendTo(ws, { type: "loop_registry_error", text: "Record not found" });
|
|
2352
|
+
}
|
|
2353
|
+
return;
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
if (msg.type === "loop_registry_rename") {
|
|
2357
|
+
if (msg.id && msg.name) {
|
|
2358
|
+
loopRegistry.updateRecord(msg.id, { name: String(msg.name).substring(0, 100) });
|
|
2359
|
+
sm.broadcastSessionList();
|
|
2360
|
+
}
|
|
2361
|
+
return;
|
|
2362
|
+
}
|
|
2363
|
+
|
|
2364
|
+
if (msg.type === "loop_registry_remove") {
|
|
2365
|
+
var removedRec = loopRegistry.remove(msg.id);
|
|
2366
|
+
if (!removedRec) {
|
|
2367
|
+
sendTo(ws, { type: "loop_registry_error", text: "Record not found" });
|
|
2368
|
+
}
|
|
2369
|
+
return;
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
if (msg.type === "loop_registry_convert") {
|
|
2373
|
+
// Convert ralph source to regular task (remove source tag)
|
|
2374
|
+
if (msg.id) {
|
|
2375
|
+
loopRegistry.updateRecord(msg.id, { source: null });
|
|
2376
|
+
sm.broadcastSessionList();
|
|
2377
|
+
}
|
|
2378
|
+
return;
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
if (msg.type === "delete_loop_group") {
|
|
2382
|
+
// Delete all sessions belonging to this loopId, then remove registry record
|
|
2383
|
+
var loopIdToDel = msg.loopId;
|
|
2384
|
+
if (!loopIdToDel) return;
|
|
2385
|
+
var sessionIds = [];
|
|
2386
|
+
sm.sessions.forEach(function (s, lid) {
|
|
2387
|
+
if (s.loop && s.loop.loopId === loopIdToDel) sessionIds.push(lid);
|
|
2388
|
+
});
|
|
2389
|
+
for (var di = 0; di < sessionIds.length; di++) {
|
|
2390
|
+
sm.deleteSessionQuiet(sessionIds[di]);
|
|
2391
|
+
}
|
|
2392
|
+
loopRegistry.remove(loopIdToDel);
|
|
2393
|
+
sm.broadcastSessionList();
|
|
2394
|
+
return;
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
if (msg.type === "loop_registry_toggle") {
|
|
2398
|
+
var toggledRec = loopRegistry.toggleEnabled(msg.id);
|
|
2399
|
+
if (!toggledRec) {
|
|
2400
|
+
sendTo(ws, { type: "loop_registry_error", text: "Record not found or not scheduled" });
|
|
2401
|
+
}
|
|
2402
|
+
return;
|
|
2403
|
+
}
|
|
2404
|
+
|
|
2405
|
+
if (msg.type === "loop_registry_rerun") {
|
|
2406
|
+
// Re-run an existing job (one-off from library)
|
|
2407
|
+
if (loopState.active || loopState.phase === "executing") {
|
|
2408
|
+
sendTo(ws, { type: "loop_registry_error", text: "A loop is already running" });
|
|
2409
|
+
return;
|
|
2410
|
+
}
|
|
2411
|
+
var rerunRec = loopRegistry.getById(msg.id);
|
|
2412
|
+
if (!rerunRec) {
|
|
2413
|
+
sendTo(ws, { type: "loop_registry_error", text: "Record not found" });
|
|
2414
|
+
return;
|
|
2415
|
+
}
|
|
2416
|
+
var rerunDir = path.join(cwd, ".claude", "loops", rerunRec.id);
|
|
2417
|
+
try {
|
|
2418
|
+
fs.accessSync(path.join(rerunDir, "PROMPT.md"));
|
|
2419
|
+
fs.accessSync(path.join(rerunDir, "JUDGE.md"));
|
|
2420
|
+
} catch (e) {
|
|
2421
|
+
sendTo(ws, { type: "loop_registry_error", text: "Loop files missing for " + rerunRec.id });
|
|
2422
|
+
return;
|
|
2423
|
+
}
|
|
2424
|
+
loopState.loopId = rerunRec.id;
|
|
2425
|
+
activeRegistryId = null; // not a scheduled trigger
|
|
2426
|
+
send({ type: "loop_rerun_started", recordId: rerunRec.id });
|
|
2427
|
+
startLoop();
|
|
2428
|
+
return;
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2132
2431
|
if (msg.type !== "message") return;
|
|
2133
2432
|
if (!msg.text && (!msg.images || msg.images.length === 0) && (!msg.pastes || msg.pastes.length === 0)) return;
|
|
2134
2433
|
|
|
@@ -2491,6 +2790,7 @@ function createProjectContext(opts) {
|
|
|
2491
2790
|
|
|
2492
2791
|
// --- Destroy ---
|
|
2493
2792
|
function destroy() {
|
|
2793
|
+
loopRegistry.stopTimer();
|
|
2494
2794
|
stopFileWatch();
|
|
2495
2795
|
stopAllDirWatches();
|
|
2496
2796
|
// Abort all active sessions
|
|
@@ -2558,9 +2858,37 @@ function createProjectContext(opts) {
|
|
|
2558
2858
|
handleDisconnection: handleDisconnection,
|
|
2559
2859
|
handleHTTP: handleHTTP,
|
|
2560
2860
|
getStatus: getStatus,
|
|
2861
|
+
getSchedules: function () { return loopRegistry.getAll(); },
|
|
2862
|
+
importSchedule: function (data) { return loopRegistry.register(data); },
|
|
2863
|
+
removeSchedule: function (id) { return loopRegistry.remove(id); },
|
|
2561
2864
|
setTitle: setTitle,
|
|
2562
2865
|
setIcon: setIcon,
|
|
2563
|
-
warmup: function () {
|
|
2866
|
+
warmup: function () {
|
|
2867
|
+
sdk.warmup();
|
|
2868
|
+
// Auto-install clay-ralph skill globally if not present
|
|
2869
|
+
var clayRalphDir = path.join(os.homedir(), ".claude", "skills", "clay-ralph", "SKILL.md");
|
|
2870
|
+
try {
|
|
2871
|
+
fs.accessSync(clayRalphDir, fs.constants.R_OK);
|
|
2872
|
+
} catch (e) {
|
|
2873
|
+
console.log("[project] Auto-installing clay-ralph skill...");
|
|
2874
|
+
var child = spawn("npx", ["skills", "add", "https://github.com/chadbyte/clay-ralph", "--skill", "clay-ralph"], {
|
|
2875
|
+
cwd: os.homedir(),
|
|
2876
|
+
stdio: "ignore",
|
|
2877
|
+
detached: false,
|
|
2878
|
+
});
|
|
2879
|
+
child.on("close", function (code) {
|
|
2880
|
+
if (code === 0) {
|
|
2881
|
+
console.log("[project] clay-ralph skill installed successfully");
|
|
2882
|
+
send({ type: "skill_installed", skill: "clay-ralph", scope: "global", success: true, error: null });
|
|
2883
|
+
} else {
|
|
2884
|
+
console.log("[project] clay-ralph skill install failed (code " + code + ")");
|
|
2885
|
+
}
|
|
2886
|
+
});
|
|
2887
|
+
child.on("error", function (err) {
|
|
2888
|
+
console.log("[project] clay-ralph skill install error: " + err.message);
|
|
2889
|
+
});
|
|
2890
|
+
}
|
|
2891
|
+
},
|
|
2564
2892
|
destroy: destroy,
|
|
2565
2893
|
};
|
|
2566
2894
|
}
|