clay-server 4.8.2 → 5.0.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.
Files changed (82) hide show
  1. package/LICENSES/README.md +13 -0
  2. package/LICENSES/open-iconic.txt +21 -0
  3. package/LICENSES/plantuml-salt.txt +674 -0
  4. package/lib/assets/salt-assets.json +15 -0
  5. package/lib/assets/salt-colors.json +156 -0
  6. package/lib/assets/salt-font-10.bin.gz +0 -0
  7. package/lib/assets/salt-font-13.bin.gz +0 -0
  8. package/lib/assets/salt-font-14.bin.gz +0 -0
  9. package/lib/assets/salt-font-16.bin.gz +0 -0
  10. package/lib/assets/salt-font-18.bin.gz +0 -0
  11. package/lib/assets/salt-font-20.bin.gz +0 -0
  12. package/lib/assets/salt-font-metrics.bin.gz +0 -0
  13. package/lib/assets/salt-font-metrics.json +101 -0
  14. package/lib/assets/salt-font-monospace-10.bin.gz +0 -0
  15. package/lib/assets/salt-font-monospace-13.bin.gz +0 -0
  16. package/lib/assets/salt-font-monospace-14.bin.gz +0 -0
  17. package/lib/assets/salt-font-monospace-16.bin.gz +0 -0
  18. package/lib/assets/salt-font-monospace-18.bin.gz +0 -0
  19. package/lib/assets/salt-font-monospace-20.bin.gz +0 -0
  20. package/lib/assets/salt-font-monospace.bin.gz +0 -0
  21. package/lib/assets/salt-icons.json +671 -0
  22. package/lib/bundled-skills/clay-sketch/SKILL.md +198 -0
  23. package/lib/mcp-browser-proxy.js +37 -0
  24. package/lib/mcp-connection-store.js +39 -0
  25. package/lib/mcp-network.js +46 -0
  26. package/lib/mcp-oauth.js +65 -0
  27. package/lib/mcp-proxy.js +32 -0
  28. package/lib/mcp-remote-connections.js +215 -0
  29. package/lib/mcp-session-access.js +15 -0
  30. package/lib/plantuml-renderer.js +43 -0
  31. package/lib/project-file-http.js +2 -0
  32. package/lib/project-mcp.js +130 -357
  33. package/lib/project-plantuml.js +48 -0
  34. package/lib/project-session-document.js +17 -1
  35. package/lib/project-user-message.js +2 -0
  36. package/lib/project-wireframe-http.js +88 -0
  37. package/lib/project.js +28 -45
  38. package/lib/public/app.js +7 -0
  39. package/lib/public/css/filebrowser.css +5 -0
  40. package/lib/public/css/mcp-connections.css +86 -0
  41. package/lib/public/css/skills-workbench.css +74 -0
  42. package/lib/public/css/wireframe.css +95 -0
  43. package/lib/public/index.html +16 -15
  44. package/lib/public/modules/app-messages.js +10 -2
  45. package/lib/public/modules/filebrowser.js +29 -3
  46. package/lib/public/modules/markdown.js +3 -1
  47. package/lib/public/modules/mcp-connection-view.js +136 -0
  48. package/lib/public/modules/mcp-permissions.js +122 -0
  49. package/lib/public/modules/mcp-ui.js +85 -461
  50. package/lib/public/modules/plantuml-preview.js +76 -0
  51. package/lib/public/modules/skills-actions.js +47 -79
  52. package/lib/public/modules/skills-detail.js +32 -145
  53. package/lib/public/modules/skills-list-view.js +29 -0
  54. package/lib/public/modules/skills-state.js +4 -2
  55. package/lib/public/modules/skills.js +104 -399
  56. package/lib/public/modules/split-view.js +14 -0
  57. package/lib/public/modules/vendor-login-browser.js +16 -5
  58. package/lib/public/modules/wireframe-chat.js +52 -0
  59. package/lib/public/modules/wireframe-render.js +69 -0
  60. package/lib/public/modules/wireframe-style.js +6 -0
  61. package/lib/public/modules/wireframe-viewport.js +70 -0
  62. package/lib/public/modules/wireframe-workbench.js +116 -0
  63. package/lib/public/style.css +3 -0
  64. package/lib/salt-chrome.js +36 -0
  65. package/lib/salt-hand.js +37 -0
  66. package/lib/salt-images.js +87 -0
  67. package/lib/salt-layout.js +60 -0
  68. package/lib/salt-options.js +23 -0
  69. package/lib/salt-overlays.js +45 -0
  70. package/lib/salt-parser.js +153 -0
  71. package/lib/salt-screen.js +111 -0
  72. package/lib/salt-svg-shapes.js +80 -0
  73. package/lib/salt-svg.js +164 -0
  74. package/lib/salt-text.js +112 -0
  75. package/lib/salt-tree.js +45 -0
  76. package/lib/sdk-bridge.js +5 -17
  77. package/lib/sdk-mcp-descriptors.js +2 -1
  78. package/lib/session-document-mcp-server.js +9 -0
  79. package/lib/vendor-login-browser-worker.js +18 -8
  80. package/lib/vendor-login-browser.js +10 -3
  81. package/lib/yoke/adapters/codex.js +1 -18
  82. package/package.json +6 -2
@@ -0,0 +1,45 @@
1
+ // Adapted from PlantUML Salt ElementTree, GPL-3.0-or-later; see LICENSES/plantuml-salt.txt.
2
+ function measure(node) {
3
+ node.columns = [];
4
+ node.heights = [];
5
+ node.rows.forEach(function (row) {
6
+ row.forEach(function (item, c) { node.columns[c] = Math.max(node.columns[c] || 0, item.width + (c ? 0 : (item.level || 0) * 10)); });
7
+ node.heights.push(Math.max(0, ...row.map(function (item) { return item.height; })));
8
+ });
9
+ node.width = node.columns.reduce(function (a, b) { return a + b; }, 0) + Math.max(0, node.columns.length - 1) * 10 + 2;
10
+ node.height = node.heights.reduce(function (a, b) { return a + b; }, 0);
11
+ }
12
+ function draw(node, x, y, s, drawChild) {
13
+ var cy = y;
14
+ var skeleton = [];
15
+ var rows = [y];
16
+ var cols = [x, x + (node.columns[0] || 0) + 5];
17
+ node.columns.slice(1).forEach(function (width) { cols.push(cols[cols.length - 1] + width + 10); });
18
+ node.rows.forEach(function (row, r) {
19
+ var cx = x;
20
+ row.forEach(function (item, c) {
21
+ drawChild(item, c ? cx : x + (item.level || 0) * 10, cy, item.width, item.height);
22
+ cx += node.columns[c] + 10;
23
+ });
24
+ skeleton.push({ x: x + (row[0].level || 0) * 10 - 7, y: cy + node.heights[r] / 2 - 1 });
25
+ cy += node.heights[r]; rows.push(cy);
26
+ });
27
+ skeleton.forEach(function (item, i) {
28
+ if (skeleton[i + 1] && skeleton[i + 1].x > item.x) s.rect(item.x, item.y, 2, 2, 'none', '#888');
29
+ var parent;
30
+ for (var j = 0; j < i; j++) if (skeleton[j].x < item.x) parent = skeleton[j];
31
+ if (parent) {
32
+ s.line(parent.x + 1, parent.y + 3, parent.x + 1, item.y + 1, '#888');
33
+ s.line(parent.x + 1, item.y + 1, item.x - 1, item.y + 1, '#888');
34
+ }
35
+ });
36
+ var mode = node.style.slice(1);
37
+ var right = cols[cols.length - 1];
38
+ if (mode === '+') {
39
+ s.line(x, y, right, y, '#888'); s.line(x, cy, right, cy, '#888');
40
+ s.line(x, y, x, cy, '#888'); s.line(right, y, right, cy, '#888');
41
+ }
42
+ if (/[#-]/.test(mode)) rows.forEach(function (yy) { s.line(x, yy, right, yy, '#888'); });
43
+ if (/[#!]/.test(mode)) cols.forEach(function (xx) { s.line(xx, y, xx, cy, '#888'); });
44
+ }
45
+ module.exports = { measure: measure, draw: draw };
package/lib/sdk-bridge.js CHANGED
@@ -44,7 +44,7 @@ function callMcpToolHandler(mcpServers, serverName, toolName, args) {
44
44
 
45
45
  // Merge in-process MCP servers with remote (extension-bridged) MCP servers.
46
46
  // Returns the merged object, or null if no servers exist.
47
- function mergeMcpServers(localServers, getRemoteFn) {
47
+ function mergeMcpServers(localServers, getRemoteFn, session) {
48
48
  var merged = {};
49
49
  var hasAny = false;
50
50
  if (localServers) {
@@ -58,7 +58,7 @@ function mergeMcpServers(localServers, getRemoteFn) {
58
58
  console.log("[mergeMcpServers] local servers: null");
59
59
  }
60
60
  if (typeof getRemoteFn === "function") {
61
- var remote = getRemoteFn();
61
+ var remote = getRemoteFn(session);
62
62
  if (remote) {
63
63
  var rk = Object.keys(remote);
64
64
  console.log("[mergeMcpServers] remote servers:", rk.join(", ") || "(none)");
@@ -713,7 +713,8 @@ function createSDKBridge(opts) {
713
713
  // This tool only prepares a read-only UI projection for the document the
714
714
  // agent is about to edit. The subsequent Edit/Write keeps its own normal
715
715
  // permission behavior.
716
- if (toolName.indexOf("mcp__clay-documents__present_markdown_edit") === 0 ||
716
+ if (toolName === "mcp__clay-documents__present_wireframe" || toolName === "clay-documents__present_wireframe" ||
717
+ toolName.indexOf("mcp__clay-documents__present_markdown_edit") === 0 ||
717
718
  toolName.indexOf("clay-documents__present_markdown_edit") !== -1) {
718
719
  return { behavior: "allow", updatedInput: input };
719
720
  }
@@ -751,19 +752,6 @@ function createSDKBridge(opts) {
751
752
  return { behavior: "allow", updatedInput: input };
752
753
  }
753
754
 
754
- // Auto-approve remote MCP tools that the user explicitly enabled in project settings.
755
- // These are user-owned local MCP servers, so no additional permission prompt needed.
756
- if (toolName.indexOf("mcp__") === 0 && getRemoteMcpServers) {
757
- var _rmcp = getRemoteMcpServers();
758
- if (_rmcp) {
759
- var _mcpParts = toolName.split("__");
760
- var _mcpServerName = _mcpParts.length >= 2 ? _mcpParts[1] : "";
761
- if (_rmcp[_mcpServerName]) {
762
- return { behavior: "allow", updatedInput: input };
763
- }
764
- }
765
- }
766
-
767
755
  // Auto-approve safe Bash commands (read-only, non-destructive)
768
756
  // Applies to ALL sessions (mates and regular projects alike).
769
757
  // These are purely read-only commands that cannot modify files, install
@@ -1896,7 +1884,7 @@ function createSDKBridge(opts) {
1896
1884
  localMcpServers[userInputServer.name || "yoke-user-input"] = userInputServer;
1897
1885
  }
1898
1886
  }
1899
- var mergedMcpServers = mergeMcpServers(localMcpServers, getRemoteMcpServers) || undefined;
1887
+ var mergedMcpServers = mergeMcpServers(localMcpServers, getRemoteMcpServers, session) || undefined;
1900
1888
  if (selectedUserInputMode === "fallback") {
1901
1889
  sessionToolDefs = sessionToolDefs.concat(yoke.userInput.fallbackToolDefs(sessionUserInputHandler, { maxQuestions: scheduleQuestionLimit }));
1902
1890
  }
@@ -18,7 +18,8 @@ function extractMcpDescriptors(mcpServers) {
18
18
  var toolDef = server.instance._registeredTools[toolName];
19
19
  var inputSchema = { type: "object", properties: {} };
20
20
  try {
21
- if (toolDef.inputSchema) inputSchema = toJSONSchema(toolDef.inputSchema);
21
+ if (toolDef._clayInputSchema) inputSchema = toolDef._clayInputSchema;
22
+ else if (toolDef.inputSchema) inputSchema = toJSONSchema(toolDef.inputSchema);
22
23
  } catch (e) {
23
24
  throw new Error("Cannot serialize MCP tool schema " + serverName + "/" + toolName + ": " + e.message);
24
25
  }
@@ -10,6 +10,15 @@ function getToolDefs(handlers) {
10
10
  path: { type: "string", description: "Project-relative or absolute path to the Markdown document that will be edited." },
11
11
  }, ["path"]),
12
12
  handler: function (args) { return handlers.present(args || {}); },
13
+ }, {
14
+ name: "present_wireframe",
15
+ description: "Open or update the temporary clay-sketch viewer in the right panel when the user wants to revise or discuss a wireframe. Supports nested layouts and common UI controls, not general UML or preprocessor directives. Supply complete Salt diagram source, a stable id reused across revisions, and a short title. This displays only; it never saves or edits files. Include the same source in a fenced clay-sketch response so the diagram remains in chat history.",
16
+ inputSchema: buildShape({
17
+ source: { type: "string", description: "Complete @startsalt/@endsalt source, or @startuml with a salt body (100 KB maximum)." },
18
+ id: { type: "string", description: "Stable design id, using letters, digits, hyphens or underscores (80 characters maximum)." },
19
+ title: { type: "string", description: "Short title for the viewer." },
20
+ }, ["source", "id"]),
21
+ handler: function (args) { return handlers.presentWireframe(args || {}); },
13
22
  }];
14
23
  }
15
24
 
@@ -3,6 +3,7 @@ var readline = require("readline");
3
3
  var spawn = require("child_process").spawn;
4
4
  var chromium = require("playwright").chromium;
5
5
  var browser = null;
6
+ var loginProxy = null;
6
7
  var page = null;
7
8
  var installing = null;
8
9
  var viewing = true;
@@ -22,7 +23,7 @@ async function launch() {
22
23
  var channels = ["chrome", "msedge", null];
23
24
  for (var i = 0; i < channels.length; i++) {
24
25
  try {
25
- return await chromium.launch({ channel: channels[i] || undefined, headless: true, chromiumSandbox: true, timeout: 20000 });
26
+ return await chromium.launch({ channel: channels[i] || undefined, headless: true, chromiumSandbox: true, timeout: 20000, proxy: loginProxy ? { server: loginProxy.server } : undefined, args: loginProxy ? ["--force-webrtc-ip-handling-policy=disable_non_proxied_udp"] : [] });
26
27
  } catch (e) {
27
28
  if (closing) throw e;
28
29
  }
@@ -39,7 +40,7 @@ async function launch() {
39
40
  });
40
41
  });
41
42
  if (closing) throw new Error("Closed");
42
- return chromium.launch({ headless: true, chromiumSandbox: true, timeout: 20000 });
43
+ return chromium.launch({ headless: true, chromiumSandbox: true, timeout: 20000, proxy: loginProxy ? { server: loginProxy.server } : undefined, args: loginProxy ? ["--force-webrtc-ip-handling-policy=disable_non_proxied_udp"] : [] });
43
44
  }
44
45
 
45
46
  async function capture() {
@@ -55,7 +56,8 @@ async function capture() {
55
56
  if (!closing) captureTimer = setTimeout(capture, 250);
56
57
  }
57
58
 
58
- async function start(url) {
59
+ async function start(url, restrictedNetwork) {
60
+ if (restrictedNetwork) loginProxy = await require("./mcp-browser-proxy").createBrowserProxy();
59
61
  browser = await launch();
60
62
  if (closing) { await browser.close(); return; }
61
63
  var session = await browser.newBrowserCDPSession();
@@ -64,12 +66,19 @@ async function start(url) {
64
66
  finally { await session.detach(); }
65
67
  // Preserve the native version/platform and replace only the headless product token.
66
68
  var userAgent = version.userAgent.replace("HeadlessChrome/", "Chrome/");
67
- var context = await browser.newContext({ viewport: viewport, acceptDownloads: false, userAgent: userAgent });
69
+ var context = await browser.newContext({ viewport: viewport, acceptDownloads: false, serviceWorkers: "block", userAgent: userAgent });
70
+ if (restrictedNetwork) await context.routeWebSocket("**/*", function (socket) { socket.close(); });
68
71
  await context.addInitScript(require("./vendor-login-browser-passkeys").disableLoginPasskeys);
69
72
  var callback = new URL(new URL(url).searchParams.get("redirect_uri"));
70
- await context.route("**/*", function (route) {
73
+ await context.route("**/*", async function (route) {
71
74
  var target = new URL(route.request().url());
72
- if (target.protocol === "https:" || (target.origin === callback.origin && target.pathname === callback.pathname)) return route.continue();
75
+ if (target.origin === callback.origin && target.pathname === callback.pathname) return route.continue();
76
+ if (target.protocol === "https:") {
77
+ if (restrictedNetwork) {
78
+ try { await require("./mcp-network").assertPublicUrl(target.href); } catch (error) { return route.abort(); }
79
+ }
80
+ return route.continue();
81
+ }
73
82
  return route.abort();
74
83
  });
75
84
  context.on("page", function (opened) {
@@ -120,6 +129,7 @@ async function close() {
120
129
  clearTimeout(captureTimer);
121
130
  if (installing) installing.kill();
122
131
  if (browser) await browser.close().catch(function () {});
132
+ if (loginProxy) loginProxy.close();
123
133
  process.exit(0);
124
134
  }
125
135
 
@@ -131,8 +141,8 @@ lines.on("line", function (line) {
131
141
  try { message = JSON.parse(line); } catch (e) { return; }
132
142
  if (message.type === "start" && !started) {
133
143
  started = true;
134
- start(message.url).catch(function () {
135
- status("Unable to open the sign-in browser. The host may need browser libraries or sandbox support. Use terminal sign-in to continue.", "error");
144
+ start(message.url, message.restrictedNetwork).catch(function () {
145
+ status("Unable to open the sign-in browser. The host may need browser libraries or sandbox support." + (message.restrictedNetwork ? " Retry or use an access token." : " Use terminal sign-in to continue."), "error");
136
146
  });
137
147
  } else if (message.type === "viewing") {
138
148
  viewing = !!message.value;
@@ -20,6 +20,12 @@ function loginUrl(value, vendor) {
20
20
  }
21
21
 
22
22
  function createLoginBrowser(options) {
23
+ var initial = null;
24
+ if (options.initialUrl) {
25
+ initial = new URL(options.initialUrl);
26
+ var callback = new URL(initial.searchParams.get("redirect_uri"));
27
+ if (options.vendor !== "mcp" || !options.restrictedNetwork || initial.protocol !== "https:" || initial.username || initial.password || !initial.searchParams.get("state") || !initial.searchParams.get("code_challenge") || callback.protocol !== "http:" || callback.hostname !== "127.0.0.1" || callback.pathname !== "/callback") throw new Error("Invalid MCP sign-in URL");
28
+ }
23
29
  var id = crypto.randomUUID();
24
30
  var dir = fs.mkdtempSync(path.join(os.tmpdir(), "clay-login-"));
25
31
  fs.chmodSync(dir, 0o700);
@@ -98,17 +104,17 @@ function createLoginBrowser(options) {
98
104
  });
99
105
  child.on("error", function () {
100
106
  statusPhase = "error";
101
- status = "The sign-in browser could not start. Use terminal sign-in to continue.";
107
+ status = "The sign-in browser could not start." + (options.vendor === "mcp" ? " Retry or use an access token." : " Use terminal sign-in to continue.");
102
108
  send({ type: "vendor_login_browser_status", text: status, phase: statusPhase });
103
109
  });
104
110
  child.on("exit", function () {
105
111
  if (closed) return;
106
112
  statusPhase = "error";
107
- status = "The sign-in browser closed. You can retry or use terminal sign-in.";
113
+ status = "The sign-in browser closed. You can retry.";
108
114
  send({ type: "vendor_login_browser_status", text: status, phase: statusPhase });
109
115
  });
110
116
  child.stdin.on("error", function () {});
111
- write({ type: "start", url: url });
117
+ write({ type: "start", url: url, restrictedNetwork: !!options.restrictedNetwork });
112
118
  }
113
119
  function acceptUrl(value) {
114
120
  var url = loginUrl(value, options.vendor);
@@ -123,6 +129,7 @@ function createLoginBrowser(options) {
123
129
  });
124
130
  }, 250);
125
131
  timer.unref();
132
+ if (initial) start(initial.href);
126
133
  return {
127
134
  id: id,
128
135
  env: { BROWSER: wrapper, CLAY_LOGIN_URL_FILE: urlFile },
@@ -1853,25 +1853,8 @@ function createCodexAdapter(opts) {
1853
1853
  if (co.config) serverOpts.config = Object.assign(serverOpts.config, co.config);
1854
1854
  }
1855
1855
 
1856
- // Track 1: Read local MCP server definitions from ~/.clay/mcp.json
1857
- // and inject into Codex config so Codex manages them natively.
1856
+ // Clay-managed external MCP runs through the owner-bound bridge.
1858
1857
  var mcpServerConfig = Object.assign({}, serverOpts.config.mcp_servers || {});
1859
- try {
1860
- var mcpLocal = require("../../mcp-local");
1861
- var localMcpServers = mcpLocal.readMergedServers();
1862
- var mcpNames = Object.keys(localMcpServers);
1863
- for (var mi = 0; mi < mcpNames.length; mi++) {
1864
- var ms = localMcpServers[mcpNames[mi]];
1865
- if (ms.command) {
1866
- mcpServerConfig[mcpNames[mi]] = { command: ms.command, args: ms.args || [] };
1867
- if (ms.env && Object.keys(ms.env).length > 0) {
1868
- mcpServerConfig[mcpNames[mi]].env = ms.env;
1869
- }
1870
- }
1871
- }
1872
- } catch (e) {
1873
- console.error("[codex] Failed to read local MCP config:", e.message);
1874
- }
1875
1858
 
1876
1859
  // Track 2: Add clay-tools bridge server for in-app + remote MCP tools.
1877
1860
  var bridgePath = require("path").join(__dirname, "..", "mcp-bridge-server.js");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "4.8.2",
3
+ "version": "5.0.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"
@@ -49,14 +49,18 @@
49
49
  "dependencies": {
50
50
  "@anthropic-ai/claude-agent-sdk": "^0.3.287",
51
51
  "@lydell/node-pty": "^1.2.0-beta.3",
52
+ "@modelcontextprotocol/sdk": "^1.32.1",
52
53
  "@openai/codex": "^0.160.0",
53
54
  "@seald-io/nedb": "^4.1.2",
54
55
  "imapflow": "^1.7.8",
56
+ "ipaddr.js": "^2.5.0",
55
57
  "nodemailer": "^9.1.1",
56
58
  "playwright": "1.63.0",
57
59
  "qrcode-terminal": "^0.12.0",
60
+ "undici": "^6.13.0",
58
61
  "web-push": "^3.6.7",
59
- "ws": "^8.18.0"
62
+ "ws": "^8.18.0",
63
+ "zod": "^4.6.5"
60
64
  },
61
65
  "license": "AGPL-3.0-only",
62
66
  "engines": {