@theokit/sdk-tools 0.17.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 +21 -0
- package/dist/index.cjs +90 -34
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +18 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +85 -29
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
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
|
+
|
|
13
|
+
## 0.18.0
|
|
14
|
+
|
|
15
|
+
### Minor Changes
|
|
16
|
+
|
|
17
|
+
- `createReadFileTool` gains three ADDITIVE, opt-in Codex-grade capabilities (all default OFF, so existing
|
|
18
|
+
consumers are byte-identical): `lineNumbers` (render a `cat -n` `<n>\t<line>` view so the model can cite/
|
|
19
|
+
edit by line), `offset`/`limit` input params (page through a large file), and `allowAbsolute` (honor an
|
|
20
|
+
absolute path outside `projectRoot` — the Codex read-only "reads-anywhere" sandbox). Security: with
|
|
21
|
+
`allowAbsolute`, the secret guard now blocks `.env`/`.git`/`node_modules`/`.theo` at ANY path depth (not
|
|
22
|
+
just the project-relative first segment), closing an absolute-path exfiltration hole. Opt-in only.
|
|
23
|
+
|
|
3
24
|
## 0.17.0
|
|
4
25
|
|
|
5
26
|
### Minor Changes
|
package/dist/index.cjs
CHANGED
|
@@ -1579,23 +1579,53 @@ function createQuestionTool(opts) {
|
|
|
1579
1579
|
}
|
|
1580
1580
|
var MAX_FILE_SIZE = 5 * 1024 * 1024;
|
|
1581
1581
|
var BINARY_PROBE_BYTES = 8 * 1024;
|
|
1582
|
+
var SENSITIVE_SEGMENTS = /* @__PURE__ */ new Set([".env", ".git", "node_modules", ".theo"]);
|
|
1583
|
+
function isForbiddenAtAnyDepth(path) {
|
|
1584
|
+
const segs = path.replace(/\\/g, "/").split("/").filter(Boolean);
|
|
1585
|
+
return segs.some((s) => {
|
|
1586
|
+
if (s === ".env.example") return false;
|
|
1587
|
+
return SENSITIVE_SEGMENTS.has(s) || /^\.env\./.test(s);
|
|
1588
|
+
});
|
|
1589
|
+
}
|
|
1590
|
+
function forbiddenReadError(path$1, allowAbsolute) {
|
|
1591
|
+
if (isForbiddenPath(path$1)) {
|
|
1592
|
+
return JSON.stringify({ ok: false, error: "forbidden_path", path: path$1 });
|
|
1593
|
+
}
|
|
1594
|
+
if (allowAbsolute && path.isAbsolute(path$1) && isForbiddenAtAnyDepth(path$1)) {
|
|
1595
|
+
return JSON.stringify({ ok: false, error: "forbidden_path", path: path$1 });
|
|
1596
|
+
}
|
|
1597
|
+
return null;
|
|
1598
|
+
}
|
|
1599
|
+
function renderView(content, opts) {
|
|
1600
|
+
const paginated = opts.offset !== void 0 || opts.limit !== void 0;
|
|
1601
|
+
if (!opts.lineNumbers && !paginated) return content;
|
|
1602
|
+
const lines = content.split("\n");
|
|
1603
|
+
const start = Math.max(1, opts.offset ?? 1);
|
|
1604
|
+
const end = opts.limit !== void 0 ? Math.min(lines.length, start - 1 + opts.limit) : lines.length;
|
|
1605
|
+
const slice = lines.slice(start - 1, end);
|
|
1606
|
+
return opts.lineNumbers ? slice.map((l, i) => `${start + i} ${l}`).join("\n") : slice.join("\n");
|
|
1607
|
+
}
|
|
1582
1608
|
function createReadFileTool(opts) {
|
|
1583
|
-
const { projectRoot, readTracker, filesystem: filesystem$1 } = opts;
|
|
1609
|
+
const { projectRoot, readTracker, filesystem: filesystem$1, lineNumbers, allowAbsolute } = opts;
|
|
1610
|
+
const numbered = lineNumbers === true ? " Returns a cat -n numbered view (`<n>\\t<line>`)." : "";
|
|
1611
|
+
const abs = allowAbsolute === true ? " Absolute paths outside the project are honored." : "";
|
|
1584
1612
|
return sdk.Tool.create({
|
|
1585
1613
|
name: "read_file",
|
|
1586
|
-
description: "Read a
|
|
1614
|
+
description: "Read a text file as UTF-8. ALWAYS read a file before you edit it (edit_file) or overwrite it (write_file), so your old_string / new content matches the real bytes exactly." + numbered + abs + " By default returns the whole file; use the optional offset (1-based first line) + limit to page through a large file, or search_text to locate a symbol. Refuses sensitive files (.env, .git/, node_modules/, .theo/, lock files) and binary files (null byte in the first 8 KB); caps at 5 MB. Returns { ok, content, size } or { ok: false, error }.",
|
|
1587
1615
|
inputSchema: zod.z.object({
|
|
1588
|
-
path: zod.z.string().min(1).describe("
|
|
1616
|
+
path: zod.z.string().min(1).describe("File path (project-relative; absolute when allowed)."),
|
|
1617
|
+
offset: zod.z.number().int().min(1).optional().describe("1-based first line to read (default 1)."),
|
|
1618
|
+
limit: zod.z.number().int().min(1).optional().describe("Max number of lines to read (default: all).")
|
|
1589
1619
|
}),
|
|
1590
|
-
handler: async ({ path }, ctx) => {
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
}
|
|
1620
|
+
handler: async ({ path, offset, limit }, ctx) => {
|
|
1621
|
+
const forbidden = forbiddenReadError(path, allowAbsolute === true);
|
|
1622
|
+
if (forbidden !== null) return forbidden;
|
|
1623
|
+
const view = { lineNumbers, offset, limit };
|
|
1594
1624
|
if (filesystem$1) {
|
|
1595
1625
|
const backend = await filesystem.resolveFilesystem(filesystem$1, ctx ?? {});
|
|
1596
|
-
return readViaBackend(backend, path, (mtimeMs) => readTracker?.record(path, mtimeMs));
|
|
1626
|
+
return readViaBackend(backend, path, view, (mtimeMs) => readTracker?.record(path, mtimeMs));
|
|
1597
1627
|
}
|
|
1598
|
-
const boundary = resolveBoundary(path, projectRoot);
|
|
1628
|
+
const boundary = resolveBoundary(path, projectRoot, allowAbsolute === true);
|
|
1599
1629
|
if ("error" in boundary) return boundary.error;
|
|
1600
1630
|
const opened = await openHandleSafe(boundary.absolutePath, path);
|
|
1601
1631
|
if ("error" in opened) return opened.error;
|
|
@@ -1603,6 +1633,7 @@ function createReadFileTool(opts) {
|
|
|
1603
1633
|
return await readContent(
|
|
1604
1634
|
opened.handle,
|
|
1605
1635
|
path,
|
|
1636
|
+
view,
|
|
1606
1637
|
(mtimeMs) => readTracker?.record(path, mtimeMs)
|
|
1607
1638
|
);
|
|
1608
1639
|
} finally {
|
|
@@ -1611,7 +1642,7 @@ function createReadFileTool(opts) {
|
|
|
1611
1642
|
}
|
|
1612
1643
|
});
|
|
1613
1644
|
}
|
|
1614
|
-
async function readViaBackend(backend, path, onRead) {
|
|
1645
|
+
async function readViaBackend(backend, path, view, onRead) {
|
|
1615
1646
|
try {
|
|
1616
1647
|
const stat2 = await backend.stat(path);
|
|
1617
1648
|
if (stat2.size > MAX_FILE_SIZE) {
|
|
@@ -1623,12 +1654,12 @@ async function readViaBackend(backend, path, onRead) {
|
|
|
1623
1654
|
limit: MAX_FILE_SIZE
|
|
1624
1655
|
});
|
|
1625
1656
|
}
|
|
1626
|
-
const
|
|
1627
|
-
if (
|
|
1657
|
+
const raw = await backend.readFile(path);
|
|
1658
|
+
if (raw.includes("\0")) {
|
|
1628
1659
|
return JSON.stringify({ ok: false, error: "binary_file", path, size: stat2.size });
|
|
1629
1660
|
}
|
|
1630
1661
|
onRead?.(stat2.mtimeMs);
|
|
1631
|
-
return JSON.stringify({ ok: true, content, size: stat2.size });
|
|
1662
|
+
return JSON.stringify({ ok: true, content: renderView(raw, view), size: stat2.size });
|
|
1632
1663
|
} catch (err) {
|
|
1633
1664
|
if (err instanceof filesystem.FileNotFoundError) {
|
|
1634
1665
|
return JSON.stringify({ ok: false, error: "not_found", path });
|
|
@@ -1639,14 +1670,17 @@ async function readViaBackend(backend, path, onRead) {
|
|
|
1639
1670
|
throw err;
|
|
1640
1671
|
}
|
|
1641
1672
|
}
|
|
1642
|
-
function resolveBoundary(path, projectRoot) {
|
|
1673
|
+
function resolveBoundary(path$1, projectRoot, allowAbsolute) {
|
|
1674
|
+
if (allowAbsolute && path.isAbsolute(path$1)) {
|
|
1675
|
+
return { absolutePath: path$1 };
|
|
1676
|
+
}
|
|
1643
1677
|
try {
|
|
1644
|
-
const absolutePath = safePathJoin(projectRoot, path);
|
|
1678
|
+
const absolutePath = safePathJoin(projectRoot, path$1);
|
|
1645
1679
|
assertNoSymlinkEscape(absolutePath, projectRoot);
|
|
1646
1680
|
return { absolutePath };
|
|
1647
1681
|
} catch (err) {
|
|
1648
1682
|
if (err instanceof PathTraversalError || err instanceof ForbiddenPathError) {
|
|
1649
|
-
return { error: JSON.stringify({ ok: false, error: "path_traversal", path }) };
|
|
1683
|
+
return { error: JSON.stringify({ ok: false, error: "path_traversal", path: path$1 }) };
|
|
1650
1684
|
}
|
|
1651
1685
|
throw err;
|
|
1652
1686
|
}
|
|
@@ -1663,7 +1697,7 @@ async function openHandleSafe(absolutePath, path) {
|
|
|
1663
1697
|
throw err;
|
|
1664
1698
|
}
|
|
1665
1699
|
}
|
|
1666
|
-
async function readContent(handle, path, onRead) {
|
|
1700
|
+
async function readContent(handle, path, view, onRead) {
|
|
1667
1701
|
const stat2 = await handle.stat();
|
|
1668
1702
|
if (stat2.size > MAX_FILE_SIZE) {
|
|
1669
1703
|
return JSON.stringify({
|
|
@@ -1677,9 +1711,9 @@ async function readContent(handle, path, onRead) {
|
|
|
1677
1711
|
if (await isBinaryProbe(handle, Number(stat2.size))) {
|
|
1678
1712
|
return JSON.stringify({ ok: false, error: "binary_file", path, size: stat2.size });
|
|
1679
1713
|
}
|
|
1680
|
-
const
|
|
1714
|
+
const raw = await handle.readFile({ encoding: "utf-8" });
|
|
1681
1715
|
onRead?.(stat2.mtimeMs);
|
|
1682
|
-
return JSON.stringify({ ok: true, content, size: stat2.size });
|
|
1716
|
+
return JSON.stringify({ ok: true, content: renderView(raw, view), size: stat2.size });
|
|
1683
1717
|
}
|
|
1684
1718
|
async function isBinaryProbe(handle, size) {
|
|
1685
1719
|
const probeLen = Math.min(BINARY_PROBE_BYTES, size);
|
|
@@ -1859,27 +1893,36 @@ function createSearchTextTool(opts) {
|
|
|
1859
1893
|
projectRoot,
|
|
1860
1894
|
maxMatches = DEFAULT_MAX_MATCHES,
|
|
1861
1895
|
maxFileSize = DEFAULT_MAX_FILE_SIZE,
|
|
1862
|
-
filesystem: filesystem$1
|
|
1896
|
+
filesystem: filesystem$1,
|
|
1897
|
+
regex = false,
|
|
1898
|
+
allowAbsolute = false
|
|
1863
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";
|
|
1864
1902
|
return sdk.Tool.create({
|
|
1865
1903
|
name: "search_text",
|
|
1866
|
-
description: `Search file CONTENTS for
|
|
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 }.`,
|
|
1867
1905
|
inputSchema: zod.z.object({
|
|
1868
|
-
query: zod.z.string().min(1).describe("Literal text to search for. Case-sensitive."),
|
|
1869
|
-
path: zod.z.string().optional().describe(
|
|
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
|
+
)
|
|
1870
1910
|
}),
|
|
1871
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;
|
|
1872
1915
|
const state = {
|
|
1873
1916
|
matches: [],
|
|
1874
1917
|
totalMatches: 0,
|
|
1875
1918
|
truncated: false,
|
|
1876
|
-
|
|
1919
|
+
matcher,
|
|
1877
1920
|
maxMatches,
|
|
1878
1921
|
maxFileSize,
|
|
1879
1922
|
projectRoot
|
|
1880
1923
|
};
|
|
1881
1924
|
if (filesystem$1 !== void 0) {
|
|
1882
|
-
const scopeRel = resolveScopeRel(path, projectRoot);
|
|
1925
|
+
const scopeRel = resolveScopeRel(path, projectRoot, allowAbsolute);
|
|
1883
1926
|
if ("error" in scopeRel) return scopeRel.error;
|
|
1884
1927
|
const backend = await filesystem.resolveFilesystem(filesystem$1, ctx ?? {});
|
|
1885
1928
|
await walkBackend(backend, scopeRel.rel, state, 0);
|
|
@@ -1890,7 +1933,7 @@ function createSearchTextTool(opts) {
|
|
|
1890
1933
|
totalMatches: state.totalMatches
|
|
1891
1934
|
});
|
|
1892
1935
|
}
|
|
1893
|
-
const scope = resolveSearchScope(path, projectRoot);
|
|
1936
|
+
const scope = resolveSearchScope(path, projectRoot, allowAbsolute);
|
|
1894
1937
|
if ("error" in scope) return scope.error;
|
|
1895
1938
|
await walk(scope.scopeAbs, state);
|
|
1896
1939
|
return JSON.stringify({
|
|
@@ -1902,15 +1945,27 @@ function createSearchTextTool(opts) {
|
|
|
1902
1945
|
}
|
|
1903
1946
|
});
|
|
1904
1947
|
}
|
|
1905
|
-
function
|
|
1906
|
-
|
|
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
|
+
}
|
|
1907
1962
|
try {
|
|
1908
1963
|
const scopeAbs = scopeRel === "." ? projectRoot : safePathJoin(projectRoot, scopeRel);
|
|
1909
1964
|
assertNoSymlinkEscape(scopeAbs, projectRoot);
|
|
1910
1965
|
return { scopeAbs };
|
|
1911
1966
|
} catch (err) {
|
|
1912
1967
|
if (err instanceof PathTraversalError || err instanceof ForbiddenPathError) {
|
|
1913
|
-
return { error: JSON.stringify({ ok: false, error: "path_traversal", path }) };
|
|
1968
|
+
return { error: JSON.stringify({ ok: false, error: "path_traversal", path: path$1 }) };
|
|
1914
1969
|
}
|
|
1915
1970
|
throw err;
|
|
1916
1971
|
}
|
|
@@ -1975,19 +2030,20 @@ async function scanFile(absPath, relPath, state) {
|
|
|
1975
2030
|
const lines = buffer.toString("utf-8").split("\n");
|
|
1976
2031
|
for (let i = 0; i < lines.length; i += 1) {
|
|
1977
2032
|
const line = lines[i];
|
|
1978
|
-
if (!
|
|
2033
|
+
if (!state.matcher(line)) continue;
|
|
1979
2034
|
if (!recordMatch(state, relPath, i + 1, line)) return;
|
|
1980
2035
|
}
|
|
1981
2036
|
}
|
|
1982
|
-
function resolveScopeRel(path, projectRoot) {
|
|
1983
|
-
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;
|
|
1984
2039
|
if (scopeRel === "") return { rel: "" };
|
|
2040
|
+
if (allowAbsolute && path.isAbsolute(scopeRel)) return { rel: scopeRel };
|
|
1985
2041
|
try {
|
|
1986
2042
|
assertNoSymlinkEscape(safePathJoin(projectRoot, scopeRel), projectRoot);
|
|
1987
2043
|
return { rel: scopeRel };
|
|
1988
2044
|
} catch (err) {
|
|
1989
2045
|
if (err instanceof PathTraversalError || err instanceof ForbiddenPathError) {
|
|
1990
|
-
return { error: JSON.stringify({ ok: false, error: "path_traversal", path }) };
|
|
2046
|
+
return { error: JSON.stringify({ ok: false, error: "path_traversal", path: path$1 }) };
|
|
1991
2047
|
}
|
|
1992
2048
|
throw err;
|
|
1993
2049
|
}
|
|
@@ -2033,7 +2089,7 @@ async function scanFileBackend(backend, relPath, size, state) {
|
|
|
2033
2089
|
const lines = content.split("\n");
|
|
2034
2090
|
for (let i = 0; i < lines.length; i += 1) {
|
|
2035
2091
|
const line = lines[i];
|
|
2036
|
-
if (!
|
|
2092
|
+
if (!state.matcher(line)) continue;
|
|
2037
2093
|
if (!recordMatch(state, relPath, i + 1, line)) return;
|
|
2038
2094
|
}
|
|
2039
2095
|
}
|