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.
Files changed (110) hide show
  1. package/README.md +195 -0
  2. package/build/claude.js +154 -0
  3. package/build/claude.js.map +1 -0
  4. package/build/cli.js +95 -0
  5. package/build/cli.js.map +1 -0
  6. package/build/env.js +26 -0
  7. package/build/env.js.map +1 -0
  8. package/build/fetch.js +41 -0
  9. package/build/fetch.js.map +1 -0
  10. package/build/jira.js +119 -0
  11. package/build/jira.js.map +1 -0
  12. package/build/mock-ai.js +47 -0
  13. package/build/mock-ai.js.map +1 -0
  14. package/build/parse-diff.js +229 -0
  15. package/build/parse-diff.js.map +1 -0
  16. package/build/parse-ref.js +28 -0
  17. package/build/parse-ref.js.map +1 -0
  18. package/build/review.js +38 -0
  19. package/build/review.js.map +1 -0
  20. package/build/server.js +272 -0
  21. package/build/server.js.map +1 -0
  22. package/build/state.js +151 -0
  23. package/build/state.js.map +1 -0
  24. package/build/submit.js +123 -0
  25. package/build/submit.js.map +1 -0
  26. package/build/types.js +10 -0
  27. package/build/types.js.map +1 -0
  28. package/dist/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
  29. package/dist/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
  30. package/dist/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
  31. package/dist/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
  32. package/dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
  33. package/dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
  34. package/dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
  35. package/dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
  36. package/dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  37. package/dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  38. package/dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  39. package/dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  40. package/dist/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
  41. package/dist/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
  42. package/dist/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
  43. package/dist/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
  44. package/dist/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
  45. package/dist/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
  46. package/dist/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
  47. package/dist/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
  48. package/dist/assets/ibm-plex-sans-cyrillic-400-normal-BTotfTJu.woff +0 -0
  49. package/dist/assets/ibm-plex-sans-cyrillic-400-normal-DZqxrq2p.woff2 +0 -0
  50. package/dist/assets/ibm-plex-sans-cyrillic-500-normal-ByOcLdNv.woff +0 -0
  51. package/dist/assets/ibm-plex-sans-cyrillic-500-normal-CocWQlwt.woff2 +0 -0
  52. package/dist/assets/ibm-plex-sans-cyrillic-600-normal-71GNu3SW.woff2 +0 -0
  53. package/dist/assets/ibm-plex-sans-cyrillic-600-normal-BGq0mW3O.woff +0 -0
  54. package/dist/assets/ibm-plex-sans-cyrillic-ext-400-normal-Dsrv2Tcn.woff +0 -0
  55. package/dist/assets/ibm-plex-sans-cyrillic-ext-400-normal-g30qAdWV.woff2 +0 -0
  56. package/dist/assets/ibm-plex-sans-cyrillic-ext-500-normal-Cs5J6C77.woff2 +0 -0
  57. package/dist/assets/ibm-plex-sans-cyrillic-ext-500-normal-DB5PtV2g.woff +0 -0
  58. package/dist/assets/ibm-plex-sans-cyrillic-ext-600-normal-Bz0x94Yp.woff +0 -0
  59. package/dist/assets/ibm-plex-sans-cyrillic-ext-600-normal-DUMzJB7m.woff2 +0 -0
  60. package/dist/assets/ibm-plex-sans-greek-400-normal-D9ESIMu3.woff +0 -0
  61. package/dist/assets/ibm-plex-sans-greek-400-normal-_efipK4i.woff2 +0 -0
  62. package/dist/assets/ibm-plex-sans-greek-500-normal-CuWXN6rf.woff +0 -0
  63. package/dist/assets/ibm-plex-sans-greek-500-normal-JMMifIXV.woff2 +0 -0
  64. package/dist/assets/ibm-plex-sans-greek-600-normal-D-CqTdkO.woff +0 -0
  65. package/dist/assets/ibm-plex-sans-greek-600-normal-DzTrcv_p.woff2 +0 -0
  66. package/dist/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
  67. package/dist/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
  68. package/dist/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
  69. package/dist/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
  70. package/dist/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
  71. package/dist/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
  72. package/dist/assets/ibm-plex-sans-latin-ext-400-normal-C5H60-Va.woff2 +0 -0
  73. package/dist/assets/ibm-plex-sans-latin-ext-400-normal-RBey6euL.woff +0 -0
  74. package/dist/assets/ibm-plex-sans-latin-ext-500-normal-D0aIdm-b.woff +0 -0
  75. package/dist/assets/ibm-plex-sans-latin-ext-500-normal-DakdToA3.woff2 +0 -0
  76. package/dist/assets/ibm-plex-sans-latin-ext-600-normal-DIrixKbi.woff +0 -0
  77. package/dist/assets/ibm-plex-sans-latin-ext-600-normal-DOrvGEcy.woff2 +0 -0
  78. package/dist/assets/ibm-plex-sans-vietnamese-400-normal-DG4YqDda.woff2 +0 -0
  79. package/dist/assets/ibm-plex-sans-vietnamese-400-normal-fK1oJ5dG.woff +0 -0
  80. package/dist/assets/ibm-plex-sans-vietnamese-500-normal-BEb3_waV.woff +0 -0
  81. package/dist/assets/ibm-plex-sans-vietnamese-500-normal-e4dixQRQ.woff2 +0 -0
  82. package/dist/assets/ibm-plex-sans-vietnamese-600-normal-DgdngZtN.woff +0 -0
  83. package/dist/assets/ibm-plex-sans-vietnamese-600-normal-DpPYBSTl.woff2 +0 -0
  84. package/dist/assets/ibm-plex-serif-cyrillic-400-italic-C_ad97oI.woff2 +0 -0
  85. package/dist/assets/ibm-plex-serif-cyrillic-400-italic-CygxzOWU.woff +0 -0
  86. package/dist/assets/ibm-plex-serif-cyrillic-400-normal-C7IY3oUc.woff +0 -0
  87. package/dist/assets/ibm-plex-serif-cyrillic-400-normal-CPQ8oqB-.woff2 +0 -0
  88. package/dist/assets/ibm-plex-serif-cyrillic-ext-400-italic-CPw2or01.woff +0 -0
  89. package/dist/assets/ibm-plex-serif-cyrillic-ext-400-italic-o20Cx6Xj.woff2 +0 -0
  90. package/dist/assets/ibm-plex-serif-cyrillic-ext-400-normal-BcBv-TKp.woff +0 -0
  91. package/dist/assets/ibm-plex-serif-cyrillic-ext-400-normal-CxUI4jC_.woff2 +0 -0
  92. package/dist/assets/ibm-plex-serif-latin-400-italic-BCf4TsCA.woff2 +0 -0
  93. package/dist/assets/ibm-plex-serif-latin-400-italic-Dd68USph.woff +0 -0
  94. package/dist/assets/ibm-plex-serif-latin-400-normal-BB-zNvJB.woff +0 -0
  95. package/dist/assets/ibm-plex-serif-latin-400-normal-BIGslYFI.woff2 +0 -0
  96. package/dist/assets/ibm-plex-serif-latin-ext-400-italic-4IJS-XHX.woff +0 -0
  97. package/dist/assets/ibm-plex-serif-latin-ext-400-italic-hOoDEQwh.woff2 +0 -0
  98. package/dist/assets/ibm-plex-serif-latin-ext-400-normal-CNMooFZX.woff2 +0 -0
  99. package/dist/assets/ibm-plex-serif-latin-ext-400-normal-DwktX9jl.woff +0 -0
  100. package/dist/assets/ibm-plex-serif-vietnamese-400-italic-1VBVfWB7.woff +0 -0
  101. package/dist/assets/ibm-plex-serif-vietnamese-400-italic-BSp0Db6W.woff2 +0 -0
  102. package/dist/assets/ibm-plex-serif-vietnamese-400-normal-BY9Vij9A.woff +0 -0
  103. package/dist/assets/ibm-plex-serif-vietnamese-400-normal-DGubAMUE.woff2 +0 -0
  104. package/dist/assets/index-BAji2qJH.css +1 -0
  105. package/dist/assets/index-D9hEIdX7.js +326 -0
  106. package/dist/icon.svg +3 -0
  107. package/dist/index.html +14 -0
  108. package/dist/logo-dark.svg +22 -0
  109. package/dist/logo.svg +22 -0
  110. 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.
@@ -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
@@ -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
@@ -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