scrollback 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.
Files changed (98) hide show
  1. scrollback-0.1.0/.github/workflows/ci.yml +71 -0
  2. scrollback-0.1.0/.github/workflows/publish.yml +98 -0
  3. scrollback-0.1.0/.gitignore +34 -0
  4. scrollback-0.1.0/CHANGELOG.md +86 -0
  5. scrollback-0.1.0/CONTRIBUTING.md +70 -0
  6. scrollback-0.1.0/LICENSE +21 -0
  7. scrollback-0.1.0/PKG-INFO +391 -0
  8. scrollback-0.1.0/README.md +332 -0
  9. scrollback-0.1.0/ROADMAP.md +71 -0
  10. scrollback-0.1.0/assets/icon-512.png +0 -0
  11. scrollback-0.1.0/assets/icon.icns +0 -0
  12. scrollback-0.1.0/assets/icon.svg +41 -0
  13. scrollback-0.1.0/assets/screenshots/cli.svg +91 -0
  14. scrollback-0.1.0/assets/screenshots/web.png +0 -0
  15. scrollback-0.1.0/pyproject.toml +100 -0
  16. scrollback-0.1.0/scripts/demo_data.py +188 -0
  17. scrollback-0.1.0/scripts/screenshots.py +147 -0
  18. scrollback-0.1.0/src/scrollback/__init__.py +8 -0
  19. scrollback-0.1.0/src/scrollback/assets/icon-256.png +0 -0
  20. scrollback-0.1.0/src/scrollback/assets/icon.icns +0 -0
  21. scrollback-0.1.0/src/scrollback/cli.py +1139 -0
  22. scrollback-0.1.0/src/scrollback/clipboard.py +34 -0
  23. scrollback-0.1.0/src/scrollback/export.py +293 -0
  24. scrollback-0.1.0/src/scrollback/fts.py +307 -0
  25. scrollback-0.1.0/src/scrollback/highlight.py +128 -0
  26. scrollback-0.1.0/src/scrollback/katexbundle.py +81 -0
  27. scrollback-0.1.0/src/scrollback/launcher_install.py +209 -0
  28. scrollback-0.1.0/src/scrollback/launchers/scrollback.bat +19 -0
  29. scrollback-0.1.0/src/scrollback/launchers/scrollback.command +19 -0
  30. scrollback-0.1.0/src/scrollback/launchers/scrollback.desktop +10 -0
  31. scrollback-0.1.0/src/scrollback/launchers/scrollback.sh +12 -0
  32. scrollback-0.1.0/src/scrollback/mathspan.py +180 -0
  33. scrollback-0.1.0/src/scrollback/minimd.py +205 -0
  34. scrollback-0.1.0/src/scrollback/models.py +135 -0
  35. scrollback-0.1.0/src/scrollback/serialize.py +83 -0
  36. scrollback-0.1.0/src/scrollback/serverconfig.py +66 -0
  37. scrollback-0.1.0/src/scrollback/sources/__init__.py +6 -0
  38. scrollback-0.1.0/src/scrollback/sources/aider.py +244 -0
  39. scrollback-0.1.0/src/scrollback/sources/base.py +117 -0
  40. scrollback-0.1.0/src/scrollback/sources/claudecode.py +631 -0
  41. scrollback-0.1.0/src/scrollback/sources/codex.py +281 -0
  42. scrollback-0.1.0/src/scrollback/sources/opencode.py +357 -0
  43. scrollback-0.1.0/src/scrollback/sources/registry.py +39 -0
  44. scrollback-0.1.0/src/scrollback/store.py +384 -0
  45. scrollback-0.1.0/src/scrollback/termrender.py +170 -0
  46. scrollback-0.1.0/src/scrollback/web/__init__.py +1 -0
  47. scrollback-0.1.0/src/scrollback/web/app.py +359 -0
  48. scrollback-0.1.0/src/scrollback/web/static/app.js +1245 -0
  49. scrollback-0.1.0/src/scrollback/web/static/apple-touch-icon.png +0 -0
  50. scrollback-0.1.0/src/scrollback/web/static/favicon.png +0 -0
  51. scrollback-0.1.0/src/scrollback/web/static/favicon.svg +41 -0
  52. scrollback-0.1.0/src/scrollback/web/static/index.html +75 -0
  53. scrollback-0.1.0/src/scrollback/web/static/style.css +628 -0
  54. scrollback-0.1.0/src/scrollback/web/static/vendor/highlight.min.js +1213 -0
  55. scrollback-0.1.0/src/scrollback/web/static/vendor/hljs-dark.min.css +10 -0
  56. scrollback-0.1.0/src/scrollback/web/static/vendor/hljs-light.min.css +10 -0
  57. scrollback-0.1.0/src/scrollback/web/static/vendor/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  58. scrollback-0.1.0/src/scrollback/web/static/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  59. scrollback-0.1.0/src/scrollback/web/static/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  60. scrollback-0.1.0/src/scrollback/web/static/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  61. scrollback-0.1.0/src/scrollback/web/static/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  62. scrollback-0.1.0/src/scrollback/web/static/vendor/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
  63. scrollback-0.1.0/src/scrollback/web/static/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  64. scrollback-0.1.0/src/scrollback/web/static/vendor/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
  65. scrollback-0.1.0/src/scrollback/web/static/vendor/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
  66. scrollback-0.1.0/src/scrollback/web/static/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  67. scrollback-0.1.0/src/scrollback/web/static/vendor/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
  68. scrollback-0.1.0/src/scrollback/web/static/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  69. scrollback-0.1.0/src/scrollback/web/static/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  70. scrollback-0.1.0/src/scrollback/web/static/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  71. scrollback-0.1.0/src/scrollback/web/static/vendor/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
  72. scrollback-0.1.0/src/scrollback/web/static/vendor/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  73. scrollback-0.1.0/src/scrollback/web/static/vendor/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  74. scrollback-0.1.0/src/scrollback/web/static/vendor/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  75. scrollback-0.1.0/src/scrollback/web/static/vendor/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  76. scrollback-0.1.0/src/scrollback/web/static/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  77. scrollback-0.1.0/src/scrollback/web/static/vendor/katex/katex.min.css +1 -0
  78. scrollback-0.1.0/src/scrollback/web/static/vendor/katex/katex.min.js +1 -0
  79. scrollback-0.1.0/src/scrollback/web/static/vendor/marked.min.js +6 -0
  80. scrollback-0.1.0/src/scrollback/web/static/vendor/purify.min.js +3 -0
  81. scrollback-0.1.0/src/scrollback/webopen.py +96 -0
  82. scrollback-0.1.0/tests/test_aider.py +87 -0
  83. scrollback-0.1.0/tests/test_claude_paging.py +84 -0
  84. scrollback-0.1.0/tests/test_claude_subagents.py +120 -0
  85. scrollback-0.1.0/tests/test_cli_helpers.py +66 -0
  86. scrollback-0.1.0/tests/test_codex.py +70 -0
  87. scrollback-0.1.0/tests/test_export.py +147 -0
  88. scrollback-0.1.0/tests/test_fts.py +151 -0
  89. scrollback-0.1.0/tests/test_highlight.py +41 -0
  90. scrollback-0.1.0/tests/test_launcher_install.py +101 -0
  91. scrollback-0.1.0/tests/test_minimd.py +140 -0
  92. scrollback-0.1.0/tests/test_models.py +63 -0
  93. scrollback-0.1.0/tests/test_serverconfig.py +66 -0
  94. scrollback-0.1.0/tests/test_sources_live.py +96 -0
  95. scrollback-0.1.0/tests/test_stats_resume.py +89 -0
  96. scrollback-0.1.0/tests/test_store_filters.py +120 -0
  97. scrollback-0.1.0/tests/test_web_api.py +240 -0
  98. scrollback-0.1.0/tests/test_webopen.py +45 -0
@@ -0,0 +1,71 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ test:
13
+ name: test (py${{ matrix.python-version }})
14
+ runs-on: ubuntu-latest
15
+ strategy:
16
+ fail-fast: false
17
+ matrix:
18
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+
22
+ - name: Set up Python ${{ matrix.python-version }}
23
+ uses: actions/setup-python@v5
24
+ with:
25
+ python-version: ${{ matrix.python-version }}
26
+
27
+ - name: Install (dev extra)
28
+ run: |
29
+ python -m pip install --upgrade pip
30
+ # The dev extra is self-sufficient for the full suite (fastapi +
31
+ # uvicorn + httpx for the web-API tests). pywebview is intentionally
32
+ # excluded -- it needs a GUI backend CI runners lack, and no test
33
+ # imports it.
34
+ python -m pip install -e ".[dev]"
35
+
36
+ - name: Lint (ruff)
37
+ run: ruff check src tests
38
+
39
+ - name: Test (pytest)
40
+ run: pytest -q
41
+
42
+ build:
43
+ name: build wheel + sdist
44
+ runs-on: ubuntu-latest
45
+ steps:
46
+ - uses: actions/checkout@v4
47
+ - uses: actions/setup-python@v5
48
+ with:
49
+ python-version: "3.12"
50
+ - name: Build
51
+ run: |
52
+ python -m pip install --upgrade pip build
53
+ python -m build
54
+ - name: Check artifacts contain data files
55
+ run: |
56
+ python - <<'PY'
57
+ import glob, zipfile
58
+ whl = sorted(glob.glob("dist/*.whl"))[-1]
59
+ names = zipfile.ZipFile(whl).namelist()
60
+ need = ["web/static/app.js", "web/static/vendor/marked.min.js",
61
+ "web/static/vendor/katex/katex.min.js",
62
+ "web/static/vendor/katex/fonts/KaTeX_Main-Regular.woff2",
63
+ "launchers/scrollback.command", "assets/icon.icns"]
64
+ missing = [n for n in need if not any(n in x for x in names)]
65
+ assert not missing, f"wheel missing data files: {missing}"
66
+ print("wheel OK:", whl.split('/')[-1])
67
+ PY
68
+ - uses: actions/upload-artifact@v4
69
+ with:
70
+ name: dist
71
+ path: dist/*
@@ -0,0 +1,98 @@
1
+ name: Publish to PyPI
2
+
3
+ # Tag-gated release. Push a version tag (e.g. `v0.1.0`) to build, validate, and
4
+ # publish the sdist + wheel to PyPI via Trusted Publishing (OIDC) -- no API
5
+ # token or password is stored in the repo. Configure the trusted publisher once
6
+ # at https://pypi.org/manage/project/scrollback/settings/publishing/ (or via
7
+ # "pending publisher" before the first upload), matching:
8
+ # owner = a-attia, repo = scrollback, workflow = publish.yml, env = pypi.
9
+
10
+ on:
11
+ push:
12
+ tags: ["v*"]
13
+
14
+ permissions:
15
+ contents: read
16
+
17
+ jobs:
18
+ # Re-run the full test matrix + lint before releasing, so a tag can never
19
+ # publish a red build.
20
+ test:
21
+ name: test (py${{ matrix.python-version }})
22
+ runs-on: ubuntu-latest
23
+ strategy:
24
+ fail-fast: false
25
+ matrix:
26
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
27
+ steps:
28
+ - uses: actions/checkout@v4
29
+ - uses: actions/setup-python@v5
30
+ with:
31
+ python-version: ${{ matrix.python-version }}
32
+ - run: |
33
+ python -m pip install --upgrade pip
34
+ python -m pip install -e ".[dev]"
35
+ - run: ruff check src tests
36
+ - run: pytest -q
37
+
38
+ build:
39
+ name: build + validate
40
+ needs: test
41
+ runs-on: ubuntu-latest
42
+ steps:
43
+ - uses: actions/checkout@v4
44
+ - uses: actions/setup-python@v5
45
+ with:
46
+ python-version: "3.12"
47
+ - name: Build sdist + wheel
48
+ run: |
49
+ python -m pip install --upgrade pip build twine
50
+ python -m build
51
+ - name: Verify the tag matches the package version
52
+ run: |
53
+ python - <<'PY'
54
+ import os, tomllib, pathlib, re
55
+ # version is dynamic from src/scrollback/__init__.py
56
+ init = pathlib.Path("src/scrollback/__init__.py").read_text()
57
+ ver = re.search(r'__version__\s*=\s*["\']([^"\']+)["\']', init).group(1)
58
+ tag = os.environ["GITHUB_REF_NAME"]
59
+ assert tag == f"v{ver}", f"tag {tag!r} does not match package version v{ver}"
60
+ print(f"tag {tag} matches version {ver}")
61
+ PY
62
+ - name: Validate metadata (twine check)
63
+ run: python -m twine check dist/*
64
+ - name: Verify wheel ships its data files
65
+ run: |
66
+ python - <<'PY'
67
+ import glob, zipfile
68
+ whl = sorted(glob.glob("dist/*.whl"))[-1]
69
+ names = zipfile.ZipFile(whl).namelist()
70
+ need = ["web/static/app.js", "web/static/vendor/marked.min.js",
71
+ "web/static/vendor/katex/katex.min.js",
72
+ "web/static/vendor/katex/fonts/KaTeX_Main-Regular.woff2",
73
+ "launchers/scrollback.command", "assets/icon.icns"]
74
+ missing = [n for n in need if not any(n in x for x in names)]
75
+ assert not missing, f"wheel missing data files: {missing}"
76
+ print("wheel OK:", whl.split('/')[-1])
77
+ PY
78
+ - uses: actions/upload-artifact@v4
79
+ with:
80
+ name: dist
81
+ path: dist/*
82
+
83
+ publish:
84
+ name: publish to PyPI
85
+ needs: build
86
+ runs-on: ubuntu-latest
87
+ environment:
88
+ name: pypi
89
+ url: https://pypi.org/p/scrollback
90
+ permissions:
91
+ id-token: write # OIDC token for Trusted Publishing
92
+ steps:
93
+ - uses: actions/download-artifact@v4
94
+ with:
95
+ name: dist
96
+ path: dist
97
+ - name: Publish
98
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,34 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .eggs/
6
+ build/
7
+ dist/
8
+ *.egg
9
+
10
+ # Test / coverage / lint
11
+ .pytest_cache/
12
+ .ruff_cache/
13
+ .coverage
14
+ htmlcov/
15
+ .tox/
16
+ .nox/
17
+
18
+ # Virtual envs
19
+ .venv/
20
+ venv/
21
+ env/
22
+
23
+ # Editors / OS
24
+ .DS_Store
25
+ .idea/
26
+ .vscode/
27
+
28
+ # scrollback: never commit anyone's exported sessions
29
+ *.session.md
30
+ *.session.html
31
+ *.session.json
32
+
33
+ # Generated icon intermediates (the SVG source + .icns are kept)
34
+ assets/icon.iconset/
@@ -0,0 +1,86 @@
1
+ # Changelog
2
+
3
+ All notable changes to scrollback are documented here. The format is based
4
+ on [Keep a Changelog](https://keepachangelog.com/), and the project aims to
5
+ follow [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.1.0] - 2026-06-30
10
+
11
+ The first release. scrollback reads AI coding-agent session history
12
+ (opencode + Claude Code) read-only and lets you browse, search, copy, and
13
+ export it from a CLI and a local web app.
14
+
15
+ ### Added
16
+
17
+ - **CLI** (`scrollback`): `sources`, `list`, `show`, `search`, `export`
18
+ (markdown / json / html / text), `copy`, `stats`, `resume`, `web`,
19
+ `index`, `doctor`, and `install-launcher`.
20
+ - **Source adapters** (pluggable, read-only): opencode (SQLite), Claude Code
21
+ (JSONL, with subagent sidechains folded under their parent), Codex
22
+ (`rollout-*.jsonl`), and Aider (`.aider.chat.history.md`). More are queued
23
+ in `ROADMAP.md`.
24
+ - `stats` aggregates session/message/token/cost totals plus top projects;
25
+ `resume` prints the native command to continue a session in its own agent.
26
+ - Listing filters: `--source`, `--dir`, `--query`, `--since` / `--until`,
27
+ pagination (`--offset` / `--page`), usage columns (`--usage`), and
28
+ subagent folding (on by default; `--no-fold`). Optional coloured output
29
+ via `rich`.
30
+ - **Web app** (`scrollback web`): local, read-only, served on
31
+ `127.0.0.1`. Session list with source filters, date filters, and a
32
+ `titles | contents` search scope; lazy, windowed transcript loading so
33
+ very large sessions open instantly; in-transcript find; per-message and
34
+ per-session copy; export and print; light/dark theme; keyboard
35
+ navigation; a frozen session header with a scrolling message body.
36
+ - **Markdown rendering**: assistant/user text renders as Markdown with code
37
+ highlighting -- in the browser (vendored marked + highlight.js) and in
38
+ the static HTML export (a dependency-free Python renderer + highlighter).
39
+ - **Math / equation rendering**: delimited LaTeX (`$...$`, `$$...$$`,
40
+ `\(...\)`, `\[...\]`) is detected and shielded from the Markdown pass so
41
+ `\`, `_`, `*`, `^` survive intact in both renderers. A render mode --
42
+ `raw` (verbatim source), `latex` (verbatim, never typeset, paste-ready),
43
+ or `rendered` (typeset) -- is a toggle in the web transcript header
44
+ (persisted like the theme) and an `--math {raw,latex,rendered}` flag on
45
+ `scrollback export` / `copy`. Typesetting uses vendored KaTeX (no CDN);
46
+ the self-contained HTML export embeds KaTeX with its fonts inlined so
47
+ saved/printed files typeset offline. The single-`$` form is recognised
48
+ conservatively so currency (`$5 to $10`) and code are left alone.
49
+ - **Optional full-text search index** (`scrollback index`): SQLite FTS5,
50
+ incremental, stored in a disposable cache DB; the source data is never
51
+ modified, and search falls back to a lexical scan without it.
52
+ - **Launching without the terminal**: `scrollback-web` / `scrollback-app`
53
+ console entry points; `install-launcher` drops a double-clickable
54
+ launcher (macOS `.command` / `.app`, Windows `.bat`, Linux `.desktop`);
55
+ a native desktop window via pywebview that frees the port on close.
56
+ - App icon (macOS `.app` + web favicon) and macOS app identity (menu name,
57
+ About panel with version and a clickable repo link).
58
+ - Configurable host/port via flags or `SCROLLBACK_HOST` / `SCROLLBACK_PORT`,
59
+ with automatic free-port selection.
60
+
61
+ ### Security
62
+
63
+ - Sanitize rendered Markdown (DOMPurify) to prevent transcript content from
64
+ injecting scripts into the web UI.
65
+ - Host-header allowlist guarding against DNS-rebinding (loopback-only by
66
+ default); loud warning on non-loopback binds.
67
+ - Path-traversal containment for Claude subagent id resolution.
68
+
69
+ ### Performance
70
+
71
+ - Cache Claude Code metadata scans by file mtime (repeated listings go from
72
+ ~1.2s to ~0.01s).
73
+ - Byte-offset paging index for Claude transcripts (deep pages on a
74
+ 31k-message session: ~1s to ~2ms).
75
+ - Lazy per-session metadata resolution on the indexed search path.
76
+
77
+ ### Fixed
78
+
79
+ - Timezone-naive timestamps no longer crash session sorting.
80
+ - Subagent folding no longer drops self-referential or cross-source records.
81
+ - Reliable downloads and printing from the native desktop window.
82
+ - Negative pagination arguments are rejected; clearer errors for unknown
83
+ sources, failed exports, and unavailable data sources.
84
+
85
+ [Unreleased]: https://github.com/a-attia/scrollback/compare/v0.1.0...HEAD
86
+ [0.1.0]: https://github.com/a-attia/scrollback/releases/tag/v0.1.0
@@ -0,0 +1,70 @@
1
+ # Contributing to scrollback
2
+
3
+ Thanks for your interest. scrollback is a small, local-first, read-only
4
+ tool, and contributions that keep it that way are very welcome.
5
+
6
+ ## Development setup
7
+
8
+ ```bash
9
+ git clone https://github.com/a-attia/scrollback
10
+ cd scrollback
11
+ python -m pip install -e ".[web,dev]" # editable install + web + dev tools
12
+ ```
13
+
14
+ The bare package has **no runtime dependencies** (stdlib only); the web app
15
+ and developer tooling come from extras. Requires Python 3.10+.
16
+
17
+ ## Running the checks
18
+
19
+ ```bash
20
+ pytest -q # the test suite
21
+ ruff check src tests # lint
22
+ ```
23
+
24
+ Both must pass before a change is merged. The test suite is fast (~2s) and
25
+ runs against synthetic fixtures plus, where present, your real local data
26
+ (those tests skip gracefully when no data is available).
27
+
28
+ ## Project conventions
29
+
30
+ - **Read-only, always.** Nothing in scrollback may write to, lock for
31
+ writing, or upload a user's agent data. The opencode SQLite DB is opened
32
+ with `mode=ro`; JSONL files are read-only. Tests assert this invariant.
33
+ - **Lightest tool that does the job.** Prefer the stdlib. New runtime
34
+ dependencies for the core CLI are a hard sell; put optional features
35
+ behind extras (see `[project.optional-dependencies]`).
36
+ - **Platform-agnostic.** Keep OS-specific code guarded by `sys.platform`
37
+ and best-effort (it must degrade, not crash, elsewhere). Window/icon
38
+ handling lives in Python, not baked into per-OS launcher scripts.
39
+ - **Tests for fixes.** Bug fixes should come with a regression test;
40
+ numeric/parsing assertions should be backed by a known-correct value.
41
+
42
+ ## Adding a new agent source
43
+
44
+ Implement the `Source` interface in `src/scrollback/sources/base.py` and
45
+ register it in `src/scrollback/sources/registry.py`. Everything else (CLI,
46
+ search, export, web, index) works against the common model automatically.
47
+ See `opencode.py` (SQLite) and `claudecode.py` (JSONL) as references.
48
+
49
+ ## Regenerating the README screenshots
50
+
51
+ The images in the README are generated from synthetic, sanitized demo data
52
+ (`scripts/demo_data.py`) — never from real sessions — so they are safe to
53
+ publish. To regenerate them after a UI change:
54
+
55
+ ```bash
56
+ pip install -e ".[screenshots]"
57
+ playwright install chromium # one-time headless-browser download
58
+ python scripts/screenshots.py # writes assets/screenshots/{cli.svg,web.png}
59
+ ```
60
+
61
+ The CLI image is rendered with `rich` (no browser); the web image is
62
+ captured with headless Chromium via Playwright. Neither the `screenshots`
63
+ extra nor the browser is needed to run scrollback.
64
+
65
+ ## Submitting changes
66
+
67
+ 1. Fork and branch.
68
+ 2. Make the change with a focused scope and a test.
69
+ 3. Run `pytest -q` and `ruff check src tests`.
70
+ 4. Open a pull request describing the change and how you verified it.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ahmed Attia
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.