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 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
- // Non-admin users own their projects and they default to private
165
- if (wsUser && wsUser.id && wsUser.role !== "admin") {
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
- if (config.osUsers || wsUser.role !== "admin") {
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
- var projectEntry = { path: targetDir, slug: slug, addedAt: Date.now() };
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
- if (config.osUsers || wsUser.role !== "admin") {
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 (500 char cap each side)
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 > 500 ? userQ.substring(0, 500) + "..." : userQ) +
3863
- "\nMate: " + (mateR.length > 500 ? mateR.substring(0, 500) + "..." : mateR);
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: Output ONLY the JSON object. Nothing else.",
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
- // Extract last user message from history
3945
- var lastUserText = "";
3946
- for (var hi = session.history.length - 1; hi >= 0; hi--) {
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
- lastUserText = entry.text;
3950
- break;
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 (!lastUserText && !lastResponseText) return;
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 = "User: " + (lastUserText.length > 500 ? lastUserText.substring(0, 500) + "..." : lastUserText) +
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: Output ONLY the JSON object. Nothing else.",
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 3 raw digests
4417
- var recent = allLines.slice(-3);
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 5 raw digests
4425
- var recent = allLines.slice(-5);
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 (500 chars each side)
4497
+ // Cap conversation content for gate
4457
4498
  var cappedContent = conversationContent;
4458
- if (cappedContent.length > 1200) {
4459
- cappedContent = cappedContent.substring(0, 1200) + "...";
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" ONLY if there is:',
4479
- "- A new decision or commitment",
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
- 'Answer "no" if:',
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 and decisions, not individual session details.",
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">&times;</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 || mu.role !== "admin") {
1516
- res.writeHead(403, { "Content-Type": "application/json" });
1517
- res.end('{"error":"Admin access required"}');
1515
+ if (!mu) {
1516
+ res.writeHead(401, { "Content-Type": "application/json" });
1517
+ res.end('{"error":"Authentication required"}');
1518
1518
  return;
1519
1519
  }
1520
- res.writeHead(200, { "Content-Type": "application/json" });
1521
- res.end(JSON.stringify({ users: users.getAllUsers() }));
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
- if (!mu || mu.role !== "admin") {
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
- if (!mu || mu.role !== "admin") {
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 only)
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
- if (!mu || mu.role !== "admin") {
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];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "2.22.0-beta.1",
3
+ "version": "2.22.0-beta.3",
4
4
  "description": "Self-hosted Claude Code in your browser. Multi-session, multi-user, push notifications.",
5
5
  "bin": {
6
6
  "clay-server": "./bin/cli.js",