hftracer 0.1.0__tar.gz

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.
@@ -0,0 +1,119 @@
1
+ name: CI and release
2
+
3
+ on:
4
+ push:
5
+ branches: ["**"]
6
+ pull_request:
7
+ workflow_dispatch:
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ checks:
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@v7
17
+ with:
18
+ persist-credentials: false
19
+ - uses: actions/setup-python@v7
20
+ with:
21
+ python-version-file: .python-version
22
+ - uses: astral-sh/setup-uv@v9.0.0
23
+ with:
24
+ version: "0.11.2"
25
+ enable-cache: true
26
+ - run: uv sync --locked
27
+ - run: uv run --no-sync ruff check .
28
+ - run: uv run --no-sync ruff format --check .
29
+ - run: uv run --no-sync ty check
30
+ - run: uv run --no-sync pytest
31
+ - run: uv build
32
+
33
+ release:
34
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
35
+ needs: checks
36
+ runs-on: ubuntu-latest
37
+ concurrency:
38
+ group: release-please
39
+ cancel-in-progress: false
40
+ permissions:
41
+ contents: write
42
+ issues: write
43
+ pull-requests: write
44
+ actions: write
45
+ outputs:
46
+ release_created: ${{ steps.release.outputs.release_created }}
47
+ tag_name: ${{ steps.release.outputs.tag_name }}
48
+ sha: ${{ steps.release.outputs.sha }}
49
+ steps:
50
+ - uses: googleapis/release-please-action@v5
51
+ id: release
52
+ with:
53
+ target-branch: main
54
+ - name: Run checks for the release PR
55
+ if: steps.release.outputs.pr != ''
56
+ env:
57
+ GH_TOKEN: ${{ github.token }}
58
+ GH_REPO: ${{ github.repository }}
59
+ RELEASE_PR: ${{ steps.release.outputs.pr }}
60
+ run: gh workflow run ci.yml --ref "$(jq -r .headBranchName <<< "$RELEASE_PR")"
61
+
62
+ build:
63
+ if: needs.release.outputs.release_created == 'true'
64
+ needs: release
65
+ runs-on: ubuntu-latest
66
+ steps:
67
+ - uses: actions/checkout@v7
68
+ with:
69
+ ref: ${{ needs.release.outputs.sha }}
70
+ persist-credentials: false
71
+ - uses: actions/setup-python@v7
72
+ with:
73
+ python-version-file: .python-version
74
+ - uses: astral-sh/setup-uv@v9.0.0
75
+ with:
76
+ version: "0.11.2"
77
+ enable-cache: true
78
+ - run: uv sync --locked --no-dev
79
+ - run: uv build
80
+ - name: Upload distributions
81
+ uses: actions/upload-artifact@v7
82
+ with:
83
+ name: dist
84
+ path: dist/*
85
+ if-no-files-found: error
86
+
87
+ pypi:
88
+ needs: build
89
+ runs-on: ubuntu-latest
90
+ environment: pypi
91
+ permissions:
92
+ id-token: write
93
+ steps:
94
+ - uses: astral-sh/setup-uv@v9.0.0
95
+ with:
96
+ version: "0.11.2"
97
+ enable-cache: false
98
+ - uses: actions/download-artifact@v8
99
+ with:
100
+ name: dist
101
+ path: dist
102
+ - run: uv publish --trusted-publishing always
103
+
104
+ github:
105
+ needs: [release, build, pypi]
106
+ runs-on: ubuntu-latest
107
+ permissions:
108
+ contents: write
109
+ steps:
110
+ - uses: actions/download-artifact@v8
111
+ with:
112
+ name: dist
113
+ path: dist
114
+ - name: Attach distributions to the release
115
+ env:
116
+ GH_TOKEN: ${{ github.token }}
117
+ GH_REPO: ${{ github.repository }}
118
+ RELEASE_TAG: ${{ needs.release.outputs.tag_name }}
119
+ run: gh release upload "$RELEASE_TAG" dist/*
@@ -0,0 +1,8 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.py[cod]
4
+ .pytest_cache/
5
+ .ruff_cache/
6
+ dist/
7
+ *.egg-info/
8
+ .idea/
@@ -0,0 +1 @@
1
+ 3.14
@@ -0,0 +1 @@
1
+ {".":"0.1.0"}
@@ -0,0 +1,15 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 (2026-10-27)
4
+
5
+
6
+ ### Features
7
+
8
+ * add private bucket session sync ([a394504](https://github.com/twangodev/hftracer/commit/a39450451cffbbc3709230c3d472f853ef7a1f77))
9
+
10
+
11
+ ### Documentation
12
+
13
+ * simplify description and organize readme ([5ef9e09](https://github.com/twangodev/hftracer/commit/5ef9e097480b95d0790780359b495ec008eb81c1))
14
+ * simplify readme and add Shields.io badges ([9566b87](https://github.com/twangodev/hftracer/commit/9566b87fcc5ac8bf53c6dd1d5ffa542b0b5ac827))
15
+ * trim readme badges and development section ([4390d6a](https://github.com/twangodev/hftracer/commit/4390d6af8a96a64d6849e718aea9530e685126f8))
@@ -0,0 +1,39 @@
1
+ # Usage & development
2
+
3
+ ## Usage
4
+
5
+ | Command | Behavior |
6
+ | --- | --- |
7
+ | `hftracer init` | Prompt for a private bucket; default: `<username>/traces` |
8
+ | `hftracer init --bucket namespace/name` | Configure without prompting |
9
+ | `hftracer sync` | Upload new and changed files with HF progress bars |
10
+ | `hftracer sync --dry-run` | Preview counts and transfer sizes |
11
+ | `hftracer sync --preview` | Alias for `--dry-run` |
12
+
13
+ - Authentication: existing HF login or `HF_TOKEN`.
14
+ - Configuration: `$XDG_CONFIG_HOME/hftracer/config.json`; default: `~/.config/hftracer/config.json`.
15
+ - Remote paths: `devices/<device>/codex/{sessions,archived_sessions}/` and `devices/<device>/claude/projects/`.
16
+ - Device identity persists across setup runs. Keep each device's config separate.
17
+ - All files, including hidden and supporting files, are included unchanged.
18
+ - HF compares size and modification time. Same-size changes with unchanged timestamps may be skipped.
19
+ - Uploads finalize in batches. Ctrl+C preserves finalized files; the current batch may need retrying.
20
+ - Re-run after active sessions finish for a settled copy.
21
+ - Missing sources are skipped. Transfer failures return a nonzero exit code.
22
+ - Schedule externally with cron, systemd timers, or a Docker scheduler.
23
+
24
+ ## Development
25
+
26
+ ```sh
27
+ uv sync --locked
28
+ uv run pytest
29
+ uv run ty check
30
+ uv run ruff check .
31
+ uv run ruff format --check .
32
+ uv build
33
+ ```
34
+
35
+ Tests use synthetic files and mocked HF calls. Live checks are described in
36
+ [manual integration](tests/manual-integration.md).
37
+
38
+ Use focused Conventional Commits, such as `feat: ...`, `fix: ...`, `docs: ...`,
39
+ and `ci: ...`.
hftracer-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 James Ding
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,47 @@
1
+ Metadata-Version: 2.5
2
+ Name: hftracer
3
+ Version: 0.1.0
4
+ Summary: Synchronize agent traces to Hugging Face buckets.
5
+ Project-URL: Repository, https://github.com/twangodev/hftracer
6
+ Project-URL: Issues, https://github.com/twangodev/hftracer/issues
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Requires-Python: >=3.14
10
+ Requires-Dist: huggingface-hub>=1.9.0
11
+ Requires-Dist: typer>=0.24.0
12
+ Description-Content-Type: text/markdown
13
+
14
+ # hftracer
15
+
16
+ [![Python](https://img.shields.io/badge/python-3.14%2B-blue?logo=python&logoColor=white)](https://www.python.org/downloads/)
17
+ [![MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
18
+
19
+ Synchronize agent traces to Hugging Face buckets.
20
+
21
+ ## Installation
22
+
23
+ ```sh
24
+ uv tool install hftracer
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ ```sh
30
+ uvx --from huggingface-hub hf auth login
31
+ hftracer init
32
+ hftracer sync --dry-run
33
+ hftracer sync
34
+ ```
35
+
36
+ ## Supported agents
37
+
38
+ | Agent | Sources |
39
+ | --- | --- |
40
+ | Codex | `~/.codex/sessions`, `~/.codex/archived_sessions` |
41
+ | Claude Code | `~/.claude/projects` |
42
+
43
+ ## Features
44
+
45
+ - Incremental uploads with progress bars
46
+ - Original files and directory structure preserved
47
+ - Separate device namespaces; no remote deletions
@@ -0,0 +1,34 @@
1
+ # hftracer
2
+
3
+ [![Python](https://img.shields.io/badge/python-3.14%2B-blue?logo=python&logoColor=white)](https://www.python.org/downloads/)
4
+ [![MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
5
+
6
+ Synchronize agent traces to Hugging Face buckets.
7
+
8
+ ## Installation
9
+
10
+ ```sh
11
+ uv tool install hftracer
12
+ ```
13
+
14
+ ## Usage
15
+
16
+ ```sh
17
+ uvx --from huggingface-hub hf auth login
18
+ hftracer init
19
+ hftracer sync --dry-run
20
+ hftracer sync
21
+ ```
22
+
23
+ ## Supported agents
24
+
25
+ | Agent | Sources |
26
+ | --- | --- |
27
+ | Codex | `~/.codex/sessions`, `~/.codex/archived_sessions` |
28
+ | Claude Code | `~/.claude/projects` |
29
+
30
+ ## Features
31
+
32
+ - Incremental uploads with progress bars
33
+ - Original files and directory structure preserved
34
+ - Separate device namespaces; no remote deletions
@@ -0,0 +1,35 @@
1
+ [project]
2
+ name = "hftracer"
3
+ version = "0.1.0"
4
+ description = "Synchronize agent traces to Hugging Face buckets."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ requires-python = ">=3.14"
8
+ dependencies = [
9
+ "huggingface-hub>=1.9.0",
10
+ "typer>=0.24.0",
11
+ ]
12
+
13
+ [project.scripts]
14
+ hftracer = "hftracer.cli:app"
15
+
16
+ [project.urls]
17
+ Repository = "https://github.com/twangodev/hftracer"
18
+ Issues = "https://github.com/twangodev/hftracer/issues"
19
+
20
+ [build-system]
21
+ requires = ["hatchling"]
22
+ build-backend = "hatchling.build"
23
+
24
+ [dependency-groups]
25
+ dev = [
26
+ "pytest>=9.0",
27
+ "ruff>=0.14",
28
+ "ty>=0.0.1",
29
+ ]
30
+
31
+ [tool.pytest.ini_options]
32
+ testpaths = ["tests"]
33
+
34
+ [tool.ruff.lint]
35
+ select = ["E", "F", "I", "UP", "B"]
@@ -0,0 +1,16 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
3
+ "release-type": "python",
4
+ "include-component-in-tag": false,
5
+ "packages": {
6
+ ".": {
7
+ "extra-files": [
8
+ {
9
+ "type": "toml",
10
+ "path": "uv.lock",
11
+ "jsonpath": "$.package[?(@.name.value == 'hftracer')].version"
12
+ }
13
+ ]
14
+ }
15
+ }
16
+ }
@@ -0,0 +1 @@
1
+ """Sync native coding-agent sessions to private Hugging Face Storage Buckets."""
@@ -0,0 +1,3 @@
1
+ from hftracer.cli import app
2
+
3
+ app()
@@ -0,0 +1,160 @@
1
+ """Discover local agent directories and delegate transfers to Hugging Face."""
2
+
3
+ import os
4
+ from contextlib import redirect_stdout
5
+ from typing import Annotated, NoReturn
6
+
7
+ import typer
8
+ from huggingface_hub import HfApi
9
+
10
+ from hftracer.config import (
11
+ Config,
12
+ config_path,
13
+ discover_sources,
14
+ load_config,
15
+ new_device_id,
16
+ save_config,
17
+ validate_bucket_id,
18
+ )
19
+
20
+ app = typer.Typer(
21
+ help="Sync Codex and Claude Code sessions to a private Hugging Face bucket.",
22
+ no_args_is_help=True,
23
+ add_completion=False,
24
+ pretty_exceptions_enable=False,
25
+ )
26
+
27
+
28
+ def fail(message: str) -> NoReturn:
29
+ typer.secho(f"Error: {message}", fg=typer.colors.RED, err=True)
30
+ raise typer.Exit(1)
31
+
32
+
33
+ def read_config() -> Config | None:
34
+ try:
35
+ return load_config()
36
+ except (OSError, ValueError) as exc:
37
+ fail(f"Cannot read configuration at {config_path()}: {exc}")
38
+
39
+
40
+ def require_private(api: HfApi, bucket_id: str) -> None:
41
+ if api.bucket_info(bucket_id).private is not True:
42
+ raise ValueError(f"Bucket {bucket_id} is public; select a private bucket.")
43
+
44
+
45
+ def format_size(size: int) -> str:
46
+ amount = float(size)
47
+ for unit in ("B", "KiB", "MiB", "GiB"):
48
+ if amount < 1024:
49
+ return f"{amount:.3g} {unit}"
50
+ amount /= 1024
51
+ return f"{amount:.3g} TiB"
52
+
53
+
54
+ @app.command()
55
+ def init(
56
+ bucket: Annotated[
57
+ str | None,
58
+ typer.Option(
59
+ "--bucket", help="Create or select namespace/name without prompting."
60
+ ),
61
+ ] = None,
62
+ ) -> None:
63
+ """Configure a private bucket and a persistent namespace for this device."""
64
+ previous = read_config()
65
+ api = HfApi()
66
+ try:
67
+ username = api.whoami()["name"]
68
+ except Exception as exc:
69
+ fail(
70
+ "Cannot authenticate with Hugging Face. "
71
+ f"Run `hf auth login` or set HF_TOKEN. {exc}"
72
+ )
73
+
74
+ if bucket is None:
75
+ default = previous.bucket_id if previous else f"{username}/traces"
76
+ bucket = typer.prompt("Bucket (namespace/name)", default=default)
77
+ try:
78
+ validate_bucket_id(bucket)
79
+ api.create_bucket(bucket, private=True, exist_ok=True)
80
+ require_private(api, bucket)
81
+ except Exception as exc:
82
+ fail(f"Cannot configure bucket: {exc}")
83
+
84
+ config = Config(bucket, previous.device_id if previous else new_device_id())
85
+ try:
86
+ sources = discover_sources()
87
+ save_config(config)
88
+ except OSError as exc:
89
+ fail(f"Cannot finish setup: {exc}")
90
+ typer.secho(f"Configured {config.bucket_id}", fg=typer.colors.GREEN)
91
+ if sources:
92
+ typer.secho(
93
+ f"Sources: {', '.join(remote for _, remote in sources)}",
94
+ fg=typer.colors.CYAN,
95
+ )
96
+ else:
97
+ typer.secho("No agent directories found yet.", fg=typer.colors.YELLOW)
98
+
99
+
100
+ @app.command()
101
+ def sync(
102
+ dry_run: Annotated[
103
+ bool,
104
+ typer.Option(
105
+ "--dry-run", "--preview", help="Preview planned changes without uploading."
106
+ ),
107
+ ] = False,
108
+ ) -> None:
109
+ """Upload new and modified files; never delete remote files."""
110
+ config = read_config()
111
+ if config is None:
112
+ fail("No configuration found. Run `hftracer init` first.")
113
+ api = HfApi()
114
+ try:
115
+ require_private(api, config.bucket_id)
116
+ except Exception as exc:
117
+ fail(f"Cannot verify private bucket: {exc}")
118
+ try:
119
+ sources = discover_sources()
120
+ except OSError as exc:
121
+ fail(f"Cannot discover agent directories: {exc}")
122
+ if not sources:
123
+ typer.secho(
124
+ "No agent directories found. Nothing to sync.", fg=typer.colors.YELLOW
125
+ )
126
+ return
127
+
128
+ mode = "Preview" if dry_run else "Sync"
129
+ upload_label = "to upload" if dry_run else "uploaded"
130
+ typer.secho(f"{mode}: {config.bucket_id}", fg=typer.colors.CYAN)
131
+ failed = False
132
+ for source, remote in sources:
133
+ destination = (
134
+ f"hf://buckets/{config.bucket_id}/devices/{config.device_id}/{remote}/"
135
+ )
136
+ try:
137
+ with open(os.devnull, "w") as sink, redirect_stdout(sink):
138
+ plan = api.sync_bucket(
139
+ str(source),
140
+ destination,
141
+ delete=False,
142
+ dry_run=dry_run,
143
+ quiet=dry_run,
144
+ )
145
+ summary = plan.summary()
146
+ size = format_size(int(summary["total_size"]))
147
+ typer.echo(
148
+ f"{remote}: {summary['uploads']} {upload_label}, "
149
+ f"{summary['skips']} unchanged ({size})"
150
+ )
151
+ except Exception as exc:
152
+ typer.secho(f"Error for {remote}: {exc}", fg=typer.colors.RED, err=True)
153
+ failed = True
154
+ if failed:
155
+ command = "hftracer sync --dry-run" if dry_run else "hftracer sync"
156
+ fail(f"Some sources failed. Run `{command}` again to retry.")
157
+ if dry_run:
158
+ typer.secho("No files uploaded.", fg=typer.colors.YELLOW)
159
+ else:
160
+ typer.secho("Sync complete.", fg=typer.colors.GREEN)
@@ -0,0 +1,87 @@
1
+ """Small, local configuration and fixed agent-directory discovery."""
2
+
3
+ import json
4
+ import os
5
+ import re
6
+ import socket
7
+ import tempfile
8
+ from dataclasses import asdict, dataclass
9
+ from pathlib import Path
10
+ from uuid import uuid4
11
+
12
+ SOURCES = (
13
+ (".codex/sessions", "codex/sessions"),
14
+ (".codex/archived_sessions", "codex/archived_sessions"),
15
+ (".claude/projects", "claude/projects"),
16
+ )
17
+
18
+
19
+ def validate_bucket_id(bucket_id: str) -> None:
20
+ """Require a bucket ID, rather than a URL or a bucket subdirectory."""
21
+ if not re.fullmatch(
22
+ r"[A-Za-z0-9_][A-Za-z0-9_.-]*/[A-Za-z0-9_][A-Za-z0-9_.-]*", bucket_id
23
+ ):
24
+ raise ValueError("Bucket must be in namespace/name format.")
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class Config:
29
+ bucket_id: str
30
+ device_id: str
31
+
32
+ def __post_init__(self) -> None:
33
+ validate_bucket_id(self.bucket_id)
34
+ if not re.fullmatch(r"[a-z0-9][a-z0-9_-]*", self.device_id):
35
+ raise ValueError("Invalid device namespace in configuration.")
36
+
37
+
38
+ def config_path() -> Path:
39
+ base = os.environ.get("XDG_CONFIG_HOME")
40
+ return (
41
+ (Path(base) if base else Path.home() / ".config") / "hftracer" / "config.json"
42
+ )
43
+
44
+
45
+ def load_config() -> Config | None:
46
+ try:
47
+ content = config_path().read_text(encoding="utf-8")
48
+ except FileNotFoundError:
49
+ return None
50
+ data = json.loads(content)
51
+ if not isinstance(data, dict):
52
+ raise ValueError("Configuration must be a JSON object.")
53
+ bucket_id, device_id = data.get("bucket_id"), data.get("device_id")
54
+ if not isinstance(bucket_id, str) or not isinstance(device_id, str):
55
+ raise ValueError("Configuration requires bucket_id and device_id strings.")
56
+ return Config(bucket_id=bucket_id, device_id=device_id)
57
+
58
+
59
+ def save_config(config: Config) -> None:
60
+ path = config_path()
61
+ path.parent.mkdir(parents=True, exist_ok=True)
62
+ temporary_path: Path | None = None
63
+ try:
64
+ with tempfile.NamedTemporaryFile(
65
+ mode="w", encoding="utf-8", dir=path.parent, delete=False
66
+ ) as stream:
67
+ temporary_path = Path(stream.name)
68
+ json.dump(asdict(config), stream, indent=2)
69
+ stream.write("\n")
70
+ stream.flush()
71
+ os.fsync(stream.fileno())
72
+ os.replace(temporary_path, path)
73
+ finally:
74
+ if temporary_path is not None:
75
+ temporary_path.unlink(missing_ok=True)
76
+
77
+
78
+ def new_device_id() -> str:
79
+ hostname = re.sub(r"[^a-z0-9-]+", "-", socket.gethostname().lower()).strip("-")
80
+ return f"{hostname or 'device'}-{uuid4().hex}"
81
+
82
+
83
+ def discover_sources() -> list[tuple[Path, str]]:
84
+ home = Path.home()
85
+ return [
86
+ (home / local, remote) for local, remote in SOURCES if (home / local).is_dir()
87
+ ]
@@ -0,0 +1,27 @@
1
+ from pathlib import Path
2
+ from types import SimpleNamespace
3
+ from unittest.mock import MagicMock, create_autospec
4
+
5
+ import pytest
6
+ from huggingface_hub import HfApi, SyncPlan
7
+
8
+ from hftracer import cli
9
+
10
+
11
+ @pytest.fixture(autouse=True)
12
+ def isolated_home(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
13
+ home = tmp_path / "home"
14
+ home.mkdir()
15
+ monkeypatch.setenv("HOME", str(home))
16
+ monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path / "config"))
17
+ return home
18
+
19
+
20
+ @pytest.fixture
21
+ def api(monkeypatch: pytest.MonkeyPatch) -> MagicMock:
22
+ mock = create_autospec(HfApi, instance=True)
23
+ mock.whoami.return_value = {"name": "alice"}
24
+ mock.bucket_info.return_value = SimpleNamespace(private=True)
25
+ mock.sync_bucket.return_value = SyncPlan(source="", dest="", timestamp="")
26
+ monkeypatch.setattr(cli, "HfApi", lambda: mock)
27
+ return mock
@@ -0,0 +1,40 @@
1
+ # Manual private-bucket integration check
2
+
3
+ Use synthetic files and a dedicated private bucket. This procedure is not part
4
+ of the automated tests and requires existing HF authentication with bucket write
5
+ access. Run it only when you intend to create that bucket and upload test data.
6
+
7
+ 1. Create two temporary home directories representing devices A and B, with
8
+ separate `XDG_CONFIG_HOME` directories. Set `HF_TOKEN` through your normal
9
+ credential mechanism before changing `HOME`, so the HF client does not lose
10
+ access to its usual credential cache. Do not copy your real session directories.
11
+ 2. In device A's home, create files beneath `.codex/sessions/2026/10/`,
12
+ `.codex/archived_sessions/`, and `.claude/projects/example/nested/`. Include a
13
+ `.jsonl` file, a hidden file, and a binary file with known contents. Keep a copy
14
+ of the synthetic fixture tree outside these source directories for comparison.
15
+ 3. With device A's `HOME` and `XDG_CONFIG_HOME`, run
16
+ `uv run hftracer init --bucket YOUR_USER/hftracer-integration`, then
17
+ `uv run hftracer sync --dry-run`. Verify the plan lists the fixture uploads
18
+ while the bucket stays empty. The `--preview` alias should produce the same
19
+ plan. Run `uv run hftracer sync` to transfer the files.
20
+ Verify the bucket is private and files appear under
21
+ `devices/<device-A>/` with the expected agent and directory prefixes. There
22
+ should be no hftracer metadata or manifest objects.
23
+ 4. Run sync again without changing the fixture files. HF's sync summary should
24
+ show no uploads and should report skipped files.
25
+ 5. Modify an existing file (changing its size and modification time), add another,
26
+ and delete a third locally. Run sync. Only the modified and new files should be
27
+ uploaded; the locally deleted file must remain remotely.
28
+ 6. Download `hf://buckets/YOUR_USER/hftracer-integration/devices/<device-A>/` into
29
+ a separate temporary directory using `hf buckets sync`. Compare downloaded
30
+ file bytes against the original fixture copy plus the modifications. Verify
31
+ hidden files, binary contents, nested paths, and the retained deleted file.
32
+ 7. Create a file at the same relative session path in device B's temporary home,
33
+ using different contents. Initialize device B against the same bucket and sync.
34
+ Verify its device ID differs from A's and both versions remain at their
35
+ respective prefixes. Re-run A's init and confirm its namespace stays unchanged.
36
+ 8. Optionally change the test bucket to public, then run sync and verify it fails
37
+ before any upload. Restore it to private through HF before further testing.
38
+
39
+ Remove the temporary local directories when finished. Remote cleanup is manual;
40
+ hftracer never deletes the bucket or its files.