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.
@@ -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(github, issueNumber, existingUrl) {
53
+ async function resolveIssueUrl(context, projectNumber, issueNumber, existingUrl) {
274
54
  if (existingUrl) return existingUrl;
275
- if (issueNumber === void 0 || !github) return void 0;
276
- const cachedRepo = github.getCachedRepoInfo();
277
- if (cachedRepo?.owner && cachedRepo?.repo) {
278
- return `https://github.com/${cachedRepo.owner}/${cachedRepo.repo}/issues/${String(issueNumber)}`;
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, github } = context;
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
- github,
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: 'Full-text search journal entries using FTS5 (supports phrases "exact match", prefix auth*, boolean NOT/OR/AND, ranked by relevance). Optional filters for GitHub Projects, Issues, PRs, and Actions.',
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 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;
812
- const perDbLimit = calcPerDbLimit(input.limit, !!teamDb);
813
- let personalEntries;
814
- if (!input.query && !hasFilters) {
815
- personalEntries = db.getRecentEntries(perDbLimit, input.is_personal);
816
- } else {
817
- personalEntries = db.searchEntries(input.query || "", {
818
- limit: perDbLimit,
819
- isPersonal: input.is_personal,
820
- projectNumber: input.project_number,
821
- issueNumber: input.issue_number,
822
- prNumber: input.pr_number,
823
- prStatus: input.pr_status,
824
- workflowRunId: input.workflow_run_id
825
- });
826
- }
827
- if (teamDb && input.is_personal !== true) {
828
- let teamEntries;
829
- if (!input.query && !hasFilters) {
830
- teamEntries = teamDb.getRecentEntries(perDbLimit);
831
- } else {
832
- teamEntries = teamDb.searchEntries(input.query || "", {
833
- limit: perDbLimit,
834
- projectNumber: input.project_number,
835
- issueNumber: input.issue_number,
836
- prNumber: input.pr_number,
837
- prStatus: input.pr_status,
838
- workflowRunId: input.workflow_run_id
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
- const results = await vectorManager.search(
938
- input.query,
939
- input.limit ?? 10,
940
- input.similarity_threshold ?? 0.25
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 and GITHUB_REPO_PATH environment variables to enable GitHub integration.",
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
- if (!context.github) {
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 and GITHUB_REPO_PATH environment variables to enable GitHub integration.",
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 context.github.getRepoInfo();
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: context.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. Only counts OPEN items (closed items excluded).",
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({}).strict(),
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 (_params) => {
2550
+ handler: async (params) => {
2555
2551
  try {
2556
- if (!context.github) {
2557
- return {
2558
- success: false,
2559
- error: "GitHub integration not available",
2560
- code: "CONFIGURATION_ERROR",
2561
- category: "configuration",
2562
- suggestion: "Set GITHUB_TOKEN and GITHUB_REPO_PATH environment variables to enable GitHub integration.",
2563
- recoverable: true
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 context.github.getRepoContext();
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 board = await resolved.github.getProjectKanban(
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
- input.project_number,
2608
- resolved.repo
2628
+ projectNum,
2629
+ effectiveRepo
2609
2630
  );
2610
2631
  if (!board) {
2611
2632
  return {
2612
2633
  success: false,
2613
- error: `Project #${String(input.project_number)} not found or Status field not configured`,
2614
- projectNumber: input.project_number,
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
- input.project_number,
2655
- resolved.repo
2692
+ projectNum,
2693
+ effectiveRepo
2656
2694
  );
2657
2695
  if (!board) {
2658
2696
  return {
2659
2697
  success: false,
2660
- error: `Project #${String(input.project_number)} not found`,
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: input.project_number,
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 = input.project_number ?? context.config?.defaultProjectNumber;
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 = input.project_number ?? context.config?.defaultProjectNumber;
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: input.project_number ?? context.config?.defaultProjectNumber
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
- if (!github) {
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 and GITHUB_REPO_PATH environment variables."
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 github.getRepoInfo();
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: "Set GITHUB_REPO_PATH to your git repository.",
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, github } = context;
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
- github,
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
- const results = await teamVectorManager.search(
5048
- input.query,
5049
- input.limit ?? 10,
5050
- input.similarity_threshold ?? 0.25
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
- const allTools = collectNonCodeModeTools(context);
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
- return pool.execute(code, bindings, timeout).then((result) => {
6169
- if (result.success && result.result !== void 0) {
6170
- const sizeCheck = security.validateResultSize(result.result);
6171
- if (!sizeCheck.valid) {
6172
- return {
6173
- success: false,
6174
- error: sizeCheck.errors.join("; "),
6175
- metrics: result.metrics
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
- return result;
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
- toolMapCache = new Map(getAllToolDefinitions(context).map((t) => [t.name, t]));
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
- return Promise.resolve(freshTool.handler(args));
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
- return Promise.resolve(tool.handler(args));
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 { ConfigurationError, ConnectionError, DEFAULT_BRIEFING_CONFIG, MemoryJournalMcpError, QueryError, ResourceNotFoundError, ValidationError, assertNoPathTraversal, callTool, execQuery, getTools, isResourceError, logger, milestoneCompletionPct, resolveGitHubRepo, sendProgress, setDefaultSandboxMode, transformEntryRow, validateDateFormatPattern };
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 };