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.
- package/README.md +157 -95
- package/dist/{chunk-BI4ZNSKA.js → chunk-2BJHLTYP.js} +1406 -446
- package/dist/chunk-ARLH46WS.js +1659 -0
- package/dist/{chunk-N6EBIDN7.js → chunk-SBNQ7MXZ.js} +631 -1793
- package/dist/cli.js +40 -3
- package/dist/github-integration-PDRLXKGM.js +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +3 -2
- package/dist/tools-FFFGXIKN.js +3 -0
- package/package.json +15 -14
- package/dist/tools-WPRY5MJ6.js +0 -2
|
@@ -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 };
|