@theokit/sdk 4.2.6 → 4.2.8

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.
Files changed (25) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/claude-template/AGENTS.md +73 -55
  3. package/claude-template/CLAUDE.md +16 -1
  4. package/claude-template/dot-claude/rules/theokit-conventions.md +2 -3
  5. package/claude-template/dot-claude/skills/theokit-agent-core/SKILL.md +3 -3
  6. package/claude-template/dot-claude/skills/theokit-auth/SKILL.md +102 -0
  7. package/claude-template/dot-claude/skills/theokit-client/SKILL.md +58 -0
  8. package/claude-template/dot-claude/skills/theokit-compaction/SKILL.md +102 -0
  9. package/claude-template/dot-claude/skills/theokit-concurrency/SKILL.md +68 -0
  10. package/claude-template/dot-claude/skills/theokit-filesystem/SKILL.md +74 -0
  11. package/claude-template/dot-claude/skills/theokit-messages/SKILL.md +58 -0
  12. package/claude-template/dot-claude/skills/theokit-models/SKILL.md +79 -0
  13. package/claude-template/dot-claude/skills/theokit-path-safety/SKILL.md +60 -0
  14. package/claude-template/dot-claude/skills/theokit-persistence/SKILL.md +85 -0
  15. package/claude-template/dot-claude/skills/theokit-project/SKILL.md +55 -0
  16. package/claude-template/dot-claude/skills/theokit-retry/SKILL.md +50 -0
  17. package/claude-template/dot-claude/skills/theokit-sandbox/SKILL.md +93 -0
  18. package/claude-template/dot-claude/skills/theokit-sanitize/SKILL.md +66 -0
  19. package/claude-template/dot-claude/skills/theokit-skills/SKILL.md +68 -0
  20. package/claude-template/dot-claude/skills/theokit-subagents/SKILL.md +109 -0
  21. package/claude-template/dot-claude/skills/theokit-subscriptions/SKILL.md +6 -6
  22. package/claude-template/dot-claude/skills/theokit-task-store/SKILL.md +75 -0
  23. package/claude-template/dot-claude/skills/theokit-tools/SKILL.md +9 -9
  24. package/package.json +1 -1
  25. package/claude-template/dot-claude/skills/theokit-rag/SKILL.md +0 -226
@@ -0,0 +1,74 @@
1
+ ---
2
+ user-invocable: false
3
+ paths:
4
+ - "**/*filesystem*"
5
+ - "**/*Filesystem*"
6
+ description: TheoKit SDK filesystem seam — FilesystemBackend, LocalFilesystem, boundary-enforced storage with read-before-write safety
7
+ ---
8
+
9
+ # TheoKit Filesystem
10
+
11
+ A pluggable, boundary-enforced file *storage* provider for agent file tools —
12
+ the storage-side twin of the sandbox seam. Ship a `FilesystemBackend` (default
13
+ `LocalFilesystem`) with an optional `readOnly` flag and a per-request resolver.
14
+
15
+ ```ts
16
+ import {
17
+ LocalFilesystem, FilesystemBackend, resolveFilesystem,
18
+ } from "@theokit/sdk/filesystem";
19
+ import type {
20
+ FilesystemProvider, FilesystemConfig, FileStat, WriteFileOptions,
21
+ } from "@theokit/sdk/filesystem";
22
+ import {
23
+ FileNotFoundError, FilesystemError,
24
+ FilesystemReadOnlyError, FilesystemSecurityError, StaleFileError,
25
+ } from "@theokit/sdk/filesystem";
26
+ ```
27
+
28
+ ## LocalFilesystem
29
+
30
+ Boundary-enforced over `node:fs/promises`. Every path resolves within `basePath`;
31
+ traversal / symlink escapes are rejected with `FilesystemSecurityError`. NOT an
32
+ isolation boundary — run untrusted code inside a container/VM.
33
+
34
+ ```ts
35
+ const fs = new LocalFilesystem({ basePath: "/workspace", readOnly: false });
36
+
37
+ const stat: FileStat = await fs.writeFile("notes.md", "# hello"); // returns FileStat
38
+ const text = await fs.readFile("notes.md");
39
+ const names = await fs.list("."); // entry names, not recursive
40
+ if (await fs.exists("notes.md")) { /* … */ }
41
+ ```
42
+
43
+ ## Read-before-write safety (SE32)
44
+
45
+ `stat().mtimeMs` is the oracle. Pass `expectedMtime` so a concurrent change makes
46
+ the write fail with `StaleFileError` instead of silently clobbering.
47
+
48
+ ```ts
49
+ const before = await fs.stat("notes.md");
50
+ try {
51
+ await fs.writeFile("notes.md", updated, { expectedMtime: before.mtimeMs });
52
+ } catch (err) {
53
+ if (err instanceof StaleFileError) { /* someone changed it — re-read + merge */ }
54
+ }
55
+ ```
56
+
57
+ ## Per-request provider (multi-tenant)
58
+
59
+ A `FilesystemProvider` is a backend OR a resolver `(ctx) => backend` run at
60
+ tool-execution time, giving each request its own root. `resolveFilesystem`
61
+ collapses either form to a concrete backend.
62
+
63
+ ```ts
64
+ const provider: FilesystemProvider<{ tenant: string }> = (ctx) =>
65
+ new LocalFilesystem({ basePath: `/data/${ctx.tenant}` });
66
+
67
+ const backend = await resolveFilesystem(provider, { tenant: "acme" });
68
+ ```
69
+
70
+ ## Custom backend
71
+
72
+ Extend `FilesystemBackend` and implement `readFile` / `writeFile` / `stat` /
73
+ `list`; `exists()`, `readOnly`, and `basePath` derive on the base class. Map raw
74
+ Node errors to the typed errors above (`FileNotFoundError`, `FilesystemError`).
@@ -0,0 +1,58 @@
1
+ ---
2
+ user-invocable: false
3
+ paths:
4
+ - "**/*message*"
5
+ - "**/*Message*"
6
+ description: TheoKit SDK message readers — assistantText, extractToolUses, costAmountUsd over the SDKMessage stream
7
+ ---
8
+
9
+ # TheoKit Messages
10
+
11
+ Pure readers over the `SDKMessage` stream — no I/O, no mutation, deterministic.
12
+ Use these instead of re-implementing a wire-event mapper.
13
+
14
+ ```typescript
15
+ import { assistantText, extractToolUses, costAmountUsd } from "@theokit/sdk/messages";
16
+ import type { SDKMessage, ToolUseBlock } from "@theokit/sdk";
17
+ import type { CostBreakdown } from "@theokit/sdk";
18
+ ```
19
+
20
+ ## `assistantText(msg)` — concatenate assistant text blocks
21
+
22
+ Returns `""` for any non-assistant message (or an assistant with no text
23
+ blocks). `tool_use` blocks are ignored — only `text` blocks contribute.
24
+
25
+ ```typescript
26
+ for await (const event of run.stream()) {
27
+ // event is an SDKMessage; assistantText is safe on any variant
28
+ const text = assistantText(event);
29
+ if (text) process.stdout.write(text);
30
+ }
31
+ ```
32
+
33
+ ## `extractToolUses(msg)` — read assistant `ToolUseBlock`s
34
+
35
+ Returns `[]` for any non-assistant message. This reads the assistant message's
36
+ content blocks — NOT the separate `tool_call` lifecycle event (a different
37
+ stream). Tool `input` is `unknown`; parse it defensively.
38
+
39
+ ```typescript
40
+ const uses: ToolUseBlock[] = extractToolUses(event);
41
+ for (const use of uses) {
42
+ console.log(use.name, use.id); // use.input is `unknown` — validate before use
43
+ }
44
+ ```
45
+
46
+ ## `costAmountUsd(cost)` — honesty-preserving cost read
47
+
48
+ Returns `number | undefined`. `undefined` means "cost unknown" — distinct from a
49
+ real `$0` (e.g. a subscription-included route). NEVER coerced to 0.
50
+
51
+ ```typescript
52
+ const amount = costAmountUsd(cost); // cost: CostBreakdown | undefined
53
+ if (amount === undefined) {
54
+ console.log("cost unknown");
55
+ } else {
56
+ console.log(`$${amount.toFixed(4)}`);
57
+ }
58
+ ```
@@ -0,0 +1,79 @@
1
+ ---
2
+ user-invocable: false
3
+ paths:
4
+ - "**/*model*"
5
+ - "**/*Model*"
6
+ description: TheoKit SDK model helpers — @theokit/sdk/models (parseModelId, resolveModelCapabilities, toModelOption, humanizeModelName); API catalog via Theokit.models.list()
7
+ ---
8
+
9
+ # TheoKit Models
10
+
11
+ Pure, sync, offline helpers for model ids. No network — these read a static catalog
12
+ and parse strings. For the live API-backed catalog, use `Theokit.models.list()` from
13
+ the main `@theokit/sdk` barrel.
14
+
15
+ ```typescript
16
+ import {
17
+ parseModelId,
18
+ resolveModelCapabilities,
19
+ toModelOption,
20
+ humanizeModelName,
21
+ type ModelCapabilities,
22
+ type ModelOption,
23
+ type ParsedModelId,
24
+ } from "@theokit/sdk/models";
25
+ ```
26
+
27
+ ## Parse a model id
28
+
29
+ `parseModelId` splits on the first `/` into `{ provider, name }`. No `/` means
30
+ `provider` is `undefined` (so callers can fall back to env-var detection). Tag
31
+ suffixes like `:3b` / `:latest` stay part of `name`.
32
+
33
+ ```typescript
34
+ const a: ParsedModelId = parseModelId("anthropic/claude-3-5-sonnet");
35
+ // { provider: "anthropic", name: "claude-3-5-sonnet" }
36
+
37
+ const b = parseModelId("openrouter/meta-llama/llama-3");
38
+ // { provider: "openrouter", name: "meta-llama/llama-3" } (embedded slash kept)
39
+
40
+ const c = parseModelId("claude-sonnet-4-6");
41
+ // { provider: undefined, name: "claude-sonnet-4-6" }
42
+ ```
43
+
44
+ ## Gate features by capability
45
+
46
+ `resolveModelCapabilities` returns typed flags + token limits from an OFFLINE
47
+ catalog. It strips routing prefixes (`openrouter/`/`vertex/`/`bedrock/`) and the
48
+ OpenRouter `:variant` suffix before lookup; unknown models get conservative defaults.
49
+
50
+ ```typescript
51
+ const caps: ModelCapabilities = resolveModelCapabilities("anthropic/claude-3-5-sonnet");
52
+ // { supportsVision, supportsStructuredOutput, supportsToolUse,
53
+ // supportsCacheControl, maxContextTokens, maxOutputTokens }
54
+
55
+ if (!caps.supportsVision) {
56
+ throw new Error("This model cannot accept images");
57
+ }
58
+ ```
59
+
60
+ ## Build dropdown options
61
+
62
+ `humanizeModelName` produces a best-effort label; `toModelOption` composes it with
63
+ `parseModelId` into `{ value, label, provider }`.
64
+
65
+ ```typescript
66
+ humanizeModelName("anthropic/claude-3-5-sonnet"); // "Claude 3 5 Sonnet"
67
+
68
+ const opt: ModelOption = toModelOption("openrouter/meta-llama/llama-3");
69
+ // { value: "openrouter/meta-llama/llama-3", label: "...", provider: "openrouter" }
70
+ ```
71
+
72
+ ## Live catalog (API-backed)
73
+
74
+ ```typescript
75
+ import { Theokit } from "@theokit/sdk";
76
+
77
+ const models = await Theokit.models.list({ apiKey: process.env.THEOKIT_API_KEY });
78
+ const options: ModelOption[] = models.map((m) => toModelOption(m.id));
79
+ ```
@@ -0,0 +1,60 @@
1
+ ---
2
+ user-invocable: false
3
+ description: Path-traversal, symlink-escape, and forbidden-path guards from @theokit/sdk/path-safety for agent file I/O.
4
+ paths:
5
+ - "**/*path-safety*"
6
+ - "**/*pathsafety*"
7
+ ---
8
+
9
+ # TheoKit SDK -- Path Safety
10
+
11
+ TOCTOU-safe path primitives to wire wherever user input becomes a filesystem path in a custom tool. `safePathJoin` resolves then prefix-checks; `assertNoSymlinkEscape` resolves the whole symlink chain via `realpathSync`; `isForbiddenPath` blocks sensitive files even when they are lexically inside the project.
12
+
13
+ ## Import
14
+
15
+ ```typescript
16
+ import {
17
+ safePathJoin,
18
+ assertNoSymlinkEscape,
19
+ isForbiddenPath,
20
+ safeFilenameForId,
21
+ sanitizeIdentifier,
22
+ PathTraversalError,
23
+ ForbiddenPathError,
24
+ } from "@theokit/sdk/path-safety";
25
+ ```
26
+
27
+ ## Signatures
28
+
29
+ ```typescript
30
+ function safePathJoin(base: string, ...parts: string[]): string; // throws PathTraversalError on escape
31
+ function assertNoSymlinkEscape(path: string, base: string): void; // throws PathTraversalError on symlink escape
32
+ function isForbiddenPath(input: string): boolean; // true for .env*, .git/, node_modules/, .theo/, lock files
33
+ function safeFilenameForId(id: string, options?: { maxLen?: number }): string; // total: passthrough or h-<16hex>
34
+ function sanitizeIdentifier(input: string, options?: { maxLen?: number }): string; // grammar ^[a-z0-9][a-z0-9-_]*$
35
+
36
+ class PathTraversalError extends ConfigurationError { constructor(input: string, resolvedPath: string); } // code "path_traversal"
37
+ class ForbiddenPathError extends ConfigurationError { constructor(path: string); } // code "forbidden_path"
38
+ ```
39
+
40
+ ## Guard a tool's file read
41
+
42
+ ```typescript
43
+ const projectRoot = process.cwd();
44
+
45
+ function resolveUserPath(userPath: string): string {
46
+ if (isForbiddenPath(userPath)) throw new ForbiddenPathError(userPath);
47
+ const safe = safePathJoin(projectRoot, userPath); // throws PathTraversalError on "../" escape
48
+ assertNoSymlinkEscape(safe, projectRoot); // throws PathTraversalError on symlink escape
49
+ return safe;
50
+ }
51
+ ```
52
+
53
+ ## Derive a safe filename from an opaque id
54
+
55
+ `safeFilenameForId` never throws on a non-empty string: it returns the id verbatim when it already matches the safe grammar, otherwise a deterministic `h-<16 hex>` sha256 token.
56
+
57
+ ```typescript
58
+ safeFilenameForId("550e8400-e29b-41d4-a716-446655440000"); // passthrough
59
+ safeFilenameForId("user@example.com"); // "h-<16hex>"
60
+ ```
@@ -0,0 +1,85 @@
1
+ ---
2
+ user-invocable: false
3
+ paths:
4
+ - "**/*persist*"
5
+ - "**/*Persist*"
6
+ - "**/*.jsonl"
7
+ - "**/*sqlite*"
8
+ description: TheoKit SDK persistence primitives — atomic writes, JSONL persist/resume, resilient SQLite, cross-process locks
9
+ ---
10
+
11
+ # TheoKit Persistence
12
+
13
+ Durable, crash-safe persistence helpers on the STABLE `@theokit/sdk/persistence`
14
+ sub-path. Do NOT import them from `@theokit/sdk/internal/persistence` (semver-exempt).
15
+
16
+ ```ts
17
+ import {
18
+ // atomic writes
19
+ replaceFileAtomic, atomicWriteText, atomicWriteJson,
20
+ // jsonl persist/resume
21
+ appendJsonl, loadJsonl, readJsonlIds, JsonlParseError,
22
+ // resilient sqlite
23
+ openSqliteResilient, applyWalWithFallback, isCorruptionError, sanitizeFts5Query,
24
+ // locks
25
+ withFileLock, withCwdMutex,
26
+ // misc
27
+ PersistenceSchema, transcriptPath, encodeProjectDir,
28
+ } from "@theokit/sdk/persistence";
29
+ ```
30
+
31
+ ## Atomic writes
32
+
33
+ Crash mid-write leaves either the old file intact or the new file complete —
34
+ never a half-written file (temp + fsync + rename). `atomicWriteJson` /
35
+ `atomicWriteText` auto-`mkdir` the parent dir; `replaceFileAtomic` does not.
36
+
37
+ ```ts
38
+ await replaceFileAtomic("/data/config.md", markdown); // (filePath, content)
39
+ await atomicWriteText("/data/notes.txt", text); // + auto-mkdir
40
+ await atomicWriteJson("/data/state.json", { runs: 3 }); // default indent 2, trailing \n
41
+ await atomicWriteJson("/data/state.json", data, { indent: 0, trailingNewline: false });
42
+ ```
43
+
44
+ ## JSONL persist / resume
45
+
46
+ `appendJsonl` writes one `\n`-terminated line per record (synchronous, interleave-safe
47
+ within a process). Resume a crashed batch by reading already-persisted keys.
48
+
49
+ ```ts
50
+ appendJsonl("/data/results.jsonl", { id: "task-1", status: "passed" });
51
+
52
+ // Skip rows already done (partial trailing line tolerated; missing file -> empty set)
53
+ const done = readJsonlIds("/data/results.jsonl", (row) =>
54
+ row.status === "passed" ? String(row.id) : undefined,
55
+ );
56
+
57
+ // loadJsonl throws JsonlParseError (with 1-based .line) on a malformed line
58
+ const rows = loadJsonl<{ id: string }>("/data/results.jsonl");
59
+ ```
60
+
61
+ ## Resilient SQLite
62
+
63
+ `openSqliteResilient` loads the driver, applies WAL (with DELETE fallback), and
64
+ recovers from corruption by renaming the file aside. Apply schema in `onOpen`.
65
+
66
+ ```ts
67
+ const db = await openSqliteResilient({
68
+ filePath: "/data/index.db",
69
+ label: "memory-index",
70
+ onOpen: (db) => db.exec("CREATE TABLE IF NOT EXISTS docs (id TEXT PRIMARY KEY)"),
71
+ });
72
+ applyWalWithFallback(db, "memory-index"); // { mode: "wal"|"delete", fellBack }
73
+ const q = sanitizeFts5Query("error-code"); // "" means caller must short-circuit
74
+ ```
75
+
76
+ ## Locks
77
+
78
+ `withFileLock` takes a cross-process lock (needs the `proper-lockfile` peer dep;
79
+ falls back to in-process `withCwdMutex` with a warning). `withCwdMutex` serializes
80
+ by key within one process.
81
+
82
+ ```ts
83
+ await withFileLock("/data/state.json", async () => { /* exclusive section */ });
84
+ await withCwdMutex("migrate-config", async () => { /* in-process critical section */ });
85
+ ```
@@ -0,0 +1,55 @@
1
+ ---
2
+ user-invocable: false
3
+ paths:
4
+ - "**/*project*"
5
+ - "**/*Project*"
6
+ - "**/THEO.md"
7
+ description: TheoKit SDK project-instruction reader/writer — hierarchical walk-up discovery + atomic write of THEO.md
8
+ ---
9
+
10
+ # TheoKit Project Instructions
11
+
12
+ Hierarchical project-instruction reader/writer on `@theokit/sdk/project`. The
13
+ reader walks up from `cwd` collecting an instruction file (default `THEO.md`) and
14
+ NEVER throws; the writer writes it atomically and FAILS LOUD on error.
15
+
16
+ ```ts
17
+ import {
18
+ readProjectInstructions, writeProjectInstructions,
19
+ } from "@theokit/sdk/project";
20
+ import type {
21
+ ProjectInstructions, ProjectInstructionFile, ProjectInstructionScope,
22
+ ReadProjectInstructionsOptions, WriteProjectInstructionsOptions,
23
+ } from "@theokit/sdk/project";
24
+ ```
25
+
26
+ ## Read (walk-up, never throws)
27
+
28
+ Discovers every `<dir>/<filename>` from `cwd` up to the filesystem root (or
29
+ `stopDir`). Returns `files` nearest-first plus a `content` reduction chosen by
30
+ `scope` (`"nearest"` → innermost file; `"merged"` → all joined root-first).
31
+ No file found → `{ files: [], content: undefined }`.
32
+
33
+ ```ts
34
+ const result: ProjectInstructions = await readProjectInstructions(process.cwd(), {
35
+ filename: "THEO.md", // default "THEO.md"
36
+ scope: "merged", // default "nearest"
37
+ stopDir: "/home/me/repo",
38
+ });
39
+
40
+ if (result.content) applySystemPrompt(result.content);
41
+ for (const f of result.files) console.log(f.path, f.content.length);
42
+ ```
43
+
44
+ ## Write (atomic, fails loud)
45
+
46
+ Writes `<cwd>/<filename>` via temp + fsync + rename. An unsafe `filename`
47
+ (traversal, separators, absolute) is rejected with `ConfigurationError`
48
+ (`code: "unsafe_filename"`); a real write error (e.g. missing parent dir)
49
+ propagates — never silently swallowed.
50
+
51
+ ```ts
52
+ await writeProjectInstructions(process.cwd(), "# Project rules\n…", {
53
+ filename: "THEO.md", // default "THEO.md"
54
+ });
55
+ ```
@@ -0,0 +1,50 @@
1
+ ---
2
+ user-invocable: false
3
+ paths:
4
+ - "**/*retry*"
5
+ - "**/*Retry*"
6
+ description: TheoKit SDK retry primitive — @theokit/sdk/retry (Retry.create executor with exponential backoff + full jitter, RetryOptions)
7
+ ---
8
+
9
+ # TheoKit Retry
10
+
11
+ `@theokit/sdk/retry` exposes one primitive: `Retry`. NOTE — `Retry.create` is an
12
+ EXECUTOR, not a constructor. `Retry.create(fn, opts)` RUNS `fn` with retry/backoff and
13
+ resolves to its result (`Promise<T>`) — it does not return a `Retry` instance.
14
+
15
+ ```typescript
16
+ import { Retry, type RetryOptions } from "@theokit/sdk/retry";
17
+ ```
18
+
19
+ ## Run a fn with retry
20
+
21
+ Exponential backoff with full jitter. The default `isRetryable` predicate is the SDK's
22
+ own `isTransientError`, so retries follow the SDK's error classification (rate-limit /
23
+ network retry; business-rule violations do not).
24
+
25
+ ```typescript
26
+ const data = await Retry.create(() => fetchJson(url));
27
+ // retries transient failures up to 3 times (4 attempts total), then throws the last error
28
+ ```
29
+
30
+ ## Tuning with `RetryOptions`
31
+
32
+ All fields are optional; defaults shown in comments.
33
+
34
+ ```typescript
35
+ const result = await Retry.create(
36
+ () => callFlakyApi(),
37
+ {
38
+ retries: 5, // retries AFTER the first attempt (default 3)
39
+ initialDelayMs: 200, // base backoff for the first retry (default 100)
40
+ maxDelayMs: 10_000, // cap per single sleep (default 30_000)
41
+ backoffMultiplier: 2, // exponential multiplier per retry (default 2)
42
+ isRetryable: (err) => err instanceof NetworkGlitch, // default: isTransientError
43
+ signal: controller.signal, // aborts the backoff loop when triggered
44
+ } satisfies RetryOptions,
45
+ );
46
+ ```
47
+
48
+ `rng` and `sleep` are also injectable — override them for deterministic tests (no real
49
+ timers). When retries are exhausted or the error is not retryable, `Retry.create`
50
+ re-throws the last error.
@@ -0,0 +1,93 @@
1
+ ---
2
+ user-invocable: false
3
+ paths:
4
+ - "**/*sandbox*"
5
+ - "**/*Sandbox*"
6
+ description: TheoKit SDK sandbox reference — SandboxBackend / LocalSandbox execution + provisionRepo
7
+ ---
8
+
9
+ # TheoKit Sandbox
10
+
11
+ ```typescript
12
+ import {
13
+ LocalSandbox,
14
+ provisionRepo,
15
+ SandboxBackend,
16
+ SandboxNotAvailableError,
17
+ SandboxSecurityError,
18
+ RepoProvisionError,
19
+ type ExecuteResult,
20
+ type SandboxConfig,
21
+ type ProvisionRepoOptions,
22
+ } from "@theokit/sdk/sandbox";
23
+ ```
24
+
25
+ ## `LocalSandbox` — subprocess execution
26
+
27
+ `LocalSandbox` runs commands via `/bin/sh -c` in the SAME OS as the host. It is
28
+ **NOT an isolation boundary** — no process, filesystem, or network isolation.
29
+ Its only safety affordances are a wall-clock timeout, an output-size cap, and
30
+ env scrubbing. For real isolation of untrusted code, use a container/VM backend.
31
+
32
+ ```typescript
33
+ const sandbox = new LocalSandbox({
34
+ workDir: "/tmp/work",
35
+ timeoutMs: 5_000,
36
+ maxOutputBytes: 1024,
37
+ // env defaults to "inherit-scrubbed": drops *KEY* / *SECRET* / *TOKEN* /
38
+ // *PASSWORD* / *_AUTH* from the child. Pass "all" or "core" to change.
39
+ });
40
+
41
+ const result: ExecuteResult = await sandbox.execute("echo hi", { timeoutMs: 2_000 });
42
+ console.log(result.stdout, result.exitCode, result.timedOut);
43
+
44
+ // Derived helpers on the base class (no need to reimplement):
45
+ await sandbox.writeFile("a.txt", "content");
46
+ const text = await sandbox.readFile("a.txt");
47
+ const files = await sandbox.glob("**/*.ts");
48
+ const hits = await sandbox.grep("TODO", "src");
49
+ ```
50
+
51
+ ## Custom backend — implement only 2 abstract methods
52
+
53
+ New backends (Docker, Firecracker, E2B) extend `SandboxBackend` and implement
54
+ only `execute` + `uploadFile`; `readFile` / `writeFile` / `glob` / `grep` /
55
+ `listDir` are derived on the base class.
56
+
57
+ ```typescript
58
+ class MyBackend extends SandboxBackend {
59
+ async execute(command: string, opts?: { timeoutMs?: number }): Promise<ExecuteResult> {
60
+ // run command remotely; return { stdout, stderr, exitCode, timedOut }
61
+ throw new SandboxNotAvailableError("backend offline");
62
+ }
63
+ async uploadFile(path: string, content: string | Buffer): Promise<void> {
64
+ /* ... */
65
+ }
66
+ }
67
+ ```
68
+
69
+ `SandboxSecurityError` (`code: "sandbox_security"`) and
70
+ `SandboxNotAvailableError` (`code: "sandbox_not_available"`) both extend `Error`.
71
+
72
+ ## `provisionRepo` — clone + checkout into the sandbox
73
+
74
+ Clones `repoUrl` into `<workDir>/<instanceId>` and checks out `ref`, running
75
+ every git command through the sandbox. The `sandbox` argument is optional — when
76
+ omitted a default `LocalSandbox` (process cwd) is used.
77
+
78
+ ```typescript
79
+ const opts: ProvisionRepoOptions = {
80
+ repoUrl: "https://github.com/acme/repo.git",
81
+ ref: "main", // branch, tag, or SHA (rejected if it begins with "-")
82
+ instanceId: "task-001", // validated to [A-Za-z0-9._-] — becomes a dir name
83
+ };
84
+
85
+ const { repoDir } = await provisionRepo(new LocalSandbox({ workDir: "/tmp/work" }), opts);
86
+ // or, with the default LocalSandbox: await provisionRepo(opts);
87
+
88
+ try {
89
+ await provisionRepo({ repoUrl: "bad", ref: "main", instanceId: "x" });
90
+ } catch (err) {
91
+ if (err instanceof RepoProvisionError) console.error(err.instanceId, err.message);
92
+ }
93
+ ```
@@ -0,0 +1,66 @@
1
+ ---
2
+ user-invocable: false
3
+ description: Clean model-emitted tool arguments with sanitizeToolInput from @theokit/sdk/sanitize (total, never throws).
4
+ paths:
5
+ - "**/*sanitize*"
6
+ - "**/*Sanitize*"
7
+ ---
8
+
9
+ # TheoKit SDK -- Sanitize
10
+
11
+ `sanitizeToolInput` cleans the raw arguments a model emitted for a tool call: trim (default on), optionally coerce string values toward their expected type, optionally repair malformed JSON. It is pure, synchronous, and TOTAL -- it NEVER throws (non-object input is returned unchanged) and never changes a value's meaning, only its hygiene/representation.
12
+
13
+ ## Import
14
+
15
+ ```typescript
16
+ import { sanitizeToolInput } from "@theokit/sdk/sanitize";
17
+ import type { SanitizeOptions, SanitizeResult } from "@theokit/sdk/sanitize";
18
+ ```
19
+
20
+ ## Signature
21
+
22
+ ```typescript
23
+ function sanitizeToolInput(
24
+ input: Record<string, unknown>,
25
+ options?: SanitizeOptions,
26
+ ): SanitizeResult;
27
+
28
+ interface SanitizeOptions {
29
+ trim?: boolean; // trim whitespace on string values. Default true
30
+ coerce?: boolean; // "5"->5, "true"->true, "null"->null, JSON. Default false
31
+ repairJson?: boolean; // repair-then-parse malformed JSON strings (jsonrepair). Default false
32
+ schema?: ZodType; // when a z.object(...), coercion is schema-aware per top-level field
33
+ deep?: boolean; // recurse into nested objects/arrays. Default false (shallow)
34
+ maxDepth?: number; // max recursion depth when deep. Default 8
35
+ }
36
+
37
+ interface SanitizeResult<T = Record<string, unknown>> {
38
+ value: T; // the sanitized copy
39
+ changed: boolean; // true when any value was altered
40
+ notes: string[]; // one human-readable line per change (for logging)
41
+ }
42
+ ```
43
+
44
+ ## Trim only (default)
45
+
46
+ ```typescript
47
+ const { value, changed, notes } = sanitizeToolInput({ query: " hello " });
48
+ // value -> { query: "hello" }, changed -> true, notes -> ["query: trimmed whitespace"]
49
+ ```
50
+
51
+ ## Schema-aware coercion inside a tool handler
52
+
53
+ Pass the tool's Zod object schema so a `z.string()` field keeps `"5"` as a string while a `z.number()` field coerces it. Never throws, so it is safe to run before `schema.parse`.
54
+
55
+ ```typescript
56
+ import { z } from "zod";
57
+
58
+ const inputSchema = z.object({ count: z.number(), label: z.string() });
59
+
60
+ const { value } = sanitizeToolInput(
61
+ { count: "5", label: "42" },
62
+ { coerce: true, schema: inputSchema },
63
+ );
64
+ // value -> { count: 5, label: "42" } (label stays a string; count coerced)
65
+ const parsed = inputSchema.parse(value);
66
+ ```