@coreyuan/vector-mind 1.0.19 → 1.0.30
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 +40 -1
- package/dist/builtin-conventions.d.ts +6 -0
- package/dist/builtin-conventions.js +93 -0
- package/dist/builtin-conventions.js.map +1 -0
- package/dist/builtin-instructions.d.ts +4 -0
- package/dist/builtin-instructions.js +5 -0
- package/dist/builtin-instructions.js.map +1 -0
- package/dist/builtin-policy.d.ts +7 -0
- package/dist/builtin-policy.js +24 -0
- package/dist/builtin-policy.js.map +1 -0
- package/dist/index.js +1291 -9
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import fs from "node:fs";
|
|
4
|
+
import * as readline from "node:readline";
|
|
4
5
|
import crypto from "node:crypto";
|
|
5
6
|
import os from "node:os";
|
|
6
7
|
import { fileURLToPath } from "node:url";
|
|
@@ -11,8 +12,10 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
|
11
12
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
12
13
|
import { toJsonSchemaCompat } from "@modelcontextprotocol/sdk/server/zod-json-schema-compat.js";
|
|
13
14
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
15
|
+
import { BUILTIN_CONVENTIONS } from "./builtin-conventions.js";
|
|
16
|
+
import { BUILTIN_ARCHITECTURE_AND_CODE_ORGANIZATION_INSTRUCTIONS, BUILTIN_DESTRUCTIVE_OPERATION_GUARD_INSTRUCTIONS, BUILTIN_PLAN_LITE_INSTRUCTIONS, BUILTIN_WRITE_POLICY_INSTRUCTIONS, } from "./builtin-instructions.js";
|
|
14
17
|
const SERVER_NAME = "vector-mind";
|
|
15
|
-
const SERVER_VERSION = "1.0.
|
|
18
|
+
const SERVER_VERSION = "1.0.30";
|
|
16
19
|
const rootFromEnv = process.env.VECTORMIND_ROOT?.trim() ?? "";
|
|
17
20
|
const prettyJsonOutput = ["1", "true", "on", "yes"].includes((process.env.VECTORMIND_PRETTY_JSON ?? "").trim().toLowerCase());
|
|
18
21
|
const debugLogEnabled = ["1", "true", "on", "yes"].includes((process.env.VECTORMIND_DEBUG_LOG ?? "").trim().toLowerCase());
|
|
@@ -61,6 +64,58 @@ const PENDING_PRUNE_EVERY = (() => {
|
|
|
61
64
|
return 500;
|
|
62
65
|
return n;
|
|
63
66
|
})();
|
|
67
|
+
function getCodexHomeDir() {
|
|
68
|
+
const raw = process.env.CODEX_HOME?.trim();
|
|
69
|
+
if (raw)
|
|
70
|
+
return path.resolve(raw);
|
|
71
|
+
return path.join(os.homedir(), ".codex");
|
|
72
|
+
}
|
|
73
|
+
function getAgentsHomeDir() {
|
|
74
|
+
const raw = process.env.AGENTS_HOME?.trim();
|
|
75
|
+
if (raw)
|
|
76
|
+
return path.resolve(raw);
|
|
77
|
+
return path.join(os.homedir(), ".agents");
|
|
78
|
+
}
|
|
79
|
+
function getAllowedCodexTextRoots() {
|
|
80
|
+
const codexHome = getCodexHomeDir();
|
|
81
|
+
const agentsHome = getAgentsHomeDir();
|
|
82
|
+
return Array.from(new Set([
|
|
83
|
+
path.join(codexHome, "skills"),
|
|
84
|
+
path.join(codexHome, "prompts"),
|
|
85
|
+
path.join(codexHome, "rules"),
|
|
86
|
+
path.join(agentsHome, "skills"),
|
|
87
|
+
].map((p) => path.resolve(p))));
|
|
88
|
+
}
|
|
89
|
+
const INDEX_MAX_CODE_BYTES = (() => {
|
|
90
|
+
const raw = process.env.VECTORMIND_INDEX_MAX_CODE_BYTES?.trim();
|
|
91
|
+
if (!raw)
|
|
92
|
+
return 400_000;
|
|
93
|
+
const n = Number.parseInt(raw, 10);
|
|
94
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
95
|
+
return 400_000;
|
|
96
|
+
return n;
|
|
97
|
+
})();
|
|
98
|
+
const INDEX_MAX_DOC_BYTES = (() => {
|
|
99
|
+
const raw = process.env.VECTORMIND_INDEX_MAX_DOC_BYTES?.trim();
|
|
100
|
+
if (!raw)
|
|
101
|
+
return 600_000;
|
|
102
|
+
const n = Number.parseInt(raw, 10);
|
|
103
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
104
|
+
return 600_000;
|
|
105
|
+
return n;
|
|
106
|
+
})();
|
|
107
|
+
const INDEX_SKIP_MINIFIED = (() => {
|
|
108
|
+
const raw = (process.env.VECTORMIND_INDEX_SKIP_MINIFIED ?? "").trim().toLowerCase();
|
|
109
|
+
if (!raw)
|
|
110
|
+
return true;
|
|
111
|
+
return ["1", "true", "on", "yes"].includes(raw);
|
|
112
|
+
})();
|
|
113
|
+
const INDEX_AUTO_PRUNE_IGNORED = (() => {
|
|
114
|
+
const raw = (process.env.VECTORMIND_INDEX_AUTO_PRUNE_IGNORED ?? "").trim().toLowerCase();
|
|
115
|
+
if (!raw)
|
|
116
|
+
return true;
|
|
117
|
+
return ["1", "true", "on", "yes"].includes(raw);
|
|
118
|
+
})();
|
|
64
119
|
const ROOTS_LIST_TIMEOUT_MS = (() => {
|
|
65
120
|
const raw = process.env.VECTORMIND_ROOTS_TIMEOUT_MS?.trim();
|
|
66
121
|
if (!raw)
|
|
@@ -197,8 +252,18 @@ function summarizeActivityEvent(e) {
|
|
|
197
252
|
return `pending_list returned=${String(d.returned ?? "")} total=${String(d.total ?? "")}`;
|
|
198
253
|
case "semantic_search":
|
|
199
254
|
return `semantic_search mode=${String(d.mode ?? "")} q=${String(d.query ?? "")} matches=${String(d.matches ?? "")}`;
|
|
255
|
+
case "grep":
|
|
256
|
+
return `grep q=${String(d.query ?? "")} matches=${String(d.matches ?? "")} truncated=${String(d.truncated ?? "")}`;
|
|
200
257
|
case "query_codebase":
|
|
201
258
|
return `query_codebase q=${String(d.query ?? "")} matches=${String(d.matches ?? "")}`;
|
|
259
|
+
case "read_file_lines":
|
|
260
|
+
return `read_file_lines file=${String(d.file_path ?? "")} returned=${String(d.returned ?? "")} truncated=${String(d.truncated ?? "")}`;
|
|
261
|
+
case "read_file_text":
|
|
262
|
+
return `read_file_text file=${String(d.file_path ?? "")} returned=${String(d.returned_chars ?? "")}/${String(d.total_chars ?? "")} truncated=${String(d.truncated ?? "")}`;
|
|
263
|
+
case "list_project_files":
|
|
264
|
+
return `list_project_files path=${String(d.path ?? "")} returned=${String(d.returned ?? "")} scanned=${String(d.scanned ?? "")} truncated=${String(d.truncated ?? "")}`;
|
|
265
|
+
case "read_codex_text_file":
|
|
266
|
+
return `read_codex_text_file file=${String(d.file_path ?? "")} returned=${String(d.returned_chars ?? "")}/${String(d.total_chars ?? "")} truncated=${String(d.truncated ?? "")}`;
|
|
202
267
|
case "start_requirement":
|
|
203
268
|
return `start_requirement #${String(d.req_id ?? "")} ${String(d.title ?? "")}`;
|
|
204
269
|
case "sync_change_intent":
|
|
@@ -406,6 +471,10 @@ const IGNORED_PATH_SEGMENTS = new Set([
|
|
|
406
471
|
".next",
|
|
407
472
|
".nuxt",
|
|
408
473
|
".svelte-kit",
|
|
474
|
+
".turbo",
|
|
475
|
+
".nx",
|
|
476
|
+
".cache",
|
|
477
|
+
".parcel-cache",
|
|
409
478
|
// .NET / VS build artifacts
|
|
410
479
|
"bin",
|
|
411
480
|
"obj",
|
|
@@ -414,9 +483,11 @@ const IGNORED_PATH_SEGMENTS = new Set([
|
|
|
414
483
|
// General build outputs
|
|
415
484
|
"dist",
|
|
416
485
|
"build",
|
|
486
|
+
"buildfiles",
|
|
417
487
|
"out",
|
|
418
488
|
"target",
|
|
419
489
|
"coverage",
|
|
490
|
+
"artifacts",
|
|
420
491
|
// Python caches/venvs
|
|
421
492
|
"__pycache__",
|
|
422
493
|
".pytest_cache",
|
|
@@ -465,7 +536,9 @@ function pruneIgnoredPendingChanges() {
|
|
|
465
536
|
try {
|
|
466
537
|
if (!IGNORED_LIKE_PATTERNS.length)
|
|
467
538
|
return;
|
|
468
|
-
const where = IGNORED_LIKE_PATTERNS
|
|
539
|
+
const where = IGNORED_LIKE_PATTERNS
|
|
540
|
+
.map(() => "LOWER(REPLACE(file_path, '\\\\', '/')) LIKE ?")
|
|
541
|
+
.join(" OR ");
|
|
469
542
|
db.prepare(`DELETE FROM pending_changes WHERE ${where}`).run(...IGNORED_LIKE_PATTERNS);
|
|
470
543
|
}
|
|
471
544
|
catch (err) {
|
|
@@ -498,6 +571,102 @@ function prunePendingChanges() {
|
|
|
498
571
|
console.error("[vectormind] prune pending_changes failed:", err);
|
|
499
572
|
}
|
|
500
573
|
}
|
|
574
|
+
function pruneIgnoredIndexesByPathPatterns() {
|
|
575
|
+
if (!db)
|
|
576
|
+
return { chunks_deleted: 0, symbols_deleted: 0 };
|
|
577
|
+
try {
|
|
578
|
+
if (!IGNORED_LIKE_PATTERNS.length)
|
|
579
|
+
return { chunks_deleted: 0, symbols_deleted: 0 };
|
|
580
|
+
const where = IGNORED_LIKE_PATTERNS
|
|
581
|
+
.map(() => "LOWER(REPLACE(file_path, '\\\\', '/')) LIKE ?")
|
|
582
|
+
.join(" OR ");
|
|
583
|
+
const chunksDeleted = db
|
|
584
|
+
.prepare(`DELETE FROM memory_items
|
|
585
|
+
WHERE file_path IS NOT NULL
|
|
586
|
+
AND (kind = 'code_chunk' OR kind = 'doc_chunk')
|
|
587
|
+
AND (${where})`)
|
|
588
|
+
.run(...IGNORED_LIKE_PATTERNS).changes;
|
|
589
|
+
const symbolsDeleted = db
|
|
590
|
+
.prepare(`DELETE FROM symbols
|
|
591
|
+
WHERE file_path IS NOT NULL
|
|
592
|
+
AND (${where})`)
|
|
593
|
+
.run(...IGNORED_LIKE_PATTERNS).changes;
|
|
594
|
+
if (chunksDeleted || symbolsDeleted) {
|
|
595
|
+
logActivity("index_prune", {
|
|
596
|
+
reason: "ignored_paths",
|
|
597
|
+
chunks_deleted: chunksDeleted,
|
|
598
|
+
symbols_deleted: symbolsDeleted,
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
return { chunks_deleted: chunksDeleted, symbols_deleted: symbolsDeleted };
|
|
602
|
+
}
|
|
603
|
+
catch (err) {
|
|
604
|
+
console.error("[vectormind] prune indexes failed:", err);
|
|
605
|
+
return { chunks_deleted: 0, symbols_deleted: 0 };
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
function pruneFilenameNoiseIndexes() {
|
|
609
|
+
if (!db)
|
|
610
|
+
return { chunks_deleted: 0, symbols_deleted: 0 };
|
|
611
|
+
const suffixes = [
|
|
612
|
+
".min.js",
|
|
613
|
+
".min.css",
|
|
614
|
+
".bundle.js",
|
|
615
|
+
".bundle.css",
|
|
616
|
+
".chunk.js",
|
|
617
|
+
".chunk.css",
|
|
618
|
+
];
|
|
619
|
+
const baseNames = [
|
|
620
|
+
"package-lock.json",
|
|
621
|
+
"pnpm-lock.yaml",
|
|
622
|
+
"yarn.lock",
|
|
623
|
+
"bun.lockb",
|
|
624
|
+
"cargo.lock",
|
|
625
|
+
"composer.lock",
|
|
626
|
+
];
|
|
627
|
+
try {
|
|
628
|
+
const suffixWhere = suffixes.map(() => "LOWER(file_path) LIKE ?").join(" OR ");
|
|
629
|
+
const baseWhere = baseNames.map(() => "LOWER(file_path) LIKE ?").join(" OR ");
|
|
630
|
+
const suffixArgs = suffixes.map((s) => `%${s}`);
|
|
631
|
+
const baseArgs = baseNames.map((n) => `%/${n}`);
|
|
632
|
+
const whereParts = [];
|
|
633
|
+
const args = [];
|
|
634
|
+
if (suffixWhere) {
|
|
635
|
+
whereParts.push(`(${suffixWhere})`);
|
|
636
|
+
args.push(...suffixArgs);
|
|
637
|
+
}
|
|
638
|
+
if (baseWhere) {
|
|
639
|
+
whereParts.push(`(${baseWhere})`);
|
|
640
|
+
args.push(...baseArgs);
|
|
641
|
+
}
|
|
642
|
+
if (!whereParts.length)
|
|
643
|
+
return { chunks_deleted: 0, symbols_deleted: 0 };
|
|
644
|
+
const where = whereParts.join(" OR ");
|
|
645
|
+
const chunksDeleted = db
|
|
646
|
+
.prepare(`DELETE FROM memory_items
|
|
647
|
+
WHERE file_path IS NOT NULL
|
|
648
|
+
AND (kind = 'code_chunk' OR kind = 'doc_chunk')
|
|
649
|
+
AND (${where})`)
|
|
650
|
+
.run(...args).changes;
|
|
651
|
+
const symbolsDeleted = db
|
|
652
|
+
.prepare(`DELETE FROM symbols
|
|
653
|
+
WHERE file_path IS NOT NULL
|
|
654
|
+
AND (${where})`)
|
|
655
|
+
.run(...args).changes;
|
|
656
|
+
if (chunksDeleted || symbolsDeleted) {
|
|
657
|
+
logActivity("index_prune", {
|
|
658
|
+
reason: "filename_noise",
|
|
659
|
+
chunks_deleted: chunksDeleted,
|
|
660
|
+
symbols_deleted: symbolsDeleted,
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
return { chunks_deleted: chunksDeleted, symbols_deleted: symbolsDeleted };
|
|
664
|
+
}
|
|
665
|
+
catch (err) {
|
|
666
|
+
console.error("[vectormind] prune filename noise failed:", err);
|
|
667
|
+
return { chunks_deleted: 0, symbols_deleted: 0 };
|
|
668
|
+
}
|
|
669
|
+
}
|
|
501
670
|
function shouldIgnorePath(inputPath) {
|
|
502
671
|
const normalizedAbs = path.resolve(inputPath);
|
|
503
672
|
const rel = path.relative(projectRoot, normalizedAbs);
|
|
@@ -515,6 +684,8 @@ function shouldIgnorePath(inputPath) {
|
|
|
515
684
|
return false;
|
|
516
685
|
}
|
|
517
686
|
function isSymbolIndexableFile(filePath) {
|
|
687
|
+
if (shouldIgnoreContentFile(filePath))
|
|
688
|
+
return false;
|
|
518
689
|
const ext = path.extname(filePath).toLowerCase();
|
|
519
690
|
const allowed = new Set([
|
|
520
691
|
".ts",
|
|
@@ -551,6 +722,56 @@ function shouldIgnoreContentFile(filePath) {
|
|
|
551
722
|
return true;
|
|
552
723
|
if (base.endsWith(".min.js") || base.endsWith(".min.css"))
|
|
553
724
|
return true;
|
|
725
|
+
if (base.endsWith(".bundle.js") || base.endsWith(".bundle.css"))
|
|
726
|
+
return true;
|
|
727
|
+
if (base.endsWith(".chunk.js") || base.endsWith(".chunk.css"))
|
|
728
|
+
return true;
|
|
729
|
+
return false;
|
|
730
|
+
}
|
|
731
|
+
function looksLikeGeneratedFile(content) {
|
|
732
|
+
const head = content.slice(0, 4000).toLowerCase();
|
|
733
|
+
if (head.includes("@generated"))
|
|
734
|
+
return true;
|
|
735
|
+
if (head.includes("do not edit") && (head.includes("generated") || head.includes("auto-generated"))) {
|
|
736
|
+
return true;
|
|
737
|
+
}
|
|
738
|
+
if (head.includes("this file was generated") && head.includes("do not edit"))
|
|
739
|
+
return true;
|
|
740
|
+
return false;
|
|
741
|
+
}
|
|
742
|
+
function looksLikeMinifiedBundle(content) {
|
|
743
|
+
if (content.length < 30_000)
|
|
744
|
+
return false;
|
|
745
|
+
let lines = 1;
|
|
746
|
+
let currentLen = 0;
|
|
747
|
+
let maxLineLen = 0;
|
|
748
|
+
let longLines = 0;
|
|
749
|
+
for (let i = 0; i < content.length; i++) {
|
|
750
|
+
const code = content.charCodeAt(i);
|
|
751
|
+
if (code === 10 /* \\n */) {
|
|
752
|
+
if (currentLen > maxLineLen)
|
|
753
|
+
maxLineLen = currentLen;
|
|
754
|
+
if (currentLen >= 800)
|
|
755
|
+
longLines += 1;
|
|
756
|
+
currentLen = 0;
|
|
757
|
+
lines += 1;
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
760
|
+
currentLen += 1;
|
|
761
|
+
}
|
|
762
|
+
if (currentLen > maxLineLen)
|
|
763
|
+
maxLineLen = currentLen;
|
|
764
|
+
if (currentLen >= 800)
|
|
765
|
+
longLines += 1;
|
|
766
|
+
const avgLineLen = content.length / Math.max(1, lines);
|
|
767
|
+
if (lines <= 2 && maxLineLen >= 2000)
|
|
768
|
+
return true;
|
|
769
|
+
if (maxLineLen >= 6000)
|
|
770
|
+
return true;
|
|
771
|
+
if (avgLineLen >= 900)
|
|
772
|
+
return true;
|
|
773
|
+
if (lines <= 10 && longLines >= Math.ceil(lines * 0.6))
|
|
774
|
+
return true;
|
|
554
775
|
return false;
|
|
555
776
|
}
|
|
556
777
|
function getContentChunkKind(filePath) {
|
|
@@ -853,6 +1074,9 @@ function indexFile(absPath, reason) {
|
|
|
853
1074
|
const indexContent = isContentIndexableFile(absPath);
|
|
854
1075
|
if (!indexSymbols && !indexContent)
|
|
855
1076
|
return;
|
|
1077
|
+
const kind = getContentChunkKind(absPath);
|
|
1078
|
+
if (!kind)
|
|
1079
|
+
return;
|
|
856
1080
|
let stat;
|
|
857
1081
|
try {
|
|
858
1082
|
stat = fs.statSync(absPath);
|
|
@@ -862,7 +1086,8 @@ function indexFile(absPath, reason) {
|
|
|
862
1086
|
}
|
|
863
1087
|
if (!stat.isFile())
|
|
864
1088
|
return;
|
|
865
|
-
|
|
1089
|
+
const maxBytes = kind === "code_chunk" ? INDEX_MAX_CODE_BYTES : INDEX_MAX_DOC_BYTES;
|
|
1090
|
+
if (maxBytes > 0 && stat.size > maxBytes)
|
|
866
1091
|
return;
|
|
867
1092
|
let content;
|
|
868
1093
|
try {
|
|
@@ -873,7 +1098,19 @@ function indexFile(absPath, reason) {
|
|
|
873
1098
|
}
|
|
874
1099
|
if (content.includes("\u0000"))
|
|
875
1100
|
return;
|
|
1101
|
+
const ext = path.extname(absPath).toLowerCase();
|
|
876
1102
|
const filePath = normalizeToDbPath(absPath);
|
|
1103
|
+
if (INDEX_SKIP_MINIFIED &&
|
|
1104
|
+
kind === "code_chunk" &&
|
|
1105
|
+
(ext === ".js" || ext === ".mjs" || ext === ".cjs" || ext === ".css") &&
|
|
1106
|
+
looksLikeMinifiedBundle(content)) {
|
|
1107
|
+
logActivity("index_skip", { file_path: filePath, reason: "minified_bundle", bytes: stat.size });
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
if (kind === "code_chunk" && stat.size >= 20_000 && looksLikeGeneratedFile(content)) {
|
|
1111
|
+
logActivity("index_skip", { file_path: filePath, reason: "generated_file", bytes: stat.size });
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
877
1114
|
let symbolCount = 0;
|
|
878
1115
|
let chunkCount = 0;
|
|
879
1116
|
if (indexSymbols) {
|
|
@@ -931,6 +1168,65 @@ const SyncChangeIntentArgsSchema = ProjectRootArgSchema.merge(z.object({
|
|
|
931
1168
|
const QueryCodebaseArgsSchema = ProjectRootArgSchema.merge(z.object({
|
|
932
1169
|
query: z.string().min(1),
|
|
933
1170
|
}));
|
|
1171
|
+
const GrepArgsSchema = ProjectRootArgSchema.merge(z.object({
|
|
1172
|
+
// Pattern to search for. Defaults to regex mode for parity with tools like ripgrep.
|
|
1173
|
+
query: z.string().min(1),
|
|
1174
|
+
mode: z.enum(["regex", "literal"]).optional().default("regex"),
|
|
1175
|
+
// If case_sensitive is omitted and smart_case=true, uppercase => case-sensitive, otherwise case-insensitive.
|
|
1176
|
+
smart_case: z.boolean().optional().default(true),
|
|
1177
|
+
case_sensitive: z.boolean().optional(),
|
|
1178
|
+
// Optional hint used to narrow candidates quickly when mode=regex and the pattern has few literals.
|
|
1179
|
+
literal_hint: z.string().optional().default(""),
|
|
1180
|
+
// Defaults to code/doc chunks; can be widened if needed.
|
|
1181
|
+
kinds: z.array(z.string().min(1)).optional(),
|
|
1182
|
+
include_paths: z.array(z.string().min(1)).optional(),
|
|
1183
|
+
exclude_paths: z.array(z.string().min(1)).optional(),
|
|
1184
|
+
max_results: z.number().int().min(1).max(5000).optional().default(200),
|
|
1185
|
+
max_candidates: z.number().int().min(1).max(50_000).optional(),
|
|
1186
|
+
}));
|
|
1187
|
+
const ReadFileLinesArgsSchema = ProjectRootArgSchema.merge(z.object({
|
|
1188
|
+
// Relative to project_root, or an absolute path under project_root.
|
|
1189
|
+
path: z.string().min(1),
|
|
1190
|
+
from_line: z.number().int().min(1).optional().default(1),
|
|
1191
|
+
to_line: z.number().int().min(1).optional(),
|
|
1192
|
+
// Convenience for "head": if set, reads from_line..(from_line+total_count-1) unless to_line is provided.
|
|
1193
|
+
total_count: z.number().int().min(1).optional(),
|
|
1194
|
+
// Hard limits to avoid huge token blow-ups.
|
|
1195
|
+
max_lines: z.number().int().min(1).max(2000).optional().default(400),
|
|
1196
|
+
max_chars: z.number().int().min(200).max(200_000).optional().default(20_000),
|
|
1197
|
+
}));
|
|
1198
|
+
const ReadFileTextArgsSchema = ProjectRootArgSchema.merge(z.object({
|
|
1199
|
+
// Relative to project_root, or an absolute path under project_root.
|
|
1200
|
+
path: z.string().min(1),
|
|
1201
|
+
// Character offset in the decoded UTF-8 text.
|
|
1202
|
+
offset: z.number().int().min(0).optional().default(0),
|
|
1203
|
+
// Hard limit on returned text to avoid huge outputs.
|
|
1204
|
+
max_chars: z.number().int().min(1).max(200_000).optional().default(20_000),
|
|
1205
|
+
// Safety guard for raw reads; use read_file_lines on larger files.
|
|
1206
|
+
max_file_bytes: z.number().int().min(1_000).max(5_000_000).optional().default(1_000_000),
|
|
1207
|
+
}));
|
|
1208
|
+
const ReadCodexTextFileArgsSchema = ProjectRootArgSchema.merge(z.object({
|
|
1209
|
+
// Absolute path, file:// URI, or a path under CODEX_HOME / AGENTS_HOME allowed roots.
|
|
1210
|
+
path: z.string().min(1),
|
|
1211
|
+
offset: z.number().int().min(0).optional().default(0),
|
|
1212
|
+
max_chars: z.number().int().min(1).max(200_000).optional().default(20_000),
|
|
1213
|
+
max_file_bytes: z.number().int().min(1_000).max(5_000_000).optional().default(1_000_000),
|
|
1214
|
+
}));
|
|
1215
|
+
const ListProjectFilesArgsSchema = ProjectRootArgSchema.merge(z.object({
|
|
1216
|
+
// Relative directory/file path under project_root. "." means the project root.
|
|
1217
|
+
path: z.string().optional().default("."),
|
|
1218
|
+
recursive: z.boolean().optional().default(false),
|
|
1219
|
+
max_depth: z.number().int().min(1).max(20).optional().default(4),
|
|
1220
|
+
include_files: z.boolean().optional().default(true),
|
|
1221
|
+
include_dirs: z.boolean().optional().default(true),
|
|
1222
|
+
include_hidden: z.boolean().optional().default(false),
|
|
1223
|
+
respect_ignore: z.boolean().optional().default(true),
|
|
1224
|
+
include_paths: z.array(z.string().min(1)).optional(),
|
|
1225
|
+
exclude_paths: z.array(z.string().min(1)).optional(),
|
|
1226
|
+
extensions: z.array(z.string().min(1)).optional(),
|
|
1227
|
+
max_results: z.number().int().min(1).max(5000).optional().default(200),
|
|
1228
|
+
include_stats: z.boolean().optional().default(false),
|
|
1229
|
+
}));
|
|
934
1230
|
const UpsertProjectSummaryArgsSchema = ProjectRootArgSchema.merge(z.object({
|
|
935
1231
|
summary: z.string().min(1),
|
|
936
1232
|
}));
|
|
@@ -939,6 +1235,13 @@ const AddNoteArgsSchema = ProjectRootArgSchema.merge(z.object({
|
|
|
939
1235
|
content: z.string().min(1),
|
|
940
1236
|
tags: z.array(z.string().min(1)).optional(),
|
|
941
1237
|
}));
|
|
1238
|
+
const PruneIndexArgsSchema = ProjectRootArgSchema.merge(z.object({
|
|
1239
|
+
dry_run: z.boolean().optional().default(true),
|
|
1240
|
+
prune_ignored_paths: z.boolean().optional().default(true),
|
|
1241
|
+
prune_minified_bundles: z.boolean().optional().default(false),
|
|
1242
|
+
max_files: z.number().int().min(1).max(50_000).optional().default(2000),
|
|
1243
|
+
vacuum: z.boolean().optional().default(false),
|
|
1244
|
+
}));
|
|
942
1245
|
const UpsertConventionArgsSchema = ProjectRootArgSchema.merge(z.object({
|
|
943
1246
|
key: z.string().min(1),
|
|
944
1247
|
content: z.string().min(1),
|
|
@@ -1220,6 +1523,34 @@ function toMemoryItemPreview(row, includeContent, previewChars, contentMaxChars)
|
|
|
1220
1523
|
updated_at: row.updated_at,
|
|
1221
1524
|
};
|
|
1222
1525
|
}
|
|
1526
|
+
function getBuiltinConventionRows() {
|
|
1527
|
+
return BUILTIN_CONVENTIONS.map((spec, idx) => ({
|
|
1528
|
+
id: -1000 - idx,
|
|
1529
|
+
kind: "convention",
|
|
1530
|
+
title: spec.key,
|
|
1531
|
+
content: spec.content,
|
|
1532
|
+
file_path: null,
|
|
1533
|
+
start_line: null,
|
|
1534
|
+
end_line: null,
|
|
1535
|
+
req_id: null,
|
|
1536
|
+
metadata_json: safeJson({ source: "builtin", key: spec.key, tags: spec.tags ?? [] }),
|
|
1537
|
+
content_hash: sha256Hex(spec.content),
|
|
1538
|
+
created_at: "builtin",
|
|
1539
|
+
updated_at: "builtin",
|
|
1540
|
+
}));
|
|
1541
|
+
}
|
|
1542
|
+
function getConventionPreviews(conventionsLimit, previewChars, contentMaxChars) {
|
|
1543
|
+
if (conventionsLimit <= 0)
|
|
1544
|
+
return [];
|
|
1545
|
+
const builtin = getBuiltinConventionRows()
|
|
1546
|
+
.map((row) => toMemoryItemPreview(row, false, previewChars, contentMaxChars))
|
|
1547
|
+
.slice(0, conventionsLimit);
|
|
1548
|
+
if (builtin.length >= conventionsLimit)
|
|
1549
|
+
return builtin;
|
|
1550
|
+
const remaining = conventionsLimit - builtin.length;
|
|
1551
|
+
const stored = listConventionsStmt.all(remaining).map((c) => toMemoryItemPreview(c, false, previewChars, contentMaxChars));
|
|
1552
|
+
return [...builtin, ...stored];
|
|
1553
|
+
}
|
|
1223
1554
|
function toRequirementPreview(req, includeContent, previewChars, contentMaxChars) {
|
|
1224
1555
|
const context = req.context_data ?? null;
|
|
1225
1556
|
const contextPreview = context ? makePreviewText(context, previewChars) : null;
|
|
@@ -1478,18 +1809,357 @@ async function semanticSearchHybridInternal(opts) {
|
|
|
1478
1809
|
}
|
|
1479
1810
|
return likeSearchInternal(opts);
|
|
1480
1811
|
}
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1812
|
+
function escapeRegExp(literal) {
|
|
1813
|
+
return literal.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1814
|
+
}
|
|
1815
|
+
function hasUppercaseAscii(s) {
|
|
1816
|
+
return /[A-Z]/.test(s);
|
|
1817
|
+
}
|
|
1818
|
+
function extractLongestLiteralFromRegex(pattern) {
|
|
1819
|
+
// Best-effort extraction: pull the longest literal run to use as an indexed candidate hint.
|
|
1820
|
+
// This is intentionally conservative; if we can't find a reasonable literal anchor, callers
|
|
1821
|
+
// should pass `literal_hint` or narrow with include_paths.
|
|
1822
|
+
let best = "";
|
|
1823
|
+
let cur = "";
|
|
1824
|
+
let inClass = false;
|
|
1825
|
+
const flush = () => {
|
|
1826
|
+
if (cur.length > best.length)
|
|
1827
|
+
best = cur;
|
|
1828
|
+
cur = "";
|
|
1829
|
+
};
|
|
1830
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
1831
|
+
const ch = pattern[i] ?? "";
|
|
1832
|
+
if (!ch)
|
|
1833
|
+
break;
|
|
1834
|
+
if (inClass) {
|
|
1835
|
+
// Skip until the closing bracket.
|
|
1836
|
+
if (ch === "]")
|
|
1837
|
+
inClass = false;
|
|
1838
|
+
flush();
|
|
1839
|
+
continue;
|
|
1840
|
+
}
|
|
1841
|
+
if (ch === "[") {
|
|
1842
|
+
inClass = true;
|
|
1843
|
+
flush();
|
|
1844
|
+
continue;
|
|
1845
|
+
}
|
|
1846
|
+
if (ch === "\\") {
|
|
1847
|
+
const next = pattern[i + 1] ?? "";
|
|
1848
|
+
if (!next) {
|
|
1849
|
+
flush();
|
|
1850
|
+
continue;
|
|
1851
|
+
}
|
|
1852
|
+
// Common regex escapes that are NOT literal characters.
|
|
1853
|
+
if (/[dDsSwWbB0-9]/.test(next)) {
|
|
1854
|
+
flush();
|
|
1855
|
+
i += 1;
|
|
1856
|
+
continue;
|
|
1857
|
+
}
|
|
1858
|
+
// Treat \x as literal x (e.g. \( \) \. \\).
|
|
1859
|
+
cur += next;
|
|
1860
|
+
i += 1;
|
|
1861
|
+
continue;
|
|
1862
|
+
}
|
|
1863
|
+
// Regex metacharacters.
|
|
1864
|
+
if (".*+?^$|(){}".includes(ch)) {
|
|
1865
|
+
flush();
|
|
1866
|
+
continue;
|
|
1867
|
+
}
|
|
1868
|
+
cur += ch;
|
|
1869
|
+
}
|
|
1870
|
+
flush();
|
|
1871
|
+
return best;
|
|
1872
|
+
}
|
|
1873
|
+
function normalizePathNeedle(s) {
|
|
1874
|
+
return s.replace(/\\/g, "/").toLowerCase();
|
|
1875
|
+
}
|
|
1876
|
+
function passesPathFilters(filePath, includePaths, excludePaths) {
|
|
1877
|
+
const fp = filePath.toLowerCase();
|
|
1878
|
+
if (excludePaths?.length) {
|
|
1879
|
+
for (const raw of excludePaths) {
|
|
1880
|
+
const n = normalizePathNeedle(raw);
|
|
1881
|
+
if (!n)
|
|
1882
|
+
continue;
|
|
1883
|
+
if (fp.includes(n))
|
|
1884
|
+
return false;
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
if (includePaths?.length) {
|
|
1888
|
+
for (const raw of includePaths) {
|
|
1889
|
+
const n = normalizePathNeedle(raw);
|
|
1890
|
+
if (!n)
|
|
1891
|
+
continue;
|
|
1892
|
+
if (fp.includes(n))
|
|
1893
|
+
return true;
|
|
1894
|
+
}
|
|
1895
|
+
return false;
|
|
1896
|
+
}
|
|
1897
|
+
return true;
|
|
1898
|
+
}
|
|
1899
|
+
function buildLineStarts(text) {
|
|
1900
|
+
const starts = [0];
|
|
1901
|
+
for (let i = 0; i < text.length; i++) {
|
|
1902
|
+
if (text.charCodeAt(i) === 10)
|
|
1903
|
+
starts.push(i + 1); // '\n'
|
|
1904
|
+
}
|
|
1905
|
+
return starts;
|
|
1906
|
+
}
|
|
1907
|
+
function lineIndexForOffset(lineStarts, offset) {
|
|
1908
|
+
let lo = 0;
|
|
1909
|
+
let hi = lineStarts.length - 1;
|
|
1910
|
+
while (lo <= hi) {
|
|
1911
|
+
const mid = (lo + hi) >> 1;
|
|
1912
|
+
const v = lineStarts[mid] ?? 0;
|
|
1913
|
+
if (v <= offset)
|
|
1914
|
+
lo = mid + 1;
|
|
1915
|
+
else
|
|
1916
|
+
hi = mid - 1;
|
|
1917
|
+
}
|
|
1918
|
+
return Math.max(0, lo - 1);
|
|
1919
|
+
}
|
|
1920
|
+
function compileGrepRegex(opts) {
|
|
1921
|
+
const flags = `${opts.caseSensitive ? "" : "i"}gm`;
|
|
1922
|
+
const source = opts.mode === "literal" ? escapeRegExp(opts.query) : opts.query;
|
|
1923
|
+
return new RegExp(source, flags);
|
|
1924
|
+
}
|
|
1925
|
+
function resolveProjectPathUnderRoot(inputPath, opts = {}) {
|
|
1926
|
+
const normalizedInput = inputPath.trim() || ".";
|
|
1927
|
+
const abs = path.isAbsolute(normalizedInput) ? normalizedInput : path.join(projectRoot, normalizedInput);
|
|
1928
|
+
const absPath = path.resolve(abs);
|
|
1929
|
+
const root = path.resolve(projectRoot);
|
|
1930
|
+
const rel = path.relative(root, absPath);
|
|
1931
|
+
const insideRoot = rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
|
|
1932
|
+
if (!insideRoot) {
|
|
1933
|
+
throw new Error(`[VectorMind] Path must be under project_root: ${inputPath}`);
|
|
1934
|
+
}
|
|
1935
|
+
if (rel === "" && !opts.allowRoot) {
|
|
1936
|
+
throw new Error(`[VectorMind] Path must not be the project_root itself: ${inputPath}`);
|
|
1937
|
+
}
|
|
1938
|
+
return {
|
|
1939
|
+
absPath,
|
|
1940
|
+
dbFilePath: rel === "" ? "." : normalizeToDbPath(absPath),
|
|
1941
|
+
};
|
|
1942
|
+
}
|
|
1943
|
+
function resolveReadPathUnderProjectRoot(inputPath) {
|
|
1944
|
+
return resolveProjectPathUnderRoot(inputPath, { allowRoot: false });
|
|
1945
|
+
}
|
|
1946
|
+
function resolveCodexTextPath(inputPath) {
|
|
1947
|
+
const trimmed = inputPath.trim();
|
|
1948
|
+
if (!trimmed)
|
|
1949
|
+
throw new Error("[VectorMind] path is required");
|
|
1950
|
+
const uriPath = trimmed.startsWith("file:") ? parseFileUriToPath(trimmed) : null;
|
|
1951
|
+
const absPath = path.resolve(uriPath ?? trimmed);
|
|
1952
|
+
const allowedRoot = getAllowedCodexTextRoots().find((root) => {
|
|
1953
|
+
const rel = path.relative(root, absPath);
|
|
1954
|
+
return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
|
|
1955
|
+
});
|
|
1956
|
+
if (!allowedRoot) {
|
|
1957
|
+
throw new Error(`[VectorMind] Path must be under one of the allowed local text roots: ${getAllowedCodexTextRoots().join(", ")}`);
|
|
1958
|
+
}
|
|
1959
|
+
return { absPath, displayPath: absPath, allowedRoot };
|
|
1960
|
+
}
|
|
1961
|
+
function isHiddenBaseName(name) {
|
|
1962
|
+
return name.startsWith(".") && name !== "." && name !== "..";
|
|
1963
|
+
}
|
|
1964
|
+
function normalizeExtensionsFilter(values) {
|
|
1965
|
+
if (!values?.length)
|
|
1966
|
+
return null;
|
|
1967
|
+
const normalized = values
|
|
1968
|
+
.map((v) => v.trim().toLowerCase())
|
|
1969
|
+
.filter(Boolean)
|
|
1970
|
+
.map((v) => (v.startsWith(".") ? v : `.${v}`));
|
|
1971
|
+
return normalized.length ? Array.from(new Set(normalized)) : null;
|
|
1972
|
+
}
|
|
1973
|
+
async function readTextFileLines(opts) {
|
|
1974
|
+
let lineNo = 0;
|
|
1975
|
+
const lines = [];
|
|
1976
|
+
let totalChars = 0;
|
|
1977
|
+
let truncated = false;
|
|
1978
|
+
const stream = fs.createReadStream(opts.absPath, { encoding: "utf8" });
|
|
1979
|
+
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
1980
|
+
try {
|
|
1981
|
+
for await (const line of rl) {
|
|
1982
|
+
lineNo += 1;
|
|
1983
|
+
if (lineNo < opts.fromLine)
|
|
1984
|
+
continue;
|
|
1985
|
+
if (lineNo > opts.toLine)
|
|
1986
|
+
break;
|
|
1987
|
+
const rendered = `${lineNo}:${line}`;
|
|
1988
|
+
totalChars += rendered.length + 1;
|
|
1989
|
+
if (lines.length >= opts.maxLines || totalChars > opts.maxChars) {
|
|
1990
|
+
truncated = true;
|
|
1991
|
+
break;
|
|
1992
|
+
}
|
|
1993
|
+
lines.push(rendered);
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
finally {
|
|
1997
|
+
try {
|
|
1998
|
+
rl.close();
|
|
1999
|
+
}
|
|
2000
|
+
catch { }
|
|
2001
|
+
try {
|
|
2002
|
+
stream.destroy();
|
|
2003
|
+
}
|
|
2004
|
+
catch { }
|
|
2005
|
+
}
|
|
2006
|
+
return { text: lines.join("\n"), returned: lines.length, truncated };
|
|
2007
|
+
}
|
|
2008
|
+
function readTextFileSlice(opts) {
|
|
2009
|
+
const st = fs.statSync(opts.absPath);
|
|
2010
|
+
if (!st.isFile())
|
|
2011
|
+
throw new Error("Not a file");
|
|
2012
|
+
if (st.size > opts.maxFileBytes) {
|
|
2013
|
+
throw new Error(`File is too large for raw text read (${st.size} bytes > limit ${opts.maxFileBytes}). Use read_file_lines instead.`);
|
|
2014
|
+
}
|
|
2015
|
+
const text = fs.readFileSync(opts.absPath, "utf8");
|
|
2016
|
+
const totalChars = text.length;
|
|
2017
|
+
const safeOffset = Math.min(opts.offset, totalChars);
|
|
2018
|
+
const slice = text.slice(safeOffset, safeOffset + opts.maxChars);
|
|
2019
|
+
const returnedChars = slice.length;
|
|
2020
|
+
const truncated = safeOffset + returnedChars < totalChars;
|
|
2021
|
+
return { text: slice, totalChars, returnedChars, truncated };
|
|
2022
|
+
}
|
|
2023
|
+
function listProjectFilesInternal(opts) {
|
|
2024
|
+
const entries = [];
|
|
2025
|
+
let scanned = 0;
|
|
2026
|
+
let truncated = false;
|
|
2027
|
+
const pushEntry = (entry) => {
|
|
2028
|
+
if (entries.length >= opts.maxResults) {
|
|
2029
|
+
truncated = true;
|
|
2030
|
+
return;
|
|
2031
|
+
}
|
|
2032
|
+
entries.push(entry);
|
|
2033
|
+
};
|
|
2034
|
+
const startStat = fs.statSync(opts.startAbsPath);
|
|
2035
|
+
if (startStat.isFile()) {
|
|
2036
|
+
const relPath = opts.startDbPath;
|
|
2037
|
+
if ((!opts.respectIgnore || !shouldIgnoreDbFilePath(relPath)) && passesPathFilters(relPath, opts.includePaths, opts.excludePaths)) {
|
|
2038
|
+
const ext = path.extname(relPath).toLowerCase();
|
|
2039
|
+
if (!opts.extensions || opts.extensions.includes(ext)) {
|
|
2040
|
+
pushEntry({
|
|
2041
|
+
path: relPath,
|
|
2042
|
+
kind: "file",
|
|
2043
|
+
depth: 0,
|
|
2044
|
+
...(opts.includeStats ? { size: startStat.size, mtime: startStat.mtime.toISOString() } : {}),
|
|
2045
|
+
});
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
return { entries, returned: entries.length, scanned: 1, truncated };
|
|
2049
|
+
}
|
|
2050
|
+
const effectiveMaxDepth = opts.recursive ? opts.maxDepth : 1;
|
|
2051
|
+
const stack = [{ absPath: opts.startAbsPath, depth: 0 }];
|
|
2052
|
+
while (stack.length > 0) {
|
|
2053
|
+
const current = stack.pop();
|
|
2054
|
+
if (!current)
|
|
2055
|
+
break;
|
|
2056
|
+
let dirEntries;
|
|
2057
|
+
try {
|
|
2058
|
+
dirEntries = fs.readdirSync(current.absPath, { withFileTypes: true });
|
|
2059
|
+
}
|
|
2060
|
+
catch {
|
|
2061
|
+
continue;
|
|
2062
|
+
}
|
|
2063
|
+
dirEntries.sort((a, b) => a.name.localeCompare(b.name));
|
|
2064
|
+
for (let idx = dirEntries.length - 1; idx >= 0; idx -= 1) {
|
|
2065
|
+
const child = dirEntries[idx];
|
|
2066
|
+
if (!child)
|
|
2067
|
+
continue;
|
|
2068
|
+
if (!opts.includeHidden && isHiddenBaseName(child.name))
|
|
2069
|
+
continue;
|
|
2070
|
+
const childAbs = path.join(current.absPath, child.name);
|
|
2071
|
+
const childRel = normalizeToDbPath(childAbs);
|
|
2072
|
+
if (opts.respectIgnore && shouldIgnoreDbFilePath(childRel))
|
|
2073
|
+
continue;
|
|
2074
|
+
scanned += 1;
|
|
2075
|
+
const childDepth = current.depth + 1;
|
|
2076
|
+
const matchesPath = passesPathFilters(childRel, opts.includePaths, opts.excludePaths);
|
|
2077
|
+
if (child.isDirectory()) {
|
|
2078
|
+
if (opts.includeDirs && matchesPath) {
|
|
2079
|
+
let stats = null;
|
|
2080
|
+
if (opts.includeStats) {
|
|
2081
|
+
try {
|
|
2082
|
+
stats = fs.statSync(childAbs);
|
|
2083
|
+
}
|
|
2084
|
+
catch {
|
|
2085
|
+
stats = null;
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
pushEntry({
|
|
2089
|
+
path: childRel,
|
|
2090
|
+
kind: "dir",
|
|
2091
|
+
depth: childDepth,
|
|
2092
|
+
...(stats ? { size: stats.size, mtime: stats.mtime.toISOString() } : {}),
|
|
2093
|
+
});
|
|
2094
|
+
if (truncated)
|
|
2095
|
+
break;
|
|
2096
|
+
}
|
|
2097
|
+
if (childDepth < effectiveMaxDepth) {
|
|
2098
|
+
stack.push({ absPath: childAbs, depth: childDepth });
|
|
2099
|
+
}
|
|
2100
|
+
continue;
|
|
2101
|
+
}
|
|
2102
|
+
if (!child.isFile())
|
|
2103
|
+
continue;
|
|
2104
|
+
if (!opts.includeFiles || !matchesPath)
|
|
2105
|
+
continue;
|
|
2106
|
+
const ext = path.extname(childRel).toLowerCase();
|
|
2107
|
+
if (opts.extensions && !opts.extensions.includes(ext))
|
|
2108
|
+
continue;
|
|
2109
|
+
let stats = null;
|
|
2110
|
+
if (opts.includeStats) {
|
|
2111
|
+
try {
|
|
2112
|
+
stats = fs.statSync(childAbs);
|
|
2113
|
+
}
|
|
2114
|
+
catch {
|
|
2115
|
+
stats = null;
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
pushEntry({
|
|
2119
|
+
path: childRel,
|
|
2120
|
+
kind: "file",
|
|
2121
|
+
depth: childDepth,
|
|
2122
|
+
...(stats ? { size: stats.size, mtime: stats.mtime.toISOString() } : {}),
|
|
2123
|
+
});
|
|
2124
|
+
if (truncated)
|
|
2125
|
+
break;
|
|
2126
|
+
}
|
|
2127
|
+
if (truncated)
|
|
2128
|
+
break;
|
|
2129
|
+
}
|
|
2130
|
+
entries.sort((a, b) => a.path.localeCompare(b.path));
|
|
2131
|
+
return { entries, returned: entries.length, scanned, truncated };
|
|
2132
|
+
}
|
|
2133
|
+
function buildServerInstructions() {
|
|
2134
|
+
return [
|
|
1484
2135
|
"VectorMind MCP is available in this session. Use it to avoid guessing project context.",
|
|
2136
|
+
"This package ships built-in baseline policy. If a client supports MCP instructions, these rules auto-apply as soon as the MCP is installed and connected; no user-side config file is required.",
|
|
2137
|
+
"The write-operation rules below are strict workflow constraints. Do not claim that the environment has real git branch locks, checkout APIs, or file-lock tools unless such tools are actually available in the current client/runtime. If such tools are absent, you must still enforce the same exclusivity semantics through explicit coordination and serialized same-file edits.",
|
|
1485
2138
|
"Project root resolution order: tool argument project_root (recommended for clients without roots/list), then VECTORMIND_ROOT (avoid hardcoding in global config), then MCP roots/list (best-effort; falls back quickly if unsupported), then process.cwd() (so start your MCP client in the project directory for per-project isolation).",
|
|
1486
2139
|
"If root_source is fallback, file watching/indexing is disabled (pass project_root to enable per-project tracking).",
|
|
1487
2140
|
"",
|
|
2141
|
+
"Built-in write-operation policy:",
|
|
2142
|
+
BUILTIN_WRITE_POLICY_INSTRUCTIONS,
|
|
2143
|
+
"",
|
|
2144
|
+
"Built-in task-list / Plan-Lite policy:",
|
|
2145
|
+
BUILTIN_PLAN_LITE_INSTRUCTIONS,
|
|
2146
|
+
"",
|
|
2147
|
+
"Built-in destructive-operation guard policy:",
|
|
2148
|
+
BUILTIN_DESTRUCTIVE_OPERATION_GUARD_INSTRUCTIONS,
|
|
2149
|
+
"",
|
|
2150
|
+
"Built-in architecture and code-organization policy:",
|
|
2151
|
+
BUILTIN_ARCHITECTURE_AND_CODE_ORGANIZATION_INSTRUCTIONS,
|
|
2152
|
+
"",
|
|
1488
2153
|
"Required workflow:",
|
|
1489
2154
|
"- On every new conversation/session: call bootstrap_context({ query: <current goal> }) first (or at least get_brain_dump()) to restore context and retrieve relevant matches from the local memory store (vector if enabled; otherwise FTS/LIKE).",
|
|
1490
2155
|
" - Output is compact by default. Use include_content=true only when you truly need full text (it increases tokens).",
|
|
1491
2156
|
" - Tune output size with: requirements_limit/changes_limit/notes_limit, preview_chars, pending_limit/pending_offset.",
|
|
1492
2157
|
" - Prefer read_memory_item(id, offset, limit) to fetch full text on demand instead of returning large content in other tool outputs.",
|
|
2158
|
+
"- To read local Codex skill/prompt/rule files (for example SKILL.md under CODEX_HOME or AGENTS_HOME), prefer read_codex_text_file({ path }) instead of assuming a filesystem MCP resource server exists.",
|
|
2159
|
+
"- For project file/directory browsing, prefer list_project_files({ path, recursive?, max_depth? }) over shelling out to Get-ChildItem/ls. It respects ignore rules and keeps output bounded.",
|
|
2160
|
+
"- For small/medium raw file reads, prefer read_file_text({ path, offset?, max_chars? }) over Get-Content -Raw. Use read_file_lines(...) when you need deterministic line ranges or the file may be large.",
|
|
2161
|
+
"- For an rg/Select-String-style search with exact file+line+col matches, prefer grep({ query: <pattern> }) over shelling out (uses the indexed code/doc chunks).",
|
|
2162
|
+
"- To read a bounded segment of a file, prefer read_file_lines({ path: <file>, from_line/to_line or total_count }) over unbounded file reads.",
|
|
1493
2163
|
"- BEFORE editing code: call start_requirement(title, background) to set the active requirement.",
|
|
1494
2164
|
"- AFTER editing + saving: call get_pending_changes() to see unsynced files, then call sync_change_intent(intent, files). (You can omit files to auto-link all pending changes.)",
|
|
1495
2165
|
"- After major milestones/decisions: call upsert_project_summary(summary) and/or add_note(...) to persist durable context locally.",
|
|
@@ -1499,7 +2169,11 @@ const server = new Server({ name: SERVER_NAME, version: SERVER_VERSION }, {
|
|
|
1499
2169
|
"- When you need to recall relevant context from history/code/docs: call semantic_search(query, ...) instead of guessing.",
|
|
1500
2170
|
"",
|
|
1501
2171
|
"If tool output conflicts with assumptions, trust the tool output.",
|
|
1502
|
-
].join("\n")
|
|
2172
|
+
].join("\n");
|
|
2173
|
+
}
|
|
2174
|
+
const server = new Server({ name: SERVER_NAME, version: SERVER_VERSION }, {
|
|
2175
|
+
capabilities: { tools: {} },
|
|
2176
|
+
instructions: buildServerInstructions(),
|
|
1503
2177
|
});
|
|
1504
2178
|
async function resolveProjectRootFromMcpRoots() {
|
|
1505
2179
|
const caps = server.getClientCapabilities();
|
|
@@ -1870,6 +2544,13 @@ function initDatabase() {
|
|
|
1870
2544
|
});
|
|
1871
2545
|
// Clean up noisy pending changes recorded by older versions (build artifacts, node_modules, etc).
|
|
1872
2546
|
prunePendingChanges();
|
|
2547
|
+
// Clean up noisy indexes recorded by older versions (build artifacts, etc).
|
|
2548
|
+
if (INDEX_AUTO_PRUNE_IGNORED) {
|
|
2549
|
+
pruneIgnoredIndexesByPathPatterns();
|
|
2550
|
+
}
|
|
2551
|
+
// Clean up common "file name noise" recorded by older versions.
|
|
2552
|
+
// (These files are ignored by current index rules; keep the DB consistent automatically.)
|
|
2553
|
+
pruneFilenameNoiseIndexes();
|
|
1873
2554
|
}
|
|
1874
2555
|
function initWatcher() {
|
|
1875
2556
|
watcherReady = false;
|
|
@@ -2049,6 +2730,31 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2049
2730
|
description: "Clear the in-memory debug activity log. Enable logging with VECTORMIND_DEBUG_LOG=1.",
|
|
2050
2731
|
inputSchema: toJsonSchemaCompat(ClearActivityLogArgsSchema),
|
|
2051
2732
|
},
|
|
2733
|
+
{
|
|
2734
|
+
name: "grep",
|
|
2735
|
+
description: "Fast indexed grep across code/doc chunks with precise file/line/col matches. Prefer this over shelling out to rg/Select-String when possible.",
|
|
2736
|
+
inputSchema: toJsonSchemaCompat(GrepArgsSchema),
|
|
2737
|
+
},
|
|
2738
|
+
{
|
|
2739
|
+
name: "list_project_files",
|
|
2740
|
+
description: "AI-friendly, ignore-aware file/directory listing under project_root with bounded output. Prefer this over Get-ChildItem/ls for local repository browsing.",
|
|
2741
|
+
inputSchema: toJsonSchemaCompat(ListProjectFilesArgsSchema),
|
|
2742
|
+
},
|
|
2743
|
+
{
|
|
2744
|
+
name: "read_codex_text_file",
|
|
2745
|
+
description: "Read bounded text from local Codex/agents files such as SKILL.md, prompt files, and rules under CODEX_HOME/AGENTS_HOME. Prefer this over assuming a filesystem MCP resource server exists.",
|
|
2746
|
+
inputSchema: toJsonSchemaCompat(ReadCodexTextFileArgsSchema),
|
|
2747
|
+
},
|
|
2748
|
+
{
|
|
2749
|
+
name: "read_file_lines",
|
|
2750
|
+
description: "Read a specific line range from a file under project_root (with strict size limits). Prefer this over Get-Content for deterministic reads.",
|
|
2751
|
+
inputSchema: toJsonSchemaCompat(ReadFileLinesArgsSchema),
|
|
2752
|
+
},
|
|
2753
|
+
{
|
|
2754
|
+
name: "read_file_text",
|
|
2755
|
+
description: "Read bounded raw UTF-8 text from a file under project_root. Prefer this over Get-Content -Raw for small/medium text files; use read_file_lines for large files or line-specific reads.",
|
|
2756
|
+
inputSchema: toJsonSchemaCompat(ReadFileTextArgsSchema),
|
|
2757
|
+
},
|
|
2052
2758
|
{
|
|
2053
2759
|
name: "query_codebase",
|
|
2054
2760
|
description: "Search the symbol index for class/function/type names (or substrings) to locate definitions by file path and signature. Use this when you need to find code—do not guess locations.",
|
|
@@ -2074,6 +2780,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2074
2780
|
description: "Semantic search across the local memory store (requirements, change intents, notes, project summary, and indexed code/doc chunks). Use this to retrieve relevant context instead of guessing.",
|
|
2075
2781
|
inputSchema: toJsonSchemaCompat(SemanticSearchArgsSchema),
|
|
2076
2782
|
},
|
|
2783
|
+
{
|
|
2784
|
+
name: "prune_index",
|
|
2785
|
+
description: "Prune noisy auto-indexed items (code_chunk/doc_chunk + symbols). Useful after tightening ignore rules to shrink the index and improve search relevance.",
|
|
2786
|
+
inputSchema: toJsonSchemaCompat(PruneIndexArgsSchema),
|
|
2787
|
+
},
|
|
2077
2788
|
],
|
|
2078
2789
|
};
|
|
2079
2790
|
});
|
|
@@ -2120,6 +2831,142 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2120
2831
|
],
|
|
2121
2832
|
};
|
|
2122
2833
|
}
|
|
2834
|
+
if (toolName === "prune_index") {
|
|
2835
|
+
const args = PruneIndexArgsSchema.parse(rawArgs);
|
|
2836
|
+
flushPendingChangeBuffer();
|
|
2837
|
+
const result = {
|
|
2838
|
+
ok: true,
|
|
2839
|
+
dry_run: args.dry_run,
|
|
2840
|
+
config: {
|
|
2841
|
+
index_max_code_bytes: INDEX_MAX_CODE_BYTES,
|
|
2842
|
+
index_max_doc_bytes: INDEX_MAX_DOC_BYTES,
|
|
2843
|
+
index_skip_minified: INDEX_SKIP_MINIFIED,
|
|
2844
|
+
index_auto_prune_ignored: INDEX_AUTO_PRUNE_IGNORED,
|
|
2845
|
+
},
|
|
2846
|
+
pruned: {
|
|
2847
|
+
ignored_paths: { chunks_deleted: 0, symbols_deleted: 0 },
|
|
2848
|
+
minified_bundles: { files_matched: 0, chunks_deleted: 0, symbols_deleted: 0 },
|
|
2849
|
+
},
|
|
2850
|
+
};
|
|
2851
|
+
if (args.prune_ignored_paths) {
|
|
2852
|
+
if (!IGNORED_LIKE_PATTERNS.length) {
|
|
2853
|
+
result.pruned.ignored_paths = { chunks_deleted: 0, symbols_deleted: 0 };
|
|
2854
|
+
}
|
|
2855
|
+
else if (args.dry_run) {
|
|
2856
|
+
const where = IGNORED_LIKE_PATTERNS
|
|
2857
|
+
.map(() => "LOWER(REPLACE(file_path, '\\\\', '/')) LIKE ?")
|
|
2858
|
+
.join(" OR ");
|
|
2859
|
+
const chunksWould = Number(db
|
|
2860
|
+
.prepare(`SELECT COUNT(1) AS c
|
|
2861
|
+
FROM memory_items
|
|
2862
|
+
WHERE file_path IS NOT NULL
|
|
2863
|
+
AND (kind = 'code_chunk' OR kind = 'doc_chunk')
|
|
2864
|
+
AND (${where})`)
|
|
2865
|
+
.get(...IGNORED_LIKE_PATTERNS)?.c ?? 0);
|
|
2866
|
+
const symbolsWould = Number(db
|
|
2867
|
+
.prepare(`SELECT COUNT(1) AS c
|
|
2868
|
+
FROM symbols
|
|
2869
|
+
WHERE file_path IS NOT NULL
|
|
2870
|
+
AND (${where})`)
|
|
2871
|
+
.get(...IGNORED_LIKE_PATTERNS)?.c ?? 0);
|
|
2872
|
+
result.pruned.ignored_paths = { chunks_deleted: chunksWould, symbols_deleted: symbolsWould };
|
|
2873
|
+
}
|
|
2874
|
+
else {
|
|
2875
|
+
result.pruned.ignored_paths = pruneIgnoredIndexesByPathPatterns();
|
|
2876
|
+
}
|
|
2877
|
+
}
|
|
2878
|
+
if (args.prune_minified_bundles) {
|
|
2879
|
+
const maxFiles = args.max_files;
|
|
2880
|
+
const candidates = db
|
|
2881
|
+
.prepare(`SELECT file_path, content
|
|
2882
|
+
FROM memory_items
|
|
2883
|
+
WHERE kind = 'code_chunk'
|
|
2884
|
+
AND file_path IS NOT NULL
|
|
2885
|
+
AND (
|
|
2886
|
+
LOWER(file_path) LIKE '%.js'
|
|
2887
|
+
OR LOWER(file_path) LIKE '%.mjs'
|
|
2888
|
+
OR LOWER(file_path) LIKE '%.cjs'
|
|
2889
|
+
OR LOWER(file_path) LIKE '%.css'
|
|
2890
|
+
)
|
|
2891
|
+
ORDER BY updated_at DESC, id DESC
|
|
2892
|
+
LIMIT ?`)
|
|
2893
|
+
.all(Math.min(50_000, maxFiles * 5));
|
|
2894
|
+
const matched = new Set();
|
|
2895
|
+
for (const row of candidates) {
|
|
2896
|
+
if (matched.size >= maxFiles)
|
|
2897
|
+
break;
|
|
2898
|
+
const fp = row.file_path;
|
|
2899
|
+
if (!fp || matched.has(fp))
|
|
2900
|
+
continue;
|
|
2901
|
+
if (looksLikeMinifiedBundle(row.content))
|
|
2902
|
+
matched.add(fp);
|
|
2903
|
+
}
|
|
2904
|
+
if (args.dry_run) {
|
|
2905
|
+
let chunksWould = 0;
|
|
2906
|
+
let symbolsWould = 0;
|
|
2907
|
+
const countChunksStmt = db.prepare(`SELECT COUNT(1) AS c
|
|
2908
|
+
FROM memory_items
|
|
2909
|
+
WHERE file_path = ?
|
|
2910
|
+
AND (kind = 'code_chunk' OR kind = 'doc_chunk')`);
|
|
2911
|
+
const countSymbolsStmt = db.prepare(`SELECT COUNT(1) AS c FROM symbols WHERE file_path = ?`);
|
|
2912
|
+
for (const fp of matched) {
|
|
2913
|
+
chunksWould += Number(countChunksStmt.get(fp)?.c ?? 0);
|
|
2914
|
+
symbolsWould += Number(countSymbolsStmt.get(fp)?.c ?? 0);
|
|
2915
|
+
}
|
|
2916
|
+
result.pruned.minified_bundles = {
|
|
2917
|
+
files_matched: matched.size,
|
|
2918
|
+
chunks_deleted: chunksWould,
|
|
2919
|
+
symbols_deleted: symbolsWould,
|
|
2920
|
+
};
|
|
2921
|
+
}
|
|
2922
|
+
else {
|
|
2923
|
+
let chunksDeleted = 0;
|
|
2924
|
+
let symbolsDeleted = 0;
|
|
2925
|
+
const tx = db.transaction(() => {
|
|
2926
|
+
for (const fp of matched) {
|
|
2927
|
+
chunksDeleted += deleteFileChunkItemsStmt.run(fp).changes;
|
|
2928
|
+
symbolsDeleted += deleteSymbolsForFileStmt.run(fp).changes;
|
|
2929
|
+
}
|
|
2930
|
+
});
|
|
2931
|
+
try {
|
|
2932
|
+
tx();
|
|
2933
|
+
}
|
|
2934
|
+
catch (err) {
|
|
2935
|
+
console.error("[vectormind] prune minified bundles failed:", err);
|
|
2936
|
+
}
|
|
2937
|
+
if (matched.size) {
|
|
2938
|
+
logActivity("index_prune", {
|
|
2939
|
+
reason: "minified_bundles",
|
|
2940
|
+
files_matched: matched.size,
|
|
2941
|
+
chunks_deleted: chunksDeleted,
|
|
2942
|
+
symbols_deleted: symbolsDeleted,
|
|
2943
|
+
});
|
|
2944
|
+
}
|
|
2945
|
+
result.pruned.minified_bundles = {
|
|
2946
|
+
files_matched: matched.size,
|
|
2947
|
+
chunks_deleted: chunksDeleted,
|
|
2948
|
+
symbols_deleted: symbolsDeleted,
|
|
2949
|
+
};
|
|
2950
|
+
}
|
|
2951
|
+
}
|
|
2952
|
+
if (!args.dry_run && args.vacuum) {
|
|
2953
|
+
try {
|
|
2954
|
+
db.exec("VACUUM");
|
|
2955
|
+
logActivity("index_prune", { reason: "vacuum" });
|
|
2956
|
+
}
|
|
2957
|
+
catch (err) {
|
|
2958
|
+
console.error("[vectormind] vacuum failed:", err);
|
|
2959
|
+
}
|
|
2960
|
+
}
|
|
2961
|
+
return {
|
|
2962
|
+
content: [
|
|
2963
|
+
{
|
|
2964
|
+
type: "text",
|
|
2965
|
+
text: toolJson(result),
|
|
2966
|
+
},
|
|
2967
|
+
],
|
|
2968
|
+
};
|
|
2969
|
+
}
|
|
2123
2970
|
if (toolName === "sync_change_intent") {
|
|
2124
2971
|
const args = SyncChangeIntentArgsSchema.parse(rawArgs);
|
|
2125
2972
|
flushPendingChangeBuffer();
|
|
@@ -2253,7 +3100,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2253
3100
|
? toMemoryItemPreview(projectSummaryRow, includeContent, previewChars, contentMaxChars)
|
|
2254
3101
|
: null;
|
|
2255
3102
|
const recent_notes = listRecentNotesStmt.all(notesLimit).map((n) => toMemoryItemPreview(n, includeContent, previewChars, contentMaxChars));
|
|
2256
|
-
const conventions =
|
|
3103
|
+
const conventions = getConventionPreviews(conventionsLimit, previewChars, contentMaxChars);
|
|
2257
3104
|
const pending_total = Number(countPendingChangesStmt.get()?.total ?? 0);
|
|
2258
3105
|
const pending_offset = args.pending_offset;
|
|
2259
3106
|
const pending_limit = args.pending_limit;
|
|
@@ -2349,7 +3196,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2349
3196
|
? toMemoryItemPreview(projectSummaryRow, includeContent, previewChars, contentMaxChars)
|
|
2350
3197
|
: null;
|
|
2351
3198
|
const recent_notes = listRecentNotesStmt.all(notesLimit).map((n) => toMemoryItemPreview(n, includeContent, previewChars, contentMaxChars));
|
|
2352
|
-
const conventions =
|
|
3199
|
+
const conventions = getConventionPreviews(conventionsLimit, previewChars, contentMaxChars);
|
|
2353
3200
|
const pending_total = Number(countPendingChangesStmt.get()?.total ?? 0);
|
|
2354
3201
|
const pending_offset = args.pending_offset;
|
|
2355
3202
|
const pending_limit = args.pending_limit;
|
|
@@ -2578,6 +3425,441 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2578
3425
|
clearActivityLog();
|
|
2579
3426
|
return { content: [{ type: "text", text: toolJson({ ok: true }) }] };
|
|
2580
3427
|
}
|
|
3428
|
+
if (toolName === "grep") {
|
|
3429
|
+
const args = GrepArgsSchema.parse(rawArgs);
|
|
3430
|
+
const q = args.query;
|
|
3431
|
+
const mode = args.mode;
|
|
3432
|
+
const smartCase = args.smart_case;
|
|
3433
|
+
const kinds = args.kinds?.length ? args.kinds : ["code_chunk", "doc_chunk"];
|
|
3434
|
+
const includePaths = args.include_paths?.length ? args.include_paths : null;
|
|
3435
|
+
const excludePaths = args.exclude_paths?.length ? args.exclude_paths : null;
|
|
3436
|
+
const maxResults = args.max_results;
|
|
3437
|
+
const hint = (() => {
|
|
3438
|
+
if (mode === "literal")
|
|
3439
|
+
return q;
|
|
3440
|
+
const explicit = args.literal_hint.trim();
|
|
3441
|
+
if (explicit)
|
|
3442
|
+
return explicit;
|
|
3443
|
+
return extractLongestLiteralFromRegex(q);
|
|
3444
|
+
})();
|
|
3445
|
+
if (mode === "regex" && hint.trim().length < 3) {
|
|
3446
|
+
return {
|
|
3447
|
+
isError: true,
|
|
3448
|
+
content: [
|
|
3449
|
+
{
|
|
3450
|
+
type: "text",
|
|
3451
|
+
text: toolJson({
|
|
3452
|
+
ok: false,
|
|
3453
|
+
error: "Regex has no sufficiently long literal anchor for indexed narrowing. Provide literal_hint (>= 3 chars) or narrow with include_paths.",
|
|
3454
|
+
query: q,
|
|
3455
|
+
mode,
|
|
3456
|
+
literal_hint: args.literal_hint,
|
|
3457
|
+
}),
|
|
3458
|
+
},
|
|
3459
|
+
],
|
|
3460
|
+
};
|
|
3461
|
+
}
|
|
3462
|
+
const caseSensitive = args.case_sensitive ?? (smartCase ? hasUppercaseAscii(q) : true);
|
|
3463
|
+
let re;
|
|
3464
|
+
try {
|
|
3465
|
+
re = compileGrepRegex({ query: q, mode, caseSensitive });
|
|
3466
|
+
}
|
|
3467
|
+
catch (err) {
|
|
3468
|
+
return {
|
|
3469
|
+
isError: true,
|
|
3470
|
+
content: [
|
|
3471
|
+
{
|
|
3472
|
+
type: "text",
|
|
3473
|
+
text: toolJson({ ok: false, error: `Invalid pattern: ${String(err)}`, query: q, mode }),
|
|
3474
|
+
},
|
|
3475
|
+
],
|
|
3476
|
+
};
|
|
3477
|
+
}
|
|
3478
|
+
const maxCandidates = args.max_candidates ?? Math.min(50_000, Math.max(1000, maxResults * 200));
|
|
3479
|
+
const candidates = (() => {
|
|
3480
|
+
if (ftsAvailable) {
|
|
3481
|
+
const matchQuery = buildFtsMatchQuery(hint);
|
|
3482
|
+
const placeholders = kinds.map(() => "?").join(", ");
|
|
3483
|
+
const stmt = db.prepare(`
|
|
3484
|
+
SELECT
|
|
3485
|
+
m.id as id,
|
|
3486
|
+
m.kind as kind,
|
|
3487
|
+
m.content as content,
|
|
3488
|
+
m.file_path as file_path,
|
|
3489
|
+
m.start_line as start_line,
|
|
3490
|
+
m.end_line as end_line
|
|
3491
|
+
FROM ${FTS_TABLE_NAME}
|
|
3492
|
+
JOIN memory_items m ON m.id = ${FTS_TABLE_NAME}.rowid
|
|
3493
|
+
WHERE ${FTS_TABLE_NAME} MATCH ?
|
|
3494
|
+
AND m.kind IN (${placeholders})
|
|
3495
|
+
ORDER BY m.file_path ASC, m.start_line ASC, m.id ASC
|
|
3496
|
+
LIMIT ?
|
|
3497
|
+
`);
|
|
3498
|
+
return stmt.all(matchQuery, ...kinds, maxCandidates);
|
|
3499
|
+
}
|
|
3500
|
+
const needle = mode === "literal" ? q : hint;
|
|
3501
|
+
const escaped = escapeLike(needle);
|
|
3502
|
+
const like = `%${escaped}%`;
|
|
3503
|
+
const placeholders = kinds.map(() => "?").join(", ");
|
|
3504
|
+
const stmt = db.prepare(`
|
|
3505
|
+
SELECT
|
|
3506
|
+
id,
|
|
3507
|
+
kind,
|
|
3508
|
+
content,
|
|
3509
|
+
file_path,
|
|
3510
|
+
start_line,
|
|
3511
|
+
end_line
|
|
3512
|
+
FROM memory_items
|
|
3513
|
+
WHERE content LIKE ? ESCAPE '\\'
|
|
3514
|
+
AND kind IN (${placeholders})
|
|
3515
|
+
ORDER BY file_path ASC, start_line ASC, id ASC
|
|
3516
|
+
LIMIT ?
|
|
3517
|
+
`);
|
|
3518
|
+
return stmt.all(like, ...kinds, maxCandidates);
|
|
3519
|
+
})();
|
|
3520
|
+
const matches = [];
|
|
3521
|
+
let candidatesScanned = 0;
|
|
3522
|
+
let truncated = false;
|
|
3523
|
+
for (const c of candidates) {
|
|
3524
|
+
candidatesScanned += 1;
|
|
3525
|
+
if (!c.file_path || c.start_line == null)
|
|
3526
|
+
continue;
|
|
3527
|
+
if (shouldIgnoreDbFilePath(c.file_path))
|
|
3528
|
+
continue;
|
|
3529
|
+
if (!passesPathFilters(c.file_path, includePaths, excludePaths))
|
|
3530
|
+
continue;
|
|
3531
|
+
const content = c.content ?? "";
|
|
3532
|
+
const lineStarts = buildLineStarts(content);
|
|
3533
|
+
re.lastIndex = 0;
|
|
3534
|
+
let m;
|
|
3535
|
+
while ((m = re.exec(content)) !== null) {
|
|
3536
|
+
const idx = m.index ?? 0;
|
|
3537
|
+
const matched = m[0] ?? "";
|
|
3538
|
+
if (!matched) {
|
|
3539
|
+
if (re.lastIndex >= content.length)
|
|
3540
|
+
break;
|
|
3541
|
+
re.lastIndex += 1;
|
|
3542
|
+
continue;
|
|
3543
|
+
}
|
|
3544
|
+
const lineIdx = lineIndexForOffset(lineStarts, idx);
|
|
3545
|
+
const lineStart = lineStarts[lineIdx] ?? 0;
|
|
3546
|
+
const lineEnd = lineIdx + 1 < lineStarts.length
|
|
3547
|
+
? (lineStarts[lineIdx + 1] ?? content.length) - 1
|
|
3548
|
+
: content.length;
|
|
3549
|
+
const previewRaw = content.slice(lineStart, Math.max(lineStart, lineEnd));
|
|
3550
|
+
const preview = previewRaw.length > 500 ? `${previewRaw.slice(0, 500)}…` : previewRaw;
|
|
3551
|
+
const matchText = matched.length > 200 ? `${matched.slice(0, 200)}…` : matched;
|
|
3552
|
+
matches.push({
|
|
3553
|
+
file_path: c.file_path,
|
|
3554
|
+
kind: c.kind,
|
|
3555
|
+
line: c.start_line + lineIdx,
|
|
3556
|
+
col: idx - lineStart + 1,
|
|
3557
|
+
preview,
|
|
3558
|
+
match: matchText,
|
|
3559
|
+
});
|
|
3560
|
+
if (matches.length >= maxResults) {
|
|
3561
|
+
truncated = true;
|
|
3562
|
+
break;
|
|
3563
|
+
}
|
|
3564
|
+
}
|
|
3565
|
+
if (truncated)
|
|
3566
|
+
break;
|
|
3567
|
+
}
|
|
3568
|
+
logActivity("grep", {
|
|
3569
|
+
query: q,
|
|
3570
|
+
mode,
|
|
3571
|
+
case_sensitive: caseSensitive,
|
|
3572
|
+
smart_case: smartCase,
|
|
3573
|
+
hint,
|
|
3574
|
+
kinds,
|
|
3575
|
+
include_paths: includePaths ?? [],
|
|
3576
|
+
exclude_paths: excludePaths ?? [],
|
|
3577
|
+
candidates: candidates.length,
|
|
3578
|
+
candidates_scanned: candidatesScanned,
|
|
3579
|
+
matches: matches.length,
|
|
3580
|
+
truncated,
|
|
3581
|
+
});
|
|
3582
|
+
return {
|
|
3583
|
+
content: [
|
|
3584
|
+
{
|
|
3585
|
+
type: "text",
|
|
3586
|
+
text: toolJson({
|
|
3587
|
+
ok: true,
|
|
3588
|
+
query: q,
|
|
3589
|
+
mode,
|
|
3590
|
+
case_sensitive: caseSensitive,
|
|
3591
|
+
smart_case: smartCase,
|
|
3592
|
+
hint,
|
|
3593
|
+
kinds,
|
|
3594
|
+
include_paths: includePaths ?? [],
|
|
3595
|
+
exclude_paths: excludePaths ?? [],
|
|
3596
|
+
candidates: { total: candidates.length, scanned: candidatesScanned },
|
|
3597
|
+
matches,
|
|
3598
|
+
truncated,
|
|
3599
|
+
}),
|
|
3600
|
+
},
|
|
3601
|
+
],
|
|
3602
|
+
};
|
|
3603
|
+
}
|
|
3604
|
+
if (toolName === "list_project_files") {
|
|
3605
|
+
const args = ListProjectFilesArgsSchema.parse(rawArgs);
|
|
3606
|
+
const resolved = resolveProjectPathUnderRoot(args.path, { allowRoot: true });
|
|
3607
|
+
let st;
|
|
3608
|
+
try {
|
|
3609
|
+
st = fs.statSync(resolved.absPath);
|
|
3610
|
+
}
|
|
3611
|
+
catch (err) {
|
|
3612
|
+
return {
|
|
3613
|
+
isError: true,
|
|
3614
|
+
content: [{ type: "text", text: toolJson({ ok: false, error: `Path not found: ${String(err)}` }) }],
|
|
3615
|
+
};
|
|
3616
|
+
}
|
|
3617
|
+
const includePaths = args.include_paths?.length ? args.include_paths : null;
|
|
3618
|
+
const excludePaths = args.exclude_paths?.length ? args.exclude_paths : null;
|
|
3619
|
+
const extensions = normalizeExtensionsFilter(args.extensions);
|
|
3620
|
+
const result = listProjectFilesInternal({
|
|
3621
|
+
startAbsPath: resolved.absPath,
|
|
3622
|
+
startDbPath: resolved.dbFilePath,
|
|
3623
|
+
recursive: args.recursive,
|
|
3624
|
+
maxDepth: args.max_depth,
|
|
3625
|
+
includeFiles: args.include_files,
|
|
3626
|
+
includeDirs: args.include_dirs,
|
|
3627
|
+
includeHidden: args.include_hidden,
|
|
3628
|
+
respectIgnore: args.respect_ignore,
|
|
3629
|
+
includePaths,
|
|
3630
|
+
excludePaths,
|
|
3631
|
+
extensions,
|
|
3632
|
+
maxResults: args.max_results,
|
|
3633
|
+
includeStats: args.include_stats,
|
|
3634
|
+
});
|
|
3635
|
+
logActivity("list_project_files", {
|
|
3636
|
+
path: resolved.dbFilePath,
|
|
3637
|
+
recursive: args.recursive,
|
|
3638
|
+
max_depth: args.max_depth,
|
|
3639
|
+
include_files: args.include_files,
|
|
3640
|
+
include_dirs: args.include_dirs,
|
|
3641
|
+
include_hidden: args.include_hidden,
|
|
3642
|
+
respect_ignore: args.respect_ignore,
|
|
3643
|
+
include_paths: includePaths ?? [],
|
|
3644
|
+
exclude_paths: excludePaths ?? [],
|
|
3645
|
+
extensions: extensions ?? [],
|
|
3646
|
+
returned: result.returned,
|
|
3647
|
+
scanned: result.scanned,
|
|
3648
|
+
truncated: result.truncated,
|
|
3649
|
+
path_kind: st.isFile() ? "file" : st.isDirectory() ? "dir" : "other",
|
|
3650
|
+
});
|
|
3651
|
+
return {
|
|
3652
|
+
content: [
|
|
3653
|
+
{
|
|
3654
|
+
type: "text",
|
|
3655
|
+
text: toolJson({
|
|
3656
|
+
ok: true,
|
|
3657
|
+
path: resolved.dbFilePath,
|
|
3658
|
+
path_kind: st.isFile() ? "file" : st.isDirectory() ? "dir" : "other",
|
|
3659
|
+
recursive: args.recursive,
|
|
3660
|
+
max_depth: args.recursive ? args.max_depth : 1,
|
|
3661
|
+
include_files: args.include_files,
|
|
3662
|
+
include_dirs: args.include_dirs,
|
|
3663
|
+
include_hidden: args.include_hidden,
|
|
3664
|
+
respect_ignore: args.respect_ignore,
|
|
3665
|
+
include_paths: includePaths ?? [],
|
|
3666
|
+
exclude_paths: excludePaths ?? [],
|
|
3667
|
+
extensions: extensions ?? [],
|
|
3668
|
+
returned: result.returned,
|
|
3669
|
+
scanned: result.scanned,
|
|
3670
|
+
truncated: result.truncated,
|
|
3671
|
+
entries: result.entries,
|
|
3672
|
+
}),
|
|
3673
|
+
},
|
|
3674
|
+
],
|
|
3675
|
+
};
|
|
3676
|
+
}
|
|
3677
|
+
if (toolName === "read_file_text") {
|
|
3678
|
+
const args = ReadFileTextArgsSchema.parse(rawArgs);
|
|
3679
|
+
const resolved = resolveReadPathUnderProjectRoot(args.path);
|
|
3680
|
+
let st;
|
|
3681
|
+
try {
|
|
3682
|
+
st = fs.statSync(resolved.absPath);
|
|
3683
|
+
}
|
|
3684
|
+
catch (err) {
|
|
3685
|
+
return {
|
|
3686
|
+
isError: true,
|
|
3687
|
+
content: [{ type: "text", text: toolJson({ ok: false, error: `File not found: ${String(err)}` }) }],
|
|
3688
|
+
};
|
|
3689
|
+
}
|
|
3690
|
+
if (!st.isFile()) {
|
|
3691
|
+
return { isError: true, content: [{ type: "text", text: toolJson({ ok: false, error: "Not a file" }) }] };
|
|
3692
|
+
}
|
|
3693
|
+
let result;
|
|
3694
|
+
try {
|
|
3695
|
+
result = readTextFileSlice({
|
|
3696
|
+
absPath: resolved.absPath,
|
|
3697
|
+
offset: args.offset,
|
|
3698
|
+
maxChars: args.max_chars,
|
|
3699
|
+
maxFileBytes: args.max_file_bytes,
|
|
3700
|
+
});
|
|
3701
|
+
}
|
|
3702
|
+
catch (err) {
|
|
3703
|
+
return { isError: true, content: [{ type: "text", text: toolJson({ ok: false, error: String(err) }) }] };
|
|
3704
|
+
}
|
|
3705
|
+
logActivity("read_file_text", {
|
|
3706
|
+
file_path: resolved.dbFilePath,
|
|
3707
|
+
offset: args.offset,
|
|
3708
|
+
returned_chars: result.returnedChars,
|
|
3709
|
+
total_chars: result.totalChars,
|
|
3710
|
+
truncated: result.truncated,
|
|
3711
|
+
});
|
|
3712
|
+
return {
|
|
3713
|
+
content: [
|
|
3714
|
+
{
|
|
3715
|
+
type: "text",
|
|
3716
|
+
text: toolJson({
|
|
3717
|
+
ok: true,
|
|
3718
|
+
file_path: resolved.dbFilePath,
|
|
3719
|
+
offset: args.offset,
|
|
3720
|
+
returned_chars: result.returnedChars,
|
|
3721
|
+
total_chars: result.totalChars,
|
|
3722
|
+
truncated: result.truncated,
|
|
3723
|
+
text: result.text,
|
|
3724
|
+
}),
|
|
3725
|
+
},
|
|
3726
|
+
],
|
|
3727
|
+
};
|
|
3728
|
+
}
|
|
3729
|
+
if (toolName === "read_codex_text_file") {
|
|
3730
|
+
const args = ReadCodexTextFileArgsSchema.parse(rawArgs);
|
|
3731
|
+
let resolved;
|
|
3732
|
+
try {
|
|
3733
|
+
resolved = resolveCodexTextPath(args.path);
|
|
3734
|
+
}
|
|
3735
|
+
catch (err) {
|
|
3736
|
+
return { isError: true, content: [{ type: "text", text: toolJson({ ok: false, error: String(err) }) }] };
|
|
3737
|
+
}
|
|
3738
|
+
let st;
|
|
3739
|
+
try {
|
|
3740
|
+
st = fs.statSync(resolved.absPath);
|
|
3741
|
+
}
|
|
3742
|
+
catch (err) {
|
|
3743
|
+
return {
|
|
3744
|
+
isError: true,
|
|
3745
|
+
content: [{ type: "text", text: toolJson({ ok: false, error: `File not found: ${String(err)}` }) }],
|
|
3746
|
+
};
|
|
3747
|
+
}
|
|
3748
|
+
if (!st.isFile()) {
|
|
3749
|
+
return { isError: true, content: [{ type: "text", text: toolJson({ ok: false, error: "Not a file" }) }] };
|
|
3750
|
+
}
|
|
3751
|
+
let result;
|
|
3752
|
+
try {
|
|
3753
|
+
result = readTextFileSlice({
|
|
3754
|
+
absPath: resolved.absPath,
|
|
3755
|
+
offset: args.offset,
|
|
3756
|
+
maxChars: args.max_chars,
|
|
3757
|
+
maxFileBytes: args.max_file_bytes,
|
|
3758
|
+
});
|
|
3759
|
+
}
|
|
3760
|
+
catch (err) {
|
|
3761
|
+
return { isError: true, content: [{ type: "text", text: toolJson({ ok: false, error: String(err) }) }] };
|
|
3762
|
+
}
|
|
3763
|
+
logActivity("read_codex_text_file", {
|
|
3764
|
+
file_path: resolved.displayPath,
|
|
3765
|
+
allowed_root: resolved.allowedRoot,
|
|
3766
|
+
offset: args.offset,
|
|
3767
|
+
returned_chars: result.returnedChars,
|
|
3768
|
+
total_chars: result.totalChars,
|
|
3769
|
+
truncated: result.truncated,
|
|
3770
|
+
});
|
|
3771
|
+
return {
|
|
3772
|
+
content: [
|
|
3773
|
+
{
|
|
3774
|
+
type: "text",
|
|
3775
|
+
text: toolJson({
|
|
3776
|
+
ok: true,
|
|
3777
|
+
file_path: resolved.displayPath,
|
|
3778
|
+
allowed_root: resolved.allowedRoot,
|
|
3779
|
+
offset: args.offset,
|
|
3780
|
+
returned_chars: result.returnedChars,
|
|
3781
|
+
total_chars: result.totalChars,
|
|
3782
|
+
truncated: result.truncated,
|
|
3783
|
+
text: result.text,
|
|
3784
|
+
}),
|
|
3785
|
+
},
|
|
3786
|
+
],
|
|
3787
|
+
};
|
|
3788
|
+
}
|
|
3789
|
+
if (toolName === "read_file_lines") {
|
|
3790
|
+
const args = ReadFileLinesArgsSchema.parse(rawArgs);
|
|
3791
|
+
const resolved = resolveReadPathUnderProjectRoot(args.path);
|
|
3792
|
+
let fromLine = args.from_line;
|
|
3793
|
+
let toLine = args.to_line;
|
|
3794
|
+
if (toLine == null) {
|
|
3795
|
+
const total = args.total_count ?? 200;
|
|
3796
|
+
toLine = fromLine + total - 1;
|
|
3797
|
+
}
|
|
3798
|
+
if (toLine < fromLine) {
|
|
3799
|
+
return {
|
|
3800
|
+
isError: true,
|
|
3801
|
+
content: [
|
|
3802
|
+
{
|
|
3803
|
+
type: "text",
|
|
3804
|
+
text: toolJson({
|
|
3805
|
+
ok: false,
|
|
3806
|
+
error: "to_line must be >= from_line",
|
|
3807
|
+
path: args.path,
|
|
3808
|
+
from_line: fromLine,
|
|
3809
|
+
to_line: toLine,
|
|
3810
|
+
}),
|
|
3811
|
+
},
|
|
3812
|
+
],
|
|
3813
|
+
};
|
|
3814
|
+
}
|
|
3815
|
+
let st;
|
|
3816
|
+
try {
|
|
3817
|
+
st = fs.statSync(resolved.absPath);
|
|
3818
|
+
}
|
|
3819
|
+
catch (err) {
|
|
3820
|
+
return {
|
|
3821
|
+
isError: true,
|
|
3822
|
+
content: [
|
|
3823
|
+
{ type: "text", text: toolJson({ ok: false, error: `File not found: ${String(err)}` }) },
|
|
3824
|
+
],
|
|
3825
|
+
};
|
|
3826
|
+
}
|
|
3827
|
+
if (!st.isFile()) {
|
|
3828
|
+
return { isError: true, content: [{ type: "text", text: toolJson({ ok: false, error: "Not a file" }) }] };
|
|
3829
|
+
}
|
|
3830
|
+
const maxLines = Math.max(1, Math.min(2000, args.max_lines));
|
|
3831
|
+
const maxChars = Math.max(200, Math.min(200_000, args.max_chars));
|
|
3832
|
+
const result = await readTextFileLines({
|
|
3833
|
+
absPath: resolved.absPath,
|
|
3834
|
+
fromLine,
|
|
3835
|
+
toLine,
|
|
3836
|
+
maxLines,
|
|
3837
|
+
maxChars,
|
|
3838
|
+
});
|
|
3839
|
+
logActivity("read_file_lines", {
|
|
3840
|
+
file_path: resolved.dbFilePath,
|
|
3841
|
+
from_line: fromLine,
|
|
3842
|
+
to_line: toLine,
|
|
3843
|
+
returned: result.returned,
|
|
3844
|
+
truncated: result.truncated,
|
|
3845
|
+
});
|
|
3846
|
+
return {
|
|
3847
|
+
content: [
|
|
3848
|
+
{
|
|
3849
|
+
type: "text",
|
|
3850
|
+
text: toolJson({
|
|
3851
|
+
ok: true,
|
|
3852
|
+
file_path: resolved.dbFilePath,
|
|
3853
|
+
from_line: fromLine,
|
|
3854
|
+
to_line: toLine,
|
|
3855
|
+
returned: result.returned,
|
|
3856
|
+
truncated: result.truncated,
|
|
3857
|
+
text: result.text,
|
|
3858
|
+
}),
|
|
3859
|
+
},
|
|
3860
|
+
],
|
|
3861
|
+
};
|
|
3862
|
+
}
|
|
2581
3863
|
if (toolName === "query_codebase") {
|
|
2582
3864
|
const args = QueryCodebaseArgsSchema.parse(rawArgs);
|
|
2583
3865
|
const q = args.query.trim();
|