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 +10 -0
- package/lib/daemon-projects.js +164 -0
- package/lib/daemon.js +11 -124
- package/lib/mates-identity.js +132 -0
- package/lib/mates-knowledge.js +113 -0
- package/lib/mates-prompts.js +398 -0
- package/lib/mates.js +40 -599
- package/lib/public/app.js +2 -0
- package/lib/public/modules/app-connection.js +4 -4
- package/lib/public/modules/app-header.js +3 -3
- package/lib/public/modules/app-messages.js +4 -0
- package/lib/public/modules/app-projects.js +8 -0
- package/lib/public/modules/input.js +30 -20
- package/lib/public/modules/scheduler-config.js +1532 -0
- package/lib/public/modules/scheduler-history.js +79 -0
- package/lib/public/modules/scheduler.js +33 -1554
- package/lib/sdk-bridge.js +26 -707
- package/lib/sdk-message-processor.js +573 -0
- package/lib/sdk-message-queue.js +42 -0
- package/lib/sdk-skill-discovery.js +131 -0
- package/lib/users-auth.js +146 -0
- package/lib/users-permissions.js +118 -0
- package/lib/users-preferences.js +210 -0
- package/lib/users.js +48 -398
- package/lib/ws-schema.js +498 -0
- package/package.json +1 -1
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 {
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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 };
|