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.
- package/.dockerignore +88 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +76 -0
- package/.github/ISSUE_TEMPLATE/config.yml +11 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +89 -0
- package/.github/ISSUE_TEMPLATE/question.md +63 -0
- package/.github/dependabot.yml +110 -0
- package/.github/pull_request_template.md +110 -0
- package/.github/workflows/DOCKER_DEPLOYMENT_SETUP.md +346 -0
- package/.github/workflows/codeql.yml +45 -0
- package/.github/workflows/dependabot-auto-merge.yml +42 -0
- package/.github/workflows/docker-publish.yml +277 -0
- package/.github/workflows/lint-and-test.yml +58 -0
- package/.github/workflows/publish-npm.yml +75 -0
- package/.github/workflows/secrets-scanning.yml +32 -0
- package/.github/workflows/security-update.yml +99 -0
- package/.memory-journal-team.db +0 -0
- package/.trivyignore +18 -0
- package/CHANGELOG.md +19 -0
- package/CODE_OF_CONDUCT.md +128 -0
- package/CONTRIBUTING.md +209 -0
- package/DOCKER_README.md +377 -0
- package/Dockerfile +64 -0
- package/LICENSE +21 -0
- package/README.md +461 -0
- package/SECURITY.md +200 -0
- package/VERSION +1 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +42 -0
- package/dist/cli.js.map +1 -0
- package/dist/constants/ServerInstructions.d.ts +8 -0
- package/dist/constants/ServerInstructions.d.ts.map +1 -0
- package/dist/constants/ServerInstructions.js +26 -0
- package/dist/constants/ServerInstructions.js.map +1 -0
- package/dist/database/SqliteAdapter.d.ts +198 -0
- package/dist/database/SqliteAdapter.d.ts.map +1 -0
- package/dist/database/SqliteAdapter.js +736 -0
- package/dist/database/SqliteAdapter.js.map +1 -0
- package/dist/filtering/ToolFilter.d.ts +63 -0
- package/dist/filtering/ToolFilter.d.ts.map +1 -0
- package/dist/filtering/ToolFilter.js +242 -0
- package/dist/filtering/ToolFilter.js.map +1 -0
- package/dist/github/GitHubIntegration.d.ts +91 -0
- package/dist/github/GitHubIntegration.d.ts.map +1 -0
- package/dist/github/GitHubIntegration.js +317 -0
- package/dist/github/GitHubIntegration.js.map +1 -0
- package/dist/handlers/prompts/index.d.ts +28 -0
- package/dist/handlers/prompts/index.d.ts.map +1 -0
- package/dist/handlers/prompts/index.js +366 -0
- package/dist/handlers/prompts/index.js.map +1 -0
- package/dist/handlers/resources/index.d.ts +27 -0
- package/dist/handlers/resources/index.d.ts.map +1 -0
- package/dist/handlers/resources/index.js +453 -0
- package/dist/handlers/resources/index.js.map +1 -0
- package/dist/handlers/tools/index.d.ts +26 -0
- package/dist/handlers/tools/index.d.ts.map +1 -0
- package/dist/handlers/tools/index.js +982 -0
- package/dist/handlers/tools/index.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/server/McpServer.d.ts +18 -0
- package/dist/server/McpServer.d.ts.map +1 -0
- package/dist/server/McpServer.js +171 -0
- package/dist/server/McpServer.js.map +1 -0
- package/dist/types/index.d.ts +300 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +15 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/McpLogger.d.ts +61 -0
- package/dist/utils/McpLogger.d.ts.map +1 -0
- package/dist/utils/McpLogger.js +113 -0
- package/dist/utils/McpLogger.js.map +1 -0
- package/dist/utils/logger.d.ts +30 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +70 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/vector/VectorSearchManager.d.ts +63 -0
- package/dist/vector/VectorSearchManager.d.ts.map +1 -0
- package/dist/vector/VectorSearchManager.js +235 -0
- package/dist/vector/VectorSearchManager.js.map +1 -0
- package/docker-compose.yml +37 -0
- package/eslint.config.js +86 -0
- package/mcp-config-example.json +21 -0
- package/package.json +71 -0
- package/releases/release-notes-v2.2.0.md +165 -0
- package/releases/release-notes.md +214 -0
- package/releases/v3.0.0.md +236 -0
- package/server.json +42 -0
- package/src/cli.ts +52 -0
- package/src/constants/ServerInstructions.ts +25 -0
- package/src/database/SqliteAdapter.ts +952 -0
- package/src/filtering/ToolFilter.ts +271 -0
- package/src/github/GitHubIntegration.ts +409 -0
- package/src/handlers/prompts/index.ts +420 -0
- package/src/handlers/resources/index.ts +529 -0
- package/src/handlers/tools/index.ts +1081 -0
- package/src/index.ts +53 -0
- package/src/server/McpServer.ts +230 -0
- package/src/types/index.ts +435 -0
- package/src/types/sql.js.d.ts +34 -0
- package/src/utils/McpLogger.ts +155 -0
- package/src/utils/logger.ts +98 -0
- package/src/vector/VectorSearchManager.ts +277 -0
- package/tools.json +300 -0
- 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
|
+
}
|