memory-journal-mcp 3.0.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 (107) hide show
  1. package/.dockerignore +88 -0
  2. package/.github/ISSUE_TEMPLATE/bug_report.md +76 -0
  3. package/.github/ISSUE_TEMPLATE/config.yml +11 -0
  4. package/.github/ISSUE_TEMPLATE/feature_request.md +89 -0
  5. package/.github/ISSUE_TEMPLATE/question.md +63 -0
  6. package/.github/dependabot.yml +110 -0
  7. package/.github/pull_request_template.md +110 -0
  8. package/.github/workflows/DOCKER_DEPLOYMENT_SETUP.md +346 -0
  9. package/.github/workflows/codeql.yml +45 -0
  10. package/.github/workflows/dependabot-auto-merge.yml +42 -0
  11. package/.github/workflows/docker-publish.yml +277 -0
  12. package/.github/workflows/lint-and-test.yml +58 -0
  13. package/.github/workflows/publish-npm.yml +75 -0
  14. package/.github/workflows/secrets-scanning.yml +32 -0
  15. package/.github/workflows/security-update.yml +99 -0
  16. package/.memory-journal-team.db +0 -0
  17. package/.trivyignore +18 -0
  18. package/CHANGELOG.md +19 -0
  19. package/CODE_OF_CONDUCT.md +128 -0
  20. package/CONTRIBUTING.md +209 -0
  21. package/DOCKER_README.md +377 -0
  22. package/Dockerfile +64 -0
  23. package/LICENSE +21 -0
  24. package/README.md +461 -0
  25. package/SECURITY.md +200 -0
  26. package/VERSION +1 -0
  27. package/dist/cli.d.ts +5 -0
  28. package/dist/cli.d.ts.map +1 -0
  29. package/dist/cli.js +42 -0
  30. package/dist/cli.js.map +1 -0
  31. package/dist/constants/ServerInstructions.d.ts +8 -0
  32. package/dist/constants/ServerInstructions.d.ts.map +1 -0
  33. package/dist/constants/ServerInstructions.js +26 -0
  34. package/dist/constants/ServerInstructions.js.map +1 -0
  35. package/dist/database/SqliteAdapter.d.ts +198 -0
  36. package/dist/database/SqliteAdapter.d.ts.map +1 -0
  37. package/dist/database/SqliteAdapter.js +736 -0
  38. package/dist/database/SqliteAdapter.js.map +1 -0
  39. package/dist/filtering/ToolFilter.d.ts +63 -0
  40. package/dist/filtering/ToolFilter.d.ts.map +1 -0
  41. package/dist/filtering/ToolFilter.js +242 -0
  42. package/dist/filtering/ToolFilter.js.map +1 -0
  43. package/dist/github/GitHubIntegration.d.ts +91 -0
  44. package/dist/github/GitHubIntegration.d.ts.map +1 -0
  45. package/dist/github/GitHubIntegration.js +317 -0
  46. package/dist/github/GitHubIntegration.js.map +1 -0
  47. package/dist/handlers/prompts/index.d.ts +28 -0
  48. package/dist/handlers/prompts/index.d.ts.map +1 -0
  49. package/dist/handlers/prompts/index.js +366 -0
  50. package/dist/handlers/prompts/index.js.map +1 -0
  51. package/dist/handlers/resources/index.d.ts +27 -0
  52. package/dist/handlers/resources/index.d.ts.map +1 -0
  53. package/dist/handlers/resources/index.js +453 -0
  54. package/dist/handlers/resources/index.js.map +1 -0
  55. package/dist/handlers/tools/index.d.ts +26 -0
  56. package/dist/handlers/tools/index.d.ts.map +1 -0
  57. package/dist/handlers/tools/index.js +982 -0
  58. package/dist/handlers/tools/index.js.map +1 -0
  59. package/dist/index.d.ts +11 -0
  60. package/dist/index.d.ts.map +1 -0
  61. package/dist/index.js +13 -0
  62. package/dist/index.js.map +1 -0
  63. package/dist/server/McpServer.d.ts +18 -0
  64. package/dist/server/McpServer.d.ts.map +1 -0
  65. package/dist/server/McpServer.js +171 -0
  66. package/dist/server/McpServer.js.map +1 -0
  67. package/dist/types/index.d.ts +300 -0
  68. package/dist/types/index.d.ts.map +1 -0
  69. package/dist/types/index.js +15 -0
  70. package/dist/types/index.js.map +1 -0
  71. package/dist/utils/McpLogger.d.ts +61 -0
  72. package/dist/utils/McpLogger.d.ts.map +1 -0
  73. package/dist/utils/McpLogger.js +113 -0
  74. package/dist/utils/McpLogger.js.map +1 -0
  75. package/dist/utils/logger.d.ts +30 -0
  76. package/dist/utils/logger.d.ts.map +1 -0
  77. package/dist/utils/logger.js +70 -0
  78. package/dist/utils/logger.js.map +1 -0
  79. package/dist/vector/VectorSearchManager.d.ts +63 -0
  80. package/dist/vector/VectorSearchManager.d.ts.map +1 -0
  81. package/dist/vector/VectorSearchManager.js +235 -0
  82. package/dist/vector/VectorSearchManager.js.map +1 -0
  83. package/docker-compose.yml +37 -0
  84. package/eslint.config.js +86 -0
  85. package/mcp-config-example.json +21 -0
  86. package/package.json +71 -0
  87. package/releases/release-notes-v2.2.0.md +165 -0
  88. package/releases/release-notes.md +214 -0
  89. package/releases/v3.0.0.md +236 -0
  90. package/server.json +42 -0
  91. package/src/cli.ts +52 -0
  92. package/src/constants/ServerInstructions.ts +25 -0
  93. package/src/database/SqliteAdapter.ts +952 -0
  94. package/src/filtering/ToolFilter.ts +271 -0
  95. package/src/github/GitHubIntegration.ts +409 -0
  96. package/src/handlers/prompts/index.ts +420 -0
  97. package/src/handlers/resources/index.ts +529 -0
  98. package/src/handlers/tools/index.ts +1081 -0
  99. package/src/index.ts +53 -0
  100. package/src/server/McpServer.ts +230 -0
  101. package/src/types/index.ts +435 -0
  102. package/src/types/sql.js.d.ts +34 -0
  103. package/src/utils/McpLogger.ts +155 -0
  104. package/src/utils/logger.ts +98 -0
  105. package/src/vector/VectorSearchManager.ts +277 -0
  106. package/tools.json +300 -0
  107. package/tsconfig.json +51 -0
@@ -0,0 +1,271 @@
1
+ /**
2
+ * Memory Journal MCP Server - Tool Filtering
3
+ *
4
+ * Configurable tool filtering system with groups and meta-groups.
5
+ * Matches mysql-mcp filtering syntax and patterns.
6
+ */
7
+
8
+ import type {
9
+ ToolGroup,
10
+ MetaGroup,
11
+ ToolFilterRule,
12
+ ToolFilterConfig,
13
+ } from '../types/index.js';
14
+
15
+ // Re-export ToolFilterConfig from types
16
+ export type { ToolFilterConfig } from '../types/index.js';
17
+
18
+ /**
19
+ * Tool group definitions mapping group names to tool names
20
+ *
21
+ * All 24 tools are categorized here for filtering support.
22
+ */
23
+ export const TOOL_GROUPS: Record<ToolGroup, string[]> = {
24
+ core: [
25
+ 'create_entry',
26
+ 'get_entry_by_id',
27
+ 'get_recent_entries',
28
+ 'create_entry_minimal',
29
+ 'test_simple',
30
+ 'list_tags',
31
+ ],
32
+ search: [
33
+ 'search_entries',
34
+ 'search_by_date_range',
35
+ 'semantic_search',
36
+ 'get_vector_index_stats',
37
+ ],
38
+ analytics: [
39
+ 'get_statistics',
40
+ 'get_cross_project_insights',
41
+ ],
42
+ relationships: [
43
+ 'link_entries',
44
+ 'visualize_relationships',
45
+ ],
46
+ export: [
47
+ 'export_entries',
48
+ ],
49
+ admin: [
50
+ 'update_entry',
51
+ 'delete_entry',
52
+ 'rebuild_vector_index',
53
+ 'add_to_vector_index',
54
+ ],
55
+ github: [
56
+ 'get_github_issues',
57
+ 'get_github_prs',
58
+ 'get_github_issue',
59
+ 'get_github_pr',
60
+ 'get_github_context',
61
+ ],
62
+ backup: [
63
+ 'backup_journal',
64
+ 'list_backups',
65
+ 'restore_backup',
66
+ ],
67
+ };
68
+
69
+ /**
70
+ * Meta-group definitions mapping shortcuts to groups
71
+ */
72
+ export const META_GROUPS: Record<MetaGroup, ToolGroup[]> = {
73
+ starter: ['core', 'search'],
74
+ essential: ['core'],
75
+ full: ['core', 'search', 'analytics', 'relationships', 'export', 'admin', 'github', 'backup'],
76
+ readonly: ['core', 'search', 'analytics', 'relationships', 'export'],
77
+ };
78
+
79
+ /**
80
+ * Get all tool names across all groups
81
+ */
82
+ export function getAllToolNames(): string[] {
83
+ const allTools: string[] = [];
84
+ for (const tools of Object.values(TOOL_GROUPS)) {
85
+ allTools.push(...tools);
86
+ }
87
+ return allTools;
88
+ }
89
+
90
+ /**
91
+ * Get the group for a specific tool
92
+ */
93
+ export function getToolGroup(toolName: string): ToolGroup | undefined {
94
+ for (const [group, tools] of Object.entries(TOOL_GROUPS)) {
95
+ if (tools.includes(toolName)) {
96
+ return group as ToolGroup;
97
+ }
98
+ }
99
+ return undefined;
100
+ }
101
+
102
+ /**
103
+ * Check if a string is a valid group name
104
+ */
105
+ function isGroup(name: string): name is ToolGroup {
106
+ return name in TOOL_GROUPS;
107
+ }
108
+
109
+ /**
110
+ * Check if a string is a valid meta-group name
111
+ */
112
+ function isMetaGroup(name: string): name is MetaGroup {
113
+ return name in META_GROUPS;
114
+ }
115
+
116
+ /**
117
+ * Parse a tool filter string into configuration
118
+ *
119
+ * Syntax:
120
+ * - `starter` - Use starter preset (whitelist mode)
121
+ * - `core,search` - Enable specific groups (whitelist mode)
122
+ * - `full,-admin` - All tools except admin group
123
+ * - `starter,-delete_entry` - Starter without specific tool
124
+ * - `+semantic_search` - Add specific tool to current set
125
+ */
126
+ export function parseToolFilter(filterString: string): ToolFilterConfig {
127
+ const rules: ToolFilterRule[] = [];
128
+ const parts = filterString.split(',').map(p => p.trim()).filter(Boolean);
129
+
130
+ // Determine if we're in whitelist or blacklist mode
131
+ // If first item has no prefix and is a group/metagroup, we're in whitelist mode
132
+ let enabledTools = new Set<string>();
133
+ let isWhitelistMode = false;
134
+
135
+ for (let i = 0; i < parts.length; i++) {
136
+ const part = parts[i];
137
+ if (!part) continue;
138
+
139
+ const isAdd = part.startsWith('+');
140
+ const isRemove = part.startsWith('-');
141
+ const name = (isAdd || isRemove) ? part.slice(1) : part;
142
+
143
+ if (i === 0 && !isAdd && !isRemove) {
144
+ // First item without prefix - whitelist mode
145
+ isWhitelistMode = true;
146
+
147
+ if (isMetaGroup(name)) {
148
+ // Expand meta-group to groups
149
+ for (const group of META_GROUPS[name]) {
150
+ enabledTools = new Set([...enabledTools, ...TOOL_GROUPS[group]]);
151
+ }
152
+ } else if (isGroup(name)) {
153
+ enabledTools = new Set([...enabledTools, ...TOOL_GROUPS[name]]);
154
+ } else {
155
+ // Single tool
156
+ enabledTools.add(name);
157
+ }
158
+
159
+ rules.push({
160
+ type: 'include',
161
+ target: name,
162
+ isGroup: isGroup(name) || isMetaGroup(name),
163
+ });
164
+ } else if (isRemove) {
165
+ // Remove group or tool
166
+ if (isGroup(name)) {
167
+ for (const tool of TOOL_GROUPS[name]) {
168
+ enabledTools.delete(tool);
169
+ }
170
+ } else {
171
+ enabledTools.delete(name);
172
+ }
173
+
174
+ rules.push({
175
+ type: 'exclude',
176
+ target: name,
177
+ isGroup: isGroup(name),
178
+ });
179
+ } else {
180
+ // Add group or tool (with or without + prefix)
181
+ if (isMetaGroup(name)) {
182
+ for (const group of META_GROUPS[name]) {
183
+ enabledTools = new Set([...enabledTools, ...TOOL_GROUPS[group]]);
184
+ }
185
+ } else if (isGroup(name)) {
186
+ enabledTools = new Set([...enabledTools, ...TOOL_GROUPS[name]]);
187
+ } else {
188
+ enabledTools.add(name);
189
+ }
190
+
191
+ rules.push({
192
+ type: 'include',
193
+ target: name,
194
+ isGroup: isGroup(name) || isMetaGroup(name),
195
+ });
196
+ }
197
+ }
198
+
199
+ // If no filter specified or starting with removal, start with all tools
200
+ if (!isWhitelistMode && rules.length > 0 && rules[0]?.type === 'exclude') {
201
+ enabledTools = new Set(getAllToolNames());
202
+ // Re-apply rules
203
+ for (const rule of rules) {
204
+ if (rule.type === 'exclude') {
205
+ if (isGroup(rule.target)) {
206
+ for (const tool of TOOL_GROUPS[rule.target]) {
207
+ enabledTools.delete(tool);
208
+ }
209
+ } else {
210
+ enabledTools.delete(rule.target);
211
+ }
212
+ }
213
+ }
214
+ }
215
+
216
+ return {
217
+ raw: filterString,
218
+ rules,
219
+ enabledTools,
220
+ };
221
+ }
222
+
223
+ /**
224
+ * Check if a tool is enabled based on filter string
225
+ */
226
+ export function isToolEnabled(toolName: string, filterConfig: ToolFilterConfig): boolean {
227
+ return filterConfig.enabledTools.has(toolName);
228
+ }
229
+
230
+ /**
231
+ * Filter tools array based on filter configuration
232
+ */
233
+ export function filterTools<T extends { name: string }>(
234
+ tools: T[],
235
+ filterConfig: ToolFilterConfig
236
+ ): T[] {
237
+ return tools.filter(tool => isToolEnabled(tool.name, filterConfig));
238
+ }
239
+
240
+ /**
241
+ * Get tool filter from environment variable
242
+ */
243
+ export function getToolFilterFromEnv(): ToolFilterConfig | null {
244
+ const filterString = process.env['MEMORY_JOURNAL_MCP_TOOL_FILTER'];
245
+ if (!filterString) return null;
246
+ return parseToolFilter(filterString);
247
+ }
248
+
249
+ /**
250
+ * Calculate token savings from filtering
251
+ */
252
+ export function calculateTokenSavings(
253
+ totalTools: number,
254
+ enabledTools: number,
255
+ avgTokensPerTool = 150
256
+ ): { reduction: number; savedTokens: number } {
257
+ const savedTokens = (totalTools - enabledTools) * avgTokensPerTool;
258
+ const reduction = ((totalTools - enabledTools) / totalTools) * 100;
259
+ return { reduction, savedTokens };
260
+ }
261
+
262
+ /**
263
+ * Get human-readable filter summary
264
+ */
265
+ export function getFilterSummary(filterConfig: ToolFilterConfig): string {
266
+ const total = getAllToolNames().length;
267
+ const enabled = filterConfig.enabledTools.size;
268
+ const { reduction } = calculateTokenSavings(total, enabled);
269
+
270
+ return `${enabled}/${total} tools enabled (${reduction.toFixed(0)}% reduction)`;
271
+ }
@@ -0,0 +1,409 @@
1
+ /**
2
+ * Memory Journal MCP Server - GitHub Integration
3
+ *
4
+ * GitHub API integration using @octokit/rest for API access
5
+ * and simple-git for local repository operations.
6
+ */
7
+
8
+ import { Octokit } from '@octokit/rest';
9
+ import * as simpleGitImport from 'simple-git';
10
+ import { logger } from '../utils/logger.js';
11
+ import type {
12
+ GitHubIssue,
13
+ GitHubPullRequest,
14
+ GitHubWorkflowRun,
15
+ ProjectContext,
16
+ } from '../types/index.js';
17
+
18
+ // Handle simpleGit ESM/CJS interop
19
+ type SimpleGitType = typeof simpleGitImport.simpleGit;
20
+ const simpleGit: SimpleGitType = simpleGitImport.simpleGit;
21
+
22
+ /**
23
+ * Local repository information
24
+ */
25
+ export interface RepoInfo {
26
+ owner: string | null;
27
+ repo: string | null;
28
+ branch: string | null;
29
+ remoteUrl: string | null;
30
+ }
31
+
32
+ /**
33
+ * GitHub issue details (extended)
34
+ */
35
+ export interface IssueDetails extends GitHubIssue {
36
+ body: string | null;
37
+ labels: string[];
38
+ assignees: string[];
39
+ createdAt: string;
40
+ updatedAt: string;
41
+ closedAt: string | null;
42
+ commentsCount: number;
43
+ }
44
+
45
+ /**
46
+ * GitHub PR details (extended)
47
+ */
48
+ export interface PullRequestDetails extends GitHubPullRequest {
49
+ body: string | null;
50
+ draft: boolean;
51
+ headBranch: string;
52
+ baseBranch: string;
53
+ author: string;
54
+ createdAt: string;
55
+ updatedAt: string;
56
+ mergedAt: string | null;
57
+ closedAt: string | null;
58
+ additions: number;
59
+ deletions: number;
60
+ changedFiles: number;
61
+ }
62
+
63
+ /**
64
+ * GitHubIntegration - Handles GitHub API and local git operations
65
+ */
66
+ export class GitHubIntegration {
67
+ private octokit: Octokit | null = null;
68
+ private git: simpleGitImport.SimpleGit;
69
+ private readonly token: string | undefined;
70
+
71
+ constructor(workingDir = '.') {
72
+ this.token = process.env['GITHUB_TOKEN'];
73
+
74
+ // Use GITHUB_REPO_PATH env var if set, otherwise fall back to workingDir
75
+ const envRepoPath = process.env['GITHUB_REPO_PATH'];
76
+ const effectiveDir = envRepoPath || workingDir;
77
+
78
+ // Resolve and log the actual working directory
79
+ const resolvedDir = effectiveDir === '.' ? process.cwd() : effectiveDir;
80
+ logger.info('GitHub integration using directory', {
81
+ module: 'GitHub',
82
+ workingDir,
83
+ envRepoPath: envRepoPath ?? 'not set',
84
+ effectiveDir,
85
+ resolvedDir,
86
+ cwd: process.cwd()
87
+ });
88
+
89
+ this.git = simpleGit(effectiveDir);
90
+
91
+ // Initialize Octokit if token is available
92
+ if (this.token) {
93
+ this.octokit = new Octokit({ auth: this.token });
94
+ logger.info('GitHub integration initialized with token', { module: 'GitHub' });
95
+ } else {
96
+ logger.info('GitHub integration initialized without token (limited functionality)', { module: 'GitHub' });
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Check if GitHub API is available (token present)
102
+ */
103
+ isApiAvailable(): boolean {
104
+ return this.octokit !== null;
105
+ }
106
+
107
+ /**
108
+ * Get local repository information
109
+ */
110
+ async getRepoInfo(): Promise<RepoInfo> {
111
+ try {
112
+ // Get current branch
113
+ const branchResult = await this.git.branch();
114
+ const branch = branchResult.current || null;
115
+
116
+ // Get remote URL
117
+ const remotes = await this.git.getRemotes(true);
118
+ const origin = remotes.find(r => r.name === 'origin');
119
+ const remoteUrl = origin?.refs?.fetch || null;
120
+
121
+ // Parse owner/repo from remote URL
122
+ const { owner, repo } = this.parseRemoteUrl(remoteUrl);
123
+
124
+ return { owner, repo, branch, remoteUrl };
125
+ } catch (error) {
126
+ logger.debug('Failed to get repo info (may not be a git repo)', {
127
+ module: 'GitHub',
128
+ error: error instanceof Error ? error.message : String(error)
129
+ });
130
+ return { owner: null, repo: null, branch: null, remoteUrl: null };
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Parse owner and repo from GitHub remote URL
136
+ */
137
+ private parseRemoteUrl(remoteUrl: string | null): { owner: string | null; repo: string | null } {
138
+ if (!remoteUrl) return { owner: null, repo: null };
139
+
140
+ // Handle SSH format: git@github.com:owner/repo.git
141
+ if (remoteUrl.startsWith('git@github.com:')) {
142
+ const pathPart = remoteUrl.replace('git@github.com:', '').replace('.git', '');
143
+ const parts = pathPart.split('/');
144
+ if (parts.length >= 2) {
145
+ return { owner: parts[0] ?? null, repo: parts[1] ?? null };
146
+ }
147
+ }
148
+
149
+ // Handle HTTPS format: https://github.com/owner/repo.git
150
+ try {
151
+ const url = new URL(remoteUrl);
152
+ if (url.hostname === 'github.com') {
153
+ const path = url.pathname.replace('.git', '').replace(/^\//, '');
154
+ const parts = path.split('/');
155
+ if (parts.length >= 2) {
156
+ return { owner: parts[0] ?? null, repo: parts[1] ?? null };
157
+ }
158
+ }
159
+ } catch {
160
+ // Not a valid URL
161
+ }
162
+
163
+ return { owner: null, repo: null };
164
+ }
165
+
166
+ /**
167
+ * Get repository issues
168
+ */
169
+ async getIssues(
170
+ owner: string,
171
+ repo: string,
172
+ state: 'open' | 'closed' | 'all' = 'open',
173
+ limit = 20
174
+ ): Promise<GitHubIssue[]> {
175
+ if (!this.octokit) {
176
+ return [];
177
+ }
178
+
179
+ try {
180
+ const response = await this.octokit.issues.listForRepo({
181
+ owner,
182
+ repo,
183
+ state,
184
+ per_page: limit,
185
+ sort: 'updated',
186
+ direction: 'desc',
187
+ });
188
+
189
+ // Filter out pull requests (GitHub API includes PRs in issues)
190
+ return response.data
191
+ .filter(issue => !issue.pull_request)
192
+ .map(issue => ({
193
+ number: issue.number,
194
+ title: issue.title,
195
+ url: issue.html_url,
196
+ state: issue.state === 'open' ? 'OPEN' : 'CLOSED',
197
+ }));
198
+ } catch (error) {
199
+ logger.error('Failed to get issues', {
200
+ module: 'GitHub',
201
+ error: error instanceof Error ? error.message : String(error)
202
+ });
203
+ return [];
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Get issue details
209
+ */
210
+ async getIssue(owner: string, repo: string, issueNumber: number): Promise<IssueDetails | null> {
211
+ if (!this.octokit) {
212
+ return null;
213
+ }
214
+
215
+ try {
216
+ const response = await this.octokit.issues.get({
217
+ owner,
218
+ repo,
219
+ issue_number: issueNumber,
220
+ });
221
+
222
+ const issue = response.data;
223
+
224
+ // Verify it's not a PR
225
+ if (issue.pull_request) {
226
+ return null;
227
+ }
228
+
229
+ return {
230
+ number: issue.number,
231
+ title: issue.title,
232
+ url: issue.html_url,
233
+ state: issue.state === 'open' ? 'OPEN' : 'CLOSED',
234
+ body: issue.body ?? null,
235
+ labels: issue.labels.map(l => (typeof l === 'string' ? l : l.name ?? '')),
236
+ assignees: issue.assignees?.map(a => a.login) ?? [],
237
+ createdAt: issue.created_at,
238
+ updatedAt: issue.updated_at,
239
+ closedAt: issue.closed_at,
240
+ commentsCount: issue.comments,
241
+ };
242
+ } catch (error) {
243
+ logger.error('Failed to get issue details', {
244
+ module: 'GitHub',
245
+ entityId: issueNumber,
246
+ error: error instanceof Error ? error.message : String(error)
247
+ });
248
+ return null;
249
+ }
250
+ }
251
+
252
+ /**
253
+ * Get repository pull requests
254
+ */
255
+ async getPullRequests(
256
+ owner: string,
257
+ repo: string,
258
+ state: 'open' | 'closed' | 'all' = 'open',
259
+ limit = 20
260
+ ): Promise<GitHubPullRequest[]> {
261
+ if (!this.octokit) {
262
+ return [];
263
+ }
264
+
265
+ try {
266
+ const response = await this.octokit.pulls.list({
267
+ owner,
268
+ repo,
269
+ state,
270
+ per_page: limit,
271
+ sort: 'updated',
272
+ direction: 'desc',
273
+ });
274
+
275
+ return response.data.map(pr => ({
276
+ number: pr.number,
277
+ title: pr.title,
278
+ url: pr.html_url,
279
+ state: pr.merged_at ? 'MERGED' : (pr.state === 'open' ? 'OPEN' : 'CLOSED'),
280
+ }));
281
+ } catch (error) {
282
+ logger.error('Failed to get pull requests', {
283
+ module: 'GitHub',
284
+ error: error instanceof Error ? error.message : String(error)
285
+ });
286
+ return [];
287
+ }
288
+ }
289
+
290
+ /**
291
+ * Get PR details
292
+ */
293
+ async getPullRequest(owner: string, repo: string, prNumber: number): Promise<PullRequestDetails | null> {
294
+ if (!this.octokit) {
295
+ return null;
296
+ }
297
+
298
+ try {
299
+ const response = await this.octokit.pulls.get({
300
+ owner,
301
+ repo,
302
+ pull_number: prNumber,
303
+ });
304
+
305
+ const pr = response.data;
306
+
307
+ return {
308
+ number: pr.number,
309
+ title: pr.title,
310
+ url: pr.html_url,
311
+ state: pr.merged_at ? 'MERGED' : (pr.state === 'open' ? 'OPEN' : 'CLOSED'),
312
+ body: pr.body,
313
+ draft: pr.draft ?? false,
314
+ headBranch: pr.head.ref,
315
+ baseBranch: pr.base.ref,
316
+ author: pr.user?.login ?? 'unknown',
317
+ createdAt: pr.created_at,
318
+ updatedAt: pr.updated_at,
319
+ mergedAt: pr.merged_at,
320
+ closedAt: pr.closed_at,
321
+ additions: pr.additions,
322
+ deletions: pr.deletions,
323
+ changedFiles: pr.changed_files,
324
+ };
325
+ } catch (error) {
326
+ logger.error('Failed to get PR details', {
327
+ module: 'GitHub',
328
+ entityId: prNumber,
329
+ error: error instanceof Error ? error.message : String(error)
330
+ });
331
+ return null;
332
+ }
333
+ }
334
+
335
+ /**
336
+ * Get workflow runs from GitHub Actions
337
+ */
338
+ async getWorkflowRuns(
339
+ owner: string,
340
+ repo: string,
341
+ limit = 10
342
+ ): Promise<GitHubWorkflowRun[]> {
343
+ if (!this.octokit) {
344
+ logger.debug('GitHub API not available - no token', { module: 'GitHub' });
345
+ return [];
346
+ }
347
+
348
+ try {
349
+ const response = await this.octokit.rest.actions.listWorkflowRunsForRepo({
350
+ owner,
351
+ repo,
352
+ per_page: limit,
353
+ });
354
+
355
+ return response.data.workflow_runs.map(run => ({
356
+ id: run.id,
357
+ name: run.name ?? 'Unknown Workflow',
358
+ status: run.status as 'queued' | 'in_progress' | 'completed',
359
+ conclusion: run.conclusion as 'success' | 'failure' | 'cancelled' | 'skipped' | null,
360
+ url: run.html_url,
361
+ headBranch: run.head_branch ?? '',
362
+ headSha: run.head_sha,
363
+ createdAt: run.created_at,
364
+ updatedAt: run.updated_at,
365
+ }));
366
+ } catch (error) {
367
+ logger.error('Failed to get workflow runs', {
368
+ module: 'GitHub',
369
+ error: error instanceof Error ? error.message : String(error),
370
+ });
371
+ return [];
372
+ }
373
+ }
374
+
375
+ /**
376
+ * Get full repository context (issues, PRs, branch info)
377
+ */
378
+ async getRepoContext(): Promise<ProjectContext> {
379
+ const repoInfo = await this.getRepoInfo();
380
+
381
+ const context: ProjectContext = {
382
+ repoName: repoInfo.repo,
383
+ branch: repoInfo.branch,
384
+ commit: null,
385
+ remoteUrl: repoInfo.remoteUrl,
386
+ projects: [],
387
+ issues: [],
388
+ pullRequests: [],
389
+ workflowRuns: [],
390
+ };
391
+
392
+ // Get current commit
393
+ try {
394
+ const log = await this.git.log({ maxCount: 1 });
395
+ context.commit = log.latest?.hash ?? null;
396
+ } catch {
397
+ // Ignore error
398
+ }
399
+
400
+ // Get issues, PRs, and workflow runs if we have owner/repo
401
+ if (repoInfo.owner && repoInfo.repo) {
402
+ context.issues = await this.getIssues(repoInfo.owner, repoInfo.repo, 'open', 10);
403
+ context.pullRequests = await this.getPullRequests(repoInfo.owner, repoInfo.repo, 'open', 10);
404
+ context.workflowRuns = await this.getWorkflowRuns(repoInfo.owner, repoInfo.repo, 10);
405
+ }
406
+
407
+ return context;
408
+ }
409
+ }