clay-server 2.27.0-beta.9 → 2.27.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.
- package/README.md +10 -0
- package/lib/daemon-projects.js +164 -0
- package/lib/daemon.js +13 -126
- package/lib/mates-identity.js +132 -0
- package/lib/mates-knowledge.js +113 -0
- package/lib/mates-prompts.js +398 -0
- package/lib/mates.js +40 -599
- package/lib/project-connection.js +2 -0
- package/lib/project-http.js +4 -2
- package/lib/project-loop.js +110 -48
- package/lib/project-mate-interaction.js +4 -0
- package/lib/project-notifications.js +210 -0
- package/lib/project-sessions.js +5 -2
- package/lib/project-user-message.js +2 -1
- package/lib/project.js +26 -2
- package/lib/public/app.js +1193 -8517
- package/lib/public/css/command-palette.css +14 -0
- package/lib/public/css/loop.css +301 -0
- package/lib/public/css/notifications-center.css +190 -0
- package/lib/public/css/rewind.css +6 -0
- package/lib/public/index.html +89 -35
- package/lib/public/modules/app-connection.js +160 -0
- package/lib/public/modules/app-cursors.js +473 -0
- package/lib/public/modules/app-debate-ui.js +389 -0
- package/lib/public/modules/app-dm.js +627 -0
- package/lib/public/modules/app-favicon.js +212 -0
- package/lib/public/modules/app-header.js +229 -0
- package/lib/public/modules/app-home-hub.js +600 -0
- package/lib/public/modules/app-loop-ui.js +589 -0
- package/lib/public/modules/app-loop-wizard.js +439 -0
- package/lib/public/modules/app-messages.js +1560 -0
- package/lib/public/modules/app-misc.js +299 -0
- package/lib/public/modules/app-notifications.js +372 -0
- package/lib/public/modules/app-panels.js +888 -0
- package/lib/public/modules/app-projects.js +798 -0
- package/lib/public/modules/app-rate-limit.js +451 -0
- package/lib/public/modules/app-rendering.js +597 -0
- package/lib/public/modules/app-skills-install.js +234 -0
- package/lib/public/modules/command-palette.js +27 -4
- package/lib/public/modules/input.js +31 -20
- package/lib/public/modules/scheduler-config.js +1532 -0
- package/lib/public/modules/scheduler-history.js +79 -0
- package/lib/public/modules/scheduler.js +33 -1554
- package/lib/public/modules/session-search.js +13 -1
- package/lib/public/modules/sidebar-mates.js +812 -0
- package/lib/public/modules/sidebar-mobile.js +1269 -0
- package/lib/public/modules/sidebar-projects.js +1449 -0
- package/lib/public/modules/sidebar-sessions.js +986 -0
- package/lib/public/modules/sidebar.js +232 -4591
- package/lib/public/modules/store.js +27 -0
- package/lib/public/modules/ws-ref.js +7 -0
- package/lib/public/style.css +1 -0
- package/lib/sdk-bridge.js +96 -717
- package/lib/sdk-message-processor.js +587 -0
- package/lib/sdk-message-queue.js +42 -0
- package/lib/sdk-skill-discovery.js +131 -0
- package/lib/server-admin.js +712 -0
- package/lib/server-auth.js +737 -0
- package/lib/server-dm.js +221 -0
- package/lib/server-mates.js +281 -0
- package/lib/server-palette.js +110 -0
- package/lib/server-settings.js +479 -0
- package/lib/server-skills.js +280 -0
- package/lib/server.js +246 -2755
- package/lib/sessions.js +11 -4
- package/lib/users-auth.js +146 -0
- package/lib/users-permissions.js +118 -0
- package/lib/users-preferences.js +210 -0
- package/lib/users.js +48 -398
- package/lib/ws-schema.js +498 -0
- package/package.json +1 -1
package/lib/project-http.js
CHANGED
|
@@ -629,11 +629,13 @@ function attachHTTP(ctx) {
|
|
|
629
629
|
var execSync = require("child_process").execSync;
|
|
630
630
|
try {
|
|
631
631
|
var out = execSync("git status --porcelain", { cwd: cwd, encoding: "utf8", timeout: 5000 });
|
|
632
|
-
var
|
|
632
|
+
var lines = out.trim().split("\n").filter(function (line) {
|
|
633
633
|
return line.trim().length > 0 && !line.startsWith("??");
|
|
634
634
|
});
|
|
635
|
+
var dirty = lines.length > 0;
|
|
636
|
+
var files = lines.map(function (line) { return line.trim(); });
|
|
635
637
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
636
|
-
res.end(JSON.stringify({ dirty: dirty }));
|
|
638
|
+
res.end(JSON.stringify({ dirty: dirty, files: files }));
|
|
637
639
|
} catch (e) {
|
|
638
640
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
639
641
|
res.end(JSON.stringify({ dirty: false }));
|
package/lib/project-loop.js
CHANGED
|
@@ -21,6 +21,7 @@ function attachLoop(ctx) {
|
|
|
21
21
|
var sendTo = ctx.sendTo;
|
|
22
22
|
var sendToSession = ctx.sendToSession;
|
|
23
23
|
var pushModule = ctx.pushModule;
|
|
24
|
+
var notificationsModule = ctx.notificationsModule;
|
|
24
25
|
var getHubSchedules = ctx.getHubSchedules;
|
|
25
26
|
var getLinuxUserForSession = ctx.getLinuxUserForSession;
|
|
26
27
|
var onProcessingChanged = ctx.onProcessingChanged;
|
|
@@ -134,13 +135,16 @@ function attachLoop(ctx) {
|
|
|
134
135
|
var _ld = path.join(_loopsBase, _loopDirs[_li]);
|
|
135
136
|
try {
|
|
136
137
|
fs.accessSync(path.join(_ld, "PROMPT.md"));
|
|
137
|
-
fs.accessSync(path.join(_ld, "JUDGE.md"));
|
|
138
138
|
fs.accessSync(path.join(_ld, "LOOP.json"));
|
|
139
|
+
var _loopCfg = JSON.parse(fs.readFileSync(path.join(_ld, "LOOP.json"), "utf8"));
|
|
140
|
+
var _isSimple = _loopCfg.loopMode === "simple";
|
|
141
|
+
if (!_isSimple) fs.accessSync(path.join(_ld, "JUDGE.md"));
|
|
139
142
|
// Found a completed loop — recover to approval phase
|
|
140
143
|
loopState.loopId = _loopDirs[_li];
|
|
141
144
|
loopState.phase = "approval";
|
|
142
|
-
var _loopCfg = JSON.parse(fs.readFileSync(path.join(_ld, "LOOP.json"), "utf8"));
|
|
143
145
|
loopState.maxIterations = _loopCfg.maxIterations || 20;
|
|
146
|
+
if (!loopState.wizardData) loopState.wizardData = {};
|
|
147
|
+
loopState.wizardData.loopMode = _loopCfg.loopMode || "judge";
|
|
144
148
|
saveLoopState();
|
|
145
149
|
console.log("[ralph-loop] Recovered orphaned loop: " + _loopDirs[_li]);
|
|
146
150
|
break;
|
|
@@ -177,7 +181,8 @@ function attachLoop(ctx) {
|
|
|
177
181
|
var hasJudge = false;
|
|
178
182
|
try { fs.accessSync(path.join(dir, "PROMPT.md")); hasPrompt = true; } catch (e) {}
|
|
179
183
|
try { fs.accessSync(path.join(dir, "JUDGE.md")); hasJudge = true; } catch (e) {}
|
|
180
|
-
|
|
184
|
+
var isSimple = loopState.wizardData && loopState.wizardData.loopMode === "simple";
|
|
185
|
+
return isSimple ? hasPrompt : (hasPrompt && hasJudge);
|
|
181
186
|
}
|
|
182
187
|
|
|
183
188
|
// .claude/ directory watcher for PROMPT.md / JUDGE.md
|
|
@@ -223,16 +228,18 @@ function attachLoop(ctx) {
|
|
|
223
228
|
try { fs.accessSync(path.join(dir, "JUDGE.md")); hasJudge = true; } catch (e) {}
|
|
224
229
|
try { fs.accessSync(path.join(dir, "LOOP.json")); hasLoopJson = true; } catch (e) {}
|
|
225
230
|
}
|
|
231
|
+
var isSimple = loopState.wizardData && loopState.wizardData.loopMode === "simple";
|
|
232
|
+
var bothReady = isSimple ? hasPrompt : (hasPrompt && hasJudge);
|
|
226
233
|
send({
|
|
227
234
|
type: "ralph_files_status",
|
|
228
235
|
promptReady: hasPrompt,
|
|
229
236
|
judgeReady: hasJudge,
|
|
230
237
|
loopJsonReady: hasLoopJson,
|
|
231
|
-
bothReady:
|
|
238
|
+
bothReady: bothReady,
|
|
232
239
|
taskId: loopState.loopId,
|
|
233
240
|
});
|
|
234
|
-
// Auto-transition to approval phase when
|
|
235
|
-
if (
|
|
241
|
+
// Auto-transition to approval phase when files are ready
|
|
242
|
+
if (bothReady && loopState.phase === "crafting") {
|
|
236
243
|
loopState.phase = "approval";
|
|
237
244
|
saveLoopState();
|
|
238
245
|
|
|
@@ -298,7 +305,10 @@ function attachLoop(ctx) {
|
|
|
298
305
|
// Set the loopId to the schedule's own id (not the linked task) so sidebar groups correctly
|
|
299
306
|
loopState.loopId = record.id;
|
|
300
307
|
loopState.loopFilesId = loopFilesId;
|
|
301
|
-
|
|
308
|
+
// Restore loopMode from LOOP.json so simple loops work correctly on trigger
|
|
309
|
+
var _triggerCfg = {};
|
|
310
|
+
try { _triggerCfg = JSON.parse(fs.readFileSync(path.join(recDir, "LOOP.json"), "utf8")); } catch (e) {}
|
|
311
|
+
loopState.wizardData = { loopMode: _triggerCfg.loopMode || "judge" };
|
|
302
312
|
activeRegistryId = record.id;
|
|
303
313
|
console.log("[loop-registry] Auto-starting loop: " + record.name + " (" + loopState.loopId + ")");
|
|
304
314
|
send({ type: "schedule_run_started", recordId: record.id });
|
|
@@ -356,12 +366,17 @@ function attachLoop(ctx) {
|
|
|
356
366
|
loopConfig = JSON.parse(fs.readFileSync(path.join(dir, "LOOP.json"), "utf8"));
|
|
357
367
|
} catch (e) {}
|
|
358
368
|
|
|
369
|
+
var isSimple = loopState.wizardData && loopState.wizardData.loopMode === "simple";
|
|
359
370
|
loopState.active = true;
|
|
360
371
|
loopState.phase = "executing";
|
|
361
372
|
loopState.promptText = promptText;
|
|
362
|
-
loopState.judgeText = judgeText;
|
|
373
|
+
loopState.judgeText = isSimple ? null : judgeText;
|
|
363
374
|
loopState.iteration = 0;
|
|
364
|
-
|
|
375
|
+
if (isSimple) {
|
|
376
|
+
loopState.maxIterations = (loopOpts.maxIterations >= 1 ? loopOpts.maxIterations : null) || loopConfig.maxIterations || 5;
|
|
377
|
+
} else {
|
|
378
|
+
loopState.maxIterations = judgeText ? ((loopOpts.maxIterations >= 1 ? loopOpts.maxIterations : null) || loopConfig.maxIterations || 20) : 1;
|
|
379
|
+
}
|
|
365
380
|
loopState.baseCommit = baseCommit;
|
|
366
381
|
loopState.currentSessionId = null;
|
|
367
382
|
loopState.judgeSessionId = null;
|
|
@@ -440,7 +455,15 @@ function attachLoop(ctx) {
|
|
|
440
455
|
setTimeout(function() { runNextIteration(); }, 2000);
|
|
441
456
|
return;
|
|
442
457
|
}
|
|
443
|
-
|
|
458
|
+
var _isSimple = loopState.wizardData && loopState.wizardData.loopMode === "simple";
|
|
459
|
+
if (_isSimple) {
|
|
460
|
+
// Simple mode: no judge, proceed to next iteration or finish
|
|
461
|
+
if (loopState.iteration >= loopState.maxIterations) {
|
|
462
|
+
finishLoop("complete");
|
|
463
|
+
} else {
|
|
464
|
+
setTimeout(function() { runNextIteration(); }, 1000);
|
|
465
|
+
}
|
|
466
|
+
} else if (loopState.judgeText && loopState.maxIterations > 1) {
|
|
444
467
|
runJudge();
|
|
445
468
|
} else {
|
|
446
469
|
finishLoop("pass");
|
|
@@ -648,19 +671,23 @@ function attachLoop(ctx) {
|
|
|
648
671
|
}
|
|
649
672
|
|
|
650
673
|
if (pushModule) {
|
|
651
|
-
var
|
|
652
|
-
? "
|
|
674
|
+
var _finishBody = reason === "pass" || reason === "complete"
|
|
675
|
+
? "Completed after " + loopState.iteration + " iteration(s)"
|
|
653
676
|
: reason === "max_iterations"
|
|
654
677
|
? "Reached max iterations (" + loopState.maxIterations + ")"
|
|
655
|
-
: reason === "stopped"
|
|
656
|
-
? "Loop stopped by user"
|
|
657
|
-
: "Loop ended due to error";
|
|
678
|
+
: reason === "stopped" ? "Stopped by user" : "Ended due to error";
|
|
658
679
|
pushModule.sendPush({
|
|
659
|
-
type: "done",
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
680
|
+
type: "done", slug: slug, title: "Loop Complete", body: _finishBody, tag: "ralph-loop-done",
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
if (notificationsModule) {
|
|
685
|
+
notificationsModule.notify("loop_complete", {
|
|
686
|
+
reason: reason,
|
|
687
|
+
name: loopState.name,
|
|
688
|
+
iterations: loopState.iteration,
|
|
689
|
+
maxIterations: loopState.maxIterations,
|
|
690
|
+
sessionId: loopState.currentSessionId,
|
|
664
691
|
});
|
|
665
692
|
}
|
|
666
693
|
}
|
|
@@ -683,14 +710,19 @@ function attachLoop(ctx) {
|
|
|
683
710
|
saveLoopState();
|
|
684
711
|
return;
|
|
685
712
|
}
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
713
|
+
var _isSimpleResume = loopState.wizardData && loopState.wizardData.loopMode === "simple";
|
|
714
|
+
if (!_isSimpleResume) {
|
|
715
|
+
try {
|
|
716
|
+
loopState.judgeText = fs.readFileSync(path.join(dir, "JUDGE.md"), "utf8");
|
|
717
|
+
} catch (e) {
|
|
718
|
+
console.error("[ralph-loop] Cannot resume: missing JUDGE.md");
|
|
719
|
+
loopState.active = false;
|
|
720
|
+
loopState.phase = "idle";
|
|
721
|
+
saveLoopState();
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
} else {
|
|
725
|
+
loopState.judgeText = null;
|
|
694
726
|
}
|
|
695
727
|
// Retry the interrupted iteration (runNextIteration will increment)
|
|
696
728
|
if (loopState.iteration > 0) {
|
|
@@ -742,7 +774,7 @@ function attachLoop(ctx) {
|
|
|
742
774
|
send({ type: "loop_scheduled", recordId: loopState.loopId, cron: loopState.wizardData.cron });
|
|
743
775
|
return true;
|
|
744
776
|
}
|
|
745
|
-
startLoop();
|
|
777
|
+
startLoop({ maxIterations: msg.maxIterations });
|
|
746
778
|
return true;
|
|
747
779
|
}
|
|
748
780
|
|
|
@@ -753,7 +785,7 @@ function attachLoop(ctx) {
|
|
|
753
785
|
|
|
754
786
|
if (msg.type === "ralph_wizard_complete") {
|
|
755
787
|
var wData = msg.data || {};
|
|
756
|
-
var maxIter = wData.maxIterations ||
|
|
788
|
+
var maxIter = wData.maxIterations || null;
|
|
757
789
|
var wizardCron = wData.cron || null;
|
|
758
790
|
var newLoopId = generateLoopId();
|
|
759
791
|
loopState.loopId = newLoopId;
|
|
@@ -762,6 +794,9 @@ function attachLoop(ctx) {
|
|
|
762
794
|
task: wData.task || "",
|
|
763
795
|
maxIterations: maxIter,
|
|
764
796
|
cron: wizardCron,
|
|
797
|
+
loopMode: wData.loopMode || "judge",
|
|
798
|
+
promptAuthor: wData.promptAuthor || "clay",
|
|
799
|
+
judgeAuthor: wData.judgeAuthor || null,
|
|
765
800
|
};
|
|
766
801
|
loopState.phase = "crafting";
|
|
767
802
|
loopState.startedAt = Date.now();
|
|
@@ -784,7 +819,7 @@ function attachLoop(ctx) {
|
|
|
784
819
|
try { fs.mkdirSync(lDir, { recursive: true }); } catch (e) {}
|
|
785
820
|
var loopJsonPath = path.join(lDir, "LOOP.json");
|
|
786
821
|
var tmpLoopJson = loopJsonPath + ".tmp";
|
|
787
|
-
fs.writeFileSync(tmpLoopJson, JSON.stringify({ maxIterations: maxIter }, null, 2));
|
|
822
|
+
fs.writeFileSync(tmpLoopJson, JSON.stringify({ maxIterations: maxIter, loopMode: wData.loopMode || "judge" }, null, 2));
|
|
788
823
|
fs.renameSync(tmpLoopJson, loopJsonPath);
|
|
789
824
|
|
|
790
825
|
var craftName = (loopState.wizardData && loopState.wizardData.name) || "";
|
|
@@ -804,19 +839,15 @@ function attachLoop(ctx) {
|
|
|
804
839
|
var tmpJudge = judgePath + ".tmp";
|
|
805
840
|
fs.writeFileSync(tmpJudge, wData.judgeText);
|
|
806
841
|
fs.renameSync(tmpJudge, judgePath);
|
|
807
|
-
} else if (!recordSource) {
|
|
808
|
-
//
|
|
809
|
-
var singleJson = loopJsonPath + ".tmp2";
|
|
810
|
-
fs.writeFileSync(singleJson, JSON.stringify({ maxIterations: 1 }, null, 2));
|
|
811
|
-
fs.renameSync(singleJson, loopJsonPath);
|
|
812
|
-
|
|
842
|
+
} else if (wData.loopMode === "simple" || !recordSource) {
|
|
843
|
+
// Simple loop or scheduled task with no judge: go straight to approval
|
|
813
844
|
loopState.phase = "approval";
|
|
814
845
|
saveLoopState();
|
|
815
846
|
send({ type: "ralph_phase", phase: "approval", source: recordSource, wizardData: loopState.wizardData });
|
|
816
847
|
send({ type: "ralph_files_status", promptReady: true, judgeReady: false, bothReady: true });
|
|
817
848
|
return true;
|
|
818
849
|
} else {
|
|
819
|
-
// Ralph with no judge: start a crafting session to create JUDGE.md
|
|
850
|
+
// Ralph with judge mode but no judge provided: start a crafting session to create JUDGE.md
|
|
820
851
|
loopState.phase = "crafting";
|
|
821
852
|
saveLoopState();
|
|
822
853
|
|
|
@@ -863,13 +894,39 @@ function attachLoop(ctx) {
|
|
|
863
894
|
return true;
|
|
864
895
|
}
|
|
865
896
|
|
|
866
|
-
// Default: "draft" mode — Clay crafts
|
|
867
|
-
var
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
"
|
|
871
|
-
|
|
872
|
-
|
|
897
|
+
// Default: "draft" mode — Clay crafts files via the clay-ralph skill
|
|
898
|
+
var _draftIsSimple = wData.loopMode === "simple";
|
|
899
|
+
var craftingPrompt;
|
|
900
|
+
if (_draftIsSimple) {
|
|
901
|
+
craftingPrompt = "Use the /clay-ralph skill to design a Ralph Loop for the following task. " +
|
|
902
|
+
"You MUST invoke the clay-ralph skill — do NOT execute the task yourself. " +
|
|
903
|
+
"This is a Simple Loop (no judge). Your job is to create ONLY a PROMPT.md file " +
|
|
904
|
+
"that a future autonomous session will execute. Do NOT create a JUDGE.md file.\n\n" +
|
|
905
|
+
"## Task\n" + (wData.task || "") +
|
|
906
|
+
"\n\n## Loop Directory\n" + lDir;
|
|
907
|
+
} else if (wData.judgeAuthor === "me") {
|
|
908
|
+
craftingPrompt = "Use the /clay-ralph skill to design a Ralph Loop for the following task. " +
|
|
909
|
+
"You MUST invoke the clay-ralph skill — do NOT execute the task yourself. " +
|
|
910
|
+
"The user will provide their own JUDGE.md, so create ONLY a PROMPT.md file " +
|
|
911
|
+
"that a future autonomous session will execute. Do NOT create a JUDGE.md file.\n\n" +
|
|
912
|
+
"## Task\n" + (wData.task || "") +
|
|
913
|
+
"\n\n## Loop Directory\n" + lDir;
|
|
914
|
+
} else {
|
|
915
|
+
craftingPrompt = "Use the /clay-ralph skill to design a Ralph Loop for the following task. " +
|
|
916
|
+
"You MUST invoke the clay-ralph skill — do NOT execute the task yourself. " +
|
|
917
|
+
"Your job is to interview me, then create PROMPT.md and JUDGE.md files " +
|
|
918
|
+
"that a future autonomous session will execute.\n\n" +
|
|
919
|
+
"## Task\n" + (wData.task || "") +
|
|
920
|
+
"\n\n## Loop Directory\n" + lDir;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// Pre-write user-provided files before crafting starts
|
|
924
|
+
if (wData.judgeText && wData.judgeAuthor === "me") {
|
|
925
|
+
var _judgePathDraft = path.join(lDir, "JUDGE.md");
|
|
926
|
+
var _tmpJudgeDraft = _judgePathDraft + ".tmp";
|
|
927
|
+
fs.writeFileSync(_tmpJudgeDraft, wData.judgeText);
|
|
928
|
+
fs.renameSync(_tmpJudgeDraft, _judgePathDraft);
|
|
929
|
+
}
|
|
873
930
|
|
|
874
931
|
// Create a new session for crafting
|
|
875
932
|
var craftingSession = sm.createSession();
|
|
@@ -1088,10 +1145,11 @@ function attachLoop(ctx) {
|
|
|
1088
1145
|
// --- Connection state: send loop state to newly connected client ---
|
|
1089
1146
|
function sendConnectionState(ws) {
|
|
1090
1147
|
// Ralph Loop availability
|
|
1148
|
+
var _connIsSimple = loopState.wizardData && loopState.wizardData.loopMode === "simple";
|
|
1091
1149
|
var hasLoopFiles = false;
|
|
1092
1150
|
try {
|
|
1093
1151
|
fs.accessSync(path.join(cwd, ".claude", "PROMPT.md"));
|
|
1094
|
-
fs.accessSync(path.join(cwd, ".claude", "JUDGE.md"));
|
|
1152
|
+
if (!_connIsSimple) fs.accessSync(path.join(cwd, ".claude", "JUDGE.md"));
|
|
1095
1153
|
hasLoopFiles = true;
|
|
1096
1154
|
} catch (e) {}
|
|
1097
1155
|
// Also check loop directory files
|
|
@@ -1100,7 +1158,7 @@ function attachLoop(ctx) {
|
|
|
1100
1158
|
if (_avDir) {
|
|
1101
1159
|
try {
|
|
1102
1160
|
fs.accessSync(path.join(_avDir, "PROMPT.md"));
|
|
1103
|
-
fs.accessSync(path.join(_avDir, "JUDGE.md"));
|
|
1161
|
+
if (!_connIsSimple) fs.accessSync(path.join(_avDir, "JUDGE.md"));
|
|
1104
1162
|
hasLoopFiles = true;
|
|
1105
1163
|
} catch (e) {}
|
|
1106
1164
|
}
|
|
@@ -1115,11 +1173,14 @@ function attachLoop(ctx) {
|
|
|
1115
1173
|
});
|
|
1116
1174
|
|
|
1117
1175
|
// Ralph phase state
|
|
1176
|
+
// Derive source from wizardData for reconnect (so client can distinguish ralph vs task)
|
|
1177
|
+
var _connSource = (loopState.wizardData && loopState.wizardData.source === "task") ? null : "ralph";
|
|
1118
1178
|
sendTo(ws, {
|
|
1119
1179
|
type: "ralph_phase",
|
|
1120
1180
|
phase: loopState.phase,
|
|
1121
1181
|
wizardData: loopState.wizardData,
|
|
1122
1182
|
craftingSessionId: loopState.craftingSessionId || null,
|
|
1183
|
+
source: _connSource,
|
|
1123
1184
|
});
|
|
1124
1185
|
if (loopState.phase === "crafting" || loopState.phase === "approval") {
|
|
1125
1186
|
var _hasPrompt = false;
|
|
@@ -1129,11 +1190,12 @@ function attachLoop(ctx) {
|
|
|
1129
1190
|
try { fs.accessSync(path.join(_lDir, "PROMPT.md")); _hasPrompt = true; } catch (e) {}
|
|
1130
1191
|
try { fs.accessSync(path.join(_lDir, "JUDGE.md")); _hasJudge = true; } catch (e) {}
|
|
1131
1192
|
}
|
|
1193
|
+
var _connBothReady = _connIsSimple ? _hasPrompt : (_hasPrompt && _hasJudge);
|
|
1132
1194
|
sendTo(ws, {
|
|
1133
1195
|
type: "ralph_files_status",
|
|
1134
1196
|
promptReady: _hasPrompt,
|
|
1135
1197
|
judgeReady: _hasJudge,
|
|
1136
|
-
bothReady:
|
|
1198
|
+
bothReady: _connBothReady,
|
|
1137
1199
|
taskId: loopState.loopId,
|
|
1138
1200
|
});
|
|
1139
1201
|
}
|
|
@@ -569,6 +569,10 @@ function attachMateInteraction(ctx) {
|
|
|
569
569
|
onDone: mentionCallbacks.onDone,
|
|
570
570
|
onError: mentionCallbacks.onError,
|
|
571
571
|
canUseTool: function (toolName, input, toolOpts) {
|
|
572
|
+
// Full-auto mode: auto-approve everything except AskUserQuestion
|
|
573
|
+
if (sm.currentPermissionMode === "bypassPermissions" && toolName !== "AskUserQuestion") {
|
|
574
|
+
return Promise.resolve({ behavior: "allow", updatedInput: input });
|
|
575
|
+
}
|
|
572
576
|
// Use the shared whitelist from sdk-bridge (read-only tools + safe bash commands)
|
|
573
577
|
var whitelisted = sdk.checkToolWhitelist(toolName, input);
|
|
574
578
|
if (whitelisted) return Promise.resolve(whitelisted);
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// project-notifications.js - Global notification queue, event-based creation
|
|
2
|
+
// Single instance shared by all projects. All formatting logic lives here.
|
|
3
|
+
// Follows attachXxx(ctx) pattern per MODULE_MAP.md
|
|
4
|
+
|
|
5
|
+
var fs = require("fs");
|
|
6
|
+
var path = require("path");
|
|
7
|
+
var config = require("./config");
|
|
8
|
+
|
|
9
|
+
var NOTIF_FILE = path.join(config.CONFIG_DIR, "notifications.json");
|
|
10
|
+
var REMINDER_INTERVAL = 60 * 60 * 1000; // 1 hour
|
|
11
|
+
|
|
12
|
+
function generateId() {
|
|
13
|
+
var rand = Math.random().toString(36).substring(2, 8);
|
|
14
|
+
return "n_" + Date.now() + "_" + rand;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// ========================================================
|
|
18
|
+
// Event -> notification formatters
|
|
19
|
+
// ========================================================
|
|
20
|
+
|
|
21
|
+
var formatters = {
|
|
22
|
+
loop_complete: function (data) {
|
|
23
|
+
var reason = data.reason || "complete";
|
|
24
|
+
var body = (reason === "pass" || reason === "complete")
|
|
25
|
+
? "Completed after " + (data.iterations || 0) + " iteration(s)"
|
|
26
|
+
: reason === "max_iterations"
|
|
27
|
+
? "Reached max iterations (" + (data.maxIterations || "?") + ")"
|
|
28
|
+
: reason === "stopped"
|
|
29
|
+
? "Stopped by user"
|
|
30
|
+
: "Ended due to error";
|
|
31
|
+
return {
|
|
32
|
+
type: reason === "error" ? "loop_error" : "loop_complete",
|
|
33
|
+
title: (data.name || "Loop") + " " + (reason === "error" ? "failed" : "complete"),
|
|
34
|
+
body: body,
|
|
35
|
+
meta: { reason: reason, iterations: data.iterations },
|
|
36
|
+
};
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
response_done: function (data) {
|
|
40
|
+
return {
|
|
41
|
+
type: "response_done",
|
|
42
|
+
title: data.title || "Response ready",
|
|
43
|
+
body: data.preview || "",
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
permission_request: function (data) {
|
|
48
|
+
return {
|
|
49
|
+
type: "permission_request",
|
|
50
|
+
title: data.title || "Permission requested",
|
|
51
|
+
body: data.body || "",
|
|
52
|
+
meta: { requestId: data.requestId, toolName: data.toolName, toolInput: data.toolInput || null },
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
mate_dm: function (data) {
|
|
57
|
+
return {
|
|
58
|
+
type: "mate_dm",
|
|
59
|
+
title: data.senderName || "Message",
|
|
60
|
+
body: data.preview || "Sent you a message",
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// ========================================================
|
|
66
|
+
// Module (global singleton)
|
|
67
|
+
// ========================================================
|
|
68
|
+
|
|
69
|
+
function attachNotifications(ctx) {
|
|
70
|
+
var broadcastAll = ctx.broadcastAll;
|
|
71
|
+
var pushModule = ctx.pushModule;
|
|
72
|
+
|
|
73
|
+
var notifications = loadNotifications();
|
|
74
|
+
var reminderTimer = null;
|
|
75
|
+
|
|
76
|
+
function loadNotifications() {
|
|
77
|
+
try {
|
|
78
|
+
var raw = fs.readFileSync(NOTIF_FILE, "utf8");
|
|
79
|
+
var parsed = JSON.parse(raw);
|
|
80
|
+
if (Array.isArray(parsed)) return parsed;
|
|
81
|
+
} catch (e) {}
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function saveNotifications() {
|
|
86
|
+
var tmp = NOTIF_FILE + ".tmp";
|
|
87
|
+
try {
|
|
88
|
+
fs.writeFileSync(tmp, JSON.stringify(notifications, null, 2));
|
|
89
|
+
fs.renameSync(tmp, NOTIF_FILE);
|
|
90
|
+
} catch (e) {
|
|
91
|
+
console.error("[notifications] Failed to save:", e.message);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function getUnreadCount() {
|
|
96
|
+
return notifications.length;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// --- Core API ---
|
|
100
|
+
|
|
101
|
+
function notify(event, data) {
|
|
102
|
+
var formatter = formatters[event];
|
|
103
|
+
if (!formatter) {
|
|
104
|
+
console.warn("[notifications] Unknown event: " + event);
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
var formatted = formatter(data || {});
|
|
108
|
+
if (!formatted) return null;
|
|
109
|
+
|
|
110
|
+
var notif = {
|
|
111
|
+
id: generateId(),
|
|
112
|
+
type: formatted.type || event,
|
|
113
|
+
title: formatted.title || "",
|
|
114
|
+
body: formatted.body || "",
|
|
115
|
+
slug: data.slug || "",
|
|
116
|
+
sessionId: data.sessionId || null,
|
|
117
|
+
mateId: data.mateId || null,
|
|
118
|
+
ownerId: data.ownerId || null,
|
|
119
|
+
createdAt: Date.now(),
|
|
120
|
+
meta: formatted.meta || {},
|
|
121
|
+
};
|
|
122
|
+
notifications.unshift(notif);
|
|
123
|
+
saveNotifications();
|
|
124
|
+
broadcastAll({
|
|
125
|
+
type: "notification_created",
|
|
126
|
+
notification: notif,
|
|
127
|
+
unreadCount: getUnreadCount(),
|
|
128
|
+
});
|
|
129
|
+
return notif;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function dismiss(ids) {
|
|
133
|
+
var before = notifications.length;
|
|
134
|
+
notifications = notifications.filter(function(n) { return ids.indexOf(n.id) === -1; });
|
|
135
|
+
if (notifications.length !== before) {
|
|
136
|
+
saveNotifications();
|
|
137
|
+
broadcastAll({
|
|
138
|
+
type: "notification_dismissed",
|
|
139
|
+
ids: ids,
|
|
140
|
+
unreadCount: getUnreadCount(),
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function dismissAll() {
|
|
146
|
+
if (notifications.length > 0) {
|
|
147
|
+
notifications = [];
|
|
148
|
+
saveNotifications();
|
|
149
|
+
broadcastAll({ type: "notification_dismissed_all", unreadCount: 0 });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// --- Periodic reminder ---
|
|
154
|
+
|
|
155
|
+
function startReminder() {
|
|
156
|
+
if (reminderTimer) return;
|
|
157
|
+
reminderTimer = setInterval(function () {
|
|
158
|
+
var count = getUnreadCount();
|
|
159
|
+
if (count > 0 && pushModule) {
|
|
160
|
+
pushModule.sendPush({
|
|
161
|
+
type: "reminder",
|
|
162
|
+
title: "Clay",
|
|
163
|
+
body: count + " unread notification" + (count > 1 ? "s" : ""),
|
|
164
|
+
tag: "clay-notif-reminder",
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}, REMINDER_INTERVAL);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function stopReminder() {
|
|
171
|
+
if (reminderTimer) { clearInterval(reminderTimer); reminderTimer = null; }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
startReminder();
|
|
175
|
+
|
|
176
|
+
// --- Connection state (called per-client with sendTo) ---
|
|
177
|
+
|
|
178
|
+
function sendConnectionState(ws, sendTo) {
|
|
179
|
+
if (!sendTo) return;
|
|
180
|
+
sendTo(ws, {
|
|
181
|
+
type: "notifications_state",
|
|
182
|
+
notifications: notifications,
|
|
183
|
+
unreadCount: getUnreadCount(),
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// --- Message handler ---
|
|
188
|
+
|
|
189
|
+
function handleNotificationMessage(ws, msg) {
|
|
190
|
+
if (msg.type === "notification_dismiss") {
|
|
191
|
+
dismiss(msg.ids || []);
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
if (msg.type === "notification_dismiss_all") {
|
|
195
|
+
dismissAll();
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
notify: notify,
|
|
203
|
+
getUnreadCount: getUnreadCount,
|
|
204
|
+
sendConnectionState: sendConnectionState,
|
|
205
|
+
handleNotificationMessage: handleNotificationMessage,
|
|
206
|
+
stopReminder: stopReminder,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
module.exports = { attachNotifications: attachNotifications };
|
package/lib/project-sessions.js
CHANGED
|
@@ -681,12 +681,15 @@ function attachSessions(ctx) {
|
|
|
681
681
|
}
|
|
682
682
|
|
|
683
683
|
if (msg.type === "permission_response") {
|
|
684
|
-
var session = getSessionForWs(ws);
|
|
685
|
-
if (!session) return true;
|
|
686
684
|
var requestId = msg.requestId;
|
|
687
685
|
var decision = msg.decision;
|
|
686
|
+
// Look up session by requestId index (O(1)), fall back to active session
|
|
687
|
+
var sessionId = sm.permissionRequestIndex[requestId];
|
|
688
|
+
var session = sessionId ? sm.sessions.get(sessionId) : getSessionForWs(ws);
|
|
689
|
+
if (!session) return true;
|
|
688
690
|
var pending = session.pendingPermissions[requestId];
|
|
689
691
|
if (!pending) return true;
|
|
692
|
+
delete sm.permissionRequestIndex[requestId];
|
|
690
693
|
delete session.pendingPermissions[requestId];
|
|
691
694
|
onProcessingChanged(); // update cross-project permission badge
|
|
692
695
|
|
|
@@ -268,7 +268,7 @@ function attachUserMessage(ctx) {
|
|
|
268
268
|
nowSession.scheduledMessage = null;
|
|
269
269
|
console.log("[project] Scheduled message sent immediately for session " + nowSession.localId);
|
|
270
270
|
sm.sendAndRecord(nowSession, { type: "scheduled_message_sent" });
|
|
271
|
-
var userMsg = { type: "user_message", text: schedText };
|
|
271
|
+
var userMsg = { type: "user_message", text: schedText, _ts: Date.now() };
|
|
272
272
|
nowSession.history.push(userMsg);
|
|
273
273
|
sm.appendToSessionFile(nowSession, userMsg);
|
|
274
274
|
sendToSession(nowSession.localId, userMsg);
|
|
@@ -320,6 +320,7 @@ function attachUserMessage(ctx) {
|
|
|
320
320
|
if (msg.pastes && msg.pastes.length > 0) {
|
|
321
321
|
userMsg2.pastes = msg.pastes;
|
|
322
322
|
}
|
|
323
|
+
if (!userMsg2._ts) userMsg2._ts = Date.now();
|
|
323
324
|
session.history.push(userMsg2);
|
|
324
325
|
sm.appendToSessionFile(session, userMsg2);
|
|
325
326
|
sendToSessionOthers(ws, session.localId, hydrateImageRefs(userMsg2));
|