diffbutler 0.5.0__py3-none-any.whl
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.
- diffbutler/__init__.py +1 -0
- diffbutler/__main__.py +6 -0
- diffbutler/config/__init__.py +1 -0
- diffbutler/config/default.toml +91 -0
- diffbutler/config/diagnostics.py +90 -0
- diffbutler/config/file.py +30 -0
- diffbutler/config/instance.py +55 -0
- diffbutler/config/settings.py +297 -0
- diffbutler/config/setup.py +92 -0
- diffbutler/config/transport.py +65 -0
- diffbutler/core/__init__.py +1 -0
- diffbutler/core/application.py +103 -0
- diffbutler/core/context.py +54 -0
- diffbutler/core/dashboard.py +393 -0
- diffbutler/core/doctor.py +200 -0
- diffbutler/core/lifecycle.py +70 -0
- diffbutler/core/managed.py +441 -0
- diffbutler/core/native.py +301 -0
- diffbutler/core/polling.py +118 -0
- diffbutler/core/process.py +64 -0
- diffbutler/core/repositories.py +260 -0
- diffbutler/core/reset.py +34 -0
- diffbutler/core/resources.py +50 -0
- diffbutler/core/review.py +331 -0
- diffbutler/core/setup.py +232 -0
- diffbutler/core/snapshot.py +70 -0
- diffbutler/core/sync.py +259 -0
- diffbutler/core/worker.py +205 -0
- diffbutler/core/workspaces.py +139 -0
- diffbutler/domain/__init__.py +1 -0
- diffbutler/domain/dashboard.py +63 -0
- diffbutler/domain/diff.py +285 -0
- diffbutler/domain/forge.py +123 -0
- diffbutler/domain/lifecycle.py +51 -0
- diffbutler/domain/polling.py +92 -0
- diffbutler/domain/process.py +29 -0
- diffbutler/domain/redaction.py +61 -0
- diffbutler/domain/resources.py +50 -0
- diffbutler/domain/review.py +167 -0
- diffbutler/domain/sync.py +196 -0
- diffbutler/forges/__init__.py +1 -0
- diffbutler/forges/base.py +39 -0
- diffbutler/forges/diagnostics.py +104 -0
- diffbutler/forges/github/__init__.py +1 -0
- diffbutler/forges/github/adapter.py +352 -0
- diffbutler/forges/github/auth.py +49 -0
- diffbutler/forges/github/http.py +326 -0
- diffbutler/forges/github/mapping.py +157 -0
- diffbutler/forges/github/setup.py +86 -0
- diffbutler/forges/registry.py +23 -0
- diffbutler/py.typed +0 -0
- diffbutler/reviewers/__init__.py +1 -0
- diffbutler/reviewers/base.py +101 -0
- diffbutler/reviewers/extraction.py +198 -0
- diffbutler/reviewers/native.py +238 -0
- diffbutler/reviewers/parsing.py +247 -0
- diffbutler/reviewers/presets.toml +12 -0
- diffbutler/reviewers/process.py +78 -0
- diffbutler/reviewers/prompt.py +148 -0
- diffbutler/reviewers/redaction.py +30 -0
- diffbutler/reviewers/resources.py +162 -0
- diffbutler/sources/__init__.py +1 -0
- diffbutler/sources/diagnostics.py +100 -0
- diffbutler/sources/fetch.py +285 -0
- diffbutler/sources/git.py +461 -0
- diffbutler/sources/remotes.py +82 -0
- diffbutler/store/__init__.py +1 -0
- diffbutler/store/base.py +88 -0
- diffbutler/store/dashboard.py +201 -0
- diffbutler/store/database.py +172 -0
- diffbutler/store/lifecycle.py +247 -0
- diffbutler/store/migrations/__init__.py +1 -0
- diffbutler/store/migrations/env.py +14 -0
- diffbutler/store/migrations/versions/0001_initial.py +147 -0
- diffbutler/store/migrations/versions/0002_monitoring.py +38 -0
- diffbutler/store/migrations/versions/0003_reviews.py +72 -0
- diffbutler/store/migrations/versions/0004_claims.py +22 -0
- diffbutler/store/migrations/versions/0005_results.py +27 -0
- diffbutler/store/migrations/versions/0006_anchors.py +17 -0
- diffbutler/store/migrations/versions/0007_lifecycle.py +36 -0
- diffbutler/store/migrations/versions/0008_processes.py +30 -0
- diffbutler/store/migrations/versions/0009_dashboard.py +43 -0
- diffbutler/store/migrations/versions/0010_run_reasons.py +17 -0
- diffbutler/store/migrations/versions/__init__.py +1 -0
- diffbutler/store/models.py +290 -0
- diffbutler/store/process.py +75 -0
- diffbutler/store/queue.py +289 -0
- diffbutler/store/review.py +583 -0
- diffbutler/ui/__init__.py +1 -0
- diffbutler/ui/cli/__init__.py +532 -0
- diffbutler/ui/cli/editor.py +33 -0
- diffbutler/ui/tui/__init__.py +1 -0
- diffbutler/ui/tui/_fake_agent.py +47 -0
- diffbutler/ui/tui/_foreground_exec.py +23 -0
- diffbutler/ui/tui/app.py +34 -0
- diffbutler/ui/tui/browse.py +119 -0
- diffbutler/ui/tui/browser.py +42 -0
- diffbutler/ui/tui/dashboard.py +926 -0
- diffbutler/ui/tui/foreground.py +103 -0
- diffbutler/ui/tui/handoff_probe.py +184 -0
- diffbutler-0.5.0.dist-info/METADATA +170 -0
- diffbutler-0.5.0.dist-info/RECORD +105 -0
- diffbutler-0.5.0.dist-info/WHEEL +4 -0
- diffbutler-0.5.0.dist-info/entry_points.txt +3 -0
- diffbutler-0.5.0.dist-info/licenses/LICENSE.txt +21 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""POSIX terminal ownership experiment; call only while the UI is suspended."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import os
|
|
5
|
+
import signal
|
|
6
|
+
import sys
|
|
7
|
+
import termios
|
|
8
|
+
from collections.abc import Awaitable, Callable, Mapping, Sequence
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from diffbutler.domain.lifecycle import ForegroundExitUncertain as ForegroundExitUncertain
|
|
12
|
+
from diffbutler.reviewers.native import stop_process_group
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _give_terminal(fd: int, process_group: int) -> None:
|
|
16
|
+
# Reclaiming the terminal is a background-process operation: block SIGTTOU.
|
|
17
|
+
previous = signal.signal(signal.SIGTTOU, signal.SIG_IGN)
|
|
18
|
+
try:
|
|
19
|
+
os.tcsetpgrp(fd, process_group)
|
|
20
|
+
finally:
|
|
21
|
+
signal.signal(signal.SIGTTOU, previous)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
async def run_foreground(
|
|
25
|
+
argv: Sequence[str],
|
|
26
|
+
*,
|
|
27
|
+
cwd: Path | None = None,
|
|
28
|
+
env: Mapping[str, str] | None = None,
|
|
29
|
+
started: Callable[[int], Awaitable[None]] | None = None,
|
|
30
|
+
) -> int:
|
|
31
|
+
"""Attach one process group, await without blocking asyncio, always restore TTY.
|
|
32
|
+
|
|
33
|
+
The caller serializes handoffs. This is not a reviewer sandbox or credential
|
|
34
|
+
boundary: the foreground command inherits the user's environment.
|
|
35
|
+
"""
|
|
36
|
+
if not argv:
|
|
37
|
+
raise ValueError("An agent command is required")
|
|
38
|
+
fd = sys.stdin.fileno()
|
|
39
|
+
if not os.isatty(fd) or os.tcgetpgrp(fd) != os.getpgrp():
|
|
40
|
+
raise RuntimeError("Handoff requires ownership of a controlling terminal")
|
|
41
|
+
owner = os.tcgetpgrp(fd)
|
|
42
|
+
modes = termios.tcgetattr(fd)
|
|
43
|
+
read_gate, write_gate = os.pipe()
|
|
44
|
+
spawn: asyncio.Task[asyncio.subprocess.Process] | None = None
|
|
45
|
+
try:
|
|
46
|
+
spawn = asyncio.create_task(
|
|
47
|
+
asyncio.create_subprocess_exec(
|
|
48
|
+
sys.executable,
|
|
49
|
+
"-m",
|
|
50
|
+
"diffbutler.ui.tui._foreground_exec",
|
|
51
|
+
str(read_gate),
|
|
52
|
+
*argv,
|
|
53
|
+
stdin=fd,
|
|
54
|
+
stdout=sys.stdout.fileno(),
|
|
55
|
+
stderr=sys.stderr.fileno(),
|
|
56
|
+
pass_fds=(read_gate,),
|
|
57
|
+
process_group=0,
|
|
58
|
+
cwd=cwd,
|
|
59
|
+
env=env,
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
process = await asyncio.shield(spawn)
|
|
63
|
+
if started is not None:
|
|
64
|
+
await started(process.pid)
|
|
65
|
+
_give_terminal(fd, process.pid)
|
|
66
|
+
os.write(write_gate, b"1")
|
|
67
|
+
return await process.wait()
|
|
68
|
+
finally:
|
|
69
|
+
|
|
70
|
+
async def stop() -> None:
|
|
71
|
+
try:
|
|
72
|
+
if spawn is not None:
|
|
73
|
+
try:
|
|
74
|
+
process = await spawn
|
|
75
|
+
except OSError:
|
|
76
|
+
return
|
|
77
|
+
await stop_process_group(process.pid)
|
|
78
|
+
await process.wait()
|
|
79
|
+
finally:
|
|
80
|
+
os.close(read_gate)
|
|
81
|
+
os.close(write_gate)
|
|
82
|
+
|
|
83
|
+
stopping = asyncio.create_task(stop())
|
|
84
|
+
cancelled = False
|
|
85
|
+
try:
|
|
86
|
+
while True:
|
|
87
|
+
try:
|
|
88
|
+
await asyncio.shield(stopping)
|
|
89
|
+
break
|
|
90
|
+
except asyncio.CancelledError:
|
|
91
|
+
cancelled = True
|
|
92
|
+
if stopping.done():
|
|
93
|
+
stopping.result()
|
|
94
|
+
break
|
|
95
|
+
except Exception:
|
|
96
|
+
raise ForegroundExitUncertain(
|
|
97
|
+
"Foreground process exit could not be verified"
|
|
98
|
+
) from None
|
|
99
|
+
finally:
|
|
100
|
+
_give_terminal(fd, owner)
|
|
101
|
+
termios.tcsetattr(fd, termios.TCSANOW, modes)
|
|
102
|
+
if cancelled:
|
|
103
|
+
raise asyncio.CancelledError
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""Runnable terminal experiment. No forge access or automatic model calls."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import asyncio
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import platform
|
|
8
|
+
import sys
|
|
9
|
+
import tempfile
|
|
10
|
+
import time
|
|
11
|
+
import uuid
|
|
12
|
+
from importlib.metadata import version
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import ClassVar
|
|
15
|
+
|
|
16
|
+
from textual.app import ComposeResult
|
|
17
|
+
from textual.binding import BindingType
|
|
18
|
+
from textual.events import Resize
|
|
19
|
+
from textual.widgets import DataTable, Footer, Header, Static
|
|
20
|
+
|
|
21
|
+
from diffbutler.ui.tui.app import TerminalApp
|
|
22
|
+
from diffbutler.ui.tui.foreground import run_foreground
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class HandoffProbe(TerminalApp):
|
|
26
|
+
"""Three fake PRs, a live heartbeat, and an explicitly launched agent."""
|
|
27
|
+
|
|
28
|
+
TITLE = "DiffButler — terminal handoff experiment"
|
|
29
|
+
BINDINGS: ClassVar[list[BindingType]] = [
|
|
30
|
+
("c", "handoff", "Resume agent"),
|
|
31
|
+
("f", "fail", "Bad command"),
|
|
32
|
+
("q", "quit", "Quit"),
|
|
33
|
+
]
|
|
34
|
+
CSS = "DataTable { height: 8; } Static { margin: 1 2; }"
|
|
35
|
+
|
|
36
|
+
def __init__(self, directory: Path, command: list[str], events: Path) -> None:
|
|
37
|
+
super().__init__()
|
|
38
|
+
self.directory = directory
|
|
39
|
+
self.command = command
|
|
40
|
+
self.events_path = events
|
|
41
|
+
self.ticks = 0
|
|
42
|
+
self.attached = False
|
|
43
|
+
self.session = directory / "fake-session.json"
|
|
44
|
+
self.session.write_text(
|
|
45
|
+
json.dumps({"id": str(uuid.uuid4()), "resumes": 0, "seed": "synthetic source review"})
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def record(self, event: str, **fields: object) -> None:
|
|
49
|
+
with self.events_path.open("a") as output:
|
|
50
|
+
output.write(
|
|
51
|
+
json.dumps(
|
|
52
|
+
{"event": event, "time": time.monotonic(), "ticks": self.ticks, **fields}
|
|
53
|
+
)
|
|
54
|
+
+ "\n"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def compose(self) -> ComposeResult:
|
|
58
|
+
yield Header()
|
|
59
|
+
yield DataTable[str](cursor_type="row")
|
|
60
|
+
yield Static("Select a row, then v. The default agent is an offline fake.", id="result")
|
|
61
|
+
yield Static("", id="heartbeat")
|
|
62
|
+
yield Footer()
|
|
63
|
+
|
|
64
|
+
def on_mount(self) -> None:
|
|
65
|
+
table = self.query_one(DataTable[str])
|
|
66
|
+
table.add_columns("PR", "Status")
|
|
67
|
+
table.add_rows([("demo#1", "Ready"), ("demo#2", "Findings"), ("demo#3", "Waiting")])
|
|
68
|
+
table.focus()
|
|
69
|
+
self.run_worker(self.heartbeat())
|
|
70
|
+
self.run_worker(self.background_output())
|
|
71
|
+
self.record(
|
|
72
|
+
"ready",
|
|
73
|
+
system=platform.system(),
|
|
74
|
+
python=platform.python_version(),
|
|
75
|
+
textual=version("textual"),
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def on_resize(self, event: Resize) -> None:
|
|
79
|
+
self.record("resize", width=event.size.width, height=event.size.height)
|
|
80
|
+
|
|
81
|
+
async def heartbeat(self) -> None:
|
|
82
|
+
while True:
|
|
83
|
+
await asyncio.sleep(0.1)
|
|
84
|
+
self.ticks += 1
|
|
85
|
+
self.record("tick", attached=self.attached)
|
|
86
|
+
if not self.attached:
|
|
87
|
+
self.query_one("#heartbeat", Static).update(f"Background ticks: {self.ticks}")
|
|
88
|
+
|
|
89
|
+
async def background_output(self) -> None:
|
|
90
|
+
with (self.directory / "background.log").open("wb") as output:
|
|
91
|
+
process = await asyncio.create_subprocess_exec(
|
|
92
|
+
sys.executable,
|
|
93
|
+
"-m",
|
|
94
|
+
"diffbutler.ui.tui._fake_agent",
|
|
95
|
+
"--ticker",
|
|
96
|
+
stdin=asyncio.subprocess.DEVNULL,
|
|
97
|
+
stdout=output,
|
|
98
|
+
stderr=output,
|
|
99
|
+
start_new_session=True,
|
|
100
|
+
)
|
|
101
|
+
try:
|
|
102
|
+
await process.wait()
|
|
103
|
+
finally:
|
|
104
|
+
if process.returncode is None:
|
|
105
|
+
process.kill()
|
|
106
|
+
await process.wait()
|
|
107
|
+
|
|
108
|
+
def action_handoff(self) -> None:
|
|
109
|
+
if not self.attached:
|
|
110
|
+
self.attached = True
|
|
111
|
+
self.run_worker(self.handoff(self.command))
|
|
112
|
+
|
|
113
|
+
def action_fail(self) -> None:
|
|
114
|
+
if not self.attached:
|
|
115
|
+
self.attached = True
|
|
116
|
+
self.run_worker(self.handoff(["/nonexistent/diffbutler-agent"]))
|
|
117
|
+
|
|
118
|
+
async def handoff(self, command: list[str]) -> None:
|
|
119
|
+
table = self.query_one(DataTable[str])
|
|
120
|
+
row = table.cursor_row
|
|
121
|
+
before = self.ticks
|
|
122
|
+
error: BaseException | None = None
|
|
123
|
+
result: int | None = None
|
|
124
|
+
self.record("handoff", row=row)
|
|
125
|
+
try:
|
|
126
|
+
with self.suspend():
|
|
127
|
+
# Textual 8.2.8 resumes *after* yield, not in a finally block.
|
|
128
|
+
# Defer all exceptions until outside the suspension context.
|
|
129
|
+
try:
|
|
130
|
+
result = await run_foreground(command)
|
|
131
|
+
except BaseException as caught:
|
|
132
|
+
error = caught
|
|
133
|
+
if isinstance(error, asyncio.CancelledError):
|
|
134
|
+
raise error
|
|
135
|
+
if error is not None and not isinstance(error, Exception):
|
|
136
|
+
raise error
|
|
137
|
+
status = f"Launch failed: {error}" if error else f"Agent exited: {result}"
|
|
138
|
+
self.query_one("#result", Static).update(
|
|
139
|
+
f"{status}. Background advanced {self.ticks - before} ticks. Press c to resume."
|
|
140
|
+
)
|
|
141
|
+
self.record(
|
|
142
|
+
"returned",
|
|
143
|
+
code=result,
|
|
144
|
+
error=str(error) if error else None,
|
|
145
|
+
row=table.cursor_row,
|
|
146
|
+
tick_delta=self.ticks - before,
|
|
147
|
+
foreground=os.tcgetpgrp(sys.stdin.fileno()) == os.getpgrp(),
|
|
148
|
+
background_bytes=(self.directory / "background.log").stat().st_size,
|
|
149
|
+
)
|
|
150
|
+
finally:
|
|
151
|
+
self.attached = False
|
|
152
|
+
self.record("released")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def main() -> None:
|
|
156
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
157
|
+
parser.add_argument("--events", type=Path, help="Write diagnostic JSONL (no agent output)")
|
|
158
|
+
parser.add_argument(
|
|
159
|
+
"command",
|
|
160
|
+
nargs=argparse.REMAINDER,
|
|
161
|
+
help="Optional foreground argv after --, e.g. codex resume SESSION_ID",
|
|
162
|
+
)
|
|
163
|
+
args = parser.parse_args()
|
|
164
|
+
if not sys.stdin.isatty() or not sys.stdout.isatty():
|
|
165
|
+
parser.error("Run in an interactive terminal; Linux and macOS are supported")
|
|
166
|
+
with tempfile.TemporaryDirectory(prefix="diffbutler-handoff-") as temporary:
|
|
167
|
+
directory = Path(temporary)
|
|
168
|
+
command = list[str](args.command)
|
|
169
|
+
if command[:1] == ["--"]:
|
|
170
|
+
command = command[1:]
|
|
171
|
+
if not command:
|
|
172
|
+
command = [
|
|
173
|
+
sys.executable,
|
|
174
|
+
"-m",
|
|
175
|
+
"diffbutler.ui.tui._fake_agent",
|
|
176
|
+
"--session",
|
|
177
|
+
str(directory / "fake-session.json"),
|
|
178
|
+
]
|
|
179
|
+
events = Path(args.events) if args.events else directory / "events.jsonl"
|
|
180
|
+
HandoffProbe(directory, command, events).run()
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
if __name__ == "__main__":
|
|
184
|
+
main()
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: diffbutler
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: A terminal dashboard for human-directed AI code reviews.
|
|
5
|
+
Keywords: code-review,github,terminal,ai
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE.txt
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
14
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
15
|
+
Classifier: Typing :: Typed
|
|
16
|
+
Requires-Dist: aiosqlite>=0.22.1
|
|
17
|
+
Requires-Dist: alembic>=1.19.1
|
|
18
|
+
Requires-Dist: httpx>=0.28.1
|
|
19
|
+
Requires-Dist: pydantic>=2.13.5
|
|
20
|
+
Requires-Dist: pydantic-settings>=2.15.0
|
|
21
|
+
Requires-Dist: rich>=15.0.0
|
|
22
|
+
Requires-Dist: sqlalchemy[asyncio]>=2.0.52
|
|
23
|
+
Requires-Dist: textual>=8.2.8
|
|
24
|
+
Requires-Dist: tomlkit>=0.15.1
|
|
25
|
+
Requires-Dist: typer>=0.27.2
|
|
26
|
+
Requires-Python: >=3.14
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# DiffButler
|
|
30
|
+
|
|
31
|
+
A terminal dashboard for human-directed AI code reviews on GitHub.
|
|
32
|
+
|
|
33
|
+
Keep track of what each pull request needs next: AI review, your review, or author
|
|
34
|
+
changes. Start an AI review, discuss its findings in the agent's own session, and
|
|
35
|
+
finish your review in the browser.
|
|
36
|
+
|
|
37
|
+

|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
Install [uv](https://docs.astral.sh/uv/getting-started/installation/) and Git.
|
|
42
|
+
From a local checkout of this repository, run:
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
uv tool install --python 3.14 .
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
uv manages Python and dependencies in an isolated environment. No virtual environment
|
|
49
|
+
activation is needed. If `diffbutler` is not on your PATH, run `uv tool update-shell`
|
|
50
|
+
and restart your shell.
|
|
51
|
+
|
|
52
|
+
Install and sign in to Codex CLI or Claude Code before requesting AI reviews.
|
|
53
|
+
The native presets support Codex CLI or Claude Code on Linux and macOS without version checks.
|
|
54
|
+
Reviews use your normal agent settings, including model and reasoning effort, with
|
|
55
|
+
background permission and output overrides. New conversations remain in the agent’s
|
|
56
|
+
own history after DiffButler cleans up its workspaces; existing private sessions
|
|
57
|
+
retain their original home for resume.
|
|
58
|
+
See [supported agents and validation](docs/native-reviews.md) for details.
|
|
59
|
+
|
|
60
|
+
## Use
|
|
61
|
+
|
|
62
|
+
Use authenticated GitHub access, including for public repositories. If you already
|
|
63
|
+
use `gh auth login`, export its token before launching DiffButler:
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
export GITHUB_TOKEN="$(gh auth token)"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Alternatively, create a [fine-grained personal access token](https://github.com/settings/personal-access-tokens)
|
|
70
|
+
for the repositories you want to review with **Pull requests: Read-only** permission,
|
|
71
|
+
and set it as `GITHUB_TOKEN`. Anonymous access is a last resort for public repositories;
|
|
72
|
+
its low API quota makes it unsuitable for regular polling. See the
|
|
73
|
+
[authentication guide](docs/github.md#authentication-options) for host/account selection,
|
|
74
|
+
HTTPS versus SSH Git setup, and token permissions.
|
|
75
|
+
|
|
76
|
+
From the GitHub repository you want to review, launch:
|
|
77
|
+
|
|
78
|
+
```sh
|
|
79
|
+
diffbutler
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
First-run setup detects the local repository, looks up your GitHub username using
|
|
83
|
+
the selected token, asks for your preferred agent, and saves your choices. If username detection
|
|
84
|
+
is unavailable, setup asks for it. Choose the recommended token access for public repositories;
|
|
85
|
+
private repositories require it. Setup asks for the token's environment variable name
|
|
86
|
+
and validates access. It never saves the token.
|
|
87
|
+
New configs silently enable both Codex and Claude when found in `PATH`; setup asks
|
|
88
|
+
only which detected agent should be the default. Missing agents get commented-out
|
|
89
|
+
entries you can enable after installation.
|
|
90
|
+
Later launches reuse your configuration.
|
|
91
|
+
|
|
92
|
+
The dashboard shows active reviews, including followed PRs whose GitHub review
|
|
93
|
+
requests have disappeared after you posted feedback. Waiting-for-author reviews
|
|
94
|
+
stay visible across refreshes and restarts. If an approved PR receives new commits,
|
|
95
|
+
it reappears as `done+` at the end of the queue without starting another AI review.
|
|
96
|
+
Authenticated polling slows when API quota is scarce and speeds back up when it
|
|
97
|
+
recovers, accounting for refresh cost and time until the quota resets.
|
|
98
|
+
|
|
99
|
+
Select a PR with the arrow keys, then work through the review:
|
|
100
|
+
|
|
101
|
+
1. Optionally press `n` to change the current configured AI reviewer for all PRs.
|
|
102
|
+
Press `r` to start a background AI review. Reviews start only when you request them.
|
|
103
|
+
The agent is instructed to read the PR description, discussion, and earlier review
|
|
104
|
+
threads and replies before reviewing the local code, reporting any unavailable context.
|
|
105
|
+
DiffButler caches full Git history and reviews changes from the PR merge base
|
|
106
|
+
to its head, excluding unrelated changes made later on the target branch.
|
|
107
|
+
2. A successful review shows `approve` only when the AI explicitly reports no findings;
|
|
108
|
+
otherwise it shows `check`. Press `Enter` to read the results, or `c` to resume
|
|
109
|
+
the saved agent conversation.
|
|
110
|
+
If the agent exits normally but DiffButler cannot parse its output, the task also
|
|
111
|
+
shows `check`; use `c` to inspect the saved conversation or `L` for captured output.
|
|
112
|
+
Changing reviewers leaves that conversation available through `c`; pressing `r`
|
|
113
|
+
starts a new run with the current reviewer and replaces the current review details.
|
|
114
|
+
Ask questions and explicitly tell the agent which comments to post, if any.
|
|
115
|
+
3. Exit the agent to return to the dashboard, which refreshes activity from GitHub.
|
|
116
|
+
Press `w` if you are waiting for author changes, or `o` to review in your browser.
|
|
117
|
+
4. After approving on GitHub, press `m` to mark the task complete locally. DiffButler
|
|
118
|
+
also detects your approval, closure, or merge and cleans up owned session resources
|
|
119
|
+
while retaining review history.
|
|
120
|
+
|
|
121
|
+
`Space` performs the selected PR's next action: start a review, discuss findings,
|
|
122
|
+
or open the browser. An AI review never counts as your approval. Background analysis
|
|
123
|
+
uses the agent's [native permission controls](docs/native-reviews.md#native-permissions-and-authentication)
|
|
124
|
+
and read-only review instructions; source sent to the agent may go to its model provider.
|
|
125
|
+
|
|
126
|
+
- `/`: Filter the queue.
|
|
127
|
+
- `a`: Toggle monitoring all open PRs in configured repositories.
|
|
128
|
+
- `b`: Browse accessible PRs and follow one.
|
|
129
|
+
- `R`: Refresh from GitHub.
|
|
130
|
+
- `k`: Stop the selected PR's review and reset its local tracking and owned resources.
|
|
131
|
+
It disappears until a forge refresh rediscovers it; review history is retained.
|
|
132
|
+
- `h`: View completed review history.
|
|
133
|
+
- `?`: Show all keyboard shortcuts.
|
|
134
|
+
- `q`: Quit.
|
|
135
|
+
|
|
136
|
+
See the [dashboard guide](docs/dashboard.md) for all actions and
|
|
137
|
+
[configuration guide](docs/configuration.md) for settings.
|
|
138
|
+
|
|
139
|
+
Your own PRs are hidden from the review queue by default, including direct and team
|
|
140
|
+
requests. Set `include_own_prs = true` in `[ui]` to review them, such as PRs an agent
|
|
141
|
+
created using your account. PRs assigned to you are also hidden by default, even when
|
|
142
|
+
you are requested directly or through a team. Set `include_assigned_prs = true` in
|
|
143
|
+
`[ui]` to include them.
|
|
144
|
+
|
|
145
|
+
## Reset
|
|
146
|
+
|
|
147
|
+
Close running DiffButler sessions, then run:
|
|
148
|
+
|
|
149
|
+
```sh
|
|
150
|
+
diffbutler reset
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
DiffButler refuses a second command or dashboard using the same data home, including
|
|
154
|
+
`reset`, while another instance is running. Its instance lock releases on process
|
|
155
|
+
exit; a leftover PID file does not block startup. See the
|
|
156
|
+
[locking details](docs/configuration.md#single-instance) before handling Git lock files
|
|
157
|
+
or resetting after a crash.
|
|
158
|
+
|
|
159
|
+
After confirmation, this permanently removes DiffButler's standard configuration,
|
|
160
|
+
data, and state directories, including review history, saved sessions, and diagnostic
|
|
161
|
+
logs. Custom files outside those directories are preserved. Use `diffbutler reset --force`
|
|
162
|
+
to skip confirmation. Run `diffbutler` again to repeat first-run setup.
|
|
163
|
+
|
|
164
|
+
## Contributing
|
|
165
|
+
|
|
166
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and checks.
|
|
167
|
+
|
|
168
|
+
## License
|
|
169
|
+
|
|
170
|
+
[MIT](LICENSE.txt).
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
diffbutler/__init__.py,sha256=NJV3PMyDUxERtwy3enDYnGmANp5J8w9oMQTFIGf8Ic4,50
|
|
2
|
+
diffbutler/__main__.py,sha256=Lso6m1-RzLUewvu8mw-fmB1XwRtbptBVdjYvIKoZegQ,115
|
|
3
|
+
diffbutler/config/__init__.py,sha256=AQ93W6L1AQalhhlxjMTuq0VIE6DY_oGtcW6SCD7QEQ8,44
|
|
4
|
+
diffbutler/config/default.toml,sha256=o92lw3YwcdGwrheP38uv6mILvbnBKiO1QCJIR5VsiMs,3165
|
|
5
|
+
diffbutler/config/diagnostics.py,sha256=jDqHJzfcuEd9c40Oqkb5LnGHpx5BQuMbqezmNlz_5Ww,2994
|
|
6
|
+
diffbutler/config/file.py,sha256=joa-7cIolowonyFxcRP1ioeCYrQ7vC7w5S06OHMiJkI,1176
|
|
7
|
+
diffbutler/config/instance.py,sha256=jJoMv06EGbOsO5llgWidzsp37-wbRu4F9rDOio_-pk4,2053
|
|
8
|
+
diffbutler/config/settings.py,sha256=0InAIv0QiqPVXmXTTwn8Pno6M-6RJdak79qK9UegRuA,10759
|
|
9
|
+
diffbutler/config/setup.py,sha256=FZIMtGOWaGqmohjAwnolQFLltMbd5XbPAjcCzDwP-kY,4163
|
|
10
|
+
diffbutler/config/transport.py,sha256=4FqFid7IzITNJ0Z5oq0kQGQMLt2PnviDuo9GpJdw_10,2846
|
|
11
|
+
diffbutler/core/__init__.py,sha256=pXkfhbiHggzoVmZW6eGwvK_6JEJCCB7aTgXvNUi3sK0,43
|
|
12
|
+
diffbutler/core/application.py,sha256=QQ8uBQLWMUYOnBfqo8ENZ0faBxzMtA5XkukpGqz4ZOk,3682
|
|
13
|
+
diffbutler/core/context.py,sha256=IPQofKZafYU0riM5l4rqD7MX2V-NZuxu_Cl8melYMPI,2415
|
|
14
|
+
diffbutler/core/dashboard.py,sha256=E0swv9ms_DJCFupp_Muod1VNuZayEc_FtmlqsOSMRjQ,16664
|
|
15
|
+
diffbutler/core/doctor.py,sha256=KT_svOkNlOYZ7HSqHFLSSuN3qpQx2Ykww22JPUvPD9M,7473
|
|
16
|
+
diffbutler/core/lifecycle.py,sha256=vjdWguWn9YwGoef8oXAkEV9a4QoH4xIQl_u0JoPujsE,2862
|
|
17
|
+
diffbutler/core/managed.py,sha256=3rtMAvNLsGcQaLAKjYyp8UEqW0oPHGxuW-4zglsRX7g,18966
|
|
18
|
+
diffbutler/core/native.py,sha256=3MNBsAEahdxE1zOIm_TY5C8Y5oTP7Mad7kgcouRqgDg,12533
|
|
19
|
+
diffbutler/core/polling.py,sha256=icvNRa4lu41H_rx7d7h4noAwXEq71fO5neh4EDTzEY4,4755
|
|
20
|
+
diffbutler/core/process.py,sha256=MPPVlIdd_tpKidIkXZtwvU5CyVgvS2KChs6j3NZqoos,2439
|
|
21
|
+
diffbutler/core/repositories.py,sha256=L4d73z6gGUN7G9azEcyUD-9XeN4UF6MteO1Zgo78NJE,11398
|
|
22
|
+
diffbutler/core/reset.py,sha256=qRVp2wkGFQK_ir1cwYQqbD8jixMB-80PWOUMNMeyX-I,1043
|
|
23
|
+
diffbutler/core/resources.py,sha256=EuAn0eGc-EYlOfebVuuzrWccZ8HmVpAbPzol7AfO-Pw,1661
|
|
24
|
+
diffbutler/core/review.py,sha256=YnCZ9nI6a-kr0Poi0_EUFhpywSBQ-5-aTye3CwsXGsc,11747
|
|
25
|
+
diffbutler/core/setup.py,sha256=FDBjWp_Tq036FRCUiJUxlzfQSgFBYhq1TsqsYz-xCiI,9312
|
|
26
|
+
diffbutler/core/snapshot.py,sha256=5GAffiprN3a3Ogk9Kxhp37LImngWc72UYxs8d_laju8,2505
|
|
27
|
+
diffbutler/core/sync.py,sha256=C8f8pjvDeGRFBUzeX17IchnJ85ksy72GcNUo_E-moKs,11496
|
|
28
|
+
diffbutler/core/worker.py,sha256=7CFULNLTnW1NZMq-K13BbbPMQEmkpqjwK1Zl-7-qNGU,8672
|
|
29
|
+
diffbutler/core/workspaces.py,sha256=LBcRiE38AJ4PD2xem90yySum1QU7BgyyryBhB28yrMw,5620
|
|
30
|
+
diffbutler/domain/__init__.py,sha256=oQI2BkzgjfgYC-74YMgfWk7wWIHvfqQsNuUy90ZmgSY,47
|
|
31
|
+
diffbutler/domain/dashboard.py,sha256=3_edpG1olIb91EOoS1S4Cq-EJUu_xL21LUxaQMwbth0,1392
|
|
32
|
+
diffbutler/domain/diff.py,sha256=E1QW82RYm0QjZTgeXLueJfFYfsVv2Xto_BtKCVlrpUQ,10104
|
|
33
|
+
diffbutler/domain/forge.py,sha256=D1SmLsQTChK5RQkSxaDZ9JfXqjcfg6t6lqnrLaMxH6E,2987
|
|
34
|
+
diffbutler/domain/lifecycle.py,sha256=Gcv-Vg-nLLRQ_KpOtbiA01LWaCCVKfLQgDUByHlEQt0,1053
|
|
35
|
+
diffbutler/domain/polling.py,sha256=qFL5x6wWhynVp1SQd0ttQ7nYzznhWvF4RmofdT8LX8U,3561
|
|
36
|
+
diffbutler/domain/process.py,sha256=1B8E75W-GFZdqGI-04EDsOTtTZUc50nM5fYWt5pjdLY,851
|
|
37
|
+
diffbutler/domain/redaction.py,sha256=SRN3PZ1uP-eSUCROB4x0O7w8gUINlqZmQ2jyeaHx0B8,2191
|
|
38
|
+
diffbutler/domain/resources.py,sha256=8Fplm5PMI6qHG1sPyZfT2vmpEc5_9BrRqlDKY0SlxDA,1713
|
|
39
|
+
diffbutler/domain/review.py,sha256=0kgZEUXY7teOnJDVy8Bku18sLNfGEXRY5QuZBGpAjwc,3976
|
|
40
|
+
diffbutler/domain/sync.py,sha256=4noDpNVYJNVpfDmya7GSpk6fm9qhgdgwnnIEJM_Ika0,6821
|
|
41
|
+
diffbutler/forges/__init__.py,sha256=IvwheQN2h9ZJP9k3zkdTDjNzGKh4jtq4yCE4yJf-bCo,48
|
|
42
|
+
diffbutler/forges/base.py,sha256=MyfxRymVIgL9Mud_ilFz1wTnDaXwEjXF12SAGQr-jus,1253
|
|
43
|
+
diffbutler/forges/diagnostics.py,sha256=OBoqo471wql9MgBtmlmdftUOgD3S1UdqNUvsFZMBLt0,3372
|
|
44
|
+
diffbutler/forges/github/__init__.py,sha256=8MHrBneaDew5CAsPWdCYAnbw_sZKnxzrYfLRfxHt_ww,60
|
|
45
|
+
diffbutler/forges/github/adapter.py,sha256=KTw3-BJLUIiJF2Ero3SrNihMuuRr9SyCi6t7OBBKirU,16143
|
|
46
|
+
diffbutler/forges/github/auth.py,sha256=7VU-xuvovtIH6JTnUR1kPtjjsfDlzJeDlXbt03EaQdw,1623
|
|
47
|
+
diffbutler/forges/github/http.py,sha256=o5BH-43llnNa7IbXDt12DSSGoM6NYZlSZiluV31bLE0,13799
|
|
48
|
+
diffbutler/forges/github/mapping.py,sha256=QmXkiuk48XQOrP48sb1Z2BAotMUPBY1tRXjBEcJ-cuM,4485
|
|
49
|
+
diffbutler/forges/github/setup.py,sha256=FTKbQr0bEh2g7oSGAf5GYSkBRT8jIsWmxR0VO6T9KEU,3159
|
|
50
|
+
diffbutler/forges/registry.py,sha256=6zOoUSQxKOkowXLYk6a9OhQqI4ICPrbOa-LYIs0o58A,884
|
|
51
|
+
diffbutler/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
52
|
+
diffbutler/reviewers/__init__.py,sha256=mFpOuwr_X82vIkMc4doTdxbxGs1gpKqQo8tmzMaYqbc,53
|
|
53
|
+
diffbutler/reviewers/base.py,sha256=EClCQV46bEuA0k7uPuhYpubMsGAWbTY3YDKmopQ35JU,2427
|
|
54
|
+
diffbutler/reviewers/extraction.py,sha256=xFudnP5r0Rv6Ximx1qGODl76akMw8b_ARDVi-FCOQFs,8071
|
|
55
|
+
diffbutler/reviewers/native.py,sha256=oVi9ophFaWcKi47BQvLHVvtM3KWkLTzU37cIGpqam0w,8630
|
|
56
|
+
diffbutler/reviewers/parsing.py,sha256=t58lbzs_cOUYFA4300lcW9r4rpBO9uPCNs64AZWImcU,8365
|
|
57
|
+
diffbutler/reviewers/presets.toml,sha256=Y-ce43kXzwgv_kKNaQe5mRp6V6CNyvHaRwEvkASnafM,1068
|
|
58
|
+
diffbutler/reviewers/process.py,sha256=lodapD4SLEYTh4IWYdNoGSmTFrqsZbMLRAZ9acxxz-U,2863
|
|
59
|
+
diffbutler/reviewers/prompt.py,sha256=Ok9Zp92_6acRcAQh7b1Tg-BV-CtMbHKD3DtFXKEPbAw,8351
|
|
60
|
+
diffbutler/reviewers/redaction.py,sha256=yfKSRQln0BUh-QuIcAqZ9FC_Bez4HZ7bJwG7jMywalI,952
|
|
61
|
+
diffbutler/reviewers/resources.py,sha256=5BAlWsqDeCXtTvudGudcMreDKqUoiPrbKt-KauvgyEk,6172
|
|
62
|
+
diffbutler/sources/__init__.py,sha256=0Pr4roOZeA53KsmarQnF1QQhPi2ryO7UuUt3vOP9s9I,82
|
|
63
|
+
diffbutler/sources/diagnostics.py,sha256=TOPbHlA8QKFuO8Zja6gkjUUd29OHTgLz6eK0WebvR0I,3975
|
|
64
|
+
diffbutler/sources/fetch.py,sha256=i3Ti0HPG33AlSzCZVEOZ4RdHTDrJh0wYTo8JZCVsqAA,10778
|
|
65
|
+
diffbutler/sources/git.py,sha256=qngDKIAPLh0wE2W1T9Zwysa-It3KW6_C5EOHtQaY6AI,18319
|
|
66
|
+
diffbutler/sources/remotes.py,sha256=H-h5Khxtl2kNol7OTR97xondyOKBTAKGFUeX-3DQNVM,2732
|
|
67
|
+
diffbutler/store/__init__.py,sha256=_LLUgyn-27dwXVdC0XD1yVfXS79AQMi6JXfofEJ3Ws4,41
|
|
68
|
+
diffbutler/store/base.py,sha256=a5A22SnsoYoYE5YL6BtDdE3ymd1JQC2ctAHt1KAgv7g,3355
|
|
69
|
+
diffbutler/store/dashboard.py,sha256=hxQBLmcgB_Xk8oS-RqvrzRDH-WDxTOXk73VQ9-41SY0,8752
|
|
70
|
+
diffbutler/store/database.py,sha256=AfidYgNNtyDyH-aidwWCdSDAMwasTMqpCviNwDTdYuQ,7334
|
|
71
|
+
diffbutler/store/lifecycle.py,sha256=KbwmIl1HxglZ4T97MOU2htBGaCuR5Xu3_9lJXyhVo_0,10255
|
|
72
|
+
diffbutler/store/migrations/__init__.py,sha256=MGLvwDONznC8aU97PWjZh3Bg_4zzPGtBiLMejK9lrmI,35
|
|
73
|
+
diffbutler/store/migrations/env.py,sha256=rMO6koeK3KoRQzfECIT5cQOM03ocvB8exJD-_ib1-Wk,379
|
|
74
|
+
diffbutler/store/migrations/versions/0001_initial.py,sha256=A_g6lPYp_avZFfO0JDCpSCaLL_sKaYqsQWTTpckbGtI,6856
|
|
75
|
+
diffbutler/store/migrations/versions/0002_monitoring.py,sha256=tRFc38qif6GVvvMRL5x4FeCsgOjdeK1Ct6cHKqdj9iE,1515
|
|
76
|
+
diffbutler/store/migrations/versions/0003_reviews.py,sha256=Ac3HWhuwaQiLqlygTGMjZ3S4xsr72OmI6izhzNJt2qo,3422
|
|
77
|
+
diffbutler/store/migrations/versions/0004_claims.py,sha256=mvKz4u7DrZ1teDvmtJutHK7Zbg6jvyKOAn5II32bNOs,515
|
|
78
|
+
diffbutler/store/migrations/versions/0005_results.py,sha256=fNV6OrGOORiJgEs8wC5vXEHL_WPGQ2oRtpTN8_9ddGM,875
|
|
79
|
+
diffbutler/store/migrations/versions/0006_anchors.py,sha256=i5YAbhdOd0d-1ugW08p-hp8B2TkSLvH9htz7Sx3jWyY,397
|
|
80
|
+
diffbutler/store/migrations/versions/0007_lifecycle.py,sha256=RduOH5hmHbC79yjQiI07AhV9xA6fliBW9ycN1cNnOcI,1153
|
|
81
|
+
diffbutler/store/migrations/versions/0008_processes.py,sha256=8MraDiTcdO480DNuDy62u2EMIgiCujw5fPN7LPBBa3U,880
|
|
82
|
+
diffbutler/store/migrations/versions/0009_dashboard.py,sha256=JUP7e1HUYCXaR8CttX7HjJ6cfaWDNuaazFVepGiM96w,1598
|
|
83
|
+
diffbutler/store/migrations/versions/0010_run_reasons.py,sha256=59T-43cgQ4Qzt_BWaJ-miXy7yv8st8TUhNO6LwWaotA,390
|
|
84
|
+
diffbutler/store/migrations/versions/__init__.py,sha256=rnLPy5gtzmh4IDg6W2EEHCZfoB6Eq8H1E-ipmCDRes8,37
|
|
85
|
+
diffbutler/store/models.py,sha256=iwfo9__htCBItOkc7SmY1mnsgvVL7i0C7j2xqt50Unk,13800
|
|
86
|
+
diffbutler/store/process.py,sha256=B4aXrdYNfuO_ecWkE56QcPrYc41vlQOK8XbWygjQv0Q,3143
|
|
87
|
+
diffbutler/store/queue.py,sha256=26AYdDmSsQwPQjT8leubTVOTjujIqqtRcXGy4NSZMDg,11673
|
|
88
|
+
diffbutler/store/review.py,sha256=YUCNRJHu1ocms9GOMO9br8QvRNpL3pV74Po8krEl6hk,25418
|
|
89
|
+
diffbutler/ui/__init__.py,sha256=JvD0okFiDU9fBbe_SO9nCq3gccohozMW2BxMKfYC-kw,38
|
|
90
|
+
diffbutler/ui/cli/__init__.py,sha256=impAB7LSOUy03inlHkj0LGG7eLJfN294yrfDZyslNiM,18628
|
|
91
|
+
diffbutler/ui/cli/editor.py,sha256=-T1wAwKLxv9a4rQn48KKNDNlAnmrahKaxHqipuJHeuQ,1116
|
|
92
|
+
diffbutler/ui/tui/__init__.py,sha256=k_PE3KbrP21uGbPZeO8eNte584DDkm7qfAOQCSHpUVE,58
|
|
93
|
+
diffbutler/ui/tui/_fake_agent.py,sha256=nutsOsYn5-_vgR5hdvn9K0DnpmWSEdwM9SMe9VtxV-s,1608
|
|
94
|
+
diffbutler/ui/tui/_foreground_exec.py,sha256=5rVMvG_51LO0iuR-uahPFJm24b_DFsHlnfEbuJmzxZ4,529
|
|
95
|
+
diffbutler/ui/tui/app.py,sha256=wznsCbrlZ44vzVBFePLOOAwdBHt-KwSQEOkG_RbNoDM,1375
|
|
96
|
+
diffbutler/ui/tui/browse.py,sha256=OKVNvjAmraycy8vfht8fZ92j5vT-l53pznOrA0oCIQk,4461
|
|
97
|
+
diffbutler/ui/tui/browser.py,sha256=KepB7qwZ-4Y_Mpo9JACiiydFVmO7HXMZ37a5N7e02BA,1279
|
|
98
|
+
diffbutler/ui/tui/dashboard.py,sha256=7zXZF12chxZCzhvXWHtU58EdJFTuWsexSisLPDdxpgg,38678
|
|
99
|
+
diffbutler/ui/tui/foreground.py,sha256=_j2CBkEzO1F8c8E64ajnvZysuOC4-9qxHXLt4gH1ioU,3505
|
|
100
|
+
diffbutler/ui/tui/handoff_probe.py,sha256=Rwbqq_rACFCSSV7o_azRN1H9kONRwQvn_6b51QjJU1U,6695
|
|
101
|
+
diffbutler-0.5.0.dist-info/licenses/LICENSE.txt,sha256=MpRzvP0oe5P1X7beCHWY3zbVmlBo-oppnt6NHDmnkTw,1080
|
|
102
|
+
diffbutler-0.5.0.dist-info/WHEEL,sha256=-i9oRNYVXXZJUIYl5zclLIg6onEb0NLibTX34uln84w,81
|
|
103
|
+
diffbutler-0.5.0.dist-info/entry_points.txt,sha256=dDGuGmpGj-YL3lvHsGgaPMRqtbe8G58f5drFwRXlIvQ,55
|
|
104
|
+
diffbutler-0.5.0.dist-info/METADATA,sha256=244pUzpTeaAB9hEjFtV-hNmXZj8dL5zn5BZwjSyBYpI,7918
|
|
105
|
+
diffbutler-0.5.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DiffButler contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|