clay-server 5.2.0-beta.8 → 5.2.0

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 (143) hide show
  1. package/README.md +0 -4
  2. package/lib/clay-product-control-tools.js +1 -0
  3. package/lib/clay-product-control.js +14 -11
  4. package/lib/clay-vendor-usage.js +44 -0
  5. package/lib/github-star-count.js +147 -0
  6. package/lib/notes-checklist.js +43 -1
  7. package/lib/notes.js +2 -1
  8. package/lib/pane-pending-interactions.js +20 -0
  9. package/lib/pane-session-activation.js +114 -0
  10. package/lib/pane-session-transport.js +26 -0
  11. package/lib/project-session-notes.js +33 -8
  12. package/lib/project-sessions.js +78 -29
  13. package/lib/project-start-protocol.js +29 -0
  14. package/lib/project-vendor-login.js +2 -0
  15. package/lib/project.js +16 -4
  16. package/lib/public/app.js +41 -18
  17. package/lib/public/css/contextual-clay.css +2 -2
  18. package/lib/public/css/filebrowser.css +1 -1
  19. package/lib/public/css/input.css +6 -0
  20. package/lib/public/css/mate-loading-scene.css +21 -0
  21. package/lib/public/css/overlays.css +64 -256
  22. package/lib/public/css/project-mate-creation.css +5 -6
  23. package/lib/public/css/project-start.css +108 -0
  24. package/lib/public/css/projects-hub.css +53 -14
  25. package/lib/public/css/settings-shell.css +103 -0
  26. package/lib/public/css/sidebar.css +7 -0
  27. package/lib/public/css/sticky-notes.css +37 -11
  28. package/lib/public/css/title-bar.css +83 -0
  29. package/lib/public/index.html +50 -63
  30. package/lib/public/modules/account-transport-settings.js +8 -2
  31. package/lib/public/modules/admin-settings-email.js +7 -2
  32. package/lib/public/modules/admin-settings-invitations.js +2 -2
  33. package/lib/public/modules/admin-settings-projects.js +2 -2
  34. package/lib/public/modules/admin-settings-ui.js +20 -4
  35. package/lib/public/modules/admin-settings-users.js +4 -4
  36. package/lib/public/modules/app-connection.js +3 -0
  37. package/lib/public/modules/app-home-hub.js +2 -1
  38. package/lib/public/modules/app-message-router.js +13 -0
  39. package/lib/public/modules/app-messages.js +35 -7
  40. package/lib/public/modules/app-misc.js +3 -1
  41. package/lib/public/modules/app-projects.js +6 -407
  42. package/lib/public/modules/app-rate-limit.js +12 -1
  43. package/lib/public/modules/clay-owner-navigation.js +0 -3
  44. package/lib/public/modules/clay-studio-render.js +3 -1
  45. package/lib/public/modules/clay-tool-presentation.js +1 -0
  46. package/lib/public/modules/command-palette.js +52 -14
  47. package/lib/public/modules/contextual-clay-workspace.js +14 -2
  48. package/lib/public/modules/contextual-clay.js +19 -4
  49. package/lib/public/modules/contextual-mate-context.js +6 -4
  50. package/lib/public/modules/elicitation-form.js +5 -0
  51. package/lib/public/modules/github-star.js +66 -0
  52. package/lib/public/modules/history-replay-batch.js +8 -0
  53. package/lib/public/modules/home-surface-boot.js +0 -1
  54. package/lib/public/modules/input.js +55 -11
  55. package/lib/public/modules/mate-loading-scene.js +17 -0
  56. package/lib/public/modules/message-delivery.js +10 -2
  57. package/lib/public/modules/model-picker.js +2 -0
  58. package/lib/public/modules/pane-activation.js +214 -0
  59. package/lib/public/modules/pane-appearance-bridge.js +59 -0
  60. package/lib/public/modules/pane-binding.js +82 -0
  61. package/lib/public/modules/pane-bridge.js +5 -2
  62. package/lib/public/modules/pane-browser-bridge.js +5 -3
  63. package/lib/public/modules/pane-file-bridge.js +4 -0
  64. package/lib/public/modules/pane-issue-bridge.js +4 -1
  65. package/lib/public/modules/profile.js +11 -3
  66. package/lib/public/modules/project-mate-creation-workspace.js +13 -4
  67. package/lib/public/modules/project-session-visibility-settings.js +19 -16
  68. package/lib/public/modules/project-settings-environment.js +12 -1
  69. package/lib/public/modules/project-settings.js +119 -129
  70. package/lib/public/modules/project-start-dialog.js +130 -0
  71. package/lib/public/modules/project-start-folder-state.js +78 -0
  72. package/lib/public/modules/project-start-folder-view.js +190 -0
  73. package/lib/public/modules/project-start-forms.js +66 -0
  74. package/lib/public/modules/project-start-path.js +60 -0
  75. package/lib/public/modules/project-start-state.js +54 -0
  76. package/lib/public/modules/project-start-submit.js +67 -0
  77. package/lib/public/modules/projects-hub-view.js +32 -44
  78. package/lib/public/modules/projects-hub.js +39 -28
  79. package/lib/public/modules/rewind.js +5 -0
  80. package/lib/public/modules/server-settings.js +36 -91
  81. package/lib/public/modules/session-actions.js +5 -2
  82. package/lib/public/modules/session-context-menu.js +5 -0
  83. package/lib/public/modules/session-rich-drafts.js +21 -0
  84. package/lib/public/modules/session-switch-defer.js +67 -0
  85. package/lib/public/modules/settings-project-target.js +56 -0
  86. package/lib/public/modules/settings-select.js +1 -1
  87. package/lib/public/modules/settings-shell-registry.js +88 -0
  88. package/lib/public/modules/settings-shell-view.js +104 -0
  89. package/lib/public/modules/settings-shell.js +342 -0
  90. package/lib/public/modules/sidebar-mobile.js +8 -30
  91. package/lib/public/modules/sidebar-projects.js +2 -1
  92. package/lib/public/modules/split-group-restore.js +68 -0
  93. package/lib/public/modules/split-pair-ui.js +1 -0
  94. package/lib/public/modules/split-pane-reconciler.js +15 -0
  95. package/lib/public/modules/split-pane-renderer.js +87 -6
  96. package/lib/public/modules/split-session-boundary.js +1 -1
  97. package/lib/public/modules/split-view.js +51 -52
  98. package/lib/public/modules/sticky-notes-checklist.js +51 -6
  99. package/lib/public/modules/stt-controller.js +4 -1
  100. package/lib/public/modules/stt-recording.js +1 -1
  101. package/lib/public/modules/theme.js +31 -3
  102. package/lib/public/modules/tools.js +18 -2
  103. package/lib/public/modules/user-settings-integrations.js +2 -1
  104. package/lib/public/modules/user-settings-layout.js +12 -34
  105. package/lib/public/modules/user-settings-profile.js +1 -1
  106. package/lib/public/modules/user-settings.js +18 -85
  107. package/lib/public/modules/vendor-usage-format.js +93 -0
  108. package/lib/public/modules/vendor-usage-list.js +45 -0
  109. package/lib/public/modules/vendor-usage-model.js +215 -0
  110. package/lib/public/modules/vendor-usage-overview.js +323 -0
  111. package/lib/public/style.css +10 -7
  112. package/lib/public/welcome-art/conversation-desk.png +0 -0
  113. package/lib/public/welcome-art/mate-clay.png +0 -0
  114. package/lib/public/welcome-art/project-clay.png +0 -0
  115. package/lib/sdk-bridge.js +6 -1
  116. package/lib/sdk-message-processor.js +9 -1
  117. package/lib/sdk-quota-snapshot.js +20 -2
  118. package/lib/server-github-stars.js +39 -0
  119. package/lib/server-global-ws.js +21 -17
  120. package/lib/server.js +11 -0
  121. package/lib/session-notes-mcp-server.js +26 -4
  122. package/lib/session-notes-writes.js +88 -0
  123. package/lib/session-visibility.js +20 -8
  124. package/lib/sessions.js +27 -10
  125. package/lib/vendor-usage-acp-readers.js +97 -0
  126. package/lib/vendor-usage-capabilities.js +88 -0
  127. package/lib/vendor-usage-http-readers.js +214 -0
  128. package/lib/vendor-usage-metrics.js +47 -0
  129. package/lib/vendor-usage-native-readers.js +106 -0
  130. package/lib/vendor-usage-overview.js +406 -0
  131. package/lib/vendor-usage-service.js +281 -0
  132. package/lib/ws-schema.js +4 -2
  133. package/lib/yoke/adapters/claude.js +12 -0
  134. package/lib/yoke/adapters/codex-query-contract.js +1 -0
  135. package/lib/yoke/adapters/codex.js +45 -12
  136. package/lib/yoke/claude-hook-installer.js +1 -0
  137. package/lib/yoke/codex-dynamic-tool-names.js +80 -0
  138. package/lib/yoke/codex-model-catalog.js +37 -3
  139. package/lib/yoke/muse-services.js +16 -3
  140. package/lib/yoke/providers/claude-model-descriptions.js +22 -2
  141. package/lib/yoke/vendor-registry.js +6 -2
  142. package/package.json +5 -3
  143. package/lib/public/modules/server-settings-navigation.js +0 -39
@@ -46,6 +46,10 @@ function attachMessageProcessor(ctx) {
46
46
  sm.sendAndRecord(session, obj, clientObj);
47
47
  }
48
48
 
49
+ function observeQuota(session, message, observedAtMs) {
50
+ if (typeof sm.notifyQuotaObserved === "function") sm.notifyQuotaObserved(session, message, observedAtMs);
51
+ }
52
+
49
53
  function recordTurnActivity(session, activity) {
50
54
  // An authoritative input card owns progress until its request settles.
51
55
  var awaitingInput = ["pendingPermissions", "pendingAskUser", "pendingElicitations", "pendingUserDialogs"].some(function (key) {
@@ -892,7 +896,7 @@ function attachMessageProcessor(ctx) {
892
896
  } else if (parsed.yokeType === "rate_limit") {
893
897
  var info = parsed.rateLimitInfo;
894
898
  if (info.source === "account_snapshot") {
895
- quotaSnapshots.recordQuotaSnapshot(session, info, sendAndRecord);
899
+ quotaSnapshots.recordQuotaSnapshot(session, info, sendAndRecord, observeQuota);
896
900
  return;
897
901
  }
898
902
  console.log("[sdk-bridge] rate_limit_event for session " + session.localId + ": status=" + info.status + " resetsAt=" + info.resetsAt + " isUsingOverage=" + info.isUsingOverage + " isProcessing=" + session.isProcessing);
@@ -907,6 +911,7 @@ function attachMessageProcessor(ctx) {
907
911
  resetsAt: info.resetsAt * 1000,
908
912
  status: info.status,
909
913
  });
914
+ observeQuota(session, { type: "rate_limit_usage", vendor: session.vendor || "claude", rateLimitType: info.rateLimitType, resetsAt: info.resetsAt * 1000, status: info.status }, Date.now());
910
915
  }
911
916
 
912
917
  // Warning/rejection handling (existing behavior)
@@ -925,6 +930,9 @@ function attachMessageProcessor(ctx) {
925
930
  overageResetsAt: info.overageResetsAt ? info.overageResetsAt * 1000 : null,
926
931
  overageDisabledReason: info.overageDisabledReason || null,
927
932
  });
933
+ if (info.rateLimitType && info.utilization) {
934
+ observeQuota(session, { type: "rate_limit", vendor: session.vendor || "claude", rateLimitType: info.rateLimitType, utilization: info.utilization }, Date.now());
935
+ }
928
936
  // Track rejection for auto-continue / scheduled message support
929
937
  if (info.status === "rejected" && info.resetsAt) {
930
938
  session.rateLimitResetsAt = info.resetsAt * 1000;
@@ -1,4 +1,4 @@
1
- function recordQuotaSnapshot(session, info, sendAndRecord) {
1
+ function recordQuotaSnapshot(session, info, sendAndRecord, observe) {
2
2
  var message = {
3
3
  type: "rate_limit_usage",
4
4
  sessionId: session.localId,
@@ -10,6 +10,9 @@ function recordQuotaSnapshot(session, info, sendAndRecord) {
10
10
  }),
11
11
  };
12
12
  if (Number.isFinite(info.observedAtMs)) message.observedAtMs = info.observedAtMs;
13
+ // The overview records every live observation, including an unchanged one:
14
+ // deduplication below only avoids re-recording identical history.
15
+ if (typeof observe === "function") observe(session, message, Number.isFinite(info.observedAtMs) ? info.observedAtMs : Date.now());
13
16
  var snapshots = session._quotaSnapshots || (session._quotaSnapshots = Object.create(null));
14
17
  var key = JSON.stringify([session.ownerId, session.vendor, info.limitId]);
15
18
  var serialized = JSON.stringify(message);
@@ -18,7 +21,22 @@ function recordQuotaSnapshot(session, info, sendAndRecord) {
18
21
  sendAndRecord(session, message);
19
22
  }
20
23
 
24
+ // Observation listeners feed the owner-scoped vendor overview. They never
25
+ // influence rejection handling or scheduling.
26
+ function attachQuotaObservers(sm) {
27
+ // Idempotent: the processor and the overview may attach in either order.
28
+ if (typeof sm.addOnQuotaObserved === "function") return;
29
+ var listeners = [];
30
+ sm.addOnQuotaObserved = function (fn) { if (typeof fn === "function") listeners.push(fn); };
31
+ sm.notifyQuotaObserved = function (session, message, observedAtMs) {
32
+ for (var i = 0; i < listeners.length; i++) {
33
+ try { listeners[i](session, message, observedAtMs); } catch (e) { /* the overview is optional */ }
34
+ }
35
+ };
36
+ }
37
+
21
38
  function attachQuotaSnapshots(sm) {
39
+ attachQuotaObservers(sm);
22
40
  if (typeof sm.addOnSessionViewed !== "function") return;
23
41
  sm.addOnSessionViewed(function (session, ws, send) {
24
42
  var seen = Object.create(null);
@@ -34,4 +52,4 @@ function attachQuotaSnapshots(sm) {
34
52
  });
35
53
  }
36
54
 
37
- module.exports = { recordQuotaSnapshot: recordQuotaSnapshot, attachQuotaSnapshots: attachQuotaSnapshots };
55
+ module.exports = { recordQuotaSnapshot: recordQuotaSnapshot, attachQuotaSnapshots: attachQuotaSnapshots, attachQuotaObservers: attachQuotaObservers };
@@ -0,0 +1,39 @@
1
+ // server-github-stars.js - GET /api/github-stars for the top-bar Star link.
2
+ //
3
+ // Read-only. Returns the cached public star count of the fixed Clay
4
+ // repository (null when not yet known). No request input reaches GitHub, and
5
+ // nothing about the caller is sent or stored.
6
+
7
+ var githubStarCount = require("./github-star-count");
8
+
9
+ function attachGithubStars(ctx) {
10
+ var cache = ctx.cache || githubStarCount.createGithubStarCount();
11
+
12
+ function json(res, status, body) {
13
+ res.writeHead(status, { "Content-Type": "application/json", "Cache-Control": "no-store" });
14
+ res.end(JSON.stringify(body));
15
+ }
16
+
17
+ function handleRequest(req, res, url) {
18
+ if (url !== "/api/github-stars") return false;
19
+ if (req.method !== "GET") {
20
+ res.writeHead(405, { "Content-Type": "application/json", "Allow": "GET" });
21
+ res.end(JSON.stringify({ error: "Method not allowed." }));
22
+ return true;
23
+ }
24
+ if (!ctx.isRequestAuthed(req)) {
25
+ json(res, 401, { error: "Sign in to continue." });
26
+ return true;
27
+ }
28
+ cache.get().then(function (snapshot) {
29
+ json(res, 200, snapshot);
30
+ }, function () {
31
+ json(res, 200, { repo: githubStarCount.REPO, stars: null, fetchedAt: null });
32
+ });
33
+ return true;
34
+ }
35
+
36
+ return { handleRequest: handleRequest };
37
+ }
38
+
39
+ module.exports = { attachGithubStars: attachGithubStars };
@@ -19,6 +19,9 @@
19
19
  var fs = require("fs");
20
20
  var path = require("path");
21
21
  var config = require("./config");
22
+ var startProtocol = require("./project-start-protocol");
23
+ var startReply = startProtocol.withRequestId;
24
+ var browseMeta = startProtocol.browseMeta;
22
25
 
23
26
  // Mirrored from lib/project.js — keep in sync if entries change there.
24
27
  var IGNORED_DIRS = new Set([
@@ -79,8 +82,9 @@ function attachGlobalWs(opts) {
79
82
  parentDir = path.dirname(absTarget);
80
83
  prefix = path.basename(absTarget).toLowerCase();
81
84
  }
82
- if (opts.canUsePath && !opts.canUsePath(ws._clayUser && ws._clayUser.id, parentDir)) {
83
- sendTo(ws, { type: "browse_dir_result", entries: [], error: "Directory access is not permitted" });
85
+ var canList = function (candidate) { return !opts.canUsePath || opts.canUsePath(ws._clayUser && ws._clayUser.id, candidate); };
86
+ if (!canList(parentDir)) {
87
+ sendTo(ws, startReply(msg, { type: "browse_dir_result", path: msg.path, entries: [], error: "Directory access is not permitted" }));
84
88
  return;
85
89
  }
86
90
  try {
@@ -96,9 +100,9 @@ function attachGlobalWs(opts) {
96
100
  dirEntries.push({ name: d.name, path: path.join(parentDir, d.name) });
97
101
  }
98
102
  dirEntries.sort(function (a, b) { return a.name.localeCompare(b.name); });
99
- sendTo(ws, { type: "browse_dir_result", path: msg.path, entries: dirEntries });
103
+ sendTo(ws, Object.assign(browseMeta(msg, parentDir, prefix, canList), { type: "browse_dir_result", path: msg.path, entries: dirEntries }));
100
104
  } catch (e) {
101
- sendTo(ws, { type: "browse_dir_result", path: msg.path, entries: [], error: e.message });
105
+ sendTo(ws, startReply(msg, { type: "browse_dir_result", path: msg.path, entries: [], error: e.message }));
102
106
  }
103
107
  return;
104
108
  }
@@ -108,24 +112,24 @@ function attachGlobalWs(opts) {
108
112
  var addPath = (msg.path || "").replace(/^~/, config.REAL_HOME);
109
113
  var addAbs = path.resolve(addPath);
110
114
  if (opts.canUsePath && !opts.canUsePath(ws._clayUser && ws._clayUser.id, addAbs)) {
111
- sendTo(ws, { type: "add_project_result", ok: false, error: "Directory access is not permitted" });
115
+ sendTo(ws, startReply(msg, { type: "add_project_result", ok: false, error: "Directory access is not permitted" }));
112
116
  return;
113
117
  }
114
118
  try {
115
119
  var addStat = fs.statSync(addAbs);
116
120
  if (!addStat.isDirectory()) {
117
- sendTo(ws, { type: "add_project_result", ok: false, error: "Not a directory" });
121
+ sendTo(ws, startReply(msg, { type: "add_project_result", ok: false, error: "Not a directory" }));
118
122
  return;
119
123
  }
120
124
  } catch (e) {
121
- sendTo(ws, { type: "add_project_result", ok: false, error: "Directory not found" });
125
+ sendTo(ws, startReply(msg, { type: "add_project_result", ok: false, error: "Directory not found" }));
122
126
  return;
123
127
  }
124
128
  if (typeof onAddProject === "function") {
125
129
  var result = onAddProject(addAbs, ws._clayUser);
126
- sendTo(ws, Object.assign({}, result, { type: "add_project_result" }));
130
+ sendTo(ws, startReply(msg, Object.assign({}, result, { type: "add_project_result" })));
127
131
  } else {
128
- sendTo(ws, { type: "add_project_result", ok: false, error: "Not supported" });
132
+ sendTo(ws, startReply(msg, { type: "add_project_result", ok: false, error: "Not supported" }));
129
133
  }
130
134
  return;
131
135
  }
@@ -135,7 +139,7 @@ function attachGlobalWs(opts) {
135
139
  if (ws._clayUser && usersModule) {
136
140
  var cpPerms = usersModule.getEffectivePermissions(ws._clayUser, osUsers);
137
141
  if (!cpPerms.createProject) {
138
- sendTo(ws, { type: "add_project_result", ok: false, error: "You do not have permission to create projects" });
142
+ sendTo(ws, startReply(msg, { type: "add_project_result", ok: false, error: "You do not have permission to create projects" }));
139
143
  return;
140
144
  }
141
145
  }
@@ -144,14 +148,14 @@ function attachGlobalWs(opts) {
144
148
  if (msg.type === "create_project") {
145
149
  var createName = (msg.name || "").trim();
146
150
  if (!createName || !/^[a-zA-Z0-9_-]+$/.test(createName)) {
147
- sendTo(ws, { type: "add_project_result", ok: false, error: "Invalid name. Use only letters, numbers, dashes, and underscores." });
151
+ sendTo(ws, startReply(msg, { type: "add_project_result", ok: false, error: "Invalid name. Use only letters, numbers, dashes, and underscores." }));
148
152
  return;
149
153
  }
150
154
  if (typeof onCreateProject === "function") {
151
155
  var createResult = onCreateProject(createName, ws._clayUser);
152
- sendTo(ws, Object.assign({}, createResult, { type: "add_project_result" }));
156
+ sendTo(ws, startReply(msg, Object.assign({}, createResult, { type: "add_project_result" })));
153
157
  } else {
154
- sendTo(ws, { type: "add_project_result", ok: false, error: "Not supported" });
158
+ sendTo(ws, startReply(msg, { type: "add_project_result", ok: false, error: "Not supported" }));
155
159
  }
156
160
  return;
157
161
  }
@@ -159,16 +163,16 @@ function attachGlobalWs(opts) {
159
163
  if (msg.type === "clone_project") {
160
164
  var cloneUrl = (msg.url || "").trim();
161
165
  if (!cloneUrl || (!/^https?:\/\//.test(cloneUrl) && !/^git@/.test(cloneUrl))) {
162
- sendTo(ws, { type: "add_project_result", ok: false, error: "Invalid URL. Use https:// or git@ format." });
166
+ sendTo(ws, startReply(msg, { type: "add_project_result", ok: false, error: "Invalid URL. Use https:// or git@ format." }));
163
167
  return;
164
168
  }
165
- sendTo(ws, { type: "clone_project_progress", status: "cloning" });
169
+ sendTo(ws, startReply(msg, { type: "clone_project_progress", status: "cloning" }));
166
170
  if (typeof onCloneProject === "function") {
167
171
  onCloneProject(cloneUrl, ws._clayUser, function (cloneResult) {
168
- sendTo(ws, { type: "add_project_result", ok: cloneResult.ok, slug: cloneResult.slug, error: cloneResult.error });
172
+ sendTo(ws, startReply(msg, { type: "add_project_result", ok: cloneResult.ok, slug: cloneResult.slug, error: cloneResult.error }));
169
173
  });
170
174
  } else {
171
- sendTo(ws, { type: "add_project_result", ok: false, error: "Not supported" });
175
+ sendTo(ws, startReply(msg, { type: "add_project_result", ok: false, error: "Not supported" }));
172
176
  }
173
177
  return;
174
178
  }
package/lib/server.js CHANGED
@@ -507,6 +507,8 @@ function createServer(opts) {
507
507
  var linear = require('./server-linear').attachLinear({ users: users, getMultiUserFromReq: getMultiUserFromReq,
508
508
  isRequestAuthed: isRequestAuthed, CONFIG_DIR: CONFIG_DIR });
509
509
 
510
+ var githubStars = require('./server-github-stars').attachGithubStars({ isRequestAuthed: isRequestAuthed });
511
+
510
512
  var speech = serverSpeech.attachSpeech({ users: users, getMultiUserFromReq: getMultiUserFromReq,
511
513
  isRequestAuthed: isRequestAuthed, CONFIG_DIR: CONFIG_DIR, getAllowedOrigins: getAllowedOrigins });
512
514
 
@@ -925,6 +927,7 @@ function createServer(opts) {
925
927
 
926
928
  if (linear.handleRequest(req, res, fullUrl)) return;
927
929
  if (speech.handleRequest(req, res, fullUrl)) return;
930
+ if (githubStars.handleRequest(req, res, fullUrl)) return;
928
931
  if (networkSettings.handleRequest(req, res, fullUrl)) return;
929
932
  if (storageSettings.handleRequest(req, res, fullUrl)) return;
930
933
  if (settings.handleRequest(req, res, fullUrl)) return;
@@ -967,6 +970,13 @@ function createServer(opts) {
967
970
  res.end(auth.getAuthPage());
968
971
  return;
969
972
  }
973
+ // The standalone project list was removed. A legacy /projects link resolves
974
+ // through the same server-backed workspace selection as the root path.
975
+ if (fullUrl !== "/") {
976
+ res.writeHead(302, { "Location": "/", "Cache-Control": "no-store" });
977
+ res.end();
978
+ return;
979
+ }
970
980
  if (projects.size > 0) {
971
981
  var targetSlug = null;
972
982
  var reqUser = users.isMultiUser() ? getMultiUserFromReq(req) : null;
@@ -1590,6 +1600,7 @@ function createServer(opts) {
1590
1600
  return ctx.getSchedules().length;
1591
1601
  },
1592
1602
  canAccessProjectSlug: canUserAccessSlug,
1603
+ onGetProjectAccess: onGetProjectAccess,
1593
1604
  canUsePath: canUsePath,
1594
1605
  onPresenceChange: broadcastPresenceChange,
1595
1606
  onProcessingChanged: broadcastProcessingChange,
@@ -1,6 +1,8 @@
1
1
  // Sticky-note memory tools for project sessions.
2
2
 
3
3
  var buildShape = require("./session-spawn-mcp-server").buildShape;
4
+ var z;
5
+ try { z = require("zod"); } catch (e) { z = null; }
4
6
 
5
7
  var MEMORY_CONTRACT =
6
8
  "The sticky-note board persists across sessions and is a user-facing artifact shared with people and Clay agents, not private agent scratch space. " +
@@ -8,15 +10,28 @@ var MEMORY_CONTRACT =
8
10
  "Use Sticky Notes for preferences, reminders, undeveloped ideas, or an explicit request to remember something; confirmed Issues also get short link notes as described below. For concrete actionable bugs, improvements, or deferred implementation, an authorized Project Driver should use Project Issues. If you lack Issues authority, route the work through an eligible Project Driver. Check active notes first when a duplicate is plausible. " +
9
11
  require("./issues-filing-guidance").NOTE_GUIDANCE +
10
12
  "Never create a note merely because work is important, lengthy, spans agents or restarts, or might help another agent. Do not record completed work, implementation details, test results, investigation logs, transient blockers, conversation summaries, or announcements of your own activity. When uncertain, do not write. " +
13
+ "One exception: when the user explicitly asks for a working checklist, keep that one native checklist note and check its items off as they are done; never start a checklist the user did not ask for. " +
11
14
  "Updates are visible too: update only when durable state materially changes. " +
12
15
  "A note is an attention item, so finishing it means closing it, never erasing it: call close_note once the thing it is asking for is done, and the note leaves the active board while the record stays and can be reopened. Do not turn a note into a completion log instead of closing it. " +
13
16
  "Put a concise plain-text title on the first line, stay focused on one topic, and include only the context needed for future action.";
14
17
 
18
+ var CHECKLIST_USAGE =
19
+ " Native checklists: pass checklist: true to create one. The first line is the title and every later non-empty, non-heading line is an item; write items as '- [ ] task' and '- [x] task' for items already done. " +
20
+ "To check or uncheck items, call list_notes, then pass id, that note's current version and checks with the exact item keys it returned. " +
21
+ "Any change to a checklist note, switching checklist mode, or changing checks needs the current version; a stale version or unknown item is refused, so list again and retry. " +
22
+ "Text, saved checks and colour you do not change are kept, and switching mode never erases content. Change text and checks in separate calls. " +
23
+ "When rewriting a checklist's text, start from checklist.effectiveText so each line's - [x] marker carries its real completion; droppedSavedChecks lists saved checks whose exact line no longer exists.";
24
+
25
+ var CHECK_ENTRY = z ? z.object({
26
+ item: z.string().describe("Exact item key from list_notes checklist.items[].item."),
27
+ checked: z.boolean().describe("true marks the item done; false marks it not done."),
28
+ }) : null;
29
+
15
30
  function getToolDefs(handlers) {
16
31
  return [
17
32
  {
18
33
  name: "list_notes",
19
- description: MEMORY_CONTRACT + " List the notes before writing when you need to avoid duplicates or inspect full memory beyond the injected summary. Lists open notes by default; pass state to see closed ones.",
34
+ description: MEMORY_CONTRACT + " List the notes before writing when you need to avoid duplicates or inspect full memory beyond the injected summary. Lists open notes by default; pass state to see closed ones. Each note includes its version and checklistMode; checklist notes also include checklist.items with exact item keys, each item's effective checked value (a saved check overrides the Markdown marker), its saved value, and completed/total counts.",
20
35
  inputSchema: buildShape({
21
36
  state: { type: "string", enum: ["open", "closed", "all"], description: "Which notes to list. Defaults to open, the notes still asking for action." },
22
37
  }),
@@ -24,12 +39,16 @@ function getToolDefs(handlers) {
24
39
  },
25
40
  {
26
41
  name: "write_note",
27
- description: MEMORY_CONTRACT + " Create a new note, or update an existing note by id. For notes other than confirmed Issue link notes, apply this test: would the user likely choose to keep this visible on their board next week? If the answer is unclear, do not call this tool. The 20000-character limit is an abuse guard, not a target.",
42
+ description: MEMORY_CONTRACT + " Create a new note, or update an existing note by id. For notes other than confirmed Issue link notes and a working checklist the user explicitly asked for, apply this test: would the user likely choose to keep this visible on their board next week? If the answer is unclear, do not call this tool. The 20000-character limit is an abuse guard, not a target." + CHECKLIST_USAGE,
28
43
  inputSchema: buildShape({
29
44
  id: { type: "string", description: "Existing note id to update. Omit to create a note." },
30
- text: { type: "string", description: "User-facing sticky-note text with a concise title on the first line and only durable, action-relevant context below it." },
45
+ text: { type: "string", description: "User-facing sticky-note text with a concise title on the first line and only durable, action-relevant context below it. Required when creating; omit on update to keep the current text." },
31
46
  color: { type: "string", enum: ["yellow", "blue", "green", "pink", "orange", "purple"], description: "Optional sticky-note color." },
32
- }, ["text"]),
47
+ checklist: { type: "boolean", description: "true creates or switches to a native checklist, false switches back to an ordinary note. Switching keeps the text and saved checks." },
48
+ version: { type: "number", description: "The note's current version from list_notes. Required for any change to a checklist note, a checklist mode switch, or checks." },
49
+ // buildShape returns no fields when zod is unavailable.
50
+ checks: { schema: CHECK_ENTRY && z.array(CHECK_ENTRY).min(1).max(1000), description: "Checklist items to check or uncheck on an existing checklist note, using exact item keys from list_notes." },
51
+ }),
33
52
  handler: function (args) { return handlers.write(args || {}); },
34
53
  },
35
54
  {
@@ -60,6 +79,9 @@ function getToolDefs(handlers) {
60
79
  }
61
80
 
62
81
  module.exports = {
82
+ // 2: native checklist fields. Conversation-scoped catalogs older than this
83
+ // receive the current schema through exact-query recovery.
84
+ CATALOG_VERSION: 2,
63
85
  MEMORY_CONTRACT: MEMORY_CONTRACT,
64
86
  getToolDefs: getToolDefs,
65
87
  };
@@ -0,0 +1,88 @@
1
+ // Pure planning for agent sticky-note updates, including native checklists.
2
+ // Every checklist-affecting write must name the note's current version and
3
+ // exact item keys, so a stale view can never overwrite newer content or marks.
4
+
5
+ var checklist = require("./notes-checklist");
6
+
7
+ var MAX_CHECKS = 1000;
8
+
9
+ function validateArgs(args) {
10
+ if (args.checklist !== undefined && typeof args.checklist !== "boolean") return "checklist must be true or false";
11
+ if (args.version !== undefined && !Number.isFinite(args.version)) return "version must be the number returned by list_notes";
12
+ if (args.checks === undefined) return null;
13
+ if (!Array.isArray(args.checks) || args.checks.length === 0 || args.checks.length > MAX_CHECKS) {
14
+ return "checks must be a non-empty array of at most " + MAX_CHECKS + " { item, checked } entries";
15
+ }
16
+ for (var i = 0; i < args.checks.length; i++) {
17
+ var entry = args.checks[i];
18
+ if (!entry || typeof entry !== "object" || typeof entry.item !== "string" || typeof entry.checked !== "boolean") {
19
+ return "each checks entry needs an item key from list_notes and checked true or false";
20
+ }
21
+ }
22
+ return null;
23
+ }
24
+
25
+ function staleError(note) {
26
+ return "note " + note.id + " has changed or the version is missing; call list_notes, review its current text and items, and retry with that version";
27
+ }
28
+
29
+ // Saved marks whose exact item line no longer exists after a text change.
30
+ function droppedChecks(note, nextText) {
31
+ var kept = checklist.normalizeState(note.checklistState, nextText);
32
+ return checklist.describe(note).filter(function (entry) {
33
+ return entry.saved !== null && kept[decodeURIComponent(entry.item)] === undefined;
34
+ }).map(function (entry) {
35
+ return { text: entry.text, checked: entry.saved };
36
+ });
37
+ }
38
+
39
+ function planUpdate(note, args, text, color) {
40
+ var isChecklist = note.checklistMode === true;
41
+ var modeChange = typeof args.checklist === "boolean" && args.checklist !== isChecklist;
42
+ var hasText = typeof args.text === "string";
43
+ var hasChecks = Array.isArray(args.checks);
44
+ var needsVersion = isChecklist || modeChange || hasChecks;
45
+ if ((needsVersion || args.version !== undefined) && args.version !== note.updatedAt) return { error: staleError(note) };
46
+ if (hasText && !text) return { error: "text must not be empty; close the note instead of erasing it" };
47
+ if (hasText && hasChecks) return { error: "change text and checks in separate calls so item keys refer to one exact version" };
48
+
49
+ var changes = {};
50
+ var dropped = [];
51
+ if (hasText && text !== note.text) {
52
+ changes.text = text;
53
+ dropped = droppedChecks(note, text);
54
+ if (isChecklist && !modeChange) {
55
+ // Storage repeats the same exact-source and version guard atomically.
56
+ changes.checklistEdit = true;
57
+ changes.checklistSource = note.text;
58
+ changes.checklistVersion = note.updatedAt;
59
+ }
60
+ }
61
+ if (color && color !== note.color) changes.color = color;
62
+ if (modeChange) changes.checklistMode = args.checklist;
63
+
64
+ if (hasChecks) {
65
+ var resultMode = modeChange ? args.checklist : isChecklist;
66
+ if (!resultMode) return { error: "checks apply only to checklist notes; pass checklist: true to switch this note to a checklist without changing its text" };
67
+ var state = checklist.normalizeState(note.checklistState, note.text);
68
+ var requested = Object.create(null);
69
+ for (var i = 0; i < args.checks.length; i++) {
70
+ var found = checklist.findItem(note, args.checks[i].item);
71
+ if (!found) return { error: "checklist item not found in note " + note.id + "; call list_notes and use its current item keys" };
72
+ if (requested[found.key] !== undefined && requested[found.key] !== args.checks[i].checked) {
73
+ return { error: "checks names the same item more than once with different values" };
74
+ }
75
+ requested[found.key] = args.checks[i].checked;
76
+ state[found.key] = args.checks[i].checked;
77
+ }
78
+ changes.checklistState = state;
79
+ }
80
+
81
+ if (!hasText && !hasChecks && !color && args.checklist === undefined) {
82
+ return { error: "nothing to change; pass text, color, checklist, or checks" };
83
+ }
84
+ // An identical request is acknowledged without bumping the version.
85
+ return { changes: Object.keys(changes).length ? changes : null, droppedSavedChecks: dropped };
86
+ }
87
+
88
+ module.exports = { validateArgs: validateArgs, planUpdate: planUpdate, MAX_CHECKS: MAX_CHECKS };
@@ -36,16 +36,29 @@ function handleDefaultChange(ctx, ws, msg) {
36
36
  defaultChangeResult(ctx, ws, msg || {}, { ok: false, error: "Session visibility default must be private or shared." });
37
37
  return true;
38
38
  }
39
- if (msg.slug && msg.slug !== ctx.slug) {
40
- defaultChangeResult(ctx, ws, msg, { ok: false, error: "Project settings access is not permitted" });
41
- return true;
39
+ var denied = { ok: false, error: "Project settings access is not permitted" };
40
+ // The target is named by the client, so it is authorised here, never trusted:
41
+ // another project must be accessible to this account and its own access
42
+ // record (owner, worktree, Mate) is the one checked below.
43
+ var target = msg.slug && typeof msg.slug === "string" ? msg.slug : ctx.slug;
44
+ var access;
45
+ if (target === ctx.slug) {
46
+ if (ctx.isMate === true) { defaultChangeResult(ctx, ws, msg, { ok: false, error: "Not supported" }); return true; }
47
+ access = ctx.getProjectAccess ? ctx.getProjectAccess() : null;
48
+ } else {
49
+ if (/^mate-/.test(target) || typeof ctx.getTargetAccess !== "function" ||
50
+ typeof ctx.canAccessProjectSlug !== "function" || ctx.canAccessProjectSlug(ws, target) !== true) {
51
+ defaultChangeResult(ctx, ws, msg, denied);
52
+ return true;
53
+ }
54
+ access = ctx.getTargetAccess(target);
55
+ if (!access || access.error || access.isMate === true) { defaultChangeResult(ctx, ws, msg, denied); return true; }
42
56
  }
43
- if (ctx.isMate === true || !ctx.usersModule || !ctx.usersModule.isMultiUser || !ctx.usersModule.isMultiUser()) {
57
+ if (!ctx.usersModule || !ctx.usersModule.isMultiUser || !ctx.usersModule.isMultiUser()) {
44
58
  defaultChangeResult(ctx, ws, msg, { ok: false, error: "Not supported" });
45
59
  return true;
46
60
  }
47
61
  var user = ws && ws._clayUser && ctx.usersModule.findUserById(ws._clayUser.id);
48
- var access = ctx.getProjectAccess ? ctx.getProjectAccess() : null;
49
62
  if (access && access.isWorktree === true) {
50
63
  defaultChangeResult(ctx, ws, msg, { ok: false, error: "Worktrees inherit this setting from their parent project." });
51
64
  return true;
@@ -53,16 +66,15 @@ function handleDefaultChange(ctx, ws, msg) {
53
66
  var permissions = user && ctx.usersModule.getEffectivePermissions
54
67
  ? ctx.usersModule.getEffectivePermissions(user, ctx.osUsers) : {};
55
68
  var isOwner = !!(user && access && access.ownerId && access.ownerId === user.id);
56
- var isAdmin = !!(user && user.role === "admin");
57
69
  if (!user || !permissions.projectSettings || !isOwner) {
58
- defaultChangeResult(ctx, ws, msg, { ok: false, error: "Project settings access is not permitted" });
70
+ defaultChangeResult(ctx, ws, msg, denied);
59
71
  return true;
60
72
  }
61
73
  if (typeof ctx.opts.onSetProjectSessionVisibilityDefault !== "function") {
62
74
  defaultChangeResult(ctx, ws, msg, { ok: false, error: "Not supported" });
63
75
  return true;
64
76
  }
65
- var result = ctx.opts.onSetProjectSessionVisibilityDefault(ctx.slug, msg.visibility);
77
+ var result = ctx.opts.onSetProjectSessionVisibilityDefault(target, msg.visibility);
66
78
  defaultChangeResult(ctx, ws, msg, { ok: !!(result && result.ok), visibility: result && result.visibility, error: result && result.error });
67
79
  return true;
68
80
  }
package/lib/sessions.js CHANGED
@@ -166,6 +166,7 @@ function createSessionManager(opts) {
166
166
  if (session.codexLinearToolCatalogVersion) metaObj.codexLinearToolCatalogVersion = session.codexLinearToolCatalogVersion;
167
167
  if (session.codexSharedBrowserToolCatalogVersion) metaObj.codexSharedBrowserToolCatalogVersion = session.codexSharedBrowserToolCatalogVersion;
168
168
  if (session.codexIssuesToolCatalogVersion) metaObj.codexIssuesToolCatalogVersion = session.codexIssuesToolCatalogVersion;
169
+ if (session.codexNotesToolCatalogVersion) metaObj.codexNotesToolCatalogVersion = session.codexNotesToolCatalogVersion;
169
170
  if (session.model) metaObj.model = session.model;
170
171
  if (session.effort) metaObj.effort = session.effort;
171
172
  // Persist the session's "born" mode so TUI sessions reappear after a
@@ -358,6 +359,7 @@ function createSessionManager(opts) {
358
359
  session.codexLinearToolCatalogVersion = Number(m.codexLinearToolCatalogVersion || 0);
359
360
  session.codexSharedBrowserToolCatalogVersion = Number(m.codexSharedBrowserToolCatalogVersion || 0);
360
361
  session.codexIssuesToolCatalogVersion = Number(m.codexIssuesToolCatalogVersion || 0);
362
+ session.codexNotesToolCatalogVersion = Number(m.codexNotesToolCatalogVersion || 0);
361
363
  session.model = m.model || deriveSessionModel(loaded[i].history);
362
364
  session.effort = m.effort || null;
363
365
  if (m.loop) session.loop = m.loop;
@@ -789,8 +791,9 @@ function createSessionManager(opts) {
789
791
  return 0;
790
792
  }
791
793
 
792
- function replayHistory(session, fromIndex, targetWs, transform) {
793
- var _send = (targetWs && sendTo) ? function (obj) { sendTo(targetWs, obj); } : send;
794
+ function replayHistory(session, fromIndex, targetWs, transform, snapshotSend) {
795
+ var _send = snapshotSend || ((targetWs && sendTo) ? function (obj) { sendTo(targetWs, obj); } : send);
796
+ var sendHistory = snapshotSend && snapshotSend.history || _send;
794
797
  var total = session.history.length;
795
798
  if (typeof fromIndex !== "number") {
796
799
  if (total <= INITIAL_HISTORY_SIZE) {
@@ -800,7 +803,7 @@ function createSessionManager(opts) {
800
803
  }
801
804
  }
802
805
 
803
- _send({ type: "history_meta", total: total, from: fromIndex });
806
+ sendHistory({ type: "history_meta", total: total, from: fromIndex });
804
807
 
805
808
  for (var i = fromIndex; i < total; i++) {
806
809
  var _item = session.history[i];
@@ -812,7 +815,7 @@ function createSessionManager(opts) {
812
815
  var normalizedItem = require("./yoke/usage").clientEvent(_item, session.vendor || "claude");
813
816
  var replayItem = transform ? transform(normalizedItem) : normalizedItem;
814
817
  if (replayItem && typeof replayItem === "object") replayItem = Object.assign({}, replayItem, { _historyIndex: i });
815
- _send(replayItem);
818
+ sendHistory(replayItem);
816
819
  }
817
820
 
818
821
  // Find the last result message in the full history for accurate context data
@@ -833,16 +836,17 @@ function createSessionManager(opts) {
833
836
  }
834
837
  }
835
838
 
836
- _send({ type: "history_done", controlHistoryRevision: require("./clay-control-target-projection").digest(session.history || []), sessionId: session.localId, sessionOriginId: session.sessionOriginId || null, lastUsage: lastUsage, lastModelUsage: lastModelUsage, lastCost: lastCost, lastStreamInputTokens: lastStreamInputTokens, context: lastContext, contextUsage: require("./yoke/usage").context(session.lastContextUsage, session.vendor || "claude") });
839
+ sendHistory({ type: "history_done", controlHistoryRevision: require("./clay-control-target-projection").digest(session.history || []), sessionId: session.localId, sessionOriginId: session.sessionOriginId || null, lastUsage: lastUsage, lastModelUsage: lastModelUsage, lastCost: lastCost, lastStreamInputTokens: lastStreamInputTokens, context: lastContext, contextUsage: require("./yoke/usage").context(session.lastContextUsage, session.vendor || "claude") });
837
840
  for (var vi = 0; vi < sessionViewedListeners.length; vi++) {
838
841
  try { sessionViewedListeners[vi](session, targetWs, _send); } catch (e) {}
839
842
  }
840
843
  }
841
844
 
842
- function switchSession(localId, targetWs, transform) {
845
+ function switchSession(localId, targetWs, transform, snapshotSend, snapshotGuard) {
843
846
  var session = sessions.get(localId);
844
847
  if (!session) return;
845
848
  if (typeof onSessionHydrate === "function") onSessionHydrate(session);
849
+ if (snapshotGuard && !snapshotGuard()) return;
846
850
 
847
851
  // Pane sockets (split-view iframes) pin their session via switch_session
848
852
  // but must not move the project-global active pointer: reloads and
@@ -863,7 +867,7 @@ function createSessionManager(opts) {
863
867
  singleUserUnread[localId] = 0;
864
868
 
865
869
  // In multi-user mode with a specific client, only send to that client
866
- var _send = (targetWs && sendTo) ? function (obj) { sendTo(targetWs, obj); } : send;
870
+ var _send = snapshotSend || ((targetWs && sendTo) ? function (obj) { sendTo(targetWs, obj); } : send);
867
871
 
868
872
  var _capsByVendor = capabilitiesByVendor || {};
869
873
  var _sessionVendor = session.vendor || defaultVendor || "claude";
@@ -876,7 +880,7 @@ function createSessionManager(opts) {
876
880
  || [];
877
881
  _send({ type: "slash_commands", commands: _vendorCmds, vendor: _sessionVendor });
878
882
  broadcastSessionList();
879
- replayHistory(session, undefined, targetWs, transform);
883
+ replayHistory(session, undefined, targetWs, transform, snapshotSend);
880
884
 
881
885
  if (session.isProcessing) {
882
886
  _send({ type: "status", status: "processing" });
@@ -894,10 +898,15 @@ function createSessionManager(opts) {
894
898
  toolInput: p.toolInput,
895
899
  toolUseId: p.toolUseId,
896
900
  decisionReason: p.decisionReason,
901
+ defaultToNo: p.defaultToNo,
902
+ suppressAlwaysAllowRule: p.suppressAlwaysAllowRule,
903
+ mateId: p.mateId || undefined,
904
+ vendor: p.vendor || session.vendor || undefined,
897
905
  });
898
906
  }
899
907
 
900
908
  // Re-send active mention indicator so returning clients restore the mate avatar state
909
+ if (snapshotSend) require('./pane-pending-interactions').send(session, _send);
901
910
  if (session._mentionInProgress && session._mentionActiveMateId) {
902
911
  _send({ type: "mention_processing", mateId: session._mentionActiveMateId, active: true });
903
912
  }
@@ -1030,6 +1039,14 @@ function createSessionManager(opts) {
1030
1039
  }
1031
1040
  }
1032
1041
 
1042
+ // An activated pane socket only accepts frames carrying its exact binding;
1043
+ // the transport adds it, or withholds the frame if the binding is stale.
1044
+ function sendLiveToSocket(ws, session, obj, data) {
1045
+ if (!ws._clayPaneActivation) { ws.send(data); return; }
1046
+ var panePayload = require("./pane-session-transport").outbound(ws, obj, session.localId);
1047
+ if (panePayload) ws.send(JSON.stringify(panePayload));
1048
+ }
1049
+
1033
1050
  function doSendToSession(session, obj) {
1034
1051
  // Send to active clients without recording to history/disk (ephemeral data)
1035
1052
  if (sendEach) {
@@ -1038,7 +1055,7 @@ function createSessionManager(opts) {
1038
1055
  if (filterFn && !filterFn(session)) return;
1039
1056
  if (session.homeDebatePlanning === true) return;
1040
1057
  if (ws._clayActiveSession === session.localId && ws.readyState === 1) {
1041
- ws.send(data);
1058
+ sendLiveToSocket(ws, session, obj, data);
1042
1059
  }
1043
1060
  });
1044
1061
  } else if (session.localId === activeSessionId) {
@@ -1070,7 +1087,7 @@ function createSessionManager(opts) {
1070
1087
  if (filterFn && !filterFn(session)) return;
1071
1088
  if (session.homeDebatePlanning === true) return;
1072
1089
  if (ws._clayActiveSession === session.localId) {
1073
- if (ws.readyState === 1) ws.send(data);
1090
+ if (ws.readyState === 1) sendLiveToSocket(ws, session, outgoingObj, data);
1074
1091
  } else if (session.isProcessing && !session._ioThrottle) {
1075
1092
  if (!ioData) ioData = JSON.stringify({ type: "session_io", id: session.localId });
1076
1093
  if (ws.readyState === 1) ws.send(ioData);