@aaronshaf/ger 0.1.4 → 0.1.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aaronshaf/ger",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "module": "index.ts",
5
5
  "type": "module",
6
6
  "bin": {
@@ -89,7 +89,7 @@ interface ReviewOptions {
89
89
  comment?: boolean
90
90
  yes?: boolean
91
91
  prompt?: string
92
- provider?: string
92
+ tool?: string
93
93
  systemPrompt?: string
94
94
  }
95
95
 
@@ -331,12 +331,12 @@ export const reviewCommand = (changeId: string, options: ReviewOptions = {}) =>
331
331
 
332
332
  if (availableStrategies.length === 0) {
333
333
  return yield* Effect.fail(
334
- new Error('No AI tools available. Please install claude, gemini, or codex CLI.'),
334
+ new Error('No AI tools available. Please install claude, gemini, or opencode CLI.'),
335
335
  )
336
336
  }
337
337
 
338
338
  // Select strategy based on user preference
339
- const selectedStrategy = yield* reviewStrategy.selectStrategy(options.provider)
339
+ const selectedStrategy = yield* reviewStrategy.selectStrategy(options.tool)
340
340
  yield* Console.log(`✓ Using AI tool: ${selectedStrategy.name}`)
341
341
 
342
342
  // Load custom review prompt if provided
package/src/cli/index.ts CHANGED
@@ -368,15 +368,12 @@ program
368
368
  .option('-y, --yes', 'Skip confirmation prompts when posting comments')
369
369
  .option('--debug', 'Show debug output including AI responses')
370
370
  .option('--prompt <file>', 'Path to custom review prompt file (e.g., ~/prompts/review.md)')
371
- .option(
372
- '--provider <provider>',
373
- 'Preferred AI provider (claude-sdk, claude, gemini, codex, opencode)',
374
- )
371
+ .option('--tool <tool>', 'Preferred AI tool (claude, gemini, opencode)')
375
372
  .option('--system-prompt <prompt>', 'Custom system prompt for the AI')
376
373
  .addHelpText(
377
374
  'after',
378
375
  `
379
- This command uses AI (Claude SDK, claude CLI, gemini CLI, codex CLI, or opencode CLI) to review a Gerrit change.
376
+ This command uses AI (claude CLI, gemini CLI, or opencode CLI) to review a Gerrit change.
380
377
  It performs a two-stage review process:
381
378
 
382
379
  1. Generates inline comments for specific code issues
@@ -387,7 +384,7 @@ Use --comment to post the review to Gerrit (with confirmation prompts).
387
384
  Use --comment --yes to post without confirmation.
388
385
 
389
386
  Requirements:
390
- - One of these AI tools must be available: Claude SDK (ANTHROPIC_API_KEY), claude CLI, gemini CLI, codex CLI, or opencode CLI
387
+ - One of these AI tools must be available: claude CLI, gemini CLI, or opencode CLI
391
388
  - Gerrit credentials must be configured (run 'ger setup' first)
392
389
 
393
390
  Examples:
@@ -400,6 +397,9 @@ Examples:
400
397
  # Review and auto-post comments without prompting
401
398
  $ ger review 12345 --comment --yes
402
399
 
400
+ # Use specific AI tool
401
+ $ ger review 12345 --tool gemini
402
+
403
403
  # Show debug output to troubleshoot issues
404
404
  $ ger review 12345 --debug
405
405
  `,
@@ -411,7 +411,7 @@ Examples:
411
411
  yes: options.yes,
412
412
  debug: options.debug,
413
413
  prompt: options.prompt,
414
- provider: options.provider,
414
+ tool: options.tool,
415
415
  systemPrompt: options.systemPrompt,
416
416
  }).pipe(
417
417
  Effect.provide(ReviewStrategyServiceLive),
@@ -1,9 +1,15 @@
1
- ## CRITICAL OUTPUT REQUIREMENT
1
+ ## IMMEDIATE TASK: ANALYZE CODE AND GENERATE INLINE COMMENTS
2
2
 
3
- **YOUR ENTIRE OUTPUT MUST BE WRAPPED IN <response></response> TAGS.**
4
- **NEVER USE BACKTICKS ANYWHERE IN YOUR RESPONSE - they cause shell execution errors.**
3
+ **YOU MUST ANALYZE THE PROVIDED CODE CHANGES RIGHT NOW AND GENERATE INLINE COMMENTS.**
5
4
 
6
- Output ONLY a JSON array wrapped in response tags. **EMPTY ARRAY IS PERFECTLY VALID** for clean code without issues. No other text before or after the tags.
5
+ **CRITICAL OUTPUT REQUIREMENT:**
6
+ - YOUR ENTIRE OUTPUT MUST BE WRAPPED IN <response></response> TAGS
7
+ - NEVER USE BACKTICKS ANYWHERE IN YOUR RESPONSE - they cause shell execution errors
8
+ - Output ONLY a JSON array wrapped in response tags
9
+ - **EMPTY ARRAY IS PERFECTLY VALID** for clean code without issues
10
+ - No other text before or after the tags
11
+
12
+ **START YOUR ANALYSIS NOW. DO NOT ASK QUESTIONS. DO NOT WAIT FOR MORE INPUT.**
7
13
 
8
14
  ## JSON Structure for Inline Comments
9
15
 
@@ -21,12 +27,17 @@ The JSON array must contain inline comment objects with these fields:
21
27
  - "start_character": Optional column start (integer)
22
28
  - "end_character": Optional column end (integer)
23
29
 
24
- **IMPORTANT**: Every comment MUST have either "line" OR "range". Comments without valid line numbers will be rejected.
30
+ **CRITICAL LINE NUMBER RULES:**
31
+ 1. **ALWAYS use final file line numbers, NEVER diff line numbers**
32
+ 2. Line numbers must match the NEW version of the file after all changes
33
+ 3. Use `git show HEAD:path/to/file` or examine the final file to get correct line numbers
34
+ 4. If you see "+50" in a diff, the actual line number is NOT 50 - check the final file
35
+ 5. Every comment MUST have either "line" OR "range". Comments without valid line numbers will be rejected.
25
36
 
26
37
  ### Optional Fields
27
38
  - "side": "REVISION" (new code, default) or "PARENT" (original code)
28
39
 
29
- Line numbers refer to the final file (REVISION), not the diff.
40
+ **VERIFICATION STEP**: Before adding any comment, verify the line number by checking the final file content to ensure your line number points to the exact code you're commenting on.
30
41
 
31
42
  ## Comment Quality Guidelines
32
43
 
@@ -85,11 +96,11 @@ You are running in a git repository with full access to:
85
96
  - All project files for architectural understanding
86
97
  - Use these commands to provide comprehensive, accurate reviews
87
98
 
88
- ## FINAL REMINDER
99
+ ## FINAL TASK INSTRUCTION
89
100
 
90
- **CRITICAL: Your ENTIRE output must be a JSON array wrapped in <response></response> tags.**
101
+ **ANALYZE THE CODE CHANGES NOW AND OUTPUT YOUR INLINE COMMENTS IMMEDIATELY.**
91
102
 
92
- Example formats:
103
+ Your output format must be:
93
104
  ```
94
105
  <response>
95
106
  []
@@ -97,6 +108,8 @@ Example formats:
97
108
  ```
98
109
  (Empty array for clean code - this is GOOD!)
99
110
 
111
+ OR:
112
+
100
113
  ```
101
114
  <response>
102
115
  [{"file": "auth.js", "line": 42, "message": "🤖 SQL injection vulnerability: query uses string concatenation"}]
@@ -104,10 +117,12 @@ Example formats:
104
117
  ```
105
118
  (Only comment on real problems)
106
119
 
107
- **REQUIREMENTS**:
120
+ **CRITICAL REQUIREMENTS**:
108
121
  - Every message must start with "🤖 "
109
122
  - Never use backticks in your response
110
123
  - Empty arrays are encouraged for clean code
111
124
  - Focus on bugs, security, crashes - ignore style preferences
112
125
  - Use git commands to understand context before commenting
113
- - NO TEXT OUTSIDE THE <response></response> TAGS
126
+ - NO TEXT OUTSIDE THE <response></response> TAGS
127
+
128
+ **DO YOUR ANALYSIS NOW. STOP ASKING QUESTIONS. GENERATE THE REVIEW.**
@@ -41,11 +41,16 @@ Gerrit uses a LIMITED markdown subset. Follow these rules EXACTLY:
41
41
 
42
42
  **YOUR ENTIRE OUTPUT MUST BE WRAPPED IN <response></response> TAGS.**
43
43
 
44
+ **IMMEDIATE TASK**: Analyze the code changes provided below and write a comprehensive engineering review.
45
+
44
46
  Start with "🤖 [Your Tool Name] ([Your Model])" then provide a **CONCISE** engineering assessment. Examples:
45
47
  - If you are Claude Sonnet 4: "🤖 Claude (Sonnet 4)"
48
+ - If you are Gemini: "🤖 Gemini (1.5 Pro)" or "🤖 Gemini (1.5 Flash)"
46
49
  - For clean code: "No significant issues found. Change is ready for merge."
47
50
  - For problematic code: Focus only on critical/important issues, skip minor concerns
48
51
 
52
+ **YOU MUST ANALYZE THE PROVIDED CODE CHANGES AND WRITE A REVIEW NOW.**
53
+
49
54
  ## Example Output Format
50
55
 
51
56
  <response>
@@ -175,3 +180,9 @@ CRITICAL FORMATTING RULES:
175
180
  - Use exactly 4 spaces to start each line of code blocks
176
181
  - Keep code blocks simple and readable
177
182
  - Add proper spacing for readability
183
+
184
+ ## TASK SUMMARY
185
+
186
+ **ANALYZE THE CODE CHANGES PROVIDED ABOVE AND WRITE YOUR ENGINEERING REVIEW IMMEDIATELY.**
187
+
188
+ Do not ask for clarification. Do not wait for more input. Analyze the provided code changes, git history, and changed files, then write your review following the format requirements above.
@@ -1,10 +1,16 @@
1
1
  import { Context, Data, Effect, Layer } from 'effect'
2
- import { Console } from 'effect'
3
2
  import { exec } from 'node:child_process'
4
3
  import { promisify } from 'node:util'
5
4
 
6
5
  const execAsync = promisify(exec)
7
6
 
7
+ // Shared response extraction logic for all AI tools
8
+ const extractResponse = (stdout: string): string => {
9
+ // Extract response from <response> tags or use full output
10
+ const responseMatch = stdout.match(/<response>([\s\S]*?)<\/response>/i)
11
+ return responseMatch ? responseMatch[1].trim() : stdout.trim()
12
+ }
13
+
8
14
  // Simple strategy focused only on review needs
9
15
  export class ReviewStrategyError extends Data.TaggedError('ReviewStrategyError')<{
10
16
  message: string
@@ -76,9 +82,7 @@ export const claudeCliStrategy: ReviewStrategy = {
76
82
  }),
77
83
  })
78
84
 
79
- // Extract response from <response> tags or use full output
80
- const responseMatch = result.stdout.match(/<response>([\s\S]*?)<\/response>/i)
81
- return responseMatch ? responseMatch[1].trim() : result.stdout.trim()
85
+ return extractResponse(result.stdout)
82
86
  }),
83
87
  }
84
88
 
@@ -136,7 +140,7 @@ export const geminiCliStrategy: ReviewStrategy = {
136
140
  }),
137
141
  })
138
142
 
139
- return result.stdout.trim()
143
+ return extractResponse(result.stdout)
140
144
  }),
141
145
  }
142
146
 
@@ -194,35 +198,7 @@ export const openCodeCliStrategy: ReviewStrategy = {
194
198
  }),
195
199
  })
196
200
 
197
- return result.stdout.trim()
198
- }),
199
- }
200
-
201
- export const codexCliStrategy: ReviewStrategy = {
202
- name: 'Codex CLI',
203
- isAvailable: () =>
204
- Effect.gen(function* () {
205
- const result = yield* Effect.tryPromise({
206
- try: () => execAsync('which codex'),
207
- catch: () => null,
208
- }).pipe(Effect.orElseSucceed(() => null))
209
-
210
- return Boolean(result && result.stdout.trim())
211
- }),
212
- executeReview: (prompt, options = {}) =>
213
- Effect.gen(function* () {
214
- const command = `codex exec "${prompt.replace(/"/g, '\\"')}"`
215
-
216
- const result = yield* Effect.tryPromise({
217
- try: () => execAsync(command, { cwd: options.cwd }),
218
- catch: (error) =>
219
- new ReviewStrategyError({
220
- message: `Codex CLI failed: ${error instanceof Error ? error.message : String(error)}`,
221
- cause: error,
222
- }),
223
- })
224
-
225
- return result.stdout.trim()
201
+ return extractResponse(result.stdout)
226
202
  }),
227
203
  }
228
204
 
@@ -247,12 +223,7 @@ export const ReviewStrategyServiceLive = Layer.succeed(
247
223
  ReviewStrategyService.of({
248
224
  getAvailableStrategies: () =>
249
225
  Effect.gen(function* () {
250
- const strategies = [
251
- claudeCliStrategy,
252
- geminiCliStrategy,
253
- openCodeCliStrategy,
254
- codexCliStrategy,
255
- ]
226
+ const strategies = [claudeCliStrategy, geminiCliStrategy, openCodeCliStrategy]
256
227
  const available: ReviewStrategy[] = []
257
228
 
258
229
  for (const strategy of strategies) {
@@ -267,12 +238,7 @@ export const ReviewStrategyServiceLive = Layer.succeed(
267
238
 
268
239
  selectStrategy: (preferredName?: string) =>
269
240
  Effect.gen(function* () {
270
- const strategies = [
271
- claudeCliStrategy,
272
- geminiCliStrategy,
273
- openCodeCliStrategy,
274
- codexCliStrategy,
275
- ]
241
+ const strategies = [claudeCliStrategy, geminiCliStrategy, openCodeCliStrategy]
276
242
  const available: ReviewStrategy[] = []
277
243
 
278
244
  for (const strategy of strategies) {
@@ -285,7 +251,7 @@ export const ReviewStrategyServiceLive = Layer.succeed(
285
251
  if (available.length === 0) {
286
252
  return yield* Effect.fail(
287
253
  new ReviewStrategyError({
288
- message: 'No AI tools available. Please install claude, gemini, or codex CLI.',
254
+ message: 'No AI tools available. Please install claude, gemini, or opencode CLI.',
289
255
  }),
290
256
  )
291
257
  }
@@ -7,11 +7,6 @@ interface MockDeps {
7
7
  spawn: (command: string, options: any) => any
8
8
  }
9
9
 
10
- // Mock for Claude SDK
11
- interface MockClaudeSDK {
12
- query: any // Will be a Bun mock function
13
- }
14
-
15
10
  // Test implementation that mirrors the real strategy structure
16
11
  const createTestStrategy = (name: string, command: string, flags: string[], deps: MockDeps) => ({
17
12
  name,
@@ -74,47 +69,6 @@ const createTestStrategy = (name: string, command: string, flags: string[], deps
74
69
  }),
75
70
  })
76
71
 
77
- // Test implementation for Claude SDK strategy
78
- const createSDKStrategy = (
79
- name: string,
80
- deps: { sdk: MockClaudeSDK | null; hasApiKey: boolean },
81
- ) => ({
82
- name,
83
- isAvailable: () =>
84
- Effect.gen(function* () {
85
- return Boolean(deps.sdk && deps.hasApiKey)
86
- }),
87
- executeReview: (prompt: string, options: { cwd?: string; systemPrompt?: string } = {}) =>
88
- Effect.gen(function* () {
89
- if (!deps.sdk) {
90
- return yield* Effect.fail(new Error(`${name} not available`))
91
- }
92
-
93
- const result = yield* Effect.tryPromise({
94
- try: async () => {
95
- for await (const message of deps.sdk!.query({
96
- prompt,
97
- options: {
98
- maxTurns: 3,
99
- customSystemPrompt: options.systemPrompt || 'You are a code review expert.',
100
- allowedTools: ['Read', 'Grep', 'Glob'],
101
- cwd: options.cwd,
102
- },
103
- })) {
104
- if (message.type === 'result' && message.subtype === 'success' && message.result) {
105
- return message.result
106
- }
107
- }
108
- throw new Error('No result received')
109
- },
110
- catch: (error) =>
111
- new Error(`${name} failed: ${error instanceof Error ? error.message : String(error)}`),
112
- })
113
-
114
- return result
115
- }),
116
- })
117
-
118
72
  describe('Review Strategy', () => {
119
73
  let mockExecAsync: any
120
74
  let mockSpawn: any
@@ -266,6 +220,14 @@ describe('Review Strategy', () => {
266
220
  expect(response).toBe('Gemini response')
267
221
  expect(mockSpawn).toHaveBeenCalledWith('gemini -p', expect.any(Object))
268
222
  })
223
+
224
+ it('should extract response from tags', async () => {
225
+ setupSuccessfulExecution('<response>Gemini tagged content</response>')
226
+
227
+ const response = await Effect.runPromise(geminiStrategy.executeReview('Test prompt'))
228
+
229
+ expect(response).toBe('Gemini tagged content')
230
+ })
269
231
  })
270
232
 
271
233
  describe('OpenCode CLI Strategy', () => {
@@ -295,120 +257,13 @@ describe('Review Strategy', () => {
295
257
  expect(response).toBe('OpenCode response')
296
258
  expect(mockSpawn).toHaveBeenCalledWith('opencode -p', expect.any(Object))
297
259
  })
298
- })
299
260
 
300
- describe('Claude SDK Strategy', () => {
301
- let mockSDK: MockClaudeSDK
302
- let claudeSDKStrategy: any
303
-
304
- beforeEach(() => {
305
- mockSDK = {
306
- query: mock(),
307
- }
308
-
309
- claudeSDKStrategy = createSDKStrategy('Claude SDK', {
310
- sdk: mockSDK,
311
- hasApiKey: true,
312
- })
313
- })
314
-
315
- it('should check availability when SDK and API key are present', async () => {
316
- const available = await Effect.runPromise(claudeSDKStrategy.isAvailable())
317
-
318
- expect(available).toBe(true)
319
- })
320
-
321
- it('should check availability when SDK is missing', async () => {
322
- const strategy = createSDKStrategy('Claude SDK', {
323
- sdk: null,
324
- hasApiKey: true,
325
- })
326
-
327
- const available = await Effect.runPromise(strategy.isAvailable())
328
-
329
- expect(available).toBe(false)
330
- })
331
-
332
- it('should check availability when API key is missing', async () => {
333
- const strategy = createSDKStrategy('Claude SDK', {
334
- sdk: mockSDK,
335
- hasApiKey: false,
336
- })
337
-
338
- const available = await Effect.runPromise(strategy.isAvailable())
339
-
340
- expect(available).toBe(false)
341
- })
342
-
343
- it('should execute review successfully', async () => {
344
- mockSDK.query = mock(async function* () {
345
- yield { type: 'message', content: 'Thinking...' }
346
- yield { type: 'result', subtype: 'success', result: 'Claude SDK response' }
347
- })
348
-
349
- const response = await Effect.runPromise(
350
- claudeSDKStrategy.executeReview('Test prompt', { cwd: '/tmp' }),
351
- )
352
-
353
- expect(response).toBe('Claude SDK response')
354
- expect(mockSDK.query).toHaveBeenCalledWith({
355
- prompt: 'Test prompt',
356
- options: {
357
- maxTurns: 3,
358
- customSystemPrompt: 'You are a code review expert.',
359
- allowedTools: ['Read', 'Grep', 'Glob'],
360
- cwd: '/tmp',
361
- },
362
- })
363
- })
364
-
365
- it('should use custom system prompt', async () => {
366
- mockSDK.query = mock(async function* () {
367
- yield { type: 'result', subtype: 'success', result: 'Custom prompt response' }
368
- })
369
-
370
- const response = await Effect.runPromise(
371
- claudeSDKStrategy.executeReview('Test prompt', {
372
- systemPrompt: 'Custom review prompt',
373
- }),
374
- )
375
-
376
- expect(response).toBe('Custom prompt response')
377
- expect(mockSDK.query).toHaveBeenCalledWith({
378
- prompt: 'Test prompt',
379
- options: {
380
- maxTurns: 3,
381
- customSystemPrompt: 'Custom review prompt',
382
- allowedTools: ['Read', 'Grep', 'Glob'],
383
- cwd: undefined,
384
- },
385
- })
386
- })
387
-
388
- it('should handle SDK failure', async () => {
389
- mockSDK.query = mock(async function* () {
390
- throw new Error('SDK error')
391
- })
392
-
393
- try {
394
- await Effect.runPromise(claudeSDKStrategy.executeReview('Test prompt'))
395
- expect(false).toBe(true) // Should not reach here
396
- } catch (error: any) {
397
- expect(error.message).toContain('Claude SDK failed')
398
- }
399
- })
261
+ it('should extract response from tags', async () => {
262
+ setupSuccessfulExecution('<response>OpenCode tagged content</response>')
400
263
 
401
- it('should handle no result received', async () => {
402
- mockSDK.query = mock(async function* () {
403
- yield { type: 'message', content: 'No result' }
404
- })
264
+ const response = await Effect.runPromise(opencodeStrategy.executeReview('Test prompt'))
405
265
 
406
- try {
407
- await Effect.runPromise(claudeSDKStrategy.executeReview('Test prompt'))
408
- expect(false).toBe(true) // Should not reach here
409
- } catch (error: any) {
410
- expect(error.message).toContain('No result received')
411
- }
266
+ expect(response).toBe('OpenCode tagged content')
412
267
  })
413
268
  })
414
269