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.
Files changed (109) hide show
  1. package/.dockerignore +131 -122
  2. package/.gitattributes +29 -0
  3. package/.github/workflows/docker-publish.yml +1 -1
  4. package/.github/workflows/lint-and-test.yml +1 -2
  5. package/.github/workflows/secrets-scanning.yml +0 -1
  6. package/.github/workflows/security-update.yml +6 -6
  7. package/.vscode/settings.json +17 -15
  8. package/CHANGELOG.md +1065 -11
  9. package/DOCKER_README.md +51 -33
  10. package/Dockerfile +14 -12
  11. package/README.md +68 -33
  12. package/SECURITY.md +225 -220
  13. package/dist/cli.js +7 -0
  14. package/dist/cli.js.map +1 -1
  15. package/dist/constants/ServerInstructions.d.ts +1 -1
  16. package/dist/constants/ServerInstructions.d.ts.map +1 -1
  17. package/dist/constants/ServerInstructions.js +70 -26
  18. package/dist/constants/ServerInstructions.js.map +1 -1
  19. package/dist/constants/icons.d.ts +2 -0
  20. package/dist/constants/icons.d.ts.map +1 -1
  21. package/dist/constants/icons.js +6 -0
  22. package/dist/constants/icons.js.map +1 -1
  23. package/dist/database/SqliteAdapter.d.ts +51 -10
  24. package/dist/database/SqliteAdapter.d.ts.map +1 -1
  25. package/dist/database/SqliteAdapter.js +143 -43
  26. package/dist/database/SqliteAdapter.js.map +1 -1
  27. package/dist/filtering/ToolFilter.d.ts +1 -1
  28. package/dist/filtering/ToolFilter.d.ts.map +1 -1
  29. package/dist/filtering/ToolFilter.js +7 -1
  30. package/dist/filtering/ToolFilter.js.map +1 -1
  31. package/dist/github/GitHubIntegration.d.ts +74 -2
  32. package/dist/github/GitHubIntegration.d.ts.map +1 -1
  33. package/dist/github/GitHubIntegration.js +508 -7
  34. package/dist/github/GitHubIntegration.js.map +1 -1
  35. package/dist/handlers/prompts/index.js +1 -0
  36. package/dist/handlers/prompts/index.js.map +1 -1
  37. package/dist/handlers/resources/index.d.ts.map +1 -1
  38. package/dist/handlers/resources/index.js +257 -13
  39. package/dist/handlers/resources/index.js.map +1 -1
  40. package/dist/handlers/tools/index.d.ts.map +1 -1
  41. package/dist/handlers/tools/index.js +595 -8
  42. package/dist/handlers/tools/index.js.map +1 -1
  43. package/dist/server/McpServer.d.ts +2 -0
  44. package/dist/server/McpServer.d.ts.map +1 -1
  45. package/dist/server/McpServer.js +69 -26
  46. package/dist/server/McpServer.js.map +1 -1
  47. package/dist/types/index.d.ts +97 -0
  48. package/dist/types/index.d.ts.map +1 -1
  49. package/dist/types/index.js.map +1 -1
  50. package/dist/utils/logger.d.ts +1 -0
  51. package/dist/utils/logger.d.ts.map +1 -1
  52. package/dist/utils/logger.js +8 -1
  53. package/dist/utils/logger.js.map +1 -1
  54. package/dist/utils/progress-utils.d.ts +18 -3
  55. package/dist/utils/progress-utils.d.ts.map +1 -1
  56. package/dist/utils/progress-utils.js.map +1 -1
  57. package/dist/utils/security-utils.d.ts +91 -0
  58. package/dist/utils/security-utils.d.ts.map +1 -0
  59. package/dist/utils/security-utils.js +184 -0
  60. package/dist/utils/security-utils.js.map +1 -0
  61. package/dist/vector/VectorSearchManager.d.ts +2 -1
  62. package/dist/vector/VectorSearchManager.d.ts.map +1 -1
  63. package/dist/vector/VectorSearchManager.js +100 -34
  64. package/dist/vector/VectorSearchManager.js.map +1 -1
  65. package/docker-compose.yml +46 -37
  66. package/mcp-config-example.json +0 -2
  67. package/package.json +21 -14
  68. package/releases/v4.3.1.md +69 -0
  69. package/releases/v4.4.0.md +120 -0
  70. package/server.json +3 -3
  71. package/src/cli.ts +11 -0
  72. package/src/constants/ServerInstructions.ts +70 -26
  73. package/src/constants/icons.ts +7 -0
  74. package/src/database/SqliteAdapter.ts +165 -44
  75. package/src/filtering/ToolFilter.ts +7 -1
  76. package/src/github/GitHubIntegration.ts +588 -8
  77. package/src/handlers/prompts/index.ts +1 -0
  78. package/src/handlers/resources/index.ts +318 -12
  79. package/src/handlers/tools/index.ts +686 -13
  80. package/src/server/McpServer.ts +79 -37
  81. package/src/types/index.ts +98 -0
  82. package/src/utils/logger.ts +10 -1
  83. package/src/utils/progress-utils.ts +17 -6
  84. package/src/utils/security-utils.ts +205 -0
  85. package/src/vector/VectorSearchManager.ts +110 -39
  86. package/tests/constants/icons.test.ts +102 -0
  87. package/tests/constants/server-instructions.test.ts +549 -0
  88. package/tests/database/sqlite-adapter.bench.ts +63 -0
  89. package/tests/database/sqlite-adapter.test.ts +555 -0
  90. package/tests/filtering/tool-filter.test.ts +266 -0
  91. package/tests/github/github-integration.test.ts +1024 -0
  92. package/tests/handlers/github-resource-handlers.test.ts +473 -0
  93. package/tests/handlers/github-tool-handlers.test.ts +556 -0
  94. package/tests/handlers/prompt-handlers.test.ts +91 -0
  95. package/tests/handlers/resource-handlers.test.ts +339 -0
  96. package/tests/handlers/tool-handlers.test.ts +497 -0
  97. package/tests/handlers/vector-tool-handlers.test.ts +238 -0
  98. package/tests/security/sql-injection.test.ts +347 -0
  99. package/tests/server/mcp-server.bench.ts +55 -0
  100. package/tests/server/mcp-server.test.ts +675 -0
  101. package/tests/utils/logger.test.ts +180 -0
  102. package/tests/utils/mcp-logger.test.ts +212 -0
  103. package/tests/utils/progress-utils.test.ts +156 -0
  104. package/tests/utils/security-utils.test.ts +82 -0
  105. package/tests/vector/vector-search-manager.test.ts +335 -0
  106. package/tests/vector/vector-search.bench.ts +53 -0
  107. package/vitest.config.ts +15 -0
  108. package/.github/workflows/DOCKER_DEPLOYMENT_SETUP.md +0 -387
  109. 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
- return response.data
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
- return {
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
- return response.data.map((pr) => ({
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 ? 'MERGED' : pr.state === 'open' ? 'OPEN' : 'CLOSED',
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
- return {
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
- return response.data.workflow_runs.map((run) => ({
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