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/lib/scheduler.js
ADDED
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loop Registry — unified storage for all Ralph Loops (one-off + scheduled).
|
|
3
|
+
*
|
|
4
|
+
* Stores loop records in ~/.clay/loops/{encodedCwd}.jsonl
|
|
5
|
+
* Each record represents a job defined via clay-ralph (PROMPT.md + JUDGE.md).
|
|
6
|
+
* Records with a `cron` field are checked every 30s and auto-triggered.
|
|
7
|
+
* Records without cron are one-off (standard Ralph Loop behavior).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
var fs = require("fs");
|
|
11
|
+
var path = require("path");
|
|
12
|
+
var { CONFIG_DIR } = require("./config");
|
|
13
|
+
var { encodeCwd } = require("./utils");
|
|
14
|
+
|
|
15
|
+
// --- Cron parser (5-field: minute hour day-of-month month day-of-week) ---
|
|
16
|
+
|
|
17
|
+
function parseCronField(field, min, max) {
|
|
18
|
+
var values = [];
|
|
19
|
+
var parts = field.split(",");
|
|
20
|
+
for (var i = 0; i < parts.length; i++) {
|
|
21
|
+
var part = parts[i].trim();
|
|
22
|
+
|
|
23
|
+
// wildcard with step: */N
|
|
24
|
+
if (part.indexOf("/") !== -1) {
|
|
25
|
+
var slashParts = part.split("/");
|
|
26
|
+
var step = parseInt(slashParts[1], 10);
|
|
27
|
+
var rangeStr = slashParts[0];
|
|
28
|
+
var rangeMin = min;
|
|
29
|
+
var rangeMax = max;
|
|
30
|
+
if (rangeStr !== "*") {
|
|
31
|
+
var rp = rangeStr.split("-");
|
|
32
|
+
rangeMin = parseInt(rp[0], 10);
|
|
33
|
+
rangeMax = rp.length > 1 ? parseInt(rp[1], 10) : rangeMin;
|
|
34
|
+
}
|
|
35
|
+
for (var v = rangeMin; v <= rangeMax; v += step) {
|
|
36
|
+
values.push(v);
|
|
37
|
+
}
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// wildcard
|
|
42
|
+
if (part === "*") {
|
|
43
|
+
for (var v = min; v <= max; v++) {
|
|
44
|
+
values.push(v);
|
|
45
|
+
}
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// range: N-M
|
|
50
|
+
if (part.indexOf("-") !== -1) {
|
|
51
|
+
var rangeParts = part.split("-");
|
|
52
|
+
var from = parseInt(rangeParts[0], 10);
|
|
53
|
+
var to = parseInt(rangeParts[1], 10);
|
|
54
|
+
for (var v = from; v <= to; v++) {
|
|
55
|
+
values.push(v);
|
|
56
|
+
}
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// single value
|
|
61
|
+
values.push(parseInt(part, 10));
|
|
62
|
+
}
|
|
63
|
+
return values;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function parseCron(expr) {
|
|
67
|
+
var fields = expr.trim().split(/\s+/);
|
|
68
|
+
if (fields.length !== 5) return null;
|
|
69
|
+
return {
|
|
70
|
+
minutes: parseCronField(fields[0], 0, 59),
|
|
71
|
+
hours: parseCronField(fields[1], 0, 23),
|
|
72
|
+
daysOfMonth: parseCronField(fields[2], 1, 31),
|
|
73
|
+
months: parseCronField(fields[3], 1, 12),
|
|
74
|
+
daysOfWeek: parseCronField(fields[4], 0, 6),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function cronMatches(parsed, date) {
|
|
79
|
+
var minute = date.getMinutes();
|
|
80
|
+
var hour = date.getHours();
|
|
81
|
+
var dayOfMonth = date.getDate();
|
|
82
|
+
var month = date.getMonth() + 1;
|
|
83
|
+
var dayOfWeek = date.getDay();
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
parsed.minutes.indexOf(minute) !== -1 &&
|
|
87
|
+
parsed.hours.indexOf(hour) !== -1 &&
|
|
88
|
+
parsed.daysOfMonth.indexOf(dayOfMonth) !== -1 &&
|
|
89
|
+
parsed.months.indexOf(month) !== -1 &&
|
|
90
|
+
parsed.daysOfWeek.indexOf(dayOfWeek) !== -1
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Calculate next run time from a cron expression after a given date.
|
|
96
|
+
* Brute-force: check each minute for up to 366 days.
|
|
97
|
+
*/
|
|
98
|
+
function nextRunTime(cronExpr, after) {
|
|
99
|
+
var parsed = parseCron(cronExpr);
|
|
100
|
+
if (!parsed) return null;
|
|
101
|
+
|
|
102
|
+
var d = new Date(after || Date.now());
|
|
103
|
+
d.setSeconds(0, 0);
|
|
104
|
+
d.setMinutes(d.getMinutes() + 1);
|
|
105
|
+
|
|
106
|
+
var limit = 366 * 24 * 60;
|
|
107
|
+
for (var i = 0; i < limit; i++) {
|
|
108
|
+
if (cronMatches(parsed, d)) {
|
|
109
|
+
return d.getTime();
|
|
110
|
+
}
|
|
111
|
+
d.setMinutes(d.getMinutes() + 1);
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// --- Loop Registry factory ---
|
|
117
|
+
|
|
118
|
+
function createLoopRegistry(opts) {
|
|
119
|
+
var cwd = opts.cwd;
|
|
120
|
+
var onTrigger = opts.onTrigger;
|
|
121
|
+
var onChange = opts.onChange;
|
|
122
|
+
|
|
123
|
+
var encoded = encodeCwd(cwd);
|
|
124
|
+
var registryDir = path.join(CONFIG_DIR, "loops");
|
|
125
|
+
var registryPath = path.join(registryDir, encoded + ".jsonl");
|
|
126
|
+
|
|
127
|
+
var records = [];
|
|
128
|
+
var timerId = null;
|
|
129
|
+
var CHECK_INTERVAL = 30 * 1000;
|
|
130
|
+
var lastTriggeredMinute = {};
|
|
131
|
+
|
|
132
|
+
// --- Persistence (JSONL) ---
|
|
133
|
+
|
|
134
|
+
function load() {
|
|
135
|
+
try {
|
|
136
|
+
var raw = fs.readFileSync(registryPath, "utf8");
|
|
137
|
+
var lines = raw.trim().split("\n");
|
|
138
|
+
records = [];
|
|
139
|
+
for (var i = 0; i < lines.length; i++) {
|
|
140
|
+
if (!lines[i].trim()) continue;
|
|
141
|
+
try {
|
|
142
|
+
var rec = JSON.parse(lines[i]);
|
|
143
|
+
// Recalculate nextRunAt for scheduled records
|
|
144
|
+
if (rec.cron && rec.enabled) {
|
|
145
|
+
rec.nextRunAt = nextRunTime(rec.cron);
|
|
146
|
+
} else if (!rec.cron && rec.enabled && rec.date && rec.time && rec.source === "schedule") {
|
|
147
|
+
// One-off: recalculate from date+time
|
|
148
|
+
var dtP = rec.date.split("-");
|
|
149
|
+
var tmP = rec.time.split(":");
|
|
150
|
+
var runD = new Date(parseInt(dtP[0], 10), parseInt(dtP[1], 10) - 1, parseInt(dtP[2], 10), parseInt(tmP[0], 10), parseInt(tmP[1], 10), 0);
|
|
151
|
+
rec.nextRunAt = runD.getTime();
|
|
152
|
+
}
|
|
153
|
+
records.push(rec);
|
|
154
|
+
} catch (e) {
|
|
155
|
+
// skip malformed line
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
} catch (e) {
|
|
159
|
+
records = [];
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function save() {
|
|
164
|
+
try {
|
|
165
|
+
fs.mkdirSync(registryDir, { recursive: true });
|
|
166
|
+
var lines = [];
|
|
167
|
+
for (var i = 0; i < records.length; i++) {
|
|
168
|
+
lines.push(JSON.stringify(records[i]));
|
|
169
|
+
}
|
|
170
|
+
var tmpPath = registryPath + ".tmp";
|
|
171
|
+
fs.writeFileSync(tmpPath, lines.join("\n") + "\n");
|
|
172
|
+
fs.renameSync(tmpPath, registryPath);
|
|
173
|
+
} catch (e) {
|
|
174
|
+
console.error("[loop-registry] Failed to save:", e.message);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// --- Timer (scheduled loops only) ---
|
|
179
|
+
|
|
180
|
+
function startTimer() {
|
|
181
|
+
if (timerId) return;
|
|
182
|
+
timerId = setInterval(function () {
|
|
183
|
+
tick();
|
|
184
|
+
}, CHECK_INTERVAL);
|
|
185
|
+
tick();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function stopTimer() {
|
|
189
|
+
if (timerId) {
|
|
190
|
+
clearInterval(timerId);
|
|
191
|
+
timerId = null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function tick() {
|
|
196
|
+
var now = Date.now();
|
|
197
|
+
var nowMinuteKey = Math.floor(now / 60000);
|
|
198
|
+
|
|
199
|
+
for (var i = 0; i < records.length; i++) {
|
|
200
|
+
var rec = records[i];
|
|
201
|
+
if (!rec.enabled) continue;
|
|
202
|
+
if (!rec.nextRunAt) continue;
|
|
203
|
+
if (rec.nextRunAt > now) continue;
|
|
204
|
+
|
|
205
|
+
// Avoid double-trigger within same minute
|
|
206
|
+
var triggerKey = rec.id + "_" + nowMinuteKey;
|
|
207
|
+
if (lastTriggeredMinute[triggerKey]) continue;
|
|
208
|
+
lastTriggeredMinute[triggerKey] = true;
|
|
209
|
+
|
|
210
|
+
// Clean old trigger keys
|
|
211
|
+
var keys = Object.keys(lastTriggeredMinute);
|
|
212
|
+
for (var k = 0; k < keys.length; k++) {
|
|
213
|
+
var keyParts = keys[k].split("_");
|
|
214
|
+
var keyMinute = parseInt(keyParts[keyParts.length - 1], 10);
|
|
215
|
+
if (keyMinute < nowMinuteKey - 1) {
|
|
216
|
+
delete lastTriggeredMinute[keys[k]];
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Update nextRunAt
|
|
221
|
+
rec.lastRunAt = now;
|
|
222
|
+
if (rec.cron) {
|
|
223
|
+
rec.nextRunAt = nextRunTime(rec.cron, now);
|
|
224
|
+
} else {
|
|
225
|
+
// One-off schedule: disable after firing
|
|
226
|
+
rec.nextRunAt = null;
|
|
227
|
+
rec.enabled = false;
|
|
228
|
+
}
|
|
229
|
+
save();
|
|
230
|
+
if (onChange) onChange(records);
|
|
231
|
+
|
|
232
|
+
console.log("[loop-registry] Triggering scheduled loop: " + rec.name + " (" + rec.id + ")");
|
|
233
|
+
if (onTrigger) {
|
|
234
|
+
try { onTrigger(rec); } catch (e) {
|
|
235
|
+
console.error("[loop-registry] Trigger error:", e.message);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// --- CRUD ---
|
|
242
|
+
|
|
243
|
+
function register(data) {
|
|
244
|
+
var rec = {
|
|
245
|
+
id: data.id || ("loop_" + Date.now() + "_" + require("crypto").randomBytes(3).toString("hex")),
|
|
246
|
+
name: data.name || "Untitled",
|
|
247
|
+
task: data.task || "",
|
|
248
|
+
cron: data.cron || null,
|
|
249
|
+
enabled: data.cron ? (data.enabled !== false) : false,
|
|
250
|
+
maxIterations: data.maxIterations || 20,
|
|
251
|
+
createdAt: Date.now(),
|
|
252
|
+
updatedAt: Date.now(),
|
|
253
|
+
lastRunAt: null,
|
|
254
|
+
lastRunResult: null,
|
|
255
|
+
nextRunAt: null,
|
|
256
|
+
description: data.description || "",
|
|
257
|
+
date: data.date || null,
|
|
258
|
+
time: data.time || null,
|
|
259
|
+
allDay: data.allDay !== undefined ? data.allDay : true,
|
|
260
|
+
linkedTaskId: data.linkedTaskId || null,
|
|
261
|
+
craftingSessionId: data.craftingSessionId || null,
|
|
262
|
+
source: data.source || null,
|
|
263
|
+
color: data.color || null,
|
|
264
|
+
recurrenceEnd: data.recurrenceEnd || null,
|
|
265
|
+
runs: [],
|
|
266
|
+
};
|
|
267
|
+
if (rec.cron && rec.enabled) {
|
|
268
|
+
rec.nextRunAt = nextRunTime(rec.cron);
|
|
269
|
+
} else if (!rec.cron && rec.date && rec.time && rec.source === "schedule") {
|
|
270
|
+
// One-off schedule: compute nextRunAt from date + time
|
|
271
|
+
var dtParts = rec.date.split("-");
|
|
272
|
+
var tmParts = rec.time.split(":");
|
|
273
|
+
var runDate = new Date(parseInt(dtParts[0], 10), parseInt(dtParts[1], 10) - 1, parseInt(dtParts[2], 10), parseInt(tmParts[0], 10), parseInt(tmParts[1], 10), 0);
|
|
274
|
+
rec.nextRunAt = runDate.getTime();
|
|
275
|
+
rec.enabled = true;
|
|
276
|
+
}
|
|
277
|
+
records.push(rec);
|
|
278
|
+
save();
|
|
279
|
+
if (onChange) onChange(records);
|
|
280
|
+
return rec;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function update(id, data) {
|
|
284
|
+
var rec = getById(id);
|
|
285
|
+
if (!rec) return null;
|
|
286
|
+
|
|
287
|
+
if (data.name !== undefined) rec.name = data.name;
|
|
288
|
+
if (data.cron !== undefined) rec.cron = data.cron;
|
|
289
|
+
if (data.enabled !== undefined) rec.enabled = data.enabled;
|
|
290
|
+
if (data.maxIterations !== undefined) rec.maxIterations = data.maxIterations;
|
|
291
|
+
if (data.date !== undefined) rec.date = data.date;
|
|
292
|
+
if (data.recurrenceEnd !== undefined) rec.recurrenceEnd = data.recurrenceEnd;
|
|
293
|
+
rec.updatedAt = Date.now();
|
|
294
|
+
if (rec.cron && rec.enabled) {
|
|
295
|
+
rec.nextRunAt = nextRunTime(rec.cron);
|
|
296
|
+
} else if (!rec.cron && rec.date && rec.time && rec.source === "schedule") {
|
|
297
|
+
var dtP2 = rec.date.split("-");
|
|
298
|
+
var tmP2 = rec.time.split(":");
|
|
299
|
+
var runD2 = new Date(parseInt(dtP2[0], 10), parseInt(dtP2[1], 10) - 1, parseInt(dtP2[2], 10), parseInt(tmP2[0], 10), parseInt(tmP2[1], 10), 0);
|
|
300
|
+
rec.nextRunAt = runD2.getTime();
|
|
301
|
+
rec.enabled = true;
|
|
302
|
+
} else {
|
|
303
|
+
rec.nextRunAt = null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
save();
|
|
307
|
+
if (onChange) onChange(records);
|
|
308
|
+
return rec;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function updateRecord(id, data) {
|
|
312
|
+
var rec = getById(id);
|
|
313
|
+
if (!rec) return null;
|
|
314
|
+
var keys = Object.keys(data);
|
|
315
|
+
for (var i = 0; i < keys.length; i++) {
|
|
316
|
+
rec[keys[i]] = data[keys[i]];
|
|
317
|
+
}
|
|
318
|
+
save();
|
|
319
|
+
if (onChange) onChange(records);
|
|
320
|
+
return rec;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function remove(id) {
|
|
324
|
+
var idx = -1;
|
|
325
|
+
for (var i = 0; i < records.length; i++) {
|
|
326
|
+
if (records[i].id === id) { idx = i; break; }
|
|
327
|
+
}
|
|
328
|
+
if (idx === -1) return false;
|
|
329
|
+
records.splice(idx, 1);
|
|
330
|
+
save();
|
|
331
|
+
if (onChange) onChange(records);
|
|
332
|
+
return true;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function toggleEnabled(id) {
|
|
336
|
+
var rec = getById(id);
|
|
337
|
+
if (!rec || !rec.cron) return null; // only toggle scheduled loops
|
|
338
|
+
rec.enabled = !rec.enabled;
|
|
339
|
+
rec.updatedAt = Date.now();
|
|
340
|
+
rec.nextRunAt = rec.enabled ? nextRunTime(rec.cron) : null;
|
|
341
|
+
save();
|
|
342
|
+
if (onChange) onChange(records);
|
|
343
|
+
return rec;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function recordRun(id, result) {
|
|
347
|
+
var rec = getById(id);
|
|
348
|
+
if (!rec) return;
|
|
349
|
+
rec.lastRunAt = result.startedAt || Date.now();
|
|
350
|
+
rec.lastRunResult = result.reason || null;
|
|
351
|
+
rec.runs.push({
|
|
352
|
+
startedAt: result.startedAt || rec.lastRunAt,
|
|
353
|
+
finishedAt: Date.now(),
|
|
354
|
+
result: result.reason || "unknown",
|
|
355
|
+
iterations: result.iterations || 0,
|
|
356
|
+
});
|
|
357
|
+
// Keep only last 20 run entries
|
|
358
|
+
if (rec.runs.length > 20) {
|
|
359
|
+
rec.runs = rec.runs.slice(-20);
|
|
360
|
+
}
|
|
361
|
+
save();
|
|
362
|
+
if (onChange) onChange(records);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function getAll() {
|
|
366
|
+
return records;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function getById(id) {
|
|
370
|
+
for (var i = 0; i < records.length; i++) {
|
|
371
|
+
if (records[i].id === id) return records[i];
|
|
372
|
+
}
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function getScheduled() {
|
|
377
|
+
var result = [];
|
|
378
|
+
for (var i = 0; i < records.length; i++) {
|
|
379
|
+
if (records[i].cron) result.push(records[i]);
|
|
380
|
+
}
|
|
381
|
+
return result;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return {
|
|
385
|
+
load: load,
|
|
386
|
+
save: save,
|
|
387
|
+
startTimer: startTimer,
|
|
388
|
+
stopTimer: stopTimer,
|
|
389
|
+
register: register,
|
|
390
|
+
update: update,
|
|
391
|
+
updateRecord: updateRecord,
|
|
392
|
+
remove: remove,
|
|
393
|
+
toggleEnabled: toggleEnabled,
|
|
394
|
+
recordRun: recordRun,
|
|
395
|
+
getAll: getAll,
|
|
396
|
+
getById: getById,
|
|
397
|
+
getScheduled: getScheduled,
|
|
398
|
+
nextRunTime: nextRunTime,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
module.exports = { createLoopRegistry: createLoopRegistry };
|
package/lib/sdk-bridge.js
CHANGED
|
@@ -487,8 +487,9 @@ function createSDKBridge(opts) {
|
|
|
487
487
|
// --- SDK query lifecycle ---
|
|
488
488
|
|
|
489
489
|
function handleCanUseTool(session, toolName, input, opts) {
|
|
490
|
-
// Ralph Loop: auto-approve all tools, deny interactive ones
|
|
491
|
-
|
|
490
|
+
// Ralph Loop execution: auto-approve all tools, deny interactive ones.
|
|
491
|
+
// Crafting sessions are interactive — user and Claude collaborate to build PROMPT.md / JUDGE.md.
|
|
492
|
+
if (session.loop && session.loop.active && session.loop.role !== "crafting") {
|
|
492
493
|
if (toolName === "AskUserQuestion") {
|
|
493
494
|
return Promise.resolve({ behavior: "deny", message: "Autonomous mode. Make your own decision." });
|
|
494
495
|
}
|
package/lib/server.js
CHANGED
|
@@ -240,7 +240,10 @@ function serveStatic(urlPath, res) {
|
|
|
240
240
|
var content = fs.readFileSync(filePath);
|
|
241
241
|
var ext = path.extname(filePath);
|
|
242
242
|
var mime = MIME_TYPES[ext] || "application/octet-stream";
|
|
243
|
-
res.writeHead(200, {
|
|
243
|
+
res.writeHead(200, {
|
|
244
|
+
"Content-Type": mime + "; charset=utf-8",
|
|
245
|
+
"Cache-Control": "no-cache",
|
|
246
|
+
});
|
|
244
247
|
res.end(content);
|
|
245
248
|
return true;
|
|
246
249
|
} catch (e) {
|
|
@@ -451,6 +454,50 @@ function createServer(opts) {
|
|
|
451
454
|
return;
|
|
452
455
|
}
|
|
453
456
|
|
|
457
|
+
// User profile
|
|
458
|
+
var profilePath = path.join(CONFIG_DIR, "profile.json");
|
|
459
|
+
|
|
460
|
+
if (req.method === "GET" && fullUrl === "/api/profile") {
|
|
461
|
+
var profile = { name: "", lang: "en-US", avatarColor: "#7c3aed", avatarStyle: "thumbs", avatarSeed: "" };
|
|
462
|
+
try {
|
|
463
|
+
var raw = fs.readFileSync(profilePath, "utf8");
|
|
464
|
+
var saved = JSON.parse(raw);
|
|
465
|
+
if (saved.name !== undefined) profile.name = saved.name;
|
|
466
|
+
if (saved.lang) profile.lang = saved.lang;
|
|
467
|
+
if (saved.avatarColor) profile.avatarColor = saved.avatarColor;
|
|
468
|
+
if (saved.avatarStyle) profile.avatarStyle = saved.avatarStyle;
|
|
469
|
+
if (saved.avatarSeed) profile.avatarSeed = saved.avatarSeed;
|
|
470
|
+
} catch (e) { /* file doesn't exist yet */ }
|
|
471
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
472
|
+
res.end(JSON.stringify(profile));
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (req.method === "PUT" && fullUrl === "/api/profile") {
|
|
477
|
+
var body = "";
|
|
478
|
+
req.on("data", function (chunk) { body += chunk; });
|
|
479
|
+
req.on("end", function () {
|
|
480
|
+
try {
|
|
481
|
+
var data = JSON.parse(body);
|
|
482
|
+
var profile = {};
|
|
483
|
+
if (typeof data.name === "string") profile.name = data.name.substring(0, 50);
|
|
484
|
+
if (typeof data.lang === "string") profile.lang = data.lang.substring(0, 10);
|
|
485
|
+
if (typeof data.avatarColor === "string" && /^#[0-9a-fA-F]{6}$/.test(data.avatarColor)) {
|
|
486
|
+
profile.avatarColor = data.avatarColor;
|
|
487
|
+
}
|
|
488
|
+
if (typeof data.avatarStyle === "string") profile.avatarStyle = data.avatarStyle.substring(0, 30);
|
|
489
|
+
if (typeof data.avatarSeed === "string") profile.avatarSeed = data.avatarSeed.substring(0, 30);
|
|
490
|
+
fs.writeFileSync(profilePath, JSON.stringify(profile, null, 2), "utf8");
|
|
491
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
492
|
+
res.end(JSON.stringify(profile));
|
|
493
|
+
} catch (e) {
|
|
494
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
495
|
+
res.end(JSON.stringify({ error: "Invalid request" }));
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
|
|
454
501
|
// Skills proxy: leaderboard list
|
|
455
502
|
if (req.method === "GET" && fullUrl === "/api/skills") {
|
|
456
503
|
var qs = req.url.indexOf("?") >= 0 ? req.url.substring(req.url.indexOf("?")) : "";
|
|
@@ -491,8 +538,20 @@ function createServer(opts) {
|
|
|
491
538
|
res.end(searchCached.data);
|
|
492
539
|
return;
|
|
493
540
|
}
|
|
494
|
-
|
|
495
|
-
|
|
541
|
+
// Reuse cached "all" tab data if available, otherwise fetch
|
|
542
|
+
var allCached = skillsCache["skills_all"];
|
|
543
|
+
var allPromise = (allCached && Date.now() - allCached.ts < 300000)
|
|
544
|
+
? Promise.resolve(JSON.parse(allCached.data))
|
|
545
|
+
: fetchSkillsPage("https://skills.sh/");
|
|
546
|
+
allPromise.then(function (data) {
|
|
547
|
+
var q = searchQ.toLowerCase();
|
|
548
|
+
var filtered = (data.skills || []).filter(function (s) {
|
|
549
|
+
var name = (s.name || "").toLowerCase();
|
|
550
|
+
var source = (s.source || "").toLowerCase();
|
|
551
|
+
var skillId = (s.skillId || "").toLowerCase();
|
|
552
|
+
return name.indexOf(q) >= 0 || source.indexOf(q) >= 0 || skillId.indexOf(q) >= 0;
|
|
553
|
+
});
|
|
554
|
+
var json = JSON.stringify({ skills: filtered });
|
|
496
555
|
skillsCache[searchCacheKey] = { ts: Date.now(), data: json };
|
|
497
556
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
498
557
|
res.end(json);
|
|
@@ -775,6 +834,68 @@ function createServer(opts) {
|
|
|
775
834
|
projects.forEach(function (ctx) { list.push(ctx.getStatus()); });
|
|
776
835
|
return list;
|
|
777
836
|
},
|
|
837
|
+
getHubSchedules: function () {
|
|
838
|
+
var allSchedules = [];
|
|
839
|
+
projects.forEach(function (ctx, s) {
|
|
840
|
+
var status = ctx.getStatus();
|
|
841
|
+
var recs = ctx.getSchedules();
|
|
842
|
+
for (var i = 0; i < recs.length; i++) {
|
|
843
|
+
// Shallow-copy full record and augment with project metadata
|
|
844
|
+
var copy = {};
|
|
845
|
+
var keys = Object.keys(recs[i]);
|
|
846
|
+
for (var k = 0; k < keys.length; k++) copy[keys[k]] = recs[i][keys[k]];
|
|
847
|
+
copy.projectSlug = s;
|
|
848
|
+
copy.projectTitle = status.title || status.project;
|
|
849
|
+
allSchedules.push(copy);
|
|
850
|
+
}
|
|
851
|
+
});
|
|
852
|
+
return allSchedules;
|
|
853
|
+
},
|
|
854
|
+
// Move a schedule record from one project to another
|
|
855
|
+
moveScheduleToProject: function (recordId, fromSlug, toSlug) {
|
|
856
|
+
var fromCtx = projects.get(fromSlug);
|
|
857
|
+
var toCtx = projects.get(toSlug);
|
|
858
|
+
if (!fromCtx || !toCtx) return { ok: false, error: "Project not found" };
|
|
859
|
+
var recs = fromCtx.getSchedules();
|
|
860
|
+
var rec = null;
|
|
861
|
+
for (var i = 0; i < recs.length; i++) {
|
|
862
|
+
if (recs[i].id === recordId) { rec = recs[i]; break; }
|
|
863
|
+
}
|
|
864
|
+
if (!rec) return { ok: false, error: "Record not found" };
|
|
865
|
+
// Copy full record data
|
|
866
|
+
var data = {};
|
|
867
|
+
var keys = Object.keys(rec);
|
|
868
|
+
for (var k = 0; k < keys.length; k++) data[keys[k]] = rec[keys[k]];
|
|
869
|
+
// Import into target, remove from source
|
|
870
|
+
toCtx.importSchedule(data);
|
|
871
|
+
fromCtx.removeSchedule(recordId);
|
|
872
|
+
return { ok: true };
|
|
873
|
+
},
|
|
874
|
+
// Bulk move all schedules from one project to another
|
|
875
|
+
moveAllSchedulesToProject: function (fromSlug, toSlug) {
|
|
876
|
+
var fromCtx = projects.get(fromSlug);
|
|
877
|
+
var toCtx = projects.get(toSlug);
|
|
878
|
+
if (!fromCtx || !toCtx) return { ok: false, error: "Project not found" };
|
|
879
|
+
var recs = fromCtx.getSchedules();
|
|
880
|
+
for (var i = 0; i < recs.length; i++) {
|
|
881
|
+
var data = {};
|
|
882
|
+
var keys = Object.keys(recs[i]);
|
|
883
|
+
for (var k = 0; k < keys.length; k++) data[keys[k]] = recs[i][keys[k]];
|
|
884
|
+
toCtx.importSchedule(data);
|
|
885
|
+
}
|
|
886
|
+
// Remove all from source
|
|
887
|
+
var ids = recs.map(function (r) { return r.id; });
|
|
888
|
+
for (var j = 0; j < ids.length; j++) {
|
|
889
|
+
fromCtx.removeSchedule(ids[j]);
|
|
890
|
+
}
|
|
891
|
+
return { ok: true };
|
|
892
|
+
},
|
|
893
|
+
// Get schedule count for a project slug
|
|
894
|
+
getScheduleCount: function (slug) {
|
|
895
|
+
var ctx = projects.get(slug);
|
|
896
|
+
if (!ctx) return 0;
|
|
897
|
+
return ctx.getSchedules().length;
|
|
898
|
+
},
|
|
778
899
|
onProcessingChanged: broadcastProcessingChange,
|
|
779
900
|
onAddProject: onAddProject,
|
|
780
901
|
onRemoveProject: onRemoveProject,
|
package/lib/sessions.js
CHANGED
|
@@ -81,6 +81,7 @@ function createSessionManager(opts) {
|
|
|
81
81
|
createdAt: session.createdAt,
|
|
82
82
|
};
|
|
83
83
|
if (session.lastRewindUuid) metaObj.lastRewindUuid = session.lastRewindUuid;
|
|
84
|
+
if (session.loop) metaObj.loop = session.loop;
|
|
84
85
|
var meta = JSON.stringify(metaObj);
|
|
85
86
|
var lines = [meta];
|
|
86
87
|
for (var i = 0; i < session.history.length; i++) {
|
|
@@ -157,6 +158,7 @@ function createSessionManager(opts) {
|
|
|
157
158
|
messageUUIDs: messageUUIDs,
|
|
158
159
|
lastRewindUuid: m.lastRewindUuid || null,
|
|
159
160
|
};
|
|
161
|
+
if (m.loop) session.loop = m.loop;
|
|
160
162
|
sessions.set(localId, session);
|
|
161
163
|
}
|
|
162
164
|
}
|
|
@@ -168,10 +170,24 @@ function createSessionManager(opts) {
|
|
|
168
170
|
return sessions.get(activeSessionId) || null;
|
|
169
171
|
}
|
|
170
172
|
|
|
173
|
+
var resolveLoopInfo = null; // optional callback: (loopId) => { name, source } or null
|
|
174
|
+
|
|
175
|
+
function setResolveLoopInfo(fn) {
|
|
176
|
+
resolveLoopInfo = fn;
|
|
177
|
+
}
|
|
178
|
+
|
|
171
179
|
function broadcastSessionList() {
|
|
172
180
|
send({
|
|
173
181
|
type: "session_list",
|
|
174
|
-
sessions: [...sessions.values()].map(function(s) {
|
|
182
|
+
sessions: [...sessions.values()].filter(function(s) { return !s.hidden; }).map(function(s) {
|
|
183
|
+
var loop = s.loop ? Object.assign({}, s.loop) : null;
|
|
184
|
+
if (loop && loop.loopId && resolveLoopInfo) {
|
|
185
|
+
var info = resolveLoopInfo(loop.loopId);
|
|
186
|
+
if (info) {
|
|
187
|
+
if (info.name) loop.name = info.name;
|
|
188
|
+
if (info.source) loop.source = info.source;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
175
191
|
return {
|
|
176
192
|
id: s.localId,
|
|
177
193
|
cliSessionId: s.cliSessionId || null,
|
|
@@ -179,7 +195,7 @@ function createSessionManager(opts) {
|
|
|
179
195
|
active: s.localId === activeSessionId,
|
|
180
196
|
isProcessing: s.isProcessing,
|
|
181
197
|
lastActivity: s.lastActivity || s.createdAt || 0,
|
|
182
|
-
loop:
|
|
198
|
+
loop: loop,
|
|
183
199
|
};
|
|
184
200
|
}),
|
|
185
201
|
});
|
|
@@ -310,6 +326,21 @@ function createSessionManager(opts) {
|
|
|
310
326
|
}
|
|
311
327
|
}
|
|
312
328
|
|
|
329
|
+
function deleteSessionQuiet(localId) {
|
|
330
|
+
var session = sessions.get(localId);
|
|
331
|
+
if (!session) return;
|
|
332
|
+
if (session.abortController) {
|
|
333
|
+
try { session.abortController.abort(); } catch(e) {}
|
|
334
|
+
}
|
|
335
|
+
if (session.messageQueue) {
|
|
336
|
+
try { session.messageQueue.end(); } catch(e) {}
|
|
337
|
+
}
|
|
338
|
+
if (session.cliSessionId) {
|
|
339
|
+
try { fs.unlinkSync(sessionFilePath(session.cliSessionId)); } catch(e) {}
|
|
340
|
+
}
|
|
341
|
+
sessions.delete(localId);
|
|
342
|
+
}
|
|
343
|
+
|
|
313
344
|
function doSendAndRecord(session, obj) {
|
|
314
345
|
session.history.push(obj);
|
|
315
346
|
appendToSessionFile(session, obj);
|
|
@@ -419,6 +450,7 @@ function createSessionManager(opts) {
|
|
|
419
450
|
createSession: createSession,
|
|
420
451
|
switchSession: switchSession,
|
|
421
452
|
deleteSession: deleteSession,
|
|
453
|
+
deleteSessionQuiet: deleteSessionQuiet,
|
|
422
454
|
resumeSession: resumeSession,
|
|
423
455
|
broadcastSessionList: broadcastSessionList,
|
|
424
456
|
saveSessionFile: saveSessionFile,
|
|
@@ -427,6 +459,7 @@ function createSessionManager(opts) {
|
|
|
427
459
|
findTurnBoundary: findTurnBoundary,
|
|
428
460
|
replayHistory: replayHistory,
|
|
429
461
|
searchSessions: searchSessions,
|
|
462
|
+
setResolveLoopInfo: setResolveLoopInfo,
|
|
430
463
|
};
|
|
431
464
|
}
|
|
432
465
|
|