@theokit/sdk-tools 0.7.0 → 0.9.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/dist/index.d.cts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { CustomTool, ConfigurationError } from '@theokit/sdk';
2
+ import { FilesystemProvider } from '@theokit/sdk/filesystem';
2
3
 
3
4
  /**
4
5
  * `apply_patch` — built-in tool for coding agents.
@@ -272,8 +273,14 @@ interface RepoMapOptions {
272
273
  /** Max directory depth to descend. Default 4. */
273
274
  maxDepth?: number;
274
275
  }
276
+ interface EnvContextOptions {
277
+ /** Injectable clock for the date line (deterministic tests). Default `new Date()`. */
278
+ now?: Date;
279
+ /** Path to the git `HEAD` file. Default `<cwd>/.git/HEAD`. */
280
+ gitHeadPath?: string;
281
+ }
275
282
  /** Render a portable `<env>` orientation block. Never throws. */
276
- declare function buildEnvContext(cwd: string): string;
283
+ declare function buildEnvContext(cwd: string, opts?: EnvContextOptions): string;
277
284
  /** Render a char-bounded, depth-limited directory tree. Never throws. */
278
285
  declare function buildRepoMap(cwd: string, opts?: RepoMapOptions): string;
279
286
 
@@ -498,6 +505,25 @@ interface QuestionTool {
498
505
  }
499
506
  declare function createQuestionTool(opts: QuestionToolOptions): QuestionTool;
500
507
 
508
+ /**
509
+ * `ReadTracker` — SE32 read-before-write safety.
510
+ *
511
+ * A per-run map of `path → mtimeMs` recorded whenever the read tool reads a
512
+ * file. The write tool (with `requireReadBeforeWrite` on) consults it before an
513
+ * overwrite: a file that was never read, or that changed on disk since it was
514
+ * read, is refused instead of silently clobbered. Scope one tracker per run /
515
+ * session — it is deliberately NOT a global singleton (no cross-run leak).
516
+ *
517
+ * @public
518
+ */
519
+ declare class ReadTracker {
520
+ private readonly seen;
521
+ /** Record the mtime observed when `path` was read. */
522
+ record(path: string, mtimeMs: number): void;
523
+ /** The mtime last recorded for `path`, or `undefined` if never read. */
524
+ expected(path: string): number | undefined;
525
+ }
526
+
501
527
  /**
502
528
  * `read_file` — built-in tool for coding agents.
503
529
  *
@@ -527,6 +553,12 @@ declare function createQuestionTool(opts: QuestionToolOptions): QuestionTool;
527
553
  interface CreateReadFileToolOptions {
528
554
  /** Absolute path to the project root. Every read is gated against this boundary. */
529
555
  projectRoot: string;
556
+ /**
557
+ * SE32 — optional read-before-write tracker. When provided, a successful read
558
+ * records the file's mtime so a paired `write_file` (with
559
+ * `requireReadBeforeWrite`) can refuse a blind or stale overwrite.
560
+ */
561
+ readTracker?: ReadTracker;
530
562
  }
531
563
  declare function createReadFileTool(opts: CreateReadFileToolOptions): CustomTool;
532
564
 
@@ -792,12 +824,12 @@ declare function createWebSearchTool(opts: CreateWebSearchToolOptions): CustomTo
792
824
  * the required auth header. Zero new dependencies. Design: blueprint m3-websearch-adapter.
793
825
  */
794
826
 
795
- type FetchLike = (url: string, init?: RequestInit) => Promise<Response>;
827
+ type FetchLike$1 = (url: string, init?: RequestInit) => Promise<Response>;
796
828
  interface CreateBraveWebSearchAdapterOptions {
797
829
  /** Brave API key. Defaults to `process.env.BRAVE_API_KEY`. */
798
830
  apiKey?: string;
799
831
  /** Injectable fetch (default `globalThis.fetch`) — set in tests for offline runs. */
800
- fetchImpl?: FetchLike;
832
+ fetchImpl?: FetchLike$1;
801
833
  /** Override the Brave endpoint (self-host / test). */
802
834
  endpoint?: string;
803
835
  }
@@ -807,6 +839,42 @@ interface CreateBraveWebSearchAdapterOptions {
807
839
  */
808
840
  declare function createBraveWebSearchAdapter(opts?: CreateBraveWebSearchAdapterOptions): WebSearchCallback;
809
841
 
842
+ /**
843
+ * Generic HTTP web-search adapter for `createWebSearchTool` (RADAR #92.d).
844
+ *
845
+ * `createGenericHttpSearchAdapter()` returns a `WebSearchCallback` that queries
846
+ * an arbitrary HTTP search endpoint keyed by env vars:
847
+ * - `THEOKIT_SEARCH_API_URL` — endpoint returning `{ results: [{title,url,snippet}] }`
848
+ * - `THEOKIT_SEARCH_API_KEY` — bearer token
849
+ *
850
+ * Request shape: `GET {endpoint}?q={query}&n={maxResults}` with
851
+ * `Authorization: Bearer {apiKey}`. Both values may be passed explicitly (params
852
+ * win over env). Unlike the Brave adapter (which fails early — fixed host, fixed
853
+ * auth), this one is provider-agnostic and **degrades gracefully**: when
854
+ * unconfigured OR on any network/parse failure it returns `[]` and never throws
855
+ * into the agent turn. A recurring empty result is a provider-config issue, not
856
+ * an agent bug. `fetchImpl` is injectable (default `globalThis.fetch`) for
857
+ * offline tests. Zero new dependencies.
858
+ *
859
+ * Promoted from theocode's `server/lib/web-search.ts` (`createSearchProvider`),
860
+ * re-keyed to neutral `THEOKIT_*` env vars.
861
+ */
862
+
863
+ type FetchLike = (url: string, init?: RequestInit) => Promise<Response>;
864
+ interface CreateGenericHttpSearchAdapterOptions {
865
+ /** Bearer token. Defaults to `process.env.THEOKIT_SEARCH_API_KEY`. */
866
+ apiKey?: string;
867
+ /** Search endpoint URL. Defaults to `process.env.THEOKIT_SEARCH_API_URL`. */
868
+ endpoint?: string;
869
+ /** Injectable fetch (default `globalThis.fetch`) — set in tests for offline runs. */
870
+ fetchImpl?: FetchLike;
871
+ }
872
+ /**
873
+ * Build a `WebSearchCallback` backed by a generic HTTP search endpoint. Returns
874
+ * `[]` (graceful no-op) when unconfigured or on any failure — never throws.
875
+ */
876
+ declare function createGenericHttpSearchAdapter(opts?: CreateGenericHttpSearchAdapterOptions): WebSearchCallback;
877
+
810
878
  /**
811
879
  * `write_file` — built-in tool for coding agents.
812
880
  *
@@ -820,10 +888,32 @@ declare function createBraveWebSearchAdapter(opts?: CreateBraveWebSearchAdapterO
820
888
  * 'binary_file' }` on refusal
821
889
  */
822
890
 
891
+ /** The run-scoped context a `defineTool` handler receives as its 2nd argument. */
892
+ type WriteToolContext = {
893
+ signal?: AbortSignal;
894
+ context?: unknown;
895
+ };
823
896
  interface CreateWriteFileToolOptions {
824
897
  /** Absolute path to the project root. Every write is gated against this boundary. */
825
898
  projectRoot: string;
899
+ /**
900
+ * SE31 — optional pluggable filesystem backend (`@theokit/sdk/filesystem`), or
901
+ * a per-request resolver `(ctx) => FilesystemBackend`. When provided, writes
902
+ * route through it (its own boundary + `readOnly` + per-request root) instead
903
+ * of the local project fs. Omitted ⇒ identical current behavior (local
904
+ * `projectRoot`). The `.env`/`.git` policy still applies (storage-independent).
905
+ */
906
+ filesystem?: FilesystemProvider<WriteToolContext>;
907
+ /**
908
+ * SE32 — when true (and a {@link ReadTracker} is supplied), refuse to
909
+ * overwrite an existing file that was not read first, or that changed on disk
910
+ * since it was read (`read_required` / `stale_file`). A NEW file writes
911
+ * freely. Default OFF (unchanged behavior).
912
+ */
913
+ requireReadBeforeWrite?: boolean;
914
+ /** SE32 — the per-run tracker populated by the paired `read_file` tool. */
915
+ readTracker?: ReadTracker;
826
916
  }
827
917
  declare function createWriteFileTool(opts: CreateWriteFileToolOptions): CustomTool;
828
918
 
829
- export { CatastrophicCommandError, type CommandPolicy, type CreateApplyPatchToolOptions, type CreateBraveWebSearchAdapterOptions, type CreateEditFileToolOptions, type CreateGitDiffToolOptions, type CreateGlobToolOptions, type CreateListDirToolOptions, type CreateReadFileToolOptions, type CreateRunVitestToolOptions, type CreateSearchTextToolOptions, type CreateShellToolOptions, type CreateWebFetchToolOptions, type CreateWebSearchToolOptions, type CreateWriteFileToolOptions, DEFAULT_TOOL_GUIDANCE, type PlanModeTool, type PlanModeToolOptions, type PlanModeToolWithStore, type PlanNode, type QuestionTool, type QuestionToolOptions, RedirectBlockedError, type RepoMapOptions, type ResolveAndScreenOptions, type ScreenedFetchOptions, type SessionArtifactStore, type SessionArtifactStoreOptions, SsrfBlockedError, type TodoItem, type TodolistTool, type ToolGuidanceMap, type TruncationOptions, type TruncationResult, type VitestSummary, type WebSearchCallback, type WebSearchResult, buildEnvContext, buildRepoMap, catastrophicShellReason, commandDenialReason, createApplyPatchTool, createBraveWebSearchAdapter, createEditFileTool, createGitDiffTool, createGlobTool, createListDirTool, createPlanModeTool, createQuestionTool, createReadFileTool, createRunVitestTool, createSearchTextTool, createSessionArtifactStore, createShellTool, createTodolistTool, createWebFetchTool, createWebSearchTool, createWriteFileTool, denyCatastrophicCommands, formatCode, formatDiff, formatError, formatFileList, injectGuidance, isBlockedIp, isCommandAllowed, renderToolList, resolveAndScreen, screenedFetch, todoItemsToPlanNodes, truncateOutput, withDefaultGuidance, withDescription, withShellExitGuidance, withToolResultGuidance };
919
+ export { CatastrophicCommandError, type CommandPolicy, type CreateApplyPatchToolOptions, type CreateBraveWebSearchAdapterOptions, type CreateEditFileToolOptions, type CreateGenericHttpSearchAdapterOptions, type CreateGitDiffToolOptions, type CreateGlobToolOptions, type CreateListDirToolOptions, type CreateReadFileToolOptions, type CreateRunVitestToolOptions, type CreateSearchTextToolOptions, type CreateShellToolOptions, type CreateWebFetchToolOptions, type CreateWebSearchToolOptions, type CreateWriteFileToolOptions, DEFAULT_TOOL_GUIDANCE, type EnvContextOptions, type PlanModeTool, type PlanModeToolOptions, type PlanModeToolWithStore, type PlanNode, type QuestionTool, type QuestionToolOptions, ReadTracker, RedirectBlockedError, type RepoMapOptions, type ResolveAndScreenOptions, type ScreenedFetchOptions, type SessionArtifactStore, type SessionArtifactStoreOptions, SsrfBlockedError, type TodoItem, type TodolistTool, type ToolGuidanceMap, type TruncationOptions, type TruncationResult, type VitestSummary, type WebSearchCallback, type WebSearchResult, buildEnvContext, buildRepoMap, catastrophicShellReason, commandDenialReason, createApplyPatchTool, createBraveWebSearchAdapter, createEditFileTool, createGenericHttpSearchAdapter, createGitDiffTool, createGlobTool, createListDirTool, createPlanModeTool, createQuestionTool, createReadFileTool, createRunVitestTool, createSearchTextTool, createSessionArtifactStore, createShellTool, createTodolistTool, createWebFetchTool, createWebSearchTool, createWriteFileTool, denyCatastrophicCommands, formatCode, formatDiff, formatError, formatFileList, injectGuidance, isBlockedIp, isCommandAllowed, renderToolList, resolveAndScreen, screenedFetch, todoItemsToPlanNodes, truncateOutput, withDefaultGuidance, withDescription, withShellExitGuidance, withToolResultGuidance };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { CustomTool, ConfigurationError } from '@theokit/sdk';
2
+ import { FilesystemProvider } from '@theokit/sdk/filesystem';
2
3
 
3
4
  /**
4
5
  * `apply_patch` — built-in tool for coding agents.
@@ -272,8 +273,14 @@ interface RepoMapOptions {
272
273
  /** Max directory depth to descend. Default 4. */
273
274
  maxDepth?: number;
274
275
  }
276
+ interface EnvContextOptions {
277
+ /** Injectable clock for the date line (deterministic tests). Default `new Date()`. */
278
+ now?: Date;
279
+ /** Path to the git `HEAD` file. Default `<cwd>/.git/HEAD`. */
280
+ gitHeadPath?: string;
281
+ }
275
282
  /** Render a portable `<env>` orientation block. Never throws. */
276
- declare function buildEnvContext(cwd: string): string;
283
+ declare function buildEnvContext(cwd: string, opts?: EnvContextOptions): string;
277
284
  /** Render a char-bounded, depth-limited directory tree. Never throws. */
278
285
  declare function buildRepoMap(cwd: string, opts?: RepoMapOptions): string;
279
286
 
@@ -498,6 +505,25 @@ interface QuestionTool {
498
505
  }
499
506
  declare function createQuestionTool(opts: QuestionToolOptions): QuestionTool;
500
507
 
508
+ /**
509
+ * `ReadTracker` — SE32 read-before-write safety.
510
+ *
511
+ * A per-run map of `path → mtimeMs` recorded whenever the read tool reads a
512
+ * file. The write tool (with `requireReadBeforeWrite` on) consults it before an
513
+ * overwrite: a file that was never read, or that changed on disk since it was
514
+ * read, is refused instead of silently clobbered. Scope one tracker per run /
515
+ * session — it is deliberately NOT a global singleton (no cross-run leak).
516
+ *
517
+ * @public
518
+ */
519
+ declare class ReadTracker {
520
+ private readonly seen;
521
+ /** Record the mtime observed when `path` was read. */
522
+ record(path: string, mtimeMs: number): void;
523
+ /** The mtime last recorded for `path`, or `undefined` if never read. */
524
+ expected(path: string): number | undefined;
525
+ }
526
+
501
527
  /**
502
528
  * `read_file` — built-in tool for coding agents.
503
529
  *
@@ -527,6 +553,12 @@ declare function createQuestionTool(opts: QuestionToolOptions): QuestionTool;
527
553
  interface CreateReadFileToolOptions {
528
554
  /** Absolute path to the project root. Every read is gated against this boundary. */
529
555
  projectRoot: string;
556
+ /**
557
+ * SE32 — optional read-before-write tracker. When provided, a successful read
558
+ * records the file's mtime so a paired `write_file` (with
559
+ * `requireReadBeforeWrite`) can refuse a blind or stale overwrite.
560
+ */
561
+ readTracker?: ReadTracker;
530
562
  }
531
563
  declare function createReadFileTool(opts: CreateReadFileToolOptions): CustomTool;
532
564
 
@@ -792,12 +824,12 @@ declare function createWebSearchTool(opts: CreateWebSearchToolOptions): CustomTo
792
824
  * the required auth header. Zero new dependencies. Design: blueprint m3-websearch-adapter.
793
825
  */
794
826
 
795
- type FetchLike = (url: string, init?: RequestInit) => Promise<Response>;
827
+ type FetchLike$1 = (url: string, init?: RequestInit) => Promise<Response>;
796
828
  interface CreateBraveWebSearchAdapterOptions {
797
829
  /** Brave API key. Defaults to `process.env.BRAVE_API_KEY`. */
798
830
  apiKey?: string;
799
831
  /** Injectable fetch (default `globalThis.fetch`) — set in tests for offline runs. */
800
- fetchImpl?: FetchLike;
832
+ fetchImpl?: FetchLike$1;
801
833
  /** Override the Brave endpoint (self-host / test). */
802
834
  endpoint?: string;
803
835
  }
@@ -807,6 +839,42 @@ interface CreateBraveWebSearchAdapterOptions {
807
839
  */
808
840
  declare function createBraveWebSearchAdapter(opts?: CreateBraveWebSearchAdapterOptions): WebSearchCallback;
809
841
 
842
+ /**
843
+ * Generic HTTP web-search adapter for `createWebSearchTool` (RADAR #92.d).
844
+ *
845
+ * `createGenericHttpSearchAdapter()` returns a `WebSearchCallback` that queries
846
+ * an arbitrary HTTP search endpoint keyed by env vars:
847
+ * - `THEOKIT_SEARCH_API_URL` — endpoint returning `{ results: [{title,url,snippet}] }`
848
+ * - `THEOKIT_SEARCH_API_KEY` — bearer token
849
+ *
850
+ * Request shape: `GET {endpoint}?q={query}&n={maxResults}` with
851
+ * `Authorization: Bearer {apiKey}`. Both values may be passed explicitly (params
852
+ * win over env). Unlike the Brave adapter (which fails early — fixed host, fixed
853
+ * auth), this one is provider-agnostic and **degrades gracefully**: when
854
+ * unconfigured OR on any network/parse failure it returns `[]` and never throws
855
+ * into the agent turn. A recurring empty result is a provider-config issue, not
856
+ * an agent bug. `fetchImpl` is injectable (default `globalThis.fetch`) for
857
+ * offline tests. Zero new dependencies.
858
+ *
859
+ * Promoted from theocode's `server/lib/web-search.ts` (`createSearchProvider`),
860
+ * re-keyed to neutral `THEOKIT_*` env vars.
861
+ */
862
+
863
+ type FetchLike = (url: string, init?: RequestInit) => Promise<Response>;
864
+ interface CreateGenericHttpSearchAdapterOptions {
865
+ /** Bearer token. Defaults to `process.env.THEOKIT_SEARCH_API_KEY`. */
866
+ apiKey?: string;
867
+ /** Search endpoint URL. Defaults to `process.env.THEOKIT_SEARCH_API_URL`. */
868
+ endpoint?: string;
869
+ /** Injectable fetch (default `globalThis.fetch`) — set in tests for offline runs. */
870
+ fetchImpl?: FetchLike;
871
+ }
872
+ /**
873
+ * Build a `WebSearchCallback` backed by a generic HTTP search endpoint. Returns
874
+ * `[]` (graceful no-op) when unconfigured or on any failure — never throws.
875
+ */
876
+ declare function createGenericHttpSearchAdapter(opts?: CreateGenericHttpSearchAdapterOptions): WebSearchCallback;
877
+
810
878
  /**
811
879
  * `write_file` — built-in tool for coding agents.
812
880
  *
@@ -820,10 +888,32 @@ declare function createBraveWebSearchAdapter(opts?: CreateBraveWebSearchAdapterO
820
888
  * 'binary_file' }` on refusal
821
889
  */
822
890
 
891
+ /** The run-scoped context a `defineTool` handler receives as its 2nd argument. */
892
+ type WriteToolContext = {
893
+ signal?: AbortSignal;
894
+ context?: unknown;
895
+ };
823
896
  interface CreateWriteFileToolOptions {
824
897
  /** Absolute path to the project root. Every write is gated against this boundary. */
825
898
  projectRoot: string;
899
+ /**
900
+ * SE31 — optional pluggable filesystem backend (`@theokit/sdk/filesystem`), or
901
+ * a per-request resolver `(ctx) => FilesystemBackend`. When provided, writes
902
+ * route through it (its own boundary + `readOnly` + per-request root) instead
903
+ * of the local project fs. Omitted ⇒ identical current behavior (local
904
+ * `projectRoot`). The `.env`/`.git` policy still applies (storage-independent).
905
+ */
906
+ filesystem?: FilesystemProvider<WriteToolContext>;
907
+ /**
908
+ * SE32 — when true (and a {@link ReadTracker} is supplied), refuse to
909
+ * overwrite an existing file that was not read first, or that changed on disk
910
+ * since it was read (`read_required` / `stale_file`). A NEW file writes
911
+ * freely. Default OFF (unchanged behavior).
912
+ */
913
+ requireReadBeforeWrite?: boolean;
914
+ /** SE32 — the per-run tracker populated by the paired `read_file` tool. */
915
+ readTracker?: ReadTracker;
826
916
  }
827
917
  declare function createWriteFileTool(opts: CreateWriteFileToolOptions): CustomTool;
828
918
 
829
- export { CatastrophicCommandError, type CommandPolicy, type CreateApplyPatchToolOptions, type CreateBraveWebSearchAdapterOptions, type CreateEditFileToolOptions, type CreateGitDiffToolOptions, type CreateGlobToolOptions, type CreateListDirToolOptions, type CreateReadFileToolOptions, type CreateRunVitestToolOptions, type CreateSearchTextToolOptions, type CreateShellToolOptions, type CreateWebFetchToolOptions, type CreateWebSearchToolOptions, type CreateWriteFileToolOptions, DEFAULT_TOOL_GUIDANCE, type PlanModeTool, type PlanModeToolOptions, type PlanModeToolWithStore, type PlanNode, type QuestionTool, type QuestionToolOptions, RedirectBlockedError, type RepoMapOptions, type ResolveAndScreenOptions, type ScreenedFetchOptions, type SessionArtifactStore, type SessionArtifactStoreOptions, SsrfBlockedError, type TodoItem, type TodolistTool, type ToolGuidanceMap, type TruncationOptions, type TruncationResult, type VitestSummary, type WebSearchCallback, type WebSearchResult, buildEnvContext, buildRepoMap, catastrophicShellReason, commandDenialReason, createApplyPatchTool, createBraveWebSearchAdapter, createEditFileTool, createGitDiffTool, createGlobTool, createListDirTool, createPlanModeTool, createQuestionTool, createReadFileTool, createRunVitestTool, createSearchTextTool, createSessionArtifactStore, createShellTool, createTodolistTool, createWebFetchTool, createWebSearchTool, createWriteFileTool, denyCatastrophicCommands, formatCode, formatDiff, formatError, formatFileList, injectGuidance, isBlockedIp, isCommandAllowed, renderToolList, resolveAndScreen, screenedFetch, todoItemsToPlanNodes, truncateOutput, withDefaultGuidance, withDescription, withShellExitGuidance, withToolResultGuidance };
919
+ export { CatastrophicCommandError, type CommandPolicy, type CreateApplyPatchToolOptions, type CreateBraveWebSearchAdapterOptions, type CreateEditFileToolOptions, type CreateGenericHttpSearchAdapterOptions, type CreateGitDiffToolOptions, type CreateGlobToolOptions, type CreateListDirToolOptions, type CreateReadFileToolOptions, type CreateRunVitestToolOptions, type CreateSearchTextToolOptions, type CreateShellToolOptions, type CreateWebFetchToolOptions, type CreateWebSearchToolOptions, type CreateWriteFileToolOptions, DEFAULT_TOOL_GUIDANCE, type EnvContextOptions, type PlanModeTool, type PlanModeToolOptions, type PlanModeToolWithStore, type PlanNode, type QuestionTool, type QuestionToolOptions, ReadTracker, RedirectBlockedError, type RepoMapOptions, type ResolveAndScreenOptions, type ScreenedFetchOptions, type SessionArtifactStore, type SessionArtifactStoreOptions, SsrfBlockedError, type TodoItem, type TodolistTool, type ToolGuidanceMap, type TruncationOptions, type TruncationResult, type VitestSummary, type WebSearchCallback, type WebSearchResult, buildEnvContext, buildRepoMap, catastrophicShellReason, commandDenialReason, createApplyPatchTool, createBraveWebSearchAdapter, createEditFileTool, createGenericHttpSearchAdapter, createGitDiffTool, createGlobTool, createListDirTool, createPlanModeTool, createQuestionTool, createReadFileTool, createRunVitestTool, createSearchTextTool, createSessionArtifactStore, createShellTool, createTodolistTool, createWebFetchTool, createWebSearchTool, createWriteFileTool, denyCatastrophicCommands, formatCode, formatDiff, formatError, formatFileList, injectGuidance, isBlockedIp, isCommandAllowed, renderToolList, resolveAndScreen, screenedFetch, todoItemsToPlanNodes, truncateOutput, withDefaultGuidance, withDescription, withShellExitGuidance, withToolResultGuidance };
package/dist/index.js CHANGED
@@ -8,6 +8,7 @@ import { safeFilenameForId, safePathJoin as safePathJoin$1 } from '@theokit/sdk/
8
8
  import { spawn } from 'child_process';
9
9
  import { lookup } from 'dns/promises';
10
10
  import { isIP } from 'net';
11
+ import { resolveFilesystem, FilesystemSecurityError, FilesystemReadOnlyError, StaleFileError, FilesystemError, FileNotFoundError } from '@theokit/sdk/filesystem';
11
12
 
12
13
  // src/apply-patch.ts
13
14
  var PathTraversalError = class extends ConfigurationError {
@@ -885,15 +886,26 @@ function safeReadHead(p, n) {
885
886
  return "";
886
887
  }
887
888
  }
888
- function buildEnvContext(cwd) {
889
+ function gitBranch(headPath) {
890
+ try {
891
+ const head = readFileSync(headPath, "utf-8").trim();
892
+ const match = head.match(/^ref:\s*refs\/heads\/(.+)$/);
893
+ return match ? match[1] : void 0;
894
+ } catch {
895
+ return void 0;
896
+ }
897
+ }
898
+ function buildEnvContext(cwd, opts = {}) {
889
899
  const lines = [
890
900
  "<env>",
891
901
  ` Working directory: ${cwd}`,
892
902
  ` Platform: ${process.platform} (${process.arch})`,
893
903
  ` Node: ${process.version}`,
894
- ` Is git repo: ${safeExists(join(cwd, ".git")) ? "yes" : "no"}`,
895
- ` Today's date: ${(/* @__PURE__ */ new Date()).toDateString()}`
904
+ ` Is git repo: ${safeExists(join(cwd, ".git")) ? "yes" : "no"}`
896
905
  ];
906
+ const branch = gitBranch(opts.gitHeadPath ?? join(cwd, ".git", "HEAD"));
907
+ if (branch) lines.push(` Branch: ${branch}`);
908
+ lines.push(` Today's date: ${(opts.now ?? /* @__PURE__ */ new Date()).toDateString()}`);
897
909
  const docs = PROJECT_DOCS.filter((d) => safeExists(join(cwd, d)));
898
910
  if (docs.length > 0) {
899
911
  lines.push(` Project docs: ${docs.join(", ")}`);
@@ -1041,7 +1053,10 @@ function withToolResultGuidance(tool, guidance) {
1041
1053
  name: tool.name,
1042
1054
  description: tool.description,
1043
1055
  inputSchema: tool.inputSchema,
1044
- handler: async (input) => injectGuidance(await tool.handler(input), guidance)
1056
+ handler: async (input) => {
1057
+ const out = await tool.handler(input);
1058
+ return typeof out === "string" ? injectGuidance(out, guidance) : out;
1059
+ }
1045
1060
  };
1046
1061
  }
1047
1062
  function withDefaultGuidance(tool) {
@@ -1055,6 +1070,7 @@ function withShellExitGuidance(tool) {
1055
1070
  inputSchema: tool.inputSchema,
1056
1071
  handler: async (input) => {
1057
1072
  const out = await tool.handler(input);
1073
+ if (typeof out !== "string") return out;
1058
1074
  let parsed;
1059
1075
  try {
1060
1076
  parsed = JSON.parse(out);
@@ -1251,7 +1267,7 @@ function createQuestionTool(opts) {
1251
1267
  var MAX_FILE_SIZE = 5 * 1024 * 1024;
1252
1268
  var BINARY_PROBE_BYTES = 8 * 1024;
1253
1269
  function createReadFileTool(opts) {
1254
- const { projectRoot } = opts;
1270
+ const { projectRoot, readTracker } = opts;
1255
1271
  return defineTool({
1256
1272
  name: "read_file",
1257
1273
  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 }.",
@@ -1267,7 +1283,11 @@ function createReadFileTool(opts) {
1267
1283
  const opened = await openHandleSafe(boundary.absolutePath, path);
1268
1284
  if ("error" in opened) return opened.error;
1269
1285
  try {
1270
- return await readContent(opened.handle, path);
1286
+ return await readContent(
1287
+ opened.handle,
1288
+ path,
1289
+ (mtimeMs) => readTracker?.record(path, mtimeMs)
1290
+ );
1271
1291
  } finally {
1272
1292
  await opened.handle.close();
1273
1293
  }
@@ -1298,7 +1318,7 @@ async function openHandleSafe(absolutePath, path) {
1298
1318
  throw err;
1299
1319
  }
1300
1320
  }
1301
- async function readContent(handle, path) {
1321
+ async function readContent(handle, path, onRead) {
1302
1322
  const stat2 = await handle.stat();
1303
1323
  if (stat2.size > MAX_FILE_SIZE) {
1304
1324
  return JSON.stringify({
@@ -1313,6 +1333,7 @@ async function readContent(handle, path) {
1313
1333
  return JSON.stringify({ ok: false, error: "binary_file", path, size: stat2.size });
1314
1334
  }
1315
1335
  const content = await handle.readFile({ encoding: "utf-8" });
1336
+ onRead?.(stat2.mtimeMs);
1316
1337
  return JSON.stringify({ ok: true, content, size: stat2.size });
1317
1338
  }
1318
1339
  async function isBinaryProbe(handle, size) {
@@ -1325,6 +1346,26 @@ async function isBinaryProbe(handle, size) {
1325
1346
  }
1326
1347
  return false;
1327
1348
  }
1349
+
1350
+ // src/read-tracker.ts
1351
+ var ReadTracker = class {
1352
+ seen = /* @__PURE__ */ new Map();
1353
+ /** Record the mtime observed when `path` was read. */
1354
+ record(path, mtimeMs) {
1355
+ this.seen.set(path, mtimeMs);
1356
+ }
1357
+ /** The mtime last recorded for `path`, or `undefined` if never read. */
1358
+ expected(path) {
1359
+ return this.seen.get(path);
1360
+ }
1361
+ };
1362
+ function evaluateReadBeforeWrite(tracker, path, currentMtimeMs) {
1363
+ if (currentMtimeMs === null) return "ok";
1364
+ const recorded = tracker.expected(path);
1365
+ if (recorded === void 0) return "read_required";
1366
+ if (recorded !== currentMtimeMs) return "stale";
1367
+ return "ok";
1368
+ }
1328
1369
  var DEFAULT_TIMEOUT_MS2 = 12e4;
1329
1370
  var DEFAULT_MAX_STDOUT_BYTES2 = 10 * 1024 * 1024;
1330
1371
  function createRunVitestTool(opts) {
@@ -1932,40 +1973,136 @@ function createBraveWebSearchAdapter(opts = {}) {
1932
1973
  }));
1933
1974
  };
1934
1975
  }
1976
+
1977
+ // src/web-search-http.ts
1978
+ function createGenericHttpSearchAdapter(opts = {}) {
1979
+ const apiKey = opts.apiKey ?? process.env.THEOKIT_SEARCH_API_KEY;
1980
+ const endpoint = opts.endpoint ?? process.env.THEOKIT_SEARCH_API_URL;
1981
+ const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
1982
+ return async (query, maxResults) => {
1983
+ if (!apiKey || !endpoint) return [];
1984
+ try {
1985
+ const url = `${endpoint}?q=${encodeURIComponent(query)}&n=${maxResults}`;
1986
+ const res = await fetchImpl(url, { headers: { Authorization: `Bearer ${apiKey}` } });
1987
+ if (!res.ok) return [];
1988
+ const data = await res.json();
1989
+ return (data?.results ?? []).slice(0, maxResults).map((r) => ({
1990
+ title: String(r?.title ?? ""),
1991
+ url: String(r?.url ?? ""),
1992
+ snippet: String(r?.snippet ?? "")
1993
+ }));
1994
+ } catch {
1995
+ return [];
1996
+ }
1997
+ };
1998
+ }
1935
1999
  var BINARY_PROBE_BYTES3 = 8 * 1024;
1936
2000
  function createWriteFileTool(opts) {
1937
- const { projectRoot } = opts;
2001
+ const { projectRoot, filesystem } = opts;
2002
+ if (opts.requireReadBeforeWrite && !opts.readTracker) {
2003
+ throw new Error(
2004
+ "createWriteFileTool: requireReadBeforeWrite is true but no readTracker was provided \u2014 pass the same ReadTracker instance to createReadFileTool and createWriteFileTool."
2005
+ );
2006
+ }
2007
+ const guard = opts.requireReadBeforeWrite ? opts.readTracker : void 0;
1938
2008
  return defineTool({
1939
2009
  name: "write_file",
1940
- 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 project root, sensitive files (.env, .git/, node_modules/, .theo/, lock files), and binary-file overwrites. Returns { ok, path, bytes } or { ok: false, error }.",
2010
+ 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 }.",
1941
2011
  inputSchema: z.object({
1942
2012
  path: z.string().min(1).describe("Project-relative file path."),
1943
2013
  content: z.string().describe("UTF-8 content to write.")
1944
2014
  }),
1945
- handler: async ({ path, content }) => {
2015
+ handler: async ({ path, content }, ctx) => {
1946
2016
  if (isForbiddenPath(path)) {
1947
2017
  return JSON.stringify({ ok: false, error: "forbidden_path", path });
1948
2018
  }
1949
- let absolutePath;
1950
- try {
1951
- absolutePath = safePathJoin(projectRoot, path);
1952
- assertNoSymlinkEscape(absolutePath, projectRoot);
1953
- } catch (err) {
1954
- if (err instanceof PathTraversalError || err instanceof ForbiddenPathError) {
1955
- return JSON.stringify({ ok: false, error: "path_traversal", path });
1956
- }
1957
- throw err;
1958
- }
1959
- if (await isBinaryFile(absolutePath)) {
1960
- return JSON.stringify({ ok: false, error: "binary_file", path });
2019
+ if (filesystem) {
2020
+ const backend = await resolveFilesystem(filesystem, ctx ?? {});
2021
+ return writeViaBackend(backend, path, content, guard);
1961
2022
  }
1962
- await mkdir(dirname(absolutePath), { recursive: true });
1963
- await writeFile(absolutePath, content, "utf-8");
1964
- const bytes = Buffer.byteLength(content, "utf-8");
1965
- return JSON.stringify({ ok: true, path, bytes });
2023
+ return writeViaLocalFs(projectRoot, path, content, guard);
1966
2024
  }
1967
2025
  });
1968
2026
  }
2027
+ function readBeforeWriteError(guard, path, currentMtimeMs) {
2028
+ if (!guard) return null;
2029
+ const decision = evaluateReadBeforeWrite(guard, path, currentMtimeMs);
2030
+ if (decision === "read_required")
2031
+ return JSON.stringify({ ok: false, error: "read_required", path });
2032
+ if (decision === "stale") return JSON.stringify({ ok: false, error: "stale_file", path });
2033
+ return null;
2034
+ }
2035
+ async function writeViaLocalFs(projectRoot, path, content, guard) {
2036
+ let absolutePath;
2037
+ try {
2038
+ absolutePath = safePathJoin(projectRoot, path);
2039
+ assertNoSymlinkEscape(absolutePath, projectRoot);
2040
+ } catch (err) {
2041
+ if (err instanceof PathTraversalError || err instanceof ForbiddenPathError) {
2042
+ return JSON.stringify({ ok: false, error: "path_traversal", path });
2043
+ }
2044
+ throw err;
2045
+ }
2046
+ const rbw = readBeforeWriteError(guard, path, await statMtimeOrNull(absolutePath));
2047
+ if (rbw) return rbw;
2048
+ if (await isBinaryFile(absolutePath)) {
2049
+ return JSON.stringify({ ok: false, error: "binary_file", path });
2050
+ }
2051
+ await mkdir(dirname(absolutePath), { recursive: true });
2052
+ await writeFile(absolutePath, content, "utf-8");
2053
+ const bytes = Buffer.byteLength(content, "utf-8");
2054
+ return JSON.stringify({ ok: true, path, bytes });
2055
+ }
2056
+ async function statMtimeOrNull(absolutePath) {
2057
+ try {
2058
+ return (await stat(absolutePath)).mtimeMs;
2059
+ } catch (err) {
2060
+ if (err.code === "ENOENT") return null;
2061
+ throw err;
2062
+ }
2063
+ }
2064
+ async function backendMtimeOrNull(backend, path) {
2065
+ try {
2066
+ return (await backend.stat(path)).mtimeMs;
2067
+ } catch (err) {
2068
+ if (err instanceof FileNotFoundError) return null;
2069
+ throw err;
2070
+ }
2071
+ }
2072
+ async function writeViaBackend(backend, path, content, guard) {
2073
+ try {
2074
+ let expectedMtime;
2075
+ if (guard) {
2076
+ const current = await backendMtimeOrNull(backend, path);
2077
+ const rbw = readBeforeWriteError(guard, path, current);
2078
+ if (rbw) return rbw;
2079
+ expectedMtime = current ?? void 0;
2080
+ }
2081
+ const stat2 = await backend.writeFile(
2082
+ path,
2083
+ content,
2084
+ expectedMtime !== void 0 ? { expectedMtime } : void 0
2085
+ );
2086
+ return JSON.stringify({ ok: true, path, bytes: stat2.size });
2087
+ } catch (err) {
2088
+ return backendErrorToJson(err, path);
2089
+ }
2090
+ }
2091
+ function backendErrorToJson(err, path) {
2092
+ if (err instanceof FilesystemSecurityError) {
2093
+ return JSON.stringify({ ok: false, error: "path_traversal", path });
2094
+ }
2095
+ if (err instanceof FilesystemReadOnlyError) {
2096
+ return JSON.stringify({ ok: false, error: "read_only", path });
2097
+ }
2098
+ if (err instanceof StaleFileError) {
2099
+ return JSON.stringify({ ok: false, error: "stale_file", path });
2100
+ }
2101
+ if (err instanceof FilesystemError) {
2102
+ return JSON.stringify({ ok: false, error: "write_failed", path });
2103
+ }
2104
+ throw err;
2105
+ }
1969
2106
  async function isBinaryFile(absolutePath) {
1970
2107
  let handle;
1971
2108
  try {
@@ -1988,6 +2125,6 @@ async function isBinaryFile(absolutePath) {
1988
2125
  }
1989
2126
  }
1990
2127
 
1991
- export { CatastrophicCommandError, DEFAULT_TOOL_GUIDANCE, RedirectBlockedError, SsrfBlockedError, buildEnvContext, buildRepoMap, catastrophicShellReason, commandDenialReason, createApplyPatchTool, createBraveWebSearchAdapter, createEditFileTool, createGitDiffTool, createGlobTool, createListDirTool, createPlanModeTool, createQuestionTool, createReadFileTool, createRunVitestTool, createSearchTextTool, createSessionArtifactStore, createShellTool, createTodolistTool, createWebFetchTool, createWebSearchTool, createWriteFileTool, denyCatastrophicCommands, formatCode, formatDiff, formatError, formatFileList, injectGuidance, isBlockedIp, isCommandAllowed, renderToolList, resolveAndScreen, screenedFetch, todoItemsToPlanNodes, truncateOutput, withDefaultGuidance, withDescription, withShellExitGuidance, withToolResultGuidance };
2128
+ export { CatastrophicCommandError, DEFAULT_TOOL_GUIDANCE, ReadTracker, RedirectBlockedError, SsrfBlockedError, buildEnvContext, buildRepoMap, catastrophicShellReason, commandDenialReason, createApplyPatchTool, createBraveWebSearchAdapter, createEditFileTool, createGenericHttpSearchAdapter, createGitDiffTool, createGlobTool, createListDirTool, createPlanModeTool, createQuestionTool, createReadFileTool, createRunVitestTool, createSearchTextTool, createSessionArtifactStore, createShellTool, createTodolistTool, createWebFetchTool, createWebSearchTool, createWriteFileTool, denyCatastrophicCommands, formatCode, formatDiff, formatError, formatFileList, injectGuidance, isBlockedIp, isCommandAllowed, renderToolList, resolveAndScreen, screenedFetch, todoItemsToPlanNodes, truncateOutput, withDefaultGuidance, withDescription, withShellExitGuidance, withToolResultGuidance };
1992
2129
  //# sourceMappingURL=index.js.map
1993
2130
  //# sourceMappingURL=index.js.map