phab-feedback 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Logan Rosen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,184 @@
1
+ Metadata-Version: 2.4
2
+ Name: phab-feedback
3
+ Version: 0.1.0
4
+ Summary: CLI for Phabricator and Phorge review feedback workflows
5
+ Author: Logan Rosen
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/loganrosen/phab-feedback
8
+ Project-URL: Repository, https://github.com/loganrosen/phab-feedback
9
+ Project-URL: Issues, https://github.com/loganrosen/phab-feedback/issues
10
+ Keywords: phabricator,phorge,code-review,cli
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: MacOS
15
+ Classifier: Operating System :: POSIX :: Linux
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Topic :: Software Development :: Version Control
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.10
26
+ Description-Content-Type: text/markdown
27
+ License-File: LICENSE
28
+ Dynamic: license-file
29
+
30
+ # phab-feedback
31
+
32
+ `phab-feedback` is a small command-line client for reviewing and acting on
33
+ Phabricator and Phorge feedback. It keeps inline replies as real inline-thread
34
+ replies, exposes draft actions explicitly, and produces structured JSON suitable
35
+ for both people and automation.
36
+
37
+ ## How this differs
38
+
39
+ `arc` and `moz-phab` handle author-side Differential workflows such as creating
40
+ or updating revisions from local commits, with `arc` also providing landing
41
+ workflows. `phabfive` provides broader Conduit-based access to Phabricator and
42
+ Phorge applications such as Maniphest, Paste, Diffusion, Passphrase, and User.
43
+
44
+ `phab-feedback` complements those tools by focusing on structured Differential
45
+ feedback, inline threads, explicit draft actions, and browser-only mutations
46
+ that Conduit does not expose. Its optional Mozilla Review Helper rating and
47
+ AI-review commands remain isolated from the generic Phabricator and Phorge
48
+ behavior.
49
+
50
+ ## Install
51
+
52
+ Python 3.10 or newer is required.
53
+
54
+ After the first PyPI release is published, run the CLI without installing it:
55
+
56
+ ```bash
57
+ uvx phab-feedback --help
58
+ ```
59
+
60
+ For a persistent installation, `uv` is recommended:
61
+
62
+ ```bash
63
+ uv tool install phab-feedback
64
+ phab-feedback --help
65
+ ```
66
+
67
+ `pipx` is an alternative:
68
+
69
+ ```bash
70
+ pipx install phab-feedback
71
+ phab-feedback --help
72
+ ```
73
+
74
+ To try unreleased development from GitHub:
75
+
76
+ ```bash
77
+ uv tool install git+https://github.com/loganrosen/phab-feedback.git
78
+ ```
79
+
80
+ For local source development:
81
+
82
+ ```bash
83
+ git clone https://github.com/loganrosen/phab-feedback.git
84
+ cd phab-feedback
85
+ python3 -m pip install -e .
86
+ python3 -m unittest discover -s tests
87
+ ```
88
+
89
+ ## Configuration
90
+
91
+ Choose a host with `--host`, `PHAB_FEEDBACK_HOST`, or
92
+ `~/.config/phab-feedback/config.json`, in that order:
93
+
94
+ ```json
95
+ {
96
+ "host": "https://phabricator.example.com",
97
+ "cookie_name": "phsid"
98
+ }
99
+ ```
100
+
101
+ If no host is configured and `~/.arcrc` contains exactly one host,
102
+ `phab-feedback` uses it. Conduit tokens come from `PHAB_FEEDBACK_TOKEN` or the
103
+ matching `~/.arcrc` entry. Tokens are never accepted as command-line arguments.
104
+
105
+ Internal web actions need a logged-in browser session. Set
106
+ `PHAB_FEEDBACK_SESSION_COOKIE` to a complete `Cookie` header value, or to the
107
+ value of the configured session cookie. The value is never printed. Mozilla
108
+ Phabricator users can instead pass `--firefox-cookies` to discover the session
109
+ from a local Firefox profile; `--firefox-profile` selects a specific profile.
110
+
111
+ `XDG_CONFIG_HOME` and `PHAB_FEEDBACK_ARCRC` are respected. The config file is
112
+ for non-secret settings; keep tokens in `.arcrc` or the environment and session
113
+ cookies in the environment or browser store.
114
+
115
+ ## Commands
116
+
117
+ All successful commands write JSON to stdout. Message-taking commands accept
118
+ exactly one of `--message`, `--message-file PATH`, or `--message-file -`.
119
+ When stdin is redirected, it is also accepted without an option.
120
+
121
+ ```bash
122
+ # Read the complete chronological feedback timeline.
123
+ phab-feedback timeline D123
124
+
125
+ # Post an immediate top-level revision comment through Conduit.
126
+ phab-feedback comment D123 --message-file reply.txt
127
+
128
+ # Create a true inline-thread reply draft, then publish it separately.
129
+ printf '%s\n' 'Handled in the latest update.' |
130
+ phab-feedback reply-inline D123 456
131
+ phab-feedback submit D123
132
+
133
+ # Explicitly create and publish an inline reply in one invocation.
134
+ phab-feedback reply-inline D123 456 --message 'Done.' --submit
135
+
136
+ # Remove an accidental top-level comment after validating its type.
137
+ phab-feedback remove-comment D123 789
138
+
139
+ # Create Done drafts, then submit them.
140
+ phab-feedback mark-done D123 456 457
141
+ phab-feedback submit D123
142
+ ```
143
+
144
+ `timeline`, `comment`, and the metadata validation used by mutations rely on
145
+ standard Conduit APIs. Inline reply drafting, top-level comment removal, Done
146
+ drafting, and draft submission use internal web endpoints available in upstream
147
+ Phabricator and Phorge. Those endpoints are less stable than Conduit and may
148
+ change between server releases.
149
+
150
+ These commands are **Mozilla-only** because they use the Review Helper
151
+ extension, not upstream Phabricator:
152
+
153
+ ```bash
154
+ phab-feedback mark-helpful D123 456
155
+ phab-feedback mark-unhelpful D123 457
156
+ phab-feedback request-ai-review D123
157
+ ```
158
+
159
+ Helpful and unhelpful ratings take effect immediately. Inline replies and Done
160
+ states remain drafts until `submit`; `reply-inline --submit` is the only
161
+ intentional combined workflow. Rating, Done, and reply actions are never
162
+ combined implicitly.
163
+
164
+ The repository also includes an optional thin agent skill. It contains workflow
165
+ and approval guidance only; the CLI remains the single implementation of all
166
+ deterministic behavior. Install it through the open Skills CLI:
167
+
168
+ ```bash
169
+ npx skills add loganrosen/phab-feedback@phab-feedback -g
170
+ ```
171
+
172
+ The Skills CLI handles the supported agent-specific installation paths. The
173
+ Python package does not modify agent configuration or install the skill
174
+ automatically.
175
+
176
+ ## Security
177
+
178
+ Credentials are sent only in request headers or bodies to the configured host.
179
+ Errors omit request bodies, tokens, and cookies. Avoid enabling shell tracing
180
+ while setting credential environment variables.
181
+
182
+ ## License
183
+
184
+ MIT
@@ -0,0 +1,155 @@
1
+ # phab-feedback
2
+
3
+ `phab-feedback` is a small command-line client for reviewing and acting on
4
+ Phabricator and Phorge feedback. It keeps inline replies as real inline-thread
5
+ replies, exposes draft actions explicitly, and produces structured JSON suitable
6
+ for both people and automation.
7
+
8
+ ## How this differs
9
+
10
+ `arc` and `moz-phab` handle author-side Differential workflows such as creating
11
+ or updating revisions from local commits, with `arc` also providing landing
12
+ workflows. `phabfive` provides broader Conduit-based access to Phabricator and
13
+ Phorge applications such as Maniphest, Paste, Diffusion, Passphrase, and User.
14
+
15
+ `phab-feedback` complements those tools by focusing on structured Differential
16
+ feedback, inline threads, explicit draft actions, and browser-only mutations
17
+ that Conduit does not expose. Its optional Mozilla Review Helper rating and
18
+ AI-review commands remain isolated from the generic Phabricator and Phorge
19
+ behavior.
20
+
21
+ ## Install
22
+
23
+ Python 3.10 or newer is required.
24
+
25
+ After the first PyPI release is published, run the CLI without installing it:
26
+
27
+ ```bash
28
+ uvx phab-feedback --help
29
+ ```
30
+
31
+ For a persistent installation, `uv` is recommended:
32
+
33
+ ```bash
34
+ uv tool install phab-feedback
35
+ phab-feedback --help
36
+ ```
37
+
38
+ `pipx` is an alternative:
39
+
40
+ ```bash
41
+ pipx install phab-feedback
42
+ phab-feedback --help
43
+ ```
44
+
45
+ To try unreleased development from GitHub:
46
+
47
+ ```bash
48
+ uv tool install git+https://github.com/loganrosen/phab-feedback.git
49
+ ```
50
+
51
+ For local source development:
52
+
53
+ ```bash
54
+ git clone https://github.com/loganrosen/phab-feedback.git
55
+ cd phab-feedback
56
+ python3 -m pip install -e .
57
+ python3 -m unittest discover -s tests
58
+ ```
59
+
60
+ ## Configuration
61
+
62
+ Choose a host with `--host`, `PHAB_FEEDBACK_HOST`, or
63
+ `~/.config/phab-feedback/config.json`, in that order:
64
+
65
+ ```json
66
+ {
67
+ "host": "https://phabricator.example.com",
68
+ "cookie_name": "phsid"
69
+ }
70
+ ```
71
+
72
+ If no host is configured and `~/.arcrc` contains exactly one host,
73
+ `phab-feedback` uses it. Conduit tokens come from `PHAB_FEEDBACK_TOKEN` or the
74
+ matching `~/.arcrc` entry. Tokens are never accepted as command-line arguments.
75
+
76
+ Internal web actions need a logged-in browser session. Set
77
+ `PHAB_FEEDBACK_SESSION_COOKIE` to a complete `Cookie` header value, or to the
78
+ value of the configured session cookie. The value is never printed. Mozilla
79
+ Phabricator users can instead pass `--firefox-cookies` to discover the session
80
+ from a local Firefox profile; `--firefox-profile` selects a specific profile.
81
+
82
+ `XDG_CONFIG_HOME` and `PHAB_FEEDBACK_ARCRC` are respected. The config file is
83
+ for non-secret settings; keep tokens in `.arcrc` or the environment and session
84
+ cookies in the environment or browser store.
85
+
86
+ ## Commands
87
+
88
+ All successful commands write JSON to stdout. Message-taking commands accept
89
+ exactly one of `--message`, `--message-file PATH`, or `--message-file -`.
90
+ When stdin is redirected, it is also accepted without an option.
91
+
92
+ ```bash
93
+ # Read the complete chronological feedback timeline.
94
+ phab-feedback timeline D123
95
+
96
+ # Post an immediate top-level revision comment through Conduit.
97
+ phab-feedback comment D123 --message-file reply.txt
98
+
99
+ # Create a true inline-thread reply draft, then publish it separately.
100
+ printf '%s\n' 'Handled in the latest update.' |
101
+ phab-feedback reply-inline D123 456
102
+ phab-feedback submit D123
103
+
104
+ # Explicitly create and publish an inline reply in one invocation.
105
+ phab-feedback reply-inline D123 456 --message 'Done.' --submit
106
+
107
+ # Remove an accidental top-level comment after validating its type.
108
+ phab-feedback remove-comment D123 789
109
+
110
+ # Create Done drafts, then submit them.
111
+ phab-feedback mark-done D123 456 457
112
+ phab-feedback submit D123
113
+ ```
114
+
115
+ `timeline`, `comment`, and the metadata validation used by mutations rely on
116
+ standard Conduit APIs. Inline reply drafting, top-level comment removal, Done
117
+ drafting, and draft submission use internal web endpoints available in upstream
118
+ Phabricator and Phorge. Those endpoints are less stable than Conduit and may
119
+ change between server releases.
120
+
121
+ These commands are **Mozilla-only** because they use the Review Helper
122
+ extension, not upstream Phabricator:
123
+
124
+ ```bash
125
+ phab-feedback mark-helpful D123 456
126
+ phab-feedback mark-unhelpful D123 457
127
+ phab-feedback request-ai-review D123
128
+ ```
129
+
130
+ Helpful and unhelpful ratings take effect immediately. Inline replies and Done
131
+ states remain drafts until `submit`; `reply-inline --submit` is the only
132
+ intentional combined workflow. Rating, Done, and reply actions are never
133
+ combined implicitly.
134
+
135
+ The repository also includes an optional thin agent skill. It contains workflow
136
+ and approval guidance only; the CLI remains the single implementation of all
137
+ deterministic behavior. Install it through the open Skills CLI:
138
+
139
+ ```bash
140
+ npx skills add loganrosen/phab-feedback@phab-feedback -g
141
+ ```
142
+
143
+ The Skills CLI handles the supported agent-specific installation paths. The
144
+ Python package does not modify agent configuration or install the skill
145
+ automatically.
146
+
147
+ ## Security
148
+
149
+ Credentials are sent only in request headers or bodies to the configured host.
150
+ Errors omit request bodies, tokens, and cookies. Avoid enabling shell tracing
151
+ while setting credential environment variables.
152
+
153
+ ## License
154
+
155
+ MIT
@@ -0,0 +1,41 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "phab-feedback"
7
+ version = "0.1.0"
8
+ description = "CLI for Phabricator and Phorge review feedback workflows"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{name = "Logan Rosen"}]
14
+ keywords = ["phabricator", "phorge", "code-review", "cli"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Environment :: Console",
18
+ "Intended Audience :: Developers",
19
+ "Operating System :: MacOS",
20
+ "Operating System :: POSIX :: Linux",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3 :: Only",
23
+ "Programming Language :: Python :: 3.10",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Programming Language :: Python :: 3.13",
27
+ "Programming Language :: Python :: 3.14",
28
+ "Topic :: Software Development :: Version Control",
29
+ "Typing :: Typed",
30
+ ]
31
+
32
+ [project.scripts]
33
+ phab-feedback = "phab_feedback.cli:main"
34
+
35
+ [project.urls]
36
+ Homepage = "https://github.com/loganrosen/phab-feedback"
37
+ Repository = "https://github.com/loganrosen/phab-feedback"
38
+ Issues = "https://github.com/loganrosen/phab-feedback/issues"
39
+
40
+ [tool.setuptools.packages.find]
41
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """Phabricator and Phorge feedback workflows."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
@@ -0,0 +1,107 @@
1
+ """Conduit and internal web API clients."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import html
6
+ import json
7
+ import re
8
+ from typing import Any, Mapping
9
+ from urllib.parse import urlencode
10
+
11
+ from .errors import APIError
12
+ from .transport import Transport
13
+
14
+
15
+ class ConduitClient:
16
+ def __init__(self, host: str, token: str, transport: Transport) -> None:
17
+ self.host = host
18
+ self._token = token
19
+ self._transport = transport
20
+
21
+ def call(self, method: str, params: Mapping[str, Any]) -> Any:
22
+ conduit_params = dict(params)
23
+ conduit_params["__conduit__"] = {"token": self._token}
24
+ data = urlencode(
25
+ {
26
+ "params": json.dumps(conduit_params, separators=(",", ":")),
27
+ "output": "json",
28
+ "__conduit__": "1",
29
+ }
30
+ ).encode()
31
+ response = self._transport.request(
32
+ "POST",
33
+ f"{self.host}/api/{method}",
34
+ headers={"Content-Type": "application/x-www-form-urlencoded"},
35
+ data=data,
36
+ )
37
+ payload = _json_object(response.body, f"Conduit method {method}")
38
+ if payload.get("error_code"):
39
+ code = payload["error_code"]
40
+ info = str(payload.get("error_info") or "request rejected")
41
+ info = info.replace(self._token, "[redacted]")
42
+ raise APIError(f"Conduit {method} failed: {code}: {info}")
43
+ if "result" not in payload:
44
+ raise APIError(f"Conduit {method} returned no result")
45
+ return payload["result"]
46
+
47
+
48
+ class WebClient:
49
+ def __init__(self, host: str, cookie_header: str, transport: Transport) -> None:
50
+ self.host = host
51
+ self._cookie_header = cookie_header
52
+ self._transport = transport
53
+ self._csrf: str | None = None
54
+
55
+ @property
56
+ def csrf(self) -> str:
57
+ if self._csrf is None:
58
+ response = self._transport.request(
59
+ "GET",
60
+ self.host,
61
+ headers={"Cookie": self._cookie_header},
62
+ )
63
+ decoded = html.unescape(
64
+ response.body.decode("utf-8", errors="replace")
65
+ )
66
+ patterns = (
67
+ r'name="__csrf__"\s+value="(B@[A-Za-z0-9]+)"',
68
+ r'"current":"(B@[A-Za-z0-9]+)"',
69
+ r'"token":"(B@[A-Za-z0-9]+)"',
70
+ )
71
+ for pattern in patterns:
72
+ match = re.search(pattern, decoded)
73
+ if match:
74
+ self._csrf = match.group(1)
75
+ break
76
+ if self._csrf is None:
77
+ raise APIError("Could not extract a CSRF token from the host")
78
+ return self._csrf
79
+
80
+ def post(self, path: str, data: Mapping[str, Any]) -> dict[str, Any]:
81
+ csrf = self.csrf
82
+ response = self._transport.request(
83
+ "POST",
84
+ f"{self.host}{path}",
85
+ headers={
86
+ "Cookie": self._cookie_header,
87
+ "X-Phabricator-Csrf": csrf,
88
+ "Content-Type": "application/x-www-form-urlencoded",
89
+ },
90
+ data=urlencode(data).encode(),
91
+ )
92
+ body = re.sub(rb"^for \(;;\);", b"", response.body)
93
+ payload = _json_object(body, f"Web endpoint {path}")
94
+ error = payload.get("error")
95
+ if error:
96
+ raise APIError(f"Web endpoint {path} failed: {error}")
97
+ return payload
98
+
99
+
100
+ def _json_object(body: bytes, operation: str) -> dict[str, Any]:
101
+ try:
102
+ payload = json.loads(body.decode("utf-8"))
103
+ except (UnicodeDecodeError, json.JSONDecodeError) as error:
104
+ raise APIError(f"{operation} returned an invalid JSON response") from error
105
+ if not isinstance(payload, dict):
106
+ raise APIError(f"{operation} returned an unexpected response")
107
+ return payload