causly-server 0.0.0-stage → 1.5.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.
Files changed (91) hide show
  1. package/.env.example +120 -0
  2. package/CHANGELOG.md +121 -0
  3. package/LICENSE +21 -0
  4. package/README.md +205 -3
  5. package/bin/causly.js +192 -0
  6. package/index.js +229 -0
  7. package/package.json +59 -6
  8. package/packs/calendar.js +141 -0
  9. package/packs/command.js +22 -0
  10. package/packs/database.js +95 -0
  11. package/packs/directory.js +63 -0
  12. package/packs/discord.js +63 -0
  13. package/packs/docker.js +202 -0
  14. package/packs/files.js +106 -0
  15. package/packs/git.js +211 -0
  16. package/packs/github.js +321 -0
  17. package/packs/gmail.js +98 -0
  18. package/packs/leads.js +46 -0
  19. package/packs/linear.js +104 -0
  20. package/packs/loader.js +247 -0
  21. package/packs/manifest.json +1152 -0
  22. package/packs/meetings.js +141 -0
  23. package/packs/notion.js +185 -0
  24. package/packs/pipeline.js +151 -0
  25. package/packs/project.js +70 -0
  26. package/packs/registry.js +149 -0
  27. package/packs/reminders.js +57 -0
  28. package/packs/research.js +34 -0
  29. package/packs/secrets.js +77 -0
  30. package/packs/sentry.js +97 -0
  31. package/packs/seo.js +70 -0
  32. package/packs/slack.js +95 -0
  33. package/packs/stripe.js +127 -0
  34. package/packs/supabase.js +73 -0
  35. package/packs/telegram.js +70 -0
  36. package/packs/terraform.js +247 -0
  37. package/packs/vercel.js +114 -0
  38. package/packs/workflow.js +84 -0
  39. package/prompts/cofounder.js +66 -0
  40. package/prompts/playbooks.js +75 -0
  41. package/tools/backupOps.js +87 -0
  42. package/tools/boundaryAttest.js +184 -0
  43. package/tools/calendarOps.js +563 -0
  44. package/tools/cliOps.js +233 -0
  45. package/tools/commandOps.js +198 -0
  46. package/tools/dbOps.js +117 -0
  47. package/tools/directoryOps.js +96 -0
  48. package/tools/discordOps.js +133 -0
  49. package/tools/dockerOps.js +259 -0
  50. package/tools/envFile.js +89 -0
  51. package/tools/envLoader.js +85 -0
  52. package/tools/fileOps.js +125 -0
  53. package/tools/gitOps.js +170 -0
  54. package/tools/githubOps.js +357 -0
  55. package/tools/gmailOps.js +221 -0
  56. package/tools/googleAuth.js +51 -0
  57. package/tools/googleLogin.js +230 -0
  58. package/tools/leadOps.js +447 -0
  59. package/tools/linearOps.js +304 -0
  60. package/tools/logger.js +47 -0
  61. package/tools/meetingOps.js +697 -0
  62. package/tools/notionOps.js +215 -0
  63. package/tools/paths.js +36 -0
  64. package/tools/pipelineOps.js +511 -0
  65. package/tools/projectOps.js +192 -0
  66. package/tools/reminderAlerts.js +48 -0
  67. package/tools/reminderOps.js +129 -0
  68. package/tools/scheduler.js +46 -0
  69. package/tools/secretsOps.js +133 -0
  70. package/tools/security.js +488 -0
  71. package/tools/sentryOps.js +130 -0
  72. package/tools/seoOps.js +561 -0
  73. package/tools/services.js +295 -0
  74. package/tools/setupOps.js +245 -0
  75. package/tools/slackOps.js +151 -0
  76. package/tools/statusOps.js +104 -0
  77. package/tools/store.js +111 -0
  78. package/tools/stripeOps.js +299 -0
  79. package/tools/supabaseOps.js +94 -0
  80. package/tools/telegramOps.js +153 -0
  81. package/tools/terminal.js +124 -0
  82. package/tools/terraformOps.js +171 -0
  83. package/tools/toolWrapper.js +60 -0
  84. package/tools/toolsetOps.js +123 -0
  85. package/tools/transcribeOps.js +221 -0
  86. package/tools/updateCheck.js +58 -0
  87. package/tools/vercelOps.js +198 -0
  88. package/tools/verifyBoundaryAttest.js +25 -0
  89. package/tools/webOps.js +474 -0
  90. package/tools/workflowOps.js +371 -0
  91. package/tools/workspace.js +85 -0
@@ -0,0 +1,170 @@
1
+ import { simpleGit } from "simple-git";
2
+
3
+ function getGit(repoPath) {
4
+ return simpleGit({ baseDir: repoPath });
5
+ }
6
+
7
+ export async function gitInit({ repo_path }) {
8
+ const git = getGit(repo_path);
9
+ const result = await git.init();
10
+ return { repo_path, initialized: true, result };
11
+ }
12
+
13
+ export async function gitStatus({ repo_path }) {
14
+ const git = getGit(repo_path);
15
+ const status = await git.status();
16
+ return { repo_path, status };
17
+ }
18
+
19
+ export async function gitAdd({ repo_path, files = "." }) {
20
+ const git = getGit(repo_path);
21
+ await git.add(files);
22
+ return { repo_path, added: files };
23
+ }
24
+
25
+ export async function gitCommit({ repo_path, message }) {
26
+ const git = getGit(repo_path);
27
+ const result = await git.commit(message);
28
+ return { repo_path, commit: result };
29
+ }
30
+
31
+ export async function gitPush({ repo_path, remote = "origin", branch }) {
32
+ const git = getGit(repo_path);
33
+ const result = branch ? await git.push(remote, branch) : await git.push();
34
+ return { repo_path, result };
35
+ }
36
+
37
+ export async function gitPull({ repo_path, remote = "origin", branch }) {
38
+ const git = getGit(repo_path);
39
+ const result = branch ? await git.pull(remote, branch) : await git.pull();
40
+ return { repo_path, result };
41
+ }
42
+
43
+ export async function gitLog({ repo_path, max_count = 10 }) {
44
+ const git = getGit(repo_path);
45
+ const log = await git.log({ maxCount: max_count });
46
+ return { repo_path, log: log.all };
47
+ }
48
+
49
+ export async function gitDiff({ repo_path, file }) {
50
+ const git = getGit(repo_path);
51
+ const diff = file ? await git.diff([file]) : await git.diff();
52
+ return { repo_path, diff };
53
+ }
54
+
55
+ export async function gitBranch({ repo_path }) {
56
+ const git = getGit(repo_path);
57
+ const branches = await git.branch();
58
+ return { repo_path, branches };
59
+ }
60
+
61
+ export async function gitCreateBranch({ repo_path, branch_name, checkout = true }) {
62
+ const git = getGit(repo_path);
63
+ if (checkout) {
64
+ await git.checkoutLocalBranch(branch_name);
65
+ } else {
66
+ await git.branch([branch_name]);
67
+ }
68
+ return { repo_path, branch_name, checked_out: checkout };
69
+ }
70
+
71
+ export async function gitCheckout({ repo_path, branch }) {
72
+ const git = getGit(repo_path);
73
+ await git.checkout(branch);
74
+ return { repo_path, checked_out: branch };
75
+ }
76
+
77
+ export async function gitMerge({ repo_path, branch }) {
78
+ const git = getGit(repo_path);
79
+ const result = await git.merge([branch]);
80
+ return { repo_path, branch, result };
81
+ }
82
+
83
+ export async function gitReset({ repo_path, mode = "mixed", ref = "HEAD" }) {
84
+ const git = getGit(repo_path);
85
+ const flag = mode === "hard" ? "--hard" : mode === "soft" ? "--soft" : "--mixed";
86
+ await git.reset([flag, ref]);
87
+ return { repo_path, mode, ref };
88
+ }
89
+
90
+ export async function gitStash({ repo_path, action = "save", message, stash_id }) {
91
+ const git = getGit(repo_path);
92
+ let result;
93
+ if (action === "save") {
94
+ result = message ? await git.stash(["push", "-m", message]) : await git.stash();
95
+ } else if (action === "pop") {
96
+ result = await git.stash(["pop", ...(stash_id ? [stash_id] : [])]);
97
+ } else if (action === "apply") {
98
+ result = await git.stash(["apply", ...(stash_id ? [stash_id] : [])]);
99
+ } else if (action === "list") {
100
+ result = await git.stashList();
101
+ } else if (action === "drop") {
102
+ result = await git.stash(["drop", ...(stash_id ? [stash_id] : [])]);
103
+ } else {
104
+ throw new Error(`Unknown stash action: ${action}`);
105
+ }
106
+ return { repo_path, action, result };
107
+ }
108
+
109
+ export async function gitShow({ repo_path, ref = "HEAD" }) {
110
+ const git = getGit(repo_path);
111
+ const result = await git.show([ref]);
112
+ return { repo_path, ref, result };
113
+ }
114
+
115
+ export async function gitRemote({ repo_path, action = "list", name, url }) {
116
+ const git = getGit(repo_path);
117
+ let result;
118
+ if (action === "list") {
119
+ result = await git.getRemotes(true);
120
+ } else if (action === "add") {
121
+ result = await git.addRemote(name, url);
122
+ } else if (action === "remove") {
123
+ result = await git.removeRemote(name);
124
+ } else {
125
+ throw new Error(`Unknown remote action: ${action}`);
126
+ }
127
+ return { repo_path, action, result };
128
+ }
129
+
130
+ export async function gitTag({ repo_path, action = "list", tag_name, message }) {
131
+ const git = getGit(repo_path);
132
+ let result;
133
+ if (action === "list") {
134
+ result = await git.tags();
135
+ } else if (action === "create") {
136
+ result = message ? await git.addAnnotatedTag(tag_name, message) : await git.addTag(tag_name);
137
+ } else if (action === "delete") {
138
+ result = await git.tag(["-d", tag_name]);
139
+ } else {
140
+ throw new Error(`Unknown tag action: ${action}`);
141
+ }
142
+ return { repo_path, action, result };
143
+ }
144
+
145
+ export async function gitChangedFiles({ repo_path }) {
146
+ const git = getGit(repo_path);
147
+ const status = await git.status();
148
+ return {
149
+ repo_path,
150
+ changed_files: [
151
+ ...status.modified.map((f) => ({ file: f, type: "modified" })),
152
+ ...status.not_added.map((f) => ({ file: f, type: "untracked" })),
153
+ ...status.created.map((f) => ({ file: f, type: "created" })),
154
+ ...status.deleted.map((f) => ({ file: f, type: "deleted" })),
155
+ ...status.renamed.map((f) => ({ file: f.to, type: "renamed" })),
156
+ ],
157
+ };
158
+ }
159
+
160
+ export async function gitDiffStat({ repo_path, file }) {
161
+ const git = getGit(repo_path);
162
+ const summary = file ? await git.diffSummary([file]) : await git.diffSummary();
163
+ return { repo_path, summary };
164
+ }
165
+
166
+ export async function gitCheckClean({ repo_path }) {
167
+ const git = getGit(repo_path);
168
+ const status = await git.status();
169
+ return { repo_path, is_clean: status.isClean(), status };
170
+ }
@@ -0,0 +1,357 @@
1
+ const GITHUB_API = "https://api.github.com";
2
+
3
+ function getToken() {
4
+ const token = process.env.GITHUB_TOKEN;
5
+ if (!token) {
6
+ throw new Error(
7
+ "GITHUB_TOKEN not set. Run `npm run setup` to configure it, or add it manually to your .env file: GITHUB_TOKEN=github_pat_..."
8
+ );
9
+ }
10
+ return token;
11
+ }
12
+
13
+ async function githubFetch(path, options = {}) {
14
+ const token = getToken();
15
+ const res = await fetch(`${GITHUB_API}${path}`, {
16
+ ...options,
17
+ headers: {
18
+ Authorization: `Bearer ${token}`,
19
+ Accept: "application/vnd.github+json",
20
+ "X-GitHub-Api-Version": "2022-11-28",
21
+ "User-Agent": "causly-server",
22
+ ...(options.body ? { "Content-Type": "application/json" } : {}),
23
+ ...options.headers,
24
+ },
25
+ });
26
+
27
+ const text = await res.text();
28
+ let data;
29
+ try {
30
+ data = text ? JSON.parse(text) : null;
31
+ } catch {
32
+ data = text;
33
+ }
34
+
35
+ if (!res.ok) {
36
+ const message = data?.message || res.statusText;
37
+ throw new Error(`GitHub API error (${res.status}): ${message}`);
38
+ }
39
+
40
+ return data;
41
+ }
42
+
43
+ /** Get the authenticated user's profile — useful as a connectivity/auth check. */
44
+ export async function githubGetAuthenticatedUser() {
45
+ const data = await githubFetch("/user");
46
+ return { login: data.login, name: data.name, public_repos: data.public_repos };
47
+ }
48
+
49
+ /** Create a new repository for the authenticated user. */
50
+ export async function githubCreateRepo({ name, description = "", private: isPrivate = false, auto_init = true }) {
51
+ const data = await githubFetch("/user/repos", {
52
+ method: "POST",
53
+ body: JSON.stringify({ name, description, private: isPrivate, auto_init }),
54
+ });
55
+ return { name: data.name, full_name: data.full_name, html_url: data.html_url, clone_url: data.clone_url };
56
+ }
57
+
58
+ /** Delete a repository. Requires Administration: write on the token. */
59
+ export async function githubDeleteRepo({ owner, repo }) {
60
+ await githubFetch(`/repos/${owner}/${repo}`, { method: "DELETE" });
61
+ return { owner, repo, deleted: true };
62
+ }
63
+
64
+ /** List repositories for the authenticated user. */
65
+ export async function githubListRepos({ per_page = 30, sort = "updated" }) {
66
+ const data = await githubFetch(`/user/repos?per_page=${per_page}&sort=${sort}`);
67
+ return {
68
+ repos: data.map((r) => ({ name: r.name, full_name: r.full_name, private: r.private, html_url: r.html_url })),
69
+ };
70
+ }
71
+
72
+ /** Create an issue on a repository. */
73
+ export async function githubCreateIssue({ owner, repo, title, body = "", labels = [] }) {
74
+ const data = await githubFetch(`/repos/${owner}/${repo}/issues`, {
75
+ method: "POST",
76
+ body: JSON.stringify({ title, body, labels }),
77
+ });
78
+ return { number: data.number, html_url: data.html_url, title: data.title };
79
+ }
80
+
81
+ /** List issues on a repository. */
82
+ export async function githubListIssues({ owner, repo, state = "open", per_page = 30 }) {
83
+ const data = await githubFetch(`/repos/${owner}/${repo}/issues?state=${state}&per_page=${per_page}`);
84
+ return {
85
+ issues: data
86
+ .filter((i) => !i.pull_request) // GitHub's issues endpoint also returns PRs
87
+ .map((i) => ({ number: i.number, title: i.title, state: i.state, html_url: i.html_url })),
88
+ };
89
+ }
90
+
91
+ /** Create a pull request. */
92
+ export async function githubCreatePullRequest({ owner, repo, title, head, base = "main", body = "" }) {
93
+ const data = await githubFetch(`/repos/${owner}/${repo}/pulls`, {
94
+ method: "POST",
95
+ body: JSON.stringify({ title, head, base, body }),
96
+ });
97
+ return { number: data.number, html_url: data.html_url, title: data.title, state: data.state };
98
+ }
99
+
100
+ /** List pull requests on a repository. */
101
+ export async function githubListPullRequests({ owner, repo, state = "open", per_page = 30 }) {
102
+ const data = await githubFetch(`/repos/${owner}/${repo}/pulls?state=${state}&per_page=${per_page}`);
103
+ return {
104
+ pull_requests: data.map((p) => ({ number: p.number, title: p.title, state: p.state, html_url: p.html_url })),
105
+ };
106
+ }
107
+
108
+ /** Add a comment to an issue or pull request (both share the same comments endpoint). */
109
+ export async function githubAddComment({ owner, repo, issue_number, body }) {
110
+ const data = await githubFetch(`/repos/${owner}/${repo}/issues/${issue_number}/comments`, {
111
+ method: "POST",
112
+ body: JSON.stringify({ body }),
113
+ });
114
+ return { id: data.id, html_url: data.html_url };
115
+ }
116
+
117
+ /** Get a repository's details. */
118
+ export async function githubGetRepo({ owner, repo }) {
119
+ const data = await githubFetch(`/repos/${owner}/${repo}`);
120
+ return {
121
+ name: data.name,
122
+ full_name: data.full_name,
123
+ private: data.private,
124
+ default_branch: data.default_branch,
125
+ html_url: data.html_url,
126
+ description: data.description,
127
+ open_issues_count: data.open_issues_count,
128
+ language: data.language,
129
+ };
130
+ }
131
+
132
+ /** Get a single issue. */
133
+ export async function githubGetIssue({ owner, repo, issue_number }) {
134
+ const data = await githubFetch(`/repos/${owner}/${repo}/issues/${issue_number}`);
135
+ return {
136
+ number: data.number,
137
+ title: data.title,
138
+ state: data.state,
139
+ body: data.body,
140
+ labels: data.labels?.map((l) => l.name),
141
+ html_url: data.html_url,
142
+ };
143
+ }
144
+
145
+ /** Update an issue (title, body, state, labels). */
146
+ export async function githubUpdateIssue({ owner, repo, issue_number, title, body, state, labels }) {
147
+ const payload = {};
148
+ if (title !== undefined) payload.title = title;
149
+ if (body !== undefined) payload.body = body;
150
+ if (state !== undefined) payload.state = state;
151
+ if (labels !== undefined) payload.labels = labels;
152
+ const data = await githubFetch(`/repos/${owner}/${repo}/issues/${issue_number}`, {
153
+ method: "PATCH",
154
+ body: JSON.stringify(payload),
155
+ });
156
+ return { number: data.number, title: data.title, state: data.state, html_url: data.html_url };
157
+ }
158
+
159
+ /** Get a single pull request. */
160
+ export async function githubGetPullRequest({ owner, repo, pull_number }) {
161
+ const data = await githubFetch(`/repos/${owner}/${repo}/pulls/${pull_number}`);
162
+ return {
163
+ number: data.number,
164
+ title: data.title,
165
+ state: data.state,
166
+ body: data.body,
167
+ head: data.head?.ref,
168
+ base: data.base?.ref,
169
+ mergeable: data.mergeable,
170
+ merged: data.merged,
171
+ html_url: data.html_url,
172
+ };
173
+ }
174
+
175
+ /** Update a pull request (title, body, state, base). */
176
+ export async function githubUpdatePullRequest({ owner, repo, pull_number, title, body, state, base }) {
177
+ const payload = {};
178
+ if (title !== undefined) payload.title = title;
179
+ if (body !== undefined) payload.body = body;
180
+ if (state !== undefined) payload.state = state;
181
+ if (base !== undefined) payload.base = base;
182
+ const data = await githubFetch(`/repos/${owner}/${repo}/pulls/${pull_number}`, {
183
+ method: "PATCH",
184
+ body: JSON.stringify(payload),
185
+ });
186
+ return { number: data.number, title: data.title, state: data.state, html_url: data.html_url };
187
+ }
188
+
189
+ /** Merge a pull request. */
190
+ export async function githubMergePullRequest({ owner, repo, pull_number, merge_method = "merge", commit_title, commit_message }) {
191
+ const payload = { merge_method };
192
+ if (commit_title) payload.commit_title = commit_title;
193
+ if (commit_message) payload.commit_message = commit_message;
194
+ const data = await githubFetch(`/repos/${owner}/${repo}/pulls/${pull_number}/merge`, {
195
+ method: "PUT",
196
+ body: JSON.stringify(payload),
197
+ });
198
+ return { merged: data.merged, sha: data.sha, message: data.message };
199
+ }
200
+
201
+ /** List files changed in a pull request. */
202
+ export async function githubGetPullRequestFiles({ owner, repo, pull_number, per_page = 30 }) {
203
+ const data = await githubFetch(`/repos/${owner}/${repo}/pulls/${pull_number}/files?per_page=${per_page}`);
204
+ return {
205
+ files: data.map((f) => ({
206
+ filename: f.filename,
207
+ status: f.status,
208
+ additions: f.additions,
209
+ deletions: f.deletions,
210
+ changes: f.changes,
211
+ })),
212
+ };
213
+ }
214
+
215
+ /** Get the raw diff of a pull request. */
216
+ export async function githubGetPullRequestDiff({ owner, repo, pull_number }) {
217
+ const token = getToken();
218
+ const res = await fetch(`${GITHUB_API}/repos/${owner}/${repo}/pulls/${pull_number}`, {
219
+ headers: {
220
+ Authorization: `Bearer ${token}`,
221
+ Accept: "application/vnd.github.v3.diff",
222
+ "X-GitHub-Api-Version": "2022-11-28",
223
+ "User-Agent": "causly-server",
224
+ },
225
+ });
226
+ if (!res.ok) {
227
+ throw new Error(`GitHub API error (${res.status}): ${res.statusText}`);
228
+ }
229
+ const diff = await res.text();
230
+ return { owner, repo, pull_number, diff };
231
+ }
232
+
233
+ /** List comments on a pull request (issue-style conversation comments). */
234
+ export async function githubGetPullRequestComments({ owner, repo, pull_number, per_page = 30 }) {
235
+ const data = await githubFetch(`/repos/${owner}/${repo}/issues/${pull_number}/comments?per_page=${per_page}`);
236
+ return {
237
+ comments: data.map((c) => ({ id: c.id, user: c.user?.login, body: c.body, html_url: c.html_url })),
238
+ };
239
+ }
240
+
241
+ /** List reviews on a pull request. */
242
+ export async function githubGetPullRequestReviews({ owner, repo, pull_number, per_page = 30 }) {
243
+ const data = await githubFetch(`/repos/${owner}/${repo}/pulls/${pull_number}/reviews?per_page=${per_page}`);
244
+ return {
245
+ reviews: data.map((r) => ({ id: r.id, user: r.user?.login, state: r.state, body: r.body })),
246
+ };
247
+ }
248
+
249
+ /** Get details of a single branch. */
250
+ export async function githubGetBranch({ owner, repo, branch }) {
251
+ const data = await githubFetch(`/repos/${owner}/${repo}/branches/${branch}`);
252
+ return {
253
+ name: data.name,
254
+ protected: data.protected,
255
+ sha: data.commit?.sha,
256
+ };
257
+ }
258
+
259
+ /** List branches on a repository. */
260
+ export async function githubListBranches({ owner, repo, per_page = 30 }) {
261
+ const data = await githubFetch(`/repos/${owner}/${repo}/branches?per_page=${per_page}`);
262
+ return {
263
+ branches: data.map((b) => ({ name: b.name, protected: b.protected })),
264
+ };
265
+ }
266
+
267
+ /** List workflows defined in a repository. */
268
+ export async function githubListWorkflows({ owner, repo }) {
269
+ const data = await githubFetch(`/repos/${owner}/${repo}/actions/workflows`);
270
+ return {
271
+ workflows: data.workflows?.map((w) => ({
272
+ id: w.id,
273
+ name: w.name,
274
+ path: w.path,
275
+ state: w.state,
276
+ })),
277
+ };
278
+ }
279
+
280
+ /** List recent workflow runs, optionally filtered by branch or status. */
281
+ export async function githubListWorkflowRuns({ owner, repo, branch, status, per_page = 20 }) {
282
+ const params = new URLSearchParams({ per_page: String(per_page) });
283
+ if (branch) params.set("branch", branch);
284
+ if (status) params.set("status", status);
285
+ const data = await githubFetch(`/repos/${owner}/${repo}/actions/runs?${params.toString()}`);
286
+ return {
287
+ runs: data.workflow_runs?.map((r) => ({
288
+ id: r.id,
289
+ name: r.name,
290
+ status: r.status,
291
+ conclusion: r.conclusion,
292
+ branch: r.head_branch,
293
+ event: r.event,
294
+ html_url: r.html_url,
295
+ created_at: r.created_at,
296
+ })),
297
+ };
298
+ }
299
+
300
+ /** Get details of a single workflow run. */
301
+ export async function githubGetWorkflowRun({ owner, repo, run_id }) {
302
+ const data = await githubFetch(`/repos/${owner}/${repo}/actions/runs/${run_id}`);
303
+ return {
304
+ id: data.id,
305
+ name: data.name,
306
+ status: data.status,
307
+ conclusion: data.conclusion,
308
+ branch: data.head_branch,
309
+ event: data.event,
310
+ html_url: data.html_url,
311
+ created_at: data.created_at,
312
+ updated_at: data.updated_at,
313
+ };
314
+ }
315
+
316
+ /** List jobs for a workflow run, with per-job status/conclusion. */
317
+ export async function githubGetWorkflowRunJobs({ owner, repo, run_id }) {
318
+ const data = await githubFetch(`/repos/${owner}/${repo}/actions/runs/${run_id}/jobs`);
319
+ return {
320
+ jobs: data.jobs?.map((j) => ({
321
+ id: j.id,
322
+ name: j.name,
323
+ status: j.status,
324
+ conclusion: j.conclusion,
325
+ html_url: j.html_url,
326
+ steps: j.steps?.map((s) => ({ name: s.name, status: s.status, conclusion: s.conclusion })),
327
+ })),
328
+ };
329
+ }
330
+
331
+ /** Get the raw log text for a job. */
332
+ export async function githubGetJobLogs({ owner, repo, job_id }) {
333
+ const token = getToken();
334
+ const res = await fetch(`${GITHUB_API}/repos/${owner}/${repo}/actions/jobs/${job_id}/logs`, {
335
+ headers: {
336
+ Authorization: `Bearer ${token}`,
337
+ Accept: "application/vnd.github+json",
338
+ "X-GitHub-Api-Version": "2022-11-28",
339
+ "User-Agent": "causly-server",
340
+ },
341
+ redirect: "follow",
342
+ });
343
+ if (!res.ok) {
344
+ throw new Error(`GitHub API error (${res.status}): ${res.statusText}`);
345
+ }
346
+ const logs = await res.text();
347
+ return { owner, repo, job_id, logs };
348
+ }
349
+
350
+ /** Re-run a workflow run (all jobs, or only failed jobs). */
351
+ export async function githubRerunWorkflow({ owner, repo, run_id, failed_only = false }) {
352
+ const path = failed_only
353
+ ? `/repos/${owner}/${repo}/actions/runs/${run_id}/rerun-failed-jobs`
354
+ : `/repos/${owner}/${repo}/actions/runs/${run_id}/rerun`;
355
+ await githubFetch(path, { method: "POST" });
356
+ return { owner, repo, run_id, failed_only, requeued: true };
357
+ }