realitydiff 0.4.0__tar.gz → 0.4.1__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 (49) hide show
  1. realitydiff-0.4.1/.github/workflows/ci.yml +32 -0
  2. realitydiff-0.4.1/.github/workflows/pypi-publish.yml +99 -0
  3. realitydiff-0.4.1/PKG-INFO +148 -0
  4. {realitydiff-0.4.0 → realitydiff-0.4.1}/README.md +12 -12
  5. {realitydiff-0.4.0 → realitydiff-0.4.1}/pyproject.toml +18 -1
  6. {realitydiff-0.4.0 → realitydiff-0.4.1}/src/realitydiff/__init__.py +1 -1
  7. {realitydiff-0.4.0 → realitydiff-0.4.1}/src/realitydiff/cli.py +38 -3
  8. {realitydiff-0.4.0 → realitydiff-0.4.1}/src/realitydiff/config.py +67 -0
  9. realitydiff-0.4.1/src/realitydiff/setup.py +198 -0
  10. realitydiff-0.4.1/tests/test_pypi_publish_workflow.py +36 -0
  11. realitydiff-0.4.1/tests/test_setup.py +141 -0
  12. {realitydiff-0.4.0 → realitydiff-0.4.1}/uv.lock +1 -1
  13. realitydiff-0.4.0/.github/workflows/pypi-publish.yml +0 -53
  14. realitydiff-0.4.0/PKG-INFO +0 -115
  15. realitydiff-0.4.0/tests/test_pypi_publish_workflow.py +0 -19
  16. {realitydiff-0.4.0 → realitydiff-0.4.1}/.codegraph/.gitignore +0 -0
  17. {realitydiff-0.4.0 → realitydiff-0.4.1}/.gitignore +0 -0
  18. {realitydiff-0.4.0 → realitydiff-0.4.1}/LICENSE +0 -0
  19. {realitydiff-0.4.0 → realitydiff-0.4.1}/data/.gitkeep +0 -0
  20. {realitydiff-0.4.0 → realitydiff-0.4.1}/install.sh +0 -0
  21. {realitydiff-0.4.0 → realitydiff-0.4.1}/src/realitydiff/__main__.py +0 -0
  22. {realitydiff-0.4.0 → realitydiff-0.4.1}/src/realitydiff/api.py +0 -0
  23. {realitydiff-0.4.0 → realitydiff-0.4.1}/src/realitydiff/blame.py +0 -0
  24. {realitydiff-0.4.0 → realitydiff-0.4.1}/src/realitydiff/claim.py +0 -0
  25. {realitydiff-0.4.0 → realitydiff-0.4.1}/src/realitydiff/compress.py +0 -0
  26. {realitydiff-0.4.0 → realitydiff-0.4.1}/src/realitydiff/diffing.py +0 -0
  27. {realitydiff-0.4.0 → realitydiff-0.4.1}/src/realitydiff/hashing.py +0 -0
  28. {realitydiff-0.4.0 → realitydiff-0.4.1}/src/realitydiff/models.py +0 -0
  29. {realitydiff-0.4.0 → realitydiff-0.4.1}/src/realitydiff/reasoner.py +0 -0
  30. {realitydiff-0.4.0 → realitydiff-0.4.1}/src/realitydiff/schedule.py +0 -0
  31. {realitydiff-0.4.0 → realitydiff-0.4.1}/src/realitydiff/service.py +0 -0
  32. {realitydiff-0.4.0 → realitydiff-0.4.1}/src/realitydiff/static/app.js +0 -0
  33. {realitydiff-0.4.0 → realitydiff-0.4.1}/src/realitydiff/static/favicon.svg +0 -0
  34. {realitydiff-0.4.0 → realitydiff-0.4.1}/src/realitydiff/static/index.html +0 -0
  35. {realitydiff-0.4.0 → realitydiff-0.4.1}/src/realitydiff/static/styles.css +0 -0
  36. {realitydiff-0.4.0 → realitydiff-0.4.1}/src/realitydiff/store.py +0 -0
  37. {realitydiff-0.4.0 → realitydiff-0.4.1}/src/realitydiff/watcher.py +0 -0
  38. {realitydiff-0.4.0 → realitydiff-0.4.1}/src/realitydiff/xai.py +0 -0
  39. {realitydiff-0.4.0 → realitydiff-0.4.1}/tests/__init__.py +0 -0
  40. {realitydiff-0.4.0 → realitydiff-0.4.1}/tests/test_api.py +0 -0
  41. {realitydiff-0.4.0 → realitydiff-0.4.1}/tests/test_close_claim.py +0 -0
  42. {realitydiff-0.4.0 → realitydiff-0.4.1}/tests/test_compress.py +0 -0
  43. {realitydiff-0.4.0 → realitydiff-0.4.1}/tests/test_config.py +0 -0
  44. {realitydiff-0.4.0 → realitydiff-0.4.1}/tests/test_git_beliefs.py +0 -0
  45. {realitydiff-0.4.0 → realitydiff-0.4.1}/tests/test_reasoner.py +0 -0
  46. {realitydiff-0.4.0 → realitydiff-0.4.1}/tests/test_robustness.py +0 -0
  47. {realitydiff-0.4.0 → realitydiff-0.4.1}/tests/test_schedule.py +0 -0
  48. {realitydiff-0.4.0 → realitydiff-0.4.1}/tests/test_ui.py +0 -0
  49. {realitydiff-0.4.0 → realitydiff-0.4.1}/tests/test_xai_cache.py +0 -0
@@ -0,0 +1,32 @@
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: Tests (Python ${{ matrix.python-version }})
14
+ runs-on: ubuntu-latest
15
+ strategy:
16
+ fail-fast: false
17
+ matrix:
18
+ python-version: ["3.11", "3.13"]
19
+ steps:
20
+ - name: Check out repository
21
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
22
+
23
+ - name: Install uv
24
+ uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
25
+ with:
26
+ python-version: ${{ matrix.python-version }}
27
+
28
+ - name: Install bun (used by the UI tests)
29
+ uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
30
+
31
+ - name: Run tests
32
+ run: uv run --extra dev pytest -q
@@ -0,0 +1,99 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ jobs:
11
+ test:
12
+ name: Tests
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - name: Check out repository
16
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
17
+
18
+ - name: Install uv
19
+ uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
20
+ with:
21
+ python-version: "3.11"
22
+
23
+ - name: Install bun (used by the UI tests)
24
+ uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
25
+
26
+ - name: Run tests
27
+ run: uv run --extra dev pytest -q
28
+
29
+ build:
30
+ name: Build distribution
31
+ needs: test
32
+ runs-on: ubuntu-latest
33
+ steps:
34
+ - name: Check out repository
35
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
36
+
37
+ - name: Install uv
38
+ uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
39
+ with:
40
+ python-version: "3.11"
41
+
42
+ - name: Check release tag matches pyproject version
43
+ env:
44
+ TAG: ${{ github.event.release.tag_name }}
45
+ run: |
46
+ version=$(python3 -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
47
+ if [ "$TAG" != "v$version" ]; then
48
+ echo "Release tag $TAG does not match pyproject version v$version" >&2
49
+ exit 1
50
+ fi
51
+
52
+ - name: Build package
53
+ run: uv build
54
+
55
+ - name: Check package contents
56
+ run: |
57
+ for whl in dist/*.whl; do
58
+ bad=$(unzip -Z1 "$whl" | grep -v -E '^(realitydiff/|realitydiff-[^/]*\.dist-info/)' || true)
59
+ if [ -n "$bad" ]; then
60
+ echo "Unexpected files in wheel:" >&2
61
+ echo "$bad" >&2
62
+ exit 1
63
+ fi
64
+ done
65
+ secrets=$( { unzip -Z1 dist/*.whl; tar tzf dist/*.tar.gz; } | grep -E '(^|/)(\.env[^/]*|[^/]*\.(pem|key))$' || true)
66
+ if [ -n "$secrets" ]; then
67
+ echo "Secret-looking files in the package:" >&2
68
+ echo "$secrets" >&2
69
+ exit 1
70
+ fi
71
+
72
+ - name: Upload distribution
73
+ uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
74
+ with:
75
+ name: python-package-distributions
76
+ path: dist/
77
+ if-no-files-found: error
78
+
79
+ publish:
80
+ name: Publish to PyPI
81
+ needs: build
82
+ runs-on: ubuntu-latest
83
+ environment:
84
+ name: pypi
85
+ url: https://pypi.org/p/realitydiff
86
+ permissions:
87
+ id-token: write
88
+ attestations: write
89
+ contents: read
90
+ actions: read
91
+ steps:
92
+ - name: Download distribution
93
+ uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
94
+ with:
95
+ name: python-package-distributions
96
+ path: dist/
97
+
98
+ - name: Publish package distributions to PyPI
99
+ uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # v1.14.3
@@ -0,0 +1,148 @@
1
+ Metadata-Version: 2.5
2
+ Name: realitydiff
3
+ Version: 0.4.1
4
+ Summary: Git for beliefs — living claims that watch the internet and version confidence.
5
+ Project-URL: Homepage, https://github.com/kavin0x/RealityDiff
6
+ Project-URL: Source, https://github.com/kavin0x/RealityDiff
7
+ Project-URL: Issues, https://github.com/kavin0x/RealityDiff/issues
8
+ Author: Kavin Shah
9
+ License: MIT License
10
+
11
+ Copyright (c) 2026 Reality Diff contributors
12
+
13
+ Permission is hereby granted, free of charge, to any person obtaining a copy
14
+ of this software and associated documentation files (the "Software"), to deal
15
+ in the Software without restriction, including without limitation the rights
16
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
+ copies of the Software, and to permit persons to whom the Software is
18
+ furnished to do so, subject to the following conditions:
19
+
20
+ The above copyright notice and this permission notice shall be included in all
21
+ copies or substantial portions of the Software.
22
+
23
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
+ SOFTWARE.
30
+ License-File: LICENSE
31
+ Keywords: beliefs,claims,fact-checking,grok,xai
32
+ Classifier: Development Status :: 3 - Alpha
33
+ Classifier: Environment :: Console
34
+ Classifier: Intended Audience :: Developers
35
+ Classifier: Programming Language :: Python :: 3
36
+ Classifier: Programming Language :: Python :: 3.11
37
+ Classifier: Programming Language :: Python :: 3.12
38
+ Classifier: Programming Language :: Python :: 3.13
39
+ Requires-Python: >=3.11
40
+ Requires-Dist: fastapi>=0.115.0
41
+ Requires-Dist: httpx>=0.28.0
42
+ Requires-Dist: pydantic>=2.10.0
43
+ Requires-Dist: uvicorn[standard]>=0.34.0
44
+ Provides-Extra: dev
45
+ Requires-Dist: pytest-asyncio>=0.25.0; extra == 'dev'
46
+ Requires-Dist: pytest>=8.3.0; extra == 'dev'
47
+ Description-Content-Type: text/markdown
48
+
49
+ # Reality Diff
50
+
51
+ Git for beliefs — a version history of a claim: evidence, confidence, and why that number moved. For people tracking a story that is still unfolding (products, policy, markets) who want a trail, not a vibe.
52
+
53
+ ## Why I built this
54
+
55
+ News feeds overwrite your memory. Last week’s confidence on a rumor disappears under today’s take. I wanted git-style operations on a belief: commit when evidence actually moves, diff two weeks, blame a confidence jump, browse the claim as of a SHA, or revert a panic. So I built Reality Diff.
56
+
57
+ ## News feed vs this
58
+
59
+ | | Typical news / search | Reality Diff |
60
+ | --- | --- | --- |
61
+ | Memory | Overwritten | Versioned history per claim |
62
+ | Confidence | Vibes / headlines | Explicit score + reason when it moves |
63
+ | Compare | Manual screenshots | `diff` between commits |
64
+ | Undo | None | `revert` to an earlier belief state |
65
+
66
+ ## Features
67
+
68
+ **Claims**
69
+
70
+ - Open a claim (e.g. “Apple is going to replace Siri with an LLM”)
71
+ - Living object: evidence for/against, unknowns, predictions, confidence
72
+ - Commits only when **meaningful** new evidence appears (cosmetic rewrites dropped)
73
+
74
+ **Git-style ops**
75
+
76
+ - `commit` · `diff` · `blame` · `revert` · `watch` · `log`
77
+ - Browse the belief as of any SHA
78
+ - Background watch via OS timer (macOS LaunchAgent / Linux systemd / Windows task) or the UI interval picker
79
+
80
+ **Surfaces**
81
+
82
+ - Local web UI (`realitydiff serve` → <http://localhost:8000>)
83
+ - CLI for agents and scripts
84
+ - REST API for the same actions
85
+
86
+ ## Install
87
+
88
+ From PyPI (needs Python 3.11+). `uv` and `pipx` each give it its own isolated environment, so use one of them first:
89
+
90
+ ```bash
91
+ uv tool install realitydiff # or: pipx install realitydiff
92
+ realitydiff # first run walks you through setup
93
+ ```
94
+
95
+ Plain `pip install realitydiff` works inside a virtualenv. On macOS and many Linux setups, system-wide `pip` refuses with an "externally-managed-environment" error, which is why I lead with `uv`/`pipx`.
96
+
97
+ The first time you run `realitydiff` in a terminal (or any time you run `realitydiff setup`), it does what the old installer did: checks the install, offers to save your `XAI_API_KEY` to `~/.config/realitydiff/env` (permissions 600, typing is hidden), and tells you if the command isn't on your PATH. `realitydiff setup --yes` never prompts, and `--no-key` skips the key.
98
+
99
+ Prefer a script that also installs `uv` for you (macOS or Linux, no sudo)?
100
+
101
+ ```bash
102
+ curl -fsSL https://raw.githubusercontent.com/kavin0x/RealityDiff/main/install.sh | sh
103
+ ```
104
+
105
+ Read it first if you like: download `install.sh`, then run `sh install.sh --help`. Options go after `sh -s --`: `--yes`, `--ref <tag-or-commit>`, `--uninstall`.
106
+
107
+ Want the latest unreleased code instead of PyPI?
108
+
109
+ ```bash
110
+ uv tool install "realitydiff @ https://github.com/kavin0x/RealityDiff/archive/main.tar.gz"
111
+ ```
112
+
113
+ Then:
114
+
115
+ ```bash
116
+ realitydiff serve # binds to 127.0.0.1 only; open http://127.0.0.1:8000
117
+ ```
118
+
119
+ The installed CLI keeps claims in `~/.local/share/realitydiff/` (set `REALITYDIFF_DB` to change it). Inside a checkout it still uses `data/`.
120
+
121
+ ## Try it (from a checkout)
122
+
123
+ ```bash
124
+ export XAI_API_KEY=...
125
+ uv sync --extra dev
126
+ uv run realitydiff serve
127
+ # open http://localhost:8000
128
+ ```
129
+
130
+ ```bash
131
+ uv run realitydiff open "Apple is going to replace Siri with an LLM."
132
+ uv run realitydiff watch <claim_id>
133
+ uv run realitydiff log <claim_id>
134
+ uv run realitydiff diff <claim_id> <sha_a> <sha_b>
135
+ ```
136
+
137
+ ```bash
138
+ uv run realitydiff service install --interval 300
139
+ uv run pytest
140
+ ```
141
+
142
+ ## Stack
143
+
144
+ Python 3.11+ · uv · local web UI + REST · xAI Responses API (web + X search) · MIT
145
+
146
+ ## License
147
+
148
+ [MIT](LICENSE)
@@ -37,35 +37,35 @@ News feeds overwrite your memory. Last week’s confidence on a rumor disappears
37
37
 
38
38
  ## Install
39
39
 
40
- One line (macOS or Linux, no sudo):
40
+ From PyPI (needs Python 3.11+). `uv` and `pipx` each give it its own isolated environment, so use one of them first:
41
41
 
42
42
  ```bash
43
- curl -fsSL https://raw.githubusercontent.com/kavin0x/RealityDiff/main/install.sh | sh
43
+ uv tool install realitydiff # or: pipx install realitydiff
44
+ realitydiff # first run walks you through setup
44
45
  ```
45
46
 
46
- It installs [uv](https://docs.astral.sh/uv/) into `~/.local/bin` if you don't have it, installs `realitydiff` as an isolated uv tool, and can save your `XAI_API_KEY` to `~/.config/realitydiff/env` (permissions 600). It never uses sudo and only touches your shell rc if you say yes.
47
+ Plain `pip install realitydiff` works inside a virtualenv. On macOS and many Linux setups, system-wide `pip` refuses with an "externally-managed-environment" error, which is why I lead with `uv`/`pipx`.
48
+
49
+ The first time you run `realitydiff` in a terminal (or any time you run `realitydiff setup`), it does what the old installer did: checks the install, offers to save your `XAI_API_KEY` to `~/.config/realitydiff/env` (permissions 600, typing is hidden), and tells you if the command isn't on your PATH. `realitydiff setup --yes` never prompts, and `--no-key` skips the key.
50
+
51
+ Prefer a script that also installs `uv` for you (macOS or Linux, no sudo)?
47
52
 
48
53
  ```bash
49
- # options go after `sh -s --`
50
- curl -fsSL https://raw.githubusercontent.com/kavin0x/RealityDiff/main/install.sh | sh -s -- --yes # no prompts
51
- curl -fsSL https://raw.githubusercontent.com/kavin0x/RealityDiff/main/install.sh | sh -s -- --ref <tag-or-commit>
52
- curl -fsSL https://raw.githubusercontent.com/kavin0x/RealityDiff/main/install.sh | sh -s -- --uninstall
54
+ curl -fsSL https://raw.githubusercontent.com/kavin0x/RealityDiff/main/install.sh | sh
53
55
  ```
54
56
 
55
- Prefer to read it first? Download `install.sh`, look it over, then run `sh install.sh --help`.
57
+ Read it first if you like: download `install.sh`, then run `sh install.sh --help`. Options go after `sh -s --`: `--yes`, `--ref <tag-or-commit>`, `--uninstall`.
56
58
 
57
- Or install by hand:
59
+ Want the latest unreleased code instead of PyPI?
58
60
 
59
61
  ```bash
60
62
  uv tool install "realitydiff @ https://github.com/kavin0x/RealityDiff/archive/main.tar.gz"
61
- # or
62
- pipx install "realitydiff @ https://github.com/kavin0x/RealityDiff/archive/main.tar.gz"
63
63
  ```
64
64
 
65
65
  Then:
66
66
 
67
67
  ```bash
68
- realitydiff serve --host 127.0.0.1 # open http://127.0.0.1:8000
68
+ realitydiff serve # binds to 127.0.0.1 only; open http://127.0.0.1:8000
69
69
  ```
70
70
 
71
71
  The installed CLI keeps claims in `~/.local/share/realitydiff/` (set `REALITYDIFF_DB` to change it). Inside a checkout it still uses `data/`.
@@ -1,9 +1,21 @@
1
1
  [project]
2
2
  name = "realitydiff"
3
- version = "0.4.0"
3
+ version = "0.4.1"
4
4
  description = "Git for beliefs — living claims that watch the internet and version confidence."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
7
+ license = { file = "LICENSE" }
8
+ authors = [{ name = "Kavin Shah" }]
9
+ keywords = ["claims", "beliefs", "fact-checking", "xai", "grok"]
10
+ classifiers = [
11
+ "Development Status :: 3 - Alpha",
12
+ "Environment :: Console",
13
+ "Intended Audience :: Developers",
14
+ "Programming Language :: Python :: 3",
15
+ "Programming Language :: Python :: 3.11",
16
+ "Programming Language :: Python :: 3.12",
17
+ "Programming Language :: Python :: 3.13",
18
+ ]
7
19
  dependencies = [
8
20
  "fastapi>=0.115.0",
9
21
  "httpx>=0.28.0",
@@ -11,6 +23,11 @@ dependencies = [
11
23
  "uvicorn[standard]>=0.34.0",
12
24
  ]
13
25
 
26
+ [project.urls]
27
+ Homepage = "https://github.com/kavin0x/RealityDiff"
28
+ Source = "https://github.com/kavin0x/RealityDiff"
29
+ Issues = "https://github.com/kavin0x/RealityDiff/issues"
30
+
14
31
  [project.optional-dependencies]
15
32
  dev = [
16
33
  "pytest>=8.3.0",
@@ -5,4 +5,4 @@ from realitydiff.models import ClaimState, Commit
5
5
  from realitydiff.service import RealityDiff
6
6
 
7
7
  __all__ = ["Claim", "ClaimState", "Commit", "RealityDiff"]
8
- __version__ = "0.4.0"
8
+ __version__ = "0.4.1"
@@ -2,11 +2,13 @@ from __future__ import annotations
2
2
 
3
3
  import argparse
4
4
  import json
5
+ import os
5
6
  import sys
6
7
  from pathlib import Path
7
8
 
8
9
  from realitydiff import __version__
9
10
  from realitydiff.config import config_file, default_db, load_env
11
+ from realitydiff.setup import needs_first_run, run_setup
10
12
  from realitydiff.service import RealityDiff
11
13
  from realitydiff.xai import XAIError
12
14
 
@@ -29,10 +31,32 @@ def _print(data) -> None:
29
31
  print(json.dumps(data, indent=2, default=str))
30
32
 
31
33
 
34
+ def _ensure_key() -> bool:
35
+ """True when an xAI key is available. Offers first-run setup to a person at a terminal."""
36
+ if os.environ.get("XAI_API_KEY"):
37
+ return True
38
+ if needs_first_run():
39
+ print("realitydiff: first run, so let's set up.\n", file=sys.stderr)
40
+ run_setup()
41
+ print()
42
+ load_env()
43
+ if os.environ.get("XAI_API_KEY"):
44
+ return True
45
+ print(
46
+ f"realitydiff: XAI_API_KEY is not set. Run `realitydiff setup`, export it, or save it in {config_file()}",
47
+ file=sys.stderr,
48
+ )
49
+ return False
50
+
51
+
32
52
  def main(argv: list[str] | None = None) -> int:
33
53
  parser = argparse.ArgumentParser(prog="realitydiff", description="Git for beliefs")
34
54
  parser.add_argument("--version", action="version", version=f"realitydiff {__version__}")
35
- sub = parser.add_subparsers(dest="cmd", required=True)
55
+ sub = parser.add_subparsers(dest="cmd")
56
+
57
+ setup_p = sub.add_parser("setup", help="Guided setup: save your xAI key and check your PATH")
58
+ setup_p.add_argument("-y", "--yes", action="store_true", help="Non-interactive: accept defaults, never prompt")
59
+ setup_p.add_argument("--no-key", action="store_true", help="Skip the XAI_API_KEY prompt")
36
60
 
37
61
  open_p = sub.add_parser("open", help="Create a living claim and survey the internet")
38
62
  open_p.add_argument("statement")
@@ -67,7 +91,7 @@ def main(argv: list[str] | None = None) -> int:
67
91
  ls_p = sub.add_parser("ls", help="List claims")
68
92
 
69
93
  serve_p = sub.add_parser("serve", help="Run the web UI")
70
- serve_p.add_argument("--host", default="0.0.0.0")
94
+ serve_p.add_argument("--host", default="127.0.0.1", help="Bind address. Use 0.0.0.0 only if you want other machines to reach it")
71
95
  serve_p.add_argument("--port", type=int, default=8000)
72
96
 
73
97
  svc = sub.add_parser("service", help="Install a background timer that runs claim.watch()")
@@ -78,7 +102,16 @@ def main(argv: list[str] | None = None) -> int:
78
102
  svc_sub.add_parser("status", help="Show whether the service is installed")
79
103
 
80
104
  args = parser.parse_args(argv)
105
+ if args.cmd is None:
106
+ if needs_first_run():
107
+ return run_setup()
108
+ parser.print_help()
109
+ return 0
110
+ if args.cmd == "setup":
111
+ return run_setup(assume_yes=args.yes, skip_key=args.no_key)
81
112
  if args.cmd == "serve":
113
+ if not _ensure_key():
114
+ return 2
82
115
  import uvicorn
83
116
 
84
117
  print(f"realitydiff: database {_db_path()}", file=sys.stderr)
@@ -97,10 +130,12 @@ def main(argv: list[str] | None = None) -> int:
97
130
  _print({**status(), "db": _db_path()})
98
131
  return 0
99
132
 
133
+ if not _ensure_key():
134
+ return 2
100
135
  try:
101
136
  engine = _engine()
102
137
  except XAIError as exc:
103
- print(f"realitydiff: {exc}. Export it or save it in {config_file()}", file=sys.stderr)
138
+ print(f"realitydiff: {exc}", file=sys.stderr)
104
139
  return 2
105
140
  if args.cmd == "watch-all":
106
141
  _print([item.model_dump(mode="json") for item in engine.watch_due()])
@@ -64,3 +64,70 @@ def default_db() -> str:
64
64
  return "data/realitydiff.sqlite"
65
65
  base = os.environ.get("XDG_DATA_HOME") or str(Path.home() / ".local" / "share")
66
66
  return str(Path(base) / "realitydiff" / "realitydiff.sqlite")
67
+
68
+
69
+ _KEY_OK = frozenset("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-")
70
+
71
+
72
+ def valid_secret(value: str) -> bool:
73
+ """Same rule as install.sh: no spaces, quotes or newlines."""
74
+ return bool(value) and all(ch in _KEY_OK for ch in value)
75
+
76
+
77
+ def config_has(key: str, path: Path | None = None) -> bool:
78
+ path = path or config_file()
79
+ try:
80
+ text = path.read_text(encoding="utf-8")
81
+ except OSError:
82
+ return False
83
+ for raw in text.splitlines():
84
+ line = raw.strip()
85
+ if line.startswith("export "):
86
+ line = line[len("export ") :].strip()
87
+ name, sep, value = line.partition("=")
88
+ if sep and name.strip() == key and value.strip():
89
+ return True
90
+ return False
91
+
92
+
93
+ def set_config(key: str, value: str, path: Path | None = None) -> Path:
94
+ """Write KEY=VALUE to the config file (mode 600), keeping other lines."""
95
+ path = path or config_file()
96
+ path.parent.mkdir(parents=True, exist_ok=True)
97
+ try:
98
+ path.parent.chmod(0o700)
99
+ except OSError:
100
+ pass
101
+ kept: list[str] = []
102
+ try:
103
+ existing = path.read_text(encoding="utf-8").splitlines()
104
+ except OSError:
105
+ existing = ["# Reality Diff settings (read by the realitydiff CLI). Keep private."]
106
+ for raw in existing:
107
+ line = raw.strip()
108
+ if line.startswith("export "):
109
+ line = line[len("export ") :].strip()
110
+ if line.partition("=")[0].strip() == key and "=" in line:
111
+ continue
112
+ kept.append(raw)
113
+ kept.append(f"{key}={value}")
114
+ tmp = path.with_name(path.name + f".tmp{os.getpid()}")
115
+ fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
116
+ try:
117
+ with os.fdopen(fd, "w", encoding="utf-8") as handle:
118
+ handle.write("\n".join(kept) + "\n")
119
+ os.replace(tmp, path)
120
+ finally:
121
+ try:
122
+ tmp.unlink()
123
+ except FileNotFoundError:
124
+ pass
125
+ try:
126
+ path.chmod(0o600)
127
+ except OSError:
128
+ pass
129
+ return path
130
+
131
+
132
+ def setup_marker() -> Path:
133
+ return config_file().with_name("setup-done")
@@ -0,0 +1,198 @@
1
+ """Interactive first-run setup: the same flow install.sh gives, for pip/pipx/uv installs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import getpass
6
+ import os
7
+ import shutil
8
+ import subprocess
9
+ import sys
10
+ from pathlib import Path
11
+
12
+ from realitydiff import __version__
13
+ from realitydiff.config import (
14
+ config_file,
15
+ config_has,
16
+ default_db,
17
+ set_config,
18
+ setup_marker,
19
+ valid_secret,
20
+ )
21
+
22
+ ART = r"""
23
+ ___ _ _ _ ___ _ __ __
24
+ | _ \___ __ _| (_) |_ _ _| \(_)/ _|/ _|
25
+ | / -_) _` | | | _| || | |) | | _| _|
26
+ |_|_\___\__,_|_|_|\__|\_, |___/|_|_| |_|
27
+ |__/
28
+ """
29
+
30
+
31
+ class _Style:
32
+ def __init__(self, out) -> None:
33
+ fancy = out.isatty() and not os.environ.get("NO_COLOR") and os.environ.get("TERM", "dumb") != "dumb"
34
+ self.fancy = fancy
35
+ esc = "\033["
36
+ self.bold = f"{esc}1m" if fancy else ""
37
+ self.dim = f"{esc}2m" if fancy else ""
38
+ self.red = f"{esc}31m" if fancy else ""
39
+ self.green = f"{esc}32m" if fancy else ""
40
+ self.yellow = f"{esc}33m" if fancy else ""
41
+ self.cyan = f"{esc}36m" if fancy else ""
42
+ self.reset = f"{esc}0m" if fancy else ""
43
+ self.ok_mark = "✓" if fancy else "[ok]"
44
+
45
+
46
+ def _tilde(path: str | Path) -> str:
47
+ text = str(path)
48
+ home = str(Path.home())
49
+ return "~" + text[len(home) :] if text.startswith(home + os.sep) else text
50
+
51
+
52
+ def is_interactive() -> bool:
53
+ return sys.stdin.isatty() and sys.stdout.isatty()
54
+
55
+
56
+ def needs_first_run() -> bool:
57
+ """True when nothing has configured this install yet and a person is at the keyboard."""
58
+ if not is_interactive():
59
+ return False
60
+ if os.environ.get("XAI_API_KEY") or config_has("XAI_API_KEY"):
61
+ return False
62
+ return not setup_marker().exists()
63
+
64
+
65
+ def _confirm(question: str, default: bool, style: _Style, assume_yes: bool, interactive: bool, ask) -> bool:
66
+ if assume_yes or not interactive:
67
+ return default
68
+ hint = "[Y/n]" if default else "[y/N]"
69
+ answer = ask(f" {style.cyan}?{style.reset} {question} {hint} ").strip().lower()
70
+ if answer in {"y", "yes"}:
71
+ return True
72
+ if answer in {"n", "no"}:
73
+ return False
74
+ return default
75
+
76
+
77
+ def _bin_dir() -> Path | None:
78
+ exe = shutil.which("realitydiff")
79
+ if exe:
80
+ return Path(exe).parent
81
+ script = Path(sys.argv[0]).resolve() if sys.argv and sys.argv[0] else None
82
+ if script and script.name.startswith("realitydiff"):
83
+ return script.parent
84
+ return None
85
+
86
+
87
+ def run_setup(
88
+ *,
89
+ assume_yes: bool = False,
90
+ skip_key: bool = False,
91
+ out=None,
92
+ ask=input,
93
+ ask_secret=getpass.getpass,
94
+ interactive: bool | None = None,
95
+ ) -> int:
96
+ out = out or sys.stdout
97
+ style = _Style(out)
98
+ interactive = is_interactive() if interactive is None else interactive
99
+ if assume_yes:
100
+ interactive = False
101
+ config = config_file()
102
+
103
+ def say(text: str = "") -> None:
104
+ print(text, file=out)
105
+
106
+ def ok(text: str) -> None:
107
+ say(f" {style.green}{style.ok_mark}{style.reset} {text}")
108
+
109
+ def info(text: str) -> None:
110
+ say(f" {style.dim}{text}{style.reset}")
111
+
112
+ def warn(text: str) -> None:
113
+ say(f" {style.yellow}!{style.reset} {text}")
114
+
115
+ def step(n: int, total: int, title: str) -> None:
116
+ say(f"{style.dim}[{n}/{total}]{style.reset} {style.bold}{title}{style.reset}")
117
+
118
+ if style.fancy:
119
+ say(f"{style.bold}{style.cyan}{ART}{style.reset} {style.dim}git for beliefs{style.reset}\n")
120
+ else:
121
+ say("Reality Diff setup")
122
+ say()
123
+
124
+ step(1, 3, "Checking the install")
125
+ ok(f"realitydiff {__version__} on Python {sys.version_info.major}.{sys.version_info.minor}")
126
+ info(f"data {_tilde(default_db())}")
127
+ info(f"config {_tilde(config)}")
128
+
129
+ step(2, 3, "xAI API key")
130
+ saved = False
131
+ if skip_key:
132
+ info("skipped (--no-key)")
133
+ else:
134
+ have_file = config_has("XAI_API_KEY")
135
+ env_key = os.environ.get("XAI_API_KEY", "")
136
+ new_key = ""
137
+ if have_file:
138
+ ok(f"already saved in {_tilde(config)}")
139
+ if interactive and _confirm("Replace the saved key?", False, style, assume_yes, interactive, ask):
140
+ new_key = ask_secret(" ? New XAI_API_KEY (input hidden): ").strip()
141
+ elif interactive:
142
+ if env_key:
143
+ info("XAI_API_KEY is set in this shell")
144
+ if _confirm(
145
+ f"Save it to {_tilde(config)} so realitydiff always finds it?", False, style, assume_yes, interactive, ask
146
+ ):
147
+ new_key = env_key
148
+ elif _confirm(
149
+ "Save an XAI_API_KEY now? (get one at https://console.x.ai)", True, style, assume_yes, interactive, ask
150
+ ):
151
+ new_key = ask_secret(" ? XAI_API_KEY (input hidden): ").strip()
152
+ else:
153
+ info("not saved (non-interactive). Export XAI_API_KEY or run `realitydiff setup`.")
154
+ if new_key:
155
+ if valid_secret(new_key):
156
+ set_config("XAI_API_KEY", new_key, config)
157
+ ok(f"saved to {_tilde(config)} (permissions 600)")
158
+ saved = True
159
+ else:
160
+ warn("that key has unexpected characters (spaces, quotes or newlines?); not saved")
161
+ elif not have_file and interactive:
162
+ info("skipped. Run `realitydiff setup` any time, or export XAI_API_KEY.")
163
+
164
+ step(3, 3, "PATH")
165
+ bin_dir = _bin_dir()
166
+ on_path = bool(shutil.which("realitydiff"))
167
+ if on_path:
168
+ ok(f"`realitydiff` is on your PATH ({_tilde(bin_dir) if bin_dir else 'found'})")
169
+ elif bin_dir is not None:
170
+ warn(f"{_tilde(bin_dir)} is not on your PATH")
171
+ uv = shutil.which("uv")
172
+ if uv and interactive and _confirm(
173
+ "Add it to your shell config (runs 'uv tool update-shell')?", False, style, assume_yes, interactive, ask
174
+ ):
175
+ subprocess.run([uv, "tool", "update-shell"], check=False)
176
+ info("open a new terminal for it to take effect")
177
+ else:
178
+ info(f'add this to your shell rc: export PATH="{bin_dir}:$PATH"')
179
+ else:
180
+ info("could not tell where the realitydiff command lives; that is fine if it already runs")
181
+
182
+ try:
183
+ marker = setup_marker()
184
+ marker.parent.mkdir(parents=True, exist_ok=True)
185
+ marker.write_text(f"{__version__}\n", encoding="utf-8")
186
+ except OSError:
187
+ pass
188
+
189
+ say()
190
+ say(f"{style.green}{style.bold}Reality Diff is set up.{style.reset}")
191
+ say()
192
+ say(" Next:")
193
+ say(f" {style.bold}realitydiff serve --host 127.0.0.1{style.reset}")
194
+ say(f" {style.dim}then open http://127.0.0.1:8000{style.reset}")
195
+ say(f' {style.bold}realitydiff open "Apple will replace Siri with an LLM"{style.reset}')
196
+ say()
197
+ say(f" {style.dim}Uninstall: pipx uninstall realitydiff | uv tool uninstall realitydiff | pip uninstall realitydiff{style.reset}")
198
+ return 0
@@ -0,0 +1,36 @@
1
+ import re
2
+ from pathlib import Path
3
+
4
+ WORKFLOWS = Path(__file__).resolve().parents[1] / ".github" / "workflows"
5
+ PUBLISH = WORKFLOWS / "pypi-publish.yml"
6
+
7
+ PINNED_ACTIONS = {
8
+ "actions/checkout": "3d3c42e5aac5ba805825da76410c181273ba90b1",
9
+ "astral-sh/setup-uv": "07a047e608db84b3d8c06cd9a7c7dcc9c355c18e",
10
+ "actions/upload-artifact": "cf430e030ddbb5b0abf93d22962f4752f3646cd9",
11
+ "actions/download-artifact": "9000827ccba6bdab643e8b6fd33ac0654aef8333",
12
+ "oven-sh/setup-bun": "0c5077e51419868618aeaa5fe8019c62421857d6",
13
+ "pypa/gh-action-pypi-publish": "7245906fb61fa33a13554f9d5fb856fe61758086",
14
+ }
15
+
16
+
17
+ def test_pypi_publish_workflow_uses_pypi_environment():
18
+ text = PUBLISH.read_text()
19
+ assert "name: pypi" in text
20
+ assert "https://pypi.org/p/realitydiff" in text
21
+
22
+
23
+ def test_pypi_publish_runs_tests_and_checks_before_publish():
24
+ text = PUBLISH.read_text()
25
+ assert "needs: test" in text
26
+ assert "needs: build" in text
27
+ assert "pytest" in text
28
+ assert "does not match pyproject version" in text
29
+ assert "Check package contents" in text
30
+
31
+
32
+ def test_all_workflow_actions_are_pinned_to_commit_shas():
33
+ for workflow in WORKFLOWS.glob("*.yml"):
34
+ for action, ref in re.findall(r"uses:\s*([\w./-]+)@(\S+)", workflow.read_text()):
35
+ assert re.fullmatch(r"[0-9a-f]{40}", ref), f"{workflow.name}: {action}@{ref} is not a commit SHA"
36
+ assert PINNED_ACTIONS.get(action) == ref, f"{workflow.name}: unexpected pin for {action}"
@@ -0,0 +1,141 @@
1
+ import io
2
+ import os
3
+ import stat
4
+
5
+ import pytest
6
+
7
+ from realitydiff import cli, config, setup
8
+
9
+
10
+ @pytest.fixture(autouse=True)
11
+ def isolated(tmp_path, monkeypatch):
12
+ monkeypatch.setenv("REALITYDIFF_CONFIG", str(tmp_path / "cfg" / "env"))
13
+ monkeypatch.delenv("XAI_API_KEY", raising=False)
14
+ monkeypatch.setenv("NO_COLOR", "1")
15
+ return tmp_path
16
+
17
+
18
+ def run(**kw):
19
+ out = io.StringIO()
20
+ code = setup.run_setup(out=out, **kw)
21
+ return code, out.getvalue()
22
+
23
+
24
+ def test_saves_key_with_600_and_never_prints_it(isolated):
25
+ code, text = run(
26
+ interactive=True,
27
+ ask=lambda _p: "y",
28
+ ask_secret=lambda _p: "xai-abc_123.DEF",
29
+ )
30
+ assert code == 0
31
+ cfg = isolated / "cfg" / "env"
32
+ assert "XAI_API_KEY=xai-abc_123.DEF" in cfg.read_text()
33
+ assert stat.S_IMODE(cfg.stat().st_mode) == 0o600
34
+ assert "xai-abc_123" not in text
35
+ assert "realitydiff serve" in text
36
+ assert config.setup_marker().exists()
37
+
38
+
39
+ def test_declining_key_still_finishes_and_marks_done(isolated):
40
+ code, text = run(interactive=True, ask=lambda _p: "n", ask_secret=lambda _p: pytest.fail("asked"))
41
+ assert code == 0
42
+ assert not (isolated / "cfg" / "env").exists()
43
+ assert "skipped" in text
44
+ assert config.setup_marker().exists()
45
+
46
+
47
+ def test_yes_mode_never_prompts(isolated):
48
+ code, text = run(assume_yes=True, ask=lambda _p: pytest.fail("prompted"), ask_secret=lambda _p: pytest.fail("prompted"))
49
+ assert code == 0
50
+ assert "non-interactive" in text
51
+
52
+
53
+ def test_no_key_flag_skips(isolated):
54
+ _, text = run(interactive=True, skip_key=True, ask=lambda _p: pytest.fail("prompted"))
55
+ assert "skipped (--no-key)" in text
56
+
57
+
58
+ def test_rejects_key_with_bad_characters(isolated):
59
+ _, text = run(interactive=True, ask=lambda _p: "y", ask_secret=lambda _p: 'bad key"')
60
+ assert "not saved" in text
61
+ assert not (isolated / "cfg" / "env").exists()
62
+
63
+
64
+ def test_set_config_keeps_other_lines(isolated):
65
+ path = isolated / "cfg" / "env"
66
+ config.set_config("REALITYDIFF_DB", "/x.sqlite", path)
67
+ config.set_config("XAI_API_KEY", "one", path)
68
+ config.set_config("XAI_API_KEY", "two", path)
69
+ lines = path.read_text().splitlines()
70
+ assert "REALITYDIFF_DB=/x.sqlite" in lines
71
+ assert lines.count("XAI_API_KEY=two") == 1
72
+ assert "XAI_API_KEY=one" not in lines
73
+
74
+
75
+ def test_needs_first_run_only_on_a_fresh_terminal(isolated, monkeypatch):
76
+ monkeypatch.setattr(setup, "is_interactive", lambda: True)
77
+ assert setup.needs_first_run()
78
+ config.set_config("XAI_API_KEY", "k", isolated / "cfg" / "env")
79
+ assert not setup.needs_first_run()
80
+ monkeypatch.setattr(setup, "is_interactive", lambda: False)
81
+ (isolated / "cfg" / "env").unlink()
82
+ assert not setup.needs_first_run()
83
+
84
+
85
+ def test_serve_without_key_prints_a_clean_error_not_a_traceback(isolated, monkeypatch, capsys):
86
+ monkeypatch.setattr(setup, "is_interactive", lambda: False)
87
+ assert cli.main(["serve"]) == 2
88
+ err = capsys.readouterr().err
89
+ assert "XAI_API_KEY is not set" in err
90
+ assert "realitydiff setup" in err
91
+ assert "Traceback" not in err
92
+
93
+
94
+ def test_bare_command_runs_setup_first_time_then_help(isolated, monkeypatch, capsys):
95
+ monkeypatch.setattr(cli, "needs_first_run", lambda: True)
96
+ called = []
97
+ monkeypatch.setattr(cli, "run_setup", lambda **kw: called.append(1) or 0)
98
+ assert cli.main([]) == 0 and called
99
+ monkeypatch.setattr(cli, "needs_first_run", lambda: False)
100
+ assert cli.main([]) == 0
101
+ assert "usage: realitydiff" in capsys.readouterr().out
102
+
103
+
104
+ def test_serve_binds_to_localhost_by_default():
105
+ import argparse
106
+
107
+ seen = {}
108
+ import uvicorn
109
+
110
+ parser_calls = {}
111
+
112
+ def fake_run(app, host, port, **kw):
113
+ parser_calls["host"] = host
114
+
115
+ import pytest as _p
116
+
117
+ with _p.MonkeyPatch.context() as mp:
118
+ mp.setattr(uvicorn, "run", fake_run)
119
+ mp.setenv("XAI_API_KEY", "k")
120
+ cli.main(["serve"])
121
+ assert parser_calls["host"] == "127.0.0.1"
122
+
123
+
124
+ def test_set_config_leaves_no_temp_file_when_write_fails(isolated, monkeypatch):
125
+ path = isolated / "cfg" / "env"
126
+
127
+ def boom(*a, **k):
128
+ raise OSError("disk full")
129
+
130
+ monkeypatch.setattr(os, "replace", boom)
131
+ with pytest.raises(OSError):
132
+ config.set_config("XAI_API_KEY", "secret", path)
133
+ assert not [p for p in path.parent.iterdir() if ".tmp" in p.name]
134
+
135
+
136
+ def test_env_key_offer_defaults_to_no(isolated, monkeypatch):
137
+ monkeypatch.setenv("XAI_API_KEY", "xai-fromenv")
138
+ answers = []
139
+ code, _ = run(interactive=True, ask=lambda _p: answers.append(_p) or "", ask_secret=lambda _p: "")
140
+ assert code == 0
141
+ assert not (isolated / "cfg" / "env").exists()
@@ -416,7 +416,7 @@ wheels = [
416
416
 
417
417
  [[package]]
418
418
  name = "realitydiff"
419
- version = "0.4.0"
419
+ version = "0.4.1"
420
420
  source = { editable = "." }
421
421
  dependencies = [
422
422
  { name = "fastapi" },
@@ -1,53 +0,0 @@
1
- name: Publish to PyPI
2
-
3
- on:
4
- release:
5
- types: [published]
6
-
7
- permissions:
8
- contents: read
9
-
10
- jobs:
11
- build:
12
- name: Build distribution
13
- runs-on: ubuntu-latest
14
- steps:
15
- - name: Check out repository
16
- uses: actions/checkout@v7.0.1
17
-
18
- - name: Install uv
19
- uses: astral-sh/setup-uv@v10.3.2
20
- with:
21
- python-version: "3.11"
22
-
23
- - name: Build package
24
- run: uv build
25
-
26
- - name: Upload distribution
27
- uses: actions/upload-artifact@v7.0.2
28
- with:
29
- name: python-package-distributions
30
- path: dist/
31
- if-no-files-found: error
32
-
33
- publish:
34
- name: Publish to PyPI
35
- needs: build
36
- runs-on: ubuntu-latest
37
- environment:
38
- name: pypi
39
- url: https://pypi.org/p/realitydiff
40
- permissions:
41
- id-token: write
42
- attestations: write
43
- contents: read
44
- actions: read
45
- steps:
46
- - name: Download distribution
47
- uses: actions/download-artifact@v8.0.2
48
- with:
49
- name: python-package-distributions
50
- path: dist/
51
-
52
- - name: Publish package distributions to PyPI
53
- uses: pypa/gh-action-pypi-publish@v1.14.3
@@ -1,115 +0,0 @@
1
- Metadata-Version: 2.5
2
- Name: realitydiff
3
- Version: 0.4.0
4
- Summary: Git for beliefs — living claims that watch the internet and version confidence.
5
- License-File: LICENSE
6
- Requires-Python: >=3.11
7
- Requires-Dist: fastapi>=0.115.0
8
- Requires-Dist: httpx>=0.28.0
9
- Requires-Dist: pydantic>=2.10.0
10
- Requires-Dist: uvicorn[standard]>=0.34.0
11
- Provides-Extra: dev
12
- Requires-Dist: pytest-asyncio>=0.25.0; extra == 'dev'
13
- Requires-Dist: pytest>=8.3.0; extra == 'dev'
14
- Description-Content-Type: text/markdown
15
-
16
- # Reality Diff
17
-
18
- Git for beliefs — a version history of a claim: evidence, confidence, and why that number moved. For people tracking a story that is still unfolding (products, policy, markets) who want a trail, not a vibe.
19
-
20
- ## Why I built this
21
-
22
- News feeds overwrite your memory. Last week’s confidence on a rumor disappears under today’s take. I wanted git-style operations on a belief: commit when evidence actually moves, diff two weeks, blame a confidence jump, browse the claim as of a SHA, or revert a panic. So I built Reality Diff.
23
-
24
- ## News feed vs this
25
-
26
- | | Typical news / search | Reality Diff |
27
- | --- | --- | --- |
28
- | Memory | Overwritten | Versioned history per claim |
29
- | Confidence | Vibes / headlines | Explicit score + reason when it moves |
30
- | Compare | Manual screenshots | `diff` between commits |
31
- | Undo | None | `revert` to an earlier belief state |
32
-
33
- ## Features
34
-
35
- **Claims**
36
-
37
- - Open a claim (e.g. “Apple is going to replace Siri with an LLM”)
38
- - Living object: evidence for/against, unknowns, predictions, confidence
39
- - Commits only when **meaningful** new evidence appears (cosmetic rewrites dropped)
40
-
41
- **Git-style ops**
42
-
43
- - `commit` · `diff` · `blame` · `revert` · `watch` · `log`
44
- - Browse the belief as of any SHA
45
- - Background watch via OS timer (macOS LaunchAgent / Linux systemd / Windows task) or the UI interval picker
46
-
47
- **Surfaces**
48
-
49
- - Local web UI (`realitydiff serve` → <http://localhost:8000>)
50
- - CLI for agents and scripts
51
- - REST API for the same actions
52
-
53
- ## Install
54
-
55
- One line (macOS or Linux, no sudo):
56
-
57
- ```bash
58
- curl -fsSL https://raw.githubusercontent.com/kavin0x/RealityDiff/main/install.sh | sh
59
- ```
60
-
61
- It installs [uv](https://docs.astral.sh/uv/) into `~/.local/bin` if you don't have it, installs `realitydiff` as an isolated uv tool, and can save your `XAI_API_KEY` to `~/.config/realitydiff/env` (permissions 600). It never uses sudo and only touches your shell rc if you say yes.
62
-
63
- ```bash
64
- # options go after `sh -s --`
65
- curl -fsSL https://raw.githubusercontent.com/kavin0x/RealityDiff/main/install.sh | sh -s -- --yes # no prompts
66
- curl -fsSL https://raw.githubusercontent.com/kavin0x/RealityDiff/main/install.sh | sh -s -- --ref <tag-or-commit>
67
- curl -fsSL https://raw.githubusercontent.com/kavin0x/RealityDiff/main/install.sh | sh -s -- --uninstall
68
- ```
69
-
70
- Prefer to read it first? Download `install.sh`, look it over, then run `sh install.sh --help`.
71
-
72
- Or install by hand:
73
-
74
- ```bash
75
- uv tool install "realitydiff @ https://github.com/kavin0x/RealityDiff/archive/main.tar.gz"
76
- # or
77
- pipx install "realitydiff @ https://github.com/kavin0x/RealityDiff/archive/main.tar.gz"
78
- ```
79
-
80
- Then:
81
-
82
- ```bash
83
- realitydiff serve --host 127.0.0.1 # open http://127.0.0.1:8000
84
- ```
85
-
86
- The installed CLI keeps claims in `~/.local/share/realitydiff/` (set `REALITYDIFF_DB` to change it). Inside a checkout it still uses `data/`.
87
-
88
- ## Try it (from a checkout)
89
-
90
- ```bash
91
- export XAI_API_KEY=...
92
- uv sync --extra dev
93
- uv run realitydiff serve
94
- # open http://localhost:8000
95
- ```
96
-
97
- ```bash
98
- uv run realitydiff open "Apple is going to replace Siri with an LLM."
99
- uv run realitydiff watch <claim_id>
100
- uv run realitydiff log <claim_id>
101
- uv run realitydiff diff <claim_id> <sha_a> <sha_b>
102
- ```
103
-
104
- ```bash
105
- uv run realitydiff service install --interval 300
106
- uv run pytest
107
- ```
108
-
109
- ## Stack
110
-
111
- Python 3.11+ · uv · local web UI + REST · xAI Responses API (web + X search) · MIT
112
-
113
- ## License
114
-
115
- [MIT](LICENSE)
@@ -1,19 +0,0 @@
1
- from pathlib import Path
2
-
3
- WORKFLOW = Path(__file__).resolve().parents[1] / ".github" / "workflows" / "pypi-publish.yml"
4
-
5
- LATEST_ACTIONS = {
6
- "actions/checkout": "v7.0.1",
7
- "astral-sh/setup-uv": "v10.3.2",
8
- "actions/upload-artifact": "v7.0.2",
9
- "actions/download-artifact": "v8.0.2",
10
- "pypa/gh-action-pypi-publish": "v1.14.3",
11
- }
12
-
13
-
14
- def test_pypi_publish_workflow_uses_pypi_environment_and_latest_actions():
15
- text = WORKFLOW.read_text()
16
- assert "name: pypi" in text
17
- assert "https://pypi.org/p/realitydiff" in text
18
- for action, version in LATEST_ACTIONS.items():
19
- assert f"uses: {action}@{version}" in text
File without changes
File without changes
File without changes
File without changes