clay-server 4.8.0-beta.1 → 4.8.0-beta.3

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 (60) hide show
  1. package/lib/bundled-skills/clay-agent-browser/SKILL.md +36 -0
  2. package/lib/clay-skill-install.js +33 -0
  3. package/lib/clay-skill-storage.js +49 -0
  4. package/lib/linear-api.js +62 -0
  5. package/lib/linear-store.js +52 -0
  6. package/lib/project-http.js +14 -5
  7. package/lib/project-linear-launch.js +70 -0
  8. package/lib/project-session-linear.js +197 -0
  9. package/lib/project-shared-browser.js +256 -0
  10. package/lib/project.js +74 -2
  11. package/lib/public/app.js +7 -0
  12. package/lib/public/css/filebrowser.css +2 -25
  13. package/lib/public/css/issues.css +1 -14
  14. package/lib/public/css/linear.css +35 -0
  15. package/lib/public/css/mcp-skills-workbench.css +51 -0
  16. package/lib/public/css/project-logs.css +0 -11
  17. package/lib/public/css/right-workbench.css +28 -0
  18. package/lib/public/css/scheduled-tasks.css +1 -1
  19. package/lib/public/css/settings-ui.css +295 -0
  20. package/lib/public/css/shared-browser-skins.css +28 -0
  21. package/lib/public/css/shared-browser.css +91 -0
  22. package/lib/public/css/sticky-notes.css +0 -11
  23. package/lib/public/index.html +50 -22
  24. package/lib/public/modules/app-messages.js +6 -0
  25. package/lib/public/modules/app-projects.js +1 -1
  26. package/lib/public/modules/extension-settings.js +110 -0
  27. package/lib/public/modules/issues.js +4 -1
  28. package/lib/public/modules/linear-panel.js +120 -0
  29. package/lib/public/modules/linear-settings.js +61 -0
  30. package/lib/public/modules/mcp-skills-workbench.js +67 -0
  31. package/lib/public/modules/mcp-ui.js +9 -12
  32. package/lib/public/modules/notifications.js +2 -107
  33. package/lib/public/modules/project-settings.js +19 -1
  34. package/lib/public/modules/session-github.js +15 -5
  35. package/lib/public/modules/shared-browser-activity.js +66 -0
  36. package/lib/public/modules/shared-browser-input.js +73 -0
  37. package/lib/public/modules/shared-browser-tabs.js +33 -0
  38. package/lib/public/modules/shared-browser-viewport-menu.js +76 -0
  39. package/lib/public/modules/shared-browser.js +214 -0
  40. package/lib/public/modules/sidebar-mobile.js +3 -3
  41. package/lib/public/modules/skills-actions.js +247 -0
  42. package/lib/public/modules/skills-detail.js +148 -0
  43. package/lib/public/modules/skills-state.js +10 -0
  44. package/lib/public/modules/skills.js +64 -459
  45. package/lib/public/modules/sticky-notes.js +23 -6
  46. package/lib/public/modules/tool-palette-order.js +11 -13
  47. package/lib/public/modules/user-settings.js +4 -0
  48. package/lib/public/style.css +8 -0
  49. package/lib/sdk-bridge.js +22 -1
  50. package/lib/server-linear.js +50 -0
  51. package/lib/server.js +5 -0
  52. package/lib/sessions.js +7 -0
  53. package/lib/shared-browser-activity-history.js +17 -0
  54. package/lib/shared-browser-policy.js +26 -0
  55. package/lib/shared-browser-runtime.js +82 -0
  56. package/lib/shared-browser-tools.js +21 -0
  57. package/lib/shared-browser-worker.js +160 -0
  58. package/lib/ws-schema.js +10 -0
  59. package/lib/yoke/skill-discovery.js +22 -2
  60. package/package.json +1 -1
@@ -0,0 +1,160 @@
1
+ // Isolated browser process. Its OS identity matches the owning Driver session.
2
+ var chromium = require("playwright").chromium;
3
+ var policy = require("./shared-browser-policy");
4
+ var browser = null;
5
+ var context = null;
6
+ var page = null;
7
+ var closing = false;
8
+ var viewing = false;
9
+ var epoch = 0;
10
+ var queue = Promise.resolve();
11
+ var pending = 0;
12
+ var timer = null;
13
+ var activeActivity = null;
14
+ var pointerSequence = 0;
15
+ var viewport = { width: 1280, height: 800 };
16
+ function emit(message) {
17
+ if (!closing && process.stdout.writableLength < 3 * 1024 * 1024) process.stdout.write(JSON.stringify(message) + "\n");
18
+ }
19
+ function state() {
20
+ return { url: page ? policy.visibleUrl(page.url()) : "", width: viewport.width, height: viewport.height };
21
+ }
22
+ async function screenshot() {
23
+ return (await page.screenshot({ type: "jpeg", quality: 70, caret: "initial", timeout: 5000 })).toString("base64");
24
+ }
25
+ async function capture() {
26
+ if (closing) return;
27
+ try {
28
+ if (viewing && page && !page.isClosed()) {
29
+ var current = page;
30
+ var data = await screenshot();
31
+ if (current === page) emit(Object.assign({ type: "frame", data: data }, state()));
32
+ }
33
+ } catch (e) {}
34
+ if (!closing) timer = setTimeout(capture, 250);
35
+ }
36
+ async function start() {
37
+ var channels = ["chrome", "msedge", null];
38
+ for (var i = 0; i < channels.length; i++) {
39
+ try { browser = await chromium.launch({ channel: channels[i] || undefined, headless: true, chromiumSandbox: true, timeout: 15000 }); break; }
40
+ catch (e) { if (closing) return; }
41
+ }
42
+ if (!browser) throw new Error("Install Chrome or run npx playwright install chromium on the Clay host. Linux also needs browser libraries and sandbox support.");
43
+ if (closing) { await browser.close(); return; }
44
+ var cdp = await browser.newBrowserCDPSession();
45
+ var version = await cdp.send("Browser.getVersion");
46
+ await cdp.detach();
47
+ context = await browser.newContext({ viewport: viewport, acceptDownloads: false, userAgent: version.userAgent.replace("HeadlessChrome/", "Chrome/") });
48
+ await context.addInitScript(require("./vendor-login-browser-passkeys").disableLoginPasskeys);
49
+ await context.route("**/*", function (route) {
50
+ var protocol = new URL(route.request().url()).protocol;
51
+ return ["http:", "https:"].includes(protocol) ? route.continue() : route.abort();
52
+ });
53
+ context.on("page", function (opened) {
54
+ page = opened;
55
+ opened.setDefaultTimeout(10000);
56
+ opened.on("dialog", function (dialog) { dialog.dismiss().catch(function () {}); });
57
+ opened.on("download", function (download) { download.cancel().catch(function () {}); });
58
+ opened.on("framenavigated", function (frame) { if (frame === opened.mainFrame() && page === opened) emit(Object.assign({ type: "state" }, state())); });
59
+ opened.on("close", function () { var pages = context.pages(); page = pages.length ? pages[pages.length - 1] : null; emit(Object.assign({ type: "state" }, state())); });
60
+ });
61
+ page = await context.newPage();
62
+ emit(Object.assign({ type: "ready" }, state()));
63
+ capture();
64
+ }
65
+ function reportActivity(phase) {
66
+ if (activeActivity) emit({ type: "activity", epoch: activeActivity.epoch, generation: activeActivity.generation,
67
+ activity: { id: activeActivity.id, text: activeActivity.text, phase: phase } });
68
+ }
69
+ function reportPointer(x, y, clicked) {
70
+ if (activeActivity) emit({ type: "pointer", epoch: activeActivity.epoch, generation: activeActivity.generation,
71
+ pointer: { x: x, y: y, width: viewport.width, height: viewport.height, clicked: !!clicked, sequence: ++pointerSequence } });
72
+ }
73
+ function activityText(message) {
74
+ var defaults = { inspect: "I'll take a look at the page to see what's there.", navigate: "I'll open this page so we can take a look.", back: "I'll go back to the previous page.", forward: "I'll move forward to the next page.", reload: "I'll refresh the page to see its latest state.", click: "I'll click this control and see what happens.", text: "I'll enter the text in this field.", select: "I'll choose this option in the form.", key: "I'll use the keyboard for this step.", wheel: "I'll scroll a little to see more of the page.", resize: "I'll change the screen size to check the layout." };
75
+ return message.activity.intent || defaults[message.action === "inspect" ? "inspect" : message.event.kind] || "I'll work through the next step in the browser.";
76
+ }
77
+ async function action(event) {
78
+ if (!page || page.isClosed()) page = await context.newPage();
79
+ event = policy.validateAction(event);
80
+ if (event.kind === "navigate") await page.goto(event.url, { waitUntil: "domcontentloaded", timeout: 20000 });
81
+ else if (event.kind === "back") await page.goBack({ waitUntil: "domcontentloaded", timeout: 20000 });
82
+ else if (event.kind === "forward") await page.goForward({ waitUntil: "domcontentloaded", timeout: 20000 });
83
+ else if (event.kind === "reload") await page.reload({ waitUntil: "domcontentloaded", timeout: 20000 });
84
+ else if (event.kind === "click" && event.selector) {
85
+ var target = page.locator(event.selector);
86
+ await target.click({ trial: true });
87
+ var box = await target.boundingBox();
88
+ if (box) {
89
+ var x = Math.max(0, Math.min(viewport.width - 1, box.x + box.width / 2));
90
+ var y = Math.max(0, Math.min(viewport.height - 1, box.y + box.height / 2));
91
+ await page.mouse.move(x, y);
92
+ reportPointer(x, y, true);
93
+ await target.click({ position: { x: x - box.x, y: y - box.y } });
94
+ } else await target.click();
95
+ }
96
+ else if (["move", "down", "up", "click"].includes(event.kind)) {
97
+ await page.mouse.move(Math.max(0, Math.min(viewport.width - 1, event.x)), Math.max(0, Math.min(viewport.height - 1, event.y)));
98
+ reportPointer(Math.max(0, Math.min(viewport.width - 1, event.x)), Math.max(0, Math.min(viewport.height - 1, event.y)), event.kind === "click" || event.kind === "down");
99
+ if (event.kind === "down") await page.mouse.down();
100
+ if (event.kind === "up") await page.mouse.up();
101
+ if (event.kind === "click") await page.mouse.click(event.x, event.y);
102
+ } else if (event.kind === "text") {
103
+ if (event.selector) await page.locator(event.selector).fill(event.text);
104
+ else await page.keyboard.insertText(event.text);
105
+ } else if (event.kind === "select") await page.locator(event.selector).selectOption(event.text);
106
+ else if (event.kind === "key") await page.keyboard.press(event.key);
107
+ else if (event.kind === "wheel") await page.mouse.wheel(0, Math.max(-2000, Math.min(2000, event.deltaY)));
108
+ else if (event.kind === "resize") {
109
+ viewport = { width: Math.round(event.width), height: Math.round(event.height) };
110
+ await page.setViewportSize(viewport);
111
+ }
112
+ return state();
113
+ }
114
+ async function command(message) {
115
+ if (message.epoch !== epoch) throw new Error("Browser control changed; this action was cancelled");
116
+ activeActivity = message.activity ? { id: message.id, epoch: message.epoch, generation: message.activity.generation, text: activityText(message) } : null;
117
+ if (message.event && ["navigate", "back", "forward", "reload", "resize"].includes(message.event.kind) && activeActivity) {
118
+ emit({ type: "pointer", epoch: message.epoch, generation: activeActivity.generation, pointer: null });
119
+ }
120
+ reportActivity("running");
121
+ if (message.action === "inspect") {
122
+ if (!page || page.isClosed()) throw new Error("Open a page first");
123
+ return Object.assign(state(), { snapshot: (await page.locator("body").ariaSnapshot()).slice(0, 24000), image: await screenshot() });
124
+ }
125
+ return action(message.event);
126
+ }
127
+ async function close() {
128
+ if (closing) return;
129
+ closing = true;
130
+ clearTimeout(timer);
131
+ if (browser) await browser.close().catch(function () {});
132
+ process.exit(0);
133
+ }
134
+ var lines = require("readline").createInterface({ input: process.stdin });
135
+ lines.on("line", function (line) {
136
+ if (line.length > 30000 || closing) return;
137
+ var msg;
138
+ try { msg = JSON.parse(line); } catch (e) { return; }
139
+ if (msg.type === "start") {
140
+ if (!browser) queue = queue.then(start).catch(function (e) { emit({ type: "error", error: e.message }); });
141
+ } else if (msg.type === "viewing") viewing = !!msg.value;
142
+ else if (msg.type === "control") {
143
+ epoch = msg.epoch;
144
+ queue = queue.then(async function () {
145
+ if (page) await page.mouse.up().catch(function () {});
146
+ emit({ type: "result", id: msg.id, value: { epoch: msg.epoch } });
147
+ });
148
+ } else if (msg.type === "command") {
149
+ if (pending >= 80) { emit({ type: "result", id: msg.id, error: "Browser is busy; retry shortly" }); return; }
150
+ pending++;
151
+ queue = queue.then(function () { return command(msg); }).then(function (value) {
152
+ reportActivity("complete"); activeActivity = null;
153
+ emit({ type: "result", id: msg.id, value: value });
154
+ emit(Object.assign({ type: "state" }, state()));
155
+ }, function (e) { reportActivity("failed"); activeActivity = null; emit({ type: "result", id: msg.id, error: e.message }); }).finally(function () { pending--; });
156
+ }
157
+ });
158
+ lines.on("close", close);
159
+ process.on("SIGTERM", close);
160
+ process.on("SIGINT", close);
package/lib/ws-schema.js CHANGED
@@ -219,6 +219,16 @@ var schema = {
219
219
  // Auth
220
220
  // -----------------------------------------------------------------------
221
221
  "auth_required": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Authentication required (e.g. API key needed)" },
222
+ "shared_browser_state_request": { direction: "c2s", handler: "lib/project-shared-browser.js", description: "Session-owned shared browser state_request" },
223
+ "shared_browser_open": { direction: "c2s", handler: "lib/project-shared-browser.js", description: "Session-owned shared browser open" },
224
+ "shared_browser_view": { direction: "c2s", handler: "lib/project-shared-browser.js", description: "Session-owned shared browser view" },
225
+ "shared_browser_control": { direction: "c2s", handler: "lib/project-shared-browser.js", description: "Session-owned shared browser control" },
226
+ "shared_browser_close": { direction: "c2s", handler: "lib/project-shared-browser.js", description: "Close one exact browser tab" },
227
+ "shared_browser_end": { direction: "c2s", handler: "lib/project-shared-browser.js", description: "Session-owned shared browser end" },
228
+ "shared_browser_input": { direction: "c2s", handler: "lib/project-shared-browser.js", description: "Session-owned shared browser input" },
229
+ "shared_browser_state": { direction: "s2c", handler: "lib/public/modules/shared-browser.js", description: "Private shared browser state" },
230
+ "shared_browser_frame": { direction: "s2c", handler: "lib/public/modules/shared-browser.js", description: "Private shared browser frame" },
231
+ "shared_browser_error": { direction: "s2c", handler: "lib/public/modules/shared-browser.js", description: "Private shared browser error" },
222
232
  "vendor_login_start": { direction: "c2s", handler: "lib/project-vendor-login.js", description: "Start (or re-attach to) the per-vendor login terminal" },
223
233
  "vendor_login_browser_attach": { direction: "c2s", handler: "lib/vendor-login-browser-flow.js", description: "Attach the sign-in owner to their private browser" },
224
234
  "vendor_login_browser_input": { direction: "c2s", handler: "lib/vendor-login-browser-flow.js", description: "Send human input to the private sign-in browser" },
@@ -1,11 +1,12 @@
1
1
  // Cross-vendor skill discovery
2
2
  // ----------------------------
3
- // Skills remain in their vendor-owned directories. This module only builds a
4
- // merged view so every YOKE adapter can expose the same installed skills.
3
+ // Ordinary skills stay in vendor-owned directories. Clay-specific skills use
4
+ // private roots and are attached only to Clay-owned adapter queries.
5
5
 
6
6
  var fs = require("fs");
7
7
  var os = require("os");
8
8
  var path = require("path");
9
+ var clayStorage = require("../clay-skill-storage");
9
10
 
10
11
  function defaultHomeDir() {
11
12
  try { return require("../config").REAL_HOME; } catch (e) { return os.homedir(); }
@@ -13,10 +14,12 @@ function defaultHomeDir() {
13
14
 
14
15
  function optionsForOsUser(userInfo, fsAsUser) {
15
16
  if (!userInfo || typeof userInfo.home !== "string" || !userInfo.home) return { disable: true };
17
+ var migrateAsUser = !fsAsUser || fsAsUser === require("../os-users").fsAsUser;
16
18
  fsAsUser = fsAsUser || require("../os-users").fsAsUser;
17
19
  return {
18
20
  homeDir: userInfo.home,
19
21
  scopedHome: true,
22
+ migrationIdentity: migrateAsUser ? userInfo : null,
20
23
  readDir: function (dir) { return fsAsUser("list", { dir: dir }, userInfo).map(function (entry) { return { name: entry.name, isDir: entry.isDir }; }); },
21
24
  readFile: function (file) { return fsAsUser("read", { file: file, readContent: true }, userInfo).content; },
22
25
  realpath: function (file) { return fsAsUser("realpath", { file: file }, userInfo).path; },
@@ -89,6 +92,7 @@ function getSkillRoots(cwd, opts) {
89
92
  var codexHome = env.CODEX_HOME ? path.resolve(homeDir, env.CODEX_HOME) : path.join(homeDir, ".codex");
90
93
  var claudeConfigDir = env.CLAUDE_CONFIG_DIR ? path.resolve(homeDir, env.CLAUDE_CONFIG_DIR) : path.join(homeDir, ".claude");
91
94
  var roots = [
95
+ { path: path.join(__dirname, "../bundled-skills"), source: "clay-builtin" },
92
96
  { path: path.join(homeDir, ".agents", "skills"), source: "agents-user" },
93
97
  { path: path.join(codexHome, "skills"), source: "codex-user" },
94
98
  { path: path.join(codexHome, "skills", ".system"), source: "codex-system-user" },
@@ -101,6 +105,8 @@ function getSkillRoots(cwd, opts) {
101
105
  roots.push({ path: path.join(cwd, ".codex", "skills", ".system"), source: "codex-system-project" });
102
106
  roots.push({ path: path.join(cwd, ".claude", "skills"), source: "claude-project" });
103
107
  }
108
+ roots.push({ path: clayStorage.privateRoot(homeDir), source: "clay-user" });
109
+ if (cwd) roots.push({ path: clayStorage.privateRoot(cwd), source: "clay-project" });
104
110
  return roots;
105
111
  }
106
112
 
@@ -108,6 +114,20 @@ function discoverSkills(cwd, opts) {
108
114
  if (opts && opts.disable) return [];
109
115
  var merged = {};
110
116
  var roots = getSkillRoots(cwd, opts);
117
+ var home = path.dirname(path.dirname(roots.find(function (root) { return root.source === "clay-user"; }).path));
118
+ var legacy = roots.filter(function (root) { return !/^clay-/.test(root.source); }).map(function (root) {
119
+ return { path: root.path, base: root.source === "project" || /-project$/.test(root.source) ? cwd : home };
120
+ });
121
+ // Run filesystem migration as the same OS owner used for skill discovery.
122
+ if (!opts || !opts.readDir || opts.migrationIdentity) {
123
+ try {
124
+ if (opts && opts.migrationIdentity) {
125
+ var identity = opts.migrationIdentity;
126
+ var job = require("../os-users").wrapSpawnAsUser(process.execPath, [require.resolve("../clay-skill-storage"), JSON.stringify(legacy)], { uid: identity.uid, gid: identity.gid, timeout: 10000 });
127
+ require("child_process").execFileSync(job.command, job.args, job.options);
128
+ } else clayStorage.migrate(legacy);
129
+ } catch (error) { console.error("[skills] Clay-only skill migration failed: " + error.message); }
130
+ }
111
131
  for (var i = 0; i < roots.length; i++) scanRoot(roots[i].path, roots[i].source, merged, opts);
112
132
  return Object.keys(merged).sort().map(function(name) { return merged[name]; });
113
133
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "4.8.0-beta.1",
3
+ "version": "4.8.0-beta.3",
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"