@blogic-cz/agent-tools 0.2.7 → 0.4.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/README.md CHANGED
@@ -15,10 +15,10 @@ These tools wrap each CLI with:
15
15
 
16
16
  ## Installation
17
17
 
18
- > **Recommended:** Copy the repo URL and tell your AI agent to install it. The agent will set up everything — dependencies, config file, credential guard — in the right places for your project.
18
+ > **Recommended:** Copy the repo URL and tell your AI agent to install it. The agent will set up everything — dev dependency, config file, credential guard, skill — in the right places for your project.
19
19
  >
20
20
  > ```
21
- > Install @blogic-cz/agent-tools from https://github.com/blogic-cz/agent-tools and set it up for this project.
21
+ > Install @blogic-cz/agent-tools from https://github.com/blogic-cz/agent-tools as a dev dependency, install the agent-tools skill, set up the credential guard, and configure the tools I need for this project.
22
22
  > ```
23
23
 
24
24
  ### Manual installation
@@ -31,11 +31,88 @@ bun add -d @blogic-cz/agent-tools
31
31
 
32
32
  ### For agents reading this
33
33
 
34
- Clone the repo and look at the source. The tools are in `src/`, each tool is self-contained:
34
+ Follow these steps to set up agent-tools in the consumer project.
35
+
36
+ **Step 1 — Install**
37
+
38
+ - **JavaScript/TypeScript project** (has `package.json`): Install as a **dev dependency**:
39
+ ```bash
40
+ bun add -d @blogic-cz/agent-tools
41
+ ```
42
+ - **Non-JavaScript project** (no `package.json`): Skip installation — run tools directly with `bunx`:
43
+ ```bash
44
+ bunx agent-tools-gh pr status
45
+ bunx agent-tools-k8s pods --env test
46
+ ```
47
+
48
+ **Step 2 — Configure**
49
+
50
+ Ask the user which tools they need (gh, db, k8s, az, logs, session). Create `agent-tools.json5` in the project root with **only the selected tools configured**. See [`examples/agent-tools.json5`](./examples/agent-tools.json5) for the full config reference with all options documented.
51
+
52
+ Minimal starting config:
53
+
54
+ ```json5
55
+ {
56
+ $schema: "https://raw.githubusercontent.com/blogic-cz/agent-tools/main/schemas/agent-tools.schema.json",
57
+ }
58
+ ```
59
+
60
+ **Step 3 — Credential Guard**
61
+
62
+ Set up the credential guard for the agent's platform:
63
+
64
+ - **Claude Code**: Add the `PreToolUse` hook to `.claude/settings.json` — see [Setup for Claude Code](#setup-for-claude-code)
65
+ - **OpenCode**: Create the plugin at `.opencode/plugins/credential-guard.ts` — see [Setup for OpenCode](#setup-for-opencode)
66
+
67
+ **Step 4 — Test & Demo**
68
+
69
+ For each tool the user selected in Step 2:
70
+
71
+ 1. Run `bunx agent-tools-<tool> --help` to verify it works
72
+ 2. Summarize the key commands available
73
+ 3. Show the user what data they have access to based on their `agent-tools.json5` — e.g. which environments, clusters, namespaces, databases, or profiles are configured and reachable
74
+
75
+ **Step 5 — Skill & Agent Docs**
76
+
77
+ Tell the user to run the following command themselves — it's interactive and lets them choose which agents to install the skill for:
78
+
79
+ ```bash
80
+ npx skills add blogic-cz/agent-tools
81
+ ```
82
+
83
+ **Do not run this command for the user** — it requires interactive selection.
84
+
85
+ Then update the project's `AGENTS.md` and/or `CLAUDE.md`:
86
+
87
+ 1. Add an `agent-tools` row to the skills table (if one exists):
88
+ ```markdown
89
+ | Agent wrapper tools (`db-tool`, `k8s-tool`, `logs-tool`, `az-tool`, `gh` patterns) | `agent-tools` |
90
+ ```
91
+ 2. Add or update the **Tooling** section:
92
+
93
+ ```markdown
94
+ ## Tooling
95
+
96
+ For tool wrappers and operational patterns, load `agent-tools`.
97
+ ```
98
+
99
+ **Step 6 — Custom Tool Scaffold**
100
+
101
+ Create an `agent-tools/` directory in the project root with an example tool so the user has a working template for building project-specific tools. Copy the scaffold from [`examples/custom-tool/`](./examples/custom-tool/):
102
+
103
+ ```
104
+ agent-tools/
105
+ package.json # private package depending on @blogic-cz/agent-tools
106
+ tsconfig.json # extends root tsconfig
107
+ noop.ts # placeholder export for typecheck
108
+ example-tool/
109
+ index.ts # ping-pong example using Effect CLI
110
+ ```
111
+
112
+ After creating the files, run `bun install` in the `agent-tools/` directory (or from the workspace root if it's a monorepo). Then verify:
35
113
 
36
114
  ```bash
37
- git clone https://github.com/blogic-cz/agent-tools.git
38
- ls src/ # gh-tool/ db-tool/ k8s-tool/ az-tool/ logs-tool/ session-tool/ credential-guard/
115
+ bun run agent-tools/example-tool/index.ts ping
39
116
  ```
40
117
 
41
118
  ## Quick Start
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blogic-cz/agent-tools",
3
- "version": "0.2.7",
3
+ "version": "0.4.0",
4
4
  "description": "CLI tools for AI coding agent workflows — GitHub, database, Kubernetes, Azure DevOps, logs, and sessions",
5
5
  "keywords": [
6
6
  "agent",
@@ -5,12 +5,13 @@ import { Effect, Layer } from "effect";
5
5
 
6
6
  import { renderCauseToStderr, VERSION } from "#shared";
7
7
  import {
8
- issueListCommand,
9
- issueViewCommand,
10
8
  issueCloseCommand,
11
- issueReopenCommand,
12
9
  issueCommentCommand,
13
10
  issueEditCommand,
11
+ issueListCommand,
12
+ issueReopenCommand,
13
+ issueTriageSummaryCommand,
14
+ issueViewCommand,
14
15
  } from "./issue";
15
16
  import {
16
17
  prViewCommand,
@@ -33,6 +34,14 @@ import {
33
34
  prReplyAndResolveCommand,
34
35
  prReviewTriageCommand,
35
36
  } from "./pr/index";
37
+ import {
38
+ releaseCreateCommand,
39
+ releaseDeleteCommand,
40
+ releaseEditCommand,
41
+ releaseListCommand,
42
+ releaseStatusCommand,
43
+ releaseViewCommand,
44
+ } from "./release";
36
45
  import { repoInfoCommand, repoListCommand, repoSearchCodeCommand } from "./repo";
37
46
  import { GitHubService } from "./service";
38
47
  import {
@@ -72,7 +81,9 @@ const prCommand = Command.make("pr", {}).pipe(
72
81
  );
73
82
 
74
83
  const issueCommand = Command.make("issue", {}).pipe(
75
- Command.withDescription("Issue operations (list, view, close, reopen, comment, edit)"),
84
+ Command.withDescription(
85
+ "Issue operations (list, view, close, reopen, comment, edit, triage-summary)",
86
+ ),
76
87
  Command.withSubcommands([
77
88
  issueListCommand,
78
89
  issueViewCommand,
@@ -80,6 +91,7 @@ const issueCommand = Command.make("issue", {}).pipe(
80
91
  issueReopenCommand,
81
92
  issueCommentCommand,
82
93
  issueEditCommand,
94
+ issueTriageSummaryCommand,
83
95
  ]),
84
96
  );
85
97
 
@@ -104,6 +116,18 @@ const workflowCommand = Command.make("workflow", {}).pipe(
104
116
  ]),
105
117
  );
106
118
 
119
+ const releaseCommand = Command.make("release", {}).pipe(
120
+ Command.withDescription("Release operations (create, list, view, edit, delete, status)"),
121
+ Command.withSubcommands([
122
+ releaseCreateCommand,
123
+ releaseListCommand,
124
+ releaseViewCommand,
125
+ releaseEditCommand,
126
+ releaseDeleteCommand,
127
+ releaseStatusCommand,
128
+ ]),
129
+ );
130
+
107
131
  const mainCommand = Command.make("gh-tool", {}).pipe(
108
132
  Command.withDescription(
109
133
  `GitHub CLI Tool for Coding Agents
@@ -126,9 +150,12 @@ WORKFLOW FOR AI AGENTS:
126
150
  12. Use 'workflow view --run N' to inspect a specific run with jobs/steps
127
151
  13. Use 'workflow logs --run N' to get logs (failed jobs by default)
128
152
  14. Use 'workflow job-logs --run N --job "build-web-app"' to get clean parsed logs for a specific job
129
- 15. Use 'workflow watch --run N' to watch until completion`,
153
+ 15. Use 'workflow watch --run N' to watch until completion
154
+ 16. Use 'release status' to inspect latest release + repository context
155
+ 17. Use 'release create --tag vX.Y.Z --generate-notes' to publish a release
156
+ 18. Use 'release edit/view/list/delete' to maintain existing releases`,
130
157
  ),
131
- Command.withSubcommands([prCommand, issueCommand, repoCommand, workflowCommand]),
158
+ Command.withSubcommands([prCommand, issueCommand, repoCommand, workflowCommand, releaseCommand]),
132
159
  );
133
160
 
134
161
  const cli = Command.run(mainCommand, {
@@ -0,0 +1,145 @@
1
+ import { Command, Flag } from "effect/unstable/cli";
2
+ import { Effect, Option } from "effect";
3
+
4
+ import { formatOption, logFormatted } from "#shared";
5
+
6
+ import { closeIssue, commentOnIssue, editIssue, listIssues, reopenIssue, viewIssue } from "./core";
7
+
8
+ export const issueListCommand = Command.make(
9
+ "list",
10
+ {
11
+ format: formatOption,
12
+ labels: Flag.string("labels").pipe(
13
+ Flag.withDescription("Filter by label (comma-separated)"),
14
+ Flag.optional,
15
+ ),
16
+ limit: Flag.integer("limit").pipe(
17
+ Flag.withDescription("Maximum number of issues to return"),
18
+ Flag.withDefault(30),
19
+ ),
20
+ state: Flag.choice("state", ["open", "closed", "all"]).pipe(
21
+ Flag.withDescription("Filter by state: open, closed, all"),
22
+ Flag.withDefault("open"),
23
+ ),
24
+ },
25
+ ({ format, labels, limit, state }) =>
26
+ Effect.gen(function* () {
27
+ const issues = yield* listIssues({
28
+ labels: Option.getOrNull(labels),
29
+ limit,
30
+ state,
31
+ });
32
+ yield* logFormatted(issues, format);
33
+ }),
34
+ ).pipe(Command.withDescription("List issues (default: open, use --state to filter)"));
35
+
36
+ export const issueViewCommand = Command.make(
37
+ "view",
38
+ {
39
+ format: formatOption,
40
+ issue: Flag.integer("issue").pipe(Flag.withDescription("Issue number")),
41
+ },
42
+ ({ format, issue }) =>
43
+ Effect.gen(function* () {
44
+ const info = yield* viewIssue(issue);
45
+ yield* logFormatted(info, format);
46
+ }),
47
+ ).pipe(Command.withDescription("View issue details"));
48
+
49
+ export const issueCloseCommand = Command.make(
50
+ "close",
51
+ {
52
+ comment: Flag.string("comment").pipe(
53
+ Flag.withDescription("Comment to add when closing"),
54
+ Flag.optional,
55
+ ),
56
+ format: formatOption,
57
+ issue: Flag.integer("issue").pipe(Flag.withDescription("Issue number to close")),
58
+ reason: Flag.choice("reason", ["completed", "not planned"]).pipe(
59
+ Flag.withDescription("Close reason: completed, not planned"),
60
+ Flag.withDefault("completed"),
61
+ ),
62
+ },
63
+ ({ comment, format, issue, reason }) =>
64
+ Effect.gen(function* () {
65
+ const result = yield* closeIssue({
66
+ comment: Option.getOrNull(comment),
67
+ issue,
68
+ reason,
69
+ });
70
+ yield* logFormatted(result, format);
71
+ }),
72
+ ).pipe(Command.withDescription("Close an issue with optional comment and reason"));
73
+
74
+ export const issueReopenCommand = Command.make(
75
+ "reopen",
76
+ {
77
+ comment: Flag.string("comment").pipe(
78
+ Flag.withDescription("Comment to add when reopening"),
79
+ Flag.optional,
80
+ ),
81
+ format: formatOption,
82
+ issue: Flag.integer("issue").pipe(Flag.withDescription("Issue number to reopen")),
83
+ },
84
+ ({ comment, format, issue }) =>
85
+ Effect.gen(function* () {
86
+ const result = yield* reopenIssue({
87
+ comment: Option.getOrNull(comment),
88
+ issue,
89
+ });
90
+ yield* logFormatted(result, format);
91
+ }),
92
+ ).pipe(Command.withDescription("Reopen a closed issue"));
93
+
94
+ export const issueCommentCommand = Command.make(
95
+ "comment",
96
+ {
97
+ body: Flag.string("body").pipe(Flag.withDescription("Comment body text")),
98
+ format: formatOption,
99
+ issue: Flag.integer("issue").pipe(Flag.withDescription("Issue number to comment on")),
100
+ },
101
+ ({ body, format, issue }) =>
102
+ Effect.gen(function* () {
103
+ const result = yield* commentOnIssue({ body, issue });
104
+ yield* logFormatted(result, format);
105
+ }),
106
+ ).pipe(Command.withDescription("Post a comment on an issue"));
107
+
108
+ export const issueEditCommand = Command.make(
109
+ "edit",
110
+ {
111
+ addAssignee: Flag.string("add-assignee").pipe(
112
+ Flag.withDescription("Add assignee login (comma-separated for multiple)"),
113
+ Flag.optional,
114
+ ),
115
+ addLabels: Flag.string("add-labels").pipe(
116
+ Flag.withDescription("Add labels (comma-separated)"),
117
+ Flag.optional,
118
+ ),
119
+ body: Flag.string("body").pipe(Flag.withDescription("New issue body"), Flag.optional),
120
+ format: formatOption,
121
+ issue: Flag.integer("issue").pipe(Flag.withDescription("Issue number to edit")),
122
+ removeAssignee: Flag.string("remove-assignee").pipe(
123
+ Flag.withDescription("Remove assignee login (comma-separated for multiple)"),
124
+ Flag.optional,
125
+ ),
126
+ removeLabels: Flag.string("remove-labels").pipe(
127
+ Flag.withDescription("Remove labels (comma-separated)"),
128
+ Flag.optional,
129
+ ),
130
+ title: Flag.string("title").pipe(Flag.withDescription("New issue title"), Flag.optional),
131
+ },
132
+ ({ addAssignee, addLabels, body, format, issue, removeAssignee, removeLabels, title }) =>
133
+ Effect.gen(function* () {
134
+ const result = yield* editIssue({
135
+ addAssignee: Option.getOrNull(addAssignee),
136
+ addLabels: Option.getOrNull(addLabels),
137
+ body: Option.getOrNull(body),
138
+ issue,
139
+ removeAssignee: Option.getOrNull(removeAssignee),
140
+ removeLabels: Option.getOrNull(removeLabels),
141
+ title: Option.getOrNull(title),
142
+ });
143
+ yield* logFormatted(result, format);
144
+ }),
145
+ ).pipe(Command.withDescription("Edit issue title, body, labels, or assignees"));
@@ -0,0 +1,208 @@
1
+ import { Effect } from "effect";
2
+
3
+ import { GitHubCommandError } from "#gh/errors";
4
+ import { GitHubService } from "#gh/service";
5
+
6
+ export type IssueInfo = {
7
+ number: number;
8
+ title: string;
9
+ state: string;
10
+ url: string;
11
+ labels: Array<{ name: string }>;
12
+ assignees: Array<{ login: string }>;
13
+ author: { login: string };
14
+ createdAt: string;
15
+ closedAt: string | null;
16
+ };
17
+
18
+ export type IssueListItem = {
19
+ number: number;
20
+ title: string;
21
+ state: string;
22
+ url: string;
23
+ labels: Array<{ name: string }>;
24
+ createdAt: string;
25
+ };
26
+
27
+ export type RawIssueComment = {
28
+ id: number;
29
+ user: { login: string };
30
+ body: string;
31
+ created_at: string;
32
+ html_url: string;
33
+ };
34
+
35
+ export const listIssues = Effect.fn("issue.listIssues")(function* (opts: {
36
+ state: string;
37
+ labels: string | null;
38
+ limit: number;
39
+ }) {
40
+ const gh = yield* GitHubService;
41
+
42
+ const args = [
43
+ "issue",
44
+ "list",
45
+ "--state",
46
+ opts.state,
47
+ "--limit",
48
+ String(opts.limit),
49
+ "--json",
50
+ "number,title,state,url,labels,createdAt",
51
+ ];
52
+
53
+ if (opts.labels !== null) {
54
+ args.push("--label", opts.labels);
55
+ }
56
+
57
+ return yield* gh.runGhJson<IssueListItem[]>(args);
58
+ });
59
+
60
+ export const viewIssue = Effect.fn("issue.viewIssue")(function* (issueNumber: number) {
61
+ const gh = yield* GitHubService;
62
+
63
+ return yield* gh.runGhJson<IssueInfo>([
64
+ "issue",
65
+ "view",
66
+ String(issueNumber),
67
+ "--json",
68
+ "number,title,state,url,labels,assignees,author,createdAt,closedAt",
69
+ ]);
70
+ });
71
+
72
+ export const closeIssue = Effect.fn("issue.closeIssue")(function* (opts: {
73
+ issue: number;
74
+ comment: string | null;
75
+ reason: string;
76
+ }) {
77
+ const gh = yield* GitHubService;
78
+
79
+ const args = ["issue", "close", String(opts.issue), "--reason", opts.reason];
80
+
81
+ if (opts.comment !== null) {
82
+ args.push("--comment", opts.comment);
83
+ }
84
+
85
+ yield* gh.runGh(args);
86
+
87
+ return yield* gh.runGhJson<IssueInfo>([
88
+ "issue",
89
+ "view",
90
+ String(opts.issue),
91
+ "--json",
92
+ "number,title,state,url,labels,assignees,author,createdAt,closedAt",
93
+ ]);
94
+ });
95
+
96
+ export const reopenIssue = Effect.fn("issue.reopenIssue")(function* (opts: {
97
+ issue: number;
98
+ comment: string | null;
99
+ }) {
100
+ const gh = yield* GitHubService;
101
+
102
+ const args = ["issue", "reopen", String(opts.issue)];
103
+
104
+ if (opts.comment !== null) {
105
+ args.push("--comment", opts.comment);
106
+ }
107
+
108
+ yield* gh.runGh(args);
109
+
110
+ return yield* gh.runGhJson<IssueInfo>([
111
+ "issue",
112
+ "view",
113
+ String(opts.issue),
114
+ "--json",
115
+ "number,title,state,url,labels,assignees,author,createdAt,closedAt",
116
+ ]);
117
+ });
118
+
119
+ export const commentOnIssue = Effect.fn("issue.commentOnIssue")(function* (opts: {
120
+ issue: number;
121
+ body: string;
122
+ }) {
123
+ const gh = yield* GitHubService;
124
+ const repoInfo = yield* gh.getRepoInfo();
125
+
126
+ const trimmedBody = opts.body.trim();
127
+ if (trimmedBody.length === 0) {
128
+ return yield* Effect.fail(
129
+ new GitHubCommandError({
130
+ command: "gh-tool issue comment",
131
+ exitCode: 0,
132
+ stderr: "Comment body cannot be empty",
133
+ message: "Comment body cannot be empty",
134
+ }),
135
+ );
136
+ }
137
+
138
+ const result = yield* gh.runGh([
139
+ "api",
140
+ "-X",
141
+ "POST",
142
+ `repos/${repoInfo.owner}/${repoInfo.name}/issues/${opts.issue}/comments`,
143
+ "-f",
144
+ `body=${trimmedBody}`,
145
+ ]);
146
+
147
+ const rawComment = yield* Effect.try({
148
+ try: () => JSON.parse(result.stdout) as RawIssueComment,
149
+ catch: (error) =>
150
+ new GitHubCommandError({
151
+ command: "gh-tool issue comment",
152
+ exitCode: 0,
153
+ stderr: `Failed to parse response: ${error instanceof Error ? error.message : String(error)}`,
154
+ message: `Failed to parse response: ${error instanceof Error ? error.message : String(error)}`,
155
+ }),
156
+ }).pipe(Effect.mapError((error) => error as GitHubCommandError));
157
+
158
+ return {
159
+ id: rawComment.id,
160
+ author: rawComment.user.login,
161
+ body: rawComment.body,
162
+ createdAt: rawComment.created_at,
163
+ url: rawComment.html_url,
164
+ };
165
+ });
166
+
167
+ export const editIssue = Effect.fn("issue.editIssue")(function* (opts: {
168
+ issue: number;
169
+ title: string | null;
170
+ body: string | null;
171
+ addLabels: string | null;
172
+ removeLabels: string | null;
173
+ addAssignee: string | null;
174
+ removeAssignee: string | null;
175
+ }) {
176
+ const gh = yield* GitHubService;
177
+
178
+ const args = ["issue", "edit", String(opts.issue)];
179
+
180
+ if (opts.title !== null) {
181
+ args.push("--title", opts.title);
182
+ }
183
+ if (opts.body !== null) {
184
+ args.push("--body", opts.body);
185
+ }
186
+ if (opts.addLabels !== null) {
187
+ args.push("--add-label", opts.addLabels);
188
+ }
189
+ if (opts.removeLabels !== null) {
190
+ args.push("--remove-label", opts.removeLabels);
191
+ }
192
+ if (opts.addAssignee !== null) {
193
+ args.push("--add-assignee", opts.addAssignee);
194
+ }
195
+ if (opts.removeAssignee !== null) {
196
+ args.push("--remove-assignee", opts.removeAssignee);
197
+ }
198
+
199
+ yield* gh.runGh(args);
200
+
201
+ return yield* gh.runGhJson<IssueInfo>([
202
+ "issue",
203
+ "view",
204
+ String(opts.issue),
205
+ "--json",
206
+ "number,title,state,url,labels,assignees,author,createdAt,closedAt",
207
+ ]);
208
+ });
@@ -0,0 +1,9 @@
1
+ export {
2
+ issueCloseCommand,
3
+ issueCommentCommand,
4
+ issueEditCommand,
5
+ issueListCommand,
6
+ issueReopenCommand,
7
+ issueViewCommand,
8
+ } from "./commands";
9
+ export { issueTriageSummaryCommand } from "./triage";