stella-coder 5.2.0 → 5.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stella-coder",
3
- "version": "5.2.0",
3
+ "version": "5.2.1",
4
4
  "private": false,
5
5
  "description": "Stella Coder 5.0 — AI coding agent with Telegram bot, huge context, TDD, Git ecosystem, presentations, computer control, smart home, Office automation, and antivirus",
6
6
  "main": "stella-cli/index.mjs",
@@ -36,6 +36,7 @@
36
36
  "class-variance-authority": "^0.7.1",
37
37
  "clsx": "^2.1.1",
38
38
  "gsap": "^3.15.0",
39
+ "jsdom": "^29.1.1",
39
40
  "lucide-react": "^1.16.0",
40
41
  "next": "16.2.6",
41
42
  "react": "^19",
@@ -0,0 +1,274 @@
1
+ import { execSync, spawn } from "child_process"
2
+ import { writeFileSync, existsSync, mkdirSync } from "fs"
3
+ import { join } from "path"
4
+ import { homedir } from "os"
5
+ import http from "http"
6
+
7
+ const STELLA_DIR = join(homedir(), ".stella")
8
+ const BROWSER_DIR = join(STELLA_DIR, "browser")
9
+ const PROFILES_DIR = join(BROWSER_DIR, "profiles")
10
+
11
+ function ensureDir() {
12
+ if (!existsSync(BROWSER_DIR)) mkdirSync(BROWSER_DIR, { recursive: true })
13
+ if (!existsSync(PROFILES_DIR)) mkdirSync(PROFILES_DIR, { recursive: true })
14
+ }
15
+
16
+ function httpGet(url) {
17
+ return new Promise((resolve, reject) => {
18
+ http.get(url, (res) => {
19
+ let data = ""
20
+ res.on("data", (chunk) => data += chunk)
21
+ res.on("end", () => {
22
+ try { resolve(JSON.parse(data)) } catch { resolve(data) }
23
+ })
24
+ }).on("error", reject)
25
+ })
26
+ }
27
+
28
+ export class ChromeBrowser {
29
+ constructor() {
30
+ ensureDir()
31
+ this.ws = null
32
+ this.pageId = null
33
+ this.debugPort = 9222
34
+ this.proc = null
35
+ this.msgId = 0
36
+ this.callbacks = new Map()
37
+ }
38
+
39
+ async launch(headless = false) {
40
+ const chromePaths = [
41
+ "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
42
+ "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
43
+ join(homedir(), "AppData\\Local\\Google\\Chrome\\Application\\chrome.exe"),
44
+ "C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe",
45
+ "C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe",
46
+ ]
47
+
48
+ let chromePath = null
49
+ for (const p of chromePaths) {
50
+ if (existsSync(p)) { chromePath = p; break }
51
+ }
52
+
53
+ if (!chromePath) {
54
+ return { success: false, error: "Chrome/Edge not found" }
55
+ }
56
+
57
+ const args = [
58
+ `--remote-debugging-port=${this.debugPort}`,
59
+ "--no-first-run",
60
+ "--no-default-browser-check",
61
+ headless ? "--headless=new" : "",
62
+ `--user-data-dir=${join(PROFILES_DIR, "default")}`,
63
+ "about:blank",
64
+ ].filter(Boolean)
65
+
66
+ this.proc = spawn(chromePath, args, { detached: true, stdio: "ignore" })
67
+ this.proc.unref()
68
+
69
+ await new Promise(r => setTimeout(r, 2000))
70
+
71
+ try {
72
+ const tabs = await httpGet(`http://127.0.0.1:${this.debugPort}/json`)
73
+ if (tabs && tabs.length > 0) {
74
+ this.pageId = tabs[0].id
75
+ return { success: true, port: this.debugPort, pid: this.proc.pid }
76
+ }
77
+ } catch (e) {
78
+ return { success: false, error: `Chrome launched but DevTools not ready: ${e.message}` }
79
+ }
80
+
81
+ return { success: true, port: this.debugPort }
82
+ }
83
+
84
+ async connect() {
85
+ try {
86
+ const tabs = await httpGet(`http://127.0.0.1:${this.debugPort}/json`)
87
+ if (!tabs || tabs.length === 0) {
88
+ return { success: false, error: "No tabs found. Launch Chrome first." }
89
+ }
90
+
91
+ const wsUrl = tabs[0].webSocketDebuggerUrl
92
+ if (!wsUrl) {
93
+ return { success: false, error: "WebSocket URL not available" }
94
+ }
95
+
96
+ return new Promise((resolve) => {
97
+ this.ws = new WebSocket(wsUrl)
98
+ this.ws.onopen = () => {
99
+ this.pageId = tabs[0].id
100
+ resolve({ success: true, title: tabs[0].title })
101
+ }
102
+ this.ws.onmessage = (event) => {
103
+ const msg = JSON.parse(typeof event.data === "string" ? event.data : event.data.toString())
104
+ if (msg.id && this.callbacks.has(msg.id)) {
105
+ this.callbacks.get(msg.id)(msg)
106
+ this.callbacks.delete(msg.id)
107
+ }
108
+ }
109
+ this.ws.onerror = (err) => {
110
+ resolve({ success: false, error: err.message || "WebSocket error" })
111
+ }
112
+ })
113
+ } catch (e) {
114
+ return { success: false, error: e.message }
115
+ }
116
+ }
117
+
118
+ async sendCommand(method, params = {}) {
119
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
120
+ const conn = await this.connect()
121
+ if (!conn.success) return { success: false, error: conn.error }
122
+ }
123
+
124
+ const id = ++this.msgId
125
+ return new Promise((resolve) => {
126
+ this.callbacks.set(id, (msg) => {
127
+ if (msg.error) {
128
+ resolve({ success: false, error: msg.error.message })
129
+ } else {
130
+ resolve({ success: true, result: msg.result })
131
+ }
132
+ })
133
+ this.ws.send(JSON.stringify({ id, method, params }))
134
+ setTimeout(() => {
135
+ if (this.callbacks.has(id)) {
136
+ this.callbacks.delete(id)
137
+ resolve({ success: false, error: "Command timeout" })
138
+ }
139
+ }, 30000)
140
+ })
141
+ }
142
+
143
+ async navigate(url) {
144
+ const result = await this.sendCommand("Page.navigate", { url })
145
+ if (result.success) await new Promise(r => setTimeout(r, 2000))
146
+ return result
147
+ }
148
+
149
+ async getPageContent() {
150
+ const result = await this.sendCommand("Runtime.evaluate", {
151
+ expression: "document.documentElement.outerHTML",
152
+ returnByValue: true,
153
+ })
154
+ return result.success ? { success: true, html: result.result?.value || "" } : result
155
+ }
156
+
157
+ async getText() {
158
+ const result = await this.sendCommand("Runtime.evaluate", {
159
+ expression: "document.body.innerText",
160
+ returnByValue: true,
161
+ })
162
+ return result.success ? { success: true, text: result.result?.value || "" } : result
163
+ }
164
+
165
+ async getTitle() {
166
+ const result = await this.sendCommand("Runtime.evaluate", {
167
+ expression: "document.title",
168
+ returnByValue: true,
169
+ })
170
+ return result.success ? { success: true, title: result.result?.value || "" } : result
171
+ }
172
+
173
+ async screenshot(path) {
174
+ const result = await this.sendCommand("Page.captureScreenshot", { format: "png" })
175
+ if (result.success && result.result?.data) {
176
+ const imgPath = path || join(BROWSER_DIR, `screenshot_${Date.now()}.png`)
177
+ writeFileSync(imgPath, Buffer.from(result.result.data, "base64"))
178
+ return { success: true, path: imgPath }
179
+ }
180
+ return result
181
+ }
182
+
183
+ async evaluate(expression) {
184
+ return await this.sendCommand("Runtime.evaluate", {
185
+ expression,
186
+ returnByValue: true,
187
+ awaitPromise: true,
188
+ })
189
+ }
190
+
191
+ async click(selector) {
192
+ const posResult = await this.sendCommand("Runtime.evaluate", {
193
+ expression: `(() => { const el = document.querySelector('${selector.replace(/'/g, "\\'")}'); if (!el) return null; const r = el.getBoundingClientRect(); return { x: r.x + r.width/2, y: r.y + r.height/2 }; })()`,
194
+ returnByValue: true,
195
+ })
196
+
197
+ if (!posResult.success || !posResult.result?.value) {
198
+ return { success: false, error: `Element not found: ${selector}` }
199
+ }
200
+
201
+ const { x, y } = posResult.result.value
202
+ await this.sendCommand("Input.dispatchMouseEvent", { type: "mousePressed", x, y, button: "left", clickCount: 1 })
203
+ await this.sendCommand("Input.dispatchMouseEvent", { type: "mouseReleased", x, y, button: "left", clickCount: 1 })
204
+ return { success: true, x, y }
205
+ }
206
+
207
+ async type(selector, text) {
208
+ await this.sendCommand("Runtime.evaluate", {
209
+ expression: `document.querySelector('${selector.replace(/'/g, "\\'")}')?.focus()`,
210
+ })
211
+ for (const char of text) {
212
+ await this.sendCommand("Input.dispatchKeyEvent", { type: "keyDown", text: char, key: char })
213
+ await this.sendCommand("Input.dispatchKeyEvent", { type: "keyUp", key: char })
214
+ }
215
+ return { success: true }
216
+ }
217
+
218
+ async scroll(deltaX = 0, deltaY = 300) {
219
+ await this.sendCommand("Input.dispatchMouseEvent", { type: "mouseWheel", x: 400, y: 400, deltaX, deltaY })
220
+ return { success: true }
221
+ }
222
+
223
+ async pressKey(key) {
224
+ const keys = {
225
+ enter: { key: "Enter", code: "Enter", windowsVirtualKeyCode: 13 },
226
+ tab: { key: "Tab", code: "Tab", windowsVirtualKeyCode: 9 },
227
+ escape: { key: "Escape", code: "Escape", windowsVirtualKeyCode: 27 },
228
+ backspace: { key: "Backspace", code: "Backspace", windowsVirtualKeyCode: 8 },
229
+ space: { key: " ", code: "Space", windowsVirtualKeyCode: 32 },
230
+ arrowup: { key: "ArrowUp", code: "ArrowUp", windowsVirtualKeyCode: 38 },
231
+ arrowdown: { key: "ArrowDown", code: "ArrowDown", windowsVirtualKeyCode: 40 },
232
+ }
233
+ const k = keys[key.toLowerCase()] || { key, code: `Key${key.toUpperCase()}` }
234
+ await this.sendCommand("Input.dispatchKeyEvent", { type: "keyDown", ...k })
235
+ await this.sendCommand("Input.dispatchKeyEvent", { type: "keyUp", ...k })
236
+ return { success: true }
237
+ }
238
+
239
+ async fillForm(fields) {
240
+ const results = []
241
+ for (const { selector, value } of fields) {
242
+ results.push({ selector, ...(await this.type(selector, value)) })
243
+ }
244
+ return { success: true, results }
245
+ }
246
+
247
+ async getLinks() {
248
+ const result = await this.sendCommand("Runtime.evaluate", {
249
+ expression: `Array.from(document.querySelectorAll('a[href]')).map(a => ({ text: a.innerText.trim().slice(0, 100), href: a.href })).filter(a => a.text).slice(0, 50)`,
250
+ returnByValue: true,
251
+ })
252
+ return result.success ? { success: true, links: result.result?.value || [] } : result
253
+ }
254
+
255
+ async getForms() {
256
+ const result = await this.sendCommand("Runtime.evaluate", {
257
+ expression: `Array.from(document.querySelectorAll('form')).map(f => ({ action: f.action, method: f.method, inputs: Array.from(f.querySelectorAll('input,textarea,select')).map(i => ({ name: i.name, type: i.type, placeholder: i.placeholder })) }))`,
258
+ returnByValue: true,
259
+ })
260
+ return result.success ? { success: true, forms: result.result?.value || [] } : result
261
+ }
262
+
263
+ close() {
264
+ if (this.ws) this.ws.close()
265
+ if (this.proc) this.proc.kill()
266
+ return { success: true }
267
+ }
268
+
269
+ static async listTabs(port = 9222) {
270
+ try { return await httpGet(`http://127.0.0.1:${port}/json`) } catch { return [] }
271
+ }
272
+ }
273
+
274
+ export default ChromeBrowser
@@ -0,0 +1,407 @@
1
+ import { execSync } from "child_process"
2
+ import { readFileSync, existsSync } from "fs"
3
+ import { join } from "path"
4
+ import http from "http"
5
+ import https from "https"
6
+
7
+ function apiRequest(url, options = {}) {
8
+ return new Promise((resolve, reject) => {
9
+ const mod = url.startsWith("https") ? https : http
10
+ const urlObj = new URL(url)
11
+ const headers = {
12
+ "Accept": "application/vnd.github+json",
13
+ ...options.headers,
14
+ }
15
+ if (options.token) headers["Authorization"] = `Bearer ${options.token}`
16
+
17
+ const reqOptions = {
18
+ hostname: urlObj.hostname,
19
+ port: urlObj.port,
20
+ path: urlObj.pathname + urlObj.search,
21
+ method: options.method || "GET",
22
+ headers,
23
+ }
24
+
25
+ const req = mod.request(reqOptions, (res) => {
26
+ let data = ""
27
+ res.on("data", (chunk) => data += chunk)
28
+ res.on("end", () => {
29
+ try { resolve({ status: res.statusCode, data: JSON.parse(data) }) }
30
+ catch { resolve({ status: res.statusCode, data }) }
31
+ })
32
+ })
33
+ req.on("error", reject)
34
+ if (options.body) req.write(JSON.stringify(options.body))
35
+ req.end()
36
+ })
37
+ }
38
+
39
+ function getGitRemote() {
40
+ try {
41
+ const remote = execSync("git remote get-url origin", { encoding: "utf-8" }).trim()
42
+ // Parse owner/repo from URL
43
+ const match = remote.match(/github\.com[/:]([^/]+)\/([^/.]+)/)
44
+ if (match) return { provider: "github", owner: match[1], repo: match[2] }
45
+ const glMatch = remote.match(/gitlab\.com[/:]([^/]+)\/([^/.]+)/)
46
+ if (glMatch) return { provider: "gitlab", owner: glMatch[1], repo: glMatch[2] }
47
+ return { provider: "unknown", remote }
48
+ } catch {
49
+ return null
50
+ }
51
+ }
52
+
53
+ function getToken(provider) {
54
+ // Try environment variables
55
+ if (provider === "github") return process.env.GITHUB_TOKEN || process.env.GH_TOKEN
56
+ if (provider === "gitlab") return process.env.GITLAB_TOKEN || process.env.GL_TOKEN
57
+
58
+ // Try git credential
59
+ try {
60
+ const config = execSync("git config --get credential.helper", { encoding: "utf-8" }).trim()
61
+ if (config.includes("store")) {
62
+ const credPath = join(process.env.USERPROFILE || process.env.HOME || "", ".git-credentials")
63
+ if (existsSync(credPath)) {
64
+ const creds = readFileSync(credPath, "utf-8")
65
+ const match = creds.match(/https:\/\/[^:]+:([^@]+)@github\.com/)
66
+ if (match) return match[1]
67
+ }
68
+ }
69
+ } catch {}
70
+
71
+ return null
72
+ }
73
+
74
+ export class GitAPI {
75
+ constructor() {
76
+ this.info = getGitRemote()
77
+ this.token = this.info ? getToken(this.info.provider) : null
78
+ }
79
+
80
+ isConfigured() {
81
+ return this.info && this.info.provider !== "unknown"
82
+ }
83
+
84
+ hasToken() {
85
+ return !!this.token
86
+ }
87
+
88
+ // ===== ISSUES =====
89
+ async listIssues(state = "open", perPage = 20) {
90
+ if (!this.isConfigured()) return { success: false, error: "No GitHub/GitLab remote configured" }
91
+ const { provider, owner, repo } = this.info
92
+ const token = this.token
93
+
94
+ if (provider === "github") {
95
+ const url = `https://api.github.com/repos/${owner}/${repo}/issues?state=${state}&per_page=${perPage}`
96
+ const res = await apiRequest(url, { token: this.token })
97
+ return { success: true, issues: Array.isArray(res.data) ? res.data : [] }
98
+ }
99
+
100
+ if (provider === "gitlab") {
101
+ const encoded = encodeURIComponent(`${owner}/${repo}`)
102
+ const url = `https://gitlab.com/api/v4/projects/${encoded}/issues?state=${state}`
103
+ const res = await apiRequest(url, { token: this.token })
104
+ return { success: true, issues: Array.isArray(res.data) ? res.data : [] }
105
+ }
106
+
107
+ return { success: false, error: "Unknown provider" }
108
+ }
109
+
110
+ async createIssue(title, body, labels = []) {
111
+ if (!this.isConfigured()) return { success: false, error: "No git remote configured" }
112
+
113
+ const { provider, owner, repo } = this.info
114
+
115
+ if (provider === "github") {
116
+ const url = `https://api.github.com/repos/${owner}/${repo}/issues`
117
+ const res = await apiRequest(url, {
118
+ method: "POST",
119
+ token: this.token,
120
+ body: { title, body, labels },
121
+ })
122
+ return { success: res.status === 201, data: res.data, url: res.data?.html_url }
123
+ }
124
+
125
+ if (provider === "gitlab") {
126
+ const encoded = encodeURIComponent(`${owner}/${repo}`)
127
+ const url = `https://gitlab.com/api/v4/projects/${encoded}/issues`
128
+ const res = await apiRequest(url, {
129
+ method: "POST",
130
+ token: this.token,
131
+ body: { title, description: body, labels: labels.join(",") },
132
+ })
133
+ return { success: res.status === 201, data: res.data, url: res.data?.web_url }
134
+ }
135
+
136
+ return { success: false, error: "Unknown provider" }
137
+ }
138
+
139
+ async closeIssue(issueNumber) {
140
+ if (!this.isConfigured()) return { success: false, error: "No git remote configured" }
141
+
142
+ const { provider, owner, repo } = this.info
143
+
144
+ if (provider === "github") {
145
+ const url = `https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}`
146
+ const res = await apiRequest(url, {
147
+ method: "PATCH",
148
+ token: this.token,
149
+ body: { state: "closed" },
150
+ })
151
+ return { success: res.status === 200, data: res.data }
152
+ }
153
+
154
+ if (provider === "gitlab") {
155
+ const encoded = encodeURIComponent(`${owner}/${repo}`)
156
+ const url = `https://gitlab.com/api/v4/projects/${encoded}/issues/${issueNumber}`
157
+ const res = await apiRequest(url, {
158
+ method: "PUT",
159
+ token: this.token,
160
+ body: { state_event: "close" },
161
+ })
162
+ return { success: res.status === 200, data: res.data }
163
+ }
164
+
165
+ return { success: false, error: "Unknown provider" }
166
+ }
167
+
168
+ // ===== PULL REQUESTS / MERGE REQUESTS =====
169
+ async listPRs(state = "open") {
170
+ if (!this.isConfigured()) return { success: false, error: "No git remote configured" }
171
+
172
+ const { provider, owner, repo } = this.info
173
+
174
+ if (provider === "github") {
175
+ const url = `https://api.github.com/repos/${owner}/${repo}/pulls?state=${state}`
176
+ const res = await apiRequest(url, { token: this.token })
177
+ return { success: true, prs: Array.isArray(res.data) ? res.data : [] }
178
+ }
179
+
180
+ if (provider === "gitlab") {
181
+ const encoded = encodeURIComponent(`${owner}/${repo}`)
182
+ const stateMap = { open: "opened", closed: "closed", merged: "merged" }
183
+ const url = `https://gitlab.com/api/v4/projects/${encoded}/merge_requests?state=${stateMap[state] || state}`
184
+ const res = await apiRequest(url, { token: this.token })
185
+ return { success: true, prs: Array.isArray(res.data) ? res.data : [] }
186
+ }
187
+
188
+ return { success: false, error: "Unknown provider" }
189
+ }
190
+
191
+ async createPR(title, body, head = "main", base = "main") {
192
+ if (!this.isConfigured()) return { success: false, error: "No git remote configured" }
193
+
194
+ const { provider, owner, repo } = this.info
195
+
196
+ if (provider === "github") {
197
+ const url = `https://api.github.com/repos/${owner}/${repo}/pulls`
198
+ const res = await apiRequest(url, {
199
+ method: "POST",
200
+ token: this.token,
201
+ body: { title, body, head, base },
202
+ })
203
+ return { success: res.status === 201, data: res.data, url: res.data?.html_url }
204
+ }
205
+
206
+ if (provider === "gitlab") {
207
+ const encoded = encodeURIComponent(`${owner}/${repo}`)
208
+ const url = `https://gitlab.com/api/v4/projects/${encoded}/merge_requests`
209
+ const res = await apiRequest(url, {
210
+ method: "POST",
211
+ token: this.token,
212
+ body: { title, description: body, source_branch: head, target_branch: base },
213
+ })
214
+ return { success: res.status === 201, data: res.data, url: res.data?.web_url }
215
+ }
216
+
217
+ return { success: false, error: "Unknown provider" }
218
+ }
219
+
220
+ async reviewPR(prNumber, event, body) {
221
+ if (!this.isConfigured()) return { success: false, error: "No git remote configured" }
222
+
223
+ const { provider, owner, repo } = this.info
224
+
225
+ if (provider === "github") {
226
+ const url = `https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}/reviews`
227
+ const res = await apiRequest(url, {
228
+ method: "POST",
229
+ token: this.token,
230
+ body: { event, body },
231
+ })
232
+ return { success: res.status === 200 || res.status === 201, data: res.data }
233
+ }
234
+
235
+ if (provider === "gitlab") {
236
+ const encoded = encodeURIComponent(`${owner}/${repo}`)
237
+ const url = `https://gitlab.com/api/v4/projects/${encoded}/merge_requests/${prNumber}/approvals`
238
+ const res = await apiRequest(url, {
239
+ method: "POST",
240
+ token: this.token,
241
+ })
242
+ return { success: res.status === 201, data: res.data }
243
+ }
244
+
245
+ return { success: false, error: "Unknown provider" }
246
+ }
247
+
248
+ async mergePR(prNumber) {
249
+ if (!this.isConfigured()) return { success: false, error: "No git remote configured" }
250
+
251
+ const { provider, owner, repo } = this.info
252
+
253
+ if (provider === "github") {
254
+ const url = `https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}/merge`
255
+ const res = await apiRequest(url, {
256
+ method: "PUT",
257
+ token: this.token,
258
+ body: { merge_method: "squash" },
259
+ })
260
+ return { success: res.status === 200, data: res.data }
261
+ }
262
+
263
+ if (provider === "gitlab") {
264
+ const encoded = encodeURIComponent(`${owner}/${repo}`)
265
+ const url = `https://gitlab.com/api/v4/projects/${encoded}/merge_requests/${prNumber}/merge`
266
+ const res = await apiRequest(url, {
267
+ method: "PUT",
268
+ token: this.token,
269
+ })
270
+ return { success: res.status === 200, data: res.data }
271
+ }
272
+
273
+ return { success: false, error: "Unknown provider" }
274
+ }
275
+
276
+ // ===== CODE REVIEW =====
277
+ async getPRFiles(prNumber) {
278
+ if (!this.isConfigured()) return { success: false, error: "No git remote configured" }
279
+
280
+ const { provider, owner, repo } = this.info
281
+
282
+ if (provider === "github") {
283
+ const url = `https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}/files`
284
+ const res = await apiRequest(url, { token: this.token })
285
+ return { success: true, files: Array.isArray(res.data) ? res.data : [] }
286
+ }
287
+
288
+ if (provider === "gitlab") {
289
+ const encoded = encodeURIComponent(`${owner}/${repo}`)
290
+ const url = `https://gitlab.com/api/v4/projects/${encoded}/merge_requests/${prNumber}/changes`
291
+ const res = await apiRequest(url, { token: this.token })
292
+ return { success: true, files: res.data?.changes || [] }
293
+ }
294
+
295
+ return { success: false, error: "Unknown provider" }
296
+ }
297
+
298
+ async addComment(prNumber, body, filePath, position) {
299
+ if (!this.isConfigured()) return { success: false, error: "No git remote configured" }
300
+
301
+ const { provider, owner, repo } = this.info
302
+
303
+ if (provider === "github" && filePath && position) {
304
+ // Get latest commit SHA
305
+ const prUrl = `https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}`
306
+ const prRes = await apiRequest(prUrl, { token: this.token })
307
+ const commitId = prRes.data?.head?.sha
308
+
309
+ if (commitId) {
310
+ const url = `https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}/comments`
311
+ const res = await apiRequest(url, {
312
+ method: "POST",
313
+ token: this.token,
314
+ body: { body, path: filePath, position, commit_id: commitId },
315
+ })
316
+ return { success: res.status === 201, data: res.data }
317
+ }
318
+ }
319
+
320
+ // Fallback: general comment
321
+ return await this.reviewPR(prNumber, "COMMENT", body)
322
+ }
323
+
324
+ // ===== REPOS =====
325
+ async getRepoInfo() {
326
+ if (!this.isConfigured()) return { success: false, error: "No git remote configured" }
327
+
328
+ const { provider, owner, repo } = this.info
329
+
330
+ if (provider === "github") {
331
+ const url = `https://api.github.com/repos/${owner}/${repo}`
332
+ const res = await apiRequest(url, { token: this.token })
333
+ return { success: true, repo: res.data }
334
+ }
335
+
336
+ if (provider === "gitlab") {
337
+ const encoded = encodeURIComponent(`${owner}/${repo}`)
338
+ const url = `https://gitlab.com/api/v4/projects/${encoded}`
339
+ const res = await apiRequest(url, { token: this.token })
340
+ return { success: true, repo: res.data }
341
+ }
342
+
343
+ return { success: false, error: "Unknown provider" }
344
+ }
345
+
346
+ async listBranches() {
347
+ if (!this.isConfigured()) return { success: false, error: "No git remote configured" }
348
+
349
+ const { provider, owner, repo } = this.info
350
+
351
+ if (provider === "github") {
352
+ const url = `https://api.github.com/repos/${owner}/${repo}/branches`
353
+ const res = await apiRequest(url, { token: this.token })
354
+ return { success: true, branches: Array.isArray(res.data) ? res.data.map(b => b.name) : [] }
355
+ }
356
+
357
+ if (provider === "gitlab") {
358
+ const encoded = encodeURIComponent(`${owner}/${repo}`)
359
+ const url = `https://gitlab.com/api/v4/projects/${encoded}/repository/branches`
360
+ const res = await apiRequest(url, { token: this.token })
361
+ return { success: true, branches: Array.isArray(res.data) ? res.data.map(b => b.name) : [] }
362
+ }
363
+
364
+ return { success: false, error: "Unknown provider" }
365
+ }
366
+
367
+ async listReleases() {
368
+ if (!this.isConfigured()) return { success: false, error: "No git remote configured" }
369
+
370
+ const { provider, owner, repo } = this.info
371
+
372
+ if (provider === "github") {
373
+ const url = `https://api.github.com/repos/${owner}/${repo}/releases`
374
+ const res = await apiRequest(url, { token: this.token })
375
+ return { success: true, releases: Array.isArray(res.data) ? res.data : [] }
376
+ }
377
+
378
+ if (provider === "gitlab") {
379
+ const encoded = encodeURIComponent(`${owner}/${repo}`)
380
+ const url = `https://gitlab.com/api/v4/projects/${encoded}/releases`
381
+ const res = await apiRequest(url, { token: this.token })
382
+ return { success: true, releases: Array.isArray(res.data) ? res.data : [] }
383
+ }
384
+
385
+ return { success: false, error: "Unknown provider" }
386
+ }
387
+
388
+ async createRelease(tag, name, body) {
389
+ if (!this.isConfigured()) return { success: false, error: "No git remote configured" }
390
+
391
+ const { provider, owner, repo } = this.info
392
+
393
+ if (provider === "github") {
394
+ const url = `https://api.github.com/repos/${owner}/${repo}/releases`
395
+ const res = await apiRequest(url, {
396
+ method: "POST",
397
+ token: this.token,
398
+ body: { tag_name: tag, name, body },
399
+ })
400
+ return { success: res.status === 201, data: res.data, url: res.data?.html_url }
401
+ }
402
+
403
+ return { success: false, error: "Release creation only supported on GitHub" }
404
+ }
405
+ }
406
+
407
+ export default GitAPI