Skip to content

Commit 924965f

Browse files
committed
feat: compress tsc (TypeScript compiler) output in post_bash hook
Adds _is_tsc_cmd detection (bare tsc, npx/yarn/pnpm wrappers, path- resolved binaries) and a post_bash compression block that fires at >= 50 lines. Timestamp/watch progress noise is stripped; diagnostic lines (error/warning TS####:) and the 'Found N errors.' summary are kept. Full output is cached for bash-output recall.
1 parent df1fec2 commit 924965f

3 files changed

Lines changed: 496 additions & 0 deletions

File tree

src/token_goat/bash_compress.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2754,6 +2754,50 @@ def _is_cargo_compile_cmd(argv: list[str]) -> bool:
27542754
return False
27552755

27562756

2757+
def _is_tsc_cmd(argv: list[str]) -> bool:
2758+
"""Return True if the command is a TypeScript compiler (tsc) invocation.
2759+
2760+
Handles: bare ``tsc``, ``npx tsc``, ``npx --yes tsc``, ``yarn tsc``,
2761+
``pnpm tsc``, ``pnpm exec tsc``, and path-resolved binaries like
2762+
``./node_modules/.bin/tsc`` or ``tsc.cmd``. All tsc flags (--build,
2763+
--noEmit, --watch, etc.) are ignored for detection purposes.
2764+
"""
2765+
if not argv:
2766+
return False
2767+
2768+
def _base(s: str) -> str:
2769+
b = s.replace("\\", "/").rsplit("/", 1)[-1].lower()
2770+
for ext in (".exe", ".cmd"):
2771+
if b.endswith(ext):
2772+
b = b[: -len(ext)]
2773+
break
2774+
return b
2775+
2776+
b0 = _base(argv[0])
2777+
# Direct invocation: tsc, ./node_modules/.bin/tsc, tsc.cmd, etc.
2778+
if b0 == "tsc":
2779+
return True
2780+
# Package manager wrappers: npx tsc, yarn tsc, pnpm tsc, pnpm exec tsc
2781+
if b0 in ("npx", "yarn", "pnpm"):
2782+
i = 1
2783+
while i < len(argv):
2784+
tok = argv[i]
2785+
if tok.startswith("-"):
2786+
# --package / -p consume next token as value
2787+
if tok in ("--package", "-p"):
2788+
i += 2
2789+
else:
2790+
i += 1
2791+
else:
2792+
# For pnpm, "exec" is a sub-command prefix; skip it and continue
2793+
if b0 == "pnpm" and tok == "exec":
2794+
i += 1
2795+
continue
2796+
return _base(tok) == "tsc"
2797+
return False
2798+
return False
2799+
2800+
27572801
class CargoFilter(Filter):
27582802
"""Compress cargo build / check / test / clippy / run / bench output.
27592803

src/token_goat/hooks_read.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4512,6 +4512,10 @@ def post_read(payload: HookPayload) -> HookResponse:
45124512
_VERBOSE_TEST_MIN_LINES: int = 80
45134513
#: Minimum line count before cargo compilation output compression fires.
45144514
_CARGO_COMPILE_MIN_LINES: int = 40
4515+
#: Minimum line count before tsc output compression fires.
4516+
_TSC_MIN_LINES: int = 50
4517+
#: Matches position-less tsc --build errors/warnings (no ``(row,col)`` token).
4518+
_TSC_BARE_DIAG_RE: _re.Pattern[str] = _re.compile(r"^(error|warning) TS\d+:")
45154519
#: Matches the base command name of directory-exploration invocations (ls, eza, tree, fd).
45164520
_RECON_CMD_RE: _re.Pattern[str] = _re.compile(r"^(?:ls|ll|la|eza|exa|tree|fd|fdfind)\b")
45174521

@@ -6039,6 +6043,99 @@ def post_bash(payload: HookPayload) -> HookResponse:
60396043
except Exception: # noqa: BLE001 — fail-soft; never block the hook
60406044
_LOG.debug("post-bash: cargo compile compression failed", exc_info=True)
60416045

6046+
# tsc compression: strip timestamp/watch noise, keep diagnostics + summary; fires at >= _TSC_MIN_LINES lines.
6047+
if stdout and len(stdout.splitlines()) >= _TSC_MIN_LINES:
6048+
try:
6049+
import re as _re_tsc # noqa: PLC0415
6050+
import shlex as _shlex_tsc # noqa: PLC0415
6051+
import sys as _sys_tsc # noqa: PLC0415
6052+
6053+
from .bash_compress import _is_tsc_cmd as _tsc_check # noqa: PLC0415
6054+
6055+
_tsc_argv = _shlex_tsc.split(display_cmd, posix=(_sys_tsc.platform != "win32"))
6056+
if _tsc_argv and _tsc_check(_tsc_argv):
6057+
_TSC_DIAG_RE = _re_tsc.compile(r"^[^\s].+\(\d+,\d+\): (error|warning) TS\d+:")
6058+
_TSC_SUMMARY_RE = _re_tsc.compile(r"^Found \d+ errors?\.")
6059+
_tsc_lines = stdout.splitlines()
6060+
_tsc_total = len(_tsc_lines)
6061+
_tsc_diag_lines: list[str] = []
6062+
_tsc_noise_lines: list[str] = []
6063+
_tsc_summary: str | None = None
6064+
for _tsc_line in _tsc_lines:
6065+
if _TSC_SUMMARY_RE.match(_tsc_line):
6066+
_tsc_summary = _tsc_line
6067+
elif _TSC_DIAG_RE.match(_tsc_line) or _TSC_BARE_DIAG_RE.match(_tsc_line):
6068+
_tsc_diag_lines.append(_tsc_line)
6069+
else:
6070+
_tsc_noise_lines.append(_tsc_line)
6071+
6072+
if len(_tsc_noise_lines) == 0:
6073+
pass # nothing to suppress — fall through
6074+
else:
6075+
_tsc_out_id: str | None = None
6076+
if session_id:
6077+
from . import bash_cache as _bc_tsc # noqa: PLC0415
6078+
with contextlib.suppress(Exception):
6079+
_tsc_meta = _bc_tsc.store_output(
6080+
session_id, display_cmd, stdout, stderr, exit_code,
6081+
cwd=cwd, min_cache_bytes=0,
6082+
)
6083+
if _tsc_meta is not None:
6084+
_bc_tsc.write_sidecar(_tsc_meta)
6085+
_tsc_out_id = _tsc_meta.output_id
6086+
_tsc_recall = (f"\n[Full output: bash-output {_tsc_out_id}]" if _tsc_out_id else "")
6087+
6088+
if not _tsc_diag_lines and exit_code in (None, 0):
6089+
# Clean build with verbose/timestamp noise only
6090+
_tsc_summary_line = _tsc_summary or next(
6091+
(_l for _l in reversed(_tsc_lines) if _l.strip()), None
6092+
)
6093+
_tsc_body = (_tsc_summary_line + "\n") if _tsc_summary_line else ""
6094+
if not stdout.endswith(("\n", "\r\n")) and _tsc_body.endswith("\n"):
6095+
_tsc_body = _tsc_body.rstrip("\n")
6096+
_tsc_suppressed = _tsc_total - (1 if _tsc_summary_line else 0)
6097+
_tsc_msg = (
6098+
f"[token-goat] tsc: 0 errors, 0 warnings"
6099+
f" ({_tsc_suppressed}/{_tsc_total} lines suppressed)\n"
6100+
+ _tsc_body
6101+
+ _tsc_recall
6102+
)
6103+
else:
6104+
# Has diagnostics — keep all, strip noise lines
6105+
_tsc_error_count = sum(
6106+
1 for _l in _tsc_diag_lines if _re_tsc.search(r": error TS\d+:", _l)
6107+
)
6108+
_tsc_warn_count = sum(
6109+
1 for _l in _tsc_diag_lines
6110+
if _re_tsc.search(r": warning TS\d+:", _l)
6111+
)
6112+
_tsc_body_lines = list(_tsc_diag_lines)
6113+
if _tsc_summary and (
6114+
not _tsc_body_lines or _tsc_body_lines[-1] != _tsc_summary
6115+
):
6116+
_tsc_body_lines.append(_tsc_summary)
6117+
_tsc_body = "\n".join(_tsc_body_lines)
6118+
if stdout.endswith(("\n", "\r\n")):
6119+
_tsc_body += "\n"
6120+
_tsc_suppressed = _tsc_total - len(_tsc_body_lines)
6121+
_tsc_msg = (
6122+
f"[token-goat] tsc: {_tsc_error_count} errors,"
6123+
f" {_tsc_warn_count} warnings"
6124+
f" ({_tsc_suppressed}/{_tsc_total} lines suppressed)\n"
6125+
+ _tsc_body
6126+
+ _tsc_recall
6127+
)
6128+
_LOG.info(
6129+
"post-bash: tsc compressed lines=%d diag=%d cmd=%.60s",
6130+
_tsc_total, len(_tsc_diag_lines), display_cmd,
6131+
)
6132+
if _sess_mod is not None and _session_cache is not None:
6133+
with contextlib.suppress(Exception):
6134+
_sess_mod.save(_session_cache)
6135+
return {"continue": True, "systemMessage": _tsc_msg}
6136+
except Exception: # noqa: BLE001 — fail-soft; never block the hook
6137+
_LOG.debug("post-bash: tsc compression failed", exc_info=True)
6138+
60426139
# Pytest failure traceback suppression (Iter 18):
60436140
# Fires when pytest output is large (>= _PYTEST_COMPRESS_MIN_BYTES) and contains
60446141
# FAILED markers. Stores full output in bash-cache first so ``bash-output <id>``

0 commit comments

Comments
 (0)