Metadata-Version: 2.4
Name: scribesh
Version: 1.0.0
Summary: Local-only shell wrapper that documents an interactive remote-shell session into a per-host Markdown file.
Author: Philipp Herkert
License-Expression: MIT
Project-URL: Homepage, https://github.com/BearlyStable/scribe
Project-URL: Repository, https://github.com/BearlyStable/scribe
Project-URL: Issues, https://github.com/BearlyStable/scribe/issues
Keywords: rlwrap,ssh,pentest,session,shell,logging,documentation
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Information Technology
Classifier: Operating System :: POSIX
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Security
Classifier: Topic :: System :: Systems Administration
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: test
Requires-Dist: pytest; extra == "test"
Requires-Dist: pexpect; extra == "test"
Dynamic: license-file

# scribe

A **local-only** shell wrapper that documents an interactive remote-shell session
(ssh, proxychains+ssh, …) into a per-host Markdown file **as you work**, with `rlwrap`
line editing (`-a -c -A -r`) on top. Nothing ever runs on the remote host — scribe only
*observes* what you type and what the child prints. See `CLAUDE.md` for the full design,
the local-only invariant, and the phase-by-phase build.

## Requirements

- `rlwrap` (`sudo dnf install rlwrap` / `apt install rlwrap`)
- `script` (util-linux; used for the raw-log safety net — optional, auto-detected)
- Python ≥ 3.11

## Install

```sh
pipx install .                       # from a checkout (recommended)
pipx install git+<repo-url>          # or straight from git, no checkout needed
```

`pipx` puts `scribe` on your `PATH` in its own isolated venv — nothing leaks into your system
Python. A plain `pip install .` (or `pip install -e .`) works too if you'd rather manage the
environment yourself.

`rlwrap` execs the filter module by path, so it must stay executable. A wheel install (what
`pipx` builds) can drop that `+x` bit, so `scribe` re-asserts it at startup — no editable
install required. The filter runs under whatever `python3` resolves to at runtime and imports
the installed `scribe` package; that needs Python ≥ 3.11 on `PATH`, same as the launcher.

## Use it — capture

Put `scribe` in front of however you connect. The rest of the line is passed through
verbatim (your own `pc`/`ssho`/`sshpass` wrappers, whatever — scribe never interprets them):

```sh
scribe pc 10.10.5.4:3492 sshpass -p 'secretpassword' ssho root@192.168.178.2 -D 5001 /bin/sh
```

While you work, scribe writes **`192.168.178.2.md`** in the current directory (IP picked from
the `user@host` target; falls back to `session-<timestamp>` if none), rewritten atomically after
**every command** — so a `kill -9` never loses a committed command. It also keeps a flushed raw
typescript **`192.168.178.2.<timestamp>.raw`** alongside as ground-truth backup.

The `.md` has `## access` (your exact invocation), `## enumeration` (a placeholder, filled later
by AI), and `## commands` — each distinct command (arguments matter) with its latest output,
ordered by a sensible heuristic (identity → users → network → processes → …), long output folded
into `<details>`.

- **Ctrl-T** pauses/resumes recording mid-session (shows `[scribe: recording OFF/ON]`); the `.raw`
  log keeps going regardless. Set `SCRIBE_NO_HOTKEY=1` to keep your own Ctrl-T binding.
- Reconnecting to the same host merges into the same `.md` (last output wins); a different host
  that collides on a filename gets `-2`, never an overwrite.

## Use it — enumerate (offline AI)

scribe fills every mechanical section; the `## enumeration` prose is written **later, offline**,
by opencode + a small model (e.g. qwen). It reads only the file scribe produced — still local.

```sh
# in opencode, with 192.168.178.2.md in the working dir:
/enumerate 192.168.178.2.md
```

…or paste `prompts/enumerate.md` into any model. It fills only the region between the
`<!-- SCRIBE:ENUM:START/END -->` markers with a terse, evidence-cited summary (OS, privilege,
users, network/pivot, egress, security stack, monitoring, verdict), writing `not observed` rather
than guessing. Edit **`enum_signatures.txt`** to add the AV/EDR/monitoring process names specific
to your estates — scribe pre-greps recorded output for them and drops a `scribe-signals` hint into
the file for the model to lean on.

## Single-file deploy (no `pip install` on the target)

For a target host you don't want to (or can't) install anything Python-packaging-shaped on,
build one self-contained executable file and scp it over instead:

```sh
python scripts/build_release.py                       # writes dist/scribe
scp dist/scribe user@target:~/scribe
ssh user@target 'chmod +x ~/scribe && ~/scribe pc ... ssho ...'
```

`dist/scribe` is a plain `.py` file (with a `#!/usr/bin/env python3` shebang) that
base64-embeds the entire `scribe` package — the target only needs `python3` and `rlwrap`
(and, optionally, `script`), nothing else from this repo. It works by pointing rlwrap's `-z`
filter back at *itself*: run normally, it execs `rlwrap ... -z <its own name>`; rlwrap then
re-execs that same file as the filter subprocess, which dispatches into `scribe.filter.main()`
instead of `scribe.cli.main()` the second time round (see `CLAUDE.md`, "Phase 6" for the full
mechanism). Nothing about *what* scribe does or sends changes — this is purely a packaging
trick, proven end to end by `tests/test_build_release.py`'s bundle integration test.

- **`./scribe --dump-enum`** writes the embedded `prompts/enumerate.md` and
  `enum_signatures.txt` into the current directory — handy on a host with nothing else from
  the scribe repo on it.
- **`python scripts/build_release.py --check dist/scribe`** regenerates the bundle in memory
  from the current source tree and diffs it against an existing release file (non-zero exit on
  a mismatch) — confirms a previously-built `dist/scribe` still matches `main` before you ship
  it.

## Environment knobs

| Var | Effect |
|---|---|
| `SCRIBE_RLWRAP` | path to the rlwrap binary (default `rlwrap`) |
| `SCRIBE_SCRIPT` | path to `script` (default `script`) |
| `SCRIBE_NO_RAW` | disable the `.raw` typescript safety net |
| `SCRIBE_NO_HOTKEY` | don't bind Ctrl-T (keep your own inputrc binding) |
| `SCRIBE_FILTER_DIR` | override where rlwrap looks for the filter (default: the package's own directory) — set automatically by the single-file bundle; not normally needed by hand |
| `SCRIBE_FILTER_NAME` | override the bare filename passed to rlwrap's `-z` (default `filter.py`) — ditto |
| `SCRIBE_RUN_AS_FILTER` | internal: set automatically so a re-exec'd single-file bundle knows to run as the filter, not the launcher |

## Licensing

scribe is **MIT-licensed** (see [`LICENSE`](LICENSE)) — Copyright (c) 2026 Philipp Herkert.

Every file in this repository is scribe's own code. `scribe/_filterproto.py` is an independent
implementation of [rlwrap](https://github.com/hanslub42/rlwrap)'s filter wire protocol (the
length-prefixed tagged-message exchange over rlwrap's `-z` pipes) — written from the protocol
itself, not derived from rlwrap's own GPL filter library — so scribe carries no third-party
copyleft. scribe still *runs under* rlwrap (a separate program you install), but ships none of
its code.
