@theokit/sdk 4.2.7 → 4.2.9

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 (35) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/claude-template/CLAUDE.md +16 -0
  3. package/claude-template/dot-claude/skills/theokit-auth/SKILL.md +102 -0
  4. package/claude-template/dot-claude/skills/theokit-client/SKILL.md +58 -0
  5. package/claude-template/dot-claude/skills/theokit-compaction/SKILL.md +102 -0
  6. package/claude-template/dot-claude/skills/theokit-concurrency/SKILL.md +68 -0
  7. package/claude-template/dot-claude/skills/theokit-filesystem/SKILL.md +74 -0
  8. package/claude-template/dot-claude/skills/theokit-messages/SKILL.md +58 -0
  9. package/claude-template/dot-claude/skills/theokit-models/SKILL.md +79 -0
  10. package/claude-template/dot-claude/skills/theokit-path-safety/SKILL.md +60 -0
  11. package/claude-template/dot-claude/skills/theokit-persistence/SKILL.md +85 -0
  12. package/claude-template/dot-claude/skills/theokit-project/SKILL.md +55 -0
  13. package/claude-template/dot-claude/skills/theokit-retry/SKILL.md +50 -0
  14. package/claude-template/dot-claude/skills/theokit-sandbox/SKILL.md +93 -0
  15. package/claude-template/dot-claude/skills/theokit-sanitize/SKILL.md +66 -0
  16. package/claude-template/dot-claude/skills/theokit-skills/SKILL.md +68 -0
  17. package/claude-template/dot-claude/skills/theokit-subagents/SKILL.md +109 -0
  18. package/claude-template/dot-claude/skills/theokit-task-store/SKILL.md +75 -0
  19. package/dist/a2a/index.cjs +8 -1
  20. package/dist/a2a/index.cjs.map +1 -1
  21. package/dist/a2a/index.js +8 -1
  22. package/dist/a2a/index.js.map +1 -1
  23. package/dist/cron.cjs +20 -16
  24. package/dist/cron.cjs.map +1 -1
  25. package/dist/cron.js +23 -19
  26. package/dist/cron.js.map +1 -1
  27. package/dist/eval.cjs +20 -16
  28. package/dist/eval.cjs.map +1 -1
  29. package/dist/eval.js +23 -19
  30. package/dist/eval.js.map +1 -1
  31. package/dist/index.cjs +20 -16
  32. package/dist/index.cjs.map +1 -1
  33. package/dist/index.js +20 -16
  34. package/dist/index.js.map +1 -1
  35. package/package.json +12 -11
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.2.9
4
+
5
+ ### Patch Changes
6
+
7
+ - d12634e: fix(a2a): register the `Agent` facade on a process-global `Symbol.for` slot so `SubAgent` works across build entries (#142). Each public entry (`.`, `./a2a`, `./cron`, `./eval`, …) is bundled with `tsup splitting: false`, which inlines its own copy of the internal `agent-factory-registry` — a module-level `let` gave each copy a private registration slot, so a subagent invoked through `@theokit/sdk/a2a` read a slot the `.` entry (via `agent.ts`'s `setAgentFacade`) never set, throwing `internal: Agent facade not registered` even when the main entry was loaded first. The registry now stores the facade on `globalThis[Symbol.for("theokit.internal.runtime.agentFacade")]`, so all duplicated copies share ONE registration. Adds regression tests. No public API change.
8
+
9
+ ## 4.2.8
10
+
11
+ ### Patch Changes
12
+
13
+ - feat(init-claude): the scaffolded `.claude/` template now covers **every public `@theokit/sdk` subpath**. Added 16 per-module skills — models, subagents (`/a2a` + tool-scope), retry, task-store, sandbox, compaction, messages, auth (`/server/auth` + errors-envelope), sanitize, skills, path-safety, concurrency, persistence, client, filesystem, project — each authored against the shipped type declarations (verified signatures: `Retry.create` executor, `Semaphore.create`, `SubAgent.create`, `Auth.create`, `sanitizeToolInput`, …). The `claude-template-no-drift` gate covers the expanded set.
14
+
3
15
  ## 4.2.7
4
16
 
5
17
  ### Patch Changes
@@ -24,6 +24,22 @@ These skills inject TheoKit knowledge automatically when you edit files matching
24
24
  | `theokit-config` | `.theokit/**`, `config.*`, `theo.config.*` |
25
25
  | `theokit-streaming` | `*stream*`, `*Stream*`, `*SDKMessage*` |
26
26
  | `theokit-budget` | `*budget*`, `*Budget*`, `*cost*`, `*token*` |
27
+ | `theokit-models` | `*model*`, `*Model*` |
28
+ | `theokit-subagents` | `*subagent*`, `*a2a*`, `*delegat*` |
29
+ | `theokit-retry` | `*retry*`, `*Retry*` |
30
+ | `theokit-task-store` | `*task-store*`, `*taskstore*`, `*TaskStore*` |
31
+ | `theokit-sandbox` | `*sandbox*`, `*Sandbox*` |
32
+ | `theokit-compaction` | `*compact*`, `*Compact*` |
33
+ | `theokit-messages` | `*message*`, `*Message*` |
34
+ | `theokit-auth` | `*auth*`, `*Auth*`, `*envelope*` |
35
+ | `theokit-sanitize` | `*sanitize*`, `*Sanitize*` |
36
+ | `theokit-skills` | `*skill*`, `*Skill*` |
37
+ | `theokit-path-safety` | `*path-safety*`, `*pathsafety*` |
38
+ | `theokit-concurrency` | `*concurren*`, `*semaphore*`, `*Semaphore*` |
39
+ | `theokit-persistence` | `*persist*`, `*Persist*` |
40
+ | `theokit-client` | `*client*`, `*Client*` |
41
+ | `theokit-filesystem` | `*filesystem*`, `*Filesystem*` |
42
+ | `theokit-project` | `*project*`, `*Project*` |
27
43
 
28
44
  ### Settings
29
45
 
@@ -0,0 +1,102 @@
1
+ ---
2
+ user-invocable: false
3
+ paths:
4
+ - "**/*auth*"
5
+ - "**/*Auth*"
6
+ - "**/*envelope*"
7
+ description: TheoKit SDK server auth — Auth.create orchestrator, validateReturnTo, and the cross-layer error envelope
8
+ ---
9
+
10
+ # TheoKit Server Auth
11
+
12
+ Server-side auth orchestrator and the cross-layer error envelope. These live
13
+ under the `@theokit/sdk/server/*` sub-paths (not the main barrel). Concrete
14
+ OAuth/email providers ship in opt-in `@theokit/auth-*` packages — the SDK only
15
+ defines the orchestrator contract.
16
+
17
+ ## `Auth.create` — session + provider orchestrator
18
+
19
+ ```typescript
20
+ import {
21
+ Auth,
22
+ validateReturnTo,
23
+ AuthConfigError,
24
+ AuthProviderNotFoundError,
25
+ AuthCallbackError,
26
+ AuthCancelledError,
27
+ AuthSecretTooShortError,
28
+ } from "@theokit/sdk/server/auth";
29
+ import type {
30
+ AuthProvider,
31
+ SessionManager,
32
+ AuthOrchestrator,
33
+ } from "@theokit/sdk/server/auth";
34
+ ```
35
+
36
+ `Auth.create(opts)` returns an `AuthOrchestrator<TSession>` with 5 methods.
37
+ `providers` is optional — an empty list is the manual-`signIn`-only escape hatch.
38
+
39
+ ```typescript
40
+ const auth: AuthOrchestrator<Session> = Auth.create<Session>({
41
+ session, // your SessionManager<Session> implementation
42
+ providers: [githubProvider], // AuthProvider<Profile>[] from a @theokit/auth-* package
43
+ onSignIn: async ({ profile, provider }) => {
44
+ return toSession(profile, provider); // returns the TSession to persist
45
+ },
46
+ onSignOut: async (session) => {
47
+ /* revoke, audit, etc. */
48
+ },
49
+ });
50
+
51
+ // OAuth flow (node:http req/res):
52
+ const redirect = await auth.startSignIn("github", req, { returnTo: "/dashboard" });
53
+ const { session, returnTo } = await auth.finishSignIn("github", req, res); // rotates session id (OWASP A07)
54
+ const current = await auth.getSession(req);
55
+ await auth.signOut(res);
56
+
57
+ // Escape hatch — persist a session directly, skipping the OAuth flow:
58
+ const s = await auth.signIn(externalProfile, "github", req, res);
59
+ ```
60
+
61
+ ## `validateReturnTo` — open-redirect guard (OWASP A01)
62
+
63
+ Returns a safe same-origin path. Cross-origin, protocol-relative (`//evil.com`),
64
+ empty, and defensive cases all collapse to `"/"`.
65
+
66
+ ```typescript
67
+ const safe = validateReturnTo(returnTo, new URL("https://app.example.com"));
68
+ // "/dashboard" -> kept; "https://evil.com" -> "/"; undefined -> "/"
69
+ ```
70
+
71
+ Typed errors: `AuthConfigError`, `AuthProviderNotFoundError`, `AuthCallbackError`,
72
+ `AuthCancelledError`, `AuthSecretTooShortError`.
73
+
74
+ ## Error envelope — cross-layer boundary translation
75
+
76
+ ```typescript
77
+ import {
78
+ toEnvelope,
79
+ fromEnvelope,
80
+ MemoryAdapterError,
81
+ } from "@theokit/sdk/server/errors-envelope";
82
+ import type {
83
+ TheokitErrorEnvelope,
84
+ TheokitErrorCode,
85
+ } from "@theokit/sdk/server/errors-envelope";
86
+ ```
87
+
88
+ `toEnvelope` translates any SDK error (or arbitrary thrown value) into the wire
89
+ envelope at egress; `fromEnvelope` reconstructs SDK class identity at ingress so
90
+ `instanceof` checks keep working across an IPC/serialization boundary.
91
+
92
+ ```typescript
93
+ try {
94
+ await agent.send(prompt);
95
+ } catch (err) {
96
+ const envelope: TheokitErrorEnvelope = toEnvelope(err); // { code, message, meta?, ext? }
97
+ send(envelope); // code is a TheokitErrorCode, e.g. "RATE_LIMITED"
98
+ }
99
+
100
+ // Inbound edge (e.g. worker receiving the envelope):
101
+ const restored = fromEnvelope(envelope); // a TheokitAgentError subclass
102
+ ```
@@ -0,0 +1,58 @@
1
+ ---
2
+ user-invocable: false
3
+ paths:
4
+ - "**/*client*"
5
+ - "**/*Client*"
6
+ description: TheoKit SDK low-level HTTP client — TheoKitClient (DEPRECATED; prefer the Agent façade)
7
+ ---
8
+
9
+ # TheoKit Client
10
+
11
+ `TheoKitClient` is a browser-safe, zero-Node-dependency HTTP client (native
12
+ `fetch` + manual SSE parsing) for a legacy server-adapter contract.
13
+
14
+ > DEPRECATED since 2.x — the `@theokit/sdk/client` sub-path consumes a legacy
15
+ > server-adapter HTTP contract (`POST /agent/send`, `GET /agent/stream`) that the
16
+ > ecosystem no longer produces, and will be removed in the next major. For
17
+ > in-process runs use the `Agent` façade (`@theokit/sdk`); for HTTP, use the
18
+ > framework's typed `POST /api/agents/<name>` client. Reach for this only when
19
+ > maintaining an existing integration against the old contract.
20
+
21
+ ```ts
22
+ import { TheoKitClient } from "@theokit/sdk/client";
23
+ import type { ClientOptions, SendResponse, StreamEvent } from "@theokit/sdk/client";
24
+ ```
25
+
26
+ ## Construct
27
+
28
+ The constructor takes `ClientOptions` — `baseUrl` (required), optional `basePath`
29
+ and `headers`.
30
+
31
+ ```ts
32
+ const client = new TheoKitClient({
33
+ baseUrl: "https://adapter.example.com",
34
+ basePath: "/agent", // optional
35
+ headers: { authorization: "Bearer …" }, // optional
36
+ });
37
+ ```
38
+
39
+ ## Send (one-shot)
40
+
41
+ `send(input)` POSTs and resolves a `SendResponse` (`{ status, output?, error? }`).
42
+
43
+ ```ts
44
+ const res: SendResponse = await client.send("summarize the repo");
45
+ if (res.error) throw new Error(res.error);
46
+ console.log(res.status, res.output);
47
+ ```
48
+
49
+ ## Stream (SSE)
50
+
51
+ `stream(input)` returns an `AsyncGenerator<StreamEvent>`; each `StreamEvent` has a
52
+ `type` and an optional `text` (plus arbitrary extra fields).
53
+
54
+ ```ts
55
+ for await (const event of client.stream("build the changelog")) {
56
+ if (event.type === "text" && event.text) process.stdout.write(event.text);
57
+ }
58
+ ```
@@ -0,0 +1,102 @@
1
+ ---
2
+ user-invocable: false
3
+ paths:
4
+ - "**/*compact*"
5
+ - "**/*Compact*"
6
+ description: TheoKit SDK compaction reference — compactTranscript, shouldCompact, checkpoints, context-overflow
7
+ ---
8
+
9
+ # TheoKit Compaction
10
+
11
+ Public context-management helpers. Every function is pure and never mutates its
12
+ input. A `CompressibleMessage` is `{ role: "user" | "assistant" | "system"; content: string }`.
13
+
14
+ ```typescript
15
+ import {
16
+ compactTranscript,
17
+ shouldCompact,
18
+ estimateTokens,
19
+ buildCheckpoint,
20
+ filterFromLatestCheckpoint,
21
+ isContextOverflowError,
22
+ CHECKPOINT_MARKER,
23
+ SUMMARY_TEMPLATE,
24
+ type CompactTranscriptOptions,
25
+ type ShouldCompactInput,
26
+ type CompressibleMessage,
27
+ } from "@theokit/sdk/compaction";
28
+ ```
29
+
30
+ ## Pre-call gate — `estimateTokens` + `shouldCompact`
31
+
32
+ `estimateTokens` is a tokenizer-free `ceil(text.length / 4)` heuristic — a cheap
33
+ gate, NOT exact tokenization. `shouldCompact` is pure: the caller supplies the
34
+ model's window.
35
+
36
+ ```typescript
37
+ const estimated = estimateTokens(transcript.map((m) => m.content).join("\n"));
38
+
39
+ const input: ShouldCompactInput = {
40
+ estimated,
41
+ contextWindow: 200_000,
42
+ buffer: 8_000, // headroom to reserve (output + safety margin)
43
+ maxOutput: 4_000, // optional; separate response reservation (default 0)
44
+ };
45
+
46
+ if (shouldCompact(input)) {
47
+ // compact before sending — see below
48
+ }
49
+ ```
50
+
51
+ ## `compactTranscript` — summarize the older window
52
+
53
+ Default `keepRecent` mode keeps the last N turns verbatim (default 6) and
54
+ preserves leading system prompts. The older window is summarized via the
55
+ caller-supplied `summarize` callback (or dropped if omitted). With `failSafe`, a
56
+ thrown summarizer returns the ORIGINAL transcript instead of propagating.
57
+
58
+ ```typescript
59
+ const opts: CompactTranscriptOptions = {
60
+ keepRecent: 6, // OR keepTokens: 40_000 (token-budget mode, takes precedence)
61
+ failSafe: true,
62
+ summarize: async (older: CompressibleMessage[], template: string) => {
63
+ // template is SUMMARY_TEMPLATE unless overridden via summaryTemplate
64
+ const summary = await callYourModel(template, older);
65
+ return { role: "system", content: summary };
66
+ },
67
+ };
68
+
69
+ const compacted = await compactTranscript(transcript, opts);
70
+ ```
71
+
72
+ ## Checkpoints — mark and filter
73
+
74
+ `buildCheckpoint` produces a `system` turn whose content starts with
75
+ `CHECKPOINT_MARKER`. `filterFromLatestCheckpoint` returns turns relative to the
76
+ most recent marker (`include: "after"` excludes it — the default; `"from"`
77
+ includes it).
78
+
79
+ ```typescript
80
+ const marked = [...transcript, buildCheckpoint("milestone: tests green")];
81
+
82
+ const recent = filterFromLatestCheckpoint(marked); // after (exclusive)
83
+ const withHead = filterFromLatestCheckpoint(marked, { include: "from" });
84
+ ```
85
+
86
+ ## Context-overflow detection
87
+
88
+ `isContextOverflowError` is `true` only for a `TheokitAgentError` reporting the
89
+ typed `context_too_long` code — never a brittle message regex.
90
+
91
+ ```typescript
92
+ try {
93
+ await agent.send(prompt);
94
+ } catch (err) {
95
+ if (isContextOverflowError(err)) {
96
+ const compacted = await compactTranscript(transcript, { keepRecent: 4 });
97
+ // retry with the compacted transcript
98
+ } else {
99
+ throw err;
100
+ }
101
+ }
102
+ ```
@@ -0,0 +1,68 @@
1
+ ---
2
+ user-invocable: false
3
+ description: Bound in-process parallelism with Semaphore.create and mapWithConcurrency from @theokit/sdk/concurrency.
4
+ paths:
5
+ - "**/*concurren*"
6
+ - "**/*semaphore*"
7
+ - "**/*Semaphore*"
8
+ ---
9
+
10
+ # TheoKit SDK -- Concurrency
11
+
12
+ In-house concurrency helpers (no `p-limit`/`p-map` dependency). `Semaphore.create(permits)` builds an N-permit async counting gate; `mapWithConcurrency` runs an async mapper over items with bounded parallelism while preserving input order.
13
+
14
+ ## Import
15
+
16
+ ```typescript
17
+ import { Semaphore, mapWithConcurrency } from "@theokit/sdk/concurrency";
18
+ import type { AsyncSemaphore } from "@theokit/sdk/concurrency";
19
+ ```
20
+
21
+ ## Signatures
22
+
23
+ ```typescript
24
+ class Semaphore {
25
+ static create(permits: number): AsyncSemaphore; // canonical factory (ADR 0015)
26
+ }
27
+
28
+ interface AsyncSemaphore {
29
+ acquire(): Promise<() => void>; // returns a release fn; call it exactly once
30
+ inFlight(): number; // permits currently held
31
+ pending(): number; // in-flight + queued waiters
32
+ }
33
+
34
+ function mapWithConcurrency<T, R>(
35
+ items: ReadonlyArray<T>,
36
+ concurrency: number, // positive integer; validated
37
+ fn: (item: T, index: number, signal: AbortSignal) => Promise<R>,
38
+ options?: { signal?: AbortSignal },
39
+ ): Promise<R[]>; // ordered; fail-fast; throws ConfigurationError on bad concurrency
40
+ ```
41
+
42
+ ## Semaphore -- release in a finally
43
+
44
+ ```typescript
45
+ const sem = Semaphore.create(4); // at most 4 in flight
46
+
47
+ async function guarded<T>(task: () => Promise<T>): Promise<T> {
48
+ const release = await sem.acquire();
49
+ try {
50
+ return await task();
51
+ } finally {
52
+ release(); // release exactly once (idempotent, but leaking it consumes a permit)
53
+ }
54
+ }
55
+ ```
56
+
57
+ ## mapWithConcurrency -- ordered bounded map
58
+
59
+ ```typescript
60
+ const controller = new AbortController();
61
+ const results = await mapWithConcurrency(
62
+ ["a", "b", "c"],
63
+ 2, // max 2 concurrent fetches
64
+ async (url, _index, signal) => (await fetch(url, { signal })).json(),
65
+ { signal: controller.signal },
66
+ );
67
+ // results align with input order; rejects on the first task error
68
+ ```
@@ -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
+ ```