@coreyuan/vector-mind 1.0.30 → 1.0.32

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/dist/index.js CHANGED
@@ -4,6 +4,7 @@ import fs from "node:fs";
4
4
  import * as readline from "node:readline";
5
5
  import crypto from "node:crypto";
6
6
  import os from "node:os";
7
+ import { spawnSync } from "node:child_process";
7
8
  import { fileURLToPath } from "node:url";
8
9
  import chokidar from "chokidar";
9
10
  import Database from "better-sqlite3";
@@ -13,9 +14,9 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
13
14
  import { toJsonSchemaCompat } from "@modelcontextprotocol/sdk/server/zod-json-schema-compat.js";
14
15
  import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
15
16
  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";
17
+ import { BUILTIN_ARCHITECTURE_AND_CODE_ORGANIZATION_INSTRUCTIONS, BUILTIN_DESTRUCTIVE_OPERATION_GUARD_INSTRUCTIONS, BUILTIN_LOW_OVERHEAD_WORKFLOW_INSTRUCTIONS, BUILTIN_PLAN_LITE_INSTRUCTIONS, BUILTIN_WRITE_POLICY_INSTRUCTIONS, } from "./builtin-instructions.js";
17
18
  const SERVER_NAME = "vector-mind";
18
- const SERVER_VERSION = "1.0.30";
19
+ const SERVER_VERSION = "1.0.32";
19
20
  const rootFromEnv = process.env.VECTORMIND_ROOT?.trim() ?? "";
20
21
  const prettyJsonOutput = ["1", "true", "on", "yes"].includes((process.env.VECTORMIND_PRETTY_JSON ?? "").trim().toLowerCase());
21
22
  const debugLogEnabled = ["1", "true", "on", "yes"].includes((process.env.VECTORMIND_DEBUG_LOG ?? "").trim().toLowerCase());
@@ -64,6 +65,11 @@ const PENDING_PRUNE_EVERY = (() => {
64
65
  return 500;
65
66
  return n;
66
67
  })();
68
+ const RIPGREP_RESOLVE_TIMEOUT_MS = 5_000;
69
+ const RIPGREP_SEARCH_TIMEOUT_MS = 30_000;
70
+ const RIPGREP_MAX_BUFFER_BYTES = 16 * 1024 * 1024;
71
+ let cachedRipgrepCommand;
72
+ let cachedRipgrepResolveError = null;
67
73
  function getCodexHomeDir() {
68
74
  const raw = process.env.CODEX_HOME?.trim();
69
75
  if (raw)
@@ -253,7 +259,7 @@ function summarizeActivityEvent(e) {
253
259
  case "semantic_search":
254
260
  return `semantic_search mode=${String(d.mode ?? "")} q=${String(d.query ?? "")} matches=${String(d.matches ?? "")}`;
255
261
  case "grep":
256
- return `grep q=${String(d.query ?? "")} matches=${String(d.matches ?? "")} truncated=${String(d.truncated ?? "")}`;
262
+ return `grep backend=${String(d.backend ?? "")} q=${String(d.query ?? "")} matches=${String(d.matches ?? "")} truncated=${String(d.truncated ?? "")}`;
257
263
  case "query_codebase":
258
264
  return `query_codebase q=${String(d.query ?? "")} matches=${String(d.matches ?? "")}`;
259
265
  case "read_file_lines":
@@ -505,6 +511,22 @@ const IGNORED_PATH_SEGMENTS = new Set([
505
511
  "x64",
506
512
  "x86",
507
513
  ].map((s) => s.toLowerCase()));
514
+ const NOISE_FILE_SUFFIXES = [
515
+ ".min.js",
516
+ ".min.css",
517
+ ".bundle.js",
518
+ ".bundle.css",
519
+ ".chunk.js",
520
+ ".chunk.css",
521
+ ];
522
+ const NOISE_FILE_BASENAMES = [
523
+ "package-lock.json",
524
+ "pnpm-lock.yaml",
525
+ "yarn.lock",
526
+ "bun.lockb",
527
+ "cargo.lock",
528
+ "composer.lock",
529
+ ];
508
530
  const IGNORED_LIKE_PATTERNS = (() => {
509
531
  const patterns = [];
510
532
  for (const seg of IGNORED_PATH_SEGMENTS) {
@@ -608,27 +630,11 @@ function pruneIgnoredIndexesByPathPatterns() {
608
630
  function pruneFilenameNoiseIndexes() {
609
631
  if (!db)
610
632
  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
633
  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}`);
634
+ const suffixWhere = NOISE_FILE_SUFFIXES.map(() => "LOWER(file_path) LIKE ?").join(" OR ");
635
+ const baseWhere = NOISE_FILE_BASENAMES.map(() => "LOWER(file_path) LIKE ?").join(" OR ");
636
+ const suffixArgs = NOISE_FILE_SUFFIXES.map((s) => `%${s}`);
637
+ const baseArgs = NOISE_FILE_BASENAMES.map((n) => `%/${n}`);
632
638
  const whereParts = [];
633
639
  const args = [];
634
640
  if (suffixWhere) {
@@ -710,21 +716,9 @@ function isSymbolIndexableFile(filePath) {
710
716
  }
711
717
  function shouldIgnoreContentFile(filePath) {
712
718
  const base = path.basename(filePath).toLowerCase();
713
- const ignoreNames = new Set([
714
- "package-lock.json",
715
- "pnpm-lock.yaml",
716
- "yarn.lock",
717
- "bun.lockb",
718
- "cargo.lock",
719
- "composer.lock",
720
- ]);
721
- if (ignoreNames.has(base))
722
- return true;
723
- if (base.endsWith(".min.js") || base.endsWith(".min.css"))
724
- return true;
725
- if (base.endsWith(".bundle.js") || base.endsWith(".bundle.css"))
719
+ if (NOISE_FILE_BASENAMES.includes(base))
726
720
  return true;
727
- if (base.endsWith(".chunk.js") || base.endsWith(".chunk.css"))
721
+ if (NOISE_FILE_SUFFIXES.some((suffix) => base.endsWith(suffix)))
728
722
  return true;
729
723
  return false;
730
724
  }
@@ -1175,13 +1169,14 @@ const GrepArgsSchema = ProjectRootArgSchema.merge(z.object({
1175
1169
  // If case_sensitive is omitted and smart_case=true, uppercase => case-sensitive, otherwise case-insensitive.
1176
1170
  smart_case: z.boolean().optional().default(true),
1177
1171
  case_sensitive: z.boolean().optional(),
1178
- // Optional hint used to narrow candidates quickly when mode=regex and the pattern has few literals.
1172
+ // Compatibility knob for the indexed fallback when ripgrep is unavailable.
1179
1173
  literal_hint: z.string().optional().default(""),
1180
- // Defaults to code/doc chunks; can be widened if needed.
1174
+ // Compatibility knob for the indexed fallback when ripgrep is unavailable.
1181
1175
  kinds: z.array(z.string().min(1)).optional(),
1182
1176
  include_paths: z.array(z.string().min(1)).optional(),
1183
1177
  exclude_paths: z.array(z.string().min(1)).optional(),
1184
1178
  max_results: z.number().int().min(1).max(5000).optional().default(200),
1179
+ // Compatibility knob for the indexed fallback when ripgrep is unavailable.
1185
1180
  max_candidates: z.number().int().min(1).max(50_000).optional(),
1186
1181
  }));
1187
1182
  const ReadFileLinesArgsSchema = ProjectRootArgSchema.merge(z.object({
@@ -1922,6 +1917,385 @@ function compileGrepRegex(opts) {
1922
1917
  const source = opts.mode === "literal" ? escapeRegExp(opts.query) : opts.query;
1923
1918
  return new RegExp(source, flags);
1924
1919
  }
1920
+ function trimGrepText(input, maxChars) {
1921
+ if (input.length <= maxChars)
1922
+ return input;
1923
+ return `${input.slice(0, maxChars)}…`;
1924
+ }
1925
+ function buildGrepPreviewSnippet(lineText, col, maxChars = 500) {
1926
+ const clean = lineText.replace(/\r$/, "");
1927
+ if (clean.length <= maxChars)
1928
+ return clean;
1929
+ const matchIndex = Math.max(0, col - 1);
1930
+ let start = Math.max(0, matchIndex - Math.floor(maxChars * 0.35));
1931
+ if (start + maxChars > clean.length)
1932
+ start = Math.max(0, clean.length - maxChars);
1933
+ const end = Math.min(clean.length, start + maxChars);
1934
+ let snippet = clean.slice(start, end);
1935
+ if (start > 0)
1936
+ snippet = `…${snippet}`;
1937
+ if (end < clean.length)
1938
+ snippet = `${snippet}…`;
1939
+ return snippet;
1940
+ }
1941
+ function extractGrepMatchText(opts) {
1942
+ const clean = opts.lineText.replace(/\r$/, "");
1943
+ const startIndex = Math.max(0, opts.col - 1);
1944
+ if (opts.mode === "literal") {
1945
+ const slice = clean.slice(startIndex, startIndex + opts.query.length) || opts.query;
1946
+ return trimGrepText(slice, 200);
1947
+ }
1948
+ try {
1949
+ const flags = opts.caseSensitive ? "m" : "im";
1950
+ const anchored = new RegExp(opts.query, flags);
1951
+ const tail = clean.slice(startIndex);
1952
+ const found = anchored.exec(tail);
1953
+ if (found?.index === 0 && found[0])
1954
+ return trimGrepText(found[0], 200);
1955
+ }
1956
+ catch { }
1957
+ const fallback = clean.slice(startIndex, Math.min(clean.length, startIndex + 200));
1958
+ return trimGrepText(fallback || opts.query, 200);
1959
+ }
1960
+ function toProcessText(value) {
1961
+ if (typeof value === "string")
1962
+ return value;
1963
+ if (value == null)
1964
+ return "";
1965
+ return value.toString("utf8");
1966
+ }
1967
+ function formatProcessFailure(result) {
1968
+ if (result.error)
1969
+ return `${result.error.name}: ${result.error.message}`;
1970
+ const stderr = toProcessText(result.stderr).trim();
1971
+ if (stderr)
1972
+ return stderr;
1973
+ const stdout = toProcessText(result.stdout).trim();
1974
+ if (stdout)
1975
+ return stdout;
1976
+ if (typeof result.status === "number")
1977
+ return `exit ${result.status}`;
1978
+ if (result.signal)
1979
+ return `signal ${result.signal}`;
1980
+ return "unknown failure";
1981
+ }
1982
+ function buildRipgrepEnv() {
1983
+ const env = { ...process.env };
1984
+ delete env.RIPGREP_CONFIG_PATH;
1985
+ return env;
1986
+ }
1987
+ function pushUniqueCandidate(candidates, seen, raw) {
1988
+ const value = raw?.trim();
1989
+ if (!value || seen.has(value))
1990
+ return;
1991
+ seen.add(value);
1992
+ candidates.push(value);
1993
+ }
1994
+ function listChildDirsSafe(dirPath) {
1995
+ try {
1996
+ return fs
1997
+ .readdirSync(dirPath, { withFileTypes: true })
1998
+ .filter((entry) => entry.isDirectory())
1999
+ .map((entry) => path.join(dirPath, entry.name));
2000
+ }
2001
+ catch {
2002
+ return [];
2003
+ }
2004
+ }
2005
+ function collectRipgrepCandidates() {
2006
+ const candidates = [];
2007
+ const seen = new Set();
2008
+ const override = process.env.VECTORMIND_RG_PATH?.trim();
2009
+ if (override)
2010
+ pushUniqueCandidate(candidates, seen, path.resolve(override));
2011
+ if (process.platform === "win32") {
2012
+ pushUniqueCandidate(candidates, seen, "rg.exe");
2013
+ pushUniqueCandidate(candidates, seen, "rg");
2014
+ }
2015
+ else {
2016
+ pushUniqueCandidate(candidates, seen, "rg");
2017
+ }
2018
+ for (const rawDir of (process.env.PATH ?? "").split(path.delimiter)) {
2019
+ const dir = rawDir.trim().replace(/^"+|"+$/g, "");
2020
+ if (!dir)
2021
+ continue;
2022
+ if (process.platform === "win32") {
2023
+ pushUniqueCandidate(candidates, seen, path.join(dir, "rg.exe"));
2024
+ pushUniqueCandidate(candidates, seen, path.join(dir, "rg"));
2025
+ }
2026
+ else {
2027
+ pushUniqueCandidate(candidates, seen, path.join(dir, "rg"));
2028
+ }
2029
+ }
2030
+ if (process.platform === "win32") {
2031
+ const localAppData = process.env.LOCALAPPDATA?.trim();
2032
+ const programsDir = localAppData ? path.join(localAppData, "Programs") : "";
2033
+ if (programsDir && fs.existsSync(programsDir)) {
2034
+ for (const appDir of listChildDirsSafe(programsDir)) {
2035
+ pushUniqueCandidate(candidates, seen, path.join(appDir, "resources", "app", "node_modules", "@vscode", "ripgrep", "bin", "rg.exe"));
2036
+ pushUniqueCandidate(candidates, seen, path.join(appDir, "resources", "app", "extensions", "kiro.kiro-agent", "node_modules", "@vscode", "ripgrep", "bin", "rg.exe"));
2037
+ for (const childDir of listChildDirsSafe(appDir)) {
2038
+ pushUniqueCandidate(candidates, seen, path.join(childDir, "resources", "app", "node_modules", "@vscode", "ripgrep", "bin", "rg.exe"));
2039
+ }
2040
+ }
2041
+ }
2042
+ }
2043
+ return candidates;
2044
+ }
2045
+ function resolveRipgrepCommand() {
2046
+ if (typeof cachedRipgrepCommand !== "undefined") {
2047
+ if (cachedRipgrepCommand)
2048
+ return { ok: true, command: cachedRipgrepCommand };
2049
+ return { ok: false, error: cachedRipgrepResolveError ?? "ripgrep unavailable", attempts: [] };
2050
+ }
2051
+ const env = buildRipgrepEnv();
2052
+ const attempts = [];
2053
+ for (const candidate of collectRipgrepCandidates()) {
2054
+ const probe = spawnSync(candidate, ["--version"], {
2055
+ cwd: projectRoot || undefined,
2056
+ env,
2057
+ encoding: "utf8",
2058
+ windowsHide: true,
2059
+ timeout: RIPGREP_RESOLVE_TIMEOUT_MS,
2060
+ maxBuffer: 256 * 1024,
2061
+ });
2062
+ if (probe.status === 0) {
2063
+ cachedRipgrepCommand = candidate;
2064
+ cachedRipgrepResolveError = null;
2065
+ return { ok: true, command: candidate };
2066
+ }
2067
+ attempts.push(`${candidate}: ${formatProcessFailure(probe)}`);
2068
+ }
2069
+ cachedRipgrepCommand = null;
2070
+ cachedRipgrepResolveError = attempts.slice(0, 8).join(" | ") || "ripgrep unavailable";
2071
+ return { ok: false, error: cachedRipgrepResolveError, attempts };
2072
+ }
2073
+ function appendBuiltInRipgrepExcludes(args) {
2074
+ for (const segment of IGNORED_PATH_SEGMENTS) {
2075
+ args.push("-g", `!${segment}/**`);
2076
+ args.push("-g", `!**/${segment}/**`);
2077
+ }
2078
+ for (const baseName of NOISE_FILE_BASENAMES) {
2079
+ args.push("-g", `!**/${baseName}`);
2080
+ }
2081
+ for (const suffix of NOISE_FILE_SUFFIXES) {
2082
+ args.push("-g", `!**/*${suffix}`);
2083
+ }
2084
+ }
2085
+ function runRipgrepSearch(opts) {
2086
+ const resolved = resolveRipgrepCommand();
2087
+ if (!resolved.ok) {
2088
+ return { ok: false, unavailable: true, error: resolved.error, attempts: resolved.attempts };
2089
+ }
2090
+ const args = ["--vimgrep", "--no-heading", "--color", "never", "-m", String(opts.maxResults)];
2091
+ args.push(opts.caseSensitive ? "-s" : "-i");
2092
+ if (opts.mode === "literal")
2093
+ args.push("-F");
2094
+ appendBuiltInRipgrepExcludes(args);
2095
+ args.push("--", opts.query, ".");
2096
+ const result = spawnSync(resolved.command, args, {
2097
+ cwd: projectRoot,
2098
+ env: buildRipgrepEnv(),
2099
+ encoding: "utf8",
2100
+ windowsHide: true,
2101
+ timeout: RIPGREP_SEARCH_TIMEOUT_MS,
2102
+ maxBuffer: RIPGREP_MAX_BUFFER_BYTES,
2103
+ });
2104
+ if (result.error) {
2105
+ return {
2106
+ ok: false,
2107
+ unavailable: false,
2108
+ error: formatProcessFailure(result),
2109
+ attempts: [],
2110
+ rg_command: resolved.command,
2111
+ exit_status: result.status,
2112
+ };
2113
+ }
2114
+ const status = result.status ?? 0;
2115
+ if (status !== 0 && status !== 1) {
2116
+ return {
2117
+ ok: false,
2118
+ unavailable: false,
2119
+ error: formatProcessFailure(result),
2120
+ attempts: [],
2121
+ rg_command: resolved.command,
2122
+ exit_status: status,
2123
+ };
2124
+ }
2125
+ const matches = [];
2126
+ let totalMatches = 0;
2127
+ let truncated = false;
2128
+ for (const rawLine of toProcessText(result.stdout).split(/\r?\n/)) {
2129
+ if (!rawLine)
2130
+ continue;
2131
+ const parsed = /^(.*?):(\d+):(\d+):(.*)$/.exec(rawLine);
2132
+ if (!parsed)
2133
+ continue;
2134
+ const filePath = path.posix
2135
+ .normalize(parsed[1].replace(/\\/g, "/"))
2136
+ .replace(/^\.\/+/, "");
2137
+ const lineNumber = Number.parseInt(parsed[2] ?? "0", 10);
2138
+ const colNumber = Number.parseInt(parsed[3] ?? "0", 10);
2139
+ const lineText = (parsed[4] ?? "").replace(/\r$/, "");
2140
+ if (!filePath || !Number.isFinite(lineNumber) || !Number.isFinite(colNumber))
2141
+ continue;
2142
+ if (shouldIgnoreDbFilePath(filePath))
2143
+ continue;
2144
+ if (shouldIgnoreContentFile(filePath))
2145
+ continue;
2146
+ if (!passesPathFilters(filePath, opts.includePaths, opts.excludePaths))
2147
+ continue;
2148
+ totalMatches += 1;
2149
+ if (matches.length >= opts.maxResults) {
2150
+ truncated = true;
2151
+ continue;
2152
+ }
2153
+ matches.push({
2154
+ file_path: filePath,
2155
+ kind: "file_match",
2156
+ line: lineNumber,
2157
+ col: colNumber,
2158
+ preview: buildGrepPreviewSnippet(lineText, colNumber),
2159
+ match: extractGrepMatchText({
2160
+ lineText,
2161
+ query: opts.query,
2162
+ mode: opts.mode,
2163
+ caseSensitive: opts.caseSensitive,
2164
+ col: colNumber,
2165
+ }),
2166
+ });
2167
+ }
2168
+ return {
2169
+ ok: true,
2170
+ backend: "ripgrep",
2171
+ rg_command: resolved.command,
2172
+ matches,
2173
+ truncated,
2174
+ total_matches: totalMatches,
2175
+ };
2176
+ }
2177
+ function runIndexedGrepSearch(opts) {
2178
+ const hint = (() => {
2179
+ if (opts.mode === "literal")
2180
+ return opts.query;
2181
+ const explicit = opts.literalHint.trim();
2182
+ if (explicit)
2183
+ return explicit;
2184
+ return extractLongestLiteralFromRegex(opts.query);
2185
+ })();
2186
+ if (opts.mode === "regex" && hint.trim().length < 3) {
2187
+ throw new Error("Regex has no sufficiently long literal anchor for indexed narrowing. Provide literal_hint (>= 3 chars) or narrow with include_paths.");
2188
+ }
2189
+ let re;
2190
+ try {
2191
+ re = compileGrepRegex({
2192
+ query: opts.query,
2193
+ mode: opts.mode,
2194
+ caseSensitive: opts.caseSensitive,
2195
+ });
2196
+ }
2197
+ catch (err) {
2198
+ throw new Error(`Invalid pattern: ${String(err)}`);
2199
+ }
2200
+ const maxCandidates = opts.maxCandidates ?? Math.min(50_000, Math.max(1000, opts.maxResults * 200));
2201
+ const candidates = (() => {
2202
+ if (ftsAvailable) {
2203
+ const matchQuery = buildFtsMatchQuery(hint);
2204
+ const placeholders = opts.kinds.map(() => "?").join(", ");
2205
+ const stmt = db.prepare(`
2206
+ SELECT
2207
+ m.id as id,
2208
+ m.kind as kind,
2209
+ m.content as content,
2210
+ m.file_path as file_path,
2211
+ m.start_line as start_line,
2212
+ m.end_line as end_line
2213
+ FROM ${FTS_TABLE_NAME}
2214
+ JOIN memory_items m ON m.id = ${FTS_TABLE_NAME}.rowid
2215
+ WHERE ${FTS_TABLE_NAME} MATCH ?
2216
+ AND m.kind IN (${placeholders})
2217
+ ORDER BY m.file_path ASC, m.start_line ASC, m.id ASC
2218
+ LIMIT ?
2219
+ `);
2220
+ return stmt.all(matchQuery, ...opts.kinds, maxCandidates);
2221
+ }
2222
+ const needle = opts.mode === "literal" ? opts.query : hint;
2223
+ const escaped = escapeLike(needle);
2224
+ const like = `%${escaped}%`;
2225
+ const placeholders = opts.kinds.map(() => "?").join(", ");
2226
+ const stmt = db.prepare(`
2227
+ SELECT
2228
+ id,
2229
+ kind,
2230
+ content,
2231
+ file_path,
2232
+ start_line,
2233
+ end_line
2234
+ FROM memory_items
2235
+ WHERE content LIKE ? ESCAPE '\\'
2236
+ AND kind IN (${placeholders})
2237
+ ORDER BY file_path ASC, start_line ASC, id ASC
2238
+ LIMIT ?
2239
+ `);
2240
+ return stmt.all(like, ...opts.kinds, maxCandidates);
2241
+ })();
2242
+ const matches = [];
2243
+ let candidatesScanned = 0;
2244
+ let truncated = false;
2245
+ for (const c of candidates) {
2246
+ candidatesScanned += 1;
2247
+ if (!c.file_path || c.start_line == null)
2248
+ continue;
2249
+ if (shouldIgnoreDbFilePath(c.file_path))
2250
+ continue;
2251
+ if (!passesPathFilters(c.file_path, opts.includePaths, opts.excludePaths))
2252
+ continue;
2253
+ const content = c.content ?? "";
2254
+ const lineStarts = buildLineStarts(content);
2255
+ re.lastIndex = 0;
2256
+ let m;
2257
+ while ((m = re.exec(content)) !== null) {
2258
+ const idx = m.index ?? 0;
2259
+ const matched = m[0] ?? "";
2260
+ if (!matched) {
2261
+ if (re.lastIndex >= content.length)
2262
+ break;
2263
+ re.lastIndex += 1;
2264
+ continue;
2265
+ }
2266
+ const lineIdx = lineIndexForOffset(lineStarts, idx);
2267
+ const lineStart = lineStarts[lineIdx] ?? 0;
2268
+ const lineEnd = lineIdx + 1 < lineStarts.length
2269
+ ? (lineStarts[lineIdx + 1] ?? content.length) - 1
2270
+ : content.length;
2271
+ const previewRaw = content.slice(lineStart, Math.max(lineStart, lineEnd));
2272
+ matches.push({
2273
+ file_path: c.file_path,
2274
+ kind: c.kind,
2275
+ line: c.start_line + lineIdx,
2276
+ col: idx - lineStart + 1,
2277
+ preview: trimGrepText(previewRaw, 500),
2278
+ match: trimGrepText(matched, 200),
2279
+ });
2280
+ if (matches.length >= opts.maxResults) {
2281
+ truncated = true;
2282
+ break;
2283
+ }
2284
+ }
2285
+ if (truncated)
2286
+ break;
2287
+ }
2288
+ return {
2289
+ backend: "indexed_fallback",
2290
+ hint,
2291
+ kinds: opts.kinds,
2292
+ include_paths: opts.includePaths ?? [],
2293
+ exclude_paths: opts.excludePaths ?? [],
2294
+ candidates: { total: candidates.length, scanned: candidatesScanned },
2295
+ matches,
2296
+ truncated,
2297
+ };
2298
+ }
1925
2299
  function resolveProjectPathUnderRoot(inputPath, opts = {}) {
1926
2300
  const normalizedInput = inputPath.trim() || ".";
1927
2301
  const abs = path.isAbsolute(normalizedInput) ? normalizedInput : path.join(projectRoot, normalizedInput);
@@ -2150,15 +2524,19 @@ function buildServerInstructions() {
2150
2524
  "Built-in architecture and code-organization policy:",
2151
2525
  BUILTIN_ARCHITECTURE_AND_CODE_ORGANIZATION_INSTRUCTIONS,
2152
2526
  "",
2527
+ "Built-in low-overhead execution and heavy-thread policy:",
2528
+ BUILTIN_LOW_OVERHEAD_WORKFLOW_INSTRUCTIONS,
2529
+ "",
2153
2530
  "Required workflow:",
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).",
2531
+ "- On every new conversation/session for analysis/design/development work: 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).",
2155
2532
  " - Output is compact by default. Use include_content=true only when you truly need full text (it increases tokens).",
2156
2533
  " - Tune output size with: requirements_limit/changes_limit/notes_limit, preview_chars, pending_limit/pending_offset.",
2157
2534
  " - Prefer read_memory_item(id, offset, limit) to fetch full text on demand instead of returning large content in other tool outputs.",
2535
+ "- For pure execution-first tasks with explicit targets (for example compile/build/run/launch/package/publish/test rerun), you may skip retrieval and go straight to the minimum necessary shell or host tools unless code/context lookup is actually needed to unblock execution.",
2158
2536
  "- 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
2537
  "- 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
2538
  "- 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).",
2539
+ "- For raw repo text search with exact file+line+col matches, prefer grep({ query: <pattern> }). It uses ripgrep against real project files when available, applies built-in noise filters, and only falls back to indexed search if ripgrep is unavailable.",
2162
2540
  "- 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.",
2163
2541
  "- BEFORE editing code: call start_requirement(title, background) to set the active requirement.",
2164
2542
  "- 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.)",
@@ -2167,6 +2545,7 @@ function buildServerInstructions() {
2167
2545
  "- When you need full text for a specific note/summary/match: call read_memory_item(id, offset, limit) and page through it.",
2168
2546
  "- When asked to locate code (class/function/type): call query_codebase(query) instead of guessing.",
2169
2547
  "- When you need to recall relevant context from history/code/docs: call semantic_search(query, ...) instead of guessing.",
2548
+ "- If the current thread is already heavy or the user reports it has become slow, switch to a lighter workflow: avoid redundant retrieval, keep outputs compact, and recommend continuing substantial new analysis in a fresh thread after a short handoff summary.",
2170
2549
  "",
2171
2550
  "If tool output conflicts with assumptions, trust the tool output.",
2172
2551
  ].join("\n");
@@ -2732,7 +3111,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2732
3111
  },
2733
3112
  {
2734
3113
  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.",
3114
+ description: "Repo text search with precise file/line/col matches, powered by ripgrep against real project files plus built-in noise filters. Falls back to indexed search only when ripgrep is unavailable.",
2736
3115
  inputSchema: toJsonSchemaCompat(GrepArgsSchema),
2737
3116
  },
2738
3117
  {
@@ -3434,15 +3813,53 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3434
3813
  const includePaths = args.include_paths?.length ? args.include_paths : null;
3435
3814
  const excludePaths = args.exclude_paths?.length ? args.exclude_paths : null;
3436
3815
  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) {
3816
+ const caseSensitive = args.case_sensitive ?? (smartCase ? hasUppercaseAscii(q) : true);
3817
+ const ripgrepResult = runRipgrepSearch({
3818
+ query: q,
3819
+ mode,
3820
+ smartCase,
3821
+ caseSensitive,
3822
+ includePaths,
3823
+ excludePaths,
3824
+ maxResults,
3825
+ });
3826
+ if (ripgrepResult.ok) {
3827
+ logActivity("grep", {
3828
+ backend: ripgrepResult.backend,
3829
+ rg_command: ripgrepResult.rg_command,
3830
+ query: q,
3831
+ mode,
3832
+ case_sensitive: caseSensitive,
3833
+ smart_case: smartCase,
3834
+ include_paths: includePaths ?? [],
3835
+ exclude_paths: excludePaths ?? [],
3836
+ matches: ripgrepResult.matches.length,
3837
+ total_matches: ripgrepResult.total_matches,
3838
+ truncated: ripgrepResult.truncated,
3839
+ });
3840
+ return {
3841
+ content: [
3842
+ {
3843
+ type: "text",
3844
+ text: toolJson({
3845
+ ok: true,
3846
+ backend: ripgrepResult.backend,
3847
+ rg_command: ripgrepResult.rg_command,
3848
+ query: q,
3849
+ mode,
3850
+ case_sensitive: caseSensitive,
3851
+ smart_case: smartCase,
3852
+ include_paths: includePaths ?? [],
3853
+ exclude_paths: excludePaths ?? [],
3854
+ matches: ripgrepResult.matches,
3855
+ total_matches: ripgrepResult.total_matches,
3856
+ truncated: ripgrepResult.truncated,
3857
+ }),
3858
+ },
3859
+ ],
3860
+ };
3861
+ }
3862
+ if (!ripgrepResult.unavailable) {
3446
3863
  return {
3447
3864
  isError: true,
3448
3865
  content: [
@@ -3450,19 +3867,31 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3450
3867
  type: "text",
3451
3868
  text: toolJson({
3452
3869
  ok: false,
3453
- error: "Regex has no sufficiently long literal anchor for indexed narrowing. Provide literal_hint (>= 3 chars) or narrow with include_paths.",
3870
+ backend: "ripgrep",
3871
+ error: ripgrepResult.error,
3872
+ rg_command: ripgrepResult.rg_command,
3873
+ exit_status: ripgrepResult.exit_status,
3454
3874
  query: q,
3455
3875
  mode,
3456
- literal_hint: args.literal_hint,
3457
3876
  }),
3458
3877
  },
3459
3878
  ],
3460
3879
  };
3461
3880
  }
3462
- const caseSensitive = args.case_sensitive ?? (smartCase ? hasUppercaseAscii(q) : true);
3463
- let re;
3881
+ let indexedResult;
3464
3882
  try {
3465
- re = compileGrepRegex({ query: q, mode, caseSensitive });
3883
+ indexedResult = runIndexedGrepSearch({
3884
+ query: q,
3885
+ mode,
3886
+ smartCase,
3887
+ caseSensitive,
3888
+ literalHint: args.literal_hint,
3889
+ kinds,
3890
+ includePaths,
3891
+ excludePaths,
3892
+ maxResults,
3893
+ maxCandidates: args.max_candidates,
3894
+ });
3466
3895
  }
3467
3896
  catch (err) {
3468
3897
  return {
@@ -3470,114 +3899,37 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3470
3899
  content: [
3471
3900
  {
3472
3901
  type: "text",
3473
- text: toolJson({ ok: false, error: `Invalid pattern: ${String(err)}`, query: q, mode }),
3902
+ text: toolJson({
3903
+ ok: false,
3904
+ backend: "indexed_fallback",
3905
+ fallback_reason: "ripgrep_unavailable",
3906
+ ripgrep_error: ripgrepResult.error,
3907
+ ripgrep_attempts: ripgrepResult.attempts,
3908
+ error: String(err),
3909
+ query: q,
3910
+ mode,
3911
+ literal_hint: args.literal_hint,
3912
+ }),
3474
3913
  },
3475
3914
  ],
3476
3915
  };
3477
3916
  }
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
3917
  logActivity("grep", {
3918
+ backend: indexedResult.backend,
3919
+ fallback_reason: "ripgrep_unavailable",
3920
+ ripgrep_error: ripgrepResult.error,
3569
3921
  query: q,
3570
3922
  mode,
3571
3923
  case_sensitive: caseSensitive,
3572
3924
  smart_case: smartCase,
3573
- hint,
3925
+ hint: indexedResult.hint,
3574
3926
  kinds,
3575
3927
  include_paths: includePaths ?? [],
3576
3928
  exclude_paths: excludePaths ?? [],
3577
- candidates: candidates.length,
3578
- candidates_scanned: candidatesScanned,
3579
- matches: matches.length,
3580
- truncated,
3929
+ candidates: indexedResult.candidates.total,
3930
+ candidates_scanned: indexedResult.candidates.scanned,
3931
+ matches: indexedResult.matches.length,
3932
+ truncated: indexedResult.truncated,
3581
3933
  });
3582
3934
  return {
3583
3935
  content: [
@@ -3585,17 +3937,21 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3585
3937
  type: "text",
3586
3938
  text: toolJson({
3587
3939
  ok: true,
3940
+ backend: indexedResult.backend,
3941
+ fallback_reason: "ripgrep_unavailable",
3942
+ ripgrep_error: ripgrepResult.error,
3943
+ ripgrep_attempts: ripgrepResult.attempts,
3588
3944
  query: q,
3589
3945
  mode,
3590
3946
  case_sensitive: caseSensitive,
3591
3947
  smart_case: smartCase,
3592
- hint,
3948
+ hint: indexedResult.hint,
3593
3949
  kinds,
3594
3950
  include_paths: includePaths ?? [],
3595
3951
  exclude_paths: excludePaths ?? [],
3596
- candidates: { total: candidates.length, scanned: candidatesScanned },
3597
- matches,
3598
- truncated,
3952
+ candidates: indexedResult.candidates,
3953
+ matches: indexedResult.matches,
3954
+ truncated: indexedResult.truncated,
3599
3955
  }),
3600
3956
  },
3601
3957
  ],