clay-server 4.0.0-beta.17 → 4.0.0-beta.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/daemon-projects.js +8 -8
- package/lib/daemon.js +42 -10
- package/lib/git-cli.js +1 -0
- package/lib/os-users.js +3 -1
- package/lib/project-log-context.js +24 -23
- package/lib/project-logs-mcp-server.js +10 -4
- package/lib/project-logs-service.js +29 -0
- package/lib/project-logs.js +5 -1
- package/lib/project-sessions.js +5 -1
- package/lib/project.js +1 -1
- package/lib/public/css/command-palette.css +19 -9
- package/lib/public/css/sidebar.css +11 -0
- package/lib/public/css/sticky-notes.css +7 -0
- package/lib/public/index.html +2 -2
- package/lib/public/modules/admin.js +8 -8
- package/lib/public/modules/app-home-hub.js +19 -7
- package/lib/public/modules/app-projects.js +5 -3
- package/lib/public/modules/chat-bubble-renderer.js +1 -1
- package/lib/public/modules/clay-log-links.js +70 -0
- package/lib/public/modules/command-palette.js +52 -15
- package/lib/public/modules/markdown.js +2 -0
- package/lib/public/modules/project-logs.js +7 -0
- package/lib/public/modules/sidebar-session-hierarchy.js +16 -8
- package/lib/public/modules/sticky-note-markdown.js +5 -0
- package/lib/public/modules/sticky-notes-browser.js +2 -0
- package/lib/public/modules/sticky-notes-card.js +2 -0
- package/lib/public/modules/sticky-notes-editor.js +3 -0
- package/lib/public/modules/sticky-notes.js +2 -0
- package/lib/public/style.css +1 -1
- package/lib/sdk-bridge.js +20 -10
- package/lib/server-global-ws.js +11 -0
- package/lib/server-home-chat-events.js +3 -0
- package/lib/server-home-chat.js +9 -2
- package/lib/server-home-clay-entry.js +1 -1
- package/lib/server-home-clay-session-links.js +34 -5
- package/lib/server.js +19 -10
- package/lib/session-hygiene.js +3 -0
- package/lib/users.js +16 -16
- package/lib/worktree.js +25 -38
- package/lib/ws-schema.js +2 -0
- package/package.json +1 -1
package/lib/daemon-projects.js
CHANGED
|
@@ -29,9 +29,9 @@ function isWorktreeSlug(slug) {
|
|
|
29
29
|
* @param {string} parentIcon - icon of parent project
|
|
30
30
|
* @param {string} parentOwnerId - owner user ID
|
|
31
31
|
*/
|
|
32
|
-
function scanAndRegisterWorktrees(relay, parentPath, parentSlug, parentIcon, parentOwnerId, projectKnowledgeId) {
|
|
33
|
-
if (isWorktree(parentPath)) return;
|
|
34
|
-
var worktrees = scanWorktrees(parentPath);
|
|
32
|
+
function scanAndRegisterWorktrees(relay, parentPath, parentSlug, parentIcon, parentOwnerId, projectKnowledgeId, osUserInfo) {
|
|
33
|
+
if (isWorktree(parentPath, osUserInfo)) return;
|
|
34
|
+
var worktrees = scanWorktrees(parentPath, osUserInfo);
|
|
35
35
|
if (worktrees.length === 0) return;
|
|
36
36
|
if (!worktreeRegistry[parentSlug]) worktreeRegistry[parentSlug] = [];
|
|
37
37
|
for (var i = 0; i < worktrees.length; i++) {
|
|
@@ -42,14 +42,14 @@ function scanAndRegisterWorktrees(relay, parentPath, parentSlug, parentIcon, par
|
|
|
42
42
|
if (worktreeRegistry[parentSlug][j] === wtSlug) { alreadyRegistered = true; break; }
|
|
43
43
|
}
|
|
44
44
|
if (alreadyRegistered) continue;
|
|
45
|
-
var context = logContext.resolveWorktreeContext(wt.path, projectKnowledgeId, { branch: wt.branch || wt.dirName });
|
|
45
|
+
var context = logContext.resolveWorktreeContext(wt.path, projectKnowledgeId, { branch: wt.branch || wt.dirName }, osUserInfo);
|
|
46
46
|
var wtMeta = Object.assign({ parentSlug: parentSlug, external: wt.external, projectKnowledgeId: projectKnowledgeId }, context);
|
|
47
47
|
relay.addProject(wt.path, wtSlug, wt.branch || wt.dirName, parentIcon, parentOwnerId, wtMeta);
|
|
48
48
|
worktreeRegistry[parentSlug].push(wtSlug);
|
|
49
49
|
console.log("[daemon] Registered worktree:", wtSlug, "->", wt.path, wt.external ? "(external)" : "(inside project folder)");
|
|
50
50
|
}
|
|
51
51
|
daemonSync.register(getWorktreeSyncKey(parentSlug), function () {
|
|
52
|
-
rescanWorktrees(relay, parentPath, parentSlug, parentIcon, parentOwnerId, null, projectKnowledgeId);
|
|
52
|
+
rescanWorktrees(relay, parentPath, parentSlug, parentIcon, parentOwnerId, null, projectKnowledgeId, osUserInfo);
|
|
53
53
|
});
|
|
54
54
|
}
|
|
55
55
|
|
|
@@ -62,11 +62,11 @@ function scanAndRegisterWorktrees(relay, parentPath, parentSlug, parentIcon, par
|
|
|
62
62
|
* @param {string} parentOwnerId - owner user ID
|
|
63
63
|
* @param {object} [config] - daemon config (optional, for broadcasting project count)
|
|
64
64
|
*/
|
|
65
|
-
function rescanWorktrees(relay, parentPath, parentSlug, parentIcon, parentOwnerId, config, projectKnowledgeId) {
|
|
65
|
+
function rescanWorktrees(relay, parentPath, parentSlug, parentIcon, parentOwnerId, config, projectKnowledgeId, osUserInfo) {
|
|
66
66
|
if (worktreeScanning[parentSlug]) return;
|
|
67
67
|
worktreeScanning[parentSlug] = true;
|
|
68
68
|
try {
|
|
69
|
-
var discovered = scanWorktrees(parentPath);
|
|
69
|
+
var discovered = scanWorktrees(parentPath, osUserInfo);
|
|
70
70
|
var changed = false;
|
|
71
71
|
var existingSlugs = worktreeRegistry[parentSlug] || [];
|
|
72
72
|
var discoveredNames = {};
|
|
@@ -81,7 +81,7 @@ function rescanWorktrees(relay, parentPath, parentSlug, parentIcon, parentOwnerI
|
|
|
81
81
|
if (existingSlugs[ei] === wtSlug) { found = true; break; }
|
|
82
82
|
}
|
|
83
83
|
if (!found) {
|
|
84
|
-
var context = logContext.resolveWorktreeContext(wt.path, projectKnowledgeId, { branch: wt.branch || wt.dirName });
|
|
84
|
+
var context = logContext.resolveWorktreeContext(wt.path, projectKnowledgeId, { branch: wt.branch || wt.dirName }, osUserInfo);
|
|
85
85
|
var wtMeta = Object.assign({ parentSlug: parentSlug, external: wt.external, projectKnowledgeId: projectKnowledgeId }, context);
|
|
86
86
|
relay.addProject(wt.path, wtSlug, wt.branch || wt.dirName, parentIcon, parentOwnerId, wtMeta);
|
|
87
87
|
if (!worktreeRegistry[parentSlug]) worktreeRegistry[parentSlug] = [];
|
package/lib/daemon.js
CHANGED
|
@@ -173,6 +173,25 @@ function projectRuntimeOptions(project) {
|
|
|
173
173
|
return { projectKnowledgeId: project.projectKnowledgeId };
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
+
function worktreeOsUserInfo(project, requestUser) {
|
|
177
|
+
if (!config.osUsers) return null;
|
|
178
|
+
var owner = project && project.ownerId ? usersModule.findUserById(project.ownerId) : null;
|
|
179
|
+
var executionUser = owner && owner.linuxUser ? owner : requestUser;
|
|
180
|
+
if (!executionUser || !executionUser.linuxUser) {
|
|
181
|
+
throw new Error("The project owner does not have a provisioned Linux account");
|
|
182
|
+
}
|
|
183
|
+
return osUsersMod.resolveOsUserInfo(executionUser.linuxUser);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function registerProjectWorktrees(project, requestUser) {
|
|
187
|
+
try {
|
|
188
|
+
var osUserInfo = worktreeOsUserInfo(project, requestUser);
|
|
189
|
+
scanAndRegisterWorktrees(relay, project.path, project.slug, project.icon, project.ownerId, project.projectKnowledgeId, osUserInfo);
|
|
190
|
+
} catch (e) {
|
|
191
|
+
console.error("[daemon] Failed to scan worktrees for " + project.slug + ": " + e.message);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
176
195
|
var relay = createServer({
|
|
177
196
|
tlsOptions: tlsOptions,
|
|
178
197
|
caPath: caRoot,
|
|
@@ -219,7 +238,7 @@ var relay = createServer({
|
|
|
219
238
|
}
|
|
220
239
|
}
|
|
221
240
|
// Discover and register worktrees for the new project
|
|
222
|
-
|
|
241
|
+
registerProjectWorktrees(projectEntry, wsUser);
|
|
223
242
|
// Broadcast updated project list to all clients
|
|
224
243
|
relay.broadcastAll({
|
|
225
244
|
type: "projects_updated",
|
|
@@ -404,8 +423,15 @@ var relay = createServer({
|
|
|
404
423
|
}
|
|
405
424
|
if (parentProject) {
|
|
406
425
|
var wtPath = path.join(parentProject.path, wtDirName);
|
|
407
|
-
var
|
|
408
|
-
var
|
|
426
|
+
var removeUser = userId ? usersModule.findUserById(userId) : null;
|
|
427
|
+
var removeOsUserInfo;
|
|
428
|
+
try {
|
|
429
|
+
removeOsUserInfo = worktreeOsUserInfo(parentProject, removeUser);
|
|
430
|
+
} catch (e) {
|
|
431
|
+
return { ok: false, error: e.message };
|
|
432
|
+
}
|
|
433
|
+
var contextStatus = projectLogContext.isMerged(parentProject.path, wtPath, removeOsUserInfo) ? "merged" : "archived";
|
|
434
|
+
var rmResult = removeWorktree(parentProject.path, wtDirName, removeOsUserInfo);
|
|
409
435
|
if (!rmResult.ok) {
|
|
410
436
|
console.log("[daemon] Failed to remove worktree:", slug, rmResult.error);
|
|
411
437
|
return { ok: false, error: rmResult.error };
|
|
@@ -1003,23 +1029,29 @@ var relay = createServer({
|
|
|
1003
1029
|
if (!config.osUsers || !linuxUser) return;
|
|
1004
1030
|
deactivateLinuxUser(linuxUser);
|
|
1005
1031
|
},
|
|
1006
|
-
onCreateWorktree: function (parentSlug, branchName, dirName, baseBranch) {
|
|
1032
|
+
onCreateWorktree: function (parentSlug, branchName, dirName, baseBranch, requestUser) {
|
|
1007
1033
|
// Find the parent project
|
|
1008
1034
|
var parent = null;
|
|
1009
1035
|
for (var j = 0; j < config.projects.length; j++) {
|
|
1010
1036
|
if (config.projects[j].slug === parentSlug) { parent = config.projects[j]; break; }
|
|
1011
1037
|
}
|
|
1012
1038
|
if (!parent) return { ok: false, error: "Parent project not found" };
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1039
|
+
var osUserInfo;
|
|
1040
|
+
try {
|
|
1041
|
+
osUserInfo = worktreeOsUserInfo(parent, requestUser);
|
|
1042
|
+
} catch (e) {
|
|
1043
|
+
return { ok: false, error: e.message };
|
|
1044
|
+
}
|
|
1045
|
+
if (isWorktree(parent.path, osUserInfo)) return { ok: false, error: "Cannot create worktrees from a worktree project" };
|
|
1046
|
+
var baseCommit = projectLogContext.commit(parent.path, baseBranch || "HEAD", osUserInfo);
|
|
1047
|
+
var result = createWorktree(parent.path, branchName, dirName, baseBranch, osUserInfo);
|
|
1016
1048
|
if (!result.ok) return result;
|
|
1017
1049
|
// Register the new worktree as ephemeral project
|
|
1018
1050
|
var wtSlug = parentSlug + "--" + dirName;
|
|
1019
1051
|
var context = projectLogContext.resolveWorktreeContext(result.path, parent.projectKnowledgeId, {
|
|
1020
1052
|
branch: branchName,
|
|
1021
1053
|
baseCommit: baseCommit,
|
|
1022
|
-
});
|
|
1054
|
+
}, osUserInfo);
|
|
1023
1055
|
var wtMeta = Object.assign({ parentSlug: parentSlug, external: false, projectKnowledgeId: parent.projectKnowledgeId }, context);
|
|
1024
1056
|
relay.addProject(result.path, wtSlug, branchName, parent.icon, parent.ownerId, wtMeta);
|
|
1025
1057
|
registerWorktreeSlug(parentSlug, wtSlug);
|
|
@@ -1114,7 +1146,7 @@ var ipc = createIPCServer(socketPath(), function (msg) {
|
|
|
1114
1146
|
try { syncClayrc(config.projects); } catch (e) {}
|
|
1115
1147
|
console.log("[daemon] Added project:", slug, "→", absPath);
|
|
1116
1148
|
// Discover and register worktrees for the new project
|
|
1117
|
-
|
|
1149
|
+
registerProjectWorktrees(projectEntry, null);
|
|
1118
1150
|
relay.broadcastAll({
|
|
1119
1151
|
type: "projects_updated",
|
|
1120
1152
|
projects: relay.getProjects(),
|
|
@@ -1345,7 +1377,7 @@ for (var i = 0; i < projects.length; i++) {
|
|
|
1345
1377
|
console.log("[daemon] Adding project:", p.slug, "→", p.path);
|
|
1346
1378
|
relay.addProject(p.path, p.slug, p.title, p.icon, p.ownerId, null, projectRuntimeOptions(p));
|
|
1347
1379
|
// Discover and register worktrees for this project
|
|
1348
|
-
|
|
1380
|
+
registerProjectWorktrees(p, null);
|
|
1349
1381
|
} else {
|
|
1350
1382
|
console.log("[daemon] Skipping missing project:", p.path);
|
|
1351
1383
|
}
|
package/lib/git-cli.js
CHANGED
package/lib/os-users.js
CHANGED
|
@@ -161,7 +161,9 @@ function fsAsUser(op, args, osUserInfo) {
|
|
|
161
161
|
"var fs = require('fs');",
|
|
162
162
|
"var f = " + JSON.stringify(args.file) + ";",
|
|
163
163
|
"var content = " + JSON.stringify(args.content || "") + ";",
|
|
164
|
-
"
|
|
164
|
+
"var flag = " + JSON.stringify(args.flag === "wx" ? "wx" : "w") + ";",
|
|
165
|
+
"var mode = " + JSON.stringify(typeof args.mode === "number" ? args.mode : 0o666) + ";",
|
|
166
|
+
"fs.writeFileSync(f, content, { encoding: 'utf8', flag: flag, mode: mode });",
|
|
165
167
|
"process.stdout.write(JSON.stringify({ ok: true }));",
|
|
166
168
|
].join(" ");
|
|
167
169
|
} else if (op === "mkdir") {
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
var crypto = require("crypto");
|
|
10
10
|
var fs = require("fs");
|
|
11
11
|
var path = require("path");
|
|
12
|
-
var
|
|
12
|
+
var { runGitSync } = require("./git-cli");
|
|
13
|
+
var { fsAsUser } = require("./os-users");
|
|
13
14
|
var logsRoot = require("./project-logs-root");
|
|
14
15
|
var utils = require("./utils");
|
|
15
16
|
|
|
@@ -47,23 +48,18 @@ function createProjectKnowledgeId() {
|
|
|
47
48
|
return newId("pk");
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
function git(cwd, args) {
|
|
51
|
-
return
|
|
52
|
-
cwd: cwd,
|
|
53
|
-
encoding: "utf8",
|
|
54
|
-
timeout: 5000,
|
|
55
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
56
|
-
}).trim();
|
|
51
|
+
function git(cwd, args, osUserInfo) {
|
|
52
|
+
return runGitSync(cwd, args, null, osUserInfo).trim();
|
|
57
53
|
}
|
|
58
54
|
|
|
59
|
-
function commit(cwd, ref) {
|
|
60
|
-
try { return git(cwd, ["rev-parse", ref || "HEAD"]); }
|
|
55
|
+
function commit(cwd, ref, osUserInfo) {
|
|
56
|
+
try { return git(cwd, ["rev-parse", ref || "HEAD"], osUserInfo); }
|
|
61
57
|
catch (e) { return null; }
|
|
62
58
|
}
|
|
63
59
|
|
|
64
|
-
function markerPath(cwd) {
|
|
60
|
+
function markerPath(cwd, osUserInfo) {
|
|
65
61
|
try {
|
|
66
|
-
var gitDir = git(cwd, ["rev-parse", "--git-dir"]);
|
|
62
|
+
var gitDir = git(cwd, ["rev-parse", "--git-dir"], osUserInfo);
|
|
67
63
|
return path.join(path.resolve(cwd, gitDir), "clay-change-set.json");
|
|
68
64
|
} catch (e) {
|
|
69
65
|
return null;
|
|
@@ -86,9 +82,18 @@ function deterministicFallback(knowledgeId, cwd) {
|
|
|
86
82
|
return "cs_" + digest.substring(0, 22);
|
|
87
83
|
}
|
|
88
84
|
|
|
89
|
-
function
|
|
85
|
+
function writeMarker(filePath, marker, osUserInfo) {
|
|
86
|
+
var content = JSON.stringify(marker, null, 2) + "\n";
|
|
87
|
+
if (osUserInfo) {
|
|
88
|
+
fsAsUser("write", { file: filePath, content: content, flag: "wx", mode: 0o600 }, osUserInfo);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
fs.writeFileSync(filePath, content, { flag: "wx", mode: 0o600 });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function resolveWorktreeContext(cwd, knowledgeId, details, osUserInfo) {
|
|
90
95
|
var input = details || {};
|
|
91
|
-
var filePath = markerPath(cwd);
|
|
96
|
+
var filePath = markerPath(cwd, osUserInfo);
|
|
92
97
|
var marker = readMarker(filePath, knowledgeId);
|
|
93
98
|
if (!marker) {
|
|
94
99
|
marker = {
|
|
@@ -99,7 +104,7 @@ function resolveWorktreeContext(cwd, knowledgeId, details) {
|
|
|
99
104
|
};
|
|
100
105
|
if (filePath) {
|
|
101
106
|
try {
|
|
102
|
-
|
|
107
|
+
writeMarker(filePath, marker, osUserInfo);
|
|
103
108
|
} catch (e) {
|
|
104
109
|
marker = readMarker(filePath, knowledgeId) || marker;
|
|
105
110
|
}
|
|
@@ -113,20 +118,16 @@ function resolveWorktreeContext(cwd, knowledgeId, details) {
|
|
|
113
118
|
changeSetId: marker.changeSetId,
|
|
114
119
|
branch: input.branch || null,
|
|
115
120
|
baseCommit: marker.baseCommit || input.baseCommit || null,
|
|
116
|
-
headCommit: commit(cwd, "HEAD"),
|
|
121
|
+
headCommit: commit(cwd, "HEAD", osUserInfo),
|
|
117
122
|
status: "active",
|
|
118
123
|
};
|
|
119
124
|
}
|
|
120
125
|
|
|
121
|
-
function isMerged(parentCwd, worktreeCwd) {
|
|
122
|
-
var head = commit(worktreeCwd, "HEAD");
|
|
126
|
+
function isMerged(parentCwd, worktreeCwd, osUserInfo) {
|
|
127
|
+
var head = commit(worktreeCwd, "HEAD", osUserInfo);
|
|
123
128
|
if (!head) return false;
|
|
124
129
|
try {
|
|
125
|
-
|
|
126
|
-
cwd: parentCwd,
|
|
127
|
-
timeout: 5000,
|
|
128
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
129
|
-
});
|
|
130
|
+
runGitSync(parentCwd, ["merge-base", "--is-ancestor", head, "HEAD"], null, osUserInfo);
|
|
130
131
|
return true;
|
|
131
132
|
} catch (e) {
|
|
132
133
|
return false;
|
|
@@ -22,6 +22,11 @@ var LOGS_CONTRACT =
|
|
|
22
22
|
"Do not paste raw conversation transcripts, log command-by-command narration, trivial confirmations, or speculation. Repository history may show the code change but usually does not preserve the user's intent, constraints, verification, or unfinished state, so it is not a substitute for the work log. " +
|
|
23
23
|
"Every write is attributed and permanently revision-tracked, so keep entries concise, concrete, and true while retaining enough context for a clean Driver handoff.";
|
|
24
24
|
|
|
25
|
+
var CLAY_READ_CONTRACT =
|
|
26
|
+
"When the user asks Clay about a project's prior work, decisions, defects, status, rationale, or unfinished work, identify the relevant project and search its Project Logs before answering. " +
|
|
27
|
+
"Use conversation history as additional evidence when useful, but do not substitute it for the ledger's durable project record. Do not search Logs for unrelated general questions. " +
|
|
28
|
+
"When citing a returned log in user-visible text, write [clayos/<ref> — short label] so Clay Studio renders an owner-validated Log control.";
|
|
29
|
+
|
|
25
30
|
// User learning moments are a durable project asset, so capturing them is a
|
|
26
31
|
// default rather than an option. This category is about a change in the user's
|
|
27
32
|
// conceptual model, never knowledge the Driver acquired while doing the work.
|
|
@@ -236,7 +241,7 @@ function globalTools(bound) {
|
|
|
236
241
|
return [
|
|
237
242
|
{
|
|
238
243
|
name: "list_project_logs",
|
|
239
|
-
description: LOGS_CONTRACT + " List logs for one project the current user is authorized to see. Read-only, and available only to authoritative builtin Clay.",
|
|
244
|
+
description: LOGS_CONTRACT + " " + CLAY_READ_CONTRACT + " List logs for one project the current user is authorized to see. Read-only, and available only to authoritative builtin Clay.",
|
|
240
245
|
inputSchema: buildShape({
|
|
241
246
|
projectSlug: { type: "string", description: "Exact project slug." },
|
|
242
247
|
kind: { type: "string", description: "Optional category filter, matched exactly against this project's vocabulary. The response lists the categories currently in use." },
|
|
@@ -250,7 +255,7 @@ function globalTools(bound) {
|
|
|
250
255
|
},
|
|
251
256
|
{
|
|
252
257
|
name: "search_project_logs",
|
|
253
|
-
description: LOGS_CONTRACT + " Search logs for one project the current user is authorized to see. Read-only, and available only to authoritative builtin Clay.",
|
|
258
|
+
description: LOGS_CONTRACT + " " + CLAY_READ_CONTRACT + " Search logs for one project the current user is authorized to see. Read-only, and available only to authoritative builtin Clay.",
|
|
254
259
|
inputSchema: buildShape({
|
|
255
260
|
projectSlug: { type: "string", description: "Exact project slug." },
|
|
256
261
|
query: { type: "string", description: "Search query." },
|
|
@@ -264,7 +269,7 @@ function globalTools(bound) {
|
|
|
264
269
|
},
|
|
265
270
|
{
|
|
266
271
|
name: "read_project_log_revision",
|
|
267
|
-
description: LOGS_CONTRACT + " Read one entry as of a given revision. Read-only, and available only to authoritative builtin Clay.",
|
|
272
|
+
description: LOGS_CONTRACT + " " + CLAY_READ_CONTRACT + " Read one entry as of a given revision. Read-only, and available only to authoritative builtin Clay.",
|
|
268
273
|
inputSchema: buildShape({
|
|
269
274
|
projectSlug: { type: "string", description: "Exact project slug." },
|
|
270
275
|
ref: { type: "string", description: REF_DESCRIPTION },
|
|
@@ -274,7 +279,7 @@ function globalTools(bound) {
|
|
|
274
279
|
},
|
|
275
280
|
{
|
|
276
281
|
name: "read_project_log",
|
|
277
|
-
description: LOGS_CONTRACT + " Read one log entry from an authorized project, including its summary and any comments people have added. Read-only, and available only to authoritative builtin Clay.",
|
|
282
|
+
description: LOGS_CONTRACT + " " + CLAY_READ_CONTRACT + " Read one log entry from an authorized project, including its summary and any comments people have added. Read-only, and available only to authoritative builtin Clay.",
|
|
278
283
|
inputSchema: buildShape({
|
|
279
284
|
projectSlug: { type: "string", description: "Exact project slug." },
|
|
280
285
|
ref: { type: "string", description: REF_DESCRIPTION },
|
|
@@ -312,6 +317,7 @@ function createMcpServer(adapter, bound, includeGlobal) {
|
|
|
312
317
|
|
|
313
318
|
module.exports = {
|
|
314
319
|
LOGS_CONTRACT: LOGS_CONTRACT,
|
|
320
|
+
CLAY_READ_CONTRACT: CLAY_READ_CONTRACT,
|
|
315
321
|
LEARNING_CONTRACT: LEARNING_CONTRACT,
|
|
316
322
|
ATTENTION_CONTRACT: ATTENTION_CONTRACT,
|
|
317
323
|
REVIEW_CONTRACT: REVIEW_CONTRACT,
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
var builtinMates = require("./builtin-mates");
|
|
23
23
|
var logsStore = require("./project-logs-store");
|
|
24
|
+
var LOG_REF_PATTERN = /^log:[A-Za-z0-9_-]{24}$/;
|
|
24
25
|
|
|
25
26
|
function readOnly() {
|
|
26
27
|
throw new Error("Project Logs are read-only for builtin Clay.");
|
|
@@ -396,6 +397,33 @@ function attachProjectLogsService(ctx) {
|
|
|
396
397
|
return storeForStatus(statusFor(principal, slug));
|
|
397
398
|
}
|
|
398
399
|
|
|
400
|
+
function resolveLogNavigation(args) {
|
|
401
|
+
var principal = revalidate();
|
|
402
|
+
var ref = args && args.ref;
|
|
403
|
+
if (!LOG_REF_PATTERN.test(ref || "")) throw new Error("Invalid log reference.");
|
|
404
|
+
var projects = getProjects();
|
|
405
|
+
var seenScopes = new Set();
|
|
406
|
+
var match = null;
|
|
407
|
+
if (!projects || typeof projects.forEach !== "function") throw new Error("Project Logs are unavailable.");
|
|
408
|
+
projects.forEach(function (project, slug) {
|
|
409
|
+
if (!project || typeof project.getStatus !== "function") return;
|
|
410
|
+
var status = project.getStatus();
|
|
411
|
+
if (!authorizedProject(principal, status)) return;
|
|
412
|
+
var governing = effectiveStatus(status);
|
|
413
|
+
var scope = governing && (governing.projectKnowledgeId || governing.path);
|
|
414
|
+
if (!scope || seenScopes.has(scope)) return;
|
|
415
|
+
seenScopes.add(scope);
|
|
416
|
+
var entry = null;
|
|
417
|
+
try { entry = storeForStatus(status).read(ref, false); } catch (e) { return; }
|
|
418
|
+
if (!entry) return;
|
|
419
|
+
var targetSlug = status.isWorktree && status.parentSlug && projectFor(status.parentSlug) ? status.parentSlug : slug;
|
|
420
|
+
if (match && match.projectSlug !== targetSlug) throw new Error("Log reference is ambiguous.");
|
|
421
|
+
match = { projectSlug: targetSlug, ref: ref };
|
|
422
|
+
});
|
|
423
|
+
if (!match) throw new Error("Log entry not found.");
|
|
424
|
+
return match;
|
|
425
|
+
}
|
|
426
|
+
|
|
399
427
|
return {
|
|
400
428
|
isClay: true,
|
|
401
429
|
canWrite: false,
|
|
@@ -413,6 +441,7 @@ function attachProjectLogsService(ctx) {
|
|
|
413
441
|
logHistory: function (args) { return target(args).history(args && args.ref, args || {}); },
|
|
414
442
|
commentLog: readOnly,
|
|
415
443
|
readLogRevision: function (args) { return target(args).readRevision(args && args.ref, args && args.revision); },
|
|
444
|
+
resolveLogNavigation: resolveLogNavigation,
|
|
416
445
|
listLogFeedback: readOnly,
|
|
417
446
|
reviewLogComment: readOnly,
|
|
418
447
|
revertLog: readOnly,
|
package/lib/project-logs.js
CHANGED
|
@@ -366,7 +366,11 @@ function attachProjectLogs(ctx) {
|
|
|
366
366
|
}
|
|
367
367
|
|
|
368
368
|
function getSystemPrompt(session) {
|
|
369
|
-
if (
|
|
369
|
+
if (!service) return "";
|
|
370
|
+
if (isMate) {
|
|
371
|
+
if (!includeGlobal() || (session && !sessionBinding(session))) return "";
|
|
372
|
+
return SYSTEM_PROMPT_LABEL + "\n" + logsMcp.CLAY_READ_CONTRACT;
|
|
373
|
+
}
|
|
370
374
|
if (session && !sessionBinding(session)) return "";
|
|
371
375
|
return SYSTEM_PROMPT_LABEL + "\n" + logsMcp.LOGS_CONTRACT +
|
|
372
376
|
"\n" + logsMcp.LEARNING_CONTRACT +
|
package/lib/project-sessions.js
CHANGED
|
@@ -1777,8 +1777,12 @@ function attachSessions(ctx) {
|
|
|
1777
1777
|
sendTo(ws, { type: "create_worktree_result", ok: false, error: "Invalid branch name" });
|
|
1778
1778
|
return true;
|
|
1779
1779
|
}
|
|
1780
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(wtDirName)) {
|
|
1781
|
+
sendTo(ws, { type: "create_worktree_result", ok: false, error: "Invalid worktree directory name" });
|
|
1782
|
+
return true;
|
|
1783
|
+
}
|
|
1780
1784
|
if (typeof onCreateWorktree === "function") {
|
|
1781
|
-
var wtResult = onCreateWorktree(slug, wtBranch, wtDirName, wtBase);
|
|
1785
|
+
var wtResult = onCreateWorktree(slug, wtBranch, wtDirName, wtBase, ws._clayUser);
|
|
1782
1786
|
sendTo(ws, { type: "create_worktree_result", ok: wtResult.ok, slug: wtResult.slug, error: wtResult.error });
|
|
1783
1787
|
} else {
|
|
1784
1788
|
sendTo(ws, { type: "create_worktree_result", ok: false, error: "Not supported" });
|
package/lib/project.js
CHANGED
|
@@ -1319,7 +1319,7 @@ function createProjectContext(opts) {
|
|
|
1319
1319
|
if (typeof opts.onDmMessage === "function") opts.onDmMessage(ws, msg);
|
|
1320
1320
|
return;
|
|
1321
1321
|
}
|
|
1322
|
-
if (msg.type === "home_clay_ask" || msg.type === "home_clay_session_resolve") {
|
|
1322
|
+
if (msg.type === "home_clay_ask" || msg.type === "home_clay_session_resolve" || msg.type === "home_clay_log_resolve") {
|
|
1323
1323
|
if (typeof opts.onDmMessage === "function") opts.onDmMessage(ws, msg, slug);
|
|
1324
1324
|
return;
|
|
1325
1325
|
}
|
|
@@ -486,7 +486,8 @@ body.mate-dm-active .search-clay-transcript .search-clay-message-user .bubble,
|
|
|
486
486
|
.search-clay-activity-item small { color: var(--text-dimmer); font-size: 10px; line-height: 1.35; }
|
|
487
487
|
@keyframes searchClaySpin { to { transform: rotate(360deg); } }
|
|
488
488
|
|
|
489
|
-
.clayos-session-link
|
|
489
|
+
.clayos-session-link,
|
|
490
|
+
.clayos-log-link {
|
|
490
491
|
display: inline-flex;
|
|
491
492
|
align-items: center;
|
|
492
493
|
gap: 6px;
|
|
@@ -503,7 +504,8 @@ body.mate-dm-active .search-clay-transcript .search-clay-message-user .bubble,
|
|
|
503
504
|
vertical-align: middle;
|
|
504
505
|
cursor: pointer;
|
|
505
506
|
}
|
|
506
|
-
.clayos-session-link::before
|
|
507
|
+
.clayos-session-link::before,
|
|
508
|
+
.clayos-log-link::before {
|
|
507
509
|
content: "\2197";
|
|
508
510
|
display: grid;
|
|
509
511
|
place-items: center;
|
|
@@ -514,13 +516,21 @@ body.mate-dm-active .search-clay-transcript .search-clay-message-user .bubble,
|
|
|
514
516
|
color: var(--accent);
|
|
515
517
|
font-size: 12px;
|
|
516
518
|
}
|
|
517
|
-
.clayos-
|
|
518
|
-
.clayos-session-link >
|
|
519
|
-
.clayos-
|
|
520
|
-
.clayos-session-link
|
|
521
|
-
.clayos-
|
|
522
|
-
.clayos-session-link
|
|
523
|
-
.clayos-
|
|
519
|
+
.clayos-log-link::before { content: "\2261"; }
|
|
520
|
+
.clayos-session-link > span,
|
|
521
|
+
.clayos-log-link > span { font-weight: 620; }
|
|
522
|
+
.clayos-session-link > small,
|
|
523
|
+
.clayos-log-link > small { color: var(--text-muted); font-size: 10px; }
|
|
524
|
+
.clayos-session-link:hover,
|
|
525
|
+
.clayos-log-link:hover { border-color: color-mix(in srgb, var(--accent) 52%, var(--border)); background: color-mix(in srgb, var(--accent) 11%, var(--bg-alt)); }
|
|
526
|
+
.clayos-session-link:focus-visible,
|
|
527
|
+
.clayos-log-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
528
|
+
.clayos-session-link.is-loading,
|
|
529
|
+
.clayos-log-link.is-loading { opacity: 0.62; cursor: progress; }
|
|
530
|
+
.clayos-session-link.is-loading::before,
|
|
531
|
+
.clayos-log-link.is-loading::before { animation: searchClaySpin 1.2s linear infinite; }
|
|
532
|
+
.clayos-session-link.is-error,
|
|
533
|
+
.clayos-log-link.is-error { border-color: color-mix(in srgb, var(--warning) 48%, var(--border)); }
|
|
524
534
|
|
|
525
535
|
.search-clay-pending {
|
|
526
536
|
align-self: flex-start;
|
|
@@ -930,6 +930,17 @@
|
|
|
930
930
|
position: relative;
|
|
931
931
|
}
|
|
932
932
|
|
|
933
|
+
.session-driver-header {
|
|
934
|
+
display: flex;
|
|
935
|
+
align-items: center;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
.session-driver-header > .session-item {
|
|
939
|
+
min-width: 0;
|
|
940
|
+
flex: 1;
|
|
941
|
+
padding-left: 0;
|
|
942
|
+
}
|
|
943
|
+
|
|
933
944
|
.session-driver-toggle {
|
|
934
945
|
width: 18px;
|
|
935
946
|
height: 24px;
|
|
@@ -85,9 +85,13 @@
|
|
|
85
85
|
min-height: 80px;
|
|
86
86
|
border: 1px solid color-mix(in srgb, var(--note-border) calc(var(--note-opacity, 0.64) * 100%), transparent);
|
|
87
87
|
background: color-mix(in srgb, var(--note-surface) calc(var(--note-opacity, 0.64) * 100%), transparent);
|
|
88
|
+
-webkit-backdrop-filter: blur(7px) saturate(0.92) contrast(1.02);
|
|
89
|
+
backdrop-filter: blur(7px) saturate(0.92) contrast(1.02);
|
|
88
90
|
color: var(--note-ink);
|
|
89
91
|
transition: box-shadow 0.15s;
|
|
90
92
|
overflow: hidden;
|
|
93
|
+
contain: layout style;
|
|
94
|
+
will-change: transform, backdrop-filter;
|
|
91
95
|
}
|
|
92
96
|
|
|
93
97
|
.sticky-note:hover {
|
|
@@ -798,8 +802,11 @@
|
|
|
798
802
|
|
|
799
803
|
@media (prefers-reduced-transparency: reduce) {
|
|
800
804
|
.sticky-note {
|
|
805
|
+
-webkit-backdrop-filter: none;
|
|
806
|
+
backdrop-filter: none;
|
|
801
807
|
border-color: var(--note-border);
|
|
802
808
|
background: var(--note-surface);
|
|
809
|
+
will-change: transform;
|
|
803
810
|
}
|
|
804
811
|
}
|
|
805
812
|
|
package/lib/public/index.html
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
(function(){try{var k="clay-theme-vars",v=localStorage.getItem(k),r=document.documentElement;if(v){var o=JSON.parse(v),p;for(p in o)r.style.setProperty(p,o[p]);var vt=localStorage.getItem(k.replace("-vars","-variant"));if(vt==="light"){r.classList.add("light-theme");r.classList.remove("dark-theme")}else{r.classList.add("dark-theme");r.classList.remove("light-theme")}var m=document.querySelector('meta[name="theme-color"]');if(m&&o["--bg"])m.setAttribute("content",o["--bg"])}else{var sl=window.matchMedia&&window.matchMedia("(prefers-color-scheme: light)").matches;if(sl){r.classList.add("light-theme");r.classList.remove("dark-theme")}}}catch(e){}})();
|
|
32
32
|
</script>
|
|
33
33
|
<script>if(window.navigator.standalone||window.matchMedia("(display-mode:standalone)").matches){document.documentElement.classList.add("pwa-standalone")}</script>
|
|
34
|
-
<link rel="stylesheet" href="style.css?v=
|
|
34
|
+
<link rel="stylesheet" href="style.css?v=20260908-log-links2">
|
|
35
35
|
<style>
|
|
36
36
|
@media(max-width:768px){
|
|
37
37
|
/* User messages: vertical stack, avatar on top, right-aligned */
|
|
@@ -2337,7 +2337,7 @@
|
|
|
2337
2337
|
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0/lib/addon-fit.min.js"></script>
|
|
2338
2338
|
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-web-links@0/lib/addon-web-links.min.js"></script>
|
|
2339
2339
|
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-webgl@0/lib/addon-webgl.min.js"></script>
|
|
2340
|
-
<script type="module" src="app.js?v=
|
|
2340
|
+
<script type="module" src="app.js?v=20260908-log-links2"></script>
|
|
2341
2341
|
<div id="pwa-install-modal" class="pwa-modal hidden">
|
|
2342
2342
|
<div class="pwa-modal-backdrop"></div>
|
|
2343
2343
|
<div class="pwa-modal-card">
|
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
import { iconHtml, refreshIcons } from './icons.js';
|
|
3
3
|
import { showToast, copyToClipboard, escapeHtml } from './utils.js';
|
|
4
4
|
|
|
5
|
-
function showConfirmDialog(message, onConfirm) {
|
|
5
|
+
function showConfirmDialog(message, actionLabel, onConfirm) {
|
|
6
6
|
var modal = document.createElement("div");
|
|
7
7
|
modal.className = "admin-modal-overlay";
|
|
8
|
-
var html = '<div class="admin-modal">' +
|
|
8
|
+
var html = '<div class="admin-modal" role="dialog" aria-modal="true" aria-label="Confirm action">' +
|
|
9
9
|
'<div class="admin-modal-body" style="padding:20px 16px 16px">' +
|
|
10
10
|
'<p class="admin-modal-desc" style="margin:0;font-size:14px;color:var(--text)">' + escapeHtml(message) + '</p>' +
|
|
11
11
|
'</div>' +
|
|
12
12
|
'<div class="admin-modal-footer">' +
|
|
13
|
-
'<button class="admin-modal-save admin-modal-confirm-danger">
|
|
13
|
+
'<button class="admin-modal-save admin-modal-confirm-danger">' + escapeHtml(actionLabel) + '</button>' +
|
|
14
14
|
'<button class="admin-modal-cancel">Cancel</button>' +
|
|
15
15
|
'</div></div>';
|
|
16
16
|
modal.innerHTML = html;
|
|
@@ -198,9 +198,9 @@ function renderUsersTab(body) {
|
|
|
198
198
|
var userId = this.dataset.userId;
|
|
199
199
|
var user = cachedUsers.find(function (u) { return u.id === userId; });
|
|
200
200
|
var name = user ? (user.displayName || user.username) : "this user";
|
|
201
|
-
|
|
201
|
+
showConfirmDialog("Remove " + name + "? This cannot be undone.", "Remove user", function () {
|
|
202
202
|
removeUser(userId, body);
|
|
203
|
-
}
|
|
203
|
+
});
|
|
204
204
|
});
|
|
205
205
|
}
|
|
206
206
|
}
|
|
@@ -509,7 +509,7 @@ function renderInvitesTab(body) {
|
|
|
509
509
|
revokeBtns[k].addEventListener("click", function () {
|
|
510
510
|
var code = this.dataset.code;
|
|
511
511
|
var btn = this;
|
|
512
|
-
showConfirmDialog("Revoke this invite? The link will no longer work.", function () {
|
|
512
|
+
showConfirmDialog("Revoke this invite? The link will no longer work.", "Revoke", function () {
|
|
513
513
|
btn.disabled = true;
|
|
514
514
|
fetch("/api/admin/invites/" + encodeURIComponent(code), { method: "DELETE" })
|
|
515
515
|
.then(function (r) { return r.json(); })
|
|
@@ -751,12 +751,12 @@ function renderSmtpTab(body, cfg) {
|
|
|
751
751
|
var removeBtn = body.querySelector("#smtp-remove");
|
|
752
752
|
if (removeBtn) {
|
|
753
753
|
removeBtn.addEventListener("click", function () {
|
|
754
|
-
|
|
754
|
+
showConfirmDialog("Remove SMTP configuration? Users will need to use PIN login.", "Remove SMTP", function () {
|
|
755
755
|
apiPost("/api/admin/smtp", { host: "", user: "", pass: "", from: "" }).then(function () {
|
|
756
756
|
showToast("SMTP configuration removed");
|
|
757
757
|
loadSmtpTab(body);
|
|
758
758
|
});
|
|
759
|
-
}
|
|
759
|
+
});
|
|
760
760
|
});
|
|
761
761
|
}
|
|
762
762
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { store } from './store.js';
|
|
4
4
|
import { getWs } from './ws-ref.js';
|
|
5
|
-
import { getCachedProjects } from './app-projects.js';
|
|
5
|
+
import { getCachedProjects, switchProject } from './app-projects.js';
|
|
6
6
|
import { mateAvatarUrl } from './avatar.js';
|
|
7
7
|
import { exitDmMode } from './app-dm.js';
|
|
8
8
|
import { closeHomeDock, initHomeDock, renderDock, requestHomeDockPreference } from './home-dock.js';
|
|
@@ -41,7 +41,22 @@ function syncHomePresentation() {
|
|
|
41
41
|
function syncHomeCloseControl() {
|
|
42
42
|
var close = document.getElementById("home-close-control");
|
|
43
43
|
if (!close) return;
|
|
44
|
-
close.classList.toggle("hidden", !
|
|
44
|
+
close.classList.toggle("hidden", !getHomeReturnSlug());
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getHomeReturnSlug() {
|
|
48
|
+
var projects = getCachedProjects() || [];
|
|
49
|
+
var candidates = [store.get('currentSlug'), store.get('homeSurfaceProjectSlug')];
|
|
50
|
+
for (var ci = 0; ci < candidates.length; ci++) {
|
|
51
|
+
if (!candidates[ci]) continue;
|
|
52
|
+
for (var pi = 0; pi < projects.length; pi++) {
|
|
53
|
+
if (projects[pi] && !projects[pi].isMate && projects[pi].slug === candidates[ci]) return candidates[ci];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
for (var i = 0; i < projects.length; i++) {
|
|
57
|
+
if (projects[i] && !projects[i].isMate && projects[i].slug) return projects[i].slug;
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
45
60
|
}
|
|
46
61
|
|
|
47
62
|
function getVisibleMates() {
|
|
@@ -390,13 +405,10 @@ export function hideHomeHub() {
|
|
|
390
405
|
}
|
|
391
406
|
|
|
392
407
|
export function minimizeHomeHub() {
|
|
393
|
-
var slug =
|
|
408
|
+
var slug = getHomeReturnSlug();
|
|
394
409
|
if (!slug || !homeHubVisible) return;
|
|
395
410
|
rememberHomePrimarySurface("project");
|
|
396
|
-
|
|
397
|
-
var route = "/p/" + slug + "/";
|
|
398
|
-
if (document.documentElement.classList.contains("pwa-standalone")) history.replaceState(null, "", route);
|
|
399
|
-
else history.pushState(null, "", route);
|
|
411
|
+
switchProject(slug);
|
|
400
412
|
var projectInput = document.getElementById("input");
|
|
401
413
|
if (projectInput && !projectInput.disabled) projectInput.focus({ preventScroll: true });
|
|
402
414
|
}
|