@blogic-cz/agent-tools 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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +236 -0
  3. package/package.json +70 -0
  4. package/schemas/agent-tools.schema.json +319 -0
  5. package/src/az-tool/build.ts +295 -0
  6. package/src/az-tool/config.ts +33 -0
  7. package/src/az-tool/errors.ts +26 -0
  8. package/src/az-tool/extract-option-value.ts +12 -0
  9. package/src/az-tool/index.ts +181 -0
  10. package/src/az-tool/security.ts +130 -0
  11. package/src/az-tool/service.ts +292 -0
  12. package/src/az-tool/types.ts +67 -0
  13. package/src/config/index.ts +12 -0
  14. package/src/config/loader.ts +170 -0
  15. package/src/config/types.ts +82 -0
  16. package/src/credential-guard/claude-hook.ts +28 -0
  17. package/src/credential-guard/index.ts +435 -0
  18. package/src/db-tool/config-service.ts +38 -0
  19. package/src/db-tool/errors.ts +40 -0
  20. package/src/db-tool/index.ts +91 -0
  21. package/src/db-tool/schema.ts +69 -0
  22. package/src/db-tool/security.ts +116 -0
  23. package/src/db-tool/service.ts +605 -0
  24. package/src/db-tool/types.ts +33 -0
  25. package/src/gh-tool/config.ts +7 -0
  26. package/src/gh-tool/errors.ts +47 -0
  27. package/src/gh-tool/index.ts +140 -0
  28. package/src/gh-tool/issue.ts +361 -0
  29. package/src/gh-tool/pr/commands.ts +432 -0
  30. package/src/gh-tool/pr/core.ts +497 -0
  31. package/src/gh-tool/pr/helpers.ts +84 -0
  32. package/src/gh-tool/pr/index.ts +19 -0
  33. package/src/gh-tool/pr/review.ts +571 -0
  34. package/src/gh-tool/repo.ts +147 -0
  35. package/src/gh-tool/service.ts +192 -0
  36. package/src/gh-tool/types.ts +97 -0
  37. package/src/gh-tool/workflow.ts +542 -0
  38. package/src/index.ts +1 -0
  39. package/src/k8s-tool/errors.ts +21 -0
  40. package/src/k8s-tool/index.ts +151 -0
  41. package/src/k8s-tool/service.ts +227 -0
  42. package/src/k8s-tool/types.ts +9 -0
  43. package/src/logs-tool/errors.ts +29 -0
  44. package/src/logs-tool/index.ts +176 -0
  45. package/src/logs-tool/service.ts +323 -0
  46. package/src/logs-tool/types.ts +40 -0
  47. package/src/session-tool/config.ts +55 -0
  48. package/src/session-tool/errors.ts +38 -0
  49. package/src/session-tool/index.ts +270 -0
  50. package/src/session-tool/service.ts +210 -0
  51. package/src/session-tool/types.ts +28 -0
  52. package/src/shared/bun.ts +59 -0
  53. package/src/shared/cli.ts +38 -0
  54. package/src/shared/error-renderer.ts +42 -0
  55. package/src/shared/exec.ts +62 -0
  56. package/src/shared/format.ts +27 -0
  57. package/src/shared/index.ts +16 -0
  58. package/src/shared/throttle.ts +35 -0
  59. package/src/shared/types.ts +25 -0
@@ -0,0 +1,140 @@
1
+ #!/usr/bin/env bun
2
+ import { Command } from "effect/unstable/cli";
3
+ import { BunRuntime, BunServices } from "@effect/platform-bun";
4
+ import { Effect, Layer } from "effect";
5
+
6
+ import { renderCauseToStderr, VERSION } from "../shared";
7
+ import {
8
+ issueListCommand,
9
+ issueViewCommand,
10
+ issueCloseCommand,
11
+ issueReopenCommand,
12
+ issueCommentCommand,
13
+ issueEditCommand,
14
+ } from "./issue";
15
+ import {
16
+ prViewCommand,
17
+ prStatusCommand,
18
+ prCreateCommand,
19
+ prEditCommand,
20
+ prMergeCommand,
21
+ prThreadsCommand,
22
+ prCommentsCommand,
23
+ prIssueCommentsCommand,
24
+ prIssueCommentsLatestCommand,
25
+ prCommentCommand,
26
+ prDiscussionSummaryCommand,
27
+ prReplyCommand,
28
+ prResolveCommand,
29
+ prSubmitReviewCommand,
30
+ prChecksCommand,
31
+ prChecksFailedCommand,
32
+ prRerunChecksCommand,
33
+ } from "./pr/index";
34
+ import { repoInfoCommand, repoListCommand, repoSearchCodeCommand } from "./repo";
35
+ import { GitHubService } from "./service";
36
+ import {
37
+ workflowCancelCommand,
38
+ workflowJobLogsCommand,
39
+ workflowJobsCommand,
40
+ workflowListCommand,
41
+ workflowLogsCommand,
42
+ workflowRerunCommand,
43
+ workflowViewCommand,
44
+ workflowWatchCommand,
45
+ } from "./workflow";
46
+
47
+ const prCommand = Command.make("pr", {}).pipe(
48
+ Command.withDescription("Pull request operations (view, create, merge, reviews, checks)"),
49
+ Command.withSubcommands([
50
+ prViewCommand,
51
+ prStatusCommand,
52
+ prCreateCommand,
53
+ prEditCommand,
54
+ prMergeCommand,
55
+ prThreadsCommand,
56
+ prCommentsCommand,
57
+ prIssueCommentsCommand,
58
+ prIssueCommentsLatestCommand,
59
+ prCommentCommand,
60
+ prDiscussionSummaryCommand,
61
+ prReplyCommand,
62
+ prResolveCommand,
63
+ prSubmitReviewCommand,
64
+ prChecksCommand,
65
+ prChecksFailedCommand,
66
+ prRerunChecksCommand,
67
+ ]),
68
+ );
69
+
70
+ const issueCommand = Command.make("issue", {}).pipe(
71
+ Command.withDescription("Issue operations (list, view, close, reopen, comment, edit)"),
72
+ Command.withSubcommands([
73
+ issueListCommand,
74
+ issueViewCommand,
75
+ issueCloseCommand,
76
+ issueReopenCommand,
77
+ issueCommentCommand,
78
+ issueEditCommand,
79
+ ]),
80
+ );
81
+
82
+ const repoCommand = Command.make("repo", {}).pipe(
83
+ Command.withDescription("Repository operations"),
84
+ Command.withSubcommands([repoInfoCommand, repoListCommand, repoSearchCodeCommand]),
85
+ );
86
+
87
+ const workflowCommand = Command.make("workflow", {}).pipe(
88
+ Command.withDescription(
89
+ "GitHub Actions workflow operations (list runs, view, jobs, logs, job-logs, rerun, cancel, watch)",
90
+ ),
91
+ Command.withSubcommands([
92
+ workflowListCommand,
93
+ workflowViewCommand,
94
+ workflowJobsCommand,
95
+ workflowLogsCommand,
96
+ workflowJobLogsCommand,
97
+ workflowRerunCommand,
98
+ workflowCancelCommand,
99
+ workflowWatchCommand,
100
+ ]),
101
+ );
102
+
103
+ const mainCommand = Command.make("gh-tool", {}).pipe(
104
+ Command.withDescription(
105
+ `GitHub CLI Tool for Coding Agents
106
+
107
+ Wraps the GitHub CLI (gh) with structured output for AI agents.
108
+ Supports PR management, issue management, reviews, CI checks, and repo info.
109
+
110
+ WORKFLOW FOR AI AGENTS:
111
+ 1. Use 'pr view' to inspect current PR
112
+ 2. Use 'pr discussion-summary' for overview (counts + latest discussion comment)
113
+ 3. Use 'pr threads' and 'pr issue-comments-latest --author <username> --body-contains "Review"' for review context
114
+ 4. Use 'pr submit-review', 'pr reply', 'pr comment' and 'pr resolve' to handle feedback
115
+ 5. Use 'pr checks' to monitor CI status
116
+ 6. Use 'pr merge' to merge (dry-run by default)
117
+ 7. Use 'issue list' to list open/closed issues
118
+ 8. Use 'issue close --issue N --comment "reason"' to close issues
119
+ 9. Use 'issue comment --issue N --body "text"' to comment on issues
120
+ 10. Use 'repo info' to get repository metadata
121
+ 11. Use 'workflow list' to list recent workflow runs
122
+ 12. Use 'workflow view --run N' to inspect a specific run with jobs/steps
123
+ 13. Use 'workflow logs --run N' to get logs (failed jobs by default)
124
+ 14. Use 'workflow job-logs --run N --job "build-web-app"' to get clean parsed logs for a specific job
125
+ 15. Use 'workflow watch --run N' to watch until completion`,
126
+ ),
127
+ Command.withSubcommands([prCommand, issueCommand, repoCommand, workflowCommand]),
128
+ );
129
+
130
+ const cli = Command.run(mainCommand, {
131
+ version: VERSION,
132
+ });
133
+
134
+ const MainLayer = GitHubService.layer.pipe(Layer.provideMerge(BunServices.layer));
135
+
136
+ const program = cli.pipe(Effect.provide(MainLayer), Effect.tapCause(renderCauseToStderr));
137
+
138
+ BunRuntime.runMain(program, {
139
+ disableErrorReporting: true,
140
+ });
@@ -0,0 +1,361 @@
1
+ import { Command, Flag } from "effect/unstable/cli";
2
+ import { Effect, Option } from "effect";
3
+
4
+ import { formatOption, logFormatted } from "../shared";
5
+ import { GitHubCommandError } from "./errors";
6
+ import { GitHubService } from "./service";
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // Types
10
+ // ---------------------------------------------------------------------------
11
+
12
+ type IssueInfo = {
13
+ number: number;
14
+ title: string;
15
+ state: string;
16
+ url: string;
17
+ labels: Array<{ name: string }>;
18
+ assignees: Array<{ login: string }>;
19
+ author: { login: string };
20
+ createdAt: string;
21
+ closedAt: string | null;
22
+ };
23
+
24
+ type IssueListItem = {
25
+ number: number;
26
+ title: string;
27
+ state: string;
28
+ url: string;
29
+ labels: Array<{ name: string }>;
30
+ createdAt: string;
31
+ };
32
+
33
+ type RawIssueComment = {
34
+ id: number;
35
+ user: { login: string };
36
+ body: string;
37
+ created_at: string;
38
+ html_url: string;
39
+ };
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // Handlers
43
+ // ---------------------------------------------------------------------------
44
+
45
+ const listIssues = Effect.fn("issue.listIssues")(function* (opts: {
46
+ state: string;
47
+ labels: string | null;
48
+ limit: number;
49
+ }) {
50
+ const gh = yield* GitHubService;
51
+
52
+ const args = [
53
+ "issue",
54
+ "list",
55
+ "--state",
56
+ opts.state,
57
+ "--limit",
58
+ String(opts.limit),
59
+ "--json",
60
+ "number,title,state,url,labels,createdAt",
61
+ ];
62
+
63
+ if (opts.labels !== null) {
64
+ args.push("--label", opts.labels);
65
+ }
66
+
67
+ return yield* gh.runGhJson<IssueListItem[]>(args);
68
+ });
69
+
70
+ const viewIssue = Effect.fn("issue.viewIssue")(function* (issueNumber: number) {
71
+ const gh = yield* GitHubService;
72
+
73
+ return yield* gh.runGhJson<IssueInfo>([
74
+ "issue",
75
+ "view",
76
+ String(issueNumber),
77
+ "--json",
78
+ "number,title,state,url,labels,assignees,author,createdAt,closedAt",
79
+ ]);
80
+ });
81
+
82
+ const closeIssue = Effect.fn("issue.closeIssue")(function* (opts: {
83
+ issue: number;
84
+ comment: string | null;
85
+ reason: string;
86
+ }) {
87
+ const gh = yield* GitHubService;
88
+
89
+ const args = ["issue", "close", String(opts.issue), "--reason", opts.reason];
90
+
91
+ if (opts.comment !== null) {
92
+ args.push("--comment", opts.comment);
93
+ }
94
+
95
+ yield* gh.runGh(args);
96
+
97
+ return yield* gh.runGhJson<IssueInfo>([
98
+ "issue",
99
+ "view",
100
+ String(opts.issue),
101
+ "--json",
102
+ "number,title,state,url,labels,assignees,author,createdAt,closedAt",
103
+ ]);
104
+ });
105
+
106
+ const reopenIssue = Effect.fn("issue.reopenIssue")(function* (opts: {
107
+ issue: number;
108
+ comment: string | null;
109
+ }) {
110
+ const gh = yield* GitHubService;
111
+
112
+ const args = ["issue", "reopen", String(opts.issue)];
113
+
114
+ if (opts.comment !== null) {
115
+ args.push("--comment", opts.comment);
116
+ }
117
+
118
+ yield* gh.runGh(args);
119
+
120
+ return yield* gh.runGhJson<IssueInfo>([
121
+ "issue",
122
+ "view",
123
+ String(opts.issue),
124
+ "--json",
125
+ "number,title,state,url,labels,assignees,author,createdAt,closedAt",
126
+ ]);
127
+ });
128
+
129
+ const commentOnIssue = Effect.fn("issue.commentOnIssue")(function* (opts: {
130
+ issue: number;
131
+ body: string;
132
+ }) {
133
+ const gh = yield* GitHubService;
134
+ const repoInfo = yield* gh.getRepoInfo();
135
+
136
+ const trimmedBody = opts.body.trim();
137
+ if (trimmedBody.length === 0) {
138
+ return yield* Effect.fail(
139
+ new GitHubCommandError({
140
+ command: "gh-tool issue comment",
141
+ exitCode: 0,
142
+ stderr: "Comment body cannot be empty",
143
+ message: "Comment body cannot be empty",
144
+ }),
145
+ );
146
+ }
147
+
148
+ const result = yield* gh.runGh([
149
+ "api",
150
+ "-X",
151
+ "POST",
152
+ `repos/${repoInfo.owner}/${repoInfo.name}/issues/${opts.issue}/comments`,
153
+ "-f",
154
+ `body=${trimmedBody}`,
155
+ ]);
156
+
157
+ const rawComment = yield* Effect.try({
158
+ try: () => JSON.parse(result.stdout) as RawIssueComment,
159
+ catch: (error) =>
160
+ new GitHubCommandError({
161
+ command: "gh-tool issue comment",
162
+ exitCode: 0,
163
+ stderr: `Failed to parse response: ${error instanceof Error ? error.message : String(error)}`,
164
+ message: `Failed to parse response: ${error instanceof Error ? error.message : String(error)}`,
165
+ }),
166
+ }).pipe(Effect.mapError((error) => error as GitHubCommandError));
167
+
168
+ return {
169
+ id: rawComment.id,
170
+ author: rawComment.user.login,
171
+ body: rawComment.body,
172
+ createdAt: rawComment.created_at,
173
+ url: rawComment.html_url,
174
+ };
175
+ });
176
+
177
+ const editIssue = Effect.fn("issue.editIssue")(function* (opts: {
178
+ issue: number;
179
+ title: string | null;
180
+ body: string | null;
181
+ addLabels: string | null;
182
+ removeLabels: string | null;
183
+ addAssignee: string | null;
184
+ removeAssignee: string | null;
185
+ }) {
186
+ const gh = yield* GitHubService;
187
+
188
+ const args = ["issue", "edit", String(opts.issue)];
189
+
190
+ if (opts.title !== null) {
191
+ args.push("--title", opts.title);
192
+ }
193
+ if (opts.body !== null) {
194
+ args.push("--body", opts.body);
195
+ }
196
+ if (opts.addLabels !== null) {
197
+ args.push("--add-label", opts.addLabels);
198
+ }
199
+ if (opts.removeLabels !== null) {
200
+ args.push("--remove-label", opts.removeLabels);
201
+ }
202
+ if (opts.addAssignee !== null) {
203
+ args.push("--add-assignee", opts.addAssignee);
204
+ }
205
+ if (opts.removeAssignee !== null) {
206
+ args.push("--remove-assignee", opts.removeAssignee);
207
+ }
208
+
209
+ yield* gh.runGh(args);
210
+
211
+ return yield* gh.runGhJson<IssueInfo>([
212
+ "issue",
213
+ "view",
214
+ String(opts.issue),
215
+ "--json",
216
+ "number,title,state,url,labels,assignees,author,createdAt,closedAt",
217
+ ]);
218
+ });
219
+
220
+ // ---------------------------------------------------------------------------
221
+ // CLI Commands
222
+ // ---------------------------------------------------------------------------
223
+
224
+ export const issueListCommand = Command.make(
225
+ "list",
226
+ {
227
+ format: formatOption,
228
+ labels: Flag.string("labels").pipe(
229
+ Flag.withDescription("Filter by label (comma-separated)"),
230
+ Flag.optional,
231
+ ),
232
+ limit: Flag.integer("limit").pipe(
233
+ Flag.withDescription("Maximum number of issues to return"),
234
+ Flag.withDefault(30),
235
+ ),
236
+ state: Flag.choice("state", ["open", "closed", "all"]).pipe(
237
+ Flag.withDescription("Filter by state: open, closed, all"),
238
+ Flag.withDefault("open"),
239
+ ),
240
+ },
241
+ ({ format, labels, limit, state }) =>
242
+ Effect.gen(function* () {
243
+ const issues = yield* listIssues({
244
+ labels: Option.getOrNull(labels),
245
+ limit,
246
+ state,
247
+ });
248
+ yield* logFormatted(issues, format);
249
+ }),
250
+ ).pipe(Command.withDescription("List issues (default: open, use --state to filter)"));
251
+
252
+ export const issueViewCommand = Command.make(
253
+ "view",
254
+ {
255
+ format: formatOption,
256
+ issue: Flag.integer("issue").pipe(Flag.withDescription("Issue number")),
257
+ },
258
+ ({ format, issue }) =>
259
+ Effect.gen(function* () {
260
+ const info = yield* viewIssue(issue);
261
+ yield* logFormatted(info, format);
262
+ }),
263
+ ).pipe(Command.withDescription("View issue details"));
264
+
265
+ export const issueCloseCommand = Command.make(
266
+ "close",
267
+ {
268
+ comment: Flag.string("comment").pipe(
269
+ Flag.withDescription("Comment to add when closing"),
270
+ Flag.optional,
271
+ ),
272
+ format: formatOption,
273
+ issue: Flag.integer("issue").pipe(Flag.withDescription("Issue number to close")),
274
+ reason: Flag.choice("reason", ["completed", "not planned"]).pipe(
275
+ Flag.withDescription("Close reason: completed, not planned"),
276
+ Flag.withDefault("completed"),
277
+ ),
278
+ },
279
+ ({ comment, format, issue, reason }) =>
280
+ Effect.gen(function* () {
281
+ const result = yield* closeIssue({
282
+ comment: Option.getOrNull(comment),
283
+ issue,
284
+ reason,
285
+ });
286
+ yield* logFormatted(result, format);
287
+ }),
288
+ ).pipe(Command.withDescription("Close an issue with optional comment and reason"));
289
+
290
+ export const issueReopenCommand = Command.make(
291
+ "reopen",
292
+ {
293
+ comment: Flag.string("comment").pipe(
294
+ Flag.withDescription("Comment to add when reopening"),
295
+ Flag.optional,
296
+ ),
297
+ format: formatOption,
298
+ issue: Flag.integer("issue").pipe(Flag.withDescription("Issue number to reopen")),
299
+ },
300
+ ({ comment, format, issue }) =>
301
+ Effect.gen(function* () {
302
+ const result = yield* reopenIssue({
303
+ comment: Option.getOrNull(comment),
304
+ issue,
305
+ });
306
+ yield* logFormatted(result, format);
307
+ }),
308
+ ).pipe(Command.withDescription("Reopen a closed issue"));
309
+
310
+ export const issueCommentCommand = Command.make(
311
+ "comment",
312
+ {
313
+ body: Flag.string("body").pipe(Flag.withDescription("Comment body text")),
314
+ format: formatOption,
315
+ issue: Flag.integer("issue").pipe(Flag.withDescription("Issue number to comment on")),
316
+ },
317
+ ({ body, format, issue }) =>
318
+ Effect.gen(function* () {
319
+ const result = yield* commentOnIssue({ body, issue });
320
+ yield* logFormatted(result, format);
321
+ }),
322
+ ).pipe(Command.withDescription("Post a comment on an issue"));
323
+
324
+ export const issueEditCommand = Command.make(
325
+ "edit",
326
+ {
327
+ addAssignee: Flag.string("add-assignee").pipe(
328
+ Flag.withDescription("Add assignee login (comma-separated for multiple)"),
329
+ Flag.optional,
330
+ ),
331
+ addLabels: Flag.string("add-labels").pipe(
332
+ Flag.withDescription("Add labels (comma-separated)"),
333
+ Flag.optional,
334
+ ),
335
+ body: Flag.string("body").pipe(Flag.withDescription("New issue body"), Flag.optional),
336
+ format: formatOption,
337
+ issue: Flag.integer("issue").pipe(Flag.withDescription("Issue number to edit")),
338
+ removeAssignee: Flag.string("remove-assignee").pipe(
339
+ Flag.withDescription("Remove assignee login (comma-separated for multiple)"),
340
+ Flag.optional,
341
+ ),
342
+ removeLabels: Flag.string("remove-labels").pipe(
343
+ Flag.withDescription("Remove labels (comma-separated)"),
344
+ Flag.optional,
345
+ ),
346
+ title: Flag.string("title").pipe(Flag.withDescription("New issue title"), Flag.optional),
347
+ },
348
+ ({ addAssignee, addLabels, body, format, issue, removeAssignee, removeLabels, title }) =>
349
+ Effect.gen(function* () {
350
+ const result = yield* editIssue({
351
+ addAssignee: Option.getOrNull(addAssignee),
352
+ addLabels: Option.getOrNull(addLabels),
353
+ body: Option.getOrNull(body),
354
+ issue,
355
+ removeAssignee: Option.getOrNull(removeAssignee),
356
+ removeLabels: Option.getOrNull(removeLabels),
357
+ title: Option.getOrNull(title),
358
+ });
359
+ yield* logFormatted(result, format);
360
+ }),
361
+ ).pipe(Command.withDescription("Edit issue title, body, labels, or assignees"));