clay-server 2.40.0-beta.2 → 2.40.0-beta.4

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
@@ -6,6 +6,7 @@ var execFileSync = require("child_process").execFileSync;
6
6
  var usersModule = require("./users");
7
7
  var { getCodexConfig } = require("./codex-defaults");
8
8
  var { splitShellSegments, attachSkillDiscovery } = require("./sdk-skill-discovery");
9
+ var { isSafeBashSegment } = require("./safe-bash-commands");
9
10
  var { createMessageQueue } = require("./sdk-message-queue");
10
11
  var { attachMessageProcessor } = require("./sdk-message-processor");
11
12
 
@@ -516,61 +517,15 @@ function createSDKBridge(opts) {
516
517
  // Read/Glob/Grep built-in tools which are already auto-approved.
517
518
  if (toolName === "Bash" && input && input.command) {
518
519
  var cmd = input.command.trim();
519
- var safeBashCommands = {
520
- // Navigation (harmless on its own, checked in compound commands below)
521
- cd: true, pushd: true, popd: true,
522
- // File/dir inspection
523
- ls: true, cat: true, head: true, tail: true, wc: true, file: true,
524
- stat: true, find: true, tree: true, du: true, df: true,
525
- readlink: true, realpath: true, basename: true, dirname: true,
526
- // Search
527
- grep: true, rg: true, ag: true, ack: true, fgrep: true, egrep: true,
528
- // Lookup
529
- which: true, type: true, whereis: true, command: true, hash: true,
530
- // Environment/system info
531
- echo: true, printf: true, env: true, printenv: true, pwd: true,
532
- whoami: true, id: true, groups: true,
533
- date: true, uname: true, hostname: true, uptime: true, arch: true,
534
- nproc: true, free: true, lsb_release: true, sw_vers: true,
535
- locale: true, timedatectl: true,
536
- // Version checks (--version only, but first-word check is sufficient
537
- // since these never take destructive subcommands as first arg)
538
- git: true, node: true, npm: true, npx: true, python: true, python3: true, pip: true,
539
- dotnet: true, ruby: true, java: true, javac: true,
540
- rustc: true, cargo: true, gcc: true, clang: true, cmake: true,
541
- go: true, deno: true, bun: true,
542
- // Text processing (pure stdin/stdout, no side effects)
543
- jq: true, yq: true, sort: true, uniq: true, cut: true, tr: true,
544
- awk: true, sed: true, paste: true, column: true, fold: true,
545
- rev: true, tac: true, nl: true, expand: true, unexpand: true,
546
- fmt: true, pr: true, csplit: true, comm: true, join: true,
547
- // Comparison/hashing
548
- diff: true, cmp: true, md5sum: true, sha256sum: true, sha1sum: true,
549
- shasum: true, cksum: true, sum: true, b2sum: true, base64: true,
550
- xxd: true, od: true, hexdump: true,
551
- // Misc read-only
552
- test: true, true: true, false: true, seq: true, yes: true,
553
- sleep: true, tee: true, xargs: true, time: true,
554
- man: true, help: true, info: true, apropos: true,
555
- cal: true, bc: true, expr: true, factor: true,
556
- lsof: true, ps: true, top: true, htop: true, pgrep: true,
557
- netstat: true, ss: true, ifconfig: true, ip: true, dig: true,
558
- nslookup: true, host: true, ping: true, traceroute: true,
559
- curl: true, wget: true, http: true,
560
- };
561
520
  // Split compound commands on operators (&&, ||, ;, |) while respecting
562
- // quoted strings and subshells so that e.g. grep -E "(a|b)" is not split
521
+ // quoted strings and subshells (so grep -E "(a|b)" is not split), then
522
+ // require every segment to pass the shared safe-command policy. The
523
+ // policy lives in safe-bash-commands.js so the TUI allow-list
524
+ // (claude-hook-installer.js) stays in lockstep with this path.
563
525
  var segments = splitShellSegments(cmd);
564
526
  var allSafe = true;
565
527
  for (var si = 0; si < segments.length; si++) {
566
- var seg = segments[si].trim();
567
- if (!seg) continue;
568
- // Strip leading env assignments (FOO=bar cmd) and sudo
569
- var firstWord = seg.replace(/^(?:\w+=\S*\s+)*/, "").split(/\s/)[0];
570
- if (firstWord === "sudo") {
571
- firstWord = seg.replace(/^(?:\w+=\S*\s+)*sudo\s+(?:-\S+\s+)*/, "").split(/\s/)[0];
572
- }
573
- if (!safeBashCommands[firstWord]) { allSafe = false; break; }
528
+ if (!isSafeBashSegment(segments[si])) { allSafe = false; break; }
574
529
  }
575
530
  if (allSafe) {
576
531
  return { behavior: "allow", updatedInput: input };
package/lib/sessions.js CHANGED
@@ -581,7 +581,7 @@ function createSessionManager(opts) {
581
581
  var _capsByVendor = capabilitiesByVendor || {};
582
582
  var _sessionVendor = session.vendor || defaultVendor || "claude";
583
583
  var _vendorCaps = _capsByVendor[_sessionVendor] || {};
584
- _send({ type: "session_switched", id: localId, cliSessionId: session.cliSessionId || null, loop: session.loop || null, vendor: session.vendor || null, hasHistory: (session.history && session.history.length > 0), capabilities: _vendorCaps, isProcessing: !!session.isProcessing, mode: session.mode || "gui", terminalId: typeof session.terminalId === "number" ? session.terminalId : null, runtimeMode: session.runtimeMode || null, runtimeTerminalId: typeof session.runtimeTerminalId === "number" ? session.runtimeTerminalId : null });
584
+ _send({ type: "session_switched", id: localId, cliSessionId: session.cliSessionId || null, loop: session.loop || null, vendor: session.vendor || null, hasHistory: (session.history && session.history.length > 0), capabilities: _vendorCaps, isProcessing: !!session.isProcessing, mode: session.mode || "gui", terminalId: typeof session.terminalId === "number" ? session.terminalId : null, runtimeMode: session.runtimeMode || null, runtimeTerminalId: typeof session.runtimeTerminalId === "number" ? session.runtimeTerminalId : null, tuiSuspended: !!session.tuiSuspended });
585
585
  // Send vendor-specific slash commands
586
586
  var _vendorCmds = slashCommandsByVendor[_sessionVendor] || slashCommands || [];
587
587
  _send({ type: "slash_commands", commands: _vendorCmds, vendor: _sessionVendor });
@@ -3,6 +3,13 @@ var { createTerminal } = require("./terminal");
3
3
  var MAX_TERMINALS = 10;
4
4
  var SCROLLBACK_MAX = 50 * 1024; // 50 KB per terminal
5
5
 
6
+ // Idle reclaim: background `claude` TUI PTYs that nobody is viewing and that
7
+ // have produced no output / received no input for this long are killed to
8
+ // free system resources. Safe because TUI sessions resume on demand from
9
+ // their on-disk transcript (lazy resume), so the conversation isn't lost.
10
+ var TUI_IDLE_REAP_MS = 3 * 60 * 1000; // 3 minutes
11
+ var TUI_REAP_INTERVAL_MS = 60 * 1000; // sweep cadence
12
+
6
13
  /**
7
14
  * Create a terminal manager for a project.
8
15
  * Manages persistent PTY sessions with scrollback buffering.
@@ -49,6 +56,7 @@ function createTerminalManager(opts) {
49
56
  rows: rows || 24,
50
57
  title: (opts && opts.title) || ("Terminal " + id),
51
58
  kind: (opts && opts.kind) || "shell",
59
+ lastActivityAt: Date.now(),
52
60
  exited: false,
53
61
  exitCode: null,
54
62
  subscribers: new Set(),
@@ -60,6 +68,7 @@ function createTerminalManager(opts) {
60
68
  pty.onData(function (data) {
61
69
  // Buffer scrollback with timestamps
62
70
  var ts = Date.now();
71
+ session.lastActivityAt = ts; // output counts as activity (don't reap a working claude)
63
72
  session.scrollback.push({ ts: ts, data: data });
64
73
  session.scrollbackSize += data.length;
65
74
  session.totalBytesWritten += data.length;
@@ -149,6 +158,7 @@ function createTerminalManager(opts) {
149
158
  function write(id, data) {
150
159
  var session = terminals.get(id);
151
160
  if (session && session.pty) {
161
+ session.lastActivityAt = Date.now(); // user input counts as activity
152
162
  session.pty.write(data);
153
163
  }
154
164
  }
@@ -225,7 +235,40 @@ function createTerminalManager(opts) {
225
235
  };
226
236
  }
227
237
 
238
+ // Periodic sweep: reclaim idle background TUI PTYs. A terminal is reaped
239
+ // only when it's a `tui-session`, has no live subscribers (nobody's viewing
240
+ // it), and has seen no input/output for TUI_IDLE_REAP_MS. The session is
241
+ // flagged reclaimed so its onExitHook keeps the Clay session record
242
+ // (lazy-resume re-spawns claude on demand) instead of deleting it.
243
+ function reapIdleTuiTerminals() {
244
+ var now = Date.now();
245
+ var toReap = [];
246
+ for (var session of terminals.values()) {
247
+ if (session.kind !== "tui-session" || session.exited || !session.pty) continue;
248
+ if (session.subscribers.size > 0) continue;
249
+ if (now - (session.lastActivityAt || 0) < TUI_IDLE_REAP_MS) continue;
250
+ toReap.push(session);
251
+ }
252
+ for (var i = 0; i < toReap.length; i++) {
253
+ toReap[i].reclaimed = true;
254
+ close(toReap[i].id); // pty.kill -> onExit -> onExitHook(session)
255
+ }
256
+ }
257
+
258
+ // Flag a terminal as reclaimed (PTY closed but the owning Clay session
259
+ // should be kept, not deleted). Used by idle reap and by an explicit
260
+ // user "Close" before close() so the onExitHook can tell the difference
261
+ // from a real /exit.
262
+ function markReclaimed(id) {
263
+ var session = terminals.get(id);
264
+ if (session) session.reclaimed = true;
265
+ }
266
+
267
+ var reapTimer = setInterval(reapIdleTuiTerminals, TUI_REAP_INTERVAL_MS);
268
+ if (reapTimer && typeof reapTimer.unref === "function") reapTimer.unref();
269
+
228
270
  function destroyAll() {
271
+ if (reapTimer) { clearInterval(reapTimer); reapTimer = null; }
229
272
  for (var session of terminals.values()) {
230
273
  // Shutdown teardown: pty.kill triggers pty.onExit asynchronously, and
231
274
  // that handler would normally invoke onExitHook (which for TUI
@@ -260,6 +303,7 @@ function createTerminalManager(opts) {
260
303
  getScrollback: getScrollback,
261
304
  destroyAll: destroyAll,
262
305
  has: has,
306
+ markReclaimed: markReclaimed,
263
307
  };
264
308
  }
265
309
 
package/lib/ws-schema.js CHANGED
@@ -17,6 +17,8 @@ var schema = {
17
17
  // Session management
18
18
  // -----------------------------------------------------------------------
19
19
  "switch_session": { direction: "c2s", handler: "lib/project-sessions.js", description: "Switch the active session by local ID" },
20
+ "resume_tui_session": { direction: "c2s", handler: "lib/project-sessions.js", description: "Spawn the claude --resume PTY for a TUI session shown read-only (lazy resume)" },
21
+ "suspend_tui_session": { direction: "c2s", handler: "lib/project-sessions.js", description: "Close a live TUI session's PTY now but keep it resumable (explicit Close)" },
20
22
  "new_session": { direction: "c2s", handler: "lib/project-sessions.js", description: "Create a new blank session" },
21
23
  "delete_session": { direction: "c2s", handler: "lib/project-sessions.js", description: "Delete a session by ID" },
22
24
  "rename_session": { direction: "c2s", handler: "lib/project-sessions.js", description: "Rename a session" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "2.40.0-beta.2",
3
+ "version": "2.40.0-beta.4",
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",