Add `logs` command to show and follow bridge log files (#209)
## Summary
Closes #205.
Adds a new `mcpc @<session> logs` command so users can read or follow
bridge log files without knowing the underlying file path. Replaces the
existing "For details, check logs at `<path>`" hints with the more
actionable "For details, run: `mcpc @<session> logs`", and surfaces
`_mcpc.logPath` / `_mcpc.logSize` on `mcpc @<session> --json` so scripts
and AI agents can still get the path when they need it.
## Command surface
```
mcpc @apify logs # last 50 lines (default), header on stderr, lines on stdout
mcpc @apify logs -n 200 # -n alias for --tail
mcpc @apify logs --tail 200 # same
mcpc @apify logs --follow # tail -f; re-opens on rotation; Ctrl+C to stop
mcpc @apify logs --since 1h # duration shorthand (30s, 5m, 2h, 1d, 1w)
mcpc @apify logs --since 2026-04-28T12:00:00Z # or ISO 8601 timestamp
mcpc @apify logs --since 1h -n 50 # since is a floor, tail is a cap
mcpc @apify logs --json # parsed records (see below)
```
Notes:
- The reader transparently spans rotated files (`.log.1` … `.log.5`)
when `-n` or `--since` needs older lines.
- Errors are friendly: unknown session, session with no log file yet,
invalid `--since`, etc.
## JSON output shape
Each line is parsed into a structured record. Lines that don't match the
`[ISO] [LEVEL] [ctx?] msg` shape (banners, stack traces) fall back to `{
ts: null, raw }`:
```json
[
{ "ts": "2026-04-28T12:01:14.231Z", "level": "info", "context": "bridge-manager", "message": "Started bridge for @apify" },
{ "ts": null, "raw": "========================================" }
]
```
With `--follow --json`, output is NDJSON (one record per line) since a
JSON array can't be streamed.
## Adjacent changes
- `mcpc @<session> --json` now includes `_mcpc.logPath` and
`_mcpc.logSize`.
- The auth-error hint, expired-session hint, and `session-client` retry
hint now point to `mcpc @<session> logs` instead of the raw file path.
The file-path fallback is kept for non-session targets where `logs`
doesn't apply.
- `mcpc @<session>` (server info) gains a new "For session logs, run: …"
line under the command list.
## Implementation notes
- New `src/lib/log-reader.ts` — pure, runtime-agnostic reader:
`parseLogLine`, `parseDuration`, `resolveSince`, `listLogFiles`,
`readRecentLogLines`, and `followLog`. `followLog` accepts an options
object (`pollIntervalMs`, `startAtBeginning`) so tests can drive it
deterministically; production still defaults to a 1 s poll.
- Rotation detection in `followLog` watches for inode change *or* size
shrink and re-opens from the start of the new file. `fs.watch` is paired
with a polling fallback for filesystems where the kernel events are
unreliable (NFS, some network mounts).
- New `src/cli/commands/logs.ts` orchestrates the human / JSON output,
handles signals, and emits the stderr header (`Session @x · <path> ·
last N lines`) ahead of the streamed content so the lines on stdout stay
pipeable.
## Test coverage
Heavily tested in both layers:
- **`test/unit/lib/log-reader.test.ts`** — 28 cases covering
`parseLogLine` (every level, empty body, stack frames), `parseDuration`
(short + long forms, case/whitespace, garbage), `resolveSince`
(durations vs. ISO), `getBridgeLogPath`, `listLogFiles` (missing dir,
non-numeric suffixes, prefix-collision sessions), `readRecentLogLines`
(trailing-newline edges, blank lines, multi-rotation tail, `tail = 0`,
`--since` drops everything, `--since` across rotations, combined
`--tail` + `--since`), and 8 `followLog` cases (append detection, no
backlog by default, partial-line buffering, rotation re-open,
`startAtBeginning`, file-doesn't-exist-yet, `stop()` flushes trailing
partial line, idempotent `stop()`).
- **`test/unit/cli/logs.test.ts`** — 10 cases for the `showLogs` handler
(@-target validation, missing-session error, invalid `--since`,
stderr/stdout split, "no logs yet" header, JSON shape with mixed parsed
+ raw records, `--tail`, `--since`, rotation file count, default tail =
50).
- **`test/e2e/suites/sessions/logs.test.sh`** — 14 end-to-end cases
driving the actual CLI: error paths, `--json` shape, `-n` / `--tail`
caps, `--since` in the future, rotation spanning across `.log.1` /
`.log.2`, chronological ordering across rotations, `--since` across
rotations, `_mcpc.logPath` / `_mcpc.logSize` on session JSON, and the
new "`mcpc <session> logs`" error hint.
- **`test/e2e/suites/sessions/unauthorized-persist.test.sh`** updated to
assert the new hint instead of the old "check logs at …" wording.
Unit suite is 633 tests (20 files), all green; the new E2E suite is also
green locally on Node.
## Test plan
- [x] `pnpm run lint`
- [x] `pnpm run build`
- [x] `pnpm run test:unit` (vitest, 633 tests)
- [x] `bash test/e2e/run.sh sessions/logs.test.sh`
- [x] `bash test/e2e/run.sh sessions/unauthorized-persist.test.sh`
- [ ] CI: Node + Bun E2E green
https://claude.ai/code/session_01C8o7VrBnDQfQ3QywPe5tMm
---------
Co-authored-by: Claude <noreply@anthropic.com> J
Jan Curn committed
9680ef4bb17ae738c5ea81fb37a4e65ba2c019e8
Parent: f1bf039
Committed by GitHub <noreply@github.com>
on 5/29/2026, 10:07:22 PM