@coreyuan/vector-mind 1.0.30 → 1.0.35

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_PAYLOAD_GUARD_INSTRUCTIONS, BUILTIN_PLAN_LITE_INSTRUCTIONS, BUILTIN_THREAD_HANDOFF_SWITCH_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.35";
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,16 +2524,31 @@ 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
+ "",
2530
+ "Built-in payload / oversized-thread guard policy:",
2531
+ BUILTIN_PAYLOAD_GUARD_INSTRUCTIONS,
2532
+ "",
2533
+ "Built-in thread handoff / switch-gate policy:",
2534
+ BUILTIN_THREAD_HANDOFF_SWITCH_INSTRUCTIONS,
2535
+ "",
2153
2536
  "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).",
2537
+ "- 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
2538
  " - Output is compact by default. Use include_content=true only when you truly need full text (it increases tokens).",
2156
2539
  " - Tune output size with: requirements_limit/changes_limit/notes_limit, preview_chars, pending_limit/pending_offset.",
2157
2540
  " - Prefer read_memory_item(id, offset, limit) to fetch full text on demand instead of returning large content in other tool outputs.",
2541
+ "- 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
2542
  "- 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
2543
  "- 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
2544
  "- 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).",
2545
+ "- 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
2546
  "- 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.",
2547
+ "- If the current thread is heavy, recently compacted, has become slow, or has already hit a 413 / Payload Too Large style error, switch to payload guard mode: avoid unbounded shell dumps, prefer bounded MCP tools, and summarize outputs instead of pasting large raw blocks.",
2548
+ "- In payload guard mode, do not use full-repo recursive listings, whole-file dumps, or broad raw match echo unless the user explicitly requests that raw output and accepts the size risk.",
2549
+ "- Thread-switch judgment must not rely on a fixed token threshold; use observable signals plus the weight of the upcoming work. If the current thread is heavy, repeatedly compacting, slow, or has already hit a 413 / Payload Too Large style error and the next work still needs broad analysis, cross-module investigation, release validation, or other substantial continuation, pause once and ask whether to switch to a fresh thread before continuing.",
2550
+ "- If the user declines that switch, continue in the current thread and do not raise the thread-switch reminder again in the same session.",
2551
+ "- If the user accepts and the host cannot create a new top-level thread automatically, output the fixed THREAD_HANDOFF_PACK template plus the fixed copy-ready first message for the new thread instead of dumping raw history or the last N chat turns.",
2163
2552
  "- BEFORE editing code: call start_requirement(title, background) to set the active requirement.",
2164
2553
  "- 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.)",
2165
2554
  "- After major milestones/decisions: call upsert_project_summary(summary) and/or add_note(...) to persist durable context locally.",
@@ -2167,6 +2556,7 @@ function buildServerInstructions() {
2167
2556
  "- When you need full text for a specific note/summary/match: call read_memory_item(id, offset, limit) and page through it.",
2168
2557
  "- When asked to locate code (class/function/type): call query_codebase(query) instead of guessing.",
2169
2558
  "- When you need to recall relevant context from history/code/docs: call semantic_search(query, ...) instead of guessing.",
2559
+ "- 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 if the user refuses thread switching, continue in light mode without repeating the switch reminder in that same session.",
2170
2560
  "",
2171
2561
  "If tool output conflicts with assumptions, trust the tool output.",
2172
2562
  ].join("\n");
@@ -2732,7 +3122,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2732
3122
  },
2733
3123
  {
2734
3124
  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.",
3125
+ 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
3126
  inputSchema: toJsonSchemaCompat(GrepArgsSchema),
2737
3127
  },
2738
3128
  {
@@ -3434,15 +3824,53 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3434
3824
  const includePaths = args.include_paths?.length ? args.include_paths : null;
3435
3825
  const excludePaths = args.exclude_paths?.length ? args.exclude_paths : null;
3436
3826
  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) {
3827
+ const caseSensitive = args.case_sensitive ?? (smartCase ? hasUppercaseAscii(q) : true);
3828
+ const ripgrepResult = runRipgrepSearch({
3829
+ query: q,
3830
+ mode,
3831
+ smartCase,
3832
+ caseSensitive,
3833
+ includePaths,
3834
+ excludePaths,
3835
+ maxResults,
3836
+ });
3837
+ if (ripgrepResult.ok) {
3838
+ logActivity("grep", {
3839
+ backend: ripgrepResult.backend,
3840
+ rg_command: ripgrepResult.rg_command,
3841
+ query: q,
3842
+ mode,
3843
+ case_sensitive: caseSensitive,
3844
+ smart_case: smartCase,
3845
+ include_paths: includePaths ?? [],
3846
+ exclude_paths: excludePaths ?? [],
3847
+ matches: ripgrepResult.matches.length,
3848
+ total_matches: ripgrepResult.total_matches,
3849
+ truncated: ripgrepResult.truncated,
3850
+ });
3851
+ return {
3852
+ content: [
3853
+ {
3854
+ type: "text",
3855
+ text: toolJson({
3856
+ ok: true,
3857
+ backend: ripgrepResult.backend,
3858
+ rg_command: ripgrepResult.rg_command,
3859
+ query: q,
3860
+ mode,
3861
+ case_sensitive: caseSensitive,
3862
+ smart_case: smartCase,
3863
+ include_paths: includePaths ?? [],
3864
+ exclude_paths: excludePaths ?? [],
3865
+ matches: ripgrepResult.matches,
3866
+ total_matches: ripgrepResult.total_matches,
3867
+ truncated: ripgrepResult.truncated,
3868
+ }),
3869
+ },
3870
+ ],
3871
+ };
3872
+ }
3873
+ if (!ripgrepResult.unavailable) {
3446
3874
  return {
3447
3875
  isError: true,
3448
3876
  content: [
@@ -3450,19 +3878,31 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3450
3878
  type: "text",
3451
3879
  text: toolJson({
3452
3880
  ok: false,
3453
- error: "Regex has no sufficiently long literal anchor for indexed narrowing. Provide literal_hint (>= 3 chars) or narrow with include_paths.",
3881
+ backend: "ripgrep",
3882
+ error: ripgrepResult.error,
3883
+ rg_command: ripgrepResult.rg_command,
3884
+ exit_status: ripgrepResult.exit_status,
3454
3885
  query: q,
3455
3886
  mode,
3456
- literal_hint: args.literal_hint,
3457
3887
  }),
3458
3888
  },
3459
3889
  ],
3460
3890
  };
3461
3891
  }
3462
- const caseSensitive = args.case_sensitive ?? (smartCase ? hasUppercaseAscii(q) : true);
3463
- let re;
3892
+ let indexedResult;
3464
3893
  try {
3465
- re = compileGrepRegex({ query: q, mode, caseSensitive });
3894
+ indexedResult = runIndexedGrepSearch({
3895
+ query: q,
3896
+ mode,
3897
+ smartCase,
3898
+ caseSensitive,
3899
+ literalHint: args.literal_hint,
3900
+ kinds,
3901
+ includePaths,
3902
+ excludePaths,
3903
+ maxResults,
3904
+ maxCandidates: args.max_candidates,
3905
+ });
3466
3906
  }
3467
3907
  catch (err) {
3468
3908
  return {
@@ -3470,114 +3910,37 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3470
3910
  content: [
3471
3911
  {
3472
3912
  type: "text",
3473
- text: toolJson({ ok: false, error: `Invalid pattern: ${String(err)}`, query: q, mode }),
3913
+ text: toolJson({
3914
+ ok: false,
3915
+ backend: "indexed_fallback",
3916
+ fallback_reason: "ripgrep_unavailable",
3917
+ ripgrep_error: ripgrepResult.error,
3918
+ ripgrep_attempts: ripgrepResult.attempts,
3919
+ error: String(err),
3920
+ query: q,
3921
+ mode,
3922
+ literal_hint: args.literal_hint,
3923
+ }),
3474
3924
  },
3475
3925
  ],
3476
3926
  };
3477
3927
  }
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
3928
  logActivity("grep", {
3929
+ backend: indexedResult.backend,
3930
+ fallback_reason: "ripgrep_unavailable",
3931
+ ripgrep_error: ripgrepResult.error,
3569
3932
  query: q,
3570
3933
  mode,
3571
3934
  case_sensitive: caseSensitive,
3572
3935
  smart_case: smartCase,
3573
- hint,
3936
+ hint: indexedResult.hint,
3574
3937
  kinds,
3575
3938
  include_paths: includePaths ?? [],
3576
3939
  exclude_paths: excludePaths ?? [],
3577
- candidates: candidates.length,
3578
- candidates_scanned: candidatesScanned,
3579
- matches: matches.length,
3580
- truncated,
3940
+ candidates: indexedResult.candidates.total,
3941
+ candidates_scanned: indexedResult.candidates.scanned,
3942
+ matches: indexedResult.matches.length,
3943
+ truncated: indexedResult.truncated,
3581
3944
  });
3582
3945
  return {
3583
3946
  content: [
@@ -3585,17 +3948,21 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3585
3948
  type: "text",
3586
3949
  text: toolJson({
3587
3950
  ok: true,
3951
+ backend: indexedResult.backend,
3952
+ fallback_reason: "ripgrep_unavailable",
3953
+ ripgrep_error: ripgrepResult.error,
3954
+ ripgrep_attempts: ripgrepResult.attempts,
3588
3955
  query: q,
3589
3956
  mode,
3590
3957
  case_sensitive: caseSensitive,
3591
3958
  smart_case: smartCase,
3592
- hint,
3959
+ hint: indexedResult.hint,
3593
3960
  kinds,
3594
3961
  include_paths: includePaths ?? [],
3595
3962
  exclude_paths: excludePaths ?? [],
3596
- candidates: { total: candidates.length, scanned: candidatesScanned },
3597
- matches,
3598
- truncated,
3963
+ candidates: indexedResult.candidates,
3964
+ matches: indexedResult.matches,
3965
+ truncated: indexedResult.truncated,
3599
3966
  }),
3600
3967
  },
3601
3968
  ],