Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ OmegaBot is online
│ ├── banner.png
│ └── omegabot.png
├── CONTRIBUTORS.md
├── data
│ └── timezones.json
├── docs
│ ├── commands.md
│ ├── dev-notes.md
Expand All @@ -142,6 +144,9 @@ OmegaBot is online
│ ├── commands
│ │ ├── general
│ │ │ └── ping.ts
│ │ ├── github
│ │ │ ├── gh.ts
│ │ │ └── pr.ts
│ │ ├── history
│ │ │ └── history.ts
│ │ ├── playback
Expand All @@ -156,6 +161,10 @@ OmegaBot is online
│ │ ├── commandLoader.ts
│ │ ├── fetchChannelMessages.ts
│ │ └── interactionHandler.ts
│ ├── github
│ │ ├── githubApi.ts
│ │ ├── githubClient.ts
│ │ └── types.ts
│ ├── summary
│ │ ├── llmSummary.ts
│ │ ├── localSummary.ts
Expand Down
98 changes: 66 additions & 32 deletions docs/commands.md
Original file line number Diff line number Diff line change
@@ -1,59 +1,93 @@
# Commands Overview
# OmegaBot Commands

This document describes all current and planned slash commands in OmegaBot.
This document lists all available slash commands supported by OmegaBot.

---

## /ping
## General Commands

Health check command.
### /ping

Purpose:
Checks whether the bot is online and reports latency.

- Confirms the bot is online
- Useful for verifying permissions and connectivity
**Usage**

```
/ping
```

---

## History & Summary Commands

### /history

Fetches recent messages from the current channel and sends them to you via DM.

**Options**

- `count` (optional): Number of messages to fetch (default: 50)

---

### /summary

Generates a summary of recent channel activity and sends it via DM.

**Notes**

- Uses local summarization by default
- LLM-based summarization may be enabled via configuration

---

## /history
## GitHub Commands

Chat playback command.
These commands use the GitHub REST API with authenticated requests.

Behavior:
### /gh issue

- Fetches the last N user messages from the current channel
- Filters out bot messages
- Sorts messages oldest → newest
- Sends transcript via DM
- Falls back to a file attachment if over Discord limits
Fetch a single GitHub issue by number.

Options:
**Usage**

- `count` (number): How many messages to fetch
```
/gh issue owner:<org|user> repo:<repo> number:<issue_number>
```

---

## /summary
### /gh issues

List open issues for a repository.

Conversation summarization command.
**Usage**

```
/gh issues owner:<org|user> repo:<repo> limit:<n> labels:<comma,separated>
```

**Notes**

- Defaults to open issues
- Pull requests are filtered out by default

---

Behavior:
### /pr

- Fetches recent messages
- Builds a transcript using the shared transcript builder
- Runs local or LLM summarization (depending on configuration)
- Sends result via DM
Fetch a GitHub pull request by number.

Options:
**Usage**

- `count` (number): How many messages to summarize
```
/pr owner:<org|user> repo:<repo> number:<pr_number>
```

---

## Planned / Future Commands
## Notes

- `/faq` — Lookup stored FAQs
- `/github issue` — Fetch GitHub issue details
- `/github pr` — Fetch pull request details
- `/announce pr` — Announce merged PRs
- `/playback` — Paginated chat playback
- GitHub commands require a valid GitHub Personal Access Token
- Results are returned as ephemeral responses by default
- Errors are handled gracefully and reported to the user
143 changes: 143 additions & 0 deletions src/commands/github/gh.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// src/commands/github/gh.ts

import {
MessageFlags,
SlashCommandBuilder,
type ChatInputCommandInteraction,
} from "discord.js";
import { GitHubApiError } from "../../services/github/githubClient.js";
import {
getIssue,
listIssues,
listPullRequests,
} from "../../services/github/githubApi.js";

/**
* /gh command
* GitHub helpers (issues + PRs)
*/
export const data = new SlashCommandBuilder()
.setName("gh")
.setDescription("GitHub helpers")

.addSubcommand((s) =>
s
.setName("issue")
.setDescription("Fetch a GitHub issue by number")
.addStringOption((o) =>
o.setName("owner").setDescription("Org/user").setRequired(true),
)
.addStringOption((o) => o.setName("repo").setDescription("Repo").setRequired(true))
.addIntegerOption((o) =>
o.setName("number").setDescription("Issue #").setRequired(true),
),
)

.addSubcommand((s) =>
s
.setName("issues")
.setDescription("List open issues")
.addStringOption((o) =>
o.setName("owner").setDescription("Org/user").setRequired(true),
)
.addStringOption((o) => o.setName("repo").setDescription("Repo").setRequired(true))
.addIntegerOption((o) =>
o
.setName("limit")
.setDescription("How many to show (default 5, max 20)")
.setMinValue(1)
.setMaxValue(20),
),
)

.addSubcommand((s) =>
s
.setName("prs")
.setDescription("List pull requests")
.addStringOption((o) =>
o.setName("owner").setDescription("Org/user").setRequired(true),
)
.addStringOption((o) => o.setName("repo").setDescription("Repo").setRequired(true))
.addIntegerOption((o) =>
o
.setName("limit")
.setDescription("How many to show (default 5, max 20)")
.setMinValue(1)
.setMaxValue(20),
),
);

export async function execute(interaction: ChatInputCommandInteraction): Promise<void> {
const sub = interaction.options.getSubcommand(true);

await interaction.deferReply({ flags: MessageFlags.Ephemeral });

const owner = interaction.options.getString("owner", true);
const repo = interaction.options.getString("repo", true);

try {
if (sub === "issue") {
const number = interaction.options.getInteger("number", true);
const issue = await getIssue(owner, repo, number);

await interaction.editReply(
[
`**${owner}/${repo}#${issue.number}**`,
issue.title,
`State: ${issue.state}`,
`Author: ${issue.user?.login ?? "unknown"}`,
issue.html_url,
].join("\n"),
);
return;
}

if (sub === "issues") {
const limit = interaction.options.getInteger("limit") ?? 5;
const issues = await listIssues(owner, repo, { state: "open", limit });

if (!issues.length) {
await interaction.editReply("No open issues found.");
return;
}

await interaction.editReply(
[
`**Open issues for ${owner}/${repo}**`,
"",
...issues.map(
(i) => `#${i.number} ${i.title} (by ${i.user?.login ?? "unknown"})`,
),
].join("\n"),
);
return;
}

if (sub === "prs") {
const limit = interaction.options.getInteger("limit") ?? 5;
const prs = await listPullRequests(owner, repo, { state: "open", limit });

if (!prs.length) {
await interaction.editReply("No open pull requests found.");
return;
}

await interaction.editReply(
[
`**Open PRs for ${owner}/${repo}**`,
"",
...prs.map((p) => `#${p.number} ${p.title} (by ${p.user?.login ?? "unknown"})`),
].join("\n"),
);
return;
}
} catch (err) {
if (err instanceof GitHubApiError) {
await interaction.editReply(`GitHub error (${err.status}): ${err.message}`);
return;
}

console.error("[gh] command failed", err);
await interaction.editReply("Something went wrong talking to GitHub.");
}
}
55 changes: 55 additions & 0 deletions src/commands/github/pr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// src/commands/github/pr.ts

import {
MessageFlags,
SlashCommandBuilder,
type ChatInputCommandInteraction,
} from "discord.js";
import { GitHubApiError } from "../../services/github/githubClient.js";
import { getPullRequest } from "../../services/github/githubApi.js";

/**
* /pr command
* Fetch a single PR by number.
*/
export const data = new SlashCommandBuilder()
.setName("pr")
.setDescription("GitHub pull request helpers")
.addIntegerOption((o) => o.setName("number").setDescription("PR #").setRequired(true))
.addStringOption((o) => o.setName("owner").setDescription("Org/user").setRequired(true))
.addStringOption((o) => o.setName("repo").setDescription("Repo").setRequired(true));

export async function execute(interaction: ChatInputCommandInteraction): Promise<void> {
await interaction.deferReply({ flags: MessageFlags.Ephemeral });

const owner = interaction.options.getString("owner", true);
const repo = interaction.options.getString("repo", true);
const number = interaction.options.getInteger("number", true);

try {
const pr = await getPullRequest(owner, repo, number);

const mergedLabel = pr.merged ? "merged" : "not merged";
const body = [
`**${owner}/${repo} PR #${pr.number}**`,
`${pr.title}`,
`State: ${pr.state} (${mergedLabel})`,
`Author: ${pr.user?.login ?? "unknown"}`,
`URL: ${pr.html_url}`,
].join("\n");

await interaction.editReply(body);
} catch (err) {
if (err instanceof GitHubApiError) {
if (err.status === 404) {
await interaction.editReply("PR not found. Check owner/repo and PR number.");
return;
}
await interaction.editReply(`GitHub API error (${err.status}): ${err.message}`);
return;
}

console.error("[pr] command failed", err);
await interaction.editReply("Something went wrong while talking to GitHub.");
}
}
3 changes: 3 additions & 0 deletions src/config/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,7 @@ export const env = {

// Optional, only needed when user chooses LLM summarization.
openAIKey: process.env.OPENAI_API_KEY ?? null,

// GitHub
githubToken: process.env.GITHUB_TOKEN ?? null,
};
Loading