clay-server 2.13.0-beta.3 → 2.13.0-beta.5
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 +46 -0
- package/lib/daemon.js +16 -3
- package/lib/notes.js +1 -1
- package/lib/project.js +290 -16
- package/lib/public/app.js +266 -36
- package/lib/public/css/admin.css +71 -0
- package/lib/public/css/command-palette.css +319 -0
- package/lib/public/css/icon-strip.css +19 -3
- package/lib/public/css/input.css +2 -1
- package/lib/public/css/loop.css +26 -0
- package/lib/public/css/mates.css +73 -0
- package/lib/public/css/overlays.css +3 -0
- package/lib/public/css/scheduler.css +29 -5
- package/lib/public/css/sidebar.css +28 -6
- package/lib/public/css/sticky-notes.css +61 -12
- package/lib/public/css/title-bar.css +24 -30
- package/lib/public/index.html +48 -13
- package/lib/public/modules/admin.js +109 -1
- package/lib/public/modules/command-palette.js +549 -0
- package/lib/public/modules/input.js +10 -2
- package/lib/public/modules/mate-wizard.js +17 -6
- package/lib/public/modules/scheduler.js +56 -64
- package/lib/public/modules/session-search.js +6 -2
- package/lib/public/modules/sidebar.js +128 -72
- package/lib/public/modules/sticky-notes.js +37 -0
- package/lib/public/modules/tools.js +8 -14
- package/lib/public/style.css +1 -0
- package/lib/scheduler.js +4 -0
- package/lib/sdk-bridge.js +10 -8
- package/lib/server.js +334 -7
- package/lib/sessions.js +5 -0
- package/lib/users.js +64 -0
- package/lib/worktree.js +2 -2
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -32,6 +32,7 @@ if (_isDev || process.argv.includes("--debug")) {
|
|
|
32
32
|
console.clear = function() {};
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
var crypto = require("crypto");
|
|
35
36
|
var { loadConfig, saveConfig, configPath, socketPath, logPath, ensureConfigDir, isDaemonAlive, isDaemonAliveAsync, generateSlug, clearStaleConfig, loadClayrc, saveClayrc, readCrashInfo } = require("../lib/config");
|
|
36
37
|
var { sendIPCCommand } = require("../lib/ipc");
|
|
37
38
|
var { generateAuthToken } = require("../lib/server");
|
|
@@ -2450,6 +2451,9 @@ function showSettingsMenu(config, ip) {
|
|
|
2450
2451
|
} else {
|
|
2451
2452
|
items.push({ label: "Enable multi-user mode", value: "multi_user" });
|
|
2452
2453
|
}
|
|
2454
|
+
if (muEnabled && hasAdmin()) {
|
|
2455
|
+
items.push({ label: "Recover admin password", value: "recover_admin" });
|
|
2456
|
+
}
|
|
2453
2457
|
if (process.platform === "darwin") {
|
|
2454
2458
|
items.push({ label: isAwake ? "Disable keep awake" : "Enable keep awake", value: "awake" });
|
|
2455
2459
|
}
|
|
@@ -2744,6 +2748,48 @@ function showSettingsMenu(config, ip) {
|
|
|
2744
2748
|
});
|
|
2745
2749
|
break;
|
|
2746
2750
|
|
|
2751
|
+
case "recover_admin": {
|
|
2752
|
+
var recoveryUrlPath = crypto.randomBytes(16).toString("hex");
|
|
2753
|
+
var recoveryPassword = crypto.randomBytes(8).toString("base64url");
|
|
2754
|
+
sendIPCCommand(socketPath(), { cmd: "enable_recovery", urlPath: recoveryUrlPath, password: recoveryPassword }).then(function (res) {
|
|
2755
|
+
if (!res.ok) {
|
|
2756
|
+
log(sym.bar + " " + a.red + "Failed to enable recovery mode." + a.reset);
|
|
2757
|
+
log(sym.bar);
|
|
2758
|
+
showSettingsMenu(config, ip);
|
|
2759
|
+
return;
|
|
2760
|
+
}
|
|
2761
|
+
var protocol = config.tls ? "https" : "http";
|
|
2762
|
+
var recoveryUrl = protocol + "://" + ip + ":" + config.port + "/recover/" + recoveryUrlPath;
|
|
2763
|
+
log(sym.bar);
|
|
2764
|
+
log(sym.bar + " " + a.yellow + sym.warn + " Admin Password Recovery" + a.reset);
|
|
2765
|
+
log(sym.bar);
|
|
2766
|
+
log(sym.bar + " " + a.dim + "Recovery URL:" + a.reset);
|
|
2767
|
+
log(sym.bar + " " + a.bold + recoveryUrl + a.reset);
|
|
2768
|
+
log(sym.bar);
|
|
2769
|
+
log(sym.bar + " " + a.dim + "Recovery password:" + a.reset);
|
|
2770
|
+
log(sym.bar + " " + a.bold + recoveryPassword + a.reset);
|
|
2771
|
+
log(sym.bar);
|
|
2772
|
+
log(sym.bar + " " + a.dim + "Open the URL in a browser and enter the password above." + a.reset);
|
|
2773
|
+
log(sym.bar + " " + a.dim + "This link is single-use and will expire when the PIN is reset." + a.reset);
|
|
2774
|
+
log(sym.bar);
|
|
2775
|
+
promptSelect("Done?", [
|
|
2776
|
+
{ label: "Disable recovery link", value: "disable" },
|
|
2777
|
+
{ label: "Back (keep link active)", value: "back" },
|
|
2778
|
+
], function (rc) {
|
|
2779
|
+
if (rc === "disable") {
|
|
2780
|
+
sendIPCCommand(socketPath(), { cmd: "disable_recovery" }).then(function () {
|
|
2781
|
+
log(sym.done + " " + a.dim + "Recovery link disabled." + a.reset);
|
|
2782
|
+
log("");
|
|
2783
|
+
showSettingsMenu(config, ip);
|
|
2784
|
+
});
|
|
2785
|
+
} else {
|
|
2786
|
+
showSettingsMenu(config, ip);
|
|
2787
|
+
}
|
|
2788
|
+
});
|
|
2789
|
+
});
|
|
2790
|
+
break;
|
|
2791
|
+
}
|
|
2792
|
+
|
|
2747
2793
|
case "back":
|
|
2748
2794
|
showMainMenu(config, ip);
|
|
2749
2795
|
break;
|
package/lib/daemon.js
CHANGED
|
@@ -793,7 +793,7 @@ var relay = createServer({
|
|
|
793
793
|
if (!config.osUsers || !linuxUser) return;
|
|
794
794
|
deactivateLinuxUser(linuxUser);
|
|
795
795
|
},
|
|
796
|
-
onCreateWorktree: function (parentSlug, branchName, baseBranch) {
|
|
796
|
+
onCreateWorktree: function (parentSlug, branchName, dirName, baseBranch) {
|
|
797
797
|
// Find the parent project
|
|
798
798
|
var parent = null;
|
|
799
799
|
for (var j = 0; j < config.projects.length; j++) {
|
|
@@ -801,10 +801,10 @@ var relay = createServer({
|
|
|
801
801
|
}
|
|
802
802
|
if (!parent) return { ok: false, error: "Parent project not found" };
|
|
803
803
|
if (isWorktree(parent.path)) return { ok: false, error: "Cannot create worktrees from a worktree project" };
|
|
804
|
-
var result = createWorktree(parent.path, branchName, baseBranch);
|
|
804
|
+
var result = createWorktree(parent.path, branchName, dirName, baseBranch);
|
|
805
805
|
if (!result.ok) return result;
|
|
806
806
|
// Register the new worktree as ephemeral project
|
|
807
|
-
var wtSlug = parentSlug + "--" +
|
|
807
|
+
var wtSlug = parentSlug + "--" + dirName;
|
|
808
808
|
var wtMeta = { parentSlug: parentSlug, branch: branchName, accessible: true };
|
|
809
809
|
relay.addProject(result.path, wtSlug, branchName, parent.icon, parent.ownerId, wtMeta);
|
|
810
810
|
if (!worktreeRegistry[parentSlug]) worktreeRegistry[parentSlug] = [];
|
|
@@ -1124,6 +1124,19 @@ var ipc = createIPCServer(socketPath(), function (msg) {
|
|
|
1124
1124
|
return { ok: true };
|
|
1125
1125
|
}
|
|
1126
1126
|
|
|
1127
|
+
case "enable_recovery": {
|
|
1128
|
+
if (!msg.urlPath || !msg.password) return { ok: false, error: "missing urlPath or password" };
|
|
1129
|
+
relay.setRecovery(msg.urlPath, msg.password);
|
|
1130
|
+
console.log("[daemon] Admin recovery mode enabled");
|
|
1131
|
+
return { ok: true };
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
case "disable_recovery": {
|
|
1135
|
+
relay.clearRecovery();
|
|
1136
|
+
console.log("[daemon] Admin recovery mode disabled");
|
|
1137
|
+
return { ok: true };
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1127
1140
|
case "shutdown":
|
|
1128
1141
|
console.log("[daemon] Shutdown requested via IPC");
|
|
1129
1142
|
gracefulShutdown();
|
package/lib/notes.js
CHANGED
|
@@ -67,7 +67,7 @@ function createNotesManager(opts) {
|
|
|
67
67
|
function update(id, changes) {
|
|
68
68
|
for (var i = 0; i < notes.length; i++) {
|
|
69
69
|
if (notes[i].id === id) {
|
|
70
|
-
var allowed = ["text", "x", "y", "w", "h", "color", "minimized", "hidden", "zIndex"];
|
|
70
|
+
var allowed = ["text", "x", "y", "w", "h", "color", "minimized", "hidden", "zIndex", "opacity"];
|
|
71
71
|
for (var j = 0; j < allowed.length; j++) {
|
|
72
72
|
var key = allowed[j];
|
|
73
73
|
if (changes[key] !== undefined) {
|
package/lib/project.js
CHANGED
|
@@ -626,13 +626,12 @@ function createProjectContext(opts) {
|
|
|
626
626
|
console.log("[loop-registry] Schedule triggered: " + record.name + " → linked task " + loopId);
|
|
627
627
|
}
|
|
628
628
|
|
|
629
|
-
// Verify the loop directory and
|
|
629
|
+
// Verify the loop directory and PROMPT.md exist
|
|
630
630
|
var recDir = path.join(cwd, ".claude", "loops", loopId);
|
|
631
631
|
try {
|
|
632
632
|
fs.accessSync(path.join(recDir, "PROMPT.md"));
|
|
633
|
-
fs.accessSync(path.join(recDir, "JUDGE.md"));
|
|
634
633
|
} catch (e) {
|
|
635
|
-
console.error("[loop-registry]
|
|
634
|
+
console.error("[loop-registry] PROMPT.md missing for " + loopId);
|
|
636
635
|
return;
|
|
637
636
|
}
|
|
638
637
|
// Set the loopId and start
|
|
@@ -675,8 +674,7 @@ function createProjectContext(opts) {
|
|
|
675
674
|
try {
|
|
676
675
|
judgeText = fs.readFileSync(judgePath, "utf8");
|
|
677
676
|
} catch (e) {
|
|
678
|
-
|
|
679
|
-
return;
|
|
677
|
+
judgeText = null;
|
|
680
678
|
}
|
|
681
679
|
|
|
682
680
|
var baseCommit;
|
|
@@ -700,7 +698,7 @@ function createProjectContext(opts) {
|
|
|
700
698
|
loopState.promptText = promptText;
|
|
701
699
|
loopState.judgeText = judgeText;
|
|
702
700
|
loopState.iteration = 0;
|
|
703
|
-
loopState.maxIterations = loopConfig.maxIterations || loopOpts.maxIterations || 20;
|
|
701
|
+
loopState.maxIterations = judgeText ? (loopConfig.maxIterations || loopOpts.maxIterations || 20) : 1;
|
|
704
702
|
loopState.baseCommit = baseCommit;
|
|
705
703
|
loopState.currentSessionId = null;
|
|
706
704
|
loopState.judgeSessionId = null;
|
|
@@ -747,7 +745,11 @@ function createProjectContext(opts) {
|
|
|
747
745
|
sessionId: session.localId,
|
|
748
746
|
});
|
|
749
747
|
|
|
748
|
+
var coderCompleted = false;
|
|
750
749
|
session.onQueryComplete = function(completedSession) {
|
|
750
|
+
if (coderCompleted) return;
|
|
751
|
+
coderCompleted = true;
|
|
752
|
+
if (coderWatchdog) { clearTimeout(coderWatchdog); coderWatchdog = null; }
|
|
751
753
|
console.log("[ralph-loop] Coder #" + loopState.iteration + " onQueryComplete fired, history length: " + completedSession.history.length);
|
|
752
754
|
if (!loopState.active) { console.log("[ralph-loop] Coder: loopState.active is false, skipping"); return; }
|
|
753
755
|
// Check if session ended with error
|
|
@@ -774,9 +776,33 @@ function createProjectContext(opts) {
|
|
|
774
776
|
setTimeout(function() { runNextIteration(); }, 2000);
|
|
775
777
|
return;
|
|
776
778
|
}
|
|
777
|
-
|
|
779
|
+
if (loopState.judgeText) {
|
|
780
|
+
runJudge();
|
|
781
|
+
} else {
|
|
782
|
+
finishLoop("pass");
|
|
783
|
+
}
|
|
778
784
|
};
|
|
779
785
|
|
|
786
|
+
// Watchdog: if onQueryComplete hasn't fired after 10 minutes, force error and retry
|
|
787
|
+
var coderWatchdog = setTimeout(function() {
|
|
788
|
+
if (!coderCompleted && loopState.active && !loopState.stopping) {
|
|
789
|
+
console.error("[ralph-loop] Coder #" + loopState.iteration + " watchdog triggered — onQueryComplete never fired");
|
|
790
|
+
coderCompleted = true;
|
|
791
|
+
loopState.results.push({
|
|
792
|
+
iteration: loopState.iteration,
|
|
793
|
+
verdict: "error",
|
|
794
|
+
summary: "Coder session timed out (no completion signal)",
|
|
795
|
+
});
|
|
796
|
+
send({
|
|
797
|
+
type: "loop_verdict",
|
|
798
|
+
iteration: loopState.iteration,
|
|
799
|
+
verdict: "error",
|
|
800
|
+
summary: "Coder session timed out, retrying...",
|
|
801
|
+
});
|
|
802
|
+
setTimeout(function() { runNextIteration(); }, 2000);
|
|
803
|
+
}
|
|
804
|
+
}, 10 * 60 * 1000);
|
|
805
|
+
|
|
780
806
|
var userMsg = { type: "user_message", text: loopState.promptText };
|
|
781
807
|
session.history.push(userMsg);
|
|
782
808
|
sm.appendToSessionFile(session, userMsg);
|
|
@@ -835,7 +861,11 @@ function createProjectContext(opts) {
|
|
|
835
861
|
sessionId: judgeSession.localId,
|
|
836
862
|
});
|
|
837
863
|
|
|
864
|
+
var judgeCompleted = false;
|
|
838
865
|
judgeSession.onQueryComplete = function(completedSession) {
|
|
866
|
+
if (judgeCompleted) return;
|
|
867
|
+
judgeCompleted = true;
|
|
868
|
+
if (judgeWatchdog) { clearTimeout(judgeWatchdog); judgeWatchdog = null; }
|
|
839
869
|
console.log("[ralph-loop] Judge #" + loopState.iteration + " onQueryComplete fired, history length: " + completedSession.history.length);
|
|
840
870
|
var verdict = parseJudgeVerdict(completedSession);
|
|
841
871
|
console.log("[ralph-loop] Judge verdict: " + (verdict.pass ? "PASS" : "FAIL") + " - " + verdict.explanation);
|
|
@@ -860,6 +890,26 @@ function createProjectContext(opts) {
|
|
|
860
890
|
}
|
|
861
891
|
};
|
|
862
892
|
|
|
893
|
+
// Watchdog: judge should complete quickly (no tool use), 3 minutes is generous
|
|
894
|
+
var judgeWatchdog = setTimeout(function() {
|
|
895
|
+
if (!judgeCompleted && loopState.active && !loopState.stopping) {
|
|
896
|
+
console.error("[ralph-loop] Judge #" + loopState.iteration + " watchdog triggered — onQueryComplete never fired");
|
|
897
|
+
judgeCompleted = true;
|
|
898
|
+
loopState.results.push({
|
|
899
|
+
iteration: loopState.iteration,
|
|
900
|
+
verdict: "error",
|
|
901
|
+
summary: "Judge session timed out (no completion signal)",
|
|
902
|
+
});
|
|
903
|
+
send({
|
|
904
|
+
type: "loop_verdict",
|
|
905
|
+
iteration: loopState.iteration,
|
|
906
|
+
verdict: "error",
|
|
907
|
+
summary: "Judge session timed out, retrying...",
|
|
908
|
+
});
|
|
909
|
+
setTimeout(function() { runNextIteration(); }, 2000);
|
|
910
|
+
}
|
|
911
|
+
}, 3 * 60 * 1000);
|
|
912
|
+
|
|
863
913
|
var userMsg = { type: "user_message", text: judgePrompt };
|
|
864
914
|
judgeSession.history.push(userMsg);
|
|
865
915
|
sm.appendToSessionFile(judgeSession, userMsg);
|
|
@@ -1461,6 +1511,13 @@ function createProjectContext(opts) {
|
|
|
1461
1511
|
}
|
|
1462
1512
|
|
|
1463
1513
|
if (msg.type === "delete_session") {
|
|
1514
|
+
if (ws._clayUser) {
|
|
1515
|
+
var sdPerms = usersModule.getEffectivePermissions(ws._clayUser, osUsers);
|
|
1516
|
+
if (!sdPerms.sessionDelete) {
|
|
1517
|
+
sendTo(ws, { type: "error", text: "You do not have permission to delete sessions" });
|
|
1518
|
+
return;
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1464
1521
|
if (msg.id && sm.sessions.has(msg.id)) {
|
|
1465
1522
|
sm.deleteSession(msg.id, ws);
|
|
1466
1523
|
}
|
|
@@ -1781,7 +1838,8 @@ function createProjectContext(opts) {
|
|
|
1781
1838
|
session.messageUUIDs = session.messageUUIDs.slice(0, targetIdx);
|
|
1782
1839
|
}
|
|
1783
1840
|
|
|
1784
|
-
|
|
1841
|
+
var kept = session.messageUUIDs;
|
|
1842
|
+
session.lastRewindUuid = kept.length > 0 ? kept[kept.length - 1].uuid : null;
|
|
1785
1843
|
}
|
|
1786
1844
|
|
|
1787
1845
|
if (session.abortController) {
|
|
@@ -1893,6 +1951,7 @@ function createProjectContext(opts) {
|
|
|
1893
1951
|
var pending = session.pendingPermissions[requestId];
|
|
1894
1952
|
if (!pending) return;
|
|
1895
1953
|
delete session.pendingPermissions[requestId];
|
|
1954
|
+
onProcessingChanged(); // update cross-project permission badge
|
|
1896
1955
|
|
|
1897
1956
|
// --- Plan approval: "allow_accept_edits" — approve + switch to acceptEdits mode ---
|
|
1898
1957
|
if (decision === "allow_accept_edits") {
|
|
@@ -2103,6 +2162,15 @@ function createProjectContext(opts) {
|
|
|
2103
2162
|
}
|
|
2104
2163
|
|
|
2105
2164
|
// --- Create new empty project ---
|
|
2165
|
+
if (msg.type === "create_project" || msg.type === "clone_project") {
|
|
2166
|
+
if (ws._clayUser) {
|
|
2167
|
+
var cpPerms = usersModule.getEffectivePermissions(ws._clayUser, osUsers);
|
|
2168
|
+
if (!cpPerms.createProject) {
|
|
2169
|
+
sendTo(ws, { type: "add_project_result", ok: false, error: "You do not have permission to create projects" });
|
|
2170
|
+
return;
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2106
2174
|
if (msg.type === "create_project") {
|
|
2107
2175
|
var createName = (msg.name || "").trim();
|
|
2108
2176
|
if (!createName || !/^[a-zA-Z0-9_-]+$/.test(createName)) {
|
|
@@ -2139,13 +2207,14 @@ function createProjectContext(opts) {
|
|
|
2139
2207
|
// --- Create worktree from web UI ---
|
|
2140
2208
|
if (msg.type === "create_worktree") {
|
|
2141
2209
|
var wtBranch = (msg.branch || "").trim();
|
|
2210
|
+
var wtDirName = (msg.dirName || "").trim() || wtBranch.replace(/\//g, "-");
|
|
2142
2211
|
var wtBase = (msg.baseBranch || "").trim() || null;
|
|
2143
2212
|
if (!wtBranch || !/^[a-zA-Z0-9_\/.@-]+$/.test(wtBranch)) {
|
|
2144
2213
|
sendTo(ws, { type: "create_worktree_result", ok: false, error: "Invalid branch name" });
|
|
2145
2214
|
return;
|
|
2146
2215
|
}
|
|
2147
2216
|
if (typeof onCreateWorktree === "function") {
|
|
2148
|
-
var wtResult = onCreateWorktree(slug, wtBranch, wtBase);
|
|
2217
|
+
var wtResult = onCreateWorktree(slug, wtBranch, wtDirName, wtBase);
|
|
2149
2218
|
sendTo(ws, { type: "create_worktree_result", ok: wtResult.ok, slug: wtResult.slug, error: wtResult.error });
|
|
2150
2219
|
} else {
|
|
2151
2220
|
sendTo(ws, { type: "create_worktree_result", ok: false, error: "Not supported" });
|
|
@@ -2167,6 +2236,13 @@ function createProjectContext(opts) {
|
|
|
2167
2236
|
|
|
2168
2237
|
// --- Remove project from web UI ---
|
|
2169
2238
|
if (msg.type === "remove_project") {
|
|
2239
|
+
if (ws._clayUser) {
|
|
2240
|
+
var dpPerms = usersModule.getEffectivePermissions(ws._clayUser, osUsers);
|
|
2241
|
+
if (!dpPerms.deleteProject) {
|
|
2242
|
+
sendTo(ws, { type: "remove_project_result", ok: false, error: "You do not have permission to delete projects" });
|
|
2243
|
+
return;
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2170
2246
|
var removeSlug = msg.slug;
|
|
2171
2247
|
if (!removeSlug) {
|
|
2172
2248
|
sendTo(ws, { type: "remove_project_result", ok: false, error: "Missing slug" });
|
|
@@ -2310,6 +2386,15 @@ function createProjectContext(opts) {
|
|
|
2310
2386
|
}
|
|
2311
2387
|
|
|
2312
2388
|
// --- File browser ---
|
|
2389
|
+
if (msg.type === "fs_list" || msg.type === "fs_read" || msg.type === "fs_write" || msg.type === "fs_delete" || msg.type === "fs_rename" || msg.type === "fs_mkdir" || msg.type === "fs_upload") {
|
|
2390
|
+
if (ws._clayUser) {
|
|
2391
|
+
var fbPerms = usersModule.getEffectivePermissions(ws._clayUser, osUsers);
|
|
2392
|
+
if (!fbPerms.fileBrowser) {
|
|
2393
|
+
sendTo(ws, { type: msg.type + "_result", error: "File browser access is not permitted" });
|
|
2394
|
+
return;
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2313
2398
|
if (msg.type === "fs_list") {
|
|
2314
2399
|
var fsDir = safePath(cwd, msg.path || ".");
|
|
2315
2400
|
if (!fsDir) {
|
|
@@ -2418,6 +2503,20 @@ function createProjectContext(opts) {
|
|
|
2418
2503
|
return;
|
|
2419
2504
|
}
|
|
2420
2505
|
|
|
2506
|
+
// --- Project settings permission gate ---
|
|
2507
|
+
if (msg.type === "get_project_env" || msg.type === "set_project_env" ||
|
|
2508
|
+
msg.type === "read_global_claude_md" || msg.type === "write_global_claude_md" ||
|
|
2509
|
+
msg.type === "get_shared_env" || msg.type === "set_shared_env" ||
|
|
2510
|
+
msg.type === "transfer_project_owner") {
|
|
2511
|
+
if (ws._clayUser) {
|
|
2512
|
+
var psPerms = usersModule.getEffectivePermissions(ws._clayUser, osUsers);
|
|
2513
|
+
if (!psPerms.projectSettings) {
|
|
2514
|
+
sendTo(ws, { type: "error", text: "Project settings access is not permitted" });
|
|
2515
|
+
return;
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2421
2520
|
// --- Project environment variables ---
|
|
2422
2521
|
if (msg.type === "get_project_env") {
|
|
2423
2522
|
var envrc = "";
|
|
@@ -2735,6 +2834,13 @@ function createProjectContext(opts) {
|
|
|
2735
2834
|
|
|
2736
2835
|
// --- Web terminal ---
|
|
2737
2836
|
if (msg.type === "term_create") {
|
|
2837
|
+
if (ws._clayUser) {
|
|
2838
|
+
var termPerms = usersModule.getEffectivePermissions(ws._clayUser, osUsers);
|
|
2839
|
+
if (!termPerms.terminal) {
|
|
2840
|
+
sendTo(ws, { type: "term_error", error: "Terminal access is not permitted" });
|
|
2841
|
+
return;
|
|
2842
|
+
}
|
|
2843
|
+
}
|
|
2738
2844
|
var t = tm.create(msg.cols || 80, msg.rows || 24, getOsUserInfoForWs(ws));
|
|
2739
2845
|
if (!t) {
|
|
2740
2846
|
sendTo(ws, { type: "term_error", error: "Cannot create terminal (node-pty not available or limit reached)" });
|
|
@@ -2784,6 +2890,20 @@ function createProjectContext(opts) {
|
|
|
2784
2890
|
return;
|
|
2785
2891
|
}
|
|
2786
2892
|
|
|
2893
|
+
// --- Scheduled tasks permission gate ---
|
|
2894
|
+
if (msg.type === "loop_start" || msg.type === "loop_stop" || msg.type === "loop_registry_files" ||
|
|
2895
|
+
msg.type === "loop_registry_list" || msg.type === "loop_registry_update" || msg.type === "loop_registry_rename" ||
|
|
2896
|
+
msg.type === "loop_registry_remove" || msg.type === "loop_registry_convert" || msg.type === "loop_registry_toggle" ||
|
|
2897
|
+
msg.type === "loop_registry_rerun" || msg.type === "schedule_create" || msg.type === "schedule_move") {
|
|
2898
|
+
if (ws._clayUser) {
|
|
2899
|
+
var schPerms = usersModule.getEffectivePermissions(ws._clayUser, osUsers);
|
|
2900
|
+
if (!schPerms.scheduledTasks) {
|
|
2901
|
+
sendTo(ws, { type: "error", text: "Scheduled tasks access is not permitted" });
|
|
2902
|
+
return;
|
|
2903
|
+
}
|
|
2904
|
+
}
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2787
2907
|
if (msg.type === "loop_start") {
|
|
2788
2908
|
// If this loop has a cron schedule, don't run immediately — just confirm registration
|
|
2789
2909
|
if (loopState.wizardData && loopState.wizardData.cron) {
|
|
@@ -2840,7 +2960,39 @@ function createProjectContext(opts) {
|
|
|
2840
2960
|
fs.writeFileSync(tmpLoopJson, JSON.stringify({ maxIterations: maxIter }, null, 2));
|
|
2841
2961
|
fs.renameSync(tmpLoopJson, loopJsonPath);
|
|
2842
2962
|
|
|
2843
|
-
|
|
2963
|
+
var craftName = (loopState.wizardData && loopState.wizardData.name) || "";
|
|
2964
|
+
var isRalphCraft = recordSource === "ralph";
|
|
2965
|
+
|
|
2966
|
+
// User provided their own PROMPT.md (and optionally JUDGE.md)
|
|
2967
|
+
if (wData.mode === "own" && wData.promptText) {
|
|
2968
|
+
// Write PROMPT.md
|
|
2969
|
+
var promptPath = path.join(lDir, "PROMPT.md");
|
|
2970
|
+
var tmpPrompt = promptPath + ".tmp";
|
|
2971
|
+
fs.writeFileSync(tmpPrompt, wData.promptText);
|
|
2972
|
+
fs.renameSync(tmpPrompt, promptPath);
|
|
2973
|
+
|
|
2974
|
+
if (wData.judgeText) {
|
|
2975
|
+
// Both provided: write JUDGE.md too
|
|
2976
|
+
var judgePath = path.join(lDir, "JUDGE.md");
|
|
2977
|
+
var tmpJudge = judgePath + ".tmp";
|
|
2978
|
+
fs.writeFileSync(tmpJudge, wData.judgeText);
|
|
2979
|
+
fs.renameSync(tmpJudge, judgePath);
|
|
2980
|
+
} else {
|
|
2981
|
+
// No judge: force single iteration
|
|
2982
|
+
var singleJson = loopJsonPath + ".tmp2";
|
|
2983
|
+
fs.writeFileSync(singleJson, JSON.stringify({ maxIterations: 1 }, null, 2));
|
|
2984
|
+
fs.renameSync(singleJson, loopJsonPath);
|
|
2985
|
+
}
|
|
2986
|
+
|
|
2987
|
+
// Go straight to approval (no crafting needed)
|
|
2988
|
+
loopState.phase = "approval";
|
|
2989
|
+
saveLoopState();
|
|
2990
|
+
send({ type: "ralph_phase", phase: "approval", wizardData: loopState.wizardData });
|
|
2991
|
+
send({ type: "ralph_files_status", promptReady: true, judgeReady: !!wData.judgeText, bothReady: !!wData.judgeText });
|
|
2992
|
+
return;
|
|
2993
|
+
}
|
|
2994
|
+
|
|
2995
|
+
// Default: "draft" mode — Clay crafts both PROMPT.md and JUDGE.md
|
|
2844
2996
|
var craftingPrompt = "Use the /clay-ralph skill to design a Ralph Loop for the following task. " +
|
|
2845
2997
|
"You MUST invoke the clay-ralph skill — do NOT execute the task yourself. " +
|
|
2846
2998
|
"Your job is to interview me, then create PROMPT.md and JUDGE.md files " +
|
|
@@ -2850,11 +3002,9 @@ function createProjectContext(opts) {
|
|
|
2850
3002
|
|
|
2851
3003
|
// Create a new session for crafting
|
|
2852
3004
|
var craftingSession = sm.createSession();
|
|
2853
|
-
var craftName = (loopState.wizardData && loopState.wizardData.name) || "";
|
|
2854
|
-
var isRalphCraft = recordSource === "ralph";
|
|
2855
3005
|
craftingSession.title = (isRalphCraft ? "Ralph" : "Task") + (craftName ? " " + craftName : "") + " Crafting";
|
|
2856
3006
|
craftingSession.ralphCraftingMode = true;
|
|
2857
|
-
craftingSession.loop = { active: true, iteration: 0, role: "crafting", loopId: newLoopId, name: craftName, source: recordSource };
|
|
3007
|
+
craftingSession.loop = { active: true, iteration: 0, role: "crafting", loopId: newLoopId, name: craftName, source: recordSource, startedAt: loopState.startedAt };
|
|
2858
3008
|
sm.saveSessionFile(craftingSession);
|
|
2859
3009
|
sm.switchSession(craftingSession.localId);
|
|
2860
3010
|
loopState.craftingSessionId = craftingSession.localId;
|
|
@@ -3047,9 +3197,8 @@ function createProjectContext(opts) {
|
|
|
3047
3197
|
var rerunDir = path.join(cwd, ".claude", "loops", rerunRec.id);
|
|
3048
3198
|
try {
|
|
3049
3199
|
fs.accessSync(path.join(rerunDir, "PROMPT.md"));
|
|
3050
|
-
fs.accessSync(path.join(rerunDir, "JUDGE.md"));
|
|
3051
3200
|
} catch (e) {
|
|
3052
|
-
sendTo(ws, { type: "loop_registry_error", text: "
|
|
3201
|
+
sendTo(ws, { type: "loop_registry_error", text: "PROMPT.md missing for " + rerunRec.id });
|
|
3053
3202
|
return;
|
|
3054
3203
|
}
|
|
3055
3204
|
loopState.loopId = rerunRec.id;
|
|
@@ -3304,6 +3453,18 @@ function createProjectContext(opts) {
|
|
|
3304
3453
|
return true;
|
|
3305
3454
|
}
|
|
3306
3455
|
|
|
3456
|
+
// Skills permission gate
|
|
3457
|
+
if (urlPath === "/api/install-skill" || urlPath === "/api/uninstall-skill" || urlPath === "/api/installed-skills") {
|
|
3458
|
+
if (req._clayUser) {
|
|
3459
|
+
var skPerms = usersModule.getEffectivePermissions(req._clayUser, osUsers);
|
|
3460
|
+
if (!skPerms.skills) {
|
|
3461
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
3462
|
+
res.end('{"error":"Skills access is not permitted"}');
|
|
3463
|
+
return true;
|
|
3464
|
+
}
|
|
3465
|
+
}
|
|
3466
|
+
}
|
|
3467
|
+
|
|
3307
3468
|
// Install a skill (background spawn)
|
|
3308
3469
|
if (req.method === "POST" && urlPath === "/api/install-skill") {
|
|
3309
3470
|
parseJsonBody(req).then(function (body) {
|
|
@@ -3453,20 +3614,24 @@ function createProjectContext(opts) {
|
|
|
3453
3614
|
var mdContent = fs.readFileSync(mdPath, "utf8");
|
|
3454
3615
|
var desc = "";
|
|
3455
3616
|
// Parse YAML frontmatter for description
|
|
3617
|
+
var version = "";
|
|
3456
3618
|
if (mdContent.startsWith("---")) {
|
|
3457
3619
|
var endIdx = mdContent.indexOf("---", 3);
|
|
3458
3620
|
if (endIdx !== -1) {
|
|
3459
3621
|
var frontmatter = mdContent.substring(3, endIdx);
|
|
3460
3622
|
var descMatch = frontmatter.match(/^description:\s*(.+)/m);
|
|
3461
3623
|
if (descMatch) desc = descMatch[1].trim();
|
|
3624
|
+
var verMatch = frontmatter.match(/version:\s*"?([^"\n]+)"?/m);
|
|
3625
|
+
if (verMatch) version = verMatch[1].trim();
|
|
3462
3626
|
}
|
|
3463
3627
|
}
|
|
3464
3628
|
if (!installed[ent.name]) {
|
|
3465
|
-
installed[ent.name] = { scope: scanDirs[sd].scope, description: desc, path: path.join(scanDirs[sd].dir, ent.name) };
|
|
3629
|
+
installed[ent.name] = { scope: scanDirs[sd].scope, description: desc, version: version, path: path.join(scanDirs[sd].dir, ent.name) };
|
|
3466
3630
|
} else {
|
|
3467
3631
|
// project-level adds to existing global entry
|
|
3468
3632
|
installed[ent.name].scope = "both";
|
|
3469
3633
|
if (desc && !installed[ent.name].description) installed[ent.name].description = desc;
|
|
3634
|
+
if (version && !installed[ent.name].version) installed[ent.name].version = version;
|
|
3470
3635
|
}
|
|
3471
3636
|
} catch (e) {}
|
|
3472
3637
|
}
|
|
@@ -3476,6 +3641,110 @@ function createProjectContext(opts) {
|
|
|
3476
3641
|
return true;
|
|
3477
3642
|
}
|
|
3478
3643
|
|
|
3644
|
+
// Check skill updates (compare installed vs remote versions)
|
|
3645
|
+
if (req.method === "POST" && urlPath === "/api/check-skill-updates") {
|
|
3646
|
+
parseJsonBody(req).then(function (body) {
|
|
3647
|
+
var skills = body.skills; // [{ name, url, scope }]
|
|
3648
|
+
if (!Array.isArray(skills) || skills.length === 0) {
|
|
3649
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
3650
|
+
res.end('{"error":"missing skills array"}');
|
|
3651
|
+
return;
|
|
3652
|
+
}
|
|
3653
|
+
// Read installed versions
|
|
3654
|
+
var globalSkillsDir = path.join(os.homedir(), ".claude", "skills");
|
|
3655
|
+
var projectSkillsDir = path.join(cwd, ".claude", "skills");
|
|
3656
|
+
var results = [];
|
|
3657
|
+
var pending = skills.length;
|
|
3658
|
+
|
|
3659
|
+
function parseVersionFromSkillMd(content) {
|
|
3660
|
+
if (!content || !content.startsWith("---")) return "";
|
|
3661
|
+
var endIdx = content.indexOf("---", 3);
|
|
3662
|
+
if (endIdx === -1) return "";
|
|
3663
|
+
var fm = content.substring(3, endIdx);
|
|
3664
|
+
var m = fm.match(/version:\s*"?([^"\n]+)"?/m);
|
|
3665
|
+
return m ? m[1].trim() : "";
|
|
3666
|
+
}
|
|
3667
|
+
|
|
3668
|
+
function getInstalledVersion(name) {
|
|
3669
|
+
var dirs = [path.join(globalSkillsDir, name, "SKILL.md"), path.join(projectSkillsDir, name, "SKILL.md")];
|
|
3670
|
+
for (var d = 0; d < dirs.length; d++) {
|
|
3671
|
+
try {
|
|
3672
|
+
var c = fs.readFileSync(dirs[d], "utf8");
|
|
3673
|
+
var v = parseVersionFromSkillMd(c);
|
|
3674
|
+
if (v) return v;
|
|
3675
|
+
} catch (e) {}
|
|
3676
|
+
}
|
|
3677
|
+
return "";
|
|
3678
|
+
}
|
|
3679
|
+
|
|
3680
|
+
function compareVersions(a, b) {
|
|
3681
|
+
// returns -1 if a < b, 0 if equal, 1 if a > b
|
|
3682
|
+
if (!a && !b) return 0;
|
|
3683
|
+
if (!a) return -1;
|
|
3684
|
+
if (!b) return 1;
|
|
3685
|
+
var pa = a.split(".").map(Number);
|
|
3686
|
+
var pb = b.split(".").map(Number);
|
|
3687
|
+
for (var i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
3688
|
+
var va = pa[i] || 0;
|
|
3689
|
+
var vb = pb[i] || 0;
|
|
3690
|
+
if (va < vb) return -1;
|
|
3691
|
+
if (va > vb) return 1;
|
|
3692
|
+
}
|
|
3693
|
+
return 0;
|
|
3694
|
+
}
|
|
3695
|
+
|
|
3696
|
+
function finishOne() {
|
|
3697
|
+
pending--;
|
|
3698
|
+
if (pending === 0) {
|
|
3699
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3700
|
+
res.end(JSON.stringify({ results: results }));
|
|
3701
|
+
}
|
|
3702
|
+
}
|
|
3703
|
+
|
|
3704
|
+
for (var si = 0; si < skills.length; si++) {
|
|
3705
|
+
(function (skill) {
|
|
3706
|
+
var installedVer = getInstalledVersion(skill.name);
|
|
3707
|
+
var installed = !!installedVer;
|
|
3708
|
+
// Convert GitHub repo URL to raw SKILL.md URL
|
|
3709
|
+
var rawUrl = "";
|
|
3710
|
+
var ghMatch = skill.url.match(/github\.com\/([^/]+)\/([^/]+)/);
|
|
3711
|
+
if (ghMatch) {
|
|
3712
|
+
rawUrl = "https://raw.githubusercontent.com/" + ghMatch[1] + "/" + ghMatch[2] + "/main/SKILL.md";
|
|
3713
|
+
}
|
|
3714
|
+
if (!rawUrl) {
|
|
3715
|
+
results.push({ name: skill.name, installed: installed, installedVersion: installedVer, remoteVersion: "", status: installed ? "ok" : "missing" });
|
|
3716
|
+
finishOne();
|
|
3717
|
+
return;
|
|
3718
|
+
}
|
|
3719
|
+
// Fetch remote SKILL.md
|
|
3720
|
+
var https = require("https");
|
|
3721
|
+
https.get(rawUrl, function (resp) {
|
|
3722
|
+
var data = "";
|
|
3723
|
+
resp.on("data", function (chunk) { data += chunk; });
|
|
3724
|
+
resp.on("end", function () {
|
|
3725
|
+
var remoteVer = parseVersionFromSkillMd(data);
|
|
3726
|
+
var status = "ok";
|
|
3727
|
+
if (!installed) {
|
|
3728
|
+
status = "missing";
|
|
3729
|
+
} else if (remoteVer && compareVersions(installedVer, remoteVer) < 0) {
|
|
3730
|
+
status = "outdated";
|
|
3731
|
+
}
|
|
3732
|
+
results.push({ name: skill.name, installed: installed, installedVersion: installedVer, remoteVersion: remoteVer, status: status });
|
|
3733
|
+
finishOne();
|
|
3734
|
+
});
|
|
3735
|
+
}).on("error", function () {
|
|
3736
|
+
results.push({ name: skill.name, installed: installed, installedVersion: installedVer, remoteVersion: "", status: installed ? "ok" : "missing" });
|
|
3737
|
+
finishOne();
|
|
3738
|
+
});
|
|
3739
|
+
})(skills[si]);
|
|
3740
|
+
}
|
|
3741
|
+
}).catch(function () {
|
|
3742
|
+
res.writeHead(400);
|
|
3743
|
+
res.end("Bad request");
|
|
3744
|
+
});
|
|
3745
|
+
return true;
|
|
3746
|
+
}
|
|
3747
|
+
|
|
3479
3748
|
// Git dirty check
|
|
3480
3749
|
if (req.method === "GET" && urlPath === "/api/git-dirty") {
|
|
3481
3750
|
var execSync = require("child_process").execSync;
|
|
@@ -3560,8 +3829,12 @@ function createProjectContext(opts) {
|
|
|
3560
3829
|
function getStatus() {
|
|
3561
3830
|
var sessionCount = sm.sessions.size;
|
|
3562
3831
|
var hasProcessing = false;
|
|
3832
|
+
var pendingPermCount = 0;
|
|
3563
3833
|
sm.sessions.forEach(function (s) {
|
|
3564
3834
|
if (s.isProcessing) hasProcessing = true;
|
|
3835
|
+
if (s.pendingPermissions) {
|
|
3836
|
+
pendingPermCount += Object.keys(s.pendingPermissions).length;
|
|
3837
|
+
}
|
|
3565
3838
|
});
|
|
3566
3839
|
var status = {
|
|
3567
3840
|
slug: slug,
|
|
@@ -3572,6 +3845,7 @@ function createProjectContext(opts) {
|
|
|
3572
3845
|
clients: clients.size,
|
|
3573
3846
|
sessions: sessionCount,
|
|
3574
3847
|
isProcessing: hasProcessing,
|
|
3848
|
+
pendingPermissions: pendingPermCount,
|
|
3575
3849
|
projectOwnerId: projectOwnerId,
|
|
3576
3850
|
};
|
|
3577
3851
|
if (isMate) {
|