newpr 0.2.0 → 0.4.0
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 +135 -103
- package/package.json +1 -1
- package/src/analyzer/pipeline.ts +12 -11
- package/src/analyzer/progress.ts +2 -0
- package/src/cli/args.ts +1 -1
- package/src/cli/index.ts +8 -2
- package/src/cli/preflight.ts +126 -0
- package/src/github/fetch-pr.ts +11 -1
- package/src/history/store.ts +1 -0
- package/src/history/types.ts +1 -0
- package/src/llm/prompts.ts +110 -19
- package/src/llm/response-parser.ts +13 -1
- package/src/types/config.ts +1 -1
- package/src/types/github.ts +3 -0
- package/src/types/output.ts +6 -0
- package/src/version.ts +23 -0
- package/src/web/client/App.tsx +51 -3
- package/src/web/client/components/AppShell.tsx +180 -39
- package/src/web/client/components/ChatSection.tsx +4 -172
- package/src/web/client/components/DetailPane.tsx +58 -5
- package/src/web/client/components/DiffViewer.tsx +47 -1
- package/src/web/client/components/InputScreen.tsx +72 -2
- package/src/web/client/components/LoadingTimeline.tsx +19 -6
- package/src/web/client/components/Markdown.tsx +107 -4
- package/src/web/client/components/ResultsScreen.tsx +44 -3
- package/src/web/client/components/ReviewModal.tsx +187 -0
- package/src/web/client/components/SettingsPanel.tsx +63 -87
- package/src/web/client/hooks/useBackgroundAnalyses.ts +147 -0
- package/src/web/client/hooks/useChatStore.ts +244 -0
- package/src/web/client/hooks/useFeatures.ts +2 -1
- package/src/web/client/panels/GroupsPanel.tsx +15 -2
- package/src/web/client/panels/StoryPanel.tsx +1 -1
- package/src/web/index.html +1 -0
- package/src/web/server/routes.ts +195 -16
- package/src/web/server/session-manager.ts +34 -0
- package/src/web/server.ts +37 -4
- package/src/web/styles/built.css +1 -1
- package/src/workspace/agent.ts +22 -6
- package/src/workspace/explore.ts +74 -16
- package/src/workspace/types.ts +1 -0
package/README.md
CHANGED
|
@@ -1,78 +1,86 @@
|
|
|
1
1
|
# newpr
|
|
2
2
|
|
|
3
|
-
AI-powered PR review tool
|
|
3
|
+
AI-powered PR review tool that turns large pull requests into readable, navigable stories.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Quick Install
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
- **Logical grouping** — clusters changed files by purpose (feature, refactor, bugfix, etc.)
|
|
11
|
-
- **Codebase exploration** — uses Claude Code / OpenCode / Codex to analyze the actual repository, not just the diff
|
|
12
|
-
- **Interactive TUI** — Ink-based terminal UI with tabbed panels, slash commands, ASCII logo
|
|
13
|
-
- **Web UI** — browser-based interface with sidebar, resizable panels, markdown rendering, dark/light mode
|
|
14
|
-
- **Streaming progress** — real-time SSE streaming of analysis steps
|
|
15
|
-
- **Session history** — saves past analyses for instant recall
|
|
16
|
-
- **Multi-language** — output in any language (auto-detected or configured)
|
|
7
|
+
```bash
|
|
8
|
+
bunx newpr --web
|
|
9
|
+
```
|
|
17
10
|
|
|
18
|
-
|
|
11
|
+
Or install globally:
|
|
19
12
|
|
|
20
13
|
```bash
|
|
21
|
-
bun
|
|
14
|
+
bun add -g newpr
|
|
15
|
+
newpr --web
|
|
22
16
|
```
|
|
23
17
|
|
|
24
|
-
|
|
18
|
+
The web UI opens automatically at `http://localhost:3000`. Paste any GitHub PR URL to start.
|
|
19
|
+
|
|
20
|
+
### Prerequisites
|
|
21
|
+
|
|
22
|
+
- [Bun](https://bun.sh) — `curl -fsSL https://bun.sh/install | bash`
|
|
23
|
+
- [GitHub CLI](https://cli.github.com) — `brew install gh && gh auth login`
|
|
24
|
+
- One of:
|
|
25
|
+
- `OPENROUTER_API_KEY` — for model selection (Claude, GPT-4, Gemini, etc.)
|
|
26
|
+
- [Claude Code](https://docs.anthropic.com/en/docs/claude-code) — zero-config fallback
|
|
25
27
|
|
|
26
28
|
```bash
|
|
29
|
+
# Option A: OpenRouter
|
|
27
30
|
export OPENROUTER_API_KEY=sk-or-...
|
|
28
|
-
newpr
|
|
31
|
+
newpr --web
|
|
32
|
+
|
|
33
|
+
# Option B: Claude Code (no API key needed)
|
|
34
|
+
newpr --web
|
|
29
35
|
```
|
|
30
36
|
|
|
31
|
-
|
|
37
|
+
## What it Does
|
|
32
38
|
|
|
33
|
-
|
|
39
|
+
newpr fetches a GitHub PR, clones the repo for deep codebase exploration using an agentic coding tool (Claude Code / OpenCode / Codex), then produces:
|
|
34
40
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
41
|
+
- **Narrative walkthrough** — prose-first story with clickable code references that open diffs at the exact line
|
|
42
|
+
- **Logical grouping** — clusters files by purpose (feature, refactor, bugfix) with key changes, risk assessment, and dependency mapping
|
|
43
|
+
- **Interactive chat** — ask follow-up questions with agentic tool execution (file diffs, GitHub API, web search)
|
|
44
|
+
- **Inline diff comments** — create/edit/delete review comments synced to GitHub
|
|
45
|
+
- **PR actions** — approve, request changes, or comment directly from the UI
|
|
46
|
+
- **React Doctor** — auto-runs [react-doctor](https://github.com/millionco/react-doctor) on React projects for code quality scoring
|
|
38
47
|
|
|
39
|
-
###
|
|
48
|
+
### Anchors & Navigation
|
|
40
49
|
|
|
41
|
-
|
|
42
|
-
newpr --web --port 3000
|
|
43
|
-
```
|
|
50
|
+
Every analysis is densely linked. The narrative contains three types of clickable references:
|
|
44
51
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
-
|
|
50
|
-
- GitHub profile integration
|
|
52
|
+
| Anchor | Appearance | Action |
|
|
53
|
+
|--------|------------|--------|
|
|
54
|
+
| `[[group:Name]]` | Blue chip | Opens group detail in sidebar |
|
|
55
|
+
| `[[file:path]]` | Blue chip | Opens file diff in sidebar |
|
|
56
|
+
| `[[line:path#L-L]](text)` | Subtle underline | Opens diff scrolled to line, highlights range |
|
|
51
57
|
|
|
52
58
|
## Usage
|
|
53
59
|
|
|
54
|
-
```
|
|
55
|
-
newpr
|
|
56
|
-
newpr <pr-url>
|
|
57
|
-
newpr --web [--port 3000]
|
|
58
|
-
newpr
|
|
59
|
-
newpr
|
|
60
|
-
newpr
|
|
60
|
+
```bash
|
|
61
|
+
newpr # interactive shell (TUI)
|
|
62
|
+
newpr <pr-url> # shell with PR pre-loaded
|
|
63
|
+
newpr --web [--port 3000] # web UI (default)
|
|
64
|
+
newpr --web --cartoon # web UI with comic strip generation
|
|
65
|
+
newpr review <pr-url> --json # non-interactive JSON output
|
|
66
|
+
newpr history # list past sessions
|
|
67
|
+
newpr auth [--key <api-key>] # configure API key
|
|
61
68
|
```
|
|
62
69
|
|
|
63
|
-
###
|
|
70
|
+
### Options
|
|
64
71
|
|
|
65
72
|
```
|
|
66
|
-
--
|
|
67
|
-
--model <model> Override LLM model (default: anthropic/claude-sonnet-4.5)
|
|
73
|
+
--model <model> LLM model (default: anthropic/claude-sonnet-4.6)
|
|
68
74
|
--agent <tool> Preferred agent: claude | opencode | codex (default: auto)
|
|
69
|
-
--
|
|
75
|
+
--port <port> Web UI port (default: 3000)
|
|
76
|
+
--cartoon Enable comic strip generation
|
|
77
|
+
--no-clone Skip git clone, diff-only analysis
|
|
70
78
|
--json Output raw JSON
|
|
71
|
-
--stream-json Stream progress as NDJSON
|
|
79
|
+
--stream-json Stream progress as NDJSON
|
|
72
80
|
--verbose Show progress on stderr
|
|
73
81
|
```
|
|
74
82
|
|
|
75
|
-
### PR
|
|
83
|
+
### PR Input Formats
|
|
76
84
|
|
|
77
85
|
```bash
|
|
78
86
|
newpr https://github.com/owner/repo/pull/123
|
|
@@ -80,66 +88,71 @@ newpr owner/repo#123
|
|
|
80
88
|
newpr review 123 --repo owner/repo
|
|
81
89
|
```
|
|
82
90
|
|
|
83
|
-
##
|
|
91
|
+
## Web UI
|
|
84
92
|
|
|
85
|
-
|
|
86
|
-
src/
|
|
87
|
-
├── cli/ # CLI entry, arg parsing, auth, history commands
|
|
88
|
-
├── config/ # Config loading (~/.newpr/config.json)
|
|
89
|
-
├── github/ # GitHub API (fetch PR data, diff, parse URL)
|
|
90
|
-
├── diff/ # Unified diff parser + chunker
|
|
91
|
-
├── llm/ # LLM clients (OpenRouter + Claude Code fallback), prompts, response parser
|
|
92
|
-
├── analyzer/ # Pipeline orchestrator + progress events
|
|
93
|
-
├── workspace/ # Agent system (claude/opencode/codex), git operations, codebase exploration
|
|
94
|
-
├── types/ # Shared TypeScript types
|
|
95
|
-
├── history/ # Session persistence (~/.newpr/history/)
|
|
96
|
-
├── tui/ # Ink TUI (shell, panels, theme, slash commands)
|
|
97
|
-
└── web/ # Web UI
|
|
98
|
-
├── server.ts # Bun.serve() with Tailwind CSS build
|
|
99
|
-
├── server/ # REST API + SSE endpoints, session manager
|
|
100
|
-
├── client/ # React frontend
|
|
101
|
-
│ ├── components/ # AppShell, ResultsScreen, Markdown, DetailPane, etc.
|
|
102
|
-
│ ├── panels/ # Story, Summary, Groups, Files, Narrative
|
|
103
|
-
│ └── hooks/ # useAnalysis, useSessions, useTheme, useGithubUser
|
|
104
|
-
└── styles/ # Tailwind v4 + Pretendard font
|
|
105
|
-
```
|
|
93
|
+
The web interface provides:
|
|
106
94
|
|
|
107
|
-
|
|
95
|
+
- **Sidebar** — sessions grouped by repository, background analysis tracking
|
|
96
|
+
- **Story tab** — narrative with inline line anchors + chat input at bottom
|
|
97
|
+
- **Discussion tab** — PR description + GitHub comments
|
|
98
|
+
- **Groups tab** — collapsible change groups with key changes and risk
|
|
99
|
+
- **Files tab** — tree/group/changes view modes with inline summaries
|
|
100
|
+
- **Comic tab** — AI-generated 4-panel comic strip (with `--cartoon`)
|
|
101
|
+
- **Right sidebar** — file diffs with syntax highlighting, inline comments, line highlighting
|
|
102
|
+
- **TipTap editor** — `@` to reference files/groups, `/` for commands
|
|
103
|
+
- **KaTeX** — LaTeX math rendering in chat and narrative
|
|
104
|
+
- **Review modal** — approve, request changes, or comment via GitHub API
|
|
105
|
+
- **Settings** — model, agent, language, API keys
|
|
106
|
+
- **Preflight checks** — system health (gh, agents, API key) on startup
|
|
108
107
|
|
|
109
|
-
|
|
110
|
-
2. **Parse** — unified diff into per-file chunks
|
|
111
|
-
3. **Clone** — bare repo clone with worktree checkout (cached)
|
|
112
|
-
4. **Explore** — 3-phase codebase exploration via agentic tool (structure → related code → issues)
|
|
113
|
-
5. **Analyze** — LLM summarizes each file chunk in parallel batches
|
|
114
|
-
6. **Group** — LLM clusters files into logical groups with types
|
|
115
|
-
7. **Summarize** — LLM generates purpose, scope, impact, risk level
|
|
116
|
-
8. **Narrate** — LLM writes a walkthrough article with cross-references
|
|
108
|
+
## Chat
|
|
117
109
|
|
|
118
|
-
|
|
110
|
+
The chat in the Story tab supports agentic tool execution:
|
|
119
111
|
|
|
120
|
-
|
|
112
|
+
| Tool | Description |
|
|
113
|
+
|------|-------------|
|
|
114
|
+
| `get_file_diff` | Fetch unified diff for a specific file |
|
|
115
|
+
| `list_files` | List all changed files with summaries |
|
|
116
|
+
| `get_pr_comments` | Fetch PR discussion comments |
|
|
117
|
+
| `get_review_comments` | Fetch inline review comments |
|
|
118
|
+
| `get_pr_details` | PR metadata, labels, reviewers |
|
|
119
|
+
| `web_search` | Search the web (delegated to agent) |
|
|
120
|
+
| `web_fetch` | Fetch URL content (delegated to agent) |
|
|
121
|
+
| `run_react_doctor` | Run react-doctor analysis |
|
|
121
122
|
|
|
122
|
-
|
|
123
|
-
|---------|-------|----------|
|
|
124
|
-
| **OpenRouter** | Set `OPENROUTER_API_KEY` | Full model selection (Claude, GPT-4, Gemini, etc.) |
|
|
125
|
-
| **Claude Code** | Install `claude` CLI | Zero-config fallback, uses your existing Claude subscription |
|
|
123
|
+
Type `/undo` to remove the last exchange.
|
|
126
124
|
|
|
127
|
-
|
|
125
|
+
## Analysis Pipeline
|
|
128
126
|
|
|
129
|
-
|
|
127
|
+
1. **Fetch** — PR metadata, commits, diff, and discussion from GitHub API
|
|
128
|
+
2. **Parse** — unified diff into per-file chunks
|
|
129
|
+
3. **Clone** — bare repo with worktree checkout (cached in `~/.newpr/repos/`)
|
|
130
|
+
4. **Explore** — 3-4 phase codebase exploration via agent:
|
|
131
|
+
- Structure — project type, architecture
|
|
132
|
+
- Related code — imports, usages, tests
|
|
133
|
+
- Issues — breaking changes, inconsistencies
|
|
134
|
+
- React Doctor — code quality score (React projects only)
|
|
135
|
+
5. **Analyze** — LLM summarizes each file in parallel batches
|
|
136
|
+
6. **Group** — LLM clusters files with key changes, risk, dependencies
|
|
137
|
+
7. **Summarize** — purpose, scope, impact, risk level
|
|
138
|
+
8. **Narrate** — prose walkthrough with line-level code references
|
|
139
|
+
|
|
140
|
+
## LLM Backends
|
|
141
|
+
|
|
142
|
+
| Backend | Setup | Use case |
|
|
143
|
+
|---------|-------|----------|
|
|
144
|
+
| **OpenRouter** | `OPENROUTER_API_KEY` | Full model selection |
|
|
145
|
+
| **Claude Code** | `claude` CLI installed | Zero-config fallback |
|
|
130
146
|
|
|
131
|
-
|
|
147
|
+
## Exploration Agents
|
|
132
148
|
|
|
133
|
-
| Agent |
|
|
149
|
+
| Agent | Install | Detection |
|
|
134
150
|
|-------|---------|-----------|
|
|
135
|
-
| Claude Code | `claude` | `which claude` |
|
|
136
|
-
| OpenCode | `opencode` | `which opencode` |
|
|
137
|
-
| Codex | `codex` | `which codex` |
|
|
151
|
+
| Claude Code | `npm i -g @anthropic-ai/claude-code` | `which claude` |
|
|
152
|
+
| OpenCode | `npm i -g opencode` | `which opencode` |
|
|
153
|
+
| Codex | `npm i -g @openai/codex` | `which codex` |
|
|
138
154
|
|
|
139
|
-
|
|
140
|
-
1. **Structure** — project type, key directories, architecture pattern
|
|
141
|
-
2. **Related code** — imports, usages, test coverage for changed files
|
|
142
|
-
3. **Issues** — breaking changes, missing error handling, inconsistencies
|
|
155
|
+
Agents run with read-only tools (Read, Glob, Grep, Bash, WebSearch, WebFetch). No write operations.
|
|
143
156
|
|
|
144
157
|
## Environment Variables
|
|
145
158
|
|
|
@@ -147,19 +160,19 @@ The agent runs 3 exploration phases:
|
|
|
147
160
|
|----------|----------|-------------|
|
|
148
161
|
| `OPENROUTER_API_KEY` | No* | OpenRouter API key (*falls back to Claude Code) |
|
|
149
162
|
| `GITHUB_TOKEN` | No | GitHub token (falls back to `gh` CLI) |
|
|
150
|
-
| `NEWPR_MODEL` | No | Default model (default: `anthropic/claude-sonnet-4.
|
|
163
|
+
| `NEWPR_MODEL` | No | Default model (default: `anthropic/claude-sonnet-4.6`) |
|
|
151
164
|
| `NEWPR_MAX_FILES` | No | Max files to analyze (default: 100) |
|
|
152
165
|
| `NEWPR_TIMEOUT` | No | Timeout per LLM call in seconds (default: 120) |
|
|
153
166
|
| `NEWPR_CONCURRENCY` | No | Parallel LLM calls (default: 5) |
|
|
154
167
|
|
|
155
|
-
## Config
|
|
168
|
+
## Config
|
|
156
169
|
|
|
157
|
-
Persistent settings
|
|
170
|
+
Persistent settings in `~/.newpr/config.json`:
|
|
158
171
|
|
|
159
172
|
```json
|
|
160
173
|
{
|
|
161
174
|
"openrouter_api_key": "sk-or-...",
|
|
162
|
-
"model": "anthropic/claude-sonnet-4.
|
|
175
|
+
"model": "anthropic/claude-sonnet-4.6",
|
|
163
176
|
"language": "auto",
|
|
164
177
|
"agent": "claude",
|
|
165
178
|
"max_files": 100,
|
|
@@ -171,19 +184,38 @@ Persistent settings are stored in `~/.newpr/config.json`:
|
|
|
171
184
|
## Development
|
|
172
185
|
|
|
173
186
|
```bash
|
|
187
|
+
git clone https://github.com/jiwonMe/newpr
|
|
188
|
+
cd newpr
|
|
174
189
|
bun install
|
|
175
|
-
bun test #
|
|
190
|
+
bun test # 91 tests
|
|
176
191
|
bun run typecheck # tsc --noEmit
|
|
177
|
-
bun run lint # biome check
|
|
178
192
|
bun run start # launch CLI
|
|
179
193
|
```
|
|
180
194
|
|
|
181
|
-
##
|
|
195
|
+
## Architecture
|
|
182
196
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
197
|
+
```
|
|
198
|
+
src/
|
|
199
|
+
├── cli/ # CLI entry, args, auth, preflight, update-check
|
|
200
|
+
├── config/ # Config loading (~/.newpr/config.json)
|
|
201
|
+
├── github/ # GitHub API (PR data, diff, comments)
|
|
202
|
+
├── diff/ # Unified diff parser + chunker
|
|
203
|
+
├── llm/ # LLM clients (OpenRouter + Claude Code), prompts, parser
|
|
204
|
+
├── analyzer/ # Pipeline orchestrator + progress events
|
|
205
|
+
├── workspace/ # Agent system, git operations, codebase exploration
|
|
206
|
+
├── types/ # Shared TypeScript types
|
|
207
|
+
├── history/ # Session persistence + sidecar files
|
|
208
|
+
├── tui/ # Ink TUI (shell, panels, theme)
|
|
209
|
+
└── web/ # Web UI
|
|
210
|
+
├── server.ts # Bun.serve()
|
|
211
|
+
├── server/ # REST/SSE API, session manager
|
|
212
|
+
├── client/ # React frontend
|
|
213
|
+
│ ├── components/ # AppShell, ChatSection, Markdown, TipTapEditor, etc.
|
|
214
|
+
│ ├── panels/ # Story, Discussion, Groups, Files, Cartoon
|
|
215
|
+
│ └── hooks/ # useAnalysis, useBackgroundAnalyses, useChatState, etc.
|
|
216
|
+
└── styles/ # Tailwind v4 + Pretendard + Tab0 Mono K
|
|
217
|
+
```
|
|
186
218
|
|
|
187
219
|
## License
|
|
188
220
|
|
|
189
|
-
|
|
221
|
+
MIT
|
package/package.json
CHANGED
package/src/analyzer/pipeline.ts
CHANGED
|
@@ -85,25 +85,21 @@ async function runExploration(
|
|
|
85
85
|
): Promise<ExplorationResult> {
|
|
86
86
|
const agent = await requireAgent(preferredAgent);
|
|
87
87
|
|
|
88
|
-
onProgress?.({ stage: "cloning", message: `${pr.owner}/${pr.repo}` });
|
|
89
88
|
const bareRepoPath = await ensureRepo(pr.owner, pr.repo, token, (msg) => {
|
|
90
|
-
onProgress?.({ stage: "cloning", message: msg });
|
|
89
|
+
onProgress?.({ stage: "cloning", message: `📦 ${msg}` });
|
|
91
90
|
});
|
|
92
|
-
onProgress?.({ stage: "cloning", message: `${pr.owner}/${pr.repo} ready` });
|
|
93
91
|
|
|
94
|
-
onProgress?.({ stage: "checkout", message: `${baseBranch} ← PR #${pr.number}` });
|
|
95
92
|
const worktrees = await createWorktrees(
|
|
96
93
|
bareRepoPath, baseBranch, pr.number, pr.owner, pr.repo,
|
|
97
|
-
(msg) => onProgress?.({ stage: "checkout", message: msg }),
|
|
94
|
+
(msg) => onProgress?.({ stage: "checkout", message: `🌿 ${msg}` }),
|
|
98
95
|
);
|
|
99
|
-
onProgress?.({ stage: "checkout", message: `${baseBranch} ← PR #${pr.number} worktrees ready` });
|
|
100
96
|
|
|
101
|
-
onProgress?.({ stage: "exploring", message:
|
|
97
|
+
onProgress?.({ stage: "exploring", message: `🤖 ${agent.name}: exploring ${changedFiles.length} changed files...` });
|
|
102
98
|
const exploration = await exploreCodebase(
|
|
103
99
|
agent, worktrees.headPath, changedFiles, prTitle, rawDiff,
|
|
104
100
|
(msg, current, total) => onProgress?.({ stage: "exploring", message: msg, current, total }),
|
|
105
101
|
);
|
|
106
|
-
onProgress?.({ stage: "exploring", message:
|
|
102
|
+
onProgress?.({ stage: "exploring", message: `🤖 ${agent.name}: exploration complete` });
|
|
107
103
|
|
|
108
104
|
await cleanupWorktrees(bareRepoPath, pr.number, pr.owner, pr.repo).catch(() => {});
|
|
109
105
|
|
|
@@ -165,7 +161,7 @@ export async function analyzePr(options: PipelineOptions): Promise<NewprOutput>
|
|
|
165
161
|
fetchPrDiff(pr, token),
|
|
166
162
|
fetchPrComments(pr, token).catch(() => []),
|
|
167
163
|
]);
|
|
168
|
-
progress({ stage: "fetching", message: `#${prData.number} "${prData.title}" by ${prData.author} · +${prData.additions} −${prData.deletions} · ${prComments.length} comments
|
|
164
|
+
progress({ stage: "fetching", message: `#${prData.number} "${prData.title}" by ${prData.author} · +${prData.additions} −${prData.deletions} · ${prComments.length} comments`, pr_title: prData.title, pr_number: prData.number });
|
|
169
165
|
|
|
170
166
|
progress({ stage: "parsing", message: "Parsing diff..." });
|
|
171
167
|
const parsed = parseDiff(rawDiff);
|
|
@@ -259,9 +255,13 @@ export async function analyzePr(options: PipelineOptions): Promise<NewprOutput>
|
|
|
259
255
|
progress({ stage: "summarizing", message: `${summary.risk_level} risk · ${summary.purpose.slice(0, 60)}` });
|
|
260
256
|
|
|
261
257
|
progress({ stage: "narrating", message: `Writing narrative${enrichedTag}...` });
|
|
258
|
+
const fileDiffs = chunks.slice(0, 30).map((c) => ({
|
|
259
|
+
path: c.file_path,
|
|
260
|
+
diff: c.diff_content.length > 3000 ? `${c.diff_content.slice(0, 3000)}\n... (truncated)` : c.diff_content,
|
|
261
|
+
}));
|
|
262
262
|
const narrativePrompt = exploration
|
|
263
|
-
? buildEnrichedNarrativePrompt(prData.title, summary, groups, exploration, promptCtx)
|
|
264
|
-
: buildNarrativePrompt(prData.title, summary, groups, promptCtx);
|
|
263
|
+
? buildEnrichedNarrativePrompt(prData.title, summary, groups, exploration, promptCtx, fileDiffs)
|
|
264
|
+
: buildNarrativePrompt(prData.title, summary, groups, promptCtx, fileDiffs);
|
|
265
265
|
const narrativeResponse = await streamLlmCall(
|
|
266
266
|
client, narrativePrompt.system, narrativePrompt.user, "narrating", "Writing narrative...", progress,
|
|
267
267
|
);
|
|
@@ -297,6 +297,7 @@ export async function analyzePr(options: PipelineOptions): Promise<NewprOutput>
|
|
|
297
297
|
pr_title: prData.title,
|
|
298
298
|
pr_body: prData.body || undefined,
|
|
299
299
|
pr_url: prData.url,
|
|
300
|
+
pr_state: prData.state,
|
|
300
301
|
base_branch: prData.base_branch,
|
|
301
302
|
head_branch: prData.head_branch,
|
|
302
303
|
author: prData.author,
|
package/src/analyzer/progress.ts
CHANGED
package/src/cli/args.ts
CHANGED
|
@@ -46,7 +46,7 @@ Options:
|
|
|
46
46
|
|
|
47
47
|
Options (review mode):
|
|
48
48
|
--repo <owner/repo> Repository (required when using PR number only)
|
|
49
|
-
--model <model> Override LLM model (default: anthropic/claude-sonnet-4.
|
|
49
|
+
--model <model> Override LLM model (default: anthropic/claude-sonnet-4.6)
|
|
50
50
|
--agent <tool> Preferred agent: claude | opencode | codex (default: auto)
|
|
51
51
|
--no-clone Skip git clone, diff-only analysis (faster, less context)
|
|
52
52
|
--json Output raw JSON (for piping/scripting)
|
package/src/cli/index.ts
CHANGED
|
@@ -10,8 +10,10 @@ import { analyzePr } from "../analyzer/pipeline.ts";
|
|
|
10
10
|
import { createStderrProgress, createSilentProgress, createStreamJsonProgress } from "../analyzer/progress.ts";
|
|
11
11
|
import { renderLoading, renderShell } from "../tui/render.tsx";
|
|
12
12
|
import { checkForUpdate, printUpdateNotice } from "./update-check.ts";
|
|
13
|
+
import { runPreflight, printPreflight } from "./preflight.ts";
|
|
14
|
+
import { getVersion } from "../version.ts";
|
|
13
15
|
|
|
14
|
-
const VERSION =
|
|
16
|
+
const VERSION = getVersion();
|
|
15
17
|
|
|
16
18
|
async function main(): Promise<void> {
|
|
17
19
|
const args = parseArgs(process.argv);
|
|
@@ -51,12 +53,14 @@ async function main(): Promise<void> {
|
|
|
51
53
|
|
|
52
54
|
if (args.command === "web") {
|
|
53
55
|
try {
|
|
56
|
+
const preflight = await runPreflight();
|
|
57
|
+
printPreflight(preflight);
|
|
54
58
|
const config = await loadConfig({ model: args.model });
|
|
55
59
|
const token = await getGithubToken();
|
|
56
60
|
const updateInfo = await updatePromise;
|
|
57
61
|
if (updateInfo) printUpdateNotice(updateInfo);
|
|
58
62
|
const { startWebServer } = await import("../web/server.ts");
|
|
59
|
-
await startWebServer({ port: args.port ?? 3000, token, config, cartoon: args.cartoon });
|
|
63
|
+
await startWebServer({ port: args.port ?? 3000, token, config, cartoon: args.cartoon, preflight });
|
|
60
64
|
} catch (error) {
|
|
61
65
|
const message = error instanceof Error ? error.message : String(error);
|
|
62
66
|
process.stderr.write(`Error: ${message}\n`);
|
|
@@ -67,6 +71,8 @@ async function main(): Promise<void> {
|
|
|
67
71
|
|
|
68
72
|
if (args.command === "shell") {
|
|
69
73
|
try {
|
|
74
|
+
const preflight = await runPreflight();
|
|
75
|
+
printPreflight(preflight);
|
|
70
76
|
const config = await loadConfig({ model: args.model });
|
|
71
77
|
const token = await getGithubToken();
|
|
72
78
|
const updateInfo = await updatePromise;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { AgentToolName } from "../workspace/types.ts";
|
|
2
|
+
|
|
3
|
+
export interface ToolStatus {
|
|
4
|
+
name: string;
|
|
5
|
+
installed: boolean;
|
|
6
|
+
version?: string;
|
|
7
|
+
detail?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface PreflightResult {
|
|
11
|
+
github: ToolStatus & { authenticated: boolean; user?: string };
|
|
12
|
+
agents: ToolStatus[];
|
|
13
|
+
openrouterKey: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function which(cmd: string): Promise<string | null> {
|
|
17
|
+
try {
|
|
18
|
+
const result = await Bun.$`which ${cmd}`.text();
|
|
19
|
+
return result.trim() || null;
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function getVersion(cmd: string, flag = "--version"): Promise<string | null> {
|
|
26
|
+
try {
|
|
27
|
+
const result = await Bun.$`${cmd} ${flag} 2>&1`.text();
|
|
28
|
+
const match = result.match(/[\d]+\.[\d]+[\d.]*/);
|
|
29
|
+
return match?.[0] ?? result.trim().slice(0, 30);
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function checkGithubCli(): Promise<PreflightResult["github"]> {
|
|
36
|
+
const path = await which("gh");
|
|
37
|
+
if (!path) {
|
|
38
|
+
return { name: "gh", installed: false, authenticated: false, detail: "brew install gh" };
|
|
39
|
+
}
|
|
40
|
+
const version = await getVersion("gh");
|
|
41
|
+
try {
|
|
42
|
+
const status = await Bun.$`gh auth status 2>&1`.text();
|
|
43
|
+
const userMatch = status.match(/Logged in to github\.com account (\S+)/i)
|
|
44
|
+
?? status.match(/account (\S+)/i);
|
|
45
|
+
return {
|
|
46
|
+
name: "gh",
|
|
47
|
+
installed: true,
|
|
48
|
+
version: version ?? undefined,
|
|
49
|
+
authenticated: true,
|
|
50
|
+
user: userMatch?.[1]?.replace(/\s*\(.*/, ""),
|
|
51
|
+
};
|
|
52
|
+
} catch {
|
|
53
|
+
return { name: "gh", installed: true, version: version ?? undefined, authenticated: false, detail: "gh auth login" };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function checkAgent(name: AgentToolName): Promise<ToolStatus> {
|
|
58
|
+
const path = await which(name);
|
|
59
|
+
if (!path) return { name, installed: false };
|
|
60
|
+
const version = await getVersion(name);
|
|
61
|
+
return { name, installed: true, version: version ?? undefined };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function runPreflight(): Promise<PreflightResult> {
|
|
65
|
+
const [github, claude, opencode, codex] = await Promise.all([
|
|
66
|
+
checkGithubCli(),
|
|
67
|
+
checkAgent("claude"),
|
|
68
|
+
checkAgent("opencode"),
|
|
69
|
+
checkAgent("codex"),
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
github,
|
|
74
|
+
agents: [claude, opencode, codex],
|
|
75
|
+
openrouterKey: !!(process.env.OPENROUTER_API_KEY || await hasStoredApiKey()),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function hasStoredApiKey(): Promise<boolean> {
|
|
80
|
+
try {
|
|
81
|
+
const { readStoredConfig } = await import("../config/store.ts");
|
|
82
|
+
const stored = await readStoredConfig();
|
|
83
|
+
return !!stored.openrouter_api_key;
|
|
84
|
+
} catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function printPreflight(result: PreflightResult): void {
|
|
90
|
+
const check = "\x1b[32m✓\x1b[0m";
|
|
91
|
+
const cross = "\x1b[31m✗\x1b[0m";
|
|
92
|
+
const dim = (s: string) => `\x1b[2m${s}\x1b[0m`;
|
|
93
|
+
const bold = (s: string) => `\x1b[1m${s}\x1b[0m`;
|
|
94
|
+
|
|
95
|
+
console.log("");
|
|
96
|
+
console.log(` ${bold("Preflight")}`);
|
|
97
|
+
console.log("");
|
|
98
|
+
|
|
99
|
+
const gh = result.github;
|
|
100
|
+
if (gh.installed && gh.authenticated) {
|
|
101
|
+
console.log(` ${check} gh ${dim(gh.version ?? "")} ${dim(`· ${gh.user ?? ""}`)}`);
|
|
102
|
+
} else if (gh.installed) {
|
|
103
|
+
console.log(` ${cross} gh ${dim(gh.version ?? "")} ${dim("· not authenticated")}`);
|
|
104
|
+
console.log(` ${dim(`run: ${gh.detail}`)}`);
|
|
105
|
+
} else {
|
|
106
|
+
console.log(` ${cross} gh ${dim("· not installed")}`);
|
|
107
|
+
console.log(` ${dim(`run: ${gh.detail}`)}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
for (const agent of result.agents) {
|
|
111
|
+
if (agent.installed) {
|
|
112
|
+
console.log(` ${check} ${agent.name} ${dim(agent.version ?? "")}`);
|
|
113
|
+
} else {
|
|
114
|
+
console.log(` ${dim("·")} ${dim(agent.name)} ${dim("not found")}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (result.openrouterKey) {
|
|
119
|
+
console.log(` ${check} OpenRouter API key`);
|
|
120
|
+
} else {
|
|
121
|
+
console.log(` ${cross} OpenRouter API key ${dim("· not configured")}`);
|
|
122
|
+
console.log(` ${dim("run: newpr auth")}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
console.log("");
|
|
126
|
+
}
|
package/src/github/fetch-pr.ts
CHANGED
|
@@ -1,15 +1,25 @@
|
|
|
1
|
-
import type { GithubPrData, PrComment, PrCommit, PrIdentifier } from "../types/github.ts";
|
|
1
|
+
import type { GithubPrData, PrComment, PrCommit, PrIdentifier, PrState } from "../types/github.ts";
|
|
2
2
|
|
|
3
3
|
export function mapPrResponse(json: Record<string, unknown>): Omit<GithubPrData, "commits"> {
|
|
4
4
|
const user = json.user as Record<string, unknown> | undefined;
|
|
5
5
|
const base = json.base as Record<string, unknown> | undefined;
|
|
6
6
|
const head = json.head as Record<string, unknown> | undefined;
|
|
7
7
|
|
|
8
|
+
let state: PrState = "open";
|
|
9
|
+
if (json.draft) {
|
|
10
|
+
state = "draft";
|
|
11
|
+
} else if (json.merged) {
|
|
12
|
+
state = "merged";
|
|
13
|
+
} else if (json.state === "closed") {
|
|
14
|
+
state = "closed";
|
|
15
|
+
}
|
|
16
|
+
|
|
8
17
|
return {
|
|
9
18
|
number: json.number as number,
|
|
10
19
|
title: json.title as string,
|
|
11
20
|
body: (json.body as string) ?? "",
|
|
12
21
|
url: json.html_url as string,
|
|
22
|
+
state,
|
|
13
23
|
base_branch: (base?.ref as string) ?? "unknown",
|
|
14
24
|
head_branch: (head?.ref as string) ?? "unknown",
|
|
15
25
|
author: (user?.login as string) ?? "unknown",
|
package/src/history/store.ts
CHANGED
|
@@ -25,6 +25,7 @@ export function buildSessionRecord(id: string, data: NewprOutput): SessionRecord
|
|
|
25
25
|
pr_url: meta.pr_url,
|
|
26
26
|
pr_number: meta.pr_number,
|
|
27
27
|
pr_title: meta.pr_title,
|
|
28
|
+
pr_state: meta.pr_state,
|
|
28
29
|
repo: repoParts?.[1] ?? "unknown",
|
|
29
30
|
author: meta.author,
|
|
30
31
|
analyzed_at: meta.analyzed_at,
|