clay-server 2.5.0 → 2.6.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/bin/claude-relay.js +6 -0
- package/bin/cli.js +26 -8
- package/lib/cli-sessions.js +2 -7
- package/lib/config.js +78 -5
- package/lib/daemon.js +233 -2
- package/lib/notes.js +3 -2
- package/lib/project.js +465 -28
- package/lib/public/app.js +187 -24
- package/lib/public/css/base.css +61 -61
- package/lib/public/css/diff.css +3 -4
- package/lib/public/css/filebrowser.css +362 -2
- package/lib/public/css/icon-strip.css +317 -1
- package/lib/public/css/input.css +127 -50
- package/lib/public/css/messages.css +1 -1
- package/lib/public/css/mobile-nav.css +8 -4
- package/lib/public/css/overlays.css +9 -6
- package/lib/public/css/server-settings.css +67 -20
- package/lib/public/css/sidebar.css +10 -101
- package/lib/public/css/skills.css +730 -0
- package/lib/public/css/title-bar.css +82 -4
- package/lib/public/index.html +277 -70
- package/lib/public/modules/input.js +119 -56
- package/lib/public/modules/project-settings.js +906 -0
- package/lib/public/modules/server-settings.js +409 -53
- package/lib/public/modules/sidebar.js +720 -1
- package/lib/public/modules/skills.js +710 -0
- package/lib/public/modules/terminal.js +7 -0
- package/lib/public/modules/theme.js +88 -89
- package/lib/public/style.css +1 -0
- package/lib/sdk-bridge.js +18 -7
- package/lib/server.js +305 -1
- package/lib/sessions.js +9 -4
- package/lib/utils.js +18 -0
- package/package.json +3 -2
package/lib/project.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
var fs = require("fs");
|
|
2
2
|
var path = require("path");
|
|
3
|
+
var os = require("os");
|
|
4
|
+
var crypto = require("crypto");
|
|
3
5
|
var { createSessionManager } = require("./sessions");
|
|
4
6
|
var { createSDKBridge } = require("./sdk-bridge");
|
|
5
7
|
var { createTerminalManager } = require("./terminal-manager");
|
|
6
8
|
var { createNotesManager } = require("./notes");
|
|
7
9
|
var { fetchLatestVersion, isNewer } = require("./updater");
|
|
8
|
-
var { execFileSync } = require("child_process");
|
|
10
|
+
var { execFileSync, spawn } = require("child_process");
|
|
11
|
+
|
|
12
|
+
var MAX_UPLOAD_BYTES = 50 * 1024 * 1024; // 50 MB
|
|
9
13
|
|
|
10
14
|
// SDK loaded dynamically (ESM module)
|
|
11
15
|
var sdkModule = null;
|
|
@@ -63,6 +67,7 @@ function createProjectContext(opts) {
|
|
|
63
67
|
var slug = opts.slug;
|
|
64
68
|
var project = path.basename(cwd);
|
|
65
69
|
var title = opts.title || null;
|
|
70
|
+
var icon = opts.icon || null;
|
|
66
71
|
var pushModule = opts.pushModule || null;
|
|
67
72
|
var debug = opts.debug || false;
|
|
68
73
|
var dangerouslySkipPermissions = opts.dangerouslySkipPermissions || false;
|
|
@@ -70,6 +75,7 @@ function createProjectContext(opts) {
|
|
|
70
75
|
var lanHost = opts.lanHost || null;
|
|
71
76
|
var getProjectCount = opts.getProjectCount || function () { return 1; };
|
|
72
77
|
var getProjectList = opts.getProjectList || function () { return []; };
|
|
78
|
+
var onProcessingChanged = opts.onProcessingChanged || function () {};
|
|
73
79
|
var latestVersion = null;
|
|
74
80
|
|
|
75
81
|
// --- Per-project clients ---
|
|
@@ -192,8 +198,17 @@ function createProjectContext(opts) {
|
|
|
192
198
|
|
|
193
199
|
// --- Session manager ---
|
|
194
200
|
var sm = createSessionManager({ cwd: cwd, send: send });
|
|
195
|
-
|
|
196
|
-
|
|
201
|
+
var _projMode = typeof opts.onGetProjectDefaultMode === "function" ? opts.onGetProjectDefaultMode(slug) : null;
|
|
202
|
+
var _srvMode = typeof opts.onGetServerDefaultMode === "function" ? opts.onGetServerDefaultMode() : null;
|
|
203
|
+
sm.currentPermissionMode = (_projMode && _projMode.mode) || (_srvMode && _srvMode.mode) || "default";
|
|
204
|
+
|
|
205
|
+
var _projEffort = typeof opts.onGetProjectDefaultEffort === "function" ? opts.onGetProjectDefaultEffort(slug) : null;
|
|
206
|
+
var _srvEffort = typeof opts.onGetServerDefaultEffort === "function" ? opts.onGetServerDefaultEffort() : null;
|
|
207
|
+
sm.currentEffort = (_projEffort && _projEffort.effort) || (_srvEffort && _srvEffort.effort) || "medium";
|
|
208
|
+
|
|
209
|
+
var _projModel = typeof opts.onGetProjectDefaultModel === "function" ? opts.onGetProjectDefaultModel(slug) : null;
|
|
210
|
+
var _srvModel = typeof opts.onGetServerDefaultModel === "function" ? opts.onGetServerDefaultModel() : null;
|
|
211
|
+
sm._savedDefaultModel = (_projModel && _projModel.model) || (_srvModel && _srvModel.model) || null;
|
|
197
212
|
|
|
198
213
|
// --- SDK bridge ---
|
|
199
214
|
var sdk = createSDKBridge({
|
|
@@ -204,6 +219,7 @@ function createProjectContext(opts) {
|
|
|
204
219
|
pushModule: pushModule,
|
|
205
220
|
getSDK: getSDK,
|
|
206
221
|
dangerouslySkipPermissions: dangerouslySkipPermissions,
|
|
222
|
+
onProcessingChanged: onProcessingChanged,
|
|
207
223
|
});
|
|
208
224
|
|
|
209
225
|
// --- Terminal manager ---
|
|
@@ -234,7 +250,7 @@ function createProjectContext(opts) {
|
|
|
234
250
|
if (sm.currentModel) {
|
|
235
251
|
sendTo(ws, { type: "model_info", model: sm.currentModel, models: sm.availableModels || [] });
|
|
236
252
|
}
|
|
237
|
-
sendTo(ws, { type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "
|
|
253
|
+
sendTo(ws, { type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [] });
|
|
238
254
|
sendTo(ws, { type: "term_list", terminals: tm.list() });
|
|
239
255
|
sendTo(ws, { type: "notes_list", notes: nm.list() });
|
|
240
256
|
|
|
@@ -489,13 +505,70 @@ function createProjectContext(opts) {
|
|
|
489
505
|
return;
|
|
490
506
|
}
|
|
491
507
|
|
|
508
|
+
if (msg.type === "set_server_default_model" && msg.model) {
|
|
509
|
+
if (typeof opts.onSetServerDefaultModel === "function") {
|
|
510
|
+
opts.onSetServerDefaultModel(msg.model);
|
|
511
|
+
}
|
|
512
|
+
var session = sm.getActiveSession();
|
|
513
|
+
if (session) {
|
|
514
|
+
sdk.setModel(session, msg.model);
|
|
515
|
+
}
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (msg.type === "set_project_default_model" && msg.model) {
|
|
520
|
+
if (typeof opts.onSetProjectDefaultModel === "function") {
|
|
521
|
+
opts.onSetProjectDefaultModel(slug, msg.model);
|
|
522
|
+
}
|
|
523
|
+
var session = sm.getActiveSession();
|
|
524
|
+
if (session) {
|
|
525
|
+
sdk.setModel(session, msg.model);
|
|
526
|
+
}
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
|
|
492
530
|
if (msg.type === "set_permission_mode" && msg.mode) {
|
|
531
|
+
// When dangerouslySkipPermissions is active, don't allow UI to change mode
|
|
532
|
+
if (dangerouslySkipPermissions) {
|
|
533
|
+
send({ type: "config_state", model: sm.currentModel || "", mode: "bypassPermissions", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [] });
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
493
536
|
sm.currentPermissionMode = msg.mode;
|
|
494
537
|
var session = sm.getActiveSession();
|
|
495
538
|
if (session) {
|
|
496
539
|
sdk.setPermissionMode(session, msg.mode);
|
|
497
540
|
}
|
|
498
|
-
send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "
|
|
541
|
+
send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [] });
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if (msg.type === "set_server_default_mode" && msg.mode) {
|
|
546
|
+
if (typeof opts.onSetServerDefaultMode === "function") {
|
|
547
|
+
opts.onSetServerDefaultMode(msg.mode);
|
|
548
|
+
}
|
|
549
|
+
if (!dangerouslySkipPermissions) {
|
|
550
|
+
sm.currentPermissionMode = msg.mode;
|
|
551
|
+
var session = sm.getActiveSession();
|
|
552
|
+
if (session) {
|
|
553
|
+
sdk.setPermissionMode(session, msg.mode);
|
|
554
|
+
}
|
|
555
|
+
send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [] });
|
|
556
|
+
}
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (msg.type === "set_project_default_mode" && msg.mode) {
|
|
561
|
+
if (typeof opts.onSetProjectDefaultMode === "function") {
|
|
562
|
+
opts.onSetProjectDefaultMode(slug, msg.mode);
|
|
563
|
+
}
|
|
564
|
+
if (!dangerouslySkipPermissions) {
|
|
565
|
+
sm.currentPermissionMode = msg.mode;
|
|
566
|
+
var session = sm.getActiveSession();
|
|
567
|
+
if (session) {
|
|
568
|
+
sdk.setPermissionMode(session, msg.mode);
|
|
569
|
+
}
|
|
570
|
+
send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [] });
|
|
571
|
+
}
|
|
499
572
|
return;
|
|
500
573
|
}
|
|
501
574
|
|
|
@@ -505,9 +578,27 @@ function createProjectContext(opts) {
|
|
|
505
578
|
return;
|
|
506
579
|
}
|
|
507
580
|
|
|
581
|
+
if (msg.type === "set_server_default_effort" && msg.effort) {
|
|
582
|
+
if (typeof opts.onSetServerDefaultEffort === "function") {
|
|
583
|
+
opts.onSetServerDefaultEffort(msg.effort);
|
|
584
|
+
}
|
|
585
|
+
sm.currentEffort = msg.effort;
|
|
586
|
+
send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort, betas: sm.currentBetas || [] });
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (msg.type === "set_project_default_effort" && msg.effort) {
|
|
591
|
+
if (typeof opts.onSetProjectDefaultEffort === "function") {
|
|
592
|
+
opts.onSetProjectDefaultEffort(slug, msg.effort);
|
|
593
|
+
}
|
|
594
|
+
sm.currentEffort = msg.effort;
|
|
595
|
+
send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort, betas: sm.currentBetas || [] });
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
|
|
508
599
|
if (msg.type === "set_betas") {
|
|
509
600
|
sm.currentBetas = msg.betas || [];
|
|
510
|
-
send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "
|
|
601
|
+
send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas });
|
|
511
602
|
return;
|
|
512
603
|
}
|
|
513
604
|
|
|
@@ -593,6 +684,7 @@ function createProjectContext(opts) {
|
|
|
593
684
|
session.pendingPermissions = {};
|
|
594
685
|
session.pendingAskUser = {};
|
|
595
686
|
session.isProcessing = false;
|
|
687
|
+
onProcessingChanged();
|
|
596
688
|
|
|
597
689
|
sm.saveSessionFile(session);
|
|
598
690
|
sm.switchSession(session.localId);
|
|
@@ -641,7 +733,7 @@ function createProjectContext(opts) {
|
|
|
641
733
|
if (decision === "allow_accept_edits") {
|
|
642
734
|
sdk.setPermissionMode(session, "acceptEdits");
|
|
643
735
|
sm.currentPermissionMode = "acceptEdits";
|
|
644
|
-
send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "
|
|
736
|
+
send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [] });
|
|
645
737
|
pending.resolve({ behavior: "allow", updatedInput: pending.toolInput });
|
|
646
738
|
sm.sendAndRecord(session, { type: "permission_resolved", requestId: requestId, decision: decision });
|
|
647
739
|
return;
|
|
@@ -653,14 +745,24 @@ function createProjectContext(opts) {
|
|
|
653
745
|
pending.resolve({ behavior: "deny", message: "User chose to clear context and restart" });
|
|
654
746
|
sm.sendAndRecord(session, { type: "permission_resolved", requestId: requestId, decision: decision });
|
|
655
747
|
|
|
656
|
-
// Abort the old session's query
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
748
|
+
// Abort the old session's query — but defer to next tick so the SDK's
|
|
749
|
+
// deny write (scheduled as microtask by pending.resolve) completes first.
|
|
750
|
+
// Aborting synchronously would kill the subprocess before the write,
|
|
751
|
+
// causing an "Operation aborted" crash in the SDK.
|
|
660
752
|
session.isProcessing = false;
|
|
753
|
+
onProcessingChanged();
|
|
661
754
|
session.pendingPermissions = {};
|
|
662
755
|
session.pendingAskUser = {};
|
|
663
756
|
sm.broadcastSessionList();
|
|
757
|
+
setImmediate(function () {
|
|
758
|
+
if (session.abortController) {
|
|
759
|
+
session.abortController.abort();
|
|
760
|
+
}
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
// Update permission mode for the new session
|
|
764
|
+
sm.currentPermissionMode = "acceptEdits";
|
|
765
|
+
send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [] });
|
|
664
766
|
|
|
665
767
|
// Build prompt from plan content (sent from client) or plan file path
|
|
666
768
|
var clientPlanContent = msg.planContent || "";
|
|
@@ -672,24 +774,34 @@ function createProjectContext(opts) {
|
|
|
672
774
|
planPrompt = "Execute the plan in " + planFilePath + ". Do NOT re-enter plan mode — read the plan file and implement it step by step.";
|
|
673
775
|
}
|
|
674
776
|
|
|
675
|
-
// Wait
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
777
|
+
// Wait for old query stream to fully terminate, then create new session + send plan
|
|
778
|
+
var oldStreamPromise = session.streamPromise || Promise.resolve();
|
|
779
|
+
Promise.race([
|
|
780
|
+
oldStreamPromise,
|
|
781
|
+
new Promise(function (resolve) { setTimeout(resolve, 3000); }),
|
|
782
|
+
]).then(function () {
|
|
783
|
+
try {
|
|
784
|
+
var newSession = sm.createSession();
|
|
785
|
+
// Send the plan as the first user message (with planContent for UI rendering)
|
|
786
|
+
var userMsg = { type: "user_message", text: planPrompt, planContent: clientPlanContent || null };
|
|
787
|
+
newSession.history.push(userMsg);
|
|
788
|
+
sm.appendToSessionFile(newSession, userMsg);
|
|
789
|
+
newSession.title = "Plan execution (cleared context)";
|
|
790
|
+
sm.saveSessionFile(newSession);
|
|
791
|
+
sm.broadcastSessionList();
|
|
792
|
+
send(userMsg);
|
|
686
793
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
794
|
+
newSession.isProcessing = true;
|
|
795
|
+
onProcessingChanged();
|
|
796
|
+
newSession.sentToolResults = {};
|
|
797
|
+
send({ type: "status", status: "processing" });
|
|
798
|
+
newSession.acceptEditsAfterStart = true;
|
|
799
|
+
sdk.startQuery(newSession, planPrompt);
|
|
800
|
+
} catch (e) {
|
|
801
|
+
console.error("[project] Error starting plan execution:", e);
|
|
802
|
+
send({ type: "error", text: "Failed to start plan execution: " + (e.message || e) });
|
|
803
|
+
}
|
|
804
|
+
});
|
|
693
805
|
return;
|
|
694
806
|
}
|
|
695
807
|
|
|
@@ -709,6 +821,7 @@ function createProjectContext(opts) {
|
|
|
709
821
|
|
|
710
822
|
if (!session.isProcessing) {
|
|
711
823
|
session.isProcessing = true;
|
|
824
|
+
onProcessingChanged();
|
|
712
825
|
session.sentToolResults = {};
|
|
713
826
|
send({ type: "status", status: "processing" });
|
|
714
827
|
if (!session.queryInstance) {
|
|
@@ -820,6 +933,52 @@ function createProjectContext(opts) {
|
|
|
820
933
|
return;
|
|
821
934
|
}
|
|
822
935
|
|
|
936
|
+
// --- Reorder projects ---
|
|
937
|
+
if (msg.type === "reorder_projects") {
|
|
938
|
+
var slugs = msg.slugs;
|
|
939
|
+
if (!Array.isArray(slugs) || slugs.length === 0) {
|
|
940
|
+
sendTo(ws, { type: "reorder_projects_result", ok: false, error: "Missing slugs" });
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
if (typeof opts.onReorderProjects === "function") {
|
|
944
|
+
var reorderResult = opts.onReorderProjects(slugs);
|
|
945
|
+
sendTo(ws, { type: "reorder_projects_result", ok: reorderResult.ok, error: reorderResult.error });
|
|
946
|
+
} else {
|
|
947
|
+
sendTo(ws, { type: "reorder_projects_result", ok: false, error: "Not supported" });
|
|
948
|
+
}
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
// --- Set project title (rename) ---
|
|
953
|
+
if (msg.type === "set_project_title") {
|
|
954
|
+
if (!msg.slug) {
|
|
955
|
+
sendTo(ws, { type: "set_project_title_result", ok: false, error: "Missing slug" });
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
if (typeof opts.onSetProjectTitle === "function") {
|
|
959
|
+
var titleResult = opts.onSetProjectTitle(msg.slug, msg.title || null);
|
|
960
|
+
sendTo(ws, { type: "set_project_title_result", ok: titleResult.ok, slug: msg.slug, error: titleResult.error });
|
|
961
|
+
} else {
|
|
962
|
+
sendTo(ws, { type: "set_project_title_result", ok: false, error: "Not supported" });
|
|
963
|
+
}
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// --- Set project icon (emoji) ---
|
|
968
|
+
if (msg.type === "set_project_icon") {
|
|
969
|
+
if (!msg.slug) {
|
|
970
|
+
sendTo(ws, { type: "set_project_icon_result", ok: false, error: "Missing slug" });
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
if (typeof opts.onSetProjectIcon === "function") {
|
|
974
|
+
var iconResult = opts.onSetProjectIcon(msg.slug, msg.icon || null);
|
|
975
|
+
sendTo(ws, { type: "set_project_icon_result", ok: iconResult.ok, slug: msg.slug, error: iconResult.error });
|
|
976
|
+
} else {
|
|
977
|
+
sendTo(ws, { type: "set_project_icon_result", ok: false, error: "Not supported" });
|
|
978
|
+
}
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
|
|
823
982
|
// --- Daemon config (server settings) ---
|
|
824
983
|
if (msg.type === "get_daemon_config") {
|
|
825
984
|
if (typeof opts.onGetDaemonConfig === "function") {
|
|
@@ -915,6 +1074,98 @@ function createProjectContext(opts) {
|
|
|
915
1074
|
return;
|
|
916
1075
|
}
|
|
917
1076
|
|
|
1077
|
+
// --- File write ---
|
|
1078
|
+
if (msg.type === "fs_write") {
|
|
1079
|
+
var fsWriteFile = safePath(cwd, msg.path);
|
|
1080
|
+
if (!fsWriteFile) {
|
|
1081
|
+
sendTo(ws, { type: "fs_write_result", path: msg.path, ok: false, error: "Access denied" });
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
try {
|
|
1085
|
+
fs.writeFileSync(fsWriteFile, msg.content || "", "utf8");
|
|
1086
|
+
sendTo(ws, { type: "fs_write_result", path: msg.path, ok: true });
|
|
1087
|
+
} catch (e) {
|
|
1088
|
+
sendTo(ws, { type: "fs_write_result", path: msg.path, ok: false, error: e.message });
|
|
1089
|
+
}
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// --- Project environment variables ---
|
|
1094
|
+
if (msg.type === "get_project_env") {
|
|
1095
|
+
var envrc = "";
|
|
1096
|
+
var hasEnvrc = false;
|
|
1097
|
+
if (typeof opts.onGetProjectEnv === "function") {
|
|
1098
|
+
var envResult = opts.onGetProjectEnv(msg.slug);
|
|
1099
|
+
envrc = envResult.envrc || "";
|
|
1100
|
+
}
|
|
1101
|
+
try {
|
|
1102
|
+
var envrcPath = path.join(cwd, ".envrc");
|
|
1103
|
+
hasEnvrc = fs.existsSync(envrcPath);
|
|
1104
|
+
} catch (e) {}
|
|
1105
|
+
sendTo(ws, { type: "project_env_result", slug: msg.slug, envrc: envrc, hasEnvrc: hasEnvrc });
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
if (msg.type === "set_project_env") {
|
|
1110
|
+
if (typeof opts.onSetProjectEnv === "function") {
|
|
1111
|
+
var setResult = opts.onSetProjectEnv(msg.slug, msg.envrc || "");
|
|
1112
|
+
sendTo(ws, { type: "set_project_env_result", ok: setResult.ok, slug: msg.slug, error: setResult.error });
|
|
1113
|
+
} else {
|
|
1114
|
+
sendTo(ws, { type: "set_project_env_result", ok: false, error: "Not supported" });
|
|
1115
|
+
}
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// --- Global CLAUDE.md ---
|
|
1120
|
+
if (msg.type === "read_global_claude_md") {
|
|
1121
|
+
var os = require("os");
|
|
1122
|
+
var globalMdPath = path.join(os.homedir(), ".claude", "CLAUDE.md");
|
|
1123
|
+
try {
|
|
1124
|
+
var globalMdContent = fs.readFileSync(globalMdPath, "utf8");
|
|
1125
|
+
sendTo(ws, { type: "global_claude_md_result", content: globalMdContent });
|
|
1126
|
+
} catch (e) {
|
|
1127
|
+
sendTo(ws, { type: "global_claude_md_result", error: e.message });
|
|
1128
|
+
}
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
if (msg.type === "write_global_claude_md") {
|
|
1133
|
+
var os2 = require("os");
|
|
1134
|
+
var globalMdDir = path.join(os2.homedir(), ".claude");
|
|
1135
|
+
var globalMdWritePath = path.join(globalMdDir, "CLAUDE.md");
|
|
1136
|
+
try {
|
|
1137
|
+
if (!fs.existsSync(globalMdDir)) {
|
|
1138
|
+
fs.mkdirSync(globalMdDir, { recursive: true });
|
|
1139
|
+
}
|
|
1140
|
+
fs.writeFileSync(globalMdWritePath, msg.content || "", "utf8");
|
|
1141
|
+
sendTo(ws, { type: "write_global_claude_md_result", ok: true });
|
|
1142
|
+
} catch (e) {
|
|
1143
|
+
sendTo(ws, { type: "write_global_claude_md_result", ok: false, error: e.message });
|
|
1144
|
+
}
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
// --- Shared environment variables ---
|
|
1149
|
+
if (msg.type === "get_shared_env") {
|
|
1150
|
+
var sharedEnvrc = "";
|
|
1151
|
+
if (typeof opts.onGetSharedEnv === "function") {
|
|
1152
|
+
var sharedResult = opts.onGetSharedEnv();
|
|
1153
|
+
sharedEnvrc = sharedResult.envrc || "";
|
|
1154
|
+
}
|
|
1155
|
+
sendTo(ws, { type: "shared_env_result", envrc: sharedEnvrc });
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
if (msg.type === "set_shared_env") {
|
|
1160
|
+
if (typeof opts.onSetSharedEnv === "function") {
|
|
1161
|
+
var sharedSetResult = opts.onSetSharedEnv(msg.envrc || "");
|
|
1162
|
+
sendTo(ws, { type: "set_shared_env_result", ok: sharedSetResult.ok, error: sharedSetResult.error });
|
|
1163
|
+
} else {
|
|
1164
|
+
sendTo(ws, { type: "set_shared_env_result", ok: false, error: "Not supported" });
|
|
1165
|
+
}
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
918
1169
|
// --- File watcher ---
|
|
919
1170
|
if (msg.type === "fs_watch") {
|
|
920
1171
|
if (msg.path) startFileWatch(msg.path);
|
|
@@ -1228,6 +1479,7 @@ function createProjectContext(opts) {
|
|
|
1228
1479
|
|
|
1229
1480
|
if (!session.isProcessing) {
|
|
1230
1481
|
session.isProcessing = true;
|
|
1482
|
+
onProcessingChanged();
|
|
1231
1483
|
session.sentToolResults = {};
|
|
1232
1484
|
send({ type: "status", status: "processing" });
|
|
1233
1485
|
if (!session.queryInstance) {
|
|
@@ -1254,6 +1506,54 @@ function createProjectContext(opts) {
|
|
|
1254
1506
|
|
|
1255
1507
|
// --- Handle project-scoped HTTP requests ---
|
|
1256
1508
|
function handleHTTP(req, res, urlPath) {
|
|
1509
|
+
// File upload
|
|
1510
|
+
if (req.method === "POST" && urlPath === "/api/upload") {
|
|
1511
|
+
parseJsonBody(req).then(function (body) {
|
|
1512
|
+
var fileName = body.name;
|
|
1513
|
+
var fileData = body.data; // base64
|
|
1514
|
+
if (!fileName || !fileData) {
|
|
1515
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1516
|
+
res.end('{"error":"missing name or data"}');
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
// Sanitize filename — strip path separators
|
|
1520
|
+
var safeName = path.basename(fileName).replace(/[^a-zA-Z0-9._\-\(\)\[\] ]/g, "_");
|
|
1521
|
+
if (!safeName) safeName = "upload";
|
|
1522
|
+
|
|
1523
|
+
// Check size
|
|
1524
|
+
var estimatedBytes = fileData.length * 0.75;
|
|
1525
|
+
if (estimatedBytes > MAX_UPLOAD_BYTES) {
|
|
1526
|
+
res.writeHead(413, { "Content-Type": "application/json" });
|
|
1527
|
+
res.end('{"error":"file too large (max 50MB)"}');
|
|
1528
|
+
return;
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
// Create tmp dir: os.tmpdir()/clay-{hash}/
|
|
1532
|
+
var cwdHash = crypto.createHash("sha256").update(cwd).digest("hex").substring(0, 12);
|
|
1533
|
+
var tmpDir = path.join(os.tmpdir(), "clay-" + cwdHash);
|
|
1534
|
+
try { fs.mkdirSync(tmpDir, { recursive: true }); } catch (e) {}
|
|
1535
|
+
|
|
1536
|
+
// Add timestamp prefix to avoid collisions
|
|
1537
|
+
var ts = Date.now();
|
|
1538
|
+
var destName = ts + "-" + safeName;
|
|
1539
|
+
var destPath = path.join(tmpDir, destName);
|
|
1540
|
+
|
|
1541
|
+
try {
|
|
1542
|
+
var buf = Buffer.from(fileData, "base64");
|
|
1543
|
+
fs.writeFileSync(destPath, buf);
|
|
1544
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1545
|
+
res.end(JSON.stringify({ path: destPath, name: safeName }));
|
|
1546
|
+
} catch (e) {
|
|
1547
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1548
|
+
res.end(JSON.stringify({ error: "failed to save: " + (e.message || e) }));
|
|
1549
|
+
}
|
|
1550
|
+
}).catch(function () {
|
|
1551
|
+
res.writeHead(400);
|
|
1552
|
+
res.end("Bad request");
|
|
1553
|
+
});
|
|
1554
|
+
return true;
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1257
1557
|
// Push subscribe
|
|
1258
1558
|
if (req.method === "POST" && urlPath === "/api/push-subscribe") {
|
|
1259
1559
|
parseJsonBody(req).then(function (body) {
|
|
@@ -1342,6 +1642,131 @@ function createProjectContext(opts) {
|
|
|
1342
1642
|
return true;
|
|
1343
1643
|
}
|
|
1344
1644
|
|
|
1645
|
+
// Install a skill (background spawn)
|
|
1646
|
+
if (req.method === "POST" && urlPath === "/api/install-skill") {
|
|
1647
|
+
parseJsonBody(req).then(function (body) {
|
|
1648
|
+
var url = body.url;
|
|
1649
|
+
var skill = body.skill;
|
|
1650
|
+
var scope = body.scope; // "global" or "project"
|
|
1651
|
+
if (!url || !skill || !scope) {
|
|
1652
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1653
|
+
res.end('{"error":"missing url, skill, or scope"}');
|
|
1654
|
+
return;
|
|
1655
|
+
}
|
|
1656
|
+
var spawnCwd = scope === "global" ? os.homedir() : cwd;
|
|
1657
|
+
var child = spawn("npx", ["skills", "add", url, "--skill", skill], {
|
|
1658
|
+
cwd: spawnCwd,
|
|
1659
|
+
stdio: "ignore",
|
|
1660
|
+
detached: false,
|
|
1661
|
+
});
|
|
1662
|
+
child.on("close", function (code) {
|
|
1663
|
+
var success = code === 0;
|
|
1664
|
+
send({
|
|
1665
|
+
type: "skill_installed",
|
|
1666
|
+
skill: skill,
|
|
1667
|
+
scope: scope,
|
|
1668
|
+
success: success,
|
|
1669
|
+
error: success ? null : "Process exited with code " + code,
|
|
1670
|
+
});
|
|
1671
|
+
});
|
|
1672
|
+
child.on("error", function (err) {
|
|
1673
|
+
send({
|
|
1674
|
+
type: "skill_installed",
|
|
1675
|
+
skill: skill,
|
|
1676
|
+
scope: scope,
|
|
1677
|
+
success: false,
|
|
1678
|
+
error: err.message,
|
|
1679
|
+
});
|
|
1680
|
+
});
|
|
1681
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1682
|
+
res.end('{"ok":true}');
|
|
1683
|
+
}).catch(function () {
|
|
1684
|
+
res.writeHead(400);
|
|
1685
|
+
res.end("Bad request");
|
|
1686
|
+
});
|
|
1687
|
+
return true;
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
// Uninstall a skill (remove directory)
|
|
1691
|
+
if (req.method === "POST" && urlPath === "/api/uninstall-skill") {
|
|
1692
|
+
parseJsonBody(req).then(function (body) {
|
|
1693
|
+
var skill = body.skill;
|
|
1694
|
+
var scope = body.scope; // "global" or "project"
|
|
1695
|
+
if (!skill || !scope) {
|
|
1696
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1697
|
+
res.end('{"error":"missing skill or scope"}');
|
|
1698
|
+
return;
|
|
1699
|
+
}
|
|
1700
|
+
var baseDir = scope === "global" ? os.homedir() : cwd;
|
|
1701
|
+
var skillDir = path.join(baseDir, ".claude", "skills", skill);
|
|
1702
|
+
// Safety: ensure skillDir is inside the expected .claude/skills directory
|
|
1703
|
+
var expectedParent = path.join(baseDir, ".claude", "skills");
|
|
1704
|
+
var resolved = path.resolve(skillDir);
|
|
1705
|
+
if (!resolved.startsWith(expectedParent + path.sep)) {
|
|
1706
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
1707
|
+
res.end('{"error":"invalid skill path"}');
|
|
1708
|
+
return;
|
|
1709
|
+
}
|
|
1710
|
+
try {
|
|
1711
|
+
fs.rmSync(resolved, { recursive: true, force: true });
|
|
1712
|
+
send({
|
|
1713
|
+
type: "skill_uninstalled",
|
|
1714
|
+
skill: skill,
|
|
1715
|
+
scope: scope,
|
|
1716
|
+
success: true,
|
|
1717
|
+
});
|
|
1718
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1719
|
+
res.end('{"ok":true}');
|
|
1720
|
+
} catch (err) {
|
|
1721
|
+
send({
|
|
1722
|
+
type: "skill_uninstalled",
|
|
1723
|
+
skill: skill,
|
|
1724
|
+
scope: scope,
|
|
1725
|
+
success: false,
|
|
1726
|
+
error: err.message,
|
|
1727
|
+
});
|
|
1728
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1729
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1730
|
+
}
|
|
1731
|
+
}).catch(function () {
|
|
1732
|
+
res.writeHead(400);
|
|
1733
|
+
res.end("Bad request");
|
|
1734
|
+
});
|
|
1735
|
+
return true;
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
// Installed skills (global + project)
|
|
1739
|
+
if (req.method === "GET" && urlPath === "/api/installed-skills") {
|
|
1740
|
+
var installed = {};
|
|
1741
|
+
var globalDir = path.join(os.homedir(), ".claude", "skills");
|
|
1742
|
+
var projectDir = path.join(cwd, ".claude", "skills");
|
|
1743
|
+
var scanDirs = [
|
|
1744
|
+
{ dir: globalDir, scope: "global" },
|
|
1745
|
+
{ dir: projectDir, scope: "project" },
|
|
1746
|
+
];
|
|
1747
|
+
for (var sd = 0; sd < scanDirs.length; sd++) {
|
|
1748
|
+
var entries;
|
|
1749
|
+
try { entries = fs.readdirSync(scanDirs[sd].dir, { withFileTypes: true }); } catch (e) { continue; }
|
|
1750
|
+
for (var si = 0; si < entries.length; si++) {
|
|
1751
|
+
var ent = entries[si];
|
|
1752
|
+
if (!ent.isDirectory() && !ent.isSymbolicLink()) continue;
|
|
1753
|
+
var mdPath = path.join(scanDirs[sd].dir, ent.name, "SKILL.md");
|
|
1754
|
+
try {
|
|
1755
|
+
fs.accessSync(mdPath, fs.constants.R_OK);
|
|
1756
|
+
if (!installed[ent.name]) {
|
|
1757
|
+
installed[ent.name] = { scope: scanDirs[sd].scope };
|
|
1758
|
+
} else {
|
|
1759
|
+
// project-level adds to existing global entry
|
|
1760
|
+
installed[ent.name].scope = "both";
|
|
1761
|
+
}
|
|
1762
|
+
} catch (e) {}
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1766
|
+
res.end(JSON.stringify({ installed: installed }));
|
|
1767
|
+
return true;
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1345
1770
|
// Info endpoint
|
|
1346
1771
|
if (req.method === "GET" && urlPath === "/info") {
|
|
1347
1772
|
res.writeHead(200, {
|
|
@@ -1374,6 +1799,12 @@ function createProjectContext(opts) {
|
|
|
1374
1799
|
try { ws.close(); } catch (e) {}
|
|
1375
1800
|
}
|
|
1376
1801
|
clients.clear();
|
|
1802
|
+
// Cleanup tmp upload directory
|
|
1803
|
+
try {
|
|
1804
|
+
var cwdHash = crypto.createHash("sha256").update(cwd).digest("hex").substring(0, 12);
|
|
1805
|
+
var tmpDir = path.join(os.tmpdir(), "clay-" + cwdHash);
|
|
1806
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1807
|
+
} catch (e) {}
|
|
1377
1808
|
}
|
|
1378
1809
|
|
|
1379
1810
|
// --- Status info ---
|
|
@@ -1388,6 +1819,7 @@ function createProjectContext(opts) {
|
|
|
1388
1819
|
path: cwd,
|
|
1389
1820
|
project: project,
|
|
1390
1821
|
title: title,
|
|
1822
|
+
icon: icon,
|
|
1391
1823
|
clients: clients.size,
|
|
1392
1824
|
sessions: sessionCount,
|
|
1393
1825
|
isProcessing: hasProcessing,
|
|
@@ -1399,6 +1831,10 @@ function createProjectContext(opts) {
|
|
|
1399
1831
|
send({ type: "info", cwd: cwd, slug: slug, project: title || project, version: currentVersion, debug: !!debug, lanHost: lanHost, projectCount: getProjectCount(), projects: getProjectList() });
|
|
1400
1832
|
}
|
|
1401
1833
|
|
|
1834
|
+
function setIcon(newIcon) {
|
|
1835
|
+
icon = newIcon || null;
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1402
1838
|
return {
|
|
1403
1839
|
cwd: cwd,
|
|
1404
1840
|
slug: slug,
|
|
@@ -1414,6 +1850,7 @@ function createProjectContext(opts) {
|
|
|
1414
1850
|
handleHTTP: handleHTTP,
|
|
1415
1851
|
getStatus: getStatus,
|
|
1416
1852
|
setTitle: setTitle,
|
|
1853
|
+
setIcon: setIcon,
|
|
1417
1854
|
warmup: function () { sdk.warmup(); },
|
|
1418
1855
|
destroy: destroy,
|
|
1419
1856
|
};
|