clay-server 4.0.0-beta.1 → 4.0.0-beta.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/lib/background-task-timing.js +105 -0
  2. package/lib/config.js +25 -1
  3. package/lib/daemon.js +22 -0
  4. package/lib/git-cli.js +94 -1
  5. package/lib/knowledge-import.js +436 -0
  6. package/lib/knowledge-record-store.js +188 -0
  7. package/lib/knowledge-search.js +240 -0
  8. package/lib/mate-knowledge-mcp-server.js +151 -0
  9. package/lib/mate-knowledge-migration.js +283 -0
  10. package/lib/mate-knowledge-service.js +488 -0
  11. package/lib/mate-knowledge-sync.js +482 -0
  12. package/lib/notes-lifecycle.js +149 -0
  13. package/lib/notes.js +60 -3
  14. package/lib/project-connection.js +17 -0
  15. package/lib/project-http.js +15 -1
  16. package/lib/project-knowledge.js +22 -0
  17. package/lib/project-logs-comments.js +137 -0
  18. package/lib/project-logs-mcp-server.js +314 -0
  19. package/lib/project-logs-query.js +167 -0
  20. package/lib/project-logs-root.js +67 -0
  21. package/lib/project-logs-schema.js +259 -0
  22. package/lib/project-logs-service.js +357 -0
  23. package/lib/project-logs-snapshot.js +202 -0
  24. package/lib/project-logs-store.js +498 -0
  25. package/lib/project-logs-versioning.js +128 -0
  26. package/lib/project-logs.js +348 -0
  27. package/lib/project-mate-interaction.js +14 -0
  28. package/lib/project-mate-knowledge.js +110 -0
  29. package/lib/project-memory.js +22 -0
  30. package/lib/project-pair-lifecycle.js +461 -0
  31. package/lib/project-session-notes.js +64 -13
  32. package/lib/project-session-pair.js +124 -111
  33. package/lib/project-sessions.js +46 -6
  34. package/lib/project-user-message.js +41 -4
  35. package/lib/project-vendor-login.js +414 -0
  36. package/lib/project-worker-permission.js +429 -0
  37. package/lib/project-worker-proposal.js +16 -1
  38. package/lib/project.js +164 -4
  39. package/lib/public/app.js +49 -20
  40. package/lib/public/css/git-placard.css +154 -0
  41. package/lib/public/css/input.css +79 -25
  42. package/lib/public/css/notifications-center.css +67 -0
  43. package/lib/public/css/pane.css +52 -0
  44. package/lib/public/css/project-logs.css +234 -0
  45. package/lib/public/css/scheduler.css +1 -1
  46. package/lib/public/css/sticky-notes.css +231 -226
  47. package/lib/public/index.html +12 -0
  48. package/lib/public/modules/app-connection.js +90 -4
  49. package/lib/public/modules/app-messages.js +54 -19
  50. package/lib/public/modules/app-notifications.js +15 -124
  51. package/lib/public/modules/app-panels.js +43 -2
  52. package/lib/public/modules/app-projects.js +2 -2
  53. package/lib/public/modules/background-tasks-ui.js +139 -16
  54. package/lib/public/modules/git-agent-sessions.js +85 -0
  55. package/lib/public/modules/git-panel.js +111 -84
  56. package/lib/public/modules/git-placard.js +161 -0
  57. package/lib/public/modules/home-sidebar.js +7 -0
  58. package/lib/public/modules/mate-sidebar.js +0 -8
  59. package/lib/public/modules/pane-bridge.js +9 -0
  60. package/lib/public/modules/project-logs-ambient.js +238 -0
  61. package/lib/public/modules/project-logs-render.js +359 -0
  62. package/lib/public/modules/project-logs.js +329 -0
  63. package/lib/public/modules/scheduler.js +17 -3
  64. package/lib/public/modules/sidebar-mobile.js +5 -9
  65. package/lib/public/modules/sidebar-sessions.js +25 -1
  66. package/lib/public/modules/sidebar.js +17 -5
  67. package/lib/public/modules/split-view.js +7 -0
  68. package/lib/public/modules/sticky-notes-browser.js +324 -0
  69. package/lib/public/modules/sticky-notes-card.js +358 -0
  70. package/lib/public/modules/sticky-notes-editor.js +288 -0
  71. package/lib/public/modules/sticky-notes-shared.js +90 -0
  72. package/lib/public/modules/sticky-notes.js +168 -946
  73. package/lib/public/modules/tool-palette-order.js +143 -0
  74. package/lib/public/modules/tool-palette-overlays.js +159 -0
  75. package/lib/public/modules/tool-palette.js +21 -188
  76. package/lib/public/modules/tui-attention.js +10 -0
  77. package/lib/public/modules/update-snooze.js +169 -0
  78. package/lib/public/modules/vendor-login.js +287 -0
  79. package/lib/public/modules/worker-pane-lock.js +174 -0
  80. package/lib/public/style.css +2 -0
  81. package/lib/sdk-bridge.js +77 -10
  82. package/lib/sdk-message-processor.js +24 -7
  83. package/lib/server.js +61 -6
  84. package/lib/session-driver-eligibility.js +183 -0
  85. package/lib/session-notes-mcp-server.js +25 -5
  86. package/lib/session-pair-factory.js +233 -0
  87. package/lib/session-pair-mcp-server.js +45 -5
  88. package/lib/session-pair-prompts.js +60 -0
  89. package/lib/sessions.js +4 -1
  90. package/lib/tools-registry.js +11 -1
  91. package/lib/update-snooze.js +437 -0
  92. package/lib/workspace-query-access.js +102 -0
  93. package/lib/workspace-query-service.js +24 -17
  94. package/lib/ws-schema.js +28 -3
  95. package/lib/yoke/adapters/claude.js +69 -4
  96. package/lib/yoke/adapters/codex.js +42 -5
  97. package/lib/yoke/codex-app-server.js +20 -2
  98. package/lib/yoke/codex-background-tasks.js +8 -2
  99. package/package.json +3 -3
@@ -0,0 +1,105 @@
1
+ // Background task list shaping shared by the server-side consumers:
2
+ // start-time tracking, plus the ambient/user split every activity indicator
3
+ // needs.
4
+ //
5
+ // Vendors report background tasks as a full replacement list on every change
6
+ // and none of them carries a reliable start time today (see
7
+ // normalizeBackgroundTasks in yoke/adapters/claude.js and mapTerminals in
8
+ // yoke/codex-background-tasks.js). The session's previous list is the only
9
+ // place that knows when a task was first seen, so the merge belongs here,
10
+ // where that list is maintained, rather than in an adapter that rebuilds the
11
+ // array from scratch each time.
12
+ //
13
+ // A vendor-supplied timestamp always wins when present; otherwise a task is
14
+ // stamped the first time it appears and keeps that stamp for its whole life.
15
+
16
+ // Accepts epoch milliseconds, epoch seconds, or an ISO date string, since
17
+ // vendors are not consistent. Returns null for anything unusable so the
18
+ // caller falls back to first-seen stamping instead of rendering 1970.
19
+ function normalizeTimestamp(value) {
20
+ if (typeof value === "number" && isFinite(value) && value > 0) {
21
+ // Values this small are epoch seconds, not milliseconds.
22
+ return value < 1e12 ? Math.round(value * 1000) : Math.round(value);
23
+ }
24
+ if (typeof value === "string" && value) {
25
+ var parsed = Date.parse(value);
26
+ if (!isNaN(parsed)) return parsed;
27
+ }
28
+ return null;
29
+ }
30
+
31
+ function vendorStartedAt(task) {
32
+ if (!task) return null;
33
+ return normalizeTimestamp(task.started_at)
34
+ || normalizeTimestamp(task.startedAt)
35
+ || normalizeTimestamp(task.createdAt)
36
+ || normalizeTimestamp(task.created_at);
37
+ }
38
+
39
+ /**
40
+ * Return `nextTasks` with a `started_at` (epoch ms) on every entry.
41
+ * Tasks already present in `previousTasks` keep their original stamp so the
42
+ * elapsed time does not reset every time the list is re-emitted.
43
+ */
44
+ function mergeStartTimes(previousTasks, nextTasks, now) {
45
+ if (!Array.isArray(nextTasks)) return [];
46
+ var stampedAt = typeof now === "number" ? now : Date.now();
47
+ // Stamps we wrote on a previous pass are already epoch milliseconds, so they
48
+ // are read as-is. Only vendor-supplied values go through normalizeTimestamp,
49
+ // whose seconds-vs-milliseconds heuristic would otherwise be re-applied to
50
+ // our own output.
51
+ var known = {};
52
+ var previous = Array.isArray(previousTasks) ? previousTasks : [];
53
+ for (var p = 0; p < previous.length; p++) {
54
+ var seen = previous[p];
55
+ if (!seen || !seen.task_id) continue;
56
+ if (typeof seen.started_at === "number" && isFinite(seen.started_at) && seen.started_at > 0) {
57
+ known[seen.task_id] = seen.started_at;
58
+ }
59
+ }
60
+
61
+ var merged = [];
62
+ for (var i = 0; i < nextTasks.length; i++) {
63
+ var task = nextTasks[i];
64
+ if (!task) continue;
65
+ var startedAt = vendorStartedAt(task) || known[task.task_id] || stampedAt;
66
+ merged.push(Object.assign({}, task, { started_at: startedAt }));
67
+ }
68
+ return merged;
69
+ }
70
+
71
+ // Ambient tasks are the CLI's own housekeeping (every skip_transcript task
72
+ // plus auto-started live-update watchers). The SDK asks hosts to keep them out
73
+ // of activity indicators, so every count that drives a badge, a summary, or a
74
+ // change notification must go through here rather than reading `.length`.
75
+ // Absent means false: vendors without an ambient concept (Codex terminals)
76
+ // and older CLI builds report only real user work.
77
+ function isAmbientTask(task) {
78
+ return !!(task && task.ambient === true);
79
+ }
80
+
81
+ function userTasks(tasks) {
82
+ if (!Array.isArray(tasks)) return [];
83
+ return tasks.filter(function (task) {
84
+ return task && !isAmbientTask(task);
85
+ });
86
+ }
87
+
88
+ function ambientTasks(tasks) {
89
+ if (!Array.isArray(tasks)) return [];
90
+ return tasks.filter(isAmbientTask);
91
+ }
92
+
93
+ // The count that represents "user-visible background work is running".
94
+ function countUserTasks(tasks) {
95
+ return userTasks(tasks).length;
96
+ }
97
+
98
+ module.exports = {
99
+ mergeStartTimes: mergeStartTimes,
100
+ normalizeTimestamp: normalizeTimestamp,
101
+ isAmbientTask: isAmbientTask,
102
+ userTasks: userTasks,
103
+ ambientTasks: ambientTasks,
104
+ countUserTasks: countUserTasks,
105
+ };
package/lib/config.js CHANGED
@@ -148,12 +148,36 @@ function saveConfig(config) {
148
148
  chmodSafe(configPath(), 0o600);
149
149
  }
150
150
 
151
+ // Does a process with this pid exist? Existence only — never ownership.
152
+ //
153
+ // `process.kill(pid, 0)` sends no signal; it just asks the kernel. Three
154
+ // answers matter, and conflating them has caused real damage:
155
+ //
156
+ // success -> the process exists and we may signal it
157
+ // EPERM -> the process EXISTS but belongs to another user. This is alive.
158
+ // Reading it as dead is how a daemon started by another OS user
159
+ // (or under OS-user isolation) got its config cleared and its
160
+ // socket unlinked by a second daemon, which then started
161
+ // alongside it.
162
+ // ESRCH -> no such process. This is the only "dead".
163
+ //
164
+ // Any other error is a question we could not answer, so we answer "alive":
165
+ // every caller uses this to gate a destructive or duplicating action
166
+ // (clearStaleConfig unlinks the live socket; the migration lock would be
167
+ // stolen), and doing nothing is always the recoverable choice.
168
+ //
169
+ // The pid is validated first, so a malformed value never reaches the kernel
170
+ // call. That matters beyond hygiene: process.kill accepts 0 and negative pids
171
+ // as process-GROUP selectors and returns success for them, so an unvalidated
172
+ // 0 or -1 in a config file used to report a live daemon that does not exist.
151
173
  function isPidAlive(pid) {
174
+ if (typeof pid !== "number" || !isFinite(pid) || Math.floor(pid) !== pid || pid <= 0) return false;
152
175
  try {
153
176
  process.kill(pid, 0);
154
177
  return true;
155
178
  } catch (e) {
156
- return false;
179
+ if (e && e.code === "ESRCH") return false;
180
+ return true;
157
181
  }
158
182
  }
159
183
 
package/lib/daemon.js CHANGED
@@ -34,6 +34,7 @@ var { checkAclSupport, grantProjectAccess, revokeProjectAccess, provisionAllUser
34
34
  var usersModule = require("./users");
35
35
  var { createWorktree, removeWorktree, isWorktree } = require("./worktree");
36
36
  var mates = require("./mates");
37
+ var mateKnowledgeMigration = require("./mate-knowledge-migration");
37
38
  var { isWorktreeSlug, scanAndRegisterWorktrees, rescanWorktrees, cleanupWorktreesForParent, getFilteredRemovedProjects, registerWorktreeSlug, unregisterWorktreeSlug } = require("./daemon-projects");
38
39
 
39
40
  var daemonVersion = require("../package.json").version;
@@ -1294,6 +1295,27 @@ for (var i = 0; i < projects.length; i++) {
1294
1295
  // Migrate legacy mates storage before registration
1295
1296
  mates.migrateLegacyMates();
1296
1297
 
1298
+ // Import durable Mate knowledge into the Clay-wide Knowledge backend. Runs on
1299
+ // every start, imports nothing when there is nothing new, and never touches
1300
+ // the legacy files that remain the live source for existing read paths.
1301
+ mateKnowledgeMigration.runAtStartup({
1302
+ extraRoots: (function () {
1303
+ var roots = [];
1304
+ try {
1305
+ if (!usersModule.isMultiUser()) return roots;
1306
+ var all = usersModule.getAllUsers() || [];
1307
+ for (var mi = 0; mi < all.length; mi++) {
1308
+ if (!all[mi] || !all[mi].linuxUser) continue;
1309
+ roots.push({
1310
+ userId: all[mi].id,
1311
+ matesRoot: mates.resolveMatesRoot({ userId: all[mi].id, multiUser: true, linuxUser: all[mi].linuxUser }),
1312
+ });
1313
+ }
1314
+ } catch (e) {}
1315
+ return roots;
1316
+ })(),
1317
+ });
1318
+
1297
1319
  // Register existing mates as projects
1298
1320
  if (usersModule.isMultiUser()) {
1299
1321
  // Multi-user mode: iterate over all users and load each user's mates
package/lib/git-cli.js CHANGED
@@ -6,6 +6,7 @@ var { execFile, execFileSync } = require("child_process");
6
6
  var { wrapSpawnAsUser } = require("./os-users");
7
7
 
8
8
  var MAX_DIFF_BYTES = 2 * 1024 * 1024;
9
+ var MAX_REMOTE_LABEL = 64;
9
10
 
10
11
  function gitEnvironment(osUserInfo) {
11
12
  var overrides = { GIT_TERMINAL_PROMPT: "0" };
@@ -157,6 +158,24 @@ function normalizeGitPath(cwd, value) {
157
158
  return path.normalize(path.isAbsolute(value) ? value : path.resolve(cwd, value));
158
159
  }
159
160
 
161
+ // Whether this checkout is a linked worktree. The two directories have to be
162
+ // compared through realpath: git answers --absolute-git-dir already resolved
163
+ // but --git-common-dir relative to cwd, so on any platform where the working
164
+ // directory sits behind a symlink (macOS /var -> /private/var, for one) a
165
+ // plain string compare reports every ordinary repository as a worktree.
166
+ function isLinkedWorktree(gitDir, commonDir) {
167
+ if (!gitDir || !commonDir) return false;
168
+ return realGitPath(gitDir) !== realGitPath(commonDir);
169
+ }
170
+
171
+ function realGitPath(value) {
172
+ try {
173
+ return fs.realpathSync(value);
174
+ } catch (e) {
175
+ return value;
176
+ }
177
+ }
178
+
160
179
  function parseWorktrees(raw) {
161
180
  var blocks = String(raw || "").trim().split(/\n\n+/);
162
181
  var result = [];
@@ -219,7 +238,7 @@ function getStatus(cwd, osUserInfo) {
219
238
  origin: origin,
220
239
  gitDir: gitDir,
221
240
  commonDir: commonDir,
222
- isWorktree: !!(gitDir && commonDir && gitDir !== commonDir),
241
+ isWorktree: isLinkedWorktree(gitDir, commonDir),
223
242
  mainWorktree: worktrees.length > 0 ? worktrees[0].path : root,
224
243
  worktrees: worktrees,
225
244
  files: parsed.files,
@@ -227,6 +246,78 @@ function getStatus(cwd, osUserInfo) {
227
246
  };
228
247
  }
229
248
 
249
+ // A bounded, credential-free remote label. The raw remote URL can carry an
250
+ // embedded token, so the userinfo segment is dropped before the label is
251
+ // built rather than after.
252
+ function shortRemoteLabel(origin) {
253
+ if (!origin) return null;
254
+ var label = String(origin).trim()
255
+ .replace(/^git@([^:]+):/, "$1/")
256
+ .replace(/^[a-z][a-z0-9+.-]*:\/\//i, "")
257
+ .replace(/^[^@/]*@/, "")
258
+ .replace(/\.git$/, "")
259
+ .replace(/\/+$/, "");
260
+ if (label.length > MAX_REMOTE_LABEL) label = label.slice(0, MAX_REMOTE_LABEL - 1) + "…";
261
+ return label || null;
262
+ }
263
+
264
+ function countChangedFiles(files) {
265
+ var counts = { changed: files.length, staged: 0, unstaged: 0, untracked: 0, conflicted: 0 };
266
+ for (var i = 0; i < files.length; i++) {
267
+ var file = files[i];
268
+ if (file.conflicted) counts.conflicted++;
269
+ if (file.untracked) counts.untracked++;
270
+ if (file.staged) counts.staged++;
271
+ if (file.unstaged && !file.untracked) counts.unstaged++;
272
+ }
273
+ return counts;
274
+ }
275
+
276
+ // Cheap always-visible repository placard data. Deliberately narrower than
277
+ // getStatus: three git invocations instead of six, no per-file payload, no
278
+ // session attribution, and no filesystem path in the result. Identity is
279
+ // derived here so the client never resolves or renders a repository path.
280
+ function getSummary(cwd, osUserInfo) {
281
+ var probe;
282
+ try {
283
+ probe = runGitSync(cwd, [
284
+ "rev-parse", "--is-inside-work-tree", "--show-toplevel",
285
+ "--absolute-git-dir", "--git-common-dir",
286
+ ], null, osUserInfo);
287
+ } catch (e) {
288
+ return { isRepository: false };
289
+ }
290
+ var lines = String(probe || "").trim().split("\n");
291
+ if (String(lines[0] || "").trim() !== "true" || !lines[1]) return { isRepository: false };
292
+
293
+ var root = String(lines[1]).trim();
294
+ var gitDir = lines[2] ? normalizeGitPath(cwd, String(lines[2]).trim()) : null;
295
+ var commonDir = lines[3] ? normalizeGitPath(cwd, String(lines[3]).trim()) : null;
296
+ var parsed = parsePorcelainV2(runGitSync(root, ["status", "--porcelain=v2", "--branch", "-z"], null, osUserInfo));
297
+ var origin = null;
298
+ try { origin = runGitSync(cwd, ["remote", "get-url", "origin"], null, osUserInfo).trim() || null; } catch (e) {}
299
+
300
+ var detached = parsed.branch === "(detached)";
301
+ var counts = countChangedFiles(parsed.files);
302
+ return {
303
+ isRepository: true,
304
+ name: path.basename(root),
305
+ branch: detached ? null : parsed.branch,
306
+ detached: detached,
307
+ shortOid: parsed.oid && parsed.oid !== "(initial)" ? String(parsed.oid).slice(0, 8) : null,
308
+ isWorktree: isLinkedWorktree(gitDir, commonDir),
309
+ hasUpstream: !!parsed.upstream,
310
+ ahead: parsed.ahead,
311
+ behind: parsed.behind,
312
+ changed: counts.changed,
313
+ staged: counts.staged,
314
+ unstaged: counts.unstaged,
315
+ untracked: counts.untracked,
316
+ conflicted: counts.conflicted,
317
+ remote: shortRemoteLabel(origin),
318
+ };
319
+ }
320
+
230
321
  function resolveChangedPaths(status, requestedPaths) {
231
322
  if (!Array.isArray(requestedPaths) || requestedPaths.length === 0 || requestedPaths.length > 200) {
232
323
  throw new Error("Select at least one changed file");
@@ -363,8 +454,10 @@ module.exports = {
363
454
  getFileAtCommit: getFileAtCommit,
364
455
  getFileDiff: getFileDiff,
365
456
  getStatus: getStatus,
457
+ getSummary: getSummary,
366
458
  parsePorcelainV2: parsePorcelainV2,
367
459
  parseWorktrees: parseWorktrees,
368
460
  readWorkingTreeFile: readWorkingTreeFile,
369
461
  runAction: runAction,
462
+ shortRemoteLabel: shortRemoteLabel,
370
463
  };