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
package/lib/notes.js CHANGED
@@ -3,6 +3,7 @@ var path = require("path");
3
3
  var crypto = require("crypto");
4
4
  var config = require("./config");
5
5
  var utils = require("./utils");
6
+ var lifecycle = require("./notes-lifecycle");
6
7
 
7
8
  function createNotesManager(opts) {
8
9
  var cwd = opts.cwd;
@@ -19,11 +20,14 @@ function createNotesManager(opts) {
19
20
  return "n_" + Date.now() + "_" + crypto.randomBytes(3).toString("hex");
20
21
  }
21
22
 
23
+ // Legacy files are normalized on load, never on read. Nothing is rewritten
24
+ // just because a note was looked at; the projected fields reach disk the next
25
+ // time some other write happens.
22
26
  function loadFromDisk() {
23
27
  try {
24
28
  var data = fs.readFileSync(notesFile, "utf8");
25
29
  var parsed = JSON.parse(data);
26
- return parsed.notes || [];
30
+ return lifecycle.normalizeAll(parsed.notes || []).notes;
27
31
  } catch (e) {
28
32
  return [];
29
33
  }
@@ -56,6 +60,10 @@ function createNotesManager(opts) {
56
60
  color: data.color || "purple",
57
61
  opacity: typeof data.opacity === "number" ? data.opacity : 0.64,
58
62
  minimized: false,
63
+ // A new note is always an open attention item.
64
+ state: lifecycle.STATE_OPEN,
65
+ closedAt: null,
66
+ closedBy: null,
59
67
  zIndex: notes.length + 1,
60
68
  createdAt: now,
61
69
  updatedAt: now,
@@ -74,13 +82,20 @@ function createNotesManager(opts) {
74
82
  function update(id, changes) {
75
83
  for (var i = 0; i < notes.length; i++) {
76
84
  if (notes[i].id === id) {
77
- var allowed = ["text", "x", "y", "w", "h", "color", "minimized", "hidden", "zIndex", "opacity"];
85
+ var allowed = ["text", "x", "y", "w", "h", "color", "minimized", "zIndex", "opacity"];
78
86
  for (var j = 0; j < allowed.length; j++) {
79
87
  var key = allowed[j];
80
88
  if (changes[key] !== undefined) {
81
89
  notes[i][key] = changes[key];
82
90
  }
83
91
  }
92
+ // `hidden` is the legacy spelling of the lifecycle. Route it through the
93
+ // transitions so state, hidden, and the close provenance can never
94
+ // disagree, and so an old client cannot half-apply a close.
95
+ if (changes.hidden !== undefined) {
96
+ if (changes.hidden === true) lifecycle.applyClose(notes[i], changes.actor || null, Date.now());
97
+ else lifecycle.applyReopen(notes[i]);
98
+ }
84
99
  notes[i].updatedAt = Date.now();
85
100
  saveToDisk();
86
101
  return notes[i];
@@ -89,6 +104,44 @@ function createNotesManager(opts) {
89
104
  return null;
90
105
  }
91
106
 
107
+ // Close a note. This is what completion means: the record stays and only its
108
+ // place on the active canvas changes. Idempotent.
109
+ function close(id, actor) {
110
+ for (var i = 0; i < notes.length; i++) {
111
+ if (notes[i].id !== id) continue;
112
+ if (lifecycle.applyClose(notes[i], actor || null, Date.now())) {
113
+ notes[i].updatedAt = Date.now();
114
+ saveToDisk();
115
+ }
116
+ return notes[i];
117
+ }
118
+ return null;
119
+ }
120
+
121
+ // Reopen a closed note. Idempotent.
122
+ function reopen(id) {
123
+ for (var i = 0; i < notes.length; i++) {
124
+ if (notes[i].id !== id) continue;
125
+ if (lifecycle.applyReopen(notes[i])) {
126
+ notes[i].updatedAt = Date.now();
127
+ saveToDisk();
128
+ }
129
+ return notes[i];
130
+ }
131
+ return null;
132
+ }
133
+
134
+ function openList() {
135
+ return lifecycle.openNotes(notes);
136
+ }
137
+
138
+ function closedList() {
139
+ return lifecycle.closedNotes(notes);
140
+ }
141
+
142
+ // Permanent deletion. Deliberately not reachable from any WebSocket message,
143
+ // MCP tool, or UI control: completion closes, it never erases. Retained only
144
+ // for maintenance callers that must truly discard a record.
92
145
  function remove(id) {
93
146
  for (var i = 0; i < notes.length; i++) {
94
147
  if (notes[i].id === id) {
@@ -123,7 +176,7 @@ function createNotesManager(opts) {
123
176
  function getActiveNotesText() {
124
177
  var active = [];
125
178
  for (var i = 0; i < notes.length; i++) {
126
- if (!notes[i].hidden && notes[i].text) active.push(notes[i]);
179
+ if (lifecycle.isOpen(notes[i]) && notes[i].text) active.push(notes[i]);
127
180
  }
128
181
  if (active.length === 0) return "";
129
182
  var lines = [];
@@ -137,8 +190,12 @@ function createNotesManager(opts) {
137
190
 
138
191
  return {
139
192
  list: list,
193
+ openList: openList,
194
+ closedList: closedList,
140
195
  create: create,
141
196
  update: update,
197
+ close: close,
198
+ reopen: reopen,
142
199
  remove: remove,
143
200
  bringToFront: bringToFront,
144
201
  getActiveNotesText: getActiveNotesText,
@@ -78,6 +78,7 @@ function attachConnection(ctx) {
78
78
  var _mcp = ctx._mcp;
79
79
  var _notifications = ctx._notifications;
80
80
  var _splitGroups = ctx._splitGroups;
81
+ var _vendorLogin = ctx._vendorLogin;
81
82
  var hydrateImageRefs = ctx.hydrateImageRefs;
82
83
  var resolveSessionForView = ctx.resolveSessionForView;
83
84
  var broadcastClientCount = ctx.broadcastClientCount;
@@ -191,6 +192,9 @@ function attachConnection(ctx) {
191
192
  sendTo(ws, { type: "last_vendor", vendor: sm.lastVendor || "" });
192
193
  sendTo(ws, Object.assign({ type: "codex_config" }, getCodexConfig(sm)));
193
194
  sendTo(ws, { type: "term_list", terminals: tm.list() });
195
+ // Any login flow already running in this project, so a reconnecting client
196
+ // re-attaches to the existing login terminal instead of starting another.
197
+ if (_vendorLogin) _vendorLogin.sendStateTo(ws);
194
198
  // Context sources sent after session is resolved (per-session storage)
195
199
  // Send email accounts list for context sources picker
196
200
  var emailUserId = (wsUser && wsUser.id) || "default";
@@ -346,9 +350,22 @@ function attachConnection(ctx) {
346
350
  broadcastPresence();
347
351
  }
348
352
 
353
+ // Keep-alive for the project socket, mirroring the `/ws` global handler.
354
+ //
355
+ // A reverse proxy resets its upstream read timeout on data flowing back from
356
+ // the server, so a client ping is only useful if the server answers it. This
357
+ // reads nothing, mutates nothing, and reveals nothing about the connection:
358
+ // it is a fixed reply on a fixed message type.
359
+ function handleKeepaliveMessage(ws, msg) {
360
+ if (!msg || typeof msg !== "object" || msg.type !== "ping") return false;
361
+ sendTo(ws, { type: "pong" });
362
+ return true;
363
+ }
364
+
349
365
  return {
350
366
  handleConnection: handleConnection,
351
367
  handleDisconnection: handleDisconnection,
368
+ handleKeepaliveMessage: handleKeepaliveMessage,
352
369
  };
353
370
  }
354
371
 
@@ -730,7 +730,7 @@ function attachHTTP(ctx) {
730
730
  }
731
731
 
732
732
  // Git panel status and actions
733
- var isGitPanelRequest = urlPath === "/api/git/status" ||
733
+ var isGitPanelRequest = urlPath === "/api/git/status" || urlPath === "/api/git/summary" ||
734
734
  urlPath === "/api/git/action" || urlPath.indexOf("/api/git/file-diff?") === 0 ||
735
735
  urlPath.indexOf("/api/git/session-diff?") === 0;
736
736
  if (isGitPanelRequest && usersModule.isMultiUser()) {
@@ -768,6 +768,20 @@ function attachHTTP(ctx) {
768
768
  return true;
769
769
  }
770
770
 
771
+ // Compact repository placard data for the always-visible sidebar block.
772
+ // Same permission gate as the panel, but no file payload and no path.
773
+ if (req.method === "GET" && urlPath === "/api/git/summary") {
774
+ try {
775
+ var gitSummary = gitCli.getSummary(cwd, getOsUserInfoForReq(req));
776
+ res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-store" });
777
+ res.end(JSON.stringify(gitSummary));
778
+ } catch (e) {
779
+ res.writeHead(500, { "Content-Type": "application/json" });
780
+ res.end(JSON.stringify({ error: e.message || "Unable to read Git summary" }));
781
+ }
782
+ return true;
783
+ }
784
+
771
785
  if (req.method === "GET" && urlPath.indexOf("/api/git/file-diff?") === 0) {
772
786
  var gitDiffParams = new URLSearchParams(urlPath.slice(urlPath.indexOf("?")));
773
787
  var gitDiffPath = gitDiffParams.get("path");
@@ -1,5 +1,6 @@
1
1
  var fs = require("fs");
2
2
  var path = require("path");
3
+ var mateSync = require("./mate-knowledge-sync");
3
4
 
4
5
  /**
5
6
  * Attach knowledge file management to a project context.
@@ -75,6 +76,23 @@ function attachKnowledge(ctx) {
75
76
  return files;
76
77
  }
77
78
 
79
+ // Mirror a Mate-owned knowledge file into the Clay-wide Knowledge backend
80
+ // after the legacy write has already succeeded. The legacy file stays
81
+ // authoritative; this only keeps the new scope from going stale.
82
+ function mirrorMateSource(ws, fileName) {
83
+ if (!isMate) return;
84
+ var user = (ws && ws._clayUser) || null;
85
+ mateSync.syncQuietly({
86
+ mateDir: cwd,
87
+ fileName: fileName,
88
+ actor: {
89
+ type: user ? "user" : "system",
90
+ userId: user ? user.id : null,
91
+ displayName: user ? (user.displayName || user.username) : null,
92
+ },
93
+ });
94
+ }
95
+
78
96
  function handleKnowledgeMessage(ws, msg) {
79
97
  if (msg.type === "knowledge_list") {
80
98
  sendTo(ws, { type: "knowledge_list", files: listKnowledgeFiles() });
@@ -111,6 +129,7 @@ function attachKnowledge(ctx) {
111
129
  var knowledgeDir = path.join(cwd, "knowledge");
112
130
  fs.mkdirSync(knowledgeDir, { recursive: true });
113
131
  fs.writeFileSync(path.join(knowledgeDir, safeName), msg.content);
132
+ mirrorMateSource(ws, safeName);
114
133
  sendTo(ws, { type: "knowledge_saved", name: safeName });
115
134
  sendTo(ws, { type: "knowledge_list", files: listKnowledgeFilesBasic() });
116
135
  return true;
@@ -121,6 +140,9 @@ function attachKnowledge(ctx) {
121
140
  var safeName = path.basename(msg.name);
122
141
  var filePath = path.join(cwd, "knowledge", safeName);
123
142
  try { fs.unlinkSync(filePath); } catch (e) {}
143
+ // Reconciling after the unlink appends a tombstone, so a deleted file
144
+ // does not stay live in the new backend.
145
+ mirrorMateSource(ws, safeName);
124
146
  sendTo(ws, { type: "knowledge_deleted", name: safeName });
125
147
  sendTo(ws, { type: "knowledge_list", files: listKnowledgeFilesBasic() });
126
148
  return true;
@@ -0,0 +1,137 @@
1
+ // Comment and review projection for Project Logs.
2
+ //
3
+ // Split from project-logs-store.js so the store stays inside the module size
4
+ // limit.
5
+ //
6
+ // A user comment is a revision proposal, never an automatic canonical
7
+ // mutation. It starts `pending` and stays that way until the project's own
8
+ // Driver session reviews it. Reviews are append-only records that reference a
9
+ // comment id; nothing is ever edited in place, and a comment written before
10
+ // reviews existed simply projects as `pending`.
11
+
12
+ var STATUS_PENDING = "pending";
13
+ var STATUS_CLARIFICATION = "clarification-needed";
14
+ var STATUS_INCORPORATED = "incorporated";
15
+ var STATUS_DECLINED = "declined";
16
+
17
+ var STATUSES = [STATUS_PENDING, STATUS_CLARIFICATION, STATUS_INCORPORATED, STATUS_DECLINED];
18
+
19
+ // Actions the Driver may take. `incorporate` is the only one that produces a
20
+ // canonical revision.
21
+ var ACTIONS = ["incorporate", "clarify", "decline"];
22
+
23
+ var ACTION_STATUS = {
24
+ incorporate: STATUS_INCORPORATED,
25
+ clarify: STATUS_CLARIFICATION,
26
+ decline: STATUS_DECLINED,
27
+ };
28
+
29
+ // Waiting on the Driver. Only an unreviewed comment qualifies.
30
+ //
31
+ // A clarification is deliberately NOT Driver work: the Driver has already
32
+ // answered, and the ball is with the user. Because a comment is settled once
33
+ // and never re-reviewed, counting a clarification as pending would leave it in
34
+ // the queue forever with no way out. The user's reply is a new comment, which
35
+ // is pending in its own right and can be incorporated or declined.
36
+ function isPendingReview(status) {
37
+ return status === STATUS_PENDING;
38
+ }
39
+
40
+ // Answered by the Driver and now waiting on the person who commented. Shown
41
+ // permanently on the original comment; never counted as Driver work.
42
+ function isAwaitingUser(status) {
43
+ return status === STATUS_CLARIFICATION;
44
+ }
45
+
46
+ function isAction(value) {
47
+ return ACTIONS.indexOf(value) !== -1;
48
+ }
49
+
50
+ // Project the comment thread of one entry, folding review records onto the
51
+ // comments they resolve. `revisionForRecordId` maps an incorporating review
52
+ // record id to the canonical revision number it produced, so an incorporated
53
+ // comment can point at its result.
54
+ function comments(chain, options) {
55
+ var settings = options || {};
56
+ var maxComments = settings.maxComments || 200;
57
+ var revisionForRecordId = settings.revisionForRecordId || {};
58
+ var byId = {};
59
+ var order = [];
60
+
61
+ var i;
62
+ for (i = 0; i < chain.length; i++) {
63
+ var record = chain[i];
64
+ if (record.op !== "comment") continue;
65
+ if (order.length >= maxComments) break;
66
+ var comment = {
67
+ id: record.id,
68
+ body: record.body || "",
69
+ at: record.at || 0,
70
+ author: record.author || null,
71
+ // A comment with no review yet is pending, which is also how every
72
+ // comment written before reviews existed projects.
73
+ status: STATUS_PENDING,
74
+ review: null,
75
+ };
76
+ byId[record.id] = comment;
77
+ order.push(comment);
78
+ }
79
+
80
+ for (i = 0; i < chain.length; i++) {
81
+ var review = chain[i];
82
+ if (review.op !== "review") continue;
83
+ var target = byId[review.commentId];
84
+ if (!target) continue;
85
+ if (!isAction(review.action)) continue;
86
+ // Append-only: the first review of a comment settles it. A later review of
87
+ // the same comment is ignored rather than silently overwriting the record
88
+ // of what was decided.
89
+ if (target.review) continue;
90
+ target.status = ACTION_STATUS[review.action];
91
+ target.review = {
92
+ action: review.action,
93
+ response: typeof review.response === "string" ? review.response : "",
94
+ at: review.at || 0,
95
+ reviewer: review.author || null,
96
+ revision: review.action === "incorporate"
97
+ ? (revisionForRecordId[review.id] || null)
98
+ : null,
99
+ };
100
+ }
101
+
102
+ return order;
103
+ }
104
+
105
+ // How many comments are still waiting on the Driver. Used for the ledger row
106
+ // count and for the system-prompt signal, neither of which carries any body.
107
+ function pendingReviewCount(list) {
108
+ var count = 0;
109
+ for (var i = 0; i < (list || []).length; i++) {
110
+ if (isPendingReview(list[i].status)) count++;
111
+ }
112
+ return count;
113
+ }
114
+
115
+ function pendingReviewComments(list) {
116
+ var out = [];
117
+ for (var i = 0; i < (list || []).length; i++) {
118
+ if (isPendingReview(list[i].status)) out.push(list[i]);
119
+ }
120
+ return out;
121
+ }
122
+
123
+ module.exports = {
124
+ STATUS_PENDING: STATUS_PENDING,
125
+ STATUS_CLARIFICATION: STATUS_CLARIFICATION,
126
+ STATUS_INCORPORATED: STATUS_INCORPORATED,
127
+ STATUS_DECLINED: STATUS_DECLINED,
128
+ STATUSES: STATUSES,
129
+ ACTIONS: ACTIONS,
130
+ ACTION_STATUS: ACTION_STATUS,
131
+ isPendingReview: isPendingReview,
132
+ isAwaitingUser: isAwaitingUser,
133
+ isAction: isAction,
134
+ comments: comments,
135
+ pendingReviewCount: pendingReviewCount,
136
+ pendingReviewComments: pendingReviewComments,
137
+ };