@dotcms/ai 26.10.20-1 → 26.10.21-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +203 -29
  2. package/adapter.cjs.default.js +1 -0
  3. package/adapter.cjs.js +14 -0
  4. package/adapter.cjs.mjs +2 -0
  5. package/adapter.cjs2.js +0 -0
  6. package/adapter.d.ts +1 -0
  7. package/adapter.esm.js +2 -0
  8. package/adapter.esm2.js +0 -0
  9. package/errors.cjs.js +192 -0
  10. package/errors.esm.js +180 -0
  11. package/format-result.cjs.js +833 -0
  12. package/format-result.esm.js +825 -0
  13. package/package.json +25 -20
  14. package/runtime.cjs.js +0 -0
  15. package/runtime.esm.js +0 -0
  16. package/sandbox.cjs.default.js +1 -0
  17. package/sandbox.cjs.js +69 -0
  18. package/sandbox.cjs.mjs +2 -0
  19. package/sandbox.d.ts +1 -0
  20. package/sandbox.esm.js +50 -0
  21. package/spec.cjs.default.js +1 -0
  22. package/spec.cjs.js +3 -23912
  23. package/spec.cjs.mjs +2 -0
  24. package/spec.cjs2.js +23916 -0
  25. package/spec.d.ts +1 -0
  26. package/spec.esm.js +1 -23914
  27. package/spec.esm2.js +23914 -0
  28. package/src/adapter/context.d.ts +11 -0
  29. package/src/adapter/request-core.d.ts +30 -0
  30. package/src/adapter.d.ts +1 -0
  31. package/src/runtime.d.ts +5 -4
  32. package/src/sandbox/errors.d.ts +23 -1
  33. package/src/sandbox/index.d.ts +1 -1
  34. package/src/sandbox.d.ts +1 -0
  35. package/src/spec.d.ts +1 -0
  36. package/src/tools/definitions/download-assets.d.ts +27 -0
  37. package/src/tools/definitions/execute.d.ts +15 -0
  38. package/src/tools/definitions/page-create.d.ts +23 -0
  39. package/src/tools/definitions/page-place-content.d.ts +32 -0
  40. package/src/tools/definitions/page-verify.d.ts +20 -0
  41. package/src/tools/definitions/search.d.ts +13 -0
  42. package/src/tools/definitions/upload-assets.d.ts +23 -0
  43. package/src/tools/index.d.ts +40 -0
  44. package/src/tools/operations/download-assets.d.ts +55 -0
  45. package/src/tools/operations/page-create.d.ts +81 -0
  46. package/src/tools/operations/page-place-content.d.ts +102 -0
  47. package/src/tools/operations/page-verify.d.ts +164 -0
  48. package/src/tools/operations/shared/asset-common.d.ts +28 -0
  49. package/src/tools/operations/shared/glob.d.ts +22 -0
  50. package/src/tools/operations/shared/local-root.d.ts +11 -0
  51. package/src/tools/operations/shared/page-common.d.ts +45 -0
  52. package/src/tools/operations/shared/page-path.d.ts +49 -0
  53. package/src/tools/operations/shared/resolve.d.ts +43 -0
  54. package/src/tools/operations/upload-assets.d.ts +64 -0
  55. package/src/tools/toolkit/connection.d.ts +56 -0
  56. package/src/tools/toolkit/create-tool.d.ts +66 -0
  57. package/src/tools/toolkit/endpoints.d.ts +52 -0
  58. package/src/tools/toolkit/lenient-boolean.d.ts +14 -0
  59. package/src/tools/toolkit/results.d.ts +32 -0
  60. package/src/tools/toolkit/tool-runtime.d.ts +108 -0
  61. package/src/tools/toolkit/types.d.ts +120 -0
  62. package/src/tools.d.ts +1 -0
  63. package/tools.cjs.default.js +1 -0
  64. package/tools.cjs.js +3850 -0
  65. package/tools.cjs.mjs +2 -0
  66. package/tools.d.ts +1 -0
  67. package/tools.esm.js +3817 -0
  68. package/index.cjs.default.js +0 -1
  69. package/index.cjs.js +0 -7
  70. package/index.cjs.mjs +0 -2
  71. package/index.d.ts +0 -1
  72. package/index.esm.js +0 -1
package/README.md CHANGED
@@ -12,7 +12,7 @@ Safety isn't a setting you turn on; it's the shape of the runtime:
12
12
 
13
13
  - **Your token never enters the sandbox.** Auth is injected on the host side; the executing code cannot read it.
14
14
  - **Adapters are the only way out.** Sandbox code reaches the network/host *only* through an adapter you grant — direct `fetch`/`require`/`process.env` are removed.
15
- - **You decide the surface.** An allow-list (or typed `defineAdapter` operations) bounds what any code — model-written or not — can reach. Expose `scan` and `read`; never expose `delete`.
15
+ - **You decide the surface.** An allow-list (or typed `defineAdapter` operations) bounds what any code — model-written or not — can reach. Expose `scan` and `read`; never expose `delete`. The allow-list judges the path that is actually requested, after dot-segments and encoded dots resolve, so `/allowed/../elsewhere` cannot slip past it. It also matches whole path segments: allowing `/api/v1/content` allows that endpoint and everything under it, not `/api/v1/contenttype`. An entry that is not an absolute path (`''`, blank, relative) throws when the runtime or tool is created, so a typo fails closed instead of opening the API; `'/'` is the explicit way to allow every path.
16
16
 
17
17
  ## Install
18
18
 
@@ -22,31 +22,19 @@ npm install @dotcms/ai
22
22
 
23
23
  ## Which SDK Version Should I Use?
24
24
 
25
- dotCMS SDKs are published in lockstep with dotCMS itself: every `@dotcms/*` package ships
26
- at the **exact same version number** as the dotCMS release it was built for (e.g. dotCMS
27
- `26.7.14-1` → `@dotcms/client@26.7.14-1`, `@dotcms/react@26.7.14-1`, and so on).
25
+ dotCMS SDKs are published in lockstep with dotCMS itself: every `@dotcms/*` package ships at the **exact same version number** as the dotCMS release it was built for (e.g. dotCMS `26.7.14-1` → `@dotcms/client@26.7.14-1`, `@dotcms/react@26.7.14-1`, and so on).
28
26
 
29
27
  **Simple rule of thumb: use the SDK version that matches your dotCMS instance's version.**
30
28
 
31
- You don't have to upgrade the SDK every time dotCMS releases a new version (or vice versa).
32
- Most releases don't change anything the SDKs rely on, so an older SDK usually keeps working
33
- fine against a newer dotCMS instance. Occasionally, though, a release does include a real
34
- breaking change — and if your SDK is older than that point, it will stop working correctly.
29
+ You don't have to upgrade the SDK every time dotCMS releases a new version (or vice versa). Most releases don't change anything the SDKs rely on, so an older SDK usually keeps working fine against a newer dotCMS instance. Occasionally, though, a release does include a real breaking change — and if your SDK is older than that point, it will stop working correctly.
35
30
 
36
- You don't need to track this yourself: your dotCMS instance always knows the oldest SDK
37
- version it still supports, and the SDK checks itself against it automatically. If you're
38
- using an SDK that's too old, you'll see a clear warning in your console telling you to
39
- upgrade.
31
+ You don't need to track this yourself: your dotCMS instance always knows the oldest SDK version it still supports, and the SDK checks itself against it automatically. If you're using an SDK that's too old, you'll see a clear warning in your console telling you to upgrade.
40
32
 
41
- **Recommendation:** pin your SDKs to the same version as your dotCMS instance, and only bump
42
- them when you upgrade dotCMS — or when the console tells you to.
33
+ **Recommendation:** pin your SDKs to the same version as your dotCMS instance, and only bump them when you upgrade dotCMS — or when the console tells you to.
43
34
 
44
- > **On an LTS release?** LTS releases don't currently get their own matching SDK version.
45
- > Until that's addressed, use the SDK version published for the closest regular release at
46
- > or before your LTS version.
35
+ > **On an LTS release?** LTS releases don't currently get their own matching SDK version. Until that's addressed, use the SDK version published for the closest regular release at or before your LTS version.
47
36
  >
48
- > Want more background on how dotCMS releases and support windows work? See
49
- > [Release & Support Lifecycle](https://dev.dotcms.com/docs/release-support-lifecycle).
37
+ > Want more background on how dotCMS releases and support windows work? See [Release & Support Lifecycle](https://dev.dotcms.com/docs/release-support-lifecycle).
50
38
 
51
39
  ## The front door — one runtime, two verbs
52
40
 
@@ -56,7 +44,7 @@ import { createRuntime } from '@dotcms/ai/runtime';
56
44
  const dotcms = createRuntime({
57
45
  url, // dotCMS instance URL
58
46
  token, // dotCMS auth token — NEVER enters the sandbox
59
- allow, // optional allow-list/policy (string[] of path prefixes, or a predicate)
47
+ allow, // optional allow-list/policy (string[] of paths, matched with everything under them; or a predicate)
60
48
  sessionId, // context-cache + isolation key
61
49
  includeSpec, // inject the `spec` global for the search use case
62
50
  timeout // sandbox wall-clock timeout (ms)
@@ -76,9 +64,201 @@ await dotcms.run(code); // SANDBOXED — a model wrote `code`.
76
64
  | `@dotcms/ai/sandbox` | Power users / custom adapters | `createSandbox`, `defineAdapter`, `Executor`, types, errors | **fully generic, lint-enforced** |
77
65
  | `@dotcms/ai/adapter` | Power users | `dotcmsAdapter`, `requestCore`, context loading + cache | dotCMS-specific |
78
66
  | `@dotcms/ai/spec` | The search use case | the OpenAPI spec (opt-in; keeps the ~400KB off the default path) | dotCMS-specific |
67
+ | `@dotcms/ai/tools` | Hosts building their own MCP server or agent | one factory per tool (`searchTool`, `executeTool`, `pageCreateTool`, …), and the operations behind them (`createPage`, `placeContent`, `verifyPage`, `uploadAssets`, `downloadAssets`) | dotCMS-specific, **top layer, lint-enforced** |
79
68
 
80
69
  `@dotcms/ai` is a pure namespace — there is no bare import; everything is reached through a subpath. It is an **umbrella** for growth: future AI surfaces (RAG, embeddings, custom agents, harness) land as new subpaths under the same package.
81
70
 
71
+ The layers only point one way: `tools` builds on `runtime`, which builds on `adapter` and `sandbox`. Lint rules keep `sandbox` free of dotCMS code, and keep everything beneath `tools` from importing it, so a bare `@dotcms/ai/runtime` import never pulls in the tool descriptions or `node:fs`.
72
+
73
+ ## Ready-made tools — `@dotcms/ai/tools`
74
+
75
+ The same seven tools dotCMS's own MCP server ships, packaged for yours. You bring the host (an MCP server, an agent loop, a workflow node); the tools bring the tuned descriptions, the validated input schemas and the sharp-edge handling behind each one.
76
+
77
+ | Factory | Tool name | What it does | Read-only | Resolves to |
78
+ |---|---|---|---|---|
79
+ | `searchTool` | `search` | Model-written JS explores the bundled OpenAPI spec | yes | `{ result: string }` |
80
+ | `executeTool` | `execute` | Model-written JS calls the dotCMS API in the sandbox | no | `{ result: string }` |
81
+ | `pageCreateTool` | `page_create` | Creates and publishes a page, creating its folder first so the URL cannot collapse | no | `CreatePageManifest` |
82
+ | `pagePlaceContentTool` | `page_place_content` | Places contentlets into page slots without wiping the slots it didn't touch | no | `PagePlaceContentManifest` |
83
+ | `pageVerifyTool` | `page_verify` | Renders a page and diagnoses empty slots, swallowed VTL errors and stale cache | yes | `VerifyPageManifest` |
84
+ | `uploadAssetsTool` | `upload_assets` | Uploads a local directory into dotCMS as file assets | no | `UploadAssetsManifest` |
85
+ | `downloadAssetsTool` | `download_assets` | Streams dotCMS file assets to a local directory | no | `DownloadAssetsManifest` |
86
+
87
+ Create one **connection**, the dotCMS instance and identity the tools act as, then call a factory for each tool you want, passing the connection. Most tool packages in the AI SDK ecosystem work this way.
88
+
89
+ ```ts
90
+ import { generateText } from 'ai';
91
+ import { dotcmsConnection, pageCreateTool, pageVerifyTool, searchTool } from '@dotcms/ai/tools';
92
+
93
+ const dotcms = dotcmsConnection({ url: 'https://demo.dotcms.com', token });
94
+
95
+ const result = await generateText({
96
+ model,
97
+ prompt: 'Create a /books page and check that it renders',
98
+ tools: {
99
+ search: searchTool(dotcms),
100
+ page_create: pageCreateTool(dotcms),
101
+ page_verify: pageVerifyTool(dotcms)
102
+ }
103
+ });
104
+ ```
105
+
106
+ Each tool is a plain object — `name`, `title`, `description`, `inputSchema` (Zod), `annotations` (MCP's read-only/destructive/idempotent/open-world hints), `execute` and `toModelOutput` — shaped the way the Vercel AI SDK and the MCP TypeScript SDK expect. Every result is an object, as Google ADK requires. The package depends on none of them.
107
+
108
+ **The connection.** You supply the URL and token; the tools never go looking for them. Either value may be a string or a resolver. A resolver is read on every call, which covers rotating tokens, secrets managers, and hosts that read their own configuration lazily:
109
+
110
+ ```ts
111
+ const dotcms = dotcmsConnection({
112
+ url: 'https://demo.dotcms.com',
113
+ token: () => secrets.get('dotcms-token'), // string | () => string | undefined | Promise<…>
114
+ onCall: (event) => tracer.record(event), // optional: fired around every request
115
+ onContextError: (label, error) => log(label, error),
116
+ onResolveError: (field, error) => log(`dotCMS ${field} resolver failed`, error)
117
+ });
118
+ ```
119
+
120
+ A connection holds no state and makes no request. A host serving many users, such as a remote MCP server with per-user OAuth or an agent with one session per user, creates one connection per session with that user's token, and builds its tools from it.
121
+
122
+ **Tool options** describe the tool, not the caller. They go in the factory's second argument:
123
+
124
+ ```ts
125
+ const tools = {
126
+ page_create: pageCreateTool(dotcms),
127
+ page_verify: pageVerifyTool(dotcms, { requestTimeout: 10_000 }),
128
+ execute: executeTool(dotcms, { allow: ['/api/v1/content', '/api/content/_search'] })
129
+ };
130
+ ```
131
+
132
+ You never tell a tool which endpoints to call — each tool owns that, and **enforces** it. Every fixed-purpose tool declares exactly the endpoints it calls, method and path, and any other request is refused with a `POLICY` failure before it reaches the wire. `page_create` knows it needs folders, content types and the workflow fire endpoint, so you don't have to list them and can't break it by leaving one out. A bug in `page_verify`, or a page path shaped to climb elsewhere, still can't get past the render and site endpoints. This covers `search` too: its sandbox gets the same `api` adapter as `execute`, and its allow-list (the context reads, nothing else) is what makes it read-only.
133
+
134
+ The exception is `execute`, where the *model* chooses the endpoints. `executeTool` takes `allow`, the same allow-list `createRuntime` takes, to bound what the model's code can reach. The context reads behind the `sites` / `contentTypes` / `languages` / `currentUser` globals are always permitted, `GET` only, so a narrow list never silently empties them.
135
+
136
+ | Option | Tools | Default |
137
+ |---|---|---|
138
+ | `allow` | `executeTool` | none — the model's code may call any endpoint the token allows (the other tools are always limited to their own endpoints) |
139
+ | `timeout` | `executeTool` | 45000 ms sandbox wall-clock |
140
+ | `includeStacks` | `executeTool` | false — host stack traces are withheld from the model |
141
+ | `requestTimeout` | page and asset tools | 30000 ms per request; for the asset tools that is per file, transfer included |
142
+ | `root` | asset tools — **required** | none — the local directory the model's `src` / `dest` must stay inside, symlinks resolved |
143
+ | `maxFileBytes` | `uploadAssetsTool` | 100 MB — a larger file fails before it is read; the rest still upload. `Infinity` lifts it |
144
+ | `maxFiles` | `uploadAssetsTool` | 1,000 — more files (after `include`) is a `VALIDATION` failure before anything is sent. `Infinity` lifts it |
145
+ | `maxScannedEntries` | `uploadAssetsTool` | 20,000 — the walk of `src` stops there, matched by `include` or not, as a `VALIDATION` failure. `Infinity` lifts it |
146
+
147
+ **The asset tools need a `root`.** `uploadAssetsTool` and `downloadAssetsTool` touch the local disk, with paths the model chooses. Without a boundary, a hosted server's `upload_assets` would read any directory the process can (and serve it back through dotCMS), and `download_assets` would write into any directory it can. Name the workspace the model may use:
148
+
149
+ ```ts
150
+ uploadAssetsTool(dotcms, { root: '/srv/agent-workspace' })
151
+ downloadAssetsTool(dotcms, { root: '/srv/agent-workspace' })
152
+ ```
153
+
154
+ Paths are compared after symlinks resolve: a link inside the root that points out of it is refused, and so is a download that would write through a symlink. `root: '/'` is the explicit whole-disk choice, for a local agent acting as its own user. The dotCMS MCP server uses it.
155
+
156
+ **File size.** The two directions differ:
157
+
158
+ - **Downloads stream.** Each file's bytes go from the response straight to disk, a chunk at a time, so memory stays flat whatever the file's size (measured: a 200 MB asset with under 40 MB of growth). A file lands under a temporary name and is renamed into place only once complete, so a broken transfer never leaves a partial file or damages the one it was replacing.
159
+ - **Uploads hold each file in memory once.** The file is opened as a disk-backed Blob and never copied or base64-encoded by the tool, but `fetch` gathers a request body into memory before sending it: measured on Node 22, a 1 GB file costs about 1 GB. Files go one at a time, so a batch costs its largest file, not its total. Because the model chooses `src`, `uploadAssetsTool` enforces limits by default: `maxFileBytes` (100 MB, which is also the upload's memory bound), `maxFiles` (1,000) and `maxScannedEntries` (20,000 entries walked, so a vast `src` with a narrow `include` is not walked to the end). The direct `uploadAssets` operation is unbounded unless you pass the same options.
160
+
161
+ Both directions are also bounded by `requestTimeout`, which covers each file's whole transfer: raise it for large files on a slow link.
162
+
163
+ **`execute` never throws.** It validates the input against `inputSchema` (it comes from the model, so this is the trust boundary), runs the tool, and resolves to the result in the table above — or to a `ToolFailure`: `{ ok: false, code, retryable, error, status? }`. The `retryable` flag is there because a model cannot `instanceof` its way through a failure; it needs to be told whether trying again can help. `isToolFailure(result)` tells the two apart. `code` is a `ToolFailureCode`, one of `VALIDATION`, `POLICY`, `HTTP`, `NETWORK`, `TIMEOUT`, `ABORT`, `SANDBOX`, `RUNTIME`, `CONFIGURATION` or `UNKNOWN`, so a host that switches on it gets completion, and a misspelt code does not compile.
164
+
165
+ **Typed input, and a type for lists.** Each factory returns a `DotCMSTool` whose `execute` input is typed from its own schema: calling `pageVerifyTool(dotcms).execute({ path: 42 })` is a compile error, and your editor completes the fields. That is a convenience for code you write; the run-time check above still applies, because what a model sends was never type-checked. To hold tools with different schemas in one list or map, and call whichever the model picked, type it `AnyDotCMSTool[]`: its `execute` takes `unknown`, and every tool is assignable to it.
166
+
167
+ **No I/O at creation.** Neither `dotcmsConnection` nor a factory does any I/O; the connection is resolved and checked on each call. The one exception is your own static configuration: an `allow` entry for `executeTool` that is not a path throws when the tool is created, because it is a security boundary. So a host started before its credentials exist still boots and lists its tools, and every call answers with a `CONFIGURATION` failure instead of the host crashing. That covers an empty value, a resolver returning `undefined`, and a resolver that throws. When a resolver throws, the model is told only that the connection could not be resolved. A secrets manager's error can carry paths, key names or hostnames, so the real error reaches you only through the connection's `onResolveError` hook; the returned failure does not carry it. Each call also builds a fresh runtime, so instance context (sites, content types, languages) is never stale from one call to the next.
168
+
169
+ **What the model sees.** Each tool decides how its results reach the model. `search`/`execute` declare a text form, and the rest are shown as structured JSON (so is any failure). In the AI SDK, the tool's `toModelOutput` is picked up automatically. A text-only transport such as MCP calls the tool's own `toText(result)`, which applies the same rule: code text unescaped, everything else pretty-printed JSON.
170
+
171
+ **Register each tool under its `name`.** The descriptions refer to their siblings by these names ("use the `search` tool first", "prefer `page_create`"). A tool registered under another name still works, but the model is pointed at one that does not exist.
172
+
173
+ ### With the MCP TypeScript SDK
174
+
175
+ The tool object is already a valid `registerTool` config. All that's left is MCP's text envelope:
176
+
177
+ ```ts
178
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
179
+ import { dotcmsConnection, pageVerifyTool, searchTool, type AnyDotCMSTool } from '@dotcms/ai/tools';
180
+
181
+ const dotcms = dotcmsConnection({ url, token });
182
+ const server = new McpServer({ name: 'my-server', version: '1.0.0' });
183
+ const tools: AnyDotCMSTool[] = [searchTool(dotcms), pageVerifyTool(dotcms)];
184
+
185
+ for (const tool of tools) {
186
+ server.registerTool(tool.name, tool, async (args) => ({
187
+ content: [{ type: 'text', text: tool.toText(await tool.execute(args)) }]
188
+ }));
189
+ }
190
+ ```
191
+
192
+ ### With Google ADK
193
+
194
+ ADK's `FunctionTool` takes the Zod schema directly as `parameters`:
195
+
196
+ ```ts
197
+ import { FunctionTool, LlmAgent } from '@google/adk';
198
+ import { dotcmsConnection, pageVerifyTool, searchTool, type AnyDotCMSTool } from '@dotcms/ai/tools';
199
+
200
+ const dotcms = dotcmsConnection({ url, token });
201
+ const tools: AnyDotCMSTool[] = [searchTool(dotcms), pageVerifyTool(dotcms)];
202
+
203
+ const agent = new LlmAgent({
204
+ model: 'gemini-flash-latest',
205
+ name: 'dotcms_agent',
206
+ instruction: 'Help authors manage their dotCMS site.',
207
+ tools: tools.map(
208
+ (t) =>
209
+ new FunctionTool({
210
+ name: t.name,
211
+ description: t.description,
212
+ parameters: t.inputSchema,
213
+ execute: (input) => t.execute(input)
214
+ })
215
+ )
216
+ });
217
+ ```
218
+
219
+ The schemas are Zod 4. Your framework and `@dotcms/ai` must resolve the **same** Zod install, which a normal install does. If you pin a second copy, TypeScript will reject `inputSchema` where the framework expects its own Zod type.
220
+
221
+ ### With a framework that takes JSON Schema
222
+
223
+ The Anthropic and OpenAI SDKs, n8n and others take JSON Schema rather than Zod. Render it from the **input** side, so fields with a default stay optional. Otherwise the model is told it must send every one of them:
224
+
225
+ ```ts
226
+ import { z } from 'zod';
227
+ import { dotcmsConnection, pageVerifyTool, searchTool, type AnyDotCMSTool } from '@dotcms/ai/tools';
228
+
229
+ const dotcms = dotcmsConnection({ url, token });
230
+ const tools: AnyDotCMSTool[] = [searchTool(dotcms), pageVerifyTool(dotcms)];
231
+ const definitions = tools.map((t) => ({
232
+ name: t.name,
233
+ description: t.description,
234
+ input_schema: z.toJSONSchema(t.inputSchema, { io: 'input' })
235
+ }));
236
+
237
+ // …when the model asks for a tool:
238
+ const result = await tools.find((t) => t.name === block.name)?.execute(block.input);
239
+ ```
240
+
241
+ ### Choosing the surface
242
+
243
+ The surface is exactly what you import and register. Expose `search` and `page_verify` (the two read-only tools), and the model cannot create or delete anything — both are held to `GET`s by their own allow-lists, not just by their annotations. Each fixed-purpose tool can only reach its own endpoints. `execute` is the open-ended one, so bound it with `allow`, or leave it out.
244
+
245
+ ### The operations behind the tools
246
+
247
+ The tools are formatting on top of typed operations, which are exported too. They are the direct path, the tool-level counterpart of `request()`: use them when **you** write the call. Typed options go in, a typed manifest comes out, and errors are thrown from the one hierarchy above.
248
+
249
+ ```ts
250
+ import { createRuntime } from '@dotcms/ai/runtime';
251
+ import { createPage, placeContent, verifyPage } from '@dotcms/ai/tools';
252
+
253
+ const dotcms = createRuntime({ url, token });
254
+
255
+ const page = await createPage({ dotcms, site: 'demo.dotcms.com', urlPath: '/books', title: 'Books', template: templateId });
256
+ await placeContent({ dotcms, path: page.fullPath, site: 'demo.dotcms.com', slots: [{ slot: 1, contentlets: [bookId] }] });
257
+ const report = await verifyPage({ dotcms, path: page.fullPath, site: 'demo.dotcms.com' });
258
+ ```
259
+
260
+ A runtime from `createRuntime` has no per-request deadline, unlike the one each tool call builds — pass a `signal` to `request` if you need one. `uploadAssets` and `downloadAssets` read and write the local file system, so they need Node or Bun.
261
+
82
262
  ## Custom, typed operations — `defineAdapter`
83
263
 
84
264
  Instead of permitting paths on a generic `request`, expose **named operations** an LLM can call by name, with Zod-validated input and a declared output contract. This is the governed path in practice — the model sees `scan`, not `/api/**`:
@@ -113,7 +293,7 @@ await sandbox.run(`return (await a11y.scan({ url: 'https://demo.dotcms.com/' }))
113
293
 
114
294
  ## Error model
115
295
 
116
- A single typed hierarchy, surfaced identically from `request()` and `run()` (one `requestCore`): `ValidationError`, `PolicyError`, `HttpError` (carries status + body), `TimeoutError`, `AbortError`, `SandboxError`, `RuntimeError` — all subclasses of `DotCMSError`, each with a stable `code` and a serializable `toJSON()`. The model-facing string an MCP tool builds is *formatting on top of* this model.
296
+ A single typed hierarchy, surfaced identically from `request()` and `run()` (one `requestCore`): `ValidationError`, `PolicyError`, `HttpError` (carries status + body), `NetworkError` (no response at all: refused, reset, DNS), `TimeoutError`, `AbortError`, `SandboxError`, `RuntimeError` — all subclasses of `DotCMSError`, each with a stable `code` and a serializable `toJSON()`. The model-facing string an MCP tool builds is *formatting on top of* this model — in `@dotcms/ai/tools` that formatting is the `ToolFailure` every tool resolves to on failure, which adds the `retryable` flag a model needs to decide whether to try again.
117
297
 
118
298
  ```ts
119
299
  import { isDotCMSError, HttpError } from '@dotcms/ai/runtime';
@@ -139,9 +319,7 @@ The governance above is **capability confinement for trusted code generators**
139
319
 
140
320
  ## Regenerating the spec
141
321
 
142
- `src/generated/spec.json` is **build-generated and git-ignored** — it is NOT committed. The
143
- `build`/`test`/`serve` targets run `sdk-ai:generate-spec` automatically (via `dependsOn`), so
144
- you rarely run it by hand; do so only to refresh the local copy or inspect the output.
322
+ `src/generated/spec.json` is **build-generated and git-ignored** — it is NOT committed. The `build`/`test`/`serve` targets run `sdk-ai:generate-spec` automatically (via `dependsOn`), so you rarely run it by hand; do so only to refresh the local copy or inspect the output.
145
323
 
146
324
  ```bash
147
325
  # Defaults to https://demo.dotcms.com/api/openapi.json
@@ -151,11 +329,7 @@ pnpm nx run sdk-ai:generate-spec
151
329
  pnpm nx run sdk-ai:generate-spec -- http://localhost:8080/api/openapi.json
152
330
  ```
153
331
 
154
- The script filters the spec to the endpoints in `ALLOWED_PREFIXES` (see `scripts/spec-transform.ts`),
155
- keeps request/response `$ref`s, and prunes `components.schemas` to just the schemas those endpoints
156
- reference. Keeping `$ref`s (rather than dereferencing) dedupes shared schemas and keeps the file
157
- small (~400KB). The output is compact JSON (machine-read only) — use `jq` to inspect it. Because the
158
- spec is regenerated at build time, there is nothing to commit.
332
+ The script filters the spec to the endpoints in `ALLOWED_PREFIXES` (see `scripts/spec-transform.ts`), keeps request/response `$ref`s, and prunes `components.schemas` to just the schemas those endpoints reference. Keeping `$ref`s (rather than dereferencing) dedupes shared schemas and keeps the file small (~400KB). The output is compact JSON (machine-read only) — use `jq` to inspect it. Because the spec is regenerated at build time, there is nothing to commit.
159
333
 
160
334
  ## Commands
161
335
 
@@ -0,0 +1 @@
1
+ exports._default = require('./adapter.cjs.js').default;
package/adapter.cjs.js ADDED
@@ -0,0 +1,14 @@
1
+ 'use strict';
2
+
3
+ var adapter = require('./adapter.cjs2.js');
4
+ require('./errors.cjs.js');
5
+
6
+
7
+
8
+ exports.ContextCache = adapter.ContextCache;
9
+ exports.createApiAdapter = adapter.createApiAdapter;
10
+ exports.dotcmsAdapter = adapter.dotcmsAdapter;
11
+ exports.getSharedContextCache = adapter.getSharedContextCache;
12
+ exports.isBinaryResponseEnvelope = adapter.isBinaryResponseEnvelope;
13
+ exports.loadDotCMSContext = adapter.loadDotCMSContext;
14
+ exports.requestCore = adapter.requestCore;
@@ -0,0 +1,2 @@
1
+ export * from './adapter.cjs.js';
2
+ export { _default as default } from './adapter.cjs.default.js';
Binary file
package/adapter.d.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "./src/adapter";
package/adapter.esm.js ADDED
@@ -0,0 +1,2 @@
1
+ export { C as ContextCache, c as createApiAdapter, d as dotcmsAdapter, g as getSharedContextCache, i as isBinaryResponseEnvelope, l as loadDotCMSContext, r as requestCore } from './adapter.esm2.js';
2
+ import './errors.esm.js';
Binary file
package/errors.cjs.js ADDED
@@ -0,0 +1,192 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * The single typed error hierarchy for `@dotcms/ai`.
5
+ *
6
+ * Both verbs of the runtime — `request()` (direct) and `run()` (sandboxed) — surface
7
+ * the *same* error types, because both route through one shared request core. The
8
+ * model-facing string an MCP tool builds is *formatting on top of* these errors, not a
9
+ * separate error model.
10
+ *
11
+ * Every error carries a stable, machine-readable `code` and a `toJSON()` so it survives
12
+ * the worker→host serialization boundary (a thrown error becomes `{ name, message, stack }`
13
+ * by default; these types preserve `code` and the structured detail too).
14
+ */
15
+ class DotCMSError extends Error {
16
+ constructor(message, options) {
17
+ super(message);
18
+ // A fallback only: every subclass declares its own `name`. The class name cannot be
19
+ // trusted — a bundler minifies it, and the built MCP server showed the model
20
+ // "b: …" for a PolicyError. A new subclass must declare `override readonly name`.
21
+ this.name = new.target.name;
22
+ if (options?.cause !== undefined) {
23
+ // Standard ES2022 `cause`, but assigned defensively for older lib targets.
24
+ this.cause = options.cause;
25
+ }
26
+ // Restore the prototype chain so `instanceof` works after transpilation to ES5/ES2017.
27
+ Object.setPrototypeOf(this, new.target.prototype);
28
+ }
29
+ /** Type-specific structured detail; overridden by subclasses that carry data. */
30
+ detail() {
31
+ return undefined;
32
+ }
33
+ toJSON() {
34
+ return {
35
+ name: this.name,
36
+ code: this.code,
37
+ message: this.message,
38
+ stack: this.stack,
39
+ detail: this.detail()
40
+ };
41
+ }
42
+ }
43
+ /** Adapter input failed validation, or request options were malformed. */
44
+ class ValidationError extends DotCMSError {
45
+ constructor(message, issues) {
46
+ super(message);
47
+ this.name = 'ValidationError';
48
+ this.code = 'VALIDATION';
49
+ this.issues = issues;
50
+ }
51
+ detail() {
52
+ return this.issues ? { issues: this.issues } : undefined;
53
+ }
54
+ }
55
+ /** The allow-list / policy rejected the call before it reached the network. */
56
+ class PolicyError extends DotCMSError {
57
+ constructor(message, method, path) {
58
+ super(message);
59
+ this.name = 'PolicyError';
60
+ this.code = 'POLICY';
61
+ this.method = method;
62
+ this.path = path;
63
+ }
64
+ detail() {
65
+ return { method: this.method, path: this.path };
66
+ }
67
+ }
68
+ /** dotCMS responded with a non-2xx status. Carries the status and the (text) body. */
69
+ class HttpError extends DotCMSError {
70
+ constructor(status, statusText, body) {
71
+ super(`HTTP ${status} ${statusText}: ${body}`);
72
+ this.name = 'HttpError';
73
+ this.code = 'HTTP';
74
+ this.status = status;
75
+ this.statusText = statusText;
76
+ this.body = body;
77
+ }
78
+ detail() {
79
+ return { status: this.status, statusText: this.statusText, body: this.body };
80
+ }
81
+ }
82
+ /**
83
+ * The request never got a response — the connection was refused or reset, the host did not
84
+ * resolve, the socket timed out. Distinct from `HttpError` (the instance answered, with an
85
+ * error) because the two call for opposite reactions: this one is usually worth retrying.
86
+ */
87
+ class NetworkError extends DotCMSError {
88
+ constructor(method, path, cause) {
89
+ const reason = networkReason(cause);
90
+ super(`Request ${method} ${path} could not reach the dotCMS instance` +
91
+ (reason ? ` (${reason})` : '') +
92
+ '. The instance may be down, unreachable or restarting; this is worth retrying.', { cause });
93
+ this.name = 'NetworkError';
94
+ this.code = 'NETWORK';
95
+ this.method = method;
96
+ this.path = path;
97
+ this.reason = reason;
98
+ }
99
+ detail() {
100
+ return { method: this.method, path: this.path, reason: this.reason };
101
+ }
102
+ }
103
+ /** The most specific reason a failed `fetch` carries — undici nests it under `cause`. */
104
+ function networkReason(error) {
105
+ const nested = error?.cause;
106
+ const code = nested?.code ?? error?.code;
107
+ if (typeof code === 'string' && code) {
108
+ return code;
109
+ }
110
+ const message = nested?.message ?? error?.message;
111
+ return typeof message === 'string' && message ? message : undefined;
112
+ }
113
+ /** A timeout elapsed (sandbox wall-clock, per-adapter-call, or context load). */
114
+ class TimeoutError extends DotCMSError {
115
+ constructor(message, timeoutMs) {
116
+ super(message);
117
+ this.name = 'TimeoutError';
118
+ this.code = 'TIMEOUT';
119
+ this.timeoutMs = timeoutMs;
120
+ }
121
+ detail() {
122
+ return this.timeoutMs === undefined ? undefined : { timeoutMs: this.timeoutMs };
123
+ }
124
+ }
125
+ /** The call was aborted in-flight (typically because the sandbox timed out). */
126
+ class AbortError extends DotCMSError {
127
+ constructor() {
128
+ super(...arguments);
129
+ this.name = 'AbortError';
130
+ this.code = 'ABORT';
131
+ }
132
+ }
133
+ /** Model-authored code threw while executing inside the sandbox. */
134
+ class SandboxError extends DotCMSError {
135
+ constructor(message, originalName, stack) {
136
+ super(message);
137
+ this.name = 'SandboxError';
138
+ this.code = 'SANDBOX';
139
+ this.originalName = originalName;
140
+ if (stack)
141
+ this.stack = stack;
142
+ }
143
+ detail() {
144
+ return this.originalName ? { originalName: this.originalName } : undefined;
145
+ }
146
+ }
147
+ /** Catch-all for runtime-internal failures that don't fit a more specific type. */
148
+ class RuntimeError extends DotCMSError {
149
+ constructor() {
150
+ super(...arguments);
151
+ this.name = 'RuntimeError';
152
+ this.code = 'RUNTIME';
153
+ }
154
+ }
155
+ /** Type guard for any error in the hierarchy. */
156
+ function isDotCMSError(value) {
157
+ return value instanceof DotCMSError;
158
+ }
159
+ /**
160
+ * Normalize any thrown value into a serializable error shape. Used at the worker→host
161
+ * boundary and when formatting a result, so callers always get a consistent structure
162
+ * regardless of what was thrown.
163
+ */
164
+ function serializeError(value) {
165
+ if (isDotCMSError(value)) {
166
+ return value.toJSON();
167
+ }
168
+ if (value instanceof Error) {
169
+ // Preserve a code already set on a plain Error (e.g. Node system errors, or an
170
+ // adapter that tags its own); fall back to RUNTIME.
171
+ const existingCode = value.code;
172
+ return {
173
+ name: value.name,
174
+ code: (typeof existingCode === 'string' ? existingCode : 'RUNTIME'),
175
+ message: value.message,
176
+ stack: value.stack
177
+ };
178
+ }
179
+ return { name: 'Error', code: 'RUNTIME', message: String(value) };
180
+ }
181
+
182
+ exports.AbortError = AbortError;
183
+ exports.DotCMSError = DotCMSError;
184
+ exports.HttpError = HttpError;
185
+ exports.NetworkError = NetworkError;
186
+ exports.PolicyError = PolicyError;
187
+ exports.RuntimeError = RuntimeError;
188
+ exports.SandboxError = SandboxError;
189
+ exports.TimeoutError = TimeoutError;
190
+ exports.ValidationError = ValidationError;
191
+ exports.isDotCMSError = isDotCMSError;
192
+ exports.serializeError = serializeError;