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
|
@@ -1,163 +1,15 @@
|
|
|
1
1
|
import { transformAutoReturn } from './chunk-OKOVZ5QE.js';
|
|
2
|
+
import { GitHubIntegration, resolveAuthor, logger, MemoryJournalMcpError, matchSuggestion, ConfigurationError } from './chunk-ARLH46WS.js';
|
|
2
3
|
import { z, ZodError } from 'zod';
|
|
3
|
-
import { execFileSync } from 'child_process';
|
|
4
4
|
import * as vm from 'vm';
|
|
5
5
|
import { MessageChannel, Worker } from 'worker_threads';
|
|
6
6
|
import * as crypto2 from 'crypto';
|
|
7
7
|
import { fileURLToPath } from 'url';
|
|
8
8
|
import * as path from 'path';
|
|
9
|
+
import { dirname } from 'path';
|
|
10
|
+
import { performance as performance$1 } from 'perf_hooks';
|
|
11
|
+
import { open, stat, mkdir, rename, appendFile } from 'fs/promises';
|
|
9
12
|
|
|
10
|
-
// src/utils/errors/suggestions.ts
|
|
11
|
-
var GENERIC_CODES = /* @__PURE__ */ new Set(["QUERY_FAILED", "INTERNAL_ERROR", "UNKNOWN_ERROR"]);
|
|
12
|
-
var ERROR_SUGGESTIONS = [
|
|
13
|
-
// Resource not found patterns
|
|
14
|
-
{
|
|
15
|
-
pattern: /not found/i,
|
|
16
|
-
suggestion: "Verify the resource identifier and try again",
|
|
17
|
-
code: "RESOURCE_NOT_FOUND"
|
|
18
|
-
},
|
|
19
|
-
{
|
|
20
|
-
pattern: /no such table/i,
|
|
21
|
-
suggestion: "The database table does not exist. Run database initialization.",
|
|
22
|
-
code: "TABLE_NOT_FOUND"
|
|
23
|
-
},
|
|
24
|
-
// Permission / access patterns
|
|
25
|
-
{
|
|
26
|
-
pattern: /permission denied|SQLITE_READONLY/i,
|
|
27
|
-
suggestion: "Check file permissions and database access rights",
|
|
28
|
-
code: "PERMISSION_DENIED"
|
|
29
|
-
},
|
|
30
|
-
// Connection / lock patterns
|
|
31
|
-
{
|
|
32
|
-
pattern: /database is locked/i,
|
|
33
|
-
suggestion: "The database is locked by another process. Retry after a short delay.",
|
|
34
|
-
code: "CONNECTION_FAILED"
|
|
35
|
-
},
|
|
36
|
-
// Constraint patterns
|
|
37
|
-
{
|
|
38
|
-
pattern: /SQLITE_CONSTRAINT|unique constraint/i,
|
|
39
|
-
suggestion: "A uniqueness or integrity constraint was violated. Check input values.",
|
|
40
|
-
code: "VALIDATION_FAILED"
|
|
41
|
-
},
|
|
42
|
-
// Disk / space patterns
|
|
43
|
-
{
|
|
44
|
-
pattern: /disk I\/O|SQLITE_FULL/i,
|
|
45
|
-
suggestion: "The disk may be full or the filesystem is read-only."
|
|
46
|
-
},
|
|
47
|
-
// Malformed input patterns
|
|
48
|
-
{
|
|
49
|
-
pattern: /malformed|invalid json|unexpected token/i,
|
|
50
|
-
suggestion: "The input appears malformed. Check the format and try again.",
|
|
51
|
-
code: "VALIDATION_FAILED"
|
|
52
|
-
}
|
|
53
|
-
];
|
|
54
|
-
function matchSuggestion(message) {
|
|
55
|
-
for (const entry of ERROR_SUGGESTIONS) {
|
|
56
|
-
if (entry.pattern.test(message)) {
|
|
57
|
-
return { suggestion: entry.suggestion, code: entry.code };
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
return void 0;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// src/types/errors.ts
|
|
64
|
-
var MemoryJournalMcpError = class extends Error {
|
|
65
|
-
/** Module-prefixed error code */
|
|
66
|
-
code;
|
|
67
|
-
/** Error category for programmatic handling */
|
|
68
|
-
category;
|
|
69
|
-
/** Actionable suggestion for resolving the error */
|
|
70
|
-
suggestion;
|
|
71
|
-
/** Whether the operation can be retried */
|
|
72
|
-
recoverable;
|
|
73
|
-
/** Additional structured context */
|
|
74
|
-
details;
|
|
75
|
-
constructor(message, code, category, options) {
|
|
76
|
-
super(message, options?.cause ? { cause: options.cause } : void 0);
|
|
77
|
-
this.name = "MemoryJournalMcpError";
|
|
78
|
-
this.category = category;
|
|
79
|
-
this.recoverable = options?.recoverable ?? false;
|
|
80
|
-
this.details = options?.details;
|
|
81
|
-
const matched = matchSuggestion(message);
|
|
82
|
-
if (GENERIC_CODES.has(code) && matched?.code) {
|
|
83
|
-
this.code = matched.code;
|
|
84
|
-
} else {
|
|
85
|
-
this.code = code;
|
|
86
|
-
}
|
|
87
|
-
this.suggestion = options?.suggestion ?? matched?.suggestion;
|
|
88
|
-
}
|
|
89
|
-
/**
|
|
90
|
-
* Convert to a structured ErrorResponse for tool responses.
|
|
91
|
-
*/
|
|
92
|
-
toResponse() {
|
|
93
|
-
return {
|
|
94
|
-
success: false,
|
|
95
|
-
error: this.message,
|
|
96
|
-
code: this.code,
|
|
97
|
-
category: this.category,
|
|
98
|
-
suggestion: this.suggestion,
|
|
99
|
-
recoverable: this.recoverable,
|
|
100
|
-
...this.details ? { details: this.details } : {}
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
};
|
|
104
|
-
var ConnectionError = class extends MemoryJournalMcpError {
|
|
105
|
-
constructor(message, details) {
|
|
106
|
-
super(message, "CONNECTION_FAILED", "connection" /* CONNECTION */, {
|
|
107
|
-
suggestion: "Check database path and file permissions",
|
|
108
|
-
recoverable: true,
|
|
109
|
-
details
|
|
110
|
-
});
|
|
111
|
-
this.name = "ConnectionError";
|
|
112
|
-
}
|
|
113
|
-
};
|
|
114
|
-
var QueryError = class extends MemoryJournalMcpError {
|
|
115
|
-
constructor(message, details) {
|
|
116
|
-
super(message, "QUERY_FAILED", "query" /* QUERY */, {
|
|
117
|
-
suggestion: "Check query parameters and database state",
|
|
118
|
-
recoverable: false,
|
|
119
|
-
details
|
|
120
|
-
});
|
|
121
|
-
this.name = "QueryError";
|
|
122
|
-
}
|
|
123
|
-
};
|
|
124
|
-
var ValidationError = class extends MemoryJournalMcpError {
|
|
125
|
-
constructor(message, details) {
|
|
126
|
-
super(message, "VALIDATION_FAILED", "validation" /* VALIDATION */, {
|
|
127
|
-
suggestion: "Check input parameters against the tool schema",
|
|
128
|
-
recoverable: false,
|
|
129
|
-
details
|
|
130
|
-
});
|
|
131
|
-
this.name = "ValidationError";
|
|
132
|
-
}
|
|
133
|
-
};
|
|
134
|
-
var ResourceNotFoundError = class extends MemoryJournalMcpError {
|
|
135
|
-
constructor(resourceType, identifier) {
|
|
136
|
-
super(
|
|
137
|
-
`${resourceType} not found: ${identifier}`,
|
|
138
|
-
"RESOURCE_NOT_FOUND",
|
|
139
|
-
"resource" /* RESOURCE */,
|
|
140
|
-
{
|
|
141
|
-
suggestion: `Verify the ${resourceType.toLowerCase()} identifier and try again`,
|
|
142
|
-
recoverable: false,
|
|
143
|
-
details: { resourceType, identifier }
|
|
144
|
-
}
|
|
145
|
-
);
|
|
146
|
-
this.name = "ResourceNotFoundError";
|
|
147
|
-
}
|
|
148
|
-
};
|
|
149
|
-
var ConfigurationError = class extends MemoryJournalMcpError {
|
|
150
|
-
constructor(message, details) {
|
|
151
|
-
super(message, "CONFIGURATION_ERROR", "configuration" /* CONFIGURATION */, {
|
|
152
|
-
suggestion: "Check server configuration and environment variables",
|
|
153
|
-
recoverable: false,
|
|
154
|
-
details
|
|
155
|
-
});
|
|
156
|
-
this.name = "ConfigurationError";
|
|
157
|
-
}
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
// src/utils/error-helpers.ts
|
|
161
13
|
function formatZodError(error) {
|
|
162
14
|
return error.issues.map((issue) => {
|
|
163
15
|
const path2 = issue.path.length > 0 ? `${issue.path.join(".")}: ` : "";
|
|
@@ -189,78 +41,6 @@ function formatHandlerError(err) {
|
|
|
189
41
|
...matched?.suggestion ? { suggestion: matched.suggestion } : {}
|
|
190
42
|
};
|
|
191
43
|
}
|
|
192
|
-
var GIT_COMMAND_TIMEOUT_MS = 3e3;
|
|
193
|
-
var SecurityError = class extends MemoryJournalMcpError {
|
|
194
|
-
constructor(message, code) {
|
|
195
|
-
super(message, code, "validation" /* VALIDATION */, {
|
|
196
|
-
suggestion: "Check input for security violations",
|
|
197
|
-
recoverable: false
|
|
198
|
-
});
|
|
199
|
-
this.name = "SecurityError";
|
|
200
|
-
}
|
|
201
|
-
};
|
|
202
|
-
var InvalidDateFormatError = class extends SecurityError {
|
|
203
|
-
constructor(value) {
|
|
204
|
-
super(`Invalid date format pattern: '${value}'`, "INVALID_DATE_FORMAT");
|
|
205
|
-
this.name = "InvalidDateFormatError";
|
|
206
|
-
}
|
|
207
|
-
};
|
|
208
|
-
var PathTraversalError = class extends SecurityError {
|
|
209
|
-
constructor(path2) {
|
|
210
|
-
super(`Path traversal detected: '${path2}'`, "PATH_TRAVERSAL");
|
|
211
|
-
this.name = "PathTraversalError";
|
|
212
|
-
}
|
|
213
|
-
};
|
|
214
|
-
var ALLOWED_DATE_FORMATS = {
|
|
215
|
-
day: "%Y-%m-%d",
|
|
216
|
-
week: "%Y-W%W",
|
|
217
|
-
month: "%Y-%m"
|
|
218
|
-
};
|
|
219
|
-
function validateDateFormatPattern(groupBy) {
|
|
220
|
-
const format = ALLOWED_DATE_FORMATS[groupBy];
|
|
221
|
-
if (!format) {
|
|
222
|
-
throw new InvalidDateFormatError(groupBy);
|
|
223
|
-
}
|
|
224
|
-
return format;
|
|
225
|
-
}
|
|
226
|
-
function assertNoPathTraversal(filename) {
|
|
227
|
-
if (filename.includes("/") || filename.includes("\\") || filename.includes("..")) {
|
|
228
|
-
throw new PathTraversalError(filename);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
var TOKEN_PATTERNS = [
|
|
232
|
-
// GitHub personal access tokens (classic and fine-grained)
|
|
233
|
-
/ghp_[A-Za-z0-9_]{36,}/g,
|
|
234
|
-
/github_pat_[A-Za-z0-9_]{82,}/g,
|
|
235
|
-
// Authorization headers in error dumps
|
|
236
|
-
/Authorization:\s*(?:token|Bearer)\s+\S+/gi,
|
|
237
|
-
// Generic Bearer tokens
|
|
238
|
-
/Bearer\s+[A-Za-z0-9._\-~+/]+=*/gi
|
|
239
|
-
];
|
|
240
|
-
function sanitizeErrorForLogging(message) {
|
|
241
|
-
let sanitized = message;
|
|
242
|
-
for (const pattern of TOKEN_PATTERNS) {
|
|
243
|
-
pattern.lastIndex = 0;
|
|
244
|
-
sanitized = sanitized.replace(pattern, "[REDACTED]");
|
|
245
|
-
}
|
|
246
|
-
return sanitized;
|
|
247
|
-
}
|
|
248
|
-
function sanitizeAuthor(raw) {
|
|
249
|
-
return raw.replace(/[\x00-\x1f\x7f]/g, "").slice(0, 100);
|
|
250
|
-
}
|
|
251
|
-
function resolveAuthor() {
|
|
252
|
-
const envAuthor = process.env["TEAM_AUTHOR"]?.trim().replace(/"/g, "");
|
|
253
|
-
if (envAuthor) return sanitizeAuthor(envAuthor);
|
|
254
|
-
try {
|
|
255
|
-
const gitUser = execFileSync("git", ["config", "user.name"], {
|
|
256
|
-
encoding: "utf-8",
|
|
257
|
-
timeout: GIT_COMMAND_TIMEOUT_MS
|
|
258
|
-
}).trim().replace(/"/g, "");
|
|
259
|
-
if (gitUser) return sanitizeAuthor(gitUser);
|
|
260
|
-
} catch {
|
|
261
|
-
}
|
|
262
|
-
return "unknown";
|
|
263
|
-
}
|
|
264
44
|
|
|
265
45
|
// src/utils/vector-index-helpers.ts
|
|
266
46
|
function autoIndexEntry(vectorManager, entryId, content) {
|
|
@@ -270,12 +50,27 @@ function autoIndexEntry(vectorManager, entryId, content) {
|
|
|
270
50
|
}
|
|
271
51
|
|
|
272
52
|
// src/utils/github-helpers.ts
|
|
273
|
-
function resolveIssueUrl(
|
|
53
|
+
async function resolveIssueUrl(context, projectNumber, issueNumber, existingUrl) {
|
|
274
54
|
if (existingUrl) return existingUrl;
|
|
275
|
-
if (issueNumber === void 0
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
55
|
+
if (issueNumber === void 0) return void 0;
|
|
56
|
+
if (projectNumber !== void 0 && context.config?.projectRegistry) {
|
|
57
|
+
const entry = Object.entries(context.config.projectRegistry).find(
|
|
58
|
+
([_, v]) => v.project_number === projectNumber
|
|
59
|
+
);
|
|
60
|
+
if (entry) {
|
|
61
|
+
const { GitHubIntegration: GitHubIntegration2 } = await import('./github-integration-PDRLXKGM.js');
|
|
62
|
+
const targetGithub = new GitHubIntegration2(entry[1].path);
|
|
63
|
+
const repoInfo = await targetGithub.getRepoInfo();
|
|
64
|
+
if (repoInfo.owner && repoInfo.repo) {
|
|
65
|
+
return `https://github.com/${repoInfo.owner}/${repoInfo.repo}/issues/${String(issueNumber)}`;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (context.github) {
|
|
70
|
+
const cachedRepo = context.github.getCachedRepoInfo() ?? await context.github.getRepoInfo();
|
|
71
|
+
if (cachedRepo?.owner && cachedRepo?.repo) {
|
|
72
|
+
return `https://github.com/${cachedRepo.owner}/${cachedRepo.repo}/issues/${String(issueNumber)}`;
|
|
73
|
+
}
|
|
279
74
|
}
|
|
280
75
|
return void 0;
|
|
281
76
|
}
|
|
@@ -287,70 +82,6 @@ var ErrorFieldsMixin = z.object({
|
|
|
287
82
|
suggestion: z.string().optional().describe("Suggested fix for the error"),
|
|
288
83
|
details: z.record(z.string(), z.unknown()).optional().describe("Additional error context")
|
|
289
84
|
});
|
|
290
|
-
|
|
291
|
-
// src/utils/logger.ts
|
|
292
|
-
var LOG_LEVELS = {
|
|
293
|
-
debug: 7,
|
|
294
|
-
info: 6,
|
|
295
|
-
notice: 5,
|
|
296
|
-
warning: 4,
|
|
297
|
-
error: 3,
|
|
298
|
-
critical: 2
|
|
299
|
-
};
|
|
300
|
-
var Logger = class {
|
|
301
|
-
minLevel;
|
|
302
|
-
constructor(level = "info") {
|
|
303
|
-
this.minLevel = LOG_LEVELS[level];
|
|
304
|
-
}
|
|
305
|
-
shouldLog(level) {
|
|
306
|
-
return LOG_LEVELS[level] <= this.minLevel;
|
|
307
|
-
}
|
|
308
|
-
log(level, message, context) {
|
|
309
|
-
if (!this.shouldLog(level)) return;
|
|
310
|
-
const safeMessage = message.replace(/\n|\r/g, "");
|
|
311
|
-
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
312
|
-
const levelUpper = level.toUpperCase().padEnd(8);
|
|
313
|
-
const mod = context?.module ? `[${context.module.replace(/\n|\r/g, "")}]` : "";
|
|
314
|
-
const op = context?.operation ? `[${context.operation.replace(/\n|\r/g, "")}]` : "";
|
|
315
|
-
let line = `[${timestamp}] [${levelUpper}] ${mod}${op} ${safeMessage}`;
|
|
316
|
-
const extras = { ...context };
|
|
317
|
-
delete extras["module"];
|
|
318
|
-
delete extras["operation"];
|
|
319
|
-
if (extras["error"] != null && typeof extras["error"] === "string") {
|
|
320
|
-
extras["error"] = sanitizeErrorForLogging(extras["error"]);
|
|
321
|
-
}
|
|
322
|
-
if (Object.keys(extras).length > 0) {
|
|
323
|
-
line += ` ${JSON.stringify(extras).replace(/\n|\r/g, "")}`;
|
|
324
|
-
}
|
|
325
|
-
console.error(line);
|
|
326
|
-
}
|
|
327
|
-
debug(message, context) {
|
|
328
|
-
this.log("debug", message, context);
|
|
329
|
-
}
|
|
330
|
-
info(message, context) {
|
|
331
|
-
this.log("info", message, context);
|
|
332
|
-
}
|
|
333
|
-
notice(message, context) {
|
|
334
|
-
this.log("notice", message, context);
|
|
335
|
-
}
|
|
336
|
-
warning(message, context) {
|
|
337
|
-
this.log("warning", message, context);
|
|
338
|
-
}
|
|
339
|
-
error(message, context) {
|
|
340
|
-
this.log("error", message, context);
|
|
341
|
-
}
|
|
342
|
-
critical(message, context) {
|
|
343
|
-
this.log("critical", message, context);
|
|
344
|
-
}
|
|
345
|
-
setLevel(level) {
|
|
346
|
-
if (level in LOG_LEVELS) {
|
|
347
|
-
this.minLevel = LOG_LEVELS[level];
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
};
|
|
351
|
-
var rawLevel = process.env["LOG_LEVEL"] ?? "info";
|
|
352
|
-
var envLevel = rawLevel in LOG_LEVELS ? rawLevel : "info";
|
|
353
|
-
var logger = new Logger(envLevel);
|
|
354
85
|
var ENTRY_TYPES = [
|
|
355
86
|
"personal_reflection",
|
|
356
87
|
"project_decision",
|
|
@@ -412,6 +143,7 @@ var EntryOutputSchema = z.object({
|
|
|
412
143
|
var EntriesListOutputSchema = z.object({
|
|
413
144
|
entries: z.array(EntryOutputSchema).optional(),
|
|
414
145
|
count: z.number().optional(),
|
|
146
|
+
searchMode: z.string().optional(),
|
|
415
147
|
success: z.boolean().optional(),
|
|
416
148
|
error: z.string().optional()
|
|
417
149
|
}).extend(ErrorFieldsMixin.shape);
|
|
@@ -522,7 +254,7 @@ var TagsListOutputSchema = z.object({
|
|
|
522
254
|
error: z.string().optional()
|
|
523
255
|
}).extend(ErrorFieldsMixin.shape);
|
|
524
256
|
function getCoreTools(context) {
|
|
525
|
-
const { db, teamDb, vectorManager
|
|
257
|
+
const { db, teamDb, vectorManager } = context;
|
|
526
258
|
return [
|
|
527
259
|
{
|
|
528
260
|
name: "create_entry",
|
|
@@ -532,11 +264,12 @@ function getCoreTools(context) {
|
|
|
532
264
|
inputSchema: CreateEntrySchemaMcp,
|
|
533
265
|
outputSchema: CreateEntryOutputSchema,
|
|
534
266
|
annotations: { readOnlyHint: false, idempotentHint: false, openWorldHint: false },
|
|
535
|
-
handler: (params) => {
|
|
267
|
+
handler: async (params) => {
|
|
536
268
|
try {
|
|
537
269
|
const input = CreateEntrySchema.parse(params);
|
|
538
|
-
const resolvedIssueUrl = resolveIssueUrl(
|
|
539
|
-
|
|
270
|
+
const resolvedIssueUrl = await resolveIssueUrl(
|
|
271
|
+
context,
|
|
272
|
+
input.project_number,
|
|
540
273
|
input.issue_number,
|
|
541
274
|
input.issue_url
|
|
542
275
|
);
|
|
@@ -716,26 +449,199 @@ function getCoreTools(context) {
|
|
|
716
449
|
}
|
|
717
450
|
];
|
|
718
451
|
}
|
|
452
|
+
|
|
453
|
+
// src/handlers/tools/search/helpers.ts
|
|
719
454
|
var MAX_QUERY_LIMIT = 500;
|
|
455
|
+
var DEDUP_KEY_LENGTH = 200;
|
|
456
|
+
function calcPerDbLimit(limit, hasTeamDb) {
|
|
457
|
+
return hasTeamDb ? Math.min(limit * 2, MAX_QUERY_LIMIT) : limit;
|
|
458
|
+
}
|
|
459
|
+
function mergeAndDedup(personal, team, limit) {
|
|
460
|
+
const seen = /* @__PURE__ */ new Set();
|
|
461
|
+
const merged = [];
|
|
462
|
+
const all = [...personal, ...team].sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
463
|
+
for (const entry of all) {
|
|
464
|
+
const key = entry.content.slice(0, DEDUP_KEY_LENGTH);
|
|
465
|
+
if (!seen.has(key)) {
|
|
466
|
+
seen.add(key);
|
|
467
|
+
merged.push(entry);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return limit !== void 0 ? merged.slice(0, limit) : merged;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// src/handlers/tools/search/auto.ts
|
|
474
|
+
var QUESTION_PATTERNS = [
|
|
475
|
+
/^(how|what|why|when|where|who|which|can|does|is|are|was|were|did|should|could|would)\b/i,
|
|
476
|
+
/\?$/
|
|
477
|
+
];
|
|
478
|
+
var QUOTED_PHRASE_PATTERN = /"[^"]+"/;
|
|
479
|
+
function classifyQuery(query) {
|
|
480
|
+
const trimmed = query.trim();
|
|
481
|
+
if (trimmed.length === 0) {
|
|
482
|
+
return "fts";
|
|
483
|
+
}
|
|
484
|
+
if (QUOTED_PHRASE_PATTERN.test(trimmed)) {
|
|
485
|
+
return "fts";
|
|
486
|
+
}
|
|
487
|
+
const words = trimmed.split(/\s+/);
|
|
488
|
+
const wordCount = words.length;
|
|
489
|
+
if (wordCount <= 2) {
|
|
490
|
+
return "fts";
|
|
491
|
+
}
|
|
492
|
+
for (const pattern of QUESTION_PATTERNS) {
|
|
493
|
+
if (pattern.test(trimmed)) {
|
|
494
|
+
return "semantic";
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return "hybrid";
|
|
498
|
+
}
|
|
499
|
+
function resolveSearchMode(mode, query) {
|
|
500
|
+
if (mode === "auto") {
|
|
501
|
+
return { resolvedMode: classifyQuery(query), isAuto: true };
|
|
502
|
+
}
|
|
503
|
+
return { resolvedMode: mode, isAuto: false };
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// src/handlers/tools/search/fts.ts
|
|
507
|
+
function ftsSearch(query, db, teamDb, options) {
|
|
508
|
+
const hasFilters = options.projectNumber !== void 0 || options.issueNumber !== void 0 || options.prNumber !== void 0 || options.prStatus !== void 0 || options.workflowRunId !== void 0 || options.isPersonal !== void 0 || options.tags !== void 0 || options.entryType !== void 0 || options.startDate !== void 0 || options.endDate !== void 0;
|
|
509
|
+
const perDbLimit = calcPerDbLimit(options.limit, !!teamDb);
|
|
510
|
+
let personalEntries;
|
|
511
|
+
if (!query && !hasFilters) {
|
|
512
|
+
personalEntries = db.getRecentEntries(perDbLimit, options.isPersonal);
|
|
513
|
+
} else {
|
|
514
|
+
personalEntries = db.searchEntries(query || "", {
|
|
515
|
+
limit: perDbLimit,
|
|
516
|
+
isPersonal: options.isPersonal,
|
|
517
|
+
projectNumber: options.projectNumber,
|
|
518
|
+
issueNumber: options.issueNumber,
|
|
519
|
+
prNumber: options.prNumber,
|
|
520
|
+
prStatus: options.prStatus,
|
|
521
|
+
workflowRunId: options.workflowRunId,
|
|
522
|
+
tags: options.tags,
|
|
523
|
+
entryType: options.entryType,
|
|
524
|
+
startDate: options.startDate,
|
|
525
|
+
endDate: options.endDate
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
if (teamDb && options.isPersonal !== true) {
|
|
529
|
+
let teamEntries;
|
|
530
|
+
if (!query && !hasFilters) {
|
|
531
|
+
teamEntries = teamDb.getRecentEntries(perDbLimit);
|
|
532
|
+
} else {
|
|
533
|
+
teamEntries = teamDb.searchEntries(query || "", {
|
|
534
|
+
limit: perDbLimit,
|
|
535
|
+
projectNumber: options.projectNumber,
|
|
536
|
+
issueNumber: options.issueNumber,
|
|
537
|
+
prNumber: options.prNumber,
|
|
538
|
+
prStatus: options.prStatus,
|
|
539
|
+
workflowRunId: options.workflowRunId,
|
|
540
|
+
tags: options.tags,
|
|
541
|
+
entryType: options.entryType,
|
|
542
|
+
startDate: options.startDate,
|
|
543
|
+
endDate: options.endDate
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
const merged = mergeAndDedup(
|
|
547
|
+
personalEntries.map((e) => ({ ...e, source: "personal" })),
|
|
548
|
+
teamEntries.map((e) => ({ ...e, source: "team" })),
|
|
549
|
+
options.limit
|
|
550
|
+
);
|
|
551
|
+
return { entries: merged, count: merged.length };
|
|
552
|
+
}
|
|
553
|
+
return {
|
|
554
|
+
entries: personalEntries.map((e) => ({ ...e, source: "personal" })),
|
|
555
|
+
count: personalEntries.length
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// src/handlers/tools/search/hybrid.ts
|
|
560
|
+
var RRF_K = 60;
|
|
561
|
+
var OVERFETCH_MULTIPLIER = 3;
|
|
562
|
+
function computeRRFScores(rankedLists) {
|
|
563
|
+
const scores = /* @__PURE__ */ new Map();
|
|
564
|
+
for (const list of rankedLists) {
|
|
565
|
+
for (let rank = 0; rank < list.length; rank++) {
|
|
566
|
+
const entryId = list[rank];
|
|
567
|
+
if (entryId === void 0) continue;
|
|
568
|
+
const rrfScore = 1 / (RRF_K + rank + 1);
|
|
569
|
+
scores.set(entryId, (scores.get(entryId) ?? 0) + rrfScore);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return scores;
|
|
573
|
+
}
|
|
574
|
+
async function hybridSearch(query, db, vectorManager, options) {
|
|
575
|
+
const overfetchLimit = Math.min(options.limit * OVERFETCH_MULTIPLIER, 500);
|
|
576
|
+
const [ftsResults, semanticResults] = await Promise.all([
|
|
577
|
+
// FTS5 search
|
|
578
|
+
Promise.resolve(
|
|
579
|
+
db.searchEntries(query, {
|
|
580
|
+
limit: overfetchLimit,
|
|
581
|
+
isPersonal: options.isPersonal,
|
|
582
|
+
projectNumber: options.projectNumber,
|
|
583
|
+
issueNumber: options.issueNumber,
|
|
584
|
+
prNumber: options.prNumber,
|
|
585
|
+
prStatus: options.prStatus,
|
|
586
|
+
workflowRunId: options.workflowRunId,
|
|
587
|
+
tags: options.tags,
|
|
588
|
+
entryType: options.entryType,
|
|
589
|
+
startDate: options.startDate,
|
|
590
|
+
endDate: options.endDate
|
|
591
|
+
})
|
|
592
|
+
),
|
|
593
|
+
// Semantic search (returns [] if vectorManager is unavailable)
|
|
594
|
+
vectorManager ? vectorManager.search(query, overfetchLimit, 0.15) : Promise.resolve([])
|
|
595
|
+
]);
|
|
596
|
+
const ftsRanked = ftsResults.map((e) => e.id);
|
|
597
|
+
const semanticRanked = semanticResults.map((r) => r.entryId);
|
|
598
|
+
const fusionScores = computeRRFScores([ftsRanked, semanticRanked]);
|
|
599
|
+
const sortedIds = [...fusionScores.entries()].sort((a, b) => b[1] - a[1]).slice(0, options.limit).map(([id]) => id);
|
|
600
|
+
const entriesMap = db.getEntriesByIds(sortedIds);
|
|
601
|
+
const entries = [];
|
|
602
|
+
for (const id of sortedIds) {
|
|
603
|
+
const entry = entriesMap.get(id);
|
|
604
|
+
if (!entry) continue;
|
|
605
|
+
if (options.isPersonal !== void 0 && entry.isPersonal !== options.isPersonal) continue;
|
|
606
|
+
entries.push({ ...entry, source: "personal" });
|
|
607
|
+
}
|
|
608
|
+
return { entries, fusionScores };
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// src/handlers/tools/search/index.ts
|
|
720
612
|
var SearchEntriesSchema = z.object({
|
|
721
613
|
query: z.string().optional(),
|
|
614
|
+
mode: z.enum(["auto", "fts", "semantic", "hybrid"]).optional().default("auto").describe(
|
|
615
|
+
"Search strategy: auto (default, heuristic-based), fts (FTS5 keyword), semantic (vector), hybrid (RRF fusion of FTS5+vector)"
|
|
616
|
+
),
|
|
722
617
|
limit: z.number().max(MAX_QUERY_LIMIT).optional().default(10),
|
|
723
618
|
is_personal: z.boolean().optional(),
|
|
724
619
|
project_number: z.number().optional(),
|
|
725
620
|
issue_number: z.number().optional(),
|
|
726
621
|
pr_number: z.number().optional(),
|
|
727
622
|
pr_status: z.enum(["draft", "open", "merged", "closed"]).optional(),
|
|
728
|
-
workflow_run_id: z.number().optional()
|
|
623
|
+
workflow_run_id: z.number().optional(),
|
|
624
|
+
tags: z.array(z.string()).optional(),
|
|
625
|
+
entry_type: z.enum(ENTRY_TYPES).optional(),
|
|
626
|
+
start_date: z.string().regex(DATE_FORMAT_REGEX, DATE_FORMAT_MESSAGE).optional(),
|
|
627
|
+
end_date: z.string().regex(DATE_FORMAT_REGEX, DATE_FORMAT_MESSAGE).optional()
|
|
729
628
|
});
|
|
730
629
|
var SearchEntriesSchemaMcp = z.object({
|
|
731
630
|
query: z.string().optional(),
|
|
631
|
+
mode: z.string().optional().default("auto").describe(
|
|
632
|
+
"Search strategy: auto (default, heuristic-based), fts (FTS5 keyword), semantic (vector), hybrid (RRF fusion of FTS5+vector)"
|
|
633
|
+
),
|
|
732
634
|
limit: relaxedNumber().optional().default(10),
|
|
733
635
|
is_personal: z.boolean().optional(),
|
|
734
636
|
project_number: relaxedNumber().optional(),
|
|
735
637
|
issue_number: relaxedNumber().optional(),
|
|
736
638
|
pr_number: relaxedNumber().optional(),
|
|
737
639
|
pr_status: z.string().optional(),
|
|
738
|
-
workflow_run_id: relaxedNumber().optional()
|
|
640
|
+
workflow_run_id: relaxedNumber().optional(),
|
|
641
|
+
tags: z.array(z.string()).optional(),
|
|
642
|
+
entry_type: z.string().optional(),
|
|
643
|
+
start_date: z.string().optional(),
|
|
644
|
+
end_date: z.string().optional()
|
|
739
645
|
});
|
|
740
646
|
var SearchByDateRangeSchema = z.object({
|
|
741
647
|
start_date: z.string().regex(DATE_FORMAT_REGEX, DATE_FORMAT_MESSAGE),
|
|
@@ -762,17 +668,31 @@ var SearchByDateRangeSchemaMcp = z.object({
|
|
|
762
668
|
limit: relaxedNumber().optional().default(500)
|
|
763
669
|
});
|
|
764
670
|
var SemanticSearchSchema = z.object({
|
|
765
|
-
query: z.string(),
|
|
671
|
+
query: z.string().optional(),
|
|
672
|
+
entry_id: z.number().optional().describe(
|
|
673
|
+
"Find entries related to this entry ID (uses existing embedding, skips re-embedding)"
|
|
674
|
+
),
|
|
766
675
|
limit: z.number().max(MAX_QUERY_LIMIT).optional().default(10),
|
|
767
676
|
similarity_threshold: z.number().optional().default(0.25),
|
|
768
677
|
is_personal: z.boolean().optional(),
|
|
678
|
+
tags: z.array(z.string()).optional().describe("Filter results by tags"),
|
|
679
|
+
entry_type: z.enum(ENTRY_TYPES).optional().describe("Filter results by entry type"),
|
|
680
|
+
start_date: z.string().regex(DATE_FORMAT_REGEX, DATE_FORMAT_MESSAGE).optional().describe("Filter results from this date (YYYY-MM-DD)"),
|
|
681
|
+
end_date: z.string().regex(DATE_FORMAT_REGEX, DATE_FORMAT_MESSAGE).optional().describe("Filter results until this date (YYYY-MM-DD)"),
|
|
769
682
|
hint_on_empty: z.boolean().optional().default(true).describe("Include hint when no results found (default: true)")
|
|
770
683
|
});
|
|
771
684
|
var SemanticSearchSchemaMcp = z.object({
|
|
772
|
-
query: z.string(),
|
|
685
|
+
query: z.string().optional(),
|
|
686
|
+
entry_id: relaxedNumber().optional().describe(
|
|
687
|
+
"Find entries related to this entry ID (uses existing embedding, skips re-embedding)"
|
|
688
|
+
),
|
|
773
689
|
limit: relaxedNumber().optional().default(10),
|
|
774
690
|
similarity_threshold: relaxedNumber().optional().default(0.25),
|
|
775
691
|
is_personal: z.boolean().optional(),
|
|
692
|
+
tags: z.array(z.string()).optional().describe("Filter results by tags"),
|
|
693
|
+
entry_type: z.string().optional().describe("Filter results by entry type"),
|
|
694
|
+
start_date: z.string().optional().describe("Filter results from this date (YYYY-MM-DD)"),
|
|
695
|
+
end_date: z.string().optional().describe("Filter results until this date (YYYY-MM-DD)"),
|
|
776
696
|
hint_on_empty: z.boolean().optional().default(true).describe("Include hint when no results found (default: true)")
|
|
777
697
|
});
|
|
778
698
|
var SemanticEntryOutputSchema = EntryOutputSchema.extend({
|
|
@@ -780,6 +700,7 @@ var SemanticEntryOutputSchema = EntryOutputSchema.extend({
|
|
|
780
700
|
});
|
|
781
701
|
var SemanticSearchOutputSchema = z.object({
|
|
782
702
|
query: z.string().optional(),
|
|
703
|
+
entryId: z.number().optional(),
|
|
783
704
|
entries: z.array(SemanticEntryOutputSchema).optional(),
|
|
784
705
|
count: z.number().optional(),
|
|
785
706
|
hint: z.string().optional(),
|
|
@@ -800,52 +721,84 @@ function getSearchTools(context) {
|
|
|
800
721
|
{
|
|
801
722
|
name: "search_entries",
|
|
802
723
|
title: "Search Entries",
|
|
803
|
-
description: '
|
|
724
|
+
description: 'Search journal entries with auto-selecting strategy. Supports modes: auto (default \u2014 heuristic selects best strategy), fts (FTS5 keyword with phrases "exact match", prefix auth*, boolean NOT/OR/AND), semantic (vector similarity), hybrid (RRF fusion of FTS5+vector). Optional filters for GitHub Projects, Issues, PRs, and Actions.',
|
|
804
725
|
group: "search",
|
|
805
726
|
inputSchema: SearchEntriesSchemaMcp,
|
|
806
727
|
outputSchema: EntriesListOutputSchema,
|
|
807
728
|
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
808
|
-
handler: (params) => {
|
|
729
|
+
handler: async (params) => {
|
|
809
730
|
try {
|
|
810
731
|
const input = SearchEntriesSchema.parse(params);
|
|
811
|
-
const
|
|
812
|
-
const
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
732
|
+
const query = input.query || "";
|
|
733
|
+
const mode = input.mode;
|
|
734
|
+
const { resolvedMode, isAuto } = resolveSearchMode(mode, query);
|
|
735
|
+
const hasFilters = input.project_number !== void 0 || input.issue_number !== void 0 || input.pr_number !== void 0 || input.pr_status !== void 0 || input.workflow_run_id !== void 0 || input.is_personal !== void 0 || input.tags !== void 0 || input.entry_type !== void 0 || input.start_date !== void 0 || input.end_date !== void 0;
|
|
736
|
+
const effectiveMode = !query && !hasFilters ? "fts" : resolvedMode;
|
|
737
|
+
const searchOptions = {
|
|
738
|
+
limit: input.limit,
|
|
739
|
+
isPersonal: input.is_personal,
|
|
740
|
+
projectNumber: input.project_number,
|
|
741
|
+
issueNumber: input.issue_number,
|
|
742
|
+
prNumber: input.pr_number,
|
|
743
|
+
prStatus: input.pr_status,
|
|
744
|
+
workflowRunId: input.workflow_run_id,
|
|
745
|
+
tags: input.tags,
|
|
746
|
+
entryType: input.entry_type,
|
|
747
|
+
startDate: input.start_date,
|
|
748
|
+
endDate: input.end_date
|
|
749
|
+
};
|
|
750
|
+
switch (effectiveMode) {
|
|
751
|
+
case "semantic": {
|
|
752
|
+
if (!vectorManager) {
|
|
753
|
+
const result = ftsSearch(input.query, db, teamDb, searchOptions);
|
|
754
|
+
return { ...result, searchMode: "fts (fallback)" };
|
|
755
|
+
}
|
|
756
|
+
const semanticResults = await vectorManager.search(
|
|
757
|
+
query,
|
|
758
|
+
input.limit,
|
|
759
|
+
0.25
|
|
760
|
+
);
|
|
761
|
+
const entryIds = semanticResults.map((r) => r.entryId);
|
|
762
|
+
const entriesMap = db.getEntriesByIds(entryIds);
|
|
763
|
+
const entries = semanticResults.map((r) => {
|
|
764
|
+
const entry = entriesMap.get(r.entryId);
|
|
765
|
+
if (!entry) return null;
|
|
766
|
+
if (input.is_personal !== void 0 && entry.isPersonal !== input.is_personal)
|
|
767
|
+
return null;
|
|
768
|
+
return { ...entry, source: "personal" };
|
|
769
|
+
}).filter((e) => e !== null);
|
|
770
|
+
return {
|
|
771
|
+
entries,
|
|
772
|
+
count: entries.length,
|
|
773
|
+
searchMode: isAuto ? "semantic (auto)" : "semantic"
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
case "hybrid": {
|
|
777
|
+
if (!vectorManager) {
|
|
778
|
+
const result = ftsSearch(input.query, db, teamDb, searchOptions);
|
|
779
|
+
return { ...result, searchMode: "fts (fallback)" };
|
|
780
|
+
}
|
|
781
|
+
const { entries } = await hybridSearch(
|
|
782
|
+
query,
|
|
783
|
+
db,
|
|
784
|
+
vectorManager,
|
|
785
|
+
searchOptions
|
|
786
|
+
);
|
|
787
|
+
return {
|
|
788
|
+
entries,
|
|
789
|
+
count: entries.length,
|
|
790
|
+
searchMode: isAuto ? "hybrid (auto)" : "hybrid"
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
case "fts":
|
|
794
|
+
default: {
|
|
795
|
+
const result = ftsSearch(input.query, db, teamDb, searchOptions);
|
|
796
|
+
return {
|
|
797
|
+
...result,
|
|
798
|
+
searchMode: isAuto ? "fts (auto)" : "fts"
|
|
799
|
+
};
|
|
840
800
|
}
|
|
841
|
-
const merged = mergeAndDedup(
|
|
842
|
-
personalEntries.map((e) => ({ ...e, source: "personal" })),
|
|
843
|
-
teamEntries.map((e) => ({ ...e, source: "team" })),
|
|
844
|
-
input.limit
|
|
845
|
-
);
|
|
846
|
-
return { entries: merged, count: merged.length };
|
|
847
801
|
}
|
|
848
|
-
return { entries: personalEntries, count: personalEntries.length };
|
|
849
802
|
} catch (err) {
|
|
850
803
|
return formatHandlerError(err);
|
|
851
804
|
}
|
|
@@ -913,7 +866,7 @@ function getSearchTools(context) {
|
|
|
913
866
|
{
|
|
914
867
|
name: "semantic_search",
|
|
915
868
|
title: "Semantic Search",
|
|
916
|
-
description: "Perform semantic/vector search on journal entries using AI embeddings",
|
|
869
|
+
description: "Perform semantic/vector search on journal entries using AI embeddings. Supports find-related-by-ID (entry_id) and metadata filters (tags, entry_type, date range).",
|
|
917
870
|
group: "search",
|
|
918
871
|
inputSchema: SemanticSearchSchemaMcp,
|
|
919
872
|
outputSchema: SemanticSearchOutputSchema,
|
|
@@ -921,6 +874,18 @@ function getSearchTools(context) {
|
|
|
921
874
|
handler: async (params) => {
|
|
922
875
|
try {
|
|
923
876
|
const input = SemanticSearchSchema.parse(params);
|
|
877
|
+
if (!input.query && input.entry_id === void 0) {
|
|
878
|
+
return {
|
|
879
|
+
success: false,
|
|
880
|
+
error: "Either query or entry_id must be provided",
|
|
881
|
+
code: "VALIDATION_ERROR",
|
|
882
|
+
category: "validation",
|
|
883
|
+
suggestion: "Provide a text query for semantic search, or an entry_id to find related entries",
|
|
884
|
+
recoverable: true,
|
|
885
|
+
entries: [],
|
|
886
|
+
count: 0
|
|
887
|
+
};
|
|
888
|
+
}
|
|
924
889
|
if (!vectorManager) {
|
|
925
890
|
return {
|
|
926
891
|
success: false,
|
|
@@ -934,11 +899,20 @@ function getSearchTools(context) {
|
|
|
934
899
|
count: 0
|
|
935
900
|
};
|
|
936
901
|
}
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
902
|
+
let results;
|
|
903
|
+
if (input.entry_id !== void 0) {
|
|
904
|
+
results = await vectorManager.searchByEntryId(
|
|
905
|
+
input.entry_id,
|
|
906
|
+
input.limit ?? 10,
|
|
907
|
+
input.similarity_threshold ?? 0.25
|
|
908
|
+
);
|
|
909
|
+
} else {
|
|
910
|
+
results = await vectorManager.search(
|
|
911
|
+
input.query ?? "",
|
|
912
|
+
input.limit ?? 10,
|
|
913
|
+
input.similarity_threshold ?? 0.25
|
|
914
|
+
);
|
|
915
|
+
}
|
|
942
916
|
const entryIds = results.map((r) => r.entryId);
|
|
943
917
|
const entriesMap = db.getEntriesByIds(entryIds);
|
|
944
918
|
const entries = results.map((r) => {
|
|
@@ -946,6 +920,22 @@ function getSearchTools(context) {
|
|
|
946
920
|
if (!entry) return null;
|
|
947
921
|
if (input.is_personal !== void 0 && entry.isPersonal !== input.is_personal)
|
|
948
922
|
return null;
|
|
923
|
+
if (input.tags && input.tags.length > 0) {
|
|
924
|
+
const entryTags = db.getTagsForEntry(entry.id);
|
|
925
|
+
if (!input.tags.some((t) => entryTags.includes(t))) return null;
|
|
926
|
+
}
|
|
927
|
+
if (input.entry_type && entry.entryType !== input.entry_type)
|
|
928
|
+
return null;
|
|
929
|
+
if (input.start_date) {
|
|
930
|
+
const entryDate = entry.timestamp.split("T")[0] ?? "";
|
|
931
|
+
if (entryDate < input.start_date) return null;
|
|
932
|
+
}
|
|
933
|
+
if (input.end_date) {
|
|
934
|
+
const entryDate = entry.timestamp.split("T")[0] ?? "";
|
|
935
|
+
if (entryDate > input.end_date) return null;
|
|
936
|
+
}
|
|
937
|
+
if (input.entry_id !== void 0 && entry.id === input.entry_id)
|
|
938
|
+
return null;
|
|
949
939
|
return {
|
|
950
940
|
...entry,
|
|
951
941
|
similarity: Math.round(r.score * 100) / 100
|
|
@@ -960,6 +950,7 @@ function getSearchTools(context) {
|
|
|
960
950
|
const hint = isIndexEmpty && includeHint ? "No entries in vector index. Use rebuild_vector_index to index existing entries." : entries.length === 0 && includeHint ? `No entries matched your query above the similarity threshold (${String(input.similarity_threshold ?? 0.25)}). Try lowering similarity_threshold (e.g., 0.15) for broader matches.` : allNoise ? `Results may be noise \u2014 best similarity (${String(bestSimilarity)}) is below quality floor (${String(QUALITY_FLOOR2)}). Try a more specific query or raise similarity_threshold to filter weak matches.` : void 0;
|
|
961
951
|
return {
|
|
962
952
|
query: input.query,
|
|
953
|
+
...input.entry_id !== void 0 ? { entryId: input.entry_id } : {},
|
|
963
954
|
entries,
|
|
964
955
|
count: entries.length,
|
|
965
956
|
...hint !== void 0 ? { hint } : {}
|
|
@@ -999,23 +990,6 @@ function getSearchTools(context) {
|
|
|
999
990
|
}
|
|
1000
991
|
];
|
|
1001
992
|
}
|
|
1002
|
-
var DEDUP_KEY_LENGTH = 200;
|
|
1003
|
-
function calcPerDbLimit(limit, hasTeamDb) {
|
|
1004
|
-
return hasTeamDb ? Math.min(limit * 2, MAX_QUERY_LIMIT) : limit;
|
|
1005
|
-
}
|
|
1006
|
-
function mergeAndDedup(personal, team, limit) {
|
|
1007
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1008
|
-
const merged = [];
|
|
1009
|
-
const all = [...personal, ...team].sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
1010
|
-
for (const entry of all) {
|
|
1011
|
-
const key = entry.content.slice(0, DEDUP_KEY_LENGTH);
|
|
1012
|
-
if (!seen.has(key)) {
|
|
1013
|
-
seen.add(key);
|
|
1014
|
-
merged.push(entry);
|
|
1015
|
-
}
|
|
1016
|
-
}
|
|
1017
|
-
return limit !== void 0 ? merged.slice(0, limit) : merged;
|
|
1018
|
-
}
|
|
1019
993
|
var INACTIVE_THRESHOLD_DAYS = 7;
|
|
1020
994
|
var MS_PER_DAY = 864e5;
|
|
1021
995
|
var MAX_TAGS_PER_PROJECT = 5;
|
|
@@ -1806,8 +1780,8 @@ function getAdminTools(context) {
|
|
|
1806
1780
|
description: 'Merge one tag into another to consolidate similar tags (e.g., merge "phase-2" into "phase2"). The source tag is deleted after merge.',
|
|
1807
1781
|
group: "admin",
|
|
1808
1782
|
inputSchema: z.object({
|
|
1809
|
-
source_tag: z.string().optional().describe("Tag to merge from (will be deleted)"),
|
|
1810
|
-
target_tag: z.string().optional().describe("Tag to merge into (will be created if not exists)")
|
|
1783
|
+
source_tag: z.union([z.string(), z.number()]).optional().describe("Tag to merge from (will be deleted)"),
|
|
1784
|
+
target_tag: z.union([z.string(), z.number()]).optional().describe("Tag to merge into (will be created if not exists)")
|
|
1811
1785
|
}),
|
|
1812
1786
|
outputSchema: MergeTagsOutputSchema,
|
|
1813
1787
|
annotations: { readOnlyHint: false, idempotentHint: false, openWorldHint: false },
|
|
@@ -2261,8 +2235,15 @@ var CopilotReviewsOutputSchema = z.object({
|
|
|
2261
2235
|
}).extend(ErrorFieldsMixin.shape);
|
|
2262
2236
|
|
|
2263
2237
|
// src/handlers/tools/github/helpers.ts
|
|
2238
|
+
function resolveProjectNumber(context, repo, explicitProjectNumber) {
|
|
2239
|
+
if (explicitProjectNumber != null) return explicitProjectNumber;
|
|
2240
|
+
if (repo && context.config?.projectRegistry?.[repo]?.project_number != null) {
|
|
2241
|
+
return context.config.projectRegistry[repo].project_number;
|
|
2242
|
+
}
|
|
2243
|
+
return context.config?.defaultProjectNumber ?? void 0;
|
|
2244
|
+
}
|
|
2264
2245
|
async function resolveOwner(context, inputOwner, entityLabel) {
|
|
2265
|
-
if (!context.github) {
|
|
2246
|
+
if (!context.github?.isApiAvailable()) {
|
|
2266
2247
|
return {
|
|
2267
2248
|
error: true,
|
|
2268
2249
|
response: {
|
|
@@ -2270,8 +2251,9 @@ async function resolveOwner(context, inputOwner, entityLabel) {
|
|
|
2270
2251
|
error: "GitHub integration not available",
|
|
2271
2252
|
code: "CONFIGURATION_ERROR",
|
|
2272
2253
|
category: "configuration",
|
|
2273
|
-
suggestion: "Set GITHUB_TOKEN
|
|
2274
|
-
recoverable: true
|
|
2254
|
+
suggestion: "Set GITHUB_TOKEN environment variable to enable GitHub integration.",
|
|
2255
|
+
recoverable: true,
|
|
2256
|
+
requiresUserInput: true
|
|
2275
2257
|
}
|
|
2276
2258
|
};
|
|
2277
2259
|
}
|
|
@@ -2297,7 +2279,14 @@ async function resolveOwner(context, inputOwner, entityLabel) {
|
|
|
2297
2279
|
return { owner, detectedOwner, repo, github: context.github };
|
|
2298
2280
|
}
|
|
2299
2281
|
async function resolveOwnerRepo(context, input, entityLabel) {
|
|
2300
|
-
|
|
2282
|
+
let toolGithub;
|
|
2283
|
+
const registryEntry = input.repo && context.config?.projectRegistry ? context.config.projectRegistry[input.repo] : void 0;
|
|
2284
|
+
if (registryEntry) {
|
|
2285
|
+
toolGithub = new GitHubIntegration(registryEntry.path);
|
|
2286
|
+
} else if (context.github) {
|
|
2287
|
+
toolGithub = context.github;
|
|
2288
|
+
}
|
|
2289
|
+
if (!toolGithub?.isApiAvailable()) {
|
|
2301
2290
|
return {
|
|
2302
2291
|
error: true,
|
|
2303
2292
|
response: {
|
|
@@ -2305,12 +2294,16 @@ async function resolveOwnerRepo(context, input, entityLabel) {
|
|
|
2305
2294
|
error: "GitHub integration not available",
|
|
2306
2295
|
code: "CONFIGURATION_ERROR",
|
|
2307
2296
|
category: "configuration",
|
|
2308
|
-
suggestion: "Set GITHUB_TOKEN
|
|
2309
|
-
recoverable: true
|
|
2297
|
+
suggestion: "Set GITHUB_TOKEN environment variable to enable GitHub integration.",
|
|
2298
|
+
recoverable: true,
|
|
2299
|
+
requiresUserInput: true
|
|
2310
2300
|
}
|
|
2311
2301
|
};
|
|
2312
2302
|
}
|
|
2313
|
-
const repoInfo = await
|
|
2303
|
+
const repoInfo = await toolGithub.getRepoInfo();
|
|
2304
|
+
if (context.github && toolGithub !== context.github) {
|
|
2305
|
+
context.github.setCachedRepoInfo(repoInfo);
|
|
2306
|
+
}
|
|
2314
2307
|
const detectedOwner = repoInfo.owner;
|
|
2315
2308
|
const detectedRepo = repoInfo.repo;
|
|
2316
2309
|
const owner = input.owner ?? detectedOwner ?? void 0;
|
|
@@ -2333,7 +2326,7 @@ async function resolveOwnerRepo(context, input, entityLabel) {
|
|
|
2333
2326
|
}
|
|
2334
2327
|
};
|
|
2335
2328
|
}
|
|
2336
|
-
return { owner, repo, detectedOwner, detectedRepo, github:
|
|
2329
|
+
return { owner, repo, detectedOwner, detectedRepo, github: toolGithub };
|
|
2337
2330
|
}
|
|
2338
2331
|
|
|
2339
2332
|
// src/handlers/tools/github/read-tools.ts
|
|
@@ -2546,24 +2539,33 @@ function getGitHubReadTools(context) {
|
|
|
2546
2539
|
{
|
|
2547
2540
|
name: "get_github_context",
|
|
2548
2541
|
title: "Get GitHub Repository Context",
|
|
2549
|
-
description: "Get current repository context including branch, open issues, and open PRs.
|
|
2542
|
+
description: "Get current repository context including branch, open issues, and open PRs. IMPORTANT: Leave owner/repo empty to auto-detect from git, OR specify the repository name if working in a multi-project registry.",
|
|
2550
2543
|
group: "github",
|
|
2551
|
-
inputSchema: z.object({
|
|
2544
|
+
inputSchema: z.object({
|
|
2545
|
+
owner: z.string().optional().describe("Repository owner"),
|
|
2546
|
+
repo: z.string().optional().describe("Repository name (use this to switch projects dynamically)")
|
|
2547
|
+
}),
|
|
2552
2548
|
outputSchema: GitHubContextOutputSchema,
|
|
2553
2549
|
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
2554
|
-
handler: async (
|
|
2550
|
+
handler: async (params) => {
|
|
2555
2551
|
try {
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2552
|
+
const input = z.object({
|
|
2553
|
+
owner: z.string().optional(),
|
|
2554
|
+
repo: z.string().optional()
|
|
2555
|
+
}).parse(params);
|
|
2556
|
+
const resolved = await resolveOwnerRepo(
|
|
2557
|
+
context,
|
|
2558
|
+
input,
|
|
2559
|
+
"would you like the context for"
|
|
2560
|
+
);
|
|
2561
|
+
let targetGithub;
|
|
2562
|
+
if ("error" in resolved) {
|
|
2563
|
+
if (!context.github) return resolved.response;
|
|
2564
|
+
targetGithub = context.github;
|
|
2565
|
+
} else {
|
|
2566
|
+
targetGithub = resolved.github;
|
|
2565
2567
|
}
|
|
2566
|
-
const ctx = await
|
|
2568
|
+
const ctx = await targetGithub.getRepoContext();
|
|
2567
2569
|
return {
|
|
2568
2570
|
repoName: ctx.repoName,
|
|
2569
2571
|
branch: ctx.branch,
|
|
@@ -2589,29 +2591,48 @@ function getKanbanTools(context) {
|
|
|
2589
2591
|
description: "View a GitHub Project v2 as a Kanban board with items grouped by Status column. Returns all columns with their items.",
|
|
2590
2592
|
group: "github",
|
|
2591
2593
|
inputSchema: z.object({
|
|
2592
|
-
project_number: z.number().describe("GitHub Project number"),
|
|
2593
|
-
owner: z.string().optional().describe("Project owner - LEAVE EMPTY to auto-detect")
|
|
2594
|
+
project_number: z.number().optional().describe("GitHub Project number (optional if repo is registered)"),
|
|
2595
|
+
owner: z.string().optional().describe("Project owner - LEAVE EMPTY to auto-detect"),
|
|
2596
|
+
repo: z.string().optional().describe("Repository name - LEAVE EMPTY to auto-detect")
|
|
2594
2597
|
}),
|
|
2595
2598
|
outputSchema: KanbanBoardOutputSchema,
|
|
2596
2599
|
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
2597
2600
|
handler: async (params) => {
|
|
2598
2601
|
try {
|
|
2599
2602
|
const input = z.object({
|
|
2600
|
-
project_number: z.number(),
|
|
2601
|
-
owner: z.string().optional()
|
|
2603
|
+
project_number: z.number().optional(),
|
|
2604
|
+
owner: z.string().optional(),
|
|
2605
|
+
repo: z.string().optional()
|
|
2602
2606
|
}).parse(params);
|
|
2603
2607
|
const resolved = await resolveOwner(context, input.owner);
|
|
2604
2608
|
if ("error" in resolved) return resolved.response;
|
|
2605
|
-
const
|
|
2609
|
+
const effectiveRepo = input.repo ?? resolved.repo;
|
|
2610
|
+
const projectNum = resolveProjectNumber(
|
|
2611
|
+
context,
|
|
2612
|
+
effectiveRepo,
|
|
2613
|
+
input.project_number
|
|
2614
|
+
);
|
|
2615
|
+
if (projectNum === void 0) {
|
|
2616
|
+
return {
|
|
2617
|
+
success: false,
|
|
2618
|
+
error: "project_number is required and could not be resolved from registry. Please supply it explicitly.",
|
|
2619
|
+
code: "VALIDATION_ERROR",
|
|
2620
|
+
category: "validation",
|
|
2621
|
+
recoverable: true,
|
|
2622
|
+
requiresUserInput: true,
|
|
2623
|
+
instruction: 'Ask the user: "What is the GitHub Project number for this repository? (Usually found in the URL: projects/<number>)"'
|
|
2624
|
+
};
|
|
2625
|
+
}
|
|
2626
|
+
const board = await resolved.github.getProjectKanban(
|
|
2606
2627
|
resolved.owner,
|
|
2607
|
-
|
|
2608
|
-
|
|
2628
|
+
projectNum,
|
|
2629
|
+
effectiveRepo
|
|
2609
2630
|
);
|
|
2610
2631
|
if (!board) {
|
|
2611
2632
|
return {
|
|
2612
2633
|
success: false,
|
|
2613
|
-
error: `Project #${String(
|
|
2614
|
-
projectNumber:
|
|
2634
|
+
error: `Project #${String(projectNum)} not found or Status field not configured`,
|
|
2635
|
+
projectNumber: projectNum,
|
|
2615
2636
|
owner: resolved.owner,
|
|
2616
2637
|
hint: "Projects can be at user, repository, or organization level.",
|
|
2617
2638
|
code: "RESOURCE_NOT_FOUND",
|
|
@@ -2632,32 +2653,49 @@ function getKanbanTools(context) {
|
|
|
2632
2653
|
description: "Move a Kanban item to a different status column. Requires the project board to have a Status field.",
|
|
2633
2654
|
group: "github",
|
|
2634
2655
|
inputSchema: z.object({
|
|
2635
|
-
project_number: z.number().describe("GitHub Project number"),
|
|
2656
|
+
project_number: z.number().optional().describe("GitHub Project number (optional if repo is registered)"),
|
|
2636
2657
|
item_id: z.string().describe("Project item ID (from get_kanban_board)"),
|
|
2637
2658
|
target_status: z.string().describe('Target status column name (e.g., "In Progress", "Done")'),
|
|
2638
|
-
owner: z.string().optional().describe("Project owner - LEAVE EMPTY to auto-detect")
|
|
2659
|
+
owner: z.string().optional().describe("Project owner - LEAVE EMPTY to auto-detect"),
|
|
2660
|
+
repo: z.string().optional().describe("Repository name - LEAVE EMPTY to auto-detect")
|
|
2639
2661
|
}),
|
|
2640
2662
|
outputSchema: MoveKanbanItemOutputSchema,
|
|
2641
2663
|
annotations: { readOnlyHint: false, idempotentHint: true, openWorldHint: true },
|
|
2642
2664
|
handler: async (params) => {
|
|
2643
2665
|
try {
|
|
2644
2666
|
const input = z.object({
|
|
2645
|
-
project_number: z.number(),
|
|
2667
|
+
project_number: z.number().optional(),
|
|
2646
2668
|
item_id: z.string(),
|
|
2647
2669
|
target_status: z.string(),
|
|
2648
|
-
owner: z.string().optional()
|
|
2670
|
+
owner: z.string().optional(),
|
|
2671
|
+
repo: z.string().optional()
|
|
2649
2672
|
}).parse(params);
|
|
2650
2673
|
const resolved = await resolveOwner(context, input.owner);
|
|
2651
2674
|
if ("error" in resolved) return resolved.response;
|
|
2675
|
+
const effectiveRepo = input.repo ?? resolved.repo;
|
|
2676
|
+
const projectNum = resolveProjectNumber(
|
|
2677
|
+
context,
|
|
2678
|
+
effectiveRepo,
|
|
2679
|
+
input.project_number
|
|
2680
|
+
);
|
|
2681
|
+
if (projectNum === void 0) {
|
|
2682
|
+
return {
|
|
2683
|
+
success: false,
|
|
2684
|
+
error: "project_number is required and could not be resolved from registry. Please supply it explicitly.",
|
|
2685
|
+
code: "VALIDATION_ERROR",
|
|
2686
|
+
category: "validation",
|
|
2687
|
+
recoverable: true
|
|
2688
|
+
};
|
|
2689
|
+
}
|
|
2652
2690
|
const board = await resolved.github.getProjectKanban(
|
|
2653
2691
|
resolved.owner,
|
|
2654
|
-
|
|
2655
|
-
|
|
2692
|
+
projectNum,
|
|
2693
|
+
effectiveRepo
|
|
2656
2694
|
);
|
|
2657
2695
|
if (!board) {
|
|
2658
2696
|
return {
|
|
2659
2697
|
success: false,
|
|
2660
|
-
error: `Project #${String(
|
|
2698
|
+
error: `Project #${String(projectNum)} not found`,
|
|
2661
2699
|
code: "RESOURCE_NOT_FOUND",
|
|
2662
2700
|
category: "resource",
|
|
2663
2701
|
suggestion: "Verify the project number and owner.",
|
|
@@ -2688,7 +2726,7 @@ function getKanbanTools(context) {
|
|
|
2688
2726
|
success: result.success,
|
|
2689
2727
|
itemId: input.item_id,
|
|
2690
2728
|
newStatus: statusOption.name,
|
|
2691
|
-
projectNumber:
|
|
2729
|
+
projectNumber: projectNum,
|
|
2692
2730
|
message: result.success ? `Moved item to "${statusOption.name}"` : void 0,
|
|
2693
2731
|
error: result.error
|
|
2694
2732
|
};
|
|
@@ -2756,7 +2794,11 @@ function getGitHubIssueTools(context) {
|
|
|
2756
2794
|
error: "Failed to create GitHub issue. Check GITHUB_TOKEN permissions."
|
|
2757
2795
|
};
|
|
2758
2796
|
}
|
|
2759
|
-
const projectNumber =
|
|
2797
|
+
const projectNumber = resolveProjectNumber(
|
|
2798
|
+
context,
|
|
2799
|
+
resolved.repo,
|
|
2800
|
+
input.project_number
|
|
2801
|
+
);
|
|
2760
2802
|
let projectResult = void 0;
|
|
2761
2803
|
if (projectNumber !== void 0 && issue.nodeId) {
|
|
2762
2804
|
try {
|
|
@@ -2931,7 +2973,11 @@ Description: ${input.body.slice(0, 200)}${input.body.length > 200 ? "..." : ""}`
|
|
|
2931
2973
|
}
|
|
2932
2974
|
let kanbanResult;
|
|
2933
2975
|
if (input.move_to_done) {
|
|
2934
|
-
const projectNum =
|
|
2976
|
+
const projectNum = resolveProjectNumber(
|
|
2977
|
+
context,
|
|
2978
|
+
resolved.repo,
|
|
2979
|
+
input.project_number
|
|
2980
|
+
);
|
|
2935
2981
|
if (projectNum === void 0) {
|
|
2936
2982
|
kanbanResult = {
|
|
2937
2983
|
moved: false,
|
|
@@ -2990,7 +3036,11 @@ Description: ${input.body.slice(0, 200)}${input.body.length > 200 ? "..." : ""}`
|
|
|
2990
3036
|
kanbanResult = {
|
|
2991
3037
|
moved: false,
|
|
2992
3038
|
error: err instanceof Error ? err.message : String(err),
|
|
2993
|
-
projectNumber:
|
|
3039
|
+
projectNumber: resolveProjectNumber(
|
|
3040
|
+
context,
|
|
3041
|
+
resolved.repo,
|
|
3042
|
+
input.project_number
|
|
3043
|
+
)
|
|
2994
3044
|
};
|
|
2995
3045
|
}
|
|
2996
3046
|
}
|
|
@@ -3040,31 +3090,37 @@ function getGitHubMutationTools(context) {
|
|
|
3040
3090
|
}
|
|
3041
3091
|
|
|
3042
3092
|
// src/handlers/resources/shared.ts
|
|
3043
|
-
async function resolveGitHubRepo(github) {
|
|
3093
|
+
async function resolveGitHubRepo(github, config, targetRepo) {
|
|
3044
3094
|
const lastModified = (/* @__PURE__ */ new Date()).toISOString();
|
|
3045
|
-
|
|
3095
|
+
let activeGithub = github;
|
|
3096
|
+
if (targetRepo && config?.projectRegistry?.[targetRepo]) {
|
|
3097
|
+
activeGithub = new GitHubIntegration(config.projectRegistry[targetRepo].path);
|
|
3098
|
+
}
|
|
3099
|
+
if (!activeGithub) {
|
|
3100
|
+
const hasRegistry = config?.projectRegistry && Object.keys(config.projectRegistry).length > 0;
|
|
3046
3101
|
return {
|
|
3047
3102
|
data: {
|
|
3048
3103
|
error: "GitHub integration not available",
|
|
3049
|
-
hint: "Set GITHUB_TOKEN
|
|
3104
|
+
hint: hasRegistry ? "Set GITHUB_TOKEN, or assure the dynamic repo URI correctly matches a registered project." : "Set GITHUB_TOKEN securely."
|
|
3050
3105
|
},
|
|
3051
3106
|
annotations: { lastModified }
|
|
3052
3107
|
};
|
|
3053
3108
|
}
|
|
3054
|
-
const repoInfo = await
|
|
3109
|
+
const repoInfo = await activeGithub.getRepoInfo();
|
|
3055
3110
|
const owner = repoInfo.owner;
|
|
3056
3111
|
const repo = repoInfo.repo;
|
|
3057
3112
|
if (!owner || !repo) {
|
|
3113
|
+
const hasRegistry = config?.projectRegistry && Object.keys(config.projectRegistry).length > 0;
|
|
3058
3114
|
return {
|
|
3059
3115
|
data: {
|
|
3060
3116
|
error: "Could not detect repository",
|
|
3061
|
-
hint: "
|
|
3117
|
+
hint: hasRegistry ? "Use a repository-specific URI suffix (e.g., memory://github/status/{repo}) for multi-project setups, or ensure the fallback project is a valid git repository." : "Run the MCP server from a valid git repository or configure PROJECT_REGISTRY.",
|
|
3062
3118
|
...repoInfo.branch ? { branch: repoInfo.branch } : {}
|
|
3063
3119
|
},
|
|
3064
3120
|
annotations: { lastModified }
|
|
3065
3121
|
};
|
|
3066
3122
|
}
|
|
3067
|
-
return { owner, repo, branch: repoInfo.branch ?? null, lastModified, github };
|
|
3123
|
+
return { owner, repo, branch: repoInfo.branch ?? null, lastModified, github: activeGithub };
|
|
3068
3124
|
}
|
|
3069
3125
|
function isResourceError(result) {
|
|
3070
3126
|
return "data" in result;
|
|
@@ -3263,7 +3319,7 @@ function getGitHubMilestoneTools(context) {
|
|
|
3263
3319
|
"What GitHub repository should I create the milestone in?"
|
|
3264
3320
|
);
|
|
3265
3321
|
if ("error" in resolved) return resolved.response;
|
|
3266
|
-
const dueOn = input.due_on ? `${input.due_on}T08:00:00Z` : void 0;
|
|
3322
|
+
const dueOn = input.due_on ? input.due_on.includes("T") ? input.due_on : `${input.due_on}T08:00:00Z` : void 0;
|
|
3267
3323
|
const milestone = await resolved.github.createMilestone(
|
|
3268
3324
|
resolved.owner,
|
|
3269
3325
|
resolved.repo,
|
|
@@ -3324,7 +3380,7 @@ function getGitHubMilestoneTools(context) {
|
|
|
3324
3380
|
"What GitHub repository is this milestone in?"
|
|
3325
3381
|
);
|
|
3326
3382
|
if ("error" in resolved) return resolved.response;
|
|
3327
|
-
const dueOn = input.due_on ? `${input.due_on}T08:00:00Z` : void 0;
|
|
3383
|
+
const dueOn = input.due_on ? input.due_on.includes("T") ? input.due_on : `${input.due_on}T08:00:00Z` : void 0;
|
|
3328
3384
|
const milestone = await resolved.github.updateMilestone(
|
|
3329
3385
|
resolved.owner,
|
|
3330
3386
|
resolved.repo,
|
|
@@ -4037,13 +4093,15 @@ var TeamBackupsListOutputSchema = z.object({
|
|
|
4037
4093
|
error: z.string().optional()
|
|
4038
4094
|
}).extend(ErrorFieldsMixin.shape);
|
|
4039
4095
|
var TeamSemanticSearchSchema = z.object({
|
|
4040
|
-
query: z.string(),
|
|
4096
|
+
query: z.string().optional(),
|
|
4097
|
+
entry_id: z.number().optional().describe("Find entries related to this entry ID"),
|
|
4041
4098
|
limit: z.number().max(500).optional().default(10),
|
|
4042
4099
|
similarity_threshold: z.number().optional().default(0.25),
|
|
4043
4100
|
hint_on_empty: z.boolean().optional().default(true).describe("Include hint when no results found (default: true)")
|
|
4044
4101
|
});
|
|
4045
4102
|
var TeamSemanticSearchSchemaMcp = z.object({
|
|
4046
4103
|
query: z.string().optional(),
|
|
4104
|
+
entry_id: relaxedNumber().optional().describe("Find entries related to this entry ID"),
|
|
4047
4105
|
limit: relaxedNumber().optional().default(10),
|
|
4048
4106
|
similarity_threshold: relaxedNumber().optional().default(0.25),
|
|
4049
4107
|
hint_on_empty: z.boolean().optional().default(true).describe("Include hint when no results found (default: true)")
|
|
@@ -4059,6 +4117,7 @@ var TeamSemanticEntryOutputSchema = TeamEntryOutputSchema.extend({
|
|
|
4059
4117
|
});
|
|
4060
4118
|
var TeamSemanticSearchOutputSchema = z.object({
|
|
4061
4119
|
query: z.string().optional(),
|
|
4120
|
+
entryId: z.number().optional(),
|
|
4062
4121
|
entries: z.array(TeamSemanticEntryOutputSchema).optional(),
|
|
4063
4122
|
count: z.number().optional(),
|
|
4064
4123
|
hint: z.string().optional(),
|
|
@@ -4128,7 +4187,7 @@ var TeamCrossProjectInsightsOutputSchema = z.object({
|
|
|
4128
4187
|
|
|
4129
4188
|
// src/handlers/tools/team/core-tools.ts
|
|
4130
4189
|
function getTeamCoreTools(context) {
|
|
4131
|
-
const { teamDb
|
|
4190
|
+
const { teamDb } = context;
|
|
4132
4191
|
return [
|
|
4133
4192
|
{
|
|
4134
4193
|
name: "team_create_entry",
|
|
@@ -4138,15 +4197,16 @@ function getTeamCoreTools(context) {
|
|
|
4138
4197
|
inputSchema: TeamCreateEntrySchemaMcp,
|
|
4139
4198
|
outputSchema: TeamCreateOutputSchema,
|
|
4140
4199
|
annotations: { readOnlyHint: false, idempotentHint: false, openWorldHint: false },
|
|
4141
|
-
handler: (params) => {
|
|
4200
|
+
handler: async (params) => {
|
|
4142
4201
|
try {
|
|
4143
4202
|
if (!teamDb) {
|
|
4144
4203
|
return { ...TEAM_DB_ERROR_RESPONSE };
|
|
4145
4204
|
}
|
|
4146
4205
|
const input = TeamCreateEntrySchema.parse(params);
|
|
4147
4206
|
const author = input.author ?? resolveAuthor();
|
|
4148
|
-
const resolvedIssueUrl = resolveIssueUrl(
|
|
4149
|
-
|
|
4207
|
+
const resolvedIssueUrl = await resolveIssueUrl(
|
|
4208
|
+
context,
|
|
4209
|
+
input.project_number,
|
|
4150
4210
|
input.issue_number,
|
|
4151
4211
|
input.issue_url
|
|
4152
4212
|
);
|
|
@@ -5031,6 +5091,18 @@ function getTeamVectorTools(context) {
|
|
|
5031
5091
|
return { ...TEAM_DB_ERROR_RESPONSE };
|
|
5032
5092
|
}
|
|
5033
5093
|
const input = TeamSemanticSearchSchema.parse(params);
|
|
5094
|
+
if (!input.query && input.entry_id === void 0) {
|
|
5095
|
+
return {
|
|
5096
|
+
success: false,
|
|
5097
|
+
error: "Either query or entry_id must be provided",
|
|
5098
|
+
code: "VALIDATION_ERROR",
|
|
5099
|
+
category: "validation",
|
|
5100
|
+
suggestion: "Provide a text query for semantic search, or an entry_id to find related entries",
|
|
5101
|
+
recoverable: true,
|
|
5102
|
+
entries: [],
|
|
5103
|
+
count: 0
|
|
5104
|
+
};
|
|
5105
|
+
}
|
|
5034
5106
|
if (!teamVectorManager) {
|
|
5035
5107
|
return {
|
|
5036
5108
|
success: false,
|
|
@@ -5044,17 +5116,28 @@ function getTeamVectorTools(context) {
|
|
|
5044
5116
|
count: 0
|
|
5045
5117
|
};
|
|
5046
5118
|
}
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5119
|
+
let results;
|
|
5120
|
+
if (input.entry_id !== void 0) {
|
|
5121
|
+
results = await teamVectorManager.searchByEntryId(
|
|
5122
|
+
input.entry_id,
|
|
5123
|
+
input.limit ?? 10,
|
|
5124
|
+
input.similarity_threshold ?? 0.25
|
|
5125
|
+
);
|
|
5126
|
+
} else {
|
|
5127
|
+
results = await teamVectorManager.search(
|
|
5128
|
+
input.query ?? "",
|
|
5129
|
+
input.limit ?? 10,
|
|
5130
|
+
input.similarity_threshold ?? 0.25
|
|
5131
|
+
);
|
|
5132
|
+
}
|
|
5052
5133
|
const entryIds = results.map((r) => r.entryId);
|
|
5053
5134
|
const entriesMap = teamDb.getEntriesByIds(entryIds);
|
|
5054
5135
|
const authorMap = batchFetchAuthors(teamDb, entryIds);
|
|
5055
5136
|
const entries = results.map((r) => {
|
|
5056
5137
|
const entry = entriesMap.get(r.entryId);
|
|
5057
5138
|
if (!entry) return null;
|
|
5139
|
+
if (input.entry_id !== void 0 && entry.id === input.entry_id)
|
|
5140
|
+
return null;
|
|
5058
5141
|
return {
|
|
5059
5142
|
...entry,
|
|
5060
5143
|
author: authorMap.get(r.entryId) ?? null,
|
|
@@ -5069,6 +5152,7 @@ function getTeamVectorTools(context) {
|
|
|
5069
5152
|
const hint = isIndexEmpty && includeHint ? "No entries in team vector index. Use team_rebuild_vector_index to index existing entries." : entries.length === 0 && includeHint ? `No entries matched your query above the similarity threshold (${String(input.similarity_threshold ?? 0.25)}). Try lowering similarity_threshold (e.g., 0.15) for broader matches.` : allNoise ? `Results may be noise \u2014 best similarity (${String(bestSimilarity)}) is below quality floor (${String(QUALITY_FLOOR)}). Try a more specific query or raise similarity_threshold to filter weak matches.` : void 0;
|
|
5070
5153
|
return {
|
|
5071
5154
|
query: input.query,
|
|
5155
|
+
...input.entry_id !== void 0 ? { entryId: input.entry_id } : {},
|
|
5072
5156
|
entries,
|
|
5073
5157
|
count: entries.length,
|
|
5074
5158
|
...hint !== void 0 ? { hint } : {}
|
|
@@ -5306,8 +5390,10 @@ var GROUP_EXAMPLES = {
|
|
|
5306
5390
|
],
|
|
5307
5391
|
search: [
|
|
5308
5392
|
'mj.search.searchEntries({ query: "performance" })',
|
|
5393
|
+
'mj.search.searchEntries({ query: "performance", mode: "hybrid" })',
|
|
5309
5394
|
'mj.search.searchByDateRange({ start_date: "2026-03-01", end_date: "2026-03-11" })',
|
|
5310
|
-
'mj.search.semanticSearch({ query: "authentication patterns" })'
|
|
5395
|
+
'mj.search.semanticSearch({ query: "authentication patterns" })',
|
|
5396
|
+
"mj.search.semanticSearch({ entry_id: 42 })"
|
|
5311
5397
|
],
|
|
5312
5398
|
analytics: ["mj.analytics.getStatistics()", "mj.analytics.getCrossProjectInsights()"],
|
|
5313
5399
|
relationships: [
|
|
@@ -6089,12 +6175,16 @@ function createSandboxPool(mode, sandboxOptions, poolOptions) {
|
|
|
6089
6175
|
var ExecuteCodeSchema = z.object({
|
|
6090
6176
|
code: z.string().min(1),
|
|
6091
6177
|
timeout: z.number().max(3e4).optional().default(3e4),
|
|
6092
|
-
readonly: z.boolean().optional().default(false)
|
|
6178
|
+
readonly: z.boolean().optional().default(false),
|
|
6179
|
+
repo: z.string().optional()
|
|
6093
6180
|
});
|
|
6094
6181
|
var ExecuteCodeSchemaMcp = z.object({
|
|
6095
6182
|
code: z.string().describe("JavaScript code to execute in the sandbox"),
|
|
6096
6183
|
timeout: relaxedNumber().optional().default(3e4).describe("Execution timeout in ms (max 30000)"),
|
|
6097
|
-
readonly: z.boolean().optional().default(false).describe("Restrict to read-only operations")
|
|
6184
|
+
readonly: z.boolean().optional().default(false).describe("Restrict to read-only operations"),
|
|
6185
|
+
repo: z.string().optional().describe(
|
|
6186
|
+
'Target repository name to set as default context for all github/kanban tools executed in this sandbox (e.g., "memory-journal-mcp").'
|
|
6187
|
+
)
|
|
6098
6188
|
});
|
|
6099
6189
|
var securityManager = null;
|
|
6100
6190
|
var sandboxPool = null;
|
|
@@ -6139,12 +6229,13 @@ function getCodeModeTools(context) {
|
|
|
6139
6229
|
idempotentHint: false,
|
|
6140
6230
|
openWorldHint: false
|
|
6141
6231
|
},
|
|
6142
|
-
handler: (params) => {
|
|
6232
|
+
handler: async (params) => {
|
|
6143
6233
|
try {
|
|
6144
6234
|
const {
|
|
6145
6235
|
code,
|
|
6146
6236
|
timeout,
|
|
6147
|
-
readonly: readonlyMode
|
|
6237
|
+
readonly: readonlyMode,
|
|
6238
|
+
repo
|
|
6148
6239
|
} = ExecuteCodeSchema.parse(params);
|
|
6149
6240
|
const security = getSecurityManager();
|
|
6150
6241
|
const validation = security.validateCode(code);
|
|
@@ -6160,24 +6251,42 @@ function getCodeModeTools(context) {
|
|
|
6160
6251
|
error: "Rate limit exceeded (60 executions per minute)"
|
|
6161
6252
|
};
|
|
6162
6253
|
}
|
|
6163
|
-
|
|
6254
|
+
let sessionContext = context;
|
|
6255
|
+
if (repo && context.config?.projectRegistry) {
|
|
6256
|
+
const registryEntry = context.config.projectRegistry[repo];
|
|
6257
|
+
if (registryEntry) {
|
|
6258
|
+
const injectedGithub = new GitHubIntegration(registryEntry.path);
|
|
6259
|
+
try {
|
|
6260
|
+
await injectedGithub.getRepoInfo();
|
|
6261
|
+
} catch {
|
|
6262
|
+
}
|
|
6263
|
+
sessionContext = {
|
|
6264
|
+
...context,
|
|
6265
|
+
github: injectedGithub,
|
|
6266
|
+
config: {
|
|
6267
|
+
...context.config,
|
|
6268
|
+
defaultProjectNumber: registryEntry.project_number ?? context.config.defaultProjectNumber
|
|
6269
|
+
}
|
|
6270
|
+
};
|
|
6271
|
+
}
|
|
6272
|
+
}
|
|
6273
|
+
const allTools = collectNonCodeModeTools(sessionContext);
|
|
6164
6274
|
const tools = readonlyMode ? allTools.filter((t) => t.annotations.readOnlyHint === true) : allTools;
|
|
6165
6275
|
const api = createJournalApi(tools);
|
|
6166
6276
|
const bindings = api.createSandboxBindings();
|
|
6167
6277
|
const pool = getSandboxPool();
|
|
6168
|
-
|
|
6169
|
-
|
|
6170
|
-
|
|
6171
|
-
|
|
6172
|
-
|
|
6173
|
-
|
|
6174
|
-
|
|
6175
|
-
|
|
6176
|
-
|
|
6177
|
-
}
|
|
6278
|
+
const result = await pool.execute(code, bindings, timeout);
|
|
6279
|
+
if (result.success && result.result !== void 0) {
|
|
6280
|
+
const sizeCheck = security.validateResultSize(result.result);
|
|
6281
|
+
if (!sizeCheck.valid) {
|
|
6282
|
+
return {
|
|
6283
|
+
success: false,
|
|
6284
|
+
error: sizeCheck.errors.join("; "),
|
|
6285
|
+
metrics: result.metrics
|
|
6286
|
+
};
|
|
6178
6287
|
}
|
|
6179
|
-
|
|
6180
|
-
|
|
6288
|
+
}
|
|
6289
|
+
return result;
|
|
6181
6290
|
} catch (err) {
|
|
6182
6291
|
return formatHandlerError(err);
|
|
6183
6292
|
}
|
|
@@ -6186,7 +6295,836 @@ function getCodeModeTools(context) {
|
|
|
6186
6295
|
];
|
|
6187
6296
|
}
|
|
6188
6297
|
|
|
6298
|
+
// src/observability/token-estimator.ts
|
|
6299
|
+
function estimateTokens(text) {
|
|
6300
|
+
if (!text) return 0;
|
|
6301
|
+
return Math.ceil(Buffer.byteLength(text, "utf8") / 4);
|
|
6302
|
+
}
|
|
6303
|
+
function estimatePayloadTokens(payload) {
|
|
6304
|
+
if (payload === null || payload === void 0) return 0;
|
|
6305
|
+
try {
|
|
6306
|
+
return estimateTokens(JSON.stringify(payload));
|
|
6307
|
+
} catch {
|
|
6308
|
+
return 0;
|
|
6309
|
+
}
|
|
6310
|
+
}
|
|
6311
|
+
function injectTokenEstimate(payload) {
|
|
6312
|
+
if (typeof payload !== "object" || payload === null || Array.isArray(payload)) {
|
|
6313
|
+
return payload;
|
|
6314
|
+
}
|
|
6315
|
+
const obj = payload;
|
|
6316
|
+
const serialized = (() => {
|
|
6317
|
+
try {
|
|
6318
|
+
return JSON.stringify(payload);
|
|
6319
|
+
} catch {
|
|
6320
|
+
return "";
|
|
6321
|
+
}
|
|
6322
|
+
})();
|
|
6323
|
+
const tokenEstimate = estimateTokens(serialized);
|
|
6324
|
+
const existingMeta = typeof obj["_meta"] === "object" && obj["_meta"] !== null ? obj["_meta"] : {};
|
|
6325
|
+
return {
|
|
6326
|
+
...obj,
|
|
6327
|
+
_meta: {
|
|
6328
|
+
...existingMeta,
|
|
6329
|
+
tokenEstimate
|
|
6330
|
+
}
|
|
6331
|
+
};
|
|
6332
|
+
}
|
|
6333
|
+
|
|
6334
|
+
// src/observability/metrics.ts
|
|
6335
|
+
var MetricsAccumulator = class {
|
|
6336
|
+
toolData = /* @__PURE__ */ new Map();
|
|
6337
|
+
upSince = (/* @__PURE__ */ new Date()).toISOString();
|
|
6338
|
+
startTime = Date.now();
|
|
6339
|
+
/**
|
|
6340
|
+
* Record a single tool invocation result.
|
|
6341
|
+
*/
|
|
6342
|
+
record(opts) {
|
|
6343
|
+
const existing = this.toolData.get(opts.toolName);
|
|
6344
|
+
const m = existing ?? {
|
|
6345
|
+
callCount: 0,
|
|
6346
|
+
errorCount: 0,
|
|
6347
|
+
totalDurationMs: 0,
|
|
6348
|
+
totalInputTokens: 0,
|
|
6349
|
+
totalOutputTokens: 0,
|
|
6350
|
+
lastCalledAt: null
|
|
6351
|
+
};
|
|
6352
|
+
m.callCount++;
|
|
6353
|
+
m.totalDurationMs += opts.durationMs;
|
|
6354
|
+
m.totalInputTokens += opts.inputTokens;
|
|
6355
|
+
m.totalOutputTokens += opts.outputTokens;
|
|
6356
|
+
if (opts.isError) m.errorCount++;
|
|
6357
|
+
m.lastCalledAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6358
|
+
this.toolData.set(opts.toolName, m);
|
|
6359
|
+
}
|
|
6360
|
+
/**
|
|
6361
|
+
* Aggregate summary across all recorded tools.
|
|
6362
|
+
*/
|
|
6363
|
+
getSummary() {
|
|
6364
|
+
let totalCalls = 0;
|
|
6365
|
+
let totalErrors = 0;
|
|
6366
|
+
let totalDurationMs = 0;
|
|
6367
|
+
let totalInputTokens = 0;
|
|
6368
|
+
let totalOutputTokens = 0;
|
|
6369
|
+
for (const m of this.toolData.values()) {
|
|
6370
|
+
totalCalls += m.callCount;
|
|
6371
|
+
totalErrors += m.errorCount;
|
|
6372
|
+
totalDurationMs += m.totalDurationMs;
|
|
6373
|
+
totalInputTokens += m.totalInputTokens;
|
|
6374
|
+
totalOutputTokens += m.totalOutputTokens;
|
|
6375
|
+
}
|
|
6376
|
+
return {
|
|
6377
|
+
totalCalls,
|
|
6378
|
+
totalErrors,
|
|
6379
|
+
totalDurationMs,
|
|
6380
|
+
totalInputTokens,
|
|
6381
|
+
totalOutputTokens,
|
|
6382
|
+
upSince: this.upSince,
|
|
6383
|
+
toolBreakdown: Object.fromEntries(this.toolData)
|
|
6384
|
+
};
|
|
6385
|
+
}
|
|
6386
|
+
/**
|
|
6387
|
+
* Per-tool token usage breakdown, sorted by total output tokens desc.
|
|
6388
|
+
*/
|
|
6389
|
+
getTokenBreakdown() {
|
|
6390
|
+
return Array.from(this.toolData.entries()).map(([toolName, m]) => ({
|
|
6391
|
+
toolName,
|
|
6392
|
+
inputTokens: m.totalInputTokens,
|
|
6393
|
+
outputTokens: m.totalOutputTokens,
|
|
6394
|
+
callCount: m.callCount,
|
|
6395
|
+
avgOutputTokens: m.callCount > 0 ? Math.round(m.totalOutputTokens / m.callCount) : 0
|
|
6396
|
+
})).sort((a, b) => b.outputTokens - a.outputTokens);
|
|
6397
|
+
}
|
|
6398
|
+
/**
|
|
6399
|
+
* Per-user call counts, sourced from a user tag injected by the interceptor.
|
|
6400
|
+
* When no user tracking is configured this returns an empty breakdown.
|
|
6401
|
+
*/
|
|
6402
|
+
getUserBreakdown() {
|
|
6403
|
+
return Object.fromEntries(this.userCounts);
|
|
6404
|
+
}
|
|
6405
|
+
userCounts = /* @__PURE__ */ new Map();
|
|
6406
|
+
recordUser(user) {
|
|
6407
|
+
this.userCounts.set(user, (this.userCounts.get(user) ?? 0) + 1);
|
|
6408
|
+
}
|
|
6409
|
+
/**
|
|
6410
|
+
* System-level metrics snapshot.
|
|
6411
|
+
*/
|
|
6412
|
+
getSystemMetrics() {
|
|
6413
|
+
const mem = process.memoryUsage();
|
|
6414
|
+
return {
|
|
6415
|
+
upSince: this.upSince,
|
|
6416
|
+
uptimeSeconds: Math.round((Date.now() - this.startTime) / 1e3),
|
|
6417
|
+
processMemoryMb: Math.round(mem.rss / (1024 * 1024)),
|
|
6418
|
+
nodeVersion: process.version,
|
|
6419
|
+
platform: process.platform
|
|
6420
|
+
};
|
|
6421
|
+
}
|
|
6422
|
+
/**
|
|
6423
|
+
* Reset all accumulated data (primarily useful in tests).
|
|
6424
|
+
*/
|
|
6425
|
+
reset() {
|
|
6426
|
+
this.toolData.clear();
|
|
6427
|
+
this.userCounts.clear();
|
|
6428
|
+
}
|
|
6429
|
+
};
|
|
6430
|
+
var globalMetrics = new MetricsAccumulator();
|
|
6431
|
+
function isErrorResult(result) {
|
|
6432
|
+
if (typeof result !== "object" || result === null) return false;
|
|
6433
|
+
const obj = result;
|
|
6434
|
+
return obj["success"] === false && typeof obj["error"] === "string";
|
|
6435
|
+
}
|
|
6436
|
+
function wrapWithMetrics(toolName, handler, accumulator) {
|
|
6437
|
+
return async (args) => {
|
|
6438
|
+
const start = performance$1.now();
|
|
6439
|
+
let result;
|
|
6440
|
+
let isError;
|
|
6441
|
+
try {
|
|
6442
|
+
result = await handler(args);
|
|
6443
|
+
isError = isErrorResult(result);
|
|
6444
|
+
} catch (err) {
|
|
6445
|
+
const durationMs2 = Math.round(performance$1.now() - start);
|
|
6446
|
+
try {
|
|
6447
|
+
accumulator.record({
|
|
6448
|
+
toolName,
|
|
6449
|
+
durationMs: durationMs2,
|
|
6450
|
+
inputTokens: estimatePayloadTokens(args),
|
|
6451
|
+
outputTokens: 0,
|
|
6452
|
+
isError: true
|
|
6453
|
+
});
|
|
6454
|
+
} catch {
|
|
6455
|
+
}
|
|
6456
|
+
throw err;
|
|
6457
|
+
}
|
|
6458
|
+
const durationMs = Math.round(performance$1.now() - start);
|
|
6459
|
+
try {
|
|
6460
|
+
accumulator.record({
|
|
6461
|
+
toolName,
|
|
6462
|
+
durationMs,
|
|
6463
|
+
inputTokens: estimatePayloadTokens(args),
|
|
6464
|
+
outputTokens: estimatePayloadTokens(result),
|
|
6465
|
+
isError
|
|
6466
|
+
});
|
|
6467
|
+
} catch {
|
|
6468
|
+
}
|
|
6469
|
+
return result;
|
|
6470
|
+
};
|
|
6471
|
+
}
|
|
6472
|
+
var BUFFER_HIGH_WATER = 50;
|
|
6473
|
+
var FLUSH_INTERVAL_MS = 100;
|
|
6474
|
+
var DEFAULT_RECENT_COUNT = 50;
|
|
6475
|
+
var STDERR_SENTINEL = "stderr";
|
|
6476
|
+
var TAIL_READ_BYTES = 65536;
|
|
6477
|
+
var MAX_ARCHIVES = 5;
|
|
6478
|
+
var AuditLogger = class {
|
|
6479
|
+
config;
|
|
6480
|
+
buffer = [];
|
|
6481
|
+
flushTimer = null;
|
|
6482
|
+
activeFlush = null;
|
|
6483
|
+
closed = false;
|
|
6484
|
+
dirEnsured = false;
|
|
6485
|
+
stderrMode;
|
|
6486
|
+
constructor(config) {
|
|
6487
|
+
this.config = config;
|
|
6488
|
+
this.stderrMode = config.logPath.toLowerCase() === STDERR_SENTINEL;
|
|
6489
|
+
if (config.enabled) {
|
|
6490
|
+
this.flushTimer = setInterval(() => {
|
|
6491
|
+
void this.flush();
|
|
6492
|
+
}, FLUSH_INTERVAL_MS);
|
|
6493
|
+
this.flushTimer.unref();
|
|
6494
|
+
}
|
|
6495
|
+
}
|
|
6496
|
+
/**
|
|
6497
|
+
* Append an audit entry to the buffer.
|
|
6498
|
+
* Non-blocking — the entry is serialised and queued; the
|
|
6499
|
+
* actual file write happens on the next flush cycle.
|
|
6500
|
+
*/
|
|
6501
|
+
log(entry) {
|
|
6502
|
+
if (this.closed || !this.config.enabled) return;
|
|
6503
|
+
this.buffer.push(JSON.stringify(entry));
|
|
6504
|
+
if (this.buffer.length >= BUFFER_HIGH_WATER) {
|
|
6505
|
+
void this.flush();
|
|
6506
|
+
}
|
|
6507
|
+
}
|
|
6508
|
+
/**
|
|
6509
|
+
* Flush the buffer to disk.
|
|
6510
|
+
* Safe to call concurrently — serialises via `this.activeFlush` Promise.
|
|
6511
|
+
*/
|
|
6512
|
+
async flush() {
|
|
6513
|
+
if (this.activeFlush) {
|
|
6514
|
+
await this.activeFlush;
|
|
6515
|
+
if (this.buffer.length === 0) return;
|
|
6516
|
+
}
|
|
6517
|
+
if (this.buffer.length === 0) return;
|
|
6518
|
+
const doFlush = async () => {
|
|
6519
|
+
await this.rotateIfNeeded();
|
|
6520
|
+
const lines = this.buffer;
|
|
6521
|
+
this.buffer = [];
|
|
6522
|
+
try {
|
|
6523
|
+
if (this.stderrMode) {
|
|
6524
|
+
process.stderr.write(lines.join("\n") + "\n");
|
|
6525
|
+
} else {
|
|
6526
|
+
await this.ensureDirectory();
|
|
6527
|
+
await appendFile(this.config.logPath, lines.join("\n") + "\n", "utf-8");
|
|
6528
|
+
}
|
|
6529
|
+
} catch (err) {
|
|
6530
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
6531
|
+
process.stderr.write(`[AUDIT] Write failed: ${message}
|
|
6532
|
+
`);
|
|
6533
|
+
this.buffer.unshift(...lines);
|
|
6534
|
+
}
|
|
6535
|
+
};
|
|
6536
|
+
this.activeFlush = doFlush();
|
|
6537
|
+
try {
|
|
6538
|
+
await this.activeFlush;
|
|
6539
|
+
} finally {
|
|
6540
|
+
this.activeFlush = null;
|
|
6541
|
+
}
|
|
6542
|
+
}
|
|
6543
|
+
/**
|
|
6544
|
+
* Gracefully close the logger — flush remaining entries and stop the timer.
|
|
6545
|
+
*/
|
|
6546
|
+
async close() {
|
|
6547
|
+
this.closed = true;
|
|
6548
|
+
if (this.flushTimer) {
|
|
6549
|
+
clearInterval(this.flushTimer);
|
|
6550
|
+
this.flushTimer = null;
|
|
6551
|
+
}
|
|
6552
|
+
await this.flush();
|
|
6553
|
+
}
|
|
6554
|
+
/**
|
|
6555
|
+
* Read the most recent audit entries from the log file.
|
|
6556
|
+
* Uses a streaming tail-read: only the last TAIL_READ_BYTES (64 KB) are
|
|
6557
|
+
* read from disk, preventing O(n) memory spikes for large audit logs.
|
|
6558
|
+
* Used by the `memory://audit` resource.
|
|
6559
|
+
*
|
|
6560
|
+
* @param count Maximum number of entries to return (default 50)
|
|
6561
|
+
*/
|
|
6562
|
+
async recent(count = DEFAULT_RECENT_COUNT) {
|
|
6563
|
+
if (this.stderrMode) return [];
|
|
6564
|
+
await this.flush();
|
|
6565
|
+
try {
|
|
6566
|
+
let fh;
|
|
6567
|
+
try {
|
|
6568
|
+
fh = await open(this.config.logPath, "r");
|
|
6569
|
+
} catch {
|
|
6570
|
+
return [];
|
|
6571
|
+
}
|
|
6572
|
+
try {
|
|
6573
|
+
const info = await stat(this.config.logPath);
|
|
6574
|
+
const fileSize = info.size;
|
|
6575
|
+
if (fileSize === 0) return [];
|
|
6576
|
+
const readSize = Math.min(fileSize, TAIL_READ_BYTES);
|
|
6577
|
+
const startOffset = fileSize - readSize;
|
|
6578
|
+
const buf = Buffer.alloc(readSize);
|
|
6579
|
+
await fh.read(buf, 0, readSize, startOffset);
|
|
6580
|
+
const chunk = buf.toString("utf-8");
|
|
6581
|
+
const rawLines = chunk.split("\n").filter(Boolean);
|
|
6582
|
+
const lines = startOffset > 0 ? rawLines.slice(1) : rawLines;
|
|
6583
|
+
const tail = lines.slice(-count);
|
|
6584
|
+
return tail.reduce((acc, line) => {
|
|
6585
|
+
try {
|
|
6586
|
+
acc.push(JSON.parse(line));
|
|
6587
|
+
} catch {
|
|
6588
|
+
}
|
|
6589
|
+
return acc;
|
|
6590
|
+
}, []);
|
|
6591
|
+
} finally {
|
|
6592
|
+
await fh.close();
|
|
6593
|
+
}
|
|
6594
|
+
} catch {
|
|
6595
|
+
return [];
|
|
6596
|
+
}
|
|
6597
|
+
}
|
|
6598
|
+
// =========================================================================
|
|
6599
|
+
// Private helpers
|
|
6600
|
+
// =========================================================================
|
|
6601
|
+
/**
|
|
6602
|
+
* Ensure the parent directory of the log file exists.
|
|
6603
|
+
*/
|
|
6604
|
+
async ensureDirectory() {
|
|
6605
|
+
if (this.dirEnsured) return;
|
|
6606
|
+
try {
|
|
6607
|
+
await mkdir(dirname(this.config.logPath), { recursive: true });
|
|
6608
|
+
this.dirEnsured = true;
|
|
6609
|
+
} catch {
|
|
6610
|
+
this.dirEnsured = true;
|
|
6611
|
+
}
|
|
6612
|
+
}
|
|
6613
|
+
/**
|
|
6614
|
+
* Rotate the log file if it exceeds the configured size limit.
|
|
6615
|
+
* Keeps up to 5 rotated files (`.1` through `.5`); older data is discarded.
|
|
6616
|
+
* Rotation failure is non-fatal — audit must not block tool execution.
|
|
6617
|
+
*/
|
|
6618
|
+
async rotateIfNeeded() {
|
|
6619
|
+
if (this.stderrMode || !this.config.maxSizeBytes) return;
|
|
6620
|
+
try {
|
|
6621
|
+
const info = await stat(this.config.logPath).catch(() => null);
|
|
6622
|
+
if (!info || info.size < this.config.maxSizeBytes) return;
|
|
6623
|
+
for (let i = MAX_ARCHIVES - 1; i >= 1; i--) {
|
|
6624
|
+
const oldFile = `${this.config.logPath}.${String(i)}`;
|
|
6625
|
+
const newFile = `${this.config.logPath}.${String(i + 1)}`;
|
|
6626
|
+
await rename(oldFile, newFile).catch(() => null);
|
|
6627
|
+
}
|
|
6628
|
+
const rotatedPath = `${this.config.logPath}.1`;
|
|
6629
|
+
await rename(this.config.logPath, rotatedPath);
|
|
6630
|
+
} catch {
|
|
6631
|
+
}
|
|
6632
|
+
}
|
|
6633
|
+
};
|
|
6634
|
+
|
|
6635
|
+
// src/filtering/tool-filter.ts
|
|
6636
|
+
var TOOL_GROUPS = {
|
|
6637
|
+
core: [
|
|
6638
|
+
"create_entry",
|
|
6639
|
+
"get_entry_by_id",
|
|
6640
|
+
"get_recent_entries",
|
|
6641
|
+
"create_entry_minimal",
|
|
6642
|
+
"test_simple",
|
|
6643
|
+
"list_tags"
|
|
6644
|
+
],
|
|
6645
|
+
search: ["search_entries", "search_by_date_range", "semantic_search", "get_vector_index_stats"],
|
|
6646
|
+
analytics: ["get_statistics", "get_cross_project_insights"],
|
|
6647
|
+
relationships: ["link_entries", "visualize_relationships"],
|
|
6648
|
+
export: ["export_entries"],
|
|
6649
|
+
admin: [
|
|
6650
|
+
"update_entry",
|
|
6651
|
+
"delete_entry",
|
|
6652
|
+
"rebuild_vector_index",
|
|
6653
|
+
"add_to_vector_index",
|
|
6654
|
+
"merge_tags"
|
|
6655
|
+
],
|
|
6656
|
+
github: [
|
|
6657
|
+
"get_github_issues",
|
|
6658
|
+
"get_github_prs",
|
|
6659
|
+
"get_github_issue",
|
|
6660
|
+
"get_github_pr",
|
|
6661
|
+
"get_github_context",
|
|
6662
|
+
"get_kanban_board",
|
|
6663
|
+
"move_kanban_item",
|
|
6664
|
+
"create_github_issue_with_entry",
|
|
6665
|
+
"close_github_issue_with_entry",
|
|
6666
|
+
"get_github_milestones",
|
|
6667
|
+
"get_github_milestone",
|
|
6668
|
+
"create_github_milestone",
|
|
6669
|
+
"update_github_milestone",
|
|
6670
|
+
"delete_github_milestone",
|
|
6671
|
+
"get_repo_insights",
|
|
6672
|
+
"get_copilot_reviews"
|
|
6673
|
+
],
|
|
6674
|
+
backup: ["backup_journal", "list_backups", "restore_backup", "cleanup_backups"],
|
|
6675
|
+
team: [
|
|
6676
|
+
"team_create_entry",
|
|
6677
|
+
"team_get_entry_by_id",
|
|
6678
|
+
"team_get_recent",
|
|
6679
|
+
"team_list_tags",
|
|
6680
|
+
"team_search",
|
|
6681
|
+
"team_search_by_date_range",
|
|
6682
|
+
"team_update_entry",
|
|
6683
|
+
"team_delete_entry",
|
|
6684
|
+
"team_merge_tags",
|
|
6685
|
+
"team_get_statistics",
|
|
6686
|
+
"team_link_entries",
|
|
6687
|
+
"team_visualize_relationships",
|
|
6688
|
+
"team_export_entries",
|
|
6689
|
+
"team_backup",
|
|
6690
|
+
"team_list_backups",
|
|
6691
|
+
"team_semantic_search",
|
|
6692
|
+
"team_get_vector_index_stats",
|
|
6693
|
+
"team_rebuild_vector_index",
|
|
6694
|
+
"team_add_to_vector_index",
|
|
6695
|
+
"team_get_cross_project_insights"
|
|
6696
|
+
],
|
|
6697
|
+
codemode: ["mj_execute_code"]
|
|
6698
|
+
};
|
|
6699
|
+
var META_GROUPS = {
|
|
6700
|
+
starter: ["core", "search", "codemode"],
|
|
6701
|
+
essential: ["core", "codemode"],
|
|
6702
|
+
full: [
|
|
6703
|
+
"core",
|
|
6704
|
+
"search",
|
|
6705
|
+
"analytics",
|
|
6706
|
+
"relationships",
|
|
6707
|
+
"export",
|
|
6708
|
+
"admin",
|
|
6709
|
+
"github",
|
|
6710
|
+
"backup",
|
|
6711
|
+
"team",
|
|
6712
|
+
"codemode"
|
|
6713
|
+
],
|
|
6714
|
+
readonly: ["core", "search", "analytics", "relationships", "export"]
|
|
6715
|
+
};
|
|
6716
|
+
function getAllToolNames() {
|
|
6717
|
+
const allTools = [];
|
|
6718
|
+
for (const tools of Object.values(TOOL_GROUPS)) {
|
|
6719
|
+
allTools.push(...tools);
|
|
6720
|
+
}
|
|
6721
|
+
return allTools;
|
|
6722
|
+
}
|
|
6723
|
+
function getToolGroup(toolName) {
|
|
6724
|
+
for (const [group, tools] of Object.entries(TOOL_GROUPS)) {
|
|
6725
|
+
if (tools.includes(toolName)) {
|
|
6726
|
+
return group;
|
|
6727
|
+
}
|
|
6728
|
+
}
|
|
6729
|
+
return void 0;
|
|
6730
|
+
}
|
|
6731
|
+
function getEnabledGroups(enabledTools) {
|
|
6732
|
+
const groups = /* @__PURE__ */ new Set();
|
|
6733
|
+
for (const [group, tools] of Object.entries(TOOL_GROUPS)) {
|
|
6734
|
+
if (tools.some((t) => enabledTools.has(t))) {
|
|
6735
|
+
groups.add(group);
|
|
6736
|
+
}
|
|
6737
|
+
}
|
|
6738
|
+
return groups;
|
|
6739
|
+
}
|
|
6740
|
+
function isGroup(name) {
|
|
6741
|
+
return name in TOOL_GROUPS;
|
|
6742
|
+
}
|
|
6743
|
+
function isMetaGroup(name) {
|
|
6744
|
+
return name in META_GROUPS;
|
|
6745
|
+
}
|
|
6746
|
+
function parseToolFilter(filterString) {
|
|
6747
|
+
const rules = [];
|
|
6748
|
+
const parts = filterString.split(",").map((p) => p.trim()).filter(Boolean);
|
|
6749
|
+
let enabledTools = /* @__PURE__ */ new Set();
|
|
6750
|
+
let isWhitelistMode = false;
|
|
6751
|
+
for (let i = 0; i < parts.length; i++) {
|
|
6752
|
+
const part = parts[i];
|
|
6753
|
+
if (!part) continue;
|
|
6754
|
+
const isAdd = part.startsWith("+");
|
|
6755
|
+
const isRemove = part.startsWith("-");
|
|
6756
|
+
const name = isAdd || isRemove ? part.slice(1) : part;
|
|
6757
|
+
if (i === 0 && !isAdd && !isRemove) {
|
|
6758
|
+
isWhitelistMode = true;
|
|
6759
|
+
if (isMetaGroup(name)) {
|
|
6760
|
+
for (const group of META_GROUPS[name]) {
|
|
6761
|
+
enabledTools = /* @__PURE__ */ new Set([...enabledTools, ...TOOL_GROUPS[group]]);
|
|
6762
|
+
}
|
|
6763
|
+
} else if (isGroup(name)) {
|
|
6764
|
+
enabledTools = /* @__PURE__ */ new Set([...enabledTools, ...TOOL_GROUPS[name]]);
|
|
6765
|
+
} else {
|
|
6766
|
+
enabledTools.add(name);
|
|
6767
|
+
}
|
|
6768
|
+
rules.push({
|
|
6769
|
+
type: "include",
|
|
6770
|
+
target: name,
|
|
6771
|
+
isGroup: isGroup(name) || isMetaGroup(name)
|
|
6772
|
+
});
|
|
6773
|
+
} else if (isRemove) {
|
|
6774
|
+
if (isGroup(name)) {
|
|
6775
|
+
for (const tool of TOOL_GROUPS[name]) {
|
|
6776
|
+
enabledTools.delete(tool);
|
|
6777
|
+
}
|
|
6778
|
+
} else {
|
|
6779
|
+
enabledTools.delete(name);
|
|
6780
|
+
}
|
|
6781
|
+
rules.push({
|
|
6782
|
+
type: "exclude",
|
|
6783
|
+
target: name,
|
|
6784
|
+
isGroup: isGroup(name)
|
|
6785
|
+
});
|
|
6786
|
+
} else {
|
|
6787
|
+
if (isMetaGroup(name)) {
|
|
6788
|
+
for (const group of META_GROUPS[name]) {
|
|
6789
|
+
enabledTools = /* @__PURE__ */ new Set([...enabledTools, ...TOOL_GROUPS[group]]);
|
|
6790
|
+
}
|
|
6791
|
+
} else if (isGroup(name)) {
|
|
6792
|
+
enabledTools = /* @__PURE__ */ new Set([...enabledTools, ...TOOL_GROUPS[name]]);
|
|
6793
|
+
} else {
|
|
6794
|
+
enabledTools.add(name);
|
|
6795
|
+
}
|
|
6796
|
+
rules.push({
|
|
6797
|
+
type: "include",
|
|
6798
|
+
target: name,
|
|
6799
|
+
isGroup: isGroup(name) || isMetaGroup(name)
|
|
6800
|
+
});
|
|
6801
|
+
}
|
|
6802
|
+
}
|
|
6803
|
+
if (!isWhitelistMode && rules.length > 0 && rules[0]?.type === "exclude") {
|
|
6804
|
+
enabledTools = new Set(getAllToolNames());
|
|
6805
|
+
for (const rule of rules) {
|
|
6806
|
+
if (rule.type === "exclude") {
|
|
6807
|
+
if (isGroup(rule.target)) {
|
|
6808
|
+
for (const tool of TOOL_GROUPS[rule.target]) {
|
|
6809
|
+
enabledTools.delete(tool);
|
|
6810
|
+
}
|
|
6811
|
+
} else {
|
|
6812
|
+
enabledTools.delete(rule.target);
|
|
6813
|
+
}
|
|
6814
|
+
}
|
|
6815
|
+
}
|
|
6816
|
+
}
|
|
6817
|
+
return {
|
|
6818
|
+
raw: filterString,
|
|
6819
|
+
rules,
|
|
6820
|
+
enabledTools
|
|
6821
|
+
};
|
|
6822
|
+
}
|
|
6823
|
+
function isToolEnabled(toolName, filterConfig) {
|
|
6824
|
+
return filterConfig.enabledTools.has(toolName);
|
|
6825
|
+
}
|
|
6826
|
+
function filterTools(tools, filterConfig) {
|
|
6827
|
+
return tools.filter((tool) => isToolEnabled(tool.name, filterConfig));
|
|
6828
|
+
}
|
|
6829
|
+
function getToolFilterFromEnv() {
|
|
6830
|
+
const filterString = process.env["MEMORY_JOURNAL_MCP_TOOL_FILTER"];
|
|
6831
|
+
if (!filterString) return null;
|
|
6832
|
+
return parseToolFilter(filterString);
|
|
6833
|
+
}
|
|
6834
|
+
function calculateTokenSavings(totalTools, enabledTools, avgTokensPerTool = 150) {
|
|
6835
|
+
const savedTokens = (totalTools - enabledTools) * avgTokensPerTool;
|
|
6836
|
+
const reduction = (totalTools - enabledTools) / totalTools * 100;
|
|
6837
|
+
return { reduction, savedTokens };
|
|
6838
|
+
}
|
|
6839
|
+
function getFilterSummary(filterConfig) {
|
|
6840
|
+
const total = getAllToolNames().length;
|
|
6841
|
+
const enabled = filterConfig.enabledTools.size;
|
|
6842
|
+
const { reduction } = calculateTokenSavings(total, enabled);
|
|
6843
|
+
return `${enabled}/${total} tools enabled (${reduction.toFixed(0)}% reduction)`;
|
|
6844
|
+
}
|
|
6845
|
+
|
|
6846
|
+
// src/auth/scopes.ts
|
|
6847
|
+
var SCOPES = {
|
|
6848
|
+
/** Read-only access */
|
|
6849
|
+
READ: "read",
|
|
6850
|
+
/** Read and write access */
|
|
6851
|
+
WRITE: "write",
|
|
6852
|
+
/** Administrative access */
|
|
6853
|
+
ADMIN: "admin",
|
|
6854
|
+
/** Unrestricted access to all operations */
|
|
6855
|
+
FULL: "full"
|
|
6856
|
+
};
|
|
6857
|
+
var BASE_SCOPES = ["read", "write", "admin", "full"];
|
|
6858
|
+
var SUPPORTED_SCOPES = ["read", "write", "admin", "full"];
|
|
6859
|
+
var TOOL_GROUP_SCOPES = {
|
|
6860
|
+
core: SCOPES.READ,
|
|
6861
|
+
search: SCOPES.READ,
|
|
6862
|
+
analytics: SCOPES.READ,
|
|
6863
|
+
relationships: SCOPES.READ,
|
|
6864
|
+
export: SCOPES.READ,
|
|
6865
|
+
admin: SCOPES.ADMIN,
|
|
6866
|
+
github: SCOPES.WRITE,
|
|
6867
|
+
backup: SCOPES.ADMIN,
|
|
6868
|
+
team: SCOPES.WRITE,
|
|
6869
|
+
codemode: SCOPES.ADMIN
|
|
6870
|
+
};
|
|
6871
|
+
var groupsForScope = (maxScope) => {
|
|
6872
|
+
const hierarchy = {
|
|
6873
|
+
read: 0,
|
|
6874
|
+
write: 1,
|
|
6875
|
+
admin: 2,
|
|
6876
|
+
full: 3
|
|
6877
|
+
};
|
|
6878
|
+
const maxLevel = hierarchy[maxScope];
|
|
6879
|
+
return Object.entries(TOOL_GROUP_SCOPES).filter(([, scope]) => hierarchy[scope] <= maxLevel).map(([group]) => group);
|
|
6880
|
+
};
|
|
6881
|
+
groupsForScope(SCOPES.READ);
|
|
6882
|
+
groupsForScope(SCOPES.WRITE);
|
|
6883
|
+
groupsForScope(SCOPES.ADMIN);
|
|
6884
|
+
function parseScopes(scopeString) {
|
|
6885
|
+
return scopeString.split(/\s+/).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
6886
|
+
}
|
|
6887
|
+
function hasScope(grantedScopes, requiredScope) {
|
|
6888
|
+
if (grantedScopes.includes(SCOPES.FULL)) {
|
|
6889
|
+
return true;
|
|
6890
|
+
}
|
|
6891
|
+
if (grantedScopes.includes(requiredScope)) {
|
|
6892
|
+
return true;
|
|
6893
|
+
}
|
|
6894
|
+
if (requiredScope === SCOPES.READ || requiredScope === SCOPES.WRITE) {
|
|
6895
|
+
if (grantedScopes.includes(SCOPES.ADMIN)) {
|
|
6896
|
+
return true;
|
|
6897
|
+
}
|
|
6898
|
+
}
|
|
6899
|
+
if (requiredScope === SCOPES.READ) {
|
|
6900
|
+
if (grantedScopes.includes(SCOPES.WRITE)) {
|
|
6901
|
+
return true;
|
|
6902
|
+
}
|
|
6903
|
+
}
|
|
6904
|
+
return false;
|
|
6905
|
+
}
|
|
6906
|
+
|
|
6907
|
+
// src/auth/scope-map.ts
|
|
6908
|
+
var toolScopeMap = /* @__PURE__ */ new Map();
|
|
6909
|
+
for (const [group, tools] of Object.entries(TOOL_GROUPS)) {
|
|
6910
|
+
const scope = TOOL_GROUP_SCOPES[group];
|
|
6911
|
+
if (scope) {
|
|
6912
|
+
for (const toolName of tools) {
|
|
6913
|
+
toolScopeMap.set(toolName, scope);
|
|
6914
|
+
}
|
|
6915
|
+
}
|
|
6916
|
+
}
|
|
6917
|
+
function getRequiredScope(toolName) {
|
|
6918
|
+
return toolScopeMap.get(toolName) ?? SCOPES.READ;
|
|
6919
|
+
}
|
|
6920
|
+
|
|
6921
|
+
// src/audit/interceptor.ts
|
|
6922
|
+
var ALWAYS_AUDITED_SCOPES = /* @__PURE__ */ new Set(["write", "admin"]);
|
|
6923
|
+
function scopeToCategory(scope) {
|
|
6924
|
+
if (scope === "admin") return "admin";
|
|
6925
|
+
if (scope === "read") return "read";
|
|
6926
|
+
return "write";
|
|
6927
|
+
}
|
|
6928
|
+
function generateRequestId() {
|
|
6929
|
+
const ts = Date.now().toString(36);
|
|
6930
|
+
const rand = Math.random().toString(36).slice(2, 8);
|
|
6931
|
+
return `aud-${ts}-${rand}`;
|
|
6932
|
+
}
|
|
6933
|
+
function createAuditInterceptor(auditLogger) {
|
|
6934
|
+
const auditReads = auditLogger.config.auditReads;
|
|
6935
|
+
return {
|
|
6936
|
+
async around(toolName, args, fn) {
|
|
6937
|
+
const scope = getRequiredScope(toolName);
|
|
6938
|
+
if (!ALWAYS_AUDITED_SCOPES.has(scope) && !auditReads) {
|
|
6939
|
+
return fn();
|
|
6940
|
+
}
|
|
6941
|
+
const isReadScope = scope === "read";
|
|
6942
|
+
const requestId = generateRequestId();
|
|
6943
|
+
const start = performance$1.now();
|
|
6944
|
+
let success = true;
|
|
6945
|
+
let error;
|
|
6946
|
+
let tokenEstimate;
|
|
6947
|
+
try {
|
|
6948
|
+
const result = await fn();
|
|
6949
|
+
if (typeof result === "object" && result !== null) {
|
|
6950
|
+
try {
|
|
6951
|
+
const json = JSON.stringify({
|
|
6952
|
+
...result,
|
|
6953
|
+
_meta: { tokenEstimate: 0 }
|
|
6954
|
+
});
|
|
6955
|
+
tokenEstimate = Math.ceil(Buffer.byteLength(json, "utf8") / 4);
|
|
6956
|
+
} catch {
|
|
6957
|
+
}
|
|
6958
|
+
} else if (typeof result === "string") {
|
|
6959
|
+
tokenEstimate = Math.ceil(Buffer.byteLength(result, "utf8") / 4);
|
|
6960
|
+
}
|
|
6961
|
+
return result;
|
|
6962
|
+
} catch (err) {
|
|
6963
|
+
success = false;
|
|
6964
|
+
error = err instanceof Error ? err.message : String(err);
|
|
6965
|
+
const errorResult = {
|
|
6966
|
+
success: false,
|
|
6967
|
+
error,
|
|
6968
|
+
code: "INTERNAL_ERROR",
|
|
6969
|
+
category: "internal",
|
|
6970
|
+
recoverable: false
|
|
6971
|
+
};
|
|
6972
|
+
const enriched = JSON.stringify({
|
|
6973
|
+
...errorResult,
|
|
6974
|
+
_meta: { tokenEstimate: 0 }
|
|
6975
|
+
});
|
|
6976
|
+
tokenEstimate = Math.ceil(Buffer.byteLength(enriched, "utf8") / 4);
|
|
6977
|
+
throw err;
|
|
6978
|
+
} finally {
|
|
6979
|
+
const durationMs = Math.round(performance$1.now() - start);
|
|
6980
|
+
if (isReadScope) {
|
|
6981
|
+
auditLogger.log({
|
|
6982
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6983
|
+
requestId,
|
|
6984
|
+
tool: toolName,
|
|
6985
|
+
category: "read",
|
|
6986
|
+
scope,
|
|
6987
|
+
user: null,
|
|
6988
|
+
scopes: [],
|
|
6989
|
+
durationMs,
|
|
6990
|
+
success,
|
|
6991
|
+
error,
|
|
6992
|
+
tokenEstimate
|
|
6993
|
+
});
|
|
6994
|
+
} else {
|
|
6995
|
+
auditLogger.log({
|
|
6996
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6997
|
+
requestId,
|
|
6998
|
+
tool: toolName,
|
|
6999
|
+
category: scopeToCategory(scope),
|
|
7000
|
+
scope,
|
|
7001
|
+
user: null,
|
|
7002
|
+
scopes: [],
|
|
7003
|
+
durationMs,
|
|
7004
|
+
success,
|
|
7005
|
+
error,
|
|
7006
|
+
args: auditLogger.config.redact ? void 0 : args,
|
|
7007
|
+
tokenEstimate
|
|
7008
|
+
});
|
|
7009
|
+
}
|
|
7010
|
+
}
|
|
7011
|
+
}
|
|
7012
|
+
};
|
|
7013
|
+
}
|
|
7014
|
+
|
|
7015
|
+
// src/audit/types.ts
|
|
7016
|
+
var DEFAULT_AUDIT_LOG_MAX_SIZE_BYTES = 10 * 1024 * 1024;
|
|
7017
|
+
|
|
7018
|
+
// src/utils/resource-annotations.ts
|
|
7019
|
+
var HIGH_PRIORITY = {
|
|
7020
|
+
priority: 0.9,
|
|
7021
|
+
audience: ["user", "assistant"]
|
|
7022
|
+
};
|
|
7023
|
+
var MEDIUM_PRIORITY = {
|
|
7024
|
+
priority: 0.6,
|
|
7025
|
+
audience: ["user", "assistant"]
|
|
7026
|
+
};
|
|
7027
|
+
var LOW_PRIORITY = {
|
|
7028
|
+
priority: 0.4,
|
|
7029
|
+
audience: ["user", "assistant"]
|
|
7030
|
+
};
|
|
7031
|
+
var ASSISTANT_FOCUSED = {
|
|
7032
|
+
priority: 0.5,
|
|
7033
|
+
audience: ["assistant"]
|
|
7034
|
+
};
|
|
7035
|
+
function withPriority(priority, base = MEDIUM_PRIORITY) {
|
|
7036
|
+
return { ...base, priority };
|
|
7037
|
+
}
|
|
7038
|
+
function withSessionInit(base = HIGH_PRIORITY) {
|
|
7039
|
+
return { ...base, sessionInit: true };
|
|
7040
|
+
}
|
|
7041
|
+
|
|
7042
|
+
// src/audit/audit-resource.ts
|
|
7043
|
+
function getAuditResourceDef(getLogger) {
|
|
7044
|
+
return {
|
|
7045
|
+
uri: "memory://audit",
|
|
7046
|
+
name: "Audit Log",
|
|
7047
|
+
title: "Audit Trail (last 50 entries)",
|
|
7048
|
+
description: "Last 50 write/admin tool call audit entries from the JSONL audit log. Each entry includes tool name, scope, duration, token estimates, and error status. Includes a session summary with total token consumption and error count.",
|
|
7049
|
+
mimeType: "text/plain",
|
|
7050
|
+
annotations: {
|
|
7051
|
+
...ASSISTANT_FOCUSED
|
|
7052
|
+
},
|
|
7053
|
+
handler: async (_uri, _context) => {
|
|
7054
|
+
const lastModified = (/* @__PURE__ */ new Date()).toISOString();
|
|
7055
|
+
const auditLogger = getLogger();
|
|
7056
|
+
if (!auditLogger) {
|
|
7057
|
+
return {
|
|
7058
|
+
data: "audit: not configured\nhint: Set AUDIT_LOG_PATH env var or --audit-log CLI flag to enable audit logging.",
|
|
7059
|
+
annotations: { lastModified }
|
|
7060
|
+
};
|
|
7061
|
+
}
|
|
7062
|
+
const entries = await auditLogger.recent(50);
|
|
7063
|
+
if (entries.length === 0) {
|
|
7064
|
+
return {
|
|
7065
|
+
data: `audit_log: ${auditLogger.config.logPath}
|
|
7066
|
+
entries: 0
|
|
7067
|
+
note: No write/admin operations have been audited yet.`,
|
|
7068
|
+
annotations: { lastModified }
|
|
7069
|
+
};
|
|
7070
|
+
}
|
|
7071
|
+
let totalTokens = 0;
|
|
7072
|
+
let errorCount = 0;
|
|
7073
|
+
let totalDuration = 0;
|
|
7074
|
+
for (const e of entries) {
|
|
7075
|
+
totalTokens += e.tokenEstimate ?? 0;
|
|
7076
|
+
totalDuration += e.durationMs;
|
|
7077
|
+
if (!e.success) errorCount++;
|
|
7078
|
+
}
|
|
7079
|
+
const formattedEntries = entries.map((e) => {
|
|
7080
|
+
const parts = [
|
|
7081
|
+
`- timestamp: ${e.timestamp}`,
|
|
7082
|
+
` tool: ${e.tool}`,
|
|
7083
|
+
` scope: ${e.scope}`,
|
|
7084
|
+
` category: ${e.category}`,
|
|
7085
|
+
` duration_ms: ${String(e.durationMs)}`,
|
|
7086
|
+
` success: ${String(e.success)}`
|
|
7087
|
+
];
|
|
7088
|
+
if (e.error) {
|
|
7089
|
+
parts.push(` error: ${e.error}`);
|
|
7090
|
+
}
|
|
7091
|
+
if (e.tokenEstimate !== void 0) {
|
|
7092
|
+
parts.push(` token_estimate: ${String(e.tokenEstimate)}`);
|
|
7093
|
+
}
|
|
7094
|
+
if (e.args !== void 0) {
|
|
7095
|
+
parts.push(` args: ${JSON.stringify(e.args)}`);
|
|
7096
|
+
}
|
|
7097
|
+
return parts.join("\n");
|
|
7098
|
+
}).join("\n");
|
|
7099
|
+
const text = `audit_log: ${auditLogger.config.logPath}
|
|
7100
|
+
entries_shown: ${String(entries.length)}
|
|
7101
|
+
as_of: ${lastModified}
|
|
7102
|
+
session_summary:
|
|
7103
|
+
total_tokens: ${String(totalTokens)}
|
|
7104
|
+
total_duration_ms: ${String(totalDuration)}
|
|
7105
|
+
error_count: ${String(errorCount)}
|
|
7106
|
+
redact_mode: ${String(auditLogger.config.redact)}
|
|
7107
|
+
|
|
7108
|
+
` + formattedEntries;
|
|
7109
|
+
return {
|
|
7110
|
+
data: text,
|
|
7111
|
+
annotations: { lastModified }
|
|
7112
|
+
};
|
|
7113
|
+
}
|
|
7114
|
+
};
|
|
7115
|
+
}
|
|
7116
|
+
|
|
6189
7117
|
// src/handlers/tools/index.ts
|
|
7118
|
+
var globalAuditLogger = null;
|
|
7119
|
+
var globalAuditInterceptor = null;
|
|
7120
|
+
function initializeAuditLogger(config) {
|
|
7121
|
+
globalAuditLogger = new AuditLogger(config);
|
|
7122
|
+
globalAuditInterceptor = createAuditInterceptor(globalAuditLogger);
|
|
7123
|
+
return globalAuditLogger;
|
|
7124
|
+
}
|
|
7125
|
+
function getGlobalAuditLogger() {
|
|
7126
|
+
return globalAuditLogger;
|
|
7127
|
+
}
|
|
6190
7128
|
function getToolIcon(group) {
|
|
6191
7129
|
const iconMap = {
|
|
6192
7130
|
core: {
|
|
@@ -6270,11 +7208,25 @@ function ensureToolCache(db, vectorManager, github, config, teamDb, teamVectorMa
|
|
|
6270
7208
|
return;
|
|
6271
7209
|
}
|
|
6272
7210
|
const context = { db, teamDb, vectorManager, teamVectorManager, github, config };
|
|
6273
|
-
|
|
7211
|
+
const rawDefs = getAllToolDefinitions(context);
|
|
7212
|
+
const instrumentedDefs = rawDefs.map((t) => {
|
|
7213
|
+
const metricsWrapped = wrapWithMetrics(
|
|
7214
|
+
t.name,
|
|
7215
|
+
(args) => Promise.resolve(t.handler(args)),
|
|
7216
|
+
globalMetrics
|
|
7217
|
+
);
|
|
7218
|
+
const interceptor = globalAuditInterceptor;
|
|
7219
|
+
const finalHandler = interceptor ? (args) => interceptor.around(t.name, args, () => metricsWrapped(args)) : metricsWrapped;
|
|
7220
|
+
return {
|
|
7221
|
+
...t,
|
|
7222
|
+
handler: finalHandler
|
|
7223
|
+
};
|
|
7224
|
+
});
|
|
7225
|
+
toolMapCache = new Map(instrumentedDefs.map((t) => [t.name, t]));
|
|
6274
7226
|
mappedToolsCache = null;
|
|
6275
7227
|
cachedContextRefs = { db, github, vectorManager, config, teamDb, teamVectorManager };
|
|
6276
7228
|
}
|
|
6277
|
-
function callTool(name, args, db, vectorManager, github, config, progress, teamDb, teamVectorManager) {
|
|
7229
|
+
async function callTool(name, args, db, vectorManager, github, config, progress, teamDb, teamVectorManager) {
|
|
6278
7230
|
ensureToolCache(db, vectorManager, github, config, teamDb, teamVectorManager);
|
|
6279
7231
|
const tool = (toolMapCache ?? EMPTY_TOOL_MAP).get(name);
|
|
6280
7232
|
if (!tool) {
|
|
@@ -6293,10 +7245,18 @@ function callTool(name, args, db, vectorManager, github, config, progress, teamD
|
|
|
6293
7245
|
const freshTools = getAllToolDefinitions(context);
|
|
6294
7246
|
const freshTool = freshTools.find((t) => t.name === name);
|
|
6295
7247
|
if (freshTool) {
|
|
6296
|
-
|
|
7248
|
+
const metricsWrapped = wrapWithMetrics(
|
|
7249
|
+
freshTool.name,
|
|
7250
|
+
(a) => Promise.resolve(freshTool.handler(a)),
|
|
7251
|
+
globalMetrics
|
|
7252
|
+
);
|
|
7253
|
+
const interceptor = globalAuditInterceptor;
|
|
7254
|
+
const freshResult = interceptor ? await interceptor.around(freshTool.name, args, () => metricsWrapped(args)) : await metricsWrapped(args);
|
|
7255
|
+
return injectTokenEstimate(freshResult);
|
|
6297
7256
|
}
|
|
6298
7257
|
}
|
|
6299
|
-
|
|
7258
|
+
const result = await Promise.resolve(tool.handler(args));
|
|
7259
|
+
return injectTokenEstimate(result);
|
|
6300
7260
|
}
|
|
6301
7261
|
function getAllToolDefinitions(context) {
|
|
6302
7262
|
return [
|
|
@@ -6313,4 +7273,4 @@ function getAllToolDefinitions(context) {
|
|
|
6313
7273
|
];
|
|
6314
7274
|
}
|
|
6315
7275
|
|
|
6316
|
-
export {
|
|
7276
|
+
export { ASSISTANT_FOCUSED, BASE_SCOPES, DEFAULT_AUDIT_LOG_MAX_SIZE_BYTES, DEFAULT_BRIEFING_CONFIG, HIGH_PRIORITY, LOW_PRIORITY, MEDIUM_PRIORITY, META_GROUPS, SUPPORTED_SCOPES, TOOL_GROUPS, calculateTokenSavings, callTool, execQuery, filterTools, getAllToolNames, getAuditResourceDef, getEnabledGroups, getFilterSummary, getGlobalAuditLogger, getRequiredScope, getToolFilterFromEnv, getToolGroup, getTools, globalMetrics, hasScope, initializeAuditLogger, isResourceError, isToolEnabled, milestoneCompletionPct, parseScopes, parseToolFilter, resolveGitHubRepo, sendProgress, setDefaultSandboxMode, transformEntryRow, withPriority, withSessionInit };
|