clay-server 2.33.1 → 2.34.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.
@@ -20,6 +20,14 @@ var todoItems = [];
20
20
  var todoWidgetEl = null;
21
21
  var todoWidgetVisible = true; // whether in-chat widget is in viewport
22
22
  var todoObserver = null;
23
+ var todoMeta = {
24
+ variant: "tasks",
25
+ title: "Tasks",
26
+ icon: "list-checks",
27
+ showProgress: true,
28
+ showCompletedCount: true,
29
+ stickyEnabled: true,
30
+ };
23
31
 
24
32
  // --- Tool tracking ---
25
33
  var tools = {};
@@ -1262,6 +1270,7 @@ function todoStatusIcon(status) {
1262
1270
 
1263
1271
  export function handleTodoWrite(input) {
1264
1272
  if (!input || !Array.isArray(input.todos)) return;
1273
+ todoMeta = normalizeTodoMeta(input.meta);
1265
1274
  todoItems = input.todos.map(function (t, i) {
1266
1275
  return {
1267
1276
  id: t.id || String(i + 1),
@@ -1275,6 +1284,7 @@ export function handleTodoWrite(input) {
1275
1284
 
1276
1285
  export function handleTaskCreate(input) {
1277
1286
  if (!input) return;
1287
+ todoMeta = normalizeTodoMeta();
1278
1288
  var id = String(todoItems.length + 1);
1279
1289
  todoItems.push({
1280
1290
  id: id,
@@ -1287,6 +1297,7 @@ export function handleTaskCreate(input) {
1287
1297
 
1288
1298
  export function handleTaskUpdate(input) {
1289
1299
  if (!input || !input.taskId) return;
1300
+ todoMeta = normalizeTodoMeta();
1290
1301
  for (var i = 0; i < todoItems.length; i++) {
1291
1302
  if (todoItems[i].id === input.taskId) {
1292
1303
  if (input.status === "deleted") {
@@ -1302,11 +1313,33 @@ export function handleTaskUpdate(input) {
1302
1313
  renderTodoWidget();
1303
1314
  }
1304
1315
 
1316
+ function normalizeTodoMeta(meta) {
1317
+ if (meta && meta.variant === "plan") {
1318
+ return {
1319
+ variant: "plan",
1320
+ title: "Plan",
1321
+ icon: "map",
1322
+ showProgress: false,
1323
+ showCompletedCount: false,
1324
+ stickyEnabled: false,
1325
+ };
1326
+ }
1327
+ return {
1328
+ variant: "tasks",
1329
+ title: "Tasks",
1330
+ icon: "list-checks",
1331
+ showProgress: true,
1332
+ showCompletedCount: true,
1333
+ stickyEnabled: true,
1334
+ };
1335
+ }
1336
+
1305
1337
  function renderTodoWidget() {
1306
1338
  if (todoItems.length === 0) {
1307
1339
  if (todoWidgetEl) { todoWidgetEl.remove(); todoWidgetEl = null; }
1308
1340
  if (todoObserver) { todoObserver.disconnect(); todoObserver = null; }
1309
1341
  todoWidgetVisible = true;
1342
+ todoMeta = normalizeTodoMeta();
1310
1343
  updateTodoSticky();
1311
1344
  return;
1312
1345
  }
@@ -1316,19 +1349,26 @@ function renderTodoWidget() {
1316
1349
  todoWidgetEl = document.createElement("div");
1317
1350
  todoWidgetEl.className = "todo-widget";
1318
1351
  }
1352
+ todoWidgetEl.className = "todo-widget" + (todoMeta.variant === "plan" ? " todo-widget-plan" : "");
1319
1353
 
1320
1354
  var completed = 0;
1321
1355
  for (var i = 0; i < todoItems.length; i++) {
1322
1356
  if (todoItems[i].status === "completed") completed++;
1323
1357
  }
1324
1358
 
1359
+ var countText = todoMeta.showCompletedCount
1360
+ ? (completed + "/" + todoItems.length)
1361
+ : (todoItems.length + " " + (todoItems.length === 1 ? "step" : "steps"));
1362
+
1325
1363
  var html = '<div class="todo-header">' +
1326
- '<span class="todo-header-icon">' + iconHtml("list-checks") + '</span>' +
1327
- '<span class="todo-header-title">Tasks</span>' +
1328
- '<span class="todo-header-count">' + completed + '/' + todoItems.length + '</span>' +
1364
+ '<span class="todo-header-icon">' + iconHtml(todoMeta.icon) + '</span>' +
1365
+ '<span class="todo-header-title">' + todoMeta.title + '</span>' +
1366
+ '<span class="todo-header-count">' + countText + '</span>' +
1329
1367
  '</div>';
1330
- html += '<div class="todo-progress"><div class="todo-progress-bar" style="width:' +
1331
- (todoItems.length > 0 ? Math.round(completed / todoItems.length * 100) : 0) + '%"></div></div>';
1368
+ if (todoMeta.showProgress) {
1369
+ html += '<div class="todo-progress"><div class="todo-progress-bar" style="width:' +
1370
+ (todoItems.length > 0 ? Math.round(completed / todoItems.length * 100) : 0) + '%"></div></div>';
1371
+ }
1332
1372
  html += '<div class="todo-items">';
1333
1373
  for (var i = 0; i < todoItems.length; i++) {
1334
1374
  var t = todoItems[i];
@@ -1369,6 +1409,10 @@ function setupTodoObserver() {
1369
1409
  function updateTodoStickyVisibility() {
1370
1410
  var stickyEl = document.getElementById("todo-sticky");
1371
1411
  if (!stickyEl) return;
1412
+ if (!todoMeta.stickyEnabled) {
1413
+ stickyEl.classList.add("hidden");
1414
+ return;
1415
+ }
1372
1416
 
1373
1417
  if (todoWidgetVisible) {
1374
1418
  stickyEl.classList.add("hidden");
@@ -1387,6 +1431,11 @@ function updateTodoStickyVisibility() {
1387
1431
  function updateTodoSticky() {
1388
1432
  var stickyEl = document.getElementById("todo-sticky");
1389
1433
  if (!stickyEl) return;
1434
+ if (!todoMeta.stickyEnabled) {
1435
+ stickyEl.classList.add("hidden");
1436
+ stickyEl.innerHTML = "";
1437
+ return;
1438
+ }
1390
1439
 
1391
1440
  // Hide if no active tasks (all completed or empty)
1392
1441
  var hasActive = false;
@@ -2195,6 +2244,7 @@ export function saveToolState() {
2195
2244
  tools: tools,
2196
2245
  currentThinking: currentThinking,
2197
2246
  todoWidgetEl: todoWidgetEl,
2247
+ todoMeta: todoMeta,
2198
2248
  inPlanMode: inPlanMode,
2199
2249
  planContent: planContent,
2200
2250
  currentPlanCardEl: currentPlanCardEl,
@@ -2209,6 +2259,7 @@ export function restoreToolState(saved) {
2209
2259
  tools = saved.tools;
2210
2260
  currentThinking = saved.currentThinking;
2211
2261
  todoWidgetEl = saved.todoWidgetEl;
2262
+ todoMeta = saved.todoMeta || normalizeTodoMeta();
2212
2263
  inPlanMode = saved.inPlanMode;
2213
2264
  planContent = saved.planContent;
2214
2265
  currentPlanCardEl = saved.currentPlanCardEl || null;
@@ -2229,6 +2280,7 @@ export function resetToolState() {
2229
2280
  planContent = null;
2230
2281
  currentPlanCardEl = null;
2231
2282
  todoItems = [];
2283
+ todoMeta = normalizeTodoMeta();
2232
2284
  todoWidgetEl = null;
2233
2285
  todoWidgetVisible = true;
2234
2286
  if (todoObserver) { todoObserver.disconnect(); todoObserver = null; }
package/lib/sdk-bridge.js CHANGED
@@ -1,7 +1,8 @@
1
1
  const crypto = require("crypto");
2
2
  var fs = require("fs");
3
3
  var path = require("path");
4
- var { execSync } = require("child_process");
4
+ var execSync = require("child_process").execSync;
5
+ var execFileSync = require("child_process").execFileSync;
5
6
  var usersModule = require("./users");
6
7
  var { getCodexConfig } = require("./codex-defaults");
7
8
  var { splitShellSegments, attachSkillDiscovery } = require("./sdk-skill-discovery");
@@ -373,12 +374,12 @@ function createSDKBridge(opts) {
373
374
  // Create and chown the project directory once
374
375
  if (!fs.existsSync(dstDir)) {
375
376
  fs.mkdirSync(dstDir, { recursive: true });
376
- try { require("child_process").execSync("chown -R " + uid + " " + JSON.stringify(path.join(linuxUserHome, ".claude"))); } catch (e2) {}
377
+ try { execFileSync("chown", ["-R", String(uid), path.join(linuxUserHome, ".claude")]); } catch (e2) {}
377
378
  } else {
378
379
  try {
379
380
  var dirStat = fs.statSync(dstDir);
380
381
  if (dirStat.uid !== uid) {
381
- require("child_process").execSync("chown " + uid + " " + JSON.stringify(dstDir));
382
+ execFileSync("chown", [String(uid), dstDir]);
382
383
  }
383
384
  } catch (e2) {}
384
385
  }
@@ -389,7 +390,7 @@ function createSDKBridge(opts) {
389
390
  var dstFile = path.join(dstDir, sessionFileName);
390
391
  if (fs.existsSync(srcFile) && !fs.existsSync(dstFile)) {
391
392
  fs.copyFileSync(srcFile, dstFile);
392
- try { require("child_process").execSync("chown " + uid + " " + JSON.stringify(dstFile)); } catch (e2) {}
393
+ try { execFileSync("chown", [String(uid), dstFile]); } catch (e2) {}
393
394
  console.log("[sdk-bridge] Pre-copied CLI session " + session.cliSessionId + " to " + linuxUser);
394
395
  }
395
396
  }
@@ -447,6 +448,12 @@ function createSDKBridge(opts) {
447
448
  }
448
449
  }
449
450
 
451
+ // Auto-approve Mate datastore tools. These are scoped to the active Mate
452
+ // project and already enforce SQL policy server-side.
453
+ if (toolName.indexOf("mcp__clay-datastore__") === 0) {
454
+ return { behavior: "allow", updatedInput: input };
455
+ }
456
+
450
457
  // Auto-approve remote MCP tools that the user explicitly enabled in project settings.
451
458
  // These are user-owned local MCP servers, so no additional permission prompt needed.
452
459
  if (toolName.indexOf("mcp__") === 0 && getRemoteMcpServers) {
@@ -646,7 +653,7 @@ function createSDKBridge(opts) {
646
653
  */
647
654
  function findConflictingClaude() {
648
655
  try {
649
- var output = execSync("ps ax -o pid,command 2>/dev/null", { encoding: "utf8", timeout: 5000 });
656
+ var output = execFileSync("ps", ["ax", "-o", "pid,command"], { encoding: "utf8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
650
657
  var lines = output.trim().split("\n");
651
658
  var candidates = [];
652
659
  for (var i = 1; i < lines.length; i++) { // skip header
@@ -691,7 +698,7 @@ function createSDKBridge(opts) {
691
698
  */
692
699
  function isClaudeProcess(pid) {
693
700
  try {
694
- var output = execSync("ps -p " + pid + " -o command= 2>/dev/null", { encoding: "utf8", timeout: 3000 }).trim();
701
+ var output = execFileSync("ps", ["-p", String(pid), "-o", "command="], { encoding: "utf8", timeout: 3000, stdio: ["pipe", "pipe", "pipe"] }).trim();
695
702
  return /\/claude(\s|$)/.test(output) || /^claude(\s|$)/.test(output);
696
703
  } catch (e) {
697
704
  return false;
@@ -1349,17 +1356,17 @@ function createSDKBridge(opts) {
1349
1356
  // Detect which vendor binaries are installed for this user.
1350
1357
  // In multi-user mode, runs checks as the specific Linux user.
1351
1358
  function detectInstalledVendors(linuxUser) {
1352
- var execSync = require("child_process").execSync;
1359
+ var execFileSync = require("child_process").execFileSync;
1353
1360
  var fs = require("fs");
1354
- var path = require("path");
1355
1361
  var result = [];
1356
1362
 
1357
- function tryExec(cmd) {
1363
+ function tryLookup(name) {
1358
1364
  try {
1359
1365
  if (linuxUser) {
1360
- execSync("su - " + linuxUser + " -c " + JSON.stringify(cmd), { timeout: 3000, stdio: ["pipe", "pipe", "pipe"] });
1366
+ execFileSync("su", ["-", linuxUser, "-c", "which " + name], { timeout: 3000, stdio: ["pipe", "pipe", "pipe"] });
1361
1367
  } else {
1362
- execSync(cmd, { timeout: 3000, stdio: ["pipe", "pipe", "pipe"] });
1368
+ if (process.platform === "win32") execFileSync("where", [name], { timeout: 3000, stdio: ["pipe", "pipe", "pipe"] });
1369
+ else execFileSync("which", [name], { timeout: 3000, stdio: ["pipe", "pipe", "pipe"] });
1363
1370
  }
1364
1371
  return true;
1365
1372
  } catch (e) {
@@ -1368,14 +1375,14 @@ function createSDKBridge(opts) {
1368
1375
  }
1369
1376
 
1370
1377
  // Claude: check if binary is in PATH
1371
- if (tryExec("which claude")) result.push("claude");
1378
+ if (tryLookup("claude")) result.push("claude");
1372
1379
 
1373
1380
  // Codex: check bundled binary or PATH
1374
1381
  var codexBin = null;
1375
1382
  try {
1376
1383
  codexBin = require("./yoke/codex-app-server").findCodexPath();
1377
1384
  } catch (e) {}
1378
- if ((codexBin && fs.existsSync(codexBin)) || tryExec("which codex")) result.push("codex");
1385
+ if ((codexBin && fs.existsSync(codexBin)) || tryLookup("codex")) result.push("codex");
1379
1386
 
1380
1387
  return result;
1381
1388
  }
@@ -234,7 +234,13 @@ function attachMessageProcessor(ctx) {
234
234
  type: "tool_executing",
235
235
  id: parsed.turnId || "codex-plan",
236
236
  name: "TodoWrite",
237
- input: { todos: todos },
237
+ input: {
238
+ todos: todos,
239
+ meta: {
240
+ variant: "plan",
241
+ title: parsed.title || "Plan",
242
+ },
243
+ },
238
244
  });
239
245
 
240
246
  } else if (parsed.yokeType === "plan_content") {
package/lib/updater.js CHANGED
@@ -1,5 +1,5 @@
1
1
  const https = require("https");
2
- const { execSync, spawn } = require("child_process");
2
+ const { execFileSync, spawn } = require("child_process");
3
3
 
4
4
  // ANSI helpers (mirrors cli.js)
5
5
  var isBasicTerm = process.env.TERM_PROGRAM === "Apple_Terminal";
@@ -84,7 +84,7 @@ function isNewer(latest, current) {
84
84
  function performUpdate(channel) {
85
85
  var tag = channel === "beta" ? "beta" : "latest";
86
86
  try {
87
- execSync("npm install -g clay-server@" + tag, { stdio: "pipe" });
87
+ execFileSync("npm", ["install", "-g", "clay-server@" + tag], { stdio: "pipe" });
88
88
  return true;
89
89
  } catch (e) {
90
90
  return false;
package/lib/users.js CHANGED
@@ -1,7 +1,7 @@
1
1
  var fs = require("fs");
2
2
  var path = require("path");
3
3
  var crypto = require("crypto");
4
- var { execSync } = require("child_process");
4
+ var execFileSync = require("child_process").execFileSync;
5
5
  var { CONFIG_DIR } = require("./config");
6
6
  var { attachAuth } = require("./users-auth");
7
7
  var { DEFAULT_PERMISSIONS, ALL_PERMISSIONS, attachPermissions } = require("./users-permissions");
@@ -243,7 +243,7 @@ function updateLinuxUser(userId, linuxUsername) {
243
243
 
244
244
  // Validate Linux user exists
245
245
  try {
246
- execSync("id " + linuxUsername, { encoding: "utf8", timeout: 5000 });
246
+ execFileSync("id", [linuxUsername], { encoding: "utf8", timeout: 5000, stdio: "pipe" });
247
247
  } catch (e) {
248
248
  return { error: "Linux user '" + linuxUsername + "' does not exist" };
249
249
  }
package/lib/ws-schema.js CHANGED
@@ -454,6 +454,20 @@ var schema = {
454
454
  "memory_delete": { direction: "c2s", handler: "lib/project.js", description: "Delete a memory entry by index" },
455
455
  "memory_deleted": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Memory entry was deleted" },
456
456
 
457
+ // -----------------------------------------------------------------------
458
+ // Mate datastore
459
+ // -----------------------------------------------------------------------
460
+ "mate_db_tables": { direction: "c2s", handler: "lib/project-mate-datastore.js", description: "List schema objects in the current Mate datastore" },
461
+ "mate_db_describe": { direction: "c2s", handler: "lib/project-mate-datastore.js", description: "Describe a table or view in the current Mate datastore" },
462
+ "mate_db_query": { direction: "c2s", handler: "lib/project-mate-datastore.js", description: "Run read-only SQL against the current Mate datastore" },
463
+ "mate_db_exec": { direction: "c2s", handler: "lib/project-mate-datastore.js", description: "Run schema or write SQL against the current Mate datastore" },
464
+ "mate_db_tables_result": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Schema objects returned from a Mate datastore" },
465
+ "mate_db_describe_result": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Table description returned from a Mate datastore" },
466
+ "mate_db_query_result": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Query results from a Mate datastore" },
467
+ "mate_db_exec_result": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Execution summary from a Mate datastore" },
468
+ "mate_db_error": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Mate datastore error" },
469
+ "mate_db_change": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Mate datastore changed" },
470
+
457
471
  // -----------------------------------------------------------------------
458
472
  // Loop (automated task runner)
459
473
  // -----------------------------------------------------------------------
@@ -164,6 +164,7 @@ function flattenEvent(notification, state) {
164
164
  yokeType: "plan_updated",
165
165
  turnId: params.turnId || null,
166
166
  explanation: params.explanation || "",
167
+ title: "Plan",
167
168
  plan: Array.isArray(params.plan) ? params.plan.map(function(step) {
168
169
  return {
169
170
  step: step && step.step ? step.step : "",
package/lib/yoke/index.js CHANGED
@@ -82,6 +82,18 @@ function checkAuth() {
82
82
  if (_authCache) return _authCache;
83
83
 
84
84
  var execSync = require("child_process").execSync;
85
+ var execFileSync = require("child_process").execFileSync;
86
+
87
+ function lookupBinary(name) {
88
+ try {
89
+ if (process.platform === "win32") {
90
+ return execFileSync("where", [name], { timeout: 3000, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim().split(/\r?\n/)[0] || null;
91
+ }
92
+ return execFileSync("which", [name], { timeout: 3000, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim().split(/\r?\n/)[0] || null;
93
+ } catch (e) {
94
+ return null;
95
+ }
96
+ }
85
97
 
86
98
  function parseClaudeAuthStatusJson(out) {
87
99
  if (!out) return null;
@@ -127,27 +139,20 @@ function checkAuth() {
127
139
  function resolveCodexBinary() {
128
140
  var fs = require("fs");
129
141
  var findCodexPath = require("./codex-app-server").findCodexPath;
130
- var pathProbeCmd = process.platform === "win32" ? "where codex" : "which codex";
131
142
 
132
143
  try {
133
144
  var codexBin = findCodexPath();
134
145
  if (codexBin && fs.existsSync(codexBin)) return codexBin;
135
146
  } catch (e) {}
136
147
 
137
- try {
138
- var whichOut = execSync(pathProbeCmd, { timeout: 3000, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
139
- var codexPath = whichOut.trim().split(/\r?\n/)[0];
140
- if (codexPath) return codexPath;
141
- } catch (e) {}
142
-
143
- return null;
148
+ return lookupBinary("codex");
144
149
  }
145
150
 
146
151
  function checkCodex() {
147
152
  try {
148
153
  var codexBin = resolveCodexBinary();
149
154
  if (!codexBin) return false;
150
- execSync('"' + codexBin + '" login status', { timeout: 5000, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
155
+ execFileSync(codexBin, ["login", "status"], { timeout: 5000, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
151
156
  return true;
152
157
  } catch (e) {
153
158
  return false;
@@ -164,10 +169,11 @@ function checkAuth() {
164
169
  */
165
170
  function checkInstalled() {
166
171
  var fs = require("fs");
167
- var execSync = require("child_process").execSync;
172
+ var execFileSync = require("child_process").execFileSync;
168
173
  var result = { claude: false, codex: false };
169
174
  try {
170
- execSync("which claude", { timeout: 3000, stdio: ["pipe", "pipe", "pipe"] });
175
+ if (process.platform === "win32") execFileSync("where", ["claude"], { timeout: 3000, stdio: ["pipe", "pipe", "pipe"] });
176
+ else execFileSync("which", ["claude"], { timeout: 3000, stdio: ["pipe", "pipe", "pipe"] });
171
177
  result.claude = true;
172
178
  } catch (e) {}
173
179
  try {
@@ -181,8 +187,9 @@ function checkInstalled() {
181
187
  }
182
188
  } catch (e) {}
183
189
 
184
- var pathProbeCmd = process.platform === "win32" ? "where codex" : "which codex";
185
- var whichOut = execSync(pathProbeCmd, { timeout: 3000, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
190
+ var whichOut = process.platform === "win32"
191
+ ? execFileSync("where", ["codex"], { timeout: 3000, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] })
192
+ : execFileSync("which", ["codex"], { timeout: 3000, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
186
193
  if (whichOut.trim()) result.codex = true;
187
194
  } catch (e) {}
188
195
  return result;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "2.33.1",
3
+ "version": "2.34.0-beta.1",
4
4
  "description": "Self-hosted Claude Code in your browser. Multi-session, multi-user, push notifications.",
5
5
  "bin": {
6
6
  "clay-server": "./bin/cli.js",