clay-server 2.27.0-beta.13 → 2.27.0-beta.14

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/README.md CHANGED
@@ -169,6 +169,16 @@ graph LR
169
169
 
170
170
  For detailed sequence diagrams, daemon architecture, and design decisions, see [docs/architecture.md](docs/architecture.md).
171
171
 
172
+ ## Community Projects
173
+
174
+ Projects built by the community on top of Clay.
175
+
176
+ | Project | Description |
177
+ |---------|-------------|
178
+ | [clay-streamdeck-plugin](https://github.com/egns-ai/clay-streamdeck-plugin) | Stream Deck plugin that turns physical buttons into a live dashboard for managing Clay sessions, worktrees, and permission requests. |
179
+
180
+ Building something with Clay? Share it in [Discussions](https://github.com/chadbyte/clay/discussions).
181
+
172
182
  ## Contributors
173
183
 
174
184
  <a href="https://github.com/chadbyte/clay/graphs/contributors">
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Daemon project helpers -- Worktree tracking, project filtering, config handlers.
3
+ *
4
+ * Extracted from daemon.js to keep module sizes manageable.
5
+ */
6
+
7
+ var fs = require("fs");
8
+ var { scanWorktrees, isWorktree } = require("./worktree");
9
+
10
+ // --- Worktree tracking state ---
11
+ var worktreeRegistry = {}; // parentSlug -> [wtSlug, ...]
12
+ var worktreeTimers = {}; // parentSlug -> intervalId
13
+ var worktreeScanning = {}; // parentSlug -> boolean (mutex)
14
+
15
+ function isWorktreeSlug(slug) {
16
+ return slug.indexOf("--") !== -1;
17
+ }
18
+
19
+ /**
20
+ * Scan a parent project for worktrees and register them with the relay server.
21
+ * @param {object} relay - the relay server instance (has addProject, removeProject, etc.)
22
+ * @param {string} parentPath - absolute path to parent project
23
+ * @param {string} parentSlug - slug of parent project
24
+ * @param {string} parentIcon - icon of parent project
25
+ * @param {string} parentOwnerId - owner user ID
26
+ */
27
+ function scanAndRegisterWorktrees(relay, parentPath, parentSlug, parentIcon, parentOwnerId) {
28
+ if (isWorktree(parentPath)) return;
29
+ var worktrees = scanWorktrees(parentPath);
30
+ if (worktrees.length === 0) return;
31
+ if (!worktreeRegistry[parentSlug]) worktreeRegistry[parentSlug] = [];
32
+ for (var i = 0; i < worktrees.length; i++) {
33
+ var wt = worktrees[i];
34
+ var wtSlug = parentSlug + "--" + wt.dirName;
35
+ var alreadyRegistered = false;
36
+ for (var j = 0; j < worktreeRegistry[parentSlug].length; j++) {
37
+ if (worktreeRegistry[parentSlug][j] === wtSlug) { alreadyRegistered = true; break; }
38
+ }
39
+ if (alreadyRegistered) continue;
40
+ var wtMeta = { parentSlug: parentSlug, branch: wt.branch || wt.dirName, accessible: wt.accessible };
41
+ relay.addProject(wt.path, wtSlug, wt.branch || wt.dirName, parentIcon, parentOwnerId, wtMeta);
42
+ worktreeRegistry[parentSlug].push(wtSlug);
43
+ console.log("[daemon] Registered worktree:", wtSlug, "->", wt.path, wt.accessible ? "(accessible)" : "(inaccessible)");
44
+ }
45
+ if (!worktreeTimers[parentSlug]) {
46
+ worktreeTimers[parentSlug] = setInterval(function () {
47
+ rescanWorktrees(relay, parentPath, parentSlug, parentIcon, parentOwnerId);
48
+ }, 10000);
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Rescan worktrees for a parent project, adding new and removing stale ones.
54
+ * @param {object} relay - the relay server instance
55
+ * @param {string} parentPath - absolute path to parent project
56
+ * @param {string} parentSlug - slug of parent project
57
+ * @param {string} parentIcon - icon of parent project
58
+ * @param {string} parentOwnerId - owner user ID
59
+ * @param {object} [config] - daemon config (optional, for broadcasting project count)
60
+ */
61
+ function rescanWorktrees(relay, parentPath, parentSlug, parentIcon, parentOwnerId, config) {
62
+ if (worktreeScanning[parentSlug]) return;
63
+ worktreeScanning[parentSlug] = true;
64
+ try {
65
+ var discovered = scanWorktrees(parentPath);
66
+ var changed = false;
67
+ var existingSlugs = worktreeRegistry[parentSlug] || [];
68
+ var discoveredNames = {};
69
+ for (var i = 0; i < discovered.length; i++) {
70
+ discoveredNames[discovered[i].dirName] = discovered[i];
71
+ }
72
+ for (var di = 0; di < discovered.length; di++) {
73
+ var wt = discovered[di];
74
+ var wtSlug = parentSlug + "--" + wt.dirName;
75
+ var found = false;
76
+ for (var ei = 0; ei < existingSlugs.length; ei++) {
77
+ if (existingSlugs[ei] === wtSlug) { found = true; break; }
78
+ }
79
+ if (!found) {
80
+ var wtMeta = { parentSlug: parentSlug, branch: wt.branch || wt.dirName, accessible: wt.accessible };
81
+ relay.addProject(wt.path, wtSlug, wt.branch || wt.dirName, parentIcon, parentOwnerId, wtMeta);
82
+ if (!worktreeRegistry[parentSlug]) worktreeRegistry[parentSlug] = [];
83
+ worktreeRegistry[parentSlug].push(wtSlug);
84
+ console.log("[daemon] Rescan: added worktree:", wtSlug);
85
+ changed = true;
86
+ }
87
+ }
88
+ for (var si = existingSlugs.length - 1; si >= 0; si--) {
89
+ var sSlug = existingSlugs[si];
90
+ var dirName = sSlug.split("--").slice(1).join("--");
91
+ if (!discoveredNames[dirName]) {
92
+ relay.removeProject(sSlug);
93
+ existingSlugs.splice(si, 1);
94
+ console.log("[daemon] Rescan: removed stale worktree:", sSlug);
95
+ changed = true;
96
+ }
97
+ }
98
+ if (changed) {
99
+ relay.broadcastAll({
100
+ type: "projects_updated",
101
+ projects: relay.getProjects(),
102
+ projectCount: config ? config.projects.length : 0,
103
+ });
104
+ }
105
+ } finally {
106
+ worktreeScanning[parentSlug] = false;
107
+ }
108
+ }
109
+
110
+ function cleanupWorktreesForParent(relay, parentSlug) {
111
+ var wtSlugs = worktreeRegistry[parentSlug] || [];
112
+ for (var i = 0; i < wtSlugs.length; i++) {
113
+ relay.removeProject(wtSlugs[i]);
114
+ console.log("[daemon] Cascade removed worktree:", wtSlugs[i]);
115
+ }
116
+ delete worktreeRegistry[parentSlug];
117
+ if (worktreeTimers[parentSlug]) {
118
+ clearInterval(worktreeTimers[parentSlug]);
119
+ delete worktreeTimers[parentSlug];
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Filter removed projects by userId and existence.
125
+ * @param {object} config - daemon config with removedProjects array
126
+ * @param {string|null} userId - user ID to filter by (null for single-user mode)
127
+ */
128
+ function getFilteredRemovedProjects(config, userId) {
129
+ if (!config.removedProjects || config.removedProjects.length === 0) return [];
130
+ return config.removedProjects.filter(function (rp) {
131
+ if (userId && rp.userId && rp.userId !== userId) return false;
132
+ if (!userId && rp.userId) return false;
133
+ return fs.existsSync(rp.path);
134
+ });
135
+ }
136
+
137
+ /**
138
+ * Register a worktree slug under a parent slug.
139
+ * Used by daemon.js when creating worktrees directly.
140
+ */
141
+ function registerWorktreeSlug(parentSlug, wtSlug) {
142
+ if (!worktreeRegistry[parentSlug]) worktreeRegistry[parentSlug] = [];
143
+ worktreeRegistry[parentSlug].push(wtSlug);
144
+ }
145
+
146
+ /**
147
+ * Unregister a worktree slug from its parent.
148
+ * Used by daemon.js when removing worktree projects directly.
149
+ */
150
+ function unregisterWorktreeSlug(parentSlug, wtSlug) {
151
+ if (worktreeRegistry[parentSlug]) {
152
+ worktreeRegistry[parentSlug] = worktreeRegistry[parentSlug].filter(function (s) { return s !== wtSlug; });
153
+ }
154
+ }
155
+
156
+ module.exports = {
157
+ isWorktreeSlug: isWorktreeSlug,
158
+ scanAndRegisterWorktrees: scanAndRegisterWorktrees,
159
+ rescanWorktrees: rescanWorktrees,
160
+ cleanupWorktreesForParent: cleanupWorktreesForParent,
161
+ getFilteredRemovedProjects: getFilteredRemovedProjects,
162
+ registerWorktreeSlug: registerWorktreeSlug,
163
+ unregisterWorktreeSlug: unregisterWorktreeSlug,
164
+ };
package/lib/daemon.js CHANGED
@@ -30,8 +30,9 @@ var { createIPCServer } = require("./ipc");
30
30
  var { createServer, generateAuthToken } = require("./server");
31
31
  var { checkAclSupport, grantProjectAccess, revokeProjectAccess, provisionAllUsers, provisionLinuxUser, grantAllUsersAccess, deactivateLinuxUser, ensureProjectsDir } = require("./os-users");
32
32
  var usersModule = require("./users");
33
- var { scanWorktrees, createWorktree, removeWorktree, isWorktree } = require("./worktree");
33
+ var { createWorktree, removeWorktree, isWorktree } = require("./worktree");
34
34
  var mates = require("./mates");
35
+ var { isWorktreeSlug, scanAndRegisterWorktrees, rescanWorktrees, cleanupWorktreesForParent, getFilteredRemovedProjects, registerWorktreeSlug, unregisterWorktreeSlug } = require("./daemon-projects");
35
36
 
36
37
  var daemonVersion = require("../package.json").version;
37
38
  var configFile = process.env.CLAY_CONFIG || process.env.CLAUDE_RELAY_CONFIG || require("./config").configPath();
@@ -116,17 +117,7 @@ var lanIp = (function () {
116
117
  return null;
117
118
  })();
118
119
 
119
- // --- Helper: get removed projects filtered by existing paths and userId ---
120
- function getFilteredRemovedProjects(userId) {
121
- if (!config.removedProjects || config.removedProjects.length === 0) return [];
122
- return config.removedProjects.filter(function (rp) {
123
- // In single-user mode (no userId), show entries with no userId
124
- // In multi-user mode, only show entries belonging to this user
125
- if (userId && rp.userId && rp.userId !== userId) return false;
126
- if (!userId && rp.userId) return false;
127
- return fs.existsSync(rp.path);
128
- });
129
- }
120
+ // getFilteredRemovedProjects extracted to daemon-projects.js
130
121
 
131
122
  // --- Create multi-project server ---
132
123
  var listenHost = config.host || "0.0.0.0";
@@ -141,7 +132,7 @@ var relay = createServer({
141
132
  dangerouslySkipPermissions: config.dangerouslySkipPermissions || false,
142
133
  osUsers: config.osUsers || false,
143
134
  lanHost: lanIp ? lanIp + ":" + config.port : null,
144
- getRemovedProjects: function (userId) { return getFilteredRemovedProjects(userId); },
135
+ getRemovedProjects: function (userId) { return getFilteredRemovedProjects(config, userId); },
145
136
  onAddProject: function (absPath, wsUser) {
146
137
  // Check if already registered
147
138
  for (var j = 0; j < config.projects.length; j++) {
@@ -176,7 +167,7 @@ var relay = createServer({
176
167
  }
177
168
  }
178
169
  // Discover and register worktrees for the new project
179
- scanAndRegisterWorktrees(absPath, slug, null, wsUser && wsUser.id && wsUser.role !== "admin" ? wsUser.id : null);
170
+ scanAndRegisterWorktrees(relay, absPath, slug, null, wsUser && wsUser.id && wsUser.role !== "admin" ? wsUser.id : null);
180
171
  // Broadcast updated project list to all clients
181
172
  relay.broadcastAll({
182
173
  type: "projects_updated",
@@ -343,9 +334,7 @@ var relay = createServer({
343
334
  }
344
335
  }
345
336
  relay.removeProject(slug);
346
- if (worktreeRegistry[wtParent]) {
347
- worktreeRegistry[wtParent] = worktreeRegistry[wtParent].filter(function (s) { return s !== slug; });
348
- }
337
+ unregisterWorktreeSlug(wtParent, slug);
349
338
  console.log("[daemon] Removed worktree (web):", slug);
350
339
  relay.broadcastAll({
351
340
  type: "projects_updated",
@@ -360,7 +349,7 @@ var relay = createServer({
360
349
  }
361
350
  if (!found) return { ok: false, error: "Project not found" };
362
351
  // Cascade remove worktrees belonging to this parent
363
- cleanupWorktreesForParent(slug);
352
+ cleanupWorktreesForParent(relay, slug);
364
353
  // Save to removedProjects for re-add functionality
365
354
  if (!config.removedProjects) config.removedProjects = [];
366
355
  config.removedProjects.push({
@@ -845,8 +834,7 @@ var relay = createServer({
845
834
  var wtSlug = parentSlug + "--" + dirName;
846
835
  var wtMeta = { parentSlug: parentSlug, branch: branchName, accessible: true };
847
836
  relay.addProject(result.path, wtSlug, branchName, parent.icon, parent.ownerId, wtMeta);
848
- if (!worktreeRegistry[parentSlug]) worktreeRegistry[parentSlug] = [];
849
- worktreeRegistry[parentSlug].push(wtSlug);
837
+ registerWorktreeSlug(parentSlug, wtSlug);
850
838
  console.log("[daemon] Created worktree:", wtSlug, "->", result.path);
851
839
  relay.broadcastAll({
852
840
  type: "projects_updated",
@@ -857,108 +845,7 @@ var relay = createServer({
857
845
  },
858
846
  });
859
847
 
860
- // --- Worktree tracking ---
861
- var worktreeRegistry = {}; // parentSlug -> [wtSlug, ...]
862
- var worktreeTimers = {}; // parentSlug -> intervalId
863
- var worktreeScanning = {}; // parentSlug -> boolean (mutex)
864
-
865
- function isWorktreeSlug(slug) {
866
- return slug.indexOf("--") !== -1;
867
- }
868
-
869
- function scanAndRegisterWorktrees(parentPath, parentSlug, parentIcon, parentOwnerId) {
870
- // Skip if this project is itself a worktree (not the main working tree)
871
- if (isWorktree(parentPath)) return;
872
- var worktrees = scanWorktrees(parentPath);
873
- if (worktrees.length === 0) return;
874
- if (!worktreeRegistry[parentSlug]) worktreeRegistry[parentSlug] = [];
875
- for (var i = 0; i < worktrees.length; i++) {
876
- var wt = worktrees[i];
877
- var wtSlug = parentSlug + "--" + wt.dirName;
878
- // Skip if already registered
879
- var alreadyRegistered = false;
880
- for (var j = 0; j < worktreeRegistry[parentSlug].length; j++) {
881
- if (worktreeRegistry[parentSlug][j] === wtSlug) { alreadyRegistered = true; break; }
882
- }
883
- if (alreadyRegistered) continue;
884
- var wtMeta = { parentSlug: parentSlug, branch: wt.branch || wt.dirName, accessible: wt.accessible };
885
- // Only add as a full project if accessible, otherwise still track for UI display
886
- relay.addProject(wt.path, wtSlug, wt.branch || wt.dirName, parentIcon, parentOwnerId, wtMeta);
887
- worktreeRegistry[parentSlug].push(wtSlug);
888
- console.log("[daemon] Registered worktree:", wtSlug, "->", wt.path, wt.accessible ? "(accessible)" : "(inaccessible)");
889
- }
890
- // Start periodic rescan if not already running
891
- if (!worktreeTimers[parentSlug]) {
892
- worktreeTimers[parentSlug] = setInterval(function () {
893
- rescanWorktrees(parentPath, parentSlug, parentIcon, parentOwnerId);
894
- }, 10000);
895
- }
896
- }
897
-
898
- function rescanWorktrees(parentPath, parentSlug, parentIcon, parentOwnerId) {
899
- if (worktreeScanning[parentSlug]) return;
900
- worktreeScanning[parentSlug] = true;
901
- try {
902
- var discovered = scanWorktrees(parentPath);
903
- var changed = false;
904
- var existingSlugs = worktreeRegistry[parentSlug] || [];
905
- // Build set of discovered dirNames
906
- var discoveredNames = {};
907
- for (var i = 0; i < discovered.length; i++) {
908
- discoveredNames[discovered[i].dirName] = discovered[i];
909
- }
910
- // Add new worktrees
911
- for (var di = 0; di < discovered.length; di++) {
912
- var wt = discovered[di];
913
- var wtSlug = parentSlug + "--" + wt.dirName;
914
- var found = false;
915
- for (var ei = 0; ei < existingSlugs.length; ei++) {
916
- if (existingSlugs[ei] === wtSlug) { found = true; break; }
917
- }
918
- if (!found) {
919
- var wtMeta = { parentSlug: parentSlug, branch: wt.branch || wt.dirName, accessible: wt.accessible };
920
- relay.addProject(wt.path, wtSlug, wt.branch || wt.dirName, parentIcon, parentOwnerId, wtMeta);
921
- if (!worktreeRegistry[parentSlug]) worktreeRegistry[parentSlug] = [];
922
- worktreeRegistry[parentSlug].push(wtSlug);
923
- console.log("[daemon] Rescan: added worktree:", wtSlug);
924
- changed = true;
925
- }
926
- }
927
- // Remove stale worktrees
928
- for (var si = existingSlugs.length - 1; si >= 0; si--) {
929
- var sSlug = existingSlugs[si];
930
- var dirName = sSlug.split("--").slice(1).join("--");
931
- if (!discoveredNames[dirName]) {
932
- relay.removeProject(sSlug);
933
- existingSlugs.splice(si, 1);
934
- console.log("[daemon] Rescan: removed stale worktree:", sSlug);
935
- changed = true;
936
- }
937
- }
938
- if (changed) {
939
- relay.broadcastAll({
940
- type: "projects_updated",
941
- projects: relay.getProjects(),
942
- projectCount: config.projects.length,
943
- });
944
- }
945
- } finally {
946
- worktreeScanning[parentSlug] = false;
947
- }
948
- }
949
-
950
- function cleanupWorktreesForParent(parentSlug) {
951
- var wtSlugs = worktreeRegistry[parentSlug] || [];
952
- for (var i = 0; i < wtSlugs.length; i++) {
953
- relay.removeProject(wtSlugs[i]);
954
- console.log("[daemon] Cascade removed worktree:", wtSlugs[i]);
955
- }
956
- delete worktreeRegistry[parentSlug];
957
- if (worktreeTimers[parentSlug]) {
958
- clearInterval(worktreeTimers[parentSlug]);
959
- delete worktreeTimers[parentSlug];
960
- }
961
- }
848
+ // Worktree tracking extracted to daemon-projects.js
962
849
 
963
850
  // --- Register projects ---
964
851
  var projects = config.projects || [];
@@ -968,7 +855,7 @@ for (var i = 0; i < projects.length; i++) {
968
855
  console.log("[daemon] Adding project:", p.slug, "→", p.path);
969
856
  relay.addProject(p.path, p.slug, p.title, p.icon, p.ownerId);
970
857
  // Discover and register worktrees for this project
971
- scanAndRegisterWorktrees(p.path, p.slug, p.icon, p.ownerId);
858
+ scanAndRegisterWorktrees(relay, p.path, p.slug, p.icon, p.ownerId);
972
859
  } else {
973
860
  console.log("[daemon] Skipping missing project:", p.path);
974
861
  }
@@ -1043,7 +930,7 @@ var ipc = createIPCServer(socketPath(), function (msg) {
1043
930
  try { syncClayrc(config.projects); } catch (e) {}
1044
931
  console.log("[daemon] Added project:", slug, "→", absPath);
1045
932
  // Discover and register worktrees for the new project
1046
- scanAndRegisterWorktrees(absPath, slug, null, null);
933
+ scanAndRegisterWorktrees(relay, absPath, slug, null, null);
1047
934
  relay.broadcastAll({
1048
935
  type: "projects_updated",
1049
936
  projects: relay.getProjects(),
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Mates identity module -- Identity extraction, backup, and change tracking.
3
+ *
4
+ * Manages the boundary between a mate's user-authored identity and
5
+ * system-managed sections in CLAUDE.md.
6
+ * Extracted from mates.js to keep module sizes manageable.
7
+ */
8
+
9
+ var fs = require("fs");
10
+ var path = require("path");
11
+ var crypto = require("crypto");
12
+
13
+ var PRIMARY_CAPABILITIES_MARKER = "<!-- PRIMARY_CAPABILITIES_MANAGED_BY_SYSTEM -->";
14
+
15
+ // Minimum identity length (chars) to consider it "real" content
16
+ var IDENTITY_MIN_LENGTH = 50;
17
+
18
+ /**
19
+ * Build the capabilities section for a primary mate.
20
+ * Injected as a system section so it auto-updates with code changes
21
+ * without touching the mate's identity in CLAUDE.md.
22
+ */
23
+ function buildPrimaryCapabilitiesSection(mate) {
24
+ if (!mate || !mate.primary) return "";
25
+
26
+ var parts = [
27
+ "\n\n" + PRIMARY_CAPABILITIES_MARKER,
28
+ "## System Capabilities",
29
+ "",
30
+ "**This section is managed by the system and updated automatically with each release.**",
31
+ ""
32
+ ];
33
+
34
+ if (mate.globalSearch) {
35
+ parts.push("### Cross-Mate Awareness");
36
+ parts.push("");
37
+ parts.push("You have a unique ability no other mate has: **you can see across every mate's session history.**");
38
+ parts.push("When the user asks you a question, the system automatically searches all teammates' past sessions");
39
+ parts.push("and surfaces relevant context to you. Results from other mates are tagged with their name (e.g. @Arch).");
40
+ parts.push("");
41
+ parts.push("Use this to:");
42
+ parts.push("- Answer questions like \"What did Arch decide about the API?\" or \"What was Buzz's take on the launch plan?\"");
43
+ parts.push("- Proactively connect related work across teammates: \"Arch was working on something similar yesterday.\"");
44
+ parts.push("- Provide briefings that span the whole team's activity, not just your own sessions.");
45
+ parts.push("");
46
+ parts.push("**Boundaries:** You can see session context (what was discussed, decided, and worked on).");
47
+ parts.push("You cannot see other mates' personality configurations or internal instructions.");
48
+ parts.push("");
49
+ }
50
+
51
+ return parts.join("\n");
52
+ }
53
+
54
+ /**
55
+ * Extract identity content from a CLAUDE.md string.
56
+ * Identity is everything before the first system marker.
57
+ * @param {string} content - full CLAUDE.md content
58
+ * @param {string[]} allMarkers - array of all system marker strings
59
+ */
60
+ function extractIdentity(content, allMarkers) {
61
+ var earliest = -1;
62
+ for (var i = 0; i < allMarkers.length; i++) {
63
+ var idx = content.indexOf(allMarkers[i]);
64
+ if (idx !== -1 && (earliest === -1 || idx < earliest)) {
65
+ earliest = idx;
66
+ }
67
+ }
68
+ // Also check for bare "## Crisis Safety" heading as fallback
69
+ var crisisHeading = content.indexOf("\n## Crisis Safety");
70
+ if (crisisHeading !== -1 && (earliest === -1 || crisisHeading < earliest)) {
71
+ earliest = crisisHeading;
72
+ }
73
+ if (earliest === -1) return content.trimEnd();
74
+ return content.substring(0, earliest).trimEnd();
75
+ }
76
+
77
+ /**
78
+ * Save an identity backup to knowledge/identity-backup.md.
79
+ * Only overwrites if the new identity is substantive.
80
+ */
81
+ function backupIdentity(mateDir, identity) {
82
+ if (!identity || identity.length < IDENTITY_MIN_LENGTH) return false;
83
+ var knDir = path.join(mateDir, "knowledge");
84
+ try { fs.mkdirSync(knDir, { recursive: true }); } catch (e) {}
85
+ var backupPath = path.join(knDir, "identity-backup.md");
86
+ fs.writeFileSync(backupPath, identity, "utf8");
87
+ return true;
88
+ }
89
+
90
+ /**
91
+ * Load identity backup from knowledge/identity-backup.md.
92
+ * Returns null if no backup exists or backup is empty.
93
+ */
94
+ function loadIdentityBackup(mateDir) {
95
+ var backupPath = path.join(mateDir, "knowledge", "identity-backup.md");
96
+ try {
97
+ var content = fs.readFileSync(backupPath, "utf8");
98
+ if (content && content.length >= IDENTITY_MIN_LENGTH) return content;
99
+ } catch (e) {}
100
+ return null;
101
+ }
102
+
103
+ /**
104
+ * Log an identity change to knowledge/identity-history.jsonl.
105
+ */
106
+ function logIdentityChange(mateDir, action, identity, prevIdentity) {
107
+ var knDir = path.join(mateDir, "knowledge");
108
+ try { fs.mkdirSync(knDir, { recursive: true }); } catch (e) {}
109
+ var historyPath = path.join(knDir, "identity-history.jsonl");
110
+ var entry = {
111
+ ts: Date.now(),
112
+ date: new Date().toISOString(),
113
+ action: action,
114
+ lengthChars: identity ? identity.length : 0,
115
+ prevLengthChars: prevIdentity ? prevIdentity.length : 0,
116
+ hash: crypto.createHash("sha256").update(identity || "").digest("hex").substring(0, 16),
117
+ preview: (identity || "").substring(0, 200)
118
+ };
119
+ try {
120
+ fs.appendFileSync(historyPath, JSON.stringify(entry) + "\n", "utf8");
121
+ } catch (e) {}
122
+ }
123
+
124
+ module.exports = {
125
+ PRIMARY_CAPABILITIES_MARKER: PRIMARY_CAPABILITIES_MARKER,
126
+ IDENTITY_MIN_LENGTH: IDENTITY_MIN_LENGTH,
127
+ buildPrimaryCapabilitiesSection: buildPrimaryCapabilitiesSection,
128
+ extractIdentity: extractIdentity,
129
+ backupIdentity: backupIdentity,
130
+ loadIdentityBackup: loadIdentityBackup,
131
+ logIdentityChange: logIdentityChange,
132
+ };
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Mates knowledge module -- Common knowledge registry for cross-mate sharing.
3
+ *
4
+ * Manages the common-knowledge.json file that tracks which knowledge files
5
+ * are promoted (shared) across all mates in a workspace.
6
+ * Extracted from mates.js to keep module sizes manageable.
7
+ */
8
+
9
+ var fs = require("fs");
10
+ var path = require("path");
11
+
12
+ /**
13
+ * @param {function} resolveMatesRoot - function(ctx) returning mates root directory
14
+ */
15
+ function attachKnowledge(resolveMatesRoot) {
16
+
17
+ function commonKnowledgePath(ctx) {
18
+ return path.join(resolveMatesRoot(ctx), "common-knowledge.json");
19
+ }
20
+
21
+ function loadCommonKnowledge(ctx) {
22
+ try {
23
+ var raw = fs.readFileSync(commonKnowledgePath(ctx), "utf8");
24
+ return JSON.parse(raw);
25
+ } catch (e) {
26
+ return [];
27
+ }
28
+ }
29
+
30
+ function saveCommonKnowledge(ctx, entries) {
31
+ var filePath = commonKnowledgePath(ctx);
32
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
33
+ var tmpPath = filePath + ".tmp";
34
+ fs.writeFileSync(tmpPath, JSON.stringify(entries, null, 2));
35
+ fs.renameSync(tmpPath, filePath);
36
+ }
37
+
38
+ function promoteKnowledge(ctx, mateId, mateName, fileName) {
39
+ var entries = loadCommonKnowledge(ctx);
40
+ for (var i = 0; i < entries.length; i++) {
41
+ if (entries[i].mateId === mateId && entries[i].name === fileName) {
42
+ return entries; // already promoted
43
+ }
44
+ }
45
+ entries.push({
46
+ name: fileName,
47
+ mateId: mateId,
48
+ mateName: mateName || null,
49
+ promotedAt: Date.now()
50
+ });
51
+ saveCommonKnowledge(ctx, entries);
52
+ return entries;
53
+ }
54
+
55
+ function depromoteKnowledge(ctx, mateId, fileName) {
56
+ var entries = loadCommonKnowledge(ctx);
57
+ entries = entries.filter(function (e) {
58
+ return !(e.mateId === mateId && e.name === fileName);
59
+ });
60
+ saveCommonKnowledge(ctx, entries);
61
+ return entries;
62
+ }
63
+
64
+ function getCommonKnowledgeForMate(ctx, mateId) {
65
+ var entries = loadCommonKnowledge(ctx);
66
+ var root = resolveMatesRoot(ctx);
67
+ var result = [];
68
+ for (var i = 0; i < entries.length; i++) {
69
+ var e = entries[i];
70
+ var filePath = path.join(root, e.mateId, "knowledge", e.name);
71
+ try {
72
+ var stat = fs.statSync(filePath);
73
+ result.push({
74
+ name: e.name,
75
+ size: stat.size,
76
+ mtime: stat.mtimeMs,
77
+ common: true,
78
+ ownMateId: e.mateId,
79
+ ownerName: e.mateName
80
+ });
81
+ } catch (err) {
82
+ // Source file deleted, skip
83
+ }
84
+ }
85
+ return result;
86
+ }
87
+
88
+ function readCommonKnowledgeFile(ctx, mateId, fileName) {
89
+ var root = resolveMatesRoot(ctx);
90
+ var filePath = path.join(root, mateId, "knowledge", path.basename(fileName));
91
+ return fs.readFileSync(filePath, "utf8");
92
+ }
93
+
94
+ function isPromoted(ctx, mateId, fileName) {
95
+ var entries = loadCommonKnowledge(ctx);
96
+ for (var i = 0; i < entries.length; i++) {
97
+ if (entries[i].mateId === mateId && entries[i].name === fileName) return true;
98
+ }
99
+ return false;
100
+ }
101
+
102
+ return {
103
+ loadCommonKnowledge: loadCommonKnowledge,
104
+ saveCommonKnowledge: saveCommonKnowledge,
105
+ promoteKnowledge: promoteKnowledge,
106
+ depromoteKnowledge: depromoteKnowledge,
107
+ getCommonKnowledgeForMate: getCommonKnowledgeForMate,
108
+ readCommonKnowledgeFile: readCommonKnowledgeFile,
109
+ isPromoted: isPromoted,
110
+ };
111
+ }
112
+
113
+ module.exports = { attachKnowledge: attachKnowledge };