memory-journal-mcp 6.3.0 → 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,11 +1,14 @@
1
1
  import { transformAutoReturn } from './chunk-OKOVZ5QE.js';
2
- import { GitHubIntegration, resolveAuthor, logger, MemoryJournalMcpError, matchSuggestion, ConfigurationError } from './chunk-K2SCUSN4.js';
2
+ import { GitHubIntegration, resolveAuthor, logger, MemoryJournalMcpError, matchSuggestion, ConfigurationError } from './chunk-ARLH46WS.js';
3
3
  import { z, ZodError } from 'zod';
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
13
  function formatZodError(error) {
11
14
  return error.issues.map((issue) => {
@@ -55,7 +58,7 @@ async function resolveIssueUrl(context, projectNumber, issueNumber, existingUrl)
55
58
  ([_, v]) => v.project_number === projectNumber
56
59
  );
57
60
  if (entry) {
58
- const { GitHubIntegration: GitHubIntegration2 } = await import('./github-integration-DTNYAKVJ.js');
61
+ const { GitHubIntegration: GitHubIntegration2 } = await import('./github-integration-PDRLXKGM.js');
59
62
  const targetGithub = new GitHubIntegration2(entry[1].path);
60
63
  const repoInfo = await targetGithub.getRepoInfo();
61
64
  if (repoInfo.owner && repoInfo.repo) {
@@ -64,7 +67,7 @@ async function resolveIssueUrl(context, projectNumber, issueNumber, existingUrl)
64
67
  }
65
68
  }
66
69
  if (context.github) {
67
- const cachedRepo = context.github.getCachedRepoInfo();
70
+ const cachedRepo = context.github.getCachedRepoInfo() ?? await context.github.getRepoInfo();
68
71
  if (cachedRepo?.owner && cachedRepo?.repo) {
69
72
  return `https://github.com/${cachedRepo.owner}/${cachedRepo.repo}/issues/${String(issueNumber)}`;
70
73
  }
@@ -140,6 +143,7 @@ var EntryOutputSchema = z.object({
140
143
  var EntriesListOutputSchema = z.object({
141
144
  entries: z.array(EntryOutputSchema).optional(),
142
145
  count: z.number().optional(),
146
+ searchMode: z.string().optional(),
143
147
  success: z.boolean().optional(),
144
148
  error: z.string().optional()
145
149
  }).extend(ErrorFieldsMixin.shape);
@@ -445,26 +449,199 @@ function getCoreTools(context) {
445
449
  }
446
450
  ];
447
451
  }
452
+
453
+ // src/handlers/tools/search/helpers.ts
448
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
449
612
  var SearchEntriesSchema = z.object({
450
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
+ ),
451
617
  limit: z.number().max(MAX_QUERY_LIMIT).optional().default(10),
452
618
  is_personal: z.boolean().optional(),
453
619
  project_number: z.number().optional(),
454
620
  issue_number: z.number().optional(),
455
621
  pr_number: z.number().optional(),
456
622
  pr_status: z.enum(["draft", "open", "merged", "closed"]).optional(),
457
- 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()
458
628
  });
459
629
  var SearchEntriesSchemaMcp = z.object({
460
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
+ ),
461
634
  limit: relaxedNumber().optional().default(10),
462
635
  is_personal: z.boolean().optional(),
463
636
  project_number: relaxedNumber().optional(),
464
637
  issue_number: relaxedNumber().optional(),
465
638
  pr_number: relaxedNumber().optional(),
466
639
  pr_status: z.string().optional(),
467
- 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()
468
645
  });
469
646
  var SearchByDateRangeSchema = z.object({
470
647
  start_date: z.string().regex(DATE_FORMAT_REGEX, DATE_FORMAT_MESSAGE),
@@ -491,17 +668,31 @@ var SearchByDateRangeSchemaMcp = z.object({
491
668
  limit: relaxedNumber().optional().default(500)
492
669
  });
493
670
  var SemanticSearchSchema = z.object({
494
- 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
+ ),
495
675
  limit: z.number().max(MAX_QUERY_LIMIT).optional().default(10),
496
676
  similarity_threshold: z.number().optional().default(0.25),
497
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)"),
498
682
  hint_on_empty: z.boolean().optional().default(true).describe("Include hint when no results found (default: true)")
499
683
  });
500
684
  var SemanticSearchSchemaMcp = z.object({
501
- 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
+ ),
502
689
  limit: relaxedNumber().optional().default(10),
503
690
  similarity_threshold: relaxedNumber().optional().default(0.25),
504
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)"),
505
696
  hint_on_empty: z.boolean().optional().default(true).describe("Include hint when no results found (default: true)")
506
697
  });
507
698
  var SemanticEntryOutputSchema = EntryOutputSchema.extend({
@@ -509,6 +700,7 @@ var SemanticEntryOutputSchema = EntryOutputSchema.extend({
509
700
  });
510
701
  var SemanticSearchOutputSchema = z.object({
511
702
  query: z.string().optional(),
703
+ entryId: z.number().optional(),
512
704
  entries: z.array(SemanticEntryOutputSchema).optional(),
513
705
  count: z.number().optional(),
514
706
  hint: z.string().optional(),
@@ -529,52 +721,84 @@ function getSearchTools(context) {
529
721
  {
530
722
  name: "search_entries",
531
723
  title: "Search Entries",
532
- 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.',
533
725
  group: "search",
534
726
  inputSchema: SearchEntriesSchemaMcp,
535
727
  outputSchema: EntriesListOutputSchema,
536
728
  annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
537
- handler: (params) => {
729
+ handler: async (params) => {
538
730
  try {
539
731
  const input = SearchEntriesSchema.parse(params);
540
- 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;
541
- const perDbLimit = calcPerDbLimit(input.limit, !!teamDb);
542
- let personalEntries;
543
- if (!input.query && !hasFilters) {
544
- personalEntries = db.getRecentEntries(perDbLimit, input.is_personal);
545
- } else {
546
- personalEntries = db.searchEntries(input.query || "", {
547
- limit: perDbLimit,
548
- isPersonal: input.is_personal,
549
- projectNumber: input.project_number,
550
- issueNumber: input.issue_number,
551
- prNumber: input.pr_number,
552
- prStatus: input.pr_status,
553
- workflowRunId: input.workflow_run_id
554
- });
555
- }
556
- if (teamDb && input.is_personal !== true) {
557
- let teamEntries;
558
- if (!input.query && !hasFilters) {
559
- teamEntries = teamDb.getRecentEntries(perDbLimit);
560
- } else {
561
- teamEntries = teamDb.searchEntries(input.query || "", {
562
- limit: perDbLimit,
563
- projectNumber: input.project_number,
564
- issueNumber: input.issue_number,
565
- prNumber: input.pr_number,
566
- prStatus: input.pr_status,
567
- workflowRunId: input.workflow_run_id
568
- });
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
+ };
569
800
  }
570
- const merged = mergeAndDedup(
571
- personalEntries.map((e) => ({ ...e, source: "personal" })),
572
- teamEntries.map((e) => ({ ...e, source: "team" })),
573
- input.limit
574
- );
575
- return { entries: merged, count: merged.length };
576
801
  }
577
- return { entries: personalEntries, count: personalEntries.length };
578
802
  } catch (err) {
579
803
  return formatHandlerError(err);
580
804
  }
@@ -642,7 +866,7 @@ function getSearchTools(context) {
642
866
  {
643
867
  name: "semantic_search",
644
868
  title: "Semantic Search",
645
- 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).",
646
870
  group: "search",
647
871
  inputSchema: SemanticSearchSchemaMcp,
648
872
  outputSchema: SemanticSearchOutputSchema,
@@ -650,6 +874,18 @@ function getSearchTools(context) {
650
874
  handler: async (params) => {
651
875
  try {
652
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
+ }
653
889
  if (!vectorManager) {
654
890
  return {
655
891
  success: false,
@@ -663,11 +899,20 @@ function getSearchTools(context) {
663
899
  count: 0
664
900
  };
665
901
  }
666
- const results = await vectorManager.search(
667
- input.query,
668
- input.limit ?? 10,
669
- input.similarity_threshold ?? 0.25
670
- );
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
+ }
671
916
  const entryIds = results.map((r) => r.entryId);
672
917
  const entriesMap = db.getEntriesByIds(entryIds);
673
918
  const entries = results.map((r) => {
@@ -675,6 +920,22 @@ function getSearchTools(context) {
675
920
  if (!entry) return null;
676
921
  if (input.is_personal !== void 0 && entry.isPersonal !== input.is_personal)
677
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;
678
939
  return {
679
940
  ...entry,
680
941
  similarity: Math.round(r.score * 100) / 100
@@ -689,6 +950,7 @@ function getSearchTools(context) {
689
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;
690
951
  return {
691
952
  query: input.query,
953
+ ...input.entry_id !== void 0 ? { entryId: input.entry_id } : {},
692
954
  entries,
693
955
  count: entries.length,
694
956
  ...hint !== void 0 ? { hint } : {}
@@ -728,23 +990,6 @@ function getSearchTools(context) {
728
990
  }
729
991
  ];
730
992
  }
731
- var DEDUP_KEY_LENGTH = 200;
732
- function calcPerDbLimit(limit, hasTeamDb) {
733
- return hasTeamDb ? Math.min(limit * 2, MAX_QUERY_LIMIT) : limit;
734
- }
735
- function mergeAndDedup(personal, team, limit) {
736
- const seen = /* @__PURE__ */ new Set();
737
- const merged = [];
738
- const all = [...personal, ...team].sort((a, b) => b.timestamp.localeCompare(a.timestamp));
739
- for (const entry of all) {
740
- const key = entry.content.slice(0, DEDUP_KEY_LENGTH);
741
- if (!seen.has(key)) {
742
- seen.add(key);
743
- merged.push(entry);
744
- }
745
- }
746
- return limit !== void 0 ? merged.slice(0, limit) : merged;
747
- }
748
993
  var INACTIVE_THRESHOLD_DAYS = 7;
749
994
  var MS_PER_DAY = 864e5;
750
995
  var MAX_TAGS_PER_PROJECT = 5;
@@ -1535,8 +1780,8 @@ function getAdminTools(context) {
1535
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.',
1536
1781
  group: "admin",
1537
1782
  inputSchema: z.object({
1538
- source_tag: z.string().optional().describe("Tag to merge from (will be deleted)"),
1539
- 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)")
1540
1785
  }),
1541
1786
  outputSchema: MergeTagsOutputSchema,
1542
1787
  annotations: { readOnlyHint: false, idempotentHint: false, openWorldHint: false },
@@ -1991,14 +2236,14 @@ var CopilotReviewsOutputSchema = z.object({
1991
2236
 
1992
2237
  // src/handlers/tools/github/helpers.ts
1993
2238
  function resolveProjectNumber(context, repo, explicitProjectNumber) {
1994
- if (explicitProjectNumber !== void 0) return explicitProjectNumber;
1995
- if (repo && context.config?.projectRegistry?.[repo]?.project_number !== void 0) {
2239
+ if (explicitProjectNumber != null) return explicitProjectNumber;
2240
+ if (repo && context.config?.projectRegistry?.[repo]?.project_number != null) {
1996
2241
  return context.config.projectRegistry[repo].project_number;
1997
2242
  }
1998
- return context.config?.defaultProjectNumber;
2243
+ return context.config?.defaultProjectNumber ?? void 0;
1999
2244
  }
2000
2245
  async function resolveOwner(context, inputOwner, entityLabel) {
2001
- if (!context.github) {
2246
+ if (!context.github?.isApiAvailable()) {
2002
2247
  return {
2003
2248
  error: true,
2004
2249
  response: {
@@ -2006,8 +2251,9 @@ async function resolveOwner(context, inputOwner, entityLabel) {
2006
2251
  error: "GitHub integration not available",
2007
2252
  code: "CONFIGURATION_ERROR",
2008
2253
  category: "configuration",
2009
- suggestion: "Set GITHUB_TOKEN and GITHUB_REPO_PATH environment variables to enable GitHub integration.",
2010
- recoverable: true
2254
+ suggestion: "Set GITHUB_TOKEN environment variable to enable GitHub integration.",
2255
+ recoverable: true,
2256
+ requiresUserInput: true
2011
2257
  }
2012
2258
  };
2013
2259
  }
@@ -2040,7 +2286,7 @@ async function resolveOwnerRepo(context, input, entityLabel) {
2040
2286
  } else if (context.github) {
2041
2287
  toolGithub = context.github;
2042
2288
  }
2043
- if (!toolGithub) {
2289
+ if (!toolGithub?.isApiAvailable()) {
2044
2290
  return {
2045
2291
  error: true,
2046
2292
  response: {
@@ -2048,12 +2294,16 @@ async function resolveOwnerRepo(context, input, entityLabel) {
2048
2294
  error: "GitHub integration not available",
2049
2295
  code: "CONFIGURATION_ERROR",
2050
2296
  category: "configuration",
2051
- suggestion: "Set GITHUB_TOKEN and GITHUB_REPO_PATH environment variables to enable GitHub integration.",
2052
- recoverable: true
2297
+ suggestion: "Set GITHUB_TOKEN environment variable to enable GitHub integration.",
2298
+ recoverable: true,
2299
+ requiresUserInput: true
2053
2300
  }
2054
2301
  };
2055
2302
  }
2056
2303
  const repoInfo = await toolGithub.getRepoInfo();
2304
+ if (context.github && toolGithub !== context.github) {
2305
+ context.github.setCachedRepoInfo(repoInfo);
2306
+ }
2057
2307
  const detectedOwner = repoInfo.owner;
2058
2308
  const detectedRepo = repoInfo.repo;
2059
2309
  const owner = input.owner ?? detectedOwner ?? void 0;
@@ -2357,7 +2607,11 @@ function getKanbanTools(context) {
2357
2607
  const resolved = await resolveOwner(context, input.owner);
2358
2608
  if ("error" in resolved) return resolved.response;
2359
2609
  const effectiveRepo = input.repo ?? resolved.repo;
2360
- const projectNum = resolveProjectNumber(context, effectiveRepo, input.project_number);
2610
+ const projectNum = resolveProjectNumber(
2611
+ context,
2612
+ effectiveRepo,
2613
+ input.project_number
2614
+ );
2361
2615
  if (projectNum === void 0) {
2362
2616
  return {
2363
2617
  success: false,
@@ -2419,7 +2673,11 @@ function getKanbanTools(context) {
2419
2673
  const resolved = await resolveOwner(context, input.owner);
2420
2674
  if ("error" in resolved) return resolved.response;
2421
2675
  const effectiveRepo = input.repo ?? resolved.repo;
2422
- const projectNum = resolveProjectNumber(context, effectiveRepo, input.project_number);
2676
+ const projectNum = resolveProjectNumber(
2677
+ context,
2678
+ effectiveRepo,
2679
+ input.project_number
2680
+ );
2423
2681
  if (projectNum === void 0) {
2424
2682
  return {
2425
2683
  success: false,
@@ -2839,10 +3097,11 @@ async function resolveGitHubRepo(github, config, targetRepo) {
2839
3097
  activeGithub = new GitHubIntegration(config.projectRegistry[targetRepo].path);
2840
3098
  }
2841
3099
  if (!activeGithub) {
3100
+ const hasRegistry = config?.projectRegistry && Object.keys(config.projectRegistry).length > 0;
2842
3101
  return {
2843
3102
  data: {
2844
3103
  error: "GitHub integration not available",
2845
- 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."
2846
3105
  },
2847
3106
  annotations: { lastModified }
2848
3107
  };
@@ -2851,10 +3110,11 @@ async function resolveGitHubRepo(github, config, targetRepo) {
2851
3110
  const owner = repoInfo.owner;
2852
3111
  const repo = repoInfo.repo;
2853
3112
  if (!owner || !repo) {
3113
+ const hasRegistry = config?.projectRegistry && Object.keys(config.projectRegistry).length > 0;
2854
3114
  return {
2855
3115
  data: {
2856
3116
  error: "Could not detect repository",
2857
- 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.",
2858
3118
  ...repoInfo.branch ? { branch: repoInfo.branch } : {}
2859
3119
  },
2860
3120
  annotations: { lastModified }
@@ -3059,7 +3319,7 @@ function getGitHubMilestoneTools(context) {
3059
3319
  "What GitHub repository should I create the milestone in?"
3060
3320
  );
3061
3321
  if ("error" in resolved) return resolved.response;
3062
- 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;
3063
3323
  const milestone = await resolved.github.createMilestone(
3064
3324
  resolved.owner,
3065
3325
  resolved.repo,
@@ -3120,7 +3380,7 @@ function getGitHubMilestoneTools(context) {
3120
3380
  "What GitHub repository is this milestone in?"
3121
3381
  );
3122
3382
  if ("error" in resolved) return resolved.response;
3123
- 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;
3124
3384
  const milestone = await resolved.github.updateMilestone(
3125
3385
  resolved.owner,
3126
3386
  resolved.repo,
@@ -3833,13 +4093,15 @@ var TeamBackupsListOutputSchema = z.object({
3833
4093
  error: z.string().optional()
3834
4094
  }).extend(ErrorFieldsMixin.shape);
3835
4095
  var TeamSemanticSearchSchema = z.object({
3836
- query: z.string(),
4096
+ query: z.string().optional(),
4097
+ entry_id: z.number().optional().describe("Find entries related to this entry ID"),
3837
4098
  limit: z.number().max(500).optional().default(10),
3838
4099
  similarity_threshold: z.number().optional().default(0.25),
3839
4100
  hint_on_empty: z.boolean().optional().default(true).describe("Include hint when no results found (default: true)")
3840
4101
  });
3841
4102
  var TeamSemanticSearchSchemaMcp = z.object({
3842
4103
  query: z.string().optional(),
4104
+ entry_id: relaxedNumber().optional().describe("Find entries related to this entry ID"),
3843
4105
  limit: relaxedNumber().optional().default(10),
3844
4106
  similarity_threshold: relaxedNumber().optional().default(0.25),
3845
4107
  hint_on_empty: z.boolean().optional().default(true).describe("Include hint when no results found (default: true)")
@@ -3855,6 +4117,7 @@ var TeamSemanticEntryOutputSchema = TeamEntryOutputSchema.extend({
3855
4117
  });
3856
4118
  var TeamSemanticSearchOutputSchema = z.object({
3857
4119
  query: z.string().optional(),
4120
+ entryId: z.number().optional(),
3858
4121
  entries: z.array(TeamSemanticEntryOutputSchema).optional(),
3859
4122
  count: z.number().optional(),
3860
4123
  hint: z.string().optional(),
@@ -4828,6 +5091,18 @@ function getTeamVectorTools(context) {
4828
5091
  return { ...TEAM_DB_ERROR_RESPONSE };
4829
5092
  }
4830
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
+ }
4831
5106
  if (!teamVectorManager) {
4832
5107
  return {
4833
5108
  success: false,
@@ -4841,17 +5116,28 @@ function getTeamVectorTools(context) {
4841
5116
  count: 0
4842
5117
  };
4843
5118
  }
4844
- const results = await teamVectorManager.search(
4845
- input.query,
4846
- input.limit ?? 10,
4847
- input.similarity_threshold ?? 0.25
4848
- );
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
+ }
4849
5133
  const entryIds = results.map((r) => r.entryId);
4850
5134
  const entriesMap = teamDb.getEntriesByIds(entryIds);
4851
5135
  const authorMap = batchFetchAuthors(teamDb, entryIds);
4852
5136
  const entries = results.map((r) => {
4853
5137
  const entry = entriesMap.get(r.entryId);
4854
5138
  if (!entry) return null;
5139
+ if (input.entry_id !== void 0 && entry.id === input.entry_id)
5140
+ return null;
4855
5141
  return {
4856
5142
  ...entry,
4857
5143
  author: authorMap.get(r.entryId) ?? null,
@@ -4866,6 +5152,7 @@ function getTeamVectorTools(context) {
4866
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;
4867
5153
  return {
4868
5154
  query: input.query,
5155
+ ...input.entry_id !== void 0 ? { entryId: input.entry_id } : {},
4869
5156
  entries,
4870
5157
  count: entries.length,
4871
5158
  ...hint !== void 0 ? { hint } : {}
@@ -5103,8 +5390,10 @@ var GROUP_EXAMPLES = {
5103
5390
  ],
5104
5391
  search: [
5105
5392
  'mj.search.searchEntries({ query: "performance" })',
5393
+ 'mj.search.searchEntries({ query: "performance", mode: "hybrid" })',
5106
5394
  'mj.search.searchByDateRange({ start_date: "2026-03-01", end_date: "2026-03-11" })',
5107
- 'mj.search.semanticSearch({ query: "authentication patterns" })'
5395
+ 'mj.search.semanticSearch({ query: "authentication patterns" })',
5396
+ "mj.search.semanticSearch({ entry_id: 42 })"
5108
5397
  ],
5109
5398
  analytics: ["mj.analytics.getStatistics()", "mj.analytics.getCrossProjectInsights()"],
5110
5399
  relationships: [
@@ -6006,7 +6295,836 @@ function getCodeModeTools(context) {
6006
6295
  ];
6007
6296
  }
6008
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
+
6009
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
+ }
6010
7128
  function getToolIcon(group) {
6011
7129
  const iconMap = {
6012
7130
  core: {
@@ -6090,11 +7208,25 @@ function ensureToolCache(db, vectorManager, github, config, teamDb, teamVectorMa
6090
7208
  return;
6091
7209
  }
6092
7210
  const context = { db, teamDb, vectorManager, teamVectorManager, github, config };
6093
- 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]));
6094
7226
  mappedToolsCache = null;
6095
7227
  cachedContextRefs = { db, github, vectorManager, config, teamDb, teamVectorManager };
6096
7228
  }
6097
- function callTool(name, args, db, vectorManager, github, config, progress, teamDb, teamVectorManager) {
7229
+ async function callTool(name, args, db, vectorManager, github, config, progress, teamDb, teamVectorManager) {
6098
7230
  ensureToolCache(db, vectorManager, github, config, teamDb, teamVectorManager);
6099
7231
  const tool = (toolMapCache ?? EMPTY_TOOL_MAP).get(name);
6100
7232
  if (!tool) {
@@ -6113,10 +7245,18 @@ function callTool(name, args, db, vectorManager, github, config, progress, teamD
6113
7245
  const freshTools = getAllToolDefinitions(context);
6114
7246
  const freshTool = freshTools.find((t) => t.name === name);
6115
7247
  if (freshTool) {
6116
- 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);
6117
7256
  }
6118
7257
  }
6119
- return Promise.resolve(tool.handler(args));
7258
+ const result = await Promise.resolve(tool.handler(args));
7259
+ return injectTokenEstimate(result);
6120
7260
  }
6121
7261
  function getAllToolDefinitions(context) {
6122
7262
  return [
@@ -6133,4 +7273,4 @@ function getAllToolDefinitions(context) {
6133
7273
  ];
6134
7274
  }
6135
7275
 
6136
- export { DEFAULT_BRIEFING_CONFIG, callTool, execQuery, getTools, isResourceError, milestoneCompletionPct, resolveGitHubRepo, sendProgress, setDefaultSandboxMode, transformEntryRow };
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 };