memory-journal-mcp 6.2.1 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1659 @@
1
+ import { Octokit } from '@octokit/rest';
2
+ import { graphql } from '@octokit/graphql';
3
+ import * as simpleGitImport from 'simple-git';
4
+ import { execFileSync } from 'child_process';
5
+
6
+ // src/github/github-integration/client.ts
7
+
8
+ // src/utils/errors/suggestions.ts
9
+ var GENERIC_CODES = /* @__PURE__ */ new Set(["QUERY_FAILED", "INTERNAL_ERROR", "UNKNOWN_ERROR"]);
10
+ var ERROR_SUGGESTIONS = [
11
+ // Resource not found patterns
12
+ {
13
+ pattern: /not found/i,
14
+ suggestion: "Verify the resource identifier and try again",
15
+ code: "RESOURCE_NOT_FOUND"
16
+ },
17
+ {
18
+ pattern: /no such table/i,
19
+ suggestion: "The database table does not exist. Run database initialization.",
20
+ code: "TABLE_NOT_FOUND"
21
+ },
22
+ // Permission / access patterns
23
+ {
24
+ pattern: /permission denied|SQLITE_READONLY/i,
25
+ suggestion: "Check file permissions and database access rights",
26
+ code: "PERMISSION_DENIED"
27
+ },
28
+ // Connection / lock patterns
29
+ {
30
+ pattern: /database is locked/i,
31
+ suggestion: "The database is locked by another process. Retry after a short delay.",
32
+ code: "CONNECTION_FAILED"
33
+ },
34
+ // Constraint patterns
35
+ {
36
+ pattern: /SQLITE_CONSTRAINT|unique constraint/i,
37
+ suggestion: "A uniqueness or integrity constraint was violated. Check input values.",
38
+ code: "VALIDATION_FAILED"
39
+ },
40
+ // Disk / space patterns
41
+ {
42
+ pattern: /disk I\/O|SQLITE_FULL/i,
43
+ suggestion: "The disk may be full or the filesystem is read-only."
44
+ },
45
+ // Malformed input patterns
46
+ {
47
+ pattern: /malformed|invalid json|unexpected token/i,
48
+ suggestion: "The input appears malformed. Check the format and try again.",
49
+ code: "VALIDATION_FAILED"
50
+ },
51
+ // Schema / types patterns
52
+ {
53
+ pattern: /invalid input syntax for type|requires a.*column/i,
54
+ suggestion: "The provided value is not valid for the assigned type.",
55
+ code: "VALIDATION_FAILED"
56
+ },
57
+ {
58
+ pattern: /^Missing required parameters:/i,
59
+ suggestion: "Provide all required parameters in your request.",
60
+ code: "VALIDATION_FAILED"
61
+ },
62
+ // Codemode / Sandbox patterns
63
+ {
64
+ pattern: /execution timed out/i,
65
+ suggestion: "Reduce code complexity or increase timeout (max 30s). Break into smaller operations.",
66
+ code: "QUERY_FAILED"
67
+ },
68
+ {
69
+ pattern: /code validation failed/i,
70
+ suggestion: "Check for blocked patterns. Use mj.* API instead.",
71
+ code: "VALIDATION_FAILED"
72
+ },
73
+ {
74
+ pattern: /sandbox.*not initialized/i,
75
+ suggestion: "Internal sandbox error. Retry the operation.",
76
+ code: "INTERNAL_ERROR"
77
+ }
78
+ ];
79
+ function matchSuggestion(message) {
80
+ for (const entry of ERROR_SUGGESTIONS) {
81
+ if (entry.pattern.test(message)) {
82
+ return { suggestion: entry.suggestion, code: entry.code };
83
+ }
84
+ }
85
+ return void 0;
86
+ }
87
+
88
+ // src/types/errors.ts
89
+ var MemoryJournalMcpError = class extends Error {
90
+ /** Module-prefixed error code */
91
+ code;
92
+ /** Error category for programmatic handling */
93
+ category;
94
+ /** Actionable suggestion for resolving the error */
95
+ suggestion;
96
+ /** Whether the operation can be retried */
97
+ recoverable;
98
+ /** Additional structured context */
99
+ details;
100
+ constructor(message, code, category, options) {
101
+ super(message, options?.cause ? { cause: options.cause } : void 0);
102
+ this.name = "MemoryJournalMcpError";
103
+ this.category = category;
104
+ this.recoverable = options?.recoverable ?? false;
105
+ this.details = options?.details;
106
+ const matched = matchSuggestion(message);
107
+ if (GENERIC_CODES.has(code) && matched?.code) {
108
+ this.code = matched.code;
109
+ } else {
110
+ this.code = code;
111
+ }
112
+ this.suggestion = options?.suggestion ?? matched?.suggestion;
113
+ }
114
+ /**
115
+ * Convert to a structured ErrorResponse for tool responses.
116
+ */
117
+ toResponse() {
118
+ return {
119
+ success: false,
120
+ error: this.message,
121
+ code: this.code,
122
+ category: this.category,
123
+ suggestion: this.suggestion,
124
+ recoverable: this.recoverable,
125
+ ...this.details ? { details: this.details } : {}
126
+ };
127
+ }
128
+ };
129
+ var ConnectionError = class extends MemoryJournalMcpError {
130
+ constructor(message, details) {
131
+ super(message, "CONNECTION_FAILED", "connection" /* CONNECTION */, {
132
+ suggestion: "Check database path and file permissions",
133
+ recoverable: true,
134
+ details
135
+ });
136
+ this.name = "ConnectionError";
137
+ }
138
+ };
139
+ var QueryError = class extends MemoryJournalMcpError {
140
+ constructor(message, details) {
141
+ super(message, "QUERY_FAILED", "query" /* QUERY */, {
142
+ suggestion: "Check query parameters and database state",
143
+ recoverable: false,
144
+ details
145
+ });
146
+ this.name = "QueryError";
147
+ }
148
+ };
149
+ var ValidationError = class extends MemoryJournalMcpError {
150
+ constructor(message, details) {
151
+ super(message, "VALIDATION_FAILED", "validation" /* VALIDATION */, {
152
+ suggestion: "Check input parameters against the tool schema",
153
+ recoverable: false,
154
+ details
155
+ });
156
+ this.name = "ValidationError";
157
+ }
158
+ };
159
+ var ResourceNotFoundError = class extends MemoryJournalMcpError {
160
+ constructor(resourceType, identifier) {
161
+ super(
162
+ `${resourceType} not found: ${identifier}`,
163
+ "RESOURCE_NOT_FOUND",
164
+ "resource" /* RESOURCE */,
165
+ {
166
+ suggestion: `Verify the ${resourceType.toLowerCase()} identifier and try again`,
167
+ recoverable: false,
168
+ details: { resourceType, identifier }
169
+ }
170
+ );
171
+ this.name = "ResourceNotFoundError";
172
+ }
173
+ };
174
+ var ConfigurationError = class extends MemoryJournalMcpError {
175
+ constructor(message, details) {
176
+ super(message, "CONFIGURATION_ERROR", "configuration" /* CONFIGURATION */, {
177
+ suggestion: "Check server configuration and environment variables",
178
+ recoverable: false,
179
+ details
180
+ });
181
+ this.name = "ConfigurationError";
182
+ }
183
+ };
184
+
185
+ // src/utils/security-utils.ts
186
+ var GIT_COMMAND_TIMEOUT_MS = 3e3;
187
+ var SecurityError = class extends MemoryJournalMcpError {
188
+ constructor(message, code) {
189
+ super(message, code, "validation" /* VALIDATION */, {
190
+ suggestion: "Check input for security violations",
191
+ recoverable: false
192
+ });
193
+ this.name = "SecurityError";
194
+ }
195
+ };
196
+ var InvalidDateFormatError = class extends SecurityError {
197
+ constructor(value) {
198
+ super(`Invalid date format pattern: '${value}'`, "INVALID_DATE_FORMAT");
199
+ this.name = "InvalidDateFormatError";
200
+ }
201
+ };
202
+ var PathTraversalError = class extends SecurityError {
203
+ constructor(path) {
204
+ super(`Path traversal detected: '${path}'`, "PATH_TRAVERSAL");
205
+ this.name = "PathTraversalError";
206
+ }
207
+ };
208
+ var ALLOWED_DATE_FORMATS = {
209
+ day: "%Y-%m-%d",
210
+ week: "%Y-W%W",
211
+ month: "%Y-%m"
212
+ };
213
+ function validateDateFormatPattern(groupBy) {
214
+ const format = ALLOWED_DATE_FORMATS[groupBy];
215
+ if (!format) {
216
+ throw new InvalidDateFormatError(groupBy);
217
+ }
218
+ return format;
219
+ }
220
+ function assertNoPathTraversal(filename) {
221
+ if (filename.includes("/") || filename.includes("\\") || filename.includes("..")) {
222
+ throw new PathTraversalError(filename);
223
+ }
224
+ }
225
+ var TOKEN_PATTERNS = [
226
+ // GitHub personal access tokens (classic and fine-grained)
227
+ /ghp_[A-Za-z0-9_]{36,}/g,
228
+ /github_pat_[A-Za-z0-9_]{82,}/g,
229
+ // Authorization headers in error dumps
230
+ /Authorization:\s*(?:token|Bearer)\s+\S+/gi,
231
+ // Generic Bearer tokens
232
+ /Bearer\s+[A-Za-z0-9._\-~+/]+=*/gi
233
+ ];
234
+ function sanitizeErrorForLogging(message) {
235
+ let sanitized = message;
236
+ for (const pattern of TOKEN_PATTERNS) {
237
+ pattern.lastIndex = 0;
238
+ sanitized = sanitized.replace(pattern, "[REDACTED]");
239
+ }
240
+ return sanitized;
241
+ }
242
+ function sanitizeAuthor(raw) {
243
+ return raw.replace(/[\x00-\x1f\x7f]/g, "").slice(0, 100);
244
+ }
245
+ function resolveAuthor() {
246
+ const envAuthor = process.env["TEAM_AUTHOR"]?.trim().replace(/"/g, "");
247
+ if (envAuthor) return sanitizeAuthor(envAuthor);
248
+ try {
249
+ const gitUser = execFileSync("git", ["config", "user.name"], {
250
+ encoding: "utf-8",
251
+ timeout: GIT_COMMAND_TIMEOUT_MS
252
+ }).trim().replace(/"/g, "");
253
+ if (gitUser) return sanitizeAuthor(gitUser);
254
+ } catch {
255
+ }
256
+ return "unknown";
257
+ }
258
+
259
+ // src/utils/logger.ts
260
+ var LOG_LEVELS = {
261
+ debug: 7,
262
+ info: 6,
263
+ notice: 5,
264
+ warning: 4,
265
+ error: 3,
266
+ critical: 2
267
+ };
268
+ var Logger = class {
269
+ minLevel;
270
+ constructor(level = "info") {
271
+ this.minLevel = LOG_LEVELS[level];
272
+ }
273
+ shouldLog(level) {
274
+ return LOG_LEVELS[level] <= this.minLevel;
275
+ }
276
+ log(level, message, context) {
277
+ if (!this.shouldLog(level)) return;
278
+ const safeMessage = message.replace(/\n|\r/g, "");
279
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
280
+ const levelUpper = level.toUpperCase().padEnd(8);
281
+ const mod = context?.module ? `[${context.module.replace(/\n|\r/g, "")}]` : "";
282
+ const op = context?.operation ? `[${context.operation.replace(/\n|\r/g, "")}]` : "";
283
+ let line = `[${timestamp}] [${levelUpper}] ${mod}${op} ${safeMessage}`;
284
+ const extras = { ...context };
285
+ delete extras["module"];
286
+ delete extras["operation"];
287
+ if (extras["error"] != null && typeof extras["error"] === "string") {
288
+ extras["error"] = sanitizeErrorForLogging(extras["error"]);
289
+ }
290
+ if (Object.keys(extras).length > 0) {
291
+ line += ` ${JSON.stringify(extras).replace(/\n|\r/g, "")}`;
292
+ }
293
+ console.error(line);
294
+ }
295
+ debug(message, context) {
296
+ this.log("debug", message, context);
297
+ }
298
+ info(message, context) {
299
+ this.log("info", message, context);
300
+ }
301
+ notice(message, context) {
302
+ this.log("notice", message, context);
303
+ }
304
+ warning(message, context) {
305
+ this.log("warning", message, context);
306
+ }
307
+ error(message, context) {
308
+ this.log("error", message, context);
309
+ }
310
+ critical(message, context) {
311
+ this.log("critical", message, context);
312
+ }
313
+ setLevel(level) {
314
+ if (level in LOG_LEVELS) {
315
+ this.minLevel = LOG_LEVELS[level];
316
+ }
317
+ }
318
+ };
319
+ var rawLevel = process.env["LOG_LEVEL"] ?? "info";
320
+ var envLevel = rawLevel in LOG_LEVELS ? rawLevel : "info";
321
+ var logger = new Logger(envLevel);
322
+
323
+ // src/github/github-integration/client.ts
324
+ var CACHE_TTL_MS = 5 * 60 * 1e3;
325
+ var TRAFFIC_CACHE_TTL_MS = 10 * 60 * 1e3;
326
+ var simpleGit2 = simpleGitImport.simpleGit;
327
+ var GitHubClient = class {
328
+ octokit = null;
329
+ graphqlWithAuth = null;
330
+ git;
331
+ token;
332
+ cachedRepoInfo = null;
333
+ apiCache = /* @__PURE__ */ new Map();
334
+ constructor(workingDir = ".") {
335
+ this.token = process.env["GITHUB_TOKEN"];
336
+ const effectiveDir = workingDir;
337
+ const resolvedDir = effectiveDir === "." ? process.cwd() : effectiveDir;
338
+ logger.info("GitHub integration using directory", {
339
+ module: "GitHub",
340
+ workingDir,
341
+ effectiveDir,
342
+ resolvedDir,
343
+ cwd: process.cwd()
344
+ });
345
+ this.git = simpleGit2(effectiveDir);
346
+ if (this.token) {
347
+ this.octokit = new Octokit({ auth: this.token });
348
+ this.graphqlWithAuth = graphql.defaults({
349
+ headers: { authorization: `token ${this.token}` }
350
+ });
351
+ logger.info("GitHub integration initialized with token", { module: "GitHub" });
352
+ } else {
353
+ logger.info("GitHub integration initialized without token (limited functionality)", {
354
+ module: "GitHub"
355
+ });
356
+ }
357
+ }
358
+ isApiAvailable() {
359
+ return this.octokit !== null;
360
+ }
361
+ getCached(key) {
362
+ const entry = this.apiCache.get(key);
363
+ if (entry && Date.now() - entry.timestamp < CACHE_TTL_MS) {
364
+ this.apiCache.delete(key);
365
+ this.apiCache.set(key, entry);
366
+ return entry.data;
367
+ }
368
+ if (entry) {
369
+ this.apiCache.delete(key);
370
+ }
371
+ return void 0;
372
+ }
373
+ getCachedWithTtl(key, ttlMs) {
374
+ const entry = this.apiCache.get(key);
375
+ if (entry && Date.now() - entry.timestamp < ttlMs) {
376
+ this.apiCache.delete(key);
377
+ this.apiCache.set(key, entry);
378
+ return entry.data;
379
+ }
380
+ if (entry) {
381
+ this.apiCache.delete(key);
382
+ }
383
+ return void 0;
384
+ }
385
+ setCache(key, data) {
386
+ this.apiCache.delete(key);
387
+ this.apiCache.set(key, { data, timestamp: Date.now() });
388
+ if (this.apiCache.size > 100) {
389
+ const oldestKey = this.apiCache.keys().next().value;
390
+ if (oldestKey !== void 0) {
391
+ this.apiCache.delete(oldestKey);
392
+ }
393
+ }
394
+ }
395
+ invalidateCache(prefix) {
396
+ for (const key of this.apiCache.keys()) {
397
+ if (key.startsWith(prefix)) {
398
+ this.apiCache.delete(key);
399
+ }
400
+ }
401
+ }
402
+ clearCache() {
403
+ this.apiCache.clear();
404
+ }
405
+ };
406
+
407
+ // src/github/github-integration/issues.ts
408
+ var IssuesManager = class {
409
+ constructor(client) {
410
+ this.client = client;
411
+ }
412
+ client;
413
+ async getIssues(owner, repo, state = "open", limit = 20) {
414
+ if (!this.client.octokit) {
415
+ return [];
416
+ }
417
+ const cacheKey = `issues:${owner}:${repo}:${state}:${String(limit)}`;
418
+ const cached = this.client.getCached(cacheKey);
419
+ if (cached) return cached;
420
+ try {
421
+ const response = await this.client.octokit.issues.listForRepo({
422
+ owner,
423
+ repo,
424
+ state,
425
+ per_page: Math.min(limit * 2, 100),
426
+ sort: "updated",
427
+ direction: "desc"
428
+ });
429
+ const result = response.data.filter((issue) => !issue.pull_request).slice(0, limit).map((issue) => ({
430
+ number: issue.number,
431
+ title: issue.title,
432
+ url: issue.html_url,
433
+ state: issue.state === "open" ? "OPEN" : "CLOSED",
434
+ milestone: issue.milestone ? {
435
+ number: issue.milestone.number,
436
+ title: issue.milestone.title
437
+ } : null
438
+ }));
439
+ this.client.setCache(cacheKey, result);
440
+ return result;
441
+ } catch (error) {
442
+ logger.error("Failed to get issues", {
443
+ module: "GitHub",
444
+ error: error instanceof Error ? error.message : String(error)
445
+ });
446
+ return [];
447
+ }
448
+ }
449
+ async getIssue(owner, repo, issueNumber) {
450
+ if (!this.client.octokit) {
451
+ return null;
452
+ }
453
+ const cacheKey = `issue:${owner}:${repo}:${String(issueNumber)}`;
454
+ const cached = this.client.getCached(cacheKey);
455
+ if (cached !== void 0) return cached;
456
+ try {
457
+ const response = await this.client.octokit.issues.get({
458
+ owner,
459
+ repo,
460
+ issue_number: issueNumber
461
+ });
462
+ const issue = response.data;
463
+ if (issue.pull_request) {
464
+ return null;
465
+ }
466
+ const details = {
467
+ number: issue.number,
468
+ title: issue.title,
469
+ url: issue.html_url,
470
+ state: issue.state === "open" ? "OPEN" : "CLOSED",
471
+ nodeId: issue.node_id,
472
+ body: issue.body ?? null,
473
+ labels: issue.labels.map((l) => typeof l === "string" ? l : l.name ?? ""),
474
+ assignees: issue.assignees?.map((a) => a.login) ?? [],
475
+ createdAt: issue.created_at,
476
+ updatedAt: issue.updated_at,
477
+ closedAt: issue.closed_at,
478
+ commentsCount: issue.comments,
479
+ milestone: issue.milestone ? { number: issue.milestone.number, title: issue.milestone.title } : null
480
+ };
481
+ this.client.setCache(cacheKey, details);
482
+ return details;
483
+ } catch (error) {
484
+ logger.error("Failed to get issue details", {
485
+ module: "GitHub",
486
+ entityId: issueNumber,
487
+ error: error instanceof Error ? error.message : String(error)
488
+ });
489
+ return null;
490
+ }
491
+ }
492
+ async createIssue(owner, repo, title, body, labels, assignees, milestone) {
493
+ if (!this.client.octokit) {
494
+ logger.error("Cannot create issue: GitHub API not available", { module: "GitHub" });
495
+ return null;
496
+ }
497
+ try {
498
+ const response = await this.client.octokit.issues.create({
499
+ owner,
500
+ repo,
501
+ title,
502
+ body,
503
+ labels,
504
+ assignees,
505
+ milestone
506
+ });
507
+ logger.info("Created GitHub issue", {
508
+ module: "GitHub",
509
+ entityId: response.data.number,
510
+ context: { title, owner, repo }
511
+ });
512
+ return {
513
+ number: response.data.number,
514
+ url: response.data.html_url,
515
+ title: response.data.title,
516
+ nodeId: response.data.node_id
517
+ };
518
+ } catch (error) {
519
+ logger.error("Failed to create issue", {
520
+ module: "GitHub",
521
+ error: error instanceof Error ? error.message : String(error),
522
+ context: { title, owner, repo }
523
+ });
524
+ return null;
525
+ } finally {
526
+ this.client.invalidateCache(`issues:${owner}:${repo}`);
527
+ this.client.invalidateCache("context:");
528
+ }
529
+ }
530
+ async closeIssue(owner, repo, issueNumber, comment) {
531
+ if (!this.client.octokit) {
532
+ logger.error("Cannot close issue: GitHub API not available", { module: "GitHub" });
533
+ return null;
534
+ }
535
+ try {
536
+ if (comment) {
537
+ await this.client.octokit.issues.createComment({
538
+ owner,
539
+ repo,
540
+ issue_number: issueNumber,
541
+ body: comment
542
+ });
543
+ }
544
+ const response = await this.client.octokit.issues.update({
545
+ owner,
546
+ repo,
547
+ issue_number: issueNumber,
548
+ state: "closed"
549
+ });
550
+ logger.info("Closed GitHub issue", {
551
+ module: "GitHub",
552
+ entityId: issueNumber,
553
+ context: { owner, repo, hadComment: !!comment }
554
+ });
555
+ return {
556
+ success: true,
557
+ url: response.data.html_url
558
+ };
559
+ } catch (error) {
560
+ logger.error("Failed to close issue", {
561
+ module: "GitHub",
562
+ entityId: issueNumber,
563
+ error: error instanceof Error ? error.message : String(error)
564
+ });
565
+ return null;
566
+ } finally {
567
+ this.client.invalidateCache(`issues:${owner}:${repo}`);
568
+ this.client.invalidateCache(`issue:${owner}:${repo}:${String(issueNumber)}`);
569
+ this.client.invalidateCache("context:");
570
+ }
571
+ }
572
+ };
573
+
574
+ // src/github/github-integration/pull-requests.ts
575
+ var PullRequestsManager = class _PullRequestsManager {
576
+ constructor(client) {
577
+ this.client = client;
578
+ }
579
+ client;
580
+ /** Known Copilot bot login patterns */
581
+ static COPILOT_BOT_PATTERNS = [
582
+ "copilot-pull-request-reviewer[bot]",
583
+ "github-copilot[bot]",
584
+ "copilot[bot]"
585
+ ];
586
+ async getPullRequests(owner, repo, state = "open", limit = 20) {
587
+ if (!this.client.octokit) {
588
+ return [];
589
+ }
590
+ const cacheKey = `prs:${owner}:${repo}:${state}:${String(limit)}`;
591
+ const cached = this.client.getCached(cacheKey);
592
+ if (cached) return cached;
593
+ try {
594
+ const response = await this.client.octokit.pulls.list({
595
+ owner,
596
+ repo,
597
+ state,
598
+ per_page: limit,
599
+ sort: "updated",
600
+ direction: "desc"
601
+ });
602
+ const result = response.data.map((pr) => ({
603
+ number: pr.number,
604
+ title: pr.title,
605
+ url: pr.html_url,
606
+ state: pr.merged_at ? "MERGED" : pr.state === "open" ? "OPEN" : "CLOSED"
607
+ }));
608
+ this.client.setCache(cacheKey, result);
609
+ return result;
610
+ } catch (error) {
611
+ logger.error("Failed to get pull requests", {
612
+ module: "GitHub",
613
+ error: error instanceof Error ? error.message : String(error)
614
+ });
615
+ return [];
616
+ }
617
+ }
618
+ async getPullRequest(owner, repo, prNumber) {
619
+ if (!this.client.octokit) {
620
+ return null;
621
+ }
622
+ const cacheKey = `pr:${owner}:${repo}:${String(prNumber)}`;
623
+ const cached = this.client.getCached(cacheKey);
624
+ if (cached !== void 0) return cached;
625
+ try {
626
+ const response = await this.client.octokit.pulls.get({
627
+ owner,
628
+ repo,
629
+ pull_number: prNumber
630
+ });
631
+ const pr = response.data;
632
+ const details = {
633
+ number: pr.number,
634
+ title: pr.title,
635
+ url: pr.html_url,
636
+ state: pr.merged_at ? "MERGED" : pr.state === "open" ? "OPEN" : "CLOSED",
637
+ body: pr.body,
638
+ draft: pr.draft ?? false,
639
+ headBranch: pr.head.ref,
640
+ baseBranch: pr.base.ref,
641
+ author: pr.user?.login ?? "unknown",
642
+ createdAt: pr.created_at,
643
+ updatedAt: pr.updated_at,
644
+ mergedAt: pr.merged_at,
645
+ closedAt: pr.closed_at,
646
+ additions: pr.additions,
647
+ deletions: pr.deletions,
648
+ changedFiles: pr.changed_files
649
+ };
650
+ this.client.setCache(cacheKey, details);
651
+ return details;
652
+ } catch (error) {
653
+ logger.error("Failed to get PR details", {
654
+ module: "GitHub",
655
+ entityId: prNumber,
656
+ error: error instanceof Error ? error.message : String(error)
657
+ });
658
+ return null;
659
+ }
660
+ }
661
+ static isCopilotAuthor(login) {
662
+ const lower = login.toLowerCase();
663
+ return _PullRequestsManager.COPILOT_BOT_PATTERNS.some(
664
+ (p) => lower === p || lower.includes("copilot")
665
+ );
666
+ }
667
+ async getReviews(owner, repo, prNumber) {
668
+ if (!this.client.octokit) return [];
669
+ const cacheKey = `reviews:${owner}:${repo}:${String(prNumber)}`;
670
+ const cached = this.client.getCached(cacheKey);
671
+ if (cached) return cached;
672
+ try {
673
+ const response = await this.client.octokit.rest.pulls.listReviews({
674
+ owner,
675
+ repo,
676
+ pull_number: prNumber,
677
+ per_page: 100
678
+ });
679
+ const reviews = response.data.map((r) => ({
680
+ id: r.id,
681
+ author: r.user?.login ?? "unknown",
682
+ state: r.state,
683
+ body: r.body ?? null,
684
+ submittedAt: r.submitted_at ?? r.commit_id ?? (/* @__PURE__ */ new Date()).toISOString(),
685
+ isCopilot: _PullRequestsManager.isCopilotAuthor(r.user?.login ?? "")
686
+ }));
687
+ this.client.setCache(cacheKey, reviews);
688
+ return reviews;
689
+ } catch (error) {
690
+ logger.error("Failed to get PR reviews", {
691
+ module: "GitHub",
692
+ entityId: prNumber,
693
+ error: error instanceof Error ? error.message : String(error)
694
+ });
695
+ return [];
696
+ }
697
+ }
698
+ async getReviewComments(owner, repo, prNumber) {
699
+ if (!this.client.octokit) return [];
700
+ const cacheKey = `review-comments:${owner}:${repo}:${String(prNumber)}`;
701
+ const cached = this.client.getCached(cacheKey);
702
+ if (cached) return cached;
703
+ try {
704
+ const response = await this.client.octokit.rest.pulls.listReviewComments({
705
+ owner,
706
+ repo,
707
+ pull_number: prNumber,
708
+ per_page: 100
709
+ });
710
+ const comments = response.data.map((c) => ({
711
+ id: c.id,
712
+ author: c.user?.login ?? "unknown",
713
+ body: c.body,
714
+ path: c.path,
715
+ line: c.line ?? c.original_line ?? null,
716
+ side: c.side ?? "RIGHT",
717
+ createdAt: c.created_at,
718
+ isCopilot: _PullRequestsManager.isCopilotAuthor(c.user?.login ?? "")
719
+ }));
720
+ this.client.setCache(cacheKey, comments);
721
+ return comments;
722
+ } catch (error) {
723
+ logger.error("Failed to get review comments", {
724
+ module: "GitHub",
725
+ entityId: prNumber,
726
+ error: error instanceof Error ? error.message : String(error)
727
+ });
728
+ return [];
729
+ }
730
+ }
731
+ async getCopilotReviewSummary(owner, repo, prNumber) {
732
+ const [reviews, comments] = await Promise.all([
733
+ this.getReviews(owner, repo, prNumber),
734
+ this.getReviewComments(owner, repo, prNumber)
735
+ ]);
736
+ const copilotReviews = reviews.filter((r) => r.isCopilot);
737
+ const copilotComments = comments.filter((c) => c.isCopilot);
738
+ let state = "none";
739
+ if (copilotReviews.length > 0) {
740
+ const latest = copilotReviews[copilotReviews.length - 1];
741
+ if (latest !== void 0) {
742
+ if (latest.state === "APPROVED") state = "approved";
743
+ else if (latest.state === "CHANGES_REQUESTED") state = "changes_requested";
744
+ else if (latest.state === "COMMENTED") state = "commented";
745
+ }
746
+ }
747
+ return {
748
+ prNumber,
749
+ state,
750
+ commentCount: copilotComments.length,
751
+ comments: copilotComments
752
+ };
753
+ }
754
+ };
755
+
756
+ // src/github/github-integration/projects.ts
757
+ var ProjectsManager = class {
758
+ constructor(client) {
759
+ this.client = client;
760
+ }
761
+ client;
762
+ async getProjectKanban(owner, projectNumber, repo) {
763
+ if (!this.client.graphqlWithAuth) {
764
+ logger.debug("GraphQL not available - no token", { module: "GitHub" });
765
+ return null;
766
+ }
767
+ const projectFragment = `
768
+ fragment ProjectData on ProjectV2 {
769
+ id
770
+ title
771
+ fields(first: 20) {
772
+ nodes {
773
+ ... on ProjectV2SingleSelectField {
774
+ id
775
+ name
776
+ options {
777
+ id
778
+ name
779
+ color
780
+ }
781
+ }
782
+ }
783
+ }
784
+ items(first: 100) {
785
+ nodes {
786
+ id
787
+ type
788
+ createdAt
789
+ updatedAt
790
+ fieldValues(first: 10) {
791
+ nodes {
792
+ ... on ProjectV2ItemFieldSingleSelectValue {
793
+ name
794
+ field {
795
+ ... on ProjectV2SingleSelectField {
796
+ name
797
+ }
798
+ }
799
+ }
800
+ }
801
+ }
802
+ content {
803
+ ... on Issue {
804
+ number
805
+ title
806
+ url
807
+ labels(first: 5) {
808
+ nodes { name }
809
+ }
810
+ assignees(first: 5) {
811
+ nodes { login }
812
+ }
813
+ }
814
+ ... on PullRequest {
815
+ number
816
+ title
817
+ url
818
+ labels(first: 5) {
819
+ nodes { name }
820
+ }
821
+ assignees(first: 5) {
822
+ nodes { login }
823
+ }
824
+ }
825
+ ... on DraftIssue {
826
+ title
827
+ }
828
+ }
829
+ }
830
+ }
831
+ }
832
+ `;
833
+ const userQuery = `
834
+ ${projectFragment}
835
+ query($owner: String!, $number: Int!) {
836
+ user(login: $owner) {
837
+ projectV2(number: $number) {
838
+ ...ProjectData
839
+ }
840
+ }
841
+ }
842
+ `;
843
+ const repoQuery = `
844
+ ${projectFragment}
845
+ query($owner: String!, $repo: String!, $number: Int!) {
846
+ repository(owner: $owner, name: $repo) {
847
+ projectV2(number: $number) {
848
+ ...ProjectData
849
+ }
850
+ }
851
+ }
852
+ `;
853
+ const orgQuery = `
854
+ ${projectFragment}
855
+ query($owner: String!, $number: Int!) {
856
+ organization(login: $owner) {
857
+ projectV2(number: $number) {
858
+ ...ProjectData
859
+ }
860
+ }
861
+ }
862
+ `;
863
+ let project = null;
864
+ let source = "";
865
+ try {
866
+ const response = await this.client.graphqlWithAuth(userQuery, {
867
+ owner,
868
+ number: projectNumber
869
+ });
870
+ if (response.user?.projectV2) {
871
+ project = response.user.projectV2;
872
+ source = "user";
873
+ }
874
+ } catch {
875
+ logger.debug("User project not found, trying repository...", { module: "GitHub" });
876
+ }
877
+ if (!project && repo) {
878
+ try {
879
+ const response = await this.client.graphqlWithAuth(repoQuery, {
880
+ owner,
881
+ repo,
882
+ number: projectNumber
883
+ });
884
+ if (response.repository?.projectV2) {
885
+ project = response.repository.projectV2;
886
+ source = "repository";
887
+ }
888
+ } catch {
889
+ logger.debug("Repository project not found, trying organization...", {
890
+ module: "GitHub"
891
+ });
892
+ }
893
+ }
894
+ if (!project) {
895
+ try {
896
+ const response = await this.client.graphqlWithAuth(orgQuery, {
897
+ owner,
898
+ number: projectNumber
899
+ });
900
+ if (response.organization?.projectV2) {
901
+ project = response.organization.projectV2;
902
+ source = "organization";
903
+ }
904
+ } catch {
905
+ logger.debug("Organization project not found", { module: "GitHub" });
906
+ }
907
+ }
908
+ if (!project) {
909
+ logger.warning("Project not found", { module: "GitHub", entityId: projectNumber });
910
+ return null;
911
+ }
912
+ const statusField = project.fields.nodes.find(
913
+ (f) => f.name === "Status" && f.options !== void 0 && f.options.length > 0
914
+ );
915
+ if (!statusField?.id || !statusField.options) {
916
+ logger.warning("Status field not found in project", {
917
+ module: "GitHub",
918
+ entityId: projectNumber
919
+ });
920
+ return null;
921
+ }
922
+ const statusOptions = statusField.options.map((opt) => ({
923
+ id: opt.id,
924
+ name: opt.name,
925
+ color: opt.color
926
+ }));
927
+ const columnMap = /* @__PURE__ */ new Map();
928
+ for (const opt of statusOptions) {
929
+ columnMap.set(opt.name, []);
930
+ }
931
+ columnMap.set("No Status", []);
932
+ for (const item of project.items.nodes) {
933
+ const statusValue = item.fieldValues.nodes.find((fv) => fv.field?.name === "Status");
934
+ const status = statusValue?.name ?? "No Status";
935
+ const content = item.content;
936
+ const projectItem = {
937
+ id: item.id,
938
+ title: content?.title ?? "Draft Issue",
939
+ url: content?.url ?? "",
940
+ type: item.type,
941
+ status,
942
+ number: content?.number,
943
+ labels: content?.labels?.nodes.map((l) => l.name) ?? [],
944
+ assignees: content?.assignees?.nodes.map((a) => a.login) ?? [],
945
+ createdAt: item.createdAt,
946
+ updatedAt: item.updatedAt
947
+ };
948
+ const column = columnMap.get(status);
949
+ if (column) {
950
+ column.push(projectItem);
951
+ } else {
952
+ columnMap.get("No Status")?.push(projectItem);
953
+ }
954
+ }
955
+ const columns = [];
956
+ for (const opt of statusOptions) {
957
+ const items = columnMap.get(opt.name) ?? [];
958
+ columns.push({
959
+ status: opt.name,
960
+ statusOptionId: opt.id,
961
+ items
962
+ });
963
+ }
964
+ const noStatusItems = columnMap.get("No Status") ?? [];
965
+ if (noStatusItems.length > 0) {
966
+ columns.push({
967
+ status: "No Status",
968
+ statusOptionId: "",
969
+ items: noStatusItems
970
+ });
971
+ }
972
+ const totalItems = project.items.nodes.length;
973
+ logger.info("Fetched Kanban board", {
974
+ module: "GitHub",
975
+ entityId: projectNumber,
976
+ context: { columns: columns.length, items: totalItems, source }
977
+ });
978
+ return {
979
+ projectId: project.id,
980
+ projectNumber,
981
+ projectTitle: project.title,
982
+ statusFieldId: statusField.id,
983
+ statusOptions,
984
+ columns,
985
+ totalItems
986
+ };
987
+ }
988
+ async moveProjectItem(projectId, itemId, statusFieldId, statusOptionId) {
989
+ if (!this.client.graphqlWithAuth) {
990
+ return { success: false, error: "GraphQL not available - no token" };
991
+ }
992
+ try {
993
+ const mutation = `
994
+ mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
995
+ updateProjectV2ItemFieldValue(
996
+ input: {
997
+ projectId: $projectId
998
+ itemId: $itemId
999
+ fieldId: $fieldId
1000
+ value: { singleSelectOptionId: $optionId }
1001
+ }
1002
+ ) {
1003
+ projectV2Item {
1004
+ id
1005
+ }
1006
+ }
1007
+ }
1008
+ `;
1009
+ await this.client.graphqlWithAuth(mutation, {
1010
+ projectId,
1011
+ itemId,
1012
+ fieldId: statusFieldId,
1013
+ optionId: statusOptionId
1014
+ });
1015
+ logger.info("Moved project item", {
1016
+ module: "GitHub",
1017
+ entityId: itemId,
1018
+ context: { targetStatus: statusOptionId }
1019
+ });
1020
+ return { success: true };
1021
+ } catch (error) {
1022
+ const errorMessage = error instanceof Error ? error.message : String(error);
1023
+ logger.error("Failed to move project item", {
1024
+ module: "GitHub",
1025
+ entityId: itemId,
1026
+ error: errorMessage
1027
+ });
1028
+ return { success: false, error: errorMessage };
1029
+ } finally {
1030
+ this.client.invalidateCache("kanban:");
1031
+ }
1032
+ }
1033
+ async addProjectItem(projectId, contentId) {
1034
+ if (!this.client.graphqlWithAuth) {
1035
+ return { success: false, error: "GraphQL not available - no token" };
1036
+ }
1037
+ try {
1038
+ const mutation = `
1039
+ mutation($projectId: ID!, $contentId: ID!) {
1040
+ addProjectV2ItemById(input: { projectId: $projectId, contentId: $contentId }) {
1041
+ item {
1042
+ id
1043
+ }
1044
+ }
1045
+ }
1046
+ `;
1047
+ const response = await this.client.graphqlWithAuth(mutation, {
1048
+ projectId,
1049
+ contentId
1050
+ });
1051
+ const itemId = response.addProjectV2ItemById?.item?.id;
1052
+ logger.info("Added item to project", {
1053
+ module: "GitHub",
1054
+ context: { projectId, contentId, itemId }
1055
+ });
1056
+ return { success: true, itemId };
1057
+ } catch (error) {
1058
+ const errorMessage = error instanceof Error ? error.message : String(error);
1059
+ logger.error("Failed to add item to project", {
1060
+ module: "GitHub",
1061
+ context: { projectId, contentId },
1062
+ error: errorMessage
1063
+ });
1064
+ return { success: false, error: errorMessage };
1065
+ } finally {
1066
+ this.client.invalidateCache("kanban:");
1067
+ }
1068
+ }
1069
+ };
1070
+
1071
+ // src/github/github-integration/milestones.ts
1072
+ var MilestonesManager = class {
1073
+ constructor(client) {
1074
+ this.client = client;
1075
+ }
1076
+ client;
1077
+ async getMilestones(owner, repo, state = "open", limit = 20) {
1078
+ if (!this.client.octokit) {
1079
+ return [];
1080
+ }
1081
+ const cacheKey = `milestones:${owner}:${repo}:${state}:${String(limit)}`;
1082
+ const cached = this.client.getCached(cacheKey);
1083
+ if (cached) return cached;
1084
+ try {
1085
+ const response = await this.client.octokit.issues.listMilestones({
1086
+ owner,
1087
+ repo,
1088
+ state,
1089
+ per_page: limit,
1090
+ sort: "due_on",
1091
+ direction: "asc"
1092
+ });
1093
+ const result = response.data.map((ms) => ({
1094
+ number: ms.number,
1095
+ title: ms.title,
1096
+ description: ms.description ?? null,
1097
+ state: ms.state === "open" ? "open" : "closed",
1098
+ url: ms.html_url,
1099
+ dueOn: ms.due_on ?? null,
1100
+ openIssues: ms.open_issues,
1101
+ closedIssues: ms.closed_issues,
1102
+ createdAt: ms.created_at,
1103
+ updatedAt: ms.updated_at,
1104
+ creator: ms.creator?.login ?? null
1105
+ }));
1106
+ this.client.setCache(cacheKey, result);
1107
+ return result;
1108
+ } catch (error) {
1109
+ logger.error("Failed to get milestones", {
1110
+ module: "GitHub",
1111
+ error: error instanceof Error ? error.message : String(error)
1112
+ });
1113
+ return [];
1114
+ }
1115
+ }
1116
+ async getMilestone(owner, repo, milestoneNumber) {
1117
+ if (!this.client.octokit) {
1118
+ return null;
1119
+ }
1120
+ const cacheKey = `milestone:${owner}:${repo}:${String(milestoneNumber)}`;
1121
+ const cached = this.client.getCached(cacheKey);
1122
+ if (cached !== void 0) return cached;
1123
+ try {
1124
+ const response = await this.client.octokit.issues.getMilestone({
1125
+ owner,
1126
+ repo,
1127
+ milestone_number: milestoneNumber
1128
+ });
1129
+ const ms = response.data;
1130
+ const milestone = {
1131
+ number: ms.number,
1132
+ title: ms.title,
1133
+ description: ms.description ?? null,
1134
+ state: ms.state === "open" ? "open" : "closed",
1135
+ url: ms.html_url,
1136
+ dueOn: ms.due_on ?? null,
1137
+ openIssues: ms.open_issues,
1138
+ closedIssues: ms.closed_issues,
1139
+ createdAt: ms.created_at,
1140
+ updatedAt: ms.updated_at,
1141
+ creator: ms.creator?.login ?? null
1142
+ };
1143
+ this.client.setCache(cacheKey, milestone);
1144
+ return milestone;
1145
+ } catch (error) {
1146
+ logger.error("Failed to get milestone", {
1147
+ module: "GitHub",
1148
+ entityId: milestoneNumber,
1149
+ error: error instanceof Error ? error.message : String(error)
1150
+ });
1151
+ return null;
1152
+ }
1153
+ }
1154
+ async createMilestone(owner, repo, title, description, dueOn) {
1155
+ if (!this.client.octokit) {
1156
+ logger.error("Cannot create milestone: GitHub API not available", {
1157
+ module: "GitHub"
1158
+ });
1159
+ return null;
1160
+ }
1161
+ try {
1162
+ const response = await this.client.octokit.issues.createMilestone({
1163
+ owner,
1164
+ repo,
1165
+ title,
1166
+ description,
1167
+ due_on: dueOn
1168
+ });
1169
+ const ms = response.data;
1170
+ logger.info("Created GitHub milestone", {
1171
+ module: "GitHub",
1172
+ entityId: ms.number,
1173
+ context: { title, owner, repo }
1174
+ });
1175
+ return {
1176
+ number: ms.number,
1177
+ title: ms.title,
1178
+ description: ms.description ?? null,
1179
+ state: ms.state === "open" ? "open" : "closed",
1180
+ url: ms.html_url,
1181
+ dueOn: ms.due_on ?? null,
1182
+ openIssues: ms.open_issues,
1183
+ closedIssues: ms.closed_issues,
1184
+ createdAt: ms.created_at,
1185
+ updatedAt: ms.updated_at,
1186
+ creator: ms.creator?.login ?? null
1187
+ };
1188
+ } catch (error) {
1189
+ logger.error("Failed to create milestone", {
1190
+ module: "GitHub",
1191
+ error: error instanceof Error ? error.message : String(error),
1192
+ context: { title, owner, repo }
1193
+ });
1194
+ return null;
1195
+ } finally {
1196
+ this.client.invalidateCache(`milestones:${owner}:${repo}`);
1197
+ this.client.invalidateCache("context:");
1198
+ }
1199
+ }
1200
+ async updateMilestone(owner, repo, milestoneNumber, updates) {
1201
+ if (!this.client.octokit) {
1202
+ logger.error("Cannot update milestone: GitHub API not available", {
1203
+ module: "GitHub"
1204
+ });
1205
+ return null;
1206
+ }
1207
+ try {
1208
+ const response = await this.client.octokit.issues.updateMilestone({
1209
+ owner,
1210
+ repo,
1211
+ milestone_number: milestoneNumber,
1212
+ title: updates.title,
1213
+ description: updates.description,
1214
+ due_on: updates.dueOn === null ? void 0 : updates.dueOn,
1215
+ state: updates.state
1216
+ });
1217
+ const ms = response.data;
1218
+ logger.info("Updated GitHub milestone", {
1219
+ module: "GitHub",
1220
+ entityId: milestoneNumber,
1221
+ context: { owner, repo, updates: Object.keys(updates) }
1222
+ });
1223
+ return {
1224
+ number: ms.number,
1225
+ title: ms.title,
1226
+ description: ms.description ?? null,
1227
+ state: ms.state === "open" ? "open" : "closed",
1228
+ url: ms.html_url,
1229
+ dueOn: ms.due_on ?? null,
1230
+ openIssues: ms.open_issues,
1231
+ closedIssues: ms.closed_issues,
1232
+ createdAt: ms.created_at,
1233
+ updatedAt: ms.updated_at,
1234
+ creator: ms.creator?.login ?? null
1235
+ };
1236
+ } catch (error) {
1237
+ logger.error("Failed to update milestone", {
1238
+ module: "GitHub",
1239
+ entityId: milestoneNumber,
1240
+ error: error instanceof Error ? error.message : String(error)
1241
+ });
1242
+ return null;
1243
+ } finally {
1244
+ this.client.invalidateCache(`milestones:${owner}:${repo}`);
1245
+ this.client.invalidateCache(`milestone:${owner}:${repo}:${String(milestoneNumber)}`);
1246
+ this.client.invalidateCache("context:");
1247
+ }
1248
+ }
1249
+ async deleteMilestone(owner, repo, milestoneNumber) {
1250
+ if (!this.client.octokit) {
1251
+ return { success: false, error: "GitHub API not available" };
1252
+ }
1253
+ try {
1254
+ await this.client.octokit.issues.deleteMilestone({
1255
+ owner,
1256
+ repo,
1257
+ milestone_number: milestoneNumber
1258
+ });
1259
+ logger.info("Deleted GitHub milestone", {
1260
+ module: "GitHub",
1261
+ entityId: milestoneNumber,
1262
+ context: { owner, repo }
1263
+ });
1264
+ return { success: true };
1265
+ } catch (error) {
1266
+ const errorMessage = error instanceof Error ? error.message : String(error);
1267
+ logger.error("Failed to delete milestone", {
1268
+ module: "GitHub",
1269
+ entityId: milestoneNumber,
1270
+ error: errorMessage
1271
+ });
1272
+ return { success: false, error: errorMessage };
1273
+ } finally {
1274
+ this.client.invalidateCache(`milestones:${owner}:${repo}`);
1275
+ this.client.invalidateCache(`milestone:${owner}:${repo}:${String(milestoneNumber)}`);
1276
+ this.client.invalidateCache("context:");
1277
+ }
1278
+ }
1279
+ };
1280
+
1281
+ // src/github/github-integration/insights.ts
1282
+ var InsightsManager = class {
1283
+ constructor(client) {
1284
+ this.client = client;
1285
+ }
1286
+ client;
1287
+ async getRepoStats(owner, repo) {
1288
+ if (!this.client.octokit) {
1289
+ return null;
1290
+ }
1291
+ const cacheKey = `repostats:${owner}:${repo}`;
1292
+ const cached = this.client.getCachedWithTtl(cacheKey, TRAFFIC_CACHE_TTL_MS);
1293
+ if (cached) return cached;
1294
+ try {
1295
+ const response = await this.client.octokit.repos.get({ owner, repo });
1296
+ const data = response.data;
1297
+ const result = {
1298
+ stars: data.stargazers_count,
1299
+ forks: data.forks_count,
1300
+ watchers: data.subscribers_count,
1301
+ openIssues: data.open_issues_count,
1302
+ size: data.size,
1303
+ defaultBranch: data.default_branch
1304
+ };
1305
+ this.client.setCache(cacheKey, result);
1306
+ return result;
1307
+ } catch (error) {
1308
+ logger.error("Failed to get repo stats", {
1309
+ module: "GitHub",
1310
+ error: error instanceof Error ? error.message : String(error),
1311
+ context: { owner, repo }
1312
+ });
1313
+ return null;
1314
+ }
1315
+ }
1316
+ async getTrafficData(owner, repo) {
1317
+ if (!this.client.octokit) {
1318
+ return null;
1319
+ }
1320
+ const cacheKey = `traffic:${owner}:${repo}`;
1321
+ const cached = this.client.getCachedWithTtl(cacheKey, TRAFFIC_CACHE_TTL_MS);
1322
+ if (cached) return cached;
1323
+ try {
1324
+ const [clonesRes, viewsRes] = await Promise.all([
1325
+ this.client.octokit.rest.repos.getClones({ owner, repo }),
1326
+ this.client.octokit.rest.repos.getViews({ owner, repo })
1327
+ ]);
1328
+ const clonesDays = clonesRes.data.clones?.length ?? 0;
1329
+ const viewsDays = viewsRes.data.views?.length ?? 0;
1330
+ const result = {
1331
+ clones: {
1332
+ total: clonesRes.data.count,
1333
+ unique: clonesRes.data.uniques,
1334
+ dailyAvg: clonesDays > 0 ? Math.round(clonesRes.data.count / clonesDays) : 0
1335
+ },
1336
+ views: {
1337
+ total: viewsRes.data.count,
1338
+ unique: viewsRes.data.uniques,
1339
+ dailyAvg: viewsDays > 0 ? Math.round(viewsRes.data.count / viewsDays) : 0
1340
+ },
1341
+ period: "14 days"
1342
+ };
1343
+ this.client.setCache(cacheKey, result);
1344
+ return result;
1345
+ } catch (error) {
1346
+ logger.error("Failed to get traffic data", {
1347
+ module: "GitHub",
1348
+ error: error instanceof Error ? error.message : String(error),
1349
+ context: { owner, repo }
1350
+ });
1351
+ return null;
1352
+ }
1353
+ }
1354
+ async getTopReferrers(owner, repo, limit = 5) {
1355
+ if (!this.client.octokit) {
1356
+ return [];
1357
+ }
1358
+ const cacheKey = `referrers:${owner}:${repo}`;
1359
+ const cached = this.client.getCachedWithTtl(cacheKey, TRAFFIC_CACHE_TTL_MS);
1360
+ if (cached) return cached.slice(0, limit);
1361
+ try {
1362
+ const response = await this.client.octokit.rest.repos.getTopReferrers({ owner, repo });
1363
+ const result = response.data.map((r) => ({
1364
+ referrer: r.referrer,
1365
+ count: r.count,
1366
+ uniques: r.uniques
1367
+ }));
1368
+ this.client.setCache(cacheKey, result);
1369
+ return result.slice(0, limit);
1370
+ } catch (error) {
1371
+ logger.error("Failed to get top referrers", {
1372
+ module: "GitHub",
1373
+ error: error instanceof Error ? error.message : String(error),
1374
+ context: { owner, repo }
1375
+ });
1376
+ return [];
1377
+ }
1378
+ }
1379
+ async getPopularPaths(owner, repo, limit = 5) {
1380
+ if (!this.client.octokit) {
1381
+ return [];
1382
+ }
1383
+ const cacheKey = `paths:${owner}:${repo}`;
1384
+ const cached = this.client.getCachedWithTtl(cacheKey, TRAFFIC_CACHE_TTL_MS);
1385
+ if (cached) return cached.slice(0, limit);
1386
+ try {
1387
+ const response = await this.client.octokit.rest.repos.getTopPaths({ owner, repo });
1388
+ const result = response.data.map((p) => ({
1389
+ path: p.path,
1390
+ title: p.title,
1391
+ count: p.count,
1392
+ uniques: p.uniques
1393
+ }));
1394
+ this.client.setCache(cacheKey, result);
1395
+ return result.slice(0, limit);
1396
+ } catch (error) {
1397
+ logger.error("Failed to get popular paths", {
1398
+ module: "GitHub",
1399
+ error: error instanceof Error ? error.message : String(error),
1400
+ context: { owner, repo }
1401
+ });
1402
+ return [];
1403
+ }
1404
+ }
1405
+ };
1406
+
1407
+ // src/github/github-integration/repository.ts
1408
+ var RepositoryManager = class {
1409
+ constructor(client) {
1410
+ this.client = client;
1411
+ }
1412
+ client;
1413
+ async getRepoInfo() {
1414
+ try {
1415
+ const branchResult = await this.client.git.branch();
1416
+ const branch = branchResult.current || null;
1417
+ const remotes = await this.client.git.getRemotes(true);
1418
+ const origin = remotes.find((r) => r.name === "origin");
1419
+ const remoteUrl = origin?.refs?.fetch || null;
1420
+ const { owner, repo } = this.parseRemoteUrl(remoteUrl);
1421
+ const repoInfo = { owner, repo, branch, remoteUrl };
1422
+ this.client.cachedRepoInfo = repoInfo;
1423
+ return repoInfo;
1424
+ } catch (error) {
1425
+ logger.debug("Failed to get repo info (may not be a git repo)", {
1426
+ module: "GitHub",
1427
+ error: error instanceof Error ? error.message : String(error)
1428
+ });
1429
+ return { owner: null, repo: null, branch: null, remoteUrl: null };
1430
+ }
1431
+ }
1432
+ getCachedRepoInfo() {
1433
+ return this.client.cachedRepoInfo;
1434
+ }
1435
+ setCachedRepoInfo(info) {
1436
+ this.client.cachedRepoInfo = info;
1437
+ }
1438
+ parseRemoteUrl(remoteUrl) {
1439
+ if (!remoteUrl) return { owner: null, repo: null };
1440
+ if (remoteUrl.startsWith("git@github.com:")) {
1441
+ const pathPart = remoteUrl.replace("git@github.com:", "").replace(".git", "");
1442
+ const parts = pathPart.split("/");
1443
+ if (parts.length >= 2) {
1444
+ return { owner: parts[0] ?? null, repo: parts[1] ?? null };
1445
+ }
1446
+ }
1447
+ try {
1448
+ const url = new URL(remoteUrl);
1449
+ if (url.hostname === "github.com") {
1450
+ const path = url.pathname.replace(".git", "").replace(/^\//, "");
1451
+ const parts = path.split("/");
1452
+ if (parts.length >= 2) {
1453
+ return { owner: parts[0] ?? null, repo: parts[1] ?? null };
1454
+ }
1455
+ }
1456
+ } catch {
1457
+ }
1458
+ return { owner: null, repo: null };
1459
+ }
1460
+ async getWorkflowRuns(owner, repo, limit = 10) {
1461
+ if (!this.client.octokit) {
1462
+ logger.debug("GitHub API not available - no token", { module: "GitHub" });
1463
+ return [];
1464
+ }
1465
+ const cacheKey = `workflows:${owner}:${repo}:${String(limit)}`;
1466
+ const cached = this.client.getCached(cacheKey);
1467
+ if (cached) return cached;
1468
+ try {
1469
+ const response = await this.client.octokit.rest.actions.listWorkflowRunsForRepo({
1470
+ owner,
1471
+ repo,
1472
+ per_page: limit
1473
+ });
1474
+ const result = response.data.workflow_runs.map((run) => ({
1475
+ id: run.id,
1476
+ name: run.name ?? "Unknown Workflow",
1477
+ status: run.status,
1478
+ conclusion: run.conclusion,
1479
+ url: run.html_url,
1480
+ headBranch: run.head_branch ?? "",
1481
+ headSha: run.head_sha,
1482
+ createdAt: run.created_at,
1483
+ updatedAt: run.updated_at
1484
+ }));
1485
+ this.client.setCache(cacheKey, result);
1486
+ return result;
1487
+ } catch (error) {
1488
+ logger.error("Failed to get workflow runs", {
1489
+ module: "GitHub",
1490
+ error: error instanceof Error ? error.message : String(error)
1491
+ });
1492
+ return [];
1493
+ }
1494
+ }
1495
+ };
1496
+
1497
+ // src/github/github-integration/index.ts
1498
+ var GitHubIntegration = class {
1499
+ client;
1500
+ issuesManager;
1501
+ pullRequestsManager;
1502
+ projectsManager;
1503
+ milestonesManager;
1504
+ insightsManager;
1505
+ repositoryManager;
1506
+ constructor(workingDir = ".") {
1507
+ this.client = new GitHubClient(workingDir);
1508
+ this.issuesManager = new IssuesManager(this.client);
1509
+ this.pullRequestsManager = new PullRequestsManager(this.client);
1510
+ this.projectsManager = new ProjectsManager(this.client);
1511
+ this.milestonesManager = new MilestonesManager(this.client);
1512
+ this.insightsManager = new InsightsManager(this.client);
1513
+ this.repositoryManager = new RepositoryManager(this.client);
1514
+ }
1515
+ isApiAvailable() {
1516
+ return this.client.isApiAvailable();
1517
+ }
1518
+ clearCache() {
1519
+ this.client.clearCache();
1520
+ }
1521
+ async getRepoInfo() {
1522
+ return this.repositoryManager.getRepoInfo();
1523
+ }
1524
+ getCachedRepoInfo() {
1525
+ return this.repositoryManager.getCachedRepoInfo();
1526
+ }
1527
+ setCachedRepoInfo(info) {
1528
+ this.repositoryManager.setCachedRepoInfo(info);
1529
+ }
1530
+ async getIssues(owner, repo, state = "open", limit = 20) {
1531
+ return this.issuesManager.getIssues(owner, repo, state, limit);
1532
+ }
1533
+ async getIssue(owner, repo, issueNumber) {
1534
+ return this.issuesManager.getIssue(owner, repo, issueNumber);
1535
+ }
1536
+ async createIssue(owner, repo, title, body, labels, assignees, milestone) {
1537
+ return this.issuesManager.createIssue(
1538
+ owner,
1539
+ repo,
1540
+ title,
1541
+ body,
1542
+ labels,
1543
+ assignees,
1544
+ milestone
1545
+ );
1546
+ }
1547
+ async closeIssue(owner, repo, issueNumber, comment) {
1548
+ return this.issuesManager.closeIssue(owner, repo, issueNumber, comment);
1549
+ }
1550
+ async getPullRequests(owner, repo, state = "open", limit = 20) {
1551
+ return this.pullRequestsManager.getPullRequests(owner, repo, state, limit);
1552
+ }
1553
+ async getPullRequest(owner, repo, prNumber) {
1554
+ return this.pullRequestsManager.getPullRequest(owner, repo, prNumber);
1555
+ }
1556
+ async getReviews(owner, repo, prNumber) {
1557
+ return this.pullRequestsManager.getReviews(owner, repo, prNumber);
1558
+ }
1559
+ async getReviewComments(owner, repo, prNumber) {
1560
+ return this.pullRequestsManager.getReviewComments(owner, repo, prNumber);
1561
+ }
1562
+ async getCopilotReviewSummary(owner, repo, prNumber) {
1563
+ return this.pullRequestsManager.getCopilotReviewSummary(owner, repo, prNumber);
1564
+ }
1565
+ async getWorkflowRuns(owner, repo, limit = 10) {
1566
+ return this.repositoryManager.getWorkflowRuns(owner, repo, limit);
1567
+ }
1568
+ async getRepoContext() {
1569
+ const cached = this.client.getCached("context:repo");
1570
+ if (cached) return cached;
1571
+ const repoInfo = await this.repositoryManager.getRepoInfo();
1572
+ const context = {
1573
+ repoName: repoInfo.repo,
1574
+ branch: repoInfo.branch,
1575
+ commit: null,
1576
+ remoteUrl: repoInfo.remoteUrl,
1577
+ projects: [],
1578
+ issues: [],
1579
+ pullRequests: [],
1580
+ workflowRuns: [],
1581
+ milestones: []
1582
+ };
1583
+ try {
1584
+ const log = await this.client.git.log({ maxCount: 1 });
1585
+ context.commit = log.latest?.hash ?? null;
1586
+ } catch {
1587
+ }
1588
+ if (repoInfo.owner && repoInfo.repo) {
1589
+ context.issues = await this.issuesManager.getIssues(
1590
+ repoInfo.owner,
1591
+ repoInfo.repo,
1592
+ "open",
1593
+ 10
1594
+ );
1595
+ context.pullRequests = await this.pullRequestsManager.getPullRequests(
1596
+ repoInfo.owner,
1597
+ repoInfo.repo,
1598
+ "open",
1599
+ 10
1600
+ );
1601
+ context.workflowRuns = await this.repositoryManager.getWorkflowRuns(
1602
+ repoInfo.owner,
1603
+ repoInfo.repo,
1604
+ 10
1605
+ );
1606
+ context.milestones = await this.milestonesManager.getMilestones(
1607
+ repoInfo.owner,
1608
+ repoInfo.repo,
1609
+ "open",
1610
+ 10
1611
+ );
1612
+ }
1613
+ this.client.setCache("context:repo", context);
1614
+ return context;
1615
+ }
1616
+ async getProjectKanban(owner, projectNumber, repo) {
1617
+ return this.projectsManager.getProjectKanban(owner, projectNumber, repo);
1618
+ }
1619
+ async moveProjectItem(projectId, itemId, statusFieldId, statusOptionId) {
1620
+ return this.projectsManager.moveProjectItem(
1621
+ projectId,
1622
+ itemId,
1623
+ statusFieldId,
1624
+ statusOptionId
1625
+ );
1626
+ }
1627
+ async addProjectItem(projectId, contentId) {
1628
+ return this.projectsManager.addProjectItem(projectId, contentId);
1629
+ }
1630
+ async getMilestones(owner, repo, state = "open", limit = 20) {
1631
+ return this.milestonesManager.getMilestones(owner, repo, state, limit);
1632
+ }
1633
+ async getMilestone(owner, repo, milestoneNumber) {
1634
+ return this.milestonesManager.getMilestone(owner, repo, milestoneNumber);
1635
+ }
1636
+ async createMilestone(owner, repo, title, description, dueOn) {
1637
+ return this.milestonesManager.createMilestone(owner, repo, title, description, dueOn);
1638
+ }
1639
+ async updateMilestone(owner, repo, milestoneNumber, updates) {
1640
+ return this.milestonesManager.updateMilestone(owner, repo, milestoneNumber, updates);
1641
+ }
1642
+ async deleteMilestone(owner, repo, milestoneNumber) {
1643
+ return this.milestonesManager.deleteMilestone(owner, repo, milestoneNumber);
1644
+ }
1645
+ async getRepoStats(owner, repo) {
1646
+ return this.insightsManager.getRepoStats(owner, repo);
1647
+ }
1648
+ async getTrafficData(owner, repo) {
1649
+ return this.insightsManager.getTrafficData(owner, repo);
1650
+ }
1651
+ async getTopReferrers(owner, repo, limit = 5) {
1652
+ return this.insightsManager.getTopReferrers(owner, repo, limit);
1653
+ }
1654
+ async getPopularPaths(owner, repo, limit = 5) {
1655
+ return this.insightsManager.getPopularPaths(owner, repo, limit);
1656
+ }
1657
+ };
1658
+
1659
+ export { ConfigurationError, ConnectionError, GitHubIntegration, MemoryJournalMcpError, QueryError, ResourceNotFoundError, ValidationError, assertNoPathTraversal, logger, matchSuggestion, resolveAuthor, validateDateFormatPattern };