@theokit/sdk-tools 0.9.0 → 0.10.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 +12 -0
- package/dist/index.cjs +120 -21
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +38 -1
- package/dist/index.d.ts +38 -1
- package/dist/index.js +122 -24
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.10.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 2606c98: SE37 — Reasoning ergonomics. Ships `ReasoningTools.create()` (`think`/`analyze` scratchpad tools, from `@theokit/sdk` core, re-exported by `@theokit/sdk-tools`) and a lightweight `AgentOptions.reasoning?: boolean` flag. When `reasoning: true`, the agent gets a chain-of-thought preamble prepended to its system prompt AND the reasoning tools auto-attached, turning a non-reasoning model into a reason→act→observe loop using the SAME model (reuses the existing tool loop; no new runtime). Inert (with a one-time warn) when a native reasoning model is configured (`model.params: [{ id: "thinking" }]`) — native reasoning wins, no double-reasoning. Default off; byte-identical behaviour when unset. Validated REAL on OpenRouter: `reasoning: true` drove the `think` tool and answered the "9.11 vs 9.9" trap correctly (9.9).
|
|
8
|
+
|
|
9
|
+
## 0.9.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 9dc221b: SE31 gap closure — wire the read-side file factories to the optional `filesystem` backend. `createReadFileTool` and `createListDirTool` now accept the same optional `filesystem` provider as `createWriteFileTool`, so a per-request / multi-tenant root isolates READS and LISTINGS too (previously only writes routed through the backend). Omitted ⇒ identical current behavior (local process fs). `createGlobTool` / `createSearchTextTool` remain on local fs in v1 — they need recursive traversal the minimal non-recursive `FilesystemBackend` seam does not expose (deferred follow-up).
|
|
14
|
+
|
|
3
15
|
## 0.9.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
package/dist/index.cjs
CHANGED
|
@@ -105,7 +105,7 @@ function isForbiddenPath(input) {
|
|
|
105
105
|
// src/apply-patch.ts
|
|
106
106
|
function createApplyPatchTool(opts) {
|
|
107
107
|
const { projectRoot } = opts;
|
|
108
|
-
return sdk.
|
|
108
|
+
return sdk.Tool.create({
|
|
109
109
|
name: "apply_patch",
|
|
110
110
|
description: "Apply a unified diff patch to project files. Each file in the diff is security-checked against the project root. Creates .bak backups before modifying. Returns { ok, files_patched } or { ok: false, error }.",
|
|
111
111
|
inputSchema: zod.z.object({
|
|
@@ -267,7 +267,7 @@ function createSessionArtifactStore(options) {
|
|
|
267
267
|
}
|
|
268
268
|
function createEditFileTool(opts) {
|
|
269
269
|
const { projectRoot } = opts;
|
|
270
|
-
return sdk.
|
|
270
|
+
return sdk.Tool.create({
|
|
271
271
|
name: "edit_file",
|
|
272
272
|
description: "Make an exact string replacement in a project-relative file. Replaces the FIRST occurrence of old_string with new_string (a whitespace-normalized fallback is attempted if the exact match fails) and writes a .bak backup first. Read the file first so old_string matches the on-disk text exactly; include enough surrounding context to make it unique \u2014 only the first match is replaced, so a too-short old_string can edit the wrong location. old_string must be non-empty and differ from new_string; to change every occurrence, call edit_file repeatedly. Returns { ok, replacements } or { ok: false, error }.",
|
|
273
273
|
inputSchema: zod.z.object({
|
|
@@ -444,7 +444,7 @@ function createGitDiffTool(opts) {
|
|
|
444
444
|
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
445
445
|
maxStdoutBytes = DEFAULT_MAX_STDOUT_BYTES
|
|
446
446
|
} = opts;
|
|
447
|
-
return sdk.
|
|
447
|
+
return sdk.Tool.create({
|
|
448
448
|
name: "git_diff",
|
|
449
449
|
description: "Return the unified diff of the project's working tree (or staged changes when cached=true). Scoped to a single file when 'path' is provided. Requires the project to be a git repository. Returns { ok, diff, truncated? } or { ok: false, error }.",
|
|
450
450
|
inputSchema: zod.z.object({
|
|
@@ -526,7 +526,7 @@ function runGitProcess(cwd, args, timeoutMs, maxStdoutBytes) {
|
|
|
526
526
|
var DEFAULT_EXCLUDES = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".theo"]);
|
|
527
527
|
function createGlobTool(opts) {
|
|
528
528
|
const { projectRoot } = opts;
|
|
529
|
-
return sdk.
|
|
529
|
+
return sdk.Tool.create({
|
|
530
530
|
name: "glob_files",
|
|
531
531
|
description: "Find files by glob pattern across the project \u2014 fast at any repo size. Use glob_files when you know the filename SHAPE; use search_text when you know the file CONTENT; use read_file when you know the exact path. The pattern supports * and ** wildcards (e.g. '**/*.ts', 'src/**/*.json'); node_modules/.git/dist/.theo are excluded and results are relative paths. Returns { ok, files } or { ok: false, error }.",
|
|
532
532
|
inputSchema: zod.z.object({
|
|
@@ -1089,26 +1089,61 @@ function withShellExitGuidance(tool) {
|
|
|
1089
1089
|
}
|
|
1090
1090
|
var DEFAULT_MAX_ENTRIES = 500;
|
|
1091
1091
|
function createListDirTool(opts) {
|
|
1092
|
-
const { projectRoot, max = DEFAULT_MAX_ENTRIES } = opts;
|
|
1093
|
-
return sdk.
|
|
1092
|
+
const { projectRoot, max = DEFAULT_MAX_ENTRIES, filesystem: filesystem$1 } = opts;
|
|
1093
|
+
return sdk.Tool.create({
|
|
1094
1094
|
name: "list_dir",
|
|
1095
1095
|
description: `Return the direct entries of a project-relative directory. Refuses paths outside the project root or in the sensitive-file blocklist (.env, .git/, node_modules/, .theo/, lock files). Caps at ${String(max)} entries by default; result carries truncated + totalCount.`,
|
|
1096
1096
|
inputSchema: zod.z.object({
|
|
1097
1097
|
path: zod.z.string().min(1).describe("Project-relative directory path. Use '.' for root.")
|
|
1098
1098
|
}),
|
|
1099
|
-
handler: async ({ path }) => {
|
|
1099
|
+
handler: async ({ path }, ctx) => {
|
|
1100
1100
|
const relative2 = path === "" || path === "." ? "." : path;
|
|
1101
1101
|
if (relative2 !== "." && isForbiddenPath(relative2)) {
|
|
1102
1102
|
return JSON.stringify({ ok: false, error: "forbidden_path", path });
|
|
1103
1103
|
}
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
return
|
|
1104
|
+
if (filesystem$1) {
|
|
1105
|
+
const backend = await filesystem.resolveFilesystem(filesystem$1, ctx ?? {});
|
|
1106
|
+
return listViaBackend(backend, relative2, path, max);
|
|
1107
|
+
}
|
|
1108
|
+
return listViaLocalFs(projectRoot, relative2, path, max);
|
|
1109
1109
|
}
|
|
1110
1110
|
});
|
|
1111
1111
|
}
|
|
1112
|
+
async function listViaLocalFs(projectRoot, relative2, originalPath, max) {
|
|
1113
|
+
const boundary = resolveDirBoundary(relative2, projectRoot, originalPath);
|
|
1114
|
+
if ("error" in boundary) return boundary.error;
|
|
1115
|
+
const readResult = await readDirSafe(boundary.absolutePath, originalPath);
|
|
1116
|
+
if ("error" in readResult) return readResult.error;
|
|
1117
|
+
return formatListing(readResult.dirents, max);
|
|
1118
|
+
}
|
|
1119
|
+
async function listViaBackend(backend, relative2, originalPath, max) {
|
|
1120
|
+
let names;
|
|
1121
|
+
try {
|
|
1122
|
+
names = await backend.list(relative2);
|
|
1123
|
+
} catch (err) {
|
|
1124
|
+
if (err instanceof filesystem.FileNotFoundError) {
|
|
1125
|
+
return JSON.stringify({ ok: false, error: "not_found", path: originalPath });
|
|
1126
|
+
}
|
|
1127
|
+
if (err instanceof filesystem.FilesystemSecurityError) {
|
|
1128
|
+
return JSON.stringify({ ok: false, error: "path_traversal", path: originalPath });
|
|
1129
|
+
}
|
|
1130
|
+
throw err;
|
|
1131
|
+
}
|
|
1132
|
+
const totalCount = names.length;
|
|
1133
|
+
const windowed = names.slice(0, max);
|
|
1134
|
+
const entries = await Promise.all(
|
|
1135
|
+
windowed.map(async (name) => {
|
|
1136
|
+
const child = relative2 === "." ? name : `${relative2}/${name}`;
|
|
1137
|
+
let type = "file";
|
|
1138
|
+
try {
|
|
1139
|
+
type = (await backend.stat(child)).isDirectory ? "directory" : "file";
|
|
1140
|
+
} catch {
|
|
1141
|
+
}
|
|
1142
|
+
return { name, type };
|
|
1143
|
+
})
|
|
1144
|
+
);
|
|
1145
|
+
return JSON.stringify({ ok: true, entries, truncated: totalCount > max, totalCount });
|
|
1146
|
+
}
|
|
1112
1147
|
function resolveDirBoundary(relative2, projectRoot, originalPath) {
|
|
1113
1148
|
try {
|
|
1114
1149
|
const absolutePath = relative2 === "." ? projectRoot : safePathJoin(projectRoot, relative2);
|
|
@@ -1269,17 +1304,21 @@ function createQuestionTool(opts) {
|
|
|
1269
1304
|
var MAX_FILE_SIZE = 5 * 1024 * 1024;
|
|
1270
1305
|
var BINARY_PROBE_BYTES = 8 * 1024;
|
|
1271
1306
|
function createReadFileTool(opts) {
|
|
1272
|
-
const { projectRoot, readTracker } = opts;
|
|
1273
|
-
return sdk.
|
|
1307
|
+
const { projectRoot, readTracker, filesystem: filesystem$1 } = opts;
|
|
1308
|
+
return sdk.Tool.create({
|
|
1274
1309
|
name: "read_file",
|
|
1275
1310
|
description: "Read a project-relative 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. Returns the WHOLE file (there is no offset or line-range parameter); to locate a symbol inside a large file, use search_text instead of re-reading. Refuses paths that escape the project root, 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 }.",
|
|
1276
1311
|
inputSchema: zod.z.object({
|
|
1277
1312
|
path: zod.z.string().min(1).describe("Project-relative file path.")
|
|
1278
1313
|
}),
|
|
1279
|
-
handler: async ({ path }) => {
|
|
1314
|
+
handler: async ({ path }, ctx) => {
|
|
1280
1315
|
if (isForbiddenPath(path)) {
|
|
1281
1316
|
return JSON.stringify({ ok: false, error: "forbidden_path", path });
|
|
1282
1317
|
}
|
|
1318
|
+
if (filesystem$1) {
|
|
1319
|
+
const backend = await filesystem.resolveFilesystem(filesystem$1, ctx ?? {});
|
|
1320
|
+
return readViaBackend(backend, path, (mtimeMs) => readTracker?.record(path, mtimeMs));
|
|
1321
|
+
}
|
|
1283
1322
|
const boundary = resolveBoundary(path, projectRoot);
|
|
1284
1323
|
if ("error" in boundary) return boundary.error;
|
|
1285
1324
|
const opened = await openHandleSafe(boundary.absolutePath, path);
|
|
@@ -1296,6 +1335,34 @@ function createReadFileTool(opts) {
|
|
|
1296
1335
|
}
|
|
1297
1336
|
});
|
|
1298
1337
|
}
|
|
1338
|
+
async function readViaBackend(backend, path, onRead) {
|
|
1339
|
+
try {
|
|
1340
|
+
const stat2 = await backend.stat(path);
|
|
1341
|
+
if (stat2.size > MAX_FILE_SIZE) {
|
|
1342
|
+
return JSON.stringify({
|
|
1343
|
+
ok: false,
|
|
1344
|
+
error: "too_large",
|
|
1345
|
+
path,
|
|
1346
|
+
size: stat2.size,
|
|
1347
|
+
limit: MAX_FILE_SIZE
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1350
|
+
const content = await backend.readFile(path);
|
|
1351
|
+
if (content.includes("\0")) {
|
|
1352
|
+
return JSON.stringify({ ok: false, error: "binary_file", path, size: stat2.size });
|
|
1353
|
+
}
|
|
1354
|
+
onRead?.(stat2.mtimeMs);
|
|
1355
|
+
return JSON.stringify({ ok: true, content, size: stat2.size });
|
|
1356
|
+
} catch (err) {
|
|
1357
|
+
if (err instanceof filesystem.FileNotFoundError) {
|
|
1358
|
+
return JSON.stringify({ ok: false, error: "not_found", path });
|
|
1359
|
+
}
|
|
1360
|
+
if (err instanceof filesystem.FilesystemSecurityError) {
|
|
1361
|
+
return JSON.stringify({ ok: false, error: "path_traversal", path });
|
|
1362
|
+
}
|
|
1363
|
+
throw err;
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1299
1366
|
function resolveBoundary(path, projectRoot) {
|
|
1300
1367
|
try {
|
|
1301
1368
|
const absolutePath = safePathJoin(projectRoot, path);
|
|
@@ -1368,6 +1435,37 @@ function evaluateReadBeforeWrite(tracker, path, currentMtimeMs) {
|
|
|
1368
1435
|
if (recorded !== currentMtimeMs) return "stale";
|
|
1369
1436
|
return "ok";
|
|
1370
1437
|
}
|
|
1438
|
+
var ReasoningTools = class {
|
|
1439
|
+
constructor() {
|
|
1440
|
+
}
|
|
1441
|
+
/** Build the tools. Pass `{ analyze: false }` for `think` only. */
|
|
1442
|
+
static create(opts) {
|
|
1443
|
+
const think = sdk.Tool.create({
|
|
1444
|
+
name: "think",
|
|
1445
|
+
description: "Use this as a scratchpad to think step by step BEFORE answering or acting. Write out your reasoning for one step. Nothing else happens \u2014 it is only your private reasoning space. Call it as many times as you need before the final answer.",
|
|
1446
|
+
inputSchema: zod.z.object({
|
|
1447
|
+
thought: zod.z.string().min(1, "think: `thought` must be a non-empty string.")
|
|
1448
|
+
}),
|
|
1449
|
+
handler: ({ thought }) => thought
|
|
1450
|
+
});
|
|
1451
|
+
if (opts?.analyze === false) return [think];
|
|
1452
|
+
const analyze = sdk.Tool.create({
|
|
1453
|
+
name: "analyze",
|
|
1454
|
+
description: "Analyze the result of a previous step or tool call. State what you looked at, your analysis, and whether to `continue` reasoning, `validate` (double-check) your work, or give the `final_answer`. Use this to catch your own mistakes before answering.",
|
|
1455
|
+
inputSchema: zod.z.object({
|
|
1456
|
+
title: zod.z.string().optional(),
|
|
1457
|
+
result: zod.z.string().min(1, "analyze: `result` must describe what you are analyzing."),
|
|
1458
|
+
analysis: zod.z.string().min(1, "analyze: `analysis` must contain your reasoning."),
|
|
1459
|
+
next_action: zod.z.enum(["continue", "validate", "final_answer"])
|
|
1460
|
+
}),
|
|
1461
|
+
handler: ({ title, result, analysis, next_action }) => `${title ? `# ${title}
|
|
1462
|
+
` : ""}Result: ${result}
|
|
1463
|
+
Analysis: ${analysis}
|
|
1464
|
+
Next: ${next_action}`
|
|
1465
|
+
});
|
|
1466
|
+
return [think, analyze];
|
|
1467
|
+
}
|
|
1468
|
+
};
|
|
1371
1469
|
var DEFAULT_TIMEOUT_MS2 = 12e4;
|
|
1372
1470
|
var DEFAULT_MAX_STDOUT_BYTES2 = 10 * 1024 * 1024;
|
|
1373
1471
|
function createRunVitestTool(opts) {
|
|
@@ -1376,7 +1474,7 @@ function createRunVitestTool(opts) {
|
|
|
1376
1474
|
timeoutMs = DEFAULT_TIMEOUT_MS2,
|
|
1377
1475
|
maxStdoutBytes = DEFAULT_MAX_STDOUT_BYTES2
|
|
1378
1476
|
} = opts;
|
|
1379
|
-
return sdk.
|
|
1477
|
+
return sdk.Tool.create({
|
|
1380
1478
|
name: "run_vitest",
|
|
1381
1479
|
description: "Run the project's vitest suite, optionally scoped to a file or pattern via 'path'. Returns parsed { ok, summary } or { ok: false, error }. Vitest stdout warnings are stripped \u2014 the parser extracts the trailing JSON report.",
|
|
1382
1480
|
inputSchema: zod.z.object({
|
|
@@ -1485,7 +1583,7 @@ function createSearchTextTool(opts) {
|
|
|
1485
1583
|
maxMatches = DEFAULT_MAX_MATCHES,
|
|
1486
1584
|
maxFileSize = DEFAULT_MAX_FILE_SIZE
|
|
1487
1585
|
} = opts;
|
|
1488
|
-
return sdk.
|
|
1586
|
+
return sdk.Tool.create({
|
|
1489
1587
|
name: "search_text",
|
|
1490
1588
|
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 }.`,
|
|
1491
1589
|
inputSchema: zod.z.object({
|
|
@@ -1596,7 +1694,7 @@ var MAX_TIMEOUT_MS = 3e5;
|
|
|
1596
1694
|
var MAX_OUTPUT_BYTES = 5 * 1024 * 1024;
|
|
1597
1695
|
function createShellTool(opts) {
|
|
1598
1696
|
const { projectRoot, defaultTimeoutMs = DEFAULT_TIMEOUT_MS3, allowCatastrophic = false } = opts;
|
|
1599
|
-
return sdk.
|
|
1697
|
+
return sdk.Tool.create({
|
|
1600
1698
|
name: "shell_exec",
|
|
1601
1699
|
description: "Execute a shell command in the project directory. Use this for terminal operations \u2014 running tests, git, package managers, build tools. Do NOT use it for file operations (reading, writing, editing, finding files): prefer the specialized read_file/write_file/edit_file/glob_files/search_text tools, which are path-checked and safer. Only commit, push, or change git state when the user explicitly asks. timeout_ms defaults to 30000 (max 300000); stdout/stderr are capped (~5 MB). Returns { ok, stdout, stderr, exit_code } or { ok: false, error }.",
|
|
1602
1700
|
inputSchema: zod.z.object({
|
|
@@ -1833,7 +1931,7 @@ function createWebFetchTool(opts) {
|
|
|
1833
1931
|
const maxRedirects = opts?.maxRedirects;
|
|
1834
1932
|
const fetchImpl = opts?.fetchImpl;
|
|
1835
1933
|
const lookup = opts?.lookup;
|
|
1836
|
-
return sdk.
|
|
1934
|
+
return sdk.Tool.create({
|
|
1837
1935
|
name: "web_fetch",
|
|
1838
1936
|
description: "Fetch the contents of a URL via HTTP/HTTPS. Use only for URLs the user provided or that you are confident help with the task; never invent or guess URLs. Rejects non-http(s) URLs and is SSRF-guarded by default (private/loopback/link-local/cloud-metadata hosts are refused with an ssrf_blocked error). The response body is capped at 1 MB. Returns { ok, content, status_code, content_type } or { ok: false, error }.",
|
|
1839
1937
|
inputSchema: zod.z.object({
|
|
@@ -1920,7 +2018,7 @@ function createWebFetchTool(opts) {
|
|
|
1920
2018
|
}
|
|
1921
2019
|
function createWebSearchTool(opts) {
|
|
1922
2020
|
const { search, defaultMaxResults = 5 } = opts;
|
|
1923
|
-
return sdk.
|
|
2021
|
+
return sdk.Tool.create({
|
|
1924
2022
|
name: "web_search",
|
|
1925
2023
|
description: "Search the web for a query \u2014 use when you need current information beyond the repo or your training cutoff (library docs, an error message, an API). Returns a list of results with title, URL, and snippet; follow up with web_fetch on a promising result to read it in full. The search provider is injected by the consumer. Returns { ok, results } or { ok: false, error }.",
|
|
1926
2024
|
inputSchema: zod.z.object({
|
|
@@ -2007,7 +2105,7 @@ function createWriteFileTool(opts) {
|
|
|
2007
2105
|
);
|
|
2008
2106
|
}
|
|
2009
2107
|
const guard = opts.requireReadBeforeWrite ? opts.readTracker : void 0;
|
|
2010
|
-
return sdk.
|
|
2108
|
+
return sdk.Tool.create({
|
|
2011
2109
|
name: "write_file",
|
|
2012
2110
|
description: "Write UTF-8 content to a project-relative file, creating parent directories as needed. OVERWRITES any existing file at the path. Prefer editing an existing file with edit_file over rewriting it; use write_file to create a NEW file or fully replace a small one. If the file already exists, read_file it first so you do not discard content you have not seen. Refuses paths that escape the write root and sensitive files (.env, .git/, node_modules/, .theo/, lock files); the default local root also refuses binary-file overwrites. Returns { ok, path, bytes } or { ok: false, error }.",
|
|
2013
2111
|
inputSchema: zod.z.object({
|
|
@@ -2130,6 +2228,7 @@ async function isBinaryFile(absolutePath) {
|
|
|
2130
2228
|
exports.CatastrophicCommandError = CatastrophicCommandError;
|
|
2131
2229
|
exports.DEFAULT_TOOL_GUIDANCE = DEFAULT_TOOL_GUIDANCE;
|
|
2132
2230
|
exports.ReadTracker = ReadTracker;
|
|
2231
|
+
exports.ReasoningTools = ReasoningTools;
|
|
2133
2232
|
exports.RedirectBlockedError = RedirectBlockedError;
|
|
2134
2233
|
exports.SsrfBlockedError = SsrfBlockedError;
|
|
2135
2234
|
exports.buildEnvContext = buildEnvContext;
|