caproom 0.7.2 → 0.7.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 (3) hide show
  1. package/README.md +4 -1
  2. package/bin/caproom +91 -3
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -51,8 +51,9 @@ caproom --limit 4096 --docker --image python:3.12-slim -- python train.py
51
51
  | `--docker` | off | opt in to the Docker cgroup backend instead of the default host-native watchdog |
52
52
  | `--image <name>` | `node:22-slim` | docker image used by the `--docker` backend |
53
53
  | `--force-watchdog` | — | legacy no-op; the watchdog IS the default. Accepted so existing scripts and `init` snippets keep working |
54
+ | `--no-intercept-tty` | off | bypass stdio interposition for TUI/pty apps — `exec` directly and monitor via detached `watch --auto-park` (preserves `OSC 10/11`, `DSR CPR`, mouse `DEC 1003`) |
54
55
 
55
- Env var overrides: `CAPROOM_LIMIT_MB`, `CAPROOM_IMAGE`, `CAPROOM_INTERVAL`, `CAPROOM_GRACE`.
56
+ Env var overrides: `CAPROOM_LIMIT_MB`, `CAPROOM_IMAGE`, `CAPROOM_INTERVAL`, `CAPROOM_GRACE`, `CAPROOM_BYPASS_TTY=1` (force bypass, same as `--no-intercept-tty`).
56
57
 
57
58
  On cap breach, the watchdog backend sends `SIGTERM` first and waits `--grace` seconds before `SIGKILL`. If the process exits cleanly during the grace window, `caproom` propagates its real exit code; only a hard `SIGKILL` (process ignored `SIGTERM`, or grace ran out) reports `137` (same convention as Docker's own OOM-kill exit code, which the docker backend always uses on breach since Docker itself sends the kill).
58
59
 
@@ -94,6 +95,8 @@ caproom init claude --limit 6144 --grace 10 >> ~/.zshrc && source ~/.zshrc
94
95
 
95
96
  This appends a shell function that wraps `claude` through the watchdog backend (host-native — no Docker isolation, so the wrapped command keeps its normal filesystem/auth/PATH access) and an alias so plain `claude` picks it up. Per-shell override without editing the rc file: `CAPROOM_LIMIT_MB=8192 claude ...`. Works for any command, not just `claude` — `caproom init npm --limit 2048` wraps `npm` the same way.
96
97
 
98
+ **TUI / pty note:** `caproom -- <TUI>` (opencode, claude, vim, htop, …) would break the pty — `OSC 10/11` (`^[]10;rgb:...`), `DSR CPR` (`^[[5;1R`), and mouse `^[[<35;` reports leak as text. Since 0.7.3, `caproom` detects `[ -t 0 ] && [ -t 1 ]` for known TUIs and **bypasses stdio interposition**: the TUI `exec`s directly and a detached `caproom watch --auto-park` monitors its pid tree (same cap, no pty corruption). Piped/batch `caproom -- opencode run "task"` stays fully capped via the watchdog. Override: `CAPROOM_BYPASS_TTY=1` / `--no-intercept-tty` forces bypass for any command; `CAPROOM_BYPASS_TTY=0` forces legacy pipe mode. `caproom init <TUI>` prints a warning and emits the tty-aware wrapper.
99
+
97
100
  ## setup / bind — shell integration for every terminal
98
101
 
99
102
  `caproom setup` binds headroom management to your shells at the **shell layer**, so it works in any terminal (Terminal.app, iTerm2, Ghostty, Windows Terminal) without touching terminal-specific config:
package/bin/caproom CHANGED
@@ -34,6 +34,9 @@ usage: caproom [--limit <mb>] [--image <docker-image>] [--interval <sec>] -- <co
34
34
  (default: node:22-slim)
35
35
  --force-watchdog no-op; the host-native watchdog IS the default — kept so
36
36
  existing scripts and 'init' snippets keep working
37
+ --no-intercept-tty bypass stdio interposition for TUI/pty apps (OSC 10/11,
38
+ DSR CPR, mouse DEC 1003) — exec directly and monitor via
39
+ detached 'caproom watch --auto-park' instead
37
40
 
38
41
  park / wake — freeze an idle process so the kernel CAN reclaim/compress its
39
42
  memory without killing it. Honest semantics: SIGSTOP only makes the pages
@@ -61,7 +64,12 @@ output to your shell rc (~/.zshrc, ~/.bashrc):
61
64
 
62
65
  caproom init claude >> ~/.zshrc && source ~/.zshrc
63
66
 
64
- env vars (override flags): CAPROOM_LIMIT_MB, CAPROOM_IMAGE, CAPROOM_INTERVAL, CAPROOM_GRACE
67
+ env vars (override flags): CAPROOM_LIMIT_MB, CAPROOM_IMAGE, CAPROOM_INTERVAL, CAPROOM_GRACE, CAPROOM_BYPASS_TTY=1
68
+
69
+ TUI note: 'caproom -- <TUI>' (opencode, vim, htop, ...) bypasses stdio when [ -t 0 ] && [ -t 1 ]
70
+ so terminal queries (OSC, mouse) stay on the pty. The cap is then advisory via a detached
71
+ 'caproom watch --auto-park' monitor. Piped/batch 'caproom -- opencode run "task"' stays fully capped.
72
+ Use --no-intercept-tty / CAPROOM_BYPASS_TTY=1 to force bypass for any command.
65
73
 
66
74
  examples:
67
75
  caproom --limit 2048 -- npm run build
@@ -92,14 +100,48 @@ cmd_init() {
92
100
  esac
93
101
  done
94
102
  local fn="${target}_capped"
95
- cat << EOF
103
+ local is_tui=0
104
+ case "$target" in
105
+ opencode|claude|codex|vim|nvim|htop|less|fzf|nano|emacs) is_tui=1 ;;
106
+ esac
107
+ if [[ $is_tui -eq 1 ]]; then
108
+ echo "caproom: warning: '$target' is a TUI — interactive tty runs will bypass stdio and use a detached 'caproom watch --auto-park' monitor to preserve pty (OSC 10/11, DSR CPR, mouse DEC 1003). Batch 'caproom -- $target run' stays fully capped. Set CAPROOM_BYPASS_TTY=1 to always bypass, or CAPROOM_BYPASS_TTY=0 + re-init to force legacy pipe mode." >&2
109
+ cat << EOF
96
110
  # caproom: auto-cap '$target' — added by 'caproom init $target'
97
111
  # override per-shell: CAPROOM_LIMIT_MB=8192 $target ...
112
+ # TUI note: when [ -t 0 ] && [ -t 1 ], '$target' runs directly and a detached 'caproom watch --auto-park' monitors it
113
+ # (preserves OSC 10/11, DSR CPR, mouse DEC 1003). Batch/headless (piped) still goes through the watchdog.
114
+ # Set CAPROOM_BYPASS_TTY=1 to always bypass, 0 to force legacy mode.
98
115
  $fn() {
116
+ if [ "\${CAPROOM_BYPASS_TTY:-0}" = "1" ]; then
117
+ command caproom --no-intercept-tty --limit "\${CAPROOM_LIMIT_MB:-$limit}" --grace "\${CAPROOM_GRACE:-$grace}" -- command $target "\$@"
118
+ return \$?
119
+ fi
120
+ if [ -t 0 ] && [ -t 1 ]; then
121
+ command $target "\$@" &
122
+ local _cap_pid=\$!
123
+ command caproom watch --threshold-mb "\${CAPROOM_LIMIT_MB:-$limit}" --interval "\${CAPROOM_INTERVAL:-0.2}" --auto-park --json "\$_cap_pid" >/dev/null 2>&1 &
124
+ wait "\$_cap_pid"
125
+ return \$?
126
+ fi
99
127
  command caproom --limit "\${CAPROOM_LIMIT_MB:-$limit}" --force-watchdog --grace "\${CAPROOM_GRACE:-$grace}" -- command $target "\$@"
100
128
  }
101
129
  alias $target=$fn
102
130
  EOF
131
+ else
132
+ cat << EOF
133
+ # caproom: auto-cap '$target' — added by 'caproom init $target'
134
+ # override per-shell: CAPROOM_LIMIT_MB=8192 $target ...
135
+ $fn() {
136
+ if [ "\${CAPROOM_BYPASS_TTY:-0}" = "1" ]; then
137
+ command caproom --no-intercept-tty --limit "\${CAPROOM_LIMIT_MB:-$limit}" --grace "\${CAPROOM_GRACE:-$grace}" -- command $target "\$@"
138
+ return \$?
139
+ fi
140
+ command caproom --limit "\${CAPROOM_LIMIT_MB:-$limit}" --force-watchdog --grace "\${CAPROOM_GRACE:-$grace}" -- command $target "\$@"
141
+ }
142
+ alias $target=$fn
143
+ EOF
144
+ fi
103
145
  }
104
146
 
105
147
  cmd_park() {
@@ -706,6 +748,8 @@ IMAGE="${CAPROOM_IMAGE:-node:22-slim}"
706
748
  INTERVAL="${CAPROOM_INTERVAL:-0.2}"
707
749
  GRACE="${CAPROOM_GRACE:-5}"
708
750
  USE_DOCKER=0
751
+ BYPASS_TTY=0
752
+ [[ "${CAPROOM_BYPASS_TTY:-0}" == "1" ]] && BYPASS_TTY=1
709
753
 
710
754
  while [[ $# -gt 0 ]]; do
711
755
  case "$1" in
@@ -717,6 +761,7 @@ while [[ $# -gt 0 ]]; do
717
761
  # legacy no-op: the watchdog IS the default now; accepted so old
718
762
  # scripts and init snippets keep working
719
763
  --force-watchdog) shift ;;
764
+ --no-intercept-tty) BYPASS_TTY=1; shift ;;
720
765
  --) shift; break ;;
721
766
  -h|--help) usage help ;;
722
767
  *) break ;;
@@ -740,13 +785,56 @@ collect_tree() {
740
785
  walk_tree "$1"
741
786
  }
742
787
 
788
+ is_known_tui() {
789
+ case "${1##*/}" in
790
+ opencode|claude|codex|vim|nvim|htop|less|fzf|nano|emacs) return 0 ;;
791
+ *) return 1 ;;
792
+ esac
793
+ }
794
+
743
795
  run_watchdog() {
796
+ # TUI bypass: if stdio is a tty and target is a known TUI (or bypass flag),
797
+ # don't sit on stdio — exec directly and monitor via detached watch.
798
+ # This preserves pty semantics (OSC 10/11, DSR CPR, DEC 1003 mouse) that
799
+ # break when a wrapper backgrounds the TUI and steals foreground pgrp.
800
+ if [[ "$BYPASS_TTY" -eq 1 ]] || { [[ -t 0 && -t 1 ]] && is_known_tui "${1:-}"; }; then
801
+ if [[ "$BYPASS_TTY" -eq 1 ]]; then
802
+ echo "caproom: bypass-tty active — exec directly, no stdio interposition (limit ${LIMIT_MB}m advisory via detached watch)" >&2
803
+ else
804
+ echo "caproom: tty TUI detected ('${1##*/}') — bypassing stdio interposition, monitoring via detached watch (limit ${LIMIT_MB}m)" >&2
805
+ fi
806
+ # Clean stale tty modes before handing off, same snapshot hygiene as
807
+ # the normal watchdog path but without taking foreground away.
808
+ printf '\033[?1049l\033[?1000l\033[?1002l\033[?1003l\033[?1006l\033[?2004l\033[?25h' >/dev/tty 2>/dev/null || true
809
+ # Detached watchdog monitors the TUI's pid tree without owning stdio.
810
+ # Resolve our own path (works via shim and direct invocation).
811
+ local _cap_bin="${BASH_SOURCE[0]:-$0}"
812
+ [[ "$_cap_bin" != /* ]] && _cap_bin="$PWD/$_cap_bin"
813
+ if [[ ! -f "$_cap_bin" ]]; then
814
+ _cap_bin="$(command -v caproom 2>/dev/null || echo "$_cap_bin")"
815
+ fi
816
+ "$@" &
817
+ local _tui_pid=$!
818
+ # Fire-and-forget watch; it exits when _tui_pid exits. Suppressed so it
819
+ # never leaks into the TUI's stdout. Threshold is the cap itself.
820
+ bash "$_cap_bin" watch --threshold-mb "$LIMIT_MB" --interval "$INTERVAL" --auto-park --json "$_tui_pid" >/dev/null 2>&1 &
821
+ local _watch_pid=$!
822
+ wait "$_tui_pid"
823
+ local _rc=$?
824
+ # Best-effort cleanup of the detached watcher (it should already exit
825
+ # via all-exited, but kill is cheap if TUI exited fast).
826
+ kill "$_watch_pid" 2>/dev/null || true
827
+ wait "$_watch_pid" 2>/dev/null || true
828
+ exit "$_rc"
829
+ fi
744
830
  echo "caproom: watchdog backend (host-native), limit=${LIMIT_MB}m poll=${INTERVAL}s (process-tree RSS)" >&2
745
831
  local limit_kb=$(( LIMIT_MB * 1024 ))
746
832
  # Terminal hygiene: a TUI child (opencode, claude, ...) puts the tty in
747
833
  # raw + mouse-tracking mode. If WE kill it, it never restores, and the
748
834
  # user's shell then prints mouse reports like [[<35;25;15M as garbage.
749
- # Snapshot termios before spawn so we can put the tty back after a kill.
835
+ # Clear any stale mode from a previous kill BEFORE snapshot, then snapshot
836
+ # the clean state so restore goes to sane, not back to garbage.
837
+ printf '\033[?1049l\033[?1000l\033[?1002l\033[?1003l\033[?1006l\033[?2004l\033[?25h' >/dev/tty 2>/dev/null || true
750
838
  local saved_stty=""
751
839
  if [[ -t 0 ]]; then
752
840
  saved_stty=$(stty -g </dev/tty 2>/dev/null || true)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "caproom",
3
- "version": "0.7.2",
3
+ "version": "0.7.3",
4
4
  "description": "Memory-cap any command (AI coding agents, builds, background jobs) on macOS, Linux, and Windows — real enforcement via Docker cgroups, Windows Job Objects, or a polling watchdog, plus park/wake to reclaim idle process memory without killing.",
5
5
  "bin": {
6
6
  "caproom": "bin/caproom.js",