clay-server 2.22.0-beta.1 → 2.22.0-beta.3
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/lib/daemon.js +11 -21
- package/lib/project.js +90 -31
- package/lib/public/app.js +3 -0
- package/lib/public/css/icon-strip.css +107 -0
- package/lib/public/modules/sidebar.js +169 -0
- package/lib/server.js +31 -12
- package/package.json +1 -1
package/lib/daemon.js
CHANGED
|
@@ -160,11 +160,10 @@ var relay = createServer({
|
|
|
160
160
|
var slugs = config.projects.map(function (p) { return p.slug; });
|
|
161
161
|
var slug = generateSlug(absPath, slugs);
|
|
162
162
|
relay.addProject(absPath, slug);
|
|
163
|
-
var projectEntry = { path: absPath, slug: slug, addedAt: Date.now() };
|
|
164
|
-
//
|
|
165
|
-
if (wsUser && wsUser.id
|
|
163
|
+
var projectEntry = { path: absPath, slug: slug, addedAt: Date.now(), visibility: "private" };
|
|
164
|
+
// The user who adds a project always becomes the owner
|
|
165
|
+
if (wsUser && wsUser.id) {
|
|
166
166
|
projectEntry.ownerId = wsUser.id;
|
|
167
|
-
projectEntry.visibility = "private";
|
|
168
167
|
}
|
|
169
168
|
config.projects.push(projectEntry);
|
|
170
169
|
// Remove from removedProjects if present
|
|
@@ -237,15 +236,10 @@ var relay = createServer({
|
|
|
237
236
|
try { fs.rmSync(targetDir, { recursive: true, force: true }); } catch (ce) {}
|
|
238
237
|
return { ok: false, error: "Failed to create project: " + e.message };
|
|
239
238
|
}
|
|
240
|
-
// Register project
|
|
241
|
-
var projectEntry = { path: targetDir, slug: slug, addedAt: Date.now() };
|
|
239
|
+
// Register project - creator always becomes owner, default private
|
|
240
|
+
var projectEntry = { path: targetDir, slug: slug, addedAt: Date.now(), visibility: "private" };
|
|
242
241
|
if (wsUser && wsUser.id) {
|
|
243
|
-
|
|
244
|
-
projectEntry.ownerId = wsUser.id;
|
|
245
|
-
}
|
|
246
|
-
if (wsUser.role !== "admin") {
|
|
247
|
-
projectEntry.visibility = "private";
|
|
248
|
-
}
|
|
242
|
+
projectEntry.ownerId = wsUser.id;
|
|
249
243
|
}
|
|
250
244
|
relay.addProject(targetDir, slug);
|
|
251
245
|
config.projects.push(projectEntry);
|
|
@@ -312,15 +306,11 @@ var relay = createServer({
|
|
|
312
306
|
execSync("chown -R " + wsUser.linuxUser + ":" + wsUser.linuxUser + " " + JSON.stringify(targetDir));
|
|
313
307
|
} catch (e) {}
|
|
314
308
|
}
|
|
315
|
-
// Register project
|
|
316
|
-
|
|
309
|
+
// Register project - creator always becomes owner
|
|
310
|
+
// Creator always becomes owner, default private
|
|
311
|
+
var projectEntry = { path: targetDir, slug: slug, addedAt: Date.now(), visibility: "private" };
|
|
317
312
|
if (wsUser && wsUser.id) {
|
|
318
|
-
|
|
319
|
-
projectEntry.ownerId = wsUser.id;
|
|
320
|
-
}
|
|
321
|
-
if (wsUser.role !== "admin") {
|
|
322
|
-
projectEntry.visibility = "private";
|
|
323
|
-
}
|
|
313
|
+
projectEntry.ownerId = wsUser.id;
|
|
324
314
|
}
|
|
325
315
|
relay.addProject(targetDir, slug);
|
|
326
316
|
config.projects.push(projectEntry);
|
|
@@ -1041,7 +1031,7 @@ var ipc = createIPCServer(socketPath(), function (msg) {
|
|
|
1041
1031
|
var slugs = config.projects.map(function (p) { return p.slug; });
|
|
1042
1032
|
var slug = generateSlug(absPath, slugs);
|
|
1043
1033
|
relay.addProject(absPath, slug);
|
|
1044
|
-
config.projects.push({ path: absPath, slug: slug, addedAt: Date.now() });
|
|
1034
|
+
config.projects.push({ path: absPath, slug: slug, addedAt: Date.now(), visibility: "private" });
|
|
1045
1035
|
saveConfig(config);
|
|
1046
1036
|
try { syncClayrc(config.projects); } catch (e) {}
|
|
1047
1037
|
console.log("[daemon] Added project:", slug, "→", absPath);
|
package/lib/project.js
CHANGED
|
@@ -1171,6 +1171,15 @@ function createProjectContext(opts) {
|
|
|
1171
1171
|
setTimeout(function() { resumeLoop(); }, 500);
|
|
1172
1172
|
}
|
|
1173
1173
|
|
|
1174
|
+
// Auto-assign owner if project has none and a user connects (e.g. IPC-added projects)
|
|
1175
|
+
if (!projectOwnerId && ws._clayUser && ws._clayUser.id && !isMate) {
|
|
1176
|
+
projectOwnerId = ws._clayUser.id;
|
|
1177
|
+
if (opts.onProjectOwnerChanged) {
|
|
1178
|
+
opts.onProjectOwnerChanged(slug, projectOwnerId);
|
|
1179
|
+
}
|
|
1180
|
+
console.log("[project] Auto-assigned owner for " + slug + ": " + projectOwnerId);
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1174
1183
|
// Send cached state
|
|
1175
1184
|
var _userId = ws._clayUser ? ws._clayUser.id : null;
|
|
1176
1185
|
var _filteredProjects = getProjectList(_userId);
|
|
@@ -3856,11 +3865,11 @@ function createProjectContext(opts) {
|
|
|
3856
3865
|
initMemorySummary(mateCtx, mateId, function () {});
|
|
3857
3866
|
}
|
|
3858
3867
|
|
|
3859
|
-
// Build conversation content for gate check
|
|
3868
|
+
// Build conversation content for gate check
|
|
3860
3869
|
var userQ = userQuestion || "(unknown)";
|
|
3861
3870
|
var mateR = mateResponse || "(unknown)";
|
|
3862
|
-
var conversationContent = "User: " + (userQ.length >
|
|
3863
|
-
"\nMate: " + (mateR.length >
|
|
3871
|
+
var conversationContent = "User: " + (userQ.length > 2000 ? userQ.substring(0, 2000) + "..." : userQ) +
|
|
3872
|
+
"\nMate: " + (mateR.length > 2000 ? mateR.substring(0, 2000) + "..." : mateR);
|
|
3864
3873
|
|
|
3865
3874
|
// Gate check: ask Haiku if this is worth remembering
|
|
3866
3875
|
gateMemory(mateCtx, mateId, conversationContent, function (shouldRemember) {
|
|
@@ -3872,6 +3881,7 @@ function createProjectContext(opts) {
|
|
|
3872
3881
|
var digestPrompt = [
|
|
3873
3882
|
"[SYSTEM: Session Digest]",
|
|
3874
3883
|
"Summarize this conversation from YOUR perspective for your long-term memory.",
|
|
3884
|
+
"Pay close attention to the user's exact words, preferences, and any personal/project context they shared.",
|
|
3875
3885
|
"Output ONLY a single valid JSON object (no markdown, no code fences, no extra text).",
|
|
3876
3886
|
"",
|
|
3877
3887
|
"Schema:",
|
|
@@ -3879,6 +3889,9 @@ function createProjectContext(opts) {
|
|
|
3879
3889
|
' "date": "YYYY-MM-DD",',
|
|
3880
3890
|
' "type": "mention",',
|
|
3881
3891
|
' "topic": "short topic description",',
|
|
3892
|
+
' "summary": "2-3 sentence summary of the full conversation",',
|
|
3893
|
+
' "key_quotes": ["exact notable things the user said, verbatim or near-verbatim, max 5"],',
|
|
3894
|
+
' "user_context": "personal info, project details, goals, preferences the user shared (null if none)",',
|
|
3882
3895
|
' "my_position": "what I said/recommended",',
|
|
3883
3896
|
' "decisions": "what was decided, or null if pending",',
|
|
3884
3897
|
' "open_items": "what remains unresolved",',
|
|
@@ -3889,7 +3902,8 @@ function createProjectContext(opts) {
|
|
|
3889
3902
|
' "tags": ["relevant", "topic", "tags"]',
|
|
3890
3903
|
"}",
|
|
3891
3904
|
"",
|
|
3892
|
-
"IMPORTANT:
|
|
3905
|
+
"IMPORTANT: Preserve the user's actual words in key_quotes. These are the most valuable part of memory.",
|
|
3906
|
+
"Output ONLY the JSON object. Nothing else.",
|
|
3893
3907
|
].join("\n");
|
|
3894
3908
|
|
|
3895
3909
|
var digestText = "";
|
|
@@ -3941,19 +3955,42 @@ function createProjectContext(opts) {
|
|
|
3941
3955
|
var mateCtx = matesModule.buildMateCtx(projectOwnerId);
|
|
3942
3956
|
if (!matesModule.isMate(mateCtx, mateId)) return;
|
|
3943
3957
|
|
|
3944
|
-
//
|
|
3945
|
-
var
|
|
3946
|
-
|
|
3958
|
+
// Collect full conversation from session history (all user + mate turns)
|
|
3959
|
+
var conversationParts = [];
|
|
3960
|
+
var totalLen = 0;
|
|
3961
|
+
var CONV_CAP = 6000; // generous cap for the full conversation
|
|
3962
|
+
for (var hi = 0; hi < session.history.length; hi++) {
|
|
3947
3963
|
var entry = session.history[hi];
|
|
3948
3964
|
if (entry.type === "user_message" && entry.text) {
|
|
3949
|
-
|
|
3950
|
-
|
|
3965
|
+
var uText = entry.text;
|
|
3966
|
+
if (totalLen + uText.length > CONV_CAP) {
|
|
3967
|
+
uText = uText.substring(0, Math.max(200, CONV_CAP - totalLen)) + "...";
|
|
3968
|
+
}
|
|
3969
|
+
conversationParts.push("User: " + uText);
|
|
3970
|
+
totalLen += uText.length;
|
|
3971
|
+
} else if (entry.type === "assistant_message" && entry.text) {
|
|
3972
|
+
var aText = entry.text;
|
|
3973
|
+
if (totalLen + aText.length > CONV_CAP) {
|
|
3974
|
+
aText = aText.substring(0, Math.max(200, CONV_CAP - totalLen)) + "...";
|
|
3975
|
+
}
|
|
3976
|
+
conversationParts.push("Mate: " + aText);
|
|
3977
|
+
totalLen += aText.length;
|
|
3951
3978
|
}
|
|
3979
|
+
if (totalLen >= CONV_CAP) break;
|
|
3952
3980
|
}
|
|
3953
|
-
|
|
3954
|
-
// Use responsePreview (full accumulated response) instead of delta fragments
|
|
3981
|
+
// Append the final response if not yet in history
|
|
3955
3982
|
var lastResponseText = responsePreview || "";
|
|
3956
|
-
if (
|
|
3983
|
+
if (lastResponseText && conversationParts.length > 0) {
|
|
3984
|
+
var lastPart = conversationParts[conversationParts.length - 1];
|
|
3985
|
+
if (lastPart.indexOf("Mate:") !== 0 || lastPart.indexOf(lastResponseText.substring(0, 50)) === -1) {
|
|
3986
|
+
var rText = lastResponseText;
|
|
3987
|
+
if (totalLen + rText.length > CONV_CAP) {
|
|
3988
|
+
rText = rText.substring(0, Math.max(200, CONV_CAP - totalLen)) + "...";
|
|
3989
|
+
}
|
|
3990
|
+
conversationParts.push("Mate: " + rText);
|
|
3991
|
+
}
|
|
3992
|
+
}
|
|
3993
|
+
if (conversationParts.length === 0) return;
|
|
3957
3994
|
|
|
3958
3995
|
var mateDir = matesModule.getMateDir(mateCtx, mateId);
|
|
3959
3996
|
var knowledgeDir = path.join(mateDir, "knowledge");
|
|
@@ -3967,8 +4004,7 @@ function createProjectContext(opts) {
|
|
|
3967
4004
|
});
|
|
3968
4005
|
}
|
|
3969
4006
|
|
|
3970
|
-
var conversationContent =
|
|
3971
|
-
"\nMate: " + (lastResponseText.length > 500 ? lastResponseText.substring(0, 500) + "..." : lastResponseText);
|
|
4007
|
+
var conversationContent = conversationParts.join("\n");
|
|
3972
4008
|
|
|
3973
4009
|
_dmDigestPending = true;
|
|
3974
4010
|
|
|
@@ -3983,6 +4019,7 @@ function createProjectContext(opts) {
|
|
|
3983
4019
|
var digestContext = [
|
|
3984
4020
|
"[SYSTEM: Session Digest]",
|
|
3985
4021
|
"Summarize this conversation from YOUR perspective for your long-term memory.",
|
|
4022
|
+
"Pay close attention to the user's exact words, preferences, and any personal/project context they shared.",
|
|
3986
4023
|
"",
|
|
3987
4024
|
conversationContent,
|
|
3988
4025
|
].join("\n");
|
|
@@ -3995,6 +4032,9 @@ function createProjectContext(opts) {
|
|
|
3995
4032
|
' "date": "YYYY-MM-DD",',
|
|
3996
4033
|
' "type": "dm",',
|
|
3997
4034
|
' "topic": "short topic description",',
|
|
4035
|
+
' "summary": "2-3 sentence summary of the full conversation",',
|
|
4036
|
+
' "key_quotes": ["exact notable things the user said, verbatim or near-verbatim, max 5"],',
|
|
4037
|
+
' "user_context": "personal info, project details, goals, preferences the user shared (null if none)",',
|
|
3998
4038
|
' "my_position": "what I said/recommended",',
|
|
3999
4039
|
' "decisions": "what was decided, or null if pending",',
|
|
4000
4040
|
' "open_items": "what remains unresolved",',
|
|
@@ -4005,7 +4045,8 @@ function createProjectContext(opts) {
|
|
|
4005
4045
|
' "tags": ["relevant", "topic", "tags"]',
|
|
4006
4046
|
"}",
|
|
4007
4047
|
"",
|
|
4008
|
-
"IMPORTANT:
|
|
4048
|
+
"IMPORTANT: Preserve the user's actual words in key_quotes. These are the most valuable part of memory.",
|
|
4049
|
+
"Output ONLY the JSON object. Nothing else.",
|
|
4009
4050
|
].join("\n");
|
|
4010
4051
|
|
|
4011
4052
|
var digestText = "";
|
|
@@ -4413,16 +4454,16 @@ function createProjectContext(opts) {
|
|
|
4413
4454
|
} catch (e) {}
|
|
4414
4455
|
|
|
4415
4456
|
if (hasSummary) {
|
|
4416
|
-
// Load summary + latest
|
|
4417
|
-
var recent = allLines.slice(-
|
|
4457
|
+
// Load summary + latest 5 raw digests for richer context
|
|
4458
|
+
var recent = allLines.slice(-5);
|
|
4418
4459
|
var result = "\n\nYour memory summary:\n" + summaryContent;
|
|
4419
4460
|
if (recent.length > 0) {
|
|
4420
4461
|
result += formatRawDigests(recent, "Latest raw session memories:");
|
|
4421
4462
|
}
|
|
4422
4463
|
return result;
|
|
4423
4464
|
} else {
|
|
4424
|
-
// Backward compatible: latest
|
|
4425
|
-
var recent = allLines.slice(-
|
|
4465
|
+
// Backward compatible: latest 8 raw digests
|
|
4466
|
+
var recent = allLines.slice(-8);
|
|
4426
4467
|
return formatRawDigests(recent, "Your recent session memories:");
|
|
4427
4468
|
}
|
|
4428
4469
|
}
|
|
@@ -4453,10 +4494,10 @@ function createProjectContext(opts) {
|
|
|
4453
4494
|
}
|
|
4454
4495
|
} catch (e) {}
|
|
4455
4496
|
|
|
4456
|
-
// Cap conversation content for gate
|
|
4497
|
+
// Cap conversation content for gate
|
|
4457
4498
|
var cappedContent = conversationContent;
|
|
4458
|
-
if (cappedContent.length >
|
|
4459
|
-
cappedContent = cappedContent.substring(0,
|
|
4499
|
+
if (cappedContent.length > 3000) {
|
|
4500
|
+
cappedContent = cappedContent.substring(0, 3000) + "...";
|
|
4460
4501
|
}
|
|
4461
4502
|
|
|
4462
4503
|
var gateContext = [
|
|
@@ -4475,20 +4516,25 @@ function createProjectContext(opts) {
|
|
|
4475
4516
|
|
|
4476
4517
|
var gatePrompt = opts.gatePrompt || [
|
|
4477
4518
|
'Should this conversation be saved to long-term memory?',
|
|
4478
|
-
'Answer "yes"
|
|
4479
|
-
"- A new decision or
|
|
4519
|
+
'Answer "yes" if ANY of these apply:',
|
|
4520
|
+
"- A new decision, commitment, or direction",
|
|
4480
4521
|
"- A change in position or strategy",
|
|
4481
4522
|
"- New information relevant to this Mate's role",
|
|
4482
|
-
"- A user preference or pattern not already in the summary",
|
|
4523
|
+
"- A user preference, opinion, or pattern not already in the summary",
|
|
4524
|
+
"- The user shared personal context, project details, or goals",
|
|
4525
|
+
"- The user expressed what they like, dislike, or care about",
|
|
4526
|
+
"- The user gave instructions on how they want things done",
|
|
4527
|
+
"- Anything the user would reasonably expect to be remembered next time",
|
|
4528
|
+
"",
|
|
4529
|
+
'Answer "no" ONLY if:',
|
|
4530
|
+
"- It exactly duplicates what is already in the memory summary",
|
|
4531
|
+
"- The entire conversation is a single trivial exchange (e.g. just 'hi' / 'hello')",
|
|
4483
4532
|
"",
|
|
4484
|
-
|
|
4485
|
-
"- It duplicates what is already in the memory summary",
|
|
4486
|
-
"- It is casual/trivial conversation",
|
|
4487
|
-
"- It is not relevant to this Mate's role",
|
|
4533
|
+
"When in doubt, answer yes. It is better to remember too much than to forget something important.",
|
|
4488
4534
|
"",
|
|
4489
4535
|
'Answer with ONLY "yes" or "no". Nothing else.',
|
|
4490
4536
|
].join("\n");
|
|
4491
|
-
var defaultOnError = !!opts.defaultYes;
|
|
4537
|
+
var defaultOnError = opts.defaultYes !== undefined ? !!opts.defaultYes : true;
|
|
4492
4538
|
|
|
4493
4539
|
var gateText = "";
|
|
4494
4540
|
var _gateSession = null;
|
|
@@ -4575,20 +4621,27 @@ function createProjectContext(opts) {
|
|
|
4575
4621
|
"3. Moving resolved open threads out of \"Open Threads\"",
|
|
4576
4622
|
"4. Adding to \"My Track Record\" if a past prediction/recommendation can now be evaluated",
|
|
4577
4623
|
"5. Removing outdated or redundant information",
|
|
4624
|
+
"6. Preserving important user quotes and context from key_quotes and user_context fields",
|
|
4578
4625
|
"",
|
|
4579
4626
|
"Maintain this structure:",
|
|
4580
4627
|
"",
|
|
4581
4628
|
"# Memory Summary",
|
|
4582
4629
|
"Last updated: YYYY-MM-DD (session count: N+1)",
|
|
4583
4630
|
"",
|
|
4631
|
+
"## User Context",
|
|
4632
|
+
"(who they are, what they work on, project details, goals)",
|
|
4584
4633
|
"## User Patterns",
|
|
4634
|
+
"(preferences, work style, communication style, likes/dislikes)",
|
|
4585
4635
|
"## Key Decisions",
|
|
4636
|
+
"## Notable Quotes",
|
|
4637
|
+
"(important things the user said, verbatim when possible)",
|
|
4586
4638
|
"## My Track Record",
|
|
4587
4639
|
"## Open Threads",
|
|
4588
4640
|
"## Recurring Topics",
|
|
4589
4641
|
"",
|
|
4590
4642
|
"Keep it concise. Each section should have at most 10 bullet points.",
|
|
4591
4643
|
"Drop the oldest/least relevant if needed.",
|
|
4644
|
+
"The Notable Quotes section is valuable for preserving the user's voice and intent.",
|
|
4592
4645
|
"Output ONLY the updated markdown. Nothing else.",
|
|
4593
4646
|
].join("\n");
|
|
4594
4647
|
|
|
@@ -4679,14 +4732,20 @@ function createProjectContext(opts) {
|
|
|
4679
4732
|
"# Memory Summary",
|
|
4680
4733
|
"Last updated: YYYY-MM-DD (session count: N)",
|
|
4681
4734
|
"",
|
|
4735
|
+
"## User Context",
|
|
4736
|
+
"(who they are, what they work on, project details, goals)",
|
|
4682
4737
|
"## User Patterns",
|
|
4738
|
+
"(preferences, work style, communication style, likes/dislikes)",
|
|
4683
4739
|
"## Key Decisions",
|
|
4740
|
+
"## Notable Quotes",
|
|
4741
|
+
"(important things the user said, verbatim when possible)",
|
|
4684
4742
|
"## My Track Record",
|
|
4685
4743
|
"## Open Threads",
|
|
4686
4744
|
"## Recurring Topics",
|
|
4687
4745
|
"",
|
|
4688
|
-
"Keep it concise. Focus on patterns
|
|
4746
|
+
"Keep it concise. Focus on patterns, decisions, and the user's own words.",
|
|
4689
4747
|
"Each section should have at most 10 bullet points.",
|
|
4748
|
+
"Preserve key_quotes from digests in the Notable Quotes section.",
|
|
4690
4749
|
"Set session count to " + digestsText.length + ".",
|
|
4691
4750
|
"Output ONLY the markdown. Nothing else.",
|
|
4692
4751
|
].join("\n");
|
package/lib/public/app.js
CHANGED
|
@@ -2212,6 +2212,9 @@ import { initDebate, handleDebateStarted, handleDebateResumed, handleDebateTurn,
|
|
|
2212
2212
|
sendBtn.disabled = false;
|
|
2213
2213
|
setSendBtnMode("send");
|
|
2214
2214
|
connectOverlay.classList.add("hidden");
|
|
2215
|
+
// Hide update banner on reconnect; server will re-send update_available if still needed
|
|
2216
|
+
var updPill = $("update-pill-wrap");
|
|
2217
|
+
if (updPill) updPill.classList.add("hidden");
|
|
2215
2218
|
stopVerbCycle();
|
|
2216
2219
|
} else if (status === "processing") {
|
|
2217
2220
|
if (dot) { dot.classList.add("connected"); dot.classList.add("processing"); }
|
|
@@ -498,6 +498,113 @@
|
|
|
498
498
|
.project-ctx-item.project-ctx-delete { color: var(--error); }
|
|
499
499
|
.project-ctx-item.project-ctx-delete:hover { background: var(--error-8); }
|
|
500
500
|
|
|
501
|
+
/* --- Project Access Popover --- */
|
|
502
|
+
.project-access-popover {
|
|
503
|
+
position: fixed;
|
|
504
|
+
background: var(--sidebar-bg);
|
|
505
|
+
border: 1px solid var(--border);
|
|
506
|
+
border-radius: 12px;
|
|
507
|
+
padding: 0;
|
|
508
|
+
width: 260px;
|
|
509
|
+
box-shadow: 0 8px 30px rgba(var(--shadow-rgb), 0.35);
|
|
510
|
+
z-index: 9999;
|
|
511
|
+
animation: ctxMenuAppear 0.12s ease-out;
|
|
512
|
+
overflow: hidden;
|
|
513
|
+
}
|
|
514
|
+
.project-access-header {
|
|
515
|
+
display: flex;
|
|
516
|
+
align-items: center;
|
|
517
|
+
justify-content: space-between;
|
|
518
|
+
padding: 12px 14px 8px;
|
|
519
|
+
border-bottom: 1px solid var(--border);
|
|
520
|
+
}
|
|
521
|
+
.project-access-title {
|
|
522
|
+
font-size: 13px;
|
|
523
|
+
font-weight: 600;
|
|
524
|
+
color: var(--text);
|
|
525
|
+
}
|
|
526
|
+
.project-access-close {
|
|
527
|
+
background: none;
|
|
528
|
+
border: none;
|
|
529
|
+
color: var(--text-secondary);
|
|
530
|
+
font-size: 18px;
|
|
531
|
+
cursor: pointer;
|
|
532
|
+
padding: 0 2px;
|
|
533
|
+
line-height: 1;
|
|
534
|
+
}
|
|
535
|
+
.project-access-close:hover { color: var(--text); }
|
|
536
|
+
.project-access-section {
|
|
537
|
+
padding: 10px 14px;
|
|
538
|
+
}
|
|
539
|
+
.project-access-label {
|
|
540
|
+
display: block;
|
|
541
|
+
font-size: 11px;
|
|
542
|
+
font-weight: 600;
|
|
543
|
+
color: var(--text-secondary);
|
|
544
|
+
text-transform: uppercase;
|
|
545
|
+
letter-spacing: 0.04em;
|
|
546
|
+
margin-bottom: 6px;
|
|
547
|
+
}
|
|
548
|
+
.project-access-vis-row {
|
|
549
|
+
display: flex;
|
|
550
|
+
gap: 6px;
|
|
551
|
+
}
|
|
552
|
+
.project-access-vis-btn {
|
|
553
|
+
flex: 1;
|
|
554
|
+
display: flex;
|
|
555
|
+
align-items: center;
|
|
556
|
+
justify-content: center;
|
|
557
|
+
gap: 6px;
|
|
558
|
+
padding: 7px 0;
|
|
559
|
+
font-size: 12px;
|
|
560
|
+
font-family: inherit;
|
|
561
|
+
color: var(--text-secondary);
|
|
562
|
+
background: rgba(var(--overlay-rgb), 0.04);
|
|
563
|
+
border: 1px solid var(--border);
|
|
564
|
+
border-radius: 8px;
|
|
565
|
+
cursor: pointer;
|
|
566
|
+
transition: all 0.15s;
|
|
567
|
+
}
|
|
568
|
+
.project-access-vis-btn .lucide { width: 14px; height: 14px; }
|
|
569
|
+
.project-access-vis-btn:hover { background: rgba(var(--overlay-rgb), 0.08); }
|
|
570
|
+
.project-access-vis-btn.active {
|
|
571
|
+
background: var(--accent-8, rgba(99, 102, 241, 0.08));
|
|
572
|
+
border-color: var(--accent, #6366f1);
|
|
573
|
+
color: var(--accent, #6366f1);
|
|
574
|
+
font-weight: 500;
|
|
575
|
+
}
|
|
576
|
+
.project-access-user-list {
|
|
577
|
+
max-height: 200px;
|
|
578
|
+
overflow-y: auto;
|
|
579
|
+
}
|
|
580
|
+
.project-access-user-item {
|
|
581
|
+
display: flex;
|
|
582
|
+
align-items: center;
|
|
583
|
+
gap: 8px;
|
|
584
|
+
padding: 5px 0;
|
|
585
|
+
font-size: 13px;
|
|
586
|
+
color: var(--text);
|
|
587
|
+
cursor: pointer;
|
|
588
|
+
}
|
|
589
|
+
.project-access-user-item input[type="checkbox"] {
|
|
590
|
+
accent-color: var(--accent, #6366f1);
|
|
591
|
+
width: 15px;
|
|
592
|
+
height: 15px;
|
|
593
|
+
cursor: pointer;
|
|
594
|
+
}
|
|
595
|
+
.project-access-user-item:hover { color: var(--text); }
|
|
596
|
+
.project-access-empty {
|
|
597
|
+
font-size: 12px;
|
|
598
|
+
color: var(--text-tertiary);
|
|
599
|
+
padding: 8px 0;
|
|
600
|
+
}
|
|
601
|
+
.project-access-loading {
|
|
602
|
+
padding: 20px 14px;
|
|
603
|
+
text-align: center;
|
|
604
|
+
font-size: 12px;
|
|
605
|
+
color: var(--text-secondary);
|
|
606
|
+
}
|
|
607
|
+
|
|
501
608
|
/* --- Emoji picker popover --- */
|
|
502
609
|
.emoji-picker {
|
|
503
610
|
position: fixed;
|
|
@@ -2672,6 +2672,158 @@ var EMOJI_CATEGORIES = [
|
|
|
2672
2672
|
]},
|
|
2673
2673
|
];
|
|
2674
2674
|
|
|
2675
|
+
// --- Project Access Popover ---
|
|
2676
|
+
var projectAccessPopover = null;
|
|
2677
|
+
|
|
2678
|
+
function closeProjectAccessPopover() {
|
|
2679
|
+
if (projectAccessPopover) {
|
|
2680
|
+
projectAccessPopover.remove();
|
|
2681
|
+
projectAccessPopover = null;
|
|
2682
|
+
document.removeEventListener("click", closeAccessOnOutside);
|
|
2683
|
+
document.removeEventListener("keydown", closeAccessOnEscape);
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
function closeAccessOnOutside(e) {
|
|
2688
|
+
if (projectAccessPopover && !projectAccessPopover.contains(e.target)) closeProjectAccessPopover();
|
|
2689
|
+
}
|
|
2690
|
+
function closeAccessOnEscape(e) {
|
|
2691
|
+
if (e.key === "Escape") closeProjectAccessPopover();
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2694
|
+
function showProjectAccessPopover(anchorEl, slug) {
|
|
2695
|
+
closeProjectAccessPopover();
|
|
2696
|
+
|
|
2697
|
+
var popover = document.createElement("div");
|
|
2698
|
+
popover.className = "project-access-popover";
|
|
2699
|
+
popover.innerHTML = '<div class="project-access-loading">Loading...</div>';
|
|
2700
|
+
popover.addEventListener("click", function (e) { e.stopPropagation(); });
|
|
2701
|
+
document.body.appendChild(popover);
|
|
2702
|
+
projectAccessPopover = popover;
|
|
2703
|
+
|
|
2704
|
+
// Position near anchor
|
|
2705
|
+
requestAnimationFrame(function () {
|
|
2706
|
+
var rect = anchorEl.getBoundingClientRect();
|
|
2707
|
+
popover.style.position = "fixed";
|
|
2708
|
+
popover.style.left = (rect.right + 8) + "px";
|
|
2709
|
+
popover.style.top = rect.top + "px";
|
|
2710
|
+
popover.style.zIndex = "9999";
|
|
2711
|
+
var popRect = popover.getBoundingClientRect();
|
|
2712
|
+
if (popRect.right > window.innerWidth - 8) {
|
|
2713
|
+
popover.style.left = (rect.left - popRect.width - 8) + "px";
|
|
2714
|
+
}
|
|
2715
|
+
if (popRect.bottom > window.innerHeight - 8) {
|
|
2716
|
+
popover.style.top = (window.innerHeight - popRect.height - 8) + "px";
|
|
2717
|
+
}
|
|
2718
|
+
});
|
|
2719
|
+
|
|
2720
|
+
setTimeout(function () {
|
|
2721
|
+
document.addEventListener("click", closeAccessOnOutside);
|
|
2722
|
+
document.addEventListener("keydown", closeAccessOnEscape);
|
|
2723
|
+
}, 0);
|
|
2724
|
+
|
|
2725
|
+
// Fetch access info and user list in parallel
|
|
2726
|
+
Promise.all([
|
|
2727
|
+
fetch("/api/admin/projects/" + encodeURIComponent(slug) + "/access").then(function (r) { return r.json(); }),
|
|
2728
|
+
fetch("/api/admin/users").then(function (r) { return r.json(); }),
|
|
2729
|
+
]).then(function (results) {
|
|
2730
|
+
var access = results[0];
|
|
2731
|
+
var usersData = results[1];
|
|
2732
|
+
if (access.error || usersData.error) {
|
|
2733
|
+
popover.innerHTML = '<div class="project-access-loading">Failed to load</div>';
|
|
2734
|
+
return;
|
|
2735
|
+
}
|
|
2736
|
+
renderAccessPopover(popover, slug, access, usersData.users || []);
|
|
2737
|
+
}).catch(function () {
|
|
2738
|
+
popover.innerHTML = '<div class="project-access-loading">Failed to load</div>';
|
|
2739
|
+
});
|
|
2740
|
+
}
|
|
2741
|
+
|
|
2742
|
+
function renderAccessPopover(popover, slug, access, allUsers) {
|
|
2743
|
+
var visibility = access.visibility || "public";
|
|
2744
|
+
var allowedUsers = access.allowedUsers || [];
|
|
2745
|
+
var ownerId = access.ownerId;
|
|
2746
|
+
|
|
2747
|
+
// Filter out the owner from the user list (owner always has access)
|
|
2748
|
+
var selectableUsers = allUsers.filter(function (u) { return u.id !== ownerId; });
|
|
2749
|
+
|
|
2750
|
+
var html = '';
|
|
2751
|
+
html += '<div class="project-access-header">';
|
|
2752
|
+
html += '<span class="project-access-title">Project Access</span>';
|
|
2753
|
+
html += '<button class="project-access-close">×</button>';
|
|
2754
|
+
html += '</div>';
|
|
2755
|
+
|
|
2756
|
+
// Visibility toggle
|
|
2757
|
+
html += '<div class="project-access-section">';
|
|
2758
|
+
html += '<label class="project-access-label">Visibility</label>';
|
|
2759
|
+
html += '<div class="project-access-vis-row">';
|
|
2760
|
+
html += '<button class="project-access-vis-btn' + (visibility === "private" ? ' active' : '') + '" data-vis="private">';
|
|
2761
|
+
html += iconHtml("lock") + ' Private';
|
|
2762
|
+
html += '</button>';
|
|
2763
|
+
html += '<button class="project-access-vis-btn' + (visibility === "public" ? ' active' : '') + '" data-vis="public">';
|
|
2764
|
+
html += iconHtml("globe") + ' Public';
|
|
2765
|
+
html += '</button>';
|
|
2766
|
+
html += '</div>';
|
|
2767
|
+
html += '</div>';
|
|
2768
|
+
|
|
2769
|
+
// Allowed users (only when private)
|
|
2770
|
+
html += '<div class="project-access-section project-access-users-section"' + (visibility !== "private" ? ' style="display:none"' : '') + '>';
|
|
2771
|
+
html += '<label class="project-access-label">Allowed Users</label>';
|
|
2772
|
+
html += '<div class="project-access-user-list">';
|
|
2773
|
+
for (var i = 0; i < selectableUsers.length; i++) {
|
|
2774
|
+
var u = selectableUsers[i];
|
|
2775
|
+
var checked = allowedUsers.indexOf(u.id) !== -1 ? " checked" : "";
|
|
2776
|
+
html += '<label class="project-access-user-item">';
|
|
2777
|
+
html += '<input type="checkbox" data-uid="' + u.id + '"' + checked + '>';
|
|
2778
|
+
html += '<span>' + escapeHtml(u.displayName || u.username || u.id) + '</span>';
|
|
2779
|
+
html += '</label>';
|
|
2780
|
+
}
|
|
2781
|
+
if (selectableUsers.length === 0) {
|
|
2782
|
+
html += '<div class="project-access-empty">No other users</div>';
|
|
2783
|
+
}
|
|
2784
|
+
html += '</div>';
|
|
2785
|
+
html += '</div>';
|
|
2786
|
+
|
|
2787
|
+
popover.innerHTML = html;
|
|
2788
|
+
refreshIcons();
|
|
2789
|
+
|
|
2790
|
+
// Close button
|
|
2791
|
+
popover.querySelector(".project-access-close").addEventListener("click", function () {
|
|
2792
|
+
closeProjectAccessPopover();
|
|
2793
|
+
});
|
|
2794
|
+
|
|
2795
|
+
// Visibility toggle
|
|
2796
|
+
popover.querySelectorAll(".project-access-vis-btn").forEach(function (btn) {
|
|
2797
|
+
btn.addEventListener("click", function () {
|
|
2798
|
+
var newVis = btn.dataset.vis;
|
|
2799
|
+
popover.querySelectorAll(".project-access-vis-btn").forEach(function (b) { b.classList.remove("active"); });
|
|
2800
|
+
btn.classList.add("active");
|
|
2801
|
+
var usersSection = popover.querySelector(".project-access-users-section");
|
|
2802
|
+
if (usersSection) usersSection.style.display = newVis === "private" ? "" : "none";
|
|
2803
|
+
fetch("/api/admin/projects/" + encodeURIComponent(slug) + "/visibility", {
|
|
2804
|
+
method: "PUT",
|
|
2805
|
+
headers: { "Content-Type": "application/json" },
|
|
2806
|
+
body: JSON.stringify({ visibility: newVis }),
|
|
2807
|
+
});
|
|
2808
|
+
});
|
|
2809
|
+
});
|
|
2810
|
+
|
|
2811
|
+
// User checkboxes
|
|
2812
|
+
popover.querySelectorAll('.project-access-user-item input[type="checkbox"]').forEach(function (cb) {
|
|
2813
|
+
cb.addEventListener("change", function () {
|
|
2814
|
+
var selected = [];
|
|
2815
|
+
popover.querySelectorAll('.project-access-user-item input[type="checkbox"]:checked').forEach(function (c) {
|
|
2816
|
+
selected.push(c.dataset.uid);
|
|
2817
|
+
});
|
|
2818
|
+
fetch("/api/admin/projects/" + encodeURIComponent(slug) + "/users", {
|
|
2819
|
+
method: "PUT",
|
|
2820
|
+
headers: { "Content-Type": "application/json" },
|
|
2821
|
+
body: JSON.stringify({ allowedUsers: selected }),
|
|
2822
|
+
});
|
|
2823
|
+
});
|
|
2824
|
+
});
|
|
2825
|
+
}
|
|
2826
|
+
|
|
2675
2827
|
function closeProjectCtxMenu() {
|
|
2676
2828
|
if (projectCtxMenu) {
|
|
2677
2829
|
projectCtxMenu.remove();
|
|
@@ -2797,6 +2949,23 @@ function showProjectCtxMenu(anchorEl, slug, name, icon, position) {
|
|
|
2797
2949
|
});
|
|
2798
2950
|
menu.appendChild(shareItem);
|
|
2799
2951
|
|
|
2952
|
+
// --- Manage Access (owner or admin, multi-user only) ---
|
|
2953
|
+
if (ctx.multiUser && slug.indexOf("--") === -1) {
|
|
2954
|
+
var isProjectOwner = ctx.myUserId && ctx.projectOwnerId && ctx.myUserId === ctx.projectOwnerId;
|
|
2955
|
+
var isAdmin = ctx.permissions && ctx.permissions.projectSettings !== false;
|
|
2956
|
+
if (isProjectOwner || isAdmin) {
|
|
2957
|
+
var accessItem = document.createElement("button");
|
|
2958
|
+
accessItem.className = "project-ctx-item";
|
|
2959
|
+
accessItem.innerHTML = iconHtml("users") + " <span>Manage Access</span>";
|
|
2960
|
+
accessItem.addEventListener("click", function (e) {
|
|
2961
|
+
e.stopPropagation();
|
|
2962
|
+
closeProjectCtxMenu();
|
|
2963
|
+
showProjectAccessPopover(anchorEl, slug);
|
|
2964
|
+
});
|
|
2965
|
+
menu.appendChild(accessItem);
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
|
|
2800
2969
|
if (!ctx.permissions || ctx.permissions.deleteProject !== false) {
|
|
2801
2970
|
// --- Separator ---
|
|
2802
2971
|
var sep = document.createElement("div");
|
package/lib/server.js
CHANGED
|
@@ -1512,13 +1512,23 @@ function createServer(opts) {
|
|
|
1512
1512
|
return;
|
|
1513
1513
|
}
|
|
1514
1514
|
var mu = getMultiUserFromReq(req);
|
|
1515
|
-
if (!mu
|
|
1516
|
-
res.writeHead(
|
|
1517
|
-
res.end('{"error":"
|
|
1515
|
+
if (!mu) {
|
|
1516
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1517
|
+
res.end('{"error":"Authentication required"}');
|
|
1518
1518
|
return;
|
|
1519
1519
|
}
|
|
1520
|
-
|
|
1521
|
-
|
|
1520
|
+
// Admins get full user list; project owners get limited list (id, displayName, username)
|
|
1521
|
+
if (mu.role === "admin") {
|
|
1522
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1523
|
+
res.end(JSON.stringify({ users: users.getAllUsers() }));
|
|
1524
|
+
} else {
|
|
1525
|
+
var allU = users.getAllUsers();
|
|
1526
|
+
var safeUsers = allU.map(function (u) {
|
|
1527
|
+
return { id: u.id, displayName: u.displayName, username: u.username };
|
|
1528
|
+
});
|
|
1529
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1530
|
+
res.end(JSON.stringify({ users: safeUsers }));
|
|
1531
|
+
}
|
|
1522
1532
|
return;
|
|
1523
1533
|
}
|
|
1524
1534
|
|
|
@@ -1990,9 +2000,12 @@ function createServer(opts) {
|
|
|
1990
2000
|
return;
|
|
1991
2001
|
}
|
|
1992
2002
|
var mu = getMultiUserFromReq(req);
|
|
1993
|
-
|
|
2003
|
+
var _visSlug = fullUrl.split("/")[4];
|
|
2004
|
+
var _visAccess = onGetProjectAccess ? onGetProjectAccess(_visSlug) : null;
|
|
2005
|
+
var _isOwner = mu && _visAccess && _visAccess.ownerId && mu.id === _visAccess.ownerId;
|
|
2006
|
+
if (!mu || (mu.role !== "admin" && !_isOwner)) {
|
|
1994
2007
|
res.writeHead(403, { "Content-Type": "application/json" });
|
|
1995
|
-
res.end('{"error":"Admin access required"}');
|
|
2008
|
+
res.end('{"error":"Admin or project owner access required"}');
|
|
1996
2009
|
return;
|
|
1997
2010
|
}
|
|
1998
2011
|
var projSlug = fullUrl.split("/")[4];
|
|
@@ -2092,9 +2105,12 @@ function createServer(opts) {
|
|
|
2092
2105
|
return;
|
|
2093
2106
|
}
|
|
2094
2107
|
var mu = getMultiUserFromReq(req);
|
|
2095
|
-
|
|
2108
|
+
var _usrSlug = fullUrl.split("/")[4];
|
|
2109
|
+
var _usrAccess = onGetProjectAccess ? onGetProjectAccess(_usrSlug) : null;
|
|
2110
|
+
var _isOwnerU = mu && _usrAccess && _usrAccess.ownerId && mu.id === _usrAccess.ownerId;
|
|
2111
|
+
if (!mu || (mu.role !== "admin" && !_isOwnerU)) {
|
|
2096
2112
|
res.writeHead(403, { "Content-Type": "application/json" });
|
|
2097
|
-
res.end('{"error":"Admin access required"}');
|
|
2113
|
+
res.end('{"error":"Admin or project owner access required"}');
|
|
2098
2114
|
return;
|
|
2099
2115
|
}
|
|
2100
2116
|
var projSlug = fullUrl.split("/")[4];
|
|
@@ -2134,7 +2150,7 @@ function createServer(opts) {
|
|
|
2134
2150
|
return;
|
|
2135
2151
|
}
|
|
2136
2152
|
|
|
2137
|
-
// Get project access info (admin
|
|
2153
|
+
// Get project access info (admin or project owner)
|
|
2138
2154
|
if (req.method === "GET" && /^\/api\/admin\/projects\/[a-z0-9_-]+\/access$/.test(fullUrl)) {
|
|
2139
2155
|
if (!users.isMultiUser()) {
|
|
2140
2156
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
@@ -2142,9 +2158,12 @@ function createServer(opts) {
|
|
|
2142
2158
|
return;
|
|
2143
2159
|
}
|
|
2144
2160
|
var mu = getMultiUserFromReq(req);
|
|
2145
|
-
|
|
2161
|
+
var _accSlug = fullUrl.split("/")[4];
|
|
2162
|
+
var _accAccess = onGetProjectAccess ? onGetProjectAccess(_accSlug) : null;
|
|
2163
|
+
var _isOwnerA = mu && _accAccess && _accAccess.ownerId && mu.id === _accAccess.ownerId;
|
|
2164
|
+
if (!mu || (mu.role !== "admin" && !_isOwnerA)) {
|
|
2146
2165
|
res.writeHead(403, { "Content-Type": "application/json" });
|
|
2147
|
-
res.end('{"error":"Admin access required"}');
|
|
2166
|
+
res.end('{"error":"Admin or project owner access required"}');
|
|
2148
2167
|
return;
|
|
2149
2168
|
}
|
|
2150
2169
|
var projSlug = fullUrl.split("/")[4];
|