clay-server 5.1.0-beta.2 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/bin/cli.js +9 -91
  2. package/lib/builtin-mates.js +7 -7
  3. package/lib/daemon-settings.js +11 -0
  4. package/lib/daemon.js +8 -37
  5. package/lib/linear-store.js +35 -8
  6. package/lib/mate-identity-file.js +39 -0
  7. package/lib/mate-knowledge-sync.js +1 -1
  8. package/lib/mate-ready-creation.js +2 -1
  9. package/lib/mates-identity.js +4 -4
  10. package/lib/mates-prompts.js +4 -3
  11. package/lib/mates.js +12 -11
  12. package/lib/os-users.js +5 -16
  13. package/lib/project-connection.js +0 -7
  14. package/lib/project-debate.js +3 -2
  15. package/lib/project-filesystem.js +21 -39
  16. package/lib/project-mate-interaction.js +7 -6
  17. package/lib/project-server-settings.js +31 -0
  18. package/lib/project-sessions.js +57 -184
  19. package/lib/project.js +3 -3
  20. package/lib/public/app.js +6 -37
  21. package/lib/public/css/input.css +3 -1
  22. package/lib/public/css/menus.css +3 -0
  23. package/lib/public/css/overlays.css +7 -0
  24. package/lib/public/css/project-settings-modal.css +56 -0
  25. package/lib/public/css/server-settings-system.css +125 -0
  26. package/lib/public/css/session-github.css +25 -16
  27. package/lib/public/css/settings-ui.css +247 -22
  28. package/lib/public/css/shared-browser.css +7 -3
  29. package/lib/public/css/title-bar.css +5 -1
  30. package/lib/public/index.html +173 -447
  31. package/lib/public/modules/account-island.js +9 -7
  32. package/lib/public/modules/admin-settings-email.js +197 -0
  33. package/lib/public/modules/admin-settings-invitations.js +183 -0
  34. package/lib/public/modules/admin-settings-projects.js +198 -0
  35. package/lib/public/modules/admin-settings-ui.js +100 -0
  36. package/lib/public/modules/admin-settings-users.js +331 -0
  37. package/lib/public/modules/admin.js +19 -949
  38. package/lib/public/modules/app-dm.js +0 -2
  39. package/lib/public/modules/app-messages.js +13 -52
  40. package/lib/public/modules/app-rate-limit.js +70 -12
  41. package/lib/public/modules/avatar.js +6 -1
  42. package/lib/public/modules/context-sources.js +27 -18
  43. package/lib/public/modules/default-ai.js +14 -7
  44. package/lib/public/modules/header-tui-font.js +10 -20
  45. package/lib/public/modules/home-mate-avatar-editor.js +2 -2
  46. package/lib/public/modules/linear-settings.js +3 -2
  47. package/lib/public/modules/markdown.js +5 -6
  48. package/lib/public/modules/notification-settings.js +81 -0
  49. package/lib/public/modules/notifications.js +44 -28
  50. package/lib/public/modules/personal-settings-dialog.js +51 -0
  51. package/lib/public/modules/profile.js +15 -6
  52. package/lib/public/modules/project-settings-environment.js +106 -0
  53. package/lib/public/modules/project-settings.js +72 -628
  54. package/lib/public/modules/server-settings-controls.js +316 -0
  55. package/lib/public/modules/server-settings-environment.js +194 -0
  56. package/lib/public/modules/server-settings-navigation.js +39 -0
  57. package/lib/public/modules/server-settings-state.js +54 -0
  58. package/lib/public/modules/server-settings.js +116 -788
  59. package/lib/public/modules/server-storage-settings.js +84 -0
  60. package/lib/public/modules/session-github.js +2 -1
  61. package/lib/public/modules/session-presence-ui.js +1 -1
  62. package/lib/public/modules/settings-env-values.js +34 -0
  63. package/lib/public/modules/settings-select.js +121 -0
  64. package/lib/public/modules/shared-browser.js +22 -12
  65. package/lib/public/modules/stt-picker.js +14 -5
  66. package/lib/public/modules/terminal-prefs.js +22 -0
  67. package/lib/public/modules/theme.js +88 -52
  68. package/lib/public/modules/user-settings-account.js +318 -0
  69. package/lib/public/modules/user-settings-appearance.js +55 -0
  70. package/lib/public/modules/user-settings-editors.js +24 -0
  71. package/lib/public/modules/user-settings-integrations.js +104 -0
  72. package/lib/public/modules/user-settings-layout.js +80 -0
  73. package/lib/public/modules/user-settings-preferences.js +48 -0
  74. package/lib/public/modules/user-settings-profile.js +53 -0
  75. package/lib/public/modules/user-settings-themes.js +38 -0
  76. package/lib/public/modules/user-settings-voice.js +75 -0
  77. package/lib/public/modules/user-settings.js +53 -480
  78. package/lib/public/style.css +4 -0
  79. package/lib/sdk-message-processor.js +16 -4
  80. package/lib/sdk-quota-snapshot.js +37 -0
  81. package/lib/server-appearance-settings.js +71 -0
  82. package/lib/server-linear.js +14 -8
  83. package/lib/server-mate-instructions.js +2 -2
  84. package/lib/server-mates.js +2 -1
  85. package/lib/server-notification-settings.js +67 -0
  86. package/lib/server-settings.js +4 -0
  87. package/lib/server-storage.js +57 -0
  88. package/lib/server.js +10 -2
  89. package/lib/sessions.js +3 -4
  90. package/lib/startup-projects.js +40 -0
  91. package/lib/storage-native-walk.js +85 -0
  92. package/lib/storage-usage.js +144 -0
  93. package/lib/users-preferences.js +0 -74
  94. package/lib/users.js +0 -4
  95. package/lib/ws-schema.js +2 -4
  96. package/lib/yoke/acp-process-manager.js +2 -2
  97. package/lib/yoke/adapters/codex.js +12 -52
  98. package/lib/yoke/codex-app-server.js +2 -2
  99. package/lib/yoke/codex-model-catalog.js +52 -0
  100. package/lib/yoke/codex-rate-limits.js +26 -0
  101. package/package.json +3 -3
package/bin/cli.js CHANGED
@@ -37,6 +37,7 @@ var { generateAuthToken } = require("../lib/server");
37
37
  var { enableMultiUser, disableMultiUser, hasAdmin, isMultiUser, getSetupCode } = require("../lib/users");
38
38
  var clayStudioCert = require("../lib/clay-studio-cert");
39
39
  var cliWordmark = require("../lib/cli-wordmark");
40
+ var buildStartupProjects = require("../lib/startup-projects").buildStartupProjects;
40
41
 
41
42
  function openUrl(url) {
42
43
  try {
@@ -446,9 +447,6 @@ async function restartDaemonFromConfig() {
446
447
  var newConfig = Object.assign({}, lastConfig, {
447
448
  pid: null,
448
449
  port: targetPort,
449
- projects: (lastConfig.projects || []).filter(function (p) {
450
- return fs.existsSync(p.path);
451
- })
452
450
  });
453
451
 
454
452
  ensureConfigDir();
@@ -1445,56 +1443,15 @@ async function forkDaemon(mode, keepAwake, extraProjects, addCwd, wantOsUsers) {
1445
1443
  return;
1446
1444
  }
1447
1445
 
1448
- var allProjects = [];
1449
- var usedSlugs = [];
1450
-
1451
- // Load previous config to preserve per-project settings (visibility, allowedUsers)
1452
1446
  var prevConfig = loadConfig();
1453
- var prevProjectMap = {};
1454
- if (prevConfig && prevConfig.projects) {
1455
- for (var pi = 0; pi < prevConfig.projects.length; pi++) {
1456
- prevProjectMap[prevConfig.projects[pi].path] = prevConfig.projects[pi];
1457
- }
1458
- }
1459
-
1460
- // Only include cwd if explicitly requested
1447
+ var recentProjects = extraProjects || [];
1461
1448
  if (addCwd) {
1462
- var slug = generateSlug(cwd, []);
1463
- var cwdEntry = { path: cwd, slug: slug, addedAt: Date.now() };
1464
- // Restore title/icon from .clayrc if available
1465
- var cwdRc = loadClayrc();
1466
- var cwdRecent = cwdRc.recentProjects || [];
1467
- for (var cr = 0; cr < cwdRecent.length; cr++) {
1468
- if (cwdRecent[cr].path === cwd) {
1469
- if (cwdRecent[cr].title) cwdEntry.title = cwdRecent[cr].title;
1470
- if (cwdRecent[cr].icon) cwdEntry.icon = cwdRecent[cr].icon;
1471
- break;
1472
- }
1473
- }
1474
- // Restore project-level settings from previous config
1475
- if (prevProjectMap[cwd]) {
1476
- cwdEntry = Object.assign({}, prevProjectMap[cwd], cwdEntry);
1477
- }
1478
- allProjects.push(cwdEntry);
1479
- usedSlugs.push(slug);
1480
- }
1481
-
1482
- // Add restored projects (from ~/.clayrc)
1483
- if (extraProjects && extraProjects.length > 0) {
1484
- for (var ep = 0; ep < extraProjects.length; ep++) {
1485
- var rp = extraProjects[ep];
1486
- if (rp.path === cwd) continue; // skip if same as cwd
1487
- if (!fs.existsSync(rp.path)) continue; // skip missing directories
1488
- var rpSlug = generateSlug(rp.path, usedSlugs);
1489
- usedSlugs.push(rpSlug);
1490
- var rpEntry = { path: rp.path, slug: rpSlug, title: rp.title || undefined, icon: rp.icon || undefined, addedAt: rp.addedAt || Date.now() };
1491
- // Restore project-level settings from previous config
1492
- if (prevProjectMap[rp.path]) {
1493
- rpEntry = Object.assign({}, prevProjectMap[rp.path], rpEntry);
1494
- }
1495
- allProjects.push(rpEntry);
1496
- }
1449
+ var cwdRecent = (loadClayrc().recentProjects || []).filter(function (p) { return p.path === cwd; });
1450
+ recentProjects = cwdRecent.concat(recentProjects);
1497
1451
  }
1452
+ var allProjects = buildStartupProjects(prevConfig, recentProjects, {
1453
+ cwd: cwd, addCwd: addCwd, existsSync: fs.existsSync, generateSlug: generateSlug,
1454
+ });
1498
1455
 
1499
1456
  var config = Object.assign({}, prevConfig || {}, {
1500
1457
  pid: null,
@@ -1629,49 +1586,10 @@ async function devMode(mode, keepAwake, existingPinHash, wantOsUsers) {
1629
1586
  return;
1630
1587
  }
1631
1588
 
1632
- var slug = generateSlug(cwd, []);
1633
- var cwdDevEntry = { path: cwd, slug: slug, addedAt: Date.now() };
1634
-
1635
- // Load previous config to preserve per-project settings (visibility, allowedUsers)
1636
1589
  var prevDevConfig = loadConfig();
1637
- var prevDevProjectMap = {};
1638
- if (prevDevConfig && prevDevConfig.projects) {
1639
- for (var pdi = 0; pdi < prevDevConfig.projects.length; pdi++) {
1640
- prevDevProjectMap[prevDevConfig.projects[pdi].path] = prevDevConfig.projects[pdi];
1641
- }
1642
- }
1643
-
1644
- // Restore previous projects
1645
- var rc = loadClayrc();
1646
- var restorable = (rc.recentProjects || []).filter(function (p) {
1647
- return p.path !== cwd && fs.existsSync(p.path);
1590
+ var allProjects = buildStartupProjects(prevDevConfig, loadClayrc().recentProjects || [], {
1591
+ cwd: cwd, addCwd: true, existsSync: fs.existsSync, generateSlug: generateSlug,
1648
1592
  });
1649
- // Restore title/icon for cwd from .clayrc
1650
- var rcAll = rc.recentProjects || [];
1651
- for (var ci = 0; ci < rcAll.length; ci++) {
1652
- if (rcAll[ci].path === cwd) {
1653
- if (rcAll[ci].title) cwdDevEntry.title = rcAll[ci].title;
1654
- if (rcAll[ci].icon) cwdDevEntry.icon = rcAll[ci].icon;
1655
- break;
1656
- }
1657
- }
1658
- // Restore access settings for cwd from previous config
1659
- if (prevDevProjectMap[cwd]) {
1660
- cwdDevEntry = Object.assign({}, prevDevProjectMap[cwd], cwdDevEntry);
1661
- }
1662
- var allProjects = [cwdDevEntry];
1663
- var usedSlugs = [slug];
1664
- for (var ri = 0; ri < restorable.length; ri++) {
1665
- var rp = restorable[ri];
1666
- var rpSlug = generateSlug(rp.path, usedSlugs);
1667
- usedSlugs.push(rpSlug);
1668
- var rpDevEntry = { path: rp.path, slug: rpSlug, title: rp.title || undefined, icon: rp.icon || undefined, addedAt: rp.addedAt || Date.now() };
1669
- // Restore project-level settings from previous config
1670
- if (prevDevProjectMap[rp.path]) {
1671
- rpDevEntry = Object.assign({}, prevDevProjectMap[rp.path], rpDevEntry);
1672
- }
1673
- allProjects.push(rpDevEntry);
1674
- }
1675
1593
 
1676
1594
  var config = Object.assign({}, prevDevConfig || {}, {
1677
1595
  pid: null,
@@ -156,7 +156,7 @@ var BUILTIN_MATES = [
156
156
  ];
157
157
 
158
158
  // ---------------------------------------------------------------------------
159
- // ALLY CLAUDE.md template
159
+ // ALLY AGENTS.md template
160
160
  // ---------------------------------------------------------------------------
161
161
 
162
162
  var ALLY_TEMPLATE =
@@ -245,7 +245,7 @@ var ALLY_TEMPLATE =
245
245
  "- Do not promote transient information or emotional states.\n";
246
246
 
247
247
  // ---------------------------------------------------------------------------
248
- // CLAY CLAUDE.md template (host agent — the app itself answers)
248
+ // CLAY AGENTS.md template (host agent — the app itself answers)
249
249
  // ---------------------------------------------------------------------------
250
250
 
251
251
  var CLAY_TEMPLATE =
@@ -321,7 +321,7 @@ var CLAY_TEMPLATE =
321
321
  "After that, jump straight into search. No more meta-conversation.\n";
322
322
 
323
323
  // ---------------------------------------------------------------------------
324
- // ARCH CLAUDE.md template
324
+ // ARCH AGENTS.md template
325
325
  // ---------------------------------------------------------------------------
326
326
 
327
327
  var ARCH_TEMPLATE =
@@ -392,7 +392,7 @@ var ARCH_TEMPLATE =
392
392
  "If nothing is there, work without it and learn through the conversation.\n";
393
393
 
394
394
  // ---------------------------------------------------------------------------
395
- // RUSH CLAUDE.md template
395
+ // RUSH AGENTS.md template
396
396
  // ---------------------------------------------------------------------------
397
397
 
398
398
  var RUSH_TEMPLATE =
@@ -459,7 +459,7 @@ var RUSH_TEMPLATE =
459
459
  "and deadlines. Use what is available. If nothing is there, work without it.\n";
460
460
 
461
461
  // ---------------------------------------------------------------------------
462
- // WARD CLAUDE.md template
462
+ // WARD AGENTS.md template
463
463
  // ---------------------------------------------------------------------------
464
464
 
465
465
  var WARD_TEMPLATE =
@@ -527,7 +527,7 @@ var WARD_TEMPLATE =
527
527
  "tech stack, and known risk areas. Use what is available. If nothing is there, work without it.\n";
528
528
 
529
529
  // ---------------------------------------------------------------------------
530
- // PIXEL CLAUDE.md template
530
+ // PIXEL AGENTS.md template
531
531
  // ---------------------------------------------------------------------------
532
532
 
533
533
  var PIXEL_TEMPLATE =
@@ -599,7 +599,7 @@ var PIXEL_TEMPLATE =
599
599
  "target audience, and UX decisions. Use what is available. If nothing is there, work without it.\n";
600
600
 
601
601
  // ---------------------------------------------------------------------------
602
- // BUZZ CLAUDE.md template
602
+ // BUZZ AGENTS.md template
603
603
  // ---------------------------------------------------------------------------
604
604
 
605
605
  var BUZZ_TEMPLATE =
@@ -0,0 +1,11 @@
1
+ // Persist before publishing a setting to live daemon state.
2
+ function saveDaemonSetting(config, key, value, saveConfig) {
3
+ var overrides = {}; overrides[key] = value;
4
+ var next = Object.assign({}, config, overrides);
5
+ if (value === undefined) delete next[key];
6
+ saveConfig(next);
7
+ if (value === undefined) delete config[key];
8
+ Object.assign(config, next);
9
+ return next;
10
+ }
11
+ module.exports = { saveDaemonSetting: saveDaemonSetting };
package/lib/daemon.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ var saveDaemonSetting = require("./daemon-settings").saveDaemonSetting;
2
3
 
3
4
  // --- Node version check ---
4
5
  var nodeMajor = parseInt(process.versions.node.split(".")[0], 10);
@@ -72,9 +73,7 @@ if (projectLogContext.initializeProjectKnowledge(config)) {
72
73
  }
73
74
 
74
75
  // --- OS users mode: check required system dependencies ---
75
- // Supplementary-group inheritance (issue #367): default on so sessions behave
76
- // like a normal login; operators can disable via config / the settings toggle.
77
- osUsersMod.setInheritGroups(config.inheritGroups !== false);
76
+ // User-switched processes inherit the account groups, like a normal login.
78
77
  if (config.osUsers) {
79
78
  var checkExec = require("child_process").execFileSync;
80
79
  var missing = [];
@@ -86,11 +85,7 @@ if (config.osUsers) {
86
85
  console.error("[daemon] Install with: sudo apt install " + missing.map(function (m) { return m.split(" ")[0]; }).join(" "));
87
86
  process.exit(78); // EX_CONFIG
88
87
  }
89
- if (config.inheritGroups !== false) {
90
- console.log("[daemon] Sessions inherit each user's supplementary groups (docker, etc.); disable in Settings > User Groups.");
91
- } else {
92
- console.log("[daemon] Sessions run with primary group only (supplementary groups dropped); enable in Settings > User Groups.");
93
- }
88
+ console.log("[daemon] Sessions inherit each user's supplementary groups when setpriv is available.");
94
89
  }
95
90
 
96
91
  // --- TLS ---
@@ -637,12 +632,7 @@ var relay = createServer({
637
632
  return { envrc: config.sharedEnv || "" };
638
633
  },
639
634
  onSetSharedEnv: function (envrc) {
640
- if (envrc) {
641
- config.sharedEnv = envrc;
642
- } else {
643
- delete config.sharedEnv;
644
- }
645
- saveConfig(config);
635
+ saveDaemonSetting(config, "sharedEnv", envrc || undefined, saveConfig);
646
636
  return { ok: true };
647
637
  },
648
638
  onGetServerDefaultEffort: function () {
@@ -812,7 +802,6 @@ var relay = createServer({
812
802
  keepAwake: !!config.keepAwake,
813
803
  autoContinueOnRateLimit: !!config.autoContinueOnRateLimit,
814
804
  osUsers: !!config.osUsers,
815
- inheritGroups: config.inheritGroups !== false,
816
805
  chatLayout: config.chatLayout || "channel",
817
806
  matesEnabled: config.matesEnabled === true,
818
807
  capsulesEnabled: config.capsulesEnabled === true,
@@ -827,25 +816,18 @@ var relay = createServer({
827
816
  onSetImageRetention: function (days) {
828
817
  var d = parseInt(days, 10);
829
818
  if (isNaN(d) || d < 0) d = 7;
830
- config.imageRetentionDays = d;
831
- saveConfig(config);
819
+ saveDaemonSetting(config, "imageRetentionDays", d, saveConfig);
832
820
  console.log("[daemon] Image retention:", d === 0 ? "forever" : d + " days");
833
821
  return { ok: true, days: d };
834
822
  },
835
823
  onSetUpdateChannel: function (channel) {
836
- config.updateChannel = channel === "beta" ? "beta" : "stable";
837
- saveConfig(config);
824
+ saveDaemonSetting(config, "updateChannel", channel === "beta" ? "beta" : "stable", saveConfig);
838
825
  console.log("[daemon] Update channel:", config.updateChannel, "(web)");
839
826
  return { ok: true, updateChannel: config.updateChannel };
840
827
  },
841
828
  onSetPin: function (pin) {
842
- if (pin) {
843
- config.pinHash = generateAuthToken(pin);
844
- } else {
845
- config.pinHash = null;
846
- }
829
+ saveDaemonSetting(config, "pinHash", pin ? generateAuthToken(pin) : null, saveConfig);
847
830
  relay.setAuthToken(config.pinHash);
848
- saveConfig(config);
849
831
  console.log("[daemon] PIN", pin ? "set" : "removed", "(web)");
850
832
  return { ok: true, pinEnabled: !!config.pinHash };
851
833
  },
@@ -921,8 +903,7 @@ var relay = createServer({
921
903
  },
922
904
  onSetKeepAwake: function (value) {
923
905
  var want = !!value;
924
- config.keepAwake = want;
925
- saveConfig(config);
906
+ saveDaemonSetting(config, "keepAwake", want, saveConfig);
926
907
  if (want && !caffeinateProc && process.platform === "darwin") {
927
908
  try {
928
909
  var { spawn: spawnCaff } = require("child_process");
@@ -936,16 +917,6 @@ var relay = createServer({
936
917
  console.log("[daemon] Keep awake:", want, "(web)");
937
918
  return { ok: true, keepAwake: want };
938
919
  },
939
- onSetInheritGroups: function (value) {
940
- var want = !!value;
941
- config.inheritGroups = want;
942
- saveConfig(config);
943
- // Apply live so subsequent session/terminal spawns pick it up without a
944
- // daemon restart; already-running sessions keep their current groups.
945
- osUsersMod.setInheritGroups(want);
946
- console.log("[daemon] Inherit supplementary groups:", want, "(web)");
947
- return { ok: true, inheritGroups: want };
948
- },
949
920
  onShutdown: function () {
950
921
  console.log("[daemon] Shutdown requested via web UI");
951
922
  gracefulShutdown();
@@ -19,34 +19,61 @@ function attachLinearStore(root) {
19
19
  catch (error) { if (error.code !== 'EEXIST') throw error; }
20
20
  return fs.readFileSync(file);
21
21
  }
22
- function key(owner) {
22
+ function record(owner, id) {
23
23
  var data = read(owner);
24
+ return !id || id === "default" ? data : Object.hasOwn(data.connections || {}, id) ? data.connections[id] : {};
25
+ }
26
+ function key(owner, id) {
27
+ var data = record(owner, id);
24
28
  if (!data.secret) return null;
25
29
  var parts = data.secret.split('.').map(function (part) { return Buffer.from(part, 'base64'); });
26
30
  var cipher = crypto.createDecipheriv('aes-256-gcm', masterKey(), parts[0]);
27
- cipher.setAAD(Buffer.from(owner)); cipher.setAuthTag(parts[1]);
31
+ cipher.setAAD(Buffer.from(id && id !== 'default' ? owner + ':' + id : owner)); cipher.setAuthTag(parts[1]);
28
32
  return Buffer.concat([cipher.update(parts[2]), cipher.final()]).toString('utf8');
29
33
  }
30
- function view(owner) {
31
- var data = read(owner);
34
+ function view(owner, id) {
35
+ var data = record(owner, id);
32
36
  return { connected: !!data.secret, workspace: data.workspace || null, revision: data.revision || null };
33
37
  }
34
- function save(owner, secret, workspace) {
38
+ function save(owner, secret, workspace, id, name) {
39
+ if (id && (!/^[a-zA-Z0-9_-]{1,64}$/.test(id) || ['__proto__', 'constructor', 'prototype'].indexOf(id) >= 0)) throw new Error("Invalid connection.");
40
+ var previous = read(owner);
35
41
  var file = filename(owner), data = { revision: crypto.randomBytes(16).toString('hex') };
36
42
  if (secret !== null) {
37
43
  if (typeof secret !== 'string' || !secret.trim() || secret.length > 4096 || /[\r\n]/.test(secret)) throw new Error('Enter a valid Linear API key.');
38
44
  var iv = crypto.randomBytes(12), cipher = crypto.createCipheriv('aes-256-gcm', masterKey(), iv);
39
- cipher.setAAD(Buffer.from(owner));
45
+ cipher.setAAD(Buffer.from(id && id !== 'default' ? owner + ':' + id : owner));
40
46
  var bytes = Buffer.concat([cipher.update(secret.trim(), 'utf8'), cipher.final()]);
41
47
  data.secret = [iv, cipher.getAuthTag(), bytes].map(function (part) { return part.toString('base64'); }).join('.');
42
48
  data.workspace = workspace;
43
49
  }
50
+ if (name !== undefined) {
51
+ if (typeof name !== 'string' || !name.trim() || name.trim().length > 80) throw new Error('Enter a connection name of up to 80 characters.');
52
+ data.name = name.trim();
53
+ } else data.name = record(owner, id).name;
54
+ if (id && id !== 'default') {
55
+ previous.connections = previous.connections || {};
56
+ if (secret === null) delete previous.connections[id];
57
+ else {
58
+ if (!previous.connections[id] && Object.keys(previous.connections).length >= 30) throw new Error('Connection limit reached.');
59
+ previous.connections[id] = data;
60
+ }
61
+ data = previous;
62
+ } else data.connections = previous.connections;
44
63
  fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
45
64
  var temp = file + '.' + crypto.randomBytes(8).toString('hex') + '.tmp';
46
65
  try { fs.writeFileSync(temp, JSON.stringify(data), { mode: 0o600 }); fs.renameSync(temp, file); }
47
66
  finally { try { fs.unlinkSync(temp); } catch (error) { if (error.code !== 'ENOENT') throw error; } }
48
- return view(owner);
67
+ return view(owner, id);
68
+ }
69
+ function list(owner) {
70
+ var data = read(owner), entries = [];
71
+ if (data.secret) entries.push(Object.assign({ id: 'default', name: data.name || 'Existing connection', legacy: true }, view(owner)));
72
+ Object.keys(data.connections || {}).forEach(function (id) {
73
+ entries.push(Object.assign({ id: id, name: data.connections[id].name || 'Linear', legacy: false }, view(owner, id)));
74
+ });
75
+ return entries;
49
76
  }
50
- return { key: key, view: view, save: save };
77
+ return { key: key, view: view, save: save, list: list };
51
78
  }
52
79
  module.exports = { attachLinearStore: attachLinearStore };
@@ -0,0 +1,39 @@
1
+ // Resolve the canonical Mate identity without overwriting either document.
2
+ var fs = require('fs');
3
+ var path = require('path');
4
+ var crypto = require('crypto');
5
+
6
+ function regularFile(file) {
7
+ try {
8
+ var stat = fs.lstatSync(file);
9
+ if (!stat.isFile()) throw new Error('Mate identity must be a regular file: ' + file);
10
+ return stat;
11
+ } catch (error) {
12
+ if (error.code === 'ENOENT') return null;
13
+ throw error;
14
+ }
15
+ }
16
+
17
+ function resolveMateIdentityFile(directory) {
18
+ var canonical = path.join(directory, 'AGENTS.md');
19
+ var legacy = path.join(directory, 'CLAUDE.md');
20
+ var current = regularFile(canonical);
21
+ if (!regularFile(legacy)) return canonical;
22
+ if (!current) {
23
+ // Exclusive linking preserves owner, mode and bytes and never replaces a
24
+ // concurrently created canonical file. Unlink only after publication.
25
+ try { fs.linkSync(legacy, canonical); }
26
+ catch (error) {
27
+ if (error.code !== 'EEXIST') throw error;
28
+ return resolveMateIdentityFile(directory);
29
+ }
30
+ } else if (fs.statSync(canonical).ino !== fs.statSync(legacy).ino) {
31
+ // Preserve a conflicting legacy document outside instruction discovery.
32
+ var backup = path.join(directory, '.identity-legacy-' + crypto.randomBytes(12).toString('hex') + '.md');
33
+ fs.linkSync(legacy, backup);
34
+ }
35
+ fs.unlinkSync(legacy);
36
+ return canonical;
37
+ }
38
+
39
+ module.exports = {resolveMateIdentityFile: resolveMateIdentityFile};
@@ -12,7 +12,7 @@
12
12
  // call idempotent and makes deletions and rewrites work without the call site
13
13
  // having to describe what changed.
14
14
  //
15
- // Identity is never mirrored. CLAUDE.md, base-template.md, identity-backup.md,
15
+ // Identity is never mirrored. AGENTS.md, legacy CLAUDE.md, base-template.md, identity-backup.md,
16
16
  // and identity-history.jsonl define who a Mate is and remain canonical files.
17
17
  // Sticky notes are a separate attention layer and are excluded too.
18
18
 
@@ -1,3 +1,4 @@
1
+ var resolveMateIdentityFile = require('./mate-identity-file').resolveMateIdentityFile;
1
2
  // Atomic finalization of an interviewed Mate. No placeholder is exposed unless
2
3
  // the complete identity has passed validation and is ready to register.
3
4
 
@@ -55,7 +56,7 @@ function createReadyMate(mates, mateCtx, definition) {
55
56
  autonomy: ready.autonomy,
56
57
  });
57
58
  var mateDir = mates.getMateDir(mateCtx, mate.id);
58
- var identityPath = path.join(mateDir, "CLAUDE.md");
59
+ var identityPath = resolveMateIdentityFile(mateDir);
59
60
  fs.writeFileSync(identityPath, ready.identityMarkdown + "\n");
60
61
  mates.enforceAllSections(identityPath, { ctx: mateCtx, mateId: mate.id });
61
62
  fs.mkdirSync(path.join(mateDir, "knowledge"), { recursive: true });
@@ -2,7 +2,7 @@
2
2
  * Mates identity module -- Identity extraction, backup, and change tracking.
3
3
  *
4
4
  * Manages the boundary between a mate's user-authored identity and
5
- * system-managed sections in CLAUDE.md.
5
+ * system-managed sections in AGENTS.md.
6
6
  * Extracted from mates.js to keep module sizes manageable.
7
7
  */
8
8
 
@@ -18,7 +18,7 @@ var IDENTITY_MIN_LENGTH = 50;
18
18
  /**
19
19
  * Build the capabilities section for a primary mate.
20
20
  * Injected as a system section so it auto-updates with code changes
21
- * without touching the mate's identity in CLAUDE.md.
21
+ * without touching the mate's identity in AGENTS.md.
22
22
  */
23
23
  function buildPrimaryCapabilitiesSection(mate) {
24
24
  if (!mate || !mate.primary) return "";
@@ -52,9 +52,9 @@ function buildPrimaryCapabilitiesSection(mate) {
52
52
  }
53
53
 
54
54
  /**
55
- * Extract identity content from a CLAUDE.md string.
55
+ * Extract identity content from a AGENTS.md string.
56
56
  * Identity is everything before the first system marker.
57
- * @param {string} content - full CLAUDE.md content
57
+ * @param {string} content - full AGENTS.md content
58
58
  * @param {string[]} allMarkers - array of all system marker strings
59
59
  */
60
60
  function extractIdentity(content, allMarkers) {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Mates prompts module -- System section enforcers for mate CLAUDE.md files.
2
+ * Mates prompts module -- System section enforcers for mate AGENTS.md files.
3
3
  *
4
4
  * Each enforcer manages a specific system-managed section (team awareness,
5
5
  * session memory, sticky notes, project registry, debate awareness).
@@ -20,7 +20,7 @@ var TEAM_SECTION =
20
20
  "**This section is managed by the system and cannot be removed.**\n\n" +
21
21
  "You are one of several AI Mates in this workspace. Your teammates and their profiles are listed in `../mates.json`. " +
22
22
  "Each teammate's identity and working style is described in their own directory:\n\n" +
23
- "- `../{mate_id}/CLAUDE.md` -- their identity, personality, and working style\n" +
23
+ "- `../{mate_id}/AGENTS.md` -- their identity, personality, and working style\n" +
24
24
  "- `../{mate_id}/mate.yaml` -- their metadata (name, role, status, activities)\n" +
25
25
  "- `../common-knowledge.json` -- shared knowledge registry; files listed here are readable by all mates\n\n" +
26
26
  "Check the team registry when it would be relevant to know who else is available or what they do. " +
@@ -31,6 +31,7 @@ var SESSION_MEMORY_MARKER = "<!-- SESSION_MEMORY_MANAGED_BY_SYSTEM -->";
31
31
  var SESSION_MEMORY_SECTION =
32
32
  "\n\n" + SESSION_MEMORY_MARKER + "\n" +
33
33
  "## Session Memory\n\n" +
34
+ "Your identity is stored in `AGENTS.md` in your Mate directory. Read and edit that file when reshaping your identity; do not create a separate `CLAUDE.md`.\n\n" +
34
35
  "**This section is managed by the system and cannot be removed.**\n\n" +
35
36
  "Your `knowledge/memory-summary.md` file contains your compressed long-term memory, " +
36
37
  "automatically maintained across sessions. Refer to it for context about past " +
@@ -115,7 +116,7 @@ function buildTeamSection(ctx, currentMateId, loadMates) {
115
116
 
116
117
  section += "\n" +
117
118
  "Each teammate's full identity is in their own directory:\n\n" +
118
- "- `../{mate_id}/CLAUDE.md` -- identity, personality, working style\n" +
119
+ "- `../{mate_id}/AGENTS.md` -- identity, personality, working style\n" +
119
120
  "- `../{mate_id}/mate.yaml` -- metadata (name, role, status, activities)\n" +
120
121
  "- `../common-knowledge.json` -- shared knowledge readable by all mates\n\n" +
121
122
  "Use the **ID** (not the name) when referencing teammates in structured data. " +
package/lib/mates.js CHANGED
@@ -1,3 +1,4 @@
1
+ var resolveMateIdentityFile = require('./mate-identity-file').resolveMateIdentityFile;
1
2
  var fs = require("fs");
2
3
  var path = require("path");
3
4
  var crypto = require("crypto");
@@ -161,7 +162,7 @@ function createMate(ctx, seedData) {
161
162
  yaml += "model: " + JSON.stringify(seedData.model || null) + "\n";
162
163
  fs.writeFileSync(path.join(mateDir, "mate.yaml"), yaml);
163
164
 
164
- // Write initial CLAUDE.md (will be replaced by interview)
165
+ // Write initial AGENTS.md (will be replaced by interview)
165
166
  var claudeMd = "# Mate Identity\n\n";
166
167
  claudeMd += "This mate is currently being interviewed. Identity will be generated after the interview.\n\n";
167
168
  claudeMd += "## Seed Data\n\n";
@@ -176,7 +177,7 @@ function createMate(ctx, seedData) {
176
177
  claudeMd += TEAM_SECTION;
177
178
  claudeMd += SESSION_MEMORY_SECTION;
178
179
  claudeMd += crisisSafety.getSection();
179
- fs.writeFileSync(path.join(mateDir, "CLAUDE.md"), claudeMd);
180
+ fs.writeFileSync(resolveMateIdentityFile(mateDir), claudeMd);
180
181
 
181
182
  // Log creation (identity is placeholder, will be replaced by interview)
182
183
  logIdentityChange(mateDir, "create_custom", initialIdentity, "");
@@ -344,7 +345,7 @@ function migrateLegacyMates() {
344
345
  }
345
346
 
346
347
  /**
347
- * Strip all system sections from CLAUDE.md content, returning only identity.
348
+ * Strip all system sections from AGENTS.md content, returning only identity.
348
349
  */
349
350
  function stripAllSystemSections(content) {
350
351
  return extractIdentity(content);
@@ -354,10 +355,10 @@ function stripAllSystemSections(content) {
354
355
  var _warnedPaths = {};
355
356
 
356
357
  /**
357
- * Atomic enforce: read CLAUDE.md once, enforce all system sections, write once.
358
+ * Atomic enforce: read AGENTS.md once, enforce all system sections, write once.
358
359
  * Includes identity backup, validation, and change tracking.
359
360
  * Returns true if the file was modified, false if already correct.
360
- * @param {string} filePath - path to CLAUDE.md
361
+ * @param {string} filePath - path to AGENTS.md
361
362
  * @param {object} opts - optional { ctx, mateId } for dynamic team section
362
363
  */
363
364
  function enforceAllSections(filePath, opts) {
@@ -529,7 +530,7 @@ function createBuiltinMate(ctx, builtinKey) {
529
530
  yaml += "autonomy: " + (seedData.autonomy || "always_ask") + "\n";
530
531
  fs.writeFileSync(path.join(mateDir, "mate.yaml"), yaml);
531
532
 
532
- // Write CLAUDE.md with full template + system sections
533
+ // Write AGENTS.md with full template + system sections
533
534
  var claudeMd = def.getClaudeMd();
534
535
  var builtinIdentity = claudeMd.trimEnd();
535
536
  claudeMd += TEAM_SECTION;
@@ -537,7 +538,7 @@ function createBuiltinMate(ctx, builtinKey) {
537
538
  claudeMd += STICKY_NOTES_SECTION;
538
539
  claudeMd += DEBATE_AWARENESS_SECTION;
539
540
  claudeMd += crisisSafety.getSection();
540
- fs.writeFileSync(path.join(mateDir, "CLAUDE.md"), claudeMd);
541
+ fs.writeFileSync(resolveMateIdentityFile(mateDir), claudeMd);
541
542
 
542
543
  // Backup identity and log creation
543
544
  backupIdentity(mateDir, builtinIdentity);
@@ -621,7 +622,7 @@ function syncArchivedBuiltinMates(ctx) {
621
622
  *
622
623
  * What gets synced:
623
624
  * - Metadata: bio, name, profile, globalSearch, primary flag
624
- * - CLAUDE.md identity: re-applied when templateVersion changes
625
+ * - AGENTS.md identity: re-applied when templateVersion changes
625
626
  * - templateVersion is stored on the mate object to track updates
626
627
  *
627
628
  * What is NOT synced (regular builtin mates):
@@ -671,10 +672,10 @@ function syncPrimaryMates(ctx) {
671
672
  }
672
673
  }
673
674
 
674
- // --- Sync CLAUDE.md identity (only when templateVersion changes) ---
675
+ // --- Sync AGENTS.md identity (only when templateVersion changes) ---
675
676
  // Uses 3-way comparison to preserve user/Ally modifications:
676
677
  // - base-template.md = the template that was last synced (what code wrote)
677
- // - current identity = what's in CLAUDE.md now (may have been modified)
678
+ // - current identity = what's in AGENTS.md now (may have been modified)
678
679
  // - new template = latest code definition
679
680
  //
680
681
  // If current identity === old base → user didn't touch it → safe to replace
@@ -684,7 +685,7 @@ function syncPrimaryMates(ctx) {
684
685
 
685
686
  if (latestVersion > currentVersion) {
686
687
  var mateDir = getMateDir(ctx, mate.id);
687
- var claudePath = path.join(mateDir, "CLAUDE.md");
688
+ var claudePath = resolveMateIdentityFile(mateDir);
688
689
  var basePath = path.join(mateDir, "knowledge", "base-template.md");
689
690
  var latestIdentity = def.getClaudeMd().trimEnd();
690
691
 
package/lib/os-users.js CHANGED
@@ -17,20 +17,11 @@ function isSafeLinuxUsername(username) {
17
17
  // --- Supplementary group inheritance (issue #367) ---
18
18
  // Node's child_process { uid, gid } options call setuid/setgid but never
19
19
  // initgroups(), so spawned sessions drop all supplementary groups (docker,
20
- // video, wheel, ...). When inheritGroups is enabled we route the spawn through
20
+ // video, wheel, ...). Route user-switched spawns through
21
21
  // `setpriv --init-groups` so the child gets the same groups as a normal login.
22
- var _inheritGroups = true; // default: behave like a normal login shell
23
22
  var _setprivAvail = null;
24
23
  var _warnedNoSetpriv = false;
25
24
 
26
- function setInheritGroups(value) {
27
- _inheritGroups = !!value;
28
- }
29
-
30
- function getInheritGroups() {
31
- return _inheritGroups;
32
- }
33
-
34
25
  function setprivAvailable() {
35
26
  if (_setprivAvail !== null) return _setprivAvail;
36
27
  try {
@@ -50,7 +41,7 @@ function setprivAvailable() {
50
41
  * spawn()/execFileSync()/pty.spawn().
51
42
  *
52
43
  * - Same-user spawns (no uid/gid in options) are returned unchanged.
53
- * - When inheritGroups is disabled, or setpriv is unavailable, falls back to
44
+ * - When setpriv is unavailable, falls back to
54
45
  * Node's uid/gid options (primary group only) so behavior degrades safely.
55
46
  * Requires the parent process to be root, which is the case in os-users mode.
56
47
  */
@@ -62,7 +53,7 @@ function wrapSpawnAsUser(command, args, options, setprivCheck) {
62
53
  if (uid == null || gid == null) return { command: command, args: args, options: options };
63
54
 
64
55
  var hasSetpriv = typeof setprivCheck === "function" ? setprivCheck() : setprivAvailable();
65
- if (_inheritGroups && hasSetpriv) {
56
+ if (hasSetpriv) {
66
57
  var wrappedArgs = ["--reuid", String(uid), "--regid", String(gid), "--init-groups", "--", command].concat(args);
67
58
  var newOpts = Object.assign({}, options);
68
59
  delete newOpts.uid;
@@ -70,9 +61,9 @@ function wrapSpawnAsUser(command, args, options, setprivCheck) {
70
61
  return { command: "setpriv", args: wrappedArgs, options: newOpts };
71
62
  }
72
63
 
73
- if (_inheritGroups && !_warnedNoSetpriv) {
64
+ if (!_warnedNoSetpriv) {
74
65
  _warnedNoSetpriv = true;
75
- console.warn("[os-users] inheritGroups enabled but `setpriv` not found; supplementary groups will be dropped. Install util-linux to restore them.");
66
+ console.warn("[os-users] `setpriv` not found; supplementary groups will be dropped. Install util-linux to restore them.");
76
67
  }
77
68
  return { command: command, args: args, options: options };
78
69
  }
@@ -673,8 +664,6 @@ module.exports = {
673
664
  grantAllUsersAccess: grantAllUsersAccess,
674
665
  installClaudeCli: installClaudeCli,
675
666
  claudeAvailableForUser: claudeAvailableForUser,
676
- setInheritGroups: setInheritGroups,
677
- getInheritGroups: getInheritGroups,
678
667
  wrapSpawnAsUser: wrapSpawnAsUser,
679
668
  deactivateLinuxUser: deactivateLinuxUser,
680
669
  ensureProjectsDir: ensureProjectsDir,