@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/dist/index.d.cts CHANGED
@@ -780,6 +780,12 @@ interface CreateSearchTextToolOptions {
780
780
  /** Optional injected filesystem (`@theokit/sdk/filesystem`) — when provided, the recursive walk reads
781
781
  * through the backend (surface-agnostic); omitted ⇒ the local `readdir`/`readFile` walk (unchanged). */
782
782
  filesystem?: FilesystemProvider;
783
+ /** M17 — treat `query` as a JavaScript RegExp instead of a literal substring (grep semantics). An
784
+ * invalid pattern returns `{ ok: false, error: 'invalid_regex' }`. Default `false` ⇒ literal (unchanged). */
785
+ regex?: boolean;
786
+ /** M17 — opt-in "reads-anywhere": honor an ABSOLUTE `path` scope outside `projectRoot` (Codex read-only
787
+ * sandbox). Forbidden dirs are still skipped. Default `false` ⇒ absolute scope rejected (unchanged). */
788
+ allowAbsolute?: boolean;
783
789
  }
784
790
  declare function createSearchTextTool(opts: CreateSearchTextToolOptions): CustomTool;
785
791
 
package/dist/index.d.ts CHANGED
@@ -780,6 +780,12 @@ interface CreateSearchTextToolOptions {
780
780
  /** Optional injected filesystem (`@theokit/sdk/filesystem`) — when provided, the recursive walk reads
781
781
  * through the backend (surface-agnostic); omitted ⇒ the local `readdir`/`readFile` walk (unchanged). */
782
782
  filesystem?: FilesystemProvider;
783
+ /** M17 — treat `query` as a JavaScript RegExp instead of a literal substring (grep semantics). An
784
+ * invalid pattern returns `{ ok: false, error: 'invalid_regex' }`. Default `false` ⇒ literal (unchanged). */
785
+ regex?: boolean;
786
+ /** M17 — opt-in "reads-anywhere": honor an ABSOLUTE `path` scope outside `projectRoot` (Codex read-only
787
+ * sandbox). Forbidden dirs are still skipped. Default `false` ⇒ absolute scope rejected (unchanged). */
788
+ allowAbsolute?: boolean;
783
789
  }
784
790
  declare function createSearchTextTool(opts: CreateSearchTextToolOptions): CustomTool;
785
791
 
package/dist/index.js CHANGED
@@ -1891,27 +1891,36 @@ function createSearchTextTool(opts) {
1891
1891
  projectRoot,
1892
1892
  maxMatches = DEFAULT_MAX_MATCHES,
1893
1893
  maxFileSize = DEFAULT_MAX_FILE_SIZE,
1894
- filesystem
1894
+ filesystem,
1895
+ regex = false,
1896
+ allowAbsolute = false
1895
1897
  } = opts;
1898
+ const queryKind = regex ? "a JavaScript REGULAR EXPRESSION" : "LITERAL, CASE-SENSITIVE text";
1899
+ const queryMatch = regex ? "matched as a regex" : "matched as a substring, not a regex";
1896
1900
  return Tool.create({
1897
1901
  name: "search_text",
1898
- 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 }.`,
1902
+ 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 }.`,
1899
1903
  inputSchema: z.object({
1900
- query: z.string().min(1).describe("Literal text to search for. Case-sensitive."),
1901
- path: z.string().optional().describe("Optional project-relative directory to scope the search.")
1904
+ query: regex ? z.string().min(1).describe("A JavaScript regular expression, e.g. 'function\\\\s+main'.") : z.string().min(1).describe("Literal text to search for. Case-sensitive."),
1905
+ path: z.string().optional().describe(
1906
+ "Optional directory to scope the search (project-relative; absolute when allowed)."
1907
+ )
1902
1908
  }),
1903
1909
  handler: async ({ query, path }, ctx) => {
1910
+ const built = buildMatcher(query, regex);
1911
+ if ("error" in built) return built.error;
1912
+ const matcher = built.matcher;
1904
1913
  const state = {
1905
1914
  matches: [],
1906
1915
  totalMatches: 0,
1907
1916
  truncated: false,
1908
- query,
1917
+ matcher,
1909
1918
  maxMatches,
1910
1919
  maxFileSize,
1911
1920
  projectRoot
1912
1921
  };
1913
1922
  if (filesystem !== void 0) {
1914
- const scopeRel = resolveScopeRel(path, projectRoot);
1923
+ const scopeRel = resolveScopeRel(path, projectRoot, allowAbsolute);
1915
1924
  if ("error" in scopeRel) return scopeRel.error;
1916
1925
  const backend = await resolveFilesystem(filesystem, ctx ?? {});
1917
1926
  await walkBackend(backend, scopeRel.rel, state, 0);
@@ -1922,7 +1931,7 @@ function createSearchTextTool(opts) {
1922
1931
  totalMatches: state.totalMatches
1923
1932
  });
1924
1933
  }
1925
- const scope = resolveSearchScope(path, projectRoot);
1934
+ const scope = resolveSearchScope(path, projectRoot, allowAbsolute);
1926
1935
  if ("error" in scope) return scope.error;
1927
1936
  await walk(scope.scopeAbs, state);
1928
1937
  return JSON.stringify({
@@ -1934,8 +1943,20 @@ function createSearchTextTool(opts) {
1934
1943
  }
1935
1944
  });
1936
1945
  }
1937
- function resolveSearchScope(path, projectRoot) {
1946
+ function buildMatcher(query, regex) {
1947
+ if (!regex) return { matcher: (line) => line.includes(query) };
1948
+ try {
1949
+ const re = new RegExp(query);
1950
+ return { matcher: (line) => re.test(line) };
1951
+ } catch {
1952
+ return { error: JSON.stringify({ ok: false, error: "invalid_regex", query }) };
1953
+ }
1954
+ }
1955
+ function resolveSearchScope(path, projectRoot, allowAbsolute) {
1938
1956
  const scopeRel = path === void 0 || path === "" || path === "." ? "." : path;
1957
+ if (allowAbsolute && isAbsolute(scopeRel)) {
1958
+ return { scopeAbs: scopeRel };
1959
+ }
1939
1960
  try {
1940
1961
  const scopeAbs = scopeRel === "." ? projectRoot : safePathJoin(projectRoot, scopeRel);
1941
1962
  assertNoSymlinkEscape(scopeAbs, projectRoot);
@@ -2007,13 +2028,14 @@ async function scanFile(absPath, relPath, state) {
2007
2028
  const lines = buffer.toString("utf-8").split("\n");
2008
2029
  for (let i = 0; i < lines.length; i += 1) {
2009
2030
  const line = lines[i];
2010
- if (!line.includes(state.query)) continue;
2031
+ if (!state.matcher(line)) continue;
2011
2032
  if (!recordMatch(state, relPath, i + 1, line)) return;
2012
2033
  }
2013
2034
  }
2014
- function resolveScopeRel(path, projectRoot) {
2035
+ function resolveScopeRel(path, projectRoot, allowAbsolute) {
2015
2036
  const scopeRel = path === void 0 || path === "" || path === "." ? "" : path;
2016
2037
  if (scopeRel === "") return { rel: "" };
2038
+ if (allowAbsolute && isAbsolute(scopeRel)) return { rel: scopeRel };
2017
2039
  try {
2018
2040
  assertNoSymlinkEscape(safePathJoin(projectRoot, scopeRel), projectRoot);
2019
2041
  return { rel: scopeRel };
@@ -2065,7 +2087,7 @@ async function scanFileBackend(backend, relPath, size, state) {
2065
2087
  const lines = content.split("\n");
2066
2088
  for (let i = 0; i < lines.length; i += 1) {
2067
2089
  const line = lines[i];
2068
- if (!line.includes(state.query)) continue;
2090
+ if (!state.matcher(line)) continue;
2069
2091
  if (!recordMatch(state, relPath, i + 1, line)) return;
2070
2092
  }
2071
2093
  }