clay-server 2.22.2 → 2.23.0-beta.1

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.
@@ -0,0 +1,57 @@
1
+ // build-user-env.js — Build a minimal, safe environment for user subprocesses.
2
+ // Prevents leaking the root daemon's full process.env to user sessions.
3
+
4
+ var os = require("os");
5
+
6
+ // Only these vars are forwarded from the daemon's environment.
7
+ var ALLOWED_KEYS = ["PATH", "LANG", "NODE_ENV"];
8
+
9
+ /**
10
+ * Build a clean env object for spawning a user process.
11
+ * osUserInfo: { uid, gid, home, user, shell } or null for same-user fallback.
12
+ */
13
+ function buildUserEnv(osUserInfo) {
14
+ var env = {};
15
+
16
+ // Copy only allowlisted keys from daemon env
17
+ for (var i = 0; i < ALLOWED_KEYS.length; i++) {
18
+ var key = ALLOWED_KEYS[i];
19
+ if (process.env[key]) env[key] = process.env[key];
20
+ }
21
+
22
+ // Copy all LC_* locale vars
23
+ var keys = Object.keys(process.env);
24
+ for (var j = 0; j < keys.length; j++) {
25
+ if (keys[j].indexOf("LC_") === 0) {
26
+ env[keys[j]] = process.env[keys[j]];
27
+ }
28
+ }
29
+
30
+ // Terminal settings
31
+ env.TERM = "xterm-256color";
32
+ env.COLORFGBG = "15;0"; // Suppress OSC 11 background-color queries
33
+
34
+ // User identity
35
+ if (osUserInfo) {
36
+ env.HOME = osUserInfo.home;
37
+ env.USER = osUserInfo.user;
38
+ env.LOGNAME = osUserInfo.user;
39
+ if (osUserInfo.shell) env.SHELL = osUserInfo.shell;
40
+ } else {
41
+ env.HOME = process.env.HOME || os.homedir();
42
+ env.USER = process.env.USER || "";
43
+ env.LOGNAME = process.env.LOGNAME || process.env.USER || "";
44
+ env.SHELL = process.env.SHELL || "/bin/bash";
45
+ }
46
+
47
+ // XDG runtime dir (needed for dbus, systemd user services, etc.)
48
+ if (osUserInfo) {
49
+ env.XDG_RUNTIME_DIR = "/run/user/" + osUserInfo.uid;
50
+ } else if (process.env.XDG_RUNTIME_DIR) {
51
+ env.XDG_RUNTIME_DIR = process.env.XDG_RUNTIME_DIR;
52
+ }
53
+
54
+ return env;
55
+ }
56
+
57
+ module.exports = { buildUserEnv: buildUserEnv };
package/lib/mates.js CHANGED
@@ -283,6 +283,89 @@ var TEAM_SECTION =
283
283
  "Check the team registry when it would be relevant to know who else is available or what they do. " +
284
284
  "You cannot message other Mates directly yet, but knowing your team helps you work with the user more effectively.\n";
285
285
 
286
+ // --- Project registry ---
287
+
288
+ var PROJECT_REGISTRY_MARKER = "<!-- PROJECT_REGISTRY_MANAGED_BY_SYSTEM -->";
289
+
290
+ function buildProjectRegistrySection(projects) {
291
+ if (!projects || projects.length === 0) return "";
292
+ var section = "\n\n" + PROJECT_REGISTRY_MARKER + "\n" +
293
+ "## Available Projects\n\n" +
294
+ "**This section is managed by the system and cannot be removed.**\n\n" +
295
+ "The following projects are registered in this workspace. " +
296
+ "Use this information when the user references a project by name, " +
297
+ "so you do not need to ask for the path.\n\n" +
298
+ "| Project | Path |\n" +
299
+ "|---------|------|\n";
300
+ for (var i = 0; i < projects.length; i++) {
301
+ var p = projects[i];
302
+ var name = (p.icon ? p.icon + " " : "") + (p.title || p.slug || path.basename(p.path));
303
+ section += "| " + name + " | `" + p.path + "` |\n";
304
+ }
305
+ return section;
306
+ }
307
+
308
+ /**
309
+ * Enforce the project registry section on a mate's CLAUDE.md.
310
+ * Inserts after team awareness section and before session memory section.
311
+ * Pass the current project list; if empty, removes the section.
312
+ * Returns true if the file was modified.
313
+ */
314
+ function enforceProjectRegistry(filePath, projects) {
315
+ if (!fs.existsSync(filePath)) return false;
316
+
317
+ var content = fs.readFileSync(filePath, "utf8");
318
+ var newSection = buildProjectRegistrySection(projects);
319
+
320
+ // Strip existing section if present
321
+ var markerIdx = content.indexOf(PROJECT_REGISTRY_MARKER);
322
+ if (markerIdx !== -1) {
323
+ var afterMarker = content.substring(markerIdx);
324
+ // Find next system marker
325
+ var nextIdx = -1;
326
+ var candidates = [SESSION_MEMORY_MARKER, STICKY_NOTES_MARKER, DEBATE_AWARENESS_MARKER, crisisSafety.MARKER];
327
+ for (var c = 0; c < candidates.length; c++) {
328
+ var ci = afterMarker.indexOf(candidates[c]);
329
+ if (ci !== -1 && (nextIdx === -1 || ci < nextIdx)) nextIdx = ci;
330
+ }
331
+
332
+ if (nextIdx !== -1) {
333
+ var existing = afterMarker.substring(0, nextIdx).trimEnd();
334
+ if (existing === newSection.trimStart().trimEnd()) return false; // already correct
335
+ content = content.substring(0, markerIdx).trimEnd() + content.substring(markerIdx + nextIdx);
336
+ } else {
337
+ var existing = afterMarker.trimEnd();
338
+ if (existing === newSection.trimStart().trimEnd()) return false;
339
+ content = content.substring(0, markerIdx).trimEnd();
340
+ }
341
+ }
342
+
343
+ // If no projects, just remove the section (already done above)
344
+ if (!newSection) {
345
+ if (markerIdx !== -1) {
346
+ fs.writeFileSync(filePath, content, "utf8");
347
+ return true;
348
+ }
349
+ return false;
350
+ }
351
+
352
+ // Insert before session memory, sticky notes, debate, or crisis safety
353
+ var insertBefore = -1;
354
+ var insertCandidates = [SESSION_MEMORY_MARKER, STICKY_NOTES_MARKER, DEBATE_AWARENESS_MARKER, crisisSafety.MARKER];
355
+ for (var ic = 0; ic < insertCandidates.length; ic++) {
356
+ var pos = content.indexOf(insertCandidates[ic]);
357
+ if (pos !== -1) { insertBefore = pos; break; }
358
+ }
359
+ if (insertBefore !== -1) {
360
+ content = content.substring(0, insertBefore).trimEnd() + newSection + "\n\n" + content.substring(insertBefore);
361
+ } else {
362
+ content = content.trimEnd() + newSection;
363
+ }
364
+
365
+ fs.writeFileSync(filePath, content, "utf8");
366
+ return true;
367
+ }
368
+
286
369
  function hasTeamSection(content) {
287
370
  return content.indexOf(TEAM_MARKER) !== -1;
288
371
  }
@@ -302,14 +385,16 @@ function enforceTeamAwareness(filePath) {
302
385
  if (teamIdx !== -1) {
303
386
  // Extract existing team section (up to next system marker or end)
304
387
  var afterTeam = content.substring(teamIdx);
305
- // Find the nearest following system marker (session memory or crisis safety)
388
+ // Find the nearest following system marker (project registry, session memory, or crisis safety)
306
389
  var nextMarkerIdx = -1;
390
+ var projIdx = afterTeam.indexOf(PROJECT_REGISTRY_MARKER);
307
391
  var memIdx = afterTeam.indexOf(SESSION_MEMORY_MARKER);
308
392
  var crisisIdx = afterTeam.indexOf(crisisSafety.MARKER);
309
- if (memIdx !== -1 && (crisisIdx === -1 || memIdx < crisisIdx)) {
310
- nextMarkerIdx = memIdx;
311
- } else if (crisisIdx !== -1) {
312
- nextMarkerIdx = crisisIdx;
393
+ var teamNextCandidates = [projIdx, memIdx, crisisIdx];
394
+ for (var tn = 0; tn < teamNextCandidates.length; tn++) {
395
+ if (teamNextCandidates[tn] !== -1 && (nextMarkerIdx === -1 || teamNextCandidates[tn] < nextMarkerIdx)) {
396
+ nextMarkerIdx = teamNextCandidates[tn];
397
+ }
313
398
  }
314
399
  var existing;
315
400
  if (nextMarkerIdx !== -1) {
@@ -324,11 +409,14 @@ function enforceTeamAwareness(filePath) {
324
409
  content = content.substring(0, teamIdx).trimEnd() + content.substring(endOfTeam);
325
410
  }
326
411
 
327
- // Insert before session memory or crisis safety section if present, otherwise append
412
+ // Insert before project registry, session memory, or crisis safety section if present, otherwise append
413
+ var projRegPos = content.indexOf(PROJECT_REGISTRY_MARKER);
328
414
  var sessionMemPos = content.indexOf(SESSION_MEMORY_MARKER);
329
415
  var crisisPos = content.indexOf(crisisSafety.MARKER);
330
416
  var insertBefore = -1;
331
- if (sessionMemPos !== -1) {
417
+ if (projRegPos !== -1) {
418
+ insertBefore = projRegPos;
419
+ } else if (sessionMemPos !== -1) {
332
420
  insertBefore = sessionMemPos;
333
421
  } else if (crisisPos !== -1) {
334
422
  insertBefore = crisisPos;
@@ -377,24 +465,34 @@ function enforceSessionMemory(filePath) {
377
465
  if (memIdx !== -1) {
378
466
  // Extract existing section (up to next system marker or end)
379
467
  var afterMem = content.substring(memIdx);
380
- var crisisIdx = afterMem.indexOf(crisisSafety.MARKER);
468
+ var nextMemIdx = -1;
469
+ var memNextCandidates = [STICKY_NOTES_MARKER, DEBATE_AWARENESS_MARKER, crisisSafety.MARKER];
470
+ for (var mn = 0; mn < memNextCandidates.length; mn++) {
471
+ var mni = afterMem.indexOf(memNextCandidates[mn]);
472
+ if (mni !== -1 && (nextMemIdx === -1 || mni < nextMemIdx)) nextMemIdx = mni;
473
+ }
381
474
  var existing;
382
- if (crisisIdx !== -1) {
383
- existing = afterMem.substring(0, crisisIdx).trimEnd();
475
+ if (nextMemIdx !== -1) {
476
+ existing = afterMem.substring(0, nextMemIdx).trimEnd();
384
477
  } else {
385
478
  existing = afterMem.trimEnd();
386
479
  }
387
480
  if (existing === SESSION_MEMORY_SECTION.trimStart().trimEnd()) return false; // already correct
388
481
 
389
482
  // Strip the existing session memory section
390
- var endOfMem = crisisIdx !== -1 ? memIdx + crisisIdx : content.length;
483
+ var endOfMem = nextMemIdx !== -1 ? memIdx + nextMemIdx : content.length;
391
484
  content = content.substring(0, memIdx).trimEnd() + content.substring(endOfMem);
392
485
  }
393
486
 
394
- // Insert before crisis safety section if present, otherwise append
395
- var crisisPos = content.indexOf(crisisSafety.MARKER);
396
- if (crisisPos !== -1) {
397
- content = content.substring(0, crisisPos).trimEnd() + SESSION_MEMORY_SECTION + "\n\n" + content.substring(crisisPos);
487
+ // Insert before sticky notes, debate, or crisis safety section if present, otherwise append
488
+ var memInsertBefore = -1;
489
+ var memInsertCandidates = [STICKY_NOTES_MARKER, DEBATE_AWARENESS_MARKER, crisisSafety.MARKER];
490
+ for (var mi = 0; mi < memInsertCandidates.length; mi++) {
491
+ var mip = content.indexOf(memInsertCandidates[mi]);
492
+ if (mip !== -1) { memInsertBefore = mip; break; }
493
+ }
494
+ if (memInsertBefore !== -1) {
495
+ content = content.substring(0, memInsertBefore).trimEnd() + SESSION_MEMORY_SECTION + "\n\n" + content.substring(memInsertBefore);
398
496
  } else {
399
497
  content = content.trimEnd() + SESSION_MEMORY_SECTION;
400
498
  }
@@ -430,6 +528,86 @@ function enforceStickyNotes(filePath) {
430
528
  var content = fs.readFileSync(filePath, "utf8");
431
529
 
432
530
  var markerIdx = content.indexOf(STICKY_NOTES_MARKER);
531
+ if (markerIdx !== -1) {
532
+ var afterMarker = content.substring(markerIdx);
533
+ var stickyNextIdx = -1;
534
+ var stickyNextCandidates = [DEBATE_AWARENESS_MARKER, crisisSafety.MARKER];
535
+ for (var sn = 0; sn < stickyNextCandidates.length; sn++) {
536
+ var sni = afterMarker.indexOf(stickyNextCandidates[sn]);
537
+ if (sni !== -1 && (stickyNextIdx === -1 || sni < stickyNextIdx)) stickyNextIdx = sni;
538
+ }
539
+ var existing;
540
+ if (stickyNextIdx !== -1) {
541
+ existing = afterMarker.substring(0, stickyNextIdx).trimEnd();
542
+ } else {
543
+ existing = afterMarker.trimEnd();
544
+ }
545
+ if (existing === STICKY_NOTES_SECTION.trimStart().trimEnd()) return false;
546
+
547
+ var endOfSection = stickyNextIdx !== -1 ? markerIdx + stickyNextIdx : content.length;
548
+ content = content.substring(0, markerIdx).trimEnd() + content.substring(endOfSection);
549
+ }
550
+
551
+ var stickyInsertBefore = -1;
552
+ var stickyInsertCandidates = [DEBATE_AWARENESS_MARKER, crisisSafety.MARKER];
553
+ for (var si = 0; si < stickyInsertCandidates.length; si++) {
554
+ var sip = content.indexOf(stickyInsertCandidates[si]);
555
+ if (sip !== -1) { stickyInsertBefore = sip; break; }
556
+ }
557
+ if (stickyInsertBefore !== -1) {
558
+ content = content.substring(0, stickyInsertBefore).trimEnd() + STICKY_NOTES_SECTION + "\n\n" + content.substring(stickyInsertBefore);
559
+ } else {
560
+ content = content.trimEnd() + STICKY_NOTES_SECTION;
561
+ }
562
+
563
+ fs.writeFileSync(filePath, content, "utf8");
564
+ return true;
565
+ }
566
+
567
+ // --- Debate awareness ---
568
+
569
+ var DEBATE_AWARENESS_MARKER = "<!-- DEBATE_AWARENESS_MANAGED_BY_SYSTEM -->";
570
+
571
+ var DEBATE_AWARENESS_SECTION =
572
+ "\n\n" + DEBATE_AWARENESS_MARKER + "\n" +
573
+ "## Proposing Debates\n\n" +
574
+ "**This section is managed by the system and cannot be removed.**\n\n" +
575
+ "When the user suggests that a topic would benefit from a multi-perspective debate " +
576
+ "(e.g., \"let's debate this\", \"I want to hear different viewpoints\"), you can propose " +
577
+ "a structured debate by writing a brief file.\n\n" +
578
+ "**How to propose a debate:**\n" +
579
+ "1. Generate a unique ID: `debate_` followed by the current timestamp in milliseconds\n" +
580
+ "2. Write the brief as JSON to: `.clay/debates/<debate_id>/brief.json` (relative to the project root)\n" +
581
+ "3. The system will detect the file and show the user an inline card with your proposal\n" +
582
+ "4. The user can then approve or cancel the debate\n\n" +
583
+ "**Brief JSON schema:**\n" +
584
+ "```json\n" +
585
+ "{\n" +
586
+ " \"topic\": \"The refined debate topic\",\n" +
587
+ " \"format\": \"free_discussion\",\n" +
588
+ " \"context\": \"Key context from the conversation that panelists should know\",\n" +
589
+ " \"specialRequests\": \"Any special instructions, or null\",\n" +
590
+ " \"panelists\": [\n" +
591
+ " {\n" +
592
+ " \"mateId\": \"<mate UUID from the team roster above>\",\n" +
593
+ " \"role\": \"The perspective or stance this panelist should take\",\n" +
594
+ " \"brief\": \"Specific guidance for this panelist\"\n" +
595
+ " }\n" +
596
+ " ]\n" +
597
+ "}\n" +
598
+ "```\n\n" +
599
+ "**Rules:**\n" +
600
+ "- Choose 2-4 panelists from the team roster. Pick mates whose expertise fits the topic.\n" +
601
+ "- Do NOT include yourself as a panelist. You will moderate the debate.\n" +
602
+ "- Only propose a debate when the user explicitly asks for one.\n" +
603
+ "- Make sure the directory exists before writing (use mkdir -p or equivalent).\n";
604
+
605
+ function enforceDebateAwareness(filePath) {
606
+ if (!fs.existsSync(filePath)) return false;
607
+
608
+ var content = fs.readFileSync(filePath, "utf8");
609
+
610
+ var markerIdx = content.indexOf(DEBATE_AWARENESS_MARKER);
433
611
  if (markerIdx !== -1) {
434
612
  var afterMarker = content.substring(markerIdx);
435
613
  var crisisIdx = afterMarker.indexOf(crisisSafety.MARKER);
@@ -439,7 +617,7 @@ function enforceStickyNotes(filePath) {
439
617
  } else {
440
618
  existing = afterMarker.trimEnd();
441
619
  }
442
- if (existing === STICKY_NOTES_SECTION.trimStart().trimEnd()) return false;
620
+ if (existing === DEBATE_AWARENESS_SECTION.trimStart().trimEnd()) return false;
443
621
 
444
622
  var endOfSection = crisisIdx !== -1 ? markerIdx + crisisIdx : content.length;
445
623
  content = content.substring(0, markerIdx).trimEnd() + content.substring(endOfSection);
@@ -447,9 +625,9 @@ function enforceStickyNotes(filePath) {
447
625
 
448
626
  var crisisPos = content.indexOf(crisisSafety.MARKER);
449
627
  if (crisisPos !== -1) {
450
- content = content.substring(0, crisisPos).trimEnd() + STICKY_NOTES_SECTION + "\n\n" + content.substring(crisisPos);
628
+ content = content.substring(0, crisisPos).trimEnd() + DEBATE_AWARENESS_SECTION + "\n\n" + content.substring(crisisPos);
451
629
  } else {
452
- content = content.trimEnd() + STICKY_NOTES_SECTION;
630
+ content = content.trimEnd() + DEBATE_AWARENESS_SECTION;
453
631
  }
454
632
 
455
633
  fs.writeFileSync(filePath, content, "utf8");
@@ -715,6 +893,12 @@ module.exports = {
715
893
  enforceStickyNotes: enforceStickyNotes,
716
894
  STICKY_NOTES_MARKER: STICKY_NOTES_MARKER,
717
895
  STICKY_NOTES_SECTION: STICKY_NOTES_SECTION,
896
+ enforceProjectRegistry: enforceProjectRegistry,
897
+ buildProjectRegistrySection: buildProjectRegistrySection,
898
+ PROJECT_REGISTRY_MARKER: PROJECT_REGISTRY_MARKER,
899
+ enforceDebateAwareness: enforceDebateAwareness,
900
+ DEBATE_AWARENESS_MARKER: DEBATE_AWARENESS_MARKER,
901
+ DEBATE_AWARENESS_SECTION: DEBATE_AWARENESS_SECTION,
718
902
  createBuiltinMate: createBuiltinMate,
719
903
  getInstalledBuiltinKeys: getInstalledBuiltinKeys,
720
904
  getMissingBuiltinKeys: getMissingBuiltinKeys,