clay-server 3.0.0 → 3.1.0-beta.1

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/sdk-bridge.js CHANGED
@@ -136,7 +136,7 @@ function createSDKBridge(opts) {
136
136
  "--port", String(clayPort),
137
137
  "--slug", slug,
138
138
  "--session", String(session.localId),
139
- "--pair-only",
139
+ "--session-only",
140
140
  ];
141
141
  if (clayTls) bridgeArgs.push("--tls");
142
142
  var env = [];
@@ -553,6 +553,30 @@ function createSDKBridge(opts) {
553
553
  return { behavior: "allow", updatedInput: input };
554
554
  }
555
555
 
556
+ // Auto-approve split-pair partner tools. The pair itself is
557
+ // user-constructed (the user created the split and its roles and
558
+ // watches both panes live), the target is structurally confined to the
559
+ // split partner, and a prompt on every delegation hop defeats the
560
+ // driver/worker workflow. spawn_sessions is NOT here: it creates new
561
+ // sessions and keeps its prompt.
562
+ var pairPartnerTools = { send_to_partner: true, read_partner: true };
563
+ if (toolName.indexOf("mcp__clay-sessions__") === 0) {
564
+ var sessionsToolName = toolName.substring(toolName.lastIndexOf("__") + 2);
565
+ if (pairPartnerTools[sessionsToolName]) {
566
+ return { behavior: "allow", updatedInput: input };
567
+ }
568
+ }
569
+
570
+ // Sticky-note reads and writes are immediately visible on the shared
571
+ // board. Deletion stays outside the whitelist and therefore prompts;
572
+ // its session ownership is enforced again inside the server handler.
573
+ var notesToolName = toolName.substring(toolName.lastIndexOf("__") + 2);
574
+ var isClayNotesTool = toolName.indexOf("mcp__clay-notes__") === 0
575
+ || toolName.indexOf("clay-notes__") !== -1;
576
+ if (isClayNotesTool && (notesToolName === "list_notes" || notesToolName === "write_note")) {
577
+ return { behavior: "allow", updatedInput: input };
578
+ }
579
+
556
580
  // Auto-approve read-only email MCP tools.
557
581
  // These only read data from accounts the user explicitly checked.
558
582
  // Write operations (send, reply, mark_read) still require permission.
@@ -0,0 +1,39 @@
1
+ // Sticky-note memory tools for project sessions.
2
+
3
+ var buildShape = require("./session-spawn-mcp-server").buildShape;
4
+
5
+ var MEMORY_CONTRACT = "Shared project memory on the user's board. One note = one fact, decision, or reminder, written like a real sticky note: a phrase or at most two short sentences. It must be worth a human's attention a week later and stand on its own without the current conversation. Never paragraphs, lists of steps, logs, or announcement notes about your own activity. Update an existing note instead of adding a near-duplicate; delete notes that stop being true.";
6
+
7
+ function getToolDefs(handlers) {
8
+ return [
9
+ {
10
+ name: "list_notes",
11
+ description: MEMORY_CONTRACT + " List the active notes before writing when you need to avoid duplicates or inspect full memory beyond the injected summary.",
12
+ inputSchema: buildShape({}),
13
+ handler: function (args) { return handlers.list(args || {}); },
14
+ },
15
+ {
16
+ name: "write_note",
17
+ description: MEMORY_CONTRACT + " Create a new note, or update an existing note by id. Long board notes are allowed, with a 2000-character abuse guard.",
18
+ inputSchema: buildShape({
19
+ id: { type: "string", description: "Existing note id to update. Omit to create a note." },
20
+ text: { type: "string", description: "Sticky-note text. Keep the register concise even though the board permits up to 2000 characters." },
21
+ color: { type: "string", enum: ["yellow", "blue", "green", "pink", "orange", "purple"], description: "Optional sticky-note color." },
22
+ }, ["text"]),
23
+ handler: function (args) { return handlers.write(args || {}); },
24
+ },
25
+ {
26
+ name: "remove_note",
27
+ description: MEMORY_CONTRACT + " Remove a note created by this same session when it is no longer true. Notes created by users or other sessions cannot be removed.",
28
+ inputSchema: buildShape({
29
+ id: { type: "string", description: "Id of the note to remove." },
30
+ }, ["id"]),
31
+ handler: function (args) { return handlers.remove(args || {}); },
32
+ },
33
+ ];
34
+ }
35
+
36
+ module.exports = {
37
+ MEMORY_CONTRACT: MEMORY_CONTRACT,
38
+ getToolDefs: getToolDefs,
39
+ };
package/lib/ws-schema.js CHANGED
@@ -355,6 +355,7 @@ var schema = {
355
355
  // -----------------------------------------------------------------------
356
356
  "note_create": { direction: "c2s", handler: "lib/project-user-message.js", description: "Create a new sticky note" },
357
357
  "note_created": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Sticky note was created" },
358
+ "note_written": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Agent created or updated a sticky note" },
358
359
  "note_update": { direction: "c2s", handler: "lib/project-user-message.js", description: "Update a sticky note" },
359
360
  "note_updated": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Sticky note was updated" },
360
361
  "note_delete": { direction: "c2s", handler: "lib/project-user-message.js", description: "Delete a sticky note" },
@@ -97,6 +97,18 @@ function normalizePlanStatus(status) {
97
97
  return "pending";
98
98
  }
99
99
 
100
+ function dynamicInputSchema(inputSchema) {
101
+ if (inputSchema && typeof inputSchema.type === "string") return inputSchema;
102
+ try {
103
+ var zod = require("zod");
104
+ if (zod.toJSONSchema && inputSchema) {
105
+ var schema = inputSchema.safeParse ? inputSchema : zod.object(inputSchema);
106
+ return zod.toJSONSchema(schema);
107
+ }
108
+ } catch (e) {}
109
+ return { type: "object", properties: {} };
110
+ }
111
+
100
112
  // Detect Codex "not logged in" errors. Codex surfaces auth failures several
101
113
  // ways depending on transport: a clean error event with
102
114
  // codexErrorInfo:"unauthorized", or a turn/failed / item error whose message
@@ -813,15 +825,39 @@ function createCodexQueryHandle(appServer, queryOpts) {
813
825
  });
814
826
  return;
815
827
  }
816
- Promise.resolve(callDynamicTool(dynamicParams.tool, dynamicParams.arguments || {})).then(function (result) {
817
- var content = result && Array.isArray(result.content) ? result.content : [];
818
- var contentItems = content.map(function (item) {
819
- return { type: "inputText", text: item && item.text ? item.text : JSON.stringify(item) };
828
+ function executeDynamicTool() {
829
+ return Promise.resolve(callDynamicTool(dynamicParams.tool, dynamicParams.arguments || {})).then(function (result) {
830
+ var content = result && Array.isArray(result.content) ? result.content : [];
831
+ var contentItems = content.map(function (item) {
832
+ return { type: "inputText", text: item && item.text ? item.text : JSON.stringify(item) };
833
+ });
834
+ if (contentItems.length === 0) {
835
+ contentItems.push({ type: "inputText", text: typeof result === "string" ? result : JSON.stringify(result) });
836
+ }
837
+ appServer.respond(msg.id, { contentItems: contentItems, success: !(result && result.isError) });
820
838
  });
821
- if (contentItems.length === 0) {
822
- contentItems.push({ type: "inputText", text: typeof result === "string" ? result : JSON.stringify(result) });
839
+ }
840
+ var permissionToolName = dynamicParams.tool;
841
+ if (permissionToolName === "list_notes" || permissionToolName === "write_note" || permissionToolName === "remove_note") {
842
+ permissionToolName = "mcp__clay-notes__" + permissionToolName;
843
+ } else if (permissionToolName === "send_to_partner" || permissionToolName === "read_partner") {
844
+ permissionToolName = "mcp__clay-sessions__" + permissionToolName;
845
+ }
846
+ var permission = canUseTool
847
+ ? Promise.resolve(canUseTool(permissionToolName, dynamicParams.arguments || {}, {
848
+ toolUseID: dynamicParams.callId || String(msg.id),
849
+ signal: abortController ? abortController.signal : null,
850
+ }))
851
+ : Promise.resolve({ behavior: "allow" });
852
+ permission.then(function (decision) {
853
+ if (!isApproved(decision)) {
854
+ appServer.respond(msg.id, {
855
+ contentItems: [{ type: "inputText", text: "Tool use was not approved" }],
856
+ success: false,
857
+ });
858
+ return;
823
859
  }
824
- appServer.respond(msg.id, { contentItems: contentItems, success: !(result && result.isError) });
860
+ return executeDynamicTool();
825
861
  }).catch(function (err) {
826
862
  appServer.respond(msg.id, {
827
863
  contentItems: [{ type: "inputText", text: "Error: " + (err.message || String(err)) }],
@@ -963,7 +999,7 @@ function createCodexQueryHandle(appServer, queryOpts) {
963
999
  type: "function",
964
1000
  name: tool.name,
965
1001
  description: tool.description || tool.name,
966
- inputSchema: tool.inputSchema || { type: "object", properties: {} },
1002
+ inputSchema: dynamicInputSchema(tool.inputSchema),
967
1003
  };
968
1004
  });
969
1005
  }
@@ -6,7 +6,7 @@
6
6
  // list/call requests to Clay's project-scoped HTTP endpoint
7
7
  // (/p/{slug}/api/mcp-bridge) when a slug is provided.
8
8
  //
9
- // Usage: node mcp-bridge-server.js --port 2633 --slug my-project [--session 42] [--pair-only]
9
+ // Usage: node mcp-bridge-server.js --port 2633 --slug my-project [--session 42] [--session-only]
10
10
  //
11
11
  // Lifecycle:
12
12
  // 1. The vendor CLI spawns this process
@@ -25,7 +25,7 @@ var clayPort = 2633;
25
25
  var claySlug = "";
26
26
  var clayTls = false;
27
27
  var claySessionId = null;
28
- var pairOnly = false;
28
+ var sessionOnly = false;
29
29
 
30
30
  for (var i = 0; i < args.length; i++) {
31
31
  if (args[i] === "--port" && args[i + 1]) {
@@ -40,8 +40,8 @@ for (var i = 0; i < args.length; i++) {
40
40
  i++;
41
41
  } else if (args[i] === "--tls") {
42
42
  clayTls = true;
43
- } else if (args[i] === "--pair-only") {
44
- pairOnly = true;
43
+ } else if (args[i] === "--session-only") {
44
+ sessionOnly = true;
45
45
  }
46
46
  }
47
47
 
@@ -119,7 +119,7 @@ function postJson(urlPath, body) {
119
119
 
120
120
  // --- Fetch tools from Clay ---
121
121
  function fetchTools() {
122
- return postJson(CLAY_MCP_PATH, { action: "list_tools", sessionId: claySessionId, pairOnly: pairOnly }).then(function (resp) {
122
+ return postJson(CLAY_MCP_PATH, { action: "list_tools", sessionId: claySessionId, sessionOnly: sessionOnly }).then(function (resp) {
123
123
  if (resp.error) {
124
124
  log("Failed to fetch tools: " + resp.error);
125
125
  return [];
@@ -146,7 +146,7 @@ function callTool(serverName, toolName, args) {
146
146
  tool: toolName,
147
147
  args: args || {},
148
148
  sessionId: claySessionId,
149
- pairOnly: pairOnly,
149
+ sessionOnly: sessionOnly,
150
150
  });
151
151
  }
152
152
 
@@ -308,4 +308,4 @@ process.stdin.on("error", function () {
308
308
  process.on("SIGTERM", function () { process.exit(0); });
309
309
  process.on("SIGINT", function () { process.exit(0); });
310
310
 
311
- log("Started: port=" + clayPort + " slug=" + claySlug + " session=" + (claySessionId === null ? "none" : claySessionId) + " pairOnly=" + pairOnly);
311
+ log("Started: port=" + clayPort + " slug=" + claySlug + " session=" + (claySessionId === null ? "none" : claySessionId) + " sessionOnly=" + sessionOnly);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "3.0.0",
3
+ "version": "3.1.0-beta.1",
4
4
  "description": "Self-hosted team workspace for Claude Code and Codex. Multi-user, browser-based, with persistent AI mates.",
5
5
  "bin": {
6
6
  "clay-server": "./bin/cli.js",
@@ -1,178 +0,0 @@
1
- // Title-bar branch switcher for a project and its worktrees.
2
-
3
- import { store } from './store.js';
4
- import { iconHtml, refreshIcons } from './icons.js';
5
- import { escapeHtml } from './utils.js';
6
- import { familyOf } from './worktree-family.js';
7
- import { switchProject, confirmRemoveProject } from './app-projects.js';
8
- import { showWorktreeModal } from './sidebar-projects.js';
9
-
10
- var chip = null;
11
- var label = null;
12
- var menu = null;
13
- var menuOpen = false;
14
- var defaultBranches = {};
15
- // slug -> true/false once /api/branches answers; undefined while unknown.
16
- // Non-git projects hide the chip (no worktrees to manage there).
17
- var gitRepoBySlug = {};
18
-
19
- function projectName(project) {
20
- return project ? (project.title || project.project || project.name || project.slug) : "";
21
- }
22
-
23
- function currentProject(projects) {
24
- var slug = store.get('currentSlug');
25
- for (var i = 0; i < projects.length; i++) {
26
- if (projects[i].slug === slug) return projects[i];
27
- }
28
- return null;
29
- }
30
-
31
- function defaultBranch(parent) {
32
- return defaultBranches[parent.slug] || "default";
33
- }
34
-
35
- function requestDefaultBranch(parent) {
36
- if (!parent || Object.prototype.hasOwnProperty.call(defaultBranches, parent.slug)) return;
37
- defaultBranches[parent.slug] = null;
38
- fetch("/p/" + encodeURIComponent(parent.slug) + "/api/branches")
39
- .then(function (response) { return response.json(); })
40
- .then(function (data) {
41
- defaultBranches[parent.slug] = data.defaultBranch || "default";
42
- gitRepoBySlug[parent.slug] = data.isGitRepo !== false;
43
- renderBranchSwitcher();
44
- })
45
- .catch(function () {
46
- defaultBranches[parent.slug] = "default";
47
- renderBranchSwitcher();
48
- });
49
- }
50
-
51
- function closeMenu() {
52
- menuOpen = false;
53
- if (menu) menu.classList.add("hidden");
54
- if (chip) {
55
- chip.classList.remove("open");
56
- chip.setAttribute("aria-expanded", "false");
57
- }
58
- }
59
-
60
- function positionMenu() {
61
- if (!chip || !menu || !menuOpen) return;
62
- var rect = chip.getBoundingClientRect();
63
- var left = Math.min(rect.left, window.innerWidth - menu.offsetWidth - 8);
64
- var top = rect.bottom + 6;
65
- if (top + menu.offsetHeight > window.innerHeight - 8) top = rect.top - menu.offsetHeight - 6;
66
- menu.style.left = Math.max(8, left) + "px";
67
- menu.style.top = Math.max(8, top) + "px";
68
- }
69
-
70
- function branchLabel(project, parent) {
71
- return project.isWorktree ? (project.branch || projectName(project)) : defaultBranch(parent);
72
- }
73
-
74
- function appendProjectRow(container, project, parent, isCurrent) {
75
- var row = document.createElement("div");
76
- row.className = "branch-chip-row" + (isCurrent ? " current" : "");
77
- var inaccessible = project.isWorktree && project.worktreeAccessible === false;
78
- var main = document.createElement("button");
79
- main.className = "branch-chip-row-main";
80
- main.disabled = inaccessible;
81
- if (inaccessible) main.title = "Outside project path";
82
- main.innerHTML = iconHtml("git-branch") + '<span class="branch-chip-row-label">' +
83
- escapeHtml(branchLabel(project, parent)) + "</span>";
84
- if (project.isProcessing) {
85
- var dot = document.createElement("span");
86
- dot.className = "branch-chip-processing";
87
- main.appendChild(dot);
88
- }
89
- if (project.pendingPermissions > 0) {
90
- var badge = document.createElement("span");
91
- badge.className = "branch-chip-pending";
92
- badge.textContent = project.pendingPermissions > 99 ? "99+" : String(project.pendingPermissions);
93
- main.appendChild(badge);
94
- }
95
- if (isCurrent) main.insertAdjacentHTML("beforeend", iconHtml("check"));
96
- if (!inaccessible) {
97
- main.addEventListener("click", function () { closeMenu(); switchProject(project.slug); });
98
- }
99
- row.appendChild(main);
100
-
101
- if (project.isWorktree && (!store.get('permissions') || store.get('permissions').deleteProject !== false)) {
102
- var remove = document.createElement("button");
103
- remove.className = "branch-chip-row-action";
104
- remove.title = "Remove worktree";
105
- remove.innerHTML = iconHtml("trash-2");
106
- remove.addEventListener("click", function (event) {
107
- event.stopPropagation();
108
- closeMenu();
109
- confirmRemoveProject(project.slug, projectName(project));
110
- });
111
- row.appendChild(remove);
112
- }
113
- container.appendChild(row);
114
- }
115
-
116
- function renderMenu(family, current) {
117
- menu.innerHTML = "";
118
- if (family.parent) appendProjectRow(menu, family.parent, family.parent, current.slug === family.parent.slug);
119
- for (var i = 0; i < family.worktrees.length; i++) {
120
- appendProjectRow(menu, family.worktrees[i], family.parent || family.worktrees[i], current.slug === family.worktrees[i].slug);
121
- }
122
- if (family.parent) {
123
- var footer = document.createElement("button");
124
- footer.className = "branch-chip-new";
125
- footer.innerHTML = iconHtml("plus") + " <span>New worktree...</span>";
126
- footer.addEventListener("click", function () {
127
- closeMenu();
128
- showWorktreeModal(family.parent.slug, projectName(family.parent));
129
- });
130
- menu.appendChild(footer);
131
- }
132
- refreshIcons();
133
- positionMenu();
134
- }
135
-
136
- export function renderBranchSwitcher() {
137
- if (!chip) return;
138
- var projects = store.get('projects') || [];
139
- var current = currentProject(projects);
140
- var family = current ? familyOf(projects, current.slug) : { parent: null, worktrees: [] };
141
- // The chip is the project's worktree management surface, so it shows even
142
- // with zero worktrees (the menu then offers the default branch and "New
143
- // worktree"). Hidden where there is no project context to manage, and for
144
- // non-git projects (optimistically visible until /api/branches answers).
145
- var gateSlug = family.parent ? family.parent.slug : (current ? current.slug : null);
146
- var hidden = !current || store.get('dmMode') || store.get('mateProjectSlug') ||
147
- store.get('homeHubVisible') || (gateSlug && gitRepoBySlug[gateSlug] === false);
148
- chip.classList.toggle("hidden", hidden);
149
- if (hidden) { closeMenu(); return; }
150
- if (family.parent) requestDefaultBranch(family.parent);
151
- label.textContent = branchLabel(current, family.parent || current);
152
- if (menuOpen) renderMenu(family, current);
153
- }
154
-
155
- export function initBranchSwitcher() {
156
- chip = document.getElementById("branch-chip");
157
- label = document.getElementById("branch-chip-label");
158
- menu = document.getElementById("branch-chip-menu");
159
- if (!chip || !label || !menu) return;
160
- chip.addEventListener("click", function (event) {
161
- event.stopPropagation();
162
- menuOpen = !menuOpen;
163
- chip.classList.toggle("open", menuOpen);
164
- chip.setAttribute("aria-expanded", menuOpen ? "true" : "false");
165
- menu.classList.toggle("hidden", !menuOpen);
166
- renderBranchSwitcher();
167
- });
168
- document.addEventListener("click", function (event) {
169
- if (menuOpen && !menu.contains(event.target) && !chip.contains(event.target)) closeMenu();
170
- });
171
- window.addEventListener("resize", positionMenu);
172
- store.subscribe(function (state, prev) {
173
- if (state.projects !== prev.projects || state.currentSlug !== prev.currentSlug ||
174
- state.dmMode !== prev.dmMode || state.mateProjectSlug !== prev.mateProjectSlug ||
175
- state.homeHubVisible !== prev.homeHubVisible) renderBranchSwitcher();
176
- });
177
- renderBranchSwitcher();
178
- }
@@ -1,60 +0,0 @@
1
- // Pure helpers for presenting worktree projects as branches of one family.
2
-
3
- function projectName(project) {
4
- return project ? (project.title || project.project || project.name || project.slug) : "";
5
- }
6
-
7
- export function familyOf(projects, slug) {
8
- var list = projects || [];
9
- var current = null;
10
- for (var i = 0; i < list.length; i++) {
11
- if (list[i].slug === slug) { current = list[i]; break; }
12
- }
13
- if (!current) return { parent: null, worktrees: [] };
14
-
15
- var parentSlug = current.isWorktree ? current.parentSlug : current.slug;
16
- var parent = current.isWorktree ? null : current;
17
- var worktrees = [];
18
- for (var j = 0; j < list.length; j++) {
19
- var project = list[j];
20
- if (!parent && project.slug === parentSlug && !project.isWorktree) parent = project;
21
- if (project.isWorktree && project.parentSlug === parentSlug) worktrees.push(project);
22
- }
23
- if (current.isWorktree && worktrees.indexOf(current) === -1) worktrees.push(current);
24
- worktrees.sort(function (a, b) { return projectName(a).localeCompare(projectName(b)); });
25
- return { parent: parent, worktrees: worktrees };
26
- }
27
-
28
- export function parentProjects(projects) {
29
- return (projects || []).filter(function (project) { return !project.isWorktree; });
30
- }
31
-
32
- export function displayProject(projects, slug) {
33
- var family = familyOf(projects, slug);
34
- if (family.parent) return family.parent;
35
- for (var i = 0; i < (projects || []).length; i++) {
36
- if (projects[i].slug === slug) return projects[i];
37
- }
38
- return null;
39
- }
40
-
41
- export function aggregateFamily(parent, worktrees) {
42
- var result = Object.assign({}, parent);
43
- var children = worktrees || [];
44
- result.isProcessing = !!result.isProcessing;
45
- result.unread = result.unread || 0;
46
- result.pendingPermissions = result.pendingPermissions || 0;
47
- for (var i = 0; i < children.length; i++) {
48
- result.isProcessing = result.isProcessing || !!children[i].isProcessing;
49
- result.unread += children[i].unread || 0;
50
- result.pendingPermissions += children[i].pendingPermissions || 0;
51
- }
52
- return result;
53
- }
54
-
55
- export function switcherProjectName(projects, project) {
56
- if (!project || !project.isWorktree) return projectName(project);
57
- var family = familyOf(projects, project.slug);
58
- var parentName = family.parent ? projectName(family.parent) : (project.parentSlug || "Project");
59
- return parentName + " \u2387 " + (project.branch || projectName(project));
60
- }