@theokit/sdk-tools 0.27.0 → 0.27.2

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';
1
+ import { rm, mkdir, writeFile, readFile, readdir, open, stat } from 'fs/promises';
2
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
 
@@ -2659,27 +2679,14 @@ function createViewImageTool(options) {
2659
2679
  });
2660
2680
  }
2661
2681
  const absolute = safePathJoin(projectRoot, input.path);
2662
- let bytes;
2663
- try {
2664
- bytes = statSync(absolute).size;
2665
- } catch {
2666
- return json({ ok: false, error: "not_found", path: input.path });
2667
- }
2668
- if (bytes > maxBytes) {
2669
- return json({
2670
- ok: false,
2671
- error: "image_too_large",
2672
- path: input.path,
2673
- bytes,
2674
- limit_bytes: maxBytes
2675
- });
2676
- }
2682
+ const outcome = readWithinBudget(absolute, maxBytes);
2683
+ if (!outcome.ok) return json(failureFor(outcome, input.path, maxBytes));
2677
2684
  return json({
2678
2685
  ok: true,
2679
2686
  path: input.path,
2680
2687
  media_type: mediaType,
2681
- bytes,
2682
- data: readFileSync(absolute).toString("base64")
2688
+ bytes: outcome.bytes,
2689
+ data: outcome.data
2683
2690
  });
2684
2691
  },
2685
2692
  /**
@@ -2705,6 +2712,31 @@ function createViewImageTool(options) {
2705
2712
  }
2706
2713
  });
2707
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
+ }
2708
2740
  var DEFAULT_TIMEOUT_MS4 = 3e4;
2709
2741
  var MAX_BODY_BYTES = 1 * 1024 * 1024;
2710
2742
  function createWebFetchTool(opts) {