@theokit/sdk-tools 0.26.3 → 0.27.1

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.js CHANGED
@@ -1,11 +1,11 @@
1
- import { rm, mkdir, writeFile, readFile, readdir, open, stat, copyFile } from 'fs/promises';
2
- import { dirname, relative, join, isAbsolute } from 'path';
1
+ import { rm, mkdir, writeFile, readFile, readdir, open, stat } from 'fs/promises';
2
+ import { dirname, relative, join, isAbsolute, extname } from 'path';
3
3
  import { Tool, ConfigurationError } from '@theokit/sdk';
4
4
  import { z } from 'zod';
5
5
  import { safePathJoin, assertNoSymlinkEscape, PathTraversalError, ForbiddenPathError, isForbiddenPath, safeFilenameForId } from '@theokit/sdk/path-safety';
6
6
  import { replaceFileAtomic } from '@theokit/sdk/persistence';
7
+ import { existsSync, statSync, mkdirSync, writeFileSync, openSync, fstatSync, readFileSync, closeSync, constants, readdirSync } from 'fs';
7
8
  import { resolveFilesystem, FileNotFoundError, FilesystemSecurityError, FilesystemReadOnlyError, StaleFileError, FilesystemError } from '@theokit/sdk/filesystem';
8
- import { existsSync, statSync, mkdirSync, writeFileSync, readFileSync, readdirSync } from 'fs';
9
9
  import { resolveSandbox } from '@theokit/sdk/sandbox';
10
10
  import { spawn } from 'child_process';
11
11
  import { resolveInteractive, InteractiveUnavailableError, NoSuchSessionError } from '@theokit/sdk/interactive';
@@ -589,7 +589,18 @@ async function editViaLocal(projectRoot, path, old_string, new_string) {
589
589
  }
590
590
  const outcome = computeEdit(content, old_string, new_string);
591
591
  if (!outcome.ok) return JSON.stringify({ ok: false, error: "no_match", path });
592
- await copyFile(absolutePath, `${absolutePath}.bak`);
592
+ try {
593
+ await writeFile(`${absolutePath}.bak`, content, {
594
+ encoding: "utf-8",
595
+ flag: constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC | constants.O_NOFOLLOW
596
+ });
597
+ } catch (err) {
598
+ const code = err.code;
599
+ if (code === "ELOOP" || code === "EEXIST") {
600
+ return JSON.stringify({ ok: false, error: "unsafe_backup_path", path });
601
+ }
602
+ throw err;
603
+ }
593
604
  await writeFile(absolutePath, outcome.result, "utf-8");
594
605
  return JSON.stringify({ ok: true, replacements: 1 });
595
606
  }
@@ -859,15 +870,15 @@ function createGitStatusTool(opts) {
859
870
  path: z.string().optional().describe("Optional project-relative path to scope the status report.")
860
871
  }),
861
872
  handler: async ({ path }, ctx) => {
862
- if (!existsSync(join(projectRoot, ".git"))) {
863
- return JSON.stringify({ ok: false, error: "not_a_repo" });
864
- }
865
873
  const scopeCheck = checkPathScope(path, projectRoot);
866
874
  if (scopeCheck !== null) return scopeCheck;
867
875
  const args = buildArgs(path, opts.includeBranch !== false);
868
876
  if (opts.sandbox !== void 0) {
869
877
  return statusViaSandbox(opts.sandbox, ctx, args, timeoutMs);
870
878
  }
879
+ if (!existsSync(join(projectRoot, ".git"))) {
880
+ return JSON.stringify({ ok: false, error: "not_a_repo" });
881
+ }
871
882
  const result = await runGitProcess(projectRoot, args, timeoutMs, maxStdoutBytes);
872
883
  return formatGitResult(result, timeoutMs);
873
884
  }
@@ -2037,6 +2048,12 @@ function validateVitestScope(path, projectRoot) {
2037
2048
  }
2038
2049
  return checkPathScope(path, projectRoot);
2039
2050
  }
2051
+ var VITEST_MISSING = [
2052
+ /npx canceled due to missing packages/i,
2053
+ /could not determine executable to run/i,
2054
+ /vitest: (?:command )?not found/i,
2055
+ /command not found: vitest/i
2056
+ ];
2040
2057
  function formatVitestResult(result, timeoutMs) {
2041
2058
  if (result.kind === "timeout") {
2042
2059
  return JSON.stringify({ ok: false, error: "timeout", timeoutMs });
@@ -2046,6 +2063,9 @@ function formatVitestResult(result, timeoutMs) {
2046
2063
  }
2047
2064
  const summary = extractTrailingJson(result.stdout);
2048
2065
  if (summary === null) {
2066
+ if (VITEST_MISSING.some((pattern) => pattern.test(result.stderr))) {
2067
+ return JSON.stringify({ ok: false, error: "no_vitest", detail: result.stderr.slice(0, 500) });
2068
+ }
2049
2069
  return JSON.stringify({
2050
2070
  ok: false,
2051
2071
  error: "unparseable_output",
@@ -2575,9 +2595,9 @@ function truncateOutput(output, opts) {
2575
2595
  return { content: output, truncated: false, originalBytes };
2576
2596
  }
2577
2597
  mkdirSync(outputDir, { recursive: true });
2578
- const filename = `overflow-${Date.now()}-${randomUUID().slice(0, 8)}.txt`;
2598
+ const filename = `overflow-${Date.now()}-${randomUUID()}.txt`;
2579
2599
  const overflowPath = join(outputDir, filename);
2580
- writeFileSync(overflowPath, output, "utf-8");
2600
+ writeFileSync(overflowPath, output, { encoding: "utf-8", flag: "wx" });
2581
2601
  const buf = Buffer.from(output, "utf-8");
2582
2602
  const trailer = `
2583
2603
 
@@ -2625,6 +2645,98 @@ function createUpdatePlanTool() {
2625
2645
  }
2626
2646
  });
2627
2647
  }
2648
+ var MEDIA_TYPES = /* @__PURE__ */ new Map([
2649
+ [".png", "image/png"],
2650
+ [".jpg", "image/jpeg"],
2651
+ [".jpeg", "image/jpeg"],
2652
+ [".gif", "image/gif"],
2653
+ [".webp", "image/webp"]
2654
+ ]);
2655
+ var DEFAULT_MAX_IMAGE_BYTES = 5 * 1024 * 1024;
2656
+ var json = (result) => JSON.stringify(result);
2657
+ function createViewImageTool(options) {
2658
+ const { projectRoot } = options;
2659
+ const maxBytes = options.maxBytes ?? DEFAULT_MAX_IMAGE_BYTES;
2660
+ return Tool.create({
2661
+ name: options.name ?? "view_image",
2662
+ description: options.description ?? "Read an image file from the project and show it to the model. Supports png, jpeg, gif and webp.",
2663
+ inputSchema: z.object({
2664
+ path: z.string().describe("Path to the image, relative to the project root.")
2665
+ }),
2666
+ handler: (input) => {
2667
+ const refused = checkPathScope(input.path, projectRoot);
2668
+ if (refused !== null) return refused;
2669
+ if (isForbiddenAtAnyDepth(input.path)) {
2670
+ return json({ ok: false, error: "path_traversal", path: input.path });
2671
+ }
2672
+ const mediaType = MEDIA_TYPES.get(extname(input.path).toLowerCase());
2673
+ if (mediaType === void 0) {
2674
+ return json({
2675
+ ok: false,
2676
+ error: "unsupported_image_type",
2677
+ path: input.path,
2678
+ supported: [...MEDIA_TYPES.keys()]
2679
+ });
2680
+ }
2681
+ const absolute = safePathJoin(projectRoot, input.path);
2682
+ const outcome = readWithinBudget(absolute, maxBytes);
2683
+ if (!outcome.ok) return json(failureFor(outcome, input.path, maxBytes));
2684
+ return json({
2685
+ ok: true,
2686
+ path: input.path,
2687
+ media_type: mediaType,
2688
+ bytes: outcome.bytes,
2689
+ data: outcome.data
2690
+ });
2691
+ },
2692
+ /**
2693
+ * Turn a successful read into an image block.
2694
+ *
2695
+ * A failed read stays TEXT on purpose: the model needs to read "not_found" and try another path,
2696
+ * and an error is not something to look at.
2697
+ */
2698
+ toModelOutput: (output) => {
2699
+ let result;
2700
+ try {
2701
+ result = JSON.parse(output);
2702
+ } catch {
2703
+ return output;
2704
+ }
2705
+ if (result.ok !== true) return output;
2706
+ return [
2707
+ {
2708
+ type: "image",
2709
+ source: { type: "base64", media_type: result.media_type, data: result.data }
2710
+ }
2711
+ ];
2712
+ }
2713
+ });
2714
+ }
2715
+ function readWithinBudget(absolute, maxBytes) {
2716
+ let fd;
2717
+ try {
2718
+ fd = openSync(absolute, "r");
2719
+ } catch {
2720
+ return { ok: false, error: "not_found" };
2721
+ }
2722
+ try {
2723
+ const bytes = fstatSync(fd).size;
2724
+ if (bytes > maxBytes) return { ok: false, error: "too_large", bytes };
2725
+ return { ok: true, bytes, data: readFileSync(fd).toString("base64") };
2726
+ } finally {
2727
+ closeSync(fd);
2728
+ }
2729
+ }
2730
+ function failureFor(outcome, path, maxBytes) {
2731
+ if (outcome.error === "not_found") return { ok: false, error: "not_found", path };
2732
+ return {
2733
+ ok: false,
2734
+ error: "image_too_large",
2735
+ path,
2736
+ bytes: outcome.bytes,
2737
+ limit_bytes: maxBytes
2738
+ };
2739
+ }
2628
2740
  var DEFAULT_TIMEOUT_MS4 = 3e4;
2629
2741
  var MAX_BODY_BYTES = 1 * 1024 * 1024;
2630
2742
  function createWebFetchTool(opts) {
@@ -2766,8 +2878,8 @@ function createBraveWebSearchAdapter(opts = {}) {
2766
2878
  headers: { "X-Subscription-Token": apiKey, Accept: "application/json" }
2767
2879
  });
2768
2880
  if (!res.ok) throw new Error(`brave_search_failed: HTTP ${res.status}`);
2769
- const json = await res.json();
2770
- const results = json?.web?.results ?? [];
2881
+ const json2 = await res.json();
2882
+ const results = json2?.web?.results ?? [];
2771
2883
  return results.map((r) => ({
2772
2884
  title: String(r?.title ?? ""),
2773
2885
  url: String(r?.url ?? ""),
@@ -2927,6 +3039,6 @@ async function isBinaryFile(absolutePath) {
2927
3039
  }
2928
3040
  }
2929
3041
 
2930
- export { CatastrophicCommandError, ContextMatchError, DEFAULT_TOOL_GUIDANCE, ReadTracker, ReasoningTools, RedirectBlockedError, SsrfBlockedError, buildEnvContext, buildRepoMap, catastrophicShellReason, commandDenialReason, createApplyPatchTool, createBraveWebSearchAdapter, createCurrentTimeTool, createEditFileTool, createGenericHttpSearchAdapter, createGitDiffTool, createGitStatusTool, createGlobTool, createInteractiveShellTool, createListDirTool, createPlanModeTool, createQuestionTool, createReadFileTool, createRunVitestTool, createSearchTextTool, createSessionArtifactStore, createShellTool, createTodolistTool, createUpdatePlanTool, createWebFetchTool, createWebSearchTool, createWriteFileTool, createWriteStdinTool, denyCatastrophicCommands, formatCode, formatDiff, formatError, formatFileList, injectGuidance, isBlockedIp, isCommandAllowed, renderToolList, replaceUnique, resolveAndScreen, screenedFetch, todoItemsToPlanNodes, truncateOutput, withDefaultGuidance, withDescription, withName, withShellExitGuidance, withToolResultGuidance };
3042
+ export { CatastrophicCommandError, ContextMatchError, DEFAULT_MAX_IMAGE_BYTES, DEFAULT_TOOL_GUIDANCE, ReadTracker, ReasoningTools, RedirectBlockedError, SsrfBlockedError, buildEnvContext, buildRepoMap, catastrophicShellReason, commandDenialReason, createApplyPatchTool, createBraveWebSearchAdapter, createCurrentTimeTool, createEditFileTool, createGenericHttpSearchAdapter, createGitDiffTool, createGitStatusTool, createGlobTool, createInteractiveShellTool, createListDirTool, createPlanModeTool, createQuestionTool, createReadFileTool, createRunVitestTool, createSearchTextTool, createSessionArtifactStore, createShellTool, createTodolistTool, createUpdatePlanTool, createViewImageTool, createWebFetchTool, createWebSearchTool, createWriteFileTool, createWriteStdinTool, denyCatastrophicCommands, formatCode, formatDiff, formatError, formatFileList, injectGuidance, isBlockedIp, isCommandAllowed, renderToolList, replaceUnique, resolveAndScreen, screenedFetch, todoItemsToPlanNodes, truncateOutput, withDefaultGuidance, withDescription, withName, withShellExitGuidance, withToolResultGuidance };
2931
3043
  //# sourceMappingURL=index.js.map
2932
3044
  //# sourceMappingURL=index.js.map