@theokit/sdk-tools 0.18.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.19.0
4
+
5
+ ### Minor Changes
6
+
7
+ - `createSearchTextTool` gains two ADDITIVE, opt-in options (both default OFF ⇒ existing literal, project-
8
+ scoped behavior unchanged): `regex` — match `query` as a JavaScript RegExp (grep semantics; an invalid
9
+ pattern returns `{ ok: false, error: 'invalid_regex' }` before walking), and `allowAbsolute` — honor an
10
+ absolute `path` scope outside `projectRoot` (Codex read-only "reads-anywhere"; forbidden dirs still
11
+ skipped). Together they let one built-in cover both literal content search and grep-style regex search.
12
+
3
13
  ## 0.18.0
4
14
 
5
15
  ### Minor Changes
package/dist/index.cjs CHANGED
@@ -1893,27 +1893,36 @@ function createSearchTextTool(opts) {
1893
1893
  projectRoot,
1894
1894
  maxMatches = DEFAULT_MAX_MATCHES,
1895
1895
  maxFileSize = DEFAULT_MAX_FILE_SIZE,
1896
- filesystem: filesystem$1
1896
+ filesystem: filesystem$1,
1897
+ regex = false,
1898
+ allowAbsolute = false
1897
1899
  } = opts;
1900
+ const queryKind = regex ? "a JavaScript REGULAR EXPRESSION" : "LITERAL, CASE-SENSITIVE text";
1901
+ const queryMatch = regex ? "matched as a regex" : "matched as a substring, not a regex";
1898
1902
  return sdk.Tool.create({
1899
1903
  name: "search_text",
1900
- description: `Search file CONTENTS for a LITERAL, CASE-SENSITIVE query across the project tree (the query is matched as a substring, not a regex). Use search_text when you know the content; use glob_files when you know the filename shape; use read_file when you know the exact path. Skips sensitive dirs (.env/.git/node_modules/.theo), binary files, and files over 1 MB; 'path' scopes the search to a subdirectory. Returns up to ${String(maxMatches)} matches as { file, line, preview } \u2014 cite locations to the user as file:line. Returns { ok, matches } or { ok: false, error }.`,
1904
+ description: `Search file CONTENTS for ${queryKind} across the project tree (the query is ${queryMatch}). Use search_text when you know the content; use glob_files when you know the filename shape; use read_file when you know the exact path. Skips sensitive dirs (.env/.git/node_modules/.theo), binary files, and files over 1 MB; 'path' scopes the search to a subdirectory. Returns up to ${String(maxMatches)} matches as { file, line, preview } \u2014 cite locations to the user as file:line. Returns { ok, matches } or { ok: false, error }.`,
1901
1905
  inputSchema: zod.z.object({
1902
- query: zod.z.string().min(1).describe("Literal text to search for. Case-sensitive."),
1903
- path: zod.z.string().optional().describe("Optional project-relative directory to scope the search.")
1906
+ query: regex ? zod.z.string().min(1).describe("A JavaScript regular expression, e.g. 'function\\\\s+main'.") : zod.z.string().min(1).describe("Literal text to search for. Case-sensitive."),
1907
+ path: zod.z.string().optional().describe(
1908
+ "Optional directory to scope the search (project-relative; absolute when allowed)."
1909
+ )
1904
1910
  }),
1905
1911
  handler: async ({ query, path }, ctx) => {
1912
+ const built = buildMatcher(query, regex);
1913
+ if ("error" in built) return built.error;
1914
+ const matcher = built.matcher;
1906
1915
  const state = {
1907
1916
  matches: [],
1908
1917
  totalMatches: 0,
1909
1918
  truncated: false,
1910
- query,
1919
+ matcher,
1911
1920
  maxMatches,
1912
1921
  maxFileSize,
1913
1922
  projectRoot
1914
1923
  };
1915
1924
  if (filesystem$1 !== void 0) {
1916
- const scopeRel = resolveScopeRel(path, projectRoot);
1925
+ const scopeRel = resolveScopeRel(path, projectRoot, allowAbsolute);
1917
1926
  if ("error" in scopeRel) return scopeRel.error;
1918
1927
  const backend = await filesystem.resolveFilesystem(filesystem$1, ctx ?? {});
1919
1928
  await walkBackend(backend, scopeRel.rel, state, 0);
@@ -1924,7 +1933,7 @@ function createSearchTextTool(opts) {
1924
1933
  totalMatches: state.totalMatches
1925
1934
  });
1926
1935
  }
1927
- const scope = resolveSearchScope(path, projectRoot);
1936
+ const scope = resolveSearchScope(path, projectRoot, allowAbsolute);
1928
1937
  if ("error" in scope) return scope.error;
1929
1938
  await walk(scope.scopeAbs, state);
1930
1939
  return JSON.stringify({
@@ -1936,15 +1945,27 @@ function createSearchTextTool(opts) {
1936
1945
  }
1937
1946
  });
1938
1947
  }
1939
- function resolveSearchScope(path, projectRoot) {
1940
- const scopeRel = path === void 0 || path === "" || path === "." ? "." : path;
1948
+ function buildMatcher(query, regex) {
1949
+ if (!regex) return { matcher: (line) => line.includes(query) };
1950
+ try {
1951
+ const re = new RegExp(query);
1952
+ return { matcher: (line) => re.test(line) };
1953
+ } catch {
1954
+ return { error: JSON.stringify({ ok: false, error: "invalid_regex", query }) };
1955
+ }
1956
+ }
1957
+ function resolveSearchScope(path$1, projectRoot, allowAbsolute) {
1958
+ const scopeRel = path$1 === void 0 || path$1 === "" || path$1 === "." ? "." : path$1;
1959
+ if (allowAbsolute && path.isAbsolute(scopeRel)) {
1960
+ return { scopeAbs: scopeRel };
1961
+ }
1941
1962
  try {
1942
1963
  const scopeAbs = scopeRel === "." ? projectRoot : safePathJoin(projectRoot, scopeRel);
1943
1964
  assertNoSymlinkEscape(scopeAbs, projectRoot);
1944
1965
  return { scopeAbs };
1945
1966
  } catch (err) {
1946
1967
  if (err instanceof PathTraversalError || err instanceof ForbiddenPathError) {
1947
- return { error: JSON.stringify({ ok: false, error: "path_traversal", path }) };
1968
+ return { error: JSON.stringify({ ok: false, error: "path_traversal", path: path$1 }) };
1948
1969
  }
1949
1970
  throw err;
1950
1971
  }
@@ -2009,19 +2030,20 @@ async function scanFile(absPath, relPath, state) {
2009
2030
  const lines = buffer.toString("utf-8").split("\n");
2010
2031
  for (let i = 0; i < lines.length; i += 1) {
2011
2032
  const line = lines[i];
2012
- if (!line.includes(state.query)) continue;
2033
+ if (!state.matcher(line)) continue;
2013
2034
  if (!recordMatch(state, relPath, i + 1, line)) return;
2014
2035
  }
2015
2036
  }
2016
- function resolveScopeRel(path, projectRoot) {
2017
- const scopeRel = path === void 0 || path === "" || path === "." ? "" : path;
2037
+ function resolveScopeRel(path$1, projectRoot, allowAbsolute) {
2038
+ const scopeRel = path$1 === void 0 || path$1 === "" || path$1 === "." ? "" : path$1;
2018
2039
  if (scopeRel === "") return { rel: "" };
2040
+ if (allowAbsolute && path.isAbsolute(scopeRel)) return { rel: scopeRel };
2019
2041
  try {
2020
2042
  assertNoSymlinkEscape(safePathJoin(projectRoot, scopeRel), projectRoot);
2021
2043
  return { rel: scopeRel };
2022
2044
  } catch (err) {
2023
2045
  if (err instanceof PathTraversalError || err instanceof ForbiddenPathError) {
2024
- return { error: JSON.stringify({ ok: false, error: "path_traversal", path }) };
2046
+ return { error: JSON.stringify({ ok: false, error: "path_traversal", path: path$1 }) };
2025
2047
  }
2026
2048
  throw err;
2027
2049
  }
@@ -2067,7 +2089,7 @@ async function scanFileBackend(backend, relPath, size, state) {
2067
2089
  const lines = content.split("\n");
2068
2090
  for (let i = 0; i < lines.length; i += 1) {
2069
2091
  const line = lines[i];
2070
- if (!line.includes(state.query)) continue;
2092
+ if (!state.matcher(line)) continue;
2071
2093
  if (!recordMatch(state, relPath, i + 1, line)) return;
2072
2094
  }
2073
2095
  }