@circuitwall/github-langchain 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,850 @@
1
+ 'use strict';
2
+
3
+ var tools = require('@langchain/core/tools');
4
+ var zod = require('zod');
5
+
6
+ // src/index.ts
7
+ function parseJsonSafe(text, fallback) {
8
+ try {
9
+ return JSON.parse(text);
10
+ } catch {
11
+ return fallback;
12
+ }
13
+ }
14
+ function isLikelyBinary(buf) {
15
+ const len = Math.min(buf.length, 4096);
16
+ if (len === 0) return false;
17
+ let suspicious = 0;
18
+ for (let i = 0; i < len; i++) {
19
+ const b = buf[i];
20
+ if (b === 0) return true;
21
+ if (b < 9 || b > 13 && b < 32) suspicious++;
22
+ }
23
+ return suspicious / len > 0.05;
24
+ }
25
+ var _resolver = resolveGithubAuthFromEnv;
26
+ function setAuthResolver(fn) {
27
+ _resolver = fn;
28
+ }
29
+ function resolveGithubAuthFromEnv() {
30
+ const env = process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN;
31
+ if (env && env.trim()) return { token: env.trim() };
32
+ return {
33
+ error: "GitHub not configured. Set GITHUB_TOKEN or GH_TOKEN env var, or call setAuthResolver() with your own credential provider."
34
+ };
35
+ }
36
+ function _resolveGithubAuth() {
37
+ return resolveAuth();
38
+ }
39
+ function resolveAuth() {
40
+ return _resolver();
41
+ }
42
+ var API = "https://api.github.com";
43
+ async function ghFetch(auth, path, init) {
44
+ const url = path.startsWith("http") ? path : `${API}${path}`;
45
+ const res = await fetch(url, {
46
+ ...init,
47
+ headers: {
48
+ Authorization: `Bearer ${auth.token}`,
49
+ Accept: "application/vnd.github+json",
50
+ "X-GitHub-Api-Version": "2022-11-28",
51
+ "User-Agent": "circuitwall-github-langchain",
52
+ ...init?.headers ?? {}
53
+ }
54
+ });
55
+ const text = await res.text();
56
+ if (!res.ok) {
57
+ return { error: `GitHub ${res.status}: ${text.slice(0, 500)}`, url };
58
+ }
59
+ if (!text) return {};
60
+ return parseJsonSafe(text, text);
61
+ }
62
+ async function githubFetch(auth, path, init) {
63
+ return ghFetch(auth, path, init);
64
+ }
65
+ async function _ghFetch(auth, path, init) {
66
+ return ghFetch(auth, path, init);
67
+ }
68
+ var BODY_CAP = 2e4;
69
+ var COMMENT_CAP = 8e3;
70
+ var PATCH_CAP = 8e3;
71
+ var SNIPPET_CAP = 400;
72
+ function truncate(text, cap) {
73
+ if (text.length <= cap) return { text, truncated: false };
74
+ return { text: text.slice(0, cap), truncated: true };
75
+ }
76
+ function decodeContentsBlob(content, encoding) {
77
+ if (!content) return { binary: false, text: "", size_bytes: 0 };
78
+ const b64 = encoding === "base64" ? content.replace(/\s+/g, "") : "";
79
+ const buf = b64 ? Buffer.from(b64, "base64") : Buffer.from(content, "utf8");
80
+ if (isLikelyBinary(buf)) return { binary: true, size_bytes: buf.length };
81
+ return { binary: false, text: buf.toString("utf8"), size_bytes: buf.length };
82
+ }
83
+ function liteUser(u) {
84
+ return u?.login ?? null;
85
+ }
86
+ function liteLabels(labels) {
87
+ return (labels ?? []).map((l) => l.name).filter(Boolean);
88
+ }
89
+ var githubSearchIssuesTool = tools.tool(
90
+ async ({ q, repo, max_results }) => {
91
+ const auth = resolveAuth();
92
+ if ("error" in auth) return JSON.stringify({ error: auth.error });
93
+ const limit = Math.min(max_results ?? 25, 100);
94
+ const query = repo ? `repo:${repo} ${q}` : q;
95
+ const data = await ghFetch(
96
+ auth,
97
+ `/search/issues?q=${encodeURIComponent(query)}&per_page=${limit}`
98
+ );
99
+ if (data.error) return JSON.stringify(data);
100
+ return JSON.stringify({
101
+ total: data.total_count ?? 0,
102
+ items: (data.items ?? []).map((i) => ({
103
+ number: i.number,
104
+ title: i.title,
105
+ state: i.state,
106
+ is_pr: !!i.pull_request,
107
+ url: i.html_url,
108
+ user: liteUser(i.user),
109
+ labels: liteLabels(i.labels),
110
+ updated_at: i.updated_at,
111
+ comments: i.comments ?? 0
112
+ }))
113
+ });
114
+ },
115
+ {
116
+ name: "github_search_issues",
117
+ description: "Search GitHub issues AND pull requests with the same query syntax as the github.com search bar. **PREFER THIS over shell-exec'ing the `gh` CLI.** Pass `repo` (\"owner/name\") to scope the search to a single repo \u2014 the tool prepends `repo:owner/name ` automatically. The `q` body accepts the full GitHub search vocabulary: 'is:issue is:open assignee:@me', 'is:pr review-requested:@me', 'label:bug created:>2026-01-01', etc. `is_pr` in the response distinguishes PRs from issues.",
118
+ schema: zod.z.object({
119
+ q: zod.z.string().describe("GitHub search query (e.g. 'is:open is:issue label:bug')"),
120
+ repo: zod.z.string().optional().describe("Optional 'owner/name' shortcut; prepended as repo: filter"),
121
+ max_results: zod.z.number().optional().describe("Max items (default 25, max 100)")
122
+ })
123
+ }
124
+ );
125
+ var githubGetIssueTool = tools.tool(
126
+ async ({ owner, repo, number }) => {
127
+ const auth = resolveAuth();
128
+ if ("error" in auth) return JSON.stringify({ error: auth.error });
129
+ const data = await ghFetch(
130
+ auth,
131
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/issues/${number}`
132
+ );
133
+ if (data.error) return JSON.stringify(data);
134
+ const assignees = (data.assignees ?? []).map((a) => a.login).filter(Boolean);
135
+ const cap = truncate(typeof data.body === "string" ? data.body : "", BODY_CAP);
136
+ return JSON.stringify({
137
+ number: data.number,
138
+ title: data.title,
139
+ state: data.state,
140
+ is_pr: !!data.pull_request,
141
+ url: data.html_url,
142
+ author: liteUser(data.user),
143
+ labels: liteLabels(data.labels),
144
+ assignees,
145
+ created_at: data.created_at,
146
+ updated_at: data.updated_at,
147
+ closed_at: data.closed_at,
148
+ comments: data.comments,
149
+ body: cap.text,
150
+ truncated: cap.truncated
151
+ });
152
+ },
153
+ {
154
+ name: "github_get_issue",
155
+ description: "Fetch a single issue (or PR \u2014 same endpoint) by number. Returns body capped at 20KB, plus labels, assignees, and timestamps. **PREFER THIS over shell-exec'ing the `gh` CLI.**",
156
+ schema: zod.z.object({
157
+ owner: zod.z.string().describe("Repository owner (user or org)"),
158
+ repo: zod.z.string().describe("Repository name"),
159
+ number: zod.z.number().int().positive().describe("Issue or PR number")
160
+ })
161
+ }
162
+ );
163
+ var githubCreateIssueTool = tools.tool(
164
+ async ({ owner, repo, title, body, labels, assignees }) => {
165
+ const auth = resolveAuth();
166
+ if ("error" in auth) return JSON.stringify({ error: auth.error });
167
+ const payload = { title };
168
+ if (body) payload.body = body;
169
+ if (labels?.length) payload.labels = labels;
170
+ if (assignees?.length) payload.assignees = assignees;
171
+ const data = await ghFetch(
172
+ auth,
173
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/issues`,
174
+ { method: "POST", body: JSON.stringify(payload) }
175
+ );
176
+ if (data.error) return JSON.stringify(data);
177
+ return JSON.stringify({ ok: true, number: data.number, url: data.html_url });
178
+ },
179
+ {
180
+ name: "github_create_issue",
181
+ description: "Open a new issue in a repository. **PREFER THIS over shell-exec'ing the `gh` CLI.** Labels and assignees must already exist on the repo \u2014 invalid values produce a 422.",
182
+ schema: zod.z.object({
183
+ owner: zod.z.string(),
184
+ repo: zod.z.string(),
185
+ title: zod.z.string().describe("Issue title"),
186
+ body: zod.z.string().optional().describe("Markdown body"),
187
+ labels: zod.z.array(zod.z.string()).optional().describe("Existing label names"),
188
+ assignees: zod.z.array(zod.z.string()).optional().describe("GitHub usernames with access")
189
+ })
190
+ }
191
+ );
192
+ var githubAddCommentTool = tools.tool(
193
+ async ({ owner, repo, number, body }) => {
194
+ const auth = resolveAuth();
195
+ if ("error" in auth) return JSON.stringify({ error: auth.error });
196
+ const data = await ghFetch(
197
+ auth,
198
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/issues/${number}/comments`,
199
+ { method: "POST", body: JSON.stringify({ body }) }
200
+ );
201
+ if (data.error) return JSON.stringify(data);
202
+ return JSON.stringify({ ok: true, comment_id: data.id, url: data.html_url });
203
+ },
204
+ {
205
+ name: "github_add_comment",
206
+ description: "Post a comment on an issue or pull request (GitHub treats both the same). **PREFER THIS over shell-exec'ing the `gh` CLI.** Body is rendered as GitHub-flavored Markdown.",
207
+ schema: zod.z.object({
208
+ owner: zod.z.string(),
209
+ repo: zod.z.string(),
210
+ number: zod.z.number().int().positive(),
211
+ body: zod.z.string().describe("Comment body (GitHub-flavored Markdown)")
212
+ })
213
+ }
214
+ );
215
+ var githubListPullsTool = tools.tool(
216
+ async ({ owner, repo, state, head, base, max_results }) => {
217
+ const auth = resolveAuth();
218
+ if ("error" in auth) return JSON.stringify({ error: auth.error });
219
+ const limit = Math.min(max_results ?? 25, 100);
220
+ const params = new URLSearchParams({ per_page: String(limit) });
221
+ if (state) params.set("state", state);
222
+ if (head) params.set("head", head);
223
+ if (base) params.set("base", base);
224
+ const data = await ghFetch(
225
+ auth,
226
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls?${params.toString()}`
227
+ );
228
+ if (!Array.isArray(data)) return JSON.stringify(data);
229
+ return JSON.stringify({
230
+ pulls: data.map((p) => ({
231
+ number: p.number,
232
+ title: p.title,
233
+ state: p.state,
234
+ draft: p.draft,
235
+ url: p.html_url,
236
+ user: liteUser(p.user),
237
+ head: p.head?.ref ?? null,
238
+ base: p.base?.ref ?? null,
239
+ created_at: p.created_at,
240
+ updated_at: p.updated_at
241
+ }))
242
+ });
243
+ },
244
+ {
245
+ name: "github_list_pulls",
246
+ description: "List pull requests for a repository, optionally filtered by state / head branch / base branch. **PREFER THIS over shell-exec'ing the `gh` CLI.** For richer detail on a single PR (mergeable, review state, file count) call github_get_pull.",
247
+ schema: zod.z.object({
248
+ owner: zod.z.string(),
249
+ repo: zod.z.string(),
250
+ state: zod.z.enum(["open", "closed", "all"]).optional().describe("Default: open"),
251
+ head: zod.z.string().optional().describe("Filter by head: 'user:branch' or 'org:branch'"),
252
+ base: zod.z.string().optional().describe("Filter by base branch (e.g. 'main')"),
253
+ max_results: zod.z.number().optional().describe("Max PRs (default 25, max 100)")
254
+ })
255
+ }
256
+ );
257
+ var githubGetPullTool = tools.tool(
258
+ async ({ owner, repo, number }) => {
259
+ const auth = resolveAuth();
260
+ if ("error" in auth) return JSON.stringify({ error: auth.error });
261
+ const data = await ghFetch(
262
+ auth,
263
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls/${number}`
264
+ );
265
+ if (data.error) return JSON.stringify(data);
266
+ const head = data.head;
267
+ const base = data.base;
268
+ const cap = truncate(typeof data.body === "string" ? data.body : "", BODY_CAP);
269
+ return JSON.stringify({
270
+ number: data.number,
271
+ title: data.title,
272
+ state: data.state,
273
+ draft: data.draft,
274
+ merged: data.merged,
275
+ mergeable: data.mergeable,
276
+ // null = GitHub still computing
277
+ mergeable_state: data.mergeable_state,
278
+ url: data.html_url,
279
+ author: liteUser(data.user),
280
+ head: head ? { ref: head.ref, sha: head.sha, repo: head.repo?.full_name } : null,
281
+ base: base ? { ref: base.ref, sha: base.sha } : null,
282
+ changed_files: data.changed_files,
283
+ additions: data.additions,
284
+ deletions: data.deletions,
285
+ review_comments: data.review_comments,
286
+ comments: data.comments,
287
+ created_at: data.created_at,
288
+ updated_at: data.updated_at,
289
+ merged_at: data.merged_at,
290
+ body: cap.text,
291
+ truncated: cap.truncated
292
+ });
293
+ },
294
+ {
295
+ name: "github_get_pull",
296
+ description: "Fetch detail on a single pull request \u2014 head/base SHAs, mergeable state, additions/deletions, changed files count, review comment count. **PREFER THIS over shell-exec'ing the `gh` CLI.** Note: `mergeable` may be null on a freshly-pushed PR; GitHub computes it asynchronously.",
297
+ schema: zod.z.object({
298
+ owner: zod.z.string(),
299
+ repo: zod.z.string(),
300
+ number: zod.z.number().int().positive()
301
+ })
302
+ }
303
+ );
304
+ var githubGetRepoTool = tools.tool(
305
+ async ({ owner, repo }) => {
306
+ const auth = resolveAuth();
307
+ if ("error" in auth) return JSON.stringify({ error: auth.error });
308
+ const data = await ghFetch(
309
+ auth,
310
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`
311
+ );
312
+ if (data.error) return JSON.stringify(data);
313
+ return JSON.stringify({
314
+ full_name: data.full_name,
315
+ url: data.html_url,
316
+ description: data.description,
317
+ visibility: data.visibility ?? (data.private ? "private" : "public"),
318
+ default_branch: data.default_branch,
319
+ topics: data.topics ?? [],
320
+ language: data.language,
321
+ stars: data.stargazers_count,
322
+ forks: data.forks_count,
323
+ open_issues: data.open_issues_count,
324
+ archived: data.archived,
325
+ pushed_at: data.pushed_at
326
+ });
327
+ },
328
+ {
329
+ name: "github_get_repo",
330
+ description: "Fetch repo summary (default branch, visibility, description, topics, star/fork counts, open-issue count). **PREFER THIS over shell-exec'ing the `gh` CLI.** Useful before opening an issue (to confirm the repo exists / pick the right default branch).",
331
+ schema: zod.z.object({
332
+ owner: zod.z.string(),
333
+ repo: zod.z.string()
334
+ })
335
+ }
336
+ );
337
+ var githubUpdateIssueTool = tools.tool(
338
+ async ({ owner, repo, number, title, body, state, state_reason, labels, assignees }) => {
339
+ const auth = resolveAuth();
340
+ if ("error" in auth) return JSON.stringify({ error: auth.error });
341
+ const payload = {};
342
+ if (title !== void 0) payload.title = title;
343
+ if (body !== void 0) payload.body = body;
344
+ if (state !== void 0) payload.state = state;
345
+ if (state_reason !== void 0) payload.state_reason = state_reason;
346
+ if (labels !== void 0) payload.labels = labels;
347
+ if (assignees !== void 0) payload.assignees = assignees;
348
+ if (Object.keys(payload).length === 0) {
349
+ return JSON.stringify({ error: "no fields to update \u2014 pass at least one of title/body/state/labels/assignees" });
350
+ }
351
+ const data = await ghFetch(
352
+ auth,
353
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/issues/${number}`,
354
+ { method: "PATCH", body: JSON.stringify(payload) }
355
+ );
356
+ if (data.error) return JSON.stringify(data);
357
+ return JSON.stringify({
358
+ ok: true,
359
+ number: data.number,
360
+ state: data.state,
361
+ url: data.html_url,
362
+ updated_fields: Object.keys(payload)
363
+ });
364
+ },
365
+ {
366
+ name: "github_update_issue",
367
+ description: 'Edit an issue or PR (same endpoint): change title, body, labels, assignees, or close/reopen via `state`. **PREFER THIS over shell-exec\'ing the `gh` CLI.** Pass `state: "closed"` with `state_reason: "completed"` for done-as-intended, or `"not_planned"` for won\'t-fix.',
368
+ schema: zod.z.object({
369
+ owner: zod.z.string(),
370
+ repo: zod.z.string(),
371
+ number: zod.z.number().int().positive(),
372
+ title: zod.z.string().optional(),
373
+ body: zod.z.string().optional().describe("New issue body (Markdown). Replaces, doesn't append."),
374
+ state: zod.z.enum(["open", "closed"]).optional(),
375
+ state_reason: zod.z.enum(["completed", "not_planned", "reopened"]).optional(),
376
+ labels: zod.z.array(zod.z.string()).optional().describe("Replaces the label set entirely."),
377
+ assignees: zod.z.array(zod.z.string()).optional().describe("Replaces the assignee set entirely.")
378
+ })
379
+ }
380
+ );
381
+ var githubListIssueCommentsTool = tools.tool(
382
+ async ({ owner, repo, number, max_results }) => {
383
+ const auth = resolveAuth();
384
+ if ("error" in auth) return JSON.stringify({ error: auth.error });
385
+ const limit = Math.min(max_results ?? 25, 100);
386
+ const data = await ghFetch(
387
+ auth,
388
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/issues/${number}/comments?per_page=${limit}`
389
+ );
390
+ if (!Array.isArray(data)) return JSON.stringify(data);
391
+ return JSON.stringify({
392
+ comments: data.map((c) => {
393
+ const cap = truncate(typeof c.body === "string" ? c.body : "", COMMENT_CAP);
394
+ return {
395
+ id: c.id,
396
+ user: liteUser(c.user),
397
+ created_at: c.created_at,
398
+ updated_at: c.updated_at,
399
+ url: c.html_url,
400
+ body: cap.text,
401
+ truncated: cap.truncated
402
+ };
403
+ })
404
+ });
405
+ },
406
+ {
407
+ name: "github_list_issue_comments",
408
+ description: "List comments on an issue or PR. **PREFER THIS over shell-exec'ing the `gh` CLI.** Each comment body is capped at 8 KB; `truncated: true` flags ones that hit the cap.",
409
+ schema: zod.z.object({
410
+ owner: zod.z.string(),
411
+ repo: zod.z.string(),
412
+ number: zod.z.number().int().positive(),
413
+ max_results: zod.z.number().optional().describe("Max comments (default 25, max 100)")
414
+ })
415
+ }
416
+ );
417
+ var githubCreatePullTool = tools.tool(
418
+ async ({ owner, repo, title, head, base, body, draft, maintainer_can_modify }) => {
419
+ const auth = resolveAuth();
420
+ if ("error" in auth) return JSON.stringify({ error: auth.error });
421
+ const payload = { title, head, base };
422
+ if (body !== void 0) payload.body = body;
423
+ if (draft !== void 0) payload.draft = draft;
424
+ if (maintainer_can_modify !== void 0) payload.maintainer_can_modify = maintainer_can_modify;
425
+ const data = await ghFetch(
426
+ auth,
427
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls`,
428
+ { method: "POST", body: JSON.stringify(payload) }
429
+ );
430
+ if (data.error) return JSON.stringify(data);
431
+ return JSON.stringify({ ok: true, number: data.number, draft: data.draft, url: data.html_url });
432
+ },
433
+ {
434
+ name: "github_create_pull",
435
+ description: "Open a pull request. **PREFER THIS over shell-exec'ing the `gh` CLI.** `head` is the branch with your changes (use `user:branch` for cross-fork PRs); `base` is the branch you want to merge into. Returns 422 if the head/base pair has no diff or if the branches don't exist.",
436
+ schema: zod.z.object({
437
+ owner: zod.z.string(),
438
+ repo: zod.z.string(),
439
+ title: zod.z.string(),
440
+ head: zod.z.string().describe("Source branch ('feature/x' for same-repo, 'user:branch' for fork PRs)"),
441
+ base: zod.z.string().describe("Target branch (typically 'main')"),
442
+ body: zod.z.string().optional().describe("PR description (Markdown)"),
443
+ draft: zod.z.boolean().optional().describe("Open as draft (default false)"),
444
+ maintainer_can_modify: zod.z.boolean().optional().describe("Allow upstream maintainers to push to your fork branch (default true)")
445
+ })
446
+ }
447
+ );
448
+ var githubUpdatePullTool = tools.tool(
449
+ async ({ owner, repo, number, title, body, state, base, maintainer_can_modify }) => {
450
+ const auth = resolveAuth();
451
+ if ("error" in auth) return JSON.stringify({ error: auth.error });
452
+ const payload = {};
453
+ if (title !== void 0) payload.title = title;
454
+ if (body !== void 0) payload.body = body;
455
+ if (state !== void 0) payload.state = state;
456
+ if (base !== void 0) payload.base = base;
457
+ if (maintainer_can_modify !== void 0) payload.maintainer_can_modify = maintainer_can_modify;
458
+ if (Object.keys(payload).length === 0) {
459
+ return JSON.stringify({ error: "no fields to update \u2014 pass at least one of title/body/state/base" });
460
+ }
461
+ const data = await ghFetch(
462
+ auth,
463
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls/${number}`,
464
+ { method: "PATCH", body: JSON.stringify(payload) }
465
+ );
466
+ if (data.error) return JSON.stringify(data);
467
+ return JSON.stringify({
468
+ ok: true,
469
+ number: data.number,
470
+ state: data.state,
471
+ url: data.html_url,
472
+ updated_fields: Object.keys(payload)
473
+ });
474
+ },
475
+ {
476
+ name: "github_update_pull",
477
+ description: "Edit a pull request: title, body, base branch, or close/reopen. **PREFER THIS over shell-exec'ing the `gh` CLI.** To MERGE a PR use github_merge_pull \u2014 `state` here only opens / closes.",
478
+ schema: zod.z.object({
479
+ owner: zod.z.string(),
480
+ repo: zod.z.string(),
481
+ number: zod.z.number().int().positive(),
482
+ title: zod.z.string().optional(),
483
+ body: zod.z.string().optional(),
484
+ state: zod.z.enum(["open", "closed"]).optional().describe("`closed` here means 'close without merging'. Use github_merge_pull to merge."),
485
+ base: zod.z.string().optional().describe("Re-target the PR at a different base branch."),
486
+ maintainer_can_modify: zod.z.boolean().optional()
487
+ })
488
+ }
489
+ );
490
+ var githubMergePullTool = tools.tool(
491
+ async ({ owner, repo, number, method, commit_title, commit_message, sha }) => {
492
+ const auth = resolveAuth();
493
+ if ("error" in auth) return JSON.stringify({ error: auth.error });
494
+ const payload = {};
495
+ if (commit_title !== void 0) payload.commit_title = commit_title;
496
+ if (commit_message !== void 0) payload.commit_message = commit_message;
497
+ if (method !== void 0) payload.merge_method = method;
498
+ if (sha !== void 0) payload.sha = sha;
499
+ const data = await ghFetch(
500
+ auth,
501
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls/${number}/merge`,
502
+ { method: "PUT", body: JSON.stringify(payload) }
503
+ );
504
+ if (data.error) return JSON.stringify(data);
505
+ return JSON.stringify({
506
+ ok: !!data.merged,
507
+ merged_sha: data.sha ?? null,
508
+ message: data.message ?? null
509
+ });
510
+ },
511
+ {
512
+ name: "github_merge_pull",
513
+ description: "Merge a pull request. **PREFER THIS over shell-exec'ing the `gh` CLI.** `method` selects the merge strategy (default `merge`); pass `sha` to refuse the merge if the PR head has been force-pushed since you last looked. 405 means the PR isn't mergeable yet (still in CI / has conflicts / needs review); 409 means the `sha` guard tripped.",
514
+ schema: zod.z.object({
515
+ owner: zod.z.string(),
516
+ repo: zod.z.string(),
517
+ number: zod.z.number().int().positive(),
518
+ method: zod.z.enum(["merge", "squash", "rebase"]).optional(),
519
+ commit_title: zod.z.string().optional().describe("Title for the merge commit (or squash commit). Default: GitHub's auto-generated title."),
520
+ commit_message: zod.z.string().optional(),
521
+ sha: zod.z.string().optional().describe("Refuse to merge unless the PR head still matches this SHA \u2014 guards against TOCTOU.")
522
+ })
523
+ }
524
+ );
525
+ var githubRequestReviewersTool = tools.tool(
526
+ async ({ owner, repo, number, reviewers, team_reviewers }) => {
527
+ const auth = resolveAuth();
528
+ if ("error" in auth) return JSON.stringify({ error: auth.error });
529
+ if (!reviewers?.length && !team_reviewers?.length) {
530
+ return JSON.stringify({ error: "pass at least one of reviewers or team_reviewers" });
531
+ }
532
+ const payload = {};
533
+ if (reviewers?.length) payload.reviewers = reviewers;
534
+ if (team_reviewers?.length) payload.team_reviewers = team_reviewers;
535
+ const data = await ghFetch(
536
+ auth,
537
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls/${number}/requested_reviewers`,
538
+ { method: "POST", body: JSON.stringify(payload) }
539
+ );
540
+ if (data.error) return JSON.stringify(data);
541
+ return JSON.stringify({
542
+ ok: true,
543
+ requested_users: (data.requested_reviewers ?? []).map((u) => u.login).filter(Boolean),
544
+ requested_teams: (data.requested_teams ?? []).map((t) => t.slug).filter(Boolean),
545
+ url: data.html_url
546
+ });
547
+ },
548
+ {
549
+ name: "github_request_reviewers",
550
+ description: "Request review on a pull request from one or more users and/or teams. **PREFER THIS over shell-exec'ing the `gh` CLI.** GitHub silently drops invalid usernames / team slugs \u2014 check the returned `requested_users` / `requested_teams` to see who actually got a notification.",
551
+ schema: zod.z.object({
552
+ owner: zod.z.string(),
553
+ repo: zod.z.string(),
554
+ number: zod.z.number().int().positive(),
555
+ reviewers: zod.z.array(zod.z.string()).optional().describe("GitHub usernames"),
556
+ team_reviewers: zod.z.array(zod.z.string()).optional().describe("Team slugs (org-scoped)")
557
+ })
558
+ }
559
+ );
560
+ var githubCreateReviewTool = tools.tool(
561
+ async ({ owner, repo, number, event, body, commit_id }) => {
562
+ const auth = resolveAuth();
563
+ if ("error" in auth) return JSON.stringify({ error: auth.error });
564
+ if (event === "REQUEST_CHANGES" && !body?.trim()) {
565
+ return JSON.stringify({ error: "REQUEST_CHANGES requires a non-empty body explaining what to change" });
566
+ }
567
+ const payload = { event };
568
+ if (body !== void 0) payload.body = body;
569
+ if (commit_id !== void 0) payload.commit_id = commit_id;
570
+ const data = await ghFetch(
571
+ auth,
572
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls/${number}/reviews`,
573
+ { method: "POST", body: JSON.stringify(payload) }
574
+ );
575
+ if (data.error) return JSON.stringify(data);
576
+ return JSON.stringify({ ok: true, review_id: data.id, state: data.state, url: data.html_url });
577
+ },
578
+ {
579
+ name: "github_create_review",
580
+ description: "Submit a pull-request review (approve / request changes / leave a comment). **PREFER THIS over shell-exec'ing the `gh` CLI.** `event=APPROVE` doesn't require a body. `event=REQUEST_CHANGES` requires a body. Line-level inline review comments aren't supported by this tool yet \u2014 use the GitHub UI for those.",
581
+ schema: zod.z.object({
582
+ owner: zod.z.string(),
583
+ repo: zod.z.string(),
584
+ number: zod.z.number().int().positive(),
585
+ event: zod.z.enum(["APPROVE", "REQUEST_CHANGES", "COMMENT"]),
586
+ body: zod.z.string().optional().describe("Review summary (Markdown). Required for REQUEST_CHANGES."),
587
+ commit_id: zod.z.string().optional().describe("Pin the review to a specific commit SHA.")
588
+ })
589
+ }
590
+ );
591
+ var githubListPullFilesTool = tools.tool(
592
+ async ({ owner, repo, number, max_results }) => {
593
+ const auth = resolveAuth();
594
+ if ("error" in auth) return JSON.stringify({ error: auth.error });
595
+ const limit = Math.min(max_results ?? 30, 100);
596
+ const data = await ghFetch(
597
+ auth,
598
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls/${number}/files?per_page=${limit}`
599
+ );
600
+ if (!Array.isArray(data)) return JSON.stringify(data);
601
+ return JSON.stringify({
602
+ files: data.map((f) => {
603
+ const patch = typeof f.patch === "string" ? truncate(f.patch, PATCH_CAP) : null;
604
+ return {
605
+ filename: f.filename,
606
+ status: f.status,
607
+ // added | modified | removed | renamed | …
608
+ additions: f.additions,
609
+ deletions: f.deletions,
610
+ changes: f.changes,
611
+ previous_filename: f.previous_filename,
612
+ sha: f.sha,
613
+ patch: patch?.text,
614
+ patch_truncated: patch?.truncated ?? false
615
+ };
616
+ })
617
+ });
618
+ },
619
+ {
620
+ name: "github_list_pull_files",
621
+ description: "List files changed in a pull request, with per-file additions/deletions and (capped) patch text. **PREFER THIS over shell-exec'ing the `gh` CLI.** Each patch caps at 8 KB; `patch_truncated: true` means the diff was longer than that. GitHub itself caps the response at 3000 files.",
622
+ schema: zod.z.object({
623
+ owner: zod.z.string(),
624
+ repo: zod.z.string(),
625
+ number: zod.z.number().int().positive(),
626
+ max_results: zod.z.number().optional().describe("Max files (default 30, max 100)")
627
+ })
628
+ }
629
+ );
630
+ var githubListPullReviewsTool = tools.tool(
631
+ async ({ owner, repo, number, max_results }) => {
632
+ const auth = resolveAuth();
633
+ if ("error" in auth) return JSON.stringify({ error: auth.error });
634
+ const limit = Math.min(max_results ?? 30, 100);
635
+ const data = await ghFetch(
636
+ auth,
637
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls/${number}/reviews?per_page=${limit}`
638
+ );
639
+ if (!Array.isArray(data)) return JSON.stringify(data);
640
+ return JSON.stringify({
641
+ reviews: data.map((r) => {
642
+ const cap = truncate(typeof r.body === "string" ? r.body : "", COMMENT_CAP);
643
+ return {
644
+ id: r.id,
645
+ user: liteUser(r.user),
646
+ state: r.state,
647
+ // APPROVED | CHANGES_REQUESTED | COMMENTED | DISMISSED | PENDING
648
+ submitted_at: r.submitted_at,
649
+ commit_id: r.commit_id,
650
+ url: r.html_url,
651
+ body: cap.text,
652
+ truncated: cap.truncated
653
+ };
654
+ })
655
+ });
656
+ },
657
+ {
658
+ name: "github_list_pull_reviews",
659
+ description: "List reviews submitted on a pull request (approvals, change-requests, comment-only). **PREFER THIS over shell-exec'ing the `gh` CLI.** Use this to check whether a PR is approved before calling github_merge_pull.",
660
+ schema: zod.z.object({
661
+ owner: zod.z.string(),
662
+ repo: zod.z.string(),
663
+ number: zod.z.number().int().positive(),
664
+ max_results: zod.z.number().optional().describe("Max reviews (default 30, max 100)")
665
+ })
666
+ }
667
+ );
668
+ var githubListBranchesTool = tools.tool(
669
+ async ({ owner, repo, max_results }) => {
670
+ const auth = resolveAuth();
671
+ if ("error" in auth) return JSON.stringify({ error: auth.error });
672
+ const limit = Math.min(max_results ?? 30, 100);
673
+ const data = await ghFetch(
674
+ auth,
675
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/branches?per_page=${limit}`
676
+ );
677
+ if (!Array.isArray(data)) return JSON.stringify(data);
678
+ return JSON.stringify({
679
+ branches: data.map((b) => ({
680
+ name: b.name,
681
+ commit_sha: b.commit?.sha ?? null,
682
+ protected: b.protected
683
+ }))
684
+ });
685
+ },
686
+ {
687
+ name: "github_list_branches",
688
+ description: "List branches in a repository with their head SHAs and protection state. **PREFER THIS over shell-exec'ing the `gh` CLI.** Useful before opening a PR (confirm the head branch exists and is the SHA you expect).",
689
+ schema: zod.z.object({
690
+ owner: zod.z.string(),
691
+ repo: zod.z.string(),
692
+ max_results: zod.z.number().optional().describe("Max branches (default 30, max 100)")
693
+ })
694
+ }
695
+ );
696
+ var githubGetFileTool = tools.tool(
697
+ async ({ owner, repo, path, ref }) => {
698
+ const auth = resolveAuth();
699
+ if ("error" in auth) return JSON.stringify({ error: auth.error });
700
+ const url = `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${path.split("/").map(encodeURIComponent).join("/")}` + (ref ? `?ref=${encodeURIComponent(ref)}` : "");
701
+ const data = await ghFetch(auth, url);
702
+ if (!Array.isArray(data) && data.error) {
703
+ return JSON.stringify(data);
704
+ }
705
+ if (Array.isArray(data) || data.type === "dir") {
706
+ return JSON.stringify({
707
+ error: `'${path}' is a directory; this tool only reads single files. Pass a path to a file.`
708
+ });
709
+ }
710
+ const f = data;
711
+ if (f.type !== "file") {
712
+ return JSON.stringify({ error: `unsupported content type '${f.type ?? "?"}' at ${path}` });
713
+ }
714
+ const decoded = decodeContentsBlob(f.content ?? "", f.encoding);
715
+ if (decoded.binary) {
716
+ return JSON.stringify({
717
+ path: f.path,
718
+ sha: f.sha,
719
+ url: f.html_url,
720
+ binary: true,
721
+ size_bytes: decoded.size_bytes
722
+ });
723
+ }
724
+ const cap = truncate(decoded.text ?? "", BODY_CAP);
725
+ return JSON.stringify({
726
+ path: f.path,
727
+ sha: f.sha,
728
+ url: f.html_url,
729
+ binary: false,
730
+ size_bytes: decoded.size_bytes,
731
+ content: cap.text,
732
+ truncated: cap.truncated
733
+ });
734
+ },
735
+ {
736
+ name: "github_get_file",
737
+ description: "Read a file's contents from a repo at an optional ref (branch / tag / commit SHA). **PREFER THIS over shell-exec'ing the `gh` CLI.** Returns up to 20 KB of UTF-8 text; longer files are truncated with `truncated: true`. Binary files return `binary: true` and `size_bytes` instead of `content`. GitHub itself rejects files larger than ~1 MB on this endpoint.",
738
+ schema: zod.z.object({
739
+ owner: zod.z.string(),
740
+ repo: zod.z.string(),
741
+ path: zod.z.string().describe("Path within the repo (e.g. 'src/lib/index.ts'). Slashes preserved."),
742
+ ref: zod.z.string().optional().describe("Branch, tag, or commit SHA. Default: repo's default branch.")
743
+ })
744
+ }
745
+ );
746
+ var githubSearchCodeTool = tools.tool(
747
+ async ({ q, repo, max_results }) => {
748
+ const auth = resolveAuth();
749
+ if ("error" in auth) return JSON.stringify({ error: auth.error });
750
+ const limit = Math.min(max_results ?? 25, 100);
751
+ const query = repo ? `repo:${repo} ${q}` : q;
752
+ const data = await ghFetch(
753
+ auth,
754
+ `/search/code?q=${encodeURIComponent(query)}&per_page=${limit}`,
755
+ { headers: { Accept: "application/vnd.github.text-match+json" } }
756
+ );
757
+ if (data.error) return JSON.stringify(data);
758
+ return JSON.stringify({
759
+ total: data.total_count ?? 0,
760
+ items: (data.items ?? []).map((i) => {
761
+ const fragment = i.text_matches?.[0]?.fragment ?? "";
762
+ const snip = truncate(fragment, SNIPPET_CAP);
763
+ return {
764
+ name: i.name,
765
+ path: i.path,
766
+ repo: i.repository?.full_name,
767
+ url: i.html_url,
768
+ sha: i.sha,
769
+ snippet: snip.text,
770
+ snippet_truncated: snip.truncated
771
+ };
772
+ })
773
+ });
774
+ },
775
+ {
776
+ name: "github_search_code",
777
+ description: "Search file contents across GitHub. **PREFER THIS over shell-exec'ing the `gh` CLI.** Pass `repo` (\"owner/name\") to scope to one repo; the tool prepends `repo:owner/name `. The `q` body accepts GitHub's full code-search syntax: 'language:ts symbolName', 'extension:py path:tests assert', etc. Each hit returns a 400-char snippet around the match. Note: GitHub code search has stricter rate limits than other endpoints (10/min for unauthenticated, 30/min for authenticated).",
778
+ schema: zod.z.object({
779
+ q: zod.z.string().describe("Code search query (e.g. 'language:ts isLikelyBinary')"),
780
+ repo: zod.z.string().optional().describe("Optional 'owner/name' shortcut; prepended as repo: filter"),
781
+ max_results: zod.z.number().optional().describe("Max items (default 25, max 100)")
782
+ })
783
+ }
784
+ );
785
+ var githubReadTools = [
786
+ // Issues — read
787
+ githubSearchIssuesTool,
788
+ githubGetIssueTool,
789
+ githubListIssueCommentsTool,
790
+ // Pull requests — read
791
+ githubListPullsTool,
792
+ githubGetPullTool,
793
+ githubListPullFilesTool,
794
+ githubListPullReviewsTool,
795
+ // Repo content
796
+ githubGetRepoTool,
797
+ githubListBranchesTool,
798
+ githubGetFileTool,
799
+ githubSearchCodeTool
800
+ ];
801
+ var githubWriteTools = [
802
+ // Issues
803
+ githubCreateIssueTool,
804
+ githubUpdateIssueTool,
805
+ githubAddCommentTool,
806
+ // Pull requests (record edits — open/update/review)
807
+ githubCreatePullTool,
808
+ githubUpdatePullTool,
809
+ githubRequestReviewersTool,
810
+ githubCreateReviewTool
811
+ ];
812
+ var githubExecuteTools = [githubMergePullTool];
813
+ var githubTools = [
814
+ ...githubReadTools,
815
+ ...githubWriteTools,
816
+ ...githubExecuteTools
817
+ ];
818
+
819
+ exports._ghFetch = _ghFetch;
820
+ exports._resolveGithubAuth = _resolveGithubAuth;
821
+ exports.decodeContentsBlob = decodeContentsBlob;
822
+ exports.githubAddCommentTool = githubAddCommentTool;
823
+ exports.githubCreateIssueTool = githubCreateIssueTool;
824
+ exports.githubCreatePullTool = githubCreatePullTool;
825
+ exports.githubCreateReviewTool = githubCreateReviewTool;
826
+ exports.githubExecuteTools = githubExecuteTools;
827
+ exports.githubFetch = githubFetch;
828
+ exports.githubGetFileTool = githubGetFileTool;
829
+ exports.githubGetIssueTool = githubGetIssueTool;
830
+ exports.githubGetPullTool = githubGetPullTool;
831
+ exports.githubGetRepoTool = githubGetRepoTool;
832
+ exports.githubListBranchesTool = githubListBranchesTool;
833
+ exports.githubListIssueCommentsTool = githubListIssueCommentsTool;
834
+ exports.githubListPullFilesTool = githubListPullFilesTool;
835
+ exports.githubListPullReviewsTool = githubListPullReviewsTool;
836
+ exports.githubListPullsTool = githubListPullsTool;
837
+ exports.githubMergePullTool = githubMergePullTool;
838
+ exports.githubReadTools = githubReadTools;
839
+ exports.githubRequestReviewersTool = githubRequestReviewersTool;
840
+ exports.githubSearchCodeTool = githubSearchCodeTool;
841
+ exports.githubSearchIssuesTool = githubSearchIssuesTool;
842
+ exports.githubTools = githubTools;
843
+ exports.githubUpdateIssueTool = githubUpdateIssueTool;
844
+ exports.githubUpdatePullTool = githubUpdatePullTool;
845
+ exports.githubWriteTools = githubWriteTools;
846
+ exports.resolveGithubAuthFromEnv = resolveGithubAuthFromEnv;
847
+ exports.setAuthResolver = setAuthResolver;
848
+ exports.truncate = truncate;
849
+ //# sourceMappingURL=index.cjs.map
850
+ //# sourceMappingURL=index.cjs.map