clay-server 2.7.1 → 2.8.0

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.
Files changed (56) hide show
  1. package/lib/project.js +176 -20
  2. package/lib/public/app.js +846 -92
  3. package/lib/public/apple-touch-icon-dark.png +0 -0
  4. package/lib/public/apple-touch-icon.png +0 -0
  5. package/lib/public/clay-logo.png +0 -0
  6. package/lib/public/css/base.css +10 -0
  7. package/lib/public/css/filebrowser.css +1 -0
  8. package/lib/public/css/home-hub.css +455 -0
  9. package/lib/public/css/icon-strip.css +6 -5
  10. package/lib/public/css/loop.css +86 -29
  11. package/lib/public/css/messages.css +2 -0
  12. package/lib/public/css/mobile-nav.css +38 -12
  13. package/lib/public/css/overlays.css +205 -169
  14. package/lib/public/css/playbook.css +264 -0
  15. package/lib/public/css/profile.css +268 -0
  16. package/lib/public/css/scheduler-modal.css +883 -0
  17. package/lib/public/css/scheduler.css +379 -18
  18. package/lib/public/css/sidebar.css +305 -11
  19. package/lib/public/css/sticky-notes.css +23 -19
  20. package/lib/public/css/stt.css +155 -0
  21. package/lib/public/css/title-bar.css +14 -6
  22. package/lib/public/favicon-banded-32.png +0 -0
  23. package/lib/public/favicon-banded.png +0 -0
  24. package/lib/public/icon-192-dark.png +0 -0
  25. package/lib/public/icon-192.png +0 -0
  26. package/lib/public/icon-512-dark.png +0 -0
  27. package/lib/public/icon-512.png +0 -0
  28. package/lib/public/icon-banded-76.png +0 -0
  29. package/lib/public/icon-banded-96.png +0 -0
  30. package/lib/public/index.html +252 -32
  31. package/lib/public/modules/ascii-logo.js +389 -0
  32. package/lib/public/modules/filebrowser.js +2 -1
  33. package/lib/public/modules/markdown.js +108 -0
  34. package/lib/public/modules/notifications.js +50 -63
  35. package/lib/public/modules/playbook.js +578 -0
  36. package/lib/public/modules/profile.js +357 -0
  37. package/lib/public/modules/project-settings.js +4 -9
  38. package/lib/public/modules/scheduler.js +1620 -34
  39. package/lib/public/modules/server-settings.js +1 -1
  40. package/lib/public/modules/sidebar.js +378 -31
  41. package/lib/public/modules/sticky-notes.js +2 -0
  42. package/lib/public/modules/stt.js +272 -0
  43. package/lib/public/modules/terminal.js +32 -0
  44. package/lib/public/modules/theme.js +3 -10
  45. package/lib/public/modules/tools.js +2 -1
  46. package/lib/public/style.css +4 -0
  47. package/lib/public/sw.js +82 -3
  48. package/lib/public/wordmark-banded-20.png +0 -0
  49. package/lib/public/wordmark-banded-32.png +0 -0
  50. package/lib/public/wordmark-banded-64.png +0 -0
  51. package/lib/public/wordmark-banded-80.png +0 -0
  52. package/lib/scheduler.js +43 -3
  53. package/lib/sdk-bridge.js +3 -2
  54. package/lib/server.js +124 -3
  55. package/lib/sessions.js +34 -1
  56. package/package.json +1 -1
package/lib/project.js CHANGED
@@ -76,6 +76,10 @@ function createProjectContext(opts) {
76
76
  var lanHost = opts.lanHost || null;
77
77
  var getProjectCount = opts.getProjectCount || function () { return 1; };
78
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; };
79
83
  var onProcessingChanged = opts.onProcessingChanged || function () {};
80
84
  var latestVersion = null;
81
85
 
@@ -438,29 +442,48 @@ function createProjectContext(opts) {
438
442
  console.log("[loop-registry] Skipping trigger — loop already active");
439
443
  return;
440
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
+
441
457
  // Verify the loop directory and files exist
442
- var recDir = path.join(cwd, ".claude", "loops", record.id);
458
+ var recDir = path.join(cwd, ".claude", "loops", loopId);
443
459
  try {
444
460
  fs.accessSync(path.join(recDir, "PROMPT.md"));
445
461
  fs.accessSync(path.join(recDir, "JUDGE.md"));
446
462
  } catch (e) {
447
- console.error("[loop-registry] Loop files missing for " + record.id);
463
+ console.error("[loop-registry] Loop files missing for " + loopId);
448
464
  return;
449
465
  }
450
466
  // Set the loopId and start
451
- loopState.loopId = record.id;
467
+ loopState.loopId = loopId;
452
468
  activeRegistryId = record.id;
453
- console.log("[loop-registry] Auto-starting loop: " + record.name);
469
+ console.log("[loop-registry] Auto-starting loop: " + record.name + " (" + loopId + ")");
454
470
  send({ type: "schedule_run_started", recordId: record.id });
455
471
  startLoop();
456
472
  },
457
- onChange: function (records) {
458
- send({ type: "loop_registry_updated", records: records });
473
+ onChange: function () {
474
+ send({ type: "loop_registry_updated", records: getHubSchedules() });
459
475
  },
460
476
  });
461
477
  loopRegistry.load();
462
478
  loopRegistry.startTimer();
463
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
+
464
487
  function startLoop(opts) {
465
488
  var loopOpts = opts || {};
466
489
  var dir = loopDir();
@@ -534,9 +557,12 @@ function createProjectContext(opts) {
534
557
  }
535
558
 
536
559
  var session = sm.createSession();
537
- session.loop = { active: true, iteration: loopState.iteration, role: "coder" };
538
560
  var loopName = (loopState.wizardData && loopState.wizardData.name) || "";
539
- session.title = "Ralph" + (loopName ? " " + loopName : "") + " #" + loopState.iteration;
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;
540
566
  sm.saveSessionFile(session);
541
567
  sm.broadcastSessionList();
542
568
 
@@ -621,9 +647,12 @@ function createProjectContext(opts) {
621
647
  "Do NOT use any tools. Just analyze and respond.";
622
648
 
623
649
  var judgeSession = sm.createSession();
624
- judgeSession.loop = { active: true, iteration: loopState.iteration, role: "judge" };
625
650
  var judgeName = (loopState.wizardData && loopState.wizardData.name) || "";
626
- judgeSession.title = "Ralph" + (judgeName ? " " + judgeName : "") + " Judge #" + loopState.iteration;
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;
627
656
  sm.saveSessionFile(judgeSession);
628
657
  sm.broadcastSessionList();
629
658
  loopState.judgeSessionId = judgeSession.localId;
@@ -841,7 +870,7 @@ function createProjectContext(opts) {
841
870
  sendTo(ws, { type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [] });
842
871
  sendTo(ws, { type: "term_list", terminals: tm.list() });
843
872
  sendTo(ws, { type: "notes_list", notes: nm.list() });
844
- sendTo(ws, { type: "loop_registry_updated", records: loopRegistry.getAll() });
873
+ sendTo(ws, { type: "loop_registry_updated", records: getHubSchedules() });
845
874
 
846
875
  // Ralph Loop availability
847
876
  var hasLoopFiles = false;
@@ -885,7 +914,15 @@ function createProjectContext(opts) {
885
914
  // Session list
886
915
  sendTo(ws, {
887
916
  type: "session_list",
888
- 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
+ }
889
926
  return {
890
927
  id: s.localId,
891
928
  cliSessionId: s.cliSessionId || null,
@@ -893,6 +930,7 @@ function createProjectContext(opts) {
893
930
  active: s.localId === sm.activeSessionId,
894
931
  isProcessing: s.isProcessing,
895
932
  lastActivity: s.lastActivity || s.createdAt || 0,
933
+ loop: loop,
896
934
  };
897
935
  }),
898
936
  });
@@ -900,7 +938,7 @@ function createProjectContext(opts) {
900
938
  // Restore active session for this client
901
939
  var active = sm.getActiveSession();
902
940
  if (active) {
903
- 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 });
904
942
 
905
943
  var total = active.history.length;
906
944
  var fromIndex = 0;
@@ -1545,6 +1583,18 @@ function createProjectContext(opts) {
1545
1583
  return;
1546
1584
  }
1547
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
+
1548
1598
  // --- Remove project from web UI ---
1549
1599
  if (msg.type === "remove_project") {
1550
1600
  var removeSlug = msg.slug;
@@ -1552,6 +1602,10 @@ function createProjectContext(opts) {
1552
1602
  sendTo(ws, { type: "remove_project_result", ok: false, error: "Missing slug" });
1553
1603
  return;
1554
1604
  }
1605
+ // If client chose to move tasks to another project before removing
1606
+ if (msg.moveTasksTo) {
1607
+ moveAllSchedulesToProject(removeSlug, msg.moveTasksTo);
1608
+ }
1555
1609
  if (typeof opts.onRemoveProject === "function") {
1556
1610
  var removeResult = opts.onRemoveProject(removeSlug);
1557
1611
  sendTo(ws, { type: "remove_project_result", ok: removeResult.ok, slug: removeSlug, error: removeResult.error });
@@ -1561,6 +1615,17 @@ function createProjectContext(opts) {
1561
1615
  return;
1562
1616
  }
1563
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
+
1564
1629
  // --- Reorder projects ---
1565
1630
  if (msg.type === "reorder_projects") {
1566
1631
  var slugs = msg.slugs;
@@ -2110,7 +2175,7 @@ function createProjectContext(opts) {
2110
2175
 
2111
2176
  if (msg.type === "ralph_wizard_complete") {
2112
2177
  var wData = msg.data || {};
2113
- var maxIter = wData.maxIterations || 25;
2178
+ var maxIter = wData.maxIterations || 3;
2114
2179
  var wizardCron = wData.cron || null;
2115
2180
  var newLoopId = generateLoopId();
2116
2181
  loopState.loopId = newLoopId;
@@ -2125,6 +2190,7 @@ function createProjectContext(opts) {
2125
2190
  saveLoopState();
2126
2191
 
2127
2192
  // Register in loop registry
2193
+ var recordSource = wData.source === "task" ? null : "ralph";
2128
2194
  loopRegistry.register({
2129
2195
  id: newLoopId,
2130
2196
  name: loopState.wizardData.name,
@@ -2132,6 +2198,7 @@ function createProjectContext(opts) {
2132
2198
  cron: wizardCron,
2133
2199
  enabled: wizardCron ? true : false,
2134
2200
  maxIterations: maxIter,
2201
+ source: recordSource,
2135
2202
  });
2136
2203
 
2137
2204
  // Create loop directory and write LOOP.json
@@ -2153,9 +2220,10 @@ function createProjectContext(opts) {
2153
2220
  // Create a new session for crafting
2154
2221
  var craftingSession = sm.createSession();
2155
2222
  var craftName = (loopState.wizardData && loopState.wizardData.name) || "";
2156
- craftingSession.title = "Ralph" + (craftName ? " " + craftName : "") + " Crafting";
2223
+ var isRalphCraft = recordSource === "ralph";
2224
+ craftingSession.title = (isRalphCraft ? "Ralph" : "Task") + (craftName ? " " + craftName : "") + " Crafting";
2157
2225
  craftingSession.ralphCraftingMode = true;
2158
- craftingSession.hidden = true;
2226
+ craftingSession.loop = { active: true, iteration: 0, role: "crafting", loopId: newLoopId, name: craftName, source: recordSource };
2159
2227
  sm.saveSessionFile(craftingSession);
2160
2228
  sm.switchSession(craftingSession.localId);
2161
2229
  loopState.craftingSessionId = craftingSession.localId;
@@ -2166,7 +2234,7 @@ function createProjectContext(opts) {
2166
2234
  // Start .claude/ directory watcher
2167
2235
  startClaudeDirWatch();
2168
2236
 
2169
- // Start query
2237
+ // Send crafting prompt and start the conversation with Claude.
2170
2238
  craftingSession.history.push({ type: "user_message", text: craftingPrompt });
2171
2239
  sm.appendToSessionFile(craftingSession, { type: "user_message", text: craftingPrompt });
2172
2240
  send({ type: "user_message", text: craftingPrompt });
@@ -2176,7 +2244,7 @@ function createProjectContext(opts) {
2176
2244
  send({ type: "status", status: "processing" });
2177
2245
  sdk.startQuery(craftingSession, craftingPrompt);
2178
2246
 
2179
- send({ type: "ralph_crafting_started", sessionId: craftingSession.localId, taskId: newLoopId });
2247
+ send({ type: "ralph_crafting_started", sessionId: craftingSession.localId, taskId: newLoopId, source: recordSource });
2180
2248
  send({ type: "ralph_phase", phase: "crafting", wizardData: loopState.wizardData, craftingSessionId: craftingSession.localId });
2181
2249
  return;
2182
2250
  }
@@ -2244,9 +2312,36 @@ function createProjectContext(opts) {
2244
2312
  return;
2245
2313
  }
2246
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
+
2247
2342
  // --- Loop Registry messages ---
2248
2343
  if (msg.type === "loop_registry_list") {
2249
- sendTo(ws, { type: "loop_registry_updated", records: loopRegistry.getAll() });
2344
+ sendTo(ws, { type: "loop_registry_updated", records: getHubSchedules() });
2250
2345
  return;
2251
2346
  }
2252
2347
 
@@ -2258,6 +2353,14 @@ function createProjectContext(opts) {
2258
2353
  return;
2259
2354
  }
2260
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
+
2261
2364
  if (msg.type === "loop_registry_remove") {
2262
2365
  var removedRec = loopRegistry.remove(msg.id);
2263
2366
  if (!removedRec) {
@@ -2266,6 +2369,31 @@ function createProjectContext(opts) {
2266
2369
  return;
2267
2370
  }
2268
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
+
2269
2397
  if (msg.type === "loop_registry_toggle") {
2270
2398
  var toggledRec = loopRegistry.toggleEnabled(msg.id);
2271
2399
  if (!toggledRec) {
@@ -2730,9 +2858,37 @@ function createProjectContext(opts) {
2730
2858
  handleDisconnection: handleDisconnection,
2731
2859
  handleHTTP: handleHTTP,
2732
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); },
2733
2864
  setTitle: setTitle,
2734
2865
  setIcon: setIcon,
2735
- warmup: function () { sdk.warmup(); },
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
+ },
2736
2892
  destroy: destroy,
2737
2893
  };
2738
2894
  }