assisted-review 0.0.1
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.
- package/README.md +195 -0
- package/build/claude.js +154 -0
- package/build/claude.js.map +1 -0
- package/build/cli.js +95 -0
- package/build/cli.js.map +1 -0
- package/build/env.js +26 -0
- package/build/env.js.map +1 -0
- package/build/fetch.js +41 -0
- package/build/fetch.js.map +1 -0
- package/build/jira.js +119 -0
- package/build/jira.js.map +1 -0
- package/build/mock-ai.js +47 -0
- package/build/mock-ai.js.map +1 -0
- package/build/parse-diff.js +229 -0
- package/build/parse-diff.js.map +1 -0
- package/build/parse-ref.js +28 -0
- package/build/parse-ref.js.map +1 -0
- package/build/review.js +38 -0
- package/build/review.js.map +1 -0
- package/build/server.js +272 -0
- package/build/server.js.map +1 -0
- package/build/state.js +151 -0
- package/build/state.js.map +1 -0
- package/build/submit.js +123 -0
- package/build/submit.js.map +1 -0
- package/build/types.js +10 -0
- package/build/types.js.map +1 -0
- package/dist/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
- package/dist/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
- package/dist/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
- package/dist/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
- package/dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
- package/dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
- package/dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
- package/dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
- package/dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
- package/dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/dist/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
- package/dist/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
- package/dist/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
- package/dist/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
- package/dist/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
- package/dist/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
- package/dist/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
- package/dist/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
- package/dist/assets/ibm-plex-sans-cyrillic-400-normal-BTotfTJu.woff +0 -0
- package/dist/assets/ibm-plex-sans-cyrillic-400-normal-DZqxrq2p.woff2 +0 -0
- package/dist/assets/ibm-plex-sans-cyrillic-500-normal-ByOcLdNv.woff +0 -0
- package/dist/assets/ibm-plex-sans-cyrillic-500-normal-CocWQlwt.woff2 +0 -0
- package/dist/assets/ibm-plex-sans-cyrillic-600-normal-71GNu3SW.woff2 +0 -0
- package/dist/assets/ibm-plex-sans-cyrillic-600-normal-BGq0mW3O.woff +0 -0
- package/dist/assets/ibm-plex-sans-cyrillic-ext-400-normal-Dsrv2Tcn.woff +0 -0
- package/dist/assets/ibm-plex-sans-cyrillic-ext-400-normal-g30qAdWV.woff2 +0 -0
- package/dist/assets/ibm-plex-sans-cyrillic-ext-500-normal-Cs5J6C77.woff2 +0 -0
- package/dist/assets/ibm-plex-sans-cyrillic-ext-500-normal-DB5PtV2g.woff +0 -0
- package/dist/assets/ibm-plex-sans-cyrillic-ext-600-normal-Bz0x94Yp.woff +0 -0
- package/dist/assets/ibm-plex-sans-cyrillic-ext-600-normal-DUMzJB7m.woff2 +0 -0
- package/dist/assets/ibm-plex-sans-greek-400-normal-D9ESIMu3.woff +0 -0
- package/dist/assets/ibm-plex-sans-greek-400-normal-_efipK4i.woff2 +0 -0
- package/dist/assets/ibm-plex-sans-greek-500-normal-CuWXN6rf.woff +0 -0
- package/dist/assets/ibm-plex-sans-greek-500-normal-JMMifIXV.woff2 +0 -0
- package/dist/assets/ibm-plex-sans-greek-600-normal-D-CqTdkO.woff +0 -0
- package/dist/assets/ibm-plex-sans-greek-600-normal-DzTrcv_p.woff2 +0 -0
- package/dist/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
- package/dist/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
- package/dist/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
- package/dist/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
- package/dist/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
- package/dist/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
- package/dist/assets/ibm-plex-sans-latin-ext-400-normal-C5H60-Va.woff2 +0 -0
- package/dist/assets/ibm-plex-sans-latin-ext-400-normal-RBey6euL.woff +0 -0
- package/dist/assets/ibm-plex-sans-latin-ext-500-normal-D0aIdm-b.woff +0 -0
- package/dist/assets/ibm-plex-sans-latin-ext-500-normal-DakdToA3.woff2 +0 -0
- package/dist/assets/ibm-plex-sans-latin-ext-600-normal-DIrixKbi.woff +0 -0
- package/dist/assets/ibm-plex-sans-latin-ext-600-normal-DOrvGEcy.woff2 +0 -0
- package/dist/assets/ibm-plex-sans-vietnamese-400-normal-DG4YqDda.woff2 +0 -0
- package/dist/assets/ibm-plex-sans-vietnamese-400-normal-fK1oJ5dG.woff +0 -0
- package/dist/assets/ibm-plex-sans-vietnamese-500-normal-BEb3_waV.woff +0 -0
- package/dist/assets/ibm-plex-sans-vietnamese-500-normal-e4dixQRQ.woff2 +0 -0
- package/dist/assets/ibm-plex-sans-vietnamese-600-normal-DgdngZtN.woff +0 -0
- package/dist/assets/ibm-plex-sans-vietnamese-600-normal-DpPYBSTl.woff2 +0 -0
- package/dist/assets/ibm-plex-serif-cyrillic-400-italic-C_ad97oI.woff2 +0 -0
- package/dist/assets/ibm-plex-serif-cyrillic-400-italic-CygxzOWU.woff +0 -0
- package/dist/assets/ibm-plex-serif-cyrillic-400-normal-C7IY3oUc.woff +0 -0
- package/dist/assets/ibm-plex-serif-cyrillic-400-normal-CPQ8oqB-.woff2 +0 -0
- package/dist/assets/ibm-plex-serif-cyrillic-ext-400-italic-CPw2or01.woff +0 -0
- package/dist/assets/ibm-plex-serif-cyrillic-ext-400-italic-o20Cx6Xj.woff2 +0 -0
- package/dist/assets/ibm-plex-serif-cyrillic-ext-400-normal-BcBv-TKp.woff +0 -0
- package/dist/assets/ibm-plex-serif-cyrillic-ext-400-normal-CxUI4jC_.woff2 +0 -0
- package/dist/assets/ibm-plex-serif-latin-400-italic-BCf4TsCA.woff2 +0 -0
- package/dist/assets/ibm-plex-serif-latin-400-italic-Dd68USph.woff +0 -0
- package/dist/assets/ibm-plex-serif-latin-400-normal-BB-zNvJB.woff +0 -0
- package/dist/assets/ibm-plex-serif-latin-400-normal-BIGslYFI.woff2 +0 -0
- package/dist/assets/ibm-plex-serif-latin-ext-400-italic-4IJS-XHX.woff +0 -0
- package/dist/assets/ibm-plex-serif-latin-ext-400-italic-hOoDEQwh.woff2 +0 -0
- package/dist/assets/ibm-plex-serif-latin-ext-400-normal-CNMooFZX.woff2 +0 -0
- package/dist/assets/ibm-plex-serif-latin-ext-400-normal-DwktX9jl.woff +0 -0
- package/dist/assets/ibm-plex-serif-vietnamese-400-italic-1VBVfWB7.woff +0 -0
- package/dist/assets/ibm-plex-serif-vietnamese-400-italic-BSp0Db6W.woff2 +0 -0
- package/dist/assets/ibm-plex-serif-vietnamese-400-normal-BY9Vij9A.woff +0 -0
- package/dist/assets/ibm-plex-serif-vietnamese-400-normal-DGubAMUE.woff2 +0 -0
- package/dist/assets/index-BAji2qJH.css +1 -0
- package/dist/assets/index-D9hEIdX7.js +326 -0
- package/dist/icon.svg +3 -0
- package/dist/index.html +14 -0
- package/dist/logo-dark.svg +22 -0
- package/dist/logo.svg +22 -0
- package/package.json +73 -0
package/README.md
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="web/public/logo-dark.svg" />
|
|
4
|
+
<img alt="assisted-review" src="web/public/logo.svg" width="440" />
|
|
5
|
+
</picture>
|
|
6
|
+
</p>
|
|
7
|
+
|
|
8
|
+
## What is assisted-review?
|
|
9
|
+
|
|
10
|
+
PR review fatigue is real. Large diffs overwhelm reviewers — context gets lost, subtle bugs slip through, and reviewers rush to finish. Standard GitHub review shows everything at once with no focus and no dedicated workspace.
|
|
11
|
+
|
|
12
|
+
assisted-review fetches a PR and presents it one hunk at a time in a focused browser UI. Each chunk gets its own page. Claude analyzes each chunk upfront and answers follow-up questions in a sidebar. Jira context (story + epic) appears on the overview page when configured. State persists to disk so you can resume a review across sessions.
|
|
13
|
+
|
|
14
|
+
You stay in control. Claude assists.
|
|
15
|
+
|
|
16
|
+
It is a standalone CLI: it fetches the PR with `gh`, parses the diff into chunks, and serves a paginated React UI from a localhost-only server. AI commentary streams from headless Claude Code. No data leaves your machine except the comments you choose to post.
|
|
17
|
+
|
|
18
|
+
> Status: early / in-progress — see the [changelog](./CHANGELOG.md) for what's shipped and the [roadmap](./ROADMAP.md) for what's planned.
|
|
19
|
+
|
|
20
|
+
## Requirements
|
|
21
|
+
|
|
22
|
+
- Node >= 20.18
|
|
23
|
+
- [`gh`](https://cli.github.com/) authenticated (`gh auth status`)
|
|
24
|
+
- [`claude`](https://claude.com/claude-code) CLI on `PATH` (for AI commentary)
|
|
25
|
+
- [pnpm](https://pnpm.io) — only for working on the project (not for the global install)
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
### Global install
|
|
30
|
+
|
|
31
|
+
Install directly from GitHub. No clone or pnpm required. Builds on install.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm i -g github:moui72/assisted-review
|
|
35
|
+
assisted-review <owner/repo#N | PR URL>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
To update, re-run the same `npm i -g …` command. To remove, `npm uninstall -g assisted-review`.
|
|
39
|
+
|
|
40
|
+
> The install builds itself from source, which needs the dev toolchain (`vite`, `tsc`).
|
|
41
|
+
> npm installs those automatically for the build and prunes them afterward. If you've
|
|
42
|
+
> globally set `npm config set omit=dev`, that build step can't fetch them and the
|
|
43
|
+
> install fails with e.g. `vite: not found`. In that case, install with:
|
|
44
|
+
> `npm i -g --include=dev github:moui72/assisted-review`
|
|
45
|
+
|
|
46
|
+
### From a checkout
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pnpm install
|
|
50
|
+
pnpm build:web # build the UI once
|
|
51
|
+
pnpm cli <owner/repo#N | PR URL> # fetch, serve, open the browser
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Example:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pnpm cli https://github.com/owner/repo/pull/123
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Configuration
|
|
61
|
+
|
|
62
|
+
### Jira (optional)
|
|
63
|
+
|
|
64
|
+
When Jira credentials are configured, the overview page pulls the referenced story and epic from the Jira REST API. Without credentials, it shows a setup banner instead.
|
|
65
|
+
|
|
66
|
+
| Variable | Required | Description |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| `JIRA_BASE_URL` | Yes | Base URL of your Jira instance, e.g. `https://your-org.atlassian.net` |
|
|
69
|
+
| `JIRA_USER` | Yes | Your Jira account email |
|
|
70
|
+
| `JIRA_TOKEN` | Yes | Jira API token |
|
|
71
|
+
| `JIRA_EPIC_FIELD` | No | Epic-Link custom field ID (default: `customfield_10008`) |
|
|
72
|
+
|
|
73
|
+
Variables are read from the environment with the first match winning:
|
|
74
|
+
|
|
75
|
+
1. Real environment variables (always win)
|
|
76
|
+
2. `$DOTENV_CONFIG_PATH`, if set
|
|
77
|
+
3. `./.env` in the current directory (useful in a checkout — copy `.env.example`)
|
|
78
|
+
4. `~/.assisted-review/.env` (user-global; use this for a global install)
|
|
79
|
+
|
|
80
|
+
All `.env` files are gitignored.
|
|
81
|
+
|
|
82
|
+
**Example `~/.assisted-review/.env`:**
|
|
83
|
+
|
|
84
|
+
```ini
|
|
85
|
+
JIRA_BASE_URL=https://your-org.atlassian.net
|
|
86
|
+
JIRA_USER=you@example.com
|
|
87
|
+
JIRA_TOKEN=your-jira-api-token
|
|
88
|
+
# JIRA_EPIC_FIELD=customfield_10008
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### State directory
|
|
92
|
+
|
|
93
|
+
Review state is stored in `~/.assisted-review/` by default. Override with:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
ASSISTED_REVIEW_STATE_DIR=/path/to/state
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Inline env vars
|
|
100
|
+
|
|
101
|
+
You can pass configuration inline for a one-off run:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
JIRA_BASE_URL=https://your-org.atlassian.net JIRA_USER=you@example.com JIRA_TOKEN=<token> assisted-review owner/repo#123
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Usage
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
assisted-review <owner/repo#N | PR URL>
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Flags
|
|
114
|
+
|
|
115
|
+
| Flag | Effect |
|
|
116
|
+
|---|---|
|
|
117
|
+
| `--no-open` | Don't open the browser automatically |
|
|
118
|
+
| `--api-only` | Serve only the API (pair with `pnpm dev:web`) |
|
|
119
|
+
| `--port <n>` | Listen port (default 4319) |
|
|
120
|
+
| `--mock-ai` | Fill chunks with placeholder commentary (offline use) |
|
|
121
|
+
|
|
122
|
+
### Keyboard shortcuts
|
|
123
|
+
|
|
124
|
+
| Key | Action |
|
|
125
|
+
|---|---|
|
|
126
|
+
| `→` / `j` | Next chunk |
|
|
127
|
+
| `←` / `k` | Previous chunk |
|
|
128
|
+
| `⌘→` / `⌘←` (Ctrl on Win/Linux) | Next / previous unread chunk |
|
|
129
|
+
| `↵` | Mark viewed and advance |
|
|
130
|
+
| `esc` | Mark unread |
|
|
131
|
+
| `f` | Flag chunk |
|
|
132
|
+
| `c` | Comment |
|
|
133
|
+
| `a` | Ask Claude |
|
|
134
|
+
| `?` | Show help |
|
|
135
|
+
|
|
136
|
+
## Submitting
|
|
137
|
+
|
|
138
|
+
When you're done reviewing, hit **Submit** in the top bar to publish to GitHub. Choose a verdict (Approve / Comment / Request changes), add an optional summary, and the drafted line comments are posted as a single PR review via `gh api`. Whole-chunk comments anchor to the chunk's last changed line.
|
|
139
|
+
|
|
140
|
+
If the PR was force-pushed since you started, the head SHA the comments were drafted against is no longer valid. In that case, submission is blocked with a stale-SHA warning rather than posting mis-anchored comments. Re-fetch the PR to re-anchor your comments to the new SHA.
|
|
141
|
+
|
|
142
|
+
## Architecture
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
src/ TypeScript backend (CommonJS, ts-node)
|
|
146
|
+
cli.ts entry: parse ref → gh fetch → chunks → Jira → serve
|
|
147
|
+
fetch.ts · parse-ref.ts · parse-diff.ts diff/PR ingestion
|
|
148
|
+
server.ts localhost server: /api/review, /api/state, /api/action, /api/claude (SSE), /api/submit
|
|
149
|
+
state.ts persisted review state (~/.assisted-review/<owner>-<repo>-<n>.json)
|
|
150
|
+
claude.ts headless Claude bridge (stream-json)
|
|
151
|
+
submit.ts publish drafted comments as a real PR review via `gh api`
|
|
152
|
+
jira.ts Jira REST fetch (env-configured)
|
|
153
|
+
web/ Vite + React + Tailwind UI → builds into dist/, served by the server
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
- **`cli.ts`** — entry point; parses the PR ref, fetches the diff and metadata, extracts Jira keys, and hands off to the server.
|
|
157
|
+
- **`fetch.ts` / `parse-ref.ts` / `parse-diff.ts`** — fetch the raw diff and PR metadata via `gh`, parse the ref format, and slice the unified diff into reviewable chunks.
|
|
158
|
+
- **`server.ts`** — Express server providing the REST and SSE API. Serves the pre-built React UI from `dist/` unless `--api-only` is set.
|
|
159
|
+
- **`state.ts`** — loads and persists review state (viewed, flagged, comments, AI notes) as JSON in `~/.assisted-review/`.
|
|
160
|
+
- **`claude.ts`** — spawns headless `claude` as a subprocess and streams JSON-formatted commentary back to the server.
|
|
161
|
+
- **`submit.ts`** — assembles drafted comments into a single PR review payload and posts it via `gh api`.
|
|
162
|
+
- **`jira.ts`** — fetches issue and epic data from the Jira REST API using env-configured credentials.
|
|
163
|
+
|
|
164
|
+
State lives in `~/.assisted-review/` (override with `ASSISTED_REVIEW_STATE_DIR`).
|
|
165
|
+
|
|
166
|
+
## Contributing
|
|
167
|
+
|
|
168
|
+
### Dev setup
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
pnpm install
|
|
172
|
+
pnpm dev # API server on :4319 + Vite HMR on :5173
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Open `http://localhost:5173` for the live-reloading UI. Set a default PR with `PR_REF=owner/repo#N` in `.env` (copy `.env.example`).
|
|
176
|
+
|
|
177
|
+
### Scripts
|
|
178
|
+
|
|
179
|
+
| Script | What it does |
|
|
180
|
+
|---|---|
|
|
181
|
+
| `dev` | Starts the API server and Vite HMR server concurrently |
|
|
182
|
+
| `build` | Compiles TypeScript (server → `build/`) and bundles the UI (→ `dist/`) |
|
|
183
|
+
| `build:web` | Builds only the React UI with Vite |
|
|
184
|
+
| `test` | Runs Jest unit tests |
|
|
185
|
+
| `test:watch` | Runs Jest in watch mode |
|
|
186
|
+
| `lint` | Runs ESLint |
|
|
187
|
+
| `format` | Runs Prettier |
|
|
188
|
+
|
|
189
|
+
### Adding a language
|
|
190
|
+
|
|
191
|
+
Syntax highlighting is registered in `web/src/highlight.ts`. Import the language grammar from `highlight.js` there and add it to the `hljs.registerLanguage` calls.
|
|
192
|
+
|
|
193
|
+
### PRs welcome
|
|
194
|
+
|
|
195
|
+
Open a PR against `main`. CI runs lint and tests on every push. Please keep commits focused and include tests for new behavior where applicable.
|
package/build/claude.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// Headless Claude bridge. Spawns `claude -p` in stream-json mode, feeds the
|
|
2
|
+
// prompt on stdin, and surfaces text deltas + completion. Runs in a temp cwd
|
|
3
|
+
// with tools disabled so it answers purely from the prompt (the diff), without
|
|
4
|
+
// loading this project's context or touching the filesystem.
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
7
|
+
const MAX_DIFF_CHARS = 12000;
|
|
8
|
+
const MAX_JIRA_DESC = 1200;
|
|
9
|
+
const NO_TOOLS = [
|
|
10
|
+
'Bash',
|
|
11
|
+
'Edit',
|
|
12
|
+
'Write',
|
|
13
|
+
'Read',
|
|
14
|
+
'Grep',
|
|
15
|
+
'Glob',
|
|
16
|
+
'WebFetch',
|
|
17
|
+
'WebSearch',
|
|
18
|
+
'Task',
|
|
19
|
+
'NotebookEdit',
|
|
20
|
+
];
|
|
21
|
+
/** Build the prompt for a chunk. Empty question → an "explain this hunk" note. */
|
|
22
|
+
export function buildPrompt(chunk, kind, question) {
|
|
23
|
+
const intro = 'You are assisting a code reviewer reviewing a GitHub pull request. ' +
|
|
24
|
+
'Be concise and direct — lead with the most important point, no hedging, no preamble. ' +
|
|
25
|
+
'Answer only from the diff shown; do not use tools.';
|
|
26
|
+
const ctx = `File: ${chunk.file}\n\nDiff hunk:\n\`\`\`diff\n${chunk.diff}\n\`\`\``;
|
|
27
|
+
if (kind === 'investigation' && question.trim()) {
|
|
28
|
+
return `${intro}\n\nThe reviewer asks about this hunk: "${question.trim()}"\n\n${ctx}\n\nAnswer in 2-5 sentences or a few short bullets.`;
|
|
29
|
+
}
|
|
30
|
+
return (`${intro}\n\n${ctx}\n\n` +
|
|
31
|
+
`Flag the most important thing the reviewer should notice about this change in 2-4 sentences ` +
|
|
32
|
+
`(a risk, bug, or behavior change); if nothing stands out, say so briefly. ` +
|
|
33
|
+
`Then add a final line in exactly this format: "Suggested action: <one concrete next step>" ` +
|
|
34
|
+
`— e.g., ask the author to clarify X, request a change to Y, or verify Z. Keep the action to one sentence. ` +
|
|
35
|
+
`Do not number or add headings to the two parts.`);
|
|
36
|
+
}
|
|
37
|
+
function clip(s, max) {
|
|
38
|
+
return s.length > max ? s.slice(0, max) + '\n… (truncated)' : s;
|
|
39
|
+
}
|
|
40
|
+
/** Build the whole-PR overview prompt. Empty question → summarize; else answer it. */
|
|
41
|
+
export function buildOverviewPrompt(meta, chunks, jira, question) {
|
|
42
|
+
const files = [...new Set(chunks.map((c) => c.file))];
|
|
43
|
+
const combinedDiff = clip(chunks.map((c) => c.diff).join('\n'), MAX_DIFF_CHARS);
|
|
44
|
+
const task = question.trim()
|
|
45
|
+
? `Answer the reviewer's question about this pull request: "${question.trim()}". Be concise and concrete.`
|
|
46
|
+
: `In 3-6 sentences, orient the reviewer: summarize what this PR changes and why — the intent and the shape of the change across files. Ground it in the description and ticket if provided. No preamble, no headings, no "suggested action" line.`;
|
|
47
|
+
const parts = [
|
|
48
|
+
`You are orienting a code reviewer before they review a pull request. ${task}`,
|
|
49
|
+
`PR title: ${meta.title}`,
|
|
50
|
+
];
|
|
51
|
+
if (meta.body.trim())
|
|
52
|
+
parts.push(`PR description:\n${clip(meta.body.trim(), 4000)}`);
|
|
53
|
+
if (jira.available && jira.issues.length) {
|
|
54
|
+
const tix = jira.issues
|
|
55
|
+
.map((i) => `${i.key} [${i.type} · ${i.status}]: ${i.summary}\n${clip(i.description, MAX_JIRA_DESC)}`)
|
|
56
|
+
.join('\n\n');
|
|
57
|
+
let block = `Linked Jira issue(s):\n${tix}`;
|
|
58
|
+
if (jira.epic)
|
|
59
|
+
block += `\n\nParent epic ${jira.epic.key}: ${jira.epic.summary}\n${clip(jira.epic.description, MAX_JIRA_DESC)}`;
|
|
60
|
+
parts.push(block);
|
|
61
|
+
}
|
|
62
|
+
parts.push(`Files changed (${files.length}): ${files.join(', ')}`);
|
|
63
|
+
parts.push(`Combined diff (may be truncated):\n\`\`\`diff\n${combinedDiff}\n\`\`\``);
|
|
64
|
+
return parts.join('\n\n');
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Split an initial note into its observation body and the trailing
|
|
68
|
+
* "Suggested action: …" line (tolerant of markdown bold around the label).
|
|
69
|
+
*/
|
|
70
|
+
export function splitSuggestedAction(text) {
|
|
71
|
+
// Strip a leading enumerator/heading the model sometimes adds (e.g. "1. ").
|
|
72
|
+
const clean = (s) => s.trim().replace(/^\s*\d+\.\s+/, '');
|
|
73
|
+
const m = text.match(/\n+\s*\*{0,2}\s*suggested\s+action\s*\*{0,2}\s*:\s*\*{0,2}\s*/i);
|
|
74
|
+
if (!m || m.index === undefined)
|
|
75
|
+
return { body: clean(text) };
|
|
76
|
+
const action = text.slice(m.index + m[0].length).trim();
|
|
77
|
+
if (!action)
|
|
78
|
+
return { body: clean(text) };
|
|
79
|
+
return { body: clean(text.slice(0, m.index)), suggestedAction: action };
|
|
80
|
+
}
|
|
81
|
+
/** Spawn claude and stream its answer. Returns a cancel function. */
|
|
82
|
+
export function streamClaude(prompt, handlers) {
|
|
83
|
+
const child = spawn('claude', [
|
|
84
|
+
'-p',
|
|
85
|
+
'--output-format',
|
|
86
|
+
'stream-json',
|
|
87
|
+
'--include-partial-messages',
|
|
88
|
+
'--verbose',
|
|
89
|
+
'--disallowed-tools',
|
|
90
|
+
...NO_TOOLS,
|
|
91
|
+
], { cwd: tmpdir(), stdio: ['pipe', 'pipe', 'pipe'] });
|
|
92
|
+
let buf = '';
|
|
93
|
+
let full = '';
|
|
94
|
+
let settled = false;
|
|
95
|
+
let stderr = '';
|
|
96
|
+
function finishOk(text) {
|
|
97
|
+
if (settled)
|
|
98
|
+
return;
|
|
99
|
+
settled = true;
|
|
100
|
+
handlers.onDone(text);
|
|
101
|
+
}
|
|
102
|
+
function finishErr(msg) {
|
|
103
|
+
if (settled)
|
|
104
|
+
return;
|
|
105
|
+
settled = true;
|
|
106
|
+
handlers.onError(msg);
|
|
107
|
+
}
|
|
108
|
+
child.stdout.on('data', (chunk) => {
|
|
109
|
+
buf += chunk.toString();
|
|
110
|
+
let nl;
|
|
111
|
+
while ((nl = buf.indexOf('\n')) >= 0) {
|
|
112
|
+
const line = buf.slice(0, nl);
|
|
113
|
+
buf = buf.slice(nl + 1);
|
|
114
|
+
if (!line.trim())
|
|
115
|
+
continue;
|
|
116
|
+
let ev;
|
|
117
|
+
try {
|
|
118
|
+
ev = JSON.parse(line);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
continue; // ignore non-JSON noise
|
|
122
|
+
}
|
|
123
|
+
if (ev.type === 'stream_event') {
|
|
124
|
+
const inner = ev.event;
|
|
125
|
+
if (inner?.type === 'content_block_delta' && inner.delta?.type === 'text_delta') {
|
|
126
|
+
const t = inner.delta.text ?? '';
|
|
127
|
+
full += t;
|
|
128
|
+
handlers.onDelta(t);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
else if (ev.type === 'result') {
|
|
132
|
+
if (ev.is_error)
|
|
133
|
+
finishErr(typeof ev.result === 'string' ? ev.result : 'Claude returned an error');
|
|
134
|
+
else
|
|
135
|
+
finishOk(typeof ev.result === 'string' ? ev.result : full);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
child.stderr.on('data', (d) => (stderr += d.toString()));
|
|
140
|
+
child.on('error', (e) => finishErr(`failed to start claude: ${e.message} (is the \`claude\` CLI installed and on PATH?)`));
|
|
141
|
+
child.on('close', (code) => {
|
|
142
|
+
if (code === 0)
|
|
143
|
+
finishOk(full);
|
|
144
|
+
else
|
|
145
|
+
finishErr(stderr.trim() || `claude exited with code ${code}`);
|
|
146
|
+
});
|
|
147
|
+
child.stdin.write(prompt);
|
|
148
|
+
child.stdin.end();
|
|
149
|
+
return () => {
|
|
150
|
+
if (!child.killed)
|
|
151
|
+
child.kill();
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
//# sourceMappingURL=claude.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claude.js","sourceRoot":"","sources":["../src/claude.ts"],"names":[],"mappings":"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"}
|
package/build/cli.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// assisted-review <owner/repo#N | PR URL>
|
|
3
|
+
//
|
|
4
|
+
// Slice 1: fetch a PR, parse it into grouped hunks, serve a browser UI that
|
|
5
|
+
// lets you navigate the chunks. No commenting/submit/Claude yet.
|
|
6
|
+
//
|
|
7
|
+
// Flags:
|
|
8
|
+
// --no-open don't open the browser
|
|
9
|
+
// --api-only serve only /api (pair with `pnpm dev:web` Vite server)
|
|
10
|
+
// --mock-ai attach placeholder (lorem) AI commentary to each chunk
|
|
11
|
+
// --port <n> listen port (default 4319)
|
|
12
|
+
import './env.js'; // load .env before any module reads process.env
|
|
13
|
+
import { execFile } from 'node:child_process';
|
|
14
|
+
import { parseRef } from './parse-ref.js';
|
|
15
|
+
import { startServer } from './server.js';
|
|
16
|
+
import { saveState } from './state.js';
|
|
17
|
+
import { loadReview } from './review.js';
|
|
18
|
+
function openBrowser(url) {
|
|
19
|
+
const [cmd, args] = process.platform === 'darwin'
|
|
20
|
+
? ['open', [url]]
|
|
21
|
+
: process.platform === 'win32'
|
|
22
|
+
? ['cmd', ['/c', 'start', '', url]]
|
|
23
|
+
: ['xdg-open', [url]];
|
|
24
|
+
execFile(cmd, args, () => { });
|
|
25
|
+
}
|
|
26
|
+
function parseArgs(argv) {
|
|
27
|
+
let ref = process.env.PR_REF;
|
|
28
|
+
let noOpen = false;
|
|
29
|
+
let apiOnly = false;
|
|
30
|
+
let mockAi = false;
|
|
31
|
+
let port = 4319;
|
|
32
|
+
for (let i = 0; i < argv.length; i++) {
|
|
33
|
+
const a = argv[i];
|
|
34
|
+
if (a === '--no-open')
|
|
35
|
+
noOpen = true;
|
|
36
|
+
else if (a === '--api-only')
|
|
37
|
+
apiOnly = true;
|
|
38
|
+
else if (a === '--mock-ai')
|
|
39
|
+
mockAi = true;
|
|
40
|
+
else if (a === '--port')
|
|
41
|
+
port = Number(argv[++i]);
|
|
42
|
+
else if (!a.startsWith('-'))
|
|
43
|
+
ref = a;
|
|
44
|
+
}
|
|
45
|
+
return { ref, noOpen, apiOnly, mockAi, port };
|
|
46
|
+
}
|
|
47
|
+
async function main() {
|
|
48
|
+
const { ref, noOpen, apiOnly, mockAi, port } = parseArgs(process.argv.slice(2));
|
|
49
|
+
let pr;
|
|
50
|
+
try {
|
|
51
|
+
pr = parseRef(ref);
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
console.error(`error: ${err.message}`);
|
|
55
|
+
console.error('usage: assisted-review <owner/repo#N | PR URL>');
|
|
56
|
+
process.exit(2);
|
|
57
|
+
}
|
|
58
|
+
console.error(`Fetching ${pr.owner}/${pr.repo}#${pr.number} ...`);
|
|
59
|
+
let review, state;
|
|
60
|
+
try {
|
|
61
|
+
({ review, state } = await loadReview(pr, { mockAi }));
|
|
62
|
+
console.error(`Parsed ${review.chunks.length} chunk(s) across the diff.`);
|
|
63
|
+
const jira = review.overview.jira;
|
|
64
|
+
const keys = jira.keys;
|
|
65
|
+
if (keys.length) {
|
|
66
|
+
console.error(jira.available
|
|
67
|
+
? `Jira: linked ${jira.issues.map((i) => i.key).join(', ') || '(none fetched)'}${jira.epic ? ` · epic ${jira.epic.key}` : ''}.`
|
|
68
|
+
: `Jira: unavailable (${jira.reason}). Overview will show a setup banner.`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
console.error(`error: failed to fetch/parse PR: ${err.message}`);
|
|
73
|
+
console.error('hint: is `gh` installed and authenticated? try `gh auth status`.');
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
const priorCount = state.comments.length + state.flagged.length + state.viewed.length;
|
|
77
|
+
if (priorCount > 0) {
|
|
78
|
+
console.error(`Resumed state: ${state.comments.length} comment(s), ${state.flagged.length} flagged, ${state.viewed.length} viewed.`);
|
|
79
|
+
}
|
|
80
|
+
await saveState(state);
|
|
81
|
+
const { url } = await startServer({ review, state }, { port, serveUi: !apiOnly, mockAi });
|
|
82
|
+
if (apiOnly) {
|
|
83
|
+
console.error(`\n assisted-review API serving at ${url}/api/review`);
|
|
84
|
+
console.error(` Start the UI with: pnpm dev:web (proxies /api here)\n`);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
console.error(`\n assisted-review serving at ${url}`);
|
|
88
|
+
console.error(` ${review.meta.title}`);
|
|
89
|
+
console.error(` Press Ctrl+C to stop.\n`);
|
|
90
|
+
if (!noOpen)
|
|
91
|
+
openBrowser(url);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
void main();
|
|
95
|
+
//# sourceMappingURL=cli.js.map
|
package/build/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
package/build/env.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Load a .env file into process.env before any other module reads it. Import
|
|
2
|
+
// this FIRST in the CLI entrypoint.
|
|
3
|
+
//
|
|
4
|
+
// dotenv never overrides a variable already present in the environment, and for
|
|
5
|
+
// any given key the FIRST file that sets it wins. The list below is therefore in
|
|
6
|
+
// precedence order, and a missing file is a no-op. This lets a global install
|
|
7
|
+
// pick up credentials (e.g. Jira) from ~/.assisted-review/.env no matter which
|
|
8
|
+
// directory `assisted-review` is run from, while a checkout's own .env still wins
|
|
9
|
+
// during development.
|
|
10
|
+
//
|
|
11
|
+
// 1. real environment variables (always win — dotenv won't override)
|
|
12
|
+
// 2. $DOTENV_CONFIG_PATH (explicit override)
|
|
13
|
+
// 3. ./.env (current dir — local / dev)
|
|
14
|
+
// 4. ~/.assisted-review/.env (user-global default; matches state root)
|
|
15
|
+
import { homedir } from 'node:os';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
import { config } from 'dotenv';
|
|
18
|
+
const candidates = [
|
|
19
|
+
process.env.DOTENV_CONFIG_PATH,
|
|
20
|
+
'.env',
|
|
21
|
+
join(homedir(), '.assisted-review', '.env'),
|
|
22
|
+
].filter((p) => Boolean(p));
|
|
23
|
+
for (const path of candidates) {
|
|
24
|
+
config({ path, quiet: true });
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=env.js.map
|
package/build/env.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env.js","sourceRoot":"","sources":["../src/env.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,oCAAoC;AACpC,EAAE;AACF,gFAAgF;AAChF,iFAAiF;AACjF,8EAA8E;AAC9E,+EAA+E;AAC/E,kFAAkF;AAClF,sBAAsB;AACtB,EAAE;AACF,gFAAgF;AAChF,gEAAgE;AAChE,wEAAwE;AACxE,sFAAsF;AAEtF,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,MAAM,UAAU,GAAG;IACjB,OAAO,CAAC,GAAG,CAAC,kBAAkB;IAC9B,MAAM;IACN,IAAI,CAAC,OAAO,EAAE,EAAE,kBAAkB,EAAE,MAAM,CAAC;CAC5C,CAAC,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;AAEzC,KAAK,MAAM,IAAI,IAAI,UAAU,EAAE,CAAC;IAC9B,MAAM,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;AAChC,CAAC"}
|
package/build/fetch.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Fetch PR diff + metadata via the `gh` CLI, then parse the diff into
|
|
2
|
+
// grouped hunks (pure TS — see parse-diff.ts).
|
|
3
|
+
import { execFile } from 'node:child_process';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
const execFileAsync = promisify(execFile);
|
|
6
|
+
function ghTarget({ owner, repo, number }) {
|
|
7
|
+
return { repo: `${owner}/${repo}`, number: String(number) };
|
|
8
|
+
}
|
|
9
|
+
export async function fetchDiff(ref) {
|
|
10
|
+
const { repo, number } = ghTarget(ref);
|
|
11
|
+
const { stdout } = await execFileAsync('gh', ['pr', 'diff', number, '--repo', repo], {
|
|
12
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
13
|
+
});
|
|
14
|
+
return stdout;
|
|
15
|
+
}
|
|
16
|
+
export async function fetchMeta(ref) {
|
|
17
|
+
const { repo, number } = ghTarget(ref);
|
|
18
|
+
const fields = [
|
|
19
|
+
'title',
|
|
20
|
+
'author',
|
|
21
|
+
'baseRefName',
|
|
22
|
+
'headRefName',
|
|
23
|
+
'isDraft',
|
|
24
|
+
'url',
|
|
25
|
+
'headRefOid',
|
|
26
|
+
'body',
|
|
27
|
+
].join(',');
|
|
28
|
+
const { stdout } = await execFileAsync('gh', ['pr', 'view', number, '--repo', repo, '--json', fields], { maxBuffer: 8 * 1024 * 1024 });
|
|
29
|
+
const raw = JSON.parse(stdout);
|
|
30
|
+
return {
|
|
31
|
+
title: raw.title,
|
|
32
|
+
author: raw.author?.login ?? 'unknown',
|
|
33
|
+
base_ref: raw.baseRefName,
|
|
34
|
+
head_ref: raw.headRefName,
|
|
35
|
+
is_draft: raw.isDraft,
|
|
36
|
+
url: raw.url,
|
|
37
|
+
head_sha: raw.headRefOid,
|
|
38
|
+
body: raw.body ?? '',
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=fetch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch.js","sourceRoot":"","sources":["../src/fetch.ts"],"names":[],"mappings":"AAAA,sEAAsE;AACtE,+CAA+C;AAE/C,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAGtC,MAAM,aAAa,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;AAE1C,SAAS,QAAQ,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAS;IAC9C,OAAO,EAAE,IAAI,EAAE,GAAG,KAAK,IAAI,IAAI,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC;AAC9D,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,GAAU;IACxC,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC;IACvC,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,aAAa,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,CAAC,EAAE;QACnF,SAAS,EAAE,EAAE,GAAG,IAAI,GAAG,IAAI;KAC5B,CAAC,CAAC;IACH,OAAO,MAAM,CAAC;AAChB,CAAC;AAaD,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,GAAU;IACxC,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC;IACvC,MAAM,MAAM,GAAG;QACb,OAAO;QACP,QAAQ;QACR,aAAa;QACb,aAAa;QACb,SAAS;QACT,KAAK;QACL,YAAY;QACZ,MAAM;KACP,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACZ,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,aAAa,CACpC,IAAI,EACJ,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,CAAC,EACxD,EAAE,SAAS,EAAE,CAAC,GAAG,IAAI,GAAG,IAAI,EAAE,CAC/B,CAAC;IACF,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAa,CAAC;IAC3C,OAAO;QACL,KAAK,EAAE,GAAG,CAAC,KAAK;QAChB,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,KAAK,IAAI,SAAS;QACtC,QAAQ,EAAE,GAAG,CAAC,WAAW;QACzB,QAAQ,EAAE,GAAG,CAAC,WAAW;QACzB,QAAQ,EAAE,GAAG,CAAC,OAAO;QACrB,GAAG,EAAE,GAAG,CAAC,GAAG;QACZ,QAAQ,EAAE,GAAG,CAAC,UAAU;QACxB,IAAI,EAAE,GAAG,CAAC,IAAI,IAAI,EAAE;KACrB,CAAC;AACJ,CAAC"}
|
package/build/jira.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// Best-effort Jira background for the overview page. Fetches the referenced
|
|
2
|
+
// story (and its epic) straight from the Jira REST API using credentials from
|
|
3
|
+
// the environment — the tool is agnostic about how they're provided.
|
|
4
|
+
//
|
|
5
|
+
// JIRA_BASE_URL e.g. https://your-org.atlassian.net
|
|
6
|
+
// JIRA_USER account email
|
|
7
|
+
// JIRA_TOKEN API token
|
|
8
|
+
// JIRA_EPIC_FIELD optional; the "Epic Link" custom field (default customfield_10008)
|
|
9
|
+
//
|
|
10
|
+
// If any are missing, returns { available: false } with a setup hint so the UI
|
|
11
|
+
// can show a banner instead of failing.
|
|
12
|
+
const BASE_URL = (process.env.JIRA_BASE_URL ?? '').replace(/\/$/, '');
|
|
13
|
+
const USER = process.env.JIRA_USER ?? '';
|
|
14
|
+
const TOKEN = process.env.JIRA_TOKEN ?? '';
|
|
15
|
+
const EPIC_FIELD = process.env.JIRA_EPIC_FIELD || 'customfield_10008';
|
|
16
|
+
const SETUP_HINT = "assisted-review can't reach Jira. Set JIRA_BASE_URL, JIRA_USER, and JIRA_TOKEN " +
|
|
17
|
+
'(an API token) in the environment to pull in ticket and epic context.';
|
|
18
|
+
/** Extract Jira issue keys (e.g. FEN-2622) from any number of text sources. */
|
|
19
|
+
export function extractIssueKeys(...texts) {
|
|
20
|
+
const re = /\b[A-Z][A-Z0-9]+-\d+\b/g;
|
|
21
|
+
const seen = new Set();
|
|
22
|
+
for (const t of texts) {
|
|
23
|
+
if (!t)
|
|
24
|
+
continue;
|
|
25
|
+
for (const m of t.match(re) ?? [])
|
|
26
|
+
seen.add(m);
|
|
27
|
+
}
|
|
28
|
+
return [...seen];
|
|
29
|
+
}
|
|
30
|
+
function configured() {
|
|
31
|
+
return Boolean(BASE_URL && USER && TOKEN);
|
|
32
|
+
}
|
|
33
|
+
/** Recursively flatten Atlassian Document Format to plain text. */
|
|
34
|
+
function adfToText(node) {
|
|
35
|
+
if (!node)
|
|
36
|
+
return '';
|
|
37
|
+
if (typeof node === 'string')
|
|
38
|
+
return node;
|
|
39
|
+
const n = node;
|
|
40
|
+
const kids = n.content ?? [];
|
|
41
|
+
const inner = () => kids.map(adfToText).join('');
|
|
42
|
+
switch (n.type) {
|
|
43
|
+
case 'text':
|
|
44
|
+
return n.text ?? '';
|
|
45
|
+
case 'hardBreak':
|
|
46
|
+
return '\n';
|
|
47
|
+
case 'paragraph':
|
|
48
|
+
case 'listItem':
|
|
49
|
+
case 'blockquote':
|
|
50
|
+
case 'heading':
|
|
51
|
+
return inner() + '\n';
|
|
52
|
+
case 'bulletList':
|
|
53
|
+
case 'orderedList':
|
|
54
|
+
return kids.map((c) => ' - ' + adfToText(c).trim() + '\n').join('');
|
|
55
|
+
default:
|
|
56
|
+
return inner();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async function fetchIssue(key) {
|
|
60
|
+
const auth = Buffer.from(`${USER}:${TOKEN}`).toString('base64');
|
|
61
|
+
const fields = `summary,status,issuetype,description,parent,${EPIC_FIELD}`;
|
|
62
|
+
const ctrl = new AbortController();
|
|
63
|
+
const timer = setTimeout(() => ctrl.abort(), 8000);
|
|
64
|
+
try {
|
|
65
|
+
const res = await fetch(`${BASE_URL}/rest/api/3/issue/${encodeURIComponent(key)}?fields=${fields}`, {
|
|
66
|
+
headers: { authorization: `Basic ${auth}`, accept: 'application/json' },
|
|
67
|
+
signal: ctrl.signal,
|
|
68
|
+
});
|
|
69
|
+
if (!res.ok)
|
|
70
|
+
return null;
|
|
71
|
+
const data = (await res.json());
|
|
72
|
+
const f = data.fields ?? {};
|
|
73
|
+
const parent = f.parent;
|
|
74
|
+
const epicLink = f[EPIC_FIELD];
|
|
75
|
+
return {
|
|
76
|
+
key: data.key,
|
|
77
|
+
summary: f.summary ?? '',
|
|
78
|
+
status: f.status?.name ?? '',
|
|
79
|
+
type: f.issuetype?.name ?? '',
|
|
80
|
+
description: adfToText(f.description).trim(),
|
|
81
|
+
url: `${BASE_URL}/browse/${data.key}`,
|
|
82
|
+
epic_key: (typeof epicLink === 'string' ? epicLink : undefined) ?? parent?.key,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
clearTimeout(timer);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/** Build the Jira context for the given issue keys (best effort). */
|
|
93
|
+
export async function buildJiraContext(keys) {
|
|
94
|
+
if (!configured()) {
|
|
95
|
+
return {
|
|
96
|
+
available: false,
|
|
97
|
+
reason: 'Jira credentials not configured',
|
|
98
|
+
setup_hint: SETUP_HINT,
|
|
99
|
+
keys,
|
|
100
|
+
issues: [],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
if (keys.length === 0)
|
|
104
|
+
return { available: true, keys, issues: [] };
|
|
105
|
+
const issues = (await Promise.all(keys.slice(0, 4).map(fetchIssue))).filter((i) => i !== null);
|
|
106
|
+
if (issues.length === 0) {
|
|
107
|
+
return {
|
|
108
|
+
available: false,
|
|
109
|
+
reason: `Could not fetch ${keys.join(', ')} (check JIRA_TOKEN scope and access)`,
|
|
110
|
+
setup_hint: SETUP_HINT,
|
|
111
|
+
keys,
|
|
112
|
+
issues: [],
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
const epicKey = issues.find((i) => i.epic_key)?.epic_key;
|
|
116
|
+
const epic = epicKey ? await fetchIssue(epicKey) : null;
|
|
117
|
+
return { available: true, keys, issues, epic };
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=jira.js.map
|