@zerotal/arch 1.7.4 → 1.8.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/CHANGELOG.md +86 -2
- package/README.md +4 -2
- package/api-surface.md +2 -0
- package/docs/about.md +16 -16
- package/docs/admin/actions.md +9 -9
- package/docs/admin/auth.md +2 -2
- package/docs/admin/dashboard.md +5 -5
- package/docs/admin/extending-ui.md +12 -12
- package/docs/admin/extending.md +16 -7
- package/docs/admin/forms.md +4 -4
- package/docs/admin/index.md +2 -2
- package/docs/admin/operations.md +9 -9
- package/docs/admin/resources.md +3 -3
- package/docs/admin/structure.md +6 -6
- package/docs/admin/tables.md +8 -8
- package/docs/admin/testing.md +4 -4
- package/docs/ai.md +118 -11
- package/docs/application.md +12 -12
- package/docs/arch.md +25 -3
- package/docs/assets.md +11 -11
- package/docs/audit.md +13 -13
- package/docs/authentication.md +39 -39
- package/docs/authorization.md +13 -13
- package/docs/broadcasting/channels.md +2 -2
- package/docs/broadcasting/client.md +2 -2
- package/docs/broadcasting/events.md +9 -10
- package/docs/broadcasting/index.md +1 -1
- package/docs/broadcasting/testing.md +5 -5
- package/docs/cache.md +15 -13
- package/docs/carbon.md +35 -35
- package/docs/changelog.md +268 -1
- package/docs/client/index.md +13 -13
- package/docs/commands.md +36 -4
- package/docs/components.md +116 -115
- package/docs/config-system.md +4 -4
- package/docs/container.md +27 -27
- package/docs/context.md +27 -27
- package/docs/contributing.md +28 -0
- package/docs/controllers.md +10 -10
- package/docs/conventions.md +23 -15
- package/docs/cookies.md +3 -3
- package/docs/csrf.md +6 -6
- package/docs/database.md +22 -17
- package/docs/deployment.md +99 -2
- package/docs/devtools.md +16 -16
- package/docs/email-verification.md +8 -8
- package/docs/encryption.md +7 -7
- package/docs/errors.md +6 -6
- package/docs/events.md +12 -11
- package/docs/flow/components.md +32 -32
- package/docs/flow/decorators.md +18 -18
- package/docs/flow/events.md +16 -16
- package/docs/flow/forms.md +17 -17
- package/docs/flow/icons.md +5 -5
- package/docs/flow/index.md +21 -21
- package/docs/flow/layouts.md +99 -35
- package/docs/flow/lifecycle.md +16 -16
- package/docs/flow/models.md +9 -9
- package/docs/flow/pagination.md +8 -8
- package/docs/flow/performance.md +7 -7
- package/docs/flow/references.md +3 -3
- package/docs/flow/routing.md +26 -26
- package/docs/flow/testing.md +20 -20
- package/docs/getting-started.md +18 -13
- package/docs/health.md +17 -8
- package/docs/helpers.md +17 -17
- package/docs/i18n.md +10 -10
- package/docs/inertia/devtools.md +4 -4
- package/docs/inertia/index.md +6 -6
- package/docs/inertia/props.md +21 -21
- package/docs/inertia/rendering.md +9 -9
- package/docs/inertia/ssr.md +2 -2
- package/docs/lifecycle.md +2 -2
- package/docs/lock.md +11 -11
- package/docs/logger.md +12 -12
- package/docs/media.md +22 -22
- package/docs/middleware.md +17 -17
- package/docs/migrations.md +13 -13
- package/docs/monitor.md +6 -6
- package/docs/notifications.md +24 -24
- package/docs/orm/casts.md +14 -14
- package/docs/orm/factories.md +8 -8
- package/docs/orm/index.md +22 -20
- package/docs/orm/lifecycle.md +13 -13
- package/docs/orm/queries.md +28 -28
- package/docs/orm/relationships.md +16 -16
- package/docs/orm/serialization.md +11 -11
- package/docs/package-development.md +10 -10
- package/docs/pagination.md +14 -14
- package/docs/password-reset.md +6 -6
- package/docs/providers.md +11 -11
- package/docs/query-builder.md +18 -18
- package/docs/queue.md +14 -12
- package/docs/rate-limiting.md +6 -6
- package/docs/responses.md +14 -14
- package/docs/roles-and-2fa.md +9 -9
- package/docs/routing.md +44 -44
- package/docs/scaffolding.md +1 -1
- package/docs/scheduler.md +10 -10
- package/docs/seeding.md +6 -6
- package/docs/session.md +16 -14
- package/docs/social.md +14 -14
- package/docs/storage.md +25 -13
- package/docs/structure.md +2 -2
- package/docs/support-policy.md +23 -10
- package/docs/telemetry.md +12 -12
- package/docs/tenancy.md +34 -40
- package/docs/testing/browser.md +6 -6
- package/docs/testing/console.md +3 -3
- package/docs/testing/database.md +10 -10
- package/docs/testing/flow-browser.md +6 -6
- package/docs/testing/http.md +12 -12
- package/docs/testing/index.md +29 -3
- package/docs/testing/mocking.md +8 -8
- package/docs/upgrade.md +1 -1
- package/docs/validator.md +19 -19
- package/docs/view.md +14 -14
- package/package.json +4 -4
- package/src/config.ts +8 -0
- package/src/index.ts +25 -1
- package/src/install/ArchInstallCommand.ts +46 -3
- package/src/install/guidelines.ts +84 -0
- package/src/install/shape.ts +177 -0
- package/src/install/skills.ts +271 -0
- package/src/provider/ArchProvider.ts +148 -0
- package/src/tools/searchDocs.ts +7 -0
package/docs/admin/operations.md
CHANGED
|
@@ -30,7 +30,7 @@ as it does today.
|
|
|
30
30
|
|
|
31
31
|
Turn it on per resource:
|
|
32
32
|
|
|
33
|
-
```ts
|
|
33
|
+
```ts fragment
|
|
34
34
|
export class ProductResource extends Resource {
|
|
35
35
|
static override history = true;
|
|
36
36
|
}
|
|
@@ -50,7 +50,7 @@ ago should not also undo the description somebody fixed yesterday.
|
|
|
50
50
|
The support request nobody can reproduce is usually solved by seeing what the person
|
|
51
51
|
actually sees.
|
|
52
52
|
|
|
53
|
-
```ts
|
|
53
|
+
```ts fragment
|
|
54
54
|
export class UserResource extends Resource {
|
|
55
55
|
static override impersonatable = true;
|
|
56
56
|
|
|
@@ -79,7 +79,7 @@ Every bit of list state already lives in the URL: search, filters, tab, sort, co
|
|
|
79
79
|
visibility, grouping, page size. So a saved view is a saved query string, and
|
|
80
80
|
restoring one is a link.
|
|
81
81
|
|
|
82
|
-
```ts
|
|
82
|
+
```ts fragment
|
|
83
83
|
Panel.savedViews({
|
|
84
84
|
async list(resource) {
|
|
85
85
|
/* … */
|
|
@@ -103,7 +103,7 @@ A file upload field puts a file somewhere and stores a path. That works until th
|
|
|
103
103
|
logo is needed on twenty products, or somebody wants to know what is still pointing at
|
|
104
104
|
a file before deleting it.
|
|
105
105
|
|
|
106
|
-
```ts
|
|
106
|
+
```ts fragment
|
|
107
107
|
import { databaseMedia, mediaPicker } from "@zerotal/admin";
|
|
108
108
|
|
|
109
109
|
Panel.media(databaseMedia());
|
|
@@ -117,7 +117,7 @@ somewhere else.
|
|
|
117
117
|
Configured, the panel gains a Media page — a grid with upload, search, folder
|
|
118
118
|
filtering, alt-text editing and deletion — and forms can use the picker:
|
|
119
119
|
|
|
120
|
-
```ts
|
|
120
|
+
```ts fragment
|
|
121
121
|
mediaPicker("imageUrl").label("Image");
|
|
122
122
|
```
|
|
123
123
|
|
|
@@ -134,7 +134,7 @@ Authorization already works without a UI: a resource's `can()` answers every que
|
|
|
134
134
|
the panel asks. What is missing is the other direction — seeing who can do what, and
|
|
135
135
|
changing it, without editing code.
|
|
136
136
|
|
|
137
|
-
```ts
|
|
137
|
+
```ts fragment
|
|
138
138
|
import { authRoles } from "@zerotal/admin";
|
|
139
139
|
|
|
140
140
|
Panel.roles(authRoles({ superusers: ["admin"] }));
|
|
@@ -162,7 +162,7 @@ week.
|
|
|
162
162
|
What belongs at the top of a dashboard differs by role, and neither the finance lead
|
|
163
163
|
nor support wants to scroll past the other's widget every morning.
|
|
164
164
|
|
|
165
|
-
```ts
|
|
165
|
+
```ts fragment
|
|
166
166
|
Panel.dashboardLayout({
|
|
167
167
|
async load() {
|
|
168
168
|
return Auth.user()?.dashboard ?? null;
|
|
@@ -193,7 +193,7 @@ stale key mentions it.
|
|
|
193
193
|
Two people editing the same record is normal; one of them losing their work silently
|
|
194
194
|
is not.
|
|
195
195
|
|
|
196
|
-
```ts
|
|
196
|
+
```ts fragment
|
|
197
197
|
export class ProductResource extends Resource {
|
|
198
198
|
static override optimisticLock = "version";
|
|
199
199
|
}
|
|
@@ -207,7 +207,7 @@ overwriting loses somebody's work, and merging blind is worse.
|
|
|
207
207
|
|
|
208
208
|
The expensive mistake is editing production believing it is staging.
|
|
209
209
|
|
|
210
|
-
```ts
|
|
210
|
+
```ts fragment
|
|
211
211
|
import { environmentIndicator } from "@zerotal/admin";
|
|
212
212
|
|
|
213
213
|
Panel.renderHook("body.start", environmentIndicator());
|
package/docs/admin/resources.md
CHANGED
|
@@ -18,7 +18,7 @@ Scaffold one with the generator, which writes `app/admin/<Name>Resource.ts`:
|
|
|
18
18
|
bun zt make:admin-resource Post
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
```ts
|
|
21
|
+
```ts fragment
|
|
22
22
|
class PostResource extends Resource {
|
|
23
23
|
static model = Post;
|
|
24
24
|
|
|
@@ -71,7 +71,7 @@ class PostResource extends Resource {
|
|
|
71
71
|
A resource does nothing until the panel knows about it. Register it where the panel
|
|
72
72
|
is configured:
|
|
73
73
|
|
|
74
|
-
```ts
|
|
74
|
+
```ts fragment
|
|
75
75
|
Panel.register(PostResource);
|
|
76
76
|
```
|
|
77
77
|
|
|
@@ -93,7 +93,7 @@ any of its links changing.
|
|
|
93
93
|
Override `can(ability, record?)` to gate the built-in actions. Abilities used by the
|
|
94
94
|
presets: `create`, `update`, `delete`, `restore`, `forceDelete`.
|
|
95
95
|
|
|
96
|
-
```ts
|
|
96
|
+
```ts fragment
|
|
97
97
|
static can(ability: string, record?: AdminRecord) {
|
|
98
98
|
return Gate.allows(ability, record ?? this.model);
|
|
99
99
|
}
|
package/docs/admin/structure.md
CHANGED
|
@@ -21,7 +21,7 @@ real back office takes.
|
|
|
21
21
|
A cluster gives a group of resources a shared URL segment and a single sidebar
|
|
22
22
|
entry. Declare one, then point resources at it:
|
|
23
23
|
|
|
24
|
-
```ts
|
|
24
|
+
```ts fragment
|
|
25
25
|
import { Cluster } from "@zerotal/admin";
|
|
26
26
|
|
|
27
27
|
export class ShopCluster extends Cluster {
|
|
@@ -42,7 +42,7 @@ the cluster segment on its own. Nothing in a resource needs to know its own path
|
|
|
42
42
|
|
|
43
43
|
A cluster's `ability` gates the whole section:
|
|
44
44
|
|
|
45
|
-
```ts
|
|
45
|
+
```ts fragment
|
|
46
46
|
export class FinanceCluster extends Cluster {
|
|
47
47
|
static override slug = "finance";
|
|
48
48
|
static override title = "Finance";
|
|
@@ -59,7 +59,7 @@ Some records have no meaning apart from their parent. Comments belong to a post;
|
|
|
59
59
|
there is no useful screen listing every comment in the database. Declare the
|
|
60
60
|
parent and the resource moves inside it:
|
|
61
61
|
|
|
62
|
-
```ts
|
|
62
|
+
```ts fragment
|
|
63
63
|
export class CommentResource extends Resource {
|
|
64
64
|
static override model = Comment;
|
|
65
65
|
static override parent = { resource: () => PostResource, foreignKey: "post_id" };
|
|
@@ -83,7 +83,7 @@ on whichever side of the import cycle evaluated first.
|
|
|
83
83
|
When the parent declares the child with `hasMany`, its view page links into the
|
|
84
84
|
nested pages instead of rendering an inline table:
|
|
85
85
|
|
|
86
|
-
```ts
|
|
86
|
+
```ts fragment
|
|
87
87
|
export class PostResource extends Resource {
|
|
88
88
|
static override relations() {
|
|
89
89
|
return [hasMany(CommentResource, "post_id").title("Comments")];
|
|
@@ -96,7 +96,7 @@ export class PostResource extends Resource {
|
|
|
96
96
|
Site settings are one row. A list of one, with a view page and an edit page behind
|
|
97
97
|
it, is three screens too many:
|
|
98
98
|
|
|
99
|
-
```ts
|
|
99
|
+
```ts fragment
|
|
100
100
|
export class SettingsResource extends Resource {
|
|
101
101
|
static override model = Setting;
|
|
102
102
|
static override singular = true;
|
|
@@ -123,7 +123,7 @@ than an error.
|
|
|
123
123
|
Most applications have one panel, and `Panel.configure(...)` / `Panel.register(...)`
|
|
124
124
|
write to it. When a second audience needs a second set of screens, make another:
|
|
125
125
|
|
|
126
|
-
```ts
|
|
126
|
+
```ts fragment
|
|
127
127
|
// The back office — everything.
|
|
128
128
|
Panel.configure({ brand: "Acme", path: "/admin", middleware: [AuthMiddleware] });
|
|
129
129
|
Panel.register(ProductResource, OrderResource, UserResource);
|
package/docs/admin/tables.md
CHANGED
|
@@ -7,7 +7,7 @@ description: Columns, filters, sorting, search, and bulk actions on the list scr
|
|
|
7
7
|
|
|
8
8
|
`columns()` returns `Column`s built with `text(key)` plus chainable modifiers.
|
|
9
9
|
|
|
10
|
-
```ts
|
|
10
|
+
```ts fragment
|
|
11
11
|
static columns() {
|
|
12
12
|
return [
|
|
13
13
|
text("title").label("Title").sortable().searchable(),
|
|
@@ -42,14 +42,14 @@ static columns() {
|
|
|
42
42
|
formatter) render a `<tfoot>` total computed over the **full filtered dataset**, plus
|
|
43
43
|
per-group subtotals when grouping is active.
|
|
44
44
|
|
|
45
|
-
```ts
|
|
45
|
+
```ts fragment
|
|
46
46
|
text("amount").align("end").sum("Revenue", (n) => `$${n.toFixed(2)}`),
|
|
47
47
|
text("id").count("Orders"),
|
|
48
48
|
```
|
|
49
49
|
|
|
50
50
|
## Filters
|
|
51
51
|
|
|
52
|
-
```ts
|
|
52
|
+
```ts fragment
|
|
53
53
|
import { selectFilter, ternaryFilter } from "@zerotal/admin";
|
|
54
54
|
|
|
55
55
|
static filters() {
|
|
@@ -70,7 +70,7 @@ Fixed filters work when you can name the useful questions in advance. A catalogu
|
|
|
70
70
|
or a ledger is queried in too many ways for that, so `queryBuilder` lets the user
|
|
71
71
|
stack their own comparisons and nest AND/OR groups:
|
|
72
72
|
|
|
73
|
-
```ts
|
|
73
|
+
```ts fragment
|
|
74
74
|
import {
|
|
75
75
|
queryBuilder, textConstraint, numberConstraint,
|
|
76
76
|
selectConstraint, booleanConstraint, dateConstraint,
|
|
@@ -112,7 +112,7 @@ link someone can send to a colleague.
|
|
|
112
112
|
`tab(key)` adds quick-filter tabs above the table, each scoping the query and
|
|
113
113
|
optionally showing a count badge.
|
|
114
114
|
|
|
115
|
-
```ts
|
|
115
|
+
```ts fragment
|
|
116
116
|
import { tab } from "@zerotal/admin";
|
|
117
117
|
|
|
118
118
|
static tabs() {
|
|
@@ -129,7 +129,7 @@ static tabs() {
|
|
|
129
129
|
`group(column)` adds a "Group by" menu; the page renders header rows per bucket with
|
|
130
130
|
counts (and per-group summary subtotals).
|
|
131
131
|
|
|
132
|
-
```ts
|
|
132
|
+
```ts fragment
|
|
133
133
|
import { group } from "@zerotal/admin";
|
|
134
134
|
|
|
135
135
|
static groups() {
|
|
@@ -143,7 +143,7 @@ static defaultGroup = "status"; // optional
|
|
|
143
143
|
Set `reorderable` to an integer position column to show up/down handles that persist
|
|
144
144
|
order:
|
|
145
145
|
|
|
146
|
-
```ts
|
|
146
|
+
```ts fragment
|
|
147
147
|
static reorderable = "sort";
|
|
148
148
|
```
|
|
149
149
|
|
|
@@ -158,7 +158,7 @@ automatically and are URL-driven.
|
|
|
158
158
|
A blank table teaches nobody anything. Override `emptyState()` to say why the list
|
|
159
159
|
is empty and what will fill it:
|
|
160
160
|
|
|
161
|
-
```ts
|
|
161
|
+
```ts fragment
|
|
162
162
|
static emptyState() {
|
|
163
163
|
return {
|
|
164
164
|
heading: "No orders yet",
|
package/docs/admin/testing.md
CHANGED
|
@@ -26,7 +26,7 @@ works alongside the admin-specific ones below.
|
|
|
26
26
|
| `AdminTest.view(Resource, id)` | View page | Infolist entries and header actions for a record |
|
|
27
27
|
| `AdminTest.form(Resource, mode?)` | Create / Edit form | Fields, validation and saving |
|
|
28
28
|
|
|
29
|
-
```ts
|
|
29
|
+
```ts fragment
|
|
30
30
|
// tests/admin/users.test.ts
|
|
31
31
|
import { AdminTest, assertHasColumn, assertHasAction } from "@zerotal/admin/testing";
|
|
32
32
|
import { UserResource } from "../../app/admin/UserResource.ts";
|
|
@@ -41,7 +41,7 @@ list.assertSee("ada@example.com");
|
|
|
41
41
|
default) or `"edit"`. Drive the form through its `form` property, which holds
|
|
42
42
|
every field's value:
|
|
43
43
|
|
|
44
|
-
```ts
|
|
44
|
+
```ts fragment
|
|
45
45
|
const form = await AdminTest.form(UserResource, "create");
|
|
46
46
|
assertHasField(form, UserResource, "name");
|
|
47
47
|
await form.set("form", { name: "" });
|
|
@@ -56,7 +56,7 @@ those values as the second argument to `AdminTest.list()` and the page mounts as
|
|
|
56
56
|
though the reader had arrived on that URL — which is how you assert on a filtered
|
|
57
57
|
or sorted table without first driving the clicks that would produce it.
|
|
58
58
|
|
|
59
|
-
```ts
|
|
59
|
+
```ts fragment
|
|
60
60
|
const list = await AdminTest.list(UserResource, {
|
|
61
61
|
search: "ada",
|
|
62
62
|
sortBy: "createdAt",
|
|
@@ -125,7 +125,7 @@ Every helper accepts a panel as its last argument, defaulting to
|
|
|
125
125
|
`Panel.default()`. Apps that register more than one panel pass the one under test,
|
|
126
126
|
so the page resolves that panel's own configuration and navigation:
|
|
127
127
|
|
|
128
|
-
```ts
|
|
128
|
+
```ts fragment
|
|
129
129
|
const shop = Panel.get("shop");
|
|
130
130
|
|
|
131
131
|
const list = await AdminTest.list(OrderResource, {}, shop);
|
package/docs/ai.md
CHANGED
|
@@ -116,7 +116,7 @@ a price with `registerModelPrice()`.
|
|
|
116
116
|
|
|
117
117
|
## Generating text
|
|
118
118
|
|
|
119
|
-
```ts
|
|
119
|
+
```ts fragment
|
|
120
120
|
import { Ai } from "@zerotal/ai";
|
|
121
121
|
|
|
122
122
|
// Just the text.
|
|
@@ -152,7 +152,7 @@ Reach for `effort` instead. It trades thoroughness against cost and latency:
|
|
|
152
152
|
|
|
153
153
|
### Streaming
|
|
154
154
|
|
|
155
|
-
```ts
|
|
155
|
+
```ts fragment
|
|
156
156
|
for await (const chunk of Ai.stream({ prompt, signal })) {
|
|
157
157
|
if (chunk.type === "text") process.stdout.write(chunk.text);
|
|
158
158
|
if (chunk.type === "done") console.log(chunk.response.usage);
|
|
@@ -166,7 +166,7 @@ generation. See [Flow](/docs/flow) for streaming straight into a component.
|
|
|
166
166
|
|
|
167
167
|
### Structured output
|
|
168
168
|
|
|
169
|
-
```ts
|
|
169
|
+
```ts fragment
|
|
170
170
|
const review = await Ai.object({ prompt: `Classify this review:\n\n${text}` }, (rule) => ({
|
|
171
171
|
sentiment: rule.string().in(["positive", "neutral", "negative"]),
|
|
172
172
|
summary: rule.string().max(140),
|
|
@@ -189,13 +189,13 @@ schema is defined, not when the request is sent.
|
|
|
189
189
|
`strippedConstraints()` names exactly what the model will not see, if you want to
|
|
190
190
|
check that a load-bearing constraint is visible to it:
|
|
191
191
|
|
|
192
|
-
```ts
|
|
192
|
+
```ts fragment
|
|
193
193
|
strippedConstraints({ title: rule.string().min(3) }); // → ["title: min"]
|
|
194
194
|
```
|
|
195
195
|
|
|
196
196
|
## Tools and the agent loop
|
|
197
197
|
|
|
198
|
-
```ts
|
|
198
|
+
```ts fragment
|
|
199
199
|
import { Ai, tool } from "@zerotal/ai";
|
|
200
200
|
|
|
201
201
|
const lookupOrder = tool({
|
|
@@ -238,7 +238,7 @@ back and re-requests.
|
|
|
238
238
|
|
|
239
239
|
### Locking a run
|
|
240
240
|
|
|
241
|
-
```ts
|
|
241
|
+
```ts fragment
|
|
242
242
|
await Ai.agent({
|
|
243
243
|
prompt: "Refund order 4821 if it shipped over 30 days ago.",
|
|
244
244
|
tools: [lookupOrder, issueRefund],
|
|
@@ -264,7 +264,7 @@ checking crashes on a response the API considers fine.
|
|
|
264
264
|
|
|
265
265
|
This package checks the stop reason first and raises a typed error:
|
|
266
266
|
|
|
267
|
-
```ts
|
|
267
|
+
```ts fragment
|
|
268
268
|
import { AiRefusedError } from "@zerotal/ai";
|
|
269
269
|
|
|
270
270
|
try {
|
|
@@ -283,7 +283,7 @@ with `drivers.anthropic.fallbacks: false`.
|
|
|
283
283
|
|
|
284
284
|
## Embeddings
|
|
285
285
|
|
|
286
|
-
```ts
|
|
286
|
+
```ts fragment
|
|
287
287
|
const { embeddings } = await Ai.embed(["first chunk", "second chunk"]);
|
|
288
288
|
```
|
|
289
289
|
|
|
@@ -294,7 +294,7 @@ One vector per input, in input order.
|
|
|
294
294
|
A queued generation is serialized, so its completion handler is registered by **name**
|
|
295
295
|
— a closure cannot survive the trip to a worker process:
|
|
296
296
|
|
|
297
|
-
```ts
|
|
297
|
+
```ts fragment
|
|
298
298
|
// in a service provider's onBooted(), so the worker registers it too
|
|
299
299
|
Ai.onGenerated("summarize-ticket", async (response, meta) => {
|
|
300
300
|
await Ticket.query().where("id", meta.ticketId).update({ summary: response.text });
|
|
@@ -313,7 +313,7 @@ in-process where its tools are.
|
|
|
313
313
|
`AiFake` replaces the container binding and answers from a script. No API key, no
|
|
314
314
|
network, no flakiness:
|
|
315
315
|
|
|
316
|
-
```ts
|
|
316
|
+
```ts fragment
|
|
317
317
|
import { AiFake } from "@zerotal/ai";
|
|
318
318
|
|
|
319
319
|
const ai = AiFake.install();
|
|
@@ -361,7 +361,7 @@ suffix, a gateway that rewrites the base URL.
|
|
|
361
361
|
Implement `AiDriver` — `text`, `stream`, `object`, `countTokens`, `verify` — and
|
|
362
362
|
register it:
|
|
363
363
|
|
|
364
|
-
```ts
|
|
364
|
+
```ts fragment
|
|
365
365
|
// in a service provider's onBooted()
|
|
366
366
|
const ai = app.container.makeSync("ai");
|
|
367
367
|
ai.extend("bedrock", () => new BedrockDriver(config));
|
|
@@ -371,3 +371,110 @@ Nothing else is needed. Spend ceilings, redaction, telemetry, the lock, and the
|
|
|
371
371
|
loop all live above the driver, so a new provider is a translation layer and nothing
|
|
372
372
|
more. `agent()` is optional on the interface and none of the built-in three implement
|
|
373
373
|
it — they all run the same shared loop.
|
|
374
|
+
|
|
375
|
+
## Reference
|
|
376
|
+
|
|
377
|
+
Every exported name, grouped by the job it belongs to. The behaviour is in the
|
|
378
|
+
sections above; this is the index.
|
|
379
|
+
|
|
380
|
+
### Requests and responses
|
|
381
|
+
|
|
382
|
+
| Name | Description |
|
|
383
|
+
| ------------------- | ------------------------------------------------------------------------------ |
|
|
384
|
+
| `AiRequest` | What every generation call takes. `prompt` and `messages` are interchangeable. |
|
|
385
|
+
| `AiResponse` | A finished, non-streaming generation. |
|
|
386
|
+
| `AiStreamChunk` | One event from a streaming generation. |
|
|
387
|
+
| `AiObjectResponse` | A structured-output generation: the parsed value plus the usual accounting. |
|
|
388
|
+
| `AiMessage` | One turn of a conversation. |
|
|
389
|
+
| `AiRole` | Who said it. Tool results ride inside a `user` turn, as the providers expect. |
|
|
390
|
+
| `AiUsage` | Token accounting for one request. Fields a provider does not report stay 0. |
|
|
391
|
+
| `AiStopReason` | Why generation stopped. `refusal` is a successful HTTP response, not an error. |
|
|
392
|
+
| `AiEffort` | How hard the model should work before answering. Mapped per-driver. |
|
|
393
|
+
| `AiProviderOptions` | Per-driver escape hatch, keyed by driver name and passed through untouched. |
|
|
394
|
+
| `AiEmbedRequest` | A vector embedding request. |
|
|
395
|
+
| `AiEmbedResponse` | Embeddings, one vector per input, in input order. |
|
|
396
|
+
|
|
397
|
+
### Tools and the agent loop
|
|
398
|
+
|
|
399
|
+
| Name | Description |
|
|
400
|
+
| ---------------- | -------------------------------------------------------------------------------------- |
|
|
401
|
+
| `AiToolCall` | A tool call the model asked for, lifted out of whatever block shape the provider used. |
|
|
402
|
+
| `AiToolResult` | The answer to one `AiToolCall`. |
|
|
403
|
+
| `AiToolContext` | What a tool handler is told about the turn that invoked it. |
|
|
404
|
+
| `AiToolCalled` | Emitted once per tool call inside an agent run. |
|
|
405
|
+
| `AiAgentRequest` | An agent run, plus the two things only the caller can decide. |
|
|
406
|
+
| `AgentOptions` | What the agent loop needs from the caller, beyond the request itself. |
|
|
407
|
+
| `AiAgentResult` | The result of running the agent loop to completion. |
|
|
408
|
+
| `AiAgentStep` | One tool call and its result within an agent run. |
|
|
409
|
+
|
|
410
|
+
### Errors
|
|
411
|
+
|
|
412
|
+
| Name | Description |
|
|
413
|
+
| -------------------------- | --------------------------------------------------------------------------- |
|
|
414
|
+
| `AiError` | Base class for all `@zerotal/ai` errors. |
|
|
415
|
+
| `AiConfigError` | Thrown at boot, or on first use, for a config combination that cannot work. |
|
|
416
|
+
| `AiRequestError` | Thrown for any other non-2xx from the provider, carrying its status. |
|
|
417
|
+
| `AiRateLimitError` | Thrown when the provider rate-limits. The SDKs already retried. |
|
|
418
|
+
| `AiSpendLimitError` | Thrown when a request would breach a configured spend ceiling. |
|
|
419
|
+
| `AiAgentLimitError` | Thrown when the agent loop hits its step or resume ceiling. |
|
|
420
|
+
| `AiCancelledError` | Thrown when the caller's `AbortSignal` fired before the call finished. |
|
|
421
|
+
| `AiDriverUnavailableError` | Thrown when a driver's optional peer package is not installed. |
|
|
422
|
+
| `UnknownAiDriverError` | Thrown for a driver name the manager does not know. |
|
|
423
|
+
|
|
424
|
+
### Configuration
|
|
425
|
+
|
|
426
|
+
| Name | Description |
|
|
427
|
+
| ----------------------- | ----------------------------------------------------------------------------- |
|
|
428
|
+
| `AiConfigInput` | What `AiConfig()` accepts — every key optional, all the way down. |
|
|
429
|
+
| `AiConfigFromEnv` | The zero-config fallback: an Anthropic driver built from `ANTHROPIC_API_KEY`. |
|
|
430
|
+
| `AiLimitsConfigShape` | Spend ceilings, enforced before the request leaves. |
|
|
431
|
+
| `AiAgentConfigShape` | How the agent loop behaves. |
|
|
432
|
+
| `AnthropicConfigShape` | Anthropic driver settings. |
|
|
433
|
+
| `OpenAiConfigShape` | OpenAI driver settings. |
|
|
434
|
+
| `OllamaConfigShape` | Ollama driver settings — a local server, so no key. |
|
|
435
|
+
| `EmbeddingsConfigShape` | Embeddings are their own block with their own driver. |
|
|
436
|
+
|
|
437
|
+
### Drivers and pricing
|
|
438
|
+
|
|
439
|
+
| Name | Description |
|
|
440
|
+
| ------------------------ | ---------------------------------------------------------------------------- |
|
|
441
|
+
| `AnthropicDriver` | The Anthropic driver. |
|
|
442
|
+
| `OpenAiDriver` | The OpenAI driver — Chat Completions over `fetch`, no SDK. |
|
|
443
|
+
| `OllamaDriver` | The Ollama driver — a local model server, so no API key and no billing. |
|
|
444
|
+
| `EmbeddingsDriver` | What an embeddings provider implements. |
|
|
445
|
+
| `OpenAiEmbeddingsDriver` | OpenAI embeddings over `fetch`. No SDK, no dependency. |
|
|
446
|
+
| `OllamaEmbeddingsDriver` | Ollama embeddings — a local server, so no key and no per-token cost. |
|
|
447
|
+
| `DriverStatus` | What `zt ai:test` prints for one driver. |
|
|
448
|
+
| `ModelPrice` | USD per million tokens. |
|
|
449
|
+
| `modelPrice` | The price for a model, or `undefined` when we have none. |
|
|
450
|
+
| `estimateCost` | Estimated USD for one request's usage. Returns 0 for an unpriced model. |
|
|
451
|
+
| `modelRejectsSampling` | Whether a Claude model rejects `temperature` / `top_p` / `top_k` with a 400. |
|
|
452
|
+
|
|
453
|
+
### Spend and statistics
|
|
454
|
+
|
|
455
|
+
| Name | Description |
|
|
456
|
+
| -------------------- | --------------------------------------------------------- |
|
|
457
|
+
| `spentToday` | USD recorded so far today, in this process. |
|
|
458
|
+
| `resetSpend` | Reset the ledger. Tests, and the `ai:spend --reset` path. |
|
|
459
|
+
| `AiDelivery` | One recorded generation. |
|
|
460
|
+
| `modelStats` | Per-model roll-up over everything still in the buffer. |
|
|
461
|
+
| `ModelStat` | Rolled-up figures for one model. |
|
|
462
|
+
| `recentGenerations` | The most recent generations, newest first. |
|
|
463
|
+
| `refusalRate` | Share of recorded calls that the provider declined, 0–1. |
|
|
464
|
+
| `resetStats` | Reset the buffer. Tests. |
|
|
465
|
+
| `CapturedGeneration` | One recorded call, as `AiFake` captures it. |
|
|
466
|
+
|
|
467
|
+
### Queued generation
|
|
468
|
+
|
|
469
|
+
| Name | Description |
|
|
470
|
+
| ---------------- | -------------------------------------------- |
|
|
471
|
+
| `AiQueueOptions` | What `Ai.queue()` needs beyond the request. |
|
|
472
|
+
| `AiQueueHandler` | What a queued generation's handler receives. |
|
|
473
|
+
|
|
474
|
+
### Structured-output schemas
|
|
475
|
+
|
|
476
|
+
| Name | Description |
|
|
477
|
+
| ----------------- | --------------------------------------------------------------------------- |
|
|
478
|
+
| `SchemaInput` | Either shape callers have on hand: the builder map, or the raw definitions. |
|
|
479
|
+
| `toSchema` | Normalise either input shape to raw definitions. |
|
|
480
|
+
| `translateSchema` | Translate a validator schema into the JSON Schema the providers accept. |
|
package/docs/application.md
CHANGED
|
@@ -73,7 +73,7 @@ helper reaches it anywhere.
|
|
|
73
73
|
> **Note** — `Application.create({ providers })` and
|
|
74
74
|
> `Application.create({ providers, config, env })` accept explicit providers and
|
|
75
75
|
> config for lightweight in-process test setups that bypass auto-discovery. See
|
|
76
|
-
> [Testing](/docs/testing
|
|
76
|
+
> [Testing](/docs/testing).
|
|
77
77
|
|
|
78
78
|
### The current application
|
|
79
79
|
|
|
@@ -159,7 +159,7 @@ Framework packages (`@zerotal/orm`, `@zerotal/auth`, `@zerotal/notifications`,
|
|
|
159
159
|
their own providers. List them in the **providers array** in
|
|
160
160
|
`bootstrap/providers.ts` and pass that array to `Application.create({ providers })`:
|
|
161
161
|
|
|
162
|
-
```typescript
|
|
162
|
+
```typescript fragment
|
|
163
163
|
// bootstrap/providers.ts
|
|
164
164
|
import { DatabaseProvider } from "@zerotal/orm";
|
|
165
165
|
import { AuthProvider } from "@zerotal/auth";
|
|
@@ -170,7 +170,7 @@ const providers = [DatabaseProvider, AuthProvider, AppServiceProvider];
|
|
|
170
170
|
export default providers;
|
|
171
171
|
```
|
|
172
172
|
|
|
173
|
-
```typescript
|
|
173
|
+
```typescript fragment
|
|
174
174
|
// bootstrap/app.ts
|
|
175
175
|
import { Application, basePath } from "zerotal";
|
|
176
176
|
import providers from "./providers.ts";
|
|
@@ -190,7 +190,7 @@ A deferred provider boots lazily — only the first time one of its container
|
|
|
190
190
|
bindings is resolved. This keeps cold-start fast when a service isn't used on
|
|
191
191
|
every request.
|
|
192
192
|
|
|
193
|
-
```typescript
|
|
193
|
+
```typescript fragment
|
|
194
194
|
// bootstrap/app.ts — defer() is called on the app instance (and returns it)
|
|
195
195
|
const app = Application.create({ providers });
|
|
196
196
|
|
|
@@ -216,7 +216,7 @@ config-driven wiring, middleware. But when you just need to register a binding o
|
|
|
216
216
|
two, that's a lot of ceremony. For those cases, `bind()` lets you register
|
|
217
217
|
straight from `bootstrap/app.ts`:
|
|
218
218
|
|
|
219
|
-
```typescript
|
|
219
|
+
```typescript fragment
|
|
220
220
|
// bootstrap/app.ts
|
|
221
221
|
import { Application, basePath } from "zerotal";
|
|
222
222
|
import { Clock, SystemClock } from "../app/services/clock.ts";
|
|
@@ -249,7 +249,7 @@ Declare where your routes live; the application loads them at boot, after every
|
|
|
249
249
|
provider has registered its middleware groups (so a route file can always
|
|
250
250
|
reference `web`, `api`, or any provider-supplied group).
|
|
251
251
|
|
|
252
|
-
```typescript
|
|
252
|
+
```typescript fragment
|
|
253
253
|
// bootstrap/app.ts
|
|
254
254
|
Application.create().routing({
|
|
255
255
|
web: "./routes/web.ts",
|
|
@@ -262,7 +262,7 @@ middleware group, `api` mounts at `/api` with the `api` group. Any other group
|
|
|
262
262
|
name must declare both `prefix` and `middleware` explicitly (or an error is thrown
|
|
263
263
|
at boot):
|
|
264
264
|
|
|
265
|
-
```typescript
|
|
265
|
+
```typescript fragment
|
|
266
266
|
// bootstrap/app.ts
|
|
267
267
|
.routing({
|
|
268
268
|
web: "./routes/web.ts",
|
|
@@ -273,7 +273,7 @@ at boot):
|
|
|
273
273
|
For directory-based routing, use `fileBasedRouting()` — same key semantics, but
|
|
274
274
|
each value is a directory that's scanned for exported HTTP-method handlers:
|
|
275
275
|
|
|
276
|
-
```typescript
|
|
276
|
+
```typescript fragment
|
|
277
277
|
// bootstrap/app.ts
|
|
278
278
|
Application.create().fileBasedRouting({ web: "./app/routes" });
|
|
279
279
|
```
|
|
@@ -285,7 +285,7 @@ root regardless of the calling file.
|
|
|
285
285
|
|
|
286
286
|
Register global middleware that runs on every request, in array order:
|
|
287
287
|
|
|
288
|
-
```typescript
|
|
288
|
+
```typescript fragment
|
|
289
289
|
// bootstrap/app.ts
|
|
290
290
|
import { CorsMiddleware, SecureHeadersMiddleware } from "zerotal";
|
|
291
291
|
|
|
@@ -296,7 +296,7 @@ The resolved pipeline runs provider-registered middleware first, then everything
|
|
|
296
296
|
you added via `.use()`. You can read the final ordering back off the app instance
|
|
297
297
|
(handy in a test or a diagnostic):
|
|
298
298
|
|
|
299
|
-
```typescript
|
|
299
|
+
```typescript fragment
|
|
300
300
|
// in a test or diagnostic
|
|
301
301
|
currentApp().globalMiddleware; // PipeClass[] in execution order
|
|
302
302
|
```
|
|
@@ -315,7 +315,7 @@ class to push it onto the global pipeline automatically. See
|
|
|
315
315
|
These are the remaining configuration methods, all called on the app instance in
|
|
316
316
|
`bootstrap/app.ts` (and all returning it, so they chain):
|
|
317
317
|
|
|
318
|
-
```typescript
|
|
318
|
+
```typescript fragment
|
|
319
319
|
// bootstrap/app.ts
|
|
320
320
|
import { Handler } from "../app/exceptions/Handler.ts";
|
|
321
321
|
|
|
@@ -360,7 +360,7 @@ provider hooks through a fixed set of phases.
|
|
|
360
360
|
5. **Started** — `Bun.serve()` is live; `onStarted()` fires. A health endpoint, a
|
|
361
361
|
PID file, and `SIGTERM`/`SIGINT`/`SIGUSR2` handlers are installed.
|
|
362
362
|
|
|
363
|
-
```typescript
|
|
363
|
+
```typescript fragment
|
|
364
364
|
// zt.ts (managed) ultimately does:
|
|
365
365
|
const app = (await import("./bootstrap/app.ts")).default;
|
|
366
366
|
await app.start(Number(env("PORT", 3000)));
|
package/docs/arch.md
CHANGED
|
@@ -24,7 +24,7 @@ bun add -d @zerotal/arch
|
|
|
24
24
|
|
|
25
25
|
Register the provider in `bootstrap/providers.ts`:
|
|
26
26
|
|
|
27
|
-
```typescript
|
|
27
|
+
```typescript fragment
|
|
28
28
|
// bootstrap/providers.ts
|
|
29
29
|
import { ArchProvider } from "@zerotal/arch";
|
|
30
30
|
|
|
@@ -189,7 +189,7 @@ The transport is exported separately from the tools, on the `@zerotal/arch/mcp`
|
|
|
189
189
|
A tool is a plain object with a schema and a `run`, so you can serve your own alongside
|
|
190
190
|
these — or serve these over a transport of your own.
|
|
191
191
|
|
|
192
|
-
```typescript
|
|
192
|
+
```typescript fragment
|
|
193
193
|
import { McpServer, serveStdio } from "@zerotal/arch/mcp";
|
|
194
194
|
import { archTools, vendoredDocsDir, spawnProbe } from "@zerotal/arch";
|
|
195
195
|
|
|
@@ -256,8 +256,30 @@ The transport is on the `@zerotal/arch/mcp` subpath: `McpServer`, `McpServerOpti
|
|
|
256
256
|
`JsonRpcRequest`, `JsonRpcResponse`, `JsonRpcSuccess`, `JsonRpcFailure`, `JsonRpcId`,
|
|
257
257
|
`RpcError`, `Meta`, `MODERN_VERSION`, `LEGACY_VERSIONS` and `SUPPORTED_VERSIONS`.
|
|
258
258
|
|
|
259
|
+
## What `stable` covers here
|
|
260
|
+
|
|
261
|
+
The usual promise — anything importable without an `@internal` marker keeps its
|
|
262
|
+
shape for the rest of the 1.x line — and one more, because this package's real
|
|
263
|
+
interface is not its exports.
|
|
264
|
+
|
|
265
|
+
**The MCP tool contract is covered.** Tool names, the arguments they accept, and
|
|
266
|
+
the shape of what they return do not change within 1.x. That is the surface an
|
|
267
|
+
agent client is configured against: a renamed tool or a dropped field breaks
|
|
268
|
+
every `.mcp.json` pointing at this server, and none of it is visible to a
|
|
269
|
+
type-level check — `archTools = (ctx) => ArchTool[]` is byte-identical whatever
|
|
270
|
+
the tools are called. `mcp-surface.md` records all nine and CI diffs it.
|
|
271
|
+
|
|
272
|
+
**The protocol revision is not.** Which version of the Model Context Protocol the
|
|
273
|
+
server speaks follows the protocol, not this package's major version. A revision
|
|
274
|
+
that requires a transport change will land in a minor release, described in the
|
|
275
|
+
notes.
|
|
276
|
+
|
|
277
|
+
**The writers behind `arch:install` are not.** `detectAgents`, `applyMcpConfig`,
|
|
278
|
+
`applyBlock`, `buildGuidelines` and the rest are `@internal`: they exist for the
|
|
279
|
+
command, and the format of the files it writes is not a promise.
|
|
280
|
+
|
|
259
281
|
## Next steps
|
|
260
282
|
|
|
261
283
|
- [Commands](/docs/commands) — the full `bun zt` vocabulary an agent is told about.
|
|
262
284
|
- [Package Development](/docs/package-development) — contributing doctor checks of your own.
|
|
263
|
-
- [Support policy](/docs/support-policy#maturity-levels) — what `
|
|
285
|
+
- [Support policy](/docs/support-policy#maturity-levels) — what `stable` promises.
|