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.
Files changed (40) hide show
  1. package/README.md +135 -103
  2. package/package.json +1 -1
  3. package/src/analyzer/pipeline.ts +12 -11
  4. package/src/analyzer/progress.ts +2 -0
  5. package/src/cli/args.ts +1 -1
  6. package/src/cli/index.ts +8 -2
  7. package/src/cli/preflight.ts +126 -0
  8. package/src/github/fetch-pr.ts +11 -1
  9. package/src/history/store.ts +1 -0
  10. package/src/history/types.ts +1 -0
  11. package/src/llm/prompts.ts +110 -19
  12. package/src/llm/response-parser.ts +13 -1
  13. package/src/types/config.ts +1 -1
  14. package/src/types/github.ts +3 -0
  15. package/src/types/output.ts +6 -0
  16. package/src/version.ts +23 -0
  17. package/src/web/client/App.tsx +51 -3
  18. package/src/web/client/components/AppShell.tsx +180 -39
  19. package/src/web/client/components/ChatSection.tsx +4 -172
  20. package/src/web/client/components/DetailPane.tsx +58 -5
  21. package/src/web/client/components/DiffViewer.tsx +47 -1
  22. package/src/web/client/components/InputScreen.tsx +72 -2
  23. package/src/web/client/components/LoadingTimeline.tsx +19 -6
  24. package/src/web/client/components/Markdown.tsx +107 -4
  25. package/src/web/client/components/ResultsScreen.tsx +44 -3
  26. package/src/web/client/components/ReviewModal.tsx +187 -0
  27. package/src/web/client/components/SettingsPanel.tsx +63 -87
  28. package/src/web/client/hooks/useBackgroundAnalyses.ts +147 -0
  29. package/src/web/client/hooks/useChatStore.ts +244 -0
  30. package/src/web/client/hooks/useFeatures.ts +2 -1
  31. package/src/web/client/panels/GroupsPanel.tsx +15 -2
  32. package/src/web/client/panels/StoryPanel.tsx +1 -1
  33. package/src/web/index.html +1 -0
  34. package/src/web/server/routes.ts +195 -16
  35. package/src/web/server/session-manager.ts +34 -0
  36. package/src/web/server.ts +37 -4
  37. package/src/web/styles/built.css +1 -1
  38. package/src/workspace/agent.ts +22 -6
  39. package/src/workspace/explore.ts +74 -16
  40. 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 for understanding large pull requests with 1000+ lines of changes.
3
+ AI-powered PR review tool that turns large pull requests into readable, navigable stories.
4
4
 
5
- newpr fetches a GitHub PR, optionally clones the repo for deep codebase exploration using an agentic coding tool, then uses an LLM to produce a structured analysis: file summaries, logical groupings, an overall summary, and a narrative walkthrough with clickable cross-references.
5
+ ## Quick Install
6
6
 
7
- ## Features
8
-
9
- - **Narrative walkthrough** — reads like an article, with `[[group:...]]` and `[[file:...]]` cross-references
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
- ## Quick Start
11
+ Or install globally:
19
12
 
20
13
  ```bash
21
- bun install
14
+ bun add -g newpr
15
+ newpr --web
22
16
  ```
23
17
 
24
- ### Option A: OpenRouter API key
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 https://github.com/owner/repo/pull/123
31
+ newpr --web
32
+
33
+ # Option B: Claude Code (no API key needed)
34
+ newpr --web
29
35
  ```
30
36
 
31
- ### Option B: Claude Code (no API key needed)
37
+ ## What it Does
32
38
 
33
- If you have [Claude Code](https://docs.anthropic.com/en/docs/claude-code) installed, newpr uses it as a fallback when no OpenRouter API key is set for both LLM analysis and codebase exploration.
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
- ```bash
36
- newpr https://github.com/owner/repo/pull/123
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
- ### Web UI
48
+ ### Anchors & Navigation
40
49
 
41
- ```bash
42
- newpr --web --port 3000
43
- ```
50
+ Every analysis is densely linked. The narrative contains three types of clickable references:
44
51
 
45
- Opens a browser-based UI at `http://localhost:3000` with:
46
- - Left sidebar with session history
47
- - Resizable detail panel on the right
48
- - Clickable group/file anchors in the narrative
49
- - Settings modal for model, agent, language configuration
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 # launch interactive shell
56
- newpr <pr-url> # shell with PR pre-loaded
57
- newpr --web [--port 3000] # launch web UI
58
- newpr review <pr-url> --json # non-interactive JSON output
59
- newpr history # list past review sessions
60
- newpr auth [--key <api-key>] # configure API key
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
- ### Review mode options
70
+ ### Options
64
71
 
65
72
  ```
66
- --repo <owner/repo> Repository (when using PR number only)
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
- --no-clone Skip git clone, diff-only analysis (faster, less context)
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, then emit result
79
+ --stream-json Stream progress as NDJSON
72
80
  --verbose Show progress on stderr
73
81
  ```
74
82
 
75
- ### PR input formats
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
- ## Architecture
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
- ## Analysis Pipeline
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
- 1. **Fetch** — PR metadata, commits, and diff from GitHub API
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
- ## LLM Backend
110
+ The chat in the Story tab supports agentic tool execution:
119
111
 
120
- newpr supports two LLM backends:
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
- | Backend | Setup | Use case |
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
- When no OpenRouter API key is configured, newpr automatically falls back to Claude Code for all LLM calls.
125
+ ## Analysis Pipeline
128
126
 
129
- ## Codebase Exploration Agents
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
- For deep analysis beyond the diff, newpr uses an agentic coding tool to explore the actual repository:
147
+ ## Exploration Agents
132
148
 
133
- | Agent | Command | Detection |
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
- The agent runs 3 exploration phases:
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.5`) |
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 File
168
+ ## Config
156
169
 
157
- Persistent settings are stored in `~/.newpr/config.json`:
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.5",
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 # run tests (91 tests)
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
- ## Requirements
195
+ ## Architecture
182
196
 
183
- - [Bun](https://bun.sh) ≥ 1.3
184
- - GitHub CLI (`gh`) for authentication, or `GITHUB_TOKEN`
185
- - One of: `OPENROUTER_API_KEY` or Claude Code (`claude` CLI)
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
- Private
221
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "newpr",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "AI-powered large PR review tool - understand PRs with 1000+ lines of changes",
5
5
  "module": "src/cli/index.ts",
6
6
  "type": "module",
@@ -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: `${agent.name}: analyzing ${changedFiles.length} files...` });
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: `${agent.name}: exploration complete` });
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,
@@ -17,6 +17,8 @@ export interface ProgressEvent {
17
17
  total?: number;
18
18
  partial_content?: string;
19
19
  timestamp?: number;
20
+ pr_title?: string;
21
+ pr_number?: number;
20
22
  }
21
23
 
22
24
  export type ProgressCallback = (event: ProgressEvent) => void;
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.5)
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 = "0.1.3";
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
+ }
@@ -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",
@@ -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,
@@ -3,6 +3,7 @@ export interface SessionRecord {
3
3
  pr_url: string;
4
4
  pr_number: number;
5
5
  pr_title: string;
6
+ pr_state?: string;
6
7
  repo: string;
7
8
  author: string;
8
9
  analyzed_at: string;