@cybernetyx1/atlasflow-github 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/LICENSE ADDED
@@ -0,0 +1,18 @@
1
+ PROPRIETARY SOFTWARE LICENSE
2
+
3
+ Copyright (c) 2026 Cybernetyx. All rights reserved.
4
+
5
+ This software and its source code are the proprietary and confidential property
6
+ of the copyright holder. The software is original work authored independently.
7
+
8
+ No part of this software may be copied, reproduced, modified, published,
9
+ distributed, sublicensed, or sold in any form or by any means without the prior
10
+ written permission of the copyright holder, except as expressly permitted by a
11
+ separate written agreement.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
15
+ FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT
16
+ HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION
17
+ OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH THE
18
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,39 @@
1
+ # @cybernetyx1/atlasflow-github
2
+
3
+ GitHub REST tools and persona slot bindings for AtlasFlow agents — read repos, PRs, issues, and files, or post guarded review output.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ pnpm add @cybernetyx1/atlasflow-github
9
+ ```
10
+
11
+ Part of the AtlasFlow monorepo. Proprietary.
12
+
13
+ ## Usage
14
+
15
+ `githubTools(options)` returns the raw GitHub tools for an agent. `githubBinding(options)` returns a persona binding that resolves a token from the environment (fine-grained PAT, GitHub App installation token, or Actions token) and fills the `github` manifest slot.
16
+
17
+ ```ts
18
+ import { githubTools } from "@cybernetyx1/atlasflow-github";
19
+
20
+ // Direct: pass a token and default owner/repo.
21
+ const tools = githubTools({
22
+ token: process.env.GITHUB_TOKEN,
23
+ owner: "cybernetyx",
24
+ repo: "atlasflow",
25
+ });
26
+ ```
27
+
28
+ ```ts
29
+ import { githubBinding } from "@cybernetyx1/atlasflow-github";
30
+
31
+ // Persona slot binding: resolves the token from env at runtime.
32
+ const binding = githubBinding({ tokenEnv: "GITHUB_TOKEN", slot: "github" });
33
+ ```
34
+
35
+ Also exported: `githubToolBinding`, `createGitHubClient`, `createGitHubAppJwt`, `resolveGitHubAppInstallationToken`, `resolveGitHubTokenFromEnv`, the `GitHubApiError` class, and the `GitHubToolsOptions` / `GitHubEnvBindingOptions` types.
36
+
37
+ ## License
38
+
39
+ Proprietary. © 2026 Cybernetyx. See LICENSE.
@@ -0,0 +1,95 @@
1
+ import { PersonaBindings, PersonaToolsBinding, RawTool } from '@cybernetyx1/atlasflow-runtime';
2
+
3
+ interface GitHubToolsOptions {
4
+ /** Fine-grained PAT, GitHub App installation token, or Actions token. */
5
+ token?: string;
6
+ /** Defaults used when a tool call omits owner/repo. */
7
+ owner?: string;
8
+ repo?: string;
9
+ /** Override for GitHub Enterprise Server. Defaults to https://api.github.com. */
10
+ baseUrl?: string;
11
+ /** REST API version header. Defaults to the current public GitHub API version. */
12
+ apiVersion?: string;
13
+ userAgent?: string;
14
+ fetch?: typeof fetch;
15
+ headers?: Record<string, string>;
16
+ /** Per-file patch/content safety caps for model-facing responses. */
17
+ maxPatchChars?: number;
18
+ maxFileContentChars?: number;
19
+ }
20
+ interface GitHubEnvBindingOptions extends Omit<GitHubToolsOptions, "token" | "owner" | "repo"> {
21
+ tokenEnv?: string;
22
+ owner?: string;
23
+ repo?: string;
24
+ ownerEnv?: string;
25
+ repoEnv?: string;
26
+ appId?: string;
27
+ appIdEnv?: string;
28
+ appPrivateKey?: string;
29
+ appPrivateKeyEnv?: string;
30
+ appJwt?: string;
31
+ appJwtEnv?: string;
32
+ installationId?: string;
33
+ installationIdEnv?: string;
34
+ /** Manifest slot satisfied by these tools. Defaults to "github". */
35
+ slot?: string;
36
+ }
37
+ interface GitHubAppJwtOptions {
38
+ appId: string | number;
39
+ privateKey: string;
40
+ /** Test hook; defaults to Date.now(). */
41
+ nowMs?: number;
42
+ }
43
+ interface GitHubAppInstallationTokenOptions {
44
+ appId?: string | number;
45
+ privateKey?: string;
46
+ jwt?: string;
47
+ installationId?: string;
48
+ owner?: string;
49
+ repo?: string;
50
+ repositories?: string[];
51
+ permissions?: Record<string, string>;
52
+ baseUrl?: string;
53
+ apiVersion?: string;
54
+ userAgent?: string;
55
+ fetch?: typeof fetch;
56
+ headers?: Record<string, string>;
57
+ nowMs?: number;
58
+ }
59
+ interface GitHubTokenEnvOptions extends Omit<GitHubAppInstallationTokenOptions, "appId" | "privateKey" | "jwt" | "installationId" | "owner" | "repo"> {
60
+ tokenEnv?: string;
61
+ owner?: string;
62
+ repo?: string;
63
+ ownerEnv?: string;
64
+ repoEnv?: string;
65
+ appId?: string;
66
+ appIdEnv?: string;
67
+ appPrivateKey?: string;
68
+ appPrivateKeyEnv?: string;
69
+ appJwt?: string;
70
+ appJwtEnv?: string;
71
+ installationId?: string;
72
+ installationIdEnv?: string;
73
+ }
74
+ declare class GitHubApiError extends Error {
75
+ status: number;
76
+ code: string;
77
+ details?: unknown | undefined;
78
+ constructor(status: number, code: string, message: string, details?: unknown | undefined);
79
+ }
80
+ interface GitHubRequestOptions {
81
+ method?: string;
82
+ accept?: string;
83
+ body?: unknown;
84
+ }
85
+ declare function createGitHubClient(options?: GitHubToolsOptions): {
86
+ request: <T>(path: string, requestOptions?: GitHubRequestOptions) => Promise<T>;
87
+ };
88
+ declare function createGitHubAppJwt(options: GitHubAppJwtOptions): Promise<string>;
89
+ declare function resolveGitHubAppInstallationToken(options: GitHubAppInstallationTokenOptions): Promise<string>;
90
+ declare function resolveGitHubTokenFromEnv(env: Record<string, unknown>, options?: GitHubTokenEnvOptions): Promise<string | undefined>;
91
+ declare function githubTools(options?: GitHubToolsOptions): RawTool[];
92
+ declare function githubToolBinding(options?: GitHubEnvBindingOptions): PersonaToolsBinding;
93
+ declare function githubBinding(options?: GitHubEnvBindingOptions): PersonaBindings;
94
+
95
+ export { GitHubApiError, type GitHubAppInstallationTokenOptions, type GitHubAppJwtOptions, type GitHubEnvBindingOptions, type GitHubTokenEnvOptions, type GitHubToolsOptions, createGitHubAppJwt, createGitHubClient, githubBinding, githubToolBinding, githubTools, resolveGitHubAppInstallationToken, resolveGitHubTokenFromEnv };
package/dist/index.js ADDED
@@ -0,0 +1,465 @@
1
+ // src/index.ts
2
+ import { rawTool } from "@cybernetyx1/atlasflow-runtime";
3
+ var GitHubApiError = class extends Error {
4
+ constructor(status, code, message, details) {
5
+ super(message);
6
+ this.status = status;
7
+ this.code = code;
8
+ this.details = details;
9
+ this.name = "GitHubApiError";
10
+ }
11
+ status;
12
+ code;
13
+ details;
14
+ };
15
+ var DEFAULT_API_VERSION = "2026-03-10";
16
+ var DEFAULT_BASE_URL = "https://api.github.com";
17
+ var DEFAULT_MAX_PATCH_CHARS = 12e3;
18
+ var DEFAULT_MAX_FILE_CONTENT_CHARS = 2e4;
19
+ function stringParam(value, name) {
20
+ if (typeof value !== "string" || value.trim() === "") throw new Error(`${name} is required`);
21
+ return value;
22
+ }
23
+ function numberParam(value, name) {
24
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 1) throw new Error(`${name} must be a positive integer`);
25
+ return value;
26
+ }
27
+ function optionalNumber(value, fallback, name) {
28
+ if (value === void 0 || value === null) return fallback;
29
+ return numberParam(value, name);
30
+ }
31
+ function resolveRepo(options, args) {
32
+ return {
33
+ owner: stringParam(args.owner ?? options.owner, "owner"),
34
+ repo: stringParam(args.repo ?? options.repo, "repo")
35
+ };
36
+ }
37
+ function enc(value) {
38
+ return encodeURIComponent(String(value));
39
+ }
40
+ function truncate(text, max) {
41
+ return text.length > max ? `${text.slice(0, max)}
42
+ ... [truncated ${text.length - max} chars]` : text;
43
+ }
44
+ function json(data) {
45
+ return JSON.stringify(data);
46
+ }
47
+ function decodeBase64(input) {
48
+ const normalized = input.replace(/\s+/g, "");
49
+ const binary = typeof globalThis.atob === "function" ? globalThis.atob(normalized) : Buffer.from(normalized, "base64").toString("binary");
50
+ const bytes = Uint8Array.from(binary, (ch) => ch.charCodeAt(0));
51
+ return new TextDecoder().decode(bytes);
52
+ }
53
+ function createGitHubClient(options = {}) {
54
+ const doFetch = options.fetch ?? fetch;
55
+ const base = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
56
+ async function request(path, requestOptions = {}) {
57
+ const res = await doFetch(`${base}${path}`, {
58
+ method: requestOptions.method ?? "GET",
59
+ headers: {
60
+ accept: requestOptions.accept ?? "application/vnd.github+json",
61
+ "content-type": "application/json",
62
+ "user-agent": options.userAgent ?? "atlasflow-github",
63
+ "x-github-api-version": options.apiVersion ?? DEFAULT_API_VERSION,
64
+ ...options.token ? { authorization: `Bearer ${options.token}` } : {},
65
+ ...options.headers
66
+ },
67
+ body: requestOptions.body === void 0 ? void 0 : JSON.stringify(requestOptions.body)
68
+ });
69
+ const contentType = res.headers.get("content-type") ?? "";
70
+ const data = contentType.includes("json") ? await res.json().catch(() => void 0) : await res.text();
71
+ if (!res.ok) {
72
+ const error = typeof data === "object" && data ? data : void 0;
73
+ throw new GitHubApiError(res.status, "github_request_failed", error?.message ?? res.statusText, data);
74
+ }
75
+ return data;
76
+ }
77
+ return { request };
78
+ }
79
+ async function createGitHubAppJwt(options) {
80
+ const subtle = globalThis.crypto?.subtle;
81
+ if (!subtle) throw new Error("GitHub App JWT signing requires Web Crypto subtle support.");
82
+ const now = Math.floor((options.nowMs ?? Date.now()) / 1e3);
83
+ const header = base64UrlEncodeString(JSON.stringify({ alg: "RS256", typ: "JWT" }));
84
+ const payload = base64UrlEncodeString(JSON.stringify({ iat: now - 60, exp: now + 9 * 60, iss: String(options.appId) }));
85
+ const data = `${header}.${payload}`;
86
+ const key = await subtle.importKey("pkcs8", privateKeyPemToPkcs8(options.privateKey), { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, false, ["sign"]);
87
+ const signature = await subtle.sign("RSASSA-PKCS1-v1_5", key, new TextEncoder().encode(data));
88
+ return `${data}.${base64UrlEncodeBytes(new Uint8Array(signature))}`;
89
+ }
90
+ async function resolveGitHubAppInstallationToken(options) {
91
+ const jwt = options.jwt ?? (options.appId !== void 0 && options.privateKey ? await createGitHubAppJwt({ appId: options.appId, privateKey: options.privateKey, nowMs: options.nowMs }) : void 0);
92
+ if (!jwt) throw new Error("GitHub App auth requires jwt or appId + privateKey.");
93
+ const client = createGitHubClient({
94
+ token: jwt,
95
+ baseUrl: options.baseUrl,
96
+ apiVersion: options.apiVersion,
97
+ userAgent: options.userAgent ?? "atlasflow-github-app",
98
+ fetch: options.fetch,
99
+ headers: options.headers
100
+ });
101
+ const installationId = options.installationId ?? (options.owner && options.repo ? String((await client.request(`/repos/${enc(options.owner)}/${enc(options.repo)}/installation`)).id ?? "") : void 0);
102
+ if (!installationId) throw new Error("GitHub App auth requires installationId or owner + repo to resolve the installation.");
103
+ const repositories = options.repositories ?? (options.repo ? [options.repo] : void 0);
104
+ const body = {
105
+ ...repositories?.length ? { repositories } : {},
106
+ ...options.permissions ? { permissions: options.permissions } : {}
107
+ };
108
+ const token = await client.request(`/app/installations/${enc(installationId)}/access_tokens`, {
109
+ method: "POST",
110
+ body
111
+ });
112
+ if (typeof token.token !== "string" || !token.token) throw new Error("GitHub App installation token response did not include a token.");
113
+ return token.token;
114
+ }
115
+ async function resolveGitHubTokenFromEnv(env, options = {}) {
116
+ const token = readEnv(env, options.tokenEnv ?? "GITHUB_TOKEN", false);
117
+ if (token) return token;
118
+ const owner = options.owner ?? readEnv(env, options.ownerEnv ?? "GITHUB_OWNER", false);
119
+ const repo = options.repo ?? readEnv(env, options.repoEnv ?? "GITHUB_REPO", false);
120
+ const appId = options.appId ?? readEnv(env, options.appIdEnv ?? "GITHUB_APP_ID", false);
121
+ const privateKey = options.appPrivateKey ?? readEnv(env, options.appPrivateKeyEnv ?? "GITHUB_APP_PRIVATE_KEY", false);
122
+ const jwt = options.appJwt ?? readEnv(env, options.appJwtEnv ?? "GITHUB_APP_JWT", false);
123
+ const installationId = options.installationId ?? readEnv(env, options.installationIdEnv ?? "GITHUB_INSTALLATION_ID", false);
124
+ if (!jwt && (!appId || !privateKey)) return void 0;
125
+ return resolveGitHubAppInstallationToken({
126
+ ...options,
127
+ owner,
128
+ repo,
129
+ appId,
130
+ privateKey,
131
+ jwt,
132
+ installationId
133
+ });
134
+ }
135
+ function githubTools(options = {}) {
136
+ const client = createGitHubClient(options);
137
+ const maxPatchChars = options.maxPatchChars ?? DEFAULT_MAX_PATCH_CHARS;
138
+ const maxFileContentChars = options.maxFileContentChars ?? DEFAULT_MAX_FILE_CONTENT_CHARS;
139
+ return [
140
+ rawTool({
141
+ name: "github_get_pull_request",
142
+ description: "Fetch pull request metadata from GitHub.",
143
+ parameters: schema(
144
+ {
145
+ owner: stringSchema("Repository owner. Optional when bound by the workspace."),
146
+ repo: stringSchema("Repository name. Optional when bound by the workspace."),
147
+ pull_number: integerSchema("Pull request number.")
148
+ },
149
+ ["pull_number"]
150
+ ),
151
+ execute: async (args) => {
152
+ const { owner, repo } = resolveRepo(options, args);
153
+ const pullNumber = numberParam(args.pull_number, "pull_number");
154
+ const pr = await client.request(`/repos/${enc(owner)}/${enc(repo)}/pulls/${enc(pullNumber)}`);
155
+ return json({
156
+ number: pr.number,
157
+ title: pr.title,
158
+ body: pr.body,
159
+ state: pr.state,
160
+ draft: pr.draft,
161
+ merged: pr.merged,
162
+ author: pr.user?.login,
163
+ head: pr.head,
164
+ base: pr.base,
165
+ additions: pr.additions,
166
+ deletions: pr.deletions,
167
+ changed_files: pr.changed_files,
168
+ html_url: pr.html_url
169
+ });
170
+ }
171
+ }),
172
+ rawTool({
173
+ name: "github_list_pull_request_files",
174
+ description: "List files changed by a pull request, including truncated patches when GitHub provides them.",
175
+ parameters: schema(
176
+ {
177
+ owner: stringSchema("Repository owner. Optional when bound by the workspace."),
178
+ repo: stringSchema("Repository name. Optional when bound by the workspace."),
179
+ pull_number: integerSchema("Pull request number."),
180
+ per_page: integerSchema("Results per page, max 100."),
181
+ page: integerSchema("Page number.")
182
+ },
183
+ ["pull_number"]
184
+ ),
185
+ execute: async (args) => {
186
+ const { owner, repo } = resolveRepo(options, args);
187
+ const pullNumber = numberParam(args.pull_number, "pull_number");
188
+ const perPage = Math.min(optionalNumber(args.per_page, 100, "per_page"), 100);
189
+ const page = optionalNumber(args.page, 1, "page");
190
+ const files = await client.request(
191
+ `/repos/${enc(owner)}/${enc(repo)}/pulls/${enc(pullNumber)}/files?per_page=${enc(perPage)}&page=${enc(page)}`
192
+ );
193
+ return json({
194
+ files: files.map((file) => ({
195
+ filename: file.filename,
196
+ status: file.status,
197
+ additions: file.additions,
198
+ deletions: file.deletions,
199
+ changes: file.changes,
200
+ previous_filename: file.previous_filename,
201
+ raw_url: file.raw_url,
202
+ blob_url: file.blob_url,
203
+ patch: typeof file.patch === "string" ? truncate(file.patch, maxPatchChars) : void 0
204
+ }))
205
+ });
206
+ }
207
+ }),
208
+ rawTool({
209
+ name: "github_get_issue",
210
+ description: "Fetch issue or pull request timeline metadata from the GitHub Issues API.",
211
+ parameters: schema(
212
+ {
213
+ owner: stringSchema("Repository owner. Optional when bound by the workspace."),
214
+ repo: stringSchema("Repository name. Optional when bound by the workspace."),
215
+ issue_number: integerSchema("Issue or pull request number.")
216
+ },
217
+ ["issue_number"]
218
+ ),
219
+ execute: async (args) => {
220
+ const { owner, repo } = resolveRepo(options, args);
221
+ const issueNumber = numberParam(args.issue_number, "issue_number");
222
+ const issue = await client.request(`/repos/${enc(owner)}/${enc(repo)}/issues/${enc(issueNumber)}`);
223
+ return json({
224
+ number: issue.number,
225
+ title: issue.title,
226
+ body: issue.body,
227
+ state: issue.state,
228
+ author: issue.user?.login,
229
+ labels: issue.labels,
230
+ assignees: issue.assignees,
231
+ pull_request: issue.pull_request,
232
+ html_url: issue.html_url
233
+ });
234
+ }
235
+ }),
236
+ rawTool({
237
+ name: "github_list_issue_comments",
238
+ description: "List comments on a GitHub issue or pull request conversation.",
239
+ parameters: schema(
240
+ {
241
+ owner: stringSchema("Repository owner. Optional when bound by the workspace."),
242
+ repo: stringSchema("Repository name. Optional when bound by the workspace."),
243
+ issue_number: integerSchema("Issue or pull request number."),
244
+ per_page: integerSchema("Results per page, max 100."),
245
+ page: integerSchema("Page number.")
246
+ },
247
+ ["issue_number"]
248
+ ),
249
+ execute: async (args) => {
250
+ const { owner, repo } = resolveRepo(options, args);
251
+ const issueNumber = numberParam(args.issue_number, "issue_number");
252
+ const perPage = Math.min(optionalNumber(args.per_page, 50, "per_page"), 100);
253
+ const page = optionalNumber(args.page, 1, "page");
254
+ const comments = await client.request(
255
+ `/repos/${enc(owner)}/${enc(repo)}/issues/${enc(issueNumber)}/comments?per_page=${enc(perPage)}&page=${enc(page)}`
256
+ );
257
+ return json({
258
+ comments: comments.map((comment) => ({
259
+ id: comment.id,
260
+ author: comment.user?.login,
261
+ body: comment.body,
262
+ created_at: comment.created_at,
263
+ updated_at: comment.updated_at,
264
+ html_url: comment.html_url
265
+ }))
266
+ });
267
+ }
268
+ }),
269
+ rawTool({
270
+ name: "github_create_issue_comment",
271
+ description: "Create a timeline comment on a GitHub issue or pull request.",
272
+ parameters: schema(
273
+ {
274
+ owner: stringSchema("Repository owner. Optional when bound by the workspace."),
275
+ repo: stringSchema("Repository name. Optional when bound by the workspace."),
276
+ issue_number: integerSchema("Issue or pull request number."),
277
+ body: stringSchema("Markdown comment body.")
278
+ },
279
+ ["issue_number", "body"]
280
+ ),
281
+ execute: async (args) => {
282
+ const { owner, repo } = resolveRepo(options, args);
283
+ const issueNumber = numberParam(args.issue_number, "issue_number");
284
+ const body = stringParam(args.body, "body");
285
+ const comment = await client.request(`/repos/${enc(owner)}/${enc(repo)}/issues/${enc(issueNumber)}/comments`, {
286
+ method: "POST",
287
+ body: { body }
288
+ });
289
+ return json({ id: comment.id, html_url: comment.html_url, body: comment.body });
290
+ }
291
+ }),
292
+ rawTool({
293
+ name: "github_get_file",
294
+ description: "Fetch and decode a file from a repository using the GitHub Contents API.",
295
+ parameters: schema(
296
+ {
297
+ owner: stringSchema("Repository owner. Optional when bound by the workspace."),
298
+ repo: stringSchema("Repository name. Optional when bound by the workspace."),
299
+ path: stringSchema("File path inside the repository."),
300
+ ref: stringSchema("Branch, tag, or commit SHA.")
301
+ },
302
+ ["path"]
303
+ ),
304
+ execute: async (args) => {
305
+ const { owner, repo } = resolveRepo(options, args);
306
+ const filePath = stringParam(args.path, "path");
307
+ const ref = typeof args.ref === "string" && args.ref ? `?ref=${enc(args.ref)}` : "";
308
+ const content = await client.request(`/repos/${enc(owner)}/${enc(repo)}/contents/${filePath.split("/").map(enc).join("/")}${ref}`);
309
+ if (content.type !== "file" || typeof content.content !== "string") throw new Error(`GitHub content is not a file: ${filePath}`);
310
+ return json({
311
+ path: content.path,
312
+ sha: content.sha,
313
+ size: content.size,
314
+ html_url: content.html_url,
315
+ content: truncate(decodeBase64(content.content), maxFileContentChars)
316
+ });
317
+ }
318
+ }),
319
+ rawTool({
320
+ name: "github_create_pull_request_review",
321
+ description: "Create a GitHub pull request review summary, optionally with diff-position comments.",
322
+ parameters: schema(
323
+ {
324
+ owner: stringSchema("Repository owner. Optional when bound by the workspace."),
325
+ repo: stringSchema("Repository name. Optional when bound by the workspace."),
326
+ pull_number: integerSchema("Pull request number."),
327
+ body: stringSchema("Review body in Markdown."),
328
+ event: {
329
+ type: "string",
330
+ enum: ["COMMENT", "APPROVE", "REQUEST_CHANGES"],
331
+ description: "Review event. COMMENT is safest for automated reviews."
332
+ },
333
+ commit_id: stringSchema("Specific commit SHA to review."),
334
+ comments: {
335
+ type: "array",
336
+ description: "Optional diff-position review comments.",
337
+ items: {
338
+ type: "object",
339
+ properties: {
340
+ path: stringSchema("Changed file path."),
341
+ position: integerSchema("Diff position, not file line number."),
342
+ body: stringSchema("Comment body.")
343
+ },
344
+ required: ["path", "position", "body"],
345
+ additionalProperties: false
346
+ }
347
+ }
348
+ },
349
+ ["pull_number", "body", "event"]
350
+ ),
351
+ execute: async (args) => {
352
+ const { owner, repo } = resolveRepo(options, args);
353
+ const pullNumber = numberParam(args.pull_number, "pull_number");
354
+ const body = stringParam(args.body, "body");
355
+ const event = stringParam(args.event, "event");
356
+ if (!["COMMENT", "APPROVE", "REQUEST_CHANGES"].includes(event)) throw new Error("event must be COMMENT, APPROVE, or REQUEST_CHANGES");
357
+ const review = await client.request(`/repos/${enc(owner)}/${enc(repo)}/pulls/${enc(pullNumber)}/reviews`, {
358
+ method: "POST",
359
+ body: {
360
+ body,
361
+ event,
362
+ ...typeof args.commit_id === "string" && args.commit_id ? { commit_id: args.commit_id } : {},
363
+ ...Array.isArray(args.comments) ? { comments: args.comments } : {}
364
+ }
365
+ });
366
+ return json({ id: review.id, state: review.state, html_url: review.html_url, body: review.body });
367
+ }
368
+ })
369
+ ];
370
+ }
371
+ function githubToolBinding(options = {}) {
372
+ return async ({ env }) => {
373
+ const owner = options.owner ?? readEnv(env, options.ownerEnv ?? "GITHUB_OWNER", false);
374
+ const repo = options.repo ?? readEnv(env, options.repoEnv ?? "GITHUB_REPO", false);
375
+ return githubTools({
376
+ ...options,
377
+ token: await resolveGitHubTokenFromEnv(env, { ...options, owner, repo }),
378
+ owner,
379
+ repo
380
+ });
381
+ };
382
+ }
383
+ function githubBinding(options = {}) {
384
+ const slot = options.slot ?? "github";
385
+ return { toolSlots: { [slot]: githubToolBinding(options) } };
386
+ }
387
+ function readEnv(env, name, required) {
388
+ const value = env[name];
389
+ if (typeof value === "string" && value) return value;
390
+ if (required) throw new Error(`${name} is required for GitHub tools`);
391
+ return void 0;
392
+ }
393
+ function stringSchema(description) {
394
+ return { type: "string", description };
395
+ }
396
+ function integerSchema(description) {
397
+ return { type: "integer", minimum: 1, description };
398
+ }
399
+ function schema(properties, required) {
400
+ return {
401
+ type: "object",
402
+ properties,
403
+ required,
404
+ additionalProperties: false
405
+ };
406
+ }
407
+ function base64UrlEncodeString(input) {
408
+ return base64UrlEncodeBytes(new TextEncoder().encode(input));
409
+ }
410
+ function base64UrlEncodeBytes(input) {
411
+ return bytesToBase64(input).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
412
+ }
413
+ function bytesToBase64(input) {
414
+ let binary = "";
415
+ for (const byte of input) binary += String.fromCharCode(byte);
416
+ if (typeof globalThis.btoa === "function") return globalThis.btoa(binary);
417
+ return Buffer.from(input).toString("base64");
418
+ }
419
+ function base64ToBytes(input) {
420
+ const binary = typeof globalThis.atob === "function" ? globalThis.atob(input) : Buffer.from(input, "base64").toString("binary");
421
+ return Uint8Array.from(binary, (ch) => ch.charCodeAt(0));
422
+ }
423
+ function privateKeyPemToPkcs8(pem) {
424
+ const normalized = pem.replace(/\\n/g, "\n");
425
+ const der2 = base64ToBytes(normalized.replace(/-----BEGIN [^-]+-----/g, "").replace(/-----END [^-]+-----/g, "").replace(/\s+/g, ""));
426
+ if (/-----BEGIN RSA PRIVATE KEY-----/.test(normalized)) return pkcs1ToPkcs8(der2);
427
+ return der2;
428
+ }
429
+ function pkcs1ToPkcs8(pkcs1) {
430
+ const version = Uint8Array.from([2, 1, 0]);
431
+ const rsaAlgorithm = Uint8Array.from([48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0]);
432
+ return der(48, concatBytes(version, rsaAlgorithm, der(4, pkcs1)));
433
+ }
434
+ function der(tag, body) {
435
+ return concatBytes(Uint8Array.from([tag]), derLength(body.length), body);
436
+ }
437
+ function derLength(length) {
438
+ if (length < 128) return Uint8Array.from([length]);
439
+ const bytes = [];
440
+ let n = length;
441
+ while (n > 0) {
442
+ bytes.unshift(n & 255);
443
+ n >>= 8;
444
+ }
445
+ return Uint8Array.from([128 | bytes.length, ...bytes]);
446
+ }
447
+ function concatBytes(...parts) {
448
+ const out = new Uint8Array(parts.reduce((sum, part) => sum + part.length, 0));
449
+ let offset = 0;
450
+ for (const part of parts) {
451
+ out.set(part, offset);
452
+ offset += part.length;
453
+ }
454
+ return out;
455
+ }
456
+ export {
457
+ GitHubApiError,
458
+ createGitHubAppJwt,
459
+ createGitHubClient,
460
+ githubBinding,
461
+ githubToolBinding,
462
+ githubTools,
463
+ resolveGitHubAppInstallationToken,
464
+ resolveGitHubTokenFromEnv
465
+ };
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@cybernetyx1/atlasflow-github",
3
+ "version": "0.1.0",
4
+ "description": "GitHub REST tools and bindings for AtlasFlow agents.",
5
+ "type": "module",
6
+ "license": "SEE LICENSE IN LICENSE",
7
+ "author": "Cybernetyx",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/Cybernetyx/atlasflow.git",
11
+ "directory": "packages/github"
12
+ },
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "import": "./dist/index.js"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist"
21
+ ],
22
+ "dependencies": {
23
+ "@cybernetyx1/atlasflow-runtime": "0.1.0"
24
+ },
25
+ "devDependencies": {
26
+ "@types/node": "^22.10.0",
27
+ "tsup": "^8.3.5",
28
+ "typescript": "^5.7.2"
29
+ },
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "scripts": {
34
+ "build": "tsup",
35
+ "typecheck": "tsc --noEmit",
36
+ "test": "node --import tsx --test test/*.test.ts"
37
+ }
38
+ }