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.
- realitydiff-0.4.1/.github/workflows/ci.yml +32 -0
- realitydiff-0.4.1/.github/workflows/pypi-publish.yml +99 -0
- realitydiff-0.4.1/PKG-INFO +148 -0
- {realitydiff-0.4.0 → realitydiff-0.4.1}/README.md +12 -12
- {realitydiff-0.4.0 → realitydiff-0.4.1}/pyproject.toml +18 -1
- {realitydiff-0.4.0 → realitydiff-0.4.1}/src/realitydiff/__init__.py +1 -1
- {realitydiff-0.4.0 → realitydiff-0.4.1}/src/realitydiff/cli.py +38 -3
- {realitydiff-0.4.0 → realitydiff-0.4.1}/src/realitydiff/config.py +67 -0
- realitydiff-0.4.1/src/realitydiff/setup.py +198 -0
- realitydiff-0.4.1/tests/test_pypi_publish_workflow.py +36 -0
- realitydiff-0.4.1/tests/test_setup.py +141 -0
- {realitydiff-0.4.0 → realitydiff-0.4.1}/uv.lock +1 -1
- realitydiff-0.4.0/.github/workflows/pypi-publish.yml +0 -53
- realitydiff-0.4.0/PKG-INFO +0 -115
- realitydiff-0.4.0/tests/test_pypi_publish_workflow.py +0 -19
- {realitydiff-0.4.0 → realitydiff-0.4.1}/.codegraph/.gitignore +0 -0
- {realitydiff-0.4.0 → realitydiff-0.4.1}/.gitignore +0 -0
- {realitydiff-0.4.0 → realitydiff-0.4.1}/LICENSE +0 -0
- {realitydiff-0.4.0 → realitydiff-0.4.1}/data/.gitkeep +0 -0
- {realitydiff-0.4.0 → realitydiff-0.4.1}/install.sh +0 -0
- {realitydiff-0.4.0 → realitydiff-0.4.1}/src/realitydiff/__main__.py +0 -0
- {realitydiff-0.4.0 → realitydiff-0.4.1}/src/realitydiff/api.py +0 -0
- {realitydiff-0.4.0 → realitydiff-0.4.1}/src/realitydiff/blame.py +0 -0
- {realitydiff-0.4.0 → realitydiff-0.4.1}/src/realitydiff/claim.py +0 -0
- {realitydiff-0.4.0 → realitydiff-0.4.1}/src/realitydiff/compress.py +0 -0
- {realitydiff-0.4.0 → realitydiff-0.4.1}/src/realitydiff/diffing.py +0 -0
- {realitydiff-0.4.0 → realitydiff-0.4.1}/src/realitydiff/hashing.py +0 -0
- {realitydiff-0.4.0 → realitydiff-0.4.1}/src/realitydiff/models.py +0 -0
- {realitydiff-0.4.0 → realitydiff-0.4.1}/src/realitydiff/reasoner.py +0 -0
- {realitydiff-0.4.0 → realitydiff-0.4.1}/src/realitydiff/schedule.py +0 -0
- {realitydiff-0.4.0 → realitydiff-0.4.1}/src/realitydiff/service.py +0 -0
- {realitydiff-0.4.0 → realitydiff-0.4.1}/src/realitydiff/static/app.js +0 -0
- {realitydiff-0.4.0 → realitydiff-0.4.1}/src/realitydiff/static/favicon.svg +0 -0
- {realitydiff-0.4.0 → realitydiff-0.4.1}/src/realitydiff/static/index.html +0 -0
- {realitydiff-0.4.0 → realitydiff-0.4.1}/src/realitydiff/static/styles.css +0 -0
- {realitydiff-0.4.0 → realitydiff-0.4.1}/src/realitydiff/store.py +0 -0
- {realitydiff-0.4.0 → realitydiff-0.4.1}/src/realitydiff/watcher.py +0 -0
- {realitydiff-0.4.0 → realitydiff-0.4.1}/src/realitydiff/xai.py +0 -0
- {realitydiff-0.4.0 → realitydiff-0.4.1}/tests/__init__.py +0 -0
- {realitydiff-0.4.0 → realitydiff-0.4.1}/tests/test_api.py +0 -0
- {realitydiff-0.4.0 → realitydiff-0.4.1}/tests/test_close_claim.py +0 -0
- {realitydiff-0.4.0 → realitydiff-0.4.1}/tests/test_compress.py +0 -0
- {realitydiff-0.4.0 → realitydiff-0.4.1}/tests/test_config.py +0 -0
- {realitydiff-0.4.0 → realitydiff-0.4.1}/tests/test_git_beliefs.py +0 -0
- {realitydiff-0.4.0 → realitydiff-0.4.1}/tests/test_reasoner.py +0 -0
- {realitydiff-0.4.0 → realitydiff-0.4.1}/tests/test_robustness.py +0 -0
- {realitydiff-0.4.0 → realitydiff-0.4.1}/tests/test_schedule.py +0 -0
- {realitydiff-0.4.0 → realitydiff-0.4.1}/tests/test_ui.py +0 -0
- {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
|
-
|
|
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
|
-
|
|
43
|
+
uv tool install realitydiff # or: pipx install realitydiff
|
|
44
|
+
realitydiff # first run walks you through setup
|
|
44
45
|
```
|
|
45
46
|
|
|
46
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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",
|
|
@@ -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"
|
|
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}
|
|
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()
|
|
@@ -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
|
realitydiff-0.4.0/PKG-INFO
DELETED
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|