memory-journal-mcp 4.3.0 → 4.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/.dockerignore +131 -122
- package/.gitattributes +29 -0
- package/.github/workflows/docker-publish.yml +1 -1
- package/.github/workflows/lint-and-test.yml +1 -2
- package/.github/workflows/secrets-scanning.yml +0 -1
- package/.github/workflows/security-update.yml +6 -6
- package/.vscode/settings.json +17 -15
- package/CHANGELOG.md +1065 -11
- package/DOCKER_README.md +51 -33
- package/Dockerfile +14 -12
- package/README.md +68 -33
- package/SECURITY.md +225 -220
- package/dist/cli.js +7 -0
- package/dist/cli.js.map +1 -1
- package/dist/constants/ServerInstructions.d.ts +1 -1
- package/dist/constants/ServerInstructions.d.ts.map +1 -1
- package/dist/constants/ServerInstructions.js +70 -26
- package/dist/constants/ServerInstructions.js.map +1 -1
- package/dist/constants/icons.d.ts +2 -0
- package/dist/constants/icons.d.ts.map +1 -1
- package/dist/constants/icons.js +6 -0
- package/dist/constants/icons.js.map +1 -1
- package/dist/database/SqliteAdapter.d.ts +51 -10
- package/dist/database/SqliteAdapter.d.ts.map +1 -1
- package/dist/database/SqliteAdapter.js +143 -43
- package/dist/database/SqliteAdapter.js.map +1 -1
- package/dist/filtering/ToolFilter.d.ts +1 -1
- package/dist/filtering/ToolFilter.d.ts.map +1 -1
- package/dist/filtering/ToolFilter.js +7 -1
- package/dist/filtering/ToolFilter.js.map +1 -1
- package/dist/github/GitHubIntegration.d.ts +74 -2
- package/dist/github/GitHubIntegration.d.ts.map +1 -1
- package/dist/github/GitHubIntegration.js +508 -7
- package/dist/github/GitHubIntegration.js.map +1 -1
- package/dist/handlers/prompts/index.js +1 -0
- package/dist/handlers/prompts/index.js.map +1 -1
- package/dist/handlers/resources/index.d.ts.map +1 -1
- package/dist/handlers/resources/index.js +257 -13
- package/dist/handlers/resources/index.js.map +1 -1
- package/dist/handlers/tools/index.d.ts.map +1 -1
- package/dist/handlers/tools/index.js +595 -8
- package/dist/handlers/tools/index.js.map +1 -1
- package/dist/server/McpServer.d.ts +2 -0
- package/dist/server/McpServer.d.ts.map +1 -1
- package/dist/server/McpServer.js +69 -26
- package/dist/server/McpServer.js.map +1 -1
- package/dist/types/index.d.ts +97 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/utils/logger.d.ts +1 -0
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +8 -1
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/progress-utils.d.ts +18 -3
- package/dist/utils/progress-utils.d.ts.map +1 -1
- package/dist/utils/progress-utils.js.map +1 -1
- package/dist/utils/security-utils.d.ts +91 -0
- package/dist/utils/security-utils.d.ts.map +1 -0
- package/dist/utils/security-utils.js +184 -0
- package/dist/utils/security-utils.js.map +1 -0
- package/dist/vector/VectorSearchManager.d.ts +2 -1
- package/dist/vector/VectorSearchManager.d.ts.map +1 -1
- package/dist/vector/VectorSearchManager.js +100 -34
- package/dist/vector/VectorSearchManager.js.map +1 -1
- package/docker-compose.yml +46 -37
- package/mcp-config-example.json +0 -2
- package/package.json +21 -14
- package/releases/v4.3.1.md +69 -0
- package/releases/v4.4.0.md +120 -0
- package/server.json +3 -3
- package/src/cli.ts +11 -0
- package/src/constants/ServerInstructions.ts +70 -26
- package/src/constants/icons.ts +7 -0
- package/src/database/SqliteAdapter.ts +165 -44
- package/src/filtering/ToolFilter.ts +7 -1
- package/src/github/GitHubIntegration.ts +588 -8
- package/src/handlers/prompts/index.ts +1 -0
- package/src/handlers/resources/index.ts +318 -12
- package/src/handlers/tools/index.ts +686 -13
- package/src/server/McpServer.ts +79 -37
- package/src/types/index.ts +98 -0
- package/src/utils/logger.ts +10 -1
- package/src/utils/progress-utils.ts +17 -6
- package/src/utils/security-utils.ts +205 -0
- package/src/vector/VectorSearchManager.ts +110 -39
- package/tests/constants/icons.test.ts +102 -0
- package/tests/constants/server-instructions.test.ts +549 -0
- package/tests/database/sqlite-adapter.bench.ts +63 -0
- package/tests/database/sqlite-adapter.test.ts +555 -0
- package/tests/filtering/tool-filter.test.ts +266 -0
- package/tests/github/github-integration.test.ts +1024 -0
- package/tests/handlers/github-resource-handlers.test.ts +473 -0
- package/tests/handlers/github-tool-handlers.test.ts +556 -0
- package/tests/handlers/prompt-handlers.test.ts +91 -0
- package/tests/handlers/resource-handlers.test.ts +339 -0
- package/tests/handlers/tool-handlers.test.ts +497 -0
- package/tests/handlers/vector-tool-handlers.test.ts +238 -0
- package/tests/security/sql-injection.test.ts +347 -0
- package/tests/server/mcp-server.bench.ts +55 -0
- package/tests/server/mcp-server.test.ts +675 -0
- package/tests/utils/logger.test.ts +180 -0
- package/tests/utils/mcp-logger.test.ts +212 -0
- package/tests/utils/progress-utils.test.ts +156 -0
- package/tests/utils/security-utils.test.ts +82 -0
- package/tests/vector/vector-search-manager.test.ts +335 -0
- package/tests/vector/vector-search.bench.ts +53 -0
- package/vitest.config.ts +15 -0
- package/.github/workflows/DOCKER_DEPLOYMENT_SETUP.md +0 -387
- package/.github/workflows/dependabot-auto-merge.yml +0 -42
|
@@ -8,6 +8,10 @@ import { Octokit } from '@octokit/rest';
|
|
|
8
8
|
import { graphql } from '@octokit/graphql';
|
|
9
9
|
import * as simpleGitImport from 'simple-git';
|
|
10
10
|
import { logger } from '../utils/logger.js';
|
|
11
|
+
/** TTL for cached GitHub API responses (5 minutes) */
|
|
12
|
+
const CACHE_TTL_MS = 5 * 60 * 1000;
|
|
13
|
+
/** TTL for cached traffic/insights responses (10 minutes - traffic data changes slowly) */
|
|
14
|
+
const TRAFFIC_CACHE_TTL_MS = 10 * 60 * 1000;
|
|
11
15
|
const simpleGit = simpleGitImport.simpleGit;
|
|
12
16
|
/**
|
|
13
17
|
* GitHubIntegration - Handles GitHub API and local git operations
|
|
@@ -18,6 +22,8 @@ export class GitHubIntegration {
|
|
|
18
22
|
git;
|
|
19
23
|
token;
|
|
20
24
|
cachedRepoInfo = null;
|
|
25
|
+
/** TTL response cache for GitHub API read methods */
|
|
26
|
+
apiCache = new Map();
|
|
21
27
|
constructor(workingDir = '.') {
|
|
22
28
|
this.token = process.env['GITHUB_TOKEN'];
|
|
23
29
|
// Use GITHUB_REPO_PATH env var if set, otherwise fall back to workingDir
|
|
@@ -54,6 +60,43 @@ export class GitHubIntegration {
|
|
|
54
60
|
isApiAvailable() {
|
|
55
61
|
return this.octokit !== null;
|
|
56
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Get a cached value if it exists and hasn't expired.
|
|
65
|
+
*/
|
|
66
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -- T is needed at call sites for type-safe casts
|
|
67
|
+
getCached(key) {
|
|
68
|
+
const entry = this.apiCache.get(key);
|
|
69
|
+
if (entry && Date.now() - entry.timestamp < CACHE_TTL_MS) {
|
|
70
|
+
return entry.data;
|
|
71
|
+
}
|
|
72
|
+
if (entry) {
|
|
73
|
+
this.apiCache.delete(key);
|
|
74
|
+
}
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Store a value in the cache.
|
|
79
|
+
*/
|
|
80
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -- T preserves type inference at call sites
|
|
81
|
+
setCache(key, data) {
|
|
82
|
+
this.apiCache.set(key, { data, timestamp: Date.now() });
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Invalidate all cache entries matching a prefix.
|
|
86
|
+
*/
|
|
87
|
+
invalidateCache(prefix) {
|
|
88
|
+
for (const key of this.apiCache.keys()) {
|
|
89
|
+
if (key.startsWith(prefix)) {
|
|
90
|
+
this.apiCache.delete(key);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Clear all cached GitHub API responses.
|
|
96
|
+
*/
|
|
97
|
+
clearCache() {
|
|
98
|
+
this.apiCache.clear();
|
|
99
|
+
}
|
|
57
100
|
/**
|
|
58
101
|
* Get local repository information
|
|
59
102
|
* Caches the result for synchronous access via getCachedRepoInfo()
|
|
@@ -127,6 +170,10 @@ export class GitHubIntegration {
|
|
|
127
170
|
if (!this.octokit) {
|
|
128
171
|
return [];
|
|
129
172
|
}
|
|
173
|
+
const cacheKey = `issues:${owner}:${repo}:${state}:${String(limit)}`;
|
|
174
|
+
const cached = this.getCached(cacheKey);
|
|
175
|
+
if (cached)
|
|
176
|
+
return cached;
|
|
130
177
|
try {
|
|
131
178
|
const response = await this.octokit.issues.listForRepo({
|
|
132
179
|
owner,
|
|
@@ -137,14 +184,22 @@ export class GitHubIntegration {
|
|
|
137
184
|
direction: 'desc',
|
|
138
185
|
});
|
|
139
186
|
// Filter out pull requests (GitHub API includes PRs in issues)
|
|
140
|
-
|
|
187
|
+
const result = response.data
|
|
141
188
|
.filter((issue) => !issue.pull_request)
|
|
142
189
|
.map((issue) => ({
|
|
143
190
|
number: issue.number,
|
|
144
191
|
title: issue.title,
|
|
145
192
|
url: issue.html_url,
|
|
146
193
|
state: issue.state === 'open' ? 'OPEN' : 'CLOSED',
|
|
194
|
+
milestone: issue.milestone
|
|
195
|
+
? {
|
|
196
|
+
number: issue.milestone.number,
|
|
197
|
+
title: issue.milestone.title,
|
|
198
|
+
}
|
|
199
|
+
: null,
|
|
147
200
|
}));
|
|
201
|
+
this.setCache(cacheKey, result);
|
|
202
|
+
return result;
|
|
148
203
|
}
|
|
149
204
|
catch (error) {
|
|
150
205
|
logger.error('Failed to get issues', {
|
|
@@ -161,6 +216,10 @@ export class GitHubIntegration {
|
|
|
161
216
|
if (!this.octokit) {
|
|
162
217
|
return null;
|
|
163
218
|
}
|
|
219
|
+
const cacheKey = `issue:${owner}:${repo}:${String(issueNumber)}`;
|
|
220
|
+
const cached = this.getCached(cacheKey);
|
|
221
|
+
if (cached !== undefined)
|
|
222
|
+
return cached;
|
|
164
223
|
try {
|
|
165
224
|
const response = await this.octokit.issues.get({
|
|
166
225
|
owner,
|
|
@@ -172,7 +231,7 @@ export class GitHubIntegration {
|
|
|
172
231
|
if (issue.pull_request) {
|
|
173
232
|
return null;
|
|
174
233
|
}
|
|
175
|
-
|
|
234
|
+
const details = {
|
|
176
235
|
number: issue.number,
|
|
177
236
|
title: issue.title,
|
|
178
237
|
url: issue.html_url,
|
|
@@ -184,7 +243,12 @@ export class GitHubIntegration {
|
|
|
184
243
|
updatedAt: issue.updated_at,
|
|
185
244
|
closedAt: issue.closed_at,
|
|
186
245
|
commentsCount: issue.comments,
|
|
246
|
+
milestone: issue.milestone
|
|
247
|
+
? { number: issue.milestone.number, title: issue.milestone.title }
|
|
248
|
+
: null,
|
|
187
249
|
};
|
|
250
|
+
this.setCache(cacheKey, details);
|
|
251
|
+
return details;
|
|
188
252
|
}
|
|
189
253
|
catch (error) {
|
|
190
254
|
logger.error('Failed to get issue details', {
|
|
@@ -198,7 +262,7 @@ export class GitHubIntegration {
|
|
|
198
262
|
/**
|
|
199
263
|
* Create a new GitHub issue
|
|
200
264
|
*/
|
|
201
|
-
async createIssue(owner, repo, title, body, labels, assignees) {
|
|
265
|
+
async createIssue(owner, repo, title, body, labels, assignees, milestone) {
|
|
202
266
|
if (!this.octokit) {
|
|
203
267
|
logger.error('Cannot create issue: GitHub API not available', { module: 'GitHub' });
|
|
204
268
|
return null;
|
|
@@ -211,6 +275,7 @@ export class GitHubIntegration {
|
|
|
211
275
|
body,
|
|
212
276
|
labels,
|
|
213
277
|
assignees,
|
|
278
|
+
milestone,
|
|
214
279
|
});
|
|
215
280
|
logger.info('Created GitHub issue', {
|
|
216
281
|
module: 'GitHub',
|
|
@@ -232,6 +297,10 @@ export class GitHubIntegration {
|
|
|
232
297
|
});
|
|
233
298
|
return null;
|
|
234
299
|
}
|
|
300
|
+
finally {
|
|
301
|
+
this.invalidateCache(`issues:${owner}:${repo}`);
|
|
302
|
+
this.invalidateCache('context:');
|
|
303
|
+
}
|
|
235
304
|
}
|
|
236
305
|
/**
|
|
237
306
|
* Close a GitHub issue with optional comment
|
|
@@ -276,6 +345,11 @@ export class GitHubIntegration {
|
|
|
276
345
|
});
|
|
277
346
|
return null;
|
|
278
347
|
}
|
|
348
|
+
finally {
|
|
349
|
+
this.invalidateCache(`issues:${owner}:${repo}`);
|
|
350
|
+
this.invalidateCache(`issue:${owner}:${repo}:${String(issueNumber)}`);
|
|
351
|
+
this.invalidateCache('context:');
|
|
352
|
+
}
|
|
279
353
|
}
|
|
280
354
|
/**
|
|
281
355
|
* Get repository pull requests
|
|
@@ -284,6 +358,10 @@ export class GitHubIntegration {
|
|
|
284
358
|
if (!this.octokit) {
|
|
285
359
|
return [];
|
|
286
360
|
}
|
|
361
|
+
const cacheKey = `prs:${owner}:${repo}:${state}:${String(limit)}`;
|
|
362
|
+
const cached = this.getCached(cacheKey);
|
|
363
|
+
if (cached)
|
|
364
|
+
return cached;
|
|
287
365
|
try {
|
|
288
366
|
const response = await this.octokit.pulls.list({
|
|
289
367
|
owner,
|
|
@@ -293,12 +371,18 @@ export class GitHubIntegration {
|
|
|
293
371
|
sort: 'updated',
|
|
294
372
|
direction: 'desc',
|
|
295
373
|
});
|
|
296
|
-
|
|
374
|
+
const result = response.data.map((pr) => ({
|
|
297
375
|
number: pr.number,
|
|
298
376
|
title: pr.title,
|
|
299
377
|
url: pr.html_url,
|
|
300
|
-
state: pr.merged_at
|
|
378
|
+
state: pr.merged_at
|
|
379
|
+
? 'MERGED'
|
|
380
|
+
: pr.state === 'open'
|
|
381
|
+
? 'OPEN'
|
|
382
|
+
: 'CLOSED',
|
|
301
383
|
}));
|
|
384
|
+
this.setCache(cacheKey, result);
|
|
385
|
+
return result;
|
|
302
386
|
}
|
|
303
387
|
catch (error) {
|
|
304
388
|
logger.error('Failed to get pull requests', {
|
|
@@ -315,6 +399,10 @@ export class GitHubIntegration {
|
|
|
315
399
|
if (!this.octokit) {
|
|
316
400
|
return null;
|
|
317
401
|
}
|
|
402
|
+
const cacheKey = `pr:${owner}:${repo}:${String(prNumber)}`;
|
|
403
|
+
const cached = this.getCached(cacheKey);
|
|
404
|
+
if (cached !== undefined)
|
|
405
|
+
return cached;
|
|
318
406
|
try {
|
|
319
407
|
const response = await this.octokit.pulls.get({
|
|
320
408
|
owner,
|
|
@@ -322,7 +410,7 @@ export class GitHubIntegration {
|
|
|
322
410
|
pull_number: prNumber,
|
|
323
411
|
});
|
|
324
412
|
const pr = response.data;
|
|
325
|
-
|
|
413
|
+
const details = {
|
|
326
414
|
number: pr.number,
|
|
327
415
|
title: pr.title,
|
|
328
416
|
url: pr.html_url,
|
|
@@ -340,6 +428,8 @@ export class GitHubIntegration {
|
|
|
340
428
|
deletions: pr.deletions,
|
|
341
429
|
changedFiles: pr.changed_files,
|
|
342
430
|
};
|
|
431
|
+
this.setCache(cacheKey, details);
|
|
432
|
+
return details;
|
|
343
433
|
}
|
|
344
434
|
catch (error) {
|
|
345
435
|
logger.error('Failed to get PR details', {
|
|
@@ -358,13 +448,17 @@ export class GitHubIntegration {
|
|
|
358
448
|
logger.debug('GitHub API not available - no token', { module: 'GitHub' });
|
|
359
449
|
return [];
|
|
360
450
|
}
|
|
451
|
+
const cacheKey = `workflows:${owner}:${repo}:${String(limit)}`;
|
|
452
|
+
const cached = this.getCached(cacheKey);
|
|
453
|
+
if (cached)
|
|
454
|
+
return cached;
|
|
361
455
|
try {
|
|
362
456
|
const response = await this.octokit.rest.actions.listWorkflowRunsForRepo({
|
|
363
457
|
owner,
|
|
364
458
|
repo,
|
|
365
459
|
per_page: limit,
|
|
366
460
|
});
|
|
367
|
-
|
|
461
|
+
const result = response.data.workflow_runs.map((run) => ({
|
|
368
462
|
id: run.id,
|
|
369
463
|
name: run.name ?? 'Unknown Workflow',
|
|
370
464
|
status: run.status,
|
|
@@ -375,6 +469,8 @@ export class GitHubIntegration {
|
|
|
375
469
|
createdAt: run.created_at,
|
|
376
470
|
updatedAt: run.updated_at,
|
|
377
471
|
}));
|
|
472
|
+
this.setCache(cacheKey, result);
|
|
473
|
+
return result;
|
|
378
474
|
}
|
|
379
475
|
catch (error) {
|
|
380
476
|
logger.error('Failed to get workflow runs', {
|
|
@@ -388,6 +484,9 @@ export class GitHubIntegration {
|
|
|
388
484
|
* Get full repository context (issues, PRs, branch info)
|
|
389
485
|
*/
|
|
390
486
|
async getRepoContext() {
|
|
487
|
+
const cached = this.getCached('context:repo');
|
|
488
|
+
if (cached)
|
|
489
|
+
return cached;
|
|
391
490
|
const repoInfo = await this.getRepoInfo();
|
|
392
491
|
const context = {
|
|
393
492
|
repoName: repoInfo.repo,
|
|
@@ -398,6 +497,7 @@ export class GitHubIntegration {
|
|
|
398
497
|
issues: [],
|
|
399
498
|
pullRequests: [],
|
|
400
499
|
workflowRuns: [],
|
|
500
|
+
milestones: [],
|
|
401
501
|
};
|
|
402
502
|
// Get current commit
|
|
403
503
|
try {
|
|
@@ -412,7 +512,9 @@ export class GitHubIntegration {
|
|
|
412
512
|
context.issues = await this.getIssues(repoInfo.owner, repoInfo.repo, 'open', 10);
|
|
413
513
|
context.pullRequests = await this.getPullRequests(repoInfo.owner, repoInfo.repo, 'open', 10);
|
|
414
514
|
context.workflowRuns = await this.getWorkflowRuns(repoInfo.owner, repoInfo.repo, 10);
|
|
515
|
+
context.milestones = await this.getMilestones(repoInfo.owner, repoInfo.repo, 'open', 10);
|
|
415
516
|
}
|
|
517
|
+
this.setCache('context:repo', context);
|
|
416
518
|
return context;
|
|
417
519
|
}
|
|
418
520
|
// ==========================================================================
|
|
@@ -712,6 +814,9 @@ export class GitHubIntegration {
|
|
|
712
814
|
});
|
|
713
815
|
return { success: false, error: errorMessage };
|
|
714
816
|
}
|
|
817
|
+
finally {
|
|
818
|
+
this.invalidateCache('kanban:');
|
|
819
|
+
}
|
|
715
820
|
}
|
|
716
821
|
/**
|
|
717
822
|
* Add an item to a GitHub Project v2
|
|
@@ -750,6 +855,402 @@ export class GitHubIntegration {
|
|
|
750
855
|
});
|
|
751
856
|
return { success: false, error: errorMessage };
|
|
752
857
|
}
|
|
858
|
+
finally {
|
|
859
|
+
this.invalidateCache('kanban:');
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
// ==========================================================================
|
|
863
|
+
// GitHub Milestones Methods
|
|
864
|
+
// ==========================================================================
|
|
865
|
+
/**
|
|
866
|
+
* List milestones for a repository
|
|
867
|
+
*/
|
|
868
|
+
async getMilestones(owner, repo, state = 'open', limit = 20) {
|
|
869
|
+
if (!this.octokit) {
|
|
870
|
+
return [];
|
|
871
|
+
}
|
|
872
|
+
const cacheKey = `milestones:${owner}:${repo}:${state}:${String(limit)}`;
|
|
873
|
+
const cached = this.getCached(cacheKey);
|
|
874
|
+
if (cached)
|
|
875
|
+
return cached;
|
|
876
|
+
try {
|
|
877
|
+
// GitHub REST API uses 'open' | 'closed' | 'all' for milestone state
|
|
878
|
+
const apiState = state === 'all' ? undefined : state;
|
|
879
|
+
const response = await this.octokit.issues.listMilestones({
|
|
880
|
+
owner,
|
|
881
|
+
repo,
|
|
882
|
+
state: apiState,
|
|
883
|
+
per_page: limit,
|
|
884
|
+
sort: 'due_on',
|
|
885
|
+
direction: 'asc',
|
|
886
|
+
});
|
|
887
|
+
const result = response.data.map((ms) => ({
|
|
888
|
+
number: ms.number,
|
|
889
|
+
title: ms.title,
|
|
890
|
+
description: ms.description ?? null,
|
|
891
|
+
state: ms.state === 'open' ? 'open' : 'closed',
|
|
892
|
+
url: ms.html_url,
|
|
893
|
+
dueOn: ms.due_on ?? null,
|
|
894
|
+
openIssues: ms.open_issues,
|
|
895
|
+
closedIssues: ms.closed_issues,
|
|
896
|
+
createdAt: ms.created_at,
|
|
897
|
+
updatedAt: ms.updated_at,
|
|
898
|
+
creator: ms.creator?.login ?? null,
|
|
899
|
+
}));
|
|
900
|
+
this.setCache(cacheKey, result);
|
|
901
|
+
return result;
|
|
902
|
+
}
|
|
903
|
+
catch (error) {
|
|
904
|
+
logger.error('Failed to get milestones', {
|
|
905
|
+
module: 'GitHub',
|
|
906
|
+
error: error instanceof Error ? error.message : String(error),
|
|
907
|
+
});
|
|
908
|
+
return [];
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
/**
|
|
912
|
+
* Get a single milestone by number
|
|
913
|
+
*/
|
|
914
|
+
async getMilestone(owner, repo, milestoneNumber) {
|
|
915
|
+
if (!this.octokit) {
|
|
916
|
+
return null;
|
|
917
|
+
}
|
|
918
|
+
const cacheKey = `milestone:${owner}:${repo}:${String(milestoneNumber)}`;
|
|
919
|
+
const cached = this.getCached(cacheKey);
|
|
920
|
+
if (cached !== undefined)
|
|
921
|
+
return cached;
|
|
922
|
+
try {
|
|
923
|
+
const response = await this.octokit.issues.getMilestone({
|
|
924
|
+
owner,
|
|
925
|
+
repo,
|
|
926
|
+
milestone_number: milestoneNumber,
|
|
927
|
+
});
|
|
928
|
+
const ms = response.data;
|
|
929
|
+
const milestone = {
|
|
930
|
+
number: ms.number,
|
|
931
|
+
title: ms.title,
|
|
932
|
+
description: ms.description ?? null,
|
|
933
|
+
state: ms.state === 'open' ? 'open' : 'closed',
|
|
934
|
+
url: ms.html_url,
|
|
935
|
+
dueOn: ms.due_on ?? null,
|
|
936
|
+
openIssues: ms.open_issues,
|
|
937
|
+
closedIssues: ms.closed_issues,
|
|
938
|
+
createdAt: ms.created_at,
|
|
939
|
+
updatedAt: ms.updated_at,
|
|
940
|
+
creator: ms.creator?.login ?? null,
|
|
941
|
+
};
|
|
942
|
+
this.setCache(cacheKey, milestone);
|
|
943
|
+
return milestone;
|
|
944
|
+
}
|
|
945
|
+
catch (error) {
|
|
946
|
+
logger.error('Failed to get milestone', {
|
|
947
|
+
module: 'GitHub',
|
|
948
|
+
entityId: milestoneNumber,
|
|
949
|
+
error: error instanceof Error ? error.message : String(error),
|
|
950
|
+
});
|
|
951
|
+
return null;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* Create a new milestone
|
|
956
|
+
*/
|
|
957
|
+
async createMilestone(owner, repo, title, description, dueOn) {
|
|
958
|
+
if (!this.octokit) {
|
|
959
|
+
logger.error('Cannot create milestone: GitHub API not available', {
|
|
960
|
+
module: 'GitHub',
|
|
961
|
+
});
|
|
962
|
+
return null;
|
|
963
|
+
}
|
|
964
|
+
try {
|
|
965
|
+
const response = await this.octokit.issues.createMilestone({
|
|
966
|
+
owner,
|
|
967
|
+
repo,
|
|
968
|
+
title,
|
|
969
|
+
description,
|
|
970
|
+
due_on: dueOn,
|
|
971
|
+
});
|
|
972
|
+
const ms = response.data;
|
|
973
|
+
logger.info('Created GitHub milestone', {
|
|
974
|
+
module: 'GitHub',
|
|
975
|
+
entityId: ms.number,
|
|
976
|
+
context: { title, owner, repo },
|
|
977
|
+
});
|
|
978
|
+
return {
|
|
979
|
+
number: ms.number,
|
|
980
|
+
title: ms.title,
|
|
981
|
+
description: ms.description ?? null,
|
|
982
|
+
state: ms.state === 'open' ? 'open' : 'closed',
|
|
983
|
+
url: ms.html_url,
|
|
984
|
+
dueOn: ms.due_on ?? null,
|
|
985
|
+
openIssues: ms.open_issues,
|
|
986
|
+
closedIssues: ms.closed_issues,
|
|
987
|
+
createdAt: ms.created_at,
|
|
988
|
+
updatedAt: ms.updated_at,
|
|
989
|
+
creator: ms.creator?.login ?? null,
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
catch (error) {
|
|
993
|
+
logger.error('Failed to create milestone', {
|
|
994
|
+
module: 'GitHub',
|
|
995
|
+
error: error instanceof Error ? error.message : String(error),
|
|
996
|
+
context: { title, owner, repo },
|
|
997
|
+
});
|
|
998
|
+
return null;
|
|
999
|
+
}
|
|
1000
|
+
finally {
|
|
1001
|
+
this.invalidateCache(`milestones:${owner}:${repo}`);
|
|
1002
|
+
this.invalidateCache('context:');
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
/**
|
|
1006
|
+
* Update an existing milestone
|
|
1007
|
+
*/
|
|
1008
|
+
async updateMilestone(owner, repo, milestoneNumber, updates) {
|
|
1009
|
+
if (!this.octokit) {
|
|
1010
|
+
logger.error('Cannot update milestone: GitHub API not available', {
|
|
1011
|
+
module: 'GitHub',
|
|
1012
|
+
});
|
|
1013
|
+
return null;
|
|
1014
|
+
}
|
|
1015
|
+
try {
|
|
1016
|
+
const response = await this.octokit.issues.updateMilestone({
|
|
1017
|
+
owner,
|
|
1018
|
+
repo,
|
|
1019
|
+
milestone_number: milestoneNumber,
|
|
1020
|
+
title: updates.title,
|
|
1021
|
+
description: updates.description,
|
|
1022
|
+
due_on: updates.dueOn === null ? undefined : updates.dueOn,
|
|
1023
|
+
state: updates.state,
|
|
1024
|
+
});
|
|
1025
|
+
const ms = response.data;
|
|
1026
|
+
logger.info('Updated GitHub milestone', {
|
|
1027
|
+
module: 'GitHub',
|
|
1028
|
+
entityId: milestoneNumber,
|
|
1029
|
+
context: { owner, repo, updates: Object.keys(updates) },
|
|
1030
|
+
});
|
|
1031
|
+
return {
|
|
1032
|
+
number: ms.number,
|
|
1033
|
+
title: ms.title,
|
|
1034
|
+
description: ms.description ?? null,
|
|
1035
|
+
state: ms.state === 'open' ? 'open' : 'closed',
|
|
1036
|
+
url: ms.html_url,
|
|
1037
|
+
dueOn: ms.due_on ?? null,
|
|
1038
|
+
openIssues: ms.open_issues,
|
|
1039
|
+
closedIssues: ms.closed_issues,
|
|
1040
|
+
createdAt: ms.created_at,
|
|
1041
|
+
updatedAt: ms.updated_at,
|
|
1042
|
+
creator: ms.creator?.login ?? null,
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
catch (error) {
|
|
1046
|
+
logger.error('Failed to update milestone', {
|
|
1047
|
+
module: 'GitHub',
|
|
1048
|
+
entityId: milestoneNumber,
|
|
1049
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1050
|
+
});
|
|
1051
|
+
return null;
|
|
1052
|
+
}
|
|
1053
|
+
finally {
|
|
1054
|
+
this.invalidateCache(`milestones:${owner}:${repo}`);
|
|
1055
|
+
this.invalidateCache(`milestone:${owner}:${repo}:${String(milestoneNumber)}`);
|
|
1056
|
+
this.invalidateCache('context:');
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
/**
|
|
1060
|
+
* Delete a milestone
|
|
1061
|
+
*/
|
|
1062
|
+
async deleteMilestone(owner, repo, milestoneNumber) {
|
|
1063
|
+
if (!this.octokit) {
|
|
1064
|
+
return { success: false, error: 'GitHub API not available' };
|
|
1065
|
+
}
|
|
1066
|
+
try {
|
|
1067
|
+
await this.octokit.issues.deleteMilestone({
|
|
1068
|
+
owner,
|
|
1069
|
+
repo,
|
|
1070
|
+
milestone_number: milestoneNumber,
|
|
1071
|
+
});
|
|
1072
|
+
logger.info('Deleted GitHub milestone', {
|
|
1073
|
+
module: 'GitHub',
|
|
1074
|
+
entityId: milestoneNumber,
|
|
1075
|
+
context: { owner, repo },
|
|
1076
|
+
});
|
|
1077
|
+
return { success: true };
|
|
1078
|
+
}
|
|
1079
|
+
catch (error) {
|
|
1080
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1081
|
+
logger.error('Failed to delete milestone', {
|
|
1082
|
+
module: 'GitHub',
|
|
1083
|
+
entityId: milestoneNumber,
|
|
1084
|
+
error: errorMessage,
|
|
1085
|
+
});
|
|
1086
|
+
return { success: false, error: errorMessage };
|
|
1087
|
+
}
|
|
1088
|
+
finally {
|
|
1089
|
+
this.invalidateCache(`milestones:${owner}:${repo}`);
|
|
1090
|
+
this.invalidateCache(`milestone:${owner}:${repo}:${String(milestoneNumber)}`);
|
|
1091
|
+
this.invalidateCache('context:');
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
// ==========================================================================
|
|
1095
|
+
// Repository Insights/Traffic Methods
|
|
1096
|
+
// ==========================================================================
|
|
1097
|
+
/**
|
|
1098
|
+
* Get a cached value with a custom TTL.
|
|
1099
|
+
* Used for traffic endpoints which change slowly (10-min TTL).
|
|
1100
|
+
*/
|
|
1101
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -- T is needed at call sites for type-safe casts
|
|
1102
|
+
getCachedWithTtl(key, ttlMs) {
|
|
1103
|
+
const entry = this.apiCache.get(key);
|
|
1104
|
+
if (entry && Date.now() - entry.timestamp < ttlMs) {
|
|
1105
|
+
return entry.data;
|
|
1106
|
+
}
|
|
1107
|
+
if (entry) {
|
|
1108
|
+
this.apiCache.delete(key);
|
|
1109
|
+
}
|
|
1110
|
+
return undefined;
|
|
1111
|
+
}
|
|
1112
|
+
/**
|
|
1113
|
+
* Get repository statistics (stars, forks, watchers).
|
|
1114
|
+
* Uses a single GET /repos/{owner}/{repo} API call.
|
|
1115
|
+
*/
|
|
1116
|
+
async getRepoStats(owner, repo) {
|
|
1117
|
+
if (!this.octokit) {
|
|
1118
|
+
return null;
|
|
1119
|
+
}
|
|
1120
|
+
const cacheKey = `repostats:${owner}:${repo}`;
|
|
1121
|
+
const cached = this.getCachedWithTtl(cacheKey, TRAFFIC_CACHE_TTL_MS);
|
|
1122
|
+
if (cached)
|
|
1123
|
+
return cached;
|
|
1124
|
+
try {
|
|
1125
|
+
const response = await this.octokit.repos.get({ owner, repo });
|
|
1126
|
+
const data = response.data;
|
|
1127
|
+
const result = {
|
|
1128
|
+
stars: data.stargazers_count,
|
|
1129
|
+
forks: data.forks_count,
|
|
1130
|
+
watchers: data.subscribers_count,
|
|
1131
|
+
openIssues: data.open_issues_count,
|
|
1132
|
+
size: data.size,
|
|
1133
|
+
defaultBranch: data.default_branch,
|
|
1134
|
+
};
|
|
1135
|
+
this.setCache(cacheKey, result);
|
|
1136
|
+
return result;
|
|
1137
|
+
}
|
|
1138
|
+
catch (error) {
|
|
1139
|
+
logger.error('Failed to get repo stats', {
|
|
1140
|
+
module: 'GitHub',
|
|
1141
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1142
|
+
context: { owner, repo },
|
|
1143
|
+
});
|
|
1144
|
+
return null;
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
/**
|
|
1148
|
+
* Get aggregated traffic data (14-day rolling clones + views).
|
|
1149
|
+
* Combines GET /repos/{owner}/{repo}/traffic/clones and /traffic/views.
|
|
1150
|
+
* Requires push access to the repository.
|
|
1151
|
+
*/
|
|
1152
|
+
async getTrafficData(owner, repo) {
|
|
1153
|
+
if (!this.octokit) {
|
|
1154
|
+
return null;
|
|
1155
|
+
}
|
|
1156
|
+
const cacheKey = `traffic:${owner}:${repo}`;
|
|
1157
|
+
const cached = this.getCachedWithTtl(cacheKey, TRAFFIC_CACHE_TTL_MS);
|
|
1158
|
+
if (cached)
|
|
1159
|
+
return cached;
|
|
1160
|
+
try {
|
|
1161
|
+
const [clonesRes, viewsRes] = await Promise.all([
|
|
1162
|
+
this.octokit.rest.repos.getClones({ owner, repo }),
|
|
1163
|
+
this.octokit.rest.repos.getViews({ owner, repo }),
|
|
1164
|
+
]);
|
|
1165
|
+
const clonesDays = clonesRes.data.clones?.length ?? 0;
|
|
1166
|
+
const viewsDays = viewsRes.data.views?.length ?? 0;
|
|
1167
|
+
const result = {
|
|
1168
|
+
clones: {
|
|
1169
|
+
total: clonesRes.data.count,
|
|
1170
|
+
unique: clonesRes.data.uniques,
|
|
1171
|
+
dailyAvg: clonesDays > 0 ? Math.round(clonesRes.data.count / clonesDays) : 0,
|
|
1172
|
+
},
|
|
1173
|
+
views: {
|
|
1174
|
+
total: viewsRes.data.count,
|
|
1175
|
+
unique: viewsRes.data.uniques,
|
|
1176
|
+
dailyAvg: viewsDays > 0 ? Math.round(viewsRes.data.count / viewsDays) : 0,
|
|
1177
|
+
},
|
|
1178
|
+
period: '14 days',
|
|
1179
|
+
};
|
|
1180
|
+
this.setCache(cacheKey, result);
|
|
1181
|
+
return result;
|
|
1182
|
+
}
|
|
1183
|
+
catch (error) {
|
|
1184
|
+
logger.error('Failed to get traffic data', {
|
|
1185
|
+
module: 'GitHub',
|
|
1186
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1187
|
+
context: { owner, repo },
|
|
1188
|
+
});
|
|
1189
|
+
return null;
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
/**
|
|
1193
|
+
* Get top referrer sources for the repository (14-day rolling).
|
|
1194
|
+
* Requires push access to the repository.
|
|
1195
|
+
*/
|
|
1196
|
+
async getTopReferrers(owner, repo, limit = 5) {
|
|
1197
|
+
if (!this.octokit) {
|
|
1198
|
+
return [];
|
|
1199
|
+
}
|
|
1200
|
+
const cacheKey = `referrers:${owner}:${repo}`;
|
|
1201
|
+
const cached = this.getCachedWithTtl(cacheKey, TRAFFIC_CACHE_TTL_MS);
|
|
1202
|
+
if (cached)
|
|
1203
|
+
return cached.slice(0, limit);
|
|
1204
|
+
try {
|
|
1205
|
+
const response = await this.octokit.rest.repos.getTopReferrers({ owner, repo });
|
|
1206
|
+
const result = response.data.map((r) => ({
|
|
1207
|
+
referrer: r.referrer,
|
|
1208
|
+
count: r.count,
|
|
1209
|
+
uniques: r.uniques,
|
|
1210
|
+
}));
|
|
1211
|
+
this.setCache(cacheKey, result);
|
|
1212
|
+
return result.slice(0, limit);
|
|
1213
|
+
}
|
|
1214
|
+
catch (error) {
|
|
1215
|
+
logger.error('Failed to get top referrers', {
|
|
1216
|
+
module: 'GitHub',
|
|
1217
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1218
|
+
context: { owner, repo },
|
|
1219
|
+
});
|
|
1220
|
+
return [];
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
/**
|
|
1224
|
+
* Get popular repository paths (14-day rolling).
|
|
1225
|
+
* Requires push access to the repository.
|
|
1226
|
+
*/
|
|
1227
|
+
async getPopularPaths(owner, repo, limit = 5) {
|
|
1228
|
+
if (!this.octokit) {
|
|
1229
|
+
return [];
|
|
1230
|
+
}
|
|
1231
|
+
const cacheKey = `paths:${owner}:${repo}`;
|
|
1232
|
+
const cached = this.getCachedWithTtl(cacheKey, TRAFFIC_CACHE_TTL_MS);
|
|
1233
|
+
if (cached)
|
|
1234
|
+
return cached.slice(0, limit);
|
|
1235
|
+
try {
|
|
1236
|
+
const response = await this.octokit.rest.repos.getTopPaths({ owner, repo });
|
|
1237
|
+
const result = response.data.map((p) => ({
|
|
1238
|
+
path: p.path,
|
|
1239
|
+
title: p.title,
|
|
1240
|
+
count: p.count,
|
|
1241
|
+
uniques: p.uniques,
|
|
1242
|
+
}));
|
|
1243
|
+
this.setCache(cacheKey, result);
|
|
1244
|
+
return result.slice(0, limit);
|
|
1245
|
+
}
|
|
1246
|
+
catch (error) {
|
|
1247
|
+
logger.error('Failed to get popular paths', {
|
|
1248
|
+
module: 'GitHub',
|
|
1249
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1250
|
+
context: { owner, repo },
|
|
1251
|
+
});
|
|
1252
|
+
return [];
|
|
1253
|
+
}
|
|
753
1254
|
}
|
|
754
1255
|
}
|
|
755
1256
|
//# sourceMappingURL=GitHubIntegration.js.map
|