@theokit/sdk-tools 0.17.0 → 0.18.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 +11 -0
- package/dist/index.cjs +53 -19
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +12 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +52 -18
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -671,6 +671,18 @@ interface CreateReadFileToolOptions {
|
|
|
671
671
|
* identical current behavior (multi-tenant / per-request roots).
|
|
672
672
|
*/
|
|
673
673
|
filesystem?: FilesystemProvider;
|
|
674
|
+
/**
|
|
675
|
+
* M17 — render a `cat -n`-style numbered view (`<n>\t<line>`) instead of raw content, so the model can
|
|
676
|
+
* cite / edit by line number (Codex / Claude-Code read idiom). Default `false` ⇒ raw content (unchanged).
|
|
677
|
+
*/
|
|
678
|
+
lineNumbers?: boolean;
|
|
679
|
+
/**
|
|
680
|
+
* M17 — opt-in "reads-anywhere" (Codex read-only sandbox): honor an ABSOLUTE `path` outside `projectRoot`
|
|
681
|
+
* instead of rejecting it as traversal. The `isForbiddenPath` secret guard STILL fires first, so `.env`/
|
|
682
|
+
* `.git`/lock files remain blocked. Default `false` ⇒ absolute paths rejected (unchanged). Opt-in only —
|
|
683
|
+
* this widens the read boundary to the whole filesystem; enable only for a trusted local agent.
|
|
684
|
+
*/
|
|
685
|
+
allowAbsolute?: boolean;
|
|
674
686
|
}
|
|
675
687
|
declare function createReadFileTool(opts: CreateReadFileToolOptions): CustomTool;
|
|
676
688
|
|
package/dist/index.d.ts
CHANGED
|
@@ -671,6 +671,18 @@ interface CreateReadFileToolOptions {
|
|
|
671
671
|
* identical current behavior (multi-tenant / per-request roots).
|
|
672
672
|
*/
|
|
673
673
|
filesystem?: FilesystemProvider;
|
|
674
|
+
/**
|
|
675
|
+
* M17 — render a `cat -n`-style numbered view (`<n>\t<line>`) instead of raw content, so the model can
|
|
676
|
+
* cite / edit by line number (Codex / Claude-Code read idiom). Default `false` ⇒ raw content (unchanged).
|
|
677
|
+
*/
|
|
678
|
+
lineNumbers?: boolean;
|
|
679
|
+
/**
|
|
680
|
+
* M17 — opt-in "reads-anywhere" (Codex read-only sandbox): honor an ABSOLUTE `path` outside `projectRoot`
|
|
681
|
+
* instead of rejecting it as traversal. The `isForbiddenPath` secret guard STILL fires first, so `.env`/
|
|
682
|
+
* `.git`/lock files remain blocked. Default `false` ⇒ absolute paths rejected (unchanged). Opt-in only —
|
|
683
|
+
* this widens the read boundary to the whole filesystem; enable only for a trusted local agent.
|
|
684
|
+
*/
|
|
685
|
+
allowAbsolute?: boolean;
|
|
674
686
|
}
|
|
675
687
|
declare function createReadFileTool(opts: CreateReadFileToolOptions): CustomTool;
|
|
676
688
|
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFile, copyFile, mkdir, writeFile, readdir, open, stat } from 'fs/promises';
|
|
2
|
-
import { dirname, join, relative, resolve, sep } from 'path';
|
|
2
|
+
import { dirname, join, relative, isAbsolute, resolve, sep } from 'path';
|
|
3
3
|
import { Tool, ConfigurationError } from '@theokit/sdk';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import { existsSync, statSync, mkdirSync, writeFileSync, realpathSync, readFileSync, lstatSync, readlinkSync, readdirSync } from 'fs';
|
|
@@ -1577,23 +1577,53 @@ function createQuestionTool(opts) {
|
|
|
1577
1577
|
}
|
|
1578
1578
|
var MAX_FILE_SIZE = 5 * 1024 * 1024;
|
|
1579
1579
|
var BINARY_PROBE_BYTES = 8 * 1024;
|
|
1580
|
+
var SENSITIVE_SEGMENTS = /* @__PURE__ */ new Set([".env", ".git", "node_modules", ".theo"]);
|
|
1581
|
+
function isForbiddenAtAnyDepth(path) {
|
|
1582
|
+
const segs = path.replace(/\\/g, "/").split("/").filter(Boolean);
|
|
1583
|
+
return segs.some((s) => {
|
|
1584
|
+
if (s === ".env.example") return false;
|
|
1585
|
+
return SENSITIVE_SEGMENTS.has(s) || /^\.env\./.test(s);
|
|
1586
|
+
});
|
|
1587
|
+
}
|
|
1588
|
+
function forbiddenReadError(path, allowAbsolute) {
|
|
1589
|
+
if (isForbiddenPath(path)) {
|
|
1590
|
+
return JSON.stringify({ ok: false, error: "forbidden_path", path });
|
|
1591
|
+
}
|
|
1592
|
+
if (allowAbsolute && isAbsolute(path) && isForbiddenAtAnyDepth(path)) {
|
|
1593
|
+
return JSON.stringify({ ok: false, error: "forbidden_path", path });
|
|
1594
|
+
}
|
|
1595
|
+
return null;
|
|
1596
|
+
}
|
|
1597
|
+
function renderView(content, opts) {
|
|
1598
|
+
const paginated = opts.offset !== void 0 || opts.limit !== void 0;
|
|
1599
|
+
if (!opts.lineNumbers && !paginated) return content;
|
|
1600
|
+
const lines = content.split("\n");
|
|
1601
|
+
const start = Math.max(1, opts.offset ?? 1);
|
|
1602
|
+
const end = opts.limit !== void 0 ? Math.min(lines.length, start - 1 + opts.limit) : lines.length;
|
|
1603
|
+
const slice = lines.slice(start - 1, end);
|
|
1604
|
+
return opts.lineNumbers ? slice.map((l, i) => `${start + i} ${l}`).join("\n") : slice.join("\n");
|
|
1605
|
+
}
|
|
1580
1606
|
function createReadFileTool(opts) {
|
|
1581
|
-
const { projectRoot, readTracker, filesystem } = opts;
|
|
1607
|
+
const { projectRoot, readTracker, filesystem, lineNumbers, allowAbsolute } = opts;
|
|
1608
|
+
const numbered = lineNumbers === true ? " Returns a cat -n numbered view (`<n>\\t<line>`)." : "";
|
|
1609
|
+
const abs = allowAbsolute === true ? " Absolute paths outside the project are honored." : "";
|
|
1582
1610
|
return Tool.create({
|
|
1583
1611
|
name: "read_file",
|
|
1584
|
-
description: "Read a
|
|
1612
|
+
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 }.",
|
|
1585
1613
|
inputSchema: z.object({
|
|
1586
|
-
path: z.string().min(1).describe("
|
|
1614
|
+
path: z.string().min(1).describe("File path (project-relative; absolute when allowed)."),
|
|
1615
|
+
offset: z.number().int().min(1).optional().describe("1-based first line to read (default 1)."),
|
|
1616
|
+
limit: z.number().int().min(1).optional().describe("Max number of lines to read (default: all).")
|
|
1587
1617
|
}),
|
|
1588
|
-
handler: async ({ path }, ctx) => {
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
}
|
|
1618
|
+
handler: async ({ path, offset, limit }, ctx) => {
|
|
1619
|
+
const forbidden = forbiddenReadError(path, allowAbsolute === true);
|
|
1620
|
+
if (forbidden !== null) return forbidden;
|
|
1621
|
+
const view = { lineNumbers, offset, limit };
|
|
1592
1622
|
if (filesystem) {
|
|
1593
1623
|
const backend = await resolveFilesystem(filesystem, ctx ?? {});
|
|
1594
|
-
return readViaBackend(backend, path, (mtimeMs) => readTracker?.record(path, mtimeMs));
|
|
1624
|
+
return readViaBackend(backend, path, view, (mtimeMs) => readTracker?.record(path, mtimeMs));
|
|
1595
1625
|
}
|
|
1596
|
-
const boundary = resolveBoundary(path, projectRoot);
|
|
1626
|
+
const boundary = resolveBoundary(path, projectRoot, allowAbsolute === true);
|
|
1597
1627
|
if ("error" in boundary) return boundary.error;
|
|
1598
1628
|
const opened = await openHandleSafe(boundary.absolutePath, path);
|
|
1599
1629
|
if ("error" in opened) return opened.error;
|
|
@@ -1601,6 +1631,7 @@ function createReadFileTool(opts) {
|
|
|
1601
1631
|
return await readContent(
|
|
1602
1632
|
opened.handle,
|
|
1603
1633
|
path,
|
|
1634
|
+
view,
|
|
1604
1635
|
(mtimeMs) => readTracker?.record(path, mtimeMs)
|
|
1605
1636
|
);
|
|
1606
1637
|
} finally {
|
|
@@ -1609,7 +1640,7 @@ function createReadFileTool(opts) {
|
|
|
1609
1640
|
}
|
|
1610
1641
|
});
|
|
1611
1642
|
}
|
|
1612
|
-
async function readViaBackend(backend, path, onRead) {
|
|
1643
|
+
async function readViaBackend(backend, path, view, onRead) {
|
|
1613
1644
|
try {
|
|
1614
1645
|
const stat2 = await backend.stat(path);
|
|
1615
1646
|
if (stat2.size > MAX_FILE_SIZE) {
|
|
@@ -1621,12 +1652,12 @@ async function readViaBackend(backend, path, onRead) {
|
|
|
1621
1652
|
limit: MAX_FILE_SIZE
|
|
1622
1653
|
});
|
|
1623
1654
|
}
|
|
1624
|
-
const
|
|
1625
|
-
if (
|
|
1655
|
+
const raw = await backend.readFile(path);
|
|
1656
|
+
if (raw.includes("\0")) {
|
|
1626
1657
|
return JSON.stringify({ ok: false, error: "binary_file", path, size: stat2.size });
|
|
1627
1658
|
}
|
|
1628
1659
|
onRead?.(stat2.mtimeMs);
|
|
1629
|
-
return JSON.stringify({ ok: true, content, size: stat2.size });
|
|
1660
|
+
return JSON.stringify({ ok: true, content: renderView(raw, view), size: stat2.size });
|
|
1630
1661
|
} catch (err) {
|
|
1631
1662
|
if (err instanceof FileNotFoundError) {
|
|
1632
1663
|
return JSON.stringify({ ok: false, error: "not_found", path });
|
|
@@ -1637,7 +1668,10 @@ async function readViaBackend(backend, path, onRead) {
|
|
|
1637
1668
|
throw err;
|
|
1638
1669
|
}
|
|
1639
1670
|
}
|
|
1640
|
-
function resolveBoundary(path, projectRoot) {
|
|
1671
|
+
function resolveBoundary(path, projectRoot, allowAbsolute) {
|
|
1672
|
+
if (allowAbsolute && isAbsolute(path)) {
|
|
1673
|
+
return { absolutePath: path };
|
|
1674
|
+
}
|
|
1641
1675
|
try {
|
|
1642
1676
|
const absolutePath = safePathJoin(projectRoot, path);
|
|
1643
1677
|
assertNoSymlinkEscape(absolutePath, projectRoot);
|
|
@@ -1661,7 +1695,7 @@ async function openHandleSafe(absolutePath, path) {
|
|
|
1661
1695
|
throw err;
|
|
1662
1696
|
}
|
|
1663
1697
|
}
|
|
1664
|
-
async function readContent(handle, path, onRead) {
|
|
1698
|
+
async function readContent(handle, path, view, onRead) {
|
|
1665
1699
|
const stat2 = await handle.stat();
|
|
1666
1700
|
if (stat2.size > MAX_FILE_SIZE) {
|
|
1667
1701
|
return JSON.stringify({
|
|
@@ -1675,9 +1709,9 @@ async function readContent(handle, path, onRead) {
|
|
|
1675
1709
|
if (await isBinaryProbe(handle, Number(stat2.size))) {
|
|
1676
1710
|
return JSON.stringify({ ok: false, error: "binary_file", path, size: stat2.size });
|
|
1677
1711
|
}
|
|
1678
|
-
const
|
|
1712
|
+
const raw = await handle.readFile({ encoding: "utf-8" });
|
|
1679
1713
|
onRead?.(stat2.mtimeMs);
|
|
1680
|
-
return JSON.stringify({ ok: true, content, size: stat2.size });
|
|
1714
|
+
return JSON.stringify({ ok: true, content: renderView(raw, view), size: stat2.size });
|
|
1681
1715
|
}
|
|
1682
1716
|
async function isBinaryProbe(handle, size) {
|
|
1683
1717
|
const probeLen = Math.min(BINARY_PROBE_BYTES, size);
|