caproom 0.7.1 → 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.
- package/README.md +22 -1
- package/bin/caproom +107 -2
- package/bin/caproom.ps1 +14 -8
- 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,26 @@ 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
|
+
|
|
100
|
+
## setup / bind — shell integration for every terminal
|
|
101
|
+
|
|
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:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
caproom setup # bind zsh/bash + fish (Windows: PowerShell $PROFILE)
|
|
106
|
+
caproom setup --uninstall # remove rc markers; backups and integration files stay
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
What it does:
|
|
110
|
+
|
|
111
|
+
- Writes one integration file per shell under `~/.caproom/` (`shell.sh`, `shell.fish`, `shell.ps1`) and adds a 3-line marker block (`# >>> caproom >>>`) to your rc files. Idempotent; timestamped `.bak` backups are made before first patch.
|
|
112
|
+
- Adds a **headroom warning on every prompt** — once per minute, only when free memory drops below `CAPROOM_HEADROOM_WARN` percent (default 20). Never blocks, never modifies your prompt text.
|
|
113
|
+
- Optional **auto-wrap**: `export CAPROOM_AUTO_WRAP="claude,codex,opencode"` in your rc creates `<cmd>_capped` twins that run through the watchdog with `CAPROOM_LIMIT_MB`. Bare command names are shadowed **only** if you also set `CAPROOM_AUTO_ALIAS=1` — caproom never hijacks an unwrapped command by default.
|
|
114
|
+
- Optional login daemon: `caproom setup --guard --threshold 15` installs a LaunchAgent (macOS) or systemd user unit (Linux) running `caproom guard` across all terminals.
|
|
115
|
+
|
|
116
|
+
npm install never touches your rc files — postinstall prints a hint, binding is always explicit. Undo any time: `caproom setup --uninstall`.
|
|
117
|
+
|
|
97
118
|
### `caproom top` — process-tree inventory for agents
|
|
98
119
|
|
|
99
120
|
Read-only snapshot of every process tree you own, sorted by tree RSS. `--json` output is a **stable contract**: `schema` version field, additive changes only.
|
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
|
-
|
|
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,9 +785,67 @@ 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 ))
|
|
832
|
+
# Terminal hygiene: a TUI child (opencode, claude, ...) puts the tty in
|
|
833
|
+
# raw + mouse-tracking mode. If WE kill it, it never restores, and the
|
|
834
|
+
# user's shell then prints mouse reports like [[<35;25;15M as garbage.
|
|
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
|
|
838
|
+
local saved_stty=""
|
|
839
|
+
if [[ -t 0 ]]; then
|
|
840
|
+
saved_stty=$(stty -g </dev/tty 2>/dev/null || true)
|
|
841
|
+
fi
|
|
842
|
+
restore_tty() {
|
|
843
|
+
# Only on OUR kill — a clean exit already restored its own state, and
|
|
844
|
+
# re-emitting resets there could clobber whatever the NEXT program drew.
|
|
845
|
+
[[ -n "$saved_stty" ]] && stty "$saved_stty" </dev/tty 2>/dev/null \
|
|
846
|
+
|| { [[ -t 0 ]] && stty sane </dev/tty 2>/dev/null || true; }
|
|
847
|
+
printf '\033[?1049l\033[?1000l\033[?1002l\033[?1003l\033[?1006l\033[?2004l\033[?25h' >/dev/tty 2>/dev/null || true
|
|
848
|
+
}
|
|
746
849
|
"$@" &
|
|
747
850
|
local pid=$!
|
|
748
851
|
local exit_code=0
|
|
@@ -776,10 +879,12 @@ run_watchdog() {
|
|
|
776
879
|
if [[ "$sweep" -gt 0 ]]; then
|
|
777
880
|
echo "caproom: SIGKILLed ${sweep} survivor(s) after grace — exit 137" >&2
|
|
778
881
|
wait "$pid" 2>/dev/null || true
|
|
882
|
+
restore_tty
|
|
779
883
|
exit 137
|
|
780
884
|
fi
|
|
781
885
|
wait "$pid" 2>/dev/null || exit_code=$?
|
|
782
886
|
echo "caproom: pid $pid exited cleanly (code $exit_code) during grace period" >&2
|
|
887
|
+
restore_tty
|
|
783
888
|
exit "$exit_code"
|
|
784
889
|
fi
|
|
785
890
|
sleep "$INTERVAL"
|
package/bin/caproom.ps1
CHANGED
|
@@ -504,18 +504,23 @@ function Invoke-Watch {
|
|
|
504
504
|
# Same NDJSON contract as the POSIX watcher (schema:1 events on stdout
|
|
505
505
|
# under --json). Explicit pids only; naming the pid IS the opt-in for
|
|
506
506
|
# --auto-park, same rule as POSIX.
|
|
507
|
+
# NOTE: args arrive via the named -Rest parameter, NOT $args — splatting
|
|
508
|
+
# into $args proved unreliable here (every element read back as null,
|
|
509
|
+
# yielding pids [0,0,0,0] and a busy-spinning interval-0 loop).
|
|
510
|
+
param([string[]]$Rest)
|
|
507
511
|
$thresholdMb = 2048; $intervalSec = 5.0; $auto = $false; $wake = -1.0; $json = $false
|
|
508
512
|
$targets = New-Object System.Collections.Generic.List[int]
|
|
509
|
-
for ($i = 0; $i -lt $
|
|
510
|
-
switch ($
|
|
511
|
-
'--threshold-mb' { $thresholdMb = [int]$
|
|
512
|
-
'--interval' { $intervalSec = [double]$
|
|
513
|
+
for ($i = 0; $i -lt $Rest.Count; $i++) {
|
|
514
|
+
switch ($Rest[$i]) {
|
|
515
|
+
'--threshold-mb' { $thresholdMb = [int]$Rest[$i + 1]; $i++ }
|
|
516
|
+
'--interval' { $intervalSec = [double]$Rest[$i + 1]; $i++ }
|
|
513
517
|
'--auto-park' { $auto = $true }
|
|
514
|
-
'--auto-wake-free-pct' { $wake = [double]$
|
|
518
|
+
'--auto-wake-free-pct' { $wake = [double]$Rest[$i + 1]; $i++ }
|
|
515
519
|
'--json' { $json = $true }
|
|
520
|
+
'--' { }
|
|
516
521
|
default {
|
|
517
|
-
try { $targets.Add([int]$
|
|
518
|
-
catch { [Console]::Error.WriteLine("caproom: unknown watch arg $($
|
|
522
|
+
try { $targets.Add([int]$Rest[$i]) }
|
|
523
|
+
catch { [Console]::Error.WriteLine("caproom: unknown watch arg $($Rest[$i])"); exit 1 }
|
|
519
524
|
}
|
|
520
525
|
}
|
|
521
526
|
}
|
|
@@ -523,6 +528,7 @@ function Invoke-Watch {
|
|
|
523
528
|
[Console]::Error.WriteLine('usage: caproom watch [--threshold-mb <mb>] [--interval <sec>] [--auto-park] [--auto-wake-free-pct <pct>] [--json] <pid...>')
|
|
524
529
|
exit 1
|
|
525
530
|
}
|
|
531
|
+
if ($intervalSec -lt 0.5) { $intervalSec = 0.5 }
|
|
526
532
|
|
|
527
533
|
function Emit([object]$Ev) {
|
|
528
534
|
[Console]::Out.WriteLine((ConvertTo-Json -Compress -Depth 6 -InputObject $Ev))
|
|
@@ -683,7 +689,7 @@ switch ($args[0]) {
|
|
|
683
689
|
exit 0
|
|
684
690
|
}
|
|
685
691
|
'watch' {
|
|
686
|
-
Invoke-Watch @($args | Select-Object -Skip 1)
|
|
692
|
+
Invoke-Watch -Rest @($args | Select-Object -Skip 1)
|
|
687
693
|
exit 0
|
|
688
694
|
}
|
|
689
695
|
'park' {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "caproom",
|
|
3
|
-
"version": "0.7.
|
|
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",
|