@g-abhishek/gitx 0.1.2 → 0.1.5

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 (165) hide show
  1. package/README.md +386 -3
  2. package/dist/ai/claudeAi.d.ts +35 -0
  3. package/dist/ai/claudeAi.d.ts.map +1 -0
  4. package/dist/ai/claudeAi.js +396 -0
  5. package/dist/ai/claudeAi.js.map +1 -0
  6. package/dist/ai/claudeCliAi.d.ts +27 -0
  7. package/dist/ai/claudeCliAi.d.ts.map +1 -0
  8. package/dist/ai/claudeCliAi.js +312 -0
  9. package/dist/ai/claudeCliAi.js.map +1 -0
  10. package/dist/ai/localClaudeAi.d.ts +2 -0
  11. package/dist/ai/localClaudeAi.d.ts.map +1 -0
  12. package/dist/ai/localClaudeAi.js +4 -0
  13. package/dist/ai/localClaudeAi.js.map +1 -0
  14. package/dist/ai/mockAi.d.ts +8 -1
  15. package/dist/ai/mockAi.d.ts.map +1 -1
  16. package/dist/ai/mockAi.js +57 -0
  17. package/dist/ai/mockAi.js.map +1 -1
  18. package/dist/ai/openAiAi.d.ts +33 -0
  19. package/dist/ai/openAiAi.d.ts.map +1 -0
  20. package/dist/ai/openAiAi.js +388 -0
  21. package/dist/ai/openAiAi.js.map +1 -0
  22. package/dist/ai/reviewHelpers.d.ts +66 -0
  23. package/dist/ai/reviewHelpers.d.ts.map +1 -0
  24. package/dist/ai/reviewHelpers.js +574 -0
  25. package/dist/ai/reviewHelpers.js.map +1 -0
  26. package/dist/ai/types.d.ts +247 -0
  27. package/dist/ai/types.d.ts.map +1 -1
  28. package/dist/ai/types.js.map +1 -1
  29. package/dist/cli/commands/ask.d.ts +27 -0
  30. package/dist/cli/commands/ask.d.ts.map +1 -0
  31. package/dist/cli/commands/ask.js +230 -0
  32. package/dist/cli/commands/ask.js.map +1 -0
  33. package/dist/cli/commands/commit.d.ts +16 -0
  34. package/dist/cli/commands/commit.d.ts.map +1 -0
  35. package/dist/cli/commands/commit.js +163 -0
  36. package/dist/cli/commands/commit.js.map +1 -0
  37. package/dist/cli/commands/config.d.ts +4 -0
  38. package/dist/cli/commands/config.d.ts.map +1 -0
  39. package/dist/cli/commands/config.js +666 -0
  40. package/dist/cli/commands/config.js.map +1 -0
  41. package/dist/cli/commands/implement.d.ts.map +1 -1
  42. package/dist/cli/commands/implement.js +149 -31
  43. package/dist/cli/commands/implement.js.map +1 -1
  44. package/dist/cli/commands/init.d.ts +4 -0
  45. package/dist/cli/commands/init.d.ts.map +1 -1
  46. package/dist/cli/commands/init.js +7 -69
  47. package/dist/cli/commands/init.js.map +1 -1
  48. package/dist/cli/commands/port.d.ts +32 -0
  49. package/dist/cli/commands/port.d.ts.map +1 -0
  50. package/dist/cli/commands/port.js +554 -0
  51. package/dist/cli/commands/port.js.map +1 -0
  52. package/dist/cli/commands/pr/close.d.ts +15 -0
  53. package/dist/cli/commands/pr/close.d.ts.map +1 -0
  54. package/dist/cli/commands/pr/close.js +71 -0
  55. package/dist/cli/commands/pr/close.js.map +1 -0
  56. package/dist/cli/commands/pr/create.d.ts +17 -0
  57. package/dist/cli/commands/pr/create.d.ts.map +1 -1
  58. package/dist/cli/commands/pr/create.js +208 -7
  59. package/dist/cli/commands/pr/create.js.map +1 -1
  60. package/dist/cli/commands/pr/fixComments.d.ts +5 -2
  61. package/dist/cli/commands/pr/fixComments.d.ts.map +1 -1
  62. package/dist/cli/commands/pr/fixComments.js +5 -13
  63. package/dist/cli/commands/pr/fixComments.js.map +1 -1
  64. package/dist/cli/commands/pr/index.d.ts.map +1 -1
  65. package/dist/cli/commands/pr/index.js +6 -2
  66. package/dist/cli/commands/pr/index.js.map +1 -1
  67. package/dist/cli/commands/pr/list.d.ts.map +1 -1
  68. package/dist/cli/commands/pr/list.js +24 -4
  69. package/dist/cli/commands/pr/list.js.map +1 -1
  70. package/dist/cli/commands/pr/merge.d.ts +23 -0
  71. package/dist/cli/commands/pr/merge.d.ts.map +1 -0
  72. package/dist/cli/commands/pr/merge.js +191 -0
  73. package/dist/cli/commands/pr/merge.js.map +1 -0
  74. package/dist/cli/commands/pr/resolve.d.ts +3 -0
  75. package/dist/cli/commands/pr/resolve.d.ts.map +1 -0
  76. package/dist/cli/commands/pr/resolve.js +92 -0
  77. package/dist/cli/commands/pr/resolve.js.map +1 -0
  78. package/dist/cli/commands/pr/review.d.ts.map +1 -1
  79. package/dist/cli/commands/pr/review.js +121 -6
  80. package/dist/cli/commands/pr/review.js.map +1 -1
  81. package/dist/cli/commands/push.d.ts +16 -0
  82. package/dist/cli/commands/push.d.ts.map +1 -0
  83. package/dist/cli/commands/push.js +166 -0
  84. package/dist/cli/commands/push.js.map +1 -0
  85. package/dist/cli/commands/sync.d.ts +24 -0
  86. package/dist/cli/commands/sync.d.ts.map +1 -0
  87. package/dist/cli/commands/sync.js +414 -0
  88. package/dist/cli/commands/sync.js.map +1 -0
  89. package/dist/cli/index.d.ts.map +1 -1
  90. package/dist/cli/index.js +34 -6
  91. package/dist/cli/index.js.map +1 -1
  92. package/dist/config/config.d.ts +20 -3
  93. package/dist/config/config.d.ts.map +1 -1
  94. package/dist/config/config.js +98 -45
  95. package/dist/config/config.js.map +1 -1
  96. package/dist/config/schema.d.ts.map +1 -1
  97. package/dist/config/schema.js +61 -6
  98. package/dist/config/schema.js.map +1 -1
  99. package/dist/core/context.d.ts +6 -0
  100. package/dist/core/context.d.ts.map +1 -1
  101. package/dist/core/context.js.map +1 -1
  102. package/dist/core/gitx.d.ts +43 -0
  103. package/dist/core/gitx.d.ts.map +1 -1
  104. package/dist/core/gitx.js +187 -20
  105. package/dist/core/gitx.js.map +1 -1
  106. package/dist/index.d.ts +1 -5
  107. package/dist/index.d.ts.map +1 -1
  108. package/dist/index.js +4 -1
  109. package/dist/index.js.map +1 -1
  110. package/dist/providers/azure.d.ts +26 -0
  111. package/dist/providers/azure.d.ts.map +1 -0
  112. package/dist/providers/azure.js +256 -0
  113. package/dist/providers/azure.js.map +1 -0
  114. package/dist/providers/base.d.ts +104 -0
  115. package/dist/providers/base.d.ts.map +1 -0
  116. package/dist/providers/base.js +5 -0
  117. package/dist/providers/base.js.map +1 -0
  118. package/dist/providers/factory.d.ts +8 -0
  119. package/dist/providers/factory.d.ts.map +1 -0
  120. package/dist/providers/factory.js +25 -0
  121. package/dist/providers/factory.js.map +1 -0
  122. package/dist/providers/github.d.ts +19 -0
  123. package/dist/providers/github.d.ts.map +1 -0
  124. package/dist/providers/github.js +291 -0
  125. package/dist/providers/github.js.map +1 -0
  126. package/dist/providers/gitlab.d.ts +19 -0
  127. package/dist/providers/gitlab.d.ts.map +1 -0
  128. package/dist/providers/gitlab.js +186 -0
  129. package/dist/providers/gitlab.js.map +1 -0
  130. package/dist/types/config.d.ts +50 -7
  131. package/dist/types/config.d.ts.map +1 -1
  132. package/dist/types/config.js.map +1 -1
  133. package/dist/utils/azureAuth.d.ts +51 -0
  134. package/dist/utils/azureAuth.d.ts.map +1 -0
  135. package/dist/utils/azureAuth.js +172 -0
  136. package/dist/utils/azureAuth.js.map +1 -0
  137. package/dist/utils/git.d.ts +19 -0
  138. package/dist/utils/git.d.ts.map +1 -1
  139. package/dist/utils/git.js +45 -8
  140. package/dist/utils/git.js.map +1 -1
  141. package/dist/utils/gitOps.d.ts +125 -0
  142. package/dist/utils/gitOps.d.ts.map +1 -0
  143. package/dist/utils/gitOps.js +396 -0
  144. package/dist/utils/gitOps.js.map +1 -0
  145. package/dist/utils/lockFile.d.ts +13 -0
  146. package/dist/utils/lockFile.d.ts.map +1 -0
  147. package/dist/utils/lockFile.js +54 -0
  148. package/dist/utils/lockFile.js.map +1 -0
  149. package/dist/utils/retry.d.ts +10 -0
  150. package/dist/utils/retry.d.ts.map +1 -0
  151. package/dist/utils/retry.js +31 -0
  152. package/dist/utils/retry.js.map +1 -0
  153. package/dist/workflows/implement.d.ts +41 -0
  154. package/dist/workflows/implement.d.ts.map +1 -0
  155. package/dist/workflows/implement.js +219 -0
  156. package/dist/workflows/implement.js.map +1 -0
  157. package/dist/workflows/pr.d.ts +41 -0
  158. package/dist/workflows/pr.d.ts.map +1 -0
  159. package/dist/workflows/pr.js +291 -0
  160. package/dist/workflows/pr.js.map +1 -0
  161. package/dist/workflows/prAddress.d.ts +55 -0
  162. package/dist/workflows/prAddress.d.ts.map +1 -0
  163. package/dist/workflows/prAddress.js +349 -0
  164. package/dist/workflows/prAddress.js.map +1 -0
  165. package/package.json +1 -1
@@ -0,0 +1,574 @@
1
+ /**
2
+ * Shared helpers for the senior-developer PR review.
3
+ *
4
+ * buildSeniorReviewSystem() — the AI system prompt
5
+ * buildSeniorReviewPrompt() — formats the user-facing context block
6
+ * parseSeniorReview() — safely parses the AI JSON response
7
+ */
8
+ // ─── System prompt ────────────────────────────────────────────────────────────
9
+ export function buildSeniorReviewSystem() {
10
+ return `You are a principal software engineer and tech lead doing a thorough pull request review.
11
+ You have access to the CHANGED SECTIONS of each file (extracted around the exact lines that changed),
12
+ plus supporting context files and the full unified diff.
13
+
14
+ Your review MUST cover every one of these dimensions:
15
+ 1. Correctness — logic errors, off-by-one, wrong conditions, silent failures
16
+ 2. Security — injection, auth bypass, secret leakage, unvalidated input
17
+ 3. Robustness — missing error handling, null/undefined guard, edge cases
18
+ 4. Performance — unnecessary loops, N+1 queries, missing caching
19
+ 5. Breaking changes — does this break existing API contracts, interfaces, or callers?
20
+ 6. Best practices — naming, DRY, SOLID, idiomatic language usage
21
+ 7. Test coverage — are critical paths tested? are tests meaningful?
22
+ 8. Documentation — are public APIs documented? are complex sections explained?
23
+
24
+ For EVERY issue that maps to a specific line, add an inline comment.
25
+ Line numbers shown in the excerpts are the REAL line numbers in the new file — use them exactly.
26
+ Only reference lines that appear in the excerpts you were given.
27
+
28
+ Verdict rules:
29
+ - "approve" → no critical or warning issues
30
+ - "request_changes" → one or more critical/warning issues found
31
+ - "comment" → only suggestions / minor observations
32
+
33
+ Respond with ONLY valid JSON (no markdown fences, no prose outside JSON):
34
+ {
35
+ "summary": "<3-5 sentence executive summary>",
36
+ "verdict": "approve|request_changes|comment",
37
+ "issues": [
38
+ { "severity": "critical|warning|suggestion", "description": "<issue>", "file": "<path or null>", "line": <number or null> }
39
+ ],
40
+ "inlineComments": [
41
+ { "path": "<relative file path>", "line": <line number>, "body": "<markdown comment>", "severity": "critical|warning|suggestion", "suggestion": "<replacement code or null>" }
42
+ ],
43
+ "positives": ["<good thing>"],
44
+ "testingNotes": "<how to manually test>",
45
+ "checklist": [
46
+ { "area": "<Correctness|Security|Robustness|Performance|Breaking changes|Best practices|Tests|Documentation>", "status": "pass|warn|fail", "note": "<one sentence>" }
47
+ ]
48
+ }`;
49
+ }
50
+ // ─── Diff parsing ─────────────────────────────────────────────────────────────
51
+ /**
52
+ * Parse a unified diff and return the NEW-file line ranges that were touched,
53
+ * grouped by file path.
54
+ *
55
+ * A diff hunk header looks like: @@ -10,7 +12,8 @@
56
+ * +12,8 → new file starts at line 12, hunk spans 8 lines
57
+ */
58
+ function parseHunkRanges(diff) {
59
+ const result = new Map();
60
+ let currentFile = "";
61
+ for (const line of diff.split("\n")) {
62
+ const fileMatch = line.match(/^\+\+\+ b\/(.+)/);
63
+ if (fileMatch?.[1] && fileMatch[1] !== "/dev/null") {
64
+ currentFile = fileMatch[1].trim();
65
+ if (!result.has(currentFile))
66
+ result.set(currentFile, []);
67
+ continue;
68
+ }
69
+ // @@ -old,count +new,count @@
70
+ const hunkMatch = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/);
71
+ if (hunkMatch && currentFile) {
72
+ const start = parseInt(hunkMatch[1], 10);
73
+ const count = parseInt(hunkMatch[2] ?? "1", 10);
74
+ result.get(currentFile).push({ start, end: start + Math.max(count - 1, 0) });
75
+ }
76
+ }
77
+ return result;
78
+ }
79
+ /**
80
+ * Scan BACKWARD from `fromLine` (1-based) to find where the containing
81
+ * function / class / method starts.
82
+ *
83
+ * Recognises common declaration patterns for TypeScript, JavaScript, Python,
84
+ * Go, Rust, Java, and C#. Falls back to `fromLine - fallback` if nothing
85
+ * is found within `maxScan` lines.
86
+ */
87
+ function findContainerStart(lines, fromLine, maxScan = 80, fallback = 30) {
88
+ // fromLine is 1-based; lines[] is 0-based
89
+ const startIdx = Math.min(lines.length - 1, Math.max(0, fromLine - 2));
90
+ for (let i = startIdx; i >= Math.max(0, startIdx - maxScan); i--) {
91
+ const line = lines[i] ?? "";
92
+ if (
93
+ // TS/JS: export [default] [abstract] [async] function foo(
94
+ /^\s*(export\s+)?(default\s+)?(abstract\s+)?(async\s+)?function[\s*]/.test(line) ||
95
+ // TS/JS: export [abstract] class Foo
96
+ /^\s*(export\s+)?(abstract\s+)?class\s+\w/.test(line) ||
97
+ // TS/JS class methods: [public|private|protected|static|override|async] methodName(
98
+ /^\s*(public|private|protected|static|override|async)(\s+(public|private|protected|static|override|async))*\s+\w+\s*[(<]/.test(line) ||
99
+ // TS/JS arrow function assigned to const/let/var
100
+ /^\s*(export\s+)?(const|let|var)\s+\w+\s*=\s*(async\s+)?\(/.test(line) ||
101
+ // TS/JS shorthand method (no keyword): methodName(args) {
102
+ /^\s*\w+\s*\([^)]*\)\s*(?::\s*\S+\s*)?\{/.test(line) ||
103
+ // Python: def foo(
104
+ /^\s*def\s+\w+\s*\(/.test(line) ||
105
+ // Go: func (recv) Foo(
106
+ /^\s*func\s+/.test(line) ||
107
+ // Rust: fn foo(
108
+ /^\s*(pub\s+)?(async\s+)?fn\s+\w+/.test(line) ||
109
+ // Java/C#: returnType methodName(
110
+ /^\s*(public|private|protected|internal|static|virtual|override)\s+\S+\s+\w+\s*\(/.test(line)) {
111
+ return i + 1; // convert back to 1-based
112
+ }
113
+ }
114
+ // Nothing found — fall back to a fixed number of lines above
115
+ return Math.max(1, fromLine - fallback);
116
+ }
117
+ /**
118
+ * Extract the sections of a file that were changed, always starting each
119
+ * window at the nearest enclosing function/class boundary (or 30 lines up,
120
+ * whichever is closer).
121
+ *
122
+ * Overlapping windows are merged so the same lines aren't shown twice.
123
+ * The first HEADER_LINES of the file are always prepended (imports, class
124
+ * declarations) so the AI understands module structure.
125
+ *
126
+ * Returns a line-numbered string ready to paste into the prompt.
127
+ */
128
+ function extractChangedSections(fileContent, hunks, contextLinesBelow = 20 // lines below the hunk (above uses function-boundary scan)
129
+ ) {
130
+ const lines = fileContent.split("\n");
131
+ const total = lines.length;
132
+ // Always include file header (imports / module preamble) for structural context
133
+ const HEADER_LINES = 20;
134
+ // Build windows: above = scan to function start, below = fixed context
135
+ const windows = [];
136
+ for (const hunk of hunks) {
137
+ const windowStart = findContainerStart(lines, hunk.start);
138
+ windows.push({
139
+ start: windowStart,
140
+ end: Math.min(total, hunk.end + contextLinesBelow),
141
+ });
142
+ }
143
+ // Sort then merge
144
+ windows.sort((a, b) => a.start - b.start);
145
+ const merged = [];
146
+ for (const w of windows) {
147
+ if (merged.length > 0 && w.start <= merged[merged.length - 1].end + 1) {
148
+ merged[merged.length - 1].end = Math.max(merged[merged.length - 1].end, w.end);
149
+ }
150
+ else {
151
+ merged.push({ ...w });
152
+ }
153
+ }
154
+ const formatRange = (start, end) => lines
155
+ .slice(start - 1, end)
156
+ .map((l, i) => `${String(start + i).padStart(5, " ")} | ${l}`)
157
+ .join("\n");
158
+ const sections = [];
159
+ // File header (always included, unless the first window already covers it)
160
+ const firstWindowStart = merged[0]?.start ?? 1;
161
+ if (firstWindowStart > HEADER_LINES + 1) {
162
+ sections.push(formatRange(1, Math.min(HEADER_LINES, total)));
163
+ sections.push(" … (lines omitted) …");
164
+ }
165
+ for (let i = 0; i < merged.length; i++) {
166
+ const { start, end } = merged[i];
167
+ if (i > 0 && start > (merged[i - 1].end + 1)) {
168
+ sections.push(" … (lines omitted) …");
169
+ }
170
+ sections.push(formatRange(start, end));
171
+ }
172
+ return sections.join("\n");
173
+ }
174
+ // ─── User prompt builder ──────────────────────────────────────────────────────
175
+ export function buildSeniorReviewPrompt(context) {
176
+ const parts = [];
177
+ parts.push(`## PR: ${context.prTitle}`);
178
+ parts.push(`Author: ${context.author} | ${context.headBranch} → ${context.baseBranch}`);
179
+ if (context.prBody.trim()) {
180
+ parts.push(`\n### Description\n${context.prBody.slice(0, 1000)}`);
181
+ }
182
+ // ── Parse which lines actually changed per file ───────────────────────────
183
+ const hunkRanges = parseHunkRanges(context.diff);
184
+ // ── Budget constants ──────────────────────────────────────────────────────
185
+ // Claude has a 200k-token context window. We stay well within that while
186
+ // sending enough code for a thorough review.
187
+ //
188
+ // Strategy:
189
+ // • Files ≤ FULL_FILE_THRESHOLD lines → send the ENTIRE file (best context)
190
+ // • Larger files → smart section extraction around hunks
191
+ // with generous context above (function boundary) and below (CONTEXT_LINES_BELOW)
192
+ // • Hard per-file cap for huge files → PER_FILE_LINE_CAP lines of excerpts
193
+ const FULL_FILE_THRESHOLD = 400; // files ≤ this many lines are sent whole
194
+ const CONTEXT_LINES_BELOW = 60; // lines below each hunk in extraction mode
195
+ const PER_FILE_LINE_CAP = 800; // hard cap on excerpt lines for very large files
196
+ const DIFF_BUDGET = 30_000; // unified diff character budget (was 5k — way too small)
197
+ const CTX_FILE_MAX = 4_000; // max chars per supporting context file (was 1.5k)
198
+ const changedEntries = Object.entries(context.changedFiles);
199
+ if (changedEntries.length > 0) {
200
+ parts.push(`\n### Changed files (line numbers are exact positions in the new file)`);
201
+ for (const [path, content] of changedEntries) {
202
+ const fileLines = content.split("\n");
203
+ const hunks = hunkRanges.get(path) ?? [];
204
+ let excerpt;
205
+ let deliveryNote;
206
+ if (fileLines.length <= FULL_FILE_THRESHOLD) {
207
+ // Small file — send the whole thing. The AI gets complete context with no gaps.
208
+ excerpt = fileLines
209
+ .map((l, i) => `${String(i + 1).padStart(5, " ")} | ${l}`)
210
+ .join("\n");
211
+ deliveryNote = `full file, ${fileLines.length} lines`;
212
+ }
213
+ else if (hunks.length === 0) {
214
+ // No hunk data (binary / rename-only) — show first 100 lines as fallback
215
+ excerpt = fileLines
216
+ .slice(0, 100)
217
+ .map((l, i) => `${String(i + 1).padStart(5, " ")} | ${l}`)
218
+ .join("\n");
219
+ if (fileLines.length > 100)
220
+ excerpt += `\n … (file continues — only first 100 lines shown; no hunk data available)`;
221
+ deliveryNote = `first 100 of ${fileLines.length} lines (no hunk data)`;
222
+ }
223
+ else {
224
+ // Large file — extract sections around changed hunks with generous context
225
+ excerpt = extractChangedSections(content, hunks, CONTEXT_LINES_BELOW);
226
+ // Cap at PER_FILE_LINE_CAP complete lines — never mid-line
227
+ const excerptLines = excerpt.split("\n");
228
+ if (excerptLines.length > PER_FILE_LINE_CAP) {
229
+ excerpt =
230
+ excerptLines.slice(0, PER_FILE_LINE_CAP).join("\n") +
231
+ `\n … (${excerptLines.length - PER_FILE_LINE_CAP} more excerpt lines omitted` +
232
+ ` — file has ${fileLines.length} total lines; see the diff below for full change)`;
233
+ }
234
+ deliveryNote = `${hunks.length} hunk${hunks.length > 1 ? "s" : ""}, ${fileLines.length} total lines`;
235
+ }
236
+ parts.push(`\n#### ${path} (${deliveryNote})\n\`\`\`\n${excerpt}\n\`\`\``);
237
+ }
238
+ }
239
+ // ── Unified diff (the raw patch — gives the AI the exact before/after for every line)
240
+ if (context.diff.trim()) {
241
+ const diffSlice = context.diff.slice(0, DIFF_BUDGET);
242
+ const diffTrunc = diffSlice.length < context.diff.length;
243
+ parts.push(`\n### Unified diff${diffTrunc ? ` (truncated to ${DIFF_BUDGET} chars — see file sections above for full content)` : ""}\n\`\`\`diff\n${diffSlice}\n\`\`\``);
244
+ }
245
+ // ── Supporting context files (unchanged files the changes depend on) ──────
246
+ const ctxEntries = Object.entries(context.contextFiles);
247
+ if (ctxEntries.length > 0) {
248
+ parts.push(`\n### Supporting context files (unchanged — imported by changed files)`);
249
+ for (const [path, content] of ctxEntries) {
250
+ const ctxLines = content.split("\n");
251
+ const ctxExcerpt = content.length <= CTX_FILE_MAX
252
+ ? content
253
+ : content.slice(0, CTX_FILE_MAX) + `\n… (truncated — ${ctxLines.length} total lines)`;
254
+ parts.push(`\n#### ${path}\n\`\`\`\n${ctxExcerpt}\n\`\`\``);
255
+ }
256
+ }
257
+ // ── Existing PR comments ──────────────────────────────────────────────────
258
+ if (context.existingComments.length > 0) {
259
+ parts.push(`\n### Existing review comments`);
260
+ for (const c of context.existingComments.slice(0, 6)) {
261
+ const loc = c.path ? ` (${c.path}${c.line ? `:${c.line}` : ""})` : "";
262
+ parts.push(`- **${c.author}**${loc}: ${c.body.slice(0, 150)}`);
263
+ }
264
+ }
265
+ // ── Repo file tree (structural awareness) ────────────────────────────────
266
+ if (context.repoFileList.length > 0) {
267
+ parts.push(`\n### Repository file tree (top 60 files)\n${context.repoFileList.slice(0, 60).join("\n")}`);
268
+ }
269
+ return parts.join("\n");
270
+ }
271
+ // ─── Response parser ──────────────────────────────────────────────────────────
272
+ export function parseSeniorReview(text) {
273
+ let parsed = {};
274
+ try {
275
+ const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/);
276
+ const raw = fenced?.[1]?.trim() ?? text.trim();
277
+ const start = raw.search(/\{/);
278
+ const end = raw.lastIndexOf("}");
279
+ const jsonStr = start !== -1 && end > start ? raw.slice(start, end + 1) : raw;
280
+ parsed = JSON.parse(jsonStr);
281
+ }
282
+ catch {
283
+ return {
284
+ summary: "AI review could not be parsed. Please inspect the diff manually.",
285
+ verdict: "comment",
286
+ issues: [],
287
+ inlineComments: [],
288
+ positives: [],
289
+ testingNotes: "Test the changed functionality manually.",
290
+ checklist: [],
291
+ };
292
+ }
293
+ return {
294
+ summary: parsed.summary ?? "Review generated.",
295
+ verdict: (["approve", "request_changes", "comment"].includes(parsed.verdict ?? ""))
296
+ ? parsed.verdict
297
+ : "comment",
298
+ issues: Array.isArray(parsed.issues) ? parsed.issues : [],
299
+ inlineComments: Array.isArray(parsed.inlineComments)
300
+ ? parsed.inlineComments.filter((c) => c.path && c.line > 0)
301
+ : [],
302
+ positives: Array.isArray(parsed.positives) ? parsed.positives : [],
303
+ testingNotes: parsed.testingNotes ?? "",
304
+ checklist: Array.isArray(parsed.checklist) ? parsed.checklist : [],
305
+ };
306
+ }
307
+ // ─── Fix generation helpers ───────────────────────────────────────────────────
308
+ /**
309
+ * System prompt for the AI fix generator.
310
+ * Instructs the model to produce a minimal, targeted line-range replacement.
311
+ */
312
+ export function buildFixSystem() {
313
+ return `You are a senior developer addressing a pull request review comment.
314
+ Your job is to generate the MINIMAL code change that addresses the reviewer's concern.
315
+
316
+ Rules:
317
+ - Change as few lines as possible — do not refactor surrounding code
318
+ - Preserve the existing indentation style exactly
319
+ - If the comment is a question or discussion (no code change needed), set isDiscussion: true
320
+ - If you are unsure of the correct fix, set confidence: "low"
321
+ - startLine and endLine are 1-based, inclusive line numbers in the CURRENT file
322
+
323
+ Respond with ONLY valid JSON (no markdown fences, no prose outside JSON):
324
+ {
325
+ "file": "<relative file path>",
326
+ "startLine": <1-based line where replacement starts>,
327
+ "endLine": <1-based line where replacement ends, inclusive>,
328
+ "replacement": "<new code lines, newline-separated, preserving indentation>",
329
+ "explanation": "<one sentence: what you changed and why>",
330
+ "confidence": "high|low",
331
+ "resolves": true|false,
332
+ "isDiscussion": true|false
333
+ }`;
334
+ }
335
+ /**
336
+ * Build the user prompt for a single fix request.
337
+ * Includes the comment, file content with line numbers, and the relevant diff.
338
+ */
339
+ export function buildFixPrompt(ctx) {
340
+ const lines = ctx.fileContent.split("\n");
341
+ // Show a window of ±30 lines around the commented line (with real line numbers)
342
+ const windowStart = Math.max(1, ctx.line - 30);
343
+ const windowEnd = Math.min(lines.length, ctx.line + 30);
344
+ const excerpt = lines
345
+ .slice(windowStart - 1, windowEnd)
346
+ .map((l, i) => `${String(windowStart + i).padStart(4, " ")} | ${l}`)
347
+ .join("\n");
348
+ const diffSection = ctx.fileDiff.length > 3000
349
+ ? ctx.fileDiff.slice(0, 3000) + "\n... (diff truncated)"
350
+ : ctx.fileDiff;
351
+ return `## Review Comment
352
+ Author: ${ctx.commentAuthor}
353
+ File: ${ctx.filePath} · Line: ${ctx.line}
354
+
355
+ > ${ctx.comment.replace(/\n/g, "\n> ")}
356
+
357
+ ## File Context (lines ${windowStart}–${windowEnd} of ${lines.length})
358
+ \`\`\`
359
+ ${excerpt}
360
+ \`\`\`
361
+
362
+ ## Diff for this file
363
+ \`\`\`diff
364
+ ${diffSection}
365
+ \`\`\`
366
+
367
+ Generate the fix JSON now.`;
368
+ }
369
+ /**
370
+ * Safely parse the AI response for a fix request.
371
+ * Returns a safe fallback (isDiscussion + low confidence) on parse failure.
372
+ */
373
+ export function parseFixResponse(text, filePath, line) {
374
+ try {
375
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
376
+ if (!jsonMatch)
377
+ throw new Error("no JSON found");
378
+ const parsed = JSON.parse(jsonMatch[0]);
379
+ if (parsed.isDiscussion) {
380
+ return {
381
+ file: parsed.file ?? filePath,
382
+ startLine: line,
383
+ endLine: line,
384
+ replacement: "",
385
+ explanation: parsed.explanation ?? "This comment is a discussion — no code change needed.",
386
+ confidence: "low",
387
+ resolves: parsed.resolves ?? false,
388
+ isDiscussion: true,
389
+ };
390
+ }
391
+ // Validate required fields; fall back to low confidence if anything is off
392
+ const hasReplacement = typeof parsed.replacement === "string";
393
+ const hasLines = typeof parsed.startLine === "number" && typeof parsed.endLine === "number";
394
+ if (!hasReplacement || !hasLines) {
395
+ throw new Error("missing required fields");
396
+ }
397
+ return {
398
+ file: parsed.file ?? filePath,
399
+ startLine: parsed.startLine,
400
+ endLine: parsed.endLine,
401
+ replacement: parsed.replacement,
402
+ explanation: parsed.explanation ?? "AI-generated fix.",
403
+ confidence: parsed.confidence === "high" ? "high" : "low",
404
+ resolves: parsed.resolves ?? false,
405
+ isDiscussion: false,
406
+ };
407
+ }
408
+ catch {
409
+ return {
410
+ file: filePath,
411
+ startLine: line,
412
+ endLine: line,
413
+ replacement: "",
414
+ explanation: "AI could not generate a fix for this comment.",
415
+ confidence: "low",
416
+ resolves: false,
417
+ isDiscussion: true, // treat parse failure as discussion → no code change
418
+ };
419
+ }
420
+ }
421
+ // ─── Ask command helpers ───────────────────────────────────────────────────────
422
+ /** Command reference embedded into the ask system prompt. */
423
+ const GITX_COMMAND_REFERENCE = `
424
+ ## gitx Command Reference
425
+
426
+ | Command | Description |
427
+ |---------|-------------|
428
+ | gitx init / gitx config setup | Interactive setup wizard — configure git & AI providers |
429
+ | gitx config show | Display current configuration (secrets redacted) |
430
+ | gitx config set <key> [value] | Set a single config value (provider, token, model, etc.) |
431
+ | gitx commit [-m msg] [--push] [--dry-run] | AI-generate commit message → commit (optionally push) |
432
+ | gitx push [-b branch] [--staged] [--dry-run] | Stage → AI-commit → push in one step; --staged uses already-staged files only |
433
+ | gitx sync [--base branch] [--strategy merge|rebase] [--continue] [--abort] | Sync current branch with base; AI resolves conflicts |
434
+ | gitx port <target…> [--base branch] [--no-pr] [--draft] [--continue] [--abort] | Cherry-pick commits onto other branches with incremental detection |
435
+ | gitx implement "<task>" [--mode plan|guided|semi-auto|auto] [--dry-run] | AI-plan and implement a task end-to-end |
436
+ | gitx pr list [--state open|closed|all] | List pull requests |
437
+ | gitx pr create [--title T] [--body B] [--draft] [--dry-run] | AI-generate PR title/body → open PR |
438
+ | gitx pr review <number> [--no-comment] [--inline] | Senior-dev AI review — posts inline comments to the PR |
439
+ | gitx pr resolve <number> [--no-commit] [--no-push] [--dry-run] | AI-fix review comments in code; --no-commit applies fixes without committing |
440
+ | gitx pr merge <number> [--strategy squash|merge|rebase] [--delete-branch] | Merge a PR |
441
+ | gitx pr close <number> [-f] | Close a PR |
442
+ | gitx ask "<question>" [--pr] | Ask a question about the repo using AI + live git context |
443
+
444
+ ## Supported Providers
445
+ - Git hosts: GitHub, GitLab, Azure DevOps
446
+ - AI backends: Anthropic Claude (API), OpenAI, Local Claude CLI
447
+
448
+ ## Environment Variables
449
+ - ANTHROPIC_API_KEY — Anthropic API key (auto-selects Claude as AI provider)
450
+ - OPENAI_API_KEY — OpenAI API key
451
+ - GITX_AI_MODEL — Override the AI model name
452
+ - GITX_DEBUG=1 — Print full stack traces on errors
453
+ `.trim();
454
+ /**
455
+ * Builds the system prompt for `gitx ask`.
456
+ * Includes the full command reference and setup guidance so the AI can answer
457
+ * both "how do I…" questions and "is X configured?" diagnostics.
458
+ */
459
+ export function buildAskSystem() {
460
+ return `You are gitx-assistant, a smart support assistant embedded in the gitx CLI.
461
+ You help users with three types of questions:
462
+
463
+ 1. SETUP / DIAGNOSTIC — "is my AI provider set up?", "why isn't gitx working?", "what provider am I using?"
464
+ → Use the GITX SETUP STATUS section in the context. Give a clear yes/no diagnostic and actionable fix steps.
465
+
466
+ 2. REPO STATE — "what did I last commit?", "do I have unstaged changes?", "show me open PRs"
467
+ → Use the LIVE REPO CONTEXT section in the context.
468
+
469
+ 3. HOW-TO — "how do I sync with main?", "how do I undo a commit?", "how do I create a PR?"
470
+ → Use the GITX COMMAND REFERENCE below. Show the exact command.
471
+
472
+ ${GITX_COMMAND_REFERENCE}
473
+
474
+ ## Setup Fix Guide (use when AI or provider is not configured)
475
+ - No AI provider → Run: gitx config setup (or set ANTHROPIC_API_KEY / OPENAI_API_KEY env var)
476
+ - AI provider configured in config but not working → Run: gitx config show to inspect; re-run gitx config set <provider>
477
+ - No git provider token → Run: gitx config set github (or gitlab / azure)
478
+ - Not inside a git repo → cd into your project folder first
479
+
480
+ Rules:
481
+ - Answer concisely and accurately. Get to the point immediately.
482
+ - For setup/diagnostic questions: state clearly whether it IS or IS NOT configured, then explain WHY and how to fix it.
483
+ - Never fabricate details — only use what is in the provided context.
484
+ - Format your answer in plain text. Use a code block only for commands or file paths.
485
+ - When suggesting commands, put them in suggestedCommands so they render highlighted.
486
+
487
+ Respond with ONLY valid JSON (no markdown fences):
488
+ {"answer":"<answer text>","suggestedCommands":["<cmd1>","<cmd2>"]}
489
+
490
+ The suggestedCommands array may be empty [] if no command applies.`;
491
+ }
492
+ /**
493
+ * Builds the user-turn prompt for `gitx ask`, injecting live repo context
494
+ * and the full gitx setup status so the AI can answer diagnostic questions accurately.
495
+ */
496
+ export function buildAskPrompt(question, ctx) {
497
+ const lines = [];
498
+ // ── Section 1: gitx setup status ──────────────────────────────────────────
499
+ lines.push(`## gitx Setup Status`);
500
+ // AI provider
501
+ const ai = ctx.aiSetup;
502
+ lines.push(`- AI provider: ${ai.provider}`);
503
+ lines.push(`- AI configured: ${ai.isConfigured ? "YES" : "NO — not configured"}`);
504
+ if (ai.model)
505
+ lines.push(`- AI model: ${ai.model}`);
506
+ lines.push(`- AI key source: ${ai.keySource}`);
507
+ // Git providers
508
+ if (ctx.gitProviders.length > 0) {
509
+ lines.push(`- Git providers configured:`);
510
+ ctx.gitProviders.forEach((p) => {
511
+ const tokenStatus = p.hasToken ? "token ✓" : "token MISSING";
512
+ lines.push(` ${p.name}: ${tokenStatus}`);
513
+ });
514
+ }
515
+ else {
516
+ lines.push(`- Git providers configured: none`);
517
+ }
518
+ if (ctx.defaultBranch) {
519
+ lines.push(`- Default base branch: ${ctx.defaultBranch}`);
520
+ }
521
+ // ── Section 2: live repo context ──────────────────────────────────────────
522
+ lines.push(``);
523
+ lines.push(`## Live Repo Context`);
524
+ lines.push(`- Inside git repo: ${ctx.isInsideGitRepo ? "YES" : "NO"}`);
525
+ if (ctx.isInsideGitRepo) {
526
+ lines.push(`- Current branch: ${ctx.currentBranch}`);
527
+ if (ctx.recentCommits.length > 0) {
528
+ lines.push(`- Recent commits (newest first):`);
529
+ ctx.recentCommits.forEach((c) => lines.push(` ${c}`));
530
+ }
531
+ else {
532
+ lines.push(`- Recent commits: (none yet)`);
533
+ }
534
+ if (ctx.gitStatus.trim()) {
535
+ lines.push(`- Working tree status:\n${ctx.gitStatus}`);
536
+ }
537
+ else {
538
+ lines.push(`- Working tree status: clean`);
539
+ }
540
+ if (ctx.stashes && ctx.stashes.length > 0) {
541
+ lines.push(`- Stashes:`);
542
+ ctx.stashes.forEach((s) => lines.push(` ${s}`));
543
+ }
544
+ if (ctx.openPRs && ctx.openPRs.length > 0) {
545
+ lines.push(`- Open PRs:`);
546
+ ctx.openPRs.forEach((pr) => lines.push(` #${pr.number} [${pr.state}] "${pr.title}" (branch: ${pr.branch})`));
547
+ }
548
+ }
549
+ // ── Section 3: question ───────────────────────────────────────────────────
550
+ lines.push(``);
551
+ lines.push(`## Question`);
552
+ lines.push(question);
553
+ return lines.join("\n");
554
+ }
555
+ /**
556
+ * Safely parses the AI JSON response for `gitx ask`.
557
+ * Falls back to using the raw text as the answer if JSON parsing fails.
558
+ */
559
+ export function parseAskResponse(raw) {
560
+ try {
561
+ const parsed = JSON.parse(raw);
562
+ return {
563
+ answer: parsed.answer?.trim() ?? raw.trim(),
564
+ suggestedCommands: Array.isArray(parsed.suggestedCommands)
565
+ ? parsed.suggestedCommands.filter((c) => typeof c === "string")
566
+ : [],
567
+ };
568
+ }
569
+ catch {
570
+ // If the AI returned plain text instead of JSON, use it directly
571
+ return { answer: raw.trim(), suggestedCommands: [] };
572
+ }
573
+ }
574
+ //# sourceMappingURL=reviewHelpers.js.map