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
|
@@ -11,6 +11,7 @@ import * as simpleGitImport from 'simple-git'
|
|
|
11
11
|
import { logger } from '../utils/logger.js'
|
|
12
12
|
import type {
|
|
13
13
|
GitHubIssue,
|
|
14
|
+
GitHubMilestone,
|
|
14
15
|
GitHubPullRequest,
|
|
15
16
|
GitHubWorkflowRun,
|
|
16
17
|
ProjectContext,
|
|
@@ -18,8 +19,24 @@ import type {
|
|
|
18
19
|
KanbanColumn,
|
|
19
20
|
ProjectV2Item,
|
|
20
21
|
ProjectV2StatusOption,
|
|
22
|
+
RepoStats,
|
|
23
|
+
TrafficData,
|
|
24
|
+
TrafficReferrer,
|
|
25
|
+
PopularPath,
|
|
21
26
|
} from '../types/index.js'
|
|
22
27
|
|
|
28
|
+
/** TTL for cached GitHub API responses (5 minutes) */
|
|
29
|
+
const CACHE_TTL_MS = 5 * 60 * 1000
|
|
30
|
+
|
|
31
|
+
/** TTL for cached traffic/insights responses (10 minutes - traffic data changes slowly) */
|
|
32
|
+
const TRAFFIC_CACHE_TTL_MS = 10 * 60 * 1000
|
|
33
|
+
|
|
34
|
+
/** Generic cache entry with timestamp */
|
|
35
|
+
interface CacheEntry<T> {
|
|
36
|
+
data: T
|
|
37
|
+
timestamp: number
|
|
38
|
+
}
|
|
39
|
+
|
|
23
40
|
// Handle simpleGit ESM/CJS interop
|
|
24
41
|
type SimpleGitType = typeof simpleGitImport.simpleGit
|
|
25
42
|
const simpleGit: SimpleGitType = simpleGitImport.simpleGit
|
|
@@ -75,6 +92,9 @@ export class GitHubIntegration {
|
|
|
75
92
|
private readonly token: string | undefined
|
|
76
93
|
private cachedRepoInfo: RepoInfo | null = null
|
|
77
94
|
|
|
95
|
+
/** TTL response cache for GitHub API read methods */
|
|
96
|
+
private readonly apiCache = new Map<string, CacheEntry<unknown>>()
|
|
97
|
+
|
|
78
98
|
constructor(workingDir = '.') {
|
|
79
99
|
this.token = process.env['GITHUB_TOKEN']
|
|
80
100
|
|
|
@@ -116,6 +136,47 @@ export class GitHubIntegration {
|
|
|
116
136
|
return this.octokit !== null
|
|
117
137
|
}
|
|
118
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Get a cached value if it exists and hasn't expired.
|
|
141
|
+
*/
|
|
142
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -- T is needed at call sites for type-safe casts
|
|
143
|
+
private getCached<T>(key: string): T | undefined {
|
|
144
|
+
const entry = this.apiCache.get(key)
|
|
145
|
+
if (entry && Date.now() - entry.timestamp < CACHE_TTL_MS) {
|
|
146
|
+
return entry.data as T
|
|
147
|
+
}
|
|
148
|
+
if (entry) {
|
|
149
|
+
this.apiCache.delete(key)
|
|
150
|
+
}
|
|
151
|
+
return undefined
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Store a value in the cache.
|
|
156
|
+
*/
|
|
157
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -- T preserves type inference at call sites
|
|
158
|
+
private setCache<T>(key: string, data: T): void {
|
|
159
|
+
this.apiCache.set(key, { data, timestamp: Date.now() })
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Invalidate all cache entries matching a prefix.
|
|
164
|
+
*/
|
|
165
|
+
private invalidateCache(prefix: string): void {
|
|
166
|
+
for (const key of this.apiCache.keys()) {
|
|
167
|
+
if (key.startsWith(prefix)) {
|
|
168
|
+
this.apiCache.delete(key)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Clear all cached GitHub API responses.
|
|
175
|
+
*/
|
|
176
|
+
clearCache(): void {
|
|
177
|
+
this.apiCache.clear()
|
|
178
|
+
}
|
|
179
|
+
|
|
119
180
|
/**
|
|
120
181
|
* Get local repository information
|
|
121
182
|
* Caches the result for synchronous access via getCachedRepoInfo()
|
|
@@ -204,6 +265,10 @@ export class GitHubIntegration {
|
|
|
204
265
|
return []
|
|
205
266
|
}
|
|
206
267
|
|
|
268
|
+
const cacheKey = `issues:${owner}:${repo}:${state}:${String(limit)}`
|
|
269
|
+
const cached = this.getCached<GitHubIssue[]>(cacheKey)
|
|
270
|
+
if (cached) return cached
|
|
271
|
+
|
|
207
272
|
try {
|
|
208
273
|
const response = await this.octokit.issues.listForRepo({
|
|
209
274
|
owner,
|
|
@@ -215,14 +280,23 @@ export class GitHubIntegration {
|
|
|
215
280
|
})
|
|
216
281
|
|
|
217
282
|
// Filter out pull requests (GitHub API includes PRs in issues)
|
|
218
|
-
|
|
283
|
+
const result = response.data
|
|
219
284
|
.filter((issue) => !issue.pull_request)
|
|
220
285
|
.map((issue) => ({
|
|
221
286
|
number: issue.number,
|
|
222
287
|
title: issue.title,
|
|
223
288
|
url: issue.html_url,
|
|
224
|
-
state: issue.state === 'open' ? 'OPEN' : 'CLOSED',
|
|
289
|
+
state: issue.state === 'open' ? ('OPEN' as const) : ('CLOSED' as const),
|
|
290
|
+
milestone: issue.milestone
|
|
291
|
+
? {
|
|
292
|
+
number: issue.milestone.number,
|
|
293
|
+
title: issue.milestone.title,
|
|
294
|
+
}
|
|
295
|
+
: null,
|
|
225
296
|
}))
|
|
297
|
+
|
|
298
|
+
this.setCache(cacheKey, result)
|
|
299
|
+
return result
|
|
226
300
|
} catch (error) {
|
|
227
301
|
logger.error('Failed to get issues', {
|
|
228
302
|
module: 'GitHub',
|
|
@@ -240,6 +314,10 @@ export class GitHubIntegration {
|
|
|
240
314
|
return null
|
|
241
315
|
}
|
|
242
316
|
|
|
317
|
+
const cacheKey = `issue:${owner}:${repo}:${String(issueNumber)}`
|
|
318
|
+
const cached = this.getCached<IssueDetails | null>(cacheKey)
|
|
319
|
+
if (cached !== undefined) return cached
|
|
320
|
+
|
|
243
321
|
try {
|
|
244
322
|
const response = await this.octokit.issues.get({
|
|
245
323
|
owner,
|
|
@@ -254,7 +332,7 @@ export class GitHubIntegration {
|
|
|
254
332
|
return null
|
|
255
333
|
}
|
|
256
334
|
|
|
257
|
-
|
|
335
|
+
const details: IssueDetails = {
|
|
258
336
|
number: issue.number,
|
|
259
337
|
title: issue.title,
|
|
260
338
|
url: issue.html_url,
|
|
@@ -266,7 +344,13 @@ export class GitHubIntegration {
|
|
|
266
344
|
updatedAt: issue.updated_at,
|
|
267
345
|
closedAt: issue.closed_at,
|
|
268
346
|
commentsCount: issue.comments,
|
|
347
|
+
milestone: issue.milestone
|
|
348
|
+
? { number: issue.milestone.number, title: issue.milestone.title }
|
|
349
|
+
: null,
|
|
269
350
|
}
|
|
351
|
+
|
|
352
|
+
this.setCache(cacheKey, details)
|
|
353
|
+
return details
|
|
270
354
|
} catch (error) {
|
|
271
355
|
logger.error('Failed to get issue details', {
|
|
272
356
|
module: 'GitHub',
|
|
@@ -286,7 +370,8 @@ export class GitHubIntegration {
|
|
|
286
370
|
title: string,
|
|
287
371
|
body?: string,
|
|
288
372
|
labels?: string[],
|
|
289
|
-
assignees?: string[]
|
|
373
|
+
assignees?: string[],
|
|
374
|
+
milestone?: number
|
|
290
375
|
): Promise<{ number: number; url: string; title: string; nodeId: string } | null> {
|
|
291
376
|
if (!this.octokit) {
|
|
292
377
|
logger.error('Cannot create issue: GitHub API not available', { module: 'GitHub' })
|
|
@@ -301,6 +386,7 @@ export class GitHubIntegration {
|
|
|
301
386
|
body,
|
|
302
387
|
labels,
|
|
303
388
|
assignees,
|
|
389
|
+
milestone,
|
|
304
390
|
})
|
|
305
391
|
|
|
306
392
|
logger.info('Created GitHub issue', {
|
|
@@ -322,6 +408,9 @@ export class GitHubIntegration {
|
|
|
322
408
|
context: { title, owner, repo },
|
|
323
409
|
})
|
|
324
410
|
return null
|
|
411
|
+
} finally {
|
|
412
|
+
this.invalidateCache(`issues:${owner}:${repo}`)
|
|
413
|
+
this.invalidateCache('context:')
|
|
325
414
|
}
|
|
326
415
|
}
|
|
327
416
|
|
|
@@ -375,6 +464,10 @@ export class GitHubIntegration {
|
|
|
375
464
|
error: error instanceof Error ? error.message : String(error),
|
|
376
465
|
})
|
|
377
466
|
return null
|
|
467
|
+
} finally {
|
|
468
|
+
this.invalidateCache(`issues:${owner}:${repo}`)
|
|
469
|
+
this.invalidateCache(`issue:${owner}:${repo}:${String(issueNumber)}`)
|
|
470
|
+
this.invalidateCache('context:')
|
|
378
471
|
}
|
|
379
472
|
}
|
|
380
473
|
|
|
@@ -391,6 +484,10 @@ export class GitHubIntegration {
|
|
|
391
484
|
return []
|
|
392
485
|
}
|
|
393
486
|
|
|
487
|
+
const cacheKey = `prs:${owner}:${repo}:${state}:${String(limit)}`
|
|
488
|
+
const cached = this.getCached<GitHubPullRequest[]>(cacheKey)
|
|
489
|
+
if (cached) return cached
|
|
490
|
+
|
|
394
491
|
try {
|
|
395
492
|
const response = await this.octokit.pulls.list({
|
|
396
493
|
owner,
|
|
@@ -401,12 +498,19 @@ export class GitHubIntegration {
|
|
|
401
498
|
direction: 'desc',
|
|
402
499
|
})
|
|
403
500
|
|
|
404
|
-
|
|
501
|
+
const result = response.data.map((pr) => ({
|
|
405
502
|
number: pr.number,
|
|
406
503
|
title: pr.title,
|
|
407
504
|
url: pr.html_url,
|
|
408
|
-
state: pr.merged_at
|
|
505
|
+
state: pr.merged_at
|
|
506
|
+
? ('MERGED' as const)
|
|
507
|
+
: pr.state === 'open'
|
|
508
|
+
? ('OPEN' as const)
|
|
509
|
+
: ('CLOSED' as const),
|
|
409
510
|
}))
|
|
511
|
+
|
|
512
|
+
this.setCache(cacheKey, result)
|
|
513
|
+
return result
|
|
410
514
|
} catch (error) {
|
|
411
515
|
logger.error('Failed to get pull requests', {
|
|
412
516
|
module: 'GitHub',
|
|
@@ -428,6 +532,10 @@ export class GitHubIntegration {
|
|
|
428
532
|
return null
|
|
429
533
|
}
|
|
430
534
|
|
|
535
|
+
const cacheKey = `pr:${owner}:${repo}:${String(prNumber)}`
|
|
536
|
+
const cached = this.getCached<PullRequestDetails | null>(cacheKey)
|
|
537
|
+
if (cached !== undefined) return cached
|
|
538
|
+
|
|
431
539
|
try {
|
|
432
540
|
const response = await this.octokit.pulls.get({
|
|
433
541
|
owner,
|
|
@@ -437,7 +545,7 @@ export class GitHubIntegration {
|
|
|
437
545
|
|
|
438
546
|
const pr = response.data
|
|
439
547
|
|
|
440
|
-
|
|
548
|
+
const details: PullRequestDetails = {
|
|
441
549
|
number: pr.number,
|
|
442
550
|
title: pr.title,
|
|
443
551
|
url: pr.html_url,
|
|
@@ -455,6 +563,9 @@ export class GitHubIntegration {
|
|
|
455
563
|
deletions: pr.deletions,
|
|
456
564
|
changedFiles: pr.changed_files,
|
|
457
565
|
}
|
|
566
|
+
|
|
567
|
+
this.setCache(cacheKey, details)
|
|
568
|
+
return details
|
|
458
569
|
} catch (error) {
|
|
459
570
|
logger.error('Failed to get PR details', {
|
|
460
571
|
module: 'GitHub',
|
|
@@ -474,6 +585,10 @@ export class GitHubIntegration {
|
|
|
474
585
|
return []
|
|
475
586
|
}
|
|
476
587
|
|
|
588
|
+
const cacheKey = `workflows:${owner}:${repo}:${String(limit)}`
|
|
589
|
+
const cached = this.getCached<GitHubWorkflowRun[]>(cacheKey)
|
|
590
|
+
if (cached) return cached
|
|
591
|
+
|
|
477
592
|
try {
|
|
478
593
|
const response = await this.octokit.rest.actions.listWorkflowRunsForRepo({
|
|
479
594
|
owner,
|
|
@@ -481,7 +596,7 @@ export class GitHubIntegration {
|
|
|
481
596
|
per_page: limit,
|
|
482
597
|
})
|
|
483
598
|
|
|
484
|
-
|
|
599
|
+
const result = response.data.workflow_runs.map((run) => ({
|
|
485
600
|
id: run.id,
|
|
486
601
|
name: run.name ?? 'Unknown Workflow',
|
|
487
602
|
status: run.status as 'queued' | 'in_progress' | 'completed',
|
|
@@ -497,6 +612,9 @@ export class GitHubIntegration {
|
|
|
497
612
|
createdAt: run.created_at,
|
|
498
613
|
updatedAt: run.updated_at,
|
|
499
614
|
}))
|
|
615
|
+
|
|
616
|
+
this.setCache(cacheKey, result)
|
|
617
|
+
return result
|
|
500
618
|
} catch (error) {
|
|
501
619
|
logger.error('Failed to get workflow runs', {
|
|
502
620
|
module: 'GitHub',
|
|
@@ -510,6 +628,9 @@ export class GitHubIntegration {
|
|
|
510
628
|
* Get full repository context (issues, PRs, branch info)
|
|
511
629
|
*/
|
|
512
630
|
async getRepoContext(): Promise<ProjectContext> {
|
|
631
|
+
const cached = this.getCached<ProjectContext>('context:repo')
|
|
632
|
+
if (cached) return cached
|
|
633
|
+
|
|
513
634
|
const repoInfo = await this.getRepoInfo()
|
|
514
635
|
|
|
515
636
|
const context: ProjectContext = {
|
|
@@ -521,6 +642,7 @@ export class GitHubIntegration {
|
|
|
521
642
|
issues: [],
|
|
522
643
|
pullRequests: [],
|
|
523
644
|
workflowRuns: [],
|
|
645
|
+
milestones: [],
|
|
524
646
|
}
|
|
525
647
|
|
|
526
648
|
// Get current commit
|
|
@@ -541,8 +663,10 @@ export class GitHubIntegration {
|
|
|
541
663
|
10
|
|
542
664
|
)
|
|
543
665
|
context.workflowRuns = await this.getWorkflowRuns(repoInfo.owner, repoInfo.repo, 10)
|
|
666
|
+
context.milestones = await this.getMilestones(repoInfo.owner, repoInfo.repo, 'open', 10)
|
|
544
667
|
}
|
|
545
668
|
|
|
669
|
+
this.setCache('context:repo', context)
|
|
546
670
|
return context
|
|
547
671
|
}
|
|
548
672
|
|
|
@@ -923,6 +1047,8 @@ export class GitHubIntegration {
|
|
|
923
1047
|
error: errorMessage,
|
|
924
1048
|
})
|
|
925
1049
|
return { success: false, error: errorMessage }
|
|
1050
|
+
} finally {
|
|
1051
|
+
this.invalidateCache('kanban:')
|
|
926
1052
|
}
|
|
927
1053
|
}
|
|
928
1054
|
|
|
@@ -971,6 +1097,460 @@ export class GitHubIntegration {
|
|
|
971
1097
|
error: errorMessage,
|
|
972
1098
|
})
|
|
973
1099
|
return { success: false, error: errorMessage }
|
|
1100
|
+
} finally {
|
|
1101
|
+
this.invalidateCache('kanban:')
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
// ==========================================================================
|
|
1106
|
+
// GitHub Milestones Methods
|
|
1107
|
+
// ==========================================================================
|
|
1108
|
+
|
|
1109
|
+
/**
|
|
1110
|
+
* List milestones for a repository
|
|
1111
|
+
*/
|
|
1112
|
+
async getMilestones(
|
|
1113
|
+
owner: string,
|
|
1114
|
+
repo: string,
|
|
1115
|
+
state: 'open' | 'closed' | 'all' = 'open',
|
|
1116
|
+
limit = 20
|
|
1117
|
+
): Promise<GitHubMilestone[]> {
|
|
1118
|
+
if (!this.octokit) {
|
|
1119
|
+
return []
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
const cacheKey = `milestones:${owner}:${repo}:${state}:${String(limit)}`
|
|
1123
|
+
const cached = this.getCached<GitHubMilestone[]>(cacheKey)
|
|
1124
|
+
if (cached) return cached
|
|
1125
|
+
|
|
1126
|
+
try {
|
|
1127
|
+
// GitHub REST API uses 'open' | 'closed' | 'all' for milestone state
|
|
1128
|
+
const apiState = state === 'all' ? undefined : state
|
|
1129
|
+
const response = await this.octokit.issues.listMilestones({
|
|
1130
|
+
owner,
|
|
1131
|
+
repo,
|
|
1132
|
+
state: apiState,
|
|
1133
|
+
per_page: limit,
|
|
1134
|
+
sort: 'due_on',
|
|
1135
|
+
direction: 'asc',
|
|
1136
|
+
})
|
|
1137
|
+
|
|
1138
|
+
const result = response.data.map((ms) => ({
|
|
1139
|
+
number: ms.number,
|
|
1140
|
+
title: ms.title,
|
|
1141
|
+
description: ms.description ?? null,
|
|
1142
|
+
state: ms.state === 'open' ? ('open' as const) : ('closed' as const),
|
|
1143
|
+
url: ms.html_url,
|
|
1144
|
+
dueOn: ms.due_on ?? null,
|
|
1145
|
+
openIssues: ms.open_issues,
|
|
1146
|
+
closedIssues: ms.closed_issues,
|
|
1147
|
+
createdAt: ms.created_at,
|
|
1148
|
+
updatedAt: ms.updated_at,
|
|
1149
|
+
creator: ms.creator?.login ?? null,
|
|
1150
|
+
}))
|
|
1151
|
+
|
|
1152
|
+
this.setCache(cacheKey, result)
|
|
1153
|
+
return result
|
|
1154
|
+
} catch (error) {
|
|
1155
|
+
logger.error('Failed to get milestones', {
|
|
1156
|
+
module: 'GitHub',
|
|
1157
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1158
|
+
})
|
|
1159
|
+
return []
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
/**
|
|
1164
|
+
* Get a single milestone by number
|
|
1165
|
+
*/
|
|
1166
|
+
async getMilestone(
|
|
1167
|
+
owner: string,
|
|
1168
|
+
repo: string,
|
|
1169
|
+
milestoneNumber: number
|
|
1170
|
+
): Promise<GitHubMilestone | null> {
|
|
1171
|
+
if (!this.octokit) {
|
|
1172
|
+
return null
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
const cacheKey = `milestone:${owner}:${repo}:${String(milestoneNumber)}`
|
|
1176
|
+
const cached = this.getCached<GitHubMilestone | null>(cacheKey)
|
|
1177
|
+
if (cached !== undefined) return cached
|
|
1178
|
+
|
|
1179
|
+
try {
|
|
1180
|
+
const response = await this.octokit.issues.getMilestone({
|
|
1181
|
+
owner,
|
|
1182
|
+
repo,
|
|
1183
|
+
milestone_number: milestoneNumber,
|
|
1184
|
+
})
|
|
1185
|
+
|
|
1186
|
+
const ms = response.data
|
|
1187
|
+
const milestone: GitHubMilestone = {
|
|
1188
|
+
number: ms.number,
|
|
1189
|
+
title: ms.title,
|
|
1190
|
+
description: ms.description ?? null,
|
|
1191
|
+
state: ms.state === 'open' ? 'open' : 'closed',
|
|
1192
|
+
url: ms.html_url,
|
|
1193
|
+
dueOn: ms.due_on ?? null,
|
|
1194
|
+
openIssues: ms.open_issues,
|
|
1195
|
+
closedIssues: ms.closed_issues,
|
|
1196
|
+
createdAt: ms.created_at,
|
|
1197
|
+
updatedAt: ms.updated_at,
|
|
1198
|
+
creator: ms.creator?.login ?? null,
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
this.setCache(cacheKey, milestone)
|
|
1202
|
+
return milestone
|
|
1203
|
+
} catch (error) {
|
|
1204
|
+
logger.error('Failed to get milestone', {
|
|
1205
|
+
module: 'GitHub',
|
|
1206
|
+
entityId: milestoneNumber,
|
|
1207
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1208
|
+
})
|
|
1209
|
+
return null
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
/**
|
|
1214
|
+
* Create a new milestone
|
|
1215
|
+
*/
|
|
1216
|
+
async createMilestone(
|
|
1217
|
+
owner: string,
|
|
1218
|
+
repo: string,
|
|
1219
|
+
title: string,
|
|
1220
|
+
description?: string,
|
|
1221
|
+
dueOn?: string
|
|
1222
|
+
): Promise<GitHubMilestone | null> {
|
|
1223
|
+
if (!this.octokit) {
|
|
1224
|
+
logger.error('Cannot create milestone: GitHub API not available', {
|
|
1225
|
+
module: 'GitHub',
|
|
1226
|
+
})
|
|
1227
|
+
return null
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
try {
|
|
1231
|
+
const response = await this.octokit.issues.createMilestone({
|
|
1232
|
+
owner,
|
|
1233
|
+
repo,
|
|
1234
|
+
title,
|
|
1235
|
+
description,
|
|
1236
|
+
due_on: dueOn,
|
|
1237
|
+
})
|
|
1238
|
+
|
|
1239
|
+
const ms = response.data
|
|
1240
|
+
|
|
1241
|
+
logger.info('Created GitHub milestone', {
|
|
1242
|
+
module: 'GitHub',
|
|
1243
|
+
entityId: ms.number,
|
|
1244
|
+
context: { title, owner, repo },
|
|
1245
|
+
})
|
|
1246
|
+
|
|
1247
|
+
return {
|
|
1248
|
+
number: ms.number,
|
|
1249
|
+
title: ms.title,
|
|
1250
|
+
description: ms.description ?? null,
|
|
1251
|
+
state: ms.state === 'open' ? 'open' : 'closed',
|
|
1252
|
+
url: ms.html_url,
|
|
1253
|
+
dueOn: ms.due_on ?? null,
|
|
1254
|
+
openIssues: ms.open_issues,
|
|
1255
|
+
closedIssues: ms.closed_issues,
|
|
1256
|
+
createdAt: ms.created_at,
|
|
1257
|
+
updatedAt: ms.updated_at,
|
|
1258
|
+
creator: ms.creator?.login ?? null,
|
|
1259
|
+
}
|
|
1260
|
+
} catch (error) {
|
|
1261
|
+
logger.error('Failed to create milestone', {
|
|
1262
|
+
module: 'GitHub',
|
|
1263
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1264
|
+
context: { title, owner, repo },
|
|
1265
|
+
})
|
|
1266
|
+
return null
|
|
1267
|
+
} finally {
|
|
1268
|
+
this.invalidateCache(`milestones:${owner}:${repo}`)
|
|
1269
|
+
this.invalidateCache('context:')
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
/**
|
|
1274
|
+
* Update an existing milestone
|
|
1275
|
+
*/
|
|
1276
|
+
async updateMilestone(
|
|
1277
|
+
owner: string,
|
|
1278
|
+
repo: string,
|
|
1279
|
+
milestoneNumber: number,
|
|
1280
|
+
updates: {
|
|
1281
|
+
title?: string
|
|
1282
|
+
description?: string
|
|
1283
|
+
dueOn?: string | null
|
|
1284
|
+
state?: 'open' | 'closed'
|
|
1285
|
+
}
|
|
1286
|
+
): Promise<GitHubMilestone | null> {
|
|
1287
|
+
if (!this.octokit) {
|
|
1288
|
+
logger.error('Cannot update milestone: GitHub API not available', {
|
|
1289
|
+
module: 'GitHub',
|
|
1290
|
+
})
|
|
1291
|
+
return null
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
try {
|
|
1295
|
+
const response = await this.octokit.issues.updateMilestone({
|
|
1296
|
+
owner,
|
|
1297
|
+
repo,
|
|
1298
|
+
milestone_number: milestoneNumber,
|
|
1299
|
+
title: updates.title,
|
|
1300
|
+
description: updates.description,
|
|
1301
|
+
due_on: updates.dueOn === null ? undefined : updates.dueOn,
|
|
1302
|
+
state: updates.state,
|
|
1303
|
+
})
|
|
1304
|
+
|
|
1305
|
+
const ms = response.data
|
|
1306
|
+
|
|
1307
|
+
logger.info('Updated GitHub milestone', {
|
|
1308
|
+
module: 'GitHub',
|
|
1309
|
+
entityId: milestoneNumber,
|
|
1310
|
+
context: { owner, repo, updates: Object.keys(updates) },
|
|
1311
|
+
})
|
|
1312
|
+
|
|
1313
|
+
return {
|
|
1314
|
+
number: ms.number,
|
|
1315
|
+
title: ms.title,
|
|
1316
|
+
description: ms.description ?? null,
|
|
1317
|
+
state: ms.state === 'open' ? 'open' : 'closed',
|
|
1318
|
+
url: ms.html_url,
|
|
1319
|
+
dueOn: ms.due_on ?? null,
|
|
1320
|
+
openIssues: ms.open_issues,
|
|
1321
|
+
closedIssues: ms.closed_issues,
|
|
1322
|
+
createdAt: ms.created_at,
|
|
1323
|
+
updatedAt: ms.updated_at,
|
|
1324
|
+
creator: ms.creator?.login ?? null,
|
|
1325
|
+
}
|
|
1326
|
+
} catch (error) {
|
|
1327
|
+
logger.error('Failed to update milestone', {
|
|
1328
|
+
module: 'GitHub',
|
|
1329
|
+
entityId: milestoneNumber,
|
|
1330
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1331
|
+
})
|
|
1332
|
+
return null
|
|
1333
|
+
} finally {
|
|
1334
|
+
this.invalidateCache(`milestones:${owner}:${repo}`)
|
|
1335
|
+
this.invalidateCache(`milestone:${owner}:${repo}:${String(milestoneNumber)}`)
|
|
1336
|
+
this.invalidateCache('context:')
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
/**
|
|
1341
|
+
* Delete a milestone
|
|
1342
|
+
*/
|
|
1343
|
+
async deleteMilestone(
|
|
1344
|
+
owner: string,
|
|
1345
|
+
repo: string,
|
|
1346
|
+
milestoneNumber: number
|
|
1347
|
+
): Promise<{ success: boolean; error?: string }> {
|
|
1348
|
+
if (!this.octokit) {
|
|
1349
|
+
return { success: false, error: 'GitHub API not available' }
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
try {
|
|
1353
|
+
await this.octokit.issues.deleteMilestone({
|
|
1354
|
+
owner,
|
|
1355
|
+
repo,
|
|
1356
|
+
milestone_number: milestoneNumber,
|
|
1357
|
+
})
|
|
1358
|
+
|
|
1359
|
+
logger.info('Deleted GitHub milestone', {
|
|
1360
|
+
module: 'GitHub',
|
|
1361
|
+
entityId: milestoneNumber,
|
|
1362
|
+
context: { owner, repo },
|
|
1363
|
+
})
|
|
1364
|
+
|
|
1365
|
+
return { success: true }
|
|
1366
|
+
} catch (error) {
|
|
1367
|
+
const errorMessage = error instanceof Error ? error.message : String(error)
|
|
1368
|
+
logger.error('Failed to delete milestone', {
|
|
1369
|
+
module: 'GitHub',
|
|
1370
|
+
entityId: milestoneNumber,
|
|
1371
|
+
error: errorMessage,
|
|
1372
|
+
})
|
|
1373
|
+
return { success: false, error: errorMessage }
|
|
1374
|
+
} finally {
|
|
1375
|
+
this.invalidateCache(`milestones:${owner}:${repo}`)
|
|
1376
|
+
this.invalidateCache(`milestone:${owner}:${repo}:${String(milestoneNumber)}`)
|
|
1377
|
+
this.invalidateCache('context:')
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
// ==========================================================================
|
|
1382
|
+
// Repository Insights/Traffic Methods
|
|
1383
|
+
// ==========================================================================
|
|
1384
|
+
|
|
1385
|
+
/**
|
|
1386
|
+
* Get a cached value with a custom TTL.
|
|
1387
|
+
* Used for traffic endpoints which change slowly (10-min TTL).
|
|
1388
|
+
*/
|
|
1389
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -- T is needed at call sites for type-safe casts
|
|
1390
|
+
private getCachedWithTtl<T>(key: string, ttlMs: number): T | undefined {
|
|
1391
|
+
const entry = this.apiCache.get(key)
|
|
1392
|
+
if (entry && Date.now() - entry.timestamp < ttlMs) {
|
|
1393
|
+
return entry.data as T
|
|
1394
|
+
}
|
|
1395
|
+
if (entry) {
|
|
1396
|
+
this.apiCache.delete(key)
|
|
1397
|
+
}
|
|
1398
|
+
return undefined
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
/**
|
|
1402
|
+
* Get repository statistics (stars, forks, watchers).
|
|
1403
|
+
* Uses a single GET /repos/{owner}/{repo} API call.
|
|
1404
|
+
*/
|
|
1405
|
+
async getRepoStats(owner: string, repo: string): Promise<RepoStats | null> {
|
|
1406
|
+
if (!this.octokit) {
|
|
1407
|
+
return null
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
const cacheKey = `repostats:${owner}:${repo}`
|
|
1411
|
+
const cached = this.getCachedWithTtl<RepoStats>(cacheKey, TRAFFIC_CACHE_TTL_MS)
|
|
1412
|
+
if (cached) return cached
|
|
1413
|
+
|
|
1414
|
+
try {
|
|
1415
|
+
const response = await this.octokit.repos.get({ owner, repo })
|
|
1416
|
+
const data = response.data
|
|
1417
|
+
|
|
1418
|
+
const result: RepoStats = {
|
|
1419
|
+
stars: data.stargazers_count,
|
|
1420
|
+
forks: data.forks_count,
|
|
1421
|
+
watchers: data.subscribers_count,
|
|
1422
|
+
openIssues: data.open_issues_count,
|
|
1423
|
+
size: data.size,
|
|
1424
|
+
defaultBranch: data.default_branch,
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
this.setCache(cacheKey, result)
|
|
1428
|
+
return result
|
|
1429
|
+
} catch (error) {
|
|
1430
|
+
logger.error('Failed to get repo stats', {
|
|
1431
|
+
module: 'GitHub',
|
|
1432
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1433
|
+
context: { owner, repo },
|
|
1434
|
+
})
|
|
1435
|
+
return null
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
/**
|
|
1440
|
+
* Get aggregated traffic data (14-day rolling clones + views).
|
|
1441
|
+
* Combines GET /repos/{owner}/{repo}/traffic/clones and /traffic/views.
|
|
1442
|
+
* Requires push access to the repository.
|
|
1443
|
+
*/
|
|
1444
|
+
async getTrafficData(owner: string, repo: string): Promise<TrafficData | null> {
|
|
1445
|
+
if (!this.octokit) {
|
|
1446
|
+
return null
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
const cacheKey = `traffic:${owner}:${repo}`
|
|
1450
|
+
const cached = this.getCachedWithTtl<TrafficData>(cacheKey, TRAFFIC_CACHE_TTL_MS)
|
|
1451
|
+
if (cached) return cached
|
|
1452
|
+
|
|
1453
|
+
try {
|
|
1454
|
+
const [clonesRes, viewsRes] = await Promise.all([
|
|
1455
|
+
this.octokit.rest.repos.getClones({ owner, repo }),
|
|
1456
|
+
this.octokit.rest.repos.getViews({ owner, repo }),
|
|
1457
|
+
])
|
|
1458
|
+
|
|
1459
|
+
const clonesDays = clonesRes.data.clones?.length ?? 0
|
|
1460
|
+
const viewsDays = viewsRes.data.views?.length ?? 0
|
|
1461
|
+
|
|
1462
|
+
const result: TrafficData = {
|
|
1463
|
+
clones: {
|
|
1464
|
+
total: clonesRes.data.count,
|
|
1465
|
+
unique: clonesRes.data.uniques,
|
|
1466
|
+
dailyAvg: clonesDays > 0 ? Math.round(clonesRes.data.count / clonesDays) : 0,
|
|
1467
|
+
},
|
|
1468
|
+
views: {
|
|
1469
|
+
total: viewsRes.data.count,
|
|
1470
|
+
unique: viewsRes.data.uniques,
|
|
1471
|
+
dailyAvg: viewsDays > 0 ? Math.round(viewsRes.data.count / viewsDays) : 0,
|
|
1472
|
+
},
|
|
1473
|
+
period: '14 days',
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
this.setCache(cacheKey, result)
|
|
1477
|
+
return result
|
|
1478
|
+
} catch (error) {
|
|
1479
|
+
logger.error('Failed to get traffic data', {
|
|
1480
|
+
module: 'GitHub',
|
|
1481
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1482
|
+
context: { owner, repo },
|
|
1483
|
+
})
|
|
1484
|
+
return null
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
/**
|
|
1489
|
+
* Get top referrer sources for the repository (14-day rolling).
|
|
1490
|
+
* Requires push access to the repository.
|
|
1491
|
+
*/
|
|
1492
|
+
async getTopReferrers(owner: string, repo: string, limit = 5): Promise<TrafficReferrer[]> {
|
|
1493
|
+
if (!this.octokit) {
|
|
1494
|
+
return []
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
const cacheKey = `referrers:${owner}:${repo}`
|
|
1498
|
+
const cached = this.getCachedWithTtl<TrafficReferrer[]>(cacheKey, TRAFFIC_CACHE_TTL_MS)
|
|
1499
|
+
if (cached) return cached.slice(0, limit)
|
|
1500
|
+
|
|
1501
|
+
try {
|
|
1502
|
+
const response = await this.octokit.rest.repos.getTopReferrers({ owner, repo })
|
|
1503
|
+
|
|
1504
|
+
const result: TrafficReferrer[] = response.data.map((r) => ({
|
|
1505
|
+
referrer: r.referrer,
|
|
1506
|
+
count: r.count,
|
|
1507
|
+
uniques: r.uniques,
|
|
1508
|
+
}))
|
|
1509
|
+
|
|
1510
|
+
this.setCache(cacheKey, result)
|
|
1511
|
+
return result.slice(0, limit)
|
|
1512
|
+
} catch (error) {
|
|
1513
|
+
logger.error('Failed to get top referrers', {
|
|
1514
|
+
module: 'GitHub',
|
|
1515
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1516
|
+
context: { owner, repo },
|
|
1517
|
+
})
|
|
1518
|
+
return []
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
/**
|
|
1523
|
+
* Get popular repository paths (14-day rolling).
|
|
1524
|
+
* Requires push access to the repository.
|
|
1525
|
+
*/
|
|
1526
|
+
async getPopularPaths(owner: string, repo: string, limit = 5): Promise<PopularPath[]> {
|
|
1527
|
+
if (!this.octokit) {
|
|
1528
|
+
return []
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
const cacheKey = `paths:${owner}:${repo}`
|
|
1532
|
+
const cached = this.getCachedWithTtl<PopularPath[]>(cacheKey, TRAFFIC_CACHE_TTL_MS)
|
|
1533
|
+
if (cached) return cached.slice(0, limit)
|
|
1534
|
+
|
|
1535
|
+
try {
|
|
1536
|
+
const response = await this.octokit.rest.repos.getTopPaths({ owner, repo })
|
|
1537
|
+
|
|
1538
|
+
const result: PopularPath[] = response.data.map((p) => ({
|
|
1539
|
+
path: p.path,
|
|
1540
|
+
title: p.title,
|
|
1541
|
+
count: p.count,
|
|
1542
|
+
uniques: p.uniques,
|
|
1543
|
+
}))
|
|
1544
|
+
|
|
1545
|
+
this.setCache(cacheKey, result)
|
|
1546
|
+
return result.slice(0, limit)
|
|
1547
|
+
} catch (error) {
|
|
1548
|
+
logger.error('Failed to get popular paths', {
|
|
1549
|
+
module: 'GitHub',
|
|
1550
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1551
|
+
context: { owner, repo },
|
|
1552
|
+
})
|
|
1553
|
+
return []
|
|
974
1554
|
}
|
|
975
1555
|
}
|
|
976
1556
|
}
|