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.
- package/README.md +91 -74
- package/dist/{chunk-VH4SRTLB.js → chunk-2BJHLTYP.js} +1237 -97
- package/dist/{chunk-K2SCUSN4.js → chunk-ARLH46WS.js} +34 -3
- package/dist/{chunk-QQ2ZY3CH.js → chunk-SBNQ7MXZ.js} +412 -365
- package/dist/cli.js +26 -4
- package/dist/github-integration-PDRLXKGM.js +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +3 -3
- package/dist/tools-FFFGXIKN.js +3 -0
- package/package.json +14 -13
- package/dist/github-integration-DTNYAKVJ.js +0 -1
- package/dist/tools-P4XXHO3Z.js +0 -3
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { transformAutoReturn } from './chunk-OKOVZ5QE.js';
|
|
2
|
-
import { GitHubIntegration, resolveAuthor, logger, MemoryJournalMcpError, matchSuggestion, ConfigurationError } from './chunk-
|
|
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-
|
|
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: '
|
|
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
|
|
541
|
-
const
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
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
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
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
|
|
1995
|
-
if (repo && context.config?.projectRegistry?.[repo]?.project_number
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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: "
|
|
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
|
-
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 };
|