caproom 0.7.0 → 0.7.2
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 +18 -0
- package/bin/caproom +17 -0
- package/bin/caproom.ps1 +22 -16
- package/package.json +4 -3
- package/scripts/postinstall.js +7 -0
package/README.md
CHANGED
|
@@ -94,6 +94,24 @@ caproom init claude --limit 6144 --grace 10 >> ~/.zshrc && source ~/.zshrc
|
|
|
94
94
|
|
|
95
95
|
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
96
|
|
|
97
|
+
## setup / bind — shell integration for every terminal
|
|
98
|
+
|
|
99
|
+
`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:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
caproom setup # bind zsh/bash + fish (Windows: PowerShell $PROFILE)
|
|
103
|
+
caproom setup --uninstall # remove rc markers; backups and integration files stay
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
What it does:
|
|
107
|
+
|
|
108
|
+
- 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.
|
|
109
|
+
- 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.
|
|
110
|
+
- 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.
|
|
111
|
+
- Optional login daemon: `caproom setup --guard --threshold 15` installs a LaunchAgent (macOS) or systemd user unit (Linux) running `caproom guard` across all terminals.
|
|
112
|
+
|
|
113
|
+
npm install never touches your rc files — postinstall prints a hint, binding is always explicit. Undo any time: `caproom setup --uninstall`.
|
|
114
|
+
|
|
97
115
|
### `caproom top` — process-tree inventory for agents
|
|
98
116
|
|
|
99
117
|
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
|
@@ -743,6 +743,21 @@ collect_tree() {
|
|
|
743
743
|
run_watchdog() {
|
|
744
744
|
echo "caproom: watchdog backend (host-native), limit=${LIMIT_MB}m poll=${INTERVAL}s (process-tree RSS)" >&2
|
|
745
745
|
local limit_kb=$(( LIMIT_MB * 1024 ))
|
|
746
|
+
# Terminal hygiene: a TUI child (opencode, claude, ...) puts the tty in
|
|
747
|
+
# raw + mouse-tracking mode. If WE kill it, it never restores, and the
|
|
748
|
+
# 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.
|
|
750
|
+
local saved_stty=""
|
|
751
|
+
if [[ -t 0 ]]; then
|
|
752
|
+
saved_stty=$(stty -g </dev/tty 2>/dev/null || true)
|
|
753
|
+
fi
|
|
754
|
+
restore_tty() {
|
|
755
|
+
# Only on OUR kill — a clean exit already restored its own state, and
|
|
756
|
+
# re-emitting resets there could clobber whatever the NEXT program drew.
|
|
757
|
+
[[ -n "$saved_stty" ]] && stty "$saved_stty" </dev/tty 2>/dev/null \
|
|
758
|
+
|| { [[ -t 0 ]] && stty sane </dev/tty 2>/dev/null || true; }
|
|
759
|
+
printf '\033[?1049l\033[?1000l\033[?1002l\033[?1003l\033[?1006l\033[?2004l\033[?25h' >/dev/tty 2>/dev/null || true
|
|
760
|
+
}
|
|
746
761
|
"$@" &
|
|
747
762
|
local pid=$!
|
|
748
763
|
local exit_code=0
|
|
@@ -776,10 +791,12 @@ run_watchdog() {
|
|
|
776
791
|
if [[ "$sweep" -gt 0 ]]; then
|
|
777
792
|
echo "caproom: SIGKILLed ${sweep} survivor(s) after grace — exit 137" >&2
|
|
778
793
|
wait "$pid" 2>/dev/null || true
|
|
794
|
+
restore_tty
|
|
779
795
|
exit 137
|
|
780
796
|
fi
|
|
781
797
|
wait "$pid" 2>/dev/null || exit_code=$?
|
|
782
798
|
echo "caproom: pid $pid exited cleanly (code $exit_code) during grace period" >&2
|
|
799
|
+
restore_tty
|
|
783
800
|
exit "$exit_code"
|
|
784
801
|
fi
|
|
785
802
|
sleep "$INTERVAL"
|
package/bin/caproom.ps1
CHANGED
|
@@ -411,7 +411,7 @@ if ($args.Count -eq 0) { Show-Usage }
|
|
|
411
411
|
|
|
412
412
|
$script:CaproomNtLoaded = $false
|
|
413
413
|
function Ensure-NtSuspend {
|
|
414
|
-
# Whole-tree park needs NtSuspendProcess/NtResumeProcess (ntdll)
|
|
414
|
+
# Whole-tree park needs NtSuspendProcess/NtResumeProcess (ntdll) --
|
|
415
415
|
# the Windows analogue of kill -STOP/-CONT. Loaded lazily, once.
|
|
416
416
|
if ($script:CaproomNtLoaded) { return }
|
|
417
417
|
try {
|
|
@@ -423,7 +423,7 @@ function Ensure-NtSuspend {
|
|
|
423
423
|
'@
|
|
424
424
|
$script:CaproomNtLoaded = $true
|
|
425
425
|
} catch {
|
|
426
|
-
[Console]::Error.WriteLine('caproom watch: cannot load ntdll suspend/resume
|
|
426
|
+
[Console]::Error.WriteLine('caproom watch: cannot load ntdll suspend/resume -- --auto-park unavailable')
|
|
427
427
|
throw
|
|
428
428
|
}
|
|
429
429
|
}
|
|
@@ -469,7 +469,7 @@ function Get-TreeStats {
|
|
|
469
469
|
function Invoke-Top {
|
|
470
470
|
# schema:1 rows identical in shape to the POSIX build. One honest
|
|
471
471
|
# divergence: Windows exposes no cheap sleep-state, so state is always
|
|
472
|
-
# 'running' and park_candidate keys off tree size alone
|
|
472
|
+
# 'running' and park_candidate keys off tree size alone -- the reason
|
|
473
473
|
# string says so instead of pretending a sleep check happened.
|
|
474
474
|
param([int]$FilterPid = 0, [int]$ParkMinMb = 512)
|
|
475
475
|
$snap = Get-CaproomSnapshot
|
|
@@ -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))
|
|
@@ -562,7 +568,7 @@ function Invoke-Watch {
|
|
|
562
568
|
if ($h -ne [IntPtr]::Zero) {
|
|
563
569
|
[void][Caproom.Nt]::NtResumeProcess($h); [void][Caproom.Nt]::CloseHandle($h)
|
|
564
570
|
if ($json) { Emit ([pscustomobject]@{ schema = 1; event = 'woke'; ts = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds(); pid = $wpid; free_pct = $pct }) }
|
|
565
|
-
else { [Console]::Error.WriteLine("caproom: watch: free mem ${pct}% >= ${wake}%
|
|
571
|
+
else { [Console]::Error.WriteLine("caproom: watch: free mem ${pct}% >= ${wake}% -- resuming pid $wpid") }
|
|
566
572
|
}
|
|
567
573
|
}
|
|
568
574
|
$parkedByUs.Clear()
|
|
@@ -589,10 +595,10 @@ function Invoke-Watch {
|
|
|
589
595
|
}
|
|
590
596
|
}
|
|
591
597
|
if ($json) { Emit ([pscustomobject]@{ schema = 1; event = 'parked'; ts = $now; pid = $tpid; tree_rss_kb = $st.RssKb; tree_pids = @($st.Pids.ToArray()); stopped = $stopped }) }
|
|
592
|
-
else { [Console]::Error.WriteLine("caproom: watch: tree of pid $tpid hit $($st.RssKb)KB (>= $([int]$threshKb)KB)
|
|
598
|
+
else { [Console]::Error.WriteLine("caproom: watch: tree of pid $tpid hit $($st.RssKb)KB (>= $([int]$threshKb)KB) -- PARKED tree ($stopped pids)") }
|
|
593
599
|
} else {
|
|
594
600
|
if ($json) { Emit ([pscustomobject]@{ schema = 1; event = 'breach'; ts = $now; pid = $tpid; tree_rss_kb = $st.RssKb }) }
|
|
595
|
-
else { [Console]::Error.WriteLine("caproom: watch: tree of pid $tpid hit $($st.RssKb)KB (>= $([int]$threshKb)KB)
|
|
601
|
+
else { [Console]::Error.WriteLine("caproom: watch: tree of pid $tpid hit $($st.RssKb)KB (>= $([int]$threshKb)KB) -- no --auto-park, reporting only") }
|
|
596
602
|
}
|
|
597
603
|
} else {
|
|
598
604
|
if ($breaching.ContainsKey($tpid)) {
|
|
@@ -616,7 +622,7 @@ function Invoke-Setup {
|
|
|
616
622
|
New-Item -ItemType Directory -Force -Path $dir | Out-Null
|
|
617
623
|
|
|
618
624
|
@'
|
|
619
|
-
# caproom PowerShell integration
|
|
625
|
+
# caproom PowerShell integration -- regenerated by `caproom setup`.
|
|
620
626
|
function global:caproom_freemem_pct {
|
|
621
627
|
$os = Get-CimInstance Win32_OperatingSystem
|
|
622
628
|
[int]($os.FreePhysicalMemory * 100 / $os.TotalVisibleMemorySize)
|
|
@@ -651,7 +657,7 @@ function global:prompt {
|
|
|
651
657
|
} else {
|
|
652
658
|
[Console]::Error.WriteLine('caproom setup: profile already bound')
|
|
653
659
|
}
|
|
654
|
-
[Console]::Error.WriteLine('caproom setup: shell.ps1 written to ' + $dir + '
|
|
660
|
+
[Console]::Error.WriteLine('caproom setup: shell.ps1 written to ' + $dir + ' -- new terminals pick it up automatically')
|
|
655
661
|
}
|
|
656
662
|
|
|
657
663
|
switch ($args[0]) {
|
|
@@ -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.2",
|
|
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",
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"bin/caproom",
|
|
11
11
|
"bin/caproom.js",
|
|
12
12
|
"bin/caproom.ps1",
|
|
13
|
-
"bin/caproom-mcp.js"
|
|
13
|
+
"bin/caproom-mcp.js",
|
|
14
|
+
"scripts/postinstall.js"
|
|
14
15
|
],
|
|
15
16
|
"keywords": [
|
|
16
17
|
"memory",
|
|
@@ -29,7 +30,7 @@
|
|
|
29
30
|
"win32"
|
|
30
31
|
],
|
|
31
32
|
"scripts": {
|
|
32
|
-
"postinstall": "node
|
|
33
|
+
"postinstall": "node scripts/postinstall.js"
|
|
33
34
|
},
|
|
34
35
|
"license": "MIT",
|
|
35
36
|
"repository": {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Hint only — never touches rc files on install. Binding is explicit:
|
|
2
|
+
// caproom setup
|
|
3
|
+
try {
|
|
4
|
+
process.stdout.write(
|
|
5
|
+
'caproom: optional next step — run "caproom setup" to bind headroom warnings to your shells (never modifies rc files on install)\n'
|
|
6
|
+
);
|
|
7
|
+
} catch (_) { /* never block installs */ }
|