diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c653d4d..4e01b16 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,7 @@ jobs: # resolves to nothing — it has to be a full version. - uses: astral-sh/setup-uv@v10.0.1 - # A mismatch would publish v0.2.0 as an asset named lochy-0.1.0, and the + # A mismatch would publish v0.2.0 as an asset named loch-0.1.0, and the # install path is a pinned asset URL, so it is not something to discover # afterwards. - name: Check the tag against the packaged version @@ -39,7 +39,7 @@ jobs: run: | python3 -m venv /tmp/smoke /tmp/smoke/bin/pip install --quiet dist/*.whl - test "$(/tmp/smoke/bin/lochy --version)" = "lochy ${GITHUB_REF_NAME#v}" + test "$(/tmp/smoke/bin/loch --version)" = "loch ${GITHUB_REF_NAME#v}" # Both artifacts: the wheel is what `uv tool install` points at, the # sdist is what anyone building from source needs. diff --git a/CLAUDE.md b/CLAUDE.md index 8f89852..5c3af6a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,11 +1,11 @@ -# lochy — repo overview for Claude +# loch — repo overview for Claude ## What this is A standalone CLI that makes agent coding sessions portable between machines. Agent CLIs store transcripts in machine-local directories keyed by the working directory they ran in, which strands a session on the -laptop that produced it. `lochy` packs those transcripts into a +laptop that produced it. `loch` packs those transcripts into a content-addressed bundle, stores it anywhere S3-compatible, and unpacks it elsewhere with paths rewritten so the session actually resumes. @@ -105,7 +105,7 @@ public API. Re-verify if something breaks. disambiguator**, and the only one available — no field marks which cwd is current. - `"cwd"` also appears *inside* tool inputs and results, naming some - other process's directory. lochy's own `--json` output is one such + other process's directory. loch's own `--json` output is one such source, so anything counting cwds has to parse each record and read the top-level field, not grep the text. - Scale reference: 750MB across 428 project dirs on one active machine; @@ -129,7 +129,7 @@ public API. Re-verify if something breaks. Python 3.10 through 3.13, Poetry, boto3 behind an extra. ``` -lochy/ +loch/ ├── cli.py # entry point, arg parsing, list/save/restore/delete/reindex ├── claude.py # Claude Code adapter: path encoding, session discovery, metadata ├── bundle.py # bundle format, gzip, content-addressed ref @@ -295,7 +295,7 @@ given and neither is inferred. **Leaving foreign cwds unrewritten is possible — it is just not better.** The obvious objection, that it needs knowing which subtrees exist on the -destination, does not apply: lochy has the exact set of candidate cwds +destination, does not apply: loch has the exact set of candidate cwds already, having computed them to choose one, and those specific strings are known to be session working directories rather than dotfile or config references. So the rule "the home pair declines a value exactly equal to @@ -325,7 +325,7 @@ single-pass guarantee depends on a nested path matching before the shorter prefix that contains it, and caller order is arbitrary. This is a "keep doing that" note, not a change. -What makes it safe is that **the caller states the mapping; lochy never +What makes it safe is that **the caller states the mapping; loch never infers it.** The first consumer (Harness "send worktree to remote") can name all four paths without a guess: the origin worktree is what the user selected, the origin repo root comes from @@ -406,7 +406,7 @@ produce the same ref. ## The JSON contract -`lochy` is meant to be embedded in other tools — Harness is the first — and +`loch` is meant to be embedded in other tools — Harness is the first — and a caller that scrapes prose breaks the moment someone rewords a sentence. So every command takes `--json`, accepted either before or after the subcommand, and answers with exactly one document on stdout: @@ -523,14 +523,14 @@ is pinned to the floor rather than following the running interpreter, so a **boto3 is an optional `[s3]` extra.** What makes that safe is the lazy import in `S3Store._client`: the `file://` backend loads nothing third-party at runtime, so the SDK was only ever an install-time cost — 8 packages and 27MB, -of which botocore alone is 24MB against lochy's own 172KB. A default install +of which botocore alone is 24MB against loch's own 172KB. A default install is one package and 184KB. Keep the import inside the method, and keep any new `botocore` import *after* the `_client()` call that would have failed first, or the SDK's absence surfaces as a bare `ImportError` and gets mislabelled `store-unreachable`. The dev group still carries boto3, moto and the stubs, so the S3 backend stays fully covered by the suite. -**`lochy --version` reads package metadata**, so pyproject is the only place +**`loch --version` reads package metadata**, so pyproject is the only place the number is set. `importlib.metadata` raises `PackageNotFoundError` in a source checkout that was never installed, which reports `"unknown"` — pyproject can't be read as a fallback, since `tomllib` arrived in 3.11 and the floor is @@ -581,7 +581,7 @@ records were on `feat/send-worktree-to-remote`. Branch and version are now read from records carrying the *chosen* cwd. This is worth its own release note rather than folding into the cwd fix, because the blast radius is different: a consumer that carries the branch itself sees a -healthy-looking transfer while the lochy entry sits under a name nobody +healthy-looking transfer while the loch entry sits under a name nobody would look for it on, and every already-saved bundle from a moved session is filed wrong until re-saved or reindexed. @@ -612,7 +612,7 @@ coverage now, which is more than it had, but mocks agree with your assumptions by construction. A live run failed on `Access Denied` (the IAM user lacked `s3:PutObject`); the region mismatch that precedes that failure surfaces as "The bucket you are attempting to access must be -addressed using the specified endpoint," so set `LOCHY_S3_REGION` +addressed using the specified endpoint," so set `LOCH_S3_REGION` when the bucket's region differs from the AWS config default. Listing and deleting need IAM actions the bucket policy may not grant @@ -648,6 +648,16 @@ CI and no release, so the first tag push is also the first execution of the release path. No release exists yet, so the wheel URL in the README points at a `v0.1.0` that has to be cut before it resolves. +The rename from lochy carried no compatibility shim: `LOCHY_STORE`, +`LOCHY_S3_ENDPOINT`, `LOCHY_S3_REGION` and the `~/.lochy/store` default are +simply no longer read. That was chosen because nothing had been released and +no consumer set the old variables — but the failure it leaves is the silent +kind, so it is worth knowing about: a stale `LOCHY_STORE=s3://...` makes +`save` succeed into the local default with `ok: true`, and an unmoved +`~/.lochy` makes `list --remote` an empty, healthy-looking answer. If a +shim is ever wanted, the shape is to read `LOCH_*` first, fall back to the +old name, and warn on stderr in text mode only. + Also missing: `restore --map` (design intent recorded above, deliberately not built on this branch), any other index dimension (`cwd` is the obvious next one, and the layout takes it without a migration), any git diff --git a/README.md b/README.md index 2ecba7b..d591b0d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# lochy +# loch Save and restore agent coding sessions across machines. @@ -7,7 +7,7 @@ working directory they ran in. That makes a session effectively stuck to the laptop that produced it — you can't hand a teammate the conversation that produced a branch, and you can't pick one up on another machine. -`lochy` packs those transcripts into a portable, content-addressed +`loch` packs those transcripts into a portable, content-addressed bundle, stores it anywhere S3-compatible, and unpacks it on another machine with the paths rewritten so the session actually resumes. @@ -16,14 +16,14 @@ Currently supports Claude Code. ## Install ```sh -uv tool install https://github.com/ness-dev/lochy/releases/download/v0.1.0/lochy-0.1.0-py3-none-any.whl -lochy --version +uv tool install https://github.com/ness-dev/loch/releases/download/v0.1.0/loch-0.1.0-py3-none-any.whl +loch --version ``` The wheel is `py3-none-any`, and [uv](https://docs.astral.sh/uv/) is a static binary that brings its own CPython, so this needs no Python on the machine and no PyPI account behind it. Releases are attached to -[GitHub releases](https://github.com/ness-dev/lochy/releases) as a wheel +[GitHub releases](https://github.com/ness-dev/loch/releases) as a wheel and an sdist. Python 3.10 or newer if you'd rather install it yourself — `pipx install`, @@ -34,7 +34,7 @@ in. The backend imports boto3 lazily, so a `file://` store never loads it and paying 27MB for a dependency most installs never reach isn't worth it: ```sh -uv tool install "lochy[s3] @ https://github.com/ness-dev/lochy/releases/download/v0.1.0/lochy-0.1.0-py3-none-any.whl" +uv tool install "loch[s3] @ https://github.com/ness-dev/loch/releases/download/v0.1.0/loch-0.1.0-py3-none-any.whl" ``` Without it, an `s3://` store fails with `code: "s3-extra-missing"` rather than @@ -44,7 +44,7 @@ For development: ```sh poetry install # brings boto3, so the S3 backend is covered by the tests -poetry run lochy --help +poetry run loch --help poetry run pytest ``` @@ -52,27 +52,27 @@ poetry run pytest ```sh # what's on this machine for the current repo? -lochy list -lochy list --branch feature/checkout-flow +loch list +loch list --branch feature/checkout-flow # pack them up and push to a store -lochy save --branch feature/checkout-flow --store s3://my-bucket/sessions +loch save --branch feature/checkout-flow --store s3://my-bucket/sessions # -> packed e71cdcb1... [feature/checkout-flow] — redacted 2 secrets # -> ref 10482276de745032... # on another machine: what's been saved for the branch I'm on? -lochy list --remote --store s3://my-bucket/sessions -lochy list --remote --branch feature/checkout-flow -lochy list --all # every branch in the store +loch list --remote --store s3://my-bucket/sessions +loch list --remote --branch feature/checkout-flow +loch list --all # every branch in the store -lochy restore 10482276de745032... --store s3://my-bucket/sessions +loch restore 10482276de745032... --store s3://my-bucket/sessions # -> cd /path/to/repo && claude --resume e71cdcb1-7c2c-410b-8c73-91cdf0cba4b8 # a transcript that shouldn't be there any more -lochy delete 10482276de745032... +loch delete 10482276de745032... # rebuild the index from the bundles, if a save was interrupted -lochy reindex +loch reindex ``` `save` bundles every session matching the filter, so a branch with several @@ -84,22 +84,22 @@ Every command takes `--json`, before or after the subcommand, and prints one document on stdout instead of the text above: ```sh -lochy save --json --branch feature/checkout-flow --store /tmp/outbound +loch save --json --branch feature/checkout-flow --store /tmp/outbound # {"schema":1,"ok":true,"command":"save","ref":"10482276...","bytes":4712, # "redacted":2,"sessions":[{"sessionId":"e71cdcb1-...","branch":"feature/checkout-flow", # "redactions":{"aws-access-key":1,"jwt":1}}],"indexed":[...]} -ref=$(lochy save --json --store /tmp/outbound | jq -r .ref) -lochy restore --json "$ref" --into /path/to/repo --store /tmp/outbound +ref=$(loch save --json --store /tmp/outbound | jq -r .ref) +loch restore --json "$ref" --into /path/to/repo --store /tmp/outbound -lochy --version --json +loch --version --json # {"schema":1,"ok":true,"command":"version","version":"0.1.0"} ``` Failures are documents too, on stdout, so one stream carries both outcomes: ```sh -lochy restore --json deadbeef --store /tmp/outbound; echo "exit $?" +loch restore --json deadbeef --store /tmp/outbound; echo "exit $?" # {"schema":1,"ok":false,"command":"restore","code":"bundle-not-found", # "error":"could not read deadbeef from /tmp/outbound: ..."} # exit 1 @@ -125,9 +125,9 @@ bundles/.loch the bundle, named by the hash of its bytes index/branch// a small entry per branch the bundle touches ``` -`lochy list --remote` reads only the index, so listing a branch never +`loch list --remote` reads only the index, so listing a branch never downloads a bundle. Entries are derived from the bundles and hold nothing -that isn't already in them — `lochy reindex` rebuilds the whole index by +that isn't already in them — `loch reindex` rebuilds the whole index by scanning `bundles/`, and drops entries no bundle backs. Branch names are percent-encoded, so `feature/foo` stays one path segment @@ -148,9 +148,14 @@ itself for `list`/`reindex` and `s3:DeleteObject` for `delete`. | Variable | Purpose | | --- | --- | -| `LOCHY_STORE` | default store URI (else `~/.lochy/store`) | -| `LOCHY_S3_ENDPOINT` | custom endpoint for R2, MinIO, Backblaze, ... | -| `LOCHY_S3_REGION` | region override | +| `LOCH_STORE` | default store URI (else `~/.loch/store`) | +| `LOCH_S3_ENDPOINT` | custom endpoint for R2, MinIO, Backblaze, ... | +| `LOCH_S3_REGION` | region override | + +These were `LOCHY_*` before the rename, and the default store was +`~/.lochy/store`. The old names are not read, so an old variable is silently +ignored rather than rejected — rename it, and `mv ~/.lochy ~/.loch` to keep +existing bundles visible. Credentials come from the standard AWS chain, so `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY`, a shared profile, or an instance role all work. diff --git a/lochy/__init__.py b/loch/__init__.py similarity index 100% rename from lochy/__init__.py rename to loch/__init__.py diff --git a/lochy/bundle.py b/loch/bundle.py similarity index 100% rename from lochy/bundle.py rename to loch/bundle.py diff --git a/lochy/claude.py b/loch/claude.py similarity index 98% rename from lochy/claude.py rename to loch/claude.py index 0b5bbee..02fd0ab 100644 --- a/lochy/claude.py +++ b/loch/claude.py @@ -92,7 +92,7 @@ def _records(raw: str) -> Iterator[dict[str, Any]]: def transcript_cwds(raw: str) -> dict[str, int]: """Every cwd a transcript's records name, with how many name it, in order of first appearance. Only the top-level field counts: `"cwd"` also occurs - inside tool inputs and results — lochy's own --json output among them — + inside tool inputs and results — loch's own --json output among them — where it names some other process's directory.""" counts: dict[str, int] = {} for record in _records(raw): diff --git a/lochy/cli.py b/loch/cli.py similarity index 96% rename from lochy/cli.py rename to loch/cli.py index 7fa41b5..75d711f 100644 --- a/lochy/cli.py +++ b/loch/cli.py @@ -55,16 +55,16 @@ ) from .store import MissingDependency, MissingObject, create_store, resolve_store_uri -USAGE = """lochy — save and restore agent coding sessions across machines +USAGE = """loch — save and restore agent coding sessions across machines Usage: - lochy list [--cwd ] [--branch ] - lochy list --remote [--branch ] [--all] [--store ] - lochy save [--cwd ] [--branch ] [--session ] [--store ] - lochy restore [--into ] [--store ] [--force] [--new-id] - lochy delete [--store ] - lochy reindex [--store ] - lochy --version + loch list [--cwd ] [--branch ] + loch list --remote [--branch ] [--all] [--store ] + loch save [--cwd ] [--branch ] [--session ] [--store ] + loch restore [--into ] [--store ] [--force] [--new-id] + loch delete [--store ] + loch reindex [--store ] + loch --version Every command takes --json, which replaces the text below with one machine- readable document on stdout. @@ -74,9 +74,9 @@ s3://bucket/prefix any S3-compatible endpoint Environment: - LOCHY_STORE default store URI - LOCHY_S3_ENDPOINT custom endpoint (R2, MinIO, ...) - LOCHY_S3_REGION region override + LOCH_STORE default store URI + LOCH_S3_ENDPOINT custom endpoint (R2, MinIO, ...) + LOCH_S3_REGION region override """ @@ -162,7 +162,7 @@ def print_help(self, file: Any = None) -> NoReturn: def _parser(command: str) -> argparse.ArgumentParser: - parser = _Parser(prog=f"lochy {command}") + parser = _Parser(prog=f"loch {command}") # Registered so the flag parses after the subcommand; main() reads the mode # off argv itself, because a failure before or during parsing has to render # in the mode the caller asked for. @@ -600,7 +600,7 @@ def installed_version() -> str: that was never installed has no metadata to read, and pyproject can't be parsed for it either — tomllib arrived in 3.11 and the floor is 3.10.""" try: - return importlib.metadata.version("lochy") + return importlib.metadata.version("loch") except importlib.metadata.PackageNotFoundError: return "unknown" @@ -608,7 +608,7 @@ def installed_version() -> str: def command_version(argv: list[str]) -> Result: version = installed_version() return Result( - command="version", payload={"version": version}, text=f"lochy {version}\n" + command="version", payload={"version": version}, text=f"loch {version}\n" ) @@ -648,7 +648,7 @@ def main() -> None: try: handler = COMMANDS.get(command) if handler is None: - fail("unknown-command", f"unknown command '{command}' (try: lochy help)") + fail("unknown-command", f"unknown command '{command}' (try: loch help)") result = handler(argv[1:]) except HelpRequested as help_text: result = Result(command, {"usage": help_text.text}, text=help_text.text) diff --git a/lochy/git.py b/loch/git.py similarity index 100% rename from lochy/git.py rename to loch/git.py diff --git a/lochy/index.py b/loch/index.py similarity index 100% rename from lochy/index.py rename to loch/index.py diff --git a/lochy/output.py b/loch/output.py similarity index 96% rename from lochy/output.py rename to loch/output.py index 3cd4e64..f186baf 100644 --- a/lochy/output.py +++ b/loch/output.py @@ -75,7 +75,7 @@ def emit_error(command: str, error: CommandError, as_json: bool) -> NoReturn: else: for warning in error.warnings: sys.stderr.write(warning) - sys.stderr.write(error.prose if error.prose else f"lochy: {error.message}\n") + sys.stderr.write(error.prose if error.prose else f"loch: {error.message}\n") raise SystemExit(error.exit_status) diff --git a/lochy/redact.py b/loch/redact.py similarity index 100% rename from lochy/redact.py rename to loch/redact.py diff --git a/lochy/rewrite.py b/loch/rewrite.py similarity index 100% rename from lochy/rewrite.py rename to loch/rewrite.py diff --git a/lochy/store.py b/loch/store.py similarity index 95% rename from lochy/store.py rename to loch/store.py index fdf1e46..a1106ad 100644 --- a/lochy/store.py +++ b/loch/store.py @@ -8,7 +8,7 @@ if TYPE_CHECKING: from mypy_boto3_s3.client import S3Client -DEFAULT_STORE = os.path.join(str(Path.home()), ".lochy", "store") +DEFAULT_STORE = os.path.join(str(Path.home()), ".loch", "store") class MissingObject(Exception): @@ -101,11 +101,11 @@ def _client(self) -> "S3Client": except ImportError as error: raise MissingDependency( "the S3 backend needs boto3, which this install doesn't have " - "(reinstall as lochy[s3])" + "(reinstall as loch[s3])" ) from error - endpoint = os.environ.get("LOCHY_S3_ENDPOINT") - region = os.environ.get("LOCHY_S3_REGION") + endpoint = os.environ.get("LOCH_S3_ENDPOINT") + region = os.environ.get("LOCH_S3_REGION") return boto3.client( "s3", endpoint_url=endpoint, @@ -175,4 +175,4 @@ def create_store(uri: str) -> Store: def resolve_store_uri(explicit: str | None = None) -> str: if explicit is not None: return explicit - return os.environ.get("LOCHY_STORE", DEFAULT_STORE) + return os.environ.get("LOCH_STORE", DEFAULT_STORE) diff --git a/pyproject.toml b/pyproject.toml index abd29ac..d1dfbc0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [project] -name = "lochy" +name = "loch" version = "0.1.0" description = "Save and restore agent coding sessions across machines" authors = [{ name = "Mike Lyons", email = "mdl0394@gmail.com" }] @@ -14,14 +14,14 @@ dependencies = [] s3 = ["boto3 (>=1.37.31,<2.0.0)"] [project.scripts] -lochy = "lochy.cli:main" +loch = "loch.cli:main" [build-system] requires = ["poetry-core>=2.0.0"] build-backend = "poetry.core.masonry.api" [tool.poetry] -packages = [{ include = "lochy" }] +packages = [{ include = "loch" }] [tool.poetry.group.dev.dependencies] pytest = "^8.2.2" diff --git a/release.sh b/release.sh index 9e0dd7f..bfdc24e 100755 --- a/release.sh +++ b/release.sh @@ -42,7 +42,7 @@ path.write_text(new) PY grep -q "^version = \"$version\"$" pyproject.toml || die "version bump did not apply" -# Not asserting `lochy --version` here: it reads installed package metadata, +# Not asserting `loch --version` here: it reads installed package metadata, # which still holds the old number until the bumped project is reinstalled. # The release workflow smoke-tests it against a freshly built wheel instead. echo "==> verifying" @@ -75,5 +75,5 @@ cat < Redaction: raise RedactionError("still matching") - monkeypatch.setattr("lochy.claude.redact", refuse) + monkeypatch.setattr("loch.claude.redact", refuse) path = write_session(tmp_path, "/Users/mike/proj", "abc", summary="anything") meta = read_session_meta(str(path)) @@ -231,7 +231,7 @@ def test_falls_back_to_the_first_cwd_when_the_directory_matches_nothing( def test_transcript_cwds_counts_only_the_top_level_field() -> None: - """`"cwd"` also occurs inside tool inputs and results — lochy's own --json + """`"cwd"` also occurs inside tool inputs and results — loch's own --json output among them — where it names some other process's directory.""" raw = "\n".join( [ diff --git a/test/test_cli.py b/test/test_cli.py index a92bed3..1925cff 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -8,9 +8,9 @@ import pytest from test_claude import SECRET, write_moved_session, write_session -from lochy.bundle import unpack_bundle -from lochy.claude import session_dir_for, transcript_cwds, transcript_path_for -from lochy.cli import main +from loch.bundle import unpack_bundle +from loch.claude import session_dir_for, transcript_cwds, transcript_path_for +from loch.cli import main def invoke( @@ -20,7 +20,7 @@ def invoke( *argv: str, ) -> tuple[str, str, int | None]: monkeypatch.setenv("HOME", str(home)) - monkeypatch.setattr("sys.argv", ["lochy", *argv]) + monkeypatch.setattr("sys.argv", ["loch", *argv]) code: int | None = None try: main() @@ -49,17 +49,17 @@ def test_help_is_printed_without_a_command( monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], tmp_path: Path ) -> None: out, _, code = invoke(monkeypatch, capsys, tmp_path) - assert out.startswith("lochy — save and restore") + assert out.startswith("loch — save and restore") assert code is None def test_version_is_reported_in_both_renderings( monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], tmp_path: Path ) -> None: - installed = importlib.metadata.version("lochy") + installed = importlib.metadata.version("loch") out, _, code = invoke(monkeypatch, capsys, tmp_path, "--version") - assert (out, code) == (f"lochy {installed}\n", None) + assert (out, code) == (f"loch {installed}\n", None) document, _, code = invoke_json( monkeypatch, capsys, tmp_path, "--version", "--json" @@ -83,7 +83,7 @@ def test_unknown_command_fails( monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], tmp_path: Path ) -> None: _, err, code = invoke(monkeypatch, capsys, tmp_path, "bogus") - assert err == "lochy: unknown command 'bogus' (try: lochy help)\n" + assert err == "loch: unknown command 'bogus' (try: loch help)\n" assert code == 1 @@ -235,7 +235,7 @@ def test_restore_skips_an_existing_session_unless_forced( str(store), ) assert "skipped abc (already exists; --force to overwrite)" in err - assert err.endswith("lochy: nothing restored\n") + assert err.endswith("loch: nothing restored\n") assert code == 1 out, _, _ = invoke( @@ -301,7 +301,7 @@ def test_save_fails_when_nothing_matches( "--store", str(tmp_path / "store"), ) - assert err.startswith("lochy: no claude-code sessions found for") + assert err.startswith("loch: no claude-code sessions found for") assert code == 1 @@ -309,7 +309,7 @@ def test_restore_requires_a_ref( monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], tmp_path: Path ) -> None: _, err, code = invoke(monkeypatch, capsys, tmp_path, "restore") - assert err == "lochy: restore requires a bundle ref\n" + assert err == "loch: restore requires a bundle ref\n" assert code == 1 @@ -319,7 +319,7 @@ def test_restore_reports_an_unreadable_ref( _, err, code = invoke( monkeypatch, capsys, tmp_path, "restore", "deadbeef", "--store", str(tmp_path) ) - assert err.startswith("lochy: could not read deadbeef from") + assert err.startswith("loch: could not read deadbeef from") assert code == 1 @@ -386,7 +386,7 @@ def test_list_remote_defaults_to_the_branch_you_are_on( home = tmp_path / "home" store = tmp_path / "store" ref = save_two_branches(monkeypatch, capsys, home, tmp_path / "proj", store) - monkeypatch.setattr("lochy.cli.current_branch", lambda cwd: "feature/foo") + monkeypatch.setattr("loch.cli.current_branch", lambda cwd: "feature/foo") out, _, _ = invoke( monkeypatch, capsys, home, "list", "--remote", "--store", str(store) @@ -431,7 +431,7 @@ def test_delete_removes_the_bundle_and_its_entries( _, err, code = invoke( monkeypatch, capsys, home, "restore", ref, "--store", str(store) ) - assert err.startswith(f"lochy: could not read {ref} from") + assert err.startswith(f"loch: could not read {ref} from") assert code == 1 @@ -441,7 +441,7 @@ def test_delete_reports_a_ref_that_is_not_there( _, err, code = invoke( monkeypatch, capsys, tmp_path, "delete", "deadbeef", "--store", str(tmp_path) ) - assert err.startswith("lochy: could not delete deadbeef from") + assert err.startswith("loch: could not delete deadbeef from") assert code == 1 @@ -974,14 +974,14 @@ def test_json_separates_an_uninstalled_sdk_from_a_store_that_did_not_answer( "s3://sessions/prefix", ) assert (document["code"], code) == ("s3-extra-missing", 1) - assert "lochy[s3]" in document["error"] + assert "loch[s3]" in document["error"] # With the SDK present, a store that doesn't answer is still that. def refuse(self: Any) -> None: raise OSError("connection refused") monkeypatch.delitem(sys.modules, "boto3") - monkeypatch.setattr("lochy.store.S3Store._client", refuse) + monkeypatch.setattr("loch.store.S3Store._client", refuse) document, _, code = invoke_json( monkeypatch, capsys, @@ -1040,7 +1040,7 @@ def test_json_help_is_a_document_rather_than_the_usage_block( ) -> None: document, _, code = invoke_json(monkeypatch, capsys, tmp_path, "--json") assert (document["command"], document["ok"], code) == ("help", True, None) - assert document["usage"].startswith("lochy — save and restore") + assert document["usage"].startswith("loch — save and restore") document, _, code = invoke_json( monkeypatch, capsys, tmp_path, "save", "--json", "--help" diff --git a/test/test_git.py b/test/test_git.py index 63f04e2..772f9ba 100644 --- a/test/test_git.py +++ b/test/test_git.py @@ -1,7 +1,7 @@ import subprocess from pathlib import Path -from lochy.git import current_branch +from loch.git import current_branch def test_current_branch_reads_the_checkout(tmp_path: Path) -> None: diff --git a/test/test_index.py b/test/test_index.py index a6087db..be1705d 100644 --- a/test/test_index.py +++ b/test/test_index.py @@ -4,7 +4,7 @@ import pytest from moto import mock_aws -from lochy.bundle import ( +from loch.bundle import ( BUNDLE_VERSION, Bundle, BundleOrigin, @@ -12,7 +12,7 @@ bundle_ref, pack_bundle, ) -from lochy.index import ( +from loch.index import ( BRANCH, bundle_key, decode_segment, @@ -27,7 +27,7 @@ reindex, value_prefix, ) -from lochy.store import FileStore, MissingObject, S3Store +from loch.store import FileStore, MissingObject, S3Store def session(session_id: str, branch: str | None, cwd: str = "/repo") -> BundleSession: @@ -213,8 +213,8 @@ def test_an_encoded_branch_survives_a_round_trip_through_s3( monkeypatch.setenv("AWS_ACCESS_KEY_ID", "testing") monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "testing") monkeypatch.setenv("AWS_DEFAULT_REGION", "us-east-1") - monkeypatch.delenv("LOCHY_S3_ENDPOINT", raising=False) - monkeypatch.delenv("LOCHY_S3_REGION", raising=False) + monkeypatch.delenv("LOCH_S3_ENDPOINT", raising=False) + monkeypatch.delenv("LOCH_S3_REGION", raising=False) boto3.client("s3", region_name="us-east-1").create_bucket(Bucket="sessions") store = S3Store("sessions", "prefix") diff --git a/test/test_output.py b/test/test_output.py index 3208787..c9ed15b 100644 --- a/test/test_output.py +++ b/test/test_output.py @@ -2,7 +2,7 @@ import pytest -from lochy.output import SCHEMA, CommandError, Result, document, emit, emit_error +from loch.output import SCHEMA, CommandError, Result, document, emit, emit_error def test_a_success_envelope_carries_no_error_fields() -> None: @@ -58,4 +58,4 @@ def test_an_error_exits_nonzero_in_both_modes( out, err = capsys.readouterr() assert failure.value.code == 2 assert out == "" - assert err == "lochy: bad flag\n" + assert err == "loch: bad flag\n" diff --git a/test/test_redact.py b/test/test_redact.py index 34adbfd..f6c52eb 100644 --- a/test/test_redact.py +++ b/test/test_redact.py @@ -2,7 +2,7 @@ import pytest -from lochy.redact import RULES, RedactionError, Rule, redact, scan, summarize +from loch.redact import RULES, RedactionError, Rule, redact, scan, summarize # Structurally valid, obviously synthetic. Nothing here is or was a credential. FAKE = { @@ -95,7 +95,7 @@ def test_ordinary_transcript_content_survives_untouched() -> None: line(message="I refactored rewrite.py to keep the single-pass property."), line(commit="c4056e9a1f2b3c4d5e6f708192a3b4c5d6e7f809"), line(uuid="3f2a1b4c-5d6e-4f70-8192-a3b4c5d6e7f8"), - line(note="see https://github.com/ness-dev/lochy for the source"), + line(note="see https://github.com/ness-dev/loch for the source"), line(usage={"input_tokens": 12345678, "output_tokens": 4096}), line(path="~/.claude/projects/-Users-mike-proj/abc.jsonl"), line(sha256="a" * 64), diff --git a/test/test_rewrite.py b/test/test_rewrite.py index 45b1b5b..ab92abb 100644 --- a/test/test_rewrite.py +++ b/test/test_rewrite.py @@ -1,6 +1,6 @@ import json -from lochy.rewrite import ( +from loch.rewrite import ( ForeignCwd, RewriteSpec, foreign_cwds, @@ -216,7 +216,7 @@ def test_foreign_cwds_keys_on_the_origin_rather_than_a_reversal() -> None: def test_foreign_cwds_ignores_a_cwd_nested_in_tool_output() -> None: - """`"cwd"` also appears inside tool results — lochy's own --json output + """`"cwd"` also appears inside tool results — loch's own --json output among them — where it names some other process's directory. Counting those would fire on a restore that was entirely correct.""" raw = dumps( diff --git a/test/test_store.py b/test/test_store.py index d2c8249..bd9c821 100644 --- a/test/test_store.py +++ b/test/test_store.py @@ -6,7 +6,7 @@ import pytest from moto import mock_aws -from lochy.store import ( +from loch.store import ( DEFAULT_STORE, FileStore, MissingDependency, @@ -22,8 +22,8 @@ def aws_credentials(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("AWS_ACCESS_KEY_ID", "testing") monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "testing") monkeypatch.setenv("AWS_DEFAULT_REGION", "us-east-1") - monkeypatch.delenv("LOCHY_S3_ENDPOINT", raising=False) - monkeypatch.delenv("LOCHY_S3_REGION", raising=False) + monkeypatch.delenv("LOCH_S3_ENDPOINT", raising=False) + monkeypatch.delenv("LOCH_S3_REGION", raising=False) def test_file_store_round_trips(tmp_path: Path) -> None: @@ -96,9 +96,9 @@ def test_create_store_rejects_a_bucketless_s3_uri() -> None: def test_resolve_store_uri_prefers_explicit_then_env( monkeypatch: pytest.MonkeyPatch, ) -> None: - monkeypatch.delenv("LOCHY_STORE", raising=False) + monkeypatch.delenv("LOCH_STORE", raising=False) assert resolve_store_uri() == DEFAULT_STORE - monkeypatch.setenv("LOCHY_STORE", "s3://from-env/x") + monkeypatch.setenv("LOCH_STORE", "s3://from-env/x") assert resolve_store_uri() == "s3://from-env/x" assert resolve_store_uri("/explicit") == "/explicit" @@ -139,7 +139,7 @@ def test_s3_store_reports_an_uninstalled_sdk_as_such( lambda: S3Store("sessions", "bundles").list(""), lambda: S3Store("sessions", "bundles").delete("abc.loch"), ): - with pytest.raises(MissingDependency, match=r"lochy\[s3\]"): + with pytest.raises(MissingDependency, match=r"loch\[s3\]"): call() @@ -198,7 +198,7 @@ def test_s3_store_lists_past_the_first_page(aws_credentials: None) -> None: def test_s3_store_honours_the_region_override( aws_credentials: None, monkeypatch: pytest.MonkeyPatch ) -> None: - monkeypatch.setenv("LOCHY_S3_REGION", "us-east-2") + monkeypatch.setenv("LOCH_S3_REGION", "us-east-2") boto3.client("s3", region_name="us-east-2").create_bucket( Bucket="sessions", CreateBucketConfiguration={"LocationConstraint": "us-east-2"},