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.
- package/lib/build-user-env.js +57 -0
- package/lib/mates.js +202 -18
- package/lib/project.js +311 -14
- package/lib/public/app.js +185 -351
- package/lib/public/css/debate.css +263 -0
- package/lib/public/modules/debate.js +289 -0
- package/lib/public/modules/sidebar.js +3 -0
- package/lib/sdk-bridge.js +7 -5
- package/lib/server.js +24 -2
- package/lib/terminal.js +6 -9
- package/package.json +1 -1
|
@@ -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
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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 (
|
|
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
|
|
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 (
|
|
383
|
-
existing = afterMem.substring(0,
|
|
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 =
|
|
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
|
|
396
|
-
|
|
397
|
-
|
|
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 ===
|
|
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() +
|
|
628
|
+
content = content.substring(0, crisisPos).trimEnd() + DEBATE_AWARENESS_SECTION + "\n\n" + content.substring(crisisPos);
|
|
451
629
|
} else {
|
|
452
|
-
content = content.trimEnd() +
|
|
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,
|